Files
heygen-com--hyperframes/packages/studio/src/components/editor/domEditOverlayStartGesture.ts
T
wehub-resource-sync 85453da49f
regression / regression-shards (style-16-prod style-9-prod style-17-prod iframe-render-compat variables-prod mp4-h265-sdr, shard-4) (push) Has been cancelled
regression / regression-shards (style-4-prod style-11-prod style-2-prod animejs-adapter typegpu-adapter parallel-capture-regression, shard-5) (push) Has been cancelled
regression / regression-shards (style-7-prod style-8-prod style-10-prod css-spinner-render-compat webm-transparency mp4-h264-sdr webm-vp9, shard-3) (push) Has been cancelled
regression / regression-shards (sub-composition-video style-18-prod raf-ball-render-compat font-variant-numeric sub-comp-t0 sub-comp-id-selector, shard-7) (push) Has been cancelled
Windows render verification / Detect changes (push) Has been cancelled
Windows render verification / Preflight (lint + format) (push) Has been cancelled
Windows render verification / Render on windows-latest (push) Has been cancelled
Windows render verification / Tests on windows-latest (push) Has been cancelled
CI / Detect changes (push) Has been cancelled
CI / Build (push) Has been cancelled
CI / Lint (push) Has been cancelled
CI / Fallow audit (push) Has been cancelled
CI / Format (push) Has been cancelled
CI / Typecheck (push) Has been cancelled
CI / Test (push) Has been cancelled
CI / Producer: integration tests (push) Has been cancelled
CI / Producer: unit tests (push) Has been cancelled
CI / File size check (push) Has been cancelled
CI / Test: skills (push) Has been cancelled
CI / Skills: manifest in sync (push) Has been cancelled
CI / CLI: npx shim (macos-latest) (push) Has been cancelled
CI / CLI: npx shim (ubuntu-latest) (push) Has been cancelled
CI / CLI: npx shim (windows-latest) (push) Has been cancelled
CI / SDK: unit + contract + smoke (push) Has been cancelled
CI / Test: runtime contract (push) Has been cancelled
CI / Studio: load smoke (push) Has been cancelled
CI / Smoke: global install (push) Has been cancelled
CI / CLI smoke (required) (push) Has been cancelled
CI / Semantic PR title (push) Has been cancelled
Player perf / Detect changes (push) Has been cancelled
Player perf / Preflight (lint + format) (push) Has been cancelled
Player perf / player-perf (push) Has been cancelled
Player perf / Perf: drift (push) Has been cancelled
Player perf / Perf: fps (push) Has been cancelled
Player perf / Perf: parity (push) Has been cancelled
Player perf / Perf: scrub (push) Has been cancelled
Player perf / Perf: load (push) Has been cancelled
preview-regression / Detect changes (push) Has been cancelled
preview-regression / Preflight (lint + format) (push) Has been cancelled
preview-regression / Preview parity (push) Has been cancelled
preview-regression / preview-regression (push) Has been cancelled
regression / regression (push) Has been cancelled
regression / Detect changes (push) Has been cancelled
regression / Preflight (lint + format) (push) Has been cancelled
regression / regression-shards (hdr-regression style-5-prod style-3-prod mov-prores, shard-1) (push) Has been cancelled
regression / regression-shards (overlay-montage-prod style-12-prod chat missing-host-comp-id png-sequence portrait-edge-bleed, shard-6) (push) Has been cancelled
regression / regression-shards (style-13-prod style-6-prod vignelli-stacking gsap-letters-render-compat audio-mux-parity, shard-8) (push) Has been cancelled
regression / regression-shards (style-15-prod hdr-hlg-regression style-1-prod many-cuts vfr-screen-recording render-symlinked-assets, shard-2) (push) Has been cancelled
CodeQL / Analyze (actions) (push) Has been cancelled
CodeQL / Analyze (javascript-typescript) (push) Has been cancelled
CodeQL / Analyze (python) (push) Has been cancelled
Docs / Validate docs (push) Has been cancelled
Sync skills to ClawHub / Publish changed skills (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 12:58:35 +08:00

225 lines
7.5 KiB
TypeScript

/**
* Gesture-begin functions: startGroupDrag and startGesture.
* These are pure "start a new gesture" operations — no draft rect updates.
*/
import { readElementGsapNumber } from "../../utils/elementGsap";
import { type DomEditSelection } from "./domEditing";
import {
createManualOffsetDragMember,
readGsapRotation,
restoreManualOffsetDragMembers,
type ManualOffsetDragMember,
} from "./manualOffsetDrag";
import {
beginStudioManualEditGesture,
captureStudioBoxSize,
captureStudioPathOffset,
captureStudioRotation,
readStudioBoxSize,
readStudioRotation,
} from "./manualEdits";
import {
type OverlayRect,
filterNestedDomEditGroupItems,
selectionCacheKey,
} from "./domEditOverlayGeometry";
import {
type GestureKind,
type GestureState,
type UseDomEditOverlayGesturesOptions,
} from "./domEditOverlayGestures";
import { collectSnapContext, buildExcludeElements } from "./snapTargetCollection";
export function startGroupDrag(
e: React.PointerEvent<HTMLElement>,
opts: UseDomEditOverlayGesturesOptions,
): boolean {
const items = opts.groupOverlayItemsRef.current;
if (items.length <= 1) return false;
const blockedSelection = items.find(
(item) => !item.selection.capabilities.canApplyManualOffset,
)?.selection;
if (blockedSelection) {
e.preventDefault();
e.stopPropagation();
opts.onBlockedMoveRef.current(blockedSelection);
return false;
}
opts.onManualDragStartRef.current?.();
const dragItems = filterNestedDomEditGroupItems(items);
const members: ManualOffsetDragMember[] = [];
for (const item of dragItems) {
const result = createManualOffsetDragMember({
key: item.key,
selection: item.selection,
element: item.element,
rect: item.rect,
});
if (!result.ok) {
restoreManualOffsetDragMembers(members);
e.preventDefault();
e.stopPropagation();
opts.onBlockedMoveRef.current(result.selection);
return false;
}
members.push(result.member);
}
const overlayEl = opts.overlayRef.current;
const iframe = opts.iframeRef.current;
const snapContext =
overlayEl && iframe
? collectSnapContext({
overlayEl,
iframe,
excludeElements: buildExcludeElements({
iframe,
groupSelections: items.map((i) => i.selection),
}),
})
: undefined;
e.preventDefault();
e.stopPropagation();
e.currentTarget.setPointerCapture(e.pointerId);
opts.rafPausedRef.current = true;
opts.groupGestureRef.current = {
startX: e.clientX,
startY: e.clientY,
originItems: items,
members,
snapContext,
};
return true;
}
// fallow-ignore-next-line complexity
export function startGesture(
kind: GestureKind,
e: React.PointerEvent<HTMLElement>,
opts: UseDomEditOverlayGesturesOptions,
options?: { selection?: DomEditSelection; rect?: OverlayRect | null },
): boolean {
const sel = options?.selection ?? opts.selectionRef.current;
const rect = options?.rect ?? opts.overlayRectRef.current;
const box = opts.boxRef.current;
const overlayEl = opts.overlayRef.current;
if (!sel || !rect) return false;
if (kind !== "drag" && !box) return false;
const mode: GestureState["mode"] =
kind === "rotate" ? "rotation" : kind === "drag" ? "path-offset" : "box-size";
if (kind === "drag" && !sel.capabilities.canApplyManualOffset) return false;
if (kind === "resize" && !sel.capabilities.canApplyManualSize) return false;
if (kind === "rotate" && !sel.capabilities.canApplyManualRotation) return false;
if (kind === "resize" && (!Number.isFinite(rect.width) || !Number.isFinite(rect.height)))
return false;
const size = readStudioBoxSize(sel.element);
// Single-source rotation base = the live GSAP transform rotation plus any legacy
// `--hf-studio-rotation` CSS var (old projects), so a rotate gesture starts from the
// element's actual visual angle and commits an absolute angle to the timeline.
const rotation = { angle: readGsapRotation(sel.element) + readStudioRotation(sel.element).angle };
// The draft writes CSS width/height, so the resize base must be the CSS
// layout size. offsetWidth/Height are transform-free; the overlay-rect
// fallback (rect / editScale) includes the element's own GSAP scale and
// would make a rescaled element's draft grow from the RENDERED size.
const layoutWidth = sel.element.offsetWidth;
const layoutHeight = sel.element.offsetHeight;
const actualWidth =
size.width > 0 ? size.width : layoutWidth > 0 ? layoutWidth : rect.width / rect.editScaleX;
const actualHeight =
size.height > 0 ? size.height : layoutHeight > 0 ? layoutHeight : rect.height / rect.editScaleY;
// overlay rect = cssSize x contentScale x editScale, so the element's own
// render factor (its GSAP scale) falls out of the measured rect. 1 when
// unscaled or unmeasurable.
const rawContentScaleX = rect.width / (rect.editScaleX * actualWidth);
const rawContentScaleY = rect.height / (rect.editScaleY * actualHeight);
const contentScaleX =
Number.isFinite(rawContentScaleX) && rawContentScaleX > 0 ? rawContentScaleX : 1;
const contentScaleY =
Number.isFinite(rawContentScaleY) && rawContentScaleY > 0 ? rawContentScaleY : 1;
let resizeAnchor: GestureState["resizeAnchor"];
if (kind === "resize") {
const startBcr = sel.element.getBoundingClientRect();
resizeAnchor = {
anchorX: startBcr.x,
anchorY: startBcr.y,
baseGsapX: readElementGsapNumber(sel.element, "x") ?? 0,
baseGsapY: readElementGsapNumber(sel.element, "y") ?? 0,
pinX: 0,
pinY: 0,
};
}
let initialPathOffset = captureStudioPathOffset(sel.element);
let manualEditDragToken: string | undefined;
let pathOffsetMember: ManualOffsetDragMember | undefined;
if (kind === "drag") {
opts.onManualDragStartRef.current?.();
opts.rafPausedRef.current = true;
const result = createManualOffsetDragMember({
key: selectionCacheKey(sel),
selection: sel,
element: sel.element,
rect,
});
if (!result.ok) {
opts.onBlockedMoveRef.current(result.selection);
return false;
}
pathOffsetMember = result.member;
initialPathOffset = result.member.initialPathOffset;
manualEditDragToken = result.member.gestureToken;
} else {
manualEditDragToken = beginStudioManualEditGesture(sel.element);
}
const overlayBounds = overlayEl?.getBoundingClientRect();
const centerX = (overlayBounds?.left ?? 0) + rect.left + rect.width / 2;
const centerY = (overlayBounds?.top ?? 0) + rect.top + rect.height / 2;
const iframe = opts.iframeRef.current;
const snapContext =
(kind === "drag" || kind === "resize") && overlayEl && iframe
? collectSnapContext({
overlayEl,
iframe,
excludeElements: buildExcludeElements({ iframe, selection: sel }),
})
: undefined;
e.preventDefault();
e.stopPropagation();
e.currentTarget.setPointerCapture(e.pointerId);
opts.rafPausedRef.current = true;
opts.gestureRef.current = {
kind,
mode,
selection: sel,
startX: e.clientX,
startY: e.clientY,
centerX,
centerY,
initialPathOffset,
initialRotation: captureStudioRotation(sel.element),
initialBoxSize: captureStudioBoxSize(sel.element),
pathOffsetMember,
originLeft: rect.left,
originTop: rect.top,
originWidth: rect.width,
originHeight: rect.height,
actualWidth,
actualHeight,
actualRotation: rotation.angle,
editScaleX: rect.editScaleX,
editScaleY: rect.editScaleY,
contentScaleX,
contentScaleY,
resizeAnchor,
manualEditDragToken,
snapContext,
};
return true;
}