🚀 Auto-deploy: melhoria no snap e medição AR em 28/05/2026 21:29:49
This commit is contained in:
+1
-1
@@ -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",
|
||||||
|
|||||||
@@ -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
@@ -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 && (
|
||||||
|
|||||||
Reference in New Issue
Block a user