Files
controller-v2/client/src/pages/eq/hooks/usePeqUndoHistory.ts
T

124 lines
3.2 KiB
TypeScript

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<PeqUndoSnapshot[]>([]);
const pendingBeforeRef = useRef<PeqUndoSnapshot | null>(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,
};
}