js-draw 0.1.12 → 0.2.1

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 (132) hide show
  1. package/.eslintrc.js +1 -0
  2. package/.firebaserc +5 -0
  3. package/.github/ISSUE_TEMPLATE/bug_report.md +34 -0
  4. package/.github/ISSUE_TEMPLATE/feature_request.md +20 -0
  5. package/.github/ISSUE_TEMPLATE/translation.md +96 -0
  6. package/.github/workflows/firebase-hosting-merge.yml +25 -0
  7. package/.github/workflows/firebase-hosting-pull-request.yml +22 -0
  8. package/.github/workflows/github-pages.yml +52 -0
  9. package/CHANGELOG.md +9 -0
  10. package/README.md +11 -6
  11. package/dist/bundle.js +1 -1
  12. package/dist/src/Color4.d.ts +19 -0
  13. package/dist/src/Color4.js +24 -3
  14. package/dist/src/Editor.d.ts +129 -2
  15. package/dist/src/Editor.js +94 -17
  16. package/dist/src/EditorImage.d.ts +7 -2
  17. package/dist/src/EditorImage.js +41 -25
  18. package/dist/src/EventDispatcher.d.ts +18 -0
  19. package/dist/src/EventDispatcher.js +19 -4
  20. package/dist/src/Pointer.js +3 -2
  21. package/dist/src/UndoRedoHistory.js +15 -2
  22. package/dist/src/Viewport.js +4 -1
  23. package/dist/src/bundle/bundled.d.ts +1 -2
  24. package/dist/src/bundle/bundled.js +1 -2
  25. package/dist/src/commands/Duplicate.d.ts +1 -1
  26. package/dist/src/commands/Duplicate.js +3 -4
  27. package/dist/src/commands/Erase.d.ts +1 -1
  28. package/dist/src/commands/Erase.js +6 -5
  29. package/dist/src/commands/SerializableCommand.d.ts +4 -5
  30. package/dist/src/commands/SerializableCommand.js +12 -4
  31. package/dist/src/commands/invertCommand.d.ts +4 -0
  32. package/dist/src/commands/invertCommand.js +44 -0
  33. package/dist/src/commands/lib.d.ts +6 -0
  34. package/dist/src/commands/lib.js +6 -0
  35. package/dist/src/commands/localization.d.ts +1 -0
  36. package/dist/src/commands/localization.js +1 -0
  37. package/dist/src/components/AbstractComponent.d.ts +13 -8
  38. package/dist/src/components/AbstractComponent.js +26 -15
  39. package/dist/src/components/SVGGlobalAttributesObject.d.ts +1 -1
  40. package/dist/src/components/SVGGlobalAttributesObject.js +7 -1
  41. package/dist/src/components/Stroke.d.ts +12 -2
  42. package/dist/src/components/Stroke.js +10 -7
  43. package/dist/src/components/Text.d.ts +2 -2
  44. package/dist/src/components/Text.js +6 -6
  45. package/dist/src/components/UnknownSVGObject.d.ts +1 -1
  46. package/dist/src/components/UnknownSVGObject.js +6 -1
  47. package/dist/src/components/lib.d.ts +4 -0
  48. package/dist/src/components/lib.js +4 -0
  49. package/dist/src/lib.d.ts +25 -0
  50. package/dist/src/lib.js +25 -0
  51. package/dist/src/localizations/de.d.ts +3 -0
  52. package/dist/src/localizations/de.js +4 -0
  53. package/dist/src/localizations/getLocalizationTable.js +2 -0
  54. package/dist/src/math/Mat33.d.ts +47 -1
  55. package/dist/src/math/Mat33.js +48 -20
  56. package/dist/src/math/Path.js +3 -3
  57. package/dist/src/math/Rect2.d.ts +2 -2
  58. package/dist/src/math/Vec3.d.ts +62 -0
  59. package/dist/src/math/Vec3.js +62 -14
  60. package/dist/src/math/lib.d.ts +7 -0
  61. package/dist/src/math/lib.js +7 -0
  62. package/dist/src/math/rounding.js +1 -0
  63. package/dist/src/rendering/Display.d.ts +44 -0
  64. package/dist/src/rendering/Display.js +45 -6
  65. package/dist/src/rendering/caching/CacheRecord.d.ts +1 -0
  66. package/dist/src/rendering/caching/CacheRecord.js +3 -0
  67. package/dist/src/rendering/caching/CacheRecordManager.d.ts +4 -3
  68. package/dist/src/rendering/caching/CacheRecordManager.js +16 -4
  69. package/dist/src/rendering/caching/RenderingCache.d.ts +2 -3
  70. package/dist/src/rendering/caching/RenderingCache.js +9 -10
  71. package/dist/src/rendering/caching/types.d.ts +1 -3
  72. package/dist/src/rendering/renderers/CanvasRenderer.js +1 -1
  73. package/dist/src/toolbar/HTMLToolbar.js +1 -0
  74. package/dist/src/toolbar/makeColorInput.js +1 -1
  75. package/dist/src/toolbar/widgets/PenWidget.js +1 -0
  76. package/dist/src/tools/Pen.d.ts +1 -2
  77. package/dist/src/tools/Pen.js +8 -1
  78. package/dist/src/tools/PipetteTool.js +1 -0
  79. package/dist/src/tools/SelectionTool.js +45 -22
  80. package/dist/src/types.d.ts +17 -6
  81. package/dist/src/types.js +7 -5
  82. package/firebase.json +16 -0
  83. package/package.json +118 -101
  84. package/src/Color4.ts +23 -2
  85. package/src/Editor.ts +147 -25
  86. package/src/EditorImage.ts +45 -27
  87. package/src/EventDispatcher.ts +21 -6
  88. package/src/Pointer.ts +3 -2
  89. package/src/UndoRedoHistory.ts +18 -2
  90. package/src/Viewport.ts +5 -2
  91. package/src/bundle/bundled.ts +1 -2
  92. package/src/commands/Duplicate.ts +3 -4
  93. package/src/commands/Erase.ts +6 -5
  94. package/src/commands/SerializableCommand.ts +17 -9
  95. package/src/commands/invertCommand.ts +51 -0
  96. package/src/commands/lib.ts +14 -0
  97. package/src/commands/localization.ts +2 -0
  98. package/src/components/AbstractComponent.ts +31 -20
  99. package/src/components/SVGGlobalAttributesObject.ts +8 -1
  100. package/src/components/Stroke.test.ts +1 -1
  101. package/src/components/Stroke.ts +11 -7
  102. package/src/components/Text.ts +6 -7
  103. package/src/components/UnknownSVGObject.ts +7 -1
  104. package/src/components/lib.ts +9 -0
  105. package/src/lib.ts +28 -0
  106. package/src/localizations/de.ts +98 -0
  107. package/src/localizations/getLocalizationTable.ts +2 -0
  108. package/src/math/Mat33.ts +48 -20
  109. package/src/math/Path.ts +3 -3
  110. package/src/math/Rect2.ts +2 -2
  111. package/src/math/Vec3.ts +62 -14
  112. package/src/math/lib.ts +15 -0
  113. package/src/math/rounding.ts +2 -0
  114. package/src/rendering/Display.ts +46 -6
  115. package/src/rendering/caching/CacheRecord.test.ts +1 -1
  116. package/src/rendering/caching/CacheRecord.ts +4 -0
  117. package/src/rendering/caching/CacheRecordManager.ts +33 -7
  118. package/src/rendering/caching/RenderingCache.ts +10 -15
  119. package/src/rendering/caching/types.ts +1 -6
  120. package/src/rendering/renderers/CanvasRenderer.ts +1 -1
  121. package/src/styles.js +4 -0
  122. package/src/toolbar/HTMLToolbar.ts +1 -0
  123. package/src/toolbar/makeColorInput.ts +1 -1
  124. package/src/toolbar/toolbar.css +8 -2
  125. package/src/toolbar/widgets/PenWidget.ts +2 -0
  126. package/src/tools/PanZoom.ts +0 -1
  127. package/src/tools/Pen.ts +11 -2
  128. package/src/tools/PipetteTool.ts +2 -0
  129. package/src/tools/SelectionTool.ts +46 -18
  130. package/src/types.ts +19 -3
  131. package/tsconfig.json +4 -1
  132. package/typedoc.json +20 -0
@@ -1,6 +1,21 @@
1
- // Code shared with Joplin
2
- // EventKeyType is used to distinguish events (e.g. a 'ClickEvent' vs a 'TouchEvent')
3
- // while EventMessageType is the type of the data sent with an event (can be `void`)
1
+ /**
2
+ * Handles notifying listeners of events.
3
+ *
4
+ * `EventKeyType` is used to distinguish events (e.g. a `ClickEvent` vs a `TouchEvent`)
5
+ * while `EventMessageType` is the type of the data sent with an event (can be `void`).
6
+ *
7
+ * @example
8
+ * ```
9
+ * const dispatcher = new EventDispatcher<'event1'|'event2'|'event3', void>();
10
+ * dispatcher.on('event1', () => {
11
+ * console.log('Event 1 triggered.');
12
+ * });
13
+ * dispatcher.dispatch('event1');
14
+ * ```
15
+ *
16
+ * @packageDocumentation
17
+ */
18
+ // { @inheritDoc EventDispatcher! }
4
19
  export default class EventDispatcher {
5
20
  constructor() {
6
21
  this.listeners = {};
@@ -26,7 +41,7 @@ export default class EventDispatcher {
26
41
  },
27
42
  };
28
43
  }
29
- // Equivalent to calling .remove() on the object returned by .on
44
+ /** Removes an event listener. This is equivalent to calling `.remove()` on the object returned by `.on`. */
30
45
  off(eventName, callback) {
31
46
  const listeners = this.listeners[eventName];
32
47
  if (!listeners)
@@ -9,7 +9,7 @@ export var PointerDevice;
9
9
  PointerDevice[PointerDevice["Other"] = 5] = "Other";
10
10
  })(PointerDevice || (PointerDevice = {}));
11
11
  // Provides a snapshot containing information about a pointer. A Pointer
12
- // object is immutable --- it will not be updated when the pointer's information changes.
12
+ // object is immutable it will not be updated when the pointer's information changes.
13
13
  export default class Pointer {
14
14
  constructor(
15
15
  // The (x, y) position of the pointer relative to the top-left corner
@@ -20,7 +20,7 @@ export default class Pointer {
20
20
  canvasPos, pressure, isPrimary, down, device,
21
21
  // Unique ID for the pointer
22
22
  id,
23
- // Numeric timestamp (milliseconds, as from (new Date).getTime())
23
+ // Numeric timestamp (milliseconds, as from `(new Date).getTime()`)
24
24
  timeStamp) {
25
25
  this.screenPos = screenPos;
26
26
  this.canvasPos = canvasPos;
@@ -31,6 +31,7 @@ export default class Pointer {
31
31
  this.id = id;
32
32
  this.timeStamp = timeStamp;
33
33
  }
34
+ // Creates a Pointer from a DOM event.
34
35
  static ofEvent(evt, isDown, viewport) {
35
36
  var _a, _b;
36
37
  const screenPos = Vec2.of(evt.offsetX, evt.offsetY);
@@ -1,5 +1,6 @@
1
1
  import { EditorEventType } from './types';
2
2
  class UndoRedoHistory {
3
+ // @internal
3
4
  constructor(editor, announceRedoCallback, announceUndoCallback) {
4
5
  this.editor = editor;
5
6
  this.announceRedoCallback = announceRedoCallback;
@@ -25,6 +26,10 @@ class UndoRedoHistory {
25
26
  }
26
27
  this.redoStack = [];
27
28
  this.fireUpdateEvent();
29
+ this.editor.notifier.dispatch(EditorEventType.CommandDone, {
30
+ kind: EditorEventType.CommandDone,
31
+ command,
32
+ });
28
33
  }
29
34
  // Remove the last command from this' undo stack and apply it.
30
35
  undo() {
@@ -33,8 +38,12 @@ class UndoRedoHistory {
33
38
  this.redoStack.push(command);
34
39
  command.unapply(this.editor);
35
40
  this.announceUndoCallback(command);
41
+ this.fireUpdateEvent();
42
+ this.editor.notifier.dispatch(EditorEventType.CommandUndone, {
43
+ kind: EditorEventType.CommandUndone,
44
+ command,
45
+ });
36
46
  }
37
- this.fireUpdateEvent();
38
47
  }
39
48
  redo() {
40
49
  const command = this.redoStack.pop();
@@ -42,8 +51,12 @@ class UndoRedoHistory {
42
51
  this.undoStack.push(command);
43
52
  command.apply(this.editor);
44
53
  this.announceRedoCallback(command);
54
+ this.fireUpdateEvent();
55
+ this.editor.notifier.dispatch(EditorEventType.CommandDone, {
56
+ kind: EditorEventType.CommandDone,
57
+ command,
58
+ });
45
59
  }
46
- this.fireUpdateEvent();
47
60
  }
48
61
  get undoStackSize() {
49
62
  return this.undoStack.length;
@@ -19,11 +19,13 @@ import { EditorEventType } from './types';
19
19
  export class ViewportTransform extends Command {
20
20
  }
21
21
  export class Viewport {
22
+ // @internal
22
23
  constructor(notifier) {
23
24
  this.notifier = notifier;
24
25
  this.resetTransform(Mat33.identity);
25
26
  this.screenRect = Rect2.empty;
26
27
  }
28
+ // @internal
27
29
  updateScreenSize(screenSize) {
28
30
  this.screenRect = this.screenRect.resizedTo(screenSize);
29
31
  }
@@ -40,7 +42,7 @@ export class Viewport {
40
42
  static transformBy(transform) {
41
43
  return new Viewport.ViewportTransform(transform);
42
44
  }
43
- // Updates the transformation directly. Using transformBy is preferred.
45
+ // Updates the transformation directly. Using `transformBy` is preferred.
44
46
  // [newTransform] should map from canvas coordinates to screen coordinates.
45
47
  resetTransform(newTransform = Mat33.identity) {
46
48
  const oldTransform = this.transform;
@@ -66,6 +68,7 @@ export class Viewport {
66
68
  // Use transformVec3 to avoid translating the vector
67
69
  return this.transform.transformVec3(Vec3.unitX).magnitude();
68
70
  }
71
+ // Returns the size of one screen pixel in canvas units.
69
72
  getSizeOfPixelOnCanvas() {
70
73
  return 1 / this.getScaleFactor();
71
74
  }
@@ -1,5 +1,4 @@
1
1
  import '../styles';
2
2
  import Editor from '../Editor';
3
- import getLocalizationTable from '../localizations/getLocalizationTable';
3
+ export * from '../lib';
4
4
  export default Editor;
5
- export { Editor, getLocalizationTable };
@@ -1,6 +1,5 @@
1
1
  // Main entrypoint for Webpack when building a bundle for release.
2
2
  import '../styles';
3
3
  import Editor from '../Editor';
4
- import getLocalizationTable from '../localizations/getLocalizationTable';
4
+ export * from '../lib';
5
5
  export default Editor;
6
- export { Editor, getLocalizationTable };
@@ -10,5 +10,5 @@ export default class Duplicate extends SerializableCommand {
10
10
  apply(editor: Editor): void;
11
11
  unapply(editor: Editor): void;
12
12
  description(_editor: Editor, localizationTable: EditorLocalization): string;
13
- protected serializeToString(): string;
13
+ protected serializeToJSON(): string[];
14
14
  }
@@ -21,13 +21,12 @@ export default class Duplicate extends SerializableCommand {
21
21
  }
22
22
  return localizationTable.duplicateAction((_a = describeComponentList(localizationTable, this.duplicates)) !== null && _a !== void 0 ? _a : localizationTable.elements, this.duplicates.length);
23
23
  }
24
- serializeToString() {
25
- return JSON.stringify(this.toDuplicate.map(elem => elem.getId()));
24
+ serializeToJSON() {
25
+ return this.toDuplicate.map(elem => elem.getId());
26
26
  }
27
27
  }
28
28
  (() => {
29
- SerializableCommand.register('duplicate', (data, editor) => {
30
- const json = JSON.parse(data);
29
+ SerializableCommand.register('duplicate', (json, editor) => {
31
30
  const elems = json.map((id) => editor.image.lookupElement(id));
32
31
  return new Duplicate(elems);
33
32
  });
@@ -10,5 +10,5 @@ export default class Erase extends SerializableCommand {
10
10
  unapply(editor: Editor): void;
11
11
  onDrop(editor: Editor): void;
12
12
  description(_editor: Editor, localizationTable: EditorLocalization): string;
13
- protected serializeToString(): string;
13
+ protected serializeToJSON(): string[];
14
14
  }
@@ -42,15 +42,16 @@ export default class Erase extends SerializableCommand {
42
42
  const description = (_a = describeComponentList(localizationTable, this.toRemove)) !== null && _a !== void 0 ? _a : localizationTable.elements;
43
43
  return localizationTable.eraseAction(description, this.toRemove.length);
44
44
  }
45
- serializeToString() {
45
+ serializeToJSON() {
46
46
  const elemIds = this.toRemove.map(elem => elem.getId());
47
- return JSON.stringify(elemIds);
47
+ return elemIds;
48
48
  }
49
49
  }
50
50
  (() => {
51
- SerializableCommand.register('erase', (data, editor) => {
52
- const json = JSON.parse(data);
53
- const elems = json.map((elemId) => editor.image.lookupElement(elemId));
51
+ SerializableCommand.register('erase', (json, editor) => {
52
+ const elems = json
53
+ .map((elemId) => editor.image.lookupElement(elemId))
54
+ .filter((elem) => elem !== null);
54
55
  return new Erase(elems);
55
56
  });
56
57
  })();
@@ -1,13 +1,12 @@
1
1
  import Editor from '../Editor';
2
2
  import Command from './Command';
3
- declare type DeserializationCallback = (data: string, editor: Editor) => SerializableCommand;
3
+ export declare type DeserializationCallback = (data: Record<string, any> | any[], editor: Editor) => SerializableCommand;
4
4
  export default abstract class SerializableCommand extends Command {
5
5
  private commandTypeId;
6
6
  constructor(commandTypeId: string);
7
- protected abstract serializeToString(): string;
7
+ protected abstract serializeToJSON(): string | Record<string, any> | any[];
8
8
  private static deserializationCallbacks;
9
- serialize(): string;
10
- static deserialize(data: string, editor: Editor): SerializableCommand;
9
+ serialize(): Record<string | symbol, any>;
10
+ static deserialize(data: string | Record<string, any>, editor: Editor): SerializableCommand;
11
11
  static register(commandTypeId: string, deserialize: DeserializationCallback): void;
12
12
  }
13
- export {};
@@ -7,20 +7,28 @@ export default class SerializableCommand extends Command {
7
7
  throw new Error(`Command ${commandTypeId} must have a registered deserialization callback. To do this, call SerializableCommand.register.`);
8
8
  }
9
9
  }
10
+ // Convert this command to an object that can be passed to `JSON.stringify`.
11
+ //
12
+ // Do not rely on the stability of the optupt of this function — it can change
13
+ // form without a major version increase.
10
14
  serialize() {
11
- return JSON.stringify({
12
- data: this.serializeToString(),
15
+ return {
16
+ data: this.serializeToJSON(),
13
17
  commandType: this.commandTypeId,
14
- });
18
+ };
15
19
  }
20
+ // Convert a `string` containing JSON data (or the output of `JSON.parse`) into a
21
+ // `Command`.
16
22
  static deserialize(data, editor) {
17
- const json = JSON.parse(data);
23
+ const json = typeof data === 'string' ? JSON.parse(data) : data;
18
24
  const commandType = json.commandType;
19
25
  if (!(commandType in SerializableCommand.deserializationCallbacks)) {
20
26
  throw new Error(`Unrecognised command type ${commandType}!`);
21
27
  }
22
28
  return SerializableCommand.deserializationCallbacks[commandType](json.data, editor);
23
29
  }
30
+ // Register a deserialization callback. This must be called at least once for every subclass of
31
+ // `SerializableCommand`.
24
32
  static register(commandTypeId, deserialize) {
25
33
  SerializableCommand.deserializationCallbacks[commandTypeId] = deserialize;
26
34
  }
@@ -0,0 +1,4 @@
1
+ import Command from './Command';
2
+ import SerializableCommand from './SerializableCommand';
3
+ declare const invertCommand: <T extends Command>(command: T) => T extends SerializableCommand ? SerializableCommand : Command;
4
+ export default invertCommand;
@@ -0,0 +1,44 @@
1
+ import Command from './Command';
2
+ import SerializableCommand from './SerializableCommand';
3
+ // Returns a command taht does the opposite of the given command --- `result.apply()` calls
4
+ // `command.unapply()` and `result.unapply()` calls `command.apply()`.
5
+ const invertCommand = (command) => {
6
+ if (command instanceof SerializableCommand) {
7
+ // SerializableCommand that does the inverse of [command]
8
+ return new class extends SerializableCommand {
9
+ serializeToJSON() {
10
+ return command.serialize();
11
+ }
12
+ apply(editor) {
13
+ command.unapply(editor);
14
+ }
15
+ unapply(editor) {
16
+ command.unapply(editor);
17
+ }
18
+ description(editor, localizationTable) {
19
+ return localizationTable.inverseOf(command.description(editor, localizationTable));
20
+ }
21
+ }('inverse');
22
+ }
23
+ else {
24
+ // Command that does the inverse of [command].
25
+ const result = new class extends Command {
26
+ apply(editor) {
27
+ command.unapply(editor);
28
+ }
29
+ unapply(editor) {
30
+ command.apply(editor);
31
+ }
32
+ description(editor, localizationTable) {
33
+ return localizationTable.inverseOf(command.description(editor, localizationTable));
34
+ }
35
+ };
36
+ // We know that T does not extend SerializableCommand, and thus returning a Command
37
+ // is appropriate.
38
+ return result;
39
+ }
40
+ };
41
+ SerializableCommand.register('inverse', (data, editor) => {
42
+ return invertCommand(SerializableCommand.deserialize(data, editor));
43
+ });
44
+ export default invertCommand;
@@ -0,0 +1,6 @@
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
+ export { Command, Duplicate, Erase, SerializableCommand, invertCommand, };
@@ -0,0 +1,6 @@
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
+ export { Command, Duplicate, Erase, SerializableCommand, invertCommand, };
@@ -16,6 +16,7 @@ export interface CommandLocalization {
16
16
  addElementAction: (elemDescription: string) => string;
17
17
  eraseAction: (elemDescription: string, numElems: number) => string;
18
18
  duplicateAction: (elemDescription: string, count: number) => string;
19
+ inverseOf: (actionDescription: string) => string;
19
20
  selectedElements: (count: number) => string;
20
21
  }
21
22
  export declare const defaultCommandLocalization: CommandLocalization;
@@ -5,6 +5,7 @@ export const defaultCommandLocalization = {
5
5
  addElementAction: (componentDescription) => `Added ${componentDescription}`,
6
6
  eraseAction: (componentDescription, numElems) => `Erased ${numElems} ${componentDescription}`,
7
7
  duplicateAction: (componentDescription, numElems) => `Duplicated ${numElems} ${componentDescription}`,
8
+ inverseOf: (actionDescription) => `Inverse of ${actionDescription}`,
8
9
  elements: 'Elements',
9
10
  erasedNoElements: 'Erased nothing',
10
11
  duplicatedNoElements: 'Duplicated nothing',
@@ -1,12 +1,12 @@
1
- import Command from '../commands/Command';
1
+ import SerializableCommand from '../commands/SerializableCommand';
2
2
  import LineSegment2 from '../math/LineSegment2';
3
3
  import Mat33 from '../math/Mat33';
4
4
  import Rect2 from '../math/Rect2';
5
5
  import AbstractRenderer from '../rendering/renderers/AbstractRenderer';
6
6
  import { ImageComponentLocalization } from './localization';
7
- declare type LoadSaveData = (string[] | Record<symbol, string | number>);
7
+ export declare type LoadSaveData = (string[] | Record<symbol, string | number>);
8
8
  export declare type LoadSaveDataTable = Record<string, Array<LoadSaveData>>;
9
- declare type DeserializeCallback = (data: string) => AbstractComponent;
9
+ export declare type DeserializeCallback = (data: string) => AbstractComponent;
10
10
  export default abstract class AbstractComponent {
11
11
  private readonly componentKind;
12
12
  protected lastChangedTime: number;
@@ -25,15 +25,20 @@ export default abstract class AbstractComponent {
25
25
  getBBox(): Rect2;
26
26
  abstract render(canvas: AbstractRenderer, visibleRect?: Rect2): void;
27
27
  abstract intersects(lineSegment: LineSegment2): boolean;
28
- protected abstract serializeToString(): string | null;
28
+ protected abstract serializeToJSON(): any[] | Record<string, any> | number | string | null;
29
29
  protected abstract applyTransformation(affineTransfm: Mat33): void;
30
- transformBy(affineTransfm: Mat33): Command;
30
+ transformBy(affineTransfm: Mat33): SerializableCommand;
31
31
  private static TransformElementCommand;
32
32
  abstract description(localizationTable: ImageComponentLocalization): string;
33
33
  protected abstract createClone(): AbstractComponent;
34
34
  clone(): AbstractComponent;
35
- serialize(): string;
35
+ serialize(): {
36
+ name: string;
37
+ zIndex: number;
38
+ id: string;
39
+ loadSaveData: LoadSaveDataTable;
40
+ data: string | number | any[] | Record<string, any>;
41
+ };
36
42
  private static isNotDeserializable;
37
- static deserialize(data: string): AbstractComponent;
43
+ static deserialize(json: string | any): AbstractComponent;
38
44
  }
39
- export {};
@@ -17,6 +17,8 @@ export default class AbstractComponent {
17
17
  throw new Error(`Component ${componentKind} has not been registered using AbstractComponent.registerComponent`);
18
18
  }
19
19
  }
20
+ // Returns a unique ID for this element.
21
+ // @see { @link EditorImage!default.lookupElement }
20
22
  getId() {
21
23
  return this.id;
22
24
  }
@@ -55,23 +57,30 @@ export default class AbstractComponent {
55
57
  }
56
58
  return clone;
57
59
  }
60
+ // Convert the component to an object that can be passed to
61
+ // `JSON.stringify`.
62
+ //
63
+ // Do not rely on the output of this function to take a particular form —
64
+ // this function's output can change form without a major version increase.
58
65
  serialize() {
59
- const data = this.serializeToString();
66
+ const data = this.serializeToJSON();
60
67
  if (data === null) {
61
68
  throw new Error(`${this} cannot be serialized.`);
62
69
  }
63
- return JSON.stringify({
70
+ return {
64
71
  name: this.componentKind,
65
72
  zIndex: this.zIndex,
66
73
  id: this.id,
67
74
  loadSaveData: this.loadSaveData,
68
75
  data,
69
- });
76
+ };
70
77
  }
71
- // Returns true if [data] is not deserializable. May return false even if [data]
78
+ // Returns true if `data` is not deserializable. May return false even if [data]
72
79
  // is not deserializable.
73
- static isNotDeserializable(data) {
74
- const json = JSON.parse(data);
80
+ static isNotDeserializable(json) {
81
+ if (typeof json === 'string') {
82
+ json = JSON.parse(json);
83
+ }
75
84
  if (typeof json !== 'object') {
76
85
  return true;
77
86
  }
@@ -83,11 +92,14 @@ export default class AbstractComponent {
83
92
  }
84
93
  return false;
85
94
  }
86
- static deserialize(data) {
87
- if (AbstractComponent.isNotDeserializable(data)) {
88
- throw new Error(`Element with data ${data} cannot be deserialized.`);
95
+ // Convert a string or an object produced by `JSON.parse` into an `AbstractComponent`.
96
+ static deserialize(json) {
97
+ if (typeof json === 'string') {
98
+ json = JSON.parse(json);
99
+ }
100
+ if (AbstractComponent.isNotDeserializable(json)) {
101
+ throw new Error(`Element with data ${json} cannot be deserialized.`);
89
102
  }
90
- const json = JSON.parse(data);
91
103
  const instance = this.deserializationCallbacks[json.name](json.data);
92
104
  instance.zIndex = json.zIndex;
93
105
  instance.id = json.id;
@@ -135,16 +147,15 @@ AbstractComponent.TransformElementCommand = (_a = class extends SerializableComm
135
147
  description(_editor, localizationTable) {
136
148
  return localizationTable.transformedElements(1);
137
149
  }
138
- serializeToString() {
139
- return JSON.stringify({
150
+ serializeToJSON() {
151
+ return {
140
152
  id: this.component.getId(),
141
153
  transfm: this.affineTransfm.toArray(),
142
- });
154
+ };
143
155
  }
144
156
  },
145
157
  (() => {
146
- SerializableCommand.register('transform-element', (data, editor) => {
147
- const json = JSON.parse(data);
158
+ SerializableCommand.register('transform-element', (json, editor) => {
148
159
  const elem = editor.image.lookupElement(json.id);
149
160
  if (!elem) {
150
161
  throw new Error(`Unable to retrieve non-existent element, ${elem}`);
@@ -14,7 +14,7 @@ export default class SVGGlobalAttributesObject extends AbstractComponent {
14
14
  protected applyTransformation(_affineTransfm: Mat33): void;
15
15
  protected createClone(): SVGGlobalAttributesObject;
16
16
  description(localization: ImageComponentLocalization): string;
17
- protected serializeToString(): string | null;
17
+ protected serializeToJSON(): string | null;
18
18
  static deserializeFromString(data: string): AbstractComponent;
19
19
  }
20
20
  export {};
@@ -1,3 +1,9 @@
1
+ //
2
+ // Used by `SVGLoader`s to store unrecognised global attributes
3
+ // (e.g. unrecognised XML namespace declarations).
4
+ // @internal
5
+ // @packageDocumentation
6
+ //
1
7
  import Rect2 from '../math/Rect2';
2
8
  import SVGRenderer from '../rendering/renderers/SVGRenderer';
3
9
  import AbstractComponent from './AbstractComponent';
@@ -29,7 +35,7 @@ export default class SVGGlobalAttributesObject extends AbstractComponent {
29
35
  description(localization) {
30
36
  return localization.svgObject;
31
37
  }
32
- serializeToString() {
38
+ serializeToJSON() {
33
39
  return JSON.stringify(this.attrs);
34
40
  }
35
41
  static deserializeFromString(data) {
@@ -16,6 +16,16 @@ export default class Stroke extends AbstractComponent {
16
16
  getPath(): Path;
17
17
  description(localization: ImageComponentLocalization): string;
18
18
  protected createClone(): AbstractComponent;
19
- protected serializeToString(): string | null;
20
- static deserializeFromString(data: string): Stroke;
19
+ protected serializeToJSON(): {
20
+ style: {
21
+ fill: string;
22
+ stroke: {
23
+ color: string;
24
+ width: number;
25
+ } | undefined;
26
+ };
27
+ path: string;
28
+ }[];
29
+ /** @internal */
30
+ static deserializeFromJSON(json: any): Stroke;
21
31
  }
@@ -87,18 +87,21 @@ export default class Stroke extends AbstractComponent {
87
87
  createClone() {
88
88
  return new Stroke(this.parts);
89
89
  }
90
- serializeToString() {
91
- return JSON.stringify(this.parts.map(part => {
90
+ serializeToJSON() {
91
+ return this.parts.map(part => {
92
92
  return {
93
93
  style: styleToJSON(part.style),
94
94
  path: part.path.serialize(),
95
95
  };
96
- }));
96
+ });
97
97
  }
98
- static deserializeFromString(data) {
99
- const json = JSON.parse(data);
98
+ /** @internal */
99
+ static deserializeFromJSON(json) {
100
+ if (typeof json === 'string') {
101
+ json = JSON.parse(json);
102
+ }
100
103
  if (typeof json !== 'object' || typeof json.length !== 'number') {
101
- throw new Error(`${data} is missing required field, parts, or parts is of the wrong type.`);
104
+ throw new Error(`${json} is missing required field, parts, or parts is of the wrong type.`);
102
105
  }
103
106
  const pathSpec = json.map((part) => {
104
107
  const style = styleFromJSON(part.style);
@@ -107,4 +110,4 @@ export default class Stroke extends AbstractComponent {
107
110
  return new Stroke(pathSpec);
108
111
  }
109
112
  }
110
- AbstractComponent.registerComponent('stroke', Stroke.deserializeFromString);
113
+ AbstractComponent.registerComponent('stroke', Stroke.deserializeFromJSON);
@@ -31,7 +31,7 @@ export default class Text extends AbstractComponent {
31
31
  protected createClone(): AbstractComponent;
32
32
  private getText;
33
33
  description(localizationTable: ImageComponentLocalization): string;
34
- protected serializeToString(): string;
35
- static deserializeFromString(data: string, getTextDimens?: GetTextDimensCallback): Text;
34
+ protected serializeToJSON(): Record<string, any>;
35
+ static deserializeFromString(json: any, getTextDimens?: GetTextDimensCallback): Text;
36
36
  }
37
37
  export {};
@@ -7,6 +7,7 @@ const componentTypeId = 'text';
7
7
  export default class Text extends AbstractComponent {
8
8
  constructor(textObjects, transform, style,
9
9
  // If not given, an HtmlCanvasElement is used to determine text boundaries.
10
+ // @internal
10
11
  getTextDimens = Text.getTextDimens) {
11
12
  super(componentTypeId);
12
13
  this.textObjects = textObjects;
@@ -117,7 +118,7 @@ export default class Text extends AbstractComponent {
117
118
  description(localizationTable) {
118
119
  return localizationTable.text(this.getText());
119
120
  }
120
- serializeToString() {
121
+ serializeToJSON() {
121
122
  const serializableStyle = Object.assign(Object.assign({}, this.style), { renderingStyle: styleToJSON(this.style.renderingStyle) });
122
123
  const textObjects = this.textObjects.map(text => {
123
124
  if (typeof text === 'string') {
@@ -127,18 +128,17 @@ export default class Text extends AbstractComponent {
127
128
  }
128
129
  else {
129
130
  return {
130
- json: text.serializeToString(),
131
+ json: text.serializeToJSON(),
131
132
  };
132
133
  }
133
134
  });
134
- return JSON.stringify({
135
+ return {
135
136
  textObjects,
136
137
  transform: this.transform.toArray(),
137
138
  style: serializableStyle,
138
- });
139
+ };
139
140
  }
140
- static deserializeFromString(data, getTextDimens = Text.getTextDimens) {
141
- const json = JSON.parse(data);
141
+ static deserializeFromString(json, getTextDimens = Text.getTextDimens) {
142
142
  const style = {
143
143
  renderingStyle: styleFromJSON(json.style.renderingStyle),
144
144
  size: json.style.size,
@@ -13,5 +13,5 @@ export default class UnknownSVGObject extends AbstractComponent {
13
13
  protected applyTransformation(_affineTransfm: Mat33): void;
14
14
  protected createClone(): AbstractComponent;
15
15
  description(localization: ImageComponentLocalization): string;
16
- protected serializeToString(): string | null;
16
+ protected serializeToJSON(): string | null;
17
17
  }
@@ -1,3 +1,8 @@
1
+ //
2
+ // Stores objects loaded from an SVG that aren't recognised by the editor.
3
+ // @internal
4
+ // @packageDocumentation
5
+ //
1
6
  import Rect2 from '../math/Rect2';
2
7
  import SVGRenderer from '../rendering/renderers/SVGRenderer';
3
8
  import AbstractComponent from './AbstractComponent';
@@ -26,7 +31,7 @@ export default class UnknownSVGObject extends AbstractComponent {
26
31
  description(localization) {
27
32
  return localization.svgObject;
28
33
  }
29
- serializeToString() {
34
+ serializeToJSON() {
30
35
  return JSON.stringify({
31
36
  html: this.svgObject.outerHTML,
32
37
  });
@@ -0,0 +1,4 @@
1
+ import AbstractComponent from './AbstractComponent';
2
+ import Stroke from './Stroke';
3
+ import Text from './Text';
4
+ export { AbstractComponent, Stroke, Text, };