three-text 0.4.3 → 0.4.5

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 CHANGED
@@ -195,6 +195,12 @@ self.onmessage = (e) => {
195
195
 
196
196
  The library will prioritize the buffer if both a path and a buffer have been set
197
197
 
198
+ #### Platform-specific notes
199
+
200
+ **NW.js with CommonJS:** If using `require()` to load the CJS build in NW.js, use Option 2 (buffer-based loading). NW.js's [dual-context architecture](https://docs.nwjs.io/For%20Users/Advanced/JavaScript%20Contexts%20in%20NW.js/#separate-context-mode) causes path resolution issues in this specific scenario. ESM imports and bundled code work normally
201
+
202
+ **Electron with `file://` protocol:** If loading HTML directly from the filesystem (not via a dev server), use Option 2 (buffer-based loading) or enable `nodeIntegration` in your BrowserWindow
203
+
198
204
  ### Hyphenation patterns
199
205
 
200
206
  **For ES Modules (recommended):** Import and register only the languages you need:
package/dist/index.cjs CHANGED
@@ -1,5 +1,5 @@
1
1
  /*!
2
- * three-text v0.4.3
2
+ * three-text v0.4.5
3
3
  * Copyright (C) 2025 Countertype LLC
4
4
  *
5
5
  * This program is free software: you can redistribute it and/or modify
@@ -557,6 +557,31 @@ class LineBreak {
557
557
  currentIndex += token.length;
558
558
  }
559
559
  else {
560
+ if (lineWidth && token.includes('-') && !token.includes('\u00AD')) {
561
+ const tokenWidth = measureText(token);
562
+ if (tokenWidth > lineWidth) {
563
+ // Break long hyphenated tokens into characters (break-all behavior)
564
+ const chars = Array.from(token);
565
+ for (let i = 0; i < chars.length; i++) {
566
+ items.push({
567
+ type: ItemType.BOX,
568
+ width: measureText(chars[i]),
569
+ text: chars[i],
570
+ originIndex: tokenStartIndex + i
571
+ });
572
+ if (i < chars.length - 1) {
573
+ items.push({
574
+ type: ItemType.PENALTY,
575
+ width: 0,
576
+ penalty: 5000,
577
+ originIndex: tokenStartIndex + i + 1
578
+ });
579
+ }
580
+ }
581
+ currentIndex += token.length;
582
+ continue;
583
+ }
584
+ }
560
585
  const segments = token.split(/(-)/);
561
586
  let segmentIndex = tokenStartIndex;
562
587
  for (const segment of segments) {
@@ -2187,7 +2212,6 @@ async function loadPattern(language, patternsPath) {
2187
2212
  }
2188
2213
  }
2189
2214
 
2190
- // Bector and bounding box types for core
2191
2215
  // 2D Vector
2192
2216
  class Vec2 {
2193
2217
  constructor(x = 0, y = 0) {
@@ -2569,7 +2593,8 @@ class Tessellator {
2569
2593
  vertices: triangleResult.vertices,
2570
2594
  indices: triangleResult.indices || []
2571
2595
  },
2572
- contours: extrusionContours
2596
+ contours: extrusionContours,
2597
+ contoursAreBoundary: removeOverlaps
2573
2598
  };
2574
2599
  }
2575
2600
  tessellateContours(contours, removeOverlaps, isCFF, needsExtrusionContours) {
@@ -2631,7 +2656,8 @@ class Tessellator {
2631
2656
  vertices: triangleResult.vertices,
2632
2657
  indices: triangleResult.indices || []
2633
2658
  },
2634
- contours: extrusionContours
2659
+ contours: extrusionContours,
2660
+ contoursAreBoundary: removeOverlaps
2635
2661
  };
2636
2662
  }
2637
2663
  pathsToContours(paths, reversePoints = false) {
@@ -2827,14 +2853,15 @@ class Extruder {
2827
2853
  const points = geometry.triangles.vertices;
2828
2854
  const triangleIndices = geometry.triangles.indices;
2829
2855
  const contours = geometry.contours;
2856
+ const contoursAreBoundary = geometry.contoursAreBoundary === true;
2830
2857
  const pointLen = points.length;
2831
2858
  const numPoints = pointLen / 2;
2832
- // Prefer contours for side walls; fall back to triangle edges
2859
+ // Use boundary contours for side walls when available
2833
2860
  let boundaryEdges = [];
2834
2861
  let sideEdgeCount = 0;
2835
2862
  let useContours = false;
2836
2863
  if (depth !== 0) {
2837
- if (contours.length > 0) {
2864
+ if (contoursAreBoundary && contours.length > 0) {
2838
2865
  useContours = true;
2839
2866
  for (const contour of contours) {
2840
2867
  const contourPointCount = contour.length >> 1;
@@ -4783,6 +4810,41 @@ class TextShaper {
4783
4810
  }
4784
4811
  }
4785
4812
 
4813
+ // Fetch with fs fallback for Electron file:// and Node.js environments
4814
+ async function loadBinary(filePath) {
4815
+ try {
4816
+ const res = await fetch(filePath);
4817
+ if (!res.ok) {
4818
+ throw new Error(`HTTP ${res.status}`);
4819
+ }
4820
+ return await res.arrayBuffer();
4821
+ }
4822
+ catch (fetchError) {
4823
+ const req = globalThis.require;
4824
+ if (typeof req !== 'function') {
4825
+ throw new Error(`Failed to fetch ${filePath}: ${fetchError}`);
4826
+ }
4827
+ try {
4828
+ const fs = req('fs');
4829
+ const nodePath = req('path');
4830
+ // file:// URLs need path resolution relative to the HTML document
4831
+ let resolvedPath = filePath;
4832
+ if (typeof window !== 'undefined' &&
4833
+ window.location?.protocol === 'file:') {
4834
+ const dir = nodePath.dirname(window.location.pathname);
4835
+ resolvedPath = nodePath.join(dir, filePath);
4836
+ }
4837
+ const buffer = fs.readFileSync(resolvedPath);
4838
+ if (buffer instanceof ArrayBuffer)
4839
+ return buffer;
4840
+ return buffer.buffer.slice(buffer.byteOffset, buffer.byteOffset + buffer.byteLength);
4841
+ }
4842
+ catch (fsError) {
4843
+ throw new Error(`Failed to load ${filePath}: fetch failed (${fetchError}), fs.readFileSync failed (${fsError})`);
4844
+ }
4845
+ }
4846
+ }
4847
+
4786
4848
  var hb = {exports: {}};
4787
4849
 
4788
4850
  var fs = {};
@@ -5373,11 +5435,9 @@ try {
5373
5435
  var hbjsExports = hbjs$2.exports;
5374
5436
  var hbjs$1 = /*@__PURE__*/getDefaultExportFromCjs(hbjsExports);
5375
5437
 
5376
- // These will be bundled by Rollup
5377
- // @ts-expect-error - no declarations for harfbuzzjs/hb.js
5378
5438
  let harfbuzzPromise = null;
5379
5439
  let wasmPath = null;
5380
- let wasmBuffer = null; // Add buffer option
5440
+ let wasmBuffer = null;
5381
5441
  const HarfBuzzLoader = {
5382
5442
  setWasmPath(path) {
5383
5443
  wasmPath = path;
@@ -5400,12 +5460,7 @@ const HarfBuzzLoader = {
5400
5460
  moduleConfig.wasmBinary = wasmBuffer;
5401
5461
  }
5402
5462
  else if (wasmPath) {
5403
- moduleConfig.locateFile = (path, scriptDirectory) => {
5404
- if (path.endsWith('.wasm')) {
5405
- return wasmPath;
5406
- }
5407
- return scriptDirectory + path;
5408
- };
5463
+ moduleConfig.wasmBinary = await loadBinary(wasmPath);
5409
5464
  }
5410
5465
  else {
5411
5466
  throw new Error('HarfBuzz WASM path or buffer must be set before initialization.');
@@ -5702,14 +5757,7 @@ class Text {
5702
5757
  Text.hbInitPromise = HarfBuzzLoader.getHarfBuzz();
5703
5758
  }
5704
5759
  await Text.hbInitPromise;
5705
- const fontBuffer = typeof fontSrc === 'string'
5706
- ? await fetch(fontSrc).then((res) => {
5707
- if (!res.ok) {
5708
- throw new Error(`Failed to load font from ${fontSrc}: HTTP ${res.status} ${res.statusText}`);
5709
- }
5710
- return res.arrayBuffer();
5711
- })
5712
- : fontSrc;
5760
+ const fontBuffer = typeof fontSrc === 'string' ? await loadBinary(fontSrc) : fontSrc;
5713
5761
  try {
5714
5762
  if (this.loadedFont) {
5715
5763
  this.destroy();
package/dist/index.d.ts CHANGED
@@ -149,6 +149,7 @@ interface Triangles {
149
149
  interface ProcessedGeometry {
150
150
  triangles: Triangles;
151
151
  contours: number[][];
152
+ contoursAreBoundary?: boolean;
152
153
  }
153
154
  interface GlyphData {
154
155
  geometry: ProcessedGeometry;
package/dist/index.js CHANGED
@@ -1,5 +1,5 @@
1
1
  /*!
2
- * three-text v0.4.3
2
+ * three-text v0.4.5
3
3
  * Copyright (C) 2025 Countertype LLC
4
4
  *
5
5
  * This program is free software: you can redistribute it and/or modify
@@ -554,6 +554,31 @@ class LineBreak {
554
554
  currentIndex += token.length;
555
555
  }
556
556
  else {
557
+ if (lineWidth && token.includes('-') && !token.includes('\u00AD')) {
558
+ const tokenWidth = measureText(token);
559
+ if (tokenWidth > lineWidth) {
560
+ // Break long hyphenated tokens into characters (break-all behavior)
561
+ const chars = Array.from(token);
562
+ for (let i = 0; i < chars.length; i++) {
563
+ items.push({
564
+ type: ItemType.BOX,
565
+ width: measureText(chars[i]),
566
+ text: chars[i],
567
+ originIndex: tokenStartIndex + i
568
+ });
569
+ if (i < chars.length - 1) {
570
+ items.push({
571
+ type: ItemType.PENALTY,
572
+ width: 0,
573
+ penalty: 5000,
574
+ originIndex: tokenStartIndex + i + 1
575
+ });
576
+ }
577
+ }
578
+ currentIndex += token.length;
579
+ continue;
580
+ }
581
+ }
557
582
  const segments = token.split(/(-)/);
558
583
  let segmentIndex = tokenStartIndex;
559
584
  for (const segment of segments) {
@@ -2184,7 +2209,6 @@ async function loadPattern(language, patternsPath) {
2184
2209
  }
2185
2210
  }
2186
2211
 
2187
- // Bector and bounding box types for core
2188
2212
  // 2D Vector
2189
2213
  class Vec2 {
2190
2214
  constructor(x = 0, y = 0) {
@@ -2566,7 +2590,8 @@ class Tessellator {
2566
2590
  vertices: triangleResult.vertices,
2567
2591
  indices: triangleResult.indices || []
2568
2592
  },
2569
- contours: extrusionContours
2593
+ contours: extrusionContours,
2594
+ contoursAreBoundary: removeOverlaps
2570
2595
  };
2571
2596
  }
2572
2597
  tessellateContours(contours, removeOverlaps, isCFF, needsExtrusionContours) {
@@ -2628,7 +2653,8 @@ class Tessellator {
2628
2653
  vertices: triangleResult.vertices,
2629
2654
  indices: triangleResult.indices || []
2630
2655
  },
2631
- contours: extrusionContours
2656
+ contours: extrusionContours,
2657
+ contoursAreBoundary: removeOverlaps
2632
2658
  };
2633
2659
  }
2634
2660
  pathsToContours(paths, reversePoints = false) {
@@ -2824,14 +2850,15 @@ class Extruder {
2824
2850
  const points = geometry.triangles.vertices;
2825
2851
  const triangleIndices = geometry.triangles.indices;
2826
2852
  const contours = geometry.contours;
2853
+ const contoursAreBoundary = geometry.contoursAreBoundary === true;
2827
2854
  const pointLen = points.length;
2828
2855
  const numPoints = pointLen / 2;
2829
- // Prefer contours for side walls; fall back to triangle edges
2856
+ // Use boundary contours for side walls when available
2830
2857
  let boundaryEdges = [];
2831
2858
  let sideEdgeCount = 0;
2832
2859
  let useContours = false;
2833
2860
  if (depth !== 0) {
2834
- if (contours.length > 0) {
2861
+ if (contoursAreBoundary && contours.length > 0) {
2835
2862
  useContours = true;
2836
2863
  for (const contour of contours) {
2837
2864
  const contourPointCount = contour.length >> 1;
@@ -4780,6 +4807,41 @@ class TextShaper {
4780
4807
  }
4781
4808
  }
4782
4809
 
4810
+ // Fetch with fs fallback for Electron file:// and Node.js environments
4811
+ async function loadBinary(filePath) {
4812
+ try {
4813
+ const res = await fetch(filePath);
4814
+ if (!res.ok) {
4815
+ throw new Error(`HTTP ${res.status}`);
4816
+ }
4817
+ return await res.arrayBuffer();
4818
+ }
4819
+ catch (fetchError) {
4820
+ const req = globalThis.require;
4821
+ if (typeof req !== 'function') {
4822
+ throw new Error(`Failed to fetch ${filePath}: ${fetchError}`);
4823
+ }
4824
+ try {
4825
+ const fs = req('fs');
4826
+ const nodePath = req('path');
4827
+ // file:// URLs need path resolution relative to the HTML document
4828
+ let resolvedPath = filePath;
4829
+ if (typeof window !== 'undefined' &&
4830
+ window.location?.protocol === 'file:') {
4831
+ const dir = nodePath.dirname(window.location.pathname);
4832
+ resolvedPath = nodePath.join(dir, filePath);
4833
+ }
4834
+ const buffer = fs.readFileSync(resolvedPath);
4835
+ if (buffer instanceof ArrayBuffer)
4836
+ return buffer;
4837
+ return buffer.buffer.slice(buffer.byteOffset, buffer.byteOffset + buffer.byteLength);
4838
+ }
4839
+ catch (fsError) {
4840
+ throw new Error(`Failed to load ${filePath}: fetch failed (${fetchError}), fs.readFileSync failed (${fsError})`);
4841
+ }
4842
+ }
4843
+ }
4844
+
4783
4845
  var hb = {exports: {}};
4784
4846
 
4785
4847
  var fs = {};
@@ -5370,11 +5432,9 @@ try {
5370
5432
  var hbjsExports = hbjs$2.exports;
5371
5433
  var hbjs$1 = /*@__PURE__*/getDefaultExportFromCjs(hbjsExports);
5372
5434
 
5373
- // These will be bundled by Rollup
5374
- // @ts-expect-error - no declarations for harfbuzzjs/hb.js
5375
5435
  let harfbuzzPromise = null;
5376
5436
  let wasmPath = null;
5377
- let wasmBuffer = null; // Add buffer option
5437
+ let wasmBuffer = null;
5378
5438
  const HarfBuzzLoader = {
5379
5439
  setWasmPath(path) {
5380
5440
  wasmPath = path;
@@ -5397,12 +5457,7 @@ const HarfBuzzLoader = {
5397
5457
  moduleConfig.wasmBinary = wasmBuffer;
5398
5458
  }
5399
5459
  else if (wasmPath) {
5400
- moduleConfig.locateFile = (path, scriptDirectory) => {
5401
- if (path.endsWith('.wasm')) {
5402
- return wasmPath;
5403
- }
5404
- return scriptDirectory + path;
5405
- };
5460
+ moduleConfig.wasmBinary = await loadBinary(wasmPath);
5406
5461
  }
5407
5462
  else {
5408
5463
  throw new Error('HarfBuzz WASM path or buffer must be set before initialization.');
@@ -5699,14 +5754,7 @@ class Text {
5699
5754
  Text.hbInitPromise = HarfBuzzLoader.getHarfBuzz();
5700
5755
  }
5701
5756
  await Text.hbInitPromise;
5702
- const fontBuffer = typeof fontSrc === 'string'
5703
- ? await fetch(fontSrc).then((res) => {
5704
- if (!res.ok) {
5705
- throw new Error(`Failed to load font from ${fontSrc}: HTTP ${res.status} ${res.statusText}`);
5706
- }
5707
- return res.arrayBuffer();
5708
- })
5709
- : fontSrc;
5757
+ const fontBuffer = typeof fontSrc === 'string' ? await loadBinary(fontSrc) : fontSrc;
5710
5758
  try {
5711
5759
  if (this.loadedFont) {
5712
5760
  this.destroy();