mirror of
https://github.com/heygen-com/hyperframes.git
synced 2026-09-05 17:30:50 +00:00
* fix(studio): make inspector commits transactional * fix(studio): make inspector persistence atomic * fix(studio): preserve synchronous gesture semantics
158 lines
6.1 KiB
TypeScript
158 lines
6.1 KiB
TypeScript
import { useCallback } from "react";
|
|
import { getDomEditTargetKey, type DomEditSelection } from "../components/editor/domEditing";
|
|
import {
|
|
applyStudioPathOffset,
|
|
applyStudioBoxSize,
|
|
applyStudioRotation,
|
|
captureStudioPathOffset,
|
|
captureStudioBoxSize,
|
|
captureStudioRotation,
|
|
restoreStudioPathOffset,
|
|
restoreStudioBoxSize,
|
|
restoreStudioRotation,
|
|
clearStudioPathOffset,
|
|
clearStudioBoxSize,
|
|
clearStudioRotation,
|
|
} from "../components/editor/manualEdits";
|
|
import {
|
|
buildPathOffsetPatches,
|
|
buildBoxSizePatches,
|
|
buildRotationPatches,
|
|
buildClearPathOffsetPatches,
|
|
buildClearBoxSizePatches,
|
|
buildClearRotationPatches,
|
|
} from "../components/editor/manualEditsDomPatches";
|
|
import type { PatchOperation } from "../utils/sourcePatcher";
|
|
import { isElementGsapTargeted } from "./gsapTargetCache";
|
|
|
|
const GSAP_CSS_FALLBACK_BLOCKED_MESSAGE =
|
|
"This element is GSAP-animated — dragging via CSS would corrupt keyframes";
|
|
|
|
// ── Hook ──
|
|
|
|
interface UseDomGeometryCommitsParams {
|
|
previewIframeRef: React.MutableRefObject<HTMLIFrameElement | null>;
|
|
showToast: (message: string, tone?: "error" | "info") => void;
|
|
commitPositionPatchToHtml: (
|
|
selection: DomEditSelection,
|
|
patches: PatchOperation[],
|
|
options: { label: string; coalesceKey: string; skipRefresh?: boolean },
|
|
) => Promise<void>;
|
|
}
|
|
|
|
export function useDomGeometryCommits({
|
|
previewIframeRef,
|
|
showToast,
|
|
commitPositionPatchToHtml,
|
|
}: UseDomGeometryCommitsParams) {
|
|
const handleDomPathOffsetCommit = useCallback(
|
|
(selection: DomEditSelection, next: { x: number; y: number }) => {
|
|
// ponytail: GSAP-targeted elements are blocked (no SDK position-in-script op); CSS-path
|
|
// elements fall through to commitPositionPatchToHtml → persistDomEditOperations →
|
|
// onTrySdkPersist and are already SDK-cut-over as setStyle/setAttribute (§3.3 done).
|
|
// Upgrade path for GSAP: add a moveElementGsap SDK op in a separate SDK PR.
|
|
const gsapTargeted = isElementGsapTargeted(previewIframeRef.current, selection.element);
|
|
if (gsapTargeted) {
|
|
const error = new Error(GSAP_CSS_FALLBACK_BLOCKED_MESSAGE);
|
|
showToast(error.message, "error");
|
|
return Promise.reject(error);
|
|
}
|
|
const before = captureStudioPathOffset(selection.element);
|
|
applyStudioPathOffset(selection.element, next);
|
|
return commitPositionPatchToHtml(selection, buildPathOffsetPatches(selection.element), {
|
|
label: "Move layer",
|
|
coalesceKey: `path-offset:${getDomEditTargetKey(selection)}`,
|
|
}).catch((error) => {
|
|
restoreStudioPathOffset(selection.element, before);
|
|
throw error;
|
|
});
|
|
},
|
|
[commitPositionPatchToHtml, previewIframeRef, showToast],
|
|
);
|
|
|
|
const handleDomBoxSizeCommit = useCallback(
|
|
(
|
|
selection: DomEditSelection,
|
|
next: { width: number; height: number },
|
|
offset?: { x: number; y: number },
|
|
) => {
|
|
if (isElementGsapTargeted(previewIframeRef.current, selection.element)) {
|
|
const error = new Error(GSAP_CSS_FALLBACK_BLOCKED_MESSAGE);
|
|
showToast(error.message, "error");
|
|
return Promise.reject(error);
|
|
}
|
|
const beforeSize = captureStudioBoxSize(selection.element);
|
|
const beforeOffset = offset ? captureStudioPathOffset(selection.element) : null;
|
|
applyStudioBoxSize(selection.element, next);
|
|
// Anchored-corner resize (NW/NE/SW) also moves the element to keep the
|
|
// opposite corner fixed. Apply the offset and emit BOTH patch sets in a
|
|
// SINGLE commit: one persist = one undo entry, and there is no
|
|
// intermediate re-stamp where the new size is in source but the anchor
|
|
// offset is not (that frame was the release "jump"). Both builders read
|
|
// the already-mutated live element, so concatenation is safe.
|
|
const patches = buildBoxSizePatches(selection.element);
|
|
if (offset) {
|
|
applyStudioPathOffset(selection.element, offset);
|
|
patches.push(...buildPathOffsetPatches(selection.element));
|
|
}
|
|
return commitPositionPatchToHtml(selection, patches, {
|
|
label: "Resize layer box",
|
|
coalesceKey: `box-size:${getDomEditTargetKey(selection)}`,
|
|
}).catch((error) => {
|
|
restoreStudioBoxSize(selection.element, beforeSize);
|
|
if (beforeOffset) restoreStudioPathOffset(selection.element, beforeOffset);
|
|
throw error;
|
|
});
|
|
},
|
|
[commitPositionPatchToHtml, previewIframeRef, showToast],
|
|
);
|
|
|
|
const handleDomRotationCommit = useCallback(
|
|
(selection: DomEditSelection, next: { angle: number }) => {
|
|
if (isElementGsapTargeted(previewIframeRef.current, selection.element)) {
|
|
const error = new Error(GSAP_CSS_FALLBACK_BLOCKED_MESSAGE);
|
|
showToast(error.message, "error");
|
|
return Promise.reject(error);
|
|
}
|
|
const before = captureStudioRotation(selection.element);
|
|
applyStudioRotation(selection.element, next);
|
|
return commitPositionPatchToHtml(selection, buildRotationPatches(selection.element), {
|
|
label: "Rotate layer",
|
|
coalesceKey: `rotation:${getDomEditTargetKey(selection)}`,
|
|
}).catch((error) => {
|
|
restoreStudioRotation(selection.element, before);
|
|
throw error;
|
|
});
|
|
},
|
|
[commitPositionPatchToHtml, previewIframeRef, showToast],
|
|
);
|
|
|
|
const handleDomManualEditsReset = useCallback(
|
|
(selection: DomEditSelection) => {
|
|
const element = selection.element;
|
|
const clearPatches = [
|
|
...buildClearPathOffsetPatches(element),
|
|
...buildClearBoxSizePatches(element),
|
|
...buildClearRotationPatches(element),
|
|
];
|
|
clearStudioPathOffset(element);
|
|
clearStudioBoxSize(element);
|
|
clearStudioRotation(element);
|
|
// skipRefresh:false triggers reloadPreview() which re-syncs selection on load
|
|
void commitPositionPatchToHtml(selection, clearPatches, {
|
|
label: "Reset layer edits",
|
|
coalesceKey: `manual-reset:${getDomEditTargetKey(selection)}`,
|
|
skipRefresh: false,
|
|
}).catch(() => undefined);
|
|
},
|
|
[commitPositionPatchToHtml],
|
|
);
|
|
|
|
return {
|
|
handleDomPathOffsetCommit,
|
|
handleDomBoxSizeCommit,
|
|
handleDomRotationCommit,
|
|
handleDomManualEditsReset,
|
|
};
|
|
}
|