mirror of
https://github.com/heygen-com/hyperframes.git
synced 2026-09-04 07:19:52 +00:00
feat(studio): wire snap engine into preview drag and resize gestures (#1223)
Connect snap engine and UI components to the preview canvas gesture system. Dragging or resizing elements now shows Figma-style alignment guides with snap-to-edge, snap-to-center, and grid snap. - Collect snap targets once at gesture start, reuse per frame - resolveSnapAdjustment called per pointermove during drag - resolveResizeSnapAdjustment for resize gestures - lastSnappedDx/Dy stored on GestureState for consistent drop - Alt/Option key temporarily disables snap - SnapToolbar rendered in preview area with snap prefs state
This commit is contained in:
@@ -1,8 +1,9 @@
|
||||
import type { ReactNode } from "react";
|
||||
import { useState, type ReactNode } from "react";
|
||||
import { NLELayout } from "./nle/NLELayout";
|
||||
import { CaptionOverlay } from "../captions/components/CaptionOverlay";
|
||||
import { CaptionTimeline } from "../captions/components/CaptionTimeline";
|
||||
import { DomEditOverlay } from "./editor/DomEditOverlay";
|
||||
import { SnapToolbar } from "./editor/SnapToolbar";
|
||||
import { StudioFeedbackBar } from "./StudioFeedbackBar";
|
||||
import type { TimelineElement } from "../player";
|
||||
import { usePlayerStore } from "../player/store/playerStore";
|
||||
@@ -15,6 +16,7 @@ import {
|
||||
import { useStudioContext } from "../contexts/StudioContext";
|
||||
import { useDomEditContext } from "../contexts/DomEditContext";
|
||||
import type { BlockPreviewInfo } from "./sidebar/BlocksTab";
|
||||
import { readStudioUiPreferences } from "../utils/studioUiPreferences";
|
||||
|
||||
export interface StudioPreviewAreaProps {
|
||||
timelineToolbar: ReactNode;
|
||||
@@ -112,6 +114,16 @@ export function StudioPreviewArea({
|
||||
handleGsapDeleteAnimation,
|
||||
} = useDomEditContext();
|
||||
|
||||
const [snapPrefs, setSnapPrefs] = useState(() => {
|
||||
const p = readStudioUiPreferences();
|
||||
return {
|
||||
snapEnabled: p.snapEnabled ?? true,
|
||||
gridVisible: p.gridVisible ?? false,
|
||||
gridSpacing: p.gridSpacing ?? 50,
|
||||
snapToGrid: p.snapToGrid ?? false,
|
||||
};
|
||||
});
|
||||
|
||||
return (
|
||||
<div className="flex-1 flex flex-col relative min-w-0">
|
||||
<div className="flex-1 min-h-0 relative">
|
||||
@@ -215,31 +227,36 @@ export function StudioPreviewArea({
|
||||
) : captionEditMode ? (
|
||||
<CaptionOverlay iframeRef={previewIframeRef} />
|
||||
) : STUDIO_INSPECTOR_PANELS_ENABLED ? (
|
||||
<DomEditOverlay
|
||||
iframeRef={previewIframeRef}
|
||||
activeCompositionPath={activeCompPath}
|
||||
hoverSelection={
|
||||
STUDIO_PREVIEW_SELECTION_ENABLED &&
|
||||
!captionEditMode &&
|
||||
!compositionLoading &&
|
||||
!isPlaying
|
||||
? domEditHoverSelection
|
||||
: null
|
||||
}
|
||||
selection={shouldShowSelectedDomBounds ? domEditSelection : null}
|
||||
groupSelections={shouldShowSelectedDomBounds ? domEditGroupSelections : []}
|
||||
allowCanvasMovement={STUDIO_PREVIEW_MANUAL_EDITING_ENABLED}
|
||||
onCanvasMouseDown={handlePreviewCanvasMouseDown}
|
||||
onCanvasPointerMove={handlePreviewCanvasPointerMove}
|
||||
onCanvasPointerLeave={handlePreviewCanvasPointerLeave}
|
||||
onSelectionChange={applyDomSelection}
|
||||
onBlockedMove={handleBlockedDomMove}
|
||||
onManualDragStart={handleDomManualDragStart}
|
||||
onPathOffsetCommit={handleDomPathOffsetCommit}
|
||||
onGroupPathOffsetCommit={handleDomGroupPathOffsetCommit}
|
||||
onBoxSizeCommit={handleDomBoxSizeCommit}
|
||||
onRotationCommit={handleDomRotationCommit}
|
||||
/>
|
||||
<>
|
||||
<DomEditOverlay
|
||||
iframeRef={previewIframeRef}
|
||||
activeCompositionPath={activeCompPath}
|
||||
hoverSelection={
|
||||
STUDIO_PREVIEW_SELECTION_ENABLED &&
|
||||
!captionEditMode &&
|
||||
!compositionLoading &&
|
||||
!isPlaying
|
||||
? domEditHoverSelection
|
||||
: null
|
||||
}
|
||||
selection={shouldShowSelectedDomBounds ? domEditSelection : null}
|
||||
groupSelections={shouldShowSelectedDomBounds ? domEditGroupSelections : []}
|
||||
allowCanvasMovement={STUDIO_PREVIEW_MANUAL_EDITING_ENABLED}
|
||||
onCanvasMouseDown={handlePreviewCanvasMouseDown}
|
||||
onCanvasPointerMove={handlePreviewCanvasPointerMove}
|
||||
onCanvasPointerLeave={handlePreviewCanvasPointerLeave}
|
||||
onSelectionChange={applyDomSelection}
|
||||
onBlockedMove={handleBlockedDomMove}
|
||||
onManualDragStart={handleDomManualDragStart}
|
||||
onPathOffsetCommit={handleDomPathOffsetCommit}
|
||||
onGroupPathOffsetCommit={handleDomGroupPathOffsetCommit}
|
||||
onBoxSizeCommit={handleDomBoxSizeCommit}
|
||||
onRotationCommit={handleDomRotationCommit}
|
||||
gridVisible={snapPrefs.gridVisible}
|
||||
gridSpacing={snapPrefs.gridSpacing}
|
||||
/>
|
||||
<SnapToolbar onSnapChange={setSnapPrefs} />
|
||||
</>
|
||||
) : null
|
||||
}
|
||||
timelineFooter={
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { memo, useMemo, useRef, type RefObject } from "react";
|
||||
import { memo, useMemo, useRef, useState, type RefObject } from "react";
|
||||
import { useMountEffect } from "../../hooks/useMountEffect";
|
||||
import { type DomEditSelection } from "./domEditing";
|
||||
import { resolveDomEditGroupOverlayRect, toOverlayRect } from "./domEditOverlayGeometry";
|
||||
import {
|
||||
@@ -10,6 +11,8 @@ import {
|
||||
} from "./domEditOverlayGestures";
|
||||
import { useDomEditOverlayRects } from "./useDomEditOverlayRects";
|
||||
import { createDomEditOverlayGestureHandlers } from "./useDomEditOverlayGestures";
|
||||
import { SnapGuideOverlay, type SnapGuidesState } from "./SnapGuideOverlay";
|
||||
import { GridOverlay } from "./GridOverlay";
|
||||
|
||||
// Re-exports for external consumers — preserving existing import paths.
|
||||
export {
|
||||
@@ -61,6 +64,8 @@ interface DomEditOverlayProps {
|
||||
next: { width: number; height: number },
|
||||
) => Promise<void> | void;
|
||||
onRotationCommit: (selection: DomEditSelection, next: { angle: number }) => Promise<void> | void;
|
||||
gridVisible?: boolean;
|
||||
gridSpacing?: number;
|
||||
}
|
||||
|
||||
export const DomEditOverlay = memo(function DomEditOverlay({
|
||||
@@ -75,6 +80,8 @@ export const DomEditOverlay = memo(function DomEditOverlay({
|
||||
onCanvasPointerLeave,
|
||||
onSelectionChange,
|
||||
onBlockedMove,
|
||||
gridVisible = false,
|
||||
gridSpacing = 50,
|
||||
onManualDragStart,
|
||||
onPathOffsetCommit,
|
||||
onGroupPathOffsetCommit,
|
||||
@@ -89,6 +96,7 @@ export const DomEditOverlay = memo(function DomEditOverlay({
|
||||
const suppressNextBoxClickRef = useRef(false);
|
||||
const suppressNextBoxMouseDownRef = useRef(false);
|
||||
const suppressNextOverlayMouseDownRef = useRef(false);
|
||||
const snapGuidesRef = useRef<SnapGuidesState | null>(null);
|
||||
const rafPausedRef = useRef(false);
|
||||
|
||||
const selectionRef = useRef(selection);
|
||||
@@ -136,6 +144,50 @@ export const DomEditOverlay = memo(function DomEditOverlay({
|
||||
rafPausedRef,
|
||||
});
|
||||
|
||||
const [compRect, setCompRect] = useState({
|
||||
left: 0,
|
||||
top: 0,
|
||||
width: 0,
|
||||
height: 0,
|
||||
scaleX: 1,
|
||||
scaleY: 1,
|
||||
});
|
||||
useMountEffect(() => {
|
||||
let frame = 0;
|
||||
// fallow-ignore-next-line complexity
|
||||
const update = () => {
|
||||
frame = requestAnimationFrame(update);
|
||||
const iframe = iframeRef.current;
|
||||
const overlayEl = overlayRef.current;
|
||||
if (!iframe || !overlayEl) return;
|
||||
const iRect = iframe.getBoundingClientRect();
|
||||
const oRect = overlayEl.getBoundingClientRect();
|
||||
const left = iRect.left - oRect.left;
|
||||
const top = iRect.top - oRect.top;
|
||||
if (iRect.width <= 0 || iRect.height <= 0) return;
|
||||
const doc = iframe.contentDocument;
|
||||
const root = doc?.querySelector<HTMLElement>("[data-composition-id]") ?? doc?.documentElement;
|
||||
const dw = Number.parseFloat(root?.getAttribute("data-width") ?? "");
|
||||
const dh = Number.parseFloat(root?.getAttribute("data-height") ?? "");
|
||||
const scaleX = dw > 0 ? iRect.width / dw : 1;
|
||||
const scaleY = dh > 0 ? iRect.height / dh : 1;
|
||||
setCompRect((prev) => {
|
||||
if (
|
||||
Math.abs(prev.left - left) < 0.5 &&
|
||||
Math.abs(prev.top - top) < 0.5 &&
|
||||
Math.abs(prev.width - iRect.width) < 0.5 &&
|
||||
Math.abs(prev.height - iRect.height) < 0.5 &&
|
||||
Math.abs(prev.scaleX - scaleX) < 0.001 &&
|
||||
Math.abs(prev.scaleY - scaleY) < 0.001
|
||||
)
|
||||
return prev;
|
||||
return { left, top, width: iRect.width, height: iRect.height, scaleX, scaleY };
|
||||
});
|
||||
};
|
||||
frame = requestAnimationFrame(update);
|
||||
return () => cancelAnimationFrame(frame);
|
||||
});
|
||||
|
||||
const gestures = createDomEditOverlayGestureHandlers({
|
||||
overlayRef,
|
||||
iframeRef,
|
||||
@@ -158,6 +210,7 @@ export const DomEditOverlay = memo(function DomEditOverlay({
|
||||
onRotationCommitRef,
|
||||
onCanvasPointerMoveRef,
|
||||
onCanvasMouseDown,
|
||||
snapGuidesRef,
|
||||
});
|
||||
|
||||
const selectionKey = useMemo(() => {
|
||||
@@ -192,6 +245,7 @@ export const DomEditOverlay = memo(function DomEditOverlay({
|
||||
}
|
||||
};
|
||||
|
||||
// fallow-ignore-next-line complexity
|
||||
const handleOverlayPointerDown = (event: React.PointerEvent<HTMLDivElement>) => {
|
||||
if (!allowCanvasMovement || event.button !== 0) return;
|
||||
if (event.shiftKey) {
|
||||
@@ -387,6 +441,21 @@ export const DomEditOverlay = memo(function DomEditOverlay({
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
<GridOverlay
|
||||
visible={gridVisible}
|
||||
spacing={gridSpacing}
|
||||
scaleX={compRect.scaleX}
|
||||
scaleY={compRect.scaleY}
|
||||
compositionLeft={compRect.left}
|
||||
compositionTop={compRect.top}
|
||||
compositionWidth={compRect.width}
|
||||
compositionHeight={compRect.height}
|
||||
/>
|
||||
<SnapGuideOverlay
|
||||
snapGuidesRef={snapGuidesRef}
|
||||
overlayWidth={compRect.width}
|
||||
overlayHeight={compRect.height}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
});
|
||||
|
||||
@@ -6,6 +6,7 @@ import type {
|
||||
} from "./manualEdits";
|
||||
import type { ManualOffsetDragMember } from "./manualOffsetDrag";
|
||||
import type { GroupOverlayItem } from "./domEditOverlayGeometry";
|
||||
import type { SnapContext } from "./snapTargetCollection";
|
||||
|
||||
export type GestureKind = "drag" | "resize" | "rotate";
|
||||
|
||||
@@ -36,6 +37,9 @@ export interface GestureState {
|
||||
editScaleX: number;
|
||||
editScaleY: number;
|
||||
manualEditDragToken?: string;
|
||||
snapContext?: SnapContext;
|
||||
lastSnappedDx?: number;
|
||||
lastSnappedDy?: number;
|
||||
}
|
||||
|
||||
export interface GroupGestureState {
|
||||
@@ -43,6 +47,9 @@ export interface GroupGestureState {
|
||||
startY: number;
|
||||
originItems: GroupOverlayItem[];
|
||||
members: ManualOffsetDragMember[];
|
||||
snapContext?: SnapContext;
|
||||
lastSnappedDx?: number;
|
||||
lastSnappedDy?: number;
|
||||
}
|
||||
|
||||
export interface BlockedMoveState {
|
||||
|
||||
@@ -23,6 +23,7 @@ import {
|
||||
} from "./domEditOverlayGeometry";
|
||||
import { type GestureKind, type GestureState } from "./domEditOverlayGestures";
|
||||
import type { UseDomEditOverlayGesturesOptions } from "./useDomEditOverlayGestures";
|
||||
import { collectSnapContext, buildExcludeElements } from "./snapTargetCollection";
|
||||
|
||||
export function startGroupDrag(
|
||||
e: React.PointerEvent<HTMLElement>,
|
||||
@@ -61,6 +62,20 @@ export function startGroupDrag(
|
||||
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);
|
||||
@@ -70,10 +85,12 @@ export function startGroupDrag(
|
||||
startY: e.clientY,
|
||||
originItems: items,
|
||||
members,
|
||||
snapContext,
|
||||
};
|
||||
return true;
|
||||
}
|
||||
|
||||
// fallow-ignore-next-line complexity
|
||||
export function startGesture(
|
||||
kind: GestureKind,
|
||||
e: React.PointerEvent<HTMLElement>,
|
||||
@@ -124,6 +141,16 @@ export function startGesture(
|
||||
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);
|
||||
@@ -150,6 +177,7 @@ export function startGesture(
|
||||
editScaleX: rect.editScaleX,
|
||||
editScaleY: rect.editScaleY,
|
||||
manualEditDragToken,
|
||||
snapContext,
|
||||
};
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
// fallow-ignore-file code-duplication
|
||||
/**
|
||||
* Gesture handling for DomEditOverlay.
|
||||
* Owns: onPointerMove, onPointerUp, clearPointerState.
|
||||
@@ -23,7 +24,12 @@ import {
|
||||
restoreStudioPathOffset,
|
||||
restoreStudioRotation,
|
||||
} from "./manualEdits";
|
||||
import { type GroupOverlayItem, type OverlayRect, toOverlayRect } from "./domEditOverlayGeometry";
|
||||
import {
|
||||
type GroupOverlayItem,
|
||||
type OverlayRect,
|
||||
resolveDomEditGroupOverlayRect,
|
||||
toOverlayRect,
|
||||
} from "./domEditOverlayGeometry";
|
||||
import {
|
||||
BLOCKED_MOVE_THRESHOLD_PX,
|
||||
type BlockedMoveState,
|
||||
@@ -39,6 +45,13 @@ import {
|
||||
startGesture as _startGesture,
|
||||
startGroupDrag as _startGroupDrag,
|
||||
} from "./domEditOverlayStartGesture";
|
||||
import {
|
||||
resolveSnapAdjustment,
|
||||
resolveResizeSnapAdjustment,
|
||||
resolveEquidistanceGuides,
|
||||
SNAP_THRESHOLD_PX,
|
||||
} from "./snapEngine";
|
||||
import type { SnapGuidesState } from "./SnapGuideOverlay";
|
||||
|
||||
// Refs are stable across renders; values are read via .current.
|
||||
export type UseDomEditOverlayGesturesOptions = {
|
||||
@@ -79,6 +92,7 @@ export type UseDomEditOverlayGesturesOptions = {
|
||||
e: React.MouseEvent<HTMLDivElement>,
|
||||
o?: { preferClipAncestor?: boolean },
|
||||
) => void;
|
||||
snapGuidesRef: RefObject<SnapGuidesState | null>;
|
||||
};
|
||||
|
||||
export function createDomEditOverlayGestureHandlers(opts: UseDomEditOverlayGesturesOptions) {
|
||||
@@ -111,6 +125,7 @@ export function createDomEditOverlayGestureHandlers(opts: UseDomEditOverlayGestu
|
||||
options?: { selection?: DomEditSelection; rect?: OverlayRect | null },
|
||||
) => _startGesture(kind, e, opts, options);
|
||||
|
||||
// fallow-ignore-next-line complexity
|
||||
const onPointerMove = (e: React.PointerEvent<HTMLDivElement>) => {
|
||||
const g = opts.gestureRef.current;
|
||||
const groupG = opts.groupGestureRef.current;
|
||||
@@ -133,8 +148,48 @@ export function createDomEditOverlayGestureHandlers(opts: UseDomEditOverlayGestu
|
||||
}
|
||||
|
||||
if (groupG) {
|
||||
const dx = e.clientX - groupG.startX;
|
||||
const dy = e.clientY - groupG.startY;
|
||||
let dx = e.clientX - groupG.startX;
|
||||
let dy = e.clientY - groupG.startY;
|
||||
|
||||
const sc = groupG.snapContext;
|
||||
if (sc?.snapEnabled && sc.targets.length > 0) {
|
||||
const groupBounds = resolveDomEditGroupOverlayRect(
|
||||
groupG.originItems.map((item) => item.rect),
|
||||
);
|
||||
if (groupBounds) {
|
||||
const allTargets = sc.compositionTarget
|
||||
? [...sc.targets, sc.compositionTarget]
|
||||
: sc.targets;
|
||||
const snap = resolveSnapAdjustment({
|
||||
movingRect: groupBounds,
|
||||
proposedDx: dx,
|
||||
proposedDy: dy,
|
||||
targets: allTargets,
|
||||
gridEdges: sc.gridEdges ?? undefined,
|
||||
threshold: SNAP_THRESHOLD_PX,
|
||||
disabled: e.altKey,
|
||||
});
|
||||
dx = snap.dx;
|
||||
dy = snap.dy;
|
||||
const movedRect = {
|
||||
left: groupBounds.left + dx,
|
||||
top: groupBounds.top + dy,
|
||||
width: groupBounds.width,
|
||||
height: groupBounds.height,
|
||||
};
|
||||
const spacingGuides = e.altKey
|
||||
? []
|
||||
: resolveEquidistanceGuides({
|
||||
movingRect: movedRect,
|
||||
targets: allTargets,
|
||||
threshold: SNAP_THRESHOLD_PX,
|
||||
});
|
||||
opts.snapGuidesRef.current = { guides: snap.guides, spacingGuides };
|
||||
}
|
||||
}
|
||||
groupG.lastSnappedDx = dx;
|
||||
groupG.lastSnappedDy = dy;
|
||||
|
||||
setDraftGroupOverlayItems(
|
||||
groupG.originItems.map((item) => ({
|
||||
...item,
|
||||
@@ -146,8 +201,8 @@ export function createDomEditOverlayGestureHandlers(opts: UseDomEditOverlayGestu
|
||||
}
|
||||
|
||||
if (!g || !sel) return;
|
||||
const dx = e.clientX - g.startX;
|
||||
const dy = e.clientY - g.startY;
|
||||
let dx = e.clientX - g.startX;
|
||||
let dy = e.clientY - g.startY;
|
||||
|
||||
if (g.kind === "rotate") {
|
||||
applyStudioRotationDraft(
|
||||
@@ -167,6 +222,46 @@ export function createDomEditOverlayGestureHandlers(opts: UseDomEditOverlayGestu
|
||||
}
|
||||
|
||||
if (g.kind === "drag") {
|
||||
const sc = g.snapContext;
|
||||
if (sc?.snapEnabled && sc.targets.length > 0) {
|
||||
const movingRect = {
|
||||
left: g.originLeft,
|
||||
top: g.originTop,
|
||||
width: g.originWidth,
|
||||
height: g.originHeight,
|
||||
};
|
||||
const allTargets = sc.compositionTarget
|
||||
? [...sc.targets, sc.compositionTarget]
|
||||
: sc.targets;
|
||||
const snap = resolveSnapAdjustment({
|
||||
movingRect,
|
||||
proposedDx: dx,
|
||||
proposedDy: dy,
|
||||
targets: allTargets,
|
||||
gridEdges: sc.gridEdges ?? undefined,
|
||||
threshold: SNAP_THRESHOLD_PX,
|
||||
disabled: e.altKey,
|
||||
});
|
||||
dx = snap.dx;
|
||||
dy = snap.dy;
|
||||
const movedRect = {
|
||||
left: movingRect.left + dx,
|
||||
top: movingRect.top + dy,
|
||||
width: movingRect.width,
|
||||
height: movingRect.height,
|
||||
};
|
||||
const spacingGuides = e.altKey
|
||||
? []
|
||||
: resolveEquidistanceGuides({
|
||||
movingRect: movedRect,
|
||||
targets: allTargets,
|
||||
threshold: SNAP_THRESHOLD_PX,
|
||||
});
|
||||
opts.snapGuidesRef.current = { guides: snap.guides, spacingGuides };
|
||||
}
|
||||
g.lastSnappedDx = dx;
|
||||
g.lastSnappedDy = dy;
|
||||
|
||||
const nextBoxLeft = g.originLeft + dx;
|
||||
const nextBoxTop = g.originTop + dy;
|
||||
setDraftOverlayRect({
|
||||
@@ -184,6 +279,32 @@ export function createDomEditOverlayGestureHandlers(opts: UseDomEditOverlayGestu
|
||||
if (g.pathOffsetMember) applyManualOffsetDragDraft(g.pathOffsetMember, dx, dy);
|
||||
} else {
|
||||
if (!box) return;
|
||||
|
||||
const sc = g.snapContext;
|
||||
if (sc?.snapEnabled && sc.targets.length > 0) {
|
||||
const movingRect = {
|
||||
left: g.originLeft,
|
||||
top: g.originTop,
|
||||
width: g.originWidth,
|
||||
height: g.originHeight,
|
||||
};
|
||||
const allTargets = sc.compositionTarget
|
||||
? [...sc.targets, sc.compositionTarget]
|
||||
: sc.targets;
|
||||
const snap = resolveResizeSnapAdjustment({
|
||||
movingRect,
|
||||
proposedDx: dx,
|
||||
proposedDy: dy,
|
||||
targets: allTargets,
|
||||
gridEdges: sc.gridEdges ?? undefined,
|
||||
threshold: SNAP_THRESHOLD_PX,
|
||||
disabled: e.altKey,
|
||||
});
|
||||
dx = snap.dx;
|
||||
dy = snap.dy;
|
||||
opts.snapGuidesRef.current = { guides: snap.guides, spacingGuides: [] };
|
||||
}
|
||||
|
||||
const nextSize = resolveDomEditResizeGesture({
|
||||
originWidth: g.originWidth,
|
||||
originHeight: g.originHeight,
|
||||
@@ -223,7 +344,9 @@ export function createDomEditOverlayGestureHandlers(opts: UseDomEditOverlayGestu
|
||||
}
|
||||
};
|
||||
|
||||
// fallow-ignore-next-line complexity
|
||||
const onPointerUp = (e: React.PointerEvent<HTMLDivElement>) => {
|
||||
opts.snapGuidesRef.current = null;
|
||||
const g = opts.gestureRef.current;
|
||||
const groupG = opts.groupGestureRef.current;
|
||||
const sel = g?.selection ?? opts.selectionRef.current;
|
||||
@@ -233,13 +356,15 @@ export function createDomEditOverlayGestureHandlers(opts: UseDomEditOverlayGestu
|
||||
if (groupG) {
|
||||
opts.groupGestureRef.current = null;
|
||||
opts.rafPausedRef.current = false;
|
||||
const dx = e.clientX - groupG.startX;
|
||||
const dy = e.clientY - groupG.startY;
|
||||
if (Math.hypot(dx, dy) < BLOCKED_MOVE_THRESHOLD_PX) {
|
||||
const rawDx = e.clientX - groupG.startX;
|
||||
const rawDy = e.clientY - groupG.startY;
|
||||
if (Math.hypot(rawDx, rawDy) < BLOCKED_MOVE_THRESHOLD_PX) {
|
||||
restoreGroupPathOffsets(groupG);
|
||||
opts.suppressNextBoxClickRef.current = true;
|
||||
return;
|
||||
}
|
||||
const dx = groupG.lastSnappedDx ?? rawDx;
|
||||
const dy = groupG.lastSnappedDy ?? rawDy;
|
||||
setDraftGroupOverlayItems(
|
||||
groupG.originItems.map((item) => ({
|
||||
...item,
|
||||
@@ -327,8 +452,8 @@ export function createDomEditOverlayGestureHandlers(opts: UseDomEditOverlayGestu
|
||||
})
|
||||
.finally(() => endStudioManualEditGesture(sel.element, g.manualEditDragToken));
|
||||
} else if (g.kind === "drag") {
|
||||
const dx = e.clientX - g.startX;
|
||||
const dy = e.clientY - g.startY;
|
||||
const dx = g.lastSnappedDx ?? e.clientX - g.startX;
|
||||
const dy = g.lastSnappedDy ?? e.clientY - g.startY;
|
||||
if (!g.pathOffsetMember) return;
|
||||
const finalOffset = applyManualOffsetDragCommit(g.pathOffsetMember, dx, dy);
|
||||
const nextBoxLeft = g.originLeft + dx;
|
||||
@@ -372,7 +497,9 @@ export function createDomEditOverlayGestureHandlers(opts: UseDomEditOverlayGestu
|
||||
}
|
||||
};
|
||||
|
||||
// fallow-ignore-next-line complexity
|
||||
const clearPointerState = (selectionRef: RefObject<DomEditSelection | null>) => {
|
||||
opts.snapGuidesRef.current = null;
|
||||
const groupG = opts.groupGestureRef.current;
|
||||
if (groupG) restoreGroupPathOffsets(groupG);
|
||||
const g = opts.gestureRef.current;
|
||||
|
||||
Reference in New Issue
Block a user