1// Copyright (C) 2015 Paul Lemire paul.lemire350@gmail.com
2// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only
3
4#include "pickboundingvolumejob_p.h"
5#include "qpicktriangleevent.h"
6#include "qpicklineevent.h"
7#include "qpickpointevent.h"
8#include <Qt3DCore/private/qaspectmanager_p.h>
9#include <Qt3DCore/private/vector_helper_p.h>
10#include <Qt3DRender/qobjectpicker.h>
11#include <Qt3DRender/qviewport.h>
12#include <Qt3DRender/qgeometryrenderer.h>
13#include <Qt3DRender/private/qobjectpicker_p.h>
14#include <Qt3DRender/private/nodemanagers_p.h>
15#include <Qt3DRender/private/entity_p.h>
16#include <Qt3DRender/private/objectpicker_p.h>
17#include <Qt3DRender/private/managers_p.h>
18#include <Qt3DRender/private/geometryrenderer_p.h>
19#include <Qt3DRender/private/rendersettings_p.h>
20#include <Qt3DRender/private/trianglesvisitor_p.h>
21#include <Qt3DRender/private/job_common_p.h>
22#include <Qt3DRender/private/qpickevent_p.h>
23#include <Qt3DRender/private/pickboundingvolumeutils_p.h>
24
25#include <QSurface>
26#include <QWindow>
27#include <QOffscreenSurface>
28
29QT_BEGIN_NAMESPACE
30
31namespace Qt3DRender {
32using namespace Qt3DRender::RayCasting;
33
34namespace Render {
35
36class PickBoundingVolumeJobPrivate : public Qt3DCore::QAspectJobPrivate
37{
38public:
39 PickBoundingVolumeJobPrivate(PickBoundingVolumeJob *q) : q_ptr(q) { }
40 ~PickBoundingVolumeJobPrivate() override = default;
41
42 bool isRequired() const override;
43 void postFrame(Qt3DCore::QAspectManager *manager) override;
44
45 enum CustomEventType {
46 MouseButtonClick = QEvent::User,
47 };
48
49 struct EventDetails {
50 Qt3DCore::QNodeId pickerId;
51 int sourceEventType;
52 QPickEventPtr resultingEvent;
53 Qt3DCore::QNodeId viewportNodeId;
54 };
55
56 QList<EventDetails> dispatches;
57 PickBoundingVolumeJob *q_ptr;
58 Q_DECLARE_PUBLIC(PickBoundingVolumeJob)
59};
60
61
62bool PickBoundingVolumeJobPrivate::isRequired() const
63{
64 Q_Q(const PickBoundingVolumeJob);
65 return !q->m_pendingMouseEvents.empty() || q->m_pickersDirty || q->m_oneEnabledAtLeast;
66}
67
68void PickBoundingVolumeJobPrivate::postFrame(Qt3DCore::QAspectManager *manager)
69{
70 using namespace Qt3DCore;
71 QNodeId previousId;
72 QObjectPicker *node = nullptr;
73
74 for (auto res: std::as_const(t&: dispatches)) {
75 if (previousId != res.pickerId) {
76 node = qobject_cast<QObjectPicker *>(object: manager->lookupNode(id: res.pickerId));
77 previousId = res.pickerId;
78 }
79 if (!node)
80 continue;
81
82 QObjectPickerPrivate *dnode = static_cast<QObjectPickerPrivate *>(QObjectPickerPrivate::get(q: node));
83
84 // resolve front end details
85 QPickEvent *pickEvent = res.resultingEvent.data();
86 if (pickEvent) {
87 QPickEventPrivate *dpickEvent = QPickEventPrivate::get(object: pickEvent);
88 dpickEvent->m_viewport = static_cast<QViewport *>(manager->lookupNode(id: res.viewportNodeId));
89 dpickEvent->m_entityPtr = static_cast<QEntity *>(manager->lookupNode(id: dpickEvent->m_entity));
90 }
91
92 // dispatch event
93 switch (res.sourceEventType) {
94 case QEvent::MouseButtonPress:
95 dnode->pressedEvent(event: pickEvent);
96 break;
97 case QEvent::MouseButtonRelease:
98 dnode->releasedEvent(event: pickEvent);
99 break;
100 case MouseButtonClick:
101 dnode->clickedEvent(event: pickEvent);
102 break;
103 case QEvent::MouseMove:
104 dnode->movedEvent(event: pickEvent);
105 break;
106 case QEvent::Enter:
107 emit node->entered();
108 dnode->setContainsMouse(true);
109 break;
110 case QEvent::Leave:
111 dnode->setContainsMouse(false);
112 emit node->exited();
113 break;
114 default: Q_UNREACHABLE();
115 }
116 }
117
118 dispatches.clear();
119}
120
121namespace {
122
123void setEventButtonAndModifiers(const QMouseEvent *event, QPickEvent::Buttons &eventButton, int &eventButtons, int &eventModifiers)
124{
125 switch (event->button()) {
126 case Qt::LeftButton:
127 eventButton = QPickEvent::LeftButton;
128 break;
129 case Qt::RightButton:
130 eventButton = QPickEvent::RightButton;
131 break;
132 case Qt::MiddleButton:
133 eventButton = QPickEvent::MiddleButton;
134 break;
135 case Qt::BackButton:
136 eventButton = QPickEvent::BackButton;
137 break;
138 default:
139 break;
140 }
141
142 if (event->buttons() & Qt::LeftButton)
143 eventButtons |= QPickEvent::LeftButton;
144 if (event->buttons() & Qt::RightButton)
145 eventButtons |= QPickEvent::RightButton;
146 if (event->buttons() & Qt::MiddleButton)
147 eventButtons |= QPickEvent::MiddleButton;
148 if (event->buttons() & Qt::BackButton)
149 eventButtons |= QPickEvent::BackButton;
150 if (event->modifiers() & Qt::ShiftModifier)
151 eventModifiers |= QPickEvent::ShiftModifier;
152 if (event->modifiers() & Qt::ControlModifier)
153 eventModifiers |= QPickEvent::ControlModifier;
154 if (event->modifiers() & Qt::AltModifier)
155 eventModifiers |= QPickEvent::AltModifier;
156 if (event->modifiers() & Qt::MetaModifier)
157 eventModifiers |= QPickEvent::MetaModifier;
158 if (event->modifiers() & Qt::KeypadModifier)
159 eventModifiers |= QPickEvent::KeypadModifier;
160}
161
162} // anonymous
163
164PickBoundingVolumeJob::PickBoundingVolumeJob()
165 : AbstractPickingJob(*new PickBoundingVolumeJobPrivate(this))
166 , m_pickersDirty(true)
167{
168 SET_JOB_RUN_STAT_TYPE(this, JobTypes::PickBoundingVolume, 0)
169}
170
171void PickBoundingVolumeJob::setRoot(Entity *root)
172{
173 m_node = root;
174}
175
176bool PickBoundingVolumeJob::processMouseEvent(QObject* object, QMouseEvent *event)
177{
178 m_pendingMouseEvents.emplace_back(args&: object, args: std::unique_ptr<QMouseEvent>(static_cast<QMouseEvent *>(event->clone())));
179 return false;
180}
181
182void PickBoundingVolumeJob::markPickersDirty()
183{
184 m_pickersDirty = true;
185}
186
187bool PickBoundingVolumeJob::runHelper()
188{
189 // Move to clear the events so that we don't process them several times
190 // if run is called several times
191 const auto mouseEvents = Qt3DCore::moveAndClear(data&: m_pendingMouseEvents);
192
193 // If we have no events return early
194 if (mouseEvents.empty())
195 return false;
196
197 // Quickly look which picker settings we've got
198 if (m_pickersDirty) {
199 m_pickersDirty = false;
200 m_oneEnabledAtLeast = false;
201 m_oneHoverAtLeast = false;
202
203 const auto activeHandles = m_manager->objectPickerManager()->activeHandles();
204 for (const auto &handle : activeHandles) {
205 auto picker = m_manager->objectPickerManager()->data(handle);
206 m_oneEnabledAtLeast |= picker->isEnabled();
207 m_oneHoverAtLeast |= picker->isHoverEnabled();
208 if (m_oneEnabledAtLeast && m_oneHoverAtLeast)
209 break;
210 }
211 }
212
213 // bail out early if no picker is enabled
214 if (!m_oneEnabledAtLeast)
215 return false;
216
217 bool hasMoveEvent = false;
218 bool hasOtherEvent = false;
219 // Quickly look which types of events we've got
220 for (const auto &event : mouseEvents) {
221 const bool isMove = (event.second->type() == QEvent::MouseMove);
222 hasMoveEvent |= isMove;
223 hasOtherEvent |= !isMove;
224 }
225
226 // In the case we have a move event, find if we actually have
227 // an object picker that cares about these
228 if (!hasOtherEvent) {
229 // Retrieve the last used object picker
230 ObjectPicker *lastCurrentPicker = m_manager->objectPickerManager()->data(handle: m_currentPicker);
231
232 // The only way to set lastCurrentPicker is to click
233 // so we can return since if we're there it means we
234 // have only move events. But keep on if hover support
235 // is needed
236 if (lastCurrentPicker == nullptr && !m_oneHoverAtLeast)
237 return false;
238
239 const bool caresAboutMove = (hasMoveEvent &&
240 (m_oneHoverAtLeast ||
241 (lastCurrentPicker && lastCurrentPicker->isDragEnabled())));
242 // Early return if the current object picker doesn't care about move events
243 if (!caresAboutMove)
244 return false;
245 }
246
247 const PickingUtils::PickConfiguration pickConfiguration(m_frameGraphRoot, m_renderSettings);
248 if (pickConfiguration.vcaDetails.empty())
249 return false;
250
251 // TO DO:
252 // If we have move or hover move events that someone cares about, we try to avoid expensive computations
253 // by compressing them into a single one
254
255 // For each mouse event
256 for (const auto &event : mouseEvents)
257 processPickEvent(pickConfiguration, object: event.first, event: event.second.get());
258
259 // Clear Hovered elements that needs to be cleared
260 // Send exit event to object pickers on which we
261 // had set the hovered flag for a previous frame
262 // and that aren't being hovered any longer
263 clearPreviouslyHoveredPickers();
264 return true;
265}
266
267void PickBoundingVolumeJob::processPickEvent(const PickingUtils::PickConfiguration &pickConfiguration, QObject *object, const QMouseEvent *event)
268{
269 m_hoveredPickersToClear = m_hoveredPickers;
270
271 QPickEvent::Buttons eventButton = QPickEvent::NoButton;
272 int eventButtons = 0;
273 int eventModifiers = QPickEvent::NoModifier;
274
275 setEventButtonAndModifiers(event, eventButton, eventButtons, eventModifiers);
276
277 // For each Viewport / Camera and Area entry
278 for (const PickingUtils::ViewportCameraAreaDetails &vca : pickConfiguration.vcaDetails) {
279 PickingUtils::HitList sphereHits;
280 QRay3D ray = rayForViewportAndCamera(vca, eventSource: object, pos: event->pos());
281 if (!ray.isValid()) {
282 // An invalid rays is when we've lost our surface or the mouse
283 // has moved out of the viewport In case of a button released
284 // outside of the viewport, we still want to notify the
285 // lastCurrent entity about this.
286 dispatchPickEvents(event, sphereHits: PickingUtils::HitList(), eventButton, eventButtons, eventModifiers, allHitsRequested: m_renderSettings->pickResultMode(),
287 viewportNodeId: vca.viewportNodeId);
288 continue;
289 }
290
291 PickingUtils::HierarchicalEntityPicker entityPicker(ray);
292 entityPicker.setLayerFilterIds(vca.layersFilters);
293
294 if (entityPicker.collectHits(manager: m_manager, root: m_node)) {
295 if (pickConfiguration.trianglePickingRequested) {
296 PickingUtils::TriangleCollisionGathererFunctor gathererFunctor;
297 gathererFunctor.m_frontFaceRequested = pickConfiguration.frontFaceRequested;
298 gathererFunctor.m_backFaceRequested = pickConfiguration.backFaceRequested;
299 gathererFunctor.m_manager = m_manager;
300 gathererFunctor.m_ray = ray;
301 gathererFunctor.m_entityToPriorityTable = entityPicker.entityToPriorityTable();
302 Qt3DCore::moveAtEnd(destination&: sphereHits, source: gathererFunctor.computeHits(entities: entityPicker.entities(), mode: m_renderSettings->pickResultMode()));
303 }
304 if (pickConfiguration.edgePickingRequested) {
305 PickingUtils::LineCollisionGathererFunctor gathererFunctor;
306 gathererFunctor.m_manager = m_manager;
307 gathererFunctor.m_ray = ray;
308 gathererFunctor.m_pickWorldSpaceTolerance = pickConfiguration.pickWorldSpaceTolerance;
309 gathererFunctor.m_entityToPriorityTable = entityPicker.entityToPriorityTable();
310 Qt3DCore::moveAtEnd(destination&: sphereHits, source: gathererFunctor.computeHits(entities: entityPicker.entities(), mode: m_renderSettings->pickResultMode()));
311 PickingUtils::AbstractCollisionGathererFunctor::sortHits(results&: sphereHits);
312 }
313 if (pickConfiguration.pointPickingRequested) {
314 PickingUtils::PointCollisionGathererFunctor gathererFunctor;
315 gathererFunctor.m_manager = m_manager;
316 gathererFunctor.m_ray = ray;
317 gathererFunctor.m_pickWorldSpaceTolerance = pickConfiguration.pickWorldSpaceTolerance;
318 gathererFunctor.m_entityToPriorityTable = entityPicker.entityToPriorityTable();
319 Qt3DCore::moveAtEnd(destination&: sphereHits, source: gathererFunctor.computeHits(entities: entityPicker.entities(), mode: m_renderSettings->pickResultMode()));
320 PickingUtils::AbstractCollisionGathererFunctor::sortHits(results&: sphereHits);
321 }
322 if (!pickConfiguration.primitivePickingRequested) {
323 Qt3DCore::moveAtEnd(destination&: sphereHits, source: entityPicker.hits());
324 PickingUtils::AbstractCollisionGathererFunctor::sortHits(results&: sphereHits);
325 if (m_renderSettings->pickResultMode() != QPickingSettings::AllPicks)
326 sphereHits = { sphereHits.front() };
327 }
328 }
329
330 // Dispatch events based on hit results
331 dispatchPickEvents(event, sphereHits, eventButton, eventButtons, eventModifiers, allHitsRequested: m_renderSettings->pickResultMode(),
332 viewportNodeId: vca.viewportNodeId);
333 }
334}
335
336void PickBoundingVolumeJob::dispatchPickEvents(const QMouseEvent *event,
337 const PickingUtils::HitList &sphereHits,
338 QPickEvent::Buttons eventButton,
339 int eventButtons,
340 int eventModifiers,
341 bool allHitsRequested,
342 Qt3DCore::QNodeId viewportNodeId)
343{
344 Q_D(PickBoundingVolumeJob);
345
346 ObjectPicker *lastCurrentPicker = m_manager->objectPickerManager()->data(handle: m_currentPicker);
347 // If we have hits
348 if (!sphereHits.empty()) {
349 // Note: how can we control that we want the first/last/all elements along the ray to be picked
350
351 // How do we differentiate betwnee an Entity with no GeometryRenderer and one with one, both having
352 // an ObjectPicker component when it comes
353
354 for (const QCollisionQueryResult::Hit &hit : std::as_const(t: sphereHits)) {
355 Entity *entity = m_manager->renderNodesManager()->lookupResource(id: hit.m_entityId);
356 HObjectPicker objectPickerHandle = entity->componentHandle<ObjectPicker>();
357
358 // If the Entity which actually received the hit doesn't have
359 // an object picker component, we need to check the parent if it has one ...
360 while (objectPickerHandle.isNull() && entity != nullptr) {
361 entity = entity->parent();
362 if (entity != nullptr)
363 objectPickerHandle = entity->componentHandle<ObjectPicker>();
364 }
365
366 ObjectPicker *objectPicker = m_manager->objectPickerManager()->data(handle: objectPickerHandle);
367 if (objectPicker != nullptr && objectPicker->isEnabled()) {
368
369 if (lastCurrentPicker && !allHitsRequested) {
370 // if there is a current picker, it will "grab" all events until released.
371 // Clients should test that entity is what they expect (or only use
372 // world coordinates)
373 objectPicker = lastCurrentPicker;
374 }
375
376 // Send the corresponding event
377 Vector3D localIntersection = hit.m_intersection;
378 if (entity && entity->worldTransform())
379 localIntersection = entity->worldTransform()->inverted().map(point: hit.m_intersection);
380
381 QPickEventPtr pickEvent;
382 const auto eventPos = event->position();
383 switch (hit.m_type) {
384 case QCollisionQueryResult::Hit::Triangle:
385 pickEvent.reset(t: new QPickTriangleEvent(eventPos,
386 convertToQVector3D(v: hit.m_intersection),
387 convertToQVector3D(v: localIntersection),
388 hit.m_distance,
389 hit.m_primitiveIndex,
390 hit.m_vertexIndex[0],
391 hit.m_vertexIndex[1],
392 hit.m_vertexIndex[2],
393 eventButton, eventButtons,
394 eventModifiers,
395 convertToQVector3D(v: hit.m_uvw)));
396 break;
397 case QCollisionQueryResult::Hit::Edge:
398 pickEvent.reset(t: new QPickLineEvent(eventPos,
399 convertToQVector3D(v: hit.m_intersection),
400 convertToQVector3D(v: localIntersection),
401 hit.m_distance,
402 hit.m_primitiveIndex,
403 hit.m_vertexIndex[0], hit.m_vertexIndex[1],
404 eventButton, eventButtons, eventModifiers));
405 break;
406 case QCollisionQueryResult::Hit::Point:
407 pickEvent.reset(t: new QPickPointEvent(eventPos,
408 convertToQVector3D(v: hit.m_intersection),
409 convertToQVector3D(v: localIntersection),
410 hit.m_distance,
411 hit.m_vertexIndex[0],
412 eventButton, eventButtons, eventModifiers));
413 break;
414 case QCollisionQueryResult::Hit::Entity:
415 pickEvent.reset(t: new QPickEvent(eventPos,
416 convertToQVector3D(v: hit.m_intersection),
417 convertToQVector3D(v: localIntersection),
418 hit.m_distance,
419 eventButton, eventButtons, eventModifiers));
420 break;
421 default:
422 Q_UNREACHABLE();
423 }
424 Qt3DRender::QPickEventPrivate::get(object: pickEvent.data())->m_entity = hit.m_entityId;
425 switch (event->type()) {
426 case QEvent::MouseButtonPress: {
427 // Store pressed object handle
428 m_currentPicker = objectPickerHandle;
429 m_currentViewport = viewportNodeId;
430 // Send pressed event to m_currentPicker
431 d->dispatches.push_back(t: {.pickerId: objectPicker->peerId(), .sourceEventType: event->type(), .resultingEvent: pickEvent, .viewportNodeId: viewportNodeId});
432 objectPicker->setPressed(true);
433 break;
434 }
435
436 case QEvent::MouseButtonRelease: {
437 // Only send the release event if it was pressed
438 if (objectPicker->isPressed()) {
439 d->dispatches.push_back(t: {.pickerId: objectPicker->peerId(), .sourceEventType: event->type(), .resultingEvent: pickEvent, .viewportNodeId: viewportNodeId});
440 objectPicker->setPressed(false);
441 }
442 if (lastCurrentPicker != nullptr && m_currentPicker == objectPickerHandle) {
443 d->dispatches.push_back(t: {.pickerId: objectPicker->peerId(),
444 .sourceEventType: PickBoundingVolumeJobPrivate::MouseButtonClick,
445 .resultingEvent: pickEvent, .viewportNodeId: viewportNodeId});
446 m_currentPicker = HObjectPicker();
447 m_currentViewport = {};
448 }
449 break;
450 }
451#if QT_CONFIG(gestures)
452 case QEvent::Gesture: {
453 d->dispatches.push_back(t: {.pickerId: objectPicker->peerId(),
454 .sourceEventType: PickBoundingVolumeJobPrivate::MouseButtonClick,
455 .resultingEvent: pickEvent, .viewportNodeId: viewportNodeId});
456 break;
457 }
458#endif
459 case QEvent::MouseMove: {
460 if ((objectPicker->isPressed() || objectPicker->isHoverEnabled()) && objectPicker->isDragEnabled())
461 d->dispatches.push_back(t: {.pickerId: objectPicker->peerId(), .sourceEventType: event->type(), .resultingEvent: pickEvent, .viewportNodeId: viewportNodeId});
462 Q_FALLTHROUGH(); // fallthrough
463 }
464 case QEvent::HoverMove: {
465 if (!m_hoveredPickers.contains(t: objectPickerHandle)) {
466 if (objectPicker->isHoverEnabled()) {
467 // Send entered event to objectPicker
468 d->dispatches.push_back(t: {.pickerId: objectPicker->peerId(), .sourceEventType: QEvent::Enter, .resultingEvent: pickEvent, .viewportNodeId: viewportNodeId});
469 // and save it in the hoveredPickers
470 m_hoveredPickers.push_back(t: objectPickerHandle);
471 }
472 }
473 break;
474 }
475
476 default:
477 break;
478 }
479 }
480
481 // The ObjectPicker was hit -> it is still being hovered
482 m_hoveredPickersToClear.removeAll(t: objectPickerHandle);
483
484 lastCurrentPicker = m_manager->objectPickerManager()->data(handle: m_currentPicker);
485 }
486
487 // Otherwise no hits
488 } else {
489 switch (event->type()) {
490 case QEvent::MouseButtonRelease: {
491 // Send release event to m_currentPicker
492 if (lastCurrentPicker != nullptr && m_currentViewport == viewportNodeId) {
493 m_currentPicker = HObjectPicker();
494 m_currentViewport = {};
495 QPickEventPtr pickEvent(new QPickEvent);
496 lastCurrentPicker->setPressed(false);
497 d->dispatches.push_back(t: {.pickerId: lastCurrentPicker->peerId(), .sourceEventType: event->type(), .resultingEvent: pickEvent, .viewportNodeId: viewportNodeId});
498 }
499 break;
500 }
501 default:
502 break;
503 }
504 }
505}
506
507void PickBoundingVolumeJob::clearPreviouslyHoveredPickers()
508{
509 Q_D(PickBoundingVolumeJob);
510
511 for (const HObjectPicker &pickHandle : std::as_const(t&: m_hoveredPickersToClear)) {
512 ObjectPicker *pick = m_manager->objectPickerManager()->data(handle: pickHandle);
513 if (pick)
514 d->dispatches.push_back(t: {.pickerId: pick->peerId(), .sourceEventType: QEvent::Leave, .resultingEvent: {}, .viewportNodeId: {}});
515 m_hoveredPickers.removeAll(t: pickHandle);
516 }
517
518 m_hoveredPickersToClear.clear();
519}
520
521} // namespace Render
522
523} // namespace Qt3DRender
524
525QT_END_NAMESPACE
526

source code of qt3d/src/render/jobs/pickboundingvolumejob.cpp