import Qt3D.Core 2.15 import Qt3D.Render 2.15 import Qt3D.Input 2.15 import Qt3D.Extras 2.15 import QtQuick 2.15 import Qt3D.Logic 2.15 import QtQuick.Controls 2.15 import Utils 1.0 /** * Simple transformation gizmo entirely made with Qt3D entities. * Uses Python Transformations3DHelper to compute matrices. * This TransformGizmo entity should only be instantiated in EntityWithGizmo entity which is its wrapper. * It means, to use it for a specified application, make sure to instantiate EntityWithGizmo. */ Entity { id: root property Camera camera property var windowSize property Layer frontLayerComponent // Used to draw gizmo on top of everything property var window readonly property alias gizmoScale: gizmoScaleLookSlider.value property bool uniformScale: false // By default, the scale is not uniform property bool focusGizmoPriority: false // If true, it is used to give the priority to the current transformation (and not to a upper-level binding) property Transform gizmoDisplayTransform: Transform { id: gizmoDisplayTransform scale: root.gizmoScale * (camera.position.minus(gizmoDisplayTransform.translation)).length() // The gizmo needs a constant apparent size } // Component the object controlled by the gizmo must use property Transform objectTransform : Transform { translation: gizmoDisplayTransform.translation rotation: gizmoDisplayTransform.rotation scale3D: Qt.vector3d(1,1,1) } signal pickedChanged(bool pressed) signal gizmoChanged(var translation, var rotation, var scale, int type) function emitGizmoChanged(type) { const translation = gizmoDisplayTransform.translation // Position in space const rotation = Qt.vector3d(gizmoDisplayTransform.rotationX, gizmoDisplayTransform.rotationY, gizmoDisplayTransform.rotationZ) // Euler angles const scale = objectTransform.scale3D // Scale of the object gizmoChanged(translation, rotation, scale, type) root.focusGizmoPriority = false } components: [gizmoDisplayTransform, mouseHandler, frontLayerComponent] /***** ENUMS *****/ enum Axis { X, Y, Z } enum Type { TRANSLATION, ROTATION, SCALE, ALL } function convertAxisEnum(axis) { switch(axis) { case TransformGizmo.Axis.X: return Qt.vector3d(1,0,0) case TransformGizmo.Axis.Y: return Qt.vector3d(0,1,0) case TransformGizmo.Axis.Z: return Qt.vector3d(0,0,1) } } function convertTypeEnum(type) { switch(type) { case TransformGizmo.Type.TRANSLATION: return "TRANSLATION" case TransformGizmo.Type.ROTATION: return "ROTATION" case TransformGizmo.Type.SCALE: return "SCALE" case TransformGizmo.Type.ALL: return "ALL" } } /***** TRANSFORMATIONS (using local vars) *****/ /** * @brief Translate locally the gizmo and the object. * * @remarks * To make local translation, we need to recompute a new matrix. * Update gizmoDisplayTransform's matrix and all its properties while avoiding the override of translation property. * Update objectTransform in the same time thanks to binding on translation property. * * @param initialModelMatrix object containing position, rotation and scale matrices + rotation quaternion * @param translateVec vector3d used to make the local translation */ function doRelativeTranslation(initialModelMatrix, translateVec) { Transformations3DHelper.relativeLocalTranslate( gizmoDisplayTransform, initialModelMatrix.position, initialModelMatrix.rotation, initialModelMatrix.scale, translateVec ) } /** * @brief Rotate the gizmo and the object around a specific axis. * * @remarks * To make local rotation around an axis, we need to recompute a new matrix from a quaternion. * Update gizmoDisplayTransform's matrix and all its properties while avoiding the override of rotation, rotationX, rotationY and rotationZ properties. * Update objectTransform in the same time thanks to binding on rotation property. * * @param initialModelMatrix object containing position, rotation and scale matrices + rotation quaternion * @param axis vector3d describing the axis to rotate around * @param degree angle of rotation in degrees */ function doRelativeRotation(initialModelMatrix, axis, degree) { Transformations3DHelper.relativeLocalRotate( gizmoDisplayTransform, initialModelMatrix.position, initialModelMatrix.quaternion, initialModelMatrix.scale, axis, degree ) } /** * @brief Scale the object relatively to its current scale. * * @remarks * To change scale of the object, we need to recompute a new matrix to avoid overriding bindings. * Update objectTransform properties only (gizmoDisplayTransform is not affected). * * @param initialModelMatrix object containing position, rotation and scale matrices + rotation quaternion * @param scaleVec vector3d used to make the relative scale */ function doRelativeScale(initialModelMatrix, scaleVec) { Transformations3DHelper.relativeLocalScale( objectTransform, initialModelMatrix.position, initialModelMatrix.rotation, initialModelMatrix.scale, scaleVec ) } /** * @brief Reset the translation of the gizmo and the object. * * @remarks * Update gizmoDisplayTransform's matrix and all its properties while avoiding the override of translation property. * Update objectTransform in the same time thanks to binding on translation property. */ function resetTranslation() { const mat = gizmoDisplayTransform.matrix const newMat = Qt.matrix4x4( mat.m11, mat.m12, mat.m13, 0, mat.m21, mat.m22, mat.m23, 0, mat.m31, mat.m32, mat.m33, 0, mat.m41, mat.m42, mat.m43, 1 ) gizmoDisplayTransform.setMatrix(newMat) } /** * @brief Reset the rotation of the gizmo and the object. * * @remarks * Update gizmoDisplayTransform's quaternion while avoiding the override of rotationX, rotationY and rotationZ properties. * Update objectTransform in the same time thanks to binding on rotation property. * Here, we can change the rotation property (but not rotationX, rotationY and rotationZ because they can be used in upper-level bindings). * * @note * We could implement a way of changing the matrix instead of overriding rotation (quaternion) property. */ function resetRotation() { gizmoDisplayTransform.rotation = Qt.quaternion(1,0,0,0) } /** * @brief Reset the scale of the object. * * @remarks * To reset the scale, we make the difference of the current one to 1 and recompute the matrix. * Like this, we kind of apply an inverse scale transformation. * It prevents overriding scale3D property (because it can be used in upper-level binding). */ function resetScale() { const modelMat = Transformations3DHelper.modelMatrixToMatrices(objectTransform.matrix) const scaleDiff = Qt.vector3d( -(objectTransform.scale3D.x - 1), -(objectTransform.scale3D.y - 1), -(objectTransform.scale3D.z - 1) ) doRelativeScale(modelMat, scaleDiff) } /***** DEVICES *****/ MouseDevice { id: mouseSourceDevice } MouseHandler { id: mouseHandler sourceDevice: enabled ? mouseSourceDevice : null property var objectPicker: null property bool enabled: false onPositionChanged: { if (objectPicker && objectPicker.button === Qt.LeftButton) { root.focusGizmoPriority = true // Get the selected axis const pickedAxis = convertAxisEnum(objectPicker.gizmoAxis) // TRANSLATION or SCALE transformation if(objectPicker.gizmoType === TransformGizmo.Type.TRANSLATION || objectPicker.gizmoType === TransformGizmo.Type.SCALE) { // Compute the vector PickedPosition -> CurrentMousePoint const pickedPosition = objectPicker.screenPoint const mouseVector = Qt.vector2d(mouse.x - pickedPosition.x, -(mouse.y - pickedPosition.y)) // Transform the positive picked axis vector from World Coord to Screen Coord const gizmoLocalPointOnAxis = gizmoDisplayTransform.matrix.times(Qt.vector4d(pickedAxis.x, pickedAxis.y, pickedAxis.z, 1)) const gizmoCenterPoint = gizmoDisplayTransform.matrix.times(Qt.vector4d(0, 0, 0, 1)) const screenPoint2D = Transformations3DHelper.pointFromWorldToScreen(gizmoLocalPointOnAxis, camera, windowSize) const screenCenter2D = Transformations3DHelper.pointFromWorldToScreen(gizmoCenterPoint, camera, windowSize) const screenAxisVector = Qt.vector2d(screenPoint2D.x - screenCenter2D.x, -(screenPoint2D.y - screenCenter2D.y)) // Get the cosinus of the angle from the screenAxisVector to the mouseVector // It will be used as a intensity factor const cosAngle = screenAxisVector.dotProduct(mouseVector) / (screenAxisVector.length() * mouseVector.length()) const offset = cosAngle * mouseVector.length() / objectPicker.scaleUnit // Do the transformation if(objectPicker.gizmoType === TransformGizmo.Type.TRANSLATION && offset !== 0) { doRelativeTranslation(objectPicker.modelMatrix, pickedAxis.times(offset)) // Do a translation from the initial Object Model Matrix when we picked the gizmo } else if(objectPicker.gizmoType === TransformGizmo.Type.SCALE && offset !== 0) { if(root.uniformScale) doRelativeScale(objectPicker.modelMatrix, Qt.vector3d(1,1,1).times(offset)) // Do a uniform scale from the initial Object Model Matrix when we picked the gizmo else doRelativeScale(objectPicker.modelMatrix, pickedAxis.times(offset)) // Do a scale on one axis from the initial Object Model Matrix when we picked the gizmo } return } // ROTATION transformation else if(objectPicker.gizmoType === TransformGizmo.Type.ROTATION) { // Get Screen Coordinates of the gizmo center const gizmoCenterPoint = gizmoDisplayTransform.matrix.times(Qt.vector4d(0, 0, 0, 1)) const screenCenter2D = Transformations3DHelper.pointFromWorldToScreen(gizmoCenterPoint, camera, root.windowSize) // Get the vector screenCenter2D -> PickedPosition const originalVector = Qt.vector2d(objectPicker.screenPoint.x - screenCenter2D.x, -(objectPicker.screenPoint.y - screenCenter2D.y)) // Compute the vector screenCenter2D -> CurrentMousePoint const mouseVector = Qt.vector2d(mouse.x - screenCenter2D.x, -(mouse.y - screenCenter2D.y)) // Get the angle from the originalVector to the mouseVector const angle = Math.atan2(-originalVector.y*mouseVector.x + originalVector.x*mouseVector.y, originalVector.x*mouseVector.x + originalVector.y*mouseVector.y) * 180 / Math.PI // Get the orientation of the gizmo in function of the camera const gizmoLocalAxisVector = gizmoDisplayTransform.matrix.times(Qt.vector4d(pickedAxis.x, pickedAxis.y, pickedAxis.z, 0)) const gizmoToCameraVector = camera.position.toVector4d().minus(gizmoCenterPoint) const orientation = gizmoLocalAxisVector.dotProduct(gizmoToCameraVector) > 0 ? 1 : -1 if (angle !== 0) doRelativeRotation(objectPicker.modelMatrix, pickedAxis, angle*orientation) // Do a rotation from the initial Object Model Matrix when we picked the gizmo return } } if(objectPicker && objectPicker.button === Qt.RightButton) { resetMenu.popup(window) } } onReleased: { if(objectPicker && mouse.button === Qt.LeftButton) { const type = objectPicker.gizmoType objectPicker = null // To prevent going again in the onPositionChanged emitGizmoChanged(type) } } } Menu { id: resetMenu MenuItem { text: "Reset Translation" onTriggered: { resetTranslation() emitGizmoChanged(TransformGizmo.Type.TRANSLATION) } } MenuItem { text: "Reset Rotation" onTriggered: { resetRotation() emitGizmoChanged(TransformGizmo.Type.ROTATION) } } MenuItem { text: "Reset Scale" onTriggered: { resetScale() emitGizmoChanged(TransformGizmo.Type.SCALE) } } MenuItem { text: "Reset All" onTriggered: { resetTranslation() resetRotation() resetScale() emitGizmoChanged(TransformGizmo.Type.ALL) } } MenuItem { text: "Gizmo Scale Look" Slider { id: gizmoScaleLookSlider anchors.right: parent.right anchors.rightMargin: 10 height: parent.height width: parent.width * 0.40 from: 0.06 to: 0.30 stepSize: 0.01 value: 0.15 } } } /***** GIZMO'S BASIC COMPONENTS *****/ Entity { id: centerSphereEntity components: [centerSphereMesh, centerSphereMaterial, frontLayerComponent] SphereMesh { id: centerSphereMesh radius: 0.04 rings: 8 slices: 8 } PhongMaterial { id: centerSphereMaterial property color base: "white" ambient: base shininess: 0.2 } } // AXIS GIZMO INSTANTIATOR => X, Y and Z NodeInstantiator { model: 3 Entity { id: axisContainer property int axis : { switch(index) { case 0: return TransformGizmo.Axis.X case 1: return TransformGizmo.Axis.Y case 2: return TransformGizmo.Axis.Z } } property color baseColor: { switch(axis) { case TransformGizmo.Axis.X: return "#e63b55" // Red case TransformGizmo.Axis.Y: return "#83c414" // Green case TransformGizmo.Axis.Z: return "#3387e2" // Blue } } property real lineRadius: 0.011 // SCALE ENTITY Entity { id: scaleEntity Entity { id: axisCylinder components: [cylinderMesh, cylinderTransform, scaleMaterial, frontLayerComponent] CylinderMesh { id: cylinderMesh length: 0.5 radius: axisContainer.lineRadius rings: 2 slices: 16 } Transform { id: cylinderTransform matrix: { const offset = cylinderMesh.length/2 + centerSphereMesh.radius const m = Qt.matrix4x4() switch(axis) { case TransformGizmo.Axis.X: { m.translate(Qt.vector3d(offset, 0, 0)) m.rotate(90, Qt.vector3d(0,0,1)) break } case TransformGizmo.Axis.Y: { m.translate(Qt.vector3d(0, offset, 0)) break } case TransformGizmo.Axis.Z: { m.translate(Qt.vector3d(0, 0, offset)) m.rotate(90, Qt.vector3d(1,0,0)) break } } return m } } } Entity { id: axisScaleBox components: [cubeScaleMesh, cubeScaleTransform, scaleMaterial, scalePicker, frontLayerComponent] CuboidMesh { id: cubeScaleMesh property real edge: 0.06 xExtent: edge yExtent: edge zExtent: edge } Transform { id: cubeScaleTransform matrix: { const offset = cylinderMesh.length + centerSphereMesh.radius const m = Qt.matrix4x4() switch(axis) { case TransformGizmo.Axis.X: { m.translate(Qt.vector3d(offset, 0, 0)) m.rotate(90, Qt.vector3d(0,0,1)) break } case TransformGizmo.Axis.Y: { m.translate(Qt.vector3d(0, offset, 0)) break } case TransformGizmo.Axis.Z: { m.translate(Qt.vector3d(0, 0, offset)) m.rotate(90, Qt.vector3d(1,0,0)) break } } return m } } } PhongMaterial { id: scaleMaterial ambient: baseColor } TransformGizmoPicker { id: scalePicker mouseController: mouseHandler gizmoMaterial: scaleMaterial gizmoBaseColor: baseColor gizmoAxis: axis gizmoType: TransformGizmo.Type.SCALE onPickedChanged: { // Save the current transformations of the OBJECT this.modelMatrix = Transformations3DHelper.modelMatrixToMatrices(objectTransform.matrix) // Compute a scale unit at picking time this.scaleUnit = Transformations3DHelper.computeScaleUnitFromModelMatrix(convertAxisEnum(gizmoAxis), gizmoDisplayTransform.matrix, camera, root.windowSize) // Prevent camera transformations root.pickedChanged(picker.isPressed) } } } // TRANSLATION ENTITY Entity { id: positionEntity components: [coneMesh, coneTransform, positionMaterial, positionPicker, frontLayerComponent] ConeMesh { id: coneMesh bottomRadius : 0.035 topRadius : 0.001 hasBottomEndcap : true hasTopEndcap : true length : 0.13 rings : 2 slices : 8 } Transform { id: coneTransform matrix: { const offset = cylinderMesh.length + centerSphereMesh.radius + 0.4 const m = Qt.matrix4x4() switch(axis) { case TransformGizmo.Axis.X: { m.translate(Qt.vector3d(offset, 0, 0)) m.rotate(-90, Qt.vector3d(0,0,1)) break } case TransformGizmo.Axis.Y: { m.translate(Qt.vector3d(0, offset, 0)) break } case TransformGizmo.Axis.Z: { m.translate(Qt.vector3d(0, 0, offset)) m.rotate(90, Qt.vector3d(1,0,0)) break } } return m } } PhongMaterial { id: positionMaterial ambient: baseColor } TransformGizmoPicker { id: positionPicker mouseController: mouseHandler gizmoMaterial: positionMaterial gizmoBaseColor: baseColor gizmoAxis: axis gizmoType: TransformGizmo.Type.TRANSLATION onPickedChanged: { // Save the current transformations of the OBJECT this.modelMatrix = Transformations3DHelper.modelMatrixToMatrices(objectTransform.matrix) // Compute a scale unit at picking time this.scaleUnit = Transformations3DHelper.computeScaleUnitFromModelMatrix(convertAxisEnum(gizmoAxis), gizmoDisplayTransform.matrix, camera, root.windowSize) // Prevent camera transformations root.pickedChanged(picker.isPressed) } } } // ROTATION ENTITY Entity { id: rotationEntity components: [torusMesh, torusTransform, rotationMaterial, rotationPicker, frontLayerComponent] TorusMesh { id: torusMesh radius: cylinderMesh.length + 0.25 minorRadius: axisContainer.lineRadius slices: 8 rings: 32 } Transform { id: torusTransform matrix: { const scaleDiff = 2*torusMesh.minorRadius + 0.01 // Just to make sure there is no face overlapping const m = Qt.matrix4x4() switch(axis) { case TransformGizmo.Axis.X: m.rotate(90, Qt.vector3d(0,1,0)); break case TransformGizmo.Axis.Y: m.rotate(90, Qt.vector3d(1,0,0)); m.scale(Qt.vector3d(1-scaleDiff, 1-scaleDiff, 1-scaleDiff)); break case TransformGizmo.Axis.Z: m.scale(Qt.vector3d(1-2*scaleDiff, 1-2*scaleDiff, 1-2*scaleDiff)); break } return m } } PhongMaterial { id: rotationMaterial ambient: baseColor } TransformGizmoPicker { id: rotationPicker mouseController: mouseHandler gizmoMaterial: rotationMaterial gizmoBaseColor: baseColor gizmoAxis: axis gizmoType: TransformGizmo.Type.ROTATION onPickedChanged: { // Save the current transformations of the OBJECT this.modelMatrix = Transformations3DHelper.modelMatrixToMatrices(objectTransform.matrix) // No need to compute a scale unit for rotation // Prevent camera transformations root.pickedChanged(picker.isPressed) } } } } } }