brepjs-bim 0.1.0 → 0.2.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
@@ -1,6 +1,10 @@
1
1
  # brepjs-bim
2
2
 
3
- > Experimental, unpublished satellite package.
3
+ > Experimental satellite package, published to npm. Early-stage — the API may change.
4
+
5
+ ```bash
6
+ npm install brepjs-bim
7
+ ```
4
8
 
5
9
  A BIM (Building Information Modeling) layer for [brepjs](https://github.com/andymai/brepjs). It
6
10
  authors IFC4-aligned parametric building elements (walls, slabs, beams, columns, roofs, curtain
@@ -37,6 +41,18 @@ coordinates — placement (`origin` / `axisX` / `axisZ`) is applied by the IFC l
37
41
  | Validation | referential integrity, schema check, geometry validity, IFC round-trip report |
38
42
  | Interop | COBie 2.4 export (CSV/JSON), IDS 1.0 checking, BCF 3.0 read/write |
39
43
 
44
+ ### Independent validation
45
+
46
+ The exported IFC is validated by **IfcOpenShell** (a separate implementation from the
47
+ web-ifc parser used internally), not just self-checked. The committed sample
48
+ (`examples/sample-building.ifc`) passes IfcOpenShell's EXPRESS schema + where-rule
49
+ validator and generates geometry for every product. See [VALIDATION.md](./VALIDATION.md)
50
+ to reproduce, and `examples/sampleBuilding.mjs` for the model it validates.
51
+
52
+ > **Not yet:** the official buildingSMART Validation Service and desktop-tool
53
+ > interop (Revit / ArchiCAD / Solibri) are unverified. This is an early-stage
54
+ > (`0.1.x`) experimental package and the API will change.
55
+
40
56
  ## Usage
41
57
 
42
58
  Author a small model and export IFC:
@@ -24,8 +24,37 @@ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__ge
24
24
  let brepjs = require("brepjs");
25
25
  let web_ifc = require("web-ifc");
26
26
  web_ifc = __toESM(web_ifc, 1);
27
+ //#region src/identity/ifcGuid.ts
28
+ var IFC_CHARS = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz_$";
29
+ function newIfcGuid() {
30
+ const bytes = crypto.getRandomValues(new Uint8Array(16));
31
+ bytes[6] = (bytes[6] ?? 0) & 15 | 64;
32
+ bytes[8] = (bytes[8] ?? 0) & 63 | 128;
33
+ return encodeIfcGuid(bytes);
34
+ }
35
+ function isValidIfcGuid(s) {
36
+ if (s.length !== 22) return false;
37
+ if (!"0123".includes(s[0] ?? "")) return false;
38
+ for (const ch of s) if (!IFC_CHARS.includes(ch)) return false;
39
+ return true;
40
+ }
41
+ function encodeIfcGuid(bytes) {
42
+ let result = "";
43
+ let acc = 0;
44
+ let bits = 4;
45
+ for (const byte of bytes) {
46
+ acc = acc << 8 | byte;
47
+ bits += 8;
48
+ while (bits >= 6) {
49
+ bits -= 6;
50
+ result += IFC_CHARS[acc >> bits & 63] ?? "";
51
+ }
52
+ }
53
+ if (bits > 0) result += IFC_CHARS[acc << 6 - bits & 63] ?? "";
54
+ return result;
55
+ }
56
+ //#endregion
27
57
  //#region src/identity/guidDerivation.ts
28
- var IFC_CHARS$1 = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz_$";
29
58
  var NAMESPACE = "brepjs-bim:v1";
30
59
  var FNV_OFFSET = 2166136261;
31
60
  var FNV_PRIME = 16777619;
@@ -59,7 +88,7 @@ function digest16(stableKey) {
59
88
  * keys yield distinct, format-valid (22-char) GlobalIds.
60
89
  */
61
90
  function deriveIfcGuidSync(stableKey) {
62
- return encodeIfcGuid$1(digest16(stableKey));
91
+ return encodeIfcGuid(digest16(stableKey));
63
92
  }
64
93
  /**
65
94
  * Async wrapper over {@link deriveIfcGuidSync} for callers that prefer a Promise
@@ -86,21 +115,6 @@ function makeRelKey(modelScope, kind, localId) {
86
115
  function makeLineKey(modelScope, expressId) {
87
116
  return `line:${modelScope}:${expressId}`;
88
117
  }
89
- function encodeIfcGuid$1(bytes) {
90
- let result = "";
91
- let acc = 0;
92
- let bits = 0;
93
- for (const byte of bytes) {
94
- acc = acc << 8 | byte;
95
- bits += 8;
96
- while (bits >= 6) {
97
- bits -= 6;
98
- result += IFC_CHARS$1[acc >> bits & 63] ?? "";
99
- }
100
- }
101
- if (bits > 0) result += IFC_CHARS$1[acc << 6 - bits & 63] ?? "";
102
- return result;
103
- }
104
118
  //#endregion
105
119
  //#region src/identity/localId.ts
106
120
  function makeLocalIdCounter(start = 1) {
@@ -5093,7 +5107,8 @@ var CoreProfileSchema = discriminatedUnion("kind", [
5093
5107
  overallWidth: number().positive(),
5094
5108
  overallDepth: number().positive(),
5095
5109
  flangeThickness: number().positive(),
5096
- webThickness: number().positive()
5110
+ webThickness: number().positive(),
5111
+ filletRadius: number().positive().optional()
5097
5112
  })
5098
5113
  ]);
5099
5114
  var Pt2Schema = tuple([number(), number()]);
@@ -5225,6 +5240,11 @@ function parseProfile(input) {
5225
5240
  if (profile.kind === "I_BEAM") {
5226
5241
  if (2 * profile.flangeThickness >= profile.overallDepth) return (0, brepjs.err)(specError("INVALID_PROFILE", "I-beam flangeThickness × 2 must be less than overallDepth"));
5227
5242
  if (profile.webThickness >= profile.overallWidth) return (0, brepjs.err)(specError("INVALID_PROFILE", "I-beam webThickness must be less than overallWidth"));
5243
+ if (profile.filletRadius !== void 0) {
5244
+ const clearHeight = profile.overallDepth / 2 - profile.flangeThickness;
5245
+ const clearSpan = (profile.overallWidth - profile.webThickness) / 2;
5246
+ if (profile.filletRadius >= clearHeight || profile.filletRadius >= clearSpan) return (0, brepjs.err)(specError("INVALID_PROFILE", "I-beam filletRadius must be smaller than the clear web height and the clear span beside the web"));
5247
+ }
5228
5248
  }
5229
5249
  return (0, brepjs.ok)(profile);
5230
5250
  }
@@ -5235,8 +5255,42 @@ function profileCrossSectionArea(profile) {
5235
5255
  switch (profile.kind) {
5236
5256
  case "RECTANGULAR": return profile.width * profile.height;
5237
5257
  case "CIRCULAR": return Math.PI * profile.radius * profile.radius;
5238
- case "I_BEAM": return 2 * profile.overallWidth * profile.flangeThickness + (profile.overallDepth - 2 * profile.flangeThickness) * profile.webThickness;
5258
+ case "I_BEAM": {
5259
+ const flangeArea = 2 * profile.overallWidth * profile.flangeThickness;
5260
+ const webArea = (profile.overallDepth - 2 * profile.flangeThickness) * profile.webThickness;
5261
+ const r = profile.filletRadius ?? 0;
5262
+ const filletArea = 4 * r * r * (1 - Math.PI / 4);
5263
+ return flangeArea + webArea + filletArea;
5264
+ }
5265
+ }
5266
+ }
5267
+ var FILLET_SEGMENTS = 8;
5268
+ var FILLET_MIN_ANGLE = .001;
5269
+ function filletArc(prev, v, next, r) {
5270
+ const aLen = Math.hypot(prev[0] - v[0], prev[1] - v[1]);
5271
+ const bLen = Math.hypot(next[0] - v[0], next[1] - v[1]);
5272
+ const a = [(prev[0] - v[0]) / aLen, (prev[1] - v[1]) / aLen];
5273
+ const b = [(next[0] - v[0]) / bLen, (next[1] - v[1]) / bLen];
5274
+ const alpha = Math.acos(Math.max(-1, Math.min(1, a[0] * b[0] + a[1] * b[1])));
5275
+ if (alpha < FILLET_MIN_ANGLE || alpha > Math.PI - FILLET_MIN_ANGLE) return [[v[0], v[1]]];
5276
+ const setback = r / Math.tan(alpha / 2);
5277
+ const center = r / Math.sin(alpha / 2);
5278
+ const bisLen = Math.hypot(a[0] + b[0], a[1] + b[1]);
5279
+ const bis = [(a[0] + b[0]) / bisLen, (a[1] + b[1]) / bisLen];
5280
+ const cx = v[0] + bis[0] * center;
5281
+ const cy = v[1] + bis[1] * center;
5282
+ const p1 = [v[0] + a[0] * setback, v[1] + a[1] * setback];
5283
+ const p2 = [v[0] + b[0] * setback, v[1] + b[1] * setback];
5284
+ const a1 = Math.atan2(p1[1] - cy, p1[0] - cx);
5285
+ let delta = Math.atan2(p2[1] - cy, p2[0] - cx) - a1;
5286
+ while (delta <= -Math.PI) delta += 2 * Math.PI;
5287
+ while (delta > Math.PI) delta -= 2 * Math.PI;
5288
+ const out = [];
5289
+ for (let i = 0; i <= FILLET_SEGMENTS; i++) {
5290
+ const ang = a1 + delta * i / FILLET_SEGMENTS;
5291
+ out.push([cx + r * Math.cos(ang), cy + r * Math.sin(ang)]);
5239
5292
  }
5293
+ return out;
5240
5294
  }
5241
5295
  function profileToPolygon(profile, circleSegments = 32) {
5242
5296
  if (isExtendedProfile(profile)) return (0, brepjs.err)(specError("EXTENDED_PROFILE_NO_POLYGON", `profileToPolygon: extended profile kind '${profile.kind}' has no single-polygon outline; use extendedProfileToFace()`));
@@ -5285,68 +5339,50 @@ function profileToPolygon(profile, circleSegments = 32) {
5285
5339
  const halfD = profile.overallDepth / 2;
5286
5340
  const halfWeb = profile.webThickness / 2;
5287
5341
  const flangeInnerY = halfD - profile.flangeThickness;
5288
- return (0, brepjs.ok)([
5289
- [
5290
- -halfW,
5291
- -halfD,
5292
- 0
5293
- ],
5294
- [
5295
- halfW,
5296
- -halfD,
5297
- 0
5298
- ],
5299
- [
5300
- halfW,
5301
- -flangeInnerY,
5302
- 0
5303
- ],
5304
- [
5305
- halfWeb,
5306
- -flangeInnerY,
5307
- 0
5308
- ],
5309
- [
5310
- halfWeb,
5311
- flangeInnerY,
5312
- 0
5313
- ],
5314
- [
5315
- halfW,
5316
- flangeInnerY,
5317
- 0
5318
- ],
5319
- [
5320
- halfW,
5321
- halfD,
5322
- 0
5323
- ],
5324
- [
5325
- -halfW,
5326
- halfD,
5327
- 0
5328
- ],
5329
- [
5330
- -halfW,
5331
- flangeInnerY,
5332
- 0
5333
- ],
5334
- [
5335
- -halfWeb,
5336
- flangeInnerY,
5337
- 0
5338
- ],
5339
- [
5340
- -halfWeb,
5341
- -flangeInnerY,
5342
- 0
5343
- ],
5344
- [
5345
- -halfW,
5346
- -flangeInnerY,
5347
- 0
5348
- ]
5342
+ const v = [
5343
+ [-halfW, -halfD],
5344
+ [halfW, -halfD],
5345
+ [halfW, -flangeInnerY],
5346
+ [halfWeb, -flangeInnerY],
5347
+ [halfWeb, flangeInnerY],
5348
+ [halfW, flangeInnerY],
5349
+ [halfW, halfD],
5350
+ [-halfW, halfD],
5351
+ [-halfW, flangeInnerY],
5352
+ [-halfWeb, flangeInnerY],
5353
+ [-halfWeb, -flangeInnerY],
5354
+ [-halfW, -flangeInnerY]
5355
+ ];
5356
+ const r = profile.filletRadius ?? 0;
5357
+ const rootCorners = new Set([
5358
+ 3,
5359
+ 4,
5360
+ 9,
5361
+ 10
5349
5362
  ]);
5363
+ const pts = [];
5364
+ for (let i = 0; i < v.length; i++) {
5365
+ const cur = v[i];
5366
+ if (cur === void 0) continue;
5367
+ if (r > 0 && rootCorners.has(i)) {
5368
+ const prev = v[(i - 1 + v.length) % v.length];
5369
+ const next = v[(i + 1) % v.length];
5370
+ if (prev !== void 0 && next !== void 0) {
5371
+ for (const [ax, ay] of filletArc(prev, cur, next, r)) pts.push([
5372
+ ax,
5373
+ ay,
5374
+ 0
5375
+ ]);
5376
+ continue;
5377
+ }
5378
+ }
5379
+ pts.push([
5380
+ cur[0],
5381
+ cur[1],
5382
+ 0
5383
+ ]);
5384
+ }
5385
+ return (0, brepjs.ok)(pts);
5350
5386
  }
5351
5387
  }
5352
5388
  }
@@ -6530,6 +6566,50 @@ var BimModel = class {
6530
6566
  getElement(id) {
6531
6567
  return this.#elements.get(id) ?? null;
6532
6568
  }
6569
+ /**
6570
+ * A serializable summary of the model's structure, rooted at the project and
6571
+ * walking the IFC spatial hierarchy (AGGREGATES: project → site → building →
6572
+ * storey) plus the elements contained in each storey (placeIn). Useful for a
6573
+ * read-only tree view of the model across a worker boundary.
6574
+ */
6575
+ toTreeSummary() {
6576
+ const aggregated = /* @__PURE__ */ new Map();
6577
+ const contained = /* @__PURE__ */ new Map();
6578
+ for (const rel of this.#relationships.values()) if (rel.kind === "AGGREGATES") {
6579
+ const list = aggregated.get(rel.relatingObject) ?? [];
6580
+ list.push(...rel.relatedObjects);
6581
+ aggregated.set(rel.relatingObject, list);
6582
+ } else if (rel.kind === "CONTAINED_IN") {
6583
+ const list = contained.get(rel.relatingStructure) ?? [];
6584
+ list.push(...rel.relatedElements);
6585
+ contained.set(rel.relatingStructure, list);
6586
+ }
6587
+ const labelFor = (el) => {
6588
+ const spec = el.spec;
6589
+ const base = typeof spec.name === "string" && spec.name.length > 0 ? spec.name : el.category;
6590
+ return el.category === "STOREY" && typeof spec.elevation === "number" ? `${base} (+${spec.elevation} mm)` : base;
6591
+ };
6592
+ const seen = /* @__PURE__ */ new Set();
6593
+ const build = (id) => {
6594
+ if (seen.has(id)) return null;
6595
+ seen.add(id);
6596
+ const el = this.#elements.get(id);
6597
+ if (el === void 0) return null;
6598
+ const children = [...aggregated.get(id) ?? [], ...contained.get(id) ?? []].map(build).filter((n) => n !== null);
6599
+ return {
6600
+ id,
6601
+ label: labelFor(el),
6602
+ category: el.category,
6603
+ children
6604
+ };
6605
+ };
6606
+ const root = this.#projectId !== null ? build(this.#projectId) : null;
6607
+ const countNodes = (node) => 1 + node.children.reduce((sum, c) => sum + countNodes(c), 0);
6608
+ return {
6609
+ root,
6610
+ elementCount: root ? countNodes(root) : 0
6611
+ };
6612
+ }
6533
6613
  getWalls() {
6534
6614
  const walls = [];
6535
6615
  for (const el of this.#elements.values()) if (el.category === "WALL") walls.push(el);
@@ -6707,27 +6787,62 @@ function schemaSupports(schema, entityName) {
6707
6787
  return true;
6708
6788
  }
6709
6789
  //#endregion
6790
+ //#region src/ifcRuntime.ts
6791
+ var wasmLocateFile;
6792
+ /**
6793
+ * Override how web-ifc finds its `.wasm` file. Applied by every web-ifc entry
6794
+ * point in this package — IFC export ({@link toIfc}), import ({@link fromIfc})
6795
+ * and validation. Required when brepjs-bim is bundled into a worker that serves
6796
+ * the wasm itself; not needed in Node.
6797
+ */
6798
+ function setIfcWasmLocateFile(locate) {
6799
+ wasmLocateFile = locate;
6800
+ }
6801
+ /**
6802
+ * Initialize a web-ifc API instance the way this package always wants it: with
6803
+ * the host-provided wasm locator and forced single-threaded.
6804
+ *
6805
+ * Single-threaded matters in a cross-origin-isolated context (e.g. a page that
6806
+ * sets COOP/COEP for another WASM kernel): web-ifc would otherwise load its
6807
+ * pthread build and spawn a sub-Worker, which fails when brepjs-bim is itself
6808
+ * bundled inside a Web Worker. In Node the flag is a no-op (web-ifc is already
6809
+ * single-threaded there), and multithreading only speeds up parsing/geometry,
6810
+ * not the one-shot serialize/read this package does.
6811
+ */
6812
+ async function initIfcApi(api) {
6813
+ await api.Init(wasmLocateFile, true);
6814
+ }
6815
+ //#endregion
6710
6816
  //#region src/ifc-writer/ifcWriter.ts
6711
6817
  /** Default MVD ViewDefinition declared in the STEP FILE_DESCRIPTION header. */
6712
6818
  var DEFAULT_MVD_VIEW_DEFINITION = "ReferenceView_v1.2";
6713
6819
  var VIEW_DEFINITION_RE = /ViewDefinition \[[^\]]*\]/;
6820
+ var FILE_NAME_RE = /(FILE_NAME\('[^']*','[^']*',)(?:\$|\(\$\)),(?:\$|\(\$\)),('[^']*','[^']*'),\$\)/;
6821
+ /** STEP single-quoted string literal with embedded quotes doubled per ISO 10303-21. */
6822
+ function stepString(value) {
6823
+ return `'${value.replace(/'/g, "''")}'`;
6824
+ }
6714
6825
  var IfcWriter = class IfcWriter {
6715
6826
  #api;
6716
6827
  #modelId;
6717
6828
  #mvdViewDefinition;
6829
+ #author;
6830
+ #organization;
6718
6831
  #nextExpressId = 1;
6719
6832
  #closed = false;
6720
6833
  #modelScope = "";
6721
- constructor(api, modelId, mvdViewDefinition) {
6834
+ constructor(api, modelId, mvdViewDefinition, header) {
6722
6835
  this.#api = api;
6723
6836
  this.#modelId = modelId;
6724
6837
  this.#mvdViewDefinition = mvdViewDefinition;
6838
+ this.#author = header.author ?? "";
6839
+ this.#organization = header.organization ?? "";
6725
6840
  }
6726
- static async create(mvdViewDefinition = DEFAULT_MVD_VIEW_DEFINITION, ifcSchema = DEFAULT_IFC_SCHEMA) {
6841
+ static async create(mvdViewDefinition = DEFAULT_MVD_VIEW_DEFINITION, ifcSchema = DEFAULT_IFC_SCHEMA, header = {}) {
6727
6842
  try {
6728
6843
  const api = new web_ifc.IfcAPI();
6729
- await api.Init();
6730
- return (0, brepjs.ok)(new IfcWriter(api, api.CreateModel({ schema: fileSchemaString(ifcSchema) }), mvdViewDefinition));
6844
+ await initIfcApi(api);
6845
+ return (0, brepjs.ok)(new IfcWriter(api, api.CreateModel({ schema: fileSchemaString(ifcSchema) }), mvdViewDefinition, header));
6731
6846
  } catch (e) {
6732
6847
  return (0, brepjs.err)(ifcError("IFC_INIT_FAILED", "Failed to initialize web-ifc", e));
6733
6848
  }
@@ -6761,7 +6876,7 @@ var IfcWriter = class IfcWriter {
6761
6876
  if (this.#closed) return (0, brepjs.err)(ifcError("IFC_ALREADY_SAVED", "Model has already been saved and closed"));
6762
6877
  try {
6763
6878
  const bytes = this.#api.SaveModel(this.#modelId);
6764
- return (0, brepjs.ok)(this.#patchMvd(bytes));
6879
+ return (0, brepjs.ok)(this.#patchHeader(bytes));
6765
6880
  } catch (e) {
6766
6881
  return (0, brepjs.err)(ifcError("IFC_SAVE_FAILED", "Failed to serialize IFC model", e));
6767
6882
  } finally {
@@ -6770,21 +6885,20 @@ var IfcWriter = class IfcWriter {
6770
6885
  }
6771
6886
  }
6772
6887
  /**
6773
- * Injects the declared MVD into the STEP FILE_DESCRIPTION header. web-ifc does
6774
- * not expose the header's ViewDefinition for configuration, so we rewrite the
6775
- * empty default in the ASCII header region. If the expected pattern is absent
6776
- * (e.g. a future web-ifc default change) the bytes are returned unchanged.
6888
+ * Rewrites the STEP header in the ASCII region web-ifc emits: declares the MVD
6889
+ * in FILE_DESCRIPTION and makes FILE_NAME's author/organization/authorization
6890
+ * spec-conformant (web-ifc leaves them as bare `$`). web-ifc exposes neither
6891
+ * for configuration. If an expected pattern is absent (e.g. a future web-ifc
6892
+ * default change) that part is skipped and the bytes returned unchanged.
6777
6893
  */
6778
- #patchMvd(bytes) {
6779
- if (this.#mvdViewDefinition.length === 0) return bytes;
6894
+ #patchHeader(bytes) {
6780
6895
  const HEADER_SCAN = Math.min(bytes.byteLength, 2048);
6781
- const head = new TextDecoder().decode(bytes.subarray(0, HEADER_SCAN));
6782
- if (!VIEW_DEFINITION_RE.test(head)) {
6783
- console.warn(`IfcWriter: FILE_DESCRIPTION ViewDefinition not found; MVD "${this.#mvdViewDefinition}" not declared`);
6784
- return bytes;
6785
- }
6786
- const patchedHead = head.replace(VIEW_DEFINITION_RE, `ViewDefinition [${this.#mvdViewDefinition}]`);
6787
- const patchedHeadBytes = new TextEncoder().encode(patchedHead);
6896
+ let head = new TextDecoder().decode(bytes.subarray(0, HEADER_SCAN));
6897
+ if (FILE_NAME_RE.test(head)) head = head.replace(FILE_NAME_RE, (_m, prefix, systems) => `${prefix}(${stepString(this.#author)}),(${stepString(this.#organization)}),${systems},${stepString("")})`);
6898
+ else console.warn("IfcWriter: FILE_NAME null-field pattern not found; author/organization/authorization left unpatched");
6899
+ if (this.#mvdViewDefinition.length > 0) if (VIEW_DEFINITION_RE.test(head)) head = head.replace(VIEW_DEFINITION_RE, `ViewDefinition [${this.#mvdViewDefinition}]`);
6900
+ else console.warn(`IfcWriter: FILE_DESCRIPTION ViewDefinition not found; MVD "${this.#mvdViewDefinition}" not declared`);
6901
+ const patchedHeadBytes = new TextEncoder().encode(head);
6788
6902
  const tail = bytes.subarray(HEADER_SCAN);
6789
6903
  const out = new Uint8Array(patchedHeadBytes.byteLength + tail.byteLength);
6790
6904
  out.set(patchedHeadBytes, 0);
@@ -8022,7 +8136,7 @@ function writeProfile(w, profile) {
8022
8136
  OverallDepth: w.mkType(web_ifc.IFCPOSITIVELENGTHMEASURE, toIfcLengthM(profile.overallDepth)),
8023
8137
  WebThickness: w.mkType(web_ifc.IFCPOSITIVELENGTHMEASURE, toIfcLengthM(profile.webThickness)),
8024
8138
  FlangeThickness: w.mkType(web_ifc.IFCPOSITIVELENGTHMEASURE, toIfcLengthM(profile.flangeThickness)),
8025
- FilletRadius: null,
8139
+ FilletRadius: profile.filletRadius === void 0 ? null : w.mkType(web_ifc.IFCPOSITIVELENGTHMEASURE, toIfcLengthM(profile.filletRadius)),
8026
8140
  FlangeEdgeRadius: null,
8027
8141
  FlangeSlope: null
8028
8142
  });
@@ -11152,35 +11266,6 @@ function checkOpeningExists(issues, elementsById, openingId, code) {
11152
11266
  if (opening.category !== "OPENING") issues.push(issue("error", code === "VOID_OPENING_MISSING" ? "VOID_OPENING_WRONG_CATEGORY" : "FILL_OPENING_WRONG_CATEGORY", `References opening localId ${openingId}, expected OPENING but found ${opening.category}`, openingId, { actual: opening.category }));
11153
11267
  }
11154
11268
  //#endregion
11155
- //#region src/identity/ifcGuid.ts
11156
- var IFC_CHARS = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz_$";
11157
- function newIfcGuid() {
11158
- const bytes = crypto.getRandomValues(new Uint8Array(16));
11159
- bytes[6] = (bytes[6] ?? 0) & 15 | 64;
11160
- bytes[8] = (bytes[8] ?? 0) & 63 | 128;
11161
- return encodeIfcGuid(bytes);
11162
- }
11163
- function isValidIfcGuid(s) {
11164
- if (s.length !== 22) return false;
11165
- for (const ch of s) if (!IFC_CHARS.includes(ch)) return false;
11166
- return true;
11167
- }
11168
- function encodeIfcGuid(bytes) {
11169
- let result = "";
11170
- let acc = 0;
11171
- let bits = 0;
11172
- for (const byte of bytes) {
11173
- acc = acc << 8 | byte;
11174
- bits += 8;
11175
- while (bits >= 6) {
11176
- bits -= 6;
11177
- result += IFC_CHARS[acc >> bits & 63] ?? "";
11178
- }
11179
- }
11180
- if (bits > 0) result += IFC_CHARS[acc << 6 - bits & 63] ?? "";
11181
- return result;
11182
- }
11183
- //#endregion
11184
11269
  //#region src/validation/schemaCheck.ts
11185
11270
  /**
11186
11271
  * EXPRESS/STEP self-validation gate.
@@ -11197,7 +11282,7 @@ function encodeIfcGuid(bytes) {
11197
11282
  async function checkSchema(bytes) {
11198
11283
  if (bytes.byteLength === 0) return appendIssue(emptyReport(), issue("error", "EMPTY_MODEL", "IFC byte buffer is empty; nothing to validate"));
11199
11284
  const api = new web_ifc.IfcAPI();
11200
- await api.Init();
11285
+ await initIfcApi(api);
11201
11286
  let modelId;
11202
11287
  try {
11203
11288
  modelId = api.OpenModel(bytes);
@@ -11291,7 +11376,7 @@ var KEY_ENTITY_TYPES = [
11291
11376
  */
11292
11377
  async function firstPassCounts(bytes) {
11293
11378
  const api = new web_ifc.IfcAPI();
11294
- await api.Init();
11379
+ await initIfcApi(api);
11295
11380
  const modelId = api.OpenModel(bytes);
11296
11381
  try {
11297
11382
  return collectCounts(api, modelId);
@@ -11305,7 +11390,7 @@ async function firstPassCounts(bytes) {
11305
11390
  */
11306
11391
  async function secondPassCounts(bytes) {
11307
11392
  const api = new web_ifc.IfcAPI();
11308
- await api.Init();
11393
+ await initIfcApi(api);
11309
11394
  const sourceModelId = api.OpenModel(bytes);
11310
11395
  let resaved;
11311
11396
  try {
@@ -11369,7 +11454,11 @@ async function checkRoundTrip(bytes) {
11369
11454
  async function toIfc(model, meta) {
11370
11455
  const project = model.getProject();
11371
11456
  if (!project) return (0, brepjs.err)(ifcError("NO_PROJECT", "BimModel has no project — call model.init() first"));
11372
- const writerResult = await IfcWriter.create(meta.mvdViewDefinition, meta.ifcSchema);
11457
+ const authorName = [meta.author?.givenName, meta.author?.familyName].filter((p) => Boolean(p)).join(" ");
11458
+ const writerResult = await IfcWriter.create(meta.mvdViewDefinition, meta.ifcSchema, {
11459
+ author: authorName,
11460
+ organization: meta.organizationName
11461
+ });
11373
11462
  if (!writerResult.ok) return writerResult;
11374
11463
  const w = writerResult.value;
11375
11464
  w.setModelScope(project.guid);
@@ -12014,7 +12103,7 @@ var SpfReader = class SpfReader {
12014
12103
  let api;
12015
12104
  try {
12016
12105
  api = new web_ifc.IfcAPI();
12017
- await api.Init();
12106
+ await initIfcApi(api);
12018
12107
  } catch (e) {
12019
12108
  return (0, brepjs.err)(importError("OPEN_MODEL_FAILED", "Failed to initialize web-ifc", e));
12020
12109
  }
@@ -15911,6 +16000,7 @@ exports.schemaSupports = schemaSupports;
15911
16000
  exports.serializeBcfFiles = serializeBcfFiles;
15912
16001
  exports.serializeCobieToCsv = serializeCobieToCsv;
15913
16002
  exports.serializeCobieToJson = serializeCobieToJson;
16003
+ exports.setIfcWasmLocateFile = setIfcWasmLocateFile;
15914
16004
  exports.specError = specError;
15915
16005
  exports.templateFor = templateFor;
15916
16006
  exports.toIfc = toIfc;
@@ -1,8 +1,37 @@
1
1
  import { addHoles, applyMatrix, autoHeal, castShape, cut, err, extrude, getKernel, isClosedWire, isOk, isPlanarWire, isSolid, isValid, isValidSolid, measureVolume, mesh, ok, outerWire, polygon, revolve, rotate, validSolid } from "brepjs";
2
2
  import * as WebIFC from "web-ifc";
3
3
  import { Handle, IfcAPI } from "web-ifc";
4
+ //#region src/identity/ifcGuid.ts
5
+ var IFC_CHARS = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz_$";
6
+ function newIfcGuid() {
7
+ const bytes = crypto.getRandomValues(new Uint8Array(16));
8
+ bytes[6] = (bytes[6] ?? 0) & 15 | 64;
9
+ bytes[8] = (bytes[8] ?? 0) & 63 | 128;
10
+ return encodeIfcGuid(bytes);
11
+ }
12
+ function isValidIfcGuid(s) {
13
+ if (s.length !== 22) return false;
14
+ if (!"0123".includes(s[0] ?? "")) return false;
15
+ for (const ch of s) if (!IFC_CHARS.includes(ch)) return false;
16
+ return true;
17
+ }
18
+ function encodeIfcGuid(bytes) {
19
+ let result = "";
20
+ let acc = 0;
21
+ let bits = 4;
22
+ for (const byte of bytes) {
23
+ acc = acc << 8 | byte;
24
+ bits += 8;
25
+ while (bits >= 6) {
26
+ bits -= 6;
27
+ result += IFC_CHARS[acc >> bits & 63] ?? "";
28
+ }
29
+ }
30
+ if (bits > 0) result += IFC_CHARS[acc << 6 - bits & 63] ?? "";
31
+ return result;
32
+ }
33
+ //#endregion
4
34
  //#region src/identity/guidDerivation.ts
5
- var IFC_CHARS$1 = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz_$";
6
35
  var NAMESPACE = "brepjs-bim:v1";
7
36
  var FNV_OFFSET = 2166136261;
8
37
  var FNV_PRIME = 16777619;
@@ -36,7 +65,7 @@ function digest16(stableKey) {
36
65
  * keys yield distinct, format-valid (22-char) GlobalIds.
37
66
  */
38
67
  function deriveIfcGuidSync(stableKey) {
39
- return encodeIfcGuid$1(digest16(stableKey));
68
+ return encodeIfcGuid(digest16(stableKey));
40
69
  }
41
70
  /**
42
71
  * Async wrapper over {@link deriveIfcGuidSync} for callers that prefer a Promise
@@ -63,21 +92,6 @@ function makeRelKey(modelScope, kind, localId) {
63
92
  function makeLineKey(modelScope, expressId) {
64
93
  return `line:${modelScope}:${expressId}`;
65
94
  }
66
- function encodeIfcGuid$1(bytes) {
67
- let result = "";
68
- let acc = 0;
69
- let bits = 0;
70
- for (const byte of bytes) {
71
- acc = acc << 8 | byte;
72
- bits += 8;
73
- while (bits >= 6) {
74
- bits -= 6;
75
- result += IFC_CHARS$1[acc >> bits & 63] ?? "";
76
- }
77
- }
78
- if (bits > 0) result += IFC_CHARS$1[acc << 6 - bits & 63] ?? "";
79
- return result;
80
- }
81
95
  //#endregion
82
96
  //#region src/identity/localId.ts
83
97
  function makeLocalIdCounter(start = 1) {
@@ -5070,7 +5084,8 @@ var CoreProfileSchema = discriminatedUnion("kind", [
5070
5084
  overallWidth: number().positive(),
5071
5085
  overallDepth: number().positive(),
5072
5086
  flangeThickness: number().positive(),
5073
- webThickness: number().positive()
5087
+ webThickness: number().positive(),
5088
+ filletRadius: number().positive().optional()
5074
5089
  })
5075
5090
  ]);
5076
5091
  var Pt2Schema = tuple([number(), number()]);
@@ -5202,6 +5217,11 @@ function parseProfile(input) {
5202
5217
  if (profile.kind === "I_BEAM") {
5203
5218
  if (2 * profile.flangeThickness >= profile.overallDepth) return err(specError("INVALID_PROFILE", "I-beam flangeThickness × 2 must be less than overallDepth"));
5204
5219
  if (profile.webThickness >= profile.overallWidth) return err(specError("INVALID_PROFILE", "I-beam webThickness must be less than overallWidth"));
5220
+ if (profile.filletRadius !== void 0) {
5221
+ const clearHeight = profile.overallDepth / 2 - profile.flangeThickness;
5222
+ const clearSpan = (profile.overallWidth - profile.webThickness) / 2;
5223
+ if (profile.filletRadius >= clearHeight || profile.filletRadius >= clearSpan) return err(specError("INVALID_PROFILE", "I-beam filletRadius must be smaller than the clear web height and the clear span beside the web"));
5224
+ }
5205
5225
  }
5206
5226
  return ok(profile);
5207
5227
  }
@@ -5212,8 +5232,42 @@ function profileCrossSectionArea(profile) {
5212
5232
  switch (profile.kind) {
5213
5233
  case "RECTANGULAR": return profile.width * profile.height;
5214
5234
  case "CIRCULAR": return Math.PI * profile.radius * profile.radius;
5215
- case "I_BEAM": return 2 * profile.overallWidth * profile.flangeThickness + (profile.overallDepth - 2 * profile.flangeThickness) * profile.webThickness;
5235
+ case "I_BEAM": {
5236
+ const flangeArea = 2 * profile.overallWidth * profile.flangeThickness;
5237
+ const webArea = (profile.overallDepth - 2 * profile.flangeThickness) * profile.webThickness;
5238
+ const r = profile.filletRadius ?? 0;
5239
+ const filletArea = 4 * r * r * (1 - Math.PI / 4);
5240
+ return flangeArea + webArea + filletArea;
5241
+ }
5242
+ }
5243
+ }
5244
+ var FILLET_SEGMENTS = 8;
5245
+ var FILLET_MIN_ANGLE = .001;
5246
+ function filletArc(prev, v, next, r) {
5247
+ const aLen = Math.hypot(prev[0] - v[0], prev[1] - v[1]);
5248
+ const bLen = Math.hypot(next[0] - v[0], next[1] - v[1]);
5249
+ const a = [(prev[0] - v[0]) / aLen, (prev[1] - v[1]) / aLen];
5250
+ const b = [(next[0] - v[0]) / bLen, (next[1] - v[1]) / bLen];
5251
+ const alpha = Math.acos(Math.max(-1, Math.min(1, a[0] * b[0] + a[1] * b[1])));
5252
+ if (alpha < FILLET_MIN_ANGLE || alpha > Math.PI - FILLET_MIN_ANGLE) return [[v[0], v[1]]];
5253
+ const setback = r / Math.tan(alpha / 2);
5254
+ const center = r / Math.sin(alpha / 2);
5255
+ const bisLen = Math.hypot(a[0] + b[0], a[1] + b[1]);
5256
+ const bis = [(a[0] + b[0]) / bisLen, (a[1] + b[1]) / bisLen];
5257
+ const cx = v[0] + bis[0] * center;
5258
+ const cy = v[1] + bis[1] * center;
5259
+ const p1 = [v[0] + a[0] * setback, v[1] + a[1] * setback];
5260
+ const p2 = [v[0] + b[0] * setback, v[1] + b[1] * setback];
5261
+ const a1 = Math.atan2(p1[1] - cy, p1[0] - cx);
5262
+ let delta = Math.atan2(p2[1] - cy, p2[0] - cx) - a1;
5263
+ while (delta <= -Math.PI) delta += 2 * Math.PI;
5264
+ while (delta > Math.PI) delta -= 2 * Math.PI;
5265
+ const out = [];
5266
+ for (let i = 0; i <= FILLET_SEGMENTS; i++) {
5267
+ const ang = a1 + delta * i / FILLET_SEGMENTS;
5268
+ out.push([cx + r * Math.cos(ang), cy + r * Math.sin(ang)]);
5216
5269
  }
5270
+ return out;
5217
5271
  }
5218
5272
  function profileToPolygon(profile, circleSegments = 32) {
5219
5273
  if (isExtendedProfile(profile)) return err(specError("EXTENDED_PROFILE_NO_POLYGON", `profileToPolygon: extended profile kind '${profile.kind}' has no single-polygon outline; use extendedProfileToFace()`));
@@ -5262,68 +5316,50 @@ function profileToPolygon(profile, circleSegments = 32) {
5262
5316
  const halfD = profile.overallDepth / 2;
5263
5317
  const halfWeb = profile.webThickness / 2;
5264
5318
  const flangeInnerY = halfD - profile.flangeThickness;
5265
- return ok([
5266
- [
5267
- -halfW,
5268
- -halfD,
5269
- 0
5270
- ],
5271
- [
5272
- halfW,
5273
- -halfD,
5274
- 0
5275
- ],
5276
- [
5277
- halfW,
5278
- -flangeInnerY,
5279
- 0
5280
- ],
5281
- [
5282
- halfWeb,
5283
- -flangeInnerY,
5284
- 0
5285
- ],
5286
- [
5287
- halfWeb,
5288
- flangeInnerY,
5289
- 0
5290
- ],
5291
- [
5292
- halfW,
5293
- flangeInnerY,
5294
- 0
5295
- ],
5296
- [
5297
- halfW,
5298
- halfD,
5299
- 0
5300
- ],
5301
- [
5302
- -halfW,
5303
- halfD,
5304
- 0
5305
- ],
5306
- [
5307
- -halfW,
5308
- flangeInnerY,
5309
- 0
5310
- ],
5311
- [
5312
- -halfWeb,
5313
- flangeInnerY,
5314
- 0
5315
- ],
5316
- [
5317
- -halfWeb,
5318
- -flangeInnerY,
5319
- 0
5320
- ],
5321
- [
5322
- -halfW,
5323
- -flangeInnerY,
5324
- 0
5325
- ]
5319
+ const v = [
5320
+ [-halfW, -halfD],
5321
+ [halfW, -halfD],
5322
+ [halfW, -flangeInnerY],
5323
+ [halfWeb, -flangeInnerY],
5324
+ [halfWeb, flangeInnerY],
5325
+ [halfW, flangeInnerY],
5326
+ [halfW, halfD],
5327
+ [-halfW, halfD],
5328
+ [-halfW, flangeInnerY],
5329
+ [-halfWeb, flangeInnerY],
5330
+ [-halfWeb, -flangeInnerY],
5331
+ [-halfW, -flangeInnerY]
5332
+ ];
5333
+ const r = profile.filletRadius ?? 0;
5334
+ const rootCorners = new Set([
5335
+ 3,
5336
+ 4,
5337
+ 9,
5338
+ 10
5326
5339
  ]);
5340
+ const pts = [];
5341
+ for (let i = 0; i < v.length; i++) {
5342
+ const cur = v[i];
5343
+ if (cur === void 0) continue;
5344
+ if (r > 0 && rootCorners.has(i)) {
5345
+ const prev = v[(i - 1 + v.length) % v.length];
5346
+ const next = v[(i + 1) % v.length];
5347
+ if (prev !== void 0 && next !== void 0) {
5348
+ for (const [ax, ay] of filletArc(prev, cur, next, r)) pts.push([
5349
+ ax,
5350
+ ay,
5351
+ 0
5352
+ ]);
5353
+ continue;
5354
+ }
5355
+ }
5356
+ pts.push([
5357
+ cur[0],
5358
+ cur[1],
5359
+ 0
5360
+ ]);
5361
+ }
5362
+ return ok(pts);
5327
5363
  }
5328
5364
  }
5329
5365
  }
@@ -6507,6 +6543,50 @@ var BimModel = class {
6507
6543
  getElement(id) {
6508
6544
  return this.#elements.get(id) ?? null;
6509
6545
  }
6546
+ /**
6547
+ * A serializable summary of the model's structure, rooted at the project and
6548
+ * walking the IFC spatial hierarchy (AGGREGATES: project → site → building →
6549
+ * storey) plus the elements contained in each storey (placeIn). Useful for a
6550
+ * read-only tree view of the model across a worker boundary.
6551
+ */
6552
+ toTreeSummary() {
6553
+ const aggregated = /* @__PURE__ */ new Map();
6554
+ const contained = /* @__PURE__ */ new Map();
6555
+ for (const rel of this.#relationships.values()) if (rel.kind === "AGGREGATES") {
6556
+ const list = aggregated.get(rel.relatingObject) ?? [];
6557
+ list.push(...rel.relatedObjects);
6558
+ aggregated.set(rel.relatingObject, list);
6559
+ } else if (rel.kind === "CONTAINED_IN") {
6560
+ const list = contained.get(rel.relatingStructure) ?? [];
6561
+ list.push(...rel.relatedElements);
6562
+ contained.set(rel.relatingStructure, list);
6563
+ }
6564
+ const labelFor = (el) => {
6565
+ const spec = el.spec;
6566
+ const base = typeof spec.name === "string" && spec.name.length > 0 ? spec.name : el.category;
6567
+ return el.category === "STOREY" && typeof spec.elevation === "number" ? `${base} (+${spec.elevation} mm)` : base;
6568
+ };
6569
+ const seen = /* @__PURE__ */ new Set();
6570
+ const build = (id) => {
6571
+ if (seen.has(id)) return null;
6572
+ seen.add(id);
6573
+ const el = this.#elements.get(id);
6574
+ if (el === void 0) return null;
6575
+ const children = [...aggregated.get(id) ?? [], ...contained.get(id) ?? []].map(build).filter((n) => n !== null);
6576
+ return {
6577
+ id,
6578
+ label: labelFor(el),
6579
+ category: el.category,
6580
+ children
6581
+ };
6582
+ };
6583
+ const root = this.#projectId !== null ? build(this.#projectId) : null;
6584
+ const countNodes = (node) => 1 + node.children.reduce((sum, c) => sum + countNodes(c), 0);
6585
+ return {
6586
+ root,
6587
+ elementCount: root ? countNodes(root) : 0
6588
+ };
6589
+ }
6510
6590
  getWalls() {
6511
6591
  const walls = [];
6512
6592
  for (const el of this.#elements.values()) if (el.category === "WALL") walls.push(el);
@@ -6684,27 +6764,62 @@ function schemaSupports(schema, entityName) {
6684
6764
  return true;
6685
6765
  }
6686
6766
  //#endregion
6767
+ //#region src/ifcRuntime.ts
6768
+ var wasmLocateFile;
6769
+ /**
6770
+ * Override how web-ifc finds its `.wasm` file. Applied by every web-ifc entry
6771
+ * point in this package — IFC export ({@link toIfc}), import ({@link fromIfc})
6772
+ * and validation. Required when brepjs-bim is bundled into a worker that serves
6773
+ * the wasm itself; not needed in Node.
6774
+ */
6775
+ function setIfcWasmLocateFile(locate) {
6776
+ wasmLocateFile = locate;
6777
+ }
6778
+ /**
6779
+ * Initialize a web-ifc API instance the way this package always wants it: with
6780
+ * the host-provided wasm locator and forced single-threaded.
6781
+ *
6782
+ * Single-threaded matters in a cross-origin-isolated context (e.g. a page that
6783
+ * sets COOP/COEP for another WASM kernel): web-ifc would otherwise load its
6784
+ * pthread build and spawn a sub-Worker, which fails when brepjs-bim is itself
6785
+ * bundled inside a Web Worker. In Node the flag is a no-op (web-ifc is already
6786
+ * single-threaded there), and multithreading only speeds up parsing/geometry,
6787
+ * not the one-shot serialize/read this package does.
6788
+ */
6789
+ async function initIfcApi(api) {
6790
+ await api.Init(wasmLocateFile, true);
6791
+ }
6792
+ //#endregion
6687
6793
  //#region src/ifc-writer/ifcWriter.ts
6688
6794
  /** Default MVD ViewDefinition declared in the STEP FILE_DESCRIPTION header. */
6689
6795
  var DEFAULT_MVD_VIEW_DEFINITION = "ReferenceView_v1.2";
6690
6796
  var VIEW_DEFINITION_RE = /ViewDefinition \[[^\]]*\]/;
6797
+ var FILE_NAME_RE = /(FILE_NAME\('[^']*','[^']*',)(?:\$|\(\$\)),(?:\$|\(\$\)),('[^']*','[^']*'),\$\)/;
6798
+ /** STEP single-quoted string literal with embedded quotes doubled per ISO 10303-21. */
6799
+ function stepString(value) {
6800
+ return `'${value.replace(/'/g, "''")}'`;
6801
+ }
6691
6802
  var IfcWriter = class IfcWriter {
6692
6803
  #api;
6693
6804
  #modelId;
6694
6805
  #mvdViewDefinition;
6806
+ #author;
6807
+ #organization;
6695
6808
  #nextExpressId = 1;
6696
6809
  #closed = false;
6697
6810
  #modelScope = "";
6698
- constructor(api, modelId, mvdViewDefinition) {
6811
+ constructor(api, modelId, mvdViewDefinition, header) {
6699
6812
  this.#api = api;
6700
6813
  this.#modelId = modelId;
6701
6814
  this.#mvdViewDefinition = mvdViewDefinition;
6815
+ this.#author = header.author ?? "";
6816
+ this.#organization = header.organization ?? "";
6702
6817
  }
6703
- static async create(mvdViewDefinition = DEFAULT_MVD_VIEW_DEFINITION, ifcSchema = DEFAULT_IFC_SCHEMA) {
6818
+ static async create(mvdViewDefinition = DEFAULT_MVD_VIEW_DEFINITION, ifcSchema = DEFAULT_IFC_SCHEMA, header = {}) {
6704
6819
  try {
6705
6820
  const api = new IfcAPI();
6706
- await api.Init();
6707
- return ok(new IfcWriter(api, api.CreateModel({ schema: fileSchemaString(ifcSchema) }), mvdViewDefinition));
6821
+ await initIfcApi(api);
6822
+ return ok(new IfcWriter(api, api.CreateModel({ schema: fileSchemaString(ifcSchema) }), mvdViewDefinition, header));
6708
6823
  } catch (e) {
6709
6824
  return err(ifcError("IFC_INIT_FAILED", "Failed to initialize web-ifc", e));
6710
6825
  }
@@ -6738,7 +6853,7 @@ var IfcWriter = class IfcWriter {
6738
6853
  if (this.#closed) return err(ifcError("IFC_ALREADY_SAVED", "Model has already been saved and closed"));
6739
6854
  try {
6740
6855
  const bytes = this.#api.SaveModel(this.#modelId);
6741
- return ok(this.#patchMvd(bytes));
6856
+ return ok(this.#patchHeader(bytes));
6742
6857
  } catch (e) {
6743
6858
  return err(ifcError("IFC_SAVE_FAILED", "Failed to serialize IFC model", e));
6744
6859
  } finally {
@@ -6747,21 +6862,20 @@ var IfcWriter = class IfcWriter {
6747
6862
  }
6748
6863
  }
6749
6864
  /**
6750
- * Injects the declared MVD into the STEP FILE_DESCRIPTION header. web-ifc does
6751
- * not expose the header's ViewDefinition for configuration, so we rewrite the
6752
- * empty default in the ASCII header region. If the expected pattern is absent
6753
- * (e.g. a future web-ifc default change) the bytes are returned unchanged.
6865
+ * Rewrites the STEP header in the ASCII region web-ifc emits: declares the MVD
6866
+ * in FILE_DESCRIPTION and makes FILE_NAME's author/organization/authorization
6867
+ * spec-conformant (web-ifc leaves them as bare `$`). web-ifc exposes neither
6868
+ * for configuration. If an expected pattern is absent (e.g. a future web-ifc
6869
+ * default change) that part is skipped and the bytes returned unchanged.
6754
6870
  */
6755
- #patchMvd(bytes) {
6756
- if (this.#mvdViewDefinition.length === 0) return bytes;
6871
+ #patchHeader(bytes) {
6757
6872
  const HEADER_SCAN = Math.min(bytes.byteLength, 2048);
6758
- const head = new TextDecoder().decode(bytes.subarray(0, HEADER_SCAN));
6759
- if (!VIEW_DEFINITION_RE.test(head)) {
6760
- console.warn(`IfcWriter: FILE_DESCRIPTION ViewDefinition not found; MVD "${this.#mvdViewDefinition}" not declared`);
6761
- return bytes;
6762
- }
6763
- const patchedHead = head.replace(VIEW_DEFINITION_RE, `ViewDefinition [${this.#mvdViewDefinition}]`);
6764
- const patchedHeadBytes = new TextEncoder().encode(patchedHead);
6873
+ let head = new TextDecoder().decode(bytes.subarray(0, HEADER_SCAN));
6874
+ if (FILE_NAME_RE.test(head)) head = head.replace(FILE_NAME_RE, (_m, prefix, systems) => `${prefix}(${stepString(this.#author)}),(${stepString(this.#organization)}),${systems},${stepString("")})`);
6875
+ else console.warn("IfcWriter: FILE_NAME null-field pattern not found; author/organization/authorization left unpatched");
6876
+ if (this.#mvdViewDefinition.length > 0) if (VIEW_DEFINITION_RE.test(head)) head = head.replace(VIEW_DEFINITION_RE, `ViewDefinition [${this.#mvdViewDefinition}]`);
6877
+ else console.warn(`IfcWriter: FILE_DESCRIPTION ViewDefinition not found; MVD "${this.#mvdViewDefinition}" not declared`);
6878
+ const patchedHeadBytes = new TextEncoder().encode(head);
6765
6879
  const tail = bytes.subarray(HEADER_SCAN);
6766
6880
  const out = new Uint8Array(patchedHeadBytes.byteLength + tail.byteLength);
6767
6881
  out.set(patchedHeadBytes, 0);
@@ -7999,7 +8113,7 @@ function writeProfile(w, profile) {
7999
8113
  OverallDepth: w.mkType(WebIFC.IFCPOSITIVELENGTHMEASURE, toIfcLengthM(profile.overallDepth)),
8000
8114
  WebThickness: w.mkType(WebIFC.IFCPOSITIVELENGTHMEASURE, toIfcLengthM(profile.webThickness)),
8001
8115
  FlangeThickness: w.mkType(WebIFC.IFCPOSITIVELENGTHMEASURE, toIfcLengthM(profile.flangeThickness)),
8002
- FilletRadius: null,
8116
+ FilletRadius: profile.filletRadius === void 0 ? null : w.mkType(WebIFC.IFCPOSITIVELENGTHMEASURE, toIfcLengthM(profile.filletRadius)),
8003
8117
  FlangeEdgeRadius: null,
8004
8118
  FlangeSlope: null
8005
8119
  });
@@ -11129,35 +11243,6 @@ function checkOpeningExists(issues, elementsById, openingId, code) {
11129
11243
  if (opening.category !== "OPENING") issues.push(issue("error", code === "VOID_OPENING_MISSING" ? "VOID_OPENING_WRONG_CATEGORY" : "FILL_OPENING_WRONG_CATEGORY", `References opening localId ${openingId}, expected OPENING but found ${opening.category}`, openingId, { actual: opening.category }));
11130
11244
  }
11131
11245
  //#endregion
11132
- //#region src/identity/ifcGuid.ts
11133
- var IFC_CHARS = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz_$";
11134
- function newIfcGuid() {
11135
- const bytes = crypto.getRandomValues(new Uint8Array(16));
11136
- bytes[6] = (bytes[6] ?? 0) & 15 | 64;
11137
- bytes[8] = (bytes[8] ?? 0) & 63 | 128;
11138
- return encodeIfcGuid(bytes);
11139
- }
11140
- function isValidIfcGuid(s) {
11141
- if (s.length !== 22) return false;
11142
- for (const ch of s) if (!IFC_CHARS.includes(ch)) return false;
11143
- return true;
11144
- }
11145
- function encodeIfcGuid(bytes) {
11146
- let result = "";
11147
- let acc = 0;
11148
- let bits = 0;
11149
- for (const byte of bytes) {
11150
- acc = acc << 8 | byte;
11151
- bits += 8;
11152
- while (bits >= 6) {
11153
- bits -= 6;
11154
- result += IFC_CHARS[acc >> bits & 63] ?? "";
11155
- }
11156
- }
11157
- if (bits > 0) result += IFC_CHARS[acc << 6 - bits & 63] ?? "";
11158
- return result;
11159
- }
11160
- //#endregion
11161
11246
  //#region src/validation/schemaCheck.ts
11162
11247
  /**
11163
11248
  * EXPRESS/STEP self-validation gate.
@@ -11174,7 +11259,7 @@ function encodeIfcGuid(bytes) {
11174
11259
  async function checkSchema(bytes) {
11175
11260
  if (bytes.byteLength === 0) return appendIssue(emptyReport(), issue("error", "EMPTY_MODEL", "IFC byte buffer is empty; nothing to validate"));
11176
11261
  const api = new WebIFC.IfcAPI();
11177
- await api.Init();
11262
+ await initIfcApi(api);
11178
11263
  let modelId;
11179
11264
  try {
11180
11265
  modelId = api.OpenModel(bytes);
@@ -11268,7 +11353,7 @@ var KEY_ENTITY_TYPES = [
11268
11353
  */
11269
11354
  async function firstPassCounts(bytes) {
11270
11355
  const api = new IfcAPI();
11271
- await api.Init();
11356
+ await initIfcApi(api);
11272
11357
  const modelId = api.OpenModel(bytes);
11273
11358
  try {
11274
11359
  return collectCounts(api, modelId);
@@ -11282,7 +11367,7 @@ async function firstPassCounts(bytes) {
11282
11367
  */
11283
11368
  async function secondPassCounts(bytes) {
11284
11369
  const api = new IfcAPI();
11285
- await api.Init();
11370
+ await initIfcApi(api);
11286
11371
  const sourceModelId = api.OpenModel(bytes);
11287
11372
  let resaved;
11288
11373
  try {
@@ -11346,7 +11431,11 @@ async function checkRoundTrip(bytes) {
11346
11431
  async function toIfc(model, meta) {
11347
11432
  const project = model.getProject();
11348
11433
  if (!project) return err(ifcError("NO_PROJECT", "BimModel has no project — call model.init() first"));
11349
- const writerResult = await IfcWriter.create(meta.mvdViewDefinition, meta.ifcSchema);
11434
+ const authorName = [meta.author?.givenName, meta.author?.familyName].filter((p) => Boolean(p)).join(" ");
11435
+ const writerResult = await IfcWriter.create(meta.mvdViewDefinition, meta.ifcSchema, {
11436
+ author: authorName,
11437
+ organization: meta.organizationName
11438
+ });
11350
11439
  if (!writerResult.ok) return writerResult;
11351
11440
  const w = writerResult.value;
11352
11441
  w.setModelScope(project.guid);
@@ -11991,7 +12080,7 @@ var SpfReader = class SpfReader {
11991
12080
  let api;
11992
12081
  try {
11993
12082
  api = new IfcAPI();
11994
- await api.Init();
12083
+ await initIfcApi(api);
11995
12084
  } catch (e) {
11996
12085
  return err(importError("OPEN_MODEL_FAILED", "Failed to initialize web-ifc", e));
11997
12086
  }
@@ -15819,4 +15908,4 @@ function assignNum(target, key, value) {
15819
15908
  if (value !== void 0) target[key] = Number(value);
15820
15909
  }
15821
15910
  //#endregion
15822
- export { BimModel, DEFAULT_IFC_SCHEMA, DEFAULT_UNITS, IFC_SCHEMAS, PSET_PROPERTY_TYPE_TABLE, PSET_TEMPLATES, SpfReader, bcfError, checkGeometryValidity, checkModelAgainstIds as checkIds, checkModelAgainstIds, checkReferentialIntegrity, checkRoundTrip, checkSchema, countBySeverity, deriveCobieModel, deriveCobieModel as exportCobie, deriveIfcGuid, deriveIfcGuidSync, disposeImportedModel, emptyReport, extendedProfileArea, extendedProfileToFace, fileSchemaString, fromBrepError, fromIfc, geometryError, hasErrors, idsError, ifcError, importError, isExtendedProfile, isIfcSchema, isSlabOpening, isValidIfcGuid, isWallOpening, issue, makeLocalIdCounter, measureTypeFor, newIfcGuid, parseBcfFiles, parseBeamSpec, parseColumnSpec, parseCoveringSpec, parseCurtainWallSpec, parseDoorSpec, parseElementAssemblySpec, parseFootingSpec, parseIdsXml, parsePileSpec, parseProfile, parseRailingSpec, parseRampFlightSpec, parseRampSpec, parseRoofSpec, parseSlabOpeningInput, parseSlabSpec, parseSpaceSpec, parseStairFlightSpec, parseStairSpec, parseSurfaceStyleSpec, parseSystemSpec, parseWallSpec, parseWindowSpec, parseZoneSpec, schemaSupports, serializeBcfFiles, serializeCobieToCsv, serializeCobieToJson, specError, templateFor, toIfc, toIfcLengthM, toIfcValidated, toLengthMm, writeClassificationRefs, writeElementAssemblyEntity, writeIfcType, writeMaterialLayerSet, writeMaterialProfileSet, writeMaterialSimple, writePresentationLayer, writeRelAggregatesElements, writeRelAssignsToGroup, writeRelConnectsElements, writeRelConnectsPathElements, writeRelNests, writeStyledItem, writeSurfaceStyle, writeSystemEntity, writeZoneEntity };
15911
+ export { BimModel, DEFAULT_IFC_SCHEMA, DEFAULT_UNITS, IFC_SCHEMAS, PSET_PROPERTY_TYPE_TABLE, PSET_TEMPLATES, SpfReader, bcfError, checkGeometryValidity, checkModelAgainstIds as checkIds, checkModelAgainstIds, checkReferentialIntegrity, checkRoundTrip, checkSchema, countBySeverity, deriveCobieModel, deriveCobieModel as exportCobie, deriveIfcGuid, deriveIfcGuidSync, disposeImportedModel, emptyReport, extendedProfileArea, extendedProfileToFace, fileSchemaString, fromBrepError, fromIfc, geometryError, hasErrors, idsError, ifcError, importError, isExtendedProfile, isIfcSchema, isSlabOpening, isValidIfcGuid, isWallOpening, issue, makeLocalIdCounter, measureTypeFor, newIfcGuid, parseBcfFiles, parseBeamSpec, parseColumnSpec, parseCoveringSpec, parseCurtainWallSpec, parseDoorSpec, parseElementAssemblySpec, parseFootingSpec, parseIdsXml, parsePileSpec, parseProfile, parseRailingSpec, parseRampFlightSpec, parseRampSpec, parseRoofSpec, parseSlabOpeningInput, parseSlabSpec, parseSpaceSpec, parseStairFlightSpec, parseStairSpec, parseSurfaceStyleSpec, parseSystemSpec, parseWallSpec, parseWindowSpec, parseZoneSpec, schemaSupports, serializeBcfFiles, serializeCobieToCsv, serializeCobieToJson, setIfcWasmLocateFile, specError, templateFor, toIfc, toIfcLengthM, toIfcValidated, toLengthMm, writeClassificationRefs, writeElementAssemblyEntity, writeIfcType, writeMaterialLayerSet, writeMaterialProfileSet, writeMaterialSimple, writePresentationLayer, writeRelAggregatesElements, writeRelAssignsToGroup, writeRelConnectsElements, writeRelConnectsPathElements, writeRelNests, writeStyledItem, writeSurfaceStyle, writeSystemEntity, writeZoneEntity };
@@ -4,4 +4,5 @@ export type IfcGuid = string & {
4
4
  };
5
5
  export declare function newIfcGuid(): IfcGuid;
6
6
  export declare function isValidIfcGuid(s: string): s is IfcGuid;
7
+ export declare function encodeIfcGuid(bytes: Uint8Array): IfcGuid;
7
8
  export {};
@@ -5,10 +5,14 @@ import { IfcSchema } from './schemaVersion.js';
5
5
  import { Result } from 'brepjs';
6
6
  /** Default MVD ViewDefinition declared in the STEP FILE_DESCRIPTION header. */
7
7
  export declare const DEFAULT_MVD_VIEW_DEFINITION = "ReferenceView_v1.2";
8
+ export interface IfcHeaderMeta {
9
+ readonly author?: string | undefined;
10
+ readonly organization?: string | undefined;
11
+ }
8
12
  export declare class IfcWriter {
9
13
  #private;
10
14
  private constructor();
11
- static create(mvdViewDefinition?: string, ifcSchema?: IfcSchema): Promise<Result<IfcWriter, BimError>>;
15
+ static create(mvdViewDefinition?: string, ifcSchema?: IfcSchema, header?: IfcHeaderMeta): Promise<Result<IfcWriter, BimError>>;
12
16
  nextId(): number;
13
17
  /**
14
18
  * Deterministic GlobalId for a writer-minted line, keyed on its express ID.
@@ -0,0 +1,20 @@
1
+ import { IfcAPI } from 'web-ifc';
2
+ /**
3
+ * Override how web-ifc finds its `.wasm` file. Applied by every web-ifc entry
4
+ * point in this package — IFC export ({@link toIfc}), import ({@link fromIfc})
5
+ * and validation. Required when brepjs-bim is bundled into a worker that serves
6
+ * the wasm itself; not needed in Node.
7
+ */
8
+ export declare function setIfcWasmLocateFile(locate: ((path: string, prefix: string) => string) | undefined): void;
9
+ /**
10
+ * Initialize a web-ifc API instance the way this package always wants it: with
11
+ * the host-provided wasm locator and forced single-threaded.
12
+ *
13
+ * Single-threaded matters in a cross-origin-isolated context (e.g. a page that
14
+ * sets COOP/COEP for another WASM kernel): web-ifc would otherwise load its
15
+ * pthread build and spawn a sub-Worker, which fails when brepjs-bim is itself
16
+ * bundled inside a Web Worker. In Node the flag is a no-op (web-ifc is already
17
+ * single-threaded there), and multithreading only speeds up parsing/geometry,
18
+ * not the one-shot serialize/read this package does.
19
+ */
20
+ export declare function initIfcApi(api: IfcAPI): Promise<void>;
package/dist/index.d.ts CHANGED
@@ -1,6 +1,8 @@
1
1
  export { BimModel } from './model/bimModel.js';
2
+ export type { BimTreeNode, BimTreeSummary } from './model/treeSummary.js';
2
3
  export { toIfc, toIfcValidated } from './serialize/toIfc.js';
3
4
  export type { ValidatedIfcResult } from './serialize/toIfc.js';
5
+ export { setIfcWasmLocateFile } from './ifcRuntime.js';
4
6
  export { fromIfc } from './import/fromIfc.js';
5
7
  export { disposeImportedModel } from './import/importedModel.js';
6
8
  export type { FromIfcOptions } from './import/fromIfc.js';
@@ -2,6 +2,7 @@ import { Result } from 'brepjs';
2
2
  import { LocalId } from '../identity/localId.js';
3
3
  import { BimError } from '../errors/bimError.js';
4
4
  import { AnyBimElement, BimElement } from '../types/bimTypes.js';
5
+ import { BimTreeSummary } from './treeSummary.js';
5
6
  import { BimRelationship } from '../types/relationships.js';
6
7
  import { ClassificationRef } from '../types/classificationTypes.js';
7
8
  import { WallSpec } from '../specs/wallSpec.js';
@@ -129,6 +130,13 @@ export declare class BimModel {
129
130
  placeIn(elementId: LocalId, containerId: LocalId): void;
130
131
  getProject(): BimElement<'PROJECT'> | null;
131
132
  getElement(id: LocalId): AnyBimElement | null;
133
+ /**
134
+ * A serializable summary of the model's structure, rooted at the project and
135
+ * walking the IFC spatial hierarchy (AGGREGATES: project → site → building →
136
+ * storey) plus the elements contained in each storey (placeIn). Useful for a
137
+ * read-only tree view of the model across a worker boundary.
138
+ */
139
+ toTreeSummary(): BimTreeSummary;
132
140
  getWalls(): BimElement<'WALL'>[];
133
141
  getSlabs(): BimElement<'SLAB'>[];
134
142
  getBeams(): BimElement<'BEAM'>[];
@@ -0,0 +1,21 @@
1
+ import { BimCategory } from '../types/bimTypes.js';
2
+ /**
3
+ * A node in a {@link BimModel}'s spatial/decomposition tree. Fully serializable
4
+ * (plain numbers/strings) so it can be posted across a worker boundary.
5
+ */
6
+ export interface BimTreeNode {
7
+ /** The element's local id. */
8
+ readonly id: number;
9
+ /** Display label — the element's name, or its category when unnamed. */
10
+ readonly label: string;
11
+ /** The element's IFC category. */
12
+ readonly category: BimCategory;
13
+ readonly children: readonly BimTreeNode[];
14
+ }
15
+ /** A serializable summary of a model's structure, rooted at the project. */
16
+ export interface BimTreeSummary {
17
+ /** The project node and its nested spatial structure + contained elements. */
18
+ readonly root: BimTreeNode | null;
19
+ /** Number of nodes in the tree (the project and everything reachable from it). */
20
+ readonly elementCount: number;
21
+ }
@@ -17,6 +17,7 @@ export type IShapeProfile = {
17
17
  readonly overallDepth: number;
18
18
  readonly flangeThickness: number;
19
19
  readonly webThickness: number;
20
+ readonly filletRadius?: number | undefined;
20
21
  };
21
22
  export type CoreProfile = RectangularProfile | CircularProfile | IShapeProfile;
22
23
  export type Profile = CoreProfile | ExtendedProfile;
@@ -35,6 +36,7 @@ export declare const ProfileSchema: z.ZodUnion<readonly [z.ZodDiscriminatedUnion
35
36
  overallDepth: z.ZodNumber;
36
37
  flangeThickness: z.ZodNumber;
37
38
  webThickness: z.ZodNumber;
39
+ filletRadius: z.ZodOptional<z.ZodNumber>;
38
40
  }, z.core.$strip>], "kind">, z.ZodDiscriminatedUnion<[z.ZodObject<{
39
41
  kind: z.ZodLiteral<"L_SHAPE">;
40
42
  depth: z.ZodNumber;
package/package.json CHANGED
@@ -1,23 +1,40 @@
1
1
  {
2
2
  "name": "brepjs-bim",
3
- "version": "0.1.0",
3
+ "version": "0.2.0",
4
4
  "description": "BIM layer for brepjs — IFC4-aligned parametric building elements",
5
- "keywords": ["bim", "ifc", "cad", "brep"],
5
+ "keywords": [
6
+ "bim",
7
+ "ifc",
8
+ "cad",
9
+ "brep"
10
+ ],
6
11
  "author": "Andy Aragon",
7
12
  "license": "Apache-2.0",
13
+ "repository": { "type": "git", "url": "https://github.com/andymai/brepjs", "directory": "packages/brepjs-bim" },
8
14
  "type": "module",
9
15
  "sideEffects": false,
10
- "engines": { "node": ">=24" },
16
+ "engines": {
17
+ "node": ">=24"
18
+ },
11
19
  "main": "./dist/brepjs-bim.cjs",
12
20
  "module": "./dist/brepjs-bim.js",
13
21
  "types": "./dist/index.d.ts",
14
22
  "exports": {
15
23
  ".": {
16
- "import": { "types": "./dist/index.d.ts", "default": "./dist/brepjs-bim.js" },
17
- "require": { "types": "./dist/index.d.ts", "default": "./dist/brepjs-bim.cjs" }
24
+ "import": {
25
+ "types": "./dist/index.d.ts",
26
+ "default": "./dist/brepjs-bim.js"
27
+ },
28
+ "require": {
29
+ "types": "./dist/index.d.ts",
30
+ "default": "./dist/brepjs-bim.cjs"
31
+ }
18
32
  }
19
33
  },
20
- "files": ["dist", "LICENSE"],
34
+ "files": [
35
+ "dist",
36
+ "LICENSE"
37
+ ],
21
38
  "scripts": {
22
39
  "build": "vite build",
23
40
  "typecheck": "tsc --noEmit",