support resizing multiple elements including texts (#1726)

Co-authored-by: David Luzar <luzar.david@gmail.com>
This commit is contained in:
Daishi Kato 2020-06-08 18:25:20 +09:00 committed by GitHub
parent ebb1341bbd
commit 53ab46126d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 192 additions and 195 deletions

View File

@ -23,7 +23,6 @@ import {
newLinearElement, newLinearElement,
resizeElements, resizeElements,
getElementWithResizeHandler, getElementWithResizeHandler,
canResizeMutlipleElements,
getResizeOffsetXY, getResizeOffsetXY,
getResizeArrowDirection, getResizeArrowDirection,
getResizeHandlerFromCoords, getResizeHandlerFromCoords,
@ -1771,7 +1770,6 @@ class App extends React.Component<any, AppState> {
return; return;
} }
} else if (selectedElements.length > 1 && !isOverScrollBar) { } else if (selectedElements.length > 1 && !isOverScrollBar) {
if (canResizeMutlipleElements(selectedElements)) {
const resizeHandle = getResizeHandlerFromCoords( const resizeHandle = getResizeHandlerFromCoords(
getCommonBounds(selectedElements), getCommonBounds(selectedElements),
scenePointerX, scenePointerX,
@ -1786,7 +1784,6 @@ class App extends React.Component<any, AppState> {
return; return;
} }
} }
}
const hitElement = getElementAtPosition( const hitElement = getElementAtPosition(
elements, elements,
this.state, this.state,
@ -2054,7 +2051,6 @@ class App extends React.Component<any, AppState> {
isResizingElements = true; isResizingElements = true;
} }
} else if (selectedElements.length > 1) { } else if (selectedElements.length > 1) {
if (canResizeMutlipleElements(selectedElements)) {
resizeHandle = getResizeHandlerFromCoords( resizeHandle = getResizeHandlerFromCoords(
getCommonBounds(selectedElements), getCommonBounds(selectedElements),
x, x,
@ -2063,15 +2059,12 @@ class App extends React.Component<any, AppState> {
event.pointerType, event.pointerType,
); );
if (resizeHandle) { if (resizeHandle) {
document.documentElement.style.cursor = getCursorForResizingElement( document.documentElement.style.cursor = getCursorForResizingElement({
{
resizeHandle, resizeHandle,
}, });
);
isResizingElements = true; isResizingElements = true;
} }
} }
}
if (isResizingElements) { if (isResizingElements) {
resizeOffsetXY = getResizeOffsetXY( resizeOffsetXY = getResizeOffsetXY(
resizeHandle, resizeHandle,

View File

@ -35,7 +35,6 @@ export {
} from "./resizeTest"; } from "./resizeTest";
export { export {
resizeElements, resizeElements,
canResizeMutlipleElements,
getResizeOffsetXY, getResizeOffsetXY,
getResizeArrowDirection, getResizeArrowDirection,
} from "./resizeElements"; } from "./resizeElements";

View File

@ -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 nextWidth = element.width * scale;
const metrics = measureText( const nextHeight = element.height * scale;
element.text, const nextFont = measureFontSizeFromWH(element, nextWidth, nextHeight);
getFontString({ fontSize: newFontSize, fontFamily: element.fontFamily }), if (nextFont === null) {
);
if (
Math.abs(metrics.width - element.width) <= 1 ||
Math.abs(metrics.height - element.height) <= 1
) {
// we ignore 1px change to avoid janky behavior
return; return;
} }
const [nextX1, nextY1, nextX2, nextY2] = getResizedElementAbsoluteCoords( const [nextX1, nextY1, nextX2, nextY2] = getResizedElementAbsoluteCoords(
element, element,
metrics.width, nextWidth,
metrics.height, 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, fontSize: nextFont.size,
width: metrics.width, width: nextWidth,
height: metrics.height, height: nextHeight,
baseline: metrics.baseline, baseline: nextFont.baseline,
x: nextElementX, x: nextElementX,
y: nextElementY, y: nextElementY,
}); });
@ -398,97 +423,77 @@ 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": { case "se":
const scale = Math.max( scale = Math.max(
(pointerX - x1) / (x2 - x1), (pointerX - x1) / (x2 - x1),
(pointerY - y1) / (y2 - y1), (pointerY - y1) / (y2 - y1),
); );
if (scale > 0) { getNextXY = (element, [origX1, origY1], [finalX1, finalY1]) => {
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 x = element.x + (origX1 - x1) * (scale - 1) + origX1 - finalX1;
const y = element.y + (origY1 - y1) * (scale - 1) + origY1 - finalY1; const y = element.y + (origY1 - y1) * (scale - 1) + origY1 - finalY1;
mutateElement(element, { width, height, x, y, ...rescaledPoints }); return { x, y };
}); };
}
break; break;
} case "nw":
case "nw": { scale = Math.max(
const scale = Math.max(
(x2 - pointerX) / (x2 - x1), (x2 - pointerX) / (x2 - x1),
(y2 - pointerY) / (y2 - y1), (y2 - pointerY) / (y2 - y1),
); );
if (scale > 0) { getNextXY = (element, [, , origX2, origY2], [, , finalX2, finalY2]) => {
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 x = element.x - (x2 - origX2) * (scale - 1) + origX2 - finalX2;
const y = element.y - (y2 - origY2) * (scale - 1) + origY2 - finalY2; const y = element.y - (y2 - origY2) * (scale - 1) + origY2 - finalY2;
mutateElement(element, { width, height, x, y, ...rescaledPoints }); return { x, y };
}); };
}
break; break;
} case "ne":
case "ne": { scale = Math.max(
const scale = Math.max(
(pointerX - x1) / (x2 - x1), (pointerX - x1) / (x2 - x1),
(y2 - pointerY) / (y2 - y1), (y2 - pointerY) / (y2 - y1),
); );
if (scale > 0) { getNextXY = (element, [origX1, , , origY2], [finalX1, , , finalY2]) => {
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 x = element.x + (origX1 - x1) * (scale - 1) + origX1 - finalX1;
const y = element.y - (y2 - origY2) * (scale - 1) + origY2 - finalY2; const y = element.y - (y2 - origY2) * (scale - 1) + origY2 - finalY2;
mutateElement(element, { width, height, x, y, ...rescaledPoints }); return { x, y };
}); };
}
break; break;
} case "sw":
case "sw": { scale = Math.max(
const scale = Math.max(
(x2 - pointerX) / (x2 - x1), (x2 - pointerX) / (x2 - x1),
(pointerY - y1) / (y2 - y1), (pointerY - y1) / (y2 - y1),
); );
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;
}
if (scale > 0) { if (scale > 0) {
elements.forEach((element) => { const updates = elements.reduce(
(prev, element) => {
if (!prev) {
return prev;
}
const width = element.width * scale; const width = element.width * scale;
const height = element.height * scale; const height = element.height * scale;
const [, origY1, origX2] = getElementAbsoluteCoords(element); 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 rescaledPoints = rescalePointsInElement(element, width, height);
const [, finalY1, finalX2] = getResizedElementAbsoluteCoords( const finalCoords = getResizedElementAbsoluteCoords(
{ {
...element, ...element,
...rescaledPoints, ...rescaledPoints,
@ -496,24 +501,27 @@ const resizeMultipleElements = (
width, width,
height, height,
); );
const x = element.x - (x2 - origX2) * (scale - 1) + origX2 - finalX2; const { x, y } = getNextXY(element, origCoords, finalCoords);
const y = element.y + (origY1 - y1) * (scale - 1) + origY1 - finalY1; return [...prev, { width, height, x, y, ...rescaledPoints, ...font }];
mutateElement(element, { width, height, x, y, ...rescaledPoints }); },
[] 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]);
}); });
} }
break;
} }
}
};
export const canResizeMutlipleElements = (
elements: readonly NonDeletedExcalidrawElement[],
) => {
return elements.every(
(element) =>
["rectangle", "diamond", "ellipse"].includes(element.type) ||
isLinearElement(element),
);
}; };
export const getResizeOffsetXY = ( export const getResizeOffsetXY = (

View File

@ -16,7 +16,6 @@ import {
handlerRectanglesFromCoords, handlerRectanglesFromCoords,
handlerRectangles, handlerRectangles,
getCommonBounds, getCommonBounds,
canResizeMutlipleElements,
} from "../element"; } from "../element";
import { roundRect } from "./roundRect"; import { roundRect } from "./roundRect";
@ -372,7 +371,6 @@ export const renderScene = (
}); });
context.translate(-sceneState.scrollX, -sceneState.scrollY); context.translate(-sceneState.scrollX, -sceneState.scrollY);
} else if (locallySelectedElements.length > 1) { } else if (locallySelectedElements.length > 1) {
if (canResizeMutlipleElements(locallySelectedElements)) {
const dashedLinePadding = 4 / sceneState.zoom; const dashedLinePadding = 4 / sceneState.zoom;
context.translate(sceneState.scrollX, sceneState.scrollY); context.translate(sceneState.scrollX, sceneState.scrollY);
context.fillStyle = oc.white; context.fillStyle = oc.white;
@ -422,7 +420,6 @@ export const renderScene = (
context.translate(-sceneState.scrollX, -sceneState.scrollY); context.translate(-sceneState.scrollX, -sceneState.scrollY);
} }
} }
}
// Reset zoom // Reset zoom
context.scale(1 / sceneState.zoom, 1 / sceneState.zoom); context.scale(1 / sceneState.zoom, 1 / sceneState.zoom);