js-draw 0.11.3 → 0.13.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 (91) hide show
  1. package/CHANGELOG.md +15 -0
  2. package/dist/bundle.js +1 -1
  3. package/dist/src/Color4.d.ts +13 -0
  4. package/dist/src/Color4.js +17 -0
  5. package/dist/src/Editor.d.ts +33 -18
  6. package/dist/src/Editor.js +26 -24
  7. package/dist/src/EditorImage.d.ts +12 -0
  8. package/dist/src/EditorImage.js +12 -0
  9. package/dist/src/Pointer.d.ts +1 -0
  10. package/dist/src/Pointer.js +8 -0
  11. package/dist/src/SVGLoader.d.ts +5 -0
  12. package/dist/src/SVGLoader.js +49 -36
  13. package/dist/src/Viewport.d.ts +30 -1
  14. package/dist/src/Viewport.js +39 -9
  15. package/dist/src/commands/invertCommand.js +1 -1
  16. package/dist/src/components/AbstractComponent.d.ts +20 -0
  17. package/dist/src/components/AbstractComponent.js +32 -2
  18. package/dist/src/lib.d.ts +6 -3
  19. package/dist/src/lib.js +4 -1
  20. package/dist/src/math/Mat33.d.ts +1 -1
  21. package/dist/src/math/Mat33.js +1 -1
  22. package/dist/src/rendering/Display.d.ts +9 -11
  23. package/dist/src/rendering/Display.js +12 -14
  24. package/dist/src/rendering/lib.d.ts +3 -0
  25. package/dist/src/rendering/lib.js +3 -0
  26. package/dist/src/rendering/renderers/DummyRenderer.js +2 -2
  27. package/dist/src/rendering/renderers/SVGRenderer.js +4 -0
  28. package/dist/src/toolbar/HTMLToolbar.d.ts +51 -0
  29. package/dist/src/toolbar/HTMLToolbar.js +63 -5
  30. package/dist/src/toolbar/IconProvider.d.ts +2 -2
  31. package/dist/src/toolbar/IconProvider.js +123 -35
  32. package/dist/src/toolbar/widgets/EraserToolWidget.d.ts +8 -1
  33. package/dist/src/toolbar/widgets/EraserToolWidget.js +45 -4
  34. package/dist/src/toolbar/widgets/PenToolWidget.js +2 -2
  35. package/dist/src/toolbar/widgets/SelectionToolWidget.js +12 -3
  36. package/dist/src/tools/Eraser.d.ts +10 -1
  37. package/dist/src/tools/Eraser.js +65 -13
  38. package/dist/src/tools/PanZoom.js +1 -1
  39. package/dist/src/tools/PasteHandler.d.ts +11 -4
  40. package/dist/src/tools/PasteHandler.js +12 -5
  41. package/dist/src/tools/Pen.d.ts +7 -2
  42. package/dist/src/tools/Pen.js +39 -6
  43. package/dist/src/tools/SelectionTool/Selection.d.ts +4 -1
  44. package/dist/src/tools/SelectionTool/Selection.js +64 -27
  45. package/dist/src/tools/SelectionTool/SelectionHandle.d.ts +3 -0
  46. package/dist/src/tools/SelectionTool/SelectionHandle.js +6 -0
  47. package/dist/src/tools/SelectionTool/SelectionTool.d.ts +3 -1
  48. package/dist/src/tools/SelectionTool/SelectionTool.js +56 -16
  49. package/dist/src/tools/TextTool.js +10 -6
  50. package/dist/src/tools/ToolSwitcherShortcut.d.ts +8 -0
  51. package/dist/src/tools/ToolSwitcherShortcut.js +9 -3
  52. package/dist/src/tools/UndoRedoShortcut.js +2 -4
  53. package/dist/src/types.d.ts +2 -2
  54. package/package.json +2 -2
  55. package/src/Color4.test.ts +11 -0
  56. package/src/Color4.ts +23 -0
  57. package/src/Editor.ts +39 -26
  58. package/src/EditorImage.ts +12 -0
  59. package/src/Pointer.ts +19 -0
  60. package/src/SVGLoader.ts +20 -15
  61. package/src/Viewport.ts +50 -11
  62. package/src/commands/invertCommand.ts +1 -1
  63. package/src/components/AbstractComponent.ts +52 -2
  64. package/src/lib.ts +6 -3
  65. package/src/math/Mat33.ts +1 -1
  66. package/src/rendering/Display.ts +12 -15
  67. package/src/rendering/RenderingStyle.ts +1 -1
  68. package/src/rendering/lib.ts +4 -0
  69. package/src/rendering/renderers/DummyRenderer.ts +2 -3
  70. package/src/rendering/renderers/SVGRenderer.ts +4 -0
  71. package/src/rendering/renderers/TextOnlyRenderer.ts +0 -1
  72. package/src/toolbar/HTMLToolbar.ts +81 -5
  73. package/src/toolbar/IconProvider.ts +132 -37
  74. package/src/toolbar/widgets/EraserToolWidget.ts +64 -5
  75. package/src/toolbar/widgets/PenToolWidget.ts +2 -2
  76. package/src/toolbar/widgets/SelectionToolWidget.ts +2 -2
  77. package/src/tools/Eraser.test.ts +79 -0
  78. package/src/tools/Eraser.ts +81 -17
  79. package/src/tools/PanZoom.ts +1 -1
  80. package/src/tools/PasteHandler.ts +12 -6
  81. package/src/tools/Pen.test.ts +44 -1
  82. package/src/tools/Pen.ts +53 -8
  83. package/src/tools/SelectionTool/Selection.ts +73 -23
  84. package/src/tools/SelectionTool/SelectionHandle.ts +9 -0
  85. package/src/tools/SelectionTool/SelectionTool.test.ts +138 -21
  86. package/src/tools/SelectionTool/SelectionTool.ts +70 -16
  87. package/src/tools/TextTool.ts +14 -8
  88. package/src/tools/ToolSwitcherShortcut.ts +10 -5
  89. package/src/tools/UndoRedoShortcut.ts +2 -5
  90. package/src/types.ts +2 -2
  91. package/typedoc.json +2 -2
@@ -20,6 +20,18 @@ export default class Color4 {
20
20
  static fromString(text: string): Color4;
21
21
  /** @returns true if `this` and `other` are approximately equal. */
22
22
  eq(other: Color4 | null | undefined): boolean;
23
+ /**
24
+ * If `fractionTo` is not in the range [0, 1], it will be clamped to the nearest number
25
+ * in that range. For example, `a.mix(b, -1)` is equivalent to `a.mix(b, 0)`.
26
+ *
27
+ * @returns a color `fractionTo` of the way from this color to `other`.
28
+ *
29
+ * @example
30
+ * ```ts
31
+ * Color4.ofRGB(1, 0, 0).mix(Color4.ofRGB(0, 1, 0), 0.1) // -> Color4(0.9, 0.1, 0)
32
+ * ```
33
+ */
34
+ mix(other: Color4, fractionTo: number): Color4;
23
35
  private hexString;
24
36
  /**
25
37
  * @returns a hexadecimal color string representation of `this`, in the form `#rrggbbaa`.
@@ -39,5 +51,6 @@ export default class Color4 {
39
51
  static yellow: Color4;
40
52
  static clay: Color4;
41
53
  static black: Color4;
54
+ static gray: Color4;
42
55
  static white: Color4;
43
56
  }
@@ -105,6 +105,22 @@ export default class Color4 {
105
105
  }
106
106
  return this.toHexString() === other.toHexString();
107
107
  }
108
+ /**
109
+ * If `fractionTo` is not in the range [0, 1], it will be clamped to the nearest number
110
+ * in that range. For example, `a.mix(b, -1)` is equivalent to `a.mix(b, 0)`.
111
+ *
112
+ * @returns a color `fractionTo` of the way from this color to `other`.
113
+ *
114
+ * @example
115
+ * ```ts
116
+ * Color4.ofRGB(1, 0, 0).mix(Color4.ofRGB(0, 1, 0), 0.1) // -> Color4(0.9, 0.1, 0)
117
+ * ```
118
+ */
119
+ mix(other, fractionTo) {
120
+ fractionTo = Math.min(Math.max(fractionTo, 0), 1);
121
+ const fractionOfThis = 1 - fractionTo;
122
+ return new Color4(this.r * fractionOfThis + other.r * fractionTo, this.g * fractionOfThis + other.g * fractionTo, this.b * fractionOfThis + other.b * fractionTo, this.a * fractionOfThis + other.a * fractionTo);
123
+ }
108
124
  /**
109
125
  * @returns a hexadecimal color string representation of `this`, in the form `#rrggbbaa`.
110
126
  *
@@ -146,4 +162,5 @@ Color4.purple = Color4.ofRGB(0.5, 0.2, 0.5);
146
162
  Color4.yellow = Color4.ofRGB(1, 1, 0.1);
147
163
  Color4.clay = Color4.ofRGB(0.8, 0.4, 0.2);
148
164
  Color4.black = Color4.ofRGB(0, 0, 0);
165
+ Color4.gray = Color4.ofRGB(0.5, 0.5, 0.5);
149
166
  Color4.white = Color4.ofRGB(1, 1, 1);
@@ -1,20 +1,3 @@
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
  import EditorImage from './EditorImage';
19
2
  import ToolController from './tools/ToolController';
20
3
  import { InputEvtType, EditorNotifier, ImageLoader } from './types';
@@ -48,9 +31,25 @@ export interface EditorSettings {
48
31
  maxZoom: number;
49
32
  iconProvider: IconProvider;
50
33
  }
34
+ /**
35
+ * The main entrypoint for the full editor.
36
+ *
37
+ * @example
38
+ * To create an editor with a toolbar,
39
+ * ```
40
+ * const editor = new Editor(document.body);
41
+ *
42
+ * const toolbar = editor.addToolbar();
43
+ * toolbar.addActionButton('Save', () => {
44
+ * const saveData = editor.toSVG().outerHTML;
45
+ * // Do something with saveData...
46
+ * });
47
+ * ```
48
+ */
51
49
  export declare class Editor {
52
50
  private container;
53
51
  private renderingRegion;
52
+ /** Manages drawing surfaces/{@link lib!AbstractRenderer}s. */
54
53
  display: Display;
55
54
  /**
56
55
  * Handles undo/redo.
@@ -87,10 +86,20 @@ export declare class Editor {
87
86
  readonly image: EditorImage;
88
87
  /** Viewport for the exported/imported image. */
89
88
  private importExportViewport;
89
+ /**
90
+ * Allows transforming the view and querying information about
91
+ * what is currently visible.
92
+ */
93
+ readonly viewport: Viewport;
90
94
  /** @internal */
91
95
  readonly localization: EditorLocalization;
96
+ /** {@link lib!EditorSettings.iconProvider} */
92
97
  readonly icons: IconProvider;
93
- readonly viewport: Viewport;
98
+ /**
99
+ * Controls the list of tools. See
100
+ * [the custom tool example](https://github.com/personalizedrefrigerator/js-draw/tree/main/docs/example-custom-tools)
101
+ * for more.
102
+ */
94
103
  readonly toolController: ToolController;
95
104
  /**
96
105
  * Global event dispatcher/subscriber.
@@ -196,7 +205,13 @@ export declare class Editor {
196
205
  */
197
206
  queueRerender(): Promise<void>;
198
207
  rerender(showImageBounds?: boolean): void;
208
+ /**
209
+ * @see {@link Display.getWetInkRenderer} {@link Display.flatten}
210
+ */
199
211
  drawWetInk(...path: RenderablePathSpec[]): void;
212
+ /**
213
+ * @see {@link Display.getWetInkRenderer}
214
+ */
200
215
  clearWetInk(): void;
201
216
  focus(): void;
202
217
  createHTMLOverlay(overlay: HTMLElement): {
@@ -1,20 +1,3 @@
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
  var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
19
2
  function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
20
3
  return new (P || (P = Promise))(function (resolve, reject) {
@@ -48,7 +31,21 @@ import untilNextAnimationFrame from './util/untilNextAnimationFrame';
48
31
  import fileToBase64 from './util/fileToBase64';
49
32
  import uniteCommands from './commands/uniteCommands';
50
33
  import SelectionTool from './tools/SelectionTool/SelectionTool';
51
- // { @inheritDoc Editor! }
34
+ /**
35
+ * The main entrypoint for the full editor.
36
+ *
37
+ * @example
38
+ * To create an editor with a toolbar,
39
+ * ```
40
+ * const editor = new Editor(document.body);
41
+ *
42
+ * const toolbar = editor.addToolbar();
43
+ * toolbar.addActionButton('Save', () => {
44
+ * const saveData = editor.toSVG().outerHTML;
45
+ * // Do something with saveData...
46
+ * });
47
+ * ```
48
+ */
52
49
  export class Editor {
53
50
  /**
54
51
  * @example
@@ -188,8 +185,7 @@ export class Editor {
188
185
  addToolbar(defaultLayout = true) {
189
186
  const toolbar = new HTMLToolbar(this, this.container, this.localization);
190
187
  if (defaultLayout) {
191
- toolbar.addDefaultToolWidgets();
192
- toolbar.addDefaultActionButtons();
188
+ toolbar.addDefaults();
193
189
  }
194
190
  return toolbar;
195
191
  }
@@ -583,11 +579,17 @@ export class Editor {
583
579
  this.nextRerenderListeners.forEach(listener => listener());
584
580
  this.nextRerenderListeners = [];
585
581
  }
582
+ /**
583
+ * @see {@link Display.getWetInkRenderer} {@link Display.flatten}
584
+ */
586
585
  drawWetInk(...path) {
587
586
  for (const part of path) {
588
587
  this.display.getWetInkRenderer().drawPath(part);
589
588
  }
590
589
  }
590
+ /**
591
+ * @see {@link Display.getWetInkRenderer}
592
+ */
591
593
  clearWetInk() {
592
594
  this.display.getWetInkRenderer().clear();
593
595
  }
@@ -682,7 +684,7 @@ export class Editor {
682
684
  // The export resolution is the same as the size of the drawing canvas.
683
685
  toDataURL(format = 'image/png') {
684
686
  const canvas = document.createElement('canvas');
685
- const resolution = this.importExportViewport.getResolution();
687
+ const resolution = this.importExportViewport.getScreenRectSize();
686
688
  canvas.width = resolution.x;
687
689
  canvas.height = resolution.y;
688
690
  const ctx = canvas.getContext('2d');
@@ -718,9 +720,9 @@ export class Editor {
718
720
  return __awaiter(this, void 0, void 0, function* () {
719
721
  this.showLoadingWarning(0);
720
722
  this.display.setDraftMode(true);
721
- yield loader.start((component) => {
722
- this.dispatchNoAnnounce(EditorImage.addElement(component));
723
- }, (countProcessed, totalToProcess) => {
723
+ yield loader.start((component) => __awaiter(this, void 0, void 0, function* () {
724
+ yield this.dispatchNoAnnounce(EditorImage.addElement(component));
725
+ }), (countProcessed, totalToProcess) => {
724
726
  if (countProcessed % 500 === 0) {
725
727
  this.showLoadingWarning(countProcessed / totalToProcess);
726
728
  this.rerender();
@@ -22,8 +22,20 @@ export default class EditorImage {
22
22
  getElementsIntersectingRegion(region: Rect2): AbstractComponent[];
23
23
  /** @internal */
24
24
  onDestroyElement(elem: AbstractComponent): void;
25
+ /**
26
+ * @returns the AbstractComponent with `id`, if it exists.
27
+ *
28
+ * @see {@link AbstractComponent.getId}
29
+ */
25
30
  lookupElement(id: string): AbstractComponent | null;
26
31
  private addElementDirectly;
32
+ /**
33
+ * Returns a command that adds the given element to the `EditorImage`.
34
+ * If `applyByFlattening` is true, the content of the wet ink renderer is
35
+ * rendered onto the main rendering canvas instead of doing a full re-render.
36
+ *
37
+ * @see {@link Display.flatten}
38
+ */
27
39
  static addElement(elem: AbstractComponent, applyByFlattening?: boolean): SerializableCommand;
28
40
  private static AddElementCommand;
29
41
  }
@@ -55,6 +55,11 @@ export default class EditorImage {
55
55
  onDestroyElement(elem) {
56
56
  delete this.componentsById[elem.getId()];
57
57
  }
58
+ /**
59
+ * @returns the AbstractComponent with `id`, if it exists.
60
+ *
61
+ * @see {@link AbstractComponent.getId}
62
+ */
58
63
  lookupElement(id) {
59
64
  var _a;
60
65
  return (_a = this.componentsById[id]) !== null && _a !== void 0 ? _a : null;
@@ -63,6 +68,13 @@ export default class EditorImage {
63
68
  this.componentsById[elem.getId()] = elem;
64
69
  return this.root.addLeaf(elem);
65
70
  }
71
+ /**
72
+ * Returns a command that adds the given element to the `EditorImage`.
73
+ * If `applyByFlattening` is true, the content of the wet ink renderer is
74
+ * rendered onto the main rendering canvas instead of doing a full re-render.
75
+ *
76
+ * @see {@link Display.flatten}
77
+ */
66
78
  static addElement(elem, applyByFlattening = false) {
67
79
  return new EditorImage.AddElementCommand(elem, applyByFlattening);
68
80
  }
@@ -18,6 +18,7 @@ export default class Pointer {
18
18
  readonly id: number;
19
19
  readonly timeStamp: number;
20
20
  private constructor();
21
+ snappedToGrid(viewport: Viewport): Pointer;
21
22
  static ofEvent(evt: PointerEvent, isDown: boolean, viewport: Viewport, relativeTo?: HTMLElement): Pointer;
22
23
  static ofCanvasPoint(canvasPos: Point2, isDown: boolean, viewport: Viewport, id?: number, device?: PointerDevice, isPrimary?: boolean, pressure?: number | null): Pointer;
23
24
  }
@@ -31,6 +31,14 @@ export default class Pointer {
31
31
  this.id = id;
32
32
  this.timeStamp = timeStamp;
33
33
  }
34
+ // Snaps this pointer to the nearest grid point (rounds the coordinates of this
35
+ // pointer based on the current zoom). Returns a new Pointer and does not modify
36
+ // this.
37
+ snappedToGrid(viewport) {
38
+ const snappedCanvasPos = viewport.snapToGrid(this.canvasPos);
39
+ const snappedScreenPos = viewport.canvasToScreen(snappedCanvasPos);
40
+ return new Pointer(snappedScreenPos, snappedCanvasPos, this.pressure, this.isPrimary, this.down, this.device, this.id, this.timeStamp);
41
+ }
34
42
  // Creates a Pointer from a DOM event. If `relativeTo` is given, (0, 0) in screen coordinates is
35
43
  // considered the top left of `relativeTo`.
36
44
  static ofEvent(evt, isDown, viewport, relativeTo) {
@@ -34,5 +34,10 @@ export default class SVGLoader implements ImageLoader {
34
34
  private visit;
35
35
  private getSourceAttrs;
36
36
  start(onAddComponent: ComponentAddedListener, onProgress: OnProgressListener, onDetermineExportRect?: OnDetermineExportRectListener | null): Promise<void>;
37
+ /**
38
+ * @see {@link Editor.loadFrom}
39
+ * @param text - Textual representation of the SVG (e.g. `<svg viewbox='...'>...</svg>`).
40
+ * @param sanitize - if `true`, don't store unknown attributes.
41
+ */
37
42
  static fromString(text: string, sanitize?: boolean): SVGLoader;
38
43
  }
@@ -23,6 +23,7 @@ export const defaultSVGViewRect = new Rect2(0, 0, 500, 500);
23
23
  export const svgAttributesDataKey = 'svgAttrs';
24
24
  export const svgStyleAttributesDataKey = 'svgStyleAttrs';
25
25
  const supportedStrokeFillStyleAttrs = ['stroke', 'fill', 'stroke-width'];
26
+ // Handles loading images from SVG.
26
27
  export default class SVGLoader {
27
28
  constructor(source, onFinish, storeUnknown = true) {
28
29
  this.source = source;
@@ -125,22 +126,24 @@ export default class SVGLoader {
125
126
  // Adds a stroke with a single path
126
127
  addPath(node) {
127
128
  var _a;
128
- let elem;
129
- try {
130
- const strokeData = this.strokeDataFromElem(node);
131
- elem = new Stroke(strokeData);
132
- this.attachUnrecognisedAttrs(elem, node, new Set([...supportedStrokeFillStyleAttrs, 'd']), new Set(supportedStrokeFillStyleAttrs));
133
- }
134
- catch (e) {
135
- console.error('Invalid path in node', node, '\nError:', e, '\nAdding as an unknown object.');
136
- if (this.storeUnknown) {
137
- elem = new UnknownSVGObject(node);
129
+ return __awaiter(this, void 0, void 0, function* () {
130
+ let elem;
131
+ try {
132
+ const strokeData = this.strokeDataFromElem(node);
133
+ elem = new Stroke(strokeData);
134
+ this.attachUnrecognisedAttrs(elem, node, new Set([...supportedStrokeFillStyleAttrs, 'd']), new Set(supportedStrokeFillStyleAttrs));
138
135
  }
139
- else {
140
- return;
136
+ catch (e) {
137
+ console.error('Invalid path in node', node, '\nError:', e, '\nAdding as an unknown object.');
138
+ if (this.storeUnknown) {
139
+ elem = new UnknownSVGObject(node);
140
+ }
141
+ else {
142
+ return;
143
+ }
141
144
  }
142
- }
143
- (_a = this.onAddComponent) === null || _a === void 0 ? void 0 : _a.call(this, elem);
145
+ yield ((_a = this.onAddComponent) === null || _a === void 0 ? void 0 : _a.call(this, elem));
146
+ });
144
147
  }
145
148
  // If given, 'supportedAttrs' will have x, y, etc. attributes that were used in computing the transform added to it,
146
149
  // to prevent storing duplicate transform information when saving the component.
@@ -223,14 +226,16 @@ export default class SVGLoader {
223
226
  }
224
227
  addText(elem) {
225
228
  var _a;
226
- try {
227
- const textElem = this.makeText(elem);
228
- (_a = this.onAddComponent) === null || _a === void 0 ? void 0 : _a.call(this, textElem);
229
- }
230
- catch (e) {
231
- console.error('Invalid text object in node', elem, '. Continuing.... Error:', e);
232
- this.addUnknownNode(elem);
233
- }
229
+ return __awaiter(this, void 0, void 0, function* () {
230
+ try {
231
+ const textElem = this.makeText(elem);
232
+ yield ((_a = this.onAddComponent) === null || _a === void 0 ? void 0 : _a.call(this, textElem));
233
+ }
234
+ catch (e) {
235
+ console.error('Invalid text object in node', elem, '. Continuing.... Error:', e);
236
+ this.addUnknownNode(elem);
237
+ }
238
+ });
234
239
  }
235
240
  addImage(elem) {
236
241
  var _a, _b, _c;
@@ -243,20 +248,22 @@ export default class SVGLoader {
243
248
  const transform = this.getTransform(elem, supportedAttrs);
244
249
  const imageElem = yield ImageComponent.fromImage(image, transform);
245
250
  this.attachUnrecognisedAttrs(imageElem, elem, new Set(supportedAttrs), new Set(['transform']));
246
- (_c = this.onAddComponent) === null || _c === void 0 ? void 0 : _c.call(this, imageElem);
251
+ yield ((_c = this.onAddComponent) === null || _c === void 0 ? void 0 : _c.call(this, imageElem));
247
252
  }
248
253
  catch (e) {
249
254
  console.error('Error loading image:', e, '. Element: ', elem, '. Continuing...');
250
- this.addUnknownNode(elem);
255
+ yield this.addUnknownNode(elem);
251
256
  }
252
257
  });
253
258
  }
254
259
  addUnknownNode(node) {
255
260
  var _a;
256
- if (this.storeUnknown) {
257
- const component = new UnknownSVGObject(node);
258
- (_a = this.onAddComponent) === null || _a === void 0 ? void 0 : _a.call(this, component);
259
- }
261
+ return __awaiter(this, void 0, void 0, function* () {
262
+ if (this.storeUnknown) {
263
+ const component = new UnknownSVGObject(node);
264
+ yield ((_a = this.onAddComponent) === null || _a === void 0 ? void 0 : _a.call(this, component));
265
+ }
266
+ });
260
267
  }
261
268
  updateViewBox(node) {
262
269
  var _a;
@@ -278,9 +285,11 @@ export default class SVGLoader {
278
285
  }
279
286
  updateSVGAttrs(node) {
280
287
  var _a;
281
- if (this.storeUnknown) {
282
- (_a = this.onAddComponent) === null || _a === void 0 ? void 0 : _a.call(this, new SVGGlobalAttributesObject(this.getSourceAttrs(node)));
283
- }
288
+ return __awaiter(this, void 0, void 0, function* () {
289
+ if (this.storeUnknown) {
290
+ yield ((_a = this.onAddComponent) === null || _a === void 0 ? void 0 : _a.call(this, new SVGGlobalAttributesObject(this.getSourceAttrs(node))));
291
+ }
292
+ });
284
293
  }
285
294
  visit(node) {
286
295
  var _a;
@@ -292,10 +301,10 @@ export default class SVGLoader {
292
301
  // Continue -- visit the node's children.
293
302
  break;
294
303
  case 'path':
295
- this.addPath(node);
304
+ yield this.addPath(node);
296
305
  break;
297
306
  case 'text':
298
- this.addText(node);
307
+ yield this.addText(node);
299
308
  visitChildren = false;
300
309
  break;
301
310
  case 'image':
@@ -308,14 +317,14 @@ export default class SVGLoader {
308
317
  this.updateSVGAttrs(node);
309
318
  break;
310
319
  case 'style':
311
- this.addUnknownNode(node);
320
+ yield this.addUnknownNode(node);
312
321
  break;
313
322
  default:
314
323
  console.warn('Unknown SVG element,', node);
315
324
  if (!(node instanceof SVGElement)) {
316
325
  console.warn('Element', node, 'is not an SVGElement!', this.storeUnknown ? 'Continuing anyway.' : 'Skipping.');
317
326
  }
318
- this.addUnknownNode(node);
327
+ yield this.addUnknownNode(node);
319
328
  return;
320
329
  }
321
330
  if (visitChildren) {
@@ -351,7 +360,11 @@ export default class SVGLoader {
351
360
  (_b = this.onFinish) === null || _b === void 0 ? void 0 : _b.call(this);
352
361
  });
353
362
  }
354
- // @param sanitize - if `true`, don't store unknown attributes.
363
+ /**
364
+ * @see {@link Editor.loadFrom}
365
+ * @param text - Textual representation of the SVG (e.g. `<svg viewbox='...'>...</svg>`).
366
+ * @param sanitize - if `true`, don't store unknown attributes.
367
+ */
355
368
  static fromString(text, sanitize = false) {
356
369
  var _a, _b;
357
370
  const sandbox = document.createElement('iframe');
@@ -2,6 +2,7 @@ import Command from './commands/Command';
2
2
  import Mat33 from './math/Mat33';
3
3
  import Rect2 from './math/Rect2';
4
4
  import { Point2, Vec2 } from './math/Vec2';
5
+ import Vec3 from './math/Vec3';
5
6
  import { StrokeDataPoint } from './types';
6
7
  import { EditorNotifier } from './types';
7
8
  type PointDataType<T extends Point2 | StrokeDataPoint | number> = T extends Point2 ? Point2 : number;
@@ -16,17 +17,45 @@ export declare class Viewport {
16
17
  private screenRect;
17
18
  constructor(notifier: EditorNotifier);
18
19
  updateScreenSize(screenSize: Vec2): void;
20
+ /** Get the screen's visible region transformed into canvas space. */
19
21
  get visibleRect(): Rect2;
22
+ /** @returns the given point, but in canvas coordinates */
20
23
  screenToCanvas(screenPoint: Point2): Point2;
24
+ /** @returns the given point transformed into screen coordinates. */
21
25
  canvasToScreen(canvasPoint: Point2): Point2;
26
+ /** @returns a command that transforms the canvas by `transform`. */
22
27
  static transformBy(transform: Mat33): ViewportTransform;
28
+ /**
29
+ * Updates the transformation directly. Using `transformBy` is preferred.
30
+ * @param newTransform - should map from canvas coordinates to screen coordinates.
31
+ */
23
32
  resetTransform(newTransform?: Mat33): void;
24
33
  get screenToCanvasTransform(): Mat33;
25
34
  get canvasToScreenTransform(): Mat33;
26
- getResolution(): Vec2;
35
+ /** @returns the size of the visible region in pixels. */
36
+ getScreenRectSize(): Vec2;
37
+ /** Alias for `getScreenRectSize`. @deprecated */
38
+ getResolution(): Vec3;
39
+ /** @returns the amount a vector on the canvas is scaled to become a vector on the screen. */
27
40
  getScaleFactor(): number;
41
+ /**
42
+ * @returns `getScaleFactor()` rounded to the nearest power of 10.
43
+ * For example, if `getScaleFactor()` returns 101, `getScaleFactorToNearestPowerOfTen()`
44
+ * should return `100` because `100` is the nearest power of 10 to 101.
45
+ */
46
+ getScaleFactorToNearestPowerOfTen(): number;
47
+ snapToGrid(canvasPos: Point2): Vec3;
48
+ /** Returns the size of one screen pixel in canvas units. */
28
49
  getSizeOfPixelOnCanvas(): number;
50
+ /**
51
+ * @returns the angle of the canvas in radians.
52
+ * This is the angle by which the canvas is rotated relative to the screen.
53
+ */
29
54
  getRotationAngle(): number;
55
+ /**
56
+ * Rounds the given `point` to a multiple of 10 such that it is within `tolerance` of
57
+ * its original location. This is useful for preparing data for base-10 conversion.
58
+ */
30
59
  static roundPoint<T extends Point2 | number>(point: T, tolerance: number): PointDataType<T>;
31
60
  roundPoint(point: Point2): Point2;
32
61
  static roundScaleRatio(scaleRatio: number, roundAmount?: number): number;
@@ -29,22 +29,26 @@ export class Viewport {
29
29
  updateScreenSize(screenSize) {
30
30
  this.screenRect = this.screenRect.resizedTo(screenSize);
31
31
  }
32
- // Get the screen's visible region transformed into canvas space.
32
+ /** Get the screen's visible region transformed into canvas space. */
33
33
  get visibleRect() {
34
34
  return this.screenRect.transformedBoundingBox(this.inverseTransform);
35
35
  }
36
- // the given point, but in canvas coordinates
36
+ /** @returns the given point, but in canvas coordinates */
37
37
  screenToCanvas(screenPoint) {
38
38
  return this.inverseTransform.transformVec2(screenPoint);
39
39
  }
40
+ /** @returns the given point transformed into screen coordinates. */
40
41
  canvasToScreen(canvasPoint) {
41
42
  return this.transform.transformVec2(canvasPoint);
42
43
  }
44
+ /** @returns a command that transforms the canvas by `transform`. */
43
45
  static transformBy(transform) {
44
46
  return new Viewport.ViewportTransform(transform);
45
47
  }
46
- // Updates the transformation directly. Using `transformBy` is preferred.
47
- // [newTransform] should map from canvas coordinates to screen coordinates.
48
+ /**
49
+ * Updates the transformation directly. Using `transformBy` is preferred.
50
+ * @param newTransform - should map from canvas coordinates to screen coordinates.
51
+ */
48
52
  resetTransform(newTransform = Mat33.identity) {
49
53
  const oldTransform = this.transform;
50
54
  this.transform = newTransform;
@@ -61,20 +65,46 @@ export class Viewport {
61
65
  get canvasToScreenTransform() {
62
66
  return this.transform;
63
67
  }
64
- getResolution() {
68
+ /** @returns the size of the visible region in pixels. */
69
+ getScreenRectSize() {
65
70
  return this.screenRect.size;
66
71
  }
67
- // Returns the amount a vector on the canvas is scaled to become a vector on the screen.
72
+ /** Alias for `getScreenRectSize`. @deprecated */
73
+ getResolution() {
74
+ return this.getScreenRectSize();
75
+ }
76
+ /** @returns the amount a vector on the canvas is scaled to become a vector on the screen. */
68
77
  getScaleFactor() {
69
78
  // Use transformVec3 to avoid translating the vector
70
79
  return this.transform.transformVec3(Vec3.unitX).magnitude();
71
80
  }
72
- // Returns the size of one screen pixel in canvas units.
81
+ /**
82
+ * @returns `getScaleFactor()` rounded to the nearest power of 10.
83
+ * For example, if `getScaleFactor()` returns 101, `getScaleFactorToNearestPowerOfTen()`
84
+ * should return `100` because `100` is the nearest power of 10 to 101.
85
+ */
86
+ getScaleFactorToNearestPowerOfTen() {
87
+ const scaleFactor = this.getScaleFactor();
88
+ return Math.pow(10, Math.round(Math.log10(scaleFactor)));
89
+ }
90
+ snapToGrid(canvasPos) {
91
+ const snapCoordinate = (coordinate) => {
92
+ const scaleFactor = this.getScaleFactorToNearestPowerOfTen();
93
+ const roundFactor = scaleFactor / 100;
94
+ const snapped = Math.round(coordinate * roundFactor) / roundFactor;
95
+ return snapped;
96
+ };
97
+ const snappedCanvasPos = Vec2.of(snapCoordinate(canvasPos.x), snapCoordinate(canvasPos.y));
98
+ return snappedCanvasPos;
99
+ }
100
+ /** Returns the size of one screen pixel in canvas units. */
73
101
  getSizeOfPixelOnCanvas() {
74
102
  return 1 / this.getScaleFactor();
75
103
  }
76
- // Returns the angle of the canvas in radians.
77
- // This is the angle by which the canvas is rotated relative to the screen.
104
+ /**
105
+ * @returns the angle of the canvas in radians.
106
+ * This is the angle by which the canvas is rotated relative to the screen.
107
+ */
78
108
  getRotationAngle() {
79
109
  return this.transform.transformVec3(Vec3.unitX).angle();
80
110
  }
@@ -1,6 +1,6 @@
1
1
  import Command from './Command';
2
2
  import SerializableCommand from './SerializableCommand';
3
- // Returns a command taht does the opposite of the given command --- `result.apply()` calls
3
+ // Returns a command that does the opposite of the given command --- `result.apply()` calls
4
4
  // `command.unapply()` and `result.unapply()` calls `command.apply()`.
5
5
  const invertCommand = (command) => {
6
6
  if (command instanceof SerializableCommand) {
@@ -7,6 +7,9 @@ import { ImageComponentLocalization } from './localization';
7
7
  export type LoadSaveData = (string[] | Record<symbol, string | number>);
8
8
  export type LoadSaveDataTable = Record<string, Array<LoadSaveData>>;
9
9
  export type DeserializeCallback = (data: string) => AbstractComponent;
10
+ /**
11
+ * A base class for everything that can be added to an {@link EditorImage}.
12
+ */
10
13
  export default abstract class AbstractComponent {
11
14
  private readonly componentKind;
12
15
  protected lastChangedTime: number;
@@ -19,12 +22,25 @@ export default abstract class AbstractComponent {
19
22
  private static deserializationCallbacks;
20
23
  static registerComponent(componentKind: string, deserialize: DeserializeCallback | null): void;
21
24
  private loadSaveData;
25
+ /**
26
+ * Attach data that can be used while exporting the component (e.g. to SVG).
27
+ *
28
+ * This is intended for use by a {@link ImageLoader}.
29
+ */
22
30
  attachLoadSaveData(key: string, data: LoadSaveData): void;
31
+ /** See {@link attachLoadSaveData} */
23
32
  getLoadSaveData(): LoadSaveDataTable;
24
33
  getZIndex(): number;
34
+ /** @returns the bounding box of */
25
35
  getBBox(): Rect2;
26
36
  abstract render(canvas: AbstractRenderer, visibleRect?: Rect2): void;
37
+ /** @return true if `lineSegment` intersects this component. */
27
38
  abstract intersects(lineSegment: LineSegment2): boolean;
39
+ /**
40
+ * @returns true if this component intersects `rect` -- it is entirely contained
41
+ * within the rectangle or one of the rectangle's edges intersects this component.
42
+ */
43
+ intersectsRect(rect: Rect2): boolean;
28
44
  protected abstract serializeToJSON(): any[] | Record<string, any> | number | string | null;
29
45
  protected abstract applyTransformation(affineTransfm: Mat33): void;
30
46
  transformBy(affineTransfm: Mat33): SerializableCommand;
@@ -34,6 +50,10 @@ export default abstract class AbstractComponent {
34
50
  private static transformElementCommandId;
35
51
  private static UnresolvedTransformElementCommand;
36
52
  private static TransformElementCommand;
53
+ /**
54
+ * @return a description that could be read by a screen reader
55
+ * (e.g. when adding/erasing the component)
56
+ */
37
57
  abstract description(localizationTable: ImageComponentLocalization): string;
38
58
  protected abstract createClone(): AbstractComponent;
39
59
  clone(): AbstractComponent;