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
@@ -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, { styleFromJSON, styleToJSON } from '../rendering/RenderingStyle';
5
6
  import AbstractComponent from './AbstractComponent';
6
7
  import { ImageComponentLocalization } from './localization';
7
8
 
@@ -13,12 +14,21 @@ export interface TextStyle {
13
14
  renderingStyle: RenderingStyle;
14
15
  }
15
16
 
17
+ type GetTextDimensCallback = (text: string, style: TextStyle) => Rect2;
16
18
 
19
+ const componentTypeId = 'text';
17
20
  export default class Text extends AbstractComponent {
18
21
  protected contentBBox: Rect2;
19
22
 
20
- public constructor(protected textObjects: Array<string|Text>, private transform: Mat33, private style: TextStyle) {
21
- super();
23
+ public constructor(
24
+ protected readonly textObjects: Array<string|Text>,
25
+ private transform: Mat33,
26
+ private readonly style: TextStyle,
27
+
28
+ // If not given, an HtmlCanvasElement is used to determine text boundaries.
29
+ private readonly getTextDimens: GetTextDimensCallback = Text.getTextDimens,
30
+ ) {
31
+ super(componentTypeId);
22
32
  this.recomputeBBox();
23
33
  }
24
34
 
@@ -52,7 +62,7 @@ export default class Text extends AbstractComponent {
52
62
 
53
63
  private computeBBoxOfPart(part: string|Text) {
54
64
  if (typeof part === 'string') {
55
- const textBBox = Text.getTextDimens(part, this.style);
65
+ const textBBox = this.getTextDimens(part, this.style);
56
66
  return textBBox.transformedBoundingBox(this.transform);
57
67
  } else {
58
68
  const bbox = part.contentBBox.transformedBoundingBox(this.transform);
@@ -120,6 +130,10 @@ export default class Text extends AbstractComponent {
120
130
  this.recomputeBBox();
121
131
  }
122
132
 
133
+ protected createClone(): AbstractComponent {
134
+ return new Text(this.textObjects, this.transform, this.style);
135
+ }
136
+
123
137
  private getText() {
124
138
  const result: string[] = [];
125
139
 
@@ -137,4 +151,65 @@ export default class Text extends AbstractComponent {
137
151
  public description(localizationTable: ImageComponentLocalization): string {
138
152
  return localizationTable.text(this.getText());
139
153
  }
140
- }
154
+
155
+ protected serializeToString(): string {
156
+ const serializableStyle = {
157
+ ...this.style,
158
+ renderingStyle: styleToJSON(this.style.renderingStyle),
159
+ };
160
+
161
+ const textObjects = this.textObjects.map(text => {
162
+ if (typeof text === 'string') {
163
+ return {
164
+ text,
165
+ };
166
+ } else {
167
+ return {
168
+ json: text.serializeToString(),
169
+ };
170
+ }
171
+ });
172
+
173
+ return JSON.stringify({
174
+ textObjects,
175
+ transform: this.transform.toArray(),
176
+ style: serializableStyle,
177
+ });
178
+ }
179
+
180
+ public static deserializeFromString(data: string, getTextDimens: GetTextDimensCallback = Text.getTextDimens): Text {
181
+ const json = JSON.parse(data);
182
+
183
+ const style: TextStyle = {
184
+ renderingStyle: styleFromJSON(json.style.renderingStyle),
185
+ size: json.style.size,
186
+ fontWeight: json.style.fontWeight,
187
+ fontVariant: json.style.fontVariant,
188
+ fontFamily: json.style.fontFamily,
189
+ };
190
+
191
+ const textObjects: Array<string|Text> = json.textObjects.map((data: any) => {
192
+ if ((data.text ?? null) !== null) {
193
+ return data.text;
194
+ }
195
+
196
+ return Text.deserializeFromString(data.json);
197
+ });
198
+
199
+ json.transform = json.transform.filter((elem: any) => typeof elem === 'number');
200
+ if (json.transform.length !== 9) {
201
+ throw new Error(`Unable to deserialize transform, ${json.transform}.`);
202
+ }
203
+
204
+ const transformData = json.transform as [
205
+ number, number, number,
206
+ number, number, number,
207
+ number, number, number,
208
+ ];
209
+ const transform = new Mat33(...transformData);
210
+
211
+ return new Text(textObjects, transform, style, getTextDimens);
212
+ }
213
+ }
214
+
215
+ AbstractComponent.registerComponent(componentTypeId, (data: string) => Text.deserializeFromString(data));
@@ -0,0 +1,10 @@
1
+ import AbstractComponent from './AbstractComponent';
2
+ import UnknownSVGObject from './UnknownSVGObject';
3
+
4
+ describe('UnknownSVGObject', () => {
5
+ it('should not be deserializable', () => {
6
+ const obj = new UnknownSVGObject(document.createElementNS('http://www.w3.org/2000/svg', 'circle'));
7
+ const serialized = obj.serialize();
8
+ expect(() => AbstractComponent.deserialize(serialized)).toThrow(/.*cannot be deserialized.*/);
9
+ });
10
+ });
@@ -6,11 +6,12 @@ import SVGRenderer from '../rendering/renderers/SVGRenderer';
6
6
  import AbstractComponent from './AbstractComponent';
7
7
  import { ImageComponentLocalization } from './localization';
8
8
 
9
+ const componentId = 'unknown-svg-object';
9
10
  export default class UnknownSVGObject extends AbstractComponent {
10
11
  protected contentBBox: Rect2;
11
12
 
12
13
  public constructor(private svgObject: SVGElement) {
13
- super();
14
+ super(componentId);
14
15
  this.contentBBox = Rect2.of(svgObject.getBoundingClientRect());
15
16
  }
16
17
 
@@ -30,7 +31,20 @@ export default class UnknownSVGObject extends AbstractComponent {
30
31
  protected applyTransformation(_affineTransfm: Mat33): void {
31
32
  }
32
33
 
34
+ protected createClone(): AbstractComponent {
35
+ return new UnknownSVGObject(this.svgObject.cloneNode(true) as SVGElement);
36
+ }
37
+
33
38
  public description(localization: ImageComponentLocalization): string {
34
39
  return localization.svgObject;
35
40
  }
41
+
42
+ protected serializeToString(): string | null {
43
+ return JSON.stringify({
44
+ html: this.svgObject.outerHTML,
45
+ });
46
+ }
36
47
  }
48
+
49
+ // null: Do not deserialize UnknownSVGObjects.
50
+ AbstractComponent.registerComponent(componentId, null);
@@ -1,5 +1,5 @@
1
1
  import { Bezier } from 'bezier-js';
2
- import AbstractRenderer, { RenderingStyle, RenderablePathSpec } from '../../rendering/renderers/AbstractRenderer';
2
+ import AbstractRenderer, { RenderablePathSpec } from '../../rendering/renderers/AbstractRenderer';
3
3
  import { Point2, Vec2 } from '../../geometry/Vec2';
4
4
  import Rect2 from '../../geometry/Rect2';
5
5
  import { PathCommand, PathCommandType } from '../../geometry/Path';
@@ -8,6 +8,7 @@ import Stroke from '../Stroke';
8
8
  import Viewport from '../../Viewport';
9
9
  import { StrokeDataPoint } from '../../types';
10
10
  import { ComponentBuilder, ComponentBuilderFactory } from './types';
11
+ import RenderingStyle from '../../rendering/RenderingStyle';
11
12
 
12
13
  export const makeFreehandLineBuilder: ComponentBuilderFactory = (initialPoint: StrokeDataPoint, viewport: Viewport) => {
13
14
  // Don't smooth if input is more than ± 7 pixels from the true curve, do smooth if
@@ -1,3 +1,4 @@
1
+ import Mat33 from '../../geometry/Mat33';
1
2
  import Path from '../../geometry/Path';
2
3
  import Rect2 from '../../geometry/Rect2';
3
4
  import AbstractRenderer from '../../rendering/renderers/AbstractRenderer';
@@ -7,18 +8,22 @@ import AbstractComponent from '../AbstractComponent';
7
8
  import Stroke from '../Stroke';
8
9
  import { ComponentBuilder, ComponentBuilderFactory } from './types';
9
10
 
10
- export const makeFilledRectangleBuilder: ComponentBuilderFactory = (initialPoint: StrokeDataPoint, _viewport: Viewport) => {
11
- return new RectangleBuilder(initialPoint, true);
11
+ export const makeFilledRectangleBuilder: ComponentBuilderFactory = (initialPoint: StrokeDataPoint, viewport: Viewport) => {
12
+ return new RectangleBuilder(initialPoint, true, viewport);
12
13
  };
13
14
 
14
- export const makeOutlinedRectangleBuilder: ComponentBuilderFactory = (initialPoint: StrokeDataPoint, _viewport: Viewport) => {
15
- return new RectangleBuilder(initialPoint, false);
15
+ export const makeOutlinedRectangleBuilder: ComponentBuilderFactory = (initialPoint: StrokeDataPoint, viewport: Viewport) => {
16
+ return new RectangleBuilder(initialPoint, false, viewport);
16
17
  };
17
18
 
18
19
  export default class RectangleBuilder implements ComponentBuilder {
19
20
  private endPoint: StrokeDataPoint;
20
21
 
21
- public constructor(private readonly startPoint: StrokeDataPoint, private filled: boolean) {
22
+ public constructor(
23
+ private readonly startPoint: StrokeDataPoint,
24
+ private filled: boolean,
25
+ private viewport: Viewport,
26
+ ) {
22
27
  // Initially, the start and end points are the same.
23
28
  this.endPoint = startPoint;
24
29
  }
@@ -29,11 +34,21 @@ export default class RectangleBuilder implements ComponentBuilder {
29
34
  }
30
35
 
31
36
  private buildPreview(): Stroke {
32
- const startPoint = this.startPoint.pos;
33
- const endPoint = this.endPoint.pos;
37
+ const canvasAngle = this.viewport.getRotationAngle();
38
+ const rotationMat = Mat33.zRotation(-canvasAngle);
39
+
40
+ // Adjust startPoint and endPoint such that applying [rotationMat] to them
41
+ // brings them to this.startPoint and this.endPoint.
42
+ const startPoint = rotationMat.inverse().transformVec2(this.startPoint.pos);
43
+ const endPoint = rotationMat.inverse().transformVec2(this.endPoint.pos);
44
+
45
+ const rect = Rect2.fromCorners(startPoint, endPoint);
34
46
  const path = Path.fromRect(
35
- Rect2.fromCorners(startPoint, endPoint),
47
+ rect,
36
48
  this.filled ? null : this.endPoint.width,
49
+ ).transformedBy(
50
+ // Rotate the canvas rectangle so that its rotation matches the screen
51
+ rotationMat
37
52
  );
38
53
 
39
54
  const preview = new Stroke([
@@ -0,0 +1,18 @@
1
+ import AbstractComponent from '../AbstractComponent';
2
+ import { ImageComponentLocalization } from '../localization';
3
+
4
+ // Returns the description of all given elements, if identical, otherwise,
5
+ // returns null.
6
+ export default (localizationTable: ImageComponentLocalization, elems: AbstractComponent[]) => {
7
+ if (elems.length === 0) {
8
+ return null;
9
+ }
10
+
11
+ const description = elems[0].description(localizationTable);
12
+ for (const elem of elems) {
13
+ if (elem.description(localizationTable) !== description) {
14
+ return null;
15
+ }
16
+ }
17
+ return description;
18
+ };
@@ -1,5 +1,6 @@
1
1
  import { Bezier } from 'bezier-js';
2
- import { RenderingStyle, RenderablePathSpec } from '../rendering/renderers/AbstractRenderer';
2
+ import { RenderablePathSpec } from '../rendering/renderers/AbstractRenderer';
3
+ import RenderingStyle from '../rendering/RenderingStyle';
3
4
  import LineSegment2 from './LineSegment2';
4
5
  import Mat33 from './Mat33';
5
6
  import Rect2 from './Rect2';
@@ -282,6 +283,10 @@ export default class Path {
282
283
  return Path.toString(this.startPoint, this.parts);
283
284
  }
284
285
 
286
+ public serialize(): string {
287
+ return this.toString();
288
+ }
289
+
285
290
  public static toString(startPoint: Point2, parts: PathCommand[]): string {
286
291
  const result: string[] = [];
287
292
 
@@ -555,4 +560,6 @@ export default class Path {
555
560
 
556
561
  return new Path(startPos ?? Vec2.zero, commands);
557
562
  }
563
+
564
+ public static empty: Path = new Path(Vec2.zero, []);
558
565
  }
@@ -3,9 +3,10 @@ import CanvasRenderer from './renderers/CanvasRenderer';
3
3
  import { Editor } from '../Editor';
4
4
  import { EditorEventType } from '../types';
5
5
  import DummyRenderer from './renderers/DummyRenderer';
6
- import { Vec2 } from '../geometry/Vec2';
6
+ import { Point2, Vec2 } from '../geometry/Vec2';
7
7
  import RenderingCache from './caching/RenderingCache';
8
8
  import TextOnlyRenderer from './renderers/TextOnlyRenderer';
9
+ import Color4 from '../Color4';
9
10
 
10
11
  export enum RenderingMode {
11
12
  DummyRenderer,
@@ -88,6 +89,10 @@ export default class Display {
88
89
  return this.cache;
89
90
  }
90
91
 
92
+ public getColorAt = (_screenPos: Point2): Color4|null => {
93
+ return null;
94
+ };
95
+
91
96
  private initializeCanvasRendering() {
92
97
  const dryInkCanvas = document.createElement('canvas');
93
98
  const wetInkCanvas = document.createElement('canvas');
@@ -132,6 +137,17 @@ export default class Display {
132
137
  this.flattenCallback = () => {
133
138
  dryInkCtx.drawImage(wetInkCanvas, 0, 0);
134
139
  };
140
+
141
+ this.getColorAt = (screenPos: Point2) => {
142
+ const pixel = dryInkCtx.getImageData(screenPos.x, screenPos.y, 1, 1);
143
+ const data = pixel?.data;
144
+
145
+ if (data) {
146
+ const color = Color4.ofRGBA(data[0] / 255, data[1] / 255, data[2] / 255, data[3] / 255);
147
+ return color;
148
+ }
149
+ return null;
150
+ };
135
151
  }
136
152
 
137
153
  private initializeTextRendering() {
@@ -0,0 +1,68 @@
1
+
2
+ import Color4 from '../Color4';
3
+ import RenderingStyle, { styleFromJSON, stylesEqual, styleToJSON } from './RenderingStyle';
4
+
5
+
6
+ describe('RenderingStyle', () => {
7
+ it('identical styles should be equal', () => {
8
+ const redFill: RenderingStyle = {
9
+ fill: Color4.red,
10
+ };
11
+ expect(stylesEqual(redFill, redFill)).toBe(true);
12
+ expect(stylesEqual(
13
+ { fill: Color4.ofRGB(1, 0, 0.3), },
14
+ { fill: Color4.ofRGB(1, 0, 0.3), },
15
+ )).toBe(true);
16
+ expect(stylesEqual(
17
+ { fill: Color4.red },
18
+ { fill: Color4.blue },
19
+ )).toBe(false);
20
+
21
+ expect(stylesEqual(
22
+ { fill: Color4.red, stroke: { width: 1, color: Color4.red }},
23
+ { fill: Color4.red },
24
+ )).toBe(false);
25
+ expect(stylesEqual(
26
+ { fill: Color4.red, stroke: { width: 1, color: Color4.red }},
27
+ { fill: Color4.red, stroke: { width: 1, color: Color4.blue }},
28
+ )).toBe(false);
29
+ expect(stylesEqual(
30
+ { fill: Color4.red, stroke: { width: 1, color: Color4.red }},
31
+ { fill: Color4.red, stroke: { width: 1, color: Color4.red }},
32
+ )).toBe(true);
33
+ expect(stylesEqual(
34
+ { fill: Color4.red, stroke: { width: 1, color: Color4.red }},
35
+ { fill: Color4.red, stroke: { width: 2, color: Color4.red }},
36
+ )).toBe(false);
37
+ });
38
+
39
+ it('styles should be convertable to JSON', () => {
40
+ expect(styleToJSON({
41
+ fill: Color4.red,
42
+ })).toMatchObject({
43
+ fill: '#ff0000',
44
+ stroke: undefined,
45
+ });
46
+
47
+ expect(styleToJSON({
48
+ fill: Color4.blue,
49
+ stroke: {
50
+ width: 4,
51
+ color: Color4.red,
52
+ }
53
+ })).toMatchObject({
54
+ fill: '#0000ff',
55
+ stroke: {
56
+ width: 4,
57
+ color: '#ff0000'
58
+ },
59
+ });
60
+ });
61
+
62
+ it('JSON should be convertable into styles', () => {
63
+ const redFillJSON = { fill: '#ff0000', };
64
+ const redFillBlueStrokeJSON = { fill: '#ff0000', stroke: { width: 4, color: '#0000ff' }};
65
+ expect(styleToJSON(styleFromJSON(redFillJSON))).toMatchObject(redFillJSON);
66
+ expect(styleToJSON(styleFromJSON(redFillBlueStrokeJSON))).toMatchObject(redFillBlueStrokeJSON);
67
+ });
68
+ });
@@ -0,0 +1,46 @@
1
+ import Color4 from '../Color4';
2
+
3
+ interface RenderingStyle {
4
+ fill: Color4;
5
+ stroke?: {
6
+ color: Color4;
7
+ width: number;
8
+ };
9
+ }
10
+
11
+ export default RenderingStyle;
12
+
13
+ export const stylesEqual = (a: RenderingStyle, b: RenderingStyle): boolean => {
14
+ const result = a === b || (a.fill.eq(b.fill)
15
+ && (a.stroke == undefined) === (b.stroke == undefined)
16
+ && (a.stroke?.color?.eq(b.stroke?.color) ?? true)
17
+ && a.stroke?.width === b.stroke?.width);
18
+
19
+ // Map undefined/null -> false
20
+ return result ?? false;
21
+ };
22
+
23
+ // Returns an object that can be converted to a JSON string with
24
+ // JSON.stringify.
25
+ export const styleToJSON = (style: RenderingStyle) => {
26
+ const stroke = !style.stroke ? undefined : {
27
+ color: style.stroke.color.toHexString(),
28
+ width: style.stroke.width,
29
+ };
30
+
31
+ return {
32
+ fill: style.fill.toHexString(),
33
+ stroke,
34
+ };
35
+ };
36
+
37
+ export const styleFromJSON = (json: Record<string, any>) => {
38
+ const stroke = json.stroke ? {
39
+ color: Color4.fromHex(json.stroke.color),
40
+ width: json.stroke.width,
41
+ } : undefined;
42
+ return {
43
+ fill: Color4.fromHex(json.fill),
44
+ stroke,
45
+ };
46
+ };
@@ -28,7 +28,7 @@ describe('RenderingCache', () => {
28
28
  editor.image.renderWithCache(screenRenderer, cache, editor.viewport);
29
29
  expect(lastRenderer).toBeNull();
30
30
 
31
- editor.dispatch(new EditorImage.AddElementCommand(testStroke));
31
+ editor.dispatch(EditorImage.addElement(testStroke));
32
32
  editor.image.renderWithCache(screenRenderer, cache, editor.viewport);
33
33
 
34
34
  expect(allocdRenderers).toBeGreaterThanOrEqual(1);
@@ -65,6 +65,11 @@ export default class RenderingCacheNode {
65
65
  if (this.instantiatedChildren.length === 0) {
66
66
  const childRects = this.region.divideIntoGrid(cacheDivisionSize, cacheDivisionSize);
67
67
 
68
+ if (this.region.size.x === 0 || this.region.size.y === 0) {
69
+ console.warn('Cache element has zero size! Not generating children.');
70
+ return;
71
+ }
72
+
68
73
  for (const rect of childRects) {
69
74
  const child = new RenderingCacheNode(rect, this.cacheState);
70
75
  child.parent = this;
@@ -357,7 +362,7 @@ export default class RenderingCacheNode {
357
362
 
358
363
  private checkRep() {
359
364
  if (this.instantiatedChildren.length !== cacheDivisionSize * cacheDivisionSize && this.instantiatedChildren.length !== 0) {
360
- throw new Error('Repcheck: Wrong number of children');
365
+ throw new Error(`Repcheck: Wrong number of children. Got ${this.instantiatedChildren.length}`);
361
366
  }
362
367
 
363
368
  if (this.renderedIds[1] !== undefined && this.renderedIds[0] >= this.renderedIds[1]) {
@@ -1,4 +1,3 @@
1
- import Color4 from '../../Color4';
2
1
  import { LoadSaveDataTable } from '../../components/AbstractComponent';
3
2
  import { TextStyle } from '../../components/Text';
4
3
  import Mat33 from '../../geometry/Mat33';
@@ -6,14 +5,7 @@ import Path, { PathCommand, PathCommandType } from '../../geometry/Path';
6
5
  import Rect2 from '../../geometry/Rect2';
7
6
  import { Point2, Vec2 } from '../../geometry/Vec2';
8
7
  import Viewport from '../../Viewport';
9
-
10
- export interface RenderingStyle {
11
- fill: Color4;
12
- stroke?: {
13
- color: Color4;
14
- width: number;
15
- };
16
- }
8
+ import RenderingStyle, { stylesEqual } from '../RenderingStyle';
17
9
 
18
10
  export interface RenderablePathSpec {
19
11
  startPoint: Point2;
@@ -21,12 +13,6 @@ export interface RenderablePathSpec {
21
13
  style: RenderingStyle;
22
14
  }
23
15
 
24
- const stylesEqual = (a: RenderingStyle, b: RenderingStyle) => {
25
- return a === b || (a.fill.eq(b.fill)
26
- && a.stroke?.color?.eq(b.stroke?.color)
27
- && a.stroke?.width === b.stroke?.width);
28
- };
29
-
30
16
  export default abstract class AbstractRenderer {
31
17
  // If null, this' transformation is linked to the Viewport
32
18
  private selfTransform: Mat33|null = null;
@@ -5,7 +5,8 @@ import Rect2 from '../../geometry/Rect2';
5
5
  import { Point2, Vec2 } from '../../geometry/Vec2';
6
6
  import Vec3 from '../../geometry/Vec3';
7
7
  import Viewport from '../../Viewport';
8
- import AbstractRenderer, { RenderablePathSpec, RenderingStyle } from './AbstractRenderer';
8
+ import RenderingStyle from '../RenderingStyle';
9
+ import AbstractRenderer, { RenderablePathSpec } from './AbstractRenderer';
9
10
 
10
11
  export default class CanvasRenderer extends AbstractRenderer {
11
12
  private ignoreObjectsAboveLevel: number|null = null;
@@ -6,7 +6,8 @@ import Rect2 from '../../geometry/Rect2';
6
6
  import { Point2, Vec2 } from '../../geometry/Vec2';
7
7
  import Vec3 from '../../geometry/Vec3';
8
8
  import Viewport from '../../Viewport';
9
- import AbstractRenderer, { RenderingStyle } from './AbstractRenderer';
9
+ import RenderingStyle from '../RenderingStyle';
10
+ import AbstractRenderer from './AbstractRenderer';
10
11
 
11
12
  export default class DummyRenderer extends AbstractRenderer {
12
13
  // Variables that track the state of what's been rendered
@@ -7,7 +7,8 @@ import Rect2 from '../../geometry/Rect2';
7
7
  import { Point2, Vec2 } from '../../geometry/Vec2';
8
8
  import { svgAttributesDataKey, SVGLoaderUnknownAttribute, SVGLoaderUnknownStyleAttribute, svgStyleAttributesDataKey } from '../../SVGLoader';
9
9
  import Viewport from '../../Viewport';
10
- import AbstractRenderer, { RenderingStyle } from './AbstractRenderer';
10
+ import RenderingStyle from '../RenderingStyle';
11
+ import AbstractRenderer from './AbstractRenderer';
11
12
 
12
13
  const svgNameSpace = 'http://www.w3.org/2000/svg';
13
14
  export default class SVGRenderer extends AbstractRenderer {
@@ -5,7 +5,8 @@ import { Vec2 } from '../../geometry/Vec2';
5
5
  import Vec3 from '../../geometry/Vec3';
6
6
  import Viewport from '../../Viewport';
7
7
  import { TextRendererLocalization } from '../localization';
8
- import AbstractRenderer, { RenderingStyle } from './AbstractRenderer';
8
+ import RenderingStyle from '../RenderingStyle';
9
+ import AbstractRenderer from './AbstractRenderer';
9
10
 
10
11
  // Outputs a description of what was rendered.
11
12