@ifc-lite/create 1.14.2 → 1.14.4

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.
@@ -4,15 +4,6 @@
4
4
  // ============================================================================
5
5
  // Internal helpers
6
6
  // ============================================================================
7
- /** Generate a 22-character IFC GlobalId (base64-ish) */
8
- function newGlobalId() {
9
- const chars = '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz_$';
10
- let result = '';
11
- for (let i = 0; i < 22; i++) {
12
- result += chars[Math.floor(Math.random() * 64)];
13
- }
14
- return result;
15
- }
16
7
  /** Escape a string for STEP format */
17
8
  function esc(str) {
18
9
  return str.replace(/\\/g, '\\\\').replace(/'/g, "''");
@@ -33,6 +24,14 @@ function num(v) {
33
24
  function vecLen(v) {
34
25
  return Math.sqrt(v[0] * v[0] + v[1] * v[1] + v[2] * v[2]);
35
26
  }
27
+ /**
28
+ * IFC types that do NOT follow the IfcElement attribute layout (no Tag/PredefinedType).
29
+ * addElement/addAxisElement skip Tag+PredefinedType for these types.
30
+ */
31
+ const NON_ELEMENT_TYPES = new Set([
32
+ 'IFCBUILDING', 'IFCSITE', 'IFCBUILDINGSTOREY', 'IFCPROJECT',
33
+ 'IFCSPACE', 'IFCZONE', 'IFCSYSTEM', 'IFCGROUP',
34
+ ]);
36
35
  /** Normalize vector — throws on zero-length (indicates geometry bug like Start === End) */
37
36
  function vecNorm(v) {
38
37
  const len = vecLen(v);
@@ -56,6 +55,8 @@ export class IfcCreator {
56
55
  lines = [];
57
56
  entities = [];
58
57
  schema;
58
+ /** Track generated GlobalIds to guarantee uniqueness per IfcCreator instance */
59
+ usedGlobalIds = new Set();
59
60
  // Shared entity IDs (created in constructor)
60
61
  projectId = 0;
61
62
  siteId = 0;
@@ -82,28 +83,68 @@ export class IfcCreator {
82
83
  // Tracking for spatial aggregation
83
84
  storeyIds = [];
84
85
  storeyElements = new Map();
86
+ /** Storey expressId → its IfcLocalPlacement id (at [0,0,elevation]) */
87
+ storeyPlacements = new Map();
88
+ /** Element expressId → containing storey expressId */
89
+ elementStoreys = new Map();
90
+ /** Wall expressId → wall local placement */
91
+ wallPlacements = new Map();
92
+ /** Wall expressId → wall thickness */
93
+ wallThicknesses = new Map();
85
94
  projectParams;
86
95
  constructor(params = {}) {
87
96
  this.projectParams = params;
88
97
  this.schema = params.Schema ?? 'IFC4';
89
98
  this.buildPreamble(params);
90
99
  }
100
+ /** Generate a 22-character IFC GlobalId (base64-ish) using crypto-strong randomness */
101
+ newGlobalId() {
102
+ const chars = '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz_$';
103
+ let result;
104
+ do {
105
+ // Use Web Crypto API (works in both Node.js and browsers)
106
+ const bytes = new Uint8Array(22);
107
+ if (typeof globalThis.crypto !== 'undefined' && globalThis.crypto.getRandomValues) {
108
+ globalThis.crypto.getRandomValues(bytes);
109
+ }
110
+ else {
111
+ // Fallback for older environments
112
+ for (let i = 0; i < 22; i++)
113
+ bytes[i] = Math.floor(Math.random() * 256);
114
+ }
115
+ result = '';
116
+ for (let i = 0; i < 22; i++) {
117
+ result += chars[bytes[i] % 64];
118
+ }
119
+ } while (this.usedGlobalIds.has(result));
120
+ this.usedGlobalIds.add(result);
121
+ return result;
122
+ }
91
123
  // ============================================================================
92
124
  // Public API — Spatial Structure
93
125
  // ============================================================================
94
126
  /** Add a building storey. Returns the storey expressId for use with element creation. */
95
127
  addIfcBuildingStorey(params) {
96
128
  const id = this.id();
97
- const globalId = newGlobalId();
129
+ const globalId = this.newGlobalId();
98
130
  const name = params.Name ?? 'Storey';
99
131
  const desc = params.Description ? `'${esc(params.Description)}'` : '$';
100
132
  const elevation = num(params.Elevation);
101
- this.line(id, 'IFCBUILDINGSTOREY', `'${globalId}',#${this.ownerHistoryId},'${esc(name)}',${desc},$,$,#${this.worldPlacementId},$,.ELEMENT.,${elevation}`);
133
+ // Create a placement at [0, 0, elevation] so child elements are offset to the correct height
134
+ const storeyPlacementId = this.addLocalPlacement(this.worldPlacementId, {
135
+ Location: [0, 0, params.Elevation],
136
+ });
137
+ this.line(id, 'IFCBUILDINGSTOREY', `'${globalId}',#${this.ownerHistoryId},'${esc(name)}',${desc},$,$,#${storeyPlacementId},$,.ELEMENT.,${elevation}`);
102
138
  this.storeyIds.push(id);
103
139
  this.storeyElements.set(id, []);
140
+ this.storeyPlacements.set(id, storeyPlacementId);
104
141
  this.entities.push({ expressId: id, type: 'IfcBuildingStorey', Name: name });
105
142
  return id;
106
143
  }
144
+ /** Get the IfcLocalPlacement for a storey (falls back to world if unknown) */
145
+ getStoreyPlacement(storeyId) {
146
+ return this.storeyPlacements.get(storeyId) ?? this.worldPlacementId;
147
+ }
107
148
  // ============================================================================
108
149
  // Public API — Building Elements
109
150
  // ============================================================================
@@ -120,8 +161,8 @@ export class IfcCreator {
120
161
  const dz = params.End[2] - params.Start[2];
121
162
  const wallLen = Math.sqrt(dx * dx + dy * dy + dz * dz);
122
163
  const dir = vecNorm([dx, dy, dz]);
123
- // Placement at Start. Local X = wall direction, Z = up (default).
124
- const placementId = this.addLocalPlacement(this.worldPlacementId, {
164
+ // Placement at Start, relative to storey. Local X = wall direction, Z = up (default).
165
+ const placementId = this.addLocalPlacement(this.getStoreyPlacement(storeyId), {
125
166
  Location: params.Start,
126
167
  RefDirection: dir,
127
168
  });
@@ -133,7 +174,7 @@ export class IfcCreator {
133
174
  const shapeId = this.addShapeRepresentation('Body', [solidId]);
134
175
  const prodShapeId = this.addProductDefinitionShape([shapeId]);
135
176
  const wallId = this.id();
136
- const globalId = newGlobalId();
177
+ const globalId = this.newGlobalId();
137
178
  const name = params.Name ?? 'Wall';
138
179
  const desc = params.Description ? `'${esc(params.Description)}'` : '$';
139
180
  const objType = params.ObjectType ? `'${esc(params.ObjectType)}'` : '$';
@@ -141,6 +182,8 @@ export class IfcCreator {
141
182
  this.line(wallId, 'IFCWALL', `'${globalId}',#${this.ownerHistoryId},'${esc(name)}',${desc},${objType},#${placementId},#${prodShapeId},${tag},.STANDARD.`);
142
183
  this.elementSolids.set(wallId, [solidId]);
143
184
  this.trackElement(storeyId, wallId);
185
+ this.wallPlacements.set(wallId, placementId);
186
+ this.wallThicknesses.set(wallId, params.Thickness);
144
187
  this.entities.push({ expressId: wallId, type: 'IfcWall', Name: name });
145
188
  // Add openings
146
189
  if (params.Openings) {
@@ -155,7 +198,7 @@ export class IfcCreator {
155
198
  * Width along +X, Depth along +Y, Thickness extruded along +Z.
156
199
  */
157
200
  addIfcSlab(storeyId, params) {
158
- const placementId = this.addLocalPlacement(this.worldPlacementId, {
201
+ const placementId = this.addLocalPlacement(this.getStoreyPlacement(storeyId), {
159
202
  Location: params.Position,
160
203
  });
161
204
  let profileId;
@@ -172,7 +215,7 @@ export class IfcCreator {
172
215
  const shapeId = this.addShapeRepresentation('Body', [solidId]);
173
216
  const prodShapeId = this.addProductDefinitionShape([shapeId]);
174
217
  const slabId = this.id();
175
- const globalId = newGlobalId();
218
+ const globalId = this.newGlobalId();
176
219
  const name = params.Name ?? 'Slab';
177
220
  const desc = params.Description ? `'${esc(params.Description)}'` : '$';
178
221
  const objType = params.ObjectType ? `'${esc(params.ObjectType)}'` : '$';
@@ -193,7 +236,7 @@ export class IfcCreator {
193
236
  * Cross-section centered, extruded upward by Height.
194
237
  */
195
238
  addIfcColumn(storeyId, params) {
196
- const placementId = this.addLocalPlacement(this.worldPlacementId, {
239
+ const placementId = this.addLocalPlacement(this.getStoreyPlacement(storeyId), {
197
240
  Location: params.Position,
198
241
  });
199
242
  // Centered profile — column base center = Position
@@ -202,7 +245,7 @@ export class IfcCreator {
202
245
  const shapeId = this.addShapeRepresentation('Body', [solidId]);
203
246
  const prodShapeId = this.addProductDefinitionShape([shapeId]);
204
247
  const colId = this.id();
205
- const globalId = newGlobalId();
248
+ const globalId = this.newGlobalId();
206
249
  const name = params.Name ?? 'Column';
207
250
  const desc = params.Description ? `'${esc(params.Description)}'` : '$';
208
251
  const objType = params.ObjectType ? `'${esc(params.ObjectType)}'` : '$';
@@ -225,7 +268,7 @@ export class IfcCreator {
225
268
  const dir = vecNorm([dx, dy, dz]);
226
269
  // Local Z = beam direction, so extrusion along Z = along beam.
227
270
  // Local X, Y define the cross-section plane.
228
- const placementId = this.addLocalPlacement(this.worldPlacementId, {
271
+ const placementId = this.addLocalPlacement(this.getStoreyPlacement(storeyId), {
229
272
  Location: params.Start,
230
273
  Axis: dir,
231
274
  RefDirection: this.computeRefDirection(dir),
@@ -236,7 +279,7 @@ export class IfcCreator {
236
279
  const shapeId = this.addShapeRepresentation('Body', [solidId]);
237
280
  const prodShapeId = this.addProductDefinitionShape([shapeId]);
238
281
  const beamId = this.id();
239
- const globalId = newGlobalId();
282
+ const globalId = this.newGlobalId();
240
283
  const name = params.Name ?? 'Beam';
241
284
  const desc = params.Description ? `'${esc(params.Description)}'` : '$';
242
285
  const objType = params.ObjectType ? `'${esc(params.ObjectType)}'` : '$';
@@ -263,7 +306,7 @@ export class IfcCreator {
263
306
  throw new Error('addStair: Width must be > 0');
264
307
  const direction = params.Direction ?? 0;
265
308
  // Use LocalPlacement rotation so both step positions AND profiles rotate together
266
- const placementId = this.addLocalPlacement(this.worldPlacementId, {
309
+ const placementId = this.addLocalPlacement(this.getStoreyPlacement(storeyId), {
267
310
  Location: params.Position,
268
311
  RefDirection: direction !== 0 ? [Math.cos(direction), Math.sin(direction), 0] : undefined,
269
312
  });
@@ -285,7 +328,7 @@ export class IfcCreator {
285
328
  const shapeId = this.addShapeRepresentation('Body', stepSolids);
286
329
  const prodShapeId = this.addProductDefinitionShape([shapeId]);
287
330
  const stairId = this.id();
288
- const globalId = newGlobalId();
331
+ const globalId = this.newGlobalId();
289
332
  const name = params.Name ?? 'Stair';
290
333
  const desc = params.Description ? `'${esc(params.Description)}'` : '$';
291
334
  const objType = params.ObjectType ? `'${esc(params.ObjectType)}'` : '$';
@@ -297,19 +340,22 @@ export class IfcCreator {
297
340
  return stairId;
298
341
  }
299
342
  /**
300
- * Create a roof. Position is the minimum corner.
301
- * Width along +X, Depth along +Y, Thickness extruded upward.
302
- * Optional Slope rotates the extrusion around the Y axis.
343
+ * Create a flat or mono-pitch roof slab. Position is the minimum corner.
344
+ * Width along +X, Depth along +Y, Thickness extruded normal to the slab.
345
+ * Optional Slope is in radians and creates a single slope along +X.
303
346
  */
304
347
  addIfcRoof(storeyId, params) {
305
348
  const slope = params.Slope ?? 0;
349
+ if (slope < 0 || slope >= Math.PI / 2) {
350
+ throw new Error('addIfcRoof: Slope must be in radians between 0 and π/2 (e.g. Math.PI / 12 for 15°)');
351
+ }
306
352
  let axis = [0, 0, 1];
307
353
  let refDir = [1, 0, 0];
308
354
  if (slope > 0) {
309
355
  axis = [Math.sin(slope), 0, Math.cos(slope)];
310
356
  refDir = [Math.cos(slope), 0, -Math.sin(slope)];
311
357
  }
312
- const placementId = this.addLocalPlacement(this.worldPlacementId, {
358
+ const placementId = this.addLocalPlacement(this.getStoreyPlacement(storeyId), {
313
359
  Location: params.Position,
314
360
  Axis: axis,
315
361
  RefDirection: refDir,
@@ -320,7 +366,7 @@ export class IfcCreator {
320
366
  const shapeId = this.addShapeRepresentation('Body', [solidId]);
321
367
  const prodShapeId = this.addProductDefinitionShape([shapeId]);
322
368
  const roofId = this.id();
323
- const globalId = newGlobalId();
369
+ const globalId = this.newGlobalId();
324
370
  const name = params.Name ?? 'Roof';
325
371
  const desc = params.Description ? `'${esc(params.Description)}'` : '$';
326
372
  const objType = params.ObjectType ? `'${esc(params.ObjectType)}'` : '$';
@@ -331,6 +377,702 @@ export class IfcCreator {
331
377
  this.entities.push({ expressId: roofId, type: 'IfcRoof', Name: name });
332
378
  return roofId;
333
379
  }
380
+ /**
381
+ * Create a standard dual-pitch gable roof from a rectangular footprint.
382
+ * The ridge runs along the longer footprint dimension to keep the roof height reasonable.
383
+ */
384
+ addIfcGableRoof(storeyId, params) {
385
+ if (params.Width <= 0)
386
+ throw new Error('addIfcGableRoof: Width must be > 0');
387
+ if (params.Depth <= 0)
388
+ throw new Error('addIfcGableRoof: Depth must be > 0');
389
+ if (params.Thickness <= 0)
390
+ throw new Error('addIfcGableRoof: Thickness must be > 0');
391
+ if (params.Slope <= 0 || params.Slope >= Math.PI / 2) {
392
+ throw new Error('addIfcGableRoof: Slope must be in radians between 0 and π/2 (e.g. Math.PI / 12 for 15°)');
393
+ }
394
+ const overhang = params.Overhang ?? 0;
395
+ if (overhang < 0)
396
+ throw new Error('addIfcGableRoof: Overhang must be >= 0');
397
+ const placementId = this.addLocalPlacement(this.getStoreyPlacement(storeyId), {
398
+ Location: params.Position,
399
+ });
400
+ const cosSlope = Math.cos(params.Slope);
401
+ const sinSlope = Math.sin(params.Slope);
402
+ const ridgeAlongX = params.Width >= params.Depth;
403
+ const span = ridgeAlongX ? params.Depth : params.Width;
404
+ const ridgeLength = (ridgeAlongX ? params.Width : params.Depth) + (overhang * 2);
405
+ const run = (span / 2) + overhang;
406
+ const rise = run * Math.tan(params.Slope);
407
+ const slopedRun = run / cosSlope;
408
+ const createRoofPlane = (center, runDir, ridgeDir) => {
409
+ const originId = this.addCartesianPoint(center);
410
+ const axisId = this.addDirection(vecNorm(vecCross(runDir, ridgeDir)));
411
+ const refDirId = this.addDirection(runDir);
412
+ const axis2Id = this.addAxis2Placement3D(originId, axisId, refDirId);
413
+ const profileId = this.addRectangleProfile(slopedRun, ridgeLength);
414
+ return this.addExtrudedAreaSolid(profileId, params.Thickness, undefined, axis2Id);
415
+ };
416
+ const solids = ridgeAlongX
417
+ ? [
418
+ createRoofPlane([params.Width / 2, (params.Depth / 4) - (overhang / 2), rise / 2], [0, -cosSlope, -sinSlope], [1, 0, 0]),
419
+ createRoofPlane([params.Width / 2, ((params.Depth * 3) / 4) + (overhang / 2), rise / 2], [0, cosSlope, -sinSlope], [-1, 0, 0]),
420
+ ]
421
+ : [
422
+ createRoofPlane([(params.Width / 4) - (overhang / 2), params.Depth / 2, rise / 2], [-cosSlope, 0, -sinSlope], [0, -1, 0]),
423
+ createRoofPlane([((params.Width * 3) / 4) + (overhang / 2), params.Depth / 2, rise / 2], [cosSlope, 0, -sinSlope], [0, 1, 0]),
424
+ ];
425
+ const shapeId = this.addShapeRepresentation('Body', solids);
426
+ const prodShapeId = this.addProductDefinitionShape([shapeId]);
427
+ const roofId = this.id();
428
+ const globalId = this.newGlobalId();
429
+ const name = params.Name ?? 'Gable Roof';
430
+ const desc = params.Description ? `'${esc(params.Description)}'` : '$';
431
+ const objType = params.ObjectType ? `'${esc(params.ObjectType)}'` : '$';
432
+ const tag = params.Tag ? `'${esc(params.Tag)}'` : '$';
433
+ this.line(roofId, 'IFCROOF', `'${globalId}',#${this.ownerHistoryId},'${esc(name)}',${desc},${objType},#${placementId},#${prodShapeId},${tag},.GABLE_ROOF.`);
434
+ this.elementSolids.set(roofId, solids);
435
+ this.trackElement(storeyId, roofId);
436
+ this.entities.push({ expressId: roofId, type: 'IfcRoof', Name: name });
437
+ return roofId;
438
+ }
439
+ /**
440
+ * Create a door hosted in a wall opening and aligned to the host wall.
441
+ * Position is wall-local: [distance_along_wall, 0, base_height].
442
+ */
443
+ addIfcWallDoor(wallId, params) {
444
+ const { storeyId, placementId, wallThickness } = this.getHostedWallInfo(wallId);
445
+ const thickness = params.Thickness ?? wallThickness;
446
+ if (thickness <= 0)
447
+ throw new Error('addIfcWallDoor: Thickness must be > 0');
448
+ const openingId = this.addWallOpening(wallId, placementId, {
449
+ Name: params.Name ? `${params.Name} Opening` : 'Door Opening',
450
+ Width: params.Width,
451
+ Height: params.Height,
452
+ Position: params.Position,
453
+ }, wallThickness);
454
+ const doorPlacementId = this.addHostedWallFillPlacement(placementId, params.Position, wallThickness);
455
+ const profileId = this.addRectangleProfile(params.Width, params.Height, [0, params.Height / 2]);
456
+ const solidId = this.addExtrudedAreaSolid(profileId, thickness);
457
+ const shapeId = this.addShapeRepresentation('Body', [solidId]);
458
+ const prodShapeId = this.addProductDefinitionShape([shapeId]);
459
+ const doorId = this.id();
460
+ const globalId = this.newGlobalId();
461
+ const name = params.Name ?? 'Door';
462
+ const desc = params.Description ? `'${esc(params.Description)}'` : '$';
463
+ const objType = params.ObjectType ? `'${esc(params.ObjectType)}'` : '$';
464
+ const tag = params.Tag ? `'${esc(params.Tag)}'` : '$';
465
+ const opType = params.OperationType ?? 'SINGLE_SWING_LEFT';
466
+ const predType = params.PredefinedType ?? 'NOTDEFINED';
467
+ this.line(doorId, 'IFCDOOR', `'${globalId}',#${this.ownerHistoryId},'${esc(name)}',${desc},${objType},#${doorPlacementId},#${prodShapeId},${tag},${num(params.Height)},${num(params.Width)},.${predType}.,.${opType}.,$`);
468
+ this.addIfcRelFillsElement(openingId, doorId);
469
+ this.elementSolids.set(doorId, [solidId]);
470
+ this.trackElement(storeyId, doorId);
471
+ this.entities.push({ expressId: doorId, type: 'IfcDoor', Name: name });
472
+ return doorId;
473
+ }
474
+ /**
475
+ * Create a window hosted in a wall opening and aligned to the host wall.
476
+ * Position is wall-local: [distance_along_wall, 0, sill_height].
477
+ */
478
+ addIfcWallWindow(wallId, params) {
479
+ const { storeyId, placementId, wallThickness } = this.getHostedWallInfo(wallId);
480
+ const thickness = params.Thickness ?? wallThickness;
481
+ if (thickness <= 0)
482
+ throw new Error('addIfcWallWindow: Thickness must be > 0');
483
+ const openingId = this.addWallOpening(wallId, placementId, {
484
+ Name: params.Name ? `${params.Name} Opening` : 'Window Opening',
485
+ Width: params.Width,
486
+ Height: params.Height,
487
+ Position: params.Position,
488
+ }, wallThickness);
489
+ const windowPlacementId = this.addHostedWallFillPlacement(placementId, params.Position, wallThickness);
490
+ const profileId = this.addRectangleProfile(params.Width, params.Height, [0, params.Height / 2]);
491
+ const solidId = this.addExtrudedAreaSolid(profileId, thickness);
492
+ const shapeId = this.addShapeRepresentation('Body', [solidId]);
493
+ const prodShapeId = this.addProductDefinitionShape([shapeId]);
494
+ const windowId = this.id();
495
+ const globalId = this.newGlobalId();
496
+ const name = params.Name ?? 'Window';
497
+ const desc = params.Description ? `'${esc(params.Description)}'` : '$';
498
+ const objType = params.ObjectType ? `'${esc(params.ObjectType)}'` : '$';
499
+ const tag = params.Tag ? `'${esc(params.Tag)}'` : '$';
500
+ const partType = params.PartitioningType ?? 'SINGLE_PANEL';
501
+ this.line(windowId, 'IFCWINDOW', `'${globalId}',#${this.ownerHistoryId},'${esc(name)}',${desc},${objType},#${windowPlacementId},#${prodShapeId},${tag},${num(params.Height)},${num(params.Width)},.NOTDEFINED.,.${partType}.,$`);
502
+ this.addIfcRelFillsElement(openingId, windowId);
503
+ this.elementSolids.set(windowId, [solidId]);
504
+ this.trackElement(storeyId, windowId);
505
+ this.entities.push({ expressId: windowId, type: 'IfcWindow', Name: name });
506
+ return windowId;
507
+ }
508
+ /**
509
+ * Create a door element. Width × Height × Thickness panel.
510
+ */
511
+ addIfcDoor(storeyId, params) {
512
+ const placementId = this.addLocalPlacement(this.worldPlacementId, {
513
+ Location: params.Position,
514
+ });
515
+ const thickness = params.Thickness ?? 0.05;
516
+ const profileId = this.addRectangleProfile(params.Width, thickness);
517
+ const solidId = this.addExtrudedAreaSolid(profileId, params.Height);
518
+ const shapeId = this.addShapeRepresentation('Body', [solidId]);
519
+ const prodShapeId = this.addProductDefinitionShape([shapeId]);
520
+ const doorId = this.id();
521
+ const globalId = this.newGlobalId();
522
+ const name = params.Name ?? 'Door';
523
+ const desc = params.Description ? `'${esc(params.Description)}'` : '$';
524
+ const objType = params.ObjectType ? `'${esc(params.ObjectType)}'` : '$';
525
+ const tag = params.Tag ? `'${esc(params.Tag)}'` : '$';
526
+ const opType = params.OperationType ?? 'SINGLE_SWING_LEFT';
527
+ const predType = params.PredefinedType ?? 'NOTDEFINED';
528
+ this.line(doorId, 'IFCDOOR', `'${globalId}',#${this.ownerHistoryId},'${esc(name)}',${desc},${objType},#${placementId},#${prodShapeId},${tag},${num(params.Height)},${num(params.Width)},.${predType}.,.${opType}.,$`);
529
+ this.elementSolids.set(doorId, [solidId]);
530
+ this.trackElement(storeyId, doorId);
531
+ this.entities.push({ expressId: doorId, type: 'IfcDoor', Name: name });
532
+ return doorId;
533
+ }
534
+ /**
535
+ * Create a window element. Width × Height × Thickness frame.
536
+ */
537
+ addIfcWindow(storeyId, params) {
538
+ const placementId = this.addLocalPlacement(this.worldPlacementId, {
539
+ Location: params.Position,
540
+ });
541
+ const thickness = params.Thickness ?? 0.05;
542
+ const profileId = this.addRectangleProfile(params.Width, thickness);
543
+ const solidId = this.addExtrudedAreaSolid(profileId, params.Height);
544
+ const shapeId = this.addShapeRepresentation('Body', [solidId]);
545
+ const prodShapeId = this.addProductDefinitionShape([shapeId]);
546
+ const windowId = this.id();
547
+ const globalId = this.newGlobalId();
548
+ const name = params.Name ?? 'Window';
549
+ const desc = params.Description ? `'${esc(params.Description)}'` : '$';
550
+ const objType = params.ObjectType ? `'${esc(params.ObjectType)}'` : '$';
551
+ const tag = params.Tag ? `'${esc(params.Tag)}'` : '$';
552
+ const partType = params.PartitioningType ?? 'SINGLE_PANEL';
553
+ this.line(windowId, 'IFCWINDOW', `'${globalId}',#${this.ownerHistoryId},'${esc(name)}',${desc},${objType},#${placementId},#${prodShapeId},${tag},${num(params.Height)},${num(params.Width)},.NOTDEFINED.,.${partType}.,$`);
554
+ this.elementSolids.set(windowId, [solidId]);
555
+ this.trackElement(storeyId, windowId);
556
+ this.entities.push({ expressId: windowId, type: 'IfcWindow', Name: name });
557
+ return windowId;
558
+ }
559
+ /**
560
+ * Create a ramp. Position is the low end.
561
+ * Width along +Y, Length along +X, Rise optionally inclines the ramp.
562
+ */
563
+ addIfcRamp(storeyId, params) {
564
+ const rise = params.Rise ?? 0;
565
+ let axis = [0, 0, 1];
566
+ let refDir = [1, 0, 0];
567
+ if (rise > 0) {
568
+ const slopeAngle = Math.atan2(rise, params.Length);
569
+ axis = [Math.sin(slopeAngle), 0, Math.cos(slopeAngle)];
570
+ refDir = [Math.cos(slopeAngle), 0, -Math.sin(slopeAngle)];
571
+ }
572
+ const placementId = this.addLocalPlacement(this.worldPlacementId, {
573
+ Location: params.Position,
574
+ Axis: axis,
575
+ RefDirection: refDir,
576
+ });
577
+ const profileId = this.addRectangleProfile(params.Length, params.Width, [params.Length / 2, params.Width / 2]);
578
+ const solidId = this.addExtrudedAreaSolid(profileId, params.Thickness);
579
+ const shapeId = this.addShapeRepresentation('Body', [solidId]);
580
+ const prodShapeId = this.addProductDefinitionShape([shapeId]);
581
+ const rampId = this.id();
582
+ const globalId = this.newGlobalId();
583
+ const name = params.Name ?? 'Ramp';
584
+ const desc = params.Description ? `'${esc(params.Description)}'` : '$';
585
+ const objType = params.ObjectType ? `'${esc(params.ObjectType)}'` : '$';
586
+ const tag = params.Tag ? `'${esc(params.Tag)}'` : '$';
587
+ this.line(rampId, 'IFCRAMP', `'${globalId}',#${this.ownerHistoryId},'${esc(name)}',${desc},${objType},#${placementId},#${prodShapeId},${tag},.STRAIGHT_RUN_RAMP.`);
588
+ this.elementSolids.set(rampId, [solidId]);
589
+ this.trackElement(storeyId, rampId);
590
+ this.entities.push({ expressId: rampId, type: 'IfcRamp', Name: name });
591
+ return rampId;
592
+ }
593
+ /**
594
+ * Create a railing from Start to End with given Height.
595
+ */
596
+ addIfcRailing(storeyId, params) {
597
+ const dx = params.End[0] - params.Start[0];
598
+ const dy = params.End[1] - params.Start[1];
599
+ const dz = params.End[2] - params.Start[2];
600
+ const railLen = Math.sqrt(dx * dx + dy * dy + dz * dz);
601
+ const dir = vecNorm([dx, dy, dz]);
602
+ // Use identity orientation so posts can extrude vertically along world Z
603
+ const placementId = this.addLocalPlacement(this.worldPlacementId, {
604
+ Location: params.Start,
605
+ });
606
+ const railWidth = params.Width ?? 0.05;
607
+ // Rail solid — extrude along the rail direction (dir) at rail Height
608
+ const railProfileId = this.addRectangleProfile(railWidth, railWidth);
609
+ const railOriginId = this.addCartesianPoint([0, 0, params.Height]);
610
+ const railAxisId = this.addDirection(dir);
611
+ const railRefId = this.addDirection(this.computeRefDirection(dir));
612
+ const railAxis2Id = this.addAxis2Placement3D(railOriginId, railAxisId, railRefId);
613
+ const railSolidId = this.id();
614
+ this.line(railSolidId, 'IFCEXTRUDEDAREASOLID', `#${railProfileId},#${railAxis2Id},#${this.dirZ},${num(railLen)}`);
615
+ const solids = [railSolidId];
616
+ // Vertical posts at start and end
617
+ const postProfile = this.addRectangleProfile(railWidth, railWidth);
618
+ // Start post — at origin, extrude up
619
+ const startPostOriginId = this.addCartesianPoint([0, 0, 0]);
620
+ const startPostAxis2Id = this.addAxis2Placement3D(startPostOriginId);
621
+ const startPostId = this.id();
622
+ this.line(startPostId, 'IFCEXTRUDEDAREASOLID', `#${postProfile},#${startPostAxis2Id},#${this.dirZ},${num(params.Height)}`);
623
+ solids.push(startPostId);
624
+ // End post — at the end of the rail, extrude up
625
+ const endPostOriginId = this.addCartesianPoint([
626
+ dx, dy, dz,
627
+ ]);
628
+ const endPostAxis2Id = this.addAxis2Placement3D(endPostOriginId);
629
+ const endPostProfile = this.addRectangleProfile(railWidth, railWidth);
630
+ const endPostId = this.id();
631
+ this.line(endPostId, 'IFCEXTRUDEDAREASOLID', `#${endPostProfile},#${endPostAxis2Id},#${this.dirZ},${num(params.Height)}`);
632
+ solids.push(endPostId);
633
+ const shapeId = this.addShapeRepresentation('Body', solids);
634
+ const prodShapeId = this.addProductDefinitionShape([shapeId]);
635
+ const railingId = this.id();
636
+ const globalId = this.newGlobalId();
637
+ const name = params.Name ?? 'Railing';
638
+ const desc = params.Description ? `'${esc(params.Description)}'` : '$';
639
+ const objType = params.ObjectType ? `'${esc(params.ObjectType)}'` : '$';
640
+ const tag = params.Tag ? `'${esc(params.Tag)}'` : '$';
641
+ this.line(railingId, 'IFCRAILING', `'${globalId}',#${this.ownerHistoryId},'${esc(name)}',${desc},${objType},#${placementId},#${prodShapeId},${tag},.HANDRAIL.`);
642
+ this.elementSolids.set(railingId, solids);
643
+ this.trackElement(storeyId, railingId);
644
+ this.entities.push({ expressId: railingId, type: 'IfcRailing', Name: name });
645
+ return railingId;
646
+ }
647
+ /**
648
+ * Create a plate (thin flat element, e.g. steel plate).
649
+ */
650
+ addIfcPlate(storeyId, params) {
651
+ const placementId = this.addLocalPlacement(this.worldPlacementId, {
652
+ Location: params.Position,
653
+ });
654
+ let profileId;
655
+ if (params.Profile && params.Profile.length >= 3) {
656
+ profileId = this.addArbitraryProfile(params.Profile);
657
+ }
658
+ else {
659
+ profileId = this.addRectangleProfile(params.Width, params.Depth, [params.Width / 2, params.Depth / 2]);
660
+ }
661
+ const solidId = this.addExtrudedAreaSolid(profileId, params.Thickness);
662
+ const shapeId = this.addShapeRepresentation('Body', [solidId]);
663
+ const prodShapeId = this.addProductDefinitionShape([shapeId]);
664
+ const plateId = this.id();
665
+ const globalId = this.newGlobalId();
666
+ const name = params.Name ?? 'Plate';
667
+ const desc = params.Description ? `'${esc(params.Description)}'` : '$';
668
+ const objType = params.ObjectType ? `'${esc(params.ObjectType)}'` : '$';
669
+ const tag = params.Tag ? `'${esc(params.Tag)}'` : '$';
670
+ this.line(plateId, 'IFCPLATE', `'${globalId}',#${this.ownerHistoryId},'${esc(name)}',${desc},${objType},#${placementId},#${prodShapeId},${tag},.NOTDEFINED.`);
671
+ this.elementSolids.set(plateId, [solidId]);
672
+ this.trackElement(storeyId, plateId);
673
+ this.entities.push({ expressId: plateId, type: 'IfcPlate', Name: name });
674
+ return plateId;
675
+ }
676
+ /**
677
+ * Create a structural member (brace, strut, etc.) from Start to End.
678
+ */
679
+ addIfcMember(storeyId, params) {
680
+ const dx = params.End[0] - params.Start[0];
681
+ const dy = params.End[1] - params.Start[1];
682
+ const dz = params.End[2] - params.Start[2];
683
+ const memberLen = Math.sqrt(dx * dx + dy * dy + dz * dz);
684
+ const dir = vecNorm([dx, dy, dz]);
685
+ const placementId = this.addLocalPlacement(this.worldPlacementId, {
686
+ Location: params.Start,
687
+ Axis: dir,
688
+ RefDirection: this.computeRefDirection(dir),
689
+ });
690
+ const profileId = this.addRectangleProfile(params.Width, params.Height);
691
+ const solidId = this.addExtrudedAreaSolid(profileId, memberLen);
692
+ const shapeId = this.addShapeRepresentation('Body', [solidId]);
693
+ const prodShapeId = this.addProductDefinitionShape([shapeId]);
694
+ const memberId = this.id();
695
+ const globalId = this.newGlobalId();
696
+ const name = params.Name ?? 'Member';
697
+ const desc = params.Description ? `'${esc(params.Description)}'` : '$';
698
+ const objType = params.ObjectType ? `'${esc(params.ObjectType)}'` : '$';
699
+ const tag = params.Tag ? `'${esc(params.Tag)}'` : '$';
700
+ this.line(memberId, 'IFCMEMBER', `'${globalId}',#${this.ownerHistoryId},'${esc(name)}',${desc},${objType},#${placementId},#${prodShapeId},${tag},.NOTDEFINED.`);
701
+ this.elementSolids.set(memberId, [solidId]);
702
+ this.trackElement(storeyId, memberId);
703
+ this.entities.push({ expressId: memberId, type: 'IfcMember', Name: name });
704
+ return memberId;
705
+ }
706
+ /**
707
+ * Create a footing (foundation). Position is top centre, Height extends downward.
708
+ */
709
+ addIfcFooting(storeyId, params) {
710
+ // Offset placement downward so extrusion starts at bottom
711
+ const placementId = this.addLocalPlacement(this.worldPlacementId, {
712
+ Location: [params.Position[0], params.Position[1], params.Position[2] - params.Height],
713
+ });
714
+ const profileId = this.addRectangleProfile(params.Width, params.Depth);
715
+ const solidId = this.addExtrudedAreaSolid(profileId, params.Height);
716
+ const shapeId = this.addShapeRepresentation('Body', [solidId]);
717
+ const prodShapeId = this.addProductDefinitionShape([shapeId]);
718
+ const footingId = this.id();
719
+ const globalId = this.newGlobalId();
720
+ const name = params.Name ?? 'Footing';
721
+ const desc = params.Description ? `'${esc(params.Description)}'` : '$';
722
+ const objType = params.ObjectType ? `'${esc(params.ObjectType)}'` : '$';
723
+ const tag = params.Tag ? `'${esc(params.Tag)}'` : '$';
724
+ const predefined = params.PredefinedType ?? 'PAD_FOOTING';
725
+ this.line(footingId, 'IFCFOOTING', `'${globalId}',#${this.ownerHistoryId},'${esc(name)}',${desc},${objType},#${placementId},#${prodShapeId},${tag},.${predefined}.`);
726
+ this.elementSolids.set(footingId, [solidId]);
727
+ this.trackElement(storeyId, footingId);
728
+ this.entities.push({ expressId: footingId, type: 'IfcFooting', Name: name });
729
+ return footingId;
730
+ }
731
+ /**
732
+ * Create a pile (deep foundation). Position is top, Length extends downward.
733
+ * Uses circular cross-section by default, rectangular if IsRectangular is set.
734
+ */
735
+ addIfcPile(storeyId, params) {
736
+ const placementId = this.addLocalPlacement(this.worldPlacementId, {
737
+ Location: [params.Position[0], params.Position[1], params.Position[2] - params.Length],
738
+ });
739
+ let profileId;
740
+ if (params.IsRectangular) {
741
+ const depth = params.RectangularDepth ?? params.Diameter;
742
+ profileId = this.addRectangleProfile(params.Diameter, depth);
743
+ }
744
+ else {
745
+ profileId = this.addCircleProfile(params.Diameter / 2);
746
+ }
747
+ const solidId = this.addExtrudedAreaSolid(profileId, params.Length);
748
+ const shapeId = this.addShapeRepresentation('Body', [solidId]);
749
+ const prodShapeId = this.addProductDefinitionShape([shapeId]);
750
+ const pileId = this.id();
751
+ const globalId = this.newGlobalId();
752
+ const name = params.Name ?? 'Pile';
753
+ const desc = params.Description ? `'${esc(params.Description)}'` : '$';
754
+ const objType = params.ObjectType ? `'${esc(params.ObjectType)}'` : '$';
755
+ const tag = params.Tag ? `'${esc(params.Tag)}'` : '$';
756
+ this.line(pileId, 'IFCPILE', `'${globalId}',#${this.ownerHistoryId},'${esc(name)}',${desc},${objType},#${placementId},#${prodShapeId},${tag},.DRIVEN.,$`);
757
+ this.elementSolids.set(pileId, [solidId]);
758
+ this.trackElement(storeyId, pileId);
759
+ this.entities.push({ expressId: pileId, type: 'IfcPile', Name: name });
760
+ return pileId;
761
+ }
762
+ /**
763
+ * Create a space (room volume).
764
+ */
765
+ addIfcSpace(storeyId, params) {
766
+ const placementId = this.addLocalPlacement(this.worldPlacementId, {
767
+ Location: params.Position,
768
+ });
769
+ let profileId;
770
+ if (params.Profile && params.Profile.length >= 3) {
771
+ profileId = this.addArbitraryProfile(params.Profile);
772
+ }
773
+ else {
774
+ profileId = this.addRectangleProfile(params.Width, params.Depth, [params.Width / 2, params.Depth / 2]);
775
+ }
776
+ const solidId = this.addExtrudedAreaSolid(profileId, params.Height);
777
+ const shapeId = this.addShapeRepresentation('Body', [solidId]);
778
+ const prodShapeId = this.addProductDefinitionShape([shapeId]);
779
+ const spaceId = this.id();
780
+ const globalId = this.newGlobalId();
781
+ const name = params.Name ?? 'Space';
782
+ const desc = params.Description ? `'${esc(params.Description)}'` : '$';
783
+ const objType = params.ObjectType ? `'${esc(params.ObjectType)}'` : '$';
784
+ const tag = params.Tag ? `'${esc(params.Tag)}'` : '$';
785
+ const longName = params.LongName ? `'${esc(params.LongName)}'` : '$';
786
+ this.line(spaceId, 'IFCSPACE', `'${globalId}',#${this.ownerHistoryId},'${esc(name)}',${desc},${objType},#${placementId},#${prodShapeId},${longName},.ELEMENT.,.INTERNAL.,$`);
787
+ this.elementSolids.set(spaceId, [solidId]);
788
+ this.trackElement(storeyId, spaceId);
789
+ this.entities.push({ expressId: spaceId, type: 'IfcSpace', Name: name });
790
+ return spaceId;
791
+ }
792
+ /**
793
+ * Create a curtain wall. Thin panel from Start to End, extruded by Height.
794
+ */
795
+ addIfcCurtainWall(storeyId, params) {
796
+ const dx = params.End[0] - params.Start[0];
797
+ const dy = params.End[1] - params.Start[1];
798
+ const dz = params.End[2] - params.Start[2];
799
+ const wallLen = Math.sqrt(dx * dx + dy * dy + dz * dz);
800
+ const dir = vecNorm([dx, dy, dz]);
801
+ const thickness = params.Thickness ?? 0.05;
802
+ const placementId = this.addLocalPlacement(this.worldPlacementId, {
803
+ Location: params.Start,
804
+ RefDirection: dir,
805
+ });
806
+ const profileId = this.addRectangleProfile(wallLen, thickness, [wallLen / 2, 0]);
807
+ const solidId = this.addExtrudedAreaSolid(profileId, params.Height);
808
+ const shapeId = this.addShapeRepresentation('Body', [solidId]);
809
+ const prodShapeId = this.addProductDefinitionShape([shapeId]);
810
+ const cwId = this.id();
811
+ const globalId = this.newGlobalId();
812
+ const name = params.Name ?? 'Curtain Wall';
813
+ const desc = params.Description ? `'${esc(params.Description)}'` : '$';
814
+ const objType = params.ObjectType ? `'${esc(params.ObjectType)}'` : '$';
815
+ const tag = params.Tag ? `'${esc(params.Tag)}'` : '$';
816
+ this.line(cwId, 'IFCCURTAINWALL', `'${globalId}',#${this.ownerHistoryId},'${esc(name)}',${desc},${objType},#${placementId},#${prodShapeId},${tag},.NOTDEFINED.`);
817
+ this.elementSolids.set(cwId, [solidId]);
818
+ this.trackElement(storeyId, cwId);
819
+ this.entities.push({ expressId: cwId, type: 'IfcCurtainWall', Name: name });
820
+ return cwId;
821
+ }
822
+ /**
823
+ * Create a furnishing element (furniture/equipment bounding box).
824
+ */
825
+ addIfcFurnishingElement(storeyId, params) {
826
+ const direction = params.Direction ?? 0;
827
+ const placementId = this.addLocalPlacement(this.worldPlacementId, {
828
+ Location: params.Position,
829
+ RefDirection: direction !== 0 ? [Math.cos(direction), Math.sin(direction), 0] : undefined,
830
+ });
831
+ const profileId = this.addRectangleProfile(params.Width, params.Depth, [params.Width / 2, params.Depth / 2]);
832
+ const solidId = this.addExtrudedAreaSolid(profileId, params.Height);
833
+ const shapeId = this.addShapeRepresentation('Body', [solidId]);
834
+ const prodShapeId = this.addProductDefinitionShape([shapeId]);
835
+ const furnId = this.id();
836
+ const globalId = this.newGlobalId();
837
+ const name = params.Name ?? 'Furnishing';
838
+ const desc = params.Description ? `'${esc(params.Description)}'` : '$';
839
+ const objType = params.ObjectType ? `'${esc(params.ObjectType)}'` : '$';
840
+ const tag = params.Tag ? `'${esc(params.Tag)}'` : '$';
841
+ this.line(furnId, 'IFCFURNISHINGELEMENT', `'${globalId}',#${this.ownerHistoryId},'${esc(name)}',${desc},${objType},#${placementId},#${prodShapeId},${tag}`);
842
+ this.elementSolids.set(furnId, [solidId]);
843
+ this.trackElement(storeyId, furnId);
844
+ this.entities.push({ expressId: furnId, type: 'IfcFurnishingElement', Name: name });
845
+ return furnId;
846
+ }
847
+ /**
848
+ * Create a proxy element (generic element for custom/unclassified objects).
849
+ */
850
+ addIfcBuildingElementProxy(storeyId, params) {
851
+ const placementId = this.addLocalPlacement(this.worldPlacementId, {
852
+ Location: params.Position,
853
+ });
854
+ let profileId;
855
+ if (params.Profile && params.Profile.length >= 3) {
856
+ profileId = this.addArbitraryProfile(params.Profile);
857
+ }
858
+ else {
859
+ profileId = this.addRectangleProfile(params.Width, params.Depth, [params.Width / 2, params.Depth / 2]);
860
+ }
861
+ const solidId = this.addExtrudedAreaSolid(profileId, params.Height);
862
+ const shapeId = this.addShapeRepresentation('Body', [solidId]);
863
+ const prodShapeId = this.addProductDefinitionShape([shapeId]);
864
+ const proxyId = this.id();
865
+ const globalId = this.newGlobalId();
866
+ const name = params.Name ?? 'Proxy';
867
+ const desc = params.Description ? `'${esc(params.Description)}'` : '$';
868
+ const objType = params.ObjectType ?? params.ProxyType ?? '';
869
+ const objTypeStr = objType ? `'${esc(objType)}'` : '$';
870
+ const tag = params.Tag ? `'${esc(params.Tag)}'` : '$';
871
+ this.line(proxyId, 'IFCBUILDINGELEMENTPROXY', `'${globalId}',#${this.ownerHistoryId},'${esc(name)}',${desc},${objTypeStr},#${placementId},#${prodShapeId},${tag},.NOTDEFINED.`);
872
+ this.elementSolids.set(proxyId, [solidId]);
873
+ this.trackElement(storeyId, proxyId);
874
+ this.entities.push({ expressId: proxyId, type: 'IfcBuildingElementProxy', Name: name });
875
+ return proxyId;
876
+ }
877
+ // ============================================================================
878
+ // Public API — Advanced Geometry (Circle, I-Shape, L-Shape, T-Shape, U-Shape, C-Shape profiles)
879
+ // ============================================================================
880
+ /**
881
+ * Create a column with circular cross-section.
882
+ */
883
+ addIfcCircularColumn(storeyId, params) {
884
+ const placementId = this.addLocalPlacement(this.worldPlacementId, {
885
+ Location: params.Position,
886
+ });
887
+ const profileId = this.addCircleProfile(params.Radius);
888
+ const solidId = this.addExtrudedAreaSolid(profileId, params.Height);
889
+ const shapeId = this.addShapeRepresentation('Body', [solidId]);
890
+ const prodShapeId = this.addProductDefinitionShape([shapeId]);
891
+ const colId = this.id();
892
+ const globalId = this.newGlobalId();
893
+ const name = params.Name ?? 'Column';
894
+ const desc = params.Description ? `'${esc(params.Description)}'` : '$';
895
+ const objType = params.ObjectType ? `'${esc(params.ObjectType)}'` : '$';
896
+ const tag = params.Tag ? `'${esc(params.Tag)}'` : '$';
897
+ this.line(colId, 'IFCCOLUMN', `'${globalId}',#${this.ownerHistoryId},'${esc(name)}',${desc},${objType},#${placementId},#${prodShapeId},${tag},.COLUMN.`);
898
+ this.elementSolids.set(colId, [solidId]);
899
+ this.trackElement(storeyId, colId);
900
+ this.entities.push({ expressId: colId, type: 'IfcColumn', Name: name });
901
+ return colId;
902
+ }
903
+ /**
904
+ * Create a beam with I-shape (wide-flange/H-shape) cross-section.
905
+ */
906
+ addIfcIShapeBeam(storeyId, params) {
907
+ const dx = params.End[0] - params.Start[0];
908
+ const dy = params.End[1] - params.Start[1];
909
+ const dz = params.End[2] - params.Start[2];
910
+ const beamLen = Math.sqrt(dx * dx + dy * dy + dz * dz);
911
+ const dir = vecNorm([dx, dy, dz]);
912
+ const placementId = this.addLocalPlacement(this.worldPlacementId, {
913
+ Location: params.Start,
914
+ Axis: dir,
915
+ RefDirection: this.computeRefDirection(dir),
916
+ });
917
+ const profileId = this.addIShapeProfile(params.OverallWidth, params.OverallDepth, params.WebThickness, params.FlangeThickness, params.FilletRadius);
918
+ const solidId = this.addExtrudedAreaSolid(profileId, beamLen);
919
+ const shapeId = this.addShapeRepresentation('Body', [solidId]);
920
+ const prodShapeId = this.addProductDefinitionShape([shapeId]);
921
+ const beamId = this.id();
922
+ const globalId = this.newGlobalId();
923
+ const name = params.Name ?? 'Beam';
924
+ const desc = params.Description ? `'${esc(params.Description)}'` : '$';
925
+ const objType = params.ObjectType ? `'${esc(params.ObjectType)}'` : '$';
926
+ const tag = params.Tag ? `'${esc(params.Tag)}'` : '$';
927
+ this.line(beamId, 'IFCBEAM', `'${globalId}',#${this.ownerHistoryId},'${esc(name)}',${desc},${objType},#${placementId},#${prodShapeId},${tag},.BEAM.`);
928
+ this.elementSolids.set(beamId, [solidId]);
929
+ this.trackElement(storeyId, beamId);
930
+ this.entities.push({ expressId: beamId, type: 'IfcBeam', Name: name });
931
+ return beamId;
932
+ }
933
+ /**
934
+ * Create a member with L-shape (angle) cross-section.
935
+ */
936
+ addIfcLShapeMember(storeyId, params) {
937
+ const dx = params.End[0] - params.Start[0];
938
+ const dy = params.End[1] - params.Start[1];
939
+ const dz = params.End[2] - params.Start[2];
940
+ const memberLen = Math.sqrt(dx * dx + dy * dy + dz * dz);
941
+ const dir = vecNorm([dx, dy, dz]);
942
+ const placementId = this.addLocalPlacement(this.worldPlacementId, {
943
+ Location: params.Start,
944
+ Axis: dir,
945
+ RefDirection: this.computeRefDirection(dir),
946
+ });
947
+ const profileId = this.addLShapeProfile(params.Depth, params.Width, params.Thickness, params.FilletRadius);
948
+ const solidId = this.addExtrudedAreaSolid(profileId, memberLen);
949
+ const shapeId = this.addShapeRepresentation('Body', [solidId]);
950
+ const prodShapeId = this.addProductDefinitionShape([shapeId]);
951
+ const memberId = this.id();
952
+ const globalId = this.newGlobalId();
953
+ const name = params.Name ?? 'Member';
954
+ const desc = params.Description ? `'${esc(params.Description)}'` : '$';
955
+ const objType = params.ObjectType ? `'${esc(params.ObjectType)}'` : '$';
956
+ const tag = params.Tag ? `'${esc(params.Tag)}'` : '$';
957
+ this.line(memberId, 'IFCMEMBER', `'${globalId}',#${this.ownerHistoryId},'${esc(name)}',${desc},${objType},#${placementId},#${prodShapeId},${tag},.NOTDEFINED.`);
958
+ this.elementSolids.set(memberId, [solidId]);
959
+ this.trackElement(storeyId, memberId);
960
+ this.entities.push({ expressId: memberId, type: 'IfcMember', Name: name });
961
+ return memberId;
962
+ }
963
+ /**
964
+ * Create a member with T-shape cross-section.
965
+ */
966
+ addIfcTShapeMember(storeyId, params) {
967
+ const dx = params.End[0] - params.Start[0];
968
+ const dy = params.End[1] - params.Start[1];
969
+ const dz = params.End[2] - params.Start[2];
970
+ const memberLen = Math.sqrt(dx * dx + dy * dy + dz * dz);
971
+ const dir = vecNorm([dx, dy, dz]);
972
+ const placementId = this.addLocalPlacement(this.worldPlacementId, {
973
+ Location: params.Start,
974
+ Axis: dir,
975
+ RefDirection: this.computeRefDirection(dir),
976
+ });
977
+ const profileId = this.addTShapeProfile(params.FlangeWidth, params.Depth, params.WebThickness, params.FlangeThickness, params.FilletRadius);
978
+ const solidId = this.addExtrudedAreaSolid(profileId, memberLen);
979
+ const shapeId = this.addShapeRepresentation('Body', [solidId]);
980
+ const prodShapeId = this.addProductDefinitionShape([shapeId]);
981
+ const memberId = this.id();
982
+ const globalId = this.newGlobalId();
983
+ const name = params.Name ?? 'Member';
984
+ const desc = params.Description ? `'${esc(params.Description)}'` : '$';
985
+ const objType = params.ObjectType ? `'${esc(params.ObjectType)}'` : '$';
986
+ const tag = params.Tag ? `'${esc(params.Tag)}'` : '$';
987
+ this.line(memberId, 'IFCMEMBER', `'${globalId}',#${this.ownerHistoryId},'${esc(name)}',${desc},${objType},#${placementId},#${prodShapeId},${tag},.NOTDEFINED.`);
988
+ this.elementSolids.set(memberId, [solidId]);
989
+ this.trackElement(storeyId, memberId);
990
+ this.entities.push({ expressId: memberId, type: 'IfcMember', Name: name });
991
+ return memberId;
992
+ }
993
+ /**
994
+ * Create a member with U-shape (channel) cross-section.
995
+ */
996
+ addIfcUShapeMember(storeyId, params) {
997
+ const dx = params.End[0] - params.Start[0];
998
+ const dy = params.End[1] - params.Start[1];
999
+ const dz = params.End[2] - params.Start[2];
1000
+ const memberLen = Math.sqrt(dx * dx + dy * dy + dz * dz);
1001
+ const dir = vecNorm([dx, dy, dz]);
1002
+ const placementId = this.addLocalPlacement(this.worldPlacementId, {
1003
+ Location: params.Start,
1004
+ Axis: dir,
1005
+ RefDirection: this.computeRefDirection(dir),
1006
+ });
1007
+ const profileId = this.addUShapeProfile(params.Depth, params.FlangeWidth, params.WebThickness, params.FlangeThickness, params.FilletRadius);
1008
+ const solidId = this.addExtrudedAreaSolid(profileId, memberLen);
1009
+ const shapeId = this.addShapeRepresentation('Body', [solidId]);
1010
+ const prodShapeId = this.addProductDefinitionShape([shapeId]);
1011
+ const memberId = this.id();
1012
+ const globalId = this.newGlobalId();
1013
+ const name = params.Name ?? 'Member';
1014
+ const desc = params.Description ? `'${esc(params.Description)}'` : '$';
1015
+ const objType = params.ObjectType ? `'${esc(params.ObjectType)}'` : '$';
1016
+ const tag = params.Tag ? `'${esc(params.Tag)}'` : '$';
1017
+ this.line(memberId, 'IFCMEMBER', `'${globalId}',#${this.ownerHistoryId},'${esc(name)}',${desc},${objType},#${placementId},#${prodShapeId},${tag},.NOTDEFINED.`);
1018
+ this.elementSolids.set(memberId, [solidId]);
1019
+ this.trackElement(storeyId, memberId);
1020
+ this.entities.push({ expressId: memberId, type: 'IfcMember', Name: name });
1021
+ return memberId;
1022
+ }
1023
+ /**
1024
+ * Create a column or pile with hollow circular cross-section.
1025
+ */
1026
+ addIfcHollowCircularColumn(storeyId, params) {
1027
+ const placementId = this.addLocalPlacement(this.worldPlacementId, {
1028
+ Location: params.Position,
1029
+ });
1030
+ const profileId = this.addCircleHollowProfile(params.Radius, params.WallThickness);
1031
+ const solidId = this.addExtrudedAreaSolid(profileId, params.Height);
1032
+ const shapeId = this.addShapeRepresentation('Body', [solidId]);
1033
+ const prodShapeId = this.addProductDefinitionShape([shapeId]);
1034
+ const colId = this.id();
1035
+ const globalId = this.newGlobalId();
1036
+ const name = params.Name ?? 'Column';
1037
+ const desc = params.Description ? `'${esc(params.Description)}'` : '$';
1038
+ const objType = params.ObjectType ? `'${esc(params.ObjectType)}'` : '$';
1039
+ const tag = params.Tag ? `'${esc(params.Tag)}'` : '$';
1040
+ this.line(colId, 'IFCCOLUMN', `'${globalId}',#${this.ownerHistoryId},'${esc(name)}',${desc},${objType},#${placementId},#${prodShapeId},${tag},.COLUMN.`);
1041
+ this.elementSolids.set(colId, [solidId]);
1042
+ this.trackElement(storeyId, colId);
1043
+ this.entities.push({ expressId: colId, type: 'IfcColumn', Name: name });
1044
+ return colId;
1045
+ }
1046
+ /**
1047
+ * Create a beam/column with hollow rectangular (tube) cross-section.
1048
+ */
1049
+ addIfcRectangleHollowBeam(storeyId, params) {
1050
+ const dx = params.End[0] - params.Start[0];
1051
+ const dy = params.End[1] - params.Start[1];
1052
+ const dz = params.End[2] - params.Start[2];
1053
+ const beamLen = Math.sqrt(dx * dx + dy * dy + dz * dz);
1054
+ const dir = vecNorm([dx, dy, dz]);
1055
+ const placementId = this.addLocalPlacement(this.worldPlacementId, {
1056
+ Location: params.Start,
1057
+ Axis: dir,
1058
+ RefDirection: this.computeRefDirection(dir),
1059
+ });
1060
+ const profileId = this.addRectangleHollowProfile(params.XDim, params.YDim, params.WallThickness, params.InnerFilletRadius, params.OuterFilletRadius);
1061
+ const solidId = this.addExtrudedAreaSolid(profileId, beamLen);
1062
+ const shapeId = this.addShapeRepresentation('Body', [solidId]);
1063
+ const prodShapeId = this.addProductDefinitionShape([shapeId]);
1064
+ const beamId = this.id();
1065
+ const globalId = this.newGlobalId();
1066
+ const name = params.Name ?? 'Beam';
1067
+ const desc = params.Description ? `'${esc(params.Description)}'` : '$';
1068
+ const objType = params.ObjectType ? `'${esc(params.ObjectType)}'` : '$';
1069
+ const tag = params.Tag ? `'${esc(params.Tag)}'` : '$';
1070
+ this.line(beamId, 'IFCBEAM', `'${globalId}',#${this.ownerHistoryId},'${esc(name)}',${desc},${objType},#${placementId},#${prodShapeId},${tag},.BEAM.`);
1071
+ this.elementSolids.set(beamId, [solidId]);
1072
+ this.trackElement(storeyId, beamId);
1073
+ this.entities.push({ expressId: beamId, type: 'IfcBeam', Name: name });
1074
+ return beamId;
1075
+ }
334
1076
  // ============================================================================
335
1077
  // Public API — Properties & Quantities
336
1078
  // ============================================================================
@@ -344,11 +1086,11 @@ export class IfcCreator {
344
1086
  propIds.push(propId);
345
1087
  }
346
1088
  const psetId = this.id();
347
- const globalId = newGlobalId();
1089
+ const globalId = this.newGlobalId();
348
1090
  const refs = propIds.map(id => `#${id}`).join(',');
349
1091
  this.line(psetId, 'IFCPROPERTYSET', `'${globalId}',#${this.ownerHistoryId},'${esc(pset.Name)}',$,(${refs})`);
350
1092
  const relId = this.id();
351
- const relGlobalId = newGlobalId();
1093
+ const relGlobalId = this.newGlobalId();
352
1094
  this.line(relId, 'IFCRELDEFINESBYPROPERTIES', `'${relGlobalId}',#${this.ownerHistoryId},$,$,(#${elementId}),#${psetId}`);
353
1095
  return psetId;
354
1096
  }
@@ -362,11 +1104,11 @@ export class IfcCreator {
362
1104
  qtyIds.push(qtyId);
363
1105
  }
364
1106
  const qsetId = this.id();
365
- const globalId = newGlobalId();
1107
+ const globalId = this.newGlobalId();
366
1108
  const refs = qtyIds.map(id => `#${id}`).join(',');
367
1109
  this.line(qsetId, 'IFCELEMENTQUANTITY', `'${globalId}',#${this.ownerHistoryId},'${esc(qset.Name)}',$,$,(${refs})`);
368
1110
  const relId = this.id();
369
- const relGlobalId = newGlobalId();
1111
+ const relGlobalId = this.newGlobalId();
370
1112
  this.line(relId, 'IFCRELDEFINESBYPROPERTIES', `'${relGlobalId}',#${this.ownerHistoryId},$,$,(#${elementId}),#${qsetId}`);
371
1113
  return qsetId;
372
1114
  }
@@ -495,19 +1237,19 @@ ENDSEC;
495
1237
  this.defaultStyleId = this.buildDefaultStyle();
496
1238
  // IfcProject
497
1239
  this.projectId = this.id();
498
- const projectGlobalId = newGlobalId();
1240
+ const projectGlobalId = this.newGlobalId();
499
1241
  const projectName = params.Name ?? 'Project';
500
1242
  const projectDesc = params.Description ? `'${esc(params.Description)}'` : '$';
501
1243
  this.line(this.projectId, 'IFCPROJECT', `'${projectGlobalId}',#${this.ownerHistoryId},'${esc(projectName)}',${projectDesc},$,$,$,(#${this.contextId}),#${this.unitAssignmentId}`);
502
1244
  this.entities.push({ expressId: this.projectId, type: 'IfcProject', Name: projectName });
503
1245
  // IfcSite
504
1246
  this.siteId = this.id();
505
- const siteGlobalId = newGlobalId();
1247
+ const siteGlobalId = this.newGlobalId();
506
1248
  this.line(this.siteId, 'IFCSITE', `'${siteGlobalId}',#${this.ownerHistoryId},'Site',$,$,#${this.worldPlacementId},$,$,.ELEMENT.,$,$,$,$,$`);
507
1249
  this.entities.push({ expressId: this.siteId, type: 'IfcSite', Name: 'Site' });
508
1250
  // IfcBuilding
509
1251
  this.buildingId = this.id();
510
- const buildingGlobalId = newGlobalId();
1252
+ const buildingGlobalId = this.newGlobalId();
511
1253
  this.line(this.buildingId, 'IFCBUILDING', `'${buildingGlobalId}',#${this.ownerHistoryId},'Building',$,$,#${this.worldPlacementId},$,$,.ELEMENT.,$,$,$`);
512
1254
  this.entities.push({ expressId: this.buildingId, type: 'IfcBuilding', Name: 'Building' });
513
1255
  }
@@ -609,7 +1351,7 @@ ENDSEC;
609
1351
  }
610
1352
  for (const [materialRefId, elementIds] of groups) {
611
1353
  const relId = this.id();
612
- const globalId = newGlobalId();
1354
+ const globalId = this.newGlobalId();
613
1355
  const refs = elementIds.map(id => `#${id}`).join(',');
614
1356
  this.line(relId, 'IFCRELASSOCIATESMATERIAL', `'${globalId}',#${this.ownerHistoryId},$,$,(${refs}),#${materialRefId}`);
615
1357
  }
@@ -639,6 +1381,11 @@ ENDSEC;
639
1381
  this.line(id, 'IFCAXIS2PLACEMENT3D', `#${originId},${axis},${refDir}`);
640
1382
  return id;
641
1383
  }
1384
+ /**
1385
+ * Create a local placement relative to the world coordinate system.
1386
+ * @param relativeTo - Parent placement ID (use getWorldPlacementId() for world origin)
1387
+ * @param placement - Location and optional axis/ref directions
1388
+ */
642
1389
  addLocalPlacement(relativeTo, placement) {
643
1390
  const originId = this.addCartesianPoint(placement.Location);
644
1391
  let axisId;
@@ -670,6 +1417,85 @@ ENDSEC;
670
1417
  this.line(id, 'IFCRECTANGLEPROFILEDEF', `.AREA.,$,#${profileAxis2dId},${num(xDim)},${num(yDim)}`);
671
1418
  return id;
672
1419
  }
1420
+ /** Create a circle profile. */
1421
+ addCircleProfile(radius) {
1422
+ const profileOriginId = this.addCartesianPoint2D([0, 0]);
1423
+ const profileAxis2dId = this.id();
1424
+ this.line(profileAxis2dId, 'IFCAXIS2PLACEMENT2D', `#${profileOriginId},$`);
1425
+ const id = this.id();
1426
+ this.line(id, 'IFCCIRCLEPROFILEDEF', `.AREA.,$,#${profileAxis2dId},${num(radius)}`);
1427
+ return id;
1428
+ }
1429
+ /** Create a hollow circle profile (pipe section). */
1430
+ addCircleHollowProfile(radius, wallThickness) {
1431
+ const profileOriginId = this.addCartesianPoint2D([0, 0]);
1432
+ const profileAxis2dId = this.id();
1433
+ this.line(profileAxis2dId, 'IFCAXIS2PLACEMENT2D', `#${profileOriginId},$`);
1434
+ const id = this.id();
1435
+ this.line(id, 'IFCCIRCLEHOLLOWPROFILEDEF', `.AREA.,$,#${profileAxis2dId},${num(radius)},${num(wallThickness)}`);
1436
+ return id;
1437
+ }
1438
+ /** Create an I-shape (wide-flange / H-beam) profile. */
1439
+ addIShapeProfile(overallWidth, overallDepth, webThickness, flangeThickness, filletRadius) {
1440
+ const profileOriginId = this.addCartesianPoint2D([0, 0]);
1441
+ const profileAxis2dId = this.id();
1442
+ this.line(profileAxis2dId, 'IFCAXIS2PLACEMENT2D', `#${profileOriginId},$`);
1443
+ const id = this.id();
1444
+ const fillet = filletRadius !== undefined ? num(filletRadius) : '$';
1445
+ this.line(id, 'IFCISHAPEPROFILEDEF', `.AREA.,$,#${profileAxis2dId},${num(overallWidth)},${num(overallDepth)},${num(webThickness)},${num(flangeThickness)},${fillet},$,$`);
1446
+ return id;
1447
+ }
1448
+ /** Create an L-shape (angle section) profile. */
1449
+ addLShapeProfile(depth, width, thickness, filletRadius) {
1450
+ const profileOriginId = this.addCartesianPoint2D([0, 0]);
1451
+ const profileAxis2dId = this.id();
1452
+ this.line(profileAxis2dId, 'IFCAXIS2PLACEMENT2D', `#${profileOriginId},$`);
1453
+ const id = this.id();
1454
+ const fillet = filletRadius !== undefined ? num(filletRadius) : '$';
1455
+ this.line(id, 'IFCLSHAPEPROFILEDEF', `.AREA.,$,#${profileAxis2dId},${num(depth)},${num(width)},${num(thickness)},${fillet},$,$`);
1456
+ return id;
1457
+ }
1458
+ /** Create a T-shape (tee section) profile. */
1459
+ addTShapeProfile(flangeWidth, depth, webThickness, flangeThickness, filletRadius) {
1460
+ const profileOriginId = this.addCartesianPoint2D([0, 0]);
1461
+ const profileAxis2dId = this.id();
1462
+ this.line(profileAxis2dId, 'IFCAXIS2PLACEMENT2D', `#${profileOriginId},$`);
1463
+ const id = this.id();
1464
+ const fillet = filletRadius !== undefined ? num(filletRadius) : '$';
1465
+ this.line(id, 'IFCTSHAPEPROFILEDEF', `.AREA.,$,#${profileAxis2dId},${num(depth)},${num(flangeWidth)},${num(webThickness)},${num(flangeThickness)},${fillet},$,$,$,$`);
1466
+ return id;
1467
+ }
1468
+ /** Create a U-shape (channel section) profile. */
1469
+ addUShapeProfile(depth, flangeWidth, webThickness, flangeThickness, filletRadius) {
1470
+ const profileOriginId = this.addCartesianPoint2D([0, 0]);
1471
+ const profileAxis2dId = this.id();
1472
+ this.line(profileAxis2dId, 'IFCAXIS2PLACEMENT2D', `#${profileOriginId},$`);
1473
+ const id = this.id();
1474
+ const fillet = filletRadius !== undefined ? num(filletRadius) : '$';
1475
+ this.line(id, 'IFCUSHAPEPROFILEDEF', `.AREA.,$,#${profileAxis2dId},${num(depth)},${num(flangeWidth)},${num(webThickness)},${num(flangeThickness)},${fillet},$`);
1476
+ return id;
1477
+ }
1478
+ /** Create a C-shape (cold-formed channel) profile. */
1479
+ addCShapeProfile(depth, width, wallThickness, girth) {
1480
+ const profileOriginId = this.addCartesianPoint2D([0, 0]);
1481
+ const profileAxis2dId = this.id();
1482
+ this.line(profileAxis2dId, 'IFCAXIS2PLACEMENT2D', `#${profileOriginId},$`);
1483
+ const id = this.id();
1484
+ this.line(id, 'IFCCSHAPEPROFILEDEF', `.AREA.,$,#${profileAxis2dId},${num(depth)},${num(width)},${num(wallThickness)},${num(girth)},$`);
1485
+ return id;
1486
+ }
1487
+ /** Create a hollow rectangle (tube section) profile. */
1488
+ addRectangleHollowProfile(xDim, yDim, wallThickness, innerFilletRadius, outerFilletRadius) {
1489
+ const profileOriginId = this.addCartesianPoint2D([0, 0]);
1490
+ const profileAxis2dId = this.id();
1491
+ this.line(profileAxis2dId, 'IFCAXIS2PLACEMENT2D', `#${profileOriginId},$`);
1492
+ const id = this.id();
1493
+ const inner = innerFilletRadius !== undefined ? num(innerFilletRadius) : '$';
1494
+ const outer = outerFilletRadius !== undefined ? num(outerFilletRadius) : '$';
1495
+ this.line(id, 'IFCRECTANGLEHOLLOWPROFILEDEF', `.AREA.,$,#${profileAxis2dId},${num(xDim)},${num(yDim)},${num(wallThickness)},${inner},${outer}`);
1496
+ return id;
1497
+ }
1498
+ /** Create an arbitrary closed profile from a polyline. Points are auto-closed. */
673
1499
  addArbitraryProfile(points) {
674
1500
  const pointIds = points.map(p => this.addCartesianPoint2D(p));
675
1501
  if (points.length > 0) {
@@ -682,14 +1508,28 @@ ENDSEC;
682
1508
  this.line(id, 'IFCARBITRARYCLOSEDPROFILEDEF', `.AREA.,$,#${polylineId}`);
683
1509
  return id;
684
1510
  }
685
- addExtrudedAreaSolid(profileId, depth, extrusionDir) {
686
- const originId = this.addCartesianPoint([0, 0, 0]);
687
- const axis2Id = this.addAxis2Placement3D(originId);
1511
+ /**
1512
+ * Create an extruded area solid from a profile.
1513
+ * @param profileId - ID returned by any addXxxProfile() method
1514
+ * @param depth - Extrusion depth
1515
+ * @param extrusionDir - Optional direction ID (default: Z-up)
1516
+ * @param positionId - Optional local solid placement (default: origin, world axes)
1517
+ */
1518
+ addExtrudedAreaSolid(profileId, depth, extrusionDir, positionId) {
1519
+ const axis2Id = positionId ?? (() => {
1520
+ const originId = this.addCartesianPoint([0, 0, 0]);
1521
+ return this.addAxis2Placement3D(originId);
1522
+ })();
688
1523
  const dirRef = extrusionDir ?? this.dirZ;
689
1524
  const id = this.id();
690
1525
  this.line(id, 'IFCEXTRUDEDAREASOLID', `#${profileId},#${axis2Id},#${dirRef},${num(depth)}`);
691
1526
  return id;
692
1527
  }
1528
+ /**
1529
+ * Create a shape representation from solid IDs.
1530
+ * @param repType - 'Body' or 'Axis'
1531
+ * @param itemIds - Array of solid IDs (from addExtrudedAreaSolid, etc.)
1532
+ */
693
1533
  addShapeRepresentation(repType, itemIds) {
694
1534
  const contextRef = repType === 'Axis' ? this.subContextAxis : this.subContextBody;
695
1535
  const refs = itemIds.map(id => `#${id}`).join(',');
@@ -699,6 +1539,7 @@ ENDSEC;
699
1539
  this.line(repId, 'IFCSHAPEREPRESENTATION', `#${contextRef},'${repIdentifier}','${repTypeName}',(${refs})`);
700
1540
  return repId;
701
1541
  }
1542
+ /** Wrap shape representations into a product definition shape. */
702
1543
  addProductDefinitionShape(repIds) {
703
1544
  const refs = repIds.map(id => `#${id}`).join(',');
704
1545
  const id = this.id();
@@ -706,6 +1547,202 @@ ENDSEC;
706
1547
  return id;
707
1548
  }
708
1549
  // ============================================================================
1550
+ // Public API — Low-level helpers
1551
+ // ============================================================================
1552
+ /** Get the world placement ID (use as relativeTo for addLocalPlacement). */
1553
+ getWorldPlacementId() {
1554
+ return this.worldPlacementId;
1555
+ }
1556
+ /** Create a direction entity. Returns the direction ID. */
1557
+ addDirection3D(d) {
1558
+ return this.addDirection(d);
1559
+ }
1560
+ /**
1561
+ * Create a profile from a ProfileDef union type.
1562
+ * This is the high-level entry point for profile creation — it dispatches
1563
+ * to the appropriate addXxxProfile() method based on the shape.
1564
+ *
1565
+ * ```ts
1566
+ * const profileId = creator.createProfile({
1567
+ * ProfileType: 'AREA',
1568
+ * Radius: 0.15,
1569
+ * }); // Creates a circle profile
1570
+ * ```
1571
+ */
1572
+ createProfile(profile) {
1573
+ if ('OuterCurve' in profile) {
1574
+ return this.addArbitraryProfile(profile.OuterCurve);
1575
+ }
1576
+ if ('Radius' in profile && 'WallThickness' in profile) {
1577
+ return this.addCircleHollowProfile(profile.Radius, profile.WallThickness);
1578
+ }
1579
+ if ('Radius' in profile) {
1580
+ return this.addCircleProfile(profile.Radius);
1581
+ }
1582
+ if ('OverallWidth' in profile && 'WebThickness' in profile) {
1583
+ // IShapeProfile
1584
+ return this.addIShapeProfile(profile.OverallWidth, profile.OverallDepth, profile.WebThickness, profile.FlangeThickness, profile.FilletRadius);
1585
+ }
1586
+ // T-shape and U-shape are structurally identical — use Shape discriminator
1587
+ if ('Shape' in profile && profile.Shape === 'IfcTShapeProfileDef') {
1588
+ const p = profile;
1589
+ return this.addTShapeProfile(p.FlangeWidth, p.Depth, p.WebThickness, p.FlangeThickness, p.FilletRadius);
1590
+ }
1591
+ if ('Shape' in profile && profile.Shape === 'IfcUShapeProfileDef') {
1592
+ const p = profile;
1593
+ return this.addUShapeProfile(p.Depth, p.FlangeWidth, p.WebThickness, p.FlangeThickness, p.FilletRadius);
1594
+ }
1595
+ if ('Girth' in profile) {
1596
+ // CShapeProfile
1597
+ const p = profile;
1598
+ return this.addCShapeProfile(p.Depth, p.Width, p.WallThickness, p.Girth);
1599
+ }
1600
+ if ('XDim' in profile && 'YDim' in profile && 'WallThickness' in profile) {
1601
+ // RectangleHollowProfile
1602
+ const p = profile;
1603
+ return this.addRectangleHollowProfile(p.XDim, p.YDim, p.WallThickness, p.InnerFilletRadius, p.OuterFilletRadius);
1604
+ }
1605
+ if ('XDim' in profile && 'YDim' in profile) {
1606
+ // RectangleProfile
1607
+ return this.addRectangleProfile(profile.XDim, profile.YDim);
1608
+ }
1609
+ if ('Depth' in profile && 'Width' in profile && 'Thickness' in profile) {
1610
+ // LShapeProfile
1611
+ const p = profile;
1612
+ return this.addLShapeProfile(p.Depth, p.Width, p.Thickness, p.FilletRadius);
1613
+ }
1614
+ throw new Error('Unrecognized profile shape — ensure ProfileType is "AREA" and required fields are set');
1615
+ }
1616
+ // ============================================================================
1617
+ // Public API — Generic element creation
1618
+ // ============================================================================
1619
+ /**
1620
+ * Create ANY IFC element type with an extruded profile at a placement.
1621
+ *
1622
+ * This is the low-level foundation that all high-level methods (addIfcWall,
1623
+ * addIfcBeam, etc.) are built on. Use it when you need an IFC type that
1624
+ * doesn't have a dedicated method, or when you need full control.
1625
+ *
1626
+ * ```ts
1627
+ * // Pipe segment with circular profile
1628
+ * creator.addElement(storeyId, {
1629
+ * IfcType: 'IFCFLOWSEGMENT',
1630
+ * Placement: { Location: [0, 0, 3] },
1631
+ * Profile: { ProfileType: 'AREA', Radius: 0.05 },
1632
+ * Depth: 5,
1633
+ * PredefinedType: '.RIGIDSEGMENT.',
1634
+ * Name: 'Pipe-001',
1635
+ * });
1636
+ *
1637
+ * // Distribution element with L-profile
1638
+ * creator.addElement(storeyId, {
1639
+ * IfcType: 'IFCDISTRIBUTIONELEMENT',
1640
+ * Placement: { Location: [2, 0, 0], Axis: [0, 0, 1], RefDirection: [1, 0, 0] },
1641
+ * Profile: { ProfileType: 'AREA', Depth: 0.1, Width: 0.1, Thickness: 0.01 },
1642
+ * Depth: 3,
1643
+ * });
1644
+ * ```
1645
+ */
1646
+ addElement(storeyId, params) {
1647
+ const placementId = this.addLocalPlacement(this.worldPlacementId, params.Placement);
1648
+ const profileId = this.createProfile(params.Profile);
1649
+ // Handle custom extrusion direction
1650
+ let extrusionDirId;
1651
+ if (params.ExtrusionDirection) {
1652
+ extrusionDirId = this.addDirection(params.ExtrusionDirection);
1653
+ }
1654
+ const solidId = this.addExtrudedAreaSolid(profileId, params.Depth, extrusionDirId);
1655
+ const shapeId = this.addShapeRepresentation('Body', [solidId]);
1656
+ const prodShapeId = this.addProductDefinitionShape([shapeId]);
1657
+ const elementId = this.id();
1658
+ const globalId = this.newGlobalId();
1659
+ const name = params.Name ?? params.IfcType;
1660
+ const desc = params.Description ? `'${esc(params.Description)}'` : '$';
1661
+ const objType = params.ObjectType ? `'${esc(params.ObjectType)}'` : '$';
1662
+ const tag = params.Tag ? `'${esc(params.Tag)}'` : '$';
1663
+ const ifcType = params.IfcType.toUpperCase();
1664
+ if (NON_ELEMENT_TYPES.has(ifcType)) {
1665
+ // Non-element types: GlobalId, OwnerHistory, Name, Description, ObjectType, ObjectPlacement, Representation
1666
+ this.line(elementId, params.IfcType, `'${globalId}',#${this.ownerHistoryId},'${esc(name)}',${desc},${objType},#${placementId},#${prodShapeId}`);
1667
+ }
1668
+ else {
1669
+ // IfcElement subtypes: ...Tag, PredefinedType
1670
+ const predefinedType = params.PredefinedType ? `.${params.PredefinedType}.` : '.NOTDEFINED.';
1671
+ this.line(elementId, params.IfcType, `'${globalId}',#${this.ownerHistoryId},'${esc(name)}',${desc},${objType},#${placementId},#${prodShapeId},${tag},${predefinedType}`);
1672
+ }
1673
+ this.elementSolids.set(elementId, [solidId]);
1674
+ this.trackElement(storeyId, elementId);
1675
+ this.entities.push({ expressId: elementId, type: params.IfcType, Name: name });
1676
+ return elementId;
1677
+ }
1678
+ /**
1679
+ * Create ANY IFC element type extruded along an axis (Start → End).
1680
+ *
1681
+ * The profile is placed at Start and extruded along the direction to End.
1682
+ * The extrusion length equals the distance between Start and End.
1683
+ *
1684
+ * ```ts
1685
+ * // Pipe segment along an axis
1686
+ * creator.addAxisElement(storeyId, {
1687
+ * IfcType: 'IFCPIPESEGMENT',
1688
+ * Start: [0, 0, 3],
1689
+ * End: [5, 0, 3],
1690
+ * Profile: { ProfileType: 'AREA', Radius: 0.05 },
1691
+ * Name: 'Pipe-001',
1692
+ * });
1693
+ *
1694
+ * // Cable tray with rectangle profile
1695
+ * creator.addAxisElement(storeyId, {
1696
+ * IfcType: 'IFCCABLETRAYSEGMENT',
1697
+ * Start: [0, 0, 2.5],
1698
+ * End: [10, 0, 2.5],
1699
+ * Profile: { ProfileType: 'AREA', XDim: 0.3, YDim: 0.1 },
1700
+ * });
1701
+ * ```
1702
+ */
1703
+ addAxisElement(storeyId, params) {
1704
+ const dx = params.End[0] - params.Start[0];
1705
+ const dy = params.End[1] - params.Start[1];
1706
+ const dz = params.End[2] - params.Start[2];
1707
+ const length = Math.sqrt(dx * dx + dy * dy + dz * dz);
1708
+ const dir = vecNorm([dx, dy, dz]);
1709
+ // Compute a perpendicular vector for the profile plane
1710
+ const up = [0, 0, 1];
1711
+ let perp = vecCross(dir, up);
1712
+ if (vecLen(perp) < 1e-6) {
1713
+ // dir is parallel to Z, use X as reference
1714
+ perp = vecCross(dir, [1, 0, 0]);
1715
+ }
1716
+ perp = vecNorm(perp);
1717
+ const placementId = this.addLocalPlacement(this.worldPlacementId, {
1718
+ Location: params.Start,
1719
+ Axis: dir, // local Z = along axis (extrusion direction)
1720
+ RefDirection: perp, // local X = perpendicular to axis
1721
+ });
1722
+ const profileId = this.createProfile(params.Profile);
1723
+ const solidId = this.addExtrudedAreaSolid(profileId, length);
1724
+ const shapeId = this.addShapeRepresentation('Body', [solidId]);
1725
+ const prodShapeId = this.addProductDefinitionShape([shapeId]);
1726
+ const elementId = this.id();
1727
+ const globalId = this.newGlobalId();
1728
+ const name = params.Name ?? params.IfcType;
1729
+ const desc = params.Description ? `'${esc(params.Description)}'` : '$';
1730
+ const objType = params.ObjectType ? `'${esc(params.ObjectType)}'` : '$';
1731
+ const tag = params.Tag ? `'${esc(params.Tag)}'` : '$';
1732
+ const ifcType = params.IfcType.toUpperCase();
1733
+ if (NON_ELEMENT_TYPES.has(ifcType)) {
1734
+ this.line(elementId, params.IfcType, `'${globalId}',#${this.ownerHistoryId},'${esc(name)}',${desc},${objType},#${placementId},#${prodShapeId}`);
1735
+ }
1736
+ else {
1737
+ const predefinedType = params.PredefinedType ? `.${params.PredefinedType}.` : '.NOTDEFINED.';
1738
+ this.line(elementId, params.IfcType, `'${globalId}',#${this.ownerHistoryId},'${esc(name)}',${desc},${objType},#${placementId},#${prodShapeId},${tag},${predefinedType}`);
1739
+ }
1740
+ this.elementSolids.set(elementId, [solidId]);
1741
+ this.trackElement(storeyId, elementId);
1742
+ this.entities.push({ expressId: elementId, type: params.IfcType, Name: name });
1743
+ return elementId;
1744
+ }
1745
+ // ============================================================================
709
1746
  // Internal — Openings
710
1747
  // ============================================================================
711
1748
  /**
@@ -741,11 +1778,11 @@ ENDSEC;
741
1778
  const shapeId = this.addShapeRepresentation('Body', [solidId]);
742
1779
  const prodShapeId = this.addProductDefinitionShape([shapeId]);
743
1780
  const openingId = this.id();
744
- const globalId = newGlobalId();
1781
+ const globalId = this.newGlobalId();
745
1782
  const name = opening.Name ?? 'Opening';
746
1783
  this.line(openingId, 'IFCOPENINGELEMENT', `'${globalId}',#${this.ownerHistoryId},'${esc(name)}',$,$,#${openingPlacementId},#${prodShapeId},$,.OPENING.`);
747
1784
  const relId = this.id();
748
- const relGlobalId = newGlobalId();
1785
+ const relGlobalId = this.newGlobalId();
749
1786
  this.line(relId, 'IFCRELVOIDSELEMENT', `'${relGlobalId}',#${this.ownerHistoryId},$,$,#${hostId},#${openingId}`);
750
1787
  this.entities.push({ expressId: openingId, type: 'IfcOpeningElement', Name: name });
751
1788
  return openingId;
@@ -765,11 +1802,11 @@ ENDSEC;
765
1802
  const shapeId = this.addShapeRepresentation('Body', [solidId]);
766
1803
  const prodShapeId = this.addProductDefinitionShape([shapeId]);
767
1804
  const openingId = this.id();
768
- const globalId = newGlobalId();
1805
+ const globalId = this.newGlobalId();
769
1806
  const name = opening.Name ?? 'Opening';
770
1807
  this.line(openingId, 'IFCOPENINGELEMENT', `'${globalId}',#${this.ownerHistoryId},'${esc(name)}',$,$,#${placementId},#${prodShapeId},$,.OPENING.`);
771
1808
  const relId = this.id();
772
- const relGlobalId = newGlobalId();
1809
+ const relGlobalId = this.newGlobalId();
773
1810
  this.line(relId, 'IFCRELVOIDSELEMENT', `'${relGlobalId}',#${this.ownerHistoryId},$,$,#${hostId},#${openingId}`);
774
1811
  this.entities.push({ expressId: openingId, type: 'IfcOpeningElement', Name: name });
775
1812
  return openingId;
@@ -825,16 +1862,21 @@ ENDSEC;
825
1862
  }
826
1863
  addIfcRelAggregates(relatingId, relatedIds) {
827
1864
  const relId = this.id();
828
- const globalId = newGlobalId();
1865
+ const globalId = this.newGlobalId();
829
1866
  const refs = relatedIds.map(id => `#${id}`).join(',');
830
1867
  this.line(relId, 'IFCRELAGGREGATES', `'${globalId}',#${this.ownerHistoryId},$,$,#${relatingId},(${refs})`);
831
1868
  }
832
1869
  addIfcRelContainedInSpatialStructure(storeyId, elementIds) {
833
1870
  const relId = this.id();
834
- const globalId = newGlobalId();
1871
+ const globalId = this.newGlobalId();
835
1872
  const refs = elementIds.map(id => `#${id}`).join(',');
836
1873
  this.line(relId, 'IFCRELCONTAINEDINSPATIALSTRUCTURE', `'${globalId}',#${this.ownerHistoryId},$,$,(${refs}),#${storeyId}`);
837
1874
  }
1875
+ addIfcRelFillsElement(openingId, fillingId) {
1876
+ const relId = this.id();
1877
+ const globalId = this.newGlobalId();
1878
+ this.line(relId, 'IFCRELFILLSELEMENT', `'${globalId}',#${this.ownerHistoryId},$,$,#${openingId},#${fillingId}`);
1879
+ }
838
1880
  // ============================================================================
839
1881
  // Internal — Utilities
840
1882
  // ============================================================================
@@ -844,12 +1886,35 @@ ENDSEC;
844
1886
  line(id, type, args) {
845
1887
  this.lines.push(stepLine(id, type, args));
846
1888
  }
1889
+ getHostedWallInfo(wallId) {
1890
+ const storeyId = this.elementStoreys.get(wallId);
1891
+ const placementId = this.wallPlacements.get(wallId);
1892
+ const wallThickness = this.wallThicknesses.get(wallId);
1893
+ if (storeyId === undefined || placementId === undefined || wallThickness === undefined) {
1894
+ throw new Error(`Unknown wallId #${wallId} — call addIfcWall() first`);
1895
+ }
1896
+ return { storeyId, placementId, wallThickness };
1897
+ }
1898
+ addHostedWallFillPlacement(hostPlacementId, position, wallThickness) {
1899
+ const originId = this.addCartesianPoint([
1900
+ position[0],
1901
+ wallThickness / 2,
1902
+ position[2],
1903
+ ]);
1904
+ const axisId = this.addDirection([0, -1, 0]);
1905
+ const refDirId = this.addDirection([1, 0, 0]);
1906
+ const axis2Id = this.addAxis2Placement3D(originId, axisId, refDirId);
1907
+ const placementId = this.id();
1908
+ this.line(placementId, 'IFCLOCALPLACEMENT', `#${hostPlacementId},#${axis2Id}`);
1909
+ return placementId;
1910
+ }
847
1911
  trackElement(storeyId, elementId) {
848
1912
  const elements = this.storeyElements.get(storeyId);
849
1913
  if (!elements) {
850
1914
  throw new Error(`Unknown storeyId #${storeyId} — call addIfcBuildingStorey() first`);
851
1915
  }
852
1916
  elements.push(elementId);
1917
+ this.elementStoreys.set(elementId, storeyId);
853
1918
  }
854
1919
  /** Compute a stable RefDirection perpendicular to a given Axis */
855
1920
  computeRefDirection(axis) {