Corrigiu menu AR
X-Lovable-Edit-ID: edt-4ec6472c-7475-46e1-abad-8dbac83a2d08 Co-authored-by: Reifonas <211114984+Reifonas@users.noreply.github.com>
This commit is contained in:
@@ -0,0 +1,84 @@
|
|||||||
|
import { useRef } from 'react';
|
||||||
|
import { useFrame, useThree } from '@react-three/fiber';
|
||||||
|
import * as THREE from 'three';
|
||||||
|
import { useModelStore } from '@/stores/useModelStore';
|
||||||
|
|
||||||
|
const TRIG_ON = 0.7;
|
||||||
|
const TRIG_OFF = 0.3;
|
||||||
|
const MAX_RAY = 10; // meters
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Right-controller trigger driven measurement for AR.
|
||||||
|
*
|
||||||
|
* When `measureMode` is on and the user pulls the right trigger, casts a ray
|
||||||
|
* forward from the controller (target-ray space). The first mesh hit becomes
|
||||||
|
* a measurement point. Two pulls = one measurement (handled by the store).
|
||||||
|
*
|
||||||
|
* Uses hysteresis so a single trigger pull adds exactly one point.
|
||||||
|
*/
|
||||||
|
export function XRControllerMeasure() {
|
||||||
|
const { scene, gl } = useThree();
|
||||||
|
const triggered = useRef(false);
|
||||||
|
const raycaster = useRef(new THREE.Raycaster());
|
||||||
|
const tmpOrigin = useRef(new THREE.Vector3());
|
||||||
|
const tmpDir = useRef(new THREE.Vector3());
|
||||||
|
const tmpQuat = useRef(new THREE.Quaternion());
|
||||||
|
|
||||||
|
useFrame((_state, _dt, frame: XRFrame | undefined) => {
|
||||||
|
const measureMode = useModelStore.getState().measureMode;
|
||||||
|
if (!measureMode || !frame) return;
|
||||||
|
const session = frame.session;
|
||||||
|
const refSpace = gl.xr.getReferenceSpace();
|
||||||
|
if (!session || !refSpace) return;
|
||||||
|
|
||||||
|
// Find right controller
|
||||||
|
let right: XRInputSource | null = null;
|
||||||
|
for (const src of session.inputSources) {
|
||||||
|
if (src.handedness === 'right') { right = src; break; }
|
||||||
|
}
|
||||||
|
if (!right) return;
|
||||||
|
|
||||||
|
const gp = right.gamepad;
|
||||||
|
const trigBtn = gp?.buttons?.[0];
|
||||||
|
const trigVal = trigBtn ? (trigBtn.value || (trigBtn.pressed ? 1 : 0)) : 0;
|
||||||
|
|
||||||
|
// Rising edge with hysteresis
|
||||||
|
if (!triggered.current && trigVal > TRIG_ON) {
|
||||||
|
triggered.current = true;
|
||||||
|
|
||||||
|
const raySpace = right.targetRaySpace ?? right.gripSpace;
|
||||||
|
if (!raySpace) return;
|
||||||
|
const pose = frame.getPose(raySpace, refSpace);
|
||||||
|
if (!pose) return;
|
||||||
|
|
||||||
|
const m = new THREE.Matrix4().fromArray(pose.transform.matrix);
|
||||||
|
tmpOrigin.current.setFromMatrixPosition(m);
|
||||||
|
tmpQuat.current.setFromRotationMatrix(m);
|
||||||
|
// Controller aims along -Z in WebXR target-ray convention
|
||||||
|
tmpDir.current.set(0, 0, -1).applyQuaternion(tmpQuat.current).normalize();
|
||||||
|
|
||||||
|
raycaster.current.set(tmpOrigin.current, tmpDir.current);
|
||||||
|
raycaster.current.far = MAX_RAY;
|
||||||
|
|
||||||
|
const hits = raycaster.current.intersectObjects(scene.children, true);
|
||||||
|
const hit = hits.find((h) => {
|
||||||
|
const o = h.object;
|
||||||
|
if (!(o instanceof THREE.Mesh)) return false;
|
||||||
|
if (o.userData.__edgeLine) return false;
|
||||||
|
if (o.geometry instanceof THREE.SphereGeometry) return false;
|
||||||
|
if (o.geometry instanceof THREE.RingGeometry) return false;
|
||||||
|
if (o.geometry instanceof THREE.PlaneGeometry) return false;
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
if (hit) {
|
||||||
|
useModelStore.getState().addMeasurePoint({
|
||||||
|
x: hit.point.x, y: hit.point.y, z: hit.point.z,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} else if (triggered.current && trigVal < TRIG_OFF) {
|
||||||
|
triggered.current = false;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
@@ -1,7 +1,8 @@
|
|||||||
import { useRef, ReactNode } from 'react';
|
import { useRef, ReactNode, useEffect } from 'react';
|
||||||
import { useFrame } from '@react-three/fiber';
|
import { useFrame } from '@react-three/fiber';
|
||||||
import * as THREE from 'three';
|
import * as THREE from 'three';
|
||||||
import { useControllerGrab, ControllerGrabSnapshot } from '@/hooks/useControllerGrab';
|
import { useControllerGrab, ControllerGrabSnapshot } from '@/hooks/useControllerGrab';
|
||||||
|
import { useModelStore } from '@/stores/useModelStore';
|
||||||
|
|
||||||
interface XRGrabbableProps {
|
interface XRGrabbableProps {
|
||||||
/** Allow uniform scaling via two-hand grab (distance between hands) */
|
/** Allow uniform scaling via two-hand grab (distance between hands) */
|
||||||
@@ -56,6 +57,17 @@ export function XRGrabbable({ allowScale = false, lockedActive = false, onGrabSt
|
|||||||
const haloRef = useRef<THREE.Mesh>(null);
|
const haloRef = useRef<THREE.Mesh>(null);
|
||||||
const everGrabbed = useRef(false);
|
const everGrabbed = useRef(false);
|
||||||
|
|
||||||
|
// Reset scale to 1 (position/rotation kept) whenever resetScale() is called.
|
||||||
|
const scaleResetNonce = useModelStore((s) => s.scaleResetNonce);
|
||||||
|
useEffect(() => {
|
||||||
|
const g = groupRef.current;
|
||||||
|
if (!g) return;
|
||||||
|
g.scale.set(1, 1, 1);
|
||||||
|
// Abort any in-progress two-hand scaling so it doesn't snap back.
|
||||||
|
dual.current = null;
|
||||||
|
single.current = null;
|
||||||
|
}, [scaleResetNonce]);
|
||||||
|
|
||||||
useFrame(() => {
|
useFrame(() => {
|
||||||
const group = groupRef.current;
|
const group = groupRef.current;
|
||||||
if (!group) return;
|
if (!group) return;
|
||||||
|
|||||||
@@ -330,6 +330,8 @@ function ToolsTab(p: XRHudInWorldProps) {
|
|||||||
const setMeasureMode = useModelStore((s) => s.setMeasureMode);
|
const setMeasureMode = useModelStore((s) => s.setMeasureMode);
|
||||||
const scaleRatio = useModelStore((s) => s.scaleRatio);
|
const scaleRatio = useModelStore((s) => s.scaleRatio);
|
||||||
const setScaleRatio = useModelStore((s) => s.setScaleRatio);
|
const setScaleRatio = useModelStore((s) => s.setScaleRatio);
|
||||||
|
const resetScale = useModelStore((s) => s.resetScale);
|
||||||
|
const clearMeasurements = useModelStore((s) => s.clearMeasurements);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<group>
|
<group>
|
||||||
@@ -380,6 +382,15 @@ function ToolsTab(p: XRHudInWorldProps) {
|
|||||||
onClick={() => setScaleRatio(preset)} />
|
onClick={() => setScaleRatio(preset)} />
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
|
{/* Reset Escala — devolve a peça ao tamanho 1:1 (cancela qualquer
|
||||||
|
escala manual feita com duas mãos). */}
|
||||||
|
<XR3DButton position={[0.10, -0.11, 0]} size={[0.062, 0.022]}
|
||||||
|
label="↺ Reset" color="#dc2626"
|
||||||
|
onClick={resetScale} fontSize={0.0085} />
|
||||||
|
{/* Limpa todos os labels de medição. */}
|
||||||
|
<XR3DButton position={[0.17, -0.11, 0]} size={[0.062, 0.022]}
|
||||||
|
label="✕ Medidas" color="#dc2626"
|
||||||
|
onClick={clearMeasurements} fontSize={0.0085} />
|
||||||
|
|
||||||
{/* Grid floor controls */}
|
{/* Grid floor controls */}
|
||||||
<GridFloorRow />
|
<GridFloorRow />
|
||||||
|
|||||||
+21
-7
@@ -16,6 +16,7 @@ import { XRBroadcastMirror } from '@/components/three/XRBroadcastMirror';
|
|||||||
import { findNearestVertex } from '@/components/three/SmartMeasure';
|
import { findNearestVertex } from '@/components/three/SmartMeasure';
|
||||||
import { XRHitTestPlacement } from '@/components/three/XRHitTestPlacement';
|
import { XRHitTestPlacement } from '@/components/three/XRHitTestPlacement';
|
||||||
import { XRGrabbable } from '@/components/three/XRGrabbable';
|
import { XRGrabbable } from '@/components/three/XRGrabbable';
|
||||||
|
import { XRControllerMeasure } from '@/components/three/XRControllerMeasure';
|
||||||
// DEVKIT: remove these imports + all `// DEVKIT:` blocks below to strip devkit
|
// DEVKIT: remove these imports + all `// DEVKIT:` blocks below to strip devkit
|
||||||
import { useDevKit } from '@/devkit/useDevKit';
|
import { useDevKit } from '@/devkit/useDevKit';
|
||||||
import { DevPanel } from '@/devkit/DevPanel';
|
import { DevPanel } from '@/devkit/DevPanel';
|
||||||
@@ -300,13 +301,24 @@ function XRMeasurementOverlay() {
|
|||||||
<mesh position={a}><sphereGeometry args={[0.003, 16, 16]} /><meshBasicMaterial color="#22c55e" /></mesh>
|
<mesh position={a}><sphereGeometry args={[0.003, 16, 16]} /><meshBasicMaterial color="#22c55e" /></mesh>
|
||||||
<mesh position={b}><sphereGeometry args={[0.003, 16, 16]} /><meshBasicMaterial color="#22c55e" /></mesh>
|
<mesh position={b}><sphereGeometry args={[0.003, 16, 16]} /><meshBasicMaterial color="#22c55e" /></mesh>
|
||||||
<Line points={[a, b]} color="#22c55e" lineWidth={2} />
|
<Line points={[a, b]} color="#22c55e" lineWidth={2} />
|
||||||
<Html position={mid} center distanceFactor={1} style={{ pointerEvents: 'none' }}>
|
<group position={mid}>
|
||||||
<div className="rounded bg-card/95 border border-primary/50 px-2 py-0.5 shadow-lg backdrop-blur-sm">
|
{/* Background plate */}
|
||||||
<span className="font-mono text-[11px] font-bold text-primary whitespace-nowrap">
|
<mesh position={[0, 0, 0]} renderOrder={998}>
|
||||||
{m.distanceMM.toFixed(1)} mm
|
<planeGeometry args={[0.09, 0.024]} />
|
||||||
</span>
|
<meshBasicMaterial color="#0b1220" transparent opacity={0.85} depthTest={false} />
|
||||||
</div>
|
</mesh>
|
||||||
</Html>
|
<Text
|
||||||
|
position={[0, 0, 0.001]}
|
||||||
|
fontSize={0.012}
|
||||||
|
color="#3b82f6"
|
||||||
|
anchorX="center"
|
||||||
|
anchorY="middle"
|
||||||
|
renderOrder={999}
|
||||||
|
material-depthTest={false}
|
||||||
|
>
|
||||||
|
{`${m.distanceMM.toFixed(1)} mm`}
|
||||||
|
</Text>
|
||||||
|
</group>
|
||||||
</group>
|
</group>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
@@ -586,9 +598,11 @@ const XRSession = () => {
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
<XRMeasurementOverlay />
|
<XRMeasurementOverlay />
|
||||||
|
<XRControllerMeasure />
|
||||||
<ControllerFineTuning freeMove={freeMove} />
|
<ControllerFineTuning freeMove={freeMove} />
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
{/* In-world HUD — visible inside passthrough where DOM overlays cannot reach */}
|
{/* In-world HUD — visible inside passthrough where DOM overlays cannot reach */}
|
||||||
<XRHudInWorld
|
<XRHudInWorld
|
||||||
freeMove={freeMove}
|
freeMove={freeMove}
|
||||||
|
|||||||
@@ -172,6 +172,10 @@ interface ModelStore {
|
|||||||
|
|
||||||
scaleRatio: ScaleRatio;
|
scaleRatio: ScaleRatio;
|
||||||
setScaleRatio: (r: ScaleRatio) => void;
|
setScaleRatio: (r: ScaleRatio) => void;
|
||||||
|
/** Incremented whenever the user asks to reset the AR scale; XRGrabbable
|
||||||
|
* watches this and snaps its group.scale back to 1. */
|
||||||
|
scaleResetNonce: number;
|
||||||
|
resetScale: () => void;
|
||||||
|
|
||||||
opacity: number;
|
opacity: number;
|
||||||
setOpacity: (opacity: number) => void;
|
setOpacity: (opacity: number) => void;
|
||||||
@@ -375,6 +379,25 @@ export const useModelStore = create<ModelStore>((set, get) => ({
|
|||||||
|
|
||||||
scaleRatio: SCALE_PRESETS.find(p => p.label === '1:1')!,
|
scaleRatio: SCALE_PRESETS.find(p => p.label === '1:1')!,
|
||||||
setScaleRatio: (scaleRatio) => set({ scaleRatio }),
|
setScaleRatio: (scaleRatio) => set({ scaleRatio }),
|
||||||
|
scaleResetNonce: 0,
|
||||||
|
resetScale: () => set((state) => {
|
||||||
|
const oneToOne = SCALE_PRESETS.find(p => p.label === '1:1')!;
|
||||||
|
const activeId = state.activeModelId;
|
||||||
|
const models = activeId
|
||||||
|
? state.models.map(m => m.id === activeId
|
||||||
|
? { ...m, fineTuning: { ...m.fineTuning, scale: 1 } }
|
||||||
|
: m)
|
||||||
|
: state.models;
|
||||||
|
const active = models.find(m => m.id === activeId);
|
||||||
|
const fineTuning = active ? { ...active.fineTuning } : { ...state.fineTuning, scale: 1 };
|
||||||
|
if (active) savePlacement(active.fileName, fineTuning);
|
||||||
|
return {
|
||||||
|
scaleRatio: oneToOne,
|
||||||
|
models,
|
||||||
|
fineTuning,
|
||||||
|
scaleResetNonce: state.scaleResetNonce + 1,
|
||||||
|
};
|
||||||
|
}),
|
||||||
|
|
||||||
opacity: 1,
|
opacity: 1,
|
||||||
setOpacity: (opacity) => set({ opacity }),
|
setOpacity: (opacity) => set({ opacity }),
|
||||||
|
|||||||
Reference in New Issue
Block a user