brepjs-verify 0.4.0 → 0.8.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,76 @@
1
1
  # Changelog
2
2
 
3
+ ## [0.8.0](https://github.com/andymai/brepjs/compare/brepjs-verify-v0.7.1...brepjs-verify-v0.8.0) (2026-06-13)
4
+
5
+
6
+ ### Features
7
+
8
+ * **brepjs-verify:** auto-open browser on --serve + document the MCP server ([#1308](https://github.com/andymai/brepjs/issues/1308)) ([de4272c](https://github.com/andymai/brepjs/commit/de4272c9196f70baec4ff762a33636fccef4c012))
9
+ * **brepjs-verify:** burn bbox dimensions into agent snapshots ([#1280](https://github.com/andymai/brepjs/issues/1280)) ([25b6b8d](https://github.com/andymai/brepjs/commit/25b6b8df68909a41eaef5307519a29a8a05ccc00))
10
+ * **verify:** add center of mass to the verify report ([#1288](https://github.com/andymai/brepjs/issues/1288)) ([5738600](https://github.com/andymai/brepjs/commit/5738600e31f56c00d928d62e32ab9d5e8220b377))
11
+ * **verify:** add export_part MCP tool and sandbox export ([#1316](https://github.com/andymai/brepjs/issues/1316)) ([8a52f90](https://github.com/andymai/brepjs/commit/8a52f90c5b3d76c8bf6ef50a36c6977833af59bb))
12
+ * **verify:** add JSONL run-record provenance for sandbox runs ([#1309](https://github.com/andymai/brepjs/issues/1309)) ([6bda9b6](https://github.com/andymai/brepjs/commit/6bda9b630b8e4edf6eb860002c58b3e36cf1bfd4))
13
+ * **verify:** add manifold flag to the topology channel ([#1291](https://github.com/andymai/brepjs/issues/1291)) ([5ea5bb4](https://github.com/andymai/brepjs/commit/5ea5bb4db5bc8539f2773752e1c42770c91b5e0d))
14
+ * **verify:** add MCP server with run_program tool (stdio) ([#1300](https://github.com/andymai/brepjs/issues/1300)) ([e3c2c9e](https://github.com/andymai/brepjs/commit/e3c2c9e678dd719608cea8b6ee38101de9775e5d))
15
+ * **verify:** add topology counts to the verify report ([#1285](https://github.com/andymai/brepjs/issues/1285)) ([17a0eed](https://github.com/andymai/brepjs/commit/17a0eede727ac29007591cb0249274a35896facb))
16
+ * **verify:** sandbox executor — run agent code in an isolated child process ([#1295](https://github.com/andymai/brepjs/issues/1295)) ([8b72aa2](https://github.com/andymai/brepjs/commit/8b72aa2e272a58aa6d3886b8304eeaefb1a09b2e))
17
+ * **verify:** validate each body of multi-solid assemblies ([#1293](https://github.com/andymai/brepjs/issues/1293)) ([deb682f](https://github.com/andymai/brepjs/commit/deb682f1104179f261f232be7d94ceb154985328))
18
+ * **viewer:** click-to-inspect face picking in verify --serve ([#1278](https://github.com/andymai/brepjs/issues/1278)) ([735dc04](https://github.com/andymai/brepjs/commit/735dc0401143ff47046a79e6fb7bac53cf00a91e))
19
+ * **viewer:** measurements info panel in verify --serve ([#1277](https://github.com/andymai/brepjs/issues/1277)) ([c1ccf1d](https://github.com/andymai/brepjs/commit/c1ccf1d7c50ab43dc0444468f92fcf9365fda9da))
20
+ * **viewer:** orthographic/perspective projection toggle ([#1281](https://github.com/andymai/brepjs/issues/1281)) ([96673e4](https://github.com/andymai/brepjs/commit/96673e45e1ee316f9d26e52c995b4daba691e8b0))
21
+ * **viewer:** section/clipping plane in verify --serve ([#1279](https://github.com/andymai/brepjs/issues/1279)) ([cc0d00b](https://github.com/andymai/brepjs/commit/cc0d00b7a6296cc698fce1ae42b7503e6d47c032))
22
+ * **viewer:** shared ViewerControls toolbar; interactive verify --serve ([#1275](https://github.com/andymai/brepjs/issues/1275)) ([139ae15](https://github.com/andymai/brepjs/commit/139ae15a29d8a7ad5e520ba21d6dc9788242c089))
23
+
24
+ ## [0.7.1](https://github.com/andymai/brepjs/compare/brepjs-verify-v0.7.0...brepjs-verify-v0.7.1) (2026-06-13)
25
+
26
+
27
+ ### Bug Fixes
28
+
29
+ * **brepjs-verify:** repair preview viewer + GLB Y-up/materials fidelity ([#1271](https://github.com/andymai/brepjs/issues/1271)) ([2823d21](https://github.com/andymai/brepjs/commit/2823d212e2fc5f79e785911ec2b9f3320bdfdbbf))
30
+
31
+ ## [0.7.0](https://github.com/andymai/brepjs/compare/brepjs-verify-v0.6.0...brepjs-verify-v0.7.0) (2026-06-10)
32
+
33
+
34
+ ### Features
35
+
36
+ * **brepjs-verify:** eval-driven skill, hint, and reference improvements ([#1219](https://github.com/andymai/brepjs/issues/1219)) ([1a9b80f](https://github.com/andymai/brepjs/commit/1a9b80f3d3dbb44d7a8ae2f601ff305b70534efe))
37
+ * **brepjs-verify:** live text-to-cad eval flywheel ([#1215](https://github.com/andymai/brepjs/issues/1215)) ([4e81fc4](https://github.com/andymai/brepjs/commit/4e81fc4053491ce3e08182d57d76bd649252ea3c))
38
+ * **brepjs-verify:** standalone bundled CLI + rename from brepjs-cad ([#1211](https://github.com/andymai/brepjs/issues/1211)) ([05b3799](https://github.com/andymai/brepjs/commit/05b3799a0e9ee4968d4cac92f3a2ea236e39cd35))
39
+
40
+
41
+ ### Bug Fixes
42
+
43
+ * **brepjs-verify:** correct fillet/chamfer arg order in no-edges hints ([#1218](https://github.com/andymai/brepjs/issues/1218)) ([835f13a](https://github.com/andymai/brepjs/commit/835f13ac966b4264ba56a5cfc371bbbbbd1a0f01))
44
+ * **brepjs-verify:** point skills entry at ./skill directory, not SKILL.md ([#1270](https://github.com/andymai/brepjs/issues/1270)) ([9413a57](https://github.com/andymai/brepjs/commit/9413a57d8c2cac943371e75bcbaf11b3fdd9a657))
45
+
46
+ ## [0.6.0](https://github.com/andymai/brepjs/compare/brepjs-verify-v0.5.1...brepjs-verify-v0.6.0) (2026-06-09)
47
+
48
+
49
+ ### Features
50
+
51
+ * **brepjs-verify:** eval-driven skill, hint, and reference improvements ([#1219](https://github.com/andymai/brepjs/issues/1219)) ([1a9b80f](https://github.com/andymai/brepjs/commit/1a9b80f3d3dbb44d7a8ae2f601ff305b70534efe))
52
+ * **brepjs-verify:** live text-to-cad eval flywheel ([#1215](https://github.com/andymai/brepjs/issues/1215)) ([4e81fc4](https://github.com/andymai/brepjs/commit/4e81fc4053491ce3e08182d57d76bd649252ea3c))
53
+ * **brepjs-verify:** standalone bundled CLI + rename from brepjs-cad ([#1211](https://github.com/andymai/brepjs/issues/1211)) ([05b3799](https://github.com/andymai/brepjs/commit/05b3799a0e9ee4968d4cac92f3a2ea236e39cd35))
54
+
55
+
56
+ ### Bug Fixes
57
+
58
+ * **brepjs-verify:** correct fillet/chamfer arg order in no-edges hints ([#1218](https://github.com/andymai/brepjs/issues/1218)) ([835f13a](https://github.com/andymai/brepjs/commit/835f13ac966b4264ba56a5cfc371bbbbbd1a0f01))
59
+
60
+ ## [0.5.0](https://github.com/andymai/brepjs/compare/brepjs-verify-v0.4.1...brepjs-verify-v0.5.0) (2026-06-08)
61
+
62
+
63
+ ### Features
64
+
65
+ * **brepjs-verify:** eval-driven skill, hint, and reference improvements ([#1219](https://github.com/andymai/brepjs/issues/1219)) ([1a9b80f](https://github.com/andymai/brepjs/commit/1a9b80f3d3dbb44d7a8ae2f601ff305b70534efe))
66
+ * **brepjs-verify:** live text-to-cad eval flywheel ([#1215](https://github.com/andymai/brepjs/issues/1215)) ([4e81fc4](https://github.com/andymai/brepjs/commit/4e81fc4053491ce3e08182d57d76bd649252ea3c))
67
+ * **brepjs-verify:** standalone bundled CLI + rename from brepjs-cad ([#1211](https://github.com/andymai/brepjs/issues/1211)) ([05b3799](https://github.com/andymai/brepjs/commit/05b3799a0e9ee4968d4cac92f3a2ea236e39cd35))
68
+
69
+
70
+ ### Bug Fixes
71
+
72
+ * **brepjs-verify:** correct fillet/chamfer arg order in no-edges hints ([#1218](https://github.com/andymai/brepjs/issues/1218)) ([835f13a](https://github.com/andymai/brepjs/commit/835f13ac966b4264ba56a5cfc371bbbbbd1a0f01))
73
+
3
74
  ## [0.4.0](https://github.com/andymai/brepjs/compare/brepjs-verify-v0.3.0...brepjs-verify-v0.4.0) (2026-06-04)
4
75
 
5
76
 
package/README.md CHANGED
@@ -45,28 +45,58 @@ export default () => box(40, 20, 10, { centered: true });
45
45
  ```
46
46
  npx -y brepjs-verify part.brep.ts --step part.step --json report.json # primary STEP + deterministic report
47
47
  npx -y brepjs-verify part.brep.ts --snapshot shots/ # iso/front/top/right PNGs
48
- npx -y brepjs-verify part.brep.ts --serve # clickable preview link (renders the real STEP)
48
+ npx -y brepjs-verify part.brep.ts --serve # preview server + opens the viewer in your browser
49
+ npx -y brepjs-verify part.brep.ts --serve --no-open # preview server; just print the URL (no browser)
49
50
  ```
50
51
 
51
- `--snapshot`/`--serve` use the bundled viewer (shipped under `viewer/dist`, including the OCCT WASM). The viewer is read-only display + screenshot in v1.
52
+ `--snapshot`/`--serve` use the bundled viewer (shipped under `viewer/dist`, including the OCCT WASM). The `--serve` link is interactive: a toolbar offers view presets + fit, solid/wireframe/x-ray modes, edge/grid toggles, a turntable, click-to-inspect face picking, a section/clipping plane, a measurements panel, and an in-browser PNG screenshot. `--snapshot` loads the same page with `ui=0` to suppress the toolbar, and burns the bounding-box size into each PNG (`dims=1`) so the agent can read scale from the image.
53
+
54
+ `--serve` prints the viewer URL and, in an interactive terminal, opens it in your default browser. Auto-open is skipped when it would be unwanted — when the server is reused (a tab is already open), under CI, when output is piped (non-TTY, e.g. agent runs), or on Linux with no display server. Pass `--no-open` to always suppress it.
52
55
 
53
56
  ## CLI reference
54
57
 
55
58
  The `brepjs-verify` bin is a multi-command CLI. `verify` is the default command, so `brepjs-verify part.brep.ts` runs it directly.
56
59
 
57
- | Command | What it does |
58
- | ------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
59
- | `brepjs-verify verify <file>` | Default command. Loads the part, runs deterministic checks, prints the JSON report. Flags: `--json <out>`, `--step <out>`, `--glb <out>`, `--snapshot <dir>`, `--serve`. Exits non-zero when the report is not `ok` (unless `--serve`). |
60
- | `brepjs-verify init <name>` | Scaffolds a parameterized `<name>.brep.ts` + `tsconfig.json` + `README.md` into `./<name>` (or `--out <dir>`). Never overwrites existing files. |
61
- | `brepjs-verify watch <file>` | Re-verifies on every save until Ctrl-C (debounced; watches the parent dir to survive editor rename-on-save). |
62
- | `brepjs-verify export <file>` | Batch artifacts behind a validity gate: `--step`, `--glb`, `--stl`, or `--all`; `--out <dir>` (default `.`). Exits non-zero on failure. |
63
- | `brepjs-verify measure <a> [b]` | Measurements for one part; with a second module, the distance between the two parts. |
64
- | `brepjs-verify diff <a> <b>` | Compares the measurements of a baseline and a comparison module. |
65
- | `brepjs-verify snapshot` | Multi-view PNG capture — surfaced via `verify --snapshot <dir>`. Requires the optional `puppeteer`/Chrome dependency; degrades with a clear message when absent. |
66
- | `brepjs-verify serve` | Preview server with a `?dir=&file=` deep link — surfaced via `verify --serve`. |
60
+ | Command | What it does |
61
+ | ------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
62
+ | `brepjs-verify verify <file>` | Default command. Loads the part, runs deterministic checks, prints the JSON report. Flags: `--json <out>`, `--step <out>`, `--glb <out>`, `--snapshot <dir>`, `--serve`, `--no-open` (with `--serve`, don't auto-open the browser). Exits non-zero when the report is not `ok` (unless `--serve`). |
63
+ | `brepjs-verify init <name>` | Scaffolds a parameterized `<name>.brep.ts` + `tsconfig.json` + `README.md` into `./<name>` (or `--out <dir>`). Never overwrites existing files. |
64
+ | `brepjs-verify watch <file>` | Re-verifies on every save until Ctrl-C (debounced; watches the parent dir to survive editor rename-on-save). |
65
+ | `brepjs-verify export <file>` | Batch artifacts behind a validity gate: `--step`, `--glb`, `--stl`, or `--all`; `--out <dir>` (default `.`). Exits non-zero on failure. |
66
+ | `brepjs-verify measure <a> [b]` | Measurements for one part; with a second module, the distance between the two parts. |
67
+ | `brepjs-verify diff <a> <b>` | Compares the measurements of a baseline and a comparison module. |
68
+ | `brepjs-verify snapshot` | Multi-view PNG capture — surfaced via `verify --snapshot <dir>`. Requires the optional `puppeteer`/Chrome dependency; degrades with a clear message when absent. |
69
+ | `brepjs-verify serve` | Preview server with a `?dir=&file=` deep link — surfaced via `verify --serve`. Auto-opens the browser in an interactive terminal (suppressed under CI / non-TTY / no display, or with `--no-open`). |
67
70
 
68
71
  Every command writes a single machine-readable JSON document to stdout; diagnostics (paths, kernel chatter, watch notices) go to stderr.
69
72
 
73
+ ## MCP server
74
+
75
+ `brepjs-verify-mcp` is a stdio [MCP](https://modelcontextprotocol.io) server that exposes the verify substrate to MCP-capable agents (Claude Code, Claude Desktop, any MCP client). It currently provides one tool:
76
+
77
+ | Tool | Input | Returns |
78
+ | ------------- | -------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
79
+ | `run_program` | `{ code: string, timeoutMs?: number }` | Executes the brepjs `.brep.ts` source in an isolated, timeout/OOM-bounded sandbox and returns the verification report (validity, measurements, topology) as JSON. `isError` is set when the part fails checks, times out, or crashes. |
80
+
81
+ This is the closed _build → verify_ loop as a single call: the agent sends part source, gets back the deterministic report. The program runs in a separate process with a wall-clock timeout and a memory cap, so a runaway part can't hang the agent.
82
+
83
+ ### Connect (local build)
84
+
85
+ Build the package, then register the server by absolute path. Run both commands from the package root (`packages/brepjs-verify`), where `dist/` is emitted — `$(pwd)` is resolved by your shell at that location:
86
+
87
+ ```bash
88
+ npm run build # emits dist/mcp/server.js
89
+ claude mcp add brepjs-verify -- node "$(pwd)/dist/mcp/server.js"
90
+ ```
91
+
92
+ Once the package is published to npm, the same server is available without a local build:
93
+
94
+ ```bash
95
+ claude mcp add brepjs-verify -- npx -y --package brepjs-verify brepjs-verify-mcp
96
+ ```
97
+
98
+ The server runs locally as a child process of your agent (stdio) — geometry never leaves your machine.
99
+
70
100
  ## Examples gallery
71
101
 
72
102
  Few-shot examples live under `skill/examples/<name>.brep.ts`, each with a `<name>.expected.json` baseline. Grouped by category:
@@ -1,5 +1,5 @@
1
1
  Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
2
- const require_diff = require("./diff-BNmCp_8I.cjs");
2
+ const require_diff = require("./diff-DDEu6YJM.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-D5U3Ie2F.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-BCECMYSQ.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,11 +1,12 @@
1
1
  #!/usr/bin/env node
2
2
  Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
3
- const require_diff = require("../diff-BNmCp_8I.cjs");
3
+ const require_diff = require("../diff-DDEu6YJM.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");
7
7
  let commander = require("commander");
8
8
  let node_os = require("node:os");
9
+ let node_child_process = require("node:child_process");
9
10
  //#region src/cli/scaffold.ts
10
11
  function partTemplate(name) {
11
12
  return `import { box, cut, unwrap } from 'brepjs';
@@ -177,6 +178,43 @@ async function exportPart(modulePath, formats, outDir) {
177
178
  };
178
179
  }
179
180
  //#endregion
181
+ //#region src/cli/openBrowser.ts
182
+ /**
183
+ * Whether `--serve` should auto-open the browser for the current environment.
184
+ *
185
+ * Opens only for an interactive session — suppressed under CI, when stderr is
186
+ * not a TTY (agent/piped runs), or on Linux without a display server — so
187
+ * automation never spawns a browser unexpectedly. An explicit `--no-open` is a
188
+ * separate, always-on override handled by the caller.
189
+ */
190
+ function shouldAutoOpen({ env = process.env, platform = process.platform, isTTY = Boolean(process.stderr.isTTY) } = {}) {
191
+ if (env["CI"]) return false;
192
+ if (!isTTY) return false;
193
+ if (platform === "linux" && !env["DISPLAY"] && !env["WAYLAND_DISPLAY"]) return false;
194
+ return true;
195
+ }
196
+ /** The platform-specific command that opens `url` in the default browser. */
197
+ function browserCommand(url, platform) {
198
+ if (platform === "darwin") return ["open", [url]];
199
+ if (platform === "win32") return ["rundll32", ["url.dll,FileProtocolHandler", url]];
200
+ return ["xdg-open", [url]];
201
+ }
202
+ /**
203
+ * Best-effort open of `url` in the default browser. Never throws and never
204
+ * blocks — a missing opener just leaves the printed URL as the fallback.
205
+ */
206
+ function openBrowser(url, platform = process.platform) {
207
+ try {
208
+ const [cmd, args] = browserCommand(url, platform);
209
+ const child = (0, node_child_process.spawn)(cmd, args, {
210
+ stdio: "ignore",
211
+ detached: true
212
+ });
213
+ child.on("error", () => {});
214
+ child.unref();
215
+ } catch {}
216
+ }
217
+ //#endregion
180
218
  //#region src/cli/main.ts
181
219
  console.log = (...args) => {
182
220
  process.stderr.write(args.map(String).join(" ") + "\n");
@@ -192,7 +230,7 @@ async function loadSnapshotShoot() {
192
230
  }
193
231
  var program = new commander.Command();
194
232
  program.name("brepjs-verify");
195
- program.command("verify", { isDefault: true }).argument("<file>", "path to a .brep.ts module with a default-exported part function").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)").action(async (file, opts) => {
233
+ program.command("verify", { isDefault: true }).argument("<file>", "path to a .brep.ts module with a default-exported part function").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)").action(async (file, opts) => {
196
234
  const wantStep = Boolean(opts.step) || Boolean(opts.snapshot) || Boolean(opts.serve);
197
235
  const { report, step, glb, shape } = await require_diff.runPart((0, node_path.resolve)(file), {
198
236
  step: wantStep,
@@ -227,8 +265,9 @@ program.command("verify", { isDefault: true }).argument("<file>", "path to a .br
227
265
  if (!require_diff.reportOk(report)) process.exitCode = 1;
228
266
  if (Boolean(opts.serve) && stepPath !== void 0 && require_diff.reportOk(report) && stepPath) {
229
267
  const { serve } = await Promise.resolve().then(() => require("../snapshot/serve.cjs"));
230
- const { url } = await serve({ file: stepPath });
268
+ const { url, reused } = await serve({ file: stepPath });
231
269
  process.stderr.write(`viewer: ${url}\n`);
270
+ if (!reused && opts.open && shouldAutoOpen()) openBrowser(url);
232
271
  }
233
272
  });
234
273
  program.command("measure").argument("<a>", "path to a .brep.ts module").argument("[b]", "optional second module; if given, measures distance between the two parts").action(async (a, b) => {
package/dist/cli/main.js CHANGED
@@ -1,10 +1,11 @@
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-D5U3Ie2F.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-BCECMYSQ.js";
3
3
  import { fileURLToPath } from "node:url";
4
4
  import { existsSync, mkdirSync, realpathSync, watch, writeFileSync } from "node:fs";
5
5
  import { basename, dirname, join, resolve } from "node:path";
6
6
  import { Command } from "commander";
7
7
  import { tmpdir } from "node:os";
8
+ import { spawn } from "node:child_process";
8
9
  //#region src/cli/scaffold.ts
9
10
  function partTemplate(name) {
10
11
  return `import { box, cut, unwrap } from 'brepjs';
@@ -176,6 +177,43 @@ async function exportPart(modulePath, formats, outDir) {
176
177
  };
177
178
  }
178
179
  //#endregion
180
+ //#region src/cli/openBrowser.ts
181
+ /**
182
+ * Whether `--serve` should auto-open the browser for the current environment.
183
+ *
184
+ * Opens only for an interactive session — suppressed under CI, when stderr is
185
+ * not a TTY (agent/piped runs), or on Linux without a display server — so
186
+ * automation never spawns a browser unexpectedly. An explicit `--no-open` is a
187
+ * separate, always-on override handled by the caller.
188
+ */
189
+ function shouldAutoOpen({ env = process.env, platform = process.platform, isTTY = Boolean(process.stderr.isTTY) } = {}) {
190
+ if (env["CI"]) return false;
191
+ if (!isTTY) return false;
192
+ if (platform === "linux" && !env["DISPLAY"] && !env["WAYLAND_DISPLAY"]) return false;
193
+ return true;
194
+ }
195
+ /** The platform-specific command that opens `url` in the default browser. */
196
+ function browserCommand(url, platform) {
197
+ if (platform === "darwin") return ["open", [url]];
198
+ if (platform === "win32") return ["rundll32", ["url.dll,FileProtocolHandler", url]];
199
+ return ["xdg-open", [url]];
200
+ }
201
+ /**
202
+ * Best-effort open of `url` in the default browser. Never throws and never
203
+ * blocks — a missing opener just leaves the printed URL as the fallback.
204
+ */
205
+ function openBrowser(url, platform = process.platform) {
206
+ try {
207
+ const [cmd, args] = browserCommand(url, platform);
208
+ const child = spawn(cmd, args, {
209
+ stdio: "ignore",
210
+ detached: true
211
+ });
212
+ child.on("error", () => {});
213
+ child.unref();
214
+ } catch {}
215
+ }
216
+ //#endregion
179
217
  //#region src/cli/main.ts
180
218
  console.log = (...args) => {
181
219
  process.stderr.write(args.map(String).join(" ") + "\n");
@@ -191,7 +229,7 @@ async function loadSnapshotShoot() {
191
229
  }
192
230
  var program = new Command();
193
231
  program.name("brepjs-verify");
194
- program.command("verify", { isDefault: true }).argument("<file>", "path to a .brep.ts module with a default-exported part function").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)").action(async (file, opts) => {
232
+ program.command("verify", { isDefault: true }).argument("<file>", "path to a .brep.ts module with a default-exported part function").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)").action(async (file, opts) => {
195
233
  const wantStep = Boolean(opts.step) || Boolean(opts.snapshot) || Boolean(opts.serve);
196
234
  const { report, step, glb, shape } = await runPart(resolve(file), {
197
235
  step: wantStep,
@@ -226,8 +264,9 @@ program.command("verify", { isDefault: true }).argument("<file>", "path to a .br
226
264
  if (!reportOk(report)) process.exitCode = 1;
227
265
  if (Boolean(opts.serve) && stepPath !== void 0 && reportOk(report) && stepPath) {
228
266
  const { serve } = await import("../snapshot/serve.js");
229
- const { url } = await serve({ file: stepPath });
267
+ const { url, reused } = await serve({ file: stepPath });
230
268
  process.stderr.write(`viewer: ${url}\n`);
269
+ if (!reused && opts.open && shouldAutoOpen()) openBrowser(url);
231
270
  }
232
271
  });
233
272
  program.command("measure").argument("<a>", "path to a .brep.ts module").argument("[b]", "optional second module; if given, measures distance between the two parts").action(async (a, b) => {
@@ -0,0 +1,23 @@
1
+ /** Inputs that decide whether auto-opening a browser is appropriate. */
2
+ export interface AutoOpenEnv {
3
+ env?: NodeJS.ProcessEnv;
4
+ platform?: NodeJS.Platform;
5
+ /** Whether stderr is an interactive terminal. */
6
+ isTTY?: boolean;
7
+ }
8
+ /**
9
+ * Whether `--serve` should auto-open the browser for the current environment.
10
+ *
11
+ * Opens only for an interactive session — suppressed under CI, when stderr is
12
+ * not a TTY (agent/piped runs), or on Linux without a display server — so
13
+ * automation never spawns a browser unexpectedly. An explicit `--no-open` is a
14
+ * separate, always-on override handled by the caller.
15
+ */
16
+ export declare function shouldAutoOpen({ env, platform, isTTY, }?: AutoOpenEnv): boolean;
17
+ /** The platform-specific command that opens `url` in the default browser. */
18
+ export declare function browserCommand(url: string, platform: NodeJS.Platform): [string, string[]];
19
+ /**
20
+ * Best-effort open of `url` in the default browser. Never throws and never
21
+ * blocks — a missing opener just leaves the printed URL as the fallback.
22
+ */
23
+ export declare function openBrowser(url: string, platform?: NodeJS.Platform): void;
@@ -259,7 +259,7 @@ function shapeTypeOf(brep, s) {
259
259
  return "Unknown";
260
260
  }
261
261
  function runChecks(brep, shape) {
262
- const { isSolid, isShape3D, isFace, measureVolume, measureArea, getBounds, validSolid, isOk } = brep;
262
+ const { isSolid, isShape3D, isFace, measureVolumeProps, measureArea, getBounds, getFaces, getEdges, getWires, getVertices, getSolids, getShells, isManifoldShell, validSolid, isOk } = brep;
263
263
  const r = emptyReport();
264
264
  r.shapeType = shapeTypeOf(brep, shape);
265
265
  if (isSolid(shape)) {
@@ -276,19 +276,41 @@ function runChecks(brep, shape) {
276
276
  });
277
277
  }
278
278
  r.checks.push(validCheck);
279
+ } else {
280
+ const solids = getSolids(shape);
281
+ if (solids.length > 0) {
282
+ const failures = [];
283
+ solids.forEach((s, i) => {
284
+ const v = validSolid(s);
285
+ if (!isOk(v)) failures.push(`body ${i}: ${v.error}`);
286
+ });
287
+ const bodiesCheck = {
288
+ name: "allBodiesValid",
289
+ passed: failures.length === 0
290
+ };
291
+ if (failures.length > 0) {
292
+ bodiesCheck.detail = `${failures.length}/${solids.length} bodies invalid — ${failures.join("; ")}`;
293
+ r.errorInfos.push({
294
+ message: `allBodiesValid: ${bodiesCheck.detail}`,
295
+ code: VALIDITY_FAILURE_CODE
296
+ });
297
+ }
298
+ r.checks.push(bodiesCheck);
299
+ }
279
300
  }
280
301
  if (isShape3D(shape)) {
281
- const vol = measureVolume(shape);
282
- if (isOk(vol)) {
283
- r.measurements.volume = vol.value;
302
+ const volProps = measureVolumeProps(shape);
303
+ if (isOk(volProps)) {
304
+ r.measurements.volume = volProps.value.volume;
305
+ r.measurements.centerOfMass = volProps.value.centerOfMass;
284
306
  r.checks.push({
285
307
  name: "positiveVolume",
286
- passed: vol.value > 0
308
+ passed: volProps.value.volume > 0
287
309
  });
288
310
  } else pushError(r, {
289
- message: `measureVolume: ${vol.error.message}`,
290
- code: vol.error.code,
291
- suggestion: vol.error.suggestion
311
+ message: `measureVolume: ${volProps.error.message}`,
312
+ code: volProps.error.code,
313
+ suggestion: volProps.error.suggestion
292
314
  });
293
315
  }
294
316
  if (isFace(shape) || isShape3D(shape)) {
@@ -300,6 +322,18 @@ function runChecks(brep, shape) {
300
322
  } catch (e) {
301
323
  pushError(r, { message: `getBounds: ${e.message}` });
302
324
  }
325
+ try {
326
+ r.topology = {
327
+ faceCount: getFaces(shape).length,
328
+ edgeCount: getEdges(shape).length,
329
+ wireCount: getWires(shape).length,
330
+ vertexCount: getVertices(shape).length
331
+ };
332
+ } catch {}
333
+ if (r.topology) try {
334
+ const shells = getShells(shape);
335
+ if (shells.length > 0) r.topology.manifold = shells.every((s) => isManifoldShell(s));
336
+ } catch {}
303
337
  r.hints = buildHints(r);
304
338
  return r;
305
339
  }
@@ -528,6 +562,39 @@ function typecheckPart(partPath, toolDir) {
528
562
  }
529
563
  //#endregion
530
564
  //#region src/verify/runPart.ts
565
+ /** Centroid of a face group's vertices, in part (Z-up, mm) coordinates. */
566
+ function faceCentroid(m, start, count) {
567
+ let x = 0;
568
+ let y = 0;
569
+ let z = 0;
570
+ for (let i = start; i < start + count; i++) {
571
+ const vi = (m.triangles[i] ?? 0) * 3;
572
+ x += m.vertices[vi] ?? 0;
573
+ y += m.vertices[vi + 1] ?? 0;
574
+ z += m.vertices[vi + 2] ?? 0;
575
+ }
576
+ const n = count || 1;
577
+ return [
578
+ x / n,
579
+ y / n,
580
+ z / n
581
+ ];
582
+ }
583
+ function buildMaterialMap(m, spec) {
584
+ if (spec === void 0 || spec === null) return {};
585
+ if (typeof spec !== "function" && (typeof spec !== "object" || Array.isArray(spec))) return { warning: "export const materials must be a function or a material object — ignored" };
586
+ const sel = spec;
587
+ const select = typeof sel === "function" ? sel : () => sel;
588
+ const map = /* @__PURE__ */ new Map();
589
+ for (const fg of m.faceGroups) {
590
+ const mat = select({
591
+ faceId: fg.faceId,
592
+ center: faceCentroid(m, fg.start, fg.count)
593
+ });
594
+ if (mat) map.set(fg.faceId, mat);
595
+ }
596
+ return map.size > 0 ? { map } : {};
597
+ }
531
598
  async function loadPart(modulePath) {
532
599
  try {
533
600
  return await import(pathToFileURL(modulePath).href);
@@ -646,7 +713,13 @@ async function runPart(modulePath, opts = {}) {
646
713
  let glb;
647
714
  let step;
648
715
  if (opts.glb) try {
649
- glb = exportGlb(mesh(shape));
716
+ const shapeMesh = mesh(shape);
717
+ const { map, warning } = buildMaterialMap(shapeMesh, mod.materials);
718
+ if (warning) pushError(result, {
719
+ message: `materials: ${warning}`,
720
+ code: "MATERIALS_IGNORED"
721
+ });
722
+ glb = map ? exportGlb(shapeMesh, { materials: map }) : exportGlb(shapeMesh);
650
723
  } catch (e) {
651
724
  pushError(result, toErrorInfo("exportGlb", e));
652
725
  }
@@ -261,7 +261,7 @@ function shapeTypeOf(brep, s) {
261
261
  return "Unknown";
262
262
  }
263
263
  function runChecks(brep, shape) {
264
- const { isSolid, isShape3D, isFace, measureVolume, measureArea, getBounds, validSolid, isOk } = brep;
264
+ const { isSolid, isShape3D, isFace, measureVolumeProps, measureArea, getBounds, getFaces, getEdges, getWires, getVertices, getSolids, getShells, isManifoldShell, validSolid, isOk } = brep;
265
265
  const r = emptyReport();
266
266
  r.shapeType = shapeTypeOf(brep, shape);
267
267
  if (isSolid(shape)) {
@@ -278,19 +278,41 @@ function runChecks(brep, shape) {
278
278
  });
279
279
  }
280
280
  r.checks.push(validCheck);
281
+ } else {
282
+ const solids = getSolids(shape);
283
+ if (solids.length > 0) {
284
+ const failures = [];
285
+ solids.forEach((s, i) => {
286
+ const v = validSolid(s);
287
+ if (!isOk(v)) failures.push(`body ${i}: ${v.error}`);
288
+ });
289
+ const bodiesCheck = {
290
+ name: "allBodiesValid",
291
+ passed: failures.length === 0
292
+ };
293
+ if (failures.length > 0) {
294
+ bodiesCheck.detail = `${failures.length}/${solids.length} bodies invalid — ${failures.join("; ")}`;
295
+ r.errorInfos.push({
296
+ message: `allBodiesValid: ${bodiesCheck.detail}`,
297
+ code: VALIDITY_FAILURE_CODE
298
+ });
299
+ }
300
+ r.checks.push(bodiesCheck);
301
+ }
281
302
  }
282
303
  if (isShape3D(shape)) {
283
- const vol = measureVolume(shape);
284
- if (isOk(vol)) {
285
- r.measurements.volume = vol.value;
304
+ const volProps = measureVolumeProps(shape);
305
+ if (isOk(volProps)) {
306
+ r.measurements.volume = volProps.value.volume;
307
+ r.measurements.centerOfMass = volProps.value.centerOfMass;
286
308
  r.checks.push({
287
309
  name: "positiveVolume",
288
- passed: vol.value > 0
310
+ passed: volProps.value.volume > 0
289
311
  });
290
312
  } else pushError(r, {
291
- message: `measureVolume: ${vol.error.message}`,
292
- code: vol.error.code,
293
- suggestion: vol.error.suggestion
313
+ message: `measureVolume: ${volProps.error.message}`,
314
+ code: volProps.error.code,
315
+ suggestion: volProps.error.suggestion
294
316
  });
295
317
  }
296
318
  if (isFace(shape) || isShape3D(shape)) {
@@ -302,6 +324,18 @@ function runChecks(brep, shape) {
302
324
  } catch (e) {
303
325
  pushError(r, { message: `getBounds: ${e.message}` });
304
326
  }
327
+ try {
328
+ r.topology = {
329
+ faceCount: getFaces(shape).length,
330
+ edgeCount: getEdges(shape).length,
331
+ wireCount: getWires(shape).length,
332
+ vertexCount: getVertices(shape).length
333
+ };
334
+ } catch {}
335
+ if (r.topology) try {
336
+ const shells = getShells(shape);
337
+ if (shells.length > 0) r.topology.manifold = shells.every((s) => isManifoldShell(s));
338
+ } catch {}
305
339
  r.hints = buildHints(r);
306
340
  return r;
307
341
  }
@@ -530,6 +564,39 @@ function typecheckPart(partPath, toolDir) {
530
564
  }
531
565
  //#endregion
532
566
  //#region src/verify/runPart.ts
567
+ /** Centroid of a face group's vertices, in part (Z-up, mm) coordinates. */
568
+ function faceCentroid(m, start, count) {
569
+ let x = 0;
570
+ let y = 0;
571
+ let z = 0;
572
+ for (let i = start; i < start + count; i++) {
573
+ const vi = (m.triangles[i] ?? 0) * 3;
574
+ x += m.vertices[vi] ?? 0;
575
+ y += m.vertices[vi + 1] ?? 0;
576
+ z += m.vertices[vi + 2] ?? 0;
577
+ }
578
+ const n = count || 1;
579
+ return [
580
+ x / n,
581
+ y / n,
582
+ z / n
583
+ ];
584
+ }
585
+ function buildMaterialMap(m, spec) {
586
+ if (spec === void 0 || spec === null) return {};
587
+ if (typeof spec !== "function" && (typeof spec !== "object" || Array.isArray(spec))) return { warning: "export const materials must be a function or a material object — ignored" };
588
+ const sel = spec;
589
+ const select = typeof sel === "function" ? sel : () => sel;
590
+ const map = /* @__PURE__ */ new Map();
591
+ for (const fg of m.faceGroups) {
592
+ const mat = select({
593
+ faceId: fg.faceId,
594
+ center: faceCentroid(m, fg.start, fg.count)
595
+ });
596
+ if (mat) map.set(fg.faceId, mat);
597
+ }
598
+ return map.size > 0 ? { map } : {};
599
+ }
533
600
  async function loadPart(modulePath) {
534
601
  try {
535
602
  return await import((0, node_url.pathToFileURL)(modulePath).href);
@@ -648,7 +715,13 @@ async function runPart(modulePath, opts = {}) {
648
715
  let glb;
649
716
  let step;
650
717
  if (opts.glb) try {
651
- glb = exportGlb(mesh(shape));
718
+ const shapeMesh = mesh(shape);
719
+ const { map, warning } = buildMaterialMap(shapeMesh, mod.materials);
720
+ if (warning) pushError(result, {
721
+ message: `materials: ${warning}`,
722
+ code: "MATERIALS_IGNORED"
723
+ });
724
+ glb = map ? exportGlb(shapeMesh, { materials: map }) : exportGlb(shapeMesh);
652
725
  } catch (e) {
653
726
  pushError(result, toErrorInfo("exportGlb", e));
654
727
  }
package/dist/index.d.ts CHANGED
@@ -2,6 +2,6 @@ export { runPart, type RunPartOptions, type RunPartResult } from './verify/runPa
2
2
  export { runChecks } from './verify/checks.js';
3
3
  export { runMeasure, type MeasureReport } from './verify/measure.js';
4
4
  export { runDiff } from './verify/diff.js';
5
- export { serializeReport, emptyReport, type VerifyReport, type VerifyCheck, type VerifyMeasurements, type VerifyAssertion, type DiffReport, type BoundsDelta, } from './verify/report.js';
5
+ export { serializeReport, emptyReport, type VerifyReport, type VerifyCheck, type VerifyMeasurements, type VerifyTopology, type VerifyAssertion, type DiffReport, type BoundsDelta, } from './verify/report.js';
6
6
  export { typecheckPart, TYPECHECK_CODE, type TypecheckResult } from './verify/typecheck.js';
7
7
  export { evaluateExpected, isExpectedDims, pctDelta, DEFAULT_TOLERANCE_PCT, type ExpectedDims, type ExpectedBounds, } from './verify/expected.js';