js-draw 1.0.0 → 1.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (244) hide show
  1. package/README.md +20 -6
  2. package/dist/bundle.js +1 -1
  3. package/dist/cjs/Editor.js +1 -1
  4. package/dist/cjs/Editor.loadFrom.test.d.ts +1 -0
  5. package/dist/cjs/Editor.test.d.ts +1 -0
  6. package/dist/cjs/Editor.toSVG.test.d.ts +1 -0
  7. package/dist/cjs/EditorImage.test.d.ts +1 -0
  8. package/dist/cjs/EventDispatcher.test.d.ts +1 -0
  9. package/dist/cjs/SVGLoader.test.d.ts +1 -0
  10. package/dist/cjs/UndoRedoHistory.test.d.ts +1 -0
  11. package/dist/cjs/commands/uniteCommands.test.d.ts +1 -0
  12. package/dist/cjs/components/AbstractComponent.transformBy.test.d.ts +1 -0
  13. package/dist/cjs/components/BackgroundComponent.test.d.ts +1 -0
  14. package/dist/cjs/components/Stroke.test.d.ts +1 -0
  15. package/dist/cjs/components/TextComponent.test.d.ts +1 -0
  16. package/dist/cjs/components/UnknownSVGObject.test.d.ts +1 -0
  17. package/dist/cjs/components/builders/FreehandLineBuilder.test.d.ts +1 -0
  18. package/dist/cjs/localizations/getLocalizationTable.test.d.ts +1 -0
  19. package/dist/cjs/rendering/RenderingStyle.test.d.ts +1 -0
  20. package/dist/cjs/rendering/caching/CacheRecord.test.d.ts +1 -0
  21. package/dist/cjs/rendering/caching/RenderingCache.test.d.ts +1 -0
  22. package/dist/cjs/rendering/renderers/DummyRenderer.test.d.ts +1 -0
  23. package/dist/cjs/rendering/renderers/TextOnlyRenderer.test.d.ts +1 -0
  24. package/dist/cjs/shortcuts/KeyBinding.test.d.ts +1 -0
  25. package/dist/cjs/shortcuts/KeyboardShortcutManager.test.d.ts +1 -0
  26. package/dist/cjs/toolbar/EdgeToolbar.test.d.ts +1 -0
  27. package/dist/cjs/tools/Eraser.test.d.ts +1 -0
  28. package/dist/cjs/tools/FindTool.test.d.ts +1 -0
  29. package/dist/cjs/tools/InputFilter/InputPipeline.test.d.ts +1 -0
  30. package/dist/cjs/tools/PanZoom.test.d.ts +1 -0
  31. package/dist/cjs/tools/Pen.test.d.ts +1 -0
  32. package/dist/cjs/tools/SelectionTool/SelectionTool.test.d.ts +1 -0
  33. package/dist/cjs/tools/UndoRedoShortcut.test.d.ts +1 -0
  34. package/dist/cjs/util/ReactiveValue.test.d.ts +1 -0
  35. package/dist/cjs/version.js +1 -1
  36. package/dist/cjs/version.test.d.ts +1 -0
  37. package/dist/mjs/Editor.loadFrom.test.d.ts +1 -0
  38. package/dist/mjs/Editor.mjs +1 -1
  39. package/dist/mjs/Editor.test.d.ts +1 -0
  40. package/dist/mjs/Editor.toSVG.test.d.ts +1 -0
  41. package/dist/mjs/EditorImage.test.d.ts +1 -0
  42. package/dist/mjs/EventDispatcher.test.d.ts +1 -0
  43. package/dist/mjs/SVGLoader.test.d.ts +1 -0
  44. package/dist/mjs/UndoRedoHistory.test.d.ts +1 -0
  45. package/dist/mjs/commands/uniteCommands.test.d.ts +1 -0
  46. package/dist/mjs/components/AbstractComponent.transformBy.test.d.ts +1 -0
  47. package/dist/mjs/components/BackgroundComponent.test.d.ts +1 -0
  48. package/dist/mjs/components/Stroke.test.d.ts +1 -0
  49. package/dist/mjs/components/TextComponent.test.d.ts +1 -0
  50. package/dist/mjs/components/UnknownSVGObject.test.d.ts +1 -0
  51. package/dist/mjs/components/builders/FreehandLineBuilder.test.d.ts +1 -0
  52. package/dist/mjs/localizations/getLocalizationTable.test.d.ts +1 -0
  53. package/dist/mjs/rendering/RenderingStyle.test.d.ts +1 -0
  54. package/dist/mjs/rendering/caching/CacheRecord.test.d.ts +1 -0
  55. package/dist/mjs/rendering/caching/RenderingCache.test.d.ts +1 -0
  56. package/dist/mjs/rendering/renderers/DummyRenderer.test.d.ts +1 -0
  57. package/dist/mjs/rendering/renderers/TextOnlyRenderer.test.d.ts +1 -0
  58. package/dist/mjs/shortcuts/KeyBinding.test.d.ts +1 -0
  59. package/dist/mjs/shortcuts/KeyboardShortcutManager.test.d.ts +1 -0
  60. package/dist/mjs/toolbar/EdgeToolbar.test.d.ts +1 -0
  61. package/dist/mjs/tools/Eraser.test.d.ts +1 -0
  62. package/dist/mjs/tools/FindTool.test.d.ts +1 -0
  63. package/dist/mjs/tools/InputFilter/InputPipeline.test.d.ts +1 -0
  64. package/dist/mjs/tools/PanZoom.test.d.ts +1 -0
  65. package/dist/mjs/tools/Pen.test.d.ts +1 -0
  66. package/dist/mjs/tools/SelectionTool/SelectionTool.test.d.ts +1 -0
  67. package/dist/mjs/tools/UndoRedoShortcut.test.d.ts +1 -0
  68. package/dist/mjs/util/ReactiveValue.test.d.ts +1 -0
  69. package/dist/mjs/version.mjs +1 -1
  70. package/dist/mjs/version.test.d.ts +1 -0
  71. package/dist-test/test_imports/package-lock.json +13 -0
  72. package/dist-test/test_imports/package.json +12 -0
  73. package/dist-test/test_imports/test-imports.js +11 -0
  74. package/dist-test/test_imports/test-require.cjs +14 -0
  75. package/package.json +2 -2
  76. package/src/Editor.loadFrom.test.ts +24 -0
  77. package/src/Editor.test.ts +107 -0
  78. package/src/Editor.toSVG.test.ts +294 -0
  79. package/src/Editor.ts +1443 -0
  80. package/src/EditorImage.test.ts +117 -0
  81. package/src/EditorImage.ts +609 -0
  82. package/src/EventDispatcher.test.ts +123 -0
  83. package/src/EventDispatcher.ts +72 -0
  84. package/src/Pointer.ts +183 -0
  85. package/src/SVGLoader.test.ts +114 -0
  86. package/src/SVGLoader.ts +672 -0
  87. package/src/UndoRedoHistory.test.ts +34 -0
  88. package/src/UndoRedoHistory.ts +102 -0
  89. package/src/Viewport.ts +322 -0
  90. package/src/bundle/bundled.ts +7 -0
  91. package/src/commands/Command.ts +45 -0
  92. package/src/commands/Duplicate.ts +75 -0
  93. package/src/commands/Erase.ts +95 -0
  94. package/src/commands/SerializableCommand.ts +49 -0
  95. package/src/commands/UnresolvedCommand.ts +37 -0
  96. package/src/commands/invertCommand.ts +58 -0
  97. package/src/commands/lib.ts +16 -0
  98. package/src/commands/localization.ts +47 -0
  99. package/src/commands/uniteCommands.test.ts +23 -0
  100. package/src/commands/uniteCommands.ts +140 -0
  101. package/src/components/AbstractComponent.transformBy.test.ts +23 -0
  102. package/src/components/AbstractComponent.ts +383 -0
  103. package/src/components/BackgroundComponent.test.ts +44 -0
  104. package/src/components/BackgroundComponent.ts +348 -0
  105. package/src/components/ImageComponent.ts +176 -0
  106. package/src/components/RestylableComponent.ts +161 -0
  107. package/src/components/SVGGlobalAttributesObject.ts +79 -0
  108. package/src/components/Stroke.test.ts +137 -0
  109. package/src/components/Stroke.ts +294 -0
  110. package/src/components/TextComponent.test.ts +202 -0
  111. package/src/components/TextComponent.ts +429 -0
  112. package/src/components/UnknownSVGObject.test.ts +10 -0
  113. package/src/components/UnknownSVGObject.ts +60 -0
  114. package/src/components/builders/ArrowBuilder.ts +106 -0
  115. package/src/components/builders/CircleBuilder.ts +100 -0
  116. package/src/components/builders/FreehandLineBuilder.test.ts +24 -0
  117. package/src/components/builders/FreehandLineBuilder.ts +210 -0
  118. package/src/components/builders/LineBuilder.ts +77 -0
  119. package/src/components/builders/PressureSensitiveFreehandLineBuilder.ts +453 -0
  120. package/src/components/builders/RectangleBuilder.ts +73 -0
  121. package/src/components/builders/types.ts +15 -0
  122. package/src/components/lib.ts +31 -0
  123. package/src/components/localization.ts +24 -0
  124. package/src/components/util/StrokeSmoother.ts +302 -0
  125. package/src/components/util/describeComponentList.ts +18 -0
  126. package/src/dialogs/makeAboutDialog.ts +82 -0
  127. package/src/inputEvents.ts +143 -0
  128. package/src/lib.ts +91 -0
  129. package/src/localization.ts +34 -0
  130. package/src/localizations/de.ts +146 -0
  131. package/src/localizations/en.ts +8 -0
  132. package/src/localizations/es.ts +74 -0
  133. package/src/localizations/getLocalizationTable.test.ts +27 -0
  134. package/src/localizations/getLocalizationTable.ts +74 -0
  135. package/src/rendering/Display.ts +247 -0
  136. package/src/rendering/RenderablePathSpec.ts +88 -0
  137. package/src/rendering/RenderingStyle.test.ts +68 -0
  138. package/src/rendering/RenderingStyle.ts +55 -0
  139. package/src/rendering/TextRenderingStyle.ts +55 -0
  140. package/src/rendering/caching/CacheRecord.test.ts +48 -0
  141. package/src/rendering/caching/CacheRecord.ts +76 -0
  142. package/src/rendering/caching/CacheRecordManager.ts +71 -0
  143. package/src/rendering/caching/RenderingCache.test.ts +43 -0
  144. package/src/rendering/caching/RenderingCache.ts +66 -0
  145. package/src/rendering/caching/RenderingCacheNode.ts +404 -0
  146. package/src/rendering/caching/testUtils.ts +35 -0
  147. package/src/rendering/caching/types.ts +34 -0
  148. package/src/rendering/lib.ts +8 -0
  149. package/src/rendering/localization.ts +20 -0
  150. package/src/rendering/renderers/AbstractRenderer.ts +232 -0
  151. package/src/rendering/renderers/CanvasRenderer.ts +312 -0
  152. package/src/rendering/renderers/DummyRenderer.test.ts +41 -0
  153. package/src/rendering/renderers/DummyRenderer.ts +142 -0
  154. package/src/rendering/renderers/SVGRenderer.ts +434 -0
  155. package/src/rendering/renderers/TextOnlyRenderer.test.ts +34 -0
  156. package/src/rendering/renderers/TextOnlyRenderer.ts +68 -0
  157. package/src/shortcuts/KeyBinding.test.ts +61 -0
  158. package/src/shortcuts/KeyBinding.ts +257 -0
  159. package/src/shortcuts/KeyboardShortcutManager.test.ts +95 -0
  160. package/src/shortcuts/KeyboardShortcutManager.ts +163 -0
  161. package/src/shortcuts/lib.ts +3 -0
  162. package/src/testing/createEditor.ts +11 -0
  163. package/src/testing/getUniquePointerId.ts +18 -0
  164. package/src/testing/lib.ts +3 -0
  165. package/src/testing/sendPenEvent.ts +36 -0
  166. package/src/testing/sendTouchEvent.ts +71 -0
  167. package/src/toolbar/AbstractToolbar.ts +542 -0
  168. package/src/toolbar/DropdownToolbar.ts +220 -0
  169. package/src/toolbar/EdgeToolbar.test.ts +54 -0
  170. package/src/toolbar/EdgeToolbar.ts +543 -0
  171. package/src/toolbar/IconProvider.ts +861 -0
  172. package/src/toolbar/constants.ts +1 -0
  173. package/src/toolbar/lib.ts +6 -0
  174. package/src/toolbar/localization.ts +136 -0
  175. package/src/toolbar/types.ts +13 -0
  176. package/src/toolbar/widgets/ActionButtonWidget.ts +39 -0
  177. package/src/toolbar/widgets/BaseToolWidget.ts +81 -0
  178. package/src/toolbar/widgets/BaseWidget.ts +495 -0
  179. package/src/toolbar/widgets/DocumentPropertiesWidget.ts +250 -0
  180. package/src/toolbar/widgets/EraserToolWidget.ts +84 -0
  181. package/src/toolbar/widgets/HandToolWidget.ts +239 -0
  182. package/src/toolbar/widgets/InsertImageWidget.ts +248 -0
  183. package/src/toolbar/widgets/OverflowWidget.ts +92 -0
  184. package/src/toolbar/widgets/PenToolWidget.ts +369 -0
  185. package/src/toolbar/widgets/SelectionToolWidget.ts +195 -0
  186. package/src/toolbar/widgets/TextToolWidget.ts +149 -0
  187. package/src/toolbar/widgets/components/makeColorInput.ts +184 -0
  188. package/src/toolbar/widgets/components/makeFileInput.ts +128 -0
  189. package/src/toolbar/widgets/components/makeGridSelector.ts +179 -0
  190. package/src/toolbar/widgets/components/makeSeparator.ts +17 -0
  191. package/src/toolbar/widgets/components/makeThicknessSlider.ts +62 -0
  192. package/src/toolbar/widgets/keybindings.ts +19 -0
  193. package/src/toolbar/widgets/layout/DropdownLayoutManager.ts +262 -0
  194. package/src/toolbar/widgets/layout/EdgeToolbarLayoutManager.ts +71 -0
  195. package/src/toolbar/widgets/layout/types.ts +74 -0
  196. package/src/toolbar/widgets/lib.ts +13 -0
  197. package/src/tools/BaseTool.ts +169 -0
  198. package/src/tools/Eraser.test.ts +103 -0
  199. package/src/tools/Eraser.ts +173 -0
  200. package/src/tools/FindTool.test.ts +67 -0
  201. package/src/tools/FindTool.ts +153 -0
  202. package/src/tools/InputFilter/FunctionMapper.ts +17 -0
  203. package/src/tools/InputFilter/InputMapper.ts +41 -0
  204. package/src/tools/InputFilter/InputPipeline.test.ts +41 -0
  205. package/src/tools/InputFilter/InputPipeline.ts +34 -0
  206. package/src/tools/InputFilter/InputStabilizer.ts +254 -0
  207. package/src/tools/InputFilter/StrokeKeyboardControl.ts +104 -0
  208. package/src/tools/PanZoom.test.ts +339 -0
  209. package/src/tools/PanZoom.ts +525 -0
  210. package/src/tools/PasteHandler.ts +94 -0
  211. package/src/tools/Pen.test.ts +260 -0
  212. package/src/tools/Pen.ts +284 -0
  213. package/src/tools/PipetteTool.ts +84 -0
  214. package/src/tools/SelectionTool/SelectAllShortcutHandler.ts +29 -0
  215. package/src/tools/SelectionTool/Selection.ts +647 -0
  216. package/src/tools/SelectionTool/SelectionHandle.ts +142 -0
  217. package/src/tools/SelectionTool/SelectionTool.test.ts +370 -0
  218. package/src/tools/SelectionTool/SelectionTool.ts +510 -0
  219. package/src/tools/SelectionTool/TransformMode.ts +112 -0
  220. package/src/tools/SelectionTool/types.ts +11 -0
  221. package/src/tools/SoundUITool.ts +221 -0
  222. package/src/tools/TextTool.ts +339 -0
  223. package/src/tools/ToolController.ts +224 -0
  224. package/src/tools/ToolEnabledGroup.ts +14 -0
  225. package/src/tools/ToolSwitcherShortcut.ts +39 -0
  226. package/src/tools/ToolbarShortcutHandler.ts +39 -0
  227. package/src/tools/UndoRedoShortcut.test.ts +62 -0
  228. package/src/tools/UndoRedoShortcut.ts +24 -0
  229. package/src/tools/keybindings.ts +85 -0
  230. package/src/tools/lib.ts +22 -0
  231. package/src/tools/localization.ts +76 -0
  232. package/src/types.ts +151 -0
  233. package/src/util/ReactiveValue.test.ts +168 -0
  234. package/src/util/ReactiveValue.ts +241 -0
  235. package/src/util/assertions.ts +55 -0
  236. package/src/util/fileToBase64.ts +18 -0
  237. package/src/util/guessKeyCodeFromKey.ts +36 -0
  238. package/src/util/listPrefixMatch.ts +19 -0
  239. package/src/util/stopPropagationOfScrollingWheelEvents.ts +20 -0
  240. package/src/util/untilNextAnimationFrame.ts +9 -0
  241. package/src/util/waitForAll.ts +18 -0
  242. package/src/util/waitForTimeout.ts +9 -0
  243. package/src/version.test.ts +12 -0
  244. package/src/version.ts +3 -0
@@ -0,0 +1,260 @@
1
+
2
+ import PenTool from './Pen';
3
+ import { Mat33, Rect2, Vec2 } from '@js-draw/math';
4
+ import createEditor from '../testing/createEditor';
5
+ import { InputEvtType } from '../inputEvents';
6
+ import StrokeComponent from '../components/Stroke';
7
+ import { makeFreehandLineBuilder } from '../components/builders/FreehandLineBuilder';
8
+ import sendPenEvent from '../testing/sendPenEvent';
9
+ import sendTouchEvent from '../testing/sendTouchEvent';
10
+
11
+ describe('Pen', () => {
12
+ it('should draw horizontal lines', () => {
13
+ const editor = createEditor();
14
+ sendPenEvent(editor, InputEvtType.PointerDownEvt, Vec2.of(0, 0));
15
+ for (let i = 0; i < 10; i++) {
16
+ sendPenEvent(editor, InputEvtType.PointerMoveEvt, Vec2.of(i, 0));
17
+ jest.advanceTimersByTime(200);
18
+ }
19
+ sendPenEvent(editor, InputEvtType.PointerUpEvt, Vec2.of(200, 0));
20
+
21
+ const elems = editor.image.getElementsIntersectingRegion(new Rect2(0, 10, 10, -10));
22
+ expect(elems).toHaveLength(1);
23
+
24
+ // Account for stroke width
25
+ const tolerableError = 8;
26
+ expect(elems[0].getBBox().topLeft).objEq(Vec2.of(0, 0), tolerableError);
27
+ expect(elems[0].getBBox().bottomRight).objEq(Vec2.of(200, 0), tolerableError);
28
+ });
29
+
30
+ it('should draw vertical line', () => {
31
+ const editor = createEditor();
32
+ sendPenEvent(editor, InputEvtType.PointerDownEvt, Vec2.of(0, 0));
33
+ for (let i = 0; i < 10; i++) {
34
+ sendPenEvent(editor, InputEvtType.PointerMoveEvt, Vec2.of(0, i * 20));
35
+ jest.advanceTimersByTime(200);
36
+ }
37
+ sendPenEvent(editor, InputEvtType.PointerUpEvt, Vec2.of(0, 150));
38
+
39
+ const elems = editor.image.getElementsIntersectingRegion(Rect2.unitSquare);
40
+ expect(elems).toHaveLength(1);
41
+
42
+ expect(elems[0].getBBox().topLeft).objEq(Vec2.of(0, 0), 8); // ± 8
43
+ expect(elems[0].getBBox().bottomRight).objEq(Vec2.of(0, 175), 25); // ± 25
44
+ });
45
+
46
+ it('should draw vertical line with slight bend', () => {
47
+ const editor = createEditor();
48
+
49
+ sendPenEvent(editor, InputEvtType.PointerDownEvt, Vec2.of(417, 24));
50
+ jest.advanceTimersByTime(245);
51
+ sendPenEvent(editor, InputEvtType.PointerMoveEvt, Vec2.of(423, 197));
52
+ jest.advanceTimersByTime(20);
53
+ sendPenEvent(editor, InputEvtType.PointerMoveEvt, Vec2.of(423, 199));
54
+ jest.advanceTimersByTime(12);
55
+ sendPenEvent(editor, InputEvtType.PointerMoveEvt, Vec2.of(423, 201));
56
+ jest.advanceTimersByTime(40);
57
+ sendPenEvent(editor, InputEvtType.PointerMoveEvt, Vec2.of(423, 203));
58
+ jest.advanceTimersByTime(14);
59
+ sendPenEvent(editor, InputEvtType.PointerMoveEvt, Vec2.of(423, 206));
60
+ jest.advanceTimersByTime(35);
61
+ sendPenEvent(editor, InputEvtType.PointerMoveEvt, Vec2.of(423, 208));
62
+ jest.advanceTimersByTime(16);
63
+ sendPenEvent(editor, InputEvtType.PointerMoveEvt, Vec2.of(423, 211));
64
+ jest.advanceTimersByTime(51);
65
+ sendPenEvent(editor, InputEvtType.PointerMoveEvt, Vec2.of(423, 215));
66
+ jest.advanceTimersByTime(32);
67
+ sendPenEvent(editor, InputEvtType.PointerMoveEvt, Vec2.of(423, 218));
68
+ jest.advanceTimersByTime(30);
69
+ sendPenEvent(editor, InputEvtType.PointerMoveEvt, Vec2.of(423, 220));
70
+ jest.advanceTimersByTime(24);
71
+ sendPenEvent(editor, InputEvtType.PointerMoveEvt, Vec2.of(423, 222));
72
+ jest.advanceTimersByTime(14);
73
+ sendPenEvent(editor, InputEvtType.PointerMoveEvt, Vec2.of(423, 224));
74
+ jest.advanceTimersByTime(32);
75
+ sendPenEvent(editor, InputEvtType.PointerMoveEvt, Vec2.of(423, 227));
76
+ jest.advanceTimersByTime(17);
77
+ sendPenEvent(editor, InputEvtType.PointerMoveEvt, Vec2.of(423, 229));
78
+ jest.advanceTimersByTime(53);
79
+ sendPenEvent(editor, InputEvtType.PointerMoveEvt, Vec2.of(423, 234));
80
+ jest.advanceTimersByTime(34);
81
+ sendPenEvent(editor, InputEvtType.PointerMoveEvt, Vec2.of(423, 236));
82
+ jest.advanceTimersByTime(17);
83
+ sendPenEvent(editor, InputEvtType.PointerMoveEvt, Vec2.of(423, 238));
84
+ jest.advanceTimersByTime(39);
85
+ sendPenEvent(editor, InputEvtType.PointerMoveEvt, Vec2.of(423, 240));
86
+ jest.advanceTimersByTime(10);
87
+ sendPenEvent(editor, InputEvtType.PointerMoveEvt, Vec2.of(423, 243));
88
+ jest.advanceTimersByTime(34);
89
+ sendPenEvent(editor, InputEvtType.PointerMoveEvt, Vec2.of(423, 250));
90
+ jest.advanceTimersByTime(57);
91
+ sendPenEvent(editor, InputEvtType.PointerMoveEvt, Vec2.of(423, 252));
92
+ jest.advanceTimersByTime(8);
93
+ sendPenEvent(editor, InputEvtType.PointerMoveEvt, Vec2.of(422, 256));
94
+ jest.advanceTimersByTime(28);
95
+ sendPenEvent(editor, InputEvtType.PointerMoveEvt, Vec2.of(422, 258));
96
+ jest.advanceTimersByTime(21);
97
+ sendPenEvent(editor, InputEvtType.PointerMoveEvt, Vec2.of(421, 262));
98
+ jest.advanceTimersByTime(34);
99
+ sendPenEvent(editor, InputEvtType.PointerMoveEvt, Vec2.of(420, 264));
100
+ jest.advanceTimersByTime(5);
101
+ sendPenEvent(editor, InputEvtType.PointerMoveEvt, Vec2.of(420, 266));
102
+ jest.advanceTimersByTime(22);
103
+ sendPenEvent(editor, InputEvtType.PointerMoveEvt, Vec2.of(420, 268));
104
+ jest.advanceTimersByTime(22);
105
+ sendPenEvent(editor, InputEvtType.PointerMoveEvt, Vec2.of(420, 271));
106
+ jest.advanceTimersByTime(18);
107
+ sendPenEvent(editor, InputEvtType.PointerMoveEvt, Vec2.of(420, 274));
108
+ jest.advanceTimersByTime(33);
109
+ sendPenEvent(editor, InputEvtType.PointerMoveEvt, Vec2.of(420, 277));
110
+ jest.advanceTimersByTime(16);
111
+ sendPenEvent(editor, InputEvtType.PointerMoveEvt, Vec2.of(419, 279));
112
+ jest.advanceTimersByTime(36);
113
+ sendPenEvent(editor, InputEvtType.PointerMoveEvt, Vec2.of(419, 282));
114
+ jest.advanceTimersByTime(15);
115
+ sendPenEvent(editor, InputEvtType.PointerMoveEvt, Vec2.of(419, 284));
116
+ jest.advanceTimersByTime(48);
117
+ sendPenEvent(editor, InputEvtType.PointerMoveEvt, Vec2.of(419, 289));
118
+ jest.advanceTimersByTime(16);
119
+ sendPenEvent(editor, InputEvtType.PointerMoveEvt, Vec2.of(419, 291));
120
+ jest.advanceTimersByTime(31);
121
+ sendPenEvent(editor, InputEvtType.PointerMoveEvt, Vec2.of(419, 295));
122
+ jest.advanceTimersByTime(23);
123
+ sendPenEvent(editor, InputEvtType.PointerMoveEvt, Vec2.of(419, 301));
124
+ jest.advanceTimersByTime(31);
125
+ sendPenEvent(editor, InputEvtType.PointerMoveEvt, Vec2.of(419, 306));
126
+ jest.advanceTimersByTime(18);
127
+ sendPenEvent(editor, InputEvtType.PointerMoveEvt, Vec2.of(419, 308));
128
+ jest.advanceTimersByTime(20);
129
+ sendPenEvent(editor, InputEvtType.PointerMoveEvt, Vec2.of(419, 310));
130
+ jest.advanceTimersByTime(13);
131
+ sendPenEvent(editor, InputEvtType.PointerMoveEvt, Vec2.of(419, 313));
132
+ jest.advanceTimersByTime(17);
133
+ sendPenEvent(editor, InputEvtType.PointerMoveEvt, Vec2.of(419, 317));
134
+ jest.advanceTimersByTime(33);
135
+ sendPenEvent(editor, InputEvtType.PointerMoveEvt, Vec2.of(419, 321));
136
+ jest.advanceTimersByTime(15);
137
+ sendPenEvent(editor, InputEvtType.PointerMoveEvt, Vec2.of(419, 324));
138
+ jest.advanceTimersByTime(23);
139
+ sendPenEvent(editor, InputEvtType.PointerMoveEvt, Vec2.of(419, 326));
140
+ jest.advanceTimersByTime(14);
141
+ sendPenEvent(editor, InputEvtType.PointerMoveEvt, Vec2.of(419, 329));
142
+ jest.advanceTimersByTime(36);
143
+ sendPenEvent(editor, InputEvtType.PointerMoveEvt, Vec2.of(420, 333));
144
+ jest.advanceTimersByTime(8);
145
+ sendPenEvent(editor, InputEvtType.PointerMoveEvt, Vec2.of(420, 340));
146
+ sendPenEvent(editor, InputEvtType.PointerUpEvt, Vec2.of(420, 340));
147
+
148
+ const elems = editor.image.getElementsIntersectingRegion(new Rect2(0, 0, 1000, 1000));
149
+ expect(elems).toHaveLength(1);
150
+
151
+ expect(elems[0].getBBox().topLeft).objEq(Vec2.of(420, 24), 32); // ± 32
152
+ expect(elems[0].getBBox().bottomRight).objEq(Vec2.of(420, 340), 25); // ± 25
153
+ });
154
+
155
+ // if `mainEventIsPen` is false, tests with touch events.
156
+ const testEventCancelation = (mainEventIsPen: boolean) => {
157
+ const editor = createEditor();
158
+
159
+ expect(editor.image.getElementsIntersectingRegion(new Rect2(0, 0, 1000, 1000))).toHaveLength(0);
160
+
161
+ const sendMainEvent = mainEventIsPen ? sendPenEvent : sendTouchEvent;
162
+
163
+ // Start the drawing
164
+ const mainPointer = sendMainEvent(editor, InputEvtType.PointerDownEvt, Vec2.of(417, 24));
165
+ jest.advanceTimersByTime(245);
166
+ sendMainEvent(editor, InputEvtType.PointerMoveEvt, Vec2.of(423, 197));
167
+ jest.advanceTimersByTime(20);
168
+ sendMainEvent(editor, InputEvtType.PointerMoveEvt, Vec2.of(423, 199));
169
+ jest.advanceTimersByTime(12);
170
+ sendMainEvent(editor, InputEvtType.PointerMoveEvt, Vec2.of(423, 201));
171
+ jest.advanceTimersByTime(40);
172
+ sendMainEvent(editor, InputEvtType.PointerMoveEvt, Vec2.of(423, 203));
173
+ jest.advanceTimersByTime(14);
174
+
175
+ // Attempt to cancel the drawing
176
+ let firstPointer = sendTouchEvent(editor, InputEvtType.PointerDownEvt, Vec2.of(0, 0), [ mainPointer ]);
177
+ let secondPointer = sendTouchEvent(editor, InputEvtType.PointerDownEvt, Vec2.of(100, 0), [ firstPointer, mainPointer ]);
178
+
179
+ const maxIterations = 10;
180
+ for (let i = 0; i < maxIterations; i++) {
181
+ jest.advanceTimersByTime(100);
182
+
183
+ const point1 = Vec2.of(-i * 5, 0);
184
+ const point2 = Vec2.of(i * 5 + 100, 0);
185
+
186
+ const eventType = InputEvtType.PointerMoveEvt;
187
+ firstPointer = sendTouchEvent(editor, eventType, point1, [ secondPointer, mainPointer ]);
188
+ secondPointer = sendTouchEvent(editor, eventType, point2, [ firstPointer, mainPointer ]);
189
+
190
+ if (i === maxIterations - 1) {
191
+ jest.advanceTimersByTime(10);
192
+
193
+ sendTouchEvent(editor, InputEvtType.PointerUpEvt, point1, [ secondPointer, mainPointer ]);
194
+ sendTouchEvent(editor, InputEvtType.PointerUpEvt, point2, [ mainPointer ]);
195
+ }
196
+
197
+ jest.advanceTimersByTime(100);
198
+ }
199
+
200
+ // Finish the drawing
201
+ sendMainEvent(editor, InputEvtType.PointerMoveEvt, Vec2.of(420, 333), [firstPointer, secondPointer]);
202
+ jest.advanceTimersByTime(8);
203
+ sendMainEvent(editor, InputEvtType.PointerMoveEvt, Vec2.of(420, 340), [firstPointer, secondPointer]);
204
+ sendMainEvent(editor, InputEvtType.PointerUpEvt, Vec2.of(420, 340), [firstPointer, secondPointer]);
205
+
206
+ const elementsInDrawingArea = editor.image.getElementsIntersectingRegion(new Rect2(0, 0, 1000, 1000));
207
+ if (mainEventIsPen) {
208
+ expect(elementsInDrawingArea).toHaveLength(1);
209
+ } else {
210
+ expect(elementsInDrawingArea).toHaveLength(0);
211
+ }
212
+ };
213
+
214
+ it('pen events should not be cancelable by touch events', () => {
215
+ testEventCancelation(true);
216
+ });
217
+
218
+ it('touch events should be cancelable by touch events', () => {
219
+ testEventCancelation(false);
220
+ });
221
+
222
+ it('ctrl+z should finalize then undo the current stroke', async () => {
223
+ const editor = createEditor();
224
+
225
+ expect(editor.history.undoStackSize).toBe(0);
226
+
227
+ sendPenEvent(editor, InputEvtType.PointerDownEvt, Vec2.of(10, 10));
228
+ jest.advanceTimersByTime(100);
229
+ sendPenEvent(editor, InputEvtType.PointerMoveEvt, Vec2.of(20, 10));
230
+
231
+ const ctrlKeyDown = true;
232
+ editor.sendKeyboardEvent(InputEvtType.KeyPressEvent, 'z', ctrlKeyDown);
233
+
234
+ // Stroke should have been undone
235
+ expect(editor.history.redoStackSize).toBe(1);
236
+
237
+ // Lifting the pointer up shouldn't clear the redo stack.
238
+ sendPenEvent(editor, InputEvtType.PointerUpEvt, Vec2.of(420, 340));
239
+ expect(editor.history.redoStackSize).toBe(1);
240
+ });
241
+
242
+ it('holding ctrl should snap the stroke to grid', () => {
243
+ const editor = createEditor();
244
+ editor.viewport.resetTransform(Mat33.identity);
245
+
246
+ const penTool = editor.toolController.getMatchingTools(PenTool)[0];
247
+ penTool.setStrokeFactory(makeFreehandLineBuilder);
248
+
249
+ sendPenEvent(editor, InputEvtType.PointerDownEvt, Vec2.of(0.1, 0.1));
250
+ jest.advanceTimersByTime(100);
251
+ sendPenEvent(editor, InputEvtType.PointerMoveEvt, Vec2.of(10.1, 10.1));
252
+ sendPenEvent(editor, InputEvtType.PointerUpEvt, Vec2.of(10.1, 10.1));
253
+
254
+ const allElems = editor.image.getAllElements();
255
+ expect(allElems).toHaveLength(1);
256
+
257
+ const firstStroke = allElems[0] as StrokeComponent;
258
+ expect(firstStroke.getPath().bbox).objEq(new Rect2(0, 0, 10, 10));
259
+ });
260
+ });
@@ -0,0 +1,284 @@
1
+ import { Color4 } from '@js-draw/math';
2
+ import Editor from '../Editor';
3
+ import EditorImage from '../EditorImage';
4
+ import Pointer, { PointerDevice } from '../Pointer';
5
+ import { makeFreehandLineBuilder } from '../components/builders/FreehandLineBuilder';
6
+ import { EditorEventType, StrokeDataPoint } from '../types';
7
+ import { KeyPressEvent, PointerEvt } from '../inputEvents';
8
+ import BaseTool from './BaseTool';
9
+ import { ComponentBuilder, ComponentBuilderFactory } from '../components/builders/types';
10
+ import { undoKeyboardShortcutId } from './keybindings';
11
+ import { decreaseSizeKeyboardShortcutId, increaseSizeKeyboardShortcutId } from './keybindings';
12
+ import InputStabilizer from './InputFilter/InputStabilizer';
13
+ import { MutableReactiveValue, ReactiveValue } from '../util/ReactiveValue';
14
+
15
+ export interface PenStyle {
16
+ readonly color: Color4;
17
+ readonly thickness: number;
18
+ readonly factory: ComponentBuilderFactory;
19
+ }
20
+
21
+ export default class Pen extends BaseTool {
22
+ protected builder: ComponentBuilder|null = null;
23
+ private lastPoint: StrokeDataPoint|null = null;
24
+ private startPoint: StrokeDataPoint|null = null;
25
+ private currentDeviceType: PointerDevice|null = null;
26
+ private styleValue: MutableReactiveValue<PenStyle>;
27
+ private style: PenStyle;
28
+
29
+ public constructor(
30
+ private editor: Editor,
31
+ description: string,
32
+ style: Partial<PenStyle>,
33
+ ) {
34
+ super(editor.notifier, description);
35
+
36
+ this.styleValue = ReactiveValue.fromInitialValue<PenStyle>({
37
+ factory: makeFreehandLineBuilder,
38
+ color: Color4.blue,
39
+ thickness: 4,
40
+ ...style,
41
+ });
42
+
43
+ this.styleValue.onUpdateAndNow(newValue => {
44
+ this.style = newValue;
45
+ this.noteUpdated();
46
+ });
47
+ }
48
+
49
+ private getPressureMultiplier() {
50
+ const thickness = this.style.thickness;
51
+ return 1 / this.editor.viewport.getScaleFactor() * thickness;
52
+ }
53
+
54
+ // Converts a `pointer` to a `StrokeDataPoint`.
55
+ protected toStrokePoint(pointer: Pointer): StrokeDataPoint {
56
+ const minPressure = 0.3;
57
+ let pressure = Math.max(pointer.pressure ?? 1.0, minPressure);
58
+
59
+ if (!isFinite(pressure)) {
60
+ console.warn('Non-finite pressure!', pointer);
61
+ pressure = minPressure;
62
+ }
63
+ console.assert(isFinite(pointer.canvasPos.length()), 'Non-finite canvas position!');
64
+ console.assert(isFinite(pointer.screenPos.length()), 'Non-finite screen position!');
65
+ console.assert(isFinite(pointer.timeStamp), 'Non-finite timeStamp on pointer!');
66
+
67
+ const pos = pointer.canvasPos;
68
+
69
+ return {
70
+ pos,
71
+ width: pressure * this.getPressureMultiplier(),
72
+ color: this.style.color,
73
+ time: pointer.timeStamp,
74
+ };
75
+ }
76
+
77
+ // Displays the stroke that is currently being built with the display's `wetInkRenderer`.
78
+ protected previewStroke() {
79
+ this.editor.clearWetInk();
80
+ this.builder?.preview(this.editor.display.getWetInkRenderer());
81
+ }
82
+
83
+ // Throws if no stroke builder exists.
84
+ protected addPointToStroke(point: StrokeDataPoint) {
85
+ if (!this.builder) {
86
+ throw new Error('No stroke is currently being generated.');
87
+ }
88
+ this.builder.addPoint(point);
89
+ this.lastPoint = point;
90
+ this.previewStroke();
91
+ }
92
+
93
+ public override onPointerDown(event: PointerEvt): boolean {
94
+ const { current, allPointers } = event;
95
+ const isEraser = current.device === PointerDevice.Eraser;
96
+
97
+ let anyDeviceIsStylus = false;
98
+ for (const pointer of allPointers) {
99
+ if (pointer.device === PointerDevice.Pen) {
100
+ anyDeviceIsStylus = true;
101
+ break;
102
+ }
103
+ }
104
+
105
+ // Avoid canceling an existing stroke
106
+ if (this.builder && !this.eventCanCancelStroke(event)) {
107
+ return true;
108
+ }
109
+
110
+ if ((allPointers.length === 1 && !isEraser) || anyDeviceIsStylus) {
111
+ this.startPoint = this.toStrokePoint(current);
112
+ this.builder = this.style.factory(this.startPoint, this.editor.viewport);
113
+ this.currentDeviceType = current.device;
114
+ return true;
115
+ }
116
+
117
+ return false;
118
+ }
119
+
120
+ private eventCanCancelStroke(event: PointerEvt) {
121
+ // If there has been a delay since the last input event,
122
+ // it's always okay to cancel
123
+ const lastInputTime = this.lastPoint?.time ?? 0;
124
+ if (event.current.timeStamp - lastInputTime > 1000) {
125
+ return true;
126
+ }
127
+
128
+ const isPenStroke = this.currentDeviceType === PointerDevice.Pen;
129
+ const isTouchEvent = event.current.device === PointerDevice.Touch;
130
+
131
+ // Don't allow pen strokes to be cancelled by touch events.
132
+ if (isPenStroke && isTouchEvent) {
133
+ return false;
134
+ }
135
+
136
+ return true;
137
+ }
138
+
139
+ public override eventCanBeDeliveredToNonActiveTool(event: PointerEvt) {
140
+ return this.eventCanCancelStroke(event);
141
+ }
142
+
143
+ public override onPointerMove({ current }: PointerEvt): void {
144
+ if (!this.builder) return;
145
+ if (current.device !== this.currentDeviceType) return;
146
+
147
+ this.addPointToStroke(this.toStrokePoint(current));
148
+ }
149
+
150
+ public override onPointerUp({ current }: PointerEvt) {
151
+ if (!this.builder) return false;
152
+ if (current.device !== this.currentDeviceType) {
153
+ // this.builder still exists, so we're handling events from another
154
+ // device type.
155
+ return true;
156
+ }
157
+
158
+ // onPointerUp events can have zero pressure. Use the last pressure instead.
159
+ const currentPoint = this.toStrokePoint(current);
160
+ const strokePoint = {
161
+ ...currentPoint,
162
+ width: this.lastPoint?.width ?? currentPoint.width,
163
+ };
164
+
165
+ this.addPointToStroke(strokePoint);
166
+
167
+ if (current.isPrimary) {
168
+ this.finalizeStroke();
169
+ }
170
+
171
+ return false;
172
+ }
173
+
174
+ public override onGestureCancel() {
175
+ this.builder = null;
176
+ this.editor.clearWetInk();
177
+ }
178
+
179
+ private finalizeStroke() {
180
+ if (this.builder) {
181
+ const stroke = this.builder.build();
182
+ this.previewStroke();
183
+
184
+ if (stroke.getBBox().area > 0) {
185
+ const canFlatten = true;
186
+ const action = EditorImage.addElement(stroke, canFlatten);
187
+ this.editor.dispatch(action);
188
+ } else {
189
+ console.warn('Pen: Not adding empty stroke', stroke, 'to the canvas.');
190
+ }
191
+ }
192
+ this.builder = null;
193
+ this.lastPoint = null;
194
+ this.editor.clearWetInk();
195
+ }
196
+
197
+ private noteUpdated() {
198
+ this.editor.notifier.dispatch(EditorEventType.ToolUpdated, {
199
+ kind: EditorEventType.ToolUpdated,
200
+ tool: this,
201
+ });
202
+ }
203
+
204
+ public setColor(color: Color4): void {
205
+ if (color.toHexString() !== this.style.color.toHexString()) {
206
+ this.styleValue.set({
207
+ ...this.style,
208
+ color,
209
+ });
210
+ }
211
+ }
212
+
213
+ public setThickness(thickness: number) {
214
+ if (thickness !== this.style.thickness) {
215
+ this.styleValue.set({
216
+ ...this.style,
217
+ thickness,
218
+ });
219
+ }
220
+ }
221
+
222
+ public setStrokeFactory(factory: ComponentBuilderFactory) {
223
+ if (factory !== this.style.factory) {
224
+ this.styleValue.set({
225
+ ...this.style,
226
+ factory,
227
+ });
228
+ }
229
+ }
230
+
231
+ public setHasStabilization(hasStabilization: boolean) {
232
+ const hasInputMapper = !!this.getInputMapper();
233
+
234
+ // TODO: Currently, this assumes that there is no other input mapper.
235
+ if (hasStabilization === hasInputMapper) {
236
+ return;
237
+ }
238
+
239
+ if (hasInputMapper) {
240
+ this.setInputMapper(null);
241
+ } else {
242
+ this.setInputMapper(new InputStabilizer(this.editor.viewport));
243
+ }
244
+ this.noteUpdated();
245
+ }
246
+
247
+ public getThickness() { return this.style.thickness; }
248
+ public getColor() { return this.style.color; }
249
+ public getStrokeFactory() { return this.style.factory; }
250
+ public getStyleValue() { return this.styleValue; }
251
+
252
+ public override setEnabled(enabled: boolean): void {
253
+ super.setEnabled(enabled);
254
+ }
255
+
256
+ public override onKeyPress(event: KeyPressEvent): boolean {
257
+ const shortcuts = this.editor.shortcuts;
258
+
259
+ // Ctrl+Z: End the stroke so that it can be undone/redone.
260
+ const isCtrlZ = shortcuts.matchesShortcut(undoKeyboardShortcutId, event);
261
+ if (this.builder && isCtrlZ) {
262
+ this.finalizeStroke();
263
+
264
+ // Return false: Allow other listeners to handle the event (e.g.
265
+ // undo/redo).
266
+ return false;
267
+ }
268
+
269
+ let newThickness: number|undefined;
270
+ if (shortcuts.matchesShortcut(decreaseSizeKeyboardShortcutId, event)) {
271
+ newThickness = this.getThickness() * 2/3;
272
+ } else if (shortcuts.matchesShortcut(increaseSizeKeyboardShortcutId, event)) {
273
+ newThickness = this.getThickness() * 3/2;
274
+ }
275
+
276
+ if (newThickness !== undefined) {
277
+ newThickness = Math.min(Math.max(1, newThickness), 256);
278
+ this.setThickness(newThickness);
279
+ return true;
280
+ }
281
+
282
+ return false;
283
+ }
284
+ }
@@ -0,0 +1,84 @@
1
+ // @internal @packageDocumentation
2
+
3
+ import { Color4 } from '@js-draw/math';
4
+ import Editor from '../Editor';
5
+ import { PointerEvt } from '../inputEvents';
6
+ import BaseTool from './BaseTool';
7
+
8
+ type ColorListener = (color: Color4|null)=>void;
9
+
10
+ /**
11
+ * A tool used internally to pick colors from the canvas.
12
+ *
13
+ * When color selection is in progress, the `pipette--color-selection-in-progress` class
14
+ * is added to the root element. This can be used by themes.
15
+ *
16
+ * @internal
17
+ */
18
+ export default class PipetteTool extends BaseTool {
19
+ private colorPreviewListener: ColorListener|null = null;
20
+ private colorSelectListener: ColorListener|null = null;
21
+
22
+ public constructor(
23
+ private editor: Editor,
24
+ description: string,
25
+ ) {
26
+ super(editor.notifier, description);
27
+
28
+ this.enabledValue().onUpdateAndNow(() => {
29
+ this.updateSelectingStatus();
30
+ });
31
+ }
32
+
33
+ // Ensures that the root editor element correctly reflects whether color selection
34
+ // is in progress.
35
+ private updateSelectingStatus() {
36
+ const className = 'pipette--color-selection-in-progress';
37
+
38
+ if (this.isEnabled() && this.colorSelectListener && this.colorPreviewListener) {
39
+ this.editor.getRootElement().classList.add(className);
40
+ }
41
+ else {
42
+ this.editor.getRootElement().classList.remove(className);
43
+ }
44
+ }
45
+
46
+ public setColorListener(
47
+ colorPreviewListener: ColorListener,
48
+
49
+ // Called when the gesture ends -- when the user has selected a color.
50
+ colorSelectListener: ColorListener,
51
+ ) {
52
+ this.colorPreviewListener = colorPreviewListener;
53
+ this.colorSelectListener = colorSelectListener;
54
+
55
+ this.updateSelectingStatus();
56
+ }
57
+
58
+ public clearColorListener() {
59
+ this.colorPreviewListener = null;
60
+ this.colorSelectListener = null;
61
+
62
+ this.updateSelectingStatus();
63
+ }
64
+
65
+ public override onPointerDown({ current, allPointers }: PointerEvt): boolean {
66
+ if (this.colorPreviewListener && allPointers.length === 1) {
67
+ this.colorPreviewListener(this.editor.display.getColorAt(current.screenPos));
68
+ return true;
69
+ }
70
+ return false;
71
+ }
72
+
73
+ public override onPointerMove({ current }: PointerEvt): void {
74
+ this.colorPreviewListener?.(this.editor.display.getColorAt(current.screenPos));
75
+ }
76
+
77
+ public override onPointerUp({ current }: PointerEvt): void {
78
+ this.colorSelectListener?.(this.editor.display.getColorAt(current.screenPos));
79
+ }
80
+
81
+ public override onGestureCancel(): void {
82
+ this.colorSelectListener?.(null);
83
+ }
84
+ }
@@ -0,0 +1,29 @@
1
+ import Editor from '../../Editor';
2
+ import { KeyPressEvent } from '../../inputEvents';
3
+ import BaseTool from '../BaseTool';
4
+ import { selectAllKeyboardShortcut } from '../keybindings';
5
+ import SelectionTool from './SelectionTool';
6
+
7
+ // Handles ctrl+a: Select all
8
+ export default class SelectAllShortcutHandler extends BaseTool {
9
+ public constructor(private editor: Editor) {
10
+ super(editor.notifier, editor.localization.selectAllTool);
11
+ }
12
+
13
+ // @internal
14
+ public override onKeyPress(event: KeyPressEvent): boolean {
15
+ if (this.editor.shortcuts.matchesShortcut(selectAllKeyboardShortcut, event)) {
16
+ const selectionTools = this.editor.toolController.getMatchingTools(SelectionTool);
17
+
18
+ if (selectionTools.length > 0) {
19
+ const selectionTool = selectionTools[0];
20
+ selectionTool.setEnabled(true);
21
+ selectionTool.setSelection(this.editor.image.getAllElements());
22
+
23
+ return true;
24
+ }
25
+ }
26
+
27
+ return false;
28
+ }
29
+ }