import { useLayoutEffect, useMemo, useRef } from 'react'; import * as THREE from 'three'; import { PivotControls } from '@react-three/drei'; import type { Vec3 } from '../types'; import { snapValue, useBuilder } from '../store/builderStore'; /** * SelectionGizmo — a translate-only pivot gizmo at the selection centroid. * Dragging an axis arrow / plane slider moves every selected part rigidly in * all three dimensions (grid-snapped, floor-clamped), as one undo step. */ export function SelectionGizmo() { const selectedIds = useBuilder((s) => s.selectedIds); const parts = useBuilder((s) => s.parts); const draggingId = useBuilder((s) => s.draggingId); const placingType = useBuilder((s) => s.placingType); const tool = useBuilder((s) => s.tool); const matrix = useMemo(() => new THREE.Matrix4(), []); const draggingRef = useRef(false); const startRef = useRef<{ centroid: Vec3; positions: Record } | null>(null); const selected = selectedIds.map((id) => parts[id]).filter(Boolean); const centroid: Vec3 = [0, 0, 0]; for (const p of selected) { centroid[0] += p.position[0] / selected.length; centroid[1] += p.position[1] / selected.length; centroid[2] += p.position[2] / selected.length; } // Keep the gizmo parked at the selection centroid whenever it isn't the // thing doing the moving (selection changes, undo, inspector edits, ...). useLayoutEffect(() => { if (!draggingRef.current) matrix.setPosition(centroid[0], centroid[1], centroid[2]); }); if (!selected.length || draggingId || placingType || tool === 'measure') return null; const onDragStart = () => { draggingRef.current = true; const s = useBuilder.getState(); s.pushHistory(); s.setGizmoDragging(true); const positions: Record = {}; for (const id of s.selectedIds) { const p = s.parts[id]; if (p) positions[id] = [...p.position] as Vec3; } startRef.current = { centroid: [...centroid] as Vec3, positions }; }; const onDrag = (l: THREE.Matrix4) => { const start = startRef.current; if (!start) return; const s = useBuilder.getState(); matrix.copy(l); // gizmo follows the pointer const v = new THREE.Vector3().setFromMatrixPosition(l); const snap = (n: number) => (s.snapToGrid ? snapValue(n, s.gridSize) : n); const delta: Vec3 = [ snap(v.x - start.centroid[0]), snap(v.y - start.centroid[1]), snap(v.z - start.centroid[2]), ]; // Don't let any part of the group sink below the floor. let minY = Infinity; for (const p of Object.values(start.positions)) minY = Math.min(minY, p[1]); if (Number.isFinite(minY)) delta[1] = Math.max(delta[1], -minY); const updates: Record = {}; for (const [id, sp] of Object.entries(start.positions)) { updates[id] = [sp[0] + delta[0], sp[1] + delta[1], sp[2] + delta[2]]; } s.setPositions(updates); }; const onDragEnd = () => { draggingRef.current = false; startRef.current = null; useBuilder.getState().setGizmoDragging(false); }; return ( ); }