fix(studio): per-property-group intercept routing + drag/resize fixes (#1356)

* 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.
This commit is contained in:
Miguel Ángel
2026-06-12 00:19:13 -04:00
committed by GitHub
parent b6bf1b1190
commit 95029e083e
12 changed files with 503 additions and 176 deletions
@@ -8,6 +8,7 @@
*/
import { useCallback } from "react";
import type { GsapAnimation } from "@hyperframes/core/gsap-parser";
import { classifyPropertyGroup } from "@hyperframes/core/gsap-parser";
import type { DomEditSelection } from "../components/editor/domEditingTypes";
import { usePlayerStore } from "../player/store/playerStore";
import { readAllAnimatedProperties, readGsapProperty } from "./gsapRuntimeBridge";
@@ -38,7 +39,7 @@ interface CommitAnimatedPropertyDeps {
function computePercentage(selection: DomEditSelection, anim?: GsapAnimation): number {
const currentTime = usePlayerStore.getState().currentTime;
const tweenPos = typeof anim?.position === "number" ? anim.position : 0;
const tweenPos = anim?.resolvedStart ?? (typeof anim?.position === "number" ? anim.position : 0);
const tweenDur = anim?.duration ?? 0;
if (tweenDur > 0) {
return Math.max(
@@ -56,18 +57,19 @@ function computePercentage(selection: DomEditSelection, anim?: GsapAnimation): n
function pickBestAnimation(
animations: GsapAnimation[],
selector: string | null,
property?: string,
): GsapAnimation | undefined {
if (animations.length <= 1) return animations[0];
const currentTime = usePlayerStore.getState().currentTime;
const targetGroup = property ? classifyPropertyGroup(property) : undefined;
const scored = animations.map((a) => {
let score = 0;
if (targetGroup && a.propertyGroup === targetGroup) score += 20;
if (a.keyframes) score += 10;
// Prefer single-element selectors over comma-separated groups
if (selector && a.targetSelector === selector) score += 5;
else if (a.targetSelector.includes(",")) score -= 3;
// Prefer tweens active at the current time
const pos = typeof a.position === "number" ? a.position : 0;
const pos = a.resolvedStart ?? (typeof a.position === "number" ? a.position : 0);
const dur = a.duration ?? 0;
if (currentTime >= pos - 0.05 && currentTime <= pos + dur + 0.05) score += 8;
return { anim: a, score };
@@ -102,7 +104,11 @@ export function useAnimatedPropertyCommit(deps: CommitAnimatedPropertyDeps) {
const iframe = previewIframeRef.current;
const selector = selectorFor(selection);
let anim: GsapAnimation | undefined = pickBestAnimation(selectedGsapAnimations, selector);
let anim: GsapAnimation | undefined = pickBestAnimation(
selectedGsapAnimations,
selector,
property,
);
// Case 3: No animation — create one first
if (!anim) {