svg-path-commander 2.0.10 → 2.1.0

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 (59) hide show
  1. package/.eslintrc.cjs +1 -0
  2. package/README.md +4 -4
  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 +137 -30
  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 +868 -698
  9. package/dist/svg-path-commander.mjs.map +1 -1
  10. package/package.json +20 -22
  11. package/src/convert/pathToAbsolute.ts +1 -1
  12. package/src/convert/pathToCurve.ts +1 -1
  13. package/src/convert/pathToRelative.ts +1 -1
  14. package/src/index.ts +30 -26
  15. package/src/interface.ts +32 -32
  16. package/src/math/arcTools.ts +217 -0
  17. package/src/math/bezier.ts +261 -0
  18. package/src/math/cubicTools.ts +81 -0
  19. package/src/math/lineTools.ts +52 -0
  20. package/src/math/quadTools.ts +79 -0
  21. package/src/parser/isMoveCommand.ts +17 -0
  22. package/src/parser/parsePathString.ts +1 -1
  23. package/src/parser/scanSegment.ts +12 -3
  24. package/src/process/normalizePath.ts +1 -1
  25. package/src/process/replaceArc.ts +52 -0
  26. package/src/process/splitPath.ts +1 -1
  27. package/src/process/transformPath.ts +14 -34
  28. package/src/types.ts +5 -0
  29. package/src/util/distanceEpsilon.ts +3 -0
  30. package/src/util/getClosestPoint.ts +1 -1
  31. package/src/util/getPathBBox.ts +4 -3
  32. package/src/util/getPointAtLength.ts +3 -3
  33. package/src/util/getPropertiesAtLength.ts +2 -1
  34. package/src/util/getPropertiesAtPoint.ts +4 -1
  35. package/src/util/getTotalLength.ts +2 -2
  36. package/src/util/isPointInStroke.ts +2 -1
  37. package/src/util/pathFactory.ts +130 -0
  38. package/src/util/shapeToPathArray.ts +8 -4
  39. package/test/class.test.ts +501 -0
  40. package/test/fixtures/getMarkup.ts +17 -0
  41. package/{cypress → test}/fixtures/shapes.js +18 -18
  42. package/{cypress → test}/fixtures/simpleShapes.js +6 -6
  43. package/test/static.test.ts +304 -0
  44. package/tsconfig.json +9 -4
  45. package/{vite.config.ts → vite.config.mts} +10 -1
  46. package/vitest.config-ui.mts +26 -0
  47. package/vitest.config.mts +26 -0
  48. package/cypress/e2e/svg-path-commander.spec.ts +0 -868
  49. package/cypress/plugins/esbuild-istanbul.ts +0 -50
  50. package/cypress/plugins/tsCompile.ts +0 -34
  51. package/cypress/support/commands.ts +0 -37
  52. package/cypress/support/e2e.ts +0 -21
  53. package/cypress/test.html +0 -36
  54. package/src/util/pathLengthFactory.ts +0 -114
  55. package/src/util/segmentArcFactory.ts +0 -219
  56. package/src/util/segmentCubicFactory.ts +0 -114
  57. package/src/util/segmentLineFactory.ts +0 -45
  58. package/src/util/segmentQuadFactory.ts +0 -109
  59. /package/{cypress/fixtures/shapeObjects.js → test/fixtures/shapeObjects.ts} +0 -0
@@ -1,12 +1,13 @@
1
1
  import normalizePath from './normalizePath';
2
- import pathToAbsolute from '../convert/pathToAbsolute';
3
- import segmentToCubic from './segmentToCubic';
4
- import fixArc from './fixArc';
2
+ // import pathToAbsolute from '../convert/pathToAbsolute';
3
+ // import segmentToCubic from './segmentToCubic';
4
+ // import fixArc from './fixArc';
5
5
  import getSVGMatrix from './getSVGMatrix';
6
6
  import projection2d from './projection2d';
7
7
  import paramsParser from '../parser/paramsParser';
8
+ import replaceArc from './replaceArc';
8
9
  import defaultOptions from '../options/options';
9
- import type { PathArray, PathCommand, TransformObjectValues } from '../types';
10
+ import type { AbsoluteArray, PathArray, TransformObjectValues } from '../types';
10
11
  import type { PathTransform, TransformObject } from '../interface';
11
12
 
12
13
  /**
@@ -28,11 +29,14 @@ const transformPath = (path: string | PathArray, transform?: Partial<TransformOb
28
29
  let jj;
29
30
  let lx;
30
31
  let ly;
31
- const absolutePath = pathToAbsolute(path);
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);
32
36
  const transformProps = transform && Object.keys(transform);
33
37
 
34
38
  // when used as a static method, invalidate somehow
35
- if (!transform || (transformProps && !transformProps.length)) return [...absolutePath];
39
+ if (!transform || (transformProps && !transformProps.length)) return absolutePath.slice(0) as PathArray;
36
40
 
37
41
  const normalizedPath = normalizePath(absolutePath);
38
42
  // transform origin is extremely important
@@ -46,34 +50,10 @@ const transformPath = (path: string | PathArray, transform?: Partial<TransformOb
46
50
  let segment = [];
47
51
  let seglen = 0;
48
52
  let pathCommand = '';
49
- let transformedPath = [] as PathTransform[];
50
- const allPathCommands = [] as PathCommand[]; // needed for arc to curve transformation
53
+ const transformedPath = [] as PathTransform[];
51
54
 
52
55
  if (!matrixInstance.isIdentity) {
53
56
  for (i = 0, ii = absolutePath.length; i < ii; i += 1) {
54
- segment = absolutePath[i];
55
-
56
- /* istanbul ignore else */
57
- if (absolutePath[i]) [pathCommand] = segment;
58
-
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;
64
-
65
- if (pathCommand === 'A') {
66
- segment = segmentToCubic(normalizedPath[i], params);
67
-
68
- absolutePath[i] = segmentToCubic(normalizedPath[i], params);
69
- fixArc(absolutePath, allPathCommands, i);
70
-
71
- normalizedPath[i] = segmentToCubic(normalizedPath[i], params);
72
- fixArc(normalizedPath, allPathCommands, i);
73
- ii = Math.max(absolutePath.length, normalizedPath.length);
74
- }
75
-
76
- /// ////////////////////////////////////////
77
57
  segment = normalizedPath[i];
78
58
  seglen = segment.length;
79
59
 
@@ -89,7 +69,7 @@ const transformPath = (path: string | PathArray, transform?: Partial<TransformOb
89
69
  y: params.y1,
90
70
  };
91
71
 
92
- transformedPath = [...transformedPath, ...[result]];
72
+ transformedPath.push(result);
93
73
  }
94
74
 
95
75
  return transformedPath.map(seg => {
@@ -98,7 +78,7 @@ const transformPath = (path: string | PathArray, transform?: Partial<TransformOb
98
78
  if (pathCommand === 'L' || pathCommand === 'H' || pathCommand === 'V') {
99
79
  [lx, ly] = projection2d(matrixInstance, [seg.x, seg.y], origin as [number, number, number]);
100
80
 
101
- /* istanbul ignore else */
81
+ /* istanbul ignore else @preserve */
102
82
  if (x !== lx && y !== ly) {
103
83
  segment = ['L', lx, ly];
104
84
  } else if (y === ly) {
@@ -123,6 +103,6 @@ const transformPath = (path: string | PathArray, transform?: Partial<TransformOb
123
103
  }
124
104
  }) as PathArray;
125
105
  }
126
- return [...absolutePath];
106
+ return absolutePath.slice(0) as AbsoluteArray;
127
107
  };
128
108
  export default transformPath;
package/src/types.ts CHANGED
@@ -191,3 +191,8 @@ export type ShapeTags = 'line' | 'polyline' | 'polygon' | 'ellipse' | 'circle' |
191
191
  export type ShapeOps = LineAttr | PolyAttr | PolyAttr | EllipseAttr | CircleAttr | RectAttr | GlyphAttr;
192
192
 
193
193
  export type TransformObjectValues = Partial<TransformObject> & { origin: [number, number, number] };
194
+
195
+ export type Point = {
196
+ x: number;
197
+ y: number;
198
+ };
@@ -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,6 +1,6 @@
1
1
  import { PathBBox } from 'src/interface';
2
2
  import { PathArray } from 'src/types';
3
- import pathLengthFactory from './pathLengthFactory';
3
+ import pathFactory from './pathFactory';
4
4
 
5
5
  /**
6
6
  * Returns the bounding box of a shape.
@@ -8,7 +8,7 @@ import pathLengthFactory from './pathLengthFactory';
8
8
  * @param path the shape `pathArray`
9
9
  * @returns the length of the cubic-bezier segment
10
10
  */
11
- const getPathBBox = (path?: PathArray | string): PathBBox => {
11
+ const getPathBBox = (path: PathArray | string): PathBBox => {
12
12
  if (!path) {
13
13
  return {
14
14
  x: 0,
@@ -23,10 +23,11 @@ const getPathBBox = (path?: PathArray | string): PathBBox => {
23
23
  };
24
24
  }
25
25
 
26
+ const props = pathFactory(path);
26
27
  const {
27
28
  min: { x: xMin, y: yMin },
28
29
  max: { x: xMax, y: yMax },
29
- } = pathLengthFactory(path);
30
+ } = props.bbox;
30
31
 
31
32
  const width = xMax - xMin;
32
33
  const height = yMax - yMin;
@@ -1,5 +1,5 @@
1
1
  import type { PathArray } from '../types';
2
- import pathLengthFactory from './pathLengthFactory';
2
+ import pathFactory from './pathFactory';
3
3
 
4
4
  /**
5
5
  * Returns [x,y] coordinates of a point at a given length of a shape.
@@ -8,7 +8,7 @@ import pathLengthFactory from './pathLengthFactory';
8
8
  * @param distance the length of the shape to look at
9
9
  * @returns the requested {x, y} point coordinates
10
10
  */
11
- const getPointAtLength = (pathInput: string | PathArray, distance: number): { x: number; y: number } => {
12
- return pathLengthFactory(pathInput, distance).point;
11
+ const getPointAtLength = (pathInput: string | PathArray, distance: number) => {
12
+ return pathFactory(pathInput, distance).point;
13
13
  };
14
14
  export default getPointAtLength;
@@ -14,7 +14,7 @@ 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;
@@ -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,5 @@
1
1
  import type { PathArray } from '../types';
2
- import pathLengthFactory from './pathLengthFactory';
2
+ import pathFactory from './pathFactory';
3
3
 
4
4
  /**
5
5
  * Returns the shape total length, or the equivalent to `shape.getTotalLength()`.
@@ -11,6 +11,6 @@ import pathLengthFactory from './pathLengthFactory';
11
11
  * @returns the shape total length
12
12
  */
13
13
  const getTotalLength = (pathInput: string | PathArray): number => {
14
- return pathLengthFactory(pathInput).length;
14
+ return pathFactory(pathInput).length;
15
15
  };
16
16
  export default getTotalLength;
@@ -1,5 +1,6 @@
1
1
  import type { PathArray } from '../types';
2
2
  import getPropertiesAtPoint from './getPropertiesAtPoint';
3
+ import DISTANCE_EPSILON from './distanceEpsilon';
3
4
 
4
5
  /**
5
6
  * Checks if a given point is in the stroke of a path.
@@ -10,6 +11,6 @@ import getPropertiesAtPoint from './getPropertiesAtPoint';
10
11
  */
11
12
  const isPointInStroke = (pathInput: string | PathArray, point: { x: number; y: number }) => {
12
13
  const { distance } = getPropertiesAtPoint(pathInput, point);
13
- return Math.abs(distance) < 0.001; // 0.01 might be more permissive
14
+ return Math.abs(distance) < DISTANCE_EPSILON; // 0.01 might be more permissive
14
15
  };
15
16
  export default isPointInStroke;
@@ -0,0 +1,130 @@
1
+ import type { MSegment, PathArray, PathSegment, Point } from '../types';
2
+ // import type { LengthFactory } from '../interface';
3
+ import normalizePath from '../process/normalizePath';
4
+ import getLineSegmentProperties from '../math/lineTools';
5
+ import getArcSegmentProperties from '../math/arcTools';
6
+ import getCubicSegmentProperties from '../math/cubicTools';
7
+ import getQuadSegmentProperties from '../math/quadTools';
8
+ import DISTANCE_EPSILON from './distanceEpsilon';
9
+
10
+ /**
11
+ * Returns a {x,y} point at a given length
12
+ * of a shape, the shape total length and
13
+ * the shape minimum and maximum {x,y} coordinates.
14
+ *
15
+ * @param pathInput the `pathArray` to look into
16
+ * @param distance the length of the shape to look at
17
+ * @returns the path length, point, min & max
18
+ */
19
+ const pathFactory = (pathInput: string | PathArray, distance?: number) => {
20
+ const path = normalizePath(pathInput);
21
+ const distanceIsNumber = typeof distance === 'number';
22
+ let isM = false;
23
+ let data = [] as number[];
24
+ let pathCommand = 'M';
25
+ let x = 0;
26
+ let y = 0;
27
+ let mx = 0;
28
+ let my = 0;
29
+ let seg = path[0] as PathSegment;
30
+ const MIN = [] as Point[];
31
+ const MAX = [] as Point[];
32
+ let min = { x: 0, y: 0 };
33
+ let max = { x: 0, y: 0 };
34
+ let POINT = min;
35
+ let LENGTH = 0;
36
+ let props = {
37
+ point: { x: 0, y: 0 },
38
+ length: 0,
39
+ bbox: {
40
+ min: { x: 0, y: 0 },
41
+ max: { x: 0, y: 0 },
42
+ },
43
+ };
44
+
45
+ for (let i = 0, ll = path.length; i < ll; i += 1) {
46
+ seg = path[i];
47
+ [pathCommand] = seg;
48
+ isM = pathCommand === 'M';
49
+ data = !isM ? [x, y, ...(seg.slice(1) as number[])] : data;
50
+
51
+ if (distanceIsNumber && distance < DISTANCE_EPSILON) {
52
+ POINT = min;
53
+ }
54
+
55
+ // this segment is always ZERO
56
+ /* istanbul ignore else @preserve */
57
+ if (isM) {
58
+ // remember mx, my for Z
59
+ [, mx, my] = seg as MSegment;
60
+ min = { x: mx, y: my };
61
+ max = { x: mx, y: my };
62
+ props = {
63
+ point: min,
64
+ length: 0,
65
+ bbox: { min, max },
66
+ };
67
+ } else if (pathCommand === 'L') {
68
+ props = getLineSegmentProperties(
69
+ ...(data as [number, number, number, number]),
70
+ distanceIsNumber ? distance - LENGTH : undefined,
71
+ );
72
+ } else if (pathCommand === 'A') {
73
+ props = getArcSegmentProperties(
74
+ ...(data as [number, number, number, number, number, number, number, number, number]),
75
+ distanceIsNumber ? distance - LENGTH : undefined,
76
+ );
77
+ } else if (pathCommand === 'C') {
78
+ props = getCubicSegmentProperties(
79
+ ...(data as [number, number, number, number, number, number, number, number]),
80
+ distanceIsNumber ? distance - LENGTH : undefined,
81
+ );
82
+ } else if (pathCommand === 'Q') {
83
+ props = getQuadSegmentProperties(
84
+ ...(data as [number, number, number, number, number, number]),
85
+ distanceIsNumber ? distance - LENGTH : undefined,
86
+ );
87
+ } else if (pathCommand === 'Z') {
88
+ data = [x, y, mx, my];
89
+ props = getLineSegmentProperties(
90
+ ...(data as [number, number, number, number]),
91
+ distanceIsNumber ? distance - LENGTH : undefined,
92
+ );
93
+ }
94
+
95
+ if (distanceIsNumber && LENGTH < distance && LENGTH + props.length >= distance) {
96
+ POINT = props.point;
97
+ }
98
+
99
+ MIN.push(props.bbox.min);
100
+ MAX.push(props.bbox.max);
101
+ LENGTH += props.length;
102
+
103
+ [x, y] = pathCommand !== 'Z' ? (seg.slice(-2) as [number, number]) : [mx, my];
104
+ }
105
+
106
+ // native `getPointAtLength` behavior when the given distance
107
+ // is higher than total length
108
+ if (distanceIsNumber && distance > LENGTH - DISTANCE_EPSILON) {
109
+ POINT = { x, y };
110
+ }
111
+
112
+ return {
113
+ point: POINT,
114
+ length: LENGTH,
115
+ get bbox() {
116
+ return {
117
+ min: {
118
+ x: Math.min(...MIN.map(n => n.x)),
119
+ y: Math.min(...MIN.map(n => n.y)),
120
+ },
121
+ max: {
122
+ x: Math.max(...MAX.map(n => n.x)),
123
+ y: Math.max(...MAX.map(n => n.y)),
124
+ },
125
+ };
126
+ },
127
+ };
128
+ };
129
+
130
+ export default pathFactory;
@@ -97,9 +97,9 @@ export const getRectanglePath = (attr: RectAttr): PathArray => {
97
97
  // rx = !rx ? ry : rx;
98
98
  // ry = !ry ? rx : ry;
99
99
 
100
- /* istanbul ignore else */
100
+ /* istanbul ignore else @preserve */
101
101
  if (rx * 2 > w) rx -= (rx * 2 - w) / 2;
102
- /* istanbul ignore else */
102
+ /* istanbul ignore else @preserve */
103
103
  if (ry * 2 > h) ry -= (ry * 2 - h) / 2;
104
104
 
105
105
  return [
@@ -134,7 +134,7 @@ export const getRectanglePath = (attr: RectAttr): PathArray => {
134
134
  * @param ownerDocument document for create element
135
135
  * @return the newly created `<path>` element
136
136
  */
137
- const shapeToPathArray = (element: ShapeTypes | ShapeOps, ownerDocument?: Document): PathArray | false => {
137
+ const shapeToPathArray = (element: ShapeTypes | ShapeOps, ownerDocument?: Document) => {
138
138
  const doc = ownerDocument || document;
139
139
  const win = doc.defaultView || /* istanbul ignore next */ window;
140
140
  const supportedShapes = Object.keys(shapeParams) as (keyof ShapeParams)[];
@@ -167,7 +167,11 @@ const shapeToPathArray = (element: ShapeTypes | ShapeOps, ownerDocument?: Docume
167
167
  else if (type === 'rect') pathArray = getRectanglePath(config as unknown as RectAttr);
168
168
  else if (type === 'line') pathArray = getLinePath(config as unknown as LineAttr);
169
169
  else if (['glyph', 'path'].includes(type)) {
170
- pathArray = parsePathString(targetIsElement ? element.getAttribute('d') || '' : (element as GlyphAttr).d || '');
170
+ pathArray = parsePathString(
171
+ targetIsElement
172
+ ? element.getAttribute('d') || /* istanbul ignore next @preserve */ ''
173
+ : (element as GlyphAttr).d || '',
174
+ );
171
175
  }
172
176
 
173
177
  // replace target element