brepjs 18.151.2 → 18.152.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/README.md CHANGED
@@ -60,6 +60,8 @@ To set expectations, this project deliberately does not:
60
60
  npm install brepjs occt-wasm
61
61
  ```
62
62
 
63
+ Starting fresh? `npm create brepjs my-project` scaffolds a TypeScript app with the kernel and the declarative family layer preconfigured.
64
+
63
65
  `brepjs/quick` handles WASM init automatically via top-level await (ESM only). Other options:
64
66
 
65
67
  ```typescript
@@ -84,6 +86,7 @@ The chapter-based guide is the recommended starting point:
84
86
  - **[Cheat Sheet](https://brepjs.dev/getting-started/cheat-sheet)**: single-page reference
85
87
  - **[Core Concepts](https://brepjs.dev/concepts/brep-vs-mesh)**: B-Rep, topology, types, kernels, tolerance
86
88
  - **[Common Tasks](https://brepjs.dev/tasks/booleans)**: booleans, fillets, sketching, lofts, sweeps, finders, measurement, IO
89
+ - **[Declarative Models](https://brepjs.dev/families/overview)**: React-like parametric components (brepjs-families), key-path identity, IFC export
87
90
  - **[Three.js Integration](https://brepjs.dev/integration/threejs)**: meshing and rendering
88
91
  - **[Migration](https://brepjs.dev/migration/replicad)**: coming from Replicad, OpenSCAD, or Three.js
89
92
  - **[Extending brepjs](https://brepjs.dev/extending/architecture)**: custom kernels, custom operations, architecture
@@ -139,6 +142,27 @@ npx -y -p brepjs-cad brep bracket.brep.ts --check --step bracket.step --json rep
139
142
 
140
143
  The command exits non-zero unless the report is `ok` (valid **and** every declared dimension within tolerance), so it drops straight into CI or an agent loop. For MCP-capable agents the package also ships a stdio MCP server (`brep-mcp`) exposing the same build-and-verify step as a `run_program` tool. See the [**Authoring with AI**](https://brepjs.dev/agent/overview) guide for the full loop, CLI reference, the MCP server, examples, and the measurement eval.
141
144
 
145
+ ## Declarative models and BIM (brepjs-families + brepjs-bim)
146
+
147
+ [`brepjs-families`](https://www.npmjs.com/package/brepjs-families) is a React-like component layer over the CSG IR: parametric parts are prop-driven, composable, zod-validated components, and identical subtrees materialize once because the IR is content-addressed. [`brepjs-bim`](https://www.npmjs.com/package/brepjs-bim) projects the same tree to IFC4 with stable GlobalIds derived from component key paths — walls, slabs, columns, beams, roofs, stairs, and real openings (`IfcOpeningElement` + `IfcRelFillsElement`).
148
+
149
+ ```typescript
150
+ const Wall = family('Wall', (p: WallProps) =>
151
+ el('Box', { size: [p.length, p.thickness, p.height], voids: p.voids ?? [] })
152
+ );
153
+ const wall = Wall({ key: 'south', length: 4000, thickness: 200, height: 2700 });
154
+ const tree = resolve(Storey({ key: 'ground', items: [wall] }));
155
+ evaluateModel(tree, evaluator); // viewport meshes, one per key path
156
+ familiesToBim(tree, { project }); // IFC via brepjs-bim
157
+ ```
158
+
159
+ ```bash
160
+ npm create brepjs my-building # scaffold a project
161
+ npx brepjs add room storey slab # copy in starter families (code you own)
162
+ ```
163
+
164
+ See the [**Declarative Models**](https://brepjs.dev/families/overview) guide.
165
+
142
166
  ## Projects Using brepjs
143
167
 
144
168
  - [Gridfinity Layout Tool](https://github.com/andymai/gridfinity-layout-tool): Web-based layout generator for Gridfinity storage systems
package/bin/brepjs.mjs CHANGED
@@ -19,14 +19,46 @@
19
19
  import { mkdir, open, readFile, writeFile, access, lstat, realpath, rename, rm } from 'node:fs/promises';
20
20
  import { constants } from 'node:fs';
21
21
  import { spawnSync } from 'node:child_process';
22
- import { join, resolve as resolvePath, sep } from 'node:path';
22
+ import { dirname, join, resolve as resolvePath, sep } from 'node:path';
23
23
  import process from 'node:process';
24
24
 
25
- const DEFAULT_REGISTRY =
26
- 'https://raw.githubusercontent.com/andymai/brepjs/main/packages/brepjs-families/registry';
25
+ // The default registry pins to the release tag of the INSTALLED
26
+ // brepjs-families, so copied sources match the package the project compiles
27
+ // against instead of tracking main (which can drift ahead). Without a local
28
+ // install, fall back to main.
29
+ const REGISTRY_PATH = 'packages/brepjs-families/registry';
30
+
31
+ async function installedFamiliesVersion() {
32
+ // Walk ancestor node_modules like Node's resolver, so a hoisted install
33
+ // (workspaces, monorepos) still pins the registry.
34
+ let dir = process.cwd();
35
+ for (;;) {
36
+ try {
37
+ const pkg = JSON.parse(
38
+ await readFile(join(dir, 'node_modules', 'brepjs-families', 'package.json'), 'utf8')
39
+ );
40
+ if (typeof pkg.version === 'string' && /^\d+\.\d+\.\d+$/.test(pkg.version)) {
41
+ return pkg.version;
42
+ }
43
+ } catch {
44
+ // keep walking
45
+ }
46
+ const parent = dirname(dir);
47
+ if (parent === dir) return null;
48
+ dir = parent;
49
+ }
50
+ }
51
+
52
+ async function defaultRegistry() {
53
+ const version = await installedFamiliesVersion();
54
+ if (version !== null) {
55
+ return `https://raw.githubusercontent.com/andymai/brepjs/brepjs-families-v${version}/${REGISTRY_PATH}`;
56
+ }
57
+ return `https://raw.githubusercontent.com/andymai/brepjs/main/${REGISTRY_PATH}`;
58
+ }
27
59
 
28
60
  function parseArgs(argv) {
29
- const args = { _: [], registry: DEFAULT_REGISTRY, dir: 'src/families', force: false, install: false };
61
+ const args = { _: [], registry: null, dir: 'src/families', force: false, install: false };
30
62
  for (let i = 0; i < argv.length; i++) {
31
63
  const a = argv[i];
32
64
  if (a === '--registry') args.registry = argv[++i];
@@ -295,6 +327,7 @@ async function diff(args) {
295
327
  async function main() {
296
328
  const [cmd, ...rest] = process.argv.slice(2);
297
329
  const args = parseArgs(rest);
330
+ args.registry = args.registry ?? (await defaultRegistry());
298
331
  if (cmd === 'add' && args._.length > 0) return add(args);
299
332
  if (cmd === 'diff' && args._.length === 1) return diff(args);
300
333
  console.error(
package/dist/brepjs.cjs CHANGED
@@ -4528,7 +4528,15 @@ function foldBuildVec(dim, comps) {
4528
4528
  nums[2]
4529
4529
  ]);
4530
4530
  }
4531
+ var optimized = /* @__PURE__ */ new WeakMap();
4531
4532
  function optimizeNode(n) {
4533
+ const cached = optimized.get(n);
4534
+ if (cached !== void 0) return cached;
4535
+ const out = rewriteNode(n);
4536
+ optimized.set(n, out);
4537
+ return out;
4538
+ }
4539
+ function rewriteNode(n) {
4532
4540
  switch (n.kind) {
4533
4541
  case "Box": return box$2(foldExpr(n.x), foldExpr(n.y), foldExpr(n.z));
4534
4542
  case "Sphere": return sphere$2(foldExpr(n.radius));
@@ -4646,13 +4654,16 @@ function optimizeTranslate(target, vector) {
4646
4654
  //#endregion
4647
4655
  //#region src/csg/edit.ts
4648
4656
  function replaceNode(root, pred, replacement) {
4649
- return walk(root, pred, replacement);
4657
+ return walk(root, pred, replacement, /* @__PURE__ */ new Map());
4650
4658
  }
4651
- function walk(node, pred, repl) {
4652
- if (pred(node)) return repl;
4653
- return rebuildChildren(node, pred, repl);
4659
+ function walk(node, pred, repl, memo) {
4660
+ const cached = memo.get(node);
4661
+ if (cached !== void 0) return cached;
4662
+ const out = pred(node) ? repl : rebuildChildren(node, pred, repl, memo);
4663
+ memo.set(node, out);
4664
+ return out;
4654
4665
  }
4655
- function rebuildChildren(n, pred, repl) {
4666
+ function rebuildChildren(n, pred, repl, memo) {
4656
4667
  switch (n.kind) {
4657
4668
  case "Box":
4658
4669
  case "Sphere":
@@ -4666,23 +4677,23 @@ function rebuildChildren(n, pred, repl) {
4666
4677
  case "Empty":
4667
4678
  case "Path":
4668
4679
  case "Profile": return n;
4669
- case "Fuse": return fuse$1(walk(n.a, pred, repl), walk(n.b, pred, repl), n.tolerance);
4670
- case "Cut": return cut$1(walk(n.a, pred, repl), walk(n.b, pred, repl), n.tolerance);
4671
- case "Intersect": return intersect$1(walk(n.a, pred, repl), walk(n.b, pred, repl), n.tolerance);
4672
- case "FuseAll": return fuseAll$1(n.shapes.map((c) => walk(c, pred, repl)), n.tolerance);
4673
- case "CutAll": return cutAll$1(walk(n.base, pred, repl), n.tools.map((c) => walk(c, pred, repl)), n.tolerance);
4674
- case "Translate": return translate$1(walk(n.target, pred, repl), n.vector);
4675
- case "Rotate": return rotate$1(walk(n.target, pred, repl), n.angle, {
4680
+ case "Fuse": return fuse$1(walk(n.a, pred, repl, memo), walk(n.b, pred, repl, memo), n.tolerance);
4681
+ case "Cut": return cut$1(walk(n.a, pred, repl, memo), walk(n.b, pred, repl, memo), n.tolerance);
4682
+ case "Intersect": return intersect$1(walk(n.a, pred, repl, memo), walk(n.b, pred, repl, memo), n.tolerance);
4683
+ case "FuseAll": return fuseAll$1(n.shapes.map((c) => walk(c, pred, repl, memo)), n.tolerance);
4684
+ case "CutAll": return cutAll$1(walk(n.base, pred, repl, memo), n.tools.map((c) => walk(c, pred, repl, memo)), n.tolerance);
4685
+ case "Translate": return translate$1(walk(n.target, pred, repl, memo), n.vector);
4686
+ case "Rotate": return rotate$1(walk(n.target, pred, repl, memo), n.angle, {
4676
4687
  axis: n.axis,
4677
4688
  at: n.at
4678
4689
  });
4679
- case "Scale": return scale$2(walk(n.target, pred, repl), n.factor, { center: n.center });
4680
- case "Mirror": return mirror$1(walk(n.target, pred, repl), {
4690
+ case "Scale": return scale$2(walk(n.target, pred, repl, memo), n.factor, { center: n.center });
4691
+ case "Mirror": return mirror$1(walk(n.target, pred, repl, memo), {
4681
4692
  normal: n.normal,
4682
4693
  at: n.at
4683
4694
  });
4684
- case "Compound": return compound$1(n.children.map((c) => walk(c, pred, repl)));
4685
- case "Instance": return instance$1(walk(n.source, pred, repl), n.placements, n.fuse);
4695
+ case "Compound": return compound$1(n.children.map((c) => walk(c, pred, repl, memo)));
4696
+ case "Instance": return instance$1(walk(n.source, pred, repl, memo), n.placements, n.fuse);
4686
4697
  case "Extrude":
4687
4698
  case "Revolve":
4688
4699
  case "Loft":
@@ -4690,27 +4701,35 @@ function rebuildChildren(n, pred, repl) {
4690
4701
  case "Color":
4691
4702
  case "Fillet":
4692
4703
  case "Chamfer":
4693
- case "Shell": return rebuildFeature(n, pred, repl);
4704
+ case "Shell": return rebuildFeature(n, pred, repl, memo);
4694
4705
  }
4695
4706
  }
4696
- function rebuildFeature(n, pred, repl) {
4707
+ function rebuildFeature(n, pred, repl, memo) {
4697
4708
  switch (n.kind) {
4698
- case "Extrude": return extrude$1(walk(n.profile, pred, repl), n.vector);
4699
- case "Revolve": return revolve$1(walk(n.profile, pred, repl), n.angle, {
4709
+ case "Extrude": return extrude$1(walk(n.profile, pred, repl, memo), n.vector);
4710
+ case "Revolve": return revolve$1(walk(n.profile, pred, repl, memo), n.angle, {
4700
4711
  axis: n.axis,
4701
4712
  at: n.at
4702
4713
  });
4703
- case "Loft": return loft$1(n.sections.map((s) => walk(s, pred, repl)), { ruled: n.ruled });
4704
- case "Sweep": return sweep$2(walk(n.profile, pred, repl), walk(n.spine, pred, repl), { frenet: n.frenet });
4705
- case "Color": return color(walk(n.target, pred, repl), [...n.color]);
4706
- case "Fillet": return fillet$1(walk(n.target, pred, repl), n.ref, n.radius);
4707
- case "Chamfer": return chamfer$1(walk(n.target, pred, repl), n.ref, n.distance);
4708
- case "Shell": return shell$1(walk(n.target, pred, repl), n.refs, n.thickness);
4714
+ case "Loft": return loft$1(n.sections.map((s) => walk(s, pred, repl, memo)), { ruled: n.ruled });
4715
+ case "Sweep": return sweep$2(walk(n.profile, pred, repl, memo), walk(n.spine, pred, repl, memo), { frenet: n.frenet });
4716
+ case "Color": return color(walk(n.target, pred, repl, memo), [...n.color]);
4717
+ case "Fillet": return fillet$1(walk(n.target, pred, repl, memo), n.ref, n.radius);
4718
+ case "Chamfer": return chamfer$1(walk(n.target, pred, repl, memo), n.ref, n.distance);
4719
+ case "Shell": return shell$1(walk(n.target, pred, repl, memo), n.refs, n.thickness);
4709
4720
  }
4710
4721
  }
4722
+ /** Visits each DISTINCT node once (identity-deduplicated), so a shared
4723
+ * subtree is reported once and a self-referencing DAG walks in linear time. */
4711
4724
  function forEachNode(root, fn) {
4712
- fn(root);
4713
- for (const child of childrenOf(root)) forEachNode(child, fn);
4725
+ const seen = /* @__PURE__ */ new Set();
4726
+ const visit = (node) => {
4727
+ if (seen.has(node)) return;
4728
+ seen.add(node);
4729
+ fn(node);
4730
+ for (const child of childrenOf(node)) visit(child);
4731
+ };
4732
+ visit(root);
4714
4733
  }
4715
4734
  function childrenOf(n) {
4716
4735
  switch (n.kind) {
package/dist/brepjs.js CHANGED
@@ -4538,7 +4538,15 @@ function foldBuildVec(dim, comps) {
4538
4538
  nums[2]
4539
4539
  ]);
4540
4540
  }
4541
+ var optimized = /* @__PURE__ */ new WeakMap();
4541
4542
  function optimizeNode(n) {
4543
+ const cached = optimized.get(n);
4544
+ if (cached !== void 0) return cached;
4545
+ const out = rewriteNode(n);
4546
+ optimized.set(n, out);
4547
+ return out;
4548
+ }
4549
+ function rewriteNode(n) {
4542
4550
  switch (n.kind) {
4543
4551
  case "Box": return box$2(foldExpr(n.x), foldExpr(n.y), foldExpr(n.z));
4544
4552
  case "Sphere": return sphere$2(foldExpr(n.radius));
@@ -4656,13 +4664,16 @@ function optimizeTranslate(target, vector) {
4656
4664
  //#endregion
4657
4665
  //#region src/csg/edit.ts
4658
4666
  function replaceNode(root, pred, replacement) {
4659
- return walk(root, pred, replacement);
4667
+ return walk(root, pred, replacement, /* @__PURE__ */ new Map());
4660
4668
  }
4661
- function walk(node, pred, repl) {
4662
- if (pred(node)) return repl;
4663
- return rebuildChildren(node, pred, repl);
4669
+ function walk(node, pred, repl, memo) {
4670
+ const cached = memo.get(node);
4671
+ if (cached !== void 0) return cached;
4672
+ const out = pred(node) ? repl : rebuildChildren(node, pred, repl, memo);
4673
+ memo.set(node, out);
4674
+ return out;
4664
4675
  }
4665
- function rebuildChildren(n, pred, repl) {
4676
+ function rebuildChildren(n, pred, repl, memo) {
4666
4677
  switch (n.kind) {
4667
4678
  case "Box":
4668
4679
  case "Sphere":
@@ -4676,23 +4687,23 @@ function rebuildChildren(n, pred, repl) {
4676
4687
  case "Empty":
4677
4688
  case "Path":
4678
4689
  case "Profile": return n;
4679
- case "Fuse": return fuse$1(walk(n.a, pred, repl), walk(n.b, pred, repl), n.tolerance);
4680
- case "Cut": return cut$1(walk(n.a, pred, repl), walk(n.b, pred, repl), n.tolerance);
4681
- case "Intersect": return intersect$1(walk(n.a, pred, repl), walk(n.b, pred, repl), n.tolerance);
4682
- case "FuseAll": return fuseAll$1(n.shapes.map((c) => walk(c, pred, repl)), n.tolerance);
4683
- case "CutAll": return cutAll$1(walk(n.base, pred, repl), n.tools.map((c) => walk(c, pred, repl)), n.tolerance);
4684
- case "Translate": return translate$1(walk(n.target, pred, repl), n.vector);
4685
- case "Rotate": return rotate$1(walk(n.target, pred, repl), n.angle, {
4690
+ case "Fuse": return fuse$1(walk(n.a, pred, repl, memo), walk(n.b, pred, repl, memo), n.tolerance);
4691
+ case "Cut": return cut$1(walk(n.a, pred, repl, memo), walk(n.b, pred, repl, memo), n.tolerance);
4692
+ case "Intersect": return intersect$1(walk(n.a, pred, repl, memo), walk(n.b, pred, repl, memo), n.tolerance);
4693
+ case "FuseAll": return fuseAll$1(n.shapes.map((c) => walk(c, pred, repl, memo)), n.tolerance);
4694
+ case "CutAll": return cutAll$1(walk(n.base, pred, repl, memo), n.tools.map((c) => walk(c, pred, repl, memo)), n.tolerance);
4695
+ case "Translate": return translate$1(walk(n.target, pred, repl, memo), n.vector);
4696
+ case "Rotate": return rotate$1(walk(n.target, pred, repl, memo), n.angle, {
4686
4697
  axis: n.axis,
4687
4698
  at: n.at
4688
4699
  });
4689
- case "Scale": return scale$2(walk(n.target, pred, repl), n.factor, { center: n.center });
4690
- case "Mirror": return mirror$1(walk(n.target, pred, repl), {
4700
+ case "Scale": return scale$2(walk(n.target, pred, repl, memo), n.factor, { center: n.center });
4701
+ case "Mirror": return mirror$1(walk(n.target, pred, repl, memo), {
4691
4702
  normal: n.normal,
4692
4703
  at: n.at
4693
4704
  });
4694
- case "Compound": return compound$1(n.children.map((c) => walk(c, pred, repl)));
4695
- case "Instance": return instance$1(walk(n.source, pred, repl), n.placements, n.fuse);
4705
+ case "Compound": return compound$1(n.children.map((c) => walk(c, pred, repl, memo)));
4706
+ case "Instance": return instance$1(walk(n.source, pred, repl, memo), n.placements, n.fuse);
4696
4707
  case "Extrude":
4697
4708
  case "Revolve":
4698
4709
  case "Loft":
@@ -4700,27 +4711,35 @@ function rebuildChildren(n, pred, repl) {
4700
4711
  case "Color":
4701
4712
  case "Fillet":
4702
4713
  case "Chamfer":
4703
- case "Shell": return rebuildFeature(n, pred, repl);
4714
+ case "Shell": return rebuildFeature(n, pred, repl, memo);
4704
4715
  }
4705
4716
  }
4706
- function rebuildFeature(n, pred, repl) {
4717
+ function rebuildFeature(n, pred, repl, memo) {
4707
4718
  switch (n.kind) {
4708
- case "Extrude": return extrude$1(walk(n.profile, pred, repl), n.vector);
4709
- case "Revolve": return revolve$1(walk(n.profile, pred, repl), n.angle, {
4719
+ case "Extrude": return extrude$1(walk(n.profile, pred, repl, memo), n.vector);
4720
+ case "Revolve": return revolve$1(walk(n.profile, pred, repl, memo), n.angle, {
4710
4721
  axis: n.axis,
4711
4722
  at: n.at
4712
4723
  });
4713
- case "Loft": return loft$1(n.sections.map((s) => walk(s, pred, repl)), { ruled: n.ruled });
4714
- case "Sweep": return sweep$2(walk(n.profile, pred, repl), walk(n.spine, pred, repl), { frenet: n.frenet });
4715
- case "Color": return color(walk(n.target, pred, repl), [...n.color]);
4716
- case "Fillet": return fillet$1(walk(n.target, pred, repl), n.ref, n.radius);
4717
- case "Chamfer": return chamfer$1(walk(n.target, pred, repl), n.ref, n.distance);
4718
- case "Shell": return shell$1(walk(n.target, pred, repl), n.refs, n.thickness);
4724
+ case "Loft": return loft$1(n.sections.map((s) => walk(s, pred, repl, memo)), { ruled: n.ruled });
4725
+ case "Sweep": return sweep$2(walk(n.profile, pred, repl, memo), walk(n.spine, pred, repl, memo), { frenet: n.frenet });
4726
+ case "Color": return color(walk(n.target, pred, repl, memo), [...n.color]);
4727
+ case "Fillet": return fillet$1(walk(n.target, pred, repl, memo), n.ref, n.radius);
4728
+ case "Chamfer": return chamfer$1(walk(n.target, pred, repl, memo), n.ref, n.distance);
4729
+ case "Shell": return shell$1(walk(n.target, pred, repl, memo), n.refs, n.thickness);
4719
4730
  }
4720
4731
  }
4732
+ /** Visits each DISTINCT node once (identity-deduplicated), so a shared
4733
+ * subtree is reported once and a self-referencing DAG walks in linear time. */
4721
4734
  function forEachNode(root, fn) {
4722
- fn(root);
4723
- for (const child of childrenOf(root)) forEachNode(child, fn);
4735
+ const seen = /* @__PURE__ */ new Set();
4736
+ const visit = (node) => {
4737
+ if (seen.has(node)) return;
4738
+ seen.add(node);
4739
+ fn(node);
4740
+ for (const child of childrenOf(node)) visit(child);
4741
+ };
4742
+ visit(root);
4724
4743
  }
4725
4744
  function childrenOf(n) {
4726
4745
  switch (n.kind) {
@@ -1,5 +1,7 @@
1
1
  import { IRNode } from './types.js';
2
2
  export type NodePredicate = (node: IRNode) => boolean;
3
3
  export declare function replaceNode(root: IRNode, pred: NodePredicate, replacement: IRNode): IRNode;
4
+ /** Visits each DISTINCT node once (identity-deduplicated), so a shared
5
+ * subtree is reported once and a self-referencing DAG walks in linear time. */
4
6
  export declare function forEachNode(root: IRNode, fn: (node: IRNode) => void): void;
5
7
  export declare function nodeCount(root: IRNode): number;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "brepjs",
3
- "version": "18.151.2",
3
+ "version": "18.152.0",
4
4
  "description": "Web CAD library with pluggable geometry kernel",
5
5
  "keywords": [
6
6
  "cad",