From 22e3ede63c66109cf92b8573da84d67f1680d63a 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 21:24:35 +0000 Subject: [PATCH 1/6] Changes Co-authored-by: Reifonas <211114984+Reifonas@users.noreply.github.com> --- src/components/three/XRControllerMeasure.tsx | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/src/components/three/XRControllerMeasure.tsx b/src/components/three/XRControllerMeasure.tsx index d5ef45c..f4a3768 100644 --- a/src/components/three/XRControllerMeasure.tsx +++ b/src/components/three/XRControllerMeasure.tsx @@ -49,12 +49,21 @@ export function XRControllerMeasure() { const dwellStart = useRef(0); const dwellFired = useRef(false); + // Throttling: raycast + snap analysis are heavy on dense IFC models and + // running them every frame at 72fps can stall the Quest right after + // toggling "Medir" from the Ferramentas tab. We cap heavy work to ~24Hz. + // Trigger/A/B/L-trigger polling continues every frame for responsiveness. + const heavyFrame = useRef(0); + const cachedSnappedPoint = useRef(null); + const cachedSnapKind = useRef<'vertex' | 'edge' | 'surface' | 'hole'>('surface'); + useFrame((_state, _dt, frame: XRFrame | undefined) => { const measureMode = useModelStore.getState().measureMode; const selectionMode = useModelStore.getState().selectionMode; if (laserRef.current) laserRef.current.visible = false; if (tipRef.current) tipRef.current.visible = false; if ((!measureMode && !selectionMode) || !frame) return; + const doHeavy = (heavyFrame.current++ % 3) === 0; const session = frame.session; const refSpace = gl.xr.getReferenceSpace(); From 16c1e7b755eb149dd81d204bd58c59f3d54c3a85 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 21:25:04 +0000 Subject: [PATCH 2/6] Changes Co-authored-by: Reifonas <211114984+Reifonas@users.noreply.github.com> --- src/components/three/XRControllerMeasure.tsx | 119 ++++++++++--------- 1 file changed, 64 insertions(+), 55 deletions(-) diff --git a/src/components/three/XRControllerMeasure.tsx b/src/components/three/XRControllerMeasure.tsx index f4a3768..bda9008 100644 --- a/src/components/three/XRControllerMeasure.tsx +++ b/src/components/three/XRControllerMeasure.tsx @@ -104,72 +104,81 @@ export function XRControllerMeasure() { raycaster.current.set(tmpOrigin.current, tmpDir.current); raycaster.current.far = MAX_RAY; - 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 (o.userData.__edgeLine) return false; - if (o.geometry instanceof THREE.SphereGeometry) return false; - if (o.geometry instanceof THREE.RingGeometry) return false; - if (o.geometry instanceof THREE.PlaneGeometry) return false; - return true; - }); - 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; - if (hit && hit.object instanceof THREE.Mesh) { - 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); + if (doHeavy) { + 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 (o.userData.__edgeLine) return false; + if (o.geometry instanceof THREE.SphereGeometry) return false; + if (o.geometry instanceof THREE.RingGeometry) return false; + if (o.geometry instanceof THREE.PlaneGeometry) return false; + return 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 (hit && hit.object instanceof THREE.Mesh) { + 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); - 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; + // 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'; - 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 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'; } - } else { - snappedPoint = hit.point.clone(); - snapKind = 'surface'; } + // Cache for non-heavy frames so the laser + trigger still have a target + cachedSnappedPoint.current = snappedPoint ? snappedPoint.clone() : null; + cachedSnapKind.current = snapKind; + } else { + // Light frame: reuse the last computed snap so visuals stay smooth + snappedPoint = cachedSnappedPoint.current ? cachedSnappedPoint.current.clone() : null; + snapKind = cachedSnapKind.current; } // ── Dwell detection (1 s) to auto-register hovered hole/edge ───── From 085184ba41be8150f1ab02077d574f393ca33846 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 21:25:21 +0000 Subject: [PATCH 3/6] Changes Co-authored-by: Reifonas <211114984+Reifonas@users.noreply.github.com> --- src/components/three/XRControllerMeasure.tsx | 42 +++++++++++++------- 1 file changed, 27 insertions(+), 15 deletions(-) diff --git a/src/components/three/XRControllerMeasure.tsx b/src/components/three/XRControllerMeasure.tsx index bda9008..63b9af9 100644 --- a/src/components/three/XRControllerMeasure.tsx +++ b/src/components/three/XRControllerMeasure.tsx @@ -239,21 +239,33 @@ export function XRControllerMeasure() { if (!trigState.current && trigVal > TRIG_ON) { trigState.current = true; const st = useModelStore.getState(); - if (st.selectionMode && hit && hit.object instanceof THREE.Mesh) { - // Walk up to find ifcElement + modelId - let cur: THREE.Object3D | null = hit.object; - let element: THREE.Object3D | null = null; - let modelId: string | null = null; - while (cur) { - if (!element && cur.userData?.ifcElement) element = cur; - if (!modelId && cur.userData?.modelId) modelId = cur.userData.modelId as string; - cur = cur.parent; - } - if (!element) element = hit.object; - if (!modelId) modelId = st.activeModelId; - if (modelId && element) { - const id = element.userData?.ifcId ?? element.name ?? element.uuid; - st.toggleElementSelection(`${modelId}:${id}`); + if (st.selectionMode) { + // Fresh raycast on the press itself (not every frame) for selection + 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 (o.userData.__edgeLine) return false; + if (o.geometry instanceof THREE.SphereGeometry) return false; + if (o.geometry instanceof THREE.RingGeometry) return false; + if (o.geometry instanceof THREE.PlaneGeometry) return false; + return true; + }); + if (triggerHit && triggerHit.object instanceof THREE.Mesh) { + let cur: THREE.Object3D | null = triggerHit.object; + let element: THREE.Object3D | null = null; + let modelId: string | null = null; + while (cur) { + if (!element && cur.userData?.ifcElement) element = cur; + if (!modelId && cur.userData?.modelId) modelId = cur.userData.modelId as string; + cur = cur.parent; + } + if (!element) element = triggerHit.object; + if (!modelId) modelId = st.activeModelId; + if (modelId && element) { + const id = element.userData?.ifcId ?? element.name ?? element.uuid; + st.toggleElementSelection(`${modelId}:${id}`); + } } } else if (snappedPoint) { st.addMeasurePoint({ From a8cfe0d3b9e12bb9188757e06104151268b8581f 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 21:25:35 +0000 Subject: [PATCH 4/6] Changes Co-authored-by: Reifonas <211114984+Reifonas@users.noreply.github.com> --- src/components/three/XRHudInWorld.tsx | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/components/three/XRHudInWorld.tsx b/src/components/three/XRHudInWorld.tsx index c44c282..4f89c49 100644 --- a/src/components/three/XRHudInWorld.tsx +++ b/src/components/three/XRHudInWorld.tsx @@ -350,9 +350,7 @@ function ToolsTab(p: XRHudInWorldProps) { active={measureMode} onClick={() => setMeasureMode(!measureMode)} /> - - From 0cce9f94b8a38b81895799e16579aa9c5d743af7 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 21:25:45 +0000 Subject: [PATCH 5/6] Changes Co-authored-by: Reifonas <211114984+Reifonas@users.noreply.github.com> --- src/components/XRHud.tsx | 22 ---------------------- 1 file changed, 22 deletions(-) diff --git a/src/components/XRHud.tsx b/src/components/XRHud.tsx index 113cd36..39f9607 100644 --- a/src/components/XRHud.tsx +++ b/src/components/XRHud.tsx @@ -310,19 +310,6 @@ export function XRHud({ freeMove, onToggleFreeMove, placementMode = false, onTog )} - {/* Two-hand scale toggle */} - {onToggleAllowScale && ( - - )} - {/* Scale presets */} @@ -332,15 +319,6 @@ export function XRHud({ freeMove, onToggleFreeMove, placementMode = false, onTog {/* Share live screen */} - -