From 717e0c6af8e0e1288e9f2953c98ba98e177623f6 Mon Sep 17 00:00:00 2001 From: "gpt-engineer-app[bot]" <159125892+gpt-engineer-app[bot]@users.noreply.github.com> Date: Thu, 21 May 2026 14:03:52 +0000 Subject: [PATCH] Changes Co-authored-by: Reifonas <211114984+Reifonas@users.noreply.github.com> --- src/components/three/XRControllerMeasure.tsx | 94 ++++++++++++++++---- 1 file changed, 76 insertions(+), 18 deletions(-) diff --git a/src/components/three/XRControllerMeasure.tsx b/src/components/three/XRControllerMeasure.tsx index 01e0245..668bb68 100644 --- a/src/components/three/XRControllerMeasure.tsx +++ b/src/components/three/XRControllerMeasure.tsx @@ -101,32 +101,90 @@ export function XRControllerMeasure() { }); let snappedPoint: THREE.Vector3 | null = null; - let snapKind: 'vertex' | 'edge' | 'surface' = '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; if (hit && hit.object instanceof THREE.Mesh) { - if (snapEnabled) { - // Use a virtual canvas size based on session — fall back to current renderer size - const size = gl.getSize(new THREE.Vector2()); - const fakeCam = new THREE.PerspectiveCamera(60, 1, 0.01, 100); - fakeCam.position.copy(tmpOrigin.current); - fakeCam.quaternion.copy(tmpQuat.current); - fakeCam.updateMatrixWorld(true); - const snap = resolveSnap( - hit.object, - hit.point, - fakeCam, - { width: size.x || 1024, height: size.y || 1024 }, - 14, - 18 - ); - snappedPoint = snap.point; - snapKind = snap.type; + const size = gl.getSize(new THREE.Vector2()); + const canvasSize = { width: size.x || 1024, height: size.y || 1024 }; + const fakeCam = new THREE.PerspectiveCamera(60, 1, 0.01, 100); + fakeCam.position.copy(tmpOrigin.current); + fakeCam.quaternion.copy(tmpQuat.current); + fakeCam.updateMatrixWorld(true); + + // Snap to existing registered hole centers first (within ~30px screen) + const existing = useModelStore.getState().measurements; + let bestHoleCenter: THREE.Vector3 | null = null; + let bestHolePx = Infinity; + const hitProj = hit.point.clone().project(fakeCam); + const hx = (hitProj.x * 0.5 + 0.5) * canvasSize.width; + const hy = (-hitProj.y * 0.5 + 0.5) * canvasSize.height; + for (const m of existing) { + if (m.kind !== 'hole') continue; + const c = new THREE.Vector3(m.pointA.x, m.pointA.y, m.pointA.z); + const p = c.clone().project(fakeCam); + const sx = (p.x * 0.5 + 0.5) * canvasSize.width; + const sy = (-p.y * 0.5 + 0.5) * canvasSize.height; + const d = Math.hypot(sx - hx, sy - hy); + if (d < 30 && d < bestHolePx) { bestHolePx = d; bestHoleCenter = c; } + } + + if (snapEnabled && bestHoleCenter) { + snappedPoint = bestHoleCenter; + snapKind = 'hole'; + } else if (snapEnabled) { + // Try detecting a circular edge (hole) at hit + const circle = detectCircularEdgeAtPoint(hit.object, hit.point, fakeCam, canvasSize, 60); + if (circle) { + snappedPoint = circle.center; + snapKind = 'hole'; + hoverDetected = { kind: 'hole', value: circle.diameterMM, position: circle.center }; + } else { + const snap = resolveSnap(hit.object, hit.point, fakeCam, canvasSize, 14, 18); + snappedPoint = snap.point; + snapKind = snap.type; + if (snap.type === 'edge') { + const seg = findNearestEdgeSegment(hit.object, hit.point, fakeCam, canvasSize, 18); + 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 } }; + } + } + } } else { snappedPoint = hit.point.clone(); snapKind = 'surface'; } } + // ── 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) { + dwellPos.current.copy(snappedPoint); + dwellStart.current = nowT; + dwellFired.current = false; + } else if (!dwellFired.current && nowT - dwellStart.current > 1000) { + dwellFired.current = true; + if (measureModeNow) { + useModelStore.getState().registerHoverMeasurement({ + kind: hoverDetected.kind, + value: hoverDetected.value, + position: { x: hoverDetected.position.x, y: hoverDetected.position.y, z: hoverDetected.position.z }, + endpoints: hoverDetected.endpoints && { + 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 }, + }, + }); + } + } + } else { + dwellFired.current = false; + dwellStart.current = nowT; + } + // ── Update laser visual ─────────────────────────────────────────── if (laserRef.current && tipRef.current) { const end = snappedPoint ?? tmpOrigin.current.clone().add(tmpDir.current.clone().multiplyScalar(MAX_RAY));