fix resizing lines with abs coords bigger than element w/h (#1427)

This commit is contained in:
Daishi Kato 2020-05-05 00:25:40 +09:00 committed by GitHub
parent 686af31d9d
commit 73d8c5b7c1
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 498 additions and 352 deletions

View File

@ -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,
];
};

View File

@ -17,7 +17,6 @@ export {
getCommonBounds, getCommonBounds,
getDiamondPoints, getDiamondPoints,
getArrowPoints, getArrowPoints,
getLinearElementAbsoluteBounds,
} from "./bounds"; } from "./bounds";
export { export {

View File

@ -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 = (

View File

@ -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[]) => {