feat(pointcloud): GitHub Pages demo with optional live backend (ADR-094) (#495)
Publishes the live 3D point cloud viewer to gh-pages/pointcloud/ so it
can be linked from the README alongside the Observatory and Dual-Modal
Pose Fusion demos. The viewer auto-selects its transport from URL
parameters:
- default / ?backend=auto — try /api/splats, fall back to synthetic demo
- ?backend=demo — synthetic in-browser only, no network
- ?backend=<url> — fetch from a CORS-permitting host running
ruview-pointcloud serve
- ?live=1 — strict mode, show offline panel instead of demo fallback
The synthetic frame matches the live API JSON shape (splats, count,
frame, live, pipeline.{skeleton,vitals}) so a single render path drives
both modes. New workflow uses keep_files: true to preserve the existing
observatory/, pose-fusion/, and nvsim/ deployments on gh-pages.
See docs/adr/ADR-094-pointcloud-github-pages-deployment.md for the full
decision record and 6 acceptance gates.
This commit is contained in:
parent
e11d569a39
commit
21b2b3352f
|
|
@ -0,0 +1,67 @@
|
||||||
|
name: Point Cloud Viewer → GitHub Pages
|
||||||
|
|
||||||
|
# Publishes the live 3D point cloud viewer to gh-pages/pointcloud/.
|
||||||
|
# The viewer defaults to a synthetic in-browser demo; users can append
|
||||||
|
# ?backend=<url> or ?backend=auto to point it at a real ruview-pointcloud
|
||||||
|
# server (CORS-permitting host required). See ADR-094.
|
||||||
|
#
|
||||||
|
# Uses keep_files: true to preserve the existing observatory/, pose-fusion/,
|
||||||
|
# nvsim/, and root index.html demos already on gh-pages.
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches: [main]
|
||||||
|
paths:
|
||||||
|
- 'v2/crates/wifi-densepose-pointcloud/src/viewer.html'
|
||||||
|
- '.github/workflows/pointcloud-pages.yml'
|
||||||
|
workflow_dispatch:
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
contents: write
|
||||||
|
|
||||||
|
concurrency:
|
||||||
|
group: pointcloud-pages
|
||||||
|
cancel-in-progress: true
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
build-and-deploy:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Checkout main
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Stage viewer for Pages
|
||||||
|
run: |
|
||||||
|
mkdir -p _site/pointcloud
|
||||||
|
cp v2/crates/wifi-densepose-pointcloud/src/viewer.html _site/pointcloud/index.html
|
||||||
|
# Drop a tiny README so direct browsers of the directory get context.
|
||||||
|
cat > _site/pointcloud/README.md <<'EOF'
|
||||||
|
# RuView — Live 3D Point Cloud Viewer
|
||||||
|
|
||||||
|
Hosted at: https://ruvnet.github.io/RuView/pointcloud/
|
||||||
|
|
||||||
|
## Modes
|
||||||
|
|
||||||
|
- Default — synthetic in-browser demo (no backend, no network calls).
|
||||||
|
- `?backend=auto` — fetch from `/api/splats` on the same origin
|
||||||
|
(only works when the viewer is served by `ruview-pointcloud serve`).
|
||||||
|
- `?backend=<url>` — fetch from `<url>/api/splats` on a CORS-permitting
|
||||||
|
host (e.g. `?backend=https://my-ruview.example.com`).
|
||||||
|
- `?live=1` — require a live backend; show an offline message instead
|
||||||
|
of falling back to the synthetic demo.
|
||||||
|
|
||||||
|
See ADR-094 for the deployment design.
|
||||||
|
EOF
|
||||||
|
|
||||||
|
- name: Deploy to gh-pages/pointcloud/
|
||||||
|
uses: peaceiris/actions-gh-pages@v4
|
||||||
|
with:
|
||||||
|
github_token: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
publish_dir: ./_site/pointcloud
|
||||||
|
destination_dir: pointcloud
|
||||||
|
# CRITICAL: preserves observatory/, pose-fusion/, nvsim/, and root
|
||||||
|
# index.html already on gh-pages.
|
||||||
|
keep_files: true
|
||||||
|
commit_message: 'deploy(pointcloud): ${{ github.sha }}'
|
||||||
|
user_name: 'github-actions[bot]'
|
||||||
|
user_email: 'github-actions[bot]@users.noreply.github.com'
|
||||||
|
|
@ -0,0 +1,164 @@
|
||||||
|
# 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/`, render a synthetic
|
||||||
|
in-browser scene (floor grid, walls, breathing/swaying figure, animated
|
||||||
|
17-keypoint skeleton) and label the badge `● DEMO Synthetic`. No
|
||||||
|
network calls are made. Renders forever, deterministic, ~200 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`. The
|
||||||
|
user supplies a CORS-permitting host running their own
|
||||||
|
`ruview-pointcloud serve` (e.g. a Tailscale-exposed home node). 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
|
||||||
|
|
||||||
|
- **Synthetic ≠ real.** The demo figure is procedural, not recorded from
|
||||||
|
hardware, so visitors cannot see *real* CSI-derived poses without
|
||||||
|
supplying `?backend=`. We accept this — the alternatives (pre-recorded
|
||||||
|
JSON, on-page WASM inference) add maintenance cost and diverge the
|
||||||
|
render path.
|
||||||
|
- **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, renders the
|
||||||
|
synthetic scene, displays `● DEMO Synthetic` badge, and shows
|
||||||
|
non-zero splat + frame counts.
|
||||||
|
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.
|
||||||
|
|
@ -104,10 +104,139 @@
|
||||||
scene.add(skeletonGroup);
|
scene.add(skeletonGroup);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ----- Transport configuration -----
|
||||||
|
// ?backend=<url> → fetch splats from <url>/api/splats (CORS-permitting host)
|
||||||
|
// ?backend=auto → try /api/splats, fall back to synthetic demo on failure (default)
|
||||||
|
// ?backend=demo → always render synthetic demo (no network)
|
||||||
|
// ?live=1 → require live; show error instead of demo fallback
|
||||||
|
var urlParams = new URLSearchParams(window.location.search);
|
||||||
|
var backendArg = urlParams.get("backend") || "auto";
|
||||||
|
var requireLive = urlParams.get("live") === "1";
|
||||||
|
var transportMode = "demo"; // resolved at first fetch: "live" | "remote" | "demo"
|
||||||
|
var demoStartMs = Date.now();
|
||||||
|
var demoFrameNum = 0;
|
||||||
|
|
||||||
|
function buildSplatsUrl() {
|
||||||
|
if (backendArg === "demo") return null;
|
||||||
|
if (backendArg === "auto") return "/api/splats";
|
||||||
|
// User-supplied URL — strip trailing slash and append /api/splats.
|
||||||
|
var base = backendArg.replace(/\/+$/, "");
|
||||||
|
return base + "/api/splats";
|
||||||
|
}
|
||||||
|
|
||||||
|
function syntheticFrame() {
|
||||||
|
// Deterministic synthetic point cloud: floor grid, two walls, and
|
||||||
|
// a standing figure that breathes/sways. Resembles the live API
|
||||||
|
// payload so the same render path drives both modes.
|
||||||
|
var t = (Date.now() - demoStartMs) / 1000.0;
|
||||||
|
var sway = Math.sin(t * 0.8) * 0.05;
|
||||||
|
var breath = Math.sin(t * 1.2) * 0.015;
|
||||||
|
var splats = [];
|
||||||
|
|
||||||
|
// Floor — 12x12 grid at y=-1
|
||||||
|
var gx, gz;
|
||||||
|
for (gx = -6; gx <= 6; gx++) {
|
||||||
|
for (gz = 0; gz <= 12; gz++) {
|
||||||
|
splats.push({
|
||||||
|
center: [gx * 0.4, -1.0, gz * 0.4],
|
||||||
|
color: [0.15, 0.18, 0.22],
|
||||||
|
opacity: 1.0,
|
||||||
|
scale: [0.05, 0.05, 0.05]
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Back wall + side walls — sparse vertical strips
|
||||||
|
var wy, wx;
|
||||||
|
for (wy = -1; wy <= 2; wy++) {
|
||||||
|
for (wx = -6; wx <= 6; wx += 2) {
|
||||||
|
splats.push({
|
||||||
|
center: [wx * 0.4, wy * 0.5, 4.8],
|
||||||
|
color: [0.12, 0.20, 0.28],
|
||||||
|
opacity: 1.0,
|
||||||
|
scale: [0.05, 0.05, 0.05]
|
||||||
|
});
|
||||||
|
}
|
||||||
|
splats.push({ center: [-2.4, wy * 0.5, 0.5 + (wy + 1) * 0.8], color: [0.12, 0.20, 0.28], opacity: 1.0, scale: [0.05, 0.05, 0.05] });
|
||||||
|
splats.push({ center: [ 2.4, wy * 0.5, 0.5 + (wy + 1) * 0.8], color: [0.12, 0.20, 0.28], opacity: 1.0, scale: [0.05, 0.05, 0.05] });
|
||||||
|
}
|
||||||
|
// Standing figure — 60 points in a vertical cylinder
|
||||||
|
var i, theta, r, py;
|
||||||
|
for (i = 0; i < 60; i++) {
|
||||||
|
theta = (i / 60) * Math.PI * 2;
|
||||||
|
py = -0.6 + (i / 60) * 1.6;
|
||||||
|
r = 0.18 + breath * (py > 0 ? 1 : 0);
|
||||||
|
splats.push({
|
||||||
|
center: [sway + Math.cos(theta) * r, py, 2.3 + Math.sin(theta) * r],
|
||||||
|
color: [0.91, 0.65, 0.20],
|
||||||
|
opacity: 1.0,
|
||||||
|
scale: [0.04, 0.04, 0.04]
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// 17 COCO keypoints in normalized [0,1] image coords (matches live shape)
|
||||||
|
var headY = 0.18;
|
||||||
|
var keypoints = [
|
||||||
|
[0.50 + sway * 0.05, headY, 0.95], // 0 nose
|
||||||
|
[0.52 + sway * 0.05, headY - 0.01, 0.92], // 1 leftEye
|
||||||
|
[0.48 + sway * 0.05, headY - 0.01, 0.92], // 2 rightEye
|
||||||
|
[0.54 + sway * 0.05, headY, 0.85], // 3 leftEar
|
||||||
|
[0.46 + sway * 0.05, headY, 0.85], // 4 rightEar
|
||||||
|
[0.60 + sway * 0.04, 0.32, 0.93], // 5 leftShoulder
|
||||||
|
[0.40 + sway * 0.04, 0.32, 0.93], // 6 rightShoulder
|
||||||
|
[0.65 + sway * 0.03, 0.46, 0.90], // 7 leftElbow
|
||||||
|
[0.35 + sway * 0.03, 0.46, 0.90], // 8 rightElbow
|
||||||
|
[0.68, 0.60 + Math.sin(t * 1.4) * 0.02, 0.86], // 9 leftWrist
|
||||||
|
[0.32, 0.60 - Math.sin(t * 1.4) * 0.02, 0.86], // 10 rightWrist
|
||||||
|
[0.57, 0.58, 0.94], // 11 leftHip
|
||||||
|
[0.43, 0.58, 0.94], // 12 rightHip
|
||||||
|
[0.58, 0.74, 0.90], // 13 leftKnee
|
||||||
|
[0.42, 0.74, 0.90], // 14 rightKnee
|
||||||
|
[0.59, 0.92, 0.88], // 15 leftAnkle
|
||||||
|
[0.41, 0.92, 0.88] // 16 rightAnkle
|
||||||
|
];
|
||||||
|
|
||||||
|
demoFrameNum += 1;
|
||||||
|
return {
|
||||||
|
splats: splats,
|
||||||
|
count: splats.length,
|
||||||
|
frame: demoFrameNum,
|
||||||
|
live: false,
|
||||||
|
pipeline: {
|
||||||
|
skeleton: { keypoints: keypoints, confidence: 0.86 },
|
||||||
|
vitals: {
|
||||||
|
breathing_rate: 14 + Math.round(Math.sin(t * 0.05) * 2),
|
||||||
|
motion_score: 0.18 + Math.abs(sway) * 2
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
async function fetchCloud() {
|
async function fetchCloud() {
|
||||||
|
// Demo-only mode: never hit the network.
|
||||||
|
if (backendArg === "demo") {
|
||||||
|
transportMode = "demo";
|
||||||
|
handleData(syntheticFrame());
|
||||||
|
return;
|
||||||
|
}
|
||||||
try {
|
try {
|
||||||
var resp = await fetch("/api/splats");
|
var resp = await fetch(buildSplatsUrl(), { cache: "no-store" });
|
||||||
|
if (!resp.ok) throw new Error("HTTP " + resp.status);
|
||||||
var data = await resp.json();
|
var data = await resp.json();
|
||||||
|
transportMode = (backendArg === "auto") ? "live" : "remote";
|
||||||
|
handleData(data);
|
||||||
|
} catch (err) {
|
||||||
|
if (requireLive) {
|
||||||
|
document.getElementById("stats").innerHTML =
|
||||||
|
'<span class="demo">● OFFLINE</span><br>Live backend required (?live=1) but unreachable.<br><span class="label">' + (err && err.message ? err.message : err) + '</span>';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
transportMode = "demo";
|
||||||
|
handleData(syntheticFrame());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleData(data) {
|
||||||
|
try {
|
||||||
if (data.splats && data.frame !== lastFrame) {
|
if (data.splats && data.frame !== lastFrame) {
|
||||||
// Compute CSI frame rate
|
// Compute CSI frame rate
|
||||||
var now = Date.now();
|
var now = Date.now();
|
||||||
|
|
@ -127,11 +256,16 @@
|
||||||
clearSkeleton();
|
clearSkeleton();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Build info panel
|
// Build info panel — badge reflects active transport
|
||||||
var mode = data.live
|
var mode;
|
||||||
? '<span class="live">● LIVE</span>'
|
if (transportMode === "live") {
|
||||||
: '<span class="demo">● DEMO</span>';
|
mode = '<span class="live">● LIVE</span> Local Backend';
|
||||||
var html = mode + " Camera + CSI<br>"
|
} else if (transportMode === "remote") {
|
||||||
|
mode = '<span class="live">● REMOTE</span> ' + backendArg;
|
||||||
|
} else {
|
||||||
|
mode = '<span class="demo">● DEMO</span> Synthetic';
|
||||||
|
}
|
||||||
|
var html = mode + "<br>"
|
||||||
+ "Splats: " + data.count + "<br>"
|
+ "Splats: " + data.count + "<br>"
|
||||||
+ "Frame: " + data.frame;
|
+ "Frame: " + data.frame;
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue