partforge 0.79.0 → 0.81.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 +22 -4
- package/docs/AUTHORING-PARTS.md +22 -158
- package/docs/ERROR-PATTERNS.md +6 -0
- package/package.json +1 -1
- package/src/framework/jobs.js +35 -12
- package/src/framework/worker.js +6 -3
- package/src/oracle.js +18 -14
- 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 -538
package/bin/cli.js
CHANGED
|
@@ -15,8 +15,6 @@ import { bootOcctKernel } from "../src/testing/occt.js";
|
|
|
15
15
|
import { bootManifoldKernel } from "../src/testing/manifold.js";
|
|
16
16
|
import { measure } from "../src/framework/oracle/measure.js";
|
|
17
17
|
import { verify } from "../src/framework/oracle/verify.js";
|
|
18
|
-
import { describe as describeMesh } from "../src/framework/oracle/describe.js";
|
|
19
|
-
import { compactDescribe } from "../src/framework/oracle/describe/report.js";
|
|
20
18
|
import { renderViews } from "../src/testing/render.js";
|
|
21
19
|
import {
|
|
22
20
|
createPickServer, requestPicks, formatPickResult,
|
|
@@ -27,6 +25,25 @@ import { matchPattern } from "../src/testing/error-patterns.js";
|
|
|
27
25
|
import { lintPart } from "../src/lint.js";
|
|
28
26
|
|
|
29
27
|
const die = (msg) => { console.error(msg); process.exit(1); };
|
|
28
|
+
|
|
29
|
+
// The semantic mesh oracle (`describe`) lives in a separate, closed package — this
|
|
30
|
+
// CLI names it only here, and resolves it at CALL time so every other verb works
|
|
31
|
+
// without it installed. PARTFORGE_ORACLE overrides the specifier (a path or module
|
|
32
|
+
// name) — how the oracle package's own repo points this CLI at its working tree.
|
|
33
|
+
const ORACLE_PACKAGE = "@pixiteapps/partforge-oracle";
|
|
34
|
+
async function loadOracle() {
|
|
35
|
+
const spec = process.env.PARTFORGE_ORACLE ?? ORACLE_PACKAGE;
|
|
36
|
+
const target = spec.startsWith(".") || spec.startsWith("/")
|
|
37
|
+
? pathToFileURL(resolve(process.cwd(), spec)).href
|
|
38
|
+
: spec;
|
|
39
|
+
try {
|
|
40
|
+
return await import(target);
|
|
41
|
+
} catch (e) {
|
|
42
|
+
die(`describe needs the mesh-oracle package (${ORACLE_PACKAGE}), which is not installed.\n` +
|
|
43
|
+
`Install it from the private registry (or set PARTFORGE_ORACLE to a local path) and retry.\n` +
|
|
44
|
+
`(import of ${spec} failed: ${e?.message ?? e})`);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
30
47
|
const USAGE = "usage: partforge <lint|measure|render|describe|pick-serve|pick> …";
|
|
31
48
|
|
|
32
49
|
// Crash contract (issue #27): with --json, a thrown error becomes structured
|
|
@@ -210,6 +227,7 @@ const commands = {
|
|
|
210
227
|
die(`describe: "${importName}" is not a declared import of ${partPath} ` +
|
|
211
228
|
`(have: ${Object.keys(part.imports ?? {}).join(", ") || "none"})`);
|
|
212
229
|
}
|
|
230
|
+
const { describe: describeMesh, compactDescribe } = await loadOracle();
|
|
213
231
|
const kernel = await bootKernel(part);
|
|
214
232
|
const solid = kernel.import(importName);
|
|
215
233
|
const report = describeMesh(kernel, solid, {
|
|
@@ -222,7 +240,7 @@ const commands = {
|
|
|
222
240
|
writeFileSync(flags.out, JSON.stringify(report, null, 2));
|
|
223
241
|
}
|
|
224
242
|
if (flags.json) console.log(JSON.stringify(report, null, 2));
|
|
225
|
-
else printDescribe(report, { surfaces: !!flags.surfaces });
|
|
243
|
+
else printDescribe(report, { surfaces: !!flags.surfaces }, compactDescribe);
|
|
226
244
|
if (flags.out) console.log(`\nwrote ${flags.out}`);
|
|
227
245
|
// A closed-set error exits non-zero; LOW COVERAGE does not. Coverage is a finding
|
|
228
246
|
// the caller must be able to read, and an exit code that conflated the two would
|
|
@@ -448,7 +466,7 @@ function printVerify(v) {
|
|
|
448
466
|
// opts back in; `--json` always has everything. Reads the SAME compactDescribe() output
|
|
449
467
|
// a model reads (not a hand-rolled subset), so what a human sees here and what an agent
|
|
450
468
|
// sees over `--json` cannot drift apart.
|
|
451
|
-
function printDescribe(report, { surfaces }) {
|
|
469
|
+
function printDescribe(report, { surfaces }, compactDescribe) {
|
|
452
470
|
if (report.error) {
|
|
453
471
|
console.error(`describe: ${report.error}${report.detail ? ` — ${report.detail}` : ""}`);
|
|
454
472
|
return;
|
package/docs/AUTHORING-PARTS.md
CHANGED
|
@@ -1496,165 +1496,29 @@ build: (k, p, d) => {
|
|
|
1496
1496
|
|
|
1497
1497
|
## Describing an imported mesh
|
|
1498
1498
|
|
|
1499
|
-
|
|
1500
|
-
|
|
1501
|
-
|
|
1502
|
-
|
|
1503
|
-
|
|
1504
|
-
|
|
1499
|
+
`npx partforge describe <part-module>#<importName>` reads an already-declared import
|
|
1500
|
+
and emits a semantic feature report — holes, bosses, pockets, extrusions, patterns,
|
|
1501
|
+
symmetry — rather than a triangle soup, so an agent can rebuild an STL parametrically.
|
|
1502
|
+
|
|
1503
|
+
The engine behind it — the semantic mesh oracle — is **not part of this package**: it
|
|
1504
|
+
ships as a separate, closed package (`@pixiteapps/partforge-oracle`) whose docs carry
|
|
1505
|
+
the full report contract (the two report shapes, feature vocabulary, `volumeShare`
|
|
1506
|
+
semantics, coverage scores, budget behavior, and the closed error set). This repo
|
|
1507
|
+
keeps only the seams:
|
|
1508
|
+
|
|
1509
|
+
- **CLI** — `partforge describe` resolves the oracle package at call time and prints
|
|
1510
|
+
an install pointer when it is absent (`PARTFORGE_ORACLE` overrides the module
|
|
1511
|
+
specifier, which is how the oracle's own repo points this CLI at its working tree).
|
|
1512
|
+
- **Worker** — the `describe` job runs whatever `runWorker(part, { loadOracle })`
|
|
1513
|
+
injected; without a loader it answers a structured
|
|
1514
|
+
`{error: "oracle-unavailable"}` report (see
|
|
1515
|
+
[ERROR-PATTERNS.md#describe-oracle-unavailable](ERROR-PATTERNS.md#describe-oracle-unavailable)),
|
|
1516
|
+
never a stall. The loader resolves the oracle barrel: `describe`, `describeMemo`,
|
|
1517
|
+
`compactDescribe`.
|
|
1518
|
+
- **Helpers** — the oracle package peer-depends on this one and consumes
|
|
1519
|
+
`partforge/oracle`'s mesh/BVH helpers and file parsers (`bounds`, `meshArea`,
|
|
1520
|
+
`meshTriangles`, `parseStl`, `parse3MF`); those exports are part of its contract.
|
|
1505
1521
|
|
|
1506
|
-
```
|
|
1507
|
-
$ npx partforge describe src/parts/import-demo.js#scan
|
|
1508
|
-
|
|
1509
|
-
scan — 12 triangles, 20.00 x 14.00 x 8.00 mm, +Z up
|
|
1510
|
-
|
|
1511
|
-
Features (1): share = fraction of part volume this feature accounts for — a size measure, not certainty
|
|
1512
|
-
f0 extrusion 8.000 share 100.0%
|
|
1513
|
-
|
|
1514
|
-
Score: 100.0% surface area explained, 100.0% volume reconstructed (residual xor 0.00% of volume)
|
|
1515
|
-
Residual: 0.00% of area in 0 region(s)
|
|
1516
|
-
```
|
|
1517
|
-
|
|
1518
|
-
**Two report shapes, and which one is authoritative.** `describe()` (`src/framework/
|
|
1519
|
-
oracle/describe.js`) returns the FULL report — every surface, every fitted edge, every
|
|
1520
|
-
feature at full precision — and that is the archive: `--json` prints it whole, and
|
|
1521
|
-
`--out <file>` writes it whole. `compactDescribe()` (`src/framework/oracle/describe/
|
|
1522
|
-
report.js`) derives a second, smaller shape from it — features, patterns, symmetry,
|
|
1523
|
-
score, residual, and the suggested rebuild, with `surfaces`/`edges` reduced to counts —
|
|
1524
|
-
and that is what a model reads, and what the terminal printer above reads too: **the
|
|
1525
|
-
same `compactDescribe()` output**, not a hand-rolled subset, so what a human sees in the
|
|
1526
|
-
default summary and what an agent gets from a cloud turn cannot drift apart. The full
|
|
1527
|
-
report is authoritative for the facts (a fitted surface's rms, an edge's exact radius);
|
|
1528
|
-
the compact report is authoritative for what's worth paying attention to.
|
|
1529
|
-
|
|
1530
|
-
**Computed once, reused for the session.** `describe` depends on nothing but the mesh's
|
|
1531
|
-
own bytes — no part params, no derived state — so unlike `measure`/`verify` it never
|
|
1532
|
-
needs to re-run on every parameter edit. The worker keys its memo on the import's
|
|
1533
|
-
content digest (`kernel._importDigest(name)`), so re-describing the same file, or asking
|
|
1534
|
-
again mid-session while you iterate on the parametric rebuild, is free after the first
|
|
1535
|
-
call. The CLI doesn't carry a memo across process invocations — each `partforge
|
|
1536
|
-
describe` run is its own process — but within one browser session or one long-running
|
|
1537
|
-
tool loop, the mesh is segmented once.
|
|
1538
|
-
|
|
1539
|
-
**`--surfaces` and `--json`, and why the default elides.** A hand-modelled block
|
|
1540
|
-
segments to six surfaces; a real 24k-triangle CAD export segments to hundreds. Printing
|
|
1541
|
-
every one of them by default would bury the two or three that actually matter under
|
|
1542
|
-
exactly the noise this oracle exists to remove — so the plain-text summary shows
|
|
1543
|
-
features, patterns, symmetry, score, and residual only. Pass `--surfaces` to add the
|
|
1544
|
-
surface table back (`s0 plane area … rms …`, one line per fitted patch); pass `--json`
|
|
1545
|
-
to get everything, always, structured — the mode an agent should default to over
|
|
1546
|
-
scraping the text summary. `--out <file>` writes the full report to disk independently
|
|
1547
|
-
of either flag, so you can capture it once and read it multiple ways.
|
|
1548
|
-
|
|
1549
|
-
**Low coverage exits zero — it's a finding, not a failure.** A poor segmentation (a
|
|
1550
|
-
dome, a free-form scan, anything the four feature detectors can't reconstruct) is
|
|
1551
|
-
exactly the situation an agent most needs to see, not one that should make itself
|
|
1552
|
-
harder to see. `describe` treats "the shape isn't well explained" and "the command
|
|
1553
|
-
failed" as different questions: a `LOW COVERAGE` banner at the top of the summary (and
|
|
1554
|
-
`compactDescribe()`'s own `warning` field in `--json` output) is how a poor result
|
|
1555
|
-
announces itself, and the process still exits 0. Only a genuine closed-set error — the
|
|
1556
|
-
mesh couldn't be read or segmented at all — exits 1. Coverage is gated on the WORSE of
|
|
1557
|
-
two numbers, both always printed rather than blended into one: `explainedArea` (how much
|
|
1558
|
-
of the mesh's *surface* a fitted primitive accounts for) and `explainedVolumeFraction`
|
|
1559
|
-
(how much of the part's actual *shape* the accepted features reconstruct). They can
|
|
1560
|
-
diverge totally — a dome segments to ~100% surface area (it fits a sphere cleanly) and
|
|
1561
|
-
0% volume (a sphere isn't a candidate-eligible feature type for any detector) — so
|
|
1562
|
-
printing only one of them, or an average, would hide precisely the gap the banner exists
|
|
1563
|
-
to catch. Each feature's own score is named `volumeShare`, never "confidence": it is the
|
|
1564
|
-
fraction of the part's volume that feature accounts for — a measure of *size* — so a
|
|
1565
|
-
small-but-certain feature (a precisely-fitted 3mm hole in a large plate) legitimately
|
|
1566
|
-
reports a small share. Read a feature's fitted surface `rms`/`maxDev` (under `--surfaces`
|
|
1567
|
-
or `--json`) for an actual certainty signal. The one-line hint at the `Features (N):`
|
|
1568
|
-
header states the size-not-certainty point at its point of use; the longer explanation
|
|
1569
|
-
(`score.note` — both this and the `explainedArea`/`explainedVolumeFraction` distinction
|
|
1570
|
-
above, spelled out for a reader who never gets this far) stays `--json`-only, since
|
|
1571
|
-
printing it in full under `Score:` used to be the single largest visual element in every
|
|
1572
|
-
plain-text run — measured at 36-43% of a typical report's line count, bigger than the
|
|
1573
|
-
feature list, the banners, and the score line combined.
|
|
1574
|
-
|
|
1575
|
-
**Why a null `volumeShare` isn't always the same story.** A feature can carry
|
|
1576
|
-
`volumeShare: null` for three different reasons, distinguished by its sibling field
|
|
1577
|
-
`volumeShareReason` (`--surfaces`/`--json`; the text view renders it as `share n/a
|
|
1578
|
-
(<reason>)`):
|
|
1579
|
-
|
|
1580
|
-
- `"not-proposed"` — this feature TYPE is never turned into an acceptance candidate at
|
|
1581
|
-
all. Fillets, chamfers, revolves and shells fall here today; they're still reported as
|
|
1582
|
-
features, just not yet reconstructable through this pipeline.
|
|
1583
|
-
- `"budget"` — a candidate WAS proposed, but the search ran out of `--budget` before
|
|
1584
|
-
reaching it. Raise `--budget` and re-describe; the feature may resolve to a real share
|
|
1585
|
-
once given the chance.
|
|
1586
|
-
- `"rejected"` — a candidate was proposed, built, and evaluated, but never won a round
|
|
1587
|
-
(`accept.js`'s `MIN_GAIN_FRACTION` gate, or simply never the best candidate available).
|
|
1588
|
-
Raising `--budget` will not change this outcome — the feature genuinely doesn't fit
|
|
1589
|
-
well enough to explain a meaningful share of the part.
|
|
1590
|
-
|
|
1591
|
-
`"budget"` and `"rejected"` are the two that matter most to get right, and the
|
|
1592
|
-
distinction is real, not cosmetic: "try a bigger budget" and "this doesn't fit" are
|
|
1593
|
-
different next actions for whoever is rebuilding the part.
|
|
1594
|
-
|
|
1595
|
-
**What `describe` reconstructs today, and what it doesn't.** The FACTS layer — surfaces,
|
|
1596
|
-
arcs, holes, dress-ups, sweeps, patterns, symmetry — covers the tool's full detection
|
|
1597
|
-
vocabulary; a feature can be *reported* there regardless of shape. The RECONSTRUCTION
|
|
1598
|
-
score (`explainedVolumeFraction`, and every accepted feature's own `volumeShare`) is
|
|
1599
|
-
narrower: `toCandidate` only proposes box and cylinder footprints as acceptance
|
|
1600
|
-
candidates today, so it currently reconstructs prismatic parts built from roughly fewer
|
|
1601
|
-
than eight box/cylinder features well, and does not yet reconstruct round bosses,
|
|
1602
|
-
revolves, fillets, chamfers, or shells — those are detected and reported (with
|
|
1603
|
-
`volumeShareReason: "not-proposed"`) but never turned into a candidate that could win
|
|
1604
|
-
volume back. Measured directly on this repo's own reference parts: `demo.js`
|
|
1605
|
-
reconstructs 65.7% of its volume, `filleted-box.js` 36.2%, `bracket.js` 23.6%, a plain
|
|
1606
|
-
tube 0.0%, and a hollow box 1.9%. The low-coverage banner fires on every one of these, so
|
|
1607
|
-
nothing here is misreported — but a low `explainedVolumeFraction` on a turned or
|
|
1608
|
-
feature-dense part means **"not yet reconstructable by this tool"**, not "not
|
|
1609
|
-
understood" or "broken." Read the FACTS (features, surfaces, patterns) as the ground
|
|
1610
|
-
truth regardless of the volume score; read the volume score as a measure of how much of
|
|
1611
|
-
that ground truth also comes with a working, boolean-verified rebuild recipe.
|
|
1612
|
-
|
|
1613
|
-
**Closed error set.** Anything short of a programming mistake comes back as
|
|
1614
|
-
`{error: "<code>", detail, diagnostic}` rather than a thrown exception — a CLI or an
|
|
1615
|
-
agent can act on a code far better than on a stack trace — and every code has an
|
|
1616
|
-
ERROR-PATTERNS.md entry:
|
|
1617
|
-
|
|
1618
|
-
- `not-manifold` — the mesh still has open edges after repair; see
|
|
1619
|
-
[ERROR-PATTERNS.md#describe-not-manifold](ERROR-PATTERNS.md#describe-not-manifold).
|
|
1620
|
-
- `too-large` — over the 400,000-triangle segmentation ceiling; see
|
|
1621
|
-
[ERROR-PATTERNS.md#describe-too-large](ERROR-PATTERNS.md#describe-too-large).
|
|
1622
|
-
- `empty` — the mesh has zero triangles; see
|
|
1623
|
-
[ERROR-PATTERNS.md#describe-empty](ERROR-PATTERNS.md#describe-empty).
|
|
1624
|
-
- `unreadable` — `solid.toMesh()` itself threw; see
|
|
1625
|
-
[ERROR-PATTERNS.md#describe-unreadable](ERROR-PATTERNS.md#describe-unreadable).
|
|
1626
|
-
|
|
1627
|
-
`not-manifold` and `empty` describe real states `describe()` itself checks for and can
|
|
1628
|
-
return through any caller that hands it a `Solid` directly, but through the CLI they are
|
|
1629
|
-
largely theoretical: `kernel.import()`'s own registration validation rejects an empty or
|
|
1630
|
-
a wide-open (non-manifold) mesh *before* a `Solid` ever exists to describe, so
|
|
1631
|
-
`partforge describe` on such a file fails earlier, as a generic import error (the
|
|
1632
|
-
`crash()` path every other verb shares — `describe: import "x": mesh is not a solid
|
|
1633
|
-
after repair…`) rather than as a structured `{error: "not-manifold"}` / `{error:
|
|
1634
|
-
"empty"}` report. See
|
|
1635
|
-
[ERROR-PATTERNS.md#import-mesh-not-solid](ERROR-PATTERNS.md#import-mesh-not-solid) for
|
|
1636
|
-
that failure. `describe-unreadable`'s own ERROR-PATTERNS entry already draws this same
|
|
1637
|
-
line for its own code (a `k.import` parse failure is a *different*, separately-thrown
|
|
1638
|
-
error, not `describe-unreadable`); this is the general version of that point.
|
|
1639
|
-
|
|
1640
|
-
A `budget-exceeded` report (the acceptance loop ran out of boolean attempts before the
|
|
1641
|
-
residual converged) is not one of these — it's a `warning` on an otherwise-valid report,
|
|
1642
|
-
same tier as low coverage, not a reason to exit non-zero; raise `--budget` and re-run.
|
|
1643
|
-
`compactDescribe()` surfaces it exactly where the low-coverage banner lives, and shows
|
|
1644
|
-
both together if a report happens to earn both — an exhausted budget is exactly the kind
|
|
1645
|
-
of run that also leaves coverage low, and neither warning is allowed to mask the other.
|
|
1646
|
-
See [ERROR-PATTERNS.md#describe-budget-exceeded](ERROR-PATTERNS.md#describe-budget-exceeded).
|
|
1647
|
-
|
|
1648
|
-
**The loop this completes.** `describe` exists to feed a specific workflow, the one
|
|
1649
|
-
"Importing geometry" above sets up and this closes: `describe` the import to get its
|
|
1650
|
-
feature list and a proposed reconstruction (`suggestion` in the full report); write a
|
|
1651
|
-
parametric part from that proposal; bind the rebuild's sub-part to the import with
|
|
1652
|
-
`reference: "scan"`; and let `verify`'s `ref*` gates (`refXorVolume`, `refVolumeDeltaPct`,
|
|
1653
|
-
`refBboxDelta` — "The `reference` field and the deviation gate", above) hold the rebuild
|
|
1654
|
-
to the scan on every future `measure`/`verify` run, long after the one-time `describe`
|
|
1655
|
-
call that suggested it. `describe` proposes; `verify` keeps you honest.
|
|
1656
|
-
|
|
1657
|
-
---
|
|
1658
1522
|
|
|
1659
1523
|
## Wiring a part into a runnable app
|
|
1660
1524
|
|
package/docs/ERROR-PATTERNS.md
CHANGED
|
@@ -601,6 +601,12 @@ between the Manifold preview and the OCCT STEP export.
|
|
|
601
601
|
|
|
602
602
|
The same channel carries every other degrade in a build: an `extrude` rim bevel reduced or skipped (`extrude bevel <b> …`), a `roundedBox` rim radius clamped to `round.side`, and the `Shape2D` corner-op clamps in the two entries above. A build result's `warnings` is the complete list of what the part asked for and did not get.
|
|
603
603
|
|
|
604
|
+
## describe-oracle-unavailable
|
|
605
|
+
|
|
606
|
+
- **Symptom:** A `describe` job answers `{"error": "oracle-unavailable"}`, or `partforge describe` exits with "describe needs the mesh-oracle package".
|
|
607
|
+
- **Cause:** The semantic mesh oracle is a separate, closed package (`@pixiteapps/partforge-oracle`), and this app or shell doesn't have it: the worker was started without `runWorker(part, { loadOracle })`, or the CLI could not import the package.
|
|
608
|
+
- **Fix:** Install the oracle package from the private registry and inject it — `runWorker(part, { loadOracle: () => import("@pixiteapps/partforge-oracle") })` in the app's worker file, or plain `npm install` for the CLI. In the oracle package's own repo, set `PARTFORGE_ORACLE` to a local path instead. Every other job (generate, export, inspect) works without it.
|
|
609
|
+
|
|
604
610
|
## describe-not-manifold
|
|
605
611
|
|
|
606
612
|
- **Symptom:** `describe` returns `{"error": "not-manifold"}`.
|
package/package.json
CHANGED
package/src/framework/jobs.js
CHANGED
|
@@ -12,23 +12,29 @@ import { safeName } from "./safe-name.js";
|
|
|
12
12
|
import { exportSubParts, resolveParams, buildPosed } from "./part-model.js";
|
|
13
13
|
|
|
14
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
|
|
16
|
-
//
|
|
17
|
-
//
|
|
18
|
-
//
|
|
19
|
-
//
|
|
20
|
-
//
|
|
21
|
-
//
|
|
15
|
+
// JS payload in the worker's graph (measure/verify/build and silhouette/match), and
|
|
16
|
+
// only the `inspect` job runs any of it — the generate/export hot path touches none.
|
|
17
|
+
// Each family below is a literal dynamic import(), which Vite splits into its own
|
|
18
|
+
// chunk under `worker.format: "es"` (this repo's config and partforge-cloud's both),
|
|
19
|
+
// so a user who never runs an oracle job never downloads or parses one. The module
|
|
20
|
+
// loader caches the namespace after the first await, so repeat jobs pay a
|
|
21
|
+
// resolved-promise tick, not a re-fetch.
|
|
22
22
|
// test/worker-layering.test.js's eager-closure guard holds this in place.
|
|
23
23
|
const loadInspect = () => Promise.all([
|
|
24
24
|
import("./oracle/build.js"),
|
|
25
25
|
import("./oracle/measure.js"),
|
|
26
26
|
import("./oracle/verify.js"),
|
|
27
27
|
]);
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
28
|
+
|
|
29
|
+
// The DESCRIBE stack is not part of this package at all: the semantic mesh oracle
|
|
30
|
+
// lives in a separate, closed package, and this open framework never names it. A
|
|
31
|
+
// host that has it INJECTS a loader — `runWorker(part, { loadOracle })`, threaded
|
|
32
|
+
// here as `opts.loadOracle`, resolving to the oracle package's barrel (describe,
|
|
33
|
+
// describeMemo, compactDescribe). Injection rather than a bare import specifier is
|
|
34
|
+
// deliberate: a literal `import("@scope/pkg")` in open source would fail every
|
|
35
|
+
// downstream Vite build where the package isn't installed, while an injected thunk
|
|
36
|
+
// is simply absent — and an absent oracle answers the job with the structured
|
|
37
|
+
// `oracle-unavailable` report below instead of stalling or throwing.
|
|
32
38
|
|
|
33
39
|
// One describe memo for the life of this worker, created alongside the stack's first
|
|
34
40
|
// load. Deliberately NOT swept on setPart the way solid-cache is: describe is pure in
|
|
@@ -417,7 +423,24 @@ export async function handle(kernel, part, msg, post, opts = {}) {
|
|
|
417
423
|
// Manifold only, and not by choice on this path: mesh imports on OCCT are never
|
|
418
424
|
// attempted, so a describe job posted to an OCCT worker is a routing bug, not a
|
|
419
425
|
// fallback opportunity. It surfaces as an ordinary error rather than a reroute.
|
|
420
|
-
|
|
426
|
+
if (!opts.loadOracle) {
|
|
427
|
+
// Same closed-set, returned-not-thrown error contract describe itself keeps
|
|
428
|
+
// (its errors are findings, not crashes) — shaped as the structured triple the
|
|
429
|
+
// whole repo emits, so a caller can act on the code. ERROR-PATTERNS.md#describe-
|
|
430
|
+
// oracle-unavailable carries the fix.
|
|
431
|
+
post({ type: "describe-report", report: {
|
|
432
|
+
error: "oracle-unavailable",
|
|
433
|
+
detail: "this app was built without the mesh oracle package",
|
|
434
|
+
diagnostic: {
|
|
435
|
+
cause: "the describe job needs the closed oracle package, and no loadOracle loader was injected into runWorker",
|
|
436
|
+
location: `describe "${msg.importName}"`,
|
|
437
|
+
correctiveAction: "install the oracle package and pass runWorker(part, { loadOracle: () => import(...) }); see ERROR-PATTERNS.md#describe-oracle-unavailable",
|
|
438
|
+
},
|
|
439
|
+
source: { name: msg.importName, digest: null },
|
|
440
|
+
} });
|
|
441
|
+
return;
|
|
442
|
+
}
|
|
443
|
+
const { describe: describeMesh, describeMemo, compactDescribe } = await opts.loadOracle();
|
|
421
444
|
const solid = kernel.import(msg.importName); // throws on an unknown name
|
|
422
445
|
// `_importDigest` is the backend's existing underscore side-channel (KERNEL-CONTRACT
|
|
423
446
|
// "Conformance classes") — the same digest already folded into every import cache key.
|
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/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";
|