# Holostrata Academy Portal — Architecture

WebXR mixed-reality LMS + scenario runtime for the Joint Aviation Academy (UAE). Static web app — no Unity, no build step.
Runs in the Meta Quest 3 browser (passthrough MR, hand + controller input) and in any desktop browser (3D fallback + dashboards).

```
Academy Portal.dc.html     LMS portal (login, dashboards, library, sessions, lobby, results, trainees, certificates, settings)
xr/session.html            XR session runtime page (desktop 3D · immersive-ar passthrough · immersive-vr)
xr/runtime.js              three.js scene, WebXR session, hands/controllers, 3D UI panels, avatars, colocation
xr/interaction.js          Interaction layer (Meta Interaction SDK vocabulary): Ray / Poke / Grab interactors, Interactables, SnapZones, UIPanel
xr/scenario.js             Scenario schema v1, validator, ScenarioEngine (goal evaluation + event log), runtime-check module
xr/scoring.js              Assessment: event log → score breakdown, recommendations, learning curve, certificate eligibility
xr/net.js                  Multiplayer transport: BroadcastChannel (same device) · PeerJS WebRTC (headsets on LAN); host-authoritative protocol
xr/store.js                Data layer: IndexedDB driver (default) · REST driver (on-prem) · demo dataset
xr/i18n.js                 Bilingual strings (EN/AR), formatting
xr/marker.png              Printable 20 cm room-origin marker (colocation)
docs/                      This folder
```

## Runtime modes
| Mode | How | Input |
|---|---|---|
| Desktop | default when WebXR is absent | mouse ray: left-click / drag = interact, right-drag = orbit, wheel = zoom |
| MR passthrough | `immersive-ar`, `local-floor`, optional `hand-tracking`, `hit-test`, `anchors`, `image-tracking` | hands (pinch = select/grab, index poke), Touch controllers (trigger/grip) |
| VR | `immersive-vr` fallback (bench + floor grid drawn) | same as MR |

## Interaction layer (Meta ISDK parity)
* **RayInteractor** — controller / hand aim pose (`targetRaySpace`) or mouse. Hover, select, distance-grab, panel press. Head-gaze unit adds hover/inspect dwell in XR.
* **PokeInteractor** — index fingertip sphere; presses UIPanel items when the tip crosses the panel surface (front → back), pokes `pokeable` objects.
* **GrabInteractor** — pinch point (thumb-tip/index-tip midpoint) or controller grip. Near-grab takes precedence over ray (ISDK "near-field first").
* **Interactable** flags: `grabbable`, `pokeable`, `inspectable`, `tool`; `locked` (network ownership) and `highlight` (step cueing).
* **SnapZone** (`kind: snap`) snaps a released object and emits `place`; `kind: safety` emits `safety` when a hand or held object enters.
* **UIPanel** — canvas-textured world-locked panel; items registered during draw become hit targets for ray + poke. Bilingual canvas text (RTL aware).
* Pointer event stream: `hover / unhover / select / grab / move / release / place / poke / toggle / inspect / safety`.

## Scenario engine
`ScenarioEngine(scenario).handle(event)` evaluates the current step's goal (`poke | grab | place | toggle | inspect | quiz`), records
`stepStart / stepComplete / sequenceError / toolError / safetyViolation / hintUsed / quizAnswer / instructorMark / instructorSkip / stepObserved / sessionEnd`
with timestamps. `onComplete` effects drive animations (`animate` → `animState`). The event log is the single source for scoring.

## Session model
* **Self-paced** — one engine, auto-advance.
* **Live** — instructor device is the host. Shared world (object poses, panel states, animations) and a shared step; each participant's engine records its own events. Host advances when the *active trainee* (or anyone, when `active = all`) reports `stepDone`; quiz steps advance when every trainee has answered or on manual Next. Trainees who did not perform a step get `stepObserved` (neutral in scoring).
* Instructor console (3D panel in headset, sidebar on desktop): active trainee, ± marks with note, lock objects, prev/next, end session.

## Network protocol (v1)
Envelope `{ v:1, type, from, role, ts, payload, to? }`. Trainees send to host; host applies and relays.

| type | direction | payload |
|---|---|---|
| hello / bye / heartbeat | any | `{user}` |
| welcome | host → joiner | `{snapshot:{stepIndex,animState}, objects:{id:{p,q}}, panels:{panelId:{btn:state}}, locked, active, startedAt}` |
| step / anim | host → all | `{index}` / `{animState}` |
| stepDone / progress | trainee → host | `{index}` / `{step, errors, done, score}` |
| grab / release / obj | owner → all | `{id}` / `{id,p,q,zone}` / `{id,p,q}` (20 Hz while held) |
| panel | any → all | `{panel,id,state}` |
| lock / active / mark / end | host → all | `{locked}` / `{userId}` / `{userId,delta,note}` / `{}` |
| pose | any → all (12 Hz) | `{head:[x,y,z,qx,qy,qz,qw], hands:[{h,p,pinch}], inXR}` — **root-space** coordinates |

All spatial data is exchanged in **scenario-root space** (the marker frame), so colocated headsets with different tracking origins agree.

## Colocation
1. Print `xr/marker.png` at 20 × 20 cm, fix it on the bench, arrow toward the back of the bench.
2. **Automatic** where the browser exposes WebXR image tracking: the root snaps to the tracked image (`+z out of print → +y up`, `+y arrow → −z`).
3. **Two-point touch-align** (works on Quest 3 today): press *Set room origin*, touch the marker centre and pinch, touch the arrow tip and pinch. Root = centre, yaw = arrow. Accuracy ≈ 1–2 cm.

## Data & scoring
IndexedDB collections: `users, scenarios, sessions, attempts, events, certificates, settings`. Attempts store the computed `breakdown`; `events` keep the raw log.
Score = Σ dimension% × weight (sequence 25, time 15, hints 10, tools 15, safety 20, quiz 15) × completion factor, ± instructor marks, pass ≥ 70 (per scenario override).
Certificates: Academy — ≥ 1 pass; IXRDL — ≥ 3 distinct scenarios passed with ≥ 2 zero-safety-violation passes (rules in `scoring.eligibility`).
