Add free draw mode (#1570)
This commit is contained in:
parent
36e0c439fb
commit
9ec43d2626
6
package-lock.json
generated
6
package-lock.json
generated
@ -16394,9 +16394,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"roughjs": {
|
"roughjs": {
|
||||||
"version": "4.3.0",
|
"version": "4.3.1",
|
||||||
"resolved": "https://registry.npmjs.org/roughjs/-/roughjs-4.3.0.tgz",
|
"resolved": "https://registry.npmjs.org/roughjs/-/roughjs-4.3.1.tgz",
|
||||||
"integrity": "sha512-aHEBK0dn50v9HP5hMghQmjpkvPD3He9+pm6UbbcmniFJlIbnvWhw72xFVYR44TorhmwpwtKZj6USniiT0Mq98w==",
|
"integrity": "sha512-m42+OBaBR7x5UhIKyjBCnWqqkaEkBKLkXvHv4pOWJXPofvMnQY4ZcFEQlqf3coKKyZN2lfWMyx7QXSg2GD7SGA==",
|
||||||
"requires": {
|
"requires": {
|
||||||
"path-data-parser": "^0.1.0",
|
"path-data-parser": "^0.1.0",
|
||||||
"points-on-curve": "^0.2.0",
|
"points-on-curve": "^0.2.0",
|
||||||
|
@ -39,7 +39,7 @@
|
|||||||
"react": "16.13.1",
|
"react": "16.13.1",
|
||||||
"react-dom": "16.13.1",
|
"react-dom": "16.13.1",
|
||||||
"react-scripts": "3.4.1",
|
"react-scripts": "3.4.1",
|
||||||
"roughjs": "4.3.0",
|
"roughjs": "4.3.1",
|
||||||
"socket.io-client": "2.3.0",
|
"socket.io-client": "2.3.0",
|
||||||
"typescript": "3.8.3"
|
"typescript": "3.8.3"
|
||||||
},
|
},
|
||||||
|
@ -16,31 +16,44 @@ export const actionFinalize = register({
|
|||||||
if (window.document.activeElement instanceof HTMLElement) {
|
if (window.document.activeElement instanceof HTMLElement) {
|
||||||
window.document.activeElement.blur();
|
window.document.activeElement.blur();
|
||||||
}
|
}
|
||||||
if (appState.multiElement) {
|
|
||||||
|
const multiPointElement = appState.multiElement
|
||||||
|
? appState.multiElement
|
||||||
|
: appState.editingElement?.type === "draw"
|
||||||
|
? appState.editingElement
|
||||||
|
: null;
|
||||||
|
|
||||||
|
if (multiPointElement) {
|
||||||
// pen and mouse have hover
|
// pen and mouse have hover
|
||||||
if (appState.lastPointerDownWith !== "touch") {
|
if (
|
||||||
const { points, lastCommittedPoint } = appState.multiElement;
|
multiPointElement.type !== "draw" &&
|
||||||
|
appState.lastPointerDownWith !== "touch"
|
||||||
|
) {
|
||||||
|
const { points, lastCommittedPoint } = multiPointElement;
|
||||||
if (
|
if (
|
||||||
!lastCommittedPoint ||
|
!lastCommittedPoint ||
|
||||||
points[points.length - 1] !== lastCommittedPoint
|
points[points.length - 1] !== lastCommittedPoint
|
||||||
) {
|
) {
|
||||||
mutateElement(appState.multiElement, {
|
mutateElement(multiPointElement, {
|
||||||
points: appState.multiElement.points.slice(0, -1),
|
points: multiPointElement.points.slice(0, -1),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (isInvisiblySmallElement(appState.multiElement)) {
|
if (isInvisiblySmallElement(multiPointElement)) {
|
||||||
newElements = newElements.slice(0, -1);
|
newElements = newElements.slice(0, -1);
|
||||||
}
|
}
|
||||||
|
|
||||||
// If the multi point line closes the loop,
|
// If the multi point line closes the loop,
|
||||||
// set the last point to first point.
|
// set the last point to first point.
|
||||||
// This ensures that loop remains closed at different scales.
|
// This ensures that loop remains closed at different scales.
|
||||||
if (appState.multiElement.type === "line") {
|
if (
|
||||||
if (isPathALoop(appState.multiElement.points)) {
|
multiPointElement.type === "line" ||
|
||||||
const linePoints = appState.multiElement.points;
|
multiPointElement.type === "draw"
|
||||||
|
) {
|
||||||
|
if (isPathALoop(multiPointElement.points)) {
|
||||||
|
const linePoints = multiPointElement.points;
|
||||||
const firstPoint = linePoints[0];
|
const firstPoint = linePoints[0];
|
||||||
mutateElement(appState.multiElement, {
|
mutateElement(multiPointElement, {
|
||||||
points: linePoints.map((point, i) =>
|
points: linePoints.map((point, i) =>
|
||||||
i === linePoints.length - 1
|
i === linePoints.length - 1
|
||||||
? ([firstPoint[0], firstPoint[1]] as const)
|
? ([firstPoint[0], firstPoint[1]] as const)
|
||||||
@ -51,10 +64,10 @@ export const actionFinalize = register({
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (!appState.elementLocked) {
|
if (!appState.elementLocked) {
|
||||||
appState.selectedElementIds[appState.multiElement.id] = true;
|
appState.selectedElementIds[multiPointElement.id] = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (!appState.elementLocked || !appState.multiElement) {
|
if (!appState.elementLocked || !multiPointElement) {
|
||||||
resetCursor();
|
resetCursor();
|
||||||
}
|
}
|
||||||
return {
|
return {
|
||||||
@ -62,13 +75,19 @@ export const actionFinalize = register({
|
|||||||
appState: {
|
appState: {
|
||||||
...appState,
|
...appState,
|
||||||
elementType:
|
elementType:
|
||||||
appState.elementLocked && appState.multiElement
|
appState.elementLocked && multiPointElement
|
||||||
? appState.elementType
|
? appState.elementType
|
||||||
: "selection",
|
: "selection",
|
||||||
draggingElement: null,
|
draggingElement: null,
|
||||||
multiElement: null,
|
multiElement: null,
|
||||||
editingElement: null,
|
editingElement: null,
|
||||||
selectedElementIds: {},
|
selectedElementIds:
|
||||||
|
multiPointElement && !appState.elementLocked
|
||||||
|
? {
|
||||||
|
...appState.selectedElementIds,
|
||||||
|
[multiPointElement.id]: true,
|
||||||
|
}
|
||||||
|
: appState.selectedElementIds,
|
||||||
},
|
},
|
||||||
commitToHistory: false,
|
commitToHistory: false,
|
||||||
};
|
};
|
||||||
|
@ -94,11 +94,11 @@ export function ShapesSwitcher({
|
|||||||
}) {
|
}) {
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{SHAPES.map(({ value, icon }, index) => {
|
{SHAPES.map(({ value, icon, key }, index) => {
|
||||||
const label = t(`toolBar.${value}`);
|
const label = t(`toolBar.${value}`);
|
||||||
const shortcut = `${capitalizeString(value)[0]} ${t(
|
const shortcut = `${capitalizeString(key)} ${t("shortcutsDialog.or")} ${
|
||||||
"shortcutsDialog.or",
|
index + 1
|
||||||
)} ${index + 1}`;
|
}`;
|
||||||
return (
|
return (
|
||||||
<ToolButton
|
<ToolButton
|
||||||
key={value}
|
key={value}
|
||||||
@ -109,7 +109,7 @@ export function ShapesSwitcher({
|
|||||||
title={`${capitalizeString(label)} — ${shortcut}`}
|
title={`${capitalizeString(label)} — ${shortcut}`}
|
||||||
keyBindingLabel={`${index + 1}`}
|
keyBindingLabel={`${index + 1}`}
|
||||||
aria-label={capitalizeString(label)}
|
aria-label={capitalizeString(label)}
|
||||||
aria-keyshortcuts={`${label[0]} ${index + 1}`}
|
aria-keyshortcuts={`${key} ${index + 1}`}
|
||||||
data-testid={value}
|
data-testid={value}
|
||||||
onChange={() => {
|
onChange={() => {
|
||||||
setAppState({
|
setAppState({
|
||||||
|
@ -3,6 +3,7 @@ import React from "react";
|
|||||||
import socketIOClient from "socket.io-client";
|
import socketIOClient from "socket.io-client";
|
||||||
import rough from "roughjs/bin/rough";
|
import rough from "roughjs/bin/rough";
|
||||||
import { RoughCanvas } from "roughjs/bin/canvas";
|
import { RoughCanvas } from "roughjs/bin/canvas";
|
||||||
|
import { simplify, Point } from "points-on-curve";
|
||||||
import { FlooredNumber, SocketUpdateData } from "../types";
|
import { FlooredNumber, SocketUpdateData } from "../types";
|
||||||
|
|
||||||
import {
|
import {
|
||||||
@ -1981,6 +1982,7 @@ class App extends React.Component<any, AppState> {
|
|||||||
return;
|
return;
|
||||||
} else if (
|
} else if (
|
||||||
this.state.elementType === "arrow" ||
|
this.state.elementType === "arrow" ||
|
||||||
|
this.state.elementType === "draw" ||
|
||||||
this.state.elementType === "line"
|
this.state.elementType === "line"
|
||||||
) {
|
) {
|
||||||
if (this.state.multiElement) {
|
if (this.state.multiElement) {
|
||||||
@ -2122,7 +2124,7 @@ class App extends React.Component<any, AppState> {
|
|||||||
window.devicePixelRatio,
|
window.devicePixelRatio,
|
||||||
);
|
);
|
||||||
|
|
||||||
// for arrows, don't start dragging until a given threshold
|
// for arrows/lines, don't start dragging until a given threshold
|
||||||
// to ensure we don't create a 2-point arrow by mistake when
|
// to ensure we don't create a 2-point arrow by mistake when
|
||||||
// user clicks mouse in a way that it moves a tiny bit (thus
|
// user clicks mouse in a way that it moves a tiny bit (thus
|
||||||
// triggering pointermove)
|
// triggering pointermove)
|
||||||
@ -2249,9 +2251,15 @@ class App extends React.Component<any, AppState> {
|
|||||||
if (points.length === 1) {
|
if (points.length === 1) {
|
||||||
mutateElement(draggingElement, { points: [...points, [dx, dy]] });
|
mutateElement(draggingElement, { points: [...points, [dx, dy]] });
|
||||||
} else if (points.length > 1) {
|
} else if (points.length > 1) {
|
||||||
mutateElement(draggingElement, {
|
if (draggingElement.type === "draw") {
|
||||||
points: [...points.slice(0, -1), [dx, dy]],
|
mutateElement(draggingElement, {
|
||||||
});
|
points: simplify([...(points as Point[]), [dx, dy]], 0.7),
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
mutateElement(draggingElement, {
|
||||||
|
points: [...points.slice(0, -1), [dx, dy]],
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
if (getResizeWithSidesSameLengthKey(event)) {
|
if (getResizeWithSidesSameLengthKey(event)) {
|
||||||
@ -2330,6 +2338,10 @@ class App extends React.Component<any, AppState> {
|
|||||||
window.removeEventListener(EVENT.POINTER_MOVE, onPointerMove);
|
window.removeEventListener(EVENT.POINTER_MOVE, onPointerMove);
|
||||||
window.removeEventListener(EVENT.POINTER_UP, onPointerUp);
|
window.removeEventListener(EVENT.POINTER_UP, onPointerUp);
|
||||||
|
|
||||||
|
if (draggingElement?.type === "draw") {
|
||||||
|
this.actionManager.executeAction(actionFinalize);
|
||||||
|
return;
|
||||||
|
}
|
||||||
if (isLinearElement(draggingElement)) {
|
if (isLinearElement(draggingElement)) {
|
||||||
if (draggingElement!.points.length > 1) {
|
if (draggingElement!.points.length > 1) {
|
||||||
history.resumeRecording();
|
history.resumeRecording();
|
||||||
|
@ -22,6 +22,10 @@ const getHints = ({ appState, elements }: Hint) => {
|
|||||||
return t("hints.linearElementMulti");
|
return t("hints.linearElementMulti");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (elementType === "draw") {
|
||||||
|
return t("hints.freeDraw");
|
||||||
|
}
|
||||||
|
|
||||||
const selectedElements = getSelectedElements(elements, appState);
|
const selectedElements = getSelectedElements(elements, appState);
|
||||||
if (
|
if (
|
||||||
isResizing &&
|
isResizing &&
|
||||||
|
@ -184,7 +184,8 @@ export const ShortcutsDialog = ({ onClose }: { onClose?: () => void }) => {
|
|||||||
<Shortcut label={t("toolBar.ellipse")} shortcuts={["E", "4"]} />
|
<Shortcut label={t("toolBar.ellipse")} shortcuts={["E", "4"]} />
|
||||||
<Shortcut label={t("toolBar.arrow")} shortcuts={["A", "5"]} />
|
<Shortcut label={t("toolBar.arrow")} shortcuts={["A", "5"]} />
|
||||||
<Shortcut label={t("toolBar.line")} shortcuts={["L", "6"]} />
|
<Shortcut label={t("toolBar.line")} shortcuts={["L", "6"]} />
|
||||||
<Shortcut label={t("toolBar.text")} shortcuts={["T", "7"]} />
|
<Shortcut label={t("toolBar.draw")} shortcuts={["X", "7"]} />
|
||||||
|
<Shortcut label={t("toolBar.text")} shortcuts={["T", "8"]} />
|
||||||
<Shortcut
|
<Shortcut
|
||||||
label={t("shortcutsDialog.textNewLine")}
|
label={t("shortcutsDialog.textNewLine")}
|
||||||
shortcuts={[
|
shortcuts={[
|
||||||
|
@ -43,7 +43,7 @@ export function restore(
|
|||||||
];
|
];
|
||||||
}
|
}
|
||||||
element.points = points;
|
element.points = points;
|
||||||
} else if (element.type === "line") {
|
} else if (element.type === "line" || element.type === "draw") {
|
||||||
// old spec, pre-arrows
|
// old spec, pre-arrows
|
||||||
// old spec, post-arrows
|
// old spec, post-arrows
|
||||||
if (!Array.isArray(element.points) || element.points.length === 0) {
|
if (!Array.isArray(element.points) || element.points.length === 0) {
|
||||||
|
@ -52,7 +52,6 @@ const getMinMaxXYFromCurvePathOps = (
|
|||||||
transformXY?: (x: number, y: number) => [number, number],
|
transformXY?: (x: number, y: number) => [number, number],
|
||||||
): [number, number, number, number] => {
|
): [number, number, number, number] => {
|
||||||
let currentP: Point = [0, 0];
|
let currentP: Point = [0, 0];
|
||||||
|
|
||||||
const { minX, minY, maxX, maxY } = ops.reduce(
|
const { minX, minY, maxX, maxY } = ops.reduce(
|
||||||
(limits, { op, data }) => {
|
(limits, { op, data }) => {
|
||||||
// There are only four operation types:
|
// There are only four operation types:
|
||||||
|
@ -26,7 +26,7 @@ function isElementDraggableFromInside(
|
|||||||
const dragFromInside =
|
const dragFromInside =
|
||||||
element.backgroundColor !== "transparent" ||
|
element.backgroundColor !== "transparent" ||
|
||||||
appState.selectedElementIds[element.id];
|
appState.selectedElementIds[element.id];
|
||||||
if (element.type === "line") {
|
if (element.type === "line" || element.type === "draw") {
|
||||||
return dragFromInside && isPathALoop(element.points);
|
return dragFromInside && isPathALoop(element.points);
|
||||||
}
|
}
|
||||||
return dragFromInside;
|
return dragFromInside;
|
||||||
|
@ -187,7 +187,11 @@ export function handlerRectangles(
|
|||||||
pointerType,
|
pointerType,
|
||||||
);
|
);
|
||||||
|
|
||||||
if (element.type === "arrow" || element.type === "line") {
|
if (
|
||||||
|
element.type === "arrow" ||
|
||||||
|
element.type === "line" ||
|
||||||
|
element.type === "draw"
|
||||||
|
) {
|
||||||
if (element.points.length === 2) {
|
if (element.points.length === 2) {
|
||||||
// only check the last point because starting point is always (0,0)
|
// only check the last point because starting point is always (0,0)
|
||||||
const [, p1] = element.points;
|
const [, p1] = element.points;
|
||||||
|
@ -21,7 +21,11 @@ export function getPerfectElementSize(
|
|||||||
const absWidth = Math.abs(width);
|
const absWidth = Math.abs(width);
|
||||||
const absHeight = Math.abs(height);
|
const absHeight = Math.abs(height);
|
||||||
|
|
||||||
if (elementType === "line" || elementType === "arrow") {
|
if (
|
||||||
|
elementType === "line" ||
|
||||||
|
elementType === "arrow" ||
|
||||||
|
elementType === "draw"
|
||||||
|
) {
|
||||||
const lockedAngle =
|
const lockedAngle =
|
||||||
Math.round(Math.atan(absHeight / absWidth) / SHIFT_LOCKING_ANGLE) *
|
Math.round(Math.atan(absHeight / absWidth) / SHIFT_LOCKING_ANGLE) *
|
||||||
SHIFT_LOCKING_ANGLE;
|
SHIFT_LOCKING_ANGLE;
|
||||||
|
@ -14,7 +14,10 @@ export function isLinearElement(
|
|||||||
element?: ExcalidrawElement | null,
|
element?: ExcalidrawElement | null,
|
||||||
): element is ExcalidrawLinearElement {
|
): element is ExcalidrawLinearElement {
|
||||||
return (
|
return (
|
||||||
element != null && (element.type === "arrow" || element.type === "line")
|
element != null &&
|
||||||
|
(element.type === "arrow" ||
|
||||||
|
element.type === "line" ||
|
||||||
|
element.type === "draw")
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -25,6 +28,7 @@ export function isExcalidrawElement(element: any): boolean {
|
|||||||
element?.type === "rectangle" ||
|
element?.type === "rectangle" ||
|
||||||
element?.type === "ellipse" ||
|
element?.type === "ellipse" ||
|
||||||
element?.type === "arrow" ||
|
element?.type === "arrow" ||
|
||||||
|
element?.type === "draw" ||
|
||||||
element?.type === "line"
|
element?.type === "line"
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -50,7 +50,7 @@ export type ExcalidrawTextElement = _ExcalidrawElementBase &
|
|||||||
|
|
||||||
export type ExcalidrawLinearElement = _ExcalidrawElementBase &
|
export type ExcalidrawLinearElement = _ExcalidrawElementBase &
|
||||||
Readonly<{
|
Readonly<{
|
||||||
type: "arrow" | "line";
|
type: "arrow" | "line" | "draw";
|
||||||
points: Point[];
|
points: Point[];
|
||||||
lastCommittedPoint?: Point | null;
|
lastCommittedPoint?: Point | null;
|
||||||
}>;
|
}>;
|
||||||
|
@ -96,6 +96,7 @@
|
|||||||
},
|
},
|
||||||
"toolBar": {
|
"toolBar": {
|
||||||
"selection": "Selection",
|
"selection": "Selection",
|
||||||
|
"draw": "Free draw",
|
||||||
"rectangle": "Rectangle",
|
"rectangle": "Rectangle",
|
||||||
"diamond": "Diamond",
|
"diamond": "Diamond",
|
||||||
"ellipse": "Ellipse",
|
"ellipse": "Ellipse",
|
||||||
@ -111,6 +112,7 @@
|
|||||||
},
|
},
|
||||||
"hints": {
|
"hints": {
|
||||||
"linearElement": "Click to start multiple points, drag for single line",
|
"linearElement": "Click to start multiple points, drag for single line",
|
||||||
|
"freeDraw": "Click and drag, release when you're finished",
|
||||||
"linearElementMulti": "Click on last point or press Escape or Enter to finish",
|
"linearElementMulti": "Click on last point or press Escape or Enter to finish",
|
||||||
"resize": "You can constrain proportions by holding SHIFT while resizing,\nhold ALT to resize from the center",
|
"resize": "You can constrain proportions by holding SHIFT while resizing,\nhold ALT to resize from the center",
|
||||||
"rotate": "You can constrain angles by holding SHIFT while rotating"
|
"rotate": "You can constrain angles by holding SHIFT while rotating"
|
||||||
|
@ -3,7 +3,7 @@ import {
|
|||||||
ExcalidrawTextElement,
|
ExcalidrawTextElement,
|
||||||
NonDeletedExcalidrawElement,
|
NonDeletedExcalidrawElement,
|
||||||
} from "../element/types";
|
} from "../element/types";
|
||||||
import { isTextElement } from "../element/typeChecks";
|
import { isTextElement, isLinearElement } from "../element/typeChecks";
|
||||||
import {
|
import {
|
||||||
getDiamondPoints,
|
getDiamondPoints,
|
||||||
getArrowPoints,
|
getArrowPoints,
|
||||||
@ -35,12 +35,10 @@ function generateElementCanvas(
|
|||||||
const canvas = document.createElement("canvas");
|
const canvas = document.createElement("canvas");
|
||||||
const context = canvas.getContext("2d")!;
|
const context = canvas.getContext("2d")!;
|
||||||
|
|
||||||
const isLinear = /\b(arrow|line)\b/.test(element.type);
|
|
||||||
|
|
||||||
let canvasOffsetX = 0;
|
let canvasOffsetX = 0;
|
||||||
let canvasOffsetY = 0;
|
let canvasOffsetY = 0;
|
||||||
|
|
||||||
if (isLinear) {
|
if (isLinearElement(element)) {
|
||||||
const [x1, y1, x2, y2] = getElementAbsoluteCoords(element);
|
const [x1, y1, x2, y2] = getElementAbsoluteCoords(element);
|
||||||
canvas.width =
|
canvas.width =
|
||||||
distance(x1, x2) * window.devicePixelRatio * zoom + CANVAS_PADDING * 2;
|
distance(x1, x2) * window.devicePixelRatio * zoom + CANVAS_PADDING * 2;
|
||||||
@ -90,6 +88,7 @@ function drawElementOnCanvas(
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case "arrow":
|
case "arrow":
|
||||||
|
case "draw":
|
||||||
case "line": {
|
case "line": {
|
||||||
(getShapeForElement(element) as Drawable[]).forEach((shape) =>
|
(getShapeForElement(element) as Drawable[]).forEach((shape) =>
|
||||||
rc.draw(shape),
|
rc.draw(shape),
|
||||||
@ -226,6 +225,7 @@ function generateElement(
|
|||||||
);
|
);
|
||||||
break;
|
break;
|
||||||
case "line":
|
case "line":
|
||||||
|
case "draw":
|
||||||
case "arrow": {
|
case "arrow": {
|
||||||
const options: Options = {
|
const options: Options = {
|
||||||
stroke: element.strokeColor,
|
stroke: element.strokeColor,
|
||||||
@ -240,7 +240,7 @@ function generateElement(
|
|||||||
|
|
||||||
// If shape is a line and is a closed shape,
|
// If shape is a line and is a closed shape,
|
||||||
// fill the shape if a color is set.
|
// fill the shape if a color is set.
|
||||||
if (element.type === "line") {
|
if (element.type === "line" || element.type === "draw") {
|
||||||
if (isPathALoop(element.points)) {
|
if (isPathALoop(element.points)) {
|
||||||
options.fillStyle = element.fillStyle;
|
options.fillStyle = element.fillStyle;
|
||||||
options.fill =
|
options.fill =
|
||||||
@ -343,6 +343,7 @@ export function renderElement(
|
|||||||
case "diamond":
|
case "diamond":
|
||||||
case "ellipse":
|
case "ellipse":
|
||||||
case "line":
|
case "line":
|
||||||
|
case "draw":
|
||||||
case "arrow":
|
case "arrow":
|
||||||
case "text": {
|
case "text": {
|
||||||
const elementWithCanvas = generateElement(element, generator, sceneState);
|
const elementWithCanvas = generateElement(element, generator, sceneState);
|
||||||
@ -410,6 +411,7 @@ export function renderElementToSvg(
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case "line":
|
case "line":
|
||||||
|
case "draw":
|
||||||
case "arrow": {
|
case "arrow": {
|
||||||
generateElement(element, generator);
|
generateElement(element, generator);
|
||||||
const group = svgRoot.ownerDocument!.createElementNS(SVG_NS, "g");
|
const group = svgRoot.ownerDocument!.createElementNS(SVG_NS, "g");
|
||||||
@ -427,7 +429,7 @@ export function renderElementToSvg(
|
|||||||
}) rotate(${degree} ${cx} ${cy})`,
|
}) rotate(${degree} ${cx} ${cy})`,
|
||||||
);
|
);
|
||||||
if (
|
if (
|
||||||
element.type === "line" &&
|
(element.type === "line" || element.type === "draw") &&
|
||||||
isPathALoop(element.points) &&
|
isPathALoop(element.points) &&
|
||||||
element.backgroundColor !== "transparent"
|
element.backgroundColor !== "transparent"
|
||||||
) {
|
) {
|
||||||
|
@ -10,6 +10,7 @@ export const hasBackground = (type: string) =>
|
|||||||
type === "rectangle" ||
|
type === "rectangle" ||
|
||||||
type === "ellipse" ||
|
type === "ellipse" ||
|
||||||
type === "diamond" ||
|
type === "diamond" ||
|
||||||
|
type === "draw" ||
|
||||||
type === "line";
|
type === "line";
|
||||||
|
|
||||||
export const hasStroke = (type: string) =>
|
export const hasStroke = (type: string) =>
|
||||||
@ -17,6 +18,7 @@ export const hasStroke = (type: string) =>
|
|||||||
type === "ellipse" ||
|
type === "ellipse" ||
|
||||||
type === "diamond" ||
|
type === "diamond" ||
|
||||||
type === "arrow" ||
|
type === "arrow" ||
|
||||||
|
type === "draw" ||
|
||||||
type === "line";
|
type === "line";
|
||||||
|
|
||||||
export const hasText = (type: string) => type === "text";
|
export const hasText = (type: string) => type === "text";
|
||||||
|
@ -11,6 +11,7 @@ export const SHAPES = [
|
|||||||
</svg>
|
</svg>
|
||||||
),
|
),
|
||||||
value: "selection",
|
value: "selection",
|
||||||
|
key: "s",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
icon: (
|
icon: (
|
||||||
@ -20,6 +21,7 @@ export const SHAPES = [
|
|||||||
</svg>
|
</svg>
|
||||||
),
|
),
|
||||||
value: "rectangle",
|
value: "rectangle",
|
||||||
|
key: "r",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
icon: (
|
icon: (
|
||||||
@ -29,6 +31,7 @@ export const SHAPES = [
|
|||||||
</svg>
|
</svg>
|
||||||
),
|
),
|
||||||
value: "diamond",
|
value: "diamond",
|
||||||
|
key: "d",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
icon: (
|
icon: (
|
||||||
@ -38,6 +41,7 @@ export const SHAPES = [
|
|||||||
</svg>
|
</svg>
|
||||||
),
|
),
|
||||||
value: "ellipse",
|
value: "ellipse",
|
||||||
|
key: "e",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
icon: (
|
icon: (
|
||||||
@ -47,6 +51,7 @@ export const SHAPES = [
|
|||||||
</svg>
|
</svg>
|
||||||
),
|
),
|
||||||
value: "arrow",
|
value: "arrow",
|
||||||
|
key: "a",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
icon: (
|
icon: (
|
||||||
@ -63,6 +68,20 @@ export const SHAPES = [
|
|||||||
</svg>
|
</svg>
|
||||||
),
|
),
|
||||||
value: "line",
|
value: "line",
|
||||||
|
key: "l",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
icon: (
|
||||||
|
// fa-pencil
|
||||||
|
<svg viewBox="0 0 512 512">
|
||||||
|
<path
|
||||||
|
fill="currentColor"
|
||||||
|
d="M290.74 93.24l128.02 128.02-277.99 277.99-114.14 12.6C11.35 513.54-1.56 500.62.14 485.34l12.7-114.22 277.9-277.88zm207.2-19.06l-60.11-60.11c-18.75-18.75-49.16-18.75-67.91 0l-56.55 56.55 128.02 128.02 56.55-56.55c18.75-18.76 18.75-49.16 0-67.91z"
|
||||||
|
></path>
|
||||||
|
</svg>
|
||||||
|
),
|
||||||
|
value: "draw",
|
||||||
|
key: "x",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
icon: (
|
icon: (
|
||||||
@ -72,20 +91,19 @@ export const SHAPES = [
|
|||||||
</svg>
|
</svg>
|
||||||
),
|
),
|
||||||
value: "text",
|
value: "text",
|
||||||
|
key: "t",
|
||||||
},
|
},
|
||||||
] as const;
|
] as const;
|
||||||
|
|
||||||
export const shapesShortcutKeys = SHAPES.map((shape, index) => [
|
export const shapesShortcutKeys = SHAPES.map((shape, index) => [
|
||||||
shape.value[0],
|
shape.key,
|
||||||
(index + 1).toString(),
|
(index + 1).toString(),
|
||||||
]).flat(1);
|
]).flat(1);
|
||||||
|
|
||||||
export function findShapeByKey(key: string) {
|
export function findShapeByKey(key: string) {
|
||||||
return (
|
return (
|
||||||
SHAPES.find((shape, index) => {
|
SHAPES.find((shape, index) => {
|
||||||
return (
|
return shape.key === key.toLowerCase() || key === (index + 1).toString();
|
||||||
shape.value[0] === key.toLowerCase() || key === (index + 1).toString()
|
|
||||||
);
|
|
||||||
})?.value || "selection"
|
})?.value || "selection"
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -1633,7 +1633,7 @@ Object {
|
|||||||
"currentItemStrokeColor": "#000000",
|
"currentItemStrokeColor": "#000000",
|
||||||
"currentItemStrokeWidth": 1,
|
"currentItemStrokeWidth": 1,
|
||||||
"currentItemTextAlign": "left",
|
"currentItemTextAlign": "left",
|
||||||
"cursorButton": "up",
|
"cursorButton": "down",
|
||||||
"cursorX": 0,
|
"cursorX": 0,
|
||||||
"cursorY": 0,
|
"cursorY": 0,
|
||||||
"draggingElement": null,
|
"draggingElement": null,
|
||||||
@ -1654,7 +1654,9 @@ Object {
|
|||||||
"scrollX": 0,
|
"scrollX": 0,
|
||||||
"scrollY": 0,
|
"scrollY": 0,
|
||||||
"scrolledOutside": false,
|
"scrolledOutside": false,
|
||||||
"selectedElementIds": Object {},
|
"selectedElementIds": Object {
|
||||||
|
"id7": true,
|
||||||
|
},
|
||||||
"selectionElement": null,
|
"selectionElement": null,
|
||||||
"shouldAddWatermark": false,
|
"shouldAddWatermark": false,
|
||||||
"shouldCacheIgnoreZoom": false,
|
"shouldCacheIgnoreZoom": false,
|
||||||
@ -1798,6 +1800,39 @@ Object {
|
|||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
|
exports[`regression tests draw every type of shape: [end of test] element 5 1`] = `
|
||||||
|
Object {
|
||||||
|
"angle": 0,
|
||||||
|
"backgroundColor": "transparent",
|
||||||
|
"fillStyle": "hachure",
|
||||||
|
"height": 10,
|
||||||
|
"id": "id7",
|
||||||
|
"isDeleted": false,
|
||||||
|
"lastCommittedPoint": null,
|
||||||
|
"opacity": 100,
|
||||||
|
"points": Array [
|
||||||
|
Array [
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
],
|
||||||
|
Array [
|
||||||
|
10,
|
||||||
|
10,
|
||||||
|
],
|
||||||
|
],
|
||||||
|
"roughness": 1,
|
||||||
|
"seed": 1051383431,
|
||||||
|
"strokeColor": "#000000",
|
||||||
|
"strokeWidth": 1,
|
||||||
|
"type": "draw",
|
||||||
|
"version": 3,
|
||||||
|
"versionNonce": 1279028647,
|
||||||
|
"width": 10,
|
||||||
|
"x": 30,
|
||||||
|
"y": 10,
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
exports[`regression tests draw every type of shape: [end of test] history 1`] = `
|
exports[`regression tests draw every type of shape: [end of test] history 1`] = `
|
||||||
Object {
|
Object {
|
||||||
"recording": false,
|
"recording": false,
|
||||||
@ -2248,9 +2283,9 @@ Object {
|
|||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
exports[`regression tests draw every type of shape: [end of test] number of elements 1`] = `5`;
|
exports[`regression tests draw every type of shape: [end of test] number of elements 1`] = `6`;
|
||||||
|
|
||||||
exports[`regression tests draw every type of shape: [end of test] number of renders 1`] = `34`;
|
exports[`regression tests draw every type of shape: [end of test] number of renders 1`] = `38`;
|
||||||
|
|
||||||
exports[`regression tests hotkey 2 selects rectangle tool: [end of test] appState 1`] = `
|
exports[`regression tests hotkey 2 selects rectangle tool: [end of test] appState 1`] = `
|
||||||
Object {
|
Object {
|
||||||
@ -2901,6 +2936,97 @@ exports[`regression tests hotkey 6 selects line tool: [end of test] number of el
|
|||||||
|
|
||||||
exports[`regression tests hotkey 6 selects line tool: [end of test] number of renders 1`] = `6`;
|
exports[`regression tests hotkey 6 selects line tool: [end of test] number of renders 1`] = `6`;
|
||||||
|
|
||||||
|
exports[`regression tests hotkey 7 selects draw tool: [end of test] appState 1`] = `
|
||||||
|
Object {
|
||||||
|
"collaborators": Map {},
|
||||||
|
"currentItemBackgroundColor": "transparent",
|
||||||
|
"currentItemFillStyle": "hachure",
|
||||||
|
"currentItemFont": "20px Virgil",
|
||||||
|
"currentItemOpacity": 100,
|
||||||
|
"currentItemRoughness": 1,
|
||||||
|
"currentItemStrokeColor": "#000000",
|
||||||
|
"currentItemStrokeWidth": 1,
|
||||||
|
"currentItemTextAlign": "left",
|
||||||
|
"cursorButton": "down",
|
||||||
|
"cursorX": 0,
|
||||||
|
"cursorY": 0,
|
||||||
|
"draggingElement": null,
|
||||||
|
"editingElement": null,
|
||||||
|
"elementLocked": false,
|
||||||
|
"elementType": "selection",
|
||||||
|
"errorMessage": null,
|
||||||
|
"exportBackground": true,
|
||||||
|
"isCollaborating": false,
|
||||||
|
"isLoading": false,
|
||||||
|
"isResizing": false,
|
||||||
|
"isRotating": false,
|
||||||
|
"lastPointerDownWith": "mouse",
|
||||||
|
"multiElement": null,
|
||||||
|
"name": "Untitled-201933152653",
|
||||||
|
"openMenu": null,
|
||||||
|
"resizingElement": null,
|
||||||
|
"scrollX": 0,
|
||||||
|
"scrollY": 0,
|
||||||
|
"scrolledOutside": false,
|
||||||
|
"selectedElementIds": Object {
|
||||||
|
"id0": true,
|
||||||
|
},
|
||||||
|
"selectionElement": null,
|
||||||
|
"shouldAddWatermark": false,
|
||||||
|
"shouldCacheIgnoreZoom": false,
|
||||||
|
"showShortcutsDialog": false,
|
||||||
|
"username": "",
|
||||||
|
"viewBackgroundColor": "#ffffff",
|
||||||
|
"zenModeEnabled": false,
|
||||||
|
"zoom": 1,
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
exports[`regression tests hotkey 7 selects draw tool: [end of test] element 0 1`] = `
|
||||||
|
Object {
|
||||||
|
"angle": 0,
|
||||||
|
"backgroundColor": "transparent",
|
||||||
|
"fillStyle": "hachure",
|
||||||
|
"height": 10,
|
||||||
|
"id": "id0",
|
||||||
|
"isDeleted": false,
|
||||||
|
"lastCommittedPoint": null,
|
||||||
|
"opacity": 100,
|
||||||
|
"points": Array [
|
||||||
|
Array [
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
],
|
||||||
|
Array [
|
||||||
|
10,
|
||||||
|
10,
|
||||||
|
],
|
||||||
|
],
|
||||||
|
"roughness": 1,
|
||||||
|
"seed": 337897,
|
||||||
|
"strokeColor": "#000000",
|
||||||
|
"strokeWidth": 1,
|
||||||
|
"type": "draw",
|
||||||
|
"version": 3,
|
||||||
|
"versionNonce": 449462985,
|
||||||
|
"width": 10,
|
||||||
|
"x": 10,
|
||||||
|
"y": 10,
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
exports[`regression tests hotkey 7 selects draw tool: [end of test] history 1`] = `
|
||||||
|
Object {
|
||||||
|
"recording": false,
|
||||||
|
"redoStack": Array [],
|
||||||
|
"stateHistory": Array [],
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
exports[`regression tests hotkey 7 selects draw tool: [end of test] number of elements 1`] = `1`;
|
||||||
|
|
||||||
|
exports[`regression tests hotkey 7 selects draw tool: [end of test] number of renders 1`] = `6`;
|
||||||
|
|
||||||
exports[`regression tests hotkey a selects arrow tool: [end of test] appState 1`] = `
|
exports[`regression tests hotkey a selects arrow tool: [end of test] appState 1`] = `
|
||||||
Object {
|
Object {
|
||||||
"collaborators": Map {},
|
"collaborators": Map {},
|
||||||
@ -3550,6 +3676,97 @@ exports[`regression tests hotkey r selects rectangle tool: [end of test] number
|
|||||||
|
|
||||||
exports[`regression tests hotkey r selects rectangle tool: [end of test] number of renders 1`] = `6`;
|
exports[`regression tests hotkey r selects rectangle tool: [end of test] number of renders 1`] = `6`;
|
||||||
|
|
||||||
|
exports[`regression tests hotkey x selects draw tool: [end of test] appState 1`] = `
|
||||||
|
Object {
|
||||||
|
"collaborators": Map {},
|
||||||
|
"currentItemBackgroundColor": "transparent",
|
||||||
|
"currentItemFillStyle": "hachure",
|
||||||
|
"currentItemFont": "20px Virgil",
|
||||||
|
"currentItemOpacity": 100,
|
||||||
|
"currentItemRoughness": 1,
|
||||||
|
"currentItemStrokeColor": "#000000",
|
||||||
|
"currentItemStrokeWidth": 1,
|
||||||
|
"currentItemTextAlign": "left",
|
||||||
|
"cursorButton": "down",
|
||||||
|
"cursorX": 0,
|
||||||
|
"cursorY": 0,
|
||||||
|
"draggingElement": null,
|
||||||
|
"editingElement": null,
|
||||||
|
"elementLocked": false,
|
||||||
|
"elementType": "selection",
|
||||||
|
"errorMessage": null,
|
||||||
|
"exportBackground": true,
|
||||||
|
"isCollaborating": false,
|
||||||
|
"isLoading": false,
|
||||||
|
"isResizing": false,
|
||||||
|
"isRotating": false,
|
||||||
|
"lastPointerDownWith": "mouse",
|
||||||
|
"multiElement": null,
|
||||||
|
"name": "Untitled-201933152653",
|
||||||
|
"openMenu": null,
|
||||||
|
"resizingElement": null,
|
||||||
|
"scrollX": 0,
|
||||||
|
"scrollY": 0,
|
||||||
|
"scrolledOutside": false,
|
||||||
|
"selectedElementIds": Object {
|
||||||
|
"id0": true,
|
||||||
|
},
|
||||||
|
"selectionElement": null,
|
||||||
|
"shouldAddWatermark": false,
|
||||||
|
"shouldCacheIgnoreZoom": false,
|
||||||
|
"showShortcutsDialog": false,
|
||||||
|
"username": "",
|
||||||
|
"viewBackgroundColor": "#ffffff",
|
||||||
|
"zenModeEnabled": false,
|
||||||
|
"zoom": 1,
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
exports[`regression tests hotkey x selects draw tool: [end of test] element 0 1`] = `
|
||||||
|
Object {
|
||||||
|
"angle": 0,
|
||||||
|
"backgroundColor": "transparent",
|
||||||
|
"fillStyle": "hachure",
|
||||||
|
"height": 10,
|
||||||
|
"id": "id0",
|
||||||
|
"isDeleted": false,
|
||||||
|
"lastCommittedPoint": null,
|
||||||
|
"opacity": 100,
|
||||||
|
"points": Array [
|
||||||
|
Array [
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
],
|
||||||
|
Array [
|
||||||
|
10,
|
||||||
|
10,
|
||||||
|
],
|
||||||
|
],
|
||||||
|
"roughness": 1,
|
||||||
|
"seed": 337897,
|
||||||
|
"strokeColor": "#000000",
|
||||||
|
"strokeWidth": 1,
|
||||||
|
"type": "draw",
|
||||||
|
"version": 3,
|
||||||
|
"versionNonce": 449462985,
|
||||||
|
"width": 10,
|
||||||
|
"x": 10,
|
||||||
|
"y": 10,
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
exports[`regression tests hotkey x selects draw tool: [end of test] history 1`] = `
|
||||||
|
Object {
|
||||||
|
"recording": false,
|
||||||
|
"redoStack": Array [],
|
||||||
|
"stateHistory": Array [],
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
exports[`regression tests hotkey x selects draw tool: [end of test] number of elements 1`] = `1`;
|
||||||
|
|
||||||
|
exports[`regression tests hotkey x selects draw tool: [end of test] number of renders 1`] = `6`;
|
||||||
|
|
||||||
exports[`regression tests pinch-to-zoom works: [end of test] appState 1`] = `
|
exports[`regression tests pinch-to-zoom works: [end of test] appState 1`] = `
|
||||||
Object {
|
Object {
|
||||||
"collaborators": Map {},
|
"collaborators": Map {},
|
||||||
|
@ -7,6 +7,7 @@ const toolMap = {
|
|||||||
ellipse: "ellipse",
|
ellipse: "ellipse",
|
||||||
arrow: "arrow",
|
arrow: "arrow",
|
||||||
line: "line",
|
line: "line",
|
||||||
|
draw: "draw",
|
||||||
};
|
};
|
||||||
|
|
||||||
export type ToolName = keyof typeof toolMap;
|
export type ToolName = keyof typeof toolMap;
|
||||||
|
@ -265,6 +265,11 @@ describe("regression tests", () => {
|
|||||||
pointerMove(30, 50);
|
pointerMove(30, 50);
|
||||||
pointerUp();
|
pointerUp();
|
||||||
hotkeyPress("ENTER");
|
hotkeyPress("ENTER");
|
||||||
|
|
||||||
|
clickTool("draw");
|
||||||
|
pointerDown(30, 10);
|
||||||
|
pointerMove(40, 20);
|
||||||
|
pointerUp();
|
||||||
});
|
});
|
||||||
|
|
||||||
it("click to select a shape", () => {
|
it("click to select a shape", () => {
|
||||||
@ -290,6 +295,7 @@ describe("regression tests", () => {
|
|||||||
["4e", "ellipse"],
|
["4e", "ellipse"],
|
||||||
["5a", "arrow"],
|
["5a", "arrow"],
|
||||||
["6l", "line"],
|
["6l", "line"],
|
||||||
|
["7x", "draw"],
|
||||||
] as [string, ExcalidrawElement["type"]][]) {
|
] as [string, ExcalidrawElement["type"]][]) {
|
||||||
for (const key of keys) {
|
for (const key of keys) {
|
||||||
it(`hotkey ${key} selects ${shape} tool`, () => {
|
it(`hotkey ${key} selects ${shape} tool`, () => {
|
||||||
|
Loading…
x
Reference in New Issue
Block a user