[sync1] wire TimelineUI to real Stage, null-mixer guard, M2 unlock + orchestrator updates

- index.html: mount TimelineUI, setMixerAuto(false) when timeline present
- timeline.js: _evalClips guards no-blocks / null-mixer (real Stage returns
  null for non-characters; the stub never did)
- tldev.html removed; stagestub.js kept as the node-test fixture
- lane files: M1 accepted for all three, answers to open questions, M2
  priorities (A: clipdrop event, PiP; B: row/scene-bar refresh; C: straight
  into M3 render pipeline)
- SCENEGOD launch.json (informational; preview uses MESHGOD's entry)

SYNC 1 verified in-browser: character+backdrop+retargeted clip scene loads,
scrub interpolates frame-exact, scene save/load round-trips via server.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
type-two 2026-07-18 18:36:16 +10:00
parent de3416869f
commit 1f8ad13ed6
7 changed files with 87 additions and 63 deletions

11
.claude/launch.json Normal file
View File

@ -0,0 +1,11 @@
{
"version": "0.0.1",
"configurations": [
{
"name": "scenegod",
"runtimeExecutable": "/Users/johnking/Documents/SCENEGOD/.venv/bin/uvicorn",
"runtimeArgs": ["scenegod.server:app", "--port", "8020", "--app-dir", "/Users/johnking/Documents/SCENEGOD"],
"port": 8020
}
]
}

View File

@ -97,4 +97,26 @@ disposeRoot). Port rules:
- Page loads clean with timeline.js absent AND with server absent (mock=1). - Page loads clean with timeline.js absent AND with server absent (mock=1).
## ORCHESTRATOR UPDATES ## ORCHESTRATOR UPDATES
(none yet — check back each session)
### 2026-07-18 — M1 ACCEPTED, SYNC 1 done. **M2 is UNLOCKED.**
M1 verified end-to-end in-browser by the orchestrator (character + backdrop +
retargeted clip + timeline scrub, all through the real Stage). Answers + M2 order:
1. **/assets/tree item shape confirmed** (from Lane C's server):
`{name, path, formats: ["fbx", ...], thumb: relpath|null}` — rigroom-style,
no `files` array. Drop your bare-string fallback. `/web/*` absolute imports
are confirmed correct as served.
2. **Priority 1 — eventize clip drops.** `stage.setMixerAuto(false)` is now
active whenever the timeline mounts, so your drop-clip-to-preview path is
dead in the integrated page. Replace it: on clip-drop onto a character,
dispatch `window CustomEvent 'scenegod:clipdrop'` with
`{detail: {id, path, clipIndex}}` (Lane B adds the block at the playhead).
Keep bake-and-preview ONLY when no timeline is present.
3. **Priority 2 — make sure `onChange` fires for every entity added via
`applyState`** (SYNC finding: tlui's names column stayed empty after a
programmatic scene load — fix on both sides; B is doing theirs).
4. Then per plan: multiple cameras + PiP overlay (`renderActiveCamera` needs
to work as the M3 final-render target — treat that contract as hard),
backdrop rebuild-on-param-change, drag-from-dock onto viewport.
Test assets now live in `assets/{characters,animations,backdrops}/test/`
(man.fbx, hiphop.fbx, carrying.fbx, street.jpg — gitignored, on ultra).

View File

@ -78,4 +78,30 @@ Clip crossfades polish, params lanes UI, camera-cut lane UI, snapping
config, undo wiring, box-select + multi-drag of keys. config, undo wiring, box-select + multi-drag of keys.
## ORCHESTRATOR UPDATES ## ORCHESTRATOR UPDATES
(none yet — check back each session)
### 2026-07-18 — M1 ACCEPTED, SYNC 1 done. **M2 is UNLOCKED.**
Your model survived integration with the real Stage; transform interp and
clip retarget playback verified frame-exact via `step()`. Notes:
1. **Fix applied by orchestrator** (in your file, keep it): `_evalClips` now
early-returns when the entity has no clip blocks or no mixer — the real
Stage returns `null` from `entityMixer` for backdrops/props/cameras; your
stub always returned an object, which hid the crash. Lesson for the stub:
mirror the contract's nullable returns.
2. **stagestub.js was NOT deleted**`timeline_test.mjs` imports it. It's
now a permanent test fixture, not dev scaffolding. tldev.html is gone.
3. **SYNC findings, fix first in M2:** (a) after `load()` + `applyState`,
your track-name rows never appeared (only "Cameras") — rebuild rows on
`load()` and on `stage.onChange`; (b) the scene-bar name/duration fields
don't refresh after `load()` — bind them. Verified repro: load a 2-entity
scene programmatically, rows stay empty.
4. **New event:** Lane A will dispatch `scenegod:clipdrop`
`{detail:{id, path, clipIndex}}` when a clip is dropped on a character —
add a clip block on that entity at the playhead (default out = clip
duration after preload, fade 0.25 when it abuts a neighbor).
5. rAF note: in the embedded preview the tab can be `document.hidden`
rAF fully suspended → `play()` appears frozen. Not a bug in your clock
(verified); don't "fix" it. `step()` is the deterministic path and works.
6. Injected `<style id=laneB-tl-css>` is fine permanently — keep it.
Then per plan: crossfade fade-IN polish, params + camera-cut lane UI,
snapping config, box-select multi-drag (undo already wired).

View File

@ -86,4 +86,23 @@ green = M1 code-done. Run with `| lm -l 2`.
- test_server.py green; paste the summary line in your log. - test_server.py green; paste the summary line in your log.
## ORCHESTRATOR UPDATES ## ORCHESTRATOR UPDATES
(none yet — check back each session)
### 2026-07-18 — M1 ACCEPTED. **M2 unlocked AND M3 render pipeline green-lit early.**
test_server.py green, tree/file/scenes verified live through the browser page
(scene save 200 + lossless reload). You're ahead of the other lanes, so skip
the wait: after the small M2 items (thumbnails endpoint; validation already
solid), go straight into §C5 — implement `/render/begin|frame|end|status|out`
+ ffmpeg encode, and `web/render.js`. Notes:
1. `timeline.step(frame)` is verified deterministic against the real Stage
(orchestrator stepped 0/30/60/90/120 and got exact eased positions) — your
final-render loop can rely on it today. `stage.renderActiveCamera` as a
render target is Lane A M2 work; until it lands, test `finalRender` with
the director view canvas, and test the ffmpeg path with synthetic PNGs
from a script (extend test_server.py: begin → POST 30 generated frames →
end → poll → assert mp4 exists + ffprobe duration ≈ 1s).
2. ffmpeg on ultra: check `shutil.which` at boot as designed; it's installed
via homebrew here.
3. Test assets for the tree now exist under `assets/*/test/` (gitignored).
4. uvicorn for local dev: `.venv/bin/uvicorn scenegod.server:app --port 8020`
— there's also a launch entry in MESHGOD/.claude/launch.json ("scenegod")
used by the orchestrator's browser preview.

View File

@ -48,8 +48,11 @@ const dock = new Dock({
// Timeline is optional — the page must never break when Lane B's file is absent // Timeline is optional — the page must never break when Lane B's file is absent
try { try {
const { Timeline } = await import('/web/timeline.js'); const { Timeline } = await import('/web/timeline.js');
const { TimelineUI } = await import('/web/tlui.js');
window.timeline = new Timeline(stage); window.timeline = new Timeline(stage);
} catch(e){ /* timeline.js not present yet */ } window.tlui = new TimelineUI(window.timeline, document.getElementById('timeline'));
stage.setMixerAuto(false); // timeline owns mixer stepping from here on
} catch(e){ console.warn('timeline not mounted:', e); }
window.stage = stage; window.dock = dock; window.stage = stage; window.dock = dock;
</script> </script>

View File

@ -218,7 +218,9 @@ export class Timeline {
_evalClips(e, t) { _evalClips(e, t) {
const blocks = e.tracks.clips; const blocks = e.tracks.clips;
if (!blocks || !blocks.length) return;
const mixer = this.stage.entityMixer(e.id); const mixer = this.stage.entityMixer(e.id);
if (!mixer) return; // SYNC1: real Stage returns null for non-characters (stub always had one)
for (let i = 0; i < blocks.length; i++) { for (let i = 0; i < blocks.length; i++) {
const b = blocks[i]; const b = blocks[i];
const rec = this._clipActions.get(b); const rec = this._clipActions.get(b);

View File

@ -1,59 +0,0 @@
<!doctype html>
<!-- tldev.html — Lane B DEV-ONLY harness to exercise the timeline UI against
the stub before Lane A's index.html exists. Deleted at SYNC 1. -->
<meta charset="utf-8">
<title>SCENEGOD · timeline dev harness</title>
<style>
html,body{margin:0;height:100%;background:#0d1117;color:#c9d1d9;font:13px ui-monospace,monospace}
#top{padding:8px 12px;border-bottom:1px solid #2b323c}
#log{height:120px;overflow:auto;padding:6px 12px;color:#565f6b;font-size:11px;white-space:pre-wrap;border-bottom:1px solid #2b323c}
#timeline{position:fixed;left:0;right:0;bottom:0}
</style>
<div id="top">Lane B dev harness — stub stage. Double-click a lane to add a key,
drag keys, ▶ to play, drag the ruler to scrub. setTransform/param calls log below.</div>
<div id="log"></div>
<div id="timeline"></div>
<script type="module">
import { StageStub } from './stagestub.js';
import { Timeline } from './timeline.js';
import { TimelineUI } from './tlui.js';
// mirror stub effects into the on-page log
const logEl = document.getElementById('log');
const stage = new StageStub();
const _st = stage.setTransform.bind(stage), _sp = stage.setParam.bind(stage), _sc = stage.setActiveCamera.bind(stage);
const line = (s) => { logEl.textContent = s + '\n' + logEl.textContent.slice(0, 4000); };
stage.setTransform = (id, t) => { _st(id, t); line(`setTransform ${id} pos=[${t.pos.map(n=>n.toFixed(2))}]`); };
stage.setParam = (id, k, v) => { _sp(id, k, v); line(`setParam ${id} ${k}=${v}`); };
stage.setActiveCamera = (id) => { _sc(id); line(`activeCamera ${id}`); };
const scene = {
version: 1, name: 'dev', fps: 30, duration: 6,
entities: [
{ id: 'lady', kind: 'character', label: 'lady',
source: { type: 'assets', path: 'characters/lady.glb' },
transform: { pos: [0,0,0], rot: [0,0,0], scale: 1 },
tracks: { transform: [
{ t: 0, pos: [-3,0,0], rot: [0,0,0], scale: 1, ease: 'linear' },
{ t: 4, pos: [3,0,0], rot: [0,1.57,0], scale: 1, ease: 'inout' } ],
clips: [ { path: 'animations/walk.fbx', clipIndex: 0, start: 0.5, in: 0, out: 2.4, loop: 1, fade: 0 } ] } },
{ id: 'sun', kind: 'light', label: 'sun',
params: { type: 'key', color: '#ffcc88', intensity: 1 },
transform: { pos: [2,5,2], rot: [0,0,0], scale: 1 },
tracks: { params: [ { t: 0, key: 'intensity', value: 0.2, ease: 'inout' }, { t: 5, key: 'intensity', value: 3, ease: 'inout' } ] } },
{ id: 'camA', kind: 'camera', label: 'wide', params: { fov: 40 }, transform: { pos: [0,1.5,6], rot: [0,0,0], scale: 1 }, tracks: {} },
{ id: 'camB', kind: 'camera', label: 'close', params: { fov: 60 }, transform: { pos: [2,1.6,2], rot: [0,0,0], scale: 1 }, tracks: {} },
],
cameraCuts: [ { t: 0, camera: 'camA' }, { t: 3, camera: 'camB' } ],
audio: [],
};
await stage.applyState(scene);
const tl = new Timeline(stage);
tl.load(scene);
await tl.preload();
const ui = new TimelineUI(tl, '#timeline');
window._tl = tl; window._ui = ui; window._stage = stage; // console poking
line('ready — scene loaded');
</script>