Ajustou cota e estabilizou snap

X-Lovable-Edit-ID: edt-c6d88e59-2709-4850-9df5-a42a0e7fce71
Co-authored-by: Reifonas <211114984+Reifonas@users.noreply.github.com>
This commit is contained in:
gpt-engineer-app[bot]
2026-05-24 19:17:50 +00:00
5 changed files with 118 additions and 40 deletions
+22 -8
View File
@@ -270,7 +270,7 @@ export function findNearestEdgeSegment(
const hitX = (hitScreen.x * 0.5 + 0.5) * canvasSize.width; const hitX = (hitScreen.x * 0.5 + 0.5) * canvasSize.width;
const hitY = (-hitScreen.y * 0.5 + 0.5) * canvasSize.height; const hitY = (-hitScreen.y * 0.5 + 0.5) * canvasSize.height;
const matrix = edgeLines ? edgeLines.matrixWorld.clone().premultiply(mesh.matrixWorld) : mesh.matrixWorld; const matrix = edgeLines ? edgeLines.matrixWorld : mesh.matrixWorld;
let bestDist = Infinity; let bestDist = Infinity;
let bestMid: THREE.Vector3 | null = null; let bestMid: THREE.Vector3 | null = null;
@@ -340,7 +340,7 @@ export function detectCircularEdgeAtPoint(
const posAttr = geo.attributes.position; const posAttr = geo.attributes.position;
if (!posAttr) return null; if (!posAttr) return null;
const matrix = edgeLines ? edgeLines.matrixWorld.clone().premultiply(mesh.matrixWorld) : mesh.matrixWorld; const matrix = edgeLines ? edgeLines.matrixWorld : mesh.matrixWorld;
const hitScreen = worldPoint.clone().project(camera); const hitScreen = worldPoint.clone().project(camera);
const hitX = (hitScreen.x * 0.5 + 0.5) * canvasSize.width; const hitX = (hitScreen.x * 0.5 + 0.5) * canvasSize.width;
@@ -443,6 +443,16 @@ const _ab = new THREE.Vector3();
const _ap = new THREE.Vector3(); const _ap = new THREE.Vector3();
const _closest = new THREE.Vector3(); const _closest = new THREE.Vector3();
function closestPointOnSegment3D(p: THREE.Vector3, a: THREE.Vector3, b: THREE.Vector3, target: THREE.Vector3): THREE.Vector3 {
_ab.subVectors(b, a);
_ap.subVectors(p, a);
const abLenSq = _ab.lengthSq();
if (abLenSq < 1e-6) return target.copy(a);
const t = THREE.MathUtils.clamp(_ap.dot(_ab) / abLenSq, 0, 1);
return target.copy(a).addScaledVector(_ab, t);
}
function pointToSegmentDistSq3D(p: THREE.Vector3, a: THREE.Vector3, b: THREE.Vector3): number { function pointToSegmentDistSq3D(p: THREE.Vector3, a: THREE.Vector3, b: THREE.Vector3): number {
_ab.subVectors(b, a); _ab.subVectors(b, a);
_ap.subVectors(p, a); _ap.subVectors(p, a);
@@ -503,7 +513,7 @@ export function findNearestEdgeSegment3D(
mesh: THREE.Mesh, mesh: THREE.Mesh,
worldPoint: THREE.Vector3, worldPoint: THREE.Vector3,
thresholdMeters: number = 0.05 thresholdMeters: number = 0.05
): { midpoint: THREE.Vector3; lengthMM: number; a: THREE.Vector3; b: THREE.Vector3 } | null { ): { midpoint: THREE.Vector3; snapPoint: THREE.Vector3; lengthMM: number; a: THREE.Vector3; b: THREE.Vector3 } | null {
let edgeLines: THREE.LineSegments | null = null; let edgeLines: THREE.LineSegments | null = null;
mesh.children.forEach(c => { mesh.children.forEach(c => {
if (c.userData.__edgeLine && c instanceof THREE.LineSegments) { if (c.userData.__edgeLine && c instanceof THREE.LineSegments) {
@@ -523,12 +533,13 @@ export function findNearestEdgeSegment3D(
const posAttr = geo.attributes.position; const posAttr = geo.attributes.position;
if (!posAttr) return null; if (!posAttr) return null;
const matrix = edgeLines ? edgeLines.matrixWorld.clone().premultiply(mesh.matrixWorld) : mesh.matrixWorld; const matrix = edgeLines ? edgeLines.matrixWorld : mesh.matrixWorld;
const invMatrix = new THREE.Matrix4().copy(matrix).invert(); const invMatrix = new THREE.Matrix4().copy(matrix).invert();
const localPoint = worldPoint.clone().applyMatrix4(invMatrix); const localPoint = worldPoint.clone().applyMatrix4(invMatrix);
let bestDistSq = Infinity; let bestDistSq = Infinity;
let bestIndex = -1; let bestIndex = -1;
const bestClosestLocal = new THREE.Vector3();
const thresholdSq = thresholdMeters * thresholdMeters; const thresholdSq = thresholdMeters * thresholdMeters;
const segCount = posAttr.count / 2; const segCount = posAttr.count / 2;
@@ -539,11 +550,13 @@ export function findNearestEdgeSegment3D(
aLocal.fromBufferAttribute(posAttr, i * 2); aLocal.fromBufferAttribute(posAttr, i * 2);
bLocal.fromBufferAttribute(posAttr, i * 2 + 1); bLocal.fromBufferAttribute(posAttr, i * 2 + 1);
const distSq = pointToSegmentDistSq3D(localPoint, aLocal, bLocal); closestPointOnSegment3D(localPoint, aLocal, bLocal, _closest);
const distSq = localPoint.distanceToSquared(_closest);
if (distSq < bestDistSq) { if (distSq < bestDistSq) {
bestDistSq = distSq; bestDistSq = distSq;
bestIndex = i; bestIndex = i;
bestClosestLocal.copy(_closest);
} }
} }
@@ -551,8 +564,9 @@ export function findNearestEdgeSegment3D(
const a = new THREE.Vector3().fromBufferAttribute(posAttr, bestIndex * 2).applyMatrix4(matrix); const a = new THREE.Vector3().fromBufferAttribute(posAttr, bestIndex * 2).applyMatrix4(matrix);
const b = new THREE.Vector3().fromBufferAttribute(posAttr, bestIndex * 2 + 1).applyMatrix4(matrix); const b = new THREE.Vector3().fromBufferAttribute(posAttr, bestIndex * 2 + 1).applyMatrix4(matrix);
const midpoint = a.clone().add(b).multiplyScalar(0.5); const midpoint = a.clone().add(b).multiplyScalar(0.5);
const snapPoint = bestClosestLocal.clone().applyMatrix4(matrix);
const lengthMM = a.distanceTo(b) * 1000; const lengthMM = a.distanceTo(b) * 1000;
return { midpoint, lengthMM, a, b }; return { midpoint, snapPoint, lengthMM, a, b };
} }
return null; return null;
} }
@@ -579,7 +593,7 @@ export function detectCircularEdgeAtPoint3D(
const posAttr = geo.attributes.position; const posAttr = geo.attributes.position;
if (!posAttr) return null; if (!posAttr) return null;
const matrix = edgeLines ? edgeLines.matrixWorld.clone().premultiply(mesh.matrixWorld) : mesh.matrixWorld; const matrix = edgeLines ? edgeLines.matrixWorld : mesh.matrixWorld;
const invMatrix = new THREE.Matrix4().copy(matrix).invert(); const invMatrix = new THREE.Matrix4().copy(matrix).invert();
const localPoint = worldPoint.clone().applyMatrix4(invMatrix); const localPoint = worldPoint.clone().applyMatrix4(invMatrix);
@@ -652,6 +666,6 @@ export function resolveSnap3D(
const v = findNearestVertex3D(mesh, hitPoint, vertexThresholdMeters); const v = findNearestVertex3D(mesh, hitPoint, vertexThresholdMeters);
if (v) return { point: v, type: 'vertex' }; if (v) return { point: v, type: 'vertex' };
const e = findNearestEdgeSegment3D(mesh, hitPoint, edgeThresholdMeters); const e = findNearestEdgeSegment3D(mesh, hitPoint, edgeThresholdMeters);
if (e) return { point: e.midpoint, type: 'edge' }; if (e) return { point: e.snapPoint, type: 'edge' };
return { point: hitPoint.clone(), type: 'surface' }; return { point: hitPoint.clone(), type: 'surface' };
} }
+41 -6
View File
@@ -26,6 +26,15 @@ function isPickableModelMesh(o: THREE.Object3D): boolean {
return false; return false;
} }
function getModelIdFromObject(o: THREE.Object3D): string | undefined {
let cur: THREE.Object3D | null = o;
while (cur) {
if (cur.userData?.modelId) return cur.userData.modelId as string;
cur = cur.parent;
}
return undefined;
}
/** /**
* Right-controller trigger driven measurement for AR. * Right-controller trigger driven measurement for AR.
* *
@@ -60,6 +69,7 @@ export function XRControllerMeasure() {
); );
const laserMat = useRef(new THREE.LineBasicMaterial({ color: '#22c55e', transparent: true, opacity: 0.7, depthTest: false })); const laserMat = useRef(new THREE.LineBasicMaterial({ color: '#22c55e', transparent: true, opacity: 0.7, depthTest: false }));
const tipColor = useRef(new THREE.Color('#22c55e')); const tipColor = useRef(new THREE.Color('#22c55e'));
const lockedSnap = useRef<{ point: THREE.Vector3; kind: 'vertex' | 'edge' | 'hole'; modelId?: string; lastSeen: number } | null>(null);
// Dwell detection for hover-based smart measurement (1 s) // Dwell detection for hover-based smart measurement (1 s)
const dwellPos = useRef(new THREE.Vector3(Infinity, Infinity, Infinity)); const dwellPos = useRef(new THREE.Vector3(Infinity, Infinity, Infinity));
@@ -118,11 +128,14 @@ export function XRControllerMeasure() {
let snappedPoint: THREE.Vector3 | null = null; let snappedPoint: THREE.Vector3 | null = null;
let snapKind: 'vertex' | 'edge' | 'surface' | 'hole' = 'surface'; let snapKind: 'vertex' | 'edge' | 'surface' | 'hole' = 'surface';
let hoverDetected: { kind: 'hole' | 'edge'; value: number; position: THREE.Vector3; endpoints?: { a: THREE.Vector3; b: THREE.Vector3 } } | null = null; let hoverDetected: { kind: 'hole' | 'edge'; value: number; position: THREE.Vector3; modelId?: string; endpoints?: { a: THREE.Vector3; b: THREE.Vector3 } } | null = null;
let hitModelId: string | undefined;
const nowT = performance.now();
const hits = raycaster.current.intersectObjects(scene.children, true); const hits = raycaster.current.intersectObjects(scene.children, true);
const hit = hits.find((h) => { const hit = hits.find((h) => {
const o = h.object; const o = h.object;
if (!(o instanceof THREE.Mesh)) return false;
if (!isPickableModelMesh(o)) return false; if (!isPickableModelMesh(o)) return false;
if (o.userData.__edgeLine) return false; if (o.userData.__edgeLine) return false;
if (o.geometry instanceof THREE.SphereGeometry) return false; if (o.geometry instanceof THREE.SphereGeometry) return false;
@@ -132,9 +145,10 @@ export function XRControllerMeasure() {
}); });
if (hit && hit.object instanceof THREE.Mesh) { if (hit && hit.object instanceof THREE.Mesh) {
hitModelId = getModelIdFromObject(hit.object);
const dist = tmpOrigin.current.distanceTo(hit.point); const dist = tmpOrigin.current.distanceTo(hit.point);
const vertexThreshold = Math.max(0.06, dist * 0.08); const vertexThreshold = Math.max(0.08, dist * 0.12);
const edgeThreshold = Math.max(0.08, dist * 0.10); const edgeThreshold = Math.max(0.10, dist * 0.14);
// Snap to existing registered hole centers first // Snap to existing registered hole centers first
const existing = useModelStore.getState().measurements; const existing = useModelStore.getState().measurements;
@@ -158,7 +172,7 @@ export function XRControllerMeasure() {
if (circle) { if (circle) {
snappedPoint = circle.center; snappedPoint = circle.center;
snapKind = 'hole'; snapKind = 'hole';
hoverDetected = { kind: 'hole', value: circle.diameterMM, position: circle.center }; hoverDetected = { kind: 'hole', value: circle.diameterMM, position: circle.center, modelId: hitModelId };
} else { } else {
const snap = resolveSnap3D(hit.object, hit.point, vertexThreshold, edgeThreshold); const snap = resolveSnap3D(hit.object, hit.point, vertexThreshold, edgeThreshold);
snappedPoint = snap.point; snappedPoint = snap.point;
@@ -167,7 +181,7 @@ export function XRControllerMeasure() {
const seg = findNearestEdgeSegment3D(hit.object, hit.point, edgeThreshold); const seg = findNearestEdgeSegment3D(hit.object, hit.point, edgeThreshold);
if (seg) { if (seg) {
const lenMM = seg.a.distanceTo(seg.b) * 1000; const lenMM = seg.a.distanceTo(seg.b) * 1000;
hoverDetected = { kind: 'edge', value: lenMM, position: seg.midpoint, endpoints: { a: seg.a, b: seg.b } }; hoverDetected = { kind: 'edge', value: lenMM, position: seg.midpoint, modelId: hitModelId, endpoints: { a: seg.a, b: seg.b } };
} }
} }
} }
@@ -177,9 +191,27 @@ export function XRControllerMeasure() {
} }
} }
const strongSnapKind = snapKind === 'vertex' || snapKind === 'edge' || snapKind === 'hole' ? snapKind : null;
if (snappedPoint && strongSnapKind) {
const locked = lockedSnap.current;
const sameTarget = locked && locked.kind === strongSnapKind && locked.modelId === hitModelId;
if (sameTarget && locked.point.distanceTo(snappedPoint) < (snapKind === 'edge' ? 0.08 : 0.12)) {
if (snapKind === 'edge') locked.point.lerp(snappedPoint, 0.25);
snappedPoint = locked.point.clone();
locked.lastSeen = nowT;
} else {
lockedSnap.current = { point: snappedPoint.clone(), kind: strongSnapKind, modelId: hitModelId, lastSeen: nowT };
}
} else if (snappedPoint && lockedSnap.current && lockedSnap.current.point.distanceTo(snappedPoint) < 0.12 && nowT - lockedSnap.current.lastSeen < 600) {
snapKind = lockedSnap.current.kind;
snappedPoint = lockedSnap.current.point.clone();
lockedSnap.current.lastSeen = nowT;
} else if (!snappedPoint || (lockedSnap.current && nowT - lockedSnap.current.lastSeen > 600)) {
lockedSnap.current = null;
}
// ── Dwell detection (1 s) to auto-register hovered hole/edge ───── // ── Dwell detection (1 s) to auto-register hovered hole/edge ─────
const measureModeNow = useModelStore.getState().measureMode; const measureModeNow = useModelStore.getState().measureMode;
const nowT = performance.now();
if (snappedPoint && hoverDetected) { if (snappedPoint && hoverDetected) {
const dist = dwellPos.current.distanceTo(snappedPoint); const dist = dwellPos.current.distanceTo(snappedPoint);
if (dist > 0.005) { if (dist > 0.005) {
@@ -197,6 +229,7 @@ export function XRControllerMeasure() {
a: { x: hoverDetected.endpoints.a.x, y: hoverDetected.endpoints.a.y, z: hoverDetected.endpoints.a.z }, a: { x: hoverDetected.endpoints.a.x, y: hoverDetected.endpoints.a.y, z: hoverDetected.endpoints.a.z },
b: { x: hoverDetected.endpoints.b.x, y: hoverDetected.endpoints.b.y, z: hoverDetected.endpoints.b.z }, b: { x: hoverDetected.endpoints.b.x, y: hoverDetected.endpoints.b.y, z: hoverDetected.endpoints.b.z },
}, },
modelId: hoverDetected.modelId,
}); });
} }
} }
@@ -246,6 +279,7 @@ export function XRControllerMeasure() {
const triggerHits = raycaster.current.intersectObjects(scene.children, true); const triggerHits = raycaster.current.intersectObjects(scene.children, true);
const triggerHit = triggerHits.find((h) => { const triggerHit = triggerHits.find((h) => {
const o = h.object; const o = h.object;
if (!(o instanceof THREE.Mesh)) return false;
if (!isPickableModelMesh(o)) return false; if (!isPickableModelMesh(o)) return false;
if (o.userData.__edgeLine) return false; if (o.userData.__edgeLine) return false;
if (o.geometry instanceof THREE.SphereGeometry) return false; if (o.geometry instanceof THREE.SphereGeometry) return false;
@@ -273,6 +307,7 @@ export function XRControllerMeasure() {
sendRemoteLog('info', 'Trigger físico pressionado no AR com snapPoint ativo', snappedPoint); sendRemoteLog('info', 'Trigger físico pressionado no AR com snapPoint ativo', snappedPoint);
st.addMeasurePoint({ st.addMeasurePoint({
x: snappedPoint.x, y: snappedPoint.y, z: snappedPoint.z, x: snappedPoint.x, y: snappedPoint.y, z: snappedPoint.z,
modelId: hitModelId,
}); });
} }
} else if (trigState.current && trigVal < TRIG_OFF) { } else if (trigState.current && trigVal < TRIG_OFF) {
+22
View File
@@ -33,3 +33,25 @@ export function worldToModelLocal(
g.worldToLocal(v); g.worldToLocal(v);
return { x: v.x, y: v.y, z: v.z }; return { x: v.x, y: v.y, z: v.z };
} }
export function modelLocalToWorld(
modelId: string | null | undefined,
p: { x: number; y: number; z: number },
): { x: number; y: number; z: number } | null {
const g = getModelLocalGroup(modelId);
if (!g) return null;
g.updateWorldMatrix(true, false);
const v = new THREE.Vector3(p.x, p.y, p.z);
g.localToWorld(v);
return { x: v.x, y: v.y, z: v.z };
}
export function getModelWorldScaleFactor(modelId: string | null | undefined): number {
const g = getModelLocalGroup(modelId);
if (!g) return 1;
g.updateWorldMatrix(true, false);
const s = new THREE.Vector3();
g.getWorldScale(s);
const factor = (Math.abs(s.x) + Math.abs(s.y) + Math.abs(s.z)) / 3;
return Number.isFinite(factor) && factor > 1e-6 ? factor : 1;
}
+5 -7
View File
@@ -383,15 +383,13 @@ export function XRSafeLine({ a, b }: { a: [number, number, number]; b: [number,
new THREE.Vector3(b[0], b[1], b[2]) new THREE.Vector3(b[0], b[1], b[2])
], [a, b]); ], [a, b]);
const geometry = useMemo(() => { const line = useMemo(() => {
return new THREE.BufferGeometry().setFromPoints(points); const geometry = new THREE.BufferGeometry().setFromPoints(points);
const material = new THREE.LineBasicMaterial({ color: '#22c55e' });
return new THREE.Line(geometry, material);
}, [points]); }, [points]);
return ( return <primitive object={line} />;
<line geometry={geometry}>
<lineBasicMaterial color="#22c55e" linewidth={2} />
</line>
);
} }
// Componente para renderizar medições que pertencem a um modelo específico // Componente para renderizar medições que pertencem a um modelo específico
+28 -19
View File
@@ -1,5 +1,5 @@
import { create } from 'zustand'; import { create } from 'zustand';
import { worldToModelLocal } from '@/lib/modelTransforms'; import { getModelWorldScaleFactor, worldToModelLocal } from '@/lib/modelTransforms';
import { sendRemoteLog } from '@/lib/remoteLogger'; import { sendRemoteLog } from '@/lib/remoteLogger';
// ── Persistência de placement (fineTuning + escala) e flags WebXR ───── // ── Persistência de placement (fineTuning + escala) e flags WebXR ─────
@@ -133,6 +133,8 @@ export interface MeasurePoint {
x: number; x: number;
y: number; y: number;
z: number; z: number;
/** Model hit by this point, used so saved dimensions attach to the right piece. */
modelId?: string;
} }
export interface Measurement { export interface Measurement {
@@ -153,6 +155,7 @@ export interface HoverInfo {
type: 'hole' | 'edge'; type: 'hole' | 'edge';
position: [number, number, number]; position: [number, number, number];
value: number; // mm (diameter or length) value: number; // mm (diameter or length)
modelId?: string;
/** For edges: the two endpoints in world coords. */ /** For edges: the two endpoints in world coords. */
endpoints?: { a: MeasurePoint; b: MeasurePoint }; endpoints?: { a: MeasurePoint; b: MeasurePoint };
} }
@@ -558,27 +561,25 @@ export const useModelStore = create<ModelStore>((set, get) => ({
const points = [...state.measurePoints, p]; const points = [...state.measurePoints, p];
if (points.length === 2) { if (points.length === 2) {
const [a, b] = points; const [a, b] = points;
const dx = (a.x - b.x) * 1000; const modelId = (a.modelId && a.modelId === b.modelId ? a.modelId : state.activeModelId) ?? undefined;
const dy = (a.y - b.y) * 1000;
const dz = (a.z - b.z) * 1000;
const distanceWorldMM = Math.sqrt(dx * dx + dy * dy + dz * dz);
const factor = state.scaleRatio?.factor ?? 1;
const distanceMM = distanceWorldMM / factor;
const modelId = state.activeModelId ?? undefined;
const aConv = modelId ? worldToModelLocal(modelId, a) : null; const aConv = modelId ? worldToModelLocal(modelId, a) : null;
const bConv = modelId ? worldToModelLocal(modelId, b) : null; const bConv = modelId ? worldToModelLocal(modelId, b) : null;
const attached = !!(aConv && bConv); const attached = !!(aConv && bConv);
const pa = attached ? aConv! : a;
const pb = attached ? bConv! : b;
const dx = (pa.x - pb.x) * 1000;
const dy = (pa.y - pb.y) * 1000;
const dz = (pa.z - pb.z) * 1000;
const distanceMM = Math.sqrt(dx * dx + dy * dy + dz * dz);
sendRemoteLog('info', `addMeasurePoint: calculando distância`, { sendRemoteLog('info', `addMeasurePoint: calculando distância`, {
distanceWorldMM,
distanceMM, distanceMM,
factor,
modelId, modelId,
attached attached
}); });
const measurement: Measurement = { const measurement: Measurement = {
id: `m_${Date.now()}`, id: `m_${Date.now()}`,
pointA: attached ? aConv! : a, pointA: pa,
pointB: attached ? bConv! : b, pointB: pb,
distanceMM, distanceMM,
modelId: attached ? modelId : undefined, modelId: attached ? modelId : undefined,
}; };
@@ -602,7 +603,8 @@ export const useModelStore = create<ModelStore>((set, get) => ({
return { measurements: state.measurements.slice(0, -1) }; return { measurements: state.measurements.slice(0, -1) };
}), }),
registerHoverMeasurement: (info) => set((state) => { registerHoverMeasurement: (info) => set((state) => {
const modelId = state.activeModelId ?? undefined; const modelId = info.modelId ?? state.activeModelId ?? undefined;
const modelScale = modelId ? getModelWorldScaleFactor(modelId) : 1;
const toLocal = (p: MeasurePoint): { point: MeasurePoint; attached: boolean } => { const toLocal = (p: MeasurePoint): { point: MeasurePoint; attached: boolean } => {
const conv = modelId ? worldToModelLocal(modelId, p) : null; const conv = modelId ? worldToModelLocal(modelId, p) : null;
return conv ? { point: conv, attached: true } : { point: p, attached: false }; return conv ? { point: conv, attached: true } : { point: p, attached: false };
@@ -613,13 +615,14 @@ export const useModelStore = create<ModelStore>((set, get) => ({
const cw: MeasurePoint = { x: info.position[0], y: info.position[1], z: info.position[2] }; const cw: MeasurePoint = { x: info.position[0], y: info.position[1], z: info.position[2] };
const c = toLocal(cw); const c = toLocal(cw);
attached = c.attached; attached = c.attached;
const valueMM = attached ? info.value / modelScale : info.value;
measurement = { measurement = {
id: `m_${Date.now()}`, id: `m_${Date.now()}`,
pointA: c.point, pointA: c.point,
pointB: c.point, pointB: c.point,
distanceMM: info.value, distanceMM: valueMM,
kind: 'hole', kind: 'hole',
label: `Ø ${info.value.toFixed(1)}`, label: `Ø ${valueMM.toFixed(1)}`,
modelId: attached ? modelId : undefined, modelId: attached ? modelId : undefined,
}; };
} else { } else {
@@ -628,13 +631,19 @@ export const useModelStore = create<ModelStore>((set, get) => ({
const a = toLocal(aw); const a = toLocal(aw);
const b = toLocal(bw); const b = toLocal(bw);
attached = a.attached && b.attached; attached = a.attached && b.attached;
const pa = attached ? a.point : aw;
const pb = attached ? b.point : bw;
const dx = (pa.x - pb.x) * 1000;
const dy = (pa.y - pb.y) * 1000;
const dz = (pa.z - pb.z) * 1000;
const valueMM = Math.sqrt(dx * dx + dy * dy + dz * dz);
measurement = { measurement = {
id: `m_${Date.now()}`, id: `m_${Date.now()}`,
pointA: attached ? a.point : aw, pointA: pa,
pointB: attached ? b.point : bw, pointB: pb,
distanceMM: info.value, distanceMM: valueMM,
kind: 'edge', kind: 'edge',
label: `${info.value.toFixed(1)} mm`, label: `${valueMM.toFixed(1)} mm`,
modelId: attached ? modelId : undefined, modelId: attached ? modelId : undefined,
}; };
} }