mirror of
https://github.com/heygen-com/hyperframes.git
synced 2026-09-03 04:38:33 +00:00
fix(studio): re-read a group's own automation after an edit lands on it
Automating a group's effect parameter from the FX rack left the group's timeline row showing nothing — no lanes, and after the previous commit no wave button either, since it now gates on the count. The rack is not group-aware: selecting a group and writing its `data-automation` goes through the ordinary element attribute path. That path's resync re-read the element's OWN automation and chain and stopped there. But a group's lanes are derived from the mirrored `audioGroup*` fields its MEMBERS carry, never from the group element — so the mirrors kept the value they were born with until a reload, and the row had nothing to draw. Fixed at the sync sink, which exists precisely so a writer does not have to know what needs refreshing: it now also re-reads what each member's group carries, through the cache that already invalidates itself on any group attribute change. The four fields are compared as one record rather than a growing `&&` chain, which keeps the callback under the complexity gate and stops the next field being added to the read but not the compare. Verified live: the Voiceover group's row went from no toggle to `∿ 1` — its one author-owned lane, the carve's three bands correctly excluded. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5
parent
624166f380
commit
f26f48b3a6
@@ -80,4 +80,30 @@ describe("syncStoredAutomationFromPreview", () => {
|
||||
syncStoredAutomationFromPreview(null);
|
||||
expect(usePlayerStore.getState().elements[0]?.automation).toBe(TWO_POINTS);
|
||||
});
|
||||
// The reported symptom: automate a parameter on a GROUP from the FX rack and
|
||||
// the group's row shows no automation. The rack is not group-aware — it
|
||||
// writes the attribute on the group node through the ordinary element path —
|
||||
// and the timeline reads a group's lanes from the mirrors its MEMBERS carry,
|
||||
// which that path used to leave untouched.
|
||||
it("re-reads what the group carries onto every member that belongs to it", () => {
|
||||
const chain = '{"version":1,"nodes":[{"type":"gain","id":"n1","params":{"gain":0}}]}';
|
||||
const groupAutomation =
|
||||
'{"version":1,"lanes":[{"target":"fx.n1.gain","points":[{"t":0,"v":0}]}]}';
|
||||
const doc = document.implementation.createHTMLDocument("preview");
|
||||
const group = doc.createElement("hf-audio-group");
|
||||
group.id = "voiceover";
|
||||
group.setAttribute("data-fx-chain", chain);
|
||||
group.setAttribute("data-automation", groupAutomation);
|
||||
const audio = doc.createElement("audio");
|
||||
audio.id = "bgm";
|
||||
audio.setAttribute("data-audio-group", "voiceover");
|
||||
doc.body.append(group, audio);
|
||||
|
||||
usePlayerStore.setState({ elements: [el({ audioGroup: "voiceover" })] });
|
||||
syncStoredAutomationFromPreview(doc);
|
||||
|
||||
const stored = usePlayerStore.getState().elements[0];
|
||||
expect(stored?.audioGroupAutomation).toBe(groupAutomation);
|
||||
expect(stored?.audioGroupFxChain).toBe(chain);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -16,6 +16,7 @@
|
||||
import { HF_AUDIO_AUTOMATION_ATTR } from "@hyperframes/core/audio-automation";
|
||||
import { HF_AUDIO_FX_ATTR } from "@hyperframes/core/audio-fx";
|
||||
import { usePlayerStore, type TimelineElement } from "../store/playerStore";
|
||||
import { groupInfoFor } from "./timelineGroupInfo";
|
||||
|
||||
/** The preview node an element stands for, by dom id and then by `data-hf-id`. */
|
||||
function previewNodeFor(doc: Document, element: TimelineElement): Element | null {
|
||||
@@ -38,6 +39,27 @@ function previewNodeFor(doc: Document, element: TimelineElement): Element | null
|
||||
* Reads rather than being told: an undo restores whole files, so the attribute it
|
||||
* reverted is only known by looking.
|
||||
*/
|
||||
/**
|
||||
* What an element's four synced fields SHOULD read, given the preview.
|
||||
*
|
||||
* Its own two come off its node; the other two are its copy of what its group
|
||||
* carries. The timeline derives a group's lanes and chain from these mirrors,
|
||||
* never from the group element — and the FX rack is not group-aware: selecting
|
||||
* a group and automating one of its parameters writes `data-automation` on the
|
||||
* group node through the ordinary element path, which used to refresh an
|
||||
* element's own two fields and nothing else. So the group's row went on reading
|
||||
* the value it was born with, and its `∿` never appeared.
|
||||
*/
|
||||
function syncedFields(doc: Document, element: TimelineElement, node: Element) {
|
||||
const group = element.audioGroup ? groupInfoFor(doc, element.audioGroup) : null;
|
||||
return {
|
||||
automation: node.getAttribute(HF_AUDIO_AUTOMATION_ATTR) ?? undefined,
|
||||
fxChain: node.getAttribute(HF_AUDIO_FX_ATTR) ?? undefined,
|
||||
audioGroupAutomation: group?.automation,
|
||||
audioGroupFxChain: group?.fxChain,
|
||||
};
|
||||
}
|
||||
|
||||
export function syncStoredAutomationFromPreview(doc: Document | null | undefined): void {
|
||||
if (!doc) return;
|
||||
usePlayerStore.setState((state) => {
|
||||
@@ -45,11 +67,13 @@ export function syncStoredAutomationFromPreview(doc: Document | null | undefined
|
||||
const elements = state.elements.map((element) => {
|
||||
const node = previewNodeFor(doc, element);
|
||||
if (!node) return element;
|
||||
const automation = node.getAttribute(HF_AUDIO_AUTOMATION_ATTR) ?? undefined;
|
||||
const fxChain = node.getAttribute(HF_AUDIO_FX_ATTR) ?? undefined;
|
||||
if (automation === element.automation && fxChain === element.fxChain) return element;
|
||||
const fields = syncedFields(doc, element, node);
|
||||
// Same array back when nothing moved: `elements` keys memos all over the
|
||||
// timeline, and a fresh object per sync would re-render every one.
|
||||
const keys = Object.keys(fields) as (keyof typeof fields)[];
|
||||
if (keys.every((key) => fields[key] === element[key])) return element;
|
||||
changed = true;
|
||||
return { ...element, automation, fxChain };
|
||||
return { ...element, ...fields };
|
||||
});
|
||||
return changed ? { elements } : {};
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user