mirror of
https://github.com/heygen-com/hyperframes.git
synced 2026-09-07 10:06:21 +00:00
* refactor: split useTimelinePlayer.ts and hyperframes-player.ts into focused modules (<500 LOC each) * fix(ci): scope 500 LOC check to packages/studio, add allowlist for grandfathered files * feat(cli): Linux ARM64 support — auto-install Chromium on DGX Spark / GB10 / Jetson Chrome Headless Shell has no Linux ARM64 binary. On arm64 Linux: - Detects the platform automatically - Tries to auto-install system Chromium via apt-get (works on Ubuntu/Debian ARM) - Falls back to clear manual instructions with exact commands - 'hyperframes browser ensure' guides through the setup interactively - After setup, all render commands work without any flags * fix(ci): disable Windows Defender real-time monitoring to prevent EPERM builds Path exclusions are insufficient — Defender re-scans new files created during bun install before the exclusion takes effect. Disable real-time monitoring for the entire job duration instead (standard CI practice). * refactor(studio): split all files >500 LOC + extract useToast, delete allowlist All 11 large files split into focused modules under 500 LOC. App.tsx extracted toast logic into useToast hook (493 LOC now). .filesize-allowlist deleted — no longer needed. * fix: remove unused imports from split files, extract useToast from App.tsx App.tsx: 504 → 493 lines (toast logic extracted to useToast hook) timelineDOM.ts: remove unused imports from re-export pattern MotionPanel.tsx: remove unused clampStudioCustomEasePoints import studioMotionOps.ts: remove unused StudioGsapMotionDirection import * fix(ci): use Set-MpPreference to fully disable Windows Defender (both jobs) * fix(producer): use node --experimental-strip-types instead of tsx for build:fonts Eliminates the tsx binary dependency that Windows Defender locks during bun install, causing EPERM errors. Node 22.6+ strips TypeScript types natively with no external binary. * chore: remove .filesize-allowlist — App.tsx is now 493 lines (<500) * fix(ci): disable Windows Defender before checkout to prevent all EPERM races * fix(producer): skip build:fonts if fontData.generated.ts already exists The generated file is tracked in git, so CI doesn't need to regenerate it. This avoids @fontsource/inter node_modules access on Windows which triggers EPERM from Defender scanning during bun install.
437 lines
17 KiB
TypeScript
437 lines
17 KiB
TypeScript
import {
|
|
STUDIO_OFFSET_X_PROP,
|
|
STUDIO_OFFSET_Y_PROP,
|
|
STUDIO_WIDTH_PROP,
|
|
STUDIO_HEIGHT_PROP,
|
|
STUDIO_ROTATION_PROP,
|
|
STUDIO_PATH_OFFSET_ATTR,
|
|
STUDIO_MANUAL_EDIT_GESTURE_ATTR,
|
|
STUDIO_BOX_SIZE_ATTR,
|
|
STUDIO_ROTATION_ATTR,
|
|
STUDIO_ORIGINAL_TRANSLATE_ATTR,
|
|
STUDIO_ORIGINAL_INLINE_TRANSLATE_ATTR,
|
|
STUDIO_ORIGINAL_WIDTH_ATTR,
|
|
STUDIO_ORIGINAL_HEIGHT_ATTR,
|
|
STUDIO_ORIGINAL_MIN_WIDTH_ATTR,
|
|
STUDIO_ORIGINAL_MIN_HEIGHT_ATTR,
|
|
STUDIO_ORIGINAL_MAX_WIDTH_ATTR,
|
|
STUDIO_ORIGINAL_MAX_HEIGHT_ATTR,
|
|
STUDIO_ORIGINAL_FLEX_BASIS_ATTR,
|
|
STUDIO_ORIGINAL_FLEX_GROW_ATTR,
|
|
STUDIO_ORIGINAL_FLEX_SHRINK_ATTR,
|
|
STUDIO_ORIGINAL_BOX_SIZING_ATTR,
|
|
STUDIO_ORIGINAL_SCALE_ATTR,
|
|
STUDIO_ORIGINAL_TRANSFORM_ORIGIN_ATTR,
|
|
STUDIO_ORIGINAL_DISPLAY_ATTR,
|
|
STUDIO_ORIGINAL_ROTATE_ATTR,
|
|
STUDIO_ORIGINAL_INLINE_ROTATE_ATTR,
|
|
STUDIO_ORIGINAL_ROTATION_TRANSFORM_ORIGIN_ATTR,
|
|
STUDIO_ROTATION_DRAFT_ATTR,
|
|
STUDIO_ORIGINAL_TRANSFORM_DISPLAY_ATTR,
|
|
STUDIO_ROTATION_TRANSFORM_ORIGIN,
|
|
} from "./manualEditsTypes";
|
|
import { roundRotationAngle } from "./manualEditsParsing";
|
|
|
|
/* ── Gesture tracking ─────────────────────────────────────────────── */
|
|
let studioManualEditGestureId = 0;
|
|
|
|
export function beginStudioManualEditGesture(element: HTMLElement): string {
|
|
studioManualEditGestureId += 1;
|
|
const token = `gesture-${studioManualEditGestureId}`;
|
|
element.setAttribute(STUDIO_MANUAL_EDIT_GESTURE_ATTR, token);
|
|
return token;
|
|
}
|
|
|
|
export function endStudioManualEditGesture(element: HTMLElement, token?: string): void {
|
|
if (token && element.getAttribute(STUDIO_MANUAL_EDIT_GESTURE_ATTR) !== token) return;
|
|
element.removeAttribute(STUDIO_MANUAL_EDIT_GESTURE_ATTR);
|
|
}
|
|
|
|
export function isStudioManualEditGestureActive(element: HTMLElement): boolean {
|
|
return element.hasAttribute(STUDIO_MANUAL_EDIT_GESTURE_ATTR);
|
|
}
|
|
|
|
export function isStudioManualEditGestureCurrent(element: HTMLElement, token: string): boolean {
|
|
return element.getAttribute(STUDIO_MANUAL_EDIT_GESTURE_ATTR) === token;
|
|
}
|
|
|
|
/* ── CSS custom-property readers ──────────────────────────────────── */
|
|
function readPxCustomProperty(element: HTMLElement, property: string): number {
|
|
const value = Number.parseFloat(element.style.getPropertyValue(property));
|
|
return Number.isFinite(value) ? value : 0;
|
|
}
|
|
|
|
export function readStudioPathOffset(element: HTMLElement): { x: number; y: number } {
|
|
return {
|
|
x: readPxCustomProperty(element, STUDIO_OFFSET_X_PROP),
|
|
y: readPxCustomProperty(element, STUDIO_OFFSET_Y_PROP),
|
|
};
|
|
}
|
|
|
|
export function readStudioBoxSize(element: HTMLElement): { width: number; height: number } {
|
|
return {
|
|
width: readPxCustomProperty(element, STUDIO_WIDTH_PROP),
|
|
height: readPxCustomProperty(element, STUDIO_HEIGHT_PROP),
|
|
};
|
|
}
|
|
|
|
export function readStudioRotation(element: HTMLElement): { angle: number } {
|
|
const value = Number.parseFloat(element.style.getPropertyValue(STUDIO_ROTATION_PROP));
|
|
return { angle: Number.isFinite(value) ? value : 0 };
|
|
}
|
|
|
|
/* ── Internal style helpers ───────────────────────────────────────── */
|
|
function safeComputedStyleProperty(element: HTMLElement, property: string): string {
|
|
try {
|
|
return (
|
|
element.ownerDocument.defaultView?.getComputedStyle(element).getPropertyValue(property) ?? ""
|
|
);
|
|
} catch {
|
|
return "";
|
|
}
|
|
}
|
|
|
|
function readStyleOrComputed(element: HTMLElement, property: string): string {
|
|
return element.style.getPropertyValue(property) || safeComputedStyleProperty(element, property);
|
|
}
|
|
|
|
function readTransformLonghandBase(element: HTMLElement, property: "translate" | "rotate"): string {
|
|
const value = readStyleOrComputed(element, property).trim();
|
|
return value === "none" ? "" : value;
|
|
}
|
|
|
|
export function styleUsesStudioOffset(value: string): boolean {
|
|
return value.includes(STUDIO_OFFSET_X_PROP) || value.includes(STUDIO_OFFSET_Y_PROP);
|
|
}
|
|
|
|
export function styleUsesStudioSize(value: string): boolean {
|
|
return value.includes(STUDIO_WIDTH_PROP) || value.includes(STUDIO_HEIGHT_PROP);
|
|
}
|
|
|
|
export function styleUsesStudioRotation(value: string): boolean {
|
|
return value.includes(STUDIO_ROTATION_PROP);
|
|
}
|
|
|
|
function compactStyleValue(value: string): string {
|
|
return value.replace(/\s+/g, "").toLowerCase();
|
|
}
|
|
|
|
function styleMatchesStudioRotationDraft(element: HTMLElement, value: string): boolean {
|
|
if (!element.hasAttribute(STUDIO_ROTATION_DRAFT_ATTR)) return false;
|
|
const rotation = element.style.getPropertyValue(STUDIO_ROTATION_PROP).trim();
|
|
if (!rotation || !value.trim()) return false;
|
|
return (
|
|
compactStyleValue(value) === compactStyleValue(composeStudioRotationValue(element, rotation))
|
|
);
|
|
}
|
|
|
|
/* ── Inline promotion ─────────────────────────────────────────────── */
|
|
function promoteInlineForTransform(element: HTMLElement): void {
|
|
const computedDisplay = safeComputedStyleProperty(element, "display");
|
|
if (computedDisplay !== "inline") return;
|
|
if (!element.hasAttribute(STUDIO_ORIGINAL_TRANSFORM_DISPLAY_ATTR)) {
|
|
element.setAttribute(
|
|
STUDIO_ORIGINAL_TRANSFORM_DISPLAY_ATTR,
|
|
element.style.getPropertyValue("display"),
|
|
);
|
|
}
|
|
element.style.setProperty("display", "inline-block");
|
|
}
|
|
|
|
export function restoreInlineDisplay(element: HTMLElement): void {
|
|
const original = element.getAttribute(STUDIO_ORIGINAL_TRANSFORM_DISPLAY_ATTR);
|
|
if (original == null) return;
|
|
if (original === "") element.style.removeProperty("display");
|
|
else element.style.setProperty("display", original);
|
|
element.removeAttribute(STUDIO_ORIGINAL_TRANSFORM_DISPLAY_ATTR);
|
|
}
|
|
|
|
/* ── Translate helpers ────────────────────────────────────────────── */
|
|
function splitTopLevelWhitespace(value: string): string[] {
|
|
const parts: string[] = [];
|
|
let depth = 0;
|
|
let current = "";
|
|
for (const char of value.trim()) {
|
|
if (char === "(") depth += 1;
|
|
if (char === ")") depth = Math.max(0, depth - 1);
|
|
if (/\s/.test(char) && depth === 0) {
|
|
if (current) parts.push(current);
|
|
current = "";
|
|
} else {
|
|
current += char;
|
|
}
|
|
}
|
|
if (current) parts.push(current);
|
|
return parts;
|
|
}
|
|
|
|
function composeTranslateValue(element: HTMLElement, x: string, y: string): string {
|
|
const original = element.getAttribute(STUDIO_ORIGINAL_TRANSLATE_ATTR)?.trim();
|
|
if (!original || original === "none") return `${x} ${y}`;
|
|
|
|
const parts = splitTopLevelWhitespace(original);
|
|
if (parts.length === 1) return `calc(${parts[0]} + ${x}) ${y}`;
|
|
if (parts.length === 2) return `calc(${parts[0]} + ${x}) calc(${parts[1]} + ${y})`;
|
|
if (parts.length === 3) {
|
|
return `calc(${parts[0]} + ${x}) calc(${parts[1]} + ${y}) ${parts[2]}`;
|
|
}
|
|
return `${x} ${y}`;
|
|
}
|
|
|
|
function prepareStudioPathOffsetBase(element: HTMLElement, updateBase: boolean): void {
|
|
const inlineTranslate = element.style.getPropertyValue("translate");
|
|
const currentTranslate = readTransformLonghandBase(element, "translate");
|
|
const hasMarker = element.hasAttribute(STUDIO_PATH_OFFSET_ATTR);
|
|
const wasResetByAnimation = !styleUsesStudioOffset(currentTranslate);
|
|
if (!hasMarker) {
|
|
element.setAttribute(
|
|
STUDIO_ORIGINAL_INLINE_TRANSLATE_ATTR,
|
|
styleUsesStudioOffset(inlineTranslate) ? "" : inlineTranslate,
|
|
);
|
|
element.setAttribute(
|
|
STUDIO_ORIGINAL_TRANSLATE_ATTR,
|
|
wasResetByAnimation ? currentTranslate : "",
|
|
);
|
|
} else if (updateBase && wasResetByAnimation && !isStudioManualEditGestureActive(element)) {
|
|
element.setAttribute(STUDIO_ORIGINAL_TRANSLATE_ATTR, currentTranslate);
|
|
}
|
|
}
|
|
|
|
function writeStudioPathOffsetVars(
|
|
element: HTMLElement,
|
|
offset: { x: number; y: number },
|
|
options: { updateBase?: boolean } = {},
|
|
): void {
|
|
prepareStudioPathOffsetBase(element, options.updateBase ?? true);
|
|
element.setAttribute(STUDIO_PATH_OFFSET_ATTR, "true");
|
|
element.style.setProperty(STUDIO_OFFSET_X_PROP, `${Math.round(offset.x)}px`);
|
|
element.style.setProperty(STUDIO_OFFSET_Y_PROP, `${Math.round(offset.y)}px`);
|
|
}
|
|
|
|
/* ── Path offset apply ────────────────────────────────────────────── */
|
|
export function applyStudioPathOffset(
|
|
element: HTMLElement,
|
|
offset: { x: number; y: number },
|
|
): void {
|
|
promoteInlineForTransform(element);
|
|
writeStudioPathOffsetVars(element, offset);
|
|
element.style.setProperty(
|
|
"translate",
|
|
composeTranslateValue(
|
|
element,
|
|
`var(${STUDIO_OFFSET_X_PROP}, 0px)`,
|
|
`var(${STUDIO_OFFSET_Y_PROP}, 0px)`,
|
|
),
|
|
);
|
|
}
|
|
|
|
export function applyStudioPathOffsetDraft(
|
|
element: HTMLElement,
|
|
offset: { x: number; y: number },
|
|
): void {
|
|
promoteInlineForTransform(element);
|
|
writeStudioPathOffsetVars(element, offset, { updateBase: false });
|
|
element.style.setProperty(
|
|
"translate",
|
|
composeTranslateValue(element, `${Math.round(offset.x)}px`, `${Math.round(offset.y)}px`),
|
|
);
|
|
}
|
|
|
|
/* ── Box size apply ───────────────────────────────────────────────── */
|
|
function readParentFlexBasisPixels(
|
|
element: HTMLElement,
|
|
size: { width: number; height: number },
|
|
): number | null {
|
|
const parent = element.parentElement;
|
|
if (!parent) return null;
|
|
|
|
const display = readStyleOrComputed(parent, "display").trim();
|
|
if (display !== "flex" && display !== "inline-flex") return null;
|
|
|
|
const direction = readStyleOrComputed(parent, "flex-direction").trim();
|
|
return Math.round(Math.max(1, direction.startsWith("column") ? size.height : size.width));
|
|
}
|
|
|
|
function restoreStaleStudioScaleResize(element: HTMLElement): void {
|
|
if (!element.hasAttribute(STUDIO_ORIGINAL_SCALE_ATTR)) return;
|
|
const origScale = element.getAttribute(STUDIO_ORIGINAL_SCALE_ATTR);
|
|
if (origScale == null || origScale === "") element.style.removeProperty("scale");
|
|
else element.style.setProperty("scale", origScale);
|
|
element.removeAttribute(STUDIO_ORIGINAL_SCALE_ATTR);
|
|
const origOrigin = element.getAttribute(STUDIO_ORIGINAL_TRANSFORM_ORIGIN_ATTR);
|
|
if (origOrigin == null || origOrigin === "") element.style.removeProperty("transform-origin");
|
|
else element.style.setProperty("transform-origin", origOrigin);
|
|
element.removeAttribute(STUDIO_ORIGINAL_TRANSFORM_ORIGIN_ATTR);
|
|
}
|
|
|
|
function writeStudioBoxSizeVars(
|
|
element: HTMLElement,
|
|
size: { width: number; height: number },
|
|
): void {
|
|
if (!element.hasAttribute(STUDIO_BOX_SIZE_ATTR)) {
|
|
element.setAttribute(STUDIO_ORIGINAL_WIDTH_ATTR, element.style.getPropertyValue("width"));
|
|
element.setAttribute(STUDIO_ORIGINAL_HEIGHT_ATTR, element.style.getPropertyValue("height"));
|
|
element.setAttribute(
|
|
STUDIO_ORIGINAL_MIN_WIDTH_ATTR,
|
|
element.style.getPropertyValue("min-width"),
|
|
);
|
|
element.setAttribute(
|
|
STUDIO_ORIGINAL_MIN_HEIGHT_ATTR,
|
|
element.style.getPropertyValue("min-height"),
|
|
);
|
|
element.setAttribute(
|
|
STUDIO_ORIGINAL_MAX_WIDTH_ATTR,
|
|
element.style.getPropertyValue("max-width"),
|
|
);
|
|
element.setAttribute(
|
|
STUDIO_ORIGINAL_MAX_HEIGHT_ATTR,
|
|
element.style.getPropertyValue("max-height"),
|
|
);
|
|
element.setAttribute(
|
|
STUDIO_ORIGINAL_FLEX_BASIS_ATTR,
|
|
element.style.getPropertyValue("flex-basis"),
|
|
);
|
|
element.setAttribute(
|
|
STUDIO_ORIGINAL_FLEX_GROW_ATTR,
|
|
element.style.getPropertyValue("flex-grow"),
|
|
);
|
|
element.setAttribute(
|
|
STUDIO_ORIGINAL_FLEX_SHRINK_ATTR,
|
|
element.style.getPropertyValue("flex-shrink"),
|
|
);
|
|
element.setAttribute(
|
|
STUDIO_ORIGINAL_BOX_SIZING_ATTR,
|
|
element.style.getPropertyValue("box-sizing"),
|
|
);
|
|
element.setAttribute(STUDIO_ORIGINAL_SCALE_ATTR, element.style.getPropertyValue("scale"));
|
|
element.setAttribute(
|
|
STUDIO_ORIGINAL_TRANSFORM_ORIGIN_ATTR,
|
|
element.style.getPropertyValue("transform-origin"),
|
|
);
|
|
element.setAttribute(STUDIO_ORIGINAL_DISPLAY_ATTR, element.style.getPropertyValue("display"));
|
|
}
|
|
|
|
element.setAttribute(STUDIO_BOX_SIZE_ATTR, "true");
|
|
element.style.setProperty(STUDIO_WIDTH_PROP, `${Math.round(Math.max(1, size.width))}px`);
|
|
element.style.setProperty(STUDIO_HEIGHT_PROP, `${Math.round(Math.max(1, size.height))}px`);
|
|
}
|
|
|
|
function applyStudioBoxSizeDimensions(
|
|
element: HTMLElement,
|
|
size: { width: number; height: number },
|
|
): void {
|
|
writeStudioBoxSizeVars(element, size);
|
|
restoreStaleStudioScaleResize(element);
|
|
|
|
const width = Math.round(Math.max(1, size.width));
|
|
const height = Math.round(Math.max(1, size.height));
|
|
element.style.setProperty("box-sizing", "border-box");
|
|
element.style.setProperty("width", `${width}px`);
|
|
element.style.setProperty("height", `${height}px`);
|
|
element.style.setProperty("min-width", "0px");
|
|
element.style.setProperty("min-height", "0px");
|
|
element.style.setProperty("max-width", "none");
|
|
element.style.setProperty("max-height", "none");
|
|
const flexBasis = readParentFlexBasisPixels(element, size);
|
|
if (flexBasis != null) {
|
|
element.style.setProperty("flex-basis", `${flexBasis}px`);
|
|
element.style.setProperty("flex-grow", "0");
|
|
element.style.setProperty("flex-shrink", "0");
|
|
}
|
|
const computedDisplay = safeComputedStyleProperty(element, "display");
|
|
if (computedDisplay === "inline") {
|
|
element.style.setProperty("display", "inline-block");
|
|
}
|
|
}
|
|
|
|
export function applyStudioBoxSize(
|
|
element: HTMLElement,
|
|
size: { width: number; height: number },
|
|
): void {
|
|
promoteInlineForTransform(element);
|
|
applyStudioBoxSizeDimensions(element, size);
|
|
}
|
|
|
|
export function applyStudioBoxSizeDraft(
|
|
element: HTMLElement,
|
|
size: { width: number; height: number },
|
|
): void {
|
|
promoteInlineForTransform(element);
|
|
applyStudioBoxSizeDimensions(element, size);
|
|
}
|
|
|
|
/* ── Rotation apply ───────────────────────────────────────────────── */
|
|
function isSimpleRotateAngle(value: string): boolean {
|
|
return /^-?(?:\d+(?:\.\d+)?|\.\d+)(?:deg|rad|turn|grad)$/.test(value.trim());
|
|
}
|
|
|
|
function composeStudioRotationValue(element: HTMLElement, rotationValue: string): string {
|
|
const original = element.getAttribute(STUDIO_ORIGINAL_ROTATE_ATTR)?.trim();
|
|
if (!original || original === "none" || !isSimpleRotateAngle(original)) {
|
|
return rotationValue;
|
|
}
|
|
return `calc(${original} + ${rotationValue})`;
|
|
}
|
|
|
|
function prepareStudioRotationBase(element: HTMLElement, updateBase: boolean): void {
|
|
const inlineRotate = element.style.getPropertyValue("rotate");
|
|
const currentRotate = readTransformLonghandBase(element, "rotate");
|
|
const hasMarker = element.hasAttribute(STUDIO_ROTATION_ATTR);
|
|
const wasResetByAnimation =
|
|
!styleUsesStudioRotation(currentRotate) &&
|
|
!styleMatchesStudioRotationDraft(element, currentRotate);
|
|
if (!hasMarker) {
|
|
element.setAttribute(
|
|
STUDIO_ORIGINAL_INLINE_ROTATE_ATTR,
|
|
styleUsesStudioRotation(inlineRotate) ? "" : inlineRotate,
|
|
);
|
|
element.setAttribute(STUDIO_ORIGINAL_ROTATE_ATTR, wasResetByAnimation ? currentRotate : "");
|
|
} else if (updateBase && wasResetByAnimation && !isStudioManualEditGestureActive(element)) {
|
|
element.setAttribute(STUDIO_ORIGINAL_ROTATE_ATTR, currentRotate);
|
|
}
|
|
if (!element.hasAttribute(STUDIO_ORIGINAL_ROTATION_TRANSFORM_ORIGIN_ATTR)) {
|
|
element.setAttribute(
|
|
STUDIO_ORIGINAL_ROTATION_TRANSFORM_ORIGIN_ATTR,
|
|
element.style.getPropertyValue("transform-origin"),
|
|
);
|
|
}
|
|
}
|
|
|
|
function writeStudioRotationVars(
|
|
element: HTMLElement,
|
|
rotation: { angle: number },
|
|
options: { updateBase?: boolean } = {},
|
|
): void {
|
|
prepareStudioRotationBase(element, options.updateBase ?? true);
|
|
element.setAttribute(STUDIO_ROTATION_ATTR, "true");
|
|
element.style.setProperty(STUDIO_ROTATION_PROP, `${roundRotationAngle(rotation.angle)}deg`);
|
|
element.style.setProperty("transform-origin", STUDIO_ROTATION_TRANSFORM_ORIGIN);
|
|
}
|
|
|
|
export function applyStudioRotation(element: HTMLElement, rotation: { angle: number }): void {
|
|
promoteInlineForTransform(element);
|
|
writeStudioRotationVars(element, rotation);
|
|
element.removeAttribute(STUDIO_ROTATION_DRAFT_ATTR);
|
|
element.style.setProperty(
|
|
"rotate",
|
|
composeStudioRotationValue(element, `var(${STUDIO_ROTATION_PROP}, 0deg)`),
|
|
);
|
|
}
|
|
|
|
export function applyStudioRotationDraft(element: HTMLElement, rotation: { angle: number }): void {
|
|
promoteInlineForTransform(element);
|
|
writeStudioRotationVars(element, rotation, { updateBase: false });
|
|
element.setAttribute(STUDIO_ROTATION_DRAFT_ATTR, "true");
|
|
element.style.setProperty(
|
|
"rotate",
|
|
composeStudioRotationValue(element, `${roundRotationAngle(rotation.angle)}deg`),
|
|
);
|
|
}
|
|
|
|
// Clear functions live in manualEditsSnapshot.ts (they depend on restoreInline* helpers).
|
|
export {
|
|
clearStudioPathOffset,
|
|
clearStudioRotation,
|
|
clearStudioBoxSize,
|
|
} from "./manualEditsSnapshot";
|