partforge 0.78.0 → 0.80.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/docs/AUTHORING-PARTS.md +14 -9
- package/package.json +8 -1
- package/src/framework/jobs.js +43 -17
- package/src/framework/oracle/describe.js +106 -1
- package/src/oracle.js +27 -0
- package/src/testing.js +4 -17
- package/types/oracle.d.ts +27 -0
package/docs/AUTHORING-PARTS.md
CHANGED
|
@@ -1596,17 +1596,18 @@ different next actions for whoever is rebuilding the part.
|
|
|
1596
1596
|
arcs, holes, dress-ups, sweeps, patterns, symmetry — covers the tool's full detection
|
|
1597
1597
|
vocabulary; a feature can be *reported* there regardless of shape. The RECONSTRUCTION
|
|
1598
1598
|
score (`explainedVolumeFraction`, and every accepted feature's own `volumeShare`) is
|
|
1599
|
-
narrower: `toCandidate`
|
|
1600
|
-
|
|
1601
|
-
|
|
1599
|
+
narrower: `toCandidate` proposes prismatic candidates from each feature's own measured
|
|
1600
|
+
footprint — the cap's boundary loops extruded directly, arbitrary polygon outlines and
|
|
1601
|
+
interior holes included, alongside the circle/cylinder path — so ordinary prismatic
|
|
1602
|
+
parts now reconstruct well regardless of footprint shape. It does not yet reconstruct
|
|
1602
1603
|
revolves, fillets, chamfers, or shells — those are detected and reported (with
|
|
1603
1604
|
`volumeShareReason: "not-proposed"`) but never turned into a candidate that could win
|
|
1604
1605
|
volume back. Measured directly on this repo's own reference parts: `demo.js`
|
|
1605
|
-
reconstructs
|
|
1606
|
-
tube
|
|
1607
|
-
|
|
1608
|
-
|
|
1609
|
-
understood" or "broken." Read the FACTS (features, surfaces, patterns) as the ground
|
|
1606
|
+
reconstructs 100% of its volume, `filleted-box.js` 94.9%, `bracket.js` 100%; a plain
|
|
1607
|
+
tube and a hollow box still score ~0%, because curved-wall extrusions and shells remain
|
|
1608
|
+
unproposed. The low-coverage banner fires wherever the worse score is low, so nothing
|
|
1609
|
+
here is misreported — but a low `explainedVolumeFraction` on a turned or shelled part
|
|
1610
|
+
means **"not yet reconstructable by this tool"**, not "not understood" or "broken." Read the FACTS (features, surfaces, patterns) as the ground
|
|
1610
1611
|
truth regardless of the volume score; read the volume score as a measure of how much of
|
|
1611
1612
|
that ground truth also comes with a working, boolean-verified rebuild recipe.
|
|
1612
1613
|
|
|
@@ -1948,7 +1949,11 @@ access. It's harmless to leave in when partforge is a normal install.)
|
|
|
1948
1949
|
## Testing a part
|
|
1949
1950
|
|
|
1950
1951
|
Tests run under **Node 24** (`nvm use` first; the default shell Node is too old) via
|
|
1951
|
-
`npx vitest run`.
|
|
1952
|
+
`npx vitest run`. The oracle half of this surface — `measure`, `verify`,
|
|
1953
|
+
`describe`, gaps, match scoring — is also published on its own as
|
|
1954
|
+
`partforge/oracle` (browser-safe import closure); `partforge/testing` re-exports
|
|
1955
|
+
it, so either import works. Build geometry directly off your part with a Manifold
|
|
1956
|
+
kernel:
|
|
1952
1957
|
|
|
1953
1958
|
```js
|
|
1954
1959
|
import { bootManifoldKernel, resolveDerived } from "partforge/testing";
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "partforge",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.80.0",
|
|
4
4
|
"description": "Turn a declarative part definition into a parametric-CAD web app (three.js + Manifold/Replicad). Requires a Vite-based consumer.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -45,6 +45,10 @@
|
|
|
45
45
|
"types": "./types/derive.d.ts",
|
|
46
46
|
"default": "./src/framework/derive.js"
|
|
47
47
|
},
|
|
48
|
+
"./oracle": {
|
|
49
|
+
"types": "./types/oracle.d.ts",
|
|
50
|
+
"default": "./src/oracle.js"
|
|
51
|
+
},
|
|
48
52
|
"./testing": {
|
|
49
53
|
"types": "./types/testing.d.ts",
|
|
50
54
|
"default": "./src/testing.js"
|
|
@@ -66,6 +70,9 @@
|
|
|
66
70
|
"derive": [
|
|
67
71
|
"./types/derive.d.ts"
|
|
68
72
|
],
|
|
73
|
+
"oracle": [
|
|
74
|
+
"./types/oracle.d.ts"
|
|
75
|
+
],
|
|
69
76
|
"testing": [
|
|
70
77
|
"./types/testing.d.ts"
|
|
71
78
|
]
|
package/src/framework/jobs.js
CHANGED
|
@@ -10,20 +10,32 @@ import { normalizeOpentype, parseFont } from "./geometry/opentype-interop.js";
|
|
|
10
10
|
import { ensureImports, resolveImports } from "./imports.js";
|
|
11
11
|
import { safeName } from "./safe-name.js";
|
|
12
12
|
import { exportSubParts, resolveParams, buildPosed } from "./part-model.js";
|
|
13
|
-
import { measure } from "./oracle/measure.js";
|
|
14
|
-
import { verify } from "./oracle/verify.js";
|
|
15
|
-
import { buildView } from "./oracle/build.js";
|
|
16
|
-
import { MATCH_VIEWS, rasterizeMeshMask, rasterizeRingsMask } from "./oracle/silhouette.js";
|
|
17
|
-
import { matchViews } from "./oracle/match.js";
|
|
18
|
-
import { describe as describeMesh, describeMemo } from "./oracle/describe.js";
|
|
19
|
-
import { compactDescribe } from "./oracle/describe/report.js";
|
|
20
13
|
|
|
21
|
-
//
|
|
22
|
-
//
|
|
23
|
-
//
|
|
24
|
-
//
|
|
25
|
-
//
|
|
26
|
-
|
|
14
|
+
// The oracle loads LAZILY, per job family, never at worker boot. It is the largest
|
|
15
|
+
// JS payload in the worker's graph (measure/verify/build, silhouette/match, and the
|
|
16
|
+
// describe stack), and only the `inspect` and `describe` jobs run any of it — the
|
|
17
|
+
// generate/export hot path touches none. Each family below is a literal dynamic
|
|
18
|
+
// import(), which Vite splits into its own chunk under `worker.format: "es"` (this
|
|
19
|
+
// repo's config and partforge-cloud's both), so a user who never runs an oracle job
|
|
20
|
+
// never downloads or parses one. The module loader caches the namespace after the
|
|
21
|
+
// first await, so repeat jobs pay a resolved-promise tick, not a re-fetch.
|
|
22
|
+
// test/worker-layering.test.js's eager-closure guard holds this in place.
|
|
23
|
+
const loadInspect = () => Promise.all([
|
|
24
|
+
import("./oracle/build.js"),
|
|
25
|
+
import("./oracle/measure.js"),
|
|
26
|
+
import("./oracle/verify.js"),
|
|
27
|
+
]);
|
|
28
|
+
const loadDescribe = () => Promise.all([
|
|
29
|
+
import("./oracle/describe.js"),
|
|
30
|
+
import("./oracle/describe/report.js"),
|
|
31
|
+
]);
|
|
32
|
+
|
|
33
|
+
// One describe memo for the life of this worker, created alongside the stack's first
|
|
34
|
+
// load. Deliberately NOT swept on setPart the way solid-cache is: describe is pure in
|
|
35
|
+
// the mesh bytes (spec §4.1), so an edit can never invalidate it, and dropping it on
|
|
36
|
+
// rebind would throw away the single most expensive thing this worker computes for no
|
|
37
|
+
// reason at all. Keyed by content digest, so a genuinely changed file misses correctly.
|
|
38
|
+
let DESCRIBE_MEMO = null;
|
|
27
39
|
|
|
28
40
|
// Handle one geometry job, posting results/progress via `post(msg, transfer?)`.
|
|
29
41
|
// Backend-agnostic and part-agnostic: every part specific comes through `part`.
|
|
@@ -47,7 +59,7 @@ const bufferOf = (data) => (ArrayBuffer.isView(data) ? data.buffer : data);
|
|
|
47
59
|
// cannot cost the others their scores (or the caller their geometry report).
|
|
48
60
|
// {kind: "profile", rings: [[[x,y], ...], ...]} — millimetres, so it carries scale
|
|
49
61
|
// {kind: "image", mask: {data, width, height}} — a photo, so it carries none
|
|
50
|
-
function referenceMask(target) {
|
|
62
|
+
function referenceMask(target, rasterizeRingsMask) {
|
|
51
63
|
if (target?.kind === "profile") return Array.isArray(target.rings) ? rasterizeRingsMask(target.rings) : null;
|
|
52
64
|
if (target?.kind === "image") {
|
|
53
65
|
const m = target.mask;
|
|
@@ -70,9 +82,15 @@ function referenceMask(target) {
|
|
|
70
82
|
//
|
|
71
83
|
// The six mesh masks are rasterized ONCE and shared across every target — the targets
|
|
72
84
|
// are the cheap side of this (a couple of reference masks), the part is not.
|
|
73
|
-
function scoreMatchTargets(built, targets, onProgress) {
|
|
85
|
+
async function scoreMatchTargets(built, targets, onProgress) {
|
|
74
86
|
if (!targets?.length) return null;
|
|
75
87
|
try {
|
|
88
|
+
// Loaded here, past the early return: an inspect with no matchTargets — the
|
|
89
|
+
// common case — never pays for the rasterizer.
|
|
90
|
+
const [{ MATCH_VIEWS, rasterizeMeshMask, rasterizeRingsMask }, { matchViews }] = await Promise.all([
|
|
91
|
+
import("./oracle/silhouette.js"),
|
|
92
|
+
import("./oracle/match.js"),
|
|
93
|
+
]);
|
|
76
94
|
const meshes = built.map((b) => b.mesh);
|
|
77
95
|
const viewMasks = {};
|
|
78
96
|
for (const view of MATCH_VIEWS) viewMasks[view] = rasterizeMeshMask(meshes, view);
|
|
@@ -80,7 +98,7 @@ function scoreMatchTargets(built, targets, onProgress) {
|
|
|
80
98
|
const out = [];
|
|
81
99
|
for (const target of targets) {
|
|
82
100
|
try {
|
|
83
|
-
const reference = referenceMask(target);
|
|
101
|
+
const reference = referenceMask(target, rasterizeRingsMask);
|
|
84
102
|
if (!reference) continue;
|
|
85
103
|
// scaleAware is the CALLER's promise that both sides are in millimetres, and
|
|
86
104
|
// this is the caller: rings are mm and the mesh masks carry mmPerPx, so a
|
|
@@ -365,6 +383,7 @@ export async function handle(kernel, part, msg, post, opts = {}) {
|
|
|
365
383
|
// unrecognized value must never quietly buy less checking than the caller
|
|
366
384
|
// asked for.
|
|
367
385
|
const quick = msg.checks === "quick";
|
|
386
|
+
const [{ buildView }, { measure }, { verify }] = await loadInspect();
|
|
368
387
|
const view = msg.view ?? Object.keys(part.views)[0];
|
|
369
388
|
const built = buildView(kernel, part, view, msg.params ?? {});
|
|
370
389
|
const measured = measure(kernel, part, view, msg.params ?? {},
|
|
@@ -375,13 +394,18 @@ export async function handle(kernel, part, msg, post, opts = {}) {
|
|
|
375
394
|
// The defaulted view, not msg.view: the seed below was measured on it, and
|
|
376
395
|
// verify's seed reuse is only sound when both name the same view.
|
|
377
396
|
view,
|
|
397
|
+
// This job's own (lazily-imported) measure, not verify's static fallback:
|
|
398
|
+
// the two are different module instances once measure.js loads through a
|
|
399
|
+
// dynamic import, and the seeding test's call-count mock only sees this
|
|
400
|
+
// one. One binding for the whole inspect keeps that countable — and true.
|
|
401
|
+
measureFn: measure,
|
|
378
402
|
quick,
|
|
379
403
|
seed: { params: msg.params ?? {}, result: measured },
|
|
380
404
|
}),
|
|
381
405
|
};
|
|
382
406
|
// `match` is present only when the caller asked for it AND something scored, so
|
|
383
407
|
// an inspect with no `matchTargets` answers on exactly the shape it always has.
|
|
384
|
-
const match = scoreMatchTargets(built, msg.matchTargets, onProgress);
|
|
408
|
+
const match = await scoreMatchTargets(built, msg.matchTargets, onProgress);
|
|
385
409
|
if (match) report.match = match;
|
|
386
410
|
post({ type: "report", ...report }, match?.map((m) => m.delta.data.buffer) ?? []);
|
|
387
411
|
} else if (msg.type === "describe") {
|
|
@@ -393,10 +417,12 @@ export async function handle(kernel, part, msg, post, opts = {}) {
|
|
|
393
417
|
// Manifold only, and not by choice on this path: mesh imports on OCCT are never
|
|
394
418
|
// attempted, so a describe job posted to an OCCT worker is a routing bug, not a
|
|
395
419
|
// fallback opportunity. It surfaces as an ordinary error rather than a reroute.
|
|
420
|
+
const [{ describe: describeMesh, describeMemo }, { compactDescribe }] = await loadDescribe();
|
|
396
421
|
const solid = kernel.import(msg.importName); // throws on an unknown name
|
|
397
422
|
// `_importDigest` is the backend's existing underscore side-channel (KERNEL-CONTRACT
|
|
398
423
|
// "Conformance classes") — the same digest already folded into every import cache key.
|
|
399
424
|
const digest = kernel._importDigest?.(msg.importName) ?? null;
|
|
425
|
+
DESCRIBE_MEMO ??= describeMemo();
|
|
400
426
|
const full = describeMesh(kernel, solid, {
|
|
401
427
|
name: msg.importName,
|
|
402
428
|
digest,
|
|
@@ -180,6 +180,66 @@ function surfaceVertices(topo, surfById, ids, faceScope) {
|
|
|
180
180
|
return out;
|
|
181
181
|
}
|
|
182
182
|
|
|
183
|
+
// A deterministic unit vector perpendicular to `w`: orthogonalize the world axis
|
|
184
|
+
// with the smallest |component| along `w`. Deterministic matters — describe is memoed
|
|
185
|
+
// by content digest, so a candidate's frame must be a pure function of the mesh.
|
|
186
|
+
const perpTo = (w) => {
|
|
187
|
+
const ax = Math.abs(w[0]) <= Math.abs(w[1]) && Math.abs(w[0]) <= Math.abs(w[2]) ? [1, 0, 0]
|
|
188
|
+
: Math.abs(w[1]) <= Math.abs(w[2]) ? [0, 1, 0] : [0, 0, 1];
|
|
189
|
+
return orthogonalize(ax, w);
|
|
190
|
+
};
|
|
191
|
+
|
|
192
|
+
// The cap's own measured boundary loops as ABSOLUTE (u,v) coordinates in the cap's
|
|
193
|
+
// plane — the real footprint, where the box branch below can only offer the
|
|
194
|
+
// footprint's bounding rectangle (mostly air on an L-bracket or a sword-shaped
|
|
195
|
+
// bookmark, so the candidate loses on xor-gain and the feature reconstructs
|
|
196
|
+
// nothing). `surface-graph.js` already chained these loops per surface; a merged
|
|
197
|
+
// surface's list can carry OTHER islands' rims too (mergeCoFamily joins co-planar
|
|
198
|
+
// patches that never touch), so when the feature carries a `faceScope` the loops are
|
|
199
|
+
// filtered to those whose every vertex belongs to this island's own triangles.
|
|
200
|
+
// The largest-area survivor is the outer contour; the rest are holes (an annular
|
|
201
|
+
// cap's second loop — exactly the signal the hole/pocket rules already read).
|
|
202
|
+
// Winding is normalized to CCW for both, the orientation `kernel.extrude` expects
|
|
203
|
+
// of a contour. Returns null when no loop survives — callers fall back to the
|
|
204
|
+
// bounding-box candidate, honest about being one.
|
|
205
|
+
function footprintLoops(topo, cap, scopeFaces, u, v, claimedVerts) {
|
|
206
|
+
let allowed = null;
|
|
207
|
+
if (scopeFaces) {
|
|
208
|
+
allowed = new Set();
|
|
209
|
+
for (const t of scopeFaces) for (let c = 0; c < 3; c++) allowed.add(topo.tris[3 * t + c]);
|
|
210
|
+
}
|
|
211
|
+
const loops = [];
|
|
212
|
+
for (const loop of cap.loops ?? []) {
|
|
213
|
+
if (loop.length < 3) continue;
|
|
214
|
+
if (allowed && !loop.every((vi) => allowed.has(vi))) continue;
|
|
215
|
+
const pts = loop.map((vi) => {
|
|
216
|
+
const pnt = [topo.verts[3 * vi], topo.verts[3 * vi + 1], topo.verts[3 * vi + 2]];
|
|
217
|
+
return [dot3(pnt, u), dot3(pnt, v)];
|
|
218
|
+
});
|
|
219
|
+
let a2 = 0; // shoelace, signed — sign is the winding, magnitude ranks outer vs holes
|
|
220
|
+
for (let i = 0; i < pts.length; i++) {
|
|
221
|
+
const a = pts[i], q = pts[(i + 1) % pts.length];
|
|
222
|
+
a2 += a[0] * q[1] - a[1] * q[0];
|
|
223
|
+
}
|
|
224
|
+
loops.push({ pts, vis: loop, area: Math.abs(a2) / 2, ccw: a2 > 0 });
|
|
225
|
+
}
|
|
226
|
+
if (!loops.length) return null;
|
|
227
|
+
loops.sort((a, b) => b.area - a.area);
|
|
228
|
+
const ccw = (l) => (l.ccw ? l.pts : [...l.pts].reverse());
|
|
229
|
+
// An interior loop whose rim another feature CLAIMS (a detected hole's bore — the
|
|
230
|
+
// rim ring is shared between the cap and the bore wall, so every loop vertex sits
|
|
231
|
+
// in the bore surface's own vertex set) is left OUT of the footprint: that hole's
|
|
232
|
+
// own cut candidate owns it. Without this, an annular cap rebuilt hole-included
|
|
233
|
+
// leaves the hole feature nothing to explain — measured on the washer fixture,
|
|
234
|
+
// whose through-hole's volumeShare went to null the moment footprints landed,
|
|
235
|
+
// exactly the double-explanation this guard prevents. A parametric author would
|
|
236
|
+
// decompose it the same way: base profile, then a bore with its own diameter.
|
|
237
|
+
// Unclaimed interior loops (a square cutout no hole rule recognizes) stay in the
|
|
238
|
+
// footprint — better an honest hole in the prism than 12.5% unexplained volume.
|
|
239
|
+
const unclaimed = (l) => !claimedVerts || !l.vis.every((vi) => claimedVerts.has(vi));
|
|
240
|
+
return { outer: ccw(loops[0]), holes: loops.slice(1).filter(unclaimed).map(ccw) };
|
|
241
|
+
}
|
|
242
|
+
|
|
183
243
|
export function describe(kernel, solid, opts = {}) {
|
|
184
244
|
// A live Solid in, not a mesh. The kernel exposes no public mesh->solid constructor —
|
|
185
245
|
// geometry only enters through `_registerImport` + `import(name)` — and acceptance needs
|
|
@@ -270,8 +330,21 @@ export function describe(kernel, solid, opts = {}) {
|
|
|
270
330
|
// world Z, or — round 2 review's CRITICAL finding — reading the whole mesh's bounds
|
|
271
331
|
// for every feature and building every candidate the same full-part size.
|
|
272
332
|
const surfById = new Map(graph.surfaces.map((s) => [s.id, s]));
|
|
333
|
+
// Every vertex belonging to a surface some HOLE feature claims — the set
|
|
334
|
+
// footprintLoops consults so a footprint never re-explains a rim a hole's own cut
|
|
335
|
+
// candidate owns (see its comment for the washer measurement that forced this).
|
|
336
|
+
const claimedVerts = new Set();
|
|
337
|
+
for (const f of features) {
|
|
338
|
+
if (f.type !== "throughHole" && f.type !== "blindHole") continue;
|
|
339
|
+
for (const id of f.surfaces ?? []) {
|
|
340
|
+
const surf = surfById.get(id);
|
|
341
|
+
for (const t of surf?.faces ?? []) {
|
|
342
|
+
for (let c = 0; c < 3; c++) claimedVerts.add(topo.tris[3 * t + c]);
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
}
|
|
273
346
|
const candidates = features
|
|
274
|
-
.map((f) => toCandidate(kernel, f, b, { surfById, topo }))
|
|
347
|
+
.map((f) => toCandidate(kernel, f, b, { surfById, topo, claimedVerts }))
|
|
275
348
|
.filter(Boolean);
|
|
276
349
|
|
|
277
350
|
const graded = acceptCandidates(kernel, solid, candidates, { budget: opts.budget });
|
|
@@ -497,6 +570,38 @@ function toCandidate(kernel, f, b, ctx) {
|
|
|
497
570
|
// projected onto that exact (u, v, direction) frame — `projectedBounds`, this
|
|
498
571
|
// file's general-frame twin of mesh.js's `bounds()` — rather than the world-axis
|
|
499
572
|
// bbox `size`/`f.depth` used above for the (rotation-insensitive) circle case.
|
|
573
|
+
// Loop-based candidate first: the cap's own measured boundary loops ARE the
|
|
574
|
+
// footprint (footprintLoops above), so when they are available the candidate is
|
|
575
|
+
// the real prism — outer contour extruded, hole contours honoured — rather than
|
|
576
|
+
// either rectangle below. The frame's in-plane axis is arbitrary (perpTo): the
|
|
577
|
+
// loop coordinates are absolute projections onto (u, v), so any orthonormal pair
|
|
578
|
+
// perpendicular to `direction` reproduces the same world-space solid after
|
|
579
|
+
// orientOnto. Depth still comes from THIS FEATURE'S OWN vertices projected onto
|
|
580
|
+
// `direction` (the same projectedBounds discipline as the box branch, round 2
|
|
581
|
+
// review's CRITICAL finding). A candidate whose loops were mis-chained or span a
|
|
582
|
+
// merged surface's other island simply scores a poor xor-gain and is rejected —
|
|
583
|
+
// the same honesty the box fallback has always leaned on.
|
|
584
|
+
const cap = ctx?.surfById?.get(f.floorFace);
|
|
585
|
+
if (cap?.loops?.length && ctx?.topo) {
|
|
586
|
+
const u = perpTo(direction);
|
|
587
|
+
const v = cross3(direction, u);
|
|
588
|
+
const fp = footprintLoops(ctx.topo, cap, f.faceScope?.[f.floorFace], u, v, ctx.claimedVerts);
|
|
589
|
+
if (fp) {
|
|
590
|
+
const ownSurfaces = [f.floorFace, ...(f.wallFaces ?? [])].filter(Boolean);
|
|
591
|
+
return {
|
|
592
|
+
key: f.key, featureKey: f.key, op, explains: [f.id],
|
|
593
|
+
dimension: f.depth, paramName: "height", hintOp: f.type === "boss" ? "union" : f.type === "pocket" ? "cut" : "box",
|
|
594
|
+
hintArgs: { shape: f.profile.kind, depth: f.depth },
|
|
595
|
+
build: () => {
|
|
596
|
+
const verts = surfaceVertices(ctx.topo, ctx.surfById, ownSurfaces, f.faceScope);
|
|
597
|
+
const bnd = projectedBounds(verts, [u, v, direction]);
|
|
598
|
+
const local = kernel.extrude({ profile: { outer: fp.outer, holes: fp.holes }, h: bnd.max[2] - bnd.min[2] });
|
|
599
|
+
const oriented = orientOnto(local, direction, u);
|
|
600
|
+
return oriented.translate(scale3(direction, bnd.min[2]));
|
|
601
|
+
},
|
|
602
|
+
};
|
|
603
|
+
}
|
|
604
|
+
}
|
|
500
605
|
const wallPlane = ctx?.surfById && f.wallFaces
|
|
501
606
|
? f.wallFaces.map((id) => ctx.surfById.get(id)).find((s) => s?.type === "plane")
|
|
502
607
|
: null;
|
package/src/oracle.js
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
// partforge/oracle — the geometric oracle as its own published entry.
|
|
2
|
+
//
|
|
3
|
+
// This is the SEAM between the oracle and everything that consumes it. The same
|
|
4
|
+
// modules serve three callers: the geometry worker lazy-loads them per job family
|
|
5
|
+
// (see jobs.js — an `inspect` pulls measure/verify/build, a `describe` pulls the
|
|
6
|
+
// describe stack, and the generate/export hot path pulls none), the CLI and Node
|
|
7
|
+
// harnesses import them here directly, and partforge/testing re-exports this whole
|
|
8
|
+
// surface so an existing downstream import keeps working. Everything below is
|
|
9
|
+
// DOM-free, three-free and node:-free — test/oracle-entry.test.js walks the closure
|
|
10
|
+
// and holds that, so the entry stays importable from a worker, a browser, or Node
|
|
11
|
+
// alike. If the oracle ever moves to its own package, this file is the boundary
|
|
12
|
+
// consumers are already importing through.
|
|
13
|
+
export { assemblyGaps, meshGaps } from "./framework/oracle/gaps.js";
|
|
14
|
+
export { meshVolume, bboxSize } from "./framework/oracle/mesh.js";
|
|
15
|
+
export { buildView } from "./framework/oracle/build.js";
|
|
16
|
+
export { measure } from "./framework/oracle/measure.js";
|
|
17
|
+
export { verify } from "./framework/oracle/verify.js";
|
|
18
|
+
export { buildBVH } from "./framework/oracle/bvh.js";
|
|
19
|
+
export { minWall } from "./framework/oracle/min-wall.js";
|
|
20
|
+
// Silhouette match scoring — the `inspect` job scores `matchTargets` with exactly
|
|
21
|
+
// these, re-exported so a downstream harness can reproduce a score outside the job loop.
|
|
22
|
+
export { MATCH_VIEWS, rasterizeMeshMask, rasterizeRingsMask } from "./framework/oracle/silhouette.js";
|
|
23
|
+
export { matchMasks, matchViews } from "./framework/oracle/match.js";
|
|
24
|
+
// The semantic mesh oracle — what the `describe` job runs.
|
|
25
|
+
export { describe, describeMemo, DESCRIBE_ERRORS } from "./framework/oracle/describe.js";
|
|
26
|
+
export { compactDescribe, LOW_COVERAGE } from "./framework/oracle/describe/report.js";
|
|
27
|
+
export { DESCRIBE_LIMITS } from "./framework/oracle/describe/limits.js";
|
package/src/testing.js
CHANGED
|
@@ -14,22 +14,9 @@ export { viewSubParts } from "./framework/part-model.js";
|
|
|
14
14
|
export { resolveDerived } from "./framework/derive.js";
|
|
15
15
|
export { relevantParamKeys, RELEVANT_ALL } from "./framework/param-deps.js";
|
|
16
16
|
export { assemblyOverlaps } from "./framework/assembly.js";
|
|
17
|
-
export { assemblyGaps, meshGaps } from "./framework/oracle/gaps.js";
|
|
18
17
|
export { bootOcctKernel } from "./testing/occt.js";
|
|
19
|
-
export { meshVolume, bboxSize } from "./framework/oracle/mesh.js";
|
|
20
|
-
export { buildView } from "./framework/oracle/build.js";
|
|
21
|
-
export { measure } from "./framework/oracle/measure.js";
|
|
22
18
|
export { renderViews, RENDER_VIEWS } from "./testing/render.js";
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
// `matchTargets` with exactly these), re-exported so a downstream harness can build
|
|
28
|
-
// the same masks and reproduce a score outside the job loop.
|
|
29
|
-
export { MATCH_VIEWS, rasterizeMeshMask, rasterizeRingsMask } from "./framework/oracle/silhouette.js";
|
|
30
|
-
export { matchMasks, matchViews } from "./framework/oracle/match.js";
|
|
31
|
-
// The semantic mesh oracle. Worker-reachable like the rest of the oracle (the
|
|
32
|
-
// `describe` job runs it); re-exported so a downstream harness can run it directly.
|
|
33
|
-
export { describe, describeMemo, DESCRIBE_ERRORS } from "./framework/oracle/describe.js";
|
|
34
|
-
export { compactDescribe, LOW_COVERAGE } from "./framework/oracle/describe/report.js";
|
|
35
|
-
export { DESCRIBE_LIMITS } from "./framework/oracle/describe/limits.js";
|
|
19
|
+
// The whole oracle surface (measure/verify/buildView, gaps/BVH/min-wall, silhouette
|
|
20
|
+
// match scoring, and the semantic mesh oracle) comes through partforge/oracle — one
|
|
21
|
+
// list of names, two doors. See src/oracle.js for what each group is.
|
|
22
|
+
export * from "./oracle.js";
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
// partforge/oracle — types for the oracle's own entry (src/oracle.js).
|
|
2
|
+
//
|
|
3
|
+
// The declarations themselves live in testing.d.ts, where this surface was first
|
|
4
|
+
// published; this file re-exports exactly the names src/oracle.js exports, plus the
|
|
5
|
+
// report/mask/gap types a caller needs to annotate results. If the oracle ever moves
|
|
6
|
+
// to its own package, the declarations migrate here and testing.d.ts re-exports
|
|
7
|
+
// instead — the direction flips, the names don't.
|
|
8
|
+
export type { GeometryKernel, Mesh, PartDefinition, ResolvedParams, Solid } from "./testing.js";
|
|
9
|
+
export {
|
|
10
|
+
// measurement + verification
|
|
11
|
+
measure, verify, buildView,
|
|
12
|
+
type MeasureReport, type SubPartFacts, type AggregateFacts, type BuiltSubPart,
|
|
13
|
+
type VerifyReport, type VerifyCaseResult, type VerifyCheck, type CheckStatus,
|
|
14
|
+
// mesh facts, gaps, BVH, min wall
|
|
15
|
+
meshVolume, bboxSize, assemblyGaps, meshGaps, buildBVH, minWall,
|
|
16
|
+
type Gap, type BVH,
|
|
17
|
+
// silhouette match scoring
|
|
18
|
+
MATCH_VIEWS, rasterizeMeshMask, rasterizeRingsMask, matchMasks, matchViews,
|
|
19
|
+
type SilhouetteMask, type MatchScores, type MatchDelta,
|
|
20
|
+
// the semantic mesh oracle
|
|
21
|
+
describe, describeMemo, compactDescribe,
|
|
22
|
+
DESCRIBE_ERRORS, DESCRIBE_LIMITS, LOW_COVERAGE,
|
|
23
|
+
type DescribeReport, type DescribeCompactReport, type DescribeFailure,
|
|
24
|
+
type DescribeSurface, type DescribeArc, type DescribeFeature, type DescribePattern,
|
|
25
|
+
type DescribeResidualRegion, type DescribeSuggestion, type DescribeSuggestionStep,
|
|
26
|
+
type DescribeScore, type DescribeTruncated, type Snapped,
|
|
27
|
+
} from "./testing.js";
|