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.d.ts
CHANGED
|
@@ -236,7 +236,6 @@ interface TextGeometryInfo {
|
|
|
236
236
|
trianglesGenerated: number;
|
|
237
237
|
verticesGenerated: number;
|
|
238
238
|
pointsRemovedByVisvalingam: number;
|
|
239
|
-
pointsRemovedByColinear: number;
|
|
240
239
|
originalPointCount: number;
|
|
241
240
|
} & Partial<CacheStats & {
|
|
242
241
|
hitRate: number;
|
|
@@ -300,6 +299,7 @@ interface TextOptions {
|
|
|
300
299
|
};
|
|
301
300
|
maxTextLength?: number;
|
|
302
301
|
removeOverlaps?: boolean;
|
|
302
|
+
curveSteps?: number;
|
|
303
303
|
curveFidelity?: CurveFidelityConfig;
|
|
304
304
|
geometryOptimization?: GeometryOptimizationOptions;
|
|
305
305
|
layout?: LayoutOptions;
|
|
@@ -315,8 +315,6 @@ interface CurveFidelityConfig {
|
|
|
315
315
|
interface GeometryOptimizationOptions {
|
|
316
316
|
enabled?: boolean;
|
|
317
317
|
areaThreshold?: number;
|
|
318
|
-
colinearThreshold?: number;
|
|
319
|
-
minSegmentLength?: number;
|
|
320
318
|
}
|
|
321
319
|
interface LayoutOptions {
|
|
322
320
|
width?: number;
|
package/dist/index.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
|
|
@@ -195,7 +195,7 @@ var FitnessClass;
|
|
|
195
195
|
FitnessClass[FitnessClass["DECENT"] = 2] = "DECENT";
|
|
196
196
|
FitnessClass[FitnessClass["TIGHT"] = 3] = "TIGHT"; // lines shrinking 0.5 to 1.0 of their shrinkability
|
|
197
197
|
})(FitnessClass || (FitnessClass = {}));
|
|
198
|
-
//
|
|
198
|
+
// Active node management with Map for O(1) lookup by (position, fitness)
|
|
199
199
|
class ActiveNodeList {
|
|
200
200
|
constructor() {
|
|
201
201
|
this.nodesByKey = new Map();
|
|
@@ -226,7 +226,6 @@ class ActiveNodeList {
|
|
|
226
226
|
this.nodesByKey.set(key, node);
|
|
227
227
|
return true;
|
|
228
228
|
}
|
|
229
|
-
// Swap-and-pop removal
|
|
230
229
|
deactivate(node) {
|
|
231
230
|
if (!node.active)
|
|
232
231
|
return;
|
|
@@ -369,36 +368,66 @@ class LineBreak {
|
|
|
369
368
|
const code = char.codePointAt(0);
|
|
370
369
|
if (code === undefined)
|
|
371
370
|
return false;
|
|
372
|
-
return ((code >= 0x4e00 && code <= 0x9fff) ||
|
|
373
|
-
(code >= 0x3400 && code <= 0x4dbf) ||
|
|
374
|
-
(code >= 0x20000 && code <= 0x2a6df) ||
|
|
375
|
-
(code >= 0x2a700 && code <= 0x2b73f) ||
|
|
376
|
-
(code >= 0x2b740 && code <= 0x2b81f) ||
|
|
377
|
-
(code >= 0x2b820 && code <= 0x2ceaf) ||
|
|
378
|
-
(code >= 0xf900 && code <= 0xfaff) ||
|
|
379
|
-
(code >= 0x3040 && code <= 0x309f) ||
|
|
380
|
-
(code >= 0x30a0 && code <= 0x30ff) ||
|
|
381
|
-
(code >= 0xac00 && code <= 0xd7af) ||
|
|
382
|
-
(code >= 0x1100 && code <= 0x11ff) ||
|
|
383
|
-
(code >= 0x3130 && code <= 0x318f) ||
|
|
384
|
-
(code >= 0xa960 && code <= 0xa97f) ||
|
|
385
|
-
(code >= 0xd7b0 && code <= 0xd7ff) ||
|
|
386
|
-
(code >= 0xffa0 && code <= 0xffdc)
|
|
387
|
-
|
|
371
|
+
return ((code >= 0x4e00 && code <= 0x9fff) || // CJK Unified Ideographs
|
|
372
|
+
(code >= 0x3400 && code <= 0x4dbf) || // CJK Extension A
|
|
373
|
+
(code >= 0x20000 && code <= 0x2a6df) || // CJK Extension B
|
|
374
|
+
(code >= 0x2a700 && code <= 0x2b73f) || // CJK Extension C
|
|
375
|
+
(code >= 0x2b740 && code <= 0x2b81f) || // CJK Extension D
|
|
376
|
+
(code >= 0x2b820 && code <= 0x2ceaf) || // CJK Extension E
|
|
377
|
+
(code >= 0xf900 && code <= 0xfaff) || // CJK Compatibility Ideographs
|
|
378
|
+
(code >= 0x3040 && code <= 0x309f) || // Hiragana
|
|
379
|
+
(code >= 0x30a0 && code <= 0x30ff) || // Katakana
|
|
380
|
+
(code >= 0xac00 && code <= 0xd7af) || // Hangul Syllables
|
|
381
|
+
(code >= 0x1100 && code <= 0x11ff) || // Hangul Jamo
|
|
382
|
+
(code >= 0x3130 && code <= 0x318f) || // Hangul Compatibility Jamo
|
|
383
|
+
(code >= 0xa960 && code <= 0xa97f) || // Hangul Jamo Extended-A
|
|
384
|
+
(code >= 0xd7b0 && code <= 0xd7ff) || // Hangul Jamo Extended-B
|
|
385
|
+
(code >= 0xffa0 && code <= 0xffdc) // Halfwidth Hangul
|
|
386
|
+
);
|
|
387
|
+
}
|
|
388
|
+
// Closing punctuation - no line break before (UAX #14 CL, JIS X 4051)
|
|
388
389
|
static isCJClosingPunctuation(char) {
|
|
389
390
|
const code = char.charCodeAt(0);
|
|
390
|
-
return (code === 0x3001 ||
|
|
391
|
-
code ===
|
|
392
|
-
code ===
|
|
393
|
-
code ===
|
|
394
|
-
code ===
|
|
395
|
-
code ===
|
|
396
|
-
|
|
391
|
+
return (code === 0x3001 || // 、
|
|
392
|
+
code === 0x3002 || // 。
|
|
393
|
+
code === 0xff0c || // ,
|
|
394
|
+
code === 0xff0e || // .
|
|
395
|
+
code === 0xff1a || // :
|
|
396
|
+
code === 0xff1b || // ;
|
|
397
|
+
code === 0xff01 || // !
|
|
398
|
+
code === 0xff1f || // ?
|
|
399
|
+
code === 0xff09 || // )
|
|
400
|
+
code === 0x3011 || // 】
|
|
401
|
+
code === 0xff5d || // }
|
|
402
|
+
code === 0x300d || // 」
|
|
403
|
+
code === 0x300f || // 』
|
|
404
|
+
code === 0x3009 || // 〉
|
|
405
|
+
code === 0x300b || // 》
|
|
406
|
+
code === 0x3015 || // 〕
|
|
407
|
+
code === 0x3017 || // 〗
|
|
408
|
+
code === 0x3019 || // 〙
|
|
409
|
+
code === 0x301b || // 〛
|
|
410
|
+
code === 0x30fc || // ー
|
|
411
|
+
code === 0x2014 || // —
|
|
412
|
+
code === 0x2026 || // …
|
|
413
|
+
code === 0x2025 // ‥
|
|
414
|
+
);
|
|
415
|
+
}
|
|
416
|
+
// Opening punctuation - no line break after (UAX #14 OP, JIS X 4051)
|
|
397
417
|
static isCJOpeningPunctuation(char) {
|
|
398
418
|
const code = char.charCodeAt(0);
|
|
399
|
-
return (code === 0xff08 ||
|
|
400
|
-
code ===
|
|
401
|
-
code ===
|
|
419
|
+
return (code === 0xff08 || // (
|
|
420
|
+
code === 0x3010 || // 【
|
|
421
|
+
code === 0xff5b || // {
|
|
422
|
+
code === 0x300c || // 「
|
|
423
|
+
code === 0x300e || // 『
|
|
424
|
+
code === 0x3008 || // 〈
|
|
425
|
+
code === 0x300a || // 《
|
|
426
|
+
code === 0x3014 || // 〔
|
|
427
|
+
code === 0x3016 || // 〖
|
|
428
|
+
code === 0x3018 || // 〘
|
|
429
|
+
code === 0x301a // 〚
|
|
430
|
+
);
|
|
402
431
|
}
|
|
403
432
|
static isCJPunctuation(char) {
|
|
404
433
|
return this.isCJClosingPunctuation(char) || this.isCJOpeningPunctuation(char);
|
|
@@ -3039,15 +3068,12 @@ class MinHeap {
|
|
|
3039
3068
|
|
|
3040
3069
|
const DEFAULT_OPTIMIZATION_CONFIG = {
|
|
3041
3070
|
enabled: true,
|
|
3042
|
-
areaThreshold: 1.0
|
|
3043
|
-
colinearThreshold: 0.0087, // ~0.5 degrees in radians
|
|
3044
|
-
minSegmentLength: 10
|
|
3071
|
+
areaThreshold: 1.0 // Remove triangles smaller than 1 square font unit
|
|
3045
3072
|
};
|
|
3046
3073
|
class PathOptimizer {
|
|
3047
3074
|
constructor(config) {
|
|
3048
3075
|
this.stats = {
|
|
3049
3076
|
pointsRemovedByVisvalingam: 0,
|
|
3050
|
-
pointsRemovedByColinear: 0,
|
|
3051
3077
|
originalPointCount: 0
|
|
3052
3078
|
};
|
|
3053
3079
|
this.config = config;
|
|
@@ -3056,7 +3082,10 @@ class PathOptimizer {
|
|
|
3056
3082
|
this.config = config;
|
|
3057
3083
|
}
|
|
3058
3084
|
optimizePath(path) {
|
|
3059
|
-
if (
|
|
3085
|
+
if (path.points.length <= 2) {
|
|
3086
|
+
return path;
|
|
3087
|
+
}
|
|
3088
|
+
if (!this.config.enabled) {
|
|
3060
3089
|
return path;
|
|
3061
3090
|
}
|
|
3062
3091
|
this.stats.originalPointCount += path.points.length;
|
|
@@ -3066,11 +3095,9 @@ class PathOptimizer {
|
|
|
3066
3095
|
if (points.length < 5) {
|
|
3067
3096
|
return path;
|
|
3068
3097
|
}
|
|
3069
|
-
let optimized =
|
|
3070
|
-
|
|
3071
|
-
|
|
3072
|
-
}
|
|
3073
|
-
optimized = this.removeColinearPoints(optimized, this.config.colinearThreshold);
|
|
3098
|
+
let optimized = points;
|
|
3099
|
+
// Visvalingam-Whyatt simplification
|
|
3100
|
+
optimized = this.simplifyPathVW(optimized, this.config.areaThreshold);
|
|
3074
3101
|
if (optimized.length < 3) {
|
|
3075
3102
|
return path;
|
|
3076
3103
|
}
|
|
@@ -3107,17 +3134,6 @@ class PathOptimizer {
|
|
|
3107
3134
|
if (!p || p.area > areaThreshold) {
|
|
3108
3135
|
break;
|
|
3109
3136
|
}
|
|
3110
|
-
if (this.config.minSegmentLength > 0 && p.prev && p.next) {
|
|
3111
|
-
const prevPoint = points[p.prev.index];
|
|
3112
|
-
const currentPoint = points[p.index];
|
|
3113
|
-
const nextPoint = points[p.next.index];
|
|
3114
|
-
const len1 = prevPoint.distanceTo(currentPoint);
|
|
3115
|
-
const len2 = currentPoint.distanceTo(nextPoint);
|
|
3116
|
-
if (len1 < this.config.minSegmentLength ||
|
|
3117
|
-
len2 < this.config.minSegmentLength) {
|
|
3118
|
-
continue;
|
|
3119
|
-
}
|
|
3120
|
-
}
|
|
3121
3137
|
if (p.prev)
|
|
3122
3138
|
p.prev.next = p.next;
|
|
3123
3139
|
if (p.next)
|
|
@@ -3142,32 +3158,6 @@ class PathOptimizer {
|
|
|
3142
3158
|
this.stats.pointsRemovedByVisvalingam += pointsRemoved;
|
|
3143
3159
|
return simplifiedPoints;
|
|
3144
3160
|
}
|
|
3145
|
-
removeColinearPoints(points, threshold) {
|
|
3146
|
-
if (points.length <= 2)
|
|
3147
|
-
return points;
|
|
3148
|
-
const result = [points[0]];
|
|
3149
|
-
for (let i = 1; i < points.length - 1; i++) {
|
|
3150
|
-
const prev = points[i - 1];
|
|
3151
|
-
const current = points[i];
|
|
3152
|
-
const next = points[i + 1];
|
|
3153
|
-
const v1x = current.x - prev.x;
|
|
3154
|
-
const v1y = current.y - prev.y;
|
|
3155
|
-
const v2x = next.x - current.x;
|
|
3156
|
-
const v2y = next.y - current.y;
|
|
3157
|
-
const angle = Math.abs(Math.atan2(v1x * v2y - v1y * v2x, v1x * v2x + v1y * v2y));
|
|
3158
|
-
const v1LenSq = v1x * v1x + v1y * v1y;
|
|
3159
|
-
const v2LenSq = v2x * v2x + v2y * v2y;
|
|
3160
|
-
const minLenSq = this.config.minSegmentLength * this.config.minSegmentLength;
|
|
3161
|
-
if (angle > threshold || v1LenSq < minLenSq || v2LenSq < minLenSq) {
|
|
3162
|
-
result.push(current);
|
|
3163
|
-
}
|
|
3164
|
-
else {
|
|
3165
|
-
this.stats.pointsRemovedByColinear++;
|
|
3166
|
-
}
|
|
3167
|
-
}
|
|
3168
|
-
result.push(points[points.length - 1]);
|
|
3169
|
-
return result;
|
|
3170
|
-
}
|
|
3171
3161
|
// Shoelace formula
|
|
3172
3162
|
calculateTriangleArea(p1, p2, p3) {
|
|
3173
3163
|
return Math.abs((p1.x * (p2.y - p3.y) + p2.x * (p3.y - p1.y) + p3.x * (p1.y - p2.y)) / 2);
|
|
@@ -3178,7 +3168,6 @@ class PathOptimizer {
|
|
|
3178
3168
|
resetStats() {
|
|
3179
3169
|
this.stats = {
|
|
3180
3170
|
pointsRemovedByVisvalingam: 0,
|
|
3181
|
-
pointsRemovedByColinear: 0,
|
|
3182
3171
|
originalPointCount: 0
|
|
3183
3172
|
};
|
|
3184
3173
|
}
|
|
@@ -3229,6 +3218,7 @@ const COLLINEARITY_EPSILON = 1e-6;
|
|
|
3229
3218
|
const RECURSION_LIMIT = 16;
|
|
3230
3219
|
class Polygonizer {
|
|
3231
3220
|
constructor(curveFidelityConfig) {
|
|
3221
|
+
this.curveSteps = null;
|
|
3232
3222
|
this.curveFidelityConfig = {
|
|
3233
3223
|
...DEFAULT_CURVE_FIDELITY,
|
|
3234
3224
|
...curveFidelityConfig
|
|
@@ -3240,18 +3230,77 @@ class Polygonizer {
|
|
|
3240
3230
|
...curveFidelityConfig
|
|
3241
3231
|
};
|
|
3242
3232
|
}
|
|
3233
|
+
// Fixed-step subdivision; overrides adaptive curveFidelity when set
|
|
3234
|
+
setCurveSteps(curveSteps) {
|
|
3235
|
+
if (curveSteps === undefined || curveSteps === null) {
|
|
3236
|
+
this.curveSteps = null;
|
|
3237
|
+
return;
|
|
3238
|
+
}
|
|
3239
|
+
if (!Number.isFinite(curveSteps)) {
|
|
3240
|
+
this.curveSteps = null;
|
|
3241
|
+
return;
|
|
3242
|
+
}
|
|
3243
|
+
const stepsInt = Math.round(curveSteps);
|
|
3244
|
+
this.curveSteps = stepsInt >= 1 ? stepsInt : null;
|
|
3245
|
+
}
|
|
3243
3246
|
polygonizeQuadratic(start, control, end) {
|
|
3247
|
+
if (this.curveSteps !== null) {
|
|
3248
|
+
return this.polygonizeQuadraticFixedSteps(start, control, end, this.curveSteps);
|
|
3249
|
+
}
|
|
3244
3250
|
const points = [];
|
|
3245
3251
|
this.recursiveQuadratic(start.x, start.y, control.x, control.y, end.x, end.y, points);
|
|
3246
3252
|
this.addPoint(end.x, end.y, points);
|
|
3247
3253
|
return points;
|
|
3248
3254
|
}
|
|
3249
3255
|
polygonizeCubic(start, control1, control2, end) {
|
|
3256
|
+
if (this.curveSteps !== null) {
|
|
3257
|
+
return this.polygonizeCubicFixedSteps(start, control1, control2, end, this.curveSteps);
|
|
3258
|
+
}
|
|
3250
3259
|
const points = [];
|
|
3251
3260
|
this.recursiveCubic(start.x, start.y, control1.x, control1.y, control2.x, control2.y, end.x, end.y, points);
|
|
3252
3261
|
this.addPoint(end.x, end.y, points);
|
|
3253
3262
|
return points;
|
|
3254
3263
|
}
|
|
3264
|
+
lerp(a, b, t) {
|
|
3265
|
+
return a + (b - a) * t;
|
|
3266
|
+
}
|
|
3267
|
+
polygonizeQuadraticFixedSteps(start, control, end, steps) {
|
|
3268
|
+
const points = [];
|
|
3269
|
+
// Emit intermediate points; caller already has start
|
|
3270
|
+
for (let i = 1; i <= steps; i++) {
|
|
3271
|
+
const t = i / steps;
|
|
3272
|
+
const x12 = this.lerp(start.x, control.x, t);
|
|
3273
|
+
const y12 = this.lerp(start.y, control.y, t);
|
|
3274
|
+
const x23 = this.lerp(control.x, end.x, t);
|
|
3275
|
+
const y23 = this.lerp(control.y, end.y, t);
|
|
3276
|
+
const x = this.lerp(x12, x23, t);
|
|
3277
|
+
const y = this.lerp(y12, y23, t);
|
|
3278
|
+
this.addPoint(x, y, points);
|
|
3279
|
+
}
|
|
3280
|
+
return points;
|
|
3281
|
+
}
|
|
3282
|
+
polygonizeCubicFixedSteps(start, control1, control2, end, steps) {
|
|
3283
|
+
const points = [];
|
|
3284
|
+
// Emit intermediate points; caller already has start
|
|
3285
|
+
for (let i = 1; i <= steps; i++) {
|
|
3286
|
+
const t = i / steps;
|
|
3287
|
+
// De Casteljau
|
|
3288
|
+
const x12 = this.lerp(start.x, control1.x, t);
|
|
3289
|
+
const y12 = this.lerp(start.y, control1.y, t);
|
|
3290
|
+
const x23 = this.lerp(control1.x, control2.x, t);
|
|
3291
|
+
const y23 = this.lerp(control1.y, control2.y, t);
|
|
3292
|
+
const x34 = this.lerp(control2.x, end.x, t);
|
|
3293
|
+
const y34 = this.lerp(control2.y, end.y, t);
|
|
3294
|
+
const x123 = this.lerp(x12, x23, t);
|
|
3295
|
+
const y123 = this.lerp(y12, y23, t);
|
|
3296
|
+
const x234 = this.lerp(x23, x34, t);
|
|
3297
|
+
const y234 = this.lerp(y23, y34, t);
|
|
3298
|
+
const x = this.lerp(x123, x234, t);
|
|
3299
|
+
const y = this.lerp(y123, y234, t);
|
|
3300
|
+
this.addPoint(x, y, points);
|
|
3301
|
+
}
|
|
3302
|
+
return points;
|
|
3303
|
+
}
|
|
3255
3304
|
recursiveQuadratic(x1, y1, x2, y2, x3, y3, points, level = 0) {
|
|
3256
3305
|
if (level > RECURSION_LIMIT)
|
|
3257
3306
|
return;
|
|
@@ -3278,8 +3327,8 @@ class Polygonizer {
|
|
|
3278
3327
|
const angleTolerance = this.curveFidelityConfig.angleTolerance ??
|
|
3279
3328
|
DEFAULT_CURVE_FIDELITY.angleTolerance;
|
|
3280
3329
|
if (angleTolerance > 0) {
|
|
3281
|
-
// Angle between segments (p1->p2) and (p2->p3)
|
|
3282
|
-
//
|
|
3330
|
+
// Angle between segments (p1->p2) and (p2->p3)
|
|
3331
|
+
// atan2(cross, dot) avoids computing 2 separate atan2() values
|
|
3283
3332
|
const v1x = x2 - x1;
|
|
3284
3333
|
const v1y = y2 - y1;
|
|
3285
3334
|
const v2x = x3 - x2;
|
|
@@ -3664,6 +3713,9 @@ class GlyphContourCollector {
|
|
|
3664
3713
|
setCurveFidelityConfig(config) {
|
|
3665
3714
|
this.polygonizer.setCurveFidelityConfig(config);
|
|
3666
3715
|
}
|
|
3716
|
+
setCurveSteps(curveSteps) {
|
|
3717
|
+
this.polygonizer.setCurveSteps(curveSteps);
|
|
3718
|
+
}
|
|
3667
3719
|
setGeometryOptimization(options) {
|
|
3668
3720
|
this.pathOptimizer.setConfig({
|
|
3669
3721
|
...DEFAULT_OPTIMIZATION_CONFIG,
|
|
@@ -3817,6 +3869,21 @@ class GlyphGeometryBuilder {
|
|
|
3817
3869
|
this.collector.setCurveFidelityConfig(config);
|
|
3818
3870
|
this.updateCacheKeyPrefix();
|
|
3819
3871
|
}
|
|
3872
|
+
setCurveSteps(curveSteps) {
|
|
3873
|
+
// Normalize: unset for undefined/null/non-finite/<=0
|
|
3874
|
+
if (curveSteps === undefined || curveSteps === null) {
|
|
3875
|
+
this.curveSteps = undefined;
|
|
3876
|
+
}
|
|
3877
|
+
else if (!Number.isFinite(curveSteps)) {
|
|
3878
|
+
this.curveSteps = undefined;
|
|
3879
|
+
}
|
|
3880
|
+
else {
|
|
3881
|
+
const stepsInt = Math.round(curveSteps);
|
|
3882
|
+
this.curveSteps = stepsInt >= 1 ? stepsInt : undefined;
|
|
3883
|
+
}
|
|
3884
|
+
this.collector.setCurveSteps(this.curveSteps);
|
|
3885
|
+
this.updateCacheKeyPrefix();
|
|
3886
|
+
}
|
|
3820
3887
|
setGeometryOptimization(options) {
|
|
3821
3888
|
this.geometryOptimizationOptions = options;
|
|
3822
3889
|
this.collector.setGeometryOptimization(options);
|
|
@@ -3830,22 +3897,24 @@ class GlyphGeometryBuilder {
|
|
|
3830
3897
|
this.cacheKeyPrefix = `${this.fontId}__${this.getGeometryConfigSignature()}`;
|
|
3831
3898
|
}
|
|
3832
3899
|
getGeometryConfigSignature() {
|
|
3833
|
-
const
|
|
3834
|
-
|
|
3835
|
-
|
|
3836
|
-
|
|
3900
|
+
const curveSignature = (() => {
|
|
3901
|
+
if (this.curveSteps !== undefined) {
|
|
3902
|
+
return `cf:steps:${this.curveSteps}`;
|
|
3903
|
+
}
|
|
3904
|
+
const distanceTolerance = this.curveFidelityConfig?.distanceTolerance ??
|
|
3905
|
+
DEFAULT_CURVE_FIDELITY.distanceTolerance;
|
|
3906
|
+
const angleTolerance = this.curveFidelityConfig?.angleTolerance ??
|
|
3907
|
+
DEFAULT_CURVE_FIDELITY.angleTolerance;
|
|
3908
|
+
return `cf:${distanceTolerance.toFixed(4)},${angleTolerance.toFixed(4)}`;
|
|
3909
|
+
})();
|
|
3837
3910
|
const enabled = this.geometryOptimizationOptions?.enabled ??
|
|
3838
3911
|
DEFAULT_OPTIMIZATION_CONFIG.enabled;
|
|
3839
3912
|
const areaThreshold = this.geometryOptimizationOptions?.areaThreshold ??
|
|
3840
3913
|
DEFAULT_OPTIMIZATION_CONFIG.areaThreshold;
|
|
3841
|
-
const colinearThreshold = this.geometryOptimizationOptions?.colinearThreshold ??
|
|
3842
|
-
DEFAULT_OPTIMIZATION_CONFIG.colinearThreshold;
|
|
3843
|
-
const minSegmentLength = this.geometryOptimizationOptions?.minSegmentLength ??
|
|
3844
|
-
DEFAULT_OPTIMIZATION_CONFIG.minSegmentLength;
|
|
3845
3914
|
// Use fixed precision to keep cache keys stable and avoid float noise
|
|
3846
3915
|
return [
|
|
3847
|
-
|
|
3848
|
-
`opt:${enabled ? 1 : 0},${areaThreshold.toFixed(4)}
|
|
3916
|
+
curveSignature,
|
|
3917
|
+
`opt:${enabled ? 1 : 0},${areaThreshold.toFixed(4)}`
|
|
3849
3918
|
].join('|');
|
|
3850
3919
|
}
|
|
3851
3920
|
// Build instanced geometry from glyph contours
|
|
@@ -3899,15 +3968,20 @@ class GlyphGeometryBuilder {
|
|
|
3899
3968
|
boundaryGroups = [[0]];
|
|
3900
3969
|
}
|
|
3901
3970
|
else {
|
|
3902
|
-
// Check clustering cache (same text + glyph IDs = same overlap groups)
|
|
3971
|
+
// Check clustering cache (same text + glyph IDs + positions = same overlap groups)
|
|
3903
3972
|
// Key must be font-specific; glyph ids/bounds differ between fonts
|
|
3973
|
+
// Positions must match since overlap detection depends on relative glyph placement
|
|
3904
3974
|
const cacheKey = `${this.cacheKeyPrefix}_${cluster.text}`;
|
|
3905
3975
|
const cached = this.clusteringCache.get(cacheKey);
|
|
3906
3976
|
let isValid = false;
|
|
3907
3977
|
if (cached && cached.glyphIds.length === cluster.glyphs.length) {
|
|
3908
3978
|
isValid = true;
|
|
3909
3979
|
for (let i = 0; i < cluster.glyphs.length; i++) {
|
|
3910
|
-
|
|
3980
|
+
const glyph = cluster.glyphs[i];
|
|
3981
|
+
const cachedPos = cached.positions[i];
|
|
3982
|
+
if (cached.glyphIds[i] !== glyph.g ||
|
|
3983
|
+
cachedPos.x !== (glyph.x ?? 0) ||
|
|
3984
|
+
cachedPos.y !== (glyph.y ?? 0)) {
|
|
3911
3985
|
isValid = false;
|
|
3912
3986
|
break;
|
|
3913
3987
|
}
|
|
@@ -3921,17 +3995,52 @@ class GlyphGeometryBuilder {
|
|
|
3921
3995
|
boundaryGroups = this.clusterer.cluster(clusterGlyphContours, relativePositions);
|
|
3922
3996
|
this.clusteringCache.set(cacheKey, {
|
|
3923
3997
|
glyphIds: cluster.glyphs.map((g) => g.g),
|
|
3998
|
+
positions: cluster.glyphs.map((g) => ({
|
|
3999
|
+
x: g.x ?? 0,
|
|
4000
|
+
y: g.y ?? 0
|
|
4001
|
+
})),
|
|
3924
4002
|
groups: boundaryGroups
|
|
3925
4003
|
});
|
|
3926
4004
|
}
|
|
3927
4005
|
}
|
|
3928
|
-
|
|
3929
|
-
|
|
3930
|
-
//
|
|
3931
|
-
|
|
4006
|
+
// Only force separate tessellation when explicitly requested via separateGlyphs
|
|
4007
|
+
const forceSeparate = separateGlyphs;
|
|
4008
|
+
// Split boundary groups so colored and non-colored glyphs don't merge together
|
|
4009
|
+
// This preserves overlap removal within each color class while keeping
|
|
4010
|
+
// geometry separate for accurate vertex coloring
|
|
4011
|
+
let finalGroups = boundaryGroups;
|
|
4012
|
+
if (coloredTextIndices && coloredTextIndices.size > 0) {
|
|
4013
|
+
finalGroups = [];
|
|
4014
|
+
for (const group of boundaryGroups) {
|
|
4015
|
+
if (group.length <= 1) {
|
|
4016
|
+
finalGroups.push(group);
|
|
4017
|
+
}
|
|
4018
|
+
else {
|
|
4019
|
+
// Split group into colored and non-colored sub-groups
|
|
4020
|
+
const coloredIndices = [];
|
|
4021
|
+
const nonColoredIndices = [];
|
|
4022
|
+
for (const idx of group) {
|
|
4023
|
+
const glyph = cluster.glyphs[idx];
|
|
4024
|
+
if (coloredTextIndices.has(glyph.absoluteTextIndex)) {
|
|
4025
|
+
coloredIndices.push(idx);
|
|
4026
|
+
}
|
|
4027
|
+
else {
|
|
4028
|
+
nonColoredIndices.push(idx);
|
|
4029
|
+
}
|
|
4030
|
+
}
|
|
4031
|
+
// Add non-empty sub-groups
|
|
4032
|
+
if (coloredIndices.length > 0) {
|
|
4033
|
+
finalGroups.push(coloredIndices);
|
|
4034
|
+
}
|
|
4035
|
+
if (nonColoredIndices.length > 0) {
|
|
4036
|
+
finalGroups.push(nonColoredIndices);
|
|
4037
|
+
}
|
|
4038
|
+
}
|
|
4039
|
+
}
|
|
4040
|
+
}
|
|
3932
4041
|
// Iterate over the geometric groups identified by BoundaryClusterer
|
|
3933
4042
|
// logical groups (words) split into geometric sub-groups
|
|
3934
|
-
for (const groupIndices of
|
|
4043
|
+
for (const groupIndices of finalGroups) {
|
|
3935
4044
|
const isOverlappingGroup = groupIndices.length > 1;
|
|
3936
4045
|
const shouldCluster = isOverlappingGroup && !forceSeparate;
|
|
3937
4046
|
if (shouldCluster) {
|
|
@@ -5362,7 +5471,13 @@ class Text {
|
|
|
5362
5471
|
this.geometryBuilder = new GlyphGeometryBuilder(globalGlyphCache, this.loadedFont);
|
|
5363
5472
|
this.geometryBuilder.setFontId(this.currentFontId);
|
|
5364
5473
|
}
|
|
5365
|
-
|
|
5474
|
+
// Curve flattening: either use fixed-step De Casteljau (`curveSteps`) OR
|
|
5475
|
+
// adaptive AGG-style tolerances (`curveFidelity`)
|
|
5476
|
+
const useCurveSteps = options.curveSteps !== undefined &&
|
|
5477
|
+
options.curveSteps !== null &&
|
|
5478
|
+
options.curveSteps > 0;
|
|
5479
|
+
this.geometryBuilder.setCurveSteps(options.curveSteps);
|
|
5480
|
+
this.geometryBuilder.setCurveFidelityConfig(useCurveSteps ? undefined : options.curveFidelity);
|
|
5366
5481
|
this.geometryBuilder.setGeometryOptimization(options.geometryOptimization);
|
|
5367
5482
|
this.loadedFont.font.setScale(this.loadedFont.upem, this.loadedFont.upem);
|
|
5368
5483
|
if (!this.textShaper) {
|
|
@@ -5592,6 +5707,7 @@ class Text {
|
|
|
5592
5707
|
else {
|
|
5593
5708
|
lineGroups.set(glyph.lineIndex, [glyph]);
|
|
5594
5709
|
}
|
|
5710
|
+
// Color vertices owned by this glyph
|
|
5595
5711
|
for (let v = 0; v < glyph.vertexCount; v++) {
|
|
5596
5712
|
const vertexIndex = (glyph.vertexStart + v) * 3;
|
|
5597
5713
|
if (vertexIndex >= 0 && vertexIndex < colors.length) {
|
|
@@ -5633,6 +5749,7 @@ class Text {
|
|
|
5633
5749
|
else {
|
|
5634
5750
|
lineGroups.set(glyph.lineIndex, [glyph]);
|
|
5635
5751
|
}
|
|
5752
|
+
// Color vertices owned by this glyph
|
|
5636
5753
|
for (let v = 0; v < glyph.vertexCount; v++) {
|
|
5637
5754
|
const vertexIndex = (glyph.vertexStart + v) * 3;
|
|
5638
5755
|
if (vertexIndex >= 0 && vertexIndex < colors.length) {
|
|
@@ -5733,7 +5850,6 @@ class Text {
|
|
|
5733
5850
|
trianglesGenerated,
|
|
5734
5851
|
verticesGenerated,
|
|
5735
5852
|
pointsRemovedByVisvalingam: optimizationStats.pointsRemovedByVisvalingam,
|
|
5736
|
-
pointsRemovedByColinear: optimizationStats.pointsRemovedByColinear,
|
|
5737
5853
|
originalPointCount: optimizationStats.originalPointCount
|
|
5738
5854
|
},
|
|
5739
5855
|
query: (() => {
|