support resizing multiple elements including texts (#1726)
Co-authored-by: David Luzar <luzar.david@gmail.com>
This commit is contained in:
parent
ebb1341bbd
commit
53ab46126d
@ -23,7 +23,6 @@ import {
|
|||||||
newLinearElement,
|
newLinearElement,
|
||||||
resizeElements,
|
resizeElements,
|
||||||
getElementWithResizeHandler,
|
getElementWithResizeHandler,
|
||||||
canResizeMutlipleElements,
|
|
||||||
getResizeOffsetXY,
|
getResizeOffsetXY,
|
||||||
getResizeArrowDirection,
|
getResizeArrowDirection,
|
||||||
getResizeHandlerFromCoords,
|
getResizeHandlerFromCoords,
|
||||||
@ -1771,20 +1770,18 @@ 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,
|
scenePointerY,
|
||||||
scenePointerY,
|
this.state.zoom,
|
||||||
this.state.zoom,
|
event.pointerType,
|
||||||
event.pointerType,
|
);
|
||||||
);
|
if (resizeHandle) {
|
||||||
if (resizeHandle) {
|
document.documentElement.style.cursor = getCursorForResizingElement({
|
||||||
document.documentElement.style.cursor = getCursorForResizingElement({
|
resizeHandle,
|
||||||
resizeHandle,
|
});
|
||||||
});
|
return;
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
const hitElement = getElementAtPosition(
|
const hitElement = getElementAtPosition(
|
||||||
@ -2054,22 +2051,18 @@ 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,
|
y,
|
||||||
y,
|
this.state.zoom,
|
||||||
this.state.zoom,
|
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) {
|
||||||
|
@ -35,7 +35,6 @@ export {
|
|||||||
} from "./resizeTest";
|
} from "./resizeTest";
|
||||||
export {
|
export {
|
||||||
resizeElements,
|
resizeElements,
|
||||||
canResizeMutlipleElements,
|
|
||||||
getResizeOffsetXY,
|
getResizeOffsetXY,
|
||||||
getResizeArrowDirection,
|
getResizeArrowDirection,
|
||||||
} from "./resizeElements";
|
} from "./resizeElements";
|
||||||
|
@ -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,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": {
|
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 x = element.x + (origX1 - x1) * (scale - 1) + origX1 - finalX1;
|
||||||
const width = element.width * scale;
|
const y = element.y + (origY1 - y1) * (scale - 1) + origY1 - finalY1;
|
||||||
const height = element.height * scale;
|
return { x, y };
|
||||||
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 });
|
|
||||||
});
|
|
||||||
}
|
|
||||||
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 x = element.x - (x2 - origX2) * (scale - 1) + origX2 - finalX2;
|
||||||
const width = element.width * scale;
|
const y = element.y - (y2 - origY2) * (scale - 1) + origY2 - finalY2;
|
||||||
const height = element.height * scale;
|
return { x, y };
|
||||||
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 });
|
|
||||||
});
|
|
||||||
}
|
|
||||||
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 x = element.x + (origX1 - x1) * (scale - 1) + origX1 - finalX1;
|
||||||
const width = element.width * scale;
|
const y = element.y - (y2 - origY2) * (scale - 1) + origY2 - finalY2;
|
||||||
const height = element.height * scale;
|
return { x, y };
|
||||||
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 });
|
|
||||||
});
|
|
||||||
}
|
|
||||||
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),
|
||||||
);
|
);
|
||||||
if (scale > 0) {
|
getNextXY = (element, [, origY1, origX2], [, finalY1, finalX2]) => {
|
||||||
elements.forEach((element) => {
|
const x = element.x - (x2 - origX2) * (scale - 1) + origX2 - finalX2;
|
||||||
const width = element.width * scale;
|
const y = element.y + (origY1 - y1) * (scale - 1) + origY1 - finalY1;
|
||||||
const height = element.height * scale;
|
return { x, y };
|
||||||
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 });
|
|
||||||
});
|
|
||||||
}
|
|
||||||
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[],
|
||||||
|
@ -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,55 +371,53 @@ 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;
|
const [x1, y1, x2, y2] = getCommonBounds(locallySelectedElements);
|
||||||
const [x1, y1, x2, y2] = getCommonBounds(locallySelectedElements);
|
const initialLineDash = context.getLineDash();
|
||||||
const initialLineDash = context.getLineDash();
|
context.setLineDash([2 / sceneState.zoom]);
|
||||||
context.setLineDash([2 / sceneState.zoom]);
|
const lineWidth = context.lineWidth;
|
||||||
const lineWidth = context.lineWidth;
|
context.lineWidth = 1 / sceneState.zoom;
|
||||||
context.lineWidth = 1 / sceneState.zoom;
|
strokeRectWithRotation(
|
||||||
strokeRectWithRotation(
|
context,
|
||||||
context,
|
x1 - dashedLinePadding,
|
||||||
x1 - dashedLinePadding,
|
y1 - dashedLinePadding,
|
||||||
y1 - dashedLinePadding,
|
x2 - x1 + dashedLinePadding * 2,
|
||||||
x2 - x1 + dashedLinePadding * 2,
|
y2 - y1 + dashedLinePadding * 2,
|
||||||
y2 - y1 + dashedLinePadding * 2,
|
(x1 + x2) / 2,
|
||||||
(x1 + x2) / 2,
|
(y1 + y2) / 2,
|
||||||
(y1 + y2) / 2,
|
0,
|
||||||
0,
|
);
|
||||||
);
|
context.lineWidth = lineWidth;
|
||||||
context.lineWidth = lineWidth;
|
context.setLineDash(initialLineDash);
|
||||||
context.setLineDash(initialLineDash);
|
const handlers = handlerRectanglesFromCoords(
|
||||||
const handlers = handlerRectanglesFromCoords(
|
[x1, y1, x2, y2],
|
||||||
[x1, y1, x2, y2],
|
0,
|
||||||
0,
|
sceneState.zoom,
|
||||||
sceneState.zoom,
|
undefined,
|
||||||
undefined,
|
OMIT_SIDES_FOR_MULTIPLE_ELEMENTS,
|
||||||
OMIT_SIDES_FOR_MULTIPLE_ELEMENTS,
|
);
|
||||||
);
|
Object.keys(handlers).forEach((key) => {
|
||||||
Object.keys(handlers).forEach((key) => {
|
const handler = handlers[key as HandlerRectanglesRet];
|
||||||
const handler = handlers[key as HandlerRectanglesRet];
|
if (handler !== undefined) {
|
||||||
if (handler !== undefined) {
|
const lineWidth = context.lineWidth;
|
||||||
const lineWidth = context.lineWidth;
|
context.lineWidth = 1 / sceneState.zoom;
|
||||||
context.lineWidth = 1 / sceneState.zoom;
|
strokeRectWithRotation(
|
||||||
strokeRectWithRotation(
|
context,
|
||||||
context,
|
handler[0],
|
||||||
handler[0],
|
handler[1],
|
||||||
handler[1],
|
handler[2],
|
||||||
handler[2],
|
handler[3],
|
||||||
handler[3],
|
handler[0] + handler[2] / 2,
|
||||||
handler[0] + handler[2] / 2,
|
handler[1] + handler[3] / 2,
|
||||||
handler[1] + handler[3] / 2,
|
0,
|
||||||
0,
|
true, // fill before stroke
|
||||||
true, // fill before stroke
|
);
|
||||||
);
|
context.lineWidth = lineWidth;
|
||||||
context.lineWidth = lineWidth;
|
}
|
||||||
}
|
});
|
||||||
});
|
context.translate(-sceneState.scrollX, -sceneState.scrollY);
|
||||||
context.translate(-sceneState.scrollX, -sceneState.scrollY);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Loading…
x
Reference in New Issue
Block a user