aboutsummaryrefslogtreecommitdiffhomepage
path: root/public/bluebey-studio/src/history.js
diff options
context:
space:
mode:
Diffstat (limited to 'public/bluebey-studio/src/history.js')
-rw-r--r--public/bluebey-studio/src/history.js105
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(' | ');
- }
-}