brepjs-cad 0.174.0 → 0.176.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,19 @@
1
1
  # Changelog
2
2
 
3
+ ## [0.176.0](https://github.com/andymai/brepjs/compare/brepjs-cad-v0.175.0...brepjs-cad-v0.176.0) (2026-08-26)
4
+
5
+
6
+ ### Features
7
+
8
+ * **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))
9
+
10
+ ## [0.175.0](https://github.com/andymai/brepjs/compare/brepjs-cad-v0.174.0...brepjs-cad-v0.175.0) (2026-08-26)
11
+
12
+
13
+ ### Features
14
+
15
+ * **brepjs-cad:** transpiling loader for multi-file TS/TSX projects + working watch ([#2228](https://github.com/andymai/brepjs/issues/2228)) ([980ead6](https://github.com/andymai/brepjs/commit/980ead6508867c31fcec1fd06a683f03bacb1e6f))
16
+
3
17
  ## [0.174.0](https://github.com/andymai/brepjs/compare/brepjs-cad-v0.173.0...brepjs-cad-v0.174.0) (2026-08-22)
4
18
 
5
19
 
@@ -1,14 +1,15 @@
1
1
  Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
2
- const require_diff = require("./diff-DDu3OLyd.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-DNbyRoEg.cjs");
3
+ const require_diff = require("./diff-CmIV_qOi.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-CRu6Pqyr.js";
1
+ import { c as evaluateExpected, d as runChecks, f as emptyReport, h as serializeReport, i as TYPECHECK_CODE, l as isExpectedDims, o as typecheckPart, r as runPart, s as DEFAULT_TOLERANCE_PCT, u as pctDelta } from "./usingCtx-DOzK2eJh.js";
2
+ import { n as runMeasure, t as runDiff } from "./diff-C2vDUt9F.js";
2
3
  export { DEFAULT_TOLERANCE_PCT, TYPECHECK_CODE, emptyReport, evaluateExpected, isExpectedDims, pctDelta, runChecks, runDiff, runMeasure, runPart, serializeReport, typecheckPart };
package/dist/cli/main.cjs CHANGED
@@ -1,12 +1,13 @@
1
1
  #!/usr/bin/env node
2
2
  Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
3
- const require_diff = require("../diff-DDu3OLyd.cjs");
3
+ const require_usingCtx = require("../usingCtx-DNbyRoEg.cjs");
4
+ const require_diff = require("../diff-CmIV_qOi.cjs");
5
+ const require_openBrowser = require("../openBrowser-C1AiX6cC.cjs");
4
6
  let node_url = require("node:url");
5
7
  let node_fs = require("node:fs");
6
8
  let node_path = require("node:path");
7
9
  let commander = require("commander");
8
10
  let node_os = require("node:os");
9
- let node_child_process = require("node:child_process");
10
11
  //#region src/cli/scaffold.ts
11
12
  function partTemplate(name) {
12
13
  return `import { box, cut, unwrap } from 'brepjs';
@@ -97,46 +98,20 @@ function scaffoldPart(name, dir) {
97
98
  files
98
99
  };
99
100
  }
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
- //#endregion
121
- //#region src/disposeShape.ts
122
- function disposeShape(shape) {
123
- const disposer = shape?.[Symbol.dispose];
124
- if (typeof disposer === "function") disposer.call(shape);
125
- }
126
101
  //#endregion
127
102
  //#region src/cli/exportPart.ts
128
103
  function stem(file) {
129
104
  return (0, node_path.basename)(file).replace(/\.brep\.ts$/, "").replace(/\.ts$/, "");
130
105
  }
131
106
  async function exportPart(modulePath, formats, outDir) {
132
- const { shape, report, step, glb } = await require_diff.runPart(modulePath, {
107
+ const { shape, report, step, glb } = await require_usingCtx.runPart(modulePath, {
133
108
  step: Boolean(formats.step),
134
109
  glb: Boolean(formats.glb)
135
110
  });
136
111
  const errors = [];
137
112
  const written = [];
138
- if (!(require_diff.reportOk(report) && shape !== null)) {
139
- disposeShape(shape);
113
+ if (!(require_usingCtx.reportOk(report) && shape !== null)) {
114
+ require_openBrowser.disposeShape(shape);
140
115
  const failures = report.errorInfos.map((e) => e.message);
141
116
  return {
142
117
  ok: false,
@@ -163,7 +138,7 @@ async function exportPart(modulePath, formats, outDir) {
163
138
  } else errors.push("GLB export produced no data");
164
139
  }
165
140
  if (formats.stl) {
166
- const { exportSTL, isOk } = await require_diff.loadBrep();
141
+ const { exportSTL, isOk } = await require_usingCtx.loadBrep();
167
142
  const r = exportSTL(shape);
168
143
  if (isOk(r)) {
169
144
  const p = (0, node_path.join)(outDir, `${base}.stl`);
@@ -172,7 +147,7 @@ async function exportPart(modulePath, formats, outDir) {
172
147
  } else errors.push(`STL export: ${r.error.message}`);
173
148
  }
174
149
  } finally {
175
- disposeShape(shape);
150
+ require_openBrowser.disposeShape(shape);
176
151
  }
177
152
  return {
178
153
  ok: errors.length === 0,
@@ -182,43 +157,6 @@ async function exportPart(modulePath, formats, outDir) {
182
157
  };
183
158
  }
184
159
  //#endregion
185
- //#region src/cli/openBrowser.ts
186
- /**
187
- * Whether `--serve` should auto-open the browser for the current environment.
188
- *
189
- * Opens only for an interactive session — suppressed under CI, when stderr is
190
- * not a TTY (agent/piped runs), or on Linux without a display server — so
191
- * automation never spawns a browser unexpectedly. An explicit `--no-open` is a
192
- * separate, always-on override handled by the caller.
193
- */
194
- function shouldAutoOpen({ env = process.env, platform = process.platform, isTTY = Boolean(process.stderr.isTTY) } = {}) {
195
- if (env["CI"]) return false;
196
- if (!isTTY) return false;
197
- if (platform === "linux" && !env["DISPLAY"] && !env["WAYLAND_DISPLAY"]) return false;
198
- return true;
199
- }
200
- /** The platform-specific command that opens `url` in the default browser. */
201
- function browserCommand(url, platform) {
202
- if (platform === "darwin") return ["open", [url]];
203
- if (platform === "win32") return ["rundll32", ["url.dll,FileProtocolHandler", url]];
204
- return ["xdg-open", [url]];
205
- }
206
- /**
207
- * Best-effort open of `url` in the default browser. Never throws and never
208
- * blocks — a missing opener just leaves the printed URL as the fallback.
209
- */
210
- function openBrowser(url, platform = process.platform) {
211
- try {
212
- const [cmd, args] = browserCommand(url, platform);
213
- const child = (0, node_child_process.spawn)(cmd, args, {
214
- stdio: "ignore",
215
- detached: true
216
- });
217
- child.on("error", () => {});
218
- child.unref();
219
- } catch {}
220
- }
221
- //#endregion
222
160
  //#region src/snapshot/aiming.ts
223
161
  /**
224
162
  * Kernel-anchored marks for the judge: `B<index>` at each body's bbox centroid (only for multi-body
@@ -304,14 +242,14 @@ program.command("verify", { isDefault: true }).argument("<files...>", "path(s) o
304
242
  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");
305
243
  const results = [];
306
244
  for (const f of targets) {
307
- const { report, shape } = await require_diff.runPart(f, {
245
+ const { report, shape } = await require_usingCtx.runPart(f, {
308
246
  check: Boolean(opts.check),
309
247
  metrics: Boolean(opts.metrics)
310
248
  });
311
- disposeShape(shape);
249
+ require_openBrowser.disposeShape(shape);
312
250
  results.push({
313
251
  file: f,
314
- ok: require_diff.reportOk(report),
252
+ ok: require_usingCtx.reportOk(report),
315
253
  report
316
254
  });
317
255
  }
@@ -323,7 +261,7 @@ program.command("verify", { isDefault: true }).argument("<files...>", "path(s) o
323
261
  }
324
262
  const file = targets[0] ?? files[0] ?? "";
325
263
  const wantStep = Boolean(opts.step) || Boolean(opts.snapshot) || Boolean(opts.serve);
326
- const { report, step, glb, shape } = await require_diff.runPart((0, node_path.resolve)(file), {
264
+ const { report, step, glb, shape } = await require_usingCtx.runPart((0, node_path.resolve)(file), {
327
265
  step: wantStep,
328
266
  glb: Boolean(opts.glb),
329
267
  check: Boolean(opts.check),
@@ -351,27 +289,27 @@ program.command("verify", { isDefault: true }).argument("<files...>", "path(s) o
351
289
  }
352
290
  } else if (opts.snapshot) process.stderr.write("snapshot skipped: STEP export produced no artifact\n");
353
291
  } catch (e) {
354
- require_diff.pushError(report, { message: `artifact write failed: ${e.message}` });
292
+ require_usingCtx.pushError(report, { message: `artifact write failed: ${e.message}` });
355
293
  } finally {
356
- disposeShape(shape);
294
+ require_openBrowser.disposeShape(shape);
357
295
  }
358
- const json = require_diff.serializeReport(report);
296
+ const json = require_usingCtx.serializeReport(report);
359
297
  if (opts.json && opts.json !== "-") (0, node_fs.writeFileSync)(opts.json, json);
360
298
  process.stdout.write(json + "\n");
361
- if (!require_diff.reportOk(report)) process.exitCode = 1;
299
+ if (!require_usingCtx.reportOk(report)) process.exitCode = 1;
362
300
  if (opts.expectInvalid || opts.expectCode) {
363
301
  const codes = [...new Set(report.errorInfos.map((e) => e.code).filter((c) => Boolean(c)))];
364
- const valid = require_diff.reportOk(report);
302
+ const valid = require_usingCtx.reportOk(report);
365
303
  const pass = (!opts.expectInvalid || !valid) && (!opts.expectCode || codes.includes(opts.expectCode));
366
304
  process.exitCode = pass ? 0 : 1;
367
305
  process.stderr.write(`expect: ${pass ? "PASS" : "FAIL"}` + (opts.expectCode ? ` code=${opts.expectCode}` : "") + (opts.expectInvalid ? " invalid" : "") + ` (ok=${valid}, codes=${codes.join(",") || "none"})\n`);
368
306
  return;
369
307
  }
370
- if (Boolean(opts.serve) && stepPath !== void 0 && require_diff.reportOk(report) && stepPath) {
308
+ if (Boolean(opts.serve) && stepPath !== void 0 && require_usingCtx.reportOk(report) && stepPath) {
371
309
  const { serve } = await Promise.resolve().then(() => require("../snapshot/serve.cjs"));
372
310
  const { url, reused } = await serve({ file: stepPath });
373
311
  process.stderr.write(`viewer: ${url}\n`);
374
- if (!reused && opts.open && shouldAutoOpen()) openBrowser(url);
312
+ if (!reused && opts.open && require_openBrowser.shouldAutoOpen()) require_openBrowser.openBrowser(url);
375
313
  }
376
314
  });
377
315
  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) => {
@@ -394,14 +332,14 @@ program.command("snapshot").argument("<file>", "path to a .brep.ts module; rende
394
332
  const resolved = (0, node_path.resolve)(file);
395
333
  const base = opts.out ? (0, node_path.resolve)(opts.out) : `${resolved}-shots`;
396
334
  const outDir = opts.label ? (0, node_path.join)(base, opts.label) : base;
397
- const { report, step, shape } = await require_diff.runPart(resolved, {
335
+ const { report, step, shape } = await require_usingCtx.runPart(resolved, {
398
336
  step: true,
399
337
  metrics: true
400
338
  });
401
339
  try {
402
340
  if (!step) {
403
341
  process.stderr.write("snapshot: part produced no STEP — nothing to render\n");
404
- process.stdout.write(require_diff.serializeReport(report) + "\n");
342
+ process.stdout.write(require_usingCtx.serializeReport(report) + "\n");
405
343
  process.exitCode = 1;
406
344
  return;
407
345
  }
@@ -409,7 +347,7 @@ program.command("snapshot").argument("<file>", "path to a .brep.ts module; rende
409
347
  (0, node_fs.writeFileSync)(stepPath, Buffer.from(step));
410
348
  const shoot = await loadSnapshotShoot();
411
349
  if (!shoot) {
412
- process.stdout.write(require_diff.serializeReport(report) + "\n");
350
+ process.stdout.write(require_usingCtx.serializeReport(report) + "\n");
413
351
  return;
414
352
  }
415
353
  const section = aimedSection(report.manufacturability?.bores ?? [], report.measurements.bounds);
@@ -423,7 +361,7 @@ program.command("snapshot").argument("<file>", "path to a .brep.ts module; rende
423
361
  });
424
362
  for (const p of pngs) process.stdout.write(`${p}\n`);
425
363
  } finally {
426
- disposeShape(shape);
364
+ require_openBrowser.disposeShape(shape);
427
365
  }
428
366
  });
429
367
  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) => {
@@ -436,35 +374,41 @@ program.command("init").argument("<name>", "part name; scaffolds <name>.brep.ts
436
374
  });
437
375
  program.command("watch").argument("<file>", "path to a .brep.ts module; re-verifies on each save until Ctrl-C").action((file) => {
438
376
  const path = (0, node_path.resolve)(file);
439
- const run = async () => {
377
+ const run = async (fresh) => {
440
378
  try {
441
- const { report, shape } = await require_diff.runPart(path);
379
+ const { report, shape } = await require_usingCtx.runPart(path, { freshImport: fresh });
442
380
  try {
443
- process.stdout.write(require_diff.serializeReport(report) + "\n");
381
+ process.stdout.write(require_usingCtx.serializeReport(report) + "\n");
444
382
  } finally {
445
- disposeShape(shape);
383
+ require_openBrowser.disposeShape(shape);
446
384
  }
447
385
  } catch (e) {
448
386
  process.stderr.write(`watch run failed: ${e.message}\n`);
449
387
  }
450
388
  };
451
- const { trigger } = debounce(run, 150);
389
+ const runner = require_openBrowser.createSerialRunner(run);
390
+ const { trigger } = require_openBrowser.debounce(runner.trigger, 150);
452
391
  process.stderr.write(`watching ${path} (Ctrl-C to stop)\n`);
453
- run();
454
- const watcher = (0, node_fs.watch)((0, node_path.dirname)(path), (_event, filename) => {
455
- if (filename === void 0 || filename === null) {
456
- trigger();
457
- return;
458
- }
459
- if ((0, node_path.basename)(path) === filename.toString()) trigger();
392
+ runner.start(false);
393
+ const stopWatching = require_openBrowser.watchTree(require_openBrowser.watchRootFor(path), (filename) => {
394
+ if (require_openBrowser.isWatchRelevant(filename)) trigger();
460
395
  });
461
396
  const stop = () => {
462
- watcher.close();
397
+ stopWatching();
463
398
  process.exit(0);
464
399
  };
465
400
  process.on("SIGINT", stop);
466
401
  process.on("SIGTERM", stop);
467
402
  });
403
+ 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 and exit without starting the viewer server").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) => {
404
+ const { runPreview } = await Promise.resolve().then(() => require("../preview/preview.cjs"));
405
+ try {
406
+ await runPreview((0, node_path.resolve)(file), opts);
407
+ } catch (e) {
408
+ process.stderr.write(`preview failed: ${e.message}\n`);
409
+ process.exitCode = 1;
410
+ }
411
+ });
468
412
  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) => {
469
413
  const formats = opts.all ? {
470
414
  step: true,
package/dist/cli/main.js CHANGED
@@ -1,11 +1,12 @@
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-CRu6Pqyr.js";
2
+ import { _ as loadBrep, h as serializeReport, m as reportOk, p as pushError, r as runPart } from "../usingCtx-DOzK2eJh.js";
3
+ import { n as runMeasure, t as runDiff } from "../diff-C2vDUt9F.js";
4
+ import { a as debounce, c as watchTree, i as createSerialRunner, n as shouldAutoOpen, o as isWatchRelevant, r as disposeShape, s as watchRootFor, t as openBrowser } from "../openBrowser-BBYw6VVb.js";
3
5
  import { fileURLToPath } from "node:url";
4
- import { existsSync, globSync, mkdirSync, realpathSync, watch, writeFileSync } from "node:fs";
5
- import { basename, dirname, join, resolve } from "node:path";
6
+ import { existsSync, globSync, mkdirSync, realpathSync, writeFileSync } from "node:fs";
7
+ import { basename, join, resolve } from "node:path";
6
8
  import { Command } from "commander";
7
9
  import { tmpdir } from "node:os";
8
- import { spawn } from "node:child_process";
9
10
  //#region src/cli/scaffold.ts
10
11
  function partTemplate(name) {
11
12
  return `import { box, cut, unwrap } from 'brepjs';
@@ -96,32 +97,6 @@ function scaffoldPart(name, dir) {
96
97
  files
97
98
  };
98
99
  }
99
- function debounce(fn, delayMs = 150) {
100
- let timer;
101
- const cancel = () => {
102
- if (timer) {
103
- clearTimeout(timer);
104
- timer = void 0;
105
- }
106
- };
107
- const trigger = () => {
108
- cancel();
109
- timer = setTimeout(() => {
110
- timer = void 0;
111
- fn();
112
- }, delayMs);
113
- };
114
- return {
115
- trigger,
116
- cancel
117
- };
118
- }
119
- //#endregion
120
- //#region src/disposeShape.ts
121
- function disposeShape(shape) {
122
- const disposer = shape?.[Symbol.dispose];
123
- if (typeof disposer === "function") disposer.call(shape);
124
- }
125
100
  //#endregion
126
101
  //#region src/cli/exportPart.ts
127
102
  function stem(file) {
@@ -181,43 +156,6 @@ async function exportPart(modulePath, formats, outDir) {
181
156
  };
182
157
  }
183
158
  //#endregion
184
- //#region src/cli/openBrowser.ts
185
- /**
186
- * Whether `--serve` should auto-open the browser for the current environment.
187
- *
188
- * Opens only for an interactive session — suppressed under CI, when stderr is
189
- * not a TTY (agent/piped runs), or on Linux without a display server — so
190
- * automation never spawns a browser unexpectedly. An explicit `--no-open` is a
191
- * separate, always-on override handled by the caller.
192
- */
193
- function shouldAutoOpen({ env = process.env, platform = process.platform, isTTY = Boolean(process.stderr.isTTY) } = {}) {
194
- if (env["CI"]) return false;
195
- if (!isTTY) return false;
196
- if (platform === "linux" && !env["DISPLAY"] && !env["WAYLAND_DISPLAY"]) return false;
197
- return true;
198
- }
199
- /** The platform-specific command that opens `url` in the default browser. */
200
- function browserCommand(url, platform) {
201
- if (platform === "darwin") return ["open", [url]];
202
- if (platform === "win32") return ["rundll32", ["url.dll,FileProtocolHandler", url]];
203
- return ["xdg-open", [url]];
204
- }
205
- /**
206
- * Best-effort open of `url` in the default browser. Never throws and never
207
- * blocks — a missing opener just leaves the printed URL as the fallback.
208
- */
209
- function openBrowser(url, platform = process.platform) {
210
- try {
211
- const [cmd, args] = browserCommand(url, platform);
212
- const child = spawn(cmd, args, {
213
- stdio: "ignore",
214
- detached: true
215
- });
216
- child.on("error", () => {});
217
- child.unref();
218
- } catch {}
219
- }
220
- //#endregion
221
159
  //#region src/snapshot/aiming.ts
222
160
  /**
223
161
  * Kernel-anchored marks for the judge: `B<index>` at each body's bbox centroid (only for multi-body
@@ -435,9 +373,9 @@ program.command("init").argument("<name>", "part name; scaffolds <name>.brep.ts
435
373
  });
436
374
  program.command("watch").argument("<file>", "path to a .brep.ts module; re-verifies on each save until Ctrl-C").action((file) => {
437
375
  const path = resolve(file);
438
- const run = async () => {
376
+ const run = async (fresh) => {
439
377
  try {
440
- const { report, shape } = await runPart(path);
378
+ const { report, shape } = await runPart(path, { freshImport: fresh });
441
379
  try {
442
380
  process.stdout.write(serializeReport(report) + "\n");
443
381
  } finally {
@@ -447,23 +385,29 @@ program.command("watch").argument("<file>", "path to a .brep.ts module; re-verif
447
385
  process.stderr.write(`watch run failed: ${e.message}\n`);
448
386
  }
449
387
  };
450
- const { trigger } = debounce(run, 150);
388
+ const runner = createSerialRunner(run);
389
+ const { trigger } = debounce(runner.trigger, 150);
451
390
  process.stderr.write(`watching ${path} (Ctrl-C to stop)\n`);
452
- run();
453
- const watcher = watch(dirname(path), (_event, filename) => {
454
- if (filename === void 0 || filename === null) {
455
- trigger();
456
- return;
457
- }
458
- if (basename(path) === filename.toString()) trigger();
391
+ runner.start(false);
392
+ const stopWatching = watchTree(watchRootFor(path), (filename) => {
393
+ if (isWatchRelevant(filename)) trigger();
459
394
  });
460
395
  const stop = () => {
461
- watcher.close();
396
+ stopWatching();
462
397
  process.exit(0);
463
398
  };
464
399
  process.on("SIGINT", stop);
465
400
  process.on("SIGTERM", stop);
466
401
  });
402
+ 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 and exit without starting the viewer server").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) => {
403
+ const { runPreview } = await import("../preview/preview.js");
404
+ try {
405
+ await runPreview(resolve(file), opts);
406
+ } catch (e) {
407
+ process.stderr.write(`preview failed: ${e.message}\n`);
408
+ process.exitCode = 1;
409
+ }
410
+ });
467
411
  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) => {
468
412
  const formats = opts.all ? {
469
413
  step: true,
@@ -3,3 +3,28 @@ export declare function debounce(fn: () => void | Promise<void>, delayMs?: numbe
3
3
  trigger: () => void;
4
4
  cancel: () => void;
5
5
  };
6
+ export declare function isWatchRelevant(filename: string | Buffer | null | undefined): boolean;
7
+ /**
8
+ * Root the watcher at the project, not the entry's own directory: with the common
9
+ * `src/main.ts` layout, tsconfig.json (whose JSX options change how sources transpile)
10
+ * and `../` imports live one level up. Nearest package.json or tsconfig.json walking up
11
+ * wins; entries with no project marker fall back to their own directory.
12
+ */
13
+ export declare function watchRootFor(entryPath: string): string;
14
+ /**
15
+ * Watch a directory tree with one non-recursive `fs.watch` per directory, skipping
16
+ * node_modules/dist/.git and never following symlinks. `fs.watch({recursive})` has no
17
+ * exclusion API, so on a real project it would descend into node_modules — tens of
18
+ * thousands of inotify watches. Whenever an event fires in a directory it is re-scanned,
19
+ * which picks up newly created subdirectories. Returns a stop function.
20
+ */
21
+ export declare function watchTree(root: string, onEvent: (filename: string | Buffer | null | undefined) => void): () => void;
22
+ /**
23
+ * Serialize an async job: while one run is in flight, triggers coalesce into exactly
24
+ * one trailing rerun (verify/preview runs share one kernel — overlapping runs would
25
+ * interleave output and reorder reports).
26
+ */
27
+ export declare function createSerialRunner(run: (fresh: boolean) => Promise<void>): {
28
+ start: (fresh: boolean) => void;
29
+ trigger: () => void;
30
+ };
@@ -0,0 +1,143 @@
1
+ import { _ as loadBrep, r as runPart, t as _usingCtx } from "./usingCtx-DOzK2eJh.js";
2
+ //#region src/verify/measure.ts
3
+ async function runMeasure(aPath, bPath) {
4
+ try {
5
+ var _usingCtx$2 = _usingCtx();
6
+ const { measureDistance, measureLength, isOk } = await loadBrep();
7
+ const errors = [];
8
+ const a = await runPart(aPath);
9
+ errors.push(...a.report.errors);
10
+ if (!a.shape) return { errors };
11
+ const sa = _usingCtx$2.u(a.shape);
12
+ if (bPath === void 0) {
13
+ const len = measureLength(sa);
14
+ if (isOk(len)) return {
15
+ length: len.value,
16
+ errors
17
+ };
18
+ errors.push(`measureLength: ${len.error.message}`);
19
+ return { errors };
20
+ }
21
+ const b = await runPart(bPath);
22
+ errors.push(...b.report.errors);
23
+ if (!b.shape) return { errors };
24
+ const dist = measureDistance(sa, _usingCtx$2.u(b.shape));
25
+ if (isOk(dist)) return {
26
+ distance: dist.value,
27
+ errors
28
+ };
29
+ errors.push(`measureDistance: ${dist.error.message}`);
30
+ return { errors };
31
+ } catch (_) {
32
+ _usingCtx$2.e = _;
33
+ } finally {
34
+ _usingCtx$2.d();
35
+ }
36
+ }
37
+ //#endregion
38
+ //#region src/verify/diff.ts
39
+ function emptyDiff(errors) {
40
+ return {
41
+ volumeDelta: 0,
42
+ areaDelta: 0,
43
+ bboxDelta: {
44
+ xMin: 0,
45
+ xMax: 0,
46
+ yMin: 0,
47
+ yMax: 0,
48
+ zMin: 0,
49
+ zMax: 0
50
+ },
51
+ symmetricDifferenceVolume: 0,
52
+ errors
53
+ };
54
+ }
55
+ function volumeOf(brep, shape, errors) {
56
+ const v = brep.measureVolume(shape);
57
+ if (brep.isOk(v)) return v.value;
58
+ errors.push(`measureVolume: ${v.error.message}`);
59
+ return 0;
60
+ }
61
+ function areaOf(brep, shape, errors) {
62
+ if (!brep.isShape3D(shape)) return 0;
63
+ const a = brep.measureArea(shape);
64
+ if (brep.isOk(a)) return a.value;
65
+ errors.push(`measureArea: ${a.error.message}`);
66
+ return 0;
67
+ }
68
+ function boundsDelta(brep, a, b, errors) {
69
+ try {
70
+ const ba = brep.getBounds(a);
71
+ const bb = brep.getBounds(b);
72
+ return {
73
+ xMin: bb.xMin - ba.xMin,
74
+ xMax: bb.xMax - ba.xMax,
75
+ yMin: bb.yMin - ba.yMin,
76
+ yMax: bb.yMax - ba.yMax,
77
+ zMin: bb.zMin - ba.zMin,
78
+ zMax: bb.zMax - ba.zMax
79
+ };
80
+ } catch (e) {
81
+ errors.push(`getBounds: ${e.message}`);
82
+ return {
83
+ xMin: 0,
84
+ xMax: 0,
85
+ yMin: 0,
86
+ yMax: 0,
87
+ zMin: 0,
88
+ zMax: 0
89
+ };
90
+ }
91
+ }
92
+ function cutVolume(brep, x, y, errors) {
93
+ try {
94
+ var _usingCtx$1 = _usingCtx();
95
+ const r = brep.cut(x, y);
96
+ if (!brep.isOk(r)) {
97
+ errors.push(`cut: ${r.error.message}`);
98
+ return 0;
99
+ }
100
+ return volumeOf(brep, _usingCtx$1.u(r.value), errors);
101
+ } catch (_) {
102
+ _usingCtx$1.e = _;
103
+ } finally {
104
+ _usingCtx$1.d();
105
+ }
106
+ }
107
+ async function runDiff(aPath, bPath) {
108
+ try {
109
+ var _usingCtx3 = _usingCtx();
110
+ const brep = await loadBrep();
111
+ const { isShape3D } = brep;
112
+ const errors = [];
113
+ const a = await runPart(aPath);
114
+ errors.push(...a.report.errors);
115
+ if (!a.shape) return emptyDiff(errors);
116
+ const sa = _usingCtx3.u(a.shape);
117
+ const b = await runPart(bPath);
118
+ errors.push(...b.report.errors);
119
+ if (!b.shape) return emptyDiff(errors);
120
+ const sb = _usingCtx3.u(b.shape);
121
+ const bboxDelta = boundsDelta(brep, sa, sb, errors);
122
+ const areaDelta = areaOf(brep, sb, errors) - areaOf(brep, sa, errors);
123
+ let volumeDelta = 0;
124
+ let symmetricDifferenceVolume = 0;
125
+ if (isShape3D(sa) && isShape3D(sb)) {
126
+ volumeDelta = volumeOf(brep, sb, errors) - volumeOf(brep, sa, errors);
127
+ symmetricDifferenceVolume = cutVolume(brep, sa, sb, errors) + cutVolume(brep, sb, sa, errors);
128
+ }
129
+ return {
130
+ volumeDelta,
131
+ areaDelta,
132
+ bboxDelta,
133
+ symmetricDifferenceVolume,
134
+ errors
135
+ };
136
+ } catch (_) {
137
+ _usingCtx3.e = _;
138
+ } finally {
139
+ _usingCtx3.d();
140
+ }
141
+ }
142
+ //#endregion
143
+ export { runMeasure as n, runDiff as t };