js-draw 0.14.0 → 0.15.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (88) hide show
  1. package/.github/ISSUE_TEMPLATE/translation.yml +8 -0
  2. package/CHANGELOG.md +8 -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 +30 -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 +4 -9
  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.ts +36 -61
  60. package/src/components/RestylableComponent.ts +142 -0
  61. package/src/components/Stroke.test.ts +68 -0
  62. package/src/components/Stroke.ts +68 -2
  63. package/src/components/TextComponent.test.ts +56 -2
  64. package/src/components/TextComponent.ts +63 -25
  65. package/src/components/lib.ts +4 -1
  66. package/src/components/localization.ts +3 -0
  67. package/src/math/Rect2.test.ts +18 -6
  68. package/src/rendering/TextRenderingStyle.ts +38 -0
  69. package/src/rendering/renderers/AbstractRenderer.ts +1 -1
  70. package/src/rendering/renderers/CanvasRenderer.ts +2 -1
  71. package/src/rendering/renderers/DummyRenderer.ts +1 -1
  72. package/src/rendering/renderers/SVGRenderer.ts +1 -1
  73. package/src/rendering/renderers/TextOnlyRenderer.ts +1 -1
  74. package/src/toolbar/IconProvider.ts +12 -1
  75. package/src/toolbar/localization.ts +2 -0
  76. package/src/toolbar/widgets/BaseWidget.ts +12 -4
  77. package/src/toolbar/widgets/InsertImageWidget.ts +2 -1
  78. package/src/toolbar/widgets/SelectionToolWidget.ts +95 -1
  79. package/src/tools/PanZoom.test.ts +2 -1
  80. package/src/tools/PasteHandler.ts +1 -1
  81. package/src/tools/Pen.ts +2 -2
  82. package/src/tools/SelectionTool/SelectAllShortcutHandler.ts +28 -0
  83. package/src/tools/SelectionTool/Selection.ts +1 -1
  84. package/src/tools/SelectionTool/SelectionTool.ts +4 -9
  85. package/src/tools/TextTool.ts +2 -1
  86. package/src/tools/ToolController.ts +2 -0
  87. package/src/tools/lib.ts +1 -0
  88. 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);
83
84
  }
84
85
  // @returns true iff this component can be selected (e.g. by the selection tool.)
85
86
  isSelectable() {
@@ -158,52 +159,31 @@ 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) {
167
+ super(AbstractComponent.transformElementCommandId, componentID, component);
201
168
  this.affineTransfm = affineTransfm;
202
- this.component = component;
203
- this.origZIndex = component.zIndex;
169
+ this.origZIndex = null;
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
+ }
176
+ resolveComponent(image) {
177
+ if (this.component) {
178
+ return;
179
+ }
180
+ super.resolveComponent(image);
181
+ this.origZIndex = this.component.getZIndex();
205
182
  }
206
183
  updateTransform(editor, newTransfm) {
184
+ if (!this.component) {
185
+ throw new Error('this.component is undefined or null!');
186
+ }
207
187
  // Any parent should have only one direct child.
208
188
  const parent = editor.image.findParent(this.component);
209
189
  let hadParent = false;
@@ -219,11 +199,13 @@ AbstractComponent.TransformElementCommand = (_a = class extends SerializableComm
219
199
  }
220
200
  }
221
201
  apply(editor) {
202
+ this.resolveComponent(editor.image);
222
203
  this.component.zIndex = this.targetZIndex;
223
204
  this.updateTransform(editor, this.affineTransfm);
224
205
  editor.queueRerender();
225
206
  }
226
207
  unapply(editor) {
208
+ this.resolveComponent(editor.image);
227
209
  this.component.zIndex = this.origZIndex;
228
210
  this.updateTransform(editor, this.affineTransfm.inverse());
229
211
  editor.queueRerender();
@@ -233,7 +215,7 @@ AbstractComponent.TransformElementCommand = (_a = class extends SerializableComm
233
215
  }
234
216
  serializeToJSON() {
235
217
  return {
236
- id: this.component.getId(),
218
+ id: this.componentID,
237
219
  transfm: this.affineTransfm.toArray(),
238
220
  targetZIndex: this.targetZIndex,
239
221
  };
@@ -241,13 +223,11 @@ AbstractComponent.TransformElementCommand = (_a = class extends SerializableComm
241
223
  },
242
224
  (() => {
243
225
  SerializableCommand.register(AbstractComponent.transformElementCommandId, (json, editor) => {
244
- const elem = editor.image.lookupElement(json.id);
226
+ var _a;
227
+ const elem = (_a = editor.image.lookupElement(json.id)) !== null && _a !== void 0 ? _a : undefined;
245
228
  const transform = new Mat33(...json.transfm);
246
229
  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);
230
+ return new AbstractComponent.TransformElementCommand(transform, json.id, elem, targetZIndex);
251
231
  });
252
232
  })(),
253
233
  _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) {