9.6 KiB
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/splatsevery 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 athttps://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:
- 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/splatspayload 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. - Auto mode (
?backend=auto) — fetch from/api/splatson the same origin. This is the local-development case (ruview-pointcloud serveserves 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. - Remote mode (
?backend=<url>) — fetch from<url>/api/splats. The user supplies a CORS-permitting host running their ownruview-pointcloud serve(e.g. a Tailscale-exposed home node). Badge reads● REMOTE <url>. Same silent demo fallback on failure. - 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 servehost 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 → drawSkeletonpipeline 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: trueplus thepointcloud/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 theirruview-pointcloud serveconfig. 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>.jsonlmode that replays captured frames at native rate; that is out of scope here.
Neutral
- The local-dev experience is unchanged.
ruview-pointcloud servestill servesviewer.htmlfrom the bundled asset and the viewer still hits/api/splatsbecause?backenddefaults toauto. 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:
- Pushing to
mainwith a viewer change triggerspointcloud-pages.yml, which deploys togh-pages/pointcloud/in under 60 seconds. 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.- Existing demos at
https://ruvnet.github.io/RuView/and…/pose-fusion.htmland…/nvsim/are still reachable after the first deploy (smoke-tested manually). https://ruvnet.github.io/RuView/pointcloud/?live=1shows the● OFFLINEpanel (because no same-origin backend exists on Pages).https://ruvnet.github.io/RuView/pointcloud/?backend=https://example.invalidfalls back to demo within one poll interval (~500 ms) without throwing in the console.- Running
./target/release/ruview-pointcloud servelocally and openinghttp://127.0.0.1:9880/(which serves the same HTML) still shows live-mode rendering with the● LIVE Local Backendbadge.
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.