|
@@ -204,6 +204,37 @@ const rescalePointsInElement = (
|
|
}
|
|
}
|
|
: {};
|
|
: {};
|
|
|
|
|
|
|
|
+// This is not computationally ideal, but can't be helped.
|
|
|
|
+const measureFontSizeFromWH = (
|
|
|
|
+ element: NonDeleted<ExcalidrawTextElement>,
|
|
|
|
+ nextWidth: number,
|
|
|
|
+ nextHeight: number,
|
|
|
|
+): { size: number; baseline: number } | null => {
|
|
|
|
+ let scale = Math.min(nextWidth / element.width, nextHeight / element.height);
|
|
|
|
+ let nextFontSize = element.fontSize * scale;
|
|
|
|
+ let metrics = measureText(
|
|
|
|
+ element.text,
|
|
|
|
+ getFontString({ fontSize: nextFontSize, fontFamily: element.fontFamily }),
|
|
|
|
+ );
|
|
|
|
+ if (metrics.width - nextWidth < 1 && metrics.height - nextHeight < 1) {
|
|
|
|
+ return { size: nextFontSize, baseline: metrics.baseline };
|
|
|
|
+ }
|
|
|
|
+ // second measurement
|
|
|
|
+ scale = Math.min(
|
|
|
|
+ Math.min(nextWidth, metrics.width) / element.width,
|
|
|
|
+ Math.min(nextHeight, metrics.height) / element.height,
|
|
|
|
+ );
|
|
|
|
+ nextFontSize = element.fontSize * scale;
|
|
|
|
+ metrics = measureText(
|
|
|
|
+ element.text,
|
|
|
|
+ getFontString({ fontSize: nextFontSize, fontFamily: element.fontFamily }),
|
|
|
|
+ );
|
|
|
|
+ if (metrics.width - nextWidth < 1 && metrics.height - nextHeight < 1) {
|
|
|
|
+ return { size: nextFontSize, baseline: metrics.baseline };
|
|
|
|
+ }
|
|
|
|
+ return null;
|
|
|
|
+};
|
|
|
|
+
|
|
const resizeSingleTextElement = (
|
|
const resizeSingleTextElement = (
|
|
element: NonDeleted<ExcalidrawTextElement>,
|
|
element: NonDeleted<ExcalidrawTextElement>,
|
|
resizeHandle: "nw" | "ne" | "sw" | "se",
|
|
resizeHandle: "nw" | "ne" | "sw" | "se",
|
|
@@ -250,22 +281,16 @@ const resizeSingleTextElement = (
|
|
break;
|
|
break;
|
|
}
|
|
}
|
|
if (scale > 0) {
|
|
if (scale > 0) {
|
|
- const newFontSize = Math.max(element.fontSize * scale, 10);
|
|
|
|
- const metrics = measureText(
|
|
|
|
- element.text,
|
|
|
|
- getFontString({ fontSize: newFontSize, fontFamily: element.fontFamily }),
|
|
|
|
- );
|
|
|
|
- if (
|
|
|
|
- Math.abs(metrics.width - element.width) <= 1 ||
|
|
|
|
- Math.abs(metrics.height - element.height) <= 1
|
|
|
|
- ) {
|
|
|
|
- // we ignore 1px change to avoid janky behavior
|
|
|
|
|
|
+ const nextWidth = element.width * scale;
|
|
|
|
+ const nextHeight = element.height * scale;
|
|
|
|
+ const nextFont = measureFontSizeFromWH(element, nextWidth, nextHeight);
|
|
|
|
+ if (nextFont === null) {
|
|
return;
|
|
return;
|
|
}
|
|
}
|
|
const [nextX1, nextY1, nextX2, nextY2] = getResizedElementAbsoluteCoords(
|
|
const [nextX1, nextY1, nextX2, nextY2] = getResizedElementAbsoluteCoords(
|
|
element,
|
|
element,
|
|
- metrics.width,
|
|
|
|
- metrics.height,
|
|
|
|
|
|
+ nextWidth,
|
|
|
|
+ nextHeight,
|
|
);
|
|
);
|
|
const deltaX1 = (x1 - nextX1) / 2;
|
|
const deltaX1 = (x1 - nextX1) / 2;
|
|
const deltaY1 = (y1 - nextY1) / 2;
|
|
const deltaY1 = (y1 - nextY1) / 2;
|
|
@@ -283,10 +308,10 @@ const resizeSingleTextElement = (
|
|
isResizeFromCenter,
|
|
isResizeFromCenter,
|
|
);
|
|
);
|
|
mutateElement(element, {
|
|
mutateElement(element, {
|
|
- fontSize: newFontSize,
|
|
|
|
- width: metrics.width,
|
|
|
|
- height: metrics.height,
|
|
|
|
- baseline: metrics.baseline,
|
|
|
|
|
|
+ fontSize: nextFont.size,
|
|
|
|
+ width: nextWidth,
|
|
|
|
+ height: nextHeight,
|
|
|
|
+ baseline: nextFont.baseline,
|
|
x: nextElementX,
|
|
x: nextElementX,
|
|
y: nextElementY,
|
|
y: nextElementY,
|
|
});
|
|
});
|
|
@@ -398,124 +423,107 @@ const resizeMultipleElements = (
|
|
pointerY: number,
|
|
pointerY: number,
|
|
) => {
|
|
) => {
|
|
const [x1, y1, x2, y2] = getCommonBounds(elements);
|
|
const [x1, y1, x2, y2] = getCommonBounds(elements);
|
|
|
|
+ let scale: number;
|
|
|
|
+ let getNextXY: (
|
|
|
|
+ element: NonDeletedExcalidrawElement,
|
|
|
|
+ origCoords: readonly [number, number, number, number],
|
|
|
|
+ finalCoords: readonly [number, number, number, number],
|
|
|
|
+ ) => { x: number; y: number };
|
|
switch (resizeHandle) {
|
|
switch (resizeHandle) {
|
|
- case "se": {
|
|
|
|
- const scale = Math.max(
|
|
|
|
|
|
+ case "se":
|
|
|
|
+ scale = Math.max(
|
|
(pointerX - x1) / (x2 - x1),
|
|
(pointerX - x1) / (x2 - x1),
|
|
(pointerY - y1) / (y2 - y1),
|
|
(pointerY - y1) / (y2 - y1),
|
|
);
|
|
);
|
|
- if (scale > 0) {
|
|
|
|
- elements.forEach((element) => {
|
|
|
|
- const width = element.width * scale;
|
|
|
|
- const height = element.height * scale;
|
|
|
|
- const [origX1, origY1] = getElementAbsoluteCoords(element);
|
|
|
|
- const rescaledPoints = rescalePointsInElement(element, width, height);
|
|
|
|
- const [finalX1, finalY1] = getResizedElementAbsoluteCoords(
|
|
|
|
- {
|
|
|
|
- ...element,
|
|
|
|
- ...rescaledPoints,
|
|
|
|
- },
|
|
|
|
- width,
|
|
|
|
- height,
|
|
|
|
- );
|
|
|
|
- const x = element.x + (origX1 - x1) * (scale - 1) + origX1 - finalX1;
|
|
|
|
- const y = element.y + (origY1 - y1) * (scale - 1) + origY1 - finalY1;
|
|
|
|
- mutateElement(element, { width, height, x, y, ...rescaledPoints });
|
|
|
|
- });
|
|
|
|
- }
|
|
|
|
|
|
+ getNextXY = (element, [origX1, origY1], [finalX1, finalY1]) => {
|
|
|
|
+ const x = element.x + (origX1 - x1) * (scale - 1) + origX1 - finalX1;
|
|
|
|
+ const y = element.y + (origY1 - y1) * (scale - 1) + origY1 - finalY1;
|
|
|
|
+ return { x, y };
|
|
|
|
+ };
|
|
break;
|
|
break;
|
|
- }
|
|
|
|
- case "nw": {
|
|
|
|
- const scale = Math.max(
|
|
|
|
|
|
+ case "nw":
|
|
|
|
+ scale = Math.max(
|
|
(x2 - pointerX) / (x2 - x1),
|
|
(x2 - pointerX) / (x2 - x1),
|
|
(y2 - pointerY) / (y2 - y1),
|
|
(y2 - pointerY) / (y2 - y1),
|
|
);
|
|
);
|
|
- if (scale > 0) {
|
|
|
|
- elements.forEach((element) => {
|
|
|
|
- const width = element.width * scale;
|
|
|
|
- const height = element.height * scale;
|
|
|
|
- const [, , origX2, origY2] = getElementAbsoluteCoords(element);
|
|
|
|
- const rescaledPoints = rescalePointsInElement(element, width, height);
|
|
|
|
- const [, , finalX2, finalY2] = getResizedElementAbsoluteCoords(
|
|
|
|
- {
|
|
|
|
- ...element,
|
|
|
|
- ...rescaledPoints,
|
|
|
|
- },
|
|
|
|
- width,
|
|
|
|
- height,
|
|
|
|
- );
|
|
|
|
- const x = element.x - (x2 - origX2) * (scale - 1) + origX2 - finalX2;
|
|
|
|
- const y = element.y - (y2 - origY2) * (scale - 1) + origY2 - finalY2;
|
|
|
|
- mutateElement(element, { width, height, x, y, ...rescaledPoints });
|
|
|
|
- });
|
|
|
|
- }
|
|
|
|
|
|
+ getNextXY = (element, [, , origX2, origY2], [, , finalX2, finalY2]) => {
|
|
|
|
+ const x = element.x - (x2 - origX2) * (scale - 1) + origX2 - finalX2;
|
|
|
|
+ const y = element.y - (y2 - origY2) * (scale - 1) + origY2 - finalY2;
|
|
|
|
+ return { x, y };
|
|
|
|
+ };
|
|
break;
|
|
break;
|
|
- }
|
|
|
|
- case "ne": {
|
|
|
|
- const scale = Math.max(
|
|
|
|
|
|
+ case "ne":
|
|
|
|
+ scale = Math.max(
|
|
(pointerX - x1) / (x2 - x1),
|
|
(pointerX - x1) / (x2 - x1),
|
|
(y2 - pointerY) / (y2 - y1),
|
|
(y2 - pointerY) / (y2 - y1),
|
|
);
|
|
);
|
|
- if (scale > 0) {
|
|
|
|
- elements.forEach((element) => {
|
|
|
|
- const width = element.width * scale;
|
|
|
|
- const height = element.height * scale;
|
|
|
|
- const [origX1, , , origY2] = getElementAbsoluteCoords(element);
|
|
|
|
- const rescaledPoints = rescalePointsInElement(element, width, height);
|
|
|
|
- const [finalX1, , , finalY2] = getResizedElementAbsoluteCoords(
|
|
|
|
- {
|
|
|
|
- ...element,
|
|
|
|
- ...rescaledPoints,
|
|
|
|
- },
|
|
|
|
- width,
|
|
|
|
- height,
|
|
|
|
- );
|
|
|
|
- const x = element.x + (origX1 - x1) * (scale - 1) + origX1 - finalX1;
|
|
|
|
- const y = element.y - (y2 - origY2) * (scale - 1) + origY2 - finalY2;
|
|
|
|
- mutateElement(element, { width, height, x, y, ...rescaledPoints });
|
|
|
|
- });
|
|
|
|
- }
|
|
|
|
|
|
+ getNextXY = (element, [origX1, , , origY2], [finalX1, , , finalY2]) => {
|
|
|
|
+ const x = element.x + (origX1 - x1) * (scale - 1) + origX1 - finalX1;
|
|
|
|
+ const y = element.y - (y2 - origY2) * (scale - 1) + origY2 - finalY2;
|
|
|
|
+ return { x, y };
|
|
|
|
+ };
|
|
break;
|
|
break;
|
|
- }
|
|
|
|
- case "sw": {
|
|
|
|
- const scale = Math.max(
|
|
|
|
|
|
+ case "sw":
|
|
|
|
+ scale = Math.max(
|
|
(x2 - pointerX) / (x2 - x1),
|
|
(x2 - pointerX) / (x2 - x1),
|
|
(pointerY - y1) / (y2 - y1),
|
|
(pointerY - y1) / (y2 - y1),
|
|
);
|
|
);
|
|
- if (scale > 0) {
|
|
|
|
- elements.forEach((element) => {
|
|
|
|
- const width = element.width * scale;
|
|
|
|
- const height = element.height * scale;
|
|
|
|
- const [, origY1, origX2] = getElementAbsoluteCoords(element);
|
|
|
|
- const rescaledPoints = rescalePointsInElement(element, width, height);
|
|
|
|
- const [, finalY1, finalX2] = getResizedElementAbsoluteCoords(
|
|
|
|
- {
|
|
|
|
- ...element,
|
|
|
|
- ...rescaledPoints,
|
|
|
|
- },
|
|
|
|
- width,
|
|
|
|
- height,
|
|
|
|
- );
|
|
|
|
- const x = element.x - (x2 - origX2) * (scale - 1) + origX2 - finalX2;
|
|
|
|
- const y = element.y + (origY1 - y1) * (scale - 1) + origY1 - finalY1;
|
|
|
|
- mutateElement(element, { width, height, x, y, ...rescaledPoints });
|
|
|
|
- });
|
|
|
|
- }
|
|
|
|
|
|
+ getNextXY = (element, [, origY1, origX2], [, finalY1, finalX2]) => {
|
|
|
|
+ const x = element.x - (x2 - origX2) * (scale - 1) + origX2 - finalX2;
|
|
|
|
+ const y = element.y + (origY1 - y1) * (scale - 1) + origY1 - finalY1;
|
|
|
|
+ return { x, y };
|
|
|
|
+ };
|
|
break;
|
|
break;
|
|
|
|
+ }
|
|
|
|
+ if (scale > 0) {
|
|
|
|
+ const updates = elements.reduce(
|
|
|
|
+ (prev, element) => {
|
|
|
|
+ if (!prev) {
|
|
|
|
+ return prev;
|
|
|
|
+ }
|
|
|
|
+ const width = element.width * scale;
|
|
|
|
+ const height = element.height * scale;
|
|
|
|
+ let font: { fontSize?: number; baseline?: number } = {};
|
|
|
|
+ if (element.type === "text") {
|
|
|
|
+ const nextFont = measureFontSizeFromWH(element, width, height);
|
|
|
|
+ if (nextFont === null) {
|
|
|
|
+ return null;
|
|
|
|
+ }
|
|
|
|
+ font = { fontSize: nextFont.size, baseline: nextFont.baseline };
|
|
|
|
+ }
|
|
|
|
+ const origCoords = getElementAbsoluteCoords(element);
|
|
|
|
+ const rescaledPoints = rescalePointsInElement(element, width, height);
|
|
|
|
+ const finalCoords = getResizedElementAbsoluteCoords(
|
|
|
|
+ {
|
|
|
|
+ ...element,
|
|
|
|
+ ...rescaledPoints,
|
|
|
|
+ },
|
|
|
|
+ width,
|
|
|
|
+ height,
|
|
|
|
+ );
|
|
|
|
+ const { x, y } = getNextXY(element, origCoords, finalCoords);
|
|
|
|
+ return [...prev, { width, height, x, y, ...rescaledPoints, ...font }];
|
|
|
|
+ },
|
|
|
|
+ [] as
|
|
|
|
+ | {
|
|
|
|
+ width: number;
|
|
|
|
+ height: number;
|
|
|
|
+ x: number;
|
|
|
|
+ y: number;
|
|
|
|
+ points?: (readonly [number, number])[];
|
|
|
|
+ fontSize?: number;
|
|
|
|
+ baseline?: number;
|
|
|
|
+ }[]
|
|
|
|
+ | null,
|
|
|
|
+ );
|
|
|
|
+ if (updates) {
|
|
|
|
+ elements.forEach((element, index) => {
|
|
|
|
+ mutateElement(element, updates[index]);
|
|
|
|
+ });
|
|
}
|
|
}
|
|
}
|
|
}
|
|
};
|
|
};
|
|
|
|
|
|
-export const canResizeMutlipleElements = (
|
|
|
|
- elements: readonly NonDeletedExcalidrawElement[],
|
|
|
|
-) => {
|
|
|
|
- return elements.every(
|
|
|
|
- (element) =>
|
|
|
|
- ["rectangle", "diamond", "ellipse"].includes(element.type) ||
|
|
|
|
- isLinearElement(element),
|
|
|
|
- );
|
|
|
|
-};
|
|
|
|
-
|
|
|
|
export const getResizeOffsetXY = (
|
|
export const getResizeOffsetXY = (
|
|
resizeHandle: ResizeTestType,
|
|
resizeHandle: ResizeTestType,
|
|
selectedElements: NonDeletedExcalidrawElement[],
|
|
selectedElements: NonDeletedExcalidrawElement[],
|