brepjs-cad 0.29.0 → 0.31.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 CHANGED
@@ -1,5 +1,75 @@
1
1
  # Changelog
2
2
 
3
+ ## [0.31.0](https://github.com/andymai/brepjs/compare/brepjs-cad-v0.30.1...brepjs-cad-v0.31.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:** CLI subcommands + verify hints + gridfinity examples + eval harness ([#1204](https://github.com/andymai/brepjs/issues/1204)) ([4d57198](https://github.com/andymai/brepjs/commit/4d5719874b5f5e685a4f909fd2d2363c0331770b))
11
+ * **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))
12
+ * **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))
13
+ * **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))
14
+ * **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))
15
+ * **brepjs-cad:** rename from brepjs-agent + make npm-publishable (publish held) ([#1201](https://github.com/andymai/brepjs/issues/1201)) ([630bbba](https://github.com/andymai/brepjs/commit/630bbbab4885604bd4d5fb2148584a6572c8d99c))
16
+ * **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))
17
+ * **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))
18
+
19
+
20
+ ### Bug Fixes
21
+
22
+ * **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))
23
+ * **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))
24
+ * **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))
25
+ * **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))
26
+ * **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))
27
+ * **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))
28
+ * **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))
29
+ * **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))
30
+ * **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))
31
+ * **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))
32
+ * **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))
33
+ * **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))
34
+ * **brepjs-cad:** load .ts parts via native type-stripping ([#1207](https://github.com/andymai/brepjs/issues/1207)) ([198078b](https://github.com/andymai/brepjs/commit/198078becf570614c0cbf61537714fc94c2de43a))
35
+ * **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))
36
+ * **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))
37
+ * **brepjs-cad:** run CLI via bin symlink + quality pass ([#1206](https://github.com/andymai/brepjs/issues/1206)) ([ac5b1fe](https://github.com/andymai/brepjs/commit/ac5b1feee3c5b424c37716ca06c397f7898838f1))
38
+ * **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))
39
+
40
+ ## [0.30.0](https://github.com/andymai/brepjs/compare/brepjs-cad-v0.29.0...brepjs-cad-v0.30.0) (2026-06-22)
41
+
42
+
43
+ ### Features
44
+
45
+ * **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))
46
+ * **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))
47
+ * **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))
48
+ * **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))
49
+ * **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))
50
+ * **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))
51
+ * **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))
52
+ * **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))
53
+
54
+
55
+ ### Bug Fixes
56
+
57
+ * **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))
58
+ * **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))
59
+ * **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))
60
+ * **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))
61
+ * **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))
62
+ * **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))
63
+ * **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))
64
+ * **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))
65
+ * **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))
66
+ * **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))
67
+ * **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))
68
+ * **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))
69
+ * **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))
70
+ * **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))
71
+ * **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))
72
+
3
73
  ## [0.29.0](https://github.com/andymai/brepjs/compare/brepjs-cad-v0.28.0...brepjs-cad-v0.29.0) (2026-06-20)
4
74
 
5
75
 
@@ -1,5 +1,5 @@
1
1
  Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
2
- const require_diff = require("./diff-kQJXMzLR.cjs");
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-DcKUadBa.js";
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-kQJXMzLR.cjs");
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, { check: Boolean(opts.check) });
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, { step: true });
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-DcKUadBa.js";
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, { check: Boolean(opts.check) });
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, { step: true });
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 {