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:
@@ -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 {
|
||||
|
||||
118
lightsync/frontend/timeline/inspector.js
Normal file
118
lightsync/frontend/timeline/inspector.js
Normal 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">✕</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);
|
||||
});
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user