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
package/src/interface.ts CHANGED
@@ -1,51 +1,51 @@
1
1
  import type { PathSegment } from './types';
2
2
 
3
- export interface SegmentProperties {
3
+ export type SegmentProperties = {
4
4
  segment: PathSegment;
5
5
  index: number;
6
6
  length: number;
7
7
  lengthAtSegment: number;
8
8
  [key: string]: any;
9
- }
9
+ };
10
10
 
11
- export interface PointProperties {
11
+ export type PointProperties = {
12
12
  closest: {
13
13
  x: number;
14
14
  y: number;
15
15
  };
16
16
  distance: number;
17
17
  segment?: SegmentProperties;
18
- }
18
+ };
19
19
 
20
- export interface LineAttr {
20
+ export type LineAttr = {
21
21
  type: 'line';
22
22
  x1: number;
23
23
  y1: number;
24
24
  x2: number;
25
25
  y2: number;
26
26
  [key: string]: string | number;
27
- }
28
- export interface PolyAttr {
27
+ };
28
+ export type PolyAttr = {
29
29
  type: 'polygon' | 'polyline';
30
30
  points: string;
31
31
  [key: string]: string | number;
32
- }
33
- export interface CircleAttr {
32
+ };
33
+ export type CircleAttr = {
34
34
  type: 'circle';
35
35
  cx: number;
36
36
  cy: number;
37
37
  r: number;
38
38
  [key: string]: string | number;
39
- }
40
- export interface EllipseAttr {
39
+ };
40
+ export type EllipseAttr = {
41
41
  type: 'ellipse';
42
42
  cx: number;
43
43
  cy: number;
44
44
  rx: number;
45
45
  ry?: number;
46
46
  [key: string]: string | number | undefined;
47
- }
48
- export interface RectAttr {
47
+ };
48
+ export type RectAttr = {
49
49
  type: 'rect';
50
50
  width: number;
51
51
  height: number;
@@ -54,14 +54,14 @@ export interface RectAttr {
54
54
  rx?: number;
55
55
  ry?: number;
56
56
  [key: string]: string | number | undefined;
57
- }
58
- export interface GlyphAttr {
57
+ };
58
+ export type GlyphAttr = {
59
59
  type: 'glyph';
60
60
  d: string;
61
61
  [key: string]: string | number;
62
- }
62
+ };
63
63
 
64
- export interface ShapeParams {
64
+ export type ShapeParams = {
65
65
  line: ['x1', 'y1', 'x2', 'y2'];
66
66
  circle: ['cx', 'cy', 'r'];
67
67
  ellipse: ['cx', 'cy', 'rx', 'ry'];
@@ -69,9 +69,9 @@ export interface ShapeParams {
69
69
  polygon: ['points'];
70
70
  polyline: ['points'];
71
71
  glyph: ['d'];
72
- }
72
+ };
73
73
 
74
- export interface PathBBox {
74
+ export type PathBBox = {
75
75
  width: number;
76
76
  height: number;
77
77
  x: number;
@@ -81,13 +81,13 @@ export interface PathBBox {
81
81
  cx: number;
82
82
  cy: number;
83
83
  cz: number;
84
- }
85
- export interface SegmentLimits {
84
+ };
85
+ export type SegmentLimits = {
86
86
  min: { x: number; y: number };
87
87
  max: { x: number; y: number };
88
- }
88
+ };
89
89
 
90
- export interface ParserParams {
90
+ export type ParserParams = {
91
91
  x1: number;
92
92
  y1: number;
93
93
  x2: number;
@@ -96,34 +96,34 @@ export interface ParserParams {
96
96
  y: number;
97
97
  qx: number | null;
98
98
  qy: number | null;
99
- }
99
+ };
100
100
 
101
- export interface LengthFactory {
101
+ export type LengthFactory = {
102
102
  length: number;
103
103
  point: { x: number; y: number };
104
104
  min: { x: number; y: number };
105
105
  max: { x: number; y: number };
106
- }
106
+ };
107
107
 
108
- export interface Options {
109
- round: 'auto' | 'off' | number;
108
+ export type Options = {
109
+ round: 'off' | number;
110
110
  origin: number[];
111
- }
111
+ };
112
112
 
113
- export interface PathTransform {
113
+ export type PathTransform = {
114
114
  s: PathSegment;
115
115
  c: string;
116
116
  x: number;
117
117
  y: number;
118
- }
118
+ };
119
119
 
120
- export interface TransformObject {
120
+ export type TransformObject = {
121
121
  translate: number | number[];
122
122
  rotate: number | number[];
123
123
  scale: number | number[];
124
124
  skew: number | number[];
125
125
  origin: number[];
126
- }
126
+ };
127
127
 
128
128
  export type TransformProps = keyof TransformObject;
129
129
  export type TransformEntries = [TransformProps, TransformObject[TransformProps]][];
@@ -0,0 +1,394 @@
1
+ import { getPointAtLineLength } from './lineTools';
2
+ import type { Point } from '../types';
3
+
4
+ /**
5
+ * Returns the Arc segment length.
6
+ * @param rx radius along X axis
7
+ * @param ry radius along Y axis
8
+ * @param theta the angle in radians
9
+ * @returns the arc length
10
+ */
11
+ const ellipticalArcLength = (rx: number, ry: number, theta: number) => {
12
+ const halfTheta = theta / 2;
13
+ const sinHalfTheta = Math.sin(halfTheta);
14
+ const cosHalfTheta = Math.cos(halfTheta);
15
+ const term1 = rx ** 2 * sinHalfTheta ** 2;
16
+ const term2 = ry ** 2 * cosHalfTheta ** 2;
17
+ const arcLength = Math.sqrt(term1 + term2) * theta;
18
+ return Math.abs(arcLength);
19
+ };
20
+
21
+ /**
22
+ * Compute point on ellipse from angle around ellipse (theta);
23
+ * @param theta the arc sweep angle
24
+ * @param cx the center X
25
+ * @param cy the center Y
26
+ * @param rx the radius X
27
+ * @param ry the radius Y
28
+ * @param alpha the angle
29
+ * @returns a point around ellipse
30
+ */
31
+ const arc = (theta: number, cx: number, cy: number, rx: number, ry: number, alpha: number) => {
32
+ // theta is angle in radians around arc
33
+ // alpha is angle of rotation of ellipse in radians
34
+ const cos = Math.cos(alpha);
35
+ const sin = Math.sin(alpha);
36
+ const x = rx * Math.cos(theta);
37
+ const y = ry * Math.sin(theta);
38
+
39
+ return {
40
+ x: cx + cos * x - sin * y,
41
+ y: cy + sin * x + cos * y,
42
+ };
43
+ };
44
+
45
+ /**
46
+ * Returns the angle between two points.
47
+ * @param v0 starting point
48
+ * @param v1 ending point
49
+ * @returns the angle
50
+ */
51
+ const angleBetween = (v0: Point, v1: Point) => {
52
+ const { x: v0x, y: v0y } = v0;
53
+ const { x: v1x, y: v1y } = v1;
54
+ const p = v0x * v1x + v0y * v1y;
55
+ const n = Math.sqrt((v0x ** 2 + v0y ** 2) * (v1x ** 2 + v1y ** 2));
56
+ const sign = v0x * v1y - v0y * v1x < 0 ? -1 : 1;
57
+ const angle = sign * Math.acos(p / n);
58
+
59
+ return angle;
60
+ };
61
+
62
+ /**
63
+ * Returns the following properties for an Arc segment: center, start angle
64
+ * and radiuses on X and Y coordinates.
65
+ *
66
+ * @param x1 the starting point X
67
+ * @param y1 the starting point Y
68
+ * @param RX the radius on X axis
69
+ * @param RY the radius on Y axis
70
+ * @param angle the ellipse rotation in degrees
71
+ * @param LAF the large arc flag
72
+ * @param SF the sweep flag
73
+ * @param x2 the ending point X
74
+ * @param y2 the ending point Y
75
+ * @returns properties specific to Arc segments
76
+ */
77
+ const getArcProps = (
78
+ x1: number,
79
+ y1: number,
80
+ RX: number,
81
+ RY: number,
82
+ angle: number,
83
+ LAF: number,
84
+ SF: number,
85
+ x: number,
86
+ y: number,
87
+ ) => {
88
+ const { abs, sin, cos, sqrt, PI } = Math;
89
+ let rx = abs(RX);
90
+ let ry = abs(RY);
91
+ const xRot = ((angle % 360) + 360) % 360;
92
+ const xRotRad = xRot * (PI / 180);
93
+
94
+ if (x1 === x && y1 === y) {
95
+ return {
96
+ rx,
97
+ ry,
98
+ startAngle: 0,
99
+ endAngle: 0,
100
+ center: { x, y },
101
+ };
102
+ }
103
+
104
+ if (rx === 0 || ry === 0) {
105
+ return {
106
+ rx,
107
+ ry,
108
+ startAngle: 0,
109
+ endAngle: 0,
110
+ center: { x, y },
111
+ };
112
+ }
113
+
114
+ const dx = (x1 - x) / 2;
115
+ const dy = (y1 - y) / 2;
116
+
117
+ const transformedPoint = {
118
+ x: cos(xRotRad) * dx + sin(xRotRad) * dy,
119
+ y: -sin(xRotRad) * dx + cos(xRotRad) * dy,
120
+ };
121
+
122
+ const radiiCheck = transformedPoint.x ** 2 / rx ** 2 + transformedPoint.y ** 2 / ry ** 2;
123
+
124
+ if (radiiCheck > 1) {
125
+ rx *= sqrt(radiiCheck);
126
+ ry *= sqrt(radiiCheck);
127
+ }
128
+
129
+ const cSquareNumerator = rx ** 2 * ry ** 2 - rx ** 2 * transformedPoint.y ** 2 - ry ** 2 * transformedPoint.x ** 2;
130
+ const cSquareRootDenom = rx ** 2 * transformedPoint.y ** 2 + ry ** 2 * transformedPoint.x ** 2;
131
+
132
+ let cRadicand = cSquareNumerator / cSquareRootDenom;
133
+ /* istanbul ignore next @preserve */
134
+ cRadicand = cRadicand < 0 ? 0 : cRadicand;
135
+ const cCoef = (LAF !== SF ? 1 : -1) * sqrt(cRadicand);
136
+ const transformedCenter = {
137
+ x: cCoef * ((rx * transformedPoint.y) / ry),
138
+ y: cCoef * (-(ry * transformedPoint.x) / rx),
139
+ };
140
+
141
+ const center = {
142
+ x: cos(xRotRad) * transformedCenter.x - sin(xRotRad) * transformedCenter.y + (x1 + x) / 2,
143
+ y: sin(xRotRad) * transformedCenter.x + cos(xRotRad) * transformedCenter.y + (y1 + y) / 2,
144
+ };
145
+
146
+ const startVector = {
147
+ x: (transformedPoint.x - transformedCenter.x) / rx,
148
+ y: (transformedPoint.y - transformedCenter.y) / ry,
149
+ };
150
+
151
+ const startAngle = angleBetween({ x: 1, y: 0 }, startVector);
152
+
153
+ const endVector = {
154
+ x: (-transformedPoint.x - transformedCenter.x) / rx,
155
+ y: (-transformedPoint.y - transformedCenter.y) / ry,
156
+ };
157
+
158
+ let sweepAngle = angleBetween(startVector, endVector);
159
+ if (!SF && sweepAngle > 0) {
160
+ sweepAngle -= 2 * PI;
161
+ } else if (SF && sweepAngle < 0) {
162
+ sweepAngle += 2 * PI;
163
+ }
164
+ sweepAngle %= 2 * PI;
165
+
166
+ const endAngle = startAngle + sweepAngle;
167
+
168
+ // to be used later
169
+ // point.ellipticalArcStartAngle = startAngle;
170
+ // point.ellipticalArcEndAngle = startAngle + sweepAngle;
171
+ // point.ellipticalArcAngle = alpha;
172
+
173
+ // point.ellipticalArcCenter = center;
174
+ // point.resultantRx = rx;
175
+ // point.resultantRy = ry;
176
+ // point.length = ellipticalArcLength(rx, ry, sweepAngle);
177
+ // point.box = minmax(center.x, center.y, rx, ry, xRotRad, startAngle, startAngle + sweepAngle);
178
+
179
+ return {
180
+ center,
181
+ startAngle,
182
+ endAngle,
183
+ rx,
184
+ ry,
185
+ };
186
+ };
187
+
188
+ /**
189
+ * Returns the length of an Arc segment.
190
+ *
191
+ * @param x1 the starting point X
192
+ * @param y1 the starting point Y
193
+ * @param c1x the first control point X
194
+ * @param c1y the first control point Y
195
+ * @param c2x the second control point X
196
+ * @param c2y the second control point Y
197
+ * @param x2 the ending point X
198
+ * @param y2 the ending point Y
199
+ * @returns the length of the Arc segment
200
+ */
201
+ export const getArcLength = (
202
+ x1: number,
203
+ y1: number,
204
+ RX: number,
205
+ RY: number,
206
+ angle: number,
207
+ LAF: number,
208
+ SF: number,
209
+ x: number,
210
+ y: number,
211
+ ) => {
212
+ const { rx, ry, startAngle, endAngle } = getArcProps(x1, y1, RX, RY, angle, LAF, SF, x, y);
213
+ return ellipticalArcLength(rx, ry, endAngle - startAngle);
214
+ };
215
+
216
+ /**
217
+ * Returns a point along an Arc segment at a given distance.
218
+ *
219
+ * @param x1 the starting point X
220
+ * @param y1 the starting point Y
221
+ * @param RX the radius on X axis
222
+ * @param RY the radius on Y axis
223
+ * @param angle the ellipse rotation in degrees
224
+ * @param LAF the large arc flag
225
+ * @param SF the sweep flag
226
+ * @param x2 the ending point X
227
+ * @param y2 the ending point Y
228
+ * @param distance a [0-1] ratio
229
+ * @returns a point along the Arc segment
230
+ */
231
+ export const getPointAtArcLength = (
232
+ x1: number,
233
+ y1: number,
234
+ RX: number,
235
+ RY: number,
236
+ angle: number,
237
+ LAF: number,
238
+ SF: number,
239
+ x: number,
240
+ y: number,
241
+ distance?: number,
242
+ ) => {
243
+ let point = { x: x1, y: y1 };
244
+ const { center, rx, ry, startAngle, endAngle } = getArcProps(x1, y1, RX, RY, angle, LAF, SF, x, y);
245
+ const length = ellipticalArcLength(rx, ry, endAngle - startAngle);
246
+
247
+ /* istanbul ignore else @preserve */
248
+ if (typeof distance === 'number') {
249
+ if (distance <= 0) {
250
+ point = { x: x1, y: y1 };
251
+ } else if (distance >= length) {
252
+ point = { x, y };
253
+ } else {
254
+ /* istanbul ignore next @preserve */
255
+ if (x1 === x && y1 === y) {
256
+ return { x, y };
257
+ }
258
+ /* istanbul ignore next @preserve */
259
+ if (rx === 0 || ry === 0) {
260
+ return getPointAtLineLength(x1, y1, x, y, distance);
261
+ }
262
+ const { PI, cos, sin } = Math;
263
+ const sweepAngle = endAngle - startAngle;
264
+ const xRot = ((angle % 360) + 360) % 360;
265
+ const xRotRad = xRot * (PI / 180);
266
+ const alpha = startAngle + sweepAngle * (distance / length);
267
+ const ellipseComponentX = rx * cos(alpha);
268
+ const ellipseComponentY = ry * sin(alpha);
269
+
270
+ point = {
271
+ x: cos(xRotRad) * ellipseComponentX - sin(xRotRad) * ellipseComponentY + center.x,
272
+ y: sin(xRotRad) * ellipseComponentX + cos(xRotRad) * ellipseComponentY + center.y,
273
+ };
274
+ }
275
+ }
276
+
277
+ return point;
278
+ };
279
+
280
+ /**
281
+ * Returns the bounding box for an Arc segment.
282
+ * @see https://github.com/herrstrietzel/svg-pathdata-getbbox
283
+ *
284
+ * @param x1 the starting point X
285
+ * @param y1 the starting point Y
286
+ * @param RX the radius on X axis
287
+ * @param RY the radius on Y axis
288
+ * @param angle the ellipse rotation in degrees
289
+ * @param LAF the large arc flag
290
+ * @param SF the sweep flag
291
+ * @param x2 the ending point X
292
+ * @param y2 the ending point Y
293
+ * @returns the extrema of the Arc segment
294
+ *
295
+ */
296
+ export const getArcBBox = (
297
+ x1: number,
298
+ y1: number,
299
+ RX: number,
300
+ RY: number,
301
+ angle: number,
302
+ LAF: number,
303
+ SF: number,
304
+ x: number,
305
+ y: number,
306
+ ) => {
307
+ const { center, rx, ry, startAngle, endAngle } = getArcProps(x1, y1, RX, RY, angle, LAF, SF, x, y);
308
+ const deltaAngle = endAngle - startAngle;
309
+
310
+ // final on path point
311
+ const p = { x, y };
312
+
313
+ // circle/elipse center coordinates
314
+ const [cx, cy] = [center.x, center.y];
315
+
316
+ // collect extreme points – add end point
317
+ const extremes = [p];
318
+
319
+ // rotation to radians
320
+ const alpha = (angle * Math.PI) / 180;
321
+ const tan = Math.tan(alpha);
322
+
323
+ /**
324
+ * find min/max from zeroes of directional derivative along x and y
325
+ * along x axis
326
+ */
327
+ const theta = Math.atan2(-ry * tan, rx);
328
+ const angle1 = theta;
329
+ const angle2 = theta + Math.PI;
330
+ const angle3 = Math.atan2(ry, rx * tan);
331
+ const angle4 = angle3 + Math.PI;
332
+
333
+ // inner bounding box
334
+ const xArr = [x1, x];
335
+ const yArr = [y1, y];
336
+ const xMin = Math.min(...xArr);
337
+ const xMax = Math.max(...xArr);
338
+ const yMin = Math.min(...yArr);
339
+ const yMax = Math.max(...yArr);
340
+
341
+ // on path point close after start
342
+ const angleAfterStart = endAngle - deltaAngle * 0.001;
343
+ const pP2 = arc(angleAfterStart, cx, cy, rx, ry, alpha);
344
+
345
+ // on path point close before end
346
+ const angleBeforeEnd = endAngle - deltaAngle * 0.999;
347
+ const pP3 = arc(angleBeforeEnd, cx, cy, rx, ry, alpha);
348
+
349
+ /**
350
+ * expected extremes
351
+ * if leaving inner bounding box
352
+ * (between segment start and end point)
353
+ * otherwise exclude elliptic extreme points
354
+ */
355
+
356
+ // right
357
+ // istanbul ignore if @preserve
358
+ if (pP2.x > xMax || pP3.x > xMax) {
359
+ // get point for this theta
360
+ extremes.push(arc(angle1, cx, cy, rx, ry, alpha));
361
+ }
362
+
363
+ // left
364
+ // istanbul ignore if @preserve
365
+ if (pP2.x < xMin || pP3.x < xMin) {
366
+ // get anti-symmetric point
367
+ extremes.push(arc(angle2, cx, cy, rx, ry, alpha));
368
+ }
369
+
370
+ // top
371
+ // istanbul ignore if @preserve
372
+ if (pP2.y < yMin || pP3.y < yMin) {
373
+ // get anti-symmetric point
374
+ extremes.push(arc(angle4, cx, cy, rx, ry, alpha));
375
+ }
376
+
377
+ // bottom
378
+ // istanbul ignore if @preserve
379
+ if (pP2.y > yMax || pP3.y > yMax) {
380
+ // get point for this theta
381
+ extremes.push(arc(angle3, cx, cy, rx, ry, alpha));
382
+ }
383
+
384
+ return {
385
+ min: {
386
+ x: Math.min(...extremes.map(n => n.x)),
387
+ y: Math.min(...extremes.map(n => n.y)),
388
+ },
389
+ max: {
390
+ x: Math.max(...extremes.map(n => n.x)),
391
+ y: Math.max(...extremes.map(n => n.y)),
392
+ },
393
+ };
394
+ };