🚀 Auto-deploy: melhoria no snap e medição AR em 29/05/2026 20:32:41

This commit is contained in:
2026-05-29 20:32:41 +00:00
parent 1e4bd78128
commit 0f017d0e4b
4 changed files with 391 additions and 29 deletions
+88 -10
View File
@@ -11,6 +11,8 @@ import {
detectCircularEdgeAtPoint3D,
findNearestEdgeSegment3D
} from './SmartMeasure';
import { xrCalibration, pushXRRealPoint } from './xrCalibrationBus';
import { toast } from 'sonner';
const TRIG_ON = 0.7;
const TRIG_OFF = 0.3;
@@ -80,12 +82,31 @@ export function XRControllerMeasure() {
// running them every frame at 72fps can stall the Quest right after
// toggling "Medir" from the Ferramentas tab. We cap heavy work to ~24Hz.
// Trigger/A/B/L-trigger polling continues every frame for responsiveness.
const hitTestSourceRef = useRef<any>(null);
const hitTestRequestedRef = useRef<boolean>(false);
useFrame((_state, _dt, frame: XRFrame | undefined) => {
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 (laserRef.current) laserRef.current.visible = false;
if (tipRef.current) tipRef.current.visible = false;
if ((!measureMode && !selectionMode) || !frame) return;
if ((!measureMode && !selectionMode && !isAligning) || !frame) {
if (hitTestRequestedRef.current) {
hitTestSourceRef.current = null;
hitTestRequestedRef.current = false;
}
return;
}
const session = frame.session;
const refSpace = gl.xr.getReferenceSpace();
@@ -126,6 +147,43 @@ 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');
if (session && right && !hitTestRequestedRef.current && isAligning) {
hitTestRequestedRef.current = true;
session.requestHitTestSource({ space: right.targetRaySpace })
.then((source) => {
hitTestSourceRef.current = source;
})
.catch((err) => {
console.error('[XR] Failed to request controller hit test source:', err);
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;
@@ -240,27 +298,32 @@ export function XRControllerMeasure() {
// ── Update laser visual ───────────────────────────────────────────
if (laserRef.current && tipRef.current) {
const end = snappedPoint ?? tmpOrigin.current.clone().add(tmpDir.current.clone().multiplyScalar(MAX_RAY));
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 = selectionMode ? '#a855f7' : (snapKind === 'hole' ? '#f59e0b' : snapKind === 'vertex' ? '#22c55e' : snapKind === 'edge' ? '#3b82f6' : '#eab308');
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;
if (snappedPoint) {
const tipPos = isRealStep ? realHitPoint : snappedPoint;
if (tipPos) {
tipRef.current.visible = true;
tipRef.current.position.copy(snappedPoint);
// Tip scales with distance from controller. Snap "forte" (vértice,
// aresta, furo) recebe um marcador ~3× maior do que uma superfície
// genérica, para que o ponto destacado fique bem visível.
const dist = tmpOrigin.current.distanceTo(snappedPoint);
const strongSnap = snapKind === 'vertex' || snapKind === 'edge' || snapKind === 'hole';
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);
@@ -274,6 +337,21 @@ export function XRControllerMeasure() {
if (!trigState.current && trigVal > TRIG_ON) {
trigState.current = true;
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);