@js-draw/math 1.0.0 → 1.0.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 (61) hide show
  1. package/LICENSE +21 -0
  2. package/dist/cjs/Color4.test.d.ts +1 -0
  3. package/dist/cjs/Mat33.test.d.ts +1 -0
  4. package/dist/cjs/Vec2.test.d.ts +1 -0
  5. package/dist/cjs/Vec3.test.d.ts +1 -0
  6. package/dist/cjs/polynomial/solveQuadratic.test.d.ts +1 -0
  7. package/dist/cjs/rounding.test.d.ts +1 -0
  8. package/dist/cjs/shapes/LineSegment2.test.d.ts +1 -0
  9. package/dist/cjs/shapes/Path.fromString.test.d.ts +1 -0
  10. package/dist/cjs/shapes/Path.test.d.ts +1 -0
  11. package/dist/cjs/shapes/Path.toString.test.d.ts +1 -0
  12. package/dist/cjs/shapes/QuadraticBezier.test.d.ts +1 -0
  13. package/dist/cjs/shapes/Rect2.test.d.ts +1 -0
  14. package/dist/cjs/shapes/Triangle.test.d.ts +1 -0
  15. package/dist/mjs/Color4.test.d.ts +1 -0
  16. package/dist/mjs/Mat33.test.d.ts +1 -0
  17. package/dist/mjs/Vec2.test.d.ts +1 -0
  18. package/dist/mjs/Vec3.test.d.ts +1 -0
  19. package/dist/mjs/polynomial/solveQuadratic.test.d.ts +1 -0
  20. package/dist/mjs/rounding.test.d.ts +1 -0
  21. package/dist/mjs/shapes/LineSegment2.test.d.ts +1 -0
  22. package/dist/mjs/shapes/Path.fromString.test.d.ts +1 -0
  23. package/dist/mjs/shapes/Path.test.d.ts +1 -0
  24. package/dist/mjs/shapes/Path.toString.test.d.ts +1 -0
  25. package/dist/mjs/shapes/QuadraticBezier.test.d.ts +1 -0
  26. package/dist/mjs/shapes/Rect2.test.d.ts +1 -0
  27. package/dist/mjs/shapes/Triangle.test.d.ts +1 -0
  28. package/dist-test/test_imports/package-lock.json +13 -0
  29. package/dist-test/test_imports/package.json +12 -0
  30. package/dist-test/test_imports/test-imports.js +15 -0
  31. package/dist-test/test_imports/test-require.cjs +15 -0
  32. package/package.json +4 -3
  33. package/src/Color4.test.ts +52 -0
  34. package/src/Color4.ts +318 -0
  35. package/src/Mat33.test.ts +244 -0
  36. package/src/Mat33.ts +450 -0
  37. package/src/Vec2.test.ts +30 -0
  38. package/src/Vec2.ts +49 -0
  39. package/src/Vec3.test.ts +51 -0
  40. package/src/Vec3.ts +245 -0
  41. package/src/lib.ts +42 -0
  42. package/src/polynomial/solveQuadratic.test.ts +39 -0
  43. package/src/polynomial/solveQuadratic.ts +43 -0
  44. package/src/rounding.test.ts +65 -0
  45. package/src/rounding.ts +167 -0
  46. package/src/shapes/Abstract2DShape.ts +63 -0
  47. package/src/shapes/BezierJSWrapper.ts +93 -0
  48. package/src/shapes/CubicBezier.ts +35 -0
  49. package/src/shapes/LineSegment2.test.ts +99 -0
  50. package/src/shapes/LineSegment2.ts +232 -0
  51. package/src/shapes/Path.fromString.test.ts +223 -0
  52. package/src/shapes/Path.test.ts +309 -0
  53. package/src/shapes/Path.toString.test.ts +77 -0
  54. package/src/shapes/Path.ts +963 -0
  55. package/src/shapes/PointShape2D.ts +33 -0
  56. package/src/shapes/QuadraticBezier.test.ts +31 -0
  57. package/src/shapes/QuadraticBezier.ts +142 -0
  58. package/src/shapes/Rect2.test.ts +209 -0
  59. package/src/shapes/Rect2.ts +346 -0
  60. package/src/shapes/Triangle.test.ts +61 -0
  61. package/src/shapes/Triangle.ts +139 -0
@@ -0,0 +1,223 @@
1
+ // Tests to ensure that Paths can be deserialized
2
+
3
+ import Path, { PathCommandType } from './Path';
4
+ import { Vec2 } from '../Vec2';
5
+
6
+ describe('Path.fromString', () => {
7
+ it('should handle an empty path', () => {
8
+ const path = Path.fromString('');
9
+ expect(path.geometry.length).toBe(0);
10
+ });
11
+
12
+ it('should properly handle absolute moveTo commands', () => {
13
+ const path1 = Path.fromString('M0,0');
14
+ const path2 = Path.fromString('M 0 0');
15
+ const path3 = Path.fromString('M 1,1M 2,2 M 3,3');
16
+
17
+ expect(path1.parts.length).toBe(0);
18
+ expect(path1.startPoint).toMatchObject(Vec2.zero);
19
+
20
+ expect(path2.parts.length).toBe(0);
21
+ expect(path2.startPoint).toMatchObject(Vec2.zero);
22
+
23
+ expect(path3.parts).toMatchObject([
24
+ {
25
+ kind: PathCommandType.MoveTo,
26
+ point: Vec2.of(2, 2),
27
+ },
28
+ {
29
+ kind: PathCommandType.MoveTo,
30
+ point: Vec2.of(3,3),
31
+ },
32
+ ]);
33
+ expect(path3.startPoint).toMatchObject(Vec2.of(1, 1));
34
+ });
35
+
36
+ it('should properly handle relative moveTo commands', () => {
37
+ const path = Path.fromString('m1,1 m0,0 m 3,3');
38
+ expect(path.parts).toMatchObject([
39
+ {
40
+ kind: PathCommandType.MoveTo,
41
+ point: Vec2.of(1, 1),
42
+ },
43
+ {
44
+ kind: PathCommandType.MoveTo,
45
+ point: Vec2.of(4, 4),
46
+ },
47
+ ]);
48
+ expect(path.startPoint).toMatchObject(Vec2.of(1, 1));
49
+ });
50
+
51
+ it('should handle lineTo commands', () => {
52
+ const path = Path.fromString('l1,2L-1,0l0.1,-1.0');
53
+ // l is a relative lineTo, but because there
54
+ // is no previous command, it should act like an
55
+ // absolute moveTo.
56
+ expect(path.startPoint).toMatchObject(Vec2.of(1, 2));
57
+
58
+ expect(path.parts).toMatchObject([
59
+ {
60
+ kind: PathCommandType.LineTo,
61
+ point: Vec2.of(-1, 0),
62
+ },
63
+ {
64
+ kind: PathCommandType.LineTo,
65
+ point: Vec2.of(-0.9, -1.0),
66
+ },
67
+ ]);
68
+ });
69
+
70
+ it('"z" should close strokes', () => {
71
+ const path1 = Path.fromString('m3,3 l1,2 l1,1 z');
72
+ const path2 = Path.fromString('m3,3 l1,2 l1,1 Z');
73
+
74
+ expect(path1.startPoint).toMatchObject(Vec2.of(3,3));
75
+ expect(path2.startPoint).toMatchObject(path1.startPoint);
76
+ expect(path1.parts).toMatchObject(path2.parts);
77
+ expect(path1.parts).toMatchObject([
78
+ {
79
+ kind: PathCommandType.LineTo,
80
+ point: Vec2.of(4, 5),
81
+ },
82
+ {
83
+ kind: PathCommandType.LineTo,
84
+ point: Vec2.of(5, 6),
85
+ },
86
+ {
87
+ kind: PathCommandType.LineTo,
88
+ point: path1.startPoint,
89
+ }
90
+ ]);
91
+ });
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
+
116
+ it('should properly handle cubic Bézier curves', () => {
117
+ const path = Path.fromString('m1,1 c1,1 0-3 4 5 C1,1 0.1, 0.1 0, 0');
118
+ expect(path.parts.length).toBe(2);
119
+ expect(path.parts).toMatchObject([
120
+ {
121
+ kind: PathCommandType.CubicBezierTo,
122
+ controlPoint1: Vec2.of(2, 2),
123
+ controlPoint2: Vec2.of(1, -2),
124
+ endPoint: Vec2.of(5, 6),
125
+ },
126
+ {
127
+ kind: PathCommandType.CubicBezierTo,
128
+ controlPoint1: Vec2.of(1, 1),
129
+ controlPoint2: Vec2.of(0.1, 0.1),
130
+ endPoint: Vec2.zero,
131
+ }
132
+ ]);
133
+ });
134
+
135
+ it('should handle quadratic Bézier curves', () => {
136
+ const path = Path.fromString(' Q1 1,2 3 q-1 -2,-3 -4 Q 1 2,3 4');
137
+ expect(path.parts).toMatchObject([
138
+ {
139
+ kind: PathCommandType.QuadraticBezierTo,
140
+ controlPoint: Vec2.of(1, 1),
141
+ endPoint: Vec2.of(2, 3),
142
+ },
143
+ {
144
+ kind: PathCommandType.QuadraticBezierTo,
145
+ controlPoint: Vec2.of(1, 1),
146
+ endPoint: Vec2.of(-1, -1),
147
+ },
148
+ {
149
+ kind: PathCommandType.QuadraticBezierTo,
150
+ controlPoint: Vec2.of(1, 2),
151
+ endPoint: Vec2.of(3, 4),
152
+ },
153
+ ]);
154
+ expect(path.startPoint).toMatchObject(Vec2.of(1, 1));
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
+ });
223
+ });
@@ -0,0 +1,309 @@
1
+ import LineSegment2 from './LineSegment2';
2
+ import Path, { PathCommandType } from './Path';
3
+ import Rect2 from './Rect2';
4
+ import { Vec2 } from '../Vec2';
5
+ import CubicBezier from './CubicBezier';
6
+ import QuadraticBezier from './QuadraticBezier';
7
+
8
+ describe('Path', () => {
9
+ it('should instantiate Beziers from cubic and quatratic commands', () => {
10
+ const path = new Path(Vec2.zero, [
11
+ {
12
+ kind: PathCommandType.CubicBezierTo,
13
+ controlPoint1: Vec2.of(1, 1),
14
+ controlPoint2: Vec2.of(-1, -1),
15
+ endPoint: Vec2.of(3, 3),
16
+ },
17
+ {
18
+ kind: PathCommandType.QuadraticBezierTo,
19
+ controlPoint: Vec2.of(1, 1),
20
+ endPoint: Vec2.of(0, 0),
21
+ },
22
+ ]);
23
+
24
+ expect(path.geometry.length).toBe(2);
25
+
26
+ const firstItem = path.geometry[0];
27
+ const secondItem = path.geometry[1];
28
+ expect(firstItem).toBeInstanceOf(CubicBezier);
29
+ expect(secondItem).toBeInstanceOf(QuadraticBezier);
30
+
31
+ // Force TypeScript to do type narrowing.
32
+ if (!(firstItem instanceof CubicBezier) || !(secondItem instanceof QuadraticBezier)) {
33
+ throw new Error('Invalid state! .toBeInstanceOf should have caused test to fail!');
34
+ }
35
+
36
+ // Make sure the control points (and start/end points) match what was set
37
+ expect(firstItem.getPoints()).toMatchObject([
38
+ { x: 0, y: 0 }, { x: 1, y: 1 }, { x: -1, y: -1 }, { x: 3, y: 3 }
39
+ ]);
40
+ expect(secondItem.getPoints()).toMatchObject([
41
+ { x: 3, y: 3 }, { x: 1, y: 1 }, { x: 0, y: 0 },
42
+ ]);
43
+ });
44
+
45
+ it('should create LineSegments from line commands', () => {
46
+ const lineStart = Vec2.zero;
47
+ const lineEnd = Vec2.of(100, 100);
48
+
49
+ const path = new Path(lineStart, [
50
+ {
51
+ kind: PathCommandType.LineTo,
52
+ point: lineEnd,
53
+ },
54
+ ]);
55
+
56
+ expect(path.geometry.length).toBe(1);
57
+ expect(path.geometry[0]).toBeInstanceOf(LineSegment2);
58
+ expect(path.geometry[0]).toMatchObject(
59
+ new LineSegment2(lineStart, lineEnd)
60
+ );
61
+ });
62
+
63
+ describe('intersection', () => {
64
+ it('should give all intersections for a path made up of lines', () => {
65
+ const lineStart = Vec2.of(100, 100);
66
+ const path = new Path(lineStart, [
67
+ {
68
+ kind: PathCommandType.LineTo,
69
+ point: Vec2.of(-100, 100),
70
+ },
71
+ {
72
+ kind: PathCommandType.LineTo,
73
+ point: Vec2.of(0, 0),
74
+ },
75
+ {
76
+ kind: PathCommandType.LineTo,
77
+ point: Vec2.of(100, -100),
78
+ },
79
+ ]);
80
+
81
+ const intersections = path.intersection(
82
+ new LineSegment2(Vec2.of(-50, 200), Vec2.of(-50, -200))
83
+ );
84
+
85
+ // Should only have intersections in quadrants II and III.
86
+ expect(intersections.length).toBe(2);
87
+
88
+ // First intersection should be with the first curve
89
+ const firstIntersection = intersections[0];
90
+ expect(firstIntersection.point.xy).toMatchObject({
91
+ x: -50,
92
+ y: 100,
93
+ });
94
+ });
95
+
96
+ it('should give all intersections for a stroked path', () => {
97
+ const lineStart = Vec2.of(100, 100);
98
+
99
+ // Create a path in this shape:
100
+ // + - - - -|- - - - +
101
+ // \ |
102
+ // \ |
103
+ // \ |
104
+ // \ |
105
+ // ---------------------
106
+ // | \
107
+ // | \
108
+ // | \
109
+ // | \
110
+ // | +
111
+ const path = new Path(lineStart, [
112
+ {
113
+ kind: PathCommandType.LineTo,
114
+ point: Vec2.of(-100, 100),
115
+ },
116
+ {
117
+ kind: PathCommandType.LineTo,
118
+ point: Vec2.of(0, 0),
119
+ },
120
+ {
121
+ kind: PathCommandType.LineTo,
122
+ point: Vec2.of(100, -100),
123
+ },
124
+ ]);
125
+
126
+ const strokeWidth = 5;
127
+ let intersections = path.intersection(
128
+ new LineSegment2(Vec2.of(2000, 200), Vec2.of(2000, 400)), strokeWidth,
129
+ );
130
+ expect(intersections.length).toBe(0);
131
+
132
+ // Test a line that only enters, but does not exit one of the strokes:
133
+ //
134
+ // * <- Line to test
135
+ // |
136
+ // _____|_____________
137
+ // |
138
+ // * Stroke
139
+ //
140
+ // ‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾
141
+
142
+ intersections = path.intersection(
143
+ new LineSegment2(Vec2.of(-50, 200), Vec2.of(-50, 100)), strokeWidth,
144
+ );
145
+ expect(intersections.length).toBe(1);
146
+ expect(intersections[0].point.xy).toMatchObject({
147
+ x: -50,
148
+ y: 105,
149
+ });
150
+
151
+
152
+ // Changing the order of the end points on the line should not change the result
153
+ intersections = path.intersection(
154
+ new LineSegment2(Vec2.of(-50, 100), Vec2.of(-50, 200)), strokeWidth,
155
+ );
156
+ expect(intersections.length).toBe(1);
157
+ expect(intersections[0].point.xy).toMatchObject({
158
+ x: -50,
159
+ y: 105,
160
+ });
161
+
162
+ // This line should intersect two of the strokes. Thus, there should be four
163
+ // intersections — one entering and one leaving for each intersection with the
164
+ // centers.
165
+ intersections = path.intersection(
166
+ new LineSegment2(Vec2.of(-50, 200), Vec2.of(-50, -200)), strokeWidth,
167
+ );
168
+ expect(intersections.length).toBe(4);
169
+
170
+ // Intersections should be in increasing order away from the
171
+ // first point on the line.
172
+ expect(intersections[0].point.xy).toMatchObject({
173
+ x: -50,
174
+ y: 105,
175
+ });
176
+ expect(intersections[1].point.xy).toMatchObject({
177
+ x: -50,
178
+ y: 95,
179
+ });
180
+ });
181
+
182
+ it('should give all intersections for a Bézier stroked path', () => {
183
+ const lineStart = Vec2.zero;
184
+ const path = new Path(lineStart, [
185
+ {
186
+ kind: PathCommandType.QuadraticBezierTo,
187
+ controlPoint: Vec2.unitX,
188
+ endPoint: Vec2.unitY,
189
+ }
190
+ ]);
191
+
192
+ const strokeWidth = 5;
193
+
194
+ // Should be no intersections for a line contained entirely within the stroke
195
+ // (including stroke width).
196
+ let intersections = path.intersection(
197
+ new LineSegment2(Vec2.of(-1, 0.5), Vec2.of(2, 0.5)), strokeWidth,
198
+ );
199
+ expect(intersections.length).toBe(0);
200
+
201
+ // Should be an intersection when exiting/entering the edge of the stroke
202
+ intersections = path.intersection(
203
+ new LineSegment2(Vec2.of(0, 0.5), Vec2.of(8, 0.5)), strokeWidth,
204
+ );
205
+ expect(intersections.length).toBe(1);
206
+ });
207
+ });
208
+
209
+ describe('polylineApproximation', () => {
210
+ it('should approximate Bézier curves with polylines', () => {
211
+ const path = Path.fromString('m0,0 l4,4 Q 1,4 4,1z');
212
+
213
+ expect(path.polylineApproximation()).toMatchObject([
214
+ new LineSegment2(Vec2.of(0, 0), Vec2.of(4, 4)),
215
+ new LineSegment2(Vec2.of(4, 4), Vec2.of(1, 4)),
216
+ new LineSegment2(Vec2.of(1, 4), Vec2.of(4, 1)),
217
+ new LineSegment2(Vec2.of(4, 1), Vec2.of(0, 0)),
218
+ ]);
219
+ });
220
+ });
221
+
222
+ describe('roughlyIntersectsClosed', () => {
223
+ it('small, line-only path', () => {
224
+ const path = Path.fromString('m0,0 l10,10 L0,10 z');
225
+ expect(
226
+ path.closedRoughlyIntersects(Rect2.fromCorners(Vec2.zero, Vec2.of(20, 20)))
227
+ ).toBe(true);
228
+ expect(
229
+ path.closedRoughlyIntersects(Rect2.fromCorners(Vec2.zero, Vec2.of(2, 2)))
230
+ ).toBe(true);
231
+ expect(
232
+ path.closedRoughlyIntersects(new Rect2(10, 1, 1, 1))
233
+ ).toBe(false);
234
+ expect(
235
+ path.closedRoughlyIntersects(new Rect2(1, 5, 1, 1))
236
+ ).toBe(true);
237
+ });
238
+
239
+ it('path with Bézier curves', () => {
240
+ const path = Path.fromString(`
241
+ M1090,2560
242
+ L1570,2620
243
+ Q1710,1300 1380,720
244
+ Q980,100 -460,-640
245
+ L-680,-200
246
+ Q670,470 960,980
247
+ Q1230,1370 1090,2560
248
+ `);
249
+ expect(
250
+ path.closedRoughlyIntersects(new Rect2(0, 0, 500, 500))
251
+ ).toBe(true);
252
+ expect(
253
+ path.closedRoughlyIntersects(new Rect2(0, 0, 5, 5))
254
+ ).toBe(true);
255
+ expect(
256
+ path.closedRoughlyIntersects(new Rect2(-10000, 0, 500, 500))
257
+ ).toBe(false);
258
+ });
259
+ });
260
+
261
+ describe('roughlyIntersects', () => {
262
+ it('should consider parts outside bbox of individual parts of a line as not intersecting', () => {
263
+ const path = Path.fromString(`
264
+ M10,10
265
+ L20,20
266
+ L100,21
267
+ `);
268
+ expect(
269
+ path.roughlyIntersects(new Rect2(0, 0, 50, 50))
270
+ ).toBe(true);
271
+ expect(
272
+ path.roughlyIntersects(new Rect2(0, 0, 5, 5))
273
+ ).toBe(false);
274
+ expect(
275
+ path.roughlyIntersects(new Rect2(8, 22, 1, 1))
276
+ ).toBe(false);
277
+ expect(
278
+ path.roughlyIntersects(new Rect2(21, 11, 1, 1))
279
+ ).toBe(false);
280
+ expect(
281
+ path.roughlyIntersects(new Rect2(50, 19, 1, 2))
282
+ ).toBe(true);
283
+ });
284
+ });
285
+
286
+ describe('fromRect', () => {
287
+ const filledRect = Path.fromRect(Rect2.unitSquare);
288
+ const strokedRect = Path.fromRect(Rect2.unitSquare, 0.1);
289
+
290
+ it('filled should be closed shape', () => {
291
+ const lastSegment = filledRect.parts[filledRect.parts.length - 1];
292
+
293
+ if (lastSegment.kind !== PathCommandType.LineTo) {
294
+ throw new Error('Rectangles should only be made up of lines');
295
+ }
296
+
297
+ expect(filledRect.startPoint).objEq(lastSegment.point);
298
+ });
299
+
300
+ it('stroked should be closed shape', () => {
301
+ const lastSegment = strokedRect.parts[strokedRect.parts.length - 1];
302
+ if (lastSegment.kind !== PathCommandType.LineTo) {
303
+ throw new Error('Rectangles should only be made up of lines');
304
+ }
305
+
306
+ expect(strokedRect.startPoint).objEq(lastSegment.point);
307
+ });
308
+ });
309
+ });
@@ -0,0 +1,77 @@
1
+ import Path, { PathCommandType } from './Path';
2
+ import { Vec2 } from '../Vec2';
3
+
4
+
5
+ describe('Path.toString', () => {
6
+ it('a single-point path should produce a move-to command', () => {
7
+ const path = new Path(Vec2.of(0, 0), []);
8
+ expect(path.toString()).toBe('M0,0');
9
+ });
10
+
11
+ it('should convert lineTo commands to L SVG commands', () => {
12
+ const path = new Path(Vec2.of(0.1, 0.2), [
13
+ {
14
+ kind: PathCommandType.LineTo,
15
+ point: Vec2.of(0.3, 0.4),
16
+ },
17
+ ]);
18
+ expect(path.toString()).toBe('M.1,.2L.3,.4');
19
+ });
20
+
21
+ it('should fix rounding errors', () => {
22
+ const path = new Path(Vec2.of(0.100000001, 0.199999999), [
23
+ {
24
+ kind: PathCommandType.QuadraticBezierTo,
25
+ controlPoint: Vec2.of(9999, -10.999999995),
26
+ endPoint: Vec2.of(0.000300001, 1.400000002),
27
+ },
28
+ {
29
+ kind: PathCommandType.LineTo,
30
+ point: Vec2.of(184.00482359999998, 1)
31
+ }
32
+ ]);
33
+
34
+ expect(path.toString()).toBe('M.1,.2Q9999,-11 .0003,1.4L184.0048236,1');
35
+ });
36
+
37
+ it('should not remove trailing zeroes before decimal points', () => {
38
+ const path = new Path(Vec2.of(1000, 2_000_000), [
39
+ {
40
+ kind: PathCommandType.LineTo,
41
+ point: Vec2.of(30.00000001, 40.000000001),
42
+ },
43
+ ]);
44
+
45
+ expect(path.toString()).toBe('M1000,2000000l-970-1999960');
46
+ });
47
+
48
+ it('deserialized path should serialize to the same/similar path, but with rounded components', () => {
49
+ const path1 = Path.fromString('M100,100 L101,101 Q102,102 90.000000001,89.99999999 Z');
50
+ path1['cachedStringVersion'] = null; // Clear the cache.
51
+
52
+ expect(path1.toString()).toBe([
53
+ 'M100,100', 'l1,1', 'q1,1 -11-11', 'l10,10'
54
+ ].join(''));
55
+ });
56
+
57
+ it('should not lose precision when saving', () => {
58
+ const pathStr = 'M184.2,52.3l-.2-.2q-2.7,2.4 -3.2,3.5q-2.8,7 -.9,6.1q4.3-2.6 4.8-6.1q1.2-8.8 .4-8.3q-4.2,5.2 -3.9,3.9q.2-1.6 .3-2.1q.2-1.3 -.2-1q-3.8,6.5 -3.2,3.3q.6-4.1 1.1-5.3q4.1-10 3.3-8.3q-5.3,13.1 -6.6,14.1q-3.3,2.8 -1.8-1.5q2.8-9.7 2.7-8.4q0,.3 0,.4q-1.4,7.1 -2.7,8.5q-2.6,3.2 -2.5,2.9q-.3-1.9 -.7-1.9q-4.1,4.4 -2.9,1.9q1.1-3 .3-2.6q-1.8,2 -2.5,2.4q-4.5,2.8 -4.2,1.9q.3-1.6 .2-1.4q1.5,2.2 1.3,2.9q-.8,3.9 -.5,3.3q.8-7.6 2.5-13.3q2.6-9.2 2.9-6.9q.3,1.4 .3,1.2q-.7-.4 -.9,0q-2.2,11.6 -7.6,13.6q-3.9,1.6 -2.1-1.3q3-5.5 2.6-3.4q-.2,1.8 -.5,1.8q-3.2,.5 -4.1,1.2q-2.6,2.6 -1.9,2.5q4.7-4.4 3.7-5.5q-1.1-.9 -1.6-.6q-7.2,7.5 -3.9,6.5q.3-.1 .4-.4q.6-5.3 -.2-4.9q-2.8,2.3 -3.1,2.4q-3.7,1.5 -3.5,.5q.3-3.6 1.4-3.3q3.5,.7 1.9,2.4q-1.7,2.3 -1.6,.8q0-3.5 -.9-3.1q-5.1,3.3 -4.9,2.8q.1-4 -.8-3.5q-4.3,3.4 -4.6,2.5q-1-2.1 .5-8.7l-.2,0q-1.6,6.6 -.7,8.9q.7,1.2 5.2-2.3q.4-.5 .2,3.1q.1,1 5.5-2.4q.4-.4 .3,2.7q.1,2 2.4-.4q1.7-2.3 -2.1-3.2q-1.7-.3 -2,3.7q0,1.4 4.1-.1q.3-.1 3.1-2.4q.3-.5 -.4,4.5q0-.1 -.2,0q-2.6,1.2 4.5-5.7q0-.2 .8,.6q.9,.6 -3.7,4.7q-.5,1 2.7-1.7q.6-.7 3.7-1.2q.7-.2 .9-2.2q.1-2.7 -3.4,3.2q-1.8,3.4 2.7,1.9q5.6-2.1 7.8-14q-.1,.1 .3,.4q.6,.1 .3-1.6q-.7-2.8 -3.7,6.7q-1.8,5.8 -2.5,13.5q.1,1.1 1.3-3.1q.2-1 -1.3-3.3q-.5-.5 -1,1.6q-.1,1.3 4.8-1.5q1-1 3-2q.1-.4 -1.1,2q-1.1,3.1 3.7-1.3q-.4,0 -.1,1.5q.3,.8 3.3-2.5q1.3-1.6 2.7-8.9q0-.1 0-.4q-.3-1.9 -3.5,8.2q-1.3,4.9 2.4,2.1q1.4-1.2 6.6-14.3q.8-2.4 -3.9,7.9q-.6,1.3 -1.1,5.5q-.3,3.7 4-3.1q-.2,0 -.6,.6q-.2,.6 -.3,2.3q0,1.8 4.7-3.5q.1-.5 -1.2,7.9q-.5,3.2 -4.6,5.7q-1.3,1 1.5-5.5q.4-1.1 3.01-3.5';
59
+
60
+ const path1 = Path.fromString(pathStr);
61
+ path1['cachedStringVersion'] = null; // Clear the cache.
62
+ const path = Path.fromString(path1.toString(true));
63
+ path1['cachedStringVersion'] = null; // Clear the cache.
64
+
65
+ expect(path.toString(true)).toBe(path1.toString(true));
66
+ });
67
+
68
+ it('should remove no-op move-tos', () => {
69
+ const path1 = Path.fromString('M50,75m0,0q0,12.5 0,50q0,6.3 25,0');
70
+ path1['cachedStringVersion'] = null;
71
+ const path2 = Path.fromString('M150,175M150,175q0,12.5 0,50q0,6.3 25,0');
72
+ path2['cachedStringVersion'] = null;
73
+
74
+ expect(path1.toString()).toBe('M50,75q0,12.5 0,50q0,6.3 25,0');
75
+ expect(path2.toString()).toBe('M150,175q0,12.5 0,50q0,6.3 25,0');
76
+ });
77
+ });