js-draw 0.15.1 → 0.16.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 (117) hide show
  1. package/.github/ISSUE_TEMPLATE/translation.yml +56 -0
  2. package/CHANGELOG.md +13 -0
  3. package/dist/bundle.js +1 -1
  4. package/dist/src/Color4.d.ts +1 -1
  5. package/dist/src/Color4.js +5 -1
  6. package/dist/src/Editor.d.ts +11 -2
  7. package/dist/src/Editor.js +66 -33
  8. package/dist/src/EditorImage.d.ts +28 -3
  9. package/dist/src/EditorImage.js +109 -18
  10. package/dist/src/EventDispatcher.d.ts +4 -3
  11. package/dist/src/SVGLoader.d.ts +1 -0
  12. package/dist/src/SVGLoader.js +15 -1
  13. package/dist/src/Viewport.d.ts +8 -3
  14. package/dist/src/Viewport.js +15 -8
  15. package/dist/src/components/AbstractComponent.d.ts +6 -1
  16. package/dist/src/components/AbstractComponent.js +15 -2
  17. package/dist/src/components/ImageBackground.d.ts +42 -0
  18. package/dist/src/components/ImageBackground.js +139 -0
  19. package/dist/src/components/ImageComponent.js +2 -0
  20. package/dist/src/components/builders/ArrowBuilder.d.ts +3 -1
  21. package/dist/src/components/builders/ArrowBuilder.js +43 -40
  22. package/dist/src/components/builders/LineBuilder.d.ts +3 -1
  23. package/dist/src/components/builders/LineBuilder.js +25 -28
  24. package/dist/src/components/builders/RectangleBuilder.js +1 -1
  25. package/dist/src/components/lib.d.ts +2 -1
  26. package/dist/src/components/lib.js +2 -1
  27. package/dist/src/components/localization.d.ts +2 -0
  28. package/dist/src/components/localization.js +2 -0
  29. package/dist/src/localizations/es.js +1 -1
  30. package/dist/src/math/Mat33.js +43 -5
  31. package/dist/src/math/Path.d.ts +5 -0
  32. package/dist/src/math/Path.js +80 -28
  33. package/dist/src/math/Vec3.js +1 -1
  34. package/dist/src/rendering/Display.js +1 -1
  35. package/dist/src/rendering/renderers/AbstractRenderer.d.ts +13 -1
  36. package/dist/src/rendering/renderers/AbstractRenderer.js +18 -3
  37. package/dist/src/rendering/renderers/CanvasRenderer.d.ts +2 -1
  38. package/dist/src/rendering/renderers/CanvasRenderer.js +12 -2
  39. package/dist/src/rendering/renderers/SVGRenderer.d.ts +1 -1
  40. package/dist/src/rendering/renderers/SVGRenderer.js +8 -2
  41. package/dist/src/testing/sendTouchEvent.d.ts +6 -0
  42. package/dist/src/testing/sendTouchEvent.js +26 -0
  43. package/dist/src/toolbar/HTMLToolbar.d.ts +25 -2
  44. package/dist/src/toolbar/HTMLToolbar.js +127 -15
  45. package/dist/src/toolbar/IconProvider.d.ts +2 -0
  46. package/dist/src/toolbar/IconProvider.js +45 -2
  47. package/dist/src/toolbar/localization.d.ts +5 -0
  48. package/dist/src/toolbar/localization.js +5 -0
  49. package/dist/src/toolbar/widgets/ActionButtonWidget.d.ts +3 -1
  50. package/dist/src/toolbar/widgets/ActionButtonWidget.js +5 -1
  51. package/dist/src/toolbar/widgets/BaseToolWidget.d.ts +1 -1
  52. package/dist/src/toolbar/widgets/BaseToolWidget.js +2 -1
  53. package/dist/src/toolbar/widgets/BaseWidget.d.ts +7 -2
  54. package/dist/src/toolbar/widgets/BaseWidget.js +23 -1
  55. package/dist/src/toolbar/widgets/DocumentPropertiesWidget.d.ts +19 -0
  56. package/dist/src/toolbar/widgets/DocumentPropertiesWidget.js +135 -0
  57. package/dist/src/toolbar/widgets/HandToolWidget.js +1 -1
  58. package/dist/src/toolbar/widgets/OverflowWidget.d.ts +25 -0
  59. package/dist/src/toolbar/widgets/OverflowWidget.js +65 -0
  60. package/dist/src/toolbar/widgets/lib.d.ts +1 -0
  61. package/dist/src/toolbar/widgets/lib.js +1 -0
  62. package/dist/src/tools/Eraser.js +5 -2
  63. package/dist/src/tools/PanZoom.js +12 -0
  64. package/dist/src/tools/PasteHandler.js +2 -2
  65. package/dist/src/tools/SelectionTool/Selection.d.ts +2 -1
  66. package/dist/src/tools/SelectionTool/Selection.js +3 -2
  67. package/dist/src/tools/SelectionTool/SelectionTool.js +5 -1
  68. package/package.json +1 -1
  69. package/src/Color4.test.ts +6 -0
  70. package/src/Color4.ts +6 -1
  71. package/src/Editor.loadFrom.test.ts +24 -0
  72. package/src/Editor.ts +73 -39
  73. package/src/EditorImage.ts +136 -21
  74. package/src/EventDispatcher.ts +4 -1
  75. package/src/SVGLoader.ts +12 -1
  76. package/src/Viewport.ts +17 -7
  77. package/src/components/AbstractComponent.ts +17 -1
  78. package/src/components/ImageBackground.test.ts +35 -0
  79. package/src/components/ImageBackground.ts +176 -0
  80. package/src/components/ImageComponent.ts +2 -0
  81. package/src/components/builders/ArrowBuilder.ts +44 -41
  82. package/src/components/builders/LineBuilder.ts +26 -28
  83. package/src/components/builders/RectangleBuilder.ts +1 -1
  84. package/src/components/lib.ts +2 -0
  85. package/src/components/localization.ts +4 -0
  86. package/src/localizations/es.ts +8 -0
  87. package/src/math/Mat33.test.ts +47 -3
  88. package/src/math/Mat33.ts +47 -5
  89. package/src/math/Path.ts +87 -28
  90. package/src/math/Vec3.test.ts +4 -0
  91. package/src/math/Vec3.ts +1 -1
  92. package/src/rendering/Display.ts +1 -1
  93. package/src/rendering/renderers/AbstractRenderer.ts +20 -3
  94. package/src/rendering/renderers/CanvasRenderer.ts +17 -4
  95. package/src/rendering/renderers/DummyRenderer.test.ts +1 -2
  96. package/src/rendering/renderers/SVGRenderer.ts +8 -1
  97. package/src/testing/sendTouchEvent.ts +43 -0
  98. package/src/toolbar/HTMLToolbar.ts +164 -16
  99. package/src/toolbar/IconProvider.ts +47 -2
  100. package/src/toolbar/localization.ts +10 -0
  101. package/src/toolbar/toolbar.css +2 -0
  102. package/src/toolbar/widgets/ActionButtonWidget.ts +5 -0
  103. package/src/toolbar/widgets/BaseToolWidget.ts +3 -1
  104. package/src/toolbar/widgets/BaseWidget.ts +34 -2
  105. package/src/toolbar/widgets/DocumentPropertiesWidget.ts +185 -0
  106. package/src/toolbar/widgets/HandToolWidget.ts +1 -1
  107. package/src/toolbar/widgets/OverflowWidget.css +9 -0
  108. package/src/toolbar/widgets/OverflowWidget.ts +83 -0
  109. package/src/toolbar/widgets/lib.ts +2 -1
  110. package/src/tools/Eraser.test.ts +24 -1
  111. package/src/tools/Eraser.ts +6 -2
  112. package/src/tools/PanZoom.test.ts +267 -23
  113. package/src/tools/PanZoom.ts +15 -1
  114. package/src/tools/PasteHandler.ts +3 -2
  115. package/src/tools/SelectionTool/Selection.ts +3 -2
  116. package/src/tools/SelectionTool/SelectionTool.ts +6 -1
  117. package/src/types.ts +1 -0
@@ -5,7 +5,7 @@ export default class Color4 {
5
5
  readonly g: number;
6
6
  /** Blue component. `b` ∈ [0, 1] */
7
7
  readonly b: number;
8
- /** Alpha/transparent component. `a` ∈ [0, 1] */
8
+ /** Alpha/transparent component. `a` ∈ [0, 1]. 0 = transparent */
9
9
  readonly a: number;
10
10
  private constructor();
11
11
  /**
@@ -6,7 +6,7 @@ export default class Color4 {
6
6
  g,
7
7
  /** Blue component. `b` ∈ [0, 1] */
8
8
  b,
9
- /** Alpha/transparent component. `a` ∈ [0, 1] */
9
+ /** Alpha/transparent component. `a` ∈ [0, 1]. 0 = transparent */
10
10
  a) {
11
11
  this.r = r;
12
12
  this.g = g;
@@ -103,6 +103,10 @@ export default class Color4 {
103
103
  if (other == null) {
104
104
  return false;
105
105
  }
106
+ // If both completely transparent,
107
+ if (this.a === 0 && other.a === 0) {
108
+ return true;
109
+ }
106
110
  return this.toHexString() === other.toHexString();
107
111
  }
108
112
  /**
@@ -8,6 +8,7 @@ import { Point2 } from './math/Vec2';
8
8
  import HTMLToolbar from './toolbar/HTMLToolbar';
9
9
  import { RenderablePathSpec } from './rendering/renderers/AbstractRenderer';
10
10
  import Display, { RenderingMode } from './rendering/Display';
11
+ import Color4 from './Color4';
11
12
  import Pointer from './Pointer';
12
13
  import Rect2 from './math/Rect2';
13
14
  import { EditorLocalization } from './localization';
@@ -84,8 +85,6 @@ export declare class Editor {
84
85
  * ```
85
86
  */
86
87
  readonly image: EditorImage;
87
- /** Viewport for the exported/imported image. */
88
- private importExportViewport;
89
88
  /**
90
89
  * Allows transforming the view and querying information about
91
90
  * what is currently visible.
@@ -224,7 +223,17 @@ export declare class Editor {
224
223
  addAndCenterComponents(components: AbstractComponent[], selectComponents?: boolean): Promise<void>;
225
224
  toDataURL(format?: 'image/png' | 'image/jpeg' | 'image/webp'): string;
226
225
  toSVG(): SVGElement;
226
+ /**
227
+ * Load editor data from an `ImageLoader` (e.g. an {@link SVGLoader}).
228
+ *
229
+ * @see loadFromSVG
230
+ */
227
231
  loadFrom(loader: ImageLoader): Promise<void>;
232
+ private getTopmostBackgroundComponent;
233
+ /**
234
+ * Set the background color of the image.
235
+ */
236
+ setBackgroundColor(color: Color4): Command;
228
237
  getImportExportRect(): Rect2;
229
238
  setImportExportRect(imageRect: Rect2): Command;
230
239
  /**
@@ -10,7 +10,6 @@ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, ge
10
10
  import EditorImage from './EditorImage';
11
11
  import ToolController from './tools/ToolController';
12
12
  import { InputEvtType, EditorEventType } from './types';
13
- import Command from './commands/Command';
14
13
  import UndoRedoHistory from './UndoRedoHistory';
15
14
  import Viewport from './Viewport';
16
15
  import EventDispatcher from './EventDispatcher';
@@ -31,6 +30,8 @@ import untilNextAnimationFrame from './util/untilNextAnimationFrame';
31
30
  import fileToBase64 from './util/fileToBase64';
32
31
  import uniteCommands from './commands/uniteCommands';
33
32
  import SelectionTool from './tools/SelectionTool/SelectionTool';
33
+ import Erase from './commands/Erase';
34
+ import ImageBackground, { BackgroundType } from './components/ImageBackground';
34
35
  /**
35
36
  * The main entrypoint for the full editor.
36
37
  *
@@ -119,15 +120,18 @@ export class Editor {
119
120
  this.renderingRegion.setAttribute('tabIndex', '0');
120
121
  this.renderingRegion.setAttribute('alt', '');
121
122
  this.notifier = new EventDispatcher();
122
- this.importExportViewport = new Viewport(this.notifier);
123
- this.viewport = new Viewport(this.notifier);
123
+ this.viewport = new Viewport((oldTransform, newTransform) => {
124
+ this.notifier.dispatch(EditorEventType.ViewportChanged, {
125
+ kind: EditorEventType.ViewportChanged,
126
+ newTransform,
127
+ oldTransform,
128
+ });
129
+ });
124
130
  this.display = new Display(this, this.settings.renderingMode, this.renderingRegion);
125
131
  this.image = new EditorImage();
126
132
  this.history = new UndoRedoHistory(this, this.announceRedoCallback, this.announceUndoCallback);
127
133
  this.toolController = new ToolController(this, this.localization);
128
134
  parent.appendChild(this.container);
129
- // Default to a 500x500 image
130
- this.importExportViewport.updateScreenSize(Vec2.of(500, 500));
131
135
  this.viewport.updateScreenSize(Vec2.of(this.display.width, this.display.height));
132
136
  this.registerListeners();
133
137
  this.queueRerender();
@@ -231,14 +235,21 @@ export class Editor {
231
235
  });
232
236
  this.notifier.on(EditorEventType.DisplayResized, _event => {
233
237
  this.viewport.updateScreenSize(Vec2.of(this.display.width, this.display.height));
238
+ this.queueRerender();
234
239
  });
235
- window.addEventListener('resize', () => {
240
+ const handleResize = () => {
236
241
  this.notifier.dispatch(EditorEventType.DisplayResized, {
237
242
  kind: EditorEventType.DisplayResized,
238
243
  newSize: Vec2.of(this.display.width, this.display.height),
239
244
  });
240
- this.queueRerender();
241
- });
245
+ };
246
+ if ('ResizeObserver' in window) {
247
+ const resizeObserver = new ResizeObserver(handleResize);
248
+ resizeObserver.observe(this.container);
249
+ }
250
+ else {
251
+ addEventListener('resize', handleResize);
252
+ }
242
253
  this.accessibilityControlArea.addEventListener('input', () => {
243
254
  this.accessibilityControlArea.value = '';
244
255
  });
@@ -582,7 +593,7 @@ export class Editor {
582
593
  if (showImageBounds) {
583
594
  const exportRectFill = { fill: Color4.fromHex('#44444455') };
584
595
  const exportRectStrokeWidth = 5 * this.viewport.getSizeOfPixelOnCanvas();
585
- renderer.drawRect(this.importExportViewport.visibleRect, exportRectStrokeWidth, exportRectFill);
596
+ renderer.drawRect(this.getImportExportRect(), exportRectStrokeWidth, exportRectFill);
586
597
  }
587
598
  this.rerenderQueued = false;
588
599
  this.nextRerenderListeners.forEach(listener => listener());
@@ -693,21 +704,22 @@ export class Editor {
693
704
  // The export resolution is the same as the size of the drawing canvas.
694
705
  toDataURL(format = 'image/png') {
695
706
  const canvas = document.createElement('canvas');
696
- const resolution = this.importExportViewport.getScreenRectSize();
707
+ const importExportViewport = this.image.getImportExportViewport();
708
+ const resolution = importExportViewport.getScreenRectSize();
697
709
  canvas.width = resolution.x;
698
710
  canvas.height = resolution.y;
699
711
  const ctx = canvas.getContext('2d');
700
- const renderer = new CanvasRenderer(ctx, this.importExportViewport);
712
+ const renderer = new CanvasRenderer(ctx, importExportViewport);
701
713
  this.image.renderAll(renderer);
702
714
  const dataURL = canvas.toDataURL(format);
703
715
  return dataURL;
704
716
  }
705
717
  toSVG() {
706
- const importExportViewport = this.importExportViewport;
718
+ const importExportViewport = this.image.getImportExportViewport().getTemporaryClone();
707
719
  const svgNameSpace = 'http://www.w3.org/2000/svg';
708
720
  const result = document.createElementNS(svgNameSpace, 'svg');
709
721
  const renderer = new SVGRenderer(result, importExportViewport);
710
- const origTransform = this.importExportViewport.canvasToScreenTransform;
722
+ const origTransform = importExportViewport.canvasToScreenTransform;
711
723
  // Render with (0,0) at (0,0) — we'll handle translation with
712
724
  // the viewBox property.
713
725
  importExportViewport.resetTransform(Mat33.identity);
@@ -725,10 +737,17 @@ export class Editor {
725
737
  result.setAttribute('xmlns', svgNameSpace);
726
738
  return result;
727
739
  }
740
+ /**
741
+ * Load editor data from an `ImageLoader` (e.g. an {@link SVGLoader}).
742
+ *
743
+ * @see loadFromSVG
744
+ */
728
745
  loadFrom(loader) {
729
746
  return __awaiter(this, void 0, void 0, function* () {
730
747
  this.showLoadingWarning(0);
731
748
  this.display.setDraftMode(true);
749
+ const originalBackgrounds = this.image.getBackgroundComponents();
750
+ const eraseBackgroundCommand = new Erase(originalBackgrounds);
732
751
  yield loader.start((component) => __awaiter(this, void 0, void 0, function* () {
733
752
  yield this.dispatchNoAnnounce(EditorImage.addElement(component));
734
753
  }), (countProcessed, totalToProcess) => {
@@ -742,36 +761,50 @@ export class Editor {
742
761
  this.dispatchNoAnnounce(this.setImportExportRect(importExportRect), false);
743
762
  this.dispatchNoAnnounce(this.viewport.zoomTo(importExportRect), false);
744
763
  });
764
+ // Ensure that we don't have multiple overlapping BackgroundComponents. Remove
765
+ // old BackgroundComponents.
766
+ // Overlapping BackgroundComponents may cause changing the background color to
767
+ // not work properly.
768
+ if (this.image.getBackgroundComponents().length !== originalBackgrounds.length) {
769
+ yield this.dispatchNoAnnounce(eraseBackgroundCommand);
770
+ }
745
771
  this.hideLoadingWarning();
746
772
  this.display.setDraftMode(false);
747
773
  this.queueRerender();
748
774
  });
749
775
  }
776
+ getTopmostBackgroundComponent() {
777
+ let background = null;
778
+ // Find a background component, if one exists.
779
+ // Use the last (topmost) background component if there are multiple.
780
+ for (const component of this.image.getBackgroundComponents()) {
781
+ if (component instanceof ImageBackground) {
782
+ background = component;
783
+ }
784
+ }
785
+ return background;
786
+ }
787
+ /**
788
+ * Set the background color of the image.
789
+ */
790
+ setBackgroundColor(color) {
791
+ let background = this.getTopmostBackgroundComponent();
792
+ if (!background) {
793
+ const backgroundType = color.eq(Color4.transparent) ? BackgroundType.None : BackgroundType.SolidColor;
794
+ background = new ImageBackground(backgroundType, color);
795
+ return this.image.addElement(background);
796
+ }
797
+ else {
798
+ return background.updateStyle({ color });
799
+ }
800
+ }
750
801
  // Returns the size of the visible region of the output SVG
751
802
  getImportExportRect() {
752
- return this.importExportViewport.visibleRect;
803
+ return this.image.getImportExportViewport().visibleRect;
753
804
  }
754
805
  // Resize the output SVG to match `imageRect`.
755
806
  setImportExportRect(imageRect) {
756
- const origSize = this.importExportViewport.visibleRect.size;
757
- const origTransform = this.importExportViewport.canvasToScreenTransform;
758
- return new class extends Command {
759
- apply(editor) {
760
- const viewport = editor.importExportViewport;
761
- viewport.updateScreenSize(imageRect.size);
762
- viewport.resetTransform(Mat33.translation(imageRect.topLeft.times(-1)));
763
- editor.queueRerender();
764
- }
765
- unapply(editor) {
766
- const viewport = editor.importExportViewport;
767
- viewport.updateScreenSize(origSize);
768
- viewport.resetTransform(origTransform);
769
- editor.queueRerender();
770
- }
771
- description(_editor, localizationTable) {
772
- return localizationTable.resizeOutputCommand(imageRect);
773
- }
774
- };
807
+ return this.image.setImportExportRect(imageRect);
775
808
  }
776
809
  /**
777
810
  * Alias for loadFrom(SVGLoader.fromString).
@@ -4,17 +4,38 @@ import AbstractComponent from './components/AbstractComponent';
4
4
  import Rect2 from './math/Rect2';
5
5
  import RenderingCache from './rendering/caching/RenderingCache';
6
6
  import SerializableCommand from './commands/SerializableCommand';
7
+ import EventDispatcher from './EventDispatcher';
8
+ import Command from './commands/Command';
7
9
  export declare const sortLeavesByZIndex: (leaves: Array<ImageNode>) => void;
10
+ export declare enum EditorImageEventType {
11
+ ExportViewportChanged = 0
12
+ }
13
+ export type EditorImageNotifier = EventDispatcher<EditorImageEventType, {
14
+ image: EditorImage;
15
+ }>;
8
16
  export default class EditorImage {
9
17
  private root;
18
+ private background;
10
19
  private componentsById;
20
+ /** Viewport for the exported/imported image. */
21
+ private importExportViewport;
22
+ readonly notifier: EditorImageNotifier;
11
23
  constructor();
24
+ /**
25
+ * @returns a `Viewport` for rendering the image when importing/exporting.
26
+ */
27
+ getImportExportViewport(): Viewport;
28
+ setImportExportRect(imageRect: Rect2): Command;
29
+ getBackgroundComponents(): AbstractComponent[];
12
30
  findParent(elem: AbstractComponent): ImageNode | null;
13
31
  queueRerenderOf(elem: AbstractComponent): void;
14
32
  /** @internal */
15
33
  renderWithCache(screenRenderer: AbstractRenderer, cache: RenderingCache, viewport: Viewport): void;
16
- /** @internal */
17
- render(renderer: AbstractRenderer, viewport: Viewport): void;
34
+ /**
35
+ * Renders all nodes visible from `viewport` (or all nodes if `viewport = null`)
36
+ * @internal
37
+ */
38
+ render(renderer: AbstractRenderer, viewport: Viewport | null): void;
18
39
  /** Renders all nodes, even ones not within the viewport. @internal */
19
40
  renderAll(renderer: AbstractRenderer): void;
20
41
  /** @returns all elements in the image, sorted by z-index. This can be slow for large images. */
@@ -30,6 +51,7 @@ export default class EditorImage {
30
51
  */
31
52
  lookupElement(id: string): AbstractComponent | null;
32
53
  private addElementDirectly;
54
+ private removeElementDirectly;
33
55
  /**
34
56
  * Returns a command that adds the given element to the `EditorImage`.
35
57
  * If `applyByFlattening` is true, the content of the wet ink renderer is
@@ -38,6 +60,8 @@ export default class EditorImage {
38
60
  * @see {@link Display.flatten}
39
61
  */
40
62
  static addElement(elem: AbstractComponent, applyByFlattening?: boolean): SerializableCommand;
63
+ /** @see EditorImage.addElement */
64
+ addElement(elem: AbstractComponent, applyByFlattening?: boolean): SerializableCommand;
41
65
  private static AddElementCommand;
42
66
  }
43
67
  type TooSmallToRenderCheck = (rect: Rect2) => boolean;
@@ -58,6 +82,7 @@ export declare class ImageNode {
58
82
  private getChildrenIntersectingRegion;
59
83
  getChildrenOrSelfIntersectingRegion(region: Rect2): ImageNode[];
60
84
  getLeavesIntersectingRegion(region: Rect2, isTooSmall?: TooSmallToRenderCheck): ImageNode[];
85
+ getChildWithContent(target: AbstractComponent): ImageNode | null;
61
86
  getLeaves(): ImageNode[];
62
87
  addLeaf(leaf: AbstractComponent): ImageNode;
63
88
  getBBox(): Rect2;
@@ -65,6 +90,6 @@ export declare class ImageNode {
65
90
  private updateParents;
66
91
  private rebalance;
67
92
  remove(): void;
68
- render(renderer: AbstractRenderer, visibleRect: Rect2): void;
93
+ render(renderer: AbstractRenderer, visibleRect?: Rect2): void;
69
94
  }
70
95
  export {};
@@ -1,27 +1,82 @@
1
1
  var _a;
2
+ import Viewport from './Viewport';
2
3
  import AbstractComponent from './components/AbstractComponent';
3
4
  import Rect2 from './math/Rect2';
4
5
  import SerializableCommand from './commands/SerializableCommand';
6
+ import EventDispatcher from './EventDispatcher';
7
+ import { Vec2 } from './math/Vec2';
8
+ import Command from './commands/Command';
9
+ import Mat33 from './math/Mat33';
5
10
  // @internal Sort by z-index, low to high
6
11
  export const sortLeavesByZIndex = (leaves) => {
7
12
  leaves.sort((a, b) => a.getContent().getZIndex() - b.getContent().getZIndex());
8
13
  };
14
+ export var EditorImageEventType;
15
+ (function (EditorImageEventType) {
16
+ EditorImageEventType[EditorImageEventType["ExportViewportChanged"] = 0] = "ExportViewportChanged";
17
+ })(EditorImageEventType || (EditorImageEventType = {}));
9
18
  // Handles lookup/storage of elements in the image
10
19
  export default class EditorImage {
11
20
  // @internal
12
21
  constructor() {
13
22
  this.root = new ImageNode();
23
+ this.background = new ImageNode();
14
24
  this.componentsById = {};
25
+ this.notifier = new EventDispatcher();
26
+ this.importExportViewport = new Viewport(() => {
27
+ this.notifier.dispatch(EditorImageEventType.ExportViewportChanged, {
28
+ image: this,
29
+ });
30
+ });
31
+ // Default to a 500x500 image
32
+ this.importExportViewport.updateScreenSize(Vec2.of(500, 500));
15
33
  }
16
- // Returns the parent of the given element, if it exists.
17
- findParent(elem) {
18
- const candidates = this.root.getLeavesIntersectingRegion(elem.getBBox());
19
- for (const candidate of candidates) {
20
- if (candidate.getContent() === elem) {
21
- return candidate;
34
+ /**
35
+ * @returns a `Viewport` for rendering the image when importing/exporting.
36
+ */
37
+ getImportExportViewport() {
38
+ return this.importExportViewport;
39
+ }
40
+ setImportExportRect(imageRect) {
41
+ const importExportViewport = this.getImportExportViewport();
42
+ const origSize = importExportViewport.visibleRect.size;
43
+ const origTransform = importExportViewport.canvasToScreenTransform;
44
+ return new class extends Command {
45
+ apply(editor) {
46
+ const viewport = editor.image.getImportExportViewport();
47
+ viewport.updateScreenSize(imageRect.size);
48
+ viewport.resetTransform(Mat33.translation(imageRect.topLeft.times(-1)));
49
+ editor.queueRerender();
50
+ }
51
+ unapply(editor) {
52
+ const viewport = editor.image.getImportExportViewport();
53
+ viewport.updateScreenSize(origSize);
54
+ viewport.resetTransform(origTransform);
55
+ editor.queueRerender();
56
+ }
57
+ description(_editor, localizationTable) {
58
+ return localizationTable.resizeOutputCommand(imageRect);
59
+ }
60
+ };
61
+ }
62
+ // Returns all components that make up the background of this image. These
63
+ // components are rendered below all other components.
64
+ getBackgroundComponents() {
65
+ const result = [];
66
+ const leaves = this.background.getLeaves();
67
+ sortLeavesByZIndex(leaves);
68
+ for (const leaf of leaves) {
69
+ const content = leaf.getContent();
70
+ if (content) {
71
+ result.push(content);
22
72
  }
23
73
  }
24
- return null;
74
+ return result;
75
+ }
76
+ // Returns the parent of the given element, if it exists.
77
+ findParent(elem) {
78
+ var _a;
79
+ return (_a = this.background.getChildWithContent(elem)) !== null && _a !== void 0 ? _a : this.root.getChildWithContent(elem);
25
80
  }
26
81
  // Forces a re-render of `elem` when the image is next re-rendered as a whole.
27
82
  // Does nothing if `elem` is not in this.
@@ -36,19 +91,20 @@ export default class EditorImage {
36
91
  }
37
92
  /** @internal */
38
93
  renderWithCache(screenRenderer, cache, viewport) {
94
+ this.background.render(screenRenderer, viewport.visibleRect);
39
95
  cache.render(screenRenderer, this.root, viewport);
40
96
  }
41
- /** @internal */
97
+ /**
98
+ * Renders all nodes visible from `viewport` (or all nodes if `viewport = null`)
99
+ * @internal
100
+ */
42
101
  render(renderer, viewport) {
43
- this.root.render(renderer, viewport.visibleRect);
102
+ this.background.render(renderer, viewport === null || viewport === void 0 ? void 0 : viewport.visibleRect);
103
+ this.root.render(renderer, viewport === null || viewport === void 0 ? void 0 : viewport.visibleRect);
44
104
  }
45
105
  /** Renders all nodes, even ones not within the viewport. @internal */
46
106
  renderAll(renderer) {
47
- const leaves = this.root.getLeaves();
48
- sortLeavesByZIndex(leaves);
49
- for (const leaf of leaves) {
50
- leaf.getContent().render(renderer, leaf.getBBox());
51
- }
107
+ this.render(renderer, null);
52
108
  }
53
109
  /** @returns all elements in the image, sorted by z-index. This can be slow for large images. */
54
110
  getAllElements() {
@@ -76,8 +132,21 @@ export default class EditorImage {
76
132
  return (_a = this.componentsById[id]) !== null && _a !== void 0 ? _a : null;
77
133
  }
78
134
  addElementDirectly(elem) {
135
+ elem.onAddToImage(this);
79
136
  this.componentsById[elem.getId()] = elem;
80
- return this.root.addLeaf(elem);
137
+ // If a background component, add to the background. Else,
138
+ // add to the normal component tree.
139
+ const parentTree = elem.isBackground() ? this.background : this.root;
140
+ return parentTree.addLeaf(elem);
141
+ }
142
+ removeElementDirectly(element) {
143
+ const container = this.findParent(element);
144
+ container === null || container === void 0 ? void 0 : container.remove();
145
+ if (container) {
146
+ this.onDestroyElement(element);
147
+ return true;
148
+ }
149
+ return false;
81
150
  }
82
151
  /**
83
152
  * Returns a command that adds the given element to the `EditorImage`.
@@ -89,6 +158,10 @@ export default class EditorImage {
89
158
  static addElement(elem, applyByFlattening = false) {
90
159
  return new EditorImage.AddElementCommand(elem, applyByFlattening);
91
160
  }
161
+ /** @see EditorImage.addElement */
162
+ addElement(elem, applyByFlattening) {
163
+ return EditorImage.addElement(elem, applyByFlattening);
164
+ }
92
165
  }
93
166
  // A Command that can access private [EditorImage] functionality
94
167
  EditorImage.AddElementCommand = (_a = class extends SerializableCommand {
@@ -117,8 +190,7 @@ EditorImage.AddElementCommand = (_a = class extends SerializableCommand {
117
190
  }
118
191
  }
119
192
  unapply(editor) {
120
- const container = editor.image.findParent(this.element);
121
- container === null || container === void 0 ? void 0 : container.remove();
193
+ editor.image.removeElementDirectly(this.element);
122
194
  editor.queueRerender();
123
195
  }
124
196
  description(_editor, localization) {
@@ -194,6 +266,17 @@ export class ImageNode {
194
266
  }
195
267
  return result;
196
268
  }
269
+ // Returns the child of this with the target content or `null` if no
270
+ // such child exists.
271
+ getChildWithContent(target) {
272
+ const candidates = this.getLeavesIntersectingRegion(target.getBBox());
273
+ for (const candidate of candidates) {
274
+ if (candidate.getContent() === target) {
275
+ return candidate;
276
+ }
277
+ }
278
+ return null;
279
+ }
197
280
  // Returns a list of leaves with this as an ancestor.
198
281
  // Like getLeavesInRegion, but does not check whether ancestors are in a given rectangle
199
282
  getLeaves() {
@@ -305,6 +388,8 @@ export class ImageNode {
305
388
  }
306
389
  // Remove this node and all of its children
307
390
  remove() {
391
+ var _a;
392
+ (_a = this.content) === null || _a === void 0 ? void 0 : _a.onRemoveFromImage();
308
393
  if (!this.parent) {
309
394
  this.content = null;
310
395
  this.children = [];
@@ -325,7 +410,13 @@ export class ImageNode {
325
410
  this.children = [];
326
411
  }
327
412
  render(renderer, visibleRect) {
328
- const leaves = this.getLeavesIntersectingRegion(visibleRect, rect => renderer.isTooSmallToRender(rect));
413
+ let leaves;
414
+ if (visibleRect) {
415
+ leaves = this.getLeavesIntersectingRegion(visibleRect, rect => renderer.isTooSmallToRender(rect));
416
+ }
417
+ else {
418
+ leaves = this.getLeaves();
419
+ }
329
420
  sortLeavesByZIndex(leaves);
330
421
  for (const leaf of leaves) {
331
422
  // Leaves by definition have content
@@ -16,13 +16,14 @@
16
16
  * @packageDocumentation
17
17
  */
18
18
  type CallbackHandler<EventType> = (data: EventType) => void;
19
+ export interface DispatcherEventListener {
20
+ remove: () => void;
21
+ }
19
22
  export default class EventDispatcher<EventKeyType extends string | symbol | number, EventMessageType> {
20
23
  private listeners;
21
24
  constructor();
22
25
  dispatch(eventName: EventKeyType, event: EventMessageType): void;
23
- on(eventName: EventKeyType, callback: CallbackHandler<EventMessageType>): {
24
- remove: () => boolean;
25
- };
26
+ on(eventName: EventKeyType, callback: CallbackHandler<EventMessageType>): DispatcherEventListener;
26
27
  /** Removes an event listener. This is equivalent to calling `.remove()` on the object returned by `.on`. */
27
28
  off(eventName: EventKeyType, callback: CallbackHandler<EventMessageType>): void;
28
29
  }
@@ -24,6 +24,7 @@ export default class SVGLoader implements ImageLoader {
24
24
  private strokeDataFromElem;
25
25
  private attachUnrecognisedAttrs;
26
26
  private addPath;
27
+ private addBackground;
27
28
  private getTransform;
28
29
  private makeText;
29
30
  private addText;
@@ -8,6 +8,7 @@ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, ge
8
8
  });
9
9
  };
10
10
  import Color4 from './Color4';
11
+ import ImageBackground, { BackgroundType, imageBackgroundCSSClassName } from './components/ImageBackground';
11
12
  import ImageComponent from './components/ImageComponent';
12
13
  import Stroke from './components/Stroke';
13
14
  import SVGGlobalAttributesObject from './components/SVGGlobalAttributesObject';
@@ -145,6 +146,14 @@ export default class SVGLoader {
145
146
  yield ((_a = this.onAddComponent) === null || _a === void 0 ? void 0 : _a.call(this, elem));
146
147
  });
147
148
  }
149
+ addBackground(node) {
150
+ var _a, _b, _c;
151
+ return __awaiter(this, void 0, void 0, function* () {
152
+ const fill = Color4.fromString((_b = (_a = node.getAttribute('fill')) !== null && _a !== void 0 ? _a : node.style.fill) !== null && _b !== void 0 ? _b : 'black');
153
+ const elem = new ImageBackground(BackgroundType.SolidColor, fill);
154
+ yield ((_c = this.onAddComponent) === null || _c === void 0 ? void 0 : _c.call(this, elem));
155
+ });
156
+ }
148
157
  // If given, 'supportedAttrs' will have x, y, etc. attributes that were used in computing the transform added to it,
149
158
  // to prevent storing duplicate transform information when saving the component.
150
159
  getTransform(elem, supportedAttrs, computedStyles) {
@@ -307,7 +316,12 @@ export default class SVGLoader {
307
316
  // Continue -- visit the node's children.
308
317
  break;
309
318
  case 'path':
310
- yield this.addPath(node);
319
+ if (node.classList.contains(imageBackgroundCSSClassName)) {
320
+ yield this.addBackground(node);
321
+ }
322
+ else {
323
+ yield this.addPath(node);
324
+ }
311
325
  break;
312
326
  case 'text':
313
327
  yield this.addText(node);
@@ -4,18 +4,23 @@ import Rect2 from './math/Rect2';
4
4
  import { Point2, Vec2 } from './math/Vec2';
5
5
  import Vec3 from './math/Vec3';
6
6
  import { StrokeDataPoint } from './types';
7
- import { EditorNotifier } from './types';
8
7
  type PointDataType<T extends Point2 | StrokeDataPoint | number> = T extends Point2 ? Point2 : number;
9
8
  export declare abstract class ViewportTransform extends Command {
10
9
  abstract readonly transform: Mat33;
11
10
  }
11
+ type TransformChangeCallback = (oldTransform: Mat33, newTransform: Mat33) => void;
12
12
  export declare class Viewport {
13
- private notifier;
13
+ private onTransformChangeCallback;
14
14
  private static ViewportTransform;
15
15
  private transform;
16
16
  private inverseTransform;
17
17
  private screenRect;
18
- constructor(notifier: EditorNotifier);
18
+ constructor(onTransformChangeCallback: TransformChangeCallback);
19
+ /**
20
+ * @returns a temporary copy of `this` that does not notify when modified. This is
21
+ * useful when rendering with a temporarily different viewport.
22
+ */
23
+ getTemporaryClone(): Viewport;
19
24
  updateScreenSize(screenSize: Vec2): void;
20
25
  /** Get the screen's visible region transformed into canvas space. */
21
26
  get visibleRect(): Rect2;
@@ -15,16 +15,26 @@ import Mat33 from './math/Mat33';
15
15
  import Rect2 from './math/Rect2';
16
16
  import { Vec2 } from './math/Vec2';
17
17
  import Vec3 from './math/Vec3';
18
- import { EditorEventType } from './types';
19
18
  export class ViewportTransform extends Command {
20
19
  }
21
20
  export class Viewport {
22
21
  // @internal
23
- constructor(notifier) {
24
- this.notifier = notifier;
22
+ constructor(onTransformChangeCallback) {
23
+ this.onTransformChangeCallback = onTransformChangeCallback;
25
24
  this.resetTransform(Mat33.identity);
26
25
  this.screenRect = Rect2.empty;
27
26
  }
27
+ /**
28
+ * @returns a temporary copy of `this` that does not notify when modified. This is
29
+ * useful when rendering with a temporarily different viewport.
30
+ */
31
+ getTemporaryClone() {
32
+ const result = new Viewport(() => { });
33
+ result.transform = this.transform;
34
+ result.inverseTransform = this.inverseTransform;
35
+ result.screenRect = this.screenRect;
36
+ return result;
37
+ }
28
38
  // @internal
29
39
  updateScreenSize(screenSize) {
30
40
  this.screenRect = this.screenRect.resizedTo(screenSize);
@@ -50,14 +60,11 @@ export class Viewport {
50
60
  * @param newTransform - should map from canvas coordinates to screen coordinates.
51
61
  */
52
62
  resetTransform(newTransform = Mat33.identity) {
63
+ var _a;
53
64
  const oldTransform = this.transform;
54
65
  this.transform = newTransform;
55
66
  this.inverseTransform = newTransform.inverse();
56
- this.notifier.dispatch(EditorEventType.ViewportChanged, {
57
- kind: EditorEventType.ViewportChanged,
58
- newTransform,
59
- oldTransform,
60
- });
67
+ (_a = this.onTransformChangeCallback) === null || _a === void 0 ? void 0 : _a.call(this, oldTransform, newTransform);
61
68
  }
62
69
  get screenToCanvasTransform() {
63
70
  return this.inverseTransform;