mirror of
https://github.com/heygen-com/hyperframes.git
synced 2026-11-05 05:37:09 +00:00
fix(studio): scale master edits in source coordinates
This commit is contained in:
committed by
Miguel Ángel
parent
d9b3be0261
commit
fa208641be
@@ -1,6 +1,8 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import {
|
||||
focusDomEditOverlayElement,
|
||||
resolveDomEditCoordinateScale,
|
||||
resolveDomEditPathOffsetGesture,
|
||||
resolveDomEditResizeGesture,
|
||||
resolveDomEditRotationGesture,
|
||||
} from "./DomEditOverlay";
|
||||
@@ -16,6 +18,54 @@ describe("focusDomEditOverlayElement", () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe("resolveDomEditCoordinateScale", () => {
|
||||
it("uses the top-level preview scale when no source boundary dimensions are available", () => {
|
||||
expect(
|
||||
resolveDomEditCoordinateScale({
|
||||
rootScaleX: 0.5,
|
||||
rootScaleY: 0.5,
|
||||
}),
|
||||
).toEqual({
|
||||
scaleX: 0.5,
|
||||
scaleY: 0.5,
|
||||
});
|
||||
});
|
||||
|
||||
it("converts source-local pixels through a scaled nested composition host", () => {
|
||||
expect(
|
||||
resolveDomEditCoordinateScale({
|
||||
rootScaleX: 0.5,
|
||||
rootScaleY: 0.5,
|
||||
sourceRectWidth: 960,
|
||||
sourceRectHeight: 540,
|
||||
sourceWidth: 1920,
|
||||
sourceHeight: 1080,
|
||||
}),
|
||||
).toEqual({
|
||||
scaleX: 0.25,
|
||||
scaleY: 0.25,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("resolveDomEditPathOffsetGesture", () => {
|
||||
it("writes source-local offsets when the edited source is scaled down in master view", () => {
|
||||
expect(
|
||||
resolveDomEditPathOffsetGesture({
|
||||
actualOffsetX: 10,
|
||||
actualOffsetY: -4,
|
||||
scaleX: 0.25,
|
||||
scaleY: 0.25,
|
||||
dx: 25,
|
||||
dy: 10,
|
||||
}),
|
||||
).toEqual({
|
||||
x: 110,
|
||||
y: 36,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("resolveDomEditResizeGesture", () => {
|
||||
it("resizes width and height independently by default", () => {
|
||||
expect(
|
||||
@@ -77,6 +127,27 @@ describe("resolveDomEditResizeGesture", () => {
|
||||
height: 260,
|
||||
});
|
||||
});
|
||||
|
||||
it("writes source-local dimensions when the edited source is scaled down in master view", () => {
|
||||
expect(
|
||||
resolveDomEditResizeGesture({
|
||||
originWidth: 100,
|
||||
originHeight: 50,
|
||||
actualWidth: 400,
|
||||
actualHeight: 200,
|
||||
scaleX: 0.25,
|
||||
scaleY: 0.25,
|
||||
dx: 25,
|
||||
dy: 10,
|
||||
uniform: false,
|
||||
}),
|
||||
).toEqual({
|
||||
overlayWidth: 125,
|
||||
overlayHeight: 60,
|
||||
width: 500,
|
||||
height: 240,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("resolveDomEditRotationGesture", () => {
|
||||
|
||||
@@ -30,8 +30,8 @@ interface OverlayRect {
|
||||
top: number;
|
||||
width: number;
|
||||
height: number;
|
||||
scaleX: number;
|
||||
scaleY: number;
|
||||
editScaleX: number;
|
||||
editScaleY: number;
|
||||
}
|
||||
|
||||
interface DomEditOverlayProps {
|
||||
@@ -70,16 +70,73 @@ function toOverlayRect(
|
||||
if (!rootWidth || !rootHeight) return null;
|
||||
|
||||
const elementRect = element.getBoundingClientRect();
|
||||
const scaleX = iframeRect.width / rootWidth;
|
||||
const scaleY = iframeRect.height / rootHeight;
|
||||
const rootScaleX = iframeRect.width / rootWidth;
|
||||
const rootScaleY = iframeRect.height / rootHeight;
|
||||
const sourceBoundary = findSourceBoundary(element);
|
||||
const sourceBoundaryRect = sourceBoundary?.getBoundingClientRect();
|
||||
const editScale = resolveDomEditCoordinateScale({
|
||||
rootScaleX,
|
||||
rootScaleY,
|
||||
sourceRectWidth: sourceBoundaryRect?.width,
|
||||
sourceRectHeight: sourceBoundaryRect?.height,
|
||||
sourceWidth: readPositiveDimension(sourceBoundary?.getAttribute("data-width") ?? null),
|
||||
sourceHeight: readPositiveDimension(sourceBoundary?.getAttribute("data-height") ?? null),
|
||||
});
|
||||
|
||||
return {
|
||||
left: iframeRect.left - overlayRect.left + elementRect.left * scaleX,
|
||||
top: iframeRect.top - overlayRect.top + elementRect.top * scaleY,
|
||||
width: elementRect.width * scaleX,
|
||||
height: elementRect.height * scaleY,
|
||||
scaleX,
|
||||
scaleY,
|
||||
left: iframeRect.left - overlayRect.left + (elementRect.left - rootRect.left) * rootScaleX,
|
||||
top: iframeRect.top - overlayRect.top + (elementRect.top - rootRect.top) * rootScaleY,
|
||||
width: elementRect.width * rootScaleX,
|
||||
height: elementRect.height * rootScaleY,
|
||||
editScaleX: editScale.scaleX,
|
||||
editScaleY: editScale.scaleY,
|
||||
};
|
||||
}
|
||||
|
||||
function readPositiveDimension(value: string | null): number | null {
|
||||
if (!value) return null;
|
||||
const parsed = Number.parseFloat(value);
|
||||
return Number.isFinite(parsed) && parsed > 0 ? parsed : null;
|
||||
}
|
||||
|
||||
function findSourceBoundary(element: HTMLElement): HTMLElement | null {
|
||||
let current: HTMLElement | null = element;
|
||||
while (current) {
|
||||
if (
|
||||
current.hasAttribute("data-composition-file") ||
|
||||
current.hasAttribute("data-composition-src")
|
||||
) {
|
||||
return current;
|
||||
}
|
||||
current = current.parentElement;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
export function resolveDomEditCoordinateScale(input: {
|
||||
rootScaleX: number;
|
||||
rootScaleY: number;
|
||||
sourceRectWidth?: number;
|
||||
sourceRectHeight?: number;
|
||||
sourceWidth?: number | null;
|
||||
sourceHeight?: number | null;
|
||||
}): { scaleX: number; scaleY: number } {
|
||||
const rootScaleX = input.rootScaleX > 0 ? input.rootScaleX : 1;
|
||||
const rootScaleY = input.rootScaleY > 0 ? input.rootScaleY : 1;
|
||||
const sourceScaleX =
|
||||
input.sourceRectWidth && input.sourceRectWidth > 0 && input.sourceWidth && input.sourceWidth > 0
|
||||
? (input.sourceRectWidth * rootScaleX) / input.sourceWidth
|
||||
: rootScaleX;
|
||||
const sourceScaleY =
|
||||
input.sourceRectHeight &&
|
||||
input.sourceRectHeight > 0 &&
|
||||
input.sourceHeight &&
|
||||
input.sourceHeight > 0
|
||||
? (input.sourceRectHeight * rootScaleY) / input.sourceHeight
|
||||
: rootScaleY;
|
||||
return {
|
||||
scaleX: sourceScaleX > 0 ? sourceScaleX : rootScaleX,
|
||||
scaleY: sourceScaleY > 0 ? sourceScaleY : rootScaleY,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -97,8 +154,8 @@ function rectsEqual(a: OverlayRect | null, b: OverlayRect | null): boolean {
|
||||
Math.abs(a.top - b.top) < OVERLAY_RECT_EPSILON_PX &&
|
||||
Math.abs(a.width - b.width) < OVERLAY_RECT_EPSILON_PX &&
|
||||
Math.abs(a.height - b.height) < OVERLAY_RECT_EPSILON_PX &&
|
||||
Math.abs(a.scaleX - b.scaleX) < 0.001 &&
|
||||
Math.abs(a.scaleY - b.scaleY) < 0.001
|
||||
Math.abs(a.editScaleX - b.editScaleX) < 0.001 &&
|
||||
Math.abs(a.editScaleY - b.editScaleY) < 0.001
|
||||
);
|
||||
}
|
||||
|
||||
@@ -156,6 +213,22 @@ export function resolveDomEditResizeGesture(input: {
|
||||
};
|
||||
}
|
||||
|
||||
export function resolveDomEditPathOffsetGesture(input: {
|
||||
actualOffsetX: number;
|
||||
actualOffsetY: number;
|
||||
scaleX: number;
|
||||
scaleY: number;
|
||||
dx: number;
|
||||
dy: number;
|
||||
}): { x: number; y: number } {
|
||||
const scaleX = input.scaleX > 0 ? input.scaleX : 1;
|
||||
const scaleY = input.scaleY > 0 ? input.scaleY : 1;
|
||||
return {
|
||||
x: input.actualOffsetX + input.dx / scaleX,
|
||||
y: input.actualOffsetY + input.dy / scaleY,
|
||||
};
|
||||
}
|
||||
|
||||
function pointerAngleDegrees(centerX: number, centerY: number, x: number, y: number): number {
|
||||
return (Math.atan2(y - centerY, x - centerX) * 180) / Math.PI;
|
||||
}
|
||||
@@ -213,8 +286,8 @@ interface GestureState {
|
||||
actualWidth: number;
|
||||
actualHeight: number;
|
||||
actualRotation: number;
|
||||
scaleX: number;
|
||||
scaleY: number;
|
||||
editScaleX: number;
|
||||
editScaleY: number;
|
||||
manualEditDragToken?: string;
|
||||
}
|
||||
|
||||
@@ -345,8 +418,8 @@ export const DomEditOverlay = memo(function DomEditOverlay({
|
||||
const offset = readStudioPathOffset(sel.element);
|
||||
const size = readStudioBoxSize(sel.element);
|
||||
const rotation = readStudioRotation(sel.element);
|
||||
const actualWidth = size.width > 0 ? size.width : rect.width / rect.scaleX;
|
||||
const actualHeight = size.height > 0 ? size.height : rect.height / rect.scaleY;
|
||||
const actualWidth = size.width > 0 ? size.width : rect.width / rect.editScaleX;
|
||||
const actualHeight = size.height > 0 ? size.height : rect.height / rect.editScaleY;
|
||||
const manualEditDragToken = beginStudioManualEditGesture(sel.element);
|
||||
const overlayBounds = overlayEl?.getBoundingClientRect();
|
||||
const centerX = (overlayBounds?.left ?? 0) + rect.left + rect.width / 2;
|
||||
@@ -377,8 +450,8 @@ export const DomEditOverlay = memo(function DomEditOverlay({
|
||||
actualWidth,
|
||||
actualHeight,
|
||||
actualRotation: rotation.angle,
|
||||
scaleX: rect.scaleX,
|
||||
scaleY: rect.scaleY,
|
||||
editScaleX: rect.editScaleX,
|
||||
editScaleY: rect.editScaleY,
|
||||
manualEditDragToken,
|
||||
};
|
||||
};
|
||||
@@ -424,18 +497,25 @@ export const DomEditOverlay = memo(function DomEditOverlay({
|
||||
const nextBoxTop = g.originTop + dy;
|
||||
box.style.left = `${nextBoxLeft}px`;
|
||||
box.style.top = `${nextBoxTop}px`;
|
||||
applyStudioPathOffsetDraft(sel.element, {
|
||||
x: g.actualOffsetX + dx / g.scaleX,
|
||||
y: g.actualOffsetY + dy / g.scaleY,
|
||||
});
|
||||
applyStudioPathOffsetDraft(
|
||||
sel.element,
|
||||
resolveDomEditPathOffsetGesture({
|
||||
actualOffsetX: g.actualOffsetX,
|
||||
actualOffsetY: g.actualOffsetY,
|
||||
scaleX: g.editScaleX,
|
||||
scaleY: g.editScaleY,
|
||||
dx,
|
||||
dy,
|
||||
}),
|
||||
);
|
||||
} else {
|
||||
const nextSize = resolveDomEditResizeGesture({
|
||||
originWidth: g.originWidth,
|
||||
originHeight: g.originHeight,
|
||||
actualWidth: g.actualWidth,
|
||||
actualHeight: g.actualHeight,
|
||||
scaleX: g.scaleX,
|
||||
scaleY: g.scaleY,
|
||||
scaleX: g.editScaleX,
|
||||
scaleY: g.editScaleY,
|
||||
dx,
|
||||
dy,
|
||||
uniform: e.shiftKey,
|
||||
|
||||
Reference in New Issue
Block a user