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

This commit is contained in:
2026-05-28 21:29:49 +00:00
parent f36fd63bee
commit b270ca0bfd
3 changed files with 73 additions and 18 deletions
+1 -1
View File
@@ -1,7 +1,7 @@
{ {
"name": "vite_react_shadcn_ts", "name": "vite_react_shadcn_ts",
"private": true, "private": true,
"version": "1.0.4", "version": "1.1.0",
"type": "module", "type": "module",
"scripts": { "scripts": {
"dev": "vite", "dev": "vite",
+71 -16
View File
@@ -2,15 +2,17 @@ import { useRef, RefObject } from 'react';
import { useFrame, useThree } from '@react-three/fiber'; import { useFrame, useThree } from '@react-three/fiber';
import { useXR } from '@react-three/xr'; import { useXR } from '@react-three/xr';
import * as THREE from 'three'; import * as THREE from 'three';
import { getAllModelLocalGroups } from '@/lib/modelTransforms';
/** /**
* Quest 3 thumbstick locomotion (rig-relative). * Quest 3 thumbstick locomotion (rig-relative).
* *
* Mapeamento (thumbstick = axes[2]/axes[3] em xr-standard): * Mapeamento (thumbstick = axes[2]/axes[3] em xr-standard):
* • Para frente (axes[1] < -0.7), libera (axis volta a ~0) → teletransporta * • Para frente (axes[1] < -0.7) → exibe mira no modelo em tempo real.
* para o ponto onde o usuário está olhando (raycast da câmera). Se não * Ao soltar (axis volta a ~0) → teletransporta para a mira.
* houver geometria atingida, dá um passo de 0.5 m na direção do olhar. * Se não houver modelo atingido pelo olhar, projeta mira a 0.5m.
* • Para trás (axes[1] > 0.7) → passo de 0.3 m oposto ao olhar. * • Para trás (axes[1] > 0.7) → passo de 0.3 m oposto ao olhar.
* Mapeado diretamente para facilitar pequenos ajustes.
* • Lateral (|axes[0]| > 0.7) → snap-rotate ±30° no eixo Y do rig. * • Lateral (|axes[0]| > 0.7) → snap-rotate ±30° no eixo Y do rig.
* *
* NÃO lê grip ou trigger — esses são responsabilidade exclusiva de * NÃO lê grip ou trigger — esses são responsabilidade exclusiva de
@@ -28,24 +30,29 @@ const STEP_BACK = 0.3;
const STEP_FWD_NO_HIT = 0.5; const STEP_FWD_NO_HIT = 0.5;
export function ControllerLocomotion({ rigRef }: Props) { export function ControllerLocomotion({ rigRef }: Props) {
const { scene, camera } = useThree(); const { camera } = useThree();
const session = useXR((s) => s.session); const session = useXR((s) => s.session);
const fwdPushed = useRef(false); const fwdPushed = useRef(false);
const backLatched = useRef(false); const backLatched = useRef(false);
const rotLatched = useRef<0 | -1 | 1>(0); const rotLatched = useRef<0 | -1 | 1>(0);
const raycaster = useRef(new THREE.Raycaster()); const raycaster = useRef(new THREE.Raycaster());
const reticleRef = useRef<THREE.Mesh>(null);
useFrame(() => { useFrame(() => {
const rig = rigRef.current; const rig = rigRef.current;
if (!rig || !session) return; const reticle = reticleRef.current;
if (!rig || !session) {
if (reticle && reticle.visible) reticle.visible = false;
return;
}
let stickX = 0; let stickX = 0;
let stickY = 0; let stickY = 0;
for (const src of session.inputSources) { for (const src of session.inputSources) {
const gp = src.gamepad; const gp = src.gamepad;
if (!gp || gp.axes.length < 4) continue; if (!gp || gp.axes.length < 4) continue;
// Prefer left stick for locomotion // Preferencia pelo stick esquerdo para locomoção
if (src.handedness === 'left') { if (src.handedness === 'left') {
stickX = gp.axes[2] ?? 0; stickX = gp.axes[2] ?? 0;
stickY = gp.axes[3] ?? 0; stickY = gp.axes[3] ?? 0;
@@ -87,30 +94,78 @@ export function ControllerLocomotion({ rigRef }: Props) {
// ── Forward → teleport on release ───────────────────────────── // ── Forward → teleport on release ─────────────────────────────
if (stickY < -DEAD) { if (stickY < -DEAD) {
fwdPushed.current = true; fwdPushed.current = true;
} else if (fwdPushed.current && stickY > -RELEASE) {
fwdPushed.current = false; // Realiza raycast a partir da direção que o usuário está olhando (câmera)
// Cast from camera forward to find target on real/virtual geometry
const origin = new THREE.Vector3(); const origin = new THREE.Vector3();
camera.getWorldPosition(origin); camera.getWorldPosition(origin);
const dir = new THREE.Vector3(); const dir = new THREE.Vector3();
camera.getWorldDirection(dir); camera.getWorldDirection(dir);
raycaster.current.set(origin, dir); raycaster.current.set(origin, dir);
raycaster.current.far = 30; raycaster.current.far = 30;
const hits = raycaster.current.intersectObjects(scene.children, true);
// Filtra o raycast para atingir exclusivamente as peças 3D carregadas
const modelsObjects = getAllModelLocalGroups();
const hits = raycaster.current.intersectObjects(modelsObjects, true);
const hit = hits.find((h) => { const hit = hits.find((h) => {
const o = h.object; const o = h.object;
if (!(o instanceof THREE.Mesh)) return false; if (!(o instanceof THREE.Mesh)) return false;
if (o.userData.__edgeLine) return false; if (o.userData.__edgeLine) return false;
return true; return true;
}); });
const target = hit ? hit.point.clone() : origin.clone().add(dir.multiplyScalar(STEP_FWD_NO_HIT)); const target = hit ? hit.point.clone() : origin.clone().add(dir.multiplyScalar(STEP_FWD_NO_HIT));
const camWorld = new THREE.Vector3();
camera.getWorldPosition(camWorld); if (reticle) {
const delta = new THREE.Vector3().subVectors(target, camWorld); // Converte o ponto de mundo para o espaço local do rig
delta.y = 0; // preserva altura do usuário const localTarget = target.clone();
rig.position.add(delta); rig.worldToLocal(localTarget);
reticle.position.copy(localTarget);
if (hit && hit.face) {
const normalWorld = hit.face.normal.clone();
const nm = new THREE.Matrix3().getNormalMatrix(hit.object.matrixWorld);
normalWorld.applyMatrix3(nm).normalize();
// Converte normal do mundo para o espaço local do rig para orientar o retículo
const rigWorldQuat = new THREE.Quaternion();
rig.getWorldQuaternion(rigWorldQuat);
const normalLocal = normalWorld.clone().applyQuaternion(rigWorldQuat.invert());
const quaternion = new THREE.Quaternion().setFromUnitVectors(new THREE.Vector3(0, 1, 0), normalLocal);
reticle.quaternion.copy(quaternion);
} else {
reticle.rotation.set(-Math.PI / 2, 0, 0);
}
reticle.visible = true;
}
} else if (fwdPushed.current && stickY > -RELEASE) {
fwdPushed.current = false;
if (reticle && reticle.visible) {
reticle.visible = false;
// Recupera o ponto projetado e calcula o deslocamento em relação à câmera
const localTarget = reticle.position.clone();
const targetWorld = localTarget.clone();
rig.localToWorld(targetWorld);
const camWorld = new THREE.Vector3();
camera.getWorldPosition(camWorld);
const delta = new THREE.Vector3().subVectors(targetWorld, camWorld);
delta.y = 0; // preserva altura do rig do usuário
rig.position.add(delta);
}
} else {
if (reticle && reticle.visible) {
reticle.visible = false;
}
} }
}); });
return null; return (
<mesh ref={reticleRef} visible={false}>
<ringGeometry args={[0.07, 0.09, 32]} />
<meshBasicMaterial color="#06b6d4" transparent opacity={0.8} side={THREE.DoubleSide} depthWrite={false} />
</mesh>
);
} }
+1 -1
View File
@@ -337,7 +337,7 @@ const Index = () => {
{/* Footer */} {/* Footer */}
<p className="mt-16 text-center font-mono text-xs text-muted-foreground/50"> <p className="mt-16 text-center font-mono text-xs text-muted-foreground/50">
TrackSteelXR v1.05 Q.C. Inspection TrackSteelXR v1.10 Q.C. Inspection
</p> </p>
{showLogs && ( {showLogs && (