mirror of
https://github.com/heygen-com/hyperframes.git
synced 2026-09-04 07:19:52 +00:00
fix(studio): apply split-bounds epsilon in razor split-all (#1404)
The single-clip razor path guards splits with isSplitTimeWithinBounds, which keeps a SPLIT_BOUNDARY_EPSILON_S margin from each clip edge so a cut never produces a degenerate near-zero slice. The split-all path filtered with raw `splitTime > start && splitTime < end` instead, so it accepted cuts inside that margin (and on clips shorter than two epsilons that the single path always rejects), producing the very degenerate slice the epsilon exists to prevent. Extract the shared predicate canSplitElementAt and a selectSplittableElements helper, and route both razor paths through them so the two stay consistent. Adds unit coverage for the new helpers, including the regression where a sub-epsilon clip with an interior split time must not be selected. Co-authored-by: Carlos Alcaraz <193642530+calcarazgre646@users.noreply.github.com>
This commit is contained in:
co-authored by
Carlos Alcaraz
parent
a4ae3c92a0
commit
66dde0898b
@@ -1,5 +1,22 @@
|
||||
import { describe, it, expect } from "vitest";
|
||||
import { SPLIT_BOUNDARY_EPSILON_S, isSplitTimeWithinBounds } from "./timelineElementSplit";
|
||||
import type { TimelineElement } from "../player/store/playerStore";
|
||||
import {
|
||||
SPLIT_BOUNDARY_EPSILON_S,
|
||||
canSplitElementAt,
|
||||
isSplitTimeWithinBounds,
|
||||
selectSplittableElements,
|
||||
} from "./timelineElementSplit";
|
||||
|
||||
function element(overrides: Partial<TimelineElement> = {}): TimelineElement {
|
||||
return {
|
||||
id: "el-1",
|
||||
tag: "div",
|
||||
start: 1,
|
||||
duration: 4,
|
||||
track: 0,
|
||||
...overrides,
|
||||
};
|
||||
}
|
||||
|
||||
describe("isSplitTimeWithinBounds", () => {
|
||||
const start = 1;
|
||||
@@ -48,3 +65,46 @@ describe("isSplitTimeWithinBounds", () => {
|
||||
expect(isSplitTimeWithinBounds(start + shortDuration / 2, start, shortDuration)).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe("canSplitElementAt", () => {
|
||||
it("accepts a splittable element at an interior time", () => {
|
||||
expect(canSplitElementAt(element({ start: 1, duration: 4 }), 3)).toBe(true);
|
||||
});
|
||||
|
||||
it("rejects a time inside the boundary epsilon", () => {
|
||||
expect(
|
||||
canSplitElementAt(element({ start: 1, duration: 4 }), 1 + SPLIT_BOUNDARY_EPSILON_S / 2),
|
||||
).toBe(false);
|
||||
});
|
||||
|
||||
it("rejects locked, implicit and sub-composition elements", () => {
|
||||
expect(canSplitElementAt(element({ timelineLocked: true }), 3)).toBe(false);
|
||||
expect(canSplitElementAt(element({ timingSource: "implicit" }), 3)).toBe(false);
|
||||
expect(canSplitElementAt(element({ compositionSrc: "child.html" }), 3)).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe("selectSplittableElements", () => {
|
||||
it("excludes a clip shorter than two epsilons even when the time is inside it", () => {
|
||||
// Regression: split-all used raw start < t < end, so a clip too short for
|
||||
// the epsilon margin was still selected and produced a degenerate slice.
|
||||
const tiny = element({ id: "tiny", start: 1, duration: SPLIT_BOUNDARY_EPSILON_S + 0.01 });
|
||||
const interiorTime = tiny.start + tiny.duration / 2;
|
||||
expect(interiorTime).toBeGreaterThan(tiny.start);
|
||||
expect(interiorTime).toBeLessThan(tiny.start + tiny.duration);
|
||||
expect(selectSplittableElements([tiny], interiorTime)).toEqual([]);
|
||||
});
|
||||
|
||||
it("keeps only the elements whose epsilon-bounded range contains the time", () => {
|
||||
const inside = element({ id: "inside", start: 0, duration: 4 });
|
||||
const outside = element({ id: "outside", start: 5, duration: 4 });
|
||||
const locked = element({ id: "locked", start: 0, duration: 4, timelineLocked: true });
|
||||
const result = selectSplittableElements([inside, outside, locked], 2);
|
||||
expect(result.map((el) => el.id)).toEqual(["inside"]);
|
||||
});
|
||||
|
||||
it("accepts an element at the exact lower clamp boundary", () => {
|
||||
const el = element({ start: 2, duration: 4 });
|
||||
expect(selectSplittableElements([el], 2 + SPLIT_BOUNDARY_EPSILON_S)).toEqual([el]);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -30,3 +30,21 @@ export function canSplitElement(el: TimelineElement): boolean {
|
||||
Number.isFinite(el.duration)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* True when `el` can be split AND `splitTime` lies within its boundary epsilon.
|
||||
* Shared by the single-clip and split-all razor paths so both honor the same
|
||||
* minimum-distance rule (split-all previously used raw `>`/`<`, letting cuts
|
||||
* land inside the epsilon margin and produce a degenerate slice).
|
||||
*/
|
||||
export function canSplitElementAt(el: TimelineElement, splitTime: number): boolean {
|
||||
return canSplitElement(el) && isSplitTimeWithinBounds(splitTime, el.start, el.duration);
|
||||
}
|
||||
|
||||
/** Elements that the split-all razor action can cut at `splitTime`. */
|
||||
export function selectSplittableElements(
|
||||
elements: TimelineElement[],
|
||||
splitTime: number,
|
||||
): TimelineElement[] {
|
||||
return elements.filter((el) => canSplitElementAt(el, splitTime));
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user