mirror of
https://github.com/heygen-com/hyperframes.git
synced 2026-09-03 04:38:33 +00:00
fix(studio): keep sub-compositions expanded at the end of the timeline
Auto-expansion picks the composition whose clip window contains the playhead, and those windows are half-open. With the playhead parked on the last clip's end - where playback stops - it sat inside nothing, so every expanded sub-composition row and its keyframe lanes collapsed to a single host row. Accept the closing boundary, but only when the strict pass matched nothing, so a playhead on the seam between two adjacent clips still expands the one that is starting.
This commit is contained in:
@@ -278,6 +278,49 @@ describe("resolveTimelineExpansionRawId", () => {
|
||||
).toBe("scene");
|
||||
});
|
||||
|
||||
it("THE BUG: keeps a composition expanded with the playhead parked on its end", () => {
|
||||
// Clip windows are half-open, so at the very end of the timeline the
|
||||
// playhead was inside nothing and every expanded row collapsed.
|
||||
const manifest = [
|
||||
clip({ id: "scene", start: 0, duration: 12 }),
|
||||
clip({ id: "headline", start: 0, duration: 12 }),
|
||||
];
|
||||
const parentMap = new Map([["headline", "scene"]]);
|
||||
|
||||
expect(
|
||||
resolveTimelineExpansionRawId({
|
||||
selectedElementId: null,
|
||||
isPlaying: false,
|
||||
currentTime: 12,
|
||||
manifest,
|
||||
parentMap,
|
||||
}),
|
||||
).toBe("scene");
|
||||
});
|
||||
|
||||
it("prefers the starting clip over the ending one on a shared seam", () => {
|
||||
const manifest = [
|
||||
clip({ id: "first", start: 0, duration: 5 }),
|
||||
clip({ id: "first-child", start: 0, duration: 5 }),
|
||||
clip({ id: "second", start: 5, duration: 5 }),
|
||||
clip({ id: "second-child", start: 5, duration: 5 }),
|
||||
];
|
||||
const parentMap = new Map([
|
||||
["first-child", "first"],
|
||||
["second-child", "second"],
|
||||
]);
|
||||
|
||||
expect(
|
||||
resolveTimelineExpansionRawId({
|
||||
selectedElementId: null,
|
||||
isPlaying: false,
|
||||
currentTime: 5,
|
||||
manifest,
|
||||
parentMap,
|
||||
}),
|
||||
).toBe("second");
|
||||
});
|
||||
|
||||
it("auto-expands the innermost active nested composition when paused", () => {
|
||||
const manifest = [
|
||||
clip({ id: "outer", start: 0, duration: 10 }),
|
||||
|
||||
@@ -45,6 +45,11 @@ function clipContainsTime(clip: ClipManifestClip, time: number): boolean {
|
||||
return Number.isFinite(time) && time >= clip.start && time < clip.start + clip.duration;
|
||||
}
|
||||
|
||||
/** Half-open containment plus the closing boundary — see the fallback below. */
|
||||
function clipTouchesTime(clip: ClipManifestClip, time: number): boolean {
|
||||
return Number.isFinite(time) && time >= clip.start && time <= clip.start + clip.duration;
|
||||
}
|
||||
|
||||
function getActiveParentDepth(id: string, parentMap: Map<string, string>, activeIds: Set<string>) {
|
||||
let depth = 0;
|
||||
let parent = parentMap.get(id);
|
||||
@@ -65,11 +70,20 @@ function findActiveExpandableCompositionId(
|
||||
parentMap: Map<string, string>,
|
||||
): string | null {
|
||||
const parentIds = new Set(parentMap.values());
|
||||
const activeIds = new Set<string>();
|
||||
for (const clip of manifest) {
|
||||
if (!clip.id || !parentIds.has(clip.id) || !clipContainsTime(clip, currentTime)) continue;
|
||||
activeIds.add(clip.id);
|
||||
}
|
||||
const collect = (matches: (clip: ClipManifestClip, time: number) => boolean) => {
|
||||
const ids = new Set<string>();
|
||||
for (const clip of manifest) {
|
||||
if (clip.id && parentIds.has(clip.id) && matches(clip, currentTime)) ids.add(clip.id);
|
||||
}
|
||||
return ids;
|
||||
};
|
||||
// Clip windows are half-open, so a playhead parked exactly on a clip's end is
|
||||
// inside nothing — at the end of the timeline that collapsed every expanded
|
||||
// sub-composition row and its keyframe lanes. Only when the strict pass finds
|
||||
// nothing do we accept the closing boundary, so a playhead landing on the seam
|
||||
// between two adjacent clips still expands the one that is starting.
|
||||
const strict = collect(clipContainsTime);
|
||||
const activeIds = strict.size > 0 ? strict : collect(clipTouchesTime);
|
||||
let bestId: string | null = null;
|
||||
let bestDepth = -1;
|
||||
for (const id of activeIds) {
|
||||
|
||||
Reference in New Issue
Block a user