209 lines
5.0 KiB
JavaScript
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');
|
|
}
|
|
}
|