import { useCallback, useRef, useState } from "react"; import { cloneBands } from "../peqMappers"; export const PEQ_UNDO_MAX_STEPS = 30; export type PeqUndoBand = { freq: number; gain: number; q: number; type: string; enabled: boolean; }; export type PeqUndoSnapshot = { abMode: "A" | "B"; bandsByMode: { A: PeqUndoBand[]; B: PeqUndoBand[] }; preamp: number; autoPre: number; }; function cloneSnapshot(snapshot: PeqUndoSnapshot): PeqUndoSnapshot { return { abMode: snapshot.abMode, bandsByMode: { A: cloneBands(snapshot.bandsByMode.A), B: cloneBands(snapshot.bandsByMode.B), }, preamp: snapshot.preamp, autoPre: snapshot.autoPre, }; } export function peqUndoSnapshotsEqual( a: PeqUndoSnapshot, b: PeqUndoSnapshot, ): boolean { if (a.abMode !== b.abMode) return false; if (a.preamp !== b.preamp || a.autoPre !== b.autoPre) return false; return ( JSON.stringify(a.bandsByMode.A) === JSON.stringify(b.bandsByMode.A) && JSON.stringify(a.bandsByMode.B) === JSON.stringify(b.bandsByMode.B) ); } /** * Undo stack for EQ parameter edits. * - One stack entry = one complete gesture (drag release / confirmed change) * - Max {@link PEQ_UNDO_MAX_STEPS} entries * - No redo */ export function usePeqUndoHistory(maxSteps = PEQ_UNDO_MAX_STEPS) { const stackRef = useRef([]); const pendingBeforeRef = useRef(null); const [canUndo, setCanUndo] = useState(false); const syncCanUndo = useCallback(() => { setCanUndo(stackRef.current.length > 0); }, []); const clear = useCallback(() => { stackRef.current = []; pendingBeforeRef.current = null; setCanUndo(false); }, []); const push = useCallback( (snapshot: PeqUndoSnapshot) => { stackRef.current.push(cloneSnapshot(snapshot)); while (stackRef.current.length > maxSteps) { stackRef.current.shift(); } syncCanUndo(); }, [maxSteps, syncCanUndo], ); /** Discrete edit: push current state as undo target before mutating. */ const captureBefore = useCallback( (snapshot: PeqUndoSnapshot) => { pendingBeforeRef.current = null; push(snapshot); }, [push], ); /** Continuous gesture start (slider / curve drag). */ const beginGesture = useCallback((snapshot: PeqUndoSnapshot) => { pendingBeforeRef.current = cloneSnapshot(snapshot); }, []); /** Continuous gesture end: commit only if state actually changed. */ const commitGesture = useCallback( (current: PeqUndoSnapshot) => { const before = pendingBeforeRef.current; pendingBeforeRef.current = null; if (!before) return; if (peqUndoSnapshotsEqual(before, current)) return; push(before); }, [push], ); const cancelGesture = useCallback(() => { pendingBeforeRef.current = null; }, []); const undo = useCallback((): PeqUndoSnapshot | null => { pendingBeforeRef.current = null; const next = stackRef.current.pop() ?? null; syncCanUndo(); return next ? cloneSnapshot(next) : null; }, [syncCanUndo]); return { canUndo, clear, captureBefore, beginGesture, commitGesture, cancelGesture, undo, }; }