@ue-too/curve 0.7.3 → 0.9.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/index.js CHANGED
@@ -1,1642 +1,1591 @@
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,
1
+ // src/b-curve.ts
2
+ import { PointCal } from "@ue-too/math";
3
+ var T = [
4
+ -0.06405689286260563,
5
+ 0.06405689286260563,
6
+ -0.1911188674736163,
7
+ 0.1911188674736163,
8
+ -0.3150426796961634,
9
+ 0.3150426796961634,
10
+ -0.4337935076260451,
11
+ 0.4337935076260451,
12
+ -0.5454214713888396,
13
+ 0.5454214713888396,
14
+ -0.6480936519369755,
15
+ 0.6480936519369755,
16
+ -0.7401241915785544,
17
+ 0.7401241915785544,
18
+ -0.820001985973903,
19
+ 0.820001985973903,
20
+ -0.8864155270044011,
21
+ 0.8864155270044011,
22
+ -0.9382745520027328,
23
+ 0.9382745520027328,
24
+ -0.9747285559713095,
25
+ 0.9747285559713095,
26
+ -0.9951872199970213,
27
+ 0.9951872199970213
28
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,
29
+ var C = [
30
+ 0.12793819534675216,
31
+ 0.12793819534675216,
32
+ 0.1258374563468283,
33
+ 0.1258374563468283,
34
+ 0.12167047292780339,
35
+ 0.12167047292780339,
36
+ 0.1155056680537256,
37
+ 0.1155056680537256,
38
+ 0.10744427011596563,
39
+ 0.10744427011596563,
40
+ 0.09761865210411388,
41
+ 0.09761865210411388,
42
+ 0.08619016153195327,
43
+ 0.08619016153195327,
44
+ 0.0733464814110803,
45
+ 0.0733464814110803,
46
+ 0.05929858491543678,
47
+ 0.05929858491543678,
48
+ 0.04427743881741981,
49
+ 0.04427743881741981,
50
+ 0.028531388628933663,
51
+ 0.028531388628933663,
52
+ 0.0123412297999872,
53
+ 0.0123412297999872
54
54
  ];
55
+
55
56
  class BCurve {
56
- /**
57
- * Gets cache statistics for performance monitoring
58
- * @returns Object containing cache size and hit rate information
59
- */
60
- getCacheStats() {
61
- return {
62
- size: this.lengthCache.size,
63
- hitRate: 0 // This would need to be tracked separately if needed
64
- };
65
- }
66
- /**
67
- * Pre-warms the cache with commonly used t values for better performance
68
- * @param steps Number of steps to pre-cache (default: 100)
69
- */
70
- preWarmCache(steps = 100) {
71
- const tSteps = 1 / steps;
72
- for (let tVal = 0; tVal <= 1; tVal += tSteps) {
73
- this.lengthAtT(tVal);
74
- }
75
- }
76
- clearCache() {
77
- this.lengthCache.clear();
78
- }
79
- constructor(controlPoints) {
80
- this.dControlPoints = [];
81
- this.arcLengthLUT = { controlPoints: [], arcLengthLUT: [] };
82
- this.lengthCache = new Map(); // Cache for lengthAtT results
83
- this.controlPoints = controlPoints;
84
- this.dControlPoints = this.getDerivativeControlPoints(this.controlPoints);
85
- this._fullLength = this.calculateFullLength();
86
- // Make arc length LUT lazy - only compute when needed
87
- this.arcLengthLUT = { controlPoints: [], arcLengthLUT: [] };
88
- this.clearCache(); // Clear cache on initialization
89
- }
90
- getPointbyPercentage(percentage) {
91
- // this leaves room for optimization
92
- let controlPointsChangedSinceLastArcLengthLUT = this.arcLengthLUT.controlPoints.length != this.controlPoints.length;
93
- controlPointsChangedSinceLastArcLengthLUT = controlPointsChangedSinceLastArcLengthLUT || this.arcLengthLUT.controlPoints.reduce((prevVal, curVal, index) => {
94
- return prevVal || !PointCal.isEqual(curVal, this.controlPoints[index]);
95
- }, false);
96
- let points = [];
97
- if (controlPointsChangedSinceLastArcLengthLUT) {
98
- this.arcLengthLUT = this.getArcLengthLUT(1000);
99
- }
100
- points = [...this.arcLengthLUT.arcLengthLUT.map((item) => item.length)];
101
- let targetLength = percentage * this.fullLength;
102
- let low = 0;
103
- let high = points.length - 1;
104
- while (low <= high) {
105
- let mid = Math.floor((low + high) / 2);
106
- if (points[mid] == targetLength) {
107
- return this.get((mid + 1) / points.length);
108
- }
109
- else if (points[mid] < targetLength) {
110
- low = mid + 1;
111
- }
112
- else {
113
- high = mid - 1;
114
- }
115
- }
116
- return low >= points.length ? this.get(1) : this.get((low + 1) / points.length);
117
- }
118
- getDerivativeControlPoints(controlPoints) {
119
- const derivativeControlPoints = [];
120
- for (let index = 1; index < controlPoints.length; index++) {
121
- derivativeControlPoints.push(PointCal.multiplyVectorByScalar(PointCal.subVector(controlPoints[index], controlPoints[index - 1]), controlPoints.length - 1));
122
- }
123
- return derivativeControlPoints;
124
- }
125
- validateTVal(tVal) {
126
- if (tVal > 1 || tVal < 0) {
127
- throw new TValOutofBoundError("tVal is greater than 1 or less than 0");
128
- }
129
- }
130
- getControlPoints() {
131
- return this.controlPoints;
132
- }
133
- setControlPoints(controlPoints) {
134
- this.controlPoints = controlPoints;
135
- this.dControlPoints = this.getDerivativeControlPoints(this.controlPoints);
136
- this._fullLength = this.calculateFullLength();
137
- // Reset LUT to trigger lazy computation when needed
138
- this.arcLengthLUT = { controlPoints: [], arcLengthLUT: [] };
139
- this.clearCache(); // Clear cache on control point change
57
+ controlPoints;
58
+ dControlPoints = [];
59
+ arcLengthLUT = { controlPoints: [], arcLengthLUT: [] };
60
+ _fullLength;
61
+ lengthCache = new Map;
62
+ getCacheStats() {
63
+ return {
64
+ size: this.lengthCache.size,
65
+ hitRate: 0
66
+ };
67
+ }
68
+ preWarmCache(steps = 100) {
69
+ const tSteps = 1 / steps;
70
+ for (let tVal = 0;tVal <= 1; tVal += tSteps) {
71
+ this.lengthAtT(tVal);
72
+ }
73
+ }
74
+ clearCache() {
75
+ this.lengthCache.clear();
76
+ }
77
+ constructor(controlPoints) {
78
+ this.controlPoints = controlPoints;
79
+ this.dControlPoints = this.getDerivativeControlPoints(this.controlPoints);
80
+ this._fullLength = this.calculateFullLength();
81
+ this.arcLengthLUT = { controlPoints: [], arcLengthLUT: [] };
82
+ this.clearCache();
83
+ }
84
+ getPointbyPercentage(percentage) {
85
+ let controlPointsChangedSinceLastArcLengthLUT = this.arcLengthLUT.controlPoints.length != this.controlPoints.length;
86
+ controlPointsChangedSinceLastArcLengthLUT = controlPointsChangedSinceLastArcLengthLUT || this.arcLengthLUT.controlPoints.reduce((prevVal, curVal, index) => {
87
+ return prevVal || !PointCal.isEqual(curVal, this.controlPoints[index]);
88
+ }, false);
89
+ let points = [];
90
+ if (controlPointsChangedSinceLastArcLengthLUT) {
91
+ this.arcLengthLUT = this.getArcLengthLUT(1000);
92
+ }
93
+ points = [...this.arcLengthLUT.arcLengthLUT.map((item) => item.length)];
94
+ let targetLength = percentage * this.fullLength;
95
+ let low = 0;
96
+ let high = points.length - 1;
97
+ while (low <= high) {
98
+ let mid = Math.floor((low + high) / 2);
99
+ if (points[mid] == targetLength) {
100
+ return this.get((mid + 1) / points.length);
101
+ } else if (points[mid] < targetLength) {
102
+ low = mid + 1;
103
+ } else {
104
+ high = mid - 1;
105
+ }
106
+ }
107
+ return low >= points.length ? this.get(1) : this.get((low + 1) / points.length);
108
+ }
109
+ getDerivativeControlPoints(controlPoints) {
110
+ const derivativeControlPoints = [];
111
+ for (let index = 1;index < controlPoints.length; index++) {
112
+ derivativeControlPoints.push(PointCal.multiplyVectorByScalar(PointCal.subVector(controlPoints[index], controlPoints[index - 1]), controlPoints.length - 1));
113
+ }
114
+ return derivativeControlPoints;
115
+ }
116
+ validateTVal(tVal) {
117
+ if (tVal > 1 || tVal < 0) {
118
+ throw new TValOutofBoundError("tVal is greater than 1 or less than 0");
119
+ }
120
+ }
121
+ getControlPoints() {
122
+ return this.controlPoints;
123
+ }
124
+ setControlPoints(controlPoints) {
125
+ this.controlPoints = controlPoints;
126
+ this.dControlPoints = this.getDerivativeControlPoints(this.controlPoints);
127
+ this._fullLength = this.calculateFullLength();
128
+ this.arcLengthLUT = { controlPoints: [], arcLengthLUT: [] };
129
+ this.clearCache();
130
+ }
131
+ setControlPointAtIndex(index, newPoint) {
132
+ if (index < 0 || index >= this.controlPoints.length) {
133
+ return false;
134
+ }
135
+ this.controlPoints[index] = newPoint;
136
+ this.dControlPoints = this.getDerivativeControlPoints(this.controlPoints);
137
+ this._fullLength = this.calculateFullLength();
138
+ this.arcLengthLUT = { controlPoints: [], arcLengthLUT: [] };
139
+ this.clearCache();
140
+ return true;
141
+ }
142
+ compute(tVal) {
143
+ this.validateTVal(tVal);
144
+ let points = this.controlPoints;
145
+ while (points.length > 1) {
146
+ let lowerLevelPoints = points.slice(1);
147
+ for (let index = 0;index < lowerLevelPoints.length; index++) {
148
+ lowerLevelPoints[index] = PointCal.addVector(PointCal.multiplyVectorByScalar(points[index], 1 - tVal), PointCal.multiplyVectorByScalar(points[index + 1], tVal));
149
+ }
150
+ points = lowerLevelPoints;
140
151
  }
141
- setControlPointAtIndex(index, newPoint) {
142
- if (index < 0 || index >= this.controlPoints.length) {
143
- return false;
144
- }
145
- this.controlPoints[index] = newPoint;
146
- this.dControlPoints = this.getDerivativeControlPoints(this.controlPoints);
147
- this._fullLength = this.calculateFullLength();
148
- // Reset LUT to trigger lazy computation when needed
149
- this.arcLengthLUT = { controlPoints: [], arcLengthLUT: [] };
150
- this.clearCache(); // Clear cache on control point change
151
- return true;
152
- }
153
- compute(tVal) {
154
- this.validateTVal(tVal);
155
- let points = this.controlPoints;
156
- while (points.length > 1) {
157
- let lowerLevelPoints = points.slice(1);
158
- for (let index = 0; index < lowerLevelPoints.length; index++) {
159
- lowerLevelPoints[index] = PointCal.addVector(PointCal.multiplyVectorByScalar(points[index], (1 - tVal)), PointCal.multiplyVectorByScalar(points[index + 1], tVal));
160
- }
161
- points = lowerLevelPoints;
162
- }
163
- return points[0];
164
- }
165
- get(tVal) {
166
- this.validateTVal(tVal);
167
- if (this.controlPoints.length == 3) {
168
- let firstTerm = PointCal.multiplyVectorByScalar(this.controlPoints[0], (1 - tVal) * (1 - tVal));
169
- let secondTerm = PointCal.multiplyVectorByScalar(this.controlPoints[1], 2 * (1 - tVal) * tVal);
170
- let thirdTerm = PointCal.multiplyVectorByScalar(this.controlPoints[2], tVal * tVal);
171
- let res = PointCal.addVector(PointCal.addVector(firstTerm, secondTerm), thirdTerm);
172
- return res;
173
- }
174
- if (this.controlPoints.length == 4) {
175
- let firstTerm = PointCal.multiplyVectorByScalar(this.controlPoints[0], (1 - tVal) * (1 - tVal) * (1 - tVal));
176
- let secondTerm = PointCal.multiplyVectorByScalar(this.controlPoints[1], 3 * (1 - tVal) * (1 - tVal) * tVal);
177
- let thirdTerm = PointCal.multiplyVectorByScalar(this.controlPoints[2], 3 * (1 - tVal) * tVal * tVal);
178
- let forthTerm = PointCal.multiplyVectorByScalar(this.controlPoints[3], tVal * tVal * tVal);
179
- let res = PointCal.addVector(PointCal.addVector(firstTerm, secondTerm), PointCal.addVector(thirdTerm, forthTerm));
180
- return res;
181
- }
182
- return this.compute(tVal);
183
- }
184
- getLUT(steps = 100) {
185
- const stepSpan = 1 / steps;
186
- const res = [];
187
- let tVal = 0;
188
- res.push(this.get(tVal));
189
- for (let index = 0; index < steps; index += 1) {
190
- tVal += stepSpan;
191
- if ((tVal > 1 && tVal - stepSpan < 1) || index == steps - 1) {
192
- tVal = 1;
193
- }
194
- res.push(this.get(tVal));
195
- }
196
- return res;
152
+ return points[0];
153
+ }
154
+ get(tVal) {
155
+ this.validateTVal(tVal);
156
+ if (this.controlPoints.length == 3) {
157
+ let firstTerm = PointCal.multiplyVectorByScalar(this.controlPoints[0], (1 - tVal) * (1 - tVal));
158
+ let secondTerm = PointCal.multiplyVectorByScalar(this.controlPoints[1], 2 * (1 - tVal) * tVal);
159
+ let thirdTerm = PointCal.multiplyVectorByScalar(this.controlPoints[2], tVal * tVal);
160
+ let res = PointCal.addVector(PointCal.addVector(firstTerm, secondTerm), thirdTerm);
161
+ return res;
162
+ }
163
+ if (this.controlPoints.length == 4) {
164
+ let firstTerm = PointCal.multiplyVectorByScalar(this.controlPoints[0], (1 - tVal) * (1 - tVal) * (1 - tVal));
165
+ let secondTerm = PointCal.multiplyVectorByScalar(this.controlPoints[1], 3 * (1 - tVal) * (1 - tVal) * tVal);
166
+ let thirdTerm = PointCal.multiplyVectorByScalar(this.controlPoints[2], 3 * (1 - tVal) * tVal * tVal);
167
+ let forthTerm = PointCal.multiplyVectorByScalar(this.controlPoints[3], tVal * tVal * tVal);
168
+ let res = PointCal.addVector(PointCal.addVector(firstTerm, secondTerm), PointCal.addVector(thirdTerm, forthTerm));
169
+ return res;
170
+ }
171
+ return this.compute(tVal);
172
+ }
173
+ getLUT(steps = 100) {
174
+ const stepSpan = 1 / steps;
175
+ const res = [];
176
+ let tVal = 0;
177
+ res.push(this.get(tVal));
178
+ for (let index = 0;index < steps; index += 1) {
179
+ tVal += stepSpan;
180
+ if (tVal > 1 && tVal - stepSpan < 1 || index == steps - 1) {
181
+ tVal = 1;
182
+ }
183
+ res.push(this.get(tVal));
197
184
  }
198
- getLUTWithTVal(steps) {
199
- if (steps == undefined) {
200
- steps = 100;
201
- }
202
- const stepSpan = 1 / steps;
203
- const res = [];
204
- let tVal = 0;
205
- res.push({ point: this.get(tVal), tVal: tVal });
206
- for (let index = 0; index < steps; index += 1) {
207
- tVal += stepSpan;
208
- if ((tVal > 1 && tVal - stepSpan < 1) || index == steps - 1) {
209
- tVal = 1;
210
- }
211
- res.push({ point: this.get(tVal), tVal: tVal });
212
- }
213
- return res;
185
+ return res;
186
+ }
187
+ getLUTWithTVal(steps) {
188
+ if (steps == undefined) {
189
+ steps = 100;
190
+ }
191
+ const stepSpan = 1 / steps;
192
+ const res = [];
193
+ let tVal = 0;
194
+ res.push({ point: this.get(tVal), tVal });
195
+ for (let index = 0;index < steps; index += 1) {
196
+ tVal += stepSpan;
197
+ if (tVal > 1 && tVal - stepSpan < 1 || index == steps - 1) {
198
+ tVal = 1;
199
+ }
200
+ res.push({ point: this.get(tVal), tVal });
214
201
  }
215
- get fullLength() {
216
- return this._fullLength;
202
+ return res;
203
+ }
204
+ get fullLength() {
205
+ return this._fullLength;
206
+ }
207
+ calculateFullLength() {
208
+ return this.lengthAtT(1);
209
+ }
210
+ lengthAtT(tVal) {
211
+ this.validateTVal(tVal);
212
+ const cacheKey = Math.round(tVal * 1e6) / 1e6;
213
+ if (this.lengthCache.has(cacheKey)) {
214
+ return this.lengthCache.get(cacheKey);
215
+ }
216
+ const z = tVal / 2, len = T.length;
217
+ let sum = 0;
218
+ for (let i = 0, t;i < len; i++) {
219
+ t = z * T[i] + z;
220
+ sum += C[i] * PointCal.magnitude(this.derivative(t));
221
+ }
222
+ const result = z * sum;
223
+ this.lengthCache.set(cacheKey, result);
224
+ return result;
225
+ }
226
+ derivative(tVal) {
227
+ return computeWithControlPoints(tVal, this.dControlPoints);
228
+ }
229
+ derivativeNormalized(tVal) {
230
+ return PointCal.unitVector(computeWithControlPoints(tVal, this.dControlPoints));
231
+ }
232
+ getArcLengthLUT(steps = 50) {
233
+ const controlPointsChanged = this.arcLengthLUT.controlPoints.length !== this.controlPoints.length || this.arcLengthLUT.controlPoints.some((cp, index) => !PointCal.isEqual(cp, this.controlPoints[index]));
234
+ if (controlPointsChanged || this.arcLengthLUT.arcLengthLUT.length === 0) {
235
+ this.clearCache();
236
+ let res = [];
237
+ let tSteps = 1 / steps;
238
+ for (let tVal = 0;tVal <= 1; tVal += tSteps) {
239
+ res.push({ tVal, length: this.lengthAtT(tVal) });
240
+ }
241
+ this.arcLengthLUT = { controlPoints: [...this.controlPoints], arcLengthLUT: res };
242
+ }
243
+ return this.arcLengthLUT;
244
+ }
245
+ splitIntoCurves(tVal) {
246
+ const res = this.split(tVal);
247
+ return [new BCurve(res[0]), new BCurve(res[1])];
248
+ }
249
+ split(tVal) {
250
+ this.validateTVal(tVal);
251
+ if (this.controlPoints.length == 3) {
252
+ let newControlPoint12 = this.controlPoints[0];
253
+ let newControlPoint22 = PointCal.subVector(PointCal.multiplyVectorByScalar(this.controlPoints[1], tVal), PointCal.multiplyVectorByScalar(this.controlPoints[0], tVal - 1));
254
+ let newControlPoint32 = PointCal.subVector(PointCal.multiplyVectorByScalar(this.controlPoints[2], tVal * tVal), PointCal.multiplyVectorByScalar(this.controlPoints[1], 2 * tVal * (tVal - 1)));
255
+ newControlPoint32 = PointCal.addVector(newControlPoint32, PointCal.multiplyVectorByScalar(this.controlPoints[0], (tVal - 1) * (tVal - 1)));
256
+ let newControlPoint42 = PointCal.subVector(PointCal.multiplyVectorByScalar(this.controlPoints[2], tVal), PointCal.multiplyVectorByScalar(this.controlPoints[1], tVal - 1));
257
+ let newControlPoint52 = this.controlPoints[2];
258
+ return [[newControlPoint12, newControlPoint22, newControlPoint32], [newControlPoint32, newControlPoint42, newControlPoint52]];
259
+ }
260
+ let newControlPoint1 = this.controlPoints[0];
261
+ let newControlPoint2 = PointCal.subVector(PointCal.multiplyVectorByScalar(this.controlPoints[1], tVal), PointCal.multiplyVectorByScalar(this.controlPoints[0], tVal - 1));
262
+ 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))));
263
+ let term1 = PointCal.multiplyVectorByScalar(this.controlPoints[3], tVal * tVal * tVal);
264
+ let term2 = PointCal.multiplyVectorByScalar(this.controlPoints[2], -(3 * tVal * tVal * (tVal - 1)));
265
+ let term3 = PointCal.multiplyVectorByScalar(this.controlPoints[1], 3 * tVal * (tVal - 1) * (tVal - 1));
266
+ let term4 = PointCal.multiplyVectorByScalar(this.controlPoints[0], -((tVal - 1) * (tVal - 1) * (tVal - 1)));
267
+ let newControlPoint4 = PointCal.addVector(term4, PointCal.addVector(term3, PointCal.addVector(term1, term2)));
268
+ 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)));
269
+ let newControlPoint6 = PointCal.addVector(PointCal.multiplyVectorByScalar(this.controlPoints[3], tVal), PointCal.multiplyVectorByScalar(this.controlPoints[2], -(tVal - 1)));
270
+ let newControlPoint7 = this.controlPoints[3];
271
+ return [[newControlPoint1, newControlPoint2, newControlPoint3, newControlPoint4], [newControlPoint4, newControlPoint5, newControlPoint6, newControlPoint7]];
272
+ }
273
+ splitIn3WithControlPoints(tVal, tVal2) {
274
+ if (tVal2 < tVal) {
275
+ console.warn("tVal2 is less than tVal, swapping them");
276
+ [tVal, tVal2] = [tVal2, tVal];
277
+ }
278
+ const firstSplit = this.split(tVal);
279
+ const secondHalf = new BCurve(firstSplit[1]);
280
+ const mappedTVal2 = map(tVal2, tVal, 1, 0, 1);
281
+ const secondSplit = secondHalf.split(mappedTVal2);
282
+ return [firstSplit[0], secondSplit[0], secondSplit[1]];
283
+ }
284
+ splitIn3Curves(tVal, tVal2) {
285
+ if (tVal2 < tVal) {
286
+ console.warn("tVal2 is less than tVal, swapping them");
287
+ [tVal, tVal2] = [tVal2, tVal];
288
+ }
289
+ const firstSplit = this.split(tVal);
290
+ const secondHalf = new BCurve(firstSplit[1]);
291
+ const mappedTVal2 = map(tVal2, tVal, 1, 0, 1);
292
+ const secondSplit = secondHalf.split(mappedTVal2);
293
+ return [new BCurve(firstSplit[0]), new BCurve(secondSplit[0]), new BCurve(secondSplit[1])];
294
+ }
295
+ splitAndTakeMidCurve(tVal, tVal2) {
296
+ const [firstSplit, secondSplit, thirdSplit] = this.splitIn3Curves(tVal, tVal2);
297
+ return secondSplit;
298
+ }
299
+ getProjection(point) {
300
+ const threshold = 0.00001;
301
+ let distance = Number.MAX_VALUE;
302
+ let preliminaryProjectionTVal = 0;
303
+ let preliminaryProjectionPoint = this.get(0);
304
+ let preliminaryProjectionIndex = 0;
305
+ const LUT = this.getLUTWithTVal(500);
306
+ LUT.forEach((curvePoint, index) => {
307
+ const curDistance = PointCal.distanceBetweenPoints(curvePoint.point, point);
308
+ if (curDistance < distance) {
309
+ distance = curDistance;
310
+ preliminaryProjectionPoint = { ...curvePoint.point };
311
+ preliminaryProjectionTVal = curvePoint.tVal;
312
+ preliminaryProjectionIndex = index;
313
+ }
314
+ });
315
+ let low = LUT[preliminaryProjectionIndex].tVal;
316
+ let high = LUT[preliminaryProjectionIndex].tVal;
317
+ if (preliminaryProjectionIndex < LUT.length - 1) {
318
+ high = LUT[preliminaryProjectionIndex + 1].tVal;
319
+ }
320
+ if (preliminaryProjectionIndex > 0) {
321
+ low = LUT[preliminaryProjectionIndex - 1].tVal;
322
+ }
323
+ while (low < high && high - low > threshold) {
324
+ let mid = low + (high - low) / 2;
325
+ let halfSpan = mid - low;
326
+ let lowMidMid = mid + halfSpan / 2;
327
+ let highMidMid = mid + halfSpan / 2;
328
+ let prevDist = distance;
329
+ if (lowMidMid <= 1 && lowMidMid >= 0) {
330
+ let curDist = PointCal.distanceBetweenPoints(this.get(lowMidMid), point);
331
+ if (curDist < distance) {
332
+ distance = curDist;
333
+ preliminaryProjectionPoint = this.get(lowMidMid);
334
+ preliminaryProjectionTVal = lowMidMid;
335
+ high = lowMidMid + halfSpan / 2;
336
+ low = lowMidMid - halfSpan / 2;
337
+ }
338
+ }
339
+ if (highMidMid <= 1 && highMidMid >= 0) {
340
+ let curDist = PointCal.distanceBetweenPoints(this.get(highMidMid), point);
341
+ if (curDist < distance) {
342
+ distance = curDist;
343
+ preliminaryProjectionPoint = this.get(highMidMid);
344
+ preliminaryProjectionTVal = highMidMid;
345
+ high = highMidMid + halfSpan / 2;
346
+ low = highMidMid - halfSpan / 2;
347
+ }
348
+ }
349
+ if (prevDist == distance) {
350
+ break;
351
+ }
352
+ }
353
+ return { projection: preliminaryProjectionPoint, tVal: preliminaryProjectionTVal };
354
+ }
355
+ findArcs(errorThreshold) {
356
+ let low = 0;
357
+ const res = [];
358
+ while (low < 1) {
359
+ let loopRes = this.findArcStartingAt(errorThreshold, low);
360
+ if (loopRes == null || loopRes.arc == undefined) {
361
+ break;
362
+ }
363
+ res.push(loopRes.arc);
364
+ low = loopRes.arc.endT;
365
+ if (low >= 1) {
366
+ break;
367
+ }
217
368
  }
218
- calculateFullLength() {
219
- return this.lengthAtT(1);
369
+ return res;
370
+ }
371
+ findArcStartingAt(errorThreshold, low) {
372
+ let high = 1;
373
+ let mid = low + (high - low) / 2;
374
+ let prevArc = { good: false };
375
+ let count = 0;
376
+ while (true) {
377
+ count++;
378
+ mid = low + (high - low) / 2;
379
+ if (high > 1 || mid > 1) {
380
+ if (prevArc.good) {
381
+ return prevArc;
382
+ } else {
383
+ return null;
384
+ }
385
+ }
386
+ const lowPoint = this.get(low);
387
+ const highPoint = this.get(high);
388
+ const midPoint = this.get(mid);
389
+ const fitArcRes = this.fitArc(lowPoint, highPoint, midPoint);
390
+ if (!fitArcRes.exists || fitArcRes.center == null || fitArcRes.radius == null) {
391
+ return null;
392
+ }
393
+ const n = high - mid;
394
+ const e1 = mid - n / 2;
395
+ const e2 = mid + n / 2;
396
+ const checkPoint1 = this.get(e1);
397
+ const checkPoint2 = this.get(e2);
398
+ const checkRadius = PointCal.distanceBetweenPoints(checkPoint1, fitArcRes.center);
399
+ const checkRadius2 = PointCal.distanceBetweenPoints(checkPoint2, fitArcRes.center);
400
+ if (Math.abs(checkRadius - fitArcRes.radius) > errorThreshold || Math.abs(checkRadius2 - fitArcRes.radius) > errorThreshold) {
401
+ if (prevArc.good == true) {
402
+ return prevArc;
403
+ }
404
+ prevArc.good = false;
405
+ high = mid;
406
+ } else {
407
+ prevArc.good = true;
408
+ if (fitArcRes.startPoint !== undefined && fitArcRes.endPoint !== undefined) {
409
+ prevArc.arc = { center: fitArcRes.center, radius: fitArcRes.radius, startPoint: fitArcRes.startPoint, endPoint: fitArcRes.endPoint, startT: low, endT: high };
410
+ }
411
+ high = high + (mid - low);
412
+ }
413
+ }
414
+ }
415
+ fitArc(startPoint, endPoint, midPoint) {
416
+ const M11 = [[startPoint.x, startPoint.y, 1], [midPoint.x, midPoint.y, 1], [endPoint.x, endPoint.y, 1]];
417
+ if (this.determinant3by3(M11) == 0) {
418
+ return { exists: false };
419
+ }
420
+ const M12 = [
421
+ [startPoint.x * startPoint.x + startPoint.y * startPoint.y, startPoint.y, 1],
422
+ [midPoint.x * midPoint.x + midPoint.y * midPoint.y, midPoint.y, 1],
423
+ [endPoint.x * endPoint.x + endPoint.y * endPoint.y, endPoint.y, 1]
424
+ ];
425
+ const M13 = [
426
+ [startPoint.x * startPoint.x + startPoint.y * startPoint.y, startPoint.x, 1],
427
+ [midPoint.x * midPoint.x + midPoint.y * midPoint.y, midPoint.x, 1],
428
+ [endPoint.x * endPoint.x + endPoint.y * endPoint.y, endPoint.x, 1]
429
+ ];
430
+ const M14 = [
431
+ [startPoint.x * startPoint.x + startPoint.y * startPoint.y, startPoint.x, startPoint.y],
432
+ [midPoint.x * midPoint.x + midPoint.y * midPoint.y, midPoint.x, midPoint.y],
433
+ [endPoint.x * endPoint.x + endPoint.y * endPoint.y, endPoint.x, endPoint.y]
434
+ ];
435
+ const centerX = 1 / 2 * (this.determinant3by3(M12) / this.determinant3by3(M11));
436
+ const centerY = -1 / 2 * (this.determinant3by3(M13) / this.determinant3by3(M11));
437
+ const radius = Math.sqrt(centerX * centerX + centerY * centerY + this.determinant3by3(M14) / this.determinant3by3(M11));
438
+ return { exists: true, center: { x: centerX, y: centerY }, radius, startPoint, endPoint };
439
+ }
440
+ determinant3by3(matrix) {
441
+ const a = matrix[0][0];
442
+ const b = matrix[0][1];
443
+ const c = matrix[0][2];
444
+ const d = matrix[1][0];
445
+ const e = matrix[1][1];
446
+ const f = matrix[1][2];
447
+ const g = matrix[2][0];
448
+ const h = matrix[2][1];
449
+ const i = matrix[2][2];
450
+ return a * (e * i - f * h) - b * (d * i - g * f) + c * (d * h - e * g);
451
+ }
452
+ curvature(tVal) {
453
+ const derivative = computeWithControlPoints(tVal, this.dControlPoints);
454
+ const secondDerivative = computeWithControlPoints(tVal, this.getDerivativeControlPoints(this.dControlPoints));
455
+ const numerator = derivative.x * secondDerivative.y - secondDerivative.x * derivative.y;
456
+ const denominator = Math.pow(derivative.x * derivative.x + derivative.y * derivative.y, 3 / 2);
457
+ if (denominator == 0)
458
+ return NaN;
459
+ return numerator / denominator;
460
+ }
461
+ secondDerivative(tVal) {
462
+ return computeWithControlPoints(tVal, this.getDerivativeControlPoints(this.dControlPoints));
463
+ }
464
+ getCoefficientOfTTerms() {
465
+ return this.getCoefficientOfTTermsWithControlPoints(this.controlPoints);
466
+ }
467
+ getDerivativeCoefficients() {
468
+ return this.getCoefficientOfTTermsWithControlPoints(this.dControlPoints);
469
+ }
470
+ getCoefficientOfTTermsWithControlPoints(controlPoints) {
471
+ const terms = [];
472
+ let matrix = [];
473
+ if (controlPoints.length == 3) {
474
+ matrix = [[1, 0, 0], [-2, 2, 0], [1, -2, 1]];
475
+ } else if (controlPoints.length == 4) {
476
+ matrix = [[1, 0, 0, 0], [-3, 3, 0, 0], [3, -6, 3, 0], [-1, 3, -3, 1]];
477
+ } else if (controlPoints.length == 2) {
478
+ matrix = [[1, 0], [-1, 1]];
479
+ } else {
480
+ throw new Error("number of control points is wrong");
481
+ }
482
+ for (let index = 0;index < controlPoints.length; index++) {
483
+ terms.push(controlPoints.reduce((prevVal, curVal, jindex) => {
484
+ return { x: prevVal.x + matrix[index][jindex] * curVal.x, y: prevVal.y + matrix[index][jindex] * curVal.y };
485
+ }, { x: 0, y: 0 }));
486
+ }
487
+ return terms;
488
+ }
489
+ getControlPointsAlignedWithXAxis() {
490
+ const alignedAxis = PointCal.unitVectorFromA2B(this.controlPoints[0], this.controlPoints[this.controlPoints.length - 1]);
491
+ const angle = PointCal.angleFromA2B({ x: 1, y: 0 }, alignedAxis);
492
+ const startingPoint = this.controlPoints[0];
493
+ const res = [{ x: 0, y: 0 }];
494
+ for (let index = 1;index < this.controlPoints.length; index++) {
495
+ const vector = PointCal.subVector(this.controlPoints[index], startingPoint);
496
+ const rotatedVector = PointCal.rotatePoint(vector, -angle);
497
+ res.push(rotatedVector);
220
498
  }
221
- lengthAtT(tVal) {
222
- this.validateTVal(tVal);
223
- // Check cache first
224
- const cacheKey = Math.round(tVal * 1000000) / 1000000; // Round to 6 decimal places for cache key
225
- if (this.lengthCache.has(cacheKey)) {
226
- return this.lengthCache.get(cacheKey);
227
- }
228
- const z = tVal / 2, len = T.length;
229
- let sum = 0;
230
- for (let i = 0, t; i < len; i++) {
231
- t = z * T[i] + z;
232
- sum += C[i] * PointCal.magnitude(this.derivative(t));
233
- }
234
- const result = z * sum;
235
- // Cache the result
236
- this.lengthCache.set(cacheKey, result);
237
- return result;
238
- }
239
- derivative(tVal) {
240
- return computeWithControlPoints(tVal, this.dControlPoints);
241
- }
242
- derivativeNormalized(tVal) {
243
- return PointCal.unitVector(computeWithControlPoints(tVal, this.dControlPoints));
244
- }
245
- getArcLengthLUT(steps = 50) {
246
- // Check if we need to recompute the LUT
247
- const controlPointsChanged = this.arcLengthLUT.controlPoints.length !== this.controlPoints.length ||
248
- this.arcLengthLUT.controlPoints.some((cp, index) => !PointCal.isEqual(cp, this.controlPoints[index]));
249
- if (controlPointsChanged || this.arcLengthLUT.arcLengthLUT.length === 0) {
250
- // Clear cache when regenerating LUT to ensure consistency
251
- this.clearCache();
252
- let res = [];
253
- let tSteps = 1 / steps;
254
- for (let tVal = 0; tVal <= 1; tVal += tSteps) {
255
- res.push({ tVal: tVal, length: this.lengthAtT(tVal) });
256
- }
257
- this.arcLengthLUT = { controlPoints: [...this.controlPoints], arcLengthLUT: res };
258
- }
259
- return this.arcLengthLUT;
260
- }
261
- split(tVal) {
262
- this.validateTVal(tVal);
263
- if (this.controlPoints.length == 3) {
264
- let newControlPoint1 = this.controlPoints[0];
265
- let newControlPoint2 = PointCal.subVector(PointCal.multiplyVectorByScalar(this.controlPoints[1], tVal), PointCal.multiplyVectorByScalar(this.controlPoints[0], tVal - 1));
266
- let newControlPoint3 = PointCal.subVector(PointCal.multiplyVectorByScalar(this.controlPoints[2], tVal * tVal), PointCal.multiplyVectorByScalar(this.controlPoints[1], 2 * tVal * (tVal - 1)));
267
- newControlPoint3 = PointCal.addVector(newControlPoint3, PointCal.multiplyVectorByScalar(this.controlPoints[0], (tVal - 1) * (tVal - 1)));
268
- let newControlPoint4 = PointCal.subVector(PointCal.multiplyVectorByScalar(this.controlPoints[2], tVal), PointCal.multiplyVectorByScalar(this.controlPoints[1], tVal - 1));
269
- let newControlPoint5 = this.controlPoints[2];
270
- return [[newControlPoint1, newControlPoint2, newControlPoint3], [newControlPoint3, newControlPoint4, newControlPoint5]];
271
- }
272
- let newControlPoint1 = this.controlPoints[0];
273
- let newControlPoint2 = PointCal.subVector(PointCal.multiplyVectorByScalar(this.controlPoints[1], tVal), PointCal.multiplyVectorByScalar(this.controlPoints[0], (tVal - 1)));
274
- 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))));
275
- let term1 = PointCal.multiplyVectorByScalar(this.controlPoints[3], tVal * tVal * tVal);
276
- let term2 = PointCal.multiplyVectorByScalar(this.controlPoints[2], -(3 * tVal * tVal * (tVal - 1)));
277
- let term3 = PointCal.multiplyVectorByScalar(this.controlPoints[1], 3 * tVal * (tVal - 1) * (tVal - 1));
278
- let term4 = PointCal.multiplyVectorByScalar(this.controlPoints[0], -((tVal - 1) * (tVal - 1) * (tVal - 1)));
279
- let newControlPoint4 = PointCal.addVector(term4, PointCal.addVector(term3, PointCal.addVector(term1, term2)));
280
- 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)));
281
- let newControlPoint6 = PointCal.addVector(PointCal.multiplyVectorByScalar(this.controlPoints[3], tVal), PointCal.multiplyVectorByScalar(this.controlPoints[2], -(tVal - 1)));
282
- let newControlPoint7 = this.controlPoints[3];
283
- return [[newControlPoint1, newControlPoint2, newControlPoint3, newControlPoint4], [newControlPoint4, newControlPoint5, newControlPoint6, newControlPoint7]];
284
- }
285
- splitIn3WithControlPoints(tVal, tVal2) {
286
- if (tVal2 < tVal) {
287
- console.warn("tVal2 is less than tVal, swapping them");
288
- [tVal, tVal2] = [tVal2, tVal];
289
- }
290
- const firstSplit = this.split(tVal);
291
- const secondHalf = new BCurve(firstSplit[1]);
292
- const secondSplit = secondHalf.split(tVal2);
293
- return [firstSplit[0], secondSplit[0], secondSplit[1]];
294
- }
295
- splitIn3Curves(tVal, tVal2) {
296
- if (tVal2 < tVal) {
297
- console.warn("tVal2 is less than tVal, swapping them");
298
- [tVal, tVal2] = [tVal2, tVal];
299
- }
300
- const firstSplit = this.split(tVal);
301
- const secondHalf = new BCurve(firstSplit[1]);
302
- const secondSplit = secondHalf.split(tVal2);
303
- return [new BCurve(firstSplit[0]), new BCurve(secondSplit[0]), new BCurve(secondSplit[1])];
304
- }
305
- splitAndTakeMidCurve(tVal, tVal2) {
306
- const [firstSplit, secondSplit, thirdSplit] = this.splitIn3Curves(tVal, tVal2);
307
- return secondSplit;
308
- }
309
- getProjection(point) {
310
- const threshold = 0.00001;
311
- let distance = Number.MAX_VALUE;
312
- let preliminaryProjectionTVal = 0;
313
- let preliminaryProjectionPoint = this.get(0);
314
- let preliminaryProjectionIndex = 0;
315
- const LUT = this.getLUTWithTVal(500);
316
- LUT.forEach((curvePoint, index) => {
317
- const curDistance = PointCal.distanceBetweenPoints(curvePoint.point, point);
318
- if (curDistance < distance) {
319
- distance = curDistance;
320
- preliminaryProjectionPoint = { ...curvePoint.point };
321
- preliminaryProjectionTVal = curvePoint.tVal;
322
- preliminaryProjectionIndex = index;
323
- }
324
- });
325
- // console.log(preliminaryProjectionIndex, preliminaryProjectionPoint, preliminaryProjectionTVal);
326
- let low = LUT[preliminaryProjectionIndex].tVal;
327
- let high = LUT[preliminaryProjectionIndex].tVal;
328
- if (preliminaryProjectionIndex < LUT.length - 1) {
329
- high = LUT[preliminaryProjectionIndex + 1].tVal;
330
- }
331
- if (preliminaryProjectionIndex > 0) {
332
- low = LUT[preliminaryProjectionIndex - 1].tVal;
333
- }
334
- while (low < high && high - low > threshold) {
335
- let mid = low + (high - low) / 2;
336
- let halfSpan = mid - low;
337
- let lowMidMid = mid + halfSpan / 2;
338
- let highMidMid = mid + halfSpan / 2;
339
- let prevDist = distance;
340
- if (lowMidMid <= 1 && lowMidMid >= 0) {
341
- let curDist = PointCal.distanceBetweenPoints(this.get(lowMidMid), point);
342
- if (curDist < distance) {
343
- distance = curDist;
344
- preliminaryProjectionPoint = this.get(lowMidMid);
345
- preliminaryProjectionTVal = lowMidMid;
346
- high = lowMidMid + halfSpan / 2;
347
- low = lowMidMid - halfSpan / 2;
348
- }
349
- }
350
- if (highMidMid <= 1 && highMidMid >= 0) {
351
- let curDist = PointCal.distanceBetweenPoints(this.get(highMidMid), point);
352
- if (curDist < distance) {
353
- distance = curDist;
354
- preliminaryProjectionPoint = this.get(highMidMid);
355
- preliminaryProjectionTVal = highMidMid;
356
- high = highMidMid + halfSpan / 2;
357
- low = highMidMid - halfSpan / 2;
358
- }
359
- }
360
- if (prevDist == distance) {
361
- break;
362
- }
363
- }
364
- return { projection: preliminaryProjectionPoint, tVal: preliminaryProjectionTVal };
365
- }
366
- findArcs(errorThreshold) {
367
- let low = 0;
368
- const res = [];
369
- while (low < 1) {
370
- let loopRes = this.findArcStartingAt(errorThreshold, low);
371
- if (loopRes == null || loopRes.arc == undefined) {
372
- break;
373
- }
374
- res.push(loopRes.arc);
375
- low = loopRes.arc.endT;
376
- if (low >= 1) {
377
- break;
378
- }
379
- }
380
- return res;
381
- }
382
- findArcStartingAt(errorThreshold, low) {
383
- let high = 1;
384
- let mid = low + (high - low) / 2;
385
- let prevArc = { good: false };
386
- while (true) {
387
- mid = low + (high - low) / 2;
388
- if (high > 1 || mid > 1) {
389
- if (prevArc.good) {
390
- return prevArc;
391
- }
392
- else {
393
- return null;
394
- }
395
- }
396
- const lowPoint = this.get(low);
397
- const highPoint = this.get(high);
398
- const midPoint = this.get(mid);
399
- const fitArcRes = this.fitArc(lowPoint, highPoint, midPoint);
400
- if (!fitArcRes.exists || fitArcRes.center == null || fitArcRes.radius == null) {
401
- return null;
402
- }
403
- const n = high - mid;
404
- const e1 = mid - n / 2;
405
- const e2 = mid + n / 2;
406
- const checkPoint1 = this.get(e1);
407
- const checkPoint2 = this.get(e2);
408
- const checkRadius = PointCal.distanceBetweenPoints(checkPoint1, fitArcRes.center);
409
- const checkRadius2 = PointCal.distanceBetweenPoints(checkPoint2, fitArcRes.center);
410
- if (Math.abs(checkRadius - fitArcRes.radius) > errorThreshold || Math.abs(checkRadius2 - fitArcRes.radius) > errorThreshold) {
411
- // arc is bad
412
- if (prevArc.good == true) {
413
- return prevArc;
414
- }
415
- prevArc.good = false;
416
- high = mid;
417
- }
418
- else {
419
- prevArc.good = true;
420
- if (fitArcRes.startPoint !== undefined && fitArcRes.endPoint !== undefined) {
421
- prevArc.arc = { center: fitArcRes.center, radius: fitArcRes.radius, startPoint: fitArcRes.startPoint, endPoint: fitArcRes.endPoint, startT: low, endT: high };
422
- }
423
- high = high + (mid - low);
424
- }
425
- }
499
+ return res;
500
+ }
501
+ getExtrema() {
502
+ const res = { x: [], y: [] };
503
+ const derivativeCoefficients = this.getDerivativeCoefficients();
504
+ let xCoefficients = [0, 0, 0, 0];
505
+ let yCoefficients = [0, 0, 0, 0];
506
+ derivativeCoefficients.forEach((coefficient, index) => {
507
+ xCoefficients[3 - index] = coefficient.x;
508
+ yCoefficients[3 - index] = coefficient.y;
509
+ });
510
+ const xRoots = solveCubic(xCoefficients[0], xCoefficients[1], xCoefficients[2], xCoefficients[3]);
511
+ const yRoots = solveCubic(yCoefficients[0], yCoefficients[1], yCoefficients[2], yCoefficients[3]);
512
+ xRoots.forEach((root) => {
513
+ if (root >= 0 && root <= 1) {
514
+ res.x.push(root);
515
+ }
516
+ });
517
+ yRoots.forEach((root) => {
518
+ if (root >= 0 && root <= 1) {
519
+ res.y.push(root);
520
+ }
521
+ });
522
+ if (derivativeCoefficients.length >= 3) {
523
+ xCoefficients = [0, 0, 0, 0];
524
+ yCoefficients = [0, 0, 0, 0];
525
+ const secondDerivativeCoefficients = this.getCoefficientOfTTermsWithControlPoints(this.getDerivativeControlPoints(this.dControlPoints));
526
+ secondDerivativeCoefficients.forEach((coefficient, index) => {
527
+ xCoefficients[3 - index] = coefficient.x;
528
+ yCoefficients[3 - index] = coefficient.y;
529
+ });
530
+ const secondXRoots = solveCubic(xCoefficients[0], xCoefficients[1], xCoefficients[2], xCoefficients[3]);
531
+ const secondYRoots = solveCubic(yCoefficients[0], yCoefficients[1], yCoefficients[2], yCoefficients[3]);
532
+ secondXRoots.forEach((root) => {
533
+ if (root >= 0 && root <= 1) {
534
+ res.x.push(root);
535
+ }
536
+ });
537
+ secondYRoots.forEach((root) => {
538
+ if (root >= 0 && root <= 1) {
539
+ res.y.push(root);
540
+ }
541
+ });
426
542
  }
427
- fitArc(startPoint, endPoint, midPoint) {
428
- const M11 = [[startPoint.x, startPoint.y, 1], [midPoint.x, midPoint.y, 1], [endPoint.x, endPoint.y, 1]];
429
- if (this.determinant3by3(M11) == 0) {
430
- // three points lie on a line no circle
431
- return { exists: false };
432
- }
433
- const M12 = [[startPoint.x * startPoint.x + startPoint.y * startPoint.y, startPoint.y, 1],
434
- [midPoint.x * midPoint.x + midPoint.y * midPoint.y, midPoint.y, 1],
435
- [endPoint.x * endPoint.x + endPoint.y * endPoint.y, endPoint.y, 1]];
436
- const M13 = [[startPoint.x * startPoint.x + startPoint.y * startPoint.y, startPoint.x, 1],
437
- [midPoint.x * midPoint.x + midPoint.y * midPoint.y, midPoint.x, 1],
438
- [endPoint.x * endPoint.x + endPoint.y * endPoint.y, endPoint.x, 1]];
439
- const M14 = [[startPoint.x * startPoint.x + startPoint.y * startPoint.y, startPoint.x, startPoint.y],
440
- [midPoint.x * midPoint.x + midPoint.y * midPoint.y, midPoint.x, midPoint.y],
441
- [endPoint.x * endPoint.x + endPoint.y * endPoint.y, endPoint.x, endPoint.y]];
442
- const centerX = (1 / 2) * (this.determinant3by3(M12) / this.determinant3by3(M11));
443
- const centerY = (-1 / 2) * (this.determinant3by3(M13) / this.determinant3by3(M11));
444
- const radius = Math.sqrt(centerX * centerX + centerY * centerY + (this.determinant3by3(M14) / this.determinant3by3(M11)));
445
- return { exists: true, center: { x: centerX, y: centerY }, radius: radius, startPoint: startPoint, endPoint: endPoint };
446
- }
447
- determinant3by3(matrix) {
448
- const a = matrix[0][0];
449
- const b = matrix[0][1];
450
- const c = matrix[0][2];
451
- const d = matrix[1][0];
452
- const e = matrix[1][1];
453
- const f = matrix[1][2];
454
- const g = matrix[2][0];
455
- const h = matrix[2][1];
456
- const i = matrix[2][2];
457
- return a * (e * i - f * h) - b * (d * i - g * f) + c * (d * h - e * g);
458
- }
459
- curvature(tVal) {
460
- const derivative = computeWithControlPoints(tVal, this.dControlPoints);
461
- const secondDerivative = computeWithControlPoints(tVal, this.getDerivativeControlPoints(this.dControlPoints));
462
- const numerator = derivative.x * secondDerivative.y - secondDerivative.x * derivative.y;
463
- const denominator = Math.pow(derivative.x * derivative.x + derivative.y * derivative.y, 3 / 2);
464
- if (denominator == 0)
465
- return NaN;
466
- return numerator / denominator;
467
- }
468
- secondDerivative(tVal) {
469
- return computeWithControlPoints(tVal, this.getDerivativeControlPoints(this.dControlPoints));
470
- }
471
- getCoefficientOfTTerms() {
472
- return this.getCoefficientOfTTermsWithControlPoints(this.controlPoints);
473
- }
474
- getDerivativeCoefficients() {
475
- return this.getCoefficientOfTTermsWithControlPoints(this.dControlPoints);
476
- }
477
- getCoefficientOfTTermsWithControlPoints(controlPoints) {
478
- const terms = [];
479
- let matrix = [];
480
- if (controlPoints.length == 3) {
481
- matrix = [[1, 0, 0], [-2, 2, 0], [1, -2, 1]];
482
- }
483
- else if (controlPoints.length == 4) {
484
- matrix = [[1, 0, 0, 0], [-3, 3, 0, 0], [3, -6, 3, 0], [-1, 3, -3, 1]];
485
- }
486
- else if (controlPoints.length == 2) {
487
- matrix = [[1, 0], [-1, 1]];
488
- }
489
- else {
490
- throw new Error("number of control points is wrong");
491
- }
492
- for (let index = 0; index < controlPoints.length; index++) {
493
- terms.push(controlPoints.reduce((prevVal, curVal, jindex) => {
494
- return { x: prevVal.x + matrix[index][jindex] * curVal.x, y: prevVal.y + matrix[index][jindex] * curVal.y };
495
- }, { x: 0, y: 0 }));
496
- }
497
- return terms;
498
- }
499
- getControlPointsAlignedWithXAxis() {
500
- const alignedAxis = PointCal.unitVectorFromA2B(this.controlPoints[0], this.controlPoints[this.controlPoints.length - 1]);
501
- const angle = PointCal.angleFromA2B({ x: 1, y: 0 }, alignedAxis);
502
- const startingPoint = this.controlPoints[0];
503
- const res = [{ x: 0, y: 0 }];
504
- for (let index = 1; index < this.controlPoints.length; index++) {
505
- const vector = PointCal.subVector(this.controlPoints[index], startingPoint);
506
- const rotatedVector = PointCal.rotatePoint(vector, -angle);
507
- res.push(rotatedVector);
508
- }
509
- return res;
510
- }
511
- getExtrema() {
512
- const res = { x: [], y: [] };
513
- const derivativeCoefficients = this.getDerivativeCoefficients();
514
- let xCoefficients = [0, 0, 0, 0];
515
- let yCoefficients = [0, 0, 0, 0];
516
- derivativeCoefficients.forEach((coefficient, index) => {
517
- xCoefficients[3 - index] = coefficient.x;
518
- yCoefficients[3 - index] = coefficient.y;
519
- });
520
- const xRoots = solveCubic(xCoefficients[0], xCoefficients[1], xCoefficients[2], xCoefficients[3]);
521
- const yRoots = solveCubic(yCoefficients[0], yCoefficients[1], yCoefficients[2], yCoefficients[3]);
522
- xRoots.forEach((root) => {
523
- if (root >= 0 && root <= 1) {
524
- res.x.push(root);
525
- }
526
- });
527
- yRoots.forEach((root) => {
528
- if (root >= 0 && root <= 1) {
529
- res.y.push(root);
530
- }
531
- });
532
- if (derivativeCoefficients.length >= 3) {
533
- xCoefficients = [0, 0, 0, 0];
534
- yCoefficients = [0, 0, 0, 0];
535
- const secondDerivativeCoefficients = this.getCoefficientOfTTermsWithControlPoints(this.getDerivativeControlPoints(this.dControlPoints));
536
- secondDerivativeCoefficients.forEach((coefficient, index) => {
537
- xCoefficients[3 - index] = coefficient.x;
538
- yCoefficients[3 - index] = coefficient.y;
539
- });
540
- const secondXRoots = solveCubic(xCoefficients[0], xCoefficients[1], xCoefficients[2], xCoefficients[3]);
541
- const secondYRoots = solveCubic(yCoefficients[0], yCoefficients[1], yCoefficients[2], yCoefficients[3]);
542
- secondXRoots.forEach((root) => {
543
- if (root >= 0 && root <= 1) {
544
- res.x.push(root);
545
- }
546
- });
547
- secondYRoots.forEach((root) => {
548
- if (root >= 0 && root <= 1) {
549
- res.y.push(root);
550
- }
551
- });
552
- }
553
- return res;
543
+ return res;
544
+ }
545
+ translateRotateControlPoints(translation, rotationAngle) {
546
+ const res = [];
547
+ for (let index = 0;index < this.controlPoints.length; index++) {
548
+ res.push(PointCal.rotatePoint(PointCal.addVector(this.controlPoints[index], translation), rotationAngle));
554
549
  }
555
- translateRotateControlPoints(translation, rotationAngle) {
556
- // rotation is in radians
557
- const res = [];
558
- for (let index = 0; index < this.controlPoints.length; index++) {
559
- res.push(PointCal.rotatePoint(PointCal.addVector(this.controlPoints[index], translation), rotationAngle));
560
- }
561
- return res;
562
- }
563
- getLineIntersections(line) {
564
- const translationRotation = line.getTranslationRotationToAlginXAxis();
565
- const res = [];
566
- const alignedControlPoints = this.translateRotateControlPoints(translationRotation.translation, translationRotation.rotationAngle);
567
- const coefficients = this.getCoefficientOfTTermsWithControlPoints(alignedControlPoints);
568
- let yCoefficients = [0, 0, 0, 0];
569
- coefficients.forEach((coefficient, index) => {
570
- yCoefficients[3 - index] = coefficient.y;
571
- });
572
- const yRoots = solveCubic(yCoefficients[0], yCoefficients[1], yCoefficients[2], yCoefficients[3]);
573
- yRoots.forEach((root) => {
574
- if (root >= 0 && root <= 1) {
575
- if (line.pointInLine(this.get(root))) {
576
- res.push(root);
577
- }
578
- }
579
- });
580
- return res;
581
- }
582
- getSelfIntersections() {
583
- const [subCurveControlPoints1, subCurveControlPoints2] = this.split(0.5);
584
- const subCurve1 = new BCurve(subCurveControlPoints1);
585
- const subCurve2 = new BCurve(subCurveControlPoints2);
586
- let initialRes = getIntersectionsBetweenCurves(subCurve1, subCurve2);
587
- initialRes.forEach((intersection) => {
588
- intersection.selfT = intersection.selfT * 0.5;
589
- intersection.otherT = intersection.otherT * 0.5 + 0.5;
590
- });
591
- initialRes.shift();
592
- return initialRes;
593
- }
594
- getCircleIntersections(circleCenter, circleRadius) {
595
- const LUT = this.getLUTWithTVal(500);
596
- LUT[0].point;
597
- LUT[0].tVal;
598
- LUT.forEach((curvePoint, index) => {
599
- Math.abs(PointCal.distanceBetweenPoints(circleCenter, curvePoint.point));
600
- });
601
- const LUTD = LUT.map((curvePoint, index) => {
602
- return { ...curvePoint, distance: 0 };
603
- });
604
- let start = 0;
605
- let count = 0;
606
- let indices = [];
607
- while (++count < 25) {
608
- let i = this.findClosest(circleCenter.x, circleCenter.y, LUTD, circleRadius, 5, LUTD[start - 2]?.distance, LUTD[start - 1]?.distance);
609
- if (i < start)
610
- break;
611
- if (i > 0 && i == start)
612
- break;
613
- indices.push(i);
614
- start = i + 2;
615
- }
616
- const finalList = [];
617
- indices.forEach((index) => {
618
- let res = this.refineBinary(this, circleCenter.x, circleCenter.y, LUTD, index, circleRadius);
619
- if (res != undefined) {
620
- finalList.push({ intersection: res.point, tVal: res.tVal });
621
- }
622
- });
623
- return finalList;
624
- }
625
- advanceAtTWithLength(tVal, length) {
626
- const currentLength = this.lengthAtT(tVal);
627
- const targetLength = currentLength + length;
628
- // Handle edge cases first
629
- if (tVal === 0 && length < 0) {
630
- return { type: "beforeCurve", remainLength: -length };
631
- }
632
- if (tVal === 1 && length > 0) {
633
- return { type: "afterCurve", remainLength: length };
634
- }
635
- if (targetLength > this.fullLength) {
636
- return { type: "afterCurve", remainLength: targetLength - this.fullLength };
637
- }
638
- else if (targetLength < 0) {
639
- return { type: "beforeCurve", remainLength: -targetLength };
640
- }
641
- // Use LUT for binary search
642
- if (this.arcLengthLUT.arcLengthLUT.length === 0) {
643
- this.arcLengthLUT = this.getArcLengthLUT(1000);
644
- }
645
- const points = this.arcLengthLUT.arcLengthLUT;
646
- let low = 0;
647
- let high = points.length - 1;
648
- // Binary search to find the interval containing targetLength
649
- while (low <= high) {
650
- const mid = Math.floor((low + high) / 2);
651
- const midLength = points[mid].length;
652
- if (approximately(midLength, targetLength, 0.01)) {
653
- // Found exact match
654
- const resultTVal = points[mid].tVal;
655
- const point = this.get(resultTVal);
656
- return { type: "withinCurve", tVal: resultTVal, point: point };
657
- }
658
- else if (midLength < targetLength) {
659
- low = mid + 1;
660
- }
661
- else {
662
- high = mid - 1;
663
- }
664
- }
665
- // After binary search, 'high' points to the largest index with length < targetLength
666
- // and 'low' points to the smallest index with length >= targetLength
667
- // Handle edge cases
668
- if (high < 0) {
669
- // targetLength is smaller than the first point's length
670
- high = 1;
671
- low = 0;
672
- }
673
- if (low >= points.length) {
674
- // targetLength is larger than the last point's length
675
- high = points.length - 1;
676
- low = points.length - 2;
677
- }
678
- // Interpolate between points[high] and points[low]
679
- const p1 = points[high];
680
- const p2 = points[low];
681
- // Linear interpolation
682
- const lengthRange = p2.length - p1.length;
683
- const tRange = p2.tVal - p1.tVal;
684
- if (lengthRange === 0) {
685
- // Both points have the same length, use the first one
686
- const point = this.get(p1.tVal);
687
- return { type: "withinCurve", tVal: p1.tVal, point: point };
688
- }
689
- const ratio = (targetLength - p1.length) / lengthRange;
690
- const interpolatedT = p1.tVal + ratio * tRange;
691
- // Clamp to valid range
692
- const clampedT = Math.max(0, Math.min(1, interpolatedT));
693
- const point = this.get(clampedT);
694
- return { type: "withinCurve", tVal: clampedT, point: point };
695
- }
696
- advanceByDistance(startT, distance) {
697
- let currentT = startT;
698
- let remainingDistance = distance;
699
- const stepSize = 0.01; // Adjust for precision vs performance
700
- if (distance > this.fullLength) {
701
- return { type: "afterCurve", remainLength: distance - this.fullLength };
702
- }
703
- else if (distance < 0) {
704
- return { type: "beforeCurve", remainLength: -distance };
705
- }
706
- while (remainingDistance > 0 && currentT < 1) {
707
- const currentPoint = this.get(currentT);
708
- const nextT = Math.min(currentT + stepSize, 1);
709
- const nextPoint = this.get(nextT);
710
- const segmentLength = Math.sqrt(Math.pow(nextPoint.x - currentPoint.x, 2) +
711
- Math.pow(nextPoint.y - currentPoint.y, 2));
712
- if (segmentLength >= remainingDistance) {
713
- // Interpolate within this segment
714
- const ratio = remainingDistance / segmentLength;
715
- return { type: "withinCurve", tVal: currentT + ratio * (nextT - currentT), point: this.get(currentT + ratio * (nextT - currentT)) };
716
- }
717
- remainingDistance -= segmentLength;
718
- currentT = nextT;
719
- }
720
- return { type: "withinCurve", tVal: currentT, point: this.get(currentT) };
721
- }
722
- refineBinary(curve, x, y, LUT, i, targetDistance = 0, epsilon = 0.01) {
723
- let q = LUT[i], count = 1, distance = Number.MAX_SAFE_INTEGER;
724
- do {
725
- 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;
726
- if (step < 0.001)
727
- break;
728
- lut.push(LUT[i1]);
729
- for (let j = 1; j <= 3; j++) {
730
- let n = curve.get(t1 + j * step);
731
- let nDistance = Math.abs(PointCal.distanceBetweenPoints(n, { x: x, y: y }) - targetDistance);
732
- if (nDistance < distance) {
733
- distance = nDistance;
734
- q = { point: n, tVal: t1 + j * step, distance: nDistance };
735
- i = j;
736
- }
737
- lut.push({ point: n, tVal: t1 + j * step, distance: nDistance });
738
- }
739
- lut.push(LUT[i2]);
740
- // update the LUT to be our new five point LUT, and run again.
741
- LUT = lut;
742
- // The "count" test is mostly a safety measure: it will
743
- // never kick in, but something that _will_ terminate is
744
- // always better than while(true). Never use while(true)
745
- } while (count++ < 25);
746
- // If we're trying to hit a target, discard the result if
747
- // it is not close enough to the target.
748
- if (targetDistance && distance > epsilon) {
749
- q = undefined;
750
- }
751
- return q;
752
- }
753
- findClosest(x, y, LUT, circleRadius, distanceEpsilon = 5, pd2, pd1) {
754
- let distance = Number.MAX_SAFE_INTEGER, prevDistance2 = pd2 || distance, prevDistance1 = pd1 || distance, i = -1;
755
- for (let index = 0, e = LUT.length; index < e; index++) {
756
- let p = LUT[index].point;
757
- LUT[index].distance = Math.abs(PointCal.distanceBetweenPoints({ x: x, y: y }, p) - circleRadius);
758
- // Realistically, there's only going to be an intersection if
759
- // the distance to the circle center is already approximately
760
- // the circle's radius.
761
- if (prevDistance1 < distanceEpsilon && prevDistance2 > prevDistance1 && prevDistance1 < LUT[index].distance) {
762
- i = index - 1;
763
- break;
764
- }
765
- if (LUT[index].distance < distance) {
766
- distance = LUT[index].distance;
767
- }
768
- prevDistance2 = prevDistance1;
769
- prevDistance1 = LUT[index].distance;
550
+ return res;
551
+ }
552
+ getLineIntersections(line) {
553
+ const translationRotation = line.getTranslationRotationToAlginXAxis();
554
+ const res = [];
555
+ const alignedControlPoints = this.translateRotateControlPoints(translationRotation.translation, translationRotation.rotationAngle);
556
+ const coefficients = this.getCoefficientOfTTermsWithControlPoints(alignedControlPoints);
557
+ let yCoefficients = [0, 0, 0, 0];
558
+ coefficients.forEach((coefficient, index) => {
559
+ yCoefficients[3 - index] = coefficient.y;
560
+ });
561
+ const yRoots = solveCubic(yCoefficients[0], yCoefficients[1], yCoefficients[2], yCoefficients[3]);
562
+ yRoots.forEach((root) => {
563
+ if (root >= 0 && root <= 1) {
564
+ if (line.pointInLine(this.get(root))) {
565
+ res.push(root);
770
566
  }
771
- return i;
772
- }
773
- getCurveIntersections(curve, deduplicationTolerance) {
774
- return getIntersectionsBetweenCurves(this, curve, deduplicationTolerance);
775
- }
776
- get AABB() {
777
- const extrema = this.getExtrema();
778
- const tVals = [0, 1];
779
- let min = { x: Number.MAX_VALUE, y: Number.MAX_VALUE };
780
- let max = { x: -Number.MAX_VALUE, y: -Number.MAX_VALUE };
781
- extrema.x.forEach((tVal) => {
782
- tVals.push(tVal);
783
- });
784
- extrema.y.forEach((tVal) => {
785
- tVals.push(tVal);
786
- });
787
- tVals.forEach((tVal) => {
788
- const curPoint = this.get(tVal);
789
- min.x = Math.min(min.x, curPoint.x);
790
- min.y = Math.min(min.y, curPoint.y);
791
- max.x = Math.max(max.x, curPoint.x);
792
- max.y = Math.max(max.y, curPoint.y);
793
- });
794
- return { min: min, max: max };
795
- }
796
- normal(tVal) {
797
- const d = this.derivative(tVal);
798
- const q = Math.sqrt(d.x * d.x + d.y * d.y);
799
- return { tVal, direction: { x: -d.y / q, y: d.x / q } };
800
- }
567
+ }
568
+ });
569
+ return res;
570
+ }
571
+ getSelfIntersections() {
572
+ const [subCurveControlPoints1, subCurveControlPoints2] = this.split(0.5);
573
+ const subCurve1 = new BCurve(subCurveControlPoints1);
574
+ const subCurve2 = new BCurve(subCurveControlPoints2);
575
+ let initialRes = getIntersectionsBetweenCurves(subCurve1, subCurve2);
576
+ initialRes.forEach((intersection) => {
577
+ intersection.selfT = intersection.selfT * 0.5;
578
+ intersection.otherT = intersection.otherT * 0.5 + 0.5;
579
+ });
580
+ initialRes.shift();
581
+ return initialRes;
582
+ }
583
+ getCircleIntersections(circleCenter, circleRadius) {
584
+ const LUT = this.getLUTWithTVal(500);
585
+ let distanceError = Number.MAX_VALUE;
586
+ let preliminaryIntersectionIndex = 0;
587
+ let preliminaryIntersectionPoint = LUT[0].point;
588
+ let preliminaryIntersectionTVal = LUT[0].tVal;
589
+ LUT.forEach((curvePoint, index) => {
590
+ let curDistanceError = Math.abs(PointCal.distanceBetweenPoints(circleCenter, curvePoint.point));
591
+ if (curDistanceError < distanceError) {
592
+ distanceError = curDistanceError;
593
+ preliminaryIntersectionIndex = index;
594
+ }
595
+ });
596
+ const LUTD = LUT.map((curvePoint, index) => {
597
+ return { ...curvePoint, distance: 0 };
598
+ });
599
+ distanceError = Number.MAX_VALUE;
600
+ let start = 0;
601
+ let count = 0;
602
+ let indices = [];
603
+ while (++count < 25) {
604
+ let i = this.findClosest(circleCenter.x, circleCenter.y, LUTD, circleRadius, 5, LUTD[start - 2]?.distance, LUTD[start - 1]?.distance);
605
+ if (i < start)
606
+ break;
607
+ if (i > 0 && i == start)
608
+ break;
609
+ indices.push(i);
610
+ start = i + 2;
611
+ }
612
+ const finalList = [];
613
+ indices.forEach((index) => {
614
+ let res = this.refineBinary(this, circleCenter.x, circleCenter.y, LUTD, index, circleRadius);
615
+ if (res != null) {
616
+ finalList.push({ intersection: res.point, tVal: res.tVal });
617
+ }
618
+ });
619
+ return finalList;
620
+ }
621
+ advanceAtTWithLength(tVal, length) {
622
+ const currentLength = this.lengthAtT(tVal);
623
+ const targetLength = currentLength + length;
624
+ if (tVal === 0 && length < 0) {
625
+ return { type: "beforeCurve", remainLength: -length };
626
+ }
627
+ if (tVal === 1 && length > 0) {
628
+ return { type: "afterCurve", remainLength: length };
629
+ }
630
+ if (targetLength > this.fullLength) {
631
+ return { type: "afterCurve", remainLength: targetLength - this.fullLength };
632
+ } else if (targetLength < 0) {
633
+ return { type: "beforeCurve", remainLength: -targetLength };
634
+ }
635
+ if (this.arcLengthLUT.arcLengthLUT.length === 0) {
636
+ this.arcLengthLUT = this.getArcLengthLUT(1000);
637
+ }
638
+ const points = this.arcLengthLUT.arcLengthLUT;
639
+ let low = 0;
640
+ let high = points.length - 1;
641
+ while (low <= high) {
642
+ const mid = Math.floor((low + high) / 2);
643
+ const midLength = points[mid].length;
644
+ if (approximately(midLength, targetLength, 0.01)) {
645
+ const resultTVal = points[mid].tVal;
646
+ const point2 = this.get(resultTVal);
647
+ return { type: "withinCurve", tVal: resultTVal, point: point2 };
648
+ } else if (midLength < targetLength) {
649
+ low = mid + 1;
650
+ } else {
651
+ high = mid - 1;
652
+ }
653
+ }
654
+ if (high < 0) {
655
+ high = 1;
656
+ low = 0;
657
+ }
658
+ if (low >= points.length) {
659
+ high = points.length - 1;
660
+ low = points.length - 2;
661
+ }
662
+ const p1 = points[high];
663
+ const p2 = points[low];
664
+ const lengthRange = p2.length - p1.length;
665
+ const tRange = p2.tVal - p1.tVal;
666
+ if (lengthRange === 0) {
667
+ const point2 = this.get(p1.tVal);
668
+ return { type: "withinCurve", tVal: p1.tVal, point: point2 };
669
+ }
670
+ const ratio = (targetLength - p1.length) / lengthRange;
671
+ const interpolatedT = p1.tVal + ratio * tRange;
672
+ const clampedT = Math.max(0, Math.min(1, interpolatedT));
673
+ const point = this.get(clampedT);
674
+ return { type: "withinCurve", tVal: clampedT, point };
675
+ }
676
+ advanceByDistance(startT, distance) {
677
+ let currentT = startT;
678
+ let remainingDistance = distance;
679
+ const stepSize = 0.01;
680
+ if (distance > this.fullLength) {
681
+ return { type: "afterCurve", remainLength: distance - this.fullLength };
682
+ } else if (distance < 0) {
683
+ return { type: "beforeCurve", remainLength: -distance };
684
+ }
685
+ while (remainingDistance > 0 && currentT < 1) {
686
+ const currentPoint = this.get(currentT);
687
+ const nextT = Math.min(currentT + stepSize, 1);
688
+ const nextPoint = this.get(nextT);
689
+ const segmentLength = Math.sqrt(Math.pow(nextPoint.x - currentPoint.x, 2) + Math.pow(nextPoint.y - currentPoint.y, 2));
690
+ if (segmentLength >= remainingDistance) {
691
+ const ratio = remainingDistance / segmentLength;
692
+ return { type: "withinCurve", tVal: currentT + ratio * (nextT - currentT), point: this.get(currentT + ratio * (nextT - currentT)) };
693
+ }
694
+ remainingDistance -= segmentLength;
695
+ currentT = nextT;
696
+ }
697
+ return { type: "withinCurve", tVal: currentT, point: this.get(currentT) };
698
+ }
699
+ refineBinary(curve, x, y, LUT, i, targetDistance = 0, epsilon = 0.01) {
700
+ let q = LUT[i], count = 1, distance = Number.MAX_SAFE_INTEGER;
701
+ do {
702
+ 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;
703
+ if (step < 0.001)
704
+ break;
705
+ lut.push(LUT[i1]);
706
+ for (let j = 1;j <= 3; j++) {
707
+ let n = curve.get(t1 + j * step);
708
+ let nDistance = Math.abs(PointCal.distanceBetweenPoints(n, { x, y }) - targetDistance);
709
+ if (nDistance < distance) {
710
+ distance = nDistance;
711
+ q = { point: n, tVal: t1 + j * step, distance: nDistance };
712
+ i = j;
713
+ }
714
+ lut.push({ point: n, tVal: t1 + j * step, distance: nDistance });
715
+ }
716
+ lut.push(LUT[i2]);
717
+ LUT = lut;
718
+ } while (count++ < 25);
719
+ if (targetDistance && distance > epsilon) {
720
+ q = undefined;
721
+ }
722
+ return q;
723
+ }
724
+ findClosest(x, y, LUT, circleRadius, distanceEpsilon = 5, pd2, pd1) {
725
+ let distance = Number.MAX_SAFE_INTEGER, prevDistance2 = pd2 || distance, prevDistance1 = pd1 || distance, i = -1;
726
+ for (let index = 0, e = LUT.length;index < e; index++) {
727
+ let p = LUT[index].point;
728
+ LUT[index].distance = Math.abs(PointCal.distanceBetweenPoints({ x, y }, p) - circleRadius);
729
+ if (prevDistance1 < distanceEpsilon && prevDistance2 > prevDistance1 && prevDistance1 < LUT[index].distance) {
730
+ i = index - 1;
731
+ break;
732
+ }
733
+ if (LUT[index].distance < distance) {
734
+ distance = LUT[index].distance;
735
+ }
736
+ prevDistance2 = prevDistance1;
737
+ prevDistance1 = LUT[index].distance;
738
+ }
739
+ return i;
740
+ }
741
+ getCurveIntersections(curve, deduplicationTolerance) {
742
+ return getIntersectionsBetweenCurves(this, curve, deduplicationTolerance);
743
+ }
744
+ get AABB() {
745
+ const extrema = this.getExtrema();
746
+ const tVals = [0, 1];
747
+ let min = { x: Number.MAX_VALUE, y: Number.MAX_VALUE };
748
+ let max = { x: -Number.MAX_VALUE, y: -Number.MAX_VALUE };
749
+ extrema.x.forEach((tVal) => {
750
+ tVals.push(tVal);
751
+ });
752
+ extrema.y.forEach((tVal) => {
753
+ tVals.push(tVal);
754
+ });
755
+ tVals.forEach((tVal) => {
756
+ const curPoint = this.get(tVal);
757
+ min.x = Math.min(min.x, curPoint.x);
758
+ min.y = Math.min(min.y, curPoint.y);
759
+ max.x = Math.max(max.x, curPoint.x);
760
+ max.y = Math.max(max.y, curPoint.y);
761
+ });
762
+ return { min, max };
763
+ }
764
+ normal(tVal) {
765
+ const d = this.derivative(tVal);
766
+ const q = Math.sqrt(d.x * d.x + d.y * d.y);
767
+ return { tVal, direction: { x: -d.y / q, y: d.x / q } };
768
+ }
801
769
  }
802
770
  function reduce(curve) {
803
- let i, t1 = 0, t2 = 0, step = 0.01, segment, pass1 = [], pass2 = [];
804
- // first pass: split on extrema
805
- let extrema = curve.getExtrema().x;
806
- if (extrema.indexOf(0) === -1) {
807
- extrema = [0].concat(extrema);
808
- }
809
- if (extrema.indexOf(1) === -1) {
810
- extrema.push(1);
811
- }
812
- for (t1 = extrema[0], i = 1; i < extrema.length; i++) {
813
- t2 = extrema[i];
814
- segment = curve.splitAndTakeMidCurve(t1, t2);
815
- pass1.push(segment);
816
- t1 = t2;
817
- }
818
- // second pass: further reduce these segments to simple segments
819
- pass1.forEach(p1 => {
820
- t1 = 0;
821
- t2 = 0;
822
- while (t2 <= 1) {
823
- for (t2 = t1 + step; t2 <= 1 + step; t2 += step) {
824
- // Clamp t2 to valid range
825
- const clampedT2 = Math.min(t2, 1);
826
- segment = p1.splitAndTakeMidCurve(t1, clampedT2);
827
- if (!curveIsSimple(segment)) {
828
- t2 -= step;
829
- if (Math.abs(t1 - t2) < step) {
830
- // we can never form a reduction
831
- return [];
832
- }
833
- const finalT2 = Math.min(t2, 1);
834
- segment = p1.splitAndTakeMidCurve(t1, finalT2);
835
- pass2.push(segment);
836
- t1 = finalT2;
837
- break;
838
- }
839
- }
840
- }
841
- if (t1 < 1) {
842
- segment = p1.splitAndTakeMidCurve(t1, 1);
843
- pass2.push(segment);
844
- }
845
- });
846
- return pass2;
771
+ let i, t1 = 0, t2 = 0, step = 0.01, segment, pass1 = [], pass2 = [];
772
+ let extrema = curve.getExtrema().x;
773
+ if (extrema.indexOf(0) === -1) {
774
+ extrema = [0].concat(extrema);
775
+ }
776
+ if (extrema.indexOf(1) === -1) {
777
+ extrema.push(1);
778
+ }
779
+ for (t1 = extrema[0], i = 1;i < extrema.length; i++) {
780
+ t2 = extrema[i];
781
+ segment = curve.splitAndTakeMidCurve(t1, t2);
782
+ pass1.push(segment);
783
+ t1 = t2;
784
+ }
785
+ pass1.forEach((p1) => {
786
+ t1 = 0;
787
+ t2 = 0;
788
+ while (t2 <= 1) {
789
+ for (t2 = t1 + step;t2 <= 1 + step; t2 += step) {
790
+ const clampedT2 = Math.min(t2, 1);
791
+ segment = p1.splitAndTakeMidCurve(t1, clampedT2);
792
+ if (!curveIsSimple(segment)) {
793
+ t2 -= step;
794
+ if (Math.abs(t1 - t2) < step) {
795
+ return [];
796
+ }
797
+ const finalT2 = Math.min(t2, 1);
798
+ segment = p1.splitAndTakeMidCurve(t1, finalT2);
799
+ pass2.push(segment);
800
+ t1 = finalT2;
801
+ break;
802
+ }
803
+ }
804
+ }
805
+ if (t1 < 1) {
806
+ segment = p1.splitAndTakeMidCurve(t1, 1);
807
+ pass2.push(segment);
808
+ }
809
+ });
810
+ return pass2;
847
811
  }
848
812
  function raiseCurveOrder(curve) {
849
- const p = curve.getControlPoints(), np = [p[0]], k = p.length;
850
- for (let i = 1; i < k; i++) {
851
- const pi = p[i];
852
- const pim = p[i - 1];
853
- np[i] = {
854
- x: ((k - i) / k) * pi.x + (i / k) * pim.x,
855
- y: ((k - i) / k) * pi.y + (i / k) * pim.y,
856
- };
857
- }
858
- np[k] = p[k - 1];
859
- return new BCurve(np);
813
+ const p = curve.getControlPoints(), np = [p[0]], k = p.length;
814
+ for (let i = 1;i < k; i++) {
815
+ const pi = p[i];
816
+ const pim = p[i - 1];
817
+ np[i] = {
818
+ x: (k - i) / k * pi.x + i / k * pim.x,
819
+ y: (k - i) / k * pi.y + i / k * pim.y
820
+ };
821
+ }
822
+ np[k] = p[k - 1];
823
+ return new BCurve(np);
860
824
  }
861
825
  function offset(curve, t, d) {
862
- if (d !== undefined) {
863
- const c = curve.get(t), n = curve.normal(t).direction;
864
- const ret = {
865
- c: c,
866
- n: n,
867
- x: c.x + n.x * d,
868
- y: c.y + n.y * d,
869
- };
870
- // if (this._3d) {
871
- // ret.z = c.z + n.z * d;
872
- // }
873
- return ret;
874
- }
875
- // Native offset implementation based on bezier-js algorithm
876
- const points = curve.getControlPoints();
877
- const linear = curveIsLinear(curve);
878
- if (linear) {
879
- const nv = curve.normal(0).direction, coords = points.map(function (p) {
880
- const ret = {
881
- x: p.x + t * nv.x,
882
- y: p.y + t * nv.y,
883
- };
884
- return ret;
885
- });
886
- return [new BCurve(coords)];
887
- }
888
- // For non-linear curves, reduce to simple segments and scale each
889
- return reduce(curve).map(function (s) {
890
- if (curveIsLinear(s)) {
891
- return offset(s, t)[0];
892
- }
893
- return scaleCurve(s, t);
826
+ if (d !== undefined) {
827
+ const c = curve.get(t), n = curve.normal(t).direction;
828
+ const ret = {
829
+ c,
830
+ n,
831
+ x: c.x + n.x * d,
832
+ y: c.y + n.y * d
833
+ };
834
+ return ret;
835
+ }
836
+ const points = curve.getControlPoints();
837
+ const linear = curveIsLinear(curve);
838
+ if (linear) {
839
+ const nv = curve.normal(0).direction, coords = points.map(function(p) {
840
+ const ret = {
841
+ x: p.x + t * nv.x,
842
+ y: p.y + t * nv.y
843
+ };
844
+ return ret;
894
845
  });
846
+ return [new BCurve(coords)];
847
+ }
848
+ return reduce(curve).map(function(s) {
849
+ if (curveIsLinear(s)) {
850
+ return offset(s, t)[0];
851
+ }
852
+ return scaleCurve(s, t);
853
+ });
895
854
  }
896
855
  function offset2(curve, d) {
897
- const lut = curve.getLUTWithTVal(100);
898
- const res = lut.map((item) => {
899
- const derivative = PointCal.unitVector(curve.derivative(item.tVal));
900
- const normal = { x: -derivative.y, y: derivative.x };
901
- const offsetPoint = { x: item.point.x + normal.x * d, y: item.point.y + normal.y * d };
902
- return offsetPoint;
903
- });
904
- return res;
856
+ const lut = curve.getLUTWithTVal(100);
857
+ const res = lut.map((item) => {
858
+ const derivative = PointCal.unitVector(curve.derivative(item.tVal));
859
+ const normal = { x: -derivative.y, y: derivative.x };
860
+ const offsetPoint = { x: item.point.x + normal.x * d, y: item.point.y + normal.y * d };
861
+ return offsetPoint;
862
+ });
863
+ return res;
905
864
  }
906
- // Helper function for line-line intersection (ported from bezier-js utils)
907
865
  function lli4(p1, p2, p3, p4) {
908
- const x1 = p1.x, y1 = p1.y, x2 = p2.x, y2 = p2.y, x3 = p3.x, y3 = p3.y, x4 = p4.x, y4 = p4.y;
909
- return lli8(x1, y1, x2, y2, x3, y3, x4, y4);
866
+ const { x: x1, y: y1 } = p1, x2 = p2.x, y2 = p2.y, x3 = p3.x, y3 = p3.y, x4 = p4.x, y4 = p4.y;
867
+ return lli8(x1, y1, x2, y2, x3, y3, x4, y4);
910
868
  }
911
869
  function lli8(x1, y1, x2, y2, x3, y3, x4, y4) {
912
- const nx = (x1 * y2 - y1 * x2) * (x3 - x4) - (x1 - x2) * (x3 * y4 - y3 * x4);
913
- const ny = (x1 * y2 - y1 * x2) * (y3 - y4) - (y1 - y2) * (x3 * y4 - y3 * x4);
914
- const d = (x1 - x2) * (y3 - y4) - (y1 - y2) * (x3 - x4);
915
- if (d == 0) {
916
- return false;
917
- }
918
- return { x: nx / d, y: ny / d };
870
+ const nx = (x1 * y2 - y1 * x2) * (x3 - x4) - (x1 - x2) * (x3 * y4 - y3 * x4);
871
+ const ny = (x1 * y2 - y1 * x2) * (y3 - y4) - (y1 - y2) * (x3 * y4 - y3 * x4);
872
+ const d = (x1 - x2) * (y3 - y4) - (y1 - y2) * (x3 - x4);
873
+ if (d == 0) {
874
+ return false;
875
+ }
876
+ return { x: nx / d, y: ny / d };
919
877
  }
920
878
  function curveIsLinear(curve) {
921
- const order = curve.getControlPoints().length - 1;
922
- const points = curve.getControlPoints();
923
- const alignedPoints = alignPointsToLine(points, { p1: points[0], p2: points[order] });
924
- const baseLength = PointCal.distanceBetweenPoints(points[0], points[order]);
925
- // Sum of distances from the line (y coordinates in aligned space)
926
- const linear = alignedPoints.reduce((t, p) => t + Math.abs(p.y), 0) < baseLength / 50;
927
- return linear;
879
+ const order = curve.getControlPoints().length - 1;
880
+ const points = curve.getControlPoints();
881
+ const alignedPoints = alignPointsToLine(points, { p1: points[0], p2: points[order] });
882
+ const baseLength = PointCal.distanceBetweenPoints(points[0], points[order]);
883
+ const linear = alignedPoints.reduce((t, p) => t + Math.abs(p.y), 0) < baseLength / 50;
884
+ return linear;
928
885
  }
929
886
  function scaleCurve(curve, d) {
930
- const order = curve.getControlPoints().length - 1;
931
- let distanceFn = undefined;
932
- if (typeof d === "function") {
933
- distanceFn = d;
934
- }
935
- if (distanceFn && order === 2) {
936
- return scaleCurve(raiseCurveOrder(curve), distanceFn);
937
- }
938
- const points = curve.getControlPoints();
939
- // Check if curve is linear
940
- if (curveIsLinear(curve)) {
941
- return translate(curve, curve.normal(0).direction, distanceFn ? distanceFn(0) : d, distanceFn ? distanceFn(1) : d);
942
- }
943
- const r1 = distanceFn ? distanceFn(0) : d;
944
- const r2 = distanceFn ? distanceFn(1) : d;
945
- // Get offset points at endpoints to find the scaling origin
946
- const v = [offset(curve, 0, 10), offset(curve, 1, 10)];
947
- const np = [];
948
- const o = lli4(v[0], v[0].c, v[1], v[1].c);
949
- if (!o) {
950
- // Fallback: use simple translation for problematic curves
951
- return translate(curve, curve.normal(0).direction, r1, r2);
952
- }
953
- // Move endpoint control points by distance along normal
954
- [0, 1].forEach(function (t) {
955
- const p = JSON.parse(JSON.stringify(points[t * order]));
956
- const vt = v[t];
957
- p.x += (t ? r2 : r1) * vt.n.x;
958
- p.y += (t ? r2 : r1) * vt.n.y;
959
- np[t * order] = p;
960
- });
961
- if (!distanceFn) {
962
- // Move control points to lie on the intersection of the offset
963
- // derivative vector, and the origin-through-control vector
964
- [0, 1].forEach((t) => {
965
- if (order === 2 && !!t)
966
- return;
967
- const p = np[t * order];
968
- const derivativeAtT = curve.derivative(t);
969
- const p2 = { x: p.x + derivativeAtT.x, y: p.y + derivativeAtT.y };
970
- const intersection = lli4(p, p2, o, points[t + 1]);
971
- if (intersection) {
972
- np[t + 1] = intersection;
973
- }
974
- else {
975
- // Fallback: use original control point with simple offset
976
- const originalPoint = points[t + 1];
977
- const normal = curve.normal((t + 1) / order).direction;
978
- np[t + 1] = {
979
- x: originalPoint.x + (t ? r2 : r1) * normal.x,
980
- y: originalPoint.y + (t ? r2 : r1) * normal.y
981
- };
982
- }
983
- });
984
- return new BCurve(np);
985
- }
986
- // For function-based distances, move control points by distance
987
- // to ensure correct tangent at endpoints
988
- [0, 1].forEach(function (t) {
989
- if (order === 2 && !!t)
990
- return;
991
- const p = points[t + 1];
992
- const ov = {
993
- x: p.x - o.x,
994
- y: p.y - o.y,
995
- };
996
- let rc = distanceFn((t + 1) / order);
997
- const m = Math.sqrt(ov.x * ov.x + ov.y * ov.y);
998
- ov.x /= m;
999
- ov.y /= m;
887
+ const order = curve.getControlPoints().length - 1;
888
+ let distanceFn = undefined;
889
+ if (typeof d === "function") {
890
+ distanceFn = d;
891
+ }
892
+ if (distanceFn && order === 2) {
893
+ return scaleCurve(raiseCurveOrder(curve), distanceFn);
894
+ }
895
+ const points = curve.getControlPoints();
896
+ if (curveIsLinear(curve)) {
897
+ return translate(curve, curve.normal(0).direction, distanceFn ? distanceFn(0) : d, distanceFn ? distanceFn(1) : d);
898
+ }
899
+ const r1 = distanceFn ? distanceFn(0) : d;
900
+ const r2 = distanceFn ? distanceFn(1) : d;
901
+ const v = [offset(curve, 0, 10), offset(curve, 1, 10)];
902
+ const np = [];
903
+ const o = lli4(v[0], v[0].c, v[1], v[1].c);
904
+ if (!o) {
905
+ return translate(curve, curve.normal(0).direction, r1, r2);
906
+ }
907
+ [0, 1].forEach(function(t) {
908
+ const p = JSON.parse(JSON.stringify(points[t * order]));
909
+ const vt = v[t];
910
+ p.x += (t ? r2 : r1) * vt.n.x;
911
+ p.y += (t ? r2 : r1) * vt.n.y;
912
+ np[t * order] = p;
913
+ });
914
+ if (!distanceFn) {
915
+ [0, 1].forEach((t) => {
916
+ if (order === 2 && !!t)
917
+ return;
918
+ const p = np[t * order];
919
+ const derivativeAtT = curve.derivative(t);
920
+ const p2 = { x: p.x + derivativeAtT.x, y: p.y + derivativeAtT.y };
921
+ const intersection = lli4(p, p2, o, points[t + 1]);
922
+ if (intersection) {
923
+ np[t + 1] = intersection;
924
+ } else {
925
+ const originalPoint = points[t + 1];
926
+ const normal = curve.normal((t + 1) / order).direction;
1000
927
  np[t + 1] = {
1001
- x: p.x + rc * ov.x,
1002
- y: p.y + rc * ov.y,
928
+ x: originalPoint.x + (t ? r2 : r1) * normal.x,
929
+ y: originalPoint.y + (t ? r2 : r1) * normal.y
1003
930
  };
931
+ }
1004
932
  });
1005
933
  return new BCurve(np);
934
+ }
935
+ [0, 1].forEach(function(t) {
936
+ if (order === 2 && !!t)
937
+ return;
938
+ const p = points[t + 1];
939
+ const ov = {
940
+ x: p.x - o.x,
941
+ y: p.y - o.y
942
+ };
943
+ let rc = distanceFn((t + 1) / order);
944
+ const m = Math.sqrt(ov.x * ov.x + ov.y * ov.y);
945
+ ov.x /= m;
946
+ ov.y /= m;
947
+ np[t + 1] = {
948
+ x: p.x + rc * ov.x,
949
+ y: p.y + rc * ov.y
950
+ };
951
+ });
952
+ return new BCurve(np);
1006
953
  }
1007
954
  function alignPointsToLine(points, line) {
1008
- const tx = line.p1.x, ty = line.p1.y, a = -Math.atan2(line.p2.y - ty, line.p2.x - tx), d = function (v) {
1009
- return {
1010
- x: (v.x - tx) * Math.cos(a) - (v.y - ty) * Math.sin(a),
1011
- y: (v.x - tx) * Math.sin(a) + (v.y - ty) * Math.cos(a),
1012
- };
955
+ const tx = line.p1.x, ty = line.p1.y, a = -Math.atan2(line.p2.y - ty, line.p2.x - tx), d = function(v) {
956
+ return {
957
+ x: (v.x - tx) * Math.cos(a) - (v.y - ty) * Math.sin(a),
958
+ y: (v.x - tx) * Math.sin(a) + (v.y - ty) * Math.cos(a)
1013
959
  };
1014
- return points.map(d);
960
+ };
961
+ return points.map(d);
962
+ }
963
+ function map(v, ds, de, ts, te) {
964
+ const d1 = de - ds, d2 = te - ts, v2 = v - ds, r = v2 / d1;
965
+ return ts + d2 * r;
1015
966
  }
967
+
1016
968
  class TValOutofBoundError extends Error {
1017
- constructor(message) {
1018
- super(message);
1019
- }
969
+ constructor(message) {
970
+ super(message);
971
+ }
1020
972
  }
1021
973
  function AABBIntersects(AABB1, AABB2) {
1022
- 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)) {
1023
- return true;
1024
- }
1025
- return false;
974
+ 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)) {
975
+ return true;
976
+ }
977
+ return false;
1026
978
  }
1027
979
  function approximately(a, b, precision) {
1028
- const epsilon = 0.000001;
1029
- return Math.abs(a - b) <= (precision || epsilon);
980
+ const epsilon = 0.000001;
981
+ return Math.abs(a - b) <= (precision || epsilon);
1030
982
  }
1031
983
  function cuberoot2(v) {
1032
- if (v < 0)
1033
- return -Math.pow(-v, 1 / 3);
1034
- return Math.pow(v, 1 / 3);
984
+ if (v < 0)
985
+ return -Math.pow(-v, 1 / 3);
986
+ return Math.pow(v, 1 / 3);
1035
987
  }
1036
988
  function accept(t) {
1037
- return 0 <= t && t <= 1;
989
+ return 0 <= t && t <= 1;
1038
990
  }
1039
991
  function getCubicRoots(pa, pb, pc, pd) {
1040
- let a = (3 * pa - 6 * pb + 3 * pc), b = (-3 * pa + 3 * pb), c = pa, d = (-pa + 3 * pb - 3 * pc + pd);
1041
- // do a check to see whether we even need cubic solving:
1042
- if (approximately(d, 0)) {
1043
- // this is not a cubic curve.
1044
- if (approximately(a, 0)) {
1045
- // in fact, this is not a quadratic curve either.
1046
- if (approximately(b, 0)) {
1047
- // in fact in fact, there are no solutions.
1048
- return [];
1049
- }
1050
- // linear solution
1051
- return [-c / b].filter(accept);
1052
- }
1053
- // quadratic solution
1054
- let q = Math.sqrt(b * b - 4 * a * c), a2 = 2 * a;
1055
- return [(q - b) / a2, (-b - q) / a2].filter(accept);
1056
- }
1057
- // at this point, we know we need a cubic solution.
1058
- a /= d;
1059
- b /= d;
1060
- c /= d;
1061
- 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;
1062
- // and some variables we're going to use later on:
1063
- let u1, v1, root1, root2, root3;
1064
- // three possible real roots:
1065
- if (discriminant < 0) {
1066
- 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;
1067
- root1 = t1 * Math.cos(phi / 3) - a / 3;
1068
- root2 = t1 * Math.cos((phi + 2 * Math.PI) / 3) - a / 3;
1069
- root3 = t1 * Math.cos((phi + 4 * Math.PI) / 3) - a / 3;
1070
- return [root1, root2, root3].filter(accept);
1071
- }
1072
- // three real roots, but two of them are equal:
1073
- if (discriminant === 0) {
1074
- u1 = q2 < 0 ? cuberoot2(-q2) : -cuberoot2(q2);
1075
- root1 = 2 * u1 - a / 3;
1076
- root2 = -u1 - a / 3;
1077
- return [root1, root2].filter(accept);
1078
- }
1079
- // one real root, two complex roots
1080
- var sd = Math.sqrt(discriminant);
1081
- u1 = cuberoot2(sd - q2);
1082
- v1 = cuberoot2(sd + q2);
1083
- root1 = u1 - v1 - a / 3;
1084
- return [root1].filter(accept);
992
+ let a = 3 * pa - 6 * pb + 3 * pc, b = -3 * pa + 3 * pb, c = pa, d = -pa + 3 * pb - 3 * pc + pd;
993
+ if (approximately(d, 0)) {
994
+ if (approximately(a, 0)) {
995
+ if (approximately(b, 0)) {
996
+ return [];
997
+ }
998
+ return [-c / b].filter(accept);
999
+ }
1000
+ let q3 = Math.sqrt(b * b - 4 * a * c), a2 = 2 * a;
1001
+ return [(q3 - b) / a2, (-b - q3) / a2].filter(accept);
1002
+ }
1003
+ a /= d;
1004
+ b /= d;
1005
+ c /= d;
1006
+ 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;
1007
+ let u1, v1, root1, root2, root3;
1008
+ if (discriminant < 0) {
1009
+ 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;
1010
+ root1 = t1 * Math.cos(phi / 3) - a / 3;
1011
+ root2 = t1 * Math.cos((phi + 2 * Math.PI) / 3) - a / 3;
1012
+ root3 = t1 * Math.cos((phi + 4 * Math.PI) / 3) - a / 3;
1013
+ return [root1, root2, root3].filter(accept);
1014
+ }
1015
+ if (discriminant === 0) {
1016
+ u1 = q2 < 0 ? cuberoot2(-q2) : -cuberoot2(q2);
1017
+ root1 = 2 * u1 - a / 3;
1018
+ root2 = -u1 - a / 3;
1019
+ return [root1, root2].filter(accept);
1020
+ }
1021
+ var sd = Math.sqrt(discriminant);
1022
+ u1 = cuberoot2(sd - q2);
1023
+ v1 = cuberoot2(sd + q2);
1024
+ root1 = u1 - v1 - a / 3;
1025
+ return [root1].filter(accept);
1085
1026
  }
1086
1027
  function getIntersectionsBetweenCurves(curve, curve2, deduplicationTolerance = 0.01) {
1087
- const threshold = 0.5;
1088
- let pairs = [{ curve1: { curve: curve, startTVal: 0, endTVal: 1 }, curve2: { curve: curve2, startTVal: 0, endTVal: 1 } }];
1089
- const finalRes = [];
1090
- while (pairs.length > 0) {
1091
- let curLength = pairs.length;
1092
- for (let index = 0; index < curLength; index++) {
1093
- let pair = pairs.shift();
1094
- if (pair == undefined) {
1095
- break;
1096
- }
1097
- let aabb1 = pair.curve1.curve.AABB;
1098
- let aabb2 = pair.curve2.curve.AABB;
1099
- let intersects = AABBIntersects(aabb1, aabb2);
1100
- if (pair.curve1.curve.fullLength < threshold && pair.curve2.curve.fullLength < threshold) {
1101
- 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 });
1102
- continue;
1103
- }
1104
- if (intersects) {
1105
- let [subCurveControlPoints1, subCurveControlPoints2] = pair.curve1.curve.split(0.5);
1106
- let [subCurveControlPoints3, subCurveControlPoints4] = pair.curve2.curve.split(0.5);
1107
- pairs.push({
1108
- curve1: {
1109
- curve: new BCurve(subCurveControlPoints1),
1110
- startTVal: pair.curve1.startTVal,
1111
- endTVal: pair.curve1.startTVal + (pair.curve1.endTVal - pair.curve1.startTVal) * 0.5
1112
- }, curve2: {
1113
- curve: new BCurve(subCurveControlPoints3),
1114
- startTVal: pair.curve2.startTVal,
1115
- endTVal: pair.curve2.startTVal + (pair.curve2.endTVal - pair.curve2.startTVal) * 0.5
1116
- }
1117
- });
1118
- pairs.push({
1119
- curve1: {
1120
- curve: new BCurve(subCurveControlPoints1),
1121
- startTVal: pair.curve1.startTVal,
1122
- endTVal: pair.curve1.startTVal + (pair.curve1.endTVal - pair.curve1.startTVal) * 0.5
1123
- }, curve2: {
1124
- curve: new BCurve(subCurveControlPoints4),
1125
- startTVal: pair.curve2.startTVal + (pair.curve2.endTVal - pair.curve2.startTVal) * 0.5,
1126
- endTVal: pair.curve2.endTVal
1127
- }
1128
- });
1129
- pairs.push({
1130
- curve1: {
1131
- curve: new BCurve(subCurveControlPoints2),
1132
- startTVal: pair.curve1.startTVal + (pair.curve1.endTVal - pair.curve1.startTVal) * 0.5,
1133
- endTVal: pair.curve1.endTVal
1134
- }, curve2: {
1135
- curve: new BCurve(subCurveControlPoints3),
1136
- startTVal: pair.curve2.startTVal,
1137
- endTVal: pair.curve2.startTVal + (pair.curve2.endTVal - pair.curve2.startTVal) * 0.5
1138
- }
1139
- });
1140
- pairs.push({
1141
- curve1: {
1142
- curve: new BCurve(subCurveControlPoints2),
1143
- startTVal: pair.curve1.startTVal + (pair.curve1.endTVal - pair.curve1.startTVal) * 0.5,
1144
- endTVal: pair.curve1.endTVal
1145
- }, curve2: {
1146
- curve: new BCurve(subCurveControlPoints4),
1147
- startTVal: pair.curve2.startTVal + (pair.curve2.endTVal - pair.curve2.startTVal) * 0.5,
1148
- endTVal: pair.curve2.endTVal
1149
- }
1150
- });
1151
- }
1152
- }
1153
- }
1154
- // Improved deduplication logic that handles close tvals on both curves
1155
- const tVals = [];
1156
- // Sort intersections by tVal1 for more predictable deduplication
1157
- finalRes.sort((a, b) => a.tVal1 - b.tVal1);
1158
- for (const intersection of finalRes) {
1159
- let isDuplicate = false;
1160
- // Check against all existing intersections
1161
- for (const existing of tVals) {
1162
- // Check if this intersection is close to an existing one on either curve
1163
- const selfTClose = approximately(intersection.tVal1, existing.selfT, deduplicationTolerance);
1164
- const otherTClose = approximately(intersection.tVal2, existing.otherT, deduplicationTolerance);
1165
- // Consider it a duplicate if both t-values are close
1166
- if (selfTClose && otherTClose) {
1167
- isDuplicate = true;
1168
- break;
1169
- }
1170
- // Also check for cases where intersections might be very close on the same curve
1171
- // This handles cases where curves are nearly tangent or have very close intersections
1172
- const selfTVeryClose = approximately(intersection.tVal1, existing.selfT, deduplicationTolerance * 10);
1173
- const otherTVeryClose = approximately(intersection.tVal2, existing.otherT, deduplicationTolerance * 10);
1174
- if (selfTVeryClose || otherTVeryClose) {
1175
- // Additional check: if the intersection points are very close spatially
1176
- const point1 = curve.get(intersection.tVal1);
1177
- const point2 = curve2.get(intersection.tVal2);
1178
- const existingPoint1 = curve.get(existing.selfT);
1179
- const existingPoint2 = curve2.get(existing.otherT);
1180
- const distance1 = PointCal.distanceBetweenPoints(point1, existingPoint1);
1181
- const distance2 = PointCal.distanceBetweenPoints(point2, existingPoint2);
1182
- // If both intersection points are spatially very close, consider it a duplicate
1183
- if (distance1 < deduplicationTolerance * 100 && distance2 < deduplicationTolerance * 100) {
1184
- isDuplicate = true;
1185
- break;
1186
- }
1187
- }
1188
- }
1189
- if (!isDuplicate) {
1190
- tVals.push({ selfT: intersection.tVal1, otherT: intersection.tVal2 });
1191
- }
1192
- }
1193
- return tVals;
1028
+ const threshold = 0.5;
1029
+ let pairs = [{ curve1: { curve, startTVal: 0, endTVal: 1 }, curve2: { curve: curve2, startTVal: 0, endTVal: 1 } }];
1030
+ const finalRes = [];
1031
+ while (pairs.length > 0) {
1032
+ let curLength = pairs.length;
1033
+ for (let index = 0;index < curLength; index++) {
1034
+ let pair = pairs.shift();
1035
+ if (pair == undefined) {
1036
+ break;
1037
+ }
1038
+ let aabb1 = pair.curve1.curve.AABB;
1039
+ let aabb2 = pair.curve2.curve.AABB;
1040
+ let intersects = AABBIntersects(aabb1, aabb2);
1041
+ if (pair.curve1.curve.fullLength < threshold && pair.curve2.curve.fullLength < threshold) {
1042
+ 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 });
1043
+ continue;
1044
+ }
1045
+ if (intersects) {
1046
+ let [subCurveControlPoints1, subCurveControlPoints2] = pair.curve1.curve.split(0.5);
1047
+ let [subCurveControlPoints3, subCurveControlPoints4] = pair.curve2.curve.split(0.5);
1048
+ pairs.push({
1049
+ curve1: {
1050
+ curve: new BCurve(subCurveControlPoints1),
1051
+ startTVal: pair.curve1.startTVal,
1052
+ endTVal: pair.curve1.startTVal + (pair.curve1.endTVal - pair.curve1.startTVal) * 0.5
1053
+ },
1054
+ curve2: {
1055
+ curve: new BCurve(subCurveControlPoints3),
1056
+ startTVal: pair.curve2.startTVal,
1057
+ endTVal: pair.curve2.startTVal + (pair.curve2.endTVal - pair.curve2.startTVal) * 0.5
1058
+ }
1059
+ });
1060
+ pairs.push({
1061
+ curve1: {
1062
+ curve: new BCurve(subCurveControlPoints1),
1063
+ startTVal: pair.curve1.startTVal,
1064
+ endTVal: pair.curve1.startTVal + (pair.curve1.endTVal - pair.curve1.startTVal) * 0.5
1065
+ },
1066
+ curve2: {
1067
+ curve: new BCurve(subCurveControlPoints4),
1068
+ startTVal: pair.curve2.startTVal + (pair.curve2.endTVal - pair.curve2.startTVal) * 0.5,
1069
+ endTVal: pair.curve2.endTVal
1070
+ }
1071
+ });
1072
+ pairs.push({
1073
+ curve1: {
1074
+ curve: new BCurve(subCurveControlPoints2),
1075
+ startTVal: pair.curve1.startTVal + (pair.curve1.endTVal - pair.curve1.startTVal) * 0.5,
1076
+ endTVal: pair.curve1.endTVal
1077
+ },
1078
+ curve2: {
1079
+ curve: new BCurve(subCurveControlPoints3),
1080
+ startTVal: pair.curve2.startTVal,
1081
+ endTVal: pair.curve2.startTVal + (pair.curve2.endTVal - pair.curve2.startTVal) * 0.5
1082
+ }
1083
+ });
1084
+ pairs.push({
1085
+ curve1: {
1086
+ curve: new BCurve(subCurveControlPoints2),
1087
+ startTVal: pair.curve1.startTVal + (pair.curve1.endTVal - pair.curve1.startTVal) * 0.5,
1088
+ endTVal: pair.curve1.endTVal
1089
+ },
1090
+ curve2: {
1091
+ curve: new BCurve(subCurveControlPoints4),
1092
+ startTVal: pair.curve2.startTVal + (pair.curve2.endTVal - pair.curve2.startTVal) * 0.5,
1093
+ endTVal: pair.curve2.endTVal
1094
+ }
1095
+ });
1096
+ }
1097
+ }
1098
+ }
1099
+ const tVals = [];
1100
+ finalRes.sort((a, b) => a.tVal1 - b.tVal1);
1101
+ for (const intersection of finalRes) {
1102
+ let isDuplicate = false;
1103
+ for (const existing of tVals) {
1104
+ const selfTClose = approximately(intersection.tVal1, existing.selfT, deduplicationTolerance);
1105
+ const otherTClose = approximately(intersection.tVal2, existing.otherT, deduplicationTolerance);
1106
+ if (selfTClose && otherTClose) {
1107
+ isDuplicate = true;
1108
+ break;
1109
+ }
1110
+ const selfTVeryClose = approximately(intersection.tVal1, existing.selfT, deduplicationTolerance * 10);
1111
+ const otherTVeryClose = approximately(intersection.tVal2, existing.otherT, deduplicationTolerance * 10);
1112
+ if (selfTVeryClose || otherTVeryClose) {
1113
+ const point1 = curve.get(intersection.tVal1);
1114
+ const point2 = curve2.get(intersection.tVal2);
1115
+ const existingPoint1 = curve.get(existing.selfT);
1116
+ const existingPoint2 = curve2.get(existing.otherT);
1117
+ const distance1 = PointCal.distanceBetweenPoints(point1, existingPoint1);
1118
+ const distance2 = PointCal.distanceBetweenPoints(point2, existingPoint2);
1119
+ if (distance1 < deduplicationTolerance * 100 && distance2 < deduplicationTolerance * 100) {
1120
+ isDuplicate = true;
1121
+ break;
1122
+ }
1123
+ }
1124
+ }
1125
+ if (!isDuplicate) {
1126
+ tVals.push({ selfT: intersection.tVal1, otherT: intersection.tVal2 });
1127
+ }
1128
+ }
1129
+ return tVals;
1194
1130
  }
1195
1131
  function solveCubic(a, b, c, d) {
1196
- if (Math.abs(a) < 1e-8) { // Quadratic case, ax^2+bx+c=0
1197
- a = b;
1198
- b = c;
1199
- c = d;
1200
- if (Math.abs(a) < 1e-8) { // Linear case, ax+b=0
1201
- a = b;
1202
- b = c;
1203
- if (Math.abs(a) < 1e-8) // Degenerate case
1204
- return [];
1205
- return [-b / a];
1206
- }
1207
- let D = b * b - 4 * a * c;
1208
- if (Math.abs(D) < 1e-8)
1209
- return [-b / (2 * a)];
1210
- else if (D > 0)
1211
- return [(-b + Math.sqrt(D)) / (2 * a), (-b - Math.sqrt(D)) / (2 * a)];
1132
+ if (Math.abs(a) < 0.00000001) {
1133
+ a = b;
1134
+ b = c;
1135
+ c = d;
1136
+ if (Math.abs(a) < 0.00000001) {
1137
+ a = b;
1138
+ b = c;
1139
+ if (Math.abs(a) < 0.00000001)
1212
1140
  return [];
1213
- }
1214
- // Convert to depressed cubic t^3+pt+q = 0 (subst x = t - b/3a)
1215
- let p = (3 * a * c - b * b) / (3 * a * a);
1216
- let q = (2 * b * b * b - 9 * a * b * c + 27 * a * a * d) / (27 * a * a * a);
1217
- let roots;
1218
- if (Math.abs(p) < 1e-8) { // p = 0 -> t^3 = -q -> t = -q^1/3
1219
- roots = [cuberoot(-q)];
1220
- }
1221
- else if (Math.abs(q) < 1e-8) { // q = 0 -> t^3 + pt = 0 -> t(t^2+p)=0
1222
- roots = [0].concat(p < 0 ? [Math.sqrt(-p), -Math.sqrt(-p)] : []);
1223
- }
1224
- else {
1225
- let D = q * q / 4 + p * p * p / 27;
1226
- if (Math.abs(D) < 1e-8) { // D = 0 -> two roots
1227
- roots = [-1.5 * q / p, 3 * q / p];
1228
- }
1229
- else if (D > 0) { // Only one real root and two complex roots
1230
- let u = cuberoot(-q / 2 - Math.sqrt(D));
1231
- //console.log("Complext Root 1 real:", -(u+v)/2.0 - a / 3.0, "imaginary:", Math.sqrt(3) / 2.0 * (v - u));
1232
- //console.log("Complext Root 2 real:", -(u+v)/2.0 - a / 3.0, "imaginary:",-1 * Math.sqrt(3) / 2.0 * (v - u));
1233
- roots = [u - p / (3 * u)];
1234
- }
1235
- else { // D < 0, three roots, but needs to use complex numbers/trigonometric solution
1236
- let u = 2 * Math.sqrt(-p / 3);
1237
- let t = Math.acos(3 * q / p / u) / 3; // D < 0 implies p < 0 and acos argument in [-1..1]
1238
- let k = 2 * Math.PI / 3;
1239
- roots = [u * Math.cos(t), u * Math.cos(t - k), u * Math.cos(t - 2 * k)];
1240
- }
1241
- }
1242
- // Convert back from depressed cubic
1243
- for (let i = 0; i < roots.length; i++)
1244
- roots[i] -= b / (3 * a);
1245
- return roots;
1141
+ return [-b / a];
1142
+ }
1143
+ let D = b * b - 4 * a * c;
1144
+ if (Math.abs(D) < 0.00000001)
1145
+ return [-b / (2 * a)];
1146
+ else if (D > 0)
1147
+ return [(-b + Math.sqrt(D)) / (2 * a), (-b - Math.sqrt(D)) / (2 * a)];
1148
+ return [];
1149
+ }
1150
+ let p = (3 * a * c - b * b) / (3 * a * a);
1151
+ let q = (2 * b * b * b - 9 * a * b * c + 27 * a * a * d) / (27 * a * a * a);
1152
+ let roots;
1153
+ if (Math.abs(p) < 0.00000001) {
1154
+ roots = [cuberoot(-q)];
1155
+ } else if (Math.abs(q) < 0.00000001) {
1156
+ roots = [0].concat(p < 0 ? [Math.sqrt(-p), -Math.sqrt(-p)] : []);
1157
+ } else {
1158
+ let D = q * q / 4 + p * p * p / 27;
1159
+ if (Math.abs(D) < 0.00000001) {
1160
+ roots = [-1.5 * q / p, 3 * q / p];
1161
+ } else if (D > 0) {
1162
+ let u = cuberoot(-q / 2 - Math.sqrt(D));
1163
+ let v = cuberoot(-q / 2 + Math.sqrt(D));
1164
+ roots = [u - p / (3 * u)];
1165
+ } else {
1166
+ let u = 2 * Math.sqrt(-p / 3);
1167
+ let t = Math.acos(3 * q / p / u) / 3;
1168
+ let k = 2 * Math.PI / 3;
1169
+ roots = [u * Math.cos(t), u * Math.cos(t - k), u * Math.cos(t - 2 * k)];
1170
+ }
1171
+ }
1172
+ for (let i = 0;i < roots.length; i++)
1173
+ roots[i] -= b / (3 * a);
1174
+ return roots;
1246
1175
  }
1247
1176
  function cuberoot(x) {
1248
- var y = Math.pow(Math.abs(x), 1 / 3);
1249
- return x < 0 ? -y : y;
1177
+ var y = Math.pow(Math.abs(x), 1 / 3);
1178
+ return x < 0 ? -y : y;
1250
1179
  }
1251
1180
  function computeWithControlPoints(tVal, controlPoints) {
1252
- let points = [...controlPoints];
1253
- while (points.length > 1) {
1254
- let lowerLevelPoints = points.slice(1);
1255
- for (let index = 0; index < lowerLevelPoints.length; index++) {
1256
- lowerLevelPoints[index] = PointCal.addVector(PointCal.multiplyVectorByScalar(points[index], (1 - tVal)), PointCal.multiplyVectorByScalar(points[index + 1], tVal));
1257
- }
1258
- points = lowerLevelPoints;
1259
- }
1260
- return points[0];
1181
+ let points = [...controlPoints];
1182
+ while (points.length > 1) {
1183
+ let lowerLevelPoints = points.slice(1);
1184
+ for (let index = 0;index < lowerLevelPoints.length; index++) {
1185
+ lowerLevelPoints[index] = PointCal.addVector(PointCal.multiplyVectorByScalar(points[index], 1 - tVal), PointCal.multiplyVectorByScalar(points[index + 1], tVal));
1186
+ }
1187
+ points = lowerLevelPoints;
1188
+ }
1189
+ return points[0];
1261
1190
  }
1262
1191
  function curveIsSimple(curve) {
1263
- if (curve.getControlPoints().length === 4) {
1264
- const points = curve.getControlPoints();
1265
- const p0ToP3Vector = PointCal.subVector(points[3], points[0]);
1266
- const p0ToP1Vector = PointCal.subVector(points[1], points[0]);
1267
- const p0ToP2Vector = PointCal.subVector(points[2], points[0]);
1268
- const a1 = PointCal.angleFromA2B(p0ToP3Vector, p0ToP1Vector);
1269
- const a2 = PointCal.angleFromA2B(p0ToP3Vector, p0ToP2Vector);
1270
- if ((a1 > 0 && a2 < 0) || (a1 < 0 && a2 > 0))
1271
- return false;
1272
- }
1273
- const n1 = curve.normal(0).direction;
1274
- const n2 = curve.normal(1).direction;
1275
- let s = n1.x * n2.x + n1.y * n2.y;
1276
- // if (this._3d) {
1277
- // s += n1.z * n2.z;
1278
- // }
1279
- return Math.abs(Math.acos(s)) < Math.PI / 3;
1192
+ if (curve.getControlPoints().length === 4) {
1193
+ const points = curve.getControlPoints();
1194
+ const p0ToP3Vector = PointCal.subVector(points[3], points[0]);
1195
+ const p0ToP1Vector = PointCal.subVector(points[1], points[0]);
1196
+ const p0ToP2Vector = PointCal.subVector(points[2], points[0]);
1197
+ const a1 = PointCal.angleFromA2B(p0ToP3Vector, p0ToP1Vector);
1198
+ const a2 = PointCal.angleFromA2B(p0ToP3Vector, p0ToP2Vector);
1199
+ if (a1 > 0 && a2 < 0 || a1 < 0 && a2 > 0)
1200
+ return false;
1201
+ }
1202
+ const n1 = curve.normal(0).direction;
1203
+ const n2 = curve.normal(1).direction;
1204
+ let s = n1.x * n2.x + n1.y * n2.y;
1205
+ return Math.abs(Math.acos(s)) < Math.PI / 3;
1280
1206
  }
1281
1207
  function translate(curve, vector, d1, d2) {
1282
- const order = curve.getControlPoints().length - 1;
1283
- const points = curve.getControlPoints();
1284
- const d = points.map((_, i) => (1 - i / order) * d1 + (i / order) * d2);
1285
- return new BCurve(points.map((p, i) => ({ x: p.x + d[i] * vector.x, y: p.y + d[i] * vector.y })));
1208
+ const order = curve.getControlPoints().length - 1;
1209
+ const points = curve.getControlPoints();
1210
+ const d = points.map((_, i) => (1 - i / order) * d1 + i / order * d2);
1211
+ return new BCurve(points.map((p, i) => ({ x: p.x + d[i] * vector.x, y: p.y + d[i] * vector.y })));
1286
1212
  }
1213
+ // src/line.ts
1214
+ import { PointCal as PointCal2 } from "@ue-too/math";
1287
1215
 
1288
1216
  class Line {
1289
- constructor(startPoint, endPoint) {
1290
- this.startPoint = startPoint;
1291
- this.endPoint = endPoint;
1292
- }
1293
- getStartPoint() {
1294
- return this.startPoint;
1295
- }
1296
- getEndPoint() {
1297
- return this.endPoint;
1298
- }
1299
- intersectionWithAnotherLine(lineToIntersect) {
1300
- return getLineIntersection(this.startPoint, this.endPoint, lineToIntersect.getStartPoint(), lineToIntersect.getEndPoint());
1301
- }
1302
- projectPoint(point) {
1303
- return projectPointOntoLine(point, this.getStartPoint(), this.getEndPoint());
1304
- }
1305
- length() {
1306
- return PointCal.distanceBetweenPoints(this.startPoint, this.endPoint);
1307
- }
1308
- getTranslationRotationToAlginXAxis() {
1309
- const translation = PointCal.subVector({ x: 0, y: 0 }, this.startPoint);
1310
- const rotationAngle = PointCal.angleFromA2B(PointCal.subVector(this.endPoint, this.startPoint), { x: 1, y: 0 });
1311
- return { translation, rotationAngle };
1312
- }
1313
- pointInLine(point) {
1314
- const baseVector = PointCal.unitVectorFromA2B(this.startPoint, this.endPoint);
1315
- const start2PointVector = PointCal.subVector(point, this.startPoint);
1316
- const length = PointCal.dotProduct(start2PointVector, baseVector);
1317
- const start2PointUnitVector = PointCal.unitVector(start2PointVector);
1318
- PointCal.distanceBetweenPoints(this.startPoint, this.endPoint) * 0.0001;
1319
- 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;
1320
- }
1321
- lerp(ratio) {
1322
- return PointCal.linearInterpolation(this.startPoint, this.endPoint, ratio);
1323
- }
1217
+ startPoint;
1218
+ endPoint;
1219
+ constructor(startPoint, endPoint) {
1220
+ this.startPoint = startPoint;
1221
+ this.endPoint = endPoint;
1222
+ }
1223
+ getStartPoint() {
1224
+ return this.startPoint;
1225
+ }
1226
+ getEndPoint() {
1227
+ return this.endPoint;
1228
+ }
1229
+ intersectionWithAnotherLine(lineToIntersect) {
1230
+ return getLineIntersection(this.startPoint, this.endPoint, lineToIntersect.getStartPoint(), lineToIntersect.getEndPoint());
1231
+ }
1232
+ projectPoint(point) {
1233
+ return projectPointOntoLine(point, this.getStartPoint(), this.getEndPoint());
1234
+ }
1235
+ length() {
1236
+ return PointCal2.distanceBetweenPoints(this.startPoint, this.endPoint);
1237
+ }
1238
+ getTranslationRotationToAlginXAxis() {
1239
+ const translation = PointCal2.subVector({ x: 0, y: 0 }, this.startPoint);
1240
+ const rotationAngle = PointCal2.angleFromA2B(PointCal2.subVector(this.endPoint, this.startPoint), { x: 1, y: 0 });
1241
+ return { translation, rotationAngle };
1242
+ }
1243
+ pointInLine(point) {
1244
+ const baseVector = PointCal2.unitVectorFromA2B(this.startPoint, this.endPoint);
1245
+ const start2PointVector = PointCal2.subVector(point, this.startPoint);
1246
+ const length = PointCal2.dotProduct(start2PointVector, baseVector);
1247
+ const start2PointUnitVector = PointCal2.unitVector(start2PointVector);
1248
+ const errorThreshold = PointCal2.distanceBetweenPoints(this.startPoint, this.endPoint) * 0.0001;
1249
+ return length <= PointCal2.distanceBetweenPoints(this.startPoint, this.endPoint) && length >= 0 && Math.abs(start2PointUnitVector.x - baseVector.x) < 0.0001 && Math.abs(start2PointUnitVector.y - baseVector.y) < 0.0001;
1250
+ }
1251
+ lerp(ratio) {
1252
+ return PointCal2.linearInterpolation(this.startPoint, this.endPoint, ratio);
1253
+ }
1324
1254
  }
1325
1255
  function getLineIntersection(startPoint, endPoint, startPoint2, endPoint2) {
1326
- const numerator = (endPoint2.x - startPoint2.x) * (startPoint.y - startPoint2.y) - (endPoint2.y - startPoint2.y) * (startPoint.x - startPoint2.x);
1327
- const denominator = (endPoint2.y - startPoint2.y) * (endPoint.x - startPoint.x) - (endPoint2.x - startPoint2.x) * (endPoint.y - startPoint.y);
1328
- if (denominator === 0) {
1329
- return { intersects: false };
1330
- }
1331
- const t = numerator / denominator;
1332
- if (t >= 0 && t <= 1) {
1333
- return {
1334
- intersects: true,
1335
- intersection: PointCal.linearInterpolation(startPoint, endPoint, t),
1336
- offset: t
1337
- };
1338
- }
1339
- else {
1340
- return {
1341
- intersects: false,
1342
- };
1343
- }
1256
+ const numerator = (endPoint2.x - startPoint2.x) * (startPoint.y - startPoint2.y) - (endPoint2.y - startPoint2.y) * (startPoint.x - startPoint2.x);
1257
+ const denominator = (endPoint2.y - startPoint2.y) * (endPoint.x - startPoint.x) - (endPoint2.x - startPoint2.x) * (endPoint.y - startPoint.y);
1258
+ if (denominator === 0) {
1259
+ return { intersects: false };
1260
+ }
1261
+ const t = numerator / denominator;
1262
+ if (t >= 0 && t <= 1) {
1263
+ return {
1264
+ intersects: true,
1265
+ intersection: PointCal2.linearInterpolation(startPoint, endPoint, t),
1266
+ offset: t
1267
+ };
1268
+ } else {
1269
+ return {
1270
+ intersects: false
1271
+ };
1272
+ }
1344
1273
  }
1345
1274
  function projectPointOntoLine(point, lineStartPoint, lineEndPoint) {
1346
- const baseVector = PointCal.unitVector(PointCal.subVector(lineEndPoint, lineStartPoint));
1347
- const vectorToPoint = PointCal.subVector(point, lineStartPoint);
1348
- const res = PointCal.dotProduct(vectorToPoint, baseVector);
1349
- if (res < 0 || res > PointCal.magnitude(PointCal.subVector(lineEndPoint, lineStartPoint))) {
1350
- return {
1351
- within: false,
1352
- };
1353
- }
1275
+ const baseVector = PointCal2.unitVector(PointCal2.subVector(lineEndPoint, lineStartPoint));
1276
+ const vectorToPoint = PointCal2.subVector(point, lineStartPoint);
1277
+ const res = PointCal2.dotProduct(vectorToPoint, baseVector);
1278
+ if (res < 0 || res > PointCal2.magnitude(PointCal2.subVector(lineEndPoint, lineStartPoint))) {
1354
1279
  return {
1355
- within: true,
1356
- projectionPoint: PointCal.addVector(lineStartPoint, PointCal.multiplyVectorByScalar(baseVector, res)),
1357
- offset: res / PointCal.magnitude(PointCal.subVector(lineEndPoint, lineStartPoint))
1280
+ within: false
1358
1281
  };
1282
+ }
1283
+ return {
1284
+ within: true,
1285
+ projectionPoint: PointCal2.addVector(lineStartPoint, PointCal2.multiplyVectorByScalar(baseVector, res)),
1286
+ offset: res / PointCal2.magnitude(PointCal2.subVector(lineEndPoint, lineStartPoint))
1287
+ };
1359
1288
  }
1289
+ // src/composite-curve.ts
1290
+ import { PointCal as PointCal3 } from "@ue-too/math";
1360
1291
 
1361
1292
  class ControlPoint {
1362
- constructor(position, leftHandle, rightHandle) {
1363
- this.position = position;
1364
- this.leftHandle = leftHandle;
1365
- this.rightHandle = rightHandle;
1366
- }
1367
- setPosition(destinationPosition, prevControlPoint, nextControlPoint) {
1368
- let diff = PointCal.subVector(destinationPosition, this.position);
1369
- this.position = destinationPosition;
1370
- this.leftHandle.position = PointCal.addVector(this.leftHandle.position, diff);
1371
- this.rightHandle.position = PointCal.addVector(this.rightHandle.position, diff);
1372
- if (this.leftHandle.type == "VECTOR" && prevControlPoint) {
1373
- let relativeVector = PointCal.subVector(prevControlPoint.getPosition(), this.position);
1374
- relativeVector = PointCal.multiplyVectorByScalar(relativeVector, 1 / 3);
1375
- this.leftHandle.position = PointCal.addVector(this.position, relativeVector);
1376
- if (this.rightHandle.type == "ALIGNED") {
1377
- let relativeVector = PointCal.subVector(this.rightHandle.position, this.position);
1378
- let mag = PointCal.magnitude(relativeVector);
1379
- let direction = PointCal.unitVectorFromA2B(this.leftHandle.position, this.position);
1380
- this.rightHandle.position = PointCal.addVector(this.position, PointCal.multiplyVectorByScalar(direction, mag));
1381
- }
1382
- }
1383
- if (this.rightHandle.type == "VECTOR" && nextControlPoint) {
1384
- let relativeVector = PointCal.subVector(nextControlPoint.getPosition(), this.position);
1385
- relativeVector = PointCal.multiplyVectorByScalar(relativeVector, 1 / 3);
1386
- this.rightHandle.position = PointCal.addVector(this.position, relativeVector);
1387
- if (this.leftHandle.type == "ALIGNED") {
1388
- let mag = PointCal.distanceBetweenPoints(this.leftHandle.position, this.position);
1389
- let direction = PointCal.subVector(this.position, this.rightHandle.position);
1390
- this.leftHandle.position = PointCal.addVector(this.position, PointCal.multiplyVectorByScalar(direction, mag));
1391
- }
1392
- }
1393
- if (prevControlPoint !== undefined && prevControlPoint.getRightHandle().type == "VECTOR") {
1394
- let relativeVector = PointCal.subVector(this.position, prevControlPoint.getPosition());
1395
- relativeVector = PointCal.multiplyVectorByScalar(relativeVector, 1 / 3);
1396
- prevControlPoint.setRightHandlePosition(PointCal.addVector(prevControlPoint.getPosition(), relativeVector));
1397
- }
1398
- if (nextControlPoint !== undefined && nextControlPoint.getLeftHandle().type == "VECTOR") {
1399
- let relativeVector = PointCal.subVector(this.position, nextControlPoint.getPosition());
1400
- relativeVector = PointCal.multiplyVectorByScalar(relativeVector, 1 / 3);
1401
- nextControlPoint.setLeftHandlePosition(PointCal.addVector(nextControlPoint.getPosition(), relativeVector));
1402
- }
1403
- }
1404
- getPosition() {
1405
- return this.position;
1406
- }
1407
- setLeftHandleTypeVector(prevControlPoint) {
1408
- if (this.rightHandle.type != "VECTOR") {
1409
- this.rightHandle.type = "FREE";
1410
- }
1411
- let relativeVector;
1412
- if (prevControlPoint == undefined) {
1413
- relativeVector = PointCal.subVector(this.leftHandle.position, this.position);
1414
- }
1415
- else {
1416
- relativeVector = PointCal.subVector(prevControlPoint.getPosition(), this.position);
1417
- relativeVector = PointCal.multiplyVectorByScalar(relativeVector, 1 / 3);
1418
- }
1419
- this.leftHandle.position = PointCal.addVector(this.position, relativeVector);
1420
- }
1421
- setLeftHandleTypeAligned() {
1422
- this.leftHandle.type = "ALIGNED";
1293
+ position;
1294
+ leftHandle;
1295
+ rightHandle;
1296
+ constructor(position, leftHandle, rightHandle) {
1297
+ this.position = position;
1298
+ this.leftHandle = leftHandle;
1299
+ this.rightHandle = rightHandle;
1300
+ }
1301
+ setPosition(destinationPosition, prevControlPoint, nextControlPoint) {
1302
+ let diff = PointCal3.subVector(destinationPosition, this.position);
1303
+ this.position = destinationPosition;
1304
+ this.leftHandle.position = PointCal3.addVector(this.leftHandle.position, diff);
1305
+ this.rightHandle.position = PointCal3.addVector(this.rightHandle.position, diff);
1306
+ if (this.leftHandle.type == "VECTOR" && prevControlPoint) {
1307
+ let relativeVector = PointCal3.subVector(prevControlPoint.getPosition(), this.position);
1308
+ relativeVector = PointCal3.multiplyVectorByScalar(relativeVector, 1 / 3);
1309
+ this.leftHandle.position = PointCal3.addVector(this.position, relativeVector);
1310
+ if (this.rightHandle.type == "ALIGNED") {
1311
+ let relativeVector2 = PointCal3.subVector(this.rightHandle.position, this.position);
1312
+ let mag = PointCal3.magnitude(relativeVector2);
1313
+ let direction = PointCal3.unitVectorFromA2B(this.leftHandle.position, this.position);
1314
+ this.rightHandle.position = PointCal3.addVector(this.position, PointCal3.multiplyVectorByScalar(direction, mag));
1315
+ }
1316
+ }
1317
+ if (this.rightHandle.type == "VECTOR" && nextControlPoint) {
1318
+ let relativeVector = PointCal3.subVector(nextControlPoint.getPosition(), this.position);
1319
+ relativeVector = PointCal3.multiplyVectorByScalar(relativeVector, 1 / 3);
1320
+ this.rightHandle.position = PointCal3.addVector(this.position, relativeVector);
1321
+ if (this.leftHandle.type == "ALIGNED") {
1322
+ let mag = PointCal3.distanceBetweenPoints(this.leftHandle.position, this.position);
1323
+ let direction = PointCal3.subVector(this.position, this.rightHandle.position);
1324
+ this.leftHandle.position = PointCal3.addVector(this.position, PointCal3.multiplyVectorByScalar(direction, mag));
1325
+ }
1326
+ }
1327
+ if (prevControlPoint !== undefined && prevControlPoint.getRightHandle().type == "VECTOR") {
1328
+ let relativeVector = PointCal3.subVector(this.position, prevControlPoint.getPosition());
1329
+ relativeVector = PointCal3.multiplyVectorByScalar(relativeVector, 1 / 3);
1330
+ prevControlPoint.setRightHandlePosition(PointCal3.addVector(prevControlPoint.getPosition(), relativeVector));
1331
+ }
1332
+ if (nextControlPoint !== undefined && nextControlPoint.getLeftHandle().type == "VECTOR") {
1333
+ let relativeVector = PointCal3.subVector(this.position, nextControlPoint.getPosition());
1334
+ relativeVector = PointCal3.multiplyVectorByScalar(relativeVector, 1 / 3);
1335
+ nextControlPoint.setLeftHandlePosition(PointCal3.addVector(nextControlPoint.getPosition(), relativeVector));
1336
+ }
1337
+ }
1338
+ getPosition() {
1339
+ return this.position;
1340
+ }
1341
+ setLeftHandleTypeVector(prevControlPoint) {
1342
+ if (this.rightHandle.type != "VECTOR") {
1343
+ this.rightHandle.type = "FREE";
1344
+ }
1345
+ let relativeVector;
1346
+ if (prevControlPoint == undefined) {
1347
+ relativeVector = PointCal3.subVector(this.leftHandle.position, this.position);
1348
+ } else {
1349
+ relativeVector = PointCal3.subVector(prevControlPoint.getPosition(), this.position);
1350
+ relativeVector = PointCal3.multiplyVectorByScalar(relativeVector, 1 / 3);
1351
+ }
1352
+ this.leftHandle.position = PointCal3.addVector(this.position, relativeVector);
1353
+ }
1354
+ setLeftHandleTypeAligned() {
1355
+ this.leftHandle.type = "ALIGNED";
1356
+ if (this.rightHandle.type == "VECTOR") {
1357
+ let direction = PointCal3.unitVectorFromA2B(this.rightHandle.position, this.position);
1358
+ let mag = PointCal3.distanceBetweenPoints(this.position, this.leftHandle.position);
1359
+ this.leftHandle.position = PointCal3.addVector(this.position, PointCal3.multiplyVectorByScalar(direction, mag));
1360
+ }
1361
+ }
1362
+ setLeftHandleTypeFree() {
1363
+ this.leftHandle.type = "FREE";
1364
+ }
1365
+ setRightHandleTypeVector(nextControlPoint) {
1366
+ if (this.leftHandle.type != "VECTOR") {
1367
+ this.leftHandle.type = "FREE";
1368
+ }
1369
+ let relativeVector;
1370
+ if (nextControlPoint == undefined) {
1371
+ relativeVector = PointCal3.subVector(this.rightHandle.position, this.position);
1372
+ } else {
1373
+ relativeVector = PointCal3.subVector(nextControlPoint.getPosition(), this.position);
1374
+ relativeVector = PointCal3.multiplyVectorByScalar(relativeVector, 1 / 3);
1375
+ }
1376
+ this.rightHandle.position = PointCal3.addVector(this.position, relativeVector);
1377
+ }
1378
+ setRightHandleTypeAligned() {
1379
+ this.rightHandle.type = "ALIGNED";
1380
+ if (this.leftHandle.type == "VECTOR") {
1381
+ let direciton = PointCal3.unitVectorFromA2B(this.leftHandle.position, this.position);
1382
+ let mag = PointCal3.distanceBetweenPoints(this.position, this.rightHandle.position);
1383
+ this.rightHandle.position = PointCal3.addVector(this.position, PointCal3.multiplyVectorByScalar(direciton, mag));
1384
+ }
1385
+ }
1386
+ setRightHandleTypeFree() {
1387
+ this.rightHandle.type = "FREE";
1388
+ }
1389
+ setLeftHandlePosition(destPos) {
1390
+ let leftHandleType = this.leftHandle.type;
1391
+ switch (leftHandleType) {
1392
+ case "ALIGNED":
1423
1393
  if (this.rightHandle.type == "VECTOR") {
1424
- let direction = PointCal.unitVectorFromA2B(this.rightHandle.position, this.position);
1425
- let mag = PointCal.distanceBetweenPoints(this.position, this.leftHandle.position);
1426
- this.leftHandle.position = PointCal.addVector(this.position, PointCal.multiplyVectorByScalar(direction, mag));
1427
- }
1428
- }
1429
- setLeftHandleTypeFree() {
1430
- this.leftHandle.type = "FREE";
1431
- }
1432
- setRightHandleTypeVector(nextControlPoint) {
1433
- if (this.leftHandle.type != "VECTOR") {
1434
- this.leftHandle.type = "FREE";
1435
- }
1436
- let relativeVector;
1437
- if (nextControlPoint == undefined) {
1438
- relativeVector = PointCal.subVector(this.rightHandle.position, this.position);
1439
- }
1440
- else {
1441
- relativeVector = PointCal.subVector(nextControlPoint.getPosition(), this.position);
1442
- relativeVector = PointCal.multiplyVectorByScalar(relativeVector, 1 / 3);
1443
- }
1444
- this.rightHandle.position = PointCal.addVector(this.position, relativeVector);
1445
- }
1446
- setRightHandleTypeAligned() {
1447
- this.rightHandle.type = "ALIGNED";
1394
+ let diff = PointCal3.subVector(destPos, this.position);
1395
+ let rightHandleDiff = PointCal3.unitVectorFromA2B(this.rightHandle.position, this.position);
1396
+ let resMag = PointCal3.dotProduct(diff, rightHandleDiff);
1397
+ let res = PointCal3.multiplyVectorByScalar(rightHandleDiff, resMag);
1398
+ this.leftHandle.position = PointCal3.addVector(this.position, res);
1399
+ } else if (this.rightHandle.type == "ALIGNED") {
1400
+ this.leftHandle.position = destPos;
1401
+ let mag = PointCal3.distanceBetweenPoints(this.rightHandle.position, this.position);
1402
+ let direction = PointCal3.unitVectorFromA2B(this.leftHandle.position, this.position);
1403
+ let res = PointCal3.multiplyVectorByScalar(direction, mag);
1404
+ this.rightHandle.position = PointCal3.addVector(res, this.position);
1405
+ } else {
1406
+ this.leftHandle.position = destPos;
1407
+ }
1408
+ break;
1409
+ case "FREE":
1410
+ this.leftHandle.position = destPos;
1411
+ break;
1412
+ case "VECTOR":
1413
+ break;
1414
+ default:
1415
+ throw new Error(`Unknown left handle type for control point`);
1416
+ }
1417
+ }
1418
+ setRightHandlePosition(destPos) {
1419
+ let rightHandleType = this.rightHandle.type;
1420
+ switch (rightHandleType) {
1421
+ case "ALIGNED":
1448
1422
  if (this.leftHandle.type == "VECTOR") {
1449
- let direciton = PointCal.unitVectorFromA2B(this.leftHandle.position, this.position);
1450
- let mag = PointCal.distanceBetweenPoints(this.position, this.rightHandle.position);
1451
- this.rightHandle.position = PointCal.addVector(this.position, PointCal.multiplyVectorByScalar(direciton, mag));
1452
- }
1453
- }
1454
- setRightHandleTypeFree() {
1455
- this.rightHandle.type = "FREE";
1456
- }
1457
- setLeftHandlePosition(destPos) {
1458
- let leftHandleType = this.leftHandle.type;
1459
- switch (leftHandleType) {
1460
- case "ALIGNED":
1461
- if (this.rightHandle.type == "VECTOR") {
1462
- let diff = PointCal.subVector(destPos, this.position);
1463
- let rightHandleDiff = PointCal.unitVectorFromA2B(this.rightHandle.position, this.position);
1464
- let resMag = PointCal.dotProduct(diff, rightHandleDiff);
1465
- let res = PointCal.multiplyVectorByScalar(rightHandleDiff, resMag);
1466
- this.leftHandle.position = PointCal.addVector(this.position, res);
1467
- }
1468
- else if (this.rightHandle.type == "ALIGNED") {
1469
- this.leftHandle.position = destPos;
1470
- let mag = PointCal.distanceBetweenPoints(this.rightHandle.position, this.position);
1471
- let direction = PointCal.unitVectorFromA2B(this.leftHandle.position, this.position);
1472
- let res = PointCal.multiplyVectorByScalar(direction, mag);
1473
- this.rightHandle.position = PointCal.addVector(res, this.position);
1474
- }
1475
- else {
1476
- this.leftHandle.position = destPos;
1477
- }
1478
- break;
1479
- case "FREE":
1480
- this.leftHandle.position = destPos;
1481
- break;
1482
- case "VECTOR":
1483
- break;
1484
- default:
1485
- throw new Error(`Unknown left handle type for control point`);
1486
- }
1487
- }
1488
- setRightHandlePosition(destPos) {
1489
- let rightHandleType = this.rightHandle.type;
1490
- switch (rightHandleType) {
1491
- case "ALIGNED":
1492
- if (this.leftHandle.type == "VECTOR") {
1493
- let diff = PointCal.subVector(destPos, this.position);
1494
- let leftHandleDiff = PointCal.unitVectorFromA2B(this.leftHandle.position, this.position);
1495
- let resMag = PointCal.dotProduct(diff, leftHandleDiff);
1496
- let res = PointCal.multiplyVectorByScalar(leftHandleDiff, resMag);
1497
- this.rightHandle.position = PointCal.addVector(this.position, res);
1498
- }
1499
- else if (this.rightHandle.type == "ALIGNED") {
1500
- this.rightHandle.position = destPos;
1501
- let mag = PointCal.distanceBetweenPoints(this.leftHandle.position, this.position);
1502
- let direction = PointCal.unitVectorFromA2B(this.rightHandle.position, this.position);
1503
- let res = PointCal.multiplyVectorByScalar(direction, mag);
1504
- this.leftHandle.position = PointCal.addVector(res, this.position);
1505
- }
1506
- else {
1507
- this.rightHandle.position = destPos;
1508
- }
1509
- break;
1510
- case "FREE":
1511
- this.rightHandle.position = destPos;
1512
- break;
1513
- case "VECTOR":
1514
- break;
1515
- default:
1516
- throw new Error(`Unknown left handle type for control point`);
1517
- }
1518
- }
1519
- getLeftHandle() {
1520
- return this.leftHandle;
1521
- }
1522
- getRightHandle() {
1523
- return this.rightHandle;
1524
- }
1423
+ let diff = PointCal3.subVector(destPos, this.position);
1424
+ let leftHandleDiff = PointCal3.unitVectorFromA2B(this.leftHandle.position, this.position);
1425
+ let resMag = PointCal3.dotProduct(diff, leftHandleDiff);
1426
+ let res = PointCal3.multiplyVectorByScalar(leftHandleDiff, resMag);
1427
+ this.rightHandle.position = PointCal3.addVector(this.position, res);
1428
+ } else if (this.rightHandle.type == "ALIGNED") {
1429
+ this.rightHandle.position = destPos;
1430
+ let mag = PointCal3.distanceBetweenPoints(this.leftHandle.position, this.position);
1431
+ let direction = PointCal3.unitVectorFromA2B(this.rightHandle.position, this.position);
1432
+ let res = PointCal3.multiplyVectorByScalar(direction, mag);
1433
+ this.leftHandle.position = PointCal3.addVector(res, this.position);
1434
+ } else {
1435
+ this.rightHandle.position = destPos;
1436
+ }
1437
+ break;
1438
+ case "FREE":
1439
+ this.rightHandle.position = destPos;
1440
+ break;
1441
+ case "VECTOR":
1442
+ break;
1443
+ default:
1444
+ throw new Error(`Unknown left handle type for control point`);
1445
+ }
1446
+ }
1447
+ getLeftHandle() {
1448
+ return this.leftHandle;
1449
+ }
1450
+ getRightHandle() {
1451
+ return this.rightHandle;
1452
+ }
1525
1453
  }
1454
+
1526
1455
  class CompositeBCurve {
1527
- constructor(controlPoints = []) {
1528
- this.controlPoints = controlPoints;
1529
- }
1530
- getControlPoints() {
1531
- return this.controlPoints;
1532
- }
1533
- appendControlPoint(position) {
1534
- let leftHandlePosition = PointCal.addVector(position, { x: -100, y: 0 });
1535
- let rightHandlePosition = PointCal.addVector(position, { x: 100, y: 0 });
1536
- let leftHandlePoint = {
1537
- position: leftHandlePosition,
1538
- type: "FREE"
1539
- };
1540
- let rightHandlePoint = {
1541
- position: rightHandlePosition,
1542
- type: "FREE"
1543
- };
1544
- let newControlPoint = new ControlPoint(position, leftHandlePoint, rightHandlePoint);
1545
- this.controlPoints.push(newControlPoint);
1546
- }
1547
- setLeftHandlePositionOfControlPoint(controlPointIndex, destPos) {
1548
- if (controlPointIndex >= this.controlPoints.length || controlPointIndex < 0) {
1549
- return;
1550
- }
1551
- this.controlPoints[controlPointIndex].setLeftHandlePosition(destPos);
1552
- }
1553
- setRightHandlePositionOfControlPoint(controlPointIndex, destPos) {
1554
- if (controlPointIndex >= this.controlPoints.length || controlPointIndex < 0) {
1555
- return;
1556
- }
1557
- this.controlPoints[controlPointIndex].setRightHandlePosition(destPos);
1558
- }
1559
- setPositionOfControlPoint(controlPointIndex, destPos) {
1560
- if (controlPointIndex >= this.controlPoints.length || controlPointIndex < 0) {
1561
- return;
1562
- }
1563
- let prevControlPoint = undefined;
1564
- let nextControlPoint = undefined;
1565
- if (controlPointIndex + 1 < this.controlPoints.length) {
1566
- nextControlPoint = this.controlPoints[controlPointIndex + 1];
1567
- }
1568
- if (controlPointIndex - 1 >= 0) {
1569
- prevControlPoint = this.controlPoints[controlPointIndex - 1];
1570
- }
1571
- this.controlPoints[controlPointIndex].setPosition(destPos, prevControlPoint, nextControlPoint);
1572
- }
1456
+ controlPoints;
1457
+ constructor(controlPoints = []) {
1458
+ this.controlPoints = controlPoints;
1459
+ }
1460
+ getControlPoints() {
1461
+ return this.controlPoints;
1462
+ }
1463
+ appendControlPoint(position) {
1464
+ let leftHandlePosition = PointCal3.addVector(position, { x: -100, y: 0 });
1465
+ let rightHandlePosition = PointCal3.addVector(position, { x: 100, y: 0 });
1466
+ let leftHandlePoint = {
1467
+ position: leftHandlePosition,
1468
+ type: "FREE"
1469
+ };
1470
+ let rightHandlePoint = {
1471
+ position: rightHandlePosition,
1472
+ type: "FREE"
1473
+ };
1474
+ let newControlPoint = new ControlPoint(position, leftHandlePoint, rightHandlePoint);
1475
+ this.controlPoints.push(newControlPoint);
1476
+ }
1477
+ setLeftHandlePositionOfControlPoint(controlPointIndex, destPos) {
1478
+ if (controlPointIndex >= this.controlPoints.length || controlPointIndex < 0) {
1479
+ return;
1480
+ }
1481
+ this.controlPoints[controlPointIndex].setLeftHandlePosition(destPos);
1482
+ }
1483
+ setRightHandlePositionOfControlPoint(controlPointIndex, destPos) {
1484
+ if (controlPointIndex >= this.controlPoints.length || controlPointIndex < 0) {
1485
+ return;
1486
+ }
1487
+ this.controlPoints[controlPointIndex].setRightHandlePosition(destPos);
1488
+ }
1489
+ setPositionOfControlPoint(controlPointIndex, destPos) {
1490
+ if (controlPointIndex >= this.controlPoints.length || controlPointIndex < 0) {
1491
+ return;
1492
+ }
1493
+ let prevControlPoint = undefined;
1494
+ let nextControlPoint = undefined;
1495
+ if (controlPointIndex + 1 < this.controlPoints.length) {
1496
+ nextControlPoint = this.controlPoints[controlPointIndex + 1];
1497
+ }
1498
+ if (controlPointIndex - 1 >= 0) {
1499
+ prevControlPoint = this.controlPoints[controlPointIndex - 1];
1500
+ }
1501
+ this.controlPoints[controlPointIndex].setPosition(destPos, prevControlPoint, nextControlPoint);
1502
+ }
1573
1503
  }
1574
-
1504
+ // src/path.ts
1575
1505
  class Path {
1576
- constructor(lines) {
1577
- this.lines = lines;
1578
- }
1579
- append(line) {
1580
- this.lines.push(line);
1581
- }
1582
- clear() {
1583
- this.lines = [];
1584
- }
1585
- prepend(line) {
1586
- this.lines.unshift(line);
1587
- }
1588
- getLines() {
1589
- return this.lines;
1590
- }
1591
- getLength() {
1592
- let res = 0;
1593
- this.lines.forEach((line) => {
1594
- res += line.length();
1595
- });
1596
- return res;
1597
- }
1598
- // advanceByDistancAtPercentage(percentage: number, distance: number): {destination: Point}
1599
- getPercentages() {
1600
- const length = this.getLength();
1601
- let currentCurvePercentage = 0;
1602
- const res = [];
1603
- this.lines.forEach((line) => {
1604
- const lineLength = line.length();
1605
- const linePercentage = lineLength / length;
1606
- let start = currentCurvePercentage;
1607
- currentCurvePercentage += linePercentage;
1608
- let end = currentCurvePercentage;
1609
- res.push({ start, end });
1610
- });
1611
- res[res.length - 1].end = 1;
1612
- return res;
1613
- }
1614
- getPointByPercentage(percentage) {
1615
- if (percentage < 0 || percentage > 1) {
1616
- throw new Error("Percentage must be between 0 and 1");
1617
- }
1618
- const percentages = this.getPercentages();
1619
- let left = 0;
1620
- let right = percentages.length - 1;
1621
- while (left <= right) {
1622
- const mid = Math.floor((left + right) / 2);
1623
- if (percentage < percentages[mid].end) {
1624
- right = mid - 1;
1625
- }
1626
- else if (percentage > percentages[mid].end) {
1627
- left = mid + 1;
1628
- }
1629
- else {
1630
- left = mid;
1631
- break;
1632
- }
1633
- }
1634
- const line = this.lines[left];
1635
- const linePercentage = percentages[left];
1636
- const ratio = (percentage - linePercentage.start) / (linePercentage.end - linePercentage.start);
1637
- return line.lerp(ratio);
1638
- }
1506
+ lines;
1507
+ constructor(lines) {
1508
+ this.lines = lines;
1509
+ }
1510
+ append(line) {
1511
+ this.lines.push(line);
1512
+ }
1513
+ clear() {
1514
+ this.lines = [];
1515
+ }
1516
+ prepend(line) {
1517
+ this.lines.unshift(line);
1518
+ }
1519
+ getLines() {
1520
+ return this.lines;
1521
+ }
1522
+ getLength() {
1523
+ let res = 0;
1524
+ this.lines.forEach((line) => {
1525
+ res += line.length();
1526
+ });
1527
+ return res;
1528
+ }
1529
+ getPercentages() {
1530
+ const length = this.getLength();
1531
+ let currentCurvePercentage = 0;
1532
+ const res = [];
1533
+ this.lines.forEach((line) => {
1534
+ const lineLength = line.length();
1535
+ const linePercentage = lineLength / length;
1536
+ let start = currentCurvePercentage;
1537
+ currentCurvePercentage += linePercentage;
1538
+ let end = currentCurvePercentage;
1539
+ res.push({ start, end });
1540
+ });
1541
+ res[res.length - 1].end = 1;
1542
+ return res;
1543
+ }
1544
+ getPointByPercentage(percentage) {
1545
+ if (percentage < 0 || percentage > 1) {
1546
+ throw new Error("Percentage must be between 0 and 1");
1547
+ }
1548
+ const percentages = this.getPercentages();
1549
+ let left = 0;
1550
+ let right = percentages.length - 1;
1551
+ while (left <= right) {
1552
+ const mid = Math.floor((left + right) / 2);
1553
+ if (percentage < percentages[mid].end) {
1554
+ right = mid - 1;
1555
+ } else if (percentage > percentages[mid].end) {
1556
+ left = mid + 1;
1557
+ } else {
1558
+ left = mid;
1559
+ break;
1560
+ }
1561
+ }
1562
+ const line = this.lines[left];
1563
+ const linePercentage = percentages[left];
1564
+ const ratio = (percentage - linePercentage.start) / (linePercentage.end - linePercentage.start);
1565
+ return line.lerp(ratio);
1566
+ }
1639
1567
  }
1568
+ export {
1569
+ solveCubic,
1570
+ reduce,
1571
+ projectPointOntoLine,
1572
+ offset2,
1573
+ offset,
1574
+ getLineIntersection,
1575
+ getIntersectionsBetweenCurves,
1576
+ getCubicRoots,
1577
+ cuberoot2,
1578
+ cuberoot,
1579
+ computeWithControlPoints,
1580
+ approximately,
1581
+ accept,
1582
+ TValOutofBoundError,
1583
+ Path,
1584
+ Line,
1585
+ ControlPoint,
1586
+ CompositeBCurve,
1587
+ BCurve,
1588
+ AABBIntersects
1589
+ };
1640
1590
 
1641
- export { AABBIntersects, BCurve, CompositeBCurve, ControlPoint, Line, Path, TValOutofBoundError, accept, approximately, computeWithControlPoints, cuberoot, cuberoot2, getCubicRoots, getIntersectionsBetweenCurves, getLineIntersection, offset, offset2, projectPointOntoLine, reduce, solveCubic };
1642
- //# sourceMappingURL=index.js.map
1591
+ //# debugId=EABF32F5EF0F7EC864756E2164756E21