[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:
parent
de3416869f
commit
1f8ad13ed6
11
.claude/launch.json
Normal file
11
.claude/launch.json
Normal 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
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
@ -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).
|
||||||
|
|||||||
@ -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).
|
||||||
|
|||||||
@ -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.
|
||||||
|
|||||||
@ -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>
|
||||||
|
|||||||
@ -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);
|
||||||
|
|||||||
@ -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>
|
|
||||||
Loading…
Reference in New Issue
Block a user