js-draw 1.2.2 → 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.
- package/README.md +29 -29
- package/dist/Editor.css +65 -4
- package/dist/bundle.js +2 -2
- package/dist/bundledStyles.js +1 -1
- package/dist/cjs/Editor.d.ts +73 -40
- package/dist/cjs/Editor.js +90 -24
- package/dist/cjs/EditorImage.d.ts +58 -6
- package/dist/cjs/EditorImage.js +336 -60
- package/dist/cjs/SVGLoader.d.ts +10 -4
- package/dist/cjs/SVGLoader.js +30 -10
- package/dist/cjs/UndoRedoHistory.d.ts +2 -2
- package/dist/cjs/UndoRedoHistory.js +4 -2
- package/dist/cjs/Viewport.d.ts +2 -1
- package/dist/cjs/Viewport.js +12 -3
- package/dist/cjs/commands/Command.d.ts +1 -0
- package/dist/cjs/commands/Command.js +1 -0
- package/dist/cjs/commands/Erase.js +1 -1
- package/dist/cjs/commands/SerializableCommand.d.ts +1 -1
- package/dist/cjs/commands/SerializableCommand.js +16 -2
- package/dist/cjs/commands/localization.d.ts +2 -0
- package/dist/cjs/commands/localization.js +2 -0
- package/dist/cjs/components/AbstractComponent.d.ts +38 -0
- package/dist/cjs/components/AbstractComponent.js +31 -0
- package/dist/cjs/components/BackgroundComponent.d.ts +10 -1
- package/dist/cjs/components/BackgroundComponent.js +60 -6
- package/dist/cjs/components/SVGGlobalAttributesObject.d.ts +2 -1
- package/dist/cjs/components/SVGGlobalAttributesObject.js +30 -1
- package/dist/cjs/components/Stroke.d.ts +1 -0
- package/dist/cjs/components/Stroke.js +44 -0
- package/dist/cjs/components/UnknownSVGObject.d.ts +2 -1
- package/dist/cjs/components/UnknownSVGObject.js +30 -1
- package/dist/cjs/lib.d.ts +2 -45
- package/dist/cjs/lib.js +2 -45
- package/dist/cjs/rendering/RenderingStyle.d.ts +1 -0
- package/dist/cjs/rendering/renderers/AbstractRenderer.js +1 -1
- package/dist/cjs/shortcuts/KeyboardShortcutManager.d.ts +2 -2
- package/dist/cjs/shortcuts/KeyboardShortcutManager.js +2 -2
- package/dist/cjs/toolbar/localization.d.ts +1 -0
- package/dist/cjs/toolbar/localization.js +1 -0
- package/dist/cjs/toolbar/widgets/BaseWidget.js +5 -0
- package/dist/cjs/toolbar/widgets/DocumentPropertiesWidget.js +54 -25
- package/dist/cjs/toolbar/widgets/components/makeGridSelector.js +8 -0
- package/dist/cjs/tools/PanZoom.js +13 -8
- package/dist/cjs/tools/ScrollbarTool.d.ts +18 -0
- package/dist/cjs/tools/ScrollbarTool.js +85 -0
- package/dist/cjs/tools/SelectionTool/SelectionTool.selecting.test.d.ts +1 -0
- package/dist/cjs/tools/ToolController.js +2 -0
- package/dist/cjs/types.d.ts +3 -1
- package/dist/cjs/util/assertions.d.ts +4 -0
- package/dist/cjs/util/assertions.js +12 -1
- package/dist/cjs/version.js +1 -1
- package/dist/mjs/Editor.d.ts +73 -40
- package/dist/mjs/Editor.mjs +90 -24
- package/dist/mjs/EditorImage.d.ts +58 -6
- package/dist/mjs/EditorImage.mjs +313 -61
- package/dist/mjs/SVGLoader.d.ts +10 -4
- package/dist/mjs/SVGLoader.mjs +29 -9
- package/dist/mjs/UndoRedoHistory.d.ts +2 -2
- package/dist/mjs/UndoRedoHistory.mjs +4 -2
- package/dist/mjs/Viewport.d.ts +2 -1
- package/dist/mjs/Viewport.mjs +12 -3
- package/dist/mjs/commands/Command.d.ts +1 -0
- package/dist/mjs/commands/Command.mjs +1 -0
- package/dist/mjs/commands/Erase.mjs +1 -1
- package/dist/mjs/commands/SerializableCommand.d.ts +1 -1
- package/dist/mjs/commands/SerializableCommand.mjs +16 -2
- package/dist/mjs/commands/localization.d.ts +2 -0
- package/dist/mjs/commands/localization.mjs +2 -0
- package/dist/mjs/components/AbstractComponent.d.ts +38 -0
- package/dist/mjs/components/AbstractComponent.mjs +30 -0
- package/dist/mjs/components/BackgroundComponent.d.ts +10 -1
- package/dist/mjs/components/BackgroundComponent.mjs +37 -6
- package/dist/mjs/components/SVGGlobalAttributesObject.d.ts +2 -1
- package/dist/mjs/components/SVGGlobalAttributesObject.mjs +7 -1
- package/dist/mjs/components/Stroke.d.ts +1 -0
- package/dist/mjs/components/Stroke.mjs +44 -0
- package/dist/mjs/components/UnknownSVGObject.d.ts +2 -1
- package/dist/mjs/components/UnknownSVGObject.mjs +7 -1
- package/dist/mjs/lib.d.ts +2 -45
- package/dist/mjs/lib.mjs +2 -45
- package/dist/mjs/rendering/RenderingStyle.d.ts +1 -0
- package/dist/mjs/rendering/renderers/AbstractRenderer.mjs +1 -1
- package/dist/mjs/shortcuts/KeyboardShortcutManager.d.ts +2 -2
- package/dist/mjs/shortcuts/KeyboardShortcutManager.mjs +2 -2
- package/dist/mjs/toolbar/localization.d.ts +1 -0
- package/dist/mjs/toolbar/localization.mjs +1 -0
- package/dist/mjs/toolbar/widgets/BaseWidget.mjs +5 -0
- package/dist/mjs/toolbar/widgets/DocumentPropertiesWidget.mjs +54 -25
- package/dist/mjs/toolbar/widgets/components/makeGridSelector.mjs +8 -0
- package/dist/mjs/tools/PanZoom.mjs +13 -8
- package/dist/mjs/tools/ScrollbarTool.d.ts +18 -0
- package/dist/mjs/tools/ScrollbarTool.mjs +79 -0
- package/dist/mjs/tools/SelectionTool/SelectionTool.selecting.test.d.ts +1 -0
- package/dist/mjs/tools/ToolController.mjs +2 -0
- package/dist/mjs/types.d.ts +3 -1
- package/dist/mjs/util/assertions.d.ts +4 -0
- package/dist/mjs/util/assertions.mjs +10 -0
- package/dist/mjs/version.mjs +1 -1
- package/package.json +3 -4
- package/src/Editor.scss +8 -0
- package/src/dialogs/dialogs.scss +2 -1
- package/src/toolbar/EdgeToolbar.scss +4 -1
- package/src/toolbar/widgets/DocumentPropertiesWidget.scss +12 -0
- package/src/toolbar/widgets/components/makeGridSelector.scss +1 -1
- package/src/tools/ScrollbarTool.scss +57 -0
- package/src/tools/{SoundUITool.css → SoundUITool.scss} +4 -0
- package/src/tools/tools.scss +2 -1
package/dist/mjs/EditorImage.mjs
CHANGED
@@ -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.
|
26
|
-
this.
|
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.
|
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
|
-
|
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
|
-
/**
|
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
|
-
|
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
|
-
|
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
|
-
|
154
|
-
|
155
|
-
|
156
|
-
return
|
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,
|
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.
|
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
|
-
|
225
|
-
|
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
|
-
|
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.
|
244
|
-
y: this.
|
245
|
-
w: this.
|
246
|
-
h: this.
|
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
|
-
|
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
|
-
/**
|
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
|
-
|
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
|
-
|
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
|
-
|
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 =
|
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.
|
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
|
-
|
456
|
-
|
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
|
+
}
|
package/dist/mjs/SVGLoader.d.ts
CHANGED
@@ -4,22 +4,28 @@ export declare const defaultSVGViewRect: Rect2;
|
|
4
4
|
export declare const svgAttributesDataKey = "svgAttrs";
|
5
5
|
export declare const svgStyleAttributesDataKey = "svgStyleAttrs";
|
6
6
|
export declare const svgLoaderAttributeContainerID = "svgContainerID";
|
7
|
+
export declare const svgLoaderAutoresizeClassName = "js-draw--autoresize";
|
7
8
|
export type SVGLoaderUnknownAttribute = [string, string];
|
8
9
|
export type SVGLoaderUnknownStyleAttribute = {
|
9
10
|
key: string;
|
10
11
|
value: string;
|
11
12
|
priority?: string;
|
12
13
|
};
|
14
|
+
export interface SVGLoaderOptions {
|
15
|
+
sanitize?: boolean;
|
16
|
+
disableUnknownObjectWarnings?: boolean;
|
17
|
+
}
|
13
18
|
export default class SVGLoader implements ImageLoader {
|
14
19
|
private source;
|
15
|
-
private onFinish
|
16
|
-
private readonly storeUnknown;
|
20
|
+
private onFinish;
|
17
21
|
private onAddComponent;
|
18
22
|
private onProgress;
|
19
23
|
private onDetermineExportRect;
|
20
24
|
private processedCount;
|
21
25
|
private totalToProcess;
|
22
26
|
private rootViewBox;
|
27
|
+
private readonly storeUnknown;
|
28
|
+
private readonly disableUnknownObjectWarnings;
|
23
29
|
private constructor();
|
24
30
|
private getStyle;
|
25
31
|
private strokeDataFromElem;
|
@@ -48,7 +54,7 @@ export default class SVGLoader implements ImageLoader {
|
|
48
54
|
*
|
49
55
|
* @see {@link Editor.loadFrom}
|
50
56
|
* @param text - Textual representation of the SVG (e.g. `<svg viewbox='...'>...</svg>`).
|
51
|
-
* @param
|
57
|
+
* @param options - if `true` or `false`, treated as the `sanitize` option -- don't store unknown attributes.
|
52
58
|
*/
|
53
|
-
static fromString(text: string,
|
59
|
+
static fromString(text: string, options?: Partial<SVGLoaderOptions> | boolean): SVGLoader;
|
54
60
|
}
|
package/dist/mjs/SVGLoader.mjs
CHANGED
@@ -15,13 +15,15 @@ export const svgStyleAttributesDataKey = 'svgStyleAttrs';
|
|
15
15
|
// Key that specifies the ID of an SVG element that contained a given node when the image
|
16
16
|
// was first loaded.
|
17
17
|
export const svgLoaderAttributeContainerID = 'svgContainerID';
|
18
|
+
// If present in the exported SVG's class list, the image will be
|
19
|
+
// autoresized when components are added/removed.
|
20
|
+
export const svgLoaderAutoresizeClassName = 'js-draw--autoresize';
|
18
21
|
const supportedStrokeFillStyleAttrs = ['stroke', 'fill', 'stroke-width'];
|
19
22
|
// Handles loading images from SVG.
|
20
23
|
export default class SVGLoader {
|
21
|
-
constructor(source, onFinish,
|
24
|
+
constructor(source, onFinish, options) {
|
22
25
|
this.source = source;
|
23
26
|
this.onFinish = onFinish;
|
24
|
-
this.storeUnknown = storeUnknown;
|
25
27
|
this.onAddComponent = null;
|
26
28
|
this.onProgress = null;
|
27
29
|
this.onDetermineExportRect = null;
|
@@ -29,6 +31,8 @@ export default class SVGLoader {
|
|
29
31
|
this.totalToProcess = 0;
|
30
32
|
this.containerGroupIDs = [];
|
31
33
|
this.encounteredIDs = [];
|
34
|
+
this.storeUnknown = !(options.sanitize ?? false);
|
35
|
+
this.disableUnknownObjectWarnings = !!options.disableUnknownObjectWarnings;
|
32
36
|
}
|
33
37
|
// If [computedStyles] is given, it is preferred to directly accessing node's style object.
|
34
38
|
getStyle(node, computedStyles) {
|
@@ -395,8 +399,9 @@ export default class SVGLoader {
|
|
395
399
|
console.warn(`node ${node} has an unparsable viewbox. Viewbox: ${viewBoxAttr}. Match: ${components}.`);
|
396
400
|
return;
|
397
401
|
}
|
402
|
+
const autoresize = node.classList.contains(svgLoaderAutoresizeClassName);
|
398
403
|
this.rootViewBox = new Rect2(x, y, width, height);
|
399
|
-
this.onDetermineExportRect?.(this.rootViewBox);
|
404
|
+
this.onDetermineExportRect?.(this.rootViewBox, { autoresize });
|
400
405
|
}
|
401
406
|
async updateSVGAttrs(node) {
|
402
407
|
if (this.storeUnknown) {
|
@@ -442,9 +447,11 @@ export default class SVGLoader {
|
|
442
447
|
await this.addUnknownNode(node);
|
443
448
|
break;
|
444
449
|
default:
|
445
|
-
|
446
|
-
|
447
|
-
|
450
|
+
if (!this.disableUnknownObjectWarnings) {
|
451
|
+
console.warn('Unknown SVG element,', node, node.tagName);
|
452
|
+
if (!(node instanceof SVGElement)) {
|
453
|
+
console.warn('Element', node, 'is not an SVGElement!', this.storeUnknown ? 'Continuing anyway.' : 'Skipping.');
|
454
|
+
}
|
448
455
|
}
|
449
456
|
await this.addUnknownNode(node);
|
450
457
|
return;
|
@@ -488,9 +495,9 @@ export default class SVGLoader {
|
|
488
495
|
*
|
489
496
|
* @see {@link Editor.loadFrom}
|
490
497
|
* @param text - Textual representation of the SVG (e.g. `<svg viewbox='...'>...</svg>`).
|
491
|
-
* @param
|
498
|
+
* @param options - if `true` or `false`, treated as the `sanitize` option -- don't store unknown attributes.
|
492
499
|
*/
|
493
|
-
static fromString(text,
|
500
|
+
static fromString(text, options = false) {
|
494
501
|
const sandbox = document.createElement('iframe');
|
495
502
|
sandbox.src = 'about:blank';
|
496
503
|
sandbox.setAttribute('sandbox', 'allow-same-origin');
|
@@ -528,9 +535,22 @@ export default class SVGLoader {
|
|
528
535
|
const svgElem = sandboxDoc.createElementNS('http://www.w3.org/2000/svg', 'svg');
|
529
536
|
svgElem.innerHTML = text;
|
530
537
|
sandboxDoc.body.appendChild(svgElem);
|
538
|
+
// Handle options
|
539
|
+
let sanitize;
|
540
|
+
let disableUnknownObjectWarnings;
|
541
|
+
if (typeof options === 'boolean') {
|
542
|
+
sanitize = options;
|
543
|
+
disableUnknownObjectWarnings = false;
|
544
|
+
}
|
545
|
+
else {
|
546
|
+
sanitize = options.sanitize ?? false;
|
547
|
+
disableUnknownObjectWarnings = options.disableUnknownObjectWarnings ?? false;
|
548
|
+
}
|
531
549
|
return new SVGLoader(svgElem, () => {
|
532
550
|
svgElem.remove();
|
533
551
|
sandbox.remove();
|
534
|
-
},
|
552
|
+
}, {
|
553
|
+
sanitize, disableUnknownObjectWarnings,
|
554
|
+
});
|
535
555
|
}
|
536
556
|
}
|