// Copyright (C) 2025 The Qt Company Ltd. // SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause import QtQuick import QtQuick3D import QtQuick3D.Xr pragma ComponentBehavior: Bound XrController { id: theController poseSpace: XrController.AimPose readonly property vector3d pickDirection: Qt.vector3d(0, 0, -1) required property QtObject view // an object that has implemented rayPick(pos, dir) property XrCursor xrCursor: null property int pickStatus: PickResult.Null property bool enableVirtualMouse: false property bool enableThumbstickMove: true property real yeetDistance: 400 property real emptyRayLength: 400 //! [signals] property Model hoveredObject: null signal objectPressed(obj: Model, pos: vector3d, direction: vector3d) signal moved(pos: vector3d, direction: vector3d) signal released() signal objectGrabbed(obj: Model) //! [signals] QtObject { id: priv property QtObject hitObject: null property bool isGrabbing: false property bool isInteracting: false property vector3d offset property quaternion rotation property Node grabbedObject function tryGrab() { theController.objectGrabbed(hitObject as Model) } function doGrab(obj: Node) { grabbedObject = obj if (grabbedObject) { const scenePos = grabbedObject.scenePosition const sceneRot = grabbedObject.sceneRotation offset = theController.mapPositionFromScene(scenePos) rotation = theController.rotation.inverted().times(sceneRot) isGrabbing = true if (xrCursor) xrCursor.visible = false } } function ungrab() { hitObject = grabbedObject grabbedObject = null isGrabbing = false } function handlePress() { theController.objectPressed(hitObject, theController.scenePosition, theController.forward) isInteracting = true pickRay.pressed = true } function handleRelease() { isInteracting = false theController.released() pickRay.pressed = false } function moveObject() { if (grabbedObject) { let newPos = theController.scenePosition.plus(theController.rotation.times(offset)) let newRot = theController.sceneRotation.times(rotation) if (grabbedObject.parent) { newPos = grabbedObject.parent.mapPositionFromScene(newPos) newRot = grabbedObject.parent.sceneRotation.inverted().times(newRot) } grabbedObject.setPosition(newPos) grabbedObject.setRotation(newRot) } } function yeet(delta: real) { const localForward = Qt.vector3d(0, 0, -1) const rayPos = localForward.times(pickRay.length) const yeetOffset = offset.minus(rayPos) pickRay.length = Math.max(10, Math.min(pickRay.length * (1 + delta/10), theController.yeetDistance)) offset = yeetOffset.plus(localForward.times(pickRay.length)) } function findObject(pickResult : var) { const didHit = pickResult.hitType !== PickResult.Null theController.pickStatus = pickResult.hitType if (didHit) { pickRay.hit = true pickRay.length = pickResult.distance * 0.75 hitObject = pickResult.objectHit } else { pickRay.hit = false pickRay.length = theController.emptyRayLength hitObject = null } theController.hoveredObject = hitObject } function handleMove() { const dir = theController.mapDirectionToScene(pickDirection) const pickResult = theController.view.rayPick(scenePosition, dir) if (xrCursor) { const didHit = pickResult.hitType !== PickResult.Null xrCursor.visible = didHit && !priv.isGrabbing if (didHit) xrCursor.setPositionAndOrientation(pickResult.scenePosition, pickResult.sceneNormal) } if (isInteracting) theController.moved(theController.scenePosition, theController.forward) else if (isGrabbing) moveObject() else findObject(pickResult) } } function startGrab(obj: Node) { priv.doGrab(obj) } PickRay { id: pickRay visible: !priv.isGrabbing length: theController.emptyRayLength } onRotationChanged: { priv.handleMove() } XrInputAction { id: grabAction controller: theController.controller actionId: [XrInputAction.SqueezeValue, XrInputAction.SqueezePressed] onPressedChanged: { if (pressed) { priv.tryGrab() } else { priv.ungrab() } } } XrInputAction { id: triggerAction controller: theController.controller actionId: [XrInputAction.TriggerValue, XrInputAction.TriggerPressed, XrInputAction.IndexFingerPinch] onPressedChanged: { if (pressed) priv.handlePress() else priv.handleRelease() } } XrInputAction { id: yeetAction enabled: theController.enableThumbstickMove controller: theController.controller actionId: XrInputAction.ThumbstickY } FrameAnimation { id: yeetAnimation running: priv.isGrabbing && Math.abs(yeetAction.value) > 0.1 onTriggered: priv.yeet(yeetAction.value * frameTime * 30) } XrVirtualMouse { enabled: theController.enableVirtualMouse source: theController view: theController.view as XrView leftMouseButton: triggerAction.pressed } }