three-text 0.4.10 → 0.4.11
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/dist/index.cjs +515 -446
- package/dist/index.d.ts +3 -0
- package/dist/index.js +515 -446
- package/dist/index.min.cjs +563 -545
- package/dist/index.min.js +550 -532
- package/dist/index.umd.js +515 -446
- package/dist/index.umd.min.js +534 -516
- package/dist/three/react.d.ts +3 -0
- package/dist/types/core/geometry/PathOptimizer.d.ts +0 -2
- package/dist/types/core/geometry/Polygonizer.d.ts +10 -5
- package/dist/types/core/types.d.ts +3 -0
- package/package.json +1 -1
- package/dist/types/utils/MinHeap.d.ts +0 -14
package/dist/index.cjs
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/*!
|
|
2
|
-
* three-text v0.4.
|
|
2
|
+
* three-text v0.4.11
|
|
3
3
|
* Copyright (C) 2025 Countertype LLC
|
|
4
4
|
*
|
|
5
5
|
* This program is free software: you can redistribute it and/or modify
|
|
@@ -3130,117 +3130,33 @@ class BoundaryClusterer {
|
|
|
3130
3130
|
}
|
|
3131
3131
|
}
|
|
3132
3132
|
|
|
3133
|
-
class MinHeap {
|
|
3134
|
-
constructor(compare) {
|
|
3135
|
-
this.heap = [];
|
|
3136
|
-
this.itemIndex = new Map();
|
|
3137
|
-
this.compare = compare;
|
|
3138
|
-
}
|
|
3139
|
-
insert(value) {
|
|
3140
|
-
const existingIndex = this.itemIndex.get(value);
|
|
3141
|
-
if (existingIndex !== undefined) {
|
|
3142
|
-
// Already exists - update in place
|
|
3143
|
-
this.siftUp(existingIndex);
|
|
3144
|
-
this.siftDown(existingIndex);
|
|
3145
|
-
return;
|
|
3146
|
-
}
|
|
3147
|
-
const index = this.heap.length;
|
|
3148
|
-
this.heap.push(value);
|
|
3149
|
-
this.itemIndex.set(value, index);
|
|
3150
|
-
this.siftUp(index);
|
|
3151
|
-
}
|
|
3152
|
-
extractMin() {
|
|
3153
|
-
const heapLength = this.heap.length;
|
|
3154
|
-
if (!heapLength)
|
|
3155
|
-
return undefined;
|
|
3156
|
-
if (heapLength === 1) {
|
|
3157
|
-
const min = this.heap.pop();
|
|
3158
|
-
this.itemIndex.clear();
|
|
3159
|
-
return min;
|
|
3160
|
-
}
|
|
3161
|
-
const min = this.heap[0];
|
|
3162
|
-
const last = this.heap.pop();
|
|
3163
|
-
this.heap[0] = last;
|
|
3164
|
-
this.itemIndex.delete(min);
|
|
3165
|
-
this.itemIndex.set(last, 0);
|
|
3166
|
-
this.siftDown(0);
|
|
3167
|
-
return min;
|
|
3168
|
-
}
|
|
3169
|
-
update(value) {
|
|
3170
|
-
const index = this.itemIndex.get(value);
|
|
3171
|
-
if (index === undefined) {
|
|
3172
|
-
this.insert(value);
|
|
3173
|
-
return;
|
|
3174
|
-
}
|
|
3175
|
-
// Percolate in both directions - one will terminate immediately
|
|
3176
|
-
this.siftUp(index);
|
|
3177
|
-
this.siftDown(index);
|
|
3178
|
-
}
|
|
3179
|
-
isEmpty() {
|
|
3180
|
-
return !this.heap.length;
|
|
3181
|
-
}
|
|
3182
|
-
swap(i, j) {
|
|
3183
|
-
const itemI = this.heap[i];
|
|
3184
|
-
const itemJ = this.heap[j];
|
|
3185
|
-
this.heap[i] = itemJ;
|
|
3186
|
-
this.heap[j] = itemI;
|
|
3187
|
-
this.itemIndex.set(itemI, j);
|
|
3188
|
-
this.itemIndex.set(itemJ, i);
|
|
3189
|
-
}
|
|
3190
|
-
siftUp(i) {
|
|
3191
|
-
const item = this.heap[i];
|
|
3192
|
-
while (i > 0) {
|
|
3193
|
-
const parentIndex = (i - 1) >> 1; // Bit shift for fast division by 2
|
|
3194
|
-
const parent = this.heap[parentIndex];
|
|
3195
|
-
// Early exit if heap property satisfied
|
|
3196
|
-
if (this.compare(item, parent) >= 0)
|
|
3197
|
-
break;
|
|
3198
|
-
// Move parent down
|
|
3199
|
-
this.heap[i] = parent;
|
|
3200
|
-
this.itemIndex.set(parent, i);
|
|
3201
|
-
i = parentIndex;
|
|
3202
|
-
}
|
|
3203
|
-
this.heap[i] = item;
|
|
3204
|
-
this.itemIndex.set(item, i);
|
|
3205
|
-
}
|
|
3206
|
-
siftDown(i) {
|
|
3207
|
-
const item = this.heap[i];
|
|
3208
|
-
const heapLength = this.heap.length;
|
|
3209
|
-
const halfLength = heapLength >> 1; // Only nodes in first half can have children
|
|
3210
|
-
while (i < halfLength) {
|
|
3211
|
-
const leftIndex = (i << 1) + 1; // Bit shift for fast multiplication by 2
|
|
3212
|
-
const rightIndex = leftIndex + 1;
|
|
3213
|
-
let smallestIndex = i;
|
|
3214
|
-
let smallest = item;
|
|
3215
|
-
const left = this.heap[leftIndex];
|
|
3216
|
-
if (this.compare(left, smallest) < 0) {
|
|
3217
|
-
smallestIndex = leftIndex;
|
|
3218
|
-
smallest = left;
|
|
3219
|
-
}
|
|
3220
|
-
if (rightIndex < heapLength) {
|
|
3221
|
-
const right = this.heap[rightIndex];
|
|
3222
|
-
if (this.compare(right, smallest) < 0) {
|
|
3223
|
-
smallestIndex = rightIndex;
|
|
3224
|
-
smallest = right;
|
|
3225
|
-
}
|
|
3226
|
-
}
|
|
3227
|
-
// Early exit if heap property satisfied
|
|
3228
|
-
if (smallestIndex === i)
|
|
3229
|
-
break;
|
|
3230
|
-
// Move smallest child up
|
|
3231
|
-
this.heap[i] = smallest;
|
|
3232
|
-
this.itemIndex.set(smallest, i);
|
|
3233
|
-
i = smallestIndex;
|
|
3234
|
-
}
|
|
3235
|
-
this.heap[i] = item;
|
|
3236
|
-
this.itemIndex.set(item, i);
|
|
3237
|
-
}
|
|
3238
|
-
}
|
|
3239
|
-
|
|
3240
3133
|
const DEFAULT_OPTIMIZATION_CONFIG = {
|
|
3241
3134
|
enabled: true,
|
|
3242
|
-
areaThreshold: 1.0
|
|
3135
|
+
areaThreshold: 1.0
|
|
3243
3136
|
};
|
|
3137
|
+
// Scratch buffers reused across calls. Grown on demand, never shrunk
|
|
3138
|
+
let _cap = 1024;
|
|
3139
|
+
let _px = new Float64Array(_cap);
|
|
3140
|
+
let _py = new Float64Array(_cap);
|
|
3141
|
+
let _area = new Float64Array(_cap);
|
|
3142
|
+
let _prev = new Int32Array(_cap);
|
|
3143
|
+
let _next = new Int32Array(_cap);
|
|
3144
|
+
let _heap = new Int32Array(_cap);
|
|
3145
|
+
let _hpos = new Int32Array(_cap);
|
|
3146
|
+
function ensureCap(n) {
|
|
3147
|
+
if (n <= _cap)
|
|
3148
|
+
return;
|
|
3149
|
+
_cap = 1;
|
|
3150
|
+
while (_cap < n)
|
|
3151
|
+
_cap <<= 1;
|
|
3152
|
+
_px = new Float64Array(_cap);
|
|
3153
|
+
_py = new Float64Array(_cap);
|
|
3154
|
+
_area = new Float64Array(_cap);
|
|
3155
|
+
_prev = new Int32Array(_cap);
|
|
3156
|
+
_next = new Int32Array(_cap);
|
|
3157
|
+
_heap = new Int32Array(_cap);
|
|
3158
|
+
_hpos = new Int32Array(_cap);
|
|
3159
|
+
}
|
|
3244
3160
|
class PathOptimizer {
|
|
3245
3161
|
constructor(config) {
|
|
3246
3162
|
this.stats = {
|
|
@@ -3253,85 +3169,26 @@ class PathOptimizer {
|
|
|
3253
3169
|
this.config = config;
|
|
3254
3170
|
}
|
|
3255
3171
|
optimizePath(path) {
|
|
3256
|
-
if (path.points.length <= 2) {
|
|
3257
|
-
return path;
|
|
3258
|
-
}
|
|
3259
|
-
if (!this.config.enabled) {
|
|
3260
|
-
return path;
|
|
3261
|
-
}
|
|
3262
|
-
this.stats.originalPointCount += path.points.length;
|
|
3263
|
-
// Most paths are already immutable after collection; avoid copying large point arrays
|
|
3264
|
-
// The optimizers below never mutate the input `points` array
|
|
3265
3172
|
const points = path.points;
|
|
3266
|
-
|
|
3173
|
+
const n = points.length;
|
|
3174
|
+
if (n < 5 || !this.config.enabled) {
|
|
3175
|
+
if (this.config.enabled)
|
|
3176
|
+
this.stats.originalPointCount += n;
|
|
3267
3177
|
return path;
|
|
3268
3178
|
}
|
|
3269
|
-
|
|
3270
|
-
|
|
3271
|
-
|
|
3272
|
-
if (optimized.length < 3) {
|
|
3179
|
+
this.stats.originalPointCount += n;
|
|
3180
|
+
const removed = simplifyVW(points, n, this.config.areaThreshold);
|
|
3181
|
+
if (removed === 0)
|
|
3273
3182
|
return path;
|
|
3183
|
+
this.stats.pointsRemovedByVisvalingam += removed;
|
|
3184
|
+
const result = new Array(n - removed);
|
|
3185
|
+
let idx = 0;
|
|
3186
|
+
let out = 0;
|
|
3187
|
+
while (idx >= 0) {
|
|
3188
|
+
result[out++] = points[idx];
|
|
3189
|
+
idx = _next[idx];
|
|
3274
3190
|
}
|
|
3275
|
-
return {
|
|
3276
|
-
...path,
|
|
3277
|
-
points: optimized
|
|
3278
|
-
};
|
|
3279
|
-
}
|
|
3280
|
-
// Visvalingam-Whyatt algorithm
|
|
3281
|
-
simplifyPathVW(points, areaThreshold) {
|
|
3282
|
-
if (points.length <= 3)
|
|
3283
|
-
return points;
|
|
3284
|
-
const originalLength = points.length;
|
|
3285
|
-
const minPoints = 3;
|
|
3286
|
-
const pointList = points.map((p, i) => ({
|
|
3287
|
-
index: i,
|
|
3288
|
-
area: Infinity,
|
|
3289
|
-
prev: null,
|
|
3290
|
-
next: null
|
|
3291
|
-
}));
|
|
3292
|
-
for (let i = 0; i < pointList.length; i++) {
|
|
3293
|
-
pointList[i].prev = pointList[i - 1] || null;
|
|
3294
|
-
pointList[i].next = pointList[i + 1] || null;
|
|
3295
|
-
}
|
|
3296
|
-
const heap = new MinHeap((a, b) => a.area - b.area);
|
|
3297
|
-
for (let i = 1; i < pointList.length - 1; i++) {
|
|
3298
|
-
const p = pointList[i];
|
|
3299
|
-
p.area = this.calculateTriangleArea(points[p.prev.index], points[p.index], points[p.next.index]);
|
|
3300
|
-
heap.insert(p);
|
|
3301
|
-
}
|
|
3302
|
-
let remainingPoints = originalLength;
|
|
3303
|
-
while (!heap.isEmpty() && remainingPoints > minPoints) {
|
|
3304
|
-
const p = heap.extractMin();
|
|
3305
|
-
if (!p || p.area > areaThreshold) {
|
|
3306
|
-
break;
|
|
3307
|
-
}
|
|
3308
|
-
if (p.prev)
|
|
3309
|
-
p.prev.next = p.next;
|
|
3310
|
-
if (p.next)
|
|
3311
|
-
p.next.prev = p.prev;
|
|
3312
|
-
remainingPoints--;
|
|
3313
|
-
if (p.prev && p.prev.prev) {
|
|
3314
|
-
p.prev.area = this.calculateTriangleArea(points[p.prev.prev.index], points[p.prev.index], points[p.next.index]);
|
|
3315
|
-
heap.update(p.prev);
|
|
3316
|
-
}
|
|
3317
|
-
if (p.next && p.next.next) {
|
|
3318
|
-
p.next.area = this.calculateTriangleArea(points[p.prev.index], points[p.next.index], points[p.next.next.index]);
|
|
3319
|
-
heap.update(p.next);
|
|
3320
|
-
}
|
|
3321
|
-
}
|
|
3322
|
-
const simplifiedPoints = [];
|
|
3323
|
-
let current = pointList[0];
|
|
3324
|
-
while (current) {
|
|
3325
|
-
simplifiedPoints.push(points[current.index]);
|
|
3326
|
-
current = current.next;
|
|
3327
|
-
}
|
|
3328
|
-
const pointsRemoved = originalLength - simplifiedPoints.length;
|
|
3329
|
-
this.stats.pointsRemovedByVisvalingam += pointsRemoved;
|
|
3330
|
-
return simplifiedPoints;
|
|
3331
|
-
}
|
|
3332
|
-
// Shoelace formula
|
|
3333
|
-
calculateTriangleArea(p1, p2, p3) {
|
|
3334
|
-
return Math.abs((p1.x * (p2.y - p3.y) + p2.x * (p3.y - p1.y) + p3.x * (p1.y - p2.y)) / 2);
|
|
3191
|
+
return { ...path, points: result };
|
|
3335
3192
|
}
|
|
3336
3193
|
getStats() {
|
|
3337
3194
|
return { ...this.stats };
|
|
@@ -3343,6 +3200,158 @@ class PathOptimizer {
|
|
|
3343
3200
|
};
|
|
3344
3201
|
}
|
|
3345
3202
|
}
|
|
3203
|
+
// Visvalingam-Whyatt simplification. Iteratively removes the point
|
|
3204
|
+
// whose removal causes the smallest change in contour area (measured
|
|
3205
|
+
// as the triangle formed with its two neighbors) until all remaining
|
|
3206
|
+
// triangles exceed areaThreshold
|
|
3207
|
+
//
|
|
3208
|
+
// Uses parallel typed arrays for the linked list and a binary min-heap
|
|
3209
|
+
// with an index-based position map for O(log n) extract and update.
|
|
3210
|
+
// Coordinates are flattened into Float64Array to keep area computation
|
|
3211
|
+
// on contiguous memory. Areas are stored as 2x actual (skipping the
|
|
3212
|
+
// shoelace /2) and the threshold is doubled to match
|
|
3213
|
+
//
|
|
3214
|
+
// Returns the number of points removed. The caller reads _next[] to
|
|
3215
|
+
// walk the surviving linked list
|
|
3216
|
+
function simplifyVW(points, n, areaThreshold) {
|
|
3217
|
+
if (n <= 3)
|
|
3218
|
+
return 0;
|
|
3219
|
+
ensureCap(n);
|
|
3220
|
+
const px = _px;
|
|
3221
|
+
const py = _py;
|
|
3222
|
+
const area = _area;
|
|
3223
|
+
const prev = _prev;
|
|
3224
|
+
const next = _next;
|
|
3225
|
+
const heap = _heap;
|
|
3226
|
+
const hpos = _hpos;
|
|
3227
|
+
// Flatten coordinates and initialize doubly-linked list
|
|
3228
|
+
for (let i = 0; i < n; i++) {
|
|
3229
|
+
const p = points[i];
|
|
3230
|
+
px[i] = p.x;
|
|
3231
|
+
py[i] = p.y;
|
|
3232
|
+
prev[i] = i - 1;
|
|
3233
|
+
next[i] = i + 1;
|
|
3234
|
+
}
|
|
3235
|
+
next[n - 1] = -1;
|
|
3236
|
+
// Compute triangle areas for interior points and seed the heap
|
|
3237
|
+
const heapLen0 = n - 2;
|
|
3238
|
+
area[0] = Infinity;
|
|
3239
|
+
area[n - 1] = Infinity;
|
|
3240
|
+
for (let i = 1; i < n - 1; i++) {
|
|
3241
|
+
area[i] = area2x(px, py, i - 1, i, i + 1);
|
|
3242
|
+
heap[i - 1] = i;
|
|
3243
|
+
hpos[i] = i - 1;
|
|
3244
|
+
}
|
|
3245
|
+
hpos[0] = -1;
|
|
3246
|
+
hpos[n - 1] = -1;
|
|
3247
|
+
// Bottom-up heapify, O(n)
|
|
3248
|
+
let heapLen = heapLen0;
|
|
3249
|
+
for (let i = (heapLen >> 1) - 1; i >= 0; i--) {
|
|
3250
|
+
siftDown(heap, hpos, area, i, heapLen);
|
|
3251
|
+
}
|
|
3252
|
+
const threshold2x = areaThreshold * 2;
|
|
3253
|
+
const maxRemovals = n - 3;
|
|
3254
|
+
let removed = 0;
|
|
3255
|
+
while (heapLen > 0 && removed < maxRemovals) {
|
|
3256
|
+
const minIdx = heap[0];
|
|
3257
|
+
if (area[minIdx] > threshold2x)
|
|
3258
|
+
break;
|
|
3259
|
+
// Extract min
|
|
3260
|
+
heapLen--;
|
|
3261
|
+
if (heapLen > 0) {
|
|
3262
|
+
const last = heap[heapLen];
|
|
3263
|
+
heap[0] = last;
|
|
3264
|
+
hpos[last] = 0;
|
|
3265
|
+
siftDown(heap, hpos, area, 0, heapLen);
|
|
3266
|
+
}
|
|
3267
|
+
hpos[minIdx] = -1;
|
|
3268
|
+
// Unlink
|
|
3269
|
+
const pi = prev[minIdx];
|
|
3270
|
+
const ni = next[minIdx];
|
|
3271
|
+
if (pi >= 0)
|
|
3272
|
+
next[pi] = ni;
|
|
3273
|
+
if (ni >= 0)
|
|
3274
|
+
prev[ni] = pi;
|
|
3275
|
+
removed++;
|
|
3276
|
+
// Recompute prev neighbor's area and update heap position
|
|
3277
|
+
if (pi >= 0 && prev[pi] >= 0) {
|
|
3278
|
+
const oldArea = area[pi];
|
|
3279
|
+
const newArea = area2x(px, py, prev[pi], pi, ni);
|
|
3280
|
+
area[pi] = newArea;
|
|
3281
|
+
const pos = hpos[pi];
|
|
3282
|
+
if (pos >= 0) {
|
|
3283
|
+
if (newArea < oldArea)
|
|
3284
|
+
siftUp(heap, hpos, area, pos);
|
|
3285
|
+
else if (newArea > oldArea)
|
|
3286
|
+
siftDown(heap, hpos, area, pos, heapLen);
|
|
3287
|
+
}
|
|
3288
|
+
}
|
|
3289
|
+
// Same for next neighbor
|
|
3290
|
+
if (ni >= 0 && next[ni] >= 0) {
|
|
3291
|
+
const oldArea = area[ni];
|
|
3292
|
+
const newArea = area2x(px, py, pi, ni, next[ni]);
|
|
3293
|
+
area[ni] = newArea;
|
|
3294
|
+
const pos = hpos[ni];
|
|
3295
|
+
if (pos >= 0) {
|
|
3296
|
+
if (newArea < oldArea)
|
|
3297
|
+
siftUp(heap, hpos, area, pos);
|
|
3298
|
+
else if (newArea > oldArea)
|
|
3299
|
+
siftDown(heap, hpos, area, pos, heapLen);
|
|
3300
|
+
}
|
|
3301
|
+
}
|
|
3302
|
+
}
|
|
3303
|
+
return removed;
|
|
3304
|
+
}
|
|
3305
|
+
function siftUp(heap, hpos, area, i) {
|
|
3306
|
+
const idx = heap[i];
|
|
3307
|
+
const val = area[idx];
|
|
3308
|
+
while (i > 0) {
|
|
3309
|
+
const parent = (i - 1) >> 1;
|
|
3310
|
+
const pidx = heap[parent];
|
|
3311
|
+
if (area[pidx] <= val)
|
|
3312
|
+
break;
|
|
3313
|
+
heap[i] = pidx;
|
|
3314
|
+
hpos[pidx] = i;
|
|
3315
|
+
i = parent;
|
|
3316
|
+
}
|
|
3317
|
+
heap[i] = idx;
|
|
3318
|
+
hpos[idx] = i;
|
|
3319
|
+
}
|
|
3320
|
+
function siftDown(heap, hpos, area, i, len) {
|
|
3321
|
+
const idx = heap[i];
|
|
3322
|
+
const val = area[idx];
|
|
3323
|
+
const half = len >> 1;
|
|
3324
|
+
while (i < half) {
|
|
3325
|
+
let child = (i << 1) + 1;
|
|
3326
|
+
let childIdx = heap[child];
|
|
3327
|
+
let childVal = area[childIdx];
|
|
3328
|
+
const right = child + 1;
|
|
3329
|
+
if (right < len) {
|
|
3330
|
+
const rIdx = heap[right];
|
|
3331
|
+
const rVal = area[rIdx];
|
|
3332
|
+
if (rVal < childVal) {
|
|
3333
|
+
child = right;
|
|
3334
|
+
childIdx = rIdx;
|
|
3335
|
+
childVal = rVal;
|
|
3336
|
+
}
|
|
3337
|
+
}
|
|
3338
|
+
if (childVal >= val)
|
|
3339
|
+
break;
|
|
3340
|
+
heap[i] = childIdx;
|
|
3341
|
+
hpos[childIdx] = i;
|
|
3342
|
+
i = child;
|
|
3343
|
+
}
|
|
3344
|
+
heap[i] = idx;
|
|
3345
|
+
hpos[idx] = i;
|
|
3346
|
+
}
|
|
3347
|
+
// Doubled triangle area via the shoelace formula. Skipping the /2
|
|
3348
|
+
// means callers compare against a doubled threshold instead
|
|
3349
|
+
function area2x(px, py, i1, i2, i3) {
|
|
3350
|
+
const v = px[i1] * (py[i2] - py[i3]) +
|
|
3351
|
+
px[i2] * (py[i3] - py[i1]) +
|
|
3352
|
+
px[i3] * (py[i1] - py[i2]);
|
|
3353
|
+
return v < 0 ? -v : v;
|
|
3354
|
+
}
|
|
3346
3355
|
|
|
3347
3356
|
/**
|
|
3348
3357
|
* @license
|
|
@@ -3383,23 +3392,314 @@ class PathOptimizer {
|
|
|
3383
3392
|
*/
|
|
3384
3393
|
const DEFAULT_CURVE_FIDELITY = {
|
|
3385
3394
|
distanceTolerance: 0.5,
|
|
3386
|
-
angleTolerance: 0.2
|
|
3395
|
+
angleTolerance: 0.2,
|
|
3396
|
+
cuspLimit: 0,
|
|
3397
|
+
collinearityEpsilon: 1e-6,
|
|
3398
|
+
recursionLimit: 16
|
|
3387
3399
|
};
|
|
3388
|
-
const COLLINEARITY_EPSILON =
|
|
3389
|
-
|
|
3400
|
+
const COLLINEARITY_EPSILON = DEFAULT_CURVE_FIDELITY.collinearityEpsilon;
|
|
3401
|
+
// Module-level state for the recursive subdivision functions,
|
|
3402
|
+
// set from instance config before each polygonize call
|
|
3403
|
+
// Output array, reset before each polygonize call
|
|
3404
|
+
let _out;
|
|
3405
|
+
// Cached tolerance state
|
|
3406
|
+
let _distTolSq = 0;
|
|
3407
|
+
let _colEps = 0;
|
|
3408
|
+
let _maxLvl = 0;
|
|
3409
|
+
let _angleTol = 0;
|
|
3410
|
+
let _tanAngSq = 0;
|
|
3411
|
+
let _cuspLim = 0;
|
|
3412
|
+
let _tanCuspSq = 0;
|
|
3413
|
+
// Collinearity checks in the recursive core prevent near-duplicate points
|
|
3414
|
+
function emit(x, y) {
|
|
3415
|
+
_out.push(new Vec2(x, y));
|
|
3416
|
+
}
|
|
3417
|
+
// Quadratic recursive subdivision (AGG curve3_div)
|
|
3418
|
+
function quadRec(x1, y1, x2, y2, x3, y3, level) {
|
|
3419
|
+
if (level > _maxLvl)
|
|
3420
|
+
return;
|
|
3421
|
+
const x12 = (x1 + x2) * 0.5;
|
|
3422
|
+
const y12 = (y1 + y2) * 0.5;
|
|
3423
|
+
const x23 = (x2 + x3) * 0.5;
|
|
3424
|
+
const y23 = (y2 + y3) * 0.5;
|
|
3425
|
+
const x123 = (x12 + x23) * 0.5;
|
|
3426
|
+
const y123 = (y12 + y23) * 0.5;
|
|
3427
|
+
const dx = x3 - x1;
|
|
3428
|
+
const dy = y3 - y1;
|
|
3429
|
+
let d = Math.abs((x2 - x3) * dy - (y2 - y3) * dx);
|
|
3430
|
+
if (d > _colEps) {
|
|
3431
|
+
if (d * d <= _distTolSq * (dx * dx + dy * dy)) {
|
|
3432
|
+
if (_angleTol > 0) {
|
|
3433
|
+
const v1x = x2 - x1;
|
|
3434
|
+
const v1y = y2 - y1;
|
|
3435
|
+
const v2x = x3 - x2;
|
|
3436
|
+
const v2y = y3 - y2;
|
|
3437
|
+
const cross = v1x * v2y - v1y * v2x;
|
|
3438
|
+
const dot = v1x * v2x + v1y * v2y;
|
|
3439
|
+
if (dot > 0 && cross * cross < _tanAngSq * dot * dot) {
|
|
3440
|
+
emit(x123, y123);
|
|
3441
|
+
return;
|
|
3442
|
+
}
|
|
3443
|
+
}
|
|
3444
|
+
else {
|
|
3445
|
+
emit(x123, y123);
|
|
3446
|
+
return;
|
|
3447
|
+
}
|
|
3448
|
+
}
|
|
3449
|
+
}
|
|
3450
|
+
else {
|
|
3451
|
+
let da = dx * dx + dy * dy;
|
|
3452
|
+
if (da === 0) {
|
|
3453
|
+
d = (x2 - x1) * (x2 - x1) + (y2 - y1) * (y2 - y1);
|
|
3454
|
+
}
|
|
3455
|
+
else {
|
|
3456
|
+
d = ((x2 - x1) * dx + (y2 - y1) * dy) / da;
|
|
3457
|
+
if (d > 0 && d < 1)
|
|
3458
|
+
return;
|
|
3459
|
+
if (d <= 0)
|
|
3460
|
+
d = (x2 - x1) * (x2 - x1) + (y2 - y1) * (y2 - y1);
|
|
3461
|
+
else if (d >= 1)
|
|
3462
|
+
d = (x2 - x3) * (x2 - x3) + (y2 - y3) * (y2 - y3);
|
|
3463
|
+
else {
|
|
3464
|
+
const px = x1 + d * dx;
|
|
3465
|
+
const py = y1 + d * dy;
|
|
3466
|
+
d = (x2 - px) * (x2 - px) + (y2 - py) * (y2 - py);
|
|
3467
|
+
}
|
|
3468
|
+
}
|
|
3469
|
+
if (d < _distTolSq) {
|
|
3470
|
+
emit(x2, y2);
|
|
3471
|
+
return;
|
|
3472
|
+
}
|
|
3473
|
+
}
|
|
3474
|
+
const nl = level + 1;
|
|
3475
|
+
quadRec(x1, y1, x12, y12, x123, y123, nl);
|
|
3476
|
+
quadRec(x123, y123, x23, y23, x3, y3, nl);
|
|
3477
|
+
}
|
|
3478
|
+
// Cubic recursive subdivision (AGG curve4_div)
|
|
3479
|
+
// cubicBegin handles level 0, which always subdivides
|
|
3480
|
+
function cubicBegin(x1, y1, x2, y2, x3, y3, x4, y4) {
|
|
3481
|
+
const x12 = (x1 + x2) * 0.5;
|
|
3482
|
+
const y12 = (y1 + y2) * 0.5;
|
|
3483
|
+
const x23 = (x2 + x3) * 0.5;
|
|
3484
|
+
const y23 = (y2 + y3) * 0.5;
|
|
3485
|
+
const x34 = (x3 + x4) * 0.5;
|
|
3486
|
+
const y34 = (y3 + y4) * 0.5;
|
|
3487
|
+
const x123 = (x12 + x23) * 0.5;
|
|
3488
|
+
const y123 = (y12 + y23) * 0.5;
|
|
3489
|
+
const x234 = (x23 + x34) * 0.5;
|
|
3490
|
+
const y234 = (y23 + y34) * 0.5;
|
|
3491
|
+
const x1234 = (x123 + x234) * 0.5;
|
|
3492
|
+
const y1234 = (y123 + y234) * 0.5;
|
|
3493
|
+
cubicRec(x1, y1, x12, y12, x123, y123, x1234, y1234, 1);
|
|
3494
|
+
cubicRec(x1234, y1234, x234, y234, x34, y34, x4, y4, 1);
|
|
3495
|
+
}
|
|
3496
|
+
function cubicRec(x1, y1, x2, y2, x3, y3, x4, y4, level) {
|
|
3497
|
+
if (level > _maxLvl)
|
|
3498
|
+
return;
|
|
3499
|
+
const x12 = (x1 + x2) * 0.5;
|
|
3500
|
+
const y12 = (y1 + y2) * 0.5;
|
|
3501
|
+
const x23 = (x2 + x3) * 0.5;
|
|
3502
|
+
const y23 = (y2 + y3) * 0.5;
|
|
3503
|
+
const x34 = (x3 + x4) * 0.5;
|
|
3504
|
+
const y34 = (y3 + y4) * 0.5;
|
|
3505
|
+
const x123 = (x12 + x23) * 0.5;
|
|
3506
|
+
const y123 = (y12 + y23) * 0.5;
|
|
3507
|
+
const x234 = (x23 + x34) * 0.5;
|
|
3508
|
+
const y234 = (y23 + y34) * 0.5;
|
|
3509
|
+
const x1234 = (x123 + x234) * 0.5;
|
|
3510
|
+
const y1234 = (y123 + y234) * 0.5;
|
|
3511
|
+
const dx = x4 - x1;
|
|
3512
|
+
const dy = y4 - y1;
|
|
3513
|
+
let d2 = Math.abs((x2 - x4) * dy - (y2 - y4) * dx);
|
|
3514
|
+
let d3 = Math.abs((x3 - x4) * dy - (y3 - y4) * dx);
|
|
3515
|
+
const sc = (d2 > _colEps ? 2 : 0) + (d3 > _colEps ? 1 : 0);
|
|
3516
|
+
switch (sc) {
|
|
3517
|
+
case 0: {
|
|
3518
|
+
let k = dx * dx + dy * dy;
|
|
3519
|
+
if (k === 0) {
|
|
3520
|
+
d2 = (x2 - x1) * (x2 - x1) + (y2 - y1) * (y2 - y1);
|
|
3521
|
+
d3 = (x3 - x1) * (x3 - x1) + (y3 - y1) * (y3 - y1);
|
|
3522
|
+
}
|
|
3523
|
+
else {
|
|
3524
|
+
k = 1 / k;
|
|
3525
|
+
let t1 = x2 - x1;
|
|
3526
|
+
let t2 = y2 - y1;
|
|
3527
|
+
d2 = k * (t1 * dx + t2 * dy);
|
|
3528
|
+
t1 = x3 - x1;
|
|
3529
|
+
t2 = y3 - y1;
|
|
3530
|
+
d3 = k * (t1 * dx + t2 * dy);
|
|
3531
|
+
if (d2 > 0 && d2 < 1 && d3 > 0 && d3 < 1)
|
|
3532
|
+
return;
|
|
3533
|
+
if (d2 <= 0)
|
|
3534
|
+
d2 = (x2 - x1) * (x2 - x1) + (y2 - y1) * (y2 - y1);
|
|
3535
|
+
else if (d2 >= 1)
|
|
3536
|
+
d2 = (x2 - x4) * (x2 - x4) + (y2 - y4) * (y2 - y4);
|
|
3537
|
+
else {
|
|
3538
|
+
const px = x1 + d2 * dx, py = y1 + d2 * dy;
|
|
3539
|
+
d2 = (x2 - px) * (x2 - px) + (y2 - py) * (y2 - py);
|
|
3540
|
+
}
|
|
3541
|
+
if (d3 <= 0)
|
|
3542
|
+
d3 = (x3 - x1) * (x3 - x1) + (y3 - y1) * (y3 - y1);
|
|
3543
|
+
else if (d3 >= 1)
|
|
3544
|
+
d3 = (x3 - x4) * (x3 - x4) + (y3 - y4) * (y3 - y4);
|
|
3545
|
+
else {
|
|
3546
|
+
const px = x1 + d3 * dx, py = y1 + d3 * dy;
|
|
3547
|
+
d3 = (x3 - px) * (x3 - px) + (y3 - py) * (y3 - py);
|
|
3548
|
+
}
|
|
3549
|
+
}
|
|
3550
|
+
if (d2 > d3) {
|
|
3551
|
+
if (d2 < _distTolSq) {
|
|
3552
|
+
emit(x2, y2);
|
|
3553
|
+
return;
|
|
3554
|
+
}
|
|
3555
|
+
}
|
|
3556
|
+
else {
|
|
3557
|
+
if (d3 < _distTolSq) {
|
|
3558
|
+
emit(x3, y3);
|
|
3559
|
+
return;
|
|
3560
|
+
}
|
|
3561
|
+
}
|
|
3562
|
+
break;
|
|
3563
|
+
}
|
|
3564
|
+
case 1:
|
|
3565
|
+
if (d3 * d3 <= _distTolSq * (dx * dx + dy * dy)) {
|
|
3566
|
+
if (_angleTol > 0) {
|
|
3567
|
+
const v1x = x3 - x2, v1y = y3 - y2;
|
|
3568
|
+
const v2x = x4 - x3, v2y = y4 - y3;
|
|
3569
|
+
const cross = v1x * v2y - v1y * v2x;
|
|
3570
|
+
const dot = v1x * v2x + v1y * v2y;
|
|
3571
|
+
if (dot > 0 && cross * cross < _tanAngSq * dot * dot) {
|
|
3572
|
+
emit(x2, y2);
|
|
3573
|
+
emit(x3, y3);
|
|
3574
|
+
return;
|
|
3575
|
+
}
|
|
3576
|
+
if (_cuspLim > 0 &&
|
|
3577
|
+
(dot <= 0 || cross * cross > _tanCuspSq * dot * dot)) {
|
|
3578
|
+
emit(x3, y3);
|
|
3579
|
+
return;
|
|
3580
|
+
}
|
|
3581
|
+
}
|
|
3582
|
+
else {
|
|
3583
|
+
emit(x23, y23);
|
|
3584
|
+
return;
|
|
3585
|
+
}
|
|
3586
|
+
}
|
|
3587
|
+
break;
|
|
3588
|
+
case 2:
|
|
3589
|
+
if (d2 * d2 <= _distTolSq * (dx * dx + dy * dy)) {
|
|
3590
|
+
if (_angleTol > 0) {
|
|
3591
|
+
const v1x = x2 - x1, v1y = y2 - y1;
|
|
3592
|
+
const v2x = x3 - x2, v2y = y3 - y2;
|
|
3593
|
+
const cross = v1x * v2y - v1y * v2x;
|
|
3594
|
+
const dot = v1x * v2x + v1y * v2y;
|
|
3595
|
+
if (dot > 0 && cross * cross < _tanAngSq * dot * dot) {
|
|
3596
|
+
emit(x2, y2);
|
|
3597
|
+
emit(x3, y3);
|
|
3598
|
+
return;
|
|
3599
|
+
}
|
|
3600
|
+
if (_cuspLim > 0 &&
|
|
3601
|
+
(dot <= 0 || cross * cross > _tanCuspSq * dot * dot)) {
|
|
3602
|
+
emit(x2, y2);
|
|
3603
|
+
return;
|
|
3604
|
+
}
|
|
3605
|
+
}
|
|
3606
|
+
else {
|
|
3607
|
+
emit(x23, y23);
|
|
3608
|
+
return;
|
|
3609
|
+
}
|
|
3610
|
+
}
|
|
3611
|
+
break;
|
|
3612
|
+
case 3: {
|
|
3613
|
+
if ((d2 + d3) * (d2 + d3) <= _distTolSq * (dx * dx + dy * dy)) {
|
|
3614
|
+
if (_angleTol > 0) {
|
|
3615
|
+
const a1x = x2 - x1, a1y = y2 - y1;
|
|
3616
|
+
const a2x = x3 - x2, a2y = y3 - y2;
|
|
3617
|
+
const c1 = a1x * a2y - a1y * a2x;
|
|
3618
|
+
const dot1 = a1x * a2x + a1y * a2y;
|
|
3619
|
+
const b2x = x4 - x3, b2y = y4 - y3;
|
|
3620
|
+
const c2 = a2x * b2y - a2y * b2x;
|
|
3621
|
+
const dot2 = a2x * b2x + a2y * b2y;
|
|
3622
|
+
// Sum of unsigned angles via tangent addition identity
|
|
3623
|
+
if (dot1 > 0 && dot2 > 0) {
|
|
3624
|
+
const ac1 = c1 < 0 ? -c1 : c1;
|
|
3625
|
+
const ac2 = c2 < 0 ? -c2 : c2;
|
|
3626
|
+
const cc = ac1 * dot2 + ac2 * dot1;
|
|
3627
|
+
const cd = dot1 * dot2 - ac1 * ac2;
|
|
3628
|
+
if (cd > 0 && cc * cc < _tanAngSq * cd * cd) {
|
|
3629
|
+
emit(x23, y23);
|
|
3630
|
+
return;
|
|
3631
|
+
}
|
|
3632
|
+
}
|
|
3633
|
+
if (_cuspLim > 0) {
|
|
3634
|
+
if (dot1 <= 0 || c1 * c1 > _tanCuspSq * dot1 * dot1) {
|
|
3635
|
+
emit(x2, y2);
|
|
3636
|
+
return;
|
|
3637
|
+
}
|
|
3638
|
+
if (dot2 <= 0 || c2 * c2 > _tanCuspSq * dot2 * dot2) {
|
|
3639
|
+
emit(x3, y3);
|
|
3640
|
+
return;
|
|
3641
|
+
}
|
|
3642
|
+
}
|
|
3643
|
+
}
|
|
3644
|
+
else {
|
|
3645
|
+
emit(x23, y23);
|
|
3646
|
+
return;
|
|
3647
|
+
}
|
|
3648
|
+
}
|
|
3649
|
+
break;
|
|
3650
|
+
}
|
|
3651
|
+
}
|
|
3652
|
+
const nl = level + 1;
|
|
3653
|
+
cubicRec(x1, y1, x12, y12, x123, y123, x1234, y1234, nl);
|
|
3654
|
+
cubicRec(x1234, y1234, x234, y234, x34, y34, x4, y4, nl);
|
|
3655
|
+
}
|
|
3390
3656
|
class Polygonizer {
|
|
3391
3657
|
constructor(curveFidelityConfig) {
|
|
3392
3658
|
this.curveSteps = null;
|
|
3659
|
+
// Precomputed tolerances
|
|
3660
|
+
this._distTolSq = 0;
|
|
3661
|
+
this._angleTol = 0;
|
|
3662
|
+
this._tanAngSq = 0;
|
|
3663
|
+
this._cuspLim = 0;
|
|
3664
|
+
this._tanCuspSq = 0;
|
|
3665
|
+
this._colEps = 0;
|
|
3666
|
+
this._maxLvl = 0;
|
|
3393
3667
|
this.curveFidelityConfig = {
|
|
3394
3668
|
...DEFAULT_CURVE_FIDELITY,
|
|
3395
3669
|
...curveFidelityConfig
|
|
3396
3670
|
};
|
|
3671
|
+
this.precompute();
|
|
3397
3672
|
}
|
|
3398
3673
|
setCurveFidelityConfig(curveFidelityConfig) {
|
|
3399
3674
|
this.curveFidelityConfig = {
|
|
3400
3675
|
...DEFAULT_CURVE_FIDELITY,
|
|
3401
3676
|
...curveFidelityConfig
|
|
3402
3677
|
};
|
|
3678
|
+
this.precompute();
|
|
3679
|
+
}
|
|
3680
|
+
precompute() {
|
|
3681
|
+
const c = this.curveFidelityConfig;
|
|
3682
|
+
const dt = c.distanceTolerance ?? DEFAULT_CURVE_FIDELITY.distanceTolerance;
|
|
3683
|
+
this._distTolSq = dt * dt;
|
|
3684
|
+
this._angleTol = c.angleTolerance ?? DEFAULT_CURVE_FIDELITY.angleTolerance;
|
|
3685
|
+
this._tanAngSq = this._angleTol > 0
|
|
3686
|
+
? Math.tan(this._angleTol) ** 2 : 0;
|
|
3687
|
+
this._cuspLim = c.cuspLimit ?? 0;
|
|
3688
|
+
this._tanCuspSq = this._cuspLim > 0
|
|
3689
|
+
? Math.tan(this._cuspLim) ** 2 : 0;
|
|
3690
|
+
this._colEps = c.collinearityEpsilon ?? DEFAULT_CURVE_FIDELITY.collinearityEpsilon;
|
|
3691
|
+
this._maxLvl = c.recursionLimit ?? DEFAULT_CURVE_FIDELITY.recursionLimit;
|
|
3692
|
+
}
|
|
3693
|
+
// Set module-level state from instance tolerances
|
|
3694
|
+
activate() {
|
|
3695
|
+
_distTolSq = this._distTolSq;
|
|
3696
|
+
_angleTol = this._angleTol;
|
|
3697
|
+
_tanAngSq = this._tanAngSq;
|
|
3698
|
+
_cuspLim = this._cuspLim;
|
|
3699
|
+
_tanCuspSq = this._tanCuspSq;
|
|
3700
|
+
_colEps = this._colEps;
|
|
3701
|
+
_maxLvl = this._maxLvl;
|
|
3702
|
+
_out = [];
|
|
3403
3703
|
}
|
|
3404
3704
|
// Fixed-step subdivision; overrides adaptive curveFidelity when set
|
|
3405
3705
|
setCurveSteps(curveSteps) {
|
|
@@ -3418,280 +3718,49 @@ class Polygonizer {
|
|
|
3418
3718
|
if (this.curveSteps !== null) {
|
|
3419
3719
|
return this.polygonizeQuadraticFixedSteps(start, control, end, this.curveSteps);
|
|
3420
3720
|
}
|
|
3421
|
-
|
|
3422
|
-
|
|
3423
|
-
|
|
3424
|
-
return
|
|
3721
|
+
this.activate();
|
|
3722
|
+
quadRec(start.x, start.y, control.x, control.y, end.x, end.y, 0);
|
|
3723
|
+
emit(end.x, end.y);
|
|
3724
|
+
return _out;
|
|
3425
3725
|
}
|
|
3426
3726
|
polygonizeCubic(start, control1, control2, end) {
|
|
3427
3727
|
if (this.curveSteps !== null) {
|
|
3428
3728
|
return this.polygonizeCubicFixedSteps(start, control1, control2, end, this.curveSteps);
|
|
3429
3729
|
}
|
|
3430
|
-
|
|
3431
|
-
|
|
3432
|
-
|
|
3433
|
-
return
|
|
3434
|
-
}
|
|
3435
|
-
lerp(a, b, t) {
|
|
3436
|
-
return a + (b - a) * t;
|
|
3730
|
+
this.activate();
|
|
3731
|
+
cubicBegin(start.x, start.y, control1.x, control1.y, control2.x, control2.y, end.x, end.y);
|
|
3732
|
+
emit(end.x, end.y);
|
|
3733
|
+
return _out;
|
|
3437
3734
|
}
|
|
3438
3735
|
polygonizeQuadraticFixedSteps(start, control, end, steps) {
|
|
3439
|
-
|
|
3440
|
-
// Emit intermediate points; caller already has start
|
|
3736
|
+
this.activate();
|
|
3441
3737
|
for (let i = 1; i <= steps; i++) {
|
|
3442
3738
|
const t = i / steps;
|
|
3443
|
-
const x12 =
|
|
3444
|
-
const y12 =
|
|
3445
|
-
const x23 =
|
|
3446
|
-
const y23 =
|
|
3447
|
-
|
|
3448
|
-
const y = this.lerp(y12, y23, t);
|
|
3449
|
-
this.addPoint(x, y, points);
|
|
3739
|
+
const x12 = start.x + (control.x - start.x) * t;
|
|
3740
|
+
const y12 = start.y + (control.y - start.y) * t;
|
|
3741
|
+
const x23 = control.x + (end.x - control.x) * t;
|
|
3742
|
+
const y23 = control.y + (end.y - control.y) * t;
|
|
3743
|
+
emit(x12 + (x23 - x12) * t, y12 + (y23 - y12) * t);
|
|
3450
3744
|
}
|
|
3451
|
-
return
|
|
3745
|
+
return _out;
|
|
3452
3746
|
}
|
|
3453
3747
|
polygonizeCubicFixedSteps(start, control1, control2, end, steps) {
|
|
3454
|
-
|
|
3455
|
-
// Emit intermediate points; caller already has start
|
|
3748
|
+
this.activate();
|
|
3456
3749
|
for (let i = 1; i <= steps; i++) {
|
|
3457
3750
|
const t = i / steps;
|
|
3458
|
-
|
|
3459
|
-
const
|
|
3460
|
-
const
|
|
3461
|
-
const
|
|
3462
|
-
const
|
|
3463
|
-
const
|
|
3464
|
-
const
|
|
3465
|
-
const
|
|
3466
|
-
const
|
|
3467
|
-
const
|
|
3468
|
-
|
|
3469
|
-
|
|
3470
|
-
|
|
3471
|
-
this.addPoint(x, y, points);
|
|
3472
|
-
}
|
|
3473
|
-
return points;
|
|
3474
|
-
}
|
|
3475
|
-
recursiveQuadratic(x1, y1, x2, y2, x3, y3, points, level = 0) {
|
|
3476
|
-
if (level > RECURSION_LIMIT)
|
|
3477
|
-
return;
|
|
3478
|
-
// De Casteljau subdivision: split the curve at t=0.5
|
|
3479
|
-
// First calculate midpoints of the two line segments
|
|
3480
|
-
const x12 = (x1 + x2) / 2;
|
|
3481
|
-
const y12 = (y1 + y2) / 2;
|
|
3482
|
-
const x23 = (x2 + x3) / 2;
|
|
3483
|
-
const y23 = (y2 + y3) / 2;
|
|
3484
|
-
// Then find the midpoint of those midpoints - this is the curve point at t=0.5
|
|
3485
|
-
const x123 = (x12 + x23) / 2;
|
|
3486
|
-
const y123 = (y12 + y23) / 2;
|
|
3487
|
-
const dx = x3 - x1;
|
|
3488
|
-
const dy = y3 - y1;
|
|
3489
|
-
const d = Math.abs((x2 - x3) * dy - (y2 - y3) * dx);
|
|
3490
|
-
const baseTolerance = this.curveFidelityConfig.distanceTolerance ??
|
|
3491
|
-
DEFAULT_CURVE_FIDELITY.distanceTolerance;
|
|
3492
|
-
const distanceTolerance = baseTolerance * baseTolerance;
|
|
3493
|
-
if (d > COLLINEARITY_EPSILON) {
|
|
3494
|
-
// Regular case
|
|
3495
|
-
// Recursion terminates when the curve is flat enough (deviation from straight line is within tolerance)
|
|
3496
|
-
if (d * d <= distanceTolerance * (dx * dx + dy * dy)) {
|
|
3497
|
-
// Angle check
|
|
3498
|
-
const angleTolerance = this.curveFidelityConfig.angleTolerance ??
|
|
3499
|
-
DEFAULT_CURVE_FIDELITY.angleTolerance;
|
|
3500
|
-
if (angleTolerance > 0) {
|
|
3501
|
-
// Angle between segments (p1->p2) and (p2->p3)
|
|
3502
|
-
// atan2(cross, dot) avoids computing 2 separate atan2() values
|
|
3503
|
-
const v1x = x2 - x1;
|
|
3504
|
-
const v1y = y2 - y1;
|
|
3505
|
-
const v2x = x3 - x2;
|
|
3506
|
-
const v2y = y3 - y2;
|
|
3507
|
-
const da = Math.abs(Math.atan2(v1x * v2y - v1y * v2x, v1x * v2x + v1y * v2y));
|
|
3508
|
-
if (da < angleTolerance) {
|
|
3509
|
-
this.addPoint(x2, y2, points);
|
|
3510
|
-
return;
|
|
3511
|
-
}
|
|
3512
|
-
}
|
|
3513
|
-
else {
|
|
3514
|
-
this.addPoint(x2, y2, points);
|
|
3515
|
-
return;
|
|
3516
|
-
}
|
|
3517
|
-
}
|
|
3518
|
-
}
|
|
3519
|
-
else {
|
|
3520
|
-
// Collinear case
|
|
3521
|
-
const da = dx * dx + dy * dy;
|
|
3522
|
-
if (da === 0) {
|
|
3523
|
-
const d2 = (x2 - x1) * (x2 - x1) + (y2 - y1) * (y2 - y1);
|
|
3524
|
-
if (d2 <= distanceTolerance) {
|
|
3525
|
-
this.addPoint(x2, y2, points);
|
|
3526
|
-
return;
|
|
3527
|
-
}
|
|
3528
|
-
}
|
|
3529
|
-
else {
|
|
3530
|
-
const d2 = ((x2 - x1) * dx + (y2 - y1) * dy) / da;
|
|
3531
|
-
if (d2 > 0 && d2 < 1 && d * d <= distanceTolerance * da) {
|
|
3532
|
-
this.addPoint(x2, y2, points);
|
|
3533
|
-
return;
|
|
3534
|
-
}
|
|
3535
|
-
}
|
|
3536
|
-
}
|
|
3537
|
-
// Continue subdividing
|
|
3538
|
-
this.recursiveQuadratic(x1, y1, x12, y12, x123, y123, points, level + 1);
|
|
3539
|
-
this.recursiveQuadratic(x123, y123, x23, y23, x3, y3, points, level + 1);
|
|
3540
|
-
}
|
|
3541
|
-
recursiveCubic(x1, y1, x2, y2, x3, y3, x4, y4, points, level = 0) {
|
|
3542
|
-
if (level > RECURSION_LIMIT)
|
|
3543
|
-
return;
|
|
3544
|
-
// De Casteljau subdivision for cubic curves
|
|
3545
|
-
const x12 = (x1 + x2) / 2;
|
|
3546
|
-
const y12 = (y1 + y2) / 2;
|
|
3547
|
-
const x23 = (x2 + x3) / 2;
|
|
3548
|
-
const y23 = (y2 + y3) / 2;
|
|
3549
|
-
const x34 = (x3 + x4) / 2;
|
|
3550
|
-
const y34 = (y3 + y4) / 2;
|
|
3551
|
-
const x123 = (x12 + x23) / 2;
|
|
3552
|
-
const y123 = (y12 + y23) / 2;
|
|
3553
|
-
const x234 = (x23 + x34) / 2;
|
|
3554
|
-
const y234 = (y23 + y34) / 2;
|
|
3555
|
-
const x1234 = (x123 + x234) / 2;
|
|
3556
|
-
const y1234 = (y123 + y234) / 2;
|
|
3557
|
-
const dx = x4 - x1;
|
|
3558
|
-
const dy = y4 - y1;
|
|
3559
|
-
const d2 = Math.abs((x2 - x4) * dy - (y2 - y4) * dx);
|
|
3560
|
-
const d3 = Math.abs((x3 - x4) * dy - (y3 - y4) * dx);
|
|
3561
|
-
const baseTolerance = this.curveFidelityConfig.distanceTolerance ??
|
|
3562
|
-
DEFAULT_CURVE_FIDELITY.distanceTolerance;
|
|
3563
|
-
const distanceTolerance = baseTolerance * baseTolerance;
|
|
3564
|
-
let switchCondition = 0;
|
|
3565
|
-
if (d2 > COLLINEARITY_EPSILON)
|
|
3566
|
-
switchCondition |= 1;
|
|
3567
|
-
if (d3 > COLLINEARITY_EPSILON)
|
|
3568
|
-
switchCondition |= 2;
|
|
3569
|
-
switch (switchCondition) {
|
|
3570
|
-
case 0:
|
|
3571
|
-
// All collinear OR p1==p4
|
|
3572
|
-
const k = dx * dx + dy * dy;
|
|
3573
|
-
if (k === 0) {
|
|
3574
|
-
const d2_sq = (x2 - x1) * (x2 - x1) + (y2 - y1) * (y2 - y1);
|
|
3575
|
-
const d3_sq = (x3 - x1) * (x3 - x1) + (y3 - y1) * (y3 - y1);
|
|
3576
|
-
if (d2_sq <= distanceTolerance && d3_sq <= distanceTolerance) {
|
|
3577
|
-
this.addPoint(x2, y2, points);
|
|
3578
|
-
this.addPoint(x3, y3, points);
|
|
3579
|
-
return;
|
|
3580
|
-
}
|
|
3581
|
-
}
|
|
3582
|
-
else {
|
|
3583
|
-
const da1 = ((x2 - x1) * dx + (y2 - y1) * dy) / k;
|
|
3584
|
-
const da2 = ((x3 - x1) * dx + (y3 - y1) * dy) / k;
|
|
3585
|
-
if (da1 > 0 &&
|
|
3586
|
-
da1 < 1 &&
|
|
3587
|
-
da2 > 0 &&
|
|
3588
|
-
da2 < 1 &&
|
|
3589
|
-
(d2 + d3) * (d2 + d3) <= distanceTolerance * k) {
|
|
3590
|
-
this.addPoint(x2, y2, points);
|
|
3591
|
-
this.addPoint(x3, y3, points);
|
|
3592
|
-
return;
|
|
3593
|
-
}
|
|
3594
|
-
}
|
|
3595
|
-
break;
|
|
3596
|
-
case 1:
|
|
3597
|
-
// p1,p2,p4 are collinear, p3 is not
|
|
3598
|
-
if (d3 * d3 <= distanceTolerance * (dx * dx + dy * dy)) {
|
|
3599
|
-
const angleTolerance = this.curveFidelityConfig.angleTolerance ??
|
|
3600
|
-
DEFAULT_CURVE_FIDELITY.angleTolerance;
|
|
3601
|
-
if (angleTolerance > 0) {
|
|
3602
|
-
// Angle between segments (p2->p3) and (p3->p4)
|
|
3603
|
-
const v1x = x3 - x2;
|
|
3604
|
-
const v1y = y3 - y2;
|
|
3605
|
-
const v2x = x4 - x3;
|
|
3606
|
-
const v2y = y4 - y3;
|
|
3607
|
-
const da1 = Math.abs(Math.atan2(v1x * v2y - v1y * v2x, v1x * v2x + v1y * v2y));
|
|
3608
|
-
if (da1 < angleTolerance) {
|
|
3609
|
-
this.addPoint(x2, y2, points);
|
|
3610
|
-
this.addPoint(x3, y3, points);
|
|
3611
|
-
return;
|
|
3612
|
-
}
|
|
3613
|
-
}
|
|
3614
|
-
else {
|
|
3615
|
-
this.addPoint(x2, y2, points);
|
|
3616
|
-
this.addPoint(x3, y3, points);
|
|
3617
|
-
return;
|
|
3618
|
-
}
|
|
3619
|
-
}
|
|
3620
|
-
break;
|
|
3621
|
-
case 2:
|
|
3622
|
-
// p1,p3,p4 are collinear, p2 is not
|
|
3623
|
-
if (d2 * d2 <= distanceTolerance * (dx * dx + dy * dy)) {
|
|
3624
|
-
const angleTolerance = this.curveFidelityConfig.angleTolerance ??
|
|
3625
|
-
DEFAULT_CURVE_FIDELITY.angleTolerance;
|
|
3626
|
-
if (angleTolerance > 0) {
|
|
3627
|
-
// Angle between segments (p1->p2) and (p2->p3)
|
|
3628
|
-
const v1x = x2 - x1;
|
|
3629
|
-
const v1y = y2 - y1;
|
|
3630
|
-
const v2x = x3 - x2;
|
|
3631
|
-
const v2y = y3 - y2;
|
|
3632
|
-
const da1 = Math.abs(Math.atan2(v1x * v2y - v1y * v2x, v1x * v2x + v1y * v2y));
|
|
3633
|
-
if (da1 < angleTolerance) {
|
|
3634
|
-
this.addPoint(x2, y2, points);
|
|
3635
|
-
this.addPoint(x3, y3, points);
|
|
3636
|
-
return;
|
|
3637
|
-
}
|
|
3638
|
-
}
|
|
3639
|
-
else {
|
|
3640
|
-
this.addPoint(x2, y2, points);
|
|
3641
|
-
this.addPoint(x3, y3, points);
|
|
3642
|
-
return;
|
|
3643
|
-
}
|
|
3644
|
-
}
|
|
3645
|
-
break;
|
|
3646
|
-
case 3:
|
|
3647
|
-
// Regular case
|
|
3648
|
-
if ((d2 + d3) * (d2 + d3) <= distanceTolerance * (dx * dx + dy * dy)) {
|
|
3649
|
-
const angleTolerance = this.curveFidelityConfig.angleTolerance ??
|
|
3650
|
-
DEFAULT_CURVE_FIDELITY.angleTolerance;
|
|
3651
|
-
if (angleTolerance > 0) {
|
|
3652
|
-
// da1: angle between (p1->p2) and (p2->p3)
|
|
3653
|
-
const a1x = x2 - x1;
|
|
3654
|
-
const a1y = y2 - y1;
|
|
3655
|
-
const a2x = x3 - x2;
|
|
3656
|
-
const a2y = y3 - y2;
|
|
3657
|
-
const da1 = Math.abs(Math.atan2(a1x * a2y - a1y * a2x, a1x * a2x + a1y * a2y));
|
|
3658
|
-
// da2: angle between (p2->p3) and (p3->p4)
|
|
3659
|
-
const b1x = a2x;
|
|
3660
|
-
const b1y = a2y;
|
|
3661
|
-
const b2x = x4 - x3;
|
|
3662
|
-
const b2y = y4 - y3;
|
|
3663
|
-
const da2 = Math.abs(Math.atan2(b1x * b2y - b1y * b2x, b1x * b2x + b1y * b2y));
|
|
3664
|
-
if (da1 + da2 < angleTolerance) {
|
|
3665
|
-
this.addPoint(x2, y2, points);
|
|
3666
|
-
this.addPoint(x3, y3, points);
|
|
3667
|
-
return;
|
|
3668
|
-
}
|
|
3669
|
-
}
|
|
3670
|
-
else {
|
|
3671
|
-
this.addPoint(x2, y2, points);
|
|
3672
|
-
this.addPoint(x3, y3, points);
|
|
3673
|
-
return;
|
|
3674
|
-
}
|
|
3675
|
-
}
|
|
3676
|
-
break;
|
|
3677
|
-
}
|
|
3678
|
-
// Continue subdividing
|
|
3679
|
-
this.recursiveCubic(x1, y1, x12, y12, x123, y123, x1234, y1234, points, level + 1);
|
|
3680
|
-
this.recursiveCubic(x1234, y1234, x234, y234, x34, y34, x4, y4, points, level + 1);
|
|
3681
|
-
}
|
|
3682
|
-
addPoint(x, y, points) {
|
|
3683
|
-
const newPoint = new Vec2(x, y);
|
|
3684
|
-
if (points.length === 0) {
|
|
3685
|
-
points.push(newPoint);
|
|
3686
|
-
return;
|
|
3687
|
-
}
|
|
3688
|
-
const lastPoint = points[points.length - 1];
|
|
3689
|
-
const dx = newPoint.x - lastPoint.x;
|
|
3690
|
-
const dy = newPoint.y - lastPoint.y;
|
|
3691
|
-
const distanceSquared = dx * dx + dy * dy;
|
|
3692
|
-
if (distanceSquared > COLLINEARITY_EPSILON * COLLINEARITY_EPSILON) {
|
|
3693
|
-
points.push(newPoint);
|
|
3694
|
-
}
|
|
3751
|
+
const x12 = start.x + (control1.x - start.x) * t;
|
|
3752
|
+
const y12 = start.y + (control1.y - start.y) * t;
|
|
3753
|
+
const x23 = control1.x + (control2.x - control1.x) * t;
|
|
3754
|
+
const y23 = control1.y + (control2.y - control1.y) * t;
|
|
3755
|
+
const x34 = control2.x + (end.x - control2.x) * t;
|
|
3756
|
+
const y34 = control2.y + (end.y - control2.y) * t;
|
|
3757
|
+
const x123 = x12 + (x23 - x12) * t;
|
|
3758
|
+
const y123 = y12 + (y23 - y12) * t;
|
|
3759
|
+
const x234 = x23 + (x34 - x23) * t;
|
|
3760
|
+
const y234 = y23 + (y34 - y23) * t;
|
|
3761
|
+
emit(x123 + (x234 - x123) * t, y123 + (y234 - y123) * t);
|
|
3762
|
+
}
|
|
3763
|
+
return _out;
|
|
3695
3764
|
}
|
|
3696
3765
|
}
|
|
3697
3766
|
|