ACloudViewer  3.9.4
A Modern Library for 3D Data Processing
cvSelectionAlgebra.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 "cvSelectionAlgebra.h"
9 
10 // CV_CORE_LIB
11 #include <CVLog.h>
12 
13 // VTK
14 #include <vtkCell.h>
15 #include <vtkCellData.h>
16 #include <vtkDataArray.h>
17 #include <vtkFieldData.h>
18 #include <vtkIdList.h>
19 #include <vtkKdTreePointLocator.h>
20 #include <vtkPointData.h>
21 #include <vtkPolyData.h>
22 #include <vtkSignedCharArray.h>
23 #include <vtkSmartPointer.h>
24 
25 // Qt
26 #include <QSet>
27 
28 // Qt5/Qt6 Compatibility
29 #include <QtCompat.h>
30 
31 //-----------------------------------------------------------------------------
32 cvSelectionAlgebra::cvSelectionAlgebra(QObject* parent) : QObject(parent) {
33  CVLog::PrintVerbose("[cvSelectionAlgebra] Initialized");
34 }
35 
36 //-----------------------------------------------------------------------------
38  const cvSelectionData& b) {
39  if (a.isEmpty()) return b;
40  if (b.isEmpty()) return a;
41 
42  if (!areCompatible(a, b)) {
43  CVLog::Error("[cvSelectionAlgebra] Incompatible selections for union");
44  return cvSelectionData();
45  }
46 
47  // Union of IDs (remove duplicates)
48  QSet<qint64> setA = qSetFromVector(a.ids());
49  QSet<qint64> setB = qSetFromVector(b.ids());
50  QSet<qint64> result = setA.unite(setB);
51 
52  QVector<qint64> resultIds = qVectorFromSet(result);
53 
54  CVLog::Print(QString("[cvSelectionAlgebra] Union: %1 U %2 = %3")
55  .arg(a.count())
56  .arg(b.count())
57  .arg(resultIds.size()));
58 
59  return cvSelectionData(resultIds, a.fieldAssociation());
60 }
61 
62 //-----------------------------------------------------------------------------
64  const cvSelectionData& b) {
65  if (a.isEmpty() || b.isEmpty()) {
66  return cvSelectionData();
67  }
68 
69  if (!areCompatible(a, b)) {
71  "[cvSelectionAlgebra] Incompatible selections for "
72  "intersection");
73  return cvSelectionData();
74  }
75 
76  // Intersection of IDs
77  QSet<qint64> setA = qSetFromVector(a.ids());
78  QSet<qint64> setB = qSetFromVector(b.ids());
79  QSet<qint64> result = setA.intersect(setB);
80 
81  QVector<qint64> resultIds = qVectorFromSet(result);
82 
83  CVLog::Print(QString("[cvSelectionAlgebra] Intersection: %1 & %2 = %3")
84  .arg(a.count())
85  .arg(b.count())
86  .arg(resultIds.size()));
87 
88  return cvSelectionData(resultIds, a.fieldAssociation());
89 }
90 
91 //-----------------------------------------------------------------------------
93  const cvSelectionData& b) {
94  if (a.isEmpty()) {
95  return cvSelectionData();
96  }
97 
98  if (b.isEmpty()) {
99  return a;
100  }
101 
102  if (!areCompatible(a, b)) {
103  CVLog::Error(
104  "[cvSelectionAlgebra] Incompatible selections for difference");
105  return cvSelectionData();
106  }
107 
108  // Difference: elements in A but not in B
109  QSet<qint64> setA = qSetFromVector(a.ids());
110  QSet<qint64> setB = qSetFromVector(b.ids());
111  QSet<qint64> result = setA.subtract(setB);
112 
113  QVector<qint64> resultIds = qVectorFromSet(result);
114 
115  CVLog::Print(QString("[cvSelectionAlgebra] Difference: %1 - %2 = %3")
116  .arg(a.count())
117  .arg(b.count())
118  .arg(resultIds.size()));
119 
120  return cvSelectionData(resultIds, a.fieldAssociation());
121 }
122 
123 //-----------------------------------------------------------------------------
125  const cvSelectionData& a, const cvSelectionData& b) {
126  if (a.isEmpty() && b.isEmpty()) {
127  return cvSelectionData();
128  }
129 
130  if (!areCompatible(a, b)) {
131  CVLog::Error(
132  "[cvSelectionAlgebra] Incompatible selections for symmetric "
133  "difference");
134  return cvSelectionData();
135  }
136 
137  // Symmetric difference: (A - B) U (B - A)
138  // Or equivalently: (A U B) - (A & B)
139  QSet<qint64> setA = qSetFromVector(a.ids());
140  QSet<qint64> setB = qSetFromVector(b.ids());
141 
142  // Elements in A but not B
143  QSet<qint64> aMinusB = setA;
144  aMinusB.subtract(setB);
145 
146  // Elements in B but not A
147  QSet<qint64> bMinusA = setB;
148  bMinusA.subtract(setA);
149 
150  // Union of both
151  QSet<qint64> result = aMinusB.unite(bMinusA);
152 
153  QVector<qint64> resultIds = qVectorFromSet(result);
154 
155  CVLog::Print(
156  QString("[cvSelectionAlgebra] Symmetric Difference: %1 ^ %2 = %3")
157  .arg(a.count())
158  .arg(b.count())
159  .arg(resultIds.size()));
160 
161  return cvSelectionData(resultIds, a.fieldAssociation());
162 }
163 
164 //-----------------------------------------------------------------------------
166  const cvSelectionData& input) {
167  if (!polyData) {
168  CVLog::Error("[cvSelectionAlgebra] polyData is nullptr for complement");
169  return cvSelectionData();
170  }
171 
172  if (input.isEmpty()) {
173  // Return all elements
174  vtkIdType totalCount =
176  ? polyData->GetNumberOfPoints()
177  : polyData->GetNumberOfCells();
178 
179  QVector<qint64> allIds;
180  for (vtkIdType i = 0; i < totalCount; ++i) {
181  allIds.append(i);
182  }
183 
184  CVLog::Print(QString("[cvSelectionAlgebra] Complement: 0 -> %1 (all)")
185  .arg(totalCount));
186 
187  return cvSelectionData(allIds, input.fieldAssociation());
188  }
189 
190  QSet<qint64> selectedSet = qSetFromVector(input.ids());
191  QVector<qint64> complementIds;
192 
193  vtkIdType totalCount = (input.fieldAssociation() == cvSelectionData::POINTS)
194  ? polyData->GetNumberOfPoints()
195  : polyData->GetNumberOfCells();
196 
197  for (vtkIdType i = 0; i < totalCount; ++i) {
198  if (!selectedSet.contains(i)) {
199  complementIds.append(i);
200  }
201  }
202 
203  CVLog::Print(QString("[cvSelectionAlgebra] Complement: ~%1 = %2")
204  .arg(input.count())
205  .arg(complementIds.size()));
206 
207  return cvSelectionData(complementIds, input.fieldAssociation());
208 }
209 
210 //-----------------------------------------------------------------------------
212  const cvSelectionData& a,
213  const cvSelectionData& b,
214  vtkPolyData* polyData) {
215  switch (op) {
216  case Operation::UNION:
217  return unionOf(a, b);
219  return intersectionOf(a, b);
221  return differenceOf(a, b);
223  return symmetricDifferenceOf(a, b);
225  if (!polyData) {
226  CVLog::Error(
227  "[cvSelectionAlgebra] polyData required for "
228  "complement");
229  return cvSelectionData();
230  }
231  return complementOf(polyData, a);
232  default:
233  CVLog::Error(QString("[cvSelectionAlgebra] Unknown operation: %1")
234  .arg(static_cast<int>(op)));
235  return cvSelectionData();
236  }
237 }
238 
239 //-----------------------------------------------------------------------------
241  vtkPolyData* polyData,
242  const cvSelectionData& input,
243  int layers,
244  bool removeSeed,
245  bool removeIntermediateLayers) {
246  if (!polyData || input.isEmpty()) {
247  return cvSelectionData();
248  }
249 
250  if (input.fieldAssociation() != cvSelectionData::CELLS) {
252  "[cvSelectionAlgebra] Grow only works with cell selection");
253  return input;
254  }
255 
256  // Store the original seed for removeSeed option
257  QSet<qint64> idsSet = qSetFromVector(input.ids());
258  QSet<vtkIdType> seedSet;
259  for (qint64 id : idsSet) {
260  seedSet.insert(static_cast<vtkIdType>(id));
261  }
262  QSet<vtkIdType> currentSet = seedSet;
263  QSet<vtkIdType> previousLayerSet = seedSet;
264 
265  // Grow layer by layer
266  for (int iter = 0; iter < layers; ++iter) {
267  QSet<vtkIdType> newSet = currentSet;
268 
269  // For each selected cell, add its neighbors
270  for (vtkIdType cellId : currentSet) {
271  QSet<vtkIdType> neighbors = getCellNeighbors(polyData, cellId);
272  newSet.unite(neighbors);
273  }
274 
275  // Track the previous layer for removeIntermediateLayers
276  previousLayerSet = currentSet;
277  currentSet = newSet;
278  }
279 
280  // ParaView-style: Apply removal options
281  QSet<vtkIdType> resultSet = currentSet;
282 
283  if (removeIntermediateLayers) {
284  // Keep only the outermost layer (elements added in the last iteration)
285  // Outermost = currentSet - previousLayerSet
286  resultSet = currentSet;
287  resultSet.subtract(previousLayerSet);
289  QString("[cvSelectionAlgebra] Removed intermediate layers, "
290  "keeping outermost: %1 cells")
291  .arg(resultSet.size()));
292  }
293 
294  if (removeSeed) {
295  // Remove the original seed elements
296  resultSet.subtract(seedSet);
298  QString("[cvSelectionAlgebra] Removed seed, result: %1 cells")
299  .arg(resultSet.size()));
300  }
301 
302  QVector<qint64> resultIds;
303  for (vtkIdType id : resultSet) {
304  resultIds.append(id);
305  }
306 
308  QString("[cvSelectionAlgebra] Grow %1 layers (removeSeed=%2, "
309  "removeIntermediate=%3): %4 -> %5 cells")
310  .arg(layers)
311  .arg(removeSeed)
312  .arg(removeIntermediateLayers)
313  .arg(input.count())
314  .arg(resultIds.size()));
315 
316  return cvSelectionData(resultIds, cvSelectionData::CELLS);
317 }
318 
319 //-----------------------------------------------------------------------------
321  vtkPolyData* polyData, const cvSelectionData& input, int iterations) {
322  if (!polyData || input.isEmpty()) {
323  return cvSelectionData();
324  }
325 
326  if (input.fieldAssociation() != cvSelectionData::CELLS) {
328  "[cvSelectionAlgebra] Shrink only works with cell selection");
329  return input;
330  }
331 
332  QSet<qint64> idsSet = qSetFromVector(input.ids());
333  QSet<vtkIdType> currentSet;
334  for (qint64 id : idsSet) {
335  currentSet.insert(static_cast<vtkIdType>(id));
336  }
337 
338  for (int iter = 0; iter < iterations; ++iter) {
339  QSet<vtkIdType> newSet;
340 
341  // Only keep cells that are not on the boundary
342  for (vtkIdType cellId : currentSet) {
343  if (!isBoundaryCell(polyData, cellId, currentSet)) {
344  newSet.insert(cellId);
345  }
346  }
347 
348  currentSet = newSet;
349 
350  if (currentSet.isEmpty()) {
351  break;
352  }
353  }
354 
355  QVector<qint64> resultIds;
356  for (vtkIdType id : currentSet) {
357  resultIds.append(id);
358  }
359 
361  QString("[cvSelectionAlgebra] Shrink %1 iterations: %2 -> %3 cells")
362  .arg(iterations)
363  .arg(input.count())
364  .arg(resultIds.size()));
365 
366  return cvSelectionData(resultIds, cvSelectionData::CELLS);
367 }
368 
369 //-----------------------------------------------------------------------------
371  vtkPolyData* polyData, const cvSelectionData& input) {
372  if (!polyData || input.isEmpty()) {
373  return cvSelectionData();
374  }
375 
376  if (input.fieldAssociation() != cvSelectionData::CELLS) {
378  "[cvSelectionAlgebra] Boundary extraction only works with cell "
379  "selection");
380  return input;
381  }
382 
383  QSet<qint64> idsSet = qSetFromVector(input.ids());
384  QSet<vtkIdType> selectedSet;
385  for (qint64 id : idsSet) {
386  selectedSet.insert(static_cast<vtkIdType>(id));
387  }
388  QVector<qint64> boundaryIds;
389 
390  for (vtkIdType cellId : selectedSet) {
391  if (isBoundaryCell(polyData, cellId, selectedSet)) {
392  boundaryIds.append(cellId);
393  }
394  }
395 
397  QString("[cvSelectionAlgebra] Boundary extraction: %1 -> %2 cells")
398  .arg(input.count())
399  .arg(boundaryIds.size()));
400 
401  return cvSelectionData(boundaryIds, cvSelectionData::CELLS);
402 }
403 
404 //-----------------------------------------------------------------------------
406  const cvSelectionData& b) {
407  if (a.isEmpty() || b.isEmpty()) {
408  return true; // Empty selections are compatible with anything
409  }
410 
411  return a.fieldAssociation() == b.fieldAssociation();
412 }
413 
414 //-----------------------------------------------------------------------------
415 // Private helper methods
416 //-----------------------------------------------------------------------------
417 
418 QSet<vtkIdType> cvSelectionAlgebra::getCellNeighbors(vtkPolyData* polyData,
419  vtkIdType cellId) {
420  QSet<vtkIdType> neighbors;
421 
422  vtkCell* cell = polyData->GetCell(cellId);
423  if (!cell) return neighbors;
424 
425  vtkIdType npts = cell->GetNumberOfPoints();
426 
427  // For each edge, find neighbor cells
428  for (vtkIdType i = 0; i < npts; ++i) {
429  vtkIdType p1 = cell->GetPointId(i);
430  vtkIdType p2 = cell->GetPointId((i + 1) % npts);
431 
433  polyData->GetCellEdgeNeighbors(cellId, p1, p2, cellIds);
434 
435  for (vtkIdType j = 0; j < cellIds->GetNumberOfIds(); ++j) {
436  neighbors.insert(cellIds->GetId(j));
437  }
438  }
439 
440  return neighbors;
441 }
442 
443 //-----------------------------------------------------------------------------
444 bool cvSelectionAlgebra::isBoundaryCell(vtkPolyData* polyData,
445  vtkIdType cellId,
446  const QSet<vtkIdType>& selectedSet) {
447  QSet<vtkIdType> neighbors = getCellNeighbors(polyData, cellId);
448 
449  // A cell is on the boundary if at least one of its neighbors is not
450  // selected
451  for (vtkIdType neighborId : neighbors) {
452  if (!selectedSet.contains(neighborId)) {
453  return true;
454  }
455  }
456 
457  // Also boundary if has no neighbors (isolated cell)
458  return neighbors.isEmpty();
459 }
460 
461 //-----------------------------------------------------------------------------
463  vtkPolyData* polyData,
464  const cvSelectionData& input,
465  int layers,
466  bool removeSeed,
467  bool removeIntermediateLayers) {
468  // ParaView-compatible expand selection API
469  // Reference: vtkSMSelectionHelper::ExpandSelection
470  // Reference: vtkSelector.cxx line 139-148
471 
472  if (!polyData || input.isEmpty()) {
473  return cvSelectionData();
474  }
475 
476  if (layers == 0) {
477  return input; // No expansion needed
478  }
479 
480  // Determine field association
481  bool isPointSelection =
483 
484  // Handle grow (layers > 0) and shrink (layers < 0) separately
485  if (layers > 0) {
486  QSet<qint64> currentSet;
487  // Convert input ids to QSet
488  for (qint64 id : input.ids()) {
489  currentSet.insert(id);
490  }
491 
492  QSet<qint64> seedSet =
493  currentSet; // Save original selection for removeSeed
494 
495  // Iteratively expand
496  for (int iter = 0; iter < layers; ++iter) {
497  QSet<qint64> newElements;
498 
499  // Find all neighbors of current selection
500  for (qint64 id : currentSet) {
501  QSet<vtkIdType> neighbors;
502  if (isPointSelection) {
503  neighbors = getPointNeighbors(polyData,
504  static_cast<vtkIdType>(id));
505  } else {
506  neighbors = getCellNeighbors(polyData,
507  static_cast<vtkIdType>(id));
508  }
509 
510  // Add neighbors that are not already in the set
511  for (vtkIdType neighborId : neighbors) {
512  if (!currentSet.contains(static_cast<qint64>(neighborId))) {
513  newElements.insert(static_cast<qint64>(neighborId));
514  }
515  }
516  }
517 
518  // Add new elements to current set
519  if (newElements.isEmpty()) {
520  CVLog::PrintVerbose(QString("[cvSelectionAlgebra] Grow stopped "
521  "at iteration %1: no new neighbors")
522  .arg(iter + 1));
523  break;
524  }
525 
526  currentSet.unite(newElements);
527  }
528 
529  // Apply removeSeed if requested
530  if (removeSeed) {
531  currentSet.subtract(seedSet);
532  }
533 
534  // Apply removeIntermediateLayers if requested
535  // This keeps only the outermost layer
536  if (removeIntermediateLayers && layers > 1) {
537  // We need to keep only elements that were added in the last
538  // iteration This requires re-doing the expansion layer by layer
539  QSet<qint64> layerSet = seedSet;
540 
541  for (int iter = 0; iter < layers; ++iter) {
542  QSet<qint64> nextLayerSet;
543 
544  // Find neighbors of current layer
545  for (qint64 id : layerSet) {
546  QSet<vtkIdType> neighbors;
547  if (isPointSelection) {
548  neighbors = getPointNeighbors(
549  polyData, static_cast<vtkIdType>(id));
550  } else {
551  neighbors = getCellNeighbors(
552  polyData, static_cast<vtkIdType>(id));
553  }
554 
555  for (vtkIdType neighborId : neighbors) {
556  if (!currentSet.contains(
557  static_cast<qint64>(neighborId)) ||
558  layerSet.contains(
559  static_cast<qint64>(neighborId))) {
560  continue;
561  }
562  nextLayerSet.insert(static_cast<qint64>(neighborId));
563  }
564  }
565 
566  if (iter == layers - 1) {
567  // This is the final layer
568  currentSet = nextLayerSet;
569  break;
570  }
571 
572  layerSet = nextLayerSet;
573  if (layerSet.isEmpty()) {
574  break;
575  }
576  }
577  }
578 
579  // Convert back to QVector
580  QVector<qint64> expandedIds;
581  for (qint64 id : currentSet) {
582  expandedIds.append(id);
583  }
584 
586  QString("[cvSelectionAlgebra] Grow: %1 layers, %2 -> %3 "
587  "%4 (removeSeed=%5, removeIntermediate=%6)")
588  .arg(layers)
589  .arg(input.ids().size())
590  .arg(expandedIds.size())
591  .arg(isPointSelection ? "points" : "cells")
592  .arg(removeSeed)
593  .arg(removeIntermediateLayers));
594 
595  // Create result selection data
596  cvSelectionData resultSelection(expandedIds, input.fieldAssociation());
597 
598  // Copy actor info if available
599  if (input.hasActorInfo()) {
600  resultSelection.setActorInfo(input.primaryActor(),
601  input.primaryPolyData());
602  }
603 
604  return resultSelection;
605  } else {
606  // SHRINK (layers < 0): Remove boundary layers iteratively
607  // ParaView's shrink is conceptually "remove N boundary layers"
608  // This is the standard morphological erosion operation
609  int shrinkLayers = -layers; // Convert negative to positive
610 
611  // Check if this is a mesh with topology (has cells)
612  // Note: For point selections on meshes, we still need cells to
613  // determine connectivity
614  vtkIdType numCells = polyData->GetNumberOfCells();
615  bool hasMeshTopology = (numCells > 0);
616 
617  if (!hasMeshTopology) {
619  "[cvSelectionAlgebra] Shrink operation requires mesh "
620  "topology. "
621  "Pure point clouds without cells are not supported. "
622  "Returning input unchanged.");
623  return input;
624  }
625 
627  QString("[cvSelectionAlgebra] Starting shrink: %1 %2, %3 "
628  "layers")
629  .arg(input.ids().size())
630  .arg(isPointSelection ? "points" : "cells")
631  .arg(shrinkLayers));
632 
633  // Convert input ids to QSet with correct type
634  QSet<qint64> currentSet;
635  for (qint64 id : input.ids()) {
636  currentSet.insert(id);
637  }
638 
639  // Improved shrink logic: Find elements that have AT LEAST ONE
640  // unselected neighbor This ensures we can properly erode the selection
641  // boundary
642  for (int iter = 0; iter < shrinkLayers; ++iter) {
643  QSet<qint64>
644  boundaryElements; // Elements to remove (on the boundary)
645  QSet<qint64>
646  interiorElements; // Elements to keep (in the interior)
647 
648  // Classify each element as boundary or interior
649  for (qint64 id : currentSet) {
650  QSet<vtkIdType> neighbors;
651 
652  if (isPointSelection) {
653  neighbors = getPointNeighbors(polyData,
654  static_cast<vtkIdType>(id));
655  } else {
656  neighbors = getCellNeighbors(polyData,
657  static_cast<vtkIdType>(id));
658  }
659 
660  // Count how many neighbors are outside the current selection
661  int neighborsOutside = 0;
662  for (vtkIdType neighborId : neighbors) {
663  if (!currentSet.contains(static_cast<qint64>(neighborId))) {
664  neighborsOutside++;
665  }
666  }
667 
668  // An element is on the boundary if:
669  // 1. It has no neighbors (isolated element), OR
670  // 2. At least one of its neighbors is NOT in the selection
671  bool isBoundary = (neighbors.isEmpty() || neighborsOutside > 0);
672 
673  if (isBoundary) {
674  boundaryElements.insert(id);
675  } else {
676  interiorElements.insert(id);
677  }
678  }
679 
681  QString("[cvSelectionAlgebra] Shrink iteration %1: "
682  "found %2 boundary, %3 interior elements")
683  .arg(iter + 1)
684  .arg(boundaryElements.size())
685  .arg(interiorElements.size()));
686 
687  // If no boundary elements found, we cannot shrink further
688  if (boundaryElements.isEmpty()) {
690  QString("[cvSelectionAlgebra] Shrink stopped at "
691  "iteration %1: "
692  "no boundary elements found (all %2 %3 have "
693  "all neighbors selected)")
694  .arg(iter + 1)
695  .arg(currentSet.size())
696  .arg(isPointSelection ? "points" : "cells"));
697  break;
698  }
699 
700  // Remove boundary elements (keep only interior)
701  currentSet = interiorElements;
702 
703  // If selection becomes empty, stop
704  if (currentSet.isEmpty()) {
706  QString("[cvSelectionAlgebra] Shrink iteration "
707  "%1: selection shrunk to empty")
708  .arg(iter + 1));
709  break;
710  }
711  }
712 
713  QVector<qint64> shrunkIds;
714  for (qint64 id : currentSet) {
715  shrunkIds.append(id);
716  }
717 
719  QString("[cvSelectionAlgebra] Shrink: %1 layers, %2 -> %3 %4")
720  .arg(shrinkLayers)
721  .arg(input.ids().size())
722  .arg(shrunkIds.size())
723  .arg(isPointSelection ? "points" : "cells"));
724 
725  // Create result selection data
726  cvSelectionData resultSelection(shrunkIds, input.fieldAssociation());
727 
728  // Copy actor info if available
729  if (input.hasActorInfo()) {
730  resultSelection.setActorInfo(input.primaryActor(),
731  input.primaryPolyData());
732  }
733 
734  return resultSelection;
735  }
736 }
737 
738 //-----------------------------------------------------------------------------
740  vtkPolyData* polyData,
741  const cvSelectionData& input,
742  int layers,
743  bool removeSeed,
744  bool removeIntermediateLayers) {
745  if (!polyData || input.isEmpty()) {
746  return cvSelectionData();
747  }
748 
749  if (input.fieldAssociation() != cvSelectionData::POINTS) {
751  "[cvSelectionAlgebra] growPointSelection only works with point "
752  "selection");
753  return input;
754  }
755 
756  // Store the original seed for removeSeed option
757  QSet<qint64> idsSet = qSetFromVector(input.ids());
758  QSet<vtkIdType> seedSet;
759  for (qint64 id : idsSet) {
760  seedSet.insert(static_cast<vtkIdType>(id));
761  }
762  QSet<vtkIdType> currentSet = seedSet;
763  QSet<vtkIdType> previousLayerSet = seedSet;
764 
765  // Grow layer by layer
766  for (int iter = 0; iter < layers; ++iter) {
767  QSet<vtkIdType> newSet = currentSet;
768  bool hasAnyNeighbors = false;
769 
770  // For each selected point, add its neighbors
771  for (vtkIdType pointId : currentSet) {
772  QSet<vtkIdType> neighbors = getPointNeighbors(polyData, pointId);
773  if (!neighbors.isEmpty()) {
774  hasAnyNeighbors = true;
775  }
776  newSet.unite(neighbors);
777  }
778 
779  // Early exit if no neighbors found (pure point cloud case)
780  // This prevents infinite loops or unnecessary iterations
781  if (!hasAnyNeighbors && iter == 0) {
783  "[cvSelectionAlgebra] No topological neighbors found for "
784  "grow operation. "
785  "This appears to be a pure point cloud. Returning input "
786  "unchanged.");
787  return input;
788  }
789 
790  // If no new points were added, we can't grow further
791  if (newSet.size() == currentSet.size()) {
793  QString("[cvSelectionAlgebra] Grow stopped at layer %1: "
794  "no new neighbors found")
795  .arg(iter + 1));
796  break;
797  }
798 
799  previousLayerSet = currentSet;
800  currentSet = newSet;
801  }
802 
803  // Apply removal options
804  QSet<vtkIdType> resultSet = currentSet;
805 
806  if (removeIntermediateLayers) {
807  resultSet = currentSet;
808  resultSet.subtract(previousLayerSet);
809  }
810 
811  if (removeSeed) {
812  resultSet.subtract(seedSet);
813  }
814 
815  QVector<qint64> resultIds;
816  for (vtkIdType id : resultSet) {
817  resultIds.append(id);
818  }
819 
821  QString("[cvSelectionAlgebra] Grow points %1 layers: %2 -> %3 "
822  "points")
823  .arg(layers)
824  .arg(input.count())
825  .arg(resultIds.size()));
826 
827  return cvSelectionData(resultIds, cvSelectionData::POINTS);
828 }
829 
830 //-----------------------------------------------------------------------------
832  vtkPolyData* polyData, const cvSelectionData& input, int iterations) {
833  if (!polyData || input.isEmpty()) {
834  return cvSelectionData();
835  }
836 
837  if (input.fieldAssociation() != cvSelectionData::POINTS) {
839  "[cvSelectionAlgebra] shrinkPointSelection only works with "
840  "point selection");
841  return input;
842  }
843 
844  QSet<qint64> idsSet = qSetFromVector(input.ids());
845  QSet<vtkIdType> currentSet;
846  for (qint64 id : idsSet) {
847  currentSet.insert(static_cast<vtkIdType>(id));
848  }
849 
850  // Quick check: if no points have topological neighbors, this is a pure
851  // point cloud In a pure point cloud, all points are boundary points, so
852  // shrink would remove everything
853  bool hasTopology = false;
854  for (vtkIdType pointId : currentSet) {
856  polyData->GetPointCells(pointId, cellIds);
857  for (vtkIdType i = 0; i < cellIds->GetNumberOfIds(); ++i) {
858  vtkCell* cell = polyData->GetCell(cellIds->GetId(i));
859  if (cell && cell->GetNumberOfPoints() > 1) {
860  hasTopology = true;
861  break;
862  }
863  }
864  if (hasTopology) {
865  break;
866  }
867  }
868 
869  if (!hasTopology) {
871  "[cvSelectionAlgebra] No topological neighbors found for "
872  "shrink operation. "
873  "This appears to be a pure point cloud. Returning input "
874  "unchanged.");
875  return input;
876  }
877 
878  for (int iter = 0; iter < iterations; ++iter) {
879  QSet<vtkIdType> newSet;
880 
881  // Only keep points that are not on the boundary
882  for (vtkIdType pointId : currentSet) {
883  if (!isBoundaryPoint(polyData, pointId, currentSet)) {
884  newSet.insert(pointId);
885  }
886  }
887 
888  // If no points were removed, we can't shrink further
889  if (newSet.size() == currentSet.size()) {
891  QString("[cvSelectionAlgebra] Shrink stopped at "
892  "iteration %1: "
893  "no boundary points found")
894  .arg(iter + 1));
895  break;
896  }
897 
898  currentSet = newSet;
899 
900  if (currentSet.isEmpty()) {
901  break;
902  }
903  }
904 
905  QVector<qint64> resultIds;
906  for (vtkIdType id : currentSet) {
907  resultIds.append(id);
908  }
909 
911  QString("[cvSelectionAlgebra] Shrink points %1 iterations: %2 "
912  "-> %3 points")
913  .arg(iterations)
914  .arg(input.count())
915  .arg(resultIds.size()));
916 
917  return cvSelectionData(resultIds, cvSelectionData::POINTS);
918 }
919 
920 //-----------------------------------------------------------------------------
921 QSet<vtkIdType> cvSelectionAlgebra::getPointNeighbors(vtkPolyData* polyData,
922  vtkIdType pointId) {
923  QSet<vtkIdType> neighbors;
924 
925  // Safety checks
926  if (!polyData) {
928  "[cvSelectionAlgebra::getPointNeighbors] polyData is null");
929  return neighbors;
930  }
931 
932  vtkIdType numPoints = polyData->GetNumberOfPoints();
933  if (numPoints == 0) {
935  "[cvSelectionAlgebra::getPointNeighbors] polyData has no "
936  "points");
937  return neighbors;
938  }
939 
940  if (pointId < 0 || pointId >= numPoints) {
941  CVLog::Warning(QString("[cvSelectionAlgebra::getPointNeighbors] "
942  "pointId %1 out of range [0, %2)")
943  .arg(pointId)
944  .arg(numPoints));
945  return neighbors;
946  }
947 
948  // First try topology-based neighbors (works for meshes)
950  polyData->GetPointCells(pointId, cellIds);
951 
952  // For each cell, get all its points (they are neighbors)
953  for (vtkIdType i = 0; i < cellIds->GetNumberOfIds(); ++i) {
954  vtkIdType cellId = cellIds->GetId(i);
955  vtkCell* cell = polyData->GetCell(cellId);
956  if (cell) {
957  for (vtkIdType j = 0; j < cell->GetNumberOfPoints(); ++j) {
958  vtkIdType neighborPtId = cell->GetPointId(j);
959  if (neighborPtId != pointId) {
960  neighbors.insert(neighborPtId);
961  }
962  }
963  }
964  }
965 
966  // If no topological neighbors found (pure point cloud case),
967  // For point clouds, each point is typically its own vertex cell with no
968  // shared edges, so we skip KD-tree based neighbor search as it's expensive
969  // and may cause issues. Grow/Shrink for pure point clouds is not
970  // well-defined without a mesh topology.
971  if (neighbors.isEmpty()) {
972  // Log that we can't find neighbors for this point cloud
973  // Don't use KD-tree as it can be expensive and cause crashes
975  QString("[cvSelectionAlgebra] Point %1: no topological "
976  "neighbors "
977  "(pure point cloud - grow/shrink not applicable)")
978  .arg(pointId));
979  }
980 
981  return neighbors;
982 }
983 
984 //-----------------------------------------------------------------------------
985 bool cvSelectionAlgebra::isBoundaryPoint(vtkPolyData* polyData,
986  vtkIdType pointId,
987  const QSet<vtkIdType>& selectedSet) {
988  // Safety checks
989  if (!polyData) {
990  return true; // Treat as boundary if invalid
991  }
992 
993  vtkIdType numPoints = polyData->GetNumberOfPoints();
994  if (pointId < 0 || pointId >= numPoints) {
995  return true; // Treat as boundary if out of range
996  }
997 
998  // For point clouds, we use spatial neighbors to determine boundary
999  // A point is on the boundary if at least one of its spatial neighbors
1000  // is not in the selection
1001 
1002  // First try topology-based neighbors
1004  polyData->GetPointCells(pointId, cellIds);
1005 
1006  QSet<vtkIdType> topoNeighbors;
1007  for (vtkIdType i = 0; i < cellIds->GetNumberOfIds(); ++i) {
1008  vtkIdType cellId = cellIds->GetId(i);
1009  vtkCell* cell = polyData->GetCell(cellId);
1010  if (cell) {
1011  for (vtkIdType j = 0; j < cell->GetNumberOfPoints(); ++j) {
1012  vtkIdType neighborPtId = cell->GetPointId(j);
1013  if (neighborPtId != pointId) {
1014  topoNeighbors.insert(neighborPtId);
1015  }
1016  }
1017  }
1018  }
1019 
1020  if (!topoNeighbors.isEmpty()) {
1021  // Use topology-based boundary detection
1022  for (vtkIdType neighborId : topoNeighbors) {
1023  if (!selectedSet.contains(neighborId)) {
1024  return true;
1025  }
1026  }
1027  return false;
1028  }
1029 
1030  // For point clouds (no topological neighbors), all points are considered
1031  // boundary Shrink operation will remove all points, which is the expected
1032  // behavior for a point cloud without mesh topology
1033  return true;
1034 }
1035 
1036 //=============================================================================
1037 // cvSelectionFilter Implementation (merged from cvSelectionFilter.cpp)
1038 //=============================================================================
1039 
1040 #include <vtkTriangle.h>
1041 
1042 //-----------------------------------------------------------------------------
1043 cvSelectionFilter::cvSelectionFilter(QObject* parent) : QObject(parent) {
1044  CVLog::PrintVerbose("[cvSelectionFilter] Initialized");
1045 }
1046 
1047 //-----------------------------------------------------------------------------
1049 
1050 //-----------------------------------------------------------------------------
1052  vtkPolyData* polyData,
1053  const cvSelectionData& input,
1054  const QString& attributeName,
1055  double minValue,
1056  double maxValue) {
1057  if (!polyData || input.isEmpty() || attributeName.isEmpty()) {
1058  return cvSelectionData();
1059  }
1060 
1061  vtkDataArray* array = nullptr;
1062  if (input.fieldAssociation() == cvSelectionData::POINTS) {
1063  array = polyData->GetPointData()->GetArray(
1064  attributeName.toUtf8().constData());
1065  } else {
1066  array = polyData->GetCellData()->GetArray(
1067  attributeName.toUtf8().constData());
1068  }
1069 
1070  if (!array) {
1071  CVLog::Warning(QString("[cvSelectionFilter] Attribute '%1' not found")
1072  .arg(attributeName));
1073  return cvSelectionData();
1074  }
1075 
1076  QVector<qint64> filteredIds;
1077  QVector<qint64> inputIds = input.ids();
1078 
1079  for (qint64 id : inputIds) {
1080  if (id < 0 || id >= array->GetNumberOfTuples()) {
1081  continue;
1082  }
1083 
1084  double value = array->GetComponent(id, 0);
1085 
1086  if (value >= minValue && value <= maxValue) {
1087  filteredIds.append(id);
1088  }
1089  }
1090 
1092  QString("[cvSelectionFilter] Attribute range filter: %1 -> %2 "
1093  "items")
1094  .arg(inputIds.size())
1095  .arg(filteredIds.size()));
1096 
1097  return cvSelectionData(filteredIds, input.fieldAssociation());
1098 }
1099 
1100 //-----------------------------------------------------------------------------
1102  vtkPolyData* polyData,
1103  const cvSelectionData& input,
1104  const QString& attributeName,
1105  ComparisonOp op,
1106  double value) {
1107  if (!polyData || input.isEmpty() || attributeName.isEmpty()) {
1108  return cvSelectionData();
1109  }
1110 
1111  vtkDataArray* array = nullptr;
1112  if (input.fieldAssociation() == cvSelectionData::POINTS) {
1113  array = polyData->GetPointData()->GetArray(
1114  attributeName.toUtf8().constData());
1115  } else {
1116  array = polyData->GetCellData()->GetArray(
1117  attributeName.toUtf8().constData());
1118  }
1119 
1120  if (!array) {
1121  CVLog::Warning(QString("[cvSelectionFilter] Attribute '%1' not found")
1122  .arg(attributeName));
1123  return cvSelectionData();
1124  }
1125 
1126  QVector<qint64> filteredIds;
1127  QVector<qint64> inputIds = input.ids();
1128 
1129  for (qint64 id : inputIds) {
1130  if (id < 0 || id >= array->GetNumberOfTuples()) {
1131  continue;
1132  }
1133 
1134  double attrValue = array->GetComponent(id, 0);
1135  bool pass = false;
1136 
1137  switch (op) {
1138  case EQUAL:
1139  pass = (qAbs(attrValue - value) < 1e-6);
1140  break;
1141  case NOT_EQUAL:
1142  pass = (qAbs(attrValue - value) >= 1e-6);
1143  break;
1144  case LESS_THAN:
1145  pass = (attrValue < value);
1146  break;
1147  case LESS_EQUAL:
1148  pass = (attrValue <= value);
1149  break;
1150  case GREATER_THAN:
1151  pass = (attrValue > value);
1152  break;
1153  case GREATER_EQUAL:
1154  pass = (attrValue >= value);
1155  break;
1156  default:
1157  break;
1158  }
1159 
1160  if (pass) {
1161  filteredIds.append(id);
1162  }
1163  }
1164 
1166  QString("[cvSelectionFilter] Attribute comparison filter: %1 "
1167  "-> %2 items")
1168  .arg(inputIds.size())
1169  .arg(filteredIds.size()));
1170 
1171  return cvSelectionData(filteredIds, input.fieldAssociation());
1172 }
1173 
1174 //-----------------------------------------------------------------------------
1176  const cvSelectionData& input,
1177  double minArea,
1178  double maxArea) {
1179  if (!polyData || input.isEmpty()) {
1180  return cvSelectionData();
1181  }
1182 
1183  if (input.fieldAssociation() != cvSelectionData::CELLS) {
1185  "[cvSelectionFilter] Area filter only works with cell "
1186  "selection");
1187  return cvSelectionData();
1188  }
1189 
1190  QVector<qint64> filteredIds;
1191  QVector<qint64> inputIds = input.ids();
1192 
1193  for (qint64 id : inputIds) {
1194  if (id < 0 || id >= polyData->GetNumberOfCells()) {
1195  continue;
1196  }
1197 
1198  double area = computeCellArea(polyData, id);
1199  if (area >= minArea && area <= maxArea) {
1200  filteredIds.append(id);
1201  }
1202  }
1203 
1205  QString("[cvSelectionFilter] Area filter: %1 -> %2 cells")
1206  .arg(inputIds.size())
1207  .arg(filteredIds.size()));
1208 
1209  return cvSelectionData(filteredIds, cvSelectionData::CELLS);
1210 }
1211 
1212 //-----------------------------------------------------------------------------
1214  vtkPolyData* polyData,
1215  const cvSelectionData& input,
1216  double refX,
1217  double refY,
1218  double refZ,
1219  double minAngleDeg,
1220  double maxAngleDeg) {
1221  if (!polyData || input.isEmpty()) {
1222  return cvSelectionData();
1223  }
1224 
1225  if (input.fieldAssociation() != cvSelectionData::CELLS) {
1227  "[cvSelectionFilter] Normal angle filter only works with cell "
1228  "selection");
1229  return cvSelectionData();
1230  }
1231 
1232  vtkDataArray* normals = polyData->GetCellData()->GetNormals();
1233  if (!normals) {
1234  CVLog::Warning("[cvSelectionFilter] No cell normals available");
1235  return cvSelectionData();
1236  }
1237 
1238  double refNormal[3] = {refX, refY, refZ};
1239  vtkMath::Normalize(refNormal);
1240 
1241  QVector<qint64> filteredIds;
1242  QVector<qint64> inputIds = input.ids();
1243 
1244  for (qint64 id : inputIds) {
1245  if (id < 0 || id >= polyData->GetNumberOfCells()) {
1246  continue;
1247  }
1248 
1249  double* normal = normals->GetTuple3(id);
1250  double angleDeg = computeAngleBetweenNormals(normal, refNormal);
1251 
1252  if (angleDeg >= minAngleDeg && angleDeg <= maxAngleDeg) {
1253  filteredIds.append(id);
1254  }
1255  }
1256 
1258  QString("[cvSelectionFilter] Normal angle filter: %1 -> %2 cells")
1259  .arg(inputIds.size())
1260  .arg(filteredIds.size()));
1261 
1262  return cvSelectionData(filteredIds, cvSelectionData::CELLS);
1263 }
1264 
1265 //-----------------------------------------------------------------------------
1267  vtkPolyData* polyData,
1268  const cvSelectionData& input,
1269  const double bounds[6]) {
1270  if (!polyData || input.isEmpty()) {
1271  return cvSelectionData();
1272  }
1273 
1274  QVector<qint64> filteredIds;
1275  QVector<qint64> inputIds = input.ids();
1276 
1277  if (input.fieldAssociation() == cvSelectionData::POINTS) {
1278  for (qint64 id : inputIds) {
1279  if (id < 0 || id >= polyData->GetNumberOfPoints()) {
1280  continue;
1281  }
1282 
1283  double point[3];
1284  polyData->GetPoint(id, point);
1285 
1286  if (isPointInBounds(point, bounds)) {
1287  filteredIds.append(id);
1288  }
1289  }
1290  } else {
1291  for (qint64 id : inputIds) {
1292  if (id < 0 || id >= polyData->GetNumberOfCells()) {
1293  continue;
1294  }
1295 
1296  vtkCell* cell = polyData->GetCell(id);
1297  if (!cell) continue;
1298 
1299  double center[3] = {0, 0, 0};
1300  vtkIdType npts = cell->GetNumberOfPoints();
1301  for (vtkIdType i = 0; i < npts; ++i) {
1302  double pt[3];
1303  polyData->GetPoint(cell->GetPointId(i), pt);
1304  center[0] += pt[0];
1305  center[1] += pt[1];
1306  center[2] += pt[2];
1307  }
1308  if (npts > 0) {
1309  center[0] /= npts;
1310  center[1] /= npts;
1311  center[2] /= npts;
1312  }
1313 
1314  if (isPointInBounds(center, bounds)) {
1315  filteredIds.append(id);
1316  }
1317  }
1318  }
1319 
1321  QString("[cvSelectionFilter] Bounding box filter: %1 -> %2 items")
1322  .arg(inputIds.size())
1323  .arg(filteredIds.size()));
1324 
1325  return cvSelectionData(filteredIds, input.fieldAssociation());
1326 }
1327 
1328 //-----------------------------------------------------------------------------
1330  vtkPolyData* polyData,
1331  const cvSelectionData& input,
1332  double x,
1333  double y,
1334  double z,
1335  double minDistance,
1336  double maxDistance) {
1337  if (!polyData || input.isEmpty()) {
1338  return cvSelectionData();
1339  }
1340 
1341  double refPoint[3] = {x, y, z};
1342  QVector<qint64> filteredIds;
1343  QVector<qint64> inputIds = input.ids();
1344 
1345  if (input.fieldAssociation() == cvSelectionData::POINTS) {
1346  for (qint64 id : inputIds) {
1347  if (id < 0 || id >= polyData->GetNumberOfPoints()) {
1348  continue;
1349  }
1350 
1351  double point[3];
1352  polyData->GetPoint(id, point);
1353 
1354  double dist = computeDistance(point, refPoint);
1355  if (dist >= minDistance && dist <= maxDistance) {
1356  filteredIds.append(id);
1357  }
1358  }
1359  } else {
1360  for (qint64 id : inputIds) {
1361  if (id < 0 || id >= polyData->GetNumberOfCells()) {
1362  continue;
1363  }
1364 
1365  vtkCell* cell = polyData->GetCell(id);
1366  if (!cell) continue;
1367 
1368  double center[3] = {0, 0, 0};
1369  vtkIdType npts = cell->GetNumberOfPoints();
1370  for (vtkIdType i = 0; i < npts; ++i) {
1371  double pt[3];
1372  polyData->GetPoint(cell->GetPointId(i), pt);
1373  center[0] += pt[0];
1374  center[1] += pt[1];
1375  center[2] += pt[2];
1376  }
1377  if (npts > 0) {
1378  center[0] /= npts;
1379  center[1] /= npts;
1380  center[2] /= npts;
1381  }
1382 
1383  double dist = computeDistance(center, refPoint);
1384  if (dist >= minDistance && dist <= maxDistance) {
1385  filteredIds.append(id);
1386  }
1387  }
1388  }
1389 
1391  QString("[cvSelectionFilter] Distance filter: %1 -> %2 items")
1392  .arg(inputIds.size())
1393  .arg(filteredIds.size()));
1394 
1395  return cvSelectionData(filteredIds, input.fieldAssociation());
1396 }
1397 
1398 //-----------------------------------------------------------------------------
1400  vtkPolyData* polyData,
1401  const cvSelectionData& input,
1402  int minNeighbors,
1403  int maxNeighbors) {
1404  if (!polyData || input.isEmpty()) {
1405  return cvSelectionData();
1406  }
1407 
1408  if (input.fieldAssociation() != cvSelectionData::CELLS) {
1410  "[cvSelectionFilter] Neighbor count filter only works with "
1411  "cell selection");
1412  return cvSelectionData();
1413  }
1414 
1415  QVector<qint64> filteredIds;
1416  QVector<qint64> inputIds = input.ids();
1417 
1418  for (qint64 id : inputIds) {
1419  if (id < 0 || id >= polyData->GetNumberOfCells()) {
1420  continue;
1421  }
1422 
1423  int neighborCount = countCellNeighbors(polyData, id);
1424  if (neighborCount >= minNeighbors && neighborCount <= maxNeighbors) {
1425  filteredIds.append(id);
1426  }
1427  }
1428 
1430  QString("[cvSelectionFilter] Neighbor count filter: %1 -> %2 cells")
1431  .arg(inputIds.size())
1432  .arg(filteredIds.size()));
1433 
1434  return cvSelectionData(filteredIds, cvSelectionData::CELLS);
1435 }
1436 
1437 //-----------------------------------------------------------------------------
1439  const cvSelectionData& b) {
1440  if (a.isEmpty() || b.isEmpty()) {
1441  return cvSelectionData();
1442  }
1443 
1444  if (a.fieldAssociation() != b.fieldAssociation()) {
1445  return cvSelectionData();
1446  }
1447 
1448  QSet<qint64> setA = qSetFromVector(a.ids());
1449  QSet<qint64> setB = qSetFromVector(b.ids());
1450  QSet<qint64> result = setA.intersect(setB);
1451 
1452  QVector<qint64> resultIds = qVectorFromSet(result);
1453 
1454  CVLog::PrintVerbose(QString("[cvSelectionFilter] AND: %1 & %2 = %3")
1455  .arg(a.count())
1456  .arg(b.count())
1457  .arg(resultIds.size()));
1458 
1459  return cvSelectionData(resultIds, a.fieldAssociation());
1460 }
1461 
1462 //-----------------------------------------------------------------------------
1464  const cvSelectionData& b) {
1465  if (a.isEmpty()) return b;
1466  if (b.isEmpty()) return a;
1467 
1468  if (a.fieldAssociation() != b.fieldAssociation()) {
1469  return cvSelectionData();
1470  }
1471 
1472  QSet<qint64> setA = qSetFromVector(a.ids());
1473  QSet<qint64> setB = qSetFromVector(b.ids());
1474  QSet<qint64> result = setA.unite(setB);
1475 
1476  QVector<qint64> resultIds = qVectorFromSet(result);
1477 
1478  CVLog::PrintVerbose(QString("[cvSelectionFilter] OR: %1 U %2 = %3")
1479  .arg(a.count())
1480  .arg(b.count())
1481  .arg(resultIds.size()));
1482 
1483  return cvSelectionData(resultIds, a.fieldAssociation());
1484 }
1485 
1486 //-----------------------------------------------------------------------------
1488  const cvSelectionData& input) {
1489  if (!polyData || input.isEmpty()) {
1490  return cvSelectionData();
1491  }
1492 
1493  QSet<qint64> selectedSet = qSetFromVector(input.ids());
1494  QVector<qint64> invertedIds;
1495 
1496  vtkIdType totalCount = (input.fieldAssociation() == cvSelectionData::POINTS)
1497  ? polyData->GetNumberOfPoints()
1498  : polyData->GetNumberOfCells();
1499 
1500  for (vtkIdType i = 0; i < totalCount; ++i) {
1501  if (!selectedSet.contains(i)) {
1502  invertedIds.append(i);
1503  }
1504  }
1505 
1506  CVLog::PrintVerbose(QString("[cvSelectionFilter] Invert: %1 -> %2")
1507  .arg(input.count())
1508  .arg(invertedIds.size()));
1509 
1510  return cvSelectionData(invertedIds, input.fieldAssociation());
1511 }
1512 
1513 //-----------------------------------------------------------------------------
1514 QStringList cvSelectionFilter::getAttributeNames(vtkPolyData* polyData,
1515  bool pointData) {
1516  QStringList names;
1517 
1518  if (!polyData) {
1519  return names;
1520  }
1521 
1522  vtkFieldData* data = pointData ? (vtkFieldData*)polyData->GetPointData()
1523  : (vtkFieldData*)polyData->GetCellData();
1524 
1525  if (!data) {
1526  return names;
1527  }
1528 
1529  int numArrays = data->GetNumberOfArrays();
1530  for (int i = 0; i < numArrays; ++i) {
1531  vtkDataArray* array = data->GetArray(i);
1532  if (array && array->GetName()) {
1533  names.append(QString::fromUtf8(array->GetName()));
1534  }
1535  }
1536 
1537  return names;
1538 }
1539 
1540 //-----------------------------------------------------------------------------
1541 // cvSelectionFilter Private helper methods
1542 //-----------------------------------------------------------------------------
1543 
1544 double cvSelectionFilter::computeCellArea(vtkPolyData* polyData,
1545  vtkIdType cellId) {
1546  vtkCell* cell = polyData->GetCell(cellId);
1547  if (!cell) return 0.0;
1548 
1549  if (cell->GetCellType() == VTK_TRIANGLE) {
1550  double p0[3], p1[3], p2[3];
1551  polyData->GetPoint(cell->GetPointId(0), p0);
1552  polyData->GetPoint(cell->GetPointId(1), p1);
1553  polyData->GetPoint(cell->GetPointId(2), p2);
1554 
1555  return vtkTriangle::TriangleArea(p0, p1, p2);
1556  }
1557 
1558  vtkIdType npts = cell->GetNumberOfPoints();
1559  if (npts < 3) return 0.0;
1560 
1561  double area = 0.0;
1562  double p0[3], p1[3], p2[3];
1563  polyData->GetPoint(cell->GetPointId(0), p0);
1564 
1565  for (vtkIdType i = 1; i < npts - 1; ++i) {
1566  polyData->GetPoint(cell->GetPointId(i), p1);
1567  polyData->GetPoint(cell->GetPointId(i + 1), p2);
1568  area += vtkTriangle::TriangleArea(p0, p1, p2);
1569  }
1570 
1571  return area;
1572 }
1573 
1574 //-----------------------------------------------------------------------------
1575 double cvSelectionFilter::computeAngleBetweenNormals(const double n1[3],
1576  const double n2[3]) {
1577  double dot = vtkMath::Dot(n1, n2);
1578  dot = qBound(-1.0, dot, 1.0);
1579  double angleRad = std::acos(dot);
1580  return vtkMath::DegreesFromRadians(angleRad);
1581 }
1582 
1583 //-----------------------------------------------------------------------------
1584 bool cvSelectionFilter::isPointInBounds(const double point[3],
1585  const double bounds[6]) {
1586  return (point[0] >= bounds[0] && point[0] <= bounds[1] &&
1587  point[1] >= bounds[2] && point[1] <= bounds[3] &&
1588  point[2] >= bounds[4] && point[2] <= bounds[5]);
1589 }
1590 
1591 //-----------------------------------------------------------------------------
1592 double cvSelectionFilter::computeDistance(const double p1[3],
1593  const double p2[3]) {
1594  double dx = p1[0] - p2[0];
1595  double dy = p1[1] - p2[1];
1596  double dz = p1[2] - p2[2];
1597  return std::sqrt(dx * dx + dy * dy + dz * dz);
1598 }
1599 
1600 //-----------------------------------------------------------------------------
1601 int cvSelectionFilter::countCellNeighbors(vtkPolyData* polyData,
1602  vtkIdType cellId) {
1603  vtkCell* cell = polyData->GetCell(cellId);
1604  if (!cell) return 0;
1605 
1606  QSet<vtkIdType> neighbors;
1607 
1608  vtkIdType npts = cell->GetNumberOfPoints();
1609  for (vtkIdType i = 0; i < npts; ++i) {
1610  vtkIdType p1 = cell->GetPointId(i);
1611  vtkIdType p2 = cell->GetPointId((i + 1) % npts);
1612 
1614  polyData->GetCellEdgeNeighbors(cellId, p1, p2, cellIds);
1615 
1616  for (vtkIdType j = 0; j < cellIds->GetNumberOfIds(); ++j) {
1617  neighbors.insert(cellIds->GetId(j));
1618  }
1619  }
1620 
1621  return neighbors.size();
1622 }
double normal[3]
QSet< T > qSetFromVector(const QVector< T > &vec)
Definition: QtCompat.h:1073
QVector< T > qVectorFromSet(const QSet< T > &set)
Definition: QtCompat.h:1078
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 cvSelectionData complementOf(vtkPolyData *polyData, const cvSelectionData &input)
Compute complement of a selection.
static cvSelectionData extractBoundary(vtkPolyData *polyData, const cvSelectionData &input)
Extract boundary elements of selection.
static cvSelectionData differenceOf(const cvSelectionData &a, const cvSelectionData &b)
Compute difference of two selections.
static cvSelectionData growPointSelection(vtkPolyData *polyData, const cvSelectionData &input, int layers=1, bool removeSeed=false, bool removeIntermediateLayers=false)
Grow point selection by adding neighbor points.
static cvSelectionData unionOf(const cvSelectionData &a, const cvSelectionData &b)
Compute union of two selections.
static cvSelectionData shrinkPointSelection(vtkPolyData *polyData, const cvSelectionData &input, int iterations=1)
Shrink point selection by removing boundary points.
Operation
Algebra operations Using enum class to avoid macro conflicts (e.g., DIFFERENCE may be defined as a ma...
cvSelectionAlgebra(QObject *parent=nullptr)
static cvSelectionData expandSelection(vtkPolyData *polyData, const cvSelectionData &input, int layers, bool removeSeed=false, bool removeIntermediateLayers=false)
Expand selection (ParaView-compatible)
static cvSelectionData symmetricDifferenceOf(const cvSelectionData &a, const cvSelectionData &b)
Compute symmetric difference of two selections.
static cvSelectionData performOperation(Operation op, const cvSelectionData &a, const cvSelectionData &b, vtkPolyData *polyData=nullptr)
Perform algebra operation on two selections.
static cvSelectionData intersectionOf(const cvSelectionData &a, const cvSelectionData &b)
Compute intersection of two selections.
static cvSelectionData shrinkSelection(vtkPolyData *polyData, const cvSelectionData &input, int iterations=1)
Shrink selection by removing boundary elements.
static bool areCompatible(const cvSelectionData &a, const cvSelectionData &b)
Validate that two selections are compatible for operations.
static cvSelectionData growSelection(vtkPolyData *polyData, const cvSelectionData &input, int layers=1, bool removeSeed=false, bool removeIntermediateLayers=false)
Grow selection by adding neighbors.
Encapsulates selection data without exposing VTK types.
FieldAssociation fieldAssociation() const
Get field association.
@ 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)
bool hasActorInfo() const
Check if actor information is available.
int count() const
Get number of selected items.
vtkPolyData * primaryPolyData() const
Get the primary (front-most) polyData.
vtkActor * primaryActor() const
Get the primary (front-most) actor.
void setActorInfo(vtkActor *actor, vtkPolyData *polyData, double zValue=1.0)
Set actor information (single actor case)
static cvSelectionData combineAND(const cvSelectionData &a, const cvSelectionData &b)
Combine two selections with AND operation.
ComparisonOp
Comparison operators.
cvSelectionData filterByAttributeRange(vtkPolyData *polyData, const cvSelectionData &input, const QString &attributeName, double minValue, double maxValue)
Filter by attribute value range.
static QStringList getAttributeNames(vtkPolyData *polyData, bool pointData=true)
Get attribute names available in polyData.
cvSelectionData filterByDistanceFromPoint(vtkPolyData *polyData, const cvSelectionData &input, double x, double y, double z, double minDistance, double maxDistance)
Filter by distance from point.
cvSelectionData filterByBoundingBox(vtkPolyData *polyData, const cvSelectionData &input, const double bounds[6])
Filter by bounding box.
cvSelectionData filterByAttributeComparison(vtkPolyData *polyData, const cvSelectionData &input, const QString &attributeName, ComparisonOp op, double value)
Filter by attribute comparison.
static cvSelectionData combineOR(const cvSelectionData &a, const cvSelectionData &b)
Combine two selections with OR operation.
cvSelectionData filterByArea(vtkPolyData *polyData, const cvSelectionData &input, double minArea, double maxArea)
Filter cells by area.
cvSelectionFilter(QObject *parent=nullptr)
static cvSelectionData invert(vtkPolyData *polyData, const cvSelectionData &input)
Invert selection (NOT operation)
cvSelectionData filterByNeighborCount(vtkPolyData *polyData, const cvSelectionData &input, int minNeighbors, int maxNeighbors)
Filter by neighbor count (topology)
cvSelectionData filterByNormalAngle(vtkPolyData *polyData, const cvSelectionData &input, double refX, double refY, double refZ, double minAngleDeg, double maxAngleDeg)
Filter by normal angle relative to reference direction.
double normals[3]
a[190]
const double * e
GraphType data
Definition: graph_cut.cc:138
normal_z y
normal_z x
normal_z z