@ifc-lite/create 1.14.2 → 1.14.3

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