docs: audit app architecture and refresh feature documentation
🔍 Lint / 🪄 Check lint (push) Has been cancelled
🔍 Lint / 🎨 Check format (push) Has been cancelled
🔍 Lint / 🔎 Typecheck (push) Has been cancelled
🔍 Lint / 🏗 Build (push) Has been cancelled
📊 Quality / 🔒 Security Audit (push) Has been cancelled
📊 Quality / 📋 Dependency Freshness (push) Has been cancelled
📊 Quality / 📦 Bundle Size (push) Has been cancelled
🔍 Lint / 🪄 Check lint (push) Has been cancelled
🔍 Lint / 🎨 Check format (push) Has been cancelled
🔍 Lint / 🔎 Typecheck (push) Has been cancelled
🔍 Lint / 🏗 Build (push) Has been cancelled
📊 Quality / 🔒 Security Audit (push) Has been cancelled
📊 Quality / 📋 Dependency Freshness (push) Has been cancelled
📊 Quality / 📦 Bundle Size (push) Has been cancelled
This commit is contained in:
@@ -0,0 +1,240 @@
|
||||
# Interaction System Technical Notes
|
||||
|
||||
This document explains the shared trigger, grab, focus, and hand-grab system.
|
||||
|
||||
## Purpose
|
||||
|
||||
The app has several ways for the player to affect the 3D scene:
|
||||
|
||||
- press `E` on focused trigger objects
|
||||
- hold the primary mouse button on grabbable objects
|
||||
- close a tracked hand into a fist to grab hand-controlled objects
|
||||
- release objects and optionally snap them into target positions
|
||||
|
||||
The implementation keeps those rules in a reusable interaction layer so gameplay features such as the repair game do not each create their own input system.
|
||||
|
||||
## Main Files
|
||||
|
||||
| File | Responsibility |
|
||||
| --------------------------------------------------------- | ----------------------------------------------- |
|
||||
| `src/managers/InteractionManager.ts` | Shared interaction state and imperative actions |
|
||||
| `src/hooks/interaction/useInteraction.ts` | React subscription to the manager |
|
||||
| `src/components/three/interaction/InteractableObject.tsx` | Distance/raycast focus detection |
|
||||
| `src/components/three/interaction/TriggerObject.tsx` | Press-to-trigger wrapper |
|
||||
| `src/components/three/interaction/GrabbableObject.tsx` | Physics-backed grab and hand grab wrapper |
|
||||
| `src/components/ui/InteractPrompt.tsx` | HTML prompt for focused trigger interactions |
|
||||
| `src/world/player/PlayerController.tsx` | Keyboard/mouse input bridge |
|
||||
|
||||
## Architecture
|
||||
|
||||
The interaction system has three layers:
|
||||
|
||||
1. R3F objects detect focus and register handles.
|
||||
2. `InteractionManager` stores the current interaction snapshot.
|
||||
3. UI and player input read the snapshot and trigger the selected action.
|
||||
|
||||
This is intentionally not Zustand. Interaction focus and holding state are short-lived, frame-adjacent runtime state. A small singleton plus `useSyncExternalStore` is a better fit than putting high-frequency interaction details into the durable game progression store.
|
||||
|
||||
## Interaction Snapshot
|
||||
|
||||
The snapshot type lives in:
|
||||
|
||||
```txt
|
||||
src/types/interaction/interaction.ts
|
||||
```
|
||||
|
||||
```ts
|
||||
interface InteractionSnapshot {
|
||||
focused: InteractableHandle | null;
|
||||
nearby: boolean;
|
||||
holding: boolean;
|
||||
handHolding: boolean;
|
||||
}
|
||||
```
|
||||
|
||||
Meaning:
|
||||
|
||||
- `focused`: the interactable currently aimed at by the camera ray
|
||||
- `nearby`: at least one interactable is within interaction radius
|
||||
- `holding`: mouse/player-controller grab is active
|
||||
- `handHolding`: hand-tracking grab is active
|
||||
|
||||
`nearby`, `holding`, and `handHolding` are also used by the hand-tracking provider to decide when webcam tracking should stay active in the debug physics scene.
|
||||
|
||||
## Focus Detection
|
||||
|
||||
Focus detection lives in:
|
||||
|
||||
```txt
|
||||
src/components/three/interaction/InteractableObject.tsx
|
||||
```
|
||||
|
||||
Each frame, it:
|
||||
|
||||
1. finds the interactable world position from its Rapier body or group transform
|
||||
2. checks distance from the camera
|
||||
3. marks the handle as nearby if it is inside radius
|
||||
4. raycasts from the camera forward direction
|
||||
5. sets the focused handle when the ray hits the object
|
||||
6. clears focus if the object is no longer nearby or no longer aimed at
|
||||
|
||||
This gives a simple first-person interaction model: the player must be close enough and looking at the object.
|
||||
|
||||
## Trigger Objects
|
||||
|
||||
Trigger implementation:
|
||||
|
||||
```txt
|
||||
src/components/three/interaction/TriggerObject.tsx
|
||||
```
|
||||
|
||||
`TriggerObject` wraps children in a fixed Rapier body and exposes a trigger handle.
|
||||
|
||||
When triggered, it can:
|
||||
|
||||
- play an optional SFX through `AudioManager`
|
||||
- call `onTrigger`
|
||||
- spawn an optional model at an offset
|
||||
|
||||
Typical users:
|
||||
|
||||
- repair-object inspection
|
||||
- repair-case open/fragment interaction
|
||||
- install target
|
||||
- completion target
|
||||
- debug scene trigger sphere
|
||||
|
||||
## Grabbable Objects
|
||||
|
||||
Grab implementation:
|
||||
|
||||
```txt
|
||||
src/components/three/interaction/GrabbableObject.tsx
|
||||
```
|
||||
|
||||
`GrabbableObject` wraps children in a dynamic Rapier body and exposes a grab handle.
|
||||
|
||||
Mouse/controller grab flow:
|
||||
|
||||
1. Player focuses the object.
|
||||
2. Mouse down calls `InteractionManager.pressInteract()`.
|
||||
3. The object enters holding mode.
|
||||
4. Each frame, velocity is pushed toward a hold target in front of the camera.
|
||||
5. Mouse up calls `releaseInteract()`.
|
||||
6. The object can snap to the nearest configured target.
|
||||
|
||||
Important tuning values live in:
|
||||
|
||||
```txt
|
||||
src/data/interaction/grabConfig.ts
|
||||
```
|
||||
|
||||
The debug GUI exposes hold stiffness, throw boost, and hold distance.
|
||||
|
||||
## Snap-To-Target
|
||||
|
||||
`GrabbableObject` supports:
|
||||
|
||||
- `snapTargets`
|
||||
- `snapRadius`
|
||||
- `snapDuration`
|
||||
- `onSnap`
|
||||
|
||||
On release, the object finds the nearest target inside `snapRadius`. If a target is found, GSAP animates the Rapier body translation to that target and calls `onSnap`.
|
||||
|
||||
The repair game uses this to place replacement parts and broken parts into case placeholders.
|
||||
|
||||
## Hand-Controlled Grab
|
||||
|
||||
If `handControlled` is true, `GrabbableObject` also reads:
|
||||
|
||||
```txt
|
||||
useHandTrackingSnapshot()
|
||||
```
|
||||
|
||||
Hand grab flow:
|
||||
|
||||
1. Find a detected hand where `hand.isFist` is true.
|
||||
2. Compute the visual center of the hand from landmark bounds.
|
||||
3. Convert that screen-space point to a camera ray.
|
||||
4. Raycast against the object.
|
||||
5. Use a small set of offset rays around the center to make hit detection more forgiving.
|
||||
6. If the object is in range and hit, enter `handHolding`.
|
||||
7. Move the object toward a hold target in front of the camera while the fist remains closed.
|
||||
8. When the fist opens or disappears, release and snap if possible.
|
||||
|
||||
This is an approximation, not a full 3D hand collider. It is a practical prototype compromise because MediaPipe gives normalized camera-space landmarks and relative depth, not stable world-space hand meshes.
|
||||
|
||||
## Player Input Bridge
|
||||
|
||||
The player controller owns raw keyboard and mouse input:
|
||||
|
||||
```txt
|
||||
src/world/player/PlayerController.tsx
|
||||
```
|
||||
|
||||
It calls:
|
||||
|
||||
- `interaction.pressInteract()` when `E` is pressed and the focused handle is a trigger
|
||||
- `interaction.pressInteract()` on mouse down when the focused handle is a grab
|
||||
- `interaction.releaseInteract()` on mouse up when a grab is active
|
||||
|
||||
Input is ignored while:
|
||||
|
||||
- the settings menu is open
|
||||
- a cinematic is playing
|
||||
|
||||
Movement lock is read separately from `useRepairMovementLocked`, but that hook currently returns `false` on this branch.
|
||||
|
||||
## UI Prompt
|
||||
|
||||
The prompt lives in:
|
||||
|
||||
```txt
|
||||
src/components/ui/InteractPrompt.tsx
|
||||
```
|
||||
|
||||
It appears only when:
|
||||
|
||||
- camera mode is `player`
|
||||
- a focused interaction exists
|
||||
- the player is not holding an object
|
||||
- the focused interaction is a trigger
|
||||
|
||||
The prompt does not appear for grab objects, because grabs are mouse/hand actions rather than `E` trigger actions.
|
||||
|
||||
## Debug Controls
|
||||
|
||||
Interaction debugging is split between:
|
||||
|
||||
- lil-gui `Interaction` folder for showing interaction spheres
|
||||
- lil-gui `GrabbableObject` folder for grab tuning
|
||||
- debug physics scene for live trigger/grab testing
|
||||
- hand-tracking debug panel for hand grab state
|
||||
|
||||
Use:
|
||||
|
||||
```txt
|
||||
http://localhost:5173/?debug
|
||||
```
|
||||
|
||||
Then switch the scene mode to `Physics` from lil-gui.
|
||||
|
||||
## Why This Architecture Works
|
||||
|
||||
The interaction layer separates concerns:
|
||||
|
||||
- R3F objects know their distance/raycast hit state.
|
||||
- The player controller owns input events.
|
||||
- UI only subscribes to a snapshot.
|
||||
- Gameplay objects receive semantic callbacks like `onTrigger`, `onSnap`, or `onPositionChange`.
|
||||
|
||||
This keeps the repair game focused on gameplay rules instead of low-level input plumbing.
|
||||
|
||||
## Known Limitations
|
||||
|
||||
- Only one focused handle is stored at a time.
|
||||
- The focus rule is camera ray based, so side-facing interactions can feel strict without larger meshes or radii.
|
||||
- Hand grab uses screen-space raycasts, not physical hand colliders.
|
||||
- The manager is singleton-based, so tests must call `destroy()` or isolate state when needed.
|
||||
- `nearby` is boolean, not a list exposed to UI, so the current UI cannot rank multiple nearby objects.
|
||||
Reference in New Issue
Block a user