diff --git a/examples/src/examples/gizmos/transform-rotate.controls.mjs b/examples/src/examples/gizmos/transform-rotate.controls.mjs index 3f0d4a0e362..42c33d64b94 100644 --- a/examples/src/examples/gizmos/transform-rotate.controls.mjs +++ b/examples/src/examples/gizmos/transform-rotate.controls.mjs @@ -280,6 +280,14 @@ export const controls = ({ observer, ReactPCUI, React, jsx, fragment }) => { max: 2 }) ), + jsx( + LabelGroup, + { text: 'Center Radius' }, + jsx(SliderInput, { + binding: new BindingTwoWay(), + link: { observer, path: 'gizmo.centerRadius' } + }) + ), jsx( LabelGroup, { text: 'Angle Guide Thickness' }, diff --git a/examples/src/examples/gizmos/transform-rotate.example.mjs b/examples/src/examples/gizmos/transform-rotate.example.mjs index 7945544587d..a9dbc908a3f 100644 --- a/examples/src/examples/gizmos/transform-rotate.example.mjs +++ b/examples/src/examples/gizmos/transform-rotate.example.mjs @@ -151,6 +151,7 @@ data.set('gizmo', { xyzRingRadius: gizmo.xyzRingRadius, faceTubeRadius: gizmo.faceTubeRadius, faceRingRadius: gizmo.faceRingRadius, + centerRadius: gizmo.centerRadius, angleGuideThickness: gizmo.angleGuideThickness }); diff --git a/src/extras/gizmo/gizmo.js b/src/extras/gizmo/gizmo.js index d89426b3048..e9b35fff8b0 100644 --- a/src/extras/gizmo/gizmo.js +++ b/src/extras/gizmo/gizmo.js @@ -506,7 +506,7 @@ class Gizmo extends EventHandler { _updateRotation() { tmpV1.set(0, 0, 0); if (this._coordSpace === 'local' && this.nodes.length !== 0) { - tmpV1.copy(this.nodes[this.nodes.length - 1].getLocalEulerAngles()); + tmpV1.copy(this.nodes[this.nodes.length - 1].getEulerAngles()); } if (tmpV1.distance(this.root.getLocalEulerAngles()) < UPDATE_EPSILON) { diff --git a/src/extras/gizmo/rotate-gizmo.js b/src/extras/gizmo/rotate-gizmo.js index 3531f424382..f6778f48188 100644 --- a/src/extras/gizmo/rotate-gizmo.js +++ b/src/extras/gizmo/rotate-gizmo.js @@ -1,13 +1,14 @@ import { math } from '../../core/math/math.js'; import { Color } from '../../core/math/color.js'; import { Quat } from '../../core/math/quat.js'; -import { Mat4 } from '../../core/math/mat4.js'; +import { Vec2 } from '../../core/math/vec2.js'; import { Vec3 } from '../../core/math/vec3.js'; import { PROJECTION_PERSPECTIVE } from '../../scene/constants.js'; import { ArcShape } from './shape/arc-shape.js'; import { TransformGizmo } from './transform-gizmo.js'; import { MeshLine } from './mesh-line.js'; +import { SphereShape } from './shape/sphere-shape.js'; /** * @import { CameraComponent } from '../../framework/components/camera/component.js' @@ -17,17 +18,19 @@ import { MeshLine } from './mesh-line.js'; */ // temporary variables +const tmpVa = new Vec2(); const tmpV1 = new Vec3(); const tmpV2 = new Vec3(); const tmpV3 = new Vec3(); const tmpV4 = new Vec3(); -const tmpM1 = new Mat4(); const tmpQ1 = new Quat(); const tmpQ2 = new Quat(); const tmpC1 = new Color(); // constants -const FACING_THRESHOLD = 0.9; +const ROTATE_FACING_EPSILON = 0.1; +const RING_FACING_EPSILON = 1e-4; +const AXES = /** @type {('x' | 'y' | 'z')[]} */ (['x', 'y', 'z']); /** * The RotateGizmo provides interactive 3D manipulation handles for rotating/reorienting @@ -71,6 +74,7 @@ class RotateGizmo extends TransformGizmo { rotation: new Vec3(90, 0, 90), defaultColor: this._theme.shapeBase.z, hoverColor: this._theme.shapeHover.z, + disabledColor: this._theme.disabled, sectorAngle: 180 }), x: new ArcShape(this._device, { @@ -79,6 +83,7 @@ class RotateGizmo extends TransformGizmo { rotation: new Vec3(0, 0, -90), defaultColor: this._theme.shapeBase.x, hoverColor: this._theme.shapeHover.x, + disabledColor: this._theme.disabled, sectorAngle: 180 }), y: new ArcShape(this._device, { @@ -87,15 +92,24 @@ class RotateGizmo extends TransformGizmo { rotation: new Vec3(0, 0, 0), defaultColor: this._theme.shapeBase.y, hoverColor: this._theme.shapeHover.y, + disabledColor: this._theme.disabled, sectorAngle: 180 }), f: new ArcShape(this._device, { axis: 'f', layers: [this._layer.id], - rotation: this._getLookAtEulerAngles(this._camera.entity.getPosition()), defaultColor: this._theme.shapeBase.f, hoverColor: this._theme.shapeHover.f, + disabledColor: this._theme.disabled, ringRadius: 0.55 + }), + xyz: new SphereShape(this._device, { + axis: 'xyz', + layers: [this._layer.id], + defaultColor: this._theme.shapeBase.xyz, + hoverColor: this._theme.shapeHover.xyz, + disabledColor: this._theme.disabled, + radius: 0.5 }) }; @@ -107,6 +121,14 @@ class RotateGizmo extends TransformGizmo { */ _selectionStartAngle = 0; + /** + * Internal selection screen point in 2D space. + * + * @type {Vec2} + * @private + */ + _selectionScreenPoint = new Vec2(); + /** * Internal mapping from each attached node to their starting rotation in local space. * @@ -179,6 +201,15 @@ class RotateGizmo extends TransformGizmo { constructor(camera, layer) { super(camera, layer, 'gizmo:rotate'); + this.setTheme({ + shapeBase: { + xyz: new Color(0, 0, 0, 0) + }, + shapeHover: { + xyz: new Color(1, 1, 1, 0.2) + } + }); + this._createTransform(); this._guideAngleLines = [ @@ -191,8 +222,11 @@ class RotateGizmo extends TransformGizmo { }); this.on(TransformGizmo.EVENT_TRANSFORMSTART, (point, x, y) => { + // store start screen point + this._selectionScreenPoint.set(x, y); + // store start angle - this._selectionStartAngle = this._calculateAngle(point, x, y); + this._selectionStartAngle = this._calculateArcAngle(point, x, y); // store initial node rotations this._storeNodeRotations(); @@ -213,24 +247,40 @@ class RotateGizmo extends TransformGizmo { return; } - let angleDelta = this._calculateAngle(point, x, y) - this._selectionStartAngle; - if (this.snap) { - angleDelta = Math.round(angleDelta / this.snapIncrement) * this.snapIncrement; - } - this._setNodeRotations(axis, angleDelta); + if (axis === 'xyz') { + // calculate angle axis and delta and update node rotations + const facingDir = tmpV1.copy(this.facingDir); + const delta = tmpV2.copy(point).sub(this._selectionStartPoint); + const angleAxis = tmpV1.cross(facingDir, delta).normalize(); + + const angleDelta = tmpVa.set(x, y).distance(this._selectionScreenPoint); + this._setNodeRotations(axis, angleAxis, angleDelta); + } else { + // calculate angle axis and delta and update node rotations + let angleDelta = this._calculateArcAngle(point, x, y) - this._selectionStartAngle; + if (this.snap) { + angleDelta = Math.round(angleDelta / this.snapIncrement) * this.snapIncrement; + } + const angleAxis = this._dirFromAxis(axis, tmpV1); + this._setNodeRotations(axis, angleAxis, angleDelta); - this._updateGuidePoints(angleDelta); + // update guide points and show angle guide + this._updateGuidePoints(angleDelta); + this._angleGuide(true); + } - this._angleGuide(true); }); this.on(TransformGizmo.EVENT_TRANSFORMEND, () => { + // show all shapes this._drag(false); + // hide angle guide this._angleGuide(false); }); this.on(TransformGizmo.EVENT_NODESDETACH, () => { + // reset stored rotations and offsets this._nodeLocalRotations.clear(); this._nodeRotations.clear(); this._nodeOffsets.clear(); @@ -309,6 +359,24 @@ class RotateGizmo extends TransformGizmo { return this._shapes.f.ringRadius; } + /** + * Sets the center radius. + * + * @type {number} + */ + set centerRadius(value) { + this._shapes.xyz.radius = value; + } + + /** + * Gets the center radius. + * + * @type {number} + */ + get centerRadius() { + return this._shapes.xyz.radius; + } + /** * Sets the ring tolerance. * @@ -396,9 +464,11 @@ class RotateGizmo extends TransformGizmo { * @private */ _angleGuide(state) { - if (state && this.dragMode !== 'show') { + const axis = this._selectedAxis; + + if (state && this.dragMode !== 'show' && axis !== 'xyz') { const gizmoPos = this.root.getLocalPosition(); - const color = this._theme.guideBase[this._selectedAxis]; + const color = this._theme.guideBase[axis]; const startColor = tmpC1.copy(color); startColor.a *= 0.3; this._guideAngleLines[0].draw(gizmoPos, tmpV1.copy(this._guideAngleStart).add(gizmoPos), @@ -413,28 +483,16 @@ class RotateGizmo extends TransformGizmo { } } - /** - * @param {Vec3} position - The position. - * @returns {Vec3} The look at euler angles. - * @private - */ - _getLookAtEulerAngles(position) { - tmpV1.set(0, 0, 0); - tmpM1.setLookAt(tmpV1, position, Vec3.UP); - tmpQ1.setFromMat4(tmpM1); - tmpQ1.getEulerAngles(tmpV1); - tmpV1.x += 90; - return tmpV1; - } - /** * @private */ _shapesLookAtCamera() { // face shape if (this._camera.projection === PROJECTION_PERSPECTIVE) { - this._shapes.f.entity.lookAt(this._camera.entity.getPosition()); - this._shapes.f.entity.rotateLocal(90, 0, 0); + const dir = this._camera.entity.getPosition().sub(this.root.getPosition()).normalize(); + const elev = Math.atan2(-dir.y, Math.sqrt(dir.x * dir.x + dir.z * dir.z)) * math.RAD_TO_DEG; + const azim = Math.atan2(-dir.x, -dir.z) * math.RAD_TO_DEG; + this._shapes.f.entity.setEulerAngles(-elev + 90, azim, 0); } else { tmpQ1.copy(this._camera.entity.getRotation()).getEulerAngles(tmpV1); this._shapes.f.entity.setEulerAngles(tmpV1); @@ -442,14 +500,29 @@ class RotateGizmo extends TransformGizmo { } // axes shapes + let angle, dot, sector; const facingDir = tmpV1.copy(this.facingDir); tmpQ1.copy(this.root.getRotation()).invert().transformVector(facingDir, facingDir); - let angle = Math.atan2(facingDir.z, facingDir.y) * math.RAD_TO_DEG; + angle = Math.atan2(facingDir.z, facingDir.y) * math.RAD_TO_DEG; this._shapes.x.entity.setLocalEulerAngles(0, angle - 90, -90); angle = Math.atan2(facingDir.x, facingDir.z) * math.RAD_TO_DEG; this._shapes.y.entity.setLocalEulerAngles(0, angle, 0); angle = Math.atan2(facingDir.y, facingDir.x) * math.RAD_TO_DEG; this._shapes.z.entity.setLocalEulerAngles(90, 0, angle + 90); + + if (!this._dragging) { + dot = facingDir.dot(this.root.right); + sector = 1 - Math.abs(dot) > RING_FACING_EPSILON; + this._shapes.x.show(sector ? 'sector' : 'ring'); + dot = facingDir.dot(this.root.up); + sector = 1 - Math.abs(dot) > RING_FACING_EPSILON; + this._shapes.y.show(sector ? 'sector' : 'ring'); + dot = facingDir.dot(this.root.forward); + sector = 1 - Math.abs(dot) > RING_FACING_EPSILON; + this._shapes.z.show(sector ? 'sector' : 'ring'); + + this.fire(TransformGizmo.EVENT_RENDERUPDATE); + } } /** @@ -459,26 +532,19 @@ class RotateGizmo extends TransformGizmo { _drag(state) { for (const axis in this._shapes) { const shape = this._shapes[axis]; + if (!(shape instanceof ArcShape)) { + continue; + } switch (this.dragMode) { case 'show': { break; } case 'hide': { - if (axis === this._selectedAxis) { - shape.drag(state); - } else { - shape.hide(state); - } + shape.show(state ? axis === this._selectedAxis ? 'ring' : 'none' : 'sector'); continue; } case 'selected': { - if (axis === this._selectedAxis) { - shape.drag(state); - } else { - if (!state) { - shape.hide(state); - } - } + shape.show(state ? axis === this._selectedAxis ? 'ring' : 'sector' : 'sector'); break; } } @@ -501,19 +567,20 @@ class RotateGizmo extends TransformGizmo { /** * @param {GizmoAxis} axis - The axis. + * @param {Vec3} angleAxis - The angle axis. * @param {number} angleDelta - The angle delta. * @private */ - _setNodeRotations(axis, angleDelta) { + _setNodeRotations(axis, angleAxis, angleDelta) { const gizmoPos = this.root.getLocalPosition(); - const isFacing = axis === 'f'; // calculate rotation from axis and angle - tmpQ1.setFromAxisAngle(this._dirFromAxis(axis, tmpV1), angleDelta); + tmpQ1.setFromAxisAngle(angleAxis, angleDelta); for (let i = 0; i < this.nodes.length; i++) { const node = this.nodes[i]; - if (!isFacing && this._coordSpace === 'local') { + + if ((axis === 'x' || axis === 'y' || axis === 'z') && this._coordSpace === 'local') { const rot = this._nodeLocalRotations.get(node); if (!rot) { continue; @@ -533,7 +600,7 @@ class RotateGizmo extends TransformGizmo { tmpQ1.transformVector(tmpV1, tmpV1); tmpQ2.copy(tmpQ1).mul(rot); - // N.B. Rotation via quaternion when scale inverted causes scale warping? + // FIXME: Rotation via quaternion when scale inverted causes scale warping? node.setEulerAngles(tmpQ2.getEulerAngles()); node.setPosition(tmpV1.add(gizmoPos)); } @@ -555,10 +622,9 @@ class RotateGizmo extends TransformGizmo { const axis = this._selectedAxis; - const ray = this._createRay(mouseWPos); - const plane = this._createPlane(axis, axis === 'f', false); - const point = new Vec3(); + const ray = this._createRay(mouseWPos); + const plane = this._createPlane(axis, axis === 'f' || axis === 'xyz', false); if (!plane.intersectsRay(ray, point)) { point.copy(this.root.getLocalPosition()); } @@ -573,7 +639,7 @@ class RotateGizmo extends TransformGizmo { * @returns {number} The angle. * @protected */ - _calculateAngle(point, x, y) { + _calculateArcAngle(point, x, y) { const gizmoPos = this.root.getLocalPosition(); const axis = this._selectedAxis; @@ -582,10 +648,10 @@ class RotateGizmo extends TransformGizmo { let angle = 0; - // calculate angle + // arc angle const facingDir = tmpV2.copy(this.facingDir); const facingDot = plane.normal.dot(facingDir); - if (this.orbitRotation || Math.abs(facingDot) > FACING_THRESHOLD) { + if (this.orbitRotation || (1 - Math.abs(facingDot)) < ROTATE_FACING_EPSILON) { // plane facing camera so based on mouse position around gizmo tmpV1.sub2(point, gizmoPos); @@ -612,6 +678,30 @@ class RotateGizmo extends TransformGizmo { return angle; } + /** + * @param {Vec3} pos - The position. + * @param {Quat} rot - The rotation. + * @param {GizmoAxis} activeAxis - The active axis. + * @param {boolean} activeIsPlane - Whether the active axis is a plane. + * @override + */ + _drawGuideLines(pos, rot, activeAxis, activeIsPlane) { + for (const axis of AXES) { + if (activeAxis === 'xyz') { + continue; + } + if (activeIsPlane) { + if (axis !== activeAxis) { + this._drawSpanLine(pos, rot, axis); + } + } else { + if (axis === activeAxis) { + this._drawSpanLine(pos, rot, axis); + } + } + } + } + /** * @override */ diff --git a/src/extras/gizmo/scale-gizmo.js b/src/extras/gizmo/scale-gizmo.js index b0e0e7b28d4..5c8f15bb83d 100644 --- a/src/extras/gizmo/scale-gizmo.js +++ b/src/extras/gizmo/scale-gizmo.js @@ -20,7 +20,7 @@ const tmpV3 = new Vec3(); const tmpQ1 = new Quat(); // constants -const GLANCE_EPSILON = 0.98; +const GLANCE_EPSILON = 0.01; /** * The ScaleGizmo provides interactive 3D manipulation handles for scaling/resizing @@ -62,49 +62,56 @@ class ScaleGizmo extends TransformGizmo { axis: 'xyz', layers: [this._layer.id], defaultColor: this._theme.shapeBase.xyz, - hoverColor: this._theme.shapeHover.xyz + hoverColor: this._theme.shapeHover.xyz, + disabledColor: this._theme.disabled }), yz: new PlaneShape(this._device, { axis: 'x', layers: [this._layer.id], rotation: new Vec3(0, 0, -90), defaultColor: this._theme.shapeBase.x, - hoverColor: this._theme.shapeHover.x + hoverColor: this._theme.shapeHover.x, + disabledColor: this._theme.disabled }), xz: new PlaneShape(this._device, { axis: 'y', layers: [this._layer.id], rotation: new Vec3(0, 0, 0), defaultColor: this._theme.shapeBase.y, - hoverColor: this._theme.shapeHover.y + hoverColor: this._theme.shapeHover.y, + disabledColor: this._theme.disabled }), xy: new PlaneShape(this._device, { axis: 'z', layers: [this._layer.id], rotation: new Vec3(90, 0, 0), defaultColor: this._theme.shapeBase.z, - hoverColor: this._theme.shapeHover.z + hoverColor: this._theme.shapeHover.z, + disabledColor: this._theme.disabled }), x: new BoxLineShape(this._device, { axis: 'x', layers: [this._layer.id], rotation: new Vec3(0, 0, -90), defaultColor: this._theme.shapeBase.x, - hoverColor: this._theme.shapeHover.x + hoverColor: this._theme.shapeHover.x, + disabledColor: this._theme.disabled }), y: new BoxLineShape(this._device, { axis: 'y', layers: [this._layer.id], rotation: new Vec3(0, 0, 0), defaultColor: this._theme.shapeBase.y, - hoverColor: this._theme.shapeHover.y + hoverColor: this._theme.shapeHover.y, + disabledColor: this._theme.disabled }), z: new BoxLineShape(this._device, { axis: 'z', layers: [this._layer.id], rotation: new Vec3(90, 0, 0), defaultColor: this._theme.shapeBase.z, - hoverColor: this._theme.shapeHover.z + hoverColor: this._theme.shapeHover.z, + disabledColor: this._theme.disabled }) }; @@ -179,21 +186,24 @@ class ScaleGizmo extends TransformGizmo { }); this.on(TransformGizmo.EVENT_TRANSFORMMOVE, (point) => { - const pointDelta = tmpV3.copy(point).sub(this._selectionStartPoint); + // calculate scale delta and update node scales + const scaleDelta = tmpV3.copy(point).sub(this._selectionStartPoint); if (this.snap) { - pointDelta.mulScalar(1 / this.snapIncrement); - pointDelta.round(); - pointDelta.mulScalar(this.snapIncrement); + scaleDelta.mulScalar(1 / this.snapIncrement); + scaleDelta.round(); + scaleDelta.mulScalar(this.snapIncrement); } - pointDelta.mulScalar(1 / this._scale); - this._setNodeScales(pointDelta.add(Vec3.ONE)); + scaleDelta.mulScalar(1 / this._scale); + this._setNodeScales(scaleDelta.add(Vec3.ONE)); }); this.on(TransformGizmo.EVENT_TRANSFORMEND, () => { + // show all shapes this._drag(false); }); this.on(TransformGizmo.EVENT_NODESDETACH, () => { + // reset stored scales this._nodeScales.clear(); }); } @@ -435,34 +445,34 @@ class ScaleGizmo extends TransformGizmo { // axes let dot = cameraDir.dot(this.root.right); - this._shapes.x.entity.enabled = Math.abs(dot) < GLANCE_EPSILON; + this._shapes.x.entity.enabled = 1 - Math.abs(dot) > GLANCE_EPSILON; if (this.flipAxes) { this._shapes.x.flipped = dot < 0; } dot = cameraDir.dot(this.root.up); - this._shapes.y.entity.enabled = Math.abs(dot) < GLANCE_EPSILON; + this._shapes.y.entity.enabled = 1 - Math.abs(dot) > GLANCE_EPSILON; if (this.flipAxes) { this._shapes.y.flipped = dot < 0; } dot = cameraDir.dot(this.root.forward); - this._shapes.z.entity.enabled = Math.abs(dot) < GLANCE_EPSILON; + this._shapes.z.entity.enabled = 1 - Math.abs(dot) > GLANCE_EPSILON; if (this.flipAxes) { this._shapes.z.flipped = dot > 0; } // planes tmpV1.cross(cameraDir, this.root.right); - this._shapes.yz.entity.enabled = tmpV1.length() < GLANCE_EPSILON; + this._shapes.yz.entity.enabled = 1 - tmpV1.length() > GLANCE_EPSILON; if (this.flipPlanes) { this._shapes.yz.flipped = tmpV2.set(0, +(tmpV1.dot(this.root.forward) < 0), +(tmpV1.dot(this.root.up) < 0)); } tmpV1.cross(cameraDir, this.root.forward); - this._shapes.xy.entity.enabled = tmpV1.length() < GLANCE_EPSILON; + this._shapes.xy.entity.enabled = 1 - tmpV1.length() > GLANCE_EPSILON; if (this.flipPlanes) { this._shapes.xy.flipped = tmpV2.set(+(tmpV1.dot(this.root.up) < 0), +(tmpV1.dot(this.root.right) > 0), 0); } tmpV1.cross(cameraDir, this.root.up); - this._shapes.xz.entity.enabled = tmpV1.length() < GLANCE_EPSILON; + this._shapes.xz.entity.enabled = 1 - tmpV1.length() > GLANCE_EPSILON; if (this.flipPlanes) { this._shapes.xz.flipped = tmpV2.set(+(tmpV1.dot(this.root.forward) > 0), 0, +(tmpV1.dot(this.root.right) > 0)); } @@ -512,17 +522,17 @@ class ScaleGizmo extends TransformGizmo { } /** - * @param {Vec3} pointDelta - The point delta. + * @param {Vec3} scaleDelta - The point delta. * @private */ - _setNodeScales(pointDelta) { + _setNodeScales(scaleDelta) { for (let i = 0; i < this.nodes.length; i++) { const node = this.nodes[i]; const scale = this._nodeScales.get(node); if (!scale) { continue; } - node.setLocalScale(tmpV1.copy(scale).mul(pointDelta).max(this.lowerBoundScale)); + node.setLocalScale(tmpV1.copy(scale).mul(scaleDelta).max(this.lowerBoundScale)); } } @@ -537,13 +547,11 @@ class ScaleGizmo extends TransformGizmo { const mouseWPos = this._camera.screenToWorld(x, y, 1); const axis = this._selectedAxis; - const isPlane = this._selectedIsPlane; + const point = new Vec3(); const ray = this._createRay(mouseWPos); const plane = this._createPlane(axis, axis === 'xyz', !isPlane); - - const point = new Vec3(); if (!plane.intersectsRay(ray, point)) { point.copy(this.root.getLocalPosition()); } diff --git a/src/extras/gizmo/shaders.js b/src/extras/gizmo/shaders.js index eaa713be1e3..3b10cb20ce1 100644 --- a/src/extras/gizmo/shaders.js +++ b/src/extras/gizmo/shaders.js @@ -30,6 +30,7 @@ export const unlitShader = { void main(void) { gl_FragColor = vec4(gammaCorrectOutput(decodeGamma(uColor)), uColor.w); + gl_FragDepth = 0.0; } `, vertexWGSL: /* wgsl */` @@ -56,6 +57,7 @@ export const unlitShader = { fn fragmentMain(input: FragmentInput) -> FragmentOutput { var output: FragmentOutput; output.color = vec4f(gammaCorrectOutput(decodeGamma(uniform.uColor)), uniform.uColor.w); + output.fragDepth = 0.0; return output; } ` diff --git a/src/extras/gizmo/shape/arc-shape.js b/src/extras/gizmo/shape/arc-shape.js index 6df17136dcb..82654714129 100644 --- a/src/extras/gizmo/shape/arc-shape.js +++ b/src/extras/gizmo/shape/arc-shape.js @@ -52,6 +52,14 @@ class ArcShape extends Shape { */ _tolerance = 0.05; + /** + * The internal cache for triangle data. + * + * @type {[TriData, TriData]} + * @private + */ + _triDataCache; + /** * @param {GraphicsDevice} device - The graphics device. * @param {ShapeArgs & ArcShapeArgs} args - The shape options. @@ -64,16 +72,18 @@ class ArcShape extends Shape { this._sectorAngle = args.sectorAngle ?? this._sectorAngle; // intersect - this.triData = [ - new TriData(this._createTorusGeometry()) + this._triDataCache = [ + new TriData(this._createTorusGeometry(this._sectorAngle)), + new TriData(this._createTorusGeometry(360)) ]; + this.triData = [this._triDataCache[0]]; // render this._createRenderComponent(this.entity, [ this._createTorusMesh(this._sectorAngle), this._createTorusMesh(360) ]); - this.drag(false); + this.show('sector'); // update transform this._update(); @@ -82,14 +92,15 @@ class ArcShape extends Shape { /** * Create the torus geometry. * + * @param {number} sectorAngle - The sector angle. * @returns {TorusGeometry} The torus geometry. * @private */ - _createTorusGeometry() { + _createTorusGeometry(sectorAngle) { return new TorusGeometry({ tubeRadius: this._tubeRadius + this._tolerance, ringRadius: this._ringRadius, - sectorAngle: this._sectorAngle, + sectorAngle: sectorAngle, segments: TORUS_INTERSECT_SEGMENTS }); } @@ -176,7 +187,8 @@ class ArcShape extends Shape { */ _update() { // intersect - this.triData[0].fromGeometry(this._createTorusGeometry()); + this._triDataCache[0].fromGeometry(this._createTorusGeometry(this._sectorAngle)); + this._triDataCache[1].fromGeometry(this._createTorusGeometry(360)); // render this.meshInstances[0].mesh = this._createTorusMesh(this._sectorAngle); @@ -184,28 +196,28 @@ class ArcShape extends Shape { } /** - * Enable or disable dragging. - * - * @param {boolean} state - The dragging state. - */ - drag(state) { - this.meshInstances[0].visible = !state; - this.meshInstances[1].visible = state; - } - - /** - * Hide the shape. - * - * @param {boolean} state - The hiding state. - */ - hide(state) { - if (state) { - this.meshInstances[0].visible = false; - this.meshInstances[1].visible = false; - return; + * @param {'sector' | 'ring' | 'none'} state - The visibility state. + */ + show(state) { + switch (state) { + case 'sector': { + this.triData[0] = this._triDataCache[0]; + this.meshInstances[0].visible = true; + this.meshInstances[1].visible = false; + break; + } + case 'ring': { + this.triData[0] = this._triDataCache[1]; + this.meshInstances[0].visible = false; + this.meshInstances[1].visible = true; + break; + } + case 'none': { + this.meshInstances[0].visible = false; + this.meshInstances[1].visible = false; + break; + } } - - this.drag(false); } } diff --git a/src/extras/gizmo/shape/arrow-shape.js b/src/extras/gizmo/shape/arrow-shape.js index 16fedd11a15..d3f6d197751 100644 --- a/src/extras/gizmo/shape/arrow-shape.js +++ b/src/extras/gizmo/shape/arrow-shape.js @@ -3,6 +3,7 @@ import { Vec3 } from '../../../core/math/vec3.js'; import { Entity } from '../../../framework/entity.js'; import { ConeGeometry } from '../../../scene/geometry/cone-geometry.js'; import { CylinderGeometry } from '../../../scene/geometry/cylinder-geometry.js'; +import { Mesh } from '../../../scene/mesh.js'; import { TriData } from '../tri-data.js'; import { Shape } from './shape.js'; @@ -124,10 +125,14 @@ class ArrowShape extends Shape { // render this._head = new Entity(`head:${this.axis}`); this.entity.addChild(this._head); - this._addRenderMesh(this._head, 'cone'); + this._createRenderComponent(this._head, [ + Mesh.fromGeometry(this.device, new ConeGeometry()) + ]); this._line = new Entity(`line:${this.axis}`); this.entity.addChild(this._line); - this._addRenderMesh(this._line, 'cylinder'); + this._createRenderComponent(this._line, [ + Mesh.fromGeometry(this.device, new CylinderGeometry()) + ]); // update this._update(); diff --git a/src/extras/gizmo/shape/box-shape.js b/src/extras/gizmo/shape/box-shape.js index ce8d61e5597..c0359106d6b 100644 --- a/src/extras/gizmo/shape/box-shape.js +++ b/src/extras/gizmo/shape/box-shape.js @@ -1,4 +1,5 @@ import { BoxGeometry } from '../../../scene/geometry/box-geometry.js'; +import { Mesh } from '../../../scene/mesh.js'; import { TriData } from '../tri-data.js'; import { Shape } from './shape.js'; @@ -49,7 +50,9 @@ class BoxShape extends Shape { ]; // render - this._addRenderMesh(this.entity, 'box'); + this._createRenderComponent(this.entity, [ + Mesh.fromGeometry(this.device, new BoxGeometry()) + ]); // update transform this._update(); diff --git a/src/extras/gizmo/shape/boxline-shape.js b/src/extras/gizmo/shape/boxline-shape.js index 022f05a0647..d5956be43be 100644 --- a/src/extras/gizmo/shape/boxline-shape.js +++ b/src/extras/gizmo/shape/boxline-shape.js @@ -3,6 +3,7 @@ import { Vec3 } from '../../../core/math/vec3.js'; import { Entity } from '../../../framework/entity.js'; import { BoxGeometry } from '../../../scene/geometry/box-geometry.js'; import { CylinderGeometry } from '../../../scene/geometry/cylinder-geometry.js'; +import { Mesh } from '../../../scene/mesh.js'; import { TriData } from '../tri-data.js'; import { Shape } from './shape.js'; @@ -114,10 +115,14 @@ class BoxLineShape extends Shape { // render this._box = new Entity(`box:${this.axis}`); this.entity.addChild(this._box); - this._addRenderMesh(this._box, 'box'); + this._createRenderComponent(this._box, [ + Mesh.fromGeometry(this.device, new BoxGeometry()) + ]); this._line = new Entity(`line:${this.axis}`); this.entity.addChild(this._line); - this._addRenderMesh(this._line, 'cylinder'); + this._createRenderComponent(this._line, [ + Mesh.fromGeometry(this.device, new CylinderGeometry()) + ]); // update transform this._update(); diff --git a/src/extras/gizmo/shape/plane-shape.js b/src/extras/gizmo/shape/plane-shape.js index 5be69c341d2..260d14d2ea2 100644 --- a/src/extras/gizmo/shape/plane-shape.js +++ b/src/extras/gizmo/shape/plane-shape.js @@ -1,6 +1,7 @@ import { Vec3 } from '../../../core/math/vec3.js'; import { CULLFACE_NONE } from '../../../platform/graphics/constants.js'; import { PlaneGeometry } from '../../../scene/geometry/plane-geometry.js'; +import { Mesh } from '../../../scene/mesh.js'; import { TriData } from '../tri-data.js'; import { Shape } from './shape.js'; @@ -69,7 +70,9 @@ class PlaneShape extends Shape { ]; // render - this._addRenderMesh(this.entity, 'plane'); + this._createRenderComponent(this.entity, [ + Mesh.fromGeometry(this.device, new PlaneGeometry()) + ]); // update transform this._update(); diff --git a/src/extras/gizmo/shape/shape.js b/src/extras/gizmo/shape/shape.js index cc952fbfec7..7d445ccef79 100644 --- a/src/extras/gizmo/shape/shape.js +++ b/src/extras/gizmo/shape/shape.js @@ -7,33 +7,15 @@ import { CULLFACE_BACK } from '../../../platform/graphics/constants.js'; import { BLEND_NORMAL } from '../../../scene/constants.js'; import { COLOR_GRAY } from '../color.js'; -import { Mesh } from '../../../scene/mesh.js'; import { Geometry } from '../../../scene/geometry/geometry.js'; -import { BoxGeometry } from '../../../scene/geometry/box-geometry.js'; -import { CylinderGeometry } from '../../../scene/geometry/cylinder-geometry.js'; -import { ConeGeometry } from '../../../scene/geometry/cone-geometry.js'; -import { PlaneGeometry } from '../../../scene/geometry/plane-geometry.js'; -import { SphereGeometry } from '../../../scene/geometry/sphere-geometry.js'; -import { TorusGeometry } from '../../../scene/geometry/torus-geometry.js'; import { unlitShader } from '../shaders.js'; /** * @import { GraphicsDevice } from '../../../platform/graphics/graphics-device.js'; + * @import { Mesh } from '../../../scene/mesh.js'; * @import { TriData } from '../tri-data.js'; */ -/** - * @type {Record} - */ -const GEOMETRIES = { - box: BoxGeometry, - cone: ConeGeometry, - cylinder: CylinderGeometry, - plane: PlaneGeometry, - sphere: SphereGeometry, - torus: TorusGeometry -}; - const tmpG = new Geometry(); tmpG.positions = []; tmpG.normals = []; @@ -291,24 +273,6 @@ class Shape { }); } - /** - * Add a render mesh to an entity. - * - * @param {Entity} entity - The entity to add the render mesh to. - * @param {string} type - The type of primitive to create. - * @throws {Error} If the primitive type is invalid. - * @protected - */ - _addRenderMesh(entity, type) { - const Geometry = GEOMETRIES[type]; - if (!Geometry) { - throw new Error('Invalid primitive type.'); - } - this._createRenderComponent(entity, [ - Mesh.fromGeometry(this.device, new Geometry()) - ]); - } - /** * Update the shape's transform. * diff --git a/src/extras/gizmo/shape/sphere-shape.js b/src/extras/gizmo/shape/sphere-shape.js index cf456807a7c..362a6fce201 100644 --- a/src/extras/gizmo/shape/sphere-shape.js +++ b/src/extras/gizmo/shape/sphere-shape.js @@ -1,4 +1,5 @@ import { SphereGeometry } from '../../../scene/geometry/sphere-geometry.js'; +import { Mesh } from '../../../scene/mesh.js'; import { TriData } from '../tri-data.js'; import { Shape } from './shape.js'; @@ -7,7 +8,7 @@ import { Shape } from './shape.js'; /** * @typedef {object} SphereShapeArgs - * @property {number} [size] - The size of the sphere. + * @property {number} [radius] - The radius of the sphere. * @property {number} [tolerance] - The intersection tolerance of the sphere. */ @@ -21,7 +22,7 @@ class SphereShape extends Shape { * @type {number} * @private */ - _size = 0.12; + _radius = 0.06; /** * The internal intersection tolerance of the sphere. @@ -40,7 +41,7 @@ class SphereShape extends Shape { constructor(device, args = {}) { super(device, 'sphereCenter', args); - this._size = args.size ?? this._size; + this._radius = args.radius ?? this._radius; this._tolerance = args.tolerance ?? this._tolerance; // intersect @@ -49,29 +50,34 @@ class SphereShape extends Shape { ]; // render - this._addRenderMesh(this.entity, 'sphere'); + this._createRenderComponent(this.entity, [ + Mesh.fromGeometry(this.device, new SphereGeometry({ + latitudeBands: 32, + longitudeBands: 32 + })) + ]); // update transform this._update(); } /** - * Set the rendered size of the sphere. + * Set the rendered radius of the sphere. * - * @param {number} value - The new size of the sphere. + * @param {number} value - The new radius of the sphere. */ - set size(value) { - this._size = value ?? this._size; + set radius(value) { + this._radius = value ?? this._radius; this._update(); } /** - * Get the rendered size of the sphere. + * Get the rendered radius of the sphere. * - * @returns {number} The size of the sphere. + * @returns {number} The radius of the sphere. */ - get size() { - return this._size; + get radius() { + return this._radius; } /** @@ -101,7 +107,8 @@ class SphereShape extends Shape { */ _update() { // intersect/render - this.entity.setLocalScale(this._size, this._size, this._size); + const scale = this._radius * 2; + this.entity.setLocalScale(scale, scale, scale); } } diff --git a/src/extras/gizmo/transform-gizmo.js b/src/extras/gizmo/transform-gizmo.js index 798bfc9bb9e..8cf43bb99ff 100644 --- a/src/extras/gizmo/transform-gizmo.js +++ b/src/extras/gizmo/transform-gizmo.js @@ -640,10 +640,9 @@ class TransformGizmo extends Gizmo { const axis = this._selectedAxis; + const point = new Vec3(); const ray = this._createRay(mouseWPos); const plane = this._createPlane(axis, isFacing, isLine); - - const point = new Vec3(); if (!plane.intersectsRay(ray, point)) { point.copy(this.root.getLocalPosition()); } @@ -683,13 +682,12 @@ class TransformGizmo extends Gizmo { * @protected */ _drawSpanLine(pos, rot, axis) { - tmpV1.set(0, 0, 0); - tmpV1[axis] = 1; - tmpV1.mulScalar(this._camera.farClip - this._camera.nearClip); - tmpV2.copy(tmpV1).mulScalar(-1); - const from = rot.transformVector(tmpV1, tmpV1).add(pos); - const to = rot.transformVector(tmpV2, tmpV2).add(pos); + const dir = this._dirFromAxis(axis, tmpV1); const color = this._theme.guideBase[axis]; + const from = tmpV1.copy(dir).mulScalar(this._camera.farClip - this._camera.nearClip); + const to = tmpV2.copy(from).mulScalar(-1); + rot.transformVector(from, from).add(pos); + rot.transformVector(to, to).add(pos); if (this._theme.guideOcclusion < 1) { const occluded = tmpC1.copy(color); occluded.a *= (1 - this._theme.guideOcclusion); diff --git a/src/extras/gizmo/translate-gizmo.js b/src/extras/gizmo/translate-gizmo.js index d9223ceeee4..ae9b331d370 100644 --- a/src/extras/gizmo/translate-gizmo.js +++ b/src/extras/gizmo/translate-gizmo.js @@ -20,7 +20,7 @@ const tmpV3 = new Vec3(); const tmpQ1 = new Quat(); // constants -const GLANCE_EPSILON = 0.98; +const GLANCE_EPSILON = 0.01; const AXES = /** @type {('x' | 'y' | 'z')[]} */ (['x', 'y', 'z']); /** @@ -63,49 +63,56 @@ class TranslateGizmo extends TransformGizmo { axis: 'xyz', layers: [this._layer.id], defaultColor: this._theme.shapeBase.xyz, - hoverColor: this._theme.shapeHover.xyz + hoverColor: this._theme.shapeHover.xyz, + disabledColor: this._theme.disabled }), yz: new PlaneShape(this._device, { axis: 'x', layers: [this._layer.id], rotation: new Vec3(0, 0, -90), defaultColor: this._theme.shapeBase.x, - hoverColor: this._theme.shapeHover.x + hoverColor: this._theme.shapeHover.x, + disabledColor: this._theme.disabled }), xz: new PlaneShape(this._device, { axis: 'y', layers: [this._layer.id], rotation: new Vec3(0, 0, 0), defaultColor: this._theme.shapeBase.y, - hoverColor: this._theme.shapeHover.y + hoverColor: this._theme.shapeHover.y, + disabledColor: this._theme.disabled }), xy: new PlaneShape(this._device, { axis: 'z', layers: [this._layer.id], rotation: new Vec3(90, 0, 0), defaultColor: this._theme.shapeBase.z, - hoverColor: this._theme.shapeHover.z + hoverColor: this._theme.shapeHover.z, + disabledColor: this._theme.disabled }), x: new ArrowShape(this._device, { axis: 'x', layers: [this._layer.id], rotation: new Vec3(0, 0, -90), defaultColor: this._theme.shapeBase.x, - hoverColor: this._theme.shapeHover.x + hoverColor: this._theme.shapeHover.x, + disabledColor: this._theme.disabled }), y: new ArrowShape(this._device, { axis: 'y', layers: [this._layer.id], rotation: new Vec3(0, 0, 0), defaultColor: this._theme.shapeBase.y, - hoverColor: this._theme.shapeHover.y + hoverColor: this._theme.shapeHover.y, + disabledColor: this._theme.disabled }), z: new ArrowShape(this._device, { axis: 'z', layers: [this._layer.id], rotation: new Vec3(90, 0, 0), defaultColor: this._theme.shapeBase.z, - hoverColor: this._theme.shapeHover.z + hoverColor: this._theme.shapeHover.z, + disabledColor: this._theme.disabled }) }; @@ -167,20 +174,23 @@ class TranslateGizmo extends TransformGizmo { }); this.on(TransformGizmo.EVENT_TRANSFORMMOVE, (point) => { - const pointDelta = tmpV3.copy(point).sub(this._selectionStartPoint); + // calculate translate delta and update node positions + const translateDelta = tmpV3.copy(point).sub(this._selectionStartPoint); if (this.snap) { - pointDelta.mulScalar(1 / this.snapIncrement); - pointDelta.round(); - pointDelta.mulScalar(this.snapIncrement); + translateDelta.mulScalar(1 / this.snapIncrement); + translateDelta.round(); + translateDelta.mulScalar(this.snapIncrement); } - this._setNodePositions(pointDelta); + this._setNodePositions(translateDelta); }); this.on(TransformGizmo.EVENT_TRANSFORMEND, () => { + // show all shapes this._drag(false); }); this.on(TransformGizmo.EVENT_NODESDETACH, () => { + // reset stored positions this._nodeLocalPositions.clear(); this._nodePositions.clear(); }); @@ -336,7 +346,7 @@ class TranslateGizmo extends TransformGizmo { * @type {number} */ set axisCenterSize(value) { - this._shapes.xyz.size = value; + this._shapes.xyz.radius = value; } /** @@ -345,7 +355,7 @@ class TranslateGizmo extends TransformGizmo { * @type {number} */ get axisCenterSize() { - return this._shapes.xyz.size; + return this._shapes.xyz.radius; } /** @@ -415,34 +425,34 @@ class TranslateGizmo extends TransformGizmo { // axes let dot = cameraDir.dot(this.root.right); - this._shapes.x.entity.enabled = Math.abs(dot) < GLANCE_EPSILON; + this._shapes.x.entity.enabled = 1 - Math.abs(dot) > GLANCE_EPSILON; if (this.flipAxes) { this._shapes.x.flipped = dot < 0; } dot = cameraDir.dot(this.root.up); - this._shapes.y.entity.enabled = Math.abs(dot) < GLANCE_EPSILON; + this._shapes.y.entity.enabled = 1 - Math.abs(dot) > GLANCE_EPSILON; if (this.flipAxes) { this._shapes.y.flipped = dot < 0; } dot = cameraDir.dot(this.root.forward); - this._shapes.z.entity.enabled = Math.abs(dot) < GLANCE_EPSILON; + this._shapes.z.entity.enabled = 1 - Math.abs(dot) > GLANCE_EPSILON; if (this.flipAxes) { this._shapes.z.flipped = dot > 0; } // planes tmpV1.cross(cameraDir, this.root.right); - this._shapes.yz.entity.enabled = tmpV1.length() < GLANCE_EPSILON; + this._shapes.yz.entity.enabled = 1 - tmpV1.length() > GLANCE_EPSILON; if (this.flipPlanes) { this._shapes.yz.flipped = tmpV2.set(0, +(tmpV1.dot(this.root.forward) < 0), +(tmpV1.dot(this.root.up) < 0)); } tmpV1.cross(cameraDir, this.root.forward); - this._shapes.xy.entity.enabled = tmpV1.length() < GLANCE_EPSILON; + this._shapes.xy.entity.enabled = 1 - tmpV1.length() > GLANCE_EPSILON; if (this.flipPlanes) { this._shapes.xy.flipped = tmpV2.set(+(tmpV1.dot(this.root.up) < 0), +(tmpV1.dot(this.root.right) > 0), 0); } tmpV1.cross(cameraDir, this.root.up); - this._shapes.xz.entity.enabled = tmpV1.length() < GLANCE_EPSILON; + this._shapes.xz.entity.enabled = 1 - tmpV1.length() > GLANCE_EPSILON; if (this.flipPlanes) { this._shapes.xz.flipped = tmpV2.set(+(tmpV1.dot(this.root.forward) > 0), 0, +(tmpV1.dot(this.root.right) > 0)); } @@ -492,10 +502,10 @@ class TranslateGizmo extends TransformGizmo { } /** - * @param {Vec3} pointDelta - The delta to apply to the node positions. + * @param {Vec3} translateDelta - The delta to apply to the node positions. * @private */ - _setNodePositions(pointDelta) { + _setNodePositions(translateDelta) { for (let i = 0; i < this.nodes.length; i++) { const node = this.nodes[i]; @@ -504,7 +514,7 @@ class TranslateGizmo extends TransformGizmo { if (!pos) { continue; } - tmpV1.copy(pointDelta); + tmpV1.copy(translateDelta); node.parent?.getWorldTransform().getScale(tmpV2); tmpV2.x = 1 / tmpV2.x; tmpV2.y = 1 / tmpV2.y; @@ -517,7 +527,7 @@ class TranslateGizmo extends TransformGizmo { if (!pos) { continue; } - node.setPosition(tmpV1.copy(pointDelta).add(pos)); + node.setPosition(tmpV1.copy(translateDelta).add(pos)); } } @@ -536,10 +546,9 @@ class TranslateGizmo extends TransformGizmo { const axis = this._selectedAxis; const isPlane = this._selectedIsPlane; + const point = new Vec3(); const ray = this._createRay(mouseWPos); const plane = this._createPlane(axis, axis === 'xyz', !isPlane); - - const point = new Vec3(); if (!plane.intersectsRay(ray, point)) { point.copy(this.root.getLocalPosition()); }