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:
@@ -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;
|
||||
|
||||
|
||||
@@ -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,14 +74,29 @@ 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'));
|
||||
panelRef.current.position.lerp(targetPos.current, 0.12);
|
||||
panelRef.current.quaternion.slerp(targetQuat.current, 0.18);
|
||||
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
|
||||
@@ -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
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user