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:
Daishi Kato 2020-04-02 17:40:26 +09:00 committed by GitHub
parent 3e3ce18755
commit 65be7973be
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
23 changed files with 664 additions and 108 deletions

View file

@ -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,

View file

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