festifun/plan/status/lane-F.md
type-two 175ab0f772 M15: moment FX (lane F) + status file
 toggle: fx.update(tGlobal) (already called each frame by main.js) tracks
the previous tGlobal and fires on marker crossings in play AND scrub;
backward jumps and >2s forward jumps just reset the tracker. pyro/confetti ->
~1s additive particle burst at the event's resolved anchor (mapped by the
backend's label convention, stage-centroid fallback); bass_drop -> point
cloud/splat scale pulse for ~a beat (60/tempo via GET /api/beats, else 0.5s),
exact scale restored. Scene access only via scene3d.addObject/removeObject.
Hot path: 6 preallocated burst slots (160 particles each), zero per-frame
allocation, whole body behind a breaker so a throw can never kill the app
loop (it disables FX instead).

Live-verified: pyro fired at t=10.00 in play, confetti on scrub crossing 17.0,
no retro-fire on backward jump, pulse 1.16x -> exact 1.0 restore, worst-case
frame body 0.101 ms CPU (~330x inside the 30fps budget), clean toggle-off
(plan/status/lane-F.md).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-17 17:54:11 +10:00

5.9 KiB
Raw Blame History

Status — lane-F (social & fun UX: M13 anchor manager + friend tags, M14 photo mode, M15 moment FX)

Round 1 — 2026-07-17 — STATUS: ready_to_merge

Directives acknowledged: Round 4 of plan/DIRECTIVES.md (phase 5b begins; foundation2 merged; worktree rule followed — this lane ran in its own worktree on branch lane/f-social).

Ownership kept: only frontend/src/anchorPanel.js, frontend/src/photoMode.js, frontend/src/fx.js, and this file were touched. No frozen file edited; no CHANGE_REQUESTS entry needed.

Friend-tag design choice (M13): the Tag flow DRIVES the frozen M8 annotator without an event — no workaround needed. Rationale: annotate.js is null-event safe by construction (startAnnotateMode() only needs the registered cells; _submit POSTs event_id: ev ? ev.id : null; every panel-content access is optional-chained), and the backend explicitly supports it (api.py: "Annotations with no event stay independent (each makes its own anchor)"). Flow: prompt for the friend's name → annotator.close() (clears any currentEvent) → annotator.startAnnotateMode() → user drags a bbox over the friend on ANY video → the resolved anchor arrives via emit("anchors-changed", anchor) → anchorPanel PATCHes the pending name + a palette color onto it and stops annotate mode. Esc or Tag again cancels. This reuses the real M8 ray-cast/nearest-point resolution (friends land ON the point cloud), keeps annotate.js untouched, and is one drag for the user.

Acceptance checklist — ALL verified live (worktree servers: backend :8010 with an isolated copy of the synthetic data — the main project DB was never touched — + Vite :5175 with VITE_API_BASE; browser = Claude Code Browser pane, loop driven with the __f4d pump per pitfall #2 where noted):

  • M13 create → rename → recolor → jump → delete roundtrip live: Tag with prompt "Zoe" + real 35×30 px drag on cam0 → anchor id 5 {label:"Zoe", color:"#7dffb0"} created via the frozen annotator; ✎ rename → "Zoe B"; color input change → #ff00aa; label-click jump → scene3d.controls.target == (1.60, 0.00, -6.50) == anchor pos exactly, follow-cam detached; ✕ delete → state.anchors 5→4, row removed.
  • M13 labels visible in 3D + video overlays: screenshot shows the pink "Zoe B" sphere+label centered in the 3D scene after jump AND "Zoe B" x-ray markers on cam0 + cam2 overlays.
  • M13 anchors survive reload: full page reload → anchor id 5 still {label:"Zoe B", color:"#ff00aa"} from GET /api/anchors.
  • M13 capsule mode: with body.capsule, tag/rename/delete/recolor controls hidden (offsetParent === null), panel + list + jump-to remain visible.
  • M14 PNG ≥3840 wide, aspect preserved, no helpers: intercepted the real toBlob → PNG 3840×3093 px (351 KB) from a 704×567 pane (aspect 1.2416 preserved); rendered the captured blob full-screen and screenshotted it: point cloud + anchor SPHERES only (spheres stay by scene3d design — "friend tags belong in photos"), zero grid/axes/frusta/path-lines/label-sprites.
  • M14 restore: pixelRatio 1→1, size 704×567, camera.aspect 1.2416, helpersVisible true all restored; shot MID-PLAY → transport paused for the shot and state.playing === true after (play-state restored). Works in free-roam; follow-cam uses the same render path (aspect saved/restored around the shot).
  • M15 pyro fires on PLAY crossing: seek 9.5 → play → burst spawned exactly at t=10.00, opacity 1→0.01 over its 1.0 s life across 53 pumped frames, then removed from the scene (allRemoved: true).
  • M15 confetti fires on SCRUB crossing: paused discrete seeks 16.4→16.6→16.8→16.95→17.1 → burst fired at the 17.1 step (crossing 17.0). Backward jump to 15.0 fired nothing (tracker reset).
  • M15 bass_drop pulse: play across t=3 → scene3d.points.scale swelled to 1.1599 and restored to exactly 1.0 after ~one beat (0.5 s default; tempo from GET /api/beats when it exists).
  • M15 performance: worst case (all 6 burst slots active + pulse) full frame body (transport.tick + fx.update + scene3d.update) = 0.101 ms/frame CPU over 300 iterations — ~330× inside the 33 ms/30 fps budget. Pool preallocated once; update() allocates nothing; toggle-off removes all objects and restores scale (verified: 0 active, 0 in scene, scale 1).
  • fx.update can never kill the master loop: whole body in try/catch → on error logs once, disables itself, cleans up (breaker _dead verified false throughout).
  • Zero console errors across the entire live session.
  • Regression: uv run pytest backend/tests -p no:warnings127 passed (floor met; backend untouched). cd frontend && npm run build clean (pre-existing >500 kB chunk warning only).

Notes / deferred to coordinator (integration2):

  • M14 splat path: the DropInViewer is a scene child and renders with the same renderer.render(scene, camera) call, so photo mode has no splat branch — but live verification ON A TRAINED SPLAT is deferred (synthetic project has point cloud only).
  • M15 event→anchor mapping: events carry no anchor id in state, so fx maps event→anchor by the backend's labeling convention (resolved anchor label = description or event_type), falling back to the stage centroid (0, 0.5, 0). Both pyro/confetti fired at the centroid in this test (no event-resolved anchors existed); please verify a burst lands ON an anchor after annotating a pyro/confetti event.
  • M12 interaction: fx pulse writes points/splat.scale between fx.update and scene3d.update — same-frame, restored exactly; no interaction with export expected, but a combined FX+export pass is worth one look.
  • Real-focused-browser eyeball (bursts/pulse at full rAF rate) — the pane verifications above used the pump; a human glance in a focused window is the usual final check.

Blockers: none.

Next: merge lane/f-socialmain (coordinator/integration2 discretion).