js-draw 0.0.10 → 0.1.2

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 (122) hide show
  1. package/CHANGELOG.md +11 -0
  2. package/dist/bundle.js +1 -1
  3. package/dist/src/Editor.d.ts +2 -2
  4. package/dist/src/Editor.js +17 -7
  5. package/dist/src/EditorImage.d.ts +15 -7
  6. package/dist/src/EditorImage.js +46 -37
  7. package/dist/src/Pointer.d.ts +3 -2
  8. package/dist/src/Pointer.js +12 -3
  9. package/dist/src/SVGLoader.d.ts +6 -2
  10. package/dist/src/SVGLoader.js +20 -8
  11. package/dist/src/Viewport.d.ts +4 -0
  12. package/dist/src/Viewport.js +51 -0
  13. package/dist/src/components/AbstractComponent.d.ts +9 -2
  14. package/dist/src/components/AbstractComponent.js +14 -0
  15. package/dist/src/components/SVGGlobalAttributesObject.d.ts +1 -1
  16. package/dist/src/components/SVGGlobalAttributesObject.js +1 -1
  17. package/dist/src/components/Stroke.d.ts +1 -1
  18. package/dist/src/components/Stroke.js +1 -1
  19. package/dist/src/components/UnknownSVGObject.d.ts +1 -1
  20. package/dist/src/components/UnknownSVGObject.js +1 -1
  21. package/dist/src/components/builders/ArrowBuilder.d.ts +1 -1
  22. package/dist/src/components/builders/FreehandLineBuilder.d.ts +1 -1
  23. package/dist/src/components/builders/FreehandLineBuilder.js +1 -1
  24. package/dist/src/components/builders/LineBuilder.d.ts +1 -1
  25. package/dist/src/components/builders/RectangleBuilder.d.ts +1 -1
  26. package/dist/src/components/builders/types.d.ts +1 -1
  27. package/dist/src/geometry/Mat33.js +3 -0
  28. package/dist/src/geometry/Path.d.ts +1 -1
  29. package/dist/src/geometry/Path.js +102 -69
  30. package/dist/src/geometry/Rect2.d.ts +1 -0
  31. package/dist/src/geometry/Rect2.js +47 -9
  32. package/dist/src/{Display.d.ts → rendering/Display.d.ts} +5 -2
  33. package/dist/src/{Display.js → rendering/Display.js} +34 -4
  34. package/dist/src/rendering/caching/CacheRecord.d.ts +19 -0
  35. package/dist/src/rendering/caching/CacheRecord.js +52 -0
  36. package/dist/src/rendering/caching/CacheRecordManager.d.ts +11 -0
  37. package/dist/src/rendering/caching/CacheRecordManager.js +31 -0
  38. package/dist/src/rendering/caching/RenderingCache.d.ts +12 -0
  39. package/dist/src/rendering/caching/RenderingCache.js +42 -0
  40. package/dist/src/rendering/caching/RenderingCacheNode.d.ts +28 -0
  41. package/dist/src/rendering/caching/RenderingCacheNode.js +301 -0
  42. package/dist/src/rendering/caching/testUtils.d.ts +9 -0
  43. package/dist/src/rendering/caching/testUtils.js +20 -0
  44. package/dist/src/rendering/caching/types.d.ts +21 -0
  45. package/dist/src/rendering/caching/types.js +1 -0
  46. package/dist/src/rendering/{AbstractRenderer.d.ts → renderers/AbstractRenderer.d.ts} +20 -9
  47. package/dist/src/rendering/{AbstractRenderer.js → renderers/AbstractRenderer.js} +37 -3
  48. package/dist/src/rendering/{CanvasRenderer.d.ts → renderers/CanvasRenderer.d.ts} +10 -5
  49. package/dist/src/rendering/{CanvasRenderer.js → renderers/CanvasRenderer.js} +60 -20
  50. package/dist/src/rendering/{DummyRenderer.d.ts → renderers/DummyRenderer.d.ts} +9 -5
  51. package/dist/src/rendering/{DummyRenderer.js → renderers/DummyRenderer.js} +35 -4
  52. package/dist/src/rendering/{SVGRenderer.d.ts → renderers/SVGRenderer.d.ts} +7 -5
  53. package/dist/src/rendering/{SVGRenderer.js → renderers/SVGRenderer.js} +35 -18
  54. package/dist/src/testing/createEditor.js +1 -1
  55. package/dist/src/toolbar/HTMLToolbar.d.ts +2 -1
  56. package/dist/src/toolbar/HTMLToolbar.js +165 -154
  57. package/dist/src/toolbar/icons.d.ts +10 -0
  58. package/dist/src/toolbar/icons.js +180 -0
  59. package/dist/src/toolbar/localization.d.ts +4 -1
  60. package/dist/src/toolbar/localization.js +4 -1
  61. package/dist/src/toolbar/types.d.ts +4 -0
  62. package/dist/src/tools/PanZoom.d.ts +9 -6
  63. package/dist/src/tools/PanZoom.js +30 -21
  64. package/dist/src/tools/Pen.js +8 -3
  65. package/dist/src/tools/SelectionTool.js +9 -24
  66. package/dist/src/tools/ToolController.d.ts +5 -6
  67. package/dist/src/tools/ToolController.js +8 -10
  68. package/dist/src/tools/localization.d.ts +1 -0
  69. package/dist/src/tools/localization.js +1 -0
  70. package/dist/src/types.d.ts +2 -1
  71. package/package.json +1 -1
  72. package/src/Editor.ts +19 -8
  73. package/src/EditorImage.test.ts +2 -2
  74. package/src/EditorImage.ts +58 -42
  75. package/src/Pointer.ts +13 -4
  76. package/src/SVGLoader.ts +36 -10
  77. package/src/Viewport.ts +68 -0
  78. package/src/components/AbstractComponent.ts +21 -2
  79. package/src/components/SVGGlobalAttributesObject.ts +2 -2
  80. package/src/components/Stroke.ts +2 -2
  81. package/src/components/UnknownSVGObject.ts +2 -2
  82. package/src/components/builders/ArrowBuilder.ts +1 -1
  83. package/src/components/builders/FreehandLineBuilder.ts +2 -2
  84. package/src/components/builders/LineBuilder.ts +1 -1
  85. package/src/components/builders/RectangleBuilder.ts +1 -1
  86. package/src/components/builders/types.ts +1 -1
  87. package/src/geometry/Mat33.ts +3 -0
  88. package/src/geometry/Path.fromString.test.ts +94 -4
  89. package/src/geometry/Path.toString.test.ts +12 -2
  90. package/src/geometry/Path.ts +107 -71
  91. package/src/geometry/Rect2.test.ts +47 -8
  92. package/src/geometry/Rect2.ts +57 -9
  93. package/src/{Display.ts → rendering/Display.ts} +39 -6
  94. package/src/rendering/caching/CacheRecord.test.ts +49 -0
  95. package/src/rendering/caching/CacheRecord.ts +73 -0
  96. package/src/rendering/caching/CacheRecordManager.ts +45 -0
  97. package/src/rendering/caching/RenderingCache.test.ts +44 -0
  98. package/src/rendering/caching/RenderingCache.ts +63 -0
  99. package/src/rendering/caching/RenderingCacheNode.ts +378 -0
  100. package/src/rendering/caching/testUtils.ts +35 -0
  101. package/src/rendering/caching/types.ts +39 -0
  102. package/src/rendering/{AbstractRenderer.ts → renderers/AbstractRenderer.ts} +57 -9
  103. package/src/rendering/{CanvasRenderer.ts → renderers/CanvasRenderer.ts} +74 -25
  104. package/src/rendering/renderers/DummyRenderer.test.ts +43 -0
  105. package/src/rendering/{DummyRenderer.ts → renderers/DummyRenderer.ts} +50 -7
  106. package/src/rendering/{SVGRenderer.ts → renderers/SVGRenderer.ts} +39 -23
  107. package/src/testing/createEditor.ts +1 -1
  108. package/src/toolbar/HTMLToolbar.ts +199 -170
  109. package/src/toolbar/icons.ts +203 -0
  110. package/src/toolbar/localization.ts +9 -2
  111. package/src/toolbar/toolbar.css +21 -8
  112. package/src/toolbar/types.ts +5 -0
  113. package/src/tools/PanZoom.ts +37 -27
  114. package/src/tools/Pen.ts +7 -3
  115. package/src/tools/SelectionTool.test.ts +1 -1
  116. package/src/tools/SelectionTool.ts +12 -33
  117. package/src/tools/ToolController.ts +3 -5
  118. package/src/tools/localization.ts +2 -0
  119. package/src/types.ts +10 -3
  120. package/tsconfig.json +1 -0
  121. package/dist/__mocks__/coloris.d.ts +0 -2
  122. package/dist/__mocks__/coloris.js +0 -5
package/src/SVGLoader.ts CHANGED
@@ -5,17 +5,25 @@ import SVGGlobalAttributesObject from './components/SVGGlobalAttributesObject';
5
5
  import UnknownSVGObject from './components/UnknownSVGObject';
6
6
  import Path from './geometry/Path';
7
7
  import Rect2 from './geometry/Rect2';
8
- import { RenderablePathSpec, RenderingStyle } from './rendering/AbstractRenderer';
9
- import { ComponentAddedListener, ImageLoader, OnProgressListener } from './types';
8
+ import { RenderablePathSpec, RenderingStyle } from './rendering/renderers/AbstractRenderer';
9
+ import { ComponentAddedListener, ImageLoader, OnDetermineExportRectListener, OnProgressListener } from './types';
10
10
 
11
11
  type OnFinishListener = ()=> void;
12
12
 
13
13
  // Size of a loaded image if no size is specified.
14
14
  export const defaultSVGViewRect = new Rect2(0, 0, 500, 500);
15
15
 
16
+ // Key to retrieve unrecognised attributes from an AbstractComponent
17
+ export const svgAttributesDataKey = 'svgAttrs';
18
+
19
+ // [key, value]
20
+ export type SVGLoaderUnknownAttribute = [ string, string ];
21
+
16
22
  export default class SVGLoader implements ImageLoader {
17
23
  private onAddComponent: ComponentAddedListener|null = null;
18
24
  private onProgress: OnProgressListener|null = null;
25
+ private onDetermineExportRect: OnDetermineExportRectListener|null = null;
26
+
19
27
  private processedCount: number = 0;
20
28
  private totalToProcess: number = 0;
21
29
  private rootViewBox: Rect2|null;
@@ -86,12 +94,31 @@ export default class SVGLoader implements ImageLoader {
86
94
  return result;
87
95
  }
88
96
 
97
+ private attachUnrecognisedAttrs(
98
+ elem: AbstractComponent,
99
+ node: SVGElement,
100
+ supportedAttrs: Set<string>
101
+ ) {
102
+ for (const attr of node.getAttributeNames()) {
103
+ if (supportedAttrs.has(attr)) {
104
+ continue;
105
+ }
106
+
107
+ elem.attachLoadSaveData(svgAttributesDataKey,
108
+ [ attr, node.getAttribute(attr) ] as SVGLoaderUnknownAttribute,
109
+ );
110
+ }
111
+ }
112
+
89
113
  // Adds a stroke with a single path
90
114
  private addPath(node: SVGPathElement) {
91
115
  let elem: AbstractComponent;
92
116
  try {
93
117
  const strokeData = this.strokeDataFromElem(node);
94
118
  elem = new Stroke(strokeData);
119
+ this.attachUnrecognisedAttrs(
120
+ elem, node, new Set([ 'stroke', 'fill', 'stroke-width', 'd' ]),
121
+ );
95
122
  } catch (e) {
96
123
  console.error(
97
124
  'Invalid path in node', node,
@@ -126,6 +153,7 @@ export default class SVGLoader implements ImageLoader {
126
153
  }
127
154
 
128
155
  this.rootViewBox = new Rect2(x, y, width, height);
156
+ this.onDetermineExportRect?.(this.rootViewBox);
129
157
  }
130
158
 
131
159
  private updateSVGAttrs(node: SVGSVGElement) {
@@ -172,10 +200,12 @@ export default class SVGLoader implements ImageLoader {
172
200
  }
173
201
 
174
202
  public async start(
175
- onAddComponent: ComponentAddedListener, onProgress: OnProgressListener
176
- ): Promise<Rect2> {
203
+ onAddComponent: ComponentAddedListener, onProgress: OnProgressListener,
204
+ onDetermineExportRect: OnDetermineExportRectListener|null = null
205
+ ): Promise<void> {
177
206
  this.onAddComponent = onAddComponent;
178
207
  this.onProgress = onProgress;
208
+ this.onDetermineExportRect = onDetermineExportRect;
179
209
 
180
210
  // Estimate the number of tags to process.
181
211
  this.totalToProcess = this.source.childElementCount;
@@ -185,14 +215,12 @@ export default class SVGLoader implements ImageLoader {
185
215
  await this.visit(this.source);
186
216
 
187
217
  const viewBox = this.rootViewBox;
188
- let result = defaultSVGViewRect;
189
218
 
190
- if (viewBox) {
191
- result = Rect2.of(viewBox);
219
+ if (!viewBox) {
220
+ this.onDetermineExportRect?.(defaultSVGViewRect);
192
221
  }
193
222
 
194
223
  this.onFinish?.();
195
- return result;
196
224
  }
197
225
 
198
226
  // TODO: Handling unsafe data! Tripple-check that this is secure!
@@ -211,9 +239,7 @@ export default class SVGLoader implements ImageLoader {
211
239
  throw new Error('SVG loading iframe is not sandboxed.');
212
240
  }
213
241
 
214
- // Try running JavaScript within the iframe
215
242
  const sandboxDoc = sandbox.contentWindow?.document ?? sandbox.contentDocument;
216
-
217
243
  if (sandboxDoc == null) throw new Error('Unable to open a sandboxed iframe!');
218
244
 
219
245
  sandboxDoc.open();
package/src/Viewport.ts CHANGED
@@ -118,12 +118,20 @@ export class Viewport {
118
118
  return this.transform;
119
119
  }
120
120
 
121
+ public getResolution(): Vec2 {
122
+ return this.screenRect.size;
123
+ }
124
+
121
125
  // Returns the amount a vector on the canvas is scaled to become a vector on the screen.
122
126
  public getScaleFactor(): number {
123
127
  // Use transformVec3 to avoid translating the vector
124
128
  return this.transform.transformVec3(Vec3.unitX).magnitude();
125
129
  }
126
130
 
131
+ public getSizeOfPixelOnCanvas(): number {
132
+ return 1/this.getScaleFactor();
133
+ }
134
+
127
135
  // Returns the angle of the canvas in radians
128
136
  public getRotationAngle(): number {
129
137
  return this.transform.transformVec3(Vec3.unitX).angle();
@@ -158,6 +166,66 @@ export class Viewport {
158
166
  public roundPoint(point: Point2): Point2 {
159
167
  return Viewport.roundPoint(point, 1 / this.getScaleFactor());
160
168
  }
169
+
170
+ // Returns a Command that transforms the view such that [rect] is visible, and perhaps
171
+ // centered in the viewport.
172
+ // Returns null if no transformation is necessary
173
+ public zoomTo(toMakeVisible: Rect2): Command {
174
+ let transform = Mat33.identity;
175
+
176
+ if (toMakeVisible.w === 0 || toMakeVisible.h === 0) {
177
+ throw new Error(`${toMakeVisible.toString()} rectangle is empty! Cannot zoom to!`);
178
+ }
179
+
180
+ if (isNaN(toMakeVisible.size.magnitude())) {
181
+ throw new Error(`${toMakeVisible.toString()} rectangle has NaN size! Cannot zoom to!`);
182
+ }
183
+
184
+ // Try to move the selection within the center 2/3rds of the viewport.
185
+ const recomputeTargetRect = () => {
186
+ // transform transforms objects on the canvas. As such, we need to invert it
187
+ // to transform the viewport.
188
+ const visibleRect = this.visibleRect.transformedBoundingBox(transform.inverse());
189
+ return visibleRect.transformedBoundingBox(Mat33.scaling2D(2 / 3, visibleRect.center));
190
+ };
191
+
192
+ let targetRect = recomputeTargetRect();
193
+ const largerThanTarget = targetRect.w < toMakeVisible.w || targetRect.h < toMakeVisible.h;
194
+
195
+ // Ensure that toMakeVisible is at least 1/8th of the visible region.
196
+ const muchSmallerThanTarget = toMakeVisible.maxDimension / targetRect.maxDimension < 0.125;
197
+
198
+ if (largerThanTarget || muchSmallerThanTarget) {
199
+ // If larger than the target, ensure that the longest axis is visible.
200
+ // If smaller, shrink the visible rectangle as much as possible
201
+ const multiplier = (largerThanTarget ? Math.max : Math.min)(
202
+ toMakeVisible.w / targetRect.w, toMakeVisible.h / targetRect.h
203
+ );
204
+ const visibleRectTransform = Mat33.scaling2D(multiplier, targetRect.topLeft);
205
+ const viewportContentTransform = visibleRectTransform.inverse();
206
+
207
+ transform = transform.rightMul(viewportContentTransform);
208
+ }
209
+
210
+ targetRect = recomputeTargetRect();
211
+
212
+ // Ensure that the center of the region is visible
213
+ if (!targetRect.containsRect(toMakeVisible)) {
214
+ // target position - current position
215
+ const translation = toMakeVisible.center.minus(targetRect.center);
216
+ const visibleRectTransform = Mat33.translation(translation);
217
+ const viewportContentTransform = visibleRectTransform.inverse();
218
+
219
+ transform = transform.rightMul(viewportContentTransform);
220
+ }
221
+
222
+ if (!transform.invertable()) {
223
+ console.warn('Unable to zoom to ', toMakeVisible, '! Computed transform', transform, 'is singular.');
224
+ transform = Mat33.identity;
225
+ }
226
+
227
+ return new Viewport.ViewportTransform(transform);
228
+ }
161
229
  }
162
230
 
163
231
  export namespace Viewport { // eslint-disable-line
@@ -4,13 +4,16 @@ import EditorImage from '../EditorImage';
4
4
  import LineSegment2 from '../geometry/LineSegment2';
5
5
  import Mat33 from '../geometry/Mat33';
6
6
  import Rect2 from '../geometry/Rect2';
7
- import AbstractRenderer from '../rendering/AbstractRenderer';
7
+ import AbstractRenderer from '../rendering/renderers/AbstractRenderer';
8
8
  import { ImageComponentLocalization } from './localization';
9
9
 
10
+ type LoadSaveData = unknown;
11
+ export type LoadSaveDataTable = Record<string, Array<LoadSaveData>>;
12
+
10
13
  export default abstract class AbstractComponent {
11
14
  protected lastChangedTime: number;
12
15
  protected abstract contentBBox: Rect2;
13
- public zIndex: number;
16
+ private zIndex: number;
14
17
 
15
18
  // Topmost z-index
16
19
  private static zIndexCounter: number = 0;
@@ -20,9 +23,25 @@ export default abstract class AbstractComponent {
20
23
  this.zIndex = AbstractComponent.zIndexCounter++;
21
24
  }
22
25
 
26
+ // Get and manage data attached by a loader.
27
+ private loadSaveData: LoadSaveDataTable = {};
28
+ public attachLoadSaveData(key: string, data: LoadSaveData) {
29
+ if (!this.loadSaveData[key]) {
30
+ this.loadSaveData[key] = [];
31
+ }
32
+ this.loadSaveData[key].push(data);
33
+ }
34
+ public getLoadSaveData(): LoadSaveDataTable {
35
+ return this.loadSaveData;
36
+ }
37
+
38
+ public getZIndex(): number {
39
+ return this.zIndex;
40
+ }
23
41
  public getBBox(): Rect2 {
24
42
  return this.contentBBox;
25
43
  }
44
+
26
45
  public abstract render(canvas: AbstractRenderer, visibleRect?: Rect2): void;
27
46
  public abstract intersects(lineSegment: LineSegment2): boolean;
28
47
 
@@ -1,8 +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 from '../rendering/AbstractRenderer';
5
- import SVGRenderer from '../rendering/SVGRenderer';
4
+ import AbstractRenderer from '../rendering/renderers/AbstractRenderer';
5
+ import SVGRenderer from '../rendering/renderers/SVGRenderer';
6
6
  import AbstractComponent from './AbstractComponent';
7
7
  import { ImageComponentLocalization } from './localization';
8
8
 
@@ -2,7 +2,7 @@ import LineSegment2 from '../geometry/LineSegment2';
2
2
  import Mat33 from '../geometry/Mat33';
3
3
  import Path from '../geometry/Path';
4
4
  import Rect2 from '../geometry/Rect2';
5
- import AbstractRenderer, { RenderablePathSpec, RenderingStyle } from '../rendering/AbstractRenderer';
5
+ import AbstractRenderer, { RenderablePathSpec, RenderingStyle } from '../rendering/renderers/AbstractRenderer';
6
6
  import AbstractComponent from './AbstractComponent';
7
7
  import { ImageComponentLocalization } from './localization';
8
8
 
@@ -58,7 +58,7 @@ export default class Stroke extends AbstractComponent {
58
58
  canvas.drawPath(part);
59
59
  }
60
60
  }
61
- canvas.endObject();
61
+ canvas.endObject(this.getLoadSaveData());
62
62
  }
63
63
 
64
64
  // Grows the bounding box for a given stroke part based on that part's style.
@@ -1,8 +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 from '../rendering/AbstractRenderer';
5
- import SVGRenderer from '../rendering/SVGRenderer';
4
+ import AbstractRenderer from '../rendering/renderers/AbstractRenderer';
5
+ import SVGRenderer from '../rendering/renderers/SVGRenderer';
6
6
  import AbstractComponent from './AbstractComponent';
7
7
  import { ImageComponentLocalization } from './localization';
8
8
 
@@ -1,6 +1,6 @@
1
1
  import { PathCommandType } from '../../geometry/Path';
2
2
  import Rect2 from '../../geometry/Rect2';
3
- import AbstractRenderer from '../../rendering/AbstractRenderer';
3
+ import AbstractRenderer from '../../rendering/renderers/AbstractRenderer';
4
4
  import { StrokeDataPoint } from '../../types';
5
5
  import Viewport from '../../Viewport';
6
6
  import AbstractComponent from '../AbstractComponent';
@@ -1,5 +1,5 @@
1
1
  import { Bezier } from 'bezier-js';
2
- import AbstractRenderer, { RenderingStyle, RenderablePathSpec } from '../../rendering/AbstractRenderer';
2
+ import AbstractRenderer, { RenderingStyle, 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';
@@ -212,7 +212,7 @@ export default class FreehandLineBuilder implements ComponentBuilder {
212
212
  const lowerBoundary = computeBoundaryCurve(-1, halfVec);
213
213
 
214
214
  // If the boundaries have two intersections, increasing the half vector's length could fix this.
215
- if (upperBoundary.intersects(lowerBoundary).length === 2) {
215
+ if (upperBoundary.intersects(lowerBoundary).length > 0) {
216
216
  halfVec = halfVec.times(2);
217
217
  }
218
218
 
@@ -1,6 +1,6 @@
1
1
  import { PathCommandType } from '../../geometry/Path';
2
2
  import Rect2 from '../../geometry/Rect2';
3
- import AbstractRenderer from '../../rendering/AbstractRenderer';
3
+ import AbstractRenderer from '../../rendering/renderers/AbstractRenderer';
4
4
  import { StrokeDataPoint } from '../../types';
5
5
  import Viewport from '../../Viewport';
6
6
  import AbstractComponent from '../AbstractComponent';
@@ -1,6 +1,6 @@
1
1
  import Path from '../../geometry/Path';
2
2
  import Rect2 from '../../geometry/Rect2';
3
- import AbstractRenderer from '../../rendering/AbstractRenderer';
3
+ import AbstractRenderer from '../../rendering/renderers/AbstractRenderer';
4
4
  import { StrokeDataPoint } from '../../types';
5
5
  import Viewport from '../../Viewport';
6
6
  import AbstractComponent from '../AbstractComponent';
@@ -1,5 +1,5 @@
1
1
  import Rect2 from '../../geometry/Rect2';
2
- import AbstractRenderer from '../../rendering/AbstractRenderer';
2
+ import AbstractRenderer from '../../rendering/renderers/AbstractRenderer';
3
3
  import { StrokeDataPoint } from '../../types';
4
4
  import Viewport from '../../Viewport';
5
5
  import AbstractComponent from '../AbstractComponent';
@@ -7,6 +7,9 @@ import Vec3 from './Vec3';
7
7
  export default class Mat33 {
8
8
  private readonly rows: Vec3[];
9
9
 
10
+ // ⎡ a1 a2 a3 ⎤
11
+ // ⎢ b1 b2 b3 ⎥
12
+ // ⎣ c1 c2 c3 ⎦
10
13
  public constructor(
11
14
  public readonly a1: number,
12
15
  public readonly a2: number,
@@ -90,15 +90,38 @@ describe('Path.fromString', () => {
90
90
  ]);
91
91
  });
92
92
 
93
+ it('should break compoents at -s', () => {
94
+ const path = Path.fromString('m1-1 L-1-1-3-4-5-6,5-1');
95
+ expect(path.parts.length).toBe(4);
96
+ expect(path.parts).toMatchObject([
97
+ {
98
+ kind: PathCommandType.LineTo,
99
+ point: Vec2.of(-1, -1),
100
+ },
101
+ {
102
+ kind: PathCommandType.LineTo,
103
+ point: Vec2.of(-3, -4),
104
+ },
105
+ {
106
+ kind: PathCommandType.LineTo,
107
+ point: Vec2.of(-5, -6),
108
+ },
109
+ {
110
+ kind: PathCommandType.LineTo,
111
+ point: Vec2.of(5, -1),
112
+ },
113
+ ]);
114
+ });
115
+
93
116
  it('should properly handle cubic Bézier curves', () => {
94
- const path = Path.fromString('c1,1 0,-3 4 5 C1,1 0.1, 0.1 0, 0');
117
+ const path = Path.fromString('m1,1 c1,1 0-3 4 5 C1,1 0.1, 0.1 0, 0');
95
118
  expect(path.parts.length).toBe(2);
96
119
  expect(path.parts).toMatchObject([
97
120
  {
98
121
  kind: PathCommandType.CubicBezierTo,
99
- controlPoint1: Vec2.of(1, 1),
122
+ controlPoint1: Vec2.of(2, 2),
100
123
  controlPoint2: Vec2.of(1, -2),
101
- endPoint: Vec2.of(5, 3),
124
+ endPoint: Vec2.of(5, 6),
102
125
  },
103
126
  {
104
127
  kind: PathCommandType.CubicBezierTo,
@@ -120,7 +143,7 @@ describe('Path.fromString', () => {
120
143
  {
121
144
  kind: PathCommandType.QuadraticBezierTo,
122
145
  controlPoint: Vec2.of(1, 1),
123
- endPoint: Vec2.of(-2, -3),
146
+ endPoint: Vec2.of(-1, -1),
124
147
  },
125
148
  {
126
149
  kind: PathCommandType.QuadraticBezierTo,
@@ -130,4 +153,71 @@ describe('Path.fromString', () => {
130
153
  ]);
131
154
  expect(path.startPoint).toMatchObject(Vec2.of(1, 1));
132
155
  });
156
+
157
+ it('should correctly handle a command followed by multiple sets of arguments', () => {
158
+ // Commands followed by multiple sets of arguments, for example,
159
+ // l 5,10 5,4 3,2,
160
+ // should be interpreted as multiple commands. Our example, is therefore equivalent to,
161
+ // l 5,10 l 5,4 l 3,2
162
+
163
+ const path = Path.fromString(`
164
+ L5,10 1,1
165
+ 2,2 -3,-1
166
+ q 1,2 1,1
167
+ -1,-1 -3,-4
168
+ h -4 -1
169
+ V 3 5 1
170
+ `);
171
+ expect(path.parts).toMatchObject([
172
+ {
173
+ kind: PathCommandType.LineTo,
174
+ point: Vec2.of(1, 1),
175
+ },
176
+ {
177
+ kind: PathCommandType.LineTo,
178
+ point: Vec2.of(2, 2),
179
+ },
180
+ {
181
+ kind: PathCommandType.LineTo,
182
+ point: Vec2.of(-3, -1),
183
+ },
184
+
185
+ // q 1,2 1,1 -1,-1 -3,-4
186
+ {
187
+ kind: PathCommandType.QuadraticBezierTo,
188
+ controlPoint: Vec2.of(-2, 1),
189
+ endPoint: Vec2.of(-2, 0),
190
+ },
191
+ {
192
+ kind: PathCommandType.QuadraticBezierTo,
193
+ controlPoint: Vec2.of(-3, -1),
194
+ endPoint: Vec2.of(-5, -4),
195
+ },
196
+
197
+ // h -4 -1
198
+ {
199
+ kind: PathCommandType.LineTo,
200
+ point: Vec2.of(-9, -4),
201
+ },
202
+ {
203
+ kind: PathCommandType.LineTo,
204
+ point: Vec2.of(-10, -4),
205
+ },
206
+
207
+ // V 3 5 1
208
+ {
209
+ kind: PathCommandType.LineTo,
210
+ point: Vec2.of(-10, 3),
211
+ },
212
+ {
213
+ kind: PathCommandType.LineTo,
214
+ point: Vec2.of(-10, 5),
215
+ },
216
+ {
217
+ kind: PathCommandType.LineTo,
218
+ point: Vec2.of(-10, 1),
219
+ },
220
+ ]);
221
+ expect(path.startPoint).toMatchObject(Vec2.of(5, 10));
222
+ });
133
223
  });
@@ -19,13 +19,23 @@ describe('Path.toString', () => {
19
19
  });
20
20
 
21
21
  it('should fix rounding errors', () => {
22
- const path = new Path(Vec2.of(0.100001, 0.199999), [
22
+ const path = new Path(Vec2.of(0.10000001, 0.19999999), [
23
23
  {
24
24
  kind: PathCommandType.QuadraticBezierTo,
25
25
  controlPoint: Vec2.of(9999, -10.999999995),
26
- endPoint: Vec2.of(0.000300001, 1.400002),
26
+ endPoint: Vec2.of(0.000300001, 1.40000002),
27
27
  },
28
28
  ]);
29
29
  expect(path.toString()).toBe('M0.1,0.2Q9999,-11 0.0003,1.4');
30
30
  });
31
+
32
+ it('should not remove trailing zeroes before decimal points', () => {
33
+ const path = new Path(Vec2.of(1000, 2_000_000), [
34
+ {
35
+ kind: PathCommandType.LineTo,
36
+ point: Vec2.of(30.0001, 40.000000001),
37
+ },
38
+ ]);
39
+ expect(path.toString()).toBe('M1000,2000000L30.0001,40');
40
+ });
31
41
  });