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
@@ -1,13 +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
3
  import defaultOptions from '../options/options';
9
- import type { PathArray, PathCommand, TransformObjectValues } from '../types';
10
- 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';
11
12
 
12
13
  /**
13
14
  * Apply a 2D / 3D transformation to a `pathArray` instance.
@@ -19,110 +20,96 @@ import type { PathTransform, TransformObject } from '../interface';
19
20
  * @param transform the transform functions `Object`
20
21
  * @returns the resulted `pathArray`
21
22
  */
22
- const transformPath = (path: string | PathArray, transform?: Partial<TransformObject>): PathArray => {
23
+ const transformPath = (pathInput: PathArray | string, transform?: Partial<TransformObject>) => {
23
24
  let x = 0;
24
25
  let y = 0;
25
- let i;
26
- let j;
27
- let ii;
28
- let jj;
29
- let lx;
30
- let ly;
31
- const absolutePath = pathToAbsolute(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);
32
38
  const transformProps = transform && Object.keys(transform);
33
39
 
34
40
  // when used as a static method, invalidate somehow
35
- if (!transform || (transformProps && !transformProps.length)) return [...absolutePath];
41
+ if (!transform || (transformProps && !transformProps.length)) return path;
36
42
 
37
- const normalizedPath = normalizePath(absolutePath);
38
43
  // transform origin is extremely important
39
44
  if (!transform.origin) {
40
- const { origin: defaultOrigin } = defaultOptions;
41
- Object.assign(transform, { origin: defaultOrigin });
45
+ Object.assign(transform, { origin: defaultOptions.origin });
42
46
  }
47
+ const origin = transform.origin as [number, number, number];
43
48
  const matrixInstance = getSVGMatrix(transform as TransformObjectValues);
44
- const { origin } = transform;
45
- const params = { ...paramsParser };
46
- let segment = [];
47
- let seglen = 0;
48
- let pathCommand = '';
49
- let transformedPath = [] as PathTransform[];
50
- const allPathCommands = [] as PathCommand[]; // needed for arc to curve transformation
51
-
52
- if (!matrixInstance.isIdentity) {
53
- for (i = 0, ii = absolutePath.length; i < ii; i += 1) {
54
- segment = absolutePath[i];
55
49
 
56
- /* istanbul ignore else */
57
- if (absolutePath[i]) [pathCommand] = segment;
50
+ if (matrixInstance.isIdentity) return path;
58
51
 
59
- // REPLACE Arc path commands with Cubic Beziers
60
- // we don't have any scripting know-how on 3d ellipse transformation
61
- // Arc segments don't work 3D transformations or skews
62
- /// ////////////////////////////////////////
63
- allPathCommands[i] = pathCommand as PathCommand;
52
+ return iterate<AbsoluteArray>(path, (seg, _, i) => {
53
+ const absSegment = absolutizeSegment(seg, transformParams);
54
+ [pathCommand] = absSegment;
64
55
 
65
- if (pathCommand === 'A') {
66
- segment = segmentToCubic(normalizedPath[i], params);
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;
67
64
 
68
- absolutePath[i] = segmentToCubic(normalizedPath[i], params);
69
- fixArc(absolutePath, allPathCommands, i);
65
+ if (isLongArc) {
66
+ path.splice(i + 1, 0, ['C', ...result.slice(7)] as CSegment);
67
+ result = result.slice(0, 7) as CSegment;
68
+ }
70
69
 
71
- normalizedPath[i] = segmentToCubic(normalizedPath[i], params);
72
- fixArc(normalizedPath, allPathCommands, i);
73
- ii = Math.max(absolutePath.length, normalizedPath.length);
70
+ if (result[0] === 'L') {
71
+ const values = result.slice(-2) as PointTuple;
72
+ [lx, ly] = projection2d(matrixInstance, values, origin);
73
+
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;
74
87
  }
75
-
76
- /// ////////////////////////////////////////
77
- segment = normalizedPath[i];
78
- seglen = segment.length;
79
-
80
- params.x1 = +segment[seglen - 2];
81
- params.y1 = +segment[seglen - 1];
82
- params.x2 = +segment[seglen - 4] || params.x1;
83
- params.y2 = +segment[seglen - 3] || params.y1;
84
-
85
- const result = {
86
- s: absolutePath[i],
87
- c: absolutePath[i][0],
88
- x: params.x1,
89
- y: params.y1,
90
- };
91
-
92
- transformedPath = [...transformedPath, ...[result]];
93
88
  }
94
-
95
- return transformedPath.map(seg => {
96
- pathCommand = seg.c;
97
- segment = seg.s;
98
- if (pathCommand === 'L' || pathCommand === 'H' || pathCommand === 'V') {
99
- [lx, ly] = projection2d(matrixInstance, [seg.x, seg.y], origin as [number, number, number]);
100
-
101
- /* istanbul ignore else */
102
- if (x !== lx && y !== ly) {
103
- segment = ['L', lx, ly];
104
- } else if (y === ly) {
105
- segment = ['H', lx];
106
- } else if (x === lx) {
107
- segment = ['V', ly];
108
- }
109
-
110
- // now update x and y
111
- x = lx;
112
- y = ly;
113
-
114
- return segment;
115
- } else {
116
- for (j = 1, jj = segment.length; j < jj; j += 2) {
117
- [x, y] = projection2d(matrixInstance, [+segment[j], +segment[j + 1]], origin as [number, number, number]);
118
- segment[j] = x;
119
- segment[j + 1] = y;
120
- }
121
-
122
- return segment;
89
+ // now update x and y
90
+ x = lx;
91
+ y = ly;
92
+
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;
123
101
  }
124
- }) as PathArray;
125
- }
126
- return [...absolutePath];
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
+ });
127
113
  };
114
+
128
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
@@ -191,3 +200,33 @@ export type ShapeTags = 'line' | 'polyline' | 'polygon' | 'ellipse' | 'circle' |
191
200
  export type ShapeOps = LineAttr | PolyAttr | PolyAttr | EllipseAttr | CircleAttr | RectAttr | GlyphAttr;
192
201
 
193
202
  export type TransformObjectValues = Partial<TransformObject> & { origin: [number, number, number] };
203
+
204
+ export type Point = {
205
+ x: number;
206
+ y: number;
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;
@@ -0,0 +1,3 @@
1
+ const DISTANCE_EPSILON = 0.00001;
2
+
3
+ export default DISTANCE_EPSILON;
@@ -8,7 +8,7 @@ import getPropertiesAtPoint from './getPropertiesAtPoint';
8
8
  * @param point the given point
9
9
  * @returns the best match
10
10
  */
11
- const getClosestPoint = (pathInput: string | PathArray, point: { x: number; y: number }): { x: number; y: number } => {
11
+ const getClosestPoint = (pathInput: string | PathArray, point: { x: number; y: number }) => {
12
12
  return getPropertiesAtPoint(pathInput, point).closest;
13
13
  };
14
14
 
@@ -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 pathLengthFactory from './pathLengthFactory';
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,11 +34,67 @@ const getPathBBox = (path?: PathArray | string): PathBBox => {
23
34
  };
24
35
  }
25
36
 
26
- const {
27
- min: { x: xMin, y: yMin },
28
- max: { x: xMax, y: yMax },
29
- } = pathLengthFactory(path);
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);
30
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));
31
98
  const width = xMax - xMin;
32
99
  const height = yMax - yMin;
33
100
 
@@ -40,8 +107,9 @@ const getPathBBox = (path?: PathArray | string): PathBBox => {
40
107
  y2: yMax,
41
108
  cx: xMin + width / 2,
42
109
  cy: yMin + height / 2,
43
- // an estimted guess
110
+ // an estimated guess
44
111
  cz: Math.max(width, height) + Math.min(width, height) / 2,
45
- };
112
+ } satisfies PathBBox;
46
113
  };
114
+
47
115
  export default getPathBBox;
@@ -1,5 +1,21 @@
1
- import type { PathArray } from '../types';
2
- import pathLengthFactory from './pathLengthFactory';
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 pathLengthFactory from './pathLengthFactory';
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): { x: number; y: number } => {
12
- return pathLengthFactory(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';
@@ -14,13 +14,13 @@ import getTotalLength from './getTotalLength';
14
14
  const getPropertiesAtLength = (pathInput: string | PathArray, distance?: number): SegmentProperties => {
15
15
  const pathArray = parsePathString(pathInput);
16
16
 
17
- let pathTemp = [...pathArray] as PathArray;
17
+ let pathTemp = pathArray.slice(0) as PathArray;
18
18
  let pathLength = getTotalLength(pathTemp);
19
19
  let index = pathTemp.length - 1;
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.
@@ -64,4 +64,5 @@ const getPropertiesAtLength = (pathInput: string | PathArray, distance?: number)
64
64
 
65
65
  return segments.find(({ lengthAtSegment: l }) => l <= distance) as SegmentProperties;
66
66
  };
67
+
67
68
  export default getPropertiesAtLength;
@@ -36,6 +36,7 @@ const getPropertiesAtPoint = (pathInput: string | PathArray, point: { x: number;
36
36
  for (let scanLength = 0; scanLength <= pathLength; scanLength += precision) {
37
37
  scan = getPointAtLength(normalPath, scanLength);
38
38
  scanDistance = distanceTo(scan);
39
+
39
40
  if (scanDistance < bestDistance) {
40
41
  closest = scan;
41
42
  bestLength = scanLength;
@@ -52,13 +53,14 @@ const getPropertiesAtPoint = (pathInput: string | PathArray, point: { x: number;
52
53
  let beforeDistance = 0;
53
54
  let afterDistance = 0;
54
55
 
55
- while (precision > 0.5) {
56
+ while (precision > 0.000001) {
56
57
  beforeLength = bestLength - precision;
57
58
  before = getPointAtLength(normalPath, beforeLength);
58
59
  beforeDistance = distanceTo(before);
59
60
  afterLength = bestLength + precision;
60
61
  after = getPointAtLength(normalPath, afterLength);
61
62
  afterDistance = distanceTo(after);
63
+
62
64
  if (beforeLength >= 0 && beforeDistance < bestDistance) {
63
65
  closest = before;
64
66
  bestLength = beforeLength;
@@ -70,6 +72,7 @@ const getPropertiesAtPoint = (pathInput: string | PathArray, point: { x: number;
70
72
  } else {
71
73
  precision /= 2;
72
74
  }
75
+ if (precision < 0.00001) break;
73
76
  }
74
77
 
75
78
  const segment = getPropertiesAtLength(path, bestLength);
@@ -1,5 +1,21 @@
1
- import type { PathArray } from '../types';
2
- import pathLengthFactory from './pathLengthFactory';
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 pathLengthFactory from './pathLengthFactory';
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 pathLengthFactory(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;