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