diff --git a/examples/src/examples/gizmos/transform-rotate.controls.mjs b/examples/src/examples/gizmos/transform-rotate.controls.mjs index 77346ef3d2c..03adf7fca1e 100644 --- a/examples/src/examples/gizmos/transform-rotate.controls.mjs +++ b/examples/src/examples/gizmos/transform-rotate.controls.mjs @@ -46,6 +46,15 @@ export const controls = ({ observer, ReactPCUI, React, jsx, fragment }) => { max: 10, precision: 0 }) + ), + jsx( + LabelGroup, + { text: 'Orbit Rotation' }, + jsx(BooleanInput, { + type: 'toggle', + binding: new BindingTwoWay(), + link: { observer, path: 'gizmo.orbitRotation' } + }) ) ), jsx( diff --git a/examples/src/examples/gizmos/transform-rotate.example.mjs b/examples/src/examples/gizmos/transform-rotate.example.mjs index 47e37a6897d..4d73936edd9 100644 --- a/examples/src/examples/gizmos/transform-rotate.example.mjs +++ b/examples/src/examples/gizmos/transform-rotate.example.mjs @@ -97,6 +97,7 @@ gizmo.attach(box); data.set('gizmo', { size: gizmo.size, snapIncrement: gizmo.snapIncrement, + orbitRotation: gizmo.orbitRotation, xAxisColor: Object.values(gizmo.xAxisColor), yAxisColor: Object.values(gizmo.yAxisColor), zAxisColor: Object.values(gizmo.zAxisColor), diff --git a/src/extras/gizmo/rotate-gizmo.js b/src/extras/gizmo/rotate-gizmo.js index d78a1999b9c..a74c45e3df4 100644 --- a/src/extras/gizmo/rotate-gizmo.js +++ b/src/extras/gizmo/rotate-gizmo.js @@ -73,6 +73,14 @@ class RotateGizmo extends TransformGizmo { }) }; + /** + * Internal selection starting angle in world space. + * + * @type {number} + * @private + */ + _selectionStartAngle = 0; + /** * Internal mapping from each attached node to their starting rotation in local space. * @@ -126,6 +134,13 @@ class RotateGizmo extends TransformGizmo { */ snapIncrement = 5; + /** + * This forces the rotation to always be calculated based on the mouse position around the gizmo. + * + * @type {boolean} + */ + orbitRotation = false; + /** * Creates a new RotateGizmo object. * @@ -139,7 +154,11 @@ class RotateGizmo extends TransformGizmo { this._createTransform(); - this.on(TransformGizmo.EVENT_TRANSFORMSTART, () => { + this.on(TransformGizmo.EVENT_TRANSFORMSTART, (point, x, y) => { + // store start angle + this._selectionStartAngle = this._calculateAngle(point, x, y); + + // store initial node rotations this._storeNodeRotations(); // store guide points @@ -149,9 +168,10 @@ class RotateGizmo extends TransformGizmo { this._drag(true); }); - this.on(TransformGizmo.EVENT_TRANSFORMMOVE, (pointDelta, angleDelta) => { + this.on(TransformGizmo.EVENT_TRANSFORMMOVE, (point, x, y) => { const axis = this._selectedAxis; + let angleDelta = this._calculateAngle(point, x, y) - this._selectionStartAngle; if (this.snap) { angleDelta = Math.round(angleDelta / this.snapIncrement) * this.snapIncrement; } @@ -406,17 +426,12 @@ class RotateGizmo extends TransformGizmo { _setNodeRotations(axis, angleDelta) { const gizmoPos = this.root.getPosition(); const isFacing = axis === GIZMOAXIS_FACE; - for (let i = 0; i < this.nodes.length; i++) { - const node = this.nodes[i]; - if (isFacing) { - tmpV1.copy(this._camera.entity.forward).mulScalar(-1); - } else { - tmpV1.set(0, 0, 0); - tmpV1[axis] = 1; - } - tmpQ1.setFromAxisAngle(tmpV1, angleDelta); + // calculate rotation from axis and angle + tmpQ1.setFromAxisAngle(this._dirFromAxis(axis, tmpV1), angleDelta); + for (let i = 0; i < this.nodes.length; i++) { + const node = this.nodes[i]; if (!isFacing && this._coordSpace === GIZMOSPACE_LOCAL) { const rot = this._nodeLocalRotations.get(node); if (!rot) { @@ -451,11 +466,10 @@ class RotateGizmo extends TransformGizmo { /** * @param {number} x - The x coordinate. * @param {number} y - The y coordinate. - * @returns {{ point: Vec3, angle: number }} The point and angle. + * @returns {Vec3} The point in world space. * @protected */ _screenToPoint(x, y) { - const gizmoPos = this.root.getPosition(); const mouseWPos = this._camera.screenToWorld(x, y, 1); const axis = this._selectedAxis; @@ -464,14 +478,32 @@ class RotateGizmo extends TransformGizmo { const plane = this._createPlane(axis, axis === GIZMOAXIS_FACE, false); const point = new Vec3(); - let angle = 0; plane.intersectsRay(ray, point); + return point; + } + + /** + * @param {Vec3} point - The point. + * @param {number} x - The x coordinate. + * @param {number} y - The y coordinate. + * @returns {number} The angle. + * @protected + */ + _calculateAngle(point, x, y) { + const gizmoPos = this.root.getPosition(); + + const axis = this._selectedAxis; + + const plane = this._createPlane(axis, axis === GIZMOAXIS_FACE, false); + + let angle = 0; + // calculate angle const facingDir = tmpV2.copy(this.facing); const facingDot = plane.normal.dot(facingDir); - if (Math.abs(facingDot) > FACING_THRESHOLD) { + if (this.orbitRotation || Math.abs(facingDot) > FACING_THRESHOLD) { // plane facing camera so based on mouse position around gizmo tmpV1.sub2(point, gizmoPos); @@ -484,14 +516,18 @@ class RotateGizmo extends TransformGizmo { // convert rotation axis to screen space tmpV1.copy(gizmoPos); tmpV2.cross(plane.normal, facingDir).normalize().add(gizmoPos); + + // convert world space vectors to screen space this._camera.worldToScreen(tmpV1, tmpV3); this._camera.worldToScreen(tmpV2, tmpV4); + + // angle is dot product with mouse position tmpV1.sub2(tmpV4, tmpV3).normalize(); tmpV2.set(x, y, 0); angle = tmpV1.dot(tmpV2); } - return { point, angle }; + return angle; } } diff --git a/src/extras/gizmo/scale-gizmo.js b/src/extras/gizmo/scale-gizmo.js index 2256ba50e41..7753f33331f 100644 --- a/src/extras/gizmo/scale-gizmo.js +++ b/src/extras/gizmo/scale-gizmo.js @@ -16,6 +16,7 @@ import { BoxLineShape } from './shape/boxline-shape.js'; // temporary variables const tmpV1 = new Vec3(); const tmpV2 = new Vec3(); +const tmpV3 = new Vec3(); const tmpQ1 = new Quat(); // constants @@ -139,7 +140,8 @@ class ScaleGizmo extends TransformGizmo { this._storeNodeScales(); }); - this.on(TransformGizmo.EVENT_TRANSFORMMOVE, (pointDelta) => { + this.on(TransformGizmo.EVENT_TRANSFORMMOVE, (point) => { + const pointDelta = tmpV3.copy(point).sub(this._selectionStartPoint); if (this.snap) { pointDelta.mulScalar(1 / this.snapIncrement); pointDelta.round(); @@ -437,7 +439,7 @@ class ScaleGizmo extends TransformGizmo { /** * @param {number} x - The x coordinate. * @param {number} y - The y coordinate. - * @returns {{ point: Vec3, angle: number }} The point and angle. + * @returns {Vec3} The point in world space. * @protected */ _screenToPoint(x, y) { @@ -453,7 +455,6 @@ class ScaleGizmo extends TransformGizmo { const plane = this._createPlane(axis, isScaleUniform, !isPlane); const point = new Vec3(); - const angle = 0; plane.intersectsRay(ray, point); @@ -489,7 +490,7 @@ class ScaleGizmo extends TransformGizmo { point[axis] = 1; } - return { point, angle }; + return point; } // rotate point back to world coords @@ -499,7 +500,7 @@ class ScaleGizmo extends TransformGizmo { this._projectToAxis(point, axis); } - return { point, angle }; + return point; } } diff --git a/src/extras/gizmo/transform-gizmo.js b/src/extras/gizmo/transform-gizmo.js index 633b605a0b5..eb29dae37de 100644 --- a/src/extras/gizmo/transform-gizmo.js +++ b/src/extras/gizmo/transform-gizmo.js @@ -15,7 +15,7 @@ import { color3from4, color4from3 } from './color.js'; -import { GIZMOAXIS_X, GIZMOAXIS_XYZ, GIZMOAXIS_Y, GIZMOAXIS_Z } from './constants.js'; +import { GIZMOAXIS_FACE, GIZMOAXIS_X, GIZMOAXIS_XYZ, GIZMOAXIS_Y, GIZMOAXIS_Z } from './constants.js'; import { Gizmo } from './gizmo.js'; /** @@ -122,14 +122,6 @@ class TransformGizmo extends Gizmo { f: COLOR_YELLOW.clone() }; - /** - * Internal point delta. - * - * @type {Vec3} - * @private - */ - _pointDelta = new Vec3(); - /** * Internal gizmo starting rotation in world space. * @@ -226,14 +218,6 @@ class TransformGizmo extends Gizmo { */ _selectionStartPoint = new Vec3(); - /** - * Internal selection starting angle in world space. - * - * @type {number} - * @protected - */ - _selectionStartAngle = 0; - /** * Internal state for if the gizmo is being dragged. * @@ -294,11 +278,10 @@ class TransformGizmo extends Gizmo { this._selectedIsPlane = this._getIsPlane(meshInstance); this._rootStartPos.copy(this.root.getPosition()); this._rootStartRot.copy(this.root.getRotation()); - const pointInfo = this._screenToPoint(x, y); - this._selectionStartPoint.copy(pointInfo.point); - this._selectionStartAngle = pointInfo.angle; + const point = this._screenToPoint(x, y); + this._selectionStartPoint.copy(point); this._dragging = true; - this.fire(TransformGizmo.EVENT_TRANSFORMSTART); + this.fire(TransformGizmo.EVENT_TRANSFORMSTART, point, x, y); }); this.on(Gizmo.EVENT_POINTERMOVE, (x, y, meshInstance) => { @@ -315,10 +298,8 @@ class TransformGizmo extends Gizmo { return; } - const pointInfo = this._screenToPoint(x, y); - this._pointDelta.copy(pointInfo.point).sub(this._selectionStartPoint); - const angleDelta = pointInfo.angle - this._selectionStartAngle; - this.fire(TransformGizmo.EVENT_TRANSFORMMOVE, this._pointDelta, angleDelta); + const point = this._screenToPoint(x, y); + this.fire(TransformGizmo.EVENT_TRANSFORMMOVE, point, x, y); this._hoverAxis = ''; this._hoverIsPlane = false; @@ -591,6 +572,22 @@ class TransformGizmo extends Gizmo { return tmpP1.setFromPointNormal(this._rootStartPos, normal); } + /** + * @param {string} axis - The axis + * @param {Vec3} dir - The direction + * @returns {Vec3} - The direction + * @protected + */ + _dirFromAxis(axis, dir) { + if (axis === GIZMOAXIS_FACE) { + dir.copy(this._camera.entity.forward).mulScalar(-1); + } else { + dir.set(0, 0, 0); + dir[axis] = 1; + } + return dir; + } + /** * @param {Vec3} point - The point to project. * @param {string} axis - The axis to project to. @@ -613,7 +610,7 @@ class TransformGizmo extends Gizmo { * @param {number} y - The y coordinate. * @param {boolean} isFacing - Whether the axis is facing the camera. * @param {boolean} isLine - Whether the axis is a line. - * @returns {{ point: Vec3, angle: number }} - The point and angle. + * @returns {Vec3} - The point. * @protected */ _screenToPoint(x, y, isFacing = false, isLine = false) { @@ -625,10 +622,9 @@ class TransformGizmo extends Gizmo { const plane = this._createPlane(axis, isFacing, isLine); const point = new Vec3(); - const angle = 0; plane.intersectsRay(ray, point); - return { point, angle }; + return point; } /** diff --git a/src/extras/gizmo/translate-gizmo.js b/src/extras/gizmo/translate-gizmo.js index 7bab93e4bb2..8169800f45a 100644 --- a/src/extras/gizmo/translate-gizmo.js +++ b/src/extras/gizmo/translate-gizmo.js @@ -22,6 +22,7 @@ import { SphereShape } from './shape/sphere-shape.js'; // temporary variables const tmpV1 = new Vec3(); const tmpV2 = new Vec3(); +const tmpV3 = new Vec3(); const tmpQ1 = new Quat(); // constants @@ -136,7 +137,8 @@ class TranslateGizmo extends TransformGizmo { this._storeNodePositions(); }); - this.on(TransformGizmo.EVENT_TRANSFORMMOVE, (pointDelta) => { + this.on(TransformGizmo.EVENT_TRANSFORMMOVE, (point) => { + const pointDelta = tmpV3.copy(point).sub(this._selectionStartPoint); if (this.snap) { pointDelta.mulScalar(1 / this.snapIncrement); pointDelta.round(); @@ -445,7 +447,7 @@ class TranslateGizmo extends TransformGizmo { /** * @param {number} x - The x coordinate. * @param {number} y - The y coordinate. - * @returns {{ point: Vec3, angle: number }} The point and angle. + * @returns {Vec3} The point in world space. * @protected */ _screenToPoint(x, y) { @@ -458,7 +460,6 @@ class TranslateGizmo extends TransformGizmo { const plane = this._createPlane(axis, axis === GIZMOAXIS_FACE, !isPlane); const point = new Vec3(); - const angle = 0; plane.intersectsRay(ray, point); @@ -469,7 +470,7 @@ class TranslateGizmo extends TransformGizmo { this._projectToAxis(point, axis); } - return { point, angle }; + return point; } }