mirror of
https://github.com/heygen-com/hyperframes.git
synced 2026-09-07 18:26:17 +00:00
Selector reads now go through one inverse of `idSelector`. Every writer emits `[id="01-hook-hero"]` for an id a `#id` selector can't address, but the readers still matched `#id` only, so the post-commit keyframe-cache refresh, the AST load and the remove-all-keyframes clear all silently skipped exactly the ids `idSelector` was added to support. A keyframe merged from two tweens with different eases kept whichever ease iterated last. Readers that don't check `easeAmbiguous` showed a curve from a different animation than an edit would target, so the ambiguous flag now clears `ease` instead of leaving an arbitrary one behind. One tolerance for "the playhead is on this keyframe". The motion-path drag used 0.05% while the toolbar and the playhead apply used 1%, so a drag that landed a fraction of a percent off an authored waypoint skipped the update-point branch and appended a near-duplicate. `buildTemporalArcKeyframes` now owns the invariant and replaces any keyframe inside the tolerance, rather than trusting each caller's own pre-check. The pending-retime bookkeeping matches on keyframe identity, not just on "something is near that percentage" — an evenly spaced row cleared the entry off an unrelated sibling. The neighbour clamp composes pending destinations in before sorting, so a second drag can't cross a neighbour that already moved. Also: `keyframeCache`/`gsapAnimations` setters return the same state for a write that changes nothing (every no-op re-rendered every subscriber), the auto-expand set drops clips that left the source so an undo/paste under the same id expands again, `invalidateGsapCache` has a stable identity instead of re-creating the whole timeline edit context each render, the studio test hook deletes its window key rather than leaving it enumerable as undefined, and the past-last-row extrapolation documents why it uses TRACK_H where the pre-first-row branch uses row 0's own height. Covers `idFromSelector` round-trips, the insert boundary band across plain, expanded and unusable row heights, and the collapsed selection key for a colon-bearing element id.
50 lines
1.8 KiB
TypeScript
50 lines
1.8 KiB
TypeScript
import { describe, expect, it } from "vitest";
|
|
import {
|
|
timelineKeyframeSelectionKey,
|
|
timelineKeyframeTargetFromSelectionKey,
|
|
} from "./timelineKeyframeIdentity";
|
|
|
|
describe("timeline keyframe selection identity", () => {
|
|
it("round-trips an expanded lane with colon-bearing identities", () => {
|
|
const key = timelineKeyframeSelectionKey("comp#a:child", {
|
|
percentage: 75,
|
|
tweenPercentage: 40,
|
|
propertyGroup: "position",
|
|
animationId: "child:position",
|
|
});
|
|
|
|
expect(timelineKeyframeTargetFromSelectionKey("comp#a:child", key)).toEqual({
|
|
percentage: 75,
|
|
tweenPercentage: 40,
|
|
propertyGroup: "position",
|
|
animationId: "child:position",
|
|
});
|
|
});
|
|
|
|
it("does not confuse an expanded lane whose element id extends the active id", () => {
|
|
const key = timelineKeyframeSelectionKey("comp#a:child", {
|
|
percentage: 75,
|
|
tweenPercentage: 40,
|
|
propertyGroup: "position",
|
|
animationId: "child-position",
|
|
});
|
|
|
|
expect(timelineKeyframeTargetFromSelectionKey("comp#a", key)).toBeNull();
|
|
});
|
|
|
|
// Timeline.tsx still writes the collapsed form for clip-lane shift-clicks, and
|
|
// an element id can itself contain a colon — the split has to be the LAST one.
|
|
it("splits the collapsed key at the last colon so a colon-bearing id survives", () => {
|
|
expect(timelineKeyframeTargetFromSelectionKey("a:b", "a:b:40")).toEqual({ percentage: 40 });
|
|
expect(timelineKeyframeTargetFromSelectionKey("a", "a:b:40")).toBeNull();
|
|
});
|
|
|
|
it("retains the collapsed key fallback and rejects malformed percentages", () => {
|
|
expect(timelineKeyframeTargetFromSelectionKey("comp#a", "comp#a:30")).toEqual({
|
|
percentage: 30,
|
|
});
|
|
expect(timelineKeyframeTargetFromSelectionKey("comp#a", "comp#a:NaN")).toBeNull();
|
|
expect(timelineKeyframeTargetFromSelectionKey("comp#a", "comp#b:30")).toBeNull();
|
|
});
|
|
});
|