Skip to content
Merged
Show file tree
Hide file tree
Changes from 21 commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
d8b3ac8
fix: reverted regression using local angles instead of world angles f…
kpal81xd Sep 3, 2025
084cd1f
fix: update shape orientation calculation to use local euler angles f…
kpal81xd Sep 4, 2025
eca2ecb
refactor: simplify visibility handling in ArcShape and update RotateG…
kpal81xd Sep 4, 2025
ba0741c
fix: update facing threshold constants for RotateGizmo and improve se…
kpal81xd Sep 4, 2025
9037a19
fix: update GLANCE_EPSILON constant and adjust shape visibility logic…
kpal81xd Sep 4, 2025
ecb9ca6
fix: improve shape visibility logic in RotateGizmo for better state h…
kpal81xd Sep 4, 2025
be30c7a
fix: trigger render update event in RotateGizmo after shape visibilit…
kpal81xd Sep 4, 2025
ca8c47c
fix: enhance triangle data caching in ArcShape for improved geometry …
kpal81xd Sep 4, 2025
fb90203
fix: update TranslateGizmo to calculate point delta and reset node po…
kpal81xd Sep 4, 2025
0d887dc
fix: rename pointDelta to translateDelta for clarity in TranslateGizm…
kpal81xd Sep 4, 2025
7126f7c
fix: rename pointDelta to scaleDelta for clarity in ScaleGizmo scalin…
kpal81xd Sep 4, 2025
4d9b97c
feat: implemented basic ball roll rotation
kpal81xd Sep 4, 2025
c2a8a73
fix: refine rotation logic in RotateGizmo to handle axis-specific con…
kpal81xd Sep 4, 2025
c414383
fix: mark potential scale warping issue in RotateGizmo with a FIXME c…
kpal81xd Sep 4, 2025
a3057f3
fix: reset depth to always render gizmos on top of each other
kpal81xd Sep 4, 2025
4789ac2
fix: enhance type annotations and parameters for drawing span lines i…
kpal81xd Sep 4, 2025
8e1e1d4
feat: add center radius control to RotateGizmo and update related pro…
kpal81xd Sep 4, 2025
0e5c1ef
feat: override theme for xyz in rotate gizmo
kpal81xd Sep 4, 2025
1ab207a
feat: refactor rendering in shape classes to use Mesh.fromGeometry fo…
kpal81xd Sep 4, 2025
8cd9c47
feat: add Mesh import to shape.js for future rendering enhancements
kpal81xd Sep 4, 2025
00b73f4
feat: enhance SphereGeometry rendering with latitude and longitude bands
kpal81xd Sep 4, 2025
5966e70
Update src/extras/gizmo/translate-gizmo.js
kpal81xd Sep 5, 2025
420a242
Merge branch 'main' into gizmo
kpal81xd Sep 5, 2025
5fc5310
feat: add disabled color to gizmo shapes for better visual feedback
kpal81xd Sep 5, 2025
c9f433a
refactor: remove unused rotation calculation and associated method fr…
kpal81xd Sep 5, 2025
b8357d3
refactor: remove unused Mat4 import and temporary variable from Rotat…
kpal81xd Sep 5, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions examples/src/examples/gizmos/transform-rotate.controls.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -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' },
Expand Down
1 change: 1 addition & 0 deletions examples/src/examples/gizmos/transform-rotate.example.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -151,6 +151,7 @@ data.set('gizmo', {
xyzRingRadius: gizmo.xyzRingRadius,
faceTubeRadius: gizmo.faceTubeRadius,
faceRingRadius: gizmo.faceRingRadius,
centerRadius: gizmo.centerRadius,
angleGuideThickness: gizmo.angleGuideThickness
});

Expand Down
2 changes: 1 addition & 1 deletion src/extras/gizmo/gizmo.js
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down
176 changes: 139 additions & 37 deletions src/extras/gizmo/rotate-gizmo.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,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'
Expand All @@ -17,6 +19,7 @@ import { MeshLine } from './mesh-line.js';
*/

// temporary variables
const tmpVa = new Vec2();
const tmpV1 = new Vec3();
const tmpV2 = new Vec3();
const tmpV3 = new Vec3();
Expand All @@ -27,7 +30,9 @@ 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
Expand Down Expand Up @@ -96,6 +101,13 @@ class RotateGizmo extends TransformGizmo {
defaultColor: this._theme.shapeBase.f,
hoverColor: this._theme.shapeHover.f,
ringRadius: 0.55
}),
xyz: new SphereShape(this._device, {
axis: 'xyz',
layers: [this._layer.id],
defaultColor: this._theme.shapeBase.xyz,
hoverColor: this._theme.shapeHover.xyz,
radius: 0.5
})
};

Expand All @@ -107,6 +119,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.
*
Expand Down Expand Up @@ -179,6 +199,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 = [
Expand All @@ -191,8 +220,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();
Expand All @@ -213,24 +245,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();
Expand Down Expand Up @@ -309,6 +357,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.
*
Expand Down Expand Up @@ -396,9 +462,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),
Expand Down Expand Up @@ -433,23 +501,40 @@ class RotateGizmo extends TransformGizmo {
_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);
this._shapes.f.entity.rotateLocal(-90, 0, 0);
}

// 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);
}
}

/**
Expand All @@ -459,26 +544,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;
}
}
Expand All @@ -501,19 +579,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;
Expand All @@ -533,7 +612,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));
}
Expand All @@ -555,10 +634,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());
}
Expand All @@ -573,7 +651,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;
Expand All @@ -582,10 +660,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);

Expand All @@ -612,6 +690,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
*/
Expand Down
Loading