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 CHANGED
@@ -1,5 +1,5 @@
1
1
  /*!
2
- * three-text v0.4.10
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 // Remove triangles smaller than 1 square font unit
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
- if (points.length < 5) {
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
- let optimized = points;
3270
- // Visvalingam-Whyatt simplification
3271
- optimized = this.simplifyPathVW(optimized, this.config.areaThreshold);
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 // ~11.5 degrees
3395
+ angleTolerance: 0.2,
3396
+ cuspLimit: 0,
3397
+ collinearityEpsilon: 1e-6,
3398
+ recursionLimit: 16
3387
3399
  };
3388
- const COLLINEARITY_EPSILON = 1e-6;
3389
- const RECURSION_LIMIT = 16;
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
- const points = [];
3422
- this.recursiveQuadratic(start.x, start.y, control.x, control.y, end.x, end.y, points);
3423
- this.addPoint(end.x, end.y, points);
3424
- return points;
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
- const points = [];
3431
- this.recursiveCubic(start.x, start.y, control1.x, control1.y, control2.x, control2.y, end.x, end.y, points);
3432
- this.addPoint(end.x, end.y, points);
3433
- return points;
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
- const points = [];
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 = this.lerp(start.x, control.x, t);
3444
- const y12 = this.lerp(start.y, control.y, t);
3445
- const x23 = this.lerp(control.x, end.x, t);
3446
- const y23 = this.lerp(control.y, end.y, t);
3447
- const x = this.lerp(x12, x23, t);
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 points;
3745
+ return _out;
3452
3746
  }
3453
3747
  polygonizeCubicFixedSteps(start, control1, control2, end, steps) {
3454
- const points = [];
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
- // De Casteljau
3459
- const x12 = this.lerp(start.x, control1.x, t);
3460
- const y12 = this.lerp(start.y, control1.y, t);
3461
- const x23 = this.lerp(control1.x, control2.x, t);
3462
- const y23 = this.lerp(control1.y, control2.y, t);
3463
- const x34 = this.lerp(control2.x, end.x, t);
3464
- const y34 = this.lerp(control2.y, end.y, t);
3465
- const x123 = this.lerp(x12, x23, t);
3466
- const y123 = this.lerp(y12, y23, t);
3467
- const x234 = this.lerp(x23, x34, t);
3468
- const y234 = this.lerp(y23, y34, t);
3469
- const x = this.lerp(x123, x234, t);
3470
- const y = this.lerp(y123, y234, t);
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