Initial project import

This commit is contained in:
drjones
2026-06-13 17:36:44 -07:00
commit ad2a18cc8d
18471 changed files with 4497570 additions and 0 deletions

View File

@@ -0,0 +1,99 @@
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<string, Vec3> } | 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<string, Vec3> = {};
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<string, Vec3> = {};
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 (
<PivotControls
matrix={matrix}
autoTransform={false}
fixed
scale={72}
lineWidth={3}
depthTest={false}
disableRotations
disableScaling
axisColors={['#f87171', '#4ade80', '#60a5fa']}
hoveredColor="#fbbf24"
onDragStart={onDragStart}
onDrag={onDrag}
onDragEnd={onDragEnd}
/>
);
}