svg-path-commander 2.1.0 → 2.1.2
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/README.md +63 -7
- package/dist/svg-path-commander.cjs +1 -1
- package/dist/svg-path-commander.cjs.map +1 -1
- package/dist/svg-path-commander.d.ts +314 -40
- package/dist/svg-path-commander.js +1 -1
- package/dist/svg-path-commander.js.map +1 -1
- package/dist/svg-path-commander.mjs +1085 -1032
- package/dist/svg-path-commander.mjs.map +1 -1
- package/package.json +8 -8
- package/src/convert/pathToAbsolute.ts +5 -88
- package/src/convert/pathToCurve.ts +22 -28
- package/src/convert/pathToRelative.ts +4 -78
- package/src/convert/pathToString.ts +40 -7
- package/src/index.ts +145 -58
- package/src/interface.ts +3 -2
- package/src/math/arcTools.ts +259 -80
- package/src/math/bezier.ts +58 -58
- package/src/math/cubicTools.ts +68 -25
- package/src/math/distanceSquareRoot.ts +3 -1
- package/src/math/lineTools.ts +42 -25
- package/src/math/midPoint.ts +3 -1
- package/src/math/polygonTools.ts +48 -0
- package/src/math/quadTools.ts +46 -25
- package/src/math/rotateVector.ts +3 -2
- package/src/math/roundTo.ts +7 -0
- package/src/parser/finalizeSegment.ts +11 -7
- package/src/parser/parsePathString.ts +4 -5
- package/src/parser/pathParser.ts +1 -1
- package/src/process/absolutizeSegment.ts +63 -0
- package/src/process/arcToCubic.ts +2 -2
- package/src/process/iterate.ts +58 -0
- package/src/process/lineToCubic.ts +1 -1
- package/src/process/normalizePath.ts +17 -28
- package/src/process/normalizeSegment.ts +55 -18
- package/src/process/optimizePath.ts +40 -60
- package/src/process/projection2d.ts +4 -3
- package/src/process/relativizeSegment.ts +59 -0
- package/src/process/reverseCurve.ts +8 -5
- package/src/process/reversePath.ts +88 -75
- package/src/process/roundPath.ts +18 -14
- package/src/process/roundSegment.ts +9 -0
- package/src/process/segmentToCubic.ts +10 -8
- package/src/process/shortenSegment.ts +26 -34
- package/src/process/splitCubic.ts +10 -9
- package/src/process/splitPath.ts +38 -4
- package/src/process/transformPath.ts +75 -73
- package/src/types.ts +30 -0
- package/src/util/getPathArea.ts +3 -3
- package/src/util/getPathBBox.ts +69 -19
- package/src/util/getPointAtLength.ts +100 -4
- package/src/util/getPropertiesAtLength.ts +3 -4
- package/src/util/getPropertiesAtPoint.ts +5 -5
- package/src/util/getTotalLength.ts +56 -4
- package/test/class.test.ts +17 -14
- package/test/fixtures/shapes.js +26 -26
- package/test/fixtures/simpleShapes.js +18 -18
- package/test/static.test.ts +54 -28
- package/cypress.config.ts +0 -29
- package/src/math/polygonArea.ts +0 -27
- package/src/math/polygonLength.ts +0 -21
- package/src/process/fixArc.ts +0 -23
- package/src/process/replaceArc.ts +0 -52
- package/src/util/pathFactory.ts +0 -130
package/src/interface.ts
CHANGED
|
@@ -5,7 +5,8 @@ export type SegmentProperties = {
|
|
|
5
5
|
index: number;
|
|
6
6
|
length: number;
|
|
7
7
|
lengthAtSegment: number;
|
|
8
|
-
|
|
8
|
+
// point: Point;
|
|
9
|
+
// [key: string]: any;
|
|
9
10
|
};
|
|
10
11
|
|
|
11
12
|
export type PointProperties = {
|
|
@@ -106,7 +107,7 @@ export type LengthFactory = {
|
|
|
106
107
|
};
|
|
107
108
|
|
|
108
109
|
export type Options = {
|
|
109
|
-
round: '
|
|
110
|
+
round: 'off' | number;
|
|
110
111
|
origin: number[];
|
|
111
112
|
};
|
|
112
113
|
|
package/src/math/arcTools.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { getPointAtLineLength } from './lineTools';
|
|
2
2
|
import type { Point } from '../types';
|
|
3
3
|
|
|
4
4
|
/**
|
|
@@ -8,59 +8,38 @@ import type { Point } from '../types';
|
|
|
8
8
|
* @param theta the angle in radians
|
|
9
9
|
* @returns the arc length
|
|
10
10
|
*/
|
|
11
|
-
const
|
|
11
|
+
const arcLength = (rx: number, ry: number, theta: number) => {
|
|
12
12
|
const halfTheta = theta / 2;
|
|
13
13
|
const sinHalfTheta = Math.sin(halfTheta);
|
|
14
14
|
const cosHalfTheta = Math.cos(halfTheta);
|
|
15
15
|
const term1 = rx ** 2 * sinHalfTheta ** 2;
|
|
16
16
|
const term2 = ry ** 2 * cosHalfTheta ** 2;
|
|
17
|
-
const
|
|
18
|
-
return Math.abs(
|
|
17
|
+
const length = Math.sqrt(term1 + term2) * theta;
|
|
18
|
+
return Math.abs(length);
|
|
19
19
|
};
|
|
20
20
|
|
|
21
21
|
/**
|
|
22
|
-
*
|
|
23
|
-
* @param
|
|
24
|
-
* @param
|
|
25
|
-
* @param rx
|
|
26
|
-
* @param ry
|
|
27
|
-
* @param
|
|
28
|
-
* @param
|
|
29
|
-
* @
|
|
30
|
-
* @see https://stackoverflow.com/questions/87734/how-do-you-calculate-the-axis-aligned-bounding-box-of-an-ellipse
|
|
22
|
+
* Find point on ellipse at given angle around ellipse (theta);
|
|
23
|
+
* @param cx the center X
|
|
24
|
+
* @param cy the center Y
|
|
25
|
+
* @param rx the radius X
|
|
26
|
+
* @param ry the radius Y
|
|
27
|
+
* @param alpha the arc rotation angle in radians
|
|
28
|
+
* @param theta the arc sweep angle in radians
|
|
29
|
+
* @returns a point around ellipse at given angle
|
|
31
30
|
*/
|
|
32
|
-
const
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
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);
|
|
31
|
+
const arcPoint = (cx: number, cy: number, rx: number, ry: number, alpha: number, theta: number) => {
|
|
32
|
+
const { sin, cos } = Math;
|
|
33
|
+
// theta is angle in radians around arc
|
|
34
|
+
// alpha is angle of rotation of ellipse in radians
|
|
35
|
+
const cosA = cos(alpha);
|
|
36
|
+
const sinA = sin(alpha);
|
|
37
|
+
const x = rx * cos(theta);
|
|
38
|
+
const y = ry * sin(theta);
|
|
58
39
|
|
|
59
|
-
// Find minimum and maximum x and y values
|
|
60
|
-
// Return AABB
|
|
61
40
|
return {
|
|
62
|
-
|
|
63
|
-
|
|
41
|
+
x: cx + cosA * x - sinA * y,
|
|
42
|
+
y: cy + sinA * x + cosA * y,
|
|
64
43
|
};
|
|
65
44
|
};
|
|
66
45
|
|
|
@@ -68,7 +47,7 @@ const minmax = (
|
|
|
68
47
|
* Returns the angle between two points.
|
|
69
48
|
* @param v0 starting point
|
|
70
49
|
* @param v1 ending point
|
|
71
|
-
* @returns the angle
|
|
50
|
+
* @returns the angle in radian
|
|
72
51
|
*/
|
|
73
52
|
const angleBetween = (v0: Point, v1: Point) => {
|
|
74
53
|
const { x: v0x, y: v0y } = v0;
|
|
@@ -76,26 +55,25 @@ const angleBetween = (v0: Point, v1: Point) => {
|
|
|
76
55
|
const p = v0x * v1x + v0y * v1y;
|
|
77
56
|
const n = Math.sqrt((v0x ** 2 + v0y ** 2) * (v1x ** 2 + v1y ** 2));
|
|
78
57
|
const sign = v0x * v1y - v0y * v1x < 0 ? -1 : 1;
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
return angle;
|
|
58
|
+
return sign * Math.acos(p / n);
|
|
82
59
|
};
|
|
83
60
|
|
|
84
61
|
/**
|
|
85
|
-
* Returns properties for an Arc segment
|
|
62
|
+
* Returns the following properties for an Arc segment: center, start angle,
|
|
63
|
+
* end angle, and radiuses on X and Y axis.
|
|
86
64
|
*
|
|
87
65
|
* @param x1 the starting point X
|
|
88
66
|
* @param y1 the starting point Y
|
|
89
|
-
* @param
|
|
90
|
-
* @param
|
|
91
|
-
* @param
|
|
92
|
-
* @param
|
|
67
|
+
* @param RX the radius on X axis
|
|
68
|
+
* @param RY the radius on Y axis
|
|
69
|
+
* @param angle the ellipse rotation in degrees
|
|
70
|
+
* @param LAF the large arc flag
|
|
71
|
+
* @param SF the sweep flag
|
|
93
72
|
* @param x2 the ending point X
|
|
94
73
|
* @param y2 the ending point Y
|
|
95
|
-
* @
|
|
96
|
-
* @returns properties specific to Arc segmentas well as the segment length, point at length and the bounding box
|
|
74
|
+
* @returns properties specific to Arc segments
|
|
97
75
|
*/
|
|
98
|
-
const
|
|
76
|
+
const getArcProps = (
|
|
99
77
|
x1: number,
|
|
100
78
|
y1: number,
|
|
101
79
|
RX: number,
|
|
@@ -105,7 +83,6 @@ const getSegmentProperties = (
|
|
|
105
83
|
SF: number,
|
|
106
84
|
x: number,
|
|
107
85
|
y: number,
|
|
108
|
-
distance?: number,
|
|
109
86
|
) => {
|
|
110
87
|
const { abs, sin, cos, sqrt, PI } = Math;
|
|
111
88
|
let rx = abs(RX);
|
|
@@ -113,16 +90,25 @@ const getSegmentProperties = (
|
|
|
113
90
|
const xRot = ((angle % 360) + 360) % 360;
|
|
114
91
|
const xRotRad = xRot * (PI / 180);
|
|
115
92
|
|
|
93
|
+
// istanbul ignore next @preserve
|
|
116
94
|
if (x1 === x && y1 === y) {
|
|
117
95
|
return {
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
96
|
+
rx,
|
|
97
|
+
ry,
|
|
98
|
+
startAngle: 0,
|
|
99
|
+
endAngle: 0,
|
|
100
|
+
center: { x, y },
|
|
121
101
|
};
|
|
122
102
|
}
|
|
123
103
|
|
|
124
104
|
if (rx === 0 || ry === 0) {
|
|
125
|
-
return
|
|
105
|
+
return {
|
|
106
|
+
rx,
|
|
107
|
+
ry,
|
|
108
|
+
startAngle: 0,
|
|
109
|
+
endAngle: 0,
|
|
110
|
+
center: { x: (x + x1) / 2, y: (y + y1) / 2 },
|
|
111
|
+
};
|
|
126
112
|
}
|
|
127
113
|
|
|
128
114
|
const dx = (x1 - x) / 2;
|
|
@@ -144,6 +130,7 @@ const getSegmentProperties = (
|
|
|
144
130
|
const cSquareRootDenom = rx ** 2 * transformedPoint.y ** 2 + ry ** 2 * transformedPoint.x ** 2;
|
|
145
131
|
|
|
146
132
|
let cRadicand = cSquareNumerator / cSquareRootDenom;
|
|
133
|
+
/* istanbul ignore next @preserve */
|
|
147
134
|
cRadicand = cRadicand < 0 ? 0 : cRadicand;
|
|
148
135
|
const cCoef = (LAF !== SF ? 1 : -1) * sqrt(cRadicand);
|
|
149
136
|
const transformedCenter = {
|
|
@@ -176,17 +163,8 @@ const getSegmentProperties = (
|
|
|
176
163
|
}
|
|
177
164
|
sweepAngle %= 2 * PI;
|
|
178
165
|
|
|
179
|
-
const alpha = startAngle + sweepAngle * (distance || 0);
|
|
180
166
|
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
167
|
|
|
189
|
-
// to be used later
|
|
190
168
|
// point.ellipticalArcStartAngle = startAngle;
|
|
191
169
|
// point.ellipticalArcEndAngle = startAngle + sweepAngle;
|
|
192
170
|
// point.ellipticalArcAngle = alpha;
|
|
@@ -194,24 +172,225 @@ const getSegmentProperties = (
|
|
|
194
172
|
// point.ellipticalArcCenter = center;
|
|
195
173
|
// point.resultantRx = rx;
|
|
196
174
|
// 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
175
|
|
|
200
176
|
return {
|
|
201
|
-
point,
|
|
202
177
|
center,
|
|
203
|
-
angle: alpha,
|
|
204
178
|
startAngle,
|
|
205
179
|
endAngle,
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
180
|
+
rx,
|
|
181
|
+
ry,
|
|
182
|
+
};
|
|
183
|
+
};
|
|
184
|
+
|
|
185
|
+
/**
|
|
186
|
+
* Returns the length of an Arc segment.
|
|
187
|
+
*
|
|
188
|
+
* @param x1 the starting point X
|
|
189
|
+
* @param y1 the starting point Y
|
|
190
|
+
* @param c1x the first control point X
|
|
191
|
+
* @param c1y the first control point Y
|
|
192
|
+
* @param c2x the second control point X
|
|
193
|
+
* @param c2y the second control point Y
|
|
194
|
+
* @param x2 the ending point X
|
|
195
|
+
* @param y2 the ending point Y
|
|
196
|
+
* @returns the length of the Arc segment
|
|
197
|
+
*/
|
|
198
|
+
const getArcLength = (
|
|
199
|
+
x1: number,
|
|
200
|
+
y1: number,
|
|
201
|
+
RX: number,
|
|
202
|
+
RY: number,
|
|
203
|
+
angle: number,
|
|
204
|
+
LAF: number,
|
|
205
|
+
SF: number,
|
|
206
|
+
x: number,
|
|
207
|
+
y: number,
|
|
208
|
+
) => {
|
|
209
|
+
const { rx, ry, startAngle, endAngle } = getArcProps(x1, y1, RX, RY, angle, LAF, SF, x, y);
|
|
210
|
+
return arcLength(rx, ry, endAngle - startAngle);
|
|
211
|
+
};
|
|
212
|
+
|
|
213
|
+
/**
|
|
214
|
+
* Returns a point along an Arc segment at a given distance.
|
|
215
|
+
*
|
|
216
|
+
* @param x1 the starting point X
|
|
217
|
+
* @param y1 the starting point Y
|
|
218
|
+
* @param RX the radius on X axis
|
|
219
|
+
* @param RY the radius on Y axis
|
|
220
|
+
* @param angle the ellipse rotation in degrees
|
|
221
|
+
* @param LAF the large arc flag
|
|
222
|
+
* @param SF the sweep flag
|
|
223
|
+
* @param x2 the ending point X
|
|
224
|
+
* @param y2 the ending point Y
|
|
225
|
+
* @param distance a [0-1] ratio
|
|
226
|
+
* @returns a point along the Arc segment
|
|
227
|
+
*/
|
|
228
|
+
const getPointAtArcLength = (
|
|
229
|
+
x1: number,
|
|
230
|
+
y1: number,
|
|
231
|
+
RX: number,
|
|
232
|
+
RY: number,
|
|
233
|
+
angle: number,
|
|
234
|
+
LAF: number,
|
|
235
|
+
SF: number,
|
|
236
|
+
x: number,
|
|
237
|
+
y: number,
|
|
238
|
+
distance?: number,
|
|
239
|
+
) => {
|
|
240
|
+
let point = { x: x1, y: y1 };
|
|
241
|
+
const { center, rx, ry, startAngle, endAngle } = getArcProps(x1, y1, RX, RY, angle, LAF, SF, x, y);
|
|
242
|
+
|
|
243
|
+
/* istanbul ignore else @preserve */
|
|
244
|
+
if (typeof distance === 'number') {
|
|
245
|
+
const length = arcLength(rx, ry, endAngle - startAngle);
|
|
246
|
+
if (distance <= 0) {
|
|
247
|
+
point = { x: x1, y: y1 };
|
|
248
|
+
} else if (distance >= length) {
|
|
249
|
+
point = { x, y };
|
|
250
|
+
} else {
|
|
251
|
+
/* istanbul ignore next @preserve */
|
|
252
|
+
if (x1 === x && y1 === y) {
|
|
253
|
+
return { x, y };
|
|
254
|
+
}
|
|
255
|
+
/* istanbul ignore next @preserve */
|
|
256
|
+
if (rx === 0 || ry === 0) {
|
|
257
|
+
return getPointAtLineLength(x1, y1, x, y, distance);
|
|
258
|
+
}
|
|
259
|
+
const { PI, cos, sin } = Math;
|
|
260
|
+
const sweepAngle = endAngle - startAngle;
|
|
261
|
+
const xRot = ((angle % 360) + 360) % 360;
|
|
262
|
+
const xRotRad = xRot * (PI / 180);
|
|
263
|
+
const alpha = startAngle + sweepAngle * (distance / length);
|
|
264
|
+
const ellipseComponentX = rx * cos(alpha);
|
|
265
|
+
const ellipseComponentY = ry * sin(alpha);
|
|
266
|
+
|
|
267
|
+
point = {
|
|
268
|
+
x: cos(xRotRad) * ellipseComponentX - sin(xRotRad) * ellipseComponentY + center.x,
|
|
269
|
+
y: sin(xRotRad) * ellipseComponentX + cos(xRotRad) * ellipseComponentY + center.y,
|
|
270
|
+
};
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
return point;
|
|
275
|
+
};
|
|
276
|
+
|
|
277
|
+
/**
|
|
278
|
+
* Returns the extrema for an Arc segment.
|
|
279
|
+
* @see https://github.com/herrstrietzel/svg-pathdata-getbbox
|
|
280
|
+
*
|
|
281
|
+
* @param x1 the starting point X
|
|
282
|
+
* @param y1 the starting point Y
|
|
283
|
+
* @param RX the radius on X axis
|
|
284
|
+
* @param RY the radius on Y axis
|
|
285
|
+
* @param angle the ellipse rotation in degrees
|
|
286
|
+
* @param LAF the large arc flag
|
|
287
|
+
* @param SF the sweep flag
|
|
288
|
+
* @param x2 the ending point X
|
|
289
|
+
* @param y2 the ending point Y
|
|
290
|
+
* @returns the extrema of the Arc segment
|
|
291
|
+
*
|
|
292
|
+
*/
|
|
293
|
+
const getArcBBox = (
|
|
294
|
+
x1: number,
|
|
295
|
+
y1: number,
|
|
296
|
+
RX: number,
|
|
297
|
+
RY: number,
|
|
298
|
+
angle: number,
|
|
299
|
+
LAF: number,
|
|
300
|
+
SF: number,
|
|
301
|
+
x: number,
|
|
302
|
+
y: number,
|
|
303
|
+
) => {
|
|
304
|
+
const { center, rx, ry, startAngle, endAngle } = getArcProps(x1, y1, RX, RY, angle, LAF, SF, x, y);
|
|
305
|
+
const deltaAngle = endAngle - startAngle;
|
|
306
|
+
const { min, max, tan, atan2, PI } = Math;
|
|
307
|
+
|
|
308
|
+
// final on path point
|
|
309
|
+
const p = { x, y };
|
|
310
|
+
|
|
311
|
+
// circle/elipse center coordinates
|
|
312
|
+
const { x: cx, y: cy } = center;
|
|
313
|
+
|
|
314
|
+
// collect extreme points – add end point
|
|
315
|
+
const extremes = [p];
|
|
316
|
+
|
|
317
|
+
// rotation to radians
|
|
318
|
+
const alpha = (angle * PI) / 180;
|
|
319
|
+
const tangent = tan(alpha);
|
|
320
|
+
|
|
321
|
+
/**
|
|
322
|
+
* find min/max from zeroes of directional derivative along x and y
|
|
323
|
+
* along x axis
|
|
324
|
+
*/
|
|
325
|
+
const theta = atan2(-ry * tangent, rx);
|
|
326
|
+
const angle1 = theta;
|
|
327
|
+
const angle2 = theta + PI;
|
|
328
|
+
const angle3 = atan2(ry, rx * tangent);
|
|
329
|
+
const angle4 = angle3 + PI;
|
|
330
|
+
|
|
331
|
+
// inner bounding box
|
|
332
|
+
const xArr = [x1, x];
|
|
333
|
+
const yArr = [y1, y];
|
|
334
|
+
const xMin = min(...xArr);
|
|
335
|
+
const xMax = max(...xArr);
|
|
336
|
+
const yMin = min(...yArr);
|
|
337
|
+
const yMax = max(...yArr);
|
|
338
|
+
|
|
339
|
+
// on path point close after start
|
|
340
|
+
const angleAfterStart = endAngle - deltaAngle * 0.001;
|
|
341
|
+
const pP2 = arcPoint(cx, cy, rx, ry, alpha, angleAfterStart);
|
|
342
|
+
|
|
343
|
+
// on path point close before end
|
|
344
|
+
const angleBeforeEnd = endAngle - deltaAngle * 0.999;
|
|
345
|
+
const pP3 = arcPoint(cx, cy, rx, ry, alpha, angleBeforeEnd);
|
|
346
|
+
|
|
347
|
+
/**
|
|
348
|
+
* expected extremes
|
|
349
|
+
* if leaving inner bounding box
|
|
350
|
+
* (between segment start and end point)
|
|
351
|
+
* otherwise exclude elliptic extreme points
|
|
352
|
+
*/
|
|
353
|
+
|
|
354
|
+
// right
|
|
355
|
+
// istanbul ignore if @preserve
|
|
356
|
+
if (pP2.x > xMax || pP3.x > xMax) {
|
|
357
|
+
// get point for this theta
|
|
358
|
+
extremes.push(arcPoint(cx, cy, rx, ry, alpha, angle1));
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
// left
|
|
362
|
+
// istanbul ignore if @preserve
|
|
363
|
+
if (pP2.x < xMin || pP3.x < xMin) {
|
|
364
|
+
// get anti-symmetric point
|
|
365
|
+
extremes.push(arcPoint(cx, cy, rx, ry, alpha, angle2));
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
// top
|
|
369
|
+
// istanbul ignore if @preserve
|
|
370
|
+
if (pP2.y < yMin || pP3.y < yMin) {
|
|
371
|
+
// get anti-symmetric point
|
|
372
|
+
extremes.push(arcPoint(cx, cy, rx, ry, alpha, angle4));
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
// bottom
|
|
376
|
+
// istanbul ignore if @preserve
|
|
377
|
+
if (pP2.y > yMax || pP3.y > yMax) {
|
|
378
|
+
// get point for this theta
|
|
379
|
+
extremes.push(arcPoint(cx, cy, rx, ry, alpha, angle3));
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
return {
|
|
383
|
+
min: {
|
|
384
|
+
x: min(...extremes.map(n => n.x)),
|
|
385
|
+
y: min(...extremes.map(n => n.y)),
|
|
210
386
|
},
|
|
211
|
-
|
|
212
|
-
|
|
387
|
+
max: {
|
|
388
|
+
x: max(...extremes.map(n => n.x)),
|
|
389
|
+
y: max(...extremes.map(n => n.y)),
|
|
213
390
|
},
|
|
214
391
|
};
|
|
215
392
|
};
|
|
216
393
|
|
|
217
|
-
export
|
|
394
|
+
export { arcPoint, angleBetween, getArcLength, arcLength, getArcBBox, getArcProps, getPointAtArcLength };
|
|
395
|
+
|
|
396
|
+
export {};
|
package/src/math/bezier.ts
CHANGED
|
@@ -1,12 +1,20 @@
|
|
|
1
|
-
import
|
|
1
|
+
import type {
|
|
2
|
+
PointTuple,
|
|
3
|
+
DerivedPoint,
|
|
4
|
+
QuadPoints,
|
|
5
|
+
CubicPoints,
|
|
6
|
+
DerivedQuadPoints,
|
|
7
|
+
DerivedCubicPoints,
|
|
8
|
+
QuadCoordinates,
|
|
9
|
+
CubicCoordinates,
|
|
10
|
+
DeriveCallback,
|
|
11
|
+
} from '../types';
|
|
2
12
|
|
|
3
13
|
/**
|
|
4
14
|
* Tools from bezier.js by Mike 'Pomax' Kamermans
|
|
5
15
|
* @see https://github.com/Pomax/bezierjs
|
|
6
16
|
*/
|
|
7
17
|
|
|
8
|
-
const ZERO = { x: 0, y: 0 };
|
|
9
|
-
|
|
10
18
|
const Tvalues = [
|
|
11
19
|
-0.0640568928626056260850430826247450385909, 0.0640568928626056260850430826247450385909,
|
|
12
20
|
-0.1911188674736163091586398207570696318404, 0.1911188674736163091586398207570696318404,
|
|
@@ -37,31 +45,12 @@ const Cvalues = [
|
|
|
37
45
|
0.0123412297999871995468056670700372915759, 0.0123412297999871995468056670700372915759,
|
|
38
46
|
];
|
|
39
47
|
|
|
40
|
-
type DerivedPoint = Point & { t: number };
|
|
41
|
-
type QuadPoints = [Point, Point, Point, Point, Point, Point];
|
|
42
|
-
type CubicPoints = [Point, Point, Point, Point, Point, Point, Point, Point];
|
|
43
|
-
type DerivedQuadPoints = [DerivedPoint, DerivedPoint, DerivedPoint, DerivedPoint, DerivedPoint, DerivedPoint];
|
|
44
|
-
type DerivedCubicPoints = [
|
|
45
|
-
DerivedPoint,
|
|
46
|
-
DerivedPoint,
|
|
47
|
-
DerivedPoint,
|
|
48
|
-
DerivedPoint,
|
|
49
|
-
DerivedPoint,
|
|
50
|
-
DerivedPoint,
|
|
51
|
-
DerivedPoint,
|
|
52
|
-
DerivedPoint,
|
|
53
|
-
];
|
|
54
|
-
export type QuadCoordinates = [number, number, number, number, number, number];
|
|
55
|
-
export type CubicCoordinates = [number, number, number, number, number, number, number, number];
|
|
56
|
-
|
|
57
|
-
type DeriveCallback = (t: number) => Point;
|
|
58
|
-
|
|
59
48
|
/**
|
|
60
49
|
*
|
|
61
50
|
* @param points
|
|
62
51
|
* @returns
|
|
63
52
|
*/
|
|
64
|
-
const
|
|
53
|
+
const deriveBezier = (points: QuadPoints | CubicPoints) => {
|
|
65
54
|
const dpoints = [] as (DerivedCubicPoints | DerivedQuadPoints)[];
|
|
66
55
|
for (let p = points, d = p.length, c = d - 1; d > 1; d -= 1, c -= 1) {
|
|
67
56
|
const list = [] as unknown as DerivedCubicPoints | DerivedQuadPoints;
|
|
@@ -83,7 +72,7 @@ const derive = (points: QuadPoints | CubicPoints) => {
|
|
|
83
72
|
* @param points
|
|
84
73
|
* @param t
|
|
85
74
|
*/
|
|
86
|
-
const
|
|
75
|
+
const computeBezier = (points: DerivedQuadPoints | DerivedCubicPoints, t: number) => {
|
|
87
76
|
// shortcuts
|
|
88
77
|
/* istanbul ignore next @preserve */
|
|
89
78
|
if (t === 0) {
|
|
@@ -128,7 +117,7 @@ const compute = (points: DerivedQuadPoints | DerivedCubicPoints, t: number) => {
|
|
|
128
117
|
let d = 0;
|
|
129
118
|
/* istanbul ignore else @preserve */
|
|
130
119
|
if (order === 2) {
|
|
131
|
-
p = [p[0], p[1], p[2],
|
|
120
|
+
p = [p[0], p[1], p[2], { x: 0, y: 0 } as DerivedPoint];
|
|
132
121
|
a = mt2;
|
|
133
122
|
b = mt * t * 2;
|
|
134
123
|
c = t2;
|
|
@@ -145,14 +134,14 @@ const compute = (points: DerivedQuadPoints | DerivedCubicPoints, t: number) => {
|
|
|
145
134
|
};
|
|
146
135
|
};
|
|
147
136
|
|
|
148
|
-
const
|
|
137
|
+
const calculateBezier = (derivativeFn: DeriveCallback, t: number) => {
|
|
149
138
|
const d = derivativeFn(t);
|
|
150
139
|
const l = d.x * d.x + d.y * d.y;
|
|
151
140
|
|
|
152
141
|
return Math.sqrt(l);
|
|
153
142
|
};
|
|
154
143
|
|
|
155
|
-
const
|
|
144
|
+
const bezierLength = (derivativeFn: DeriveCallback) => {
|
|
156
145
|
const z = 0.5;
|
|
157
146
|
const len = Tvalues.length;
|
|
158
147
|
|
|
@@ -160,7 +149,7 @@ const lengthFn = (derivativeFn: DeriveCallback) => {
|
|
|
160
149
|
|
|
161
150
|
for (let i = 0, t; i < len; i++) {
|
|
162
151
|
t = z * Tvalues[i] + z;
|
|
163
|
-
sum += Cvalues[i] *
|
|
152
|
+
sum += Cvalues[i] * calculateBezier(derivativeFn, t);
|
|
164
153
|
}
|
|
165
154
|
return z * sum;
|
|
166
155
|
};
|
|
@@ -169,7 +158,7 @@ const lengthFn = (derivativeFn: DeriveCallback) => {
|
|
|
169
158
|
* Returns the length of CubicBezier / Quad segment.
|
|
170
159
|
* @param curve cubic / quad bezier segment
|
|
171
160
|
*/
|
|
172
|
-
|
|
161
|
+
const getBezierLength = (curve: CubicCoordinates | QuadCoordinates) => {
|
|
173
162
|
const points = [] as unknown as CubicPoints | QuadPoints;
|
|
174
163
|
for (let idx = 0, len = curve.length, step = 2; idx < len; idx += step) {
|
|
175
164
|
points.push({
|
|
@@ -177,9 +166,9 @@ export const length = (curve: CubicCoordinates | QuadCoordinates) => {
|
|
|
177
166
|
y: curve[idx + 1],
|
|
178
167
|
});
|
|
179
168
|
}
|
|
180
|
-
const dpoints =
|
|
181
|
-
return
|
|
182
|
-
return
|
|
169
|
+
const dpoints = deriveBezier(points);
|
|
170
|
+
return bezierLength((t: number) => {
|
|
171
|
+
return computeBezier(dpoints[0], t);
|
|
183
172
|
});
|
|
184
173
|
};
|
|
185
174
|
|
|
@@ -188,66 +177,64 @@ const CBEZIER_MINMAX_EPSILON = 0.00000001;
|
|
|
188
177
|
|
|
189
178
|
/**
|
|
190
179
|
* Returns the most extreme points in a Quad Bezier segment.
|
|
191
|
-
* @param A
|
|
180
|
+
* @param A an array which consist of X/Y values
|
|
192
181
|
*/
|
|
193
182
|
// https://github.com/kpym/SVGPathy/blob/acd1a50c626b36d81969f6e98e8602e128ba4302/lib/box.js#L89
|
|
194
|
-
|
|
195
|
-
const min = Math.min(
|
|
196
|
-
const max = Math.max(
|
|
183
|
+
const minmaxQ = ([v1, cp, v2]: [number, number, number]) => {
|
|
184
|
+
const min = Math.min(v1, v2);
|
|
185
|
+
const max = Math.max(v1, v2);
|
|
197
186
|
|
|
198
|
-
/* istanbul ignore
|
|
199
|
-
if (
|
|
187
|
+
/* istanbul ignore next @preserve */
|
|
188
|
+
if (cp >= v1 ? v2 >= cp : v2 <= cp) {
|
|
200
189
|
// if no extremum in ]0,1[
|
|
201
|
-
return [min, max] as
|
|
190
|
+
return [min, max] as PointTuple;
|
|
202
191
|
}
|
|
203
192
|
|
|
204
193
|
// check if the extremum E is min or max
|
|
205
|
-
const E = (
|
|
206
|
-
return (E < min ? [E, max] : [min, E]) as
|
|
194
|
+
const E = (v1 * v2 - cp * cp) / (v1 - 2 * cp + v2);
|
|
195
|
+
return (E < min ? [E, max] : [min, E]) as PointTuple;
|
|
207
196
|
};
|
|
208
197
|
|
|
209
198
|
/**
|
|
210
199
|
* Returns the most extreme points in a Cubic Bezier segment.
|
|
211
|
-
* @param A
|
|
200
|
+
* @param A an array which consist of X/Y values
|
|
212
201
|
* @see https://github.com/kpym/SVGPathy/blob/acd1a50c626b36d81969f6e98e8602e128ba4302/lib/box.js#L127
|
|
213
202
|
*/
|
|
214
|
-
|
|
215
|
-
const K =
|
|
203
|
+
const minmaxC = ([v1, cp1, cp2, v2]: [number, number, number, number]) => {
|
|
204
|
+
const K = v1 - 3 * cp1 + 3 * cp2 - v2;
|
|
216
205
|
|
|
217
206
|
// if the polynomial is (almost) quadratic and not cubic
|
|
218
|
-
/* istanbul ignore
|
|
207
|
+
/* istanbul ignore next @preserve */
|
|
219
208
|
if (Math.abs(K) < CBEZIER_MINMAX_EPSILON) {
|
|
220
|
-
if (
|
|
209
|
+
if (v1 === v2 && v1 === cp1) {
|
|
221
210
|
// no curve, point targeting same location
|
|
222
|
-
return [
|
|
211
|
+
return [v1, v2] as PointTuple;
|
|
223
212
|
}
|
|
224
213
|
|
|
225
|
-
return minmaxQ([
|
|
214
|
+
return minmaxQ([v1, -0.5 * v1 + 1.5 * cp1, v1 - 3 * cp1 + 3 * cp2]);
|
|
226
215
|
}
|
|
227
216
|
|
|
228
217
|
// the reduced discriminant of the derivative
|
|
229
|
-
const T = -
|
|
218
|
+
const T = -v1 * cp2 + v1 * v2 - cp1 * cp2 - cp1 * v2 + cp1 * cp1 + cp2 * cp2;
|
|
230
219
|
|
|
231
220
|
// if the polynomial is monotone in [0,1]
|
|
232
221
|
if (T <= 0) {
|
|
233
|
-
return [Math.min(
|
|
222
|
+
return [Math.min(v1, v2), Math.max(v1, v2)] as PointTuple;
|
|
234
223
|
}
|
|
235
224
|
const S = Math.sqrt(T);
|
|
236
225
|
|
|
237
226
|
// potential extrema
|
|
238
|
-
let min = Math.min(
|
|
239
|
-
let max = Math.max(
|
|
227
|
+
let min = Math.min(v1, v2);
|
|
228
|
+
let max = Math.max(v1, v2);
|
|
240
229
|
|
|
241
|
-
const L =
|
|
230
|
+
const L = v1 - 2 * cp1 + cp2;
|
|
242
231
|
// check local extrema
|
|
243
232
|
for (let R = (L + S) / K, i = 1; i <= 2; R = (L - S) / K, i++) {
|
|
233
|
+
// istanbul ignore next @preserve
|
|
244
234
|
if (R > 0 && R < 1) {
|
|
245
235
|
// if the extrema is for R in [0,1]
|
|
246
236
|
const Q =
|
|
247
|
-
|
|
248
|
-
A[1] * 3 * (1 - R) * (1 - R) * R +
|
|
249
|
-
A[2] * 3 * (1 - R) * R * R +
|
|
250
|
-
A[3] * R * R * R;
|
|
237
|
+
v1 * (1 - R) * (1 - R) * (1 - R) + cp1 * 3 * (1 - R) * (1 - R) * R + cp2 * 3 * (1 - R) * R * R + v2 * R * R * R;
|
|
251
238
|
if (Q < min) {
|
|
252
239
|
min = Q;
|
|
253
240
|
}
|
|
@@ -257,5 +244,18 @@ export const minmaxC = (A: [number, number, number, number]) => {
|
|
|
257
244
|
}
|
|
258
245
|
}
|
|
259
246
|
|
|
260
|
-
return [min, max] as
|
|
247
|
+
return [min, max] as PointTuple;
|
|
248
|
+
};
|
|
249
|
+
|
|
250
|
+
export {
|
|
251
|
+
Cvalues,
|
|
252
|
+
Tvalues,
|
|
253
|
+
minmaxC,
|
|
254
|
+
minmaxQ,
|
|
255
|
+
getBezierLength,
|
|
256
|
+
bezierLength,
|
|
257
|
+
calculateBezier,
|
|
258
|
+
computeBezier,
|
|
259
|
+
deriveBezier,
|
|
260
|
+
CBEZIER_MINMAX_EPSILON,
|
|
261
261
|
};
|