mirror of
https://github.com/heygen-com/hyperframes.git
synced 2026-09-03 12:54:29 +00:00
* feat(studio): add pasteboard background to preview viewport Adds bg-neutral-800 to the preview viewport so the area outside the canvas is visually distinct from the composition content — consistent with professional video editors (Premiere, DaVinci, Figma). * feat(studio): pasteboard background and canvas outline around preview - NLEPreview: viewport gets bg-neutral-700 (#404040) as the pasteboard color surrounding the canvas — distinct from the app chrome (#0a0a0a) - Player wrapper: drop bg-black so the pasteboard shows around the canvas (loading overlays still cover the area with bg-black during load) - Player: set host background to transparent via inline style (overrides :host { background: #000 } in shadow DOM), and inject a style rule into the open shadow root so .hfp-container has overflow:visible and the canvas iframe gets a thin white ring + soft drop-shadow — making the canvas boundary legible against the pasteboard * feat(studio): disable manual positioning JSON by default, add toggle Manual edits were always stored in `.hyperframes/studio-manual-edits.json`, making it hard to share source without the sidecar file and easy to accidentally reposition elements via drag. Changes: - `enabled` field added to `StudioManualEditManifest` (defaults to `false` when absent — existing projects are unaffected until they opt in) - Drag handles, resize, and rotation handles are hidden when disabled - Layout X/Y/W/H/R fields in the Design panel are read-only when disabled - "Manual positioning" toggle added at the bottom of the Design panel, visible whether or not an element is selected - Toggle state is persisted to `.hyperframes/studio-manual-edits.json` so each project can opt in independently - `STUDIO_PREVIEW_MANUAL_EDITING_ENABLED` env flag still acts as a hard cap (env off → feature off regardless of project setting) * feat(studio): enable manual positioning by default (opt-out) * feat(studio): allow absolute elements to drag without toggle; gate JSON-backed drag behind toggle * feat(studio): persist positions directly to HTML; remove JSON sidecar and manual positioning toggle Replace the `.hyperframes/studio-manual-edits.json` sidecar with inline-style persistence baked directly into the HTML source. Drag/resize/rotation values are written as CSS custom properties (`--hf-studio-offset-x/y`, `--hf-studio-width/height`, `--hf-studio-rotation`) plus `translate`/`width`/`height`/`rotate` inline styles via `persistDomEditOperations` — no re-apply step needed on load. Key changes: - `sourcePatcher`: add `value: string | null` to `PatchOperation` — null removes the property/attribute from the HTML tag instead of setting it - `manualEditsDom`: add `build*Patches` / `buildClear*Patches` helpers that capture live element state into `PatchOperation[]` for HTML source writes; add `reapplyPositionEditsAfterSeek` (DOM-query-based seek hook, queries data-attribute markers) - `manualEdits.ts`: remove `applyStudioManualEditManifest` and all manifest target resolution; export `reapplyPositionEditsAfterSeek`; keep seek/play wrap infrastructure - `useManifestPersistence`: remove all JSON I/O — no disk read on load, no manifest state, no toggle state; `applyCurrentStudioManualEditsToPreview` now only installs seek hooks via `reapplyPositionEditsAfterSeek` - `useDomEditCommits`: replace `commitStudioManualEditManifestOptimistically` calls with direct DOM apply + `commitPositionPatchToHtml` (queued HTML patch write, skipRefresh) - `DomEditOverlay`: remove `manualEditsEnabled` prop; revert all `canMove || manualEditsEnabled` gates to just `canApplyManualOffset` — every draggable element is always draggable - `PropertyPanel`: remove `ManualPositioningToggle` component and all toggle props - `manualEditsParsing/manualEditsTypes`: remove manifest types, upsert functions, and `STUDIO_MANUAL_EDITS_PATH`; keep `finiteNumber`, `readStudioFileChangePath`, `roundRotationAngle`, and snapshot/CSS-property types * fix(studio): sync keyboard shortcut handler with main; fix keepPlaying seek assertions in test * fix(studio): strip GSAP-cached translate from transform on path offset apply * fix(studio): remove Reset edits button from design panel * feat(studio): wire reloadPreview into manifest persistence; drop stale group-selection refresh - Pass `reloadPreview` into `useManifestPersistence` so undo/redo reloads via the refresh-key path instead of directly touching the iframe. - Remove `refreshDomEditGroupSelectionsFromPreview` from commit handlers; HTML is now the source of truth so no stale-ref refresh is needed. - Add `manualEditsRenderScript` helper; export via studio-api and apply it in `htmlCompiler` during HTML compilation. * fix(studio): prevent root composition from being selected; correct overlay drift on resize - Guard `getDomLayerPatchTarget` against elements with `data-composition-id` so the root composition div is never returned as a visual selection target. - Apply the same guard to the raw `elementFromPoint` fallback in `getPreviewTargetFromPointer`, which was the actual escape path. - Thread `iframeRef` into gesture handler opts; after applying draft dimensions during resize, re-read the element BCR via `toOverlayRect` and update the overlay box position to compensate for visual drift on elements with centered transform-origin (e.g. GSAP scale tweens). * fix(studio): correct resize overlay for scaled elements; block invisible element selection - Resize: use BCR from `toOverlayRect` for both position and size after applying draft dimensions — GSAP scale makes visual size diverge from raw CSS size, BCR is the only accurate source during a gesture. - Click selection: add `isElementComputedVisible` guard to the `elementFromPoint` fallback so opacity-0 / autoAlpha-hidden elements cannot be selected even though the browser hit-test returns them. * fix(studio): reload preview on external file changes via SSE/HMR Share the app-level domEditSaveTimestampRef with useManifestPersistence so the SSE/HMR handler can suppress echoes from all studio saves (code tab, timeline, DOM edits), then call reloadPreview() for non-motion external changes that aren't echoes of our own saves. * fix(studio): suppress post-resize click to keep selection on resized element * fix(studio): serve registry blocks without index.html in preview Blocks ship as {id}.html + assets/ with no index.html. The preview route hard-coded index.html so these projects returned 404 and their assets (e.g. korea-map.png, map-nyc-paris.png) were never served. Add resolveProjectMainHtml() that falls back to {id}.html, thread the resolved compositionPath through transformPreviewHtml and injectStudioPreviewAugmentations, and update listProjects() in the vite adapter to surface block directories in the project list. * fix(render): preserve studio drag/resize/rotation offsets in rendered video Three issues caused studio-edited positions to be lost during rendering: 1. The seek-reapply script used setInterval to wrap window.__hf.seek, but Puppeteer's page.evaluate() calls don't yield the event loop for macrotasks — the interval never fired, so reapplyAll() never ran after GSAP seeks. Fix: use Object.defineProperty to trap writes to the seek property, wrapping it synchronously the instant the bridge assigns it. 2. MEDIA_VISUAL_STYLE_PROPERTIES (copied from <video> to proxy <img> during render) included "transform" but not "translate", "rotate", or "scale" — the CSS Transforms Level 2 individual properties used by studio drag/resize/rotation. The proxy was positioned at offsetLeft/ offsetTop without the translate offset. 3. getViewportMatrix (HDR compositor) only read cs.transform, missing individual transform properties entirely. Added composeIndividualTransforms to build the translate × rotate × scale matrix and compose it before the legacy transform matrix. * fix(studio): select elements with pointer-events: none in preview Compositions often set pointer-events: none on scenes, avatar wrappers, and decorative layers. elementsFromPoint() skips these elements entirely, making them unselectable in the Studio. Fix: temporarily inject a * { pointer-events: auto !important } stylesheet during hit-testing, then remove it immediately after. Also adds a pointer_events_none lint rule (info severity, visible with --verbose) so authors know which selectors may affect Studio selection.
443 lines
15 KiB
TypeScript
443 lines
15 KiB
TypeScript
import { useCallback } from "react";
|
|
import { usePlayerStore } from "../player";
|
|
import { FONT_EXT } from "../utils/mediaTypes";
|
|
import { applyPatchByTarget } from "../utils/sourcePatcher";
|
|
import { saveProjectFilesWithHistory } from "../utils/studioFileHistory";
|
|
import { primaryFontFamilyValue } from "../utils/studioFontHelpers";
|
|
import { getDomEditTargetKey, type DomEditSelection } from "../components/editor/domEditing";
|
|
import {
|
|
applyStudioPathOffset,
|
|
applyStudioBoxSize,
|
|
applyStudioRotation,
|
|
clearStudioPathOffset,
|
|
clearStudioBoxSize,
|
|
clearStudioRotation,
|
|
} from "../components/editor/manualEdits";
|
|
import {
|
|
buildPathOffsetPatches,
|
|
buildBoxSizePatches,
|
|
buildRotationPatches,
|
|
buildClearPathOffsetPatches,
|
|
buildClearBoxSizePatches,
|
|
buildClearRotationPatches,
|
|
} from "../components/editor/manualEditsDom";
|
|
import {
|
|
removeStudioMotionForSelection,
|
|
type StudioGsapMotion,
|
|
type StudioMotionManifest,
|
|
upsertStudioGsapMotion,
|
|
} from "../components/editor/studioMotion";
|
|
import { fontFamilyFromAssetPath, type ImportedFontAsset } from "../components/editor/fontAssets";
|
|
import type { DomEditGroupPathOffsetCommit } from "../components/editor/DomEditOverlay";
|
|
import type { EditHistoryKind } from "../utils/editHistory";
|
|
import { useDomEditTextCommits } from "./useDomEditTextCommits";
|
|
|
|
// ── Types ──
|
|
|
|
interface RecordEditInput {
|
|
label: string;
|
|
kind: EditHistoryKind;
|
|
coalesceKey?: string;
|
|
files: Record<string, { before: string; after: string }>;
|
|
}
|
|
|
|
export type PersistDomEditOperations = (
|
|
selection: DomEditSelection,
|
|
operations: Parameters<typeof applyPatchByTarget>[2][],
|
|
options?: {
|
|
label?: string;
|
|
coalesceKey?: string;
|
|
skipRefresh?: boolean;
|
|
prepareContent?: (html: string, sourceFile: string) => string;
|
|
shouldSave?: () => boolean;
|
|
},
|
|
) => Promise<void>;
|
|
|
|
export interface UseDomEditCommitsParams {
|
|
activeCompPath: string | null;
|
|
previewIframeRef: React.MutableRefObject<HTMLIFrameElement | null>;
|
|
showToast: (message: string, tone?: "error" | "info") => void;
|
|
queueDomEditSave: (save: () => Promise<void>) => Promise<void>;
|
|
commitStudioMotionManifestOptimistically: (
|
|
updateManifest: (manifest: StudioMotionManifest) => StudioMotionManifest,
|
|
options: { label: string; coalesceKey: string },
|
|
) => void;
|
|
applyCurrentStudioMotionToPreview: (iframe: HTMLIFrameElement | null) => void;
|
|
writeProjectFile: (path: string, content: string) => Promise<void>;
|
|
domEditSaveTimestampRef: React.MutableRefObject<number>;
|
|
editHistory: { recordEdit: (entry: RecordEditInput) => Promise<void> };
|
|
fileTree: string[];
|
|
importedFontAssetsRef: React.MutableRefObject<ImportedFontAsset[]>;
|
|
projectId: string | null;
|
|
projectIdRef: React.MutableRefObject<string | null>;
|
|
reloadPreview: () => void;
|
|
|
|
// From useDomSelection
|
|
domEditSelection: DomEditSelection | null;
|
|
applyDomSelection: (
|
|
selection: DomEditSelection | null,
|
|
options?: { revealPanel?: boolean; additive?: boolean; preserveGroup?: boolean },
|
|
) => void;
|
|
clearDomSelection: () => void;
|
|
refreshDomEditSelectionFromPreview: (selection: DomEditSelection) => void;
|
|
buildDomSelectionFromTarget: (
|
|
target: HTMLElement,
|
|
options?: { preferClipAncestor?: boolean },
|
|
) => DomEditSelection | null;
|
|
}
|
|
|
|
// ── Hook ──
|
|
|
|
export function useDomEditCommits({
|
|
activeCompPath,
|
|
previewIframeRef,
|
|
showToast,
|
|
queueDomEditSave,
|
|
commitStudioMotionManifestOptimistically,
|
|
applyCurrentStudioMotionToPreview,
|
|
writeProjectFile,
|
|
domEditSaveTimestampRef,
|
|
editHistory,
|
|
fileTree,
|
|
importedFontAssetsRef,
|
|
projectId,
|
|
projectIdRef,
|
|
reloadPreview,
|
|
domEditSelection,
|
|
applyDomSelection,
|
|
clearDomSelection,
|
|
refreshDomEditSelectionFromPreview,
|
|
buildDomSelectionFromTarget,
|
|
}: UseDomEditCommitsParams) {
|
|
const resolveImportedFontAsset = useCallback(
|
|
(fontFamilyValue: string): ImportedFontAsset | null => {
|
|
const family = primaryFontFamilyValue(fontFamilyValue);
|
|
if (!family) return null;
|
|
const imported = importedFontAssetsRef.current.find(
|
|
(font) => font.family.toLowerCase() === family.toLowerCase(),
|
|
);
|
|
if (imported) return imported;
|
|
const asset = fileTree.find(
|
|
(path) =>
|
|
FONT_EXT.test(path) &&
|
|
fontFamilyFromAssetPath(path).toLowerCase() === family.toLowerCase(),
|
|
);
|
|
if (!asset) return null;
|
|
return {
|
|
family: fontFamilyFromAssetPath(asset),
|
|
path: asset,
|
|
url: `/api/projects/${projectId}/preview/${asset}`,
|
|
};
|
|
},
|
|
[fileTree, projectId, importedFontAssetsRef],
|
|
);
|
|
|
|
const persistDomEditOperations: PersistDomEditOperations = useCallback(
|
|
async (selection, operations, options) => {
|
|
const pid = projectIdRef.current;
|
|
if (!pid) throw new Error("No active project");
|
|
if (options?.shouldSave && !options.shouldSave()) return;
|
|
|
|
const targetPath = selection.sourceFile || activeCompPath || "index.html";
|
|
const response = await fetch(`/api/projects/${pid}/files/${encodeURIComponent(targetPath)}`);
|
|
if (!response.ok) {
|
|
throw new Error(`Failed to read ${targetPath}`);
|
|
}
|
|
|
|
const data = (await response.json()) as { content?: string };
|
|
const originalContent = data.content;
|
|
if (typeof originalContent !== "string") {
|
|
throw new Error(`Missing file contents for ${targetPath}`);
|
|
}
|
|
|
|
let patchedContent = originalContent;
|
|
for (const operation of operations) {
|
|
patchedContent = applyPatchByTarget(patchedContent, selection, operation);
|
|
}
|
|
if (options?.prepareContent) {
|
|
patchedContent = options.prepareContent(patchedContent, targetPath);
|
|
}
|
|
if (options?.shouldSave && !options.shouldSave()) return;
|
|
|
|
if (patchedContent === originalContent) {
|
|
throw new Error(`Unable to patch ${selection.selector ?? selection.id ?? "selection"}`);
|
|
}
|
|
|
|
await saveProjectFilesWithHistory({
|
|
projectId: pid,
|
|
label: options?.label ?? "Edit layer",
|
|
kind: "manual",
|
|
coalesceKey: options?.coalesceKey,
|
|
files: { [targetPath]: patchedContent },
|
|
readFile: async () => originalContent,
|
|
writeFile: writeProjectFile,
|
|
recordEdit: editHistory.recordEdit,
|
|
});
|
|
|
|
if (options?.skipRefresh) {
|
|
domEditSaveTimestampRef.current = Date.now();
|
|
} else {
|
|
reloadPreview();
|
|
}
|
|
},
|
|
[
|
|
activeCompPath,
|
|
editHistory.recordEdit,
|
|
writeProjectFile,
|
|
projectIdRef,
|
|
domEditSaveTimestampRef,
|
|
reloadPreview,
|
|
],
|
|
);
|
|
|
|
// ── Text & style commits (delegated to useDomEditTextCommits) ──
|
|
|
|
const {
|
|
handleDomStyleCommit,
|
|
handleDomTextCommit,
|
|
commitDomTextFields,
|
|
handleDomTextFieldStyleCommit,
|
|
handleDomAddTextField,
|
|
handleDomRemoveTextField,
|
|
} = useDomEditTextCommits({
|
|
activeCompPath,
|
|
previewIframeRef,
|
|
domEditSelection,
|
|
applyDomSelection,
|
|
refreshDomEditSelectionFromPreview,
|
|
buildDomSelectionFromTarget,
|
|
persistDomEditOperations,
|
|
resolveImportedFontAsset,
|
|
});
|
|
|
|
// ── Position patch helper ──
|
|
|
|
const commitPositionPatchToHtml = useCallback(
|
|
(
|
|
selection: DomEditSelection,
|
|
patches: Parameters<typeof applyPatchByTarget>[2][],
|
|
options: { label: string; coalesceKey: string; skipRefresh?: boolean },
|
|
) => {
|
|
void queueDomEditSave(async () => {
|
|
await persistDomEditOperations(selection, patches, {
|
|
label: options.label,
|
|
coalesceKey: options.coalesceKey,
|
|
skipRefresh: options.skipRefresh ?? true,
|
|
});
|
|
}).catch((error) => {
|
|
const message = error instanceof Error ? error.message : "Failed to save position";
|
|
showToast(message);
|
|
});
|
|
},
|
|
[persistDomEditOperations, queueDomEditSave, showToast],
|
|
);
|
|
|
|
// ── Position commits ──
|
|
|
|
const handleDomPathOffsetCommit = useCallback(
|
|
(selection: DomEditSelection, next: { x: number; y: number }) => {
|
|
applyStudioPathOffset(selection.element, next);
|
|
commitPositionPatchToHtml(selection, buildPathOffsetPatches(selection.element), {
|
|
label: "Move layer",
|
|
coalesceKey: `path-offset:${getDomEditTargetKey(selection)}`,
|
|
});
|
|
},
|
|
[commitPositionPatchToHtml],
|
|
);
|
|
|
|
const handleDomGroupPathOffsetCommit = useCallback(
|
|
(updates: DomEditGroupPathOffsetCommit[]) => {
|
|
if (updates.length === 0) return;
|
|
const coalesceKey = updates
|
|
.map((u) => getDomEditTargetKey(u.selection))
|
|
.sort()
|
|
.join(":");
|
|
for (const { selection, next } of updates) {
|
|
applyStudioPathOffset(selection.element, next);
|
|
commitPositionPatchToHtml(selection, buildPathOffsetPatches(selection.element), {
|
|
label: `Move ${updates.length} layers`,
|
|
coalesceKey: `group-path-offset:${coalesceKey}`,
|
|
});
|
|
}
|
|
},
|
|
[commitPositionPatchToHtml],
|
|
);
|
|
|
|
const handleDomBoxSizeCommit = useCallback(
|
|
(selection: DomEditSelection, next: { width: number; height: number }) => {
|
|
applyStudioBoxSize(selection.element, next);
|
|
commitPositionPatchToHtml(selection, buildBoxSizePatches(selection.element), {
|
|
label: "Resize layer box",
|
|
coalesceKey: `box-size:${getDomEditTargetKey(selection)}`,
|
|
});
|
|
},
|
|
[commitPositionPatchToHtml],
|
|
);
|
|
|
|
const handleDomRotationCommit = useCallback(
|
|
(selection: DomEditSelection, next: { angle: number }) => {
|
|
applyStudioRotation(selection.element, next);
|
|
commitPositionPatchToHtml(selection, buildRotationPatches(selection.element), {
|
|
label: "Rotate layer",
|
|
coalesceKey: `rotation:${getDomEditTargetKey(selection)}`,
|
|
});
|
|
},
|
|
[commitPositionPatchToHtml],
|
|
);
|
|
|
|
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
|
|
commitPositionPatchToHtml(selection, clearPatches, {
|
|
label: "Reset layer edits",
|
|
coalesceKey: `manual-reset:${getDomEditTargetKey(selection)}`,
|
|
skipRefresh: false,
|
|
});
|
|
},
|
|
[commitPositionPatchToHtml],
|
|
);
|
|
|
|
// ── Motion commits ──
|
|
|
|
const handleDomMotionCommit = useCallback(
|
|
(
|
|
selection: DomEditSelection,
|
|
motion: Omit<StudioGsapMotion, "kind" | "target" | "updatedAt">,
|
|
) => {
|
|
commitStudioMotionManifestOptimistically(
|
|
(manifest) => upsertStudioGsapMotion(manifest, selection, motion),
|
|
{
|
|
label: "Set GSAP motion",
|
|
coalesceKey: `motion:${getDomEditTargetKey(selection)}`,
|
|
},
|
|
);
|
|
refreshDomEditSelectionFromPreview(selection);
|
|
},
|
|
[commitStudioMotionManifestOptimistically, refreshDomEditSelectionFromPreview],
|
|
);
|
|
|
|
const handleDomMotionClear = useCallback(
|
|
(selection: DomEditSelection) => {
|
|
commitStudioMotionManifestOptimistically(
|
|
(manifest) => removeStudioMotionForSelection(manifest, selection),
|
|
{
|
|
label: "Clear GSAP motion",
|
|
coalesceKey: `motion:${getDomEditTargetKey(selection)}`,
|
|
},
|
|
);
|
|
applyCurrentStudioMotionToPreview(previewIframeRef.current);
|
|
refreshDomEditSelectionFromPreview(selection);
|
|
},
|
|
[
|
|
applyCurrentStudioMotionToPreview,
|
|
commitStudioMotionManifestOptimistically,
|
|
refreshDomEditSelectionFromPreview,
|
|
previewIframeRef,
|
|
],
|
|
);
|
|
|
|
const handleDomEditElementDelete = useCallback(
|
|
async (selection: DomEditSelection) => {
|
|
const pid = projectIdRef.current;
|
|
if (!pid) return;
|
|
const label = selection.label || selection.id || selection.selector || selection.tagName;
|
|
|
|
const targetPath = selection.sourceFile || activeCompPath || "index.html";
|
|
try {
|
|
const response = await fetch(
|
|
`/api/projects/${pid}/files/${encodeURIComponent(targetPath)}`,
|
|
);
|
|
if (!response.ok) throw new Error(`Failed to read ${targetPath}`);
|
|
|
|
const data = (await response.json()) as { content?: string };
|
|
const originalContent = data.content;
|
|
if (typeof originalContent !== "string")
|
|
throw new Error(`Missing file contents for ${targetPath}`);
|
|
|
|
const patchTarget: { id?: string; selector?: string; selectorIndex?: number } = selection.id
|
|
? {
|
|
id: selection.id,
|
|
selector: selection.selector,
|
|
selectorIndex: selection.selectorIndex,
|
|
}
|
|
: selection.selector
|
|
? { selector: selection.selector, selectorIndex: selection.selectorIndex }
|
|
: ({} as never);
|
|
if (!patchTarget.id && !patchTarget.selector) {
|
|
throw new Error("Selected element has no patchable target");
|
|
}
|
|
|
|
const removeResponse = await fetch(
|
|
`/api/projects/${pid}/file-mutations/remove-element/${encodeURIComponent(targetPath)}`,
|
|
{
|
|
method: "POST",
|
|
headers: { "Content-Type": "application/json" },
|
|
body: JSON.stringify({ target: patchTarget }),
|
|
},
|
|
);
|
|
if (!removeResponse.ok) throw new Error(`Failed to delete element from ${targetPath}`);
|
|
|
|
const removeData = (await removeResponse.json()) as { changed?: boolean; content?: string };
|
|
const patchedContent =
|
|
typeof removeData.content === "string" ? removeData.content : originalContent;
|
|
|
|
domEditSaveTimestampRef.current = Date.now();
|
|
await saveProjectFilesWithHistory({
|
|
projectId: pid,
|
|
label: "Delete element",
|
|
kind: "timeline",
|
|
files: { [targetPath]: patchedContent },
|
|
readFile: async () => originalContent,
|
|
writeFile: writeProjectFile,
|
|
recordEdit: editHistory.recordEdit,
|
|
});
|
|
|
|
clearDomSelection();
|
|
usePlayerStore.getState().setSelectedElementId(null);
|
|
reloadPreview();
|
|
showToast(`Deleted ${label}. Use Undo to restore it.`, "info");
|
|
} catch (error) {
|
|
const message = error instanceof Error ? error.message : "Failed to delete element";
|
|
showToast(message);
|
|
}
|
|
},
|
|
[
|
|
activeCompPath,
|
|
clearDomSelection,
|
|
domEditSaveTimestampRef,
|
|
editHistory.recordEdit,
|
|
projectIdRef,
|
|
reloadPreview,
|
|
showToast,
|
|
writeProjectFile,
|
|
],
|
|
);
|
|
|
|
return {
|
|
resolveImportedFontAsset,
|
|
handleDomStyleCommit,
|
|
handleDomTextCommit,
|
|
commitDomTextFields,
|
|
handleDomTextFieldStyleCommit,
|
|
handleDomAddTextField,
|
|
handleDomRemoveTextField,
|
|
handleDomPathOffsetCommit,
|
|
handleDomGroupPathOffsetCommit,
|
|
handleDomBoxSizeCommit,
|
|
handleDomRotationCommit,
|
|
handleDomManualEditsReset,
|
|
handleDomMotionCommit,
|
|
handleDomMotionClear,
|
|
handleDomEditElementDelete,
|
|
};
|
|
}
|