feat: rotating multiple elements (#1960)

This commit is contained in:
Daishi Kato 2020-07-26 19:21:38 +09:00 committed by GitHub
parent ebf2923c5e
commit a2e7d8d560
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 151 additions and 24 deletions

View File

@ -205,6 +205,10 @@ type PointerDownState = Readonly<{
offset: { x: number; y: number };
// This is determined on the initial pointer down event
arrowDirection: "origin" | "end";
// This is a center point of selected elements determined on the initial pointer down event (for rotation only)
center: { x: number; y: number };
// This is a list of selected elements determined on the initial pointer down event (for rotation only)
originalElements: readonly NonDeleted<ExcalidrawElement>[];
};
hit: {
// The element the pointer is "hitting", is determined on the initial
@ -2213,6 +2217,11 @@ class App extends React.Component<ExcalidrawProps, AppState> {
this.canvas,
window.devicePixelRatio,
);
const selectedElements = getSelectedElements(
globalSceneState.getElements(),
this.state,
);
const [minX, minY, maxX, maxY] = getCommonBounds(selectedElements);
return {
origin,
@ -2231,6 +2240,8 @@ class App extends React.Component<ExcalidrawProps, AppState> {
isResizing: false,
offset: { x: 0, y: 0 },
arrowDirection: "origin",
center: { x: (maxX + minX) / 2, y: (maxY + minY) / 2 },
originalElements: selectedElements.map((element) => ({ ...element })),
},
hit: {
element: null,
@ -2709,6 +2720,9 @@ class App extends React.Component<ExcalidrawProps, AppState> {
getResizeCenterPointKey(event),
resizeX,
resizeY,
pointerDownState.resize.center.x,
pointerDownState.resize.center.y,
pointerDownState.resize.originalElements,
)
) {
return;

View File

@ -18,7 +18,6 @@ export const OMIT_SIDES_FOR_MULTIPLE_ELEMENTS = {
s: true,
n: true,
w: true,
rotation: true,
};
const OMIT_SIDES_FOR_TEXT_ELEMENT = {

View File

@ -23,18 +23,28 @@ import {
} from "./resizeTest";
import { measureText, getFontString } from "../utils";
const normalizeAngle = (angle: number): number => {
if (angle >= 2 * Math.PI) {
return angle - 2 * Math.PI;
}
return angle;
};
type ResizeTestType = ReturnType<typeof resizeTest>;
export const resizeElements = (
resizeHandle: ResizeTestType,
setResizeHandle: (nextResizeHandle: ResizeTestType) => void,
selectedElements: NonDeletedExcalidrawElement[],
selectedElements: readonly NonDeletedExcalidrawElement[],
resizeArrowDirection: "origin" | "end",
isRotateWithDiscreteAngle: boolean,
isResizeWithSidesSameLength: boolean,
isResizeCenterPoint: boolean,
pointerX: number,
pointerY: number,
centerX: number,
centerY: number,
originalElements: readonly NonDeletedExcalidrawElement[],
) => {
if (selectedElements.length === 1) {
const [element] = selectedElements;
@ -100,15 +110,32 @@ export const resizeElements = (
});
return true;
} else if (
selectedElements.length > 1 &&
(resizeHandle === "nw" ||
} else if (selectedElements.length > 1) {
if (resizeHandle === "rotation") {
rotateMultipleElements(
selectedElements,
pointerX,
pointerY,
isRotateWithDiscreteAngle,
centerX,
centerY,
originalElements,
);
return true;
} else if (
resizeHandle === "nw" ||
resizeHandle === "ne" ||
resizeHandle === "sw" ||
resizeHandle === "se")
) {
resizeMultipleElements(selectedElements, resizeHandle, pointerX, pointerY);
return true;
resizeHandle === "se"
) {
resizeMultipleElements(
selectedElements,
resizeHandle,
pointerX,
pointerY,
);
return true;
}
}
return false;
};
@ -127,9 +154,7 @@ const rotateSingleElement = (
angle += SHIFT_LOCKING_ANGLE / 2;
angle -= angle % SHIFT_LOCKING_ANGLE;
}
if (angle >= 2 * Math.PI) {
angle -= 2 * Math.PI;
}
angle = normalizeAngle(angle);
mutateElement(element, { angle });
};
@ -536,6 +561,85 @@ const resizeMultipleElements = (
}
};
const rotateMultipleElements = (
elements: readonly NonDeletedExcalidrawElement[],
pointerX: number,
pointerY: number,
isRotateWithDiscreteAngle: boolean,
centerX: number,
centerY: number,
originalElements: readonly NonDeletedExcalidrawElement[],
) => {
let centerAngle =
(5 * Math.PI) / 2 + Math.atan2(pointerY - centerY, pointerX - centerX);
if (isRotateWithDiscreteAngle) {
centerAngle += SHIFT_LOCKING_ANGLE / 2;
centerAngle -= centerAngle % SHIFT_LOCKING_ANGLE;
}
elements.forEach((element, index) => {
if (isLinearElement(element) && element.points.length === 2) {
// FIXME this is a bit tricky (how can we make this more readable?)
const originalElement = originalElements[index];
if (
!isLinearElement(originalElement) ||
originalElement.points.length !== 2
) {
throw new Error("original element not compatible"); // should not happen
}
const [x1, y1, x2, y2] = getElementAbsoluteCoords(originalElement);
const cx = (x1 + x2) / 2;
const cy = (y1 + y2) / 2;
const [rotatedCX, rotatedCY] = rotate(
cx,
cy,
centerX,
centerY,
centerAngle,
);
const { points } = originalElement;
const [rotatedX, rotatedY] = rotate(
points[1][0],
points[1][1],
points[0][0],
points[0][1],
centerAngle,
);
mutateElement(element, {
x:
originalElement.x +
(rotatedCX - cx) +
((originalElement.points[0][0] + originalElement.points[1][0]) / 2 -
(points[0][0] + rotatedX) / 2),
y:
originalElement.y +
(rotatedCY - cy) +
((originalElement.points[0][1] + originalElement.points[1][1]) / 2 -
(points[0][1] + rotatedY) / 2),
points: [
[points[0][0], points[0][1]],
[rotatedX, rotatedY],
],
});
} else {
const [x1, y1, x2, y2] = getElementAbsoluteCoords(element);
const cx = (x1 + x2) / 2;
const cy = (y1 + y2) / 2;
const [rotatedCX, rotatedCY] = rotate(
cx,
cy,
centerX,
centerY,
centerAngle + originalElements[index].angle - element.angle,
);
mutateElement(element, {
x: element.x + (rotatedCX - cx),
y: element.y + (rotatedCY - cy),
angle: normalizeAngle(centerAngle + originalElements[index].angle),
});
}
});
};
export const getResizeOffsetXY = (
resizeHandle: ResizeTestType,
selectedElements: NonDeletedExcalidrawElement[],

View File

@ -399,7 +399,7 @@ export const renderScene = (
}
});
context.translate(-sceneState.scrollX, -sceneState.scrollY);
} else if (locallySelectedElements.length > 1) {
} else if (locallySelectedElements.length > 1 && !appState.isRotating) {
const dashedLinePadding = 4 / sceneState.zoom;
context.translate(sceneState.scrollX, sceneState.scrollY);
context.fillStyle = oc.white;
@ -432,17 +432,27 @@ export const renderScene = (
if (handler !== undefined) {
const lineWidth = context.lineWidth;
context.lineWidth = 1 / sceneState.zoom;
strokeRectWithRotation(
context,
handler[0],
handler[1],
handler[2],
handler[3],
handler[0] + handler[2] / 2,
handler[1] + handler[3] / 2,
0,
true, // fill before stroke
);
if (key === "rotation") {
strokeCircle(
context,
handler[0],
handler[1],
handler[2],
handler[3],
);
} else {
strokeRectWithRotation(
context,
handler[0],
handler[1],
handler[2],
handler[3],
handler[0] + handler[2] / 2,
handler[1] + handler[3] / 2,
0,
true, // fill before stroke
);
}
context.lineWidth = lineWidth;
}
});