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,58 @@
1
+ import type { ParserParams } from '../interface';
2
+ import type {
3
+ AbsoluteSegment,
4
+ AbsoluteCommand,
5
+ ASegment,
6
+ VSegment,
7
+ HSegment,
8
+ QSegment,
9
+ SSegment,
10
+ TSegment,
11
+ CSegment,
12
+ PathSegment,
13
+ MSegment,
14
+ } from '../types';
15
+
16
+ /**
17
+ * Returns an absolute segment of a `PathArray` object.
18
+ *
19
+ * @param segment the segment object
20
+ * @param params the coordinates of the previous segment
21
+ * @returns the absolute segment
22
+ */
23
+ const absolutizeSegment = (segment: PathSegment, params: ParserParams) => {
24
+ const [pathCommand] = segment;
25
+ const { x, y } = params;
26
+ const values = segment.slice(1).map(Number);
27
+ const absCommand = pathCommand.toUpperCase() as AbsoluteCommand;
28
+ const isAbsolute = absCommand === pathCommand;
29
+
30
+ /* istanbul ignore else @preserve */
31
+ if (!isAbsolute) {
32
+ if (absCommand === 'A') {
33
+ return [
34
+ absCommand,
35
+ values[0],
36
+ values[1],
37
+ values[2],
38
+ values[3],
39
+ values[4],
40
+ values[5] + x,
41
+ values[6] + y,
42
+ ] as ASegment;
43
+ } else if (absCommand === 'V') {
44
+ return [absCommand, values[0] + y] as VSegment;
45
+ } else if (absCommand === 'H') {
46
+ return [absCommand, values[0] + x] as HSegment;
47
+ } else {
48
+ // use brakets for `eslint: no-case-declaration`
49
+ // https://stackoverflow.com/a/50753272/803358
50
+ const absValues = values.map((n, j) => n + (j % 2 ? y : x));
51
+ // for n, l, c, s, q, t
52
+ return [absCommand, ...absValues] as MSegment | QSegment | TSegment | SSegment | CSegment;
53
+ }
54
+ }
55
+
56
+ return segment as AbsoluteSegment;
57
+ };
58
+ export default absolutizeSegment;
@@ -0,0 +1,33 @@
1
+ import paramsParser from '../parser/paramsParser';
2
+ import type { PathArray, PathCommand, PathSegment, IteratorCallback } from '../types';
3
+
4
+ const iterate = <T extends PathArray>(path: PathArray, iterator: IteratorCallback) => {
5
+ const allPathCommands = [] as PathCommand[];
6
+ const params = { ...paramsParser };
7
+ let pathLen = path.length;
8
+ let segment: PathSegment;
9
+ let pathCommand = 'M' as PathCommand;
10
+
11
+ for (let i = 0; i < pathLen; i += 1) {
12
+ segment = path[i];
13
+ [pathCommand] = segment;
14
+ allPathCommands[i] = pathCommand;
15
+ const iteratorResult = iterator(segment, params, i);
16
+ path[i] = iteratorResult;
17
+
18
+ if (iteratorResult[0] === 'C') {
19
+ pathLen = path.length;
20
+ }
21
+
22
+ segment = path[i];
23
+ const seglen = segment.length;
24
+ params.x1 = +segment[seglen - 2];
25
+ params.y1 = +segment[seglen - 1];
26
+ params.x2 = +segment[seglen - 4] || params.x1;
27
+ params.y2 = +segment[seglen - 3] || params.y1;
28
+ }
29
+ // console.log('iteration: ', ...path)
30
+ return path as T;
31
+ };
32
+
33
+ export default iterate;
@@ -1,8 +1,8 @@
1
- import pathToAbsolute from '../convert/pathToAbsolute';
2
1
  import normalizeSegment from './normalizeSegment';
3
- import isNormalizedArray from '../util/isNormalizedArray';
4
- import paramsParser from '../parser/paramsParser';
5
- import type { NormalArray, PathArray } from '../types';
2
+ import type { AbsoluteCommand, NormalArray, PathArray, PointTuple } from '../types';
3
+ import iterate from './iterate';
4
+ import parsePathString from '../parser/parsePathString';
5
+ import absolutizeSegment from './absolutizeSegment';
6
6
 
7
7
  /**
8
8
  * Normalizes a `path` object for further processing:
@@ -12,33 +12,39 @@ import type { NormalArray, PathArray } from '../types';
12
12
  * @param pathInput the string to be parsed or 'pathArray'
13
13
  * @returns the normalized `pathArray`
14
14
  */
15
- const normalizePath = (pathInput: string | PathArray): NormalArray => {
16
- if (isNormalizedArray(pathInput)) {
17
- return [...pathInput];
18
- }
15
+ const normalizePath = (pathInput: string | PathArray) => {
16
+ let x = 0;
17
+ let y = 0;
18
+ let mx = 0;
19
+ let my = 0;
20
+ let pathCommand = 'M';
19
21
 
20
- const path = pathToAbsolute(pathInput);
21
- const params = { ...paramsParser };
22
- const allPathCommands = [];
23
- const ii = path.length;
24
- let pathCommand = '';
22
+ return iterate<NormalArray>(parsePathString(pathInput), (seg, params) => {
23
+ const absoluteSegment = absolutizeSegment(seg, params);
24
+ const result = normalizeSegment(absoluteSegment, params);
25
+ [pathCommand] = result;
26
+ const absCommand = pathCommand.toUpperCase() as AbsoluteCommand;
25
27
 
26
- for (let i = 0; i < ii; i += 1) {
27
- [pathCommand] = path[i];
28
+ if (absCommand === 'Z') {
29
+ x = mx;
30
+ y = my;
31
+ } else {
32
+ [x, y] = result.slice(-2) as PointTuple;
28
33
 
29
- // Save current path command
30
- allPathCommands[i] = pathCommand;
31
- path[i] = normalizeSegment(path[i], params);
34
+ if (absCommand === 'M') {
35
+ mx = x;
36
+ my = y;
37
+ }
38
+ }
32
39
 
33
- const segment = path[i];
34
- const seglen = segment.length;
35
-
36
- params.x1 = +segment[seglen - 2];
37
- params.y1 = +segment[seglen - 1];
38
- params.x2 = +segment[seglen - 4] || params.x1;
39
- params.y2 = +segment[seglen - 3] || params.y1;
40
- }
41
-
42
- return path as NormalArray;
40
+ // const seglen = result.length;
41
+ // params.x1 = +result[seglen - 2];
42
+ // params.y1 = +result[seglen - 1];
43
+ // params.x2 = +result[seglen - 4] || params.x1;
44
+ // params.y2 = +result[seglen - 3] || params.y1;
45
+ params.x = x;
46
+ params.y = y;
47
+ return result;
48
+ });
43
49
  };
44
50
  export default normalizePath;
@@ -1,5 +1,5 @@
1
1
  import type { ParserParams } from '../interface';
2
- import type { NormalSegment, PathSegment } from '../types';
2
+ import type { NormalSegment, PointTuple, PathSegment, QSegment, CSegment, LSegment } from '../types';
3
3
 
4
4
  /**
5
5
  * Normalizes a single segment of a `pathArray` object.
@@ -8,11 +8,10 @@ import type { NormalSegment, PathSegment } from '../types';
8
8
  * @param params the coordinates of the previous segment
9
9
  * @returns the normalized segment
10
10
  */
11
- const normalizeSegment = (segment: PathSegment, params: ParserParams): NormalSegment => {
11
+ const normalizeSegment = (segment: PathSegment, params: ParserParams) => {
12
12
  const [pathCommand] = segment;
13
13
  const { x1: px1, y1: py1, x2: px2, y2: py2 } = params;
14
14
  const values = segment.slice(1).map(Number);
15
- let result = segment;
16
15
 
17
16
  if (!'TQ'.includes(pathCommand)) {
18
17
  // optional but good to be cautious
@@ -21,27 +20,27 @@ const normalizeSegment = (segment: PathSegment, params: ParserParams): NormalSeg
21
20
  }
22
21
 
23
22
  if (pathCommand === 'H') {
24
- result = ['L', segment[1], py1];
23
+ return ['L', segment[1], py1] as LSegment;
25
24
  } else if (pathCommand === 'V') {
26
- result = ['L', px1, segment[1]];
25
+ return ['L', px1, segment[1]] as LSegment;
27
26
  } else if (pathCommand === 'S') {
28
27
  const x1 = px1 * 2 - px2;
29
28
  const y1 = py1 * 2 - py2;
30
29
  params.x1 = x1;
31
30
  params.y1 = y1;
32
- result = ['C', x1, y1, ...(values as [number, number, number, number])];
31
+ return ['C', x1, y1, ...values] as CSegment;
33
32
  } else if (pathCommand === 'T') {
34
33
  const qx = px1 * 2 - (params.qx ? params.qx : /* istanbul ignore next */ 0);
35
34
  const qy = py1 * 2 - (params.qy ? params.qy : /* istanbul ignore next */ 0);
36
35
  params.qx = qx;
37
36
  params.qy = qy;
38
- result = ['Q', qx, qy, ...(values as [number, number])];
37
+ return ['Q', qx, qy, ...values] as QSegment;
39
38
  } else if (pathCommand === 'Q') {
40
- const [nqx, nqy] = values as [number, number];
39
+ const [nqx, nqy] = values as PointTuple;
41
40
  params.qx = nqx;
42
41
  params.qy = nqy;
43
42
  }
44
43
 
45
- return result as NormalSegment;
44
+ return segment as NormalSegment;
46
45
  };
47
46
  export default normalizeSegment;
@@ -1,4 +1,5 @@
1
1
  import CSSMatrix from '@thednp/dommatrix';
2
+ import { type PointTuple } from '../types';
2
3
 
3
4
  /**
4
5
  * Transforms a specified point using a matrix, returning a new
@@ -34,7 +35,7 @@ const translatePoint = (cssm: CSSMatrix, v: [number, number, number, number]): [
34
35
  * @param origin the [x,y,z] transform origin
35
36
  * @returns the projected [x,y] coordinates
36
37
  */
37
- const projection2d = (m: CSSMatrix, point2D: [number, number], origin: [number, number, number]): [number, number] => {
38
+ const projection2d = (m: CSSMatrix, point2D: PointTuple, origin: [number, number, number]): PointTuple => {
38
39
  const [originX, originY, originZ] = origin;
39
40
  const [x, y, z] = translatePoint(m, [...point2D, 0, 1]);
40
41
 
@@ -0,0 +1,61 @@
1
+ import type { ParserParams } from '../interface';
2
+ import type {
3
+ RelativeSegment,
4
+ RelativeCommand,
5
+ PathSegment,
6
+ aSegment,
7
+ vSegment,
8
+ hSegment,
9
+ qSegment,
10
+ tSegment,
11
+ sSegment,
12
+ cSegment,
13
+ } from '../types';
14
+
15
+ /**
16
+ * Returns a relative segment of a `PathArray` object.
17
+ *
18
+ * @param segment the segment object
19
+ * @param params the coordinates of the previous segment
20
+ * @param index the segment index
21
+ * @returns the absolute segment
22
+ */
23
+ const relativizeSegment = (segment: PathSegment, params: ParserParams, index: number) => {
24
+ const [pathCommand] = segment;
25
+ const { x, y } = params;
26
+ const values = segment.slice(1).map(Number);
27
+ const relCommand = pathCommand.toLowerCase() as RelativeCommand;
28
+
29
+ if (index === 0 && pathCommand === 'M') {
30
+ return segment;
31
+ }
32
+
33
+ /* istanbul ignore else @preserve */
34
+ if (pathCommand !== relCommand) {
35
+ if (relCommand === 'a') {
36
+ return [
37
+ relCommand,
38
+ values[0],
39
+ values[1],
40
+ values[2],
41
+ values[3],
42
+ values[4],
43
+ values[5] - x,
44
+ values[6] - y,
45
+ ] as aSegment;
46
+ } else if (relCommand === 'v') {
47
+ return [relCommand, values[0] - y] as vSegment;
48
+ } else if (relCommand === 'h') {
49
+ return [relCommand, values[0] - x] as hSegment;
50
+ } else {
51
+ // use brakets for `eslint: no-case-declaration`
52
+ // https://stackoverflow.com/a/50753272/803358
53
+ const relValues = values.map((n, j) => n - (j % 2 ? y : x));
54
+ // for n, l, c, s, q, t
55
+ return [relCommand, ...relValues] as qSegment | tSegment | sSegment | cSegment;
56
+ }
57
+ }
58
+
59
+ return segment as RelativeSegment;
60
+ };
61
+ export default relativizeSegment;
@@ -9,7 +9,7 @@ import type {
9
9
  SSegment,
10
10
  TSegment,
11
11
  VSegment,
12
- } from 'src/types';
12
+ } from '../types';
13
13
  import pathToAbsolute from '../convert/pathToAbsolute';
14
14
  import normalizePath from './normalizePath';
15
15
 
@@ -1,5 +1,6 @@
1
- import type { PathArray } from 'src/types';
1
+ import type { PathArray, PathSegment } from '../types';
2
2
  import defaultOptions from '../options/options';
3
+ import iterate from './iterate';
3
4
 
4
5
  /**
5
6
  * Rounds the values of a `pathArray` instance to
@@ -9,21 +10,18 @@ import defaultOptions from '../options/options';
9
10
  * @param roundOption the amount of decimals to round numbers to
10
11
  * @returns the resulted `pathArray` with rounded values
11
12
  */
12
- const roundPath = (path: PathArray, roundOption?: number | 'off'): PathArray => {
13
+ const roundPath = (path: PathArray, roundOption?: number | 'off') => {
13
14
  let { round } = defaultOptions;
14
- if (roundOption === 'off' || round === 'off') return [...path];
15
+ if (roundOption === 'off' || round === 'off') return path.slice(0) as PathArray;
15
16
  // allow for ZERO decimals
16
17
  round = typeof roundOption === 'number' && roundOption >= 0 ? roundOption : round;
17
18
  // to round values to the power
18
19
  // the `round` value must be integer
19
20
  const pow = typeof round === 'number' && round >= 1 ? 10 ** round : 1;
20
21
 
21
- return path.map(pi => {
22
- const values = pi
23
- .slice(1)
24
- .map(Number)
25
- .map(n => (round ? Math.round(n * pow) / pow : Math.round(n)));
26
- return [pi[0], ...values];
27
- }) as PathArray;
22
+ return iterate<typeof path>(path, segment => {
23
+ const values = (segment.slice(1) as number[]).map(n => (round ? Math.round(n * pow) / pow : Math.round(n)));
24
+ return [segment[0], ...values] as PathSegment;
25
+ });
28
26
  };
29
27
  export default roundPath;
@@ -11,7 +11,7 @@ import type { ParserParams } from '../interface';
11
11
  * @param params the source segment parameters
12
12
  * @returns the cubic-bezier segment
13
13
  */
14
- const segmentToCubic = (segment: PathSegment, params: ParserParams): MSegment | CSegment => {
14
+ const segmentToCubic = (segment: PathSegment, params: ParserParams) => {
15
15
  const [pathCommand] = segment;
16
16
  const values = segment.slice(1).map(Number);
17
17
  const [x, y] = values;
@@ -1,4 +1,4 @@
1
- import type { ParserParams } from 'src/interface';
1
+ import type { ParserParams } from '../interface';
2
2
  import type {
3
3
  AbsoluteSegment,
4
4
  HSegment,
@@ -28,8 +28,8 @@ const shortenSegment = (
28
28
  ): ShortSegment => {
29
29
  const [pathCommand] = segment;
30
30
  const round4 = (n: number) => Math.round(n * 10 ** 4) / 10 ** 4;
31
- const segmentValues = segment.slice(1).map(n => +n);
32
- const normalValues = normalSegment.slice(1).map(n => +n);
31
+ const segmentValues = segment.slice(1) as number[];
32
+ const normalValues = normalSegment.slice(1) as number[];
33
33
  const { x1: px1, y1: py1, x2: px2, y2: py2, x: px, y: py } = params;
34
34
  let result = segment;
35
35
  const [x, y] = normalValues.slice(-2);
@@ -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 {
22
- path = [...path, seg];
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
+ }
53
+ path.push(seg);
23
54
  }
55
+
56
+ params.x = x;
57
+ params.y = y;
24
58
  composite[pi] = path;
25
59
  });
26
60