ACloudViewer  3.9.4
A Modern Library for 3D Data Processing
cvSelectionExporter.cpp
Go to the documentation of this file.
1 // ----------------------------------------------------------------------------
2 // - CloudViewer: www.cloudViewer.org -
3 // ----------------------------------------------------------------------------
4 // Copyright (c) 2018-2024 www.cloudViewer.org
5 // SPDX-License-Identifier: MIT
6 // ----------------------------------------------------------------------------
7 
8 #include "cvSelectionExporter.h"
9 
10 // CV_CORE_LIB
11 #include <CVLog.h>
12 #include <ReferenceCloud.h>
13 
14 // CV_DB_LIB
15 #include <ecvGenericMesh.h>
16 #include <ecvMaterialSet.h>
17 #include <ecvMesh.h>
18 #include <ecvObject.h>
19 #include <ecvPointCloud.h>
20 #include <ecvScalarField.h>
21 #include <ecvSerializableObject.h>
22 
23 // CV_IO_LIB - Use existing I/O infrastructure
24 #include <AutoIO.h>
25 #include <FileIO.h>
26 #include <FileIOFilter.h>
27 
28 // PCL Utils - Use enhanced vtk2cc for conversion
29 #include <Utils/vtk2cc.h>
30 
31 // VTK
32 #include <vtkCell.h>
33 #include <vtkCellArray.h>
34 #include <vtkCellData.h>
35 #include <vtkDataArray.h>
36 #include <vtkExtractSelection.h>
37 #include <vtkFieldData.h>
38 #include <vtkFloatArray.h>
39 #include <vtkGeometryFilter.h>
40 #include <vtkIdTypeArray.h>
41 #include <vtkPointData.h>
42 #include <vtkPoints.h>
43 #include <vtkPolyData.h>
44 #include <vtkSelection.h>
45 #include <vtkSelectionNode.h>
46 #include <vtkSmartPointer.h>
47 #include <vtkTriangle.h>
48 #include <vtkUnsignedCharArray.h>
49 #include <vtkUnstructuredGrid.h>
50 
51 // Qt
52 #include <QDir>
53 #include <QFileDialog>
54 #include <QFileInfo>
55 #include <QMap>
56 #include <QSettings>
57 #include <QWidget>
58 
59 //-----------------------------------------------------------------------------
61  const cvSelectionData& selectionData,
62  const ExportOptions& options) {
63  if (!polyData) {
64  CVLog::Error("[cvSelectionExporter] polyData is nullptr");
65  return nullptr;
66  }
67 
68  if (selectionData.isEmpty()) {
69  CVLog::Error("[cvSelectionExporter] Selection is empty");
70  return nullptr;
71  }
72 
73  if (selectionData.fieldAssociation() != cvSelectionData::CELLS) {
75  "[cvSelectionExporter] Selection must be CELLS for mesh "
76  "export");
77  return nullptr;
78  }
79 
80  // Extract selected geometry
82  extractSelection(polyData, selectionData);
83  if (!extracted) {
84  CVLog::Error("[cvSelectionExporter] Failed to extract selection");
85  return nullptr;
86  }
87 
88  // Convert to ccMesh
89  QString meshName =
90  options.name.isEmpty()
91  ? QString("Selection_%1_cells").arg(selectionData.count())
92  : options.name;
93  ccMesh* mesh = vtkPolyDataToCCMesh(extracted, meshName);
94 
95  if (!mesh) {
96  return nullptr;
97  }
98 
99  CVLog::Print(QString("[cvSelectionExporter] Created mesh '%1' with %2 "
100  "triangles")
101  .arg(meshName)
102  .arg(mesh->size()));
103 
104  // Save to file if requested
105  if (options.saveToFile && !options.filename.isEmpty()) {
106  if (!saveObjectToFile(mesh, options.filename, options.writeAscii,
107  options.compressed)) {
108  CVLog::Warning(QString("[cvSelectionExporter] Failed to save mesh "
109  "to file: %1")
110  .arg(options.filename));
111  }
112  }
113  return mesh;
114 }
115 
116 //-----------------------------------------------------------------------------
118  vtkPolyData* polyData,
119  const cvSelectionData& selectionData,
120  const ExportOptions& options) {
121  if (!polyData) {
122  CVLog::Error("[cvSelectionExporter] polyData is nullptr");
123  return nullptr;
124  }
125 
126  if (selectionData.isEmpty()) {
127  CVLog::Error("[cvSelectionExporter] Selection is empty");
128  return nullptr;
129  }
130 
131  if (selectionData.fieldAssociation() != cvSelectionData::POINTS) {
132  CVLog::Error(
133  "[cvSelectionExporter] Selection must be POINTS for point "
134  "cloud export");
135  return nullptr;
136  }
137 
138  // Extract selected geometry
139  vtkSmartPointer<vtkPolyData> extracted =
140  extractSelection(polyData, selectionData);
141  if (!extracted) {
142  CVLog::Error("[cvSelectionExporter] Failed to extract selection");
143  return nullptr;
144  }
145 
146  // Convert to ccPointCloud
147  QString cloudName =
148  options.name.isEmpty()
149  ? QString("Selection_%1_points").arg(selectionData.count())
150  : options.name;
151  ccPointCloud* cloud = vtkPolyDataToCCPointCloud(extracted, cloudName);
152 
153  // Smart pointer handles cleanup automatically
154 
155  if (!cloud) {
156  return nullptr;
157  }
158 
160  QString("[cvSelectionExporter] Created point cloud '%1' "
161  "with %2 points")
162  .arg(cloudName)
163  .arg(cloud->size()));
164 
165  // Save to file if requested
166  if (options.saveToFile && !options.filename.isEmpty()) {
167  if (!saveObjectToFile(cloud, options.filename, options.writeAscii,
168  options.compressed)) {
169  CVLog::Warning(QString("[cvSelectionExporter] Failed to save point "
170  "cloud to file: %1")
171  .arg(options.filename));
172  }
173  }
174  return cloud;
175 }
176 
177 //-----------------------------------------------------------------------------
180  const cvSelectionData& selectionData,
181  const ExportOptions& options) {
182  if (!sourceCloud) {
183  CVLog::Error("[cvSelectionExporter] sourceCloud is nullptr");
184  return nullptr;
185  }
186 
187  if (selectionData.isEmpty()) {
188  CVLog::Error("[cvSelectionExporter] Selection is empty");
189  return nullptr;
190  }
191 
192  if (selectionData.fieldAssociation() != cvSelectionData::POINTS) {
193  CVLog::Error(
194  "[cvSelectionExporter] Selection must be POINTS for point "
195  "cloud export");
196  return nullptr;
197  }
198 
199  // Get selected point IDs
200  const QVector<vtkIdType>& selectedIds = selectionData.ids();
201  if (selectedIds.isEmpty()) {
202  CVLog::Error("[cvSelectionExporter] No point IDs in selection");
203  return nullptr;
204  }
205 
206  // Validate IDs and create ReferenceCloud
207  unsigned int cloudSize = sourceCloud->size();
209  if (!refCloud.reserve(static_cast<unsigned>(selectedIds.size()))) {
210  CVLog::Error(
211  "[cvSelectionExporter] Failed to reserve memory for "
212  "reference cloud");
213  return nullptr;
214  }
215 
216  int validCount = 0;
217  int invalidCount = 0;
218  for (vtkIdType id : selectedIds) {
219  if (id >= 0 && static_cast<unsigned int>(id) < cloudSize) {
220  refCloud.addPointIndex(static_cast<unsigned>(id));
221  ++validCount;
222  } else {
223  ++invalidCount;
224  }
225  }
226 
227  if (invalidCount > 0) {
229  QString("[cvSelectionExporter] Filtered %1 invalid point IDs "
230  "(out of range [0, %2))")
231  .arg(invalidCount)
232  .arg(cloudSize));
233  }
234 
235  if (validCount == 0) {
236  CVLog::Error(
237  "[cvSelectionExporter] No valid point IDs after filtering");
238  return nullptr;
239  }
240 
241  // Use partialClone to extract selected points with all attributes
242  int warnings = 0;
243  ccPointCloud* result = sourceCloud->partialClone(&refCloud, &warnings);
244 
245  if (!result) {
246  CVLog::Error("[cvSelectionExporter] partialClone failed");
247  return nullptr;
248  }
249 
250  // Log any warnings from partialClone
253  "[cvSelectionExporter] partialClone: out of memory for "
254  "colors");
255  }
258  "[cvSelectionExporter] partialClone: out of memory for "
259  "normals");
260  }
261  if (warnings & ccPointCloud::WRN_OUT_OF_MEM_FOR_SFS) {
263  "[cvSelectionExporter] partialClone: out of memory for "
264  "scalar fields");
265  }
266  if (warnings & ccPointCloud::WRN_OUT_OF_MEM_FOR_FWF) {
268  "[cvSelectionExporter] partialClone: out of memory for "
269  "full waveform data");
270  }
271 
272  // Set name
273  QString cloudName = options.name.isEmpty()
274  ? QString("Selection_%1_points").arg(validCount)
275  : options.name;
276  result->setName(cloudName);
277 
279  QString("[cvSelectionExporter] SUCCESS: Created point cloud "
280  "'%1' with %2 points, %3 scalar fields, "
281  "hasColors=%4, hasNormals=%5")
282  .arg(cloudName)
283  .arg(result->size())
284  .arg(result->getNumberOfScalarFields())
285  .arg(result->hasColors() ? "yes" : "no")
286  .arg(result->hasNormals() ? "yes" : "no"));
287 
288  // Save to file if requested
289  if (options.saveToFile && !options.filename.isEmpty()) {
290  if (!saveObjectToFile(result, options.filename, options.writeAscii,
291  options.compressed)) {
292  CVLog::Warning(QString("[cvSelectionExporter] Failed to save point "
293  "cloud to file: %1")
294  .arg(options.filename));
295  }
296  }
297 
298  return result;
299 }
300 
301 //-----------------------------------------------------------------------------
303  ccMesh* sourceMesh,
304  const cvSelectionData& selectionData,
305  const ExportOptions& options) {
306  CVLog::Print(
307  "[cvSelectionExporter::exportFromSourceMesh] START - Direct "
308  "extraction from source ccMesh using partialClone");
309 
310  if (!sourceMesh) {
311  CVLog::Error("[cvSelectionExporter] sourceMesh is nullptr");
312  return nullptr;
313  }
314 
315  if (selectionData.isEmpty()) {
316  CVLog::Error("[cvSelectionExporter] Selection is empty");
317  return nullptr;
318  }
319 
320  if (selectionData.fieldAssociation() != cvSelectionData::CELLS) {
321  CVLog::Error(
322  "[cvSelectionExporter] Selection must be CELLS for mesh "
323  "export");
324  return nullptr;
325  }
326 
327  // Get selected triangle IDs
328  const QVector<vtkIdType>& selectedIds = selectionData.ids();
329  if (selectedIds.isEmpty()) {
330  CVLog::Error("[cvSelectionExporter] No triangle IDs in selection");
331  return nullptr;
332  }
333 
334  // Convert QVector<vtkIdType> to std::vector<unsigned>
335  unsigned int meshSize = sourceMesh->size();
336  std::vector<unsigned> triangleIndices;
337  triangleIndices.reserve(selectedIds.size());
338 
339  for (vtkIdType id : selectedIds) {
340  if (id >= 0 && static_cast<unsigned int>(id) < meshSize) {
341  triangleIndices.push_back(static_cast<unsigned>(id));
342  }
343  }
344 
345  if (triangleIndices.empty()) {
346  CVLog::Error(
347  "[cvSelectionExporter] No valid triangle IDs after filtering");
348  return nullptr;
349  }
350  // Use ccMesh::partialClone to create the extracted mesh
351  // This handles all the vertex mapping, normals, materials, and texture
352  // coordinates
353  int warnings = 0;
354  ccMesh* result = sourceMesh->partialClone(triangleIndices, &warnings);
355  // Report warnings if any
356  if (warnings != 0) {
357  CVLog::Warning(QString("[cvSelectionExporter] partialClone completed "
358  "with warnings: %1")
359  .arg(warnings));
360  }
361 
362  if (!result) {
363  CVLog::Error("[cvSelectionExporter] partialClone failed");
364  return nullptr;
365  }
366 
367  // Set the mesh name
368  QString meshName =
369  options.name.isEmpty()
370  ? QString("Selection_%1_triangles").arg(result->size())
371  : options.name;
372  result->setName(meshName);
373 
374  // Copy display properties from source mesh
375  result->showColors(sourceMesh->colorsShown());
376  result->showNormals(sourceMesh->normalsShown());
377  result->showSF(sourceMesh->sfShown());
378  result->showMaterials(sourceMesh->materialsShown());
379 
381  QString("[cvSelectionExporter] SUCCESS: Created mesh '%1' "
382  "with %2 triangles, %3 vertices")
383  .arg(meshName)
384  .arg(result->size())
385  .arg(result->getAssociatedCloud()
386  ? result->getAssociatedCloud()->size()
387  : 0));
388 
389  // Save to file if requested
390  if (options.saveToFile && !options.filename.isEmpty()) {
391  if (!saveObjectToFile(result, options.filename, options.writeAscii,
392  options.compressed)) {
393  CVLog::Warning(QString("[cvSelectionExporter] Failed to save mesh "
394  "to file: %1")
395  .arg(options.filename));
396  }
397  }
398 
399  return result;
400 }
401 
402 //-----------------------------------------------------------------------------
403 bool cvSelectionExporter::exportToFile(vtkPolyData* polyData,
404  const cvSelectionData& selectionData,
405  const QString& filename,
406  bool writeAscii,
407  bool compressed) {
408  if (!polyData || selectionData.isEmpty() || filename.isEmpty()) {
409  CVLog::Error("[cvSelectionExporter] Invalid parameters");
410  return false;
411  }
412 
413  // Extract selection
414  vtkSmartPointer<vtkPolyData> extracted =
415  extractSelection(polyData, selectionData);
416  if (!extracted) {
417  CVLog::Error("[cvSelectionExporter] Failed to extract selection");
418  return false;
419  }
420 
421  // Convert to ccHObject
422  QFileInfo fileInfo(filename);
423  ccHObject* object = nullptr;
424 
425  if (selectionData.fieldAssociation() == cvSelectionData::CELLS) {
426  object = vtkPolyDataToCCMesh(extracted, fileInfo.baseName());
427  } else {
428  object = vtkPolyDataToCCPointCloud(extracted, fileInfo.baseName());
429  }
430 
431  // Smart pointer handles cleanup automatically
432 
433  if (!object) {
434  CVLog::Error("[cvSelectionExporter] Failed to convert selection");
435  return false;
436  }
437 
438  // Save using CV_io module
439  bool success = saveObjectToFile(object, filename, writeAscii, compressed);
440  delete object;
441 
442  if (success) {
443  CVLog::Print(QString("[cvSelectionExporter] Exported selection to: %1")
444  .arg(filename));
445  } else {
446  CVLog::Error(QString("[cvSelectionExporter] Failed to export to: %1")
447  .arg(filename));
448  }
449 
450  return success;
451 }
452 
453 //-----------------------------------------------------------------------------
455  vtkPolyData* polyData, const cvSelectionData& selectionData) {
456  if (!polyData || selectionData.isEmpty()) {
457  CVLog::Error("[cvSelectionExporter] extractSelection: Invalid input");
458  return nullptr;
459  }
460 
461  // Get and validate VTK array
462  vtkSmartPointer<vtkIdTypeArray> vtkArray = selectionData.vtkArray();
463  if (!vtkArray || vtkArray->GetNumberOfTuples() == 0) {
464  CVLog::Error(
465  "[cvSelectionExporter] extractSelection: Selection array is "
466  "null or empty");
467  return nullptr;
468  }
469 
470  bool isPointSelection =
471  (selectionData.fieldAssociation() == cvSelectionData::POINTS);
472 
473  // Validate selection IDs against polyData bounds
474  vtkIdType maxValidId = isPointSelection ? polyData->GetNumberOfPoints()
475  : polyData->GetNumberOfCells();
476 
477  // Filter out invalid IDs to prevent crashes
480  for (vtkIdType i = 0; i < vtkArray->GetNumberOfTuples(); ++i) {
481  vtkIdType id = vtkArray->GetValue(i);
482  if (id >= 0 && id < maxValidId) {
483  validArray->InsertNextValue(id);
484  }
485  }
486 
487  if (validArray->GetNumberOfTuples() == 0) {
488  CVLog::Error(
489  QString("[cvSelectionExporter] extractSelection: No valid IDs "
490  "(all %1 IDs are out of range [0, %2))")
491  .arg(vtkArray->GetNumberOfTuples())
492  .arg(maxValidId));
493  return nullptr;
494  }
495 
496  if (validArray->GetNumberOfTuples() < vtkArray->GetNumberOfTuples()) {
498  QString("[cvSelectionExporter] extractSelection: Filtered "
499  "%1 invalid IDs (kept %2 of %3)")
500  .arg(vtkArray->GetNumberOfTuples() -
501  validArray->GetNumberOfTuples())
502  .arg(validArray->GetNumberOfTuples())
503  .arg(vtkArray->GetNumberOfTuples()));
504  }
505 
507 
508  // For POINT selections, directly copy selected points to new polydata
509  // vtkGeometryFilter doesn't handle point-only extractions well
510  if (isPointSelection) {
511  vtkIdType numSelectedPoints = validArray->GetNumberOfTuples();
512  // Create new points array
513  vtkSmartPointer<vtkPoints> newPoints =
515  newPoints->SetNumberOfPoints(numSelectedPoints);
516 
517  // Create vertex cells for each point (so they can be rendered)
520 
521  // Copy point data arrays
522  vtkPointData* srcPointData = polyData->GetPointData();
523  vtkPointData* dstPointData = result->GetPointData();
524 
525  // Log source point data info
526  if (srcPointData) {
528  QString("[cvSelectionExporter] Source has %1 arrays, "
529  "normals=%2, scalars=%3, tcoords=%4")
530  .arg(srcPointData->GetNumberOfArrays())
531  .arg(srcPointData->GetNormals() ? "yes" : "no")
532  .arg(srcPointData->GetScalars() ? "yes" : "no")
533  .arg(srcPointData->GetTCoords() ? "yes" : "no"));
534 
535  // Log details of each array for debugging
536  for (int a = 0; a < srcPointData->GetNumberOfArrays(); ++a) {
537  vtkDataArray* arr = srcPointData->GetArray(a);
538  if (arr) {
540  QString("[cvSelectionExporter] Array[%1]: "
541  "name='%2', components=%3, tuples=%4, "
542  "type=%5")
543  .arg(a)
544  .arg(arr->GetName() ? arr->GetName()
545  : "(unnamed)")
546  .arg(arr->GetNumberOfComponents())
547  .arg(arr->GetNumberOfTuples())
548  .arg(arr->GetClassName()));
549  }
550  }
551  }
552 
553  // Copy selected points and their data
554  // CVLog::Print("[cvSelectionExporter] Copying points...");
555  for (vtkIdType i = 0; i < numSelectedPoints; ++i) {
556  vtkIdType srcId = validArray->GetValue(i);
557  double pt[3];
558  polyData->GetPoint(srcId, pt);
559  newPoints->SetPoint(i, pt);
560 
561  // Add vertex cell
562  vertices->InsertNextCell(1);
563  vertices->InsertCellPoint(i);
564  }
565 
566  // Set points first
567  result->SetPoints(newPoints);
568  result->SetVerts(vertices);
569 
570  // Now copy point data arrays AFTER setting points
571  if (srcPointData && numSelectedPoints > 0) {
572  // Copy each named array
573  for (int a = 0; a < srcPointData->GetNumberOfArrays(); ++a) {
574  vtkDataArray* srcArray = srcPointData->GetArray(a);
575  if (!srcArray) continue;
576 
577  // Skip arrays without names (coordinate arrays)
578  const char* arrName = srcArray->GetName();
579  if (!arrName || strlen(arrName) == 0) {
581  QString("[cvSelectionExporter] Skipping "
582  "unnamed array[%1]")
583  .arg(a));
584  continue;
585  }
586 
588  dstArray.TakeReference(srcArray->NewInstance());
589  dstArray->SetName(arrName);
590  dstArray->SetNumberOfComponents(
591  srcArray->GetNumberOfComponents());
592  dstArray->SetNumberOfTuples(numSelectedPoints);
593 
594  int numComp = srcArray->GetNumberOfComponents();
595 
596  // Debug: Check source array values for first few points
597  if (numSelectedPoints > 0 && numComp == 1) {
598  vtkIdType firstSrcId = validArray->GetValue(0);
599  double firstVal = srcArray->GetTuple1(firstSrcId);
600  CVLog::PrintVerbose(QString("[cvSelectionExporter] Array "
601  "'%1': srcId[0]=%2 -> value=%3")
602  .arg(arrName)
603  .arg(firstSrcId)
604  .arg(firstVal));
605  }
606 
607  // Copy data for each selected point using appropriate method
608  for (vtkIdType i = 0; i < numSelectedPoints; ++i) {
609  vtkIdType srcId = validArray->GetValue(i);
610 
611  // Bounds check
612  if (srcId < 0 || srcId >= srcArray->GetNumberOfTuples()) {
614  QString("[cvSelectionExporter] Invalid srcId "
615  "%1 for array '%2' (max=%3)")
616  .arg(srcId)
617  .arg(arrName)
618  .arg(srcArray->GetNumberOfTuples()));
619  continue;
620  }
621 
622  // Use GetTuple/SetTuple for all component types
623  double* tuple = srcArray->GetTuple(srcId);
624  dstArray->SetTuple(i, tuple);
625  }
626 
627  // Debug: Verify copied values
628  if (numSelectedPoints > 0 && numComp == 1) {
629  double copiedVal = dstArray->GetTuple1(0);
630  CVLog::PrintVerbose(QString("[cvSelectionExporter] Array "
631  "'%1': copied[0] = %2")
632  .arg(arrName)
633  .arg(copiedVal));
634  }
635 
636  dstPointData->AddArray(dstArray);
637  }
638 
639  // Set active arrays (normals, scalars, tcoords) from copied arrays
640  // The arrays were already copied in the loop above - we just need
641  // to set them as "active" in the destination point data
642 
643  // Set active normals if source has them
644  vtkDataArray* srcNormals = srcPointData->GetNormals();
645  if (srcNormals && srcNormals->GetName()) {
646  vtkDataArray* dstNormals =
647  dstPointData->GetArray(srcNormals->GetName());
648  if (dstNormals) {
649  dstPointData->SetNormals(dstNormals);
650  }
651  }
652 
653  // Set active scalars (colors) if source has them
654  vtkDataArray* srcScalars = srcPointData->GetScalars();
655  if (srcScalars && srcScalars->GetName()) {
656  vtkDataArray* dstScalars =
657  dstPointData->GetArray(srcScalars->GetName());
658  if (dstScalars) {
659  dstPointData->SetScalars(dstScalars);
660  }
661  }
662 
663  // Set active TCoords if source has them
664  vtkDataArray* srcTCoords = srcPointData->GetTCoords();
665  if (srcTCoords && srcTCoords->GetName()) {
666  vtkDataArray* dstTCoords =
667  dstPointData->GetArray(srcTCoords->GetName());
668  if (dstTCoords) {
669  dstPointData->SetTCoords(dstTCoords);
670  }
671  }
672  }
673  } else {
674  // For CELL selections, use vtkExtractSelection + vtkGeometryFilter
675  vtkSmartPointer<vtkSelectionNode> selectionNode =
677  selectionNode->SetContentType(vtkSelectionNode::INDICES);
678  selectionNode->SetFieldType(vtkSelectionNode::CELL);
679  selectionNode->SetSelectionList(validArray);
680 
683  selection->AddNode(selectionNode);
684 
687  extractor->SetInputData(0, polyData);
688  extractor->SetInputData(1, selection);
689  extractor->Update();
690 
691  vtkUnstructuredGrid* extracted =
692  vtkUnstructuredGrid::SafeDownCast(extractor->GetOutput());
693 
694  if (!extracted || extracted->GetNumberOfPoints() == 0) {
695  CVLog::Error("[cvSelectionExporter] Cell extraction failed");
696  return nullptr;
697  }
698 
700  QString("[cvSelectionExporter] Extracted %1 points, "
701  "%2 cells (cell selection)")
702  .arg(extracted->GetNumberOfPoints())
703  .arg(extracted->GetNumberOfCells()));
704 
705  // Convert to polydata
706  vtkSmartPointer<vtkGeometryFilter> geometryFilter =
708  geometryFilter->SetInputData(extracted);
709  geometryFilter->Update();
710 
711  vtkPolyData* filteredOutput = geometryFilter->GetOutput();
712  if (!filteredOutput || filteredOutput->GetNumberOfPoints() == 0) {
714  "[cvSelectionExporter] Geometry filter produced no output");
715  return nullptr;
716  }
717 
718  result->DeepCopy(filteredOutput);
719  }
720 
721  if (result->GetNumberOfPoints() == 0) {
722  CVLog::Warning("[cvSelectionExporter] Extraction produced 0 points");
723  return nullptr;
724  }
725 
726  // Copy field data (metadata like DatasetName) from source to result
727  vtkFieldData* srcFieldData = polyData->GetFieldData();
728  if (srcFieldData && srcFieldData->GetNumberOfArrays() > 0) {
729  vtkFieldData* dstFieldData = result->GetFieldData();
730  for (int i = 0; i < srcFieldData->GetNumberOfArrays(); ++i) {
731  vtkAbstractArray* arr = srcFieldData->GetAbstractArray(i);
732  if (arr) {
733  dstFieldData->AddArray(arr);
734  }
735  }
737  QString("[cvSelectionExporter] Copied %1 field data arrays")
738  .arg(srcFieldData->GetNumberOfArrays()));
739  }
740 
741  // Return raw pointer (caller must manage)
742  result->Register(nullptr);
743  return result.Get();
744 }
745 
746 //-----------------------------------------------------------------------------
747 ccMesh* cvSelectionExporter::vtkPolyDataToCCMesh(vtkPolyData* polyData,
748  const QString& name) {
749  if (!polyData) {
750  return nullptr;
751  }
752 
753  // Use enhanced vtk2cc with full ScalarField support
754  ccMesh* mesh = vtk2cc::ConvertToMesh(polyData, false);
755  if (mesh) {
756  mesh->setName(name);
757  }
758  return mesh;
759 }
760 
761 //-----------------------------------------------------------------------------
762 ccPointCloud* cvSelectionExporter::vtkPolyDataToCCPointCloud(
763  vtkPolyData* polyData, const QString& name) {
764  if (!polyData) {
765  return nullptr;
766  }
767 
768  // Use enhanced vtk2cc with full ScalarField support
769  ccPointCloud* cloud = vtk2cc::ConvertToPointCloud(polyData, false);
770  if (cloud) {
771  cloud->setName(name);
772  }
773  return cloud;
774 }
775 
776 //-----------------------------------------------------------------------------
777 bool cvSelectionExporter::saveObjectToFile(ccHObject* object,
778  const QString& filename,
779  bool writeAscii,
780  bool compressed) {
781  if (!object || filename.isEmpty()) {
782  CVLog::Error(
783  "[cvSelectionExporter] Invalid parameters for file export");
784  return false;
785  }
786 
787  // Use CV_io module for saving
788  // This supports all formats: BIN, OBJ, PLY, STL, PCD, etc.
789  std::string filenameStr = filename.toStdString();
790 
791  try {
792  // Try using AutoIO module first (supports most formats)
793  bool success = cloudViewer::io::WriteEntity(filenameStr, *object,
794  writeAscii, compressed,
795  false // print_progress
796  );
797 
798  if (success) {
799  CVLog::Print(
800  QString("[cvSelectionExporter] Successfully saved to: %1")
801  .arg(filename));
802  return true;
803  }
804 
805  // Fallback to FileIOFilter for formats not supported by AutoIO
807  params.alwaysDisplaySaveDialog = false;
808  params.parentWidget = nullptr;
809 
810  QFileInfo fileInfo(filename);
811  QString ext = fileInfo.suffix().toLower();
812 
813  // Try to find appropriate filter
814  FileIOFilter::Shared filter =
816 
817  if (!filter) {
818  CVLog::Error(QString("[cvSelectionExporter] No filter found for "
819  "extension: %1")
820  .arg(ext));
821  return false;
822  }
823 
825  FileIOFilter::SaveToFile(object, filename, params, filter);
826 
827  if (result != CC_FERR_NO_ERROR) {
829  return false;
830  }
831 
832  CVLog::Print(QString("[cvSelectionExporter] Successfully saved to: %1")
833  .arg(filename));
834  return true;
835 
836  } catch (const std::exception& e) {
837  CVLog::Error(QString("[cvSelectionExporter] Exception while saving: %1")
838  .arg(e.what()));
839  return false;
840  }
841 }
842 
843 //-----------------------------------------------------------------------------
845  bool isMesh,
846  QWidget* parent) {
847  if (!object) {
848  CVLog::Error("[cvSelectionExporter] Object is nullptr");
849  return false;
850  }
851 
852  // Load last used path from settings
853  QSettings settings;
854  settings.beginGroup("SelectionExport");
855  QString currentPath =
856  settings.value("LastPath", QDir::homePath()).toString();
857  QString defaultName = object->getName();
858 
859  // Load last used filter for this type
860  QString settingsKey = isMesh ? "LastFilterMesh" : "LastFilterCloud";
861  QString lastFilter = settings.value(settingsKey).toString();
862 
863  // Build file filters based on object type
864  QStringList fileFilters;
865  QString selectedFilter = lastFilter;
866 
867  for (const FileIOFilter::Shared& filter : FileIOFilter::GetFilters()) {
868  bool canSave = false;
869 
870  if (isMesh) {
871  bool isExclusive = true;
872  bool multiple = false;
873  canSave = filter->canSave(CV_TYPES::MESH, multiple, isExclusive);
874  } else {
875  bool isExclusive = true;
876  bool multiple = false;
877  canSave = filter->canSave(CV_TYPES::POINT_CLOUD, multiple,
878  isExclusive);
879  }
880 
881  if (canSave) {
882  // getFileFilters returns a QStringList
883  QStringList filterStrs = filter->getFileFilters(false);
884  fileFilters << filterStrs;
885 
886  // If no last filter, use the first one
887  if (selectedFilter.isEmpty() && !filterStrs.isEmpty()) {
888  selectedFilter = filterStrs.first();
889  }
890  }
891  }
892 
893  if (fileFilters.isEmpty()) {
894  CVLog::Error("[cvSelectionExporter] No suitable file filter found");
895  settings.endGroup();
896  return false;
897  }
898 
899  // Show file save dialog
900  QString fullPath = currentPath + "/" + defaultName;
901  QString selectedFilename = QFileDialog::getSaveFileName(
902  parent, QObject::tr("Export Selection to File"), fullPath,
903  fileFilters.join(";;"), &selectedFilter);
904 
905  if (selectedFilename.isEmpty()) {
906  settings.endGroup();
907  return false; // User cancelled
908  }
909 
910  // Save using FileIOFilter
911  FileIOFilter::SaveParameters parameters;
912  parameters.alwaysDisplaySaveDialog = true;
913  parameters.parentWidget = parent;
914 
915  CC_FILE_ERROR result = FileIOFilter::SaveToFile(object, selectedFilename,
916  parameters, selectedFilter);
917 
918  if (result != CC_FERR_NO_ERROR) {
919  CVLog::Error(QString("[cvSelectionExporter] Failed to save file: %1")
920  .arg(selectedFilename));
921  settings.endGroup();
922  return false;
923  }
924 
925  // Save settings for next time
926  currentPath = QFileInfo(selectedFilename).absolutePath();
927  settings.setValue("LastPath", currentPath);
928  settings.setValue(settingsKey, selectedFilter);
929  settings.endGroup();
930 
931  CVLog::Print(QString("[cvSelectionExporter] Selection exported to: %1")
932  .arg(selectedFilename));
933 
934  return true;
935 }
936 
937 //=============================================================================
938 // Batch Export Implementation (merged from cvSelectionExporterBatch.cpp)
939 //=============================================================================
940 
941 //-----------------------------------------------------------------------------
943  vtkPolyData* polyData,
944  const QList<cvSelectionData>& selections,
945  const QString& baseName) {
946  QList<ccMesh*> meshes;
947 
948  if (!polyData || selections.isEmpty()) {
949  CVLog::Error(
950  "[cvSelectionExporter] Invalid parameters for batch export");
951  return meshes;
952  }
953 
954  int index = 1;
955  for (const cvSelectionData& selection : selections) {
956  if (selection.isEmpty()) {
958  QString("[cvSelectionExporter] Skipping empty selection %1")
959  .arg(index));
960  ++index;
961  continue;
962  }
963 
964  if (selection.fieldAssociation() != cvSelectionData::CELLS) {
965  CVLog::Warning(QString("[cvSelectionExporter] Skipping non-cell "
966  "selection %1")
967  .arg(index));
968  ++index;
969  continue;
970  }
971 
972  QString name =
973  QString("%1_%2").arg(baseName).arg(index, 3, 10, QChar('0'));
975  opts.name = name;
976  ccMesh* mesh = exportToMesh(polyData, selection, opts);
977 
978  if (mesh) {
979  meshes.append(mesh);
980  CVLog::Print(QString("[cvSelectionExporter] Batch exported mesh "
981  "%1/%2: %3")
982  .arg(index)
983  .arg(selections.size())
984  .arg(name));
985  } else {
986  CVLog::Error(
987  QString("[cvSelectionExporter] Failed to export mesh %1")
988  .arg(index));
989  }
990 
991  ++index;
992  }
993 
994  CVLog::Print(
995  QString("[cvSelectionExporter] Batch export complete: %1/%2 meshes")
996  .arg(meshes.size())
997  .arg(selections.size()));
998 
999  return meshes;
1000 }
1001 
1002 //-----------------------------------------------------------------------------
1004  vtkPolyData* polyData,
1005  const QList<cvSelectionData>& selections,
1006  const QString& baseName) {
1007  QList<ccPointCloud*> clouds;
1008 
1009  if (!polyData || selections.isEmpty()) {
1010  CVLog::Error(
1011  "[cvSelectionExporter] Invalid parameters for batch export");
1012  return clouds;
1013  }
1014 
1015  int index = 1;
1016  for (const cvSelectionData& selection : selections) {
1017  if (selection.isEmpty()) {
1019  QString("[cvSelectionExporter] Skipping empty selection %1")
1020  .arg(index));
1021  ++index;
1022  continue;
1023  }
1024 
1025  if (selection.fieldAssociation() != cvSelectionData::POINTS) {
1026  CVLog::Warning(QString("[cvSelectionExporter] Skipping non-point "
1027  "selection %1")
1028  .arg(index));
1029  ++index;
1030  continue;
1031  }
1032 
1033  QString name =
1034  QString("%1_%2").arg(baseName).arg(index, 3, 10, QChar('0'));
1036  opts.name = name;
1037  ccPointCloud* cloud = exportToPointCloud(polyData, selection, opts);
1038 
1039  if (cloud) {
1040  clouds.append(cloud);
1041  CVLog::Print(QString("[cvSelectionExporter] Batch exported cloud "
1042  "%1/%2: %3")
1043  .arg(index)
1044  .arg(selections.size())
1045  .arg(name));
1046  } else {
1047  CVLog::Error(
1048  QString("[cvSelectionExporter] Failed to export cloud %1")
1049  .arg(index));
1050  }
1051 
1052  ++index;
1053  }
1054 
1055  CVLog::Print(
1056  QString("[cvSelectionExporter] Batch export complete: %1/%2 clouds")
1057  .arg(clouds.size())
1058  .arg(selections.size()));
1059 
1060  return clouds;
1061 }
1062 
1063 //-----------------------------------------------------------------------------
1065  vtkPolyData* polyData,
1066  const QList<cvSelectionData>& selections,
1067  const QString& outputDir,
1068  const QString& format,
1069  const QString& baseName,
1070  std::function<void(int)> progressCallback) {
1071  if (!polyData || selections.isEmpty() || outputDir.isEmpty()) {
1072  CVLog::Error(
1073  "[cvSelectionExporter] Invalid parameters for batch export to "
1074  "files");
1075  return 0;
1076  }
1077 
1078  // Create output directory if it doesn't exist
1079  QDir dir;
1080  if (!dir.exists(outputDir)) {
1081  if (!dir.mkpath(outputDir)) {
1082  CVLog::Error(QString("[cvSelectionExporter] Failed to create "
1083  "output directory: %1")
1084  .arg(outputDir));
1085  return 0;
1086  }
1087  }
1088 
1089  int successCount = 0;
1090  int totalCount = selections.size();
1091 
1092  for (int i = 0; i < totalCount; ++i) {
1093  const cvSelectionData& selection = selections[i];
1094  int index = i + 1;
1095 
1096  if (selection.isEmpty()) {
1098  QString("[cvSelectionExporter] Skipping empty selection %1")
1099  .arg(index));
1100  if (progressCallback) {
1101  progressCallback((index * 100) / totalCount);
1102  }
1103  continue;
1104  }
1105 
1106  // Build filename
1107  QString filename = QString("%1/%2_%3.%4")
1108  .arg(outputDir)
1109  .arg(baseName)
1110  .arg(index, 3, 10, QChar('0'))
1111  .arg(format.toLower());
1112 
1113  // Export directly to file
1114  bool success = cvSelectionExporter::exportToFile(
1115  polyData, selection, filename, false, false);
1116 
1117  if (success) {
1118  ++successCount;
1119  CVLog::Print(QString("[cvSelectionExporter] Exported %1/%2: %3")
1120  .arg(index)
1121  .arg(totalCount)
1122  .arg(filename));
1123  } else {
1124  CVLog::Error(QString("[cvSelectionExporter] Failed to export %1/%2")
1125  .arg(index)
1126  .arg(totalCount));
1127  }
1128 
1129  // Progress callback
1130  if (progressCallback) {
1131  progressCallback((index * 100) / totalCount);
1132  }
1133  }
1134 
1135  CVLog::Print(QString("[cvSelectionExporter] Batch export complete: %1/%2 "
1136  "files exported to %3")
1137  .arg(successCount)
1138  .arg(totalCount)
1139  .arg(outputDir));
1140 
1141  return successCount;
1142 }
1143 
1144 //-----------------------------------------------------------------------------
1145 bool cvSelectionExporter::exportNumbered(vtkPolyData* polyData,
1146  const cvSelectionData& selection,
1147  const QString& outputPath,
1148  int number) {
1149  if (!polyData || selection.isEmpty() || outputPath.isEmpty()) {
1150  return false;
1151  }
1152 
1153  // Replace %1 with number
1154  QString filename = outputPath.arg(number, 3, 10, QChar('0'));
1155 
1156  // Determine format from extension
1157  QFileInfo fileInfo(filename);
1158  QString format = fileInfo.suffix().toLower();
1159 
1160  // Export directly to file
1161  return cvSelectionExporter::exportToFile(polyData, selection, filename,
1162  false, false);
1163 }
Compressed compressed
std::string filename
filament::Texture::InternalFormat format
std::string name
CC_FILE_ERROR
Typical I/O filter errors.
Definition: FileIOFilter.h:20
@ CC_FERR_NO_ERROR
Definition: FileIOFilter.h:21
core::Tensor result
Definition: VtkUtils.cpp:76
static bool Warning(const char *format,...)
Prints out a formatted warning message in console.
Definition: CVLog.cpp:133
static bool Print(const char *format,...)
Prints out a formatted message in console.
Definition: CVLog.cpp:113
static bool PrintVerbose(const char *format,...)
Prints out a verbose formatted message in console.
Definition: CVLog.cpp:103
static bool Error(const char *format,...)
Display an error dialog with formatted message.
Definition: CVLog.cpp:143
static CC_FILE_ERROR SaveToFile(ccHObject *entities, const QString &filename, const SaveParameters &parameters, Shared filter)
static const FilterContainer & GetFilters()
Returns the set of all registered filters.
static Shared FindBestFilterForExtension(const QString &ext)
Returns the best filter (presumably) to open a given file extension.
QSharedPointer< FileIOFilter > Shared
Shared type.
Definition: FileIOFilter.h:97
static void DisplayErrorMessage(CC_FILE_ERROR err, const QString &action, const QString &filename)
Displays (to console) the message corresponding to a given error code.
virtual bool colorsShown() const
Returns whether colors are shown or not.
virtual bool sfShown() const
Returns whether active scalar field is visible.
virtual bool materialsShown() const
Sets whether textures/material should be displayed or not.
Hierarchical CLOUDVIEWER Object.
Definition: ecvHObject.h:25
Triangular mesh.
Definition: ecvMesh.h:35
bool normalsShown() const override
Returns whether normals are shown or not.
Definition: ecvMesh.cpp:388
virtual unsigned size() const override
Returns the number of triangles.
Definition: ecvMesh.cpp:2143
ccMesh * partialClone(const std::vector< unsigned > &triangleIndices, int *warnings=nullptr) const
Creates a new mesh from a selection of triangles (partial clone)
Definition: ecvMesh.cpp:1347
virtual void setName(const QString &name)
Sets object name.
Definition: ecvObject.h:75
A 3D cloud and its associated features (color, normals, scalar fields, etc.)
@ WRN_OUT_OF_MEM_FOR_COLORS
@ WRN_OUT_OF_MEM_FOR_NORMALS
virtual unsigned size() const =0
Returns the number of points.
unsigned size() const override
Definition: PointCloudTpl.h:38
A very simple point cloud (no point duplication)
virtual bool addPointIndex(unsigned globalIndex)
Point global index insertion mechanism.
virtual bool reserve(unsigned n)
Reserves some memory for hosting the point references.
Encapsulates selection data without exposing VTK types.
FieldAssociation fieldAssociation() const
Get field association.
vtkSmartPointer< vtkIdTypeArray > vtkArray() const
Get the underlying VTK array (for internal use only)
@ CELLS
Selection applies to cells.
@ POINTS
Selection applies to points.
bool isEmpty() const
Check if selection is empty.
QVector< qint64 > ids() const
Get selected IDs as a vector (copy)
int count() const
Get number of selected items.
static bool exportToFile(vtkPolyData *polyData, const cvSelectionData &selectionData, const QString &filename, bool writeAscii=false, bool compressed=false)
Export selection to file (uses CV_io module)
static bool exportNumbered(vtkPolyData *polyData, const cvSelectionData &selection, const QString &outputPath, int number)
Export selection with numbered naming.
static ccPointCloud * exportToPointCloud(vtkPolyData *polyData, const cvSelectionData &selectionData, const ExportOptions &options=ExportOptions())
Export selected points to ccPointCloud.
static QList< ccMesh * > batchExportToMeshes(vtkPolyData *polyData, const QList< cvSelectionData > &selections, const QString &baseName="Selection")
Export multiple selections to meshes.
static QList< ccPointCloud * > batchExportToPointClouds(vtkPolyData *polyData, const QList< cvSelectionData > &selections, const QString &baseName="Selection")
Export multiple selections to point clouds.
static ccMesh * exportFromSourceMesh(ccMesh *sourceMesh, const cvSelectionData &selectionData, const ExportOptions &options=ExportOptions())
Export selected cells directly from source ccMesh.
static ccMesh * exportToMesh(vtkPolyData *polyData, const cvSelectionData &selectionData, const ExportOptions &options=ExportOptions())
Export selected cells to ccMesh.
static int batchExportToFiles(vtkPolyData *polyData, const QList< cvSelectionData > &selections, const QString &outputDir, const QString &format, const QString &baseName="selection", std::function< void(int)> progressCallback=nullptr)
Export multiple selections to files.
static vtkPolyData * extractSelection(vtkPolyData *polyData, const cvSelectionData &selectionData)
Extract selected geometry as new vtkPolyData.
static ccPointCloud * exportFromSourceCloud(ccPointCloud *sourceCloud, const cvSelectionData &selectionData, const ExportOptions &options=ExportOptions())
Export selected points directly from source ccPointCloud.
static bool saveObjectToFileWithDialog(ccHObject *object, bool isMesh, QWidget *parent=nullptr)
Save a ccHObject to file with file dialog Uses QFileDialog to let user choose filename and format Rem...
static ccMesh * ConvertToMesh(vtkPolyData *polydata, bool silent=false)
Definition: vtk2cc.cpp:290
static ccPointCloud * ConvertToPointCloud(vtkPolyData *polydata, bool silent=false)
Definition: vtk2cc.cpp:53
a[190]
const double * e
@ MESH
Definition: CVTypes.h:105
@ POINT_CLOUD
Definition: CVTypes.h:104
bool WriteEntity(const std::string &filename, const ccHObject &obj, bool write_ascii=false, bool compressed=false, bool print_progress=false)
Definition: AutoIO.cpp:373
ccGenericPointCloud * sourceCloud
Generic saving parameters.
Definition: FileIOFilter.h:84
QWidget * parentWidget
Parent widget (if any)
Definition: FileIOFilter.h:93
Options for exporting selections.