SCENEGOD/logs/laneB.md
type-two 65b7757fd2 [laneB] log: M2 session 3
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-18 20:41:59 +10:00

8.5 KiB

Lane B log — append-only, newest at bottom

Format per session:

## <date> session N
DONE: …
DECISIONS: …
BLOCKED/REQUESTS: …
NEXT: …

2026-07-18 session 1

DONE: M1 complete. All files under scenegod/web/.

  • timeline.js — model + clock, no DOM, no three import (self-contained quat/euler/slerp math so node can run it). load/toJSON lossless round-trip (unknown entity fields preserved via structuredClone + a WeakMap for cached quats). evaluate(t): transform (pos/scale lerp, rot slerp→euler, easing incl. step-hold), params (num lerp, color sRGB lerp), clip blocks (set-time exact scrubbing, fade-out ramp), camera cuts (fires only on change). Clock: play() rAF deltas, seek clamp, step(frame) pure/deterministic for Lane C render. Mutators addKey/moveKey/deleteKey/ addClipBlock/moveClipBlock/trimClipBlock/addCut + undo stack (undo() wired; Ctrl+Z hooked in tlui). capturekey window listener present. Clips need await tl.preload() before evaluate (prepareClip is async).
  • tlui.js — canvas panel into #timeline. Ruler scrub, drag playhead, space=play/pause, dblclick lane=add key from stage state, drag key (snap to frame, Alt=free), right-click=delete, drag clip block/edge, Home/End, Ctrl+Z. Scene bar name/dur + Save/Load (POST/GET /scenes/{name}, localStorage fallback). Names column auto-syncs to stage.entities() via onChange.
  • stagestub.js — full §4.2 Stage as plain objects; records effects into stage.applied[]. Dev-only, delete at SYNC 1.
  • timeline_test.mjsnode scenegod/web/timeline_test.mjs → green (midpoint interp, step-ease hold, clip local time @3 frames, camera flip, lossless round-trip, unknown-field preservation).
  • tldev.html — dev-only browser harness. Verified in browser: scrub moves entities (interp live in log), play runs to duration + auto-stops, camera cut flips camA→camB at t=3, keys/clips/cuts render. Delete at SYNC 1. DECISIONS:
  • Timeline imports NO three.js — inlined euler↔quat + slerp (~30 lines) so the node self-check needs zero deps (there is no node_modules/three in this stack). setTransform rot is euler (matches entityTransform format).
  • One <canvas> for all lanes (not canvas-per-lane); hit-testing via a hitbox list rebuilt each draw. Simpler, fast enough at this scale.
  • CSS injected from tlui.js via a <style id=laneB-tl-css> rather than creating web/style.css — that file is Lane A's and didn't exist yet. Move into a /* === LANE B === */ block at SYNC if John wants it centralized. (See REQUEST below.)
  • M1 crossfade = fade-OUT only; cross-block fade-IN is M2 "crossfade polish" per the milestone list. Marked with a ponytail comment in timeline.js. BLOCKED/REQUESTS:
  • None blocking. Note for orchestrator/Lane A: my timeline styles live in an injected <style> in tlui.js. If you'd rather they sit in style.css, say so and I'll emit a /* === LANE B === */ block for Lane A to paste (I won't edit style.css directly — ownership).
  • Lane A's files (index.html, stage.js, room3d.js, dock.js, style.css) are present untracked in the shared working dir but uncommitted — I staged ONLY my 5 files, left theirs alone. NEXT: Hold at M1 boundary. At SYNC 1 the stub swaps for the real Stage (one-line change in the page import) and tldev.html/stagestub.js get deleted. M2 (locked): clip crossfade polish, params/cut lane UI, snapping config, undo Ctrl+Z already stubbed, box-select multi-drag.

2026-07-18 session 2 (M2)

DONE: M2 per orchestrator update. SYNC findings fixed first, then polish.

  • SYNC1 #3a/#3b: added Timeline.onLoad(cb); load() fires it → TimelineUI rebuilds rows + refreshes name/duration fields. Verified with the exact repro (UI built against empty stage, then load() a 2-entity scene → rows now appear, scene bar updates).
  • SYNC1 #4 clipdrop: scenegod:clipdrop {id,path,clipIndex}addClipDrop drops a block at the playhead, out = clip.duration (from preload), auto fade:0.25 where it abuts a neighbour. Verified via dispatched event.
  • Crossfade fade-IN (was fade-OUT only): a predecessor's fade pre-rolls the successor's playback start earlier by pred.fade so local time is continuous across the boundary; weights ramp 1→0 / 0→1 and sum to 1 (asserted in test).
  • Params lanes (per-key hue), camera-cut lane add via dblclick on Cameras row (uses active cam / first camera), snap toggle in scene bar (Alt still = free), box-select + multi-drag + Delete of keys, selection highlight, fade wedge drawn on clip blocks, panel grows to content height (was fixed 200px → clipped the Cameras row once params rows were added).
  • timeline_test.mjs extended: onLoad fires once, addClipDrop out=duration, crossfade weights sum to 1. All green. DECISIONS / BUG:
  • tlui reads tracks from the TIMELINE, not the stage entity. Found mid- verify: load() structuredClones the scene, so timeline.scene.entities and stage entities are SEPARATE objects. Drawing from stage meant clipdrop/ addKey (which mutate the timeline copy) never appeared. Repointed all draw methods to _te(id) = tl.scene.entities.find(id). Rows now built from the timeline list merged with any stage-only (dock-added, not-yet-keyframed) entities. Per PLAN the timeline owns tracks — this is the correct source.
  • Grow-to-content height, no scroll yet (ponytail: add max-height+scroll only if a scene ever has enough entities to dominate the viewport). BLOCKED/REQUESTS:
  • Gap for the orchestrator/Lane A to note: there's no bridge for a dock-added stage entity to enter timeline.scene.entities, so a freshly dropped character can't be keyframed until a scene load. tlui shows it as a row (empty tracks) but mutators need the entity present. If Lane A's dock should register new entities with the timeline, we need a timeline.addEntity(desc) (or the dock calls stage.captureState()timeline.load). Flagging for SYNC 2 — not building it uninvited.
  • Verified with a throwaway harness (_check.html) served on :8099; deleted, NOT committed (orchestrator removed the M1 tldev.html deliberately). NEXT: Hold at M2. Remaining M2 polish if a session 3 is granted: undo already wired to Ctrl+Z; could add snapping increment config + keyable-param add UI (currently params keys come from Lane A capture/inspector). Await SYNC 2.

2026-07-18 session 3 (M2 polish)

DONE: All three orchestrator session-3 items.

  1. Absorbed _mirror (orchestrator's seam) — reviewed; it upserts dock-added stage entities into scene.entities with empty tracks and drops them when stage.getEntity(id)===null. Fixed the STUB to make removal testable: removeEntity now fires onChange WITH the removed entity object (was firing null, so _mirror(null) early-returned and never removed). Added timeline_test.mjs lifecycle case: dock-add → keyframe → remove, asserting entity+empty-tracks appear then vanish.
  2. setDuration(x) mutator: clamps the playhead into range, KEEPS keys past the new end (lossless), notifies the UI (reuses onLoad → syncRows + refreshBar). Wired the dur field to it — plain scene.duration = x skipped the bar refresh. Test asserts clamp + key retention + notify.
  3. Snap-increment select (frame / 0.1 / 0.25 / 1s) in the scene bar; _snap rounds to the chosen step (Alt still = free). Keyable-param add: dblclick a params lane captures the entity's current param values as keys (skips type/boolean descriptors) — mirrors the transform-capture dblclick. DECISIONS:
  • params lane now shows only when the entity has ≥1 param key OR kind is camera/light. _mirror seeds tracks.params=[] (truthy), which was giving every mirrored character an empty params lane; tightened to tracks?.params?.length > 0 || camera|light.
  • setDuration reuses the onLoad callback list as its "refresh UI chrome" hook rather than adding a second callback channel (lazy; onLoad handler already does syncRows+refreshBar+draw). BLOCKED/REQUESTS: none. Verified with a throwaway harness (_check.html, served :8099, deleted, NOT committed). timeline_test.mjs green. NEXT: M2 polish complete. Undo already wired (Ctrl+Z). Open for M4 timeline work when unlocked (audio tracks on the timeline UI + Web Audio sync, per PLAN §5 M4) or any SYNC 3 fixes. Holding until orchestrator flips M4.