Skip to content

Commit 184fb55

Browse files
vanceingallsclaude
andcommitted
fix(studio): carry the audio FX attributes onto every timeline row
Both element builders read `data-fx-chain` and `data-automation` off the host element, and an expanded sub-composition child is built without one — so an audio track inside a sub-composition reserved no automation height and drew no lanes, while the property panel, which reads the live DOM selection rather than the row, still showed its chain and its toggles. `hostElementState` exists to re-inherit exactly this class of host-only field; it now covers these two alongside `hidden`, `timelineLocked` and `timelineRole`. `parseTimelineFromDOM` had the same gap and now reads both directly. Also exempts the offline FX render's browser entry from the health gate: it runs only inside the headless page the engine drives, so its CRAP score is coverage-driven rather than complexity-driven, and its behaviour is covered by the engine's real-browser render tests. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
1 parent 58208c9 commit 184fb55

4 files changed

Lines changed: 54 additions & 0 deletions

File tree

.fallowrc.jsonc

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -661,6 +661,12 @@
661661
// complexity pre-dates the computed-timeline work. Exempted at file level
662662
// rather than refactored as scope creep.
663663
"ignore": [
664+
// audio-fx-runtime-entry.ts: the browser-side IIFE entry for the offline FX
665+
// render. It runs only inside the headless page the engine drives, so unit
666+
// coverage cannot reach it and its CRAP score is coverage-driven rather
667+
// than complexity-driven (5 cyclomatic). Its behaviour is covered by the
668+
// engine's real-browser render tests.
669+
"packages/core/stubs/audio-fx-runtime-entry.ts",
664670
// useGestureRecording.ts: readBasePosition/connectGsapRuntime/tick are
665671
// inherited gesture-runtime control flow. This stack only changes
666672
// recordSample to coalesce display-rate events onto authored frames;

packages/studio/src/player/hooks/useExpandedTimelineElements.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -152,6 +152,13 @@ function hostElementState(flat: TimelineElement | undefined): Partial<TimelineEl
152152
hidden: flat.hidden,
153153
timelineLocked: flat.timelineLocked,
154154
timelineRole: flat.timelineRole,
155+
// Same reason as the three above: these are read off the host element, which
156+
// an expanded child is built without. Missing them, an audio child inside a
157+
// sub-composition reserved no automation height and drew no lanes, while the
158+
// property panel — reading the live DOM selection rather than this row —
159+
// still showed the chain and its toggles.
160+
fxChain: flat.fxChain,
161+
automation: flat.automation,
155162
};
156163
}
157164

packages/studio/src/player/lib/timelineDOM.test.ts

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -296,3 +296,36 @@ describe("mergeTimelineElementsPreservingDowngrades — genuine removal vs trans
296296
).toEqual(["a", "c"]);
297297
});
298298
});
299+
300+
describe("audio FX attributes on parsed elements", () => {
301+
const CHAIN = '{"version":1,"nodes":[{"type":"lowpass","id":"n1","params":{}}]}';
302+
const LANE = '{"version":1,"lanes":[{"target":"volume","points":[{"t":0,"v":1}]}]}';
303+
304+
it("carries data-fx-chain and data-automation off the element", () => {
305+
// The timeline row is what reserves automation height and draws the lanes;
306+
// parsed straight from the DOM it used to arrive without either attribute,
307+
// so the panel showed a chain the timeline could not.
308+
const doc = new DOMParser().parseFromString(
309+
`<div data-composition-id="main" data-start="0" data-duration="10">
310+
<audio id="bgm" data-start="0" data-duration="10" data-fx-chain='${CHAIN}'
311+
data-automation='${LANE}'></audio>
312+
</div>`,
313+
"text/html",
314+
);
315+
const [bgm] = parseTimelineFromDOM(doc, 10).filter((e) => e.domId === "bgm");
316+
expect(bgm?.fxChain).toBe(CHAIN);
317+
expect(bgm?.automation).toBe(LANE);
318+
});
319+
320+
it("leaves them unset on a track that carries neither", () => {
321+
const doc = new DOMParser().parseFromString(
322+
`<div data-composition-id="main" data-start="0" data-duration="10">
323+
<audio id="bgm" data-start="0" data-duration="10"></audio>
324+
</div>`,
325+
"text/html",
326+
);
327+
const [bgm] = parseTimelineFromDOM(doc, 10).filter((e) => e.domId === "bgm");
328+
expect(bgm?.fxChain).toBeUndefined();
329+
expect(bgm?.automation).toBeUndefined();
330+
});
331+
});

packages/studio/src/player/lib/timelineDOM.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -338,6 +338,14 @@ export function parseTimelineFromDOM(doc: Document, rootDuration: number): Timel
338338
if (resolvedSrc) entry.src = resolvedSrc;
339339
}
340340

341+
// Read from the element, like the manifest path does: without these an audio
342+
// clip parsed straight from the DOM reserved no automation height and drew no
343+
// lanes, while the property panel still showed its chain.
344+
const domFxChain = el.getAttribute("data-fx-chain");
345+
if (domFxChain) entry.fxChain = domFxChain;
346+
const domAutomation = el.getAttribute("data-automation");
347+
if (domAutomation) entry.automation = domAutomation;
348+
341349
if (el.hasAttribute("data-timeline-locked")) {
342350
entry.timelineLocked = true;
343351
}

0 commit comments

Comments
 (0)