"use strict"; // User-facing undo/redo for direct editing actions. (function (global) { const MAX_HISTORY = 32; const MAX_HISTORY_BYTES = 8 * 1024 * 1024; function ensure(worldRef) { if (!worldRef) return null; if (!Array.isArray(worldRef._undoStack)) worldRef._undoStack = []; if (!Array.isArray(worldRef._redoStack)) worldRef._redoStack = []; return worldRef; } function codec() { const value = global.TarinaiSaveCodec; if (!value || typeof value.encodeBinarySnapshot !== "function" || typeof value.decodeBinarySnapshot !== "function") { throw new Error("TarinaiSaveCodec binary history API is unavailable"); } return value; } // Two independent 32-bit accumulators make accidental duplicate suppression // extremely unlikely without converting the complete snapshot to JSON text. function fingerprintBytes(bytes) { let h1 = 0x811c9dc5; let h2 = 0x9e3779b9; for (let i = 0; i < bytes.length; i += 1) { const value = bytes[i]; h1 ^= value; h1 = Math.imul(h1, 0x01000193) >>> 0; h2 ^= value + ((i & 255) << 8); h2 = Math.imul(h2 ^ (h2 >>> 16), 0x85ebca6b) >>> 0; } return `${bytes.length}:${h1.toString(36)}:${h2.toString(36)}`; } function makeHistoryEntry(world, snapshot, label) { if (!snapshot) return null; const bytes = codec().encodeBinarySnapshot(snapshot); return { bytes, key: fingerprintBytes(bytes), label: String(label || "action"), time: Number(world?.time) || 0, }; } function captureEntry(world, label) { const snapshot = global.TarinaiSnapshot.createSnapshot(world); return makeHistoryEntry(world, snapshot, label); } function stackBytes(stack) { let total = 0; for (const entry of stack || []) total += Number(entry?.bytes?.byteLength || entry?.bytes?.length || 0) || 0; return total; } function trim(stack, maxBytes = MAX_HISTORY_BYTES, keepRecent = 1) { if (stack.length > MAX_HISTORY) stack.splice(0, stack.length - MAX_HISTORY); let total = stackBytes(stack); const minKeep = Math.max(0, Math.min(stack.length, Math.floor(Number(keepRecent) || 0))); while (stack.length > minKeep && total > maxBytes) { const removed = stack.shift(); total -= Number(removed?.bytes?.byteLength || removed?.bytes?.length || 0) || 0; } return total; } function trimMemory(worldRef = global.world, options = {}) { const world = ensure(worldRef); if (!world) return 0; const targetBytes = Math.max(1 * 1024 * 1024, Number(options.targetBytes || MAX_HISTORY_BYTES) || MAX_HISTORY_BYTES); const keepRecent = Math.max(1, Math.floor(Number(options.keepRecent || 6) || 6)); const half = Math.floor(targetBytes * 0.5); const undoBytes = trim(world._undoStack, half, keepRecent); const redoBytes = trim(world._redoStack, targetBytes - Math.min(half, undoBytes), Math.min(keepRecent, 4)); return undoBytes + redoBytes; } function capture(worldRef = global.world, label = "action") { const world = ensure(worldRef); if (!world || world._historyRestoring) return false; const entry = captureEntry(world, label); if (!entry) return false; if (entry.key && entry.key === world._lastUndoSnapshotKey) return false; world._undoStack.push(entry); trim(world._undoStack); world._redoStack.length = 0; world._lastUndoSnapshotKey = entry.key; return true; } function restore(worldRef, entry) { if (!worldRef || !entry?.bytes) return false; const snapshot = codec().decodeBinarySnapshot(entry.bytes); if (!snapshot) return false; worldRef._historyRestoring = true; try { global.TarinaiRestoreCoordinator.restoreSnapshot(snapshot, worldRef, { syncUi: true }); worldRef._lastUndoSnapshotKey = entry.key || fingerprintBytes(entry.bytes); return true; } finally { worldRef._historyRestoring = false; } } function moveHistory(worldRef, fromKey, toKey, fallbackLabel) { const world = ensure(worldRef); const from = world?.[fromKey]; const to = world?.[toKey]; if (!world || !from?.length || !Array.isArray(to)) return { ok: false, reason: "empty" }; const current = captureEntry(world, fallbackLabel); const target = from.pop(); if (current) { to.push(current); trim(to); } const ok = restore(world, target); return { ok, label: target?.label || "" }; } function undo(worldRef = global.world) { return moveHistory(worldRef, "_undoStack", "_redoStack", "redo"); } function redo(worldRef = global.world) { return moveHistory(worldRef, "_redoStack", "_undoStack", "undo"); } global.TarinaiHistory = Object.freeze({ capture, undo, redo, trimMemory }); })(typeof window !== "undefined" ? window : globalThis);