js-draw 0.14.0 → 0.15.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 (90) hide show
  1. package/.github/ISSUE_TEMPLATE/translation.yml +24 -0
  2. package/CHANGELOG.md +14 -1
  3. package/dist/bundle.js +1 -1
  4. package/dist/src/Color4.d.ts +4 -0
  5. package/dist/src/Color4.js +22 -0
  6. package/dist/src/Editor.d.ts +2 -1
  7. package/dist/src/Editor.js +10 -1
  8. package/dist/src/EditorImage.d.ts +1 -0
  9. package/dist/src/EditorImage.js +11 -0
  10. package/dist/src/commands/UnresolvedCommand.d.ts +14 -0
  11. package/dist/src/commands/UnresolvedCommand.js +22 -0
  12. package/dist/src/commands/uniteCommands.js +4 -2
  13. package/dist/src/components/AbstractComponent.d.ts +0 -1
  14. package/dist/src/components/AbstractComponent.js +36 -50
  15. package/dist/src/components/RestylableComponent.d.ts +24 -0
  16. package/dist/src/components/RestylableComponent.js +80 -0
  17. package/dist/src/components/Stroke.d.ts +8 -1
  18. package/dist/src/components/Stroke.js +49 -1
  19. package/dist/src/components/TextComponent.d.ts +10 -10
  20. package/dist/src/components/TextComponent.js +46 -13
  21. package/dist/src/components/lib.d.ts +2 -1
  22. package/dist/src/components/lib.js +2 -1
  23. package/dist/src/components/localization.d.ts +1 -0
  24. package/dist/src/components/localization.js +1 -0
  25. package/dist/src/rendering/TextRenderingStyle.d.ts +23 -0
  26. package/dist/src/rendering/TextRenderingStyle.js +20 -0
  27. package/dist/src/rendering/renderers/AbstractRenderer.d.ts +1 -1
  28. package/dist/src/rendering/renderers/CanvasRenderer.d.ts +1 -1
  29. package/dist/src/rendering/renderers/DummyRenderer.d.ts +1 -1
  30. package/dist/src/rendering/renderers/SVGRenderer.d.ts +1 -1
  31. package/dist/src/rendering/renderers/TextOnlyRenderer.d.ts +1 -1
  32. package/dist/src/toolbar/IconProvider.d.ts +2 -1
  33. package/dist/src/toolbar/IconProvider.js +10 -0
  34. package/dist/src/toolbar/localization.d.ts +1 -0
  35. package/dist/src/toolbar/localization.js +1 -0
  36. package/dist/src/toolbar/widgets/BaseWidget.js +10 -4
  37. package/dist/src/toolbar/widgets/InsertImageWidget.js +2 -1
  38. package/dist/src/toolbar/widgets/SelectionToolWidget.js +77 -1
  39. package/dist/src/tools/Pen.js +2 -2
  40. package/dist/src/tools/SelectionTool/SelectAllShortcutHandler.d.ts +8 -0
  41. package/dist/src/tools/SelectionTool/SelectAllShortcutHandler.js +22 -0
  42. package/dist/src/tools/SelectionTool/Selection.js +1 -1
  43. package/dist/src/tools/SelectionTool/SelectionTool.js +7 -10
  44. package/dist/src/tools/TextTool.d.ts +1 -1
  45. package/dist/src/tools/ToolController.js +2 -0
  46. package/dist/src/tools/lib.d.ts +1 -0
  47. package/dist/src/tools/lib.js +1 -0
  48. package/dist/src/tools/localization.d.ts +1 -0
  49. package/dist/src/tools/localization.js +1 -0
  50. package/package.json +1 -1
  51. package/src/Color4.test.ts +4 -0
  52. package/src/Color4.ts +26 -0
  53. package/src/Editor.toSVG.test.ts +1 -1
  54. package/src/Editor.ts +12 -1
  55. package/src/EditorImage.ts +13 -0
  56. package/src/SVGLoader.ts +2 -1
  57. package/src/commands/UnresolvedCommand.ts +37 -0
  58. package/src/commands/uniteCommands.ts +5 -2
  59. package/src/components/AbstractComponent.transformBy.test.ts +22 -0
  60. package/src/components/AbstractComponent.ts +41 -59
  61. package/src/components/RestylableComponent.ts +142 -0
  62. package/src/components/Stroke.test.ts +68 -0
  63. package/src/components/Stroke.ts +68 -2
  64. package/src/components/TextComponent.test.ts +56 -2
  65. package/src/components/TextComponent.ts +63 -25
  66. package/src/components/lib.ts +4 -1
  67. package/src/components/localization.ts +3 -0
  68. package/src/math/Rect2.test.ts +18 -6
  69. package/src/rendering/TextRenderingStyle.ts +38 -0
  70. package/src/rendering/renderers/AbstractRenderer.ts +1 -1
  71. package/src/rendering/renderers/CanvasRenderer.ts +2 -1
  72. package/src/rendering/renderers/DummyRenderer.ts +1 -1
  73. package/src/rendering/renderers/SVGRenderer.ts +1 -1
  74. package/src/rendering/renderers/TextOnlyRenderer.ts +1 -1
  75. package/src/toolbar/IconProvider.ts +12 -1
  76. package/src/toolbar/localization.ts +2 -0
  77. package/src/toolbar/toolbar.css +7 -0
  78. package/src/toolbar/widgets/BaseWidget.ts +12 -4
  79. package/src/toolbar/widgets/InsertImageWidget.ts +2 -1
  80. package/src/toolbar/widgets/SelectionToolWidget.ts +95 -1
  81. package/src/tools/PanZoom.test.ts +2 -1
  82. package/src/tools/PasteHandler.ts +1 -1
  83. package/src/tools/Pen.ts +2 -2
  84. package/src/tools/SelectionTool/SelectAllShortcutHandler.ts +28 -0
  85. package/src/tools/SelectionTool/Selection.ts +1 -1
  86. package/src/tools/SelectionTool/SelectionTool.ts +6 -9
  87. package/src/tools/TextTool.ts +2 -1
  88. package/src/tools/ToolController.ts +2 -0
  89. package/src/tools/lib.ts +1 -0
  90. package/src/tools/localization.ts +2 -0
@@ -32,6 +32,10 @@ export default class Color4 {
32
32
  * ```
33
33
  */
34
34
  mix(other: Color4, fractionTo: number): Color4;
35
+ /**
36
+ * @returns the component-wise average of `colors`, or `Color4.transparent` if `colors` is empty.
37
+ */
38
+ static average(colors: Color4[]): Color4;
35
39
  private hexString;
36
40
  /**
37
41
  * @returns a hexadecimal color string representation of `this`, in the form `#rrggbbaa`.
@@ -121,6 +121,28 @@ export default class Color4 {
121
121
  const fractionOfThis = 1 - fractionTo;
122
122
  return new Color4(this.r * fractionOfThis + other.r * fractionTo, this.g * fractionOfThis + other.g * fractionTo, this.b * fractionOfThis + other.b * fractionTo, this.a * fractionOfThis + other.a * fractionTo);
123
123
  }
124
+ /**
125
+ * @returns the component-wise average of `colors`, or `Color4.transparent` if `colors` is empty.
126
+ */
127
+ static average(colors) {
128
+ let averageA = 0;
129
+ let averageR = 0;
130
+ let averageG = 0;
131
+ let averageB = 0;
132
+ for (const color of colors) {
133
+ averageA += color.a;
134
+ averageR += color.r;
135
+ averageG += color.g;
136
+ averageB += color.b;
137
+ }
138
+ if (colors.length > 0) {
139
+ averageA /= colors.length;
140
+ averageR /= colors.length;
141
+ averageG /= colors.length;
142
+ averageB /= colors.length;
143
+ }
144
+ return new Color4(averageR, averageG, averageB, averageA);
145
+ }
124
146
  /**
125
147
  * @returns a hexadecimal color string representation of `this`, in the form `#rrggbbaa`.
126
148
  *
@@ -192,7 +192,7 @@ export declare class Editor {
192
192
  */
193
193
  asyncApplyOrUnapplyCommands(commands: Command[], apply: boolean, updateChunkSize: number): Promise<void>;
194
194
  asyncApplyCommands(commands: Command[], chunkSize: number): Promise<void>;
195
- asyncUnapplyCommands(commands: Command[], chunkSize: number): Promise<void>;
195
+ asyncUnapplyCommands(commands: Command[], chunkSize: number, unapplyInReverseOrder?: boolean): Promise<void>;
196
196
  private announceUndoCallback;
197
197
  private announceRedoCallback;
198
198
  private nextRerenderListeners;
@@ -204,6 +204,7 @@ export declare class Editor {
204
204
  * @returns a promise that resolves when
205
205
  */
206
206
  queueRerender(): Promise<void>;
207
+ isRerenderQueued(): boolean;
207
208
  rerender(showImageBounds?: boolean): void;
208
209
  /**
209
210
  * @see {@link Display.getWetInkRenderer} {@link Display.flatten}
@@ -535,8 +535,13 @@ export class Editor {
535
535
  asyncApplyCommands(commands, chunkSize) {
536
536
  return this.asyncApplyOrUnapplyCommands(commands, true, chunkSize);
537
537
  }
538
+ // If `unapplyInReverseOrder`, commands are reversed before unapplying.
538
539
  // @see {@link #asyncApplyOrUnapplyCommands }
539
- asyncUnapplyCommands(commands, chunkSize) {
540
+ asyncUnapplyCommands(commands, chunkSize, unapplyInReverseOrder = false) {
541
+ if (unapplyInReverseOrder) {
542
+ commands = [...commands]; // copy
543
+ commands.reverse();
544
+ }
540
545
  return this.asyncApplyOrUnapplyCommands(commands, false, chunkSize);
541
546
  }
542
547
  /**
@@ -561,6 +566,10 @@ export class Editor {
561
566
  this.nextRerenderListeners.push(() => resolve());
562
567
  });
563
568
  }
569
+ // @internal
570
+ isRerenderQueued() {
571
+ return this.rerenderQueued;
572
+ }
564
573
  rerender(showImageBounds = true) {
565
574
  this.display.startRerender();
566
575
  // Don't render if the display has zero size.
@@ -10,6 +10,7 @@ export default class EditorImage {
10
10
  private componentsById;
11
11
  constructor();
12
12
  findParent(elem: AbstractComponent): ImageNode | null;
13
+ queueRerenderOf(elem: AbstractComponent): void;
13
14
  /** @internal */
14
15
  renderWithCache(screenRenderer: AbstractRenderer, cache: RenderingCache, viewport: Viewport): void;
15
16
  /** @internal */
@@ -23,6 +23,17 @@ export default class EditorImage {
23
23
  }
24
24
  return null;
25
25
  }
26
+ // Forces a re-render of `elem` when the image is next re-rendered as a whole.
27
+ // Does nothing if `elem` is not in this.
28
+ queueRerenderOf(elem) {
29
+ // TODO: Make more efficient (e.g. increase IDs of all parents,
30
+ // make cache take into account last modified time instead of IDs, etc.)
31
+ const parent = this.findParent(elem);
32
+ if (parent) {
33
+ parent.remove();
34
+ this.addElementDirectly(elem);
35
+ }
36
+ }
26
37
  /** @internal */
27
38
  renderWithCache(screenRenderer, cache, viewport) {
28
39
  cache.render(screenRenderer, this.root, viewport);
@@ -0,0 +1,14 @@
1
+ import EditorImage from '../EditorImage';
2
+ import AbstractComponent from '../components/AbstractComponent';
3
+ import SerializableCommand from './SerializableCommand';
4
+ export type ResolveFromComponentCallback = () => SerializableCommand;
5
+ /**
6
+ * A command that requires a component that may or may not be present in the editor when
7
+ * the command is created.
8
+ */
9
+ export default abstract class UnresolvedSerializableCommand extends SerializableCommand {
10
+ protected component: AbstractComponent | null;
11
+ protected readonly componentID: string;
12
+ protected constructor(commandId: string, componentID: string, component?: AbstractComponent);
13
+ protected resolveComponent(image: EditorImage): void;
14
+ }
@@ -0,0 +1,22 @@
1
+ import SerializableCommand from './SerializableCommand';
2
+ /**
3
+ * A command that requires a component that may or may not be present in the editor when
4
+ * the command is created.
5
+ */
6
+ export default class UnresolvedSerializableCommand extends SerializableCommand {
7
+ constructor(commandId, componentID, component) {
8
+ super(commandId);
9
+ this.component = component !== null && component !== void 0 ? component : null;
10
+ this.componentID = componentID;
11
+ }
12
+ resolveComponent(image) {
13
+ if (this.component) {
14
+ return;
15
+ }
16
+ const component = image.lookupElement(this.componentID);
17
+ if (!component) {
18
+ throw new Error(`Unable to resolve component with ID ${this.componentID}`);
19
+ }
20
+ this.component = component;
21
+ }
22
+ }
@@ -27,12 +27,14 @@ class NonSerializableUnion extends Command {
27
27
  }
28
28
  }
29
29
  unapply(editor) {
30
+ const commands = [...this.commands];
31
+ commands.reverse();
30
32
  if (this.applyChunkSize === undefined) {
31
- const results = this.commands.map(cmd => cmd.unapply(editor));
33
+ const results = commands.map(cmd => cmd.unapply(editor));
32
34
  return NonSerializableUnion.waitForAll(results);
33
35
  }
34
36
  else {
35
- return editor.asyncUnapplyCommands(this.commands, this.applyChunkSize);
37
+ return editor.asyncUnapplyCommands(commands, this.applyChunkSize, false);
36
38
  }
37
39
  }
38
40
  description(editor, localizationTable) {
@@ -48,7 +48,6 @@ export default abstract class AbstractComponent {
48
48
  isSelectable(): boolean;
49
49
  getProportionalRenderingTime(): number;
50
50
  private static transformElementCommandId;
51
- private static UnresolvedTransformElementCommand;
52
51
  private static TransformElementCommand;
53
52
  /**
54
53
  * @return a description that could be read by a screen reader
@@ -2,6 +2,7 @@ var _a;
2
2
  import SerializableCommand from '../commands/SerializableCommand';
3
3
  import EditorImage from '../EditorImage';
4
4
  import Mat33 from '../math/Mat33';
5
+ import UnresolvedSerializableCommand from '../commands/UnresolvedCommand';
5
6
  /**
6
7
  * A base class for everything that can be added to an {@link EditorImage}.
7
8
  */
@@ -75,11 +76,11 @@ export default class AbstractComponent {
75
76
  // Returns a command that, when applied, transforms this by [affineTransfm] and
76
77
  // updates the editor.
77
78
  transformBy(affineTransfm) {
78
- return new AbstractComponent.TransformElementCommand(affineTransfm, this);
79
+ return new AbstractComponent.TransformElementCommand(affineTransfm, this.getId(), this);
79
80
  }
80
81
  // Returns a command that updates this component's z-index.
81
82
  setZIndex(newZIndex) {
82
- return new AbstractComponent.TransformElementCommand(Mat33.identity, this, newZIndex);
83
+ return new AbstractComponent.TransformElementCommand(Mat33.identity, this.getId(), this, newZIndex, this.getZIndex());
83
84
  }
84
85
  // @returns true iff this component can be selected (e.g. by the selection tool.)
85
86
  isSelectable() {
@@ -158,52 +159,35 @@ export default class AbstractComponent {
158
159
  AbstractComponent.zIndexCounter = 0;
159
160
  AbstractComponent.deserializationCallbacks = {};
160
161
  AbstractComponent.transformElementCommandId = 'transform-element';
161
- AbstractComponent.UnresolvedTransformElementCommand = class extends SerializableCommand {
162
- constructor(affineTransfm, componentID, targetZIndex) {
163
- super(AbstractComponent.transformElementCommandId);
164
- this.affineTransfm = affineTransfm;
165
- this.componentID = componentID;
166
- this.targetZIndex = targetZIndex;
167
- this.command = null;
168
- }
169
- resolveCommand(editor) {
170
- if (this.command) {
171
- return;
172
- }
173
- const component = editor.image.lookupElement(this.componentID);
174
- if (!component) {
175
- throw new Error(`Unable to resolve component with ID ${this.componentID}`);
176
- }
177
- this.command = new AbstractComponent.TransformElementCommand(this.affineTransfm, component, this.targetZIndex);
178
- }
179
- apply(editor) {
180
- this.resolveCommand(editor);
181
- this.command.apply(editor);
182
- }
183
- unapply(editor) {
184
- this.resolveCommand(editor);
185
- this.command.unapply(editor);
186
- }
187
- description(_editor, localizationTable) {
188
- return localizationTable.transformedElements(1);
189
- }
190
- serializeToJSON() {
191
- return {
192
- id: this.componentID,
193
- transfm: this.affineTransfm.toArray(),
194
- targetZIndex: this.targetZIndex,
195
- };
196
- }
197
- };
198
- AbstractComponent.TransformElementCommand = (_a = class extends SerializableCommand {
199
- constructor(affineTransfm, component, targetZIndex) {
200
- super(AbstractComponent.transformElementCommandId);
162
+ AbstractComponent.TransformElementCommand = (_a = class extends UnresolvedSerializableCommand {
163
+ // Construct a new TransformElementCommand. `component`, while optional, should
164
+ // be provided if available. If not provided, it will be fetched from the editor's
165
+ // document when the command is applied.
166
+ constructor(affineTransfm, componentID, component, targetZIndex, origZIndex) {
167
+ super(AbstractComponent.transformElementCommandId, componentID, component);
201
168
  this.affineTransfm = affineTransfm;
202
- this.component = component;
203
- this.origZIndex = component.zIndex;
169
+ this.origZIndex = origZIndex;
204
170
  this.targetZIndex = targetZIndex !== null && targetZIndex !== void 0 ? targetZIndex : AbstractComponent.zIndexCounter++;
171
+ // Ensure that we keep drawing on top even after changing the z-index.
172
+ if (this.targetZIndex >= AbstractComponent.zIndexCounter) {
173
+ AbstractComponent.zIndexCounter = this.targetZIndex + 1;
174
+ }
175
+ if (component && origZIndex === undefined) {
176
+ this.origZIndex = component.getZIndex();
177
+ }
178
+ }
179
+ resolveComponent(image) {
180
+ var _a;
181
+ if (this.component) {
182
+ return;
183
+ }
184
+ super.resolveComponent(image);
185
+ (_a = this.origZIndex) !== null && _a !== void 0 ? _a : (this.origZIndex = this.component.getZIndex());
205
186
  }
206
187
  updateTransform(editor, newTransfm) {
188
+ if (!this.component) {
189
+ throw new Error('this.component is undefined or null!');
190
+ }
207
191
  // Any parent should have only one direct child.
208
192
  const parent = editor.image.findParent(this.component);
209
193
  let hadParent = false;
@@ -219,11 +203,13 @@ AbstractComponent.TransformElementCommand = (_a = class extends SerializableComm
219
203
  }
220
204
  }
221
205
  apply(editor) {
206
+ this.resolveComponent(editor.image);
222
207
  this.component.zIndex = this.targetZIndex;
223
208
  this.updateTransform(editor, this.affineTransfm);
224
209
  editor.queueRerender();
225
210
  }
226
211
  unapply(editor) {
212
+ this.resolveComponent(editor.image);
227
213
  this.component.zIndex = this.origZIndex;
228
214
  this.updateTransform(editor, this.affineTransfm.inverse());
229
215
  editor.queueRerender();
@@ -233,21 +219,21 @@ AbstractComponent.TransformElementCommand = (_a = class extends SerializableComm
233
219
  }
234
220
  serializeToJSON() {
235
221
  return {
236
- id: this.component.getId(),
222
+ id: this.componentID,
237
223
  transfm: this.affineTransfm.toArray(),
238
224
  targetZIndex: this.targetZIndex,
225
+ origZIndex: this.origZIndex,
239
226
  };
240
227
  }
241
228
  },
242
229
  (() => {
243
230
  SerializableCommand.register(AbstractComponent.transformElementCommandId, (json, editor) => {
244
- const elem = editor.image.lookupElement(json.id);
231
+ var _a, _b;
232
+ const elem = (_a = editor.image.lookupElement(json.id)) !== null && _a !== void 0 ? _a : undefined;
245
233
  const transform = new Mat33(...json.transfm);
246
234
  const targetZIndex = json.targetZIndex;
247
- if (!elem) {
248
- return new AbstractComponent.UnresolvedTransformElementCommand(transform, json.id, targetZIndex);
249
- }
250
- return new AbstractComponent.TransformElementCommand(transform, elem, targetZIndex);
235
+ const origZIndex = (_b = json.origZIndex) !== null && _b !== void 0 ? _b : undefined;
236
+ return new AbstractComponent.TransformElementCommand(transform, json.id, elem, targetZIndex, origZIndex);
251
237
  });
252
238
  })(),
253
239
  _a);
@@ -0,0 +1,24 @@
1
+ import Color4 from '../Color4';
2
+ import SerializableCommand from '../commands/SerializableCommand';
3
+ import Editor from '../Editor';
4
+ import TextStyle from '../rendering/TextRenderingStyle';
5
+ import AbstractComponent from './AbstractComponent';
6
+ export interface ComponentStyle {
7
+ color?: Color4;
8
+ textStyle?: TextStyle;
9
+ }
10
+ export declare const createRestyleComponentCommand: (initialStyle: ComponentStyle, newStyle: ComponentStyle, component: RestyleableComponent) => SerializableCommand;
11
+ export declare const isRestylableComponent: (component: AbstractComponent) => component is RestyleableComponent;
12
+ export interface RestyleableComponent extends AbstractComponent {
13
+ getStyle(): ComponentStyle;
14
+ updateStyle(style: ComponentStyle): SerializableCommand;
15
+ /**
16
+ * Set the style of this component in a way that can't be undone/redone
17
+ * (does not create a command).
18
+ *
19
+ * Prefer `updateStyle(style).apply(editor)`.
20
+ */
21
+ forceStyle(style: ComponentStyle, editor: Editor | null): void;
22
+ isRestylableComponent: true;
23
+ }
24
+ export default RestyleableComponent;
@@ -0,0 +1,80 @@
1
+ import Color4 from '../Color4';
2
+ import SerializableCommand from '../commands/SerializableCommand';
3
+ import UnresolvedSerializableCommand from '../commands/UnresolvedCommand';
4
+ import { textStyleFromJSON, textStyleToJSON } from '../rendering/TextRenderingStyle';
5
+ const serializeComponentStyle = (style) => {
6
+ const result = {};
7
+ if (style.color) {
8
+ result.color = style.color.toHexString();
9
+ }
10
+ if (style.textStyle) {
11
+ result.textStyle = textStyleToJSON(style.textStyle);
12
+ }
13
+ return result;
14
+ };
15
+ const deserializeComponentStyle = (json) => {
16
+ const color = json.color ? Color4.fromHex(json.color) : undefined;
17
+ const textStyle = json.textStyle ? textStyleFromJSON(json.textStyle) : undefined;
18
+ return {
19
+ color,
20
+ textStyle,
21
+ };
22
+ };
23
+ // For internal use by Components implementing `updateStyle`:
24
+ export const createRestyleComponentCommand = (initialStyle, newStyle, component) => {
25
+ return new DefaultRestyleComponentCommand(initialStyle, newStyle, component.getId(), component);
26
+ };
27
+ // Returns true if `component` is a `RestylableComponent`.
28
+ export const isRestylableComponent = (component) => {
29
+ const hasMethods = 'getStyle' in component && 'updateStyle' in component && 'forceStyle' in component;
30
+ if (!hasMethods) {
31
+ return false;
32
+ }
33
+ if (!('isRestylableComponent' in component) || !component['isRestylableComponent']) {
34
+ return false;
35
+ }
36
+ return true;
37
+ };
38
+ const defaultRestyleComponentCommandId = 'default-restyle-element';
39
+ class DefaultRestyleComponentCommand extends UnresolvedSerializableCommand {
40
+ constructor(originalStyle, newStyle, componentID, component) {
41
+ super(defaultRestyleComponentCommandId, componentID, component);
42
+ this.originalStyle = originalStyle;
43
+ this.newStyle = newStyle;
44
+ }
45
+ getComponent(editor) {
46
+ this.resolveComponent(editor.image);
47
+ const component = this.component;
48
+ if (!component || !component['forceStyle'] || !component['updateStyle']) {
49
+ throw new Error('this.component is missing forceStyle and/or updateStyle methods!');
50
+ }
51
+ return component;
52
+ }
53
+ apply(editor) {
54
+ this.getComponent(editor).forceStyle(this.newStyle, editor);
55
+ }
56
+ unapply(editor) {
57
+ this.getComponent(editor).forceStyle(this.originalStyle, editor);
58
+ }
59
+ description(_editor, localizationTable) {
60
+ return localizationTable.restyledElements;
61
+ }
62
+ serializeToJSON() {
63
+ return {
64
+ id: this.componentID,
65
+ originalStyle: serializeComponentStyle(this.originalStyle),
66
+ newStyle: serializeComponentStyle(this.newStyle),
67
+ };
68
+ }
69
+ }
70
+ (() => {
71
+ SerializableCommand.register(defaultRestyleComponentCommandId, (json, _editor) => {
72
+ const origStyle = deserializeComponentStyle(json.originalStyle);
73
+ const newStyle = deserializeComponentStyle(json.newStyle);
74
+ const id = json.id;
75
+ if (typeof json.id !== 'string') {
76
+ throw new Error(`json.id is of type ${(typeof json.id)}, not string.`);
77
+ }
78
+ return new DefaultRestyleComponentCommand(origStyle, newStyle, id);
79
+ });
80
+ })();
@@ -1,15 +1,22 @@
1
+ import SerializableCommand from '../commands/SerializableCommand';
1
2
  import LineSegment2 from '../math/LineSegment2';
2
3
  import Mat33 from '../math/Mat33';
3
4
  import Path from '../math/Path';
4
5
  import Rect2 from '../math/Rect2';
6
+ import Editor from '../Editor';
5
7
  import AbstractRenderer, { RenderablePathSpec } from '../rendering/renderers/AbstractRenderer';
6
8
  import AbstractComponent from './AbstractComponent';
7
9
  import { ImageComponentLocalization } from './localization';
8
- export default class Stroke extends AbstractComponent {
10
+ import RestyleableComponent, { ComponentStyle } from './RestylableComponent';
11
+ export default class Stroke extends AbstractComponent implements RestyleableComponent {
9
12
  private parts;
10
13
  protected contentBBox: Rect2;
14
+ readonly isRestylableComponent: true;
11
15
  private approximateRenderingTime;
12
16
  constructor(parts: RenderablePathSpec[]);
17
+ getStyle(): ComponentStyle;
18
+ updateStyle(style: ComponentStyle): SerializableCommand;
19
+ forceStyle(style: ComponentStyle, editor: Editor | null): void;
13
20
  intersects(line: LineSegment2): boolean;
14
21
  render(canvas: AbstractRenderer, visibleRect?: Rect2): void;
15
22
  getProportionalRenderingTime(): number;
@@ -2,11 +2,15 @@ import Path from '../math/Path';
2
2
  import Rect2 from '../math/Rect2';
3
3
  import { styleFromJSON, styleToJSON } from '../rendering/RenderingStyle';
4
4
  import AbstractComponent from './AbstractComponent';
5
+ import { createRestyleComponentCommand } from './RestylableComponent';
5
6
  export default class Stroke extends AbstractComponent {
6
- // Creates a `Stroke` from the given `parts`.
7
+ // Creates a `Stroke` from the given `parts`. All parts should have the
8
+ // same color.
7
9
  constructor(parts) {
8
10
  var _a;
9
11
  super('stroke');
12
+ // eslint-disable-next-line @typescript-eslint/prefer-as-const
13
+ this.isRestylableComponent = true;
10
14
  this.approximateRenderingTime = 0;
11
15
  this.parts = [];
12
16
  for (const section of parts) {
@@ -29,6 +33,50 @@ export default class Stroke extends AbstractComponent {
29
33
  }
30
34
  (_a = this.contentBBox) !== null && _a !== void 0 ? _a : (this.contentBBox = Rect2.empty);
31
35
  }
36
+ getStyle() {
37
+ if (this.parts.length === 0) {
38
+ return {};
39
+ }
40
+ const firstPart = this.parts[0];
41
+ if (firstPart.style.stroke === undefined
42
+ || firstPart.style.stroke.width === 0) {
43
+ return {
44
+ color: firstPart.style.fill,
45
+ };
46
+ }
47
+ return {
48
+ color: firstPart.style.stroke.color,
49
+ };
50
+ }
51
+ updateStyle(style) {
52
+ return createRestyleComponentCommand(this.getStyle(), style, this);
53
+ }
54
+ forceStyle(style, editor) {
55
+ if (!style.color) {
56
+ return;
57
+ }
58
+ this.parts = this.parts.map((part) => {
59
+ const newStyle = Object.assign(Object.assign({}, part.style), { stroke: part.style.stroke ? Object.assign({}, part.style.stroke) : undefined });
60
+ // Change the stroke color if a stroked shape. Else,
61
+ // change the fill.
62
+ if (newStyle.stroke && newStyle.stroke.width > 0) {
63
+ newStyle.stroke.color = style.color;
64
+ }
65
+ else {
66
+ newStyle.fill = style.color;
67
+ }
68
+ return {
69
+ path: part.path,
70
+ startPoint: part.startPoint,
71
+ commands: part.commands,
72
+ style: newStyle,
73
+ };
74
+ });
75
+ if (editor) {
76
+ editor.image.queueRerenderOf(this);
77
+ editor.queueRerender();
78
+ }
79
+ }
32
80
  intersects(line) {
33
81
  for (const part of this.parts) {
34
82
  if (part.path.intersection(line).length > 0) {
@@ -1,22 +1,19 @@
1
+ import SerializableCommand from '../commands/SerializableCommand';
1
2
  import LineSegment2 from '../math/LineSegment2';
2
3
  import Mat33 from '../math/Mat33';
3
4
  import Rect2 from '../math/Rect2';
5
+ import Editor from '../Editor';
4
6
  import AbstractRenderer from '../rendering/renderers/AbstractRenderer';
5
- import RenderingStyle from '../rendering/RenderingStyle';
7
+ import { TextStyle } from '../rendering/TextRenderingStyle';
6
8
  import AbstractComponent from './AbstractComponent';
7
9
  import { ImageComponentLocalization } from './localization';
8
- export interface TextStyle {
9
- size: number;
10
- fontFamily: string;
11
- fontWeight?: string;
12
- fontVariant?: string;
13
- renderingStyle: RenderingStyle;
14
- }
15
- export default class TextComponent extends AbstractComponent {
10
+ import RestyleableComponent, { ComponentStyle } from './RestylableComponent';
11
+ export default class TextComponent extends AbstractComponent implements RestyleableComponent {
16
12
  protected readonly textObjects: Array<string | TextComponent>;
17
13
  private transform;
18
14
  private style;
19
15
  protected contentBBox: Rect2;
16
+ readonly isRestylableComponent: true;
20
17
  constructor(textObjects: Array<string | TextComponent>, transform: Mat33, style: TextStyle);
21
18
  static applyTextStyles(ctx: CanvasRenderingContext2D, style: TextStyle): void;
22
19
  private static textMeasuringCtx;
@@ -28,8 +25,11 @@ export default class TextComponent extends AbstractComponent {
28
25
  render(canvas: AbstractRenderer, _visibleRect?: Rect2): void;
29
26
  getProportionalRenderingTime(): number;
30
27
  intersects(lineSegment: LineSegment2): boolean;
31
- getBaselinePos(): import("../lib").Vec3;
28
+ getStyle(): ComponentStyle;
29
+ updateStyle(style: ComponentStyle): SerializableCommand;
30
+ forceStyle(style: ComponentStyle, editor: Editor | null): void;
32
31
  getTextStyle(): TextStyle;
32
+ getBaselinePos(): import("../lib").Vec3;
33
33
  getTransform(): Mat33;
34
34
  protected applyTransformation(affineTransfm: Mat33): void;
35
35
  protected createClone(): AbstractComponent;
@@ -2,8 +2,9 @@ import LineSegment2 from '../math/LineSegment2';
2
2
  import Mat33 from '../math/Mat33';
3
3
  import Rect2 from '../math/Rect2';
4
4
  import { Vec2 } from '../math/Vec2';
5
- import { styleFromJSON, styleToJSON } from '../rendering/RenderingStyle';
5
+ import { textStyleFromJSON, textStyleToJSON } from '../rendering/TextRenderingStyle';
6
6
  import AbstractComponent from './AbstractComponent';
7
+ import { createRestyleComponentCommand } from './RestylableComponent';
7
8
  const componentTypeId = 'text';
8
9
  export default class TextComponent extends AbstractComponent {
9
10
  constructor(textObjects, transform, style) {
@@ -11,6 +12,8 @@ export default class TextComponent extends AbstractComponent {
11
12
  this.textObjects = textObjects;
12
13
  this.transform = transform;
13
14
  this.style = style;
15
+ // eslint-disable-next-line @typescript-eslint/prefer-as-const
16
+ this.isRestylableComponent = true;
14
17
  this.recomputeBBox();
15
18
  // If this has no direct children, choose a style representative of this' content
16
19
  // (useful for estimating the style of the TextComponent).
@@ -117,12 +120,43 @@ export default class TextComponent extends AbstractComponent {
117
120
  }
118
121
  return false;
119
122
  }
120
- getBaselinePos() {
121
- return this.transform.transformVec2(Vec2.zero);
123
+ getStyle() {
124
+ return {
125
+ color: this.style.renderingStyle.fill,
126
+ // Make a copy
127
+ textStyle: Object.assign(Object.assign({}, this.style), { renderingStyle: Object.assign({}, this.style.renderingStyle) }),
128
+ };
129
+ }
130
+ updateStyle(style) {
131
+ return createRestyleComponentCommand(this.getStyle(), style, this);
132
+ }
133
+ forceStyle(style, editor) {
134
+ if (style.textStyle) {
135
+ this.style = style.textStyle;
136
+ }
137
+ else if (style.color) {
138
+ this.style.renderingStyle = Object.assign(Object.assign({}, this.style.renderingStyle), { fill: style.color });
139
+ }
140
+ else {
141
+ return;
142
+ }
143
+ for (const child of this.textObjects) {
144
+ if (child instanceof TextComponent) {
145
+ child.forceStyle(style, editor);
146
+ }
147
+ }
148
+ if (editor) {
149
+ editor.image.queueRerenderOf(this);
150
+ editor.queueRerender();
151
+ }
122
152
  }
153
+ // See this.getStyle
123
154
  getTextStyle() {
124
155
  return this.style;
125
156
  }
157
+ getBaselinePos() {
158
+ return this.transform.transformVec2(Vec2.zero);
159
+ }
126
160
  getTransform() {
127
161
  return this.transform;
128
162
  }
@@ -148,9 +182,10 @@ export default class TextComponent extends AbstractComponent {
148
182
  description(localizationTable) {
149
183
  return localizationTable.text(this.getText());
150
184
  }
185
+ // Do not rely on the output of `serializeToJSON` taking any particular format.
151
186
  serializeToJSON() {
152
- const serializableStyle = Object.assign(Object.assign({}, this.style), { renderingStyle: styleToJSON(this.style.renderingStyle) });
153
- const textObjects = this.textObjects.map(text => {
187
+ const serializableStyle = textStyleToJSON(this.style);
188
+ const serializedTextObjects = this.textObjects.map(text => {
154
189
  if (typeof text === 'string') {
155
190
  return {
156
191
  text,
@@ -163,19 +198,17 @@ export default class TextComponent extends AbstractComponent {
163
198
  }
164
199
  });
165
200
  return {
166
- textObjects,
201
+ textObjects: serializedTextObjects,
167
202
  transform: this.transform.toArray(),
168
203
  style: serializableStyle,
169
204
  };
170
205
  }
206
+ // @internal
171
207
  static deserializeFromString(json) {
172
- const style = {
173
- renderingStyle: styleFromJSON(json.style.renderingStyle),
174
- size: json.style.size,
175
- fontWeight: json.style.fontWeight,
176
- fontVariant: json.style.fontVariant,
177
- fontFamily: json.style.fontFamily,
178
- };
208
+ if (typeof json === 'string') {
209
+ json = JSON.parse(json);
210
+ }
211
+ const style = textStyleFromJSON(json.style);
179
212
  const textObjects = json.textObjects.map((data) => {
180
213
  var _a;
181
214
  if (((_a = data.text) !== null && _a !== void 0 ? _a : null) !== null) {