diff options
Diffstat (limited to 'public/bluebey-studio/src/history.js')
| -rw-r--r-- | public/bluebey-studio/src/history.js | 105 |
1 files changed, 0 insertions, 105 deletions
diff --git a/public/bluebey-studio/src/history.js b/public/bluebey-studio/src/history.js deleted file mode 100644 index d988b50..0000000 --- a/public/bluebey-studio/src/history.js +++ /dev/null @@ -1,105 +0,0 @@ -/** - * Undo / redo for the whole studio state. - * - * The studio is a big pile of sliders, and before this every experiment was - * one-way: nudging the wrong slider meant dialling the old value back by hand. - * A history of whole-state snapshots is the simplest thing that can possibly - * work here, because the state is already the single source of truth and every - * control funnels through `applyState`. - * - * Two details matter for it to feel right rather than merely correct: - * - * - Dragging a slider fires an event per pixel, which would bury the history in - * hundreds of near-identical entries. Entries therefore carry a label, and a - * new entry with the *same* label within `coalesceMs` REPLACES the previous - * one instead of stacking on top of it. So a whole drag becomes one step. - * - * - `view.backgroundImage` can be a multi-megabyte data URL, and `state` also - * holds the caption text. The snapshots copy objects by hand rather than via - * `JSON.parse(JSON.stringify(...))`, because assigning a string in JavaScript - * shares it instead of duplicating it - so a hundred snapshots of a heavy - * state stay cheap. - */ - -const SHALLOW_TYPES = new Set(['string', 'number', 'boolean', 'undefined']); - -/** Deep copy that shares string data (and handles the odd null/array). */ -function copy(value) { - if (value === null || SHALLOW_TYPES.has(typeof value)) return value; - if (Array.isArray(value)) return value.map(copy); - if (typeof value === 'object') { - const out = {}; - for (const [key, inner] of Object.entries(value)) out[key] = copy(inner); - return out; - } - return value; // functions, symbols: not part of the saved state -} - -export class History { - constructor({ limit = 120, coalesceMs = 700, onChange = null } = {}) { - this.limit = Math.max(2, limit); - this.coalesceMs = coalesceMs; - this.onChange = onChange; - /** @type {{ state: object, label: string, at: number }[]} */ - this.entries = []; - this.index = -1; - } - - /** Forget everything and start from `state` (call after load/reset). */ - reset(state, label = 'start') { - this.entries = [{ state: copy(state), label, at: Date.now() }]; - this.index = 0; - this.onChange?.(this); - } - - get canUndo() { return this.index > 0; } - get canRedo() { return this.index >= 0 && this.index < this.entries.length - 1; } - - /** Label of the step undo would jump to, for the button tooltip. */ - get undoLabel() { return this.canUndo ? this.entries[this.index].label : null; } - get redoLabel() { return this.canRedo ? this.entries[this.index + 1].label : null; } - - /** - * Record the state *after* a change. Repeating the same label in quick - * succession (a slider drag) keeps a single entry that follows the value. - */ - push(state, label = '変更') { - const now = Date.now(); - const top = this.entries[this.index]; - const sameDrag = top - && top.label === label - && now - top.at <= this.coalesceMs - && this.index === this.entries.length - 1; - - if (sameDrag) { - this.entries[this.index] = { state: copy(state), label, at: now }; - } else { - this.entries.length = this.index + 1; - this.entries.push({ state: copy(state), label, at: now }); - if (this.entries.length > this.limit) this.entries.shift(); - this.index = this.entries.length - 1; - } - this.onChange?.(this); - return this; - } - - /** The previous snapshot, or `null` when there is nothing to go back to. */ - undo() { - if (!this.canUndo) return null; - this.index -= 1; - this.onChange?.(this); - return copy(this.entries[this.index].state); - } - - redo() { - if (!this.canRedo) return null; - this.index += 1; - this.onChange?.(this); - return copy(this.entries[this.index].state); - } - - /** A plain description of where we are, for tests and debug output. */ - describe() { - return this.entries.map((entry, i) => `${i === this.index ? '*' : ' '}${entry.label}`).join(' | '); - } -} |
