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/README.md +33 -39
- package/dist/index.cjs +214 -98
- package/dist/index.d.ts +1 -3
- package/dist/index.js +214 -98
- package/dist/index.min.cjs +434 -417
- package/dist/index.min.js +402 -385
- package/dist/index.umd.js +214 -98
- package/dist/index.umd.min.js +487 -470
- package/dist/three/react.d.ts +1 -3
- package/dist/types/core/cache/GlyphContourCollector.d.ts +1 -0
- package/dist/types/core/cache/GlyphGeometryBuilder.d.ts +2 -0
- package/dist/types/core/cache/sharedCaches.d.ts +4 -0
- package/dist/types/core/geometry/PathOptimizer.d.ts +0 -4
- package/dist/types/core/geometry/Polygonizer.d.ts +5 -0
- package/dist/types/core/types.d.ts +1 -3
- package/package.json +3 -3
package/dist/index.umd.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/*!
|
|
2
|
-
* three-text v0.
|
|
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
|
-
//
|
|
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 ||
|
|
396
|
-
code ===
|
|
397
|
-
code ===
|
|
398
|
-
code ===
|
|
399
|
-
code ===
|
|
400
|
-
code ===
|
|
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 ||
|
|
405
|
-
code ===
|
|
406
|
-
code ===
|
|
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
|
|
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 (
|
|
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 =
|
|
3077
|
-
|
|
3078
|
-
|
|
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
|
-
//
|
|
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
|
|
3841
|
-
|
|
3842
|
-
|
|
3843
|
-
|
|
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
|
-
|
|
3855
|
-
`opt:${enabled ? 1 : 0},${areaThreshold.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
|
-
|
|
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
|
-
|
|
3936
|
-
|
|
3937
|
-
//
|
|
3938
|
-
|
|
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
|
|
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
|
-
|
|
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: (() => {
|