mirror of
https://github.com/heygen-com/hyperframes.git
synced 2026-09-03 23:03:09 +00:00
fix(studio): resolve a panel edit through the lane groups it can see
animIdForProp matched on the parser's whole-tween propertyGroup, which is
undefined for a legacy mixed tween such as {x, opacity}. Such a tween never
matched, so an edit to either property fell through to the selection's
default animation, a different tween than the lane the user is editing.
Resolve through animationLaneGroups, the same per-keyframe helper the
rendered lanes and the reserved row heights already count groups with.
This commit is contained in:
@@ -16,7 +16,7 @@ import {
|
|||||||
} from "./propertyPanelHelpers";
|
} from "./propertyPanelHelpers";
|
||||||
import { MetricField, Section } from "./propertyPanelPrimitives";
|
import { MetricField, Section } from "./propertyPanelPrimitives";
|
||||||
import { createTransformCommitHandlers } from "./propertyPanelTransformCommit";
|
import { createTransformCommitHandlers } from "./propertyPanelTransformCommit";
|
||||||
import { classifyPropertyGroup } from "@hyperframes/core/gsap-parser";
|
import { resolveAnimIdForProperty } from "../../player/components/TimelinePropertyLanes";
|
||||||
import { resolveEditingSections } from "@hyperframes/core/editing";
|
import { resolveEditingSections } from "@hyperframes/core/editing";
|
||||||
import { MediaSection } from "./propertyPanelMediaSection";
|
import { MediaSection } from "./propertyPanelMediaSection";
|
||||||
import { ColorGradingSection } from "./propertyPanelColorGradingSection";
|
import { ColorGradingSection } from "./propertyPanelColorGradingSection";
|
||||||
@@ -241,12 +241,8 @@ export const PropertyPanel = memo(function PropertyPanel(props: PropertyPanelPro
|
|||||||
const navKeyframes = cacheEntry?.keyframes ?? gsapKeyframes;
|
const navKeyframes = cacheEntry?.keyframes ?? gsapKeyframes;
|
||||||
const seekFromKfPct = (pct: number) => onSeekToTime?.(elStart + (pct / 100) * elDuration);
|
const seekFromKfPct = (pct: number) => onSeekToTime?.(elStart + (pct / 100) * elDuration);
|
||||||
|
|
||||||
const animIdForProp = (prop: string): string => {
|
const animIdForProp = (prop: string): string =>
|
||||||
const group = classifyPropertyGroup(prop);
|
resolveAnimIdForProperty(prop, gsapAnimations, gsapAnimId);
|
||||||
const groupAnim = gsapAnimations?.find((a) => a.propertyGroup === group);
|
|
||||||
if (groupAnim) return groupAnim.id;
|
|
||||||
return gsapAnimId ?? "";
|
|
||||||
};
|
|
||||||
|
|
||||||
const displayX = gsapRuntimeValues?.x ?? manualOffset.x;
|
const displayX = gsapRuntimeValues?.x ?? manualOffset.x;
|
||||||
const displayY = gsapRuntimeValues?.y ?? manualOffset.y;
|
const displayY = gsapRuntimeValues?.y ?? manualOffset.y;
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import { afterEach, describe, expect, it, vi } from "vitest";
|
|||||||
import { TimelineClipDiamonds } from "./TimelineClipDiamonds";
|
import { TimelineClipDiamonds } from "./TimelineClipDiamonds";
|
||||||
import {
|
import {
|
||||||
getTimelinePropertyLanes,
|
getTimelinePropertyLanes,
|
||||||
|
resolveAnimIdForProperty,
|
||||||
TimelinePropertyLanes,
|
TimelinePropertyLanes,
|
||||||
type TimelinePropertyLanesProps,
|
type TimelinePropertyLanesProps,
|
||||||
} from "./TimelinePropertyLanes";
|
} from "./TimelinePropertyLanes";
|
||||||
@@ -555,3 +556,49 @@ describe("TimelinePropertyLanes", () => {
|
|||||||
act(() => root.unmount());
|
act(() => root.unmount());
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe("resolveAnimIdForProperty", () => {
|
||||||
|
/** A legacy mixed tween: the parser leaves propertyGroup undefined for it. */
|
||||||
|
const mixed = {
|
||||||
|
id: "mixed-1",
|
||||||
|
targetSelector: "#box",
|
||||||
|
method: "to",
|
||||||
|
position: 0,
|
||||||
|
properties: {},
|
||||||
|
keyframes: {
|
||||||
|
keyframes: [
|
||||||
|
{ percentage: 0, properties: { x: 0, opacity: 0 } },
|
||||||
|
{ percentage: 100, properties: { x: 40, opacity: 1 } },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
} as unknown as GsapAnimation;
|
||||||
|
|
||||||
|
it("routes both groups of a mixed tween to that tween, not the fallback", () => {
|
||||||
|
expect(mixed.propertyGroup).toBeUndefined();
|
||||||
|
|
||||||
|
expect(resolveAnimIdForProperty("x", [mixed], "fallback")).toBe("mixed-1");
|
||||||
|
expect(resolveAnimIdForProperty("opacity", [mixed], "fallback")).toBe("mixed-1");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("falls back only when no tween animates the property's group", () => {
|
||||||
|
expect(resolveAnimIdForProperty("rotation", [mixed], "fallback")).toBe("fallback");
|
||||||
|
expect(resolveAnimIdForProperty("rotation", [mixed], undefined)).toBe("");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("prefers a single-group tween that owns the lane", () => {
|
||||||
|
const opacityOnly = {
|
||||||
|
...mixed,
|
||||||
|
id: "opacity-1",
|
||||||
|
propertyGroup: "visual",
|
||||||
|
keyframes: {
|
||||||
|
keyframes: [
|
||||||
|
{ percentage: 0, properties: { opacity: 0 } },
|
||||||
|
{ percentage: 100, properties: { opacity: 1 } },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
} as unknown as GsapAnimation;
|
||||||
|
|
||||||
|
expect(resolveAnimIdForProperty("opacity", [opacityOnly, mixed], "fallback")).toBe("opacity-1");
|
||||||
|
expect(resolveAnimIdForProperty("x", [opacityOnly, mixed], "fallback")).toBe("mixed-1");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|||||||
@@ -77,6 +77,28 @@ export function animationLaneGroups(animation: GsapAnimation): PropertyGroupName
|
|||||||
return Array.from(groups);
|
return Array.from(groups);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Which tween a panel edit to `prop` belongs to.
|
||||||
|
*
|
||||||
|
* Matches on the groups the tween's KEYFRAMES animate, not on the parser's
|
||||||
|
* whole-tween `propertyGroup` verdict: that field is undefined for a legacy
|
||||||
|
* mixed tween such as `{ x, opacity }`, so matching it dropped every such
|
||||||
|
* tween and sent the edit to the selection's default animation instead, which
|
||||||
|
* is a different tween than the lane the user is looking at.
|
||||||
|
* {@link animationLaneGroups} is the single owner the rendered lanes count
|
||||||
|
* groups through, so resolving here through the same helper keeps the panel
|
||||||
|
* and the lanes on one answer.
|
||||||
|
*/
|
||||||
|
export function resolveAnimIdForProperty(
|
||||||
|
prop: string,
|
||||||
|
animations: readonly GsapAnimation[] | undefined,
|
||||||
|
fallbackAnimId: string | undefined,
|
||||||
|
): string {
|
||||||
|
const group = classifyPropertyGroup(prop);
|
||||||
|
const groupAnim = animations?.find((a) => animationLaneGroups(a).includes(group));
|
||||||
|
return groupAnim?.id ?? fallbackAnimId ?? "";
|
||||||
|
}
|
||||||
|
|
||||||
/** A tween contributes a property lane when it animates at least one property
|
/** A tween contributes a property lane when it animates at least one property
|
||||||
* on at least one editable keyframe (real or synthesized). */
|
* on at least one editable keyframe (real or synthesized). */
|
||||||
export function animationContributesLane(animation: GsapAnimation): boolean {
|
export function animationContributesLane(animation: GsapAnimation): boolean {
|
||||||
|
|||||||
Reference in New Issue
Block a user