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
@@ -1,11 +1,12 @@
1
1
  export interface ToolLocalization {
2
- rightClickDragPanTool: string;
3
2
  penTool: (penId: number) => string;
4
3
  selectionTool: string;
5
4
  eraserTool: string;
6
5
  touchPanTool: string;
7
6
  twoFingerPanZoomTool: string;
8
7
  undoRedoTool: string;
8
+ pipetteTool: string;
9
+ rightClickDragPanTool: string;
9
10
  textTool: string;
10
11
  enterTextToInsert: string;
11
12
  toolEnabledAnnouncement: (toolName: string) => string;
@@ -2,10 +2,11 @@ export const defaultToolLocalization = {
2
2
  penTool: (penId) => `Pen ${penId}`,
3
3
  selectionTool: 'Selection',
4
4
  eraserTool: 'Eraser',
5
- touchPanTool: 'Touch Panning',
6
- twoFingerPanZoomTool: 'Panning and Zooming',
5
+ touchPanTool: 'Touch panning',
6
+ twoFingerPanZoomTool: 'Panning and zooming',
7
7
  undoRedoTool: 'Undo/Redo',
8
8
  rightClickDragPanTool: 'Right-click drag',
9
+ pipetteTool: 'Pick color from screen',
9
10
  textTool: 'Text',
10
11
  enterTextToInsert: 'Text to insert',
11
12
  toolEnabledAnnouncement: (toolName) => `${toolName} enabled`,
@@ -58,7 +58,8 @@ export declare enum EditorEventType {
58
58
  ObjectAdded = 4,
59
59
  ViewportChanged = 5,
60
60
  DisplayResized = 6,
61
- ColorPickerToggled = 7
61
+ ColorPickerToggled = 7,
62
+ ColorPickerColorSelected = 8
62
63
  }
63
64
  declare type EditorToolEventType = EditorEventType.ToolEnabled | EditorEventType.ToolDisabled | EditorEventType.ToolUpdated;
64
65
  export interface EditorToolEvent {
@@ -86,7 +87,11 @@ export interface ColorPickerToggled {
86
87
  readonly kind: EditorEventType.ColorPickerToggled;
87
88
  readonly open: boolean;
88
89
  }
89
- export declare type EditorEventDataType = EditorToolEvent | EditorObjectEvent | EditorViewportChangedEvent | DisplayResizedEvent | EditorUndoStackUpdated | ColorPickerToggled;
90
+ export interface ColorPickerColorSelected {
91
+ readonly kind: EditorEventType.ColorPickerColorSelected;
92
+ readonly color: Color4;
93
+ }
94
+ export declare type EditorEventDataType = EditorToolEvent | EditorObjectEvent | EditorViewportChangedEvent | DisplayResizedEvent | EditorUndoStackUpdated | ColorPickerToggled | ColorPickerColorSelected;
90
95
  export declare type OnProgressListener = (amountProcessed: number, totalToProcess: number) => Promise<void> | null;
91
96
  export declare type ComponentAddedListener = (component: AbstractComponent) => void;
92
97
  export declare type OnDetermineExportRectListener = (exportRect: Rect2) => void;
package/dist/src/types.js CHANGED
@@ -18,4 +18,5 @@ export var EditorEventType;
18
18
  EditorEventType[EditorEventType["ViewportChanged"] = 5] = "ViewportChanged";
19
19
  EditorEventType[EditorEventType["DisplayResized"] = 6] = "DisplayResized";
20
20
  EditorEventType[EditorEventType["ColorPickerToggled"] = 7] = "ColorPickerToggled";
21
+ EditorEventType[EditorEventType["ColorPickerColorSelected"] = 8] = "ColorPickerColorSelected";
21
22
  })(EditorEventType || (EditorEventType = {}));
package/jest.config.js CHANGED
@@ -17,6 +17,8 @@ const config = {
17
17
  '\\.(css|lessc)': '<rootDir>/__mocks__/styleMock.js',
18
18
  '@melloware/coloris': '<rootDir>/__mocks__/coloris.ts',
19
19
  },
20
+
21
+ testEnvironment: 'jsdom',
20
22
  };
21
23
 
22
24
  module.exports = config;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "js-draw",
3
- "version": "0.1.6",
3
+ "version": "0.1.9",
4
4
  "description": "Draw pictures using a pen, touchscreen, or mouse! JS-draw is a drawing library for JavaScript and TypeScript. ",
5
5
  "main": "dist/src/Editor.js",
6
6
  "types": "dist/src/Editor.d.ts",
@@ -59,14 +59,14 @@
59
59
  "@types/bezier-js": "^4.1.0",
60
60
  "@types/jest": "^28.1.7",
61
61
  "@types/jsdom": "^20.0.0",
62
- "@types/node": "^18.7.9",
63
- "@typescript-eslint/eslint-plugin": "^5.33.1",
64
- "@typescript-eslint/parser": "^5.33.1",
62
+ "@types/node": "^18.7.15",
63
+ "@typescript-eslint/eslint-plugin": "^5.36.2",
64
+ "@typescript-eslint/parser": "^5.36.2",
65
65
  "css-loader": "^6.7.1",
66
- "eslint": "^8.22.0",
66
+ "eslint": "^8.23.0",
67
67
  "husky": "^8.0.1",
68
68
  "jest": "^28.1.3",
69
- "jest-environment-jsdom": "^28.1.3",
69
+ "jest-environment-jsdom": "^29.0.2",
70
70
  "jsdom": "^20.0.0",
71
71
  "lint-staged": "^13.0.3",
72
72
  "pinst": "^3.0.0",
package/src/Color4.ts CHANGED
@@ -5,14 +5,20 @@ export default class Color4 {
5
5
  public readonly g: number,
6
6
  public readonly b: number,
7
7
  public readonly a: number
8
- ) { }
8
+ ) {
9
+ }
9
10
 
10
11
  // Each component should be in the range [0, 1]
11
12
  public static ofRGB(red: number, green: number, blue: number): Color4 {
12
- return new Color4(red, green, blue, 1.0);
13
+ return Color4.ofRGBA(red, green, blue, 1.0);
13
14
  }
14
15
 
15
16
  public static ofRGBA(red: number, green: number, blue: number, alpha: number): Color4 {
17
+ red = Math.max(0, Math.min(red, 1));
18
+ green = Math.max(0, Math.min(green, 1));
19
+ blue = Math.max(0, Math.min(blue, 1));
20
+ alpha = Math.max(0, Math.min(alpha, 1));
21
+
16
22
  return new Color4(red, green, blue, alpha);
17
23
  }
18
24
 
@@ -49,7 +55,7 @@ export default class Color4 {
49
55
  throw new Error(`Unable to parse ${hexString}: Wrong number of components.`);
50
56
  }
51
57
 
52
- return new Color4(components[0], components[1], components[2], components[3]);
58
+ return Color4.ofRGBA(components[0], components[1], components[2], components[3]);
53
59
  }
54
60
 
55
61
  // Like fromHex, but can handle additional colors if an HTML5Canvas is available.
package/src/Editor.ts CHANGED
@@ -108,7 +108,7 @@ export class Editor {
108
108
  );
109
109
 
110
110
  this.registerListeners();
111
- this.rerender();
111
+ this.queueRerender();
112
112
  this.hideLoadingWarning();
113
113
  }
114
114
 
@@ -307,6 +307,7 @@ export class Editor {
307
307
  });
308
308
  }
309
309
 
310
+ // Adds to history by default
310
311
  public dispatch(command: Command, addToHistory: boolean = true) {
311
312
  if (addToHistory) {
312
313
  // .push applies [command] to this
@@ -318,6 +319,15 @@ export class Editor {
318
319
  this.announceForAccessibility(command.description(this.localization));
319
320
  }
320
321
 
322
+ // Dispatches a command without announcing it. By default, does not add to history.
323
+ public dispatchNoAnnounce(command: Command, addToHistory: boolean = false) {
324
+ if (addToHistory) {
325
+ this.history.push(command);
326
+ } else {
327
+ command.apply(this);
328
+ }
329
+ }
330
+
321
331
  // Apply a large transformation in chunks.
322
332
  // If [apply] is false, the commands are unapplied.
323
333
  // Triggers a re-render after each [updateChunkSize]-sized group of commands
@@ -381,6 +391,11 @@ export class Editor {
381
391
  public rerender(showImageBounds: boolean = true) {
382
392
  this.display.startRerender();
383
393
 
394
+ // Don't render if the display has zero size.
395
+ if (this.display.width === 0 || this.display.height === 0) {
396
+ return;
397
+ }
398
+
384
399
  // Draw a rectangle around the region that will be visible on save
385
400
  const renderer = this.display.getDryInkRenderer();
386
401
 
@@ -486,7 +501,7 @@ export class Editor {
486
501
  this.display.setDraftMode(true);
487
502
 
488
503
  await loader.start((component) => {
489
- (new EditorImage.AddElementCommand(component)).apply(this);
504
+ this.dispatchNoAnnounce(EditorImage.addElement(component));
490
505
  }, (countProcessed: number, totalToProcess: number) => {
491
506
  if (countProcessed % 500 === 0) {
492
507
  this.showLoadingWarning(countProcessed / totalToProcess);
@@ -498,8 +513,8 @@ export class Editor {
498
513
 
499
514
  return null;
500
515
  }, (importExportRect: Rect2) => {
501
- this.setImportExportRect(importExportRect).apply(this);
502
- this.viewport.zoomTo(importExportRect).apply(this);
516
+ this.dispatchNoAnnounce(this.setImportExportRect(importExportRect), false);
517
+ this.dispatchNoAnnounce(this.viewport.zoomTo(importExportRect), false);
503
518
  });
504
519
  this.hideLoadingWarning();
505
520
 
@@ -517,22 +532,24 @@ export class Editor {
517
532
  const origSize = this.importExportViewport.visibleRect.size;
518
533
  const origTransform = this.importExportViewport.canvasToScreenTransform;
519
534
 
520
- return {
521
- apply(editor) {
535
+ return new class extends Command {
536
+ public apply(editor: Editor) {
522
537
  const viewport = editor.importExportViewport;
523
538
  viewport.updateScreenSize(imageRect.size);
524
539
  viewport.resetTransform(Mat33.translation(imageRect.topLeft.times(-1)));
525
540
  editor.queueRerender();
526
- },
527
- unapply(editor) {
541
+ }
542
+
543
+ public unapply(editor: Editor) {
528
544
  const viewport = editor.importExportViewport;
529
545
  viewport.updateScreenSize(origSize);
530
546
  viewport.resetTransform(origTransform);
531
547
  editor.queueRerender();
532
- },
533
- description(localizationTable) {
548
+ }
549
+
550
+ public description(localizationTable: EditorLocalization) {
534
551
  return localizationTable.resizeOutputCommand(imageRect);
535
- },
552
+ }
536
553
  };
537
554
  }
538
555
 
@@ -6,8 +6,8 @@ import { Vec2 } from './geometry/Vec2';
6
6
  import Path, { PathCommandType } from './geometry/Path';
7
7
  import Color4 from './Color4';
8
8
  import DummyRenderer from './rendering/renderers/DummyRenderer';
9
- import { RenderingStyle } from './rendering/renderers/AbstractRenderer';
10
9
  import createEditor from './testing/createEditor';
10
+ import RenderingStyle from './rendering/RenderingStyle';
11
11
 
12
12
  describe('EditorImage', () => {
13
13
  const testStroke = new Stroke([
@@ -25,7 +25,7 @@ describe('EditorImage', () => {
25
25
  },
26
26
  ]);
27
27
  const testFill: RenderingStyle = { fill: Color4.black };
28
- const addTestStrokeCommand = new EditorImage.AddElementCommand(testStroke);
28
+ const addTestStrokeCommand = EditorImage.addElement(testStroke);
29
29
 
30
30
  it('elements added to the image should be findable', () => {
31
31
  const editor = createEditor();
@@ -48,7 +48,7 @@ describe('EditorImage', () => {
48
48
  expect(renderer.objectNestingLevel).toBe(0);
49
49
  editor.dispatch(addTestStrokeCommand);
50
50
  editor.rerender();
51
- expect(renderer.renderedPathCount - emptyDocumentPathCount).toBe(1);
51
+ expect(renderer.renderedPathCount - emptyDocumentPathCount).toBeGreaterThanOrEqual(1);
52
52
 
53
53
  // Should not be within objects after finished rendering
54
54
  expect(renderer.objectNestingLevel).toBe(0);
@@ -69,7 +69,7 @@ describe('EditorImage', () => {
69
69
 
70
70
  expect(!leftmostStroke.getBBox().intersects(rightmostStroke.getBBox()));
71
71
 
72
- (new EditorImage.AddElementCommand(leftmostStroke)).apply(editor);
72
+ (EditorImage.addElement(leftmostStroke)).apply(editor);
73
73
 
74
74
  // The first node should be at the image's root.
75
75
  let firstParent = image.findParent(leftmostStroke);
@@ -77,7 +77,7 @@ describe('EditorImage', () => {
77
77
  expect(firstParent?.getParent()).toBe(null);
78
78
  expect(firstParent?.getBBox()?.corners).toMatchObject(leftmostStroke.getBBox()?.corners);
79
79
 
80
- (new EditorImage.AddElementCommand(rightmostStroke)).apply(editor);
80
+ (EditorImage.addElement(rightmostStroke)).apply(editor);
81
81
 
82
82
  firstParent = image.findParent(leftmostStroke);
83
83
  const secondParent = image.findParent(rightmostStroke);
@@ -6,6 +6,7 @@ import AbstractComponent from './components/AbstractComponent';
6
6
  import Rect2 from './geometry/Rect2';
7
7
  import { EditorLocalization } from './localization';
8
8
  import RenderingCache from './rendering/caching/RenderingCache';
9
+ import SerializableCommand from './commands/SerializableCommand';
9
10
 
10
11
  export const sortLeavesByZIndex = (leaves: Array<ImageNode>) => {
11
12
  leaves.sort((a, b) => a.getContent()!.getZIndex() - b.getContent()!.getZIndex());
@@ -14,13 +15,11 @@ export const sortLeavesByZIndex = (leaves: Array<ImageNode>) => {
14
15
  // Handles lookup/storage of elements in the image
15
16
  export default class EditorImage {
16
17
  private root: ImageNode;
18
+ private componentsById: Record<string, AbstractComponent>;
17
19
 
18
20
  public constructor() {
19
21
  this.root = new ImageNode();
20
- }
21
-
22
- private addElement(elem: AbstractComponent): ImageNode {
23
- return this.root.addLeaf(elem);
22
+ this.componentsById = {};
24
23
  }
25
24
 
26
25
  // Returns the parent of the given element, if it exists.
@@ -59,50 +58,76 @@ export default class EditorImage {
59
58
  return leaves.map(leaf => leaf.getContent()!);
60
59
  }
61
60
 
62
- // A Command that can access private [EditorImage] functionality
63
- public static AddElementCommand = class implements Command {
64
- readonly #element: AbstractComponent;
65
- #applyByFlattening: boolean = false;
61
+ public onDestroyElement(elem: AbstractComponent) {
62
+ delete this.componentsById[elem.getId()];
63
+ }
64
+
65
+ public lookupElement(id: string): AbstractComponent|null {
66
+ return this.componentsById[id] ?? null;
67
+ }
66
68
 
69
+ private addElementDirectly(elem: AbstractComponent): ImageNode {
70
+ this.componentsById[elem.getId()] = elem;
71
+ return this.root.addLeaf(elem);
72
+ }
73
+
74
+ public static addElement(elem: AbstractComponent, applyByFlattening: boolean = false): Command {
75
+ return new EditorImage.AddElementCommand(elem, applyByFlattening);
76
+ }
77
+
78
+ // A Command that can access private [EditorImage] functionality
79
+ private static AddElementCommand = class extends SerializableCommand {
67
80
  // If [applyByFlattening], then the rendered content of this element
68
81
  // is present on the display's wet ink canvas. As such, no re-render is necessary
69
82
  // the first time this command is applied (the surfaces are joined instead).
70
83
  public constructor(
71
- element: AbstractComponent,
72
- applyByFlattening: boolean = false
84
+ private element: AbstractComponent,
85
+ private applyByFlattening: boolean = false
73
86
  ) {
74
- this.#element = element;
75
- this.#applyByFlattening = applyByFlattening;
87
+ super('add-element');
76
88
 
77
- if (isNaN(this.#element.getBBox().area)) {
89
+ if (isNaN(element.getBBox().area)) {
78
90
  throw new Error('Elements in the image cannot have NaN bounding boxes');
79
91
  }
80
92
  }
81
93
 
82
94
  public apply(editor: Editor) {
83
- editor.image.addElement(this.#element);
95
+ editor.image.addElementDirectly(this.element);
84
96
 
85
- if (!this.#applyByFlattening) {
97
+ if (!this.applyByFlattening) {
86
98
  editor.queueRerender();
87
99
  } else {
88
- this.#applyByFlattening = false;
100
+ this.applyByFlattening = false;
89
101
  editor.display.flatten();
90
102
  }
91
103
  }
92
104
 
93
105
  public unapply(editor: Editor) {
94
- const container = editor.image.findParent(this.#element);
106
+ const container = editor.image.findParent(this.element);
95
107
  container?.remove();
96
108
  editor.queueRerender();
97
109
  }
98
110
 
99
111
  public description(localization: EditorLocalization) {
100
- return localization.addElementAction(this.#element.description(localization));
112
+ return localization.addElementAction(this.element.description(localization));
113
+ }
114
+
115
+ protected serializeToString() {
116
+ return JSON.stringify({
117
+ elemData: this.element.serialize(),
118
+ });
119
+ }
120
+
121
+ static {
122
+ SerializableCommand.register('add-element', (data: string, _editor: Editor) => {
123
+ const json = JSON.parse(data);
124
+ const elem = AbstractComponent.deserialize(json.elemData);
125
+ return new EditorImage.AddElementCommand(elem);
126
+ });
101
127
  }
102
128
  };
103
129
  }
104
130
 
105
- export type AddElementCommand = typeof EditorImage.AddElementCommand.prototype;
106
131
  type TooSmallToRenderCheck = (rect: Rect2)=> boolean;
107
132
 
108
133
  // TODO: Assign leaf nodes to CacheNodes. When leaf nodes are modified, the corresponding CacheNodes can be updated.
@@ -221,6 +246,7 @@ export class ImageNode {
221
246
  nodeForChildren.children = this.children;
222
247
  this.children = [nodeForNewLeaf, nodeForChildren];
223
248
  nodeForChildren.recomputeBBox(true);
249
+ nodeForChildren.updateParents();
224
250
 
225
251
  return nodeForNewLeaf.addLeaf(leaf);
226
252
  }
@@ -279,6 +305,16 @@ export class ImageNode {
279
305
  }
280
306
  }
281
307
 
308
+ private updateParents(recursive: boolean = false) {
309
+ for (const child of this.children) {
310
+ child.parent = this;
311
+
312
+ if (recursive) {
313
+ child.updateParents(recursive);
314
+ }
315
+ }
316
+ }
317
+
282
318
  private rebalance() {
283
319
  // If the current node is its parent's only child,
284
320
  if (this.parent && this.parent.children.length === 1) {
@@ -296,6 +332,7 @@ export class ImageNode {
296
332
  } else if (this.content === null) {
297
333
  // Remove this and transfer this' children to the parent.
298
334
  this.parent.children = this.children;
335
+ this.parent.updateParents();
299
336
  this.parent = null;
300
337
  }
301
338
  }
@@ -314,7 +351,11 @@ export class ImageNode {
314
351
  this.parent.children = this.parent.children.filter(node => {
315
352
  return node !== this;
316
353
  });
317
- console.assert(this.parent.children.length === oldChildCount - 1);
354
+
355
+ console.assert(
356
+ this.parent.children.length === oldChildCount - 1,
357
+ `${oldChildCount - 1} ≠ ${this.parent.children.length} after removing all nodes equal to ${this}. Nodes should only be removed once.`
358
+ );
318
359
 
319
360
  this.parent.children.forEach(child => {
320
361
  child.rebalance();
package/src/SVGLoader.ts CHANGED
@@ -8,7 +8,8 @@ import Mat33 from './geometry/Mat33';
8
8
  import Path from './geometry/Path';
9
9
  import Rect2 from './geometry/Rect2';
10
10
  import { Vec2 } from './geometry/Vec2';
11
- import { RenderablePathSpec, RenderingStyle } from './rendering/renderers/AbstractRenderer';
11
+ import { RenderablePathSpec } from './rendering/renderers/AbstractRenderer';
12
+ import RenderingStyle from './rendering/RenderingStyle';
12
13
  import { ComponentAddedListener, ImageLoader, OnDetermineExportRectListener, OnProgressListener } from './types';
13
14
 
14
15
  type OnFinishListener = ()=> void;
package/src/Viewport.ts CHANGED
@@ -13,10 +13,11 @@ type PointDataType<T extends Point2|StrokeDataPoint|number> = T extends Point2 ?
13
13
 
14
14
  export class Viewport {
15
15
  // Command that translates/scales the viewport.
16
- public static ViewportTransform = class implements Command {
16
+ public static ViewportTransform = class extends Command {
17
17
  readonly #inverseTransform: Mat33;
18
18
 
19
19
  public constructor(public readonly transform: Mat33) {
20
+ super();
20
21
  this.#inverseTransform = transform.inverse();
21
22
  }
22
23
 
@@ -1,32 +1,28 @@
1
1
  import Editor from '../Editor';
2
2
  import { EditorLocalization } from '../localization';
3
3
 
4
- interface Command {
5
- apply(editor: Editor): void;
6
- unapply(editor: Editor): void;
4
+ export abstract class Command {
5
+ public abstract apply(editor: Editor): void;
6
+ public abstract unapply(editor: Editor): void;
7
7
 
8
- description(localizationTable: EditorLocalization): string;
9
- }
8
+ // Called when the command is being deleted
9
+ public onDrop(_editor: Editor) { }
10
10
 
11
- // eslint-disable-next-line no-redeclare
12
- namespace Command {
13
- export const empty = {
14
- apply(_editor: Editor) { },
15
- unapply(_editor: Editor) { },
16
- };
11
+ public abstract description(localizationTable: EditorLocalization): string;
17
12
 
18
- export const union = (a: Command, b: Command): Command => {
19
- return {
20
- apply(editor: Editor) {
13
+ public static union(a: Command, b: Command): Command {
14
+ return new class extends Command {
15
+ public apply(editor: Editor) {
21
16
  a.apply(editor);
22
17
  b.apply(editor);
23
- },
24
- unapply(editor: Editor) {
18
+ }
19
+
20
+ public unapply(editor: Editor) {
25
21
  b.unapply(editor);
26
22
  a.unapply(editor);
27
- },
23
+ }
28
24
 
29
- description(localizationTable: EditorLocalization) {
25
+ public description(localizationTable: EditorLocalization) {
30
26
  const aDescription = a.description(localizationTable);
31
27
  const bDescription = b.description(localizationTable);
32
28
 
@@ -35,8 +31,14 @@ namespace Command {
35
31
  }
36
32
 
37
33
  return `${aDescription}, ${bDescription}`;
38
- },
34
+ }
39
35
  };
36
+ }
37
+
38
+ public static readonly empty = new class extends Command {
39
+ public description(_localizationTable: EditorLocalization) { return ''; }
40
+ public apply(_editor: Editor) { }
41
+ public unapply(_editor: Editor) { }
40
42
  };
41
43
  }
42
44
 
@@ -0,0 +1,49 @@
1
+ import AbstractComponent from '../components/AbstractComponent';
2
+ import describeComponentList from '../components/util/describeComponentList';
3
+ import Editor from '../Editor';
4
+ import { EditorLocalization } from '../localization';
5
+ import Erase from './Erase';
6
+ import SerializableCommand from './SerializableCommand';
7
+
8
+ export default class Duplicate extends SerializableCommand {
9
+ private duplicates: AbstractComponent[];
10
+ private reverse: Erase;
11
+
12
+ public constructor(private toDuplicate: AbstractComponent[]) {
13
+ super('duplicate');
14
+
15
+ this.duplicates = toDuplicate.map(elem => elem.clone());
16
+ this.reverse = new Erase(this.duplicates);
17
+ }
18
+
19
+ public apply(editor: Editor): void {
20
+ this.reverse.unapply(editor);
21
+ }
22
+
23
+ public unapply(editor: Editor): void {
24
+ this.reverse.apply(editor);
25
+ }
26
+
27
+ public description(localizationTable: EditorLocalization): string {
28
+ if (this.duplicates.length === 0) {
29
+ return localizationTable.duplicatedNoElements;
30
+ }
31
+
32
+ return localizationTable.duplicateAction(
33
+ describeComponentList(localizationTable, this.duplicates) ?? localizationTable.elements,
34
+ this.duplicates.length
35
+ );
36
+ }
37
+
38
+ protected serializeToString(): string {
39
+ return JSON.stringify(this.toDuplicate.map(elem => elem.getId()));
40
+ }
41
+
42
+ static {
43
+ SerializableCommand.register('duplicate', (data: string, editor: Editor) => {
44
+ const json = JSON.parse(data);
45
+ const elems = json.map((id: string) => editor.image.lookupElement(id));
46
+ return new Duplicate(elems);
47
+ });
48
+ }
49
+ }
@@ -1,18 +1,23 @@
1
1
  import AbstractComponent from '../components/AbstractComponent';
2
+ import describeComponentList from '../components/util/describeComponentList';
2
3
  import Editor from '../Editor';
3
4
  import EditorImage from '../EditorImage';
4
5
  import { EditorLocalization } from '../localization';
5
- import Command from './Command';
6
+ import SerializableCommand from './SerializableCommand';
6
7
 
7
- export default class Erase implements Command {
8
+ export default class Erase extends SerializableCommand {
8
9
  private toRemove: AbstractComponent[];
10
+ private applied: boolean;
9
11
 
10
12
  public constructor(toRemove: AbstractComponent[]) {
13
+ super('erase');
14
+
11
15
  // Clone the list
12
16
  this.toRemove = toRemove.map(elem => elem);
17
+ this.applied = false;
13
18
  }
14
19
 
15
- public apply(editor: Editor): void {
20
+ public apply(editor: Editor) {
16
21
  for (const part of this.toRemove) {
17
22
  const parent = editor.image.findParent(part);
18
23
 
@@ -21,32 +26,48 @@ export default class Erase implements Command {
21
26
  }
22
27
  }
23
28
 
29
+ this.applied = true;
24
30
  editor.queueRerender();
25
31
  }
26
32
 
27
- public unapply(editor: Editor): void {
33
+ public unapply(editor: Editor) {
28
34
  for (const part of this.toRemove) {
29
35
  if (!editor.image.findParent(part)) {
30
- new EditorImage.AddElementCommand(part).apply(editor);
36
+ EditorImage.addElement(part).apply(editor);
31
37
  }
32
38
  }
33
39
 
40
+ this.applied = false;
34
41
  editor.queueRerender();
35
42
  }
36
43
 
44
+ public onDrop(editor: Editor) {
45
+ if (this.applied) {
46
+ for (const part of this.toRemove) {
47
+ editor.image.onDestroyElement(part);
48
+ }
49
+ }
50
+ }
51
+
37
52
  public description(localizationTable: EditorLocalization): string {
38
53
  if (this.toRemove.length === 0) {
39
54
  return localizationTable.erasedNoElements;
40
55
  }
41
56
 
42
- let description = this.toRemove[0].description(localizationTable);
43
- for (const elem of this.toRemove) {
44
- if (elem.description(localizationTable) !== description) {
45
- description = localizationTable.elements;
46
- break;
47
- }
48
- }
49
-
57
+ const description = describeComponentList(localizationTable, this.toRemove) ?? localizationTable.elements;
50
58
  return localizationTable.eraseAction(description, this.toRemove.length);
51
59
  }
60
+
61
+ protected serializeToString() {
62
+ const elemIds = this.toRemove.map(elem => elem.getId());
63
+ return JSON.stringify(elemIds);
64
+ }
65
+
66
+ static {
67
+ SerializableCommand.register('erase', (data: string, editor: Editor) => {
68
+ const json = JSON.parse(data);
69
+ const elems = json.map((elemId: string) => editor.image.lookupElement(elemId));
70
+ return new Erase(elems);
71
+ });
72
+ }
52
73
  }