partforge 0.78.0 → 0.79.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.
@@ -1948,7 +1948,11 @@ access. It's harmless to leave in when partforge is a normal install.)
1948
1948
  ## Testing a part
1949
1949
 
1950
1950
  Tests run under **Node 24** (`nvm use` first; the default shell Node is too old) via
1951
- `npx vitest run`. Build geometry directly off your part with a Manifold kernel:
1951
+ `npx vitest run`. The oracle half of this surface `measure`, `verify`,
1952
+ `describe`, gaps, match scoring — is also published on its own as
1953
+ `partforge/oracle` (browser-safe import closure); `partforge/testing` re-exports
1954
+ it, so either import works. Build geometry directly off your part with a Manifold
1955
+ kernel:
1952
1956
 
1953
1957
  ```js
1954
1958
  import { bootManifoldKernel, resolveDerived } from "partforge/testing";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "partforge",
3
- "version": "0.78.0",
3
+ "version": "0.79.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
  ]
@@ -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
- // One describe memo for the life of this worker. Deliberately NOT swept on setPart the
22
- // way solid-cache is: describe is pure in the mesh bytes (spec §4.1), so an edit can
23
- // never invalidate it, and dropping it on rebind would throw away the single most
24
- // expensive thing this worker computes for no reason at all. Keyed by content digest, so
25
- // a genuinely changed file misses correctly.
26
- const DESCRIBE_MEMO = describeMemo();
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,
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
- export { verify } from "./framework/oracle/verify.js";
24
- export { buildBVH } from "./framework/oracle/bvh.js";
25
- export { minWall } from "./framework/oracle/min-wall.js";
26
- // Silhouette match scoring — also worker-reachable (the `inspect` job scores
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";