From 1e4bd78128261cf6512b0237f7cd539f6aa83f0b Mon Sep 17 00:00:00 2001 From: admtracksteel Date: Thu, 28 May 2026 21:57:52 +0000 Subject: [PATCH] =?UTF-8?q?=F0=9F=9A=80=20Auto-deploy:=20melhoria=20no=20s?= =?UTF-8?q?nap=20e=20medi=C3=A7=C3=A3o=20AR=20em=2028/05/2026=2021:57:52?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/components/three/XRHudInWorld.tsx | 11 +-- src/pages/XRSession.tsx | 127 +++++++++++++++++++++++++- src/stores/useModelStore.ts | 5 + 3 files changed, 135 insertions(+), 8 deletions(-) diff --git a/src/components/three/XRHudInWorld.tsx b/src/components/three/XRHudInWorld.tsx index fa2afab..3e86107 100644 --- a/src/components/three/XRHudInWorld.tsx +++ b/src/components/three/XRHudInWorld.tsx @@ -617,12 +617,9 @@ function GridFloorRow() { const nudgeGridY = useModelStore((s) => s.nudgeGridY); const gridCalibMode = useModelStore((s) => s.gridCalibMode); const setGridCalibMode = useModelStore((s) => s.setGridCalibMode); + const gridLandingMode = useModelStore((s) => s.gridLandingMode); + const setGridLandingMode = useModelStore((s) => s.setGridLandingMode); - const placeAtFloor = () => { - // Headset eye Y minus avg standing eye height (1.6 m) - const floor = camera.position.y - 1.6; - setGridY(floor); - }; return ( @@ -636,8 +633,8 @@ function GridFloorRow() { onClick={() => nudgeGridY(-0.01)} /> nudgeGridY(0.01)} /> - + setGridLandingMode(!gridLandingMode)} /> setGridCalibMode(!gridCalibMode)} /> diff --git a/src/pages/XRSession.tsx b/src/pages/XRSession.tsx index 6e3bf45..592d3a4 100644 --- a/src/pages/XRSession.tsx +++ b/src/pages/XRSession.tsx @@ -2,7 +2,7 @@ import { useEffect, useRef, useMemo, useCallback, useState } from 'react'; import { useNavigate } from 'react-router-dom'; import { Canvas, useFrame, useThree } from '@react-three/fiber'; import { useGLTF, Grid, Html, Line, OrbitControls, Text } from '@react-three/drei'; -import { XR, createXRStore, useXR, XROrigin } from '@react-three/xr'; +import { XR, createXRStore, useXR, XROrigin, useXRHitTest } from '@react-three/xr'; import * as THREE from 'three'; import { useModelStore } from '@/stores/useModelStore'; import { toast } from 'sonner'; @@ -304,6 +304,130 @@ function XRBackgroundModels() { ); } +/** Component that renders a laser from the camera (headset) to the grid landing target. */ +function XRGridLandingLaser({ reticlePos }: { reticlePos: THREE.Vector3 | null }) { + const { camera } = useThree(); + const geom = useMemo(() => new THREE.BufferGeometry().setFromPoints([new THREE.Vector3(), new THREE.Vector3(0, 0, -1)]), []); + const mat = useMemo(() => new THREE.LineBasicMaterial({ color: '#eab308', transparent: true, opacity: 0.6 }), []); + const lineRef = useRef(null); + + useFrame(() => { + if (lineRef.current && reticlePos) { + const posAttr = geom.attributes.position as THREE.BufferAttribute; + // Laser origin slightly below eyes + posAttr.setXYZ(0, camera.position.x, camera.position.y - 0.05, camera.position.z); + posAttr.setXYZ(1, reticlePos.x, reticlePos.y, reticlePos.z); + posAttr.needsUpdate = true; + geom.computeBoundingSphere(); + lineRef.current.visible = true; + } else if (lineRef.current) { + lineRef.current.visible = false; + } + }); + + return ; +} + +/** Handles physics surface hit-testing using WebXR for grid and active model landing. */ +function XRGridLandingHandler() { + const gridLandingMode = useModelStore((s) => s.gridLandingMode); + const gridY = useModelStore((s) => s.gridY); + + const reticleRef = useRef(null); + const [reticlePos, setReticlePos] = useState(null); + + // Continuous hit-test from viewer center + useXRHitTest( + gridLandingMode + ? (results, getWorldMatrix) => { + if (results.length === 0) { + setReticlePos(null); + return; + } + const matrixHelper = new THREE.Matrix4(); + getWorldMatrix(matrixHelper, results[0]); + const positionHelper = new THREE.Vector3().setFromMatrixPosition(matrixHelper); + setReticlePos(positionHelper.clone()); + } + : undefined, + 'viewer' + ); + + // Trata a confirmação (clique/gatilho no óculos ou controle) + const handleSelect = useCallback(() => { + if (!gridLandingMode || !reticlePos) return; + + const targetY = reticlePos.y; + const deltaY = targetY - gridY; + + // Desloca a peça ativa na mesma diferença (deltaY) para que ela "pouse junto" + const state = useModelStore.getState(); + const activeModel = state.models.find(m => m.id === state.activeModelId); + if (activeModel) { + const ft = activeModel.fineTuning; + state.setFineTuning({ + posY: ft.posY + deltaY, + }); + } + + // Atualiza o grid Y no store + useModelStore.setState({ + gridY: targetY, + gridAutoFollow: false, + gridLandingMode: false, + showGrid: true, + }); + + toast.success("Grid e peça posicionados na superfície real!"); + }, [gridLandingMode, reticlePos, gridY]); + + // Atualiza posição da retícula a cada frame + useFrame(() => { + if (reticleRef.current) { + if (reticlePos && gridLandingMode) { + reticleRef.current.visible = true; + reticleRef.current.position.copy(reticlePos); + } else { + reticleRef.current.visible = false; + } + } + }); + + if (!gridLandingMode) return null; + + return ( + <> + + + {/* Retícula dourada premium */} + + + + + + + + + + + + + + + + {/* Plano invisível de click no fundo */} + + + + + + ); +} + /** Renders the currently-active model (the one wrapped by grab/placement). */ function XRActiveModel() { const models = useModelStore((s) => s.models); @@ -845,6 +969,7 @@ const XRSession = () => { + {/* In-world HUD — visible inside passthrough where DOM overlays cannot reach */} diff --git a/src/stores/useModelStore.ts b/src/stores/useModelStore.ts index 6216d0c..37d1bf3 100644 --- a/src/stores/useModelStore.ts +++ b/src/stores/useModelStore.ts @@ -260,6 +260,9 @@ interface ModelStore { /** When true, the next click on a model face sets grid Y to that point. */ gridCalibMode: boolean; setGridCalibMode: (b: boolean) => void; + /** When true, the VR controller/headset hit-tests real world surfaces to place grid & models. */ + gridLandingMode: boolean; + setGridLandingMode: (b: boolean) => void; /** Section/clipping cut tool (X/Y/Z axis-aligned planes in world space). */ sectionEnabled: { x: boolean; y: boolean; z: boolean }; @@ -555,6 +558,8 @@ export const useModelStore = create((set, get) => ({ nudgeGridY: (delta) => set((s) => ({ gridY: s.gridY + delta, gridAutoFollow: false })), gridCalibMode: false, setGridCalibMode: (gridCalibMode) => set({ gridCalibMode }), + gridLandingMode: false, + setGridLandingMode: (gridLandingMode) => set({ gridLandingMode }), sectionEnabled: { x: false, y: false, z: false }, sectionInvert: { x: false, y: false, z: false },