From ae5f40c27c03521a8e6d156255a3fe781d009f92 Mon Sep 17 00:00:00 2001 From: "gpt-engineer-app[bot]" <159125892+gpt-engineer-app[bot]@users.noreply.github.com> Date: Fri, 15 May 2026 11:35:26 +0000 Subject: [PATCH 1/7] Changes Co-authored-by: Reifonas <211114984+Reifonas@users.noreply.github.com> --- src/stores/useModelStore.ts | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/src/stores/useModelStore.ts b/src/stores/useModelStore.ts index c4f771e..34afc02 100644 --- a/src/stores/useModelStore.ts +++ b/src/stores/useModelStore.ts @@ -133,6 +133,15 @@ interface ModelStore { showGrid: boolean; setShowGrid: (show: boolean) => void; + /** World Y position of the grid plane (meters). */ + gridY: number; + setGridY: (y: number) => void; + /** When true, grid follows the bottom of the loaded model automatically. */ + gridAutoFollow: boolean; + setGridAutoFollow: (b: boolean) => void; + /** Nudge grid by delta meters and disable auto-follow. */ + nudgeGridY: (delta: number) => void; + wireframeColor: string; setWireframeColor: (color: string) => void; From 9cbebdd5cbb1253cf034de7845587a2d8da9b0e1 Mon Sep 17 00:00:00 2001 From: "gpt-engineer-app[bot]" <159125892+gpt-engineer-app[bot]@users.noreply.github.com> Date: Fri, 15 May 2026 11:35:35 +0000 Subject: [PATCH 2/7] Changes Co-authored-by: Reifonas <211114984+Reifonas@users.noreply.github.com> --- src/stores/useModelStore.ts | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/stores/useModelStore.ts b/src/stores/useModelStore.ts index 34afc02..c9ce865 100644 --- a/src/stores/useModelStore.ts +++ b/src/stores/useModelStore.ts @@ -317,6 +317,12 @@ export const useModelStore = create((set, get) => ({ showGrid: true, setShowGrid: (showGrid) => set({ showGrid }), + gridY: -0.09, + setGridY: (gridY) => set({ gridY, gridAutoFollow: false }), + gridAutoFollow: true, + setGridAutoFollow: (gridAutoFollow) => set({ gridAutoFollow }), + nudgeGridY: (delta) => set((s) => ({ gridY: s.gridY + delta, gridAutoFollow: false })), + wireframeColor: '#8899aa', setWireframeColor: (wireframeColor) => set({ wireframeColor }), From c6bd96dfb340a3c42a886f783c7238f04b5aff79 Mon Sep 17 00:00:00 2001 From: "gpt-engineer-app[bot]" <159125892+gpt-engineer-app[bot]@users.noreply.github.com> Date: Fri, 15 May 2026 11:35:53 +0000 Subject: [PATCH 3/7] Changes Co-authored-by: Reifonas <211114984+Reifonas@users.noreply.github.com> --- src/components/three/ModelViewer.tsx | 38 +++++++++++++++++++++++++++- 1 file changed, 37 insertions(+), 1 deletion(-) diff --git a/src/components/three/ModelViewer.tsx b/src/components/three/ModelViewer.tsx index 046c96c..2a97659 100644 --- a/src/components/three/ModelViewer.tsx +++ b/src/components/three/ModelViewer.tsx @@ -454,10 +454,11 @@ function MeasureClickHandler() { function GridLayer() { const showGrid = useModelStore((s) => s.showGrid); + const gridY = useModelStore((s) => s.gridY); if (!showGrid) return null; return ( s.models); + const { scene } = useThree(); + useEffect(() => { + const auto = useModelStore.getState().gridAutoFollow; + if (!auto) return; + // Defer to next tick so models have rendered/positioned + const id = setTimeout(() => { + if (!useModelStore.getState().gridAutoFollow) return; + const box = new THREE.Box3(); + let has = false; + scene.traverse((obj) => { + if (obj instanceof THREE.Mesh && obj.geometry) { + // Skip helpers (markers, rings) + if (obj.geometry instanceof THREE.SphereGeometry) return; + if (obj.geometry instanceof THREE.RingGeometry) return; + if (obj.userData.__edgeLine) return; + obj.updateWorldMatrix(true, false); + const b = new THREE.Box3().setFromObject(obj); + if (isFinite(b.min.y)) { + if (!has) { box.copy(b); has = true; } + else box.union(b); + } + } + }); + if (has) { + useModelStore.setState({ gridY: box.min.y - 0.005 }); + } + }, 100); + return () => clearTimeout(id); + }, [models, scene]); + return null; +} + export function ModelViewerCanvas({ url }: ModelViewerProps) { return ( Date: Fri, 15 May 2026 11:36:04 +0000 Subject: [PATCH 4/7] Changes Co-authored-by: Reifonas <211114984+Reifonas@users.noreply.github.com> --- src/components/three/ModelViewer.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/src/components/three/ModelViewer.tsx b/src/components/three/ModelViewer.tsx index 2a97659..f1363ab 100644 --- a/src/components/three/ModelViewer.tsx +++ b/src/components/three/ModelViewer.tsx @@ -534,6 +534,7 @@ export function ModelViewerCanvas({ url }: ModelViewerProps) { + Date: Fri, 15 May 2026 11:36:17 +0000 Subject: [PATCH 5/7] Changes Co-authored-by: Reifonas <211114984+Reifonas@users.noreply.github.com> --- src/pages/XRSession.tsx | 33 ++++++++++++++++++++++++++++++++- 1 file changed, 32 insertions(+), 1 deletion(-) diff --git a/src/pages/XRSession.tsx b/src/pages/XRSession.tsx index 7abd1b7..436ad82 100644 --- a/src/pages/XRSession.tsx +++ b/src/pages/XRSession.tsx @@ -359,10 +359,11 @@ function XRSnapHandler() { // ─── XR Grid ─────────────────────────────────────────── function XRGrid() { const showGrid = useModelStore((s) => s.showGrid); + const gridY = useModelStore((s) => s.gridY); if (!showGrid) return null; return ( s.models); + const { scene } = useThree(); + useEffect(() => { + if (!useModelStore.getState().gridAutoFollow) return; + const id = setTimeout(() => { + if (!useModelStore.getState().gridAutoFollow) return; + const box = new THREE.Box3(); + let has = false; + scene.traverse((obj) => { + if (obj instanceof THREE.Mesh && obj.geometry) { + if (obj.geometry instanceof THREE.SphereGeometry) return; + if (obj.geometry instanceof THREE.RingGeometry) return; + if (obj.userData.__edgeLine) return; + obj.updateWorldMatrix(true, false); + const b = new THREE.Box3().setFromObject(obj); + if (isFinite(b.min.y)) { + if (!has) { box.copy(b); has = true; } + else box.union(b); + } + } + }); + if (has) useModelStore.setState({ gridY: box.min.y - 0.005 }); + }, 150); + return () => clearTimeout(id); + }, [models, scene]); + return null; +} + // ImageTrackingAnchor removed — replaced by XRHitTestPlacement // ─── XRSession Page ──────────────────────────────────── From 5c155b2e1b578b9698d664153f89be7279c8ddeb Mon Sep 17 00:00:00 2001 From: "gpt-engineer-app[bot]" <159125892+gpt-engineer-app[bot]@users.noreply.github.com> Date: Fri, 15 May 2026 11:36:27 +0000 Subject: [PATCH 6/7] 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 436ad82..0c69b64 100644 --- a/src/pages/XRSession.tsx +++ b/src/pages/XRSession.tsx @@ -616,6 +616,7 @@ const XRSession = () => { + From 6e11620e30b6b5a85eae5e996a1eeddf63bbdf72 Mon Sep 17 00:00:00 2001 From: "gpt-engineer-app[bot]" <159125892+gpt-engineer-app[bot]@users.noreply.github.com> Date: Fri, 15 May 2026 11:36:52 +0000 Subject: [PATCH 7/7] Changes Co-authored-by: Reifonas <211114984+Reifonas@users.noreply.github.com> --- src/components/three/XRHudInWorld.tsx | 34 +++++++++++++++++++++++++++ 1 file changed, 34 insertions(+) diff --git a/src/components/three/XRHudInWorld.tsx b/src/components/three/XRHudInWorld.tsx index ce1921b..5c6bf98 100644 --- a/src/components/three/XRHudInWorld.tsx +++ b/src/components/three/XRHudInWorld.tsx @@ -309,6 +309,40 @@ function ToolsTab(p: XRHudInWorldProps) { onClick={() => setScaleRatio(preset)} /> ); })} + + {/* Grid floor controls */} + + + ); +} + +function GridFloorRow() { + const { camera } = useThree(); + const gridY = useModelStore((s) => s.gridY); + const gridAutoFollow = useModelStore((s) => s.gridAutoFollow); + const setGridAutoFollow = useModelStore((s) => s.setGridAutoFollow); + const setGridY = useModelStore((s) => s.setGridY); + const nudgeGridY = useModelStore((s) => s.nudgeGridY); + const placeAtFloor = () => { + // Headset eye Y minus avg standing eye height (1.6 m) + const floor = camera.position.y - 1.6; + setGridY(floor); + }; + return ( + + + Grid Y {(gridY * 1000).toFixed(0)}mm {gridAutoFollow ? '(auto)' : '(travado)'} + + setGridAutoFollow(true)} /> + setGridAutoFollow(false)} /> + nudgeGridY(-0.01)} /> + nudgeGridY(0.01)} /> + ); }