svg-path-commander 2.0.10 → 2.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (75) hide show
  1. package/.eslintrc.cjs +1 -0
  2. package/README.md +64 -8
  3. package/dist/svg-path-commander.cjs +1 -1
  4. package/dist/svg-path-commander.cjs.map +1 -1
  5. package/dist/svg-path-commander.d.ts +236 -43
  6. package/dist/svg-path-commander.js +1 -1
  7. package/dist/svg-path-commander.js.map +1 -1
  8. package/dist/svg-path-commander.mjs +790 -650
  9. package/dist/svg-path-commander.mjs.map +1 -1
  10. package/package.json +20 -22
  11. package/src/convert/pathToAbsolute.ts +16 -70
  12. package/src/convert/pathToCurve.ts +36 -28
  13. package/src/convert/pathToRelative.ts +33 -62
  14. package/src/index.ts +37 -39
  15. package/src/interface.ts +33 -33
  16. package/src/math/arcTools.ts +394 -0
  17. package/src/math/bezier.ts +253 -0
  18. package/src/math/cubicTools.ts +122 -0
  19. package/src/math/distanceSquareRoot.ts +3 -1
  20. package/src/math/lineTools.ts +67 -0
  21. package/src/math/midPoint.ts +3 -1
  22. package/src/math/polygonArea.ts +3 -1
  23. package/src/math/polygonLength.ts +2 -1
  24. package/src/math/quadTools.ts +98 -0
  25. package/src/parser/isMoveCommand.ts +17 -0
  26. package/src/parser/parsePathString.ts +5 -5
  27. package/src/parser/scanSegment.ts +12 -3
  28. package/src/process/absolutizeSegment.ts +58 -0
  29. package/src/process/iterate.ts +33 -0
  30. package/src/process/normalizePath.ts +34 -28
  31. package/src/process/normalizeSegment.ts +8 -9
  32. package/src/process/projection2d.ts +2 -1
  33. package/src/process/relativizeSegment.ts +61 -0
  34. package/src/process/reversePath.ts +1 -1
  35. package/src/process/roundPath.ts +8 -10
  36. package/src/process/segmentToCubic.ts +1 -1
  37. package/src/process/shortenSegment.ts +3 -3
  38. package/src/process/splitCubic.ts +8 -7
  39. package/src/process/splitPath.ts +39 -5
  40. package/src/process/transformPath.ts +81 -94
  41. package/src/types.ts +40 -1
  42. package/src/util/distanceEpsilon.ts +3 -0
  43. package/src/util/getClosestPoint.ts +1 -1
  44. package/src/util/getPathArea.ts +3 -3
  45. package/src/util/getPathBBox.ts +86 -18
  46. package/src/util/getPointAtLength.ts +98 -4
  47. package/src/util/getPropertiesAtLength.ts +4 -3
  48. package/src/util/getPropertiesAtPoint.ts +4 -1
  49. package/src/util/getTotalLength.ts +71 -4
  50. package/src/util/isPointInStroke.ts +2 -1
  51. package/src/util/shapeToPathArray.ts +8 -4
  52. package/test/class.test.ts +502 -0
  53. package/test/fixtures/getMarkup.ts +17 -0
  54. package/{cypress → test}/fixtures/shapes.js +39 -39
  55. package/test/fixtures/simpleShapes.js +75 -0
  56. package/test/static.test.ts +324 -0
  57. package/tsconfig.json +9 -4
  58. package/{vite.config.ts → vite.config.mts} +10 -1
  59. package/vitest.config-ui.mts +26 -0
  60. package/vitest.config.mts +26 -0
  61. package/cypress/e2e/svg-path-commander.spec.ts +0 -868
  62. package/cypress/fixtures/simpleShapes.js +0 -75
  63. package/cypress/plugins/esbuild-istanbul.ts +0 -50
  64. package/cypress/plugins/tsCompile.ts +0 -34
  65. package/cypress/support/commands.ts +0 -37
  66. package/cypress/support/e2e.ts +0 -21
  67. package/cypress/test.html +0 -36
  68. package/cypress.config.ts +0 -29
  69. package/src/process/fixArc.ts +0 -23
  70. package/src/util/pathLengthFactory.ts +0 -114
  71. package/src/util/segmentArcFactory.ts +0 -219
  72. package/src/util/segmentCubicFactory.ts +0 -114
  73. package/src/util/segmentLineFactory.ts +0 -45
  74. package/src/util/segmentQuadFactory.ts +0 -109
  75. /package/{cypress/fixtures/shapeObjects.js → test/fixtures/shapeObjects.ts} +0 -0
@@ -0,0 +1,253 @@
1
+ import type {
2
+ PointTuple,
3
+ DerivedPoint,
4
+ QuadPoints,
5
+ CubicPoints,
6
+ DerivedQuadPoints,
7
+ DerivedCubicPoints,
8
+ QuadCoordinates,
9
+ CubicCoordinates,
10
+ DeriveCallback,
11
+ } from '../types';
12
+
13
+ /**
14
+ * Tools from bezier.js by Mike 'Pomax' Kamermans
15
+ * @see https://github.com/Pomax/bezierjs
16
+ */
17
+
18
+ const ZERO = { x: 0, y: 0 };
19
+
20
+ const Tvalues = [
21
+ -0.0640568928626056260850430826247450385909, 0.0640568928626056260850430826247450385909,
22
+ -0.1911188674736163091586398207570696318404, 0.1911188674736163091586398207570696318404,
23
+ -0.3150426796961633743867932913198102407864, 0.3150426796961633743867932913198102407864,
24
+ -0.4337935076260451384870842319133497124524, 0.4337935076260451384870842319133497124524,
25
+ -0.5454214713888395356583756172183723700107, 0.5454214713888395356583756172183723700107,
26
+ -0.6480936519369755692524957869107476266696, 0.6480936519369755692524957869107476266696,
27
+ -0.7401241915785543642438281030999784255232, 0.7401241915785543642438281030999784255232,
28
+ -0.8200019859739029219539498726697452080761, 0.8200019859739029219539498726697452080761,
29
+ -0.8864155270044010342131543419821967550873, 0.8864155270044010342131543419821967550873,
30
+ -0.9382745520027327585236490017087214496548, 0.9382745520027327585236490017087214496548,
31
+ -0.9747285559713094981983919930081690617411, 0.9747285559713094981983919930081690617411,
32
+ -0.9951872199970213601799974097007368118745, 0.9951872199970213601799974097007368118745,
33
+ ];
34
+
35
+ const Cvalues = [
36
+ 0.1279381953467521569740561652246953718517, 0.1279381953467521569740561652246953718517,
37
+ 0.1258374563468282961213753825111836887264, 0.1258374563468282961213753825111836887264,
38
+ 0.121670472927803391204463153476262425607, 0.121670472927803391204463153476262425607,
39
+ 0.1155056680537256013533444839067835598622, 0.1155056680537256013533444839067835598622,
40
+ 0.1074442701159656347825773424466062227946, 0.1074442701159656347825773424466062227946,
41
+ 0.0976186521041138882698806644642471544279, 0.0976186521041138882698806644642471544279,
42
+ 0.086190161531953275917185202983742667185, 0.086190161531953275917185202983742667185,
43
+ 0.0733464814110803057340336152531165181193, 0.0733464814110803057340336152531165181193,
44
+ 0.0592985849154367807463677585001085845412, 0.0592985849154367807463677585001085845412,
45
+ 0.0442774388174198061686027482113382288593, 0.0442774388174198061686027482113382288593,
46
+ 0.0285313886289336631813078159518782864491, 0.0285313886289336631813078159518782864491,
47
+ 0.0123412297999871995468056670700372915759, 0.0123412297999871995468056670700372915759,
48
+ ];
49
+
50
+ /**
51
+ *
52
+ * @param points
53
+ * @returns
54
+ */
55
+ const derive = (points: QuadPoints | CubicPoints) => {
56
+ const dpoints = [] as (DerivedCubicPoints | DerivedQuadPoints)[];
57
+ for (let p = points, d = p.length, c = d - 1; d > 1; d -= 1, c -= 1) {
58
+ const list = [] as unknown as DerivedCubicPoints | DerivedQuadPoints;
59
+ for (let j = 0; j < c; j += 1) {
60
+ list.push({
61
+ x: c * (p[j + 1].x - p[j].x),
62
+ y: c * (p[j + 1].y - p[j].y),
63
+ t: 0,
64
+ });
65
+ }
66
+ dpoints.push(list);
67
+ p = list;
68
+ }
69
+ return dpoints;
70
+ };
71
+
72
+ /**
73
+ *
74
+ * @param points
75
+ * @param t
76
+ */
77
+ const compute = (points: DerivedQuadPoints | DerivedCubicPoints, t: number) => {
78
+ // shortcuts
79
+ /* istanbul ignore next @preserve */
80
+ if (t === 0) {
81
+ points[0].t = 0;
82
+ return points[0];
83
+ }
84
+
85
+ const order = points.length - 1;
86
+
87
+ /* istanbul ignore next @preserve */
88
+ if (t === 1) {
89
+ points[order].t = 1;
90
+ return points[order];
91
+ }
92
+
93
+ const mt = 1 - t;
94
+ let p = points as typeof points | [DerivedPoint, DerivedPoint, DerivedPoint, DerivedPoint];
95
+
96
+ // constant?
97
+ /* istanbul ignore next @preserve */
98
+ if (order === 0) {
99
+ points[0].t = t;
100
+ return points[0];
101
+ }
102
+
103
+ // linear?
104
+ /* istanbul ignore else @preserve */
105
+ if (order === 1) {
106
+ return {
107
+ x: mt * p[0].x + t * p[1].x,
108
+ y: mt * p[0].y + t * p[1].y,
109
+ t,
110
+ };
111
+ }
112
+
113
+ // quadratic/cubic curve?
114
+ const mt2 = mt * mt;
115
+ const t2 = t * t;
116
+ let a = 0;
117
+ let b = 0;
118
+ let c = 0;
119
+ let d = 0;
120
+ /* istanbul ignore else @preserve */
121
+ if (order === 2) {
122
+ p = [p[0], p[1], p[2], ZERO as DerivedPoint];
123
+ a = mt2;
124
+ b = mt * t * 2;
125
+ c = t2;
126
+ } else if (order === 3) {
127
+ a = mt2 * mt;
128
+ b = mt2 * t * 3;
129
+ c = mt * t2 * 3;
130
+ d = t * t2;
131
+ }
132
+ return {
133
+ x: a * p[0].x + b * p[1].x + c * p[2].x + d * p[3].x,
134
+ y: a * p[0].y + b * p[1].y + c * p[2].y + d * p[3].y,
135
+ t,
136
+ };
137
+ };
138
+
139
+ const arcfn = (derivativeFn: DeriveCallback, t: number) => {
140
+ const d = derivativeFn(t);
141
+ const l = d.x * d.x + d.y * d.y;
142
+
143
+ return Math.sqrt(l);
144
+ };
145
+
146
+ const lengthFn = (derivativeFn: DeriveCallback) => {
147
+ const z = 0.5;
148
+ const len = Tvalues.length;
149
+
150
+ let sum = 0;
151
+
152
+ for (let i = 0, t; i < len; i++) {
153
+ t = z * Tvalues[i] + z;
154
+ sum += Cvalues[i] * arcfn(derivativeFn, t);
155
+ }
156
+ return z * sum;
157
+ };
158
+
159
+ /**
160
+ * Returns the length of CubicBezier / Quad segment.
161
+ * @param curve cubic / quad bezier segment
162
+ */
163
+ export const length = (curve: CubicCoordinates | QuadCoordinates) => {
164
+ const points = [] as unknown as CubicPoints | QuadPoints;
165
+ for (let idx = 0, len = curve.length, step = 2; idx < len; idx += step) {
166
+ points.push({
167
+ x: curve[idx],
168
+ y: curve[idx + 1],
169
+ });
170
+ }
171
+ const dpoints = derive(points);
172
+ return lengthFn((t: number) => {
173
+ return compute(dpoints[0], t);
174
+ });
175
+ };
176
+
177
+ // Precision for consider cubic polynom as quadratic one
178
+ const CBEZIER_MINMAX_EPSILON = 0.00000001;
179
+
180
+ /**
181
+ * Returns the most extreme points in a Quad Bezier segment.
182
+ * @param A
183
+ */
184
+ // https://github.com/kpym/SVGPathy/blob/acd1a50c626b36d81969f6e98e8602e128ba4302/lib/box.js#L89
185
+ export const minmaxQ = (A: [number, number, number]) => {
186
+ const min = Math.min(A[0], A[2]);
187
+ const max = Math.max(A[0], A[2]);
188
+
189
+ /* istanbul ignore next @preserve */
190
+ if (A[1] >= A[0] ? A[2] >= A[1] : A[2] <= A[1]) {
191
+ // if no extremum in ]0,1[
192
+ return [min, max] as PointTuple;
193
+ }
194
+
195
+ // check if the extremum E is min or max
196
+ const E = (A[0] * A[2] - A[1] * A[1]) / (A[0] - 2 * A[1] + A[2]);
197
+ return (E < min ? [E, max] : [min, E]) as PointTuple;
198
+ };
199
+
200
+ /**
201
+ * Returns the most extreme points in a Cubic Bezier segment.
202
+ * @param A
203
+ * @see https://github.com/kpym/SVGPathy/blob/acd1a50c626b36d81969f6e98e8602e128ba4302/lib/box.js#L127
204
+ */
205
+ export const minmaxC = (A: [number, number, number, number]) => {
206
+ const K = A[0] - 3 * A[1] + 3 * A[2] - A[3];
207
+
208
+ // if the polynomial is (almost) quadratic and not cubic
209
+ /* istanbul ignore next @preserve */
210
+ if (Math.abs(K) < CBEZIER_MINMAX_EPSILON) {
211
+ if (A[0] === A[3] && A[0] === A[1]) {
212
+ // no curve, point targeting same location
213
+ return [A[0], A[3]] as PointTuple;
214
+ }
215
+
216
+ return minmaxQ([A[0], -0.5 * A[0] + 1.5 * A[1], A[0] - 3 * A[1] + 3 * A[2]]);
217
+ }
218
+
219
+ // the reduced discriminant of the derivative
220
+ const T = -A[0] * A[2] + A[0] * A[3] - A[1] * A[2] - A[1] * A[3] + A[1] * A[1] + A[2] * A[2];
221
+
222
+ // if the polynomial is monotone in [0,1]
223
+ if (T <= 0) {
224
+ return [Math.min(A[0], A[3]), Math.max(A[0], A[3])] as PointTuple;
225
+ }
226
+ const S = Math.sqrt(T);
227
+
228
+ // potential extrema
229
+ let min = Math.min(A[0], A[3]);
230
+ let max = Math.max(A[0], A[3]);
231
+
232
+ const L = A[0] - 2 * A[1] + A[2];
233
+ // check local extrema
234
+ for (let R = (L + S) / K, i = 1; i <= 2; R = (L - S) / K, i++) {
235
+ // istanbul ignore next @preserve
236
+ if (R > 0 && R < 1) {
237
+ // if the extrema is for R in [0,1]
238
+ const Q =
239
+ A[0] * (1 - R) * (1 - R) * (1 - R) +
240
+ A[1] * 3 * (1 - R) * (1 - R) * R +
241
+ A[2] * 3 * (1 - R) * R * R +
242
+ A[3] * R * R * R;
243
+ if (Q < min) {
244
+ min = Q;
245
+ }
246
+ if (Q > max) {
247
+ max = Q;
248
+ }
249
+ }
250
+ }
251
+
252
+ return [min, max] as PointTuple;
253
+ };
@@ -0,0 +1,122 @@
1
+ import { length, minmaxC } from './bezier';
2
+ import { type CubicCoordinates } from '../types';
3
+
4
+ /**
5
+ * Returns a {x,y} point at a given length of a CubicBezier segment.
6
+ *
7
+ * @param x1 the starting point X
8
+ * @param y1 the starting point Y
9
+ * @param c1x the first control point X
10
+ * @param c1y the first control point Y
11
+ * @param c2x the second control point X
12
+ * @param c2y the second control point Y
13
+ * @param x2 the ending point X
14
+ * @param y2 the ending point Y
15
+ * @param t a [0-1] ratio
16
+ * @returns the point at cubic-bezier segment length
17
+ */
18
+ export const getPointAtCubicSegmentLength = ([x1, y1, c1x, c1y, c2x, c2y, x2, y2]: CubicCoordinates, t: number) => {
19
+ const t1 = 1 - t;
20
+ return {
21
+ x: t1 ** 3 * x1 + 3 * t1 ** 2 * t * c1x + 3 * t1 * t ** 2 * c2x + t ** 3 * x2,
22
+ y: t1 ** 3 * y1 + 3 * t1 ** 2 * t * c1y + 3 * t1 * t ** 2 * c2y + t ** 3 * y2,
23
+ };
24
+ };
25
+
26
+ /**
27
+ * Returns the length of a CubicBezier segment.
28
+ *
29
+ * @param x1 the starting point X
30
+ * @param y1 the starting point Y
31
+ * @param c1x the first control point X
32
+ * @param c1y the first control point Y
33
+ * @param c2x the second control point X
34
+ * @param c2y the second control point Y
35
+ * @param x2 the ending point X
36
+ * @param y2 the ending point Y
37
+ * @returns the CubicBezier segment length
38
+ */
39
+ export const getCubicLength = (
40
+ x1: number,
41
+ y1: number,
42
+ c1x: number,
43
+ c1y: number,
44
+ c2x: number,
45
+ c2y: number,
46
+ x2: number,
47
+ y2: number,
48
+ ) => {
49
+ return length([x1, y1, c1x, c1y, c2x, c2y, x2, y2]);
50
+ };
51
+
52
+ /**
53
+ * Returns the point along a CubicBezier segment at a given distance.
54
+ *
55
+ * @param x1 the starting point X
56
+ * @param y1 the starting point Y
57
+ * @param c1x the first control point X
58
+ * @param c1y the first control point Y
59
+ * @param c2x the second control point X
60
+ * @param c2y the second control point Y
61
+ * @param x2 the ending point X
62
+ * @param y2 the ending point Y
63
+ * @param distance the distance to look at
64
+ * @returns the point at CubicBezier length
65
+ */
66
+ export const getPointAtCubicLength = (
67
+ x1: number,
68
+ y1: number,
69
+ c1x: number,
70
+ c1y: number,
71
+ c2x: number,
72
+ c2y: number,
73
+ x2: number,
74
+ y2: number,
75
+ distance?: number,
76
+ ) => {
77
+ const distanceIsNumber = typeof distance === 'number';
78
+ let point = { x: x1, y: y1 };
79
+ /* istanbul ignore else @preserve */
80
+ if (distanceIsNumber) {
81
+ const currentLength = length([x1, y1, c1x, c1y, c2x, c2y, x2, y2]);
82
+ if (distance <= 0) {
83
+ // first point already defined
84
+ } else if (distance >= currentLength) {
85
+ point = { x: x2, y: y2 };
86
+ } else {
87
+ point = getPointAtCubicSegmentLength([x1, y1, c1x, c1y, c2x, c2y, x2, y2], distance / currentLength);
88
+ }
89
+ }
90
+ return point;
91
+ };
92
+
93
+ /**
94
+ * Returns the boundig box of a CubicBezier segment.
95
+ *
96
+ * @param x1 the starting point X
97
+ * @param y1 the starting point Y
98
+ * @param c1x the first control point X
99
+ * @param c1y the first control point Y
100
+ * @param c2x the second control point X
101
+ * @param c2y the second control point Y
102
+ * @param x2 the ending point X
103
+ * @param y2 the ending point Y
104
+ * @returns the point at CubicBezier length
105
+ */
106
+ export const getCubicBBox = (
107
+ x1: number,
108
+ y1: number,
109
+ c1x: number,
110
+ c1y: number,
111
+ c2x: number,
112
+ c2y: number,
113
+ x2: number,
114
+ y2: number,
115
+ ) => {
116
+ const cxMinMax = minmaxC([x1, c1x, c2x, x2]);
117
+ const cyMinMax = minmaxC([y1, c1y, c2y, y2]);
118
+ return {
119
+ min: { x: cxMinMax[0], y: cyMinMax[0] },
120
+ max: { x: cxMinMax[1], y: cyMinMax[1] },
121
+ };
122
+ };
@@ -1,3 +1,5 @@
1
+ import { type PointTuple } from '../types';
2
+
1
3
  /**
2
4
  * Returns the square root of the distance
3
5
  * between two given points.
@@ -6,7 +8,7 @@
6
8
  * @param b the second point coordinates
7
9
  * @returns the distance value
8
10
  */
9
- const distanceSquareRoot = (a: [number, number], b: [number, number]): number => {
11
+ const distanceSquareRoot = (a: PointTuple, b: PointTuple): number => {
10
12
  return Math.sqrt((a[0] - b[0]) * (a[0] - b[0]) + (a[1] - b[1]) * (a[1] - b[1]));
11
13
  };
12
14
 
@@ -0,0 +1,67 @@
1
+ import midPoint from './midPoint';
2
+ import distanceSquareRoot from './distanceSquareRoot';
3
+
4
+ /**
5
+ * Returns length for line segments (MoveTo, LineTo).
6
+ *
7
+ * @param x1 the starting point X
8
+ * @param y1 the starting point Y
9
+ * @param x2 the ending point X
10
+ * @param y2 the ending point Y
11
+ * @returns the line segment length
12
+ */
13
+ export const getLineLength = (x1: number, y1: number, x2: number, y2: number) => {
14
+ return distanceSquareRoot([x1, y1], [x2, y2]);
15
+ };
16
+
17
+ /**
18
+ * Returns a point along the line segments (MoveTo, LineTo).
19
+ *
20
+ * @param x1 the starting point X
21
+ * @param y1 the starting point Y
22
+ * @param x2 the ending point X
23
+ * @param y2 the ending point Y
24
+ * @param distance the distance to point in [0-1] range
25
+ * @returns the point at length
26
+ */
27
+ export const getPointAtLineLength = (x1: number, y1: number, x2: number, y2: number, distance?: number) => {
28
+ const length = distanceSquareRoot([x1, y1], [x2, y2]);
29
+ let point = { x: x1, y: y1 };
30
+
31
+ /* istanbul ignore else @preserve */
32
+ if (typeof distance === 'number') {
33
+ if (distance <= 0) {
34
+ point = { x: x1, y: y1 };
35
+ } else if (distance >= length) {
36
+ point = { x: x2, y: y2 };
37
+ } else {
38
+ const [x, y] = midPoint([x1, y1], [x2, y2], distance / length);
39
+ point = { x, y };
40
+ }
41
+ }
42
+ return point;
43
+ };
44
+
45
+ /**
46
+ * Returns bounding box for line segments (MoveTo, LineTo).
47
+ *
48
+ * @param x1 the starting point X
49
+ * @param y1 the starting point Y
50
+ * @param x2 the ending point X
51
+ * @param y2 the ending point Y
52
+ * @param distance the distance to point in [0-1] range
53
+ * @returns the extrema for line segments
54
+ */
55
+ export const getLineBBox = (x1: number, y1: number, x2: number, y2: number) => {
56
+ const { min, max } = Math;
57
+ return {
58
+ min: {
59
+ x: min(x1, x2),
60
+ y: min(y1, y2),
61
+ },
62
+ max: {
63
+ x: max(x1, x2),
64
+ y: max(y1, y2),
65
+ },
66
+ };
67
+ };
@@ -1,3 +1,5 @@
1
+ import { PointTuple } from '../types';
2
+
1
3
  /**
2
4
  * Returns the coordinates of a specified distance
3
5
  * ratio between two points.
@@ -7,7 +9,7 @@
7
9
  * @param t the ratio
8
10
  * @returns the midpoint coordinates
9
11
  */
10
- const midPoint = (a: [number, number], b: [number, number], t: number): [number, number] => {
12
+ const midPoint = (a: PointTuple, b: PointTuple, t: number): PointTuple => {
11
13
  const [ax, ay] = a;
12
14
  const [bx, by] = b;
13
15
  return [ax + (bx - ax) * t, ay + (by - ay) * t];
@@ -1,3 +1,5 @@
1
+ import { type PointTuple } from '../types';
2
+
1
3
  /**
2
4
  * d3-polygon-area
3
5
  * https://github.com/d3/d3-polygon
@@ -7,7 +9,7 @@
7
9
  * @param polygon an array of coordinates
8
10
  * @returns the polygon area
9
11
  */
10
- const polygonArea = (polygon: [number, number][]): number => {
12
+ const polygonArea = (polygon: PointTuple[]): number => {
11
13
  const n = polygon.length;
12
14
  let i = -1;
13
15
  let a;
@@ -1,3 +1,4 @@
1
+ import { type PointTuple } from '../types';
1
2
  import distanceSquareRoot from './distanceSquareRoot';
2
3
 
3
4
  /**
@@ -9,7 +10,7 @@ import distanceSquareRoot from './distanceSquareRoot';
9
10
  * @param polygon an array of coordinates
10
11
  * @returns {number} the polygon length
11
12
  */
12
- const polygonLength = (polygon: [number, number][]): number => {
13
+ const polygonLength = (polygon: PointTuple[]): number => {
13
14
  return polygon.reduce((length, point, i) => {
14
15
  if (i) {
15
16
  return length + distanceSquareRoot(polygon[i - 1], point);
@@ -0,0 +1,98 @@
1
+ import { length, minmaxQ } from './bezier';
2
+ import { type QuadCoordinates } from '../types';
3
+
4
+ /**
5
+ * Returns the {x,y} coordinates of a point at a
6
+ * given length of a quadratic-bezier segment.
7
+ *
8
+ * @see https://github.com/substack/point-at-length
9
+ *
10
+ * @param x1 the starting point X
11
+ * @param y1 the starting point Y
12
+ * @param cx the control point X
13
+ * @param cy the control point Y
14
+ * @param x2 the ending point X
15
+ * @param y2 the ending point Y
16
+ * @param t a [0-1] ratio
17
+ * @returns the requested {x,y} coordinates
18
+ */
19
+ const getPointAtQuadSegmentLength = ([x1, y1, cx, cy, x2, y2]: QuadCoordinates, t: number) => {
20
+ const t1 = 1 - t;
21
+ return {
22
+ x: t1 ** 2 * x1 + 2 * t1 * t * cx + t ** 2 * x2,
23
+ y: t1 ** 2 * y1 + 2 * t1 * t * cy + t ** 2 * y2,
24
+ };
25
+ };
26
+
27
+ /**
28
+ * Returns the length of a QuadraticBezier segment.
29
+ *
30
+ * @param x1 the starting point X
31
+ * @param y1 the starting point Y
32
+ * @param cx the control point X
33
+ * @param cy the control point Y
34
+ * @param x2 the ending point X
35
+ * @param y2 the ending point Y
36
+ * @returns the QuadraticBezier segment length
37
+ */
38
+ export const getQuadLength = (x1: number, y1: number, cx: number, cy: number, x2: number, y2: number) => {
39
+ return length([x1, y1, cx, cy, x2, y2]);
40
+ };
41
+
42
+ /**
43
+ * Returns the point along a QuadraticBezier segment at a given distance.
44
+ *
45
+ * @param x1 the starting point X
46
+ * @param y1 the starting point Y
47
+ * @param cx the control point X
48
+ * @param cy the control point Y
49
+ * @param x2 the ending point X
50
+ * @param y2 the ending point Y
51
+ * @param distance the distance to look at
52
+ * @returns the point at QuadraticBezier length
53
+ */
54
+ export const getPointAtQuadLength = (
55
+ x1: number,
56
+ y1: number,
57
+ cx: number,
58
+ cy: number,
59
+ x2: number,
60
+ y2: number,
61
+ distance?: number,
62
+ ) => {
63
+ const distanceIsNumber = typeof distance === 'number';
64
+ let point = { x: x1, y: y1 };
65
+
66
+ /* istanbul ignore else @preserve */
67
+ if (distanceIsNumber) {
68
+ const currentLength = length([x1, y1, cx, cy, x2, y2]);
69
+ if (distance <= 0) {
70
+ // first point already defined
71
+ } else if (distance >= currentLength) {
72
+ point = { x: x2, y: y2 };
73
+ } else {
74
+ point = getPointAtQuadSegmentLength([x1, y1, cx, cy, x2, y2], distance / currentLength);
75
+ }
76
+ }
77
+ return point;
78
+ };
79
+
80
+ /**
81
+ * Returns the boundig box of a QuadraticBezier segment.
82
+ *
83
+ * @param x1 the starting point X
84
+ * @param y1 the starting point Y
85
+ * @param cx the control point X
86
+ * @param cy the control point Y
87
+ * @param x2 the ending point X
88
+ * @param y2 the ending point Y
89
+ * @returns the point at CubicBezier length
90
+ */
91
+ export const getQuadBBox = (x1: number, y1: number, cx: number, cy: number, x2: number, y2: number) => {
92
+ const cxMinMax = minmaxQ([x1, cx, x2]);
93
+ const cyMinMax = minmaxQ([y1, cy, y2]);
94
+ return {
95
+ min: { x: cxMinMax[0], y: cyMinMax[0] },
96
+ max: { x: cxMinMax[1], y: cyMinMax[1] },
97
+ };
98
+ };
@@ -0,0 +1,17 @@
1
+ /**
2
+ * Checks if the character is a MoveTo command.
3
+ *
4
+ * @param code the character to check
5
+ * @returns check result
6
+ */
7
+ const isMoveCommand = (code: number): code is 0x6d | 0x4d => {
8
+ // eslint-disable-next-line no-bitwise -- Impossible to satisfy
9
+ switch (code | 0x20) {
10
+ case 0x6d /* m */:
11
+ case 0x4d /* M */:
12
+ return true;
13
+ default:
14
+ return false;
15
+ }
16
+ };
17
+ export default isMoveCommand;
@@ -1,7 +1,6 @@
1
1
  import scanSegment from './scanSegment';
2
2
  import skipSpaces from './skipSpaces';
3
3
  import PathParser from './pathParser';
4
- import isPathArray from '../util/isPathArray';
5
4
  import type { PathArray } from '../types';
6
5
 
7
6
  /**
@@ -11,9 +10,10 @@ import type { PathArray } from '../types';
11
10
  * @param pathInput the string to be parsed
12
11
  * @returns the resulted `pathArray` or error string
13
12
  */
14
- const parsePathString = (pathInput: string | PathArray): PathArray => {
15
- if (isPathArray(pathInput)) {
16
- return [...pathInput];
13
+ const parsePathString = (pathInput: string | PathArray) => {
14
+ if (typeof pathInput !== 'string') {
15
+ return pathInput.slice(0) as PathArray;
16
+ // return pathInput;
17
17
  }
18
18
 
19
19
  const path = new PathParser(pathInput);
@@ -24,7 +24,7 @@ const parsePathString = (pathInput: string | PathArray): PathArray => {
24
24
  scanSegment(path);
25
25
  }
26
26
 
27
- if (path.err && path.err.length) {
27
+ if (path?.err.length) {
28
28
  throw TypeError(path.err);
29
29
  }
30
30
 
@@ -6,11 +6,12 @@ import skipSpaces from './skipSpaces';
6
6
  import isPathCommand from './isPathCommand';
7
7
  import isDigitStart from './isDigitStart';
8
8
  import isArcCommand from './isArcCommand';
9
+ import isMoveCommand from './isMoveCommand';
9
10
  import invalidPathValue from './invalidPathValue';
10
11
  import error from './error';
11
12
 
12
13
  import type PathParser from './pathParser';
13
- import { RelativeCommand } from '../types';
14
+ import type { PathSegment, RelativeCommand } from '../types';
14
15
 
15
16
  /**
16
17
  * Scans every character in the path string to determine
@@ -19,14 +20,22 @@ import { RelativeCommand } from '../types';
19
20
  * @param path the `PathParser` instance
20
21
  */
21
22
  const scanSegment = (path: PathParser) => {
22
- const { max, pathValue, index } = path;
23
+ const { max, pathValue, index, segments } = path;
23
24
  const cmdCode = pathValue.charCodeAt(index);
24
25
  const reqParams = paramCounts[pathValue[index].toLowerCase() as RelativeCommand];
25
26
 
26
27
  path.segmentStart = index;
27
28
 
29
+ // segments always start with a path command
28
30
  if (!isPathCommand(cmdCode)) {
29
- path.err = `${error}: ${invalidPathValue} "${pathValue[index]}" is not a path command`;
31
+ path.err = `${error}: ${invalidPathValue} "${pathValue[index]}" is not a path command at index ${index}`;
32
+ return;
33
+ }
34
+
35
+ // after a Z segment, we only expect a MoveTo path command
36
+ const lastSegment = segments[segments.length - 1] as PathSegment | undefined;
37
+ if (!isMoveCommand(cmdCode) && lastSegment?.[0]?.toLocaleLowerCase() === 'z') {
38
+ path.err = `${error}: ${invalidPathValue} "${pathValue[index]}" is not a MoveTo path command at index ${index}`;
30
39
  return;
31
40
  }
32
41