Corrigiu HUD AR e grapple

X-Lovable-Edit-ID: edt-6b43df93-de2b-4454-b9e5-aa4be0671b1b
Co-authored-by: Reifonas <211114984+Reifonas@users.noreply.github.com>
This commit is contained in:
gpt-engineer-app[bot]
2026-05-14 18:27:48 +00:00
3 changed files with 77 additions and 69 deletions
+11 -1
View File
@@ -6,6 +6,8 @@ import { useControllerGrab, ControllerGrabSnapshot } from '@/hooks/useController
interface XRGrabbableProps {
/** Allow uniform scaling via two-hand grab (distance between hands) */
allowScale?: boolean;
/** When true, the active model is locked — grab is ignored entirely */
lockedActive?: boolean;
/** Called the first time the user grabs (e.g. to exit placement mode) */
onGrabStart?: () => void;
children: ReactNode;
@@ -45,7 +47,7 @@ const _tmpQuat = new THREE.Quaternion();
* On release the group keeps its last pose (we don't write to the store —
* the manual fine-tuning sliders remain available as additional offsets).
*/
export function XRGrabbable({ allowScale = false, onGrabStart, children }: XRGrabbableProps) {
export function XRGrabbable({ allowScale = false, lockedActive = false, onGrabStart, children }: XRGrabbableProps) {
const groupRef = useRef<THREE.Group>(null);
const grab = useControllerGrab();
@@ -61,6 +63,14 @@ export function XRGrabbable({ allowScale = false, onGrabStart, children }: XRGra
const L = snap.left;
const R = snap.right;
// Locked: ignore all grab input — release any in-progress grabs and bail.
if (lockedActive) {
if (single.current) single.current = null;
if (dual.current) dual.current = null;
if (haloRef.current) haloRef.current.visible = false;
return;
}
const lActive = L.isGrabbing && L.hasPose;
const rActive = R.isGrabbing && R.hasPose;
+47 -9
View File
@@ -53,6 +53,10 @@ export function XRHudInWorld(props: XRHudInWorldProps) {
const leftCtrl = useXRInputSourceState('controller', 'left');
const [open, setOpen] = useState(true);
const [tab, setTab] = useState<Tab>('scene');
/** When pinned, the panel stays fixed in world space and does NOT follow the head. */
const [pinned, setPinned] = useState(true);
/** Forces re-anchor on next frame (used by "trazer" / unpin→pin). */
const reanchorRequested = useRef(true);
const panelRef = useRef<THREE.Group>(null);
const wristRef = useRef<THREE.Group>(null);
@@ -70,15 +74,30 @@ export function XRHudInWorld(props: XRHudInWorldProps) {
lastABtn.current = pressed;
}
// Floating panel ~70cm in front of head, yaw-locked, damped
// Floating panel:
// • If pinned and already anchored once → don't move it (fixed in space).
// • If unpinned → continuously follow head with damping.
// • On (re)anchor request → snap target once and stop.
if (panelRef.current && open) {
const fwd = new THREE.Vector3(0, -0.05, -0.7).applyQuaternion(camera.quaternion);
targetPos.current.copy(camera.position).add(fwd);
const wantPos = new THREE.Vector3().copy(camera.position).add(fwd);
const euler = new THREE.Euler().setFromQuaternion(camera.quaternion, 'YXZ');
targetQuat.current.setFromEuler(new THREE.Euler(0, euler.y, 0, 'YXZ'));
const wantQuat = new THREE.Quaternion().setFromEuler(new THREE.Euler(0, euler.y, 0, 'YXZ'));
if (reanchorRequested.current) {
targetPos.current.copy(wantPos);
targetQuat.current.copy(wantQuat);
panelRef.current.position.copy(wantPos);
panelRef.current.quaternion.copy(wantQuat);
reanchorRequested.current = false;
} else if (!pinned) {
targetPos.current.copy(wantPos);
targetQuat.current.copy(wantQuat);
panelRef.current.position.lerp(targetPos.current, 0.12);
panelRef.current.quaternion.slerp(targetQuat.current, 0.18);
}
// pinned + already anchored → leave panel where it is.
}
// Wrist toggle follows left controller
if (wristRef.current) {
@@ -102,14 +121,29 @@ export function XRHudInWorld(props: XRHudInWorldProps) {
<WristToggle ref={wristRef} open={open} onToggle={() => setOpen((v) => !v)} />
{open && (
<group ref={panelRef}>
<FloatingPanel tab={tab} setTab={setTab} {...props} />
<FloatingPanel
tab={tab}
setTab={setTab}
pinned={pinned}
onTogglePin={() => setPinned((v) => !v)}
onRecenter={() => { reanchorRequested.current = true; }}
{...props}
/>
</group>
)}
</>
);
}
function FloatingPanel({ tab, setTab, ...p }: { tab: Tab; setTab: (t: Tab) => void } & XRHudInWorldProps) {
function FloatingPanel({
tab, setTab, pinned, onTogglePin, onRecenter, ...p
}: {
tab: Tab;
setTab: (t: Tab) => void;
pinned: boolean;
onTogglePin: () => void;
onRecenter: () => void;
} & XRHudInWorldProps) {
const tabs: { id: Tab; label: string; icon: string }[] = [
{ id: 'scene', label: 'Cena', icon: '🧩' },
{ id: 'tools', label: 'Ferram.', icon: '🛠' },
@@ -124,10 +158,14 @@ function FloatingPanel({ tab, setTab, ...p }: { tab: Tab; setTab: (t: Tab) => vo
anchorX="left" anchorY="middle">
TrackSteelXR · HUD AR
</Text>
<Text position={[W / 2 - 0.012, H / 2 - 0.018, 0.002]} fontSize={0.008} color="#94a3b8"
anchorX="right" anchorY="middle">
Botão A (esq) abre/fecha
</Text>
{/* Pin / Recenter controls (replace the previous static "A button" hint) */}
<group position={[W / 2 - 0.012, H / 2 - 0.018, 0.002]}>
<XR3DButton position={[-0.045, 0, 0]} size={[0.05, 0.022]}
label={pinned ? '📌 Fixo' : '↻ Solto'}
active={pinned} onClick={onTogglePin} fontSize={0.007} />
<XR3DButton position={[0.012, 0, 0]} size={[0.058, 0.022]}
label="⎚ Trazer" onClick={onRecenter} fontSize={0.007} />
</group>
<group position={[0, H / 2 - 0.045, 0.001]}>
{tabs.map((t, i) => (
+17 -57
View File
@@ -172,9 +172,13 @@ function XRActiveModel() {
function ControllerFineTuning({ freeMove }: { freeMove: boolean }) {
const { setFineTuning } = useModelStore();
const session = useXR((s) => s.session);
const isActiveLocked = useModelStore((s) => {
const a = s.models.find(m => m.id === s.activeModelId);
return !!a?.locked;
});
useFrame(() => {
if (!session) return;
if (!session || isActiveLocked) return;
const inputSources = session.inputSources;
if (!inputSources) return;
@@ -374,63 +378,14 @@ function XRGrid() {
// ImageTrackingAnchor removed — replaced by XRHitTestPlacement
// ─── XR Debug HUD (floats in front of viewer) ──────────
// Shows live grip/trigger values and isGrabbing state per hand.
// Essential for diagnosing grab issues without external monitoring.
function XRDebugHud() {
const session = useXR((s) => s.session);
const groupRef = useRef<THREE.Group>(null);
const { camera } = useThree();
const [text, setText] = useState('XR aguardando inputs...');
useFrame(() => {
if (!groupRef.current) return;
// Anchor the HUD to a fixed offset in front of the camera
const offset = new THREE.Vector3(-0.25, -0.18, -0.6).applyQuaternion(camera.quaternion);
groupRef.current.position.copy(camera.position).add(offset);
groupRef.current.quaternion.copy(camera.quaternion);
if (!session) return;
const sources = Array.from(session.inputSources);
const lines: string[] = [`inputs: ${sources.length}`];
for (const src of sources) {
const gp = src.gamepad;
const grip = gp?.buttons[2];
const trig = gp?.buttons[1];
const gv = grip ? (grip.value || (grip.pressed ? 1 : 0)) : 0;
const tv = trig ? (trig.value || (trig.pressed ? 1 : 0)) : 0;
const grabbing = gv > 0.7 ? 'GRAB' : gv > 0.3 ? '...' : ' ';
lines.push(`${src.handedness.padEnd(5)} grip:${gv.toFixed(2)} trg:${tv.toFixed(2)} ${grabbing}`);
}
setText(lines.join('\n'));
});
return (
<group ref={groupRef}>
<mesh position={[0, 0, -0.001]}>
<planeGeometry args={[0.32, 0.10]} />
<meshBasicMaterial color="#000000" transparent opacity={0.55} depthWrite={false} />
</mesh>
<Text
fontSize={0.012}
color="#22c55e"
anchorX="left"
anchorY="top"
position={[-0.155, 0.045, 0]}
maxWidth={0.30}
lineHeight={1.25}
>
{text}
</Text>
</group>
);
}
// ─── XRSession Page ────────────────────────────────────
const XRSession = () => {
const navigate = useNavigate();
const { model, anchorMode, setAnchorMode } = useModelStore();
const isActiveLocked = useModelStore((s) => {
const a = s.models.find(m => m.id === s.activeModelId);
return !!a?.locked;
});
const [inXR, setInXR] = useState(false);
const [freeMove, setFreeMove] = useState(true);
const [placementMode, setPlacementMode] = useState(true); // start in placement mode
@@ -538,12 +493,15 @@ const XRSession = () => {
{effectiveInXR ? (
<>
{/* In XR (or SimXR): grab wrapper → ACTIVE model. Other models render as static background. */}
<XRBackgroundModels />
{/* In XR (or SimXR): all models share the same placement origin so
switching active doesn't make others appear to "disappear".
Only the ACTIVE model receives grab transforms. */}
{simXR ? (
<group position={[0, 1.0, -1.2]}>
<XRBackgroundModels />
<XRGrabbable
allowScale={allowScale}
lockedActive={isActiveLocked}
onGrabStart={() => { if (placementMode) setPlacementMode(false); }}
>
<XRActiveModel />
@@ -558,8 +516,10 @@ const XRSession = () => {
toast.success('Modelo posicionado na superfície!');
}}
>
<XRBackgroundModels />
<XRGrabbable
allowScale={allowScale}
lockedActive={isActiveLocked}
onGrabStart={() => { if (placementMode) setPlacementMode(false); }}
>
<XRActiveModel />
@@ -569,7 +529,7 @@ const XRSession = () => {
<XRMeasurementOverlay />
<ControllerFineTuning freeMove={freeMove} />
<XRDebugHud />
{/* In-world HUD — visible inside passthrough where DOM overlays cannot reach */}
<XRHudInWorld