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
This commit is contained in:
Claude
2026-04-06 23:51:48 +00:00
parent d1965dcefc
commit e82fc4a775
2 changed files with 139 additions and 0 deletions

View File

@@ -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 {

View File

@@ -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 = `
<span class="inspector-label">${animName}</span>
<label class="inspector-field">
COLOR
<div class="inspector-color-swatch" id="color-swatch" style="background:${color};" title="Click to change color"></div>
<input type="color" id="inspector-color" value="${color}" style="display:none;">
</label>
<label class="inspector-field">
SPEED
<input type="number" id="inspector-speed" step="0.1" min="0.1" max="10" value="${speed}">
</label>
<label class="inspector-field">
DIR
<select id="inspector-direction">
<option value="forward" ${direction === 'forward' ? 'selected' : ''}>FWD</option>
<option value="reverse" ${direction === 'reverse' ? 'selected' : ''}>REV</option>
</select>
</label>
<label class="inspector-field">
LEN
<input type="number" id="inspector-length" step="0.05" min="0.05" max="1" value="${length}">
</label>
<div style="flex:1"></div>
<button class="inspector-remove" title="Remove block">&#10005;</button>
`;
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);
});
}
}