diff --git a/.planning/ROADMAP.md b/.planning/ROADMAP.md index 14401b7..b67d6c8 100644 --- a/.planning/ROADMAP.md +++ b/.planning/ROADMAP.md @@ -90,7 +90,7 @@ Plans: **Plans**: 4 plans Plans: -- [ ] 04-01: Timeline canvas — per-device tracks, time axis, block rendering, playback cursor +- [x] 04-01: Timeline canvas — per-device tracks, time axis, block rendering, playback cursor - [ ] 04-02: Block interactions — drag-and-place, move, resize, delete; command pattern undo/redo (min 20 steps) - [ ] 04-03: Beat detection + snap — librosa beat_track + plp + onset_detect in ProcessPoolExecutor, beat mark overlay, snap-to-beat, calibration offset UI - [ ] 04-04: Block editor — animation library panel, color picker, parameter controls per block, changes reflected in show file @@ -155,7 +155,7 @@ Phases execute in numeric order: 1 → 2 → 3 → 4 → 5 → 6 → 7 | 1. Foundation | 4/4 | Complete | 2026-04-05 | | 2. Audio Engine | 3/3 | Complete | 2026-04-06 | | 3. Communication Protocol | 3/3 | Complete | 2026-04-06 | -| 4. Timeline Editor | 0/4 | Not started | - | +| 4. Timeline Editor | 1/4 | In Progress| | | 5. Live Show Execution | 0/3 | Not started | - | | 6. AI Sync | 0/3 | Not started | - | | 7. Microcontroller Firmware | 0/3 | Not started | - | diff --git a/.planning/STATE.md b/.planning/STATE.md index c6129a2..546b27a 100644 --- a/.planning/STATE.md +++ b/.planning/STATE.md @@ -2,15 +2,15 @@ gsd_state_version: 1.0 milestone: v1.0 milestone_name: milestone -status: verifying -stopped_at: Phase 4 UI-SPEC approved -last_updated: "2026-04-06T23:11:05.583Z" +status: executing +stopped_at: Completed 04-timeline-editor/04-01-PLAN.md +last_updated: "2026-04-06T23:35:19.480Z" last_activity: 2026-04-06 progress: total_phases: 7 completed_phases: 3 - total_plans: 10 - completed_plans: 10 + total_plans: 14 + completed_plans: 11 percent: 0 --- @@ -21,13 +21,13 @@ progress: See: .planning/PROJECT.md (updated 2026-04-05) **Core value:** Load a song, build a synchronized light show, play it back live — with precise control over every effect on every device. -**Current focus:** Phase 03 — communication-protocol +**Current focus:** Phase 04 — timeline-editor ## Current Position -Phase: 4 -Plan: Not started -Status: Phase complete — ready for verification +Phase: 04 (timeline-editor) — EXECUTING +Plan: 2 of 4 +Status: Ready to execute Last activity: 2026-04-06 Progress: [░░░░░░░░░░] 0% @@ -62,6 +62,7 @@ Progress: [░░░░░░░░░░] 0% | Phase 03-communication-protocol P01 | 4min | 2 tasks | 8 files | | Phase 03-communication-protocol P02 | 3min | 1 tasks | 10 files | | Phase 03-communication-protocol P03 | 2min | 2 tasks | 3 files | +| Phase 04-timeline-editor P01 | 4min | 2 tasks | 5 files | ## Accumulated Context @@ -98,6 +99,8 @@ Recent decisions affecting current work: - [Phase 03-communication-protocol]: FireAnimation lazy-initializes heat array per led_count to support variable strip lengths - [Phase 03-communication-protocol]: asyncio.run() test pattern chosen over pytest-asyncio — not installed, plan provided this fallback - [Phase 03-communication-protocol]: UDPSender stopped before registry.save() in shutdown — orderly teardown, no sends after socket close +- [Phase 04-timeline-editor]: Beats API uses 'beats' field not 'beat_times' — loadBeats() handles both with fallback +- [Phase 04-timeline-editor]: TimelineCanvas auto-scroll at 80%/20% threshold — standard DAW follow behavior ### Pending Todos @@ -111,8 +114,8 @@ None yet. ## Session Continuity -Last session: 2026-04-06T23:11:05.576Z -Stopped at: Phase 4 UI-SPEC approved -Resume file: .planning/phases/04-timeline-editor/04-UI-SPEC.md +Last session: 2026-04-06T23:35:19.475Z +Stopped at: Completed 04-timeline-editor/04-01-PLAN.md +Resume file: None Next action: `/gsd:plan-phase 1` diff --git a/.planning/phases/04-timeline-editor/04-01-SUMMARY.md b/.planning/phases/04-timeline-editor/04-01-SUMMARY.md new file mode 100644 index 0000000..5c565da --- /dev/null +++ b/.planning/phases/04-timeline-editor/04-01-SUMMARY.md @@ -0,0 +1,83 @@ +--- +phase: 04-timeline-editor +plan: 01 +subsystem: frontend-canvas +tags: [canvas, timeline, daw-ui, audio, waveform] +dependency_graph: + requires: [Phase 03 communication protocol — UDP sender, animation encoders] + provides: [TimelineCanvas class, timeline rendering foundation] + affects: [lightsync/frontend, lightsync/models/show.py] +tech_stack: + added: [HTML5 Canvas 2D API, ES module import/export, requestAnimationFrame render loop] + patterns: [HiDPI canvas with devicePixelRatio, coordinate system with HEADER_WIDTH/TIME_AXIS_HEIGHT offsets, layer-ordered canvas render] +key_files: + created: + - lightsync/frontend/timeline/timeline.js + modified: + - lightsync/models/show.py + - lightsync/frontend/index.html + - lightsync/frontend/style.css + - lightsync/frontend/app.js +decisions: + - beats API returns 'beats' field (not 'beat_times') — loadBeats() uses data.beats with data.beat_times as fallback + - Auto-scroll threshold at 80% canvas width, cursor repositioned to 20% — smooth follow during playback + - Empty state messages rendered inside canvas (not DOM overlay) — avoids z-index issues +metrics: + duration: 4min + completed: 2026-04-06 + tasks: 2 + files: 5 +--- + +# Phase 04 Plan 01: Timeline Canvas Foundation Summary + +**One-liner:** HTML5 canvas DAW timeline with per-device tracks, waveform background, beat marks, and realtime playback cursor synced to audio.currentTime via requestAnimationFrame. + +## What Was Built + +- **CueModel.duration** (float = 4.0) — block length in seconds, Pydantic default handles old show files +- **AnalysisBlock.calibration_offset** (float = 0.03) — compensates librosa latency bias, applied when rendering beat marks +- **TimelineCanvas class** (`lightsync/frontend/timeline/timeline.js`) — 280+ line canvas engine: + - Coordinate system: `timeToX()`, `xToTime()`, `trackIndexToY()`, `yToTrackIndex()` + - HiDPI rendering via `devicePixelRatio` in `_resizeCanvas()` + - Layer render order: background → track banding → waveform → beat marks → cue blocks → drag ghost → headers → time axis → cursor + - Auto-scroll: cursor triggers viewport shift when it reaches 80% of canvas width + - Mouse wheel horizontal scroll + - Empty state messages for no-devices and no-audio cases +- **HTML layout restructured** — main-area now contains toolbar → canvas → inspector strip; ANIMATIONS panel moved to sidebar +- **CSS added** — timeline toolbar, canvas fill, inspector strip, snap button active state +- **app.js wired** — imports TimelineCanvas, creates instance, wires zoom/beat-offset/snap controls, populates animation palette dynamically + +## Decisions Made + +| Decision | Rationale | +|----------|-----------| +| beats API 'beats' field (not 'beat_times') | API returns `{"beats": [...]}` — plan expected `beat_times` — auto-fixed (Rule 1) | +| Auto-scroll at 80%/20% threshold | Standard DAW follow behavior — keeps cursor in view without jerky jumps | +| Empty state in canvas | Simpler than DOM overlay, no z-index conflicts with toolbar/inspector | + +## Deviations from Plan + +### Auto-fixed Issues + +**1. [Rule 1 - Bug] beats API field name mismatch** +- **Found during:** Task 2 implementation +- **Issue:** Plan specified `data.beat_times` but `/api/audio/beats` returns `{"beats": [...]}` (from `analyze()` in `beats.py`) +- **Fix:** `loadBeats()` uses `data.beats || data.beat_times || []` — handles both field names +- **Files modified:** `lightsync/frontend/timeline/timeline.js` +- **Commit:** efa6123 + +## Known Stubs + +None — timeline renders live data from `/api/devices`, `/api/audio/waveform`, and `/api/audio/beats`. Cue blocks will be empty (`track.cues = []`) until Plan 04-02 adds block drag-and-drop, but that is expected for this phase. + +## Self-Check: PASSED + +- `lightsync/frontend/timeline/timeline.js` exists: YES (efa6123) +- `lightsync/models/show.py` has `duration: float = 4.0`: YES (454fddc) +- `lightsync/models/show.py` has `calibration_offset: float = 0.03`: YES (454fddc) +- `lightsync/frontend/index.html` has `id="timeline-canvas"`: YES +- `lightsync/frontend/style.css` has `#timeline-canvas`, `.block-inspector`: YES +- `lightsync/frontend/app.js` imports TimelineCanvas: YES +- Docker build and startup: CLEAN (no errors) +- Python model import test: PASSED (`CueModel(timestamp=1.0).duration == 4.0`)