brepjs-cad 0.29.0 → 0.30.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/CHANGELOG.md +33 -0
- package/dist/{brepjs-verify.cjs → brepjs-cad.cjs} +1 -1
- package/dist/{brepjs-verify.js → brepjs-cad.js} +1 -1
- package/dist/cli/main.cjs +83 -7
- package/dist/cli/main.js +83 -7
- package/dist/{diff-kQJXMzLR.cjs → diff-DhOTZnG_.cjs} +227 -22
- package/dist/{diff-DcKUadBa.js → diff-MBAc87Xi.js} +227 -22
- package/dist/mcp/server.cjs +2 -2
- package/dist/mcp/server.js +2 -2
- package/dist/sandbox/runProgram.d.ts +2 -0
- package/dist/snapshot/aiming.d.ts +57 -0
- package/dist/snapshot/registry.cjs +1 -1
- package/dist/snapshot/registry.js +1 -1
- package/dist/snapshot/shoot.cjs +46 -11
- package/dist/snapshot/shoot.d.ts +17 -1
- package/dist/snapshot/shoot.js +46 -11
- package/dist/snapshot/static.cjs +1 -1
- package/dist/snapshot/static.d.ts +1 -1
- package/dist/snapshot/static.js +1 -1
- package/dist/verify/checks.d.ts +7 -1
- package/dist/verify/manufacturability.d.ts +11 -0
- package/dist/verify/report.d.ts +59 -0
- package/dist/verify/runPart.d.ts +7 -0
- package/package.json +7 -7
- package/viewer/dist/assets/{brepjs-uHg2lsd8.js → brepjs-D4FXZDQp.js} +2 -2
- package/viewer/dist/assets/index-DNI62iOP.js +4204 -0
- package/viewer/dist/assets/{kernelWorker-DrYQAV_l.js → kernelWorker-C64yFqcn.js} +1 -1
- package/viewer/dist/index.html +1 -1
- package/viewer/dist/assets/index-y1-v1YEX.js +0 -4167
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,38 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [0.30.0](https://github.com/andymai/brepjs/compare/brepjs-cad-v0.29.0...brepjs-cad-v0.30.0) (2026-06-22)
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
### Features
|
|
7
|
+
|
|
8
|
+
* **brepjs-cad:** add airfoils.md feature recipe (swept fans/props/impellers) ([#1566](https://github.com/andymai/brepjs/issues/1566)) ([8c786db](https://github.com/andymai/brepjs/commit/8c786db69ead59edcdc9d0f8525e43324552e4db))
|
|
9
|
+
* **brepjs-cad:** aimed section cut for the design judge (Phase 3) ([#1543](https://github.com/andymai/brepjs/issues/1543)) ([40e5acd](https://github.com/andymai/brepjs/commit/40e5acd54318d768dc5a620403d17d43e358e698))
|
|
10
|
+
* **brepjs-cad:** decomposed, cross-checked design-judge rubric (Phase 1.5) ([#1539](https://github.com/andymai/brepjs/issues/1539)) ([8059b6d](https://github.com/andymai/brepjs/commit/8059b6d701ae5980e6bb11f51695aa102fe2bb43))
|
|
11
|
+
* **brepjs-cad:** deterministic body/interference metrics for the design judge (Phase 1) ([#1531](https://github.com/andymai/brepjs/issues/1531)) ([d296b54](https://github.com/andymai/brepjs/commit/d296b545f2d7084b861683d61b626ace4747a371))
|
|
12
|
+
* **brepjs-cad:** kernel-anchored Set-of-Marks for the design judge ([#1545](https://github.com/andymai/brepjs/issues/1545)) ([2b6f5b4](https://github.com/andymai/brepjs/commit/2b6f5b436f2a73c96747cc71f83afc8715765cd0))
|
|
13
|
+
* **brepjs-cad:** reliable internal-bore detection for the design judge (keystone) ([#1542](https://github.com/andymai/brepjs/issues/1542)) ([0d780c3](https://github.com/andymai/brepjs/commit/0d780c3b73c2551dab2f04a6ff31bf9ec8cae2ab))
|
|
14
|
+
* **brepjs-cad:** surface fragmentation in verify --check (the [#1](https://github.com/andymai/brepjs/issues/1) design defect) ([#1560](https://github.com/andymai/brepjs/issues/1560)) ([45a4f6e](https://github.com/andymai/brepjs/commit/45a4f6e268331b7816cd9957fbdab4c0893b2000))
|
|
15
|
+
* **brepjs-cad:** xray internal-reveal shot for the design judge (Phase 2a) ([#1540](https://github.com/andymai/brepjs/issues/1540)) ([eca5bc9](https://github.com/andymai/brepjs/commit/eca5bc9ad20fc7947132d647952245cfe466a7f4))
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
### Bug Fixes
|
|
19
|
+
|
|
20
|
+
* **brepjs-cad:** add a triangular-gusset recipe to the implement skill (bracket/enclosure eval) ([#1571](https://github.com/andymai/brepjs/issues/1571)) ([346d042](https://github.com/andymai/brepjs/commit/346d0423fefa3bd163f4075f69bf96d478543bf5))
|
|
21
|
+
* **brepjs-cad:** add the "realize the designed object" bar to the implement skill ([#1563](https://github.com/andymai/brepjs/issues/1563)) ([e150357](https://github.com/andymai/brepjs/commit/e1503570aaf271ebef1e39d48be9c70a5ffe8f03))
|
|
22
|
+
* **brepjs-cad:** close gear-teeth edge cases found by /eval-skill (ring, worm tip relief, GT2) ([#1548](https://github.com/andymai/brepjs/issues/1548)) ([4bd0494](https://github.com/andymai/brepjs/commit/4bd049401f3affcc3f38e3a31bb9520653a3e89c))
|
|
23
|
+
* **brepjs-cad:** code + hint the degenerate-edge (duplicate-vertex) verify failure ([#1550](https://github.com/andymai/brepjs/issues/1550)) ([502dffc](https://github.com/andymai/brepjs/commit/502dffc4a2ae495ae5c98b0c7966721abe0d5f48))
|
|
24
|
+
* **brepjs-cad:** correct spur-gear example flanks + add root fillet ([#1528](https://github.com/andymai/brepjs/issues/1528)) ([6971c6b](https://github.com/andymai/brepjs/commit/6971c6befa1fea36a8c2f3ee89c4aa20946020c7))
|
|
25
|
+
* **brepjs-cad:** exploded-joint legibility + disjoint-compound caveat (basics eval) ([#1565](https://github.com/andymai/brepjs/issues/1565)) ([9d06121](https://github.com/andymai/brepjs/commit/9d0612111a595e57fcfca6fd191ed9169fc2c062))
|
|
26
|
+
* **brepjs-cad:** handle brepjs/playground refs + summarize body relations in the digest ([#1547](https://github.com/andymai/brepjs/issues/1547)) ([b0acef0](https://github.com/andymai/brepjs/commit/b0acef0ad80cdd61f0d2683f1e6d0ece0cae028e))
|
|
27
|
+
* **brepjs-cad:** heal 6 implement-skill findings from the full-flywheel re-run ([#1568](https://github.com/andymai/brepjs/issues/1568)) ([f63bf3d](https://github.com/andymai/brepjs/commit/f63bf3d5a0aa638d06e11dd7e2c27e84dbf74746))
|
|
28
|
+
* **brepjs-cad:** heal implement + polish skills from clean-room eval ([#1526](https://github.com/andymai/brepjs/issues/1526)) ([0645e16](https://github.com/andymai/brepjs/commit/0645e163e1d8759c95406d0f9526d0aa3b5dd480))
|
|
29
|
+
* **brepjs-cad:** heal implement skill — deep-stack bounds extremes aren't datums ([#1529](https://github.com/andymai/brepjs/issues/1529)) ([5ed366f](https://github.com/andymai/brepjs/commit/5ed366f266b3e6e50ae4b54d5779aa05453e894a))
|
|
30
|
+
* **brepjs-cad:** heal implement skill from the eval flywheel (3 findings) ([#1552](https://github.com/andymai/brepjs/issues/1552)) ([51def82](https://github.com/andymai/brepjs/commit/51def82db4e0600e85f45e4dd828cc9caee58d15))
|
|
31
|
+
* **brepjs-cad:** heal implement skill from the full-corpus eval flywheel (4 findings) ([#1554](https://github.com/andymai/brepjs/issues/1554)) ([3cfc28e](https://github.com/andymai/brepjs/commit/3cfc28ed8b82c46330f1d75f1e98c13fd4d65a06))
|
|
32
|
+
* **brepjs-cad:** make blind-judge reference adaptation render multi-body parts at scale ([#1530](https://github.com/andymai/brepjs/issues/1530)) ([18d4523](https://github.com/andymai/brepjs/commit/18d4523c85e1b22496bb5c0b8fb382a1146429c6))
|
|
33
|
+
* **brepjs-cad:** polish skill — bosses on shells must fuse, or they float (eval finding) ([#1553](https://github.com/andymai/brepjs/issues/1553)) ([07cbf93](https://github.com/andymai/brepjs/commit/07cbf93e47fbf880df53bc2a1cee05bf0d17e2d5))
|
|
34
|
+
* **brepjs-cad:** teach worm-wheel and rack tooth recipes; fix gear-build contradiction ([#1544](https://github.com/andymai/brepjs/issues/1544)) ([b0da85f](https://github.com/andymai/brepjs/commit/b0da85f218d463e0f1ef0ca8760cec54b4814b02))
|
|
35
|
+
|
|
3
36
|
## [0.29.0](https://github.com/andymai/brepjs/compare/brepjs-cad-v0.28.0...brepjs-cad-v0.29.0) (2026-06-20)
|
|
4
37
|
|
|
5
38
|
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
2
|
-
const require_diff = require("./diff-
|
|
2
|
+
const require_diff = require("./diff-DhOTZnG_.cjs");
|
|
3
3
|
exports.DEFAULT_TOLERANCE_PCT = require_diff.DEFAULT_TOLERANCE_PCT;
|
|
4
4
|
exports.TYPECHECK_CODE = require_diff.TYPECHECK_CODE;
|
|
5
5
|
exports.emptyReport = require_diff.emptyReport;
|
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import { a as typecheckPart, c as isExpectedDims, d as emptyReport, i as TYPECHECK_CODE, l as pctDelta, m as serializeReport, n as runMeasure, o as DEFAULT_TOLERANCE_PCT, r as runPart, s as evaluateExpected, t as runDiff, u as runChecks } from "./diff-
|
|
1
|
+
import { a as typecheckPart, c as isExpectedDims, d as emptyReport, i as TYPECHECK_CODE, l as pctDelta, m as serializeReport, n as runMeasure, o as DEFAULT_TOLERANCE_PCT, r as runPart, s as evaluateExpected, t as runDiff, u as runChecks } from "./diff-MBAc87Xi.js";
|
|
2
2
|
export { DEFAULT_TOLERANCE_PCT, TYPECHECK_CODE, emptyReport, evaluateExpected, isExpectedDims, pctDelta, runChecks, runDiff, runMeasure, runPart, serializeReport, typecheckPart };
|
package/dist/cli/main.cjs
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
3
|
-
const require_diff = require("../diff-
|
|
3
|
+
const require_diff = require("../diff-DhOTZnG_.cjs");
|
|
4
4
|
let node_url = require("node:url");
|
|
5
5
|
let node_fs = require("node:fs");
|
|
6
6
|
let node_path = require("node:path");
|
|
@@ -215,6 +215,67 @@ function openBrowser(url, platform = process.platform) {
|
|
|
215
215
|
} catch {}
|
|
216
216
|
}
|
|
217
217
|
//#endregion
|
|
218
|
+
//#region src/snapshot/aiming.ts
|
|
219
|
+
/**
|
|
220
|
+
* Kernel-anchored marks for the judge: `B<index>` at each body's bbox centroid (only for multi-body
|
|
221
|
+
* parts — a single body needs no label) and `H1..` at each bore's axis. The same id lands on the same
|
|
222
|
+
* feature across every view (it's a 3D anchor projected per-view), giving the judge view-invariant,
|
|
223
|
+
* part-space addressing without per-body mesh color.
|
|
224
|
+
*
|
|
225
|
+
* Body labels use the body's INDEX (0-based), matching how the measured-facts digest refers to bodies
|
|
226
|
+
* ("bodies 0&1: interfering"), so the judge can cross-reference a mark with the facts. A body that
|
|
227
|
+
* can't be located (no bounds) simply gets no mark — there is no resequenced gap.
|
|
228
|
+
*/
|
|
229
|
+
function featureMarks(bodies, bores) {
|
|
230
|
+
const marks = [];
|
|
231
|
+
if (bodies.length > 1) for (const b of bodies) {
|
|
232
|
+
if (!b.bounds) continue;
|
|
233
|
+
marks.push({
|
|
234
|
+
label: `B${b.index}`,
|
|
235
|
+
pos: [
|
|
236
|
+
(b.bounds.xMin + b.bounds.xMax) / 2,
|
|
237
|
+
(b.bounds.yMin + b.bounds.yMax) / 2,
|
|
238
|
+
(b.bounds.zMin + b.bounds.zMax) / 2
|
|
239
|
+
]
|
|
240
|
+
});
|
|
241
|
+
}
|
|
242
|
+
bores.forEach((bore, i) => {
|
|
243
|
+
marks.push({
|
|
244
|
+
label: `H${i + 1}`,
|
|
245
|
+
pos: bore.axisOrigin
|
|
246
|
+
});
|
|
247
|
+
});
|
|
248
|
+
return marks;
|
|
249
|
+
}
|
|
250
|
+
/**
|
|
251
|
+
* Aim a section cut through the dominant (largest-radius) bore: cut on the basis axis MOST
|
|
252
|
+
* perpendicular to the bore axis, positioned to pass THROUGH the bore origin — so the bore opens as
|
|
253
|
+
* a clean cross-section (reads wall thickness, where the xray is cluttered). Returns null when there
|
|
254
|
+
* are no bores or no bounds. Plain structural inputs keep this decoupled from the verify report.
|
|
255
|
+
*/
|
|
256
|
+
function aimedSection(bores, bounds) {
|
|
257
|
+
if (bores.length === 0 || !bounds) return null;
|
|
258
|
+
const bore = bores.reduce((a, b) => b.radius > a.radius ? b : a);
|
|
259
|
+
const d = bore.axisDir;
|
|
260
|
+
const perp = {
|
|
261
|
+
x: Math.abs(d[0]),
|
|
262
|
+
y: Math.abs(d[1]),
|
|
263
|
+
z: Math.abs(d[2])
|
|
264
|
+
};
|
|
265
|
+
const axis = [
|
|
266
|
+
"x",
|
|
267
|
+
"y",
|
|
268
|
+
"z"
|
|
269
|
+
].reduce((a, b) => perp[b] < perp[a] ? b : a);
|
|
270
|
+
const i = axis === "x" ? 0 : axis === "y" ? 1 : 2;
|
|
271
|
+
const min = axis === "x" ? bounds.xMin : axis === "y" ? bounds.yMin : bounds.zMin;
|
|
272
|
+
const span = (axis === "x" ? bounds.xMax : axis === "y" ? bounds.yMax : bounds.zMax) - min || 1;
|
|
273
|
+
return {
|
|
274
|
+
axis,
|
|
275
|
+
frac: Math.min(1, Math.max(0, (bore.axisOrigin[i] - min) / span))
|
|
276
|
+
};
|
|
277
|
+
}
|
|
278
|
+
//#endregion
|
|
218
279
|
//#region src/cli/main.ts
|
|
219
280
|
console.log = (...args) => {
|
|
220
281
|
process.stderr.write(args.map(String).join(" ") + "\n");
|
|
@@ -230,7 +291,7 @@ async function loadSnapshotShoot() {
|
|
|
230
291
|
}
|
|
231
292
|
var program = new commander.Command();
|
|
232
293
|
program.name("brep");
|
|
233
|
-
program.command("verify", { isDefault: true }).argument("<files...>", "path(s) or glob(s) to .brep.ts modules; multiple files → a batch validity JSON array").option("--step <out>", "write the primary STEP artifact to this path").option("--glb <out>", "write a derived GLB preview to this path").option("--json <out>", "write the JSON report to this path").option("--check", "type-check the part (against brepjs types) before running; skip execution on type errors").option("--snapshot <dir>", "render iso/front/top/right PNGs to this dir (requires built viewer)").option("--serve", "after verifying, start a preview server and print a ?dir=&file= deep link (stays running)").option("--no-open", "with --serve, do not auto-open the browser (just print the viewer URL)").option("--expect-code <code>", "exit 0 only if the report emits this error code (for fixtures/eval recall)").option("--expect-invalid", "exit 0 only if the part is invalid (ok:false) — for known-bad fixtures").action(async (files, opts) => {
|
|
294
|
+
program.command("verify", { isDefault: true }).argument("<files...>", "path(s) or glob(s) to .brep.ts modules; multiple files → a batch validity JSON array").option("--step <out>", "write the primary STEP artifact to this path").option("--glb <out>", "write a derived GLB preview to this path").option("--json <out>", "write the JSON report to this path").option("--check", "type-check the part (against brepjs types) before running; skip execution on type errors").option("--snapshot <dir>", "render iso/front/top/right PNGs to this dir (requires built viewer)").option("--serve", "after verifying, start a preview server and print a ?dir=&file= deep link (stays running)").option("--no-open", "with --serve, do not auto-open the browser (just print the viewer URL)").option("--expect-code <code>", "exit 0 only if the report emits this error code (for fixtures/eval recall)").option("--expect-invalid", "exit 0 only if the part is invalid (ok:false) — for known-bad fixtures").option("--metrics", "compute deterministic manufacturability metrics (per-body breakdown + interference matrix) for the design judge — slower; off by default").action(async (files, opts) => {
|
|
234
295
|
const targets = [...new Set(files.flatMap((f) => {
|
|
235
296
|
const m = (0, node_fs.globSync)(f);
|
|
236
297
|
return m.length ? m : [f];
|
|
@@ -239,7 +300,10 @@ program.command("verify", { isDefault: true }).argument("<files...>", "path(s) o
|
|
|
239
300
|
if (opts.step || opts.serve || opts.snapshot || opts.glb || opts.expectCode || opts.expectInvalid) process.stderr.write("batch mode: --step/--glb/--serve/--snapshot/--expect-code/--expect-invalid ignored (single-file only)\n");
|
|
240
301
|
const results = [];
|
|
241
302
|
for (const f of targets) {
|
|
242
|
-
const { report, shape } = await require_diff.runPart(f, {
|
|
303
|
+
const { report, shape } = await require_diff.runPart(f, {
|
|
304
|
+
check: Boolean(opts.check),
|
|
305
|
+
metrics: Boolean(opts.metrics)
|
|
306
|
+
});
|
|
243
307
|
disposeShape(shape);
|
|
244
308
|
results.push({
|
|
245
309
|
file: f,
|
|
@@ -258,7 +322,8 @@ program.command("verify", { isDefault: true }).argument("<files...>", "path(s) o
|
|
|
258
322
|
const { report, step, glb, shape } = await require_diff.runPart((0, node_path.resolve)(file), {
|
|
259
323
|
step: wantStep,
|
|
260
324
|
glb: Boolean(opts.glb),
|
|
261
|
-
check: Boolean(opts.check)
|
|
325
|
+
check: Boolean(opts.check),
|
|
326
|
+
metrics: Boolean(opts.metrics) || Boolean(opts.snapshot)
|
|
262
327
|
});
|
|
263
328
|
let stepPath = opts.step;
|
|
264
329
|
try {
|
|
@@ -270,9 +335,13 @@ program.command("verify", { isDefault: true }).argument("<files...>", "path(s) o
|
|
|
270
335
|
if (opts.snapshot && stepPath) {
|
|
271
336
|
const shoot = await loadSnapshotShoot();
|
|
272
337
|
if (shoot) {
|
|
338
|
+
const section = aimedSection(report.manufacturability?.bores ?? [], report.measurements.bounds);
|
|
339
|
+
const marks = featureMarks(report.bodies ?? [], report.manufacturability?.bores ?? []);
|
|
273
340
|
const { pngs } = await shoot({
|
|
274
341
|
file: stepPath,
|
|
275
|
-
outDir: opts.snapshot
|
|
342
|
+
outDir: opts.snapshot,
|
|
343
|
+
...section ? { section } : {},
|
|
344
|
+
...marks.length > 0 ? { marks } : {}
|
|
276
345
|
});
|
|
277
346
|
for (const p of pngs) process.stderr.write(`snapshot: ${p}\n`);
|
|
278
347
|
}
|
|
@@ -321,7 +390,10 @@ program.command("snapshot").argument("<file>", "path to a .brep.ts module; rende
|
|
|
321
390
|
const resolved = (0, node_path.resolve)(file);
|
|
322
391
|
const base = opts.out ? (0, node_path.resolve)(opts.out) : `${resolved}-shots`;
|
|
323
392
|
const outDir = opts.label ? (0, node_path.join)(base, opts.label) : base;
|
|
324
|
-
const { report, step, shape } = await require_diff.runPart(resolved, {
|
|
393
|
+
const { report, step, shape } = await require_diff.runPart(resolved, {
|
|
394
|
+
step: true,
|
|
395
|
+
metrics: true
|
|
396
|
+
});
|
|
325
397
|
try {
|
|
326
398
|
if (!step) {
|
|
327
399
|
process.stderr.write("snapshot: part produced no STEP — nothing to render\n");
|
|
@@ -336,10 +408,14 @@ program.command("snapshot").argument("<file>", "path to a .brep.ts module; rende
|
|
|
336
408
|
process.stdout.write(require_diff.serializeReport(report) + "\n");
|
|
337
409
|
return;
|
|
338
410
|
}
|
|
411
|
+
const section = aimedSection(report.manufacturability?.bores ?? [], report.measurements.bounds);
|
|
412
|
+
const marks = featureMarks(report.bodies ?? [], report.manufacturability?.bores ?? []);
|
|
339
413
|
const { pngs } = await shoot({
|
|
340
414
|
file: stepPath,
|
|
341
415
|
outDir,
|
|
342
|
-
fresh: true
|
|
416
|
+
fresh: true,
|
|
417
|
+
...section ? { section } : {},
|
|
418
|
+
...marks.length > 0 ? { marks } : {}
|
|
343
419
|
});
|
|
344
420
|
for (const p of pngs) process.stdout.write(`${p}\n`);
|
|
345
421
|
} finally {
|
package/dist/cli/main.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import { f as pushError, h as loadBrep, m as serializeReport, n as runMeasure, p as reportOk, r as runPart, t as runDiff } from "../diff-
|
|
2
|
+
import { f as pushError, h as loadBrep, m as serializeReport, n as runMeasure, p as reportOk, r as runPart, t as runDiff } from "../diff-MBAc87Xi.js";
|
|
3
3
|
import { fileURLToPath } from "node:url";
|
|
4
4
|
import { existsSync, globSync, mkdirSync, realpathSync, watch, writeFileSync } from "node:fs";
|
|
5
5
|
import { basename, dirname, join, resolve } from "node:path";
|
|
@@ -214,6 +214,67 @@ function openBrowser(url, platform = process.platform) {
|
|
|
214
214
|
} catch {}
|
|
215
215
|
}
|
|
216
216
|
//#endregion
|
|
217
|
+
//#region src/snapshot/aiming.ts
|
|
218
|
+
/**
|
|
219
|
+
* Kernel-anchored marks for the judge: `B<index>` at each body's bbox centroid (only for multi-body
|
|
220
|
+
* parts — a single body needs no label) and `H1..` at each bore's axis. The same id lands on the same
|
|
221
|
+
* feature across every view (it's a 3D anchor projected per-view), giving the judge view-invariant,
|
|
222
|
+
* part-space addressing without per-body mesh color.
|
|
223
|
+
*
|
|
224
|
+
* Body labels use the body's INDEX (0-based), matching how the measured-facts digest refers to bodies
|
|
225
|
+
* ("bodies 0&1: interfering"), so the judge can cross-reference a mark with the facts. A body that
|
|
226
|
+
* can't be located (no bounds) simply gets no mark — there is no resequenced gap.
|
|
227
|
+
*/
|
|
228
|
+
function featureMarks(bodies, bores) {
|
|
229
|
+
const marks = [];
|
|
230
|
+
if (bodies.length > 1) for (const b of bodies) {
|
|
231
|
+
if (!b.bounds) continue;
|
|
232
|
+
marks.push({
|
|
233
|
+
label: `B${b.index}`,
|
|
234
|
+
pos: [
|
|
235
|
+
(b.bounds.xMin + b.bounds.xMax) / 2,
|
|
236
|
+
(b.bounds.yMin + b.bounds.yMax) / 2,
|
|
237
|
+
(b.bounds.zMin + b.bounds.zMax) / 2
|
|
238
|
+
]
|
|
239
|
+
});
|
|
240
|
+
}
|
|
241
|
+
bores.forEach((bore, i) => {
|
|
242
|
+
marks.push({
|
|
243
|
+
label: `H${i + 1}`,
|
|
244
|
+
pos: bore.axisOrigin
|
|
245
|
+
});
|
|
246
|
+
});
|
|
247
|
+
return marks;
|
|
248
|
+
}
|
|
249
|
+
/**
|
|
250
|
+
* Aim a section cut through the dominant (largest-radius) bore: cut on the basis axis MOST
|
|
251
|
+
* perpendicular to the bore axis, positioned to pass THROUGH the bore origin — so the bore opens as
|
|
252
|
+
* a clean cross-section (reads wall thickness, where the xray is cluttered). Returns null when there
|
|
253
|
+
* are no bores or no bounds. Plain structural inputs keep this decoupled from the verify report.
|
|
254
|
+
*/
|
|
255
|
+
function aimedSection(bores, bounds) {
|
|
256
|
+
if (bores.length === 0 || !bounds) return null;
|
|
257
|
+
const bore = bores.reduce((a, b) => b.radius > a.radius ? b : a);
|
|
258
|
+
const d = bore.axisDir;
|
|
259
|
+
const perp = {
|
|
260
|
+
x: Math.abs(d[0]),
|
|
261
|
+
y: Math.abs(d[1]),
|
|
262
|
+
z: Math.abs(d[2])
|
|
263
|
+
};
|
|
264
|
+
const axis = [
|
|
265
|
+
"x",
|
|
266
|
+
"y",
|
|
267
|
+
"z"
|
|
268
|
+
].reduce((a, b) => perp[b] < perp[a] ? b : a);
|
|
269
|
+
const i = axis === "x" ? 0 : axis === "y" ? 1 : 2;
|
|
270
|
+
const min = axis === "x" ? bounds.xMin : axis === "y" ? bounds.yMin : bounds.zMin;
|
|
271
|
+
const span = (axis === "x" ? bounds.xMax : axis === "y" ? bounds.yMax : bounds.zMax) - min || 1;
|
|
272
|
+
return {
|
|
273
|
+
axis,
|
|
274
|
+
frac: Math.min(1, Math.max(0, (bore.axisOrigin[i] - min) / span))
|
|
275
|
+
};
|
|
276
|
+
}
|
|
277
|
+
//#endregion
|
|
217
278
|
//#region src/cli/main.ts
|
|
218
279
|
console.log = (...args) => {
|
|
219
280
|
process.stderr.write(args.map(String).join(" ") + "\n");
|
|
@@ -229,7 +290,7 @@ async function loadSnapshotShoot() {
|
|
|
229
290
|
}
|
|
230
291
|
var program = new Command();
|
|
231
292
|
program.name("brep");
|
|
232
|
-
program.command("verify", { isDefault: true }).argument("<files...>", "path(s) or glob(s) to .brep.ts modules; multiple files → a batch validity JSON array").option("--step <out>", "write the primary STEP artifact to this path").option("--glb <out>", "write a derived GLB preview to this path").option("--json <out>", "write the JSON report to this path").option("--check", "type-check the part (against brepjs types) before running; skip execution on type errors").option("--snapshot <dir>", "render iso/front/top/right PNGs to this dir (requires built viewer)").option("--serve", "after verifying, start a preview server and print a ?dir=&file= deep link (stays running)").option("--no-open", "with --serve, do not auto-open the browser (just print the viewer URL)").option("--expect-code <code>", "exit 0 only if the report emits this error code (for fixtures/eval recall)").option("--expect-invalid", "exit 0 only if the part is invalid (ok:false) — for known-bad fixtures").action(async (files, opts) => {
|
|
293
|
+
program.command("verify", { isDefault: true }).argument("<files...>", "path(s) or glob(s) to .brep.ts modules; multiple files → a batch validity JSON array").option("--step <out>", "write the primary STEP artifact to this path").option("--glb <out>", "write a derived GLB preview to this path").option("--json <out>", "write the JSON report to this path").option("--check", "type-check the part (against brepjs types) before running; skip execution on type errors").option("--snapshot <dir>", "render iso/front/top/right PNGs to this dir (requires built viewer)").option("--serve", "after verifying, start a preview server and print a ?dir=&file= deep link (stays running)").option("--no-open", "with --serve, do not auto-open the browser (just print the viewer URL)").option("--expect-code <code>", "exit 0 only if the report emits this error code (for fixtures/eval recall)").option("--expect-invalid", "exit 0 only if the part is invalid (ok:false) — for known-bad fixtures").option("--metrics", "compute deterministic manufacturability metrics (per-body breakdown + interference matrix) for the design judge — slower; off by default").action(async (files, opts) => {
|
|
233
294
|
const targets = [...new Set(files.flatMap((f) => {
|
|
234
295
|
const m = globSync(f);
|
|
235
296
|
return m.length ? m : [f];
|
|
@@ -238,7 +299,10 @@ program.command("verify", { isDefault: true }).argument("<files...>", "path(s) o
|
|
|
238
299
|
if (opts.step || opts.serve || opts.snapshot || opts.glb || opts.expectCode || opts.expectInvalid) process.stderr.write("batch mode: --step/--glb/--serve/--snapshot/--expect-code/--expect-invalid ignored (single-file only)\n");
|
|
239
300
|
const results = [];
|
|
240
301
|
for (const f of targets) {
|
|
241
|
-
const { report, shape } = await runPart(f, {
|
|
302
|
+
const { report, shape } = await runPart(f, {
|
|
303
|
+
check: Boolean(opts.check),
|
|
304
|
+
metrics: Boolean(opts.metrics)
|
|
305
|
+
});
|
|
242
306
|
disposeShape(shape);
|
|
243
307
|
results.push({
|
|
244
308
|
file: f,
|
|
@@ -257,7 +321,8 @@ program.command("verify", { isDefault: true }).argument("<files...>", "path(s) o
|
|
|
257
321
|
const { report, step, glb, shape } = await runPart(resolve(file), {
|
|
258
322
|
step: wantStep,
|
|
259
323
|
glb: Boolean(opts.glb),
|
|
260
|
-
check: Boolean(opts.check)
|
|
324
|
+
check: Boolean(opts.check),
|
|
325
|
+
metrics: Boolean(opts.metrics) || Boolean(opts.snapshot)
|
|
261
326
|
});
|
|
262
327
|
let stepPath = opts.step;
|
|
263
328
|
try {
|
|
@@ -269,9 +334,13 @@ program.command("verify", { isDefault: true }).argument("<files...>", "path(s) o
|
|
|
269
334
|
if (opts.snapshot && stepPath) {
|
|
270
335
|
const shoot = await loadSnapshotShoot();
|
|
271
336
|
if (shoot) {
|
|
337
|
+
const section = aimedSection(report.manufacturability?.bores ?? [], report.measurements.bounds);
|
|
338
|
+
const marks = featureMarks(report.bodies ?? [], report.manufacturability?.bores ?? []);
|
|
272
339
|
const { pngs } = await shoot({
|
|
273
340
|
file: stepPath,
|
|
274
|
-
outDir: opts.snapshot
|
|
341
|
+
outDir: opts.snapshot,
|
|
342
|
+
...section ? { section } : {},
|
|
343
|
+
...marks.length > 0 ? { marks } : {}
|
|
275
344
|
});
|
|
276
345
|
for (const p of pngs) process.stderr.write(`snapshot: ${p}\n`);
|
|
277
346
|
}
|
|
@@ -320,7 +389,10 @@ program.command("snapshot").argument("<file>", "path to a .brep.ts module; rende
|
|
|
320
389
|
const resolved = resolve(file);
|
|
321
390
|
const base = opts.out ? resolve(opts.out) : `${resolved}-shots`;
|
|
322
391
|
const outDir = opts.label ? join(base, opts.label) : base;
|
|
323
|
-
const { report, step, shape } = await runPart(resolved, {
|
|
392
|
+
const { report, step, shape } = await runPart(resolved, {
|
|
393
|
+
step: true,
|
|
394
|
+
metrics: true
|
|
395
|
+
});
|
|
324
396
|
try {
|
|
325
397
|
if (!step) {
|
|
326
398
|
process.stderr.write("snapshot: part produced no STEP — nothing to render\n");
|
|
@@ -335,10 +407,14 @@ program.command("snapshot").argument("<file>", "path to a .brep.ts module; rende
|
|
|
335
407
|
process.stdout.write(serializeReport(report) + "\n");
|
|
336
408
|
return;
|
|
337
409
|
}
|
|
410
|
+
const section = aimedSection(report.manufacturability?.bores ?? [], report.measurements.bounds);
|
|
411
|
+
const marks = featureMarks(report.bodies ?? [], report.manufacturability?.bores ?? []);
|
|
338
412
|
const { pngs } = await shoot({
|
|
339
413
|
file: stepPath,
|
|
340
414
|
outDir,
|
|
341
|
-
fresh: true
|
|
415
|
+
fresh: true,
|
|
416
|
+
...section ? { section } : {},
|
|
417
|
+
...marks.length > 0 ? { marks } : {}
|
|
342
418
|
});
|
|
343
419
|
for (const p of pngs) process.stdout.write(`${p}\n`);
|
|
344
420
|
} finally {
|
|
@@ -50,6 +50,199 @@ function initOcctWasm(brep) {
|
|
|
50
50
|
return kernelReady;
|
|
51
51
|
}
|
|
52
52
|
//#endregion
|
|
53
|
+
//#region src/verify/manufacturability.ts
|
|
54
|
+
var RELATION_BUDGET_MS = 15e3;
|
|
55
|
+
var MAX_BODIES = 24;
|
|
56
|
+
var VOL_EPS = 1e-9;
|
|
57
|
+
var SUBSTANTIAL_USPAN = 2.5;
|
|
58
|
+
var BORE_DOT = .5;
|
|
59
|
+
var v3sub = (a, b) => [
|
|
60
|
+
a[0] - b[0],
|
|
61
|
+
a[1] - b[1],
|
|
62
|
+
a[2] - b[2]
|
|
63
|
+
];
|
|
64
|
+
var v3dot = (a, b) => a[0] * b[0] + a[1] * b[1] + a[2] * b[2];
|
|
65
|
+
var v3scale = (a, s) => [
|
|
66
|
+
a[0] * s,
|
|
67
|
+
a[1] * s,
|
|
68
|
+
a[2] * s
|
|
69
|
+
];
|
|
70
|
+
var v3unit = (a) => {
|
|
71
|
+
const n = Math.hypot(a[0], a[1], a[2]) || 1;
|
|
72
|
+
return [
|
|
73
|
+
a[0] / n,
|
|
74
|
+
a[1] / n,
|
|
75
|
+
a[2] / n
|
|
76
|
+
];
|
|
77
|
+
};
|
|
78
|
+
function diagonalOf(boundsList) {
|
|
79
|
+
if (boundsList.length === 0) return 1;
|
|
80
|
+
const u = {
|
|
81
|
+
xMin: Math.min(...boundsList.map((b) => b.xMin)),
|
|
82
|
+
yMin: Math.min(...boundsList.map((b) => b.yMin)),
|
|
83
|
+
zMin: Math.min(...boundsList.map((b) => b.zMin)),
|
|
84
|
+
xMax: Math.max(...boundsList.map((b) => b.xMax)),
|
|
85
|
+
yMax: Math.max(...boundsList.map((b) => b.yMax)),
|
|
86
|
+
zMax: Math.max(...boundsList.map((b) => b.zMax))
|
|
87
|
+
};
|
|
88
|
+
return Math.hypot(u.xMax - u.xMin, u.yMax - u.yMin, u.zMax - u.zMin) || 1;
|
|
89
|
+
}
|
|
90
|
+
function aabbDisjoint(a, b, eps) {
|
|
91
|
+
return a.xMax + eps < b.xMin || b.xMax + eps < a.xMin || a.yMax + eps < b.yMin || b.yMax + eps < a.yMin || a.zMax + eps < b.zMin || b.zMax + eps < a.zMin;
|
|
92
|
+
}
|
|
93
|
+
/** Per-body volume/bounds/validity. Used both for the report and to drive the relation matrix. */
|
|
94
|
+
function computeBodies(brep, solids) {
|
|
95
|
+
const { measureVolume, getBounds, validSolid, isOk } = brep;
|
|
96
|
+
return solids.map((s, index) => {
|
|
97
|
+
const v = measureVolume(s);
|
|
98
|
+
const volume = isOk(v) ? v.value : 0;
|
|
99
|
+
let bounds;
|
|
100
|
+
try {
|
|
101
|
+
bounds = getBounds(s);
|
|
102
|
+
} catch {}
|
|
103
|
+
return {
|
|
104
|
+
index,
|
|
105
|
+
volume,
|
|
106
|
+
...bounds ? { bounds } : {},
|
|
107
|
+
valid: isOk(validSolid(s))
|
|
108
|
+
};
|
|
109
|
+
});
|
|
110
|
+
}
|
|
111
|
+
/**
|
|
112
|
+
* Pairwise body relationships. `separate` when the bodies are apart (AABB-disjoint, or measured
|
|
113
|
+
* clearance above eps); `interfering` when they touch or overlap (clearance within eps). `touching`/
|
|
114
|
+
* `nested` (which need a boolean-volume tier) are reserved for a later phase. eps scales to the
|
|
115
|
+
* assembly diagonal so it is meaningful from millimetre parts to metre-scale BIM.
|
|
116
|
+
*/
|
|
117
|
+
function computeBodyRelations(brep, solids, bodies) {
|
|
118
|
+
const { checkInterference, isOk } = brep;
|
|
119
|
+
const relations = [];
|
|
120
|
+
const eps = Math.max(1e-6, 1e-4 * diagonalOf(bodies.flatMap((b) => b.bounds ? [b.bounds] : [])));
|
|
121
|
+
const start = Date.now();
|
|
122
|
+
for (let i = 0; i < solids.length; i++) for (let j = i + 1; j < solids.length; j++) {
|
|
123
|
+
if (Date.now() - start > RELATION_BUDGET_MS) return {
|
|
124
|
+
relations,
|
|
125
|
+
truncated: true
|
|
126
|
+
};
|
|
127
|
+
const bi = bodies[i]?.bounds;
|
|
128
|
+
const bj = bodies[j]?.bounds;
|
|
129
|
+
if (bi && bj && aabbDisjoint(bi, bj, eps)) {
|
|
130
|
+
relations.push({
|
|
131
|
+
a: i,
|
|
132
|
+
b: j,
|
|
133
|
+
relation: "separate"
|
|
134
|
+
});
|
|
135
|
+
continue;
|
|
136
|
+
}
|
|
137
|
+
const si = solids[i];
|
|
138
|
+
const sj = solids[j];
|
|
139
|
+
if (!si || !sj) continue;
|
|
140
|
+
const res = checkInterference(si, sj, eps);
|
|
141
|
+
if (!isOk(res)) continue;
|
|
142
|
+
relations.push({
|
|
143
|
+
a: i,
|
|
144
|
+
b: j,
|
|
145
|
+
relation: res.value.hasInterference ? "interfering" : "separate",
|
|
146
|
+
clearance: res.value.minDistance
|
|
147
|
+
});
|
|
148
|
+
}
|
|
149
|
+
return {
|
|
150
|
+
relations,
|
|
151
|
+
truncated: false
|
|
152
|
+
};
|
|
153
|
+
}
|
|
154
|
+
/**
|
|
155
|
+
* Cylindrical features: the smallest substantial cylinder radius (bore/shaft; fillet strips excluded
|
|
156
|
+
* by angular extent) and the internal bores (concave, detected by an outward-normal-vs-radial test).
|
|
157
|
+
* The bore axis lets a later phase aim a section cut or anchor a feature mark.
|
|
158
|
+
*/
|
|
159
|
+
function computeCylinderFeatures(brep, shape) {
|
|
160
|
+
const { getFaces, faceGeomType, faceCenter, normalAt, faceOrientation, faceAxis, measureCurvatureAtMid, uvBounds, isOk } = brep;
|
|
161
|
+
const bores = [];
|
|
162
|
+
let minRadius;
|
|
163
|
+
let faces;
|
|
164
|
+
try {
|
|
165
|
+
faces = getFaces(shape);
|
|
166
|
+
} catch {
|
|
167
|
+
return { bores };
|
|
168
|
+
}
|
|
169
|
+
for (const f of faces) {
|
|
170
|
+
if ((() => {
|
|
171
|
+
try {
|
|
172
|
+
return faceGeomType(f);
|
|
173
|
+
} catch {
|
|
174
|
+
return null;
|
|
175
|
+
}
|
|
176
|
+
})() !== "CYLINDRE") continue;
|
|
177
|
+
const ax = faceAxis(f);
|
|
178
|
+
if (!ax) continue;
|
|
179
|
+
const curv = measureCurvatureAtMid(f);
|
|
180
|
+
if (!isOk(curv)) continue;
|
|
181
|
+
const k = Math.max(Math.abs(curv.value.maxCurvature), Math.abs(curv.value.minCurvature));
|
|
182
|
+
if (k <= 0) continue;
|
|
183
|
+
const radius = 1 / k;
|
|
184
|
+
try {
|
|
185
|
+
const uv = uvBounds(f);
|
|
186
|
+
if (uv.uMax - uv.uMin <= SUBSTANTIAL_USPAN) continue;
|
|
187
|
+
} catch {
|
|
188
|
+
continue;
|
|
189
|
+
}
|
|
190
|
+
minRadius = minRadius === void 0 ? radius : Math.min(minRadius, radius);
|
|
191
|
+
try {
|
|
192
|
+
const p = faceCenter(f);
|
|
193
|
+
const o = ax.origin;
|
|
194
|
+
const d = v3unit(ax.direction);
|
|
195
|
+
const rel = v3sub(p, o);
|
|
196
|
+
const radial = v3unit(v3sub(rel, v3scale(d, v3dot(rel, d))));
|
|
197
|
+
const n = normalAt(f);
|
|
198
|
+
if (v3dot(v3unit(faceOrientation(f) === "forward" ? n : v3scale(n, -1)), radial) > BORE_DOT) bores.push({
|
|
199
|
+
radius,
|
|
200
|
+
axisOrigin: [
|
|
201
|
+
o[0],
|
|
202
|
+
o[1],
|
|
203
|
+
o[2]
|
|
204
|
+
],
|
|
205
|
+
axisDir: [
|
|
206
|
+
d[0],
|
|
207
|
+
d[1],
|
|
208
|
+
d[2]
|
|
209
|
+
]
|
|
210
|
+
});
|
|
211
|
+
} catch {}
|
|
212
|
+
}
|
|
213
|
+
return {
|
|
214
|
+
...minRadius !== void 0 ? { minRadius } : {},
|
|
215
|
+
bores
|
|
216
|
+
};
|
|
217
|
+
}
|
|
218
|
+
/** Orchestrate the deterministic metrics for a verified shape. Never throws (caller wraps too). */
|
|
219
|
+
function computeMetrics(brep, shape, solids) {
|
|
220
|
+
const bodies = computeBodies(brep, solids);
|
|
221
|
+
const violations = [];
|
|
222
|
+
bodies.forEach((b) => {
|
|
223
|
+
if (b.volume <= VOL_EPS) violations.push(`body ${b.index} has ~zero volume (degenerate)`);
|
|
224
|
+
});
|
|
225
|
+
const manufacturability = { violations };
|
|
226
|
+
const feat = computeCylinderFeatures(brep, shape);
|
|
227
|
+
if (feat.minRadius !== void 0) manufacturability.minRadius = feat.minRadius;
|
|
228
|
+
if (feat.bores.length > 0) manufacturability.bores = feat.bores;
|
|
229
|
+
if (bodies.length <= 1) return { manufacturability };
|
|
230
|
+
if (bodies.length > MAX_BODIES) {
|
|
231
|
+
manufacturability.relationsTruncated = true;
|
|
232
|
+
return {
|
|
233
|
+
bodies,
|
|
234
|
+
manufacturability
|
|
235
|
+
};
|
|
236
|
+
}
|
|
237
|
+
const { relations, truncated } = computeBodyRelations(brep, solids, bodies);
|
|
238
|
+
if (truncated) manufacturability.relationsTruncated = true;
|
|
239
|
+
return {
|
|
240
|
+
bodies,
|
|
241
|
+
bodyRelations: relations,
|
|
242
|
+
manufacturability
|
|
243
|
+
};
|
|
244
|
+
}
|
|
245
|
+
//#endregion
|
|
53
246
|
//#region src/verify/report.ts
|
|
54
247
|
function emptyReport() {
|
|
55
248
|
return {
|
|
@@ -104,6 +297,10 @@ var HINT_TABLE = {
|
|
|
104
297
|
fix: "Pass a non-zero thickness to `.extrude(h)` — a height of 0 is a zero-length vector, no solid.",
|
|
105
298
|
nextStep: "Set a positive extrusion height (units: mm) and re-verify."
|
|
106
299
|
},
|
|
300
|
+
DEGENERATE_EDGE: {
|
|
301
|
+
fix: "A polygon/wire has coincident consecutive points (a zero-length edge). Dedupe the point list before building — common in computed tooth/gear loops where a land and a groove point land on the same coordinate.",
|
|
302
|
+
nextStep: "Remove duplicate consecutive points from the profile (a tooth’s trailing point is the next tooth’s leading point), then re-verify."
|
|
303
|
+
},
|
|
107
304
|
ZERO_OFFSET: {
|
|
108
305
|
fix: "Offset by a non-zero amount — an offset of 0 is a no-op the kernel rejects.",
|
|
109
306
|
nextStep: "Use a small non-zero offset (positive grows, negative shrinks) and re-verify."
|
|
@@ -264,10 +461,11 @@ function shapeTypeOf(brep, s) {
|
|
|
264
461
|
if (isVertex(s)) return "Vertex";
|
|
265
462
|
return "Unknown";
|
|
266
463
|
}
|
|
267
|
-
function runChecks(brep, shape) {
|
|
464
|
+
function runChecks(brep, shape, opts = {}) {
|
|
268
465
|
const { isSolid, isShape3D, isFace, measureVolumeProps, measureArea, getBounds, getFaces, getEdges, getWires, getVertices, getSolids, getShells, isManifoldShell, validSolid, isOk } = brep;
|
|
269
466
|
const r = emptyReport();
|
|
270
467
|
r.shapeType = shapeTypeOf(brep, shape);
|
|
468
|
+
const solids = isSolid(shape) ? [shape] : getSolids(shape);
|
|
271
469
|
if (isSolid(shape)) {
|
|
272
470
|
const valid = validSolid(shape);
|
|
273
471
|
const validCheck = {
|
|
@@ -282,28 +480,26 @@ function runChecks(brep, shape) {
|
|
|
282
480
|
});
|
|
283
481
|
}
|
|
284
482
|
r.checks.push(validCheck);
|
|
285
|
-
} else {
|
|
286
|
-
const
|
|
287
|
-
|
|
288
|
-
const
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
483
|
+
} else if (solids.length > 0) {
|
|
484
|
+
const failures = [];
|
|
485
|
+
solids.forEach((s, i) => {
|
|
486
|
+
const v = validSolid(s);
|
|
487
|
+
if (!isOk(v)) failures.push(`body ${i}: ${v.error}`);
|
|
488
|
+
});
|
|
489
|
+
const bodiesCheck = {
|
|
490
|
+
name: "allBodiesValid",
|
|
491
|
+
passed: failures.length === 0
|
|
492
|
+
};
|
|
493
|
+
if (failures.length > 0) {
|
|
494
|
+
bodiesCheck.detail = `${failures.length}/${solids.length} bodies invalid — ${failures.join("; ")}`;
|
|
495
|
+
r.errorInfos.push({
|
|
496
|
+
message: `allBodiesValid: ${bodiesCheck.detail}`,
|
|
497
|
+
code: VALIDITY_FAILURE_CODE
|
|
292
498
|
});
|
|
293
|
-
const bodiesCheck = {
|
|
294
|
-
name: "allBodiesValid",
|
|
295
|
-
passed: failures.length === 0
|
|
296
|
-
};
|
|
297
|
-
if (failures.length > 0) {
|
|
298
|
-
bodiesCheck.detail = `${failures.length}/${solids.length} bodies invalid — ${failures.join("; ")}`;
|
|
299
|
-
r.errorInfos.push({
|
|
300
|
-
message: `allBodiesValid: ${bodiesCheck.detail}`,
|
|
301
|
-
code: VALIDITY_FAILURE_CODE
|
|
302
|
-
});
|
|
303
|
-
}
|
|
304
|
-
r.checks.push(bodiesCheck);
|
|
305
499
|
}
|
|
500
|
+
r.checks.push(bodiesCheck);
|
|
306
501
|
}
|
|
502
|
+
if (!isSolid(shape) && solids.length > 1) (r.notes ??= []).push(`shapeType is ${r.shapeType} with ${solids.length} solids. If this is meant to be ONE part, the bodies are NOT welded — give the operands a real overlap and fuseAll(shapes, { unsafe: true }). If it is a deliberate assembly, this is expected. Run --metrics to see whether the bodies touch (a failed weld) or sit apart (an assembly).`);
|
|
307
503
|
if (isShape3D(shape)) {
|
|
308
504
|
const volProps = measureVolumeProps(shape);
|
|
309
505
|
if (isOk(volProps)) {
|
|
@@ -340,6 +536,12 @@ function runChecks(brep, shape) {
|
|
|
340
536
|
const shells = getShells(shape);
|
|
341
537
|
if (shells.length > 0) r.topology.manifold = shells.every((s) => isManifoldShell(s));
|
|
342
538
|
} catch {}
|
|
539
|
+
if (opts.metrics) try {
|
|
540
|
+
const m = computeMetrics(brep, shape, solids);
|
|
541
|
+
if (m.bodies) r.bodies = m.bodies;
|
|
542
|
+
if (m.bodyRelations) r.bodyRelations = m.bodyRelations;
|
|
543
|
+
r.manufacturability = m.manufacturability;
|
|
544
|
+
} catch {}
|
|
343
545
|
r.hints = buildHints(r);
|
|
344
546
|
return r;
|
|
345
547
|
}
|
|
@@ -625,7 +827,7 @@ function toErrorInfo(prefix, e) {
|
|
|
625
827
|
suggestion: e.suggestion
|
|
626
828
|
};
|
|
627
829
|
if (e instanceof Error) {
|
|
628
|
-
const code = e.message.match(/\[[A-Z][A-Z0-9_]*\]\s+([A-Z][A-Z0-9_]+):/)?.[1];
|
|
830
|
+
const code = e.message.match(/\[[A-Z][A-Z0-9_]*\]\s+([A-Z][A-Z0-9_]+):/)?.[1] ?? classifyKernelMessage(e.message);
|
|
629
831
|
return code ? {
|
|
630
832
|
message: `${prefix}: ${e.message}`,
|
|
631
833
|
code
|
|
@@ -633,6 +835,9 @@ function toErrorInfo(prefix, e) {
|
|
|
633
835
|
}
|
|
634
836
|
return { message: `${prefix}: ${String(e)}` };
|
|
635
837
|
}
|
|
838
|
+
function classifyKernelMessage(message) {
|
|
839
|
+
if (/makeLineEdge: construction failed/i.test(message)) return "DEGENERATE_EDGE";
|
|
840
|
+
}
|
|
636
841
|
function finalize(result) {
|
|
637
842
|
result.report.hints = buildHints(result.report);
|
|
638
843
|
return result;
|
|
@@ -706,7 +911,7 @@ async function runPart(modulePath, opts = {}) {
|
|
|
706
911
|
report
|
|
707
912
|
});
|
|
708
913
|
}
|
|
709
|
-
const result = runChecks(brep, shape);
|
|
914
|
+
const result = runChecks(brep, shape, { metrics: Boolean(opts.metrics) });
|
|
710
915
|
if (isExpectedDims(mod.expected)) {
|
|
711
916
|
const expected = mod.expected;
|
|
712
917
|
result.assertions = evaluateExpected(expected, result.measurements);
|