@metamesh/modeling 0.0.0-stage → 0.7.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/CHANGELOG.md +111 -0
- package/LICENSE +200 -0
- package/README.md +376 -2
- package/THIRD_PARTY_NOTICES.md +153 -0
- package/dist/api/engine.d.ts +71 -0
- package/dist/api/geometry.d.ts +3136 -0
- package/dist/api/index.d.ts +2087 -0
- package/dist/api/text.d.ts +93 -0
- package/dist/engine/construction-operations.js +189 -0
- package/dist/engine/execution.js +227 -0
- package/dist/engine/export-3mf.js +100 -0
- package/dist/engine/extrusion-finishing.js +82 -0
- package/dist/engine/finalization.js +214 -0
- package/dist/engine/index.js +83 -0
- package/dist/engine/inspection-operations.js +52 -0
- package/dist/engine/kernel/LICENSE +201 -0
- package/dist/engine/kernel/manifold.js +2 -0
- package/dist/engine/kernel/manifold.wasm +0 -0
- package/dist/engine/loft-operations.js +156 -0
- package/dist/engine/mechanical-operations.js +51 -0
- package/dist/engine/modeling-context.js +38 -0
- package/dist/engine/native/adapter.js +20 -0
- package/dist/engine/native/context.js +209 -0
- package/dist/engine/native/copied-views.js +46 -0
- package/dist/engine/native/index.js +13 -0
- package/dist/engine/native/operations.js +115 -0
- package/dist/engine/native/ownership.js +98 -0
- package/dist/engine/native/regions.js +138 -0
- package/dist/engine/native/scoped-primitives.js +127 -0
- package/dist/engine/native/scoped-threads.js +34 -0
- package/dist/engine/native/threads.js +52 -0
- package/dist/engine/native/transforms.js +111 -0
- package/dist/engine/operation-environment.js +20 -0
- package/dist/engine/pattern-operations.js +36 -0
- package/dist/engine/pattern-placements.js +176 -0
- package/dist/engine/primitive-anchors.js +70 -0
- package/dist/engine/primitive-correspondence.js +74 -0
- package/dist/engine/primitive-operations.js +151 -0
- package/dist/engine/primitive-profile-types.js +1 -0
- package/dist/engine/primitive-profiles.js +250 -0
- package/dist/engine/profile-mesh.js +279 -0
- package/dist/engine/section-operations.js +93 -0
- package/dist/engine/shape-anchors.js +88 -0
- package/dist/engine/transform-operations.js +225 -0
- package/dist/engine/types.js +1 -0
- package/dist/geometry/anchors.js +226 -0
- package/dist/geometry/curves/evaluation.js +162 -0
- package/dist/geometry/curves/sampling.js +266 -0
- package/dist/geometry/curves/types.js +1 -0
- package/dist/geometry/cylinder.js +117 -0
- package/dist/geometry/edge-finishing.js +160 -0
- package/dist/geometry/errors.js +76 -0
- package/dist/geometry/extrusion.js +150 -0
- package/dist/geometry/heightfield.js +115 -0
- package/dist/geometry/index.js +42 -0
- package/dist/geometry/loft-correspondence.js +204 -0
- package/dist/geometry/loft-geometry.js +184 -0
- package/dist/geometry/loft-options.js +56 -0
- package/dist/geometry/loft-simple-mesh.js +92 -0
- package/dist/geometry/loft-triangulation.js +64 -0
- package/dist/geometry/loft.js +95 -0
- package/dist/geometry/math/vector3.js +71 -0
- package/dist/geometry/mechanical/gear-motion.js +94 -0
- package/dist/geometry/mechanical/gears.js +484 -0
- package/dist/geometry/mechanical/index.js +13 -0
- package/dist/geometry/mechanical/standards.js +67 -0
- package/dist/geometry/mechanical/thread-helices.js +44 -0
- package/dist/geometry/mechanical/thread-mesh-plan.js +358 -0
- package/dist/geometry/mechanical/thread-mesh.js +142 -0
- package/dist/geometry/mechanical/thread-profiles.js +373 -0
- package/dist/geometry/mechanical/thread-solids.js +230 -0
- package/dist/geometry/mechanical/thread-surface.js +433 -0
- package/dist/geometry/mechanical/threads.js +138 -0
- package/dist/geometry/mesh-edge-table.js +126 -0
- package/dist/geometry/mesh-planar.js +408 -0
- package/dist/geometry/mesh-quality.js +396 -0
- package/dist/geometry/mesh-signature.js +137 -0
- package/dist/geometry/mesh.js +283 -0
- package/dist/geometry/model-limits.js +50 -0
- package/dist/geometry/numeric.js +11 -0
- package/dist/geometry/paths/frames.js +118 -0
- package/dist/geometry/paths/helix.js +49 -0
- package/dist/geometry/paths/path.js +32 -0
- package/dist/geometry/paths/planar.js +135 -0
- package/dist/geometry/paths/resampling.js +125 -0
- package/dist/geometry/polygon-clipping.js +52 -0
- package/dist/geometry/primitive-options.js +1 -0
- package/dist/geometry/primitive-resolution.js +17 -0
- package/dist/geometry/primitive-resolvers.js +235 -0
- package/dist/geometry/profiles/matched.js +192 -0
- package/dist/geometry/profiles/open-profile.js +137 -0
- package/dist/geometry/profiles/profiles.js +91 -0
- package/dist/geometry/profiles/rectangle.js +249 -0
- package/dist/geometry/profiles/shapes.js +41 -0
- package/dist/geometry/profiles/span-sampling.js +28 -0
- package/dist/geometry/profiles/types.js +1 -0
- package/dist/geometry/profiles/validation.js +20 -0
- package/dist/geometry/regions.js +371 -0
- package/dist/geometry/resolution.js +236 -0
- package/dist/geometry/revolve/revolve-mesh.js +129 -0
- package/dist/geometry/sampling-limits.js +29 -0
- package/dist/geometry/stroke-path.js +181 -0
- package/dist/geometry/svg/errors.js +23 -0
- package/dist/geometry/svg/fill.js +172 -0
- package/dist/geometry/svg/index.js +1 -0
- package/dist/geometry/svg/matrix.js +130 -0
- package/dist/geometry/svg/parser.js +536 -0
- package/dist/geometry/svg/path.js +349 -0
- package/dist/geometry/svg/types.js +1 -0
- package/dist/geometry/svg-import.js +662 -0
- package/dist/geometry/sweeps/options.js +28 -0
- package/dist/geometry/sweeps/orientation.js +106 -0
- package/dist/geometry/sweeps/stations.js +161 -0
- package/dist/geometry/sweeps/sweep-closed.js +96 -0
- package/dist/geometry/sweeps/sweep-mesh.js +30 -0
- package/dist/geometry/sweeps/sweep-open.js +76 -0
- package/dist/geometry/sweeps/validation.js +116 -0
- package/dist/geometry/text-contours.js +32 -0
- package/dist/geometry/text-layout.js +1 -0
- package/dist/geometry/tolerance.js +53 -0
- package/dist/geometry/transform-geometry.js +41 -0
- package/dist/geometry/transform3.js +340 -0
- package/dist/geometry/types.js +1 -0
- package/dist/index.js +1 -0
- package/dist/model/execution-binding.js +30 -0
- package/dist/model/operations.js +154 -0
- package/dist/model/options.js +1 -0
- package/dist/model/types.js +1 -0
- package/dist/text/index.js +58 -0
- package/dist/text/vector-text-layout.js +279 -0
- package/package.json +100 -3
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import { SVGPathData, SVGPathDataTransformer } from "svg-pathdata";
|
|
2
|
+
import { createVectorTextLayout } from "./vector-text-layout.js";
|
|
3
|
+
/** Limits apply before parsing or shaping untrusted font data. */
|
|
4
|
+
export const MAX_VECTOR_FONT_BYTES = 16 * 1024 * 1024;
|
|
5
|
+
function fail(message) {
|
|
6
|
+
throw new Error(message);
|
|
7
|
+
}
|
|
8
|
+
function serializeVectorText({ paths, metrics }) {
|
|
9
|
+
const bounds = metrics.bounds;
|
|
10
|
+
if (bounds === null || paths.length === 0)
|
|
11
|
+
fail("The selected font produced no visible glyph outlines.");
|
|
12
|
+
const d = paths
|
|
13
|
+
.map((path) => new SVGPathData(path).transform(SVGPathDataTransformer.MATRIX(1, 0, 0, -1, 0, 0)).encode())
|
|
14
|
+
.join(" ");
|
|
15
|
+
const width = Math.max(0.001, bounds.max[0] - bounds.min[0]);
|
|
16
|
+
const height = Math.max(0.001, bounds.max[1] - bounds.min[1]);
|
|
17
|
+
const svg = `<svg xmlns="http://www.w3.org/2000/svg" width="${width}mm" height="${height}mm" viewBox="${bounds.min[0]} ${-bounds.max[1]} ${width} ${height}"><path fill="black" fill-rule="nonzero" d="${d}"/></svg>`;
|
|
18
|
+
if (svg.length > 256 * 1024)
|
|
19
|
+
fail("Text outlines exceed 256 KiB. Use fewer characters or a simpler font.");
|
|
20
|
+
return svg;
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* Parses bounded font bytes using optional fontkit and WOFF2 peers; no font fetching occurs here.
|
|
24
|
+
*
|
|
25
|
+
* @example Prepare a label before building geometry
|
|
26
|
+
* ```ts
|
|
27
|
+
* async function prepareLabel(fontBytes: Uint8Array) {
|
|
28
|
+
* const font = await prepareVectorFont(fontBytes);
|
|
29
|
+
* return font.layout("MetaMesh", { fontSize: 8 });
|
|
30
|
+
* }
|
|
31
|
+
* ```
|
|
32
|
+
*/
|
|
33
|
+
export async function prepareVectorFont(bytes, options = {}) {
|
|
34
|
+
if (bytes.byteLength === 0 || bytes.byteLength > MAX_VECTOR_FONT_BYTES)
|
|
35
|
+
fail("Font files must be non-empty and at most 16 MiB.");
|
|
36
|
+
const staticWeight = options.staticWeight ?? 400;
|
|
37
|
+
if (!Number.isInteger(staticWeight) || staticWeight < 1 || staticWeight > 1000)
|
|
38
|
+
fail("staticWeight must be an integer from 1 through 1000.");
|
|
39
|
+
// Own the original bytes: a caller mutating its buffer must not change later lazy font reads.
|
|
40
|
+
let decoded = Uint8Array.from(bytes);
|
|
41
|
+
if (bytes[0] === 0x77 && bytes[1] === 0x4f && bytes[2] === 0x46 && bytes[3] === 0x32) {
|
|
42
|
+
const { default: decompress } = await import("woff2-encoder/decompress").catch(() => fail("Install woff2-encoder to prepare WOFF2 fonts with @metamesh/modeling/text."));
|
|
43
|
+
decoded = await decompress(decoded);
|
|
44
|
+
if (decoded.byteLength === 0 || decoded.byteLength > 32 * 1024 * 1024)
|
|
45
|
+
fail("The decoded font must be non-empty and at most 32 MiB.");
|
|
46
|
+
}
|
|
47
|
+
const fontkit = await import("fontkit").catch(() => fail("Install fontkit to prepare fonts with @metamesh/modeling/text."));
|
|
48
|
+
const font = fontkit.create(decoded);
|
|
49
|
+
if (!("layout" in font))
|
|
50
|
+
fail("Font collections are not supported.");
|
|
51
|
+
const layout = createVectorTextLayout(font, staticWeight);
|
|
52
|
+
const prepared = {
|
|
53
|
+
layout,
|
|
54
|
+
measure: (text, controls) => layout(text, controls).metrics,
|
|
55
|
+
toSvg: (text, controls) => serializeVectorText(layout(text, controls)),
|
|
56
|
+
};
|
|
57
|
+
return Object.freeze(prepared);
|
|
58
|
+
}
|
|
@@ -0,0 +1,279 @@
|
|
|
1
|
+
import { SVGPathData, SVGPathDataTransformer } from "svg-pathdata";
|
|
2
|
+
const MAX_TEXT_LENGTH = 512;
|
|
3
|
+
const MAX_TEXT_LINES = 16;
|
|
4
|
+
const MAX_TEXT_GLYPHS = 2048;
|
|
5
|
+
const MAX_TEXT_COMMANDS = 32000;
|
|
6
|
+
const MAX_TEXT_PATH_BYTES = 1024 * 1024;
|
|
7
|
+
const supportedText = /^[\p{Script=Latin}\p{Script=Greek}\p{Script=Cyrillic}\p{Script=Han}\p{Script=Hiragana}\p{Script=Katakana}\p{Script=Hangul}\p{Script=Common}\p{Script=Inherited}]*$/u;
|
|
8
|
+
function fail(message) {
|
|
9
|
+
throw new Error(message);
|
|
10
|
+
}
|
|
11
|
+
function checkText(text) {
|
|
12
|
+
if (text.length > MAX_TEXT_LENGTH)
|
|
13
|
+
fail("Text must contain at most 512 characters and 16 lines.");
|
|
14
|
+
const lines = text.replaceAll("\r\n", "\n").replaceAll("\r", "\n").split("\n");
|
|
15
|
+
if (lines.length > MAX_TEXT_LINES)
|
|
16
|
+
fail("Text must contain at most 512 characters and 16 lines.");
|
|
17
|
+
if (!supportedText.test(text) ||
|
|
18
|
+
/\p{Emoji_Presentation}|\p{Variation_Selector}|[\u200c-\u200f\u202a-\u202e\u2066-\u2069]/u.test(text))
|
|
19
|
+
fail("This writing system or text direction is not supported yet. Use Latin, Greek, Cyrillic, or horizontal CJK text.");
|
|
20
|
+
if (lines.some((line) => /\p{Control}/u.test(line)))
|
|
21
|
+
fail("Control characters such as tabs are not supported in text layout.");
|
|
22
|
+
return lines;
|
|
23
|
+
}
|
|
24
|
+
function normalizeControls(options, staticWeight) {
|
|
25
|
+
const controls = {
|
|
26
|
+
fontSize: options.fontSize,
|
|
27
|
+
fontWeight: options.fontWeight ?? staticWeight,
|
|
28
|
+
letterSpacing: options.letterSpacing ?? 0,
|
|
29
|
+
align: options.align ?? "left",
|
|
30
|
+
verticalAlign: options.verticalAlign ?? "baseline",
|
|
31
|
+
lineHeight: options.lineHeight,
|
|
32
|
+
};
|
|
33
|
+
if (!Number.isFinite(controls.fontSize) || controls.fontSize <= 0 || controls.fontSize > 10000)
|
|
34
|
+
fail("fontSize must be positive and at most 10000 mm.");
|
|
35
|
+
if (!Number.isInteger(controls.fontWeight) ||
|
|
36
|
+
controls.fontWeight < 1 ||
|
|
37
|
+
controls.fontWeight > 1000)
|
|
38
|
+
fail("fontWeight must be an integer from 1 through 1000.");
|
|
39
|
+
if (!Number.isFinite(controls.letterSpacing) || Math.abs(controls.letterSpacing) > 100)
|
|
40
|
+
fail("letterSpacing must be finite and between -100 and 100 mm.");
|
|
41
|
+
if (controls.align !== "left" && controls.align !== "center" && controls.align !== "right")
|
|
42
|
+
fail("Invalid text alignment.");
|
|
43
|
+
if (controls.verticalAlign !== "baseline" &&
|
|
44
|
+
controls.verticalAlign !== "top" &&
|
|
45
|
+
controls.verticalAlign !== "center" &&
|
|
46
|
+
controls.verticalAlign !== "bottom")
|
|
47
|
+
fail("Invalid vertical text alignment.");
|
|
48
|
+
if (controls.lineHeight !== undefined &&
|
|
49
|
+
(!Number.isFinite(controls.lineHeight) || controls.lineHeight < 0.5 || controls.lineHeight > 5))
|
|
50
|
+
fail("lineHeight must be between 0.5 and 5.");
|
|
51
|
+
return controls;
|
|
52
|
+
}
|
|
53
|
+
function selectFont(font, weight, staticWeight) {
|
|
54
|
+
const axis = font.variationAxes.wght;
|
|
55
|
+
if (axis === undefined) {
|
|
56
|
+
if (weight !== staticWeight)
|
|
57
|
+
fail("The selected static face does not support this weight.");
|
|
58
|
+
return font;
|
|
59
|
+
}
|
|
60
|
+
if (weight < axis.min || weight > axis.max)
|
|
61
|
+
fail(`fontWeight must be between ${axis.min} and ${axis.max} for this font.`);
|
|
62
|
+
return font.getVariation({
|
|
63
|
+
...Object.fromEntries(Object.entries(font.variationAxes).flatMap(([tag, value]) => value === undefined ? [] : [[tag, value.default]])),
|
|
64
|
+
wght: weight,
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
function readFontMetrics(font) {
|
|
68
|
+
if (!Number.isFinite(font.unitsPerEm) || font.unitsPerEm <= 0)
|
|
69
|
+
fail("The font has invalid units-per-em metadata.");
|
|
70
|
+
// OpenType USE_TYPO_METRICS selects OS/2 values; fontkit's public getters always read hhea.
|
|
71
|
+
const os2 = "OS/2" in font ? font["OS/2"] : undefined;
|
|
72
|
+
let { ascent, descent, lineGap } = font;
|
|
73
|
+
if (typeof os2 === "object" &&
|
|
74
|
+
os2 !== null &&
|
|
75
|
+
"fsSelection" in os2 &&
|
|
76
|
+
typeof os2.fsSelection === "object" &&
|
|
77
|
+
os2.fsSelection !== null &&
|
|
78
|
+
"useTypoMetrics" in os2.fsSelection &&
|
|
79
|
+
os2.fsSelection.useTypoMetrics === true) {
|
|
80
|
+
if (!("typoAscender" in os2) ||
|
|
81
|
+
typeof os2.typoAscender !== "number" ||
|
|
82
|
+
!("typoDescender" in os2) ||
|
|
83
|
+
typeof os2.typoDescender !== "number" ||
|
|
84
|
+
!("typoLineGap" in os2) ||
|
|
85
|
+
typeof os2.typoLineGap !== "number")
|
|
86
|
+
fail("The selected font has invalid typographic metrics.");
|
|
87
|
+
ascent = os2.typoAscender;
|
|
88
|
+
descent = os2.typoDescender;
|
|
89
|
+
lineGap = os2.typoLineGap;
|
|
90
|
+
}
|
|
91
|
+
const lineHeight = ascent - descent + Math.max(0, lineGap);
|
|
92
|
+
if (![ascent, descent, lineGap, lineHeight].every(Number.isFinite) ||
|
|
93
|
+
ascent < 0 ||
|
|
94
|
+
descent > 0 ||
|
|
95
|
+
lineHeight <= 0)
|
|
96
|
+
fail("The selected font has invalid vertical metrics.");
|
|
97
|
+
return { ascent, descent: -descent, lineHeight };
|
|
98
|
+
}
|
|
99
|
+
function shapeLines(font, lines, controls) {
|
|
100
|
+
const scale = controls.fontSize / font.unitsPerEm;
|
|
101
|
+
let glyphs = 0;
|
|
102
|
+
return lines.map((line) => {
|
|
103
|
+
for (const character of line) {
|
|
104
|
+
const code = character.codePointAt(0);
|
|
105
|
+
if (!font.hasGlyphForCodePoint(code))
|
|
106
|
+
fail(`The selected font does not contain glyph U+${code.toString(16).toUpperCase().padStart(4, "0")}.`);
|
|
107
|
+
}
|
|
108
|
+
// CSS tracking disables optional ligatures, retaining required shaping and combining marks.
|
|
109
|
+
const run = font.layout(line, controls.letterSpacing === 0 ? undefined : { liga: false, clig: false });
|
|
110
|
+
glyphs += run.glyphs.length;
|
|
111
|
+
if (run.glyphs.length !== run.positions.length || glyphs > MAX_TEXT_GLYPHS)
|
|
112
|
+
fail("The font returned an invalid or excessive glyph run.");
|
|
113
|
+
let advance = 0;
|
|
114
|
+
let spacingSteps = -1;
|
|
115
|
+
for (let index = 0; index < run.glyphs.length; index++) {
|
|
116
|
+
if (run.glyphs[index].id === 0)
|
|
117
|
+
fail("The selected font shaped a missing glyph.");
|
|
118
|
+
const position = run.positions[index];
|
|
119
|
+
if (![position.xAdvance, position.yAdvance, position.xOffset, position.yOffset].every(Number.isFinite))
|
|
120
|
+
fail("The font returned a non-finite glyph position.");
|
|
121
|
+
if (index === 0 || position.xAdvance !== 0)
|
|
122
|
+
spacingSteps++;
|
|
123
|
+
advance += position.xAdvance;
|
|
124
|
+
}
|
|
125
|
+
const advanceWidth = advance * scale + Math.max(0, spacingSteps) * controls.letterSpacing;
|
|
126
|
+
if (!Number.isFinite(advanceWidth) || advanceWidth < 0)
|
|
127
|
+
fail("The shaped text advance must be finite and non-negative after letter spacing.");
|
|
128
|
+
return { run, advanceWidth };
|
|
129
|
+
});
|
|
130
|
+
}
|
|
131
|
+
function convertCommand({ command, args }) {
|
|
132
|
+
const argumentCount = command === "closePath"
|
|
133
|
+
? 0
|
|
134
|
+
: command === "quadraticCurveTo"
|
|
135
|
+
? 4
|
|
136
|
+
: command === "bezierCurveTo"
|
|
137
|
+
? 6
|
|
138
|
+
: 2;
|
|
139
|
+
if (args.length !== argumentCount || !args.every(Number.isFinite))
|
|
140
|
+
fail("The font returned invalid glyph path coordinates.");
|
|
141
|
+
switch (command) {
|
|
142
|
+
case "moveTo":
|
|
143
|
+
return { type: SVGPathData.MOVE_TO, relative: false, x: args[0], y: args[1] };
|
|
144
|
+
case "lineTo":
|
|
145
|
+
return { type: SVGPathData.LINE_TO, relative: false, x: args[0], y: args[1] };
|
|
146
|
+
case "quadraticCurveTo":
|
|
147
|
+
return {
|
|
148
|
+
type: SVGPathData.QUAD_TO,
|
|
149
|
+
relative: false,
|
|
150
|
+
x1: args[0],
|
|
151
|
+
y1: args[1],
|
|
152
|
+
x: args[2],
|
|
153
|
+
y: args[3],
|
|
154
|
+
};
|
|
155
|
+
case "bezierCurveTo":
|
|
156
|
+
return {
|
|
157
|
+
type: SVGPathData.CURVE_TO,
|
|
158
|
+
relative: false,
|
|
159
|
+
x1: args[0],
|
|
160
|
+
y1: args[1],
|
|
161
|
+
x2: args[2],
|
|
162
|
+
y2: args[3],
|
|
163
|
+
x: args[4],
|
|
164
|
+
y: args[5],
|
|
165
|
+
};
|
|
166
|
+
case "closePath":
|
|
167
|
+
return { type: SVGPathData.CLOSE_PATH };
|
|
168
|
+
default:
|
|
169
|
+
return fail("The font returned an unsupported glyph path command.");
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
function freezeBounds(minX, minY, maxX, maxY) {
|
|
173
|
+
return Object.freeze({
|
|
174
|
+
min: Object.freeze([minX, minY]),
|
|
175
|
+
max: Object.freeze([maxX, maxY]),
|
|
176
|
+
});
|
|
177
|
+
}
|
|
178
|
+
function buildLayout(font, fontMetrics, lines, controls) {
|
|
179
|
+
const scale = controls.fontSize / font.unitsPerEm;
|
|
180
|
+
const lineHeight = controls.lineHeight === undefined
|
|
181
|
+
? fontMetrics.lineHeight * scale
|
|
182
|
+
: controls.lineHeight * controls.fontSize;
|
|
183
|
+
const shaped = shapeLines(font, lines, controls);
|
|
184
|
+
const paths = [];
|
|
185
|
+
let minX = Infinity;
|
|
186
|
+
let minY = Infinity;
|
|
187
|
+
let maxX = -Infinity;
|
|
188
|
+
let maxY = -Infinity;
|
|
189
|
+
let commands = 0;
|
|
190
|
+
for (let lineIndex = 0; lineIndex < shaped.length; lineIndex++) {
|
|
191
|
+
const { run, advanceWidth } = shaped[lineIndex];
|
|
192
|
+
const lineX = controls.align === "center"
|
|
193
|
+
? -advanceWidth / 2
|
|
194
|
+
: controls.align === "right"
|
|
195
|
+
? -advanceWidth
|
|
196
|
+
: 0;
|
|
197
|
+
let x = 0;
|
|
198
|
+
let y = 0;
|
|
199
|
+
let spacingSteps = -1;
|
|
200
|
+
for (let index = 0; index < run.glyphs.length; index++) {
|
|
201
|
+
const position = run.positions[index];
|
|
202
|
+
// Zero-advance marks retain their base glyph's tracking offset.
|
|
203
|
+
if (index === 0 || position.xAdvance !== 0)
|
|
204
|
+
spacingSteps++;
|
|
205
|
+
const source = run.glyphs[index].path.commands;
|
|
206
|
+
commands += source.length;
|
|
207
|
+
if (commands > MAX_TEXT_COMMANDS)
|
|
208
|
+
fail("Text outlines exceed the supported complexity. Use fewer characters or a simpler font.");
|
|
209
|
+
if (source.length > 0) {
|
|
210
|
+
// Preserve full precision: fontkit's toSVG() rounds before scaling and reparsing it wastes work.
|
|
211
|
+
const path = new SVGPathData(source.map(convertCommand)).transform(SVGPathDataTransformer.MATRIX(scale, 0, 0, scale, (x + position.xOffset) * scale + spacingSteps * controls.letterSpacing + lineX, (y + position.yOffset) * scale - lineIndex * lineHeight));
|
|
212
|
+
const bounds = path.getBounds();
|
|
213
|
+
if (![bounds.minX, bounds.minY, bounds.maxX, bounds.maxY].every(Number.isFinite))
|
|
214
|
+
fail("The font returned invalid glyph outline bounds.");
|
|
215
|
+
minX = Math.min(minX, bounds.minX);
|
|
216
|
+
minY = Math.min(minY, bounds.minY);
|
|
217
|
+
maxX = Math.max(maxX, bounds.maxX);
|
|
218
|
+
maxY = Math.max(maxY, bounds.maxY);
|
|
219
|
+
paths.push(path);
|
|
220
|
+
}
|
|
221
|
+
x += position.xAdvance;
|
|
222
|
+
y += position.yAdvance;
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
const offsetY = paths.length === 0 || controls.verticalAlign === "baseline"
|
|
226
|
+
? 0
|
|
227
|
+
: controls.verticalAlign === "top"
|
|
228
|
+
? -maxY
|
|
229
|
+
: controls.verticalAlign === "bottom"
|
|
230
|
+
? -minY
|
|
231
|
+
: -(minY + maxY) / 2;
|
|
232
|
+
let bytes = 0;
|
|
233
|
+
const encoded = paths.map((path) => {
|
|
234
|
+
if (offsetY !== 0)
|
|
235
|
+
path.transform(SVGPathDataTransformer.TRANSLATE(0, offsetY));
|
|
236
|
+
const result = path.encode();
|
|
237
|
+
// Path commands and finite numeric coordinates are ASCII, so length is the encoded byte count.
|
|
238
|
+
bytes += result.length;
|
|
239
|
+
if (bytes > MAX_TEXT_PATH_BYTES)
|
|
240
|
+
fail("Text outlines exceed 1 MiB. Use fewer characters or a simpler font.");
|
|
241
|
+
return result;
|
|
242
|
+
});
|
|
243
|
+
const metrics = Object.freeze({
|
|
244
|
+
bounds: paths.length === 0 ? null : freezeBounds(minX, minY + offsetY, maxX, maxY + offsetY),
|
|
245
|
+
advance: Object.freeze([
|
|
246
|
+
shaped.at(-1).advanceWidth,
|
|
247
|
+
lines.length === 1 ? 0 : -(lines.length - 1) * lineHeight,
|
|
248
|
+
]),
|
|
249
|
+
logicalWidth: Math.max(...shaped.map((line) => line.advanceWidth)),
|
|
250
|
+
ascent: fontMetrics.ascent * scale,
|
|
251
|
+
descent: fontMetrics.descent * scale,
|
|
252
|
+
lineHeight,
|
|
253
|
+
lineCount: lines.length,
|
|
254
|
+
});
|
|
255
|
+
return Object.freeze({ paths: Object.freeze(encoded), metrics });
|
|
256
|
+
}
|
|
257
|
+
/** One retained variation and one immutable layout bound work and memory per prepared font. */
|
|
258
|
+
export function createVectorTextLayout(font, staticWeight) {
|
|
259
|
+
let selected;
|
|
260
|
+
let previous;
|
|
261
|
+
return (text, options) => {
|
|
262
|
+
const lines = checkText(text);
|
|
263
|
+
const controls = normalizeControls(options, staticWeight);
|
|
264
|
+
const key = JSON.stringify([lines, controls]);
|
|
265
|
+
if (previous?.key === key)
|
|
266
|
+
return previous.layout;
|
|
267
|
+
if (selected?.weight !== controls.fontWeight) {
|
|
268
|
+
const variation = selectFont(font, controls.fontWeight, staticWeight);
|
|
269
|
+
selected = {
|
|
270
|
+
weight: controls.fontWeight,
|
|
271
|
+
font: variation,
|
|
272
|
+
metrics: readFontMetrics(variation),
|
|
273
|
+
};
|
|
274
|
+
}
|
|
275
|
+
const layout = buildLayout(selected.font, selected.metrics, lines, controls);
|
|
276
|
+
previous = { key, layout };
|
|
277
|
+
return layout;
|
|
278
|
+
};
|
|
279
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,103 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@metamesh/modeling",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"
|
|
5
|
-
"
|
|
3
|
+
"version": "0.7.0",
|
|
4
|
+
"description": "Scoped solid modeling and pure geometry foundations for Node and browsers.",
|
|
5
|
+
"license": "Apache-2.0",
|
|
6
|
+
"repository": {
|
|
7
|
+
"type": "git",
|
|
8
|
+
"url": "git+https://github.com/JasonKoolman/metamesh.git",
|
|
9
|
+
"directory": "packages/modeling"
|
|
10
|
+
},
|
|
11
|
+
"homepage": "https://github.com/JasonKoolman/metamesh#readme",
|
|
12
|
+
"bugs": {
|
|
13
|
+
"url": "https://github.com/JasonKoolman/metamesh/issues"
|
|
14
|
+
},
|
|
15
|
+
"type": "module",
|
|
16
|
+
"sideEffects": false,
|
|
17
|
+
"files": [
|
|
18
|
+
"CHANGELOG.md",
|
|
19
|
+
"dist/**/*.js",
|
|
20
|
+
"dist/api/*.d.ts",
|
|
21
|
+
"THIRD_PARTY_NOTICES.md",
|
|
22
|
+
"dist/**/*.wasm",
|
|
23
|
+
"dist/engine/kernel/LICENSE",
|
|
24
|
+
"!dist/geometry-module.js",
|
|
25
|
+
"!dist/modeling-module.js"
|
|
26
|
+
],
|
|
27
|
+
"exports": {
|
|
28
|
+
".": {
|
|
29
|
+
"types": "./dist/api/index.d.ts",
|
|
30
|
+
"import": "./dist/index.js"
|
|
31
|
+
},
|
|
32
|
+
"./geometry": {
|
|
33
|
+
"types": "./dist/api/geometry.d.ts",
|
|
34
|
+
"import": "./dist/geometry/index.js"
|
|
35
|
+
},
|
|
36
|
+
"./engine": {
|
|
37
|
+
"types": "./dist/api/engine.d.ts",
|
|
38
|
+
"import": "./dist/engine/index.js"
|
|
39
|
+
},
|
|
40
|
+
"./text": {
|
|
41
|
+
"types": "./dist/api/text.d.ts",
|
|
42
|
+
"import": "./dist/text/index.js"
|
|
43
|
+
}
|
|
44
|
+
},
|
|
45
|
+
"publishConfig": {
|
|
46
|
+
"access": "public",
|
|
47
|
+
"registry": "https://registry.npmjs.org/"
|
|
48
|
+
},
|
|
49
|
+
"dependencies": {
|
|
50
|
+
"polygon-clipping": "^0.15.7",
|
|
51
|
+
"saxes": "^6.0.0",
|
|
52
|
+
"svg-pathdata": "^9.0.0",
|
|
53
|
+
"fflate": "^0.8.3"
|
|
54
|
+
},
|
|
55
|
+
"devDependencies": {
|
|
56
|
+
"@metamesh/config": "0.0.0",
|
|
57
|
+
"@microsoft/api-extractor": "^7.59.4",
|
|
58
|
+
"@types/fontkit": "^2.0.9",
|
|
59
|
+
"@vitest/coverage-v8": "^5.0.3",
|
|
60
|
+
"esbuild": "^0.28.2",
|
|
61
|
+
"fast-check": "4.10.2",
|
|
62
|
+
"typedoc": "0.28.20",
|
|
63
|
+
"typedoc-plugin-markdown": "4.13.1",
|
|
64
|
+
"typescript": "6.0.3",
|
|
65
|
+
"vitest": "^5.0.3",
|
|
66
|
+
"manifold-3d": "3.5.4",
|
|
67
|
+
"fontkit": "^2.0.4",
|
|
68
|
+
"woff2-encoder": "2.0.0"
|
|
69
|
+
},
|
|
70
|
+
"peerDependencies": {
|
|
71
|
+
"fontkit": "^2.0.4",
|
|
72
|
+
"woff2-encoder": "2.0.0"
|
|
73
|
+
},
|
|
74
|
+
"peerDependenciesMeta": {
|
|
75
|
+
"fontkit": {
|
|
76
|
+
"optional": true
|
|
77
|
+
},
|
|
78
|
+
"woff2-encoder": {
|
|
79
|
+
"optional": true
|
|
80
|
+
}
|
|
81
|
+
},
|
|
82
|
+
"scripts": {
|
|
83
|
+
"api:extract:check": "api-extractor run --config config/api-extractor.root.json && api-extractor run --config config/api-extractor.geometry.json && api-extractor run --config config/api-extractor.engine.json && api-extractor run --config config/api-extractor.text.json && node scripts/build-engine-declarations.mjs && node scripts/check-model-declarations.mjs",
|
|
84
|
+
"api:extract:update": "api-extractor run --local --config config/api-extractor.root.json && api-extractor run --local --config config/api-extractor.geometry.json && api-extractor run --local --config config/api-extractor.engine.json && api-extractor run --local --config config/api-extractor.text.json && node scripts/build-engine-declarations.mjs && node scripts/check-model-declarations.mjs",
|
|
85
|
+
"build": "pnpm run build:declarations && pnpm run api:extract:check && pnpm run runtime:verify",
|
|
86
|
+
"build:update-api": "pnpm run build:declarations && pnpm run api:extract:update && pnpm run runtime:verify",
|
|
87
|
+
"build:declarations": "node ../../scripts/clean-dist.mjs && node scripts/build-kernel-assets.mjs && tsc -p tsconfig.build.json && node scripts/build-geometry-module.mjs && node scripts/build-modeling-module.mjs",
|
|
88
|
+
"coverage": "node scripts/build-kernel-assets.mjs && vitest run src --coverage",
|
|
89
|
+
"docs": "pnpm run build && typedoc --options typedoc.json",
|
|
90
|
+
"docs:reference:generate": "pnpm run build:update-api && pnpm run docs:reference:test && node scripts/generate-reference-docs.mjs",
|
|
91
|
+
"docs:reference:check": "pnpm run build && pnpm run docs:reference:verify",
|
|
92
|
+
"docs:reference:test": "node --test scripts/reference-model.spec.mjs",
|
|
93
|
+
"docs:reference:verify": "pnpm run docs:reference:test && node scripts/generate-reference-docs.mjs --check",
|
|
94
|
+
"fixtures:check": "pnpm run build && pnpm run fixtures:verify",
|
|
95
|
+
"fixtures:verify": "tsc -p fixtures/tsconfig.json --pretty false && node scripts/check-pure-virtual-module.mjs",
|
|
96
|
+
"lint": "node scripts/check-import-boundaries.mjs && eslint .",
|
|
97
|
+
"runtime:verify": "node scripts/check-runtime-entrypoints.mjs",
|
|
98
|
+
"test": "node scripts/build-kernel-assets.mjs && vitest run src",
|
|
99
|
+
"test:mesh-snapshots": "vitest run src/geometry/mechanical/mesh-regression.spec.ts",
|
|
100
|
+
"test:mesh-snapshots:update": "vitest run src/geometry/mechanical/mesh-regression.spec.ts -u",
|
|
101
|
+
"typecheck": "tsc --noEmit --pretty false"
|
|
102
|
+
}
|
|
6
103
|
}
|