🚀 Auto-deploy: melhoria no snap e medição AR em 28/05/2026 21:57:52

This commit is contained in:
2026-05-28 21:57:52 +00:00
parent d47fb9dd37
commit 1e4bd78128
3 changed files with 135 additions and 8 deletions
+126 -1
View File
@@ -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<THREE.Line>(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 <primitive ref={lineRef} object={new THREE.Line(geom, mat)} />;
}
/** 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<THREE.Group>(null);
const [reticlePos, setReticlePos] = useState<THREE.Vector3 | null>(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 (
<>
<XRGridLandingLaser reticlePos={reticlePos} />
{/* Retícula dourada premium */}
<group ref={reticleRef as any}>
<mesh onClick={handleSelect}>
<ringGeometry args={[0.08, 0.1, 32]} />
<meshBasicMaterial color="#eab308" side={THREE.DoubleSide} transparent opacity={0.8} />
</mesh>
<mesh rotation={[-Math.PI / 2, 0, 0]}>
<ringGeometry args={[0.005, 0.015, 16]} />
<meshBasicMaterial color="#eab308" side={THREE.DoubleSide} transparent opacity={0.6} />
</mesh>
<mesh>
<ringGeometry args={[0.11, 0.115, 64]} />
<meshBasicMaterial color="#eab308" side={THREE.DoubleSide} transparent opacity={0.3} />
</mesh>
</group>
{/* Plano invisível de click no fundo */}
<mesh
position={[0, 0, -2]}
onClick={handleSelect}
visible={false}
>
<planeGeometry args={[10, 10]} />
<meshBasicMaterial transparent opacity={0} />
</mesh>
</>
);
}
/** 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 = () => {
<XRControllerMeasure />
<ControllerLocomotion rigRef={rigRef} />
<VisibilityApplier />
<XRGridLandingHandler />
{/* In-world HUD — visible inside passthrough where DOM overlays cannot reach */}