js-draw 0.1.11 → 0.2.0

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 (220) hide show
  1. package/.eslintrc.js +1 -0
  2. package/.firebaserc +5 -0
  3. package/.github/workflows/firebase-hosting-merge.yml +25 -0
  4. package/.github/workflows/firebase-hosting-pull-request.yml +22 -0
  5. package/.github/workflows/github-pages.yml +52 -0
  6. package/CHANGELOG.md +13 -0
  7. package/README.md +11 -6
  8. package/dist/bundle.js +1 -1
  9. package/dist/src/Color4.d.ts +19 -0
  10. package/dist/src/Color4.js +24 -3
  11. package/dist/src/Editor.d.ts +133 -4
  12. package/dist/src/Editor.js +124 -27
  13. package/dist/src/EditorImage.d.ts +8 -3
  14. package/dist/src/EditorImage.js +42 -26
  15. package/dist/src/EventDispatcher.d.ts +18 -0
  16. package/dist/src/EventDispatcher.js +19 -4
  17. package/dist/src/Pointer.d.ts +1 -1
  18. package/dist/src/Pointer.js +4 -3
  19. package/dist/src/SVGLoader.d.ts +1 -1
  20. package/dist/src/SVGLoader.js +14 -6
  21. package/dist/src/UndoRedoHistory.js +15 -2
  22. package/dist/src/Viewport.d.ts +8 -25
  23. package/dist/src/Viewport.js +18 -10
  24. package/dist/src/bundle/bundled.d.ts +1 -2
  25. package/dist/src/bundle/bundled.js +1 -2
  26. package/dist/src/commands/Command.d.ts +2 -2
  27. package/dist/src/commands/Command.js +4 -4
  28. package/dist/src/commands/Duplicate.d.ts +2 -2
  29. package/dist/src/commands/Duplicate.js +4 -5
  30. package/dist/src/commands/Erase.d.ts +2 -2
  31. package/dist/src/commands/Erase.js +7 -6
  32. package/dist/src/commands/SerializableCommand.d.ts +4 -5
  33. package/dist/src/commands/SerializableCommand.js +12 -4
  34. package/dist/src/commands/invertCommand.d.ts +4 -0
  35. package/dist/src/commands/invertCommand.js +44 -0
  36. package/dist/src/commands/lib.d.ts +6 -0
  37. package/dist/src/commands/lib.js +6 -0
  38. package/dist/src/commands/localization.d.ts +2 -1
  39. package/dist/src/commands/localization.js +1 -0
  40. package/dist/src/components/AbstractComponent.d.ts +16 -11
  41. package/dist/src/components/AbstractComponent.js +28 -17
  42. package/dist/src/components/SVGGlobalAttributesObject.d.ts +4 -4
  43. package/dist/src/components/SVGGlobalAttributesObject.js +8 -2
  44. package/dist/src/components/Stroke.d.ts +16 -6
  45. package/dist/src/components/Stroke.js +12 -9
  46. package/dist/src/components/Text.d.ts +5 -5
  47. package/dist/src/components/Text.js +9 -9
  48. package/dist/src/components/UnknownSVGObject.d.ts +4 -4
  49. package/dist/src/components/UnknownSVGObject.js +7 -2
  50. package/dist/src/components/builders/ArrowBuilder.d.ts +1 -1
  51. package/dist/src/components/builders/ArrowBuilder.js +1 -1
  52. package/dist/src/components/builders/FreehandLineBuilder.d.ts +8 -3
  53. package/dist/src/components/builders/FreehandLineBuilder.js +142 -71
  54. package/dist/src/components/builders/LineBuilder.d.ts +1 -1
  55. package/dist/src/components/builders/LineBuilder.js +1 -1
  56. package/dist/src/components/builders/RectangleBuilder.d.ts +1 -1
  57. package/dist/src/components/builders/RectangleBuilder.js +3 -3
  58. package/dist/src/components/builders/types.d.ts +1 -1
  59. package/dist/src/components/lib.d.ts +4 -0
  60. package/dist/src/components/lib.js +4 -0
  61. package/dist/src/lib.d.ts +25 -0
  62. package/dist/src/lib.js +25 -0
  63. package/dist/src/localization.d.ts +1 -0
  64. package/dist/src/localization.js +5 -1
  65. package/dist/src/localizations/es.js +1 -1
  66. package/dist/src/{geometry → math}/LineSegment2.d.ts +0 -0
  67. package/dist/src/{geometry → math}/LineSegment2.js +0 -0
  68. package/dist/src/math/Mat33.d.ts +78 -0
  69. package/dist/src/{geometry → math}/Mat33.js +48 -20
  70. package/dist/src/{geometry → math}/Path.d.ts +2 -1
  71. package/dist/src/{geometry → math}/Path.js +59 -52
  72. package/dist/src/{geometry → math}/Rect2.d.ts +2 -2
  73. package/dist/src/{geometry → math}/Rect2.js +0 -0
  74. package/dist/src/{geometry → math}/Vec2.d.ts +0 -0
  75. package/dist/src/{geometry → math}/Vec2.js +0 -0
  76. package/dist/src/math/Vec3.d.ts +96 -0
  77. package/dist/src/{geometry → math}/Vec3.js +63 -15
  78. package/dist/src/math/lib.d.ts +7 -0
  79. package/dist/src/math/lib.js +7 -0
  80. package/dist/src/math/rounding.d.ts +3 -0
  81. package/dist/src/math/rounding.js +121 -0
  82. package/dist/src/rendering/Display.d.ts +47 -1
  83. package/dist/src/rendering/Display.js +60 -15
  84. package/dist/src/rendering/caching/CacheRecord.d.ts +3 -2
  85. package/dist/src/rendering/caching/CacheRecord.js +4 -1
  86. package/dist/src/rendering/caching/CacheRecordManager.d.ts +5 -4
  87. package/dist/src/rendering/caching/CacheRecordManager.js +16 -4
  88. package/dist/src/rendering/caching/RenderingCache.d.ts +2 -3
  89. package/dist/src/rendering/caching/RenderingCache.js +10 -11
  90. package/dist/src/rendering/caching/RenderingCacheNode.d.ts +2 -1
  91. package/dist/src/rendering/caching/RenderingCacheNode.js +18 -7
  92. package/dist/src/rendering/caching/testUtils.js +1 -1
  93. package/dist/src/rendering/caching/types.d.ts +2 -4
  94. package/dist/src/rendering/localization.d.ts +2 -0
  95. package/dist/src/rendering/localization.js +2 -0
  96. package/dist/src/rendering/renderers/AbstractRenderer.d.ts +4 -4
  97. package/dist/src/rendering/renderers/AbstractRenderer.js +2 -2
  98. package/dist/src/rendering/renderers/CanvasRenderer.d.ts +4 -4
  99. package/dist/src/rendering/renderers/CanvasRenderer.js +2 -2
  100. package/dist/src/rendering/renderers/DummyRenderer.d.ts +4 -4
  101. package/dist/src/rendering/renderers/DummyRenderer.js +1 -1
  102. package/dist/src/rendering/renderers/SVGRenderer.d.ts +3 -3
  103. package/dist/src/rendering/renderers/SVGRenderer.js +8 -2
  104. package/dist/src/rendering/renderers/TextOnlyRenderer.d.ts +5 -3
  105. package/dist/src/rendering/renderers/TextOnlyRenderer.js +13 -3
  106. package/dist/src/toolbar/HTMLToolbar.js +1 -0
  107. package/dist/src/toolbar/icons.d.ts +3 -0
  108. package/dist/src/toolbar/icons.js +142 -132
  109. package/dist/src/toolbar/localization.d.ts +2 -1
  110. package/dist/src/toolbar/localization.js +2 -1
  111. package/dist/src/toolbar/makeColorInput.js +3 -2
  112. package/dist/src/toolbar/widgets/ActionButtonWidget.d.ts +13 -0
  113. package/dist/src/toolbar/widgets/ActionButtonWidget.js +21 -0
  114. package/dist/src/toolbar/widgets/BaseWidget.js +2 -0
  115. package/dist/src/toolbar/widgets/HandToolWidget.js +3 -3
  116. package/dist/src/toolbar/widgets/PenWidget.js +1 -0
  117. package/dist/src/toolbar/widgets/SelectionWidget.d.ts +0 -1
  118. package/dist/src/toolbar/widgets/SelectionWidget.js +23 -30
  119. package/dist/src/tools/Eraser.js +1 -1
  120. package/dist/src/tools/PanZoom.d.ts +1 -1
  121. package/dist/src/tools/PanZoom.js +24 -14
  122. package/dist/src/tools/Pen.d.ts +1 -2
  123. package/dist/src/tools/Pen.js +8 -1
  124. package/dist/src/tools/PipetteTool.js +1 -0
  125. package/dist/src/tools/SelectionTool.d.ts +3 -3
  126. package/dist/src/tools/SelectionTool.js +51 -28
  127. package/dist/src/tools/TextTool.js +1 -1
  128. package/dist/src/types.d.ts +21 -10
  129. package/dist/src/types.js +7 -5
  130. package/firebase.json +16 -0
  131. package/package.json +118 -101
  132. package/src/Color4.ts +23 -2
  133. package/src/Editor.ts +181 -37
  134. package/src/EditorImage.test.ts +2 -4
  135. package/src/EditorImage.ts +46 -28
  136. package/src/EventDispatcher.ts +21 -6
  137. package/src/Pointer.ts +4 -3
  138. package/src/SVGLoader.ts +14 -6
  139. package/src/UndoRedoHistory.ts +18 -2
  140. package/src/Viewport.ts +23 -18
  141. package/src/bundle/bundled.ts +1 -2
  142. package/src/commands/Command.ts +5 -5
  143. package/src/commands/Duplicate.ts +4 -5
  144. package/src/commands/Erase.ts +7 -6
  145. package/src/commands/SerializableCommand.ts +17 -9
  146. package/src/commands/invertCommand.ts +51 -0
  147. package/src/commands/lib.ts +14 -0
  148. package/src/commands/localization.ts +3 -1
  149. package/src/components/AbstractComponent.ts +35 -24
  150. package/src/components/SVGGlobalAttributesObject.ts +11 -4
  151. package/src/components/Stroke.test.ts +4 -6
  152. package/src/components/Stroke.ts +15 -11
  153. package/src/components/Text.test.ts +2 -2
  154. package/src/components/Text.ts +9 -10
  155. package/src/components/UnknownSVGObject.ts +10 -4
  156. package/src/components/builders/ArrowBuilder.ts +2 -2
  157. package/src/components/builders/FreehandLineBuilder.ts +190 -80
  158. package/src/components/builders/LineBuilder.ts +2 -2
  159. package/src/components/builders/RectangleBuilder.ts +3 -3
  160. package/src/components/builders/types.ts +1 -1
  161. package/src/components/lib.ts +9 -0
  162. package/src/lib.ts +28 -0
  163. package/src/localization.ts +6 -0
  164. package/src/localizations/es.ts +2 -1
  165. package/src/{geometry → math}/LineSegment2.test.ts +0 -0
  166. package/src/{geometry → math}/LineSegment2.ts +0 -0
  167. package/src/{geometry → math}/Mat33.test.ts +0 -0
  168. package/src/{geometry → math}/Mat33.ts +48 -20
  169. package/src/{geometry → math}/Path.fromString.test.ts +0 -0
  170. package/src/{geometry → math}/Path.test.ts +0 -0
  171. package/src/{geometry → math}/Path.toString.test.ts +11 -2
  172. package/src/{geometry → math}/Path.ts +61 -58
  173. package/src/{geometry → math}/Rect2.test.ts +0 -0
  174. package/src/{geometry → math}/Rect2.ts +2 -2
  175. package/src/{geometry → math}/Vec2.test.ts +0 -0
  176. package/src/{geometry → math}/Vec2.ts +0 -0
  177. package/src/{geometry → math}/Vec3.test.ts +0 -0
  178. package/src/{geometry → math}/Vec3.ts +64 -16
  179. package/src/math/lib.ts +15 -0
  180. package/src/math/rounding.test.ts +40 -0
  181. package/src/math/rounding.ts +147 -0
  182. package/src/rendering/Display.ts +63 -15
  183. package/src/rendering/caching/CacheRecord.test.ts +3 -3
  184. package/src/rendering/caching/CacheRecord.ts +6 -2
  185. package/src/rendering/caching/CacheRecordManager.ts +34 -8
  186. package/src/rendering/caching/RenderingCache.test.ts +3 -3
  187. package/src/rendering/caching/RenderingCache.ts +11 -16
  188. package/src/rendering/caching/RenderingCacheNode.ts +23 -7
  189. package/src/rendering/caching/testUtils.ts +1 -1
  190. package/src/rendering/caching/types.ts +2 -7
  191. package/src/rendering/localization.ts +4 -0
  192. package/src/rendering/renderers/AbstractRenderer.ts +4 -4
  193. package/src/rendering/renderers/CanvasRenderer.ts +5 -5
  194. package/src/rendering/renderers/DummyRenderer.test.ts +2 -2
  195. package/src/rendering/renderers/DummyRenderer.ts +4 -4
  196. package/src/rendering/renderers/SVGRenderer.ts +10 -4
  197. package/src/rendering/renderers/TextOnlyRenderer.ts +17 -6
  198. package/src/toolbar/HTMLToolbar.ts +1 -0
  199. package/src/toolbar/icons.ts +157 -137
  200. package/src/toolbar/localization.ts +4 -2
  201. package/src/toolbar/makeColorInput.ts +3 -2
  202. package/src/toolbar/toolbar.css +1 -1
  203. package/src/toolbar/widgets/ActionButtonWidget.ts +31 -0
  204. package/src/toolbar/widgets/BaseWidget.ts +2 -0
  205. package/src/toolbar/widgets/HandToolWidget.ts +3 -3
  206. package/src/toolbar/widgets/PenWidget.ts +2 -0
  207. package/src/toolbar/widgets/SelectionWidget.ts +46 -41
  208. package/src/tools/Eraser.ts +2 -2
  209. package/src/tools/PanZoom.ts +28 -17
  210. package/src/tools/Pen.ts +11 -2
  211. package/src/tools/PipetteTool.ts +2 -0
  212. package/src/tools/SelectionTool.test.ts +2 -4
  213. package/src/tools/SelectionTool.ts +52 -24
  214. package/src/tools/TextTool.ts +2 -2
  215. package/src/tools/UndoRedoShortcut.test.ts +1 -1
  216. package/src/types.ts +23 -7
  217. package/tsconfig.json +4 -1
  218. package/typedoc.json +20 -0
  219. package/dist/src/geometry/Mat33.d.ts +0 -32
  220. package/dist/src/geometry/Vec3.d.ts +0 -34
package/src/Editor.ts CHANGED
@@ -1,3 +1,21 @@
1
+ /**
2
+ * The main entrypoint for the full editor.
3
+ *
4
+ * @example
5
+ * To create an editor with a toolbar,
6
+ * ```
7
+ * const editor = new Editor(document.body);
8
+ *
9
+ * const toolbar = editor.addToolbar();
10
+ * toolbar.addActionButton('Save', () => {
11
+ * const saveData = editor.toSVG().outerHTML;
12
+ * // Do something with saveData...
13
+ * });
14
+ * ```
15
+ *
16
+ * @packageDocumentation
17
+ */
18
+
1
19
 
2
20
  import EditorImage from './EditorImage';
3
21
  import ToolController from './tools/ToolController';
@@ -6,8 +24,8 @@ import Command from './commands/Command';
6
24
  import UndoRedoHistory from './UndoRedoHistory';
7
25
  import Viewport from './Viewport';
8
26
  import EventDispatcher from './EventDispatcher';
9
- import { Point2, Vec2 } from './geometry/Vec2';
10
- import Vec3 from './geometry/Vec3';
27
+ import { Point2, Vec2 } from './math/Vec2';
28
+ import Vec3 from './math/Vec3';
11
29
  import HTMLToolbar from './toolbar/HTMLToolbar';
12
30
  import { RenderablePathSpec } from './rendering/renderers/AbstractRenderer';
13
31
  import Display, { RenderingMode } from './rendering/Display';
@@ -15,49 +33,117 @@ import SVGRenderer from './rendering/renderers/SVGRenderer';
15
33
  import Color4 from './Color4';
16
34
  import SVGLoader from './SVGLoader';
17
35
  import Pointer from './Pointer';
18
- import Mat33 from './geometry/Mat33';
19
- import Rect2 from './geometry/Rect2';
36
+ import Mat33 from './math/Mat33';
37
+ import Rect2 from './math/Rect2';
20
38
  import { EditorLocalization } from './localization';
21
39
  import getLocalizationTable from './localizations/getLocalizationTable';
22
40
 
23
41
  export interface EditorSettings {
24
- // Defaults to RenderingMode.CanvasRenderer
42
+ /** Defaults to `RenderingMode.CanvasRenderer` */
25
43
  renderingMode: RenderingMode,
26
44
 
27
- // Uses a default English localization if a translation is not given.
45
+ /** Uses a default English localization if a translation is not given. */
28
46
  localization: Partial<EditorLocalization>,
29
47
 
30
- // True if touchpad/mousewheel scrolling should scroll the editor instead of the document.
31
- // This does not include pinch-zoom events.
32
- // Defaults to true.
48
+ /**
49
+ * `true` if touchpad/mousewheel scrolling should scroll the editor instead of the document.
50
+ * This does not include pinch-zoom events.
51
+ * Defaults to true.
52
+ */
33
53
  wheelEventsEnabled: boolean|'only-if-focused';
34
54
 
55
+ /** Minimum zoom fraction (e.g. 0.5 → 50% zoom). */
35
56
  minZoom: number,
36
57
  maxZoom: number,
37
58
  }
38
59
 
60
+ // { @inheritDoc Editor! }
39
61
  export class Editor {
40
62
  // Wrapper around the viewport and toolbar
41
63
  private container: HTMLElement;
42
64
  private renderingRegion: HTMLElement;
43
65
 
44
- public history: UndoRedoHistory;
45
66
  public display: Display;
67
+
68
+ /**
69
+ * Handles undo/redo.
70
+ *
71
+ * @example
72
+ * ```
73
+ * const editor = new Editor(document.body);
74
+ *
75
+ * // Do something undoable.
76
+ * // ...
77
+ *
78
+ * // Undo the last action
79
+ * editor.history.undo();
80
+ * ```
81
+ */
82
+ public history: UndoRedoHistory;
83
+
84
+ /**
85
+ * Data structure for adding/removing/querying objects in the image.
86
+ *
87
+ * @example
88
+ * ```
89
+ * const editor = new Editor(document.body);
90
+ *
91
+ * // Create a path.
92
+ * const stroke = new Stroke([
93
+ * Path.fromString('M0,0 L30,30 z').toRenderable({ fill: Color4.black }),
94
+ * ]);
95
+ * const addElementCommand = editor.image.addElement(stroke);
96
+ *
97
+ * // Add the stroke to the editor
98
+ * editor.dispatch(addElementCommand);
99
+ * ```
100
+ */
46
101
  public image: EditorImage;
47
102
 
48
- // Viewport for the exported/imported image
103
+ /** Viewport for the exported/imported image. */
49
104
  private importExportViewport: Viewport;
105
+
106
+ /** @internal */
50
107
  public localization: EditorLocalization;
51
108
 
52
109
  public viewport: Viewport;
53
110
  public toolController: ToolController;
111
+
112
+ /**
113
+ * Global event dispatcher/subscriber.
114
+ * @see {@link types.EditorEventType}
115
+ */
54
116
  public notifier: EditorNotifier;
55
117
 
56
118
  private loadingWarning: HTMLElement;
57
119
  private accessibilityAnnounceArea: HTMLElement;
120
+ private accessibilityControlArea: HTMLTextAreaElement;
58
121
 
59
122
  private settings: EditorSettings;
60
123
 
124
+ /**
125
+ * @example
126
+ * ```
127
+ * const container = document.body;
128
+ *
129
+ * // Create an editor
130
+ * const editor = new Editor(container, {
131
+ * // 2e-10 and 1e12 are the default values for minimum/maximum zoom.
132
+ * minZoom: 2e-10,
133
+ * maxZoom: 1e12,
134
+ * });
135
+ *
136
+ * // Add the default toolbar
137
+ * const toolbar = editor.addToolbar();
138
+ * toolbar.addActionButton({
139
+ * label: 'Save'
140
+ * icon: createSaveIcon(),
141
+ * }, () => {
142
+ * const saveData = editor.toSVG().outerHTML;
143
+ * // Do something with saveData
144
+ * });
145
+ * ```
146
+ */
61
147
  public constructor(
62
148
  parent: HTMLElement,
63
149
  settings: Partial<EditorSettings> = {},
@@ -86,15 +172,23 @@ export class Editor {
86
172
  this.loadingWarning.ariaLive = 'polite';
87
173
  this.container.appendChild(this.loadingWarning);
88
174
 
175
+ this.accessibilityControlArea = document.createElement('textarea');
176
+ this.accessibilityControlArea.setAttribute('placeholder', this.localization.accessibilityInputInstructions);
177
+ this.accessibilityControlArea.style.opacity = '0';
178
+ this.accessibilityControlArea.style.width = '0';
179
+ this.accessibilityControlArea.style.height = '0';
180
+ this.accessibilityControlArea.style.position = 'absolute';
181
+
89
182
  this.accessibilityAnnounceArea = document.createElement('div');
90
- this.accessibilityAnnounceArea.ariaLive = 'assertive';
183
+ this.accessibilityAnnounceArea.setAttribute('aria-live', 'assertive');
91
184
  this.accessibilityAnnounceArea.className = 'accessibilityAnnouncement';
92
185
  this.container.appendChild(this.accessibilityAnnounceArea);
93
186
 
94
187
  this.renderingRegion.style.touchAction = 'none';
95
188
  this.renderingRegion.className = 'imageEditorRenderArea';
189
+ this.renderingRegion.appendChild(this.accessibilityControlArea);
96
190
  this.renderingRegion.setAttribute('tabIndex', '0');
97
- this.renderingRegion.ariaLabel = this.localization.imageEditor;
191
+ this.renderingRegion.setAttribute('alt', '');
98
192
 
99
193
  this.notifier = new EventDispatcher();
100
194
  this.importExportViewport = new Viewport(this.notifier);
@@ -136,14 +230,19 @@ export class Editor {
136
230
  });
137
231
  }
138
232
 
139
- // Returns a reference to this' container.
140
- // Example usage:
141
- // editor.getRootElement().style.height = '500px';
233
+ /**
234
+ * @returns a reference to the editor's container.
235
+ *
236
+ * @example
237
+ * ```
238
+ * editor.getRootElement().style.height = '500px';
239
+ * ```
240
+ */
142
241
  public getRootElement(): HTMLElement {
143
242
  return this.container;
144
243
  }
145
244
 
146
- // [fractionLoaded] should be a number from 0 to 1, where 1 represents completely loaded.
245
+ /** @param fractionLoaded - should be a number from 0 to 1, where 1 represents completely loaded. */
147
246
  public showLoadingWarning(fractionLoaded: number) {
148
247
  const loadingPercent = Math.round(fractionLoaded * 100);
149
248
  this.loadingWarning.innerText = this.localization.loading(loadingPercent);
@@ -156,10 +255,23 @@ export class Editor {
156
255
  this.announceForAccessibility(this.localization.doneLoading);
157
256
  }
158
257
 
258
+ private previousAccessibilityAnnouncement: string = '';
259
+
260
+ // Announce `message` for screen readers. If `message` is the same as the previous
261
+ // message, it is re-announced.
159
262
  public announceForAccessibility(message: string) {
263
+ // Force re-announcing an announcement if announced again.
264
+ if (message === this.previousAccessibilityAnnouncement) {
265
+ message = message + '. ';
266
+ }
160
267
  this.accessibilityAnnounceArea.innerText = message;
268
+ this.previousAccessibilityAnnouncement = message;
161
269
  }
162
270
 
271
+ /**
272
+ * Creates a toolbar. If `defaultLayout` is true, default buttons are used.
273
+ * @returns a reference to the toolbar.
274
+ */
163
275
  public addToolbar(defaultLayout: boolean = true): HTMLToolbar {
164
276
  const toolbar = new HTMLToolbar(this, this.container, this.localization);
165
277
 
@@ -319,13 +431,19 @@ export class Editor {
319
431
  });
320
432
  this.queueRerender();
321
433
  });
434
+
435
+ this.accessibilityControlArea.addEventListener('input', () => {
436
+ this.accessibilityControlArea.value = '';
437
+ });
322
438
  }
323
439
 
324
- // Adds event listners for keypresses to [elem] and forwards those events to the
325
- // editor.
440
+ /** Adds event listners for keypresses to `elem` and forwards those events to the editor. */
326
441
  public handleKeyEventsFrom(elem: HTMLElement) {
327
442
  elem.addEventListener('keydown', evt => {
328
- if (this.toolController.dispatchInputEvent({
443
+ if (evt.key === 't' || evt.key === 'T') {
444
+ evt.preventDefault();
445
+ this.display.rerenderAsText();
446
+ } else if (this.toolController.dispatchInputEvent({
329
447
  kind: InputEvtType.KeyPressEvent,
330
448
  key: evt.key,
331
449
  ctrlKey: evt.ctrlKey,
@@ -333,7 +451,7 @@ export class Editor {
333
451
  evt.preventDefault();
334
452
  } else if (evt.key === 'Escape') {
335
453
  this.renderingRegion.blur();
336
- }
454
+ }
337
455
  });
338
456
 
339
457
  elem.addEventListener('keyup', evt => {
@@ -347,7 +465,7 @@ export class Editor {
347
465
  });
348
466
  }
349
467
 
350
- // Adds to history by default
468
+ /** `apply` a command. `command` will be announced for accessibility. */
351
469
  public dispatch(command: Command, addToHistory: boolean = true) {
352
470
  if (addToHistory) {
353
471
  // .push applies [command] to this
@@ -356,10 +474,24 @@ export class Editor {
356
474
  command.apply(this);
357
475
  }
358
476
 
359
- this.announceForAccessibility(command.description(this.localization));
477
+ this.announceForAccessibility(command.description(this, this.localization));
360
478
  }
361
479
 
362
- // Dispatches a command without announcing it. By default, does not add to history.
480
+ /**
481
+ * Dispatches a command without announcing it. By default, does not add to history.
482
+ * Use this to show finalized commands that don't need to have `announceForAccessibility`
483
+ * called.
484
+ *
485
+ * Prefer `command.apply(editor)` for incomplete commands. `dispatchNoAnnounce` may allow
486
+ * clients to listen for the application of commands (e.g. `SerializableCommand`s so they can
487
+ * be sent across the network), while `apply` does not.
488
+ *
489
+ * @example
490
+ * ```
491
+ * const addToHistory = false;
492
+ * editor.dispatchNoAnnounce(editor.viewport.zoomTo(someRectangle), addToHistory);
493
+ * ```
494
+ */
363
495
  public dispatchNoAnnounce(command: Command, addToHistory: boolean = false) {
364
496
  if (addToHistory) {
365
497
  this.history.push(command);
@@ -368,11 +500,13 @@ export class Editor {
368
500
  }
369
501
  }
370
502
 
371
- // Apply a large transformation in chunks.
372
- // If [apply] is false, the commands are unapplied.
373
- // Triggers a re-render after each [updateChunkSize]-sized group of commands
374
- // has been applied.
375
- private async asyncApplyOrUnapplyCommands(
503
+ /**
504
+ * Apply a large transformation in chunks.
505
+ * If `apply` is `false`, the commands are unapplied.
506
+ * Triggers a re-render after each `updateChunkSize`-sized group of commands
507
+ * has been applied.
508
+ */
509
+ public async asyncApplyOrUnapplyCommands(
376
510
  commands: Command[], apply: boolean, updateChunkSize: number
377
511
  ) {
378
512
  this.display.setDraftMode(true);
@@ -401,23 +535,27 @@ export class Editor {
401
535
  this.hideLoadingWarning();
402
536
  }
403
537
 
538
+ // @see {@link #asyncApplyOrUnapplyCommands }
404
539
  public asyncApplyCommands(commands: Command[], chunkSize: number) {
405
540
  return this.asyncApplyOrUnapplyCommands(commands, true, chunkSize);
406
541
  }
407
542
 
543
+ // @see {@link #asyncApplyOrUnapplyCommands }
408
544
  public asyncUnapplyCommands(commands: Command[], chunkSize: number) {
409
545
  return this.asyncApplyOrUnapplyCommands(commands, false, chunkSize);
410
546
  }
411
547
 
412
548
  private announceUndoCallback = (command: Command) => {
413
- this.announceForAccessibility(this.localization.undoAnnouncement(command.description(this.localization)));
549
+ this.announceForAccessibility(this.localization.undoAnnouncement(command.description(this, this.localization)));
414
550
  };
415
551
 
416
552
  private announceRedoCallback = (command: Command) => {
417
- this.announceForAccessibility(this.localization.redoAnnouncement(command.description(this.localization)));
553
+ this.announceForAccessibility(this.localization.redoAnnouncement(command.description(this, this.localization)));
418
554
  };
419
555
 
420
556
  private rerenderQueued: boolean = false;
557
+ // Schedule a re-render for some time in the near future. Does not schedule an additional
558
+ // re-render if a re-render is already queued.
421
559
  public queueRerender() {
422
560
  if (!this.rerenderQueued) {
423
561
  this.rerenderQueued = true;
@@ -464,11 +602,13 @@ export class Editor {
464
602
  this.display.getWetInkRenderer().clear();
465
603
  }
466
604
 
467
- // Focuses the region used for text input
605
+ // Focuses the region used for text input/key commands.
468
606
  public focus() {
469
607
  this.renderingRegion.focus();
470
608
  }
471
609
 
610
+ // Creates an element that will be positioned on top of the dry/wet ink
611
+ // renderers.
472
612
  public createHTMLOverlay(overlay: HTMLElement) {
473
613
  overlay.classList.add('overlay');
474
614
  this.container.appendChild(overlay);
@@ -487,7 +627,7 @@ export class Editor {
487
627
  }
488
628
 
489
629
  // Dispatch a pen event to the currently selected tool.
490
- // Intented for unit tests.
630
+ // Intended primarially for unit tests.
491
631
  public sendPenEvent(
492
632
  eventType: InputEvtType.PointerDownEvt|InputEvtType.PointerMoveEvt|InputEvtType.PointerUpEvt,
493
633
  point: Point2,
@@ -567,7 +707,7 @@ export class Editor {
567
707
  return this.importExportViewport.visibleRect;
568
708
  }
569
709
 
570
- // Resize the output SVG
710
+ // Resize the output SVG to match `imageRect`.
571
711
  public setImportExportRect(imageRect: Rect2): Command {
572
712
  const origSize = this.importExportViewport.visibleRect.size;
573
713
  const origTransform = this.importExportViewport.canvasToScreenTransform;
@@ -587,14 +727,18 @@ export class Editor {
587
727
  editor.queueRerender();
588
728
  }
589
729
 
590
- public description(localizationTable: EditorLocalization) {
730
+ public description(_editor: Editor, localizationTable: EditorLocalization) {
591
731
  return localizationTable.resizeOutputCommand(imageRect);
592
732
  }
593
733
  };
594
734
  }
595
735
 
596
- // Alias for loadFrom(SVGLoader.fromString).
597
- // This is particularly useful when accessing a bundled version of the editor.
736
+ /**
737
+ * Alias for loadFrom(SVGLoader.fromString).
738
+ *
739
+ * This is particularly useful when accessing a bundled version of the editor,
740
+ * where `SVGLoader.fromString` is unavailable.
741
+ */
598
742
  public async loadFromSVG(svgData: string) {
599
743
  const loader = SVGLoader.fromString(svgData);
600
744
  await this.loadFrom(loader);
@@ -1,9 +1,7 @@
1
- /* @jest-environment jsdom */
2
-
3
1
  import EditorImage from './EditorImage';
4
2
  import Stroke from './components/Stroke';
5
- import { Vec2 } from './geometry/Vec2';
6
- import Path, { PathCommandType } from './geometry/Path';
3
+ import { Vec2 } from './math/Vec2';
4
+ import Path, { PathCommandType } from './math/Path';
7
5
  import Color4 from './Color4';
8
6
  import DummyRenderer from './rendering/renderers/DummyRenderer';
9
7
  import createEditor from './testing/createEditor';
@@ -1,13 +1,13 @@
1
1
  import Editor from './Editor';
2
2
  import AbstractRenderer from './rendering/renderers/AbstractRenderer';
3
- import Command from './commands/Command';
4
3
  import Viewport from './Viewport';
5
4
  import AbstractComponent from './components/AbstractComponent';
6
- import Rect2 from './geometry/Rect2';
5
+ import Rect2 from './math/Rect2';
7
6
  import { EditorLocalization } from './localization';
8
7
  import RenderingCache from './rendering/caching/RenderingCache';
9
8
  import SerializableCommand from './commands/SerializableCommand';
10
9
 
10
+ // @internal
11
11
  export const sortLeavesByZIndex = (leaves: Array<ImageNode>) => {
12
12
  leaves.sort((a, b) => a.getContent()!.getZIndex() - b.getContent()!.getZIndex());
13
13
  };
@@ -17,6 +17,7 @@ export default class EditorImage {
17
17
  private root: ImageNode;
18
18
  private componentsById: Record<string, AbstractComponent>;
19
19
 
20
+ // @internal
20
21
  public constructor() {
21
22
  this.root = new ImageNode();
22
23
  this.componentsById = {};
@@ -33,15 +34,17 @@ export default class EditorImage {
33
34
  return null;
34
35
  }
35
36
 
37
+ /** @internal */
36
38
  public renderWithCache(screenRenderer: AbstractRenderer, cache: RenderingCache, viewport: Viewport) {
37
39
  cache.render(screenRenderer, this.root, viewport);
38
40
  }
39
41
 
42
+ /** @internal */
40
43
  public render(renderer: AbstractRenderer, viewport: Viewport) {
41
44
  this.root.render(renderer, viewport.visibleRect);
42
45
  }
43
46
 
44
- // Renders all nodes, even ones not within the viewport
47
+ /** Renders all nodes, even ones not within the viewport. @internal */
45
48
  public renderAll(renderer: AbstractRenderer) {
46
49
  const leaves = this.root.getLeaves();
47
50
  sortLeavesByZIndex(leaves);
@@ -58,6 +61,7 @@ export default class EditorImage {
58
61
  return leaves.map(leaf => leaf.getContent()!);
59
62
  }
60
63
 
64
+ /** @internal */
61
65
  public onDestroyElement(elem: AbstractComponent) {
62
66
  delete this.componentsById[elem.getId()];
63
67
  }
@@ -71,7 +75,7 @@ export default class EditorImage {
71
75
  return this.root.addLeaf(elem);
72
76
  }
73
77
 
74
- public static addElement(elem: AbstractComponent, applyByFlattening: boolean = false): Command {
78
+ public static addElement(elem: AbstractComponent, applyByFlattening: boolean = false): SerializableCommand {
75
79
  return new EditorImage.AddElementCommand(elem, applyByFlattening);
76
80
  }
77
81
 
@@ -108,20 +112,21 @@ export default class EditorImage {
108
112
  editor.queueRerender();
109
113
  }
110
114
 
111
- public description(localization: EditorLocalization) {
115
+ public description(_editor: Editor, localization: EditorLocalization) {
112
116
  return localization.addElementAction(this.element.description(localization));
113
117
  }
114
118
 
115
- protected serializeToString() {
116
- return JSON.stringify({
119
+ protected serializeToJSON() {
120
+ return {
117
121
  elemData: this.element.serialize(),
118
- });
122
+ };
119
123
  }
120
124
 
121
125
  static {
122
- SerializableCommand.register('add-element', (data: string, _editor: Editor) => {
123
- const json = JSON.parse(data);
124
- const elem = AbstractComponent.deserialize(json.elemData);
126
+ SerializableCommand.register('add-element', (json: any, editor: Editor) => {
127
+ const id = json.elemData.id;
128
+ const foundElem = editor.image.lookupElement(id);
129
+ const elem = foundElem ?? AbstractComponent.deserialize(json.elemData);
125
130
  return new EditorImage.AddElementCommand(elem);
126
131
  });
127
132
  }
@@ -130,7 +135,7 @@ export default class EditorImage {
130
135
 
131
136
  type TooSmallToRenderCheck = (rect: Rect2)=> boolean;
132
137
 
133
- // TODO: Assign leaf nodes to CacheNodes. When leaf nodes are modified, the corresponding CacheNodes can be updated.
138
+ /** Part of the Editor's image. @internal */
134
139
  export class ImageNode {
135
140
  private content: AbstractComponent|null;
136
141
  private bbox: Rect2;
@@ -182,19 +187,29 @@ export class ImageNode {
182
187
  // Returns a list of `ImageNode`s with content (and thus no children).
183
188
  public getLeavesIntersectingRegion(region: Rect2, isTooSmall?: TooSmallToRenderCheck): ImageNode[] {
184
189
  const result: ImageNode[] = [];
190
+ let current: ImageNode|undefined;
191
+ const workList: ImageNode[] = [];
185
192
 
186
- // Don't render if too small
187
- if (isTooSmall?.(this.bbox)) {
188
- return [];
189
- }
193
+ workList.push(this);
194
+ const toNext = () => {
195
+ current = undefined;
190
196
 
191
- if (this.content !== null && this.getBBox().intersects(region)) {
192
- result.push(this);
193
- }
197
+ const next = workList.pop();
198
+ if (next && !isTooSmall?.(next.bbox)) {
199
+ current = next;
200
+
201
+ if (current.content !== null && current.getBBox().intersection(region)) {
202
+ result.push(current);
203
+ }
194
204
 
195
- const children = this.getChildrenIntersectingRegion(region);
196
- for (const child of children) {
197
- result.push(...child.getLeavesIntersectingRegion(region, isTooSmall));
205
+ workList.push(
206
+ ...current.getChildrenIntersectingRegion(region)
207
+ );
208
+ }
209
+ };
210
+
211
+ while (workList.length > 0) {
212
+ toNext();
198
213
  }
199
214
 
200
215
  return result;
@@ -239,15 +254,18 @@ export class ImageNode {
239
254
  // share a parent.
240
255
  const leafBBox = leaf.getBBox();
241
256
  if (leafBBox.containsRect(this.getBBox())) {
242
- // Create a node for this' children and for the new content..
243
257
  const nodeForNewLeaf = new ImageNode(this);
244
- const nodeForChildren = new ImageNode(this);
245
258
 
246
- nodeForChildren.children = this.children;
247
- this.children = [nodeForNewLeaf, nodeForChildren];
248
- nodeForChildren.recomputeBBox(true);
249
- nodeForChildren.updateParents();
259
+ if (this.children.length < this.targetChildCount) {
260
+ this.children.push(nodeForNewLeaf);
261
+ } else {
262
+ const nodeForChildren = new ImageNode(this);
250
263
 
264
+ nodeForChildren.children = this.children;
265
+ this.children = [nodeForNewLeaf, nodeForChildren];
266
+ nodeForChildren.recomputeBBox(true);
267
+ nodeForChildren.updateParents();
268
+ }
251
269
  return nodeForNewLeaf.addLeaf(leaf);
252
270
  }
253
271
 
@@ -1,13 +1,28 @@
1
- // Code shared with Joplin
1
+ /**
2
+ * Handles notifying listeners of events.
3
+ *
4
+ * `EventKeyType` is used to distinguish events (e.g. a `ClickEvent` vs a `TouchEvent`)
5
+ * while `EventMessageType` is the type of the data sent with an event (can be `void`).
6
+ *
7
+ * @example
8
+ * ```
9
+ * const dispatcher = new EventDispatcher<'event1'|'event2'|'event3', void>();
10
+ * dispatcher.on('event1', () => {
11
+ * console.log('Event 1 triggered.');
12
+ * });
13
+ * dispatcher.dispatch('event1');
14
+ * ```
15
+ *
16
+ * @packageDocumentation
17
+ */
18
+
19
+ // Code shared with Joplin (js-draw was originally intended to be part of Joplin).
2
20
 
3
21
  type Listener<Value> = (data: Value)=> void;
4
22
  type CallbackHandler<EventType> = (data: EventType)=> void;
5
23
 
6
- // EventKeyType is used to distinguish events (e.g. a 'ClickEvent' vs a 'TouchEvent')
7
- // while EventMessageType is the type of the data sent with an event (can be `void`)
24
+ // { @inheritDoc EventDispatcher! }
8
25
  export default class EventDispatcher<EventKeyType extends string|symbol|number, EventMessageType> {
9
- // Partial marks all fields as optional. To initialize with an empty object, this is required.
10
- // See https://stackoverflow.com/a/64526384
11
26
  private listeners: Partial<Record<EventKeyType, Array<Listener<EventMessageType>>>>;
12
27
  public constructor() {
13
28
  this.listeners = {};
@@ -38,7 +53,7 @@ export default class EventDispatcher<EventKeyType extends string|symbol|number,
38
53
  };
39
54
  }
40
55
 
41
- // Equivalent to calling .remove() on the object returned by .on
56
+ /** Removes an event listener. This is equivalent to calling `.remove()` on the object returned by `.on`. */
42
57
  public off(eventName: EventKeyType, callback: CallbackHandler<EventMessageType>) {
43
58
  const listeners = this.listeners[eventName];
44
59
  if (!listeners) return;
package/src/Pointer.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { Point2, Vec2 } from './geometry/Vec2';
1
+ import { Point2, Vec2 } from './math/Vec2';
2
2
  import Viewport from './Viewport';
3
3
 
4
4
  export enum PointerDevice {
@@ -11,7 +11,7 @@ export enum PointerDevice {
11
11
  }
12
12
 
13
13
  // Provides a snapshot containing information about a pointer. A Pointer
14
- // object is immutable --- it will not be updated when the pointer's information changes.
14
+ // object is immutable it will not be updated when the pointer's information changes.
15
15
  export default class Pointer {
16
16
  private constructor(
17
17
  // The (x, y) position of the pointer relative to the top-left corner
@@ -31,11 +31,12 @@ export default class Pointer {
31
31
  // Unique ID for the pointer
32
32
  public readonly id: number,
33
33
 
34
- // Numeric timestamp (milliseconds, as from (new Date).getTime())
34
+ // Numeric timestamp (milliseconds, as from `(new Date).getTime()`)
35
35
  public readonly timeStamp: number,
36
36
  ) {
37
37
  }
38
38
 
39
+ // Creates a Pointer from a DOM event.
39
40
  public static ofEvent(evt: PointerEvent, isDown: boolean, viewport: Viewport): Pointer {
40
41
  const screenPos = Vec2.of(evt.offsetX, evt.offsetY);
41
42
 
package/src/SVGLoader.ts CHANGED
@@ -4,10 +4,10 @@ import Stroke from './components/Stroke';
4
4
  import SVGGlobalAttributesObject from './components/SVGGlobalAttributesObject';
5
5
  import Text, { TextStyle } from './components/Text';
6
6
  import UnknownSVGObject from './components/UnknownSVGObject';
7
- import Mat33 from './geometry/Mat33';
8
- import Path from './geometry/Path';
9
- import Rect2 from './geometry/Rect2';
10
- import { Vec2 } from './geometry/Vec2';
7
+ import Mat33 from './math/Mat33';
8
+ import Path from './math/Path';
9
+ import Rect2 from './math/Rect2';
10
+ import { Vec2 } from './math/Vec2';
11
11
  import { RenderablePathSpec } from './rendering/renderers/AbstractRenderer';
12
12
  import RenderingStyle from './rendering/RenderingStyle';
13
13
  import { ComponentAddedListener, ImageLoader, OnDetermineExportRectListener, OnProgressListener } from './types';
@@ -210,8 +210,16 @@ export default class SVGLoader implements ImageLoader {
210
210
  transformProperty = elem.style.transform || 'none';
211
211
  }
212
212
 
213
- // Compute transform matrix
214
- let transform = Mat33.fromCSSMatrix(transformProperty);
213
+ // Compute transform matrix. Prefer the actual .style.transform
214
+ // to the computed stylesheet -- in some browsers, the computedStyles version
215
+ // can have lower precision.
216
+ let transform;
217
+ try {
218
+ transform = Mat33.fromCSSMatrix(elem.style.transform);
219
+ } catch(_e) {
220
+ transform = Mat33.fromCSSMatrix(transformProperty);
221
+ }
222
+
215
223
  const supportedAttrs = [];
216
224
  const elemX = elem.getAttribute('x');
217
225
  const elemY = elem.getAttribute('y');