mirror of
https://github.com/excalidraw/excalidraw.git
synced 2025-05-03 10:00:07 -04:00
Resize multiple elements (rectangles, diamonds and ellipses onl… (#1193)
* experiment resizing multiple elements * hack common component * calculate scale properly (still se only)fg * prioritize multi selection * take handle offset into calculation * fix master merge * refactor resizeElements out from App * wip: handlerRectanglesFromCoords * fix test with type assertion * properly show handles wip * revert previous one and do a tweak * remove unnecessary assignments * replace hack code with good one * refactor coords in arg * resize NW * resize from sw,ne * fix with setResizeHandle * do not show hint while resizing multiple elements * empty commit * fix format
This commit is contained in:
parent
b60f5fcf06
commit
2cc1105ff5
9 changed files with 808 additions and 510 deletions
|
@ -4,14 +4,12 @@ import socketIOClient from "socket.io-client";
|
|||
import rough from "roughjs/bin/rough";
|
||||
import { RoughCanvas } from "roughjs/bin/canvas";
|
||||
import { FlooredNumber } from "../types";
|
||||
import { getElementAbsoluteCoords } from "../element/bounds";
|
||||
|
||||
import {
|
||||
newElement,
|
||||
newTextElement,
|
||||
duplicateElement,
|
||||
resizeTest,
|
||||
normalizeResizeHandle,
|
||||
isInvisiblySmallElement,
|
||||
isTextElement,
|
||||
textWysiwyg,
|
||||
|
@ -24,6 +22,11 @@ import {
|
|||
getSyncableElements,
|
||||
hasNonDeletedElements,
|
||||
newLinearElement,
|
||||
ResizeArrowFnType,
|
||||
resizeElements,
|
||||
getElementWithResizeHandler,
|
||||
canResizeMutlipleElements,
|
||||
getResizeHandlerFromCoords,
|
||||
} from "../element";
|
||||
import {
|
||||
deleteSelectedElements,
|
||||
|
@ -50,12 +53,7 @@ import {
|
|||
|
||||
import { renderScene } from "../renderer";
|
||||
import { AppState, GestureEvent, Gesture } from "../types";
|
||||
import {
|
||||
ExcalidrawElement,
|
||||
ExcalidrawLinearElement,
|
||||
ExcalidrawTextElement,
|
||||
} from "../element/types";
|
||||
import { rotate, adjustXYWithRotation } from "../math";
|
||||
import { ExcalidrawElement, ExcalidrawTextElement } from "../element/types";
|
||||
|
||||
import {
|
||||
isWritableElement,
|
||||
|
@ -76,7 +74,6 @@ import { createHistory, SceneHistory } from "../history";
|
|||
|
||||
import ContextMenu from "./ContextMenu";
|
||||
|
||||
import { getElementWithResizeHandler } from "../element/resizeTest";
|
||||
import { ActionManager } from "../actions/manager";
|
||||
import "../actions";
|
||||
import { actions } from "../actions/register";
|
||||
|
@ -102,7 +99,6 @@ import {
|
|||
DRAGGING_THRESHOLD,
|
||||
TEXT_TO_CENTER_SNAP_THRESHOLD,
|
||||
ARROW_CONFIRM_THRESHOLD,
|
||||
SHIFT_LOCKING_ANGLE,
|
||||
} from "../constants";
|
||||
import { LayerUI } from "./LayerUI";
|
||||
import { ScrollBars, SceneState } from "../scene/types";
|
||||
|
@ -112,7 +108,6 @@ import { invalidateShapeForElement } from "../renderer/renderElement";
|
|||
import { unstable_batchedUpdates } from "react-dom";
|
||||
import { SceneStateCallbackRemover } from "../scene/globalScene";
|
||||
import { isLinearElement } from "../element/typeChecks";
|
||||
import { rescalePoints } from "../points";
|
||||
import { actionFinalize } from "../actions";
|
||||
|
||||
/**
|
||||
|
@ -929,7 +924,7 @@ export class App extends React.Component<any, AppState> {
|
|||
};
|
||||
});
|
||||
});
|
||||
this.socket.on("new-user", async (socketID: string) => {
|
||||
this.socket.on("new-user", async (_socketID: string) => {
|
||||
this.broadcastScene("SCENE_INIT");
|
||||
});
|
||||
|
||||
|
@ -1485,19 +1480,34 @@ export class App extends React.Component<any, AppState> {
|
|||
this.state,
|
||||
);
|
||||
if (selectedElements.length === 1 && !isOverScrollBar) {
|
||||
const resizeElement = getElementWithResizeHandler(
|
||||
const elementWithResizeHandler = getElementWithResizeHandler(
|
||||
globalSceneState.getAllElements(),
|
||||
this.state,
|
||||
{ x, y },
|
||||
this.state.zoom,
|
||||
event.pointerType,
|
||||
);
|
||||
if (resizeElement && resizeElement.resizeHandle) {
|
||||
if (elementWithResizeHandler && elementWithResizeHandler.resizeHandle) {
|
||||
document.documentElement.style.cursor = getCursorForResizingElement(
|
||||
resizeElement,
|
||||
elementWithResizeHandler,
|
||||
);
|
||||
return;
|
||||
}
|
||||
} else if (selectedElements.length > 1 && !isOverScrollBar) {
|
||||
if (canResizeMutlipleElements(selectedElements)) {
|
||||
const resizeHandle = getResizeHandlerFromCoords(
|
||||
getCommonBounds(selectedElements),
|
||||
{ x, y },
|
||||
this.state.zoom,
|
||||
event.pointerType,
|
||||
);
|
||||
if (resizeHandle) {
|
||||
document.documentElement.style.cursor = getCursorForResizingElement({
|
||||
resizeHandle,
|
||||
});
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
const hitElement = getElementAtPosition(
|
||||
globalSceneState.getAllElements(),
|
||||
|
@ -1691,34 +1701,57 @@ export class App extends React.Component<any, AppState> {
|
|||
|
||||
type ResizeTestType = ReturnType<typeof resizeTest>;
|
||||
let resizeHandle: ResizeTestType = false;
|
||||
const setResizeHandle = (nextResizeHandle: ResizeTestType) => {
|
||||
resizeHandle = nextResizeHandle;
|
||||
};
|
||||
let isResizingElements = false;
|
||||
let draggingOccurred = false;
|
||||
let hitElement: ExcalidrawElement | null = null;
|
||||
let hitElementWasAddedToSelection = false;
|
||||
if (this.state.elementType === "selection") {
|
||||
const resizeElement = getElementWithResizeHandler(
|
||||
globalSceneState.getAllElements(),
|
||||
this.state,
|
||||
{ x, y },
|
||||
this.state.zoom,
|
||||
event.pointerType,
|
||||
);
|
||||
|
||||
const selectedElements = getSelectedElements(
|
||||
globalSceneState.getAllElements(),
|
||||
this.state,
|
||||
);
|
||||
if (selectedElements.length === 1 && resizeElement) {
|
||||
this.setState({
|
||||
resizingElement: resizeElement ? resizeElement.element : null,
|
||||
});
|
||||
|
||||
resizeHandle = resizeElement.resizeHandle;
|
||||
document.documentElement.style.cursor = getCursorForResizingElement(
|
||||
resizeElement,
|
||||
if (selectedElements.length === 1) {
|
||||
const elementWithResizeHandler = getElementWithResizeHandler(
|
||||
globalSceneState.getAllElements(),
|
||||
this.state,
|
||||
{ x, y },
|
||||
this.state.zoom,
|
||||
event.pointerType,
|
||||
);
|
||||
isResizingElements = true;
|
||||
} else {
|
||||
if (elementWithResizeHandler) {
|
||||
this.setState({
|
||||
resizingElement: elementWithResizeHandler
|
||||
? elementWithResizeHandler.element
|
||||
: null,
|
||||
});
|
||||
resizeHandle = elementWithResizeHandler.resizeHandle;
|
||||
document.documentElement.style.cursor = getCursorForResizingElement(
|
||||
elementWithResizeHandler,
|
||||
);
|
||||
isResizingElements = true;
|
||||
}
|
||||
} else if (selectedElements.length > 1) {
|
||||
if (canResizeMutlipleElements(selectedElements)) {
|
||||
resizeHandle = getResizeHandlerFromCoords(
|
||||
getCommonBounds(selectedElements),
|
||||
{ x, y },
|
||||
this.state.zoom,
|
||||
event.pointerType,
|
||||
);
|
||||
if (resizeHandle) {
|
||||
document.documentElement.style.cursor = getCursorForResizingElement(
|
||||
{
|
||||
resizeHandle,
|
||||
},
|
||||
);
|
||||
isResizingElements = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (!isResizingElements) {
|
||||
hitElement = getElementAtPosition(
|
||||
globalSceneState.getAllElements(),
|
||||
this.state,
|
||||
|
@ -1908,82 +1941,9 @@ export class App extends React.Component<any, AppState> {
|
|||
}
|
||||
}
|
||||
|
||||
let resizeArrowFn:
|
||||
| ((
|
||||
element: ExcalidrawLinearElement,
|
||||
pointIndex: number,
|
||||
deltaX: number,
|
||||
deltaY: number,
|
||||
pointerX: number,
|
||||
pointerY: number,
|
||||
perfect: boolean,
|
||||
) => void)
|
||||
| null = null;
|
||||
|
||||
const arrowResizeOrigin = (
|
||||
element: ExcalidrawLinearElement,
|
||||
pointIndex: number,
|
||||
deltaX: number,
|
||||
deltaY: number,
|
||||
pointerX: number,
|
||||
pointerY: number,
|
||||
perfect: boolean,
|
||||
) => {
|
||||
const [px, py] = element.points[pointIndex];
|
||||
let x = element.x + deltaX;
|
||||
let y = element.y + deltaY;
|
||||
let pointX = px - deltaX;
|
||||
let pointY = py - deltaY;
|
||||
|
||||
if (perfect) {
|
||||
const { width, height } = getPerfectElementSize(
|
||||
element.type,
|
||||
px + element.x - pointerX,
|
||||
py + element.y - pointerY,
|
||||
);
|
||||
x = px + element.x - width;
|
||||
y = py + element.y - height;
|
||||
pointX = width;
|
||||
pointY = height;
|
||||
}
|
||||
|
||||
mutateElement(element, {
|
||||
x,
|
||||
y,
|
||||
points: element.points.map((point, i) =>
|
||||
i === pointIndex ? ([pointX, pointY] as const) : point,
|
||||
),
|
||||
});
|
||||
};
|
||||
|
||||
const arrowResizeEnd = (
|
||||
element: ExcalidrawLinearElement,
|
||||
pointIndex: number,
|
||||
deltaX: number,
|
||||
deltaY: number,
|
||||
pointerX: number,
|
||||
pointerY: number,
|
||||
perfect: boolean,
|
||||
) => {
|
||||
const [px, py] = element.points[pointIndex];
|
||||
if (perfect) {
|
||||
const { width, height } = getPerfectElementSize(
|
||||
element.type,
|
||||
pointerX - element.x,
|
||||
pointerY - element.y,
|
||||
);
|
||||
mutateElement(element, {
|
||||
points: element.points.map((point, i) =>
|
||||
i === pointIndex ? ([width, height] as const) : point,
|
||||
),
|
||||
});
|
||||
} else {
|
||||
mutateElement(element, {
|
||||
points: element.points.map((point, i) =>
|
||||
i === pointIndex ? ([px + deltaX, py + deltaY] as const) : point,
|
||||
),
|
||||
});
|
||||
}
|
||||
let resizeArrowFn: ResizeArrowFnType | null = null;
|
||||
const setResizeArrrowFn = (fn: ResizeArrowFnType) => {
|
||||
resizeArrowFn = fn;
|
||||
};
|
||||
|
||||
const onPointerMove = withBatchedUpdates((event: PointerEvent) => {
|
||||
|
@ -2012,6 +1972,13 @@ export class App extends React.Component<any, AppState> {
|
|||
return;
|
||||
}
|
||||
|
||||
const { x, y } = viewportCoordsToSceneCoords(
|
||||
event,
|
||||
this.state,
|
||||
this.canvas,
|
||||
window.devicePixelRatio,
|
||||
);
|
||||
|
||||
// for arrows, don't start dragging until a given threshold
|
||||
// to ensure we don't create a 2-point arrow by mistake when
|
||||
// user clicks mouse in a way that it moves a tiny bit (thus
|
||||
|
@ -2021,289 +1988,30 @@ export class App extends React.Component<any, AppState> {
|
|||
(this.state.elementType === "arrow" ||
|
||||
this.state.elementType === "line")
|
||||
) {
|
||||
const { x, y } = viewportCoordsToSceneCoords(
|
||||
event,
|
||||
this.state,
|
||||
this.canvas,
|
||||
window.devicePixelRatio,
|
||||
);
|
||||
if (distance2d(x, y, originX, originY) < DRAGGING_THRESHOLD) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (isResizingElements && this.state.resizingElement) {
|
||||
this.setState({
|
||||
isResizing: resizeHandle !== "rotation",
|
||||
isRotating: resizeHandle === "rotation",
|
||||
});
|
||||
const el = this.state.resizingElement;
|
||||
const selectedElements = getSelectedElements(
|
||||
globalSceneState.getAllElements(),
|
||||
const resized =
|
||||
isResizingElements &&
|
||||
resizeElements(
|
||||
resizeHandle,
|
||||
setResizeHandle,
|
||||
this.state,
|
||||
this.setAppState,
|
||||
resizeArrowFn,
|
||||
setResizeArrrowFn,
|
||||
event,
|
||||
x,
|
||||
y,
|
||||
lastX,
|
||||
lastY,
|
||||
);
|
||||
if (selectedElements.length === 1) {
|
||||
const { x, y } = viewportCoordsToSceneCoords(
|
||||
event,
|
||||
this.state,
|
||||
this.canvas,
|
||||
window.devicePixelRatio,
|
||||
);
|
||||
const element = selectedElements[0];
|
||||
const angle = element.angle;
|
||||
// reverse rotate delta
|
||||
const [deltaX, deltaY] = rotate(x - lastX, y - lastY, 0, 0, -angle);
|
||||
switch (resizeHandle) {
|
||||
case "nw":
|
||||
if (isLinearElement(element) && element.points.length === 2) {
|
||||
const [, p1] = element.points;
|
||||
|
||||
if (!resizeArrowFn) {
|
||||
if (p1[0] < 0 || p1[1] < 0) {
|
||||
resizeArrowFn = arrowResizeEnd;
|
||||
} else {
|
||||
resizeArrowFn = arrowResizeOrigin;
|
||||
}
|
||||
}
|
||||
resizeArrowFn(element, 1, deltaX, deltaY, x, y, event.shiftKey);
|
||||
} else {
|
||||
const width = element.width - deltaX;
|
||||
const height = event.shiftKey ? width : element.height - deltaY;
|
||||
const dY = element.height - height;
|
||||
mutateElement(element, {
|
||||
width,
|
||||
height,
|
||||
...adjustXYWithRotation("nw", element, deltaX, dY, angle),
|
||||
...(isLinearElement(element) && width >= 0 && height >= 0
|
||||
? {
|
||||
points: rescalePoints(
|
||||
0,
|
||||
width,
|
||||
rescalePoints(1, height, element.points),
|
||||
),
|
||||
}
|
||||
: {}),
|
||||
});
|
||||
}
|
||||
break;
|
||||
case "ne":
|
||||
if (isLinearElement(element) && element.points.length === 2) {
|
||||
const [, p1] = element.points;
|
||||
if (!resizeArrowFn) {
|
||||
if (p1[0] >= 0) {
|
||||
resizeArrowFn = arrowResizeEnd;
|
||||
} else {
|
||||
resizeArrowFn = arrowResizeOrigin;
|
||||
}
|
||||
}
|
||||
resizeArrowFn(element, 1, deltaX, deltaY, x, y, event.shiftKey);
|
||||
} else {
|
||||
const width = element.width + deltaX;
|
||||
const height = event.shiftKey ? width : element.height - deltaY;
|
||||
const dY = element.height - height;
|
||||
mutateElement(element, {
|
||||
width,
|
||||
height,
|
||||
...adjustXYWithRotation("ne", element, deltaX, dY, angle),
|
||||
...(isLinearElement(element) && width >= 0 && height >= 0
|
||||
? {
|
||||
points: rescalePoints(
|
||||
0,
|
||||
width,
|
||||
rescalePoints(1, height, element.points),
|
||||
),
|
||||
}
|
||||
: {}),
|
||||
});
|
||||
}
|
||||
break;
|
||||
case "sw":
|
||||
if (isLinearElement(element) && element.points.length === 2) {
|
||||
const [, p1] = element.points;
|
||||
if (!resizeArrowFn) {
|
||||
if (p1[0] <= 0) {
|
||||
resizeArrowFn = arrowResizeEnd;
|
||||
} else {
|
||||
resizeArrowFn = arrowResizeOrigin;
|
||||
}
|
||||
}
|
||||
resizeArrowFn(element, 1, deltaX, deltaY, x, y, event.shiftKey);
|
||||
} else {
|
||||
const width = element.width - deltaX;
|
||||
const height = event.shiftKey ? width : element.height + deltaY;
|
||||
const dY = height - element.height;
|
||||
mutateElement(element, {
|
||||
width,
|
||||
height,
|
||||
...adjustXYWithRotation("sw", element, deltaX, dY, angle),
|
||||
...(isLinearElement(element) && width >= 0 && height >= 0
|
||||
? {
|
||||
points: rescalePoints(
|
||||
0,
|
||||
width,
|
||||
rescalePoints(1, height, element.points),
|
||||
),
|
||||
}
|
||||
: {}),
|
||||
});
|
||||
}
|
||||
break;
|
||||
case "se":
|
||||
if (isLinearElement(element) && element.points.length === 2) {
|
||||
const [, p1] = element.points;
|
||||
if (!resizeArrowFn) {
|
||||
if (p1[0] > 0 || p1[1] > 0) {
|
||||
resizeArrowFn = arrowResizeEnd;
|
||||
} else {
|
||||
resizeArrowFn = arrowResizeOrigin;
|
||||
}
|
||||
}
|
||||
resizeArrowFn(element, 1, deltaX, deltaY, x, y, event.shiftKey);
|
||||
} else {
|
||||
const width = element.width + deltaX;
|
||||
const height = event.shiftKey ? width : element.height + deltaY;
|
||||
const dY = height - element.height;
|
||||
mutateElement(element, {
|
||||
width,
|
||||
height,
|
||||
...adjustXYWithRotation("se", element, deltaX, dY, angle),
|
||||
...(isLinearElement(element) && width >= 0 && height >= 0
|
||||
? {
|
||||
points: rescalePoints(
|
||||
0,
|
||||
width,
|
||||
rescalePoints(1, height, element.points),
|
||||
),
|
||||
}
|
||||
: {}),
|
||||
});
|
||||
}
|
||||
break;
|
||||
case "n": {
|
||||
const height = element.height - deltaY;
|
||||
|
||||
if (isLinearElement(element)) {
|
||||
if (element.points.length > 2 && height <= 0) {
|
||||
// Someday we should implement logic to flip the shape.
|
||||
// But for now, just stop.
|
||||
break;
|
||||
}
|
||||
mutateElement(element, {
|
||||
height,
|
||||
...adjustXYWithRotation("n", element, 0, deltaY, angle),
|
||||
points: rescalePoints(1, height, element.points),
|
||||
});
|
||||
} else {
|
||||
mutateElement(element, {
|
||||
height,
|
||||
...adjustXYWithRotation("n", element, 0, deltaY, angle),
|
||||
});
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
case "w": {
|
||||
const width = element.width - deltaX;
|
||||
|
||||
if (isLinearElement(element)) {
|
||||
if (element.points.length > 2 && width <= 0) {
|
||||
// Someday we should implement logic to flip the shape.
|
||||
// But for now, just stop.
|
||||
break;
|
||||
}
|
||||
|
||||
mutateElement(element, {
|
||||
width,
|
||||
...adjustXYWithRotation("w", element, deltaX, 0, angle),
|
||||
points: rescalePoints(0, width, element.points),
|
||||
});
|
||||
} else {
|
||||
mutateElement(element, {
|
||||
width,
|
||||
...adjustXYWithRotation("w", element, deltaX, 0, angle),
|
||||
});
|
||||
}
|
||||
break;
|
||||
}
|
||||
case "s": {
|
||||
const height = element.height + deltaY;
|
||||
|
||||
if (isLinearElement(element)) {
|
||||
if (element.points.length > 2 && height <= 0) {
|
||||
// Someday we should implement logic to flip the shape.
|
||||
// But for now, just stop.
|
||||
break;
|
||||
}
|
||||
mutateElement(element, {
|
||||
height,
|
||||
...adjustXYWithRotation("s", element, 0, deltaY, angle),
|
||||
points: rescalePoints(1, height, element.points),
|
||||
});
|
||||
} else {
|
||||
mutateElement(element, {
|
||||
height,
|
||||
...adjustXYWithRotation("s", element, 0, deltaY, angle),
|
||||
});
|
||||
}
|
||||
break;
|
||||
}
|
||||
case "e": {
|
||||
const width = element.width + deltaX;
|
||||
|
||||
if (isLinearElement(element)) {
|
||||
if (element.points.length > 2 && width <= 0) {
|
||||
// Someday we should implement logic to flip the shape.
|
||||
// But for now, just stop.
|
||||
break;
|
||||
}
|
||||
mutateElement(element, {
|
||||
width,
|
||||
...adjustXYWithRotation("e", element, deltaX, 0, angle),
|
||||
points: rescalePoints(0, width, element.points),
|
||||
});
|
||||
} else {
|
||||
mutateElement(element, {
|
||||
width,
|
||||
...adjustXYWithRotation("e", element, deltaX, 0, angle),
|
||||
});
|
||||
}
|
||||
break;
|
||||
}
|
||||
case "rotation": {
|
||||
const [x1, y1, x2, y2] = getElementAbsoluteCoords(element);
|
||||
const cx = (x1 + x2) / 2;
|
||||
const cy = (y1 + y2) / 2;
|
||||
let angle = (5 * Math.PI) / 2 + Math.atan2(y - cy, x - cx);
|
||||
if (event.shiftKey) {
|
||||
angle += SHIFT_LOCKING_ANGLE / 2;
|
||||
angle -= angle % SHIFT_LOCKING_ANGLE;
|
||||
}
|
||||
if (angle >= 2 * Math.PI) {
|
||||
angle -= 2 * Math.PI;
|
||||
}
|
||||
mutateElement(element, { angle });
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (resizeHandle) {
|
||||
resizeHandle = normalizeResizeHandle(element, resizeHandle);
|
||||
}
|
||||
normalizeDimensions(element);
|
||||
|
||||
document.documentElement.style.cursor = getCursorForResizingElement({
|
||||
element,
|
||||
resizeHandle,
|
||||
});
|
||||
mutateElement(el, {
|
||||
x: element.x,
|
||||
y: element.y,
|
||||
});
|
||||
|
||||
lastX = x;
|
||||
lastY = y;
|
||||
return;
|
||||
}
|
||||
if (resized) {
|
||||
lastX = x;
|
||||
lastY = y;
|
||||
return;
|
||||
}
|
||||
|
||||
if (hitElement && this.state.selectedElementIds[hitElement.id]) {
|
||||
|
@ -2341,13 +2049,6 @@ export class App extends React.Component<any, AppState> {
|
|||
return;
|
||||
}
|
||||
|
||||
const { x, y } = viewportCoordsToSceneCoords(
|
||||
event,
|
||||
this.state,
|
||||
this.canvas,
|
||||
window.devicePixelRatio,
|
||||
);
|
||||
|
||||
let width = distance(originX, x);
|
||||
let height = distance(originY, y);
|
||||
|
||||
|
@ -2533,7 +2234,7 @@ export class App extends React.Component<any, AppState> {
|
|||
},
|
||||
}));
|
||||
} else {
|
||||
this.setState((prevState) => ({
|
||||
this.setState((_prevState) => ({
|
||||
selectedElementIds: { [hitElement!.id]: true },
|
||||
}));
|
||||
}
|
||||
|
|
|
@ -22,8 +22,12 @@ const getHints = ({ appState, elements }: Hint) => {
|
|||
return t("hints.linearElementMulti");
|
||||
}
|
||||
|
||||
if (isResizing && lastPointerDownWith === "mouse") {
|
||||
const selectedElements = getSelectedElements(elements, appState);
|
||||
const selectedElements = getSelectedElements(elements, appState);
|
||||
if (
|
||||
isResizing &&
|
||||
lastPointerDownWith === "mouse" &&
|
||||
selectedElements.length === 1
|
||||
) {
|
||||
const targetElement = selectedElements[0];
|
||||
if (isLinearElement(targetElement) && targetElement.points.length > 2) {
|
||||
return null;
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue