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

View File

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

View File

@ -23,18 +23,28 @@ import {
} from "./resizeTest"; } from "./resizeTest";
import { measureText, getFontString } from "../utils"; 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>; type ResizeTestType = ReturnType<typeof resizeTest>;
export const resizeElements = ( export const resizeElements = (
resizeHandle: ResizeTestType, resizeHandle: ResizeTestType,
setResizeHandle: (nextResizeHandle: ResizeTestType) => void, setResizeHandle: (nextResizeHandle: ResizeTestType) => void,
selectedElements: NonDeletedExcalidrawElement[], selectedElements: readonly NonDeletedExcalidrawElement[],
resizeArrowDirection: "origin" | "end", resizeArrowDirection: "origin" | "end",
isRotateWithDiscreteAngle: boolean, isRotateWithDiscreteAngle: boolean,
isResizeWithSidesSameLength: boolean, isResizeWithSidesSameLength: boolean,
isResizeCenterPoint: boolean, isResizeCenterPoint: boolean,
pointerX: number, pointerX: number,
pointerY: number, pointerY: number,
centerX: number,
centerY: number,
originalElements: readonly NonDeletedExcalidrawElement[],
) => { ) => {
if (selectedElements.length === 1) { if (selectedElements.length === 1) {
const [element] = selectedElements; const [element] = selectedElements;
@ -99,17 +109,34 @@ export const resizeElements = (
resizeHandle, resizeHandle,
}); });
return true;
} else if (selectedElements.length > 1) {
if (resizeHandle === "rotation") {
rotateMultipleElements(
selectedElements,
pointerX,
pointerY,
isRotateWithDiscreteAngle,
centerX,
centerY,
originalElements,
);
return true; return true;
} else if ( } else if (
selectedElements.length > 1 && resizeHandle === "nw" ||
(resizeHandle === "nw" ||
resizeHandle === "ne" || resizeHandle === "ne" ||
resizeHandle === "sw" || resizeHandle === "sw" ||
resizeHandle === "se") resizeHandle === "se"
) { ) {
resizeMultipleElements(selectedElements, resizeHandle, pointerX, pointerY); resizeMultipleElements(
selectedElements,
resizeHandle,
pointerX,
pointerY,
);
return true; return true;
} }
}
return false; return false;
}; };
@ -127,9 +154,7 @@ const rotateSingleElement = (
angle += SHIFT_LOCKING_ANGLE / 2; angle += SHIFT_LOCKING_ANGLE / 2;
angle -= angle % SHIFT_LOCKING_ANGLE; angle -= angle % SHIFT_LOCKING_ANGLE;
} }
if (angle >= 2 * Math.PI) { angle = normalizeAngle(angle);
angle -= 2 * Math.PI;
}
mutateElement(element, { 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 = ( export const getResizeOffsetXY = (
resizeHandle: ResizeTestType, resizeHandle: ResizeTestType,
selectedElements: NonDeletedExcalidrawElement[], selectedElements: NonDeletedExcalidrawElement[],

View File

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