🚀 Auto-deploy: melhoria no snap e medição AR em 28/05/2026 21:57:52
This commit is contained in:
@@ -617,12 +617,9 @@ function GridFloorRow() {
|
|||||||
const nudgeGridY = useModelStore((s) => s.nudgeGridY);
|
const nudgeGridY = useModelStore((s) => s.nudgeGridY);
|
||||||
const gridCalibMode = useModelStore((s) => s.gridCalibMode);
|
const gridCalibMode = useModelStore((s) => s.gridCalibMode);
|
||||||
const setGridCalibMode = useModelStore((s) => s.setGridCalibMode);
|
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 (
|
return (
|
||||||
<group>
|
<group>
|
||||||
<Text position={[-0.24, -0.15, 0]} fontSize={0.008} color="#94a3b8" anchorX="left">
|
<Text position={[-0.24, -0.15, 0]} fontSize={0.008} color="#94a3b8" anchorX="left">
|
||||||
@@ -636,8 +633,8 @@ function GridFloorRow() {
|
|||||||
onClick={() => nudgeGridY(-0.01)} />
|
onClick={() => nudgeGridY(-0.01)} />
|
||||||
<XR3DButton position={[0.005, -0.175, 0]} size={[0.04, 0.022]} label="+1cm"
|
<XR3DButton position={[0.005, -0.175, 0]} size={[0.04, 0.022]} label="+1cm"
|
||||||
onClick={() => nudgeGridY(0.01)} />
|
onClick={() => nudgeGridY(0.01)} />
|
||||||
<XR3DButton position={[0.07, -0.175, 0]} size={[0.075, 0.022]} label="Pousar chão"
|
<XR3DButton position={[0.07, -0.175, 0]} size={[0.075, 0.022]} label={gridLandingMode ? "Pousando" : "Pousar"}
|
||||||
onClick={placeAtFloor} />
|
active={gridLandingMode} onClick={() => setGridLandingMode(!gridLandingMode)} />
|
||||||
<XR3DButton position={[0.155, -0.175, 0]} size={[0.075, 0.022]} label={gridCalibMode ? "Calibrando" : "Calibrar"}
|
<XR3DButton position={[0.155, -0.175, 0]} size={[0.075, 0.022]} label={gridCalibMode ? "Calibrando" : "Calibrar"}
|
||||||
active={gridCalibMode} onClick={() => setGridCalibMode(!gridCalibMode)} />
|
active={gridCalibMode} onClick={() => setGridCalibMode(!gridCalibMode)} />
|
||||||
</group>
|
</group>
|
||||||
|
|||||||
+126
-1
@@ -2,7 +2,7 @@ import { useEffect, useRef, useMemo, useCallback, useState } from 'react';
|
|||||||
import { useNavigate } from 'react-router-dom';
|
import { useNavigate } from 'react-router-dom';
|
||||||
import { Canvas, useFrame, useThree } from '@react-three/fiber';
|
import { Canvas, useFrame, useThree } from '@react-three/fiber';
|
||||||
import { useGLTF, Grid, Html, Line, OrbitControls, Text } from '@react-three/drei';
|
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 * as THREE from 'three';
|
||||||
import { useModelStore } from '@/stores/useModelStore';
|
import { useModelStore } from '@/stores/useModelStore';
|
||||||
import { toast } from 'sonner';
|
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). */
|
/** Renders the currently-active model (the one wrapped by grab/placement). */
|
||||||
function XRActiveModel() {
|
function XRActiveModel() {
|
||||||
const models = useModelStore((s) => s.models);
|
const models = useModelStore((s) => s.models);
|
||||||
@@ -845,6 +969,7 @@ const XRSession = () => {
|
|||||||
<XRControllerMeasure />
|
<XRControllerMeasure />
|
||||||
<ControllerLocomotion rigRef={rigRef} />
|
<ControllerLocomotion rigRef={rigRef} />
|
||||||
<VisibilityApplier />
|
<VisibilityApplier />
|
||||||
|
<XRGridLandingHandler />
|
||||||
|
|
||||||
|
|
||||||
{/* In-world HUD — visible inside passthrough where DOM overlays cannot reach */}
|
{/* In-world HUD — visible inside passthrough where DOM overlays cannot reach */}
|
||||||
|
|||||||
@@ -260,6 +260,9 @@ interface ModelStore {
|
|||||||
/** When true, the next click on a model face sets grid Y to that point. */
|
/** When true, the next click on a model face sets grid Y to that point. */
|
||||||
gridCalibMode: boolean;
|
gridCalibMode: boolean;
|
||||||
setGridCalibMode: (b: boolean) => void;
|
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). */
|
/** Section/clipping cut tool (X/Y/Z axis-aligned planes in world space). */
|
||||||
sectionEnabled: { x: boolean; y: boolean; z: boolean };
|
sectionEnabled: { x: boolean; y: boolean; z: boolean };
|
||||||
@@ -555,6 +558,8 @@ export const useModelStore = create<ModelStore>((set, get) => ({
|
|||||||
nudgeGridY: (delta) => set((s) => ({ gridY: s.gridY + delta, gridAutoFollow: false })),
|
nudgeGridY: (delta) => set((s) => ({ gridY: s.gridY + delta, gridAutoFollow: false })),
|
||||||
gridCalibMode: false,
|
gridCalibMode: false,
|
||||||
setGridCalibMode: (gridCalibMode) => set({ gridCalibMode }),
|
setGridCalibMode: (gridCalibMode) => set({ gridCalibMode }),
|
||||||
|
gridLandingMode: false,
|
||||||
|
setGridLandingMode: (gridLandingMode) => set({ gridLandingMode }),
|
||||||
|
|
||||||
sectionEnabled: { x: false, y: false, z: false },
|
sectionEnabled: { x: false, y: false, z: false },
|
||||||
sectionInvert: { x: false, y: false, z: false },
|
sectionInvert: { x: false, y: false, z: false },
|
||||||
|
|||||||
Reference in New Issue
Block a user