@ifc-lite/viewer 1.11.5 → 1.14.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (36) hide show
  1. package/CHANGELOG.md +91 -0
  2. package/dist/assets/{Arrow.dom-HFGUoQyp.js → Arrow.dom-CNguvlQi.js} +1 -1
  3. package/dist/assets/{browser-CllJKxsx.js → browser-D6lgLpkA.js} +1 -1
  4. package/dist/assets/{index-CQd80vMv.js → index-BMwpw264.js} +4 -4
  5. package/dist/assets/index-Qp8stcGO.css +1 -0
  6. package/dist/assets/{index-B69WAU-m.js → index-UaDsJsCR.js} +26434 -23023
  7. package/dist/assets/{native-bridge-Bu4SptAa.js → native-bridge-DqELq4X0.js} +1 -1
  8. package/dist/assets/{wasm-bridge-CR2KvcQN.js → wasm-bridge-CVWvHlfH.js} +1 -1
  9. package/dist/index.html +2 -2
  10. package/package.json +19 -19
  11. package/src/App.tsx +2 -0
  12. package/src/components/ui/toast.tsx +121 -0
  13. package/src/components/viewer/BulkPropertyEditor.tsx +8 -1
  14. package/src/components/viewer/DataConnector.tsx +8 -1
  15. package/src/components/viewer/ExportChangesButton.tsx +11 -2
  16. package/src/components/viewer/ExportDialog.tsx +224 -132
  17. package/src/components/viewer/MainToolbar.tsx +9 -2
  18. package/src/components/viewer/PropertiesPanel.tsx +300 -15
  19. package/src/components/viewer/properties/BsddCard.tsx +507 -0
  20. package/src/components/viewer/properties/QuantitySetCard.tsx +1 -0
  21. package/src/components/viewer/useGeometryStreaming.ts +4 -4
  22. package/src/index.css +7 -0
  23. package/src/lib/scripts/templates/bim-globals.d.ts +33 -0
  24. package/src/lib/scripts/templates/create-building.ts +491 -0
  25. package/src/lib/scripts/templates.ts +8 -0
  26. package/src/sdk/adapters/export-adapter.ts +84 -0
  27. package/src/sdk/adapters/lens-adapter.ts +1 -1
  28. package/src/sdk/adapters/model-adapter.ts +8 -0
  29. package/src/sdk/adapters/viewer-adapter.ts +1 -1
  30. package/src/services/bsdd.ts +262 -0
  31. package/src/store/index.ts +2 -2
  32. package/src/store/slices/measurementSlice.test.ts +22 -22
  33. package/src/store/slices/modelSlice.test.ts +2 -0
  34. package/src/store/slices/mutationSlice.ts +155 -1
  35. package/vite.config.ts +7 -0
  36. package/dist/assets/index-BoYyWYAu.css +0 -1
@@ -0,0 +1,491 @@
1
+ /* This Source Code Form is subject to the terms of the Mozilla Public
2
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
3
+ * file, You can obtain one at https://mozilla.org/MPL/2.0/. */
4
+
5
+ export {} // module boundary for type checking
6
+ /**
7
+ * Create IFC from scratch — generate a fully attributed building
8
+ *
9
+ * Demonstrates the bim.create API: build a complete IFC file with
10
+ * walls, slab, columns, beams, stair, and a parametric timber gridshell
11
+ * roof — each with material colours, standard IFC property sets, and
12
+ * base quantities. The roof is a doubly-curved diamond lattice of
13
+ * small-diameter timber laths generated from a mathematical surface.
14
+ */
15
+
16
+ // ─── 1. Project ─────────────────────────────────────────────────────────
17
+
18
+ const h = bim.create.project({
19
+ Name: 'Sample Building',
20
+ Description: 'Demonstration of ifc-lite IFC creation from scratch',
21
+ Author: 'ifc-lite',
22
+ Organization: 'ifc-lite',
23
+ });
24
+
25
+ // ─── 2. Storey ──────────────────────────────────────────────────────────
26
+
27
+ const gf = bim.create.addIfcBuildingStorey(h, { Name: 'Ground Floor', Elevation: 0 });
28
+
29
+ // ─── 3. Walls — 5 × 8 m footprint, 3 m high, 0.2 m thick ─────────────
30
+
31
+ const southWall = bim.create.addIfcWall(h, gf, {
32
+ Name: 'South Wall', Description: 'Exterior south façade', ObjectType: 'Basic Wall:Exterior - 200mm',
33
+ Start: [0, 0, 0], End: [5, 0, 0], Thickness: 0.2, Height: 3,
34
+ Openings: [
35
+ { Name: 'W-01 Window', Width: 1.2, Height: 1.5, Position: [1.5, 0, 0.9] },
36
+ ],
37
+ });
38
+ const eastWall = bim.create.addIfcWall(h, gf, {
39
+ Name: 'East Wall', Description: 'Exterior east façade', ObjectType: 'Basic Wall:Exterior - 200mm',
40
+ Start: [5, 0, 0], End: [5, 8, 0], Thickness: 0.2, Height: 3,
41
+ Openings: [
42
+ { Name: 'D-01 Entrance Door', Width: 0.9, Height: 2.1, Position: [3, 0, 0] },
43
+ ],
44
+ });
45
+ const northWall = bim.create.addIfcWall(h, gf, {
46
+ Name: 'North Wall', Description: 'Exterior north façade', ObjectType: 'Basic Wall:Exterior - 200mm',
47
+ Start: [5, 8, 0], End: [0, 8, 0], Thickness: 0.2, Height: 3,
48
+ Openings: [
49
+ { Name: 'W-02 Window', Width: 1.4, Height: 1.5, Position: [1.8, 0, 0.9] },
50
+ ],
51
+ });
52
+ const westWall = bim.create.addIfcWall(h, gf, {
53
+ Name: 'West Wall', Description: 'Exterior west façade', ObjectType: 'Basic Wall:Exterior - 200mm',
54
+ Start: [0, 8, 0], End: [0, 0, 0], Thickness: 0.2, Height: 3,
55
+ });
56
+
57
+ // Wall colours and materials
58
+ for (const wId of [southWall, eastWall, northWall, westWall]) {
59
+ bim.create.setColor(h, wId, 'Plaster - Beige', [0.92, 0.88, 0.80]);
60
+ bim.create.addIfcMaterial(h, wId, {
61
+ Name: 'Exterior Wall Assembly',
62
+ Layers: [
63
+ { Name: 'Gypsum Board', Thickness: 0.013, Category: 'Finish' },
64
+ { Name: 'Mineral Wool Insulation', Thickness: 0.08, Category: 'Insulation' },
65
+ { Name: 'Concrete C30/37', Thickness: 0.2, Category: 'Structural' },
66
+ { Name: 'External Render', Thickness: 0.015, Category: 'Finish' },
67
+ ],
68
+ });
69
+ }
70
+
71
+ // Wall properties & quantities (all walls share the same spec)
72
+ for (const [wId, wName, wLen] of [
73
+ [southWall, 'South Wall', 5],
74
+ [eastWall, 'East Wall', 8],
75
+ [northWall, 'North Wall', 5],
76
+ [westWall, 'West Wall', 8],
77
+ ] as [number, string, number][]) {
78
+ bim.create.addIfcPropertySet(h, wId, {
79
+ Name: 'Pset_WallCommon',
80
+ Properties: [
81
+ { Name: 'Reference', NominalValue: 'Exterior - 200mm', Type: 'IfcIdentifier' },
82
+ { Name: 'IsExternal', NominalValue: true, Type: 'IfcBoolean' },
83
+ { Name: 'LoadBearing', NominalValue: true, Type: 'IfcBoolean' },
84
+ { Name: 'FireRating', NominalValue: 'REI60', Type: 'IfcLabel' },
85
+ { Name: 'AcousticRating', NominalValue: 'STC 45', Type: 'IfcLabel' },
86
+ { Name: 'ThermalTransmittance', NominalValue: 0.25, Type: 'IfcReal' },
87
+ ],
88
+ });
89
+ bim.create.addIfcElementQuantity(h, wId, {
90
+ Name: 'Qto_WallBaseQuantities',
91
+ Quantities: [
92
+ { Name: 'Length', Value: wLen, Kind: 'IfcQuantityLength' },
93
+ { Name: 'Height', Value: 3, Kind: 'IfcQuantityLength' },
94
+ { Name: 'Width', Value: 0.2, Kind: 'IfcQuantityLength' },
95
+ { Name: 'GrossSideArea', Value: wLen * 3, Kind: 'IfcQuantityArea' },
96
+ { Name: 'GrossVolume', Value: wLen * 3 * 0.2, Kind: 'IfcQuantityVolume' },
97
+ ],
98
+ });
99
+ }
100
+
101
+ // ─── 4. Floor slab ──────────────────────────────────────────────────────
102
+
103
+ const slab = bim.create.addIfcSlab(h, gf, {
104
+ Name: 'Ground Floor Slab', Description: 'Reinforced concrete floor slab', ObjectType: 'Floor:Concrete - 300mm',
105
+ Position: [0, 0, -0.3], Thickness: 0.3, Width: 5, Depth: 8,
106
+ });
107
+ bim.create.setColor(h, slab, 'Concrete - Grey', [0.65, 0.65, 0.65]);
108
+ bim.create.addIfcMaterial(h, slab, {
109
+ Name: 'Floor Slab Assembly',
110
+ Layers: [
111
+ { Name: 'Ceramic Tile', Thickness: 0.01, Category: 'Finish' },
112
+ { Name: 'Screed', Thickness: 0.05, Category: 'Finish' },
113
+ { Name: 'Reinforced Concrete C30/37', Thickness: 0.24, Category: 'Structural' },
114
+ ],
115
+ });
116
+ bim.create.addIfcPropertySet(h, slab, {
117
+ Name: 'Pset_SlabCommon',
118
+ Properties: [
119
+ { Name: 'Reference', NominalValue: 'Concrete - 300mm', Type: 'IfcIdentifier' },
120
+ { Name: 'IsExternal', NominalValue: false, Type: 'IfcBoolean' },
121
+ { Name: 'LoadBearing', NominalValue: true, Type: 'IfcBoolean' },
122
+ { Name: 'FireRating', NominalValue: 'REI90', Type: 'IfcLabel' },
123
+ { Name: 'AcousticRating', NominalValue: 'STC 52', Type: 'IfcLabel' },
124
+ { Name: 'Combustible', NominalValue: false, Type: 'IfcBoolean' },
125
+ ],
126
+ });
127
+ bim.create.addIfcElementQuantity(h, slab, {
128
+ Name: 'Qto_SlabBaseQuantities',
129
+ Quantities: [
130
+ { Name: 'Width', Value: 0.3, Kind: 'IfcQuantityLength' },
131
+ { Name: 'GrossArea', Value: 40, Kind: 'IfcQuantityArea' },
132
+ { Name: 'NetArea', Value: 40, Kind: 'IfcQuantityArea' },
133
+ { Name: 'GrossVolume', Value: 12, Kind: 'IfcQuantityVolume' },
134
+ { Name: 'GrossWeight', Value: 28800, Kind: 'IfcQuantityWeight' },
135
+ ],
136
+ });
137
+
138
+ // ─── 5. Columns at corners ──────────────────────────────────────────────
139
+
140
+ const columnPositions: [string, number, number][] = [
141
+ ['C-01 SW', 0.1, 0.1],
142
+ ['C-02 SE', 4.7, 0.1],
143
+ ['C-03 NE', 4.7, 7.7],
144
+ ['C-04 NW', 0.1, 7.7],
145
+ ];
146
+ for (const [cName, cx, cy] of columnPositions) {
147
+ const colId = bim.create.addIfcColumn(h, gf, {
148
+ Name: cName, Description: 'Reinforced concrete column', ObjectType: 'Column:Concrete 300x300',
149
+ Position: [cx, cy, 0], Width: 0.3, Depth: 0.3, Height: 3,
150
+ });
151
+ bim.create.setColor(h, colId, 'Concrete - Light', [0.72, 0.72, 0.74]);
152
+ bim.create.addIfcMaterial(h, colId, { Name: 'Reinforced Concrete C30/37', Category: 'Concrete' });
153
+ bim.create.addIfcPropertySet(h, colId, {
154
+ Name: 'Pset_ColumnCommon',
155
+ Properties: [
156
+ { Name: 'Reference', NominalValue: 'Concrete 300x300', Type: 'IfcIdentifier' },
157
+ { Name: 'LoadBearing', NominalValue: true, Type: 'IfcBoolean' },
158
+ { Name: 'IsExternal', NominalValue: false, Type: 'IfcBoolean' },
159
+ { Name: 'FireRating', NominalValue: 'R120', Type: 'IfcLabel' },
160
+ { Name: 'Slope', NominalValue: 0, Type: 'IfcInteger' },
161
+ ],
162
+ });
163
+ bim.create.addIfcElementQuantity(h, colId, {
164
+ Name: 'Qto_ColumnBaseQuantities',
165
+ Quantities: [
166
+ { Name: 'Length', Value: 3, Kind: 'IfcQuantityLength' },
167
+ { Name: 'CrossSectionArea', Value: 0.09, Kind: 'IfcQuantityArea' },
168
+ { Name: 'GrossVolume', Value: 0.27, Kind: 'IfcQuantityVolume' },
169
+ { Name: 'GrossWeight', Value: 648, Kind: 'IfcQuantityWeight' },
170
+ ],
171
+ });
172
+ }
173
+
174
+ // ─── 6. Beams along the top ─────────────────────────────────────────────
175
+
176
+ const beamDefs: [string, [number, number, number], [number, number, number]][] = [
177
+ ['B-01 South Beam', [0, 0, 3], [5, 0, 3]],
178
+ ['B-02 North Beam', [0, 8, 3], [5, 8, 3]],
179
+ ];
180
+ for (const [bName, bStart, bEnd] of beamDefs) {
181
+ const bLen = Math.sqrt(
182
+ (bEnd[0] - bStart[0]) ** 2 + (bEnd[1] - bStart[1]) ** 2 + (bEnd[2] - bStart[2]) ** 2,
183
+ );
184
+ const beamId = bim.create.addIfcBeam(h, gf, {
185
+ Name: bName, Description: 'Steel I-beam', ObjectType: 'Beam:IPE 200',
186
+ Start: bStart, End: bEnd, Width: 0.2, Height: 0.4,
187
+ });
188
+ bim.create.setColor(h, beamId, 'Steel - Grey', [0.55, 0.55, 0.58]);
189
+ bim.create.addIfcMaterial(h, beamId, { Name: 'Structural Steel S235', Category: 'Steel' });
190
+ bim.create.addIfcPropertySet(h, beamId, {
191
+ Name: 'Pset_BeamCommon',
192
+ Properties: [
193
+ { Name: 'Reference', NominalValue: 'IPE 200', Type: 'IfcIdentifier' },
194
+ { Name: 'LoadBearing', NominalValue: true, Type: 'IfcBoolean' },
195
+ { Name: 'IsExternal', NominalValue: false, Type: 'IfcBoolean' },
196
+ { Name: 'FireRating', NominalValue: 'R60', Type: 'IfcLabel' },
197
+ { Name: 'Span', NominalValue: bLen, Type: 'IfcReal' },
198
+ ],
199
+ });
200
+ bim.create.addIfcElementQuantity(h, beamId, {
201
+ Name: 'Qto_BeamBaseQuantities',
202
+ Quantities: [
203
+ { Name: 'Length', Value: bLen, Kind: 'IfcQuantityLength' },
204
+ { Name: 'CrossSectionArea', Value: 0.08, Kind: 'IfcQuantityArea' },
205
+ { Name: 'GrossVolume', Value: bLen * 0.08, Kind: 'IfcQuantityVolume' },
206
+ { Name: 'GrossWeight', Value: bLen * 0.08 * 7850, Kind: 'IfcQuantityWeight' },
207
+ ],
208
+ });
209
+ }
210
+
211
+ // ─── 7. Stair ───────────────────────────────────────────────────────────
212
+
213
+ const numRisers = 17;
214
+ const riserH = 0.176;
215
+ const treadL = 0.28;
216
+ const stairW = 1.0;
217
+ // Run along +Y (the 8 m axis) so the 4.76 m run fits comfortably.
218
+ // Position is stair origin; with Direction = PI/2 the width extends toward −X.
219
+ // Place at x = stairW so the stair sits flush against the west wall (x = 0).
220
+ const stairId = bim.create.addIfcStair(h, gf, {
221
+ Name: 'ST-01 Main Stair', Description: 'Straight-run concrete stair', ObjectType: 'Stair:Concrete - Straight Run',
222
+ Position: [stairW, 1, 0], Direction: Math.PI / 2,
223
+ NumberOfRisers: numRisers, RiserHeight: riserH, TreadLength: treadL, Width: stairW,
224
+ });
225
+ bim.create.setColor(h, stairId, 'Concrete - Warm', [0.80, 0.78, 0.74]);
226
+ bim.create.addIfcMaterial(h, stairId, { Name: 'Reinforced Concrete C25/30', Category: 'Concrete' });
227
+ bim.create.addIfcPropertySet(h, stairId, {
228
+ Name: 'Pset_StairCommon',
229
+ Properties: [
230
+ { Name: 'Reference', NominalValue: 'Concrete - Straight Run', Type: 'IfcIdentifier' },
231
+ { Name: 'FireRating', NominalValue: 'REI60', Type: 'IfcLabel' },
232
+ { Name: 'NumberOfRiser', NominalValue: numRisers, Type: 'IfcInteger' },
233
+ { Name: 'NumberOfTreads', NominalValue: numRisers - 1, Type: 'IfcInteger' },
234
+ { Name: 'RiserHeight', NominalValue: riserH, Type: 'IfcReal' },
235
+ { Name: 'TreadLength', NominalValue: treadL, Type: 'IfcReal' },
236
+ { Name: 'IsExternal', NominalValue: false, Type: 'IfcBoolean' },
237
+ { Name: 'HandicapAccessible', NominalValue: false, Type: 'IfcBoolean' },
238
+ ],
239
+ });
240
+ bim.create.addIfcElementQuantity(h, stairId, {
241
+ Name: 'Qto_StairBaseQuantities',
242
+ Quantities: [
243
+ { Name: 'Length', Value: numRisers * treadL, Kind: 'IfcQuantityLength' },
244
+ { Name: 'GrossVolume', Value: numRisers * treadL * stairW * riserH * 0.5, Kind: 'IfcQuantityVolume' },
245
+ ],
246
+ });
247
+
248
+ // ─── 8. Second floor ─────────────────────────────────────────────────
249
+
250
+ const ff = bim.create.addIfcBuildingStorey(h, { Name: 'First Floor', Elevation: 3 });
251
+
252
+ // Floor slab at z = 3 with stair opening
253
+ // Opening must be long enough for headroom (≥ 2.1 m) over the ascending stair.
254
+ // At the slab level the stair is still rising, so the opening needs to extend
255
+ // back far enough that a person standing on a lower step has full clearance.
256
+ const stairRunEnd = 1 + numRisers * treadL; // y ≈ 5.76
257
+ const stairOpenLen = 4.2; // ~1.5 × run ensures headroom
258
+ const stairOpenY = stairRunEnd - stairOpenLen / 2; // center near arrival end
259
+ const ffSlab = bim.create.addIfcSlab(h, ff, {
260
+ Name: 'First Floor Slab', Description: 'Reinforced concrete floor slab', ObjectType: 'Floor:Concrete - 300mm',
261
+ Position: [0, 0, 2.7], Thickness: 0.3, Width: 5, Depth: 8,
262
+ Openings: [
263
+ { Name: 'Stair Opening', Width: stairW + 0.2, Height: stairOpenLen, Position: [stairW / 2, stairOpenY, 0] },
264
+ ],
265
+ });
266
+ bim.create.setColor(h, ffSlab, 'Concrete - Grey', [0.65, 0.65, 0.65]);
267
+ bim.create.addIfcMaterial(h, ffSlab, {
268
+ Name: 'Floor Slab Assembly',
269
+ Layers: [
270
+ { Name: 'Ceramic Tile', Thickness: 0.01, Category: 'Finish' },
271
+ { Name: 'Screed', Thickness: 0.05, Category: 'Finish' },
272
+ { Name: 'Reinforced Concrete C30/37', Thickness: 0.24, Category: 'Structural' },
273
+ ],
274
+ });
275
+ bim.create.addIfcPropertySet(h, ffSlab, {
276
+ Name: 'Pset_SlabCommon',
277
+ Properties: [
278
+ { Name: 'Reference', NominalValue: 'Concrete - 300mm', Type: 'IfcIdentifier' },
279
+ { Name: 'IsExternal', NominalValue: false, Type: 'IfcBoolean' },
280
+ { Name: 'LoadBearing', NominalValue: true, Type: 'IfcBoolean' },
281
+ { Name: 'FireRating', NominalValue: 'REI90', Type: 'IfcLabel' },
282
+ ],
283
+ });
284
+
285
+ // Second floor walls — same footprint, window on east instead of door
286
+ const ffSouthWall = bim.create.addIfcWall(h, ff, {
287
+ Name: 'South Wall', Description: 'Exterior south façade', ObjectType: 'Basic Wall:Exterior - 200mm',
288
+ Start: [0, 0, 3], End: [5, 0, 3], Thickness: 0.2, Height: 3,
289
+ Openings: [
290
+ { Name: 'W-03 Window', Width: 1.2, Height: 1.5, Position: [1.5, 0, 0.9] },
291
+ ],
292
+ });
293
+ const ffEastWall = bim.create.addIfcWall(h, ff, {
294
+ Name: 'East Wall', Description: 'Exterior east façade', ObjectType: 'Basic Wall:Exterior - 200mm',
295
+ Start: [5, 0, 3], End: [5, 8, 3], Thickness: 0.2, Height: 3,
296
+ Openings: [
297
+ { Name: 'W-04 Window', Width: 1.4, Height: 1.5, Position: [3, 0, 0.9] },
298
+ ],
299
+ });
300
+ const ffNorthWall = bim.create.addIfcWall(h, ff, {
301
+ Name: 'North Wall', Description: 'Exterior north façade', ObjectType: 'Basic Wall:Exterior - 200mm',
302
+ Start: [5, 8, 3], End: [0, 8, 3], Thickness: 0.2, Height: 3,
303
+ Openings: [
304
+ { Name: 'W-05 Window', Width: 1.4, Height: 1.5, Position: [1.8, 0, 0.9] },
305
+ ],
306
+ });
307
+ const ffWestWall = bim.create.addIfcWall(h, ff, {
308
+ Name: 'West Wall', Description: 'Exterior west façade', ObjectType: 'Basic Wall:Exterior - 200mm',
309
+ Start: [0, 8, 3], End: [0, 0, 3], Thickness: 0.2, Height: 3,
310
+ });
311
+
312
+ for (const wId of [ffSouthWall, ffEastWall, ffNorthWall, ffWestWall]) {
313
+ bim.create.setColor(h, wId, 'Plaster - Beige', [0.92, 0.88, 0.80]);
314
+ bim.create.addIfcMaterial(h, wId, {
315
+ Name: 'Exterior Wall Assembly',
316
+ Layers: [
317
+ { Name: 'Gypsum Board', Thickness: 0.013, Category: 'Finish' },
318
+ { Name: 'Mineral Wool Insulation', Thickness: 0.08, Category: 'Insulation' },
319
+ { Name: 'Concrete C30/37', Thickness: 0.2, Category: 'Structural' },
320
+ { Name: 'External Render', Thickness: 0.015, Category: 'Finish' },
321
+ ],
322
+ });
323
+ }
324
+
325
+ for (const [wId, wName, wLen] of [
326
+ [ffSouthWall, 'South Wall', 5],
327
+ [ffEastWall, 'East Wall', 8],
328
+ [ffNorthWall, 'North Wall', 5],
329
+ [ffWestWall, 'West Wall', 8],
330
+ ] as [number, string, number][]) {
331
+ bim.create.addIfcPropertySet(h, wId, {
332
+ Name: 'Pset_WallCommon',
333
+ Properties: [
334
+ { Name: 'Reference', NominalValue: 'Exterior - 200mm', Type: 'IfcIdentifier' },
335
+ { Name: 'IsExternal', NominalValue: true, Type: 'IfcBoolean' },
336
+ { Name: 'LoadBearing', NominalValue: true, Type: 'IfcBoolean' },
337
+ { Name: 'FireRating', NominalValue: 'REI60', Type: 'IfcLabel' },
338
+ { Name: 'ThermalTransmittance', NominalValue: 0.25, Type: 'IfcReal' },
339
+ ],
340
+ });
341
+ bim.create.addIfcElementQuantity(h, wId, {
342
+ Name: 'Qto_WallBaseQuantities',
343
+ Quantities: [
344
+ { Name: 'Length', Value: wLen, Kind: 'IfcQuantityLength' },
345
+ { Name: 'Height', Value: 3, Kind: 'IfcQuantityLength' },
346
+ { Name: 'Width', Value: 0.2, Kind: 'IfcQuantityLength' },
347
+ { Name: 'GrossSideArea', Value: wLen * 3, Kind: 'IfcQuantityArea' },
348
+ { Name: 'GrossVolume', Value: wLen * 3 * 0.2, Kind: 'IfcQuantityVolume' },
349
+ ],
350
+ });
351
+ }
352
+
353
+ // Second floor columns
354
+ for (const [cName, cx, cy] of columnPositions) {
355
+ const colId = bim.create.addIfcColumn(h, ff, {
356
+ Name: cName, Description: 'Reinforced concrete column', ObjectType: 'Column:Concrete 300x300',
357
+ Position: [cx, cy, 3], Width: 0.3, Depth: 0.3, Height: 3,
358
+ });
359
+ bim.create.setColor(h, colId, 'Concrete - Light', [0.72, 0.72, 0.74]);
360
+ bim.create.addIfcMaterial(h, colId, { Name: 'Reinforced Concrete C30/37', Category: 'Concrete' });
361
+ }
362
+
363
+ // Second floor beams
364
+ for (const [bName, bStart, bEnd] of beamDefs) {
365
+ const beamId = bim.create.addIfcBeam(h, ff, {
366
+ Name: bName, Description: 'Steel I-beam', ObjectType: 'Beam:IPE 200',
367
+ Start: [bStart[0], bStart[1], 6], End: [bEnd[0], bEnd[1], 6], Width: 0.2, Height: 0.4,
368
+ });
369
+ bim.create.setColor(h, beamId, 'Steel - Grey', [0.55, 0.55, 0.58]);
370
+ bim.create.addIfcMaterial(h, beamId, { Name: 'Structural Steel S235', Category: 'Steel' });
371
+ }
372
+
373
+ // ─── 9. Parametric timber gridshell roof ─────────────────────────────
374
+ //
375
+ // A doubly-curved gridshell spanning the 5 × 8 m footprint.
376
+ // Two families of diagonal timber laths form a diamond lattice.
377
+ // Three surface shapes are provided — uncomment one at a time.
378
+
379
+ const ROOF_W = 5; // building width (X)
380
+ const ROOF_D = 8; // building depth (Y)
381
+ const WALL_H = 6; // wall-top elevation (2 storeys × 3 m)
382
+ const CROWN = 1.8; // rise above walls
383
+ const NU = 10; // grid divisions along X
384
+ const NV = 16; // grid divisions along Y
385
+ const LATH_W = 0.06; // lath width 60 mm
386
+ const LATH_D = 0.08; // lath depth 80 mm
387
+
388
+ // ── Shape A: sinusoidal dome — smooth symmetric shell ──────────────
389
+ // function roofZ(u: number, v: number): number {
390
+ // return WALL_H + CROWN * Math.sin(Math.PI * u) * Math.sin(Math.PI * v);
391
+ // }
392
+
393
+ // ── Shape B: rolling dunes — three asymmetric peaks ────────────────
394
+ // function roofZ(u: number, v: number): number {
395
+ // const env = Math.sin(Math.PI * u) * Math.sin(Math.PI * v);
396
+ // const p1 = 1.0 * Math.exp(-((u - 0.3) ** 2 + (v - 0.30) ** 2) / 0.04);
397
+ // const p2 = 0.65 * Math.exp(-((u - 0.72) ** 2 + (v - 0.55) ** 2) / 0.06);
398
+ // const p3 = 0.45 * Math.exp(-((u - 0.40) ** 2 + (v - 0.80) ** 2) / 0.03);
399
+ // return WALL_H + CROWN * env * (0.25 + p1 + p2 + p3);
400
+ // }
401
+
402
+ // ── Shape C: twisted hypar — saddle with raised diagonal corners ───
403
+ function roofZ(u: number, v: number): number {
404
+ const env = Math.sin(Math.PI * u) * Math.sin(Math.PI * v);
405
+ const twist = (2 * u - 1) * (2 * v - 1);
406
+ return WALL_H + CROWN * env * (1.0 + 0.8 * twist);
407
+ }
408
+ // Build grid of surface points
409
+ const grid: [number, number, number][][] = [];
410
+ for (let i = 0; i <= NU; i++) {
411
+ const row: [number, number, number][] = [];
412
+ for (let j = 0; j <= NV; j++) {
413
+ const u = i / NU, v = j / NV;
414
+ row.push([u * ROOF_W, v * ROOF_D, roofZ(u, v)]);
415
+ }
416
+ grid.push(row);
417
+ }
418
+
419
+ let lathCount = 0;
420
+
421
+ // Family A — diagonal (i+1, j+1): warm larch tone
422
+ for (let d = -NV; d <= NU; d++) {
423
+ const i0 = Math.max(0, d);
424
+ const i1 = Math.min(NU, NV + d);
425
+ for (let i = i0; i < i1; i++) {
426
+ const j = i - d;
427
+ if (j < 0 || j >= NV) continue;
428
+ const s = grid[i][j], e = grid[i + 1][j + 1];
429
+ const id = bim.create.addIfcBeam(h, ff, {
430
+ Name: `GL-A${(d + NV).toString().padStart(2, '0')}/${i - i0}`,
431
+ Description: 'Gridshell lath family A', ObjectType: 'Timber Lath:GL24h 60x80',
432
+ Start: s, End: e, Width: LATH_W, Height: LATH_D,
433
+ });
434
+ bim.create.setColor(h, id, 'Timber - Larch', [0.82, 0.62, 0.38]);
435
+ bim.create.addIfcMaterial(h, id, { Name: 'Glulam GL24h Spruce', Category: 'Timber' });
436
+ lathCount++;
437
+ }
438
+ }
439
+
440
+ // Family B — diagonal (i+1, j-1): darker oak tone
441
+ for (let s = 0; s <= NU + NV; s++) {
442
+ const i0 = Math.max(0, s - NV);
443
+ const i1 = Math.min(NU, s);
444
+ for (let i = i0; i < i1; i++) {
445
+ const j = s - i;
446
+ if (j <= 0 || j > NV) continue;
447
+ const p0 = grid[i][j], p1 = grid[i + 1][j - 1];
448
+ const id = bim.create.addIfcBeam(h, ff, {
449
+ Name: `GL-B${s.toString().padStart(2, '0')}/${i - i0}`,
450
+ Description: 'Gridshell lath family B', ObjectType: 'Timber Lath:GL24h 60x80',
451
+ Start: p0, End: p1, Width: LATH_W, Height: LATH_D,
452
+ });
453
+ bim.create.setColor(h, id, 'Timber - Oak', [0.72, 0.52, 0.30]);
454
+ bim.create.addIfcMaterial(h, id, { Name: 'Glulam GL24h Spruce', Category: 'Timber' });
455
+ lathCount++;
456
+ }
457
+ }
458
+
459
+ // Perimeter ring beam at wall top — ties laths together
460
+ const RING_N = 2 * (NU + NV);
461
+ const ringPts: [number, number, number][] = [];
462
+ for (let i = 0; i <= NU; i++) ringPts.push(grid[i][0]); // south edge
463
+ for (let j = 1; j <= NV; j++) ringPts.push(grid[NU][j]); // east edge
464
+ for (let i = NU - 1; i >= 0; i--) ringPts.push(grid[i][NV]); // north edge
465
+ for (let j = NV - 1; j >= 1; j--) ringPts.push(grid[0][j]); // west edge
466
+ for (let k = 0; k < ringPts.length; k++) {
467
+ const rStart = ringPts[k];
468
+ const rEnd = ringPts[(k + 1) % ringPts.length];
469
+ const rId = bim.create.addIfcBeam(h, ff, {
470
+ Name: `GL-Ring/${k.toString().padStart(2, '0')}`,
471
+ Description: 'Gridshell perimeter ring beam', ObjectType: 'Beam:GL28h 120x200',
472
+ Start: rStart, End: rEnd, Width: 0.12, Height: 0.20,
473
+ });
474
+ bim.create.setColor(h, rId, 'Timber - Dark', [0.45, 0.32, 0.20]);
475
+ bim.create.addIfcMaterial(h, rId, { Name: 'Glulam GL28h Larch', Category: 'Timber' });
476
+ }
477
+
478
+ console.log(`Gridshell roof: ${lathCount} laths + ${ringPts.length} ring segments`);
479
+
480
+ // ─── 10. Generate, preview, download ────────────────────────────────────
481
+
482
+ const result = bim.create.toIfc(h);
483
+
484
+ console.log(
485
+ `Created ${result.entities.length} entities, ` +
486
+ `${result.stats.entityCount} STEP lines, ` +
487
+ `${(result.stats.fileSize / 1024).toFixed(1)} KB`,
488
+ );
489
+
490
+ bim.model.loadIfc(result.content, 'sample-building.ifc');
491
+ bim.export.download(result.content, 'sample-building.ifc', 'application/x-step');
@@ -23,6 +23,8 @@ import mepEquipmentSchedule from './templates/mep-equipment-schedule.ts?raw';
23
23
  import spaceValidation from './templates/space-validation.ts?raw';
24
24
  import federationCompare from './templates/federation-compare.ts?raw';
25
25
  import resetView from './templates/reset-view.ts?raw';
26
+ import createBuilding from './templates/create-building.ts?raw';
27
+
26
28
 
27
29
  export interface ScriptTemplate {
28
30
  name: string;
@@ -78,6 +80,12 @@ export const SCRIPT_TEMPLATES: ScriptTemplate[] = [
78
80
  'Project Manager — compare multiple loaded models side by side: entity counts, type coverage, naming consistency, coordination issues',
79
81
  code: stripModuleLine(federationCompare),
80
82
  },
83
+ {
84
+ name: 'Create building (IFC from scratch)',
85
+ description:
86
+ 'Developer — generate a complete IFC file with walls, slab, columns, beams, stair, and parametric timber gridshell roof with diamond lattice',
87
+ code: stripModuleLine(createBuilding),
88
+ },
81
89
  {
82
90
  name: 'Reset view',
83
91
  description: 'Utility — remove all color overrides and show all entities',
@@ -5,6 +5,7 @@
5
5
  import type { StoreApi } from './types.js';
6
6
  import type { EntityRef, EntityData, PropertySetData, QuantitySetData, ExportBackendMethods } from '@ifc-lite/sdk';
7
7
  import { EntityNode } from '@ifc-lite/query';
8
+ import { StepExporter, type StepExportOptions } from '@ifc-lite/export';
8
9
  import { getModelForRef } from './model-compat.js';
9
10
 
10
11
  /** Options for CSV export */
@@ -14,6 +15,26 @@ interface CsvOptions {
14
15
  filename?: string;
15
16
  }
16
17
 
18
+
19
+ /** Options for IFC STEP export */
20
+ interface IfcExportOptions {
21
+ schema?: 'IFC2X3' | 'IFC4' | 'IFC4X3';
22
+ filename?: string;
23
+ includeMutations?: boolean;
24
+ visibleOnly?: boolean;
25
+ }
26
+
27
+ /** Validate that a value is an IfcExportOptions object. */
28
+ function isIfcExportOptions(v: unknown): v is IfcExportOptions {
29
+ if (v === null || typeof v !== 'object') return false;
30
+ const options = v as IfcExportOptions;
31
+ if (options.schema !== undefined && options.schema !== 'IFC2X3' && options.schema !== 'IFC4' && options.schema !== 'IFC4X3') return false;
32
+ if (options.filename !== undefined && typeof options.filename !== 'string') return false;
33
+ if (options.includeMutations !== undefined && typeof options.includeMutations !== 'boolean') return false;
34
+ if (options.visibleOnly !== undefined && typeof options.visibleOnly !== 'boolean') return false;
35
+ return true;
36
+ }
37
+
17
38
  /**
18
39
  * Validate that a value is a CsvOptions object.
19
40
  */
@@ -264,6 +285,66 @@ export function createExportAdapter(store: StoreApi): ExportBackendMethods {
264
285
  return result;
265
286
  },
266
287
 
288
+ ifc(rawRefs: unknown, rawOptions: unknown) {
289
+ const candidateOptions = rawOptions ?? {};
290
+ if (!isEntityRefArray(rawRefs)) {
291
+ throw new Error('export.ifc: first argument must be an array of entity references');
292
+ }
293
+ if (!isIfcExportOptions(candidateOptions)) {
294
+ throw new Error('export.ifc: second argument must be { schema?: IFC2X3|IFC4|IFC4X3, filename?: string, includeMutations?: boolean, visibleOnly?: boolean }');
295
+ }
296
+
297
+ const refs = normalizeRefs(rawRefs);
298
+ if (refs.length === 0) {
299
+ throw new Error('export.ifc: expected at least one entity reference');
300
+ }
301
+
302
+ const modelIds = new Set(refs.map(ref => ref.modelId));
303
+ if (modelIds.size !== 1) {
304
+ throw new Error('export.ifc: all entity references must belong to the same model');
305
+ }
306
+
307
+ const modelId = refs[0].modelId;
308
+ const state = store.getState();
309
+ const model = getModelForRef(state, modelId);
310
+ if (!model?.ifcDataStore) {
311
+ throw new Error(`export.ifc: model '${modelId}' is not loaded`);
312
+ }
313
+
314
+ if (model.ifcDataStore.schemaVersion === 'IFC5') {
315
+ throw new Error('export.ifc: IFC5 export is not supported by STEP exporter, use IFC2X3/IFC4/IFC4X3 models');
316
+ }
317
+
318
+ const options = candidateOptions;
319
+ const selectedExpressIds = new Set(refs.map(ref => ref.expressId));
320
+ const modelHidden = state.hiddenEntitiesByModel.get(modelId);
321
+ const modelIsolated = state.isolatedEntitiesByModel.get(modelId) ?? null;
322
+
323
+ const shouldLimitToSelection = selectedExpressIds.size < model.ifcDataStore.entityCount;
324
+ const visibleOnly = options.visibleOnly === true || shouldLimitToSelection;
325
+ const hiddenEntityIds = shouldLimitToSelection
326
+ ? new Set<number>()
327
+ : new Set<number>(modelHidden ?? []);
328
+ const isolatedEntityIds = shouldLimitToSelection
329
+ ? selectedExpressIds
330
+ : modelIsolated;
331
+
332
+ const exporter = new StepExporter(model.ifcDataStore);
333
+ const exportOptions: StepExportOptions = {
334
+ schema: options.schema ?? model.ifcDataStore.schemaVersion,
335
+ includeGeometry: true,
336
+ includeProperties: true,
337
+ includeQuantities: true,
338
+ includeRelationships: true,
339
+ applyMutations: options.includeMutations ?? true,
340
+ visibleOnly,
341
+ hiddenEntityIds,
342
+ isolatedEntityIds,
343
+ };
344
+
345
+ return exporter.export(exportOptions).content;
346
+ },
347
+
267
348
  download(content: string, filename: string, mimeType?: string) {
268
349
  triggerDownload(content, filename, mimeType ?? 'text/plain');
269
350
  return undefined;
@@ -273,6 +354,9 @@ export function createExportAdapter(store: StoreApi): ExportBackendMethods {
273
354
 
274
355
  /** Trigger a browser file download */
275
356
  function triggerDownload(content: string, filename: string, mimeType: string): void {
357
+ if (typeof document === 'undefined') {
358
+ throw new Error('download() requires a browser environment (document is unavailable)');
359
+ }
276
360
  const blob = new Blob([content], { type: mimeType });
277
361
  const url = URL.createObjectURL(blob);
278
362
  const a = document.createElement('a');
@@ -14,7 +14,7 @@ function isLensConfig(v: unknown): v is Record<string, unknown> {
14
14
  export function createLensAdapter(store: StoreApi): LensBackendMethods {
15
15
  return {
16
16
  presets() {
17
- return BUILTIN_LENSES;
17
+ return [...BUILTIN_LENSES];
18
18
  },
19
19
  create(config: unknown) {
20
20
  if (!isLensConfig(config)) {
@@ -28,5 +28,13 @@ export function createModelAdapter(store: StoreApi): ModelBackendMethods {
28
28
  // For legacy single-model, return the sentinel ID when no active model is set
29
29
  return state.activeModelId ?? (state.models.size === 0 && state.ifcDataStore ? LEGACY_MODEL_ID : null);
30
30
  },
31
+
32
+ loadIfc(content: string, filename: string) {
33
+ // Create a File from IFC content and dispatch the standard load event.
34
+ // MainToolbar listens for 'ifc-lite:load-file' and routes to loadFile().
35
+ const blob = new Blob([content], { type: 'application/x-step' });
36
+ const file = new File([blob], filename || 'created.ifc', { type: 'application/x-step' });
37
+ window.dispatchEvent(new CustomEvent('ifc-lite:load-file', { detail: file }));
38
+ },
31
39
  };
32
40
  }
@@ -2,7 +2,7 @@
2
2
  * License, v. 2.0. If a copy of the MPL was not distributed with this
3
3
  * file, You can obtain one at https://mozilla.org/MPL/2.0/. */
4
4
 
5
- import type { EntityRef, SectionPlane, CameraState, ViewerBackendMethods, RGBAColor } from '@ifc-lite/sdk';
5
+ import type { EntityRef, SectionPlane, CameraState, ViewerBackendMethods } from '@ifc-lite/sdk';
6
6
  import type { StoreApi } from './types.js';
7
7
  import { getModelForRef } from './model-compat.js';
8
8