Demos 04 and 05 work fine locally — operator has assets/X Bot.fbx
present. On the gh-pages deploy the FBX is intentionally absent
(Mixamo license boundary, .gitignored) and the previous onError
handler just logged 'FBX load failed' to the console and left a
stuck '⚠ Load failed — see console' message in the overlay.
Replaces both onError handlers with an in-page card that:
- Explains why the asset is missing (license boundary, not a bug)
- Tells you exactly how to run it locally (Mixamo download path,
where to drop the file, the serve-demo.py command)
- Links to Mixamo + the repo source + back to the gallery
- Lets the ADR-097 helpers scene keep rendering behind it
- Logs at warn (not error) — no more uncaught console.error noise
The success branch is untouched, so local development is identical
to before.
Adds a new GitHub Pages workflow that publishes the ADR-097 three.js
demo gallery alongside the existing observatory/, pose-fusion/,
pointcloud/, and nvsim/ deployments. Uses keep_files: true so the
other deployments are preserved.
What ships:
* `examples/three.js/index.html` — new landing page that lists all 5
demos with screenshots, "standalone" vs "needs FBX" badges, and an
honest note explaining the Mixamo X Bot.fbx license boundary
(demos 04 and 05 need a local download from mixamo.com; demos
01-03 run standalone in any modern browser).
* `.github/workflows/threejs-pages.yml` — staged copy of demos/,
screenshots/, README.md, and the new index.html into
`_site/three.js/`. Drops an `assets/README.txt` placeholder
explaining the FBX-not-shipped policy. Triggered on changes to
examples/three.js/** or the workflow itself.
* README.md — adds the live link to the existing demo row
(`▶ three.js Demos (5)`) plus a one-line callout describing the
gallery and the FBX caveat.
After this PR merges, the workflow runs and publishes:
https://ruvnet.github.io/RuView/three.js/
* feat(examples/three.js): cinematic skinned realtime pose demo + ESP32 CSI bridge
Five-stage example progression exploring three.js helpers (ADR-097 surface) as
a viewer for live RuView sensor data:
1. helpers-demo.html — clean ADR-097 helper reference (GridHelper,
PolarGridHelper, BoxHelper, AxesHelper),
file://-safe, no backend
2. helpers-cinematic.html — same scene + UnrealBloomPass + pseudo-CSI
sonar pings + tomography sweep + procedural
cyber floor + ambient drift particles
3. helpers-skinned.html — replaces sphere skeleton with Mixamo X Bot
via GLTFLoader from threejs.org CDN, plays
bundled animations with additive blending
4. helpers-skinned-fbx.html — same but loads a local Mixamo FBX (needs
serve-demo.py — file:// can't fetch local
siblings). Drop X Bot.fbx alongside.
5. helpers-skinned-realtime.html — webcam → MediaPipe Pose Heavy →
poseWorldLandmarks → direct quaternion
retargeting onto the Mixamo skeleton.
Real ESP32-S3 CSI streamed over WebSocket
from ruvultra (Tailscale, port 8766).
Supporting:
- serve-demo.py threaded HTTP server with no-cache headers
(fixes net::ERR_EMPTY_RESPONSE on the FBX path)
- ruvultra-csi-bridge.py ESP32 RuView firmware tick → WebSocket bridge,
runs as systemd-run unit on ruvultra
Bugs found + fixed along the way (all documented in code comments):
- FBX exports yield TWO parallel Bone trees with identical names; only the
SkinnedMesh.skeleton.bones one drives visible deformation. model.traverse
finds orphans.
- Mixamo FBX nests a zero-length wrapper bone above the real bone, same name.
bone.children[0].getWorldPosition == bone.getWorldPosition → restDir is
(0,0,0) → setFromUnitVectors collapses to identity. Walk past same-named
same-position wrappers when computing tail.
- AnimationMixer.update() with a "stopped" action still mutates bones unless
enabled=false is set.
Retargeting layer in helpers-skinned-realtime.html:
- 12 bones direct quaternion retarget (arms × 2, legs × 2, spine × 3, neck)
- Hips root rotation from shoulder/hip line basis (torso twist + lean)
- Neck aims at ear-midpoint (kp 7+8), not nose (kp 0), to remove the
forward bias of the protruding-nose anchor
- One Euro Filter per landmark per axis (Casiez 2012) — adaptive low-pass
- Visibility-weighted per-bone slerp gain — occluded limbs relax to rest
- URL toggles: ?mirror= ?yflip= ?zflip= ?cnn=0/1/2 ?csi=ws://...
Live CSI integration:
- Bridge parses adaptive_ctrl tick lines (motion/presence/rssi/yield)
- Browser fans single ESP32 reading across 4 UI nodes with phase-shifted
wobble (0.88–1.00 × sin(t·0.55 + offsetᵢ))
- EMA α=0.06 (~3 sec time constant), HUD update throttled 3 Hz
Co-Authored-By: claude-flow <ruv@ruv.net>
* refactor(examples/three.js): organize into demos/screenshots/server/assets + add README
Flatten the 13-file flat layout into purposeful subfolders so the demo
collection has a clean top-level entry point (README.md) and the file roles
are obvious from a directory listing.
Layout:
demos/ 01..05 — numbered for the progression (helpers → cinematic →
skinned → skinned-fbx → skinned-realtime)
screenshots/ one PNG per demo, matching the demo's filename prefix
server/ serve-demo.py + ruvultra-csi-bridge.py
assets/ X Bot.fbx (gitignored, used by demos 04 and 05)
Touched files (beyond the renames):
- 04-skinned-fbx.html, 05-skinned-realtime.html: MODEL_URL now resolves
'../assets/X%20Bot.fbx' instead of './X%20Bot.fbx'
- server/serve-demo.py: chdir() walks 3 levels up to repo root (was 2), and
the URL banner now lists all 5 demos
- .gitignore: comment refresh — points at assets/ and screenshots/
- 05-skinned-realtime.html also picks up in-flight fps-tune work from this
branch (Holistic script, SMOOTH_K URL param, slerp gain scaling) since
those edits and the rename hit the same file
Verified end-to-end:
- python examples/three.js/server/serve-demo.py
- all 5 demos return 200, X Bot.fbx returns 200 from new asset/ path
- demos 04 + 05 render the X Bot mesh; 0 JS errors via browser eval
- screenshots reproduced match the originals
Co-Authored-By: claude-flow <ruv@ruv.net>