From 0a9a83aec3848ec0b216624ed93c4154aca1f765 Mon Sep 17 00:00:00 2001 From: "gpt-engineer-app[bot]" <159125892+gpt-engineer-app[bot]@users.noreply.github.com> Date: Tue, 19 May 2026 21:10:27 +0000 Subject: [PATCH 01/10] Changes Co-authored-by: Reifonas <211114984+Reifonas@users.noreply.github.com> --- src/stores/useModelStore.ts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/stores/useModelStore.ts b/src/stores/useModelStore.ts index 16bc7e9..b34919e 100644 --- a/src/stores/useModelStore.ts +++ b/src/stores/useModelStore.ts @@ -172,6 +172,10 @@ interface ModelStore { scaleRatio: ScaleRatio; setScaleRatio: (r: ScaleRatio) => void; + /** Incremented whenever the user asks to reset the AR scale; XRGrabbable + * watches this and snaps its group.scale back to 1. */ + scaleResetNonce: number; + resetScale: () => void; opacity: number; setOpacity: (opacity: number) => void; From 55767c0141be1825e61cbead6109a6b1a42bccf9 Mon Sep 17 00:00:00 2001 From: "gpt-engineer-app[bot]" <159125892+gpt-engineer-app[bot]@users.noreply.github.com> Date: Tue, 19 May 2026 21:10:38 +0000 Subject: [PATCH 02/10] Changes Co-authored-by: Reifonas <211114984+Reifonas@users.noreply.github.com> --- src/stores/useModelStore.ts | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/src/stores/useModelStore.ts b/src/stores/useModelStore.ts index b34919e..8fb4c1a 100644 --- a/src/stores/useModelStore.ts +++ b/src/stores/useModelStore.ts @@ -379,6 +379,25 @@ export const useModelStore = create((set, get) => ({ scaleRatio: SCALE_PRESETS.find(p => p.label === '1:1')!, setScaleRatio: (scaleRatio) => set({ scaleRatio }), + scaleResetNonce: 0, + resetScale: () => set((state) => { + const oneToOne = SCALE_PRESETS.find(p => p.label === '1:1')!; + const activeId = state.activeModelId; + const models = activeId + ? state.models.map(m => m.id === activeId + ? { ...m, fineTuning: { ...m.fineTuning, scale: 1 } } + : m) + : state.models; + const active = models.find(m => m.id === activeId); + const fineTuning = active ? { ...active.fineTuning } : { ...state.fineTuning, scale: 1 }; + if (active) savePlacement(active.fileName, fineTuning); + return { + scaleRatio: oneToOne, + models, + fineTuning, + scaleResetNonce: state.scaleResetNonce + 1, + }; + }), opacity: 1, setOpacity: (opacity) => set({ opacity }), From 14c11ca612f47de6ca8ffad5469457b930422f66 Mon Sep 17 00:00:00 2001 From: "gpt-engineer-app[bot]" <159125892+gpt-engineer-app[bot]@users.noreply.github.com> Date: Tue, 19 May 2026 21:10:48 +0000 Subject: [PATCH 03/10] Changes Co-authored-by: Reifonas <211114984+Reifonas@users.noreply.github.com> --- src/components/three/XRGrabbable.tsx | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/components/three/XRGrabbable.tsx b/src/components/three/XRGrabbable.tsx index 03ace6e..d8ac111 100644 --- a/src/components/three/XRGrabbable.tsx +++ b/src/components/three/XRGrabbable.tsx @@ -1,7 +1,8 @@ -import { useRef, ReactNode } from 'react'; +import { useRef, ReactNode, useEffect } from 'react'; import { useFrame } from '@react-three/fiber'; import * as THREE from 'three'; import { useControllerGrab, ControllerGrabSnapshot } from '@/hooks/useControllerGrab'; +import { useModelStore } from '@/stores/useModelStore'; interface XRGrabbableProps { /** Allow uniform scaling via two-hand grab (distance between hands) */ From 048ca8d78150143f8717e8de407eede555bddf9b Mon Sep 17 00:00:00 2001 From: "gpt-engineer-app[bot]" <159125892+gpt-engineer-app[bot]@users.noreply.github.com> Date: Tue, 19 May 2026 21:10:57 +0000 Subject: [PATCH 04/10] Changes Co-authored-by: Reifonas <211114984+Reifonas@users.noreply.github.com> --- src/components/three/XRGrabbable.tsx | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/src/components/three/XRGrabbable.tsx b/src/components/three/XRGrabbable.tsx index d8ac111..6404ef8 100644 --- a/src/components/three/XRGrabbable.tsx +++ b/src/components/three/XRGrabbable.tsx @@ -57,6 +57,17 @@ export function XRGrabbable({ allowScale = false, lockedActive = false, onGrabSt const haloRef = useRef(null); const everGrabbed = useRef(false); + // Reset scale to 1 (position/rotation kept) whenever resetScale() is called. + const scaleResetNonce = useModelStore((s) => s.scaleResetNonce); + useEffect(() => { + const g = groupRef.current; + if (!g) return; + g.scale.set(1, 1, 1); + // Abort any in-progress two-hand scaling so it doesn't snap back. + dual.current = null; + single.current = null; + }, [scaleResetNonce]); + useFrame(() => { const group = groupRef.current; if (!group) return; From baf77fea0fe6d1c589501a033cadaf002a78de97 Mon Sep 17 00:00:00 2001 From: "gpt-engineer-app[bot]" <159125892+gpt-engineer-app[bot]@users.noreply.github.com> Date: Tue, 19 May 2026 21:11:13 +0000 Subject: [PATCH 05/10] Changes Co-authored-by: Reifonas <211114984+Reifonas@users.noreply.github.com> --- src/components/three/XRControllerMeasure.tsx | 84 ++++++++++++++++++++ 1 file changed, 84 insertions(+) create mode 100644 src/components/three/XRControllerMeasure.tsx diff --git a/src/components/three/XRControllerMeasure.tsx b/src/components/three/XRControllerMeasure.tsx new file mode 100644 index 0000000..1702e0d --- /dev/null +++ b/src/components/three/XRControllerMeasure.tsx @@ -0,0 +1,84 @@ +import { useRef } from 'react'; +import { useFrame, useThree } from '@react-three/fiber'; +import * as THREE from 'three'; +import { useModelStore } from '@/stores/useModelStore'; + +const TRIG_ON = 0.7; +const TRIG_OFF = 0.3; +const MAX_RAY = 10; // meters + +/** + * Right-controller trigger driven measurement for AR. + * + * When `measureMode` is on and the user pulls the right trigger, casts a ray + * forward from the controller (target-ray space). The first mesh hit becomes + * a measurement point. Two pulls = one measurement (handled by the store). + * + * Uses hysteresis so a single trigger pull adds exactly one point. + */ +export function XRControllerMeasure() { + const { scene, gl } = useThree(); + const triggered = useRef(false); + const raycaster = useRef(new THREE.Raycaster()); + const tmpOrigin = useRef(new THREE.Vector3()); + const tmpDir = useRef(new THREE.Vector3()); + const tmpQuat = useRef(new THREE.Quaternion()); + + useFrame((_state, _dt, frame: XRFrame | undefined) => { + const measureMode = useModelStore.getState().measureMode; + if (!measureMode || !frame) return; + const session = frame.session; + const refSpace = gl.xr.getReferenceSpace(); + if (!session || !refSpace) return; + + // Find right controller + let right: XRInputSource | null = null; + for (const src of session.inputSources) { + if (src.handedness === 'right') { right = src; break; } + } + if (!right) return; + + const gp = right.gamepad; + const trigBtn = gp?.buttons?.[0]; + const trigVal = trigBtn ? (trigBtn.value || (trigBtn.pressed ? 1 : 0)) : 0; + + // Rising edge with hysteresis + if (!triggered.current && trigVal > TRIG_ON) { + triggered.current = true; + + const raySpace = right.targetRaySpace ?? right.gripSpace; + if (!raySpace) return; + const pose = frame.getPose(raySpace, refSpace); + if (!pose) return; + + const m = new THREE.Matrix4().fromArray(pose.transform.matrix); + tmpOrigin.current.setFromMatrixPosition(m); + tmpQuat.current.setFromRotationMatrix(m); + // Controller aims along -Z in WebXR target-ray convention + tmpDir.current.set(0, 0, -1).applyQuaternion(tmpQuat.current).normalize(); + + 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; + }); + if (hit) { + useModelStore.getState().addMeasurePoint({ + x: hit.point.x, y: hit.point.y, z: hit.point.z, + }); + } + } else if (triggered.current && trigVal < TRIG_OFF) { + triggered.current = false; + } + }); + + return null; +} From 2042e5c01d2846dbc3c2ceda04c75e3e6fc4817e Mon Sep 17 00:00:00 2001 From: "gpt-engineer-app[bot]" <159125892+gpt-engineer-app[bot]@users.noreply.github.com> Date: Tue, 19 May 2026 21:11:22 +0000 Subject: [PATCH 06/10] Changes Co-authored-by: Reifonas <211114984+Reifonas@users.noreply.github.com> --- src/pages/XRSession.tsx | 25 ++++++++++++++++++------- 1 file changed, 18 insertions(+), 7 deletions(-) diff --git a/src/pages/XRSession.tsx b/src/pages/XRSession.tsx index 9804999..6b98b8f 100644 --- a/src/pages/XRSession.tsx +++ b/src/pages/XRSession.tsx @@ -300,13 +300,24 @@ function XRMeasurementOverlay() { - -
- - {m.distanceMM.toFixed(1)} mm - -
- + + {/* Background plate */} + + + + + + {`${m.distanceMM.toFixed(1)} mm`} + + ); })} From 0e4da2eaf4299c31eb84fa554a1d73109fe7171e Mon Sep 17 00:00:00 2001 From: "gpt-engineer-app[bot]" <159125892+gpt-engineer-app[bot]@users.noreply.github.com> Date: Tue, 19 May 2026 21:11:30 +0000 Subject: [PATCH 07/10] Changes Co-authored-by: Reifonas <211114984+Reifonas@users.noreply.github.com> --- src/pages/XRSession.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/src/pages/XRSession.tsx b/src/pages/XRSession.tsx index 6b98b8f..7575e17 100644 --- a/src/pages/XRSession.tsx +++ b/src/pages/XRSession.tsx @@ -16,6 +16,7 @@ import { XRBroadcastMirror } from '@/components/three/XRBroadcastMirror'; import { findNearestVertex } from '@/components/three/SmartMeasure'; import { XRHitTestPlacement } from '@/components/three/XRHitTestPlacement'; import { XRGrabbable } from '@/components/three/XRGrabbable'; +import { XRControllerMeasure } from '@/components/three/XRControllerMeasure'; // DEVKIT: remove these imports + all `// DEVKIT:` blocks below to strip devkit import { useDevKit } from '@/devkit/useDevKit'; import { DevPanel } from '@/devkit/DevPanel'; From f82ee55ff067ef4905585297de3a84f95f05d889 Mon Sep 17 00:00:00 2001 From: "gpt-engineer-app[bot]" <159125892+gpt-engineer-app[bot]@users.noreply.github.com> Date: Tue, 19 May 2026 21:11:36 +0000 Subject: [PATCH 08/10] Changes Co-authored-by: Reifonas <211114984+Reifonas@users.noreply.github.com> --- src/pages/XRSession.tsx | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/pages/XRSession.tsx b/src/pages/XRSession.tsx index 7575e17..210c99f 100644 --- a/src/pages/XRSession.tsx +++ b/src/pages/XRSession.tsx @@ -598,8 +598,10 @@ const XRSession = () => { )} + + {/* In-world HUD — visible inside passthrough where DOM overlays cannot reach */} Date: Tue, 19 May 2026 21:11:47 +0000 Subject: [PATCH 09/10] Changes Co-authored-by: Reifonas <211114984+Reifonas@users.noreply.github.com> --- src/components/three/XRHudInWorld.tsx | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/components/three/XRHudInWorld.tsx b/src/components/three/XRHudInWorld.tsx index f0e7276..29db575 100644 --- a/src/components/three/XRHudInWorld.tsx +++ b/src/components/three/XRHudInWorld.tsx @@ -330,6 +330,8 @@ function ToolsTab(p: XRHudInWorldProps) { const setMeasureMode = useModelStore((s) => s.setMeasureMode); const scaleRatio = useModelStore((s) => s.scaleRatio); const setScaleRatio = useModelStore((s) => s.setScaleRatio); + const resetScale = useModelStore((s) => s.resetScale); + const clearMeasurements = useModelStore((s) => s.clearMeasurements); return ( From 8033be580a7f5d3356a638df83e3bbb2af5ac163 Mon Sep 17 00:00:00 2001 From: "gpt-engineer-app[bot]" <159125892+gpt-engineer-app[bot]@users.noreply.github.com> Date: Tue, 19 May 2026 21:11:59 +0000 Subject: [PATCH 10/10] Changes Co-authored-by: Reifonas <211114984+Reifonas@users.noreply.github.com> --- src/components/three/XRHudInWorld.tsx | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/src/components/three/XRHudInWorld.tsx b/src/components/three/XRHudInWorld.tsx index 29db575..182ff60 100644 --- a/src/components/three/XRHudInWorld.tsx +++ b/src/components/three/XRHudInWorld.tsx @@ -382,6 +382,15 @@ function ToolsTab(p: XRHudInWorldProps) { onClick={() => setScaleRatio(preset)} /> ); })} + {/* Reset Escala — devolve a peça ao tamanho 1:1 (cancela qualquer + escala manual feita com duas mãos). */} + + {/* Limpa todos os labels de medição. */} + {/* Grid floor controls */}