js-draw 0.1.6 → 0.1.9

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 (138) hide show
  1. package/.eslintrc.js +1 -0
  2. package/CHANGELOG.md +16 -0
  3. package/README.md +2 -2
  4. package/dist/bundle.js +1 -1
  5. package/dist/src/Color4.js +6 -2
  6. package/dist/src/Editor.d.ts +1 -0
  7. package/dist/src/Editor.js +23 -8
  8. package/dist/src/EditorImage.d.ts +8 -13
  9. package/dist/src/EditorImage.js +51 -29
  10. package/dist/src/Viewport.d.ts +9 -1
  11. package/dist/src/Viewport.js +3 -1
  12. package/dist/src/commands/Command.d.ts +9 -8
  13. package/dist/src/commands/Command.js +15 -14
  14. package/dist/src/commands/Duplicate.d.ts +14 -0
  15. package/dist/src/commands/Duplicate.js +34 -0
  16. package/dist/src/commands/Erase.d.ts +5 -2
  17. package/dist/src/commands/Erase.js +28 -9
  18. package/dist/src/commands/SerializableCommand.d.ts +13 -0
  19. package/dist/src/commands/SerializableCommand.js +28 -0
  20. package/dist/src/commands/localization.d.ts +2 -0
  21. package/dist/src/commands/localization.js +2 -0
  22. package/dist/src/components/AbstractComponent.d.ts +15 -2
  23. package/dist/src/components/AbstractComponent.js +122 -26
  24. package/dist/src/components/SVGGlobalAttributesObject.d.ts +6 -1
  25. package/dist/src/components/SVGGlobalAttributesObject.js +23 -1
  26. package/dist/src/components/Stroke.d.ts +5 -0
  27. package/dist/src/components/Stroke.js +32 -1
  28. package/dist/src/components/Text.d.ts +11 -4
  29. package/dist/src/components/Text.js +57 -3
  30. package/dist/src/components/UnknownSVGObject.d.ts +2 -0
  31. package/dist/src/components/UnknownSVGObject.js +12 -1
  32. package/dist/src/components/builders/RectangleBuilder.d.ts +3 -1
  33. package/dist/src/components/builders/RectangleBuilder.js +17 -8
  34. package/dist/src/components/util/describeComponentList.d.ts +4 -0
  35. package/dist/src/components/util/describeComponentList.js +14 -0
  36. package/dist/src/geometry/Path.d.ts +4 -1
  37. package/dist/src/geometry/Path.js +4 -0
  38. package/dist/src/rendering/Display.d.ts +3 -0
  39. package/dist/src/rendering/Display.js +13 -0
  40. package/dist/src/rendering/RenderingStyle.d.ts +24 -0
  41. package/dist/src/rendering/RenderingStyle.js +32 -0
  42. package/dist/src/rendering/caching/RenderingCacheNode.js +5 -1
  43. package/dist/src/rendering/renderers/AbstractRenderer.d.ts +1 -8
  44. package/dist/src/rendering/renderers/AbstractRenderer.js +1 -6
  45. package/dist/src/rendering/renderers/CanvasRenderer.d.ts +2 -1
  46. package/dist/src/rendering/renderers/DummyRenderer.d.ts +2 -1
  47. package/dist/src/rendering/renderers/SVGRenderer.d.ts +2 -1
  48. package/dist/src/rendering/renderers/TextOnlyRenderer.d.ts +2 -1
  49. package/dist/src/toolbar/HTMLToolbar.d.ts +2 -1
  50. package/dist/src/toolbar/HTMLToolbar.js +57 -535
  51. package/dist/src/toolbar/icons.d.ts +5 -0
  52. package/dist/src/toolbar/icons.js +186 -13
  53. package/dist/src/toolbar/localization.d.ts +4 -0
  54. package/dist/src/toolbar/localization.js +4 -0
  55. package/dist/src/toolbar/makeColorInput.d.ts +5 -0
  56. package/dist/src/toolbar/makeColorInput.js +95 -0
  57. package/dist/src/toolbar/widgets/BaseToolWidget.d.ts +12 -0
  58. package/dist/src/toolbar/widgets/BaseToolWidget.js +44 -0
  59. package/dist/src/toolbar/widgets/BaseWidget.d.ts +32 -0
  60. package/dist/src/toolbar/widgets/BaseWidget.js +148 -0
  61. package/dist/src/toolbar/widgets/EraserWidget.d.ts +6 -0
  62. package/dist/src/toolbar/widgets/EraserWidget.js +14 -0
  63. package/dist/src/toolbar/widgets/HandToolWidget.d.ts +13 -0
  64. package/dist/src/toolbar/widgets/HandToolWidget.js +133 -0
  65. package/dist/src/toolbar/widgets/PenWidget.d.ts +20 -0
  66. package/dist/src/toolbar/widgets/PenWidget.js +131 -0
  67. package/dist/src/toolbar/widgets/SelectionWidget.d.ts +11 -0
  68. package/dist/src/toolbar/widgets/SelectionWidget.js +56 -0
  69. package/dist/src/toolbar/widgets/TextToolWidget.d.ts +13 -0
  70. package/dist/src/toolbar/widgets/TextToolWidget.js +72 -0
  71. package/dist/src/tools/Pen.js +1 -1
  72. package/dist/src/tools/PipetteTool.d.ts +20 -0
  73. package/dist/src/tools/PipetteTool.js +40 -0
  74. package/dist/src/tools/SelectionTool.d.ts +2 -0
  75. package/dist/src/tools/SelectionTool.js +41 -23
  76. package/dist/src/tools/TextTool.js +1 -1
  77. package/dist/src/tools/ToolController.d.ts +3 -1
  78. package/dist/src/tools/ToolController.js +4 -0
  79. package/dist/src/tools/localization.d.ts +2 -1
  80. package/dist/src/tools/localization.js +3 -2
  81. package/dist/src/types.d.ts +7 -2
  82. package/dist/src/types.js +1 -0
  83. package/jest.config.js +2 -0
  84. package/package.json +6 -6
  85. package/src/Color4.ts +9 -3
  86. package/src/Editor.ts +28 -11
  87. package/src/EditorImage.test.ts +5 -5
  88. package/src/EditorImage.ts +61 -20
  89. package/src/SVGLoader.ts +2 -1
  90. package/src/Viewport.ts +2 -1
  91. package/src/commands/Command.ts +21 -19
  92. package/src/commands/Duplicate.ts +49 -0
  93. package/src/commands/Erase.ts +34 -13
  94. package/src/commands/SerializableCommand.ts +41 -0
  95. package/src/commands/localization.ts +5 -0
  96. package/src/components/AbstractComponent.ts +168 -26
  97. package/src/components/SVGGlobalAttributesObject.ts +34 -2
  98. package/src/components/Stroke.test.ts +53 -0
  99. package/src/components/Stroke.ts +37 -2
  100. package/src/components/Text.test.ts +38 -0
  101. package/src/components/Text.ts +80 -5
  102. package/src/components/UnknownSVGObject.test.ts +10 -0
  103. package/src/components/UnknownSVGObject.ts +15 -1
  104. package/src/components/builders/FreehandLineBuilder.ts +2 -1
  105. package/src/components/builders/RectangleBuilder.ts +23 -8
  106. package/src/components/util/describeComponentList.ts +18 -0
  107. package/src/geometry/Path.ts +8 -1
  108. package/src/rendering/Display.ts +17 -1
  109. package/src/rendering/RenderingStyle.test.ts +68 -0
  110. package/src/rendering/RenderingStyle.ts +46 -0
  111. package/src/rendering/caching/RenderingCache.test.ts +1 -1
  112. package/src/rendering/caching/RenderingCacheNode.ts +6 -1
  113. package/src/rendering/renderers/AbstractRenderer.ts +1 -15
  114. package/src/rendering/renderers/CanvasRenderer.ts +2 -1
  115. package/src/rendering/renderers/DummyRenderer.ts +2 -1
  116. package/src/rendering/renderers/SVGRenderer.ts +2 -1
  117. package/src/rendering/renderers/TextOnlyRenderer.ts +2 -1
  118. package/src/toolbar/HTMLToolbar.ts +64 -661
  119. package/src/toolbar/icons.ts +205 -13
  120. package/src/toolbar/localization.ts +10 -2
  121. package/src/toolbar/makeColorInput.ts +120 -0
  122. package/src/toolbar/toolbar.css +116 -78
  123. package/src/toolbar/widgets/BaseToolWidget.ts +53 -0
  124. package/src/toolbar/widgets/BaseWidget.ts +175 -0
  125. package/src/toolbar/widgets/EraserWidget.ts +16 -0
  126. package/src/toolbar/widgets/HandToolWidget.ts +186 -0
  127. package/src/toolbar/widgets/PenWidget.ts +165 -0
  128. package/src/toolbar/widgets/SelectionWidget.ts +72 -0
  129. package/src/toolbar/widgets/TextToolWidget.ts +90 -0
  130. package/src/tools/Pen.ts +1 -1
  131. package/src/tools/PipetteTool.ts +56 -0
  132. package/src/tools/SelectionTool.test.ts +2 -4
  133. package/src/tools/SelectionTool.ts +47 -27
  134. package/src/tools/TextTool.ts +1 -1
  135. package/src/tools/ToolController.ts +10 -6
  136. package/src/tools/UndoRedoShortcut.test.ts +1 -1
  137. package/src/tools/localization.ts +6 -3
  138. package/src/types.ts +12 -1
@@ -8,9 +8,13 @@ export default class Color4 {
8
8
  }
9
9
  // Each component should be in the range [0, 1]
10
10
  static ofRGB(red, green, blue) {
11
- return new Color4(red, green, blue, 1.0);
11
+ return Color4.ofRGBA(red, green, blue, 1.0);
12
12
  }
13
13
  static ofRGBA(red, green, blue, alpha) {
14
+ red = Math.max(0, Math.min(red, 1));
15
+ green = Math.max(0, Math.min(green, 1));
16
+ blue = Math.max(0, Math.min(blue, 1));
17
+ alpha = Math.max(0, Math.min(alpha, 1));
14
18
  return new Color4(red, green, blue, alpha);
15
19
  }
16
20
  static fromHex(hexString) {
@@ -40,7 +44,7 @@ export default class Color4 {
40
44
  if (components.length !== 4) {
41
45
  throw new Error(`Unable to parse ${hexString}: Wrong number of components.`);
42
46
  }
43
- return new Color4(components[0], components[1], components[2], components[3]);
47
+ return Color4.ofRGBA(components[0], components[1], components[2], components[3]);
44
48
  }
45
49
  // Like fromHex, but can handle additional colors if an HTML5Canvas is available.
46
50
  static fromString(text) {
@@ -38,6 +38,7 @@ export declare class Editor {
38
38
  addToolbar(defaultLayout?: boolean): HTMLToolbar;
39
39
  private registerListeners;
40
40
  dispatch(command: Command, addToHistory?: boolean): void;
41
+ dispatchNoAnnounce(command: Command, addToHistory?: boolean): void;
41
42
  private asyncApplyOrUnapplyCommands;
42
43
  asyncApplyCommands(commands: Command[], chunkSize: number): Promise<void>;
43
44
  asyncUnapplyCommands(commands: Command[], chunkSize: number): Promise<void>;
@@ -10,6 +10,7 @@ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, ge
10
10
  import EditorImage from './EditorImage';
11
11
  import ToolController from './tools/ToolController';
12
12
  import { InputEvtType, EditorEventType } from './types';
13
+ import Command from './commands/Command';
13
14
  import UndoRedoHistory from './UndoRedoHistory';
14
15
  import Viewport from './Viewport';
15
16
  import EventDispatcher from './EventDispatcher';
@@ -69,7 +70,7 @@ export class Editor {
69
70
  this.importExportViewport.updateScreenSize(Vec2.of(500, 500));
70
71
  this.viewport.updateScreenSize(Vec2.of(this.display.width, this.display.height));
71
72
  this.registerListeners();
72
- this.rerender();
73
+ this.queueRerender();
73
74
  this.hideLoadingWarning();
74
75
  }
75
76
  // Returns a reference to this' container.
@@ -233,6 +234,7 @@ export class Editor {
233
234
  this.queueRerender();
234
235
  });
235
236
  }
237
+ // Adds to history by default
236
238
  dispatch(command, addToHistory = true) {
237
239
  if (addToHistory) {
238
240
  // .push applies [command] to this
@@ -243,6 +245,15 @@ export class Editor {
243
245
  }
244
246
  this.announceForAccessibility(command.description(this.localization));
245
247
  }
248
+ // Dispatches a command without announcing it. By default, does not add to history.
249
+ dispatchNoAnnounce(command, addToHistory = false) {
250
+ if (addToHistory) {
251
+ this.history.push(command);
252
+ }
253
+ else {
254
+ command.apply(this);
255
+ }
256
+ }
246
257
  // Apply a large transformation in chunks.
247
258
  // If [apply] is false, the commands are unapplied.
248
259
  // Triggers a re-render after each [updateChunkSize]-sized group of commands
@@ -290,6 +301,10 @@ export class Editor {
290
301
  }
291
302
  rerender(showImageBounds = true) {
292
303
  this.display.startRerender();
304
+ // Don't render if the display has zero size.
305
+ if (this.display.width === 0 || this.display.height === 0) {
306
+ return;
307
+ }
293
308
  // Draw a rectangle around the region that will be visible on save
294
309
  const renderer = this.display.getDryInkRenderer();
295
310
  this.image.renderWithCache(renderer, this.display.getCache(), this.viewport);
@@ -365,7 +380,7 @@ export class Editor {
365
380
  this.showLoadingWarning(0);
366
381
  this.display.setDraftMode(true);
367
382
  yield loader.start((component) => {
368
- (new EditorImage.AddElementCommand(component)).apply(this);
383
+ this.dispatchNoAnnounce(EditorImage.addElement(component));
369
384
  }, (countProcessed, totalToProcess) => {
370
385
  if (countProcessed % 500 === 0) {
371
386
  this.showLoadingWarning(countProcessed / totalToProcess);
@@ -376,8 +391,8 @@ export class Editor {
376
391
  }
377
392
  return null;
378
393
  }, (importExportRect) => {
379
- this.setImportExportRect(importExportRect).apply(this);
380
- this.viewport.zoomTo(importExportRect).apply(this);
394
+ this.dispatchNoAnnounce(this.setImportExportRect(importExportRect), false);
395
+ this.dispatchNoAnnounce(this.viewport.zoomTo(importExportRect), false);
381
396
  });
382
397
  this.hideLoadingWarning();
383
398
  this.display.setDraftMode(false);
@@ -392,22 +407,22 @@ export class Editor {
392
407
  setImportExportRect(imageRect) {
393
408
  const origSize = this.importExportViewport.visibleRect.size;
394
409
  const origTransform = this.importExportViewport.canvasToScreenTransform;
395
- return {
410
+ return new class extends Command {
396
411
  apply(editor) {
397
412
  const viewport = editor.importExportViewport;
398
413
  viewport.updateScreenSize(imageRect.size);
399
414
  viewport.resetTransform(Mat33.translation(imageRect.topLeft.times(-1)));
400
415
  editor.queueRerender();
401
- },
416
+ }
402
417
  unapply(editor) {
403
418
  const viewport = editor.importExportViewport;
404
419
  viewport.updateScreenSize(origSize);
405
420
  viewport.resetTransform(origTransform);
406
421
  editor.queueRerender();
407
- },
422
+ }
408
423
  description(localizationTable) {
409
424
  return localizationTable.resizeOutputCommand(imageRect);
410
- },
425
+ }
411
426
  };
412
427
  }
413
428
  // Alias for loadFrom(SVGLoader.fromString).
@@ -1,31 +1,25 @@
1
- import Editor from './Editor';
2
1
  import AbstractRenderer from './rendering/renderers/AbstractRenderer';
2
+ import Command from './commands/Command';
3
3
  import Viewport from './Viewport';
4
4
  import AbstractComponent from './components/AbstractComponent';
5
5
  import Rect2 from './geometry/Rect2';
6
- import { EditorLocalization } from './localization';
7
6
  import RenderingCache from './rendering/caching/RenderingCache';
8
7
  export declare const sortLeavesByZIndex: (leaves: Array<ImageNode>) => void;
9
8
  export default class EditorImage {
10
9
  private root;
10
+ private componentsById;
11
11
  constructor();
12
- private addElement;
13
12
  findParent(elem: AbstractComponent): ImageNode | null;
14
13
  renderWithCache(screenRenderer: AbstractRenderer, cache: RenderingCache, viewport: Viewport): void;
15
14
  render(renderer: AbstractRenderer, viewport: Viewport): void;
16
15
  renderAll(renderer: AbstractRenderer): void;
17
16
  getElementsIntersectingRegion(region: Rect2): AbstractComponent[];
18
- static AddElementCommand: {
19
- new (element: AbstractComponent, applyByFlattening?: boolean): {
20
- readonly "__#679@#element": AbstractComponent;
21
- "__#679@#applyByFlattening": boolean;
22
- apply(editor: Editor): void;
23
- unapply(editor: Editor): void;
24
- description(localization: EditorLocalization): string;
25
- };
26
- };
17
+ onDestroyElement(elem: AbstractComponent): void;
18
+ lookupElement(id: string): AbstractComponent | null;
19
+ private addElementDirectly;
20
+ static addElement(elem: AbstractComponent, applyByFlattening?: boolean): Command;
21
+ private static AddElementCommand;
27
22
  }
28
- export declare type AddElementCommand = typeof EditorImage.AddElementCommand.prototype;
29
23
  declare type TooSmallToRenderCheck = (rect: Rect2) => boolean;
30
24
  export declare class ImageNode {
31
25
  private parent;
@@ -47,6 +41,7 @@ export declare class ImageNode {
47
41
  addLeaf(leaf: AbstractComponent): ImageNode;
48
42
  getBBox(): Rect2;
49
43
  recomputeBBox(bubbleUp: boolean): void;
44
+ private updateParents;
50
45
  private rebalance;
51
46
  remove(): void;
52
47
  render(renderer: AbstractRenderer, visibleRect: Rect2): void;
@@ -1,16 +1,7 @@
1
- var __classPrivateFieldSet = (this && this.__classPrivateFieldSet) || function (receiver, state, value, kind, f) {
2
- if (kind === "m") throw new TypeError("Private method is not writable");
3
- if (kind === "a" && !f) throw new TypeError("Private accessor was defined without a setter");
4
- if (typeof state === "function" ? receiver !== state || !f : !state.has(receiver)) throw new TypeError("Cannot write private member to an object whose class did not declare it");
5
- return (kind === "a" ? f.call(receiver, value) : f ? f.value = value : state.set(receiver, value)), value;
6
- };
7
- var __classPrivateFieldGet = (this && this.__classPrivateFieldGet) || function (receiver, state, kind, f) {
8
- if (kind === "a" && !f) throw new TypeError("Private accessor was defined without a getter");
9
- if (typeof state === "function" ? receiver !== state || !f : !state.has(receiver)) throw new TypeError("Cannot read private member from an object whose class did not declare it");
10
- return kind === "m" ? f : kind === "a" ? f.call(receiver) : f ? f.value : state.get(receiver);
11
- };
12
- var _element, _applyByFlattening, _a;
1
+ var _a;
2
+ import AbstractComponent from './components/AbstractComponent';
13
3
  import Rect2 from './geometry/Rect2';
4
+ import SerializableCommand from './commands/SerializableCommand';
14
5
  export const sortLeavesByZIndex = (leaves) => {
15
6
  leaves.sort((a, b) => a.getContent().getZIndex() - b.getContent().getZIndex());
16
7
  };
@@ -18,9 +9,7 @@ export const sortLeavesByZIndex = (leaves) => {
18
9
  export default class EditorImage {
19
10
  constructor() {
20
11
  this.root = new ImageNode();
21
- }
22
- addElement(elem) {
23
- return this.root.addLeaf(elem);
12
+ this.componentsById = {};
24
13
  }
25
14
  // Returns the parent of the given element, if it exists.
26
15
  findParent(elem) {
@@ -51,42 +40,65 @@ export default class EditorImage {
51
40
  sortLeavesByZIndex(leaves);
52
41
  return leaves.map(leaf => leaf.getContent());
53
42
  }
43
+ onDestroyElement(elem) {
44
+ delete this.componentsById[elem.getId()];
45
+ }
46
+ lookupElement(id) {
47
+ var _a;
48
+ return (_a = this.componentsById[id]) !== null && _a !== void 0 ? _a : null;
49
+ }
50
+ addElementDirectly(elem) {
51
+ this.componentsById[elem.getId()] = elem;
52
+ return this.root.addLeaf(elem);
53
+ }
54
+ static addElement(elem, applyByFlattening = false) {
55
+ return new EditorImage.AddElementCommand(elem, applyByFlattening);
56
+ }
54
57
  }
55
58
  // A Command that can access private [EditorImage] functionality
56
- EditorImage.AddElementCommand = (_a = class {
59
+ EditorImage.AddElementCommand = (_a = class extends SerializableCommand {
57
60
  // If [applyByFlattening], then the rendered content of this element
58
61
  // is present on the display's wet ink canvas. As such, no re-render is necessary
59
62
  // the first time this command is applied (the surfaces are joined instead).
60
63
  constructor(element, applyByFlattening = false) {
61
- _element.set(this, void 0);
62
- _applyByFlattening.set(this, false);
63
- __classPrivateFieldSet(this, _element, element, "f");
64
- __classPrivateFieldSet(this, _applyByFlattening, applyByFlattening, "f");
65
- if (isNaN(__classPrivateFieldGet(this, _element, "f").getBBox().area)) {
64
+ super('add-element');
65
+ this.element = element;
66
+ this.applyByFlattening = applyByFlattening;
67
+ if (isNaN(element.getBBox().area)) {
66
68
  throw new Error('Elements in the image cannot have NaN bounding boxes');
67
69
  }
68
70
  }
69
71
  apply(editor) {
70
- editor.image.addElement(__classPrivateFieldGet(this, _element, "f"));
71
- if (!__classPrivateFieldGet(this, _applyByFlattening, "f")) {
72
+ editor.image.addElementDirectly(this.element);
73
+ if (!this.applyByFlattening) {
72
74
  editor.queueRerender();
73
75
  }
74
76
  else {
75
- __classPrivateFieldSet(this, _applyByFlattening, false, "f");
77
+ this.applyByFlattening = false;
76
78
  editor.display.flatten();
77
79
  }
78
80
  }
79
81
  unapply(editor) {
80
- const container = editor.image.findParent(__classPrivateFieldGet(this, _element, "f"));
82
+ const container = editor.image.findParent(this.element);
81
83
  container === null || container === void 0 ? void 0 : container.remove();
82
84
  editor.queueRerender();
83
85
  }
84
86
  description(localization) {
85
- return localization.addElementAction(__classPrivateFieldGet(this, _element, "f").description(localization));
87
+ return localization.addElementAction(this.element.description(localization));
88
+ }
89
+ serializeToString() {
90
+ return JSON.stringify({
91
+ elemData: this.element.serialize(),
92
+ });
86
93
  }
87
94
  },
88
- _element = new WeakMap(),
89
- _applyByFlattening = new WeakMap(),
95
+ (() => {
96
+ SerializableCommand.register('add-element', (data, _editor) => {
97
+ const json = JSON.parse(data);
98
+ const elem = AbstractComponent.deserialize(json.elemData);
99
+ return new EditorImage.AddElementCommand(elem);
100
+ });
101
+ })(),
90
102
  _a);
91
103
  // TODO: Assign leaf nodes to CacheNodes. When leaf nodes are modified, the corresponding CacheNodes can be updated.
92
104
  export class ImageNode {
@@ -174,6 +186,7 @@ export class ImageNode {
174
186
  nodeForChildren.children = this.children;
175
187
  this.children = [nodeForNewLeaf, nodeForChildren];
176
188
  nodeForChildren.recomputeBBox(true);
189
+ nodeForChildren.updateParents();
177
190
  return nodeForNewLeaf.addLeaf(leaf);
178
191
  }
179
192
  const containingNodes = this.children.filter(child => child.getBBox().containsRect(leafBBox));
@@ -221,6 +234,14 @@ export class ImageNode {
221
234
  (_a = this.parent) === null || _a === void 0 ? void 0 : _a.recomputeBBox(true);
222
235
  }
223
236
  }
237
+ updateParents(recursive = false) {
238
+ for (const child of this.children) {
239
+ child.parent = this;
240
+ if (recursive) {
241
+ child.updateParents(recursive);
242
+ }
243
+ }
244
+ }
224
245
  rebalance() {
225
246
  // If the current node is its parent's only child,
226
247
  if (this.parent && this.parent.children.length === 1) {
@@ -238,6 +259,7 @@ export class ImageNode {
238
259
  else if (this.content === null) {
239
260
  // Remove this and transfer this' children to the parent.
240
261
  this.parent.children = this.children;
262
+ this.parent.updateParents();
241
263
  this.parent = null;
242
264
  }
243
265
  }
@@ -253,7 +275,7 @@ export class ImageNode {
253
275
  this.parent.children = this.parent.children.filter(node => {
254
276
  return node !== this;
255
277
  });
256
- console.assert(this.parent.children.length === oldChildCount - 1);
278
+ 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.`);
257
279
  this.parent.children.forEach(child => {
258
280
  child.rebalance();
259
281
  });
@@ -11,11 +11,19 @@ export declare class Viewport {
11
11
  private notifier;
12
12
  static ViewportTransform: {
13
13
  new (transform: Mat33): {
14
- readonly "__#678@#inverseTransform": Mat33;
14
+ readonly "__#679@#inverseTransform": Mat33;
15
15
  readonly transform: Mat33;
16
16
  apply(editor: Editor): void;
17
17
  unapply(editor: Editor): void;
18
18
  description(localizationTable: CommandLocalization): string;
19
+ onDrop(_editor: Editor): void;
20
+ };
21
+ union(a: Command, b: Command): Command;
22
+ readonly empty: {
23
+ description(_localizationTable: import("./localization").EditorLocalization): string;
24
+ apply(_editor: Editor): void;
25
+ unapply(_editor: Editor): void;
26
+ onDrop(_editor: Editor): void;
19
27
  };
20
28
  };
21
29
  private transform;
@@ -10,6 +10,7 @@ var __classPrivateFieldGet = (this && this.__classPrivateFieldGet) || function (
10
10
  return kind === "m" ? f : kind === "a" ? f.call(receiver) : f ? f.value : state.get(receiver);
11
11
  };
12
12
  var _inverseTransform, _a;
13
+ import Command from './commands/Command';
13
14
  import Mat33 from './geometry/Mat33';
14
15
  import Rect2 from './geometry/Rect2';
15
16
  import { Vec2 } from './geometry/Vec2';
@@ -129,8 +130,9 @@ export class Viewport {
129
130
  }
130
131
  }
131
132
  // Command that translates/scales the viewport.
132
- Viewport.ViewportTransform = (_a = class {
133
+ Viewport.ViewportTransform = (_a = class extends Command {
133
134
  constructor(transform) {
135
+ super();
134
136
  this.transform = transform;
135
137
  _inverseTransform.set(this, void 0);
136
138
  __classPrivateFieldSet(this, _inverseTransform, transform.inverse(), "f");
@@ -1,15 +1,16 @@
1
1
  import Editor from '../Editor';
2
2
  import { EditorLocalization } from '../localization';
3
- interface Command {
4
- apply(editor: Editor): void;
5
- unapply(editor: Editor): void;
6
- description(localizationTable: EditorLocalization): string;
7
- }
8
- declare namespace Command {
9
- const empty: {
3
+ export declare abstract class Command {
4
+ abstract apply(editor: Editor): void;
5
+ abstract unapply(editor: Editor): void;
6
+ onDrop(_editor: Editor): void;
7
+ abstract description(localizationTable: EditorLocalization): string;
8
+ static union(a: Command, b: Command): Command;
9
+ static readonly empty: {
10
+ description(_localizationTable: EditorLocalization): string;
10
11
  apply(_editor: Editor): void;
11
12
  unapply(_editor: Editor): void;
13
+ onDrop(_editor: Editor): void;
12
14
  };
13
- const union: (a: Command, b: Command) => Command;
14
15
  }
15
16
  export default Command;
@@ -1,20 +1,16 @@
1
- // eslint-disable-next-line no-redeclare
2
- var Command;
3
- (function (Command) {
4
- Command.empty = {
5
- apply(_editor) { },
6
- unapply(_editor) { },
7
- };
8
- Command.union = (a, b) => {
9
- return {
1
+ export class Command {
2
+ // Called when the command is being deleted
3
+ onDrop(_editor) { }
4
+ static union(a, b) {
5
+ return new class extends Command {
10
6
  apply(editor) {
11
7
  a.apply(editor);
12
8
  b.apply(editor);
13
- },
9
+ }
14
10
  unapply(editor) {
15
11
  b.unapply(editor);
16
12
  a.unapply(editor);
17
- },
13
+ }
18
14
  description(localizationTable) {
19
15
  const aDescription = a.description(localizationTable);
20
16
  const bDescription = b.description(localizationTable);
@@ -22,8 +18,13 @@ var Command;
22
18
  return aDescription;
23
19
  }
24
20
  return `${aDescription}, ${bDescription}`;
25
- },
21
+ }
26
22
  };
27
- };
28
- })(Command || (Command = {}));
23
+ }
24
+ }
25
+ Command.empty = new class extends Command {
26
+ description(_localizationTable) { return ''; }
27
+ apply(_editor) { }
28
+ unapply(_editor) { }
29
+ };
29
30
  export default Command;
@@ -0,0 +1,14 @@
1
+ import AbstractComponent from '../components/AbstractComponent';
2
+ import Editor from '../Editor';
3
+ import { EditorLocalization } from '../localization';
4
+ import SerializableCommand from './SerializableCommand';
5
+ export default class Duplicate extends SerializableCommand {
6
+ private toDuplicate;
7
+ private duplicates;
8
+ private reverse;
9
+ constructor(toDuplicate: AbstractComponent[]);
10
+ apply(editor: Editor): void;
11
+ unapply(editor: Editor): void;
12
+ description(localizationTable: EditorLocalization): string;
13
+ protected serializeToString(): string;
14
+ }
@@ -0,0 +1,34 @@
1
+ import describeComponentList from '../components/util/describeComponentList';
2
+ import Erase from './Erase';
3
+ import SerializableCommand from './SerializableCommand';
4
+ export default class Duplicate extends SerializableCommand {
5
+ constructor(toDuplicate) {
6
+ super('duplicate');
7
+ this.toDuplicate = toDuplicate;
8
+ this.duplicates = toDuplicate.map(elem => elem.clone());
9
+ this.reverse = new Erase(this.duplicates);
10
+ }
11
+ apply(editor) {
12
+ this.reverse.unapply(editor);
13
+ }
14
+ unapply(editor) {
15
+ this.reverse.apply(editor);
16
+ }
17
+ description(localizationTable) {
18
+ var _a;
19
+ if (this.duplicates.length === 0) {
20
+ return localizationTable.duplicatedNoElements;
21
+ }
22
+ return localizationTable.duplicateAction((_a = describeComponentList(localizationTable, this.duplicates)) !== null && _a !== void 0 ? _a : localizationTable.elements, this.duplicates.length);
23
+ }
24
+ serializeToString() {
25
+ return JSON.stringify(this.toDuplicate.map(elem => elem.getId()));
26
+ }
27
+ }
28
+ (() => {
29
+ SerializableCommand.register('duplicate', (data, editor) => {
30
+ const json = JSON.parse(data);
31
+ const elems = json.map((id) => editor.image.lookupElement(id));
32
+ return new Duplicate(elems);
33
+ });
34
+ })();
@@ -1,11 +1,14 @@
1
1
  import AbstractComponent from '../components/AbstractComponent';
2
2
  import Editor from '../Editor';
3
3
  import { EditorLocalization } from '../localization';
4
- import Command from './Command';
5
- export default class Erase implements Command {
4
+ import SerializableCommand from './SerializableCommand';
5
+ export default class Erase extends SerializableCommand {
6
6
  private toRemove;
7
+ private applied;
7
8
  constructor(toRemove: AbstractComponent[]);
8
9
  apply(editor: Editor): void;
9
10
  unapply(editor: Editor): void;
11
+ onDrop(editor: Editor): void;
10
12
  description(localizationTable: EditorLocalization): string;
13
+ protected serializeToString(): string;
11
14
  }
@@ -1,8 +1,12 @@
1
+ import describeComponentList from '../components/util/describeComponentList';
1
2
  import EditorImage from '../EditorImage';
2
- export default class Erase {
3
+ import SerializableCommand from './SerializableCommand';
4
+ export default class Erase extends SerializableCommand {
3
5
  constructor(toRemove) {
6
+ super('erase');
4
7
  // Clone the list
5
8
  this.toRemove = toRemove.map(elem => elem);
9
+ this.applied = false;
6
10
  }
7
11
  apply(editor) {
8
12
  for (const part of this.toRemove) {
@@ -11,27 +15,42 @@ export default class Erase {
11
15
  parent.remove();
12
16
  }
13
17
  }
18
+ this.applied = true;
14
19
  editor.queueRerender();
15
20
  }
16
21
  unapply(editor) {
17
22
  for (const part of this.toRemove) {
18
23
  if (!editor.image.findParent(part)) {
19
- new EditorImage.AddElementCommand(part).apply(editor);
24
+ EditorImage.addElement(part).apply(editor);
20
25
  }
21
26
  }
27
+ this.applied = false;
22
28
  editor.queueRerender();
23
29
  }
30
+ onDrop(editor) {
31
+ if (this.applied) {
32
+ for (const part of this.toRemove) {
33
+ editor.image.onDestroyElement(part);
34
+ }
35
+ }
36
+ }
24
37
  description(localizationTable) {
38
+ var _a;
25
39
  if (this.toRemove.length === 0) {
26
40
  return localizationTable.erasedNoElements;
27
41
  }
28
- let description = this.toRemove[0].description(localizationTable);
29
- for (const elem of this.toRemove) {
30
- if (elem.description(localizationTable) !== description) {
31
- description = localizationTable.elements;
32
- break;
33
- }
34
- }
42
+ const description = (_a = describeComponentList(localizationTable, this.toRemove)) !== null && _a !== void 0 ? _a : localizationTable.elements;
35
43
  return localizationTable.eraseAction(description, this.toRemove.length);
36
44
  }
45
+ serializeToString() {
46
+ const elemIds = this.toRemove.map(elem => elem.getId());
47
+ return JSON.stringify(elemIds);
48
+ }
37
49
  }
50
+ (() => {
51
+ SerializableCommand.register('erase', (data, editor) => {
52
+ const json = JSON.parse(data);
53
+ const elems = json.map((elemId) => editor.image.lookupElement(elemId));
54
+ return new Erase(elems);
55
+ });
56
+ })();
@@ -0,0 +1,13 @@
1
+ import Editor from '../Editor';
2
+ import Command from './Command';
3
+ declare type DeserializationCallback = (data: string, editor: Editor) => SerializableCommand;
4
+ export default abstract class SerializableCommand extends Command {
5
+ private commandTypeId;
6
+ constructor(commandTypeId: string);
7
+ protected abstract serializeToString(): string;
8
+ private static deserializationCallbacks;
9
+ serialize(): string;
10
+ static deserialize(data: string, editor: Editor): SerializableCommand;
11
+ static register(commandTypeId: string, deserialize: DeserializationCallback): void;
12
+ }
13
+ export {};
@@ -0,0 +1,28 @@
1
+ import Command from './Command';
2
+ export default class SerializableCommand extends Command {
3
+ constructor(commandTypeId) {
4
+ super();
5
+ this.commandTypeId = commandTypeId;
6
+ if (!(commandTypeId in SerializableCommand.deserializationCallbacks)) {
7
+ throw new Error(`Command ${commandTypeId} must have a registered deserialization callback. To do this, call SerializableCommand.register.`);
8
+ }
9
+ }
10
+ serialize() {
11
+ return JSON.stringify({
12
+ data: this.serializeToString(),
13
+ commandType: this.commandTypeId,
14
+ });
15
+ }
16
+ static deserialize(data, editor) {
17
+ const json = JSON.parse(data);
18
+ const commandType = json.commandType;
19
+ if (!(commandType in SerializableCommand.deserializationCallbacks)) {
20
+ throw new Error(`Unrecognised command type ${commandType}!`);
21
+ }
22
+ return SerializableCommand.deserializationCallbacks[commandType](json.data, editor);
23
+ }
24
+ static register(commandTypeId, deserialize) {
25
+ SerializableCommand.deserializationCallbacks[commandTypeId] = deserialize;
26
+ }
27
+ }
28
+ SerializableCommand.deserializationCallbacks = {};
@@ -8,12 +8,14 @@ export interface CommandLocalization {
8
8
  zoomedOut: string;
9
9
  zoomedIn: string;
10
10
  erasedNoElements: string;
11
+ duplicatedNoElements: string;
11
12
  elements: string;
12
13
  updatedViewport: string;
13
14
  transformedElements: (elemCount: number) => string;
14
15
  resizeOutputCommand: (newSize: Rect2) => string;
15
16
  addElementAction: (elemDescription: string) => string;
16
17
  eraseAction: (elemDescription: string, numElems: number) => string;
18
+ duplicateAction: (elemDescription: string, count: number) => string;
17
19
  selectedElements: (count: number) => string;
18
20
  }
19
21
  export declare const defaultCommandLocalization: CommandLocalization;
@@ -4,8 +4,10 @@ export const defaultCommandLocalization = {
4
4
  resizeOutputCommand: (newSize) => `Resized image to ${newSize.w}x${newSize.h}`,
5
5
  addElementAction: (componentDescription) => `Added ${componentDescription}`,
6
6
  eraseAction: (componentDescription, numElems) => `Erased ${numElems} ${componentDescription}`,
7
+ duplicateAction: (componentDescription, numElems) => `Duplicated ${numElems} ${componentDescription}`,
7
8
  elements: 'Elements',
8
9
  erasedNoElements: 'Erased nothing',
10
+ duplicatedNoElements: 'Duplicated nothing',
9
11
  rotatedBy: (degrees) => `Rotated by ${Math.abs(degrees)} degrees ${degrees < 0 ? 'clockwise' : 'counter-clockwise'}`,
10
12
  movedLeft: 'Moved left',
11
13
  movedUp: 'Moved up',