partforge 0.80.0 → 0.82.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/bin/cli.js +50 -4
- package/docs/AUTHORING-PARTS.md +99 -161
- package/docs/ERROR-PATTERNS.md +6 -0
- package/package.json +1 -1
- package/src/framework/geometry/probe.js +21 -8
- package/src/framework/jobs.js +35 -12
- package/src/framework/lint/rules-build.js +12 -2
- package/src/framework/lint/rules-shape.js +19 -0
- package/src/framework/oracle/measure.js +87 -0
- package/src/framework/oracle/verify.js +4 -1
- package/src/framework/worker.js +6 -3
- package/src/oracle.js +18 -14
- package/src/parts/import-demo.js +22 -1
- package/types/oracle.d.ts +6 -11
- package/types/testing.d.ts +15 -177
- package/types/worker.d.ts +10 -2
- package/src/framework/oracle/describe/accept.js +0 -188
- package/src/framework/oracle/describe/features/dressups.js +0 -173
- package/src/framework/oracle/describe/features/holes.js +0 -129
- package/src/framework/oracle/describe/features/prismatic.js +0 -454
- package/src/framework/oracle/describe/features/sweeps.js +0 -233
- package/src/framework/oracle/describe/fit.js +0 -535
- package/src/framework/oracle/describe/hints.js +0 -91
- package/src/framework/oracle/describe/limits.js +0 -19
- package/src/framework/oracle/describe/patterns.js +0 -494
- package/src/framework/oracle/describe/ransac.js +0 -391
- package/src/framework/oracle/describe/report.js +0 -217
- package/src/framework/oracle/describe/segment.js +0 -498
- package/src/framework/oracle/describe/snap.js +0 -83
- package/src/framework/oracle/describe/surface-graph.js +0 -396
- package/src/framework/oracle/describe/topology.js +0 -121
- package/src/framework/oracle/describe.js +0 -643
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { buildView } from "./build.js";
|
|
2
2
|
import { cachedBVH } from "./bvh.js";
|
|
3
3
|
import { assemblyOverlaps } from "../assembly.js";
|
|
4
|
+
import { resolveParams } from "../part-model.js";
|
|
4
5
|
import { meshGaps, pairKey, CONTACT_EPS, GAP_THRESHOLD } from "./gaps.js";
|
|
5
6
|
import { bounds, meshArea, meshCentroid } from "./mesh.js";
|
|
6
7
|
import { minWall, DIAGNOSTIC_SAMPLES } from "./min-wall.js";
|
|
@@ -12,6 +13,80 @@ const unionBounds = (list) => list.reduce(
|
|
|
12
13
|
{ min: [Infinity, Infinity, Infinity], max: [-Infinity, -Infinity, -Infinity] },
|
|
13
14
|
);
|
|
14
15
|
|
|
16
|
+
// ── probes ──────────────────────────────────────────────────────────────────
|
|
17
|
+
// Part-declared measurements: `probes: { name: (k, p, d) => Solid | JSON }`,
|
|
18
|
+
// pure functions with build's exact contract but whose result lands in the
|
|
19
|
+
// REPORT instead of the scene. The instrument a rebuild-against-reference
|
|
20
|
+
// workflow needs — before this, getting a cross-section's numbers out of the
|
|
21
|
+
// pipeline meant authoring throwaway `exportable: false` sub-parts and fishing
|
|
22
|
+
// their facts out of the sub-part list (the "Probes" feedback report).
|
|
23
|
+
// A Solid anywhere in the return value (duck-typed on volume+toMesh, the two
|
|
24
|
+
// queries the facts need) is replaced by a fact object; scalars/arrays/objects
|
|
25
|
+
// pass through; a throw becomes `{ error }` — probes are instrumentation, so
|
|
26
|
+
// they never crash the measurement and never gate `ok`.
|
|
27
|
+
|
|
28
|
+
const isSolid = (v) => v !== null && typeof v === "object"
|
|
29
|
+
&& typeof v.volume === "function" && typeof v.toMesh === "function";
|
|
30
|
+
|
|
31
|
+
function solidProbeFacts(solid) {
|
|
32
|
+
const mesh = solid.toMesh();
|
|
33
|
+
// Empty = the probe's boolean found nothing (a slab that misses the part).
|
|
34
|
+
// A first-class answer, not degenerate infinite bounds: "the reference has no
|
|
35
|
+
// material here" is exactly what a localizing probe is asked.
|
|
36
|
+
const empty = typeof solid.isEmpty === "function" ? solid.isEmpty() : mesh.triangles === 0;
|
|
37
|
+
if (empty) {
|
|
38
|
+
return { empty: true, bbox: null, bounds: null, centerOfMass: null,
|
|
39
|
+
volume: 0, surfaceArea: 0, triangleCount: 0, watertight: null, holes: null };
|
|
40
|
+
}
|
|
41
|
+
const b = bounds(mesh.positions);
|
|
42
|
+
return {
|
|
43
|
+
empty: false,
|
|
44
|
+
bbox: size(b),
|
|
45
|
+
bounds: { min: b.min, max: b.max },
|
|
46
|
+
centerOfMass: meshCentroid(mesh.positions, mesh.indices),
|
|
47
|
+
volume: solid.volume(),
|
|
48
|
+
surfaceArea: meshArea(mesh.positions, mesh.indices),
|
|
49
|
+
triangleCount: mesh.triangles,
|
|
50
|
+
// Mirrors the sub-part fact: answered by isEmpty where the backend has it
|
|
51
|
+
// (and this branch already means it said false), null where it can't say.
|
|
52
|
+
watertight: typeof solid.isEmpty === "function" ? true : null,
|
|
53
|
+
holes: typeof solid.genus === "function" ? solid.genus() : null,
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// Bounded so a self-referential or absurdly deep return value can't hang the
|
|
58
|
+
// report; past the cap the value is summarized rather than walked.
|
|
59
|
+
const MAX_PROBE_VALUE_DEPTH = 4;
|
|
60
|
+
function resolveProbeValue(v, depth = 0) {
|
|
61
|
+
if (isSolid(v)) return solidProbeFacts(v);
|
|
62
|
+
if (v === null || typeof v !== "object") {
|
|
63
|
+
return typeof v === "function" ? { error: "probe returned a function — return a Solid or plain JSON" } : v;
|
|
64
|
+
}
|
|
65
|
+
if (depth >= MAX_PROBE_VALUE_DEPTH) return { error: `probe value deeper than ${MAX_PROBE_VALUE_DEPTH} levels` };
|
|
66
|
+
if (Array.isArray(v)) return v.map((x) => resolveProbeValue(x, depth + 1));
|
|
67
|
+
return Object.fromEntries(Object.entries(v).map(([key, x]) => [key, resolveProbeValue(x, depth + 1)]));
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// Evaluate every declared probe with resolved (p, d). Reads all solid facts
|
|
71
|
+
// eagerly, so the caller may free the kernel's objects afterwards. Never
|
|
72
|
+
// throws: each probe's failure is its own `{ error }` entry.
|
|
73
|
+
function evaluateProbes(kernel, part, params) {
|
|
74
|
+
const { p, d } = resolveParams(part, params);
|
|
75
|
+
// Oracle-owned cache round, same reasoning as buildView's: probe geometry must
|
|
76
|
+
// not evict what the viewer is showing, and the next round evicts this one.
|
|
77
|
+
kernel.beginSubPart?.("oracle:probes");
|
|
78
|
+
try {
|
|
79
|
+
return Object.fromEntries(Object.entries(part.probes).map(([name, fn]) => {
|
|
80
|
+
try {
|
|
81
|
+
if (typeof fn !== "function") throw new Error("probe must be a function (k, p, d)");
|
|
82
|
+
return [name, resolveProbeValue(fn(kernel, p, d))];
|
|
83
|
+
} catch (e) {
|
|
84
|
+
return [name, { error: e?.message || String(e) }];
|
|
85
|
+
}
|
|
86
|
+
}));
|
|
87
|
+
} finally { kernel.endSubPart?.(); }
|
|
88
|
+
}
|
|
89
|
+
|
|
15
90
|
// Headless geometric report for one view of a part (Manifold-only). Reads exact
|
|
16
91
|
// solid facts (volume/genus/emptiness) and mesh facts (bbox/area/triangles), plus
|
|
17
92
|
// the assembly overlap check plus pair gap distances (near misses are reported,
|
|
@@ -100,6 +175,15 @@ export function measure(kernel, part, view = Object.keys(part.views)[0], params
|
|
|
100
175
|
};
|
|
101
176
|
});
|
|
102
177
|
|
|
178
|
+
// Declared probes, evaluated regardless of view (they are part-level facts —
|
|
179
|
+
// per-view probes were exactly the annoyance this replaces) and before
|
|
180
|
+
// assemblyOverlaps/cleanup below frees the kernel's objects. `opts.probes:
|
|
181
|
+
// false` skips them: verify's per-case re-measures pass it because no gate
|
|
182
|
+
// reads probe values, so re-running their booleans per case buys nothing.
|
|
183
|
+
const probes = opts.probes !== false && part.probes && Object.keys(part.probes).length
|
|
184
|
+
? evaluateProbes(kernel, part, params)
|
|
185
|
+
: undefined;
|
|
186
|
+
|
|
103
187
|
// Pair surface distances from the meshes already built — no kernel dependency,
|
|
104
188
|
// so this reads on OCCT too. nearMisses = the issue-#29 signal: pairs that
|
|
105
189
|
// *almost* touch; overlapping pairs are excluded by name (a fully-contained
|
|
@@ -155,6 +239,9 @@ export function measure(kernel, part, view = Object.keys(part.views)[0], params
|
|
|
155
239
|
overlaps,
|
|
156
240
|
gaps,
|
|
157
241
|
nearMisses,
|
|
242
|
+
// Present only when the part declares probes AND this run evaluated them —
|
|
243
|
+
// a probe error stays inside its own entry and never reaches `ok` below.
|
|
244
|
+
...(probes ? { probes } : {}),
|
|
158
245
|
ok: subparts.every((s) => s.watertight !== false) && overlaps.length === 0,
|
|
159
246
|
};
|
|
160
247
|
}
|
|
@@ -266,7 +266,10 @@ export function verify(kernel, part, { process, view, measureFn = defaultMeasure
|
|
|
266
266
|
const key = signature(params);
|
|
267
267
|
if (memo.has(key)) return memo.get(key);
|
|
268
268
|
if (quick) return null; // a case the seed does not cover — reported, never built
|
|
269
|
-
|
|
269
|
+
// `probes: false` — no gate reads probe values, so re-running their booleans
|
|
270
|
+
// for every case buys nothing. (A seed measured WITH probes is a superset in
|
|
271
|
+
// the same way a min-wall seed is: the extra key is simply never read here.)
|
|
272
|
+
memo.set(key, measureFn(kernel, part, view, params, { minWall: needMinWall, probes: false }));
|
|
270
273
|
return memo.get(key);
|
|
271
274
|
};
|
|
272
275
|
|
package/src/framework/worker.js
CHANGED
|
@@ -36,7 +36,10 @@ async function occtKernel() {
|
|
|
36
36
|
return createOcctKernel(replicad);
|
|
37
37
|
}
|
|
38
38
|
|
|
39
|
-
|
|
39
|
+
// `opts.loadOracle` — the injection seam for the closed mesh-oracle package (see
|
|
40
|
+
// jobs.js's describe branch): a thunk resolving to the oracle barrel. Apps without
|
|
41
|
+
// the package simply omit it and describe jobs answer `oracle-unavailable`.
|
|
42
|
+
export function runWorker(part, opts = {}) {
|
|
40
43
|
const backend = self.name === "occt" ? "occt" : "manifold";
|
|
41
44
|
let manifold = null; // { preview, print }
|
|
42
45
|
let occt = null;
|
|
@@ -99,7 +102,7 @@ export function runWorker(part) {
|
|
|
99
102
|
const kernel = await kernelFor(job.data);
|
|
100
103
|
// handle() declares each message's transferables (the big binary buffers).
|
|
101
104
|
const post = (m, transfer = []) => postMessage(m, transfer);
|
|
102
|
-
if (job.epoch === null) { await handle(kernel, job.part, job.data, post, { importMeshes }); continue; }
|
|
105
|
+
if (job.epoch === null) { await handle(kernel, job.part, job.data, post, { importMeshes, loadOracle: opts.loadOracle }); continue; }
|
|
103
106
|
const isStale = () => job.epoch !== epoch;
|
|
104
107
|
// Post gate. The boundary check cannot catch a generate that goes stale during
|
|
105
108
|
// its FINAL sub-part — there is no boundary after it — nor a single-sub-part
|
|
@@ -108,7 +111,7 @@ export function runWorker(part) {
|
|
|
108
111
|
// contract simple: a `meshes` post is current as of the moment it is posted.
|
|
109
112
|
const gated = (m, transfer = []) =>
|
|
110
113
|
(m.type === "meshes" && isStale() ? post({ type: "superseded" }) : post(m, transfer));
|
|
111
|
-
await handle(kernel, job.part, job.data, gated, { isStale, importMeshes });
|
|
114
|
+
await handle(kernel, job.part, job.data, gated, { isStale, importMeshes, loadOracle: opts.loadOracle });
|
|
112
115
|
} catch (err) {
|
|
113
116
|
// Same shape jobs.js posts for a failed build, so hosts need no new branch.
|
|
114
117
|
// Carry the job's jobId when it has one (capture/export are correlated by it):
|
package/src/oracle.js
CHANGED
|
@@ -2,26 +2,30 @@
|
|
|
2
2
|
//
|
|
3
3
|
// This is the SEAM between the oracle and everything that consumes it. The same
|
|
4
4
|
// modules serve three callers: the geometry worker lazy-loads them per job family
|
|
5
|
-
// (see jobs.js — an `inspect` pulls measure/verify/build,
|
|
6
|
-
//
|
|
7
|
-
//
|
|
8
|
-
//
|
|
9
|
-
//
|
|
10
|
-
//
|
|
11
|
-
//
|
|
12
|
-
//
|
|
5
|
+
// (see jobs.js — an `inspect` pulls measure/verify/build, and the generate/export
|
|
6
|
+
// hot path pulls none), the CLI and Node harnesses import them here directly, and
|
|
7
|
+
// partforge/testing re-exports this whole surface so an existing downstream import
|
|
8
|
+
// keeps working. Everything below is DOM-free, three-free and node:-free —
|
|
9
|
+
// test/oracle-entry.test.js walks the closure and holds that, so the entry stays
|
|
10
|
+
// importable from a worker, a browser, or Node alike.
|
|
11
|
+
//
|
|
12
|
+
// The SEMANTIC MESH ORACLE (`describe`) is NOT here: it lives in its own closed
|
|
13
|
+
// package, which peer-depends on this one and consumes exactly this entry — the
|
|
14
|
+
// mesh/BVH helpers and file parsers below are exported for it. The framework
|
|
15
|
+
// reaches it only through injection (`runWorker(part, { loadOracle })`, jobs.js)
|
|
16
|
+
// and the CLI resolves it at call time; neither ever bundles it.
|
|
13
17
|
export { assemblyGaps, meshGaps } from "./framework/oracle/gaps.js";
|
|
14
|
-
export { meshVolume, bboxSize } from "./framework/oracle/mesh.js";
|
|
18
|
+
export { meshVolume, bboxSize, bounds, meshArea } from "./framework/oracle/mesh.js";
|
|
15
19
|
export { buildView } from "./framework/oracle/build.js";
|
|
16
20
|
export { measure } from "./framework/oracle/measure.js";
|
|
17
21
|
export { verify } from "./framework/oracle/verify.js";
|
|
18
|
-
export { buildBVH } from "./framework/oracle/bvh.js";
|
|
22
|
+
export { buildBVH, meshTriangles } from "./framework/oracle/bvh.js";
|
|
19
23
|
export { minWall } from "./framework/oracle/min-wall.js";
|
|
24
|
+
// Mesh file parsers — the import pipeline's own readers, browser-safe pure
|
|
25
|
+
// functions; the oracle package's corpus tests read real files through them.
|
|
26
|
+
export { parseStl } from "./framework/geometry/stl-parse.js";
|
|
27
|
+
export { parse3MF } from "./framework/geometry/threemf-parse.js";
|
|
20
28
|
// Silhouette match scoring — the `inspect` job scores `matchTargets` with exactly
|
|
21
29
|
// these, re-exported so a downstream harness can reproduce a score outside the job loop.
|
|
22
30
|
export { MATCH_VIEWS, rasterizeMeshMask, rasterizeRingsMask } from "./framework/oracle/silhouette.js";
|
|
23
31
|
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/parts/import-demo.js
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
// Reference part for docs/AUTHORING-PARTS.md's "Importing geometry" section —
|
|
2
|
-
//
|
|
2
|
+
// and for its "Probes" section (the `probes` block below measures the import
|
|
3
|
+
// live into the measure report) — the worked example for BOTH import uses in
|
|
4
|
+
// one part:
|
|
3
5
|
// • reference — `ref` is a translucent ghost of the imported scan (never
|
|
4
6
|
// exported); `body` is a parametric rebuild of the same block, bound to
|
|
5
7
|
// the scan via `reference: "scan"` and held to it by the three ref*
|
|
@@ -108,6 +110,25 @@ export default {
|
|
|
108
110
|
// socket never touches `body` (see `mountOffsetX`). "reference" is the
|
|
109
111
|
// ghost-overlay view, browsed by hand or with an explicit view argument.
|
|
110
112
|
views: { assembly: { label: "Assembly" }, reference: { label: "Reference overlay" } },
|
|
113
|
+
// Probes — measurements that land in the `measure` report instead of the
|
|
114
|
+
// scene (docs/AUTHORING-PARTS.md "Probes"). Pure (k, p, d) functions like
|
|
115
|
+
// build; never rendered, never exported, reported for every view.
|
|
116
|
+
probes: {
|
|
117
|
+
// Paired 1 mm cross-sections of the rebuild and the scan at the same X
|
|
118
|
+
// station — the localizing instrument for the deviation gate above: when
|
|
119
|
+
// refXorVolume creeps up, slide the slab along X to find WHERE the two
|
|
120
|
+
// solids disagree instead of guessing from one whole-part number.
|
|
121
|
+
midSlab: (k, p) => {
|
|
122
|
+
const slab = () => k.box({ min: [9.5, -50, -50], max: [10.5, 50, 50] });
|
|
123
|
+
return {
|
|
124
|
+
body: k.box({ min: [0, 0, 0], max: [p.scanW, p.scanD, p.scanH] }).intersect(slab()),
|
|
125
|
+
scan: k.import("scan").intersect(slab()),
|
|
126
|
+
};
|
|
127
|
+
},
|
|
128
|
+
// A live reading straight off the import — the numbers `defaults` were
|
|
129
|
+
// measured from. Plain JSON passes through the report verbatim.
|
|
130
|
+
scanBounds: (k) => k.import("scan").boundingBox(),
|
|
131
|
+
},
|
|
111
132
|
verify: {
|
|
112
133
|
process: "fdm-pla",
|
|
113
134
|
expect: {
|
package/types/oracle.d.ts
CHANGED
|
@@ -2,9 +2,9 @@
|
|
|
2
2
|
//
|
|
3
3
|
// The declarations themselves live in testing.d.ts, where this surface was first
|
|
4
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.
|
|
6
|
-
//
|
|
7
|
-
//
|
|
5
|
+
// report/mask/gap types a caller needs to annotate results. The semantic mesh
|
|
6
|
+
// oracle (`describe`) is its own closed package now — its types ship with it, and
|
|
7
|
+
// the direction of this seam is what lets that package consume these helpers.
|
|
8
8
|
export type { GeometryKernel, Mesh, PartDefinition, ResolvedParams, Solid } from "./testing.js";
|
|
9
9
|
export {
|
|
10
10
|
// measurement + verification
|
|
@@ -12,16 +12,11 @@ export {
|
|
|
12
12
|
type MeasureReport, type SubPartFacts, type AggregateFacts, type BuiltSubPart,
|
|
13
13
|
type VerifyReport, type VerifyCaseResult, type VerifyCheck, type CheckStatus,
|
|
14
14
|
// mesh facts, gaps, BVH, min wall
|
|
15
|
-
meshVolume, bboxSize, assemblyGaps, meshGaps, buildBVH, minWall,
|
|
15
|
+
meshVolume, bboxSize, bounds, meshArea, assemblyGaps, meshGaps, buildBVH, meshTriangles, minWall,
|
|
16
16
|
type Gap, type BVH,
|
|
17
|
+
// mesh file parsers (the import pipeline's own readers)
|
|
18
|
+
parseStl, parse3MF,
|
|
17
19
|
// silhouette match scoring
|
|
18
20
|
MATCH_VIEWS, rasterizeMeshMask, rasterizeRingsMask, matchMasks, matchViews,
|
|
19
21
|
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
22
|
} from "./testing.js";
|
package/types/testing.d.ts
CHANGED
|
@@ -186,6 +186,21 @@ export function meshVolume(positions: ArrayLike<number>, indices?: ArrayLike<num
|
|
|
186
186
|
/** `[dx, dy, dz]` extent of a flat position array. */
|
|
187
187
|
export function bboxSize(positions: ArrayLike<number>): [number, number, number];
|
|
188
188
|
|
|
189
|
+
/** World-frame axis-aligned bounds of a flat position array. */
|
|
190
|
+
export function bounds(positions: ArrayLike<number>): { min: [number, number, number]; max: [number, number, number] };
|
|
191
|
+
|
|
192
|
+
/** Total surface area of an indexed (or soup, when `indices` is omitted) triangle mesh, mm². */
|
|
193
|
+
export function meshArea(positions: ArrayLike<number>, indices?: ArrayLike<number>): number;
|
|
194
|
+
|
|
195
|
+
/** Triangles as `[v0, v1, v2]` coordinate triples, from an indexed mesh or a soup. */
|
|
196
|
+
export function meshTriangles(mesh: Mesh): [number, number, number][][];
|
|
197
|
+
|
|
198
|
+
/** Parse a binary or ASCII STL into a welded, indexed mesh. */
|
|
199
|
+
export function parseStl(bytes: Uint8Array | ArrayBuffer): { positions: Float32Array; indices: Uint32Array };
|
|
200
|
+
|
|
201
|
+
/** Parse a 3MF archive's first mesh object into a welded, indexed mesh. */
|
|
202
|
+
export function parse3MF(bytes: Uint8Array | ArrayBuffer): { positions: Float32Array; indices: Uint32Array };
|
|
203
|
+
|
|
189
204
|
// --- the BVH ----------------------------------------------------------------
|
|
190
205
|
|
|
191
206
|
/** A triangle BVH over one mesh — nearest ray hit, nearest point, exact mesh distance. */
|
|
@@ -454,183 +469,6 @@ export function matchViews(
|
|
|
454
469
|
opts?: { scaleAware?: boolean },
|
|
455
470
|
): { best: ({ view: string } & MatchScores) | null; views: Record<string, number> };
|
|
456
471
|
|
|
457
|
-
// --- describe (the semantic mesh oracle) ------------------------------------
|
|
458
|
-
|
|
459
|
-
/** A raw measurement snapped to intent — never destroys the measurement. */
|
|
460
|
-
export interface Snapped {
|
|
461
|
-
raw: number;
|
|
462
|
-
to: number;
|
|
463
|
-
note: string | null;
|
|
464
|
-
}
|
|
465
|
-
|
|
466
|
-
/** One fitted surface patch. `fit` is the raw per-type fit record (fit.js). */
|
|
467
|
-
export interface DescribeSurface {
|
|
468
|
-
id: string;
|
|
469
|
-
type: string;
|
|
470
|
-
area: number;
|
|
471
|
-
triangles: number;
|
|
472
|
-
rms: number;
|
|
473
|
-
maxDev: number;
|
|
474
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- shape varies by surface type (plane/cylinder/cone/torus/sphere)
|
|
475
|
-
fit: Record<string, any>;
|
|
476
|
-
}
|
|
477
|
-
|
|
478
|
-
/** One fitted edge between two surfaces. */
|
|
479
|
-
export interface DescribeArc {
|
|
480
|
-
between: [string, string];
|
|
481
|
-
convexity: "convex" | "concave" | "flat";
|
|
482
|
-
kind: string;
|
|
483
|
-
radius: number | null;
|
|
484
|
-
axis: unknown;
|
|
485
|
-
length: number;
|
|
486
|
-
}
|
|
487
|
-
|
|
488
|
-
/**
|
|
489
|
-
* One recognised feature. Shape varies by `type` (a hole carries `diameter`/
|
|
490
|
-
* `axis`, a fillet carries `radius`/`between`, …) — the fields every family
|
|
491
|
-
* shares are pulled out here; the rest is read by `type`. Every field here
|
|
492
|
-
* must be something a rebuilding agent would want (round 4 review) —
|
|
493
|
-
* `surfaces`/`evidence` are; nothing per-triangle belongs here. describe.js
|
|
494
|
-
* strips any such internal-plumbing field (`faceScope`, a prismatic
|
|
495
|
-
* candidate builder's own per-triangle index map) before a feature reaches
|
|
496
|
-
* this shape, so this catch-all is not a substitute for that discipline.
|
|
497
|
-
*/
|
|
498
|
-
export interface DescribeFeature {
|
|
499
|
-
id: string;
|
|
500
|
-
key: string;
|
|
501
|
-
type: string;
|
|
502
|
-
/** The marginal xor-volume reduction that admitted this feature, normalised
|
|
503
|
-
* to the source volume — the fraction of the PART'S VOLUME this feature
|
|
504
|
-
* accounts for, not a certainty rating (see `DescribeScore`'s own note): a
|
|
505
|
-
* small-but-certain feature legitimately reports a small share. `null` for
|
|
506
|
-
* a type acceptCandidates never proposes (fillet, chamfer, revolve, shell). */
|
|
507
|
-
volumeShare: number | null;
|
|
508
|
-
/** Snapped values for whichever of diameter/depth/radius/width/thickness this feature carries. */
|
|
509
|
-
snapped: Record<string, Snapped>;
|
|
510
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- per-type facts (axis, profile, evidence, …)
|
|
511
|
-
[key: string]: any;
|
|
512
|
-
}
|
|
513
|
-
|
|
514
|
-
/** A repetition (grid/linear/circular) or a detected mirror plane over the feature list. */
|
|
515
|
-
export interface DescribePattern {
|
|
516
|
-
id: string;
|
|
517
|
-
type: "grid" | "linear" | "circular";
|
|
518
|
-
members: string[];
|
|
519
|
-
axis: number[] | null;
|
|
520
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- per-type spacing/count facts
|
|
521
|
-
[key: string]: any;
|
|
522
|
-
}
|
|
523
|
-
|
|
524
|
-
/** One connected island of mesh area no surface fit claimed. */
|
|
525
|
-
export interface DescribeResidualRegion {
|
|
526
|
-
triangles: number;
|
|
527
|
-
centroid: number[];
|
|
528
|
-
bounds: { min: number[]; max: number[] };
|
|
529
|
-
}
|
|
530
|
-
|
|
531
|
-
/** One proposed rebuild step, in the order acceptCandidates actually admitted it. */
|
|
532
|
-
export interface DescribeSuggestionStep {
|
|
533
|
-
op: string;
|
|
534
|
-
explains: string[];
|
|
535
|
-
pattern: string | null;
|
|
536
|
-
score: number;
|
|
537
|
-
args: Record<string, unknown>;
|
|
538
|
-
}
|
|
539
|
-
|
|
540
|
-
/** A proposed reconstruction — an interpretation, not a measurement (see `disclaimer`). */
|
|
541
|
-
export interface DescribeSuggestion {
|
|
542
|
-
disclaimer: string;
|
|
543
|
-
params: Array<{ name: string; value: number; from: string }>;
|
|
544
|
-
steps: DescribeSuggestionStep[];
|
|
545
|
-
}
|
|
546
|
-
|
|
547
|
-
/** Which of the report's capped arrays actually hit their ceiling (`DESCRIBE_LIMITS`). */
|
|
548
|
-
export interface DescribeTruncated {
|
|
549
|
-
surfaces: boolean;
|
|
550
|
-
edges: boolean;
|
|
551
|
-
features: boolean;
|
|
552
|
-
patterns: boolean;
|
|
553
|
-
residualRegions: boolean;
|
|
554
|
-
suggestionSteps: boolean;
|
|
555
|
-
}
|
|
556
|
-
|
|
557
|
-
export interface DescribeScore {
|
|
558
|
-
/** Surface coverage from segmentation: fraction of the mesh's area fitted to some surface type. */
|
|
559
|
-
explainedArea: number;
|
|
560
|
-
/** Shape coverage from reconstruction: fraction of the part's volume the accepted features rebuild. */
|
|
561
|
-
explainedVolumeFraction: number;
|
|
562
|
-
xorFraction: number;
|
|
563
|
-
xorVolume: number;
|
|
564
|
-
note: string;
|
|
565
|
-
}
|
|
566
|
-
|
|
567
|
-
/** A full `describe()` report — everything measured, meant for archival/`--json`. */
|
|
568
|
-
export interface DescribeReport {
|
|
569
|
-
source: { name: string | null; digest: string | null; triangles: number; watertight: boolean | null; units: "mm" };
|
|
570
|
-
frame: { up: "+Z"; note: string };
|
|
571
|
-
bounds: { min: number[]; max: number[]; size: number[] };
|
|
572
|
-
counts: { surfaces: number; edges: number };
|
|
573
|
-
surfaces: DescribeSurface[];
|
|
574
|
-
edges: DescribeArc[];
|
|
575
|
-
features: DescribeFeature[];
|
|
576
|
-
patterns: DescribePattern[];
|
|
577
|
-
symmetry: unknown[];
|
|
578
|
-
residual: { areaFraction: number; regions: DescribeResidualRegion[] };
|
|
579
|
-
score: DescribeScore;
|
|
580
|
-
suggestion: DescribeSuggestion | null;
|
|
581
|
-
truncated: DescribeTruncated;
|
|
582
|
-
/** Present only when the acceptance loop hit its boolean budget before converging. */
|
|
583
|
-
warning?: "budget-exceeded";
|
|
584
|
-
}
|
|
585
|
-
|
|
586
|
-
/** The model-facing view: capped arrays elided to counts, a coverage banner first when low. */
|
|
587
|
-
export type DescribeCompactReport = Omit<DescribeReport, "surfaces" | "edges"> & {
|
|
588
|
-
/** Present, and serialized FIRST, only when coverage is below `LOW_COVERAGE`. */
|
|
589
|
-
warning?: string;
|
|
590
|
-
};
|
|
591
|
-
|
|
592
|
-
/** A closed-set failure, returned rather than thrown (spec §5's diagnostic triple). */
|
|
593
|
-
export interface DescribeFailure {
|
|
594
|
-
error: "not-manifold" | "too-large" | "empty" | "budget-exceeded" | "unreadable";
|
|
595
|
-
detail: string;
|
|
596
|
-
diagnostic: { cause: string; location: string; correctiveAction: string };
|
|
597
|
-
source: { name: string | null; digest: string | null; [key: string]: unknown };
|
|
598
|
-
}
|
|
599
|
-
|
|
600
|
-
/** The closed set of error codes `describe()` can return. */
|
|
601
|
-
export const DESCRIBE_ERRORS: readonly string[];
|
|
602
|
-
|
|
603
|
-
/** Report array ceilings (`MAX_SURFACES`, `MAX_FEATURES`, …) — a plain-data module, no imports. */
|
|
604
|
-
export const DESCRIBE_LIMITS: {
|
|
605
|
-
MAX_SURFACES: number;
|
|
606
|
-
MAX_EDGES: number;
|
|
607
|
-
MAX_FEATURES: number;
|
|
608
|
-
MAX_PATTERNS: number;
|
|
609
|
-
MAX_RESIDUAL_REGIONS: number;
|
|
610
|
-
MAX_SUGGESTION_STEPS: number;
|
|
611
|
-
};
|
|
612
|
-
|
|
613
|
-
/** Below this fraction (the worse of `explainedArea`/`explainedVolumeFraction`) `compactDescribe` prepends a warning. */
|
|
614
|
-
export const LOW_COVERAGE: number;
|
|
615
|
-
|
|
616
|
-
/** A fresh, caller-owned digest memo — scope one per worker, or per test. */
|
|
617
|
-
export function describeMemo(): Map<string, DescribeReport>;
|
|
618
|
-
|
|
619
|
-
/**
|
|
620
|
-
* Mesh in, semantic feature report out. `solid` is a LIVE kernel `Solid` — the
|
|
621
|
-
* kernel has no public mesh->solid constructor, so a caller reads one back via
|
|
622
|
-
* `kernel.import(name)`. Keyed by `opts.digest` in `opts.memo` (when both are
|
|
623
|
-
* given): the report depends on nothing but the mesh bytes, so an edit to the
|
|
624
|
-
* part that produced the mesh can never invalidate it.
|
|
625
|
-
*/
|
|
626
|
-
export function describe(
|
|
627
|
-
kernel: GeometryKernel,
|
|
628
|
-
solid: Solid,
|
|
629
|
-
opts?: { name?: string; digest?: string; budget?: number; memo?: Map<string, DescribeReport> },
|
|
630
|
-
): DescribeReport | DescribeFailure;
|
|
631
|
-
|
|
632
|
-
/** The full report, reduced to what a model should read: capped arrays elided to counts, low-coverage banner first. */
|
|
633
|
-
export function compactDescribe(full: DescribeReport): DescribeCompactReport;
|
|
634
472
|
|
|
635
473
|
// --- rendering --------------------------------------------------------------
|
|
636
474
|
|
package/types/worker.d.ts
CHANGED
|
@@ -17,5 +17,13 @@ export interface WorkerHandle {
|
|
|
17
17
|
setPart(newPart: PartDefinition): void;
|
|
18
18
|
}
|
|
19
19
|
|
|
20
|
-
/**
|
|
21
|
-
|
|
20
|
+
/**
|
|
21
|
+
* Run the worker job loop for `part`. Call once, at worker module top level.
|
|
22
|
+
* `opts.loadOracle` injects the closed semantic-mesh-oracle package (a thunk
|
|
23
|
+
* resolving its barrel: `describe`, `describeMemo`, `compactDescribe`); omitted,
|
|
24
|
+
* describe jobs answer with a structured `oracle-unavailable` report.
|
|
25
|
+
*/
|
|
26
|
+
export function runWorker(
|
|
27
|
+
part: PartDefinition,
|
|
28
|
+
opts?: { loadOracle?: () => Promise<{ describe: Function; describeMemo: () => Map<string, unknown>; compactDescribe: Function }> },
|
|
29
|
+
): WorkerHandle;
|