diff --git a/src/components/three/XRControllerMeasure.tsx b/src/components/three/XRControllerMeasure.tsx index 0adad8d..5c87a20 100644 --- a/src/components/three/XRControllerMeasure.tsx +++ b/src/components/three/XRControllerMeasure.tsx @@ -11,7 +11,6 @@ import { detectCircularEdgeAtPoint3D, findNearestEdgeSegment3D } from './SmartMeasure'; -import { xrCalibration, pushXRRealPoint, subscribeXRCalibration } from './xrCalibrationBus'; import { toast } from 'sonner'; const TRIG_ON = 0.7; @@ -58,12 +57,6 @@ export function XRControllerMeasure() { const lTrigState = useRef(false); const [snapEnabled, setSnapEnabled] = useState(true); - // Escuta o barramento de calibração do XR para sincronização e re-renderizações - const [, forceCalib] = useState(0); - useEffect(() => subscribeXRCalibration(() => forceCalib((t) => t + 1)), []); - - const lastStepRef = useRef('idle'); - const ignoreTriggerUntilReleaseRef = useRef(false); const raycaster = useRef(new THREE.Raycaster()); const tmpOrigin = useRef(new THREE.Vector3()); @@ -96,25 +89,9 @@ export function XRControllerMeasure() { const measureMode = useModelStore.getState().measureMode; const selectionMode = useModelStore.getState().selectionMode; - // Alinhamento Virt/Real ativo - const isAligning = xrCalibration.alignType === 'virt-real' && - (xrCalibration.step === 'await-real-1' || - xrCalibration.step === 'await-virtual-1' || - xrCalibration.step === 'await-real-2' || - xrCalibration.step === 'await-virtual-2' || - xrCalibration.step === 'await-real-3' || - xrCalibration.step === 'await-virtual-3'); - - if (xrCalibration.step !== lastStepRef.current) { - lastStepRef.current = xrCalibration.step; - if (xrCalibration.step !== 'idle' && xrCalibration.step !== 'done') { - ignoreTriggerUntilReleaseRef.current = true; - } - } - if (laserRef.current) laserRef.current.visible = false; if (tipRef.current) tipRef.current.visible = false; - if ((!measureMode && !selectionMode && !isAligning) || !frame) { + if ((!measureMode && !selectionMode) || !frame) { if (hitTestRequestedRef.current) { hitTestSourceRef.current = null; hitTestRequestedRef.current = false; @@ -161,58 +138,6 @@ export function XRControllerMeasure() { raycaster.current.set(tmpOrigin.current, tmpDir.current); raycaster.current.far = MAX_RAY; - // Hit test do mundo real para alinhamento - const isRealStep = xrCalibration.alignType === 'virt-real' && - (xrCalibration.step === 'await-real-1' || - xrCalibration.step === 'await-real-2' || - xrCalibration.step === 'await-real-3'); - - const isVirtualStep = xrCalibration.alignType === 'virt-real' && - (xrCalibration.step === 'await-virtual-1' || - xrCalibration.step === 'await-virtual-2' || - xrCalibration.step === 'await-virtual-3'); - - if (session && right && !hitTestRequestedRef.current && isAligning) { - hitTestRequestedRef.current = true; - const spaceToUse = right.targetRaySpace ?? right.gripSpace; - if (spaceToUse) { - try { - session.requestHitTestSource({ space: spaceToUse }) - .then((source) => { - hitTestSourceRef.current = source; - }) - .catch((err) => { - console.error('[XR] Failed to request controller hit test source:', err); - hitTestRequestedRef.current = false; - }); - } catch (err) { - console.error('[XR] Synchronous error requesting controller hit test source:', err); - hitTestRequestedRef.current = false; - } - } else { - hitTestRequestedRef.current = false; - } - } - - let realHitPoint: THREE.Vector3 | null = null; - if (isRealStep) { - if (hitTestSourceRef.current && frame) { - const results = frame.getHitTestResults(hitTestSourceRef.current); - if (results.length > 0) { - const poseResult = results[0].getPose(refSpace); - if (poseResult) { - realHitPoint = new THREE.Vector3().setFromMatrixPosition( - new THREE.Matrix4().fromArray(poseResult.transform.matrix) - ); - } - } - } - if (!realHitPoint) { - // Fallback: 1.5 metros na direção do controle - realHitPoint = tmpOrigin.current.clone().add(tmpDir.current.clone().multiplyScalar(1.5)); - } - } - let snappedPoint: THREE.Vector3 | null = null; let snapKind: 'vertex' | 'edge' | 'surface' | 'hole' = 'surface'; let hoverDetected: { kind: 'hole' | 'edge'; value: number; position: THREE.Vector3; modelId?: string; endpoints?: { a: THREE.Vector3; b: THREE.Vector3 } } | null = null; @@ -327,75 +252,44 @@ export function XRControllerMeasure() { // ── Update laser visual ─────────────────────────────────────────── if (laserRef.current && tipRef.current) { - if (isVirtualStep) { - laserRef.current.visible = false; - tipRef.current.visible = false; + const end = snappedPoint ?? tmpOrigin.current.clone().add(tmpDir.current.clone().multiplyScalar(MAX_RAY)); + + const positions = laserGeom.current.attributes.position as THREE.BufferAttribute; + positions.setXYZ(0, tmpOrigin.current.x, tmpOrigin.current.y, tmpOrigin.current.z); + positions.setXYZ(1, end.x, end.y, end.z); + positions.needsUpdate = true; + laserGeom.current.computeBoundingSphere(); + + const color = selectionMode ? '#a855f7' : (snapKind === 'hole' ? '#f59e0b' : snapKind === 'vertex' ? '#22c55e' : snapKind === 'edge' ? '#3b82f6' : '#eab308'); + + tipColor.current.set(color); + (laserRef.current.material as THREE.LineBasicMaterial).color.set(color); + ((tipRef.current.material as THREE.MeshBasicMaterial)).color.copy(tipColor.current); + + laserRef.current.visible = true; + + const tipPos = snappedPoint; + if (tipPos) { + tipRef.current.visible = true; + tipRef.current.position.copy(tipPos); + const dist = tmpOrigin.current.distanceTo(tipPos); + const strongSnap = snapKind === 'vertex' || snapKind === 'edge' || snapKind === 'hole'; + const factor = strongSnap ? 0.030 : 0.012; + const s = Math.max(strongSnap ? 0.010 : 0.004, dist * factor); + tipRef.current.scale.setScalar(s); } else { - const end = isRealStep - ? realHitPoint - : (snappedPoint ?? tmpOrigin.current.clone().add(tmpDir.current.clone().multiplyScalar(MAX_RAY))); - - const positions = laserGeom.current.attributes.position as THREE.BufferAttribute; - positions.setXYZ(0, tmpOrigin.current.x, tmpOrigin.current.y, tmpOrigin.current.z); - positions.setXYZ(1, end.x, end.y, end.z); - positions.needsUpdate = true; - laserGeom.current.computeBoundingSphere(); - - const color = isAligning - ? '#eab308' // dourado para alinhamento Virt/Real - : (selectionMode ? '#a855f7' : (snapKind === 'hole' ? '#f59e0b' : snapKind === 'vertex' ? '#22c55e' : snapKind === 'edge' ? '#3b82f6' : '#eab308')); - - tipColor.current.set(color); - (laserRef.current.material as THREE.LineBasicMaterial).color.set(color); - ((tipRef.current.material as THREE.MeshBasicMaterial)).color.copy(tipColor.current); - - laserRef.current.visible = true; - - const tipPos = isRealStep ? realHitPoint : snappedPoint; - if (tipPos) { - tipRef.current.visible = true; - tipRef.current.position.copy(tipPos); - const dist = tmpOrigin.current.distanceTo(tipPos); - const strongSnap = isAligning || snapKind === 'vertex' || snapKind === 'edge' || snapKind === 'hole'; - const factor = strongSnap ? 0.030 : 0.012; - const s = Math.max(strongSnap ? 0.010 : 0.004, dist * factor); - tipRef.current.scale.setScalar(s); - } else { - tipRef.current.visible = false; - } + tipRef.current.visible = false; } } // ── Right trigger: add point (measure) OR toggle selection ──────── const trigVal = gp?.buttons?.[0]?.value ?? (gp?.buttons?.[0]?.pressed ? 1 : 0); - if (trigVal < TRIG_OFF) { - ignoreTriggerUntilReleaseRef.current = false; - } - if (!trigState.current && trigVal > TRIG_ON) { trigState.current = true; - if (ignoreTriggerUntilReleaseRef.current) { - return; - } - const st = useModelStore.getState(); - if (isRealStep) { - if (realHitPoint) { - pushXRRealPoint(realHitPoint); - const stepNum = xrCalibration.step.endsWith('1') ? 1 : xrCalibration.step.endsWith('2') ? 2 : 3; - toast.success(`Ponto real ${stepNum} registrado!`); - } - return; - } - - if (isAligning) { - // Ignora o trigger físico para medições quando no modo virtual pois o clique do modelo é capturado pelo R3F onClick. - return; - } - if (st.selectionMode) { // Fresh raycast on the press itself (not every frame) for selection const triggerHits = raycaster.current.intersectObjects(scene.children, true); diff --git a/src/pages/XRSession.tsx b/src/pages/XRSession.tsx index ee60f6f..30d51e5 100644 --- a/src/pages/XRSession.tsx +++ b/src/pages/XRSession.tsx @@ -476,6 +476,115 @@ function XRGridLandingHandler() { ); } +/** Component that renders a laser from the camera (headset) to the virtual/real calibration target. */ +function XRVirtRealCalibLaser({ 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.8, depthTest: false }), []); + 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 virtual-real piece alignment. */ +function XRVirtRealCalibHandler() { + const [, force] = useState(0); + useEffect(() => subscribeXRCalibration(() => force(t => t + 1)), []); + + const isRealStep = xrCalibration.alignType === 'virt-real' && + (xrCalibration.step === 'await-real-1' || + xrCalibration.step === 'await-real-2' || + xrCalibration.step === 'await-real-3'); + + const reticleRef = useRef(null); + const [reticlePos, setReticlePos] = useState(null); + + // Continuous hit-test from viewer center + useXRHitTest( + isRealStep + ? (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' + ); + + const handleSelect = useCallback(() => { + if (!isRealStep || !reticlePos) return; + + const stepNum = xrCalibration.step.endsWith('1') ? 1 : xrCalibration.step.endsWith('2') ? 2 : 3; + pushXRRealPoint(reticlePos.clone()); + toast.success(`Ponto real ${stepNum} registrado!`); + }, [isRealStep, reticlePos]); + + // Update reticle position + useFrame(() => { + if (reticleRef.current) { + if (reticlePos && isRealStep) { + reticleRef.current.visible = true; + reticleRef.current.position.copy(reticlePos); + } else { + reticleRef.current.visible = false; + } + } + }); + + if (!isRealStep) return null; + + return ( + <> + + + {/* Golden reticle */} + + + + + + + + + + + + + + + + {/* Tap plane to capture select events */} + + + + + + ); +} + /** Renders the currently-active model (the one wrapped by grab/placement). */ function XRActiveModel() { const models = useModelStore((s) => s.models); @@ -1065,7 +1174,7 @@ const XRSession = () => { - + {/* In-world HUD — visible inside passthrough where DOM overlays cannot reach */}