diff --git a/src/components/three/SmartMeasure.ts b/src/components/three/SmartMeasure.ts index fda52d5..90d7fa5 100644 --- a/src/components/three/SmartMeasure.ts +++ b/src/components/three/SmartMeasure.ts @@ -270,7 +270,7 @@ export function findNearestEdgeSegment( const hitX = (hitScreen.x * 0.5 + 0.5) * canvasSize.width; 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 bestMid: THREE.Vector3 | null = null; @@ -340,7 +340,7 @@ export function detectCircularEdgeAtPoint( const posAttr = geo.attributes.position; 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 hitX = (hitScreen.x * 0.5 + 0.5) * canvasSize.width; @@ -443,6 +443,16 @@ const _ab = new THREE.Vector3(); const _ap = 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 { _ab.subVectors(b, a); _ap.subVectors(p, a); @@ -503,7 +513,7 @@ export function findNearestEdgeSegment3D( mesh: THREE.Mesh, worldPoint: THREE.Vector3, 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; mesh.children.forEach(c => { if (c.userData.__edgeLine && c instanceof THREE.LineSegments) { @@ -523,12 +533,13 @@ export function findNearestEdgeSegment3D( const posAttr = geo.attributes.position; 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 localPoint = worldPoint.clone().applyMatrix4(invMatrix); let bestDistSq = Infinity; let bestIndex = -1; + const bestClosestLocal = new THREE.Vector3(); const thresholdSq = thresholdMeters * thresholdMeters; const segCount = posAttr.count / 2; @@ -539,11 +550,13 @@ export function findNearestEdgeSegment3D( aLocal.fromBufferAttribute(posAttr, i * 2); 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) { bestDistSq = distSq; bestIndex = i; + bestClosestLocal.copy(_closest); } } @@ -551,8 +564,9 @@ export function findNearestEdgeSegment3D( const a = new THREE.Vector3().fromBufferAttribute(posAttr, bestIndex * 2).applyMatrix4(matrix); const b = new THREE.Vector3().fromBufferAttribute(posAttr, bestIndex * 2 + 1).applyMatrix4(matrix); const midpoint = a.clone().add(b).multiplyScalar(0.5); + const snapPoint = bestClosestLocal.clone().applyMatrix4(matrix); const lengthMM = a.distanceTo(b) * 1000; - return { midpoint, lengthMM, a, b }; + return { midpoint, snapPoint, lengthMM, a, b }; } return null; } @@ -579,7 +593,7 @@ export function detectCircularEdgeAtPoint3D( const posAttr = geo.attributes.position; 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 localPoint = worldPoint.clone().applyMatrix4(invMatrix); @@ -652,6 +666,6 @@ export function resolveSnap3D( const v = findNearestVertex3D(mesh, hitPoint, vertexThresholdMeters); if (v) return { point: v, type: 'vertex' }; 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' }; } diff --git a/src/components/three/XRControllerMeasure.tsx b/src/components/three/XRControllerMeasure.tsx index de52e40..ffb0dff 100644 --- a/src/components/three/XRControllerMeasure.tsx +++ b/src/components/three/XRControllerMeasure.tsx @@ -26,6 +26,15 @@ function isPickableModelMesh(o: THREE.Object3D): boolean { 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. * @@ -60,6 +69,7 @@ export function XRControllerMeasure() { ); const laserMat = useRef(new THREE.LineBasicMaterial({ color: '#22c55e', transparent: true, opacity: 0.7, depthTest: false })); 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) const dwellPos = useRef(new THREE.Vector3(Infinity, Infinity, Infinity)); @@ -118,11 +128,14 @@ export function XRControllerMeasure() { let snappedPoint: THREE.Vector3 | null = null; 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 hit = hits.find((h) => { const o = h.object; + if (!(o instanceof THREE.Mesh)) return false; if (!isPickableModelMesh(o)) return false; if (o.userData.__edgeLine) return false; if (o.geometry instanceof THREE.SphereGeometry) return false; @@ -132,9 +145,10 @@ export function XRControllerMeasure() { }); if (hit && hit.object instanceof THREE.Mesh) { + hitModelId = getModelIdFromObject(hit.object); const dist = tmpOrigin.current.distanceTo(hit.point); - const vertexThreshold = Math.max(0.06, dist * 0.08); - const edgeThreshold = Math.max(0.08, dist * 0.10); + const vertexThreshold = Math.max(0.08, dist * 0.12); + const edgeThreshold = Math.max(0.10, dist * 0.14); // Snap to existing registered hole centers first const existing = useModelStore.getState().measurements; @@ -158,7 +172,7 @@ export function XRControllerMeasure() { if (circle) { snappedPoint = circle.center; snapKind = 'hole'; - hoverDetected = { kind: 'hole', value: circle.diameterMM, position: circle.center }; + hoverDetected = { kind: 'hole', value: circle.diameterMM, position: circle.center, modelId: hitModelId }; } else { const snap = resolveSnap3D(hit.object, hit.point, vertexThreshold, edgeThreshold); snappedPoint = snap.point; @@ -167,7 +181,7 @@ export function XRControllerMeasure() { const seg = findNearestEdgeSegment3D(hit.object, hit.point, edgeThreshold); if (seg) { 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 ───── const measureModeNow = useModelStore.getState().measureMode; - const nowT = performance.now(); if (snappedPoint && hoverDetected) { const dist = dwellPos.current.distanceTo(snappedPoint); 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 }, 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 triggerHit = triggerHits.find((h) => { const o = h.object; + if (!(o instanceof THREE.Mesh)) return false; if (!isPickableModelMesh(o)) return false; if (o.userData.__edgeLine) 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); st.addMeasurePoint({ x: snappedPoint.x, y: snappedPoint.y, z: snappedPoint.z, + modelId: hitModelId, }); } } else if (trigState.current && trigVal < TRIG_OFF) { diff --git a/src/lib/modelTransforms.ts b/src/lib/modelTransforms.ts index 58d8807..c3ef211 100644 --- a/src/lib/modelTransforms.ts +++ b/src/lib/modelTransforms.ts @@ -33,3 +33,25 @@ export function worldToModelLocal( g.worldToLocal(v); 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; +} diff --git a/src/pages/XRSession.tsx b/src/pages/XRSession.tsx index cc59e3d..6448e09 100644 --- a/src/pages/XRSession.tsx +++ b/src/pages/XRSession.tsx @@ -383,15 +383,13 @@ export function XRSafeLine({ a, b }: { a: [number, number, number]; b: [number, new THREE.Vector3(b[0], b[1], b[2]) ], [a, b]); - const geometry = useMemo(() => { - return new THREE.BufferGeometry().setFromPoints(points); + const line = useMemo(() => { + const geometry = new THREE.BufferGeometry().setFromPoints(points); + const material = new THREE.LineBasicMaterial({ color: '#22c55e' }); + return new THREE.Line(geometry, material); }, [points]); - return ( - - - - ); + return ; } // Componente para renderizar medições que pertencem a um modelo específico diff --git a/src/stores/useModelStore.ts b/src/stores/useModelStore.ts index e07045f..a7e1a41 100644 --- a/src/stores/useModelStore.ts +++ b/src/stores/useModelStore.ts @@ -1,5 +1,5 @@ import { create } from 'zustand'; -import { worldToModelLocal } from '@/lib/modelTransforms'; +import { getModelWorldScaleFactor, worldToModelLocal } from '@/lib/modelTransforms'; import { sendRemoteLog } from '@/lib/remoteLogger'; // ── Persistência de placement (fineTuning + escala) e flags WebXR ───── @@ -133,6 +133,8 @@ export interface MeasurePoint { x: number; y: number; z: number; + /** Model hit by this point, used so saved dimensions attach to the right piece. */ + modelId?: string; } export interface Measurement { @@ -153,6 +155,7 @@ export interface HoverInfo { type: 'hole' | 'edge'; position: [number, number, number]; value: number; // mm (diameter or length) + modelId?: string; /** For edges: the two endpoints in world coords. */ endpoints?: { a: MeasurePoint; b: MeasurePoint }; } @@ -558,27 +561,25 @@ export const useModelStore = create((set, get) => ({ const points = [...state.measurePoints, p]; if (points.length === 2) { const [a, b] = points; - const dx = (a.x - b.x) * 1000; - 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 modelId = (a.modelId && a.modelId === b.modelId ? a.modelId : state.activeModelId) ?? undefined; const aConv = modelId ? worldToModelLocal(modelId, a) : null; const bConv = modelId ? worldToModelLocal(modelId, b) : null; 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`, { - distanceWorldMM, distanceMM, - factor, modelId, attached }); const measurement: Measurement = { id: `m_${Date.now()}`, - pointA: attached ? aConv! : a, - pointB: attached ? bConv! : b, + pointA: pa, + pointB: pb, distanceMM, modelId: attached ? modelId : undefined, }; @@ -602,7 +603,8 @@ export const useModelStore = create((set, get) => ({ return { measurements: state.measurements.slice(0, -1) }; }), 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 conv = modelId ? worldToModelLocal(modelId, p) : null; return conv ? { point: conv, attached: true } : { point: p, attached: false }; @@ -613,13 +615,14 @@ export const useModelStore = create((set, get) => ({ const cw: MeasurePoint = { x: info.position[0], y: info.position[1], z: info.position[2] }; const c = toLocal(cw); attached = c.attached; + const valueMM = attached ? info.value / modelScale : info.value; measurement = { id: `m_${Date.now()}`, pointA: c.point, pointB: c.point, - distanceMM: info.value, + distanceMM: valueMM, kind: 'hole', - label: `Ø ${info.value.toFixed(1)}`, + label: `Ø ${valueMM.toFixed(1)}`, modelId: attached ? modelId : undefined, }; } else { @@ -628,13 +631,19 @@ export const useModelStore = create((set, get) => ({ const a = toLocal(aw); const b = toLocal(bw); 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 = { id: `m_${Date.now()}`, - pointA: attached ? a.point : aw, - pointB: attached ? b.point : bw, - distanceMM: info.value, + pointA: pa, + pointB: pb, + distanceMM: valueMM, kind: 'edge', - label: `${info.value.toFixed(1)} mm`, + label: `${valueMM.toFixed(1)} mm`, modelId: attached ? modelId : undefined, }; }