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 +6 -0
- package/dist/index.cjs +71 -23
- package/dist/index.d.ts +1 -0
- package/dist/index.js +71 -23
- package/dist/index.min.cjs +665 -655
- package/dist/index.min.js +664 -654
- package/dist/index.umd.js +71 -23
- package/dist/index.umd.min.js +602 -592
- package/dist/types/core/cache/GlyphContourCollector.d.ts +1 -1
- package/dist/types/core/geometry/BoundaryClusterer.d.ts +1 -1
- package/dist/types/core/geometry/Polygonizer.d.ts +1 -1
- package/dist/types/core/types.d.ts +2 -1
- package/dist/types/utils/loadBinary.d.ts +1 -0
- package/package.json +1 -1
- /package/dist/types/{core → utils}/vectors.d.ts +0 -0
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.
|
|
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
|
-
//
|
|
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;
|
|
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.
|
|
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
package/dist/index.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/*!
|
|
2
|
-
* three-text v0.4.
|
|
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
|
-
//
|
|
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;
|
|
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.
|
|
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();
|