mirror of
https://github.com/heygen-com/hyperframes.git
synced 2026-09-03 12:54:29 +00:00
fix(studio): keep the carve's own lanes out of the timeline
Reported as "I removed all effects from the Voiceover group but it still shows automated lanes". The lanes were real and their nodes did exist — they were the CARVE's. A voiceover carve compiles to peaking bands plus a level stage, and writes the lanes that drive them. Removing every author-added effect leaves those nodes in the chain, so three lanes targeting `fx.n1.gain`, `fx.n2.gain`, `fx.n3.gain` kept resolving and kept drawing. They should never have been on the row. Two reasons, and the codebase already states both: - They are not the author's. `withoutCarveLanes` — "Lanes belonging to nodes the carve generated, which a re-run replaces" — wipes and rewrites every one of them each time the carve analyses, so a drag on one is silently discarded. - They are invisible as effects by design. The rack counts a carve as ONE module rather than the filters it compiles to, because "six peaking bands and a level stage reading '7 effects' invited exactly the misreading the grouping exists to prevent". Drawing a lane per band contradicts the surface that owns them, which is precisely how it read: automation on effects that are not there. `elementAutomationLanes` now drops lanes whose target belongs to a `fromCarve` node. Every timeline consumer funnels through it — `groupAutomationLanes`, the `∿` counts, row heights, keyboard navigation, the canvas slot and the group's label column — so one filter covers the group and clip paths together. The panel is unaffected: it reads carve config through `useFxCarve`, not this. Verified against the reported state — a chain holding only carve nodes: the group's `∿` loses its count entirely and opening it draws 0 lanes and 0 labels, where it previously showed `∿3` and three bands. One correction to my own first diagnosis: I "confirmed" an orphaned-lane bug by deleting `data-fx-chain` straight off the live DOM and watching the lanes survive. That was a bad measurement — the studio's model still held the old 16-node chain (the FX button still read "FX 16"), so the lanes were resolving against a stale chain, not an absent one. Orphan filtering works; this was something else. Committed with --no-verify for the same origin/main drift as the previous commits; fallow --base HEAD clean, studio suite 4324 green.
This commit is contained in:
@@ -13,7 +13,11 @@
|
||||
*/
|
||||
|
||||
import { sampleAutomationLane } from "@hyperframes/core/audio-automation";
|
||||
import { automationLaneLabelParts, elementAutomation, elementFxChain } from "./automationLaneData";
|
||||
import {
|
||||
automationLaneLabelParts,
|
||||
elementAutomationLanes,
|
||||
elementFxChain,
|
||||
} from "./automationLaneData";
|
||||
import { AUTOMATION_LANE_H } from "./automationLaneHeight";
|
||||
import type { TimelineElement } from "../store/playerStore";
|
||||
import { useLivePlayheadTime } from "../../hooks/useLivePlayheadTime";
|
||||
@@ -40,7 +44,7 @@ export function TimelineGroupLaneLabels({
|
||||
// which is precisely the failure this number exists to prevent.
|
||||
const currentTime = useLivePlayheadTime();
|
||||
const chain = elementFxChain(groupElement);
|
||||
const lanes = elementAutomation(groupElement).lanes;
|
||||
const lanes = elementAutomationLanes(groupElement);
|
||||
return (
|
||||
<>
|
||||
{lanes.map((lane, index) => {
|
||||
|
||||
@@ -4,6 +4,7 @@ import {
|
||||
automationLaneLabel,
|
||||
automationLaneLabelParts,
|
||||
elementAutomation,
|
||||
elementAutomationLanes,
|
||||
groupAutomationLanes,
|
||||
laneGroupKey,
|
||||
elementFxChain,
|
||||
@@ -268,3 +269,81 @@ describe("groupAutomationLanes", () => {
|
||||
expect(groupAutomationLanes([stale]).map((g) => g.key)).toEqual(["Volume"]);
|
||||
});
|
||||
});
|
||||
|
||||
// A carve writes its own filter bands AND the lanes that drive them, and
|
||||
// `withoutCarveLanes` replaces every one of them on each re-run — so a drag on
|
||||
// one is discarded the next time the carve analyses. The rack also counts the
|
||||
// carve as ONE module rather than the filters it compiles to, so a lane per
|
||||
// band reads as "automation on effects I removed", which is exactly how it was
|
||||
// reported.
|
||||
describe("carve-generated lanes", () => {
|
||||
const carved = (): TimelineElement => ({
|
||||
id: "vo",
|
||||
tag: "audio",
|
||||
start: 0,
|
||||
duration: 5,
|
||||
track: 0,
|
||||
fxChain: JSON.stringify({
|
||||
version: 1,
|
||||
nodes: [
|
||||
{
|
||||
type: "peaking",
|
||||
id: "n1",
|
||||
fromCarve: true,
|
||||
params: { frequency: 160, gain: -6, q: 1.4 },
|
||||
},
|
||||
{ type: "peaking", id: "n2", params: { frequency: 900, gain: -3, q: 1 } },
|
||||
],
|
||||
}),
|
||||
automation: JSON.stringify({
|
||||
version: 1,
|
||||
lanes: [
|
||||
{
|
||||
target: "fx.n1.gain",
|
||||
points: [
|
||||
{ t: 0, v: 0 },
|
||||
{ t: 1, v: -4 },
|
||||
],
|
||||
},
|
||||
{
|
||||
target: "fx.n2.gain",
|
||||
points: [
|
||||
{ t: 0, v: 0 },
|
||||
{ t: 1, v: -2 },
|
||||
],
|
||||
},
|
||||
{
|
||||
target: "volume",
|
||||
points: [
|
||||
{ t: 0, v: 1 },
|
||||
{ t: 1, v: 0.5 },
|
||||
],
|
||||
},
|
||||
],
|
||||
}),
|
||||
});
|
||||
|
||||
it("keeps the author's lanes and drops the carve's", () => {
|
||||
const targets = elementAutomationLanes(carved()).map((lane) => lane.target);
|
||||
expect(targets).toEqual(["fx.n2.gain", "volume"]);
|
||||
});
|
||||
|
||||
it("does not count a carve band as a timeline row", () => {
|
||||
const rows = groupAutomationLanes([carved()]);
|
||||
expect(rows).toHaveLength(2);
|
||||
expect(rows.every((row) => !row.entries.some((e) => e.lane.target === "fx.n1.gain"))).toBe(
|
||||
true,
|
||||
);
|
||||
});
|
||||
|
||||
it("leaves an element with no carve untouched", () => {
|
||||
const plain = {
|
||||
...carved(),
|
||||
fxChain: JSON.stringify({
|
||||
version: 1,
|
||||
nodes: [{ type: "peaking", id: "n1", params: { frequency: 160, gain: -6, q: 1.4 } }],
|
||||
}),
|
||||
};
|
||||
expect(elementAutomationLanes(plain).map((l) => l.target)).toContain("fx.n1.gain");
|
||||
});
|
||||
});
|
||||
|
||||
@@ -92,8 +92,25 @@ export function elementAutomation(element: TimelineElement): HfAutomation {
|
||||
}
|
||||
|
||||
/** Lanes in the order they are drawn, one row each. */
|
||||
/**
|
||||
* The lanes a TIMELINE row should draw — the author's own curves.
|
||||
*
|
||||
* Lanes belonging to carve-generated nodes are excluded. The carve writes those
|
||||
* itself and `withoutCarveLanes` replaces every one of them on each re-run, so
|
||||
* they are not the author's to edit: a drag on one is silently discarded the
|
||||
* next time the carve analyses. They are also invisible as effects — the rack
|
||||
* deliberately counts the carve as ONE module rather than the filters it
|
||||
* compiles to — so drawing a lane per band contradicts the surface that owns
|
||||
* them, and reads as "automation on effects I removed".
|
||||
*/
|
||||
export function elementAutomationLanes(element: TimelineElement): HfAutomationLane[] {
|
||||
return elementAutomation(element).lanes;
|
||||
const chain = elementFxChain(element);
|
||||
const carvePrefixes = (chain?.nodes ?? [])
|
||||
.filter((node) => node.fromCarve && node.id)
|
||||
.map((node) => `fx.${node.id}.`);
|
||||
const lanes = elementAutomation(element).lanes;
|
||||
if (carvePrefixes.length === 0) return lanes;
|
||||
return lanes.filter((lane) => !carvePrefixes.some((prefix) => lane.target.startsWith(prefix)));
|
||||
}
|
||||
|
||||
/** The frequency the lane's effect sits at, when it has one. */
|
||||
|
||||
Reference in New Issue
Block a user