From c435a4ee46adf0ce8cd6c0dbae876bfc0a171010 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miguel=20=C3=81ngel?= Date: Fri, 12 Jun 2026 00:19:31 -0400 Subject: [PATCH] fix(studio): property panel group-aware keyframe routing (#1358) * fix(core): per-property-group keyframe foundations Add PropertyGroupName type system (position/scale/size/rotation/visual/other), PROPERTY_GROUPS constant, classifyPropertyGroup/classifyTweenPropertyGroup functions. Parser generates group-aware animation IDs, resolves position strings (+=, -=, <, >), uses numeric matching with 2% tolerance, and preserves IDs across all mutations. * fix(core): add split-into-property-groups and replace-with-keyframes mutations Server-side mutations for atomic property-group splitting and keyframe replacement. Client commitMutation returns early on changed:false instead of throwing. * fix(studio): per-property-group intercept routing + drag/resize fixes Rewire GSAP runtime bridge for property-group routing: drag sends only {x,y} to position group, resize routes to scale group via data-hf-studio-original-width, rotation routes to rotation group. Add resolveGroupTween helper, from-extend with split-first-then-position-only pattern, autoKeyframeEnabled guards, GSAP base + delta fix in drag draft, cancel-restores-GSAP-x/y from data attrs. * fix(studio): keyframe cache propertyGroup tagging + timeline UI fixes Tag cached keyframes with propertyGroup for group-aware operations. Add tweenPercentage for accurate keyframe matching, activeKeyframePct for diamond-click targeting, context menu offset, selected diamond z-index, clearProps after kill in soft reload. * fix(studio): property panel group-aware keyframe routing Add animIdForProp helper routing keyframe diamonds to correct property-group animation. Wire StudioPreviewArea delete/move/toggle handlers to use propertyGroup for routing. Fix per-property epsilon in rdpSimplify. --- .../src/components/StudioPreviewArea.tsx | 33 ++++++++++++++----- .../src/components/editor/PropertyPanel.tsx | 29 ++++++++++------ .../editor/propertyPanel3dTransform.tsx | 23 ++++++++++--- packages/studio/src/utils/rdpSimplify.ts | 5 +-- 4 files changed, 65 insertions(+), 25 deletions(-) diff --git a/packages/studio/src/components/StudioPreviewArea.tsx b/packages/studio/src/components/StudioPreviewArea.tsx index 76dc47c96..f3f68b4ac 100644 --- a/packages/studio/src/components/StudioPreviewArea.tsx +++ b/packages/studio/src/components/StudioPreviewArea.tsx @@ -154,25 +154,40 @@ export function StudioPreviewArea({ onRazorSplitAll={handleRazorSplitAll} onSelectTimelineElement={handleTimelineElementSelect} onDeleteAllKeyframes={(_elId) => { - const anim = - selectedGsapAnimations.find((a) => a.keyframes) ?? selectedGsapAnimations[0]; - if (anim) handleGsapDeleteAnimation(anim.id); + for (const anim of selectedGsapAnimations) { + handleGsapDeleteAnimation(anim.id); + } }} onDeleteKeyframe={(_elId, pct) => { - const anim = selectedGsapAnimations.find((a) => a.keyframes); - if (anim) handleGsapRemoveKeyframe(anim.id, pct); + const cacheKey = domEditSelection?.id ?? ""; + const cached = usePlayerStore.getState().keyframeCache.get(cacheKey); + const kf = cached?.keyframes.find((k) => Math.abs(k.percentage - pct) < 0.2); + const group = kf?.propertyGroup; + const anim = + (group ? selectedGsapAnimations.find((a) => a.propertyGroup === group) : undefined) ?? + selectedGsapAnimations.find((a) => a.keyframes); + if (!anim) return; + handleGsapRemoveKeyframe(anim.id, kf?.tweenPercentage ?? pct); }} onChangeKeyframeEase={(_elId, _pct, ease) => { - const anim = selectedGsapAnimations.find((a) => a.keyframes); - if (anim) handleGsapUpdateMeta(anim.id, { ease }); + for (const anim of selectedGsapAnimations) { + if (anim.keyframes) handleGsapUpdateMeta(anim.id, { ease }); + } }} // fallow-ignore-next-line complexity onMoveKeyframe={(_el, oldPct, newPct) => { - const anim = selectedGsapAnimations.find((a) => a.keyframes); + const cacheKey = domEditSelection?.id ?? ""; + const cached = usePlayerStore.getState().keyframeCache.get(cacheKey); + const cachedKf = cached?.keyframes.find((k) => Math.abs(k.percentage - oldPct) < 0.2); + const group = cachedKf?.propertyGroup; + const anim = + (group ? selectedGsapAnimations.find((a) => a.propertyGroup === group) : undefined) ?? + selectedGsapAnimations.find((a) => a.keyframes); if (!anim?.keyframes) return; + const tweenOldPct = cachedKf?.tweenPercentage ?? oldPct; const kf = anim.keyframes.keyframes.find((k) => k.percentage === oldPct); if (!kf) return; - handleGsapRemoveKeyframe(anim.id, oldPct); + handleGsapRemoveKeyframe(anim.id, tweenOldPct); for (const [prop, val] of Object.entries(kf.properties)) { handleGsapAddKeyframe(anim.id, newPct, prop, val); } diff --git a/packages/studio/src/components/editor/PropertyPanel.tsx b/packages/studio/src/components/editor/PropertyPanel.tsx index 2cadbd2d8..05946d3c0 100644 --- a/packages/studio/src/components/editor/PropertyPanel.tsx +++ b/packages/studio/src/components/editor/PropertyPanel.tsx @@ -11,6 +11,7 @@ import { readGsapBorderRadiusForPanel, } from "./propertyPanelHelpers"; import { MetricField, Section } from "./propertyPanelPrimitives"; +import { classifyPropertyGroup } from "@hyperframes/core/gsap-parser"; import { isMediaElement, MediaSection } from "./propertyPanelMediaSection"; import { TextSection, StyleSections } from "./propertyPanelSections"; import { GsapAnimationSection } from "./GsapAnimationSection"; @@ -227,6 +228,13 @@ export const PropertyPanel = memo(function PropertyPanel({ const navKeyframes = cacheEntry?.keyframes ?? gsapKeyframes; const seekFromKfPct = (pct: number) => onSeekToTime?.(elStart + (pct / 100) * elDuration); + const animIdForProp = (prop: string): string => { + const group = classifyPropertyGroup(prop); + const groupAnim = gsapAnimations?.find((a) => a.propertyGroup === group); + if (groupAnim) return groupAnim.id; + return gsapAnimId ?? ""; + }; + // Read ALL GSAP-interpolated values at the current seek time. const gsapRuntimeValues = readGsapRuntimeValuesForPanel( gsapAnimId, @@ -395,8 +403,8 @@ export const PropertyPanel = memo(function PropertyPanel({ onCommitAnimatedProperty && void onCommitAnimatedProperty(element, "x", displayX) } - onRemoveKeyframe={(pct) => onRemoveKeyframe?.(gsapAnimId, pct)} - onConvertToKeyframes={() => onConvertToKeyframes?.(gsapAnimId)} + onRemoveKeyframe={(pct) => onRemoveKeyframe?.(animIdForProp("x"), pct)} + onConvertToKeyframes={() => onConvertToKeyframes?.(animIdForProp("x"))} /> )} @@ -420,8 +428,8 @@ export const PropertyPanel = memo(function PropertyPanel({ onCommitAnimatedProperty && void onCommitAnimatedProperty(element, "y", displayY) } - onRemoveKeyframe={(pct) => onRemoveKeyframe?.(gsapAnimId, pct)} - onConvertToKeyframes={() => onConvertToKeyframes?.(gsapAnimId)} + onRemoveKeyframe={(pct) => onRemoveKeyframe?.(animIdForProp("y"), pct)} + onConvertToKeyframes={() => onConvertToKeyframes?.(animIdForProp("y"))} /> )} @@ -445,8 +453,8 @@ export const PropertyPanel = memo(function PropertyPanel({ onCommitAnimatedProperty && void onCommitAnimatedProperty(element, "width", displayW) } - onRemoveKeyframe={(pct) => onRemoveKeyframe?.(gsapAnimId, pct)} - onConvertToKeyframes={() => onConvertToKeyframes?.(gsapAnimId)} + onRemoveKeyframe={(pct) => onRemoveKeyframe?.(animIdForProp("width"), pct)} + onConvertToKeyframes={() => onConvertToKeyframes?.(animIdForProp("width"))} /> )} @@ -470,8 +478,8 @@ export const PropertyPanel = memo(function PropertyPanel({ onCommitAnimatedProperty && void onCommitAnimatedProperty(element, "height", displayH) } - onRemoveKeyframe={(pct) => onRemoveKeyframe?.(gsapAnimId, pct)} - onConvertToKeyframes={() => onConvertToKeyframes?.(gsapAnimId)} + onRemoveKeyframe={(pct) => onRemoveKeyframe?.(animIdForProp("height"), pct)} + onConvertToKeyframes={() => onConvertToKeyframes?.(animIdForProp("height"))} /> )} @@ -493,8 +501,8 @@ export const PropertyPanel = memo(function PropertyPanel({ onCommitAnimatedProperty && void onCommitAnimatedProperty(element, "rotation", displayR) } - onRemoveKeyframe={(pct) => onRemoveKeyframe?.(gsapAnimId, pct)} - onConvertToKeyframes={() => onConvertToKeyframes?.(gsapAnimId)} + onRemoveKeyframe={(pct) => onRemoveKeyframe?.(animIdForProp("rotation"), pct)} + onConvertToKeyframes={() => onConvertToKeyframes?.(animIdForProp("rotation"))} /> )} @@ -503,6 +511,7 @@ export const PropertyPanel = memo(function PropertyPanel({ ; gsapAnimId: string | null; + resolveAnimIdForProp?: (prop: string) => string | null; gsapKeyframes: KeyframeEntry; currentPct: number; elStart: number; @@ -31,6 +32,7 @@ interface PropertyPanel3dTransformProps { export function PropertyPanel3dTransform({ gsapRuntimeValues, gsapAnimId, + resolveAnimIdForProp, gsapKeyframes, currentPct, elStart, @@ -41,6 +43,7 @@ export function PropertyPanel3dTransform({ onRemoveKeyframe, onConvertToKeyframes, }: PropertyPanel3dTransformProps) { + const idFor = (prop: string) => resolveAnimIdForProp?.(prop) ?? gsapAnimId; return (
@@ -72,8 +75,14 @@ export function PropertyPanel3dTransform({ void onCommitAnimatedProperty(element, "z", gsapRuntimeValues?.z ?? 0); } }} - onRemoveKeyframe={(pct) => gsapAnimId && onRemoveKeyframe?.(gsapAnimId, pct)} - onConvertToKeyframes={() => gsapAnimId && onConvertToKeyframes?.(gsapAnimId)} + onRemoveKeyframe={(pct) => { + const id = idFor("z"); + if (id) onRemoveKeyframe?.(id, pct); + }} + onConvertToKeyframes={() => { + const id = idFor("z"); + if (id) onConvertToKeyframes?.(id); + }} /> )}
@@ -102,8 +111,14 @@ export function PropertyPanel3dTransform({ void onCommitAnimatedProperty(element, "scale", gsapRuntimeValues?.scale ?? 1); } }} - onRemoveKeyframe={(pct) => gsapAnimId && onRemoveKeyframe?.(gsapAnimId, pct)} - onConvertToKeyframes={() => gsapAnimId && onConvertToKeyframes?.(gsapAnimId)} + onRemoveKeyframe={(pct) => { + const id = idFor("scale"); + if (id) onRemoveKeyframe?.(id, pct); + }} + onConvertToKeyframes={() => { + const id = idFor("scale"); + if (id) onConvertToKeyframes?.(id); + }} /> )}
diff --git a/packages/studio/src/utils/rdpSimplify.ts b/packages/studio/src/utils/rdpSimplify.ts index f2d4da9e1..dd65dfd11 100644 --- a/packages/studio/src/utils/rdpSimplify.ts +++ b/packages/studio/src/utils/rdpSimplify.ts @@ -97,7 +97,7 @@ function simplifyTimeSeries( export function simplifyGestureSamples( samples: Array<{ time: number; properties: Record }>, totalDuration: number, - epsilon: number, + epsilon: number | ((key: string) => number), ): Map> { if (samples.length === 0) return new Map(); if (totalDuration <= 0) return new Map(); @@ -120,7 +120,8 @@ export function simplifyGestureSamples( series.push({ time: s.time, value: s.properties[key] }); } } - const simplified = simplifyTimeSeries(series, epsilon); + const keyEpsilon = typeof epsilon === "function" ? epsilon(key) : epsilon; + const simplified = simplifyTimeSeries(series, keyEpsilon); for (const pt of simplified) { survivingTimes.add(pt.time); }