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
package/package.json CHANGED
@@ -2,7 +2,7 @@
2
2
  "name": "svg-path-commander",
3
3
  "author": "thednp",
4
4
  "license": "MIT",
5
- "version": "2.0.10",
5
+ "version": "2.1.0",
6
6
  "description": "Modern TypeScript tools for SVG",
7
7
  "source": "./src/index.ts",
8
8
  "main": "./dist/svg-path-commander.js",
@@ -38,28 +38,24 @@
38
38
  },
39
39
  "homepage": "http://thednp.github.io/svg-path-commander",
40
40
  "devDependencies": {
41
- "@bahmutov/cypress-esbuild-preprocessor": "^2.2.2",
42
- "@cypress/code-coverage": "^3.12.44",
43
- "@types/istanbul-lib-instrument": "^1.7.7",
44
41
  "@typescript-eslint/eslint-plugin": "^5.62.0",
45
42
  "@typescript-eslint/parser": "^5.62.0",
46
- "cypress": "^13.13.2",
43
+ "@vitest/browser": "^2.1.2",
44
+ "@vitest/coverage-istanbul": "^2.1.2",
45
+ "@vitest/ui": "^2.1.2",
47
46
  "dts-bundle-generator": "^9.5.1",
48
- "eslint": "^8.57.0",
47
+ "eslint": "^8.57.1",
49
48
  "eslint-plugin-jsdoc": "^46.10.1",
50
49
  "eslint-plugin-prefer-arrow": "^1.2.3",
51
50
  "eslint-plugin-prettier": "^4.2.1",
52
- "istanbul-lib-coverage": "^3.2.2",
53
- "istanbul-lib-instrument": "^5.2.1",
54
- "ncp": "^2.0.0",
55
- "nyc": "^17.0.0",
51
+ "playwright": "^1.47.2",
56
52
  "prettier": "^2.8.8",
57
- "rimraf": "^6.0.1",
58
- "typescript": "^5.5.4",
59
- "vite": "^5.4.0"
53
+ "typescript": "^5.6.2",
54
+ "vite": "^5.4.8",
55
+ "vitest": "^2.1.2"
60
56
  },
61
57
  "dependencies": {
62
- "@thednp/dommatrix": "^2.0.7"
58
+ "@thednp/dommatrix": "^2.0.8"
63
59
  },
64
60
  "engines": {
65
61
  "node": ">=16",
@@ -67,17 +63,19 @@
67
63
  },
68
64
  "scripts": {
69
65
  "pre-test": "pnpm clean-coverage",
70
- "badges": "npx -p dependency-version-badge update-badge typescript cypress eslint prettier vite",
71
- "test": "pnpm pre-test && cypress run",
72
- "cypress": "pnpm pre-test && npx cypress open",
73
- "clean-coverage": "rimraf coverage .nyc_output",
74
- "coverage:report": "nyc report --reporter=lcov --reporter=json --reporter=text --reporter=json-summary",
66
+ "badges": "npx -p dependency-version-badge update-badge typescript eslint prettier vitest vite",
67
+ "dev": "vite serve docs --port 3000",
68
+ "test": "pnpm pre-test && vitest --config vitest.config.mts",
69
+ "test-ui": "pnpm pre-test && vitest --config vitest.config-ui.mts --browser=chrome",
70
+ "clean-coverage": "rm -rf coverage .nyc_output",
75
71
  "format": "prettier --write \"src/**/*.ts\"",
72
+ "lint": "pnpm lint:ts && pnpm check:ts",
76
73
  "fix:ts": "eslint src --config .eslintrc.cjs --fix",
77
74
  "lint:ts": "eslint src --config .eslintrc.cjs",
78
- "build": "pnpm lint:ts && pnpm build-vite && pnpm build-ts",
79
- "build-vite": "vite build && pnpm build-docs",
75
+ "check:ts": "tsc --noEmit",
76
+ "build": "pnpm lint && pnpm build-vite && pnpm build-ts",
77
+ "build-vite": "vite build && pnpm copy-docs",
80
78
  "build-ts": "dts-bundle-generator --config ./dts.config.ts",
81
- "build-docs": "ncp dist/svg-path-commander.js docs/svg-path-commander.js && ncp dist/svg-path-commander.js.map docs/svg-path-commander.js.map"
79
+ "copy-docs": "cp dist/svg-path-commander.js docs/svg-path-commander.js && cp dist/svg-path-commander.js.map docs/svg-path-commander.js.map"
82
80
  }
83
81
  }
@@ -25,7 +25,7 @@ import type {
25
25
  const pathToAbsolute = (pathInput: string | PathArray): AbsoluteArray => {
26
26
  /* istanbul ignore else */
27
27
  if (isAbsoluteArray(pathInput)) {
28
- return [...pathInput];
28
+ return pathInput.slice(0) as AbsoluteArray;
29
29
  }
30
30
 
31
31
  const path = parsePathString(pathInput);
@@ -18,7 +18,7 @@ import { CurveArray, PathArray, PathCommand } from '../types';
18
18
  const pathToCurve = (pathInput: string | PathArray): CurveArray => {
19
19
  /* istanbul ignore else */
20
20
  if (isCurveArray(pathInput)) {
21
- return [...pathInput];
21
+ return pathInput.slice(0) as CurveArray;
22
22
  }
23
23
 
24
24
  const path = normalizePath(pathInput);
@@ -20,7 +20,7 @@ import isRelativeArray from '../util/isRelativeArray';
20
20
  const pathToRelative = (pathInput: string | PathArray): RelativeArray => {
21
21
  /* istanbul ignore else */
22
22
  if (isRelativeArray(pathInput)) {
23
- return [...pathInput];
23
+ return pathInput.slice(0) as RelativeArray;
24
24
  }
25
25
 
26
26
  const path = parsePathString(pathInput);
package/src/index.ts CHANGED
@@ -1,5 +1,7 @@
1
1
  import { PathArray, TransformObjectValues } from './types';
2
- import { Options, PathBBox, TransformEntries, TransformObject } from './interface';
2
+ import type { Options, TransformEntries, TransformObject } from './interface';
3
+ export * from './types';
4
+ export * from './interface';
3
5
  import defaultOptions from './options/options';
4
6
 
5
7
  import error from './parser/error';
@@ -14,7 +16,7 @@ import getPathArea from './util/getPathArea';
14
16
  import getTotalLength from './util/getTotalLength';
15
17
  import getDrawDirection from './util/getDrawDirection';
16
18
  import getPointAtLength from './util/getPointAtLength';
17
- import pathLengthFactory from './util/pathLengthFactory';
19
+ import pathFactory from './util/pathFactory';
18
20
 
19
21
  import getPropertiesAtLength from './util/getPropertiesAtLength';
20
22
  import getPropertiesAtPoint from './util/getPropertiesAtPoint';
@@ -41,6 +43,7 @@ import reversePath from './process/reversePath';
41
43
  import normalizePath from './process/normalizePath';
42
44
  import transformPath from './process/transformPath';
43
45
  import splitCubic from './process/splitCubic';
46
+ import replaceArc from './process/replaceArc';
44
47
 
45
48
  import pathToAbsolute from './convert/pathToAbsolute';
46
49
  import pathToRelative from './convert/pathToRelative';
@@ -55,7 +58,7 @@ import pathToString from './convert/pathToString';
55
58
  *
56
59
  * @class
57
60
  * @author thednp <https://github.com/thednp/svg-path-commander>
58
- * @returns {SVGPathCommander} a new SVGPathCommander instance
61
+ * @returns a new SVGPathCommander instance
59
62
  */
60
63
  class SVGPathCommander {
61
64
  // bring main utilities to front
@@ -66,7 +69,7 @@ class SVGPathCommander {
66
69
  public static getTotalLength = getTotalLength;
67
70
  public static getDrawDirection = getDrawDirection;
68
71
  public static getPointAtLength = getPointAtLength;
69
- public static pathLengthFactory = pathLengthFactory;
72
+ public static pathFactory = pathFactory;
70
73
  public static getPropertiesAtLength = getPropertiesAtLength;
71
74
  public static getPropertiesAtPoint = getPropertiesAtPoint;
72
75
  public static polygonLength = polygonLength;
@@ -87,6 +90,7 @@ class SVGPathCommander {
87
90
  public static roundPath = roundPath;
88
91
  public static splitPath = splitPath;
89
92
  public static splitCubic = splitCubic;
93
+ public static replaceArc = replaceArc;
90
94
  public static optimizePath = optimizePath;
91
95
  public static reverseCurve = reverseCurve;
92
96
  public static reversePath = reversePath;
@@ -103,8 +107,8 @@ class SVGPathCommander {
103
107
 
104
108
  /**
105
109
  * @constructor
106
- * @param {string} pathValue the path string
107
- * @param {any} config instance options
110
+ * @param pathValue the path string
111
+ * @param config instance options
108
112
  */
109
113
  constructor(pathValue: string, config?: Partial<Options>) {
110
114
  const instanceOptions = config || {};
@@ -115,13 +119,8 @@ class SVGPathCommander {
115
119
  }
116
120
 
117
121
  const segments = parsePathString(pathValue);
118
- // if (typeof segments === 'string') {
119
- // throw TypeError(segments);
120
- // }
121
-
122
122
  this.segments = segments;
123
-
124
- const { width, height, cx, cy, cz } = this.getBBox();
123
+ const { width, height, cx, cy, cz } = this.bbox;
125
124
 
126
125
  // set instance options.round
127
126
  const { round: roundOption, origin: originOption } = instanceOptions;
@@ -138,7 +137,8 @@ class SVGPathCommander {
138
137
 
139
138
  // set instance options.origin
140
139
  // the SVGPathCommander class will always override the default origin
141
- let origin: [number, number, number];
140
+ let origin = [cx, cy, cz] as [number, number, number];
141
+ /* istanbul ignore else @preserve */
142
142
  if (Array.isArray(originOption) && originOption.length >= 2) {
143
143
  const [originX, originY, originZ] = originOption.map(Number);
144
144
  origin = [
@@ -146,8 +146,6 @@ class SVGPathCommander {
146
146
  !Number.isNaN(originY) ? originY : cy,
147
147
  !Number.isNaN(originZ) ? originZ : cz,
148
148
  ];
149
- } else {
150
- origin = [cx, cy, cz];
151
149
  }
152
150
 
153
151
  this.round = round;
@@ -155,6 +153,12 @@ class SVGPathCommander {
155
153
 
156
154
  return this;
157
155
  }
156
+ get bbox() {
157
+ return getPathBBox(this.segments);
158
+ }
159
+ get length() {
160
+ return getTotalLength(this.segments);
161
+ }
158
162
 
159
163
  /**
160
164
  * Returns the path bounding box, equivalent to native `path.getBBox()`.
@@ -162,8 +166,8 @@ class SVGPathCommander {
162
166
  * @public
163
167
  * @returns the pathBBox
164
168
  */
165
- getBBox(): PathBBox {
166
- return getPathBBox(this.segments);
169
+ getBBox() {
170
+ return this.bbox;
167
171
  }
168
172
 
169
173
  /**
@@ -173,7 +177,7 @@ class SVGPathCommander {
173
177
  * @returns the path total length
174
178
  */
175
179
  getTotalLength() {
176
- return getTotalLength(this.segments);
180
+ return this.length;
177
181
  }
178
182
 
179
183
  /**
@@ -184,7 +188,7 @@ class SVGPathCommander {
184
188
  * @param length the length
185
189
  * @returns the requested point
186
190
  */
187
- getPointAtLength(length: number): { x: number; y: number } {
191
+ getPointAtLength(length: number) {
188
192
  return getPointAtLength(this.segments, length);
189
193
  }
190
194
 
@@ -236,23 +240,22 @@ class SVGPathCommander {
236
240
  const subPath = split.length > 1 ? split : false;
237
241
 
238
242
  const absoluteMultiPath = subPath
239
- ? [...subPath].map((x, i) => {
243
+ ? subPath.map((x, i) => {
240
244
  if (onlySubpath) {
241
- // return i ? reversePath(x) : parsePathString(x);
242
- return i ? reversePath(x) : [...x];
245
+ return i ? reversePath(x) : x.slice(0);
243
246
  }
244
247
  return reversePath(x);
245
248
  })
246
- : [...segments];
249
+ : segments.slice(0);
247
250
 
248
- let path = [];
251
+ let path = [] as unknown as PathArray;
249
252
  if (subPath) {
250
- path = absoluteMultiPath.flat(1);
253
+ path = absoluteMultiPath.flat(1) as PathArray;
251
254
  } else {
252
255
  path = onlySubpath ? segments : reversePath(segments);
253
256
  }
254
257
 
255
- this.segments = [...path] as PathArray;
258
+ this.segments = path.slice(0) as PathArray;
256
259
  return this;
257
260
  }
258
261
 
@@ -306,6 +309,7 @@ class SVGPathCommander {
306
309
  } = this;
307
310
  const transform = {} as TransformObjectValues;
308
311
  for (const [k, v] of Object.entries(source) as TransformEntries) {
312
+ // istanbul ignore else @preserve
309
313
  if (k === 'skew' && Array.isArray(v)) {
310
314
  transform[k] = v.map(Number) as [number, number];
311
315
  } else if ((k === 'rotate' || k === 'translate' || k === 'origin' || k === 'scale') && Array.isArray(v)) {
package/src/interface.ts CHANGED
@@ -1,51 +1,51 @@
1
1
  import type { PathSegment } from './types';
2
2
 
3
- export interface SegmentProperties {
3
+ export type SegmentProperties = {
4
4
  segment: PathSegment;
5
5
  index: number;
6
6
  length: number;
7
7
  lengthAtSegment: number;
8
8
  [key: string]: any;
9
- }
9
+ };
10
10
 
11
- export interface PointProperties {
11
+ export type PointProperties = {
12
12
  closest: {
13
13
  x: number;
14
14
  y: number;
15
15
  };
16
16
  distance: number;
17
17
  segment?: SegmentProperties;
18
- }
18
+ };
19
19
 
20
- export interface LineAttr {
20
+ export type LineAttr = {
21
21
  type: 'line';
22
22
  x1: number;
23
23
  y1: number;
24
24
  x2: number;
25
25
  y2: number;
26
26
  [key: string]: string | number;
27
- }
28
- export interface PolyAttr {
27
+ };
28
+ export type PolyAttr = {
29
29
  type: 'polygon' | 'polyline';
30
30
  points: string;
31
31
  [key: string]: string | number;
32
- }
33
- export interface CircleAttr {
32
+ };
33
+ export type CircleAttr = {
34
34
  type: 'circle';
35
35
  cx: number;
36
36
  cy: number;
37
37
  r: number;
38
38
  [key: string]: string | number;
39
- }
40
- export interface EllipseAttr {
39
+ };
40
+ export type EllipseAttr = {
41
41
  type: 'ellipse';
42
42
  cx: number;
43
43
  cy: number;
44
44
  rx: number;
45
45
  ry?: number;
46
46
  [key: string]: string | number | undefined;
47
- }
48
- export interface RectAttr {
47
+ };
48
+ export type RectAttr = {
49
49
  type: 'rect';
50
50
  width: number;
51
51
  height: number;
@@ -54,14 +54,14 @@ export interface RectAttr {
54
54
  rx?: number;
55
55
  ry?: number;
56
56
  [key: string]: string | number | undefined;
57
- }
58
- export interface GlyphAttr {
57
+ };
58
+ export type GlyphAttr = {
59
59
  type: 'glyph';
60
60
  d: string;
61
61
  [key: string]: string | number;
62
- }
62
+ };
63
63
 
64
- export interface ShapeParams {
64
+ export type ShapeParams = {
65
65
  line: ['x1', 'y1', 'x2', 'y2'];
66
66
  circle: ['cx', 'cy', 'r'];
67
67
  ellipse: ['cx', 'cy', 'rx', 'ry'];
@@ -69,9 +69,9 @@ export interface ShapeParams {
69
69
  polygon: ['points'];
70
70
  polyline: ['points'];
71
71
  glyph: ['d'];
72
- }
72
+ };
73
73
 
74
- export interface PathBBox {
74
+ export type PathBBox = {
75
75
  width: number;
76
76
  height: number;
77
77
  x: number;
@@ -81,13 +81,13 @@ export interface PathBBox {
81
81
  cx: number;
82
82
  cy: number;
83
83
  cz: number;
84
- }
85
- export interface SegmentLimits {
84
+ };
85
+ export type SegmentLimits = {
86
86
  min: { x: number; y: number };
87
87
  max: { x: number; y: number };
88
- }
88
+ };
89
89
 
90
- export interface ParserParams {
90
+ export type ParserParams = {
91
91
  x1: number;
92
92
  y1: number;
93
93
  x2: number;
@@ -96,34 +96,34 @@ export interface ParserParams {
96
96
  y: number;
97
97
  qx: number | null;
98
98
  qy: number | null;
99
- }
99
+ };
100
100
 
101
- export interface LengthFactory {
101
+ export type LengthFactory = {
102
102
  length: number;
103
103
  point: { x: number; y: number };
104
104
  min: { x: number; y: number };
105
105
  max: { x: number; y: number };
106
- }
106
+ };
107
107
 
108
- export interface Options {
108
+ export type Options = {
109
109
  round: 'auto' | 'off' | number;
110
110
  origin: number[];
111
- }
111
+ };
112
112
 
113
- export interface PathTransform {
113
+ export type PathTransform = {
114
114
  s: PathSegment;
115
115
  c: string;
116
116
  x: number;
117
117
  y: number;
118
- }
118
+ };
119
119
 
120
- export interface TransformObject {
120
+ export type TransformObject = {
121
121
  translate: number | number[];
122
122
  rotate: number | number[];
123
123
  scale: number | number[];
124
124
  skew: number | number[];
125
125
  origin: number[];
126
- }
126
+ };
127
127
 
128
128
  export type TransformProps = keyof TransformObject;
129
129
  export type TransformEntries = [TransformProps, TransformObject[TransformProps]][];
@@ -0,0 +1,217 @@
1
+ import { default as getLineSegmentProperties } from './lineTools';
2
+ import type { Point } from '../types';
3
+
4
+ /**
5
+ * Returns the Arc segment length.
6
+ * @param rx radius along X axis
7
+ * @param ry radius along Y axis
8
+ * @param theta the angle in radians
9
+ * @returns the arc length
10
+ */
11
+ const ellipticalArcLength = (rx: number, ry: number, theta: number) => {
12
+ const halfTheta = theta / 2;
13
+ const sinHalfTheta = Math.sin(halfTheta);
14
+ const cosHalfTheta = Math.cos(halfTheta);
15
+ const term1 = rx ** 2 * sinHalfTheta ** 2;
16
+ const term2 = ry ** 2 * cosHalfTheta ** 2;
17
+ const arcLength = Math.sqrt(term1 + term2) * theta;
18
+ return Math.abs(arcLength);
19
+ };
20
+
21
+ /**
22
+ * Returns the most extreme points in an Arc segment.
23
+ * @param x Center X coordinate of the ellipse arc
24
+ * @param y Center Y coordinate of the ellipse arc
25
+ * @param rx Radius on the X axis of the ellipse
26
+ * @param ry Radius on the Y axis of the ellipse
27
+ * @param rotation The ellipse rotation angle in radians
28
+ * @param startAngle The ellipse start angle in radians
29
+ * @param endAngle The ellipse end angle in radians
30
+ * @see https://stackoverflow.com/questions/87734/how-do-you-calculate-the-axis-aligned-bounding-box-of-an-ellipse
31
+ */
32
+ const minmax = (
33
+ x: number,
34
+ y: number,
35
+ rx: number,
36
+ ry: number,
37
+ rotation: number,
38
+ startAngle: number,
39
+ endAngle: number,
40
+ ) => {
41
+ const { cos, sin, min, max } = Math;
42
+ const cosRotation = cos(rotation);
43
+ const sinRotation = sin(rotation);
44
+
45
+ // Rotate parametric equations
46
+ const xRotated = (t: number) => {
47
+ return x + rx * cos(t) * cosRotation - ry * sin(t) * sinRotation;
48
+ };
49
+ const yRotated = (t: number) => {
50
+ return y + ry * sin(t) * cosRotation + rx * cos(t) * sinRotation;
51
+ };
52
+
53
+ // Evaluate at start and end angles
54
+ const startX = xRotated(startAngle);
55
+ const startY = yRotated(startAngle);
56
+ const endX = xRotated(endAngle);
57
+ const endY = yRotated(endAngle);
58
+
59
+ // Find minimum and maximum x and y values
60
+ // Return AABB
61
+ return {
62
+ min: { x: min(startX, endX), y: min(startY, endY) },
63
+ max: { x: max(startX, endX), y: max(startY, endY) },
64
+ };
65
+ };
66
+
67
+ /**
68
+ * Returns the angle between two points.
69
+ * @param v0 starting point
70
+ * @param v1 ending point
71
+ * @returns the angle
72
+ */
73
+ const angleBetween = (v0: Point, v1: Point) => {
74
+ const { x: v0x, y: v0y } = v0;
75
+ const { x: v1x, y: v1y } = v1;
76
+ const p = v0x * v1x + v0y * v1y;
77
+ const n = Math.sqrt((v0x ** 2 + v0y ** 2) * (v1x ** 2 + v1y ** 2));
78
+ const sign = v0x * v1y - v0y * v1x < 0 ? -1 : 1;
79
+ const angle = sign * Math.acos(p / n);
80
+
81
+ return angle;
82
+ };
83
+
84
+ /**
85
+ * Returns properties for an Arc segment.
86
+ *
87
+ * @param x1 the starting point X
88
+ * @param y1 the starting point Y
89
+ * @param c1x the first control point X
90
+ * @param c1y the first control point Y
91
+ * @param c2x the second control point X
92
+ * @param c2y the second control point Y
93
+ * @param x2 the ending point X
94
+ * @param y2 the ending point Y
95
+ * @param distance a [0-1] ratio
96
+ * @returns properties specific to Arc segmentas well as the segment length, point at length and the bounding box
97
+ */
98
+ const getSegmentProperties = (
99
+ x1: number,
100
+ y1: number,
101
+ RX: number,
102
+ RY: number,
103
+ angle: number,
104
+ LAF: number,
105
+ SF: number,
106
+ x: number,
107
+ y: number,
108
+ distance?: number,
109
+ ) => {
110
+ const { abs, sin, cos, sqrt, PI } = Math;
111
+ let rx = abs(RX);
112
+ let ry = abs(RY);
113
+ const xRot = ((angle % 360) + 360) % 360;
114
+ const xRotRad = xRot * (PI / 180);
115
+
116
+ if (x1 === x && y1 === y) {
117
+ return {
118
+ point: { x, y },
119
+ length: 0,
120
+ bbox: { min: { x, y }, max: { x, y } },
121
+ };
122
+ }
123
+
124
+ if (rx === 0 || ry === 0) {
125
+ return getLineSegmentProperties(x1, y1, x, y, distance);
126
+ }
127
+
128
+ const dx = (x1 - x) / 2;
129
+ const dy = (y1 - y) / 2;
130
+
131
+ const transformedPoint = {
132
+ x: cos(xRotRad) * dx + sin(xRotRad) * dy,
133
+ y: -sin(xRotRad) * dx + cos(xRotRad) * dy,
134
+ };
135
+
136
+ const radiiCheck = transformedPoint.x ** 2 / rx ** 2 + transformedPoint.y ** 2 / ry ** 2;
137
+
138
+ if (radiiCheck > 1) {
139
+ rx *= sqrt(radiiCheck);
140
+ ry *= sqrt(radiiCheck);
141
+ }
142
+
143
+ const cSquareNumerator = rx ** 2 * ry ** 2 - rx ** 2 * transformedPoint.y ** 2 - ry ** 2 * transformedPoint.x ** 2;
144
+ const cSquareRootDenom = rx ** 2 * transformedPoint.y ** 2 + ry ** 2 * transformedPoint.x ** 2;
145
+
146
+ let cRadicand = cSquareNumerator / cSquareRootDenom;
147
+ cRadicand = cRadicand < 0 ? 0 : cRadicand;
148
+ const cCoef = (LAF !== SF ? 1 : -1) * sqrt(cRadicand);
149
+ const transformedCenter = {
150
+ x: cCoef * ((rx * transformedPoint.y) / ry),
151
+ y: cCoef * (-(ry * transformedPoint.x) / rx),
152
+ };
153
+
154
+ const center = {
155
+ x: cos(xRotRad) * transformedCenter.x - sin(xRotRad) * transformedCenter.y + (x1 + x) / 2,
156
+ y: sin(xRotRad) * transformedCenter.x + cos(xRotRad) * transformedCenter.y + (y1 + y) / 2,
157
+ };
158
+
159
+ const startVector = {
160
+ x: (transformedPoint.x - transformedCenter.x) / rx,
161
+ y: (transformedPoint.y - transformedCenter.y) / ry,
162
+ };
163
+
164
+ const startAngle = angleBetween({ x: 1, y: 0 }, startVector);
165
+
166
+ const endVector = {
167
+ x: (-transformedPoint.x - transformedCenter.x) / rx,
168
+ y: (-transformedPoint.y - transformedCenter.y) / ry,
169
+ };
170
+
171
+ let sweepAngle = angleBetween(startVector, endVector);
172
+ if (!SF && sweepAngle > 0) {
173
+ sweepAngle -= 2 * PI;
174
+ } else if (SF && sweepAngle < 0) {
175
+ sweepAngle += 2 * PI;
176
+ }
177
+ sweepAngle %= 2 * PI;
178
+
179
+ const alpha = startAngle + sweepAngle * (distance || 0);
180
+ const endAngle = startAngle + sweepAngle;
181
+ const ellipseComponentX = rx * cos(alpha);
182
+ const ellipseComponentY = ry * sin(alpha);
183
+
184
+ const point = {
185
+ x: cos(xRotRad) * ellipseComponentX - sin(xRotRad) * ellipseComponentY + center.x,
186
+ y: sin(xRotRad) * ellipseComponentX + cos(xRotRad) * ellipseComponentY + center.y,
187
+ };
188
+
189
+ // to be used later
190
+ // point.ellipticalArcStartAngle = startAngle;
191
+ // point.ellipticalArcEndAngle = startAngle + sweepAngle;
192
+ // point.ellipticalArcAngle = alpha;
193
+
194
+ // point.ellipticalArcCenter = center;
195
+ // point.resultantRx = rx;
196
+ // point.resultantRy = ry;
197
+ // point.length = ellipticalArcLength(rx, ry, sweepAngle);
198
+ // point.box = minmax(center.x, center.y, rx, ry, xRotRad, startAngle, startAngle + sweepAngle);
199
+
200
+ return {
201
+ point,
202
+ center,
203
+ angle: alpha,
204
+ startAngle,
205
+ endAngle,
206
+ radiusX: rx,
207
+ radiusY: ry,
208
+ get length() {
209
+ return ellipticalArcLength(rx, ry, sweepAngle);
210
+ },
211
+ get bbox() {
212
+ return minmax(center.x, center.y, rx, ry, xRotRad, startAngle, startAngle + sweepAngle);
213
+ },
214
+ };
215
+ };
216
+
217
+ export default getSegmentProperties;