partforge 0.145.0 → 0.145.1
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/core/bvh-core.js +2 -2
- package/src/framework/core/core-status.js +56 -0
- package/src/framework/core/core.js +34 -5
- package/src/framework/geometry/creased-normals.js +11 -7
- package/src/framework/oracle/bvh.js +2 -2
- package/src/framework/worker.js +4 -0
- package/src/oracle.js +1 -1
- package/types/oracle.d.ts +2 -1
- package/types/testing.d.ts +22 -0
- package/types/worker.d.ts +2 -0
package/package.json
CHANGED
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
// Where this realm's native core stands, and who wants to hear about it — kept
|
|
2
|
+
// apart from core.js so a host can import it (partforge/worker re-exports it)
|
|
3
|
+
// without pulling in the WebAssembly bytes. core.js records into it.
|
|
4
|
+
//
|
|
5
|
+
// Falling back to the JS is silent to the user (the answers are identical) but
|
|
6
|
+
// must not be silent to the host: onCoreFallback(listener) is told — once — when
|
|
7
|
+
// the core becomes unavailable or faults, with a fixed reason and nothing else,
|
|
8
|
+
// so it can go straight into a host's telemetry.
|
|
9
|
+
|
|
10
|
+
// Every reason a fallback can carry. A fixed vocabulary on purpose: hosts record
|
|
11
|
+
// it, and an error message could carry anything.
|
|
12
|
+
export const CORE_FALLBACK_REASONS = Object.freeze(["no_webassembly", "boot_failed", "out_of_memory", "trap", "error"]);
|
|
13
|
+
|
|
14
|
+
let fallback = null; // { state: "unavailable" | "faulted", reason } once it happened
|
|
15
|
+
let refused = 0; // meshes the core declined (malformed MeshGL) — the JS took them
|
|
16
|
+
let probe = () => "idle"; // core.js installs the live answer when it loads
|
|
17
|
+
const listeners = new Set();
|
|
18
|
+
|
|
19
|
+
export function setStateProbe(fn) { probe = fn; }
|
|
20
|
+
|
|
21
|
+
export function recordFallback(state, reason) {
|
|
22
|
+
if (fallback) return; // the first cause is the one worth knowing
|
|
23
|
+
fallback = { state, reason };
|
|
24
|
+
for (const fn of listeners) {
|
|
25
|
+
try { fn({ ...fallback }); } catch { /* a host listener must never break a build */ }
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// A pass sent a mesh to the JS because the core declined it (not a fault).
|
|
30
|
+
export function countRefusedMesh() { refused++; }
|
|
31
|
+
|
|
32
|
+
// Where this realm's core stands:
|
|
33
|
+
// state: "idle" (not needed yet) | "on" | "off" (switched off) |
|
|
34
|
+
// "unavailable" (could not start) | "faulted" (stopped after a fault)
|
|
35
|
+
// reason: one of CORE_FALLBACK_REASONS for unavailable/faulted, else null
|
|
36
|
+
// refusedMeshes: how many meshes went to the JS because the core declined them
|
|
37
|
+
export function coreStatus() {
|
|
38
|
+
return { state: fallback?.state ?? probe(), reason: fallback?.reason ?? null, refusedMeshes: refused };
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// Call `listener({ state, reason })` once when the core becomes unavailable or
|
|
42
|
+
// faults — immediately if it already has. Returns an unsubscribe function.
|
|
43
|
+
export function onCoreFallback(listener) {
|
|
44
|
+
if (fallback) {
|
|
45
|
+
try { listener({ ...fallback }); } catch { /* see recordFallback */ }
|
|
46
|
+
}
|
|
47
|
+
listeners.add(listener);
|
|
48
|
+
return () => listeners.delete(listener);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// Tests only (via core.js's setCoreForTesting): forget everything recorded.
|
|
52
|
+
export function resetCoreStatus() {
|
|
53
|
+
fallback = null;
|
|
54
|
+
refused = 0;
|
|
55
|
+
listeners.clear();
|
|
56
|
+
}
|
|
@@ -25,7 +25,16 @@
|
|
|
25
25
|
// WebAssembly memory only ever grows, an instance whose memory passed
|
|
26
26
|
// RECYCLE_BYTES is dropped once nothing holds it (releaseIfIdle); the next pass
|
|
27
27
|
// boots a fresh one in a few milliseconds and the old memory goes to the GC.
|
|
28
|
+
//
|
|
29
|
+
// Falling back is silent to the user (the answers are identical) but must not be
|
|
30
|
+
// silent to the host: coreStatus() says where this realm stands, and
|
|
31
|
+
// onCoreFallback(listener) is told — once — when the core becomes unavailable or
|
|
32
|
+
// faults, with a fixed reason (CORE_FALLBACK_REASONS) and nothing else, so a host
|
|
33
|
+
// can count fallbacks in its own telemetry.
|
|
28
34
|
import { CORE_WASM_BASE64 } from "./core-wasm.js";
|
|
35
|
+
import { recordFallback, resetCoreStatus, setStateProbe } from "./core-status.js";
|
|
36
|
+
|
|
37
|
+
export { CORE_FALLBACK_REASONS, coreStatus, countRefusedMesh, onCoreFallback } from "./core-status.js";
|
|
29
38
|
|
|
30
39
|
// What the core may keep holding between passes. Ordinary forges never grow it
|
|
31
40
|
// past its 16 MB start; a preview mesh needs ~145 bytes/triangle at the peak
|
|
@@ -35,6 +44,16 @@ export const RECYCLE_BYTES = 64 * 1024 * 1024;
|
|
|
35
44
|
|
|
36
45
|
let enabled = true;
|
|
37
46
|
let booted; // undefined: not tried yet (or recycled); null: unavailable or poisoned
|
|
47
|
+
const OUT_OF_MEMORY = "partforge core: out of WebAssembly memory";
|
|
48
|
+
|
|
49
|
+
function reasonOf(err) {
|
|
50
|
+
if (err?.message === OUT_OF_MEMORY) return "out_of_memory";
|
|
51
|
+
if (typeof WebAssembly === "object" && err instanceof WebAssembly.RuntimeError) return "trap";
|
|
52
|
+
return "error";
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// The live answer for coreStatus() (core-status.js) while nothing has failed.
|
|
56
|
+
setStateProbe(() => (!enabled || flagOff() ? "off" : booted ? "on" : "idle"));
|
|
38
57
|
|
|
39
58
|
export function setCoreEnabled(on) { enabled = !!on; }
|
|
40
59
|
|
|
@@ -49,9 +68,11 @@ export function core() {
|
|
|
49
68
|
|
|
50
69
|
// The instance `c` faulted (a trap, or an allocation that failed): never use it
|
|
51
70
|
// again in this realm. Objects still holding it check `c.poisoned` and fall back.
|
|
52
|
-
|
|
71
|
+
// `err` is what was thrown; only its category is kept (reasonOf).
|
|
72
|
+
export function poisonCore(c, err) {
|
|
53
73
|
if (c) c.poisoned = true;
|
|
54
74
|
if (booted === c || c == null) booted = null;
|
|
75
|
+
recordFallback("faulted", reasonOf(err));
|
|
55
76
|
}
|
|
56
77
|
|
|
57
78
|
// Drop `c` if its memory grew past RECYCLE_BYTES and nothing holds it (no live
|
|
@@ -60,8 +81,12 @@ export function releaseIfIdle(c) {
|
|
|
60
81
|
if (booted === c && c.live === 0 && c.x.memory.buffer.byteLength > RECYCLE_BYTES) booted = undefined;
|
|
61
82
|
}
|
|
62
83
|
|
|
63
|
-
// Tests only: install a stand-in instance (or undefined to re-boot)
|
|
64
|
-
|
|
84
|
+
// Tests only: install a stand-in instance (or undefined to re-boot), and forget
|
|
85
|
+
// any recorded fallback, refusal count and listeners.
|
|
86
|
+
export function setCoreForTesting(c) {
|
|
87
|
+
booted = c;
|
|
88
|
+
resetCoreStatus();
|
|
89
|
+
}
|
|
65
90
|
|
|
66
91
|
const B64 = (() => {
|
|
67
92
|
const t = new Uint8Array(128);
|
|
@@ -86,7 +111,10 @@ function decodeBase64(s) {
|
|
|
86
111
|
}
|
|
87
112
|
|
|
88
113
|
function boot() {
|
|
89
|
-
if (typeof WebAssembly !== "object")
|
|
114
|
+
if (typeof WebAssembly !== "object") {
|
|
115
|
+
recordFallback("unavailable", "no_webassembly");
|
|
116
|
+
return null;
|
|
117
|
+
}
|
|
90
118
|
try {
|
|
91
119
|
const stub = () => 0;
|
|
92
120
|
// Synchronous on purpose: a pass asks mid-build and must get an answer now.
|
|
@@ -108,7 +136,7 @@ function boot() {
|
|
|
108
136
|
// nothing is ever written over the module's low memory.
|
|
109
137
|
alloc(bytes) {
|
|
110
138
|
const ptr = x.malloc(bytes || 1);
|
|
111
|
-
if (!ptr) throw new Error(
|
|
139
|
+
if (!ptr) throw new Error(OUT_OF_MEMORY);
|
|
112
140
|
return ptr;
|
|
113
141
|
},
|
|
114
142
|
// Views are rebuilt per use: any call that grows memory detaches the old buffer.
|
|
@@ -122,6 +150,7 @@ function boot() {
|
|
|
122
150
|
copyOut: (Ctor, ptr, len) => (ptr && len ? new Ctor(x.memory.buffer, ptr, len).slice() : new Ctor(0)),
|
|
123
151
|
};
|
|
124
152
|
} catch {
|
|
153
|
+
recordFallback("unavailable", "boot_failed");
|
|
125
154
|
return null;
|
|
126
155
|
}
|
|
127
156
|
}
|
|
@@ -19,7 +19,7 @@
|
|
|
19
19
|
import { SMOOTH, COPLANAR_ANGLE, MIN_EDGE, MIN_FACE, cosDeg } from "./shading-policy.js";
|
|
20
20
|
import { affineAt, IDENTITY, runEvaluator } from "./blend-surfaces.js";
|
|
21
21
|
import { hypot3 } from "./js-math.js";
|
|
22
|
-
import { core, poisonCore } from "../core/core.js";
|
|
22
|
+
import { core, countRefusedMesh, poisonCore } from "../core/core.js";
|
|
23
23
|
import { coreAccepts, creasedNormalsCore } from "../core/creased-normals-core.js";
|
|
24
24
|
|
|
25
25
|
const COPLANAR_COS = cosDeg(COPLANAR_ANGLE);
|
|
@@ -43,12 +43,16 @@ const SHADE_SLIVER = 1e-3;
|
|
|
43
43
|
// poisons that instance and this mesh, and every one after, runs the JS.
|
|
44
44
|
export function creasedNormals(g, opts) {
|
|
45
45
|
const c = core();
|
|
46
|
-
if (c
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
46
|
+
if (c) {
|
|
47
|
+
if (!coreAccepts(g)) countRefusedMesh();
|
|
48
|
+
else {
|
|
49
|
+
try {
|
|
50
|
+
const out = creasedNormalsCore(c, g, opts);
|
|
51
|
+
if (out) return out;
|
|
52
|
+
countRefusedMesh();
|
|
53
|
+
} catch (err) {
|
|
54
|
+
poisonCore(c, err);
|
|
55
|
+
}
|
|
52
56
|
}
|
|
53
57
|
}
|
|
54
58
|
return creasedNormalsJS(g, opts);
|
|
@@ -316,8 +316,8 @@ function indexFor(mesh, allowCore) {
|
|
|
316
316
|
if (c) {
|
|
317
317
|
try {
|
|
318
318
|
return buildCoreBVH(c, triangleVertices(mesh), () => buildBVH(mesh));
|
|
319
|
-
} catch {
|
|
320
|
-
poisonCore(c); // a fault while building: this realm's core is done; JS from here
|
|
319
|
+
} catch (err) {
|
|
320
|
+
poisonCore(c, err); // a fault while building: this realm's core is done; JS from here
|
|
321
321
|
}
|
|
322
322
|
}
|
|
323
323
|
return buildBVH(mesh);
|
package/src/framework/worker.js
CHANGED
|
@@ -9,6 +9,10 @@
|
|
|
9
9
|
// guarantees about epochs, cache sweeps, and the re-posted ready) is normative
|
|
10
10
|
// in docs/KERNEL-CONTRACT.md.
|
|
11
11
|
import { handle } from "./jobs.js";
|
|
12
|
+
// The native core's status (core/core-status.js): a host's worker entry
|
|
13
|
+
// subscribes here to count fallbacks. That module carries no WebAssembly, so
|
|
14
|
+
// re-exporting it costs a worker nothing at boot.
|
|
15
|
+
export { coreStatus, onCoreFallback, CORE_FALLBACK_REASONS } from "./core/core-status.js";
|
|
12
16
|
import { lintPart } from "../lint.js";
|
|
13
17
|
import { cachedVectorDocs } from "./vectors.js";
|
|
14
18
|
|
package/src/oracle.js
CHANGED
|
@@ -27,7 +27,7 @@ export { buildBVH, meshTriangles } from "./framework/oracle/bvh.js";
|
|
|
27
27
|
export { minWall } from "./framework/oracle/min-wall.js";
|
|
28
28
|
// The native core's switch (framework/core/core.js): bit-identical either way, so
|
|
29
29
|
// this is a performance and kill switch, never a behaviour choice.
|
|
30
|
-
export { setCoreEnabled } from "./framework/core/core.js";
|
|
30
|
+
export { setCoreEnabled, coreStatus, onCoreFallback, CORE_FALLBACK_REASONS } from "./framework/core/core.js";
|
|
31
31
|
export { overhang } from "./framework/oracle/overhang.js";
|
|
32
32
|
// Mesh file parsers — the import pipeline's own readers, browser-safe pure
|
|
33
33
|
// functions; the oracle package's corpus tests read real files through them.
|
package/types/oracle.d.ts
CHANGED
|
@@ -17,7 +17,8 @@ export {
|
|
|
17
17
|
// mesh facts, gaps, BVH, min wall
|
|
18
18
|
meshVolume, bboxSize, bounds, meshArea, assemblyGaps, meshGaps, buildBVH, meshTriangles, minWall, overhang,
|
|
19
19
|
// the native core's switch
|
|
20
|
-
setCoreEnabled,
|
|
20
|
+
setCoreEnabled, coreStatus, onCoreFallback, CORE_FALLBACK_REASONS,
|
|
21
|
+
type CoreStatus, type CoreFallback, type CoreFallbackReason,
|
|
21
22
|
type Gap, type BVH,
|
|
22
23
|
// mesh file parsers (the import pipeline's own readers)
|
|
23
24
|
parseStl, parse3MF,
|
package/types/testing.d.ts
CHANGED
|
@@ -274,6 +274,28 @@ export function minWall(
|
|
|
274
274
|
*/
|
|
275
275
|
export function setCoreEnabled(on: boolean): void;
|
|
276
276
|
|
|
277
|
+
/** Why the core stopped answering in this realm — a fixed vocabulary, safe to record. */
|
|
278
|
+
export type CoreFallbackReason = "no_webassembly" | "boot_failed" | "out_of_memory" | "trap" | "error";
|
|
279
|
+
export const CORE_FALLBACK_REASONS: readonly CoreFallbackReason[];
|
|
280
|
+
export interface CoreFallback {
|
|
281
|
+
state: "unavailable" | "faulted";
|
|
282
|
+
reason: CoreFallbackReason;
|
|
283
|
+
}
|
|
284
|
+
export interface CoreStatus {
|
|
285
|
+
/** idle: not needed yet · on · off: switched off · unavailable: could not start · faulted: stopped after a fault */
|
|
286
|
+
state: "idle" | "on" | "off" | "unavailable" | "faulted";
|
|
287
|
+
reason: CoreFallbackReason | null;
|
|
288
|
+
/** Meshes sent to the JS pass because the core declined them (malformed MeshGL) — not a fault. */
|
|
289
|
+
refusedMeshes: number;
|
|
290
|
+
}
|
|
291
|
+
/** Where this realm's native core stands. Output is identical either way; this is for telemetry. */
|
|
292
|
+
export function coreStatus(): CoreStatus;
|
|
293
|
+
/**
|
|
294
|
+
* Called once when the core becomes unavailable or faults (immediately if it
|
|
295
|
+
* already has), so a host can count fallbacks. Returns an unsubscribe function.
|
|
296
|
+
*/
|
|
297
|
+
export function onCoreFallback(listener: (fallback: CoreFallback) => void): () => void;
|
|
298
|
+
|
|
277
299
|
/**
|
|
278
300
|
* Unsupported downward-facing surface (oracle/overhang.js): the mm² of faces
|
|
279
301
|
* steeper than `maxAngle` from vertical, excluding the footprint on the bed
|
package/types/worker.d.ts
CHANGED
|
@@ -5,6 +5,8 @@
|
|
|
5
5
|
// its own backend.
|
|
6
6
|
|
|
7
7
|
import type { PartDefinition } from "./part.js";
|
|
8
|
+
export type { CoreStatus, CoreFallback, CoreFallbackReason } from "./testing.js";
|
|
9
|
+
export { coreStatus, onCoreFallback, CORE_FALLBACK_REASONS } from "./testing.js";
|
|
8
10
|
|
|
9
11
|
/** The rebind handle `runWorker` returns. */
|
|
10
12
|
export interface WorkerHandle {
|