js-draw 0.3.0 → 0.3.2

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 (113) hide show
  1. package/.github/ISSUE_TEMPLATE/translation.md +4 -1
  2. package/CHANGELOG.md +15 -0
  3. package/dist/bundle.js +1 -1
  4. package/dist/src/Editor.d.ts +4 -1
  5. package/dist/src/Editor.js +117 -2
  6. package/dist/src/EditorImage.js +4 -1
  7. package/dist/src/SVGLoader.d.ts +4 -1
  8. package/dist/src/SVGLoader.js +78 -33
  9. package/dist/src/UndoRedoHistory.d.ts +1 -0
  10. package/dist/src/UndoRedoHistory.js +6 -0
  11. package/dist/src/Viewport.d.ts +1 -0
  12. package/dist/src/Viewport.js +12 -4
  13. package/dist/src/commands/lib.d.ts +2 -1
  14. package/dist/src/commands/lib.js +2 -1
  15. package/dist/src/commands/localization.d.ts +1 -0
  16. package/dist/src/commands/localization.js +1 -0
  17. package/dist/src/commands/uniteCommands.d.ts +4 -0
  18. package/dist/src/commands/uniteCommands.js +105 -0
  19. package/dist/src/components/AbstractComponent.d.ts +2 -0
  20. package/dist/src/components/AbstractComponent.js +41 -5
  21. package/dist/src/components/ImageComponent.d.ts +27 -0
  22. package/dist/src/components/ImageComponent.js +129 -0
  23. package/dist/src/components/Stroke.js +11 -6
  24. package/dist/src/components/builders/FreehandLineBuilder.js +7 -7
  25. package/dist/src/components/lib.d.ts +4 -2
  26. package/dist/src/components/lib.js +4 -2
  27. package/dist/src/components/localization.d.ts +2 -0
  28. package/dist/src/components/localization.js +2 -0
  29. package/dist/src/math/LineSegment2.d.ts +4 -0
  30. package/dist/src/math/LineSegment2.js +9 -0
  31. package/dist/src/math/Path.d.ts +5 -1
  32. package/dist/src/math/Path.js +89 -7
  33. package/dist/src/math/Rect2.js +1 -1
  34. package/dist/src/math/Triangle.d.ts +11 -0
  35. package/dist/src/math/Triangle.js +19 -0
  36. package/dist/src/rendering/Display.js +2 -2
  37. package/dist/src/rendering/localization.d.ts +3 -0
  38. package/dist/src/rendering/localization.js +3 -0
  39. package/dist/src/rendering/renderers/AbstractRenderer.d.ts +9 -1
  40. package/dist/src/rendering/renderers/CanvasRenderer.d.ts +2 -1
  41. package/dist/src/rendering/renderers/CanvasRenderer.js +7 -0
  42. package/dist/src/rendering/renderers/DummyRenderer.d.ts +3 -1
  43. package/dist/src/rendering/renderers/DummyRenderer.js +5 -0
  44. package/dist/src/rendering/renderers/SVGRenderer.d.ts +14 -12
  45. package/dist/src/rendering/renderers/SVGRenderer.js +71 -87
  46. package/dist/src/rendering/renderers/TextOnlyRenderer.d.ts +3 -1
  47. package/dist/src/rendering/renderers/TextOnlyRenderer.js +8 -1
  48. package/dist/src/toolbar/HTMLToolbar.d.ts +1 -0
  49. package/dist/src/toolbar/HTMLToolbar.js +1 -0
  50. package/dist/src/toolbar/widgets/BaseWidget.d.ts +3 -0
  51. package/dist/src/toolbar/widgets/BaseWidget.js +21 -1
  52. package/dist/src/tools/BaseTool.d.ts +4 -1
  53. package/dist/src/tools/BaseTool.js +12 -0
  54. package/dist/src/tools/PasteHandler.d.ts +16 -0
  55. package/dist/src/tools/PasteHandler.js +142 -0
  56. package/dist/src/tools/Pen.d.ts +2 -1
  57. package/dist/src/tools/Pen.js +16 -0
  58. package/dist/src/tools/SelectionTool.d.ts +7 -1
  59. package/dist/src/tools/SelectionTool.js +63 -5
  60. package/dist/src/tools/ToolController.d.ts +1 -0
  61. package/dist/src/tools/ToolController.js +45 -29
  62. package/dist/src/tools/ToolSwitcherShortcut.d.ts +8 -0
  63. package/dist/src/tools/ToolSwitcherShortcut.js +26 -0
  64. package/dist/src/tools/lib.d.ts +2 -0
  65. package/dist/src/tools/lib.js +2 -0
  66. package/dist/src/tools/localization.d.ts +4 -0
  67. package/dist/src/tools/localization.js +4 -0
  68. package/dist/src/types.d.ts +21 -4
  69. package/dist/src/types.js +3 -0
  70. package/package.json +2 -2
  71. package/src/Editor.ts +131 -2
  72. package/src/EditorImage.ts +7 -1
  73. package/src/SVGLoader.ts +90 -36
  74. package/src/UndoRedoHistory.test.ts +33 -0
  75. package/src/UndoRedoHistory.ts +8 -0
  76. package/src/Viewport.ts +13 -4
  77. package/src/commands/lib.ts +2 -0
  78. package/src/commands/localization.ts +2 -0
  79. package/src/commands/uniteCommands.test.ts +23 -0
  80. package/src/commands/uniteCommands.ts +121 -0
  81. package/src/components/AbstractComponent.ts +55 -9
  82. package/src/components/ImageComponent.ts +153 -0
  83. package/src/components/Stroke.test.ts +5 -0
  84. package/src/components/Stroke.ts +13 -7
  85. package/src/components/builders/FreehandLineBuilder.ts +7 -7
  86. package/src/components/lib.ts +7 -2
  87. package/src/components/localization.ts +4 -0
  88. package/src/math/LineSegment2.test.ts +9 -0
  89. package/src/math/LineSegment2.ts +13 -0
  90. package/src/math/Path.test.ts +53 -0
  91. package/src/math/Path.toString.test.ts +4 -2
  92. package/src/math/Path.ts +109 -11
  93. package/src/math/Rect2.ts +1 -1
  94. package/src/math/Triangle.ts +29 -0
  95. package/src/rendering/Display.ts +2 -2
  96. package/src/rendering/localization.ts +6 -0
  97. package/src/rendering/renderers/AbstractRenderer.ts +17 -0
  98. package/src/rendering/renderers/CanvasRenderer.ts +10 -1
  99. package/src/rendering/renderers/DummyRenderer.ts +6 -1
  100. package/src/rendering/renderers/SVGRenderer.ts +76 -101
  101. package/src/rendering/renderers/TextOnlyRenderer.ts +10 -2
  102. package/src/toolbar/HTMLToolbar.ts +1 -1
  103. package/src/toolbar/types.ts +1 -1
  104. package/src/toolbar/widgets/BaseWidget.ts +27 -1
  105. package/src/tools/BaseTool.ts +17 -1
  106. package/src/tools/PasteHandler.ts +156 -0
  107. package/src/tools/Pen.ts +20 -1
  108. package/src/tools/SelectionTool.ts +80 -8
  109. package/src/tools/ToolController.ts +60 -46
  110. package/src/tools/ToolSwitcherShortcut.ts +34 -0
  111. package/src/tools/lib.ts +2 -0
  112. package/src/tools/localization.ts +10 -0
  113. package/src/types.ts +29 -3
@@ -94,6 +94,7 @@ export declare class Editor {
94
94
  private loadingWarning;
95
95
  private accessibilityAnnounceArea;
96
96
  private accessibilityControlArea;
97
+ private eventListenerTargets;
97
98
  private settings;
98
99
  /**
99
100
  * @example
@@ -139,6 +140,8 @@ export declare class Editor {
139
140
  */
140
141
  addToolbar(defaultLayout?: boolean): HTMLToolbar;
141
142
  private registerListeners;
143
+ private isEventSink;
144
+ private handlePaste;
142
145
  /** Adds event listners for keypresses to `elem` and forwards those events to the editor. */
143
146
  handleKeyEventsFrom(elem: HTMLElement): void;
144
147
  /** `apply` a command. `command` will be announced for accessibility. */
@@ -191,6 +194,6 @@ export declare class Editor {
191
194
  * This is particularly useful when accessing a bundled version of the editor,
192
195
  * where `SVGLoader.fromString` is unavailable.
193
196
  */
194
- loadFromSVG(svgData: string): Promise<void>;
197
+ loadFromSVG(svgData: string, sanitize?: boolean): Promise<void>;
195
198
  }
196
199
  export default Editor;
@@ -68,6 +68,7 @@ export class Editor {
68
68
  */
69
69
  constructor(parent, settings = {}) {
70
70
  var _a, _b, _c, _d;
71
+ this.eventListenerTargets = [];
71
72
  this.previousAccessibilityAnnouncement = '';
72
73
  this.announceUndoCallback = (command) => {
73
74
  this.announceForAccessibility(this.localization.undoAnnouncement(command.description(this, this.localization)));
@@ -305,6 +306,110 @@ export class Editor {
305
306
  this.accessibilityControlArea.addEventListener('input', () => {
306
307
  this.accessibilityControlArea.value = '';
307
308
  });
309
+ document.addEventListener('copy', evt => {
310
+ if (!this.isEventSink(document.querySelector(':focus'))) {
311
+ return;
312
+ }
313
+ const clipboardData = evt.clipboardData;
314
+ if (this.toolController.dispatchInputEvent({
315
+ kind: InputEvtType.CopyEvent,
316
+ setData: (mime, data) => {
317
+ clipboardData === null || clipboardData === void 0 ? void 0 : clipboardData.setData(mime, data);
318
+ },
319
+ })) {
320
+ evt.preventDefault();
321
+ }
322
+ });
323
+ document.addEventListener('paste', evt => {
324
+ this.handlePaste(evt);
325
+ });
326
+ }
327
+ isEventSink(evtTarget) {
328
+ let currentElem = evtTarget;
329
+ while (currentElem !== null) {
330
+ for (const elem of this.eventListenerTargets) {
331
+ if (elem === currentElem) {
332
+ return true;
333
+ }
334
+ }
335
+ currentElem = currentElem.parentElement;
336
+ }
337
+ return false;
338
+ }
339
+ handlePaste(evt) {
340
+ var _a, _b;
341
+ return __awaiter(this, void 0, void 0, function* () {
342
+ const target = (_a = document.querySelector(':focus')) !== null && _a !== void 0 ? _a : evt.target;
343
+ if (!this.isEventSink(target)) {
344
+ return;
345
+ }
346
+ const clipboardData = (_b = evt.dataTransfer) !== null && _b !== void 0 ? _b : evt.clipboardData;
347
+ if (!clipboardData) {
348
+ return;
349
+ }
350
+ // Handle SVG files (prefer to PNG/JPEG)
351
+ for (const file of clipboardData.files) {
352
+ if (file.type.toLowerCase() === 'image/svg+xml') {
353
+ const text = yield file.text();
354
+ if (this.toolController.dispatchInputEvent({
355
+ kind: InputEvtType.PasteEvent,
356
+ mime: file.type,
357
+ data: text,
358
+ })) {
359
+ evt.preventDefault();
360
+ return;
361
+ }
362
+ }
363
+ }
364
+ // Handle image files.
365
+ for (const file of clipboardData.files) {
366
+ const fileType = file.type.toLowerCase();
367
+ if (fileType === 'image/png' || fileType === 'image/jpg') {
368
+ const reader = new FileReader();
369
+ this.showLoadingWarning(0);
370
+ try {
371
+ const data = yield new Promise((resolve, reject) => {
372
+ reader.onload = () => resolve(reader.result);
373
+ reader.onerror = reject;
374
+ reader.onabort = reject;
375
+ reader.onprogress = (evt) => {
376
+ this.showLoadingWarning(evt.loaded / evt.total);
377
+ };
378
+ reader.readAsDataURL(file);
379
+ });
380
+ if (data && this.toolController.dispatchInputEvent({
381
+ kind: InputEvtType.PasteEvent,
382
+ mime: fileType,
383
+ data: data,
384
+ })) {
385
+ evt.preventDefault();
386
+ this.hideLoadingWarning();
387
+ return;
388
+ }
389
+ }
390
+ catch (e) {
391
+ console.error('Error reading image:', e);
392
+ }
393
+ this.hideLoadingWarning();
394
+ }
395
+ }
396
+ // Supported MIMEs for text data, in order of preference
397
+ const supportedMIMEs = [
398
+ 'image/svg+xml',
399
+ 'text/plain',
400
+ ];
401
+ for (const mime of supportedMIMEs) {
402
+ const data = clipboardData.getData(mime);
403
+ if (data && this.toolController.dispatchInputEvent({
404
+ kind: InputEvtType.PasteEvent,
405
+ mime,
406
+ data,
407
+ })) {
408
+ evt.preventDefault();
409
+ return;
410
+ }
411
+ }
412
+ });
308
413
  }
309
414
  /** Adds event listners for keypresses to `elem` and forwards those events to the editor. */
310
415
  handleKeyEventsFrom(elem) {
@@ -333,6 +438,15 @@ export class Editor {
333
438
  evt.preventDefault();
334
439
  }
335
440
  });
441
+ // Allow drop.
442
+ elem.ondragover = evt => {
443
+ evt.preventDefault();
444
+ };
445
+ elem.ondrop = evt => {
446
+ evt.preventDefault();
447
+ this.handlePaste(evt);
448
+ };
449
+ this.eventListenerTargets.push(elem);
336
450
  }
337
451
  /** `apply` a command. `command` will be announced for accessibility. */
338
452
  dispatch(command, addToHistory = true) {
@@ -376,6 +490,7 @@ export class Editor {
376
490
  */
377
491
  asyncApplyOrUnapplyCommands(commands, apply, updateChunkSize) {
378
492
  return __awaiter(this, void 0, void 0, function* () {
493
+ console.assert(updateChunkSize > 0);
379
494
  this.display.setDraftMode(true);
380
495
  for (let i = 0; i < commands.length; i += updateChunkSize) {
381
496
  this.showLoadingWarning(i / commands.length);
@@ -553,9 +668,9 @@ export class Editor {
553
668
  * This is particularly useful when accessing a bundled version of the editor,
554
669
  * where `SVGLoader.fromString` is unavailable.
555
670
  */
556
- loadFromSVG(svgData) {
671
+ loadFromSVG(svgData, sanitize = false) {
557
672
  return __awaiter(this, void 0, void 0, function* () {
558
- const loader = SVGLoader.fromString(svgData);
673
+ const loader = SVGLoader.fromString(svgData, sanitize);
559
674
  yield this.loadFrom(loader);
560
675
  });
561
676
  }
@@ -69,6 +69,9 @@ EditorImage.AddElementCommand = (_a = class extends SerializableCommand {
69
69
  super('add-element');
70
70
  this.element = element;
71
71
  this.applyByFlattening = applyByFlattening;
72
+ // Store the element's serialization --- .serializeToJSON may be called on this
73
+ // even when this is not at the top of the undo/redo stack.
74
+ this.serializedElem = element.serialize();
72
75
  if (isNaN(element.getBBox().area)) {
73
76
  throw new Error('Elements in the image cannot have NaN bounding boxes');
74
77
  }
@@ -93,7 +96,7 @@ EditorImage.AddElementCommand = (_a = class extends SerializableCommand {
93
96
  }
94
97
  serializeToJSON() {
95
98
  return {
96
- elemData: this.element.serialize(),
99
+ elemData: this.serializedElem,
97
100
  };
98
101
  }
99
102
  },
@@ -12,6 +12,7 @@ export declare type SVGLoaderUnknownStyleAttribute = {
12
12
  export default class SVGLoader implements ImageLoader {
13
13
  private source;
14
14
  private onFinish?;
15
+ private readonly storeUnknown;
15
16
  private onAddComponent;
16
17
  private onProgress;
17
18
  private onDetermineExportRect;
@@ -23,13 +24,15 @@ export default class SVGLoader implements ImageLoader {
23
24
  private strokeDataFromElem;
24
25
  private attachUnrecognisedAttrs;
25
26
  private addPath;
27
+ private getTransform;
26
28
  private makeText;
27
29
  private addText;
30
+ private addImage;
28
31
  private addUnknownNode;
29
32
  private updateViewBox;
30
33
  private updateSVGAttrs;
31
34
  private visit;
32
35
  private getSourceAttrs;
33
36
  start(onAddComponent: ComponentAddedListener, onProgress: OnProgressListener, onDetermineExportRect?: OnDetermineExportRectListener | null): Promise<void>;
34
- static fromString(text: string): SVGLoader;
37
+ static fromString(text: string, sanitize?: boolean): SVGLoader;
35
38
  }
@@ -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 ImageComponent from './components/ImageComponent';
11
12
  import Stroke from './components/Stroke';
12
13
  import SVGGlobalAttributesObject from './components/SVGGlobalAttributesObject';
13
14
  import Text from './components/Text';
@@ -22,9 +23,10 @@ export const defaultSVGViewRect = new Rect2(0, 0, 500, 500);
22
23
  export const svgAttributesDataKey = 'svgAttrs';
23
24
  export const svgStyleAttributesDataKey = 'svgStyleAttrs';
24
25
  export default class SVGLoader {
25
- constructor(source, onFinish) {
26
+ constructor(source, onFinish, storeUnknown = true) {
26
27
  this.source = source;
27
28
  this.onFinish = onFinish;
29
+ this.storeUnknown = storeUnknown;
28
30
  this.onAddComponent = null;
29
31
  this.onProgress = null;
30
32
  this.onDetermineExportRect = null;
@@ -88,6 +90,9 @@ export default class SVGLoader {
88
90
  return result;
89
91
  }
90
92
  attachUnrecognisedAttrs(elem, node, supportedAttrs, supportedStyleAttrs) {
93
+ if (!this.storeUnknown) {
94
+ return;
95
+ }
91
96
  for (const attr of node.getAttributeNames()) {
92
97
  if (supportedAttrs.has(attr) || (attr === 'style' && supportedStyleAttrs)) {
93
98
  continue;
@@ -123,10 +128,45 @@ export default class SVGLoader {
123
128
  }
124
129
  catch (e) {
125
130
  console.error('Invalid path in node', node, '\nError:', e, '\nAdding as an unknown object.');
126
- elem = new UnknownSVGObject(node);
131
+ if (this.storeUnknown) {
132
+ elem = new UnknownSVGObject(node);
133
+ }
134
+ else {
135
+ return;
136
+ }
127
137
  }
128
138
  (_a = this.onAddComponent) === null || _a === void 0 ? void 0 : _a.call(this, elem);
129
139
  }
140
+ // If given, 'supportedAttrs' will have x, y, etc. attributes that were used in computing the transform added to it,
141
+ // to prevent storing duplicate transform information when saving the component.
142
+ getTransform(elem, supportedAttrs, computedStyles) {
143
+ computedStyles !== null && computedStyles !== void 0 ? computedStyles : (computedStyles = window.getComputedStyle(elem));
144
+ let transformProperty = computedStyles.transform;
145
+ if (transformProperty === '' || transformProperty === 'none') {
146
+ transformProperty = elem.style.transform || 'none';
147
+ }
148
+ // Prefer the actual .style.transform
149
+ // to the computed stylesheet -- in some browsers, the computedStyles version
150
+ // can have lower precision.
151
+ let transform;
152
+ try {
153
+ transform = Mat33.fromCSSMatrix(elem.style.transform);
154
+ }
155
+ catch (_e) {
156
+ transform = Mat33.fromCSSMatrix(transformProperty);
157
+ }
158
+ const elemX = elem.getAttribute('x');
159
+ const elemY = elem.getAttribute('y');
160
+ if (elemX && elemY) {
161
+ const x = parseFloat(elemX);
162
+ const y = parseFloat(elemY);
163
+ if (!isNaN(x) && !isNaN(y)) {
164
+ supportedAttrs === null || supportedAttrs === void 0 ? void 0 : supportedAttrs.push('x', 'y');
165
+ transform = transform.rightMul(Mat33.translation(Vec2.of(x, y)));
166
+ }
167
+ }
168
+ return transform;
169
+ }
130
170
  makeText(elem) {
131
171
  var _a;
132
172
  const contentList = [];
@@ -167,31 +207,8 @@ export default class SVGLoader {
167
207
  fill: Color4.fromString(computedStyles.fill)
168
208
  },
169
209
  };
170
- let transformProperty = computedStyles.transform;
171
- if (transformProperty === '' || transformProperty === 'none') {
172
- transformProperty = elem.style.transform || 'none';
173
- }
174
- // Compute transform matrix. Prefer the actual .style.transform
175
- // to the computed stylesheet -- in some browsers, the computedStyles version
176
- // can have lower precision.
177
- let transform;
178
- try {
179
- transform = Mat33.fromCSSMatrix(elem.style.transform);
180
- }
181
- catch (_e) {
182
- transform = Mat33.fromCSSMatrix(transformProperty);
183
- }
184
210
  const supportedAttrs = [];
185
- const elemX = elem.getAttribute('x');
186
- const elemY = elem.getAttribute('y');
187
- if (elemX && elemY) {
188
- const x = parseFloat(elemX);
189
- const y = parseFloat(elemY);
190
- if (!isNaN(x) && !isNaN(y)) {
191
- supportedAttrs.push('x', 'y');
192
- transform = transform.rightMul(Mat33.translation(Vec2.of(x, y)));
193
- }
194
- }
211
+ const transform = this.getTransform(elem, supportedAttrs, computedStyles);
195
212
  const result = new Text(contentList, transform, style);
196
213
  this.attachUnrecognisedAttrs(result, elem, new Set(supportedAttrs), new Set(supportedStyleAttrs));
197
214
  return result;
@@ -203,14 +220,34 @@ export default class SVGLoader {
203
220
  (_a = this.onAddComponent) === null || _a === void 0 ? void 0 : _a.call(this, textElem);
204
221
  }
205
222
  catch (e) {
206
- console.error('Invalid text object in node', elem, '. Adding as an unknown object. Error:', e);
223
+ console.error('Invalid text object in node', elem, '. Continuing.... Error:', e);
207
224
  this.addUnknownNode(elem);
208
225
  }
209
226
  }
227
+ addImage(elem) {
228
+ var _a, _b;
229
+ return __awaiter(this, void 0, void 0, function* () {
230
+ const image = new Image();
231
+ image.src = (_a = elem.getAttribute('xlink:href')) !== null && _a !== void 0 ? _a : elem.href.baseVal;
232
+ try {
233
+ const supportedAttrs = [];
234
+ const transform = this.getTransform(elem, supportedAttrs);
235
+ const imageElem = yield ImageComponent.fromImage(image, transform);
236
+ this.attachUnrecognisedAttrs(imageElem, elem, new Set(supportedAttrs), new Set(['transform']));
237
+ (_b = this.onAddComponent) === null || _b === void 0 ? void 0 : _b.call(this, imageElem);
238
+ }
239
+ catch (e) {
240
+ console.error('Error loading image:', e, '. Element: ', elem, '. Continuing...');
241
+ this.addUnknownNode(elem);
242
+ }
243
+ });
244
+ }
210
245
  addUnknownNode(node) {
211
246
  var _a;
212
- const component = new UnknownSVGObject(node);
213
- (_a = this.onAddComponent) === null || _a === void 0 ? void 0 : _a.call(this, component);
247
+ if (this.storeUnknown) {
248
+ const component = new UnknownSVGObject(node);
249
+ (_a = this.onAddComponent) === null || _a === void 0 ? void 0 : _a.call(this, component);
250
+ }
214
251
  }
215
252
  updateViewBox(node) {
216
253
  var _a;
@@ -232,7 +269,9 @@ export default class SVGLoader {
232
269
  }
233
270
  updateSVGAttrs(node) {
234
271
  var _a;
235
- (_a = this.onAddComponent) === null || _a === void 0 ? void 0 : _a.call(this, new SVGGlobalAttributesObject(this.getSourceAttrs(node)));
272
+ if (this.storeUnknown) {
273
+ (_a = this.onAddComponent) === null || _a === void 0 ? void 0 : _a.call(this, new SVGGlobalAttributesObject(this.getSourceAttrs(node)));
274
+ }
236
275
  }
237
276
  visit(node) {
238
277
  var _a;
@@ -250,6 +289,11 @@ export default class SVGLoader {
250
289
  this.addText(node);
251
290
  visitChildren = false;
252
291
  break;
292
+ case 'image':
293
+ yield this.addImage(node);
294
+ // Images should not have children.
295
+ visitChildren = false;
296
+ break;
253
297
  case 'svg':
254
298
  this.updateViewBox(node);
255
299
  this.updateSVGAttrs(node);
@@ -257,7 +301,7 @@ export default class SVGLoader {
257
301
  default:
258
302
  console.warn('Unknown SVG element,', node);
259
303
  if (!(node instanceof SVGElement)) {
260
- console.warn('Element', node, 'is not an SVGElement! Continuing anyway.');
304
+ console.warn('Element', node, 'is not an SVGElement!', this.storeUnknown ? 'Continuing anyway.' : 'Skipping.');
261
305
  }
262
306
  this.addUnknownNode(node);
263
307
  return;
@@ -296,7 +340,8 @@ export default class SVGLoader {
296
340
  });
297
341
  }
298
342
  // TODO: Handling unsafe data! Tripple-check that this is secure!
299
- static fromString(text) {
343
+ // @param sanitize - if `true`, don't store unknown attributes.
344
+ static fromString(text, sanitize = false) {
300
345
  var _a, _b;
301
346
  const sandbox = document.createElement('iframe');
302
347
  sandbox.src = 'about:blank';
@@ -336,6 +381,6 @@ export default class SVGLoader {
336
381
  return new SVGLoader(svgElem, () => {
337
382
  svgElem.remove();
338
383
  sandbox.remove();
339
- });
384
+ }, !sanitize);
340
385
  }
341
386
  }
@@ -8,6 +8,7 @@ declare class UndoRedoHistory {
8
8
  private announceUndoCallback;
9
9
  private undoStack;
10
10
  private redoStack;
11
+ private maxUndoRedoStackSize;
11
12
  constructor(editor: Editor, announceRedoCallback: AnnounceRedoCallback, announceUndoCallback: AnnounceUndoCallback);
12
13
  private fireUpdateEvent;
13
14
  push(command: Command, apply?: boolean): void;
@@ -5,6 +5,7 @@ class UndoRedoHistory {
5
5
  this.editor = editor;
6
6
  this.announceRedoCallback = announceRedoCallback;
7
7
  this.announceUndoCallback = announceUndoCallback;
8
+ this.maxUndoRedoStackSize = 700;
8
9
  this.undoStack = [];
9
10
  this.redoStack = [];
10
11
  }
@@ -25,6 +26,11 @@ class UndoRedoHistory {
25
26
  elem.onDrop(this.editor);
26
27
  }
27
28
  this.redoStack = [];
29
+ if (this.undoStack.length > this.maxUndoRedoStackSize) {
30
+ const removeAtOnceCount = 10;
31
+ const removedElements = this.undoStack.splice(0, removeAtOnceCount);
32
+ removedElements.forEach(elem => elem.onDrop(this.editor));
33
+ }
28
34
  this.fireUpdateEvent();
29
35
  this.editor.notifier.dispatch(EditorEventType.CommandDone, {
30
36
  kind: EditorEventType.CommandDone,
@@ -29,6 +29,7 @@ export declare class Viewport {
29
29
  getRotationAngle(): number;
30
30
  static roundPoint<T extends Point2 | number>(point: T, tolerance: number): PointDataType<T>;
31
31
  roundPoint(point: Point2): Point2;
32
+ computeZoomToTransform(toMakeVisible: Rect2, allowZoomIn?: boolean, allowZoomOut?: boolean): Mat33;
32
33
  zoomTo(toMakeVisible: Rect2, allowZoomIn?: boolean, allowZoomOut?: boolean): Command;
33
34
  }
34
35
  export default Viewport;
@@ -29,6 +29,7 @@ export class Viewport {
29
29
  updateScreenSize(screenSize) {
30
30
  this.screenRect = this.screenRect.resizedTo(screenSize);
31
31
  }
32
+ // Get the screen's visible region transformed into canvas space.
32
33
  get visibleRect() {
33
34
  return this.screenRect.transformedBoundingBox(this.inverseTransform);
34
35
  }
@@ -93,10 +94,8 @@ export class Viewport {
93
94
  roundPoint(point) {
94
95
  return Viewport.roundPoint(point, 1 / this.getScaleFactor());
95
96
  }
96
- // Returns a Command that transforms the view such that [rect] is visible, and perhaps
97
- // centered in the viewport.
98
- // Returns null if no transformation is necessary
99
- zoomTo(toMakeVisible, allowZoomIn = true, allowZoomOut = true) {
97
+ // Computes and returns an affine transformation that makes `toMakeVisible` visible and roughly centered on the screen.
98
+ computeZoomToTransform(toMakeVisible, allowZoomIn = true, allowZoomOut = true) {
100
99
  let transform = Mat33.identity;
101
100
  if (toMakeVisible.w === 0 || toMakeVisible.h === 0) {
102
101
  throw new Error(`${toMakeVisible.toString()} rectangle is empty! Cannot zoom to!`);
@@ -136,6 +135,15 @@ export class Viewport {
136
135
  console.warn('Unable to zoom to ', toMakeVisible, '! Computed transform', transform, 'is singular.');
137
136
  transform = Mat33.identity;
138
137
  }
138
+ return transform;
139
+ }
140
+ // Returns a Command that transforms the view such that [rect] is visible, and perhaps
141
+ // centered in the viewport.
142
+ // Returns null if no transformation is necessary
143
+ //
144
+ // @see {@link computeZoomToTransform}
145
+ zoomTo(toMakeVisible, allowZoomIn = true, allowZoomOut = true) {
146
+ const transform = this.computeZoomToTransform(toMakeVisible, allowZoomIn, allowZoomOut);
139
147
  return new Viewport.ViewportTransform(transform);
140
148
  }
141
149
  }
@@ -3,4 +3,5 @@ import Duplicate from './Duplicate';
3
3
  import Erase from './Erase';
4
4
  import invertCommand from './invertCommand';
5
5
  import SerializableCommand from './SerializableCommand';
6
- export { Command, Duplicate, Erase, SerializableCommand, invertCommand, };
6
+ import uniteCommands from './uniteCommands';
7
+ export { Command, Duplicate, Erase, SerializableCommand, invertCommand, uniteCommands, };
@@ -3,4 +3,5 @@ import Duplicate from './Duplicate';
3
3
  import Erase from './Erase';
4
4
  import invertCommand from './invertCommand';
5
5
  import SerializableCommand from './SerializableCommand';
6
- export { Command, Duplicate, Erase, SerializableCommand, invertCommand, };
6
+ import uniteCommands from './uniteCommands';
7
+ export { Command, Duplicate, Erase, SerializableCommand, invertCommand, uniteCommands, };
@@ -17,6 +17,7 @@ export interface CommandLocalization {
17
17
  eraseAction: (elemDescription: string, numElems: number) => string;
18
18
  duplicateAction: (elemDescription: string, count: number) => string;
19
19
  inverseOf: (actionDescription: string) => string;
20
+ unionOf: (actionDescription: string, actionCount: number) => string;
20
21
  selectedElements: (count: number) => string;
21
22
  }
22
23
  export declare const defaultCommandLocalization: CommandLocalization;
@@ -5,6 +5,7 @@ export const defaultCommandLocalization = {
5
5
  addElementAction: (componentDescription) => `Added ${componentDescription}`,
6
6
  eraseAction: (componentDescription, numElems) => `Erased ${numElems} ${componentDescription}`,
7
7
  duplicateAction: (componentDescription, numElems) => `Duplicated ${numElems} ${componentDescription}`,
8
+ unionOf: (actionDescription, actionCount) => `Union: ${actionCount} ${actionDescription}`,
8
9
  inverseOf: (actionDescription) => `Inverse of ${actionDescription}`,
9
10
  elements: 'Elements',
10
11
  erasedNoElements: 'Erased nothing',
@@ -0,0 +1,4 @@
1
+ import Command from './Command';
2
+ import SerializableCommand from './SerializableCommand';
3
+ declare const uniteCommands: <T extends Command>(commands: T[], applyChunkSize?: number) => T extends SerializableCommand ? SerializableCommand : Command;
4
+ export default uniteCommands;
@@ -0,0 +1,105 @@
1
+ import Command from './Command';
2
+ import SerializableCommand from './SerializableCommand';
3
+ class NonSerializableUnion extends Command {
4
+ constructor(commands, applyChunkSize) {
5
+ super();
6
+ this.commands = commands;
7
+ this.applyChunkSize = applyChunkSize;
8
+ }
9
+ apply(editor) {
10
+ if (this.applyChunkSize === undefined) {
11
+ for (const command of this.commands) {
12
+ command.apply(editor);
13
+ }
14
+ }
15
+ else {
16
+ editor.asyncApplyCommands(this.commands, this.applyChunkSize);
17
+ }
18
+ }
19
+ unapply(editor) {
20
+ if (this.applyChunkSize === undefined) {
21
+ for (const command of this.commands) {
22
+ command.unapply(editor);
23
+ }
24
+ }
25
+ else {
26
+ editor.asyncUnapplyCommands(this.commands, this.applyChunkSize);
27
+ }
28
+ }
29
+ description(editor, localizationTable) {
30
+ const descriptions = [];
31
+ let lastDescription = null;
32
+ let duplicateDescriptionCount = 0;
33
+ for (const part of this.commands) {
34
+ const description = part.description(editor, localizationTable);
35
+ if (description !== lastDescription && lastDescription !== null) {
36
+ descriptions.push(localizationTable.unionOf(lastDescription, duplicateDescriptionCount));
37
+ lastDescription = null;
38
+ duplicateDescriptionCount = 0;
39
+ }
40
+ duplicateDescriptionCount++;
41
+ lastDescription !== null && lastDescription !== void 0 ? lastDescription : (lastDescription = description);
42
+ }
43
+ if (duplicateDescriptionCount > 1) {
44
+ descriptions.push(localizationTable.unionOf(lastDescription, duplicateDescriptionCount));
45
+ }
46
+ else if (duplicateDescriptionCount === 1) {
47
+ descriptions.push(lastDescription);
48
+ }
49
+ return descriptions.join(', ');
50
+ }
51
+ }
52
+ class SerializableUnion extends SerializableCommand {
53
+ constructor(commands, applyChunkSize) {
54
+ super('union');
55
+ this.commands = commands;
56
+ this.applyChunkSize = applyChunkSize;
57
+ this.nonserializableCommand = new NonSerializableUnion(commands, applyChunkSize);
58
+ }
59
+ serializeToJSON() {
60
+ return {
61
+ applyChunkSize: this.applyChunkSize,
62
+ data: this.commands.map(command => command.serialize()),
63
+ };
64
+ }
65
+ apply(editor) {
66
+ this.nonserializableCommand.apply(editor);
67
+ }
68
+ unapply(editor) {
69
+ this.nonserializableCommand.unapply(editor);
70
+ }
71
+ description(editor, localizationTable) {
72
+ return this.nonserializableCommand.description(editor, localizationTable);
73
+ }
74
+ }
75
+ const uniteCommands = (commands, applyChunkSize) => {
76
+ let allSerializable = true;
77
+ for (const command of commands) {
78
+ if (!(command instanceof SerializableCommand)) {
79
+ allSerializable = false;
80
+ break;
81
+ }
82
+ }
83
+ if (!allSerializable) {
84
+ return new NonSerializableUnion(commands, applyChunkSize);
85
+ }
86
+ else {
87
+ const castedCommands = commands;
88
+ return new SerializableUnion(castedCommands, applyChunkSize);
89
+ }
90
+ };
91
+ SerializableCommand.register('union', (data, editor) => {
92
+ if (typeof data.data.length !== 'number') {
93
+ throw new Error('Unions of commands must serialize to lists of serialization data.');
94
+ }
95
+ const applyChunkSize = data.applyChunkSize;
96
+ if (typeof applyChunkSize !== 'number' && applyChunkSize !== undefined) {
97
+ throw new Error('serialized applyChunkSize is neither undefined nor a number.');
98
+ }
99
+ const commands = [];
100
+ for (const part of data.data) {
101
+ commands.push(SerializableCommand.deserialize(part, editor));
102
+ }
103
+ return uniteCommands(commands, applyChunkSize);
104
+ });
105
+ export default uniteCommands;
@@ -28,6 +28,8 @@ export default abstract class AbstractComponent {
28
28
  protected abstract serializeToJSON(): any[] | Record<string, any> | number | string | null;
29
29
  protected abstract applyTransformation(affineTransfm: Mat33): void;
30
30
  transformBy(affineTransfm: Mat33): SerializableCommand;
31
+ private static transformElementCommandId;
32
+ private static UnresolvedTransformElementCommand;
31
33
  private static TransformElementCommand;
32
34
  abstract description(localizationTable: ImageComponentLocalization): string;
33
35
  protected abstract createClone(): AbstractComponent;