Add free draw mode (#1570)

This commit is contained in:
Kostas Bariotis 2020-05-12 20:10:11 +01:00 committed by GitHub
parent 36e0c439fb
commit 9ec43d2626
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
21 changed files with 344 additions and 49 deletions

6
package-lock.json generated
View File

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

View File

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

View File

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

View File

@ -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({

View File

@ -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();

View File

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

View File

@ -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={[

View File

@ -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) {

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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"
) { ) {

View File

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

View File

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

View File

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

View File

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

View File

@ -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`, () => {