/**************************************************************************** ** ** Copyright (C) 2015 Paul Lemire paul.lemire350@gmail.com ** Contact: https://www.qt.io/licensing/ ** ** This file is part of the Qt3D module of the Qt Toolkit. ** ** $QT_BEGIN_LICENSE:LGPL$ ** Commercial License Usage ** Licensees holding valid commercial Qt licenses may use this file in ** accordance with the commercial license agreement provided with the ** Software or, alternatively, in accordance with the terms contained in ** a written agreement between you and The Qt Company. For licensing terms ** and conditions see https://www.qt.io/terms-conditions. For further ** information use the contact form at https://www.qt.io/contact-us. ** ** GNU Lesser General Public License Usage ** Alternatively, this file may be used under the terms of the GNU Lesser ** General Public License version 3 as published by the Free Software ** Foundation and appearing in the file LICENSE.LGPL3 included in the ** packaging of this file. Please review the following information to ** ensure the GNU Lesser General Public License version 3 requirements ** will be met: https://www.gnu.org/licenses/lgpl-3.0.html. ** ** GNU General Public License Usage ** Alternatively, this file may be used under the terms of the GNU ** General Public License version 2.0 or (at your option) the GNU General ** Public license version 3 or any later version approved by the KDE Free ** Qt Foundation. The licenses are as published by the Free Software ** Foundation and appearing in the file LICENSE.GPL2 and LICENSE.GPL3 ** included in the packaging of this file. Please review the following ** information to ensure the GNU General Public License requirements will ** be met: https://www.gnu.org/licenses/gpl-2.0.html and ** https://www.gnu.org/licenses/gpl-3.0.html. ** ** $QT_END_LICENSE$ ** ****************************************************************************/ #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 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() override; void postFrame(Qt3DCore::QAspectManager *manager) override; enum CustomEventType { MouseButtonClick = QEvent::User, }; struct EventDetails { Qt3DCore::QNodeId pickerId; int sourceEventType; QPickEventPtr resultingEvent; Qt3DCore::QNodeId viewportNodeId; }; QVector dispatches; PickBoundingVolumeJob *q_ptr; Q_DECLARE_PUBLIC(PickBoundingVolumeJob) }; bool PickBoundingVolumeJobPrivate::isRequired() { Q_Q(PickBoundingVolumeJob); return !q->m_pendingMouseEvents.isEmpty() || q->m_pickersDirty || q->m_oneEnabledAtLeast; } void PickBoundingVolumeJobPrivate::postFrame(Qt3DCore::QAspectManager *manager) { using namespace Qt3DCore; QNodeId previousId; QObjectPicker *node = nullptr; for (auto res: qAsConst(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; } void PickBoundingVolumeJob::setMouseEvents(const QList> &pendingEvents) { m_pendingMouseEvents.append(pendingEvents); } void PickBoundingVolumeJob::setKeyEvents(const QList &pendingEvents) { m_pendingKeyEvents.append(pendingEvents); } 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 = std::move(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; } PickingUtils::ViewportCameraAreaGatherer vcaGatherer; // TO DO: We could cache this and only gather when we know the FrameGraph tree has changed const QVector vcaDetails = vcaGatherer.gather(m_frameGraphRoot); // If we have no viewport / camera or area, return early if (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 const bool trianglePickingRequested = (m_renderSettings->pickMethod() & QPickingSettings::TrianglePicking); const bool edgePickingRequested = (m_renderSettings->pickMethod() & QPickingSettings::LinePicking); const bool pointPickingRequested = (m_renderSettings->pickMethod() & QPickingSettings::PointPicking); const bool primitivePickingRequested = pointPickingRequested | edgePickingRequested | trianglePickingRequested; const bool frontFaceRequested = m_renderSettings->faceOrientationPickingMode() != QPickingSettings::BackFace; const bool backFaceRequested = m_renderSettings->faceOrientationPickingMode() != QPickingSettings::FrontFace; const float pickWorldSpaceTolerance = m_renderSettings->pickWorldSpaceTolerance(); // For each mouse event for (const auto &event : mouseEvents) { m_hoveredPickersToClear = m_hoveredPickers; QPickEvent::Buttons eventButton = QPickEvent::NoButton; int eventButtons = 0; int eventModifiers = QPickEvent::NoModifier; setEventButtonAndModifiers(event.second, eventButton, eventButtons, eventModifiers); // For each Viewport / Camera and Area entry for (const PickingUtils::ViewportCameraAreaDetails &vca : vcaDetails) { PickingUtils::HitList sphereHits; QRay3D ray = rayForViewportAndCamera(vca, event.first, event.second.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.second, PickingUtils::HitList(), eventButton, eventButtons, eventModifiers, m_renderSettings->pickResultMode(), vca.viewportNodeId); continue; } PickingUtils::HierarchicalEntityPicker entityPicker(ray); if (entityPicker.collectHits(m_manager, m_node)) { if (trianglePickingRequested) { PickingUtils::TriangleCollisionGathererFunctor gathererFunctor; gathererFunctor.m_frontFaceRequested = frontFaceRequested; gathererFunctor.m_backFaceRequested = backFaceRequested; gathererFunctor.m_manager = m_manager; gathererFunctor.m_ray = ray; gathererFunctor.m_entityToPriorityTable = entityPicker.entityToPriorityTable(); sphereHits << gathererFunctor.computeHits(entityPicker.entities(), m_renderSettings->pickResultMode()); } if (edgePickingRequested) { PickingUtils::LineCollisionGathererFunctor gathererFunctor; gathererFunctor.m_manager = m_manager; gathererFunctor.m_ray = ray; gathererFunctor.m_pickWorldSpaceTolerance = pickWorldSpaceTolerance; gathererFunctor.m_entityToPriorityTable = entityPicker.entityToPriorityTable(); sphereHits << gathererFunctor.computeHits(entityPicker.entities(), m_renderSettings->pickResultMode()); PickingUtils::AbstractCollisionGathererFunctor::sortHits(sphereHits); } if (pointPickingRequested) { PickingUtils::PointCollisionGathererFunctor gathererFunctor; gathererFunctor.m_manager = m_manager; gathererFunctor.m_ray = ray; gathererFunctor.m_pickWorldSpaceTolerance = pickWorldSpaceTolerance; gathererFunctor.m_entityToPriorityTable = entityPicker.entityToPriorityTable(); sphereHits << gathererFunctor.computeHits(entityPicker.entities(), m_renderSettings->pickResultMode()); PickingUtils::AbstractCollisionGathererFunctor::sortHits(sphereHits); } if (!primitivePickingRequested) { sphereHits << entityPicker.hits(); PickingUtils::AbstractCollisionGathererFunctor::sortHits(sphereHits); if (m_renderSettings->pickResultMode() != QPickingSettings::AllPicks) sphereHits = { sphereHits.front() }; } } // Dispatch events based on hit results dispatchPickEvents(event.second, sphereHits, eventButton, eventButtons, eventModifiers, m_renderSettings->pickResultMode(), vca.viewportNodeId); } } // 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::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.isEmpty()) { // 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 : qAsConst(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() * hit.m_intersection; QPickEventPtr pickEvent; switch (hit.m_type) { case QCollisionQueryResult::Hit::Triangle: pickEvent.reset(new QPickTriangleEvent(event.localPos(), 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(event.localPos(), 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(event.localPos(), 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(event.localPos(), 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; // 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(); } 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_currentPicker = HObjectPicker(); 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 : qAsConst(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