fix resizing lines with abs coords bigger than element w/h (#1427)
This commit is contained in:
parent
686af31d9d
commit
73d8c5b7c1
@ -1,9 +1,11 @@
|
|||||||
import { ExcalidrawElement, ExcalidrawLinearElement } from "./types";
|
import { ExcalidrawElement, ExcalidrawLinearElement } from "./types";
|
||||||
import { rotate } from "../math";
|
import { rotate } from "../math";
|
||||||
import { Drawable, Op } from "roughjs/bin/core";
|
import rough from "roughjs/bin/rough";
|
||||||
|
import { Drawable, Op, Options } from "roughjs/bin/core";
|
||||||
import { Point } from "../types";
|
import { Point } from "../types";
|
||||||
import { getShapeForElement } from "../renderer/renderElement";
|
import { getShapeForElement } from "../renderer/renderElement";
|
||||||
import { isLinearElement } from "./typeChecks";
|
import { isLinearElement } from "./typeChecks";
|
||||||
|
import { rescalePoints } from "../points";
|
||||||
|
|
||||||
// If the element is created from right to left, the width is going to be negative
|
// If the element is created from right to left, the width is going to be negative
|
||||||
// This set of functions retrieves the absolute position of the 4 points.
|
// This set of functions retrieves the absolute position of the 4 points.
|
||||||
@ -11,7 +13,7 @@ export function getElementAbsoluteCoords(
|
|||||||
element: ExcalidrawElement,
|
element: ExcalidrawElement,
|
||||||
): [number, number, number, number] {
|
): [number, number, number, number] {
|
||||||
if (isLinearElement(element)) {
|
if (isLinearElement(element)) {
|
||||||
return getLinearElementAbsoluteBounds(element);
|
return getLinearElementAbsoluteCoords(element);
|
||||||
}
|
}
|
||||||
return [
|
return [
|
||||||
element.x,
|
element.x,
|
||||||
@ -45,35 +47,10 @@ export function getCurvePathOps(shape: Drawable): Op[] {
|
|||||||
return shape.sets[0].ops;
|
return shape.sets[0].ops;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getLinearElementAbsoluteBounds(
|
const getMinMaxXYFromCurvePathOps = (
|
||||||
element: ExcalidrawLinearElement,
|
ops: Op[],
|
||||||
): [number, number, number, number] {
|
transformXY?: (x: number, y: number) => [number, number],
|
||||||
if (element.points.length < 2 || !getShapeForElement(element)) {
|
): [number, number, number, number] => {
|
||||||
const { minX, minY, maxX, maxY } = element.points.reduce(
|
|
||||||
(limits, [x, y]) => {
|
|
||||||
limits.minY = Math.min(limits.minY, y);
|
|
||||||
limits.minX = Math.min(limits.minX, x);
|
|
||||||
|
|
||||||
limits.maxX = Math.max(limits.maxX, x);
|
|
||||||
limits.maxY = Math.max(limits.maxY, y);
|
|
||||||
|
|
||||||
return limits;
|
|
||||||
},
|
|
||||||
{ minX: Infinity, minY: Infinity, maxX: -Infinity, maxY: -Infinity },
|
|
||||||
);
|
|
||||||
return [
|
|
||||||
minX + element.x,
|
|
||||||
minY + element.y,
|
|
||||||
maxX + element.x,
|
|
||||||
maxY + element.y,
|
|
||||||
];
|
|
||||||
}
|
|
||||||
|
|
||||||
const shape = getShapeForElement(element) as Drawable[];
|
|
||||||
|
|
||||||
// first element is always the curve
|
|
||||||
const ops = getCurvePathOps(shape[0]);
|
|
||||||
|
|
||||||
let currentP: Point = [0, 0];
|
let currentP: Point = [0, 0];
|
||||||
|
|
||||||
const { minX, minY, maxX, maxY } = ops.reduce(
|
const { minX, minY, maxX, maxY } = ops.reduce(
|
||||||
@ -104,8 +81,11 @@ export function getLinearElementAbsoluteBounds(
|
|||||||
|
|
||||||
let t = 0;
|
let t = 0;
|
||||||
while (t <= 1.0) {
|
while (t <= 1.0) {
|
||||||
const x = equation(t, 0);
|
let x = equation(t, 0);
|
||||||
const y = equation(t, 1);
|
let y = equation(t, 1);
|
||||||
|
if (transformXY) {
|
||||||
|
[x, y] = transformXY(x, y);
|
||||||
|
}
|
||||||
|
|
||||||
limits.minY = Math.min(limits.minY, y);
|
limits.minY = Math.min(limits.minY, y);
|
||||||
limits.minX = Math.min(limits.minX, x);
|
limits.minX = Math.min(limits.minX, x);
|
||||||
@ -125,13 +105,47 @@ export function getLinearElementAbsoluteBounds(
|
|||||||
{ minX: Infinity, minY: Infinity, maxX: -Infinity, maxY: -Infinity },
|
{ minX: Infinity, minY: Infinity, maxX: -Infinity, maxY: -Infinity },
|
||||||
);
|
);
|
||||||
|
|
||||||
|
return [minX, minY, maxX, maxY];
|
||||||
|
};
|
||||||
|
|
||||||
|
const getLinearElementAbsoluteCoords = (
|
||||||
|
element: ExcalidrawLinearElement,
|
||||||
|
): [number, number, number, number] => {
|
||||||
|
if (element.points.length < 2 || !getShapeForElement(element)) {
|
||||||
|
const { minX, minY, maxX, maxY } = element.points.reduce(
|
||||||
|
(limits, [x, y]) => {
|
||||||
|
limits.minY = Math.min(limits.minY, y);
|
||||||
|
limits.minX = Math.min(limits.minX, x);
|
||||||
|
|
||||||
|
limits.maxX = Math.max(limits.maxX, x);
|
||||||
|
limits.maxY = Math.max(limits.maxY, y);
|
||||||
|
|
||||||
|
return limits;
|
||||||
|
},
|
||||||
|
{ minX: Infinity, minY: Infinity, maxX: -Infinity, maxY: -Infinity },
|
||||||
|
);
|
||||||
|
return [
|
||||||
|
minX + element.x,
|
||||||
|
minY + element.y,
|
||||||
|
maxX + element.x,
|
||||||
|
maxY + element.y,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
const shape = getShapeForElement(element) as Drawable[];
|
||||||
|
|
||||||
|
// first element is always the curve
|
||||||
|
const ops = getCurvePathOps(shape[0]);
|
||||||
|
|
||||||
|
const [minX, minY, maxX, maxY] = getMinMaxXYFromCurvePathOps(ops);
|
||||||
|
|
||||||
return [
|
return [
|
||||||
minX + element.x,
|
minX + element.x,
|
||||||
minY + element.y,
|
minY + element.y,
|
||||||
maxX + element.x,
|
maxX + element.x,
|
||||||
maxY + element.y,
|
maxY + element.y,
|
||||||
];
|
];
|
||||||
}
|
};
|
||||||
|
|
||||||
export function getArrowPoints(
|
export function getArrowPoints(
|
||||||
element: ExcalidrawLinearElement,
|
element: ExcalidrawLinearElement,
|
||||||
@ -197,8 +211,6 @@ export function getArrowPoints(
|
|||||||
return [x2, y2, x3, y3, x4, y4];
|
return [x2, y2, x3, y3, x4, y4];
|
||||||
}
|
}
|
||||||
|
|
||||||
// this function has some code in common with getLinearElementAbsoluteBounds
|
|
||||||
// there might be more efficient way
|
|
||||||
const getLinearElementRotatedBounds = (
|
const getLinearElementRotatedBounds = (
|
||||||
element: ExcalidrawLinearElement,
|
element: ExcalidrawLinearElement,
|
||||||
cx: number,
|
cx: number,
|
||||||
@ -224,56 +236,9 @@ const getLinearElementRotatedBounds = (
|
|||||||
// first element is always the curve
|
// first element is always the curve
|
||||||
const ops = getCurvePathOps(shape[0]);
|
const ops = getCurvePathOps(shape[0]);
|
||||||
|
|
||||||
let currentP: Point = [0, 0];
|
const transformXY = (x: number, y: number) =>
|
||||||
|
rotate(element.x + x, element.y + y, cx, cy, element.angle);
|
||||||
const { minX, minY, maxX, maxY } = ops.reduce(
|
return getMinMaxXYFromCurvePathOps(ops, transformXY);
|
||||||
(limits, { op, data }) => {
|
|
||||||
// There are only four operation types:
|
|
||||||
// move, bcurveTo, lineTo, and curveTo
|
|
||||||
if (op === "move") {
|
|
||||||
// change starting point
|
|
||||||
currentP = (data as unknown) as Point;
|
|
||||||
// move operation does not draw anything; so, it always
|
|
||||||
// returns false
|
|
||||||
} else if (op === "bcurveTo") {
|
|
||||||
// create points from bezier curve
|
|
||||||
// bezier curve stores data as a flattened array of three positions
|
|
||||||
// [x1, y1, x2, y2, x3, y3]
|
|
||||||
const p1 = [data[0], data[1]] as Point;
|
|
||||||
const p2 = [data[2], data[3]] as Point;
|
|
||||||
const p3 = [data[4], data[5]] as Point;
|
|
||||||
|
|
||||||
const p0 = currentP;
|
|
||||||
currentP = p3;
|
|
||||||
|
|
||||||
const equation = (t: number, idx: number) =>
|
|
||||||
Math.pow(1 - t, 3) * p3[idx] +
|
|
||||||
3 * t * Math.pow(1 - t, 2) * p2[idx] +
|
|
||||||
3 * Math.pow(t, 2) * (1 - t) * p1[idx] +
|
|
||||||
p0[idx] * Math.pow(t, 3);
|
|
||||||
|
|
||||||
let t = 0;
|
|
||||||
while (t <= 1.0) {
|
|
||||||
let x = equation(t, 0);
|
|
||||||
let y = equation(t, 1);
|
|
||||||
[x, y] = rotate(element.x + x, element.y + y, cx, cy, element.angle);
|
|
||||||
limits.minY = Math.min(limits.minY, y);
|
|
||||||
limits.minX = Math.min(limits.minX, x);
|
|
||||||
limits.maxX = Math.max(limits.maxX, x);
|
|
||||||
limits.maxY = Math.max(limits.maxY, y);
|
|
||||||
t += 0.1;
|
|
||||||
}
|
|
||||||
} else if (op === "lineTo") {
|
|
||||||
// TODO: Implement this
|
|
||||||
} else if (op === "qcurveTo") {
|
|
||||||
// TODO: Implement this
|
|
||||||
}
|
|
||||||
return limits;
|
|
||||||
},
|
|
||||||
{ minX: Infinity, minY: Infinity, maxX: -Infinity, maxY: -Infinity },
|
|
||||||
);
|
|
||||||
|
|
||||||
return [minX, minY, maxX, maxY];
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export const getElementBounds = (
|
export const getElementBounds = (
|
||||||
@ -338,3 +303,40 @@ export const getCommonBounds = (
|
|||||||
|
|
||||||
return [minX, minY, maxX, maxY];
|
return [minX, minY, maxX, maxY];
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const getResizedElementAbsoluteCoords = (
|
||||||
|
element: ExcalidrawElement,
|
||||||
|
nextWidth: number,
|
||||||
|
nextHeight: number,
|
||||||
|
): [number, number, number, number] => {
|
||||||
|
if (!isLinearElement(element) || element.points.length <= 2) {
|
||||||
|
return [
|
||||||
|
element.x,
|
||||||
|
element.y,
|
||||||
|
element.x + nextWidth,
|
||||||
|
element.y + nextHeight,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
const points = rescalePoints(
|
||||||
|
0,
|
||||||
|
nextWidth,
|
||||||
|
rescalePoints(1, nextHeight, element.points),
|
||||||
|
);
|
||||||
|
|
||||||
|
const options: Options = {
|
||||||
|
strokeWidth: element.strokeWidth,
|
||||||
|
roughness: element.roughness,
|
||||||
|
seed: element.seed,
|
||||||
|
};
|
||||||
|
const gen = rough.generator();
|
||||||
|
const curve = gen.curve(points as [number, number][], options);
|
||||||
|
const ops = getCurvePathOps(curve);
|
||||||
|
const [minX, minY, maxX, maxY] = getMinMaxXYFromCurvePathOps(ops);
|
||||||
|
return [
|
||||||
|
minX + element.x,
|
||||||
|
minY + element.y,
|
||||||
|
maxX + element.x,
|
||||||
|
maxY + element.y,
|
||||||
|
];
|
||||||
|
};
|
||||||
|
@ -17,7 +17,6 @@ export {
|
|||||||
getCommonBounds,
|
getCommonBounds,
|
||||||
getDiamondPoints,
|
getDiamondPoints,
|
||||||
getArrowPoints,
|
getArrowPoints,
|
||||||
getLinearElementAbsoluteBounds,
|
|
||||||
} from "./bounds";
|
} from "./bounds";
|
||||||
|
|
||||||
export {
|
export {
|
||||||
|
@ -3,17 +3,21 @@ import { SHIFT_LOCKING_ANGLE } from "../constants";
|
|||||||
import { getSelectedElements, globalSceneState } from "../scene";
|
import { getSelectedElements, globalSceneState } from "../scene";
|
||||||
import { rescalePoints } from "../points";
|
import { rescalePoints } from "../points";
|
||||||
|
|
||||||
import { rotate, resizeXYWidthHightWithRotation } from "../math";
|
import { rotate, adjustXYWithRotation, getFlipAdjustment } from "../math";
|
||||||
import {
|
import {
|
||||||
ExcalidrawLinearElement,
|
ExcalidrawLinearElement,
|
||||||
NonDeletedExcalidrawElement,
|
NonDeletedExcalidrawElement,
|
||||||
NonDeleted,
|
NonDeleted,
|
||||||
ResizeArrowFnType,
|
ResizeArrowFnType,
|
||||||
} from "./types";
|
} from "./types";
|
||||||
import { getElementAbsoluteCoords, getCommonBounds } from "./bounds";
|
import {
|
||||||
|
getElementAbsoluteCoords,
|
||||||
|
getCommonBounds,
|
||||||
|
getResizedElementAbsoluteCoords,
|
||||||
|
} from "./bounds";
|
||||||
import { isLinearElement } from "./typeChecks";
|
import { isLinearElement } from "./typeChecks";
|
||||||
import { mutateElement } from "./mutateElement";
|
import { mutateElement } from "./mutateElement";
|
||||||
import { getPerfectElementSize, normalizeDimensions } from "./sizeHelpers";
|
import { getPerfectElementSize } from "./sizeHelpers";
|
||||||
import {
|
import {
|
||||||
resizeTest,
|
resizeTest,
|
||||||
getCursorForResizingElement,
|
getCursorForResizingElement,
|
||||||
@ -26,6 +30,155 @@ import {
|
|||||||
|
|
||||||
type ResizeTestType = ReturnType<typeof resizeTest>;
|
type ResizeTestType = ReturnType<typeof resizeTest>;
|
||||||
|
|
||||||
|
export const resizeElements = (
|
||||||
|
resizeHandle: ResizeTestType,
|
||||||
|
setResizeHandle: (nextResizeHandle: ResizeTestType) => void,
|
||||||
|
appState: AppState,
|
||||||
|
setAppState: (obj: any) => void,
|
||||||
|
resizeArrowFn: ResizeArrowFnType | null, // XXX eliminate in #1339
|
||||||
|
setResizeArrowFn: (fn: ResizeArrowFnType) => void, // XXX eliminate in #1339
|
||||||
|
event: PointerEvent, // XXX we want to make it independent?
|
||||||
|
xPointer: number,
|
||||||
|
yPointer: number,
|
||||||
|
lastX: number, // XXX eliminate in #1339
|
||||||
|
lastY: number, // XXX eliminate in #1339
|
||||||
|
) => {
|
||||||
|
setAppState({
|
||||||
|
isResizing: resizeHandle && resizeHandle !== "rotation",
|
||||||
|
isRotating: resizeHandle === "rotation",
|
||||||
|
});
|
||||||
|
const selectedElements = getSelectedElements(
|
||||||
|
globalSceneState.getElements(),
|
||||||
|
appState,
|
||||||
|
);
|
||||||
|
const handleOffset = 4 / appState.zoom; // XXX import constant
|
||||||
|
const dashedLinePadding = 4 / appState.zoom; // XXX import constant
|
||||||
|
const offsetPointer = handleOffset + dashedLinePadding;
|
||||||
|
if (selectedElements.length === 1) {
|
||||||
|
const [element] = selectedElements;
|
||||||
|
if (resizeHandle === "rotation") {
|
||||||
|
rotateSingleElement(element, xPointer, yPointer, event.shiftKey);
|
||||||
|
} else if (
|
||||||
|
isLinearElement(element) &&
|
||||||
|
element.points.length === 2 &&
|
||||||
|
(resizeHandle === "nw" ||
|
||||||
|
resizeHandle === "ne" ||
|
||||||
|
resizeHandle === "sw" ||
|
||||||
|
resizeHandle === "se")
|
||||||
|
) {
|
||||||
|
resizeSingleTwoPointElement(
|
||||||
|
element,
|
||||||
|
resizeHandle,
|
||||||
|
resizeArrowFn,
|
||||||
|
setResizeArrowFn,
|
||||||
|
event.shiftKey,
|
||||||
|
xPointer,
|
||||||
|
yPointer,
|
||||||
|
lastX,
|
||||||
|
lastY,
|
||||||
|
);
|
||||||
|
} else if (resizeHandle) {
|
||||||
|
resizeSingleElement(
|
||||||
|
element,
|
||||||
|
resizeHandle,
|
||||||
|
getResizeWithSidesSameLengthKey(event),
|
||||||
|
getResizeCenterPointKey(event),
|
||||||
|
xPointer,
|
||||||
|
yPointer,
|
||||||
|
offsetPointer,
|
||||||
|
);
|
||||||
|
setResizeHandle(normalizeResizeHandle(element, resizeHandle));
|
||||||
|
if (element.width < 0) {
|
||||||
|
mutateElement(element, { width: -element.width });
|
||||||
|
}
|
||||||
|
if (element.height < 0) {
|
||||||
|
mutateElement(element, { height: -element.height });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// XXX do we need this?
|
||||||
|
document.documentElement.style.cursor = getCursorForResizingElement({
|
||||||
|
element,
|
||||||
|
resizeHandle,
|
||||||
|
});
|
||||||
|
// XXX why do we need this?
|
||||||
|
if (appState.resizingElement) {
|
||||||
|
mutateElement(appState.resizingElement, {
|
||||||
|
x: element.x,
|
||||||
|
y: element.y,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
} else if (
|
||||||
|
selectedElements.length > 1 &&
|
||||||
|
(resizeHandle === "nw" ||
|
||||||
|
resizeHandle === "ne" ||
|
||||||
|
resizeHandle === "sw" ||
|
||||||
|
resizeHandle === "se")
|
||||||
|
) {
|
||||||
|
resizeMultipleElements(
|
||||||
|
selectedElements,
|
||||||
|
resizeHandle,
|
||||||
|
xPointer,
|
||||||
|
yPointer,
|
||||||
|
offsetPointer,
|
||||||
|
);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
|
||||||
|
const rotateSingleElement = (
|
||||||
|
element: NonDeletedExcalidrawElement,
|
||||||
|
xPointer: number,
|
||||||
|
yPointer: number,
|
||||||
|
isAngleLocking: boolean,
|
||||||
|
) => {
|
||||||
|
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(yPointer - cy, xPointer - cx);
|
||||||
|
if (isAngleLocking) {
|
||||||
|
angle += SHIFT_LOCKING_ANGLE / 2;
|
||||||
|
angle -= angle % SHIFT_LOCKING_ANGLE;
|
||||||
|
}
|
||||||
|
if (angle >= 2 * Math.PI) {
|
||||||
|
angle -= 2 * Math.PI;
|
||||||
|
}
|
||||||
|
mutateElement(element, { angle });
|
||||||
|
};
|
||||||
|
|
||||||
|
const resizeSingleTwoPointElement = (
|
||||||
|
element: NonDeleted<ExcalidrawLinearElement>,
|
||||||
|
resizeHandle: "nw" | "ne" | "sw" | "se",
|
||||||
|
resizeArrowFn: ResizeArrowFnType | null,
|
||||||
|
setResizeArrowFn: (fn: ResizeArrowFnType) => void,
|
||||||
|
sidesWithSameLength: boolean,
|
||||||
|
xPointer: number,
|
||||||
|
yPointer: number,
|
||||||
|
lastX: number,
|
||||||
|
lastY: number,
|
||||||
|
) => {
|
||||||
|
const [, [px, py]] = element.points;
|
||||||
|
const isResizeEnd =
|
||||||
|
(resizeHandle === "nw" && (px < 0 || py < 0)) ||
|
||||||
|
(resizeHandle === "ne" && px >= 0) ||
|
||||||
|
(resizeHandle === "sw" && px <= 0) ||
|
||||||
|
(resizeHandle === "se" && (px > 0 || py > 0));
|
||||||
|
applyResizeArrowFn(
|
||||||
|
element,
|
||||||
|
resizeArrowFn,
|
||||||
|
setResizeArrowFn,
|
||||||
|
isResizeEnd,
|
||||||
|
sidesWithSameLength,
|
||||||
|
xPointer,
|
||||||
|
yPointer,
|
||||||
|
lastX,
|
||||||
|
lastY,
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
const arrowResizeOrigin: ResizeArrowFnType = (
|
const arrowResizeOrigin: ResizeArrowFnType = (
|
||||||
element,
|
element,
|
||||||
pointIndex,
|
pointIndex,
|
||||||
@ -116,199 +269,196 @@ const applyResizeArrowFn = (
|
|||||||
setResizeArrowFn(resizeArrowFn);
|
setResizeArrowFn(resizeArrowFn);
|
||||||
};
|
};
|
||||||
|
|
||||||
export const resizeElements = (
|
const resizeSingleElement = (
|
||||||
resizeHandle: ResizeTestType,
|
element: NonDeletedExcalidrawElement,
|
||||||
setResizeHandle: (nextResizeHandle: ResizeTestType) => void,
|
resizeHandle: "n" | "s" | "w" | "e" | "nw" | "ne" | "sw" | "se",
|
||||||
appState: AppState,
|
sidesWithSameLength: boolean,
|
||||||
setAppState: (obj: any) => void,
|
isResizeFromCenter: boolean,
|
||||||
resizeArrowFn: ResizeArrowFnType | null, // XXX eliminate in #1339
|
|
||||||
setResizeArrowFn: (fn: ResizeArrowFnType) => void, // XXX eliminate in #1339
|
|
||||||
event: PointerEvent, // XXX we want to make it independent?
|
|
||||||
xPointer: number,
|
xPointer: number,
|
||||||
yPointer: number,
|
yPointer: number,
|
||||||
lastX: number, // XXX eliminate in #1339
|
offsetPointer: number,
|
||||||
lastY: number, // XXX eliminate in #1339
|
|
||||||
) => {
|
) => {
|
||||||
setAppState({
|
const [x1, y1, x2, y2] = getElementAbsoluteCoords(element);
|
||||||
isResizing: resizeHandle !== "rotation",
|
const cx = (x1 + x2) / 2;
|
||||||
isRotating: resizeHandle === "rotation",
|
const cy = (y1 + y2) / 2;
|
||||||
});
|
// rotation pointer with reverse angle
|
||||||
const selectedElements = getSelectedElements(
|
const [rotatedX, rotatedY] = rotate(
|
||||||
globalSceneState.getElements(),
|
xPointer,
|
||||||
appState,
|
yPointer,
|
||||||
|
cx,
|
||||||
|
cy,
|
||||||
|
-element.angle,
|
||||||
);
|
);
|
||||||
const handleOffset = 4 / appState.zoom; // XXX import constant
|
// XXX this might be slow with closure
|
||||||
const dashedLinePadding = 4 / appState.zoom; // XXX import constant
|
const adjustWithOffsetPointer = (wh: number) => {
|
||||||
const offsetPointer = handleOffset + dashedLinePadding;
|
if (wh > offsetPointer) {
|
||||||
const minSize = 0;
|
return wh - offsetPointer;
|
||||||
if (selectedElements.length === 1) {
|
} else if (wh < -offsetPointer) {
|
||||||
const [element] = selectedElements;
|
return wh + offsetPointer;
|
||||||
if (resizeHandle === "rotation") {
|
}
|
||||||
const [x1, y1, x2, y2] = getElementAbsoluteCoords(element);
|
return 0;
|
||||||
const cx = (x1 + x2) / 2;
|
};
|
||||||
const cy = (y1 + y2) / 2;
|
let scaleX = 1;
|
||||||
let angle = (5 * Math.PI) / 2 + Math.atan2(yPointer - cy, xPointer - cx);
|
let scaleY = 1;
|
||||||
if (event.shiftKey) {
|
if (resizeHandle === "e" || resizeHandle === "ne" || resizeHandle === "se") {
|
||||||
angle += SHIFT_LOCKING_ANGLE / 2;
|
scaleX = adjustWithOffsetPointer(rotatedX - x1) / (x2 - x1);
|
||||||
angle -= angle % SHIFT_LOCKING_ANGLE;
|
}
|
||||||
|
if (resizeHandle === "s" || resizeHandle === "sw" || resizeHandle === "se") {
|
||||||
|
scaleY = adjustWithOffsetPointer(rotatedY - y1) / (y2 - y1);
|
||||||
|
}
|
||||||
|
if (resizeHandle === "w" || resizeHandle === "nw" || resizeHandle === "sw") {
|
||||||
|
scaleX = adjustWithOffsetPointer(x2 - rotatedX) / (x2 - x1);
|
||||||
|
}
|
||||||
|
if (resizeHandle === "n" || resizeHandle === "nw" || resizeHandle === "ne") {
|
||||||
|
scaleY = adjustWithOffsetPointer(y2 - rotatedY) / (y2 - y1);
|
||||||
|
}
|
||||||
|
let nextWidth = element.width * scaleX;
|
||||||
|
let nextHeight = element.height * scaleY;
|
||||||
|
if (sidesWithSameLength) {
|
||||||
|
nextWidth = nextHeight = Math.max(nextWidth, nextHeight);
|
||||||
|
}
|
||||||
|
const [nextX1, nextY1, nextX2, nextY2] = getResizedElementAbsoluteCoords(
|
||||||
|
element,
|
||||||
|
nextWidth,
|
||||||
|
nextHeight,
|
||||||
|
);
|
||||||
|
const deltaX1 = (x1 - nextX1) / 2;
|
||||||
|
const deltaY1 = (y1 - nextY1) / 2;
|
||||||
|
const deltaX2 = (x2 - nextX2) / 2;
|
||||||
|
const deltaY2 = (y2 - nextY2) / 2;
|
||||||
|
const rescaledPoints = isLinearElement(element)
|
||||||
|
? {
|
||||||
|
points: rescalePoints(
|
||||||
|
0,
|
||||||
|
nextWidth,
|
||||||
|
rescalePoints(1, nextHeight, element.points),
|
||||||
|
),
|
||||||
}
|
}
|
||||||
if (angle >= 2 * Math.PI) {
|
: {};
|
||||||
angle -= 2 * Math.PI;
|
const [finalX1, finalY1, finalX2, finalY2] = getResizedElementAbsoluteCoords(
|
||||||
}
|
{
|
||||||
mutateElement(element, { angle });
|
...element,
|
||||||
} else if (
|
...rescaledPoints,
|
||||||
isLinearElement(element) &&
|
},
|
||||||
element.points.length === 2 &&
|
Math.abs(nextWidth),
|
||||||
(resizeHandle === "nw" ||
|
Math.abs(nextHeight),
|
||||||
resizeHandle === "ne" ||
|
);
|
||||||
resizeHandle === "sw" ||
|
const [flipDiffX, flipDiffY] = getFlipAdjustment(
|
||||||
resizeHandle === "se")
|
resizeHandle,
|
||||||
) {
|
nextWidth,
|
||||||
const [, [px, py]] = element.points;
|
nextHeight,
|
||||||
const isResizeEnd =
|
nextX1,
|
||||||
(resizeHandle === "nw" && (px < 0 || py < 0)) ||
|
nextY1,
|
||||||
(resizeHandle === "ne" && px >= 0) ||
|
nextX2,
|
||||||
(resizeHandle === "sw" && px <= 0) ||
|
nextY2,
|
||||||
(resizeHandle === "se" && (px > 0 || py > 0));
|
finalX1,
|
||||||
applyResizeArrowFn(
|
finalY1,
|
||||||
element,
|
finalX2,
|
||||||
resizeArrowFn,
|
finalY2,
|
||||||
setResizeArrowFn,
|
isLinearElement(element),
|
||||||
isResizeEnd,
|
element.angle,
|
||||||
event.shiftKey,
|
);
|
||||||
xPointer,
|
const [nextElementX, nextElementY] = adjustXYWithRotation(
|
||||||
yPointer,
|
resizeHandle,
|
||||||
lastX,
|
element.x - flipDiffX,
|
||||||
lastY,
|
element.y - flipDiffY,
|
||||||
|
element.angle,
|
||||||
|
deltaX1,
|
||||||
|
deltaY1,
|
||||||
|
deltaX2,
|
||||||
|
deltaY2,
|
||||||
|
isResizeFromCenter,
|
||||||
|
);
|
||||||
|
if (
|
||||||
|
nextWidth !== 0 &&
|
||||||
|
nextHeight !== 0 &&
|
||||||
|
Number.isFinite(nextElementX) &&
|
||||||
|
Number.isFinite(nextElementY)
|
||||||
|
) {
|
||||||
|
mutateElement(element, {
|
||||||
|
width: nextWidth,
|
||||||
|
height: nextHeight,
|
||||||
|
x: nextElementX,
|
||||||
|
y: nextElementY,
|
||||||
|
...rescaledPoints,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const resizeMultipleElements = (
|
||||||
|
elements: readonly NonDeletedExcalidrawElement[],
|
||||||
|
resizeHandle: "nw" | "ne" | "sw" | "se",
|
||||||
|
xPointer: number,
|
||||||
|
yPointer: number,
|
||||||
|
offsetPointer: number,
|
||||||
|
) => {
|
||||||
|
const [x1, y1, x2, y2] = getCommonBounds(elements);
|
||||||
|
switch (resizeHandle) {
|
||||||
|
case "se": {
|
||||||
|
const scale = Math.max(
|
||||||
|
(xPointer - offsetPointer - x1) / (x2 - x1),
|
||||||
|
(yPointer - offsetPointer - y1) / (y2 - y1),
|
||||||
);
|
);
|
||||||
} else if (resizeHandle) {
|
if (scale > 0) {
|
||||||
const [x1, y1, x2, y2] = getElementAbsoluteCoords(element);
|
elements.forEach((element) => {
|
||||||
const resized = resizeXYWidthHightWithRotation(
|
const width = element.width * scale;
|
||||||
resizeHandle,
|
const height = element.height * scale;
|
||||||
x1,
|
const x = element.x + (element.x - x1) * (scale - 1);
|
||||||
y1,
|
const y = element.y + (element.y - y1) * (scale - 1);
|
||||||
x2,
|
mutateElement(element, { width, height, x, y });
|
||||||
y2,
|
|
||||||
element.width,
|
|
||||||
element.height,
|
|
||||||
element.x,
|
|
||||||
element.y,
|
|
||||||
element.angle,
|
|
||||||
xPointer,
|
|
||||||
yPointer,
|
|
||||||
offsetPointer,
|
|
||||||
getResizeWithSidesSameLengthKey(event),
|
|
||||||
getResizeCenterPointKey(event),
|
|
||||||
);
|
|
||||||
if (
|
|
||||||
Math.abs(resized.width) > minSize &&
|
|
||||||
Math.abs(resized.height) > minSize
|
|
||||||
) {
|
|
||||||
mutateElement(element, {
|
|
||||||
...resized,
|
|
||||||
...(isLinearElement(element)
|
|
||||||
? {
|
|
||||||
points: rescalePoints(
|
|
||||||
0,
|
|
||||||
resized.width,
|
|
||||||
rescalePoints(1, resized.height, element.points),
|
|
||||||
),
|
|
||||||
}
|
|
||||||
: {}),
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
|
case "nw": {
|
||||||
if (resizeHandle) {
|
const scale = Math.max(
|
||||||
setResizeHandle(normalizeResizeHandle(element, resizeHandle));
|
(x2 - offsetPointer - xPointer) / (x2 - x1),
|
||||||
|
(y2 - offsetPointer - yPointer) / (y2 - y1),
|
||||||
|
);
|
||||||
|
if (scale > 0) {
|
||||||
|
elements.forEach((element) => {
|
||||||
|
const width = element.width * scale;
|
||||||
|
const height = element.height * scale;
|
||||||
|
const x = element.x - (x2 - element.x) * (scale - 1);
|
||||||
|
const y = element.y - (y2 - element.y) * (scale - 1);
|
||||||
|
mutateElement(element, { width, height, x, y });
|
||||||
|
});
|
||||||
|
}
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
normalizeDimensions(element);
|
case "ne": {
|
||||||
|
const scale = Math.max(
|
||||||
// do we need this?
|
(xPointer - offsetPointer - x1) / (x2 - x1),
|
||||||
document.documentElement.style.cursor = getCursorForResizingElement({
|
(y2 - offsetPointer - yPointer) / (y2 - y1),
|
||||||
element,
|
);
|
||||||
resizeHandle,
|
if (scale > 0) {
|
||||||
});
|
elements.forEach((element) => {
|
||||||
// why do we need this?
|
const width = element.width * scale;
|
||||||
if (appState.resizingElement) {
|
const height = element.height * scale;
|
||||||
mutateElement(appState.resizingElement, {
|
const x = element.x + (element.x - x1) * (scale - 1);
|
||||||
x: element.x,
|
const y = element.y - (y2 - element.y) * (scale - 1);
|
||||||
y: element.y,
|
mutateElement(element, { width, height, x, y });
|
||||||
});
|
});
|
||||||
|
}
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
|
case "sw": {
|
||||||
return true;
|
const scale = Math.max(
|
||||||
} else if (selectedElements.length > 1) {
|
(x2 - offsetPointer - xPointer) / (x2 - x1),
|
||||||
const [x1, y1, x2, y2] = getCommonBounds(selectedElements);
|
(yPointer - offsetPointer - y1) / (y2 - y1),
|
||||||
const minScale = Math.max(minSize / (x2 - x1), minSize / (y2 - y1));
|
);
|
||||||
switch (resizeHandle) {
|
if (scale > 0) {
|
||||||
case "se": {
|
elements.forEach((element) => {
|
||||||
const scale = Math.max(
|
const width = element.width * scale;
|
||||||
(xPointer - offsetPointer - x1) / (x2 - x1),
|
const height = element.height * scale;
|
||||||
(yPointer - offsetPointer - y1) / (y2 - y1),
|
const x = element.x - (x2 - element.x) * (scale - 1);
|
||||||
);
|
const y = element.y + (element.y - y1) * (scale - 1);
|
||||||
if (scale > minScale) {
|
mutateElement(element, { width, height, x, y });
|
||||||
selectedElements.forEach((element) => {
|
});
|
||||||
const width = element.width * scale;
|
|
||||||
const height = element.height * scale;
|
|
||||||
const x = element.x + (element.x - x1) * (scale - 1);
|
|
||||||
const y = element.y + (element.y - y1) * (scale - 1);
|
|
||||||
mutateElement(element, { width, height, x, y });
|
|
||||||
});
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
case "nw": {
|
|
||||||
const scale = Math.max(
|
|
||||||
(x2 - offsetPointer - xPointer) / (x2 - x1),
|
|
||||||
(y2 - offsetPointer - yPointer) / (y2 - y1),
|
|
||||||
);
|
|
||||||
if (scale > minScale) {
|
|
||||||
selectedElements.forEach((element) => {
|
|
||||||
const width = element.width * scale;
|
|
||||||
const height = element.height * scale;
|
|
||||||
const x = element.x - (x2 - element.x) * (scale - 1);
|
|
||||||
const y = element.y - (y2 - element.y) * (scale - 1);
|
|
||||||
mutateElement(element, { width, height, x, y });
|
|
||||||
});
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
case "ne": {
|
|
||||||
const scale = Math.max(
|
|
||||||
(xPointer - offsetPointer - x1) / (x2 - x1),
|
|
||||||
(y2 - offsetPointer - yPointer) / (y2 - y1),
|
|
||||||
);
|
|
||||||
if (scale > minScale) {
|
|
||||||
selectedElements.forEach((element) => {
|
|
||||||
const width = element.width * scale;
|
|
||||||
const height = element.height * scale;
|
|
||||||
const x = element.x + (element.x - x1) * (scale - 1);
|
|
||||||
const y = element.y - (y2 - element.y) * (scale - 1);
|
|
||||||
mutateElement(element, { width, height, x, y });
|
|
||||||
});
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
case "sw": {
|
|
||||||
const scale = Math.max(
|
|
||||||
(x2 - offsetPointer - xPointer) / (x2 - x1),
|
|
||||||
(yPointer - offsetPointer - y1) / (y2 - y1),
|
|
||||||
);
|
|
||||||
if (scale > minScale) {
|
|
||||||
selectedElements.forEach((element) => {
|
|
||||||
const width = element.width * scale;
|
|
||||||
const height = element.height * scale;
|
|
||||||
const x = element.x - (x2 - element.x) * (scale - 1);
|
|
||||||
const y = element.y + (element.y - y1) * (scale - 1);
|
|
||||||
mutateElement(element, { width, height, x, y });
|
|
||||||
});
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
}
|
}
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return false;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export const canResizeMutlipleElements = (
|
export const canResizeMutlipleElements = (
|
||||||
|
161
src/math.ts
161
src/math.ts
@ -56,123 +56,118 @@ export function rotate(
|
|||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
const adjustXYWithRotation = (
|
export const adjustXYWithRotation = (
|
||||||
side: "n" | "s" | "w" | "e" | "nw" | "ne" | "sw" | "se",
|
side: "n" | "s" | "w" | "e" | "nw" | "ne" | "sw" | "se",
|
||||||
x: number,
|
x: number,
|
||||||
y: number,
|
y: number,
|
||||||
angle: number,
|
angle: number,
|
||||||
deltaX: number,
|
deltaX1: number,
|
||||||
deltaY: number,
|
deltaY1: number,
|
||||||
|
deltaX2: number,
|
||||||
|
deltaY2: number,
|
||||||
isResizeFromCenter: boolean,
|
isResizeFromCenter: boolean,
|
||||||
) => {
|
): [number, number] => {
|
||||||
const cos = Math.cos(angle);
|
const cos = Math.cos(angle);
|
||||||
const sin = Math.sin(angle);
|
const sin = Math.sin(angle);
|
||||||
if (side === "e" || side === "ne" || side === "se") {
|
if (side === "e" || side === "ne" || side === "se") {
|
||||||
if (isResizeFromCenter) {
|
if (isResizeFromCenter) {
|
||||||
x += deltaX;
|
x += deltaX1 + deltaX2;
|
||||||
} else {
|
} else {
|
||||||
x += deltaX * (1 - cos);
|
x += deltaX1 * (1 + cos);
|
||||||
y += deltaX * -sin;
|
y += deltaX1 * sin;
|
||||||
|
x += deltaX2 * (1 - cos);
|
||||||
|
y += deltaX2 * -sin;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (side === "s" || side === "sw" || side === "se") {
|
if (side === "s" || side === "sw" || side === "se") {
|
||||||
if (isResizeFromCenter) {
|
if (isResizeFromCenter) {
|
||||||
y += deltaY;
|
y += deltaY1 + deltaY2;
|
||||||
} else {
|
} else {
|
||||||
x += deltaY * sin;
|
x += deltaY1 * -sin;
|
||||||
y += deltaY * (1 - cos);
|
y += deltaY1 * (1 + cos);
|
||||||
|
x += deltaY2 * sin;
|
||||||
|
y += deltaY2 * (1 - cos);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (side === "w" || side === "nw" || side === "sw") {
|
if (side === "w" || side === "nw" || side === "sw") {
|
||||||
if (isResizeFromCenter) {
|
if (isResizeFromCenter) {
|
||||||
x += deltaX;
|
x += deltaX1 + deltaX2;
|
||||||
} else {
|
} else {
|
||||||
x += deltaX * (1 + cos);
|
x += deltaX1 * (1 - cos);
|
||||||
y += deltaX * sin;
|
y += deltaX1 * -sin;
|
||||||
|
x += deltaX2 * (1 + cos);
|
||||||
|
y += deltaX2 * sin;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (side === "n" || side === "nw" || side === "ne") {
|
if (side === "n" || side === "nw" || side === "ne") {
|
||||||
if (isResizeFromCenter) {
|
if (isResizeFromCenter) {
|
||||||
y += deltaY;
|
y += deltaY1 + deltaY2;
|
||||||
} else {
|
} else {
|
||||||
x += deltaY * -sin;
|
x += deltaY1 * sin;
|
||||||
y += deltaY * (1 + cos);
|
y += deltaY1 * (1 - cos);
|
||||||
|
x += deltaY2 * -sin;
|
||||||
|
y += deltaY2 * (1 + cos);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return { x, y };
|
return [x, y];
|
||||||
};
|
};
|
||||||
|
|
||||||
export const resizeXYWidthHightWithRotation = (
|
export const getFlipAdjustment = (
|
||||||
side: "n" | "s" | "w" | "e" | "nw" | "ne" | "sw" | "se",
|
side: "n" | "s" | "w" | "e" | "nw" | "ne" | "sw" | "se",
|
||||||
x1: number,
|
nextWidth: number,
|
||||||
y1: number,
|
nextHeight: number,
|
||||||
x2: number,
|
nextX1: number,
|
||||||
y2: number,
|
nextY1: number,
|
||||||
elementWidth: number,
|
nextX2: number,
|
||||||
elementHeight: number,
|
nextY2: number,
|
||||||
elementX: number,
|
finalX1: number,
|
||||||
elementY: number,
|
finalY1: number,
|
||||||
|
finalX2: number,
|
||||||
|
finalY2: number,
|
||||||
|
needsRotation: boolean,
|
||||||
angle: number,
|
angle: number,
|
||||||
xPointer: number,
|
): [number, number] => {
|
||||||
yPointer: number,
|
const cos = Math.cos(angle);
|
||||||
offsetPointer: number,
|
const sin = Math.sin(angle);
|
||||||
sidesWithSameLength: boolean,
|
let flipDiffX = 0;
|
||||||
isResizeFromCenter: boolean,
|
let flipDiffY = 0;
|
||||||
) => {
|
if (nextWidth < 0) {
|
||||||
// center point for rotation
|
if (side === "e" || side === "ne" || side === "se") {
|
||||||
const cx = (x1 + x2) / 2;
|
if (needsRotation) {
|
||||||
const cy = (y1 + y2) / 2;
|
flipDiffX += (finalX2 - nextX1) * cos;
|
||||||
|
flipDiffY += (finalX2 - nextX1) * sin;
|
||||||
// rotation with current angle
|
} else {
|
||||||
const [rotatedX, rotatedY] = rotate(xPointer, yPointer, cx, cy, -angle);
|
flipDiffX += finalX2 - nextX1;
|
||||||
|
}
|
||||||
// XXX this might be slow with closure
|
}
|
||||||
const adjustWithOffsetPointer = (w: number) => {
|
if (side === "w" || side === "nw" || side === "sw") {
|
||||||
if (w > offsetPointer) {
|
if (needsRotation) {
|
||||||
return w - offsetPointer;
|
flipDiffX += (finalX1 - nextX2) * cos;
|
||||||
} else if (w < -offsetPointer) {
|
flipDiffY += (finalX1 - nextX2) * sin;
|
||||||
return w + offsetPointer;
|
} else {
|
||||||
|
flipDiffX += finalX1 - nextX2;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return 0;
|
|
||||||
};
|
|
||||||
|
|
||||||
let scaleX = 1;
|
|
||||||
let scaleY = 1;
|
|
||||||
if (side === "e" || side === "ne" || side === "se") {
|
|
||||||
scaleX = adjustWithOffsetPointer(rotatedX - x1) / (x2 - x1);
|
|
||||||
}
|
}
|
||||||
if (side === "s" || side === "sw" || side === "se") {
|
if (nextHeight < 0) {
|
||||||
scaleY = adjustWithOffsetPointer(rotatedY - y1) / (y2 - y1);
|
if (side === "s" || side === "se" || side === "sw") {
|
||||||
|
if (needsRotation) {
|
||||||
|
flipDiffY += (finalY2 - nextY1) * cos;
|
||||||
|
flipDiffX += (finalY2 - nextY1) * -sin;
|
||||||
|
} else {
|
||||||
|
flipDiffY += finalY2 - nextY1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (side === "n" || side === "ne" || side === "nw") {
|
||||||
|
if (needsRotation) {
|
||||||
|
flipDiffY += (finalY1 - nextY2) * cos;
|
||||||
|
flipDiffX += (finalY1 - nextY2) * -sin;
|
||||||
|
} else {
|
||||||
|
flipDiffY += finalY1 - nextY2;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if (side === "w" || side === "nw" || side === "sw") {
|
return [flipDiffX, flipDiffY];
|
||||||
scaleX = adjustWithOffsetPointer(x2 - rotatedX) / (x2 - x1);
|
|
||||||
}
|
|
||||||
if (side === "n" || side === "nw" || side === "ne") {
|
|
||||||
scaleY = adjustWithOffsetPointer(y2 - rotatedY) / (y2 - y1);
|
|
||||||
}
|
|
||||||
|
|
||||||
let nextWidth = elementWidth * scaleX;
|
|
||||||
let nextHeight = elementHeight * scaleY;
|
|
||||||
if (sidesWithSameLength) {
|
|
||||||
nextWidth = nextHeight = Math.max(nextWidth, nextHeight);
|
|
||||||
}
|
|
||||||
|
|
||||||
const deltaX = (elementWidth - nextWidth) / 2;
|
|
||||||
const deltaY = (elementHeight - nextHeight) / 2;
|
|
||||||
|
|
||||||
return {
|
|
||||||
width: nextWidth,
|
|
||||||
height: nextHeight,
|
|
||||||
...adjustXYWithRotation(
|
|
||||||
side,
|
|
||||||
elementX,
|
|
||||||
elementY,
|
|
||||||
angle,
|
|
||||||
deltaX,
|
|
||||||
deltaY,
|
|
||||||
isResizeFromCenter,
|
|
||||||
),
|
|
||||||
};
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export const getPointOnAPath = (point: Point, path: Point[]) => {
|
export const getPointOnAPath = (point: Point, path: Point[]) => {
|
||||||
|
Loading…
x
Reference in New Issue
Block a user