js-draw 1.9.0 → 1.10.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 (109) hide show
  1. package/dist/Editor.css +48 -1
  2. package/dist/bundle.js +2 -2
  3. package/dist/bundledStyles.js +1 -1
  4. package/dist/cjs/Editor.d.ts +41 -0
  5. package/dist/cjs/Editor.js +33 -13
  6. package/dist/cjs/Pointer.js +1 -1
  7. package/dist/cjs/Viewport.d.ts +6 -0
  8. package/dist/cjs/Viewport.js +6 -1
  9. package/dist/cjs/commands/Erase.d.ts +22 -2
  10. package/dist/cjs/commands/Erase.js +22 -2
  11. package/dist/cjs/commands/uniteCommands.d.ts +36 -0
  12. package/dist/cjs/commands/uniteCommands.js +36 -0
  13. package/dist/cjs/components/ImageComponent.d.ts +12 -0
  14. package/dist/cjs/components/ImageComponent.js +16 -9
  15. package/dist/cjs/components/Stroke.d.ts +16 -2
  16. package/dist/cjs/components/Stroke.js +17 -1
  17. package/dist/cjs/components/builders/ArrowBuilder.js +3 -3
  18. package/dist/cjs/components/builders/CircleBuilder.js +3 -3
  19. package/dist/cjs/components/builders/FreehandLineBuilder.js +3 -3
  20. package/dist/cjs/components/builders/LineBuilder.js +3 -3
  21. package/dist/cjs/components/builders/PressureSensitiveFreehandLineBuilder.js +3 -3
  22. package/dist/cjs/components/builders/RectangleBuilder.js +5 -6
  23. package/dist/cjs/components/builders/autocorrect/makeShapeFitAutocorrect.d.ts +3 -0
  24. package/dist/cjs/components/builders/autocorrect/makeShapeFitAutocorrect.js +168 -0
  25. package/dist/cjs/components/builders/autocorrect/makeSnapToGridAutocorrect.d.ts +3 -0
  26. package/dist/cjs/components/builders/autocorrect/makeSnapToGridAutocorrect.js +46 -0
  27. package/dist/cjs/components/builders/types.d.ts +1 -0
  28. package/dist/cjs/image/EditorImage.d.ts +32 -1
  29. package/dist/cjs/image/EditorImage.js +32 -1
  30. package/dist/cjs/rendering/Display.js +8 -1
  31. package/dist/cjs/rendering/RenderablePathSpec.d.ts +5 -1
  32. package/dist/cjs/rendering/RenderablePathSpec.js +4 -0
  33. package/dist/cjs/toolbar/IconProvider.d.ts +2 -0
  34. package/dist/cjs/toolbar/IconProvider.js +17 -0
  35. package/dist/cjs/toolbar/localization.d.ts +3 -0
  36. package/dist/cjs/toolbar/localization.js +4 -1
  37. package/dist/cjs/toolbar/widgets/InsertImageWidget.d.ts +2 -1
  38. package/dist/cjs/toolbar/widgets/InsertImageWidget.js +102 -22
  39. package/dist/cjs/toolbar/widgets/PenToolWidget.d.ts +1 -2
  40. package/dist/cjs/toolbar/widgets/PenToolWidget.js +50 -20
  41. package/dist/cjs/tools/Pen.d.ts +9 -0
  42. package/dist/cjs/tools/Pen.js +77 -3
  43. package/dist/cjs/tools/TextTool.js +5 -1
  44. package/dist/cjs/tools/util/StationaryPenDetector.d.ts +22 -0
  45. package/dist/cjs/tools/util/StationaryPenDetector.js +95 -0
  46. package/dist/cjs/util/ReactiveValue.d.ts +2 -0
  47. package/dist/cjs/util/ReactiveValue.js +2 -0
  48. package/dist/cjs/util/lib.d.ts +1 -0
  49. package/dist/cjs/util/lib.js +4 -1
  50. package/dist/cjs/util/waitForImageLoaded.d.ts +2 -0
  51. package/dist/cjs/util/waitForImageLoaded.js +12 -0
  52. package/dist/cjs/version.js +1 -1
  53. package/dist/mjs/Editor.d.ts +41 -0
  54. package/dist/mjs/Editor.mjs +33 -13
  55. package/dist/mjs/Pointer.mjs +1 -1
  56. package/dist/mjs/Viewport.d.ts +6 -0
  57. package/dist/mjs/Viewport.mjs +6 -1
  58. package/dist/mjs/commands/Erase.d.ts +22 -2
  59. package/dist/mjs/commands/Erase.mjs +22 -2
  60. package/dist/mjs/commands/uniteCommands.d.ts +36 -0
  61. package/dist/mjs/commands/uniteCommands.mjs +36 -0
  62. package/dist/mjs/components/ImageComponent.d.ts +12 -0
  63. package/dist/mjs/components/ImageComponent.mjs +16 -9
  64. package/dist/mjs/components/Stroke.d.ts +16 -2
  65. package/dist/mjs/components/Stroke.mjs +17 -1
  66. package/dist/mjs/components/builders/ArrowBuilder.mjs +3 -2
  67. package/dist/mjs/components/builders/CircleBuilder.mjs +3 -2
  68. package/dist/mjs/components/builders/FreehandLineBuilder.mjs +3 -2
  69. package/dist/mjs/components/builders/LineBuilder.mjs +3 -2
  70. package/dist/mjs/components/builders/PressureSensitiveFreehandLineBuilder.mjs +3 -2
  71. package/dist/mjs/components/builders/RectangleBuilder.mjs +5 -4
  72. package/dist/mjs/components/builders/autocorrect/makeShapeFitAutocorrect.d.ts +3 -0
  73. package/dist/mjs/components/builders/autocorrect/makeShapeFitAutocorrect.mjs +166 -0
  74. package/dist/mjs/components/builders/autocorrect/makeSnapToGridAutocorrect.d.ts +3 -0
  75. package/dist/mjs/components/builders/autocorrect/makeSnapToGridAutocorrect.mjs +44 -0
  76. package/dist/mjs/components/builders/types.d.ts +1 -0
  77. package/dist/mjs/image/EditorImage.d.ts +32 -1
  78. package/dist/mjs/image/EditorImage.mjs +32 -1
  79. package/dist/mjs/rendering/Display.mjs +8 -1
  80. package/dist/mjs/rendering/RenderablePathSpec.d.ts +5 -1
  81. package/dist/mjs/rendering/RenderablePathSpec.mjs +4 -0
  82. package/dist/mjs/toolbar/IconProvider.d.ts +2 -0
  83. package/dist/mjs/toolbar/IconProvider.mjs +17 -0
  84. package/dist/mjs/toolbar/localization.d.ts +3 -0
  85. package/dist/mjs/toolbar/localization.mjs +4 -1
  86. package/dist/mjs/toolbar/widgets/InsertImageWidget.d.ts +2 -1
  87. package/dist/mjs/toolbar/widgets/InsertImageWidget.mjs +102 -22
  88. package/dist/mjs/toolbar/widgets/PenToolWidget.d.ts +1 -2
  89. package/dist/mjs/toolbar/widgets/PenToolWidget.mjs +50 -20
  90. package/dist/mjs/tools/Pen.d.ts +9 -0
  91. package/dist/mjs/tools/Pen.mjs +77 -3
  92. package/dist/mjs/tools/TextTool.mjs +5 -1
  93. package/dist/mjs/tools/util/StationaryPenDetector.d.ts +22 -0
  94. package/dist/mjs/tools/util/StationaryPenDetector.mjs +92 -0
  95. package/dist/mjs/util/ReactiveValue.d.ts +2 -0
  96. package/dist/mjs/util/ReactiveValue.mjs +2 -0
  97. package/dist/mjs/util/lib.d.ts +1 -0
  98. package/dist/mjs/util/lib.mjs +1 -0
  99. package/dist/mjs/util/waitForImageLoaded.d.ts +2 -0
  100. package/dist/mjs/util/waitForImageLoaded.mjs +10 -0
  101. package/dist/mjs/version.mjs +1 -1
  102. package/package.json +3 -3
  103. package/src/Editor.scss +7 -0
  104. package/src/toolbar/AbstractToolbar.scss +20 -0
  105. package/src/toolbar/toolbar.scss +1 -1
  106. package/src/toolbar/widgets/InsertImageWidget.scss +6 -1
  107. package/src/toolbar/widgets/PenToolWidget.scss +33 -0
  108. package/src/tools/SelectionTool/SelectionTool.scss +6 -0
  109. package/src/toolbar/widgets/PenToolWidget.css +0 -2
@@ -34,6 +34,8 @@ const noOpSetUpdateListener = () => {
34
34
  *
35
35
  * Static methods in the `ReactiveValue` and `MutableReactiveValue` classes are
36
36
  * constructors (e.g. `fromImmutable`).
37
+ *
38
+ * Avoid extending this class from an external library, as that may not be stable.
37
39
  */
38
40
  class ReactiveValue {
39
41
  /** Creates a `ReactiveValue` with an initial value, `initialValue`. */
@@ -1 +1,2 @@
1
1
  export { default as adjustEditorThemeForContrast } from './adjustEditorThemeForContrast';
2
+ export { ReactiveValue, MutableReactiveValue } from './ReactiveValue';
@@ -3,6 +3,9 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
3
3
  return (mod && mod.__esModule) ? mod : { "default": mod };
4
4
  };
5
5
  Object.defineProperty(exports, "__esModule", { value: true });
6
- exports.adjustEditorThemeForContrast = void 0;
6
+ exports.MutableReactiveValue = exports.ReactiveValue = exports.adjustEditorThemeForContrast = void 0;
7
7
  var adjustEditorThemeForContrast_1 = require("./adjustEditorThemeForContrast");
8
8
  Object.defineProperty(exports, "adjustEditorThemeForContrast", { enumerable: true, get: function () { return __importDefault(adjustEditorThemeForContrast_1).default; } });
9
+ var ReactiveValue_1 = require("./ReactiveValue");
10
+ Object.defineProperty(exports, "ReactiveValue", { enumerable: true, get: function () { return ReactiveValue_1.ReactiveValue; } });
11
+ Object.defineProperty(exports, "MutableReactiveValue", { enumerable: true, get: function () { return ReactiveValue_1.MutableReactiveValue; } });
@@ -0,0 +1,2 @@
1
+ declare const waitForImageLoad: (image: HTMLImageElement) => Promise<void>;
2
+ export default waitForImageLoad;
@@ -0,0 +1,12 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ const waitForImageLoad = async (image) => {
4
+ if (!image.complete) {
5
+ await new Promise((resolve, reject) => {
6
+ image.onload = event => resolve(event);
7
+ image.onerror = event => reject(event);
8
+ image.onabort = event => reject(event);
9
+ });
10
+ }
11
+ };
12
+ exports.default = waitForImageLoad;
@@ -1,5 +1,5 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.default = {
4
- number: '1.9.0',
4
+ number: '1.10.0',
5
5
  };
@@ -156,6 +156,38 @@ export declare class Editor {
156
156
  readonly toolController: ToolController;
157
157
  /**
158
158
  * Global event dispatcher/subscriber.
159
+ *
160
+ * @example
161
+ *
162
+ * ```ts,runnable
163
+ * import { Editor, EditorEventType, SerializableCommand } from 'js-draw';
164
+ *
165
+ * // Create a minimal editor
166
+ * const editor = new Editor(document.body);
167
+ * editor.addToolbar();
168
+ *
169
+ * // Create a place to show text output
170
+ * const log = document.createElement('textarea');
171
+ * document.body.appendChild(log);
172
+ * log.style.width = '100%';
173
+ * log.style.height = '200px';
174
+ *
175
+ * // Listen for CommandDone events (there's also a CommandUndone)
176
+ * editor.notifier.on(EditorEventType.CommandDone, event => {
177
+ * // Type narrowing for TypeScript -- event will always be of kind CommandDone,
178
+ * // but TypeScript doesn't know this.
179
+ * if (event.kind !== EditorEventType.CommandDone) return;
180
+ *
181
+ * log.value = `Command done ${event.command.description(editor, editor.localization)}\n`;
182
+ *
183
+ * if (event.command instanceof SerializableCommand) {
184
+ * log.value += `serializes to: ${JSON.stringify(event.command.serialize())}`;
185
+ * }
186
+ * });
187
+ *
188
+ * // Dispatch an initial command to trigger the event listener for the first time
189
+ * editor.dispatch(editor.image.setAutoresizeEnabled(true));
190
+ * ```
159
191
  */
160
192
  readonly notifier: EditorNotifier;
161
193
  private loadingWarning;
@@ -395,6 +427,12 @@ export declare class Editor {
395
427
  * @see {@link sendPenEvent} {@link sendTouchEvent}
396
428
  */
397
429
  sendPenEvent(eventType: InputEvtType.PointerDownEvt | InputEvtType.PointerMoveEvt | InputEvtType.PointerUpEvt, point: Point2, allPointers?: Pointer[]): void;
430
+ /**
431
+ * Adds all components in `components` such that they are in the center of the screen.
432
+ * This is a convenience method that creates **and applies** a single command.
433
+ *
434
+ * If `selectComponents` is true (the default), the components are selected.
435
+ */
398
436
  addAndCenterComponents(components: AbstractComponent[], selectComponents?: boolean): Promise<void>;
399
437
  /**
400
438
  * Get a data URL (e.g. as produced by `HTMLCanvasElement::toDataURL`).
@@ -403,6 +441,9 @@ export declare class Editor {
403
441
  *
404
442
  * The export resolution is the same as the size of the drawing canvas, unless `outputSize`
405
443
  * is given.
444
+ *
445
+ * **Example**:
446
+ * [[include:doc-pages/inline-examples/adding-an-image-and-data-urls.md]]
406
447
  */
407
448
  toDataURL(format?: 'image/png' | 'image/jpeg' | 'image/webp', outputSize?: Vec2): string;
408
449
  /**
@@ -160,19 +160,30 @@ export class Editor {
160
160
  this.hideLoadingWarning();
161
161
  // Enforce zoom limits.
162
162
  this.notifier.on(EditorEventType.ViewportChanged, evt => {
163
- if (evt.kind === EditorEventType.ViewportChanged) {
164
- const zoom = evt.newTransform.transformVec3(Vec2.unitX).length();
165
- if (zoom > this.settings.maxZoom || zoom < this.settings.minZoom) {
166
- const oldZoom = evt.oldTransform.transformVec3(Vec2.unitX).length();
167
- let resetTransform = Mat33.identity;
168
- if (oldZoom <= this.settings.maxZoom && oldZoom >= this.settings.minZoom) {
169
- resetTransform = evt.oldTransform;
170
- }
171
- else {
172
- // If 1x zoom isn't acceptable, try a zoom between the minimum and maximum.
173
- resetTransform = Mat33.scaling2D((this.settings.minZoom + this.settings.maxZoom) / 2);
174
- }
175
- this.viewport.resetTransform(resetTransform);
163
+ if (evt.kind !== EditorEventType.ViewportChanged)
164
+ return;
165
+ const getZoom = (mat) => mat.transformVec3(Vec2.unitX).length();
166
+ const zoom = getZoom(evt.newTransform);
167
+ if (zoom > this.settings.maxZoom || zoom < this.settings.minZoom) {
168
+ const oldZoom = getZoom(evt.oldTransform);
169
+ let resetTransform = Mat33.identity;
170
+ if (oldZoom <= this.settings.maxZoom && oldZoom >= this.settings.minZoom) {
171
+ resetTransform = evt.oldTransform;
172
+ }
173
+ else {
174
+ // If 1x zoom isn't acceptable, try a zoom between the minimum and maximum.
175
+ resetTransform = Mat33.scaling2D((this.settings.minZoom + this.settings.maxZoom) / 2);
176
+ }
177
+ this.viewport.resetTransform(resetTransform);
178
+ }
179
+ else if (!isFinite(zoom)) {
180
+ // Recover from possible division-by-zero
181
+ console.warn(`Non-finite zoom (${zoom}) detected. Resetting the viewport. This was likely caused by division by zero.`);
182
+ if (isFinite(getZoom(evt.oldTransform))) {
183
+ this.viewport.resetTransform(evt.oldTransform);
184
+ }
185
+ else {
186
+ this.viewport.resetTransform();
176
187
  }
177
188
  }
178
189
  });
@@ -875,6 +886,12 @@ export class Editor {
875
886
  allPointers) {
876
887
  sendPenEvent(this, eventType, point, allPointers);
877
888
  }
889
+ /**
890
+ * Adds all components in `components` such that they are in the center of the screen.
891
+ * This is a convenience method that creates **and applies** a single command.
892
+ *
893
+ * If `selectComponents` is true (the default), the components are selected.
894
+ */
878
895
  async addAndCenterComponents(components, selectComponents = true) {
879
896
  let bbox = null;
880
897
  for (const component of components) {
@@ -921,6 +938,9 @@ export class Editor {
921
938
  *
922
939
  * The export resolution is the same as the size of the drawing canvas, unless `outputSize`
923
940
  * is given.
941
+ *
942
+ * **Example**:
943
+ * [[include:doc-pages/inline-examples/adding-an-image-and-data-urls.md]]
924
944
  */
925
945
  toDataURL(format = 'image/png', outputSize) {
926
946
  const canvas = document.createElement('canvas');
@@ -112,7 +112,7 @@ export default class Pointer {
112
112
  // Intended for unit tests.
113
113
  static ofCanvasPoint(canvasPos, isDown, viewport, id = 0, device = PointerDevice.Pen, isPrimary = true, pressure = null) {
114
114
  const screenPos = viewport.canvasToScreen(canvasPos);
115
- const timeStamp = (new Date()).getTime();
115
+ const timeStamp = performance.now();
116
116
  return new Pointer(screenPos, canvasPos, pressure, isPrimary, isDown, device, id, timeStamp);
117
117
  }
118
118
  }
@@ -50,6 +50,11 @@ export declare class Viewport {
50
50
  private getScaleFactorToNearestPowerOf;
51
51
  /** Returns the size of a grid cell (in canvas units) as used by {@link snapToGrid}. */
52
52
  static getGridSize(scaleFactor: number): number;
53
+ /**
54
+ * Snaps `canvasPos` to the nearest grid cell corner.
55
+ *
56
+ * @see {@link getGridSize} and {@link getScaleFactorToNearestPowerOf}.
57
+ */
53
58
  snapToGrid(canvasPos: Point2): Vec3;
54
59
  /** Returns the size of one screen pixel in canvas units. */
55
60
  getSizeOfPixelOnCanvas(): number;
@@ -65,6 +70,7 @@ export declare class Viewport {
65
70
  * its original location. This is useful for preparing data for base-10 conversion.
66
71
  */
67
72
  static roundPoint<T extends Point2 | number>(point: T, tolerance: number): PointDataType<T>;
73
+ /** Round a point with a tolerance of ±1 screen unit. */
68
74
  roundPoint(point: Point2): Point2;
69
75
  static roundScaleRatio(scaleRatio: number, roundAmount?: number): number;
70
76
  computeZoomToTransform(toMakeVisible: Rect2, allowZoomIn?: boolean, allowZoomOut?: boolean): Mat33;
@@ -97,6 +97,11 @@ export class Viewport {
97
97
  static getGridSize(scaleFactor) {
98
98
  return 50 / scaleFactor;
99
99
  }
100
+ /**
101
+ * Snaps `canvasPos` to the nearest grid cell corner.
102
+ *
103
+ * @see {@link getGridSize} and {@link getScaleFactorToNearestPowerOf}.
104
+ */
100
105
  snapToGrid(canvasPos) {
101
106
  const scaleFactor = this.getScaleFactorToNearestPowerOf(2);
102
107
  const snapCoordinate = (coordinate) => {
@@ -133,7 +138,7 @@ export class Viewport {
133
138
  }
134
139
  return point.map(roundComponent);
135
140
  }
136
- // Round a point with a tolerance of ±1 screen unit.
141
+ /** Round a point with a tolerance of ±1 screen unit. */
137
142
  roundPoint(point) {
138
143
  return Viewport.roundPoint(point, 1 / this.getScaleFactor());
139
144
  }
@@ -5,8 +5,28 @@ import SerializableCommand from './SerializableCommand';
5
5
  /**
6
6
  * Removes the given {@link AbstractComponent}s from the image.
7
7
  *
8
- * @example
9
- * ```ts
8
+ * **Example**:
9
+ * ```ts,runnable
10
+ * import { Editor, Erase, uniteCommands, Color4, Path, Stroke, Rect2, pathToRenderable } from 'js-draw';
11
+ *
12
+ * const editor = new Editor(document.body);
13
+ * editor.addToolbar();
14
+ *
15
+ * // Add a large number of strokes
16
+ * const commands = [];
17
+ * for (let x = -20; x < 20; x++) {
18
+ * for (let y = 0; y < 60; y++) {
19
+ * const stroke = new Stroke([
20
+ * pathToRenderable(
21
+ * Path.fromString(`m${x * 5},${y * 5}l1,1`),
22
+ * { fill: Color4.transparent, stroke: {width: 2, color: Color4.ofRGB(x / 10, y / 10, 0.5)}} )
23
+ * ]);
24
+ * commands.push(editor.image.addElement(stroke));
25
+ * }
26
+ * }
27
+ * await editor.dispatch(uniteCommands(commands, 100));
28
+ *
29
+ * ---visible---
10
30
  * // Given some editor...
11
31
  *
12
32
  * // Find all elements intersecting the rectangle with top left (-10,-30) and
@@ -5,8 +5,28 @@ import SerializableCommand from './SerializableCommand.mjs';
5
5
  /**
6
6
  * Removes the given {@link AbstractComponent}s from the image.
7
7
  *
8
- * @example
9
- * ```ts
8
+ * **Example**:
9
+ * ```ts,runnable
10
+ * import { Editor, Erase, uniteCommands, Color4, Path, Stroke, Rect2, pathToRenderable } from 'js-draw';
11
+ *
12
+ * const editor = new Editor(document.body);
13
+ * editor.addToolbar();
14
+ *
15
+ * // Add a large number of strokes
16
+ * const commands = [];
17
+ * for (let x = -20; x < 20; x++) {
18
+ * for (let y = 0; y < 60; y++) {
19
+ * const stroke = new Stroke([
20
+ * pathToRenderable(
21
+ * Path.fromString(`m${x * 5},${y * 5}l1,1`),
22
+ * { fill: Color4.transparent, stroke: {width: 2, color: Color4.ofRGB(x / 10, y / 10, 0.5)}} )
23
+ * ]);
24
+ * commands.push(editor.image.addElement(stroke));
25
+ * }
26
+ * }
27
+ * await editor.dispatch(uniteCommands(commands, 100));
28
+ *
29
+ * ---visible---
10
30
  * // Given some editor...
11
31
  *
12
32
  * // Find all elements intersecting the rectangle with top left (-10,-30) and
@@ -1,4 +1,40 @@
1
1
  import Command from './Command';
2
2
  import SerializableCommand from './SerializableCommand';
3
+ /**
4
+ * Creates a single command from `commands`. This is useful when undoing should undo *all* commands
5
+ * in `commands` at once, rather than one at a time.
6
+ *
7
+ * @example
8
+ *
9
+ * ```ts,runnable
10
+ * import { Editor, pathToRenderable, Stroke, uniteCommands } from 'js-draw';
11
+ * import { Path, Color4 } from '@js-draw/math';
12
+ *
13
+ * const editor = new Editor(document.body);
14
+ * editor.addToolbar();
15
+ *
16
+ * // Create strokes!
17
+ * const strokes = [];
18
+ * for (let i = 0; i < 10; i++) {
19
+ * const renderablePath = pathToRenderable(
20
+ * Path.fromString(`M0,${i * 10} L100,100 L300,30 z`),
21
+ * { fill: Color4.transparent, stroke: { color: Color4.red, width: 1, } }
22
+ * );
23
+ * strokes.push(new Stroke([ renderablePath ]));
24
+ * }
25
+ *
26
+ * // Convert to commands
27
+ * const addStrokesCommands = strokes.map(stroke => editor.image.addElement(stroke));
28
+ *
29
+ * // Apply all as a single undoable command (try applying each in a loop instead!)
30
+ * await editor.dispatch(uniteCommands(addStrokesCommands));
31
+ *
32
+ * // The second parameter to uniteCommands is for very large numbers of commands, when
33
+ * // applying them shouldn't be done all at once (which would block the UI).
34
+ *
35
+ * // The second parameter to uniteCommands is for very large numbers of commands, when
36
+ * // applying them shouldn't be done all at once (which would block the UI).
37
+ * ```
38
+ */
3
39
  declare const uniteCommands: <T extends Command>(commands: T[], applyChunkSize?: number) => T extends SerializableCommand ? SerializableCommand : Command;
4
40
  export default uniteCommands;
@@ -84,6 +84,42 @@ class SerializableUnion extends SerializableCommand {
84
84
  return this.nonserializableCommand.description(editor, localizationTable);
85
85
  }
86
86
  }
87
+ /**
88
+ * Creates a single command from `commands`. This is useful when undoing should undo *all* commands
89
+ * in `commands` at once, rather than one at a time.
90
+ *
91
+ * @example
92
+ *
93
+ * ```ts,runnable
94
+ * import { Editor, pathToRenderable, Stroke, uniteCommands } from 'js-draw';
95
+ * import { Path, Color4 } from '@js-draw/math';
96
+ *
97
+ * const editor = new Editor(document.body);
98
+ * editor.addToolbar();
99
+ *
100
+ * // Create strokes!
101
+ * const strokes = [];
102
+ * for (let i = 0; i < 10; i++) {
103
+ * const renderablePath = pathToRenderable(
104
+ * Path.fromString(`M0,${i * 10} L100,100 L300,30 z`),
105
+ * { fill: Color4.transparent, stroke: { color: Color4.red, width: 1, } }
106
+ * );
107
+ * strokes.push(new Stroke([ renderablePath ]));
108
+ * }
109
+ *
110
+ * // Convert to commands
111
+ * const addStrokesCommands = strokes.map(stroke => editor.image.addElement(stroke));
112
+ *
113
+ * // Apply all as a single undoable command (try applying each in a loop instead!)
114
+ * await editor.dispatch(uniteCommands(addStrokesCommands));
115
+ *
116
+ * // The second parameter to uniteCommands is for very large numbers of commands, when
117
+ * // applying them shouldn't be done all at once (which would block the UI).
118
+ *
119
+ * // The second parameter to uniteCommands is for very large numbers of commands, when
120
+ * // applying them shouldn't be done all at once (which would block the UI).
121
+ * ```
122
+ */
87
123
  const uniteCommands = (commands, applyChunkSize) => {
88
124
  let allSerializable = true;
89
125
  for (const command of commands) {
@@ -2,12 +2,24 @@ import { Mat33Array, Rect2, Mat33, LineSegment2 } from '@js-draw/math';
2
2
  import AbstractRenderer, { RenderableImage } from '../rendering/renderers/AbstractRenderer';
3
3
  import AbstractComponent from './AbstractComponent';
4
4
  import { ImageComponentLocalization } from './localization';
5
+ /**
6
+ * Represents a raster image.
7
+ *
8
+ * **Example: Adding images**:
9
+ * [[include:doc-pages/inline-examples/adding-an-image-and-data-urls.md]]
10
+ */
5
11
  export default class ImageComponent extends AbstractComponent {
6
12
  protected contentBBox: Rect2;
7
13
  private image;
8
14
  constructor(image: RenderableImage);
9
15
  private getImageRect;
10
16
  private recomputeBBox;
17
+ /**
18
+ * Load from an image. Waits for the image to load if incomplete.
19
+ *
20
+ * The image, `elem`, must not [taint](https://developer.mozilla.org/en-US/docs/Web/HTML/CORS_enabled_image#security_and_tainted_canvases)
21
+ * an HTMLCanvasElement when rendered.
22
+ */
11
23
  static fromImage(elem: HTMLImageElement, transform: Mat33): Promise<ImageComponent>;
12
24
  render(canvas: AbstractRenderer, _visibleRect?: Rect2): void;
13
25
  getProportionalRenderingTime(): number;
@@ -1,7 +1,13 @@
1
1
  import { Rect2, Mat33 } from '@js-draw/math';
2
2
  import { assertIsNumber, assertIsNumberArray } from '../util/assertions.mjs';
3
3
  import AbstractComponent from './AbstractComponent.mjs';
4
- // Represents a raster image.
4
+ import waitForImageLoaded from '../util/waitForImageLoaded.mjs';
5
+ /**
6
+ * Represents a raster image.
7
+ *
8
+ * **Example: Adding images**:
9
+ * [[include:doc-pages/inline-examples/adding-an-image-and-data-urls.md]]
10
+ */
5
11
  export default class ImageComponent extends AbstractComponent {
6
12
  constructor(image) {
7
13
  super('image-component');
@@ -24,15 +30,14 @@ export default class ImageComponent extends AbstractComponent {
24
30
  this.contentBBox = this.getImageRect();
25
31
  this.contentBBox = this.contentBBox.transformedBoundingBox(this.image.transform);
26
32
  }
27
- // Load from an image. Waits for the image to load if incomplete.
33
+ /**
34
+ * Load from an image. Waits for the image to load if incomplete.
35
+ *
36
+ * The image, `elem`, must not [taint](https://developer.mozilla.org/en-US/docs/Web/HTML/CORS_enabled_image#security_and_tainted_canvases)
37
+ * an HTMLCanvasElement when rendered.
38
+ */
28
39
  static async fromImage(elem, transform) {
29
- if (!elem.complete) {
30
- await new Promise((resolve, reject) => {
31
- elem.onload = resolve;
32
- elem.onerror = reject;
33
- elem.onabort = reject;
34
- });
35
- }
40
+ await waitForImageLoaded(elem);
36
41
  let width, height;
37
42
  if (typeof elem.width === 'number' && typeof elem.height === 'number'
38
43
  && elem.width !== 0 && elem.height !== 0) {
@@ -74,6 +79,7 @@ export default class ImageComponent extends AbstractComponent {
74
79
  canvas.drawImage(this.image);
75
80
  canvas.endObject(this.getLoadSaveData());
76
81
  }
82
+ // A *very* rough estimate of how long it takes to render this component
77
83
  getProportionalRenderingTime() {
78
84
  // Estimate: Equivalent to a stroke with 10 segments.
79
85
  return 10;
@@ -98,6 +104,7 @@ export default class ImageComponent extends AbstractComponent {
98
104
  getAltText() {
99
105
  return this.image.label;
100
106
  }
107
+ // The base64 image URL of this image.
101
108
  getURL() {
102
109
  return this.image.base64Url;
103
110
  }
@@ -5,7 +5,7 @@ import AbstractRenderer from '../rendering/renderers/AbstractRenderer';
5
5
  import AbstractComponent from './AbstractComponent';
6
6
  import { ImageComponentLocalization } from './localization';
7
7
  import RestyleableComponent, { ComponentStyle } from './RestylableComponent';
8
- import RenderablePathSpec from '../rendering/RenderablePathSpec';
8
+ import RenderablePathSpec, { RenderablePathSpecWithPath } from '../rendering/RenderablePathSpec';
9
9
  /**
10
10
  * Represents an {@link AbstractComponent} made up of one or more {@link Path}s.
11
11
  *
@@ -21,6 +21,9 @@ import RenderablePathSpec from '../rendering/RenderablePathSpec';
21
21
  * ```ts
22
22
  * editor.dispatch(stroke.transformBy(Mat33.translation(Vec2.of(10, 0))));
23
23
  * ```
24
+ *
25
+ * **Adding**:
26
+ * [[include:doc-pages/inline-examples/adding-a-stroke.md]]
24
27
  */
25
28
  export default class Stroke extends AbstractComponent implements RestyleableComponent {
26
29
  private parts;
@@ -39,7 +42,7 @@ export default class Stroke extends AbstractComponent implements RestyleableComp
39
42
  *
40
43
  * const stroke = new Stroke([
41
44
  * // Fill with red
42
- * pathToRenderable({ fill: Color4.red })
45
+ * pathToRenderable(path, { fill: Color4.red })
43
46
  * ]);
44
47
  * ```
45
48
  */
@@ -57,6 +60,17 @@ export default class Stroke extends AbstractComponent implements RestyleableComp
57
60
  private bboxForPart;
58
61
  getExactBBox(): Rect2;
59
62
  protected applyTransformation(affineTransfm: Mat33): void;
63
+ /**
64
+ * @returns A list of the parts that make up this path. Many paths only have one part.
65
+ *
66
+ * Each part (a {@link RenderablePathSpec}) contains information about the style and geometry
67
+ * of that part of the stroke. Use the `.path` property to do collision detection and other
68
+ * operations involving the stroke's geometry.
69
+ *
70
+ * Note that many of {@link Path}'s methods (e.g. {@link Path.intersection}) take a
71
+ * `strokeWidth` parameter that can be gotten from {@link RenderablePathSpec.style} `.stroke.width`.
72
+ */
73
+ getParts(): Readonly<RenderablePathSpecWithPath>[];
60
74
  /**
61
75
  * @returns the {@link Path.union} of all paths that make up this stroke.
62
76
  */
@@ -18,6 +18,9 @@ import { pathFromRenderable, pathToRenderable, simplifyPathToFullScreenOrEmpty
18
18
  * ```ts
19
19
  * editor.dispatch(stroke.transformBy(Mat33.translation(Vec2.of(10, 0))));
20
20
  * ```
21
+ *
22
+ * **Adding**:
23
+ * [[include:doc-pages/inline-examples/adding-a-stroke.md]]
21
24
  */
22
25
  export default class Stroke extends AbstractComponent {
23
26
  /**
@@ -32,7 +35,7 @@ export default class Stroke extends AbstractComponent {
32
35
  *
33
36
  * const stroke = new Stroke([
34
37
  * // Fill with red
35
- * pathToRenderable({ fill: Color4.red })
38
+ * pathToRenderable(path, { fill: Color4.red })
36
39
  * ]);
37
40
  * ```
38
41
  */
@@ -290,6 +293,19 @@ export default class Stroke extends AbstractComponent {
290
293
  };
291
294
  });
292
295
  }
296
+ /**
297
+ * @returns A list of the parts that make up this path. Many paths only have one part.
298
+ *
299
+ * Each part (a {@link RenderablePathSpec}) contains information about the style and geometry
300
+ * of that part of the stroke. Use the `.path` property to do collision detection and other
301
+ * operations involving the stroke's geometry.
302
+ *
303
+ * Note that many of {@link Path}'s methods (e.g. {@link Path.intersection}) take a
304
+ * `strokeWidth` parameter that can be gotten from {@link RenderablePathSpec.style} `.stroke.width`.
305
+ */
306
+ getParts() {
307
+ return [...this.parts];
308
+ }
293
309
  /**
294
310
  * @returns the {@link Path.union} of all paths that make up this stroke.
295
311
  */
@@ -1,8 +1,9 @@
1
1
  import { Path, PathCommandType } from '@js-draw/math';
2
2
  import Stroke from '../Stroke.mjs';
3
- export const makeArrowBuilder = (initialPoint, viewport) => {
3
+ import makeSnapToGridAutocorrect from './autocorrect/makeSnapToGridAutocorrect.mjs';
4
+ export const makeArrowBuilder = makeSnapToGridAutocorrect((initialPoint, viewport) => {
4
5
  return new ArrowBuilder(initialPoint, viewport);
5
- };
6
+ });
6
7
  export default class ArrowBuilder {
7
8
  constructor(startPoint, viewport) {
8
9
  this.startPoint = startPoint;
@@ -2,9 +2,10 @@ import { Vec2, Path, PathCommandType, Color4 } from '@js-draw/math';
2
2
  import { pathToRenderable } from '../../rendering/RenderablePathSpec.mjs';
3
3
  import Viewport from '../../Viewport.mjs';
4
4
  import Stroke from '../Stroke.mjs';
5
- export const makeOutlinedCircleBuilder = (initialPoint, viewport) => {
5
+ import makeSnapToGridAutocorrect from './autocorrect/makeSnapToGridAutocorrect.mjs';
6
+ export const makeOutlinedCircleBuilder = makeSnapToGridAutocorrect((initialPoint, viewport) => {
6
7
  return new CircleBuilder(initialPoint, viewport);
7
- };
8
+ });
8
9
  class CircleBuilder {
9
10
  constructor(startPoint, viewport) {
10
11
  this.startPoint = startPoint;
@@ -2,13 +2,14 @@ import { Vec2, Rect2, Color4, PathCommandType } from '@js-draw/math';
2
2
  import Stroke from '../Stroke.mjs';
3
3
  import Viewport from '../../Viewport.mjs';
4
4
  import { StrokeSmoother } from '../util/StrokeSmoother.mjs';
5
- export const makeFreehandLineBuilder = (initialPoint, viewport) => {
5
+ import makeShapeFitAutocorrect from './autocorrect/makeShapeFitAutocorrect.mjs';
6
+ export const makeFreehandLineBuilder = makeShapeFitAutocorrect((initialPoint, viewport) => {
6
7
  // Don't smooth if input is more than ± 3 pixels from the true curve, do smooth if
7
8
  // less than ±1 px from the curve.
8
9
  const maxSmoothingDist = viewport.getSizeOfPixelOnCanvas() * 3;
9
10
  const minSmoothingDist = viewport.getSizeOfPixelOnCanvas();
10
11
  return new FreehandLineBuilder(initialPoint, minSmoothingDist, maxSmoothingDist, viewport);
11
- };
12
+ });
12
13
  // Handles stroke smoothing and creates Strokes from user/stylus input.
13
14
  export default class FreehandLineBuilder {
14
15
  constructor(startPoint, minFitAllowed, maxFitAllowed, viewport) {
@@ -1,9 +1,10 @@
1
1
  import { Path, PathCommandType } from '@js-draw/math';
2
2
  import { pathToRenderable } from '../../rendering/RenderablePathSpec.mjs';
3
3
  import Stroke from '../Stroke.mjs';
4
- export const makeLineBuilder = (initialPoint, viewport) => {
4
+ import makeSnapToGridAutocorrect from './autocorrect/makeSnapToGridAutocorrect.mjs';
5
+ export const makeLineBuilder = makeSnapToGridAutocorrect((initialPoint, viewport) => {
5
6
  return new LineBuilder(initialPoint, viewport);
6
- };
7
+ });
7
8
  export default class LineBuilder {
8
9
  constructor(startPoint, viewport) {
9
10
  this.startPoint = startPoint;
@@ -3,13 +3,14 @@ import { Vec2, Rect2, PathCommandType } from '@js-draw/math';
3
3
  import Stroke from '../Stroke.mjs';
4
4
  import Viewport from '../../Viewport.mjs';
5
5
  import { StrokeSmoother } from '../util/StrokeSmoother.mjs';
6
- export const makePressureSensitiveFreehandLineBuilder = (initialPoint, viewport) => {
6
+ import makeShapeFitAutocorrect from './autocorrect/makeShapeFitAutocorrect.mjs';
7
+ export const makePressureSensitiveFreehandLineBuilder = makeShapeFitAutocorrect((initialPoint, viewport) => {
7
8
  // Don't smooth if input is more than ± 3 pixels from the true curve, do smooth if
8
9
  // less than ±1 px from the curve.
9
10
  const maxSmoothingDist = viewport.getSizeOfPixelOnCanvas() * 3;
10
11
  const minSmoothingDist = viewport.getSizeOfPixelOnCanvas();
11
12
  return new PressureSensitiveFreehandLineBuilder(initialPoint, minSmoothingDist, maxSmoothingDist, viewport);
12
- };
13
+ });
13
14
  // Handles stroke smoothing and creates Strokes from user/stylus input.
14
15
  export default class PressureSensitiveFreehandLineBuilder {
15
16
  constructor(startPoint,
@@ -1,12 +1,13 @@
1
1
  import { Mat33, Rect2, Path } from '@js-draw/math';
2
2
  import { pathToRenderable } from '../../rendering/RenderablePathSpec.mjs';
3
3
  import Stroke from '../Stroke.mjs';
4
- export const makeFilledRectangleBuilder = (initialPoint, viewport) => {
4
+ import makeSnapToGridAutocorrect from './autocorrect/makeSnapToGridAutocorrect.mjs';
5
+ export const makeFilledRectangleBuilder = makeSnapToGridAutocorrect((initialPoint, viewport) => {
5
6
  return new RectangleBuilder(initialPoint, true, viewport);
6
- };
7
- export const makeOutlinedRectangleBuilder = (initialPoint, viewport) => {
7
+ });
8
+ export const makeOutlinedRectangleBuilder = makeSnapToGridAutocorrect((initialPoint, viewport) => {
8
9
  return new RectangleBuilder(initialPoint, false, viewport);
9
- };
10
+ });
10
11
  export default class RectangleBuilder {
11
12
  constructor(startPoint, filled, viewport) {
12
13
  this.startPoint = startPoint;
@@ -0,0 +1,3 @@
1
+ import { ComponentBuilderFactory } from '../types';
2
+ declare const makeShapeFitAutocorrect: (sourceFactory: ComponentBuilderFactory) => ComponentBuilderFactory;
3
+ export default makeShapeFitAutocorrect;