SCENEGOD/logs/laneA.md
type-two dfd4cebd2b [laneA] M2: PiP + cameras/lights toolbar, eventized clip drops, onChange-on-add
- dock: /assets/tree {name,path,formats,thumb}; +camera/+key/+ambient toolbar;
  clip apply dispatches scenegod:clipdrop when timeline present (else preview);
  drag-from-dock places onto the viewport (screenToFloor / entityAt)
- stage: PiP overlay from active camera; renderActiveCamera hardened as M3 target
  + pause()/resume(); backdrop rebuild-on-param; fire onChange on add+remove
  (fixes tlui empty-names + a gizmo mouseUp id-vs-entity bug); set-active-camera
- verified in the integrated app (:8020, Lane B timeline mounted), zero console
  errors. Found + logged the entity-mirror integration gap (timeline model isn't
  seeded from live stage adds) for the orchestrator.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-18 20:14:52 +10:00

8.2 KiB

Lane A log — append-only, newest at bottom

Format per session:

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

(no sessions yet)

2026-07-18 session 1

DONE: M1 shipped and verified end-to-end in a browser.

  • web/room3d.js — ported the rigroom retarget as a pure ES module (no DOM/globals). Exports parseAny/canon/boneMap/captureRest/bakeRetarget/disposeRoot/stats + _selftest. bakeRetarget is now (clip, srcRoot, srcRest, tgtRoot, tgtRest, opts?) — target is a parameter instead of the closed-over char. Algorithm kept byte-for-byte (30fps world-delta bake, hips-height ratio, quat sign continuity, dup-hierarchy dedupe). Added .bvh to parseAny.
  • web/stage.jsclass Stage per PLAN §4.2. IBL (RoomEnvironment+PMREM), grid+circle floor, OrbitControls director cam, TransformControls gizmo (W/E/R), raycast select, shadow-casting. addEntity handles character/prop/backdrop(plane|corner|dome)/camera/light; wrapper-Group holds all transforms, loaded root normalized+grounded underneath. prepareClip caches parse-per-path and bake-per-(entity,path,index). captureState/applyState round-trip entities.
  • web/dock.js — asset dock (Characters/Props/Backdrops/Animations tabs from /assets/tree, offline→file-drop) + inspector (transform/params editors, ⏺key dispatches scenegod:capturekey, delete). Local drop: image→backdrop, mesh→character, clip-onto-selected-character→bake+play.
  • web/index.html + web/style.css — CSS-grid layout (dock/view/inspector/timeline), rigroom importmap (three 0.160.0), ?selftest=1 badge. Timeline loaded via guarded dynamic import so the page never breaks when Lane B's file is absent.

VERIFICATION (browser, in-app):

  • ?selftest=1: PASS — two 3-bone skeletons, different rests+namespaces, baked 1s clip, world-delta match maxErr=6.4e-12, srcMoved=0.224rad (guard against a silently-frozen source).
  • Real end-to-end (Female_mixamo.fbx char + "Running.fbx" clip + a jpg backdrop, served from a throwaway scratchpad harness): char loaded (65 bones, 18.6k tris, normalized), gizmo-moved, clip retargeted (66 tracks) and animating live. Screenshot: logs/shots/m1-lady-running.png.
  • Page boots with zero console errors with BOTH timeline.js and the server absent.

DECISIONS:

  • selftest uses underscore namespaces (mixamorig1_/mixamorig4_), not colon — a colon in a bone name breaks three's PropertyBinding so the source clip wouldn't bind (silent pass). canon() still collapses both to the same key, which is the thing under test.
  • Added Stage.prepareClipUpload(id, buf, name, idx) (bake from in-memory bytes) so the offline file-drop path can retarget without a server. Stage.setMixerAuto(false) lets Lane B take over mixer stepping at SYNC 1 (default true so dropped clips preview standalone now).
  • backdrop param edits (mode/image/width) don't rebuild the mesh yet — marked M2. fov/light color/intensity are live.

BLOCKED/REQUESTS:

  • @Lane C: please confirm the item shape inside /assets/tree arrays. dock.js tolerates both a bare path string and {path,name,files:[{path,ext}]} (rigroom-style) for now — tell me the real one and I'll drop the fallback. Also confirm the page is served such that absolute /web/*.js imports resolve (I used /web/... to be URL-independent).

NEXT (M2, do not start until orchestrator flips the milestone line):

  • multiple cameras + PiP overlay via renderActiveCamera, light entity param plumbing polish, backdrop rebuild-on-param-change, drag-from-dock onto viewport (currently click-to-add).

2026-07-18 session 2 (M2)

DONE: M2 Lane-A scope shipped and verified in the integrated app (Lane C server :8020 + Lane B timeline/tlui mounted). Worked the orchestrator's priority order:

  • P0 /assets/tree shape — dropped the bare-string fallback; dock.js now reads the confirmed {name, path, formats, thumb} and renders thumb (→ /assets/file?path=) when present.
  • P1 eventize clip dropsdock._applyClip(id,path,idx): when window.timeline exists it dispatches scenegod:clipdrop {detail:{id,path,clipIndex}} (Lane B places the block); only bakes+previews standalone when no timeline. Uploaded/local clip files can't hand off a server path, so with a timeline up they toast a "use the dock" note instead of a dead preview.
  • P2 onChange on every entityStage.addEntity/removeEntity now fire _fireChange(en), so tlui's onChange→_syncRows builds a row per entity, incl. programmatic applyState loads. Fixed the empty-names bug on both add and remove. Also fixed a latent bug: the gizmo mouseUp fired _fireChange with the id not the entity, so the inspector never refreshed on drag.
  • P4 cameras/lights/PiP/backdrop/drag:
    • Stage PiP: overlay <canvas class="pip"> in #view, rendered each frame from the active cam (render active→main canvas, blit to pip, then render director last so the main view is correct). setActiveCamera shows/hides it. Verified 16,495 lit pixels (real content).
    • renderActiveCamera(canvas|null) hardened as the M3 final-render target; added pause()/resume() so Lane C can freeze the director loop and drive frames deterministically.
    • dock "add rig" toolbar: + camera / + key light / + ambient (these have no asset file); inspector gains ◉ set active camera for cameras; light castShadow param plumbed.
    • setParam rebuilds the backdrop mesh on mode/image/width change (_rebuildBackdrop).
    • drag-from-dock onto the viewport: cards are draggable; #view drop places chars/props at the cursor (screenToFloor) and bakes clips onto the character under the cursor (entityAt).

VERIFICATION (in-app, :8020, zero console errors throughout):

  • Added man + key light + ambient + camera via toolbar → all 4 appear as tlui rows immediately (P2 proven). Man lit by the scene lights. Screenshot: logs/shots/m2-cameras-lights-dome.png.
  • Clicked ANIMATIONS→hiphop on the selected man → scenegod:clipdrop fired with {id:'e1', path:'animations/test/hiphop.fbx', clipIndex:0} (P1 proven). Lane B DOES consume it (timeline.js:98) but the block doesn't land — see the integration gap below.
  • capturekey button → scenegod:capturekey {id:'e1'}. backdrop plane→dome rebuild swapped the root mesh with no error.

DECISIONS:

  • PiP renders the active cam into the main WebGL canvas then blits to the 2D pip canvas (one shared context); director view is rendered last each frame so it always wins the main canvas. Same primitive is the M3 render path: pause() the loop, renderActiveCamera(null), read pixels.
  • Local/upload clip files are a no-op-with-note when a timeline is mounted (no server path to place a block). ponytail: revisit only if John wants drag-in clip files to become timeline blocks.

BLOCKED/REQUESTS — INTEGRATION GAP (orchestrator, please route):

  • Live dock/stage-added entities never reach the timeline's model. Timeline.scene.entities starts [] (timeline.js:80) and is only filled by timeline.load(sceneJson). Both scenegod:clipdropaddClipDrop and scenegod:capturekeyaddKey call _tracks(id), which throws no entity <id> for anything added through the dock. Verified live at :8020: manOnStage=true, inTimelineModel=false, addClipDropErr="no entity e1". So on today's build, keyframing/clip-dropping a dock-added entity is dead — it only works after a scene load().
  • This is a cross-lane seam, not a Lane-A-file fix (timeline.js is Lane B's). The natural hook is already in place on my side: stage.onChange(en) now fires on every add AND remove (my P2 fix). Suggested resolution (Lane B, ~4 lines): in the Timeline ctor, stage.onChange(en => this._mirror(en)) where _mirror upserts/removes a scene.entities entry from {id,kind,label,source,params, transform: stage.entityTransform(id), tracks:{}}. I can instead dispatch a dedicated scenegod:entityadd/entityremove event from the dock if the orchestrator prefers an explicit contract over reusing onChange — say the word and I'll add it.

NEXT: once the entity-mirror seam is decided, the "lady + man + street + 2 cameras + sunset" SYNC-2 demo should play. Holding at the M2 boundary until the orchestrator flips the milestone line.