js-draw 1.2.1 → 1.3.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 (122) hide show
  1. package/README.md +30 -30
  2. package/dist/Editor.css +70 -4
  3. package/dist/bundle.js +2 -2
  4. package/dist/bundledStyles.js +1 -1
  5. package/dist/cjs/Editor.d.ts +73 -40
  6. package/dist/cjs/Editor.js +90 -24
  7. package/dist/cjs/EditorImage.d.ts +58 -6
  8. package/dist/cjs/EditorImage.js +336 -60
  9. package/dist/cjs/SVGLoader.d.ts +10 -4
  10. package/dist/cjs/SVGLoader.js +30 -10
  11. package/dist/cjs/UndoRedoHistory.d.ts +2 -2
  12. package/dist/cjs/UndoRedoHistory.js +4 -2
  13. package/dist/cjs/Viewport.d.ts +2 -1
  14. package/dist/cjs/Viewport.js +12 -3
  15. package/dist/cjs/commands/Command.d.ts +1 -0
  16. package/dist/cjs/commands/Command.js +1 -0
  17. package/dist/cjs/commands/Erase.js +1 -1
  18. package/dist/cjs/commands/SerializableCommand.d.ts +1 -1
  19. package/dist/cjs/commands/SerializableCommand.js +16 -2
  20. package/dist/cjs/commands/localization.d.ts +2 -0
  21. package/dist/cjs/commands/localization.js +2 -0
  22. package/dist/cjs/components/AbstractComponent.d.ts +38 -0
  23. package/dist/cjs/components/AbstractComponent.js +31 -0
  24. package/dist/cjs/components/BackgroundComponent.d.ts +10 -1
  25. package/dist/cjs/components/BackgroundComponent.js +60 -6
  26. package/dist/cjs/components/SVGGlobalAttributesObject.d.ts +2 -1
  27. package/dist/cjs/components/SVGGlobalAttributesObject.js +30 -1
  28. package/dist/cjs/components/Stroke.d.ts +1 -0
  29. package/dist/cjs/components/Stroke.js +44 -0
  30. package/dist/cjs/components/UnknownSVGObject.d.ts +2 -1
  31. package/dist/cjs/components/UnknownSVGObject.js +30 -1
  32. package/dist/cjs/components/lib.d.ts +2 -2
  33. package/dist/cjs/components/lib.js +15 -2
  34. package/dist/cjs/lib.d.ts +2 -45
  35. package/dist/cjs/lib.js +2 -45
  36. package/dist/cjs/rendering/RenderablePathSpec.d.ts +1 -0
  37. package/dist/cjs/rendering/RenderablePathSpec.js +1 -0
  38. package/dist/cjs/rendering/RenderingStyle.d.ts +1 -0
  39. package/dist/cjs/rendering/lib.d.ts +1 -0
  40. package/dist/cjs/rendering/lib.js +5 -1
  41. package/dist/cjs/rendering/renderers/AbstractRenderer.js +1 -1
  42. package/dist/cjs/shortcuts/KeyboardShortcutManager.d.ts +2 -2
  43. package/dist/cjs/shortcuts/KeyboardShortcutManager.js +2 -2
  44. package/dist/cjs/toolbar/localization.d.ts +1 -0
  45. package/dist/cjs/toolbar/localization.js +1 -0
  46. package/dist/cjs/toolbar/widgets/BaseWidget.js +5 -0
  47. package/dist/cjs/toolbar/widgets/DocumentPropertiesWidget.js +54 -25
  48. package/dist/cjs/toolbar/widgets/components/makeGridSelector.js +8 -0
  49. package/dist/cjs/tools/PanZoom.js +13 -8
  50. package/dist/cjs/tools/ScrollbarTool.d.ts +18 -0
  51. package/dist/cjs/tools/ScrollbarTool.js +85 -0
  52. package/dist/cjs/tools/SelectionTool/SelectionTool.selecting.test.d.ts +1 -0
  53. package/dist/cjs/tools/ToolController.js +2 -0
  54. package/dist/cjs/types.d.ts +3 -1
  55. package/dist/cjs/util/adjustEditorThemeForContrast.js +1 -0
  56. package/dist/cjs/util/assertions.d.ts +4 -0
  57. package/dist/cjs/util/assertions.js +12 -1
  58. package/dist/cjs/version.js +1 -1
  59. package/dist/mjs/Editor.d.ts +73 -40
  60. package/dist/mjs/Editor.mjs +90 -24
  61. package/dist/mjs/EditorImage.d.ts +58 -6
  62. package/dist/mjs/EditorImage.mjs +313 -61
  63. package/dist/mjs/SVGLoader.d.ts +10 -4
  64. package/dist/mjs/SVGLoader.mjs +29 -9
  65. package/dist/mjs/UndoRedoHistory.d.ts +2 -2
  66. package/dist/mjs/UndoRedoHistory.mjs +4 -2
  67. package/dist/mjs/Viewport.d.ts +2 -1
  68. package/dist/mjs/Viewport.mjs +12 -3
  69. package/dist/mjs/commands/Command.d.ts +1 -0
  70. package/dist/mjs/commands/Command.mjs +1 -0
  71. package/dist/mjs/commands/Erase.mjs +1 -1
  72. package/dist/mjs/commands/SerializableCommand.d.ts +1 -1
  73. package/dist/mjs/commands/SerializableCommand.mjs +16 -2
  74. package/dist/mjs/commands/localization.d.ts +2 -0
  75. package/dist/mjs/commands/localization.mjs +2 -0
  76. package/dist/mjs/components/AbstractComponent.d.ts +38 -0
  77. package/dist/mjs/components/AbstractComponent.mjs +30 -0
  78. package/dist/mjs/components/BackgroundComponent.d.ts +10 -1
  79. package/dist/mjs/components/BackgroundComponent.mjs +37 -6
  80. package/dist/mjs/components/SVGGlobalAttributesObject.d.ts +2 -1
  81. package/dist/mjs/components/SVGGlobalAttributesObject.mjs +7 -1
  82. package/dist/mjs/components/Stroke.d.ts +1 -0
  83. package/dist/mjs/components/Stroke.mjs +44 -0
  84. package/dist/mjs/components/UnknownSVGObject.d.ts +2 -1
  85. package/dist/mjs/components/UnknownSVGObject.mjs +7 -1
  86. package/dist/mjs/components/lib.d.ts +2 -2
  87. package/dist/mjs/components/lib.mjs +2 -2
  88. package/dist/mjs/lib.d.ts +2 -45
  89. package/dist/mjs/lib.mjs +2 -45
  90. package/dist/mjs/rendering/RenderablePathSpec.d.ts +1 -0
  91. package/dist/mjs/rendering/RenderablePathSpec.mjs +1 -0
  92. package/dist/mjs/rendering/RenderingStyle.d.ts +1 -0
  93. package/dist/mjs/rendering/lib.d.ts +1 -0
  94. package/dist/mjs/rendering/lib.mjs +1 -0
  95. package/dist/mjs/rendering/renderers/AbstractRenderer.mjs +1 -1
  96. package/dist/mjs/shortcuts/KeyboardShortcutManager.d.ts +2 -2
  97. package/dist/mjs/shortcuts/KeyboardShortcutManager.mjs +2 -2
  98. package/dist/mjs/toolbar/localization.d.ts +1 -0
  99. package/dist/mjs/toolbar/localization.mjs +1 -0
  100. package/dist/mjs/toolbar/widgets/BaseWidget.mjs +5 -0
  101. package/dist/mjs/toolbar/widgets/DocumentPropertiesWidget.mjs +54 -25
  102. package/dist/mjs/toolbar/widgets/components/makeGridSelector.mjs +8 -0
  103. package/dist/mjs/tools/PanZoom.mjs +13 -8
  104. package/dist/mjs/tools/ScrollbarTool.d.ts +18 -0
  105. package/dist/mjs/tools/ScrollbarTool.mjs +79 -0
  106. package/dist/mjs/tools/SelectionTool/SelectionTool.selecting.test.d.ts +1 -0
  107. package/dist/mjs/tools/ToolController.mjs +2 -0
  108. package/dist/mjs/types.d.ts +3 -1
  109. package/dist/mjs/util/adjustEditorThemeForContrast.mjs +1 -0
  110. package/dist/mjs/util/assertions.d.ts +4 -0
  111. package/dist/mjs/util/assertions.mjs +10 -0
  112. package/dist/mjs/version.mjs +1 -1
  113. package/package.json +3 -4
  114. package/src/Editor.scss +8 -0
  115. package/src/dialogs/dialogs.scss +2 -1
  116. package/src/toolbar/AbstractToolbar.scss +3 -0
  117. package/src/toolbar/EdgeToolbar.scss +4 -1
  118. package/src/toolbar/widgets/DocumentPropertiesWidget.scss +12 -0
  119. package/src/toolbar/widgets/components/makeGridSelector.scss +6 -1
  120. package/src/tools/ScrollbarTool.scss +57 -0
  121. package/src/tools/{SoundUITool.css → SoundUITool.scss} +4 -0
  122. package/src/tools/tools.scss +2 -1
@@ -5,9 +5,11 @@ import { Rect2 } from '@js-draw/math';
5
5
  import RenderingCache from './rendering/caching/RenderingCache';
6
6
  import SerializableCommand from './commands/SerializableCommand';
7
7
  import EventDispatcher from './EventDispatcher';
8
+ import Command from './commands/Command';
8
9
  export declare const sortLeavesByZIndex: (leaves: Array<ImageNode>) => void;
9
10
  export declare enum EditorImageEventType {
10
- ExportViewportChanged = 0
11
+ ExportViewportChanged = 0,
12
+ AutoresizeModeChanged = 1
11
13
  }
12
14
  export type EditorImageNotifier = EventDispatcher<EditorImageEventType, {
13
15
  image: EditorImage;
@@ -19,6 +21,7 @@ export default class EditorImage {
19
21
  private componentCount;
20
22
  /** Viewport for the exported/imported image. */
21
23
  private importExportViewport;
24
+ private shouldAutoresizeExportViewport;
22
25
  readonly notifier: EditorImageNotifier;
23
26
  constructor();
24
27
  getBackgroundComponents(): AbstractComponent[];
@@ -35,14 +38,20 @@ export default class EditorImage {
35
38
  render(renderer: AbstractRenderer, viewport: Viewport | null): void;
36
39
  /** Renders all nodes, even ones not within the viewport. @internal */
37
40
  renderAll(renderer: AbstractRenderer): void;
38
- /** @returns all elements in the image, sorted by z-index. This can be slow for large images. */
41
+ /**
42
+ * @returns all elements in the image, sorted by z-index. This can be slow for large images.
43
+ *
44
+ * Does not include background elements. See {@link getBackgroundComponents}.
45
+ */
39
46
  getAllElements(): AbstractComponent[];
40
47
  /** Returns the number of elements added to this image. @internal */
41
48
  estimateNumElements(): number;
42
49
  /** @returns a list of `AbstractComponent`s intersecting `region`, sorted by z-index. */
43
- getElementsIntersectingRegion(region: Rect2): AbstractComponent[];
44
- /** Called whenever an element is completely removed. @internal */
50
+ getElementsIntersectingRegion(region: Rect2, includeBackground?: boolean): AbstractComponent[];
51
+ /** Called whenever (just after) an element is completely removed. @internal */
45
52
  onDestroyElement(elem: AbstractComponent): void;
53
+ /** Called just after an element is added. @internal */
54
+ private onElementAdded;
46
55
  /**
47
56
  * @returns the AbstractComponent with `id`, if it exists.
48
57
  *
@@ -66,11 +75,40 @@ export default class EditorImage {
66
75
  * @returns a `Viewport` for rendering the image when importing/exporting.
67
76
  */
68
77
  getImportExportViewport(): Viewport;
78
+ /**
79
+ * @see {@link setImportExportRect}
80
+ */
81
+ getImportExportRect(): Rect2;
82
+ /**
83
+ * Sets the import/export rectangle to the given `imageRect`. Disables
84
+ * autoresize (if it was previously enabled).
85
+ */
69
86
  setImportExportRect(imageRect: Rect2): SerializableCommand;
87
+ getAutoresizeEnabled(): boolean;
88
+ /** Returns a `Command` that sets whether the image should autoresize. */
89
+ setAutoresizeEnabled(autoresize: boolean): Command;
90
+ private setAutoresizeEnabledDirectly;
91
+ /** Updates the size/position of the viewport */
92
+ private autoresizeExportViewport;
93
+ private settingExportRect;
94
+ /**
95
+ * Sets the import/export viewport directly, without returning a `Command`.
96
+ * As such, this is not undoable.
97
+ *
98
+ * See setImportExportRect
99
+ *
100
+ * Returns true if changes to the viewport were made (and thus
101
+ * ExportViewportChanged was fired.)
102
+ */
103
+ private setExportRectDirectly;
104
+ private onExportViewportChanged;
70
105
  private static SetImportExportRectCommand;
71
106
  }
72
107
  type TooSmallToRenderCheck = (rect: Rect2) => boolean;
73
- /** Part of the Editor's image. @internal */
108
+ /**
109
+ * Part of the Editor's image. Does not handle fullscreen/invisible components.
110
+ * @internal
111
+ */
74
112
  export declare class ImageNode {
75
113
  private parent;
76
114
  private content;
@@ -84,17 +122,31 @@ export declare class ImageNode {
84
122
  onContentChange(): void;
85
123
  getContent(): AbstractComponent | null;
86
124
  getParent(): ImageNode | null;
87
- private getChildrenIntersectingRegion;
125
+ protected getChildrenIntersectingRegion(region: Rect2, isTooSmallFilter?: TooSmallToRenderCheck): ImageNode[];
88
126
  getChildrenOrSelfIntersectingRegion(region: Rect2): ImageNode[];
89
127
  getLeavesIntersectingRegion(region: Rect2, isTooSmall?: TooSmallToRenderCheck): ImageNode[];
90
128
  getChildWithContent(target: AbstractComponent): ImageNode | null;
91
129
  getLeaves(): ImageNode[];
92
130
  addLeaf(leaf: AbstractComponent): ImageNode;
131
+ protected static createLeafNode(parent: ImageNode, content: AbstractComponent): ImageNode;
93
132
  getBBox(): Rect2;
94
133
  recomputeBBox(bubbleUp: boolean): void;
134
+ private unionBBoxWith;
95
135
  private updateParents;
96
136
  private rebalance;
137
+ protected removeChild(child: ImageNode): void;
97
138
  remove(): void;
98
139
  render(renderer: AbstractRenderer, visibleRect?: Rect2): void;
140
+ renderDebugBoundingBoxes(renderer: AbstractRenderer, visibleRect: Rect2, depth?: number): void;
141
+ }
142
+ /** An `ImageNode` that can properly handle fullscreen/data components. @internal */
143
+ export declare class RootImageNode extends ImageNode {
144
+ private fullscreenChildren;
145
+ private dataComponents;
146
+ protected getChildrenIntersectingRegion(region: Rect2, _isTooSmall?: TooSmallToRenderCheck): ImageNode[];
147
+ getLeaves(): ImageNode[];
148
+ removeChild(child: ImageNode): void;
149
+ getChildWithContent(target: AbstractComponent): ImageNode | null;
150
+ addLeaf(leafContent: AbstractComponent): ImageNode;
99
151
  }
100
152
  export {};
@@ -4,11 +4,12 @@ var __setFunctionName = (this && this.__setFunctionName) || function (f, name, p
4
4
  };
5
5
  var _a, _b, _c;
6
6
  import Viewport from './Viewport.mjs';
7
- import AbstractComponent from './components/AbstractComponent.mjs';
8
- import { Rect2, Vec2, Mat33 } from '@js-draw/math';
7
+ import AbstractComponent, { ComponentSizingMode } from './components/AbstractComponent.mjs';
8
+ import { Rect2, Vec2, Mat33, Color4 } from '@js-draw/math';
9
9
  import SerializableCommand from './commands/SerializableCommand.mjs';
10
10
  import EventDispatcher from './EventDispatcher.mjs';
11
- import { assertIsNumber, assertIsNumberArray } from './util/assertions.mjs';
11
+ import { assertIsBoolean, assertIsNumber, assertIsNumberArray } from './util/assertions.mjs';
12
+ import Command from './commands/Command.mjs';
12
13
  // @internal Sort by z-index, low to high
13
14
  export const sortLeavesByZIndex = (leaves) => {
14
15
  leaves.sort((a, b) => a.getContent().getZIndex() - b.getContent().getZIndex());
@@ -16,23 +17,25 @@ export const sortLeavesByZIndex = (leaves) => {
16
17
  export var EditorImageEventType;
17
18
  (function (EditorImageEventType) {
18
19
  EditorImageEventType[EditorImageEventType["ExportViewportChanged"] = 0] = "ExportViewportChanged";
20
+ EditorImageEventType[EditorImageEventType["AutoresizeModeChanged"] = 1] = "AutoresizeModeChanged";
19
21
  })(EditorImageEventType || (EditorImageEventType = {}));
22
+ const debugMode = false;
20
23
  // Handles lookup/storage of elements in the image
21
24
  class EditorImage {
22
25
  // @internal
23
26
  constructor() {
24
27
  this.componentCount = 0;
25
- this.root = new ImageNode();
26
- this.background = new ImageNode();
28
+ this.settingExportRect = false;
29
+ this.root = new RootImageNode();
30
+ this.background = new RootImageNode();
27
31
  this.componentsById = {};
28
32
  this.notifier = new EventDispatcher();
29
33
  this.importExportViewport = new Viewport(() => {
30
- this.notifier.dispatch(EditorImageEventType.ExportViewportChanged, {
31
- image: this,
32
- });
34
+ this.onExportViewportChanged();
33
35
  });
34
36
  // Default to a 500x500 image
35
37
  this.importExportViewport.updateScreenSize(Vec2.of(500, 500));
38
+ this.shouldAutoresizeExportViewport = false;
36
39
  }
37
40
  // Returns all components that make up the background of this image. These
38
41
  // components are rendered below all other components.
@@ -66,7 +69,13 @@ class EditorImage {
66
69
  /** @internal */
67
70
  renderWithCache(screenRenderer, cache, viewport) {
68
71
  this.background.render(screenRenderer, viewport.visibleRect);
69
- cache.render(screenRenderer, this.root, viewport);
72
+ // If in debug mode, avoid rendering with cache to show additional debug information
73
+ if (!debugMode) {
74
+ cache.render(screenRenderer, this.root, viewport);
75
+ }
76
+ else {
77
+ this.root.render(screenRenderer, viewport.visibleRect);
78
+ }
70
79
  }
71
80
  /**
72
81
  * Renders all nodes visible from `viewport` (or all nodes if `viewport = null`).
@@ -82,7 +91,11 @@ class EditorImage {
82
91
  renderAll(renderer) {
83
92
  this.render(renderer, null);
84
93
  }
85
- /** @returns all elements in the image, sorted by z-index. This can be slow for large images. */
94
+ /**
95
+ * @returns all elements in the image, sorted by z-index. This can be slow for large images.
96
+ *
97
+ * Does not include background elements. See {@link getBackgroundComponents}.
98
+ */
86
99
  getAllElements() {
87
100
  const leaves = this.root.getLeaves();
88
101
  sortLeavesByZIndex(leaves);
@@ -93,15 +106,25 @@ class EditorImage {
93
106
  return this.componentCount;
94
107
  }
95
108
  /** @returns a list of `AbstractComponent`s intersecting `region`, sorted by z-index. */
96
- getElementsIntersectingRegion(region) {
97
- const leaves = this.root.getLeavesIntersectingRegion(region);
109
+ getElementsIntersectingRegion(region, includeBackground = false) {
110
+ let leaves = this.root.getLeavesIntersectingRegion(region);
111
+ if (includeBackground) {
112
+ leaves = leaves.concat(this.background.getLeavesIntersectingRegion(region));
113
+ }
98
114
  sortLeavesByZIndex(leaves);
99
115
  return leaves.map(leaf => leaf.getContent());
100
116
  }
101
- /** Called whenever an element is completely removed. @internal */
117
+ /** Called whenever (just after) an element is completely removed. @internal */
102
118
  onDestroyElement(elem) {
103
119
  this.componentCount--;
104
120
  delete this.componentsById[elem.getId()];
121
+ this.autoresizeExportViewport();
122
+ }
123
+ /** Called just after an element is added. @internal */
124
+ onElementAdded(elem) {
125
+ this.componentCount++;
126
+ this.componentsById[elem.getId()] = elem;
127
+ this.autoresizeExportViewport();
105
128
  }
106
129
  /**
107
130
  * @returns the AbstractComponent with `id`, if it exists.
@@ -112,13 +135,15 @@ class EditorImage {
112
135
  return this.componentsById[id] ?? null;
113
136
  }
114
137
  addElementDirectly(elem) {
138
+ // Because onAddToImage can affect the element's bounding box,
139
+ // this needs to be called before parentTree.addLeaf.
115
140
  elem.onAddToImage(this);
116
- this.componentCount++;
117
- this.componentsById[elem.getId()] = elem;
118
141
  // If a background component, add to the background. Else,
119
142
  // add to the normal component tree.
120
143
  const parentTree = elem.isBackground() ? this.background : this.root;
121
- return parentTree.addLeaf(elem);
144
+ const result = parentTree.addLeaf(elem);
145
+ this.onElementAdded(elem);
146
+ return result;
122
147
  }
123
148
  removeElementDirectly(element) {
124
149
  const container = this.findParent(element);
@@ -149,11 +174,82 @@ class EditorImage {
149
174
  getImportExportViewport() {
150
175
  return this.importExportViewport;
151
176
  }
177
+ /**
178
+ * @see {@link setImportExportRect}
179
+ */
180
+ getImportExportRect() {
181
+ return this.getImportExportViewport().visibleRect;
182
+ }
183
+ /**
184
+ * Sets the import/export rectangle to the given `imageRect`. Disables
185
+ * autoresize (if it was previously enabled).
186
+ */
152
187
  setImportExportRect(imageRect) {
153
- const importExportViewport = this.getImportExportViewport();
154
- const origSize = importExportViewport.visibleRect.size;
155
- const origTransform = importExportViewport.canvasToScreenTransform;
156
- return new EditorImage.SetImportExportRectCommand(origSize, origTransform, imageRect);
188
+ return EditorImage.SetImportExportRectCommand.of(this, imageRect, false);
189
+ }
190
+ getAutoresizeEnabled() {
191
+ return this.shouldAutoresizeExportViewport;
192
+ }
193
+ /** Returns a `Command` that sets whether the image should autoresize. */
194
+ setAutoresizeEnabled(autoresize) {
195
+ if (autoresize === this.shouldAutoresizeExportViewport) {
196
+ return Command.empty;
197
+ }
198
+ const newBBox = this.root.getBBox();
199
+ return EditorImage.SetImportExportRectCommand.of(this, newBBox, autoresize);
200
+ }
201
+ setAutoresizeEnabledDirectly(shouldAutoresize) {
202
+ if (shouldAutoresize !== this.shouldAutoresizeExportViewport) {
203
+ this.shouldAutoresizeExportViewport = shouldAutoresize;
204
+ this.notifier.dispatch(EditorImageEventType.AutoresizeModeChanged, {
205
+ image: this,
206
+ });
207
+ }
208
+ }
209
+ /** Updates the size/position of the viewport */
210
+ autoresizeExportViewport() {
211
+ // Only autoresize if in autoresize mode -- otherwise resizing the image
212
+ // should be done with undoable commands.
213
+ if (this.shouldAutoresizeExportViewport) {
214
+ this.setExportRectDirectly(this.root.getBBox());
215
+ }
216
+ }
217
+ /**
218
+ * Sets the import/export viewport directly, without returning a `Command`.
219
+ * As such, this is not undoable.
220
+ *
221
+ * See setImportExportRect
222
+ *
223
+ * Returns true if changes to the viewport were made (and thus
224
+ * ExportViewportChanged was fired.)
225
+ */
226
+ setExportRectDirectly(newRect) {
227
+ const viewport = this.getImportExportViewport();
228
+ const lastSize = viewport.getScreenRectSize();
229
+ const lastTransform = viewport.canvasToScreenTransform;
230
+ const newTransform = Mat33.translation(newRect.topLeft.times(-1));
231
+ if (!lastSize.eq(newRect.size) || !lastTransform.eq(newTransform)) {
232
+ // Prevent the ExportViewportChanged event from being fired
233
+ // multiple times for the same viewport change: Set settingExportRect
234
+ // to true.
235
+ this.settingExportRect = true;
236
+ viewport.updateScreenSize(newRect.size);
237
+ viewport.resetTransform(newTransform);
238
+ this.settingExportRect = false;
239
+ this.onExportViewportChanged();
240
+ return true;
241
+ }
242
+ return false;
243
+ }
244
+ onExportViewportChanged() {
245
+ // Prevent firing duplicate events -- changes
246
+ // made by exportViewport.resetTransform may cause this method to be
247
+ // called.
248
+ if (!this.settingExportRect) {
249
+ this.notifier.dispatch(EditorImageEventType.ExportViewportChanged, {
250
+ image: this,
251
+ });
252
+ }
157
253
  }
158
254
  }
159
255
  _a = EditorImage;
@@ -214,37 +310,57 @@ EditorImage.AddElementCommand = (_b = class extends SerializableCommand {
214
310
  _b);
215
311
  // Handles resizing the background import/export region of the image.
216
312
  EditorImage.SetImportExportRectCommand = (_c = class extends SerializableCommand {
217
- constructor(originalSize, originalTransform, finalRect) {
313
+ constructor(originalSize, originalTransform, originalAutoresize, newExportRect, newAutoresize) {
218
314
  super(EditorImage.SetImportExportRectCommand.commandId);
219
315
  this.originalSize = originalSize;
220
316
  this.originalTransform = originalTransform;
221
- this.finalRect = finalRect;
317
+ this.originalAutoresize = originalAutoresize;
318
+ this.newExportRect = newExportRect;
319
+ this.newAutoresize = newAutoresize;
320
+ }
321
+ // Uses `image` to store the original size/transform
322
+ static of(image, newExportRect, newAutoresize) {
323
+ const importExportViewport = image.getImportExportViewport();
324
+ const originalSize = importExportViewport.visibleRect.size;
325
+ const originalTransform = importExportViewport.canvasToScreenTransform;
326
+ const originalAutoresize = image.getAutoresizeEnabled();
327
+ return new EditorImage.SetImportExportRectCommand(originalSize, originalTransform, originalAutoresize, newExportRect, newAutoresize);
222
328
  }
223
329
  apply(editor) {
224
- const viewport = editor.image.getImportExportViewport();
225
- viewport.updateScreenSize(this.finalRect.size);
226
- viewport.resetTransform(Mat33.translation(this.finalRect.topLeft.times(-1)));
330
+ editor.image.setAutoresizeEnabledDirectly(this.newAutoresize);
331
+ editor.image.setExportRectDirectly(this.newExportRect);
227
332
  editor.queueRerender();
228
333
  }
229
334
  unapply(editor) {
230
335
  const viewport = editor.image.getImportExportViewport();
336
+ editor.image.setAutoresizeEnabledDirectly(this.originalAutoresize);
231
337
  viewport.updateScreenSize(this.originalSize);
232
338
  viewport.resetTransform(this.originalTransform);
233
339
  editor.queueRerender();
234
340
  }
235
341
  description(_editor, localization) {
236
- return localization.resizeOutputCommand(this.finalRect);
342
+ if (this.newAutoresize !== this.originalAutoresize) {
343
+ if (this.newAutoresize) {
344
+ return localization.enabledAutoresizeOutputCommand;
345
+ }
346
+ else {
347
+ return localization.disabledAutoresizeOutputCommand;
348
+ }
349
+ }
350
+ return localization.resizeOutputCommand(this.newExportRect);
237
351
  }
238
352
  serializeToJSON() {
239
353
  return {
240
354
  originalSize: this.originalSize.xy,
241
355
  originalTransform: this.originalTransform.toArray(),
242
356
  newRegion: {
243
- x: this.finalRect.x,
244
- y: this.finalRect.y,
245
- w: this.finalRect.w,
246
- h: this.finalRect.h,
357
+ x: this.newExportRect.x,
358
+ y: this.newExportRect.y,
359
+ w: this.newExportRect.w,
360
+ h: this.newExportRect.h,
247
361
  },
362
+ autoresize: this.newAutoresize,
363
+ originalAutoresize: this.originalAutoresize,
248
364
  };
249
365
  }
250
366
  },
@@ -262,15 +378,22 @@ EditorImage.SetImportExportRectCommand = (_c = class extends SerializableCommand
262
378
  json.newRegion.w,
263
379
  json.newRegion.h,
264
380
  ]);
381
+ assertIsBoolean(json.autoresize ?? false);
382
+ assertIsBoolean(json.originalAutoresize ?? false);
265
383
  const originalSize = Vec2.ofXY(json.originalSize);
266
384
  const originalTransform = new Mat33(...json.originalTransform);
267
385
  const finalRect = new Rect2(json.newRegion.x, json.newRegion.y, json.newRegion.w, json.newRegion.h);
268
- return new EditorImage.SetImportExportRectCommand(originalSize, originalTransform, finalRect);
386
+ const autoresize = json.autoresize ?? false;
387
+ const originalAutoresize = json.originalAutoresize ?? false;
388
+ return new EditorImage.SetImportExportRectCommand(originalSize, originalTransform, originalAutoresize, finalRect, autoresize);
269
389
  });
270
390
  })(),
271
391
  _c);
272
392
  export default EditorImage;
273
- /** Part of the Editor's image. @internal */
393
+ /**
394
+ * Part of the Editor's image. Does not handle fullscreen/invisible components.
395
+ * @internal
396
+ */
274
397
  export class ImageNode {
275
398
  constructor(parent = null) {
276
399
  this.parent = parent;
@@ -292,9 +415,11 @@ export class ImageNode {
292
415
  getParent() {
293
416
  return this.parent;
294
417
  }
295
- getChildrenIntersectingRegion(region) {
418
+ // Override this to change how children are considered within a given region.
419
+ getChildrenIntersectingRegion(region, isTooSmallFilter) {
296
420
  return this.children.filter(child => {
297
- return child.getBBox().intersects(region);
421
+ const bbox = child.getBBox();
422
+ return !isTooSmallFilter?.(bbox) && bbox.intersects(region);
298
423
  });
299
424
  }
300
425
  getChildrenOrSelfIntersectingRegion(region) {
@@ -304,29 +429,25 @@ export class ImageNode {
304
429
  return this.getChildrenIntersectingRegion(region);
305
430
  }
306
431
  // Returns a list of `ImageNode`s with content (and thus no children).
432
+ // Override getChildrenIntersectingRegion to customize how this method
433
+ // determines whether/which children are in `region`.
307
434
  getLeavesIntersectingRegion(region, isTooSmall) {
308
435
  const result = [];
309
- let current;
310
436
  const workList = [];
311
437
  workList.push(this);
312
- const toNext = () => {
313
- current = undefined;
314
- const next = workList.pop();
315
- if (next && !isTooSmall?.(next.bbox)) {
316
- current = next;
317
- if (current.content !== null && current.getBBox().intersection(region)) {
318
- result.push(current);
319
- }
320
- workList.push(...current.getChildrenIntersectingRegion(region));
321
- }
322
- };
323
438
  while (workList.length > 0) {
324
- toNext();
439
+ const current = workList.pop();
440
+ if (current.content !== null) {
441
+ result.push(current);
442
+ }
443
+ workList.push(...current.getChildrenIntersectingRegion(region, isTooSmall));
325
444
  }
326
445
  return result;
327
446
  }
328
447
  // Returns the child of this with the target content or `null` if no
329
448
  // such child exists.
449
+ //
450
+ // Note: Relies on all children to have valid bounding boxes.
330
451
  getChildWithContent(target) {
331
452
  const candidates = this.getLeavesIntersectingRegion(target.getBBox());
332
453
  for (const candidate of candidates) {
@@ -390,12 +511,19 @@ export class ImageNode {
390
511
  result.rebalance();
391
512
  return result;
392
513
  }
393
- const newNode = new ImageNode(this);
514
+ const newNode = ImageNode.createLeafNode(this, leaf);
394
515
  this.children.push(newNode);
395
- newNode.content = leaf;
396
516
  newNode.recomputeBBox(true);
397
517
  return newNode;
398
518
  }
519
+ // Creates a new leaf node with the given content.
520
+ // This only establishes the parent-child linking in one direction. Callers
521
+ // must add the resultant node to the list of children manually.
522
+ static createLeafNode(parent, content) {
523
+ const newNode = new ImageNode(parent);
524
+ newNode.content = content;
525
+ return newNode;
526
+ }
399
527
  getBBox() {
400
528
  return this.bbox;
401
529
  }
@@ -411,9 +539,20 @@ export class ImageNode {
411
539
  this.bbox = Rect2.union(...this.children.map(child => child.getBBox()));
412
540
  }
413
541
  if (bubbleUp && !oldBBox.eq(this.bbox)) {
414
- this.parent?.recomputeBBox(true);
542
+ if (!this.bbox.containsRect(oldBBox)) {
543
+ this.parent?.unionBBoxWith(this.bbox);
544
+ }
545
+ else {
546
+ this.parent?.recomputeBBox(true);
547
+ }
415
548
  }
416
549
  }
550
+ // Grows this' bounding box to also include `other`.
551
+ // Always bubbles up.
552
+ unionBBoxWith(other) {
553
+ this.bbox = this.bbox.union(other);
554
+ this.parent?.unionBBoxWith(other);
555
+ }
417
556
  updateParents(recursive = false) {
418
557
  for (const child of this.children) {
419
558
  child.parent = this;
@@ -444,6 +583,19 @@ export class ImageNode {
444
583
  }
445
584
  }
446
585
  }
586
+ // Removes the parent-to-child link.
587
+ // Called internally by `.remove`
588
+ removeChild(child) {
589
+ const oldChildCount = this.children.length;
590
+ this.children = this.children.filter(node => {
591
+ return node !== child;
592
+ });
593
+ console.assert(this.children.length === oldChildCount - 1, `${oldChildCount - 1} ≠ ${this.children.length} after removing all nodes equal to ${child}. Nodes should only be removed once.`);
594
+ this.children.forEach(child => {
595
+ child.rebalance();
596
+ });
597
+ this.recomputeBBox(true);
598
+ }
447
599
  // Remove this node and all of its children
448
600
  remove() {
449
601
  this.content?.onRemoveFromImage();
@@ -452,18 +604,10 @@ export class ImageNode {
452
604
  this.children = [];
453
605
  return;
454
606
  }
455
- const oldChildCount = this.parent.children.length;
456
- this.parent.children = this.parent.children.filter(node => {
457
- return node !== this;
458
- });
459
- console.assert(this.parent.children.length === oldChildCount - 1, `${oldChildCount - 1} ≠ ${this.parent.children.length} after removing all nodes equal to ${this}. Nodes should only be removed once.`);
460
- this.parent.children.forEach(child => {
461
- child.rebalance();
462
- });
463
- this.parent.recomputeBBox(true);
464
- // Invalidate/disconnect this.
465
- this.content = null;
607
+ this.parent.removeChild(this);
608
+ // Remove the child-to-parent link and invalid this
466
609
  this.parent = null;
610
+ this.content = null;
467
611
  this.children = [];
468
612
  }
469
613
  render(renderer, visibleRect) {
@@ -479,6 +623,114 @@ export class ImageNode {
479
623
  // Leaves by definition have content
480
624
  leaf.getContent().render(renderer, visibleRect);
481
625
  }
626
+ // Show debug information
627
+ if (debugMode && visibleRect) {
628
+ this.renderDebugBoundingBoxes(renderer, visibleRect);
629
+ }
630
+ }
631
+ // Debug only: Shows bounding boxes of this and all children.
632
+ renderDebugBoundingBoxes(renderer, visibleRect, depth = 0) {
633
+ const bbox = this.getBBox();
634
+ const pixelSize = 1 / (renderer.getSizeOfCanvasPixelOnScreen() || 1);
635
+ if (bbox.maxDimension < 3 * pixelSize || !bbox.intersects(visibleRect)) {
636
+ return;
637
+ }
638
+ // Render debug information for this
639
+ renderer.startObject(bbox);
640
+ // Different styling for leaf nodes
641
+ const isLeaf = !!this.content;
642
+ const fill = isLeaf ? Color4.ofRGBA(1, 0, 1, 0.4) : Color4.ofRGBA(0, 1, Math.sin(depth), 0.6);
643
+ const lineWidth = isLeaf ? 1 * pixelSize : 2 * pixelSize;
644
+ renderer.drawRect(bbox.intersection(visibleRect), lineWidth, { fill });
645
+ renderer.endObject();
646
+ // Render debug information for children
647
+ for (const child of this.children) {
648
+ child.renderDebugBoundingBoxes(renderer, visibleRect, depth + 1);
649
+ }
482
650
  }
483
651
  }
484
652
  ImageNode.idCounter = 0;
653
+ /** An `ImageNode` that can properly handle fullscreen/data components. @internal */
654
+ export class RootImageNode extends ImageNode {
655
+ constructor() {
656
+ super(...arguments);
657
+ // Nodes that will always take up the entire screen
658
+ this.fullscreenChildren = [];
659
+ // Nodes that will never be visible unless a full render is done.
660
+ this.dataComponents = [];
661
+ }
662
+ getChildrenIntersectingRegion(region, _isTooSmall) {
663
+ const result = super.getChildrenIntersectingRegion(region);
664
+ for (const node of this.fullscreenChildren) {
665
+ result.push(node);
666
+ }
667
+ return result;
668
+ }
669
+ getLeaves() {
670
+ const leaves = super.getLeaves();
671
+ // Add fullscreen/data components — this method should
672
+ // return *all* leaves.
673
+ return this.dataComponents.concat(this.fullscreenChildren, leaves);
674
+ }
675
+ removeChild(child) {
676
+ let removed = false;
677
+ const checkTargetChild = (component) => {
678
+ const isTarget = component === child;
679
+ removed ||= isTarget;
680
+ return !isTarget;
681
+ };
682
+ // Check whether the child is stored in the data/fullscreen
683
+ // component arrays first.
684
+ this.dataComponents = this.dataComponents
685
+ .filter(checkTargetChild);
686
+ this.fullscreenChildren = this.fullscreenChildren
687
+ .filter(checkTargetChild);
688
+ if (!removed) {
689
+ super.removeChild(child);
690
+ }
691
+ }
692
+ getChildWithContent(target) {
693
+ const searchExtendedChildren = () => {
694
+ // Search through all extended children
695
+ const candidates = this.fullscreenChildren.concat(this.dataComponents);
696
+ for (const candidate of candidates) {
697
+ if (candidate.getContent() === target) {
698
+ return candidate;
699
+ }
700
+ }
701
+ return null;
702
+ };
703
+ // If positioned as if a standard child, search using the superclass first.
704
+ // Because it could be mislabeled, also search the extended children if the superclass
705
+ // search fails.
706
+ if (target.getSizingMode() === ComponentSizingMode.BoundingBox) {
707
+ return super.getChildWithContent(target) ?? searchExtendedChildren();
708
+ }
709
+ // Fall back to the superclass -- it's possible that the component has
710
+ // changed labels.
711
+ return super.getChildWithContent(target) ?? searchExtendedChildren();
712
+ }
713
+ addLeaf(leafContent) {
714
+ const sizingMode = leafContent.getSizingMode();
715
+ if (sizingMode === ComponentSizingMode.BoundingBox) {
716
+ return super.addLeaf(leafContent);
717
+ }
718
+ else if (sizingMode === ComponentSizingMode.FillScreen) {
719
+ this.onContentChange();
720
+ const newNode = ImageNode.createLeafNode(this, leafContent);
721
+ this.fullscreenChildren.push(newNode);
722
+ return newNode;
723
+ }
724
+ else if (sizingMode === ComponentSizingMode.Anywhere) {
725
+ this.onContentChange();
726
+ const newNode = ImageNode.createLeafNode(this, leafContent);
727
+ this.dataComponents.push(newNode);
728
+ return newNode;
729
+ }
730
+ else {
731
+ const exhaustivenessCheck = sizingMode;
732
+ throw new Error(`Invalid sizing mode, ${sizingMode}`);
733
+ return exhaustivenessCheck;
734
+ }
735
+ }
736
+ }