Files
2026-01-18 00:13:02 +09:00

209 lines
5.0 KiB
JavaScript

/**
* 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');
}
}