@ue-too/curve 0.5.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/index.js ADDED
@@ -0,0 +1,1223 @@
1
+ import { PointCal } from '@ue-too/math';
2
+
3
+ const T = [
4
+ -0.06405689286260563,
5
+ 0.0640568928626056260850430826247450385909,
6
+ -0.1911188674736163,
7
+ 0.1911188674736163091586398207570696318404,
8
+ -0.3150426796961634,
9
+ 0.3150426796961633743867932913198102407864,
10
+ -0.4337935076260451,
11
+ 0.4337935076260451384870842319133497124524,
12
+ -0.5454214713888396,
13
+ 0.5454214713888395356583756172183723700107,
14
+ -0.6480936519369755,
15
+ 0.6480936519369755692524957869107476266696,
16
+ -0.7401241915785544,
17
+ 0.7401241915785543642438281030999784255232,
18
+ -0.820001985973903,
19
+ 0.8200019859739029219539498726697452080761,
20
+ -0.8864155270044011,
21
+ 0.8864155270044010342131543419821967550873,
22
+ -0.9382745520027328,
23
+ 0.9382745520027327585236490017087214496548,
24
+ -0.9747285559713095,
25
+ 0.9747285559713094981983919930081690617411,
26
+ -0.9951872199970213,
27
+ 0.9951872199970213601799974097007368118745,
28
+ ];
29
+ const C = [
30
+ 0.1279381953467521569740561652246953718517,
31
+ 0.1279381953467521569740561652246953718517,
32
+ 0.1258374563468282961213753825111836887264,
33
+ 0.1258374563468282961213753825111836887264,
34
+ 0.121670472927803391204463153476262425607,
35
+ 0.121670472927803391204463153476262425607,
36
+ 0.1155056680537256013533444839067835598622,
37
+ 0.1155056680537256013533444839067835598622,
38
+ 0.1074442701159656347825773424466062227946,
39
+ 0.1074442701159656347825773424466062227946,
40
+ 0.0976186521041138882698806644642471544279,
41
+ 0.0976186521041138882698806644642471544279,
42
+ 0.086190161531953275917185202983742667185,
43
+ 0.086190161531953275917185202983742667185,
44
+ 0.0733464814110803057340336152531165181193,
45
+ 0.0733464814110803057340336152531165181193,
46
+ 0.0592985849154367807463677585001085845412,
47
+ 0.0592985849154367807463677585001085845412,
48
+ 0.0442774388174198061686027482113382288593,
49
+ 0.0442774388174198061686027482113382288593,
50
+ 0.0285313886289336631813078159518782864491,
51
+ 0.0285313886289336631813078159518782864491,
52
+ 0.0123412297999871995468056670700372915759,
53
+ 0.0123412297999871995468056670700372915759,
54
+ ];
55
+ class BCurve {
56
+ constructor(controlPoints) {
57
+ this.dControlPoints = [];
58
+ this.arcLengthLUT = { controlPoints: [], arcLengthLUT: [] };
59
+ this.controlPoints = controlPoints;
60
+ this.dControlPoints = this.getDerivativeControlPoints(this.controlPoints);
61
+ this._fullLength = this.calculateFullLength();
62
+ this.arcLengthLUT = this.getArcLengthLUT(1000);
63
+ }
64
+ getPointbyPercentage(percentage) {
65
+ // this leaves room for optimization
66
+ let controlPointsChangedSinceLastArcLengthLUT = this.arcLengthLUT.controlPoints.length != this.controlPoints.length;
67
+ controlPointsChangedSinceLastArcLengthLUT = controlPointsChangedSinceLastArcLengthLUT || this.arcLengthLUT.controlPoints.reduce((prevVal, curVal, index) => {
68
+ return prevVal || !PointCal.isEqual(curVal, this.controlPoints[index]);
69
+ }, false);
70
+ let points = [];
71
+ if (controlPointsChangedSinceLastArcLengthLUT) {
72
+ this.arcLengthLUT = this.getArcLengthLUT(1000);
73
+ }
74
+ points = [...this.arcLengthLUT.arcLengthLUT.map((item) => item.length)];
75
+ let targetLength = percentage * this.fullLength;
76
+ let low = 0;
77
+ let high = points.length - 1;
78
+ while (low <= high) {
79
+ let mid = Math.floor((low + high) / 2);
80
+ if (points[mid] == targetLength) {
81
+ return this.get((mid + 1) / points.length);
82
+ }
83
+ else if (points[mid] < targetLength) {
84
+ low = mid + 1;
85
+ }
86
+ else {
87
+ high = mid - 1;
88
+ }
89
+ }
90
+ return low >= points.length ? this.get(1) : this.get((low + 1) / points.length);
91
+ }
92
+ getDerivativeControlPoints(controlPoints) {
93
+ const derivativeControlPoints = [];
94
+ for (let index = 1; index < controlPoints.length; index++) {
95
+ derivativeControlPoints.push(PointCal.multiplyVectorByScalar(PointCal.subVector(controlPoints[index], controlPoints[index - 1]), controlPoints.length - 1));
96
+ }
97
+ return derivativeControlPoints;
98
+ }
99
+ validateTVal(tVal) {
100
+ if (tVal > 1 || tVal < 0) {
101
+ throw new TValOutofBoundError("tVal is greater than 1 or less than 0");
102
+ }
103
+ }
104
+ getControlPoints() {
105
+ return this.controlPoints;
106
+ }
107
+ setControlPointAtIndex(index, newPoint) {
108
+ if (index < 0 || index >= this.controlPoints.length) {
109
+ return false;
110
+ }
111
+ this.controlPoints[index] = newPoint;
112
+ this.dControlPoints = this.getDerivativeControlPoints(this.controlPoints);
113
+ this._fullLength = this.calculateFullLength();
114
+ return true;
115
+ }
116
+ compute(tVal) {
117
+ this.validateTVal(tVal);
118
+ let points = this.controlPoints;
119
+ while (points.length > 1) {
120
+ let lowerLevelPoints = points.slice(1);
121
+ for (let index = 0; index < lowerLevelPoints.length; index++) {
122
+ lowerLevelPoints[index] = PointCal.addVector(PointCal.multiplyVectorByScalar(points[index], (1 - tVal)), PointCal.multiplyVectorByScalar(points[index + 1], tVal));
123
+ }
124
+ points = lowerLevelPoints;
125
+ }
126
+ return points[0];
127
+ }
128
+ get(tVal) {
129
+ this.validateTVal(tVal);
130
+ if (this.controlPoints.length == 3) {
131
+ let firstTerm = PointCal.multiplyVectorByScalar(this.controlPoints[0], (1 - tVal) * (1 - tVal));
132
+ let secondTerm = PointCal.multiplyVectorByScalar(this.controlPoints[1], 2 * (1 - tVal) * tVal);
133
+ let thirdTerm = PointCal.multiplyVectorByScalar(this.controlPoints[2], tVal * tVal);
134
+ let res = PointCal.addVector(PointCal.addVector(firstTerm, secondTerm), thirdTerm);
135
+ return res;
136
+ }
137
+ if (this.controlPoints.length == 4) {
138
+ let firstTerm = PointCal.multiplyVectorByScalar(this.controlPoints[0], (1 - tVal) * (1 - tVal) * (1 - tVal));
139
+ let secondTerm = PointCal.multiplyVectorByScalar(this.controlPoints[1], 3 * (1 - tVal) * (1 - tVal) * tVal);
140
+ let thirdTerm = PointCal.multiplyVectorByScalar(this.controlPoints[2], 3 * (1 - tVal) * tVal * tVal);
141
+ let forthTerm = PointCal.multiplyVectorByScalar(this.controlPoints[3], tVal * tVal * tVal);
142
+ let res = PointCal.addVector(PointCal.addVector(firstTerm, secondTerm), PointCal.addVector(thirdTerm, forthTerm));
143
+ return res;
144
+ }
145
+ return this.compute(tVal);
146
+ }
147
+ getLUT(steps = 100) {
148
+ const stepSpan = 1 / steps;
149
+ const res = [];
150
+ let tVal = 0;
151
+ res.push(this.get(tVal));
152
+ for (let index = 0; index < steps; index += 1) {
153
+ tVal += stepSpan;
154
+ if ((tVal > 1 && tVal - stepSpan < 1) || index == steps - 1) {
155
+ tVal = 1;
156
+ }
157
+ res.push(this.get(tVal));
158
+ }
159
+ return res;
160
+ }
161
+ getLUTWithTVal(steps) {
162
+ if (steps == undefined) {
163
+ steps = 100;
164
+ }
165
+ const stepSpan = 1 / steps;
166
+ const res = [];
167
+ let tVal = 0;
168
+ res.push({ point: this.get(tVal), tVal: tVal });
169
+ for (let index = 0; index < steps; index += 1) {
170
+ tVal += stepSpan;
171
+ if ((tVal > 1 && tVal - stepSpan < 1) || index == steps - 1) {
172
+ tVal = 1;
173
+ }
174
+ res.push({ point: this.get(tVal), tVal: tVal });
175
+ }
176
+ return res;
177
+ }
178
+ get fullLength() {
179
+ return this._fullLength;
180
+ }
181
+ calculateFullLength() {
182
+ return this.lengthAtT(1);
183
+ }
184
+ lengthAtT(tVal) {
185
+ this.validateTVal(tVal);
186
+ const z = tVal / 2, len = T.length;
187
+ let sum = 0;
188
+ for (let i = 0, t; i < len; i++) {
189
+ t = z * T[i] + z;
190
+ sum += C[i] * PointCal.magnitude(this.derivative(t));
191
+ }
192
+ return z * sum;
193
+ }
194
+ derivative(tVal) {
195
+ return computeWithControlPoints(tVal, this.dControlPoints);
196
+ }
197
+ derivativeNormalized(tVal) {
198
+ return PointCal.unitVector(computeWithControlPoints(tVal, this.dControlPoints));
199
+ }
200
+ getArcLengthLUT(steps = 100) {
201
+ let res = [];
202
+ let tSteps = 1 / steps;
203
+ for (let tVal = 0; tVal <= 1; tVal += tSteps) {
204
+ res.push({ tVal: tVal, length: this.lengthAtT(tVal) });
205
+ }
206
+ return { controlPoints: this.controlPoints, arcLengthLUT: res };
207
+ }
208
+ split(tVal, tVal2) {
209
+ this.validateTVal(tVal);
210
+ if (this.controlPoints.length == 3) {
211
+ let newControlPoint1 = this.controlPoints[0];
212
+ let newControlPoint2 = PointCal.subVector(PointCal.multiplyVectorByScalar(this.controlPoints[1], tVal), PointCal.multiplyVectorByScalar(this.controlPoints[0], tVal - 1));
213
+ let newControlPoint3 = PointCal.subVector(PointCal.multiplyVectorByScalar(this.controlPoints[2], tVal * tVal), PointCal.multiplyVectorByScalar(this.controlPoints[1], 2 * tVal * (tVal - 1)));
214
+ newControlPoint3 = PointCal.addVector(newControlPoint3, PointCal.multiplyVectorByScalar(this.controlPoints[0], (tVal - 1) * (tVal - 1)));
215
+ let newControlPoint4 = PointCal.subVector(PointCal.multiplyVectorByScalar(this.controlPoints[2], tVal), PointCal.multiplyVectorByScalar(this.controlPoints[1], tVal - 1));
216
+ let newControlPoint5 = this.controlPoints[2];
217
+ return [[newControlPoint1, newControlPoint2, newControlPoint3], [newControlPoint3, newControlPoint4, newControlPoint5]];
218
+ }
219
+ let newControlPoint1 = this.controlPoints[0];
220
+ let newControlPoint2 = PointCal.subVector(PointCal.multiplyVectorByScalar(this.controlPoints[1], tVal), PointCal.multiplyVectorByScalar(this.controlPoints[0], (tVal - 1)));
221
+ let newControlPoint3 = PointCal.addVector(PointCal.multiplyVectorByScalar(this.controlPoints[2], tVal * tVal), PointCal.addVector(PointCal.multiplyVectorByScalar(this.controlPoints[1], -(2 * tVal * (tVal - 1))), PointCal.multiplyVectorByScalar(this.controlPoints[0], (tVal - 1) * (tVal - 1))));
222
+ let term1 = PointCal.multiplyVectorByScalar(this.controlPoints[3], tVal * tVal * tVal);
223
+ let term2 = PointCal.multiplyVectorByScalar(this.controlPoints[2], -(3 * tVal * tVal * (tVal - 1)));
224
+ let term3 = PointCal.multiplyVectorByScalar(this.controlPoints[1], 3 * tVal * (tVal - 1) * (tVal - 1));
225
+ let term4 = PointCal.multiplyVectorByScalar(this.controlPoints[0], -((tVal - 1) * (tVal - 1) * (tVal - 1)));
226
+ let newControlPoint4 = PointCal.addVector(term4, PointCal.addVector(term3, PointCal.addVector(term1, term2)));
227
+ let newControlPoint5 = PointCal.addVector(PointCal.addVector(PointCal.multiplyVectorByScalar(this.controlPoints[3], tVal * tVal), PointCal.multiplyVectorByScalar(this.controlPoints[2], -(2 * tVal * (tVal - 1)))), PointCal.multiplyVectorByScalar(this.controlPoints[1], (tVal - 1) * (tVal - 1)));
228
+ let newControlPoint6 = PointCal.addVector(PointCal.multiplyVectorByScalar(this.controlPoints[3], tVal), PointCal.multiplyVectorByScalar(this.controlPoints[2], -(tVal - 1)));
229
+ let newControlPoint7 = this.controlPoints[3];
230
+ return [[newControlPoint1, newControlPoint2, newControlPoint3, newControlPoint4], [newControlPoint4, newControlPoint5, newControlPoint6, newControlPoint7]];
231
+ }
232
+ getProjection(point) {
233
+ const threshold = 0.00001;
234
+ let distance = Number.MAX_VALUE;
235
+ let preliminaryProjectionTVal = 0;
236
+ let preliminaryProjectionPoint = this.get(0);
237
+ let preliminaryProjectionIndex = 0;
238
+ const LUT = this.getLUTWithTVal(500);
239
+ LUT.forEach((curvePoint, index) => {
240
+ const curDistance = PointCal.distanceBetweenPoints(curvePoint.point, point);
241
+ if (curDistance < distance) {
242
+ distance = curDistance;
243
+ preliminaryProjectionPoint = { ...curvePoint.point };
244
+ preliminaryProjectionTVal = curvePoint.tVal;
245
+ preliminaryProjectionIndex = index;
246
+ }
247
+ });
248
+ // console.log(preliminaryProjectionIndex, preliminaryProjectionPoint, preliminaryProjectionTVal);
249
+ let low = LUT[preliminaryProjectionIndex].tVal;
250
+ let high = LUT[preliminaryProjectionIndex].tVal;
251
+ if (preliminaryProjectionIndex < LUT.length - 1) {
252
+ high = LUT[preliminaryProjectionIndex + 1].tVal;
253
+ }
254
+ if (preliminaryProjectionIndex > 0) {
255
+ low = LUT[preliminaryProjectionIndex - 1].tVal;
256
+ }
257
+ while (low < high && high - low > threshold) {
258
+ let mid = low + (high - low) / 2;
259
+ let halfSpan = mid - low;
260
+ let lowMidMid = mid + halfSpan / 2;
261
+ let highMidMid = mid + halfSpan / 2;
262
+ let prevDist = distance;
263
+ if (lowMidMid <= 1 && lowMidMid >= 0) {
264
+ let curDist = PointCal.distanceBetweenPoints(this.get(lowMidMid), point);
265
+ if (curDist < distance) {
266
+ distance = curDist;
267
+ preliminaryProjectionPoint = this.get(lowMidMid);
268
+ preliminaryProjectionTVal = lowMidMid;
269
+ high = lowMidMid + halfSpan / 2;
270
+ low = lowMidMid - halfSpan / 2;
271
+ }
272
+ }
273
+ if (highMidMid <= 1 && highMidMid >= 0) {
274
+ let curDist = PointCal.distanceBetweenPoints(this.get(highMidMid), point);
275
+ if (curDist < distance) {
276
+ distance = curDist;
277
+ preliminaryProjectionPoint = this.get(highMidMid);
278
+ preliminaryProjectionTVal = highMidMid;
279
+ high = highMidMid + halfSpan / 2;
280
+ low = highMidMid - halfSpan / 2;
281
+ }
282
+ }
283
+ if (prevDist == distance) {
284
+ break;
285
+ }
286
+ }
287
+ return { projection: preliminaryProjectionPoint, tVal: preliminaryProjectionTVal };
288
+ }
289
+ findArcs(errorThreshold) {
290
+ let low = 0;
291
+ const res = [];
292
+ while (low < 1) {
293
+ let loopRes = this.findArcStartingAt(errorThreshold, low);
294
+ if (loopRes == null || loopRes.arc == undefined) {
295
+ break;
296
+ }
297
+ res.push(loopRes.arc);
298
+ low = loopRes.arc.endT;
299
+ if (low >= 1) {
300
+ break;
301
+ }
302
+ }
303
+ return res;
304
+ }
305
+ findArcStartingAt(errorThreshold, low) {
306
+ let high = 1;
307
+ let mid = low + (high - low) / 2;
308
+ let prevArc = { good: false };
309
+ while (true) {
310
+ mid = low + (high - low) / 2;
311
+ if (high > 1 || mid > 1) {
312
+ if (prevArc.good) {
313
+ return prevArc;
314
+ }
315
+ else {
316
+ return null;
317
+ }
318
+ }
319
+ const lowPoint = this.get(low);
320
+ const highPoint = this.get(high);
321
+ const midPoint = this.get(mid);
322
+ const fitArcRes = this.fitArc(lowPoint, highPoint, midPoint);
323
+ if (!fitArcRes.exists || fitArcRes.center == null || fitArcRes.radius == null) {
324
+ return null;
325
+ }
326
+ const n = high - mid;
327
+ const e1 = mid - n / 2;
328
+ const e2 = mid + n / 2;
329
+ const checkPoint1 = this.get(e1);
330
+ const checkPoint2 = this.get(e2);
331
+ const checkRadius = PointCal.distanceBetweenPoints(checkPoint1, fitArcRes.center);
332
+ const checkRadius2 = PointCal.distanceBetweenPoints(checkPoint2, fitArcRes.center);
333
+ if (Math.abs(checkRadius - fitArcRes.radius) > errorThreshold || Math.abs(checkRadius2 - fitArcRes.radius) > errorThreshold) {
334
+ // arc is bad
335
+ if (prevArc.good == true) {
336
+ return prevArc;
337
+ }
338
+ prevArc.good = false;
339
+ high = mid;
340
+ }
341
+ else {
342
+ prevArc.good = true;
343
+ if (fitArcRes.startPoint !== undefined && fitArcRes.endPoint !== undefined) {
344
+ prevArc.arc = { center: fitArcRes.center, radius: fitArcRes.radius, startPoint: fitArcRes.startPoint, endPoint: fitArcRes.endPoint, startT: low, endT: high };
345
+ }
346
+ high = high + (mid - low);
347
+ }
348
+ }
349
+ }
350
+ fitArc(startPoint, endPoint, midPoint) {
351
+ const M11 = [[startPoint.x, startPoint.y, 1], [midPoint.x, midPoint.y, 1], [endPoint.x, endPoint.y, 1]];
352
+ if (this.determinant3by3(M11) == 0) {
353
+ // three points lie on a line no circle
354
+ return { exists: false };
355
+ }
356
+ const M12 = [[startPoint.x * startPoint.x + startPoint.y * startPoint.y, startPoint.y, 1],
357
+ [midPoint.x * midPoint.x + midPoint.y * midPoint.y, midPoint.y, 1],
358
+ [endPoint.x * endPoint.x + endPoint.y * endPoint.y, endPoint.y, 1]];
359
+ const M13 = [[startPoint.x * startPoint.x + startPoint.y * startPoint.y, startPoint.x, 1],
360
+ [midPoint.x * midPoint.x + midPoint.y * midPoint.y, midPoint.x, 1],
361
+ [endPoint.x * endPoint.x + endPoint.y * endPoint.y, endPoint.x, 1]];
362
+ const M14 = [[startPoint.x * startPoint.x + startPoint.y * startPoint.y, startPoint.x, startPoint.y],
363
+ [midPoint.x * midPoint.x + midPoint.y * midPoint.y, midPoint.x, midPoint.y],
364
+ [endPoint.x * endPoint.x + endPoint.y * endPoint.y, endPoint.x, endPoint.y]];
365
+ const centerX = (1 / 2) * (this.determinant3by3(M12) / this.determinant3by3(M11));
366
+ const centerY = (-1 / 2) * (this.determinant3by3(M13) / this.determinant3by3(M11));
367
+ const radius = Math.sqrt(centerX * centerX + centerY * centerY + (this.determinant3by3(M14) / this.determinant3by3(M11)));
368
+ return { exists: true, center: { x: centerX, y: centerY }, radius: radius, startPoint: startPoint, endPoint: endPoint };
369
+ }
370
+ determinant3by3(matrix) {
371
+ const a = matrix[0][0];
372
+ const b = matrix[0][1];
373
+ const c = matrix[0][2];
374
+ const d = matrix[1][0];
375
+ const e = matrix[1][1];
376
+ const f = matrix[1][2];
377
+ const g = matrix[2][0];
378
+ const h = matrix[2][1];
379
+ const i = matrix[2][2];
380
+ return a * (e * i - f * h) - b * (d * i - g * f) + c * (d * h - e * g);
381
+ }
382
+ curvature(tVal) {
383
+ const derivative = computeWithControlPoints(tVal, this.dControlPoints);
384
+ const secondDerivative = computeWithControlPoints(tVal, this.getDerivativeControlPoints(this.dControlPoints));
385
+ const numerator = derivative.x * secondDerivative.y - secondDerivative.x * derivative.y;
386
+ const denominator = Math.pow(derivative.x * derivative.x + derivative.y * derivative.y, 3 / 2);
387
+ if (denominator == 0)
388
+ return NaN;
389
+ return numerator / denominator;
390
+ }
391
+ getCoefficientOfTTerms() {
392
+ return this.getCoefficientOfTTermsWithControlPoints(this.controlPoints);
393
+ }
394
+ getDerivativeCoefficients() {
395
+ return this.getCoefficientOfTTermsWithControlPoints(this.dControlPoints);
396
+ }
397
+ getCoefficientOfTTermsWithControlPoints(controlPoints) {
398
+ const terms = [];
399
+ let matrix = [];
400
+ if (controlPoints.length == 3) {
401
+ matrix = [[1, 0, 0], [-2, 2, 0], [1, -2, 1]];
402
+ }
403
+ else if (controlPoints.length == 4) {
404
+ matrix = [[1, 0, 0, 0], [-3, 3, 0, 0], [3, -6, 3, 0], [-1, 3, -3, 1]];
405
+ }
406
+ else if (controlPoints.length == 2) {
407
+ matrix = [[1, 0], [-1, 1]];
408
+ }
409
+ else {
410
+ throw new Error("number of control points is wrong");
411
+ }
412
+ for (let index = 0; index < controlPoints.length; index++) {
413
+ terms.push(controlPoints.reduce((prevVal, curVal, jindex) => {
414
+ return { x: prevVal.x + matrix[index][jindex] * curVal.x, y: prevVal.y + matrix[index][jindex] * curVal.y };
415
+ }, { x: 0, y: 0 }));
416
+ }
417
+ return terms;
418
+ }
419
+ getControlPointsAlignedWithXAxis() {
420
+ const alignedAxis = PointCal.unitVectorFromA2B(this.controlPoints[0], this.controlPoints[this.controlPoints.length - 1]);
421
+ const angle = PointCal.angleFromA2B({ x: 1, y: 0 }, alignedAxis);
422
+ const startingPoint = this.controlPoints[0];
423
+ const res = [{ x: 0, y: 0 }];
424
+ for (let index = 1; index < this.controlPoints.length; index++) {
425
+ const vector = PointCal.subVector(this.controlPoints[index], startingPoint);
426
+ const rotatedVector = PointCal.rotatePoint(vector, -angle);
427
+ res.push(rotatedVector);
428
+ }
429
+ return res;
430
+ }
431
+ getExtrema() {
432
+ const res = { x: [], y: [] };
433
+ const derivativeCoefficients = this.getDerivativeCoefficients();
434
+ let xCoefficients = [0, 0, 0, 0];
435
+ let yCoefficients = [0, 0, 0, 0];
436
+ derivativeCoefficients.forEach((coefficient, index) => {
437
+ xCoefficients[3 - index] = coefficient.x;
438
+ yCoefficients[3 - index] = coefficient.y;
439
+ });
440
+ const xRoots = solveCubic(xCoefficients[0], xCoefficients[1], xCoefficients[2], xCoefficients[3]);
441
+ const yRoots = solveCubic(yCoefficients[0], yCoefficients[1], yCoefficients[2], yCoefficients[3]);
442
+ xRoots.forEach((root) => {
443
+ if (root >= 0 && root <= 1) {
444
+ res.x.push(root);
445
+ }
446
+ });
447
+ yRoots.forEach((root) => {
448
+ if (root >= 0 && root <= 1) {
449
+ res.y.push(root);
450
+ }
451
+ });
452
+ if (derivativeCoefficients.length >= 3) {
453
+ xCoefficients = [0, 0, 0, 0];
454
+ yCoefficients = [0, 0, 0, 0];
455
+ const secondDerivativeCoefficients = this.getCoefficientOfTTermsWithControlPoints(this.getDerivativeControlPoints(this.dControlPoints));
456
+ secondDerivativeCoefficients.forEach((coefficient, index) => {
457
+ xCoefficients[3 - index] = coefficient.x;
458
+ yCoefficients[3 - index] = coefficient.y;
459
+ });
460
+ const secondXRoots = solveCubic(xCoefficients[0], xCoefficients[1], xCoefficients[2], xCoefficients[3]);
461
+ const secondYRoots = solveCubic(yCoefficients[0], yCoefficients[1], yCoefficients[2], yCoefficients[3]);
462
+ secondXRoots.forEach((root) => {
463
+ if (root >= 0 && root <= 1) {
464
+ res.x.push(root);
465
+ }
466
+ });
467
+ secondYRoots.forEach((root) => {
468
+ if (root >= 0 && root <= 1) {
469
+ res.y.push(root);
470
+ }
471
+ });
472
+ }
473
+ return res;
474
+ }
475
+ translateRotateControlPoints(translation, rotationAngle) {
476
+ // rotation is in radians
477
+ const res = [];
478
+ for (let index = 0; index < this.controlPoints.length; index++) {
479
+ res.push(PointCal.rotatePoint(PointCal.addVector(this.controlPoints[index], translation), rotationAngle));
480
+ }
481
+ return res;
482
+ }
483
+ getLineIntersections(line) {
484
+ const translationRotation = line.getTranslationRotationToAlginXAxis();
485
+ const res = [];
486
+ const alignedControlPoints = this.translateRotateControlPoints(translationRotation.translation, translationRotation.rotationAngle);
487
+ const coefficients = this.getCoefficientOfTTermsWithControlPoints(alignedControlPoints);
488
+ let yCoefficients = [0, 0, 0, 0];
489
+ coefficients.forEach((coefficient, index) => {
490
+ yCoefficients[3 - index] = coefficient.y;
491
+ });
492
+ const yRoots = solveCubic(yCoefficients[0], yCoefficients[1], yCoefficients[2], yCoefficients[3]);
493
+ yRoots.forEach((root) => {
494
+ if (root >= 0 && root <= 1) {
495
+ if (line.pointInLine(this.get(root))) {
496
+ res.push(root);
497
+ }
498
+ }
499
+ });
500
+ return res;
501
+ }
502
+ getSelfIntersections() {
503
+ const [subCurveControlPoints1, subCurveControlPoints2] = this.split(0.5);
504
+ const subCurve1 = new BCurve(subCurveControlPoints1);
505
+ const subCurve2 = new BCurve(subCurveControlPoints2);
506
+ let initialRes = getIntersectionsBetweenCurves(subCurve1, subCurve2);
507
+ initialRes.forEach((intersection) => {
508
+ intersection.selfT = intersection.selfT * 0.5;
509
+ intersection.otherT = intersection.otherT * 0.5 + 0.5;
510
+ });
511
+ initialRes.shift();
512
+ return initialRes;
513
+ }
514
+ getCircleIntersections(circleCenter, circleRadius) {
515
+ const LUT = this.getLUTWithTVal(500);
516
+ LUT[0].point;
517
+ LUT[0].tVal;
518
+ LUT.forEach((curvePoint, index) => {
519
+ Math.abs(PointCal.distanceBetweenPoints(circleCenter, curvePoint.point));
520
+ });
521
+ const LUTD = LUT.map((curvePoint, index) => {
522
+ return { ...curvePoint, distance: 0 };
523
+ });
524
+ let start = 0;
525
+ let count = 0;
526
+ let indices = [];
527
+ while (++count < 25) {
528
+ let i = this.findClosest(circleCenter.x, circleCenter.y, LUTD, circleRadius, 5, LUTD[start - 2]?.distance, LUTD[start - 1]?.distance);
529
+ if (i < start)
530
+ break;
531
+ if (i > 0 && i == start)
532
+ break;
533
+ indices.push(i);
534
+ start = i + 2;
535
+ }
536
+ const finalList = [];
537
+ indices.forEach((index) => {
538
+ let res = this.refineBinary(this, circleCenter.x, circleCenter.y, LUTD, index, circleRadius);
539
+ if (res != undefined) {
540
+ finalList.push({ intersection: res.point, tVal: res.tVal });
541
+ }
542
+ });
543
+ return finalList;
544
+ }
545
+ advanceAtTWithLength(tVal, length) {
546
+ const currentLength = this.lengthAtT(tVal);
547
+ const targetLength = currentLength + length;
548
+ if (targetLength > this.fullLength) {
549
+ return { type: "afterCurve", remainLenth: targetLength - this.fullLength };
550
+ }
551
+ else if (targetLength < 0) {
552
+ return { type: "beforeCurve", remainLength: -targetLength };
553
+ }
554
+ let points = [...this.arcLengthLUT.arcLengthLUT];
555
+ let low = 0;
556
+ let high = points.length - 1;
557
+ while (low <= high) {
558
+ let mid = Math.floor((low + high) / 2);
559
+ if (points[mid].length == targetLength) {
560
+ const point = this.get((mid + 1) / points.length);
561
+ return { type: "withinCurve", tVal: points[mid].tVal, point: point };
562
+ }
563
+ else if (points[mid].length < targetLength) {
564
+ low = mid + 1;
565
+ }
566
+ else {
567
+ high = mid - 1;
568
+ }
569
+ }
570
+ return low >= points.length ? { type: "withinCurve", tVal: 1, point: this.get(1) } : { type: "withinCurve", tVal: (low + 1) / points.length, point: this.get((low + 1) / points.length) };
571
+ }
572
+ refineBinary(curve, x, y, LUT, i, targetDistance = 0, epsilon = 0.01) {
573
+ let q = LUT[i], count = 1, distance = Number.MAX_SAFE_INTEGER;
574
+ do {
575
+ let i1 = i === 0 ? 0 : i - 1, i2 = i === LUT.length - 1 ? LUT.length - 1 : i + 1, t1 = LUT[i1].tVal, t2 = LUT[i2].tVal, lut = [], step = (t2 - t1) / 4;
576
+ if (step < 0.001)
577
+ break;
578
+ lut.push(LUT[i1]);
579
+ for (let j = 1; j <= 3; j++) {
580
+ let n = curve.get(t1 + j * step);
581
+ let nDistance = Math.abs(PointCal.distanceBetweenPoints(n, { x: x, y: y }) - targetDistance);
582
+ if (nDistance < distance) {
583
+ distance = nDistance;
584
+ q = { point: n, tVal: t1 + j * step, distance: nDistance };
585
+ i = j;
586
+ }
587
+ lut.push({ point: n, tVal: t1 + j * step, distance: nDistance });
588
+ }
589
+ lut.push(LUT[i2]);
590
+ // update the LUT to be our new five point LUT, and run again.
591
+ LUT = lut;
592
+ // The "count" test is mostly a safety measure: it will
593
+ // never kick in, but something that _will_ terminate is
594
+ // always better than while(true). Never use while(true)
595
+ } while (count++ < 25);
596
+ // If we're trying to hit a target, discard the result if
597
+ // it is not close enough to the target.
598
+ if (targetDistance && distance > epsilon) {
599
+ q = undefined;
600
+ }
601
+ return q;
602
+ }
603
+ findClosest(x, y, LUT, circleRadius, distanceEpsilon = 5, pd2, pd1) {
604
+ let distance = Number.MAX_SAFE_INTEGER, prevDistance2 = pd2 || distance, prevDistance1 = pd1 || distance, i = -1;
605
+ for (let index = 0, e = LUT.length; index < e; index++) {
606
+ let p = LUT[index].point;
607
+ LUT[index].distance = Math.abs(PointCal.distanceBetweenPoints({ x: x, y: y }, p) - circleRadius);
608
+ // Realistically, there's only going to be an intersection if
609
+ // the distance to the circle center is already approximately
610
+ // the circle's radius.
611
+ if (prevDistance1 < distanceEpsilon && prevDistance2 > prevDistance1 && prevDistance1 < LUT[index].distance) {
612
+ i = index - 1;
613
+ break;
614
+ }
615
+ if (LUT[index].distance < distance) {
616
+ distance = LUT[index].distance;
617
+ }
618
+ prevDistance2 = prevDistance1;
619
+ prevDistance1 = LUT[index].distance;
620
+ }
621
+ return i;
622
+ }
623
+ getCurveIntersections(curve) {
624
+ return getIntersectionsBetweenCurves(this, curve);
625
+ }
626
+ get AABB() {
627
+ const extrema = this.getExtrema();
628
+ const tVals = [0, 1];
629
+ let min = { x: Number.MAX_VALUE, y: Number.MAX_VALUE };
630
+ let max = { x: -Number.MAX_VALUE, y: -Number.MAX_VALUE };
631
+ extrema.x.forEach((tVal) => {
632
+ tVals.push(tVal);
633
+ });
634
+ extrema.y.forEach((tVal) => {
635
+ tVals.push(tVal);
636
+ });
637
+ tVals.forEach((tVal) => {
638
+ const curPoint = this.get(tVal);
639
+ min.x = Math.min(min.x, curPoint.x);
640
+ min.y = Math.min(min.y, curPoint.y);
641
+ max.x = Math.max(max.x, curPoint.x);
642
+ max.y = Math.max(max.y, curPoint.y);
643
+ });
644
+ return { min: min, max: max };
645
+ }
646
+ }
647
+ class TValOutofBoundError extends Error {
648
+ constructor(message) {
649
+ super(message);
650
+ }
651
+ }
652
+ function AABBIntersects(AABB1, AABB2) {
653
+ if ((AABB1.min.x <= AABB2.max.x && AABB2.min.x <= AABB1.max.x) && (AABB1.min.y <= AABB2.max.y && AABB2.min.y <= AABB1.max.y)) {
654
+ return true;
655
+ }
656
+ return false;
657
+ }
658
+ function approximately(a, b, precision) {
659
+ const epsilon = 0.000001;
660
+ return Math.abs(a - b) <= (precision || epsilon);
661
+ }
662
+ function cuberoot2(v) {
663
+ if (v < 0)
664
+ return -Math.pow(-v, 1 / 3);
665
+ return Math.pow(v, 1 / 3);
666
+ }
667
+ function accept(t) {
668
+ return 0 <= t && t <= 1;
669
+ }
670
+ function getCubicRoots(pa, pb, pc, pd) {
671
+ let a = (3 * pa - 6 * pb + 3 * pc), b = (-3 * pa + 3 * pb), c = pa, d = (-pa + 3 * pb - 3 * pc + pd);
672
+ // do a check to see whether we even need cubic solving:
673
+ if (approximately(d, 0)) {
674
+ // this is not a cubic curve.
675
+ if (approximately(a, 0)) {
676
+ // in fact, this is not a quadratic curve either.
677
+ if (approximately(b, 0)) {
678
+ // in fact in fact, there are no solutions.
679
+ return [];
680
+ }
681
+ // linear solution
682
+ return [-c / b].filter(accept);
683
+ }
684
+ // quadratic solution
685
+ let q = Math.sqrt(b * b - 4 * a * c), a2 = 2 * a;
686
+ return [(q - b) / a2, (-b - q) / a2].filter(accept);
687
+ }
688
+ // at this point, we know we need a cubic solution.
689
+ a /= d;
690
+ b /= d;
691
+ c /= d;
692
+ let p = (3 * b - a * a) / 3, p3 = p / 3, q = (2 * a * a * a - 9 * a * b + 27 * c) / 27, q2 = q / 2, discriminant = q2 * q2 + p3 * p3 * p3;
693
+ // and some variables we're going to use later on:
694
+ let u1, v1, root1, root2, root3;
695
+ // three possible real roots:
696
+ if (discriminant < 0) {
697
+ let mp3 = -p / 3, mp33 = mp3 * mp3 * mp3, r = Math.sqrt(mp33), t = -q / (2 * r), cosphi = t < -1 ? -1 : t > 1 ? 1 : t, phi = Math.acos(cosphi), crtr = cuberoot2(r), t1 = 2 * crtr;
698
+ root1 = t1 * Math.cos(phi / 3) - a / 3;
699
+ root2 = t1 * Math.cos((phi + 2 * Math.PI) / 3) - a / 3;
700
+ root3 = t1 * Math.cos((phi + 4 * Math.PI) / 3) - a / 3;
701
+ return [root1, root2, root3].filter(accept);
702
+ }
703
+ // three real roots, but two of them are equal:
704
+ if (discriminant === 0) {
705
+ u1 = q2 < 0 ? cuberoot2(-q2) : -cuberoot2(q2);
706
+ root1 = 2 * u1 - a / 3;
707
+ root2 = -u1 - a / 3;
708
+ return [root1, root2].filter(accept);
709
+ }
710
+ // one real root, two complex roots
711
+ var sd = Math.sqrt(discriminant);
712
+ u1 = cuberoot2(sd - q2);
713
+ v1 = cuberoot2(sd + q2);
714
+ root1 = u1 - v1 - a / 3;
715
+ return [root1].filter(accept);
716
+ }
717
+ function getIntersectionsBetweenCurves(curve, curve2) {
718
+ const threshold = 0.0001;
719
+ let pairs = [{ curve1: { curve: curve, startTVal: 0, endTVal: 1 }, curve2: { curve: curve2, startTVal: 0, endTVal: 1 } }];
720
+ const finalRes = [];
721
+ while (pairs.length > 0) {
722
+ let curLength = pairs.length;
723
+ for (let index = 0; index < curLength; index++) {
724
+ let pair = pairs.shift();
725
+ if (pair == undefined) {
726
+ break;
727
+ }
728
+ let aabb1 = pair.curve1.curve.AABB;
729
+ let aabb2 = pair.curve2.curve.AABB;
730
+ let intersects = AABBIntersects(aabb1, aabb2);
731
+ if (pair.curve1.curve.fullLength < threshold && pair.curve2.curve.fullLength < threshold) {
732
+ finalRes.push({ intersection: pair.curve1.curve.get(0.5), tVal1: (pair.curve1.startTVal + pair.curve1.endTVal) * 0.5, tVal2: (pair.curve2.startTVal + pair.curve2.endTVal) * 0.5 });
733
+ continue;
734
+ }
735
+ if (intersects) {
736
+ let [subCurveControlPoints1, subCurveControlPoints2] = pair.curve1.curve.split(0.5);
737
+ let [subCurveControlPoints3, subCurveControlPoints4] = pair.curve2.curve.split(0.5);
738
+ pairs.push({
739
+ curve1: {
740
+ curve: new BCurve(subCurveControlPoints1),
741
+ startTVal: pair.curve1.startTVal,
742
+ endTVal: pair.curve1.startTVal + (pair.curve1.endTVal - pair.curve1.startTVal) * 0.5
743
+ }, curve2: {
744
+ curve: new BCurve(subCurveControlPoints3),
745
+ startTVal: pair.curve2.startTVal,
746
+ endTVal: pair.curve2.startTVal + (pair.curve2.endTVal - pair.curve2.startTVal) * 0.5
747
+ }
748
+ });
749
+ pairs.push({
750
+ curve1: {
751
+ curve: new BCurve(subCurveControlPoints1),
752
+ startTVal: pair.curve1.startTVal,
753
+ endTVal: pair.curve1.startTVal + (pair.curve1.endTVal - pair.curve1.startTVal) * 0.5
754
+ }, curve2: {
755
+ curve: new BCurve(subCurveControlPoints4),
756
+ startTVal: pair.curve2.startTVal + (pair.curve2.endTVal - pair.curve2.startTVal) * 0.5,
757
+ endTVal: pair.curve2.endTVal
758
+ }
759
+ });
760
+ pairs.push({
761
+ curve1: {
762
+ curve: new BCurve(subCurveControlPoints2),
763
+ startTVal: pair.curve1.startTVal + (pair.curve1.endTVal - pair.curve1.startTVal) * 0.5,
764
+ endTVal: pair.curve1.endTVal
765
+ }, curve2: {
766
+ curve: new BCurve(subCurveControlPoints3),
767
+ startTVal: pair.curve2.startTVal,
768
+ endTVal: pair.curve2.startTVal + (pair.curve2.endTVal - pair.curve2.startTVal) * 0.5
769
+ }
770
+ });
771
+ pairs.push({
772
+ curve1: {
773
+ curve: new BCurve(subCurveControlPoints2),
774
+ startTVal: pair.curve1.startTVal + (pair.curve1.endTVal - pair.curve1.startTVal) * 0.5,
775
+ endTVal: pair.curve1.endTVal
776
+ }, curve2: {
777
+ curve: new BCurve(subCurveControlPoints4),
778
+ startTVal: pair.curve2.startTVal + (pair.curve2.endTVal - pair.curve2.startTVal) * 0.5,
779
+ endTVal: pair.curve2.endTVal
780
+ }
781
+ });
782
+ }
783
+ }
784
+ }
785
+ const tVals = [];
786
+ const tVal1s = [];
787
+ finalRes.forEach((intersections) => {
788
+ let within = false;
789
+ for (let index = 0; index < tVal1s.length; index++) {
790
+ if (approximately(intersections.tVal1, tVal1s[index], 0.000005)) {
791
+ within = true;
792
+ break;
793
+ }
794
+ }
795
+ if (!within) {
796
+ tVal1s.push(intersections.tVal1);
797
+ tVals.push({ selfT: intersections.tVal1, otherT: intersections.tVal2 });
798
+ }
799
+ });
800
+ return tVals;
801
+ }
802
+ function solveCubic(a, b, c, d) {
803
+ if (Math.abs(a) < 1e-8) { // Quadratic case, ax^2+bx+c=0
804
+ a = b;
805
+ b = c;
806
+ c = d;
807
+ if (Math.abs(a) < 1e-8) { // Linear case, ax+b=0
808
+ a = b;
809
+ b = c;
810
+ if (Math.abs(a) < 1e-8) // Degenerate case
811
+ return [];
812
+ return [-b / a];
813
+ }
814
+ let D = b * b - 4 * a * c;
815
+ if (Math.abs(D) < 1e-8)
816
+ return [-b / (2 * a)];
817
+ else if (D > 0)
818
+ return [(-b + Math.sqrt(D)) / (2 * a), (-b - Math.sqrt(D)) / (2 * a)];
819
+ return [];
820
+ }
821
+ // Convert to depressed cubic t^3+pt+q = 0 (subst x = t - b/3a)
822
+ let p = (3 * a * c - b * b) / (3 * a * a);
823
+ let q = (2 * b * b * b - 9 * a * b * c + 27 * a * a * d) / (27 * a * a * a);
824
+ let roots;
825
+ if (Math.abs(p) < 1e-8) { // p = 0 -> t^3 = -q -> t = -q^1/3
826
+ roots = [cuberoot(-q)];
827
+ }
828
+ else if (Math.abs(q) < 1e-8) { // q = 0 -> t^3 + pt = 0 -> t(t^2+p)=0
829
+ roots = [0].concat(p < 0 ? [Math.sqrt(-p), -Math.sqrt(-p)] : []);
830
+ }
831
+ else {
832
+ let D = q * q / 4 + p * p * p / 27;
833
+ if (Math.abs(D) < 1e-8) { // D = 0 -> two roots
834
+ roots = [-1.5 * q / p, 3 * q / p];
835
+ }
836
+ else if (D > 0) { // Only one real root and two complex roots
837
+ let u = cuberoot(-q / 2 - Math.sqrt(D));
838
+ //console.log("Complext Root 1 real:", -(u+v)/2.0 - a / 3.0, "imaginary:", Math.sqrt(3) / 2.0 * (v - u));
839
+ //console.log("Complext Root 2 real:", -(u+v)/2.0 - a / 3.0, "imaginary:",-1 * Math.sqrt(3) / 2.0 * (v - u));
840
+ roots = [u - p / (3 * u)];
841
+ }
842
+ else { // D < 0, three roots, but needs to use complex numbers/trigonometric solution
843
+ let u = 2 * Math.sqrt(-p / 3);
844
+ let t = Math.acos(3 * q / p / u) / 3; // D < 0 implies p < 0 and acos argument in [-1..1]
845
+ let k = 2 * Math.PI / 3;
846
+ roots = [u * Math.cos(t), u * Math.cos(t - k), u * Math.cos(t - 2 * k)];
847
+ }
848
+ }
849
+ // Convert back from depressed cubic
850
+ for (let i = 0; i < roots.length; i++)
851
+ roots[i] -= b / (3 * a);
852
+ return roots;
853
+ }
854
+ function cuberoot(x) {
855
+ var y = Math.pow(Math.abs(x), 1 / 3);
856
+ return x < 0 ? -y : y;
857
+ }
858
+ function computeWithControlPoints(tVal, controlPoints) {
859
+ let points = [...controlPoints];
860
+ while (points.length > 1) {
861
+ let lowerLevelPoints = points.slice(1);
862
+ for (let index = 0; index < lowerLevelPoints.length; index++) {
863
+ lowerLevelPoints[index] = PointCal.addVector(PointCal.multiplyVectorByScalar(points[index], (1 - tVal)), PointCal.multiplyVectorByScalar(points[index + 1], tVal));
864
+ }
865
+ points = lowerLevelPoints;
866
+ }
867
+ return points[0];
868
+ }
869
+
870
+ class Line {
871
+ constructor(startPoint, endPoint) {
872
+ this.startPoint = startPoint;
873
+ this.endPoint = endPoint;
874
+ }
875
+ getStartPoint() {
876
+ return this.startPoint;
877
+ }
878
+ getEndPoint() {
879
+ return this.endPoint;
880
+ }
881
+ intersectionWithAnotherLine(lineToIntersect) {
882
+ return getLineIntersection(this.startPoint, this.endPoint, lineToIntersect.getStartPoint(), lineToIntersect.getEndPoint());
883
+ }
884
+ projectPoint(point) {
885
+ return projectPointOntoLine(point, this.getStartPoint(), this.getEndPoint());
886
+ }
887
+ length() {
888
+ return PointCal.distanceBetweenPoints(this.startPoint, this.endPoint);
889
+ }
890
+ getTranslationRotationToAlginXAxis() {
891
+ const translation = PointCal.subVector({ x: 0, y: 0 }, this.startPoint);
892
+ const rotationAngle = PointCal.angleFromA2B(PointCal.subVector(this.endPoint, this.startPoint), { x: 1, y: 0 });
893
+ return { translation, rotationAngle };
894
+ }
895
+ pointInLine(point) {
896
+ const baseVector = PointCal.unitVectorFromA2B(this.startPoint, this.endPoint);
897
+ const start2PointVector = PointCal.subVector(point, this.startPoint);
898
+ const length = PointCal.dotProduct(start2PointVector, baseVector);
899
+ const start2PointUnitVector = PointCal.unitVector(start2PointVector);
900
+ PointCal.distanceBetweenPoints(this.startPoint, this.endPoint) * 0.0001;
901
+ return length <= PointCal.distanceBetweenPoints(this.startPoint, this.endPoint) && length >= 0 && Math.abs(start2PointUnitVector.x - baseVector.x) < 0.0001 && Math.abs(start2PointUnitVector.y - baseVector.y) < 0.0001;
902
+ }
903
+ lerp(ratio) {
904
+ return PointCal.linearInterpolation(this.startPoint, this.endPoint, ratio);
905
+ }
906
+ }
907
+ function getLineIntersection(startPoint, endPoint, startPoint2, endPoint2) {
908
+ const numerator = (endPoint2.x - startPoint2.x) * (startPoint.y - startPoint2.y) - (endPoint2.y - startPoint2.y) * (startPoint.x - startPoint2.x);
909
+ const denominator = (endPoint2.y - startPoint2.y) * (endPoint.x - startPoint.x) - (endPoint2.x - startPoint2.x) * (endPoint.y - startPoint.y);
910
+ if (denominator === 0) {
911
+ return { intersects: false };
912
+ }
913
+ const t = numerator / denominator;
914
+ if (t >= 0 && t <= 1) {
915
+ return {
916
+ intersects: true,
917
+ intersection: PointCal.linearInterpolation(startPoint, endPoint, t),
918
+ offset: t
919
+ };
920
+ }
921
+ else {
922
+ return {
923
+ intersects: false,
924
+ };
925
+ }
926
+ }
927
+ function projectPointOntoLine(point, lineStartPoint, lineEndPoint) {
928
+ const baseVector = PointCal.unitVector(PointCal.subVector(lineEndPoint, lineStartPoint));
929
+ const vectorToPoint = PointCal.subVector(point, lineStartPoint);
930
+ const res = PointCal.dotProduct(vectorToPoint, baseVector);
931
+ if (res < 0 || res > PointCal.magnitude(PointCal.subVector(lineEndPoint, lineStartPoint))) {
932
+ return {
933
+ within: false,
934
+ };
935
+ }
936
+ return {
937
+ within: true,
938
+ projectionPoint: PointCal.addVector(lineStartPoint, PointCal.multiplyVectorByScalar(baseVector, res)),
939
+ offset: res / PointCal.magnitude(PointCal.subVector(lineEndPoint, lineStartPoint))
940
+ };
941
+ }
942
+
943
+ class ControlPoint {
944
+ constructor(position, leftHandle, rightHandle) {
945
+ this.position = position;
946
+ this.leftHandle = leftHandle;
947
+ this.rightHandle = rightHandle;
948
+ }
949
+ setPosition(destinationPosition, prevControlPoint, nextControlPoint) {
950
+ let diff = PointCal.subVector(destinationPosition, this.position);
951
+ this.position = destinationPosition;
952
+ this.leftHandle.position = PointCal.addVector(this.leftHandle.position, diff);
953
+ this.rightHandle.position = PointCal.addVector(this.rightHandle.position, diff);
954
+ if (this.leftHandle.type == "VECTOR" && prevControlPoint) {
955
+ let relativeVector = PointCal.subVector(prevControlPoint.getPosition(), this.position);
956
+ relativeVector = PointCal.multiplyVectorByScalar(relativeVector, 1 / 3);
957
+ this.leftHandle.position = PointCal.addVector(this.position, relativeVector);
958
+ if (this.rightHandle.type == "ALIGNED") {
959
+ let relativeVector = PointCal.subVector(this.rightHandle.position, this.position);
960
+ let mag = PointCal.magnitude(relativeVector);
961
+ let direction = PointCal.unitVectorFromA2B(this.leftHandle.position, this.position);
962
+ this.rightHandle.position = PointCal.addVector(this.position, PointCal.multiplyVectorByScalar(direction, mag));
963
+ }
964
+ }
965
+ if (this.rightHandle.type == "VECTOR" && nextControlPoint) {
966
+ let relativeVector = PointCal.subVector(nextControlPoint.getPosition(), this.position);
967
+ relativeVector = PointCal.multiplyVectorByScalar(relativeVector, 1 / 3);
968
+ this.rightHandle.position = PointCal.addVector(this.position, relativeVector);
969
+ if (this.leftHandle.type == "ALIGNED") {
970
+ let mag = PointCal.distanceBetweenPoints(this.leftHandle.position, this.position);
971
+ let direction = PointCal.subVector(this.position, this.rightHandle.position);
972
+ this.leftHandle.position = PointCal.addVector(this.position, PointCal.multiplyVectorByScalar(direction, mag));
973
+ }
974
+ }
975
+ if (prevControlPoint !== undefined && prevControlPoint.getRightHandle().type == "VECTOR") {
976
+ let relativeVector = PointCal.subVector(this.position, prevControlPoint.getPosition());
977
+ relativeVector = PointCal.multiplyVectorByScalar(relativeVector, 1 / 3);
978
+ prevControlPoint.setRightHandlePosition(PointCal.addVector(prevControlPoint.getPosition(), relativeVector));
979
+ }
980
+ if (nextControlPoint !== undefined && nextControlPoint.getLeftHandle().type == "VECTOR") {
981
+ let relativeVector = PointCal.subVector(this.position, nextControlPoint.getPosition());
982
+ relativeVector = PointCal.multiplyVectorByScalar(relativeVector, 1 / 3);
983
+ nextControlPoint.setLeftHandlePosition(PointCal.addVector(nextControlPoint.getPosition(), relativeVector));
984
+ }
985
+ }
986
+ getPosition() {
987
+ return this.position;
988
+ }
989
+ setLeftHandleTypeVector(prevControlPoint) {
990
+ if (this.rightHandle.type != "VECTOR") {
991
+ this.rightHandle.type = "FREE";
992
+ }
993
+ let relativeVector;
994
+ if (prevControlPoint == undefined) {
995
+ relativeVector = PointCal.subVector(this.leftHandle.position, this.position);
996
+ }
997
+ else {
998
+ relativeVector = PointCal.subVector(prevControlPoint.getPosition(), this.position);
999
+ relativeVector = PointCal.multiplyVectorByScalar(relativeVector, 1 / 3);
1000
+ }
1001
+ this.leftHandle.position = PointCal.addVector(this.position, relativeVector);
1002
+ }
1003
+ setLeftHandleTypeAligned() {
1004
+ this.leftHandle.type = "ALIGNED";
1005
+ if (this.rightHandle.type == "VECTOR") {
1006
+ let direction = PointCal.unitVectorFromA2B(this.rightHandle.position, this.position);
1007
+ let mag = PointCal.distanceBetweenPoints(this.position, this.leftHandle.position);
1008
+ this.leftHandle.position = PointCal.addVector(this.position, PointCal.multiplyVectorByScalar(direction, mag));
1009
+ }
1010
+ }
1011
+ setLeftHandleTypeFree() {
1012
+ this.leftHandle.type = "FREE";
1013
+ }
1014
+ setRightHandleTypeVector(nextControlPoint) {
1015
+ if (this.leftHandle.type != "VECTOR") {
1016
+ this.leftHandle.type = "FREE";
1017
+ }
1018
+ let relativeVector;
1019
+ if (nextControlPoint == undefined) {
1020
+ relativeVector = PointCal.subVector(this.rightHandle.position, this.position);
1021
+ }
1022
+ else {
1023
+ relativeVector = PointCal.subVector(nextControlPoint.getPosition(), this.position);
1024
+ relativeVector = PointCal.multiplyVectorByScalar(relativeVector, 1 / 3);
1025
+ }
1026
+ this.rightHandle.position = PointCal.addVector(this.position, relativeVector);
1027
+ }
1028
+ setRightHandleTypeAligned() {
1029
+ this.rightHandle.type = "ALIGNED";
1030
+ if (this.leftHandle.type == "VECTOR") {
1031
+ let direciton = PointCal.unitVectorFromA2B(this.leftHandle.position, this.position);
1032
+ let mag = PointCal.distanceBetweenPoints(this.position, this.rightHandle.position);
1033
+ this.rightHandle.position = PointCal.addVector(this.position, PointCal.multiplyVectorByScalar(direciton, mag));
1034
+ }
1035
+ }
1036
+ setRightHandleTypeFree() {
1037
+ this.rightHandle.type = "FREE";
1038
+ }
1039
+ setLeftHandlePosition(destPos) {
1040
+ let leftHandleType = this.leftHandle.type;
1041
+ switch (leftHandleType) {
1042
+ case "ALIGNED":
1043
+ if (this.rightHandle.type == "VECTOR") {
1044
+ let diff = PointCal.subVector(destPos, this.position);
1045
+ let rightHandleDiff = PointCal.unitVectorFromA2B(this.rightHandle.position, this.position);
1046
+ let resMag = PointCal.dotProduct(diff, rightHandleDiff);
1047
+ let res = PointCal.multiplyVectorByScalar(rightHandleDiff, resMag);
1048
+ this.leftHandle.position = PointCal.addVector(this.position, res);
1049
+ }
1050
+ else if (this.rightHandle.type == "ALIGNED") {
1051
+ this.leftHandle.position = destPos;
1052
+ let mag = PointCal.distanceBetweenPoints(this.rightHandle.position, this.position);
1053
+ let direction = PointCal.unitVectorFromA2B(this.leftHandle.position, this.position);
1054
+ let res = PointCal.multiplyVectorByScalar(direction, mag);
1055
+ this.rightHandle.position = PointCal.addVector(res, this.position);
1056
+ }
1057
+ else {
1058
+ this.leftHandle.position = destPos;
1059
+ }
1060
+ break;
1061
+ case "FREE":
1062
+ this.leftHandle.position = destPos;
1063
+ break;
1064
+ case "VECTOR":
1065
+ break;
1066
+ default:
1067
+ throw new Error(`Unknown left handle type for control point`);
1068
+ }
1069
+ }
1070
+ setRightHandlePosition(destPos) {
1071
+ let rightHandleType = this.rightHandle.type;
1072
+ switch (rightHandleType) {
1073
+ case "ALIGNED":
1074
+ if (this.leftHandle.type == "VECTOR") {
1075
+ let diff = PointCal.subVector(destPos, this.position);
1076
+ let leftHandleDiff = PointCal.unitVectorFromA2B(this.leftHandle.position, this.position);
1077
+ let resMag = PointCal.dotProduct(diff, leftHandleDiff);
1078
+ let res = PointCal.multiplyVectorByScalar(leftHandleDiff, resMag);
1079
+ this.rightHandle.position = PointCal.addVector(this.position, res);
1080
+ }
1081
+ else if (this.rightHandle.type == "ALIGNED") {
1082
+ this.rightHandle.position = destPos;
1083
+ let mag = PointCal.distanceBetweenPoints(this.leftHandle.position, this.position);
1084
+ let direction = PointCal.unitVectorFromA2B(this.rightHandle.position, this.position);
1085
+ let res = PointCal.multiplyVectorByScalar(direction, mag);
1086
+ this.leftHandle.position = PointCal.addVector(res, this.position);
1087
+ }
1088
+ else {
1089
+ this.rightHandle.position = destPos;
1090
+ }
1091
+ break;
1092
+ case "FREE":
1093
+ this.rightHandle.position = destPos;
1094
+ break;
1095
+ case "VECTOR":
1096
+ break;
1097
+ default:
1098
+ throw new Error(`Unknown left handle type for control point`);
1099
+ }
1100
+ }
1101
+ getLeftHandle() {
1102
+ return this.leftHandle;
1103
+ }
1104
+ getRightHandle() {
1105
+ return this.rightHandle;
1106
+ }
1107
+ }
1108
+ class CompositeBCurve {
1109
+ constructor(controlPoints = []) {
1110
+ this.controlPoints = controlPoints;
1111
+ }
1112
+ getControlPoints() {
1113
+ return this.controlPoints;
1114
+ }
1115
+ appendControlPoint(position) {
1116
+ let leftHandlePosition = PointCal.addVector(position, { x: -100, y: 0 });
1117
+ let rightHandlePosition = PointCal.addVector(position, { x: 100, y: 0 });
1118
+ let leftHandlePoint = {
1119
+ position: leftHandlePosition,
1120
+ type: "FREE"
1121
+ };
1122
+ let rightHandlePoint = {
1123
+ position: rightHandlePosition,
1124
+ type: "FREE"
1125
+ };
1126
+ let newControlPoint = new ControlPoint(position, leftHandlePoint, rightHandlePoint);
1127
+ this.controlPoints.push(newControlPoint);
1128
+ }
1129
+ setLeftHandlePositionOfControlPoint(controlPointIndex, destPos) {
1130
+ if (controlPointIndex >= this.controlPoints.length || controlPointIndex < 0) {
1131
+ return;
1132
+ }
1133
+ this.controlPoints[controlPointIndex].setLeftHandlePosition(destPos);
1134
+ }
1135
+ setRightHandlePositionOfControlPoint(controlPointIndex, destPos) {
1136
+ if (controlPointIndex >= this.controlPoints.length || controlPointIndex < 0) {
1137
+ return;
1138
+ }
1139
+ this.controlPoints[controlPointIndex].setRightHandlePosition(destPos);
1140
+ }
1141
+ setPositionOfControlPoint(controlPointIndex, destPos) {
1142
+ if (controlPointIndex >= this.controlPoints.length || controlPointIndex < 0) {
1143
+ return;
1144
+ }
1145
+ let prevControlPoint = undefined;
1146
+ let nextControlPoint = undefined;
1147
+ if (controlPointIndex + 1 < this.controlPoints.length) {
1148
+ nextControlPoint = this.controlPoints[controlPointIndex + 1];
1149
+ }
1150
+ if (controlPointIndex - 1 >= 0) {
1151
+ prevControlPoint = this.controlPoints[controlPointIndex - 1];
1152
+ }
1153
+ this.controlPoints[controlPointIndex].setPosition(destPos, prevControlPoint, nextControlPoint);
1154
+ }
1155
+ }
1156
+
1157
+ class Path {
1158
+ constructor(lines) {
1159
+ this.lines = lines;
1160
+ }
1161
+ append(line) {
1162
+ this.lines.push(line);
1163
+ }
1164
+ clear() {
1165
+ this.lines = [];
1166
+ }
1167
+ prepend(line) {
1168
+ this.lines.unshift(line);
1169
+ }
1170
+ getLines() {
1171
+ return this.lines;
1172
+ }
1173
+ getLength() {
1174
+ let res = 0;
1175
+ this.lines.forEach((line) => {
1176
+ res += line.length();
1177
+ });
1178
+ return res;
1179
+ }
1180
+ getPercentages() {
1181
+ const length = this.getLength();
1182
+ let currentCurvePercentage = 0;
1183
+ const res = [];
1184
+ this.lines.forEach((line) => {
1185
+ const lineLength = line.length();
1186
+ const linePercentage = lineLength / length;
1187
+ let start = currentCurvePercentage;
1188
+ currentCurvePercentage += linePercentage;
1189
+ let end = currentCurvePercentage;
1190
+ res.push({ start, end });
1191
+ });
1192
+ res[res.length - 1].end = 1;
1193
+ return res;
1194
+ }
1195
+ getPointByPercentage(percentage) {
1196
+ if (percentage < 0 || percentage > 1) {
1197
+ throw new Error("Percentage must be between 0 and 1");
1198
+ }
1199
+ const percentages = this.getPercentages();
1200
+ let left = 0;
1201
+ let right = percentages.length - 1;
1202
+ while (left <= right) {
1203
+ const mid = Math.floor((left + right) / 2);
1204
+ if (percentage < percentages[mid].end) {
1205
+ right = mid - 1;
1206
+ }
1207
+ else if (percentage > percentages[mid].end) {
1208
+ left = mid + 1;
1209
+ }
1210
+ else {
1211
+ left = mid;
1212
+ break;
1213
+ }
1214
+ }
1215
+ const line = this.lines[left];
1216
+ const linePercentage = percentages[left];
1217
+ const ratio = (percentage - linePercentage.start) / (linePercentage.end - linePercentage.start);
1218
+ return line.lerp(ratio);
1219
+ }
1220
+ }
1221
+
1222
+ export { AABBIntersects, BCurve, CompositeBCurve, ControlPoint, Line, Path, TValOutofBoundError, accept, approximately, computeWithControlPoints, cuberoot, cuberoot2, getCubicRoots, getIntersectionsBetweenCurves, getLineIntersection, projectPointOntoLine, solveCubic };
1223
+ //# sourceMappingURL=index.js.map