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 +17 -1
- package/dist/brepjs-bim.cjs +223 -133
- package/dist/brepjs-bim.js +223 -134
- package/dist/identity/ifcGuid.d.ts +1 -0
- package/dist/ifc-writer/ifcWriter.d.ts +5 -1
- package/dist/ifcRuntime.d.ts +20 -0
- package/dist/index.d.ts +2 -0
- package/dist/model/bimModel.d.ts +8 -0
- package/dist/model/treeSummary.d.ts +21 -0
- package/dist/specs/profile.d.ts +2 -0
- package/package.json +23 -6
package/README.md
CHANGED
|
@@ -1,6 +1,10 @@
|
|
|
1
1
|
# brepjs-bim
|
|
2
2
|
|
|
3
|
-
> Experimental,
|
|
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:
|
package/dist/brepjs-bim.cjs
CHANGED
|
@@ -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
|
|
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":
|
|
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
|
-
|
|
5289
|
-
[
|
|
5290
|
-
|
|
5291
|
-
|
|
5292
|
-
|
|
5293
|
-
],
|
|
5294
|
-
[
|
|
5295
|
-
|
|
5296
|
-
|
|
5297
|
-
|
|
5298
|
-
],
|
|
5299
|
-
[
|
|
5300
|
-
|
|
5301
|
-
|
|
5302
|
-
|
|
5303
|
-
|
|
5304
|
-
|
|
5305
|
-
|
|
5306
|
-
|
|
5307
|
-
|
|
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
|
|
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.#
|
|
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
|
-
*
|
|
6774
|
-
*
|
|
6775
|
-
*
|
|
6776
|
-
* (e.g. a future web-ifc
|
|
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
|
-
#
|
|
6779
|
-
if (this.#mvdViewDefinition.length === 0) return bytes;
|
|
6894
|
+
#patchHeader(bytes) {
|
|
6780
6895
|
const HEADER_SCAN = Math.min(bytes.byteLength, 2048);
|
|
6781
|
-
|
|
6782
|
-
if (
|
|
6783
|
-
|
|
6784
|
-
|
|
6785
|
-
}
|
|
6786
|
-
const
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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;
|
package/dist/brepjs-bim.js
CHANGED
|
@@ -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
|
|
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":
|
|
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
|
-
|
|
5266
|
-
[
|
|
5267
|
-
|
|
5268
|
-
|
|
5269
|
-
|
|
5270
|
-
],
|
|
5271
|
-
[
|
|
5272
|
-
|
|
5273
|
-
|
|
5274
|
-
|
|
5275
|
-
],
|
|
5276
|
-
[
|
|
5277
|
-
|
|
5278
|
-
|
|
5279
|
-
|
|
5280
|
-
|
|
5281
|
-
|
|
5282
|
-
|
|
5283
|
-
|
|
5284
|
-
|
|
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
|
|
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.#
|
|
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
|
-
*
|
|
6751
|
-
*
|
|
6752
|
-
*
|
|
6753
|
-
* (e.g. a future web-ifc
|
|
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
|
-
#
|
|
6756
|
-
if (this.#mvdViewDefinition.length === 0) return bytes;
|
|
6871
|
+
#patchHeader(bytes) {
|
|
6757
6872
|
const HEADER_SCAN = Math.min(bytes.byteLength, 2048);
|
|
6758
|
-
|
|
6759
|
-
if (
|
|
6760
|
-
|
|
6761
|
-
|
|
6762
|
-
}
|
|
6763
|
-
const
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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 };
|
|
@@ -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';
|
package/dist/model/bimModel.d.ts
CHANGED
|
@@ -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
|
+
}
|
package/dist/specs/profile.d.ts
CHANGED
|
@@ -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.
|
|
3
|
+
"version": "0.2.0",
|
|
4
4
|
"description": "BIM layer for brepjs — IFC4-aligned parametric building elements",
|
|
5
|
-
"keywords": [
|
|
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": {
|
|
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": {
|
|
17
|
-
|
|
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": [
|
|
34
|
+
"files": [
|
|
35
|
+
"dist",
|
|
36
|
+
"LICENSE"
|
|
37
|
+
],
|
|
21
38
|
"scripts": {
|
|
22
39
|
"build": "vite build",
|
|
23
40
|
"typecheck": "tsc --noEmit",
|