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.
- package/.eslintrc.cjs +1 -0
- package/README.md +4 -4
- package/dist/svg-path-commander.cjs +1 -1
- package/dist/svg-path-commander.cjs.map +1 -1
- package/dist/svg-path-commander.d.ts +137 -30
- package/dist/svg-path-commander.js +1 -1
- package/dist/svg-path-commander.js.map +1 -1
- package/dist/svg-path-commander.mjs +868 -698
- package/dist/svg-path-commander.mjs.map +1 -1
- package/package.json +20 -22
- package/src/convert/pathToAbsolute.ts +1 -1
- package/src/convert/pathToCurve.ts +1 -1
- package/src/convert/pathToRelative.ts +1 -1
- package/src/index.ts +30 -26
- package/src/interface.ts +32 -32
- package/src/math/arcTools.ts +217 -0
- package/src/math/bezier.ts +261 -0
- package/src/math/cubicTools.ts +81 -0
- package/src/math/lineTools.ts +52 -0
- package/src/math/quadTools.ts +79 -0
- package/src/parser/isMoveCommand.ts +17 -0
- package/src/parser/parsePathString.ts +1 -1
- package/src/parser/scanSegment.ts +12 -3
- package/src/process/normalizePath.ts +1 -1
- package/src/process/replaceArc.ts +52 -0
- package/src/process/splitPath.ts +1 -1
- package/src/process/transformPath.ts +14 -34
- package/src/types.ts +5 -0
- package/src/util/distanceEpsilon.ts +3 -0
- package/src/util/getClosestPoint.ts +1 -1
- package/src/util/getPathBBox.ts +4 -3
- package/src/util/getPointAtLength.ts +3 -3
- package/src/util/getPropertiesAtLength.ts +2 -1
- package/src/util/getPropertiesAtPoint.ts +4 -1
- package/src/util/getTotalLength.ts +2 -2
- package/src/util/isPointInStroke.ts +2 -1
- package/src/util/pathFactory.ts +130 -0
- package/src/util/shapeToPathArray.ts +8 -4
- package/test/class.test.ts +501 -0
- package/test/fixtures/getMarkup.ts +17 -0
- package/{cypress → test}/fixtures/shapes.js +18 -18
- package/{cypress → test}/fixtures/simpleShapes.js +6 -6
- package/test/static.test.ts +304 -0
- package/tsconfig.json +9 -4
- package/{vite.config.ts → vite.config.mts} +10 -1
- package/vitest.config-ui.mts +26 -0
- package/vitest.config.mts +26 -0
- package/cypress/e2e/svg-path-commander.spec.ts +0 -868
- package/cypress/plugins/esbuild-istanbul.ts +0 -50
- package/cypress/plugins/tsCompile.ts +0 -34
- package/cypress/support/commands.ts +0 -37
- package/cypress/support/e2e.ts +0 -21
- package/cypress/test.html +0 -36
- package/src/util/pathLengthFactory.ts +0 -114
- package/src/util/segmentArcFactory.ts +0 -219
- package/src/util/segmentCubicFactory.ts +0 -114
- package/src/util/segmentLineFactory.ts +0 -45
- package/src/util/segmentQuadFactory.ts +0 -109
- /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
|
|
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
|
-
"
|
|
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.
|
|
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
|
-
"
|
|
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
|
-
"
|
|
58
|
-
"
|
|
59
|
-
"
|
|
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.
|
|
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
|
|
71
|
-
"
|
|
72
|
-
"
|
|
73
|
-
"
|
|
74
|
-
"coverage
|
|
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
|
-
"
|
|
79
|
-
"build
|
|
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
|
-
"
|
|
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
|
|
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
|
|
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
|
|
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,
|
|
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
|
|
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
|
|
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
|
|
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
|
|
107
|
-
* @param
|
|
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
|
|
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()
|
|
166
|
-
return
|
|
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
|
|
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)
|
|
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
|
-
?
|
|
243
|
+
? subPath.map((x, i) => {
|
|
240
244
|
if (onlySubpath) {
|
|
241
|
-
|
|
242
|
-
return i ? reversePath(x) : [...x];
|
|
245
|
+
return i ? reversePath(x) : x.slice(0);
|
|
243
246
|
}
|
|
244
247
|
return reversePath(x);
|
|
245
248
|
})
|
|
246
|
-
:
|
|
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 =
|
|
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
|
|
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
|
|
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
|
|
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
|
|
27
|
+
};
|
|
28
|
+
export type PolyAttr = {
|
|
29
29
|
type: 'polygon' | 'polyline';
|
|
30
30
|
points: string;
|
|
31
31
|
[key: string]: string | number;
|
|
32
|
-
}
|
|
33
|
-
export
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
108
|
+
export type Options = {
|
|
109
109
|
round: 'auto' | 'off' | number;
|
|
110
110
|
origin: number[];
|
|
111
|
-
}
|
|
111
|
+
};
|
|
112
112
|
|
|
113
|
-
export
|
|
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
|
|
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;
|