partforge 0.67.1 → 0.67.3
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/package.json +1 -1
- package/src/framework/geometry/contour-offset.js +39 -3
- package/src/framework/geometry/kernel-front.js +6 -11
- package/src/framework/geometry/opentype-interop.js +35 -0
- package/src/framework/jobs.js +6 -3
- package/src/testing/manifold.js +3 -2
- package/src/testing/occt.js +3 -2
package/package.json
CHANGED
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
// The cubic subdivision approach is ported from glenzli/paperjs-offset
|
|
9
9
|
// (https://github.com/glenzli/paperjs-offset, MIT License, Copyright (c) glenzli),
|
|
10
10
|
// adapted from paper.js Segments to the partforge contour IR.
|
|
11
|
-
import { arcCenterAndSweep } from "./paper-bridge.js";
|
|
11
|
+
import { arcCenterAndSweep, booleanRegions } from "./paper-bridge.js";
|
|
12
12
|
import { cubicAt, splitCubic, jointTangents, SMOOTH_JOINT_DEG } from "./contour-ops.js";
|
|
13
13
|
import { tessellateContour, closeContourGap } from "./profile.js";
|
|
14
14
|
import { ringArea, pointInRing } from "./shape2d-regions.js";
|
|
@@ -718,7 +718,11 @@ const flattenRing = (contour, segs) => {
|
|
|
718
718
|
// All seven rescues are oracle-checked: median area error 0.0972 %, worst 1.663 %, with zero
|
|
719
719
|
// region-count losses and zero complete arc losses. The ladder stays because those seven raw
|
|
720
720
|
// arrangements remain numerically unclosable, not because the formerly parked comb/text
|
|
721
|
-
// failures still exist.
|
|
721
|
+
// failures still exist. Those seven are all erosion (negative delta) or single-region cases;
|
|
722
|
+
// the per-region rung below is positive-delta-and-multi-region only, so it wins none of them
|
|
723
|
+
// and the rates above are unchanged by its addition — its own coverage class (whole-word text
|
|
724
|
+
// dilation, feedback 86970b00) sits outside this corpus, whose glyphs are single characters
|
|
725
|
+
// and whose "Scott" case never reaches the delta band where the merged word fails to close.
|
|
722
726
|
//
|
|
723
727
|
// Rung ORDER is by fidelity of what survives, not by hit rate:
|
|
724
728
|
// 1. delta perturbed by ±1e-9 relative. Escapes an exactly-degenerate arrangement (two
|
|
@@ -729,7 +733,13 @@ const flattenRing = (contour, segs) => {
|
|
|
729
733
|
// that radius apart onto one vertex. This can merge a genuine severing pinch, so the
|
|
730
734
|
// 20x rung is not widened further even though the current seven rescues preserve the
|
|
731
735
|
// oracle's region count.
|
|
732
|
-
// 3.
|
|
736
|
+
// 3. per-region-union (delta > 0, more than one region only). Offsets each region alone and
|
|
737
|
+
// unites the results through paper's curve-native boolean. EXACT, not approximate: a
|
|
738
|
+
// positive dilation distributes over union, so this equals the whole-region offset the
|
|
739
|
+
// resolver could not close — with arcs intact, above the polyline rungs. It is here rather
|
|
740
|
+
// than at #1 only because it costs a boolean per region and re-runs the earlier rungs on
|
|
741
|
+
// each; the two cheaper exact rungs get first refusal.
|
|
742
|
+
// 4. the raw outline re-run as polylines (64/256/1024 facets per turn). Geometrically
|
|
733
743
|
// faithful to the chord error of that tessellation, but it DEGRADES THE IR: round joins
|
|
734
744
|
// come back as chords, so A STEP EXPORT OF A POLYLINE-RUNG RESULT LOSES ITS TRUE CIRCLES.
|
|
735
745
|
// No current corpus rescue loses every arc, but these rungs remain last because that
|
|
@@ -753,6 +763,25 @@ const flattenRing = (contour, segs) => {
|
|
|
753
763
|
// (±1e-4 and ±1e-3 mm absolute rungs) was measured too: it bought ONE extra case out of 62 and
|
|
754
764
|
// more than doubled the worst absolute error, 0.048 → 0.112 mm², so it is not here either.
|
|
755
765
|
//
|
|
766
|
+
// Offset each region on its own and unite the results through paper's planar boolean engine.
|
|
767
|
+
// This is EXACT for a positive delta, not an approximation: Minkowski dilation distributes
|
|
768
|
+
// over union, (⋃ Rᵢ) ⊕ B = ⋃ (Rᵢ ⊕ B), so the whole-region offset the winding resolver cannot
|
|
769
|
+
// close as one merged arrangement equals the union of the single-region offsets. Each single
|
|
770
|
+
// region is a far simpler arrangement — a lone glyph rather than a whole word's worth of offset
|
|
771
|
+
// walls meeting near-tangentially — and its own base offset still gets the earlier rungs'
|
|
772
|
+
// rescues, because the per-region call re-enters the public offsetRegions (which runs its own
|
|
773
|
+
// ladder for that one region). booleanRegions unites through paper's CURVE-native engine rather
|
|
774
|
+
// than a tessellation, so arcs stay arcs and a STEP export keeps its true circles — which is why
|
|
775
|
+
// this rung sits ABOVE the polyline rungs, whose chord approximation is the fidelity floor.
|
|
776
|
+
function perRegionUnion(regions, delta, corners) {
|
|
777
|
+
let out = [];
|
|
778
|
+
for (const rg of regions) {
|
|
779
|
+
const one = offsetRegions([rg], delta, { corners }); // single region: never re-enters this rung
|
|
780
|
+
out = out.length ? booleanRegions(out, one, "unite") : one;
|
|
781
|
+
}
|
|
782
|
+
return out;
|
|
783
|
+
}
|
|
784
|
+
|
|
756
785
|
// The ladder as named, LAZY rungs — one list, walked by chainFallback below and by
|
|
757
786
|
// scripts/offset-rates.mjs, so a measurement of "what each rung costs" can never drift from
|
|
758
787
|
// the ladder that actually ships. Every rung's whole body (including tessellating the outline
|
|
@@ -765,6 +794,13 @@ export function _ladderRungs(regions, raw, delta, corners) {
|
|
|
765
794
|
run: () => resolveOrRaw(rawOffset(regions, delta * (1 + sign * 1e-9), corners)) })),
|
|
766
795
|
...[4, 20].map((mult) => ({ name: `clusterTol*${mult}`,
|
|
767
796
|
run: () => resolveOffsetWinding(raw, { clusterTol: CLUSTER_TOL * mult }) })),
|
|
797
|
+
// Exact for positive dilation and arc-preserving, so it ranks above the polyline rungs but
|
|
798
|
+
// below the two cheaper exact rungs that need no boolean. Only meaningful when there is more
|
|
799
|
+
// than one region to decompose, and only distributive for a positive delta; the guard is
|
|
800
|
+
// also what bounds the recursion — a single-region offsetRegions call never reaches here.
|
|
801
|
+
...(delta > 0 && regions.length > 1
|
|
802
|
+
? [{ name: "per-region-union", run: () => perRegionUnion(regions, delta, corners) }]
|
|
803
|
+
: []),
|
|
768
804
|
...[64, 256, 1024].map((segs) => ({ name: `polyline@${segs}`,
|
|
769
805
|
run: () => resolveOffsetWinding(raw.map((rg) => ({ outer: flattenRing(rg.outer, segs),
|
|
770
806
|
holes: rg.holes.map((h) => flattenRing(h, segs)) }))) })),
|
|
@@ -12,16 +12,11 @@
|
|
|
12
12
|
// capability (Manifold can't do toSTEP; both backends now define shape2d, so
|
|
13
13
|
// that stub is dead in practice — kept as a safety net for a future backend).
|
|
14
14
|
// The per-Solid twin of this layer is addSugar() in solid-sugar.js.
|
|
15
|
-
// opentype.js
|
|
16
|
-
//
|
|
17
|
-
// exports Node cannot statically detect — the namespace holds only `default`).
|
|
18
|
-
// So `import * as opentype` gives a working `.parse` in the browser and `undefined`
|
|
19
|
-
// under Node, which broke every headless text2d build (`opentype.parse is not a
|
|
20
|
-
// function`) while the browser stayed green. Normalize both interop shapes here.
|
|
15
|
+
// opentype.js's namespace shape differs between bundler and Node resolution —
|
|
16
|
+
// see opentype-interop.js for the trap (it has bitten once in each direction).
|
|
21
17
|
import * as opentypeNamespace from "opentype.js";
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
: (opentypeNamespace.default ?? opentypeNamespace);
|
|
18
|
+
import { normalizeOpentype, parseFont } from "./opentype-interop.js";
|
|
19
|
+
const opentype = normalizeOpentype(opentypeNamespace);
|
|
25
20
|
import { KernelCapabilityError } from "./errors.js";
|
|
26
21
|
import { isPlainOptions, KERNEL_OP_SPECS } from "./op-options.js";
|
|
27
22
|
import { textGlyphs } from "./text2d.js";
|
|
@@ -100,7 +95,7 @@ export function finishKernel(k) {
|
|
|
100
95
|
// EXACT byte range — arg.buffer alone spans the whole (possibly pooled) backing
|
|
101
96
|
// buffer, which would feed opentype garbage for a byteOffset>0 view.
|
|
102
97
|
const buf = ArrayBuffer.isView(arg) ? arg.buffer.slice(arg.byteOffset, arg.byteOffset + arg.byteLength) : arg;
|
|
103
|
-
f = opentype
|
|
98
|
+
f = parseFont(opentype, buf); byteCache.set(arg, f);
|
|
104
99
|
}
|
|
105
100
|
return f;
|
|
106
101
|
};
|
|
@@ -114,7 +109,7 @@ export function finishKernel(k) {
|
|
|
114
109
|
// guards against that ever changing.
|
|
115
110
|
if (!k._defaultFont) {
|
|
116
111
|
const { buffer, byteOffset, byteLength } = DEFAULT_FONT_BYTES;
|
|
117
|
-
k._defaultFont = opentype
|
|
112
|
+
k._defaultFont = parseFont(opentype, buffer.slice(byteOffset, byteOffset + byteLength), "the bundled default");
|
|
118
113
|
}
|
|
119
114
|
return k._defaultFont;
|
|
120
115
|
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
// opentype.js 2.x ships no `exports` map, so what importing it yields splits by
|
|
2
|
+
// resolver: bundlers take the `module` field (real ESM — named `parse`, NO
|
|
3
|
+
// default export), while Node takes `main` (a UMD/CJS bundle whose named exports
|
|
4
|
+
// Node's lexer cannot statically detect — the namespace holds ONLY `default`).
|
|
5
|
+
// Reading `.default` unconditionally is therefore correct under Node and
|
|
6
|
+
// `undefined` in every browser bundle; reading a named export is the reverse.
|
|
7
|
+
// That asymmetry once broke headless text2d builds (kernel-front's static
|
|
8
|
+
// import) and later broke every BROWSER build of a part declaring `fonts`
|
|
9
|
+
// ("undefined is not an object (evaluating 'p.parse')") while headless tests
|
|
10
|
+
// stayed green (jobs' dynamic import). Both call sites normalize through this
|
|
11
|
+
// one function so the two interop shapes stay handled in one place.
|
|
12
|
+
export const normalizeOpentype = (ns) =>
|
|
13
|
+
typeof ns?.parse === "function" ? ns : (ns?.default ?? ns);
|
|
14
|
+
|
|
15
|
+
// Parse font bytes into an opentype.Font, turning opentype.js's own low-level parse
|
|
16
|
+
// failures into a NAMED, actionable error. A single unreadable font in a part's `fonts`
|
|
17
|
+
// map otherwise kills the whole build with a message that names neither the font nor the
|
|
18
|
+
// fix — a RangeError deep in the TrueType reader, or opentype.js's raw "WOFF2 require an
|
|
19
|
+
// external decompressor library" URL — and the part just "won't build" with no clue which
|
|
20
|
+
// font or why. This is the exact dead end a variable font or a WOFF/WOFF2 upload lands in:
|
|
21
|
+
// opentype.js 2.x reads neither, so the guidance is always the same (supply a static TTF or
|
|
22
|
+
// OTF). `label` is the declared font name where one is known (the `fonts` map key), and is
|
|
23
|
+
// omitted for an inline-bytes font, which has no name to give. All parse sites route through
|
|
24
|
+
// here so the message stays in one place.
|
|
25
|
+
export function parseFont(opentype, buf, label) {
|
|
26
|
+
try {
|
|
27
|
+
return opentype.parse(buf);
|
|
28
|
+
} catch (err) {
|
|
29
|
+
const who = label ? `font "${label}"` : "an inline font";
|
|
30
|
+
throw new Error(
|
|
31
|
+
`text2d: ${who} could not be read as a TTF or OTF — a variable font or a WOFF/WOFF2 ` +
|
|
32
|
+
`file will fail here; supply a static TTF or OTF instead. (${err?.message ?? err})`,
|
|
33
|
+
);
|
|
34
|
+
}
|
|
35
|
+
}
|
package/src/framework/jobs.js
CHANGED
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
import { meshTo3MF } from "./geometry/threemf.js";
|
|
6
6
|
import { exportablePartNames } from "./export-select.js";
|
|
7
7
|
import { resolveFonts } from "./fonts.js";
|
|
8
|
+
import { normalizeOpentype, parseFont } from "./geometry/opentype-interop.js";
|
|
8
9
|
import { ensureImports, resolveImports } from "./imports.js";
|
|
9
10
|
import { safeName } from "./safe-name.js";
|
|
10
11
|
import { exportSubParts, resolveParams, buildPosed } from "./part-model.js";
|
|
@@ -100,11 +101,13 @@ export async function handle(kernel, part, msg, post, opts = {}) {
|
|
|
100
101
|
try {
|
|
101
102
|
// Preload any part-declared fonts into the kernel before building — once per
|
|
102
103
|
// font name; a lazy dynamic import because this is async context (unlike the
|
|
103
|
-
// synchronous kernel-front), so it doesn't cost sync callers anything.
|
|
104
|
+
// synchronous kernel-front), so it doesn't cost sync callers anything. The
|
|
105
|
+
// namespace shape differs between bundler and Node resolution (a bare
|
|
106
|
+
// `.default` here is undefined in every browser bundle) — normalize it.
|
|
104
107
|
if (part.fonts && kernel._fonts) {
|
|
105
|
-
const opentype = (await import("opentype.js"))
|
|
108
|
+
const opentype = normalizeOpentype(await import("opentype.js"));
|
|
106
109
|
const bufs = await resolveFonts(part.fonts);
|
|
107
|
-
for (const [name, buf] of bufs) if (!kernel._fonts.has(name)) kernel._fonts.set(name, opentype
|
|
110
|
+
for (const [name, buf] of bufs) if (!kernel._fonts.has(name)) kernel._fonts.set(name, parseFont(opentype, buf, name));
|
|
108
111
|
}
|
|
109
112
|
// Register this part's declared imports on the kernel running this job — the
|
|
110
113
|
// import-asset sibling of the fonts preload above. See ensureImports for the
|
package/src/testing/manifold.js
CHANGED
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
import Module from "manifold-3d";
|
|
5
5
|
import { createManifoldKernel } from "../framework/geometry/manifold-backend.js";
|
|
6
6
|
import { resolveFonts } from "../framework/fonts.js";
|
|
7
|
+
import { normalizeOpentype, parseFont } from "../framework/geometry/opentype-interop.js";
|
|
7
8
|
import { ensureImports } from "../framework/imports.js";
|
|
8
9
|
import { nodeAssetSources } from "./assets.js";
|
|
9
10
|
import { tessellateStepAssets } from "./step-mesh.js";
|
|
@@ -12,8 +13,8 @@ export async function bootManifoldKernel({ quality = "preview", fonts, imports,
|
|
|
12
13
|
const wasm = await Module();
|
|
13
14
|
wasm.setup();
|
|
14
15
|
const kernel = createManifoldKernel(wasm, { quality });
|
|
15
|
-
if (fonts) { const opentype = (await import("opentype.js"))
|
|
16
|
-
for (const [name, buf] of await resolveFonts(nodeAssetSources(fonts))) kernel._fonts.set(name, opentype
|
|
16
|
+
if (fonts) { const opentype = normalizeOpentype(await import("opentype.js"));
|
|
17
|
+
for (const [name, buf] of await resolveFonts(nodeAssetSources(fonts))) kernel._fonts.set(name, parseFont(opentype, buf, name)); }
|
|
17
18
|
if (imports) {
|
|
18
19
|
const decl = nodeAssetSources(imports);
|
|
19
20
|
const { resolveImports } = await import("../framework/imports.js");
|
package/src/testing/occt.js
CHANGED
|
@@ -6,6 +6,7 @@ import path from "path";
|
|
|
6
6
|
import fs from "fs";
|
|
7
7
|
import { createOcctKernel } from "../framework/geometry/occt-backend.js";
|
|
8
8
|
import { resolveFonts } from "../framework/fonts.js";
|
|
9
|
+
import { normalizeOpentype, parseFont } from "../framework/geometry/opentype-interop.js";
|
|
9
10
|
import { ensureImports } from "../framework/imports.js";
|
|
10
11
|
import { nodeAssetSources } from "./assets.js";
|
|
11
12
|
|
|
@@ -18,8 +19,8 @@ export async function bootOcctKernel({ fonts, imports, importMeshes } = {}) {
|
|
|
18
19
|
const replicad = await import("replicad");
|
|
19
20
|
replicad.setOC(OC);
|
|
20
21
|
const kernel = createOcctKernel(replicad);
|
|
21
|
-
if (fonts) { const opentype = (await import("opentype.js"))
|
|
22
|
-
for (const [name, buf] of await resolveFonts(nodeAssetSources(fonts))) kernel._fonts.set(name, opentype
|
|
22
|
+
if (fonts) { const opentype = normalizeOpentype(await import("opentype.js"));
|
|
23
|
+
for (const [name, buf] of await resolveFonts(nodeAssetSources(fonts))) kernel._fonts.set(name, parseFont(opentype, buf, name)); }
|
|
23
24
|
if (imports) await ensureImports(kernel, nodeAssetSources(imports), importMeshes ?? null);
|
|
24
25
|
return kernel;
|
|
25
26
|
}
|