| /**************************************************************************** |
| ** |
| ** 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 <Qt3DCore/private/qaspectmanager_p.h> |
| #include <Qt3DRender/qobjectpicker.h> |
| #include <Qt3DRender/qviewport.h> |
| #include <Qt3DRender/qgeometryrenderer.h> |
| #include <Qt3DRender/private/qobjectpicker_p.h> |
| #include <Qt3DRender/private/nodemanagers_p.h> |
| #include <Qt3DRender/private/entity_p.h> |
| #include <Qt3DRender/private/objectpicker_p.h> |
| #include <Qt3DRender/private/managers_p.h> |
| #include <Qt3DRender/private/geometryrenderer_p.h> |
| #include <Qt3DRender/private/rendersettings_p.h> |
| #include <Qt3DRender/private/trianglesvisitor_p.h> |
| #include <Qt3DRender/private/job_common_p.h> |
| #include <Qt3DRender/private/qpickevent_p.h> |
| #include <Qt3DRender/private/pickboundingvolumeutils_p.h> |
| |
| #include <QSurface> |
| #include <QWindow> |
| #include <QOffscreenSurface> |
| |
| 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; |
| }; |
| |
| QVector<EventDetails> dispatches; |
| PickBoundingVolumeJob *q_ptr; |
| Q_DECLARE_PUBLIC(PickBoundingVolumeJob) |
| }; |
| |
| |
| bool PickBoundingVolumeJobPrivate::isRequired() const |
| { |
| Q_Q(const 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<QObjectPicker *>(manager->lookupNode(res.pickerId)); |
| previousId = res.pickerId; |
| } |
| if (!node) |
| continue; |
| |
| QObjectPickerPrivate *dnode = static_cast<QObjectPickerPrivate *>(QObjectPickerPrivate::get(node)); |
| |
| // resolve front end details |
| QPickEvent *pickEvent = res.resultingEvent.data(); |
| if (pickEvent) { |
| QPickEventPrivate *dpickEvent = QPickEventPrivate::get(pickEvent); |
| dpickEvent->m_viewport = static_cast<QViewport *>(manager->lookupNode(res.viewportNodeId)); |
| dpickEvent->m_entityPtr = static_cast<QEntity *>(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<QPair<QObject*, QMouseEvent>> &pendingEvents) |
| { |
| m_pendingMouseEvents.append(pendingEvents); |
| } |
| |
| void PickBoundingVolumeJob::setKeyEvents(const QList<QKeyEvent> &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<PickingUtils::ViewportCameraAreaDetails> 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<ObjectPicker>(); |
| |
| // 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 *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 |