mirror of
https://github.com/heygen-com/hyperframes.git
synced 2026-11-12 20:47:30 +00:00
feat(studio): automation selection slice
This commit is contained in:
@@ -0,0 +1,14 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { usePlayerStore } from "./playerStore";
|
||||
|
||||
describe("automationSelectionSlice", () => {
|
||||
it("stores one ordered selection and clears it", () => {
|
||||
const store = usePlayerStore.getState();
|
||||
store.setAutomationSelection({ elementKey: "bgm", target: "volume", t0: 2, t1: 1 });
|
||||
const sel = usePlayerStore.getState().automationSelection;
|
||||
// Ordered on write, so every consumer can assume t0 < t1.
|
||||
expect(sel).toEqual({ elementKey: "bgm", target: "volume", t0: 1, t1: 2 });
|
||||
usePlayerStore.getState().clearAutomationSelection();
|
||||
expect(usePlayerStore.getState().automationSelection).toBeNull();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,38 @@
|
||||
/**
|
||||
* The active time selection on one automation lane.
|
||||
*
|
||||
* A store slice, not lane-local state, for the same reason keyframe selection
|
||||
* is one: Delete/copy/paste handlers and the shape menu live outside the lane
|
||||
* component and need to read it. Ephemeral by construction — nothing
|
||||
* serializes store state, and the selection must never survive into a render.
|
||||
*/
|
||||
import type { StoreApi } from "zustand";
|
||||
|
||||
export interface AutomationSelection {
|
||||
/** TimelineElement key (key ?? id) of the clip that owns the lane. */
|
||||
elementKey: string;
|
||||
/** Lane target: "volume" or "fx.<nodeId>.<param>". */
|
||||
target: string;
|
||||
/** Clip-local seconds; always t0 < t1 (ordered on write). */
|
||||
t0: number;
|
||||
t1: number;
|
||||
}
|
||||
|
||||
export interface AutomationSelectionSlice {
|
||||
automationSelection: AutomationSelection | null;
|
||||
setAutomationSelection: (sel: AutomationSelection) => void;
|
||||
clearAutomationSelection: () => void;
|
||||
}
|
||||
|
||||
export function createAutomationSelectionSlice(
|
||||
set: StoreApi<AutomationSelectionSlice>["setState"],
|
||||
): AutomationSelectionSlice {
|
||||
return {
|
||||
automationSelection: null,
|
||||
setAutomationSelection: (sel) =>
|
||||
set({
|
||||
automationSelection: sel.t0 <= sel.t1 ? sel : { ...sel, t0: sel.t1, t1: sel.t0 },
|
||||
}),
|
||||
clearAutomationSelection: () => set({ automationSelection: null }),
|
||||
};
|
||||
}
|
||||
@@ -10,6 +10,10 @@ import {
|
||||
} from "../../utils/studioUiPreferences";
|
||||
import { clampTimelineZoomPercent, computePinnedZoomPercent } from "../components/timelineZoom";
|
||||
import { createKeyframeSlice, type KeyframeCacheEntry, type KeyframeSlice } from "./keyframeSlice";
|
||||
import {
|
||||
createAutomationSelectionSlice,
|
||||
type AutomationSelectionSlice,
|
||||
} from "./automationSelectionSlice";
|
||||
|
||||
export type { KeyframeCacheEntry } from "./keyframeSlice";
|
||||
|
||||
@@ -40,7 +44,7 @@ function resolveElementSelection(
|
||||
};
|
||||
}
|
||||
|
||||
interface PlayerState extends KeyframeSlice {
|
||||
interface PlayerState extends KeyframeSlice, AutomationSelectionSlice {
|
||||
isPlaying: boolean;
|
||||
currentTime: number;
|
||||
duration: number;
|
||||
@@ -296,6 +300,8 @@ export const usePlayerStore = create<PlayerState>((set, get) => ({
|
||||
timelineSessionEpoch: get().timelineSessionEpoch,
|
||||
})),
|
||||
|
||||
...createAutomationSelectionSlice(set),
|
||||
|
||||
activeKeyframePct: null,
|
||||
setActiveKeyframePct: (pct) => set({ activeKeyframePct: pct }),
|
||||
motionPathArmed: false,
|
||||
|
||||
Reference in New Issue
Block a user