/** * History Manager Module * * Provides undo/redo functionality for annotation operations. * Stores snapshots of the strokes array after each action. * * ## Architecture * * Uses a simple state-snapshot approach: * - Each action stores a complete copy of the strokes array * - Undo restores the previous snapshot * - Redo restores the next snapshot * * This is simpler than command-pattern and works well for * moderate numbers of strokes. * * ## History Structure * * history = [state0, state1, state2, ...] * currentIndex points to the current state * * - Undo: move currentIndex backward * - Redo: move currentIndex forward * - New action: truncate history after currentIndex, push new state */ // Default configuration const DEFAULT_MAX_HISTORY = 50; // History state let history = []; let currentIndex = -1; let maxHistory = DEFAULT_MAX_HISTORY; // Callback for when strokes need to be restored let restoreCallback = null; /** * Initializes the history manager. * @param {Object} options * @param {number} [options.maxHistory=50] - Maximum history depth * @param {(strokes: Array) => void} options.onRestore - Callback when strokes are restored */ export function initHistory(options = {}) { maxHistory = options.maxHistory || DEFAULT_MAX_HISTORY; restoreCallback = options.onRestore || null; history = []; currentIndex = -1; console.log(`[History] Initialized with max depth: ${maxHistory}`); } /** * Deep clones the strokes array for storage. * @param {Array} strokes * @returns {Array} */ function cloneStrokes(strokes) { return strokes.map(stroke => ({ ...stroke, points: stroke.points.map(p => ({ ...p })) })); } /** * Records a new state in history. * Call this after any action that modifies strokes (add, erase, clear). * * @param {Array} strokes - The current strokes array * @param {string} [actionName='action'] - Name of the action for debugging */ export function recordState(strokes, actionName = 'action') { // If we're not at the end of history, truncate forward history if (currentIndex < history.length - 1) { history = history.slice(0, currentIndex + 1); } // Clone and store the new state const snapshot = cloneStrokes(strokes); history.push(snapshot); // Enforce max history limit if (history.length > maxHistory) { history.shift(); } else { currentIndex++; } console.log(`[History] Recorded "${actionName}" (index: ${currentIndex}, total: ${history.length})`); } /** * Undoes the last action. * @returns {Array | null} The restored strokes array, or null if can't undo */ export function undo() { if (!canUndo()) { console.log('[History] Nothing to undo'); return null; } currentIndex--; const restoredStrokes = cloneStrokes(history[currentIndex]); console.log(`[History] Undo (index: ${currentIndex})`); if (restoreCallback) { restoreCallback(restoredStrokes); } return restoredStrokes; } /** * Redoes the last undone action. * @returns {Array | null} The restored strokes array, or null if can't redo */ export function redo() { if (!canRedo()) { console.log('[History] Nothing to redo'); return null; } currentIndex++; const restoredStrokes = cloneStrokes(history[currentIndex]); console.log(`[History] Redo (index: ${currentIndex})`); if (restoreCallback) { restoreCallback(restoredStrokes); } return restoredStrokes; } /** * Checks if undo is available. * @returns {boolean} */ export function canUndo() { return currentIndex > 0; } /** * Checks if redo is available. * @returns {boolean} */ export function canRedo() { return currentIndex < history.length - 1; } /** * Clears all history. */ export function clearHistory() { history = []; currentIndex = -1; console.log('[History] Cleared'); } /** * Gets the current history state for debugging. * @returns {{ currentIndex: number, historyLength: number, canUndo: boolean, canRedo: boolean }} */ export function getHistoryState() { return { currentIndex, historyLength: history.length, canUndo: canUndo(), canRedo: canRedo(), maxHistory }; } /** * Sets the maximum history depth. * @param {number} max */ export function setMaxHistory(max) { maxHistory = Math.max(10, Math.min(500, max)); // Trim history if needed while (history.length > maxHistory) { history.shift(); currentIndex = Math.max(0, currentIndex - 1); } console.log(`[History] Max history set to: ${maxHistory}`); } /** * Gets the maximum history depth. * @returns {number} */ export function getMaxHistory() { return maxHistory; } /** * Records the initial empty state. * Call this once when the annotation system initializes. */ export function recordInitialState() { if (history.length === 0) { history.push([]); currentIndex = 0; console.log('[History] Initial empty state recorded'); } }