🚀 Auto-deploy: melhoria no snap e medição AR em 30/05/2026 10:10:48
This commit is contained in:
@@ -11,7 +11,6 @@ import {
|
|||||||
detectCircularEdgeAtPoint3D,
|
detectCircularEdgeAtPoint3D,
|
||||||
findNearestEdgeSegment3D
|
findNearestEdgeSegment3D
|
||||||
} from './SmartMeasure';
|
} from './SmartMeasure';
|
||||||
import { xrCalibration, pushXRRealPoint, subscribeXRCalibration } from './xrCalibrationBus';
|
|
||||||
import { toast } from 'sonner';
|
import { toast } from 'sonner';
|
||||||
|
|
||||||
const TRIG_ON = 0.7;
|
const TRIG_ON = 0.7;
|
||||||
@@ -58,12 +57,6 @@ export function XRControllerMeasure() {
|
|||||||
const lTrigState = useRef(false);
|
const lTrigState = useRef(false);
|
||||||
const [snapEnabled, setSnapEnabled] = useState(true);
|
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<string>('idle');
|
|
||||||
const ignoreTriggerUntilReleaseRef = useRef<boolean>(false);
|
|
||||||
|
|
||||||
const raycaster = useRef(new THREE.Raycaster());
|
const raycaster = useRef(new THREE.Raycaster());
|
||||||
const tmpOrigin = useRef(new THREE.Vector3());
|
const tmpOrigin = useRef(new THREE.Vector3());
|
||||||
@@ -96,25 +89,9 @@ export function XRControllerMeasure() {
|
|||||||
const measureMode = useModelStore.getState().measureMode;
|
const measureMode = useModelStore.getState().measureMode;
|
||||||
const selectionMode = useModelStore.getState().selectionMode;
|
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 (laserRef.current) laserRef.current.visible = false;
|
||||||
if (tipRef.current) tipRef.current.visible = false;
|
if (tipRef.current) tipRef.current.visible = false;
|
||||||
if ((!measureMode && !selectionMode && !isAligning) || !frame) {
|
if ((!measureMode && !selectionMode) || !frame) {
|
||||||
if (hitTestRequestedRef.current) {
|
if (hitTestRequestedRef.current) {
|
||||||
hitTestSourceRef.current = null;
|
hitTestSourceRef.current = null;
|
||||||
hitTestRequestedRef.current = false;
|
hitTestRequestedRef.current = false;
|
||||||
@@ -161,58 +138,6 @@ export function XRControllerMeasure() {
|
|||||||
raycaster.current.set(tmpOrigin.current, tmpDir.current);
|
raycaster.current.set(tmpOrigin.current, tmpDir.current);
|
||||||
raycaster.current.far = MAX_RAY;
|
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 snappedPoint: THREE.Vector3 | null = null;
|
||||||
let snapKind: 'vertex' | 'edge' | 'surface' | 'hole' = 'surface';
|
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;
|
let hoverDetected: { kind: 'hole' | 'edge'; value: number; position: THREE.Vector3; modelId?: string; endpoints?: { a: THREE.Vector3; b: THREE.Vector3 } } | null = null;
|
||||||
@@ -327,13 +252,7 @@ export function XRControllerMeasure() {
|
|||||||
|
|
||||||
// ── Update laser visual ───────────────────────────────────────────
|
// ── Update laser visual ───────────────────────────────────────────
|
||||||
if (laserRef.current && tipRef.current) {
|
if (laserRef.current && tipRef.current) {
|
||||||
if (isVirtualStep) {
|
const end = snappedPoint ?? tmpOrigin.current.clone().add(tmpDir.current.clone().multiplyScalar(MAX_RAY));
|
||||||
laserRef.current.visible = false;
|
|
||||||
tipRef.current.visible = false;
|
|
||||||
} 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;
|
const positions = laserGeom.current.attributes.position as THREE.BufferAttribute;
|
||||||
positions.setXYZ(0, tmpOrigin.current.x, tmpOrigin.current.y, tmpOrigin.current.z);
|
positions.setXYZ(0, tmpOrigin.current.x, tmpOrigin.current.y, tmpOrigin.current.z);
|
||||||
@@ -341,9 +260,7 @@ export function XRControllerMeasure() {
|
|||||||
positions.needsUpdate = true;
|
positions.needsUpdate = true;
|
||||||
laserGeom.current.computeBoundingSphere();
|
laserGeom.current.computeBoundingSphere();
|
||||||
|
|
||||||
const color = isAligning
|
const color = selectionMode ? '#a855f7' : (snapKind === 'hole' ? '#f59e0b' : snapKind === 'vertex' ? '#22c55e' : snapKind === 'edge' ? '#3b82f6' : '#eab308');
|
||||||
? '#eab308' // dourado para alinhamento Virt/Real
|
|
||||||
: (selectionMode ? '#a855f7' : (snapKind === 'hole' ? '#f59e0b' : snapKind === 'vertex' ? '#22c55e' : snapKind === 'edge' ? '#3b82f6' : '#eab308'));
|
|
||||||
|
|
||||||
tipColor.current.set(color);
|
tipColor.current.set(color);
|
||||||
(laserRef.current.material as THREE.LineBasicMaterial).color.set(color);
|
(laserRef.current.material as THREE.LineBasicMaterial).color.set(color);
|
||||||
@@ -351,12 +268,12 @@ export function XRControllerMeasure() {
|
|||||||
|
|
||||||
laserRef.current.visible = true;
|
laserRef.current.visible = true;
|
||||||
|
|
||||||
const tipPos = isRealStep ? realHitPoint : snappedPoint;
|
const tipPos = snappedPoint;
|
||||||
if (tipPos) {
|
if (tipPos) {
|
||||||
tipRef.current.visible = true;
|
tipRef.current.visible = true;
|
||||||
tipRef.current.position.copy(tipPos);
|
tipRef.current.position.copy(tipPos);
|
||||||
const dist = tmpOrigin.current.distanceTo(tipPos);
|
const dist = tmpOrigin.current.distanceTo(tipPos);
|
||||||
const strongSnap = isAligning || snapKind === 'vertex' || snapKind === 'edge' || snapKind === 'hole';
|
const strongSnap = snapKind === 'vertex' || snapKind === 'edge' || snapKind === 'hole';
|
||||||
const factor = strongSnap ? 0.030 : 0.012;
|
const factor = strongSnap ? 0.030 : 0.012;
|
||||||
const s = Math.max(strongSnap ? 0.010 : 0.004, dist * factor);
|
const s = Math.max(strongSnap ? 0.010 : 0.004, dist * factor);
|
||||||
tipRef.current.scale.setScalar(s);
|
tipRef.current.scale.setScalar(s);
|
||||||
@@ -364,38 +281,15 @@ export function XRControllerMeasure() {
|
|||||||
tipRef.current.visible = false;
|
tipRef.current.visible = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
// ── Right trigger: add point (measure) OR toggle selection ────────
|
// ── Right trigger: add point (measure) OR toggle selection ────────
|
||||||
const trigVal = gp?.buttons?.[0]?.value ?? (gp?.buttons?.[0]?.pressed ? 1 : 0);
|
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) {
|
if (!trigState.current && trigVal > TRIG_ON) {
|
||||||
trigState.current = true;
|
trigState.current = true;
|
||||||
|
|
||||||
if (ignoreTriggerUntilReleaseRef.current) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const st = useModelStore.getState();
|
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) {
|
if (st.selectionMode) {
|
||||||
// Fresh raycast on the press itself (not every frame) for selection
|
// Fresh raycast on the press itself (not every frame) for selection
|
||||||
const triggerHits = raycaster.current.intersectObjects(scene.children, true);
|
const triggerHits = raycaster.current.intersectObjects(scene.children, true);
|
||||||
|
|||||||
+110
-1
@@ -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<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 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<THREE.Group>(null);
|
||||||
|
const [reticlePos, setReticlePos] = useState<THREE.Vector3 | null>(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 (
|
||||||
|
<>
|
||||||
|
<XRVirtRealCalibLaser reticlePos={reticlePos} />
|
||||||
|
|
||||||
|
{/* Golden reticle */}
|
||||||
|
<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} depthTest={false} />
|
||||||
|
</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} depthTest={false} />
|
||||||
|
</mesh>
|
||||||
|
<mesh>
|
||||||
|
<ringGeometry args={[0.11, 0.115, 64]} />
|
||||||
|
<meshBasicMaterial color="#eab308" side={THREE.DoubleSide} transparent opacity={0.3} depthTest={false} />
|
||||||
|
</mesh>
|
||||||
|
</group>
|
||||||
|
|
||||||
|
{/* Tap plane to capture select events */}
|
||||||
|
<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);
|
||||||
@@ -1065,7 +1174,7 @@ const XRSession = () => {
|
|||||||
<ControllerLocomotion rigRef={rigRef} />
|
<ControllerLocomotion rigRef={rigRef} />
|
||||||
<VisibilityApplier />
|
<VisibilityApplier />
|
||||||
<XRGridLandingHandler />
|
<XRGridLandingHandler />
|
||||||
|
<XRVirtRealCalibHandler />
|
||||||
|
|
||||||
{/* In-world HUD — visible inside passthrough where DOM overlays cannot reach */}
|
{/* In-world HUD — visible inside passthrough where DOM overlays cannot reach */}
|
||||||
<XRHudInWorld
|
<XRHudInWorld
|
||||||
|
|||||||
Reference in New Issue
Block a user