// Copyright (C) 2015 Paul Lemire paul.lemire350@gmail.com // SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only #include "pickboundingvolumejob_p.h" #include "qpicktriangleevent.h" #include "qpicklineevent.h" #include "qpickpointevent.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include QT_BEGIN_NAMESPACE namespace Qt3DRender { using namespace Qt3DRender::RayCasting; namespace Render { class PickBoundingVolumeJobPrivate : public Qt3DCore::QAspectJobPrivate { public: PickBoundingVolumeJobPrivate(PickBoundingVolumeJob *q) : q_ptr(q) { } ~PickBoundingVolumeJobPrivate() override = default; bool isRequired() const override; void postFrame(Qt3DCore::QAspectManager *manager) override; enum CustomEventType { MouseButtonClick = QEvent::User, }; struct EventDetails { Qt3DCore::QNodeId pickerId; int sourceEventType; QPickEventPtr resultingEvent; Qt3DCore::QNodeId viewportNodeId; }; QList dispatches; PickBoundingVolumeJob *q_ptr; Q_DECLARE_PUBLIC(PickBoundingVolumeJob) }; bool PickBoundingVolumeJobPrivate::isRequired() const { Q_Q(const PickBoundingVolumeJob); return !q->m_pendingMouseEvents.empty() || q->m_pickersDirty || q->m_oneEnabledAtLeast; } void PickBoundingVolumeJobPrivate::postFrame(Qt3DCore::QAspectManager *manager) { using namespace Qt3DCore; QNodeId previousId; QObjectPicker *node = nullptr; for (auto res: std::as_const(dispatches)) { if (previousId != res.pickerId) { node = qobject_cast(manager->lookupNode(res.pickerId)); previousId = res.pickerId; } if (!node) continue; QObjectPickerPrivate *dnode = static_cast(QObjectPickerPrivate::get(node)); // resolve front end details QPickEvent *pickEvent = res.resultingEvent.data(); if (pickEvent) { QPickEventPrivate *dpickEvent = QPickEventPrivate::get(pickEvent); dpickEvent->m_viewport = static_cast(manager->lookupNode(res.viewportNodeId)); dpickEvent->m_entityPtr = static_cast(manager->lookupNode(dpickEvent->m_entity)); } // dispatch event switch (res.sourceEventType) { case QEvent::MouseButtonPress: dnode->pressedEvent(pickEvent); break; case QEvent::MouseButtonRelease: dnode->releasedEvent(pickEvent); break; case MouseButtonClick: dnode->clickedEvent(pickEvent); break; case QEvent::MouseMove: dnode->movedEvent(pickEvent); break; case QEvent::Enter: emit node->entered(); dnode->setContainsMouse(true); break; case QEvent::Leave: dnode->setContainsMouse(false); emit node->exited(); break; default: Q_UNREACHABLE(); } } dispatches.clear(); } namespace { void setEventButtonAndModifiers(const QMouseEvent *event, QPickEvent::Buttons &eventButton, int &eventButtons, int &eventModifiers) { switch (event->button()) { case Qt::LeftButton: eventButton = QPickEvent::LeftButton; break; case Qt::RightButton: eventButton = QPickEvent::RightButton; break; case Qt::MiddleButton: eventButton = QPickEvent::MiddleButton; break; case Qt::BackButton: eventButton = QPickEvent::BackButton; break; default: break; } if (event->buttons() & Qt::LeftButton) eventButtons |= QPickEvent::LeftButton; if (event->buttons() & Qt::RightButton) eventButtons |= QPickEvent::RightButton; if (event->buttons() & Qt::MiddleButton) eventButtons |= QPickEvent::MiddleButton; if (event->buttons() & Qt::BackButton) eventButtons |= QPickEvent::BackButton; if (event->modifiers() & Qt::ShiftModifier) eventModifiers |= QPickEvent::ShiftModifier; if (event->modifiers() & Qt::ControlModifier) eventModifiers |= QPickEvent::ControlModifier; if (event->modifiers() & Qt::AltModifier) eventModifiers |= QPickEvent::AltModifier; if (event->modifiers() & Qt::MetaModifier) eventModifiers |= QPickEvent::MetaModifier; if (event->modifiers() & Qt::KeypadModifier) eventModifiers |= QPickEvent::KeypadModifier; } } // anonymous PickBoundingVolumeJob::PickBoundingVolumeJob() : AbstractPickingJob(*new PickBoundingVolumeJobPrivate(this)) , m_pickersDirty(true) { SET_JOB_RUN_STAT_TYPE(this, JobTypes::PickBoundingVolume, 0) } void PickBoundingVolumeJob::setRoot(Entity *root) { m_node = root; } bool PickBoundingVolumeJob::processMouseEvent(QObject* object, QMouseEvent *event) { #if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0) m_pendingMouseEvents.emplace_back(object, std::unique_ptr(static_cast(event->clone()))); #else m_pendingMouseEvents.emplace_back(object, new QMouseEvent(*event)); #endif return false; } void PickBoundingVolumeJob::markPickersDirty() { m_pickersDirty = true; } bool PickBoundingVolumeJob::runHelper() { // Move to clear the events so that we don't process them several times // if run is called several times const auto mouseEvents = Qt3DCore::moveAndClear(m_pendingMouseEvents); // If we have no events return early if (mouseEvents.empty()) return false; // Quickly look which picker settings we've got if (m_pickersDirty) { m_pickersDirty = false; m_oneEnabledAtLeast = false; m_oneHoverAtLeast = false; const auto activeHandles = m_manager->objectPickerManager()->activeHandles(); for (const auto &handle : activeHandles) { auto picker = m_manager->objectPickerManager()->data(handle); m_oneEnabledAtLeast |= picker->isEnabled(); m_oneHoverAtLeast |= picker->isHoverEnabled(); if (m_oneEnabledAtLeast && m_oneHoverAtLeast) break; } } // bail out early if no picker is enabled if (!m_oneEnabledAtLeast) return false; bool hasMoveEvent = false; bool hasOtherEvent = false; // Quickly look which types of events we've got for (const auto &event : mouseEvents) { const bool isMove = (event.second->type() == QEvent::MouseMove); hasMoveEvent |= isMove; hasOtherEvent |= !isMove; } // In the case we have a move event, find if we actually have // an object picker that cares about these if (!hasOtherEvent) { // Retrieve the last used object picker ObjectPicker *lastCurrentPicker = m_manager->objectPickerManager()->data(m_currentPicker); // The only way to set lastCurrentPicker is to click // so we can return since if we're there it means we // have only move events. But keep on if hover support // is needed if (lastCurrentPicker == nullptr && !m_oneHoverAtLeast) return false; const bool caresAboutMove = (hasMoveEvent && (m_oneHoverAtLeast || (lastCurrentPicker && lastCurrentPicker->isDragEnabled()))); // Early return if the current object picker doesn't care about move events if (!caresAboutMove) return false; } const PickingUtils::PickConfiguration pickConfiguration(m_frameGraphRoot, m_renderSettings); if (pickConfiguration.vcaDetails.empty()) return false; // TO DO: // If we have move or hover move events that someone cares about, we try to avoid expensive computations // by compressing them into a single one // For each mouse event for (const auto &event : mouseEvents) processPickEvent(pickConfiguration, event.first, event.second.get()); // Clear Hovered elements that needs to be cleared // Send exit event to object pickers on which we // had set the hovered flag for a previous frame // and that aren't being hovered any longer clearPreviouslyHoveredPickers(); return true; } void PickBoundingVolumeJob::processPickEvent(const PickingUtils::PickConfiguration &pickConfiguration, QObject *object, const QMouseEvent *event) { m_hoveredPickersToClear = m_hoveredPickers; QPickEvent::Buttons eventButton = QPickEvent::NoButton; int eventButtons = 0; int eventModifiers = QPickEvent::NoModifier; setEventButtonAndModifiers(event, eventButton, eventButtons, eventModifiers); // For each Viewport / Camera and Area entry for (const PickingUtils::ViewportCameraAreaDetails &vca : pickConfiguration.vcaDetails) { PickingUtils::HitList sphereHits; QRay3D ray = rayForViewportAndCamera(vca, object, event->pos()); if (!ray.isValid()) { // An invalid rays is when we've lost our surface or the mouse // has moved out of the viewport In case of a button released // outside of the viewport, we still want to notify the // lastCurrent entity about this. dispatchPickEvents(event, PickingUtils::HitList(), eventButton, eventButtons, eventModifiers, m_renderSettings->pickResultMode(), vca.viewportNodeId); continue; } PickingUtils::HierarchicalEntityPicker entityPicker(ray); entityPicker.setLayerFilterIds(vca.layersFilters); if (entityPicker.collectHits(m_manager, m_node)) { if (pickConfiguration.trianglePickingRequested) { PickingUtils::TriangleCollisionGathererFunctor gathererFunctor; gathererFunctor.m_frontFaceRequested = pickConfiguration.frontFaceRequested; gathererFunctor.m_backFaceRequested = pickConfiguration.backFaceRequested; gathererFunctor.m_manager = m_manager; gathererFunctor.m_ray = ray; gathererFunctor.m_entityToPriorityTable = entityPicker.entityToPriorityTable(); Qt3DCore::moveAtEnd(sphereHits, gathererFunctor.computeHits(entityPicker.entities(), m_renderSettings->pickResultMode())); } if (pickConfiguration.edgePickingRequested) { PickingUtils::LineCollisionGathererFunctor gathererFunctor; gathererFunctor.m_manager = m_manager; gathererFunctor.m_ray = ray; gathererFunctor.m_pickWorldSpaceTolerance = pickConfiguration.pickWorldSpaceTolerance; gathererFunctor.m_entityToPriorityTable = entityPicker.entityToPriorityTable(); Qt3DCore::moveAtEnd(sphereHits, gathererFunctor.computeHits(entityPicker.entities(), m_renderSettings->pickResultMode())); PickingUtils::AbstractCollisionGathererFunctor::sortHits(sphereHits); } if (pickConfiguration.pointPickingRequested) { PickingUtils::PointCollisionGathererFunctor gathererFunctor; gathererFunctor.m_manager = m_manager; gathererFunctor.m_ray = ray; gathererFunctor.m_pickWorldSpaceTolerance = pickConfiguration.pickWorldSpaceTolerance; gathererFunctor.m_entityToPriorityTable = entityPicker.entityToPriorityTable(); Qt3DCore::moveAtEnd(sphereHits, gathererFunctor.computeHits(entityPicker.entities(), m_renderSettings->pickResultMode())); PickingUtils::AbstractCollisionGathererFunctor::sortHits(sphereHits); } if (!pickConfiguration.primitivePickingRequested) { Qt3DCore::moveAtEnd(sphereHits, entityPicker.hits()); PickingUtils::AbstractCollisionGathererFunctor::sortHits(sphereHits); if (m_renderSettings->pickResultMode() != QPickingSettings::AllPicks) sphereHits = { sphereHits.front() }; } } // Dispatch events based on hit results dispatchPickEvents(event, sphereHits, eventButton, eventButtons, eventModifiers, m_renderSettings->pickResultMode(), vca.viewportNodeId); } } void PickBoundingVolumeJob::dispatchPickEvents(const QMouseEvent *event, const PickingUtils::HitList &sphereHits, QPickEvent::Buttons eventButton, int eventButtons, int eventModifiers, bool allHitsRequested, Qt3DCore::QNodeId viewportNodeId) { Q_D(PickBoundingVolumeJob); ObjectPicker *lastCurrentPicker = m_manager->objectPickerManager()->data(m_currentPicker); // If we have hits if (!sphereHits.empty()) { // Note: how can we control that we want the first/last/all elements along the ray to be picked // How do we differentiate betwnee an Entity with no GeometryRenderer and one with one, both having // an ObjectPicker component when it comes for (const QCollisionQueryResult::Hit &hit : std::as_const(sphereHits)) { Entity *entity = m_manager->renderNodesManager()->lookupResource(hit.m_entityId); HObjectPicker objectPickerHandle = entity->componentHandle(); // If the Entity which actually received the hit doesn't have // an object picker component, we need to check the parent if it has one ... while (objectPickerHandle.isNull() && entity != nullptr) { entity = entity->parent(); if (entity != nullptr) objectPickerHandle = entity->componentHandle(); } ObjectPicker *objectPicker = m_manager->objectPickerManager()->data(objectPickerHandle); if (objectPicker != nullptr && objectPicker->isEnabled()) { if (lastCurrentPicker && !allHitsRequested) { // if there is a current picker, it will "grab" all events until released. // Clients should test that entity is what they expect (or only use // world coordinates) objectPicker = lastCurrentPicker; } // Send the corresponding event Vector3D localIntersection = hit.m_intersection; if (entity && entity->worldTransform()) localIntersection = entity->worldTransform()->inverted().map(hit.m_intersection); QPickEventPtr pickEvent; #if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0) const auto eventPos = event->position(); #else const auto eventPos = event->pos(); #endif switch (hit.m_type) { case QCollisionQueryResult::Hit::Triangle: pickEvent.reset(new QPickTriangleEvent(eventPos, convertToQVector3D(hit.m_intersection), convertToQVector3D(localIntersection), hit.m_distance, hit.m_primitiveIndex, hit.m_vertexIndex[0], hit.m_vertexIndex[1], hit.m_vertexIndex[2], eventButton, eventButtons, eventModifiers, convertToQVector3D(hit.m_uvw))); break; case QCollisionQueryResult::Hit::Edge: pickEvent.reset(new QPickLineEvent(eventPos, convertToQVector3D(hit.m_intersection), convertToQVector3D(localIntersection), hit.m_distance, hit.m_primitiveIndex, hit.m_vertexIndex[0], hit.m_vertexIndex[1], eventButton, eventButtons, eventModifiers)); break; case QCollisionQueryResult::Hit::Point: pickEvent.reset(new QPickPointEvent(eventPos, convertToQVector3D(hit.m_intersection), convertToQVector3D(localIntersection), hit.m_distance, hit.m_vertexIndex[0], eventButton, eventButtons, eventModifiers)); break; case QCollisionQueryResult::Hit::Entity: pickEvent.reset(new QPickEvent(eventPos, convertToQVector3D(hit.m_intersection), convertToQVector3D(localIntersection), hit.m_distance, eventButton, eventButtons, eventModifiers)); break; default: Q_UNREACHABLE(); } Qt3DRender::QPickEventPrivate::get(pickEvent.data())->m_entity = hit.m_entityId; switch (event->type()) { case QEvent::MouseButtonPress: { // Store pressed object handle m_currentPicker = objectPickerHandle; m_currentViewport = viewportNodeId; // Send pressed event to m_currentPicker d->dispatches.push_back({objectPicker->peerId(), event->type(), pickEvent, viewportNodeId}); objectPicker->setPressed(true); break; } case QEvent::MouseButtonRelease: { // Only send the release event if it was pressed if (objectPicker->isPressed()) { d->dispatches.push_back({objectPicker->peerId(), event->type(), pickEvent, viewportNodeId}); objectPicker->setPressed(false); } if (lastCurrentPicker != nullptr && m_currentPicker == objectPickerHandle) { d->dispatches.push_back({objectPicker->peerId(), PickBoundingVolumeJobPrivate::MouseButtonClick, pickEvent, viewportNodeId}); m_currentPicker = HObjectPicker(); m_currentViewport = {}; } break; } #if QT_CONFIG(gestures) case QEvent::Gesture: { d->dispatches.push_back({objectPicker->peerId(), PickBoundingVolumeJobPrivate::MouseButtonClick, pickEvent, viewportNodeId}); break; } #endif case QEvent::MouseMove: { if ((objectPicker->isPressed() || objectPicker->isHoverEnabled()) && objectPicker->isDragEnabled()) d->dispatches.push_back({objectPicker->peerId(), event->type(), pickEvent, viewportNodeId}); Q_FALLTHROUGH(); // fallthrough } case QEvent::HoverMove: { if (!m_hoveredPickers.contains(objectPickerHandle)) { if (objectPicker->isHoverEnabled()) { // Send entered event to objectPicker d->dispatches.push_back({objectPicker->peerId(), QEvent::Enter, pickEvent, viewportNodeId}); // and save it in the hoveredPickers m_hoveredPickers.push_back(objectPickerHandle); } } break; } default: break; } } // The ObjectPicker was hit -> it is still being hovered m_hoveredPickersToClear.removeAll(objectPickerHandle); lastCurrentPicker = m_manager->objectPickerManager()->data(m_currentPicker); } // Otherwise no hits } else { switch (event->type()) { case QEvent::MouseButtonRelease: { // Send release event to m_currentPicker if (lastCurrentPicker != nullptr && m_currentViewport == viewportNodeId) { m_currentPicker = HObjectPicker(); m_currentViewport = {}; QPickEventPtr pickEvent(new QPickEvent); lastCurrentPicker->setPressed(false); d->dispatches.push_back({lastCurrentPicker->peerId(), event->type(), pickEvent, viewportNodeId}); } break; } default: break; } } } void PickBoundingVolumeJob::clearPreviouslyHoveredPickers() { Q_D(PickBoundingVolumeJob); for (const HObjectPicker &pickHandle : std::as_const(m_hoveredPickersToClear)) { ObjectPicker *pick = m_manager->objectPickerManager()->data(pickHandle); if (pick) d->dispatches.push_back({pick->peerId(), QEvent::Leave, {}, {}}); m_hoveredPickers.removeAll(pickHandle); } m_hoveredPickersToClear.clear(); } } // namespace Render } // namespace Qt3DRender QT_END_NAMESPACE