Преглед изворни кода

feat: improved freedraw (#3512)

Co-authored-by: dwelle <luzar.david@gmail.com>
vanilla_orig
Steve Ruiz пре 4 година
родитељ
комит
49c6bdd520
No account linked to committer's email address
66 измењених фајлова са 782 додато и 243 уклоњено
  1. 1
    0
      package.json
  2. 10
    8
      src/actions/actionFinalize.tsx
  3. 2
    2
      src/actions/actionFlip.ts
  4. 1
    0
      src/actions/types.ts
  5. 12
    4
      src/components/Actions.tsx
  6. 120
    32
      src/components/App.tsx
  7. 1
    1
      src/components/HelpDialog.tsx
  8. 1
    1
      src/components/HintViewer.tsx
  9. 13
    2
      src/data/restore.ts
  10. 116
    44
      src/element/bounds.ts
  11. 98
    3
      src/element/collision.ts
  12. 17
    0
      src/element/newElement.ts
  13. 11
    3
      src/element/resizeElements.ts
  14. 3
    3
      src/element/sizeHelpers.ts
  15. 1
    1
      src/element/transformHandles.ts
  16. 15
    2
      src/element/typeChecks.ts
  17. 11
    1
      src/element/types.ts
  18. 1
    1
      src/locales/ar-SA.json
  19. 1
    1
      src/locales/bg-BG.json
  20. 1
    1
      src/locales/ca-ES.json
  21. 1
    1
      src/locales/de-DE.json
  22. 1
    1
      src/locales/el-GR.json
  23. 4
    1
      src/locales/en.json
  24. 1
    1
      src/locales/es-ES.json
  25. 1
    1
      src/locales/fa-IR.json
  26. 1
    1
      src/locales/fi-FI.json
  27. 1
    1
      src/locales/fr-FR.json
  28. 1
    1
      src/locales/he-IL.json
  29. 1
    1
      src/locales/hi-IN.json
  30. 1
    1
      src/locales/hu-HU.json
  31. 1
    1
      src/locales/id-ID.json
  32. 1
    1
      src/locales/it-IT.json
  33. 1
    1
      src/locales/ja-JP.json
  34. 1
    1
      src/locales/kab-KAB.json
  35. 1
    1
      src/locales/ko-KR.json
  36. 1
    1
      src/locales/my-MM.json
  37. 1
    1
      src/locales/nb-NO.json
  38. 1
    1
      src/locales/nl-NL.json
  39. 1
    1
      src/locales/nn-NO.json
  40. 1
    1
      src/locales/oc-FR.json
  41. 1
    1
      src/locales/pa-IN.json
  42. 1
    1
      src/locales/pl-PL.json
  43. 1
    1
      src/locales/pt-BR.json
  44. 1
    1
      src/locales/pt-PT.json
  45. 1
    1
      src/locales/ro-RO.json
  46. 1
    1
      src/locales/ru-RU.json
  47. 1
    1
      src/locales/sk-SK.json
  48. 1
    1
      src/locales/sv-SE.json
  49. 1
    1
      src/locales/tr-TR.json
  50. 1
    1
      src/locales/uk-UA.json
  51. 1
    1
      src/locales/zh-CN.json
  52. 1
    1
      src/locales/zh-TW.json
  53. 1
    0
      src/math.ts
  54. 1
    0
      src/points.ts
  55. 180
    28
      src/renderer/renderElement.ts
  56. 2
    1
      src/renderer/renderScene.ts
  57. 8
    5
      src/scene/comparisons.ts
  58. 2
    1
      src/scene/index.ts
  59. 1
    1
      src/shapes.tsx
  60. 91
    55
      src/tests/__snapshots__/regressionTests.test.tsx.snap
  61. 1
    1
      src/tests/flip.test.tsx
  62. 13
    3
      src/tests/helpers/api.ts
  63. 2
    2
      src/tests/helpers/ui.ts
  64. 1
    1
      src/tests/queries/toolQueries.ts
  65. 3
    3
      src/tests/regressionTests.test.tsx
  66. 5
    0
      yarn.lock

+ 1
- 0
package.json Прегледај датотеку

35
     "nanoid": "3.1.22",
35
     "nanoid": "3.1.22",
36
     "open-color": "1.8.0",
36
     "open-color": "1.8.0",
37
     "pako": "1.0.11",
37
     "pako": "1.0.11",
38
+    "perfect-freehand": "0.4.7",
38
     "png-chunk-text": "1.0.0",
39
     "png-chunk-text": "1.0.0",
39
     "png-chunks-encode": "1.0.0",
40
     "png-chunks-encode": "1.0.0",
40
     "png-chunks-extract": "1.0.0",
41
     "png-chunks-extract": "1.0.0",

+ 10
- 8
src/actions/actionFinalize.tsx Прегледај датотеку

56
 
56
 
57
     const multiPointElement = appState.multiElement
57
     const multiPointElement = appState.multiElement
58
       ? appState.multiElement
58
       ? appState.multiElement
59
-      : appState.editingElement?.type === "draw"
59
+      : appState.editingElement?.type === "freedraw"
60
       ? appState.editingElement
60
       ? appState.editingElement
61
       : null;
61
       : null;
62
 
62
 
63
     if (multiPointElement) {
63
     if (multiPointElement) {
64
       // pen and mouse have hover
64
       // pen and mouse have hover
65
       if (
65
       if (
66
-        multiPointElement.type !== "draw" &&
66
+        multiPointElement.type !== "freedraw" &&
67
         appState.lastPointerDownWith !== "touch"
67
         appState.lastPointerDownWith !== "touch"
68
       ) {
68
       ) {
69
         const { points, lastCommittedPoint } = multiPointElement;
69
         const { points, lastCommittedPoint } = multiPointElement;
86
       const isLoop = isPathALoop(multiPointElement.points, appState.zoom.value);
86
       const isLoop = isPathALoop(multiPointElement.points, appState.zoom.value);
87
       if (
87
       if (
88
         multiPointElement.type === "line" ||
88
         multiPointElement.type === "line" ||
89
-        multiPointElement.type === "draw"
89
+        multiPointElement.type === "freedraw"
90
       ) {
90
       ) {
91
         if (isLoop) {
91
         if (isLoop) {
92
           const linePoints = multiPointElement.points;
92
           const linePoints = multiPointElement.points;
118
         );
118
         );
119
       }
119
       }
120
 
120
 
121
-      if (!appState.elementLocked && appState.elementType !== "draw") {
121
+      if (!appState.elementLocked && appState.elementType !== "freedraw") {
122
         appState.selectedElementIds[multiPointElement.id] = true;
122
         appState.selectedElementIds[multiPointElement.id] = true;
123
       }
123
       }
124
     }
124
     }
125
+
125
     if (
126
     if (
126
-      (!appState.elementLocked && appState.elementType !== "draw") ||
127
+      (!appState.elementLocked && appState.elementType !== "freedraw") ||
127
       !multiPointElement
128
       !multiPointElement
128
     ) {
129
     ) {
129
       resetCursor(canvas);
130
       resetCursor(canvas);
130
     }
131
     }
132
+
131
     return {
133
     return {
132
       elements: newElements,
134
       elements: newElements,
133
       appState: {
135
       appState: {
134
         ...appState,
136
         ...appState,
135
         elementType:
137
         elementType:
136
-          (appState.elementLocked || appState.elementType === "draw") &&
138
+          (appState.elementLocked || appState.elementType === "freedraw") &&
137
           multiPointElement
139
           multiPointElement
138
             ? appState.elementType
140
             ? appState.elementType
139
             : "selection",
141
             : "selection",
145
         selectedElementIds:
147
         selectedElementIds:
146
           multiPointElement &&
148
           multiPointElement &&
147
           !appState.elementLocked &&
149
           !appState.elementLocked &&
148
-          appState.elementType !== "draw"
150
+          appState.elementType !== "freedraw"
149
             ? {
151
             ? {
150
                 ...appState.selectedElementIds,
152
                 ...appState.selectedElementIds,
151
                 [multiPointElement.id]: true,
153
                 [multiPointElement.id]: true,
152
               }
154
               }
153
             : appState.selectedElementIds,
155
             : appState.selectedElementIds,
154
       },
156
       },
155
-      commitToHistory: appState.elementType === "draw",
157
+      commitToHistory: appState.elementType === "freedraw",
156
     };
158
     };
157
   },
159
   },
158
   keyTest: (event, appState) =>
160
   keyTest: (event, appState) =>

+ 2
- 2
src/actions/actionFlip.ts Прегледај датотеку

6
 import { normalizeAngle, resizeSingleElement } from "../element/resizeElements";
6
 import { normalizeAngle, resizeSingleElement } from "../element/resizeElements";
7
 import { AppState } from "../types";
7
 import { AppState } from "../types";
8
 import { getTransformHandles } from "../element/transformHandles";
8
 import { getTransformHandles } from "../element/transformHandles";
9
-import { isLinearElement } from "../element/typeChecks";
9
+import { isFreeDrawElement, isLinearElement } from "../element/typeChecks";
10
 import { updateBoundElements } from "../element/binding";
10
 import { updateBoundElements } from "../element/binding";
11
 import { LinearElementEditor } from "../element/linearElementEditor";
11
 import { LinearElementEditor } from "../element/linearElementEditor";
12
 
12
 
114
   const originalAngle = normalizeAngle(element.angle);
114
   const originalAngle = normalizeAngle(element.angle);
115
 
115
 
116
   let finalOffsetX = 0;
116
   let finalOffsetX = 0;
117
-  if (isLinearElement(element)) {
117
+  if (isLinearElement(element) || isFreeDrawElement(element)) {
118
     finalOffsetX =
118
     finalOffsetX =
119
       element.points.reduce((max, point) => Math.max(max, point[0]), 0) * 2 -
119
       element.points.reduce((max, point) => Math.max(max, point[0]), 0) * 2 -
120
       element.width;
120
       element.width;

+ 1
- 0
src/actions/types.ts Прегледај датотеку

52
   | "changeBackgroundColor"
52
   | "changeBackgroundColor"
53
   | "changeFillStyle"
53
   | "changeFillStyle"
54
   | "changeStrokeWidth"
54
   | "changeStrokeWidth"
55
+  | "changeStrokeShape"
55
   | "changeSloppiness"
56
   | "changeSloppiness"
56
   | "changeStrokeStyle"
57
   | "changeStrokeStyle"
57
   | "changeArrowhead"
58
   | "changeArrowhead"

+ 12
- 4
src/components/Actions.tsx Прегледај датотеку

9
   canHaveArrowheads,
9
   canHaveArrowheads,
10
   getTargetElements,
10
   getTargetElements,
11
   hasBackground,
11
   hasBackground,
12
-  hasStroke,
12
+  hasStrokeStyle,
13
+  hasStrokeWidth,
13
   hasText,
14
   hasText,
14
 } from "../scene";
15
 } from "../scene";
15
 import { SHAPES } from "../shapes";
16
 import { SHAPES } from "../shapes";
53
       {showChangeBackgroundIcons && renderAction("changeBackgroundColor")}
54
       {showChangeBackgroundIcons && renderAction("changeBackgroundColor")}
54
       {showFillIcons && renderAction("changeFillStyle")}
55
       {showFillIcons && renderAction("changeFillStyle")}
55
 
56
 
56
-      {(hasStroke(elementType) ||
57
-        targetElements.some((element) => hasStroke(element.type))) && (
57
+      {(hasStrokeWidth(elementType) ||
58
+        targetElements.some((element) => hasStrokeWidth(element.type))) &&
59
+        renderAction("changeStrokeWidth")}
60
+
61
+      {(elementType === "freedraw" ||
62
+        targetElements.some((element) => element.type === "freedraw")) &&
63
+        renderAction("changeStrokeShape")}
64
+
65
+      {(hasStrokeStyle(elementType) ||
66
+        targetElements.some((element) => hasStrokeStyle(element.type))) && (
58
         <>
67
         <>
59
-          {renderAction("changeStrokeWidth")}
60
           {renderAction("changeStrokeStyle")}
68
           {renderAction("changeStrokeStyle")}
61
           {renderAction("changeSloppiness")}
69
           {renderAction("changeSloppiness")}
62
         </>
70
         </>

+ 120
- 32
src/components/App.tsx Прегледај датотеку

1
-import { Point, simplify } from "points-on-curve";
2
 import React, { useContext } from "react";
1
 import React, { useContext } from "react";
3
 import { RoughCanvas } from "roughjs/bin/canvas";
2
 import { RoughCanvas } from "roughjs/bin/canvas";
4
 import rough from "roughjs/bin/rough";
3
 import rough from "roughjs/bin/rough";
70
 import { loadFromBlob } from "../data";
69
 import { loadFromBlob } from "../data";
71
 import { isValidLibrary } from "../data/json";
70
 import { isValidLibrary } from "../data/json";
72
 import Library from "../data/library";
71
 import Library from "../data/library";
73
-import { restore } from "../data/restore";
72
+import { restore, restoreElements } from "../data/restore";
74
 import {
73
 import {
75
   dragNewElement,
74
   dragNewElement,
76
   dragSelectedElements,
75
   dragSelectedElements,
111
 } from "../element/binding";
110
 } from "../element/binding";
112
 import { LinearElementEditor } from "../element/linearElementEditor";
111
 import { LinearElementEditor } from "../element/linearElementEditor";
113
 import { mutateElement } from "../element/mutateElement";
112
 import { mutateElement } from "../element/mutateElement";
114
-import { deepCopyElement } from "../element/newElement";
113
+import { deepCopyElement, newFreeDrawElement } from "../element/newElement";
115
 import { MaybeTransformHandleType } from "../element/transformHandles";
114
 import { MaybeTransformHandleType } from "../element/transformHandles";
116
 import {
115
 import {
117
   isBindingElement,
116
   isBindingElement,
122
 import {
121
 import {
123
   ExcalidrawBindableElement,
122
   ExcalidrawBindableElement,
124
   ExcalidrawElement,
123
   ExcalidrawElement,
124
+  ExcalidrawFreeDrawElement,
125
   ExcalidrawGenericElement,
125
   ExcalidrawGenericElement,
126
   ExcalidrawLinearElement,
126
   ExcalidrawLinearElement,
127
   ExcalidrawTextElement,
127
   ExcalidrawTextElement,
1266
         });
1266
         });
1267
       } else if (data.elements) {
1267
       } else if (data.elements) {
1268
         this.addElementsFromPasteOrLibrary({
1268
         this.addElementsFromPasteOrLibrary({
1269
-          elements: data.elements,
1269
+          elements: restoreElements(data.elements),
1270
           position: "cursor",
1270
           position: "cursor",
1271
         });
1271
         });
1272
       } else if (data.text) {
1272
       } else if (data.text) {
2341
       return;
2341
       return;
2342
     } else if (
2342
     } else if (
2343
       this.state.elementType === "arrow" ||
2343
       this.state.elementType === "arrow" ||
2344
-      this.state.elementType === "draw" ||
2345
       this.state.elementType === "line"
2344
       this.state.elementType === "line"
2346
     ) {
2345
     ) {
2347
       this.handleLinearElementOnPointerDown(
2346
       this.handleLinearElementOnPointerDown(
2349
         this.state.elementType,
2348
         this.state.elementType,
2350
         pointerDownState,
2349
         pointerDownState,
2351
       );
2350
       );
2351
+    } else if (this.state.elementType === "freedraw") {
2352
+      this.handleFreeDrawElementOnPointerDown(
2353
+        event,
2354
+        this.state.elementType,
2355
+        pointerDownState,
2356
+      );
2352
     } else {
2357
     } else {
2353
       this.createGenericElementOnPointerDown(
2358
       this.createGenericElementOnPointerDown(
2354
         this.state.elementType,
2359
         this.state.elementType,
2845
     }
2850
     }
2846
   };
2851
   };
2847
 
2852
 
2853
+  private handleFreeDrawElementOnPointerDown = (
2854
+    event: React.PointerEvent<HTMLCanvasElement>,
2855
+    elementType: ExcalidrawFreeDrawElement["type"],
2856
+    pointerDownState: PointerDownState,
2857
+  ) => {
2858
+    // Begin a mark capture. This does not have to update state yet.
2859
+    const [gridX, gridY] = getGridPoint(
2860
+      pointerDownState.origin.x,
2861
+      pointerDownState.origin.y,
2862
+      null,
2863
+    );
2864
+
2865
+    const element = newFreeDrawElement({
2866
+      type: elementType,
2867
+      x: gridX,
2868
+      y: gridY,
2869
+      strokeColor: this.state.currentItemStrokeColor,
2870
+      backgroundColor: this.state.currentItemBackgroundColor,
2871
+      fillStyle: this.state.currentItemFillStyle,
2872
+      strokeWidth: this.state.currentItemStrokeWidth,
2873
+      strokeStyle: this.state.currentItemStrokeStyle,
2874
+      roughness: this.state.currentItemRoughness,
2875
+      opacity: this.state.currentItemOpacity,
2876
+      strokeSharpness: this.state.currentItemLinearStrokeSharpness,
2877
+      simulatePressure: event.pressure === 0.5,
2878
+    });
2879
+
2880
+    this.setState((prevState) => ({
2881
+      selectedElementIds: {
2882
+        ...prevState.selectedElementIds,
2883
+        [element.id]: false,
2884
+      },
2885
+    }));
2886
+
2887
+    const pressures = element.simulatePressure
2888
+      ? element.pressures
2889
+      : [...element.pressures, event.pressure];
2890
+
2891
+    mutateElement(element, {
2892
+      points: [[0, 0]],
2893
+      pressures,
2894
+    });
2895
+
2896
+    const boundElement = getHoveredElementForBinding(
2897
+      pointerDownState.origin,
2898
+      this.scene,
2899
+    );
2900
+    this.scene.replaceAllElements([
2901
+      ...this.scene.getElementsIncludingDeleted(),
2902
+      element,
2903
+    ]);
2904
+    this.setState({
2905
+      draggingElement: element,
2906
+      editingElement: element,
2907
+      startBoundElement: boundElement,
2908
+      suggestedBindings: [],
2909
+    });
2910
+  };
2911
+
2848
   private handleLinearElementOnPointerDown = (
2912
   private handleLinearElementOnPointerDown = (
2849
     event: React.PointerEvent<HTMLCanvasElement>,
2913
     event: React.PointerEvent<HTMLCanvasElement>,
2850
     elementType: ExcalidrawLinearElement["type"],
2914
     elementType: ExcalidrawLinearElement["type"],
2899
       const [gridX, gridY] = getGridPoint(
2963
       const [gridX, gridY] = getGridPoint(
2900
         pointerDownState.origin.x,
2964
         pointerDownState.origin.x,
2901
         pointerDownState.origin.y,
2965
         pointerDownState.origin.y,
2902
-        elementType === "draw" ? null : this.state.gridSize,
2966
+        this.state.gridSize,
2903
       );
2967
       );
2904
 
2968
 
2905
       /* If arrow is pre-arrowheads, it will have undefined for both start and end arrowheads.
2969
       /* If arrow is pre-arrowheads, it will have undefined for both start and end arrowheads.
3107
       const hasHitASelectedElement = pointerDownState.hit.allHitElements.some(
3171
       const hasHitASelectedElement = pointerDownState.hit.allHitElements.some(
3108
         (element) => this.isASelectedElement(element),
3172
         (element) => this.isASelectedElement(element),
3109
       );
3173
       );
3174
+
3110
       if (
3175
       if (
3111
         hasHitASelectedElement ||
3176
         hasHitASelectedElement ||
3112
         pointerDownState.hit.hasHitCommonBoundingBoxOfSelectedElements
3177
         pointerDownState.hit.hasHitCommonBoundingBoxOfSelectedElements
3207
         return;
3272
         return;
3208
       }
3273
       }
3209
 
3274
 
3210
-      if (isLinearElement(draggingElement)) {
3275
+      if (draggingElement.type === "freedraw") {
3276
+        const points = draggingElement.points;
3277
+        const dx = pointerCoords.x - draggingElement.x;
3278
+        const dy = pointerCoords.y - draggingElement.y;
3279
+
3280
+        const pressures = draggingElement.simulatePressure
3281
+          ? draggingElement.pressures
3282
+          : [...draggingElement.pressures, event.pressure];
3283
+
3284
+        mutateElement(draggingElement, {
3285
+          points: [...points, [dx, dy]],
3286
+          pressures,
3287
+        });
3288
+      } else if (isLinearElement(draggingElement)) {
3211
         pointerDownState.drag.hasOccurred = true;
3289
         pointerDownState.drag.hasOccurred = true;
3212
         const points = draggingElement.points;
3290
         const points = draggingElement.points;
3213
-        let dx: number;
3214
-        let dy: number;
3215
-        if (draggingElement.type === "draw") {
3216
-          dx = pointerCoords.x - draggingElement.x;
3217
-          dy = pointerCoords.y - draggingElement.y;
3218
-        } else {
3219
-          dx = gridX - draggingElement.x;
3220
-          dy = gridY - draggingElement.y;
3221
-        }
3291
+        let dx = gridX - draggingElement.x;
3292
+        let dy = gridY - draggingElement.y;
3222
 
3293
 
3223
         if (getRotateWithDiscreteAngleKey(event) && points.length === 2) {
3294
         if (getRotateWithDiscreteAngleKey(event) && points.length === 2) {
3224
           ({ width: dx, height: dy } = getPerfectElementSize(
3295
           ({ width: dx, height: dy } = getPerfectElementSize(
3231
         if (points.length === 1) {
3302
         if (points.length === 1) {
3232
           mutateElement(draggingElement, { points: [...points, [dx, dy]] });
3303
           mutateElement(draggingElement, { points: [...points, [dx, dy]] });
3233
         } else if (points.length > 1) {
3304
         } else if (points.length > 1) {
3234
-          if (draggingElement.type === "draw") {
3235
-            mutateElement(draggingElement, {
3236
-              points: simplify(
3237
-                [...(points as Point[]), [dx, dy]],
3238
-                0.7 / this.state.zoom.value,
3239
-              ),
3240
-            });
3241
-          } else {
3242
-            mutateElement(draggingElement, {
3243
-              points: [...points.slice(0, -1), [dx, dy]],
3244
-            });
3245
-          }
3305
+          mutateElement(draggingElement, {
3306
+            points: [...points.slice(0, -1), [dx, dy]],
3307
+          });
3246
         }
3308
         }
3309
+
3247
         if (isBindingElement(draggingElement)) {
3310
         if (isBindingElement(draggingElement)) {
3248
           // When creating a linear element by dragging
3311
           // When creating a linear element by dragging
3249
           this.maybeSuggestBindingForLinearElementAtCursor(
3312
           this.maybeSuggestBindingForLinearElementAtCursor(
3383
         pointerDownState.eventListeners.onKeyUp!,
3446
         pointerDownState.eventListeners.onKeyUp!,
3384
       );
3447
       );
3385
 
3448
 
3386
-      if (draggingElement?.type === "draw") {
3449
+      if (draggingElement?.type === "freedraw") {
3450
+        const pointerCoords = viewportCoordsToSceneCoords(
3451
+          childEvent,
3452
+          this.state,
3453
+        );
3454
+
3455
+        const points = draggingElement.points;
3456
+        let dx = pointerCoords.x - draggingElement.x;
3457
+        let dy = pointerCoords.y - draggingElement.y;
3458
+
3459
+        // Allows dots to avoid being flagged as infinitely small
3460
+        if (dx === points[0][0] && dy === points[0][1]) {
3461
+          dy += 0.0001;
3462
+          dx += 0.0001;
3463
+        }
3464
+
3465
+        const pressures = draggingElement.simulatePressure
3466
+          ? []
3467
+          : [...draggingElement.pressures, childEvent.pressure];
3468
+
3469
+        mutateElement(draggingElement, {
3470
+          points: [...points, [dx, dy]],
3471
+          pressures,
3472
+        });
3473
+
3387
         this.actionManager.executeAction(actionFinalize);
3474
         this.actionManager.executeAction(actionFinalize);
3475
+
3388
         return;
3476
         return;
3389
       }
3477
       }
3390
 
3478
 
3428
             );
3516
             );
3429
           }
3517
           }
3430
           this.setState({ suggestedBindings: [], startBoundElement: null });
3518
           this.setState({ suggestedBindings: [], startBoundElement: null });
3431
-          if (!elementLocked && elementType !== "draw") {
3519
+          if (!elementLocked) {
3432
             resetCursor(this.canvas);
3520
             resetCursor(this.canvas);
3433
             this.setState((prevState) => ({
3521
             this.setState((prevState) => ({
3434
               draggingElement: null,
3522
               draggingElement: null,
3575
         return;
3663
         return;
3576
       }
3664
       }
3577
 
3665
 
3578
-      if (!elementLocked && elementType !== "draw" && draggingElement) {
3666
+      if (!elementLocked && elementType !== "freedraw" && draggingElement) {
3579
         this.setState((prevState) => ({
3667
         this.setState((prevState) => ({
3580
           selectedElementIds: {
3668
           selectedElementIds: {
3581
             ...prevState.selectedElementIds,
3669
             ...prevState.selectedElementIds,
3599
         );
3687
         );
3600
       }
3688
       }
3601
 
3689
 
3602
-      if (!elementLocked && elementType !== "draw") {
3690
+      if (!elementLocked && elementType !== "freedraw") {
3603
         resetCursor(this.canvas);
3691
         resetCursor(this.canvas);
3604
         this.setState({
3692
         this.setState({
3605
           draggingElement: null,
3693
           draggingElement: null,

+ 1
- 1
src/components/HelpDialog.tsx Прегледај датотеку

153
                 <Shortcut label={t("toolBar.arrow")} shortcuts={["A", "5"]} />
153
                 <Shortcut label={t("toolBar.arrow")} shortcuts={["A", "5"]} />
154
                 <Shortcut label={t("toolBar.line")} shortcuts={["P", "6"]} />
154
                 <Shortcut label={t("toolBar.line")} shortcuts={["P", "6"]} />
155
                 <Shortcut
155
                 <Shortcut
156
-                  label={t("toolBar.draw")}
156
+                  label={t("toolBar.freedraw")}
157
                   shortcuts={["Shift+P", "7"]}
157
                   shortcuts={["Shift+P", "7"]}
158
                 />
158
                 />
159
                 <Shortcut label={t("toolBar.text")} shortcuts={["T", "8"]} />
159
                 <Shortcut label={t("toolBar.text")} shortcuts={["T", "8"]} />

+ 1
- 1
src/components/HintViewer.tsx Прегледај датотеку

23
     return t("hints.linearElementMulti");
23
     return t("hints.linearElementMulti");
24
   }
24
   }
25
 
25
 
26
-  if (elementType === "draw") {
26
+  if (elementType === "freedraw") {
27
     return t("hints.freeDraw");
27
     return t("hints.freeDraw");
28
   }
28
   }
29
 
29
 

+ 13
- 2
src/data/restore.ts Прегледај датотеку

37
 
37
 
38
 const restoreElementWithProperties = <T extends ExcalidrawElement>(
38
 const restoreElementWithProperties = <T extends ExcalidrawElement>(
39
   element: Required<T>,
39
   element: Required<T>,
40
-  extra: Omit<Required<T>, keyof ExcalidrawElement>,
40
+  extra: Omit<Required<T>, keyof ExcalidrawElement> & {
41
+    type?: ExcalidrawElement["type"];
42
+  },
41
 ): T => {
43
 ): T => {
42
   const base: Pick<T, keyof ExcalidrawElement> = {
44
   const base: Pick<T, keyof ExcalidrawElement> = {
43
-    type: element.type,
45
+    type: extra.type || element.type,
44
     // all elements must have version > 0 so getSceneVersion() will pick up
46
     // all elements must have version > 0 so getSceneVersion() will pick up
45
     // newly added elements
47
     // newly added elements
46
     version: element.version || 1,
48
     version: element.version || 1,
97
         textAlign: element.textAlign || DEFAULT_TEXT_ALIGN,
99
         textAlign: element.textAlign || DEFAULT_TEXT_ALIGN,
98
         verticalAlign: element.verticalAlign || DEFAULT_VERTICAL_ALIGN,
100
         verticalAlign: element.verticalAlign || DEFAULT_VERTICAL_ALIGN,
99
       });
101
       });
102
+    case "freedraw": {
103
+      return restoreElementWithProperties(element, {
104
+        points: element.points,
105
+        lastCommittedPoint: null,
106
+        simulatePressure: element.simulatePressure,
107
+        pressures: element.pressures,
108
+      });
109
+    }
100
     case "draw":
110
     case "draw":
101
     case "line":
111
     case "line":
102
     case "arrow": {
112
     case "arrow": {
106
       } = element;
116
       } = element;
107
 
117
 
108
       return restoreElementWithProperties(element, {
118
       return restoreElementWithProperties(element, {
119
+        type: element.type === "draw" ? "line" : element.type,
109
         startBinding: element.startBinding,
120
         startBinding: element.startBinding,
110
         endBinding: element.endBinding,
121
         endBinding: element.endBinding,
111
         points:
122
         points:

+ 116
- 44
src/element/bounds.ts Прегледај датотеку

1
-import { ExcalidrawElement, ExcalidrawLinearElement, Arrowhead } from "./types";
1
+import {
2
+  ExcalidrawElement,
3
+  ExcalidrawLinearElement,
4
+  Arrowhead,
5
+  ExcalidrawFreeDrawElement,
6
+} from "./types";
2
 import { distance2d, rotate } from "../math";
7
 import { distance2d, rotate } from "../math";
3
 import rough from "roughjs/bin/rough";
8
 import rough from "roughjs/bin/rough";
4
 import { Drawable, Op } from "roughjs/bin/core";
9
 import { Drawable, Op } from "roughjs/bin/core";
7
   getShapeForElement,
12
   getShapeForElement,
8
   generateRoughOptions,
13
   generateRoughOptions,
9
 } from "../renderer/renderElement";
14
 } from "../renderer/renderElement";
10
-import { isLinearElement } from "./typeChecks";
15
+import { isFreeDrawElement, isLinearElement } from "./typeChecks";
11
 import { rescalePoints } from "../points";
16
 import { rescalePoints } from "../points";
12
 
17
 
13
 // x and y position of top left corner, x and y position of bottom right corner
18
 // x and y position of top left corner, x and y position of bottom right corner
18
 export const getElementAbsoluteCoords = (
23
 export const getElementAbsoluteCoords = (
19
   element: ExcalidrawElement,
24
   element: ExcalidrawElement,
20
 ): Bounds => {
25
 ): Bounds => {
21
-  if (isLinearElement(element)) {
26
+  if (isFreeDrawElement(element)) {
27
+    return getFreeDrawElementAbsoluteCoords(element);
28
+  } else if (isLinearElement(element)) {
22
     return getLinearElementAbsoluteCoords(element);
29
     return getLinearElementAbsoluteCoords(element);
23
   }
30
   }
24
   return [
31
   return [
120
   return [minX, minY, maxX, maxY];
127
   return [minX, minY, maxX, maxY];
121
 };
128
 };
122
 
129
 
130
+const getBoundsFromPoints = (
131
+  points: ExcalidrawFreeDrawElement["points"],
132
+): [number, number, number, number] => {
133
+  let minX = Infinity;
134
+  let minY = Infinity;
135
+  let maxX = -Infinity;
136
+  let maxY = -Infinity;
137
+
138
+  for (const [x, y] of points) {
139
+    minX = Math.min(minX, x);
140
+    minY = Math.min(minY, y);
141
+    maxX = Math.max(maxX, x);
142
+    maxY = Math.max(maxY, y);
143
+  }
144
+
145
+  return [minX, minY, maxX, maxY];
146
+};
147
+
148
+const getFreeDrawElementAbsoluteCoords = (
149
+  element: ExcalidrawFreeDrawElement,
150
+): [number, number, number, number] => {
151
+  const [minX, minY, maxX, maxY] = getBoundsFromPoints(element.points);
152
+
153
+  return [
154
+    minX + element.x,
155
+    minY + element.y,
156
+    maxX + element.x,
157
+    maxY + element.y,
158
+  ];
159
+};
160
+
123
 const getLinearElementAbsoluteCoords = (
161
 const getLinearElementAbsoluteCoords = (
124
   element: ExcalidrawLinearElement,
162
   element: ExcalidrawLinearElement,
125
 ): [number, number, number, number] => {
163
 ): [number, number, number, number] => {
164
+  let coords: [number, number, number, number];
165
+
126
   if (element.points.length < 2 || !getShapeForElement(element)) {
166
   if (element.points.length < 2 || !getShapeForElement(element)) {
127
     // XXX this is just a poor estimate and not very useful
167
     // XXX this is just a poor estimate and not very useful
128
     const { minX, minY, maxX, maxY } = element.points.reduce(
168
     const { minX, minY, maxX, maxY } = element.points.reduce(
137
       },
177
       },
138
       { minX: Infinity, minY: Infinity, maxX: -Infinity, maxY: -Infinity },
178
       { minX: Infinity, minY: Infinity, maxX: -Infinity, maxY: -Infinity },
139
     );
179
     );
140
-    return [
180
+    coords = [
141
       minX + element.x,
181
       minX + element.x,
142
       minY + element.y,
182
       minY + element.y,
143
       maxX + element.x,
183
       maxX + element.x,
144
       maxY + element.y,
184
       maxY + element.y,
145
     ];
185
     ];
146
-  }
186
+  } else {
187
+    const shape = getShapeForElement(element) as Drawable[];
147
 
188
 
148
-  const shape = getShapeForElement(element) as Drawable[];
189
+    // first element is always the curve
190
+    const ops = getCurvePathOps(shape[0]);
149
 
191
 
150
-  // first element is always the curve
151
-  const ops = getCurvePathOps(shape[0]);
192
+    const [minX, minY, maxX, maxY] = getMinMaxXYFromCurvePathOps(ops);
152
 
193
 
153
-  const [minX, minY, maxX, maxY] = getMinMaxXYFromCurvePathOps(ops);
194
+    coords = [
195
+      minX + element.x,
196
+      minY + element.y,
197
+      maxX + element.x,
198
+      maxY + element.y,
199
+    ];
200
+  }
154
 
201
 
155
-  return [
156
-    minX + element.x,
157
-    minY + element.y,
158
-    maxX + element.x,
159
-    maxY + element.y,
160
-  ];
202
+  return coords;
161
 };
203
 };
162
 
204
 
163
 export const getArrowheadPoints = (
205
 export const getArrowheadPoints = (
231
   const ys = y2 - ny * minSize;
273
   const ys = y2 - ny * minSize;
232
 
274
 
233
   if (arrowhead === "dot") {
275
   if (arrowhead === "dot") {
234
-    const r = Math.hypot(ys - y2, xs - x2);
276
+    const r = Math.hypot(ys - y2, xs - x2) + element.strokeWidth;
235
     return [x2, y2, r];
277
     return [x2, y2, r];
236
   }
278
   }
237
 
279
 
277
   return getMinMaxXYFromCurvePathOps(ops, transformXY);
319
   return getMinMaxXYFromCurvePathOps(ops, transformXY);
278
 };
320
 };
279
 
321
 
322
+// We could cache this stuff
280
 export const getElementBounds = (
323
 export const getElementBounds = (
281
   element: ExcalidrawElement,
324
   element: ExcalidrawElement,
282
 ): [number, number, number, number] => {
325
 ): [number, number, number, number] => {
326
+  let bounds: [number, number, number, number];
327
+
283
   const [x1, y1, x2, y2] = getElementAbsoluteCoords(element);
328
   const [x1, y1, x2, y2] = getElementAbsoluteCoords(element);
284
   const cx = (x1 + x2) / 2;
329
   const cx = (x1 + x2) / 2;
285
   const cy = (y1 + y2) / 2;
330
   const cy = (y1 + y2) / 2;
286
-  if (isLinearElement(element)) {
287
-    return getLinearElementRotatedBounds(element, cx, cy);
288
-  }
289
-  if (element.type === "diamond") {
331
+  if (isFreeDrawElement(element)) {
332
+    const [minX, minY, maxX, maxY] = getBoundsFromPoints(
333
+      element.points.map(([x, y]) =>
334
+        rotate(x, y, cx - element.x, cy - element.y, element.angle),
335
+      ),
336
+    );
337
+
338
+    return [
339
+      minX + element.x,
340
+      minY + element.y,
341
+      maxX + element.x,
342
+      maxY + element.y,
343
+    ];
344
+  } else if (isLinearElement(element)) {
345
+    bounds = getLinearElementRotatedBounds(element, cx, cy);
346
+  } else if (element.type === "diamond") {
290
     const [x11, y11] = rotate(cx, y1, cx, cy, element.angle);
347
     const [x11, y11] = rotate(cx, y1, cx, cy, element.angle);
291
     const [x12, y12] = rotate(cx, y2, cx, cy, element.angle);
348
     const [x12, y12] = rotate(cx, y2, cx, cy, element.angle);
292
     const [x22, y22] = rotate(x1, cy, cx, cy, element.angle);
349
     const [x22, y22] = rotate(x1, cy, cx, cy, element.angle);
295
     const minY = Math.min(y11, y12, y22, y21);
352
     const minY = Math.min(y11, y12, y22, y21);
296
     const maxX = Math.max(x11, x12, x22, x21);
353
     const maxX = Math.max(x11, x12, x22, x21);
297
     const maxY = Math.max(y11, y12, y22, y21);
354
     const maxY = Math.max(y11, y12, y22, y21);
298
-    return [minX, minY, maxX, maxY];
299
-  }
300
-  if (element.type === "ellipse") {
355
+    bounds = [minX, minY, maxX, maxY];
356
+  } else if (element.type === "ellipse") {
301
     const w = (x2 - x1) / 2;
357
     const w = (x2 - x1) / 2;
302
     const h = (y2 - y1) / 2;
358
     const h = (y2 - y1) / 2;
303
     const cos = Math.cos(element.angle);
359
     const cos = Math.cos(element.angle);
304
     const sin = Math.sin(element.angle);
360
     const sin = Math.sin(element.angle);
305
     const ww = Math.hypot(w * cos, h * sin);
361
     const ww = Math.hypot(w * cos, h * sin);
306
     const hh = Math.hypot(h * cos, w * sin);
362
     const hh = Math.hypot(h * cos, w * sin);
307
-    return [cx - ww, cy - hh, cx + ww, cy + hh];
363
+    bounds = [cx - ww, cy - hh, cx + ww, cy + hh];
364
+  } else {
365
+    const [x11, y11] = rotate(x1, y1, cx, cy, element.angle);
366
+    const [x12, y12] = rotate(x1, y2, cx, cy, element.angle);
367
+    const [x22, y22] = rotate(x2, y2, cx, cy, element.angle);
368
+    const [x21, y21] = rotate(x2, y1, cx, cy, element.angle);
369
+    const minX = Math.min(x11, x12, x22, x21);
370
+    const minY = Math.min(y11, y12, y22, y21);
371
+    const maxX = Math.max(x11, x12, x22, x21);
372
+    const maxY = Math.max(y11, y12, y22, y21);
373
+    bounds = [minX, minY, maxX, maxY];
308
   }
374
   }
309
-  const [x11, y11] = rotate(x1, y1, cx, cy, element.angle);
310
-  const [x12, y12] = rotate(x1, y2, cx, cy, element.angle);
311
-  const [x22, y22] = rotate(x2, y2, cx, cy, element.angle);
312
-  const [x21, y21] = rotate(x2, y1, cx, cy, element.angle);
313
-  const minX = Math.min(x11, x12, x22, x21);
314
-  const minY = Math.min(y11, y12, y22, y21);
315
-  const maxX = Math.max(x11, x12, x22, x21);
316
-  const maxY = Math.max(y11, y12, y22, y21);
317
-  return [minX, minY, maxX, maxY];
375
+
376
+  return bounds;
318
 };
377
 };
319
 
378
 
320
 export const getCommonBounds = (
379
 export const getCommonBounds = (
345
   nextWidth: number,
404
   nextWidth: number,
346
   nextHeight: number,
405
   nextHeight: number,
347
 ): [number, number, number, number] => {
406
 ): [number, number, number, number] => {
348
-  if (!isLinearElement(element)) {
407
+  if (!(isLinearElement(element) || isFreeDrawElement(element))) {
349
     return [
408
     return [
350
       element.x,
409
       element.x,
351
       element.y,
410
       element.y,
360
     rescalePoints(1, nextHeight, element.points),
419
     rescalePoints(1, nextHeight, element.points),
361
   );
420
   );
362
 
421
 
363
-  const gen = rough.generator();
364
-  const curve =
365
-    element.strokeSharpness === "sharp"
366
-      ? gen.linearPath(
367
-          points as [number, number][],
368
-          generateRoughOptions(element),
369
-        )
370
-      : gen.curve(points as [number, number][], generateRoughOptions(element));
371
-  const ops = getCurvePathOps(curve);
372
-  const [minX, minY, maxX, maxY] = getMinMaxXYFromCurvePathOps(ops);
422
+  let bounds: [number, number, number, number];
423
+
424
+  if (isFreeDrawElement(element)) {
425
+    // Free Draw
426
+    bounds = getBoundsFromPoints(points);
427
+  } else {
428
+    // Line
429
+    const gen = rough.generator();
430
+    const curve =
431
+      element.strokeSharpness === "sharp"
432
+        ? gen.linearPath(
433
+            points as [number, number][],
434
+            generateRoughOptions(element),
435
+          )
436
+        : gen.curve(
437
+            points as [number, number][],
438
+            generateRoughOptions(element),
439
+          );
440
+    const ops = getCurvePathOps(curve);
441
+    bounds = getMinMaxXYFromCurvePathOps(ops);
442
+  }
443
+
444
+  const [minX, minY, maxX, maxY] = bounds;
373
   return [
445
   return [
374
     minX + element.x,
446
     minX + element.x,
375
     minY + element.y,
447
     minY + element.y,

+ 98
- 3
src/element/collision.ts Прегледај датотеку

4
 import * as GALine from "../galines";
4
 import * as GALine from "../galines";
5
 import * as GATransform from "../gatransforms";
5
 import * as GATransform from "../gatransforms";
6
 
6
 
7
-import { isPathALoop, isPointInPolygon, rotate } from "../math";
7
+import {
8
+  distance2d,
9
+  rotatePoint,
10
+  isPathALoop,
11
+  isPointInPolygon,
12
+  rotate,
13
+} from "../math";
8
 import { pointsOnBezierCurves } from "points-on-curve";
14
 import { pointsOnBezierCurves } from "points-on-curve";
9
 
15
 
10
 import {
16
 import {
16
   ExcalidrawTextElement,
22
   ExcalidrawTextElement,
17
   ExcalidrawEllipseElement,
23
   ExcalidrawEllipseElement,
18
   NonDeleted,
24
   NonDeleted,
25
+  ExcalidrawFreeDrawElement,
19
 } from "./types";
26
 } from "./types";
20
 
27
 
21
 import { getElementAbsoluteCoords, getCurvePathOps, Bounds } from "./bounds";
28
 import { getElementAbsoluteCoords, getCurvePathOps, Bounds } from "./bounds";
30
   if (element.type === "arrow") {
37
   if (element.type === "arrow") {
31
     return false;
38
     return false;
32
   }
39
   }
40
+
41
+  if (element.type === "freedraw") {
42
+    return true;
43
+  }
44
+
33
   const isDraggableFromInside = element.backgroundColor !== "transparent";
45
   const isDraggableFromInside = element.backgroundColor !== "transparent";
34
-  if (element.type === "line" || element.type === "draw") {
46
+
47
+  if (element.type === "line") {
35
     return isDraggableFromInside && isPathALoop(element.points);
48
     return isDraggableFromInside && isPathALoop(element.points);
36
   }
49
   }
50
+
37
   return isDraggableFromInside;
51
   return isDraggableFromInside;
38
 };
52
 };
39
 
53
 
81
       : isElementDraggableFromInside(element)
95
       : isElementDraggableFromInside(element)
82
       ? isInsideCheck
96
       ? isInsideCheck
83
       : isNearCheck;
97
       : isNearCheck;
98
+
84
   return hitTestPointAgainstElement({ element, point, threshold, check });
99
   return hitTestPointAgainstElement({ element, point, threshold, check });
85
 };
100
 };
86
 
101
 
151
     case "ellipse":
166
     case "ellipse":
152
       const distance = distanceToBindableElement(args.element, args.point);
167
       const distance = distanceToBindableElement(args.element, args.point);
153
       return args.check(distance, args.threshold);
168
       return args.check(distance, args.threshold);
169
+    case "freedraw": {
170
+      if (
171
+        !args.check(
172
+          distanceToRectangle(args.element, args.point),
173
+          args.threshold,
174
+        )
175
+      ) {
176
+        return false;
177
+      }
178
+
179
+      return hitTestFreeDrawElement(args.element, args.point, args.threshold);
180
+    }
154
     case "arrow":
181
     case "arrow":
155
     case "line":
182
     case "line":
156
     case "draw":
183
     case "draw":
195
 };
222
 };
196
 
223
 
197
 const distanceToRectangle = (
224
 const distanceToRectangle = (
198
-  element: ExcalidrawRectangleElement | ExcalidrawTextElement,
225
+  element:
226
+    | ExcalidrawRectangleElement
227
+    | ExcalidrawTextElement
228
+    | ExcalidrawFreeDrawElement,
199
   point: Point,
229
   point: Point,
200
 ): number => {
230
 ): number => {
201
   const [, pointRel, hwidth, hheight] = pointRelativeToElement(element, point);
231
   const [, pointRel, hwidth, hheight] = pointRelativeToElement(element, point);
267
   return [pointRel, tangent];
297
   return [pointRel, tangent];
268
 };
298
 };
269
 
299
 
300
+const hitTestFreeDrawElement = (
301
+  element: ExcalidrawFreeDrawElement,
302
+  point: Point,
303
+  threshold: number,
304
+): boolean => {
305
+  // Check point-distance-to-line-segment for every segment in the
306
+  // element's points (its input points, not its outline points).
307
+  // This is... okay? It's plenty fast, but the GA library may
308
+  // have a faster option.
309
+
310
+  let x: number;
311
+  let y: number;
312
+
313
+  if (element.angle === 0) {
314
+    x = point[0] - element.x;
315
+    y = point[1] - element.y;
316
+  } else {
317
+    // Counter-rotate the point around center before testing
318
+    const [minX, minY, maxX, maxY] = getElementAbsoluteCoords(element);
319
+    const rotatedPoint = rotatePoint(
320
+      point,
321
+      [minX + (maxX - minX) / 2, minY + (maxY - minY) / 2],
322
+      -element.angle,
323
+    );
324
+    x = rotatedPoint[0] - element.x;
325
+    y = rotatedPoint[1] - element.y;
326
+  }
327
+
328
+  let [A, B] = element.points;
329
+  let P: readonly [number, number];
330
+
331
+  // For freedraw dots
332
+  if (element.points.length === 2) {
333
+    return (
334
+      distance2d(A[0], A[1], x, y) < threshold ||
335
+      distance2d(B[0], B[1], x, y) < threshold
336
+    );
337
+  }
338
+
339
+  // For freedraw lines
340
+  for (let i = 1; i < element.points.length - 1; i++) {
341
+    const delta = [B[0] - A[0], B[1] - A[1]];
342
+    const length = Math.hypot(delta[1], delta[0]);
343
+
344
+    const U = [delta[0] / length, delta[1] / length];
345
+    const C = [x - A[0], y - A[1]];
346
+    const d = (C[0] * U[0] + C[1] * U[1]) / Math.hypot(U[1], U[0]);
347
+    P = [A[0] + U[0] * d, A[1] + U[1] * d];
348
+
349
+    const da = distance2d(P[0], P[1], A[0], A[1]);
350
+    const db = distance2d(P[0], P[1], B[0], B[1]);
351
+
352
+    P = db < da && da > length ? B : da < db && db > length ? A : P;
353
+
354
+    if (Math.hypot(y - P[1], x - P[0]) < threshold) {
355
+      return true;
356
+    }
357
+
358
+    A = B;
359
+    B = element.points[i + 1];
360
+  }
361
+
362
+  return false;
363
+};
364
+
270
 const hitTestLinear = (args: HitTestArgs): boolean => {
365
 const hitTestLinear = (args: HitTestArgs): boolean => {
271
   const { element, threshold } = args;
366
   const { element, threshold } = args;
272
   if (!getShapeForElement(element)) {
367
   if (!getShapeForElement(element)) {

+ 17
- 0
src/element/newElement.ts Прегледај датотеку

9
   GroupId,
9
   GroupId,
10
   VerticalAlign,
10
   VerticalAlign,
11
   Arrowhead,
11
   Arrowhead,
12
+  ExcalidrawFreeDrawElement,
12
 } from "../element/types";
13
 } from "../element/types";
13
 import { measureText, getFontString } from "../utils";
14
 import { measureText, getFontString } from "../utils";
14
 import { randomInteger, randomId } from "../random";
15
 import { randomInteger, randomId } from "../random";
212
   });
213
   });
213
 };
214
 };
214
 
215
 
216
+export const newFreeDrawElement = (
217
+  opts: {
218
+    type: "freedraw";
219
+    points?: ExcalidrawFreeDrawElement["points"];
220
+    simulatePressure: boolean;
221
+  } & ElementConstructorOpts,
222
+): NonDeleted<ExcalidrawFreeDrawElement> => {
223
+  return {
224
+    ..._newElementBase<ExcalidrawFreeDrawElement>(opts.type, opts),
225
+    points: opts.points || [],
226
+    pressures: [],
227
+    simulatePressure: opts.simulatePressure,
228
+    lastCommittedPoint: null,
229
+  };
230
+};
231
+
215
 export const newLinearElement = (
232
 export const newLinearElement = (
216
   opts: {
233
   opts: {
217
     type: ExcalidrawLinearElement["type"];
234
     type: ExcalidrawLinearElement["type"];

+ 11
- 3
src/element/resizeElements.ts Прегледај датотеку

18
   getCommonBounds,
18
   getCommonBounds,
19
   getResizedElementAbsoluteCoords,
19
   getResizedElementAbsoluteCoords,
20
 } from "./bounds";
20
 } from "./bounds";
21
-import { isLinearElement, isTextElement } from "./typeChecks";
21
+import {
22
+  isFreeDrawElement,
23
+  isLinearElement,
24
+  isTextElement,
25
+} from "./typeChecks";
22
 import { mutateElement } from "./mutateElement";
26
 import { mutateElement } from "./mutateElement";
23
 import { getPerfectElementSize } from "./sizeHelpers";
27
 import { getPerfectElementSize } from "./sizeHelpers";
24
 import { measureText, getFontString } from "../utils";
28
 import { measureText, getFontString } from "../utils";
244
   width: number,
248
   width: number,
245
   height: number,
249
   height: number,
246
 ) =>
250
 ) =>
247
-  isLinearElement(element)
251
+  isLinearElement(element) || isFreeDrawElement(element)
248
     ? {
252
     ? {
249
         points: rescalePoints(
253
         points: rescalePoints(
250
           0,
254
           0,
404
     -stateAtResizeStart.angle,
408
     -stateAtResizeStart.angle,
405
   );
409
   );
406
 
410
 
407
-  //Get bounds corners rendered on screen
411
+  // Get bounds corners rendered on screen
408
   const [esx1, esy1, esx2, esy2] = getResizedElementAbsoluteCoords(
412
   const [esx1, esy1, esx2, esy2] = getResizedElementAbsoluteCoords(
409
     element,
413
     element,
410
     element.width,
414
     element.width,
644
           font = { fontSize: nextFont.size, baseline: nextFont.baseline };
648
           font = { fontSize: nextFont.size, baseline: nextFont.baseline };
645
         }
649
         }
646
         const origCoords = getElementAbsoluteCoords(element);
650
         const origCoords = getElementAbsoluteCoords(element);
651
+
647
         const rescaledPoints = rescalePointsInElement(element, width, height);
652
         const rescaledPoints = rescalePointsInElement(element, width, height);
653
+
648
         updateBoundElements(element, {
654
         updateBoundElements(element, {
649
           newSize: { width, height },
655
           newSize: { width, height },
650
           simultaneouslyUpdated: elements,
656
           simultaneouslyUpdated: elements,
651
         });
657
         });
658
+
652
         const finalCoords = getResizedElementAbsoluteCoords(
659
         const finalCoords = getResizedElementAbsoluteCoords(
653
           {
660
           {
654
             ...element,
661
             ...element,
657
           width,
664
           width,
658
           height,
665
           height,
659
         );
666
         );
667
+
660
         const { x, y } = getNextXY(element, origCoords, finalCoords);
668
         const { x, y } = getNextXY(element, origCoords, finalCoords);
661
         return [...prev, { width, height, x, y, ...rescaledPoints, ...font }];
669
         return [...prev, { width, height, x, y, ...rescaledPoints, ...font }];
662
       },
670
       },

+ 3
- 3
src/element/sizeHelpers.ts Прегледај датотеку

1
 import { ExcalidrawElement } from "./types";
1
 import { ExcalidrawElement } from "./types";
2
 import { mutateElement } from "./mutateElement";
2
 import { mutateElement } from "./mutateElement";
3
-import { isLinearElement } from "./typeChecks";
3
+import { isFreeDrawElement, isLinearElement } from "./typeChecks";
4
 import { SHIFT_LOCKING_ANGLE } from "../constants";
4
 import { SHIFT_LOCKING_ANGLE } from "../constants";
5
 
5
 
6
 export const isInvisiblySmallElement = (
6
 export const isInvisiblySmallElement = (
7
   element: ExcalidrawElement,
7
   element: ExcalidrawElement,
8
 ): boolean => {
8
 ): boolean => {
9
-  if (isLinearElement(element)) {
9
+  if (isLinearElement(element) || isFreeDrawElement(element)) {
10
     return element.points.length < 2;
10
     return element.points.length < 2;
11
   }
11
   }
12
   return element.width === 0 && element.height === 0;
12
   return element.width === 0 && element.height === 0;
26
   if (
26
   if (
27
     elementType === "line" ||
27
     elementType === "line" ||
28
     elementType === "arrow" ||
28
     elementType === "arrow" ||
29
-    elementType === "draw"
29
+    elementType === "freedraw"
30
   ) {
30
   ) {
31
     const lockedAngle =
31
     const lockedAngle =
32
       Math.round(Math.atan(absHeight / absWidth) / SHIFT_LOCKING_ANGLE) *
32
       Math.round(Math.atan(absHeight / absWidth) / SHIFT_LOCKING_ANGLE) *

+ 1
- 1
src/element/transformHandles.ts Прегледај датотеку

225
   if (
225
   if (
226
     element.type === "arrow" ||
226
     element.type === "arrow" ||
227
     element.type === "line" ||
227
     element.type === "line" ||
228
-    element.type === "draw"
228
+    element.type === "freedraw"
229
   ) {
229
   ) {
230
     if (element.points.length === 2) {
230
     if (element.points.length === 2) {
231
       // only check the last point because starting point is always (0,0)
231
       // only check the last point because starting point is always (0,0)

+ 15
- 2
src/element/typeChecks.ts Прегледај датотеку

4
   ExcalidrawLinearElement,
4
   ExcalidrawLinearElement,
5
   ExcalidrawBindableElement,
5
   ExcalidrawBindableElement,
6
   ExcalidrawGenericElement,
6
   ExcalidrawGenericElement,
7
+  ExcalidrawFreeDrawElement,
7
 } from "./types";
8
 } from "./types";
8
 
9
 
9
 export const isGenericElement = (
10
 export const isGenericElement = (
24
   return element != null && element.type === "text";
25
   return element != null && element.type === "text";
25
 };
26
 };
26
 
27
 
28
+export const isFreeDrawElement = (
29
+  element?: ExcalidrawElement | null,
30
+): element is ExcalidrawFreeDrawElement => {
31
+  return element != null && isFreeDrawElementType(element.type);
32
+};
33
+
34
+export const isFreeDrawElementType = (
35
+  elementType: ExcalidrawElement["type"],
36
+): boolean => {
37
+  return elementType === "freedraw";
38
+};
39
+
27
 export const isLinearElement = (
40
 export const isLinearElement = (
28
   element?: ExcalidrawElement | null,
41
   element?: ExcalidrawElement | null,
29
 ): element is ExcalidrawLinearElement => {
42
 ): element is ExcalidrawLinearElement => {
34
   elementType: ExcalidrawElement["type"],
47
   elementType: ExcalidrawElement["type"],
35
 ): boolean => {
48
 ): boolean => {
36
   return (
49
   return (
37
-    elementType === "arrow" || elementType === "line" || elementType === "draw"
50
+    elementType === "arrow" || elementType === "line" // || elementType === "freedraw"
38
   );
51
   );
39
 };
52
 };
40
 
53
 
69
     element?.type === "rectangle" ||
82
     element?.type === "rectangle" ||
70
     element?.type === "ellipse" ||
83
     element?.type === "ellipse" ||
71
     element?.type === "arrow" ||
84
     element?.type === "arrow" ||
72
-    element?.type === "draw" ||
85
+    element?.type === "freedraw" ||
73
     element?.type === "line"
86
     element?.type === "line"
74
   );
87
   );
75
 };
88
 };

+ 11
- 1
src/element/types.ts Прегледај датотеку

78
 export type ExcalidrawElement =
78
 export type ExcalidrawElement =
79
   | ExcalidrawGenericElement
79
   | ExcalidrawGenericElement
80
   | ExcalidrawTextElement
80
   | ExcalidrawTextElement
81
-  | ExcalidrawLinearElement;
81
+  | ExcalidrawLinearElement
82
+  | ExcalidrawFreeDrawElement;
82
 
83
 
83
 export type NonDeleted<TElement extends ExcalidrawElement> = TElement & {
84
 export type NonDeleted<TElement extends ExcalidrawElement> = TElement & {
84
   isDeleted: false;
85
   isDeleted: false;
121
     startArrowhead: Arrowhead | null;
122
     startArrowhead: Arrowhead | null;
122
     endArrowhead: Arrowhead | null;
123
     endArrowhead: Arrowhead | null;
123
   }>;
124
   }>;
125
+
126
+export type ExcalidrawFreeDrawElement = _ExcalidrawElementBase &
127
+  Readonly<{
128
+    type: "freedraw";
129
+    points: readonly Point[];
130
+    pressures: readonly number[];
131
+    simulatePressure: boolean;
132
+    lastCommittedPoint: Point | null;
133
+  }>;

+ 1
- 1
src/locales/ar-SA.json Прегледај датотеку

152
   },
152
   },
153
   "toolBar": {
153
   "toolBar": {
154
     "selection": "تحديد",
154
     "selection": "تحديد",
155
-    "draw": "الكتابة الحرة",
155
+    "freedraw": "الكتابة الحرة",
156
     "rectangle": "مستطيل",
156
     "rectangle": "مستطيل",
157
     "diamond": "مضلع",
157
     "diamond": "مضلع",
158
     "ellipse": "دائرة",
158
     "ellipse": "دائرة",

+ 1
- 1
src/locales/bg-BG.json Прегледај датотеку

152
   },
152
   },
153
   "toolBar": {
153
   "toolBar": {
154
     "selection": "Селекция",
154
     "selection": "Селекция",
155
-    "draw": "Рисуване",
155
+    "freedraw": "Рисуване",
156
     "rectangle": "Правоъгълник",
156
     "rectangle": "Правоъгълник",
157
     "diamond": "Диамант",
157
     "diamond": "Диамант",
158
     "ellipse": "Елипс",
158
     "ellipse": "Елипс",

+ 1
- 1
src/locales/ca-ES.json Прегледај датотеку

152
   },
152
   },
153
   "toolBar": {
153
   "toolBar": {
154
     "selection": "Selecció",
154
     "selection": "Selecció",
155
-    "draw": "Dibuix lliure",
155
+    "freedraw": "Dibuix lliure",
156
     "rectangle": "Rectangle",
156
     "rectangle": "Rectangle",
157
     "diamond": "Rombe",
157
     "diamond": "Rombe",
158
     "ellipse": "El·lipse",
158
     "ellipse": "El·lipse",

+ 1
- 1
src/locales/de-DE.json Прегледај датотеку

152
   },
152
   },
153
   "toolBar": {
153
   "toolBar": {
154
     "selection": "Auswahl",
154
     "selection": "Auswahl",
155
-    "draw": "Freies Zeichnen",
155
+    "freedraw": "Freies Zeichnen",
156
     "rectangle": "Rechteck",
156
     "rectangle": "Rechteck",
157
     "diamond": "Raute",
157
     "diamond": "Raute",
158
     "ellipse": "Ellipse",
158
     "ellipse": "Ellipse",

+ 1
- 1
src/locales/el-GR.json Прегледај датотеку

152
   },
152
   },
153
   "toolBar": {
153
   "toolBar": {
154
     "selection": "Επιλογή",
154
     "selection": "Επιλογή",
155
-    "draw": "Ελεύθερο σχέδιο",
155
+    "freedraw": "Ελεύθερο σχέδιο",
156
     "rectangle": "Ορθογώνιο",
156
     "rectangle": "Ορθογώνιο",
157
     "diamond": "Ρόμβος",
157
     "diamond": "Ρόμβος",
158
     "ellipse": "Έλλειψη",
158
     "ellipse": "Έλλειψη",

+ 4
- 1
src/locales/en.json Прегледај датотеку

20
     "background": "Background",
20
     "background": "Background",
21
     "fill": "Fill",
21
     "fill": "Fill",
22
     "strokeWidth": "Stroke width",
22
     "strokeWidth": "Stroke width",
23
+    "strokeShape": "Stroke shape",
24
+    "strokeShape_gel": "Gel pen",
25
+    "strokeShape_fountain": "Fountain pen",
26
+    "strokeShape_brush": "Brush pen",
23
     "strokeStyle": "Stroke style",
27
     "strokeStyle": "Stroke style",
24
     "strokeStyle_solid": "Solid",
28
     "strokeStyle_solid": "Solid",
25
     "strokeStyle_dashed": "Dashed",
29
     "strokeStyle_dashed": "Dashed",
153
   },
157
   },
154
   "toolBar": {
158
   "toolBar": {
155
     "selection": "Selection",
159
     "selection": "Selection",
156
-    "draw": "Free draw",
157
     "rectangle": "Rectangle",
160
     "rectangle": "Rectangle",
158
     "diamond": "Diamond",
161
     "diamond": "Diamond",
159
     "ellipse": "Ellipse",
162
     "ellipse": "Ellipse",

+ 1
- 1
src/locales/es-ES.json Прегледај датотеку

152
   },
152
   },
153
   "toolBar": {
153
   "toolBar": {
154
     "selection": "Selección",
154
     "selection": "Selección",
155
-    "draw": "Dibujo libre",
155
+    "freedraw": "Dibujo libre",
156
     "rectangle": "Rectángulo",
156
     "rectangle": "Rectángulo",
157
     "diamond": "Diamante",
157
     "diamond": "Diamante",
158
     "ellipse": "Elipse",
158
     "ellipse": "Elipse",

+ 1
- 1
src/locales/fa-IR.json Прегледај датотеку

152
   },
152
   },
153
   "toolBar": {
153
   "toolBar": {
154
     "selection": "گزینش",
154
     "selection": "گزینش",
155
-    "draw": "طراحی آزاد",
155
+    "freedraw": "طراحی آزاد",
156
     "rectangle": "مستطیل",
156
     "rectangle": "مستطیل",
157
     "diamond": "لوزی",
157
     "diamond": "لوزی",
158
     "ellipse": "بیضی",
158
     "ellipse": "بیضی",

+ 1
- 1
src/locales/fi-FI.json Прегледај датотеку

152
   },
152
   },
153
   "toolBar": {
153
   "toolBar": {
154
     "selection": "Valinta",
154
     "selection": "Valinta",
155
-    "draw": "Vapaa piirto",
155
+    "freedraw": "Vapaa piirto",
156
     "rectangle": "Suorakulmio",
156
     "rectangle": "Suorakulmio",
157
     "diamond": "Vinoneliö",
157
     "diamond": "Vinoneliö",
158
     "ellipse": "Soikio",
158
     "ellipse": "Soikio",

+ 1
- 1
src/locales/fr-FR.json Прегледај датотеку

152
   },
152
   },
153
   "toolBar": {
153
   "toolBar": {
154
     "selection": "Sélection",
154
     "selection": "Sélection",
155
-    "draw": "Dessin libre",
155
+    "freedraw": "Dessin libre",
156
     "rectangle": "Rectangle",
156
     "rectangle": "Rectangle",
157
     "diamond": "Losange",
157
     "diamond": "Losange",
158
     "ellipse": "Ellipse",
158
     "ellipse": "Ellipse",

+ 1
- 1
src/locales/he-IL.json Прегледај датотеку

152
   },
152
   },
153
   "toolBar": {
153
   "toolBar": {
154
     "selection": "בחירה",
154
     "selection": "בחירה",
155
-    "draw": "ציור חופשי",
155
+    "freedraw": "ציור חופשי",
156
     "rectangle": "מרובע",
156
     "rectangle": "מרובע",
157
     "diamond": "מעוין",
157
     "diamond": "מעוין",
158
     "ellipse": "אליפסה",
158
     "ellipse": "אליפסה",

+ 1
- 1
src/locales/hi-IN.json Прегледај датотеку

152
   },
152
   },
153
   "toolBar": {
153
   "toolBar": {
154
     "selection": "चयन",
154
     "selection": "चयन",
155
-    "draw": "मुफ्त ड्रा",
155
+    "freedraw": "मुफ्त ड्रा",
156
     "rectangle": "आयात",
156
     "rectangle": "आयात",
157
     "diamond": "तिर्यग्वर्ग",
157
     "diamond": "तिर्यग्वर्ग",
158
     "ellipse": "दीर्घवृत्त",
158
     "ellipse": "दीर्घवृत्त",

+ 1
- 1
src/locales/hu-HU.json Прегледај датотеку

152
   },
152
   },
153
   "toolBar": {
153
   "toolBar": {
154
     "selection": "Kijelölés",
154
     "selection": "Kijelölés",
155
-    "draw": "Szabadkézi rajz",
155
+    "freedraw": "Szabadkézi rajz",
156
     "rectangle": "Téglalap",
156
     "rectangle": "Téglalap",
157
     "diamond": "Rombusz",
157
     "diamond": "Rombusz",
158
     "ellipse": "Ellipszis",
158
     "ellipse": "Ellipszis",

+ 1
- 1
src/locales/id-ID.json Прегледај датотеку

152
   },
152
   },
153
   "toolBar": {
153
   "toolBar": {
154
     "selection": "Pilihan",
154
     "selection": "Pilihan",
155
-    "draw": "Menggambar bebas",
155
+    "freedraw": "Menggambar bebas",
156
     "rectangle": "Persegi",
156
     "rectangle": "Persegi",
157
     "diamond": "Berlian",
157
     "diamond": "Berlian",
158
     "ellipse": "Elips",
158
     "ellipse": "Elips",

+ 1
- 1
src/locales/it-IT.json Прегледај датотеку

152
   },
152
   },
153
   "toolBar": {
153
   "toolBar": {
154
     "selection": "Selezione",
154
     "selection": "Selezione",
155
-    "draw": "Disegno libero",
155
+    "freedraw": "Disegno libero",
156
     "rectangle": "Rettangolo",
156
     "rectangle": "Rettangolo",
157
     "diamond": "Rombo",
157
     "diamond": "Rombo",
158
     "ellipse": "Ellisse",
158
     "ellipse": "Ellisse",

+ 1
- 1
src/locales/ja-JP.json Прегледај датотеку

152
   },
152
   },
153
   "toolBar": {
153
   "toolBar": {
154
     "selection": "選択",
154
     "selection": "選択",
155
-    "draw": "手書き",
155
+    "freedraw": "手書き",
156
     "rectangle": "矩形",
156
     "rectangle": "矩形",
157
     "diamond": "ひし形",
157
     "diamond": "ひし形",
158
     "ellipse": "楕円",
158
     "ellipse": "楕円",

+ 1
- 1
src/locales/kab-KAB.json Прегледај датотеку

152
   },
152
   },
153
   "toolBar": {
153
   "toolBar": {
154
     "selection": "Tafrayt",
154
     "selection": "Tafrayt",
155
-    "draw": "Unuɣ ilelli",
155
+    "freedraw": "Unuɣ ilelli",
156
     "rectangle": "Asrem",
156
     "rectangle": "Asrem",
157
     "diamond": "Ameɣṛun",
157
     "diamond": "Ameɣṛun",
158
     "ellipse": "Taglayt",
158
     "ellipse": "Taglayt",

+ 1
- 1
src/locales/ko-KR.json Прегледај датотеку

152
   },
152
   },
153
   "toolBar": {
153
   "toolBar": {
154
     "selection": "선택",
154
     "selection": "선택",
155
-    "draw": "자유롭게 그리기",
155
+    "freedraw": "자유롭게 그리기",
156
     "rectangle": "사각형",
156
     "rectangle": "사각형",
157
     "diamond": "다이아몬드",
157
     "diamond": "다이아몬드",
158
     "ellipse": "타원",
158
     "ellipse": "타원",

+ 1
- 1
src/locales/my-MM.json Прегледај датотеку

152
   },
152
   },
153
   "toolBar": {
153
   "toolBar": {
154
     "selection": "ရွေးချယ်",
154
     "selection": "ရွေးချယ်",
155
-    "draw": "အလွတ်ရေးဆွဲ",
155
+    "freedraw": "အလွတ်ရေးဆွဲ",
156
     "rectangle": "စတုဂံ",
156
     "rectangle": "စတုဂံ",
157
     "diamond": "စိန်",
157
     "diamond": "စိန်",
158
     "ellipse": "အဝိုင်း",
158
     "ellipse": "အဝိုင်း",

+ 1
- 1
src/locales/nb-NO.json Прегледај датотеку

152
   },
152
   },
153
   "toolBar": {
153
   "toolBar": {
154
     "selection": "Velg",
154
     "selection": "Velg",
155
-    "draw": "Frihåndstegning",
155
+    "freedraw": "Frihåndstegning",
156
     "rectangle": "Rektangel",
156
     "rectangle": "Rektangel",
157
     "diamond": "Diamant",
157
     "diamond": "Diamant",
158
     "ellipse": "Ellipse",
158
     "ellipse": "Ellipse",

+ 1
- 1
src/locales/nl-NL.json Прегледај датотеку

152
   },
152
   },
153
   "toolBar": {
153
   "toolBar": {
154
     "selection": "Selectie",
154
     "selection": "Selectie",
155
-    "draw": "Vrij tekenen",
155
+    "freedraw": "Vrij tekenen",
156
     "rectangle": "Rechthoek",
156
     "rectangle": "Rechthoek",
157
     "diamond": "Ruit",
157
     "diamond": "Ruit",
158
     "ellipse": "Ovaal",
158
     "ellipse": "Ovaal",

+ 1
- 1
src/locales/nn-NO.json Прегледај датотеку

152
   },
152
   },
153
   "toolBar": {
153
   "toolBar": {
154
     "selection": "Vel",
154
     "selection": "Vel",
155
-    "draw": "Frihandsteikning",
155
+    "freedraw": "Frihandsteikning",
156
     "rectangle": "Rektangel",
156
     "rectangle": "Rektangel",
157
     "diamond": "Diamant",
157
     "diamond": "Diamant",
158
     "ellipse": "Ellipse",
158
     "ellipse": "Ellipse",

+ 1
- 1
src/locales/oc-FR.json Прегледај датотеку

152
   },
152
   },
153
   "toolBar": {
153
   "toolBar": {
154
     "selection": "Seleccion",
154
     "selection": "Seleccion",
155
-    "draw": "Dessenh liure",
155
+    "freedraw": "Dessenh liure",
156
     "rectangle": "Rectangle",
156
     "rectangle": "Rectangle",
157
     "diamond": "Lausange",
157
     "diamond": "Lausange",
158
     "ellipse": "Ellipsa",
158
     "ellipse": "Ellipsa",

+ 1
- 1
src/locales/pa-IN.json Прегледај датотеку

152
   },
152
   },
153
   "toolBar": {
153
   "toolBar": {
154
     "selection": "ਚੋਣਕਾਰ",
154
     "selection": "ਚੋਣਕਾਰ",
155
-    "draw": "ਖੁੱਲ੍ਹੀ ਵਾਹੀ",
155
+    "freedraw": "ਖੁੱਲ੍ਹੀ ਵਾਹੀ",
156
     "rectangle": "ਆਇਤ",
156
     "rectangle": "ਆਇਤ",
157
     "diamond": "ਹੀਰਾ",
157
     "diamond": "ਹੀਰਾ",
158
     "ellipse": "ਅੰਡਾਕਾਰ",
158
     "ellipse": "ਅੰਡਾਕਾਰ",

+ 1
- 1
src/locales/pl-PL.json Прегледај датотеку

152
   },
152
   },
153
   "toolBar": {
153
   "toolBar": {
154
     "selection": "Zaznaczenie",
154
     "selection": "Zaznaczenie",
155
-    "draw": "Swobodne rysowanie",
155
+    "freedraw": "Swobodne rysowanie",
156
     "rectangle": "Prostokąt",
156
     "rectangle": "Prostokąt",
157
     "diamond": "Romb",
157
     "diamond": "Romb",
158
     "ellipse": "Elipsa",
158
     "ellipse": "Elipsa",

+ 1
- 1
src/locales/pt-BR.json Прегледај датотеку

152
   },
152
   },
153
   "toolBar": {
153
   "toolBar": {
154
     "selection": "Seleção",
154
     "selection": "Seleção",
155
-    "draw": "Desenho livre",
155
+    "freedraw": "Desenho livre",
156
     "rectangle": "Retângulo",
156
     "rectangle": "Retângulo",
157
     "diamond": "Losango",
157
     "diamond": "Losango",
158
     "ellipse": "Elipse",
158
     "ellipse": "Elipse",

+ 1
- 1
src/locales/pt-PT.json Прегледај датотеку

152
   },
152
   },
153
   "toolBar": {
153
   "toolBar": {
154
     "selection": "Seleção",
154
     "selection": "Seleção",
155
-    "draw": "Desenho livre",
155
+    "freedraw": "Desenho livre",
156
     "rectangle": "Retângulo",
156
     "rectangle": "Retângulo",
157
     "diamond": "Losango",
157
     "diamond": "Losango",
158
     "ellipse": "Elipse",
158
     "ellipse": "Elipse",

+ 1
- 1
src/locales/ro-RO.json Прегледај датотеку

152
   },
152
   },
153
   "toolBar": {
153
   "toolBar": {
154
     "selection": "Selecție",
154
     "selection": "Selecție",
155
-    "draw": "Desenare liberă",
155
+    "freedraw": "Desenare liberă",
156
     "rectangle": "Dreptunghi",
156
     "rectangle": "Dreptunghi",
157
     "diamond": "Romb",
157
     "diamond": "Romb",
158
     "ellipse": "Elipsă",
158
     "ellipse": "Elipsă",

+ 1
- 1
src/locales/ru-RU.json Прегледај датотеку

152
   },
152
   },
153
   "toolBar": {
153
   "toolBar": {
154
     "selection": "Выделение области",
154
     "selection": "Выделение области",
155
-    "draw": "Свободное рисование",
155
+    "freedraw": "Свободное рисование",
156
     "rectangle": "Прямоугольник",
156
     "rectangle": "Прямоугольник",
157
     "diamond": "Ромб",
157
     "diamond": "Ромб",
158
     "ellipse": "Эллипс",
158
     "ellipse": "Эллипс",

+ 1
- 1
src/locales/sk-SK.json Прегледај датотеку

152
   },
152
   },
153
   "toolBar": {
153
   "toolBar": {
154
     "selection": "Výber",
154
     "selection": "Výber",
155
-    "draw": "Voľné kreslenie",
155
+    "freedraw": "Voľné kreslenie",
156
     "rectangle": "Obdĺžnik",
156
     "rectangle": "Obdĺžnik",
157
     "diamond": "Diamant",
157
     "diamond": "Diamant",
158
     "ellipse": "Elipsa",
158
     "ellipse": "Elipsa",

+ 1
- 1
src/locales/sv-SE.json Прегледај датотеку

152
   },
152
   },
153
   "toolBar": {
153
   "toolBar": {
154
     "selection": "Markering",
154
     "selection": "Markering",
155
-    "draw": "Frihand",
155
+    "freedraw": "Frihand",
156
     "rectangle": "Rektangel",
156
     "rectangle": "Rektangel",
157
     "diamond": "Diamant",
157
     "diamond": "Diamant",
158
     "ellipse": "Ellips",
158
     "ellipse": "Ellips",

+ 1
- 1
src/locales/tr-TR.json Прегледај датотеку

152
   },
152
   },
153
   "toolBar": {
153
   "toolBar": {
154
     "selection": "Seçme",
154
     "selection": "Seçme",
155
-    "draw": "Serbest çizim",
155
+    "freedraw": "Serbest çizim",
156
     "rectangle": "Dikdörtgen",
156
     "rectangle": "Dikdörtgen",
157
     "diamond": "Elmas",
157
     "diamond": "Elmas",
158
     "ellipse": "Elips",
158
     "ellipse": "Elips",

+ 1
- 1
src/locales/uk-UA.json Прегледај датотеку

152
   },
152
   },
153
   "toolBar": {
153
   "toolBar": {
154
     "selection": "Виділення",
154
     "selection": "Виділення",
155
-    "draw": "Вільне креслення",
155
+    "freedraw": "Вільне креслення",
156
     "rectangle": "Прямокутник",
156
     "rectangle": "Прямокутник",
157
     "diamond": "Ромб",
157
     "diamond": "Ромб",
158
     "ellipse": "Еліпс",
158
     "ellipse": "Еліпс",

+ 1
- 1
src/locales/zh-CN.json Прегледај датотеку

152
   },
152
   },
153
   "toolBar": {
153
   "toolBar": {
154
     "selection": "选择",
154
     "selection": "选择",
155
-    "draw": "自由书写",
155
+    "freedraw": "自由书写",
156
     "rectangle": "矩形",
156
     "rectangle": "矩形",
157
     "diamond": "菱形",
157
     "diamond": "菱形",
158
     "ellipse": "椭圆",
158
     "ellipse": "椭圆",

+ 1
- 1
src/locales/zh-TW.json Прегледај датотеку

152
   },
152
   },
153
   "toolBar": {
153
   "toolBar": {
154
     "selection": "選取",
154
     "selection": "選取",
155
-    "draw": "繪圖",
155
+    "freedraw": "繪圖",
156
     "rectangle": "長方形",
156
     "rectangle": "長方形",
157
     "diamond": "菱形",
157
     "diamond": "菱形",
158
     "ellipse": "橢圓",
158
     "ellipse": "橢圓",

+ 1
- 0
src/math.ts Прегледај датотеку

249
   return false;
249
   return false;
250
 };
250
 };
251
 
251
 
252
+// TODO: Rounding this point causes some shake when free drawing
252
 export const getGridPoint = (
253
 export const getGridPoint = (
253
   x: number,
254
   x: number,
254
   y: number,
255
   y: number,

+ 1
- 0
src/points.ts Прегледај датотеку

8
     height: Math.max(...ys) - Math.min(...ys),
8
     height: Math.max(...ys) - Math.min(...ys),
9
   };
9
   };
10
 };
10
 };
11
+
11
 export const rescalePoints = (
12
 export const rescalePoints = (
12
   dimension: 0 | 1,
13
   dimension: 0 | 1,
13
   nextDimensionSize: number,
14
   nextDimensionSize: number,

+ 180
- 28
src/renderer/renderElement.ts Прегледај датотеку

4
   ExcalidrawTextElement,
4
   ExcalidrawTextElement,
5
   Arrowhead,
5
   Arrowhead,
6
   NonDeletedExcalidrawElement,
6
   NonDeletedExcalidrawElement,
7
+  ExcalidrawFreeDrawElement,
7
 } from "../element/types";
8
 } from "../element/types";
8
-import { isTextElement, isLinearElement } from "../element/typeChecks";
9
+import {
10
+  isTextElement,
11
+  isLinearElement,
12
+  isFreeDrawElement,
13
+} from "../element/typeChecks";
9
 import {
14
 import {
10
   getDiamondPoints,
15
   getDiamondPoints,
11
   getElementAbsoluteCoords,
16
   getElementAbsoluteCoords,
27
 import rough from "roughjs/bin/rough";
32
 import rough from "roughjs/bin/rough";
28
 import { Zoom } from "../types";
33
 import { Zoom } from "../types";
29
 import { getDefaultAppState } from "../appState";
34
 import { getDefaultAppState } from "../appState";
35
+import getFreeDrawShape from "perfect-freehand";
30
 
36
 
31
 const defaultAppState = getDefaultAppState();
37
 const defaultAppState = getDefaultAppState();
32
 
38
 
33
-const CANVAS_PADDING = 20;
34
-
35
 const getDashArrayDashed = (strokeWidth: number) => [8, 8 + strokeWidth];
39
 const getDashArrayDashed = (strokeWidth: number) => [8, 8 + strokeWidth];
40
+
36
 const getDashArrayDotted = (strokeWidth: number) => [1.5, 6 + strokeWidth];
41
 const getDashArrayDotted = (strokeWidth: number) => [1.5, 6 + strokeWidth];
37
 
42
 
43
+const getCanvasPadding = (element: ExcalidrawElement) =>
44
+  element.type === "freedraw" ? element.strokeWidth * 12 : 20;
45
+
38
 export interface ExcalidrawElementWithCanvas {
46
 export interface ExcalidrawElementWithCanvas {
39
   element: ExcalidrawElement | ExcalidrawTextElement;
47
   element: ExcalidrawElement | ExcalidrawTextElement;
40
   canvas: HTMLCanvasElement;
48
   canvas: HTMLCanvasElement;
49
 ): ExcalidrawElementWithCanvas => {
57
 ): ExcalidrawElementWithCanvas => {
50
   const canvas = document.createElement("canvas");
58
   const canvas = document.createElement("canvas");
51
   const context = canvas.getContext("2d")!;
59
   const context = canvas.getContext("2d")!;
60
+  const padding = getCanvasPadding(element);
52
 
61
 
53
   let canvasOffsetX = 0;
62
   let canvasOffsetX = 0;
54
   let canvasOffsetY = 0;
63
   let canvasOffsetY = 0;
55
 
64
 
56
-  if (isLinearElement(element)) {
57
-    const [x1, y1, x2, y2] = getElementAbsoluteCoords(element);
65
+  if (isLinearElement(element) || isFreeDrawElement(element)) {
66
+    let [x1, y1, x2, y2] = getElementAbsoluteCoords(element);
67
+
68
+    x1 = Math.floor(x1);
69
+    x2 = Math.ceil(x2);
70
+    y1 = Math.floor(y1);
71
+    y2 = Math.ceil(y2);
72
+
58
     canvas.width =
73
     canvas.width =
59
       distance(x1, x2) * window.devicePixelRatio * zoom.value +
74
       distance(x1, x2) * window.devicePixelRatio * zoom.value +
60
-      CANVAS_PADDING * zoom.value * 2;
75
+      padding * zoom.value * 2;
61
     canvas.height =
76
     canvas.height =
62
       distance(y1, y2) * window.devicePixelRatio * zoom.value +
77
       distance(y1, y2) * window.devicePixelRatio * zoom.value +
63
-      CANVAS_PADDING * zoom.value * 2;
78
+      padding * zoom.value * 2;
64
 
79
 
65
     canvasOffsetX =
80
     canvasOffsetX =
66
       element.x > x1
81
       element.x > x1
80
   } else {
95
   } else {
81
     canvas.width =
96
     canvas.width =
82
       element.width * window.devicePixelRatio * zoom.value +
97
       element.width * window.devicePixelRatio * zoom.value +
83
-      CANVAS_PADDING * zoom.value * 2;
98
+      padding * zoom.value * 2;
84
     canvas.height =
99
     canvas.height =
85
       element.height * window.devicePixelRatio * zoom.value +
100
       element.height * window.devicePixelRatio * zoom.value +
86
-      CANVAS_PADDING * zoom.value * 2;
101
+      padding * zoom.value * 2;
87
   }
102
   }
88
 
103
 
89
-  context.translate(CANVAS_PADDING * zoom.value, CANVAS_PADDING * zoom.value);
104
+  context.translate(padding * zoom.value, padding * zoom.value);
90
 
105
 
91
   context.scale(
106
   context.scale(
92
     window.devicePixelRatio * zoom.value,
107
     window.devicePixelRatio * zoom.value,
94
   );
109
   );
95
 
110
 
96
   const rc = rough.canvas(canvas);
111
   const rc = rough.canvas(canvas);
112
+
97
   drawElementOnCanvas(element, rc, context);
113
   drawElementOnCanvas(element, rc, context);
98
-  context.translate(
99
-    -(CANVAS_PADDING * zoom.value),
100
-    -(CANVAS_PADDING * zoom.value),
101
-  );
114
+
115
+  context.translate(-(padding * zoom.value), -(padding * zoom.value));
102
   context.scale(
116
   context.scale(
103
     1 / (window.devicePixelRatio * zoom.value),
117
     1 / (window.devicePixelRatio * zoom.value),
104
     1 / (window.devicePixelRatio * zoom.value),
118
     1 / (window.devicePixelRatio * zoom.value),
138
       });
152
       });
139
       break;
153
       break;
140
     }
154
     }
155
+    case "freedraw": {
156
+      // Draw directly to canvas
157
+      context.save();
158
+      context.fillStyle = element.strokeColor;
159
+
160
+      const path = getFreeDrawPath2D(element) as Path2D;
161
+
162
+      context.fillStyle = element.strokeColor;
163
+      context.fill(path);
164
+
165
+      context.restore();
166
+      break;
167
+    }
141
     default: {
168
     default: {
142
       if (isTextElement(element)) {
169
       if (isTextElement(element)) {
143
         const rtl = isRTL(element.text);
170
         const rtl = isRTL(element.text);
243
       }
270
       }
244
       return options;
271
       return options;
245
     }
272
     }
246
-    case "line":
247
-    case "draw": {
248
-      // If shape is a line and is a closed shape,
249
-      // fill the shape if a color is set.
273
+    case "draw":
274
+    case "line": {
250
       if (isPathALoop(element.points)) {
275
       if (isPathALoop(element.points)) {
251
         options.fillStyle = element.fillStyle;
276
         options.fillStyle = element.fillStyle;
252
         options.fill =
277
         options.fill =
256
       }
281
       }
257
       return options;
282
       return options;
258
     }
283
     }
284
+    case "freedraw":
259
     case "arrow":
285
     case "arrow":
260
       return options;
286
       return options;
261
     default: {
287
     default: {
264
   }
290
   }
265
 };
291
 };
266
 
292
 
293
+/**
294
+ * Generates the element's shape and puts it into the cache.
295
+ * @param element
296
+ * @param generator
297
+ */
267
 const generateElementShape = (
298
 const generateElementShape = (
268
   element: NonDeletedExcalidrawElement,
299
   element: NonDeletedExcalidrawElement,
269
   generator: RoughGenerator,
300
   generator: RoughGenerator,
270
 ) => {
301
 ) => {
271
   let shape = shapeCache.get(element) || null;
302
   let shape = shapeCache.get(element) || null;
303
+
272
   if (!shape) {
304
   if (!shape) {
273
     elementWithCanvasCache.delete(element);
305
     elementWithCanvasCache.delete(element);
274
 
306
 
327
           generateRoughOptions(element),
359
           generateRoughOptions(element),
328
         );
360
         );
329
         break;
361
         break;
330
-      case "line":
331
       case "draw":
362
       case "draw":
363
+      case "line":
332
       case "arrow": {
364
       case "arrow": {
333
         const options = generateRoughOptions(element);
365
         const options = generateRoughOptions(element);
334
 
366
 
380
                   ...options,
412
                   ...options,
381
                   fill: element.strokeColor,
413
                   fill: element.strokeColor,
382
                   fillStyle: "solid",
414
                   fillStyle: "solid",
415
+                  stroke: "none",
383
                 }),
416
                 }),
384
               ];
417
               ];
385
             }
418
             }
386
 
419
 
387
             // Arrow arrowheads
420
             // Arrow arrowheads
388
             const [x2, y2, x3, y3, x4, y4] = arrowheadPoints;
421
             const [x2, y2, x3, y3, x4, y4] = arrowheadPoints;
422
+
389
             if (element.strokeStyle === "dotted") {
423
             if (element.strokeStyle === "dotted") {
390
               // for dotted arrows caps, reduce gap to make it more legible
424
               // for dotted arrows caps, reduce gap to make it more legible
391
-              options.strokeLineDash = [3, 4];
425
+              const dash = getDashArrayDotted(element.strokeWidth - 1);
426
+              options.strokeLineDash = [dash[0], dash[1] - 1];
392
             } else {
427
             } else {
393
               // for solid/dashed, keep solid arrow cap
428
               // for solid/dashed, keep solid arrow cap
394
               delete options.strokeLineDash;
429
               delete options.strokeLineDash;
423
             shape.push(...shapes);
458
             shape.push(...shapes);
424
           }
459
           }
425
         }
460
         }
461
+
462
+        break;
463
+      }
464
+      case "freedraw": {
465
+        generateFreeDrawShape(element);
466
+        shape = [];
426
         break;
467
         break;
427
       }
468
       }
428
       case "text": {
469
       case "text": {
447
     !sceneState?.shouldCacheIgnoreZoom;
488
     !sceneState?.shouldCacheIgnoreZoom;
448
   if (!prevElementWithCanvas || shouldRegenerateBecauseZoom) {
489
   if (!prevElementWithCanvas || shouldRegenerateBecauseZoom) {
449
     const elementWithCanvas = generateElementCanvas(element, zoom);
490
     const elementWithCanvas = generateElementCanvas(element, zoom);
491
+
450
     elementWithCanvasCache.set(element, elementWithCanvas);
492
     elementWithCanvasCache.set(element, elementWithCanvas);
493
+
451
     return elementWithCanvas;
494
     return elementWithCanvas;
452
   }
495
   }
453
   return prevElementWithCanvas;
496
   return prevElementWithCanvas;
460
   sceneState: SceneState,
503
   sceneState: SceneState,
461
 ) => {
504
 ) => {
462
   const element = elementWithCanvas.element;
505
   const element = elementWithCanvas.element;
463
-  const [x1, y1, x2, y2] = getElementAbsoluteCoords(element);
506
+  const padding = getCanvasPadding(element);
507
+  let [x1, y1, x2, y2] = getElementAbsoluteCoords(element);
508
+
509
+  // Free draw elements will otherwise "shuffle" as the min x and y change
510
+  if (isFreeDrawElement(element)) {
511
+    x1 = Math.floor(x1);
512
+    x2 = Math.ceil(x2);
513
+    y1 = Math.floor(y1);
514
+    y2 = Math.ceil(y2);
515
+  }
516
+
464
   const cx = ((x1 + x2) / 2 + sceneState.scrollX) * window.devicePixelRatio;
517
   const cx = ((x1 + x2) / 2 + sceneState.scrollX) * window.devicePixelRatio;
465
   const cy = ((y1 + y2) / 2 + sceneState.scrollY) * window.devicePixelRatio;
518
   const cy = ((y1 + y2) / 2 + sceneState.scrollY) * window.devicePixelRatio;
466
   context.scale(1 / window.devicePixelRatio, 1 / window.devicePixelRatio);
519
   context.scale(1 / window.devicePixelRatio, 1 / window.devicePixelRatio);
467
   context.translate(cx, cy);
520
   context.translate(cx, cy);
468
   context.rotate(element.angle);
521
   context.rotate(element.angle);
522
+
469
   context.drawImage(
523
   context.drawImage(
470
     elementWithCanvas.canvas!,
524
     elementWithCanvas.canvas!,
471
     (-(x2 - x1) / 2) * window.devicePixelRatio -
525
     (-(x2 - x1) / 2) * window.devicePixelRatio -
472
-      (CANVAS_PADDING * elementWithCanvas.canvasZoom) /
473
-        elementWithCanvas.canvasZoom,
526
+      (padding * elementWithCanvas.canvasZoom) / elementWithCanvas.canvasZoom,
474
     (-(y2 - y1) / 2) * window.devicePixelRatio -
527
     (-(y2 - y1) / 2) * window.devicePixelRatio -
475
-      (CANVAS_PADDING * elementWithCanvas.canvasZoom) /
476
-        elementWithCanvas.canvasZoom,
528
+      (padding * elementWithCanvas.canvasZoom) / elementWithCanvas.canvasZoom,
477
     elementWithCanvas.canvas!.width / elementWithCanvas.canvasZoom,
529
     elementWithCanvas.canvas!.width / elementWithCanvas.canvasZoom,
478
     elementWithCanvas.canvas!.height / elementWithCanvas.canvasZoom,
530
     elementWithCanvas.canvas!.height / elementWithCanvas.canvasZoom,
479
   );
531
   );
508
       );
560
       );
509
       break;
561
       break;
510
     }
562
     }
563
+    case "freedraw": {
564
+      generateElementShape(element, generator);
565
+
566
+      if (renderOptimizations) {
567
+        const elementWithCanvas = generateElementWithCanvas(
568
+          element,
569
+          sceneState,
570
+        );
571
+        drawElementFromCanvas(elementWithCanvas, rc, context, sceneState);
572
+      } else {
573
+        const [x1, y1, x2, y2] = getElementAbsoluteCoords(element);
574
+        const cx = (x1 + x2) / 2 + sceneState.scrollX;
575
+        const cy = (y1 + y2) / 2 + sceneState.scrollY;
576
+        const shiftX = (x2 - x1) / 2 - (element.x - x1);
577
+        const shiftY = (y2 - y1) / 2 - (element.y - y1);
578
+        context.translate(cx, cy);
579
+        context.rotate(element.angle);
580
+        context.translate(-shiftX, -shiftY);
581
+        drawElementOnCanvas(element, rc, context);
582
+        context.translate(shiftX, shiftY);
583
+        context.rotate(-element.angle);
584
+        context.translate(-cx, -cy);
585
+      }
586
+
587
+      break;
588
+    }
511
     case "rectangle":
589
     case "rectangle":
512
     case "diamond":
590
     case "diamond":
513
     case "ellipse":
591
     case "ellipse":
514
-    case "line":
515
     case "draw":
592
     case "draw":
593
+    case "line":
516
     case "arrow":
594
     case "arrow":
517
     case "text": {
595
     case "text": {
518
       generateElementShape(element, generator);
596
       generateElementShape(element, generator);
583
       svgRoot.appendChild(node);
661
       svgRoot.appendChild(node);
584
       break;
662
       break;
585
     }
663
     }
586
-    case "line":
587
     case "draw":
664
     case "draw":
665
+    case "line":
588
     case "arrow": {
666
     case "arrow": {
589
       generateElementShape(element, generator);
667
       generateElementShape(element, generator);
590
       const group = svgRoot.ownerDocument!.createElementNS(SVG_NS, "g");
668
       const group = svgRoot.ownerDocument!.createElementNS(SVG_NS, "g");
604
           }) rotate(${degree} ${cx} ${cy})`,
682
           }) rotate(${degree} ${cx} ${cy})`,
605
         );
683
         );
606
         if (
684
         if (
607
-          (element.type === "line" || element.type === "draw") &&
685
+          element.type === "line" &&
608
           isPathALoop(element.points) &&
686
           isPathALoop(element.points) &&
609
           element.backgroundColor !== "transparent"
687
           element.backgroundColor !== "transparent"
610
         ) {
688
         ) {
615
       svgRoot.appendChild(group);
693
       svgRoot.appendChild(group);
616
       break;
694
       break;
617
     }
695
     }
696
+    case "freedraw": {
697
+      generateFreeDrawShape(element);
698
+      const opacity = element.opacity / 100;
699
+      const node = svgRoot.ownerDocument!.createElementNS(SVG_NS, "g");
700
+      if (opacity !== 1) {
701
+        node.setAttribute("stroke-opacity", `${opacity}`);
702
+        node.setAttribute("fill-opacity", `${opacity}`);
703
+      }
704
+      node.setAttribute(
705
+        "transform",
706
+        `translate(${offsetX || 0} ${
707
+          offsetY || 0
708
+        }) rotate(${degree} ${cx} ${cy})`,
709
+      );
710
+      const path = svgRoot.ownerDocument!.createElementNS(SVG_NS, "path");
711
+      node.setAttribute("stroke", "none");
712
+      node.setAttribute("fill", element.strokeStyle);
713
+      path.setAttribute("d", getFreeDrawSvgPath(element));
714
+      node.appendChild(path);
715
+      svgRoot.appendChild(node);
716
+      break;
717
+    }
618
     default: {
718
     default: {
619
       if (isTextElement(element)) {
719
       if (isTextElement(element)) {
620
         const opacity = element.opacity / 100;
720
         const opacity = element.opacity / 100;
666
     }
766
     }
667
   }
767
   }
668
 };
768
 };
769
+
770
+export const pathsCache = new WeakMap<ExcalidrawFreeDrawElement, Path2D>([]);
771
+
772
+export function generateFreeDrawShape(element: ExcalidrawFreeDrawElement) {
773
+  const svgPathData = getFreeDrawSvgPath(element);
774
+  const path = new Path2D(svgPathData);
775
+  pathsCache.set(element, path);
776
+  return path;
777
+}
778
+
779
+export function getFreeDrawPath2D(element: ExcalidrawFreeDrawElement) {
780
+  return pathsCache.get(element);
781
+}
782
+
783
+export function getFreeDrawSvgPath(element: ExcalidrawFreeDrawElement) {
784
+  const inputPoints = element.simulatePressure
785
+    ? element.points
786
+    : element.points.length
787
+    ? element.points.map(([x, y], i) => [x, y, element.pressures[i]])
788
+    : [[0, 0, 0]];
789
+
790
+  // Consider changing the options for simulated pressure vs real pressure
791
+  const options = {
792
+    simulatePressure: element.simulatePressure,
793
+    size: element.strokeWidth * 6,
794
+    thinning: 0.5,
795
+    smoothing: 0.5,
796
+    streamline: 0.5,
797
+    easing: (t: number) => t * (2 - t),
798
+    last: true,
799
+  };
800
+
801
+  const points = getFreeDrawShape(inputPoints as number[][], options);
802
+  const d: (string | number)[] = [];
803
+
804
+  let [p0, p1] = points;
805
+
806
+  d.push("M", p0[0], p0[1], "Q");
807
+
808
+  for (let i = 0; i < points.length; i++) {
809
+    d.push(p0[0], p0[1], (p0[0] + p1[0]) / 2, (p0[1] + p1[1]) / 2);
810
+    p0 = p1;
811
+    p1 = points[i];
812
+  }
813
+
814
+  p1 = points[0];
815
+  d.push(p0[0], p0[1], (p0[0] + p1[0]) / 2, (p0[1] + p1[1]) / 2);
816
+
817
+  d.push("Z");
818
+
819
+  return d.join(" ");
820
+}

+ 2
- 1
src/renderer/renderScene.ts Прегледај датотеку

201
     renderGrid?: boolean;
201
     renderGrid?: boolean;
202
   } = {},
202
   } = {},
203
 ) => {
203
 ) => {
204
-  if (!canvas) {
204
+  if (canvas === null) {
205
     return { atLeastOneVisibleElement: false };
205
     return { atLeastOneVisibleElement: false };
206
   }
206
   }
207
 
207
 
208
   const context = canvas.getContext("2d")!;
208
   const context = canvas.getContext("2d")!;
209
+
209
   context.scale(scale, scale);
210
   context.scale(scale, scale);
210
 
211
 
211
   // When doing calculations based on canvas width we should used normalized one
212
   // When doing calculations based on canvas width we should used normalized one

+ 8
- 5
src/scene/comparisons.ts Прегледај датотеку

9
   type === "rectangle" ||
9
   type === "rectangle" ||
10
   type === "ellipse" ||
10
   type === "ellipse" ||
11
   type === "diamond" ||
11
   type === "diamond" ||
12
-  type === "draw" ||
13
   type === "line";
12
   type === "line";
14
 
13
 
15
-export const hasStroke = (type: string) =>
14
+export const hasStrokeWidth = (type: string) =>
16
   type === "rectangle" ||
15
   type === "rectangle" ||
17
   type === "ellipse" ||
16
   type === "ellipse" ||
18
   type === "diamond" ||
17
   type === "diamond" ||
18
+  type === "freedraw" ||
19
   type === "arrow" ||
19
   type === "arrow" ||
20
-  type === "draw" ||
21
   type === "line";
20
   type === "line";
22
 
21
 
23
-export const canChangeSharpness = (type: string) =>
22
+export const hasStrokeStyle = (type: string) =>
24
   type === "rectangle" ||
23
   type === "rectangle" ||
24
+  type === "ellipse" ||
25
+  type === "diamond" ||
25
   type === "arrow" ||
26
   type === "arrow" ||
26
-  type === "draw" ||
27
   type === "line";
27
   type === "line";
28
 
28
 
29
+export const canChangeSharpness = (type: string) =>
30
+  type === "rectangle" || type === "arrow" || type === "line";
31
+
29
 export const hasText = (type: string) => type === "text";
32
 export const hasText = (type: string) => type === "text";
30
 
33
 
31
 export const canHaveArrowheads = (type: string) => type === "arrow";
34
 export const canHaveArrowheads = (type: string) => type === "arrow";

+ 2
- 1
src/scene/index.ts Прегледај датотеку

9
 export { calculateScrollCenter } from "./scroll";
9
 export { calculateScrollCenter } from "./scroll";
10
 export {
10
 export {
11
   hasBackground,
11
   hasBackground,
12
-  hasStroke,
12
+  hasStrokeWidth,
13
+  hasStrokeStyle,
13
   canHaveArrowheads,
14
   canHaveArrowheads,
14
   canChangeSharpness,
15
   canChangeSharpness,
15
   getElementAtPosition,
16
   getElementAtPosition,

+ 1
- 1
src/shapes.tsx Прегледај датотеку

80
         ></path>
80
         ></path>
81
       </svg>
81
       </svg>
82
     ),
82
     ),
83
-    value: "draw",
83
+    value: "freedraw",
84
     key: KEYS.X,
84
     key: KEYS.X,
85
   },
85
   },
86
   {
86
   {

+ 91
- 55
src/tests/__snapshots__/regressionTests.test.tsx.snap Прегледај датотеку

6280
   "editingGroupId": null,
6280
   "editingGroupId": null,
6281
   "editingLinearElement": null,
6281
   "editingLinearElement": null,
6282
   "elementLocked": false,
6282
   "elementLocked": false,
6283
-  "elementType": "draw",
6283
+  "elementType": "freedraw",
6284
   "errorMessage": null,
6284
   "errorMessage": null,
6285
   "exportBackground": true,
6285
   "exportBackground": true,
6286
   "exportEmbedScene": false,
6286
   "exportEmbedScene": false,
6596
   "angle": 0,
6596
   "angle": 0,
6597
   "backgroundColor": "transparent",
6597
   "backgroundColor": "transparent",
6598
   "boundElementIds": null,
6598
   "boundElementIds": null,
6599
-  "endArrowhead": null,
6600
-  "endBinding": null,
6601
   "fillStyle": "hachure",
6599
   "fillStyle": "hachure",
6602
   "groupIds": Array [],
6600
   "groupIds": Array [],
6603
   "height": 10,
6601
   "height": 10,
6614
       50,
6612
       50,
6615
       10,
6613
       10,
6616
     ],
6614
     ],
6615
+    Array [
6616
+      50,
6617
+      10,
6618
+    ],
6619
+  ],
6620
+  "pressures": Array [
6621
+    0,
6622
+    0,
6623
+    0,
6617
   ],
6624
   ],
6618
   "roughness": 1,
6625
   "roughness": 1,
6619
   "seed": 941653321,
6626
   "seed": 941653321,
6620
-  "startArrowhead": null,
6621
-  "startBinding": null,
6627
+  "simulatePressure": false,
6622
   "strokeColor": "#000000",
6628
   "strokeColor": "#000000",
6623
   "strokeSharpness": "round",
6629
   "strokeSharpness": "round",
6624
   "strokeStyle": "solid",
6630
   "strokeStyle": "solid",
6625
   "strokeWidth": 1,
6631
   "strokeWidth": 1,
6626
-  "type": "draw",
6627
-  "version": 3,
6628
-  "versionNonce": 1402203177,
6632
+  "type": "freedraw",
6633
+  "version": 4,
6634
+  "versionNonce": 1359939303,
6629
   "width": 50,
6635
   "width": 50,
6630
   "x": 550,
6636
   "x": 550,
6631
   "y": -10,
6637
   "y": -10,
8246
           "angle": 0,
8252
           "angle": 0,
8247
           "backgroundColor": "transparent",
8253
           "backgroundColor": "transparent",
8248
           "boundElementIds": null,
8254
           "boundElementIds": null,
8249
-          "endArrowhead": null,
8250
-          "endBinding": null,
8251
           "fillStyle": "hachure",
8255
           "fillStyle": "hachure",
8252
           "groupIds": Array [],
8256
           "groupIds": Array [],
8253
           "height": 10,
8257
           "height": 10,
8264
               50,
8268
               50,
8265
               10,
8269
               10,
8266
             ],
8270
             ],
8271
+            Array [
8272
+              50,
8273
+              10,
8274
+            ],
8275
+          ],
8276
+          "pressures": Array [
8277
+            0,
8278
+            0,
8279
+            0,
8267
           ],
8280
           ],
8268
           "roughness": 1,
8281
           "roughness": 1,
8269
           "seed": 941653321,
8282
           "seed": 941653321,
8270
-          "startArrowhead": null,
8271
-          "startBinding": null,
8283
+          "simulatePressure": false,
8272
           "strokeColor": "#000000",
8284
           "strokeColor": "#000000",
8273
           "strokeSharpness": "round",
8285
           "strokeSharpness": "round",
8274
           "strokeStyle": "solid",
8286
           "strokeStyle": "solid",
8275
           "strokeWidth": 1,
8287
           "strokeWidth": 1,
8276
-          "type": "draw",
8277
-          "version": 3,
8278
-          "versionNonce": 1402203177,
8288
+          "type": "freedraw",
8289
+          "version": 4,
8290
+          "versionNonce": 1359939303,
8279
           "width": 50,
8291
           "width": 50,
8280
           "x": 550,
8292
           "x": 550,
8281
           "y": -10,
8293
           "y": -10,
10355
 
10367
 
10356
 exports[`regression tests key 6 selects line tool: [end of test] number of renders 1`] = `8`;
10368
 exports[`regression tests key 6 selects line tool: [end of test] number of renders 1`] = `8`;
10357
 
10369
 
10358
-exports[`regression tests key 7 selects draw tool: [end of test] appState 1`] = `
10370
+exports[`regression tests key 7 selects freedraw tool: [end of test] appState 1`] = `
10359
 Object {
10371
 Object {
10360
   "collaborators": Map {},
10372
   "collaborators": Map {},
10361
   "currentChartType": "bar",
10373
   "currentChartType": "bar",
10379
   "editingGroupId": null,
10391
   "editingGroupId": null,
10380
   "editingLinearElement": null,
10392
   "editingLinearElement": null,
10381
   "elementLocked": false,
10393
   "elementLocked": false,
10382
-  "elementType": "draw",
10394
+  "elementType": "freedraw",
10383
   "errorMessage": null,
10395
   "errorMessage": null,
10384
   "exportBackground": true,
10396
   "exportBackground": true,
10385
   "exportEmbedScene": false,
10397
   "exportEmbedScene": false,
10434
 }
10446
 }
10435
 `;
10447
 `;
10436
 
10448
 
10437
-exports[`regression tests key 7 selects draw tool: [end of test] element 0 1`] = `
10449
+exports[`regression tests key 7 selects freedraw tool: [end of test] element 0 1`] = `
10438
 Object {
10450
 Object {
10439
   "angle": 0,
10451
   "angle": 0,
10440
   "backgroundColor": "transparent",
10452
   "backgroundColor": "transparent",
10441
   "boundElementIds": null,
10453
   "boundElementIds": null,
10442
-  "endArrowhead": null,
10443
-  "endBinding": null,
10444
   "fillStyle": "hachure",
10454
   "fillStyle": "hachure",
10445
   "groupIds": Array [],
10455
   "groupIds": Array [],
10446
   "height": 10,
10456
   "height": 10,
10457
       10,
10467
       10,
10458
       10,
10468
       10,
10459
     ],
10469
     ],
10470
+    Array [
10471
+      10,
10472
+      10,
10473
+    ],
10474
+  ],
10475
+  "pressures": Array [
10476
+    0,
10477
+    0,
10478
+    0,
10460
   ],
10479
   ],
10461
   "roughness": 1,
10480
   "roughness": 1,
10462
   "seed": 337897,
10481
   "seed": 337897,
10463
-  "startArrowhead": null,
10464
-  "startBinding": null,
10482
+  "simulatePressure": false,
10465
   "strokeColor": "#000000",
10483
   "strokeColor": "#000000",
10466
   "strokeSharpness": "round",
10484
   "strokeSharpness": "round",
10467
   "strokeStyle": "solid",
10485
   "strokeStyle": "solid",
10468
   "strokeWidth": 1,
10486
   "strokeWidth": 1,
10469
-  "type": "draw",
10470
-  "version": 3,
10471
-  "versionNonce": 449462985,
10487
+  "type": "freedraw",
10488
+  "version": 4,
10489
+  "versionNonce": 453191,
10472
   "width": 10,
10490
   "width": 10,
10473
   "x": 10,
10491
   "x": 10,
10474
   "y": 10,
10492
   "y": 10,
10475
 }
10493
 }
10476
 `;
10494
 `;
10477
 
10495
 
10478
-exports[`regression tests key 7 selects draw tool: [end of test] history 1`] = `
10496
+exports[`regression tests key 7 selects freedraw tool: [end of test] history 1`] = `
10479
 Object {
10497
 Object {
10480
   "recording": false,
10498
   "recording": false,
10481
   "redoStack": Array [],
10499
   "redoStack": Array [],
10505
           "angle": 0,
10523
           "angle": 0,
10506
           "backgroundColor": "transparent",
10524
           "backgroundColor": "transparent",
10507
           "boundElementIds": null,
10525
           "boundElementIds": null,
10508
-          "endArrowhead": null,
10509
-          "endBinding": null,
10510
           "fillStyle": "hachure",
10526
           "fillStyle": "hachure",
10511
           "groupIds": Array [],
10527
           "groupIds": Array [],
10512
           "height": 10,
10528
           "height": 10,
10523
               10,
10539
               10,
10524
               10,
10540
               10,
10525
             ],
10541
             ],
10542
+            Array [
10543
+              10,
10544
+              10,
10545
+            ],
10546
+          ],
10547
+          "pressures": Array [
10548
+            0,
10549
+            0,
10550
+            0,
10526
           ],
10551
           ],
10527
           "roughness": 1,
10552
           "roughness": 1,
10528
           "seed": 337897,
10553
           "seed": 337897,
10529
-          "startArrowhead": null,
10530
-          "startBinding": null,
10554
+          "simulatePressure": false,
10531
           "strokeColor": "#000000",
10555
           "strokeColor": "#000000",
10532
           "strokeSharpness": "round",
10556
           "strokeSharpness": "round",
10533
           "strokeStyle": "solid",
10557
           "strokeStyle": "solid",
10534
           "strokeWidth": 1,
10558
           "strokeWidth": 1,
10535
-          "type": "draw",
10536
-          "version": 3,
10537
-          "versionNonce": 449462985,
10559
+          "type": "freedraw",
10560
+          "version": 4,
10561
+          "versionNonce": 453191,
10538
           "width": 10,
10562
           "width": 10,
10539
           "x": 10,
10563
           "x": 10,
10540
           "y": 10,
10564
           "y": 10,
10545
 }
10569
 }
10546
 `;
10570
 `;
10547
 
10571
 
10548
-exports[`regression tests key 7 selects draw tool: [end of test] number of elements 1`] = `1`;
10572
+exports[`regression tests key 7 selects freedraw tool: [end of test] number of elements 1`] = `1`;
10549
 
10573
 
10550
-exports[`regression tests key 7 selects draw tool: [end of test] number of renders 1`] = `8`;
10574
+exports[`regression tests key 7 selects freedraw tool: [end of test] number of renders 1`] = `8`;
10551
 
10575
 
10552
 exports[`regression tests key a selects arrow tool: [end of test] appState 1`] = `
10576
 exports[`regression tests key a selects arrow tool: [end of test] appState 1`] = `
10553
 Object {
10577
 Object {
11429
 
11453
 
11430
 exports[`regression tests key r selects rectangle tool: [end of test] number of renders 1`] = `8`;
11454
 exports[`regression tests key r selects rectangle tool: [end of test] number of renders 1`] = `8`;
11431
 
11455
 
11432
-exports[`regression tests key x selects draw tool: [end of test] appState 1`] = `
11456
+exports[`regression tests key x selects freedraw tool: [end of test] appState 1`] = `
11433
 Object {
11457
 Object {
11434
   "collaborators": Map {},
11458
   "collaborators": Map {},
11435
   "currentChartType": "bar",
11459
   "currentChartType": "bar",
11453
   "editingGroupId": null,
11477
   "editingGroupId": null,
11454
   "editingLinearElement": null,
11478
   "editingLinearElement": null,
11455
   "elementLocked": false,
11479
   "elementLocked": false,
11456
-  "elementType": "draw",
11480
+  "elementType": "freedraw",
11457
   "errorMessage": null,
11481
   "errorMessage": null,
11458
   "exportBackground": true,
11482
   "exportBackground": true,
11459
   "exportEmbedScene": false,
11483
   "exportEmbedScene": false,
11508
 }
11532
 }
11509
 `;
11533
 `;
11510
 
11534
 
11511
-exports[`regression tests key x selects draw tool: [end of test] element 0 1`] = `
11535
+exports[`regression tests key x selects freedraw tool: [end of test] element 0 1`] = `
11512
 Object {
11536
 Object {
11513
   "angle": 0,
11537
   "angle": 0,
11514
   "backgroundColor": "transparent",
11538
   "backgroundColor": "transparent",
11515
   "boundElementIds": null,
11539
   "boundElementIds": null,
11516
-  "endArrowhead": null,
11517
-  "endBinding": null,
11518
   "fillStyle": "hachure",
11540
   "fillStyle": "hachure",
11519
   "groupIds": Array [],
11541
   "groupIds": Array [],
11520
   "height": 10,
11542
   "height": 10,
11531
       10,
11553
       10,
11532
       10,
11554
       10,
11533
     ],
11555
     ],
11556
+    Array [
11557
+      10,
11558
+      10,
11559
+    ],
11560
+  ],
11561
+  "pressures": Array [
11562
+    0,
11563
+    0,
11564
+    0,
11534
   ],
11565
   ],
11535
   "roughness": 1,
11566
   "roughness": 1,
11536
   "seed": 337897,
11567
   "seed": 337897,
11537
-  "startArrowhead": null,
11538
-  "startBinding": null,
11568
+  "simulatePressure": false,
11539
   "strokeColor": "#000000",
11569
   "strokeColor": "#000000",
11540
   "strokeSharpness": "round",
11570
   "strokeSharpness": "round",
11541
   "strokeStyle": "solid",
11571
   "strokeStyle": "solid",
11542
   "strokeWidth": 1,
11572
   "strokeWidth": 1,
11543
-  "type": "draw",
11544
-  "version": 3,
11545
-  "versionNonce": 449462985,
11573
+  "type": "freedraw",
11574
+  "version": 4,
11575
+  "versionNonce": 453191,
11546
   "width": 10,
11576
   "width": 10,
11547
   "x": 10,
11577
   "x": 10,
11548
   "y": 10,
11578
   "y": 10,
11549
 }
11579
 }
11550
 `;
11580
 `;
11551
 
11581
 
11552
-exports[`regression tests key x selects draw tool: [end of test] history 1`] = `
11582
+exports[`regression tests key x selects freedraw tool: [end of test] history 1`] = `
11553
 Object {
11583
 Object {
11554
   "recording": false,
11584
   "recording": false,
11555
   "redoStack": Array [],
11585
   "redoStack": Array [],
11579
           "angle": 0,
11609
           "angle": 0,
11580
           "backgroundColor": "transparent",
11610
           "backgroundColor": "transparent",
11581
           "boundElementIds": null,
11611
           "boundElementIds": null,
11582
-          "endArrowhead": null,
11583
-          "endBinding": null,
11584
           "fillStyle": "hachure",
11612
           "fillStyle": "hachure",
11585
           "groupIds": Array [],
11613
           "groupIds": Array [],
11586
           "height": 10,
11614
           "height": 10,
11597
               10,
11625
               10,
11598
               10,
11626
               10,
11599
             ],
11627
             ],
11628
+            Array [
11629
+              10,
11630
+              10,
11631
+            ],
11632
+          ],
11633
+          "pressures": Array [
11634
+            0,
11635
+            0,
11636
+            0,
11600
           ],
11637
           ],
11601
           "roughness": 1,
11638
           "roughness": 1,
11602
           "seed": 337897,
11639
           "seed": 337897,
11603
-          "startArrowhead": null,
11604
-          "startBinding": null,
11640
+          "simulatePressure": false,
11605
           "strokeColor": "#000000",
11641
           "strokeColor": "#000000",
11606
           "strokeSharpness": "round",
11642
           "strokeSharpness": "round",
11607
           "strokeStyle": "solid",
11643
           "strokeStyle": "solid",
11608
           "strokeWidth": 1,
11644
           "strokeWidth": 1,
11609
-          "type": "draw",
11610
-          "version": 3,
11611
-          "versionNonce": 449462985,
11645
+          "type": "freedraw",
11646
+          "version": 4,
11647
+          "versionNonce": 453191,
11612
           "width": 10,
11648
           "width": 10,
11613
           "x": 10,
11649
           "x": 10,
11614
           "y": 10,
11650
           "y": 10,
11619
 }
11655
 }
11620
 `;
11656
 `;
11621
 
11657
 
11622
-exports[`regression tests key x selects draw tool: [end of test] number of elements 1`] = `1`;
11658
+exports[`regression tests key x selects freedraw tool: [end of test] number of elements 1`] = `1`;
11623
 
11659
 
11624
-exports[`regression tests key x selects draw tool: [end of test] number of renders 1`] = `8`;
11660
+exports[`regression tests key x selects freedraw tool: [end of test] number of renders 1`] = `8`;
11625
 
11661
 
11626
 exports[`regression tests make a group and duplicate it: [end of test] appState 1`] = `
11662
 exports[`regression tests make a group and duplicate it: [end of test] appState 1`] = `
11627
 Object {
11663
 Object {

+ 1
- 1
src/tests/flip.test.tsx Прегледај датотеку

71
 };
71
 };
72
 
72
 
73
 const createAndReturnOneDraw = (angle: number = 0) => {
73
 const createAndReturnOneDraw = (angle: number = 0) => {
74
-  return UI.createElement("draw", {
74
+  return UI.createElement("freedraw", {
75
     x: 0,
75
     x: 0,
76
     y: 0,
76
     y: 0,
77
     width: 50,
77
     width: 50,

+ 13
- 3
src/tests/helpers/api.ts Прегледај датотеку

3
   ExcalidrawGenericElement,
3
   ExcalidrawGenericElement,
4
   ExcalidrawTextElement,
4
   ExcalidrawTextElement,
5
   ExcalidrawLinearElement,
5
   ExcalidrawLinearElement,
6
+  ExcalidrawFreeDrawElement,
6
 } from "../../element/types";
7
 } from "../../element/types";
7
 import { newElement, newTextElement, newLinearElement } from "../../element";
8
 import { newElement, newTextElement, newLinearElement } from "../../element";
8
 import { DEFAULT_VERTICAL_ALIGN } from "../../constants";
9
 import { DEFAULT_VERTICAL_ALIGN } from "../../constants";
12
 import util from "util";
13
 import util from "util";
13
 import path from "path";
14
 import path from "path";
14
 import { getMimeType } from "../../data/blob";
15
 import { getMimeType } from "../../data/blob";
16
+import { newFreeDrawElement } from "../../element/newElement";
15
 
17
 
16
 const readFile = util.promisify(fs.readFile);
18
 const readFile = util.promisify(fs.readFile);
17
 
19
 
81
     verticalAlign?: T extends "text"
83
     verticalAlign?: T extends "text"
82
       ? ExcalidrawTextElement["verticalAlign"]
84
       ? ExcalidrawTextElement["verticalAlign"]
83
       : never;
85
       : never;
84
-  }): T extends "arrow" | "line" | "draw"
86
+  }): T extends "arrow" | "line"
85
     ? ExcalidrawLinearElement
87
     ? ExcalidrawLinearElement
88
+    : T extends "freedraw"
89
+    ? ExcalidrawFreeDrawElement
86
     : T extends "text"
90
     : T extends "text"
87
     ? ExcalidrawTextElement
91
     ? ExcalidrawTextElement
88
     : ExcalidrawGenericElement => {
92
     : ExcalidrawGenericElement => {
125
           verticalAlign: rest.verticalAlign ?? DEFAULT_VERTICAL_ALIGN,
129
           verticalAlign: rest.verticalAlign ?? DEFAULT_VERTICAL_ALIGN,
126
         });
130
         });
127
         break;
131
         break;
132
+      case "freedraw":
133
+        element = newFreeDrawElement({
134
+          type: type as "freedraw",
135
+          simulatePressure: true,
136
+          ...base,
137
+        });
138
+        break;
128
       case "arrow":
139
       case "arrow":
129
       case "line":
140
       case "line":
130
-      case "draw":
131
         element = newLinearElement({
141
         element = newLinearElement({
132
-          type: type as "arrow" | "line" | "draw",
142
+          type: type as "arrow" | "line",
133
           startArrowhead: null,
143
           startArrowhead: null,
134
           endArrowhead: null,
144
           endArrowhead: null,
135
           ...base,
145
           ...base,

+ 2
- 2
src/tests/helpers/ui.ts Прегледај датотеку

213
       height?: number;
213
       height?: number;
214
       angle?: number;
214
       angle?: number;
215
     } = {},
215
     } = {},
216
-  ): (T extends "arrow" | "line" | "draw"
216
+  ): (T extends "arrow" | "line" | "freedraw"
217
     ? ExcalidrawLinearElement
217
     ? ExcalidrawLinearElement
218
     : T extends "text"
218
     : T extends "text"
219
     ? ExcalidrawTextElement
219
     ? ExcalidrawTextElement
220
     : ExcalidrawElement) & {
220
     : ExcalidrawElement) & {
221
     /** Returns the actual, current element from the elements array, instead
221
     /** Returns the actual, current element from the elements array, instead
222
         of the proxy */
222
         of the proxy */
223
-    get(): T extends "arrow" | "line" | "draw"
223
+    get(): T extends "arrow" | "line" | "freedraw"
224
       ? ExcalidrawLinearElement
224
       ? ExcalidrawLinearElement
225
       : T extends "text"
225
       : T extends "text"
226
       ? ExcalidrawTextElement
226
       ? ExcalidrawTextElement

+ 1
- 1
src/tests/queries/toolQueries.ts Прегледај датотеку

7
   ellipse: "ellipse",
7
   ellipse: "ellipse",
8
   arrow: "arrow",
8
   arrow: "arrow",
9
   line: "line",
9
   line: "line",
10
-  draw: "draw",
10
+  freedraw: "freedraw",
11
   text: "text",
11
   text: "text",
12
 };
12
 };
13
 
13
 

+ 3
- 3
src/tests/regressionTests.test.tsx Прегледај датотеку

106
     mouse.click(30, 10);
106
     mouse.click(30, 10);
107
     Keyboard.keyPress(KEYS.ENTER);
107
     Keyboard.keyPress(KEYS.ENTER);
108
 
108
 
109
-    UI.clickTool("draw");
109
+    UI.clickTool("freedraw");
110
     mouse.down(40, -20);
110
     mouse.down(40, -20);
111
     mouse.up(50, 10);
111
     mouse.up(50, 10);
112
 
112
 
118
       "line",
118
       "line",
119
       "arrow",
119
       "arrow",
120
       "line",
120
       "line",
121
-      "draw",
121
+      "freedraw",
122
     ]);
122
     ]);
123
   });
123
   });
124
 
124
 
146
     [`4${KEYS.E}`, "ellipse", true],
146
     [`4${KEYS.E}`, "ellipse", true],
147
     [`5${KEYS.A}`, "arrow", true],
147
     [`5${KEYS.A}`, "arrow", true],
148
     [`6${KEYS.L}`, "line", true],
148
     [`6${KEYS.L}`, "line", true],
149
-    [`7${KEYS.X}`, "draw", false],
149
+    [`7${KEYS.X}`, "freedraw", false],
150
   ] as [string, ExcalidrawElement["type"], boolean][]) {
150
   ] as [string, ExcalidrawElement["type"], boolean][]) {
151
     for (const key of keys) {
151
     for (const key of keys) {
152
       it(`key ${key} selects ${shape} tool`, () => {
152
       it(`key ${key} selects ${shape} tool`, () => {

+ 5
- 0
yarn.lock Прегледај датотеку

9249
   version "0.5.3"
9249
   version "0.5.3"
9250
   resolved "https://registry.npmjs.org/pepjs/-/pepjs-0.5.3.tgz"
9250
   resolved "https://registry.npmjs.org/pepjs/-/pepjs-0.5.3.tgz"
9251
 
9251
 
9252
+perfect-freehand@0.4.7:
9253
+  version "0.4.7"
9254
+  resolved "https://registry.yarnpkg.com/perfect-freehand/-/perfect-freehand-0.4.7.tgz#4d85fd64881ba81b2a4eaa6ac4e8983ccb21dd43"
9255
+  integrity sha512-SSSFL8VzXiOHQdUTyNyOb0JC+btVZRy9bi6jos7Nb7PBTI0PHX5jM6RgCTSrubQ8Ul9qOYWmWgJBrwVGHwyJZQ==
9256
+
9252
 performance-now@^2.1.0:
9257
 performance-now@^2.1.0:
9253
   version "2.1.0"
9258
   version "2.1.0"
9254
   resolved "https://registry.npmjs.org/performance-now/-/performance-now-2.1.0.tgz"
9259
   resolved "https://registry.npmjs.org/performance-now/-/performance-now-2.1.0.tgz"

Loading…
Откажи
Сачувај