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 {
|
interface XRGrabbableProps {
|
||||||
/** Allow uniform scaling via two-hand grab (distance between hands) */
|
/** Allow uniform scaling via two-hand grab (distance between hands) */
|
||||||
allowScale?: boolean;
|
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) */
|
/** Called the first time the user grabs (e.g. to exit placement mode) */
|
||||||
onGrabStart?: () => void;
|
onGrabStart?: () => void;
|
||||||
children: ReactNode;
|
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 —
|
* 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).
|
* 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 groupRef = useRef<THREE.Group>(null);
|
||||||
const grab = useControllerGrab();
|
const grab = useControllerGrab();
|
||||||
|
|
||||||
@@ -61,6 +63,14 @@ export function XRGrabbable({ allowScale = false, onGrabStart, children }: XRGra
|
|||||||
const L = snap.left;
|
const L = snap.left;
|
||||||
const R = snap.right;
|
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 lActive = L.isGrabbing && L.hasPose;
|
||||||
const rActive = R.isGrabbing && R.hasPose;
|
const rActive = R.isGrabbing && R.hasPose;
|
||||||
|
|
||||||
|
|||||||
@@ -53,6 +53,10 @@ export function XRHudInWorld(props: XRHudInWorldProps) {
|
|||||||
const leftCtrl = useXRInputSourceState('controller', 'left');
|
const leftCtrl = useXRInputSourceState('controller', 'left');
|
||||||
const [open, setOpen] = useState(true);
|
const [open, setOpen] = useState(true);
|
||||||
const [tab, setTab] = useState<Tab>('scene');
|
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 panelRef = useRef<THREE.Group>(null);
|
||||||
const wristRef = useRef<THREE.Group>(null);
|
const wristRef = useRef<THREE.Group>(null);
|
||||||
@@ -70,14 +74,29 @@ export function XRHudInWorld(props: XRHudInWorldProps) {
|
|||||||
lastABtn.current = pressed;
|
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) {
|
if (panelRef.current && open) {
|
||||||
const fwd = new THREE.Vector3(0, -0.05, -0.7).applyQuaternion(camera.quaternion);
|
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');
|
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'));
|
||||||
panelRef.current.position.lerp(targetPos.current, 0.12);
|
|
||||||
panelRef.current.quaternion.slerp(targetQuat.current, 0.18);
|
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
|
// Wrist toggle follows left controller
|
||||||
@@ -102,14 +121,29 @@ export function XRHudInWorld(props: XRHudInWorldProps) {
|
|||||||
<WristToggle ref={wristRef} open={open} onToggle={() => setOpen((v) => !v)} />
|
<WristToggle ref={wristRef} open={open} onToggle={() => setOpen((v) => !v)} />
|
||||||
{open && (
|
{open && (
|
||||||
<group ref={panelRef}>
|
<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>
|
</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 }[] = [
|
const tabs: { id: Tab; label: string; icon: string }[] = [
|
||||||
{ id: 'scene', label: 'Cena', icon: '🧩' },
|
{ id: 'scene', label: 'Cena', icon: '🧩' },
|
||||||
{ id: 'tools', label: 'Ferram.', 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">
|
anchorX="left" anchorY="middle">
|
||||||
TrackSteelXR · HUD AR
|
TrackSteelXR · HUD AR
|
||||||
</Text>
|
</Text>
|
||||||
<Text position={[W / 2 - 0.012, H / 2 - 0.018, 0.002]} fontSize={0.008} color="#94a3b8"
|
{/* Pin / Recenter controls (replace the previous static "A button" hint) */}
|
||||||
anchorX="right" anchorY="middle">
|
<group position={[W / 2 - 0.012, H / 2 - 0.018, 0.002]}>
|
||||||
Botão A (esq) abre/fecha
|
<XR3DButton position={[-0.045, 0, 0]} size={[0.05, 0.022]}
|
||||||
</Text>
|
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]}>
|
<group position={[0, H / 2 - 0.045, 0.001]}>
|
||||||
{tabs.map((t, i) => (
|
{tabs.map((t, i) => (
|
||||||
|
|||||||
+17
-57
@@ -172,9 +172,13 @@ function XRActiveModel() {
|
|||||||
function ControllerFineTuning({ freeMove }: { freeMove: boolean }) {
|
function ControllerFineTuning({ freeMove }: { freeMove: boolean }) {
|
||||||
const { setFineTuning } = useModelStore();
|
const { setFineTuning } = useModelStore();
|
||||||
const session = useXR((s) => s.session);
|
const session = useXR((s) => s.session);
|
||||||
|
const isActiveLocked = useModelStore((s) => {
|
||||||
|
const a = s.models.find(m => m.id === s.activeModelId);
|
||||||
|
return !!a?.locked;
|
||||||
|
});
|
||||||
|
|
||||||
useFrame(() => {
|
useFrame(() => {
|
||||||
if (!session) return;
|
if (!session || isActiveLocked) return;
|
||||||
const inputSources = session.inputSources;
|
const inputSources = session.inputSources;
|
||||||
if (!inputSources) return;
|
if (!inputSources) return;
|
||||||
|
|
||||||
@@ -374,63 +378,14 @@ function XRGrid() {
|
|||||||
|
|
||||||
// ImageTrackingAnchor removed — replaced by XRHitTestPlacement
|
// 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 ────────────────────────────────────
|
// ─── XRSession Page ────────────────────────────────────
|
||||||
const XRSession = () => {
|
const XRSession = () => {
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const { model, anchorMode, setAnchorMode } = useModelStore();
|
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 [inXR, setInXR] = useState(false);
|
||||||
const [freeMove, setFreeMove] = useState(true);
|
const [freeMove, setFreeMove] = useState(true);
|
||||||
const [placementMode, setPlacementMode] = useState(true); // start in placement mode
|
const [placementMode, setPlacementMode] = useState(true); // start in placement mode
|
||||||
@@ -538,12 +493,15 @@ const XRSession = () => {
|
|||||||
|
|
||||||
{effectiveInXR ? (
|
{effectiveInXR ? (
|
||||||
<>
|
<>
|
||||||
{/* In XR (or SimXR): grab wrapper → ACTIVE model. Other models render as static background. */}
|
{/* In XR (or SimXR): all models share the same placement origin so
|
||||||
<XRBackgroundModels />
|
switching active doesn't make others appear to "disappear".
|
||||||
|
Only the ACTIVE model receives grab transforms. */}
|
||||||
{simXR ? (
|
{simXR ? (
|
||||||
<group position={[0, 1.0, -1.2]}>
|
<group position={[0, 1.0, -1.2]}>
|
||||||
|
<XRBackgroundModels />
|
||||||
<XRGrabbable
|
<XRGrabbable
|
||||||
allowScale={allowScale}
|
allowScale={allowScale}
|
||||||
|
lockedActive={isActiveLocked}
|
||||||
onGrabStart={() => { if (placementMode) setPlacementMode(false); }}
|
onGrabStart={() => { if (placementMode) setPlacementMode(false); }}
|
||||||
>
|
>
|
||||||
<XRActiveModel />
|
<XRActiveModel />
|
||||||
@@ -558,8 +516,10 @@ const XRSession = () => {
|
|||||||
toast.success('Modelo posicionado na superfície!');
|
toast.success('Modelo posicionado na superfície!');
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
|
<XRBackgroundModels />
|
||||||
<XRGrabbable
|
<XRGrabbable
|
||||||
allowScale={allowScale}
|
allowScale={allowScale}
|
||||||
|
lockedActive={isActiveLocked}
|
||||||
onGrabStart={() => { if (placementMode) setPlacementMode(false); }}
|
onGrabStart={() => { if (placementMode) setPlacementMode(false); }}
|
||||||
>
|
>
|
||||||
<XRActiveModel />
|
<XRActiveModel />
|
||||||
@@ -569,7 +529,7 @@ const XRSession = () => {
|
|||||||
|
|
||||||
<XRMeasurementOverlay />
|
<XRMeasurementOverlay />
|
||||||
<ControllerFineTuning freeMove={freeMove} />
|
<ControllerFineTuning freeMove={freeMove} />
|
||||||
<XRDebugHud />
|
|
||||||
|
|
||||||
{/* In-world HUD — visible inside passthrough where DOM overlays cannot reach */}
|
{/* In-world HUD — visible inside passthrough where DOM overlays cannot reach */}
|
||||||
<XRHudInWorld
|
<XRHudInWorld
|
||||||
|
|||||||
Reference in New Issue
Block a user