From e82fc4a775cd2de51dd23c25524b0f02be6ac373 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 6 Apr 2026 23:51:48 +0000 Subject: [PATCH] feat(04-04): add UpdateParamsCommand + BlockInspector module - UpdateParamsCommand with execute/undo for color/speed/direction/length - Uses module-level currentShowId (not this._showId) matching existing pattern - BlockInspector renders compact strip with animation label, color swatch, speed input, direction select, length input, and remove button - Color change committed on 'change' event (not 'input') to avoid command flood - _onDelete callback wired to remove button for app.js to handle --- lightsync/frontend/timeline/commands.js | 21 ++++ lightsync/frontend/timeline/inspector.js | 118 +++++++++++++++++++++++ 2 files changed, 139 insertions(+) create mode 100644 lightsync/frontend/timeline/inspector.js diff --git a/lightsync/frontend/timeline/commands.js b/lightsync/frontend/timeline/commands.js index 2d66049..0bf1acc 100644 --- a/lightsync/frontend/timeline/commands.js +++ b/lightsync/frontend/timeline/commands.js @@ -88,6 +88,27 @@ export class ResizeBlockCommand { } } +// ── UpdateParamsCommand ────────────────────────────────────────────────────── + +export class UpdateParamsCommand { + constructor(tracks, block, oldParams, newParams) { + this.tracks = tracks; + this.block = block; + this.oldParams = { ...oldParams }; // shallow copy + this.newParams = { ...newParams }; // shallow copy + } + + execute() { + Object.assign(this.block.params, this.newParams); + syncBlock(currentShowId, this.block.device_id, this.block, 'PATCH'); + } + + undo() { + Object.assign(this.block.params, this.oldParams); + syncBlock(currentShowId, this.block.device_id, this.block, 'PATCH'); + } +} + // ── DeleteBlockCommand ─────────────────────────────────────────────────────── export class DeleteBlockCommand { diff --git a/lightsync/frontend/timeline/inspector.js b/lightsync/frontend/timeline/inspector.js new file mode 100644 index 0000000..f3b2d54 --- /dev/null +++ b/lightsync/frontend/timeline/inspector.js @@ -0,0 +1,118 @@ +// BlockInspector — compact horizontal strip for editing selected block params +// Appears between timeline canvas and transport bar when a block is selected. +// All parameter changes go through UpdateParamsCommand for full undo/redo support. + +import { UpdateParamsCommand } from './commands.js'; + +export class BlockInspector { + constructor(containerEl, history) { + this.el = containerEl; // #block-inspector div + this.history = history; // CommandHistory reference + this.tracks = null; // set by app.js from timeline.tracks + this.currentBlock = null; + this._onDelete = null; // callback(block) — set by app.js + } + + show(block) { + this.currentBlock = block; + if (!block) { + this.el.style.display = 'none'; + return; + } + this.el.style.display = 'flex'; + this._render(block); + } + + hide() { + this.currentBlock = null; + this.el.style.display = 'none'; + } + + _render(block) { + const animName = (block.animation || 'BLOCK').toUpperCase().replace(/_/g, ' '); + const color = block.params?.color || '#00ffff'; + const speed = block.params?.speed ?? 1.0; + const direction = block.params?.direction || 'forward'; + const length = block.params?.length ?? 0.3; + + this.el.innerHTML = ` + ${animName} + + + + +
+ + `; + + this._wireEvents(block); + } + + _wireEvents(block) { + // Color swatch → open hidden color input + const swatch = this.el.querySelector('#color-swatch'); + const colorInput = this.el.querySelector('#inspector-color'); + swatch?.addEventListener('click', () => colorInput?.click()); + + // Live preview: update swatch as color drags + colorInput?.addEventListener('input', (e) => { + swatch.style.background = e.target.value; + }); + // Commit on change (not during drag — avoids command flood) + colorInput?.addEventListener('change', (e) => { + const oldParams = { ...block.params }; + const newParams = { ...block.params, color: e.target.value }; + const cmd = new UpdateParamsCommand(this.tracks, block, oldParams, newParams); + this.history.execute(cmd); + }); + + // Speed + this.el.querySelector('#inspector-speed')?.addEventListener('change', (e) => { + const val = parseFloat(e.target.value); + if (isNaN(val) || val < 0.1 || val > 10) return; + const oldParams = { ...block.params }; + const newParams = { ...block.params, speed: val }; + const cmd = new UpdateParamsCommand(this.tracks, block, oldParams, newParams); + this.history.execute(cmd); + }); + + // Direction + this.el.querySelector('#inspector-direction')?.addEventListener('change', (e) => { + const oldParams = { ...block.params }; + const newParams = { ...block.params, direction: e.target.value }; + const cmd = new UpdateParamsCommand(this.tracks, block, oldParams, newParams); + this.history.execute(cmd); + }); + + // Length + this.el.querySelector('#inspector-length')?.addEventListener('change', (e) => { + const val = parseFloat(e.target.value); + if (isNaN(val) || val < 0.05 || val > 1) return; + const oldParams = { ...block.params }; + const newParams = { ...block.params, length: val }; + const cmd = new UpdateParamsCommand(this.tracks, block, oldParams, newParams); + this.history.execute(cmd); + }); + + // Remove button + this.el.querySelector('.inspector-remove')?.addEventListener('click', () => { + if (this._onDelete) this._onDelete(block); + }); + } +}