mirror of
https://github.com/heygen-com/hyperframes.git
synced 2026-09-03 12:54:29 +00:00
Sixth review pass. Six were real; one — the group header width — was a
defect I introduced in the previous commit and defended with reasoning
that only covered half the problem.
The header overhang was wrong. `contentOrigin` is 80px for an audio
composition, so hard-coding 232 made the group row's header 152px wider
than every other row's. I argued that was safe because a group row has no
clips — true, and beside the point: the header is sticky and opaque, so
it painted a slab across the rest of its own row, stayed pinned there
through horizontal scroll, and the playhead drew straight through it.
Groups now turn `labelMode` on instead, which is what that flag is for.
Every row gets the same 232px header, verified in the browser.
Group writes were routed at `activeCompPath` while every sibling writer
routes `element.sourceFile || activeCompPath`. Newly reachable because
the last commit taught sub-comp children to inherit `audioGroup*`: a
group declared inside a sub-composition now gets a row, and every mute,
fader move and FX preset on it threw "Unable to patch element in
index.html".
`Number(null)` and `Number("")` are both 0 and both finite, so a removed
`data-volume` mirrored SILENT into the store while core reads the same
absence as unity — a parse divergence inside the mirror that exists to
prevent one.
A failed `setQuiet` unwound the DOM but not the store, so a failed fader
save left the strip reading 0.4 while the preview played 1.0, with
nothing to re-parse and correct it.
`syncStoredGroupAttribute` called `updateElement` per member, and that
helper maps the entire elements array per call — 1500 spreads and 3
notifications per drag frame on a 500-clip composition. One pass now.
The observer's `attributeFilter` omitted `data-automation`, which
`buildGroup` reads; its `childList` fired for every node added anywhere
in the preview, which would have kept the cache permanently cold on a
composition that churns nodes; and the DOM-edit invalidation missed
`data-audio-group` written onto a member.
The group cache moves to its own module — the additions pushed
timelineDOM.ts past the 600-line ceiling.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
70 lines
2.7 KiB
TypeScript
70 lines
2.7 KiB
TypeScript
import type { TimelineElement } from "../store/playerStore";
|
|
import { getTimelineElementIdentity } from "../lib/timelineElementHelpers";
|
|
import type { ResizingClipState } from "./timelineClipDragTypes";
|
|
import type { GsapAnimation } from "@hyperframes/core/gsap-parser";
|
|
import { animationContributesLane } from "./TimelinePropertyLanes";
|
|
|
|
function hasKeyframedTimelineClips(
|
|
animationsByElement: ReadonlyMap<string, readonly GsapAnimation[]>,
|
|
): boolean {
|
|
return Array.from(animationsByElement.values()).some((animations) =>
|
|
animations.some(animationContributesLane),
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Does the timeline need the wide label column?
|
|
*
|
|
* Keyframed clips need it for their property-lane names. Audio GROUPS need it
|
|
* for the same reason a keyframed clip does — a row whose name has nowhere else
|
|
* to go. A track row survives a narrow gutter because its CLIPS carry the name
|
|
* on the bar; a group row has no clips at all, so in the 80px gutter its label
|
|
* rendered at zero width and its solo, FX and lane buttons were clipped off the
|
|
* side.
|
|
*
|
|
* Widening the column for the whole timeline, rather than letting just the
|
|
* group row overhang: the header is sticky and opaque, so an oversized one
|
|
* painted a slab across the rest of its own row, stayed pinned there through
|
|
* horizontal scroll, and had the playhead drawn straight through it.
|
|
*/
|
|
export function timelineNeedsLabelColumn(
|
|
animationsByElement: ReadonlyMap<string, readonly GsapAnimation[]>,
|
|
elements: readonly TimelineElement[],
|
|
): boolean {
|
|
return (
|
|
hasKeyframedTimelineClips(animationsByElement) ||
|
|
elements.some((element) => Boolean(element.audioGroup))
|
|
);
|
|
}
|
|
|
|
export function getEffectiveTimelineDuration(
|
|
duration: number,
|
|
elements: readonly TimelineElement[],
|
|
): number {
|
|
const safeDuration = Number.isFinite(duration) ? duration : 0;
|
|
if (elements.length === 0) return safeDuration;
|
|
const result = Math.max(
|
|
safeDuration,
|
|
...elements.map((element) => element.start + element.duration),
|
|
);
|
|
return Number.isFinite(result) ? result : safeDuration;
|
|
}
|
|
|
|
export function getTimelinePreviewElement(
|
|
element: TimelineElement,
|
|
resizingClip: ResizingClipState | null,
|
|
): TimelineElement {
|
|
const elementIdentity = getTimelineElementIdentity(element);
|
|
const groupPreview = resizingClip?.groupPreview?.find((change) => change.key === elementIdentity);
|
|
if (groupPreview) return { ...element, ...groupPreview };
|
|
if (resizingClip && getTimelineElementIdentity(resizingClip.element) === elementIdentity) {
|
|
return {
|
|
...element,
|
|
start: resizingClip.previewStart,
|
|
duration: resizingClip.previewDuration,
|
|
playbackStart: resizingClip.previewPlaybackStart,
|
|
};
|
|
}
|
|
return element;
|
|
}
|