js-draw 0.1.5 → 0.1.8

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 (139) 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 +24 -9
  8. package/dist/src/EditorImage.d.ts +8 -13
  9. package/dist/src/EditorImage.js +51 -29
  10. package/dist/src/SVGLoader.js +6 -2
  11. package/dist/src/Viewport.d.ts +10 -2
  12. package/dist/src/Viewport.js +8 -6
  13. package/dist/src/commands/Command.d.ts +9 -8
  14. package/dist/src/commands/Command.js +15 -14
  15. package/dist/src/commands/Duplicate.d.ts +14 -0
  16. package/dist/src/commands/Duplicate.js +34 -0
  17. package/dist/src/commands/Erase.d.ts +5 -2
  18. package/dist/src/commands/Erase.js +28 -9
  19. package/dist/src/commands/SerializableCommand.d.ts +13 -0
  20. package/dist/src/commands/SerializableCommand.js +28 -0
  21. package/dist/src/commands/localization.d.ts +2 -0
  22. package/dist/src/commands/localization.js +2 -0
  23. package/dist/src/components/AbstractComponent.d.ts +15 -2
  24. package/dist/src/components/AbstractComponent.js +122 -26
  25. package/dist/src/components/SVGGlobalAttributesObject.d.ts +6 -1
  26. package/dist/src/components/SVGGlobalAttributesObject.js +23 -1
  27. package/dist/src/components/Stroke.d.ts +5 -0
  28. package/dist/src/components/Stroke.js +32 -1
  29. package/dist/src/components/Text.d.ts +11 -4
  30. package/dist/src/components/Text.js +57 -3
  31. package/dist/src/components/UnknownSVGObject.d.ts +2 -0
  32. package/dist/src/components/UnknownSVGObject.js +12 -1
  33. package/dist/src/components/builders/RectangleBuilder.d.ts +3 -1
  34. package/dist/src/components/builders/RectangleBuilder.js +17 -8
  35. package/dist/src/components/util/describeComponentList.d.ts +4 -0
  36. package/dist/src/components/util/describeComponentList.js +14 -0
  37. package/dist/src/geometry/Path.d.ts +4 -1
  38. package/dist/src/geometry/Path.js +4 -0
  39. package/dist/src/rendering/Display.d.ts +3 -0
  40. package/dist/src/rendering/Display.js +13 -0
  41. package/dist/src/rendering/RenderingStyle.d.ts +24 -0
  42. package/dist/src/rendering/RenderingStyle.js +32 -0
  43. package/dist/src/rendering/caching/RenderingCacheNode.js +5 -1
  44. package/dist/src/rendering/renderers/AbstractRenderer.d.ts +1 -8
  45. package/dist/src/rendering/renderers/AbstractRenderer.js +1 -6
  46. package/dist/src/rendering/renderers/CanvasRenderer.d.ts +2 -1
  47. package/dist/src/rendering/renderers/DummyRenderer.d.ts +2 -1
  48. package/dist/src/rendering/renderers/SVGRenderer.d.ts +2 -1
  49. package/dist/src/rendering/renderers/TextOnlyRenderer.d.ts +2 -1
  50. package/dist/src/toolbar/HTMLToolbar.d.ts +1 -1
  51. package/dist/src/toolbar/HTMLToolbar.js +52 -534
  52. package/dist/src/toolbar/icons.d.ts +5 -0
  53. package/dist/src/toolbar/icons.js +186 -13
  54. package/dist/src/toolbar/localization.d.ts +4 -0
  55. package/dist/src/toolbar/localization.js +4 -0
  56. package/dist/src/toolbar/makeColorInput.d.ts +5 -0
  57. package/dist/src/toolbar/makeColorInput.js +81 -0
  58. package/dist/src/toolbar/widgets/BaseToolWidget.d.ts +12 -0
  59. package/dist/src/toolbar/widgets/BaseToolWidget.js +44 -0
  60. package/dist/src/toolbar/widgets/BaseWidget.d.ts +32 -0
  61. package/dist/src/toolbar/widgets/BaseWidget.js +148 -0
  62. package/dist/src/toolbar/widgets/EraserWidget.d.ts +6 -0
  63. package/dist/src/toolbar/widgets/EraserWidget.js +14 -0
  64. package/dist/src/toolbar/widgets/HandToolWidget.d.ts +13 -0
  65. package/dist/src/toolbar/widgets/HandToolWidget.js +133 -0
  66. package/dist/src/toolbar/widgets/PenWidget.d.ts +20 -0
  67. package/dist/src/toolbar/widgets/PenWidget.js +131 -0
  68. package/dist/src/toolbar/widgets/SelectionWidget.d.ts +11 -0
  69. package/dist/src/toolbar/widgets/SelectionWidget.js +56 -0
  70. package/dist/src/toolbar/widgets/TextToolWidget.d.ts +13 -0
  71. package/dist/src/toolbar/widgets/TextToolWidget.js +72 -0
  72. package/dist/src/tools/Pen.js +1 -1
  73. package/dist/src/tools/PipetteTool.d.ts +20 -0
  74. package/dist/src/tools/PipetteTool.js +40 -0
  75. package/dist/src/tools/SelectionTool.d.ts +2 -0
  76. package/dist/src/tools/SelectionTool.js +41 -23
  77. package/dist/src/tools/TextTool.js +1 -1
  78. package/dist/src/tools/ToolController.d.ts +3 -1
  79. package/dist/src/tools/ToolController.js +4 -0
  80. package/dist/src/tools/localization.d.ts +2 -1
  81. package/dist/src/tools/localization.js +3 -2
  82. package/dist/src/types.d.ts +7 -2
  83. package/dist/src/types.js +1 -0
  84. package/jest.config.js +2 -0
  85. package/package.json +6 -6
  86. package/src/Color4.ts +9 -3
  87. package/src/Editor.ts +29 -12
  88. package/src/EditorImage.test.ts +5 -5
  89. package/src/EditorImage.ts +61 -20
  90. package/src/SVGLoader.ts +9 -3
  91. package/src/Viewport.ts +7 -6
  92. package/src/commands/Command.ts +21 -19
  93. package/src/commands/Duplicate.ts +49 -0
  94. package/src/commands/Erase.ts +34 -13
  95. package/src/commands/SerializableCommand.ts +41 -0
  96. package/src/commands/localization.ts +5 -0
  97. package/src/components/AbstractComponent.ts +168 -26
  98. package/src/components/SVGGlobalAttributesObject.ts +34 -2
  99. package/src/components/Stroke.test.ts +53 -0
  100. package/src/components/Stroke.ts +37 -2
  101. package/src/components/Text.test.ts +38 -0
  102. package/src/components/Text.ts +80 -5
  103. package/src/components/UnknownSVGObject.test.ts +10 -0
  104. package/src/components/UnknownSVGObject.ts +15 -1
  105. package/src/components/builders/FreehandLineBuilder.ts +2 -1
  106. package/src/components/builders/RectangleBuilder.ts +23 -8
  107. package/src/components/util/describeComponentList.ts +18 -0
  108. package/src/geometry/Path.ts +8 -1
  109. package/src/rendering/Display.ts +17 -1
  110. package/src/rendering/RenderingStyle.test.ts +68 -0
  111. package/src/rendering/RenderingStyle.ts +46 -0
  112. package/src/rendering/caching/RenderingCache.test.ts +1 -1
  113. package/src/rendering/caching/RenderingCacheNode.ts +6 -1
  114. package/src/rendering/renderers/AbstractRenderer.ts +1 -15
  115. package/src/rendering/renderers/CanvasRenderer.ts +2 -1
  116. package/src/rendering/renderers/DummyRenderer.ts +2 -1
  117. package/src/rendering/renderers/SVGRenderer.ts +2 -1
  118. package/src/rendering/renderers/TextOnlyRenderer.ts +2 -1
  119. package/src/toolbar/HTMLToolbar.ts +58 -660
  120. package/src/toolbar/icons.ts +205 -13
  121. package/src/toolbar/localization.ts +10 -2
  122. package/src/toolbar/makeColorInput.ts +105 -0
  123. package/src/toolbar/toolbar.css +116 -78
  124. package/src/toolbar/widgets/BaseToolWidget.ts +53 -0
  125. package/src/toolbar/widgets/BaseWidget.ts +175 -0
  126. package/src/toolbar/widgets/EraserWidget.ts +16 -0
  127. package/src/toolbar/widgets/HandToolWidget.ts +186 -0
  128. package/src/toolbar/widgets/PenWidget.ts +165 -0
  129. package/src/toolbar/widgets/SelectionWidget.ts +72 -0
  130. package/src/toolbar/widgets/TextToolWidget.ts +90 -0
  131. package/src/tools/Pen.ts +1 -1
  132. package/src/tools/PipetteTool.ts +56 -0
  133. package/src/tools/SelectionTool.test.ts +2 -4
  134. package/src/tools/SelectionTool.ts +47 -27
  135. package/src/tools/TextTool.ts +1 -1
  136. package/src/tools/ToolController.ts +10 -6
  137. package/src/tools/UndoRedoShortcut.test.ts +1 -1
  138. package/src/tools/localization.ts +6 -3
  139. package/src/types.ts +12 -1
@@ -0,0 +1,28 @@
1
+ import Command from './Command';
2
+ export default class SerializableCommand extends Command {
3
+ constructor(commandTypeId) {
4
+ super();
5
+ this.commandTypeId = commandTypeId;
6
+ if (!(commandTypeId in SerializableCommand.deserializationCallbacks)) {
7
+ throw new Error(`Command ${commandTypeId} must have a registered deserialization callback. To do this, call SerializableCommand.register.`);
8
+ }
9
+ }
10
+ serialize() {
11
+ return JSON.stringify({
12
+ data: this.serializeToString(),
13
+ commandType: this.commandTypeId,
14
+ });
15
+ }
16
+ static deserialize(data, editor) {
17
+ const json = JSON.parse(data);
18
+ const commandType = json.commandType;
19
+ if (!(commandType in SerializableCommand.deserializationCallbacks)) {
20
+ throw new Error(`Unrecognised command type ${commandType}!`);
21
+ }
22
+ return SerializableCommand.deserializationCallbacks[commandType](json.data, editor);
23
+ }
24
+ static register(commandTypeId, deserialize) {
25
+ SerializableCommand.deserializationCallbacks[commandTypeId] = deserialize;
26
+ }
27
+ }
28
+ SerializableCommand.deserializationCallbacks = {};
@@ -8,12 +8,14 @@ export interface CommandLocalization {
8
8
  zoomedOut: string;
9
9
  zoomedIn: string;
10
10
  erasedNoElements: string;
11
+ duplicatedNoElements: string;
11
12
  elements: string;
12
13
  updatedViewport: string;
13
14
  transformedElements: (elemCount: number) => string;
14
15
  resizeOutputCommand: (newSize: Rect2) => string;
15
16
  addElementAction: (elemDescription: string) => string;
16
17
  eraseAction: (elemDescription: string, numElems: number) => string;
18
+ duplicateAction: (elemDescription: string, count: number) => string;
17
19
  selectedElements: (count: number) => string;
18
20
  }
19
21
  export declare const defaultCommandLocalization: CommandLocalization;
@@ -4,8 +4,10 @@ export const defaultCommandLocalization = {
4
4
  resizeOutputCommand: (newSize) => `Resized image to ${newSize.w}x${newSize.h}`,
5
5
  addElementAction: (componentDescription) => `Added ${componentDescription}`,
6
6
  eraseAction: (componentDescription, numElems) => `Erased ${numElems} ${componentDescription}`,
7
+ duplicateAction: (componentDescription, numElems) => `Duplicated ${numElems} ${componentDescription}`,
7
8
  elements: 'Elements',
8
9
  erasedNoElements: 'Erased nothing',
10
+ duplicatedNoElements: 'Duplicated nothing',
9
11
  rotatedBy: (degrees) => `Rotated by ${Math.abs(degrees)} degrees ${degrees < 0 ? 'clockwise' : 'counter-clockwise'}`,
10
12
  movedLeft: 'Moved left',
11
13
  movedUp: 'Moved up',
@@ -4,14 +4,20 @@ import Mat33 from '../geometry/Mat33';
4
4
  import Rect2 from '../geometry/Rect2';
5
5
  import AbstractRenderer from '../rendering/renderers/AbstractRenderer';
6
6
  import { ImageComponentLocalization } from './localization';
7
- declare type LoadSaveData = unknown;
7
+ 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
10
  export default abstract class AbstractComponent {
11
+ private readonly componentKind;
10
12
  protected lastChangedTime: number;
11
13
  protected abstract contentBBox: Rect2;
12
14
  private zIndex;
15
+ private id;
13
16
  private static zIndexCounter;
14
- protected constructor();
17
+ protected constructor(componentKind: string);
18
+ getId(): string;
19
+ private static deserializationCallbacks;
20
+ static registerComponent(componentKind: string, deserialize: DeserializeCallback | null): void;
15
21
  private loadSaveData;
16
22
  attachLoadSaveData(key: string, data: LoadSaveData): void;
17
23
  getLoadSaveData(): LoadSaveDataTable;
@@ -19,8 +25,15 @@ export default abstract class AbstractComponent {
19
25
  getBBox(): Rect2;
20
26
  abstract render(canvas: AbstractRenderer, visibleRect?: Rect2): void;
21
27
  abstract intersects(lineSegment: LineSegment2): boolean;
28
+ protected abstract serializeToString(): string | null;
22
29
  protected abstract applyTransformation(affineTransfm: Mat33): void;
23
30
  transformBy(affineTransfm: Mat33): Command;
31
+ private static TransformElementCommand;
24
32
  abstract description(localizationTable: ImageComponentLocalization): string;
33
+ protected abstract createClone(): AbstractComponent;
34
+ clone(): AbstractComponent;
35
+ serialize(): string;
36
+ private static isNotDeserializable;
37
+ static deserialize(data: string): AbstractComponent;
25
38
  }
26
39
  export {};
@@ -1,10 +1,30 @@
1
+ var _a;
2
+ import SerializableCommand from '../commands/SerializableCommand';
1
3
  import EditorImage from '../EditorImage';
4
+ import Mat33 from '../geometry/Mat33';
2
5
  export default class AbstractComponent {
3
- constructor() {
6
+ constructor(
7
+ // A unique identifier for the type of component
8
+ componentKind) {
9
+ this.componentKind = componentKind;
4
10
  // Get and manage data attached by a loader.
5
11
  this.loadSaveData = {};
6
12
  this.lastChangedTime = (new Date()).getTime();
7
13
  this.zIndex = AbstractComponent.zIndexCounter++;
14
+ // Create a unique ID.
15
+ this.id = `${new Date().getTime()}-${Math.random()}`;
16
+ if (AbstractComponent.deserializationCallbacks[componentKind] === undefined) {
17
+ throw new Error(`Component ${componentKind} has not been registered using AbstractComponent.registerComponent`);
18
+ }
19
+ }
20
+ getId() {
21
+ return this.id;
22
+ }
23
+ // Store the deserialization callback (or lack of it) for [componentKind].
24
+ // If components are registered multiple times (as may be done in automated tests),
25
+ // the most recent deserialization callback is used.
26
+ static registerComponent(componentKind, deserialize) {
27
+ this.deserializationCallbacks[componentKind] = deserialize !== null && deserialize !== void 0 ? deserialize : null;
8
28
  }
9
29
  attachLoadSaveData(key, data) {
10
30
  if (!this.loadSaveData[key]) {
@@ -24,37 +44,113 @@ export default class AbstractComponent {
24
44
  // Returns a command that, when applied, transforms this by [affineTransfm] and
25
45
  // updates the editor.
26
46
  transformBy(affineTransfm) {
27
- const updateTransform = (editor, newTransfm) => {
47
+ return new AbstractComponent.TransformElementCommand(affineTransfm, this);
48
+ }
49
+ clone() {
50
+ const clone = this.createClone();
51
+ for (const attachmentKey in this.loadSaveData) {
52
+ for (const val of this.loadSaveData[attachmentKey]) {
53
+ clone.attachLoadSaveData(attachmentKey, val);
54
+ }
55
+ }
56
+ return clone;
57
+ }
58
+ serialize() {
59
+ const data = this.serializeToString();
60
+ if (data === null) {
61
+ throw new Error(`${this} cannot be serialized.`);
62
+ }
63
+ return JSON.stringify({
64
+ name: this.componentKind,
65
+ zIndex: this.zIndex,
66
+ id: this.id,
67
+ loadSaveData: this.loadSaveData,
68
+ data,
69
+ });
70
+ }
71
+ // Returns true if [data] is not deserializable. May return false even if [data]
72
+ // is not deserializable.
73
+ static isNotDeserializable(data) {
74
+ const json = JSON.parse(data);
75
+ if (typeof json !== 'object') {
76
+ return true;
77
+ }
78
+ if (!this.deserializationCallbacks[json === null || json === void 0 ? void 0 : json.name]) {
79
+ return true;
80
+ }
81
+ if (!json.data) {
82
+ return true;
83
+ }
84
+ return false;
85
+ }
86
+ static deserialize(data) {
87
+ if (AbstractComponent.isNotDeserializable(data)) {
88
+ throw new Error(`Element with data ${data} cannot be deserialized.`);
89
+ }
90
+ const json = JSON.parse(data);
91
+ const instance = this.deserializationCallbacks[json.name](json.data);
92
+ instance.zIndex = json.zIndex;
93
+ instance.id = json.id;
94
+ // TODO: What should we do with json.loadSaveData?
95
+ // If we attach it to [instance], we create a potential security risk — loadSaveData
96
+ // is often used to store unrecognised attributes so they can be preserved on output.
97
+ // ...but what if we're deserializing data sent across the network?
98
+ return instance;
99
+ }
100
+ }
101
+ // Topmost z-index
102
+ AbstractComponent.zIndexCounter = 0;
103
+ AbstractComponent.deserializationCallbacks = {};
104
+ AbstractComponent.TransformElementCommand = (_a = class extends SerializableCommand {
105
+ constructor(affineTransfm, component) {
106
+ super('transform-element');
107
+ this.affineTransfm = affineTransfm;
108
+ this.component = component;
109
+ this.origZIndex = component.zIndex;
110
+ }
111
+ updateTransform(editor, newTransfm) {
28
112
  // Any parent should have only one direct child.
29
- const parent = editor.image.findParent(this);
113
+ const parent = editor.image.findParent(this.component);
30
114
  let hadParent = false;
31
115
  if (parent) {
32
116
  parent.remove();
33
117
  hadParent = true;
34
118
  }
35
- this.applyTransformation(newTransfm);
119
+ this.component.applyTransformation(newTransfm);
36
120
  // Add the element back to the document.
37
121
  if (hadParent) {
38
- new EditorImage.AddElementCommand(this).apply(editor);
122
+ EditorImage.addElement(this.component).apply(editor);
39
123
  }
40
- };
41
- const origZIndex = this.zIndex;
42
- return {
43
- apply: (editor) => {
44
- this.zIndex = AbstractComponent.zIndexCounter++;
45
- updateTransform(editor, affineTransfm);
46
- editor.queueRerender();
47
- },
48
- unapply: (editor) => {
49
- this.zIndex = origZIndex;
50
- updateTransform(editor, affineTransfm.inverse());
51
- editor.queueRerender();
52
- },
53
- description(localizationTable) {
54
- return localizationTable.transformedElements(1);
55
- },
56
- };
57
- }
58
- }
59
- // Topmost z-index
60
- AbstractComponent.zIndexCounter = 0;
124
+ }
125
+ apply(editor) {
126
+ this.component.zIndex = AbstractComponent.zIndexCounter++;
127
+ this.updateTransform(editor, this.affineTransfm);
128
+ editor.queueRerender();
129
+ }
130
+ unapply(editor) {
131
+ this.component.zIndex = this.origZIndex;
132
+ this.updateTransform(editor, this.affineTransfm.inverse());
133
+ editor.queueRerender();
134
+ }
135
+ description(localizationTable) {
136
+ return localizationTable.transformedElements(1);
137
+ }
138
+ serializeToString() {
139
+ return JSON.stringify({
140
+ id: this.component.getId(),
141
+ transfm: this.affineTransfm.toArray(),
142
+ });
143
+ }
144
+ },
145
+ (() => {
146
+ SerializableCommand.register('transform-element', (data, editor) => {
147
+ const json = JSON.parse(data);
148
+ const elem = editor.image.lookupElement(json.id);
149
+ if (!elem) {
150
+ throw new Error(`Unable to retrieve non-existent element, ${elem}`);
151
+ }
152
+ const transform = json.transfm;
153
+ return new AbstractComponent.TransformElementCommand(new Mat33(...transform), elem);
154
+ });
155
+ })(),
156
+ _a);
@@ -4,12 +4,17 @@ import Rect2 from '../geometry/Rect2';
4
4
  import AbstractRenderer from '../rendering/renderers/AbstractRenderer';
5
5
  import AbstractComponent from './AbstractComponent';
6
6
  import { ImageComponentLocalization } from './localization';
7
+ declare type GlobalAttrsList = Array<[string, string | null]>;
7
8
  export default class SVGGlobalAttributesObject extends AbstractComponent {
8
9
  private readonly attrs;
9
10
  protected contentBBox: Rect2;
10
- constructor(attrs: Array<[string, string | null]>);
11
+ constructor(attrs: GlobalAttrsList);
11
12
  render(canvas: AbstractRenderer, _visibleRect?: Rect2): void;
12
13
  intersects(_lineSegment: LineSegment2): boolean;
13
14
  protected applyTransformation(_affineTransfm: Mat33): void;
15
+ protected createClone(): SVGGlobalAttributesObject;
14
16
  description(localization: ImageComponentLocalization): string;
17
+ protected serializeToString(): string | null;
18
+ static deserializeFromString(data: string): AbstractComponent;
15
19
  }
20
+ export {};
@@ -1,10 +1,11 @@
1
1
  import Rect2 from '../geometry/Rect2';
2
2
  import SVGRenderer from '../rendering/renderers/SVGRenderer';
3
3
  import AbstractComponent from './AbstractComponent';
4
+ const componentKind = 'svg-global-attributes';
4
5
  // Stores global SVG attributes (e.g. namespace identifiers.)
5
6
  export default class SVGGlobalAttributesObject extends AbstractComponent {
6
7
  constructor(attrs) {
7
- super();
8
+ super(componentKind);
8
9
  this.attrs = attrs;
9
10
  this.contentBBox = Rect2.empty;
10
11
  }
@@ -22,7 +23,28 @@ export default class SVGGlobalAttributesObject extends AbstractComponent {
22
23
  }
23
24
  applyTransformation(_affineTransfm) {
24
25
  }
26
+ createClone() {
27
+ return new SVGGlobalAttributesObject(this.attrs);
28
+ }
25
29
  description(localization) {
26
30
  return localization.svgObject;
27
31
  }
32
+ serializeToString() {
33
+ return JSON.stringify(this.attrs);
34
+ }
35
+ static deserializeFromString(data) {
36
+ const json = JSON.parse(data);
37
+ const attrs = [];
38
+ const numericAndSpaceContentExp = /^[ \t\n0-9.-eE]+$/;
39
+ // Don't deserialize all attributes, just those that should be safe.
40
+ for (const [key, val] of json) {
41
+ if (key === 'viewBox' || key === 'width' || key === 'height') {
42
+ if (val && numericAndSpaceContentExp.exec(val)) {
43
+ attrs.push([key, val]);
44
+ }
45
+ }
46
+ }
47
+ return new SVGGlobalAttributesObject(attrs);
48
+ }
28
49
  }
50
+ AbstractComponent.registerComponent(componentKind, SVGGlobalAttributesObject.deserializeFromString);
@@ -1,5 +1,6 @@
1
1
  import LineSegment2 from '../geometry/LineSegment2';
2
2
  import Mat33 from '../geometry/Mat33';
3
+ import Path from '../geometry/Path';
3
4
  import Rect2 from '../geometry/Rect2';
4
5
  import AbstractRenderer, { RenderablePathSpec } from '../rendering/renderers/AbstractRenderer';
5
6
  import AbstractComponent from './AbstractComponent';
@@ -12,5 +13,9 @@ export default class Stroke extends AbstractComponent {
12
13
  render(canvas: AbstractRenderer, visibleRect?: Rect2): void;
13
14
  private bboxForPart;
14
15
  protected applyTransformation(affineTransfm: Mat33): void;
16
+ getPath(): Path;
15
17
  description(localization: ImageComponentLocalization): string;
18
+ protected createClone(): AbstractComponent;
19
+ protected serializeToString(): string | null;
20
+ static deserializeFromString(data: string): Stroke;
16
21
  }
@@ -1,10 +1,11 @@
1
1
  import Path from '../geometry/Path';
2
2
  import Rect2 from '../geometry/Rect2';
3
+ import { styleFromJSON, styleToJSON } from '../rendering/RenderingStyle';
3
4
  import AbstractComponent from './AbstractComponent';
4
5
  export default class Stroke extends AbstractComponent {
5
6
  constructor(parts) {
6
7
  var _a;
7
- super();
8
+ super('stroke');
8
9
  this.parts = parts.map(section => {
9
10
  const path = Path.fromRenderable(section);
10
11
  const pathBBox = this.bboxForPart(path.bbox, section.style);
@@ -73,7 +74,37 @@ export default class Stroke extends AbstractComponent {
73
74
  };
74
75
  });
75
76
  }
77
+ getPath() {
78
+ var _a;
79
+ return (_a = this.parts.reduce((accumulator, current) => {
80
+ var _a;
81
+ return (_a = accumulator === null || accumulator === void 0 ? void 0 : accumulator.union(current.path)) !== null && _a !== void 0 ? _a : current.path;
82
+ }, null)) !== null && _a !== void 0 ? _a : Path.empty;
83
+ }
76
84
  description(localization) {
77
85
  return localization.stroke;
78
86
  }
87
+ createClone() {
88
+ return new Stroke(this.parts);
89
+ }
90
+ serializeToString() {
91
+ return JSON.stringify(this.parts.map(part => {
92
+ return {
93
+ style: styleToJSON(part.style),
94
+ path: part.path.serialize(),
95
+ };
96
+ }));
97
+ }
98
+ static deserializeFromString(data) {
99
+ const json = JSON.parse(data);
100
+ 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.`);
102
+ }
103
+ const pathSpec = json.map((part) => {
104
+ const style = styleFromJSON(part.style);
105
+ return Path.fromString(part.path).toRenderable(style);
106
+ });
107
+ return new Stroke(pathSpec);
108
+ }
79
109
  }
110
+ AbstractComponent.registerComponent('stroke', Stroke.deserializeFromString);
@@ -1,7 +1,8 @@
1
1
  import LineSegment2 from '../geometry/LineSegment2';
2
2
  import Mat33 from '../geometry/Mat33';
3
3
  import Rect2 from '../geometry/Rect2';
4
- import AbstractRenderer, { RenderingStyle } from '../rendering/renderers/AbstractRenderer';
4
+ import AbstractRenderer from '../rendering/renderers/AbstractRenderer';
5
+ import RenderingStyle from '../rendering/RenderingStyle';
5
6
  import AbstractComponent from './AbstractComponent';
6
7
  import { ImageComponentLocalization } from './localization';
7
8
  export interface TextStyle {
@@ -11,12 +12,14 @@ export interface TextStyle {
11
12
  fontVariant?: string;
12
13
  renderingStyle: RenderingStyle;
13
14
  }
15
+ declare type GetTextDimensCallback = (text: string, style: TextStyle) => Rect2;
14
16
  export default class Text extends AbstractComponent {
15
- protected textObjects: Array<string | Text>;
17
+ protected readonly textObjects: Array<string | Text>;
16
18
  private transform;
17
- private style;
19
+ private readonly style;
20
+ private readonly getTextDimens;
18
21
  protected contentBBox: Rect2;
19
- constructor(textObjects: Array<string | Text>, transform: Mat33, style: TextStyle);
22
+ constructor(textObjects: Array<string | Text>, transform: Mat33, style: TextStyle, getTextDimens?: GetTextDimensCallback);
20
23
  static applyTextStyles(ctx: CanvasRenderingContext2D, style: TextStyle): void;
21
24
  private static textMeasuringCtx;
22
25
  private static getTextDimens;
@@ -25,6 +28,10 @@ export default class Text extends AbstractComponent {
25
28
  render(canvas: AbstractRenderer, _visibleRect?: Rect2): void;
26
29
  intersects(lineSegment: LineSegment2): boolean;
27
30
  protected applyTransformation(affineTransfm: Mat33): void;
31
+ protected createClone(): AbstractComponent;
28
32
  private getText;
29
33
  description(localizationTable: ImageComponentLocalization): string;
34
+ protected serializeToString(): string;
35
+ static deserializeFromString(data: string, getTextDimens?: GetTextDimensCallback): Text;
30
36
  }
37
+ export {};
@@ -1,12 +1,18 @@
1
1
  import LineSegment2 from '../geometry/LineSegment2';
2
+ import Mat33 from '../geometry/Mat33';
2
3
  import Rect2 from '../geometry/Rect2';
4
+ import { styleFromJSON, styleToJSON } from '../rendering/RenderingStyle';
3
5
  import AbstractComponent from './AbstractComponent';
6
+ const componentTypeId = 'text';
4
7
  export default class Text extends AbstractComponent {
5
- constructor(textObjects, transform, style) {
6
- super();
8
+ constructor(textObjects, transform, style,
9
+ // If not given, an HtmlCanvasElement is used to determine text boundaries.
10
+ getTextDimens = Text.getTextDimens) {
11
+ super(componentTypeId);
7
12
  this.textObjects = textObjects;
8
13
  this.transform = transform;
9
14
  this.style = style;
15
+ this.getTextDimens = getTextDimens;
10
16
  this.recomputeBBox();
11
17
  }
12
18
  static applyTextStyles(ctx, style) {
@@ -34,7 +40,7 @@ export default class Text extends AbstractComponent {
34
40
  }
35
41
  computeBBoxOfPart(part) {
36
42
  if (typeof part === 'string') {
37
- const textBBox = Text.getTextDimens(part, this.style);
43
+ const textBBox = this.getTextDimens(part, this.style);
38
44
  return textBBox.transformedBoundingBox(this.transform);
39
45
  }
40
46
  else {
@@ -93,6 +99,9 @@ export default class Text extends AbstractComponent {
93
99
  this.transform = affineTransfm.rightMul(this.transform);
94
100
  this.recomputeBBox();
95
101
  }
102
+ createClone() {
103
+ return new Text(this.textObjects, this.transform, this.style);
104
+ }
96
105
  getText() {
97
106
  const result = [];
98
107
  for (const textObject of this.textObjects) {
@@ -108,4 +117,49 @@ export default class Text extends AbstractComponent {
108
117
  description(localizationTable) {
109
118
  return localizationTable.text(this.getText());
110
119
  }
120
+ serializeToString() {
121
+ const serializableStyle = Object.assign(Object.assign({}, this.style), { renderingStyle: styleToJSON(this.style.renderingStyle) });
122
+ const textObjects = this.textObjects.map(text => {
123
+ if (typeof text === 'string') {
124
+ return {
125
+ text,
126
+ };
127
+ }
128
+ else {
129
+ return {
130
+ json: text.serializeToString(),
131
+ };
132
+ }
133
+ });
134
+ return JSON.stringify({
135
+ textObjects,
136
+ transform: this.transform.toArray(),
137
+ style: serializableStyle,
138
+ });
139
+ }
140
+ static deserializeFromString(data, getTextDimens = Text.getTextDimens) {
141
+ const json = JSON.parse(data);
142
+ const style = {
143
+ renderingStyle: styleFromJSON(json.style.renderingStyle),
144
+ size: json.style.size,
145
+ fontWeight: json.style.fontWeight,
146
+ fontVariant: json.style.fontVariant,
147
+ fontFamily: json.style.fontFamily,
148
+ };
149
+ const textObjects = json.textObjects.map((data) => {
150
+ var _a;
151
+ if (((_a = data.text) !== null && _a !== void 0 ? _a : null) !== null) {
152
+ return data.text;
153
+ }
154
+ return Text.deserializeFromString(data.json);
155
+ });
156
+ json.transform = json.transform.filter((elem) => typeof elem === 'number');
157
+ if (json.transform.length !== 9) {
158
+ throw new Error(`Unable to deserialize transform, ${json.transform}.`);
159
+ }
160
+ const transformData = json.transform;
161
+ const transform = new Mat33(...transformData);
162
+ return new Text(textObjects, transform, style, getTextDimens);
163
+ }
111
164
  }
165
+ AbstractComponent.registerComponent(componentTypeId, (data) => Text.deserializeFromString(data));
@@ -11,5 +11,7 @@ export default class UnknownSVGObject extends AbstractComponent {
11
11
  render(canvas: AbstractRenderer, _visibleRect?: Rect2): void;
12
12
  intersects(lineSegment: LineSegment2): boolean;
13
13
  protected applyTransformation(_affineTransfm: Mat33): void;
14
+ protected createClone(): AbstractComponent;
14
15
  description(localization: ImageComponentLocalization): string;
16
+ protected serializeToString(): string | null;
15
17
  }
@@ -1,9 +1,10 @@
1
1
  import Rect2 from '../geometry/Rect2';
2
2
  import SVGRenderer from '../rendering/renderers/SVGRenderer';
3
3
  import AbstractComponent from './AbstractComponent';
4
+ const componentId = 'unknown-svg-object';
4
5
  export default class UnknownSVGObject extends AbstractComponent {
5
6
  constructor(svgObject) {
6
- super();
7
+ super(componentId);
7
8
  this.svgObject = svgObject;
8
9
  this.contentBBox = Rect2.of(svgObject.getBoundingClientRect());
9
10
  }
@@ -19,7 +20,17 @@ export default class UnknownSVGObject extends AbstractComponent {
19
20
  }
20
21
  applyTransformation(_affineTransfm) {
21
22
  }
23
+ createClone() {
24
+ return new UnknownSVGObject(this.svgObject.cloneNode(true));
25
+ }
22
26
  description(localization) {
23
27
  return localization.svgObject;
24
28
  }
29
+ serializeToString() {
30
+ return JSON.stringify({
31
+ html: this.svgObject.outerHTML,
32
+ });
33
+ }
25
34
  }
35
+ // null: Do not deserialize UnknownSVGObjects.
36
+ AbstractComponent.registerComponent(componentId, null);
@@ -1,6 +1,7 @@
1
1
  import Rect2 from '../../geometry/Rect2';
2
2
  import AbstractRenderer from '../../rendering/renderers/AbstractRenderer';
3
3
  import { StrokeDataPoint } from '../../types';
4
+ import Viewport from '../../Viewport';
4
5
  import AbstractComponent from '../AbstractComponent';
5
6
  import { ComponentBuilder, ComponentBuilderFactory } from './types';
6
7
  export declare const makeFilledRectangleBuilder: ComponentBuilderFactory;
@@ -8,8 +9,9 @@ export declare const makeOutlinedRectangleBuilder: ComponentBuilderFactory;
8
9
  export default class RectangleBuilder implements ComponentBuilder {
9
10
  private readonly startPoint;
10
11
  private filled;
12
+ private viewport;
11
13
  private endPoint;
12
- constructor(startPoint: StrokeDataPoint, filled: boolean);
14
+ constructor(startPoint: StrokeDataPoint, filled: boolean, viewport: Viewport);
13
15
  getBBox(): Rect2;
14
16
  private buildPreview;
15
17
  build(): AbstractComponent;
@@ -1,16 +1,18 @@
1
+ import Mat33 from '../../geometry/Mat33';
1
2
  import Path from '../../geometry/Path';
2
3
  import Rect2 from '../../geometry/Rect2';
3
4
  import Stroke from '../Stroke';
4
- export const makeFilledRectangleBuilder = (initialPoint, _viewport) => {
5
- return new RectangleBuilder(initialPoint, true);
5
+ export const makeFilledRectangleBuilder = (initialPoint, viewport) => {
6
+ return new RectangleBuilder(initialPoint, true, viewport);
6
7
  };
7
- export const makeOutlinedRectangleBuilder = (initialPoint, _viewport) => {
8
- return new RectangleBuilder(initialPoint, false);
8
+ export const makeOutlinedRectangleBuilder = (initialPoint, viewport) => {
9
+ return new RectangleBuilder(initialPoint, false, viewport);
9
10
  };
10
11
  export default class RectangleBuilder {
11
- constructor(startPoint, filled) {
12
+ constructor(startPoint, filled, viewport) {
12
13
  this.startPoint = startPoint;
13
14
  this.filled = filled;
15
+ this.viewport = viewport;
14
16
  // Initially, the start and end points are the same.
15
17
  this.endPoint = startPoint;
16
18
  }
@@ -19,9 +21,16 @@ export default class RectangleBuilder {
19
21
  return preview.getBBox();
20
22
  }
21
23
  buildPreview() {
22
- const startPoint = this.startPoint.pos;
23
- const endPoint = this.endPoint.pos;
24
- const path = Path.fromRect(Rect2.fromCorners(startPoint, endPoint), this.filled ? null : this.endPoint.width);
24
+ const canvasAngle = this.viewport.getRotationAngle();
25
+ const rotationMat = Mat33.zRotation(-canvasAngle);
26
+ // Adjust startPoint and endPoint such that applying [rotationMat] to them
27
+ // brings them to this.startPoint and this.endPoint.
28
+ const startPoint = rotationMat.inverse().transformVec2(this.startPoint.pos);
29
+ const endPoint = rotationMat.inverse().transformVec2(this.endPoint.pos);
30
+ const rect = Rect2.fromCorners(startPoint, endPoint);
31
+ const path = Path.fromRect(rect, this.filled ? null : this.endPoint.width).transformedBy(
32
+ // Rotate the canvas rectangle so that its rotation matches the screen
33
+ rotationMat);
25
34
  const preview = new Stroke([
26
35
  path.toRenderable({
27
36
  fill: this.endPoint.color
@@ -0,0 +1,4 @@
1
+ import AbstractComponent from '../AbstractComponent';
2
+ import { ImageComponentLocalization } from '../localization';
3
+ declare const _default: (localizationTable: ImageComponentLocalization, elems: AbstractComponent[]) => string | null;
4
+ export default _default;
@@ -0,0 +1,14 @@
1
+ // Returns the description of all given elements, if identical, otherwise,
2
+ // returns null.
3
+ export default (localizationTable, elems) => {
4
+ if (elems.length === 0) {
5
+ return null;
6
+ }
7
+ const description = elems[0].description(localizationTable);
8
+ for (const elem of elems) {
9
+ if (elem.description(localizationTable) !== description) {
10
+ return null;
11
+ }
12
+ }
13
+ return description;
14
+ };