204 lines
11 KiB
Markdown
204 lines
11 KiB
Markdown
# ADR-094: Live 3D Point Cloud Viewer — GitHub Pages Deployment with Optional Real-Data Stream
|
|
|
|
| Field | Value |
|
|
|---|---|
|
|
| **Status** | Proposed (2026-04-29) |
|
|
| **Date** | 2026-04-29 |
|
|
| **Authors** | ruv |
|
|
| **Related** | ADR-092 (nvsim dashboard Pages deployment), ADR-059 (live ESP32 CSI pipeline), ADR-079 (camera ground-truth training) |
|
|
| **Branch** | `feat/pointcloud-pages-demo` |
|
|
|
|
---
|
|
|
|
## 1. Context
|
|
|
|
The `wifi-densepose-pointcloud` crate ships a Three.js-based viewer
|
|
(`v2/crates/wifi-densepose-pointcloud/src/viewer.html`) that renders the
|
|
fused camera-depth + WiFi CSI + mmWave point cloud produced by the
|
|
`ruview-pointcloud serve` binary. Today the viewer is local-only:
|
|
|
|
- It is served by the Axum binary on `127.0.0.1:9880`.
|
|
- It polls `/api/splats` every 500 ms expecting a backend on the same
|
|
origin.
|
|
- There is no GitHub Pages deployment, so the README's
|
|
"▶ Live 3D Point Cloud" link points at the moved-content section in
|
|
`docs/readme-details.md`, not at a hosted demo. The two sibling demos
|
|
(Live Observatory, Dual-Modal Pose Fusion) are already hosted at
|
|
`https://ruvnet.github.io/RuView/` and `…/pose-fusion.html`.
|
|
|
|
This is an asymmetry: a first-time visitor can preview the WiFi pose
|
|
demo and the Observatory in one click, but cannot preview the point
|
|
cloud without cloning the repo, building Rust, plugging in an ESP32,
|
|
and pointing a webcam at themselves. That gap suppresses the most
|
|
visually compelling demonstration of the v0.7+ sensor-fusion work.
|
|
|
|
A naive fix — drop the static HTML at `gh-pages/pointcloud/` — does
|
|
not work because the viewer's `fetch("/api/splats")` will 404 on Pages
|
|
and the canvas will hang at "Loading…". A second naive fix — bake in a
|
|
fixed sample dataset — solves the loading state but loses the live-data
|
|
story entirely, and forks the viewer into a "demo build" and a "real
|
|
build" that drift apart.
|
|
|
|
## 2. Decision
|
|
|
|
Ship **one** viewer that auto-selects its transport from URL parameters,
|
|
and publish it to `gh-pages/pointcloud/` alongside the other demos:
|
|
|
|
1. **Default mode** — when the viewer is opened with no query parameters
|
|
on `https://ruvnet.github.io/RuView/pointcloud/`, present a "▶ Enable
|
|
camera" CTA. On click the viewer requests webcam access, runs
|
|
**MediaPipe Face Mesh** in-browser (~30 fps, 478 refined landmarks),
|
|
and renders the visitor's own face as a point cloud — the closest
|
|
browser equivalent of the local pipeline's depth-backprojected face
|
|
geometry that motivated this ADR (`I could see the outline of my face
|
|
in points`). The viewer mirrors x to match selfie convention and
|
|
maps Face Mesh's relative-z to the same world-coordinate range the
|
|
live `/api/splats` payload uses, so a single render path drives both.
|
|
Badge reads `● DEMO Your Face (MediaPipe)`. If the user denies
|
|
camera permission, dismisses the prompt, or visits on a device
|
|
without a webcam, the viewer falls back automatically to a
|
|
procedural scaffold (floor grid, walls, breathing figure, 17-keypoint
|
|
skeleton). All processing is client-side; no frames leave the
|
|
browser. ~480-500 splats from the face plus ~110 floor/wall context
|
|
splats.
|
|
2. **Auto mode** (`?backend=auto`) — fetch from `/api/splats` on the same
|
|
origin. This is the local-development case (`ruview-pointcloud serve`
|
|
serves the viewer and the API together). On any failure (404, network
|
|
error, CORS), fall back silently to synthetic-demo rendering so the
|
|
tab never dies.
|
|
3. **Remote mode** (`?backend=<url>`) — fetch from `<url>/api/splats`.
|
|
This is the **integrated-ESP32** path: the user runs
|
|
`ruview-pointcloud serve --bind 127.0.0.1:9880` locally with an
|
|
ESP32-S3 streaming CSI to UDP port 3333, then opens
|
|
`https://ruvnet.github.io/RuView/pointcloud/?backend=http://127.0.0.1:9880`.
|
|
The hosted Pages viewer becomes a thin client for the local Rust
|
|
fusion pipeline (camera depth + WiFi CSI + mmWave) without a clone
|
|
or rebuild. The viewer also exposes a "📡 Connect ESP32" button that
|
|
prompts for the URL, persists it in `localStorage`, and reloads
|
|
with the query param.
|
|
|
|
For this to work the local server must answer the browser's CORS
|
|
preflight. `stream.rs` therefore installs a `tower_http` `CorsLayer`
|
|
that allows three origin classes:
|
|
|
|
- `https://ruvnet.github.io` — the published Pages demo.
|
|
- `http://localhost:*` and `http://127.0.0.1:*` — developer running
|
|
the bundled `viewer.html` directly.
|
|
- `null` — `file://` origins.
|
|
|
|
Mixed-content (HTTPS Pages → HTTP loopback) is permitted because
|
|
modern browsers (Chrome 94+, Firefox 116+, Safari 16.4+) classify
|
|
`127.0.0.1` and `localhost` as "potentially trustworthy" origins.
|
|
Any other origin (a public hostname, etc.) is denied — this is not
|
|
a wildcard CORS posture. Badge reads `● REMOTE <url>`. Same silent
|
|
demo fallback on failure.
|
|
4. **Strict-live mode** (`?live=1`) — disable the demo fallback. If the
|
|
chosen transport fails, replace the info panel with an explicit offline
|
|
message (`● OFFLINE — Live backend required but unreachable`). Useful
|
|
for embedding the viewer in a status page or kiosk.
|
|
|
|
The synthetic frame returned by the in-browser generator matches the
|
|
JSON shape of the live `/api/splats` payload exactly (`splats`, `count`,
|
|
`frame`, `live`, `pipeline.{skeleton,vitals,…}`), so a single render path
|
|
drives both modes. There is no demo build vs real build — only one HTML
|
|
file, one render path, and one set of bugs.
|
|
|
|
A new GitHub Actions workflow (`.github/workflows/pointcloud-pages.yml`)
|
|
copies the viewer to `gh-pages/pointcloud/index.html` on every push to
|
|
`main` that touches the viewer, using `peaceiris/actions-gh-pages@v4`
|
|
with `keep_files: true` to preserve the existing observatory, pose-fusion,
|
|
and nvsim deployments.
|
|
|
|
## 3. Consequences
|
|
|
|
### Positive
|
|
|
|
- **First-click demo.** Visitors clicking the README's
|
|
"▶ Live 3D Point Cloud" link land on a working Three.js scene in <1 s,
|
|
no toolchain required. Matches the parity of the other two demos.
|
|
- **Real-data on demand.** Users with their own `ruview-pointcloud serve`
|
|
host can use the same hosted viewer URL with
|
|
`?backend=https://their-host.example.com` — no clone, no rebuild. The
|
|
hosted demo doubles as a thin client for self-hosted backends.
|
|
- **Single render path.** Synthetic frames flow through the same
|
|
`handleData → updateSplats → drawSkeleton` pipeline as live frames, so
|
|
visual regressions surface in the demo and the live build at the same
|
|
time. This is the same dual-transport pattern ADR-092 chose for nvsim.
|
|
- **No backend deploy required.** Pages serves static HTML; the demo
|
|
works without standing up an Axum host on the public internet, and
|
|
there is no per-visitor CSI/camera plumbing to provision.
|
|
- **Preserves existing deployments.** `keep_files: true` plus the
|
|
`pointcloud/` destination means observatory/, pose-fusion/, nvsim/,
|
|
and the root index.html on gh-pages are untouched.
|
|
|
|
### Negative / tradeoffs
|
|
|
|
- **Face mesh ≠ CSI.** Browser webcam + MediaPipe gives real face
|
|
geometry but does not produce CSI-derived pose. Visitors who want to
|
|
see the *WiFi-driven* path still need `?backend=<their-host>`. The
|
|
procedural fallback is not WiFi-driven either; it is purely visual
|
|
scaffolding. We accept this — the goal of the hosted demo is to
|
|
convey the *shape* of what the local pipeline produces (a point
|
|
cloud of the user) rather than reproduce the WiFi physics in the
|
|
browser. The latter is a future ADR (WASM port of the fusion crate).
|
|
- **CORS burden on remote mode.** Users who want to share their backend
|
|
must add `Access-Control-Allow-Origin: https://ruvnet.github.io` (or
|
|
`*`) to their `ruview-pointcloud serve` config. We document this in the
|
|
workflow's generated README; we do **not** add a public proxy.
|
|
- **Synthetic generator lives in the viewer.** ~80 LOC of procedural JS
|
|
is now part of `viewer.html`. Acceptable: the file is already the
|
|
client-side render bundle, and the generator is bounded and inert
|
|
(deterministic, no I/O, no eval).
|
|
- **No replay-from-recording in this ADR.** A future ADR may add a
|
|
`?recording=<url>.jsonl` mode that replays captured frames at native
|
|
rate; that is out of scope here.
|
|
|
|
### Neutral
|
|
|
|
- The local-dev experience is unchanged. `ruview-pointcloud serve` still
|
|
serves `viewer.html` from the bundled asset and the viewer still hits
|
|
`/api/splats` because `?backend` defaults to `auto`. Nothing in the
|
|
Rust crate changes — this is HTML + workflow only.
|
|
|
|
## 4. Implementation
|
|
|
|
| File | Change |
|
|
|---|---|
|
|
| `v2/crates/wifi-densepose-pointcloud/src/viewer.html` | Add URL-param transport selector (`backend`, `live`), synthetic frame generator, demo-fallback path, transport-aware mode badge. ~120 LOC added, no removed behavior. |
|
|
| `.github/workflows/pointcloud-pages.yml` | New workflow: stage viewer to `_site/pointcloud/index.html`, deploy to `gh-pages/pointcloud/` with `keep_files: true`. Triggers on viewer changes and on manual dispatch. |
|
|
| `README.md` | Already updated — `▶ Live 3D Point Cloud` link will be retargeted to `https://ruvnet.github.io/RuView/pointcloud/` once the first deploy succeeds. (Tracked separately, not blocking this ADR.) |
|
|
| `docs/adr/README.md` | ADR index — add ADR-094 row. |
|
|
|
|
## 5. Acceptance Gates
|
|
|
|
This ADR is **Implemented** when all of the following hold:
|
|
|
|
1. Pushing to `main` with a viewer change triggers
|
|
`pointcloud-pages.yml`, which deploys to `gh-pages/pointcloud/` in
|
|
under 60 seconds.
|
|
2. `https://ruvnet.github.io/RuView/pointcloud/` loads, shows the
|
|
"Enable camera" CTA, and on accept renders the visitor's face as a
|
|
point cloud with badge `● DEMO Your Face (MediaPipe)` and non-zero
|
|
splat + frame counts. On camera denial, falls back to the
|
|
procedural scene with badge `● DEMO Synthetic`.
|
|
3. Existing demos at `https://ruvnet.github.io/RuView/` and
|
|
`…/pose-fusion.html` and `…/nvsim/` are still reachable after the
|
|
first deploy (smoke-tested manually).
|
|
4. `https://ruvnet.github.io/RuView/pointcloud/?live=1` shows the
|
|
`● OFFLINE` panel (because no same-origin backend exists on Pages).
|
|
5. `https://ruvnet.github.io/RuView/pointcloud/?backend=https://example.invalid`
|
|
falls back to demo within one poll interval (~500 ms) without
|
|
throwing in the console.
|
|
6. Running `./target/release/ruview-pointcloud serve` locally and
|
|
opening `http://127.0.0.1:9880/` (which serves the same HTML) still
|
|
shows live-mode rendering with the `● LIVE Local Backend` badge.
|
|
|
|
## 6. Out of Scope
|
|
|
|
- Replaying recorded JSONL frames in the browser (future ADR).
|
|
- WASM-side execution of the fusion pipeline in the browser (would
|
|
require porting the camera + mmWave path; deferred).
|
|
- Authentication / signed splats payloads — backend-side concern,
|
|
unaffected by this client-side change.
|
|
- Hosting a public CORS proxy for users without their own backend.
|