js-draw 0.3.2 → 0.4.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (104) hide show
  1. package/.github/pull_request_template.md +15 -0
  2. package/.github/workflows/firebase-hosting-merge.yml +7 -0
  3. package/.github/workflows/firebase-hosting-pull-request.yml +10 -0
  4. package/.github/workflows/github-pages.yml +2 -0
  5. package/CHANGELOG.md +16 -1
  6. package/README.md +1 -3
  7. package/dist/bundle.js +1 -1
  8. package/dist/src/Editor.d.ts +11 -0
  9. package/dist/src/Editor.js +107 -77
  10. package/dist/src/Pointer.d.ts +1 -1
  11. package/dist/src/Pointer.js +8 -3
  12. package/dist/src/Viewport.d.ts +1 -0
  13. package/dist/src/Viewport.js +14 -1
  14. package/dist/src/components/AbstractComponent.js +1 -0
  15. package/dist/src/components/ImageComponent.d.ts +2 -2
  16. package/dist/src/components/Stroke.js +15 -9
  17. package/dist/src/components/Text.d.ts +1 -1
  18. package/dist/src/components/Text.js +1 -1
  19. package/dist/src/components/builders/FreehandLineBuilder.d.ts +1 -0
  20. package/dist/src/components/builders/FreehandLineBuilder.js +34 -36
  21. package/dist/src/language/assertions.d.ts +1 -0
  22. package/dist/src/language/assertions.js +5 -0
  23. package/dist/src/math/Mat33.d.ts +38 -2
  24. package/dist/src/math/Mat33.js +30 -1
  25. package/dist/src/math/Path.d.ts +1 -1
  26. package/dist/src/math/Path.js +10 -8
  27. package/dist/src/math/Vec3.d.ts +12 -2
  28. package/dist/src/math/Vec3.js +16 -1
  29. package/dist/src/math/rounding.d.ts +1 -0
  30. package/dist/src/math/rounding.js +13 -6
  31. package/dist/src/rendering/renderers/AbstractRenderer.js +2 -1
  32. package/dist/src/testing/beforeEachFile.d.ts +1 -0
  33. package/dist/src/testing/beforeEachFile.js +3 -0
  34. package/dist/src/testing/createEditor.d.ts +1 -0
  35. package/dist/src/testing/createEditor.js +7 -1
  36. package/dist/src/testing/loadExpectExtensions.d.ts +0 -15
  37. package/dist/src/toolbar/HTMLToolbar.js +5 -4
  38. package/dist/src/toolbar/widgets/SelectionToolWidget.d.ts +1 -1
  39. package/dist/src/tools/PasteHandler.js +3 -1
  40. package/dist/src/tools/Pen.js +1 -1
  41. package/dist/src/tools/SelectionTool/Selection.d.ts +54 -0
  42. package/dist/src/tools/SelectionTool/Selection.js +337 -0
  43. package/dist/src/tools/SelectionTool/SelectionHandle.d.ts +35 -0
  44. package/dist/src/tools/SelectionTool/SelectionHandle.js +75 -0
  45. package/dist/src/tools/SelectionTool/SelectionTool.d.ts +31 -0
  46. package/dist/src/tools/SelectionTool/SelectionTool.js +284 -0
  47. package/dist/src/tools/SelectionTool/TransformMode.d.ts +34 -0
  48. package/dist/src/tools/SelectionTool/TransformMode.js +98 -0
  49. package/dist/src/tools/SelectionTool/types.d.ts +9 -0
  50. package/dist/src/tools/SelectionTool/types.js +11 -0
  51. package/dist/src/tools/ToolController.js +1 -1
  52. package/dist/src/tools/lib.d.ts +1 -1
  53. package/dist/src/tools/lib.js +1 -1
  54. package/dist/src/types.d.ts +1 -1
  55. package/jest.config.js +5 -0
  56. package/package.json +15 -14
  57. package/src/Editor.css +1 -0
  58. package/src/Editor.ts +147 -108
  59. package/src/Pointer.ts +8 -3
  60. package/src/Viewport.ts +17 -2
  61. package/src/components/AbstractComponent.ts +4 -6
  62. package/src/components/ImageComponent.ts +2 -6
  63. package/src/components/Stroke.test.ts +0 -3
  64. package/src/components/Stroke.ts +14 -7
  65. package/src/components/Text.test.ts +0 -3
  66. package/src/components/Text.ts +4 -8
  67. package/src/components/builders/FreehandLineBuilder.ts +37 -43
  68. package/src/language/assertions.ts +6 -0
  69. package/src/math/LineSegment2.test.ts +8 -10
  70. package/src/math/Mat33.test.ts +14 -2
  71. package/src/math/Mat33.ts +43 -2
  72. package/src/math/Path.toString.test.ts +12 -1
  73. package/src/math/Path.ts +11 -9
  74. package/src/math/Rect2.test.ts +0 -3
  75. package/src/math/Vec2.test.ts +0 -3
  76. package/src/math/Vec3.test.ts +0 -3
  77. package/src/math/Vec3.ts +23 -2
  78. package/src/math/rounding.test.ts +30 -5
  79. package/src/math/rounding.ts +16 -7
  80. package/src/rendering/renderers/AbstractRenderer.ts +3 -2
  81. package/src/testing/beforeEachFile.ts +3 -0
  82. package/src/testing/createEditor.ts +8 -1
  83. package/src/testing/global.d.ts +17 -0
  84. package/src/testing/loadExpectExtensions.ts +0 -15
  85. package/src/toolbar/HTMLToolbar.ts +5 -4
  86. package/src/toolbar/toolbar.css +3 -2
  87. package/src/toolbar/widgets/SelectionToolWidget.ts +1 -1
  88. package/src/tools/PasteHandler.ts +4 -1
  89. package/src/tools/Pen.test.ts +150 -0
  90. package/src/tools/Pen.ts +1 -1
  91. package/src/tools/SelectionTool/Selection.ts +455 -0
  92. package/src/tools/SelectionTool/SelectionHandle.ts +99 -0
  93. package/src/tools/SelectionTool/SelectionTool.css +22 -0
  94. package/src/tools/{SelectionTool.test.ts → SelectionTool/SelectionTool.test.ts} +21 -21
  95. package/src/tools/SelectionTool/SelectionTool.ts +344 -0
  96. package/src/tools/SelectionTool/TransformMode.ts +114 -0
  97. package/src/tools/SelectionTool/types.ts +11 -0
  98. package/src/tools/ToolController.ts +1 -1
  99. package/src/tools/lib.ts +1 -1
  100. package/src/types.ts +1 -1
  101. package/tsconfig.json +3 -1
  102. package/dist/src/tools/SelectionTool.d.ts +0 -65
  103. package/dist/src/tools/SelectionTool.js +0 -647
  104. package/src/tools/SelectionTool.ts +0 -797
package/src/Editor.ts CHANGED
@@ -1,18 +1,18 @@
1
1
  /**
2
2
  * The main entrypoint for the full editor.
3
- *
3
+ *
4
4
  * @example
5
5
  * To create an editor with a toolbar,
6
6
  * ```
7
7
  * const editor = new Editor(document.body);
8
- *
8
+ *
9
9
  * const toolbar = editor.addToolbar();
10
10
  * toolbar.addActionButton('Save', () => {
11
11
  * const saveData = editor.toSVG().outerHTML;
12
12
  * // Do something with saveData...
13
13
  * });
14
14
  * ```
15
- *
15
+ *
16
16
  * @packageDocumentation
17
17
  */
18
18
 
@@ -38,6 +38,9 @@ import Rect2 from './math/Rect2';
38
38
  import { EditorLocalization } from './localization';
39
39
  import getLocalizationTable from './localizations/getLocalizationTable';
40
40
 
41
+ type HTMLPointerEventType = 'pointerdown'|'pointermove'|'pointerup'|'pointercancel';
42
+ type HTMLPointerEventFilter = (eventName: HTMLPointerEventType, event: PointerEvent)=>boolean;
43
+
41
44
  export interface EditorSettings {
42
45
  /** Defaults to `RenderingMode.CanvasRenderer` */
43
46
  renderingMode: RenderingMode,
@@ -67,14 +70,14 @@ export class Editor {
67
70
 
68
71
  /**
69
72
  * Handles undo/redo.
70
- *
73
+ *
71
74
  * @example
72
75
  * ```
73
76
  * const editor = new Editor(document.body);
74
- *
77
+ *
75
78
  * // Do something undoable.
76
79
  * // ...
77
- *
80
+ *
78
81
  * // Undo the last action
79
82
  * editor.history.undo();
80
83
  * ```
@@ -83,17 +86,17 @@ export class Editor {
83
86
 
84
87
  /**
85
88
  * Data structure for adding/removing/querying objects in the image.
86
- *
89
+ *
87
90
  * @example
88
91
  * ```
89
92
  * const editor = new Editor(document.body);
90
- *
93
+ *
91
94
  * // Create a path.
92
95
  * const stroke = new Stroke([
93
96
  * Path.fromString('M0,0 L30,30 z').toRenderable({ fill: Color4.black }),
94
97
  * ]);
95
98
  * const addElementCommand = editor.image.addElement(stroke);
96
- *
99
+ *
97
100
  * // Add the stroke to the editor
98
101
  * editor.dispatch(addElementCommand);
99
102
  * ```
@@ -126,14 +129,14 @@ export class Editor {
126
129
  * @example
127
130
  * ```
128
131
  * const container = document.body;
129
- *
132
+ *
130
133
  * // Create an editor
131
134
  * const editor = new Editor(container, {
132
135
  * // 2e-10 and 1e12 are the default values for minimum/maximum zoom.
133
136
  * minZoom: 2e-10,
134
137
  * maxZoom: 1e12,
135
138
  * });
136
- *
139
+ *
137
140
  * // Add the default toolbar
138
141
  * const toolbar = editor.addToolbar();
139
142
  * toolbar.addActionButton({
@@ -224,7 +227,7 @@ export class Editor {
224
227
  if (oldZoom <= this.settings.maxZoom && oldZoom >= this.settings.minZoom) {
225
228
  resetTransform = evt.oldTransform;
226
229
  }
227
-
230
+
228
231
  this.viewport.resetTransform(resetTransform);
229
232
  }
230
233
  }
@@ -233,7 +236,7 @@ export class Editor {
233
236
 
234
237
  /**
235
238
  * @returns a reference to the editor's container.
236
- *
239
+ *
237
240
  * @example
238
241
  * ```
239
242
  * editor.getRootElement().style.height = '500px';
@@ -285,96 +288,7 @@ export class Editor {
285
288
  }
286
289
 
287
290
  private registerListeners() {
288
- const pointers: Record<number, Pointer> = {};
289
- const getPointerList = () => {
290
- const nowTime = (new Date()).getTime();
291
-
292
- const res: Pointer[] = [];
293
- for (const id in pointers) {
294
- const maxUnupdatedTime = 2000; // Maximum time without a pointer update (ms)
295
- if (pointers[id] && (nowTime - pointers[id].timeStamp) < maxUnupdatedTime) {
296
- res.push(pointers[id]);
297
- }
298
- }
299
- return res;
300
- };
301
-
302
- // May be required to prevent text selection on iOS/Safari:
303
- // See https://stackoverflow.com/a/70992717/17055750
304
- this.renderingRegion.addEventListener('touchstart', evt => evt.preventDefault());
305
- this.renderingRegion.addEventListener('contextmenu', evt => {
306
- // Don't show a context menu
307
- evt.preventDefault();
308
- });
309
-
310
- this.renderingRegion.addEventListener('pointerdown', evt => {
311
- const pointer = Pointer.ofEvent(evt, true, this.viewport);
312
- pointers[pointer.id] = pointer;
313
-
314
- this.renderingRegion.setPointerCapture(pointer.id);
315
- const event: PointerEvt = {
316
- kind: InputEvtType.PointerDownEvt,
317
- current: pointer,
318
- allPointers: getPointerList(),
319
- };
320
- this.toolController.dispatchInputEvent(event);
321
-
322
- return true;
323
- });
324
-
325
- this.renderingRegion.addEventListener('pointermove', evt => {
326
- const pointer = Pointer.ofEvent(
327
- evt, pointers[evt.pointerId]?.down ?? false, this.viewport
328
- );
329
- if (pointer.down) {
330
- const prevData = pointers[pointer.id];
331
-
332
- if (prevData) {
333
- const distanceMoved = pointer.screenPos.minus(prevData.screenPos).magnitude();
334
-
335
- // If the pointer moved less than two pixels, don't send a new event.
336
- if (distanceMoved < 2) {
337
- return;
338
- }
339
- }
340
-
341
- pointers[pointer.id] = pointer;
342
- if (this.toolController.dispatchInputEvent({
343
- kind: InputEvtType.PointerMoveEvt,
344
- current: pointer,
345
- allPointers: getPointerList(),
346
- })) {
347
- evt.preventDefault();
348
- }
349
- }
350
- });
351
-
352
- const pointerEnd = (evt: PointerEvent) => {
353
- const pointer = Pointer.ofEvent(evt, false, this.viewport);
354
- if (!pointers[pointer.id]) {
355
- return;
356
- }
357
-
358
- pointers[pointer.id] = pointer;
359
- this.renderingRegion.releasePointerCapture(pointer.id);
360
- if (this.toolController.dispatchInputEvent({
361
- kind: InputEvtType.PointerUpEvt,
362
- current: pointer,
363
- allPointers: getPointerList(),
364
- })) {
365
- evt.preventDefault();
366
- }
367
- delete pointers[pointer.id];
368
- };
369
-
370
- this.renderingRegion.addEventListener('pointerup', evt => {
371
- pointerEnd(evt);
372
- });
373
-
374
- this.renderingRegion.addEventListener('pointercancel', evt => {
375
- pointerEnd(evt);
376
- });
377
-
291
+ this.handlePointerEventsFrom(this.renderingRegion);
378
292
  this.handleKeyEventsFrom(this.renderingRegion);
379
293
 
380
294
  this.container.addEventListener('wheel', evt => {
@@ -404,7 +318,10 @@ export class Editor {
404
318
  delta = Vec3.of(0, 0, evt.deltaY);
405
319
  }
406
320
 
407
- const pos = Vec2.of(evt.offsetX, evt.offsetY);
321
+ // Ensure that `pos` is relative to `this.container`
322
+ const bbox = this.container.getBoundingClientRect();
323
+ const pos = Vec2.of(evt.clientX, evt.clientY).minus(Vec2.of(bbox.left, bbox.top));
324
+
408
325
  if (this.toolController.dispatchInputEvent({
409
326
  kind: InputEvtType.WheelEvt,
410
327
  delta,
@@ -459,6 +376,91 @@ export class Editor {
459
376
  });
460
377
  }
461
378
 
379
+ private pointers: Record<number, Pointer> = {};
380
+ private getPointerList() {
381
+ const nowTime = (new Date()).getTime();
382
+
383
+ const res: Pointer[] = [];
384
+ for (const id in this.pointers) {
385
+ const maxUnupdatedTime = 2000; // Maximum time without a pointer update (ms)
386
+ if (this.pointers[id] && (nowTime - this.pointers[id].timeStamp) < maxUnupdatedTime) {
387
+ res.push(this.pointers[id]);
388
+ }
389
+ }
390
+ return res;
391
+ }
392
+
393
+ /**
394
+ * Dispatches a `PointerEvent` to the editor. The target element for `evt` must have the same top left
395
+ * as the content of the editor.
396
+ */
397
+ public handleHTMLPointerEvent(eventType: 'pointerdown'|'pointermove'|'pointerup'|'pointercancel', evt: PointerEvent): boolean {
398
+ const eventsRelativeTo = this.renderingRegion;
399
+ const eventTarget = (evt.target as HTMLElement|null) ?? this.renderingRegion;
400
+
401
+ if (eventType === 'pointerdown') {
402
+ const pointer = Pointer.ofEvent(evt, true, this.viewport, eventsRelativeTo);
403
+ this.pointers[pointer.id] = pointer;
404
+
405
+ eventTarget.setPointerCapture(pointer.id);
406
+ const event: PointerEvt = {
407
+ kind: InputEvtType.PointerDownEvt,
408
+ current: pointer,
409
+ allPointers: this.getPointerList(),
410
+ };
411
+ this.toolController.dispatchInputEvent(event);
412
+
413
+ return true;
414
+ }
415
+ else if (eventType === 'pointermove') {
416
+ const pointer = Pointer.ofEvent(
417
+ evt, this.pointers[evt.pointerId]?.down ?? false, this.viewport, eventsRelativeTo
418
+ );
419
+ if (pointer.down) {
420
+ const prevData = this.pointers[pointer.id];
421
+
422
+ if (prevData) {
423
+ const distanceMoved = pointer.screenPos.minus(prevData.screenPos).magnitude();
424
+
425
+ // If the pointer moved less than two pixels, don't send a new event.
426
+ if (distanceMoved < 2) {
427
+ return false;
428
+ }
429
+ }
430
+
431
+ this.pointers[pointer.id] = pointer;
432
+ if (this.toolController.dispatchInputEvent({
433
+ kind: InputEvtType.PointerMoveEvt,
434
+ current: pointer,
435
+ allPointers: this.getPointerList(),
436
+ })) {
437
+ evt.preventDefault();
438
+ }
439
+ }
440
+ return true;
441
+ }
442
+ else if (eventType === 'pointercancel' || eventType === 'pointerup') {
443
+ const pointer = Pointer.ofEvent(evt, false, this.viewport, eventsRelativeTo);
444
+ if (!this.pointers[pointer.id]) {
445
+ return false;
446
+ }
447
+
448
+ this.pointers[pointer.id] = pointer;
449
+ eventTarget.releasePointerCapture(pointer.id);
450
+ if (this.toolController.dispatchInputEvent({
451
+ kind: InputEvtType.PointerUpEvt,
452
+ current: pointer,
453
+ allPointers: this.getPointerList(),
454
+ })) {
455
+ evt.preventDefault();
456
+ }
457
+ delete this.pointers[pointer.id];
458
+ return true;
459
+ }
460
+
461
+ return eventType;
462
+ }
463
+
462
464
  private isEventSink(evtTarget: Element|EventTarget|null) {
463
465
  let currentElem: Element|null = evtTarget as Element|null;
464
466
  while (currentElem !== null) {
@@ -553,6 +555,27 @@ export class Editor {
553
555
  }
554
556
  }
555
557
 
558
+ public handlePointerEventsFrom(elem: HTMLElement, filter?: HTMLPointerEventFilter) {
559
+ // May be required to prevent text selection on iOS/Safari:
560
+ // See https://stackoverflow.com/a/70992717/17055750
561
+ elem.addEventListener('touchstart', evt => evt.preventDefault());
562
+ elem.addEventListener('contextmenu', evt => {
563
+ // Don't show a context menu
564
+ evt.preventDefault();
565
+ });
566
+
567
+ const eventNames: HTMLPointerEventType[] = ['pointerdown', 'pointermove', 'pointerup', 'pointercancel'];
568
+ for (const eventName of eventNames) {
569
+ elem.addEventListener(eventName, evt => {
570
+ if (filter && !filter(eventName, evt)) {
571
+ return true;
572
+ }
573
+
574
+ return this.handleHTMLPointerEvent(eventName, evt);
575
+ });
576
+ }
577
+ }
578
+
556
579
  /** Adds event listners for keypresses to `elem` and forwards those events to the editor. */
557
580
  public handleKeyEventsFrom(elem: HTMLElement) {
558
581
  elem.addEventListener('keydown', evt => {
@@ -567,7 +590,7 @@ export class Editor {
567
590
  evt.preventDefault();
568
591
  } else if (evt.key === 'Escape') {
569
592
  this.renderingRegion.blur();
570
- }
593
+ }
571
594
  });
572
595
 
573
596
  elem.addEventListener('keyup', evt => {
@@ -609,11 +632,11 @@ export class Editor {
609
632
  * Dispatches a command without announcing it. By default, does not add to history.
610
633
  * Use this to show finalized commands that don't need to have `announceForAccessibility`
611
634
  * called.
612
- *
635
+ *
613
636
  * Prefer `command.apply(editor)` for incomplete commands. `dispatchNoAnnounce` may allow
614
637
  * clients to listen for the application of commands (e.g. `SerializableCommand`s so they can
615
638
  * be sent across the network), while `apply` does not.
616
- *
639
+ *
617
640
  * @example
618
641
  * ```
619
642
  * const addToHistory = false;
@@ -755,11 +778,27 @@ export class Editor {
755
778
  return styleSheet;
756
779
  }
757
780
 
781
+ // Dispatch a keyboard event to the currently selected tool.
782
+ // Intended for unit testing
783
+ public sendKeyboardEvent(
784
+ eventType: InputEvtType.KeyPressEvent|InputEvtType.KeyUpEvent,
785
+ key: string,
786
+ ctrlKey: boolean = false
787
+ ) {
788
+ this.toolController.dispatchInputEvent({
789
+ kind: eventType,
790
+ key,
791
+ ctrlKey
792
+ });
793
+ }
794
+
758
795
  // Dispatch a pen event to the currently selected tool.
759
796
  // Intended primarially for unit tests.
760
797
  public sendPenEvent(
761
798
  eventType: InputEvtType.PointerDownEvt|InputEvtType.PointerMoveEvt|InputEvtType.PointerUpEvt,
762
799
  point: Point2,
800
+
801
+ // @deprecated
763
802
  allPointers?: Pointer[]
764
803
  ) {
765
804
  const mainPointer = Pointer.ofCanvasPoint(
@@ -864,7 +903,7 @@ export class Editor {
864
903
 
865
904
  /**
866
905
  * Alias for loadFrom(SVGLoader.fromString).
867
- *
906
+ *
868
907
  * This is particularly useful when accessing a bundled version of the editor,
869
908
  * where `SVGLoader.fromString` is unavailable.
870
909
  */
package/src/Pointer.ts CHANGED
@@ -36,9 +36,14 @@ export default class Pointer {
36
36
  ) {
37
37
  }
38
38
 
39
- // Creates a Pointer from a DOM event.
40
- public static ofEvent(evt: PointerEvent, isDown: boolean, viewport: Viewport): Pointer {
41
- const screenPos = Vec2.of(evt.offsetX, evt.offsetY);
39
+ // Creates a Pointer from a DOM event. If `relativeTo` is given, (0, 0) in screen coordinates is
40
+ // considered the top left of `relativeTo`.
41
+ public static ofEvent(evt: PointerEvent, isDown: boolean, viewport: Viewport, relativeTo?: HTMLElement): Pointer {
42
+ let screenPos = Vec2.of(evt.clientX, evt.clientY);
43
+ if (relativeTo) {
44
+ const bbox = relativeTo.getBoundingClientRect();
45
+ screenPos = screenPos.minus(Vec2.of(bbox.left, bbox.top));
46
+ }
42
47
 
43
48
  const pointerTypeToDevice: Record<string, PointerDevice> = {
44
49
  'mouse': PointerDevice.PrimaryButtonMouse,
package/src/Viewport.ts CHANGED
@@ -146,7 +146,8 @@ export class Viewport {
146
146
  return 1/this.getScaleFactor();
147
147
  }
148
148
 
149
- // Returns the angle of the canvas in radians
149
+ // Returns the angle of the canvas in radians.
150
+ // This is the angle by which the canvas is rotated relative to the screen.
150
151
  public getRotationAngle(): number {
151
152
  return this.transform.transformVec3(Vec3.unitX).angle();
152
153
  }
@@ -175,12 +176,26 @@ export class Viewport {
175
176
  return point.map(roundComponent);
176
177
  }
177
178
 
178
-
179
179
  // Round a point with a tolerance of ±1 screen unit.
180
180
  public roundPoint(point: Point2): Point2 {
181
181
  return Viewport.roundPoint(point, 1 / this.getScaleFactor());
182
182
  }
183
183
 
184
+ // `roundAmount`: An integer >= 0, larger numbers cause less rounding. Smaller numbers cause more
185
+ // (as such `roundAmount = 0` does the most rounding).
186
+ public static roundScaleRatio(scaleRatio: number, roundAmount: number = 1): number {
187
+ if (Math.abs(scaleRatio) <= 1e-12) {
188
+ return 0;
189
+ }
190
+
191
+ // Represent as k 10ⁿ for some n, k ∈ ℤ.
192
+ const decimalComponent = 10 ** Math.floor(Math.log10(Math.abs(scaleRatio)));
193
+ const roundAnountFactor = 2 ** roundAmount;
194
+ scaleRatio = Math.round(scaleRatio / decimalComponent * roundAnountFactor) / roundAnountFactor * decimalComponent;
195
+
196
+ return scaleRatio;
197
+ }
198
+
184
199
  // Computes and returns an affine transformation that makes `toMakeVisible` visible and roughly centered on the screen.
185
200
  public computeZoomToTransform(toMakeVisible: Rect2, allowZoomIn: boolean = true, allowZoomOut: boolean = true): Mat33 {
186
201
  let transform = Mat33.identity;
@@ -2,7 +2,7 @@ import SerializableCommand from '../commands/SerializableCommand';
2
2
  import Editor from '../Editor';
3
3
  import EditorImage from '../EditorImage';
4
4
  import LineSegment2 from '../math/LineSegment2';
5
- import Mat33 from '../math/Mat33';
5
+ import Mat33, { Mat33Array } from '../math/Mat33';
6
6
  import Rect2 from '../math/Rect2';
7
7
  import { EditorLocalization } from '../localization';
8
8
  import AbstractRenderer from '../rendering/renderers/AbstractRenderer';
@@ -183,11 +183,7 @@ export default abstract class AbstractComponent {
183
183
  SerializableCommand.register(AbstractComponent.transformElementCommandId, (json: any, editor: Editor) => {
184
184
  const elem = editor.image.lookupElement(json.id);
185
185
 
186
- const transform = new Mat33(...(json.transfm as [
187
- number, number, number,
188
- number, number, number,
189
- number, number, number,
190
- ]));
186
+ const transform = new Mat33(...(json.transfm as Mat33Array));
191
187
 
192
188
  if (!elem) {
193
189
  return new AbstractComponent.UnresolvedTransformElementCommand(transform, json.id);
@@ -210,8 +206,10 @@ export default abstract class AbstractComponent {
210
206
 
211
207
  public abstract description(localizationTable: ImageComponentLocalization): string;
212
208
 
209
+ // Component-specific implementation of {@link clone}.
213
210
  protected abstract createClone(): AbstractComponent;
214
211
 
212
+ // Returns a copy of this component.
215
213
  public clone() {
216
214
  const clone = this.createClone();
217
215
 
@@ -1,5 +1,5 @@
1
1
  import LineSegment2 from '../math/LineSegment2';
2
- import Mat33 from '../math/Mat33';
2
+ import Mat33, { Mat33Array } from '../math/Mat33';
3
3
  import Rect2 from '../math/Rect2';
4
4
  import AbstractRenderer, { RenderableImage } from '../rendering/renderers/AbstractRenderer';
5
5
  import AbstractComponent from './AbstractComponent';
@@ -141,11 +141,7 @@ export default class ImageComponent extends AbstractComponent {
141
141
  image: image,
142
142
  base64Url: image.src,
143
143
  label: data.label,
144
- transform: new Mat33(...(data.transform as [
145
- number, number, number,
146
- number, number, number,
147
- number, number, number,
148
- ])),
144
+ transform: new Mat33(...(data.transform as Mat33Array)),
149
145
  });
150
146
  }
151
147
  }
@@ -2,12 +2,9 @@ import Color4 from '../Color4';
2
2
  import Path from '../math/Path';
3
3
  import { Vec2 } from '../math/Vec2';
4
4
  import Stroke from './Stroke';
5
- import { loadExpectExtensions } from '../testing/loadExpectExtensions';
6
5
  import createEditor from '../testing/createEditor';
7
6
  import Mat33 from '../math/Mat33';
8
7
 
9
- loadExpectExtensions();
10
-
11
8
  describe('Stroke', () => {
12
9
  it('empty stroke should have an empty bounding box', () => {
13
10
  const stroke = new Stroke([{
@@ -18,7 +18,8 @@ export default class Stroke extends AbstractComponent {
18
18
  public constructor(parts: RenderablePathSpec[]) {
19
19
  super('stroke');
20
20
 
21
- this.parts = parts.map((section): StrokePart => {
21
+ this.parts = [];
22
+ for (const section of parts) {
22
23
  const path = Path.fromRenderable(section);
23
24
  const pathBBox = this.bboxForPart(path.bbox, section.style);
24
25
 
@@ -28,15 +29,15 @@ export default class Stroke extends AbstractComponent {
28
29
  this.contentBBox = this.contentBBox.union(pathBBox);
29
30
  }
30
31
 
31
- return {
32
+ this.parts.push({
32
33
  path,
33
34
 
34
35
  // To implement RenderablePathSpec
35
36
  startPoint: path.startPoint,
36
37
  style: section.style,
37
38
  commands: path.parts,
38
- };
39
- });
39
+ });
40
+ }
40
41
  this.contentBBox ??= Rect2.empty;
41
42
  }
42
43
 
@@ -104,9 +105,15 @@ export default class Stroke extends AbstractComponent {
104
105
  }
105
106
 
106
107
  public getPath() {
107
- return this.parts.reduce((accumulator: Path|null, current: StrokePart) => {
108
- return accumulator?.union(current.path) ?? current.path;
109
- }, null) ?? Path.empty;
108
+ let result: Path|null = null;
109
+ for (const part of this.parts) {
110
+ if (result) {
111
+ result = result.union(part.path);
112
+ } else {
113
+ result ??= part.path;
114
+ }
115
+ }
116
+ return result ?? Path.empty;
110
117
  }
111
118
 
112
119
  public description(localization: ImageComponentLocalization): string {
@@ -3,9 +3,6 @@ import Mat33 from '../math/Mat33';
3
3
  import Rect2 from '../math/Rect2';
4
4
  import AbstractComponent from './AbstractComponent';
5
5
  import Text, { TextStyle } from './Text';
6
- import { loadExpectExtensions } from '../testing/loadExpectExtensions';
7
-
8
- loadExpectExtensions();
9
6
 
10
7
  const estimateTextBounds = (text: string, style: TextStyle): Rect2 => {
11
8
  const widthEst = text.length * style.size;
@@ -1,5 +1,5 @@
1
1
  import LineSegment2 from '../math/LineSegment2';
2
- import Mat33 from '../math/Mat33';
2
+ import Mat33, { Mat33Array } from '../math/Mat33';
3
3
  import Rect2 from '../math/Rect2';
4
4
  import AbstractRenderer from '../rendering/renderers/AbstractRenderer';
5
5
  import RenderingStyle, { styleFromJSON, styleToJSON } from '../rendering/RenderingStyle';
@@ -135,7 +135,7 @@ export default class Text extends AbstractComponent {
135
135
  return new Text(this.textObjects, this.transform, this.style);
136
136
  }
137
137
 
138
- private getText() {
138
+ public getText() {
139
139
  const result: string[] = [];
140
140
 
141
141
  for (const textObject of this.textObjects) {
@@ -146,7 +146,7 @@ export default class Text extends AbstractComponent {
146
146
  }
147
147
  }
148
148
 
149
- return result.join(' ');
149
+ return result.join('\n');
150
150
  }
151
151
 
152
152
  public description(localizationTable: ImageComponentLocalization): string {
@@ -200,11 +200,7 @@ export default class Text extends AbstractComponent {
200
200
  throw new Error(`Unable to deserialize transform, ${json.transform}.`);
201
201
  }
202
202
 
203
- const transformData = json.transform as [
204
- number, number, number,
205
- number, number, number,
206
- number, number, number,
207
- ];
203
+ const transformData = json.transform as Mat33Array;
208
204
  const transform = new Mat33(...transformData);
209
205
 
210
206
  return new Text(textObjects, transform, style, getTextDimens);