three-text 0.2.4 → 0.2.6

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.2.4
2
+ * three-text v0.2.6
3
3
  * Copyright (C) 2025 Countertype LLC
4
4
  *
5
5
  * This program is free software: you can redistribute it and/or modify
@@ -27,7 +27,7 @@
27
27
  }
28
28
  return false;
29
29
  })();
30
- class DebugLogger {
30
+ class Logger {
31
31
  warn(message, ...args) {
32
32
  console.warn(message, ...args);
33
33
  }
@@ -38,7 +38,7 @@
38
38
  isLogEnabled && console.log(message, ...args);
39
39
  }
40
40
  }
41
- const debugLogger = new DebugLogger();
41
+ const logger = new Logger();
42
42
 
43
43
  class PerformanceLogger {
44
44
  constructor() {
@@ -64,7 +64,7 @@
64
64
  const endTime = performance.now();
65
65
  const startTime = this.activeTimers.get(name);
66
66
  if (startTime === undefined) {
67
- debugLogger.warn(`Performance timer "${name}" was not started`);
67
+ logger.warn(`Performance timer "${name}" was not started`);
68
68
  return null;
69
69
  }
70
70
  const duration = endTime - startTime;
@@ -567,7 +567,7 @@
567
567
  let useHyphenation = hyphenate;
568
568
  if (useHyphenation &&
569
569
  (!hyphenationPatterns || !hyphenationPatterns[language])) {
570
- debugLogger.warn(`Hyphenation patterns for ${language} not available`);
570
+ logger.warn(`Hyphenation patterns for ${language} not available`);
571
571
  useHyphenation = false;
572
572
  }
573
573
  // Calculate initial emergency stretch (TeX default: 0)
@@ -1182,22 +1182,47 @@
1182
1182
  }
1183
1183
  }
1184
1184
 
1185
+ // Convert feature objects to HarfBuzz comma-separated format
1186
+ function convertFontFeaturesToString(features) {
1187
+ if (!features || Object.keys(features).length === 0) {
1188
+ return undefined;
1189
+ }
1190
+ const featureStrings = [];
1191
+ for (const [tag, value] of Object.entries(features)) {
1192
+ if (!/^[a-zA-Z0-9]{4}$/.test(tag)) {
1193
+ logger.warn(`Invalid OpenType feature tag: "${tag}". Tags must be exactly 4 alphanumeric characters.`);
1194
+ continue;
1195
+ }
1196
+ if (value === false || value === 0) {
1197
+ featureStrings.push(`${tag}=0`);
1198
+ }
1199
+ else if (value === true || value === 1) {
1200
+ featureStrings.push(tag);
1201
+ }
1202
+ else if (typeof value === 'number' && value > 1) {
1203
+ featureStrings.push(`${tag}=${Math.floor(value)}`);
1204
+ }
1205
+ else {
1206
+ logger.warn(`Invalid value for feature "${tag}": ${value}. Expected boolean or positive number.`);
1207
+ }
1208
+ }
1209
+ return featureStrings.length > 0 ? featureStrings.join(',') : undefined;
1210
+ }
1211
+
1185
1212
  class TextMeasurer {
1186
1213
  static measureTextWidth(loadedFont, text, letterSpacing = 0) {
1187
1214
  const buffer = loadedFont.hb.createBuffer();
1188
1215
  buffer.addText(text);
1189
1216
  buffer.guessSegmentProperties();
1190
- loadedFont.hb.shape(loadedFont.font, buffer);
1217
+ const featuresString = convertFontFeaturesToString(loadedFont.fontFeatures);
1218
+ loadedFont.hb.shape(loadedFont.font, buffer, featuresString);
1191
1219
  const glyphInfos = buffer.json(loadedFont.font);
1192
1220
  const letterSpacingInFontUnits = letterSpacing * loadedFont.upem;
1193
1221
  // Calculate total advance width with letter spacing
1194
1222
  let totalWidth = 0;
1195
- glyphInfos.forEach((glyph, index) => {
1223
+ glyphInfos.forEach((glyph) => {
1196
1224
  totalWidth += glyph.ax;
1197
- // Spaces measured alone need letter spacing to match final rendering
1198
- const isLastChar = index === glyphInfos.length - 1;
1199
- const isSingleSpace = text === ' ' || text === ' ' || /^\s+$/.test(text);
1200
- if (letterSpacingInFontUnits !== 0 && (!isLastChar || isSingleSpace)) {
1225
+ if (letterSpacingInFontUnits !== 0) {
1201
1226
  totalWidth += letterSpacingInFontUnits;
1202
1227
  }
1203
1228
  });
@@ -1252,7 +1277,7 @@
1252
1277
  originalEnd: currentIndex + line.length - 1,
1253
1278
  xOffset: 0
1254
1279
  });
1255
- currentIndex += line.length + 1; // +1 for the newline character
1280
+ currentIndex += line.length + 1;
1256
1281
  }
1257
1282
  }
1258
1283
  return { lines };
@@ -1273,7 +1298,7 @@
1273
1298
  offset = width - planeBounds.max.x;
1274
1299
  }
1275
1300
  if (offset !== 0) {
1276
- // Translate vertices directly
1301
+ // Translate vertices
1277
1302
  for (let i = 0; i < vertices.length; i += 3) {
1278
1303
  vertices[i] += offset;
1279
1304
  }
@@ -1291,6 +1316,17 @@
1291
1316
  const FONT_SIGNATURE_TRUE_TYPE_COLLECTION = 0x74746366; // 'ttcf'
1292
1317
  const FONT_SIGNATURE_WOFF = 0x774f4646; // 'wOFF'
1293
1318
  const FONT_SIGNATURE_WOFF2 = 0x774f4632; // 'wOF2'
1319
+ // Table Tags
1320
+ const TABLE_TAG_HEAD = 0x68656164; // 'head'
1321
+ const TABLE_TAG_HHEA = 0x68686561; // 'hhea'
1322
+ const TABLE_TAG_OS2 = 0x4f532f32; // 'OS/2'
1323
+ const TABLE_TAG_FVAR = 0x66766172; // 'fvar'
1324
+ const TABLE_TAG_STAT = 0x53544154; // 'STAT'
1325
+ const TABLE_TAG_NAME = 0x6e616d65; // 'name'
1326
+ const TABLE_TAG_CFF = 0x43464620; // 'CFF '
1327
+ const TABLE_TAG_CFF2 = 0x43464632; // 'CFF2'
1328
+ const TABLE_TAG_GSUB = 0x47535542; // 'GSUB'
1329
+ const TABLE_TAG_GPOS = 0x47504f53; // 'GPOS'
1294
1330
 
1295
1331
  class FontMetadataExtractor {
1296
1332
  static extractMetadata(fontBuffer) {
@@ -1307,7 +1343,6 @@
1307
1343
  if (!validSignatures.includes(sfntVersion)) {
1308
1344
  throw new Error(`Invalid font format. Expected TrueType or OpenType, got signature: 0x${sfntVersion.toString(16)}`);
1309
1345
  }
1310
- const buffer = new Uint8Array(fontBuffer);
1311
1346
  const numTables = view.getUint16(4); // OpenType header - number of tables is at offset 4
1312
1347
  let isCFF = false;
1313
1348
  let headTableOffset = 0;
@@ -1317,30 +1352,28 @@
1317
1352
  let nameTableOffset = 0;
1318
1353
  let fvarTableOffset = 0;
1319
1354
  for (let i = 0; i < numTables; i++) {
1320
- const tag = new TextDecoder().decode(buffer.slice(12 + i * 16, 12 + i * 16 + 4));
1321
- if (tag === 'CFF ') {
1355
+ const offset = 12 + i * 16;
1356
+ const tag = view.getUint32(offset);
1357
+ if (tag === TABLE_TAG_CFF || tag === TABLE_TAG_CFF2) {
1322
1358
  isCFF = true;
1323
1359
  }
1324
- else if (tag === 'CFF2') {
1325
- isCFF = true;
1360
+ else if (tag === TABLE_TAG_HEAD) {
1361
+ headTableOffset = view.getUint32(offset + 8);
1326
1362
  }
1327
- if (tag === 'head') {
1328
- headTableOffset = view.getUint32(12 + i * 16 + 8);
1363
+ else if (tag === TABLE_TAG_HHEA) {
1364
+ hheaTableOffset = view.getUint32(offset + 8);
1329
1365
  }
1330
- if (tag === 'hhea') {
1331
- hheaTableOffset = view.getUint32(12 + i * 16 + 8);
1366
+ else if (tag === TABLE_TAG_OS2) {
1367
+ os2TableOffset = view.getUint32(offset + 8);
1332
1368
  }
1333
- if (tag === 'OS/2') {
1334
- os2TableOffset = view.getUint32(12 + i * 16 + 8);
1369
+ else if (tag === TABLE_TAG_FVAR) {
1370
+ fvarTableOffset = view.getUint32(offset + 8);
1335
1371
  }
1336
- if (tag === 'fvar') {
1337
- fvarTableOffset = view.getUint32(12 + i * 16 + 8);
1372
+ else if (tag === TABLE_TAG_STAT) {
1373
+ statTableOffset = view.getUint32(offset + 8);
1338
1374
  }
1339
- if (tag === 'STAT') {
1340
- statTableOffset = view.getUint32(12 + i * 16 + 8);
1341
- }
1342
- if (tag === 'name') {
1343
- nameTableOffset = view.getUint32(12 + i * 16 + 8);
1375
+ else if (tag === TABLE_TAG_NAME) {
1376
+ nameTableOffset = view.getUint32(offset + 8);
1344
1377
  }
1345
1378
  }
1346
1379
  const unitsPerEm = headTableOffset
@@ -1383,6 +1416,88 @@
1383
1416
  axisNames
1384
1417
  };
1385
1418
  }
1419
+ static extractFeatureTags(fontBuffer) {
1420
+ const view = new DataView(fontBuffer);
1421
+ const numTables = view.getUint16(4);
1422
+ let gsubTableOffset = 0;
1423
+ let gposTableOffset = 0;
1424
+ let nameTableOffset = 0;
1425
+ for (let i = 0; i < numTables; i++) {
1426
+ const offset = 12 + i * 16;
1427
+ const tag = view.getUint32(offset);
1428
+ if (tag === TABLE_TAG_GSUB) {
1429
+ gsubTableOffset = view.getUint32(offset + 8);
1430
+ }
1431
+ else if (tag === TABLE_TAG_GPOS) {
1432
+ gposTableOffset = view.getUint32(offset + 8);
1433
+ }
1434
+ else if (tag === TABLE_TAG_NAME) {
1435
+ nameTableOffset = view.getUint32(offset + 8);
1436
+ }
1437
+ }
1438
+ const features = new Set();
1439
+ const featureNames = {};
1440
+ try {
1441
+ if (gsubTableOffset) {
1442
+ const gsubData = this.extractFeatureDataFromTable(view, gsubTableOffset, nameTableOffset);
1443
+ gsubData.features.forEach(f => features.add(f));
1444
+ Object.assign(featureNames, gsubData.names);
1445
+ }
1446
+ if (gposTableOffset) {
1447
+ const gposData = this.extractFeatureDataFromTable(view, gposTableOffset, nameTableOffset);
1448
+ gposData.features.forEach(f => features.add(f));
1449
+ Object.assign(featureNames, gposData.names);
1450
+ }
1451
+ }
1452
+ catch (e) {
1453
+ return undefined;
1454
+ }
1455
+ const featureArray = Array.from(features).sort();
1456
+ if (featureArray.length === 0)
1457
+ return undefined;
1458
+ return {
1459
+ tags: featureArray,
1460
+ names: Object.keys(featureNames).length > 0 ? featureNames : {}
1461
+ };
1462
+ }
1463
+ static extractFeatureDataFromTable(view, tableOffset, nameTableOffset) {
1464
+ const featureListOffset = view.getUint16(tableOffset + 6);
1465
+ const featureListStart = tableOffset + featureListOffset;
1466
+ const featureCount = view.getUint16(featureListStart);
1467
+ const features = [];
1468
+ const names = {};
1469
+ for (let i = 0; i < featureCount; i++) {
1470
+ const recordOffset = featureListStart + 2 + i * 6;
1471
+ // Decode feature tag
1472
+ const tag = String.fromCharCode(view.getUint8(recordOffset), view.getUint8(recordOffset + 1), view.getUint8(recordOffset + 2), view.getUint8(recordOffset + 3));
1473
+ features.push(tag);
1474
+ // Extract feature name for stylistic sets and character variants
1475
+ if (/^(ss\d{2}|cv\d{2})$/.test(tag) && nameTableOffset) {
1476
+ const featureOffset = view.getUint16(recordOffset + 4);
1477
+ const featureTableStart = featureListStart + featureOffset;
1478
+ // Feature table structure:
1479
+ // uint16 FeatureParams offset
1480
+ // uint16 LookupCount
1481
+ // uint16[LookupCount] LookupListIndex
1482
+ const featureParamsOffset = view.getUint16(featureTableStart);
1483
+ // FeatureParams for ss features:
1484
+ // uint16 Version (should be 0)
1485
+ // uint16 UINameID
1486
+ if (featureParamsOffset !== 0) {
1487
+ const paramsStart = featureTableStart + featureParamsOffset;
1488
+ const version = view.getUint16(paramsStart);
1489
+ if (version === 0) {
1490
+ const nameID = view.getUint16(paramsStart + 2);
1491
+ const name = this.getNameFromNameTable(view, nameTableOffset, nameID);
1492
+ if (name) {
1493
+ names[tag] = name;
1494
+ }
1495
+ }
1496
+ }
1497
+ }
1498
+ }
1499
+ return { features, names };
1500
+ }
1386
1501
  static extractAxisNames(view, statOffset, nameOffset) {
1387
1502
  try {
1388
1503
  // STAT table structure
@@ -1593,7 +1708,7 @@
1593
1708
  const padding = (4 - (table.origLength % 4)) % 4;
1594
1709
  sfntOffset += padding;
1595
1710
  }
1596
- debugLogger.log('WOFF font decompressed successfully');
1711
+ logger.log('WOFF font decompressed successfully');
1597
1712
  return sfntData.buffer.slice(0, sfntOffset);
1598
1713
  }
1599
1714
  static async decompressZlib(compressedData) {
@@ -1622,7 +1737,7 @@
1622
1737
  // Check if this is a WOFF font and decompress if needed
1623
1738
  const format = WoffConverter.detectFormat(fontBuffer);
1624
1739
  if (format === 'woff') {
1625
- debugLogger.log('WOFF font detected, decompressing...');
1740
+ logger.log('WOFF font detected, decompressing...');
1626
1741
  fontBuffer = await WoffConverter.decompressWoff(fontBuffer);
1627
1742
  }
1628
1743
  else if (format === 'woff2') {
@@ -1660,6 +1775,7 @@
1660
1775
  };
1661
1776
  }
1662
1777
  }
1778
+ const featureData = FontMetadataExtractor.extractFeatureTags(fontBuffer);
1663
1779
  return {
1664
1780
  hb,
1665
1781
  fontBlob,
@@ -1670,11 +1786,13 @@
1670
1786
  metrics,
1671
1787
  fontVariations,
1672
1788
  isVariable,
1673
- variationAxes
1789
+ variationAxes,
1790
+ availableFeatures: featureData?.tags,
1791
+ featureNames: featureData?.names
1674
1792
  };
1675
1793
  }
1676
1794
  catch (error) {
1677
- debugLogger.error('Failed to load font:', error);
1795
+ logger.error('Failed to load font:', error);
1678
1796
  throw error;
1679
1797
  }
1680
1798
  finally {
@@ -1695,7 +1813,7 @@
1695
1813
  }
1696
1814
  }
1697
1815
  catch (error) {
1698
- debugLogger.error('Error destroying font resources:', error);
1816
+ logger.error('Error destroying font resources:', error);
1699
1817
  }
1700
1818
  }
1701
1819
  }
@@ -2099,7 +2217,7 @@
2099
2217
  if (valid.length === 0) {
2100
2218
  return { triangles: { vertices: [], indices: [] }, contours: [] };
2101
2219
  }
2102
- debugLogger.log(`Tessellator: removeOverlaps=${removeOverlaps}, processing ${valid.length} paths`);
2220
+ logger.log(`Tessellator: removeOverlaps=${removeOverlaps}, processing ${valid.length} paths`);
2103
2221
  return this.tessellate(valid, removeOverlaps, isCFF);
2104
2222
  }
2105
2223
  tessellate(paths, removeOverlaps, isCFF) {
@@ -2109,19 +2227,19 @@
2109
2227
  : paths;
2110
2228
  let contours = this.pathsToContours(normalizedPaths);
2111
2229
  if (removeOverlaps) {
2112
- debugLogger.log('Two-pass: boundary extraction then triangulation');
2230
+ logger.log('Two-pass: boundary extraction then triangulation');
2113
2231
  // Extract boundaries to remove overlaps
2114
2232
  const boundaryResult = this.performTessellation(contours, 'boundary');
2115
2233
  if (!boundaryResult) {
2116
- debugLogger.warn('libtess returned empty result from boundary pass');
2234
+ logger.warn('libtess returned empty result from boundary pass');
2117
2235
  return { triangles: { vertices: [], indices: [] }, contours: [] };
2118
2236
  }
2119
2237
  // Convert boundary elements back to contours
2120
2238
  contours = this.boundaryToContours(boundaryResult);
2121
- debugLogger.log(`Boundary pass created ${contours.length} contours. Starting triangulation pass.`);
2239
+ logger.log(`Boundary pass created ${contours.length} contours. Starting triangulation pass.`);
2122
2240
  }
2123
2241
  else {
2124
- debugLogger.log(`Single-pass triangulation for ${isCFF ? 'CFF' : 'TTF'}`);
2242
+ logger.log(`Single-pass triangulation for ${isCFF ? 'CFF' : 'TTF'}`);
2125
2243
  }
2126
2244
  // Triangulate the contours
2127
2245
  const triangleResult = this.performTessellation(contours, 'triangles');
@@ -2129,7 +2247,7 @@
2129
2247
  const warning = removeOverlaps
2130
2248
  ? 'libtess returned empty result from triangulation pass'
2131
2249
  : 'libtess returned empty result from single-pass triangulation';
2132
- debugLogger.warn(warning);
2250
+ logger.warn(warning);
2133
2251
  return { triangles: { vertices: [], indices: [] }, contours };
2134
2252
  }
2135
2253
  return {
@@ -2184,7 +2302,7 @@
2184
2302
  return idx;
2185
2303
  });
2186
2304
  tess.gluTessCallback(libtess_minExports.gluEnum.GLU_TESS_ERROR, (errno) => {
2187
- debugLogger.warn(`libtess error: ${errno}`);
2305
+ logger.warn(`libtess error: ${errno}`);
2188
2306
  });
2189
2307
  tess.gluTessNormal(0, 0, 1);
2190
2308
  tess.gluTessBeginPolygon(null);
@@ -3206,7 +3324,7 @@
3206
3324
  }
3207
3325
  }
3208
3326
  catch (error) {
3209
- debugLogger.warn('Error destroying draw callbacks:', error);
3327
+ logger.warn('Error destroying draw callbacks:', error);
3210
3328
  }
3211
3329
  this.collector = undefined;
3212
3330
  }
@@ -3496,7 +3614,8 @@
3496
3614
  }
3497
3615
  buffer.addText(lineInfo.text);
3498
3616
  buffer.guessSegmentProperties();
3499
- this.loadedFont.hb.shape(this.loadedFont.font, buffer);
3617
+ const featuresString = convertFontFeaturesToString(this.loadedFont.fontFeatures);
3618
+ this.loadedFont.hb.shape(this.loadedFont.font, buffer, featuresString);
3500
3619
  const glyphInfos = buffer.json(this.loadedFont.font);
3501
3620
  buffer.destroy();
3502
3621
  const clusters = [];
@@ -3571,15 +3690,14 @@
3571
3690
  naturalSpaceWidth = TextMeasurer.measureTextWidth(this.loadedFont, ' ', letterSpacing);
3572
3691
  this.cachedSpaceWidth.set(letterSpacing, naturalSpaceWidth);
3573
3692
  }
3693
+ const width = naturalSpaceWidth;
3574
3694
  const stretchFactor = SPACE_STRETCH_RATIO;
3575
3695
  const shrinkFactor = SPACE_SHRINK_RATIO;
3576
3696
  if (lineInfo.adjustmentRatio > 0) {
3577
- spaceAdjustment =
3578
- lineInfo.adjustmentRatio * naturalSpaceWidth * stretchFactor;
3697
+ spaceAdjustment = lineInfo.adjustmentRatio * width * stretchFactor;
3579
3698
  }
3580
3699
  else if (lineInfo.adjustmentRatio < 0) {
3581
- spaceAdjustment =
3582
- lineInfo.adjustmentRatio * naturalSpaceWidth * shrinkFactor;
3700
+ spaceAdjustment = lineInfo.adjustmentRatio * width * shrinkFactor;
3583
3701
  }
3584
3702
  }
3585
3703
  return spaceAdjustment;
@@ -4504,12 +4622,16 @@
4504
4622
  const baseFontKey = typeof options.font === 'string'
4505
4623
  ? options.font
4506
4624
  : `buffer-${Text.generateFontContentHash(options.font)}`;
4507
- const fontKey = options.fontVariations
4508
- ? `${baseFontKey}_${JSON.stringify(options.fontVariations)}`
4509
- : baseFontKey;
4625
+ let fontKey = baseFontKey;
4626
+ if (options.fontVariations) {
4627
+ fontKey += `_var_${JSON.stringify(options.fontVariations)}`;
4628
+ }
4629
+ if (options.fontFeatures) {
4630
+ fontKey += `_feat_${JSON.stringify(options.fontFeatures)}`;
4631
+ }
4510
4632
  let loadedFont = Text.fontCache.get(fontKey);
4511
4633
  if (!loadedFont) {
4512
- loadedFont = await Text.loadAndCacheFont(fontKey, options.font, options.fontVariations);
4634
+ loadedFont = await Text.loadAndCacheFont(fontKey, options.font, options.fontVariations, options.fontFeatures);
4513
4635
  }
4514
4636
  const text = new Text({ maxCacheSizeMB: options.maxCacheSizeMB });
4515
4637
  text.setLoadedFont(loadedFont);
@@ -4523,12 +4645,11 @@
4523
4645
  measureTextWidth: (textString, letterSpacing) => text.measureTextWidth(textString, letterSpacing)
4524
4646
  };
4525
4647
  }
4526
- static async loadAndCacheFont(fontKey, font, fontVariations) {
4648
+ static async loadAndCacheFont(fontKey, font, fontVariations, fontFeatures) {
4527
4649
  const tempText = new Text();
4528
- await tempText.loadFont(font, fontVariations);
4650
+ await tempText.loadFont(font, fontVariations, fontFeatures);
4529
4651
  const loadedFont = tempText.getLoadedFont();
4530
4652
  Text.fontCache.set(fontKey, loadedFont);
4531
- // Don't destroy tempText - the cached font references its HarfBuzz objects
4532
4653
  return loadedFont;
4533
4654
  }
4534
4655
  static generateFontContentHash(buffer) {
@@ -4547,10 +4668,13 @@
4547
4668
  const contentHash = Text.generateFontContentHash(loadedFont._buffer);
4548
4669
  this.currentFontId = `font_${contentHash}`;
4549
4670
  if (loadedFont.fontVariations) {
4550
- this.currentFontId += `_${JSON.stringify(loadedFont.fontVariations)}`;
4671
+ this.currentFontId += `_var_${JSON.stringify(loadedFont.fontVariations)}`;
4672
+ }
4673
+ if (loadedFont.fontFeatures) {
4674
+ this.currentFontId += `_feat_${JSON.stringify(loadedFont.fontFeatures)}`;
4551
4675
  }
4552
4676
  }
4553
- async loadFont(fontSrc, fontVariations) {
4677
+ async loadFont(fontSrc, fontVariations, fontFeatures) {
4554
4678
  perfLogger.start('Text.loadFont', {
4555
4679
  fontSrc: typeof fontSrc === 'string' ? fontSrc : `buffer(${fontSrc.byteLength})`
4556
4680
  });
@@ -4571,14 +4695,20 @@
4571
4695
  this.destroy();
4572
4696
  }
4573
4697
  this.loadedFont = await this.fontLoader.loadFont(fontBuffer, fontVariations);
4698
+ if (fontFeatures) {
4699
+ this.loadedFont.fontFeatures = fontFeatures;
4700
+ }
4574
4701
  const contentHash = Text.generateFontContentHash(fontBuffer);
4575
4702
  this.currentFontId = `font_${contentHash}`;
4576
4703
  if (fontVariations) {
4577
- this.currentFontId += `_${JSON.stringify(fontVariations)}`;
4704
+ this.currentFontId += `_var_${JSON.stringify(fontVariations)}`;
4705
+ }
4706
+ if (fontFeatures) {
4707
+ this.currentFontId += `_feat_${JSON.stringify(fontFeatures)}`;
4578
4708
  }
4579
4709
  }
4580
4710
  catch (error) {
4581
- debugLogger.error('Failed to load font:', error);
4711
+ logger.error('Failed to load font:', error);
4582
4712
  throw error;
4583
4713
  }
4584
4714
  finally {
@@ -4653,7 +4783,7 @@
4653
4783
  };
4654
4784
  }
4655
4785
  catch (error) {
4656
- debugLogger.warn(`Failed to load patterns for ${language}: ${error}`);
4786
+ logger.warn(`Failed to load patterns for ${language}: ${error}`);
4657
4787
  return {
4658
4788
  ...options,
4659
4789
  layout: {
@@ -4917,7 +5047,7 @@
4917
5047
  Text.patternCache.set(language, pattern);
4918
5048
  }
4919
5049
  catch (error) {
4920
- debugLogger.warn(`Failed to pre-load patterns for ${language}: ${error}`);
5050
+ logger.warn(`Failed to pre-load patterns for ${language}: ${error}`);
4921
5051
  }
4922
5052
  }
4923
5053
  }));
@@ -4979,7 +5109,7 @@
4979
5109
  FontLoader.destroyFont(currentFont);
4980
5110
  }
4981
5111
  catch (error) {
4982
- debugLogger.warn('Error destroying HarfBuzz objects:', error);
5112
+ logger.warn('Error destroying HarfBuzz objects:', error);
4983
5113
  }
4984
5114
  finally {
4985
5115
  this.loadedFont = undefined;