ACloudViewer  3.9.4
A Modern Library for 3D Data Processing
cvSelectionPropertiesWidget.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 
9 
10 // LOCAL
11 #include "PclUtils/PCLVis.h"
12 #include "cvExpanderButton.h"
14 #include "cvSelectionAlgebra.h"
15 #include "cvSelectionAnnotation.h"
16 #include "cvSelectionExporter.h"
17 #include "cvSelectionHighlighter.h"
19 #include "cvViewSelectionManager.h"
20 
21 // CV_DB_LIB
22 #include <ecvDisplayTools.h>
23 #include <ecvGenericVisualizer3D.h>
24 #include <ecvMesh.h>
25 #include <ecvPointCloud.h>
26 
27 // CV_CORE_LIB
28 #include <CVLog.h>
29 
30 // CV_IO_LIB
31 #include <FileIOFilter.h>
32 
33 // Qt
34 #include <QApplication>
35 #include <QDateTime>
36 #include <QDialog>
37 #include <QEvent>
38 #include <QFileDialog>
39 #include <QInputDialog>
40 #include <QMessageBox>
41 #include <QProgressDialog>
42 #include <QRegularExpression>
43 #include <QResizeEvent>
44 #include <QTabWidget>
45 #include <QTimer>
46 
47 // Qt5/Qt6 Compatibility
48 #include <QtCompat.h>
49 
50 // STL
51 #include <cmath>
52 #include <limits>
53 
54 // QCustomPlot (PCLEngine uses its own copy)
56 
57 // VTK
58 #include <vtkAbstractArray.h>
59 #include <vtkActor.h>
60 #include <vtkCell.h>
61 #include <vtkCellData.h>
62 #include <vtkDataArray.h>
63 #include <vtkDataSetMapper.h>
64 #include <vtkFieldData.h>
65 #include <vtkIdTypeArray.h>
66 #include <vtkMapper.h>
67 #include <vtkPointData.h>
68 #include <vtkPolyData.h>
69 #include <vtkPolyDataMapper.h>
70 #include <vtkProp.h>
71 #include <vtkPropCollection.h>
72 #include <vtkRenderer.h>
73 #include <vtkSmartPointer.h>
74 #include <vtkStringArray.h>
75 #include <vtkVariant.h>
76 
77 // Qt
78 #include <QAbstractButton>
79 #include <QApplication>
80 #include <QBrush>
81 #include <QCheckBox>
82 #include <QClipboard>
83 #include <QColorDialog>
84 #include <QComboBox>
85 #include <QDoubleSpinBox>
86 #include <QEvent>
87 #include <QFileDialog>
88 #include <QFormLayout>
89 #include <QGridLayout>
90 #include <QGroupBox>
91 #include <QHBoxLayout>
92 #include <QHeaderView>
93 #include <QIcon>
94 #include <QLabel>
95 #include <QLineEdit>
96 #include <QMenu>
97 #include <QMessageBox>
98 #include <QPainter>
99 #include <QPixmap>
100 #include <QPushButton>
101 #include <QResizeEvent>
102 #include <QScrollArea>
103 #include <QSpinBox>
104 #include <QTabWidget>
105 #include <QTableWidget>
106 #include <QToolButton>
107 #include <QVBoxLayout>
108 
109 // ParaView-style selection colors palette
110 const QColor cvSelectionPropertiesWidget::s_selectionColors[] = {
111  QColor(255, 0, 0), // Red
112  QColor(0, 255, 0), // Green
113  QColor(0, 0, 255), // Blue
114  QColor(255, 255, 0), // Yellow
115  QColor(255, 0, 255), // Magenta
116  QColor(0, 255, 255), // Cyan
117  QColor(255, 128, 0), // Orange
118  QColor(128, 0, 255), // Purple
119  QColor(0, 255, 128), // Spring Green
120  QColor(255, 0, 128), // Rose
121 };
122 const int cvSelectionPropertiesWidget::s_selectionColorsCount = 10;
123 
124 //-----------------------------------------------------------------------------
126  : QWidget(parent),
127  cvSelectionBase(),
128  m_highlighter(nullptr),
129  m_tooltipFormatter(new cvTooltipFormatter()),
130  m_selectionManager(nullptr),
131  m_selectionCount(0),
132  m_volume(0.0),
133  m_selectionNameCounter(0),
134  m_extractCounter(0),
135  m_lastHighlightedId(-1),
136  // Expander buttons and containers (ParaView-style collapsible sections)
137  m_selectedDataExpander(nullptr),
138  m_selectedDataContainer(nullptr),
139  m_selectionDisplayExpander(nullptr),
140  m_selectionDisplayContainer(nullptr),
141  m_selectionEditorExpander(nullptr),
142  m_selectionEditorContainer(nullptr),
143  m_createSelectionExpander(nullptr),
144  m_createSelectionContainer(nullptr),
145  m_selectedDataSpreadsheetExpander(nullptr),
146  m_selectedDataSpreadsheetContainer(nullptr),
147  m_compactStatsExpander(nullptr),
148  m_compactStatsContainer(nullptr),
149  // Selected Data header widgets
150  m_freezeButton(nullptr),
151  m_extractButton(nullptr),
152  m_plotOverTimeButton(nullptr),
153  // Create Selection section
154  m_createSelectionGroup(nullptr),
155  m_dataProducerCombo(nullptr),
156  m_elementTypeCombo(nullptr),
157  m_attributeCombo(nullptr),
158  m_operatorCombo(nullptr),
159  m_valueEdit(nullptr),
160  m_processIdSpinBox(nullptr),
161  m_findDataButton(nullptr),
162  m_resetButton(nullptr),
163  m_clearButton(nullptr),
164  m_queriesLayout(nullptr),
165  m_tabWidget(nullptr),
166  // Legacy UI elements (kept for backward compatibility)
167  m_selectionTableWidget(nullptr),
168  m_listInfoLabel(nullptr),
169  m_algebraOpCombo(nullptr),
170  m_applyAlgebraButton(nullptr),
171  m_extractBoundaryButton(nullptr),
172  // Filter UI removed - not implemented
173  m_addAnnotationButton(nullptr),
174  // Legacy color/opacity controls
175  m_hoverColorButton(nullptr),
176  m_preselectedColorButton(nullptr),
177  m_selectedColorButton(nullptr),
178  m_boundaryColorButton(nullptr),
179  m_hoverOpacitySpin(nullptr),
180  m_preselectedOpacitySpin(nullptr),
181  m_selectedOpacitySpin(nullptr),
182  m_boundaryOpacitySpin(nullptr) {
183  // Initialize saved preselected color (yellow by default)
184  m_savedPreselectedColor[0] = 1.0;
185  m_savedPreselectedColor[1] = 1.0;
186  m_savedPreselectedColor[2] = 0.0;
187 
188  // Colors are now stored in cvSelectionHighlighter (single source of truth)
189  // UI buttons will be initialized from highlighter in
190  // syncUIWithHighlighter()
191 
192  for (int i = 0; i < 6; ++i) {
193  m_bounds[i] = 0.0;
194  }
195  for (int i = 0; i < 3; ++i) {
196  m_center[i] = 0.0;
197  }
198 
199  setupUi();
200 
201  // Set size policy to expand and fill available space
202  // This is especially important when the widget is displayed alone (no DB
203  // object selected)
204  setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Expanding);
205 
207  "[cvSelectionPropertiesWidget] Initialized with ParaView-style UI");
208 }
209 
210 //-----------------------------------------------------------------------------
212  delete m_tooltipFormatter;
213 }
214 
215 //-----------------------------------------------------------------------------
216 //-----------------------------------------------------------------------------
217 void cvSelectionPropertiesWidget::updateScrollContentWidth() {
218  // Helper function to update scroll content width
219  // Called from multiple places to ensure consistency
220  if (m_scrollContent && m_scrollArea && m_scrollArea->viewport()) {
221  int viewportWidth = m_scrollArea->viewport()->width();
222  if (viewportWidth > 0) {
223  // CRITICAL: Set width BEFORE adjustSize to prevent it from being
224  // reset Use setFixedWidth to ensure width constraint is maintained
225  m_scrollContent->setFixedWidth(viewportWidth);
226  // Force immediate layout update
227  m_scrollContent->updateGeometry();
228  // Update content size to recalculate layout (height only, width is
229  // fixed)
230  m_scrollContent->adjustSize();
231  // Ensure width is still correct after adjustSize (defensive check)
232  if (m_scrollContent->width() != viewportWidth) {
233  m_scrollContent->setFixedWidth(viewportWidth);
234  }
235  }
236  }
237 }
238 
239 //-----------------------------------------------------------------------------
240 bool cvSelectionPropertiesWidget::eventFilter(QObject* obj, QEvent* event) {
241  // Handle scroll area resize events to update content width
242  if (obj == m_scrollArea && event->type() == QEvent::Resize) {
243  updateScrollContentWidth();
244  }
245  // Handle viewport resize events (more reliable for drag resize)
246  else if (obj == m_scrollArea->viewport() &&
247  event->type() == QEvent::Resize) {
248  updateScrollContentWidth();
249  }
250  // Handle resize events for color buttons (like ParaView's
251  // pqColorChooserButton::resizeEvent)
252  else if (event->type() == QEvent::Resize) {
253  if (obj == m_selectionColorButton && m_highlighter) {
254  QColor color = m_highlighter->getHighlightQColor(
256  updateColorButtonIcon(m_selectionColorButton, color);
257  } else if (obj == m_interactiveSelectionColorButton && m_highlighter) {
258  QColor color = m_highlighter->getHighlightQColor(
260  updateColorButtonIcon(m_interactiveSelectionColorButton, color);
261  }
262  }
263  // Call base class event filter
264  return QWidget::eventFilter(obj, event);
265 }
266 
267 //-----------------------------------------------------------------------------
269  QWidget::resizeEvent(event);
270  // Update scroll content width immediately when widget is resized (e.g., by
271  // dragging) This ensures real-time responsiveness during drag resize
272  // operations
273  updateScrollContentWidth();
274 }
275 
276 // setVisualizer is inherited from cvGenericSelectionTool
277 
278 //-----------------------------------------------------------------------------
280  cvSelectionHighlighter* highlighter) {
281  // Disconnect previous connections if highlighter changes
282  if (m_highlighter && m_highlighter != highlighter) {
283  disconnect(m_highlighter, nullptr, this, nullptr);
285  nullptr, nullptr);
287  nullptr, nullptr);
288  }
289 
290  m_highlighter = highlighter;
291 
292  if (m_highlighter) {
293  // Connect highlighter signals to UI update slots (bidirectional sync)
294  // When highlighter properties change externally, update UI
295  connect(m_highlighter, &cvSelectionHighlighter::colorChanged, this,
296  &cvSelectionPropertiesWidget::onHighlighterColorChanged,
297  Qt::UniqueConnection);
298  connect(m_highlighter, &cvSelectionHighlighter::opacityChanged, this,
299  &cvSelectionPropertiesWidget::onHighlighterOpacityChanged,
300  Qt::UniqueConnection);
301  connect(m_highlighter, &cvSelectionHighlighter::labelPropertiesChanged,
302  this,
304  onHighlighterLabelPropertiesChanged,
305  Qt::UniqueConnection);
306 
307  // Connect UI signals to highlighter (forward user changes)
308  // Use Qt::UniqueConnection to prevent duplicate connections
309  connect(
311  [this](double r, double g, double b, int mode) {
312  if (!m_highlighter) return;
313 
316  mode);
317 
318  // Block signals to prevent feedback loop
319  m_highlighter->blockSignals(true);
320  m_highlighter->setHighlightColor(r, g, b, hlMode);
321  m_highlighter->blockSignals(false);
322 
323  // Refresh display
324  PclUtils::PCLVis* pclVis = getPCLVis();
325  if (pclVis) {
326  pclVis->UpdateScreen();
327  }
328  },
329  Qt::UniqueConnection);
330 
331  connect(
333  this,
334  [this](double opacity, int mode) {
335  if (!m_highlighter) return;
336 
339  mode);
340 
341  // Block signals to prevent feedback loop
342  m_highlighter->blockSignals(true);
343  m_highlighter->setHighlightOpacity(opacity, hlMode);
344  m_highlighter->blockSignals(false);
345 
346  // Refresh display
347  PclUtils::PCLVis* pclVis = getPCLVis();
348  if (pclVis) {
349  pclVis->UpdateScreen();
350  }
351  },
352  Qt::UniqueConnection);
353 
354  // Sync UI with highlighter's current settings
356  }
357 }
358 
359 //-----------------------------------------------------------------------------
360 void cvSelectionPropertiesWidget::syncInternalColorArray(double r,
361  double g,
362  double b,
363  int mode) {
364  // DEPRECATED: Colors are now stored in cvSelectionHighlighter
365  // This method now only updates UI buttons to reflect color changes
366  // Called when colors change via highlightColorChanged signal
367  // Use ParaView-style icon-based color buttons
368  QColor color = QColor::fromRgbF(r, g, b);
369 
370  switch (mode) {
372  if (m_interactiveSelectionColorButton) {
373  updateColorButtonIcon(m_interactiveSelectionColorButton, color);
374  }
375  if (m_hoverColorButton) {
376  updateColorButtonIcon(m_hoverColorButton, color);
377  }
378  break;
380  if (m_preselectedColorButton) {
381  updateColorButtonIcon(m_preselectedColorButton, color);
382  }
383  break;
385  if (m_selectionColorButton) {
386  updateColorButtonIcon(m_selectionColorButton, color);
387  }
388  if (m_selectedColorButton) {
389  updateColorButtonIcon(m_selectedColorButton, color);
390  }
391  break;
393  if (m_boundaryColorButton) {
394  updateColorButtonIcon(m_boundaryColorButton, color);
395  }
396  break;
397  default:
398  break;
399  }
400 }
401 
402 //-----------------------------------------------------------------------------
403 void cvSelectionPropertiesWidget::updateColorButtonIcon(QAbstractButton* button,
404  const QColor& color) {
405  if (!button) return;
406 
407  // ParaView style: use button height * 0.75 for icon radius (same as
408  // pqColorChooserButton) Reference: pqColorChooserButton::renderColorSwatch
409  int buttonHeight = button->height();
410  if (buttonHeight <= 0) {
411  button->adjustSize();
412  buttonHeight = button->height();
413  }
414  if (buttonHeight <= 0) {
415  buttonHeight = button->sizeHint().height();
416  }
417  if (buttonHeight <= 0) {
418  buttonHeight = 25; // Fallback default
419  }
420 
421  // Calculate radius based on height (ParaView style: IconRadiusHeightRatio =
422  // 0.75)
423  int radius = qRound(buttonHeight * 0.75);
424  radius = std::max(radius, 10); // Minimum 10px (ParaView default)
425 
426  // Create circular color swatch icon (ParaView-style)
427  // Use exact same approach as pqColorChooserButton::renderColorSwatch
428  QPixmap pix(radius, radius);
429  pix.fill(QColor(0, 0, 0, 0)); // Transparent background
430 
431  QPainter painter(&pix);
432  painter.setRenderHint(QPainter::Antialiasing, true);
433  painter.setBrush(QBrush(color));
434  painter.drawEllipse(1, 1, radius - 2, radius - 2);
435  painter.end();
436 
437  QIcon icon(pix);
438 
439  // Add high-dpi version for retina displays (ParaView exact style)
440  QPixmap pix2x(radius * 2, radius * 2);
441  pix2x.setDevicePixelRatio(2.0);
442  pix2x.fill(QColor(0, 0, 0, 0));
443 
444  QPainter painter2x(&pix2x);
445  painter2x.setRenderHint(QPainter::Antialiasing, true);
446  painter2x.setBrush(QBrush(color));
447  // ParaView uses: drawEllipse(2, 2, radius - 4, radius - 4) for 2x version
448  painter2x.drawEllipse(2, 2, radius - 4, radius - 4);
449  painter2x.end();
450 
451  icon.addPixmap(pix2x);
452 
453  button->setIcon(icon);
454 
455  // Set icon size (QToolButton will use this)
456  if (QToolButton* toolButton = qobject_cast<QToolButton*>(button)) {
457  toolButton->setIconSize(QSize(radius, radius));
458  }
459 }
460 
461 //-----------------------------------------------------------------------------
462 void cvSelectionPropertiesWidget::onHighlighterColorChanged(int mode) {
463  // Called when highlighter color changes externally
464  // Update UI buttons to reflect the new color using ParaView-style icons
465  if (!m_highlighter) return;
466 
468  static_cast<cvSelectionHighlighter::HighlightMode>(mode);
469  QColor color = m_highlighter->getHighlightQColor(hlMode);
470 
471  switch (mode) {
473  if (m_hoverColorButton)
474  updateColorButtonIcon(m_hoverColorButton, color);
475  if (m_interactiveSelectionColorButton)
476  updateColorButtonIcon(m_interactiveSelectionColorButton, color);
477  break;
479  if (m_preselectedColorButton)
480  updateColorButtonIcon(m_preselectedColorButton, color);
481  break;
483  if (m_selectedColorButton)
484  updateColorButtonIcon(m_selectedColorButton, color);
485  if (m_selectionColorButton)
486  updateColorButtonIcon(m_selectionColorButton, color);
487  break;
489  if (m_boundaryColorButton)
490  updateColorButtonIcon(m_boundaryColorButton, color);
491  break;
492  }
493 
494  CVLog::PrintVerbose(QString("[cvSelectionPropertiesWidget] UI updated for "
495  "external color change (mode=%1)")
496  .arg(mode));
497 }
498 
499 //-----------------------------------------------------------------------------
500 void cvSelectionPropertiesWidget::onHighlighterOpacityChanged(int mode) {
501  // Called when highlighter opacity changes externally
502  // Update UI spinboxes to reflect the new opacity
503  if (!m_highlighter) return;
504 
506  static_cast<cvSelectionHighlighter::HighlightMode>(mode);
507  double opacity = m_highlighter->getHighlightOpacity(hlMode);
508 
509  QDoubleSpinBox* spinbox = nullptr;
510  switch (mode) {
512  spinbox = m_hoverOpacitySpin;
513  break;
515  spinbox = m_preselectedOpacitySpin;
516  break;
518  spinbox = m_selectedOpacitySpin;
519  break;
521  spinbox = m_boundaryOpacitySpin;
522  break;
523  }
524 
525  if (spinbox) {
526  spinbox->blockSignals(true);
527  spinbox->setValue(opacity);
528  spinbox->blockSignals(false);
529  }
530 }
531 
532 //-----------------------------------------------------------------------------
533 void cvSelectionPropertiesWidget::onHighlighterLabelPropertiesChanged(
534  bool interactive) {
535  // Called when highlighter label properties change externally
536  // This could trigger a full UI sync if needed
538  QString("[cvSelectionPropertiesWidget] Label properties "
539  "changed externally (interactive=%1)")
540  .arg(interactive));
541  // For now, just log - UI will be updated on next syncUIWithHighlighter()
542  // call
543 }
544 
545 //-----------------------------------------------------------------------------
546 QColor cvSelectionPropertiesWidget::getSelectionColor() const {
547  if (m_highlighter) {
548  return m_highlighter->getHighlightQColor(
550  }
551  return QColor(255, 0, 255); // Default magenta
552 }
553 
554 //-----------------------------------------------------------------------------
555 QColor cvSelectionPropertiesWidget::getInteractiveSelectionColor() const {
556  if (m_highlighter) {
557  return m_highlighter->getHighlightQColor(cvSelectionHighlighter::HOVER);
558  }
559  return QColor(0, 255, 0); // Default green
560 }
561 
562 //-----------------------------------------------------------------------------
564  if (!m_highlighter) {
565  return;
566  }
567 
568  // Get current colors from highlighter (single source of truth)
569  QColor hoverColor =
571  QColor preselectedColor = m_highlighter->getHighlightQColor(
573  QColor selectedColor =
575  QColor boundaryColor =
577 
578  double hoverOpacity =
580  double preselectedOpacity = m_highlighter->getHighlightOpacity(
582  double selectedOpacity = m_highlighter->getHighlightOpacity(
584  double boundaryOpacity = m_highlighter->getHighlightOpacity(
586 
587  // Update UI controls - colors are read directly from highlighter
588  // Use ParaView-style icon-based color buttons
589  if (m_hoverColorButton) {
590  updateColorButtonIcon(m_hoverColorButton, hoverColor);
591  }
592  if (m_preselectedColorButton) {
593  updateColorButtonIcon(m_preselectedColorButton, preselectedColor);
594  }
595  if (m_selectedColorButton) {
596  updateColorButtonIcon(m_selectedColorButton, selectedColor);
597  }
598  if (m_boundaryColorButton) {
599  updateColorButtonIcon(m_boundaryColorButton, boundaryColor);
600  }
601 
602  // Update opacity spinboxes
603  if (m_hoverOpacitySpin) {
604  m_hoverOpacitySpin->blockSignals(true);
605  m_hoverOpacitySpin->setValue(hoverOpacity);
606  m_hoverOpacitySpin->blockSignals(false);
607  }
608  if (m_preselectedOpacitySpin) {
609  m_preselectedOpacitySpin->blockSignals(true);
610  m_preselectedOpacitySpin->setValue(preselectedOpacity);
611  m_preselectedOpacitySpin->blockSignals(false);
612  }
613  if (m_selectedOpacitySpin) {
614  m_selectedOpacitySpin->blockSignals(true);
615  m_selectedOpacitySpin->setValue(selectedOpacity);
616  m_selectedOpacitySpin->blockSignals(false);
617  }
618  if (m_boundaryOpacitySpin) {
619  m_boundaryOpacitySpin->blockSignals(true);
620  m_boundaryOpacitySpin->setValue(boundaryOpacity);
621  m_boundaryOpacitySpin->blockSignals(false);
622  }
623 
624  // Label properties are now stored in highlighter (single source of truth)
625  // No local copy needed - dialog will read directly from highlighter
626 
627  // Update ParaView-style selection color buttons using icons
628  if (m_selectionColorButton) {
629  updateColorButtonIcon(m_selectionColorButton, selectedColor);
630  }
631  if (m_interactiveSelectionColorButton) {
632  updateColorButtonIcon(m_interactiveSelectionColorButton, hoverColor);
633  }
634 
636  "[cvSelectionPropertiesWidget] UI synchronized with highlighter "
637  "settings");
638 }
639 
640 //-----------------------------------------------------------------------------
641 void cvSelectionPropertiesWidget::setupUi() {
642  QVBoxLayout* mainLayout = new QVBoxLayout(this);
643  mainLayout->setContentsMargins(0, 0, 0, 0);
644  mainLayout->setSpacing(0);
645 
646  // Create scroll area for ParaView-style layout
647  m_scrollArea = new QScrollArea(this);
648  // Set widgetResizable to false so content maintains its natural size
649  // This allows scrollbars to appear when content exceeds available space
650  m_scrollArea->setWidgetResizable(false);
651  m_scrollArea->setFrameShape(QFrame::NoFrame);
652  m_scrollArea->setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff);
653  m_scrollArea->setVerticalScrollBarPolicy(Qt::ScrollBarAsNeeded);
654 
655  m_scrollContent = new QWidget();
656  // Set size policy to allow content to expand naturally based on its
657  // contents
658  m_scrollContent->setSizePolicy(QSizePolicy::Preferred,
660  QVBoxLayout* scrollLayout = new QVBoxLayout(m_scrollContent);
661  scrollLayout->setContentsMargins(5, 5, 5, 5);
662  scrollLayout->setSpacing(
663  0); // ParaView-style: no spacing between expanders
664  // Set size constraint to ensure layout calculates minimum size correctly
665  scrollLayout->setSizeConstraint(QLayout::SetMinimumSize);
666 
667  // === ParaView-style sections with cvExpanderButton ===
668 
669  // 1. Create Selection section (ParaView's Find Data) - first section
670  m_createSelectionExpander = new cvExpanderButton(m_scrollContent);
671  m_createSelectionExpander->setText(tr("Create Selection"));
672  m_createSelectionExpander->setChecked(true); // Expanded by default
673  scrollLayout->addWidget(m_createSelectionExpander);
674 
675  m_createSelectionContainer = new QWidget(m_scrollContent);
676  m_createSelectionContainer->setMinimumHeight(50);
677  setupCreateSelectionSection();
678  scrollLayout->addWidget(m_createSelectionContainer);
679 
680  connect(m_createSelectionExpander, &cvExpanderButton::toggled,
681  m_createSelectionContainer, &QWidget::setVisible);
682  // Also connect to show/hide the action buttons below
683  connect(m_createSelectionExpander, &cvExpanderButton::toggled,
684  [this](bool checked) {
685  if (m_findDataButton) m_findDataButton->setVisible(checked);
686  if (m_resetButton) m_resetButton->setVisible(checked);
687  if (m_clearButton) m_clearButton->setVisible(checked);
688  // Update scroll content size when section is toggled
689  // Ensure width is maintained and content size is updated
690  QTimer::singleShot(0, this, [this]() {
691  // First ensure width is set, then adjust size
692  updateScrollContentWidth();
693  });
694  });
695 
696  // 2. Selected Data section (with Freeze/Extract/Plot Over Time buttons)
697  m_selectedDataSpreadsheetExpander = new cvExpanderButton(m_scrollContent);
698  m_selectedDataSpreadsheetExpander->setText(tr("Selected Data (none)"));
699  m_selectedDataSpreadsheetExpander->setChecked(true);
700  scrollLayout->addWidget(m_selectedDataSpreadsheetExpander);
701 
702  m_selectedDataSpreadsheetContainer = new QWidget(m_scrollContent);
703  m_selectedDataSpreadsheetContainer->setMinimumHeight(200);
704  setupSelectedDataSpreadsheet();
705  scrollLayout->addWidget(m_selectedDataSpreadsheetContainer);
706 
707  connect(m_selectedDataSpreadsheetExpander, &cvExpanderButton::toggled,
708  m_selectedDataSpreadsheetContainer, &QWidget::setVisible);
709  // Update scroll content size when section is toggled
710  connect(m_selectedDataSpreadsheetExpander, &cvExpanderButton::toggled,
711  [this](bool) {
712  QTimer::singleShot(0, this, [this]() {
713  // First ensure width is set, then adjust size
714  updateScrollContentWidth();
715  });
716  });
717 
718  // Action buttons row (Freeze, Extract, Plot Over Time)
719  setupSelectedDataHeader();
720  QHBoxLayout* buttonLayout = new QHBoxLayout();
721  buttonLayout->setSpacing(2);
722  buttonLayout->addWidget(m_freezeButton);
723  buttonLayout->addWidget(m_extractButton);
724  if (m_plotOverTimeButton) {
725  buttonLayout->addWidget(m_plotOverTimeButton);
726  }
727  QWidget* buttonContainer = new QWidget(m_scrollContent);
728  buttonContainer->setLayout(buttonLayout);
729  scrollLayout->addWidget(buttonContainer);
730 
731  // 3. Selection Display section
732  m_selectionDisplayExpander = new cvExpanderButton(m_scrollContent);
733  m_selectionDisplayExpander->setText(tr("Selection Display"));
734  m_selectionDisplayExpander->setChecked(true);
735  scrollLayout->addWidget(m_selectionDisplayExpander);
736 
737  m_selectionDisplayContainer = new QWidget(m_scrollContent);
738  m_selectionDisplayContainer->setMinimumHeight(50);
739  setupSelectionDisplaySection();
740  scrollLayout->addWidget(m_selectionDisplayContainer);
741 
742  connect(m_selectionDisplayExpander, &cvExpanderButton::toggled,
743  m_selectionDisplayContainer, &QWidget::setVisible);
744  // Update scroll content size when section is toggled
745  connect(m_selectionDisplayExpander, &cvExpanderButton::toggled,
746  [this](bool) {
747  QTimer::singleShot(0, this, [this]() {
748  // First ensure width is set, then adjust size
749  updateScrollContentWidth();
750  });
751  });
752 
753  // 4. Selection Editor section (for combining and managing selections)
754  m_selectionEditorExpander = new cvExpanderButton(m_scrollContent);
755  m_selectionEditorExpander->setText(tr("Selection Editor"));
756  m_selectionEditorExpander->setChecked(false); // Collapsed by default
757  scrollLayout->addWidget(m_selectionEditorExpander);
758 
759  m_selectionEditorContainer = new QWidget(m_scrollContent);
760  m_selectionEditorContainer->setMinimumHeight(50);
761  m_selectionEditorContainer->setVisible(false); // Hidden by default
762  setupSelectionEditorSection();
763  scrollLayout->addWidget(m_selectionEditorContainer);
764 
765  connect(m_selectionEditorExpander, &cvExpanderButton::toggled,
766  m_selectionEditorContainer, &QWidget::setVisible);
767  // Update scroll content size when section is toggled
768  connect(m_selectionEditorExpander, &cvExpanderButton::toggled,
769  [this](bool) {
770  QTimer::singleShot(0, this, [this]() {
771  // First ensure width is set, then adjust size
772  updateScrollContentWidth();
773  });
774  });
775 
776  // 5. Compact Statistics Section (ParaView-style: no tabs)
777  m_compactStatsExpander = new cvExpanderButton(m_scrollContent);
778  m_compactStatsExpander->setText(tr("Selection Statistics"));
779  m_compactStatsExpander->setChecked(false); // Collapsed by default
780  scrollLayout->addWidget(m_compactStatsExpander);
781 
782  m_compactStatsContainer = new QWidget(m_scrollContent);
783  m_compactStatsContainer->setMinimumHeight(50);
784  m_compactStatsContainer->setVisible(false); // Hidden by default
785  setupCompactStatisticsSection();
786  scrollLayout->addWidget(m_compactStatsContainer);
787 
788  connect(m_compactStatsExpander, &cvExpanderButton::toggled,
789  m_compactStatsContainer, &QWidget::setVisible);
790  // Update scroll content size when section is toggled
791  connect(m_compactStatsExpander, &cvExpanderButton::toggled, [this](bool) {
792  QTimer::singleShot(0, this, [this]() {
793  // First ensure width is set, then adjust size
794  updateScrollContentWidth();
795  });
796  });
797 
798  // Note: Tab widget removed to align with ParaView's simpler UI design
799  // Export/Advanced features are now accessible via action buttons or menus
800  m_tabWidget = nullptr;
801 
802  scrollLayout->addStretch();
803 
804  m_scrollContent->setLayout(scrollLayout);
805 
806  m_scrollArea->setWidget(m_scrollContent);
807  // Set the scroll area to expand and fill available space
808  m_scrollArea->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Expanding);
809  mainLayout->addWidget(m_scrollArea);
810 
811  // Install event filter on scroll area and viewport to detect resize events
812  // This ensures content width matches scroll area width (prevents horizontal
813  // scrolling) while allowing vertical scrolling when content height exceeds
814  // available space
815  m_scrollArea->installEventFilter(this);
816  if (m_scrollArea->viewport()) {
817  m_scrollArea->viewport()->installEventFilter(this);
818  }
819 
820  // Update scroll content size after everything is set up
821  // Use QTimer to ensure this happens after the widget is shown
822  QTimer::singleShot(0, this, [this]() { updateScrollContentWidth(); });
823 
824  setLayout(mainLayout);
825 }
826 
827 //-----------------------------------------------------------------------------
828 void cvSelectionPropertiesWidget::setupSelectedDataHeader() {
829  // Selected Data header label
830  m_selectedDataLabel = new QLabel(tr("<b>Selected Data</b>"));
831  m_selectedDataLabel->setStyleSheet(
832  "QLabel { background-color: #e0e0e0; padding: 5px; border-radius: "
833  "3px; }");
834 
835  // Action buttons (ParaView-style with icons)
836 
837  // Freeze button (ParaView: converts selection to a frozen representation)
838  m_freezeButton = new QPushButton(QIcon(":/Resources/images/svg/pqLock.svg"),
839  tr("Freeze"));
840  m_freezeButton->setToolTip(tr(
841  "Freeze the current selection (convert to independent dataset)"));
842  m_freezeButton->setFixedHeight(25);
843  m_freezeButton->setEnabled(false); // Enabled when selection exists
844  connect(m_freezeButton, &QPushButton::clicked, this,
845  &cvSelectionPropertiesWidget::onFreezeClicked);
846 
847  // Extract button (ParaView: pqExtractSelection.svg - creates new object
848  // from selection)
849  QIcon extractIcon(":/Resources/images/svg/pqExtractSelection.svg");
850  if (extractIcon.isNull()) {
851  extractIcon = QIcon(":/Resources/images/exportCloud.png"); // Fallback
852  }
853  m_extractButton = new QPushButton(extractIcon, tr("Extract"));
854  m_extractButton->setToolTip(
855  tr("Extract selected elements to a new dataset and add to scene"));
856  m_extractButton->setFixedHeight(25);
857  m_extractButton->setEnabled(false); // Enabled when selection exists
858  connect(m_extractButton, &QPushButton::clicked, this,
859  &cvSelectionPropertiesWidget::onExtractClicked);
860 
861  // Note: Plot Distribution button removed - feature not fully implemented
862  // Can be re-added when histogram plotting is available
863 }
864 
865 //-----------------------------------------------------------------------------
866 void cvSelectionPropertiesWidget::setupCompactStatisticsSection() {
867  // Compact statistics section (replaces Statistics tab)
868  // Content is placed inside m_compactStatsContainer which is controlled by
869  // cvExpanderButton No QGroupBox needed - the expander handles collapsing
870 
871  QFormLayout* statsLayout = new QFormLayout(m_compactStatsContainer);
872  statsLayout->setSpacing(4);
873  statsLayout->setContentsMargins(8, 8, 8, 8);
874 
875  m_countLabel = new QLabel(tr("0"));
876  m_countLabel->setStyleSheet("font-weight: bold;");
877  statsLayout->addRow(tr("Count:"), m_countLabel);
878 
879  m_typeLabel = new QLabel(tr("None"));
880  statsLayout->addRow(tr("Type:"), m_typeLabel);
881 
882  m_boundsLabel = new QLabel(tr("N/A"));
883  m_boundsLabel->setWordWrap(true);
884  m_boundsLabel->setStyleSheet("font-size: 9pt;");
885  statsLayout->addRow(tr("Bounds:"), m_boundsLabel);
886 
887  m_centerLabel = new QLabel(tr("N/A"));
888  m_centerLabel->setStyleSheet("font-size: 9pt;");
889  statsLayout->addRow(tr("Center:"), m_centerLabel);
890 
891  m_volumeLabel = new QLabel(tr("N/A"));
892  m_volumeLabel->setStyleSheet("font-size: 9pt;");
893  statsLayout->addRow(tr("Volume:"), m_volumeLabel);
894 
895  // No need for setupCollapsibleGroupBox - handled by cvExpanderButton
896 }
897 
898 //-----------------------------------------------------------------------------
899 void cvSelectionPropertiesWidget::setupCreateSelectionSection() {
900  // Create Selection section - ParaView's "Find Data" functionality
901  // Uses m_createSelectionContainer instead of QGroupBox for ParaView-style
902  // collapsible behavior with cvExpanderButton
903  m_createSelectionGroup = nullptr; // Not using QGroupBox anymore
904 
905  QVBoxLayout* mainLayout = new QVBoxLayout(m_createSelectionContainer);
906  mainLayout->setSpacing(5);
907  mainLayout->setContentsMargins(8, 8, 8, 8);
908 
909  // === Selection Criteria ===
910  QLabel* criteriaLabel = new QLabel(tr("<b>Selection Criteria</b>"));
911  mainLayout->addWidget(criteriaLabel);
912 
913  QFormLayout* criteriaLayout = new QFormLayout();
914  criteriaLayout->setSpacing(3);
915 
916  // Data Producer combo
917  m_dataProducerCombo = new QComboBox();
918  m_dataProducerCombo->setToolTip(tr("Select the data source"));
919  connect(m_dataProducerCombo,
920  QOverload<int>::of(&QComboBox::currentIndexChanged), this,
921  &cvSelectionPropertiesWidget::onDataProducerChanged);
922  criteriaLayout->addRow(tr("Data Producer"), m_dataProducerCombo);
923 
924  // Element Type combo (Point/Cell)
925  m_elementTypeCombo = new QComboBox();
926  QIcon pointIcon(":/Resources/images/svg/pqPointData.svg");
927  m_elementTypeCombo->addItem(pointIcon, tr("Point"), 0);
928 
929  QIcon cellIcon(":/Resources/images/svg/pqCellData.svg");
930  m_elementTypeCombo->addItem(cellIcon, tr("Cell"), 1);
931  m_elementTypeCombo->setToolTip(tr("Select element type (Point or Cell)"));
932  connect(m_elementTypeCombo,
933  QOverload<int>::of(&QComboBox::currentIndexChanged), this,
934  &cvSelectionPropertiesWidget::onElementTypeChanged);
935  criteriaLayout->addRow(tr("Element Type"), m_elementTypeCombo);
936 
937  mainLayout->addLayout(criteriaLayout);
938 
939  // Container for query rows (multiple conditions with +/- buttons)
940  m_queriesLayout = new QVBoxLayout();
941  m_queriesLayout->setSpacing(3);
942  mainLayout->addLayout(m_queriesLayout);
943 
944  // Add the first query row
945  addQueryRow();
946 
947  // Keep legacy pointers for compatibility (pointing to first row)
948  if (!m_queryRows.isEmpty()) {
949  m_attributeCombo = m_queryRows[0].attributeCombo;
950  m_operatorCombo = m_queryRows[0].operatorCombo;
951  m_valueEdit = m_queryRows[0].valueEdit;
952  } else {
953  m_attributeCombo = nullptr;
954  m_operatorCombo = nullptr;
955  m_valueEdit = nullptr;
956  }
957 
958  // === Selection Qualifiers ===
959  QLabel* qualifiersLabel = new QLabel(tr("<b>Selection Qualifiers</b>"));
960  mainLayout->addWidget(qualifiersLabel);
961 
962  QFormLayout* qualifiersLayout = new QFormLayout();
963  qualifiersLayout->setSpacing(3);
964 
965  // Process ID
966  m_processIdSpinBox = new QSpinBox();
967  m_processIdSpinBox->setRange(-1, 9999);
968  m_processIdSpinBox->setValue(-1);
969  m_processIdSpinBox->setToolTip(tr("Process ID (-1 for all)"));
970  qualifiersLayout->addRow(tr("Process ID"), m_processIdSpinBox);
971 
972  mainLayout->addLayout(qualifiersLayout);
973 
974  // Action buttons: Find Data | Reset | Clear
975  QHBoxLayout* buttonLayout = new QHBoxLayout();
976  buttonLayout->setSpacing(5);
977 
978  // Use ParaView-style icons
979  QIcon findIcon(":/Resources/images/svg/pqApply.svg");
980  if (findIcon.isNull()) {
981  findIcon = QIcon(":/Resources/images/svg/pqApply.png");
982  }
983  m_findDataButton = new QPushButton(findIcon, tr("Find Data"));
984  m_findDataButton->setToolTip(tr("Find data using selection criteria"));
985  connect(m_findDataButton, &QPushButton::clicked, this,
986  &cvSelectionPropertiesWidget::onFindDataClicked);
987  buttonLayout->addWidget(m_findDataButton);
988 
989  QIcon resetIcon(":/Resources/images/svg/pqCancel.svg");
990  if (resetIcon.isNull()) {
991  resetIcon = QIcon(":/Resources/images/svg/pqCancel.png");
992  }
993  m_resetButton = new QPushButton(resetIcon, tr("Reset"));
994  m_resetButton->setToolTip(tr("Reset any unaccepted changes"));
995  connect(m_resetButton, &QPushButton::clicked, this,
996  &cvSelectionPropertiesWidget::onResetClicked);
997  buttonLayout->addWidget(m_resetButton);
998 
999  QIcon clearIcon(":/Resources/images/svg/pqReset.svg");
1000  if (clearIcon.isNull()) {
1001  clearIcon = QIcon(":/Resources/images/svg/pqReset.png");
1002  }
1003  m_clearButton = new QPushButton(clearIcon, tr("Clear"));
1004  m_clearButton->setToolTip(tr("Clear selection criteria and qualifiers"));
1005  connect(m_clearButton, &QPushButton::clicked, this,
1006  &cvSelectionPropertiesWidget::onClearClicked);
1007  buttonLayout->addWidget(m_clearButton);
1008 
1009  mainLayout->addLayout(buttonLayout);
1010 
1011  // Layout is already set on m_createSelectionContainer
1012  // No need for setupCollapsibleGroupBox - handled by cvExpanderButton
1013 }
1014 
1015 //-----------------------------------------------------------------------------
1016 void cvSelectionPropertiesWidget::setupSelectionDisplaySection() {
1017  // Uses m_selectionDisplayContainer instead of QGroupBox for ParaView-style
1018  // collapsible behavior with cvExpanderButton
1019  m_selectionDisplayGroup = nullptr; // Not using QGroupBox anymore
1020 
1021  QVBoxLayout* displayLayout = new QVBoxLayout(m_selectionDisplayContainer);
1022  displayLayout->setSpacing(0); // ParaView-style: compact spacing
1023  displayLayout->setContentsMargins(8, 8, 8, 8);
1024 
1025  // === Selection Labels === (ParaView-style header with separator line)
1026  QVBoxLayout* labelsHeaderLayout = new QVBoxLayout();
1027  labelsHeaderLayout->setSpacing(0);
1028  QLabel* labelsHeader = new QLabel(
1029  tr("<html><body><p><span style=\"font-weight:600;\">Selection "
1030  "Labels</span></p></body></html>"));
1031  labelsHeaderLayout->addWidget(labelsHeader);
1032  QFrame* labelsSeparator = new QFrame();
1033  labelsSeparator->setFrameShape(QFrame::HLine);
1034  labelsSeparator->setFrameShadow(QFrame::Sunken);
1035  labelsHeaderLayout->addWidget(labelsSeparator);
1036  displayLayout->addLayout(labelsHeaderLayout);
1037 
1038  // Cell Labels and Point Labels buttons (ParaView-style: horizontal,
1039  // spacing=2)
1040  QHBoxLayout* labelsLayout = new QHBoxLayout();
1041  labelsLayout->setSpacing(2);
1042 
1043  // Cell Labels button with dropdown menu (ParaView: pqCellData.svg icon)
1044  m_cellLabelsButton = new QPushButton(tr("Cell Labels"));
1045  m_cellLabelsButton->setIcon(QIcon(":/Resources/images/svg/pqCellData.svg"));
1046  m_cellLabelsButton->setToolTip(
1047  tr("Set the array to label selected cells with"));
1048  m_cellLabelsMenu = new QMenu(this);
1049  m_cellLabelsButton->setMenu(m_cellLabelsMenu);
1050  // Populate menu when about to show (NOT when button clicked)
1051  connect(m_cellLabelsMenu, &QMenu::aboutToShow, this,
1052  &cvSelectionPropertiesWidget::onCellLabelsClicked);
1053  labelsLayout->addWidget(m_cellLabelsButton);
1054 
1055  // Point Labels button with dropdown menu (ParaView: pqPointData.svg icon)
1056  m_pointLabelsButton = new QPushButton(tr("Point Labels"));
1057  m_pointLabelsButton->setIcon(
1058  QIcon(":/Resources/images/svg/pqPointData.svg"));
1059  m_pointLabelsButton->setToolTip(
1060  tr("Set the array to label selected points with"));
1061  m_pointLabelsMenu = new QMenu(this);
1062  m_pointLabelsButton->setMenu(m_pointLabelsMenu);
1063  // Populate menu when about to show (NOT when button clicked)
1064  connect(m_pointLabelsMenu, &QMenu::aboutToShow, this,
1065  &cvSelectionPropertiesWidget::onPointLabelsClicked);
1066  labelsLayout->addWidget(m_pointLabelsButton);
1067 
1068  displayLayout->addLayout(labelsLayout);
1069 
1070  // Edit Label Properties button (ParaView: pqAdvanced.svg icon)
1071  m_editLabelPropertiesButton = new QPushButton(tr("Edit Label Properties"));
1072  m_editLabelPropertiesButton->setIcon(
1073  QIcon(":/Resources/images/svg/pqAdvanced.png"));
1074  m_editLabelPropertiesButton->setToolTip(
1075  tr("Edit selection label properties"));
1076  connect(m_editLabelPropertiesButton, &QPushButton::clicked, this,
1077  &cvSelectionPropertiesWidget::onEditLabelPropertiesClicked);
1078  displayLayout->addWidget(m_editLabelPropertiesButton);
1079 
1080  // === Selection Appearance === (ParaView-style header with separator line)
1081  QVBoxLayout* appearanceHeaderLayout = new QVBoxLayout();
1082  appearanceHeaderLayout->setSpacing(0);
1083  QLabel* appearanceHeader = new QLabel(
1084  tr("<html><body><p><span style=\"font-weight:600;\">Selection "
1085  "Appearance</span></p></body></html>"));
1086  appearanceHeaderLayout->addWidget(appearanceHeader);
1087  QFrame* appearanceSeparator = new QFrame();
1088  appearanceSeparator->setFrameShape(QFrame::HLine);
1089  appearanceSeparator->setFrameShadow(QFrame::Sunken);
1090  appearanceHeaderLayout->addWidget(appearanceSeparator);
1091  displayLayout->addLayout(appearanceHeaderLayout);
1092 
1093  // Selection Color button (ParaView: pqColorChooserButton style)
1094  // Use QToolButton like ParaView's pqColorChooserButton (which extends
1095  // QToolButton)
1096  m_selectionColorButton = new QToolButton();
1097  m_selectionColorButton->setText(tr("Selection Color"));
1098  m_selectionColorButton->setToolTip(
1099  tr("Set the color to use to show selected elements"));
1100  m_selectionColorButton->setSizePolicy(QSizePolicy::Minimum,
1101  QSizePolicy::Fixed);
1102  // ParaView style: TextBesideIcon - text and icon side by side
1103  m_selectionColorButton->setToolButtonStyle(Qt::ToolButtonTextBesideIcon);
1104  // Install event filter to handle resize events (like ParaView's
1105  // pqColorChooserButton)
1106  m_selectionColorButton->installEventFilter(this);
1107  // ParaView style: use icon to display color (will be updated when
1108  // highlighter is set) Default color (magenta) - will be updated when
1109  // highlighter is set
1110  updateColorButtonIcon(m_selectionColorButton,
1111  QColor(255, 0, 255)); // Magenta default
1112  connect(m_selectionColorButton, &QToolButton::clicked, this,
1113  &cvSelectionPropertiesWidget::onSelectionColorClicked);
1114  displayLayout->addWidget(m_selectionColorButton);
1115 
1116  // === Interactive Selection === (ParaView-style header with separator line)
1117  QVBoxLayout* interactiveHeaderLayout = new QVBoxLayout();
1118  interactiveHeaderLayout->setSpacing(0);
1119  QLabel* interactiveHeader = new QLabel(
1120  tr("<html><body><p><span style=\"font-weight:600;\">Interactive "
1121  "Selection</span></p></body></html>"));
1122  interactiveHeaderLayout->addWidget(interactiveHeader);
1123  QFrame* interactiveSeparator = new QFrame();
1124  interactiveSeparator->setFrameShape(QFrame::HLine);
1125  interactiveSeparator->setFrameShadow(QFrame::Sunken);
1126  interactiveHeaderLayout->addWidget(interactiveSeparator);
1127  displayLayout->addLayout(interactiveHeaderLayout);
1128 
1129  // Interactive Selection Color button (ParaView: pqColorChooserButton style)
1130  // Use QToolButton like ParaView's pqColorChooserButton (which extends
1131  // QToolButton)
1132  m_interactiveSelectionColorButton = new QToolButton();
1133  m_interactiveSelectionColorButton->setText(
1134  tr("Interactive Selection Color"));
1135  m_interactiveSelectionColorButton->setToolTip(
1136  tr("Set the color to use to show selected elements during "
1137  "interaction"));
1138  m_interactiveSelectionColorButton->setSizePolicy(QSizePolicy::Minimum,
1139  QSizePolicy::Fixed);
1140  // ParaView style: TextBesideIcon - text and icon side by side
1141  m_interactiveSelectionColorButton->setToolButtonStyle(
1142  Qt::ToolButtonTextBesideIcon);
1143  // Install event filter to handle resize events (like ParaView's
1144  // pqColorChooserButton)
1145  m_interactiveSelectionColorButton->installEventFilter(this);
1146  // ParaView style: use icon to display color (will be updated when
1147  // highlighter is set) Default color (cyan) - will be updated when
1148  // highlighter is set
1149  updateColorButtonIcon(m_interactiveSelectionColorButton,
1150  QColor(0, 255, 255)); // Cyan default
1151  connect(m_interactiveSelectionColorButton, &QToolButton::clicked, this,
1152  &cvSelectionPropertiesWidget::onInteractiveSelectionColorClicked);
1153  displayLayout->addWidget(m_interactiveSelectionColorButton);
1154 
1155  // Edit Interactive Label Properties button (ParaView: pqAdvanced.svg icon)
1156  m_editInteractiveLabelPropertiesButton =
1157  new QPushButton(tr("Edit Interactive Label Properties"));
1158  m_editInteractiveLabelPropertiesButton->setIcon(
1159  QIcon(":/Resources/images/svg/pqAdvanced.png"));
1160  m_editInteractiveLabelPropertiesButton->setToolTip(
1161  tr("Edit interactive selection label properties"));
1162  connect(m_editInteractiveLabelPropertiesButton, &QPushButton::clicked, this,
1164  onEditInteractiveLabelPropertiesClicked);
1165  displayLayout->addWidget(m_editInteractiveLabelPropertiesButton);
1166 
1167  // Add vertical spacer at bottom (ParaView-style)
1168  displayLayout->addStretch();
1169 
1170  // Layout is already set on m_selectionDisplayContainer
1171  // No need for setupCollapsibleGroupBox - handled by cvExpanderButton
1172 }
1173 
1174 //-----------------------------------------------------------------------------
1175 void cvSelectionPropertiesWidget::setupSelectionEditorSection() {
1176  // Content is placed inside m_selectionEditorContainer which is controlled
1177  // by cvExpanderButton No QGroupBox needed - the expander handles collapsing
1178 
1179  QVBoxLayout* editorLayout = new QVBoxLayout(m_selectionEditorContainer);
1180  editorLayout->setSpacing(5); // ParaView-style spacing
1181  editorLayout->setContentsMargins(8, 8, 8, 8);
1182 
1183  // Data Producer row (ParaView-style: spacing=5)
1184  QHBoxLayout* producerLayout = new QHBoxLayout();
1185  producerLayout->setSpacing(5);
1186  m_dataProducerLabel = new QLabel(tr("Data Producer"));
1187  m_dataProducerLabel->setSizePolicy(QSizePolicy::Maximum,
1188  QSizePolicy::Preferred);
1189  m_dataProducerLabel->setToolTip(
1190  tr("The dataset for which selections are saved"));
1191  m_dataProducerValue = new QLabel();
1192  m_dataProducerValue->setStyleSheet(
1193  "QLabel { background-color: snow; border: 1px inset grey; "
1194  "}"); // ParaView exact style
1195  m_dataProducerValue->setToolTip(
1196  tr("The dataset for which selections are saved"));
1197  m_dataProducerValue->setText(
1198  m_dataProducerName.isEmpty() ? QString() : m_dataProducerName);
1199  producerLayout->addWidget(m_dataProducerLabel);
1200  producerLayout->addWidget(m_dataProducerValue, 1);
1201  editorLayout->addLayout(producerLayout);
1202 
1203  // Element Type row (ParaView-style: spacing=9)
1204  QHBoxLayout* elementTypeLayout = new QHBoxLayout();
1205  elementTypeLayout->setSpacing(9);
1206  m_elementTypeLabel = new QLabel(tr("Element Type"));
1207  m_elementTypeLabel->setSizePolicy(QSizePolicy::Maximum,
1208  QSizePolicy::Preferred);
1209  m_elementTypeLabel->setToolTip(
1210  tr("The element type of the saved selections"));
1211  m_elementTypeValue = new QLabel();
1212  m_elementTypeValue->setStyleSheet(
1213  "QLabel { background-color: snow; border: 1px inset grey; "
1214  "}"); // ParaView exact style
1215  m_elementTypeValue->setToolTip(
1216  tr("The element type of the saved selections"));
1217  elementTypeLayout->addWidget(m_elementTypeLabel);
1218  elementTypeLayout->addWidget(m_elementTypeValue, 1);
1219  editorLayout->addLayout(elementTypeLayout);
1220 
1221  // Expression row (ParaView-style: spacing=29)
1222  QHBoxLayout* expressionLayout = new QHBoxLayout();
1223  expressionLayout->setSpacing(29);
1224  m_expressionLabel = new QLabel(tr("Expression"));
1225  m_expressionLabel->setToolTip(
1226  tr("Specify the expression which defines the relation between "
1227  "saved selections using boolean operators: !(NOT), &(AND), "
1228  "|(OR), ^(XOR) and ()."));
1229  m_expressionEdit = new QLineEdit();
1230  m_expressionEdit->setPlaceholderText(
1231  tr("e.g., (s0|s1)&s2|(s3&s4)|s5|s6|s7"));
1232  m_expressionEdit->setToolTip(
1233  tr("Specify the expression which defines the relation between "
1234  "saved selections using boolean operators: !(NOT), &(AND), "
1235  "|(OR), ^(XOR) and ()."));
1236  connect(m_expressionEdit, &QLineEdit::textChanged, this,
1237  &cvSelectionPropertiesWidget::onExpressionChanged);
1238  expressionLayout->addWidget(m_expressionLabel);
1239  expressionLayout->addWidget(m_expressionEdit, 1);
1240  editorLayout->addLayout(expressionLayout);
1241 
1242  // Selection table with toolbar (ParaView-style: ScrollArea with HBox)
1243  QHBoxLayout* tableLayout = new QHBoxLayout();
1244  tableLayout->setContentsMargins(0, 0, 0, 0);
1245 
1246  // Table: Name, Type, Color columns (ParaView: pqExpandableTableView)
1247  m_selectionEditorTable = new QTableWidget();
1248  m_selectionEditorTable->setColumnCount(3);
1249  m_selectionEditorTable->setHorizontalHeaderLabels(
1250  {tr("Name"), tr("Type"), tr("Color")});
1251  m_selectionEditorTable->setSelectionBehavior(QAbstractItemView::SelectRows);
1252  m_selectionEditorTable->setSelectionMode(
1253  QAbstractItemView::ExtendedSelection);
1254  m_selectionEditorTable->setAlternatingRowColors(true);
1255  m_selectionEditorTable->horizontalHeader()->setStretchLastSection(true);
1256  m_selectionEditorTable->verticalHeader()->setVisible(false);
1257  m_selectionEditorTable->setMinimumHeight(120);
1258  connect(m_selectionEditorTable, &QTableWidget::itemSelectionChanged, this,
1260  onSelectionEditorTableSelectionChanged);
1261  // Handle cell clicks for color editing and row selection highlighting
1262  connect(m_selectionEditorTable, &QTableWidget::cellClicked, this,
1263  &cvSelectionPropertiesWidget::onSelectionEditorCellClicked);
1264  connect(m_selectionEditorTable, &QTableWidget::cellDoubleClicked, this,
1265  &cvSelectionPropertiesWidget::onSelectionEditorCellDoubleClicked);
1266  tableLayout->addWidget(m_selectionEditorTable);
1267 
1268  // Toolbar (vertical) - ParaView-style icons
1269  QVBoxLayout* toolbarLayout = new QVBoxLayout();
1270  toolbarLayout->setSpacing(0);
1271 
1272  // Add active selection button (ParaView: pqPlus.svg/png)
1273  m_addSelectionButton = new QToolButton();
1274  // Try SVG first, fallback to PNG if not available
1275  QIcon addIcon(":/Resources/images/svg/pqPlus.svg");
1276  if (addIcon.isNull()) {
1277  addIcon = QIcon(":/Resources/images/svg/pqPlus.png");
1278  }
1279  if (addIcon.isNull()) {
1280  addIcon = QIcon(":/Resources/images/ecvPlus.png"); // Final fallback
1281  }
1282  m_addSelectionButton->setIcon(addIcon);
1283  m_addSelectionButton->setToolTip(tr("Add active selection"));
1284  m_addSelectionButton->setIconSize(QSize(16, 16)); // Ensure visible size
1285  m_addSelectionButton->setEnabled(
1286  false); // Initially disabled until selection exists
1287  connect(m_addSelectionButton, &QToolButton::clicked, this,
1288  &cvSelectionPropertiesWidget::onAddActiveSelectionClicked);
1289  toolbarLayout->addWidget(m_addSelectionButton);
1290 
1291  // Remove selected selection button (ParaView: pqMinus.svg/png)
1292  m_removeSelectionButton = new QToolButton();
1293  QIcon removeIcon(":/Resources/images/svg/pqMinus.svg");
1294  if (removeIcon.isNull()) {
1295  removeIcon = QIcon(":/Resources/images/ecvMinus.png"); // Fallback
1296  }
1297  m_removeSelectionButton->setIcon(removeIcon);
1298  m_removeSelectionButton->setToolTip(
1299  tr("Remove selected selection from the saved selections. Remember "
1300  "to edit the Expression."));
1301  m_removeSelectionButton->setIconSize(QSize(16, 16)); // Ensure visible size
1302  m_removeSelectionButton->setEnabled(false);
1303  connect(m_removeSelectionButton, &QToolButton::clicked, this,
1304  &cvSelectionPropertiesWidget::onRemoveSelectedSelectionClicked);
1305  toolbarLayout->addWidget(m_removeSelectionButton);
1306 
1307  // Vertical spacer between buttons (ParaView-style)
1308  toolbarLayout->addStretch();
1309 
1310  // Remove all selections button (ParaView: pqDelete.svg - using
1311  // smallTrash.png as alternative)
1312  m_removeAllSelectionsButton = new QToolButton();
1313  QIcon trashIcon(":/Resources/images/smallTrash.png");
1314  if (trashIcon.isNull()) {
1315  trashIcon = QIcon(":/Resources/images/ecvdelete.png"); // Fallback
1316  }
1317  m_removeAllSelectionsButton->setIcon(trashIcon);
1318  m_removeAllSelectionsButton->setToolTip(tr("Remove all saved selections"));
1319  m_removeAllSelectionsButton->setIconSize(
1320  QSize(16, 16)); // Ensure visible size
1321  m_removeAllSelectionsButton->setEnabled(false);
1322  connect(m_removeAllSelectionsButton, &QToolButton::clicked, this,
1323  &cvSelectionPropertiesWidget::onRemoveAllSelectionsClicked);
1324  toolbarLayout->addWidget(m_removeAllSelectionsButton);
1325 
1326  tableLayout->addLayout(toolbarLayout);
1327  editorLayout->addLayout(tableLayout);
1328 
1329  // Activate Combined Selections button (ParaView: pqApply.svg icon)
1330  QHBoxLayout* activateLayout = new QHBoxLayout();
1331  activateLayout->setSpacing(2);
1332  m_activateCombinedSelectionsButton =
1333  new QPushButton(tr("Activate Combined Selections"));
1334  m_activateCombinedSelectionsButton->setIcon(
1335  QIcon(":/Resources/images/smallValidate.png"));
1336  m_activateCombinedSelectionsButton->setToolTip(
1337  tr("Set the combined saved selections as the active selection"));
1338  m_activateCombinedSelectionsButton->setFocusPolicy(Qt::TabFocus);
1339  m_activateCombinedSelectionsButton->setDefault(true);
1340  m_activateCombinedSelectionsButton->setEnabled(false);
1341  connect(m_activateCombinedSelectionsButton, &QPushButton::clicked, this,
1342  &cvSelectionPropertiesWidget::onActivateCombinedSelectionsClicked);
1343  activateLayout->addWidget(m_activateCombinedSelectionsButton);
1344  editorLayout->addLayout(activateLayout);
1345 
1346  // No need for setupCollapsibleGroupBox - handled by cvExpanderButton
1347 }
1348 
1349 //-----------------------------------------------------------------------------
1350 void cvSelectionPropertiesWidget::setupSelectedDataSpreadsheet() {
1351  // Uses m_selectedDataSpreadsheetContainer instead of QGroupBox for
1352  // ParaView-style collapsible behavior with cvExpanderButton
1353  m_selectedDataGroup = nullptr; // Not using QGroupBox anymore
1354 
1355  QGridLayout* dataLayout =
1356  new QGridLayout(m_selectedDataSpreadsheetContainer);
1357  dataLayout->setSpacing(3); // ParaView: spacing=3
1358  dataLayout->setContentsMargins(8, 8, 8, 8);
1359 
1360  // Row 0: Attribute label, combo, buttons, checkbox (ParaView:
1361  // columnstretch="0,1,0") Column 0: Attribute label
1362  QLabel* attributeLabel = new QLabel(tr(
1363  "<html><body><p><span "
1364  "style=\"font-weight:600;\">Attribute:</span></p></body></html>"));
1365  attributeLabel->setSizePolicy(QSizePolicy::Maximum, QSizePolicy::Preferred);
1366  dataLayout->addWidget(attributeLabel, 0, 0);
1367 
1368  // Column 1: Attribute combo box (ParaView-style with icons)
1369  m_attributeTypeCombo = new QComboBox();
1370 
1371  // Point Data with icon (ParaView: pqPointData.svg)
1372  QIcon pointDataIcon(":/Resources/images/svg/pqPointData.svg");
1373  m_attributeTypeCombo->addItem(pointDataIcon, tr("Point Data"), 0);
1374 
1375  // Cell Data with icon (ParaView: pqCellData.svg)
1376  QIcon cellDataIcon(":/Resources/images/svg/pqCellData.svg");
1377  m_attributeTypeCombo->addItem(cellDataIcon, tr("Cell Data"), 1);
1378 
1379  m_attributeTypeCombo->setSizeAdjustPolicy(QComboBox::AdjustToContents);
1380  m_attributeTypeCombo->setIconSize(
1381  QSize(16, 16)); // Ensure icons are visible
1382  connect(m_attributeTypeCombo,
1383  QOverload<int>::of(&QComboBox::currentIndexChanged), this,
1384  &cvSelectionPropertiesWidget::onAttributeTypeChanged);
1385  dataLayout->addWidget(m_attributeTypeCombo, 0, 1);
1386 
1387  // Column 2: Toggle column visibility button (ParaView:
1388  // pqRectilinearGrid16.png)
1389  m_toggleColumnVisibilityButton = new QToolButton();
1390  QIcon colVisIcon(":/Resources/images/interactors.png");
1391  if (colVisIcon.isNull()) {
1392  colVisIcon = QIcon(":/Resources/images/settings.png"); // Fallback
1393  }
1394  m_toggleColumnVisibilityButton->setIcon(colVisIcon);
1395  m_toggleColumnVisibilityButton->setToolTip(tr("Toggle column visibility"));
1396  m_toggleColumnVisibilityButton->setStatusTip(
1397  tr("Toggle column visibility"));
1398  m_toggleColumnVisibilityButton->setIconSize(
1399  QSize(16, 16)); // Ensure visible size
1400  m_toggleColumnVisibilityButton->setPopupMode(QToolButton::InstantPopup);
1401  connect(m_toggleColumnVisibilityButton, &QToolButton::clicked, this,
1402  &cvSelectionPropertiesWidget::onToggleColumnVisibility);
1403  dataLayout->addWidget(m_toggleColumnVisibilityButton, 0, 2);
1404 
1405  // Column 3: Toggle field data button (ParaView: pqGlobalData.svg)
1406  m_toggleFieldDataButton = new QToolButton();
1407  QIcon fieldDataIcon(":/Resources/images/svg/pqGlobalData.svg");
1408  m_toggleFieldDataButton->setIcon(fieldDataIcon);
1409  m_toggleFieldDataButton->setToolTip(
1410  tr("Toggle field data visibility (show raw data arrays)"));
1411  m_toggleFieldDataButton->setIconSize(QSize(16, 16)); // Ensure visible size
1412  m_toggleFieldDataButton->setCheckable(true);
1413  connect(m_toggleFieldDataButton, &QToolButton::toggled, this,
1414  &cvSelectionPropertiesWidget::onToggleFieldDataClicked);
1415  dataLayout->addWidget(m_toggleFieldDataButton, 0, 3);
1416 
1417  // Column 4: Invert selection checkbox
1418  m_invertSelectionCheck = new QCheckBox(tr("Invert selection"));
1419  m_invertSelectionCheck->setToolTip(tr("Invert the selection"));
1420  m_invertSelectionCheck->setEnabled(
1421  false); // ParaView: enabled=false by default
1422  connect(m_invertSelectionCheck, &QCheckBox::toggled, this,
1423  &cvSelectionPropertiesWidget::onInvertSelectionToggled);
1424  dataLayout->addWidget(m_invertSelectionCheck, 0, 4);
1425 
1426  // Row 2: Spreadsheet table (ParaView: pqSpreadSheetViewWidget, spans all 5
1427  // columns)
1428  m_spreadsheetTable = new QTableWidget();
1429  m_spreadsheetTable->setSizePolicy(QSizePolicy::Preferred,
1430  QSizePolicy::MinimumExpanding);
1431  m_spreadsheetTable->setMinimumHeight(120); // ParaView: minimumHeight=120
1432  m_spreadsheetTable->setSelectionBehavior(QAbstractItemView::SelectRows);
1433  m_spreadsheetTable->setSelectionMode(QAbstractItemView::ExtendedSelection);
1434  m_spreadsheetTable->setAlternatingRowColors(true);
1435  m_spreadsheetTable->setEditTriggers(QAbstractItemView::NoEditTriggers);
1436 
1437  // ParaView-style: Use cvMultiColumnHeaderView for header merging
1438  // This will automatically merge adjacent columns with the same DisplayRole
1439  // text
1440  cvMultiColumnHeaderView* multiColHeader =
1441  new cvMultiColumnHeaderView(Qt::Horizontal, m_spreadsheetTable);
1442  multiColHeader->setStretchLastSection(true);
1443  multiColHeader->setDefaultSectionSize(100);
1444  m_spreadsheetTable->setHorizontalHeader(multiColHeader);
1445 
1446  m_spreadsheetTable->verticalHeader()->setDefaultSectionSize(20);
1447  // ParaView-style: Remove bold from header font
1448  QFont headerFont = m_spreadsheetTable->horizontalHeader()->font();
1449  headerFont.setBold(false);
1450  m_spreadsheetTable->horizontalHeader()->setFont(headerFont);
1451  connect(m_spreadsheetTable, &QTableWidget::itemClicked, this,
1452  &cvSelectionPropertiesWidget::onSpreadsheetItemClicked);
1453  dataLayout->addWidget(m_spreadsheetTable, 2, 0, 1,
1454  5); // Row 2, spanning all 5 columns
1455 
1456  // Row 3: Action buttons (ParaView-style: Freeze | Extract | Plot Over Time)
1457  // These buttons span all 5 columns
1458  QHBoxLayout* actionLayout = new QHBoxLayout();
1459  actionLayout->setSpacing(3);
1460 
1461  // Freeze button (ParaView style)
1462  actionLayout->addWidget(m_freezeButton);
1463 
1464  // Extract button (ParaView style)
1465  actionLayout->addWidget(m_extractButton);
1466 
1467  // Plot Over Time button (ParaView style)
1468  m_plotOverTimeButton = new QPushButton(tr("Plot Over Time"));
1469  m_plotOverTimeButton->setToolTip(tr("Plot selection over time"));
1470  m_plotOverTimeButton->setEnabled(false); // Enabled when selection exists
1471  connect(m_plotOverTimeButton, &QPushButton::clicked, this,
1472  &cvSelectionPropertiesWidget::onPlotOverTimeClicked);
1473  actionLayout->addWidget(m_plotOverTimeButton);
1474 
1475  dataLayout->addLayout(actionLayout, 3, 0, 1,
1476  5); // Row 3, spanning all 5 columns
1477 
1478  // Layout is already set on m_selectedDataSpreadsheetContainer
1479  // No need for setupCollapsibleGroupBox - handled by cvExpanderButton
1480 }
1481 
1482 //-----------------------------------------------------------------------------
1484  cvViewSelectionManager* manager) {
1485  m_selectionManager = manager;
1486 
1487  // Update bookmark combo when manager is set
1488  // Bookmark functionality removed - UI not implemented
1489 
1490  // Update data producer combo
1491  updateDataProducerCombo();
1492 
1493  // Always use the manager's shared highlighter
1494  // This ensures all tools (including tooltip tools) share the same
1495  // highlighter so color settings are automatically synchronized
1496  if (m_selectionManager) {
1497  cvSelectionHighlighter* sharedHighlighter =
1498  m_selectionManager->getHighlighter();
1499  if (sharedHighlighter && sharedHighlighter != m_highlighter) {
1500  setHighlighter(sharedHighlighter);
1501  }
1502 
1503  // Initialize default label properties for annotations
1504  // Get properties from highlighter (single source of truth)
1505  cvSelectionAnnotationManager* annotations =
1506  m_selectionManager->getAnnotations();
1507  if (annotations && m_highlighter) {
1508  // Convert SelectionLabelProperties to LabelProperties for
1509  // annotations
1510  const SelectionLabelProperties& selProps =
1511  m_highlighter->getLabelProperties(false);
1513  labelProps.opacity = selProps.opacity;
1514  labelProps.pointSize = selProps.pointSize;
1515  labelProps.lineWidth = selProps.lineWidth;
1516  labelProps.cellLabelFontFamily = selProps.cellLabelFontFamily;
1517  labelProps.cellLabelFontSize = selProps.cellLabelFontSize;
1518  labelProps.cellLabelColor = selProps.cellLabelColor;
1519  annotations->setDefaultLabelProperties(labelProps,
1520  true); // cell labels
1521 
1522  const SelectionLabelProperties& interProps =
1523  m_highlighter->getLabelProperties(true);
1525  interLabelProps.opacity = interProps.opacity;
1526  interLabelProps.pointSize = interProps.pointSize;
1527  interLabelProps.lineWidth = interProps.lineWidth;
1528  interLabelProps.pointLabelFontFamily =
1529  interProps.pointLabelFontFamily;
1530  interLabelProps.pointLabelFontSize = interProps.pointLabelFontSize;
1531  interLabelProps.pointLabelColor = interProps.pointLabelColor;
1532  annotations->setDefaultLabelProperties(interLabelProps,
1533  false); // point labels
1534  }
1535  }
1536 }
1537 
1538 // updateBookmarkCombo removed - UI not implemented
1539 
1540 //-----------------------------------------------------------------------------
1542  const cvSelectionData& selectionData, vtkPolyData* polyData) {
1543  m_selectionData = selectionData;
1544 
1545  // ParaView-style: Only clear original selection for truly NEW selections
1546  // A new selection is determined by comparing IDs
1547  bool isNewSelection = m_originalSelectionIds.isEmpty() ||
1548  m_originalSelectionIds != selectionData.ids();
1549 
1550  if (isNewSelection) {
1551  // This is a brand new selection from user interaction
1552  // Clear original selection and reset invert checkbox
1553  m_originalSelectionIds.clear();
1554  if (m_invertSelectionCheck) {
1555  m_invertSelectionCheck->blockSignals(true);
1556  m_invertSelectionCheck->setChecked(false);
1557  m_invertSelectionCheck->blockSignals(false);
1558  }
1559  }
1560  // If not a new selection, preserve m_originalSelectionIds and checkbox
1561  // state
1562 
1563  if (m_selectionData.isEmpty()) {
1564  clearSelection();
1565  return false;
1566  }
1567 
1568  // Enable invert selection checkbox when there's an active selection
1569  if (m_invertSelectionCheck) {
1570  m_invertSelectionCheck->setEnabled(true);
1571  }
1572 
1573  // ParaView behavior: Auto-update element type combo based on selection type
1574  // This ensures the UI reflects the current selection's field association
1575  if (m_elementTypeCombo) {
1576  int expectedIndex =
1577  (m_selectionData.fieldAssociation() == cvSelectionData::CELLS)
1578  ? 1
1579  : 0; // 0 = Point, 1 = Cell
1580  if (m_elementTypeCombo->currentIndex() != expectedIndex) {
1581  // Block signals to prevent recursive updates
1582  m_elementTypeCombo->blockSignals(true);
1583  m_elementTypeCombo->setCurrentIndex(expectedIndex);
1584  m_elementTypeCombo->blockSignals(false);
1586  QString("[cvSelectionPropertiesWidget] Auto-switched "
1587  "element type to %1")
1588  .arg(expectedIndex == 0 ? "Point" : "Cell"));
1589  }
1590  }
1591 
1592  // Update Selection Editor UI with current selection info (ParaView style)
1593  // Update element type value label
1594  if (m_elementTypeValue) {
1595  QString elementTypeStr =
1596  (m_selectionData.fieldAssociation() == cvSelectionData::CELLS)
1597  ? tr("Cell")
1598  : tr("Point");
1599  m_elementTypeValue->setText(elementTypeStr);
1600  }
1601 
1602  // Update Data Producer from source object name (ParaView style)
1604  if (manager) {
1605  ccHObject* sourceObj = manager->getSourceObject();
1606  if (sourceObj) {
1607  QString sourceName = sourceObj->getName();
1608  setDataProducerName(sourceName);
1609 
1610  // Also update the Data Producer combo selection if exists
1611  if (m_dataProducerCombo) {
1612  int comboIndex = m_dataProducerCombo->findText(sourceName);
1613  if (comboIndex >= 0) {
1614  m_dataProducerCombo->blockSignals(true);
1615  m_dataProducerCombo->setCurrentIndex(comboIndex);
1616  m_dataProducerCombo->blockSignals(false);
1617  // IMPORTANT: Manually trigger enable/disable logic since we
1618  // blocked signals
1619  onDataProducerChanged(comboIndex);
1620  }
1621  }
1622 
1623  CVLog::PrintVerbose(QString("[cvSelectionPropertiesWidget] Updated "
1624  "Data Producer to '%1'")
1625  .arg(sourceName));
1626  }
1627  }
1628 
1629  // Get polyData if not provided (using centralized ParaView-style method)
1630  if (!polyData) {
1631  // First try from selection manager (most reliable source)
1633  if (manager) {
1634  polyData = manager->getPolyData();
1635  }
1636 
1637  // Fallback to getPolyDataForSelection
1638  if (!polyData) {
1639  polyData = getPolyDataForSelection(&m_selectionData);
1640  }
1641  }
1642 
1643  if (!polyData) {
1645  "[cvSelectionPropertiesWidget] No polyData available for "
1646  "statistics");
1647  return false;
1648  }
1649 
1650  // Validate polyData before using
1651  try {
1652  vtkIdType numPoints = polyData->GetNumberOfPoints();
1653  vtkIdType numCells = polyData->GetNumberOfCells();
1654  if (numPoints < 0 || numCells < 0) {
1655  CVLog::Warning("[cvSelectionPropertiesWidget] Invalid polyData");
1656  return false;
1657  }
1658  } catch (...) {
1660  "[cvSelectionPropertiesWidget] polyData validation failed");
1661  return false;
1662  }
1663 
1664  // Update statistics and list
1665  updateStatistics(polyData);
1666  updateSelectionList(polyData);
1667 
1668  // Update spreadsheet data (ParaView-style: auto-update on selection change)
1669  updateSpreadsheetData(polyData);
1670 
1671  // Enable export buttons
1672  bool isCells =
1673  (m_selectionData.fieldAssociation() == cvSelectionData::CELLS);
1674  bool isPoints =
1675  (m_selectionData.fieldAssociation() == cvSelectionData::POINTS);
1676 
1677  // Enable legacy advanced tab buttons (may be nullptr in simplified UI)
1678  if (m_applyAlgebraButton) {
1679  m_applyAlgebraButton->setEnabled(m_selectionCount > 0);
1680  }
1681  if (m_extractBoundaryButton) {
1682  m_extractBoundaryButton->setEnabled(isCells && m_selectionCount > 0);
1683  }
1684  // Filter button removed - UI not implemented
1685  // Bookmark button removed - UI not implemented
1686  if (m_addAnnotationButton) {
1687  m_addAnnotationButton->setEnabled(m_selectionCount > 0);
1688  }
1689 
1690  // ParaView behavior: Enable the + button when a new selection is made
1691  // This allows the user to add the new selection to the saved selections
1692  if (m_addSelectionButton && m_selectionCount > 0) {
1693  m_addSelectionButton->setEnabled(true);
1694  }
1695 
1696  // Enable header action buttons (ParaView-style)
1697  if (m_freezeButton) {
1698  m_freezeButton->setEnabled(m_selectionCount > 0);
1699  }
1700  if (m_extractButton) {
1701  m_extractButton->setEnabled(m_selectionCount > 0);
1702  }
1703  if (m_plotOverTimeButton) {
1704  m_plotOverTimeButton->setEnabled(m_selectionCount > 0);
1705  }
1706 
1707  // Enable Selection Editor + button when there's an active selection
1708  // (ParaView behavior: can only add selection when one exists)
1709  if (m_addSelectionButton) {
1710  m_addSelectionButton->setEnabled(m_selectionCount > 0);
1711  }
1712 
1713  return true;
1714 }
1715 
1716 //-----------------------------------------------------------------------------
1718  m_selectionData.clear();
1719  m_selectionCount = 0;
1720  m_selectionType = tr("None");
1721 
1722  // Clear original selection IDs for invert toggle
1723  m_originalSelectionIds.clear();
1724 
1725  // Disable and reset invert selection checkbox when selection is cleared
1726  if (m_invertSelectionCheck) {
1727  m_invertSelectionCheck->blockSignals(true);
1728  m_invertSelectionCheck->setChecked(false);
1729  m_invertSelectionCheck->setEnabled(false);
1730  m_invertSelectionCheck->blockSignals(false);
1731  }
1732 
1733  // CRITICAL: Clear labels from the 3D scene when selection is cleared
1734  if (m_highlighter) {
1735  // Clear point labels
1736  if (m_highlighter->isPointLabelVisible()) {
1737  m_highlighter->setPointLabelArray(
1738  "", false); // Empty array name + hide
1739  }
1740  // Clear cell labels
1741  if (m_highlighter->isCellLabelVisible()) {
1742  m_highlighter->setCellLabelArray("",
1743  false); // Empty array name + hide
1744  }
1745  }
1746 
1747  // Clear statistics (may be nullptr in simplified UI)
1748  if (m_countLabel) {
1749  m_countLabel->setText(QString::number(m_selectionCount));
1750  }
1751  if (m_typeLabel) {
1752  m_typeLabel->setText(m_selectionType);
1753  }
1754  if (m_boundsLabel) {
1755  m_boundsLabel->setText(tr("N/A"));
1756  }
1757  if (m_centerLabel) {
1758  m_centerLabel->setText(tr("N/A"));
1759  }
1760  if (m_volumeLabel) {
1761  m_volumeLabel->setText(tr("N/A"));
1762  }
1763 
1764  // Clear legacy table (may be nullptr in simplified UI)
1765  if (m_selectionTableWidget) {
1766  m_selectionTableWidget->clear();
1767  m_selectionTableWidget->setRowCount(0);
1768  m_selectionTableWidget->setColumnCount(0);
1769  }
1770  if (m_listInfoLabel) {
1771  m_listInfoLabel->setText(tr("No selection"));
1772  m_listInfoLabel->setStyleSheet("font-style: italic; color: gray;");
1773  }
1774 
1775  // Clear spreadsheet table (ParaView-style Selected Data)
1776  if (m_spreadsheetTable) {
1777  m_spreadsheetTable->clear();
1778  m_spreadsheetTable->setRowCount(0);
1779  m_spreadsheetTable->setColumnCount(0);
1780  }
1781 
1782  // Disable header action buttons
1783  if (m_freezeButton) {
1784  m_freezeButton->setEnabled(false);
1785  }
1786  if (m_extractButton) {
1787  m_extractButton->setEnabled(false);
1788  }
1789  if (m_plotOverTimeButton) {
1790  m_plotOverTimeButton->setEnabled(false);
1791  }
1792 
1793  // Disable Selection Editor + button when no selection
1794  if (m_addSelectionButton) {
1795  m_addSelectionButton->setEnabled(false);
1796  }
1797 }
1798 
1799 //-----------------------------------------------------------------------------
1800 void cvSelectionPropertiesWidget::updateStatistics(
1801  vtkPolyData* polyData, const cvSelectionData* customSelection) {
1802  if (!polyData) {
1804  "[cvSelectionPropertiesWidget::updateStatistics] polyData is "
1805  "nullptr");
1806  return;
1807  }
1808 
1809  // Validate polyData is still valid by checking basic properties
1810  // This helps catch use-after-free issues
1811  try {
1812  if (polyData->GetNumberOfPoints() < 0 ||
1813  polyData->GetNumberOfCells() < 0) {
1815  "[cvSelectionPropertiesWidget::updateStatistics] Invalid "
1816  "polyData state");
1817  return;
1818  }
1819  } catch (...) {
1821  "[cvSelectionPropertiesWidget::updateStatistics] polyData "
1822  "access failed - possible dangling pointer");
1823  return;
1824  }
1825 
1826  // Use custom selection if provided, otherwise use m_selectionData
1827  const cvSelectionData& selection =
1828  customSelection ? *customSelection : m_selectionData;
1829 
1830  m_selectionCount = selection.count();
1831  m_selectionType = selection.fieldTypeString();
1832 
1833  // Update labels (may be nullptr in simplified UI)
1834  if (m_countLabel) {
1835  m_countLabel->setText(QString::number(m_selectionCount));
1836  }
1837  if (m_typeLabel) {
1838  m_typeLabel->setText(m_selectionType);
1839  }
1840 
1841  // Compute bounding box
1842  if (m_selectionCount > 0) {
1843  computeBoundingBox(polyData, m_bounds);
1844 
1845  // Bounds
1846  if (m_boundsLabel) {
1847  m_boundsLabel->setText(formatBounds(m_bounds));
1848  }
1849 
1850  // Center
1851  m_center[0] = (m_bounds[0] + m_bounds[1]) / 2.0;
1852  m_center[1] = (m_bounds[2] + m_bounds[3]) / 2.0;
1853  m_center[2] = (m_bounds[4] + m_bounds[5]) / 2.0;
1854  if (m_centerLabel) {
1855  m_centerLabel->setText(QString("(%1, %2, %3)")
1856  .arg(m_center[0], 0, 'g', 6)
1857  .arg(m_center[1], 0, 'g', 6)
1858  .arg(m_center[2], 0, 'g', 6));
1859  }
1860 
1861  // Volume
1862  double dx = m_bounds[1] - m_bounds[0];
1863  double dy = m_bounds[3] - m_bounds[2];
1864  double dz = m_bounds[5] - m_bounds[4];
1865  m_volume = dx * dy * dz;
1866  if (m_volumeLabel) {
1867  m_volumeLabel->setText(QString("%1").arg(m_volume, 0, 'g', 6));
1868  }
1869  } else {
1870  if (m_boundsLabel) {
1871  m_boundsLabel->setText(tr("N/A"));
1872  }
1873  if (m_centerLabel) {
1874  m_centerLabel->setText(tr("N/A"));
1875  }
1876  if (m_volumeLabel) {
1877  m_volumeLabel->setText(tr("N/A"));
1878  }
1879  }
1880 }
1881 
1882 //-----------------------------------------------------------------------------
1883 void cvSelectionPropertiesWidget::updateSelectionList(vtkPolyData* polyData) {
1884  // Legacy UI elements check - these may be nullptr in simplified UI
1885  if (!m_selectionTableWidget) {
1886  return;
1887  }
1888 
1889  m_selectionTableWidget->clear();
1890  m_selectionTableWidget->setRowCount(0);
1891 
1892  QVector<qint64> ids = m_selectionData.ids();
1893  if (ids.isEmpty()) {
1894  m_selectionTableWidget->setColumnCount(0);
1895  if (m_listInfoLabel) {
1896  m_listInfoLabel->setText(tr("No selection"));
1897  m_listInfoLabel->setStyleSheet("font-style: italic; color: gray;");
1898  }
1899  return;
1900  }
1901 
1902  bool isPoints =
1903  (m_selectionData.fieldAssociation() == cvSelectionData::POINTS);
1904 
1905  // Update info label (ParaView-style: "Selected Data (source.ply)")
1906  if (m_listInfoLabel) {
1907  m_listInfoLabel->setText(
1908  tr("Showing %1 %2")
1909  .arg(ids.size())
1910  .arg(m_selectionData.fieldTypeString().toLower()));
1911  m_listInfoLabel->setStyleSheet("font-weight: bold;");
1912  }
1913 
1914  // Setup columns based on selection type (ParaView-style)
1915  QStringList headers;
1916  if (isPoints) {
1917  headers << tr("Point ID") << tr("X") << tr("Y") << tr("Z");
1918  // Check for additional point attributes
1919  if (polyData && polyData->GetPointData()) {
1920  vtkPointData* pd = polyData->GetPointData();
1921  for (int i = 0; i < pd->GetNumberOfArrays(); ++i) {
1922  vtkDataArray* arr = pd->GetArray(i);
1923  if (arr && arr->GetName()) {
1924  QString name = QString::fromUtf8(arr->GetName());
1925  // Skip common coordinate arrays
1926  if (name != "Points" && name != "Normals") {
1927  headers << name;
1928  }
1929  }
1930  }
1931  }
1932  } else {
1933  // Cells
1934  headers << tr("Cell ID") << tr("Type") << tr("Num Points");
1935  // Check for additional cell attributes
1936  if (polyData && polyData->GetCellData()) {
1937  vtkCellData* cd = polyData->GetCellData();
1938  for (int i = 0; i < cd->GetNumberOfArrays(); ++i) {
1939  vtkDataArray* arr = cd->GetArray(i);
1940  if (arr && arr->GetName()) {
1941  headers << QString::fromUtf8(arr->GetName());
1942  }
1943  }
1944  }
1945  }
1946 
1947  m_selectionTableWidget->setColumnCount(headers.size());
1948  m_selectionTableWidget->setHorizontalHeaderLabels(headers);
1949 
1950  // Limit display for performance
1951  int maxDisplay = qMin(ids.size(), 500);
1952  m_selectionTableWidget->setRowCount(maxDisplay);
1953 
1954  for (int row = 0; row < maxDisplay; ++row) {
1955  qint64 id = ids[row];
1956  int col = 0;
1957 
1958  // ID column
1959  QTableWidgetItem* idItem = new QTableWidgetItem(QString::number(id));
1960  idItem->setData(
1961  Qt::UserRole,
1962  QVariant::fromValue(id)); // Store ID for click handling
1963  m_selectionTableWidget->setItem(row, col++, idItem);
1964 
1965  if (isPoints && polyData) {
1966  if (id >= 0 && id < polyData->GetNumberOfPoints()) {
1967  double pt[3];
1968  polyData->GetPoint(id, pt);
1969 
1970  // X, Y, Z columns
1971  m_selectionTableWidget->setItem(
1972  row, col++,
1973  new QTableWidgetItem(QString::number(pt[0], 'g', 6)));
1974  m_selectionTableWidget->setItem(
1975  row, col++,
1976  new QTableWidgetItem(QString::number(pt[1], 'g', 6)));
1977  m_selectionTableWidget->setItem(
1978  row, col++,
1979  new QTableWidgetItem(QString::number(pt[2], 'g', 6)));
1980 
1981  // Additional attributes
1982  if (polyData->GetPointData()) {
1983  vtkPointData* pd = polyData->GetPointData();
1984  for (int i = 0; i < pd->GetNumberOfArrays(); ++i) {
1985  vtkDataArray* arr = pd->GetArray(i);
1986  if (arr && arr->GetName()) {
1987  QString name = QString::fromUtf8(arr->GetName());
1988  if (name != "Points" && name != "Normals") {
1989  double val = arr->GetComponent(id, 0);
1990  m_selectionTableWidget->setItem(
1991  row, col++,
1992  new QTableWidgetItem(
1993  QString::number(val, 'g', 6)));
1994  }
1995  }
1996  }
1997  }
1998  }
1999  } else if (!isPoints && polyData) {
2000  // Cell data
2001  if (id >= 0 && id < polyData->GetNumberOfCells()) {
2002  vtkCell* cell = polyData->GetCell(id);
2003  if (cell) {
2004  // Cell type
2005  m_selectionTableWidget->setItem(
2006  row, col++,
2007  new QTableWidgetItem(
2008  QString::number(cell->GetCellType())));
2009  // Number of points
2010  m_selectionTableWidget->setItem(
2011  row, col++,
2012  new QTableWidgetItem(QString::number(
2013  cell->GetNumberOfPoints())));
2014 
2015  // Additional cell attributes
2016  if (polyData->GetCellData()) {
2017  vtkCellData* cd = polyData->GetCellData();
2018  for (int i = 0; i < cd->GetNumberOfArrays(); ++i) {
2019  vtkDataArray* arr = cd->GetArray(i);
2020  if (arr && arr->GetName()) {
2021  double val = arr->GetComponent(id, 0);
2022  m_selectionTableWidget->setItem(
2023  row, col++,
2024  new QTableWidgetItem(
2025  QString::number(val, 'g', 6)));
2026  }
2027  }
2028  }
2029  }
2030  }
2031  }
2032  }
2033 
2034  // Resize columns to contents
2035  m_selectionTableWidget->resizeColumnsToContents();
2036 
2037  // Update info if truncated
2038  if (ids.size() > maxDisplay && m_listInfoLabel) {
2039  m_listInfoLabel->setText(
2040  tr("Showing %1 of %2 %3")
2041  .arg(maxDisplay)
2042  .arg(ids.size())
2043  .arg(m_selectionData.fieldTypeString().toLower()));
2044  }
2045 }
2046 
2047 //-----------------------------------------------------------------------------
2048 void cvSelectionPropertiesWidget::computeBoundingBox(vtkPolyData* polyData,
2049  double bounds[6]) {
2050  // Initialize bounds
2051  bounds[0] = bounds[2] = bounds[4] = std::numeric_limits<double>::max();
2052  bounds[1] = bounds[3] = bounds[5] = std::numeric_limits<double>::lowest();
2053 
2054  QVector<qint64> ids = m_selectionData.ids();
2055 
2056  if (m_selectionData.fieldAssociation() == cvSelectionData::POINTS) {
2057  // Points: compute bounds from selected point coordinates
2058  for (qint64 id : ids) {
2059  if (id >= 0 && id < polyData->GetNumberOfPoints()) {
2060  double pt[3];
2061  polyData->GetPoint(id, pt);
2062 
2063  bounds[0] = qMin(bounds[0], pt[0]);
2064  bounds[1] = qMax(bounds[1], pt[0]);
2065  bounds[2] = qMin(bounds[2], pt[1]);
2066  bounds[3] = qMax(bounds[3], pt[1]);
2067  bounds[4] = qMin(bounds[4], pt[2]);
2068  bounds[5] = qMax(bounds[5], pt[2]);
2069  }
2070  }
2071  } else {
2072  // Cells: compute bounds from all points in selected cells
2073  for (qint64 id : ids) {
2074  if (id >= 0 && id < polyData->GetNumberOfCells()) {
2075  vtkCell* cell = polyData->GetCell(id);
2076  if (cell) {
2077  vtkIdType npts = cell->GetNumberOfPoints();
2078  for (vtkIdType i = 0; i < npts; ++i) {
2079  double pt[3];
2080  polyData->GetPoint(cell->GetPointId(i), pt);
2081 
2082  bounds[0] = qMin(bounds[0], pt[0]);
2083  bounds[1] = qMax(bounds[1], pt[0]);
2084  bounds[2] = qMin(bounds[2], pt[1]);
2085  bounds[3] = qMax(bounds[3], pt[1]);
2086  bounds[4] = qMin(bounds[4], pt[2]);
2087  bounds[5] = qMax(bounds[5], pt[2]);
2088  }
2089  }
2090  }
2091  }
2092  }
2093 }
2094 
2095 //-----------------------------------------------------------------------------
2096 QString cvSelectionPropertiesWidget::formatBounds(const double bounds[6]) {
2097  return QString("X: [%1, %2]\nY: [%3, %4]\nZ: [%5, %6]")
2098  .arg(bounds[0], 0, 'g', 6)
2099  .arg(bounds[1], 0, 'g', 6)
2100  .arg(bounds[2], 0, 'g', 6)
2101  .arg(bounds[3], 0, 'g', 6)
2102  .arg(bounds[4], 0, 'g', 6)
2103  .arg(bounds[5], 0, 'g', 6);
2104 }
2105 
2106 //-----------------------------------------------------------------------------
2107 void cvSelectionPropertiesWidget::showColorDialog(const QString& title,
2108  double currentColor[3],
2109  int mode) {
2110  // Get current color from highlighter (single source of truth)
2112  static_cast<cvSelectionHighlighter::HighlightMode>(mode);
2113 
2114  QColor initialColor;
2115  if (m_highlighter) {
2116  initialColor = m_highlighter->getHighlightQColor(hlMode);
2117  } else {
2118  // Fallback if highlighter not set yet
2119  initialColor =
2120  QColor(int(currentColor[0] * 255), int(currentColor[1] * 255),
2121  int(currentColor[2] * 255));
2122  }
2123 
2124  QColor newColor = QColorDialog::getColor(initialColor, this, title);
2125 
2126  if (newColor.isValid()) {
2127  // Update highlighter directly (single source of truth)
2128  if (m_highlighter) {
2129  m_highlighter->setHighlightQColor(newColor, hlMode);
2130  }
2131 
2132  // UI will be updated via colorChanged signal from highlighter
2133  // No need to update currentColor[] or buttons here
2134 
2135  // Emit signal for any additional processing
2136  emit highlightColorChanged(newColor.redF(), newColor.greenF(),
2137  newColor.blueF(), mode);
2138  }
2139 }
2140 
2141 //-----------------------------------------------------------------------------
2142 // Slot implementations
2143 //-----------------------------------------------------------------------------
2144 
2145 void cvSelectionPropertiesWidget::onHoverColorClicked() {
2146  double dummy[3] = {0, 1, 1}; // Fallback value (cyan)
2147  showColorDialog(tr("Select Hover Highlight Color"), dummy,
2149 }
2150 
2151 void cvSelectionPropertiesWidget::onPreselectedColorClicked() {
2152  double dummy[3] = {1, 1, 0}; // Fallback value (yellow)
2153  showColorDialog(tr("Select Pre-selected Highlight Color"), dummy,
2155 }
2156 
2157 void cvSelectionPropertiesWidget::onSelectedColorClicked() {
2158  double dummy[3] = {1, 0, 1}; // Fallback value (magenta)
2159  showColorDialog(tr("Select Selected Highlight Color"), dummy,
2161 }
2162 
2163 void cvSelectionPropertiesWidget::onBoundaryColorClicked() {
2164  double dummy[3] = {1, 0.65, 0}; // Fallback value (orange)
2165  showColorDialog(tr("Select Boundary Highlight Color"), dummy,
2167 }
2168 
2169 void cvSelectionPropertiesWidget::onHoverOpacityChanged(double value) {
2170  emit highlightOpacityChanged(value, 0); // HOVER = 0
2171 }
2172 
2173 void cvSelectionPropertiesWidget::onPreselectedOpacityChanged(double value) {
2174  emit highlightOpacityChanged(value, 1); // PRESELECTED = 1
2175 }
2176 
2177 void cvSelectionPropertiesWidget::onSelectedOpacityChanged(double value) {
2178  emit highlightOpacityChanged(value, 2); // SELECTED = 2
2179 }
2180 
2181 void cvSelectionPropertiesWidget::onBoundaryOpacityChanged(double value) {
2182  emit highlightOpacityChanged(value, 3); // BOUNDARY = 3
2183 }
2184 
2185 void cvSelectionPropertiesWidget::onExportToMeshClicked() {
2186  if (m_selectionData.isEmpty()) {
2187  CVLog::Warning("[cvSelectionPropertiesWidget] No selection to export");
2188  QMessageBox::warning(this, tr("Export Failed"),
2189  tr("No selection to export. Please select some "
2190  "cells first."));
2191  return;
2192  }
2193 
2194  if (m_selectionData.fieldAssociation() != cvSelectionData::CELLS) {
2196  "[cvSelectionPropertiesWidget] Can only export cells as mesh");
2197  QMessageBox::warning(this, tr("Export Failed"),
2198  tr("Can only export cell selections as mesh. "
2199  "Current selection is points."));
2200  return;
2201  }
2202 
2203  // First, try to use direct extraction from source ccMesh if available
2204  // This bypasses VTK→ccMesh conversion and preserves all attributes
2205  if (m_selectionManager && m_selectionManager->isSourceObjectValid()) {
2206  ccMesh* sourceMesh = m_selectionManager->getSourceMesh();
2207 
2208  if (sourceMesh) {
2209  // CVLog::Print(QString("[cvSelectionPropertiesWidget] Using direct
2210  // "
2211  // "extraction from source mesh '%1'")
2212  // .arg(sourceMesh->getName()));
2213 
2215  ccMesh* mesh = nullptr;
2216 
2217  try {
2219  sourceMesh, m_selectionData, options);
2220  } catch (const std::exception& e) {
2221  CVLog::Error(QString("[cvSelectionPropertiesWidget] Exception "
2222  "during direct export: %1")
2223  .arg(e.what()));
2224  // Fall through to VTK-based extraction
2225  } catch (...) {
2226  CVLog::Error(
2227  "[cvSelectionPropertiesWidget] Unknown exception "
2228  "during direct export");
2229  // Fall through to VTK-based extraction
2230  }
2231 
2232  if (mesh && mesh->size() > 0) {
2233  // ParaView-style naming: ExtractSelection1, ExtractSelection2,
2234  // ...
2235  m_extractCounter++;
2236  QString meshName =
2237  QString("ExtractSelection%1").arg(m_extractCounter);
2238  mesh->setName(meshName);
2239  // ParaView behavior: Extract objects are hidden by default
2240  // This prevents them from blocking selection of the original
2241  // object
2242  mesh->setEnabled(false);
2243  mesh->setVisible(true);
2244  emit extractedObjectReady(mesh);
2246  return;
2247  } else {
2248  if (mesh) {
2249  delete mesh;
2250  mesh = nullptr;
2251  }
2253  "[cvSelectionPropertiesWidget] Direct "
2254  "extraction failed, falling back to VTK-based "
2255  "extraction");
2256  }
2257  }
2258  }
2259 
2260  // Fall back to VTK-based extraction if direct extraction not available
2261  CVLog::Print(
2262  "[cvSelectionPropertiesWidget] Using VTK-based mesh extraction");
2263 
2264  // Export selection to mesh
2265  // Get polyData (using centralized ParaView-style method)
2266  vtkPolyData* polyData = getPolyDataForSelection(&m_selectionData);
2267 
2268  if (!polyData) {
2269  CVLog::Error(
2270  "[cvSelectionPropertiesWidget] Failed to get polyData from "
2271  "visualizer");
2272  QMessageBox::critical(this, tr("Export Failed"),
2273  tr("Failed to get data from visualizer. Please "
2274  "ensure a mesh is loaded."));
2275  return;
2276  }
2277 
2278  // Validate that polyData has enough cells for the selection
2279  vtkIdType polyDataCells = polyData->GetNumberOfCells();
2280  QVector<qint64> selectionIds = m_selectionData.ids();
2281  bool hasValidIds = false;
2282  for (qint64 id : selectionIds) {
2283  if (id >= 0 && id < polyDataCells) {
2284  hasValidIds = true;
2285  break;
2286  }
2287  }
2288 
2289  if (!hasValidIds) {
2290  CVLog::Error(QString("[cvSelectionPropertiesWidget] Selection IDs "
2291  "(%1 items) are not valid for polyData (%2 cells)")
2292  .arg(selectionIds.size())
2293  .arg(polyDataCells));
2294  QMessageBox::critical(
2295  this, tr("Export Failed"),
2296  tr("Selection IDs are not valid for the current data. "
2297  "The selection may have been made on a different object."));
2298  return;
2299  }
2300 
2301  CVLog::Print(
2302  "[cvSelectionPropertiesWidget] Creating mesh from selection...");
2303 
2305  ccMesh* mesh = nullptr;
2306 
2307  try {
2308  mesh = cvSelectionExporter::exportToMesh(polyData, m_selectionData,
2309  options);
2310  } catch (const std::exception& e) {
2311  CVLog::Error(QString("[cvSelectionPropertiesWidget] Exception during "
2312  "export: %1")
2313  .arg(e.what()));
2314  QMessageBox::critical(
2315  this, tr("Export Failed"),
2316  tr("An error occurred during export: %1").arg(e.what()));
2317  return;
2318  } catch (...) {
2319  CVLog::Error(
2320  "[cvSelectionPropertiesWidget] Unknown exception during "
2321  "export");
2322  QMessageBox::critical(this, tr("Export Failed"),
2323  tr("An unknown error occurred during export."));
2324  return;
2325  }
2326 
2327  if (mesh && mesh->size() > 0) {
2328  // ParaView-style naming: ExtractSelection1, ExtractSelection2, ...
2329  m_extractCounter++;
2330  QString meshName = QString("ExtractSelection%1").arg(m_extractCounter);
2331  mesh->setName(meshName);
2332  // ParaView behavior: Extract objects are hidden by default
2333  // This prevents them from blocking selection of the original object
2334  mesh->setEnabled(false);
2335  mesh->setVisible(true);
2336 
2337  // Emit signal for main application to add to DB Tree
2338  // The main application should connect to this signal and call addToDB()
2339  emit extractedObjectReady(mesh);
2341  } else {
2342  // Clean up empty mesh if created
2343  if (mesh) {
2344  delete mesh;
2345  mesh = nullptr;
2346  }
2347  CVLog::Error(
2348  "[cvSelectionPropertiesWidget] Failed to export selection as "
2349  "mesh - no cells extracted");
2350  QMessageBox::critical(
2351  this, tr("Export Failed"),
2352  tr("Failed to export selection. No cells could be extracted "
2353  "from the selection."));
2354  }
2355 }
2356 
2357 void cvSelectionPropertiesWidget::onExportToPointCloudClicked() {
2358  if (m_selectionData.isEmpty()) {
2359  CVLog::Warning("[cvSelectionPropertiesWidget] No selection to export");
2360  QMessageBox::warning(this, tr("Export Failed"),
2361  tr("No selection to export. Please select some "
2362  "points first."));
2363  return;
2364  }
2365 
2366  // First, try to use direct extraction from source ccPointCloud if available
2367  // This bypasses VTK→ccPointCloud conversion and preserves all attributes
2369  QString("[cvSelectionPropertiesWidget] Extract point cloud check: "
2370  "manager=%1, sourceValid=%2")
2371  .arg(m_selectionManager != nullptr ? "yes" : "no")
2372  .arg(m_selectionManager
2373  ? (m_selectionManager->isSourceObjectValid()
2374  ? "yes"
2375  : "no")
2376  : "N/A"));
2377 
2378  if (m_selectionManager && m_selectionManager->isSourceObjectValid()) {
2379  ccPointCloud* sourceCloud = m_selectionManager->getSourcePointCloud();
2380  CVLog::PrintVerbose(QString("[cvSelectionPropertiesWidget] "
2381  "getSourcePointCloud returned: %1")
2382  .arg(sourceCloud != nullptr
2383  ? sourceCloud->getName()
2384  : "nullptr"));
2385  if (sourceCloud) {
2386  // For cell selection on point cloud, convert to point selection
2387  cvSelectionData exportSelection = m_selectionData;
2388  if (m_selectionData.fieldAssociation() == cvSelectionData::CELLS) {
2389  // Converting cell selection to point selection
2390  exportSelection = cvSelectionData(m_selectionData.ids(),
2392  }
2393 
2395  ccPointCloud* cloud = nullptr;
2396 
2397  try {
2399  sourceCloud, exportSelection, options);
2400  } catch (const std::exception& e) {
2401  CVLog::Error(QString("[cvSelectionPropertiesWidget] Exception "
2402  "during direct export: %1")
2403  .arg(e.what()));
2404  // Fall through to VTK-based extraction
2405  } catch (...) {
2406  CVLog::Error(
2407  "[cvSelectionPropertiesWidget] Unknown exception "
2408  "during direct export");
2409  // Fall through to VTK-based extraction
2410  }
2411 
2412  if (cloud && cloud->size() > 0) {
2413  // ParaView-style naming: ExtractSelection1, ExtractSelection2,
2414  // ...
2415  m_extractCounter++;
2416  QString cloudName =
2417  QString("ExtractSelection%1").arg(m_extractCounter);
2418  cloud->setName(cloudName);
2419  // ParaView behavior: Extract objects are hidden by default
2420  // This prevents them from blocking selection of the original
2421  // object
2422  cloud->setEnabled(false);
2423  cloud->setVisible(true);
2424 
2425  CVLog::Print(QString("[cvSelectionPropertiesWidget] Created "
2426  "point cloud '%1' with %2 points (direct "
2427  "extraction)")
2428  .arg(cloudName)
2429  .arg(cloud->size()));
2430 
2431  emit extractedObjectReady(cloud);
2433  return;
2434  } else {
2435  if (cloud) {
2436  delete cloud;
2437  cloud = nullptr;
2438  }
2440  "[cvSelectionPropertiesWidget] Direct "
2441  "extraction failed, falling back to VTK-based "
2442  "extraction");
2443  }
2444  }
2445  }
2446 
2447  // Fall back to VTK-based extraction if direct extraction not available
2448 
2449  // ParaView behavior: Allow both POINTS and CELLS selections for point cloud
2450  // export For point clouds, cell IDs ARE point IDs (each vertex is a cell)
2451  // Check if source is a point cloud (no polygons)
2452  vtkPolyData* polyData = getPolyDataForSelection(&m_selectionData);
2453  bool isSourcePointCloud = polyData && (polyData->GetNumberOfPolys() == 0);
2454 
2455  if (m_selectionData.fieldAssociation() != cvSelectionData::POINTS &&
2456  !isSourcePointCloud) {
2458  "[cvSelectionPropertiesWidget] Can only export points as point "
2459  "cloud");
2460  QMessageBox::warning(
2461  this, tr("Export Failed"),
2462  tr("Can only export point selections as point "
2463  "cloud. Current selection is cells on a mesh."));
2464  return;
2465  }
2466 
2467  // Export selection to point cloud
2468  // Get polyData (using centralized ParaView-style method)
2469 
2470  if (!polyData) {
2471  CVLog::Error(
2472  "[cvSelectionPropertiesWidget] Failed to get polyData from "
2473  "visualizer");
2474  QMessageBox::critical(this, tr("Export Failed"),
2475  tr("Failed to get data from visualizer. Please "
2476  "ensure a point cloud is loaded."));
2477  return;
2478  }
2479 
2480  // Determine if this is a cell selection on a point cloud
2481  // For point clouds, cell IDs ARE point IDs (each vertex is a cell)
2482  bool isCellSelectionOnPointCloud =
2483  (m_selectionData.fieldAssociation() == cvSelectionData::CELLS) &&
2484  (polyData->GetNumberOfPolys() == 0);
2485 
2486  // Validate that polyData has enough elements for the selection
2487  vtkIdType maxId = isCellSelectionOnPointCloud
2488  ? polyData->GetNumberOfCells()
2489  : polyData->GetNumberOfPoints();
2490  QVector<qint64> selectionIds = m_selectionData.ids();
2491  bool hasValidIds = false;
2492  int validCount = 0;
2493  int invalidCount = 0;
2494  for (qint64 id : selectionIds) {
2495  if (id >= 0 && id < maxId) {
2496  hasValidIds = true;
2497  validCount++;
2498  } else {
2499  invalidCount++;
2500  }
2501  }
2502 
2503  // CVLog::Print(QString("[cvSelectionPropertiesWidget] Validation: %1 valid
2504  // "
2505  // "IDs, %2 invalid IDs (maxId=%3, type=%4)")
2506  // .arg(validCount)
2507  // .arg(invalidCount)
2508  // .arg(maxId)
2509  // .arg(isCellSelectionOnPointCloud ? "cells->points"
2510  // : "points"));
2511 
2512  if (!hasValidIds) {
2513  CVLog::Error(
2514  QString("[cvSelectionPropertiesWidget] Selection IDs "
2515  "(%1 items) are not valid for polyData (%2 elements)")
2516  .arg(selectionIds.size())
2517  .arg(maxId));
2518  QMessageBox::critical(
2519  this, tr("Export Failed"),
2520  tr("Selection IDs are not valid for the current data. "
2521  "The selection may have been made on a different object."));
2522  return;
2523  }
2524 
2525  CVLog::Print(
2526  "[cvSelectionPropertiesWidget] Creating point cloud from "
2527  "selection...");
2528 
2529  // For cell selection on point cloud, convert to point selection
2530  // (cell IDs = point IDs for vertex cells)
2531  cvSelectionData exportSelection = m_selectionData;
2532  if (isCellSelectionOnPointCloud) {
2533  CVLog::Print(
2534  "[cvSelectionPropertiesWidget] Converting cell selection to "
2535  "point selection for point cloud export");
2536  exportSelection =
2538  }
2539 
2541  ccPointCloud* cloud = nullptr;
2542 
2543  try {
2545  polyData, exportSelection, options);
2546  } catch (const std::exception& e) {
2547  CVLog::Error(QString("[cvSelectionPropertiesWidget] Exception during "
2548  "export: %1")
2549  .arg(e.what()));
2550  QMessageBox::critical(
2551  this, tr("Export Failed"),
2552  tr("An error occurred during export: %1").arg(e.what()));
2553  return;
2554  } catch (...) {
2555  CVLog::Error(
2556  "[cvSelectionPropertiesWidget] Unknown exception during "
2557  "export");
2558  QMessageBox::critical(this, tr("Export Failed"),
2559  tr("An unknown error occurred during export."));
2560  return;
2561  }
2562 
2563  if (cloud && cloud->size() > 0) {
2564  // ParaView-style naming: ExtractSelection1, ExtractSelection2, ...
2565  m_extractCounter++;
2566  QString cloudName = QString("ExtractSelection%1").arg(m_extractCounter);
2567  cloud->setName(cloudName);
2568  // ParaView behavior: Extract objects are hidden by default
2569  // This prevents them from blocking selection of the original object
2570  cloud->setEnabled(false);
2571  cloud->setVisible(true);
2572 
2573  CVLog::Print(QString("[cvSelectionPropertiesWidget] Created point "
2574  "cloud '%1' with %2 points")
2575  .arg(cloudName)
2576  .arg(cloud->size()));
2577 
2578  // Emit signal for main application to add to DB Tree
2579  // The main application should connect to this signal and call addToDB()
2580  emit extractedObjectReady(cloud);
2582 
2583  // ParaView behavior: Keep selection visible after extract
2584  // Selection is only cleared when user explicitly deletes it
2585  CVLog::Print(QString("[cvSelectionPropertiesWidget] Exported %1 points "
2586  "as point cloud (selection preserved)")
2587  .arg(cloud->size()));
2588  } else {
2589  // Clean up empty cloud if created
2590  if (cloud) {
2591  delete cloud;
2592  cloud = nullptr;
2593  }
2594  CVLog::Error(
2595  "[cvSelectionPropertiesWidget] Failed to export selection as "
2596  "point cloud - no points extracted");
2597  QMessageBox::critical(
2598  this, tr("Export Failed"),
2599  tr("Failed to export selection. No points could be extracted "
2600  "from the selection."));
2601  }
2602 }
2603 
2604 void cvSelectionPropertiesWidget::onSelectionTableItemClicked(
2605  QTableWidgetItem* item) {
2606  if (!item || !m_selectionTableWidget) {
2607  return;
2608  }
2609 
2610  // Get the ID from the first column of the clicked row
2611  int row = item->row();
2612  QTableWidgetItem* idItem = m_selectionTableWidget->item(row, 0);
2613  if (!idItem) {
2614  return;
2615  }
2616 
2617  // Get stored ID from UserRole data
2618  QVariant idData = idItem->data(Qt::UserRole);
2619  if (!idData.isValid()) {
2620  return;
2621  }
2622 
2623  qint64 id = idData.toLongLong();
2624 
2625  // Highlight this specific item in 3D view
2626  highlightSingleItem(id);
2627 
2628  CVLog::Print(QString("[cvSelectionPropertiesWidget] Highlighting %1 ID: %2")
2629  .arg(m_selectionData.fieldTypeString().toLower())
2630  .arg(id));
2631 }
2632 
2633 //-----------------------------------------------------------------------------
2634 qint64 cvSelectionPropertiesWidget::extractIdFromItemText(
2635  const QString& itemText) {
2636  // Format: "ID: 123" or "ID: 123 (x, y, z)"
2637  QRegularExpression idRegex("ID:\\s*(\\d+)");
2638  QRegularExpressionMatch match = idRegex.match(itemText);
2639 
2640  if (match.hasMatch()) {
2641  bool ok;
2642  qint64 id = match.captured(1).toLongLong(&ok);
2643  if (ok) {
2644  return id;
2645  }
2646  }
2647 
2648  return -1;
2649 }
2650 
2651 //-----------------------------------------------------------------------------
2652 void cvSelectionPropertiesWidget::highlightSingleItem(qint64 id) {
2653  if (!m_highlighter) {
2655  "[cvSelectionPropertiesWidget] Highlighter not available");
2656  return;
2657  }
2658 
2659  // Get polyData using centralized ParaView-style method
2660  vtkPolyData* polyData = getPolyDataForSelection(&m_selectionData);
2661 
2662  if (!polyData) {
2664  "[cvSelectionPropertiesWidget] No polyData available for "
2665  "highlighting");
2666  return;
2667  }
2668 
2669  // Determine the data type and validate ID
2670  bool isPointData =
2671  (m_selectionData.fieldAssociation() == cvSelectionData::POINTS);
2672  QString dataType = isPointData ? tr("Point") : tr("Cell");
2673 
2674  if (isPointData) {
2675  if (id < 0 || id >= polyData->GetNumberOfPoints()) {
2676  CVLog::Warning(QString("[cvSelectionPropertiesWidget] Point ID %1 "
2677  "out of range")
2678  .arg(id));
2679  return;
2680  }
2681  } else {
2682  if (id < 0 || id >= polyData->GetNumberOfCells()) {
2683  CVLog::Warning(QString("[cvSelectionPropertiesWidget] Cell ID %1 "
2684  "out of range")
2685  .arg(id));
2686  return;
2687  }
2688  }
2689 
2690  // Create a temporary single-item selection
2691  vtkSmartPointer<vtkIdTypeArray> singleIdArray =
2693  singleIdArray->InsertNextValue(id);
2694 
2695  // === ParaView-style: Use RED for interactive selection highlighting ===
2696  // Store original PRESELECTED color to restore later
2697  const double* originalColor = m_highlighter->getHighlightColor(
2699  double savedColor[3] = {originalColor[0], originalColor[1],
2700  originalColor[2]};
2701 
2702  // Set PRESELECTED mode to RED for emphasis (ParaView uses red for
2703  // interactive selection)
2704  m_highlighter->setHighlightColor(1.0, 0.0, 0.0,
2706 
2707  // Highlight this single item with RED using PRESELECTED mode
2708  m_highlighter->highlightSelection(singleIdArray,
2709  m_selectionData.fieldAssociation(),
2711 
2712  // Store for later restoration
2713  m_savedPreselectedColor[0] = savedColor[0];
2714  m_savedPreselectedColor[1] = savedColor[1];
2715  m_savedPreselectedColor[2] = savedColor[2];
2716  m_lastHighlightedId = id;
2717 
2718  // Log the highlighted item with data type info
2719  if (isPointData) {
2720  double pt[3];
2721  polyData->GetPoint(id, pt);
2723  QString("[cvSelectionPropertiesWidget] RED highlight: %1 "
2724  "ID=%2 at (%3, %4, %5)")
2725  .arg(dataType)
2726  .arg(id)
2727  .arg(pt[0], 0, 'f', 4)
2728  .arg(pt[1], 0, 'f', 4)
2729  .arg(pt[2], 0, 'f', 4));
2730  } else {
2731  vtkCell* cell = polyData->GetCell(id);
2732  if (cell) {
2733  double center[3] = {0, 0, 0};
2734  double* weights = new double[cell->GetNumberOfPoints()];
2735  double pcoords[3] = {0.5, 0.5, 0.5};
2736  int subId = 0;
2737  cell->EvaluateLocation(subId, pcoords, center, weights);
2738  delete[] weights;
2740  QString("[cvSelectionPropertiesWidget] RED highlight: "
2741  "%1 ID=%2 "
2742  "(Type:%3, Points:%4) center=(%5, %6, %7)")
2743  .arg(dataType)
2744  .arg(id)
2745  .arg(cell->GetCellType())
2746  .arg(cell->GetNumberOfPoints())
2747  .arg(center[0], 0, 'f', 4)
2748  .arg(center[1], 0, 'f', 4)
2749  .arg(center[2], 0, 'f', 4));
2750  }
2751  }
2752 
2753  // Refresh display immediately
2754  PclUtils::PCLVis* pclVis = getPCLVis();
2755  if (pclVis) {
2756  pclVis->UpdateScreen();
2757  }
2758 
2759  // Use a timer to restore original selection after 3 seconds
2760  QTimer::singleShot(3000, this, [this]() {
2761  if (m_highlighter) {
2762  // Restore original PRESELECTED color
2763  m_highlighter->setHighlightColor(
2764  this->m_savedPreselectedColor[0],
2765  this->m_savedPreselectedColor[1],
2766  this->m_savedPreselectedColor[2],
2768 
2769  if (!m_selectionData.isEmpty()) {
2770  // Restore original full selection highlight with SELECTED mode
2771  // (green)
2772  m_highlighter->highlightSelection(
2773  m_selectionData.vtkArray(),
2774  m_selectionData.fieldAssociation(),
2775  cvSelectionHighlighter::SELECTED);
2776  }
2777 
2778  PclUtils::PCLVis* pclVis = getPCLVis();
2779  if (pclVis) {
2780  pclVis->UpdateScreen();
2781  }
2782  }
2783  });
2784 }
2785 
2786 //-----------------------------------------------------------------------------
2787 // Advanced operations (new)
2788 //-----------------------------------------------------------------------------
2789 
2790 void cvSelectionPropertiesWidget::onAlgebraOperationTriggered() {
2791  if (!m_selectionManager || m_selectionData.isEmpty() || !m_algebraOpCombo) {
2792  return;
2793  }
2794 
2796  static_cast<cvSelectionAlgebra::Operation>(
2797  m_algebraOpCombo->currentData().toInt());
2798 
2799  // For now, emit signal - actual implementation depends on having two
2800  // selections
2801  emit algebraOperationRequested(static_cast<int>(op));
2802 
2803  CVLog::Print(QString("[cvSelectionPropertiesWidget] Algebra operation %1 "
2804  "requested")
2805  .arg(static_cast<int>(op)));
2806 }
2807 
2808 //-----------------------------------------------------------------------------
2809 void cvSelectionPropertiesWidget::onExtractBoundaryClicked() {
2810  if (!m_selectionManager || m_selectionData.isEmpty()) {
2811  return;
2812  }
2813 
2814  cvSelectionAlgebra* algebra = m_selectionManager->getAlgebra();
2815  if (!algebra) {
2816  return;
2817  }
2818 
2819  // Get polyData using centralized ParaView-style method
2820  vtkPolyData* polyData = getPolyDataForSelection(&m_selectionData);
2821 
2822  if (!polyData) {
2824  "[cvSelectionPropertiesWidget] No polyData for boundary "
2825  "extraction");
2826  return;
2827  }
2828 
2829  cvSelectionData boundary =
2830  algebra->extractBoundary(polyData, m_selectionData);
2831 
2832  if (!boundary.isEmpty()) {
2833  m_selectionManager->setCurrentSelection(boundary);
2834  CVLog::Print(QString("[cvSelectionPropertiesWidget] Extracted "
2835  "boundary: %1 -> %2 cells")
2836  .arg(m_selectionData.count())
2837  .arg(boundary.count()));
2838  }
2839 }
2840 
2841 //-----------------------------------------------------------------------------
2842 
2843 //-----------------------------------------------------------------------------
2844 // onLoadBookmarkClicked removed - UI not implemented
2845 
2846 //-----------------------------------------------------------------------------
2847 
2848 //-----------------------------------------------------------------------------
2849 void cvSelectionPropertiesWidget::onAddAnnotationClicked() {
2850  if (!m_selectionManager || m_selectionData.isEmpty()) {
2851  return;
2852  }
2853 
2854  cvSelectionAnnotationManager* annotations =
2855  m_selectionManager->getAnnotations();
2856  if (!annotations) {
2857  return;
2858  }
2859 
2860  // Show input dialog for annotation text
2861  bool ok;
2862  QString text = QInputDialog::getText(this, tr("Add Annotation"),
2863  tr("Annotation text:"),
2864  QLineEdit::Normal, QString(), &ok);
2865 
2866  if (ok && !text.isEmpty()) {
2867  QString id = annotations->addAnnotation(m_selectionData, text, true);
2868  if (!id.isEmpty()) {
2869  emit annotationRequested(text);
2870  CVLog::Print(QString("[cvSelectionPropertiesWidget] Added "
2871  "annotation: %1")
2872  .arg(id));
2873  }
2874  }
2875 }
2876 
2877 // ============================================================================
2878 // ParaView-style Selection Display slots
2879 // ============================================================================
2880 
2881 //-----------------------------------------------------------------------------
2882 void cvSelectionPropertiesWidget::onCellLabelsClicked() {
2883  // Dynamically populate the menu with available cell data arrays
2884  // ParaView reference: pqFindDataSelectionDisplayFrame::fillLabels
2885  if (!m_cellLabelsMenu) return;
2886 
2887  m_cellLabelsMenu->clear();
2888 
2889  // Add "None" option to disable labels
2890  QAction* noneAction = m_cellLabelsMenu->addAction(tr("None"));
2891  noneAction->setCheckable(true);
2892  noneAction->setChecked(m_currentCellLabelArray.isEmpty());
2893  connect(noneAction, &QAction::triggered, [this]() {
2894  m_currentCellLabelArray.clear();
2895  // Apply to highlighter
2896  if (m_highlighter) {
2897  m_highlighter->setCellLabelArray(QString(), false);
2898  }
2899  CVLog::Print("[cvSelectionPropertiesWidget] Cell labels disabled");
2900  });
2901 
2902  // Add "ID" option - ParaView uses vtkOriginalCellIds for this
2903  QAction* idAction = m_cellLabelsMenu->addAction(tr("ID"));
2904  idAction->setCheckable(true);
2905  idAction->setChecked(m_currentCellLabelArray == "ID");
2906  connect(idAction, &QAction::triggered, [this]() {
2907  m_currentCellLabelArray = "ID";
2908  // Apply to highlighter
2909  if (m_highlighter) {
2910  m_highlighter->setCellLabelArray("ID", true);
2911  }
2912  CVLog::Print("[cvSelectionPropertiesWidget] Cell labels set to ID");
2913  });
2914 
2915  m_cellLabelsMenu->addSeparator();
2916 
2917  // Add available cell data arrays from current polyData
2918  vtkPolyData* polyData = getPolyDataForSelection(&m_selectionData);
2919  if (polyData && polyData->GetCellData()) {
2920  vtkCellData* cellData = polyData->GetCellData();
2921  for (int i = 0; i < cellData->GetNumberOfArrays(); ++i) {
2922  vtkDataArray* arr = cellData->GetArray(i);
2923  if (arr && arr->GetName()) {
2924  QString name = QString::fromUtf8(arr->GetName());
2925  // Skip VTK internal arrays (ParaView does this)
2926  if (name.startsWith("vtk", Qt::CaseInsensitive)) {
2927  continue;
2928  }
2929  QAction* action = m_cellLabelsMenu->addAction(name);
2930  action->setCheckable(true);
2931  action->setChecked(m_currentCellLabelArray == name);
2932  connect(action, &QAction::triggered, [this, name]() {
2933  m_currentCellLabelArray = name;
2934  // Apply to highlighter
2935  if (m_highlighter) {
2936  m_highlighter->setCellLabelArray(name, true);
2937  }
2938  CVLog::Print(QString("[cvSelectionPropertiesWidget] "
2939  "Cell labels set to %1")
2940  .arg(name));
2941  });
2942  }
2943  }
2944  }
2945 
2946  // Menu will be shown automatically by Qt since this is connected to
2947  // aboutToShow
2948 }
2949 
2950 //-----------------------------------------------------------------------------
2951 void cvSelectionPropertiesWidget::onPointLabelsClicked() {
2952  // Dynamically populate the menu with available point data arrays
2953  // ParaView reference: pqFindDataSelectionDisplayFrame::fillLabels
2954  if (!m_pointLabelsMenu) return;
2955 
2956  m_pointLabelsMenu->clear();
2957 
2958  // Add "None" option to disable labels
2959  QAction* noneAction = m_pointLabelsMenu->addAction(tr("None"));
2960  noneAction->setCheckable(true);
2961  noneAction->setChecked(m_currentPointLabelArray.isEmpty());
2962  connect(noneAction, &QAction::triggered, [this]() {
2963  m_currentPointLabelArray.clear();
2964  // Apply to highlighter
2965  if (m_highlighter) {
2966  m_highlighter->setPointLabelArray(QString(), false);
2967  }
2968  CVLog::Print("[cvSelectionPropertiesWidget] Point labels disabled");
2969  });
2970 
2971  // Add "ID" option - ParaView uses vtkOriginalPointIds for this
2972  QAction* idAction = m_pointLabelsMenu->addAction(tr("ID"));
2973  idAction->setCheckable(true);
2974  idAction->setChecked(m_currentPointLabelArray == "ID");
2975  connect(idAction, &QAction::triggered, [this]() {
2976  m_currentPointLabelArray = "ID";
2977  // Apply to highlighter
2978  if (m_highlighter) {
2979  m_highlighter->setPointLabelArray("ID", true);
2980  }
2981  });
2982 
2983  m_pointLabelsMenu->addSeparator();
2984 
2985  // Add available point data arrays from current polyData
2986  vtkPolyData* polyData = getPolyDataForSelection(&m_selectionData);
2987  if (polyData && polyData->GetPointData()) {
2988  vtkPointData* pointData = polyData->GetPointData();
2989  for (int i = 0; i < pointData->GetNumberOfArrays(); ++i) {
2990  vtkDataArray* arr = pointData->GetArray(i);
2991  if (arr && arr->GetName()) {
2992  QString name = QString::fromUtf8(arr->GetName());
2993  // Skip VTK internal arrays (ParaView does this)
2994  if (name.startsWith("vtk", Qt::CaseInsensitive)) {
2995  continue;
2996  }
2997  // Allow Normals and other arrays
2998  QAction* action = m_pointLabelsMenu->addAction(name);
2999  action->setCheckable(true);
3000  action->setChecked(m_currentPointLabelArray == name);
3001  connect(action, &QAction::triggered, [this, name]() {
3002  m_currentPointLabelArray = name;
3003  // Apply to highlighter
3004  if (m_highlighter) {
3005  m_highlighter->setPointLabelArray(name, true);
3006  }
3007  CVLog::Print(QString("[cvSelectionPropertiesWidget] "
3008  "Point labels set to %1")
3009  .arg(name));
3010  });
3011  }
3012  }
3013  }
3014 
3015  // Menu will be shown automatically by Qt since this is connected to
3016  // aboutToShow
3017 }
3018 
3019 //-----------------------------------------------------------------------------
3020 void cvSelectionPropertiesWidget::onEditLabelPropertiesClicked() {
3021  cvSelectionLabelPropertiesDialog dialog(this, false);
3022 
3023  // Convert SelectionLabelProperties to LabelProperties for dialog
3025  if (m_highlighter) {
3026  const SelectionLabelProperties& hlProps =
3027  m_highlighter->getLabelProperties(false);
3028  dialogProps.opacity = hlProps.opacity;
3029  dialogProps.pointSize = hlProps.pointSize;
3030  dialogProps.lineWidth = hlProps.lineWidth;
3031  dialogProps.cellLabelFontFamily = hlProps.cellLabelFontFamily;
3032  dialogProps.cellLabelFontSize = hlProps.cellLabelFontSize;
3033  dialogProps.cellLabelColor = hlProps.cellLabelColor;
3034  dialogProps.cellLabelBold = hlProps.cellLabelBold;
3035  dialogProps.cellLabelItalic = hlProps.cellLabelItalic;
3036  dialogProps.cellLabelShadow = hlProps.cellLabelShadow;
3037  dialogProps.cellLabelOpacity = hlProps.cellLabelOpacity;
3038  dialogProps.cellLabelFormat = hlProps.cellLabelFormat;
3039  dialogProps.pointLabelFontFamily = hlProps.pointLabelFontFamily;
3040  dialogProps.pointLabelFontSize = hlProps.pointLabelFontSize;
3041  dialogProps.pointLabelColor = hlProps.pointLabelColor;
3042  dialogProps.pointLabelBold = hlProps.pointLabelBold;
3043  dialogProps.pointLabelItalic = hlProps.pointLabelItalic;
3044  dialogProps.pointLabelShadow = hlProps.pointLabelShadow;
3045  dialogProps.pointLabelOpacity = hlProps.pointLabelOpacity;
3046  dialogProps.pointLabelFormat = hlProps.pointLabelFormat;
3047  dialogProps.showTooltips = hlProps.showTooltips;
3048  dialogProps.maxTooltipAttributes = hlProps.maxTooltipAttributes;
3049  }
3050 
3051  dialog.setProperties(dialogProps);
3053  &cvSelectionPropertiesWidget::onLabelPropertiesApplied);
3054  dialog.exec();
3055 }
3056 
3057 //-----------------------------------------------------------------------------
3058 void cvSelectionPropertiesWidget::onSelectionColorClicked() {
3059  QColor currentColor = getSelectionColor();
3060  QColor color = QColorDialog::getColor(currentColor, this, tr("Set Color"));
3061  if (color.isValid() && m_highlighter) {
3062  // Set color directly on highlighter (single source of truth)
3063  // This will trigger colorChanged signal which updates UI
3064  m_highlighter->setHighlightQColor(color,
3066 
3067  // Refresh display
3068  PclUtils::PCLVis* pclVis = getPCLVis();
3069  if (pclVis) {
3070  pclVis->UpdateScreen();
3071  }
3072 
3073  CVLog::PrintVerbose(QString("[cvSelectionPropertiesWidget] Selection "
3074  "color changed to %1")
3075  .arg(color.name()));
3076  }
3077 }
3078 
3079 //-----------------------------------------------------------------------------
3080 void cvSelectionPropertiesWidget::onInteractiveSelectionColorClicked() {
3081  QColor currentColor = getInteractiveSelectionColor();
3082  QColor color = QColorDialog::getColor(currentColor, this, tr("Set Color"));
3083  if (color.isValid() && m_highlighter) {
3084  // Set color directly on highlighter (single source of truth)
3085  // This will trigger colorChanged signal which updates UI
3087 
3088  // Refresh display
3089  PclUtils::PCLVis* pclVis = getPCLVis();
3090  if (pclVis) {
3091  pclVis->UpdateScreen();
3092  }
3093 
3094  CVLog::PrintVerbose(QString("[cvSelectionPropertiesWidget] Interactive "
3095  "selection color changed to %1")
3096  .arg(color.name()));
3097  }
3098 }
3099 
3100 //-----------------------------------------------------------------------------
3101 void cvSelectionPropertiesWidget::onEditInteractiveLabelPropertiesClicked() {
3102  cvSelectionLabelPropertiesDialog dialog(this, true);
3103 
3104  // Convert SelectionLabelProperties to LabelProperties for dialog
3106  if (m_highlighter) {
3107  const SelectionLabelProperties& hlProps =
3108  m_highlighter->getLabelProperties(true);
3109  dialogProps.opacity = hlProps.opacity;
3110  dialogProps.pointSize = hlProps.pointSize;
3111  dialogProps.lineWidth = hlProps.lineWidth;
3112  dialogProps.cellLabelFontFamily = hlProps.cellLabelFontFamily;
3113  dialogProps.cellLabelFontSize = hlProps.cellLabelFontSize;
3114  dialogProps.cellLabelColor = hlProps.cellLabelColor;
3115  dialogProps.cellLabelBold = hlProps.cellLabelBold;
3116  dialogProps.cellLabelItalic = hlProps.cellLabelItalic;
3117  dialogProps.cellLabelShadow = hlProps.cellLabelShadow;
3118  dialogProps.cellLabelOpacity = hlProps.cellLabelOpacity;
3119  dialogProps.cellLabelFormat = hlProps.cellLabelFormat;
3120  dialogProps.pointLabelFontFamily = hlProps.pointLabelFontFamily;
3121  dialogProps.pointLabelFontSize = hlProps.pointLabelFontSize;
3122  dialogProps.pointLabelColor = hlProps.pointLabelColor;
3123  dialogProps.pointLabelBold = hlProps.pointLabelBold;
3124  dialogProps.pointLabelItalic = hlProps.pointLabelItalic;
3125  dialogProps.pointLabelShadow = hlProps.pointLabelShadow;
3126  dialogProps.pointLabelOpacity = hlProps.pointLabelOpacity;
3127  dialogProps.pointLabelFormat = hlProps.pointLabelFormat;
3128  dialogProps.showTooltips = hlProps.showTooltips;
3129  dialogProps.maxTooltipAttributes = hlProps.maxTooltipAttributes;
3130  }
3131 
3132  dialog.setProperties(dialogProps);
3134  &cvSelectionPropertiesWidget::onInteractiveLabelPropertiesApplied);
3135  dialog.exec();
3136 }
3137 
3138 //-----------------------------------------------------------------------------
3139 void cvSelectionPropertiesWidget::onLabelPropertiesApplied(
3141  // Convert dialog properties to SelectionLabelProperties
3142  SelectionLabelProperties hlProps;
3143  hlProps.opacity = props.opacity;
3144  hlProps.pointSize = props.pointSize;
3145  hlProps.lineWidth = props.lineWidth;
3146  hlProps.cellLabelFontFamily = props.cellLabelFontFamily;
3147  hlProps.cellLabelFontSize = props.cellLabelFontSize;
3148  hlProps.cellLabelColor = props.cellLabelColor;
3149  hlProps.cellLabelBold = props.cellLabelBold;
3150  hlProps.cellLabelItalic = props.cellLabelItalic;
3151  hlProps.cellLabelShadow = props.cellLabelShadow;
3152  hlProps.cellLabelOpacity = props.cellLabelOpacity;
3153  hlProps.cellLabelFormat = props.cellLabelFormat;
3155  hlProps.pointLabelFontSize = props.pointLabelFontSize;
3156  hlProps.pointLabelColor = props.pointLabelColor;
3157  hlProps.pointLabelBold = props.pointLabelBold;
3158  hlProps.pointLabelItalic = props.pointLabelItalic;
3159  hlProps.pointLabelShadow = props.pointLabelShadow;
3160  hlProps.pointLabelOpacity = props.pointLabelOpacity;
3161  hlProps.pointLabelFormat = props.pointLabelFormat;
3162  hlProps.showTooltips = props.showTooltips;
3164 
3165  // Store to highlighter (single source of truth)
3166  if (m_highlighter) {
3167  m_highlighter->setLabelProperties(hlProps,
3168  false); // false = SELECTED mode
3169 
3170  // Refresh display
3171  PclUtils::PCLVis* pclVis = getPCLVis();
3172  if (pclVis) {
3173  pclVis->UpdateScreen();
3174  }
3175  }
3176 
3177  // Apply font properties to annotations (cell labels)
3178  if (m_selectionManager) {
3179  cvSelectionAnnotationManager* annotations =
3180  m_selectionManager->getAnnotations();
3181  if (annotations) {
3182  // Set default properties for new annotations
3183  annotations->setDefaultLabelProperties(props,
3184  true); // true = cell labels
3185 
3186  // Apply to all existing annotations
3187  annotations->applyLabelProperties(props,
3188  true); // true = cell labels
3189  }
3190  }
3191 
3193  QString("[cvSelectionPropertiesWidget] Label properties applied: "
3194  "opacity=%1, pointSize=%2, lineWidth=%3")
3195  .arg(props.opacity)
3196  .arg(props.pointSize)
3197  .arg(props.lineWidth));
3198 }
3199 
3200 //-----------------------------------------------------------------------------
3201 void cvSelectionPropertiesWidget::onInteractiveLabelPropertiesApplied(
3203  // Convert dialog properties to SelectionLabelProperties
3204  SelectionLabelProperties hlProps;
3205  hlProps.opacity = props.opacity;
3206  hlProps.pointSize = props.pointSize;
3207  hlProps.lineWidth = props.lineWidth;
3208  hlProps.cellLabelFontFamily = props.cellLabelFontFamily;
3209  hlProps.cellLabelFontSize = props.cellLabelFontSize;
3210  hlProps.cellLabelColor = props.cellLabelColor;
3211  hlProps.cellLabelBold = props.cellLabelBold;
3212  hlProps.cellLabelItalic = props.cellLabelItalic;
3213  hlProps.cellLabelShadow = props.cellLabelShadow;
3214  hlProps.cellLabelOpacity = props.cellLabelOpacity;
3215  hlProps.cellLabelFormat = props.cellLabelFormat;
3217  hlProps.pointLabelFontSize = props.pointLabelFontSize;
3218  hlProps.pointLabelColor = props.pointLabelColor;
3219  hlProps.pointLabelBold = props.pointLabelBold;
3220  hlProps.pointLabelItalic = props.pointLabelItalic;
3221  hlProps.pointLabelShadow = props.pointLabelShadow;
3222  hlProps.pointLabelOpacity = props.pointLabelOpacity;
3223  hlProps.pointLabelFormat = props.pointLabelFormat;
3224  hlProps.showTooltips = props.showTooltips;
3226 
3227  // Apply default properties to annotation manager for point labels
3228  // (interactive)
3229  if (m_selectionManager) {
3230  cvSelectionAnnotationManager* annotations =
3231  m_selectionManager->getAnnotations();
3232  if (annotations) {
3233  // Set default properties for new annotations (point labels)
3234  annotations->setDefaultLabelProperties(
3235  props, false); // false = point labels
3236 
3237  // Apply to all existing annotations (point labels)
3238  annotations->applyLabelProperties(props,
3239  false); // false = point labels
3240  }
3241  }
3242 
3243  // Store to highlighter (single source of truth)
3244  if (m_highlighter) {
3245  m_highlighter->setLabelProperties(
3246  hlProps, true); // true = interactive/HOVER mode
3247 
3248  // Refresh display
3249  PclUtils::PCLVis* pclVis = getPCLVis();
3250  if (pclVis) {
3251  pclVis->UpdateScreen();
3252  }
3253  }
3254 
3255  CVLog::Print(QString("[cvSelectionPropertiesWidget] Interactive label "
3256  "properties applied: "
3257  "opacity=%1, pointSize=%2, lineWidth=%3")
3258  .arg(props.opacity)
3259  .arg(props.pointSize)
3260  .arg(props.lineWidth));
3261 }
3262 
3263 // ============================================================================
3264 // ParaView-style Selection Editor slots
3265 // ============================================================================
3266 
3267 //-----------------------------------------------------------------------------
3268 void cvSelectionPropertiesWidget::onExpressionChanged(const QString& text) {
3269  emit expressionChanged(text);
3270 
3271  // Update the activate button state
3272  m_activateCombinedSelectionsButton->setEnabled(
3273  !text.isEmpty() && !m_savedSelections.isEmpty());
3274 }
3275 
3276 //-----------------------------------------------------------------------------
3277 void cvSelectionPropertiesWidget::onAddActiveSelectionClicked() {
3278  if (m_selectionData.isEmpty()) {
3279  QMessageBox::information(this, tr("Add Selection"),
3280  tr("No active selection to add."));
3281  return;
3282  }
3283 
3284  // Create new saved selection
3285  SavedSelection saved;
3286  saved.name = generateSelectionName();
3287  saved.type = tr("ID Selection");
3288  saved.color = generateSelectionColor();
3289  saved.data = m_selectionData;
3290 
3291  m_savedSelections.append(saved);
3292  updateSelectionEditorTable();
3293 
3294  // Update expression with new selection (ParaView-style)
3295  // ParaView wraps existing expression in parentheses when there are multiple
3296  // inputs Format: s0 -> s0|s1 -> (s0|s1)|s2 -> ((s0|s1)|s2)|s3 etc.
3297  QString expr = m_expressionEdit->text();
3298  if (!expr.isEmpty()) {
3299  // Wrap existing expression in parentheses if we have more than one
3300  // selection
3301  if (m_savedSelections.size() > 2) {
3302  expr = QString("(%1)").arg(expr);
3303  }
3304  expr += "|" + saved.name;
3305  } else {
3306  expr = saved.name;
3307  }
3308  m_expressionEdit->setText(expr);
3309 
3310  // Enable buttons
3311  m_removeAllSelectionsButton->setEnabled(true);
3312  m_activateCombinedSelectionsButton->setEnabled(
3313  !m_expressionEdit->text().isEmpty());
3314 
3315  // ParaView behavior: Disable the + button after adding a selection.
3316  // The button is re-enabled when a NEW selection is made.
3317  // This prevents adding the same selection multiple times.
3318  if (m_addSelectionButton) {
3319  m_addSelectionButton->setEnabled(false);
3320  }
3321 
3322  // Clear the current selection data to indicate it has been "consumed"
3323  // A new selection must be made to enable the + button again
3324  m_selectionData.clear();
3325 
3326  emit selectionAdded(saved.data);
3327 
3329  QString("[cvSelectionPropertiesWidget] Added selection: %1 "
3330  "(+ button disabled until new selection)")
3331  .arg(saved.name));
3332 }
3333 
3334 //-----------------------------------------------------------------------------
3335 void cvSelectionPropertiesWidget::onRemoveSelectedSelectionClicked() {
3336  QList<QTableWidgetItem*> selectedItems =
3337  m_selectionEditorTable->selectedItems();
3338  if (selectedItems.isEmpty()) {
3339  return;
3340  }
3341 
3342  // Get unique rows
3343  QSet<int> rows;
3344  for (QTableWidgetItem* item : selectedItems) {
3345  rows.insert(item->row());
3346  }
3347 
3348  // Remove in reverse order to maintain valid indices
3349  QList<int> sortedRows = rows.values();
3350  std::sort(sortedRows.begin(), sortedRows.end(), std::greater<int>());
3351 
3352  for (int row : sortedRows) {
3353  if (row >= 0 && row < m_savedSelections.size()) {
3354  QString name = m_savedSelections[row].name;
3355  m_savedSelections.removeAt(row);
3356  emit selectionRemoved(row);
3357  CVLog::PrintVerbose(QString("[cvSelectionPropertiesWidget] Removed "
3358  "selection: %1")
3359  .arg(name));
3360  }
3361  }
3362 
3363  updateSelectionEditorTable();
3364 
3365  // Update button states
3366  m_removeAllSelectionsButton->setEnabled(!m_savedSelections.isEmpty());
3367  m_activateCombinedSelectionsButton->setEnabled(
3368  !m_expressionEdit->text().isEmpty() &&
3369  !m_savedSelections.isEmpty());
3370 }
3371 
3372 //-----------------------------------------------------------------------------
3373 void cvSelectionPropertiesWidget::onRemoveAllSelectionsClicked() {
3374  if (m_savedSelections.isEmpty()) {
3375  return;
3376  }
3377 
3378  int result = QMessageBox::question(this, tr("Remove All Selections"),
3379  tr("Remove all saved selections?"),
3380  QMessageBox::Yes | QMessageBox::No);
3381  if (result != QMessageBox::Yes) {
3382  return;
3383  }
3384 
3385  m_savedSelections.clear();
3386  m_selectionNameCounter = 0;
3387  m_expressionEdit->clear();
3388  updateSelectionEditorTable();
3389 
3390  // Update button states
3391  m_removeSelectionButton->setEnabled(false);
3392  m_removeAllSelectionsButton->setEnabled(false);
3393  m_activateCombinedSelectionsButton->setEnabled(false);
3394 
3395  emit allSelectionsRemoved();
3396 
3397  CVLog::Print("[cvSelectionPropertiesWidget] Removed all saved selections");
3398 }
3399 
3400 //-----------------------------------------------------------------------------
3401 void cvSelectionPropertiesWidget::onActivateCombinedSelectionsClicked() {
3402  QString expression = m_expressionEdit->text().trimmed();
3403  if (expression.isEmpty()) {
3404  QMessageBox::warning(this, tr("Activate Combined Selections"),
3405  tr("Expression is empty. Please enter an "
3406  "expression like: s0 & s1"));
3407  return;
3408  }
3409 
3410  if (m_savedSelections.isEmpty()) {
3411  QMessageBox::warning(
3412  this, tr("Activate Combined Selections"),
3413  tr("No saved selections. Please add selections first."));
3414  return;
3415  }
3416 
3417  CVLog::Print(
3418  QString("[cvSelectionPropertiesWidget] Evaluating expression: '%1'")
3419  .arg(expression));
3420 
3421  // Evaluate the expression with error handling
3423  try {
3424  result = evaluateExpression(expression);
3425  } catch (const std::exception& e) {
3426  CVLog::Error(
3427  QString("[cvSelectionPropertiesWidget] Expression evaluation "
3428  "exception: %1")
3429  .arg(e.what()));
3430  QMessageBox::warning(
3431  this, tr("Activate Combined Selections"),
3432  tr("Expression evaluation failed: %1").arg(e.what()));
3433  return;
3434  }
3435 
3436  if (result.isEmpty()) {
3437  QMessageBox::warning(
3438  this, tr("Activate Combined Selections"),
3439  tr("Expression evaluation resulted in empty selection.\n"
3440  "Please check your expression syntax."));
3441  return;
3442  }
3443 
3444  CVLog::Print(QString("[cvSelectionPropertiesWidget] Expression evaluated: "
3445  "%1 elements")
3446  .arg(result.count()));
3447 
3448  // Set the combined selection as the current selection
3449  if (m_selectionManager) {
3450  m_selectionManager->setCurrentSelection(result);
3451 
3452  // Update our own display - get polyData safely
3453  vtkPolyData* polyData = getPolyDataForSelection(&result);
3454  if (polyData) {
3455  updateSelection(result, polyData);
3456 
3457  CVLog::Print(
3458  QString("[cvSelectionPropertiesWidget] Activated combined "
3459  "selection: %1 elements from expression '%2'")
3460  .arg(result.count())
3461  .arg(expression));
3462  } else {
3464  "[cvSelectionPropertiesWidget] Could not get polyData "
3465  "for combined selection");
3466  // Still update the selection data without polyData
3467  m_selectionData = result;
3468  m_selectionCount = result.count();
3470  }
3471  } else {
3473  "[cvSelectionPropertiesWidget] No selection manager available");
3474  }
3475 
3477 }
3478 
3479 //-----------------------------------------------------------------------------
3480 cvSelectionData cvSelectionPropertiesWidget::evaluateExpression(
3481  const QString& expression) {
3482  // ParaView expression syntax:
3483  // - Selection names: s0, s1, s2, ...
3484  // - NOT operator: ! (prefix)
3485  // - AND operator: &
3486  // - OR operator: |
3487  // - XOR operator: ^
3488  // - Parentheses: ( )
3489  // Example: "(s0 & s1) | !s2"
3490 
3491  QString expr = expression.simplified();
3492  if (expr.isEmpty()) {
3493  return cvSelectionData();
3494  }
3495 
3496  // Tokenize the expression
3497  QStringList tokens = tokenizeExpression(expr);
3498  if (tokens.isEmpty()) {
3500  "[cvSelectionPropertiesWidget] Failed to tokenize expression");
3501  return cvSelectionData();
3502  }
3503 
3504  // Parse and evaluate
3505  int pos = 0;
3506  cvSelectionData result = parseOrExpression(tokens, pos);
3507 
3508  if (pos < tokens.size()) {
3509  CVLog::Warning(QString("[cvSelectionPropertiesWidget] Unexpected token "
3510  "at position %1: %2")
3511  .arg(pos)
3512  .arg(tokens[pos]));
3513  }
3514 
3515  return result;
3516 }
3517 
3518 //-----------------------------------------------------------------------------
3519 QStringList cvSelectionPropertiesWidget::tokenizeExpression(
3520  const QString& expression) {
3521  QStringList tokens;
3522  QString current;
3523 
3524  for (int i = 0; i < expression.length(); ++i) {
3525  QChar c = expression[i];
3526 
3527  if (c.isSpace()) {
3528  if (!current.isEmpty()) {
3529  tokens.append(current);
3530  current.clear();
3531  }
3532  } else if (c == '(' || c == ')' || c == '!' || c == '&' || c == '|' ||
3533  c == '^') {
3534  if (!current.isEmpty()) {
3535  tokens.append(current);
3536  current.clear();
3537  }
3538  tokens.append(QString(c));
3539  } else {
3540  current.append(c);
3541  }
3542  }
3543 
3544  if (!current.isEmpty()) {
3545  tokens.append(current);
3546  }
3547 
3548  return tokens;
3549 }
3550 
3551 //-----------------------------------------------------------------------------
3552 cvSelectionData cvSelectionPropertiesWidget::parseOrExpression(
3553  const QStringList& tokens, int& pos) {
3554  // OR has lowest precedence: expr = xor_expr (| xor_expr)*
3555  cvSelectionData left = parseXorExpression(tokens, pos);
3556 
3557  while (pos < tokens.size() && tokens[pos] == "|") {
3558  pos++; // consume '|'
3559  cvSelectionData right = parseXorExpression(tokens, pos);
3560  left = cvSelectionAlgebra::unionOf(left, right);
3561  }
3562 
3563  return left;
3564 }
3565 
3566 //-----------------------------------------------------------------------------
3567 cvSelectionData cvSelectionPropertiesWidget::parseXorExpression(
3568  const QStringList& tokens, int& pos) {
3569  // XOR: expr = and_expr (^ and_expr)*
3570  cvSelectionData left = parseAndExpression(tokens, pos);
3571 
3572  while (pos < tokens.size() && tokens[pos] == "^") {
3573  pos++; // consume '^'
3574  cvSelectionData right = parseAndExpression(tokens, pos);
3575  left = cvSelectionAlgebra::symmetricDifferenceOf(left, right);
3576  }
3577 
3578  return left;
3579 }
3580 
3581 //-----------------------------------------------------------------------------
3582 cvSelectionData cvSelectionPropertiesWidget::parseAndExpression(
3583  const QStringList& tokens, int& pos) {
3584  // AND: expr = unary_expr (& unary_expr)*
3585  cvSelectionData left = parseUnaryExpression(tokens, pos);
3586 
3587  while (pos < tokens.size() && tokens[pos] == "&") {
3588  pos++; // consume '&'
3589  cvSelectionData right = parseUnaryExpression(tokens, pos);
3590  left = cvSelectionAlgebra::intersectionOf(left, right);
3591  }
3592 
3593  return left;
3594 }
3595 
3596 //-----------------------------------------------------------------------------
3597 cvSelectionData cvSelectionPropertiesWidget::parseUnaryExpression(
3598  const QStringList& tokens, int& pos) {
3599  // Unary: expr = !expr | primary
3600  if (pos < tokens.size() && tokens[pos] == "!") {
3601  pos++; // consume '!'
3602  cvSelectionData operand = parseUnaryExpression(tokens, pos);
3603 
3604  // Complement needs polyData
3605  vtkPolyData* polyData = getPolyDataForSelection(&operand);
3606  if (polyData) {
3607  return cvSelectionAlgebra::complementOf(polyData, operand);
3608  } else {
3610  "[cvSelectionPropertiesWidget] Cannot compute complement: "
3611  "no polyData");
3612  return cvSelectionData();
3613  }
3614  }
3615 
3616  return parsePrimaryExpression(tokens, pos);
3617 }
3618 
3619 //-----------------------------------------------------------------------------
3620 cvSelectionData cvSelectionPropertiesWidget::parsePrimaryExpression(
3621  const QStringList& tokens, int& pos) {
3622  // Primary: ( expr ) | selection_name
3623  if (pos >= tokens.size()) {
3625  "[cvSelectionPropertiesWidget] Unexpected end of expression");
3626  return cvSelectionData();
3627  }
3628 
3629  if (tokens[pos] == "(") {
3630  pos++; // consume '('
3631  cvSelectionData result = parseOrExpression(tokens, pos);
3632 
3633  if (pos < tokens.size() && tokens[pos] == ")") {
3634  pos++; // consume ')'
3635  } else {
3637  "[cvSelectionPropertiesWidget] Missing closing "
3638  "parenthesis");
3639  }
3640 
3641  return result;
3642  }
3643 
3644  // Must be a selection name (e.g., "s0", "s1")
3645  QString name = tokens[pos];
3646  pos++;
3647 
3648  // Find the saved selection by name
3649  for (const SavedSelection& saved : m_savedSelections) {
3650  if (saved.name == name) {
3651  return saved.data;
3652  }
3653  }
3654 
3656  QString("[cvSelectionPropertiesWidget] Unknown selection: %1")
3657  .arg(name));
3658  return cvSelectionData();
3659 }
3660 
3661 //-----------------------------------------------------------------------------
3662 void cvSelectionPropertiesWidget::onSelectionEditorTableSelectionChanged() {
3663  bool hasSelection = !m_selectionEditorTable->selectedItems().isEmpty();
3664  m_removeSelectionButton->setEnabled(hasSelection);
3665 
3666  // Highlight the corresponding selection when row is selected
3667  QList<QTableWidgetItem*> selectedItems =
3668  m_selectionEditorTable->selectedItems();
3669  if (!selectedItems.isEmpty()) {
3670  int row = selectedItems.first()->row();
3671  if (row >= 0 && row < m_savedSelections.size()) {
3672  const SavedSelection& sel = m_savedSelections[row];
3673 
3674  // Highlight this selection's data
3675  if (m_highlighter && !sel.data.isEmpty()) {
3676  m_highlighter->highlightSelection(
3678 
3679  // Update the viewer
3680  PclUtils::PCLVis* pclVis = getPCLVis();
3681  if (pclVis) {
3682  pclVis->UpdateScreen();
3683  }
3684  }
3685  }
3686  }
3687 }
3688 
3689 //-----------------------------------------------------------------------------
3690 void cvSelectionPropertiesWidget::onSelectionEditorCellClicked(int row,
3691  int column) {
3692  // When clicking on Color column, open color picker
3693  if (column == 2 && row >= 0 && row < m_savedSelections.size()) {
3694  // Get current color
3695  QColor currentColor = m_savedSelections[row].color;
3696 
3697  // Open color dialog
3698  QColor newColor = QColorDialog::getColor(
3699  currentColor, this,
3700  tr("Select Color for %1").arg(m_savedSelections[row].name));
3701 
3702  if (newColor.isValid() && newColor != currentColor) {
3703  // Update the saved selection color
3704  m_savedSelections[row].color = newColor;
3705 
3706  // Update the table display
3707  QTableWidgetItem* colorItem = m_selectionEditorTable->item(row, 2);
3708  if (colorItem) {
3709  colorItem->setText(newColor.name());
3710  colorItem->setBackground(newColor);
3711  colorItem->setForeground(
3712  newColor.lightness() > 128 ? Qt::black : Qt::white);
3713  }
3714 
3715  CVLog::Print(QString("[cvSelectionPropertiesWidget] Changed color "
3716  "for %1 to %2")
3717  .arg(m_savedSelections[row].name)
3718  .arg(newColor.name()));
3719  }
3720  }
3721 
3722  // Highlight the selection for any cell click
3723  if (row >= 0 && row < m_savedSelections.size()) {
3724  const SavedSelection& sel = m_savedSelections[row];
3725  if (m_highlighter && !sel.data.isEmpty()) {
3726  m_highlighter->highlightSelection(
3728 
3729  PclUtils::PCLVis* pclVis = getPCLVis();
3730  if (pclVis) {
3731  pclVis->UpdateScreen();
3732  }
3733  }
3734  }
3735 }
3736 
3737 //-----------------------------------------------------------------------------
3738 void cvSelectionPropertiesWidget::onSelectionEditorCellDoubleClicked(
3739  int row, int column) {
3740  // Double-click on Color column also opens color picker
3741  if (column == 2) {
3742  onSelectionEditorCellClicked(row, column);
3743  }
3744  // Double-click on Name column could allow editing (future feature)
3745 }
3746 
3747 // ============================================================================
3748 // ParaView-style Find Data / Selected Data slots
3749 // ============================================================================
3750 
3751 //-----------------------------------------------------------------------------
3752 void cvSelectionPropertiesWidget::onAttributeTypeChanged(int index) {
3753  // Re-populate the spreadsheet based on selected attribute type
3754  vtkPolyData* polyData = getPolyDataForSelection(&m_selectionData);
3755  if (polyData) {
3756  updateSpreadsheetData(polyData);
3757  }
3758 }
3759 
3760 //-----------------------------------------------------------------------------
3761 void cvSelectionPropertiesWidget::onInvertSelectionToggled(bool checked) {
3762  // ParaView-style invert selection implementation
3763  // Reference: ParaView/Qt/Components/pqFindDataCurrentSelectionFrame.cxx
3764  // line 377-397
3765  //
3766  // Key principle: NEVER modify m_originalSelectionIds during invert toggle
3767  // Only modify display (highlight + spreadsheet), not the underlying
3768  // selection state
3769 
3771  QString("[cvSelectionPropertiesWidget] Invert selection: %1")
3772  .arg(checked ? "ON" : "OFF"));
3773 
3774  // Store original selection on first invert (if not already stored)
3775  if (m_originalSelectionIds.isEmpty()) {
3776  m_originalSelectionIds = m_selectionData.ids();
3777  }
3778 
3779  if (m_originalSelectionIds.isEmpty()) {
3781  "[cvSelectionPropertiesWidget] No original selection for "
3782  "inversion");
3783  return;
3784  }
3785 
3786  // Get polyData
3787  vtkPolyData* polyData = getPolyDataForSelection(&m_selectionData);
3788  if (!polyData) {
3790  "[cvSelectionPropertiesWidget] No polyData for inversion");
3791  return;
3792  }
3793 
3794  // Determine which IDs to display
3795  QVector<qint64> displayIds;
3796 
3797  bool isCellSelection =
3798  (m_selectionData.fieldAssociation() == cvSelectionData::CELLS);
3799  vtkIdType totalCount = isCellSelection ? polyData->GetNumberOfCells()
3800  : polyData->GetNumberOfPoints();
3801 
3802  if (checked) {
3803  // Invert: show all IDs NOT in original selection
3804  QSet<qint64> originalIdSet = qSetFromVector(m_originalSelectionIds);
3805 
3806  displayIds.reserve(static_cast<int>(totalCount) -
3807  m_originalSelectionIds.size());
3808  for (vtkIdType i = 0; i < totalCount; ++i) {
3809  if (!originalIdSet.contains(static_cast<qint64>(i))) {
3810  displayIds.append(static_cast<qint64>(i));
3811  }
3812  }
3813  } else {
3814  // Not inverted: show original selection
3815  displayIds = m_originalSelectionIds;
3816  }
3817 
3818  // Update 3D highlight
3819  if (m_highlighter) {
3822  idArray->SetNumberOfTuples(displayIds.size());
3823  for (int i = 0; i < displayIds.size(); ++i) {
3824  idArray->SetValue(i, static_cast<vtkIdType>(displayIds[i]));
3825  }
3826 
3827  int fieldAssoc =
3828  (m_selectionData.fieldAssociation() == cvSelectionData::CELLS)
3829  ? 0
3830  : 1;
3831  m_highlighter->highlightSelection(polyData, idArray, fieldAssoc,
3833 
3834  // Update labels
3835  QString pointLabelArray = m_highlighter->getPointLabelArrayName();
3836  QString cellLabelArray = m_highlighter->getCellLabelArrayName();
3837  if (!pointLabelArray.isEmpty() &&
3838  m_highlighter->isPointLabelVisible()) {
3839  m_highlighter->setPointLabelArray(pointLabelArray, true);
3840  }
3841  if (!cellLabelArray.isEmpty() && m_highlighter->isCellLabelVisible()) {
3842  m_highlighter->setCellLabelArray(cellLabelArray, true);
3843  }
3844  }
3845 
3846  // Update spreadsheet display ONLY (not full updateSelection)
3847  // Create temporary display data without modifying m_selectionData
3848  cvSelectionData displaySelection(displayIds,
3849  m_selectionData.fieldAssociation());
3850  if (m_selectionData.hasActorInfo()) {
3851  displaySelection.setActorInfo(m_selectionData.primaryActor(),
3852  m_selectionData.primaryPolyData());
3853  }
3854 
3855  // Update UI with custom selection (without modifying m_selectionData)
3856  // This is much cleaner and safer than temporarily replacing m_selectionData
3857  updateSpreadsheetData(polyData, &displaySelection);
3858  updateStatistics(polyData, &displaySelection);
3859 
3860  emit invertSelectionRequested();
3861 }
3862 
3863 //-----------------------------------------------------------------------------
3864 void cvSelectionPropertiesWidget::onFreezeClicked() {
3865  if (m_selectionData.isEmpty()) {
3866  CVLog::Warning("[cvSelectionPropertiesWidget] No selection to freeze");
3867  return;
3868  }
3869 
3870  // Freeze selection: Create a static copy that won't change with new
3871  // selections In ParaView, this converts the selection to an
3872  // "AppendSelection" filter For CloudViewer, we save current selection to
3873  // bookmarks with "Frozen_" prefix
3874 
3875  QString frozenName = QString("Frozen_%1")
3876  .arg(QDateTime::currentDateTime().toString(
3877  "yyyyMMdd_HHmmss"));
3878 
3879  // Bookmark functionality removed - UI not implemented
3880  CVLog::Print(
3881  QString("[cvSelectionPropertiesWidget] Selection frozen as: %1")
3882  .arg(frozenName));
3883 
3884  QMessageBox::information(this, tr("Freeze Selection"),
3885  tr("Selection frozen as: %1").arg(frozenName));
3886 
3887  emit freezeSelectionRequested();
3888 }
3889 
3890 //-----------------------------------------------------------------------------
3891 void cvSelectionPropertiesWidget::onExtractClicked() {
3892  if (m_selectionData.isEmpty()) {
3893  CVLog::Warning("[cvSelectionPropertiesWidget] No selection to extract");
3894  return;
3895  }
3896 
3897  // Extract selection: Create a new object from selected elements
3898  // This is equivalent to ParaView's "Extract Selection" filter
3899  // ParaView behavior: Export type depends on source data type, not selection
3900  // type
3901  // - Point clouds always export as point clouds (even with cell selection)
3902  // - Meshes export as meshes for cell selection, point clouds for point
3903  // selection
3904 
3905  bool isCells =
3906  (m_selectionData.fieldAssociation() == cvSelectionData::CELLS);
3907  bool isPoints =
3908  (m_selectionData.fieldAssociation() == cvSelectionData::POINTS);
3909 
3910  // Check source data type
3911  vtkPolyData* polyData = getPolyDataForSelection(&m_selectionData);
3912  bool isSourceMesh = false;
3913  if (polyData) {
3914  // A mesh has polygons (triangles/quads), a point cloud only has
3915  // vertices
3916  isSourceMesh = (polyData->GetNumberOfPolys() > 0);
3918  QString("[cvSelectionPropertiesWidget::onExtractClicked] "
3919  "Source: %1 points, %2 cells, %3 polys -> %4")
3920  .arg(polyData->GetNumberOfPoints())
3921  .arg(polyData->GetNumberOfCells())
3922  .arg(polyData->GetNumberOfPolys())
3923  .arg(isSourceMesh ? "mesh" : "point cloud"));
3924  }
3925 
3926  if (isCells && isSourceMesh) {
3927  // Cell selection on mesh -> export as mesh
3928  onExportToMeshClicked();
3929  } else {
3930  // Point selection OR cell selection on point cloud -> export as point
3931  // cloud For cell selection on point cloud, the cell IDs ARE the point
3932  // IDs (each vertex is a cell in VTK point cloud representation)
3933  onExportToPointCloudClicked();
3934  }
3935 
3937 }
3938 
3939 //-----------------------------------------------------------------------------
3940 void cvSelectionPropertiesWidget::onPlotOverTimeClicked() {
3941  // Plot Over Time: Show histogram/distribution of selected data
3942  // Reference: ParaView's SelectionPlot functionality
3943  if (m_selectionData.isEmpty()) {
3944  QMessageBox::information(this, tr("Plot Over Time"),
3945  tr("No selection data to plot."));
3946  return;
3947  }
3948 
3949  // Get polyData for the selection
3950  vtkPolyData* polyData = getPolyDataForSelection(&m_selectionData);
3951  if (!polyData) {
3952  QMessageBox::warning(this, tr("Plot Over Time"),
3953  tr("Cannot access selection data for plotting."));
3954  return;
3955  }
3956 
3957  // Emit signal for external handling
3958  emit plotOverTimeRequested();
3959 
3960  // Create plot dialog
3961  QDialog* plotDialog = new QDialog(this);
3962  plotDialog->setWindowTitle(tr("Selection Data Distribution"));
3963  plotDialog->setMinimumSize(700, 500);
3964  plotDialog->setAttribute(Qt::WA_DeleteOnClose);
3965 
3966  QVBoxLayout* dialogLayout = new QVBoxLayout(plotDialog);
3967 
3968  // Attribute selector
3969  QHBoxLayout* controlLayout = new QHBoxLayout();
3970  controlLayout->addWidget(new QLabel(tr("Attribute:")));
3971 
3972  QComboBox* attributeCombo = new QComboBox();
3973  bool isPointData =
3974  (m_selectionData.fieldAssociation() == cvSelectionData::POINTS);
3975 
3976  // Add "Coordinates" option for points
3977  if (isPointData) {
3978  attributeCombo->addItem(tr("X Coordinate"),
3979  QVariant::fromValue(QString("__X__")));
3980  attributeCombo->addItem(tr("Y Coordinate"),
3981  QVariant::fromValue(QString("__Y__")));
3982  attributeCombo->addItem(tr("Z Coordinate"),
3983  QVariant::fromValue(QString("__Z__")));
3984  }
3985 
3986  // Add data arrays
3987  vtkDataSetAttributes* data = isPointData
3988  ? static_cast<vtkDataSetAttributes*>(
3989  polyData->GetPointData())
3990  : static_cast<vtkDataSetAttributes*>(
3991  polyData->GetCellData());
3992 
3993  if (data) {
3994  for (int i = 0; i < data->GetNumberOfArrays(); ++i) {
3995  vtkDataArray* arr = data->GetArray(i);
3996  if (arr && arr->GetName()) {
3997  attributeCombo->addItem(
3998  QString::fromUtf8(arr->GetName()),
3999  QVariant::fromValue(QString::fromUtf8(arr->GetName())));
4000  }
4001  }
4002  }
4003 
4004  controlLayout->addWidget(attributeCombo);
4005  controlLayout->addStretch();
4006  dialogLayout->addLayout(controlLayout);
4007 
4008  // Create QCustomPlot widget
4009  QCustomPlot* customPlot = new QCustomPlot(plotDialog);
4010  customPlot->setMinimumHeight(400);
4011  customPlot->xAxis->setLabel(tr("Index / Timestamp"));
4012  customPlot->yAxis->setLabel(tr("Attribute Value"));
4015  customPlot->legend->setVisible(true);
4016  customPlot->legend->setFont(QFont("Helvetica", 9));
4017  dialogLayout->addWidget(customPlot);
4018 
4019  // Copy selection IDs for the lambda (avoid dangling pointer)
4020  QVector<qint64> selectionIds = m_selectionData.ids();
4021 
4022  // Helper to update plot
4023  auto updatePlot = [customPlot, polyData, selectionIds,
4024  isPointData](const QString& attrName) {
4025  // Clear all existing plots before drawing new ones
4026  while (customPlot->plottableCount() > 0) {
4027  customPlot->removePlottable(0);
4028  }
4029  customPlot->clearGraphs();
4030 
4031  if (selectionIds.isEmpty()) {
4032  customPlot->replot();
4033  return;
4034  }
4035 
4036  const QVector<qint64>& ids = selectionIds;
4037 
4038  // Collect X (timestamp or ID) and Y (attribute value) data
4039  QVector<double> xData; // timestamps or indices
4040  QVector<double> yData; // attribute values
4041  xData.reserve(ids.size());
4042  yData.reserve(ids.size());
4043 
4044  // Try to find timestamp array in field data
4045  vtkDataSetAttributes* data =
4046  isPointData ? static_cast<vtkDataSetAttributes*>(
4047  polyData->GetPointData())
4048  : static_cast<vtkDataSetAttributes*>(
4049  polyData->GetCellData());
4050 
4051  vtkDataArray* timestampArray = nullptr;
4052  if (data) {
4053  // Common timestamp field names
4054  const char* timestampNames[] = {"TimeValue", "Time",
4055  "timestamp", "time_value",
4056  "TimeStep", nullptr};
4057  for (int i = 0; timestampNames[i] != nullptr; ++i) {
4058  timestampArray = data->GetArray(timestampNames[i]);
4059  if (timestampArray) break;
4060  }
4061  }
4062 
4063  // Collect Y values (attribute) and X values (timestamp or index)
4064  for (int idx = 0; idx < ids.size(); ++idx) {
4065  qint64 id = ids[idx];
4066  if (id < 0) continue;
4067 
4068  double yVal = 0.0;
4069  if (attrName == "__X__" || attrName == "__Y__" ||
4070  attrName == "__Z__") {
4071  if (id < polyData->GetNumberOfPoints()) {
4072  double pt[3];
4073  polyData->GetPoint(id, pt);
4074  yVal = (attrName == "__X__") ? pt[0]
4075  : (attrName == "__Y__") ? pt[1]
4076  : pt[2];
4077  }
4078  } else {
4079  if (data) {
4080  vtkDataArray* arr =
4081  data->GetArray(attrName.toUtf8().constData());
4082  if (arr && id >= 0 && id < arr->GetNumberOfTuples()) {
4083  yVal = arr->GetTuple1(id);
4084  }
4085  }
4086  }
4087 
4088  // Get X value (timestamp if available, otherwise index)
4089  double xVal = idx; // default to index
4090  if (timestampArray && id >= 0 &&
4091  id < timestampArray->GetNumberOfTuples()) {
4092  xVal = timestampArray->GetTuple1(id);
4093  }
4094 
4095  xData.append(xVal);
4096  yData.append(yVal);
4097  }
4098 
4099  if (yData.isEmpty()) return;
4100 
4101  // Create line graph
4102  QCPGraph* graph = customPlot->addGraph();
4103  graph->setData(xData, yData);
4104  graph->setPen(QPen(QColor(0, 100, 180), 2));
4106  graph->setName(attrName.startsWith("__") ? attrName.mid(2, 1) + " Coord"
4107  : attrName);
4108 
4109  customPlot->rescaleAxes();
4110  customPlot->xAxis->setLabel(timestampArray ? tr("Timestamp")
4111  : tr("Index"));
4112  customPlot->yAxis->setLabel(attrName.startsWith("__")
4113  ? attrName.mid(2, 1) + " Coordinate"
4114  : attrName);
4115  customPlot->replot();
4116  };
4117 
4118  // Connect attribute change
4119  connect(attributeCombo, QOverload<int>::of(&QComboBox::currentIndexChanged),
4120  [updatePlot, attributeCombo]() {
4121  QString attrName = attributeCombo->currentData().toString();
4122  updatePlot(attrName);
4123  });
4124 
4125  // Initial plot
4126  if (attributeCombo->count() > 0) {
4127  QString attrName = attributeCombo->currentData().toString();
4128  updatePlot(attrName);
4129  }
4130 
4131  // Close button
4132  QDialogButtonBox* buttonBox = new QDialogButtonBox(QDialogButtonBox::Close);
4133  connect(buttonBox, &QDialogButtonBox::rejected, plotDialog,
4134  &QDialog::close);
4135  dialogLayout->addWidget(buttonBox);
4136 
4137  plotDialog->show();
4138 
4139  CVLog::Print(QString("[cvSelectionPropertiesWidget] Plot Over Time: "
4140  "Showing distribution for %1 %2")
4141  .arg(m_selectionData.count())
4142  .arg(m_selectionData.fieldTypeString()));
4143 }
4144 
4145 //-----------------------------------------------------------------------------
4146 void cvSelectionPropertiesWidget::onToggleColumnVisibility() {
4147  // Create a menu to toggle column visibility
4148  QMenu menu(this);
4149 
4150  for (int col = 0; col < m_spreadsheetTable->columnCount(); ++col) {
4151  QString header = m_spreadsheetTable->horizontalHeaderItem(col)->text();
4152  QAction* action = menu.addAction(header);
4153  action->setCheckable(true);
4154  action->setChecked(!m_spreadsheetTable->isColumnHidden(col));
4155  action->setData(col);
4156  connect(action, &QAction::toggled, [this, col](bool visible) {
4157  m_spreadsheetTable->setColumnHidden(col, !visible);
4158  });
4159  }
4160 
4161  menu.exec(QCursor::pos());
4162 }
4163 
4164 //-----------------------------------------------------------------------------
4165 void cvSelectionPropertiesWidget::onToggleFieldDataClicked(bool checked) {
4166  // Toggle between showing Point/Cell data and Field data
4167  // ParaView: When enabled, shows vtkFieldData arrays instead of selection
4168  // IDs
4169 
4170  if (!m_spreadsheetTable) {
4171  return;
4172  }
4173 
4174  // Get current polyData
4175  vtkPolyData* polyData = getPolyDataForSelection(&m_selectionData);
4176  if (!polyData) {
4177  return;
4178  }
4179 
4180  m_spreadsheetTable->clear();
4181  m_spreadsheetTable->setRowCount(0);
4182 
4183  if (checked) {
4184  // Show field data arrays (global data, not per-point/cell)
4185  vtkFieldData* fieldData = polyData->GetFieldData();
4186  if (!fieldData || fieldData->GetNumberOfArrays() == 0) {
4187  // No field data available
4188  m_spreadsheetTable->setColumnCount(1);
4189  m_spreadsheetTable->setHorizontalHeaderLabels(QStringList()
4190  << tr("Field Data"));
4191  m_spreadsheetTable->setRowCount(1);
4192  m_spreadsheetTable->setItem(
4193  0, 0, new QTableWidgetItem(tr("No field data available")));
4194  return;
4195  }
4196 
4197  // Build column headers from field data array names
4198  QStringList headers;
4199  headers << tr("Index");
4200  for (int i = 0; i < fieldData->GetNumberOfArrays(); ++i) {
4201  vtkAbstractArray* arr = fieldData->GetAbstractArray(i);
4202  if (arr && arr->GetName()) {
4203  headers << QString::fromStdString(arr->GetName());
4204  }
4205  }
4206 
4207  m_spreadsheetTable->setColumnCount(headers.size());
4208  m_spreadsheetTable->setHorizontalHeaderLabels(headers);
4209 
4210  // Find max number of tuples across all arrays
4211  vtkIdType maxTuples = 0;
4212  for (int i = 0; i < fieldData->GetNumberOfArrays(); ++i) {
4213  vtkAbstractArray* arr = fieldData->GetAbstractArray(i);
4214  if (arr) {
4215  maxTuples = std::max(maxTuples, arr->GetNumberOfTuples());
4216  }
4217  }
4218 
4219  // Limit rows
4220  int rowCount = std::min(1000, static_cast<int>(maxTuples));
4221  m_spreadsheetTable->setRowCount(rowCount);
4222 
4223  // Populate rows
4224  for (int row = 0; row < rowCount; ++row) {
4225  int col = 0;
4226 
4227  // Index column
4228  m_spreadsheetTable->setItem(
4229  row, col++, new QTableWidgetItem(QString::number(row)));
4230 
4231  // Data columns
4232  for (int i = 0; i < fieldData->GetNumberOfArrays(); ++i) {
4233  vtkAbstractArray* arr = fieldData->GetAbstractArray(i);
4234  if (arr && arr->GetName()) {
4235  QString valueStr;
4236  if (row < arr->GetNumberOfTuples()) {
4237  vtkDataArray* dataArr = vtkDataArray::SafeDownCast(arr);
4238  if (dataArr) {
4239  valueStr = QString::number(dataArr->GetTuple1(row),
4240  'g', 6);
4241  } else {
4242  // String array or other type
4243  vtkVariant v = arr->GetVariantValue(row);
4244  valueStr = QString::fromStdString(v.ToString());
4245  }
4246  }
4247  m_spreadsheetTable->setItem(row, col++,
4248  new QTableWidgetItem(valueStr));
4249  }
4250  }
4251  }
4252  } else {
4253  // Show normal selection data (Point/Cell data)
4254  updateSpreadsheetData(polyData);
4255  }
4256 }
4257 
4258 // ============================================================================
4259 // Create Selection (Find Data) slots
4260 // ============================================================================
4261 
4262 //-----------------------------------------------------------------------------
4263 void cvSelectionPropertiesWidget::onDataProducerChanged(int index) {
4264  // ParaView behavior: When Data Producer is "None" (index 0), disable
4265  // the rest of Create Selection controls
4266  bool hasProducer = (index > 0);
4267 
4268  // Enable/disable all Create Selection controls except Data Producer combo
4269  if (m_elementTypeCombo) m_elementTypeCombo->setEnabled(hasProducer);
4270  if (m_attributeCombo) m_attributeCombo->setEnabled(hasProducer);
4271  if (m_operatorCombo) m_operatorCombo->setEnabled(hasProducer);
4272  if (m_valueEdit) m_valueEdit->setEnabled(hasProducer);
4273 
4274  // Update available attributes based on selected data producer
4275  updateAttributeCombo();
4276 }
4277 
4278 //-----------------------------------------------------------------------------
4279 void cvSelectionPropertiesWidget::onElementTypeChanged(int index) {
4280  Q_UNUSED(index);
4281  // Update available attributes based on element type (Point/Cell)
4282  updateAttributeCombo();
4283 }
4284 
4285 //-----------------------------------------------------------------------------
4286 void cvSelectionPropertiesWidget::onFindDataClicked() {
4287  // Execute the query with support for multiple query rows
4288  if (m_queryRows.isEmpty()) {
4289  CVLog::Warning("[cvSelectionPropertiesWidget] No query rows available");
4290  return;
4291  }
4292 
4293  QString dataProducer = m_dataProducerCombo
4294  ? m_dataProducerCombo->currentText()
4295  : QString();
4296  QString elementType = m_elementTypeCombo ? m_elementTypeCombo->currentText()
4297  : tr("Point");
4298  bool isCell = (elementType == tr("Cell"));
4299 
4300  // Special operators that don't need a value
4301  QStringList noValueOps = {tr("is min"), tr("is max"), tr("is <= mean"),
4302  tr("is >= mean")};
4303 
4304  // Validate all query rows and collect conditions
4305  QVector<QPair<QString, QString>>
4306  queries; // attribute, operator, value stored as pair
4307  QStringList queryDescriptions;
4308 
4309  for (int i = 0; i < m_queryRows.size(); ++i) {
4310  const QueryRow& row = m_queryRows[i];
4311  QString attribute = row.attributeCombo->currentText();
4312  QString op = row.operatorCombo->currentText();
4313  QString value = row.valueEdit->text();
4314 
4315  if (attribute.isEmpty()) {
4316  QMessageBox::warning(
4317  this, tr("Find Data"),
4318  tr("Please select an attribute in query row %1.")
4319  .arg(i + 1));
4320  return;
4321  }
4322 
4323  if (value.isEmpty() && !noValueOps.contains(op)) {
4324  QMessageBox::warning(
4325  this, tr("Find Data"),
4326  tr("Please enter a value in query row %1.").arg(i + 1));
4327  return;
4328  }
4329 
4330  queryDescriptions.append(
4331  QString("%1 %2 %3").arg(attribute).arg(op).arg(value));
4332  }
4333 
4334  CVLog::Print(QString("[cvSelectionPropertiesWidget] Find Data with %1 "
4335  "condition(s): %2 (Element: %3)")
4336  .arg(m_queryRows.size())
4337  .arg(queryDescriptions.join(" AND "))
4338  .arg(elementType));
4339 
4340  // For backward compatibility, emit signal for the first condition
4341  if (!m_queryRows.isEmpty()) {
4342  const QueryRow& firstRow = m_queryRows[0];
4343  emit findDataRequested(dataProducer, elementType,
4344  firstRow.attributeCombo->currentText(),
4345  firstRow.operatorCombo->currentText(),
4346  firstRow.valueEdit->text());
4347  }
4348 
4349  // Perform the query with all conditions (combined with AND logic)
4350  // Start with first query
4351  if (!m_queryRows.isEmpty()) {
4352  const QueryRow& firstRow = m_queryRows[0];
4353  performFindData(firstRow.attributeCombo->currentText(),
4354  firstRow.operatorCombo->currentText(),
4355  firstRow.valueEdit->text(), isCell);
4356 
4357  // For additional rows, we would need to perform intersection with
4358  // current selection This requires more complex logic to combine
4359  // multiple selection queries For now, we apply only the first condition
4360  // as a starting implementation
4361  // TODO: Implement AND logic for multiple query conditions
4362  if (m_queryRows.size() > 1) {
4364  QString("[cvSelectionPropertiesWidget] Multiple query "
4365  "conditions detected (%1 rows). "
4366  "Currently only the first condition is applied. "
4367  "Full AND logic implementation is pending.")
4368  .arg(m_queryRows.size()));
4369  }
4370  }
4371 }
4372 
4373 //-----------------------------------------------------------------------------
4374 void cvSelectionPropertiesWidget::onResetClicked() {
4375  // Reset all query rows to default state
4376  for (auto& row : m_queryRows) {
4377  if (row.attributeCombo && row.attributeCombo->count() > 0) {
4378  row.attributeCombo->setCurrentIndex(0);
4379  }
4380  if (row.operatorCombo) {
4381  row.operatorCombo->setCurrentIndex(0);
4382  }
4383  if (row.valueEdit) {
4384  row.valueEdit->clear();
4385  }
4386  }
4387 
4388  if (m_processIdSpinBox) {
4389  m_processIdSpinBox->setValue(-1);
4390  }
4391 
4392  // Remove additional query rows (keep only the first one)
4393  while (m_queriesLayout && m_queriesLayout->count() > 0) {
4394  QLayoutItem* item = m_queriesLayout->takeAt(0);
4395  if (item->widget()) {
4396  delete item->widget();
4397  }
4398  delete item;
4399  }
4400 
4401  CVLog::Print("[cvSelectionPropertiesWidget] Query reset to default.");
4402 }
4403 
4404 //-----------------------------------------------------------------------------
4405 void cvSelectionPropertiesWidget::onClearClicked() {
4406  // Clear current selection
4407  clearSelection();
4408 
4409  // Also clear the query
4410  onResetClicked();
4411 
4412  CVLog::Print("[cvSelectionPropertiesWidget] Selection and query cleared.");
4413 }
4414 
4415 //-----------------------------------------------------------------------------
4416 void cvSelectionPropertiesWidget::addQueryRow(int index,
4417  const QString& attribute,
4418  const QString& op,
4419  const QString& value) {
4420  if (index == -1) {
4421  index = m_queryRows.size();
4422  }
4423 
4424  QueryRow row;
4425 
4426  // Create container widget for this row
4427  row.container = new QWidget(m_createSelectionContainer);
4428  QHBoxLayout* rowLayout = new QHBoxLayout(row.container);
4429  rowLayout->setContentsMargins(0, 0, 0, 0);
4430  rowLayout->setSpacing(3);
4431 
4432  // Attribute combo
4433  row.attributeCombo = new QComboBox(row.container);
4434  row.attributeCombo->setMinimumWidth(80);
4435  row.attributeCombo->setToolTip(tr("Select attribute to query"));
4436  rowLayout->addWidget(row.attributeCombo);
4437 
4438  // Operator combo
4439  row.operatorCombo = new QComboBox(row.container);
4440  row.operatorCombo->addItems({tr("is"), tr(">="), tr("<="), tr(">"), tr("<"),
4441  tr("!="), tr("is min"), tr("is max"),
4442  tr("is <= mean"), tr("is >= mean")});
4443  row.operatorCombo->setToolTip(tr("Select comparison operator"));
4444  if (!op.isEmpty()) {
4445  int opIndex = row.operatorCombo->findText(op);
4446  if (opIndex >= 0) {
4447  row.operatorCombo->setCurrentIndex(opIndex);
4448  }
4449  }
4450  rowLayout->addWidget(row.operatorCombo);
4451 
4452  // Value input
4453  row.valueEdit = new QLineEdit(row.container);
4454  row.valueEdit->setPlaceholderText(tr("value"));
4455  row.valueEdit->setToolTip(tr("Enter comparison value"));
4456  if (!value.isEmpty()) {
4457  row.valueEdit->setText(value);
4458  }
4459  rowLayout->addWidget(row.valueEdit, 1); // stretch factor 1
4460 
4461  // Plus button (add row after this one)
4462  row.plusButton = new QPushButton(row.container);
4463  QIcon plusIcon(":/Resources/images/svg/pqPlus.svg");
4464  if (plusIcon.isNull()) {
4465  row.plusButton->setText("+");
4466  } else {
4467  row.plusButton->setIcon(plusIcon);
4468  }
4469  row.plusButton->setToolTip(tr("Add query condition"));
4470  row.plusButton->setMaximumWidth(32);
4471  rowLayout->addWidget(row.plusButton);
4472 
4473  // Minus button (remove this row)
4474  row.minusButton = new QPushButton(row.container);
4475  QIcon minusIcon(":/Resources/images/svg/pqMinus.svg");
4476  if (minusIcon.isNull()) {
4477  row.minusButton->setText("-");
4478  } else {
4479  row.minusButton->setIcon(minusIcon);
4480  }
4481  row.minusButton->setToolTip(tr("Remove query condition"));
4482  row.minusButton->setMaximumWidth(32);
4483  rowLayout->addWidget(row.minusButton);
4484 
4485  row.container->setLayout(rowLayout);
4486 
4487  // Insert into layout and list
4488  m_queriesLayout->insertWidget(index, row.container);
4489  m_queryRows.insert(index, row);
4490 
4491  // Connect signals
4492  connect(row.plusButton, &QPushButton::clicked, [this, row]() {
4493  int idx = m_queryRows.indexOf(row);
4494  if (idx >= 0) {
4495  addQueryRow(idx + 1);
4496  }
4497  });
4498 
4499  connect(row.minusButton, &QPushButton::clicked, [this, row]() {
4500  int idx = m_queryRows.indexOf(row);
4501  if (idx >= 0) {
4502  removeQueryRow(idx);
4503  }
4504  });
4505 
4506  // Update attribute combo if first row
4507  if (index == 0 && !attribute.isEmpty()) {
4508  int attrIndex = row.attributeCombo->findText(attribute);
4509  if (attrIndex >= 0) {
4510  row.attributeCombo->setCurrentIndex(attrIndex);
4511  }
4512  }
4513 
4514  // Update button states (disable minus if only one row)
4515  updateQueryRowButtons();
4516 
4518  QString("[cvSelectionPropertiesWidget] Added query row at index %1")
4519  .arg(index));
4520 }
4521 
4522 //-----------------------------------------------------------------------------
4523 void cvSelectionPropertiesWidget::removeQueryRow(int index) {
4524  if (index < 0 || index >= m_queryRows.size()) {
4525  return;
4526  }
4527 
4528  // Can't remove if only one row
4529  if (m_queryRows.size() <= 1) {
4531  "[cvSelectionPropertiesWidget] Cannot remove the last query "
4532  "row");
4533  return;
4534  }
4535 
4536  QueryRow row = m_queryRows.takeAt(index);
4537 
4538  // Remove from layout and delete widgets
4539  m_queriesLayout->removeWidget(row.container);
4540  delete row.container; // This will delete all child widgets
4541 
4542  // Update legacy pointers if we removed the first row
4543  if (index == 0 && !m_queryRows.isEmpty()) {
4544  m_attributeCombo = m_queryRows[0].attributeCombo;
4545  m_operatorCombo = m_queryRows[0].operatorCombo;
4546  m_valueEdit = m_queryRows[0].valueEdit;
4547  }
4548 
4549  // Update button states
4550  updateQueryRowButtons();
4551 
4553  QString("[cvSelectionPropertiesWidget] Removed query row "
4554  "at index %1")
4555  .arg(index));
4556 }
4557 
4558 //-----------------------------------------------------------------------------
4559 void cvSelectionPropertiesWidget::updateQueryRowButtons() {
4560  bool canRemove = (m_queryRows.size() > 1);
4561  for (auto& row : m_queryRows) {
4562  row.minusButton->setEnabled(canRemove);
4563  }
4564 }
4565 
4566 //-----------------------------------------------------------------------------
4567 void cvSelectionPropertiesWidget::updateDataProducerCombo() {
4568  if (!m_dataProducerCombo) {
4570  "[cvSelectionPropertiesWidget] m_dataProducerCombo is null!");
4571  return;
4572  }
4573 
4574  m_dataProducerCombo->clear();
4575 
4576  // Add "(none)" option as default like ParaView
4577  m_dataProducerCombo->addItem(tr("(none)"));
4578 
4579  // Get list of available data sources from all renderers
4580  // Use "DatasetName" field data like PCLVis stores entity names
4581  PclUtils::PCLVis* pclVis = getPCLVis();
4582  if (pclVis) {
4583  vtkRendererCollection* renderers = pclVis->getRendererCollection();
4584  if (renderers) {
4585  QSet<QString> addedNames;
4586 
4587  // Iterate through ALL renderers to get all data producers
4588  renderers->InitTraversal();
4589  vtkRenderer* renderer;
4590  while ((renderer = renderers->GetNextItem()) != nullptr) {
4591  vtkActorCollection* actors = renderer->GetActors();
4592  if (!actors) continue;
4593 
4594  actors->InitTraversal();
4595  vtkActor* actor;
4596  while ((actor = actors->GetNextActor()) != nullptr) {
4597  // Include visible OR pickable actors (more inclusive)
4598  if (!actor->GetVisibility()) {
4599  continue;
4600  }
4601 
4602  QString name;
4603 
4604  // Get the entity name from "DatasetName" field data
4605  // This is how PCLVis stores the ccHObject name
4606  vtkPolyData* polyData = vtkPolyData::SafeDownCast(
4607  actor->GetMapper() ? actor->GetMapper()->GetInput()
4608  : nullptr);
4609  if (polyData) {
4610  vtkFieldData* fieldData = polyData->GetFieldData();
4611  if (fieldData) {
4612  // Primary: check "DatasetName" (used by PCLVis)
4613  vtkStringArray* datasetNameArray =
4614  vtkStringArray::SafeDownCast(
4615  fieldData->GetAbstractArray(
4616  "DatasetName"));
4617  if (datasetNameArray &&
4618  datasetNameArray->GetNumberOfTuples() > 0) {
4619  name = QString::fromStdString(
4620  datasetNameArray->GetValue(0));
4621  }
4622 
4623  // Fallback: check "Name" array
4624  if (name.isEmpty()) {
4625  vtkAbstractArray* nameArray =
4626  fieldData->GetAbstractArray("Name");
4627  if (nameArray &&
4628  nameArray->GetNumberOfTuples() > 0) {
4629  vtkVariant v =
4630  nameArray->GetVariantValue(0);
4631  name = QString::fromStdString(v.ToString());
4632  }
4633  }
4634  }
4635  }
4636 
4637  // Skip if no valid name found
4638  if (name.isEmpty()) {
4639  continue;
4640  }
4641 
4642  // Add only unique names
4643  if (!addedNames.contains(name)) {
4644  m_dataProducerCombo->addItem(name);
4645  addedNames.insert(name);
4646  }
4647  }
4648  }
4649 
4651  QString("[cvSelectionPropertiesWidget] Found %1 "
4652  "data producers")
4653  .arg(addedNames.size()));
4654  }
4655  }
4656 
4657  // Also update attribute combo with first data producer
4658  updateAttributeCombo();
4659 
4660  // IMPORTANT: Manually trigger the enable/disable logic for Create Selection
4661  // controls When items are added to combo, the currentIndex may still be 0
4662  // (None), but we need to re-evaluate the enable state based on the new
4663  // state
4664  onDataProducerChanged(m_dataProducerCombo->currentIndex());
4665 }
4666 
4667 //-----------------------------------------------------------------------------
4668 void cvSelectionPropertiesWidget::updateAttributeCombo() {
4669  // Update all query row attribute combos
4670  if (m_queryRows.isEmpty()) {
4671  return;
4672  }
4673 
4674  // Store current selections to preserve them if possible
4675  QStringList currentAttributes;
4676  for (const auto& row : m_queryRows) {
4677  if (row.attributeCombo && row.attributeCombo->count() > 0) {
4678  currentAttributes.append(row.attributeCombo->currentText());
4679  } else {
4680  currentAttributes.append(QString());
4681  }
4682  }
4683 
4684  // Clear all attribute combos (but keep the legacy pointer updated)
4685  for (auto& row : m_queryRows) {
4686  if (row.attributeCombo) {
4687  row.attributeCombo->clear();
4688  }
4689  }
4690 
4691  // ParaView behavior: ID fields should be associated with the selected Data
4692  // Producer IMPORTANT: Only use polyData from the SELECTED Data Producer,
4693  // NOT from other objects
4694  vtkPolyData* polyData = nullptr;
4695  PclUtils::PCLVis* pclVis = getPCLVis();
4696  bool hasExplicitProducer =
4697  m_dataProducerCombo && m_dataProducerCombo->currentIndex() > 0;
4698 
4699  // First, try to get from selected Data Producer (if any)
4700  if (pclVis && hasExplicitProducer) {
4701  QString producerName = m_dataProducerCombo->currentText();
4702  vtkRenderer* renderer =
4703  pclVis->getRendererCollection()->GetFirstRenderer();
4704  if (renderer) {
4705  vtkActorCollection* actors = renderer->GetActors();
4706  actors->InitTraversal();
4707  vtkActor* actor;
4708  while ((actor = actors->GetNextActor()) != nullptr) {
4709  if (!actor->GetVisibility() || !actor->GetPickable()) {
4710  continue;
4711  }
4712  vtkPolyData* actorPolyData = vtkPolyData::SafeDownCast(
4713  actor->GetMapper() ? actor->GetMapper()->GetInput()
4714  : nullptr);
4715  if (actorPolyData) {
4716  vtkFieldData* fieldData = actorPolyData->GetFieldData();
4717  if (fieldData) {
4718  vtkStringArray* nameArray =
4719  vtkStringArray::SafeDownCast(
4720  fieldData->GetAbstractArray(
4721  "DatasetName"));
4722  if (nameArray && nameArray->GetNumberOfTuples() > 0) {
4723  QString name = QString::fromStdString(
4724  nameArray->GetValue(0));
4725  if (name == producerName) {
4726  polyData = actorPolyData;
4727  break;
4728  }
4729  }
4730  }
4731  }
4732  }
4733  }
4734 
4735  // If explicit producer selected but not found, don't fallback to other
4736  // objects
4737  if (!polyData) {
4738  CVLog::PrintVerbose(QString("[updateAttributeCombo] Data Producer "
4739  "'%1' not found")
4740  .arg(producerName));
4741  // Just add ID field, no other attributes
4742  m_attributeCombo->addItem(tr("ID"));
4743  return;
4744  }
4745  }
4746 
4747  // Fallback: Get from current selection ONLY if no explicit Data Producer
4748  // selected
4749  if (!polyData && !hasExplicitProducer) {
4750  polyData = getPolyDataForSelection(&m_selectionData);
4751  }
4752 
4753  if (!polyData) {
4754  // No polyData available - just show ID field
4755  m_attributeCombo->addItem(tr("ID"));
4756  return;
4757  }
4758 
4759  bool isCell = m_elementTypeCombo && m_elementTypeCombo->currentIndex() == 1;
4760  vtkDataSetAttributes* attrData =
4761  isCell ? static_cast<vtkDataSetAttributes*>(polyData->GetCellData())
4762  : static_cast<vtkDataSetAttributes*>(
4763  polyData->GetPointData());
4764 
4765  // Add ID first (like ParaView - just "ID", not "PointID")
4766  m_attributeCombo->addItem(tr("ID"));
4767 
4768  // Add "Points" array (coordinates) - ParaView style with Magnitude, X, Y, Z
4769  // This represents the 3D position of each point/cell center
4770  if (polyData && polyData->GetPoints()) {
4771  m_attributeCombo->addItem(tr("Points (Magnitude)"));
4772  m_attributeCombo->addItem(tr("Points (X)"));
4773  m_attributeCombo->addItem(tr("Points (Y)"));
4774  m_attributeCombo->addItem(tr("Points (Z)"));
4775  }
4776 
4777  // Helper lambda to add multi-component arrays in ParaView format
4778  // For color arrays (RGB/RGBA), we add the name without "(magnitude)"
4779  // For vector arrays (Normals, etc.), we add "(Magnitude)" then components
4780  auto addArrayToCombo = [this](vtkDataArray* array, const QString& name,
4781  const char* compNames[], int numCompNames,
4782  bool isColor) {
4783  if (!array) return;
4784  int numComponents = array->GetNumberOfComponents();
4785  if (numComponents == 1) {
4786  m_attributeCombo->addItem(name);
4787  } else if (numComponents > 1) {
4788  if (isColor) {
4789  // For color arrays, add the name first (not magnitude)
4790  // ParaView shows "Colors" or "RGB", not "Colors (magnitude)"
4791  m_attributeCombo->addItem(name);
4792  } else {
4793  // For vector arrays, add magnitude first
4794  m_attributeCombo->addItem(QString("%1 (Magnitude)").arg(name));
4795  }
4796  // Add individual components
4797  for (int c = 0; c < numComponents; ++c) {
4798  QString compName;
4799  const char* vtkCompName = array->GetComponentName(c);
4800  if (vtkCompName && strlen(vtkCompName) > 0) {
4801  compName = QString::fromUtf8(vtkCompName);
4802  } else if (c < numCompNames && compNames) {
4803  compName = compNames[c];
4804  } else {
4805  compName = QString::number(c);
4806  }
4807  m_attributeCombo->addItem(
4808  QString("%1 (%2)").arg(name, compName));
4809  }
4810  }
4811  };
4812 
4813  // Track added array names to avoid duplicates
4814  QSet<QString> addedArrays;
4815 
4816  // Track color arrays to avoid duplicates (RGB and Colors are the same
4817  // thing) PCLVis.cpp uses "RGB" for point clouds and cc2sm.cpp uses "Colors"
4818  // for meshes
4819  QSet<QString> colorArrayVariants = {"RGB", "Colors", "rgba", "rgb", "RGBA"};
4820 
4821  // Add VTK "active" arrays first (Normals, TCoords, Scalars/Colors)
4822  // These are set in PCLVis draw functions via SetNormals(), SetTCoords(),
4823  // SetScalars() Field names in PCLVis.cpp:
4824  // - Normals: set via polydata->GetPointData()->SetNormals() (only when
4825  // showNorms=true)
4826  // - Colors: named "Colors", set via polydata->GetPointData()->SetScalars()
4827  // - TCoords: named "TCoords0", "TCoords1", etc., first set via SetTCoords()
4828  if (attrData) {
4829  // Add Normals (3 components: X, Y, Z)
4830  // Set by updateShadingMode() and addTextureMesh() in PCLVis.cpp
4831  // NOTE: Normals are only added to VTK when "show normals" is enabled!
4832  // Check both active normals AND PCL-style separate arrays
4833  vtkDataArray* normals = attrData->GetNormals();
4834  bool hasNormals = (normals != nullptr);
4835 
4836  // If no active normals, check for PCL-style separate normal arrays
4837  // PCL stores normals as normal_x, normal_y, normal_z in PCLCloud
4838  // These might be present as 3-component arrays or separate 1-component
4839  // arrays
4840  if (!hasNormals) {
4841  // Check for a 3-component array named "Normals" or similar
4842  for (int i = 0; i < attrData->GetNumberOfArrays(); ++i) {
4843  vtkDataArray* arr = attrData->GetArray(i);
4844  if (arr && arr->GetNumberOfComponents() == 3) {
4845  const char* name = arr->GetName();
4846  if (name) {
4847  QString qname = QString::fromUtf8(name).toLower();
4848  if (qname.contains("normal")) {
4849  normals = arr;
4850  hasNormals = true;
4851  break;
4852  }
4853  }
4854  }
4855  }
4856  }
4857 
4858  // Also check for separate normal_x, normal_y, normal_z arrays
4859  // (PCL style when not combined)
4860  if (!hasNormals) {
4861  vtkDataArray* nx = attrData->GetArray("normal_x");
4862  vtkDataArray* ny = attrData->GetArray("normal_y");
4863  vtkDataArray* nz = attrData->GetArray("normal_z");
4864  if (nx && ny && nz) {
4865  hasNormals = true;
4866  // Mark these as processed
4867  addedArrays.insert("normal_x");
4868  addedArrays.insert("normal_y");
4869  addedArrays.insert("normal_z");
4871  "[updateAttributeCombo] Found PCL-style separate "
4872  "normals");
4873  }
4874  }
4875 
4876  if (hasNormals) {
4877  QString normalsName =
4878  (normals && normals->GetName() &&
4879  strlen(normals->GetName()))
4880  ? QString::fromUtf8(normals->GetName())
4881  : tr("Normals");
4882  // Add Normals entry with components (Magnitude, X, Y, Z)
4883  m_attributeCombo->addItem(
4884  QString("%1 (Magnitude)").arg(normalsName));
4885  m_attributeCombo->addItem(QString("%1 (X)").arg(normalsName));
4886  m_attributeCombo->addItem(QString("%1 (Y)").arg(normalsName));
4887  m_attributeCombo->addItem(QString("%1 (Z)").arg(normalsName));
4888  addedArrays.insert(normalsName);
4889  addedArrays.insert("Normals");
4890  if (normals && normals->GetName()) {
4891  addedArrays.insert(QString::fromUtf8(normals->GetName()));
4892  }
4893  }
4894 
4895  // Add TCoords/Texture Coordinates (2 or 3 components: U, V, [W])
4896  // Set by addTextureMesh() in PCLVis.cpp, named "TCoords0", "TCoords1"
4897  vtkDataArray* tcoords = attrData->GetTCoords();
4898  if (tcoords) {
4899  QString tcoordsName =
4900  (tcoords->GetName() && strlen(tcoords->GetName()))
4901  ? QString::fromUtf8(tcoords->GetName())
4902  : tr("TCoords");
4903  static const char* tcoordsComps[] = {"U", "V", "W"};
4904  addArrayToCombo(tcoords, tcoordsName, tcoordsComps, 3, false);
4905  addedArrays.insert(tcoordsName);
4906  }
4907 
4908  // Add Scalars/Colors (RGB: 3 components, RGBA: 4 components)
4909  // Set by addTextureMesh() in PCLVis.cpp
4910  // ParaView convention: always use "RGB" for color arrays, not "Colors"
4911  vtkDataArray* scalars = attrData->GetScalars();
4912  if (scalars) {
4913  QString scalarsName =
4914  (scalars->GetName() && strlen(scalars->GetName()))
4915  ? QString::fromUtf8(scalars->GetName())
4916  : tr("RGB");
4917  // // Normalize "Colors" to "RGB" for consistency
4918  // if (scalarsName.compare("Colors", Qt::CaseInsensitive) == 0) {
4919  // scalarsName = tr("RGB");
4920  // }
4921  int numComp = scalars->GetNumberOfComponents();
4922  if (numComp == 3) {
4923  static const char* rgbComps[] = {"R", "G", "B"};
4924  addArrayToCombo(scalars, scalarsName, rgbComps, 3, true);
4925  } else if (numComp == 4) {
4926  static const char* rgbaComps[] = {"R", "G", "B", "A"};
4927  addArrayToCombo(scalars, scalarsName, rgbaComps, 4, true);
4928  } else {
4929  addArrayToCombo(scalars, scalarsName, nullptr, 0, false);
4930  }
4931  addedArrays.insert(scalarsName);
4932 
4933  // Mark all color array variants as added to avoid duplicates
4934  // RGB and Colors are the same thing - just different names used by
4935  // different code paths (PCLVis uses "RGB", cc2sm uses "Colors")
4936  for (const QString& variant : colorArrayVariants) {
4937  addedArrays.insert(variant);
4938  }
4939  }
4940  }
4941 
4942  // Add all other arrays with component handling - ParaView format:
4943  // - Single component: just array name
4944  // - Multi-component: "{name} (Magnitude)" then "{name} ({componentName})"
4945  if (attrData) {
4946  for (int i = 0; i < attrData->GetNumberOfArrays(); ++i) {
4947  vtkDataArray* array = attrData->GetArray(i);
4948  if (!array) continue;
4949 
4950  const char* arrayName = array->GetName();
4951  if (!arrayName || strlen(arrayName) == 0) continue;
4952 
4953  QString name = QString::fromUtf8(arrayName);
4954 
4955  // Skip VTK internal arrays - ParaView filters these out
4956  // Reference: vtkSMTooltipSelectionPipeline.cxx
4957  if (name.startsWith("vtk", Qt::CaseInsensitive) ||
4958  name == "vtkOriginalPointIds" || name == "vtkOriginalCellIds" ||
4959  name == "vtkCompositeIndex" || name == "vtkBlockColors" ||
4960  name == "vtkGhostType") {
4961  continue;
4962  }
4963 
4964  // Skip if already added as a special array
4965  if (addedArrays.contains(name)) continue;
4966  addedArrays.insert(name);
4967 
4968  int numComponents = array->GetNumberOfComponents();
4969 
4970  // Check if this looks like a color array (RGB/RGBA unsigned char)
4971  bool isColorArray = false;
4972  QString lowerName = name.toLower();
4973  if ((numComponents == 3 || numComponents == 4) &&
4974  (lowerName.contains("color") || lowerName.contains("rgb") ||
4975  lowerName.contains("rgba"))) {
4976  isColorArray = true;
4977 
4978  // If this is a color array and we've already added any color
4979  // array, skip it to avoid duplicates (RGB and Colors are the
4980  // same thing)
4981  bool alreadyHasColors = false;
4982  for (const QString& variant : colorArrayVariants) {
4983  if (addedArrays.contains(variant)) {
4984  alreadyHasColors = true;
4985  break;
4986  }
4987  }
4988  if (alreadyHasColors) {
4989  // Mark this as added to avoid future duplicates
4990  addedArrays.insert(name);
4991  continue; // Skip this array
4992  }
4993 
4994  // Mark all color variants as added
4995  for (const QString& variant : colorArrayVariants) {
4996  addedArrays.insert(variant);
4997  }
4998  }
4999 
5000  if (numComponents == 1) {
5001  // Single component - add as is (like ParaView)
5002  m_attributeCombo->addItem(name);
5003  } else if (numComponents > 1) {
5004  if (isColorArray) {
5005  // Color array - add name without magnitude
5006  m_attributeCombo->addItem(name);
5007  } else {
5008  // Multi-component - ParaView format: magnitude first, then
5009  // individual components
5010  m_attributeCombo->addItem(
5011  QString("%1 (Magnitude)").arg(name));
5012  }
5013 
5014  // Add individual components with their names
5015  for (int c = 0; c < numComponents; ++c) {
5016  QString compName;
5017  const char* vtkCompName = array->GetComponentName(c);
5018  if (vtkCompName && strlen(vtkCompName) > 0) {
5019  compName = QString::fromUtf8(vtkCompName);
5020  } else {
5021  // Default component names based on component count
5022  if (isColorArray && numComponents >= 3) {
5023  static const char* rgbaComps[] = {"R", "G", "B",
5024  "A"};
5025  compName = rgbaComps[c];
5026  } else if (numComponents == 3) {
5027  static const char* xyz[] = {"X", "Y", "Z"};
5028  compName = xyz[c];
5029  } else if (numComponents == 4) {
5030  static const char* xyzw[] = {"X", "Y", "Z", "W"};
5031  compName = xyzw[c];
5032  } else if (numComponents == 2) {
5033  static const char* xy[] = {"X", "Y"};
5034  compName = xy[c];
5035  } else {
5036  compName = QString::number(c);
5037  }
5038  }
5039  m_attributeCombo->addItem(
5040  QString("%1 (%2)").arg(name, compName));
5041  }
5042  }
5043  }
5044  }
5045 
5046  // Add Point/Cell at the end (like ParaView)
5047  if (!isCell && polyData->GetPoints()) {
5048  m_attributeCombo->addItem(tr("Point"));
5049  } else if (isCell) {
5050  m_attributeCombo->addItem(tr("Cell"));
5051  }
5052 
5053  // Update all query rows with the same attributes
5054  // First, collect all items from the first combo (which we just populated)
5055  QStringList attributes;
5056  if (!m_queryRows.isEmpty() && m_queryRows[0].attributeCombo) {
5057  QComboBox* firstCombo = m_queryRows[0].attributeCombo;
5058  for (int i = 0; i < firstCombo->count(); ++i) {
5059  attributes.append(firstCombo->itemText(i));
5060  }
5061 
5062  // Apply to all other combos
5063  for (int rowIdx = 1; rowIdx < m_queryRows.size(); ++rowIdx) {
5064  if (m_queryRows[rowIdx].attributeCombo) {
5065  QString current =
5066  m_queryRows[rowIdx].attributeCombo->currentText();
5067  m_queryRows[rowIdx].attributeCombo->clear();
5068  m_queryRows[rowIdx].attributeCombo->addItems(attributes);
5069 
5070  // Try to restore previous selection
5071  if (!current.isEmpty()) {
5072  int idx = m_queryRows[rowIdx].attributeCombo->findText(
5073  current);
5074  if (idx >= 0) {
5075  m_queryRows[rowIdx].attributeCombo->setCurrentIndex(
5076  idx);
5077  }
5078  }
5079  }
5080  }
5081  }
5082 
5083  // Set first item as current for all combos if they're empty
5084  for (auto& row : m_queryRows) {
5085  if (row.attributeCombo && row.attributeCombo->count() > 0 &&
5086  row.attributeCombo->currentIndex() < 0) {
5087  row.attributeCombo->setCurrentIndex(0);
5088  }
5089  }
5090 }
5091 
5092 //-----------------------------------------------------------------------------
5093 void cvSelectionPropertiesWidget::performFindData(const QString& attribute,
5094  const QString& op,
5095  const QString& value,
5096  bool isCell) {
5097  // Perform the query and create selection
5098  vtkPolyData* polyData = getPolyDataForSelection(&m_selectionData);
5099  if (!polyData) {
5100  QMessageBox::warning(this, tr("Find Data"),
5101  tr("No data available to query."));
5102  return;
5103  }
5104 
5105  // Parse attribute name and component
5106  // Supports both formats:
5107  // - ParaView-style: "NormalsX", "NormalsY", "RGB (Magnitude)"
5108  // - Legacy: "Normals (X)", "RGB (R)"
5109  QString arrayName = attribute;
5110  int componentIndex = -1; // -1 means use magnitude or scalar value
5111  bool isMagnitude = false;
5112  bool isIdQuery = (attribute == tr("ID"));
5113  // ParaView-style attribute format: "{arrayName} ({componentName})" or
5114  // "{arrayName} (magnitude)"
5115  bool isPointQuery = (attribute == tr("Point"));
5116  bool isCellQuery = (attribute == tr("Cell"));
5117 
5118  // Parse ParaView-style format: "{arrayName} ({componentName})" or
5119  // "{arrayName} (magnitude)"
5120  static QRegularExpression componentRegex(R"((.*)\s+\‍((.*)\)\s*$)");
5121  QRegularExpressionMatch match = componentRegex.match(attribute);
5122 
5123  if (match.hasMatch()) {
5124  arrayName = match.captured(1).trimmed();
5125  QString componentStr = match.captured(2).trimmed();
5126 
5127  // Check if it's magnitude (lowercase in ParaView)
5128  if (componentStr.toLower() == "magnitude") {
5129  isMagnitude = true;
5130  } else if (componentStr == "X" || componentStr == "R" ||
5131  componentStr == "U" || componentStr == "0") {
5132  componentIndex = 0;
5133  } else if (componentStr == "Y" || componentStr == "G" ||
5134  componentStr == "V" || componentStr == "1") {
5135  componentIndex = 1;
5136  } else if (componentStr == "Z" || componentStr == "B" ||
5137  componentStr == "W" || componentStr == "2") {
5138  componentIndex = 2;
5139  } else if (componentStr == "A" || componentStr == "3") {
5140  componentIndex = 3;
5141  } else {
5142  // Try to parse as numeric component index
5143  bool ok;
5144  componentIndex = componentStr.toInt(&ok);
5145  if (!ok) componentIndex = 0;
5146  }
5147  }
5148 
5149  // Handle Point/Cell query (position-based queries)
5150  vtkPoints* points = polyData->GetPoints();
5151 
5152  // Get the data array
5153  vtkDataArray* dataArray = nullptr;
5154 
5155  if (!isIdQuery && !isPointQuery && !isCellQuery) {
5156  vtkDataSetAttributes* attrData =
5157  isCell ? static_cast<vtkDataSetAttributes*>(
5158  polyData->GetCellData())
5159  : static_cast<vtkDataSetAttributes*>(
5160  polyData->GetPointData());
5161 
5162  // First try to get the array by name
5163  dataArray = attrData->GetArray(arrayName.toUtf8().constData());
5164 
5165  // If not found, check if it's one of the special "active" arrays
5166  // These arrays may be set via SetNormals(), SetTCoords(), SetScalars()
5167  // without being added to the named array list
5168  if (!dataArray) {
5169  // Check for Normals array
5170  vtkDataArray* normals = attrData->GetNormals();
5171  if (normals) {
5172  QString normalsName =
5173  normals->GetName() && strlen(normals->GetName())
5174  ? QString::fromUtf8(normals->GetName())
5175  : tr("Normals");
5176  if (arrayName == normalsName || arrayName == tr("Normals")) {
5177  dataArray = normals;
5178  }
5179  }
5180 
5181  // Check for TCoords array
5182  if (!dataArray) {
5183  vtkDataArray* tcoords = attrData->GetTCoords();
5184  if (tcoords) {
5185  QString tcoordsName =
5186  tcoords->GetName() && strlen(tcoords->GetName())
5187  ? QString::fromUtf8(tcoords->GetName())
5188  : tr("TCoords");
5189  if (arrayName == tcoordsName ||
5190  arrayName == tr("TCoords")) {
5191  dataArray = tcoords;
5192  }
5193  }
5194  }
5195 
5196  // Check for Scalars/RGB array
5197  if (!dataArray) {
5198  vtkDataArray* scalars = attrData->GetScalars();
5199  if (scalars) {
5200  QString scalarsName =
5201  scalars->GetName() && strlen(scalars->GetName())
5202  ? QString::fromUtf8(scalars->GetName())
5203  : tr("RGB");
5204  // Normalize "Colors" to "RGB" for matching
5205  // if (scalarsName.compare("Colors", Qt::CaseInsensitive) ==
5206  // 0) {
5207  // scalarsName = tr("RGB");
5208  // }
5209  if (arrayName == scalarsName || arrayName == tr("RGB") ||
5210  arrayName == tr("RGBA") || arrayName == tr("Colors")) {
5211  dataArray = scalars;
5212  }
5213  }
5214  }
5215  }
5216 
5217  if (!dataArray) {
5218  QMessageBox::warning(
5219  this, tr("Find Data"),
5220  tr("Attribute '%1' not found.").arg(arrayName));
5221  return;
5222  }
5223  }
5224 
5225  // Parse value
5226  double queryValue = 0.0;
5227  if (!value.isEmpty()) {
5228  bool ok;
5229  queryValue = value.toDouble(&ok);
5230  if (!ok) {
5231  QMessageBox::warning(this, tr("Find Data"),
5232  tr("Invalid numeric value: %1").arg(value));
5233  return;
5234  }
5235  }
5236 
5237  // Lambda to get value from element
5238  auto getValue = [&](vtkIdType i) -> double {
5239  if (isIdQuery) {
5240  return static_cast<double>(i);
5241  } else if (isPointQuery && points) {
5242  // "Point" query - return magnitude of position vector
5243  double pt[3];
5244  points->GetPoint(i, pt);
5245  return std::sqrt(pt[0] * pt[0] + pt[1] * pt[1] + pt[2] * pt[2]);
5246  } else if (isCellQuery) {
5247  // "Cell" query - return cell ID (for containment checks, not yet
5248  // implemented)
5249  return static_cast<double>(i);
5250  } else if (dataArray) {
5251  int numComponents = dataArray->GetNumberOfComponents();
5252  if (isMagnitude && numComponents > 1) {
5253  double* tuple = dataArray->GetTuple(i);
5254  double sum = 0.0;
5255  for (int c = 0; c < numComponents; ++c) {
5256  sum += tuple[c] * tuple[c];
5257  }
5258  return std::sqrt(sum);
5259  } else if (componentIndex >= 0 && componentIndex < numComponents) {
5260  return dataArray->GetComponent(i, componentIndex);
5261  } else {
5262  return dataArray->GetTuple1(i);
5263  }
5264  }
5265  return 0.0;
5266  };
5267 
5268  // Calculate statistics if needed
5269  double minVal = 0.0, maxVal = 0.0, meanVal = 0.0;
5270  vtkIdType numElements = isCell ? polyData->GetNumberOfCells()
5271  : polyData->GetNumberOfPoints();
5272 
5273  if ((dataArray || isPointQuery || isCellQuery) &&
5274  (op == tr("is min") || op == tr("is max") || op == tr("is <= mean") ||
5275  op == tr("is >= mean"))) {
5276  double sum = 0.0;
5277  minVal = std::numeric_limits<double>::max();
5278  maxVal = std::numeric_limits<double>::lowest();
5279 
5280  for (vtkIdType i = 0; i < numElements; ++i) {
5281  double val = getValue(i);
5282  sum += val;
5283  minVal = std::min(minVal, val);
5284  maxVal = std::max(maxVal, val);
5285  }
5286  meanVal = sum / numElements;
5287  }
5288 
5289  // Find matching elements
5290  QVector<qint64> matchingIds;
5291 
5292  for (vtkIdType i = 0; i < numElements; ++i) {
5293  double val = getValue(i);
5294  bool matchResult = false;
5295 
5296  if (op == tr("is") || op == tr("==")) {
5297  matchResult = (std::abs(val - queryValue) < 1e-9);
5298  } else if (op == tr(">=") || op == "is >=") {
5299  matchResult = (val >= queryValue);
5300  } else if (op == tr("<=") || op == "is <=") {
5301  matchResult = (val <= queryValue);
5302  } else if (op == tr(">")) {
5303  matchResult = (val > queryValue);
5304  } else if (op == tr("<")) {
5305  matchResult = (val < queryValue);
5306  } else if (op == tr("!=")) {
5307  matchResult = (std::abs(val - queryValue) >= 1e-9);
5308  } else if (op == tr("is min")) {
5309  matchResult = (std::abs(val - minVal) < 1e-9);
5310  } else if (op == tr("is max")) {
5311  matchResult = (std::abs(val - maxVal) < 1e-9);
5312  } else if (op == tr("is <= mean")) {
5313  matchResult = (val <= meanVal);
5314  } else if (op == tr("is >= mean")) {
5315  matchResult = (val >= meanVal);
5316  }
5317 
5318  if (matchResult) {
5319  matchingIds.append(static_cast<qint64>(i));
5320  }
5321  }
5322 
5323  CVLog::Print(QString("[cvSelectionPropertiesWidget] Find Data: Found %1 "
5324  "matching elements")
5325  .arg(matchingIds.size()));
5326 
5327  if (matchingIds.isEmpty()) {
5328  QMessageBox::information(this, tr("Find Data"),
5329  tr("No elements match the query criteria."));
5330  return;
5331  }
5332 
5333  // Create selection from matching IDs
5336  cvSelectionData newSelection(matchingIds, assoc);
5337 
5338  // Update the selection
5339  m_selectionData = newSelection;
5340  updateSelection(m_selectionData, polyData);
5341 
5342  // Highlight the selection
5343  if (m_highlighter) {
5344  m_highlighter->highlightSelection(m_selectionData,
5346  }
5347 
5348  // Update viewer
5349  PclUtils::PCLVis* pclVis = getPCLVis();
5350  if (pclVis) {
5351  pclVis->UpdateScreen();
5352  }
5353 
5354  // Update the Selected Data spreadsheet table with the query results
5355  updateSpreadsheetData(polyData);
5356 
5357  QMessageBox::information(this, tr("Find Data"),
5358  tr("Selected %1 %2(s) matching '%3 %4 %5'")
5359  .arg(matchingIds.size())
5360  .arg(isCell ? tr("cell") : tr("point"))
5361  .arg(attribute)
5362  .arg(op)
5363  .arg(value.isEmpty() ? QString() : value));
5364 }
5365 
5366 //-----------------------------------------------------------------------------
5367 void cvSelectionPropertiesWidget::onSpreadsheetItemClicked(
5368  QTableWidgetItem* item) {
5369  if (!item) return;
5370 
5371  int row = item->row();
5372 
5373  // Get the ID from the first column
5374  QTableWidgetItem* idItem = m_spreadsheetTable->item(row, 0);
5375  if (idItem) {
5376  qint64 id = idItem->data(Qt::UserRole).toLongLong();
5377  highlightSingleItem(id);
5378  }
5379 }
5380 
5381 // ============================================================================
5382 // Helper functions
5383 // ============================================================================
5384 
5385 //-----------------------------------------------------------------------------
5386 void cvSelectionPropertiesWidget::updateSelectionEditorTable() {
5387  m_selectionEditorTable->setRowCount(m_savedSelections.size());
5388 
5389  for (int i = 0; i < m_savedSelections.size(); ++i) {
5390  const SavedSelection& sel = m_savedSelections[i];
5391 
5392  // Name column
5393  QTableWidgetItem* nameItem = new QTableWidgetItem(sel.name);
5394  m_selectionEditorTable->setItem(i, 0, nameItem);
5395 
5396  // Type column
5397  QTableWidgetItem* typeItem = new QTableWidgetItem(sel.type);
5398  m_selectionEditorTable->setItem(i, 1, typeItem);
5399 
5400  // Color column (use background color)
5401  QTableWidgetItem* colorItem = new QTableWidgetItem(sel.color.name());
5402  colorItem->setBackground(sel.color);
5403  colorItem->setForeground(sel.color.lightness() > 128 ? Qt::black
5404  : Qt::white);
5405  m_selectionEditorTable->setItem(i, 2, colorItem);
5406  }
5407 
5408  m_selectionEditorTable->resizeColumnsToContents();
5409 }
5410 
5411 //-----------------------------------------------------------------------------
5412 QString cvSelectionPropertiesWidget::generateSelectionName() {
5413  return QString("s%1").arg(m_selectionNameCounter++);
5414 }
5415 
5416 //-----------------------------------------------------------------------------
5417 QColor cvSelectionPropertiesWidget::generateSelectionColor() const {
5418  int index = m_savedSelections.size() % s_selectionColorsCount;
5419  return s_selectionColors[index];
5420 }
5421 
5422 //-----------------------------------------------------------------------------
5424  m_dataProducerName = name;
5425  if (m_dataProducerValue) {
5426  m_dataProducerValue->setText(name.isEmpty() ? tr("(none)") : name);
5427  }
5428 
5429  // Update the selected data header
5430  if (m_selectedDataLabel) {
5431  m_selectedDataLabel->setText(
5432  QString("<b>Selected Data (%1)</b>")
5433  .arg(name.isEmpty() ? tr("none") : name));
5434  }
5435 }
5436 
5437 //-----------------------------------------------------------------------------
5439  updateDataProducerCombo();
5440 }
5441 
5442 //-----------------------------------------------------------------------------
5443 void cvSelectionPropertiesWidget::updateSpreadsheetData(
5444  vtkPolyData* polyData, const cvSelectionData* customSelection) {
5445  if (!polyData || !m_spreadsheetTable) {
5446  return;
5447  }
5448 
5449  bool isPointData = m_attributeTypeCombo
5450  ? (m_attributeTypeCombo->currentIndex() == 0)
5451  : true;
5452 
5453  // Clear existing data
5454  m_spreadsheetTable->clear();
5455  m_spreadsheetTable->setRowCount(0);
5456 
5457  // Use custom selection if provided, otherwise use m_selectionData
5458  const cvSelectionData& selection =
5459  customSelection ? *customSelection : m_selectionData;
5460 
5461  // Get selection IDs
5462  if (selection.isEmpty()) {
5463  return;
5464  }
5465 
5466  const QVector<qint64>& ids = selection.ids();
5467  if (ids.isEmpty()) {
5468  return;
5469  }
5470 
5471  // Helper lambda to generate column headers for multi-component arrays
5472  // ParaView-style: Each component gets the SAME base name for header merging
5473  // cvMultiColumnHeaderView will merge adjacent columns with same name
5474  auto getArrayHeaders = [](vtkDataArray* arr) -> QStringList {
5475  QStringList headers;
5476  if (!arr || !arr->GetName()) return headers;
5477 
5478  QString baseName = QString::fromStdString(arr->GetName());
5479  int numComponents = arr->GetNumberOfComponents();
5480 
5481  // Skip VTK internal arrays
5482  if (baseName.startsWith("vtk", Qt::CaseInsensitive)) {
5483  return headers;
5484  }
5485 
5486  if (numComponents == 1) {
5487  // Single component: just the array name
5488  headers << baseName;
5489  } else {
5490  // Multi-component arrays: ALL components use the SAME base name
5491  // This allows cvMultiColumnHeaderView to merge them visually
5492  // ParaView approach: adjacent columns with same DisplayRole text
5493  // are merged
5494  for (int c = 0; c < numComponents; ++c) {
5495  headers << baseName; // Same name for all components
5496  }
5497  // Add magnitude column with _Magnitude suffix (won't be merged)
5498  headers << QString("%1_Magnitude").arg(baseName);
5499  }
5500  return headers;
5501  };
5502 
5503  // Build column headers
5504  QStringList headers;
5505  headers << (isPointData ? tr("Point ID") : tr("Cell ID"));
5506 
5507  if (isPointData) {
5508  // Add point data arrays with ParaView-style multi-component handling
5509  vtkPointData* pointData = polyData->GetPointData();
5510  if (pointData) {
5511  for (int i = 0; i < pointData->GetNumberOfArrays(); ++i) {
5512  vtkDataArray* arr = pointData->GetArray(i);
5513  headers << getArrayHeaders(arr);
5514  }
5515  }
5516  // Add Points columns at the end (ParaView-style: separate X, Y, Z
5517  // columns + magnitude) All three components use "Points" for header
5518  // merging
5519  headers << tr("Points") << tr("Points") << tr("Points")
5520  << tr("Points_Magnitude");
5521  } else {
5522  headers << tr("Type") << tr("Num Points");
5523 
5524  // Add cell data arrays with ParaView-style multi-component handling
5525  vtkCellData* cellData = polyData->GetCellData();
5526  if (cellData) {
5527  for (int i = 0; i < cellData->GetNumberOfArrays(); ++i) {
5528  vtkDataArray* arr = cellData->GetArray(i);
5529  headers << getArrayHeaders(arr);
5530  }
5531  }
5532  }
5533 
5534  m_spreadsheetTable->setColumnCount(headers.size());
5535  m_spreadsheetTable->setHorizontalHeaderLabels(headers);
5536 
5537  // Resize columns to fit content
5538  m_spreadsheetTable->horizontalHeader()->setSectionResizeMode(
5539  QHeaderView::ResizeToContents);
5540 
5541  // Populate rows
5542  int rowCount =
5543  std::min(1000, static_cast<int>(ids.size())); // Limit to 1000 rows
5544  m_spreadsheetTable->setRowCount(rowCount);
5545 
5546  for (int row = 0; row < rowCount; ++row) {
5547  qint64 id = ids[row];
5548  int col = 0;
5549 
5550  // ID column
5551  QTableWidgetItem* idItem = new QTableWidgetItem(QString::number(id));
5552  idItem->setData(Qt::UserRole, static_cast<qlonglong>(id));
5553  m_spreadsheetTable->setItem(row, col++, idItem);
5554 
5555  if (isPointData) {
5556  // Point data arrays (ParaView-style: separate columns for
5557  // each component)
5558  if (id >= 0 && id < polyData->GetNumberOfPoints()) {
5559  vtkPointData* pointData = polyData->GetPointData();
5560  if (pointData) {
5561  for (int i = 0; i < pointData->GetNumberOfArrays(); ++i) {
5562  vtkDataArray* arr = pointData->GetArray(i);
5563  if (arr && arr->GetName() &&
5564  id < arr->GetNumberOfTuples()) {
5565  int numComponents = arr->GetNumberOfComponents();
5566  QString baseName =
5567  QString::fromStdString(arr->GetName());
5568 
5569  // Skip VTK internal arrays
5570  if (baseName.startsWith("vtk",
5572  continue;
5573  }
5574 
5575  if (numComponents == 1) {
5576  double value = arr->GetTuple1(id);
5577  m_spreadsheetTable->setItem(
5578  row, col++,
5579  new QTableWidgetItem(QString::number(
5580  value, 'g', 6)));
5581  } else {
5582  // ParaView-style: All multi-component arrays -
5583  // separate column for each component
5584  double* tuple = arr->GetTuple(id);
5585 
5586  // Add each component as a separate column
5587  for (int c = 0; c < numComponents; ++c) {
5588  m_spreadsheetTable->setItem(
5589  row, col++,
5590  new QTableWidgetItem(
5591  QString::number(tuple[c],
5592  'g', 6)));
5593  }
5594 
5595  // Add magnitude column
5596  double magnitude = 0.0;
5597  for (int c = 0; c < numComponents; ++c) {
5598  magnitude += tuple[c] * tuple[c];
5599  }
5600  magnitude = std::sqrt(magnitude);
5601  m_spreadsheetTable->setItem(
5602  row, col++,
5603  new QTableWidgetItem(QString::number(
5604  magnitude, 'g', 6)));
5605  }
5606  } else if (arr && arr->GetName()) {
5607  QString baseName =
5608  QString::fromStdString(arr->GetName());
5609  // Skip VTK internal arrays
5610  if (baseName.startsWith("vtk",
5612  continue;
5613  }
5614  // Fill N/A for all columns of this array
5615  QStringList arrayHeaders = getArrayHeaders(arr);
5616  for (int c = 0; c < arrayHeaders.size(); ++c) {
5617  m_spreadsheetTable->setItem(
5618  row, col++,
5619  new QTableWidgetItem(tr("N/A")));
5620  }
5621  }
5622  }
5623  }
5624 
5625  // Points columns (ParaView-style: separate X, Y, Z columns +
5626  // magnitude)
5627  double pt[3];
5628  polyData->GetPoint(id, pt);
5629  // Separate column for each coordinate component
5630  m_spreadsheetTable->setItem(
5631  row, col++,
5632  new QTableWidgetItem(QString::number(pt[0], 'g', 6)));
5633  m_spreadsheetTable->setItem(
5634  row, col++,
5635  new QTableWidgetItem(QString::number(pt[1], 'g', 6)));
5636  m_spreadsheetTable->setItem(
5637  row, col++,
5638  new QTableWidgetItem(QString::number(pt[2], 'g', 6)));
5639  // Points magnitude
5640  double mag = std::sqrt(pt[0] * pt[0] + pt[1] * pt[1] +
5641  pt[2] * pt[2]);
5642  m_spreadsheetTable->setItem(
5643  row, col++,
5644  new QTableWidgetItem(QString::number(mag, 'g', 6)));
5645  }
5646  } else {
5647  // Cell data
5648  if (id >= 0 && id < polyData->GetNumberOfCells()) {
5649  vtkCell* cell = polyData->GetCell(id);
5650  if (cell) {
5651  // Type
5652  QString typeName;
5653  switch (cell->GetCellType()) {
5654  case VTK_TRIANGLE:
5655  typeName = tr("Triangle");
5656  break;
5657  case VTK_QUAD:
5658  typeName = tr("Quad");
5659  break;
5660  case VTK_POLYGON:
5661  typeName = tr("Polygon");
5662  break;
5663  case VTK_LINE:
5664  typeName = tr("Line");
5665  break;
5666  case VTK_VERTEX:
5667  typeName = tr("Vertex");
5668  break;
5669  default:
5670  typeName = tr("Unknown");
5671  break;
5672  }
5673  m_spreadsheetTable->setItem(row, col++,
5674  new QTableWidgetItem(typeName));
5675 
5676  // Num Points
5677  m_spreadsheetTable->setItem(
5678  row, col++,
5679  new QTableWidgetItem(QString::number(
5680  cell->GetNumberOfPoints())));
5681  }
5682 
5683  // Cell data arrays with multi-component handling
5684  vtkCellData* cellData = polyData->GetCellData();
5685  if (cellData) {
5686  for (int i = 0; i < cellData->GetNumberOfArrays(); ++i) {
5687  vtkDataArray* arr = cellData->GetArray(i);
5688  if (arr && arr->GetName() &&
5689  id < arr->GetNumberOfTuples()) {
5690  int numComponents = arr->GetNumberOfComponents();
5691  if (numComponents == 1) {
5692  double value = arr->GetTuple1(id);
5693  m_spreadsheetTable->setItem(
5694  row, col++,
5695  new QTableWidgetItem(QString::number(
5696  value, 'g', 6)));
5697  } else {
5698  // For multi-component arrays, show as tuple
5699  // format
5700  QStringList values;
5701  for (int c = 0; c < numComponents; ++c) {
5702  values << QString::number(
5703  arr->GetComponent(id, c), 'g', 6);
5704  }
5705  m_spreadsheetTable->setItem(
5706  row, col++,
5707  new QTableWidgetItem(
5708  QString("(%1)").arg(
5709  values.join(", "))));
5710  }
5711  } else if (arr && arr->GetName()) {
5712  // Single N/A column since we use tuple format
5713  m_spreadsheetTable->setItem(
5714  row, col++,
5715  new QTableWidgetItem(tr("N/A")));
5716  }
5717  }
5718  }
5719  }
5720  }
5721  }
5722 
5723  m_spreadsheetTable->resizeColumnsToContents();
5724 
5725  CVLog::PrintVerbose(QString("[cvSelectionPropertiesWidget] Updated "
5726  "spreadsheet with %1 rows")
5727  .arg(rowCount));
5728 }
5729 
5730 //-----------------------------------------------------------------------------
5731 void cvSelectionPropertiesWidget::setupCollapsibleGroupBox(
5732  QGroupBox* groupBox) {
5733  if (!groupBox) return;
5734 
5735  // Connect collapsible behavior - toggle content visibility when groupbox is
5736  // toggled
5737  connect(groupBox, &QGroupBox::toggled, [groupBox](bool checked) {
5738  QLayout* layout = groupBox->layout();
5739  if (!layout) return;
5740 
5741  for (int i = 0; i < layout->count(); ++i) {
5742  QLayoutItem* item = layout->itemAt(i);
5743  if (item->widget()) {
5744  item->widget()->setVisible(checked);
5745  }
5746  if (item->layout()) {
5747  for (int j = 0; j < item->layout()->count(); ++j) {
5748  QLayoutItem* subItem = item->layout()->itemAt(j);
5749  if (subItem->widget()) {
5750  subItem->widget()->setVisible(checked);
5751  }
5752  }
5753  }
5754  }
5755  });
5756 }
MouseEvent event
std::string name
int points
math::float4 color
QSet< T > qSetFromVector(const QVector< T > &vec)
Definition: QtCompat.h:1073
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
void UpdateScreen()
UpdateScreen - Updates/refreshes the render window This method forces a render update after actor cha...
Definition: PCLVis.cpp:3156
void setPen(const QPen &pen)
void setName(const QString &name)
void setLabel(const QString &str)
A plottable representing a graph in a plot.
Definition: qcustomplot.h:5996
void setScatterStyle(const QCPScatterStyle &style)
void setData(QSharedPointer< QCPGraphDataContainer > data)
void setVisible(bool on)
void setFont(const QFont &font)
Represents the visual appearance of scatter points.
Definition: qcustomplot.h:2695
@ ssCircle
\enumimage{ssCircle.png} a circle
Definition: qcustomplot.h:2744
The central class of the library. This is the QWidget which displays the plot and interacts with the ...
Definition: qcustomplot.h:4167
QCPLegend * legend
Definition: qcustomplot.h:4394
void setInteractions(const QCP::Interactions &interactions)
int plottableCount() const
QCPGraph * addGraph(QCPAxis *keyAxis=0, QCPAxis *valueAxis=0)
QCPAxis * xAxis
Definition: qcustomplot.h:4393
Q_SLOT void replot(QCustomPlot::RefreshPriority refreshPriority=QCustomPlot::rpRefreshHint)
Q_SLOT void rescaleAxes(bool onlyVisiblePlottables=false)
bool removePlottable(QCPAbstractPlottable *plottable)
QCPAxis * yAxis
Definition: qcustomplot.h:4393
virtual void setVisible(bool state)
Sets entity visibility.
Hierarchical CLOUDVIEWER Object.
Definition: ecvHObject.h:25
Triangular mesh.
Definition: ecvMesh.h:35
virtual unsigned size() const override
Returns the number of triangles.
Definition: ecvMesh.cpp:2143
virtual QString getName() const
Returns object name.
Definition: ecvObject.h:72
virtual void setName(const QString &name)
Sets object name.
Definition: ecvObject.h:75
virtual void setEnabled(bool state)
Sets the "enabled" property.
Definition: ecvObject.h:102
A 3D cloud and its associated features (color, normals, scalar fields, etc.)
unsigned size() const override
Definition: PointCloudTpl.h:38
cvExpanderButton provides a frame with a toggle mode for collapsible sections.
void setText(const QString &text)
This property holds the text shown on the button.
void setChecked(bool checked)
This property holds whether the button is checked. By default, the button is unchecked (collapsed).
void toggled(bool checked)
This signal is emitted whenever the button changes its state.
QHeaderView that supports showing multiple sections as one.
Selection algebra operations.
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 unionOf(const cvSelectionData &a, const cvSelectionData &b)
Compute union of two selections.
Operation
Algebra operations Using enum class to avoid macro conflicts (e.g., DIFFERENCE may be defined as a ma...
static cvSelectionData symmetricDifferenceOf(const cvSelectionData &a, const cvSelectionData &b)
Compute symmetric difference of two selections.
static cvSelectionData intersectionOf(const cvSelectionData &a, const cvSelectionData &b)
Compute intersection of two selections.
Selection annotation manager.
void applyLabelProperties(const cvSelectionLabelPropertiesDialog::LabelProperties &props, bool isCellLabel)
Apply label properties to all annotations (for cell or point labels)
void setDefaultLabelProperties(const cvSelectionLabelPropertiesDialog::LabelProperties &props, bool isCellLabel)
Set default label properties for new annotations.
QString addAnnotation(const cvSelectionData &selection, const QString &text, bool autoPosition=true)
Add annotation for a selection.
Lightweight base class for all selection-related components.
PclUtils::PCLVis * getPCLVis() const
Get PCLVis instance (for VTK-specific operations)
virtual vtkPolyData * getPolyDataForSelection(const cvSelectionData *selectionData=nullptr)
Get polyData using ParaView-style priority (centralized method)
Encapsulates selection data without exposing VTK types.
FieldAssociation fieldAssociation() const
Get field association.
QString fieldTypeString() const
Get human-readable field type string.
FieldAssociation
Field association for selection.
@ 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.
void clear()
Clear the selection.
vtkPolyData * primaryPolyData() const
Get the primary (front-most) polyData.
vtkActor * primaryActor() const
Get the primary (front-most) actor.
static ccPointCloud * exportToPointCloud(vtkPolyData *polyData, const cvSelectionData &selectionData, const ExportOptions &options=ExportOptions())
Export selected points to ccPointCloud.
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 ccPointCloud * exportFromSourceCloud(ccPointCloud *sourceCloud, const cvSelectionData &selectionData, const ExportOptions &options=ExportOptions())
Export selected points directly from source ccPointCloud.
Helper class for highlighting selected elements in the visualizer.
void setHighlightQColor(const QColor &color, HighlightMode mode=SELECTED)
Set highlight color from QColor.
QString getCellLabelArrayName() const
Get current cell label array name.
bool highlightSelection(const vtkSmartPointer< vtkIdTypeArray > &selection, int fieldAssociation, HighlightMode mode=SELECTED)
Highlight selected elements (automatically gets polyData from visualizer)
const SelectionLabelProperties & getLabelProperties(bool interactive=false) const
Get label properties for selection mode.
void colorChanged(int mode)
Emitted when any highlight color changes.
void opacityChanged(int mode)
Emitted when any opacity changes.
void labelPropertiesChanged(bool interactive)
Emitted when label properties change.
HighlightMode
Highlight mode (Enhanced multi-level highlighting)
void setHighlightColor(double r, double g, double b, HighlightMode mode=SELECTED)
Set highlight color.
const double * getHighlightColor(HighlightMode mode) const
Get highlight color for a specific mode.
void setCellLabelArray(const QString &arrayName, bool visible=true)
Set the cell label array name.
void setLabelProperties(const SelectionLabelProperties &props, bool interactive=false)
Set label properties for selection mode.
QString getPointLabelArrayName() const
Get current point label array name.
bool isCellLabelVisible() const
Check if cell labels are visible.
double getHighlightOpacity(HighlightMode mode) const
Get highlight opacity for a specific mode.
QColor getHighlightQColor(HighlightMode mode) const
Get highlight color as QColor.
bool isPointLabelVisible() const
Check if point labels are visible.
void setPointLabelArray(const QString &arrayName, bool visible=true)
Set the point label array name.
void setHighlightOpacity(double opacity, HighlightMode mode=SELECTED)
Set highlight opacity.
Dialog for editing selection label properties.
void propertiesApplied(const LabelProperties &props)
Emitted when Apply is clicked.
Comprehensive selection properties and management widget.
void selectionAdded(const cvSelectionData &selection)
bool eventFilter(QObject *obj, QEvent *event) override
void extractedObjectReady(ccHObject *obj)
void resizeEvent(QResizeEvent *event) override
void algebraOperationRequested(int operation)
void setHighlighter(cvSelectionHighlighter *highlighter)
void findDataRequested(const QString &dataProducer, const QString &elementType, const QString &attribute, const QString &op, const QString &value)
void refreshDataProducers()
Refresh the data producer combo box Call this when data sources change (e.g., after loading new data)
void setDataProducerName(const QString &name)
Set the data producer name (source of selection)
const cvSelectionData & selectionData() const
bool updateSelection(const cvSelectionData &selectionData, vtkPolyData *polyData=nullptr)
void expressionChanged(const QString &expression)
void setSelectionManager(cvViewSelectionManager *manager)
void highlightColorChanged(double r, double g, double b, int mode)
void highlightOpacityChanged(double opacity, int mode)
void selectionRemoved(int index)
cvSelectionPropertiesWidget(QWidget *parent=nullptr)
void annotationRequested(const QString &text)
Formatter class for generating tooltip text from VTK data.
Central manager for all selection operations in the view.
ccPointCloud * getSourcePointCloud() const
Get the source object as ccPointCloud.
void setCurrentSelection(const cvSelectionData &selectionData, bool resetLayers=true)
Set the current selection data.
bool isSourceObjectValid() const
Check if the source object is still valid.
cvSelectionAlgebra * getAlgebra()
Get the selection algebra utility.
vtkPolyData * getPolyData() const
Get the underlying polyData for selection operations.
ccHObject * getSourceObject() const
Get the source object for the current selection.
static cvViewSelectionManager * instance()
Get the singleton instance.
ccMesh * getSourceMesh() const
Get the source object as ccMesh.
cvSelectionAnnotationManager * getAnnotations()
Get the annotation manager.
cvSelectionHighlighter * getHighlighter()
Get the shared highlighter.
double normals[3]
const double * e
GraphType data
Definition: graph_cut.cc:138
@ iRangeDrag
Definition: qcustomplot.h:321
@ iSelectPlottables
Definition: qcustomplot.h:334
@ iRangeZoom
Definition: qcustomplot.h:325
constexpr QRegularExpression::PatternOption CaseInsensitive
Definition: QtCompat.h:174
Tensor Minimum(const Tensor &input, const Tensor &other)
Computes the element-wise minimum of input and other. The tensors must have same data type and device...
Tensor Maximum(const Tensor &input, const Tensor &other)
Computes the element-wise maximum of input and other. The tensors must have same data type and device...
void Resize(const core::Tensor &src_im, core::Tensor &dst_im, Image::InterpType interp_type)
Definition: IPPImage.cpp:94
std::string toString(T x)
Definition: Common.h:80
constexpr Rgb black(0, 0, 0)
constexpr Rgb white(MAX, MAX, MAX)
ccGenericPointCloud * sourceCloud
Label properties for selection annotations.
Options for exporting selections.