js-draw 0.1.11 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (220) hide show
  1. package/.eslintrc.js +1 -0
  2. package/.firebaserc +5 -0
  3. package/.github/workflows/firebase-hosting-merge.yml +25 -0
  4. package/.github/workflows/firebase-hosting-pull-request.yml +22 -0
  5. package/.github/workflows/github-pages.yml +52 -0
  6. package/CHANGELOG.md +13 -0
  7. package/README.md +11 -6
  8. package/dist/bundle.js +1 -1
  9. package/dist/src/Color4.d.ts +19 -0
  10. package/dist/src/Color4.js +24 -3
  11. package/dist/src/Editor.d.ts +133 -4
  12. package/dist/src/Editor.js +124 -27
  13. package/dist/src/EditorImage.d.ts +8 -3
  14. package/dist/src/EditorImage.js +42 -26
  15. package/dist/src/EventDispatcher.d.ts +18 -0
  16. package/dist/src/EventDispatcher.js +19 -4
  17. package/dist/src/Pointer.d.ts +1 -1
  18. package/dist/src/Pointer.js +4 -3
  19. package/dist/src/SVGLoader.d.ts +1 -1
  20. package/dist/src/SVGLoader.js +14 -6
  21. package/dist/src/UndoRedoHistory.js +15 -2
  22. package/dist/src/Viewport.d.ts +8 -25
  23. package/dist/src/Viewport.js +18 -10
  24. package/dist/src/bundle/bundled.d.ts +1 -2
  25. package/dist/src/bundle/bundled.js +1 -2
  26. package/dist/src/commands/Command.d.ts +2 -2
  27. package/dist/src/commands/Command.js +4 -4
  28. package/dist/src/commands/Duplicate.d.ts +2 -2
  29. package/dist/src/commands/Duplicate.js +4 -5
  30. package/dist/src/commands/Erase.d.ts +2 -2
  31. package/dist/src/commands/Erase.js +7 -6
  32. package/dist/src/commands/SerializableCommand.d.ts +4 -5
  33. package/dist/src/commands/SerializableCommand.js +12 -4
  34. package/dist/src/commands/invertCommand.d.ts +4 -0
  35. package/dist/src/commands/invertCommand.js +44 -0
  36. package/dist/src/commands/lib.d.ts +6 -0
  37. package/dist/src/commands/lib.js +6 -0
  38. package/dist/src/commands/localization.d.ts +2 -1
  39. package/dist/src/commands/localization.js +1 -0
  40. package/dist/src/components/AbstractComponent.d.ts +16 -11
  41. package/dist/src/components/AbstractComponent.js +28 -17
  42. package/dist/src/components/SVGGlobalAttributesObject.d.ts +4 -4
  43. package/dist/src/components/SVGGlobalAttributesObject.js +8 -2
  44. package/dist/src/components/Stroke.d.ts +16 -6
  45. package/dist/src/components/Stroke.js +12 -9
  46. package/dist/src/components/Text.d.ts +5 -5
  47. package/dist/src/components/Text.js +9 -9
  48. package/dist/src/components/UnknownSVGObject.d.ts +4 -4
  49. package/dist/src/components/UnknownSVGObject.js +7 -2
  50. package/dist/src/components/builders/ArrowBuilder.d.ts +1 -1
  51. package/dist/src/components/builders/ArrowBuilder.js +1 -1
  52. package/dist/src/components/builders/FreehandLineBuilder.d.ts +8 -3
  53. package/dist/src/components/builders/FreehandLineBuilder.js +142 -71
  54. package/dist/src/components/builders/LineBuilder.d.ts +1 -1
  55. package/dist/src/components/builders/LineBuilder.js +1 -1
  56. package/dist/src/components/builders/RectangleBuilder.d.ts +1 -1
  57. package/dist/src/components/builders/RectangleBuilder.js +3 -3
  58. package/dist/src/components/builders/types.d.ts +1 -1
  59. package/dist/src/components/lib.d.ts +4 -0
  60. package/dist/src/components/lib.js +4 -0
  61. package/dist/src/lib.d.ts +25 -0
  62. package/dist/src/lib.js +25 -0
  63. package/dist/src/localization.d.ts +1 -0
  64. package/dist/src/localization.js +5 -1
  65. package/dist/src/localizations/es.js +1 -1
  66. package/dist/src/{geometry → math}/LineSegment2.d.ts +0 -0
  67. package/dist/src/{geometry → math}/LineSegment2.js +0 -0
  68. package/dist/src/math/Mat33.d.ts +78 -0
  69. package/dist/src/{geometry → math}/Mat33.js +48 -20
  70. package/dist/src/{geometry → math}/Path.d.ts +2 -1
  71. package/dist/src/{geometry → math}/Path.js +59 -52
  72. package/dist/src/{geometry → math}/Rect2.d.ts +2 -2
  73. package/dist/src/{geometry → math}/Rect2.js +0 -0
  74. package/dist/src/{geometry → math}/Vec2.d.ts +0 -0
  75. package/dist/src/{geometry → math}/Vec2.js +0 -0
  76. package/dist/src/math/Vec3.d.ts +96 -0
  77. package/dist/src/{geometry → math}/Vec3.js +63 -15
  78. package/dist/src/math/lib.d.ts +7 -0
  79. package/dist/src/math/lib.js +7 -0
  80. package/dist/src/math/rounding.d.ts +3 -0
  81. package/dist/src/math/rounding.js +121 -0
  82. package/dist/src/rendering/Display.d.ts +47 -1
  83. package/dist/src/rendering/Display.js +60 -15
  84. package/dist/src/rendering/caching/CacheRecord.d.ts +3 -2
  85. package/dist/src/rendering/caching/CacheRecord.js +4 -1
  86. package/dist/src/rendering/caching/CacheRecordManager.d.ts +5 -4
  87. package/dist/src/rendering/caching/CacheRecordManager.js +16 -4
  88. package/dist/src/rendering/caching/RenderingCache.d.ts +2 -3
  89. package/dist/src/rendering/caching/RenderingCache.js +10 -11
  90. package/dist/src/rendering/caching/RenderingCacheNode.d.ts +2 -1
  91. package/dist/src/rendering/caching/RenderingCacheNode.js +18 -7
  92. package/dist/src/rendering/caching/testUtils.js +1 -1
  93. package/dist/src/rendering/caching/types.d.ts +2 -4
  94. package/dist/src/rendering/localization.d.ts +2 -0
  95. package/dist/src/rendering/localization.js +2 -0
  96. package/dist/src/rendering/renderers/AbstractRenderer.d.ts +4 -4
  97. package/dist/src/rendering/renderers/AbstractRenderer.js +2 -2
  98. package/dist/src/rendering/renderers/CanvasRenderer.d.ts +4 -4
  99. package/dist/src/rendering/renderers/CanvasRenderer.js +2 -2
  100. package/dist/src/rendering/renderers/DummyRenderer.d.ts +4 -4
  101. package/dist/src/rendering/renderers/DummyRenderer.js +1 -1
  102. package/dist/src/rendering/renderers/SVGRenderer.d.ts +3 -3
  103. package/dist/src/rendering/renderers/SVGRenderer.js +8 -2
  104. package/dist/src/rendering/renderers/TextOnlyRenderer.d.ts +5 -3
  105. package/dist/src/rendering/renderers/TextOnlyRenderer.js +13 -3
  106. package/dist/src/toolbar/HTMLToolbar.js +1 -0
  107. package/dist/src/toolbar/icons.d.ts +3 -0
  108. package/dist/src/toolbar/icons.js +142 -132
  109. package/dist/src/toolbar/localization.d.ts +2 -1
  110. package/dist/src/toolbar/localization.js +2 -1
  111. package/dist/src/toolbar/makeColorInput.js +3 -2
  112. package/dist/src/toolbar/widgets/ActionButtonWidget.d.ts +13 -0
  113. package/dist/src/toolbar/widgets/ActionButtonWidget.js +21 -0
  114. package/dist/src/toolbar/widgets/BaseWidget.js +2 -0
  115. package/dist/src/toolbar/widgets/HandToolWidget.js +3 -3
  116. package/dist/src/toolbar/widgets/PenWidget.js +1 -0
  117. package/dist/src/toolbar/widgets/SelectionWidget.d.ts +0 -1
  118. package/dist/src/toolbar/widgets/SelectionWidget.js +23 -30
  119. package/dist/src/tools/Eraser.js +1 -1
  120. package/dist/src/tools/PanZoom.d.ts +1 -1
  121. package/dist/src/tools/PanZoom.js +24 -14
  122. package/dist/src/tools/Pen.d.ts +1 -2
  123. package/dist/src/tools/Pen.js +8 -1
  124. package/dist/src/tools/PipetteTool.js +1 -0
  125. package/dist/src/tools/SelectionTool.d.ts +3 -3
  126. package/dist/src/tools/SelectionTool.js +51 -28
  127. package/dist/src/tools/TextTool.js +1 -1
  128. package/dist/src/types.d.ts +21 -10
  129. package/dist/src/types.js +7 -5
  130. package/firebase.json +16 -0
  131. package/package.json +118 -101
  132. package/src/Color4.ts +23 -2
  133. package/src/Editor.ts +181 -37
  134. package/src/EditorImage.test.ts +2 -4
  135. package/src/EditorImage.ts +46 -28
  136. package/src/EventDispatcher.ts +21 -6
  137. package/src/Pointer.ts +4 -3
  138. package/src/SVGLoader.ts +14 -6
  139. package/src/UndoRedoHistory.ts +18 -2
  140. package/src/Viewport.ts +23 -18
  141. package/src/bundle/bundled.ts +1 -2
  142. package/src/commands/Command.ts +5 -5
  143. package/src/commands/Duplicate.ts +4 -5
  144. package/src/commands/Erase.ts +7 -6
  145. package/src/commands/SerializableCommand.ts +17 -9
  146. package/src/commands/invertCommand.ts +51 -0
  147. package/src/commands/lib.ts +14 -0
  148. package/src/commands/localization.ts +3 -1
  149. package/src/components/AbstractComponent.ts +35 -24
  150. package/src/components/SVGGlobalAttributesObject.ts +11 -4
  151. package/src/components/Stroke.test.ts +4 -6
  152. package/src/components/Stroke.ts +15 -11
  153. package/src/components/Text.test.ts +2 -2
  154. package/src/components/Text.ts +9 -10
  155. package/src/components/UnknownSVGObject.ts +10 -4
  156. package/src/components/builders/ArrowBuilder.ts +2 -2
  157. package/src/components/builders/FreehandLineBuilder.ts +190 -80
  158. package/src/components/builders/LineBuilder.ts +2 -2
  159. package/src/components/builders/RectangleBuilder.ts +3 -3
  160. package/src/components/builders/types.ts +1 -1
  161. package/src/components/lib.ts +9 -0
  162. package/src/lib.ts +28 -0
  163. package/src/localization.ts +6 -0
  164. package/src/localizations/es.ts +2 -1
  165. package/src/{geometry → math}/LineSegment2.test.ts +0 -0
  166. package/src/{geometry → math}/LineSegment2.ts +0 -0
  167. package/src/{geometry → math}/Mat33.test.ts +0 -0
  168. package/src/{geometry → math}/Mat33.ts +48 -20
  169. package/src/{geometry → math}/Path.fromString.test.ts +0 -0
  170. package/src/{geometry → math}/Path.test.ts +0 -0
  171. package/src/{geometry → math}/Path.toString.test.ts +11 -2
  172. package/src/{geometry → math}/Path.ts +61 -58
  173. package/src/{geometry → math}/Rect2.test.ts +0 -0
  174. package/src/{geometry → math}/Rect2.ts +2 -2
  175. package/src/{geometry → math}/Vec2.test.ts +0 -0
  176. package/src/{geometry → math}/Vec2.ts +0 -0
  177. package/src/{geometry → math}/Vec3.test.ts +0 -0
  178. package/src/{geometry → math}/Vec3.ts +64 -16
  179. package/src/math/lib.ts +15 -0
  180. package/src/math/rounding.test.ts +40 -0
  181. package/src/math/rounding.ts +147 -0
  182. package/src/rendering/Display.ts +63 -15
  183. package/src/rendering/caching/CacheRecord.test.ts +3 -3
  184. package/src/rendering/caching/CacheRecord.ts +6 -2
  185. package/src/rendering/caching/CacheRecordManager.ts +34 -8
  186. package/src/rendering/caching/RenderingCache.test.ts +3 -3
  187. package/src/rendering/caching/RenderingCache.ts +11 -16
  188. package/src/rendering/caching/RenderingCacheNode.ts +23 -7
  189. package/src/rendering/caching/testUtils.ts +1 -1
  190. package/src/rendering/caching/types.ts +2 -7
  191. package/src/rendering/localization.ts +4 -0
  192. package/src/rendering/renderers/AbstractRenderer.ts +4 -4
  193. package/src/rendering/renderers/CanvasRenderer.ts +5 -5
  194. package/src/rendering/renderers/DummyRenderer.test.ts +2 -2
  195. package/src/rendering/renderers/DummyRenderer.ts +4 -4
  196. package/src/rendering/renderers/SVGRenderer.ts +10 -4
  197. package/src/rendering/renderers/TextOnlyRenderer.ts +17 -6
  198. package/src/toolbar/HTMLToolbar.ts +1 -0
  199. package/src/toolbar/icons.ts +157 -137
  200. package/src/toolbar/localization.ts +4 -2
  201. package/src/toolbar/makeColorInput.ts +3 -2
  202. package/src/toolbar/toolbar.css +1 -1
  203. package/src/toolbar/widgets/ActionButtonWidget.ts +31 -0
  204. package/src/toolbar/widgets/BaseWidget.ts +2 -0
  205. package/src/toolbar/widgets/HandToolWidget.ts +3 -3
  206. package/src/toolbar/widgets/PenWidget.ts +2 -0
  207. package/src/toolbar/widgets/SelectionWidget.ts +46 -41
  208. package/src/tools/Eraser.ts +2 -2
  209. package/src/tools/PanZoom.ts +28 -17
  210. package/src/tools/Pen.ts +11 -2
  211. package/src/tools/PipetteTool.ts +2 -0
  212. package/src/tools/SelectionTool.test.ts +2 -4
  213. package/src/tools/SelectionTool.ts +52 -24
  214. package/src/tools/TextTool.ts +2 -2
  215. package/src/tools/UndoRedoShortcut.test.ts +1 -1
  216. package/src/types.ts +23 -7
  217. package/tsconfig.json +4 -1
  218. package/typedoc.json +20 -0
  219. package/dist/src/geometry/Mat33.d.ts +0 -32
  220. package/dist/src/geometry/Vec3.d.ts +0 -34
@@ -9,6 +9,7 @@ class UndoRedoHistory {
9
9
  private undoStack: Command[];
10
10
  private redoStack: Command[];
11
11
 
12
+ // @internal
12
13
  public constructor(
13
14
  private readonly editor: Editor,
14
15
  private announceRedoCallback: AnnounceRedoCallback,
@@ -37,7 +38,12 @@ class UndoRedoHistory {
37
38
  elem.onDrop(this.editor);
38
39
  }
39
40
  this.redoStack = [];
41
+
40
42
  this.fireUpdateEvent();
43
+ this.editor.notifier.dispatch(EditorEventType.CommandDone, {
44
+ kind: EditorEventType.CommandDone,
45
+ command,
46
+ });
41
47
  }
42
48
 
43
49
  // Remove the last command from this' undo stack and apply it.
@@ -47,8 +53,13 @@ class UndoRedoHistory {
47
53
  this.redoStack.push(command);
48
54
  command.unapply(this.editor);
49
55
  this.announceUndoCallback(command);
56
+
57
+ this.fireUpdateEvent();
58
+ this.editor.notifier.dispatch(EditorEventType.CommandUndone, {
59
+ kind: EditorEventType.CommandUndone,
60
+ command,
61
+ });
50
62
  }
51
- this.fireUpdateEvent();
52
63
  }
53
64
 
54
65
  public redo() {
@@ -57,8 +68,13 @@ class UndoRedoHistory {
57
68
  this.undoStack.push(command);
58
69
  command.apply(this.editor);
59
70
  this.announceRedoCallback(command);
71
+
72
+ this.fireUpdateEvent();
73
+ this.editor.notifier.dispatch(EditorEventType.CommandDone, {
74
+ kind: EditorEventType.CommandDone,
75
+ command,
76
+ });
60
77
  }
61
- this.fireUpdateEvent();
62
78
  }
63
79
 
64
80
  public get undoStackSize(): number {
package/src/Viewport.ts CHANGED
@@ -1,19 +1,23 @@
1
1
  import Command from './commands/Command';
2
2
  import { CommandLocalization } from './commands/localization';
3
3
  import Editor from './Editor';
4
- import Mat33 from './geometry/Mat33';
5
- import Rect2 from './geometry/Rect2';
6
- import { Point2, Vec2 } from './geometry/Vec2';
7
- import Vec3 from './geometry/Vec3';
4
+ import Mat33 from './math/Mat33';
5
+ import Rect2 from './math/Rect2';
6
+ import { Point2, Vec2 } from './math/Vec2';
7
+ import Vec3 from './math/Vec3';
8
8
  import { StrokeDataPoint } from './types';
9
9
  import { EditorEventType, EditorNotifier } from './types';
10
10
 
11
11
  // Returns the base type of some type of point/number
12
12
  type PointDataType<T extends Point2|StrokeDataPoint|number> = T extends Point2 ? Point2 : number;
13
13
 
14
+ export abstract class ViewportTransform extends Command {
15
+ public abstract readonly transform: Mat33;
16
+ }
17
+
14
18
  export class Viewport {
15
19
  // Command that translates/scales the viewport.
16
- public static ViewportTransform = class extends Command {
20
+ private static ViewportTransform = class extends ViewportTransform {
17
21
  readonly #inverseTransform: Mat33;
18
22
 
19
23
  public constructor(public readonly transform: Mat33) {
@@ -33,14 +37,14 @@ export class Viewport {
33
37
  editor.queueRerender();
34
38
  }
35
39
 
36
- public description(localizationTable: CommandLocalization): string {
40
+ public description(editor: Editor, localizationTable: CommandLocalization): string {
37
41
  const result: string[] = [];
38
42
 
39
43
  // Describe the transformation's affect on the viewport (note that transformation transforms
40
44
  // the **elements** within the viewport). Assumes the transformation only does rotation/scale/translation.
41
- const origVec = Vec2.unitX;
45
+ const origVec = editor.viewport.visibleRect.center;
42
46
  const linearTransformedVec = this.transform.transformVec3(Vec2.unitX);
43
- const affineTransformedVec = this.transform.transformVec2(Vec2.unitX);
47
+ const affineTransformedVec = this.transform.transformVec2(origVec);
44
48
 
45
49
  const scale = linearTransformedVec.magnitude();
46
50
  const rotation = 180 / Math.PI * linearTransformedVec.angle();
@@ -48,8 +52,7 @@ export class Viewport {
48
52
 
49
53
  if (scale > 1.2) {
50
54
  result.push(localizationTable.zoomedIn);
51
- }
52
- else if (scale < 0.8) {
55
+ } else if (scale < 0.8) {
53
56
  result.push(localizationTable.zoomedOut);
54
57
  }
55
58
 
@@ -64,7 +67,7 @@ export class Viewport {
64
67
  result.push(localizationTable.movedRight);
65
68
  }
66
69
 
67
- if (translation.y < minTranslation) {
70
+ if (translation.y < -minTranslation) {
68
71
  result.push(localizationTable.movedDown);
69
72
  } else if (translation.y > minTranslation) {
70
73
  result.push(localizationTable.movedUp);
@@ -78,11 +81,13 @@ export class Viewport {
78
81
  private inverseTransform: Mat33;
79
82
  private screenRect: Rect2;
80
83
 
84
+ // @internal
81
85
  public constructor(private notifier: EditorNotifier) {
82
86
  this.resetTransform(Mat33.identity);
83
87
  this.screenRect = Rect2.empty;
84
88
  }
85
89
 
90
+ // @internal
86
91
  public updateScreenSize(screenSize: Vec2) {
87
92
  this.screenRect = this.screenRect.resizedTo(screenSize);
88
93
  }
@@ -100,7 +105,11 @@ export class Viewport {
100
105
  return this.transform.transformVec2(canvasPoint);
101
106
  }
102
107
 
103
- // Updates the transformation directly. Using ViewportTransform is preferred.
108
+ public static transformBy(transform: Mat33): ViewportTransform {
109
+ return new Viewport.ViewportTransform(transform);
110
+ }
111
+
112
+ // Updates the transformation directly. Using `transformBy` is preferred.
104
113
  // [newTransform] should map from canvas coordinates to screen coordinates.
105
114
  public resetTransform(newTransform: Mat33 = Mat33.identity) {
106
115
  const oldTransform = this.transform;
@@ -131,6 +140,7 @@ export class Viewport {
131
140
  return this.transform.transformVec3(Vec3.unitX).magnitude();
132
141
  }
133
142
 
143
+ // Returns the size of one screen pixel in canvas units.
134
144
  public getSizeOfPixelOnCanvas(): number {
135
145
  return 1/this.getScaleFactor();
136
146
  }
@@ -140,7 +150,7 @@ export class Viewport {
140
150
  return this.transform.transformVec3(Vec3.unitX).angle();
141
151
  }
142
152
 
143
- // Rounds the given [point] to a multiple of 10 such that it is within [tolerance] of
153
+ // Rounds the given `point` to a multiple of 10 such that it is within `tolerance` of
144
154
  // its original location. This is useful for preparing data for base-10 conversion.
145
155
  public static roundPoint<T extends Point2|number>(
146
156
  point: T, tolerance: number,
@@ -231,9 +241,4 @@ export class Viewport {
231
241
  }
232
242
  }
233
243
 
234
- export namespace Viewport { // eslint-disable-line
235
- // Needed to allow accessing as a type. See https://stackoverflow.com/a/68201883
236
- export type ViewportTransform = typeof Viewport.ViewportTransform.prototype;
237
- }
238
-
239
244
  export default Viewport;
@@ -2,7 +2,6 @@
2
2
 
3
3
  import '../styles';
4
4
  import Editor from '../Editor';
5
- import getLocalizationTable from '../localizations/getLocalizationTable';
5
+ export * from '../lib';
6
6
 
7
7
  export default Editor;
8
- export { Editor, getLocalizationTable };
@@ -8,7 +8,7 @@ export abstract class Command {
8
8
  // Called when the command is being deleted
9
9
  public onDrop(_editor: Editor) { }
10
10
 
11
- public abstract description(localizationTable: EditorLocalization): string;
11
+ public abstract description(editor: Editor, localizationTable: EditorLocalization): string;
12
12
 
13
13
  public static union(a: Command, b: Command): Command {
14
14
  return new class extends Command {
@@ -22,9 +22,9 @@ export abstract class Command {
22
22
  a.unapply(editor);
23
23
  }
24
24
 
25
- public description(localizationTable: EditorLocalization) {
26
- const aDescription = a.description(localizationTable);
27
- const bDescription = b.description(localizationTable);
25
+ public description(editor: Editor, localizationTable: EditorLocalization) {
26
+ const aDescription = a.description(editor, localizationTable);
27
+ const bDescription = b.description(editor, localizationTable);
28
28
 
29
29
  if (aDescription === bDescription) {
30
30
  return aDescription;
@@ -36,7 +36,7 @@ export abstract class Command {
36
36
  }
37
37
 
38
38
  public static readonly empty = new class extends Command {
39
- public description(_localizationTable: EditorLocalization) { return ''; }
39
+ public description(_editor: Editor, _localizationTable: EditorLocalization) { return ''; }
40
40
  public apply(_editor: Editor) { }
41
41
  public unapply(_editor: Editor) { }
42
42
  };
@@ -24,7 +24,7 @@ export default class Duplicate extends SerializableCommand {
24
24
  this.reverse.apply(editor);
25
25
  }
26
26
 
27
- public description(localizationTable: EditorLocalization): string {
27
+ public description(_editor: Editor, localizationTable: EditorLocalization): string {
28
28
  if (this.duplicates.length === 0) {
29
29
  return localizationTable.duplicatedNoElements;
30
30
  }
@@ -35,13 +35,12 @@ export default class Duplicate extends SerializableCommand {
35
35
  );
36
36
  }
37
37
 
38
- protected serializeToString(): string {
39
- return JSON.stringify(this.toDuplicate.map(elem => elem.getId()));
38
+ protected serializeToJSON() {
39
+ return this.toDuplicate.map(elem => elem.getId());
40
40
  }
41
41
 
42
42
  static {
43
- SerializableCommand.register('duplicate', (data: string, editor: Editor) => {
44
- const json = JSON.parse(data);
43
+ SerializableCommand.register('duplicate', (json: any, editor: Editor) => {
45
44
  const elems = json.map((id: string) => editor.image.lookupElement(id));
46
45
  return new Duplicate(elems);
47
46
  });
@@ -49,7 +49,7 @@ export default class Erase extends SerializableCommand {
49
49
  }
50
50
  }
51
51
 
52
- public description(localizationTable: EditorLocalization): string {
52
+ public description(_editor: Editor, localizationTable: EditorLocalization): string {
53
53
  if (this.toRemove.length === 0) {
54
54
  return localizationTable.erasedNoElements;
55
55
  }
@@ -58,15 +58,16 @@ export default class Erase extends SerializableCommand {
58
58
  return localizationTable.eraseAction(description, this.toRemove.length);
59
59
  }
60
60
 
61
- protected serializeToString() {
61
+ protected serializeToJSON() {
62
62
  const elemIds = this.toRemove.map(elem => elem.getId());
63
- return JSON.stringify(elemIds);
63
+ return elemIds;
64
64
  }
65
65
 
66
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));
67
+ SerializableCommand.register('erase', (json: any, editor) => {
68
+ const elems = json
69
+ .map((elemId: string) => editor.image.lookupElement(elemId))
70
+ .filter((elem: AbstractComponent|null) => elem !== null);
70
71
  return new Erase(elems);
71
72
  });
72
73
  }
@@ -1,7 +1,7 @@
1
1
  import Editor from '../Editor';
2
2
  import Command from './Command';
3
3
 
4
- type DeserializationCallback = (data: string, editor: Editor) => SerializableCommand;
4
+ export type DeserializationCallback = (data: Record<string, any>|any[], editor: Editor) => SerializableCommand;
5
5
 
6
6
  export default abstract class SerializableCommand extends Command {
7
7
  public constructor(private commandTypeId: string) {
@@ -14,27 +14,35 @@ export default abstract class SerializableCommand extends Command {
14
14
  }
15
15
  }
16
16
 
17
- protected abstract serializeToString(): string;
17
+ protected abstract serializeToJSON(): string|Record<string, any>|any[];
18
18
  private static deserializationCallbacks: Record<string, DeserializationCallback> = {};
19
19
 
20
- public serialize(): string {
21
- return JSON.stringify({
22
- data: this.serializeToString(),
20
+ // Convert this command to an object that can be passed to `JSON.stringify`.
21
+ //
22
+ // Do not rely on the stability of the optupt of this function — it can change
23
+ // form without a major version increase.
24
+ public serialize(): Record<string|symbol, any> {
25
+ return {
26
+ data: this.serializeToJSON(),
23
27
  commandType: this.commandTypeId,
24
- });
28
+ };
25
29
  }
26
30
 
27
- public static deserialize(data: string, editor: Editor): SerializableCommand {
28
- const json = JSON.parse(data);
31
+ // Convert a `string` containing JSON data (or the output of `JSON.parse`) into a
32
+ // `Command`.
33
+ public static deserialize(data: string|Record<string, any>, editor: Editor): SerializableCommand {
34
+ const json = typeof data === 'string' ? JSON.parse(data) : data;
29
35
  const commandType = json.commandType as string;
30
36
 
31
37
  if (!(commandType in SerializableCommand.deserializationCallbacks)) {
32
38
  throw new Error(`Unrecognised command type ${commandType}!`);
33
39
  }
34
40
 
35
- return SerializableCommand.deserializationCallbacks[commandType](json.data as string, editor);
41
+ return SerializableCommand.deserializationCallbacks[commandType](json.data, editor);
36
42
  }
37
43
 
44
+ // Register a deserialization callback. This must be called at least once for every subclass of
45
+ // `SerializableCommand`.
38
46
  public static register(commandTypeId: string, deserialize: DeserializationCallback) {
39
47
  SerializableCommand.deserializationCallbacks[commandTypeId] = deserialize;
40
48
  }
@@ -0,0 +1,51 @@
1
+ import Editor from '../Editor';
2
+ import { EditorLocalization } from '../localization';
3
+ import Command from './Command';
4
+ import SerializableCommand from './SerializableCommand';
5
+
6
+ // Returns a command taht does the opposite of the given command --- `result.apply()` calls
7
+ // `command.unapply()` and `result.unapply()` calls `command.apply()`.
8
+ const invertCommand = <T extends Command> (command: T): T extends SerializableCommand ? SerializableCommand : Command => {
9
+ if (command instanceof SerializableCommand) {
10
+ // SerializableCommand that does the inverse of [command]
11
+ return new class extends SerializableCommand {
12
+ protected serializeToJSON() {
13
+ return command.serialize();
14
+ }
15
+ public apply(editor: Editor): void {
16
+ command.unapply(editor);
17
+ }
18
+ public unapply(editor: Editor): void {
19
+ command.unapply(editor);
20
+ }
21
+ public description(editor: Editor, localizationTable: EditorLocalization): string {
22
+ return localizationTable.inverseOf(command.description(editor, localizationTable));
23
+ }
24
+ }('inverse');
25
+ } else {
26
+ // Command that does the inverse of [command].
27
+ const result = new class extends Command {
28
+ public apply(editor: Editor) {
29
+ command.unapply(editor);
30
+ }
31
+
32
+ public unapply(editor: Editor) {
33
+ command.apply(editor);
34
+ }
35
+
36
+ public description(editor: Editor, localizationTable: EditorLocalization) {
37
+ return localizationTable.inverseOf(command.description(editor, localizationTable));
38
+ }
39
+ };
40
+
41
+ // We know that T does not extend SerializableCommand, and thus returning a Command
42
+ // is appropriate.
43
+ return result as any;
44
+ }
45
+ };
46
+
47
+ SerializableCommand.register('inverse', (data, editor) => {
48
+ return invertCommand(SerializableCommand.deserialize(data, editor));
49
+ });
50
+
51
+ export default invertCommand;
@@ -0,0 +1,14 @@
1
+ import Command from './Command';
2
+ import Duplicate from './Duplicate';
3
+ import Erase from './Erase';
4
+ import invertCommand from './invertCommand';
5
+ import SerializableCommand from './SerializableCommand';
6
+
7
+ export {
8
+ Command,
9
+ Duplicate,
10
+ Erase,
11
+ SerializableCommand,
12
+
13
+ invertCommand,
14
+ };
@@ -1,4 +1,4 @@
1
- import Rect2 from '../geometry/Rect2';
1
+ import Rect2 from '../math/Rect2';
2
2
 
3
3
  export interface CommandLocalization {
4
4
  movedLeft: string;
@@ -17,6 +17,7 @@ export interface CommandLocalization {
17
17
  addElementAction: (elemDescription: string) => string;
18
18
  eraseAction: (elemDescription: string, numElems: number) => string;
19
19
  duplicateAction: (elemDescription: string, count: number)=> string;
20
+ inverseOf: (actionDescription: string)=> string;
20
21
 
21
22
  selectedElements: (count: number)=>string;
22
23
  }
@@ -28,6 +29,7 @@ export const defaultCommandLocalization: CommandLocalization = {
28
29
  addElementAction: (componentDescription: string) => `Added ${componentDescription}`,
29
30
  eraseAction: (componentDescription: string, numElems: number) => `Erased ${numElems} ${componentDescription}`,
30
31
  duplicateAction: (componentDescription: string, numElems: number) => `Duplicated ${numElems} ${componentDescription}`,
32
+ inverseOf: (actionDescription: string) => `Inverse of ${actionDescription}`,
31
33
  elements: 'Elements',
32
34
  erasedNoElements: 'Erased nothing',
33
35
  duplicatedNoElements: 'Duplicated nothing',
@@ -1,17 +1,16 @@
1
- import Command from '../commands/Command';
2
1
  import SerializableCommand from '../commands/SerializableCommand';
3
2
  import Editor from '../Editor';
4
3
  import EditorImage from '../EditorImage';
5
- import LineSegment2 from '../geometry/LineSegment2';
6
- import Mat33 from '../geometry/Mat33';
7
- import Rect2 from '../geometry/Rect2';
4
+ import LineSegment2 from '../math/LineSegment2';
5
+ import Mat33 from '../math/Mat33';
6
+ import Rect2 from '../math/Rect2';
8
7
  import { EditorLocalization } from '../localization';
9
8
  import AbstractRenderer from '../rendering/renderers/AbstractRenderer';
10
9
  import { ImageComponentLocalization } from './localization';
11
10
 
12
- type LoadSaveData = (string[]|Record<symbol, string|number>);
11
+ export type LoadSaveData = (string[]|Record<symbol, string|number>);
13
12
  export type LoadSaveDataTable = Record<string, Array<LoadSaveData>>;
14
- type DeserializeCallback = (data: string)=>AbstractComponent;
13
+ export type DeserializeCallback = (data: string)=>AbstractComponent;
15
14
  type ComponentId = string;
16
15
 
17
16
  export default abstract class AbstractComponent {
@@ -38,6 +37,8 @@ export default abstract class AbstractComponent {
38
37
  }
39
38
  }
40
39
 
40
+ // Returns a unique ID for this element.
41
+ // @see { @link EditorImage!default.lookupElement }
41
42
  public getId() {
42
43
  return this.id;
43
44
  }
@@ -77,14 +78,14 @@ export default abstract class AbstractComponent {
77
78
  public abstract intersects(lineSegment: LineSegment2): boolean;
78
79
 
79
80
  // Return null iff this object cannot be safely serialized/deserialized.
80
- protected abstract serializeToString(): string|null;
81
+ protected abstract serializeToJSON(): any[]|Record<string, any>|number|string|null;
81
82
 
82
83
  // Private helper for transformBy: Apply the given transformation to all points of this.
83
84
  protected abstract applyTransformation(affineTransfm: Mat33): void;
84
85
 
85
86
  // Returns a command that, when applied, transforms this by [affineTransfm] and
86
87
  // updates the editor.
87
- public transformBy(affineTransfm: Mat33): Command {
88
+ public transformBy(affineTransfm: Mat33): SerializableCommand {
88
89
  return new AbstractComponent.TransformElementCommand(affineTransfm, this);
89
90
  }
90
91
 
@@ -128,13 +129,12 @@ export default abstract class AbstractComponent {
128
129
  editor.queueRerender();
129
130
  }
130
131
 
131
- public description(localizationTable: EditorLocalization) {
132
+ public description(_editor: Editor, localizationTable: EditorLocalization) {
132
133
  return localizationTable.transformedElements(1);
133
134
  }
134
135
 
135
136
  static {
136
- SerializableCommand.register('transform-element', (data: string, editor: Editor) => {
137
- const json = JSON.parse(data);
137
+ SerializableCommand.register('transform-element', (json: any, editor: Editor) => {
138
138
  const elem = editor.image.lookupElement(json.id);
139
139
 
140
140
  if (!elem) {
@@ -154,11 +154,11 @@ export default abstract class AbstractComponent {
154
154
  });
155
155
  }
156
156
 
157
- protected serializeToString(): string {
158
- return JSON.stringify({
157
+ protected serializeToJSON() {
158
+ return {
159
159
  id: this.component.getId(),
160
160
  transfm: this.affineTransfm.toArray(),
161
- });
161
+ };
162
162
  }
163
163
  };
164
164
 
@@ -178,26 +178,33 @@ export default abstract class AbstractComponent {
178
178
  return clone;
179
179
  }
180
180
 
181
+ // Convert the component to an object that can be passed to
182
+ // `JSON.stringify`.
183
+ //
184
+ // Do not rely on the output of this function to take a particular form —
185
+ // this function's output can change form without a major version increase.
181
186
  public serialize() {
182
- const data = this.serializeToString();
187
+ const data = this.serializeToJSON();
183
188
 
184
189
  if (data === null) {
185
190
  throw new Error(`${this} cannot be serialized.`);
186
191
  }
187
192
 
188
- return JSON.stringify({
193
+ return {
189
194
  name: this.componentKind,
190
195
  zIndex: this.zIndex,
191
196
  id: this.id,
192
197
  loadSaveData: this.loadSaveData,
193
198
  data,
194
- });
199
+ };
195
200
  }
196
201
 
197
- // Returns true if [data] is not deserializable. May return false even if [data]
202
+ // Returns true if `data` is not deserializable. May return false even if [data]
198
203
  // is not deserializable.
199
- private static isNotDeserializable(data: string) {
200
- const json = JSON.parse(data);
204
+ private static isNotDeserializable(json: any|string) {
205
+ if (typeof json === 'string') {
206
+ json = JSON.parse(json);
207
+ }
201
208
 
202
209
  if (typeof json !== 'object') {
203
210
  return true;
@@ -214,12 +221,16 @@ export default abstract class AbstractComponent {
214
221
  return false;
215
222
  }
216
223
 
217
- public static deserialize(data: string): AbstractComponent {
218
- if (AbstractComponent.isNotDeserializable(data)) {
219
- throw new Error(`Element with data ${data} cannot be deserialized.`);
224
+ // Convert a string or an object produced by `JSON.parse` into an `AbstractComponent`.
225
+ public static deserialize(json: string|any): AbstractComponent {
226
+ if (typeof json === 'string') {
227
+ json = JSON.parse(json);
228
+ }
229
+
230
+ if (AbstractComponent.isNotDeserializable(json)) {
231
+ throw new Error(`Element with data ${json} cannot be deserialized.`);
220
232
  }
221
233
 
222
- const json = JSON.parse(data);
223
234
  const instance = this.deserializationCallbacks[json.name]!(json.data);
224
235
  instance.zIndex = json.zIndex;
225
236
  instance.id = json.id;
@@ -1,6 +1,13 @@
1
- import LineSegment2 from '../geometry/LineSegment2';
2
- import Mat33 from '../geometry/Mat33';
3
- import Rect2 from '../geometry/Rect2';
1
+ //
2
+ // Used by `SVGLoader`s to store unrecognised global attributes
3
+ // (e.g. unrecognised XML namespace declarations).
4
+ // @internal
5
+ // @packageDocumentation
6
+ //
7
+
8
+ import LineSegment2 from '../math/LineSegment2';
9
+ import Mat33 from '../math/Mat33';
10
+ import Rect2 from '../math/Rect2';
4
11
  import AbstractRenderer from '../rendering/renderers/AbstractRenderer';
5
12
  import SVGRenderer from '../rendering/renderers/SVGRenderer';
6
13
  import AbstractComponent from './AbstractComponent';
@@ -44,7 +51,7 @@ export default class SVGGlobalAttributesObject extends AbstractComponent {
44
51
  return localization.svgObject;
45
52
  }
46
53
 
47
- protected serializeToString(): string | null {
54
+ protected serializeToJSON(): string | null {
48
55
  return JSON.stringify(this.attrs);
49
56
  }
50
57
 
@@ -1,12 +1,10 @@
1
- /* @jest-environment jsdom */
2
-
3
1
  import Color4 from '../Color4';
4
- import Path from '../geometry/Path';
5
- import { Vec2 } from '../geometry/Vec2';
2
+ import Path from '../math/Path';
3
+ import { Vec2 } from '../math/Vec2';
6
4
  import Stroke from './Stroke';
7
5
  import { loadExpectExtensions } from '../testing/loadExpectExtensions';
8
6
  import createEditor from '../testing/createEditor';
9
- import Mat33 from '../geometry/Mat33';
7
+ import Mat33 from '../math/Mat33';
10
8
 
11
9
  loadExpectExtensions();
12
10
 
@@ -60,7 +58,7 @@ describe('Stroke', () => {
60
58
  });
61
59
 
62
60
  it('strokes should deserialize from JSON data', () => {
63
- const deserialized = Stroke.deserializeFromString(`[
61
+ const deserialized = Stroke.deserializeFromJSON(`[
64
62
  {
65
63
  "style": { "fill": "#f00" },
66
64
  "path": "m0,0 l10,10z"
@@ -1,7 +1,7 @@
1
- import LineSegment2 from '../geometry/LineSegment2';
2
- import Mat33 from '../geometry/Mat33';
3
- import Path from '../geometry/Path';
4
- import Rect2 from '../geometry/Rect2';
1
+ import LineSegment2 from '../math/LineSegment2';
2
+ import Mat33 from '../math/Mat33';
3
+ import Path from '../math/Path';
4
+ import Rect2 from '../math/Rect2';
5
5
  import AbstractRenderer, { RenderablePathSpec } from '../rendering/renderers/AbstractRenderer';
6
6
  import RenderingStyle, { styleFromJSON, styleToJSON } from '../rendering/RenderingStyle';
7
7
  import AbstractComponent from './AbstractComponent';
@@ -111,19 +111,23 @@ export default class Stroke extends AbstractComponent {
111
111
  return new Stroke(this.parts);
112
112
  }
113
113
 
114
- protected serializeToString(): string | null {
115
- return JSON.stringify(this.parts.map(part => {
114
+ protected serializeToJSON() {
115
+ return this.parts.map(part => {
116
116
  return {
117
117
  style: styleToJSON(part.style),
118
118
  path: part.path.serialize(),
119
119
  };
120
- }));
120
+ });
121
121
  }
122
122
 
123
- public static deserializeFromString(data: string): Stroke {
124
- const json = JSON.parse(data);
123
+ /** @internal */
124
+ public static deserializeFromJSON(json: any): Stroke {
125
+ if (typeof json === 'string') {
126
+ json = JSON.parse(json);
127
+ }
128
+
125
129
  if (typeof json !== 'object' || typeof json.length !== 'number') {
126
- throw new Error(`${data} is missing required field, parts, or parts is of the wrong type.`);
130
+ throw new Error(`${json} is missing required field, parts, or parts is of the wrong type.`);
127
131
  }
128
132
 
129
133
  const pathSpec: RenderablePathSpec[] = json.map((part: any) => {
@@ -134,4 +138,4 @@ export default class Stroke extends AbstractComponent {
134
138
  }
135
139
  }
136
140
 
137
- AbstractComponent.registerComponent('stroke', Stroke.deserializeFromString);
141
+ AbstractComponent.registerComponent('stroke', Stroke.deserializeFromJSON);
@@ -1,6 +1,6 @@
1
1
  import Color4 from '../Color4';
2
- import Mat33 from '../geometry/Mat33';
3
- import Rect2 from '../geometry/Rect2';
2
+ import Mat33 from '../math/Mat33';
3
+ import Rect2 from '../math/Rect2';
4
4
  import AbstractComponent from './AbstractComponent';
5
5
  import Text, { TextStyle } from './Text';
6
6
  import { loadExpectExtensions } from '../testing/loadExpectExtensions';