svg-path-commander 2.1.0 → 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 (51) hide show
  1. package/README.md +61 -5
  2. package/dist/svg-path-commander.cjs +1 -1
  3. package/dist/svg-path-commander.cjs.map +1 -1
  4. package/dist/svg-path-commander.d.ts +119 -33
  5. package/dist/svg-path-commander.js +1 -1
  6. package/dist/svg-path-commander.js.map +1 -1
  7. package/dist/svg-path-commander.mjs +798 -828
  8. package/dist/svg-path-commander.mjs.map +1 -1
  9. package/package.json +3 -3
  10. package/src/convert/pathToAbsolute.ts +16 -70
  11. package/src/convert/pathToCurve.ts +36 -28
  12. package/src/convert/pathToRelative.ts +33 -62
  13. package/src/index.ts +13 -19
  14. package/src/interface.ts +1 -1
  15. package/src/math/arcTools.ts +248 -71
  16. package/src/math/bezier.ts +19 -27
  17. package/src/math/cubicTools.ts +67 -26
  18. package/src/math/distanceSquareRoot.ts +3 -1
  19. package/src/math/lineTools.ts +41 -26
  20. package/src/math/midPoint.ts +3 -1
  21. package/src/math/polygonArea.ts +3 -1
  22. package/src/math/polygonLength.ts +2 -1
  23. package/src/math/quadTools.ts +45 -26
  24. package/src/parser/parsePathString.ts +4 -4
  25. package/src/process/absolutizeSegment.ts +58 -0
  26. package/src/process/iterate.ts +33 -0
  27. package/src/process/normalizePath.ts +34 -28
  28. package/src/process/normalizeSegment.ts +8 -9
  29. package/src/process/projection2d.ts +2 -1
  30. package/src/process/relativizeSegment.ts +61 -0
  31. package/src/process/reversePath.ts +1 -1
  32. package/src/process/roundPath.ts +8 -10
  33. package/src/process/segmentToCubic.ts +1 -1
  34. package/src/process/shortenSegment.ts +3 -3
  35. package/src/process/splitCubic.ts +8 -7
  36. package/src/process/splitPath.ts +38 -4
  37. package/src/process/transformPath.ts +80 -73
  38. package/src/types.ts +35 -1
  39. package/src/util/getPathArea.ts +3 -3
  40. package/src/util/getPathBBox.ts +86 -19
  41. package/src/util/getPointAtLength.ts +98 -4
  42. package/src/util/getPropertiesAtLength.ts +2 -2
  43. package/src/util/getTotalLength.ts +71 -4
  44. package/test/class.test.ts +15 -14
  45. package/test/fixtures/shapes.js +27 -27
  46. package/test/fixtures/simpleShapes.js +18 -18
  47. package/test/static.test.ts +37 -17
  48. package/cypress.config.ts +0 -29
  49. package/src/process/fixArc.ts +0 -23
  50. package/src/process/replaceArc.ts +0 -52
  51. package/src/util/pathFactory.ts +0 -130
@@ -1,18 +1,19 @@
1
1
  import midPoint from '../math/midPoint';
2
- import type { CubicSegment } from '../types';
2
+ import type { CubicSegment, PointTuple } from '../types';
3
3
 
4
4
  /**
5
5
  * Split a cubic-bezier segment into two.
6
6
  *
7
7
  * @param pts the cubic-bezier parameters
8
+ * @param ratio the cubic-bezier parameters
8
9
  * @return two new cubic-bezier segments
9
10
  */
10
- const splitCubic = (pts: number[] /* , ratio */): [CubicSegment, CubicSegment] => {
11
- const t = /* ratio || */ 0.5;
12
- const p0 = pts.slice(0, 2) as [number, number];
13
- const p1 = pts.slice(2, 4) as [number, number];
14
- const p2 = pts.slice(4, 6) as [number, number];
15
- const p3 = pts.slice(6, 8) as [number, number];
11
+ const splitCubic = (pts: number[], ratio = 0.5): [CubicSegment, CubicSegment] => {
12
+ const t = ratio;
13
+ const p0 = pts.slice(0, 2) as PointTuple;
14
+ const p1 = pts.slice(2, 4) as PointTuple;
15
+ const p2 = pts.slice(4, 6) as PointTuple;
16
+ const p3 = pts.slice(6, 8) as PointTuple;
16
17
  const p4 = midPoint(p0, p1, t);
17
18
  const p5 = midPoint(p1, p2, t);
18
19
  const p6 = midPoint(p2, p3, t);
@@ -1,4 +1,5 @@
1
- import type { PathArray } from '../types';
1
+ import paramsParser from '../parser/paramsParser';
2
+ import type { AbsoluteCommand, HSegment, MSegment, PathArray, PointTuple, RelativeCommand, VSegment } from '../types';
2
3
 
3
4
  /**
4
5
  * Split a path into an `Array` of sub-path strings.
@@ -7,20 +8,53 @@ import type { PathArray } from '../types';
7
8
  * for visual consistency.
8
9
  *
9
10
  * @param pathInput the source `pathArray`
10
- * @return {SVGPath.pathArray[]} an array with all sub-path strings
11
+ * @return an array with all sub-path strings
11
12
  */
12
13
  const splitPath = (pathInput: PathArray): PathArray[] => {
13
14
  const composite = [] as PathArray[];
14
15
  let path: PathArray;
15
16
  let pi = -1;
17
+ let x = 0;
18
+ let y = 0;
19
+ let mx = 0;
20
+ let my = 0;
21
+ const params = { ...paramsParser };
16
22
 
17
23
  pathInput.forEach(seg => {
18
- if (seg[0] === 'M') {
19
- path = [seg];
24
+ const [pathCommand] = seg;
25
+ const absCommand = pathCommand.toUpperCase() as AbsoluteCommand;
26
+ const relCommand = pathCommand.toLowerCase() as RelativeCommand;
27
+ const isRelative = pathCommand === relCommand;
28
+ const values = seg.slice(1) as number[];
29
+
30
+ if (absCommand === 'M') {
20
31
  pi += 1;
32
+ [x, y] = values as PointTuple;
33
+ x += isRelative ? params.x : 0;
34
+ y += isRelative ? params.y : 0;
35
+ mx = x;
36
+ my = y;
37
+ path = [(isRelative ? [absCommand, mx, my] : seg) as MSegment];
21
38
  } else {
39
+ if (absCommand === 'Z') {
40
+ x = mx;
41
+ y = my;
42
+ } else if (absCommand === 'H') {
43
+ [, x] = seg as HSegment;
44
+ x += isRelative ? params.x : /* istanbul ignore next @preserve */ 0;
45
+ } else if (absCommand === 'V') {
46
+ [, y] = seg as VSegment;
47
+ y += isRelative ? params.y : /* istanbul ignore next @preserve */ 0;
48
+ } else {
49
+ [x, y] = seg.slice(-2) as PointTuple;
50
+ x += isRelative ? params.x : 0;
51
+ y += isRelative ? params.y : 0;
52
+ }
22
53
  path.push(seg);
23
54
  }
55
+
56
+ params.x = x;
57
+ params.y = y;
24
58
  composite[pi] = path;
25
59
  });
26
60
 
@@ -1,14 +1,14 @@
1
- import normalizePath from './normalizePath';
2
- // import pathToAbsolute from '../convert/pathToAbsolute';
3
- // import segmentToCubic from './segmentToCubic';
4
- // import fixArc from './fixArc';
5
1
  import getSVGMatrix from './getSVGMatrix';
6
2
  import projection2d from './projection2d';
7
- import paramsParser from '../parser/paramsParser';
8
- import replaceArc from './replaceArc';
9
3
  import defaultOptions from '../options/options';
10
- import type { AbsoluteArray, PathArray, TransformObjectValues } from '../types';
11
- import type { PathTransform, TransformObject } from '../interface';
4
+ import type { AbsoluteArray, CSegment, PathArray, PointTuple, TransformObjectValues } from '../types';
5
+ import type { TransformObject } from '../interface';
6
+ import iterate from './iterate';
7
+ import parsePathString from '../parser/parsePathString';
8
+ import absolutizeSegment from './absolutizeSegment';
9
+ import segmentToCubic from './segmentToCubic';
10
+ import normalizeSegment from './normalizeSegment';
11
+ import paramsParser from '../parser/paramsParser';
12
12
 
13
13
  /**
14
14
  * Apply a 2D / 3D transformation to a `pathArray` instance.
@@ -20,89 +20,96 @@ import type { PathTransform, TransformObject } from '../interface';
20
20
  * @param transform the transform functions `Object`
21
21
  * @returns the resulted `pathArray`
22
22
  */
23
- const transformPath = (path: string | PathArray, transform?: Partial<TransformObject>): PathArray => {
23
+ const transformPath = (pathInput: PathArray | string, transform?: Partial<TransformObject>) => {
24
24
  let x = 0;
25
25
  let y = 0;
26
- let i;
27
- let j;
28
- let ii;
29
- let jj;
30
- let lx;
31
- let ly;
32
- // REPLACE Arc path commands with Cubic Beziers
33
- // we don't have any scripting know-how on 3d ellipse transformation
34
- // Arc segments don't work with 3D transformations or skews
35
- const absolutePath = replaceArc(path);
26
+ let mx = 0;
27
+ let my = 0;
28
+ let lx = 0;
29
+ let ly = 0;
30
+ let j = 0;
31
+ let jj = 0;
32
+ let nx = 0;
33
+ let ny = 0;
34
+ let pathCommand = 'M';
35
+ // transform uses it's own set of params
36
+ const transformParams = { ...paramsParser };
37
+ const path = parsePathString(pathInput);
36
38
  const transformProps = transform && Object.keys(transform);
37
39
 
38
40
  // when used as a static method, invalidate somehow
39
- if (!transform || (transformProps && !transformProps.length)) return absolutePath.slice(0) as PathArray;
41
+ if (!transform || (transformProps && !transformProps.length)) return path;
40
42
 
41
- const normalizedPath = normalizePath(absolutePath);
42
43
  // transform origin is extremely important
43
44
  if (!transform.origin) {
44
- const { origin: defaultOrigin } = defaultOptions;
45
- Object.assign(transform, { origin: defaultOrigin });
45
+ Object.assign(transform, { origin: defaultOptions.origin });
46
46
  }
47
+ const origin = transform.origin as [number, number, number];
47
48
  const matrixInstance = getSVGMatrix(transform as TransformObjectValues);
48
- const { origin } = transform;
49
- const params = { ...paramsParser };
50
- let segment = [];
51
- let seglen = 0;
52
- let pathCommand = '';
53
- const transformedPath = [] as PathTransform[];
54
49
 
55
- if (!matrixInstance.isIdentity) {
56
- for (i = 0, ii = absolutePath.length; i < ii; i += 1) {
57
- segment = normalizedPath[i];
58
- seglen = segment.length;
50
+ if (matrixInstance.isIdentity) return path;
59
51
 
60
- params.x1 = +segment[seglen - 2];
61
- params.y1 = +segment[seglen - 1];
62
- params.x2 = +segment[seglen - 4] || params.x1;
63
- params.y2 = +segment[seglen - 3] || params.y1;
52
+ return iterate<AbsoluteArray>(path, (seg, _, i) => {
53
+ const absSegment = absolutizeSegment(seg, transformParams);
54
+ [pathCommand] = absSegment;
64
55
 
65
- const result = {
66
- s: absolutePath[i],
67
- c: absolutePath[i][0],
68
- x: params.x1,
69
- y: params.y1,
70
- };
56
+ let result =
57
+ pathCommand === 'A'
58
+ ? segmentToCubic(absSegment, transformParams)
59
+ : ['V', 'H'].includes(pathCommand)
60
+ ? normalizeSegment(absSegment, transformParams)
61
+ : absSegment;
62
+ const isLongArc = result[0] === 'C' && result.length > 7;
63
+ const normalizedSegment = (isLongArc ? result.slice(0, 7) : result.slice(0)) as typeof result;
71
64
 
72
- transformedPath.push(result);
65
+ if (isLongArc) {
66
+ path.splice(i + 1, 0, ['C', ...result.slice(7)] as CSegment);
67
+ result = result.slice(0, 7) as CSegment;
73
68
  }
74
69
 
75
- return transformedPath.map(seg => {
76
- pathCommand = seg.c;
77
- segment = seg.s;
78
- if (pathCommand === 'L' || pathCommand === 'H' || pathCommand === 'V') {
79
- [lx, ly] = projection2d(matrixInstance, [seg.x, seg.y], origin as [number, number, number]);
80
-
81
- /* istanbul ignore else @preserve */
82
- if (x !== lx && y !== ly) {
83
- segment = ['L', lx, ly];
84
- } else if (y === ly) {
85
- segment = ['H', lx];
86
- } else if (x === lx) {
87
- segment = ['V', ly];
88
- }
70
+ if (result[0] === 'L') {
71
+ const values = result.slice(-2) as PointTuple;
72
+ [lx, ly] = projection2d(matrixInstance, values, origin);
89
73
 
90
- // now update x and y
91
- x = lx;
92
- y = ly;
93
-
94
- return segment;
95
- } else {
96
- for (j = 1, jj = segment.length; j < jj; j += 2) {
97
- [x, y] = projection2d(matrixInstance, [+segment[j], +segment[j + 1]], origin as [number, number, number]);
98
- segment[j] = x;
99
- segment[j + 1] = y;
100
- }
74
+ /* istanbul ignore else @preserve */
75
+ if (x !== lx && y !== ly) {
76
+ result = ['L', lx, ly];
77
+ } else if (y === ly) {
78
+ result = ['H', lx];
79
+ } else if (x === lx) {
80
+ result = ['V', ly];
81
+ }
82
+ } else {
83
+ for (j = 1, jj = result.length; j < jj; j += 2) {
84
+ [lx, ly] = projection2d(matrixInstance, [+result[j], +result[j + 1]], origin);
85
+ result[j] = lx;
86
+ result[j + 1] = ly;
87
+ }
88
+ }
89
+ // now update x and y
90
+ x = lx;
91
+ y = ly;
101
92
 
102
- return segment;
93
+ if (pathCommand === 'Z') {
94
+ nx = mx;
95
+ ny = my;
96
+ } else {
97
+ [nx, ny] = normalizedSegment.slice(-2) as PointTuple;
98
+ if (pathCommand === 'M') {
99
+ mx = nx;
100
+ my = ny;
103
101
  }
104
- }) as PathArray;
105
- }
106
- return absolutePath.slice(0) as AbsoluteArray;
102
+ }
103
+
104
+ const seglen = normalizedSegment.length;
105
+ transformParams.x1 = +normalizedSegment[seglen - 2];
106
+ transformParams.y1 = +normalizedSegment[seglen - 1];
107
+ transformParams.x2 = +normalizedSegment[seglen - 4] || transformParams.x1;
108
+ transformParams.y2 = +normalizedSegment[seglen - 3] || transformParams.y1;
109
+ transformParams.x = nx;
110
+ transformParams.y = ny;
111
+ return result;
112
+ });
107
113
  };
114
+
108
115
  export default transformPath;
package/src/types.ts CHANGED
@@ -1,4 +1,13 @@
1
- import type { LineAttr, CircleAttr, PolyAttr, RectAttr, EllipseAttr, GlyphAttr, TransformObject } from './interface';
1
+ import type {
2
+ LineAttr,
3
+ CircleAttr,
4
+ PolyAttr,
5
+ RectAttr,
6
+ EllipseAttr,
7
+ GlyphAttr,
8
+ TransformObject,
9
+ ParserParams,
10
+ } from './interface';
2
11
 
3
12
  export type SpaceNumber =
4
13
  | 0x1680
@@ -196,3 +205,28 @@ export type Point = {
196
205
  x: number;
197
206
  y: number;
198
207
  };
208
+
209
+ export type PointTuple = [number, number];
210
+
211
+ export type DerivedPoint = Point & { t: number };
212
+ export type QuadPoints = [Point, Point, Point, Point, Point, Point];
213
+ export type CubicPoints = [Point, Point, Point, Point, Point, Point, Point, Point];
214
+ export type DerivedQuadPoints = [DerivedPoint, DerivedPoint, DerivedPoint, DerivedPoint, DerivedPoint, DerivedPoint];
215
+ export type DerivedCubicPoints = [
216
+ DerivedPoint,
217
+ DerivedPoint,
218
+ DerivedPoint,
219
+ DerivedPoint,
220
+ DerivedPoint,
221
+ DerivedPoint,
222
+ DerivedPoint,
223
+ DerivedPoint,
224
+ ];
225
+ export type QuadCoordinates = [number, number, number, number, number, number];
226
+ export type CubicCoordinates = [number, number, number, number, number, number, number, number];
227
+ export type ArcCoordinates = [number, number, number, number, number, number, number, number, number];
228
+ export type LineCoordinates = [number, number, number, number];
229
+
230
+ export type DeriveCallback = (t: number) => Point;
231
+
232
+ export type IteratorCallback = (segment: PathSegment, params: ParserParams, index: number) => PathSegment;
@@ -1,5 +1,5 @@
1
1
  import pathToCurve from '../convert/pathToCurve';
2
- import type { PathArray } from '../types';
2
+ import type { PointTuple, PathArray, QuadCoordinates } from '../types';
3
3
 
4
4
  /**
5
5
  * Returns the area of a single cubic-bezier segment.
@@ -60,8 +60,8 @@ const getPathArea = (path: PathArray) => {
60
60
  [, x, y] = seg;
61
61
  return 0;
62
62
  default:
63
- len = getCubicSegArea(x, y, ...(seg.slice(1) as [number, number, number, number, number, number]));
64
- [x, y] = seg.slice(-2) as [number, number];
63
+ len = getCubicSegArea(x, y, ...(seg.slice(1) as QuadCoordinates));
64
+ [x, y] = seg.slice(-2) as PointTuple;
65
65
  return len;
66
66
  }
67
67
  })
@@ -1,15 +1,26 @@
1
- import { PathBBox } from 'src/interface';
2
- import { PathArray } from 'src/types';
3
- import pathFactory from './pathFactory';
4
-
5
- /**
6
- * Returns the bounding box of a shape.
7
- *
8
- * @param path the shape `pathArray`
9
- * @returns the length of the cubic-bezier segment
10
- */
11
- const getPathBBox = (path: PathArray | string): PathBBox => {
12
- if (!path) {
1
+ import iterate from '../process/iterate';
2
+ import { PathBBox } from '../interface';
3
+ import {
4
+ ArcCoordinates,
5
+ CubicCoordinates,
6
+ LineCoordinates,
7
+ MSegment,
8
+ PathArray,
9
+ Point,
10
+ PointTuple,
11
+ QuadCoordinates,
12
+ } from '../types';
13
+ // import pathFactory from './pathFactory';
14
+ import absolutizeSegment from '../process/absolutizeSegment';
15
+ import normalizeSegment from '../process/normalizeSegment';
16
+ import parsePathString from '../parser/parsePathString';
17
+ import { getLineBBox } from '../math/lineTools';
18
+ import { getArcBBox } from '../math/arcTools';
19
+ import { getCubicBBox } from '../math/cubicTools';
20
+ import { getQuadBBox } from '../math/quadTools';
21
+
22
+ const getPathBBox = (pathInput: PathArray | string) => {
23
+ if (!pathInput) {
13
24
  return {
14
25
  x: 0,
15
26
  y: 0,
@@ -23,12 +34,67 @@ const getPathBBox = (path: PathArray | string): PathBBox => {
23
34
  };
24
35
  }
25
36
 
26
- const props = pathFactory(path);
27
- const {
28
- min: { x: xMin, y: yMin },
29
- max: { x: xMax, y: yMax },
30
- } = props.bbox;
37
+ const path = parsePathString(pathInput);
38
+ let data = [] as number[];
39
+ let pathCommand = 'M';
40
+ let x = 0;
41
+ let y = 0;
42
+ let mx = 0;
43
+ let my = 0;
44
+ const MIN = [] as Point[];
45
+ const MAX = [] as Point[];
46
+ let min = { x, y };
47
+ let max = { x, y };
48
+
49
+ iterate(path, (seg, params) => {
50
+ const absoluteSegment = absolutizeSegment(seg, params);
51
+ const normalSegment = normalizeSegment(absoluteSegment, params);
52
+ [pathCommand] = normalSegment;
53
+ data = [x, y, ...normalSegment.slice(1)] as number[];
54
+
55
+ // this segment is always ZERO
56
+ /* istanbul ignore else @preserve */
57
+ if (pathCommand === 'M') {
58
+ // remember mx, my for Z
59
+ [, mx, my] = normalSegment as MSegment;
60
+ min = { x: mx, y: my };
61
+ max = { x: mx, y: my };
62
+ } else if (pathCommand === 'L') {
63
+ ({ min, max } = getLineBBox(...(data as LineCoordinates)));
64
+ } else if (pathCommand === 'A') {
65
+ ({ min, max } = getArcBBox(...(data as ArcCoordinates)));
66
+ } else if (pathCommand === 'C') {
67
+ ({ min, max } = getCubicBBox(...(data as CubicCoordinates)));
68
+ } else if (pathCommand === 'Q') {
69
+ ({ min, max } = getQuadBBox(...(data as QuadCoordinates)));
70
+ } else if (pathCommand === 'Z') {
71
+ data = [x, y, mx, my];
72
+ ({ min, max } = getLineBBox(...(data as LineCoordinates)));
73
+ }
74
+
75
+ MIN.push(min);
76
+ MAX.push(max);
31
77
 
78
+ if (pathCommand === 'Z') {
79
+ x = mx;
80
+ y = my;
81
+ } else {
82
+ [x, y] = normalSegment.slice(-2) as PointTuple;
83
+
84
+ if (pathCommand === 'M') {
85
+ mx = x;
86
+ my = y;
87
+ }
88
+ }
89
+ params.x = x;
90
+ params.y = y;
91
+ return normalSegment;
92
+ });
93
+
94
+ const xMin = Math.min(...MIN.map(n => n.x));
95
+ const xMax = Math.max(...MAX.map(n => n.x));
96
+ const yMin = Math.min(...MIN.map(n => n.y));
97
+ const yMax = Math.max(...MAX.map(n => n.y));
32
98
  const width = xMax - xMin;
33
99
  const height = yMax - yMin;
34
100
 
@@ -41,8 +107,9 @@ const getPathBBox = (path: PathArray | string): PathBBox => {
41
107
  y2: yMax,
42
108
  cx: xMin + width / 2,
43
109
  cy: yMin + height / 2,
44
- // an estimted guess
110
+ // an estimated guess
45
111
  cz: Math.max(width, height) + Math.min(width, height) / 2,
46
- };
112
+ } satisfies PathBBox;
47
113
  };
114
+
48
115
  export default getPathBBox;
@@ -1,5 +1,21 @@
1
- import type { PathArray } from '../types';
2
- import pathFactory from './pathFactory';
1
+ import DISTANCE_EPSILON from './distanceEpsilon';
2
+ import parsePathString from '../parser/parsePathString';
3
+ import type {
4
+ ArcCoordinates,
5
+ CubicCoordinates,
6
+ LineCoordinates,
7
+ MSegment,
8
+ PathArray,
9
+ PointTuple,
10
+ QuadCoordinates,
11
+ } from '../types';
12
+ import iterate from '../process/iterate';
13
+ import absolutizeSegment from '../process/absolutizeSegment';
14
+ import normalizeSegment from '../process/normalizeSegment';
15
+ import { getLineLength, getPointAtLineLength } from '../math/lineTools';
16
+ import { getArcLength, getPointAtArcLength } from '../math/arcTools';
17
+ import { getCubicLength, getPointAtCubicLength } from '../math/cubicTools';
18
+ import { getQuadLength, getPointAtQuadLength } from '../math/quadTools';
3
19
 
4
20
  /**
5
21
  * Returns [x,y] coordinates of a point at a given length of a shape.
@@ -8,7 +24,85 @@ import pathFactory from './pathFactory';
8
24
  * @param distance the length of the shape to look at
9
25
  * @returns the requested {x, y} point coordinates
10
26
  */
11
- const getPointAtLength = (pathInput: string | PathArray, distance: number) => {
12
- return pathFactory(pathInput, distance).point;
27
+ const getPointAtLength = (pathInput: string | PathArray, distance?: number) => {
28
+ const path = parsePathString(pathInput);
29
+ let isM = false;
30
+ let data = [] as number[];
31
+ let pathCommand = 'M';
32
+ let x = 0;
33
+ let y = 0;
34
+ let [mx, my] = path[0].slice(1) as PointTuple;
35
+ const distanceIsNumber = typeof distance === 'number';
36
+ let point = { x: mx, y: my };
37
+ let length = 0;
38
+ let POINT = point;
39
+ let totalLength = 0;
40
+
41
+ if (!distanceIsNumber) return point;
42
+
43
+ if (distance < DISTANCE_EPSILON) {
44
+ POINT = point;
45
+ }
46
+
47
+ iterate(path, (seg, params) => {
48
+ const absoluteSegment = absolutizeSegment(seg, params);
49
+ const normalSegment = normalizeSegment(absoluteSegment, params);
50
+ [pathCommand] = normalSegment;
51
+ isM = pathCommand === 'M';
52
+ data = !isM ? [x, y, ...(normalSegment.slice(1) as number[])] : data;
53
+
54
+ // this segment is always ZERO
55
+ /* istanbul ignore else @preserve */
56
+ if (isM) {
57
+ // remember mx, my for Z
58
+ [, mx, my] = seg as MSegment;
59
+ point = { x: mx, y: my };
60
+ length = 0;
61
+ } else if (pathCommand === 'L') {
62
+ point = getPointAtLineLength(...(data as LineCoordinates), distance - totalLength);
63
+ length = getLineLength(...(data as LineCoordinates));
64
+ } else if (pathCommand === 'A') {
65
+ point = getPointAtArcLength(...(data as ArcCoordinates), distance - totalLength);
66
+ length = getArcLength(...(data as ArcCoordinates));
67
+ } else if (pathCommand === 'C') {
68
+ point = getPointAtCubicLength(...(data as CubicCoordinates), distance - totalLength);
69
+ length = getCubicLength(...(data as CubicCoordinates));
70
+ } else if (pathCommand === 'Q') {
71
+ point = getPointAtQuadLength(...(data as QuadCoordinates), distance - totalLength);
72
+ length = getQuadLength(...(data as QuadCoordinates));
73
+ } else if (pathCommand === 'Z') {
74
+ data = [x, y, mx, my];
75
+ point = { x: mx, y: my };
76
+ length = getLineLength(...(data as LineCoordinates));
77
+ }
78
+
79
+ if (totalLength < distance && totalLength + length >= distance) {
80
+ POINT = point;
81
+ }
82
+
83
+ totalLength += length;
84
+ if (pathCommand === 'Z') {
85
+ x = mx;
86
+ y = my;
87
+ } else {
88
+ [x, y] = normalSegment.slice(-2) as PointTuple;
89
+
90
+ if (pathCommand === 'M') {
91
+ mx = x;
92
+ my = y;
93
+ }
94
+ }
95
+ params.x = x;
96
+ params.y = y;
97
+ return normalSegment;
98
+ });
99
+
100
+ // native `getPointAtLength` behavior when the given distance
101
+ // is higher than total length
102
+ if (distance > totalLength - DISTANCE_EPSILON) {
103
+ POINT = { x, y };
104
+ }
105
+
106
+ return POINT;
13
107
  };
14
108
  export default getPointAtLength;
@@ -1,4 +1,4 @@
1
- import type { PathArray, PathSegment } from '../types';
1
+ import type { PointTuple, PathArray, PathSegment } from '../types';
2
2
  import type { SegmentProperties } from '../interface';
3
3
  import parsePathString from '../parser/parsePathString';
4
4
  import getTotalLength from './getTotalLength';
@@ -20,7 +20,7 @@ const getPropertiesAtLength = (pathInput: string | PathArray, distance?: number)
20
20
  let lengthAtSegment = 0;
21
21
  let length = 0;
22
22
  let segment = pathArray[0] as PathSegment;
23
- const [x, y] = segment.slice(-2) as [number, number];
23
+ const [x, y] = segment.slice(-2) as PointTuple;
24
24
  const point = { x, y };
25
25
 
26
26
  // If the path is empty, return 0.
@@ -1,5 +1,21 @@
1
- import type { PathArray } from '../types';
2
- import pathFactory from './pathFactory';
1
+ import parsePathString from '../parser/parsePathString';
2
+ import type {
3
+ ArcCoordinates,
4
+ CubicCoordinates,
5
+ LineCoordinates,
6
+ MSegment,
7
+ PathArray,
8
+ PointTuple,
9
+ QuadCoordinates,
10
+ } from '../types';
11
+ import { getLineLength } from '../math/lineTools';
12
+ import { getArcLength } from '../math/arcTools';
13
+ import { getCubicLength } from '../math/cubicTools';
14
+ import { getQuadLength } from '../math/quadTools';
15
+ import iterate from '../process/iterate';
16
+ import absolutizeSegment from '../process/absolutizeSegment';
17
+ import normalizeSegment from '../process/normalizeSegment';
18
+ // import pathFactory from './pathFactory';
3
19
 
4
20
  /**
5
21
  * Returns the shape total length, or the equivalent to `shape.getTotalLength()`.
@@ -10,7 +26,58 @@ import pathFactory from './pathFactory';
10
26
  * @param pathInput the target `pathArray`
11
27
  * @returns the shape total length
12
28
  */
13
- const getTotalLength = (pathInput: string | PathArray): number => {
14
- return pathFactory(pathInput).length;
29
+ const getTotalLength = (pathInput: string | PathArray) => {
30
+ const path = parsePathString(pathInput);
31
+ let isM = false;
32
+ let data = [] as number[];
33
+ let pathCommand = 'M';
34
+ let x = 0;
35
+ let y = 0;
36
+ let mx = 0;
37
+ let my = 0;
38
+ let totalLength = 0;
39
+
40
+ iterate(path, (seg, params) => {
41
+ const absoluteSegment = absolutizeSegment(seg, params);
42
+ const normalSegment = normalizeSegment(absoluteSegment, params);
43
+ [pathCommand] = normalSegment;
44
+ isM = pathCommand === 'M';
45
+ data = !isM ? [x, y, ...(normalSegment.slice(1) as number[])] : data;
46
+
47
+ // this segment is always ZERO
48
+ /* istanbul ignore else @preserve */
49
+ if (isM) {
50
+ // remember mx, my for Z
51
+ [, mx, my] = seg as MSegment;
52
+ } else if (pathCommand === 'L') {
53
+ totalLength += getLineLength(...(data as LineCoordinates));
54
+ } else if (pathCommand === 'A') {
55
+ totalLength += getArcLength(...(data as ArcCoordinates));
56
+ } else if (pathCommand === 'C') {
57
+ totalLength += getCubicLength(...(data as CubicCoordinates));
58
+ } else if (pathCommand === 'Q') {
59
+ totalLength += getQuadLength(...(data as QuadCoordinates));
60
+ } else if (pathCommand === 'Z') {
61
+ data = [x, y, mx, my];
62
+ totalLength += getLineLength(...(data as LineCoordinates));
63
+ }
64
+ if (pathCommand === 'Z') {
65
+ x = mx;
66
+ y = my;
67
+ } else {
68
+ [x, y] = normalSegment.slice(-2) as PointTuple;
69
+
70
+ if (isM) {
71
+ mx = x;
72
+ my = y;
73
+ }
74
+ }
75
+ params.x = x;
76
+ params.y = y;
77
+ return normalSegment;
78
+ });
79
+
80
+ return totalLength;
15
81
  };
82
+
16
83
  export default getTotalLength;