From 2359533df5c26d1e6b2deb2c05b1a83596623301 Mon Sep 17 00:00:00 2001 From: Nettika Date: Thu, 29 Jan 2026 22:40:21 -0800 Subject: [PATCH] Adjust translate gizmo rendering --- src/composables/useTranslateGizmo.ts | 90 +++++++++++++++++----------- 1 file changed, 54 insertions(+), 36 deletions(-) diff --git a/src/composables/useTranslateGizmo.ts b/src/composables/useTranslateGizmo.ts index 12f17fd1e..d2bb850b6 100644 --- a/src/composables/useTranslateGizmo.ts +++ b/src/composables/useTranslateGizmo.ts @@ -33,7 +33,7 @@ const HIGHLIGHT_COLORS = { export function createTranslateGizmo(options: TranslateGizmoOptions = {}): TranslateGizmo { const { - arrowLength = 1.0, + arrowLength = 1.0, // Extension past bounding box arrowHeadLength = 0.2, arrowHeadRadius = 0.08, shaftRadius = 0.025, @@ -50,17 +50,23 @@ export function createTranslateGizmo(options: TranslateGizmoOptions = {}): Trans // Store arrow references for positioning const arrows: Record = {} - // Store world-space offsets from center to each arrow origin + // Store shaft and cone meshes for dynamic length adjustment + const shafts: Record = {} + const cones: Record = {} + + // Store world-space half-sizes from center to each bounding box face type ArrowKey = 'x_pos' | 'x_neg' | 'y_pos' | 'y_neg' | 'z_pos' | 'z_neg' - const arrowOffsets: Record = { - x_pos: new THREE.Vector3(), - x_neg: new THREE.Vector3(), - y_pos: new THREE.Vector3(), - y_neg: new THREE.Vector3(), - z_pos: new THREE.Vector3(), - z_neg: new THREE.Vector3(), + const boundsHalfSizes: Record = { + x_pos: 0, + x_neg: 0, + y_pos: 0, + y_neg: 0, + z_pos: 0, + z_neg: 0, } + let currentScale = 1 + function createArrow(axis: 'x' | 'y' | 'z', negative: boolean): THREE.Group { const arrowGroup = new THREE.Group() const suffix = negative ? '_neg' : '_pos' @@ -69,22 +75,21 @@ export function createTranslateGizmo(options: TranslateGizmoOptions = {}): Trans const color = AXIS_COLORS[axis] - // Create shaft (cylinder) - const shaftLength = arrowLength - arrowHeadLength - const shaftGeometry = new THREE.CylinderGeometry(shaftRadius, shaftRadius, shaftLength, 8) + // Create shaft with unit height - will be scaled dynamically + const shaftGeometry = new THREE.CylinderGeometry(shaftRadius, shaftRadius, 1, 8) const shaftMaterial = new THREE.MeshBasicMaterial({ color }) materials[`${axis}${suffix}_shaft`] = shaftMaterial const shaft = new THREE.Mesh(shaftGeometry, shaftMaterial) shaft.userData = { axis, isGizmoHandle: true, negative } - shaft.position.y = shaftLength / 2 + shafts[`${axis}${suffix}`] = shaft - // Create cone (arrow head) + // Create cone (arrow head) - positioned dynamically const coneGeometry = new THREE.ConeGeometry(arrowHeadRadius, arrowHeadLength, 16) const coneMaterial = new THREE.MeshBasicMaterial({ color }) materials[`${axis}${suffix}_cone`] = coneMaterial const cone = new THREE.Mesh(coneGeometry, coneMaterial) cone.userData = { axis, isGizmoHandle: true, negative } - cone.position.y = shaftLength + arrowHeadLength / 2 + cones[`${axis}${suffix}`] = cone arrowGroup.add(shaft) arrowGroup.add(cone) @@ -111,8 +116,6 @@ export function createTranslateGizmo(options: TranslateGizmoOptions = {}): Trans group.add(createArrow('z', false)) group.add(createArrow('z', true)) - let currentScale = 1 - function show(): void { group.visible = true } @@ -124,28 +127,43 @@ export function createTranslateGizmo(options: TranslateGizmoOptions = {}): Trans function setPositionAndBounds(center: THREE.Vector3, box: THREE.Box3): void { group.position.copy(center) - // Calculate world-space offsets from center to each bounding box face - arrowOffsets.x_pos.set(box.max.x - center.x, 0, 0) - arrowOffsets.x_neg.set(box.min.x - center.x, 0, 0) - arrowOffsets.y_pos.set(0, box.max.y - center.y, 0) - arrowOffsets.y_neg.set(0, box.min.y - center.y, 0) - arrowOffsets.z_pos.set(0, 0, box.max.z - center.z) - arrowOffsets.z_neg.set(0, 0, box.min.z - center.z) + // Store world-space distances from center to each bounding box face + boundsHalfSizes.x_pos = box.max.x - center.x + boundsHalfSizes.x_neg = center.x - box.min.x + boundsHalfSizes.y_pos = box.max.y - center.y + boundsHalfSizes.y_neg = center.y - box.min.y + boundsHalfSizes.z_pos = box.max.z - center.z + boundsHalfSizes.z_neg = center.z - box.min.z - // Apply offsets to arrow positions (compensating for current scale) - updateArrowPositions() + // Update arrow lengths based on bounds + updateArrowLengths() } - function updateArrowPositions(): void { - // Position arrows so they appear at bounding box faces in world space - // Since group.scale affects child positions, we divide by scale to compensate + function updateArrowLengths(): void { + // Arrows originate from center and extend past bounding box by arrowLength + // Total length = boundsHalfSize + arrowLength (in world space) + // In local space, divide by currentScale const keys: ArrowKey[] = ['x_pos', 'x_neg', 'y_pos', 'y_neg', 'z_pos', 'z_neg'] + for (const key of keys) { - const arrow = arrows[key] - const offset = arrowOffsets[key] - if (arrow) { - arrow.position.copy(offset).divideScalar(currentScale) - } + const shaft = shafts[key] + const cone = cones[key] + if (!shaft || !cone) continue + + // Total world-space length from center to arrow tip + const worldLength = boundsHalfSizes[key] + arrowLength + // Convert to local space (compensating for group scale) + const localLength = worldLength / currentScale + + // Shaft length = total length - cone head length + const shaftLength = localLength - arrowHeadLength + + // Update shaft: scale Y to desired length, position at midpoint + shaft.scale.y = Math.max(0.01, shaftLength) + shaft.position.y = shaftLength / 2 + + // Position cone at end of shaft + cone.position.y = shaftLength + arrowHeadLength / 2 } } @@ -198,8 +216,8 @@ export function createTranslateGizmo(options: TranslateGizmoOptions = {}): Trans currentScale = scale group.scale.setScalar(scale) - // Update arrow positions to compensate for new scale - updateArrowPositions() + // Update arrow lengths to compensate for new scale + updateArrowLengths() } function dispose(): void {