mirror of
https://github.com/excalidraw/excalidraw.git
synced 2025-05-03 10:00:07 -04:00
Rotation support (#1099)
* rotate rectanble with fixed angle * rotate dashed rectangle with fixed angle * fix rotate handler rect * fix canvas size with rotation * angle in element base * fix bug in calculating canvas size * trial only for rectangle * hitTest for rectangle rotation * properly resize rotated rectangle * fix canvas size calculation * giving up... workaround for now * **experimental** handler to rotate rectangle * remove rotation on copy for debugging * update snapshots * better rotation handler with atan2 * rotate when drawImage * add rotation handler * hitTest for any shapes * fix hitTest for curved lines * rotate text element * rotation locking * hint messaage for rotating * show proper handlers on mobile (a workaround, there should be a better way) * refactor hitTest * support exporting png * support exporting svg * fix rotating curved line * refactor drawElementFromCanvas with getElementAbsoluteCoords * fix export png and svg * adjust resize positions for lines (N, E, S, W) * do not make handlers big on mobile * Update src/locales/en.json Alright! Co-Authored-By: Lipis <lipiridis@gmail.com> * do not show rotation/resizing hints on mobile * proper calculation for N and W positions * simplify calculation * use "rotation" as property name for clarification (may increase bundle size) * update snapshots excluding rotation handle * refactor with adjustPositionWithRotation * refactor with adjustXYWithRotation * forgot to rename rotation * rename internal function * initialize element angle on restore * rotate wysiwyg editor * fix shift-rotate around 270deg * improve rotation locking * refactor adjustXYWithRotation * avoid rotation degree becomes >=360 * refactor with generateHandler Co-authored-by: Lipis <lipiridis@gmail.com> Co-authored-by: dwelle <luzar.david@gmail.com>
This commit is contained in:
parent
3e3ce18755
commit
65be7973be
23 changed files with 664 additions and 108 deletions
|
@ -4,6 +4,7 @@ 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,
|
||||
|
@ -50,6 +51,7 @@ import { restore } from "../data/restore";
|
|||
import { renderScene } from "../renderer";
|
||||
import { AppState, GestureEvent, Gesture } from "../types";
|
||||
import { ExcalidrawElement, ExcalidrawLinearElement } from "../element/types";
|
||||
import { rotate, adjustXYWithRotation } from "../math";
|
||||
|
||||
import {
|
||||
isWritableElement,
|
||||
|
@ -1208,6 +1210,7 @@ export class App extends React.Component<any, AppState> {
|
|||
font: element.font,
|
||||
opacity: this.state.currentItemOpacity,
|
||||
zoom: this.state.zoom,
|
||||
angle: element.angle,
|
||||
onSubmit: (text) => {
|
||||
if (text) {
|
||||
globalSceneState.replaceAllElements([
|
||||
|
@ -1703,6 +1706,7 @@ export class App extends React.Component<any, AppState> {
|
|||
opacity: this.state.currentItemOpacity,
|
||||
font: this.state.currentItemFont,
|
||||
zoom: this.state.zoom,
|
||||
angle: 0,
|
||||
onSubmit: (text) => {
|
||||
if (text) {
|
||||
globalSceneState.replaceAllElements([
|
||||
|
@ -1974,7 +1978,10 @@ export class App extends React.Component<any, AppState> {
|
|||
}
|
||||
|
||||
if (isResizingElements && this.state.resizingElement) {
|
||||
this.setState({ isResizing: true });
|
||||
this.setState({
|
||||
isResizing: resizeHandle !== "rotation",
|
||||
isRotating: resizeHandle === "rotation",
|
||||
});
|
||||
const el = this.state.resizingElement;
|
||||
const selectedElements = getSelectedElements(
|
||||
globalSceneState.getAllElements(),
|
||||
|
@ -1987,9 +1994,10 @@ export class App extends React.Component<any, AppState> {
|
|||
this.canvas,
|
||||
window.devicePixelRatio,
|
||||
);
|
||||
const deltaX = x - lastX;
|
||||
const deltaY = y - lastY;
|
||||
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) {
|
||||
|
@ -2005,16 +2013,12 @@ export class App extends React.Component<any, AppState> {
|
|||
resizeArrowFn(element, 1, deltaX, deltaY, x, y, event.shiftKey);
|
||||
} else {
|
||||
const width = element.width - deltaX;
|
||||
const height = event.shiftKey
|
||||
? element.width
|
||||
: element.height - deltaY;
|
||||
const height = event.shiftKey ? width : element.height - deltaY;
|
||||
const dY = element.height - height;
|
||||
mutateElement(element, {
|
||||
x: element.x + deltaX,
|
||||
y: event.shiftKey
|
||||
? element.y + element.height - element.width
|
||||
: element.y + deltaY,
|
||||
width,
|
||||
height,
|
||||
...adjustXYWithRotation("nw", element, deltaX, dY, angle),
|
||||
...(isLinearElement(element) && width >= 0 && height >= 0
|
||||
? {
|
||||
points: rescalePoints(
|
||||
|
@ -2041,12 +2045,11 @@ export class App extends React.Component<any, AppState> {
|
|||
} else {
|
||||
const width = element.width + deltaX;
|
||||
const height = event.shiftKey ? width : element.height - deltaY;
|
||||
const dY = element.height - height;
|
||||
mutateElement(element, {
|
||||
y: event.shiftKey
|
||||
? element.y + element.height - width
|
||||
: element.y + deltaY,
|
||||
width,
|
||||
height,
|
||||
...adjustXYWithRotation("ne", element, deltaX, dY, angle),
|
||||
...(isLinearElement(element) && width >= 0 && height >= 0
|
||||
? {
|
||||
points: rescalePoints(
|
||||
|
@ -2072,13 +2075,12 @@ export class App extends React.Component<any, AppState> {
|
|||
resizeArrowFn(element, 1, deltaX, deltaY, x, y, event.shiftKey);
|
||||
} else {
|
||||
const width = element.width - deltaX;
|
||||
const height = event.shiftKey
|
||||
? element.width
|
||||
: element.height + deltaY;
|
||||
const height = event.shiftKey ? width : element.height + deltaY;
|
||||
const dY = height - element.height;
|
||||
mutateElement(element, {
|
||||
x: element.x + deltaX,
|
||||
width,
|
||||
height,
|
||||
...adjustXYWithRotation("sw", element, deltaX, dY, angle),
|
||||
...(isLinearElement(element) && width >= 0 && height >= 0
|
||||
? {
|
||||
points: rescalePoints(
|
||||
|
@ -2104,12 +2106,12 @@ export class App extends React.Component<any, AppState> {
|
|||
resizeArrowFn(element, 1, deltaX, deltaY, x, y, event.shiftKey);
|
||||
} else {
|
||||
const width = element.width + deltaX;
|
||||
const height = event.shiftKey
|
||||
? element.width
|
||||
: element.height + deltaY;
|
||||
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(
|
||||
|
@ -2133,13 +2135,13 @@ export class App extends React.Component<any, AppState> {
|
|||
}
|
||||
mutateElement(element, {
|
||||
height,
|
||||
y: element.y + deltaY,
|
||||
...adjustXYWithRotation("n", element, 0, deltaY, angle),
|
||||
points: rescalePoints(1, height, element.points),
|
||||
});
|
||||
} else {
|
||||
mutateElement(element, {
|
||||
height,
|
||||
y: element.y + deltaY,
|
||||
...adjustXYWithRotation("n", element, 0, deltaY, angle),
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -2157,13 +2159,13 @@ export class App extends React.Component<any, AppState> {
|
|||
|
||||
mutateElement(element, {
|
||||
width,
|
||||
x: element.x + deltaX,
|
||||
...adjustXYWithRotation("w", element, deltaX, 0, angle),
|
||||
points: rescalePoints(0, width, element.points),
|
||||
});
|
||||
} else {
|
||||
mutateElement(element, {
|
||||
width,
|
||||
x: element.x + deltaX,
|
||||
...adjustXYWithRotation("w", element, deltaX, 0, angle),
|
||||
});
|
||||
}
|
||||
break;
|
||||
|
@ -2179,11 +2181,13 @@ export class App extends React.Component<any, AppState> {
|
|||
}
|
||||
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;
|
||||
|
@ -2199,15 +2203,32 @@ export class App extends React.Component<any, AppState> {
|
|||
}
|
||||
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 += Math.PI / 16;
|
||||
angle -= angle % (Math.PI / 8);
|
||||
}
|
||||
if (angle >= 2 * Math.PI) {
|
||||
angle -= 2 * Math.PI;
|
||||
}
|
||||
mutateElement(element, { angle });
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (resizeHandle) {
|
||||
|
@ -2351,6 +2372,7 @@ export class App extends React.Component<any, AppState> {
|
|||
|
||||
this.setState({
|
||||
isResizing: false,
|
||||
isRotating: false,
|
||||
resizingElement: null,
|
||||
selectionElement: null,
|
||||
editingElement: multiElement ? this.state.editingElement : null,
|
||||
|
|
|
@ -13,7 +13,7 @@ interface Hint {
|
|||
}
|
||||
|
||||
const getHints = ({ appState, elements }: Hint) => {
|
||||
const { elementType, isResizing } = appState;
|
||||
const { elementType, isResizing, isRotating, lastPointerDownWith } = appState;
|
||||
const multiMode = appState.multiElement !== null;
|
||||
if (elementType === "arrow" || elementType === "line") {
|
||||
if (!multiMode) {
|
||||
|
@ -22,7 +22,7 @@ const getHints = ({ appState, elements }: Hint) => {
|
|||
return t("hints.linearElementMulti");
|
||||
}
|
||||
|
||||
if (isResizing) {
|
||||
if (isResizing && lastPointerDownWith === "mouse") {
|
||||
const selectedElements = getSelectedElements(elements, appState);
|
||||
const targetElement = selectedElements[0];
|
||||
if (isLinearElement(targetElement) && targetElement.points.length > 2) {
|
||||
|
@ -31,6 +31,10 @@ const getHints = ({ appState, elements }: Hint) => {
|
|||
return t("hints.resize");
|
||||
}
|
||||
|
||||
if (isRotating && lastPointerDownWith === "mouse") {
|
||||
return t("hints.rotate");
|
||||
}
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue