ACloudViewer  3.9.4
A Modern Library for 3D Data Processing
cvPointPickingHelper.cpp
Go to the documentation of this file.
1 // ----------------------------------------------------------------------------
2 // - CloudViewer: www.cloudViewer.org -
3 // ----------------------------------------------------------------------------
4 // Copyright (c) 2018-2024 www.cloudViewer.org
5 // SPDX-License-Identifier: MIT
6 // ----------------------------------------------------------------------------
7 
8 #include "cvPointPickingHelper.h"
9 
10 // CV_CORE_LIB
11 #include <CVLog.h>
12 
13 // CV_DB_LIB
15 
16 // VTK
17 #include <vtkActor.h>
18 #include <vtkCell.h>
19 #include <vtkCellData.h>
20 #include <vtkCellPicker.h>
21 #include <vtkDataArray.h>
22 #include <vtkDataSet.h>
23 #include <vtkHardwareSelector.h>
24 #include <vtkIdTypeArray.h>
25 #include <vtkInformation.h>
26 #include <vtkMapper.h>
27 #include <vtkPointData.h>
28 #include <vtkPointPicker.h>
29 #include <vtkPoints.h>
30 #include <vtkProp.h>
31 #include <vtkProp3D.h>
32 #include <vtkPropPicker.h>
33 #include <vtkRenderWindow.h>
34 #include <vtkRenderWindowInteractor.h>
35 #include <vtkRenderer.h>
36 #include <vtkSelection.h>
37 #include <vtkSelectionNode.h>
38 
39 // QT
40 #include <QApplication>
41 #include <QCursor>
42 #include <QShortcut>
43 #include <QWidget>
44 #include <cmath>
45 
46 //-----------------------------------------------------------------------------
47 cvPointPickingHelper::cvPointPickingHelper(const QKeySequence& keySequence,
48  bool pickOnPoint,
49  QWidget* parent,
50  PickOption pickOpt)
51  : QObject(parent), m_pickOnPoint(pickOnPoint), m_pickOption(pickOpt) {
52  if (!parent) {
54  "[cvPointPickingHelper] Parent widget is null, shortcut may "
55  "not work properly");
56  return;
57  }
58 
59  // Use ParaView-style modal shortcuts to prevent conflicts when multiple
60  // tool instances exist. ecvKeySequences ensures only one shortcut with
61  // the same key sequence is active at a time, automatically disabling
62  // siblings when a new one is enabled.
64  keySequence, /* action */ nullptr, parent);
65 
66  // CRITICAL: Set context widget to ApplicationShortcut for global shortcuts
67  // This must be done AFTER addModalShortcut() because addModalShortcut()
68  // initially creates the shortcut with default context (WindowShortcut).
69  // We need to explicitly set ApplicationShortcut context so the shortcut
70  // works globally, especially for "Ctrl+C" which may conflict with system
71  // shortcuts or other application shortcuts.
72  m_shortcut->setContextWidget(parent, Qt::ApplicationShortcut);
73 
74  // Verify the shortcut was created and context is set correctly
76  QString("[cvPointPickingHelper] Created shortcut: %1, context: %2, "
77  "enabled: %3")
78  .arg(keySequence.toString())
79  .arg(m_shortcut->isEnabled() ? "ApplicationShortcut"
80  : "unknown")
81  .arg(m_shortcut->isEnabled() ? "yes" : "no"));
82 
83  connect(m_shortcut, &ecvModalShortcut::activated, this,
84  &cvPointPickingHelper::pickPoint);
85 
86  // Set focus policy so the widget can receive focus when needed
87  parent->setFocusPolicy(Qt::StrongFocus);
88 }
89 
90 //-----------------------------------------------------------------------------
92  // ecvModalShortcut will automatically unregister itself when deleted
93  // via the unregister signal connected to ecvKeySequences
94  delete m_shortcut;
95 }
96 
97 //-----------------------------------------------------------------------------
99  vtkRenderWindowInteractor* interactor) {
100  m_interactor = interactor;
101 }
102 
103 //-----------------------------------------------------------------------------
104 void cvPointPickingHelper::setRenderer(vtkRenderer* renderer) {
105  m_renderer = renderer;
106 }
107 
108 //-----------------------------------------------------------------------------
110  m_contextWidget = widget;
111  // Update the modal shortcut's context widget if needed
112  // Note: The shortcut's parent remains the VTK render window widget for
113  // proper focus handling, but we can update the context widget for
114  // visibility checks
115  if (m_shortcut) {
116  // Get the current parent widget (VTK render window widget)
117  QWidget* parentWidget = qobject_cast<QWidget*>(m_shortcut->parent());
118  if (parentWidget) {
119  // Keep ApplicationShortcut context for global shortcuts
120  // Use the parent widget (VTK widget) as context, not the tool
121  // dialog This ensures shortcuts work globally while still being
122  // associated with the VTK render window
123  m_shortcut->setContextWidget(parentWidget, Qt::ApplicationShortcut);
124  }
125  }
126 }
127 
128 //-----------------------------------------------------------------------------
129 void cvPointPickingHelper::setEnabled(bool enabled, bool setFocus) {
130  if (m_shortcut) {
131  // Use ecvModalShortcut::setEnabled which automatically disables
132  // siblings via ecvKeySequences::disableSiblings(). This ensures only
133  // one tool instance's shortcut is active at a time (ParaView-style).
134  m_shortcut->setEnabled(enabled, setFocus);
135  }
136 }
137 
138 //-----------------------------------------------------------------------------
140  return m_shortcut ? m_shortcut->isEnabled() : false;
141 }
142 
143 //-----------------------------------------------------------------------------
144 void cvPointPickingHelper::getCellNormal(vtkDataSet* dataset,
145  vtkIdType cellId,
146  vtkCell* cell,
147  double normal[3]) {
148  // Try to get normal from cell data first
149  vtkDataArray* cellNormals = dataset->GetCellData()->GetNormals();
150  if (cellNormals && cellId < cellNormals->GetNumberOfTuples()) {
151  cellNormals->GetTuple(cellId, normal);
152  return;
153  }
154 
155  // Fallback: compute normal from cell geometry
156  int cellType = cell->GetCellType();
157  if (cellType == VTK_TRIANGLE || cellType == VTK_QUAD ||
158  cellType == VTK_POLYGON) {
159  // For polygonal cells, compute normal from first 3 points
160  double p0[3], p1[3], p2[3];
161  cell->GetPoints()->GetPoint(0, p0);
162  cell->GetPoints()->GetPoint(1, p1);
163  cell->GetPoints()->GetPoint(2, p2);
164 
165  // Compute two edge vectors
166  double v1[3] = {p1[0] - p0[0], p1[1] - p0[1], p1[2] - p0[2]};
167  double v2[3] = {p2[0] - p0[0], p2[1] - p0[1], p2[2] - p0[2]};
168 
169  // Cross product
170  normal[0] = v1[1] * v2[2] - v1[2] * v2[1];
171  normal[1] = v1[2] * v2[0] - v1[0] * v2[2];
172  normal[2] = v1[0] * v2[1] - v1[1] * v2[0];
173 
174  // Normalize
175  double len = std::sqrt(normal[0] * normal[0] + normal[1] * normal[1] +
176  normal[2] * normal[2]);
177  if (len > 0) {
178  normal[0] /= len;
179  normal[1] /= len;
180  normal[2] /= len;
181  }
182  }
183 }
184 
185 //-----------------------------------------------------------------------------
186 void cvPointPickingHelper::pickPoint() {
188  QString("[cvPointPickingHelper::pickPoint] Helper=%1, shortcut "
189  "enabled=%2, contextWidget=%3")
190  .arg((quintptr)this, 0, 16)
191  .arg(m_shortcut && m_shortcut->isEnabled() ? "yes" : "no")
192  .arg((quintptr)m_contextWidget.data(), 0, 16));
193 
194  // CRITICAL: Check if shortcut is enabled
195  // When using ApplicationShortcut context, shortcuts from all tool instances
196  // can be triggered. We must check if this specific instance's shortcut is
197  // enabled to ensure only the active (unlocked) tool responds.
198  if (!m_shortcut || !m_shortcut->isEnabled()) {
199  // This shortcut is disabled (tool is locked), don't process picking
201  "[cvPointPickingHelper::pickPoint] Shortcut disabled, "
202  "skipping");
203  return;
204  }
205 
206  // CRITICAL: Check if context widget is valid and visible
207  // This prevents crashes when multiple tool instances exist or tool is being
208  // deleted
209  if (!m_contextWidget) {
210  CVLog::Warning("[cvPointPickingHelper] Context widget is null");
211  return;
212  }
213 
214  if (!m_contextWidget->isVisible()) {
215  // Tool dialog is hidden, don't process picking
217  "[cvPointPickingHelper::pickPoint] Context widget not visible, "
218  "skipping");
219  return;
220  }
221 
222  // CRITICAL: Additional check - ensure the context widget is actually the
223  // active tool instance. When multiple tool instances exist with the same
224  // shortcut (e.g., "Ctrl+C"), Qt may trigger shortcuts from all instances,
225  // but we only want the active (visible) one to respond. This check ensures
226  // that only the tool instance whose dialog is currently visible will
227  // process the pick.
228  // Note: This is especially important for "Ctrl+C" which may conflict with
229  // main window shortcuts or other tool instances.
230  // The visibility check above should be sufficient, but we also verify that
231  // the widget is actually active (has focus or is the top-level visible
232  // widget).
233 
234  CVLog::PrintDebug("[cvPointPickingHelper::pickPoint] Processing pick...");
235 
236  if (!m_interactor || !m_renderer) {
237  CVLog::Warning("[cvPointPickingHelper] Interactor or renderer not set");
238  return;
239  }
240 
241  // Get the render window widget
242  QWidget* renderWidget = nullptr;
243  if (m_interactor->GetRenderWindow()) {
244  renderWidget = static_cast<QWidget*>(
245  m_interactor->GetRenderWindow()->GetGenericWindowId());
246  }
247 
248  // Fallback to shortcut's parent widget
249  if (!renderWidget) {
250  renderWidget = qobject_cast<QWidget*>(m_shortcut ? m_shortcut->parent()
251  : nullptr);
252  }
253 
254  if (!renderWidget) {
255  CVLog::Warning("[cvPointPickingHelper] Cannot determine render widget");
256  return;
257  }
258 
259  // ParaView approach: Check if the keypress event actually happened in the
260  // window This matches ParaView's pqPointPickingHelper::pickPoint()
261  // implementation
262  QPointF pos = renderWidget->mapFromGlobal(QCursor::pos());
263  QSize sz = renderWidget->size();
264  bool outside = pos.x() < 0 || pos.x() > sz.width() || pos.y() < 0 ||
265  pos.y() > sz.height();
266  if (outside) {
268  "[cvPointPickingHelper] Cursor is outside the render widget");
269  return;
270  }
271 
272  // ParaView approach: Use GetEventPosition() from the interactor
273  // This matches ParaView's pqPointPickingHelper::pickPoint() implementation
274  // exactly GetEventPosition() returns the position of the last event
275  // (keyboard shortcut activation) which is more reliable than QCursor::pos()
276  // for keyboard-triggered picking, especially on macOS
277  int displayX = 0;
278  int displayY = 0;
279  const int* eventpos = m_interactor->GetEventPosition();
280  if (eventpos && (eventpos[0] >= 0 && eventpos[1] >= 0)) {
281  // Use event position from interactor (ParaView style)
282  // GetEventPosition() returns coordinates in VTK display space (origin
283  // at bottom-left)
284  displayX = eventpos[0];
285  displayY = eventpos[1];
286 
287  // Validate coordinates against render window size
288  int* renderSize = m_interactor->GetRenderWindow()->GetSize();
289  if (displayX < 0 || displayX >= renderSize[0] || displayY < 0 ||
290  displayY >= renderSize[1]) {
291  // Event position is out of bounds, fallback to cursor position
293  QString("[cvPointPickingHelper] GetEventPosition() out of "
294  "bounds (%1,%2), "
295  "falling back to QCursor::pos()")
296  .arg(displayX)
297  .arg(displayY));
298  displayX = static_cast<int>(pos.x());
299  displayY = sz.height() - static_cast<int>(pos.y()) - 1;
300  }
301  } else {
302  // Fallback to cursor position if GetEventPosition() is invalid
303  // Convert to VTK display coordinates (origin at bottom-left)
304  displayX = static_cast<int>(pos.x());
305  displayY = sz.height() - static_cast<int>(pos.y()) - 1;
306  }
307 
308  double position[3] = {0.0, 0.0, 0.0};
309  double normal[3] = {0.0, 0.0, 1.0}; // Default normal
310  bool pickSuccess = false;
311 
312  // ParaView-style hybrid picking strategy:
313  // 1. Point mode: Use HardwareSelector (GPU-accelerated, fastest for points)
314  // 2. Cell mode: Use PropPicker + CellPicker (accurate surface intersection)
315  // 3. Cache results to avoid redundant picks
316 
317  // Check cache first
318  if (m_selectionCache.valid && m_selectionCache.displayX == displayX &&
319  m_selectionCache.displayY == displayY &&
320  m_selectionCache.pickOnPoint == m_pickOnPoint) {
321  // Use cached result
322  std::copy(m_selectionCache.position, m_selectionCache.position + 3,
323  position);
324  std::copy(m_selectionCache.normal, m_selectionCache.normal + 3, normal);
325  pickSuccess = true;
326  } else {
327  // Clear cache
328  m_selectionCache.valid = false;
329 
330  if (m_pickOnPoint) {
331  // ===== Point Picking Mode =====
332  // Hybrid strategy: HardwareSelector for point clouds (GPU fast),
333  // PropPicker + PointPicker for meshes (accurate)
334 
335  // Try HardwareSelector first (fastest for point clouds)
336  if (!m_hardwareSelector) {
337  m_hardwareSelector =
339  }
340 
341  m_hardwareSelector->SetRenderer(m_renderer);
342  m_hardwareSelector->SetArea(displayX, displayY, displayX, displayY);
343  m_hardwareSelector->SetFieldAssociation(
344  vtkDataObject::FIELD_ASSOCIATION_POINTS);
345 
346  vtkSelection* selection = m_hardwareSelector->Select();
347 
348  if (selection && selection->GetNumberOfNodes() > 0) {
349  vtkSelectionNode* node = selection->GetNode(0);
350  if (node) {
351  vtkProp* prop =
352  vtkProp::SafeDownCast(node->GetProperties()->Get(
353  vtkSelectionNode::PROP()));
354  vtkActor* actor = vtkActor::SafeDownCast(prop);
355 
356  if (actor && actor->GetMapper()) {
357  vtkIdTypeArray* selectionIds =
358  vtkIdTypeArray::SafeDownCast(
359  node->GetSelectionList());
360 
361  if (selectionIds &&
362  selectionIds->GetNumberOfTuples() > 0) {
363  vtkIdType pointId = selectionIds->GetValue(0);
364  vtkDataSet* dataset =
365  actor->GetMapper()->GetInput();
366 
367  if (dataset && pointId >= 0 &&
368  pointId < dataset->GetNumberOfPoints()) {
369  dataset->GetPoint(pointId, position);
370  pickSuccess = true;
371 
372  if (m_pickOption != Coordinates) {
373  vtkDataArray* normals =
374  dataset->GetPointData()
375  ->GetNormals();
376  if (normals &&
377  pointId <
378  normals->GetNumberOfTuples()) {
379  normals->GetTuple(pointId, normal);
380  }
381  }
382  }
383  }
384  }
385  }
386  }
387 
388  if (selection) {
389  selection->Delete();
390  }
391 
392  // Fallback: If HardwareSelector failed (e.g., mesh with no visible
393  // vertex at cursor), use PointPicker with PickList for accurate
394  // mesh point picking
395  if (!pickSuccess) {
396  if (!m_propPicker) {
397  m_propPicker = vtkSmartPointer<vtkPropPicker>::New();
398  }
399 
400  if (m_propPicker->Pick(displayX, displayY, 0, m_renderer)) {
401  vtkProp3D* prop = m_propPicker->GetProp3D();
402  vtkActor* actor = vtkActor::SafeDownCast(prop);
403 
404  if (actor && actor->GetMapper()) {
405  if (!m_pointPicker) {
406  m_pointPicker =
408  m_pointPicker->SetTolerance(0.01);
409  }
410 
411  // Limit to this actor only (huge performance boost)
412  m_pointPicker->AddPickList(actor);
413  m_pointPicker->PickFromListOn();
414 
415  if (m_pointPicker->Pick(displayX, displayY, 0,
416  m_renderer)) {
417  vtkIdType pointId = m_pointPicker->GetPointId();
418  vtkDataSet* dataset =
419  actor->GetMapper()->GetInput();
420 
421  if (dataset && pointId >= 0 &&
422  pointId < dataset->GetNumberOfPoints()) {
423  dataset->GetPoint(pointId, position);
424  pickSuccess = true;
425 
426  if (m_pickOption != Coordinates) {
427  vtkDataArray* normals =
428  dataset->GetPointData()
429  ->GetNormals();
430  if (normals &&
431  pointId <
432  normals->GetNumberOfTuples()) {
433  normals->GetTuple(pointId, normal);
434  }
435  }
436  }
437  }
438 
439  // Clean up
440  m_pointPicker->InitializePickList();
441  m_pointPicker->PickFromListOff();
442  }
443  }
444  }
445 
446  } else {
447  // ===== Surface/Cell Picking Mode =====
448  // ParaView strategy: Use CellPicker with PropPicker pre-filter for
449  // accuracy
450 
451  // Step 1: Fast PropPicker to identify actor
452  if (!m_propPicker) {
453  m_propPicker = vtkSmartPointer<vtkPropPicker>::New();
454  }
455 
456  if (m_propPicker->Pick(displayX, displayY, 0, m_renderer)) {
457  vtkProp3D* prop = m_propPicker->GetProp3D();
458  vtkActor* actor = vtkActor::SafeDownCast(prop);
459 
460  if (actor && actor->GetMapper()) {
461  // Step 2: Precise CellPicker with PickList (ParaView
462  // approach)
463  if (!m_cellPicker) {
464  m_cellPicker = vtkSmartPointer<vtkCellPicker>::New();
465  m_cellPicker->SetTolerance(0.005);
466  }
467 
468  // Limit to this actor only (huge performance boost)
469  m_cellPicker->AddPickList(actor);
470  m_cellPicker->PickFromListOn();
471 
472  if (m_cellPicker->Pick(displayX, displayY, 0, m_renderer)) {
473  m_cellPicker->GetPickPosition(position);
474  pickSuccess = true;
475 
476  // Get accurate normal from CellPicker
477  if (m_pickOption != Coordinates) {
478  double* pickedNormal =
479  m_cellPicker->GetPickNormal();
480  if (pickedNormal) {
481  normal[0] = pickedNormal[0];
482  normal[1] = pickedNormal[1];
483  normal[2] = pickedNormal[2];
484  }
485  }
486  }
487 
488  // Clean up
489  m_cellPicker->InitializePickList();
490  m_cellPicker->PickFromListOff();
491  }
492  }
493  }
494 
495  // Update cache
496  if (pickSuccess) {
497  m_selectionCache.displayX = displayX;
498  m_selectionCache.displayY = displayY;
499  m_selectionCache.pickOnPoint = m_pickOnPoint;
500  std::copy(position, position + 3, m_selectionCache.position);
501  std::copy(normal, normal + 3, m_selectionCache.normal);
502  m_selectionCache.valid = true;
503  }
504  }
505 
506  if (!pickSuccess) {
507  // Pick failed - cursor might not be over any geometry
508  return;
509  }
510 
511  // Validate picked position
512  auto isValidVector = [](const double x[3]) {
513  return !std::isnan(x[0]) && !std::isnan(x[1]) && !std::isnan(x[2]) &&
514  !std::isinf(x[0]) && !std::isinf(x[1]) && !std::isinf(x[2]);
515  };
516 
517  switch (m_pickOption) {
518  case Coordinates:
519  if (isValidVector(position)) {
520  Q_EMIT pick(position[0], position[1], position[2]);
521  } else {
523  "[cvPointPickingHelper] Invalid position picked");
524  }
525  break;
526 
527  case Normal:
528  if (isValidVector(normal)) {
529  Q_EMIT pick(normal[0], normal[1], normal[2]);
530  } else {
532  "[cvPointPickingHelper] Normal was not available");
533  }
534  break;
535 
537  if (isValidVector(position) && isValidVector(normal)) {
539  QString("[cvPointPickingHelper] Picked point: "
540  "(%1, %2, %3), normal: (%4, %5, %6)")
541  .arg(position[0])
542  .arg(position[1])
543  .arg(position[2])
544  .arg(normal[0])
545  .arg(normal[1])
546  .arg(normal[2]));
547  Q_EMIT pickNormal(position[0], position[1], position[2],
548  normal[0], normal[1], normal[2]);
549  } else {
551  "[cvPointPickingHelper] Position or normal was not "
552  "available");
553  }
554  break;
555  }
556 }
557 
558 //-----------------------------------------------------------------------------
560  m_selectionCache.valid = false;
561  m_selectionCache.displayX = -1;
562  m_selectionCache.displayY = -1;
563  m_selectionCache.id = -1;
564 }
double normal[3]
math::float3 position
bool copy
Definition: VtkUtils.cpp:74
static bool PrintDebug(const char *format,...)
Same as Print, but works only in Debug mode.
Definition: CVLog.cpp:153
static bool Warning(const char *format,...)
Prints out a formatted warning message in console.
Definition: CVLog.cpp:133
static bool PrintVerbose(const char *format,...)
Prints out a verbose formatted message in console.
Definition: CVLog.cpp:103
bool isEnabled() const
Check if shortcut is enabled.
@ Coordinates
Pick point coordinates only.
@ Normal
Pick normal only.
@ CoordinatesAndNormal
Pick both coordinates and normal.
void setEnabled(bool enabled, bool setFocus=false)
Enable or disable the shortcut.
void setContextWidget(QWidget *widget)
Set the context widget for the shortcut.
cvPointPickingHelper(const QKeySequence &keySequence, bool pickOnPoint, QWidget *parent=nullptr, PickOption pickOpt=Coordinates)
Constructor.
void setInteractor(vtkRenderWindowInteractor *interactor)
Set the VTK interactor for picking.
void setRenderer(vtkRenderer *renderer)
Set the VTK renderer for picking.
void clearSelectionCache()
Clear the selection cache (ParaView-style optimization) Call this when the scene changes to invalidat...
void pickNormal(double px, double py, double pz, double nx, double ny, double nz)
Emitted when a point and normal are picked.
void pick(double x, double y, double z)
Emitted when a point is picked.
ecvModalShortcut * addModalShortcut(const QKeySequence &keySequence, QAction *action, QWidget *parent)
static ecvKeySequences & instance()
double normals[3]
normal_z x