brepjs-cad 0.175.0 → 0.177.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.
Files changed (40) hide show
  1. package/CHANGELOG.md +19 -0
  2. package/README.md +2 -1
  3. package/dist/brepjs-cad.cjs +12 -11
  4. package/dist/brepjs-cad.js +2 -1
  5. package/dist/cli/exportPart.d.ts +1 -0
  6. package/dist/cli/main.cjs +77 -183
  7. package/dist/cli/main.js +54 -160
  8. package/dist/cli/watch.d.ts +10 -1
  9. package/dist/diff-CBp5KDNZ.cjs +154 -0
  10. package/dist/diff-DmFtXXiX.js +143 -0
  11. package/dist/disposeShape-B4lsczOO.js +7 -0
  12. package/dist/disposeShape-Bui9k40G.cjs +12 -0
  13. package/dist/evaluate-DDwQkzN-.cjs +11832 -0
  14. package/dist/evaluate-TZ0h4RP0.js +11816 -0
  15. package/dist/export3dm/export3dm.cjs +144 -0
  16. package/dist/export3dm/export3dm.d.ts +10 -0
  17. package/dist/export3dm/export3dm.js +141 -0
  18. package/dist/loader/brepjsResolve.mjs +10 -4
  19. package/dist/mcp/server.cjs +2 -2
  20. package/dist/mcp/server.js +2 -2
  21. package/dist/openBrowser-Bip99cQv.cjs +211 -0
  22. package/dist/openBrowser-Dg9oDOte.js +170 -0
  23. package/dist/preview/evaluate.d.ts +35 -0
  24. package/dist/preview/preview.cjs +170 -0
  25. package/dist/preview/preview.d.ts +9 -0
  26. package/dist/preview/preview.js +169 -0
  27. package/dist/preview/server.d.ts +17 -0
  28. package/dist/{rolldown-runtime-VH7oDXx4.cjs → rolldown-runtime-y5ARvAq5.cjs} +7 -0
  29. package/dist/snapshot/shoot.cjs +1 -1
  30. package/dist/snapshot/static.cjs +10 -1
  31. package/dist/snapshot/static.d.ts +4 -0
  32. package/dist/snapshot/static.js +8 -2
  33. package/dist/{diff-B1sSWjLa.js → usingCtx-BDYtpr4t.js} +21 -153
  34. package/dist/{diff-DX9zgrss.cjs → usingCtx-LY0tPCbQ.cjs} +51 -165
  35. package/dist/verify/runPart.d.ts +5 -0
  36. package/dist/verify/typecheck.d.ts +8 -0
  37. package/package.json +3 -2
  38. package/viewer/dist/assets/index-Cq6KRusM.js +4208 -0
  39. package/viewer/dist/index.html +1 -1
  40. package/viewer/dist/assets/index-CH09YZ3K.js +0 -4208
package/CHANGELOG.md CHANGED
@@ -1,5 +1,24 @@
1
1
  # Changelog
2
2
 
3
+ ## [0.177.0](https://github.com/andymai/brepjs/compare/brepjs-cad-v0.176.0...brepjs-cad-v0.177.0) (2026-08-26)
4
+
5
+
6
+ ### Features
7
+
8
+ * **brepjs-cad:** brep export --3dm — element-aware Rhino export ([#2236](https://github.com/andymai/brepjs/issues/2236)) ([91b0235](https://github.com/andymai/brepjs/commit/91b02355d9ba005f69dd4c72002a946600179398))
9
+
10
+
11
+ ### Bug Fixes
12
+
13
+ * **brepjs-cad:** quality pass over the preview/loader/3dm/scaffold delta ([#2243](https://github.com/andymai/brepjs/issues/2243)) ([b6ae66e](https://github.com/andymai/brepjs/commit/b6ae66e6a1c4175bfd13846b382972e1d7356adc))
14
+
15
+ ## [0.176.0](https://github.com/andymai/brepjs/compare/brepjs-cad-v0.175.0...brepjs-cad-v0.176.0) (2026-08-26)
16
+
17
+
18
+ ### Features
19
+
20
+ * **brepjs-cad:** brep preview — element-aware model viewer with live watch ([#2231](https://github.com/andymai/brepjs/issues/2231)) ([002a20f](https://github.com/andymai/brepjs/commit/002a20f9ad8d38cb812f00a875583bdb850a004d))
21
+
3
22
  ## [0.175.0](https://github.com/andymai/brepjs/compare/brepjs-cad-v0.174.0...brepjs-cad-v0.175.0) (2026-08-26)
4
23
 
5
24
 
package/README.md CHANGED
@@ -66,7 +66,8 @@ The `brep` bin is a multi-command CLI. `verify` is the default command, so `brep
66
66
  | `brep verify <files...>` | Default command. Loads the part, runs deterministic checks, prints the JSON report. Flags: `--check`, `--json <out\|->`, `--step <out>`, `--glb <out>`, `--snapshot <dir>`, `--serve`, `--no-open`, `--expect-code <CODE>` / `--expect-invalid` (assert a known-bad part fails the right way — for fixtures/eval). Multiple files or a quoted glob → a validity-only JSON array. Exits non-zero when not `ok` (unless `--serve`). |
67
67
  | `brep init <name>` | Scaffolds a parameterized `<name>.brep.ts` + `tsconfig.json` + `README.md` into `./<name>` (or `--out <dir>`). Never overwrites existing files. |
68
68
  | `brep watch <file>` | Re-verifies on every save until Ctrl-C (debounced; watches the parent dir to survive editor rename-on-save). |
69
- | `brep export <file>` | Batch artifacts behind a validity gate: `--step`, `--glb`, `--stl`, or `--all`; `--out <dir>` (default `.`). Exits non-zero on failure. |
69
+ | `brep preview <file>` | Element-aware live viewer for a model module (families element tree, resolved tree, or plain shape): key-path picking, failed elements listed, real edges. `--watch` re-evaluates on save and pushes to the open tab; `--write-only` (CI), `--glb <out>`, `--json <out>`, `--port <n>`, `--no-open`. |
70
+ | `brep export <file>` | Batch artifacts behind a validity gate: `--step`, `--glb`, `--stl`, or `--all`; `--3dm` writes an element-aware Rhino file (one named object per element, layered; needs the optional `rhino3dm` install; not combinable with the other formats); `--out <dir>` (default `.`). Exits non-zero on failure. |
70
71
  | `brep measure <a> [b]` | Measurements for one part; with a second module, the distance between the two parts. |
71
72
  | `brep diff <a> <b>` | Compares the measurements of a baseline and a comparison module. |
72
73
  | `brep snapshot <file>` | Standalone multi-view PNG capture (no report assertions): `--out <dir>`, `--label <tag>` (subfolder, for pre/post A/B pairing). Renders on a private ephemeral server so parallel snapshots don't contend. Requires the optional `puppeteer`/Chrome dependency; degrades with a clear message when absent. (Also available inline via `verify --snapshot <dir>`.) |
@@ -1,14 +1,15 @@
1
1
  Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
2
- const require_diff = require("./diff-DX9zgrss.cjs");
3
- exports.DEFAULT_TOLERANCE_PCT = require_diff.DEFAULT_TOLERANCE_PCT;
4
- exports.TYPECHECK_CODE = require_diff.TYPECHECK_CODE;
5
- exports.emptyReport = require_diff.emptyReport;
6
- exports.evaluateExpected = require_diff.evaluateExpected;
7
- exports.isExpectedDims = require_diff.isExpectedDims;
8
- exports.pctDelta = require_diff.pctDelta;
9
- exports.runChecks = require_diff.runChecks;
2
+ const require_usingCtx = require("./usingCtx-LY0tPCbQ.cjs");
3
+ const require_diff = require("./diff-CBp5KDNZ.cjs");
4
+ exports.DEFAULT_TOLERANCE_PCT = require_usingCtx.DEFAULT_TOLERANCE_PCT;
5
+ exports.TYPECHECK_CODE = require_usingCtx.TYPECHECK_CODE;
6
+ exports.emptyReport = require_usingCtx.emptyReport;
7
+ exports.evaluateExpected = require_usingCtx.evaluateExpected;
8
+ exports.isExpectedDims = require_usingCtx.isExpectedDims;
9
+ exports.pctDelta = require_usingCtx.pctDelta;
10
+ exports.runChecks = require_usingCtx.runChecks;
10
11
  exports.runDiff = require_diff.runDiff;
11
12
  exports.runMeasure = require_diff.runMeasure;
12
- exports.runPart = require_diff.runPart;
13
- exports.serializeReport = require_diff.serializeReport;
14
- exports.typecheckPart = require_diff.typecheckPart;
13
+ exports.runPart = require_usingCtx.runPart;
14
+ exports.serializeReport = require_usingCtx.serializeReport;
15
+ exports.typecheckPart = require_usingCtx.typecheckPart;
@@ -1,2 +1,3 @@
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-B1sSWjLa.js";
1
+ import { c as DEFAULT_TOLERANCE_PCT, d as pctDelta, f as runChecks, g as serializeReport, i as TYPECHECK_CODE, l as evaluateExpected, p as emptyReport, r as runPart, s as typecheckPart, u as isExpectedDims } from "./usingCtx-BDYtpr4t.js";
2
+ import { n as runMeasure, t as runDiff } from "./diff-DmFtXXiX.js";
2
3
  export { DEFAULT_TOLERANCE_PCT, TYPECHECK_CODE, emptyReport, evaluateExpected, isExpectedDims, pctDelta, runChecks, runDiff, runMeasure, runPart, serializeReport, typecheckPart };
@@ -10,4 +10,5 @@ export interface ExportResult {
10
10
  written: string[];
11
11
  errors: string[];
12
12
  }
13
+ export declare function stem(file: string): string;
13
14
  export declare function exportPart(modulePath: string, formats: ExportFormats, outDir: string): Promise<ExportResult>;
package/dist/cli/main.cjs CHANGED
@@ -1,12 +1,14 @@
1
1
  #!/usr/bin/env node
2
2
  Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
3
- const require_diff = require("../diff-DX9zgrss.cjs");
3
+ const require_usingCtx = require("../usingCtx-LY0tPCbQ.cjs");
4
+ const require_diff = require("../diff-CBp5KDNZ.cjs");
5
+ const require_openBrowser = require("../openBrowser-Bip99cQv.cjs");
6
+ const require_disposeShape = require("../disposeShape-Bui9k40G.cjs");
4
7
  let node_url = require("node:url");
5
8
  let node_fs = require("node:fs");
6
9
  let node_path = require("node:path");
7
10
  let commander = require("commander");
8
11
  let node_os = require("node:os");
9
- let node_child_process = require("node:child_process");
10
12
  //#region src/cli/scaffold.ts
11
13
  function partTemplate(name) {
12
14
  return `import { box, cut, unwrap } from 'brepjs';
@@ -97,114 +99,20 @@ function scaffoldPart(name, dir) {
97
99
  files
98
100
  };
99
101
  }
100
- function debounce(fn, delayMs = 150) {
101
- let timer;
102
- const cancel = () => {
103
- if (timer) {
104
- clearTimeout(timer);
105
- timer = void 0;
106
- }
107
- };
108
- const trigger = () => {
109
- cancel();
110
- timer = setTimeout(() => {
111
- timer = void 0;
112
- fn();
113
- }, delayMs);
114
- };
115
- return {
116
- trigger,
117
- cancel
118
- };
119
- }
120
- var SOURCE_FILE_RE = /\.(?:m?[jt]s|[jt]sx)$/;
121
- function isWatchRelevant(filename) {
122
- if (filename === void 0 || filename === null) return true;
123
- const name = filename.toString();
124
- if (/(?:^|[\\/])tsconfig(?:\..+)?\.json$/.test(name)) return true;
125
- return SOURCE_FILE_RE.test(name);
126
- }
127
- /**
128
- * Root the watcher at the project, not the entry's own directory: with the common
129
- * `src/main.ts` layout, tsconfig.json (whose JSX options change how sources transpile)
130
- * and `../` imports live one level up. Nearest package.json or tsconfig.json walking up
131
- * wins; entries with no project marker fall back to their own directory.
132
- */
133
- function watchRootFor(entryPath) {
134
- const start = (0, node_path.dirname)(entryPath);
135
- let dir = start;
136
- for (;;) {
137
- if ((0, node_fs.existsSync)((0, node_path.join)(dir, "package.json")) || (0, node_fs.existsSync)((0, node_path.join)(dir, "tsconfig.json"))) return dir;
138
- const parent = (0, node_path.dirname)(dir);
139
- if (parent === dir) return start;
140
- dir = parent;
141
- }
142
- }
143
- var IGNORED_DIR_NAMES = /* @__PURE__ */ new Set([
144
- "node_modules",
145
- "dist",
146
- ".git"
147
- ]);
148
- /**
149
- * Watch a directory tree with one non-recursive `fs.watch` per directory, skipping
150
- * node_modules/dist/.git and never following symlinks. `fs.watch({recursive})` has no
151
- * exclusion API, so on a real project it would descend into node_modules — tens of
152
- * thousands of inotify watches. Whenever an event fires in a directory it is re-scanned,
153
- * which picks up newly created subdirectories. Returns a stop function.
154
- */
155
- function watchTree(root, onEvent) {
156
- const watchers = /* @__PURE__ */ new Map();
157
- const add = (dir) => {
158
- if (watchers.has(dir)) return;
159
- try {
160
- watchers.set(dir, (0, node_fs.watch)(dir, (_event, filename) => {
161
- scan(dir);
162
- onEvent(filename);
163
- }));
164
- } catch {}
165
- };
166
- const scan = (dir) => {
167
- let entries;
168
- try {
169
- entries = (0, node_fs.readdirSync)(dir, { withFileTypes: true });
170
- } catch {
171
- return;
172
- }
173
- for (const entry of entries) {
174
- if (!entry.isDirectory() || IGNORED_DIR_NAMES.has(entry.name)) continue;
175
- const child = (0, node_path.join)(dir, entry.name);
176
- if (watchers.has(child)) continue;
177
- add(child);
178
- scan(child);
179
- }
180
- };
181
- add(root);
182
- scan(root);
183
- return () => {
184
- for (const watcher of watchers.values()) watcher.close();
185
- watchers.clear();
186
- };
187
- }
188
- //#endregion
189
- //#region src/disposeShape.ts
190
- function disposeShape(shape) {
191
- const disposer = shape?.[Symbol.dispose];
192
- if (typeof disposer === "function") disposer.call(shape);
193
- }
194
102
  //#endregion
195
103
  //#region src/cli/exportPart.ts
196
104
  function stem(file) {
197
- return (0, node_path.basename)(file).replace(/\.brep\.ts$/, "").replace(/\.ts$/, "");
105
+ return (0, node_path.basename)(file).replace(/\.brep\.ts$/, "").replace(/\.(?:m?ts|tsx)$/, "");
198
106
  }
199
107
  async function exportPart(modulePath, formats, outDir) {
200
- const { shape, report, step, glb } = await require_diff.runPart(modulePath, {
108
+ const { shape, report, step, glb } = await require_usingCtx.runPart(modulePath, {
201
109
  step: Boolean(formats.step),
202
110
  glb: Boolean(formats.glb)
203
111
  });
204
112
  const errors = [];
205
113
  const written = [];
206
- if (!(require_diff.reportOk(report) && shape !== null)) {
207
- disposeShape(shape);
114
+ if (!(require_usingCtx.reportOk(report) && shape !== null)) {
115
+ require_disposeShape.disposeShape(shape);
208
116
  const failures = report.errorInfos.map((e) => e.message);
209
117
  return {
210
118
  ok: false,
@@ -231,7 +139,7 @@ async function exportPart(modulePath, formats, outDir) {
231
139
  } else errors.push("GLB export produced no data");
232
140
  }
233
141
  if (formats.stl) {
234
- const { exportSTL, isOk } = await require_diff.loadBrep();
142
+ const { exportSTL, isOk } = await require_usingCtx.loadBrep();
235
143
  const r = exportSTL(shape);
236
144
  if (isOk(r)) {
237
145
  const p = (0, node_path.join)(outDir, `${base}.stl`);
@@ -240,7 +148,7 @@ async function exportPart(modulePath, formats, outDir) {
240
148
  } else errors.push(`STL export: ${r.error.message}`);
241
149
  }
242
150
  } finally {
243
- disposeShape(shape);
151
+ require_disposeShape.disposeShape(shape);
244
152
  }
245
153
  return {
246
154
  ok: errors.length === 0,
@@ -250,43 +158,6 @@ async function exportPart(modulePath, formats, outDir) {
250
158
  };
251
159
  }
252
160
  //#endregion
253
- //#region src/cli/openBrowser.ts
254
- /**
255
- * Whether `--serve` should auto-open the browser for the current environment.
256
- *
257
- * Opens only for an interactive session — suppressed under CI, when stderr is
258
- * not a TTY (agent/piped runs), or on Linux without a display server — so
259
- * automation never spawns a browser unexpectedly. An explicit `--no-open` is a
260
- * separate, always-on override handled by the caller.
261
- */
262
- function shouldAutoOpen({ env = process.env, platform = process.platform, isTTY = Boolean(process.stderr.isTTY) } = {}) {
263
- if (env["CI"]) return false;
264
- if (!isTTY) return false;
265
- if (platform === "linux" && !env["DISPLAY"] && !env["WAYLAND_DISPLAY"]) return false;
266
- return true;
267
- }
268
- /** The platform-specific command that opens `url` in the default browser. */
269
- function browserCommand(url, platform) {
270
- if (platform === "darwin") return ["open", [url]];
271
- if (platform === "win32") return ["rundll32", ["url.dll,FileProtocolHandler", url]];
272
- return ["xdg-open", [url]];
273
- }
274
- /**
275
- * Best-effort open of `url` in the default browser. Never throws and never
276
- * blocks — a missing opener just leaves the printed URL as the fallback.
277
- */
278
- function openBrowser(url, platform = process.platform) {
279
- try {
280
- const [cmd, args] = browserCommand(url, platform);
281
- const child = (0, node_child_process.spawn)(cmd, args, {
282
- stdio: "ignore",
283
- detached: true
284
- });
285
- child.on("error", () => {});
286
- child.unref();
287
- } catch {}
288
- }
289
- //#endregion
290
161
  //#region src/snapshot/aiming.ts
291
162
  /**
292
163
  * Kernel-anchored marks for the judge: `B<index>` at each body's bbox centroid (only for multi-body
@@ -372,14 +243,14 @@ program.command("verify", { isDefault: true }).argument("<files...>", "path(s) o
372
243
  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");
373
244
  const results = [];
374
245
  for (const f of targets) {
375
- const { report, shape } = await require_diff.runPart(f, {
246
+ const { report, shape } = await require_usingCtx.runPart(f, {
376
247
  check: Boolean(opts.check),
377
248
  metrics: Boolean(opts.metrics)
378
249
  });
379
- disposeShape(shape);
250
+ require_disposeShape.disposeShape(shape);
380
251
  results.push({
381
252
  file: f,
382
- ok: require_diff.reportOk(report),
253
+ ok: require_usingCtx.reportOk(report),
383
254
  report
384
255
  });
385
256
  }
@@ -391,7 +262,7 @@ program.command("verify", { isDefault: true }).argument("<files...>", "path(s) o
391
262
  }
392
263
  const file = targets[0] ?? files[0] ?? "";
393
264
  const wantStep = Boolean(opts.step) || Boolean(opts.snapshot) || Boolean(opts.serve);
394
- const { report, step, glb, shape } = await require_diff.runPart((0, node_path.resolve)(file), {
265
+ const { report, step, glb, shape } = await require_usingCtx.runPart((0, node_path.resolve)(file), {
395
266
  step: wantStep,
396
267
  glb: Boolean(opts.glb),
397
268
  check: Boolean(opts.check),
@@ -419,27 +290,27 @@ program.command("verify", { isDefault: true }).argument("<files...>", "path(s) o
419
290
  }
420
291
  } else if (opts.snapshot) process.stderr.write("snapshot skipped: STEP export produced no artifact\n");
421
292
  } catch (e) {
422
- require_diff.pushError(report, { message: `artifact write failed: ${e.message}` });
293
+ require_usingCtx.pushError(report, { message: `artifact write failed: ${e.message}` });
423
294
  } finally {
424
- disposeShape(shape);
295
+ require_disposeShape.disposeShape(shape);
425
296
  }
426
- const json = require_diff.serializeReport(report);
297
+ const json = require_usingCtx.serializeReport(report);
427
298
  if (opts.json && opts.json !== "-") (0, node_fs.writeFileSync)(opts.json, json);
428
299
  process.stdout.write(json + "\n");
429
- if (!require_diff.reportOk(report)) process.exitCode = 1;
300
+ if (!require_usingCtx.reportOk(report)) process.exitCode = 1;
430
301
  if (opts.expectInvalid || opts.expectCode) {
431
302
  const codes = [...new Set(report.errorInfos.map((e) => e.code).filter((c) => Boolean(c)))];
432
- const valid = require_diff.reportOk(report);
303
+ const valid = require_usingCtx.reportOk(report);
433
304
  const pass = (!opts.expectInvalid || !valid) && (!opts.expectCode || codes.includes(opts.expectCode));
434
305
  process.exitCode = pass ? 0 : 1;
435
306
  process.stderr.write(`expect: ${pass ? "PASS" : "FAIL"}` + (opts.expectCode ? ` code=${opts.expectCode}` : "") + (opts.expectInvalid ? " invalid" : "") + ` (ok=${valid}, codes=${codes.join(",") || "none"})\n`);
436
307
  return;
437
308
  }
438
- if (Boolean(opts.serve) && stepPath !== void 0 && require_diff.reportOk(report) && stepPath) {
309
+ if (Boolean(opts.serve) && stepPath !== void 0 && require_usingCtx.reportOk(report) && stepPath) {
439
310
  const { serve } = await Promise.resolve().then(() => require("../snapshot/serve.cjs"));
440
311
  const { url, reused } = await serve({ file: stepPath });
441
312
  process.stderr.write(`viewer: ${url}\n`);
442
- if (!reused && opts.open && shouldAutoOpen()) openBrowser(url);
313
+ if (!reused && opts.open && require_openBrowser.shouldAutoOpen()) require_openBrowser.openBrowser(url);
443
314
  }
444
315
  });
445
316
  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) => {
@@ -462,14 +333,14 @@ program.command("snapshot").argument("<file>", "path to a .brep.ts module; rende
462
333
  const resolved = (0, node_path.resolve)(file);
463
334
  const base = opts.out ? (0, node_path.resolve)(opts.out) : `${resolved}-shots`;
464
335
  const outDir = opts.label ? (0, node_path.join)(base, opts.label) : base;
465
- const { report, step, shape } = await require_diff.runPart(resolved, {
336
+ const { report, step, shape } = await require_usingCtx.runPart(resolved, {
466
337
  step: true,
467
338
  metrics: true
468
339
  });
469
340
  try {
470
341
  if (!step) {
471
342
  process.stderr.write("snapshot: part produced no STEP — nothing to render\n");
472
- process.stdout.write(require_diff.serializeReport(report) + "\n");
343
+ process.stdout.write(require_usingCtx.serializeReport(report) + "\n");
473
344
  process.exitCode = 1;
474
345
  return;
475
346
  }
@@ -477,7 +348,7 @@ program.command("snapshot").argument("<file>", "path to a .brep.ts module; rende
477
348
  (0, node_fs.writeFileSync)(stepPath, Buffer.from(step));
478
349
  const shoot = await loadSnapshotShoot();
479
350
  if (!shoot) {
480
- process.stdout.write(require_diff.serializeReport(report) + "\n");
351
+ process.stdout.write(require_usingCtx.serializeReport(report) + "\n");
481
352
  return;
482
353
  }
483
354
  const section = aimedSection(report.manufacturability?.bores ?? [], report.measurements.bounds);
@@ -491,7 +362,7 @@ program.command("snapshot").argument("<file>", "path to a .brep.ts module; rende
491
362
  });
492
363
  for (const p of pngs) process.stdout.write(`${p}\n`);
493
364
  } finally {
494
- disposeShape(shape);
365
+ require_disposeShape.disposeShape(shape);
495
366
  }
496
367
  });
497
368
  program.command("init").argument("<name>", "part name; scaffolds <name>.brep.ts + tsconfig.json + README.md").option("--out <dir>", "target directory (defaults to ./<name>)").action((name, opts) => {
@@ -506,40 +377,22 @@ program.command("watch").argument("<file>", "path to a .brep.ts module; re-verif
506
377
  const path = (0, node_path.resolve)(file);
507
378
  const run = async (fresh) => {
508
379
  try {
509
- const { report, shape } = await require_diff.runPart(path, { freshImport: fresh });
380
+ const { report, shape } = await require_usingCtx.runPart(path, { freshImport: fresh });
510
381
  try {
511
- process.stdout.write(require_diff.serializeReport(report) + "\n");
382
+ process.stdout.write(require_usingCtx.serializeReport(report) + "\n");
512
383
  } finally {
513
- disposeShape(shape);
384
+ require_disposeShape.disposeShape(shape);
514
385
  }
515
386
  } catch (e) {
516
387
  process.stderr.write(`watch run failed: ${e.message}\n`);
517
388
  }
518
389
  };
519
- let inFlight = false;
520
- let rerunQueued = false;
521
- const start = (fresh) => {
522
- inFlight = true;
523
- run(fresh).finally(() => {
524
- inFlight = false;
525
- if (rerunQueued) {
526
- rerunQueued = false;
527
- start(true);
528
- }
529
- });
530
- };
531
- const schedule = () => {
532
- if (inFlight) {
533
- rerunQueued = true;
534
- return;
535
- }
536
- start(true);
537
- };
538
- const { trigger } = debounce(schedule, 150);
390
+ const runner = require_openBrowser.createSerialRunner(run);
391
+ const { trigger } = require_openBrowser.debounce(runner.trigger, 150);
539
392
  process.stderr.write(`watching ${path} (Ctrl-C to stop)\n`);
540
- start(false);
541
- const stopWatching = watchTree(watchRootFor(path), (filename) => {
542
- if (isWatchRelevant(filename)) trigger();
393
+ runner.start(false);
394
+ const stopWatching = require_openBrowser.watchTree(require_openBrowser.watchRootFor(path), (filename) => {
395
+ if (require_openBrowser.isWatchRelevant(filename)) trigger();
543
396
  });
544
397
  const stop = () => {
545
398
  stopWatching();
@@ -548,7 +401,48 @@ program.command("watch").argument("<file>", "path to a .brep.ts module; re-verif
548
401
  process.on("SIGINT", stop);
549
402
  process.on("SIGTERM", stop);
550
403
  });
551
- program.command("export").argument("<file>", "path to a .brep.ts module").option("--step", "write a STEP artifact").option("--glb", "write a GLB artifact").option("--stl", "write an STL artifact").option("--all", "write STEP + GLB + STL").option("--out <dir>", "output directory", ".").action(async (file, opts) => {
404
+ program.command("preview").argument("<file>", "path to a model module (.ts/.tsx); a families element tree renders per element").option("--watch", "re-evaluate on save and push updates to the open viewer tab").option("--write-only", "write artifacts without starting the viewer server (exits unless --watch)").option("--glb <out>", "write a merged GLB of the whole model").option("--json <out>", "write the JSON summary to this path").option("--port <n>", "viewer server port (default: OS-assigned)", (v) => parseInt(v, 10)).option("--no-open", "do not auto-open the browser").action(async (file, opts) => {
405
+ const { runPreview } = await Promise.resolve().then(() => require("../preview/preview.cjs"));
406
+ try {
407
+ await runPreview((0, node_path.resolve)(file), opts);
408
+ } catch (e) {
409
+ process.stderr.write(`preview failed: ${e.message}\n`);
410
+ process.exitCode = 1;
411
+ }
412
+ });
413
+ program.command("export").argument("<file>", "path to a part (.brep.ts) module; --3dm also accepts a model (.ts/.tsx) default-exporting a families element tree").option("--step", "write a STEP artifact").option("--glb", "write a GLB artifact").option("--stl", "write an STL artifact").option("--all", "write STEP + GLB + STL").option("--3dm", "write a Rhino .3dm (element-aware: one named object per element, layered)").option("--out <dir>", "output directory", ".").action(async (file, opts) => {
414
+ const path = (0, node_path.resolve)(file);
415
+ if (opts["3dm"] && (opts.step || opts.glb || opts.stl || opts.all)) {
416
+ process.stderr.write("--3dm cannot be combined with --step/--glb/--stl/--all\n");
417
+ process.exitCode = 1;
418
+ return;
419
+ }
420
+ if (opts["3dm"]) {
421
+ let run;
422
+ try {
423
+ ({export3dm: run} = await Promise.resolve().then(() => require("../export3dm/export3dm.cjs")));
424
+ } catch {
425
+ process.stderr.write(".3dm export needs rhino3dm — run: npm i rhino3dm\n");
426
+ process.exitCode = 1;
427
+ return;
428
+ }
429
+ const outPath = (0, node_path.join)((0, node_path.resolve)(opts.out), stem(path) + ".3dm");
430
+ try {
431
+ const r = await run(path, outPath);
432
+ process.stderr.write(`wrote: ${outPath}\n`);
433
+ process.stdout.write(JSON.stringify({
434
+ ok: r.failed.length === 0,
435
+ written: [outPath],
436
+ elements: r.elements,
437
+ failed: r.failed
438
+ }, null, 2) + "\n");
439
+ if (r.failed.length > 0) process.exitCode = 1;
440
+ } catch (e) {
441
+ process.stderr.write(`3dm export failed: ${e.message}\n`);
442
+ process.exitCode = 1;
443
+ }
444
+ return;
445
+ }
552
446
  const formats = opts.all ? {
553
447
  step: true,
554
448
  glb: true,
@@ -559,11 +453,11 @@ program.command("export").argument("<file>", "path to a .brep.ts module").option
559
453
  stl: Boolean(opts.stl)
560
454
  };
561
455
  if (!formats.step && !formats.glb && !formats.stl) {
562
- process.stderr.write("no formats requested — pass --step/--glb/--stl or --all\n");
456
+ process.stderr.write("no formats requested — pass --step/--glb/--stl/--3dm or --all\n");
563
457
  process.exitCode = 1;
564
458
  return;
565
459
  }
566
- const result = await exportPart((0, node_path.resolve)(file), formats, (0, node_path.resolve)(opts.out));
460
+ const result = await exportPart(path, formats, (0, node_path.resolve)(opts.out));
567
461
  for (const p of result.written) process.stderr.write(`wrote: ${p}\n`);
568
462
  for (const e of result.errors) process.stderr.write(`error: ${e}\n`);
569
463
  process.stdout.write(JSON.stringify({