🚀 Auto-deploy: melhoria no snap e medição AR em 24/05/2026 02:00:33

This commit is contained in:
2026-05-24 02:00:33 +00:00
parent c839653a97
commit c8d51e0e1b
7 changed files with 170 additions and 26 deletions
+1 -1
View File
@@ -1,7 +1,7 @@
{ {
"name": "vite_react_shadcn_ts", "name": "vite_react_shadcn_ts",
"private": true, "private": true,
"version": "1.0.4", "version": "1.0.6",
"type": "module", "type": "module",
"scripts": { "scripts": {
"dev": "vite", "dev": "vite",
+21 -4
View File
@@ -59,6 +59,15 @@ export function findElementRoot(obj: THREE.Object3D | null): THREE.Object3D | nu
return fallback; return fallback;
} }
export function findModelId(obj: THREE.Object3D | null): string | undefined {
let cur: THREE.Object3D | null = obj;
while (cur) {
if (cur.userData?.modelId) return cur.userData.modelId as string;
cur = cur.parent;
}
return undefined;
}
function hasIfcAncestor(obj: THREE.Object3D): boolean { function hasIfcAncestor(obj: THREE.Object3D): boolean {
let cur = obj.parent; let cur = obj.parent;
while (cur) { while (cur) {
@@ -574,7 +583,9 @@ function SmartSnapHandler() {
} }
} }
if (bestHole) { if (bestHole) {
setSnapPoint(bestHole); // Find the measurement we matched
const matched = st.measurements.find(m => m.pointA.x === bestHole.x && m.pointA.y === bestHole.y && m.pointA.z === bestHole.z);
setSnapPoint({ ...bestHole, modelId: matched?.modelId });
return; return;
} }
@@ -585,7 +596,8 @@ function SmartSnapHandler() {
10 10
); );
if (snap) { if (snap) {
setSnapPoint({ x: snap.x, y: snap.y, z: snap.z }); const hitModelId = findModelId(hit.object);
setSnapPoint({ x: snap.x, y: snap.y, z: snap.z, modelId: hitModelId });
} else { } else {
setSnapPoint(null); setSnapPoint(null);
} }
@@ -653,11 +665,13 @@ function HoverDetector() {
// 1) Try circle fit from edge vertices around the hit (catches holes on flat faces) // 1) Try circle fit from edge vertices around the hit (catches holes on flat faces)
const circ = detectCircularEdgeAtPoint(hit.object, hit.point, camera, canvasSize, 50); const circ = detectCircularEdgeAtPoint(hit.object, hit.point, camera, canvasSize, 50);
const hitModelId = findModelId(hit.object);
if (circ) { if (circ) {
setHoverInfo({ setHoverInfo({
type: 'hole', type: 'hole',
position: [circ.center.x, circ.center.y, circ.center.z], position: [circ.center.x, circ.center.y, circ.center.z],
value: circ.diameterMM, value: circ.diameterMM,
modelId: hitModelId,
}); });
} else if (hit.faceIndex !== undefined && (() => { } else if (hit.faceIndex !== undefined && (() => {
const hole = detectHoleAtFace(hit.object, hit.faceIndex); const hole = detectHoleAtFace(hit.object, hit.faceIndex);
@@ -666,6 +680,7 @@ function HoverDetector() {
type: 'hole', type: 'hole',
position: [hole.center.x, hole.center.y, hole.center.z], position: [hole.center.x, hole.center.y, hole.center.z],
value: hole.diameterMM, value: hole.diameterMM,
modelId: hitModelId,
}); });
return true; return true;
} }
@@ -680,6 +695,7 @@ function HoverDetector() {
type: 'edge', type: 'edge',
position: [edge.midpoint.x, edge.midpoint.y, edge.midpoint.z], position: [edge.midpoint.x, edge.midpoint.y, edge.midpoint.z],
value: edge.lengthMM, value: edge.lengthMM,
modelId: hitModelId,
endpoints: { endpoints: {
a: { x: edge.a.x, y: edge.a.y, z: edge.a.z }, a: { x: edge.a.x, y: edge.a.y, z: edge.a.z },
b: { x: edge.b.x, y: edge.b.y, z: edge.b.z }, b: { x: edge.b.x, y: edge.b.y, z: edge.b.z },
@@ -763,7 +779,7 @@ function MeasureClickHandler() {
// Prefer snap (vertex snap, or hole-center snap from SmartSnapHandler) // Prefer snap (vertex snap, or hole-center snap from SmartSnapHandler)
if (st.snapPoint) { if (st.snapPoint) {
st.addMeasurePoint({ x: st.snapPoint.x, y: st.snapPoint.y, z: st.snapPoint.z }); st.addMeasurePoint({ x: st.snapPoint.x, y: st.snapPoint.y, z: st.snapPoint.z, modelId: st.snapPoint.modelId });
return; return;
} }
@@ -782,7 +798,8 @@ function MeasureClickHandler() {
return true; return true;
}); });
if (hit) { if (hit) {
st.addMeasurePoint({ x: hit.point.x, y: hit.point.y, z: hit.point.z }); const hitModelId = findModelId(hit.object);
st.addMeasurePoint({ x: hit.point.x, y: hit.point.y, z: hit.point.z, modelId: hitModelId });
} }
}; };
@@ -26,6 +26,31 @@ function isPickableModelMesh(o: THREE.Object3D): boolean {
return false; return false;
} }
function findModelId(obj: THREE.Object3D): string | undefined {
let cur: THREE.Object3D | null = obj;
while (cur) {
if (cur.userData?.modelId) return cur.userData.modelId as string;
cur = cur.parent;
}
return undefined;
}
function triggerHaptic(session: XRSession | null, handedness: 'left' | 'right', intensity = 0.5, duration = 40) {
if (!session) return;
try {
for (const source of session.inputSources) {
if (source.handedness === handedness) {
const gp = source.gamepad;
if (gp && gp.hapticActuators && gp.hapticActuators.length > 0) {
gp.hapticActuators[0].pulse(intensity, duration).catch(() => {});
}
}
}
} catch (e) {
console.warn('[XR][Haptic] erro ao vibrar controle:', e);
}
}
/** /**
* Right-controller trigger driven measurement for AR. * Right-controller trigger driven measurement for AR.
* *
@@ -65,6 +90,7 @@ export function XRControllerMeasure() {
const dwellPos = useRef(new THREE.Vector3(Infinity, Infinity, Infinity)); const dwellPos = useRef(new THREE.Vector3(Infinity, Infinity, Infinity));
const dwellStart = useRef(0); const dwellStart = useRef(0);
const dwellFired = useRef(false); const dwellFired = useRef(false);
const lastSnapKind = useRef<string | null>(null);
// Throttling: raycast + snap analysis are heavy on dense IFC models and // Throttling: raycast + snap analysis are heavy on dense IFC models and
// running them every frame at 72fps can stall the Quest right after // running them every frame at 72fps can stall the Quest right after
@@ -177,6 +203,12 @@ export function XRControllerMeasure() {
} }
} }
// ── Haptic feedback on snap state changes ────────────────────────
if (snappedPoint && snapKind !== 'surface' && lastSnapKind.current !== snapKind) {
triggerHaptic(session, 'right', 0.4, 15);
}
lastSnapKind.current = snappedPoint ? snapKind : 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(); const nowT = performance.now();
@@ -189,15 +221,18 @@ export function XRControllerMeasure() {
} else if (!dwellFired.current && nowT - dwellStart.current > 1000) { } else if (!dwellFired.current && nowT - dwellStart.current > 1000) {
dwellFired.current = true; dwellFired.current = true;
if (measureModeNow) { if (measureModeNow) {
const hitModelId = hit ? findModelId(hit.object) : undefined;
useModelStore.getState().registerHoverMeasurement({ useModelStore.getState().registerHoverMeasurement({
type: hoverDetected.kind, type: hoverDetected.kind,
value: hoverDetected.value, value: hoverDetected.value,
position: [hoverDetected.position.x, hoverDetected.position.y, hoverDetected.position.z], position: [hoverDetected.position.x, hoverDetected.position.y, hoverDetected.position.z],
modelId: hitModelId,
endpoints: hoverDetected.endpoints && { endpoints: hoverDetected.endpoints && {
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 },
}, },
}); });
triggerHaptic(session, 'right', 0.6, 50);
} }
} }
} else { } else {
@@ -270,10 +305,13 @@ export function XRControllerMeasure() {
} }
} }
} else if (snappedPoint) { } else if (snappedPoint) {
const hitModelId = hit ? findModelId(hit.object) : undefined;
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,
}); });
triggerHaptic(session, 'right', 0.8, 25);
} }
} else if (trigState.current && trigVal < TRIG_OFF) { } else if (trigState.current && trigVal < TRIG_OFF) {
trigState.current = false; trigState.current = false;
@@ -285,6 +323,7 @@ export function XRControllerMeasure() {
if (!aState.current && aVal > BTN_ON) { if (!aState.current && aVal > BTN_ON) {
aState.current = true; aState.current = true;
useModelStore.getState().undoLastMeasurement(); useModelStore.getState().undoLastMeasurement();
triggerHaptic(session, 'right', 0.5, 30);
} else if (aState.current && aVal < TRIG_OFF) { } else if (aState.current && aVal < TRIG_OFF) {
aState.current = false; aState.current = false;
} }
@@ -295,6 +334,7 @@ export function XRControllerMeasure() {
if (!bState.current && bVal > BTN_ON) { if (!bState.current && bVal > BTN_ON) {
bState.current = true; bState.current = true;
useModelStore.getState().clearMeasurements(); useModelStore.getState().clearMeasurements();
triggerHaptic(session, 'right', 0.7, 60);
} else if (bState.current && bVal < TRIG_OFF) { } else if (bState.current && bVal < TRIG_OFF) {
bState.current = false; bState.current = false;
} }
+75 -5
View File
@@ -1,9 +1,25 @@
import { useRef, ReactNode, useEffect } from 'react'; import { useRef, ReactNode, useEffect } from 'react';
import { useFrame } from '@react-three/fiber'; import { useFrame, useThree } from '@react-three/fiber';
import * as THREE from 'three'; import * as THREE from 'three';
import { useControllerGrab, ControllerGrabSnapshot } from '@/hooks/useControllerGrab'; import { useControllerGrab, ControllerGrabSnapshot } from '@/hooks/useControllerGrab';
import { useModelStore } from '@/stores/useModelStore'; import { useModelStore } from '@/stores/useModelStore';
function triggerHaptic(session: XRSession | null, handedness: 'left' | 'right', intensity = 0.5, duration = 40) {
if (!session) return;
try {
for (const source of session.inputSources) {
if (source.handedness === handedness) {
const gp = source.gamepad;
if (gp && gp.hapticActuators && gp.hapticActuators.length > 0) {
gp.hapticActuators[0].pulse(intensity, duration).catch(() => {});
}
}
}
} catch (e) {
console.warn('[XR][Haptic] erro ao vibrar controle no grab:', e);
}
}
interface XRGrabbableProps { interface XRGrabbableProps {
/** /**
* Quando true (padrão), o grip duplo aplica ZOOM uniforme além da rotação. * Quando true (padrão), o grip duplo aplica ZOOM uniforme além da rotação.
@@ -70,9 +86,12 @@ export function XRGrabbable({ allowScale = true, lockedActive = false, onGrabSta
single.current = null; single.current = null;
}, [scaleResetNonce]); }, [scaleResetNonce]);
useFrame(() => { const { gl } = useThree();
useFrame((_state, dt) => {
const group = groupRef.current; const group = groupRef.current;
if (!group) return; if (!group) return;
const session = gl.xr.getSession();
const snap: ControllerGrabSnapshot = grab.current; const snap: ControllerGrabSnapshot = grab.current;
const L = snap.left; const L = snap.left;
const R = snap.right; const R = snap.right;
@@ -95,6 +114,33 @@ export function XRGrabbable({ allowScale = true, lockedActive = false, onGrabSta
haloRef.current.visible = maxGrip > 0.05; haloRef.current.visible = maxGrip > 0.05;
} }
const commitGrabTransforms = () => {
const activeId = useModelStore.getState().activeModelId;
if (!activeId) return;
const active = useModelStore.getState().models.find(m => m.id === activeId);
if (!active) return;
const ft = { ...active.fineTuning };
ft.posX += group.position.x;
ft.posY += group.position.y;
ft.posZ += group.position.z;
ft.rotX += (group.rotation.x * 180) / Math.PI;
ft.rotY += (group.rotation.y * 180) / Math.PI;
ft.rotZ += (group.rotation.z * 180) / Math.PI;
ft.scale = (ft.scale ?? 1) * group.scale.x;
useModelStore.getState().setFineTuning(ft);
group.position.set(0, 0, 0);
group.rotation.set(0, 0, 0);
group.scale.set(1, 1, 1);
triggerHaptic(session, 'left', 0.25, 12);
triggerHaptic(session, 'right', 0.25, 12);
};
// ─── Two-hand mode (orbit + zoom, sempre) ──────────────────── // ─── Two-hand mode (orbit + zoom, sempre) ────────────────────
if (lActive && rActive) { if (lActive && rActive) {
_tmpVecL.setFromMatrixPosition(L.gripWorld); _tmpVecL.setFromMatrixPosition(L.gripWorld);
@@ -116,12 +162,13 @@ export function XRGrabbable({ allowScale = true, lockedActive = false, onGrabSta
}; };
single.current = null; single.current = null;
if (!everGrabbed.current) { everGrabbed.current = true; onGrabStart?.(); } if (!everGrabbed.current) { everGrabbed.current = true; onGrabStart?.(); }
triggerHaptic(session, 'left', 0.5, 30);
triggerHaptic(session, 'right', 0.5, 30);
console.log('[XR][grab] ◆ TWO-HAND start (orbit+zoom)'); console.log('[XR][grab] ◆ TWO-HAND start (orbit+zoom)');
} }
const d = dual.current; const d = dual.current;
const deltaQuat = new THREE.Quaternion().setFromUnitVectors(d.startAxis, axisNow); const deltaQuat = new THREE.Quaternion().setFromUnitVectors(d.startAxis, axisNow);
// ZOOM só quando allowScale=true. Caso contrário, mantém escala 1× (só orbita).
const scaleRatio = allowScale const scaleRatio = allowScale
? THREE.MathUtils.clamp(distNow / d.startDist, 0.1, 10) ? THREE.MathUtils.clamp(distNow / d.startDist, 0.1, 10)
: 1; : 1;
@@ -137,10 +184,27 @@ export function XRGrabbable({ allowScale = true, lockedActive = false, onGrabSta
.multiply(tToOrigin) .multiply(tToOrigin)
.multiply(d.startGroupWorld); .multiply(d.startGroupWorld);
applyWorldMatrixToLocal(group, m); // Decompose target world matrix
const targetPos = new THREE.Vector3();
const targetQuat = new THREE.Quaternion();
const targetScale = new THREE.Vector3();
let parentLocalMatrix = m;
if (group.parent) {
group.parent.updateMatrixWorld();
parentLocalMatrix = new THREE.Matrix4().copy(group.parent.matrixWorld).invert().multiply(m);
}
parentLocalMatrix.decompose(targetPos, targetQuat, targetScale);
// Apply smooth interpolation (anti-jitter dampening)
const t = Math.min(1.0, dt * 18);
group.position.lerp(targetPos, t);
group.quaternion.slerp(targetQuat, t);
group.scale.lerp(targetScale, t);
return; return;
} else if (dual.current) { } else if (dual.current) {
console.log('[XR][grab] ◆ TWO-HAND end'); console.log('[XR][grab] ◆ TWO-HAND end');
commitGrabTransforms();
dual.current = null; dual.current = null;
} }
@@ -156,6 +220,7 @@ export function XRGrabbable({ allowScale = true, lockedActive = false, onGrabSta
const offsetWorld = new THREE.Vector3().subVectors(groupWorldPos, ctrlPos); const offsetWorld = new THREE.Vector3().subVectors(groupWorldPos, ctrlPos);
single.current = { hand: activeHand, offsetWorld }; single.current = { hand: activeHand, offsetWorld };
if (!everGrabbed.current) { everGrabbed.current = true; onGrabStart?.(); } if (!everGrabbed.current) { everGrabbed.current = true; onGrabStart?.(); }
triggerHaptic(session, activeHand, 0.45, 25);
console.log(`[XR][grab] ● ONE-HAND start (${activeHand}) — pan only`); console.log(`[XR][grab] ● ONE-HAND start (${activeHand}) — pan only`);
} }
@@ -168,8 +233,13 @@ export function XRGrabbable({ allowScale = true, lockedActive = false, onGrabSta
const invParent = new THREE.Matrix4().copy(group.parent.matrixWorld).invert(); const invParent = new THREE.Matrix4().copy(group.parent.matrixWorld).invert();
targetWorldPos.applyMatrix4(invParent); targetWorldPos.applyMatrix4(invParent);
} }
group.position.copy(targetWorldPos);
// Amortecimento suave da translação
const t = Math.min(1.0, dt * 18);
group.position.lerp(targetWorldPos, t);
} else if (single.current) { } else if (single.current) {
console.log('[XR][grab] ● ONE-HAND end');
commitGrabTransforms();
single.current = null; single.current = null;
} }
}); });
+28 -13
View File
@@ -1,6 +1,6 @@
import { useEffect, useRef, useMemo, useState, useCallback } from 'react'; import { useEffect, useRef, useMemo, useState, useCallback } from 'react';
import { useFrame, useThree } from '@react-three/fiber'; import { useFrame, useThree } from '@react-three/fiber';
import { Grid } from '@react-three/drei'; import { Grid, Billboard } from '@react-three/drei';
import { GLTFLoader } from 'three/examples/jsm/loaders/GLTFLoader.js'; import { GLTFLoader } from 'three/examples/jsm/loaders/GLTFLoader.js';
import * as THREE from 'three'; import * as THREE from 'three';
import { useModelStore, type SceneModel } from '@/stores/useModelStore'; import { useModelStore, type SceneModel } from '@/stores/useModelStore';
@@ -8,6 +8,15 @@ import { findNearestVertex } from './SmartMeasure';
import { registerModelLocalGroup, unregisterModelLocalGroup } from '@/lib/modelTransforms'; import { registerModelLocalGroup, unregisterModelLocalGroup } from '@/lib/modelTransforms';
import { parseIFCtoThree } from '@/lib/convertIFC'; import { parseIFCtoThree } from '@/lib/convertIFC';
function findModelId(obj: THREE.Object3D): string | undefined {
let cur: THREE.Object3D | null = obj;
while (cur) {
if (cur.userData?.modelId) return cur.userData.modelId as string;
cur = cur.parent;
}
return undefined;
}
// ─── XRModel ─────────────────────────────────────────── // ─── XRModel ───────────────────────────────────────────
export function XRModel({ sceneModel }: { sceneModel: SceneModel }) { export function XRModel({ sceneModel }: { sceneModel: SceneModel }) {
const [rawScene, setRawScene] = useState<THREE.Object3D | null>(null); const [rawScene, setRawScene] = useState<THREE.Object3D | null>(null);
@@ -282,15 +291,17 @@ function XRMeasurementText({ text, color }: { text: string; color: string }) {
}, [text, color]); }, [text, color]);
return ( return (
<mesh renderOrder={999}> <Billboard follow={true}>
<planeGeometry args={[0.08, 0.02]} /> <mesh renderOrder={999}>
<meshBasicMaterial <planeGeometry args={[0.08, 0.02]} />
map={texture} <meshBasicMaterial
transparent map={texture}
depthTest={false} transparent
side={THREE.DoubleSide} depthTest={false}
/> side={THREE.DoubleSide}
</mesh> />
</mesh>
</Billboard>
); );
} }
@@ -444,7 +455,8 @@ export function XRSnapHandler() {
if (hit && hit.object instanceof THREE.Mesh) { if (hit && hit.object instanceof THREE.Mesh) {
const canvas = gl.domElement; const canvas = gl.domElement;
const snap = findNearestVertex(hit.object, hit.point, camera, { width: canvas.clientWidth, height: canvas.clientHeight }, 10); const snap = findNearestVertex(hit.object, hit.point, camera, { width: canvas.clientWidth, height: canvas.clientHeight }, 10);
setSnapPoint(snap ? { x: snap.x, y: snap.y, z: snap.z } : null); const hitModelId = findModelId(hit.object);
setSnapPoint(snap ? { x: snap.x, y: snap.y, z: snap.z, modelId: hitModelId } : null);
} else { } else {
setSnapPoint(null); setSnapPoint(null);
} }
@@ -455,7 +467,7 @@ export function XRSnapHandler() {
if (gl.xr.isPresenting) return; if (gl.xr.isPresenting) return;
const onClick = (e: MouseEvent) => { const onClick = (e: MouseEvent) => {
if (snapPoint) { if (snapPoint) {
addMeasurePoint({ x: snapPoint.x, y: snapPoint.y, z: snapPoint.z }); addMeasurePoint({ x: snapPoint.x, y: snapPoint.y, z: snapPoint.z, modelId: snapPoint.modelId });
return; return;
} }
const rect = gl.domElement.getBoundingClientRect(); const rect = gl.domElement.getBoundingClientRect();
@@ -464,7 +476,10 @@ export function XRSnapHandler() {
raycaster.setFromCamera(mouse, camera); raycaster.setFromCamera(mouse, camera);
const intersects = raycaster.intersectObjects(scene.children, true); const intersects = raycaster.intersectObjects(scene.children, true);
const hit = intersects.find(i => i.object instanceof THREE.Mesh && !i.object.userData.__edgeLine); const hit = intersects.find(i => i.object instanceof THREE.Mesh && !i.object.userData.__edgeLine);
if (hit) addMeasurePoint({ x: hit.point.x, y: hit.point.y, z: hit.point.z }); if (hit) {
const hitModelId = findModelId(hit.object);
addMeasurePoint({ x: hit.point.x, y: hit.point.y, z: hit.point.z, modelId: hitModelId });
}
}; };
gl.domElement.addEventListener('click', onClick); gl.domElement.addEventListener('click', onClick);
return () => gl.domElement.removeEventListener('click', onClick); return () => gl.domElement.removeEventListener('click', onClick);
+1 -1
View File
@@ -327,7 +327,7 @@ const Index = () => {
{/* Footer */} {/* Footer */}
<p className="mt-16 text-center font-mono text-xs text-muted-foreground/50"> <p className="mt-16 text-center font-mono text-xs text-muted-foreground/50">
TrackSteelXR v1.05 Q.C. Inspection TrackSteelXR v1.06 Q.C. Inspection
</p> </p>
{showLogs && ( {showLogs && (
+4 -2
View File
@@ -133,6 +133,7 @@ export interface MeasurePoint {
x: number; x: number;
y: number; y: number;
z: number; z: number;
modelId?: string;
} }
export interface Measurement { export interface Measurement {
@@ -155,6 +156,7 @@ export interface HoverInfo {
value: number; // mm (diameter or length) value: number; // mm (diameter or length)
/** 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 };
modelId?: string;
} }
export interface ChecklistItem { export interface ChecklistItem {
@@ -564,7 +566,7 @@ export const useModelStore = create<ModelStore>((set, get) => ({
const distanceWorldMM = Math.sqrt(dx * dx + dy * dy + dz * dz); const distanceWorldMM = Math.sqrt(dx * dx + dy * dy + dz * dz);
const factor = state.scaleRatio?.factor ?? 1; const factor = state.scaleRatio?.factor ?? 1;
const distanceMM = distanceWorldMM / factor; const distanceMM = distanceWorldMM / factor;
const modelId = state.activeModelId ?? undefined; const modelId = a.modelId ?? b.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);
@@ -602,7 +604,7 @@ 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 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 };