mirror of
https://github.com/heygen-com/hyperframes.git
synced 2026-09-03 04:38:33 +00:00
fix(studio): keep useGsapTweenCache under the size cap + correct non-sticky drill test (#1770)
- extract deduplicateKeyframes + synthesizeFlatTweenKeyframes into gsapTweenSynth.ts so useGsapTweenCache.ts drops from 628 to 573 lines (600-line cap) - the drilled-into-group selection test asserted the old sticky behavior; the code is intentionally non-sticky (clicking outside exits the group and resolves the clicked element), so the test now asserts that
This commit is contained in:
@@ -154,7 +154,7 @@ describe("resolveDomEditSelection — data-hf-group capture", () => {
|
||||
expect(scoped).not.toContain("outside");
|
||||
});
|
||||
|
||||
it("returns null when clicking outside the group the user is drilled into", async () => {
|
||||
it("exits the drilled-into group and selects the outside element (non-sticky drill)", async () => {
|
||||
const { parent, inner } = buildNestedGroups();
|
||||
const outside = document.createElement("div");
|
||||
outside.id = "outside";
|
||||
@@ -167,6 +167,8 @@ describe("resolveDomEditSelection — data-hf-group capture", () => {
|
||||
document.body.removeChild(parent);
|
||||
document.body.removeChild(outside);
|
||||
|
||||
expect(selection).toBeNull();
|
||||
// Drill-in is non-sticky: clicking outside the active group exits it and
|
||||
// resolves the clicked element normally (rather than selecting nothing).
|
||||
expect(selection?.id).toBe("outside");
|
||||
});
|
||||
});
|
||||
|
||||
@@ -0,0 +1,64 @@
|
||||
import type {
|
||||
GsapAnimation,
|
||||
GsapKeyframesData,
|
||||
GsapPercentageKeyframe,
|
||||
} from "@hyperframes/core/gsap-parser";
|
||||
import { PROPERTY_DEFAULTS } from "./gsapShared";
|
||||
|
||||
export function deduplicateKeyframes(
|
||||
keyframes: GsapPercentageKeyframe[],
|
||||
): GsapPercentageKeyframe[] {
|
||||
const byPct = new Map<number, GsapPercentageKeyframe>();
|
||||
for (const kf of keyframes) {
|
||||
const existing = byPct.get(kf.percentage);
|
||||
if (existing) {
|
||||
existing.properties = { ...existing.properties, ...kf.properties };
|
||||
if (kf.ease) existing.ease = kf.ease;
|
||||
} else {
|
||||
byPct.set(kf.percentage, { ...kf, properties: { ...kf.properties } });
|
||||
}
|
||||
}
|
||||
return Array.from(byPct.values()).sort((a, b) => a.percentage - b.percentage);
|
||||
}
|
||||
|
||||
// fallow-ignore-next-line complexity
|
||||
export function synthesizeFlatTweenKeyframes(anim: GsapAnimation): GsapKeyframesData | null {
|
||||
if (anim.method === "set") {
|
||||
// A `set` is a STATIC HOLD — a value applied at one point, not an animated
|
||||
// keyframe. It must NOT synthesize a keyframe, or the timeline + panel show a
|
||||
// phantom diamond for a value that doesn't animate. This holds for a base
|
||||
// `gsap.set` (off-timeline) AND an on-timeline `tl.set`, and aligns the AST
|
||||
// path with the runtime scan, which already skips every zero-duration set.
|
||||
return null;
|
||||
}
|
||||
const toProps = anim.properties;
|
||||
const fromProps = anim.fromProperties;
|
||||
if (!toProps || Object.keys(toProps).length === 0) return null;
|
||||
|
||||
const startProps: Record<string, number | string> = {};
|
||||
const endProps: Record<string, number | string> = {};
|
||||
|
||||
if (anim.method === "from") {
|
||||
for (const [k, v] of Object.entries(toProps)) {
|
||||
startProps[k] = v;
|
||||
endProps[k] = PROPERTY_DEFAULTS[k] ?? 0;
|
||||
}
|
||||
} else if (anim.method === "fromTo" && fromProps) {
|
||||
Object.assign(startProps, fromProps);
|
||||
Object.assign(endProps, toProps);
|
||||
} else {
|
||||
for (const [k, v] of Object.entries(toProps)) {
|
||||
startProps[k] = PROPERTY_DEFAULTS[k] ?? 0;
|
||||
endProps[k] = v;
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
format: "percentage",
|
||||
keyframes: [
|
||||
{ percentage: 0, properties: startProps },
|
||||
{ percentage: 100, properties: endProps },
|
||||
],
|
||||
...(anim.ease ? { ease: anim.ease } : {}),
|
||||
};
|
||||
}
|
||||
@@ -1,6 +1,5 @@
|
||||
import { useEffect, useMemo, useRef, useState, useCallback } from "react";
|
||||
import type { GsapAnimation, GsapKeyframesData, ParsedGsap } from "@hyperframes/core/gsap-parser";
|
||||
import type { GsapPercentageKeyframe } from "@hyperframes/core/gsap-parser";
|
||||
import { isStudioHoldSet } from "@hyperframes/core/gsap-parser";
|
||||
import { usePlayerStore } from "../player/store/playerStore";
|
||||
import { readRuntimeKeyframes, scanAllRuntimeKeyframes } from "./gsapRuntimeBridge";
|
||||
@@ -8,63 +7,8 @@ import {
|
||||
clearKeyframeCacheForElement,
|
||||
clearKeyframeCacheForFile,
|
||||
} from "./gsapKeyframeCacheHelpers";
|
||||
import { PROPERTY_DEFAULTS, toAbsoluteTime } from "./gsapShared";
|
||||
|
||||
function deduplicateKeyframes(keyframes: GsapPercentageKeyframe[]): GsapPercentageKeyframe[] {
|
||||
const byPct = new Map<number, GsapPercentageKeyframe>();
|
||||
for (const kf of keyframes) {
|
||||
const existing = byPct.get(kf.percentage);
|
||||
if (existing) {
|
||||
existing.properties = { ...existing.properties, ...kf.properties };
|
||||
if (kf.ease) existing.ease = kf.ease;
|
||||
} else {
|
||||
byPct.set(kf.percentage, { ...kf, properties: { ...kf.properties } });
|
||||
}
|
||||
}
|
||||
return Array.from(byPct.values()).sort((a, b) => a.percentage - b.percentage);
|
||||
}
|
||||
|
||||
// fallow-ignore-next-line complexity
|
||||
function synthesizeFlatTweenKeyframes(anim: GsapAnimation): GsapKeyframesData | null {
|
||||
if (anim.method === "set") {
|
||||
// A `set` is a STATIC HOLD — a value applied at one point, not an animated
|
||||
// keyframe. It must NOT synthesize a keyframe, or the timeline + panel show a
|
||||
// phantom diamond for a value that doesn't animate. This holds for a base
|
||||
// `gsap.set` (off-timeline) AND an on-timeline `tl.set`, and aligns the AST
|
||||
// path with the runtime scan, which already skips every zero-duration set.
|
||||
return null;
|
||||
}
|
||||
const toProps = anim.properties;
|
||||
const fromProps = anim.fromProperties;
|
||||
if (!toProps || Object.keys(toProps).length === 0) return null;
|
||||
|
||||
const startProps: Record<string, number | string> = {};
|
||||
const endProps: Record<string, number | string> = {};
|
||||
|
||||
if (anim.method === "from") {
|
||||
for (const [k, v] of Object.entries(toProps)) {
|
||||
startProps[k] = v;
|
||||
endProps[k] = PROPERTY_DEFAULTS[k] ?? 0;
|
||||
}
|
||||
} else if (anim.method === "fromTo" && fromProps) {
|
||||
Object.assign(startProps, fromProps);
|
||||
Object.assign(endProps, toProps);
|
||||
} else {
|
||||
for (const [k, v] of Object.entries(toProps)) {
|
||||
startProps[k] = PROPERTY_DEFAULTS[k] ?? 0;
|
||||
endProps[k] = v;
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
format: "percentage",
|
||||
keyframes: [
|
||||
{ percentage: 0, properties: startProps },
|
||||
{ percentage: 100, properties: endProps },
|
||||
],
|
||||
...(anim.ease ? { ease: anim.ease } : {}),
|
||||
};
|
||||
}
|
||||
import { toAbsoluteTime } from "./gsapShared";
|
||||
import { deduplicateKeyframes, synthesizeFlatTweenKeyframes } from "./gsapTweenSynth";
|
||||
|
||||
function extractIdFromSelector(selector: string): string | null {
|
||||
const match = selector.match(/^#([\w-]+)/);
|
||||
|
||||
Reference in New Issue
Block a user