three-text 0.3.4 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.umd.js CHANGED
@@ -1,5 +1,5 @@
1
1
  /*!
2
- * three-text v0.3.4
2
+ * three-text v0.4.0
3
3
  * Copyright (C) 2025 Countertype LLC
4
4
  *
5
5
  * This program is free software: you can redistribute it and/or modify
@@ -200,7 +200,7 @@
200
200
  FitnessClass[FitnessClass["DECENT"] = 2] = "DECENT";
201
201
  FitnessClass[FitnessClass["TIGHT"] = 3] = "TIGHT"; // lines shrinking 0.5 to 1.0 of their shrinkability
202
202
  })(FitnessClass || (FitnessClass = {}));
203
- // Fast active node management with O(1) operations
203
+ // Active node management with Map for O(1) lookup by (position, fitness)
204
204
  class ActiveNodeList {
205
205
  constructor() {
206
206
  this.nodesByKey = new Map();
@@ -231,7 +231,6 @@
231
231
  this.nodesByKey.set(key, node);
232
232
  return true;
233
233
  }
234
- // Swap-and-pop removal
235
234
  deactivate(node) {
236
235
  if (!node.active)
237
236
  return;
@@ -374,36 +373,66 @@
374
373
  const code = char.codePointAt(0);
375
374
  if (code === undefined)
376
375
  return false;
377
- return ((code >= 0x4e00 && code <= 0x9fff) ||
378
- (code >= 0x3400 && code <= 0x4dbf) ||
379
- (code >= 0x20000 && code <= 0x2a6df) ||
380
- (code >= 0x2a700 && code <= 0x2b73f) ||
381
- (code >= 0x2b740 && code <= 0x2b81f) ||
382
- (code >= 0x2b820 && code <= 0x2ceaf) ||
383
- (code >= 0xf900 && code <= 0xfaff) ||
384
- (code >= 0x3040 && code <= 0x309f) ||
385
- (code >= 0x30a0 && code <= 0x30ff) ||
386
- (code >= 0xac00 && code <= 0xd7af) ||
387
- (code >= 0x1100 && code <= 0x11ff) ||
388
- (code >= 0x3130 && code <= 0x318f) ||
389
- (code >= 0xa960 && code <= 0xa97f) ||
390
- (code >= 0xd7b0 && code <= 0xd7ff) ||
391
- (code >= 0xffa0 && code <= 0xffdc));
392
- }
376
+ return ((code >= 0x4e00 && code <= 0x9fff) || // CJK Unified Ideographs
377
+ (code >= 0x3400 && code <= 0x4dbf) || // CJK Extension A
378
+ (code >= 0x20000 && code <= 0x2a6df) || // CJK Extension B
379
+ (code >= 0x2a700 && code <= 0x2b73f) || // CJK Extension C
380
+ (code >= 0x2b740 && code <= 0x2b81f) || // CJK Extension D
381
+ (code >= 0x2b820 && code <= 0x2ceaf) || // CJK Extension E
382
+ (code >= 0xf900 && code <= 0xfaff) || // CJK Compatibility Ideographs
383
+ (code >= 0x3040 && code <= 0x309f) || // Hiragana
384
+ (code >= 0x30a0 && code <= 0x30ff) || // Katakana
385
+ (code >= 0xac00 && code <= 0xd7af) || // Hangul Syllables
386
+ (code >= 0x1100 && code <= 0x11ff) || // Hangul Jamo
387
+ (code >= 0x3130 && code <= 0x318f) || // Hangul Compatibility Jamo
388
+ (code >= 0xa960 && code <= 0xa97f) || // Hangul Jamo Extended-A
389
+ (code >= 0xd7b0 && code <= 0xd7ff) || // Hangul Jamo Extended-B
390
+ (code >= 0xffa0 && code <= 0xffdc) // Halfwidth Hangul
391
+ );
392
+ }
393
+ // Closing punctuation - no line break before (UAX #14 CL, JIS X 4051)
393
394
  static isCJClosingPunctuation(char) {
394
395
  const code = char.charCodeAt(0);
395
- return (code === 0x3001 || code === 0x3002 || code === 0xff0c || code === 0xff0e ||
396
- code === 0xff1a || code === 0xff1b || code === 0xff01 || code === 0xff1f ||
397
- code === 0xff09 || code === 0x3011 || code === 0xff5d || code === 0x300d ||
398
- code === 0x300f || code === 0x3009 || code === 0x300b || code === 0x3015 ||
399
- code === 0x3017 || code === 0x3019 || code === 0x301b || code === 0x30fc ||
400
- code === 0x2014 || code === 0x2026 || code === 0x2025);
401
- }
396
+ return (code === 0x3001 || //
397
+ code === 0x3002 || //
398
+ code === 0xff0c || //
399
+ code === 0xff0e || //
400
+ code === 0xff1a || //
401
+ code === 0xff1b || //
402
+ code === 0xff01 || // !
403
+ code === 0xff1f || // ?
404
+ code === 0xff09 || // )
405
+ code === 0x3011 || // 】
406
+ code === 0xff5d || // }
407
+ code === 0x300d || // 」
408
+ code === 0x300f || // 』
409
+ code === 0x3009 || // 〉
410
+ code === 0x300b || // 》
411
+ code === 0x3015 || // 〕
412
+ code === 0x3017 || // 〗
413
+ code === 0x3019 || // 〙
414
+ code === 0x301b || // 〛
415
+ code === 0x30fc || // ー
416
+ code === 0x2014 || // —
417
+ code === 0x2026 || // …
418
+ code === 0x2025 // ‥
419
+ );
420
+ }
421
+ // Opening punctuation - no line break after (UAX #14 OP, JIS X 4051)
402
422
  static isCJOpeningPunctuation(char) {
403
423
  const code = char.charCodeAt(0);
404
- return (code === 0xff08 || code === 0x3010 || code === 0xff5b || code === 0x300c ||
405
- code === 0x300e || code === 0x3008 || code === 0x300a || code === 0x3014 ||
406
- code === 0x3016 || code === 0x3018 || code === 0x301a);
424
+ return (code === 0xff08 || //
425
+ code === 0x3010 || //
426
+ code === 0xff5b || //
427
+ code === 0x300c || // 「
428
+ code === 0x300e || // 『
429
+ code === 0x3008 || // 〈
430
+ code === 0x300a || // 《
431
+ code === 0x3014 || // 〔
432
+ code === 0x3016 || // 〖
433
+ code === 0x3018 || // 〘
434
+ code === 0x301a // 〚
435
+ );
407
436
  }
408
437
  static isCJPunctuation(char) {
409
438
  return this.isCJClosingPunctuation(char) || this.isCJOpeningPunctuation(char);
@@ -3046,15 +3075,12 @@
3046
3075
 
3047
3076
  const DEFAULT_OPTIMIZATION_CONFIG = {
3048
3077
  enabled: true,
3049
- areaThreshold: 1.0, // Remove triangles smaller than 1 square font unit
3050
- colinearThreshold: 0.0087, // ~0.5 degrees in radians
3051
- minSegmentLength: 10
3078
+ areaThreshold: 1.0 // Remove triangles smaller than 1 square font unit
3052
3079
  };
3053
3080
  class PathOptimizer {
3054
3081
  constructor(config) {
3055
3082
  this.stats = {
3056
3083
  pointsRemovedByVisvalingam: 0,
3057
- pointsRemovedByColinear: 0,
3058
3084
  originalPointCount: 0
3059
3085
  };
3060
3086
  this.config = config;
@@ -3063,7 +3089,10 @@
3063
3089
  this.config = config;
3064
3090
  }
3065
3091
  optimizePath(path) {
3066
- if (!this.config.enabled || path.points.length <= 2) {
3092
+ if (path.points.length <= 2) {
3093
+ return path;
3094
+ }
3095
+ if (!this.config.enabled) {
3067
3096
  return path;
3068
3097
  }
3069
3098
  this.stats.originalPointCount += path.points.length;
@@ -3073,11 +3102,9 @@
3073
3102
  if (points.length < 5) {
3074
3103
  return path;
3075
3104
  }
3076
- let optimized = this.simplifyPathVW(points, this.config.areaThreshold);
3077
- if (optimized.length < 3) {
3078
- return path;
3079
- }
3080
- optimized = this.removeColinearPoints(optimized, this.config.colinearThreshold);
3105
+ let optimized = points;
3106
+ // Visvalingam-Whyatt simplification
3107
+ optimized = this.simplifyPathVW(optimized, this.config.areaThreshold);
3081
3108
  if (optimized.length < 3) {
3082
3109
  return path;
3083
3110
  }
@@ -3114,17 +3141,6 @@
3114
3141
  if (!p || p.area > areaThreshold) {
3115
3142
  break;
3116
3143
  }
3117
- if (this.config.minSegmentLength > 0 && p.prev && p.next) {
3118
- const prevPoint = points[p.prev.index];
3119
- const currentPoint = points[p.index];
3120
- const nextPoint = points[p.next.index];
3121
- const len1 = prevPoint.distanceTo(currentPoint);
3122
- const len2 = currentPoint.distanceTo(nextPoint);
3123
- if (len1 < this.config.minSegmentLength ||
3124
- len2 < this.config.minSegmentLength) {
3125
- continue;
3126
- }
3127
- }
3128
3144
  if (p.prev)
3129
3145
  p.prev.next = p.next;
3130
3146
  if (p.next)
@@ -3149,32 +3165,6 @@
3149
3165
  this.stats.pointsRemovedByVisvalingam += pointsRemoved;
3150
3166
  return simplifiedPoints;
3151
3167
  }
3152
- removeColinearPoints(points, threshold) {
3153
- if (points.length <= 2)
3154
- return points;
3155
- const result = [points[0]];
3156
- for (let i = 1; i < points.length - 1; i++) {
3157
- const prev = points[i - 1];
3158
- const current = points[i];
3159
- const next = points[i + 1];
3160
- const v1x = current.x - prev.x;
3161
- const v1y = current.y - prev.y;
3162
- const v2x = next.x - current.x;
3163
- const v2y = next.y - current.y;
3164
- const angle = Math.abs(Math.atan2(v1x * v2y - v1y * v2x, v1x * v2x + v1y * v2y));
3165
- const v1LenSq = v1x * v1x + v1y * v1y;
3166
- const v2LenSq = v2x * v2x + v2y * v2y;
3167
- const minLenSq = this.config.minSegmentLength * this.config.minSegmentLength;
3168
- if (angle > threshold || v1LenSq < minLenSq || v2LenSq < minLenSq) {
3169
- result.push(current);
3170
- }
3171
- else {
3172
- this.stats.pointsRemovedByColinear++;
3173
- }
3174
- }
3175
- result.push(points[points.length - 1]);
3176
- return result;
3177
- }
3178
3168
  // Shoelace formula
3179
3169
  calculateTriangleArea(p1, p2, p3) {
3180
3170
  return Math.abs((p1.x * (p2.y - p3.y) + p2.x * (p3.y - p1.y) + p3.x * (p1.y - p2.y)) / 2);
@@ -3185,7 +3175,6 @@
3185
3175
  resetStats() {
3186
3176
  this.stats = {
3187
3177
  pointsRemovedByVisvalingam: 0,
3188
- pointsRemovedByColinear: 0,
3189
3178
  originalPointCount: 0
3190
3179
  };
3191
3180
  }
@@ -3236,6 +3225,7 @@
3236
3225
  const RECURSION_LIMIT = 16;
3237
3226
  class Polygonizer {
3238
3227
  constructor(curveFidelityConfig) {
3228
+ this.curveSteps = null;
3239
3229
  this.curveFidelityConfig = {
3240
3230
  ...DEFAULT_CURVE_FIDELITY,
3241
3231
  ...curveFidelityConfig
@@ -3247,18 +3237,77 @@
3247
3237
  ...curveFidelityConfig
3248
3238
  };
3249
3239
  }
3240
+ // Fixed-step subdivision; overrides adaptive curveFidelity when set
3241
+ setCurveSteps(curveSteps) {
3242
+ if (curveSteps === undefined || curveSteps === null) {
3243
+ this.curveSteps = null;
3244
+ return;
3245
+ }
3246
+ if (!Number.isFinite(curveSteps)) {
3247
+ this.curveSteps = null;
3248
+ return;
3249
+ }
3250
+ const stepsInt = Math.round(curveSteps);
3251
+ this.curveSteps = stepsInt >= 1 ? stepsInt : null;
3252
+ }
3250
3253
  polygonizeQuadratic(start, control, end) {
3254
+ if (this.curveSteps !== null) {
3255
+ return this.polygonizeQuadraticFixedSteps(start, control, end, this.curveSteps);
3256
+ }
3251
3257
  const points = [];
3252
3258
  this.recursiveQuadratic(start.x, start.y, control.x, control.y, end.x, end.y, points);
3253
3259
  this.addPoint(end.x, end.y, points);
3254
3260
  return points;
3255
3261
  }
3256
3262
  polygonizeCubic(start, control1, control2, end) {
3263
+ if (this.curveSteps !== null) {
3264
+ return this.polygonizeCubicFixedSteps(start, control1, control2, end, this.curveSteps);
3265
+ }
3257
3266
  const points = [];
3258
3267
  this.recursiveCubic(start.x, start.y, control1.x, control1.y, control2.x, control2.y, end.x, end.y, points);
3259
3268
  this.addPoint(end.x, end.y, points);
3260
3269
  return points;
3261
3270
  }
3271
+ lerp(a, b, t) {
3272
+ return a + (b - a) * t;
3273
+ }
3274
+ polygonizeQuadraticFixedSteps(start, control, end, steps) {
3275
+ const points = [];
3276
+ // Emit intermediate points; caller already has start
3277
+ for (let i = 1; i <= steps; i++) {
3278
+ const t = i / steps;
3279
+ const x12 = this.lerp(start.x, control.x, t);
3280
+ const y12 = this.lerp(start.y, control.y, t);
3281
+ const x23 = this.lerp(control.x, end.x, t);
3282
+ const y23 = this.lerp(control.y, end.y, t);
3283
+ const x = this.lerp(x12, x23, t);
3284
+ const y = this.lerp(y12, y23, t);
3285
+ this.addPoint(x, y, points);
3286
+ }
3287
+ return points;
3288
+ }
3289
+ polygonizeCubicFixedSteps(start, control1, control2, end, steps) {
3290
+ const points = [];
3291
+ // Emit intermediate points; caller already has start
3292
+ for (let i = 1; i <= steps; i++) {
3293
+ const t = i / steps;
3294
+ // De Casteljau
3295
+ const x12 = this.lerp(start.x, control1.x, t);
3296
+ const y12 = this.lerp(start.y, control1.y, t);
3297
+ const x23 = this.lerp(control1.x, control2.x, t);
3298
+ const y23 = this.lerp(control1.y, control2.y, t);
3299
+ const x34 = this.lerp(control2.x, end.x, t);
3300
+ const y34 = this.lerp(control2.y, end.y, t);
3301
+ const x123 = this.lerp(x12, x23, t);
3302
+ const y123 = this.lerp(y12, y23, t);
3303
+ const x234 = this.lerp(x23, x34, t);
3304
+ const y234 = this.lerp(y23, y34, t);
3305
+ const x = this.lerp(x123, x234, t);
3306
+ const y = this.lerp(y123, y234, t);
3307
+ this.addPoint(x, y, points);
3308
+ }
3309
+ return points;
3310
+ }
3262
3311
  recursiveQuadratic(x1, y1, x2, y2, x3, y3, points, level = 0) {
3263
3312
  if (level > RECURSION_LIMIT)
3264
3313
  return;
@@ -3285,8 +3334,8 @@
3285
3334
  const angleTolerance = this.curveFidelityConfig.angleTolerance ??
3286
3335
  DEFAULT_CURVE_FIDELITY.angleTolerance;
3287
3336
  if (angleTolerance > 0) {
3288
- // Angle between segments (p1->p2) and (p2->p3).
3289
- // Using atan2(cross, dot) avoids computing 2 separate atan2() values + wrap logic.
3337
+ // Angle between segments (p1->p2) and (p2->p3)
3338
+ // atan2(cross, dot) avoids computing 2 separate atan2() values
3290
3339
  const v1x = x2 - x1;
3291
3340
  const v1y = y2 - y1;
3292
3341
  const v2x = x3 - x2;
@@ -3671,6 +3720,9 @@
3671
3720
  setCurveFidelityConfig(config) {
3672
3721
  this.polygonizer.setCurveFidelityConfig(config);
3673
3722
  }
3723
+ setCurveSteps(curveSteps) {
3724
+ this.polygonizer.setCurveSteps(curveSteps);
3725
+ }
3674
3726
  setGeometryOptimization(options) {
3675
3727
  this.pathOptimizer.setConfig({
3676
3728
  ...DEFAULT_OPTIMIZATION_CONFIG,
@@ -3824,6 +3876,21 @@
3824
3876
  this.collector.setCurveFidelityConfig(config);
3825
3877
  this.updateCacheKeyPrefix();
3826
3878
  }
3879
+ setCurveSteps(curveSteps) {
3880
+ // Normalize: unset for undefined/null/non-finite/<=0
3881
+ if (curveSteps === undefined || curveSteps === null) {
3882
+ this.curveSteps = undefined;
3883
+ }
3884
+ else if (!Number.isFinite(curveSteps)) {
3885
+ this.curveSteps = undefined;
3886
+ }
3887
+ else {
3888
+ const stepsInt = Math.round(curveSteps);
3889
+ this.curveSteps = stepsInt >= 1 ? stepsInt : undefined;
3890
+ }
3891
+ this.collector.setCurveSteps(this.curveSteps);
3892
+ this.updateCacheKeyPrefix();
3893
+ }
3827
3894
  setGeometryOptimization(options) {
3828
3895
  this.geometryOptimizationOptions = options;
3829
3896
  this.collector.setGeometryOptimization(options);
@@ -3837,22 +3904,24 @@
3837
3904
  this.cacheKeyPrefix = `${this.fontId}__${this.getGeometryConfigSignature()}`;
3838
3905
  }
3839
3906
  getGeometryConfigSignature() {
3840
- const distanceTolerance = this.curveFidelityConfig?.distanceTolerance ??
3841
- DEFAULT_CURVE_FIDELITY.distanceTolerance;
3842
- const angleTolerance = this.curveFidelityConfig?.angleTolerance ??
3843
- DEFAULT_CURVE_FIDELITY.angleTolerance;
3907
+ const curveSignature = (() => {
3908
+ if (this.curveSteps !== undefined) {
3909
+ return `cf:steps:${this.curveSteps}`;
3910
+ }
3911
+ const distanceTolerance = this.curveFidelityConfig?.distanceTolerance ??
3912
+ DEFAULT_CURVE_FIDELITY.distanceTolerance;
3913
+ const angleTolerance = this.curveFidelityConfig?.angleTolerance ??
3914
+ DEFAULT_CURVE_FIDELITY.angleTolerance;
3915
+ return `cf:${distanceTolerance.toFixed(4)},${angleTolerance.toFixed(4)}`;
3916
+ })();
3844
3917
  const enabled = this.geometryOptimizationOptions?.enabled ??
3845
3918
  DEFAULT_OPTIMIZATION_CONFIG.enabled;
3846
3919
  const areaThreshold = this.geometryOptimizationOptions?.areaThreshold ??
3847
3920
  DEFAULT_OPTIMIZATION_CONFIG.areaThreshold;
3848
- const colinearThreshold = this.geometryOptimizationOptions?.colinearThreshold ??
3849
- DEFAULT_OPTIMIZATION_CONFIG.colinearThreshold;
3850
- const minSegmentLength = this.geometryOptimizationOptions?.minSegmentLength ??
3851
- DEFAULT_OPTIMIZATION_CONFIG.minSegmentLength;
3852
3921
  // Use fixed precision to keep cache keys stable and avoid float noise
3853
3922
  return [
3854
- `cf:${distanceTolerance.toFixed(4)},${angleTolerance.toFixed(4)}`,
3855
- `opt:${enabled ? 1 : 0},${areaThreshold.toFixed(4)},${colinearThreshold.toFixed(6)},${minSegmentLength.toFixed(4)}`
3923
+ curveSignature,
3924
+ `opt:${enabled ? 1 : 0},${areaThreshold.toFixed(4)}`
3856
3925
  ].join('|');
3857
3926
  }
3858
3927
  // Build instanced geometry from glyph contours
@@ -3906,15 +3975,20 @@
3906
3975
  boundaryGroups = [[0]];
3907
3976
  }
3908
3977
  else {
3909
- // Check clustering cache (same text + glyph IDs = same overlap groups)
3978
+ // Check clustering cache (same text + glyph IDs + positions = same overlap groups)
3910
3979
  // Key must be font-specific; glyph ids/bounds differ between fonts
3980
+ // Positions must match since overlap detection depends on relative glyph placement
3911
3981
  const cacheKey = `${this.cacheKeyPrefix}_${cluster.text}`;
3912
3982
  const cached = this.clusteringCache.get(cacheKey);
3913
3983
  let isValid = false;
3914
3984
  if (cached && cached.glyphIds.length === cluster.glyphs.length) {
3915
3985
  isValid = true;
3916
3986
  for (let i = 0; i < cluster.glyphs.length; i++) {
3917
- if (cached.glyphIds[i] !== cluster.glyphs[i].g) {
3987
+ const glyph = cluster.glyphs[i];
3988
+ const cachedPos = cached.positions[i];
3989
+ if (cached.glyphIds[i] !== glyph.g ||
3990
+ cachedPos.x !== (glyph.x ?? 0) ||
3991
+ cachedPos.y !== (glyph.y ?? 0)) {
3918
3992
  isValid = false;
3919
3993
  break;
3920
3994
  }
@@ -3928,17 +4002,52 @@
3928
4002
  boundaryGroups = this.clusterer.cluster(clusterGlyphContours, relativePositions);
3929
4003
  this.clusteringCache.set(cacheKey, {
3930
4004
  glyphIds: cluster.glyphs.map((g) => g.g),
4005
+ positions: cluster.glyphs.map((g) => ({
4006
+ x: g.x ?? 0,
4007
+ y: g.y ?? 0
4008
+ })),
3931
4009
  groups: boundaryGroups
3932
4010
  });
3933
4011
  }
3934
4012
  }
3935
- const clusterHasColoredGlyphs = coloredTextIndices &&
3936
- cluster.glyphs.some((g) => coloredTextIndices.has(g.absoluteTextIndex));
3937
- // Use glyph-level caching when separateGlyphs is set or when cluster contains colored text
3938
- const forceSeparate = separateGlyphs || clusterHasColoredGlyphs;
4013
+ // Only force separate tessellation when explicitly requested via separateGlyphs
4014
+ const forceSeparate = separateGlyphs;
4015
+ // Split boundary groups so colored and non-colored glyphs don't merge together
4016
+ // This preserves overlap removal within each color class while keeping
4017
+ // geometry separate for accurate vertex coloring
4018
+ let finalGroups = boundaryGroups;
4019
+ if (coloredTextIndices && coloredTextIndices.size > 0) {
4020
+ finalGroups = [];
4021
+ for (const group of boundaryGroups) {
4022
+ if (group.length <= 1) {
4023
+ finalGroups.push(group);
4024
+ }
4025
+ else {
4026
+ // Split group into colored and non-colored sub-groups
4027
+ const coloredIndices = [];
4028
+ const nonColoredIndices = [];
4029
+ for (const idx of group) {
4030
+ const glyph = cluster.glyphs[idx];
4031
+ if (coloredTextIndices.has(glyph.absoluteTextIndex)) {
4032
+ coloredIndices.push(idx);
4033
+ }
4034
+ else {
4035
+ nonColoredIndices.push(idx);
4036
+ }
4037
+ }
4038
+ // Add non-empty sub-groups
4039
+ if (coloredIndices.length > 0) {
4040
+ finalGroups.push(coloredIndices);
4041
+ }
4042
+ if (nonColoredIndices.length > 0) {
4043
+ finalGroups.push(nonColoredIndices);
4044
+ }
4045
+ }
4046
+ }
4047
+ }
3939
4048
  // Iterate over the geometric groups identified by BoundaryClusterer
3940
4049
  // logical groups (words) split into geometric sub-groups
3941
- for (const groupIndices of boundaryGroups) {
4050
+ for (const groupIndices of finalGroups) {
3942
4051
  const isOverlappingGroup = groupIndices.length > 1;
3943
4052
  const shouldCluster = isOverlappingGroup && !forceSeparate;
3944
4053
  if (shouldCluster) {
@@ -5369,7 +5478,13 @@
5369
5478
  this.geometryBuilder = new GlyphGeometryBuilder(globalGlyphCache, this.loadedFont);
5370
5479
  this.geometryBuilder.setFontId(this.currentFontId);
5371
5480
  }
5372
- this.geometryBuilder.setCurveFidelityConfig(options.curveFidelity);
5481
+ // Curve flattening: either use fixed-step De Casteljau (`curveSteps`) OR
5482
+ // adaptive AGG-style tolerances (`curveFidelity`)
5483
+ const useCurveSteps = options.curveSteps !== undefined &&
5484
+ options.curveSteps !== null &&
5485
+ options.curveSteps > 0;
5486
+ this.geometryBuilder.setCurveSteps(options.curveSteps);
5487
+ this.geometryBuilder.setCurveFidelityConfig(useCurveSteps ? undefined : options.curveFidelity);
5373
5488
  this.geometryBuilder.setGeometryOptimization(options.geometryOptimization);
5374
5489
  this.loadedFont.font.setScale(this.loadedFont.upem, this.loadedFont.upem);
5375
5490
  if (!this.textShaper) {
@@ -5599,6 +5714,7 @@
5599
5714
  else {
5600
5715
  lineGroups.set(glyph.lineIndex, [glyph]);
5601
5716
  }
5717
+ // Color vertices owned by this glyph
5602
5718
  for (let v = 0; v < glyph.vertexCount; v++) {
5603
5719
  const vertexIndex = (glyph.vertexStart + v) * 3;
5604
5720
  if (vertexIndex >= 0 && vertexIndex < colors.length) {
@@ -5640,6 +5756,7 @@
5640
5756
  else {
5641
5757
  lineGroups.set(glyph.lineIndex, [glyph]);
5642
5758
  }
5759
+ // Color vertices owned by this glyph
5643
5760
  for (let v = 0; v < glyph.vertexCount; v++) {
5644
5761
  const vertexIndex = (glyph.vertexStart + v) * 3;
5645
5762
  if (vertexIndex >= 0 && vertexIndex < colors.length) {
@@ -5740,7 +5857,6 @@
5740
5857
  trianglesGenerated,
5741
5858
  verticesGenerated,
5742
5859
  pointsRemovedByVisvalingam: optimizationStats.pointsRemovedByVisvalingam,
5743
- pointsRemovedByColinear: optimizationStats.pointsRemovedByColinear,
5744
5860
  originalPointCount: optimizationStats.originalPointCount
5745
5861
  },
5746
5862
  query: (() => {