fix(studio): correct gesture commits for scaled and graded elements

- resize on a scale-driven element commits per-axis scale (scaleX/scaleY
  for non-uniform drags) with keyframe normalization to the longhands, and
  clears the width/height draft so size can't double-apply; the intercept
  moves to gsapResizeIntercept.ts
- the drop frame applies the corrected position synchronously in the same
  microtask chain as the soft reload (no network-window jump), and the
  draft pins the anchor through accumulated moves on scaled elements
- gesture size/position math divides by the element's own content scale
- convert-to-keyframes resolves current values through the property-group
  filter for ALL capture passes (opacity/rotationX from unrelated tweens
  no longer leak into a rotation commit), and a grading-hidden source's
  opacity is read from its canvas, not the inline hide
- canvas pointer-down confirms the hover target with a synchronous
  hit-test before starting a marquee (stale-hover race lost selections)
This commit is contained in:
Miguel Angel Simon Sierra
2026-07-11 04:07:06 -04:00
parent 5cc14c2221
commit 3525c7ff52
10 changed files with 703 additions and 257 deletions
@@ -1,4 +1,5 @@
import { memo, useEffect, useMemo, useRef, useState, type RefObject } from "react";
import { getPreviewTargetFromPointer } from "../../utils/studioPreviewHelpers";
import { type DomEditSelection } from "./domEditing";
import type { PreviewMouseDownOptions } from "../../hooks/usePreviewInteraction";
import { useMarqueeGestures } from "./marqueeCommit";
@@ -305,8 +306,18 @@ export const DomEditOverlay = memo(function DomEditOverlay({
const target = event.target as HTMLElement | null;
if (target?.closest('[data-dom-edit-selection-box="true"]')) return;
// Start marquee if clicking on empty canvas (no element under pointer)
// Start marquee if clicking on empty canvas (no element under pointer).
// The hover selection is an ASYNC cache: on a fast click (or when the
// pointer was already resting over an element) it can still be empty while
// an element IS under the pointer — starting a marquee here would swallow
// the selection mousedown and the click would silently select nothing.
// Confirm emptiness with a fresh SYNCHRONOUS hit-test before committing.
if (!hoverSelectionRef.current && onMarqueeSelectRef.current && compRect.width > 0) {
const iframe = iframeRef.current;
const freshTarget = iframe
? getPreviewTargetFromPointer(iframe, event.clientX, event.clientY, activeCompositionPath)
: null;
if (freshTarget) return;
const overlayEl = overlayRef.current;
if (overlayEl) {
const oRect = overlayEl.getBoundingClientRect();