Skip to content

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

Merged
miguel-heygen merged 3 commits into
mainfrom
fix/kf-intercept-routing
Jun 12, 2026
Merged

fix(studio): per-property-group intercept routing + drag/resize fixes#1356
miguel-heygen merged 3 commits into
mainfrom
fix/kf-intercept-routing

Conversation

@miguel-heygen

@miguel-heygen miguel-heygen commented Jun 11, 2026

Copy link
Copy Markdown
Collaborator

Summary

  • 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 for correct scale computation
  • Rotation routes to rotation group
  • Add resolveGroupTween helper for group-aware tween resolution
  • From-extend with split-first-then-position-only pattern in drag commit
  • autoKeyframeEnabled guard on all 3 GSAP-aware commit paths
  • Fix H1 bug: drag draft reads GSAP base + delta correctly
  • Fix H5 bug: cancel restores GSAP x/y from data attributes

Test plan

  • Build passes
  • Typecheck clean
  • Drag on GSAP-animated element persists position correctly
  • Resize computes scale via original-width attribute

Stack: 3/7 — depends on #1355

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.
…mutations

Server-side mutations for atomic property-group splitting and keyframe
replacement. Client commitMutation returns early on changed:false instead
of throwing.
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.

@james-russo-rames-d-jusso james-russo-rames-d-jusso left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Verdict: approve-with-nits. R2 hotspot of the stack — most R2 carry-overs from monolithic #1344 verified resolved here. Top-priority flag regression did NOT recur.

R2 carry-overs

  1. STUDIO_GSAP_DRAG_INTERCEPT_ENABLED flag default — RESOLVED. manualEditingAvailability.ts:90-94 ships false; test L30 expects false. The accidental flip from monolith #1344 R2 did not survive the restructure. (Matches #1341's disable-by-default intent.)
  2. Soft-reload tests — not in this PR's diff. Carry to #1357.
  3. tryGsapResizeIntercept size — unchanged. gsapRuntimeBridge.ts:255-425 ≈ 170 LOC, same shape as monolith R2. Restructure did not refactor down. (Concern, not blocker — single big resize fn is the price of routing all 3 sub-paths through one entry.)
  4. Duplicate IIFE outside-range — partial. synthesizeIdentityProps helper extracted at L242-251 ✅. Outside-range still has an IIFE wrapper at L354-368 (now ?? (() => {...})() lazy-fallback shape, but still inline). (Nit.)
  5. Cache triple-write — not in this PR. Carry to #1357.
  6. RC5 throw flip ✅ — useGsapScriptCommits.ts:136-144 throws on !result, silent return on result.changed === false. File untouched by this PR (carried fine from #1355).
  7. activeKeyframePct consumption ✅ — gsapRuntimeBridge.ts:307-309 + gsapDragCommit.ts:151-153. Last-clicked-diamond override works.

New finding

  • Hand-rolled fetch() + window.location.hash parsing in drag commitgsapDragCommit.ts:280-292. Outside-range drag path scrapes the project ID from window.location.hash and fetches /api/projects/.../gsap-animations/... directly instead of going through the existing projectIdRef/mutateGsapScript channel. User-gesture path so it's not a render-time determinism violation, but it's an ad-hoc backend call inside drag commit with no auth/error/retry plumbing and a fragile hash regex. Concern — wire it through the established mutation channel instead.

Stack coherence

PROPERTY_GROUPS taxonomy from #1354 consumed correctly — propertyGroup === "position" | "scale" | "rotation" predicates (not hardcoded property names). resolveGroupTween is the consistent entry point across drag/resize/rotate.

What I didn't verify

Studio vitest suite — CI shows no test job ran on this PR (preflight + previews only). Could not confirm manualEditingAvailability.test.ts:30 actually passes in CI vs. just looking right in source.

Review by Rames D Jusso

@james-russo-rames-d-jusso james-russo-rames-d-jusso left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

R3 verification — blocker. Two R2 findings did not land + a new path-prefix bug strictly worsens one.

🚨 gsapDragCommit.ts:284window.location.hash regex still there, AND now mismatched. R3 shape:

`/api/projects/${encodeURIComponent(window.location.hash.match(/project\/([^?/]+)/)?.[1] ?? "")}/gsap-animations/${encodeURIComponent(pid)}`

Regex matches /project/ (singular) while the API path is /api/projects/ (plural). If HF routes use either shape, mid-drag the captured group is "" → fetch hits /api/projects//gsap-animations/... → silent 404 → empty allAnims[] → "no existing position tween, create one" path → duplicate position tween on every from()/fromTo() drag that's out-of-range. Fix unchanged from R2: thread projectId through GsapDragCommitCallbacks or use the already-wired fetchFallbackAnimations.

🚨 gsapDragCommit.ts:151-153commitKeyframedPosition clears activeKeyframePct BEFORE await mutation. R3 shape:

const { activeKeyframePct, setActiveKeyframePct } = usePlayerStore.getState();
const pct = activeKeyframePct ?? computeCurrentPercentage(selection, anim);
if (activeKeyframePct != null) setActiveKeyframePct(null);
await callbacks.commitMutation(...)

setActiveKeyframePct(null) fires synchronously before await callbacks.commitMutation(...). If the mutation throws (RC5 flip from #1355), the targeted keyframe is lost and retry recomputes from playhead — silently shifting the keyframe. Fix: move into .finally() after success, or guard with try/catch that restores on throw.

⚠️ tryGsapResizeIntercept grew ~+60 LOC at gsapRuntimeBridge.ts:255-425 (~170 → ~230 with the new outside-range branch synthesizing identity props + replaying keyframes at lines 343-407). Not blocking, but the structural smell grew. Worth a follow-up split into applyInRangeResize / applyOutOfRangeResize.

New concern (surfacing, not deep-diving): resolveGroupTween (lines 121-175) calls commitMutation with split-into-property-groups then immediately re-fetches via fetchFallbackAnimations. No skipReload: false guarantee or ordering proof that the re-fetched list sees the post-split state. If the server mutation is async-coalesced (coalesceKey pattern used elsewhere in this file), the re-fetch may see pre-split tween → infinite-recurse on next intercept. Worth a closer look.

Test gap (still): zero new tests for the new from()/fromTo() drag-extension path (~120 LOC), zero for the new outside-range resize path (~60 LOC), zero for resolveGroupTween.

Review by Rames D Jusso

@vanceingalls vanceingalls left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Verification pass — R3 item checked against current HEAD (8ae1921).

commitKeyframedPositionactiveKeyframePct cleared before await

The fix is in #1360, not this PR. In this PR's HEAD (gsapDragCommit.ts:153):

if (activeKeyframePct != null) setActiveKeyframePct(null);
await callbacks.commitMutation(...);  // clear happens before this

The same pre-clear pattern exists in gsapRuntimeBridge.ts:309-310.

#1360's PR body claims the fix ("clear moved to after mutation succeeds") and #1360's gsapDragCommit.ts:164 confirms it — clear is after the await there. Since #1360 is above this PR in the stack, the fix will land when the stack merges in order. As a standalone PR this blocker is unresolved; the fix is deferred to #1360.

If the stack merges in order (bottom-up), this is fine architecturally. Flagging for awareness — stamp decision is Vance's call given the stacked context.

— Vai (verification pass)

Base automatically changed from fix/kf-server-mutations to main June 12, 2026 04:12
@github-actions

Copy link
Copy Markdown

Fallow audit report

Found 20 findings.

Duplication (6)
Severity Rule Location Description
minor fallow/code-duplication packages/studio/src/components/editor/manualEditsDom.ts:155 Code clone group 1 (7 lines, 2 instances)
minor fallow/code-duplication packages/studio/src/components/editor/propertyPanelHelpers.ts:313 Code clone group 1 (7 lines, 2 instances)
minor fallow/code-duplication packages/studio/src/hooks/gsapDragCommit.ts:35 Code clone group 2 (12 lines, 2 instances)
minor fallow/code-duplication packages/studio/src/hooks/gsapRuntimeBridge.ts:336 Code clone group 3 (6 lines, 2 instances)
minor fallow/code-duplication packages/studio/src/hooks/gsapRuntimeBridge.ts:481 Code clone group 3 (6 lines, 2 instances)
minor fallow/code-duplication packages/studio/src/hooks/useAnimatedPropertyCommit.ts:44 Code clone group 2 (12 lines, 2 instances)
Health (14)
Severity Rule Location Description
minor fallow/high-crap-score packages/studio/src/components/editor/manualEditsDom.ts:226 'stripGsapTranslateFromTransform' has CRAP score 43.1 (threshold: 30.0, cyclomatic 12)
major fallow/high-cognitive-complexity packages/studio/src/components/editor/manualEditsDom.ts:271 'applyStudioPathOffsetDraft' has cognitive complexity 26 (threshold: 15)
minor fallow/high-crap-score packages/studio/src/hooks/gsapRuntimeBridge.ts:85 '<arrow>' has CRAP score 37.1 (threshold: 30.0, cyclomatic 11)
minor fallow/high-crap-score packages/studio/src/hooks/gsapRuntimeBridge.ts:120 'resolveGroupTween' has CRAP score 43.1 (threshold: 30.0, cyclomatic 12)
critical fallow/high-crap-score packages/studio/src/hooks/gsapRuntimeBridge.ts:255 'tryGsapResizeIntercept' has CRAP score 51.8 (threshold: 30.0, cyclomatic 45)
major fallow/high-complexity packages/studio/src/hooks/gsapRuntimeBridge.ts:429 'tryGsapRotationIntercept' has cyclomatic complexity 24 (threshold: 20) and cognitive complexity 25 (threshold: 15)
critical fallow/high-crap-score packages/studio/src/hooks/gsapRuntimeReaders.ts:52 'readAllAnimatedProperties' has CRAP score 51.8 (threshold: 30.0, cyclomatic 45)
major fallow/high-crap-score packages/studio/src/hooks/useAnimatedPropertyCommit.ts:40 'computePercentage' has CRAP score 63.6 (threshold: 30.0, cyclomatic 15)
minor fallow/high-crap-score packages/studio/src/hooks/useAnimatedPropertyCommit.ts:66 'scored' has CRAP score 43.1 (threshold: 30.0, cyclomatic 12)
minor fallow/high-crap-score packages/studio/src/hooks/useAnimatedPropertyCommit.ts:97 'commitAnimatedProperty' has CRAP score 31.6 (threshold: 30.0, cyclomatic 10)
critical fallow/high-crap-score packages/studio/src/hooks/useEnableKeyframes.ts:37 'readElementPosition' has CRAP score 156.0 (threshold: 30.0, cyclomatic 12)
minor fallow/high-crap-score packages/studio/src/hooks/useEnableKeyframes.ts:66 'fetchAnimationsForElement' has CRAP score 30.0 (threshold: 30.0, cyclomatic 5)
major fallow/high-crap-score packages/studio/src/hooks/useEnableKeyframes.ts:78 'computePercentage' has CRAP score 72.0 (threshold: 30.0, cyclomatic 8)
critical fallow/high-crap-score packages/studio/src/hooks/useEnableKeyframes.ts:89 '<arrow>' has CRAP score 870.0 (threshold: 30.0, cyclomatic 29)

Generated by fallow.

@miguel-heygen miguel-heygen merged commit 95029e0 into main Jun 12, 2026
31 of 32 checks passed
@miguel-heygen miguel-heygen deleted the fix/kf-intercept-routing branch June 12, 2026 04:19
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants