新增 EQ撤销功能,用户调节的参数不满意可以点击回退到上一次,撤销栈最多存放 30 步
This commit is contained in:
@@ -0,0 +1,123 @@
|
||||
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,
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user