diff --git a/src/components/three/ModelViewer.tsx b/src/components/three/ModelViewer.tsx
index a46f032..47f0dbe 100644
--- a/src/components/three/ModelViewer.tsx
+++ b/src/components/three/ModelViewer.tsx
@@ -352,15 +352,70 @@ function MeasurementLabel({ position, text, variant, fixed = false }: { position
);
}
-/** Renders all measurements, snap point, and hover info */
+/** Renders a single measurement's geometry + label.
+ * Coordinates are interpreted in whichever frame the component is mounted in. */
+function MeasurementGraphic({ m }: { m: import('@/stores/useModelStore').Measurement }) {
+ const a: [number, number, number] = [m.pointA.x, m.pointA.y, m.pointA.z];
+ const b: [number, number, number] = [m.pointB.x, m.pointB.y, m.pointB.z];
+ const mid: [number, number, number] = [
+ (a[0] + b[0]) / 2,
+ (a[1] + b[1]) / 2,
+ (a[2] + b[2]) / 2,
+ ];
+
+ if (m.kind === 'hole') {
+ return (
+
+
+
+
+ );
+ }
+
+ if (m.kind === 'edge') {
+ return (
+
+
+
+
+
+
+ );
+ }
+
+ return (
+
+
+
+
+
+
+ );
+}
+
+/** Renders measurements attached to a given model, in that model's local frame.
+ * Mounted inside GLBModel so they follow the model when it is repositioned. */
+function ModelMeasurements({ modelId }: { modelId: string }) {
+ const measurements = useModelStore((s) => s.measurements);
+ const attached = measurements.filter((m) => m.modelId === modelId);
+ if (attached.length === 0) return null;
+ return (
+ <>
+ {attached.map((m) => (
+
+ ))}
+ >
+ );
+}
+
+/** Renders snap indicator, hover tooltip, pending point and any *legacy*
+ * world-frame measurements that aren't attached to a specific model. */
function MeasurementOverlay() {
const measurements = useModelStore((s) => s.measurements);
const measurePoints = useModelStore((s) => s.measurePoints);
const snapPoint = useModelStore((s) => s.snapPoint);
const hoverInfo = useModelStore((s) => s.hoverInfo);
const measureMode = useModelStore((s) => s.measureMode);
- const scaleRatio = useModelStore((s) => s.scaleRatio);
- const factor = scaleRatio?.factor ?? 1;
return (
<>
@@ -381,55 +436,15 @@ function MeasurementOverlay() {
/>
)}
- {/* Pending first point */}
+ {/* Pending first point (always in world coords) */}
{measurePoints.length === 1 && (
)}
- {/* Completed measurements */}
- {measurements.map((m) => {
- const a: [number, number, number] = [m.pointA.x, m.pointA.y, m.pointA.z];
- const b: [number, number, number] = [m.pointB.x, m.pointB.y, m.pointB.z];
- const mid: [number, number, number] = [
- (a[0] + b[0]) / 2,
- (a[1] + b[1]) / 2,
- (a[2] + b[2]) / 2,
- ];
-
- if (m.kind === 'hole') {
- return (
-
-
-
-
- );
- }
-
- if (m.kind === 'edge') {
- return (
-
-
-
-
-
-
- );
- }
-
- return (
-
-
-
-
-
-
- );
- })}
+ {/* Legacy world-frame measurements (no modelId) */}
+ {measurements.filter((m) => !m.modelId).map((m) => (
+
+ ))}
>
);
}