@ifc-lite/create 1.14.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +373 -0
- package/dist/ifc-creator.d.ts +156 -0
- package/dist/ifc-creator.d.ts.map +1 -0
- package/dist/ifc-creator.js +861 -0
- package/dist/ifc-creator.js.map +1 -0
- package/dist/index.d.ts +21 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +23 -0
- package/dist/index.js.map +1 -0
- package/dist/types.d.ts +213 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +5 -0
- package/dist/types.js.map +1 -0
- package/package.json +47 -0
|
@@ -0,0 +1,861 @@
|
|
|
1
|
+
/* This Source Code Form is subject to the terms of the Mozilla Public
|
|
2
|
+
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
3
|
+
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
|
|
4
|
+
// ============================================================================
|
|
5
|
+
// Internal helpers
|
|
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
|
+
/** Escape a string for STEP format */
|
|
17
|
+
function esc(str) {
|
|
18
|
+
return str.replace(/\\/g, '\\\\').replace(/'/g, "''");
|
|
19
|
+
}
|
|
20
|
+
/** Format a STEP line: #ID=TYPE(args); */
|
|
21
|
+
function stepLine(id, type, args) {
|
|
22
|
+
return `#${id}=${type}(${args});`;
|
|
23
|
+
}
|
|
24
|
+
/** Serialize a number in STEP format (always with decimal point, no exponent notation) */
|
|
25
|
+
function num(v) {
|
|
26
|
+
// Exponent notation (e.g. 1e-7) is not valid STEP — use fixed decimal
|
|
27
|
+
const s = v.toString();
|
|
28
|
+
if (s.includes('e') || s.includes('E'))
|
|
29
|
+
return v.toFixed(10).replace(/0+$/, '0');
|
|
30
|
+
return s.includes('.') ? s : s + '.';
|
|
31
|
+
}
|
|
32
|
+
/** Vector length */
|
|
33
|
+
function vecLen(v) {
|
|
34
|
+
return Math.sqrt(v[0] * v[0] + v[1] * v[1] + v[2] * v[2]);
|
|
35
|
+
}
|
|
36
|
+
/** Normalize vector — throws on zero-length (indicates geometry bug like Start === End) */
|
|
37
|
+
function vecNorm(v) {
|
|
38
|
+
const len = vecLen(v);
|
|
39
|
+
if (len === 0)
|
|
40
|
+
throw new Error('Cannot normalize zero-length vector (check that Start and End are not identical)');
|
|
41
|
+
return [v[0] / len, v[1] / len, v[2] / len];
|
|
42
|
+
}
|
|
43
|
+
/** Cross product */
|
|
44
|
+
function vecCross(a, b) {
|
|
45
|
+
return [
|
|
46
|
+
a[1] * b[2] - a[2] * b[1],
|
|
47
|
+
a[2] * b[0] - a[0] * b[2],
|
|
48
|
+
a[0] * b[1] - a[1] * b[0],
|
|
49
|
+
];
|
|
50
|
+
}
|
|
51
|
+
// ============================================================================
|
|
52
|
+
// IfcCreator
|
|
53
|
+
// ============================================================================
|
|
54
|
+
export class IfcCreator {
|
|
55
|
+
nextId = 1;
|
|
56
|
+
lines = [];
|
|
57
|
+
entities = [];
|
|
58
|
+
schema;
|
|
59
|
+
// Shared entity IDs (created in constructor)
|
|
60
|
+
projectId = 0;
|
|
61
|
+
siteId = 0;
|
|
62
|
+
buildingId = 0;
|
|
63
|
+
ownerHistoryId = 0;
|
|
64
|
+
contextId = 0;
|
|
65
|
+
subContextBody = 0;
|
|
66
|
+
subContextAxis = 0;
|
|
67
|
+
originId = 0;
|
|
68
|
+
dirZ = 0;
|
|
69
|
+
dirX = 0;
|
|
70
|
+
worldPlacementId = 0;
|
|
71
|
+
unitAssignmentId = 0;
|
|
72
|
+
// Guard against repeated toIfc() calls (relationships are not idempotent)
|
|
73
|
+
finalized = false;
|
|
74
|
+
// Default surface style (applied to elements without custom color)
|
|
75
|
+
defaultStyleId = 0;
|
|
76
|
+
// Per-element style tracking (deferred to finalization)
|
|
77
|
+
elementSolids = new Map();
|
|
78
|
+
elementColors = new Map();
|
|
79
|
+
// Material tracking (deferred IfcRelAssociatesMaterial at finalization)
|
|
80
|
+
materialCache = new Map(); // name → IfcMaterial id
|
|
81
|
+
elementMaterials = new Map(); // elementId → materialRefId
|
|
82
|
+
// Tracking for spatial aggregation
|
|
83
|
+
storeyIds = [];
|
|
84
|
+
storeyElements = new Map();
|
|
85
|
+
projectParams;
|
|
86
|
+
constructor(params = {}) {
|
|
87
|
+
this.projectParams = params;
|
|
88
|
+
this.schema = params.Schema ?? 'IFC4';
|
|
89
|
+
this.buildPreamble(params);
|
|
90
|
+
}
|
|
91
|
+
// ============================================================================
|
|
92
|
+
// Public API — Spatial Structure
|
|
93
|
+
// ============================================================================
|
|
94
|
+
/** Add a building storey. Returns the storey expressId for use with element creation. */
|
|
95
|
+
addIfcBuildingStorey(params) {
|
|
96
|
+
const id = this.id();
|
|
97
|
+
const globalId = newGlobalId();
|
|
98
|
+
const name = params.Name ?? 'Storey';
|
|
99
|
+
const desc = params.Description ? `'${esc(params.Description)}'` : '$';
|
|
100
|
+
const elevation = num(params.Elevation);
|
|
101
|
+
this.line(id, 'IFCBUILDINGSTOREY', `'${globalId}',#${this.ownerHistoryId},'${esc(name)}',${desc},$,$,#${this.worldPlacementId},$,.ELEMENT.,${elevation}`);
|
|
102
|
+
this.storeyIds.push(id);
|
|
103
|
+
this.storeyElements.set(id, []);
|
|
104
|
+
this.entities.push({ expressId: id, type: 'IfcBuildingStorey', Name: name });
|
|
105
|
+
return id;
|
|
106
|
+
}
|
|
107
|
+
// ============================================================================
|
|
108
|
+
// Public API — Building Elements
|
|
109
|
+
// ============================================================================
|
|
110
|
+
/**
|
|
111
|
+
* Create a wall from Start to End with given Thickness and Height.
|
|
112
|
+
*
|
|
113
|
+
* Geometry: placement at Start. Profile offset so the solid extends
|
|
114
|
+
* exactly from Start to End, centered on the thickness axis. Extruded
|
|
115
|
+
* upward by Height.
|
|
116
|
+
*/
|
|
117
|
+
addIfcWall(storeyId, params) {
|
|
118
|
+
const dx = params.End[0] - params.Start[0];
|
|
119
|
+
const dy = params.End[1] - params.Start[1];
|
|
120
|
+
const dz = params.End[2] - params.Start[2];
|
|
121
|
+
const wallLen = Math.sqrt(dx * dx + dy * dy + dz * dz);
|
|
122
|
+
const dir = vecNorm([dx, dy, dz]);
|
|
123
|
+
// Placement at Start. Local X = wall direction, Z = up (default).
|
|
124
|
+
const placementId = this.addLocalPlacement(this.worldPlacementId, {
|
|
125
|
+
Location: params.Start,
|
|
126
|
+
RefDirection: dir,
|
|
127
|
+
});
|
|
128
|
+
// Rectangle profile centered at (wallLen/2, 0) so it spans 0..wallLen along local X
|
|
129
|
+
// and -thickness/2..+thickness/2 along local Y.
|
|
130
|
+
const profileId = this.addRectangleProfile(wallLen, params.Thickness, [wallLen / 2, 0]);
|
|
131
|
+
// Extrude along Z (up) by Height
|
|
132
|
+
const solidId = this.addExtrudedAreaSolid(profileId, params.Height);
|
|
133
|
+
const shapeId = this.addShapeRepresentation('Body', [solidId]);
|
|
134
|
+
const prodShapeId = this.addProductDefinitionShape([shapeId]);
|
|
135
|
+
const wallId = this.id();
|
|
136
|
+
const globalId = newGlobalId();
|
|
137
|
+
const name = params.Name ?? 'Wall';
|
|
138
|
+
const desc = params.Description ? `'${esc(params.Description)}'` : '$';
|
|
139
|
+
const objType = params.ObjectType ? `'${esc(params.ObjectType)}'` : '$';
|
|
140
|
+
const tag = params.Tag ? `'${esc(params.Tag)}'` : '$';
|
|
141
|
+
this.line(wallId, 'IFCWALL', `'${globalId}',#${this.ownerHistoryId},'${esc(name)}',${desc},${objType},#${placementId},#${prodShapeId},${tag},.STANDARD.`);
|
|
142
|
+
this.elementSolids.set(wallId, [solidId]);
|
|
143
|
+
this.trackElement(storeyId, wallId);
|
|
144
|
+
this.entities.push({ expressId: wallId, type: 'IfcWall', Name: name });
|
|
145
|
+
// Add openings
|
|
146
|
+
if (params.Openings) {
|
|
147
|
+
for (const opening of params.Openings) {
|
|
148
|
+
this.addWallOpening(wallId, placementId, opening, params.Thickness);
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
return wallId;
|
|
152
|
+
}
|
|
153
|
+
/**
|
|
154
|
+
* Create a slab. Position is the minimum corner.
|
|
155
|
+
* Width along +X, Depth along +Y, Thickness extruded along +Z.
|
|
156
|
+
*/
|
|
157
|
+
addIfcSlab(storeyId, params) {
|
|
158
|
+
const placementId = this.addLocalPlacement(this.worldPlacementId, {
|
|
159
|
+
Location: params.Position,
|
|
160
|
+
});
|
|
161
|
+
let profileId;
|
|
162
|
+
if (params.Profile && params.Profile.length >= 3) {
|
|
163
|
+
profileId = this.addArbitraryProfile(params.Profile);
|
|
164
|
+
}
|
|
165
|
+
else {
|
|
166
|
+
const w = params.Width ?? 5;
|
|
167
|
+
const d = params.Depth ?? 5;
|
|
168
|
+
// Profile centered at (w/2, d/2) so slab starts at Position corner
|
|
169
|
+
profileId = this.addRectangleProfile(w, d, [w / 2, d / 2]);
|
|
170
|
+
}
|
|
171
|
+
const solidId = this.addExtrudedAreaSolid(profileId, params.Thickness);
|
|
172
|
+
const shapeId = this.addShapeRepresentation('Body', [solidId]);
|
|
173
|
+
const prodShapeId = this.addProductDefinitionShape([shapeId]);
|
|
174
|
+
const slabId = this.id();
|
|
175
|
+
const globalId = newGlobalId();
|
|
176
|
+
const name = params.Name ?? 'Slab';
|
|
177
|
+
const desc = params.Description ? `'${esc(params.Description)}'` : '$';
|
|
178
|
+
const objType = params.ObjectType ? `'${esc(params.ObjectType)}'` : '$';
|
|
179
|
+
const tag = params.Tag ? `'${esc(params.Tag)}'` : '$';
|
|
180
|
+
this.line(slabId, 'IFCSLAB', `'${globalId}',#${this.ownerHistoryId},'${esc(name)}',${desc},${objType},#${placementId},#${prodShapeId},${tag},.FLOOR.`);
|
|
181
|
+
this.elementSolids.set(slabId, [solidId]);
|
|
182
|
+
this.trackElement(storeyId, slabId);
|
|
183
|
+
this.entities.push({ expressId: slabId, type: 'IfcSlab', Name: name });
|
|
184
|
+
if (params.Openings) {
|
|
185
|
+
for (const opening of params.Openings) {
|
|
186
|
+
this.addSlabOpening(slabId, placementId, opening, params.Thickness);
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
return slabId;
|
|
190
|
+
}
|
|
191
|
+
/**
|
|
192
|
+
* Create a column. Position is the base center.
|
|
193
|
+
* Cross-section centered, extruded upward by Height.
|
|
194
|
+
*/
|
|
195
|
+
addIfcColumn(storeyId, params) {
|
|
196
|
+
const placementId = this.addLocalPlacement(this.worldPlacementId, {
|
|
197
|
+
Location: params.Position,
|
|
198
|
+
});
|
|
199
|
+
// Centered profile — column base center = Position
|
|
200
|
+
const profileId = this.addRectangleProfile(params.Width, params.Depth);
|
|
201
|
+
const solidId = this.addExtrudedAreaSolid(profileId, params.Height);
|
|
202
|
+
const shapeId = this.addShapeRepresentation('Body', [solidId]);
|
|
203
|
+
const prodShapeId = this.addProductDefinitionShape([shapeId]);
|
|
204
|
+
const colId = this.id();
|
|
205
|
+
const globalId = newGlobalId();
|
|
206
|
+
const name = params.Name ?? 'Column';
|
|
207
|
+
const desc = params.Description ? `'${esc(params.Description)}'` : '$';
|
|
208
|
+
const objType = params.ObjectType ? `'${esc(params.ObjectType)}'` : '$';
|
|
209
|
+
const tag = params.Tag ? `'${esc(params.Tag)}'` : '$';
|
|
210
|
+
this.line(colId, 'IFCCOLUMN', `'${globalId}',#${this.ownerHistoryId},'${esc(name)}',${desc},${objType},#${placementId},#${prodShapeId},${tag},.COLUMN.`);
|
|
211
|
+
this.elementSolids.set(colId, [solidId]);
|
|
212
|
+
this.trackElement(storeyId, colId);
|
|
213
|
+
this.entities.push({ expressId: colId, type: 'IfcColumn', Name: name });
|
|
214
|
+
return colId;
|
|
215
|
+
}
|
|
216
|
+
/**
|
|
217
|
+
* Create a beam from Start to End.
|
|
218
|
+
* Cross-section (Width × Height) centered on the beam axis.
|
|
219
|
+
*/
|
|
220
|
+
addIfcBeam(storeyId, params) {
|
|
221
|
+
const dx = params.End[0] - params.Start[0];
|
|
222
|
+
const dy = params.End[1] - params.Start[1];
|
|
223
|
+
const dz = params.End[2] - params.Start[2];
|
|
224
|
+
const beamLen = Math.sqrt(dx * dx + dy * dy + dz * dz);
|
|
225
|
+
const dir = vecNorm([dx, dy, dz]);
|
|
226
|
+
// Local Z = beam direction, so extrusion along Z = along beam.
|
|
227
|
+
// Local X, Y define the cross-section plane.
|
|
228
|
+
const placementId = this.addLocalPlacement(this.worldPlacementId, {
|
|
229
|
+
Location: params.Start,
|
|
230
|
+
Axis: dir,
|
|
231
|
+
RefDirection: this.computeRefDirection(dir),
|
|
232
|
+
});
|
|
233
|
+
// Centered cross-section
|
|
234
|
+
const profileId = this.addRectangleProfile(params.Width, params.Height);
|
|
235
|
+
const solidId = this.addExtrudedAreaSolid(profileId, beamLen);
|
|
236
|
+
const shapeId = this.addShapeRepresentation('Body', [solidId]);
|
|
237
|
+
const prodShapeId = this.addProductDefinitionShape([shapeId]);
|
|
238
|
+
const beamId = this.id();
|
|
239
|
+
const globalId = newGlobalId();
|
|
240
|
+
const name = params.Name ?? 'Beam';
|
|
241
|
+
const desc = params.Description ? `'${esc(params.Description)}'` : '$';
|
|
242
|
+
const objType = params.ObjectType ? `'${esc(params.ObjectType)}'` : '$';
|
|
243
|
+
const tag = params.Tag ? `'${esc(params.Tag)}'` : '$';
|
|
244
|
+
this.line(beamId, 'IFCBEAM', `'${globalId}',#${this.ownerHistoryId},'${esc(name)}',${desc},${objType},#${placementId},#${prodShapeId},${tag},.BEAM.`);
|
|
245
|
+
this.elementSolids.set(beamId, [solidId]);
|
|
246
|
+
this.trackElement(storeyId, beamId);
|
|
247
|
+
this.entities.push({ expressId: beamId, type: 'IfcBeam', Name: name });
|
|
248
|
+
return beamId;
|
|
249
|
+
}
|
|
250
|
+
/**
|
|
251
|
+
* Create a straight-run stair.
|
|
252
|
+
* Position is the nose of the first tread. Treads advance along local +X
|
|
253
|
+
* (rotated into world space by Direction). Width extends along local +Y.
|
|
254
|
+
*/
|
|
255
|
+
addIfcStair(storeyId, params) {
|
|
256
|
+
if (params.NumberOfRisers <= 0)
|
|
257
|
+
throw new Error('addStair: NumberOfRisers must be > 0');
|
|
258
|
+
if (params.RiserHeight <= 0)
|
|
259
|
+
throw new Error('addStair: RiserHeight must be > 0');
|
|
260
|
+
if (params.TreadLength <= 0)
|
|
261
|
+
throw new Error('addStair: TreadLength must be > 0');
|
|
262
|
+
if (params.Width <= 0)
|
|
263
|
+
throw new Error('addStair: Width must be > 0');
|
|
264
|
+
const direction = params.Direction ?? 0;
|
|
265
|
+
// Use LocalPlacement rotation so both step positions AND profiles rotate together
|
|
266
|
+
const placementId = this.addLocalPlacement(this.worldPlacementId, {
|
|
267
|
+
Location: params.Position,
|
|
268
|
+
RefDirection: direction !== 0 ? [Math.cos(direction), Math.sin(direction), 0] : undefined,
|
|
269
|
+
});
|
|
270
|
+
const stepSolids = [];
|
|
271
|
+
for (let i = 0; i < params.NumberOfRisers; i++) {
|
|
272
|
+
// Steps in stair-local coordinates: +X = run direction, +Z = up
|
|
273
|
+
const stepOriginId = this.addCartesianPoint([
|
|
274
|
+
i * params.TreadLength,
|
|
275
|
+
0,
|
|
276
|
+
i * params.RiserHeight,
|
|
277
|
+
]);
|
|
278
|
+
const stepAxis2Id = this.addAxis2Placement3D(stepOriginId);
|
|
279
|
+
// Profile: TreadLength along local X, Width along local Y, offset from origin corner
|
|
280
|
+
const profileId = this.addRectangleProfile(params.TreadLength, params.Width, [params.TreadLength / 2, params.Width / 2]);
|
|
281
|
+
const solidId = this.id();
|
|
282
|
+
this.line(solidId, 'IFCEXTRUDEDAREASOLID', `#${profileId},#${stepAxis2Id},#${this.dirZ},${num(params.RiserHeight)}`);
|
|
283
|
+
stepSolids.push(solidId);
|
|
284
|
+
}
|
|
285
|
+
const shapeId = this.addShapeRepresentation('Body', stepSolids);
|
|
286
|
+
const prodShapeId = this.addProductDefinitionShape([shapeId]);
|
|
287
|
+
const stairId = this.id();
|
|
288
|
+
const globalId = newGlobalId();
|
|
289
|
+
const name = params.Name ?? 'Stair';
|
|
290
|
+
const desc = params.Description ? `'${esc(params.Description)}'` : '$';
|
|
291
|
+
const objType = params.ObjectType ? `'${esc(params.ObjectType)}'` : '$';
|
|
292
|
+
const tag = params.Tag ? `'${esc(params.Tag)}'` : '$';
|
|
293
|
+
this.line(stairId, 'IFCSTAIR', `'${globalId}',#${this.ownerHistoryId},'${esc(name)}',${desc},${objType},#${placementId},#${prodShapeId},${tag},.STRAIGHT_RUN_STAIR.`);
|
|
294
|
+
this.elementSolids.set(stairId, [...stepSolids]);
|
|
295
|
+
this.trackElement(storeyId, stairId);
|
|
296
|
+
this.entities.push({ expressId: stairId, type: 'IfcStair', Name: name });
|
|
297
|
+
return stairId;
|
|
298
|
+
}
|
|
299
|
+
/**
|
|
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.
|
|
303
|
+
*/
|
|
304
|
+
addIfcRoof(storeyId, params) {
|
|
305
|
+
const slope = params.Slope ?? 0;
|
|
306
|
+
let axis = [0, 0, 1];
|
|
307
|
+
let refDir = [1, 0, 0];
|
|
308
|
+
if (slope > 0) {
|
|
309
|
+
axis = [Math.sin(slope), 0, Math.cos(slope)];
|
|
310
|
+
refDir = [Math.cos(slope), 0, -Math.sin(slope)];
|
|
311
|
+
}
|
|
312
|
+
const placementId = this.addLocalPlacement(this.worldPlacementId, {
|
|
313
|
+
Location: params.Position,
|
|
314
|
+
Axis: axis,
|
|
315
|
+
RefDirection: refDir,
|
|
316
|
+
});
|
|
317
|
+
// Profile from corner, like slab
|
|
318
|
+
const profileId = this.addRectangleProfile(params.Width, params.Depth, [params.Width / 2, params.Depth / 2]);
|
|
319
|
+
const solidId = this.addExtrudedAreaSolid(profileId, params.Thickness);
|
|
320
|
+
const shapeId = this.addShapeRepresentation('Body', [solidId]);
|
|
321
|
+
const prodShapeId = this.addProductDefinitionShape([shapeId]);
|
|
322
|
+
const roofId = this.id();
|
|
323
|
+
const globalId = newGlobalId();
|
|
324
|
+
const name = params.Name ?? 'Roof';
|
|
325
|
+
const desc = params.Description ? `'${esc(params.Description)}'` : '$';
|
|
326
|
+
const objType = params.ObjectType ? `'${esc(params.ObjectType)}'` : '$';
|
|
327
|
+
const tag = params.Tag ? `'${esc(params.Tag)}'` : '$';
|
|
328
|
+
this.line(roofId, 'IFCROOF', `'${globalId}',#${this.ownerHistoryId},'${esc(name)}',${desc},${objType},#${placementId},#${prodShapeId},${tag},.FLAT_ROOF.`);
|
|
329
|
+
this.elementSolids.set(roofId, [solidId]);
|
|
330
|
+
this.trackElement(storeyId, roofId);
|
|
331
|
+
this.entities.push({ expressId: roofId, type: 'IfcRoof', Name: name });
|
|
332
|
+
return roofId;
|
|
333
|
+
}
|
|
334
|
+
// ============================================================================
|
|
335
|
+
// Public API — Properties & Quantities
|
|
336
|
+
// ============================================================================
|
|
337
|
+
/** Attach a property set to an element */
|
|
338
|
+
addIfcPropertySet(elementId, pset) {
|
|
339
|
+
const propIds = [];
|
|
340
|
+
for (const prop of pset.Properties) {
|
|
341
|
+
const propId = this.id();
|
|
342
|
+
const valueStr = this.serializePropertyValue(prop);
|
|
343
|
+
this.line(propId, 'IFCPROPERTYSINGLEVALUE', `'${esc(prop.Name)}',$,${valueStr},$`);
|
|
344
|
+
propIds.push(propId);
|
|
345
|
+
}
|
|
346
|
+
const psetId = this.id();
|
|
347
|
+
const globalId = newGlobalId();
|
|
348
|
+
const refs = propIds.map(id => `#${id}`).join(',');
|
|
349
|
+
this.line(psetId, 'IFCPROPERTYSET', `'${globalId}',#${this.ownerHistoryId},'${esc(pset.Name)}',$,(${refs})`);
|
|
350
|
+
const relId = this.id();
|
|
351
|
+
const relGlobalId = newGlobalId();
|
|
352
|
+
this.line(relId, 'IFCRELDEFINESBYPROPERTIES', `'${relGlobalId}',#${this.ownerHistoryId},$,$,(#${elementId}),#${psetId}`);
|
|
353
|
+
return psetId;
|
|
354
|
+
}
|
|
355
|
+
/** Attach element quantities to an element */
|
|
356
|
+
addIfcElementQuantity(elementId, qset) {
|
|
357
|
+
const qtyIds = [];
|
|
358
|
+
for (const qty of qset.Quantities) {
|
|
359
|
+
const qtyId = this.id();
|
|
360
|
+
const valueField = this.quantityValueField(qty);
|
|
361
|
+
this.line(qtyId, qty.Kind.toUpperCase(), `'${esc(qty.Name)}',$,${valueField}`);
|
|
362
|
+
qtyIds.push(qtyId);
|
|
363
|
+
}
|
|
364
|
+
const qsetId = this.id();
|
|
365
|
+
const globalId = newGlobalId();
|
|
366
|
+
const refs = qtyIds.map(id => `#${id}`).join(',');
|
|
367
|
+
this.line(qsetId, 'IFCELEMENTQUANTITY', `'${globalId}',#${this.ownerHistoryId},'${esc(qset.Name)}',$,$,(${refs})`);
|
|
368
|
+
const relId = this.id();
|
|
369
|
+
const relGlobalId = newGlobalId();
|
|
370
|
+
this.line(relId, 'IFCRELDEFINESBYPROPERTIES', `'${relGlobalId}',#${this.ownerHistoryId},$,$,(#${elementId}),#${qsetId}`);
|
|
371
|
+
return qsetId;
|
|
372
|
+
}
|
|
373
|
+
// ============================================================================
|
|
374
|
+
// Public API — Styling
|
|
375
|
+
// ============================================================================
|
|
376
|
+
/**
|
|
377
|
+
* Assign a named colour to an element. Call before toIfc().
|
|
378
|
+
* Elements without a custom colour get the default grey.
|
|
379
|
+
*
|
|
380
|
+
* @param elementId The expressId returned by addWall/addSlab/…
|
|
381
|
+
* @param name Material name shown in IFC viewers (e.g. 'Concrete')
|
|
382
|
+
* @param rgb [r, g, b] each 0‒1
|
|
383
|
+
*/
|
|
384
|
+
setColor(elementId, name, rgb) {
|
|
385
|
+
this.elementColors.set(elementId, { name, rgb });
|
|
386
|
+
}
|
|
387
|
+
// ============================================================================
|
|
388
|
+
// Public API — Materials
|
|
389
|
+
// ============================================================================
|
|
390
|
+
/**
|
|
391
|
+
* Assign an IFC material to an element. Creates proper IfcMaterial entities
|
|
392
|
+
* and links them via IfcRelAssociatesMaterial during finalization.
|
|
393
|
+
*
|
|
394
|
+
* Simple material: `{ Name: 'Concrete', Category: 'Structural' }`
|
|
395
|
+
* Layered material: `{ Name: 'Wall Assembly', Layers: [{ Name: 'Concrete', Thickness: 0.2 }, …] }`
|
|
396
|
+
*/
|
|
397
|
+
addIfcMaterial(elementId, def) {
|
|
398
|
+
let materialRefId;
|
|
399
|
+
if (def.Layers && def.Layers.length > 0) {
|
|
400
|
+
// IfcMaterialLayerSet path
|
|
401
|
+
const layerIds = [];
|
|
402
|
+
for (const layer of def.Layers) {
|
|
403
|
+
const matId = this.getOrCreateMaterial(layer.Name, layer.Category);
|
|
404
|
+
const layerId = this.id();
|
|
405
|
+
const ventilated = layer.IsVentilated ? '.T.' : '.F.';
|
|
406
|
+
const layerName = `'${esc(layer.Name)}'`;
|
|
407
|
+
const layerCategory = layer.Category ? `'${esc(layer.Category)}'` : '$';
|
|
408
|
+
// IFC4: Material, LayerThickness, IsVentilated, Name, Description, Category, Priority
|
|
409
|
+
this.line(layerId, 'IFCMATERIALLAYER', `#${matId},${num(layer.Thickness)},${ventilated},${layerName},$,${layerCategory},$`);
|
|
410
|
+
layerIds.push(layerId);
|
|
411
|
+
}
|
|
412
|
+
const layerRefs = layerIds.map(id => `#${id}`).join(',');
|
|
413
|
+
materialRefId = this.id();
|
|
414
|
+
// IFC4: MaterialLayers, LayerSetName, Description
|
|
415
|
+
this.line(materialRefId, 'IFCMATERIALLAYERSET', `(${layerRefs}),'${esc(def.Name)}',$`);
|
|
416
|
+
}
|
|
417
|
+
else {
|
|
418
|
+
// Simple IfcMaterial
|
|
419
|
+
materialRefId = this.getOrCreateMaterial(def.Name, def.Category);
|
|
420
|
+
}
|
|
421
|
+
this.elementMaterials.set(elementId, materialRefId);
|
|
422
|
+
}
|
|
423
|
+
// ============================================================================
|
|
424
|
+
// Public API — Export
|
|
425
|
+
// ============================================================================
|
|
426
|
+
/** Generate the complete IFC STEP file. May only be called once. */
|
|
427
|
+
toIfc() {
|
|
428
|
+
if (this.finalized)
|
|
429
|
+
throw new Error('toIfc() has already been called — creator is not reusable');
|
|
430
|
+
this.finalized = true;
|
|
431
|
+
this.finalizeStyles();
|
|
432
|
+
this.finalizeMaterials();
|
|
433
|
+
this.finalizeRelationships();
|
|
434
|
+
const header = this.buildHeader();
|
|
435
|
+
const data = this.lines.join('\n');
|
|
436
|
+
const content = `${header}DATA;\n${data}\nENDSEC;\nEND-ISO-10303-21;\n`;
|
|
437
|
+
return {
|
|
438
|
+
content,
|
|
439
|
+
entities: [...this.entities],
|
|
440
|
+
stats: {
|
|
441
|
+
entityCount: this.lines.length,
|
|
442
|
+
fileSize: new TextEncoder().encode(content).length,
|
|
443
|
+
},
|
|
444
|
+
};
|
|
445
|
+
}
|
|
446
|
+
buildHeader() {
|
|
447
|
+
const now = new Date().toISOString().replace(/[-:]/g, '').split('.')[0];
|
|
448
|
+
const desc = 'Created by ifc-lite';
|
|
449
|
+
const author = this.projectParams.Author ?? '';
|
|
450
|
+
const org = this.projectParams.Organization ?? '';
|
|
451
|
+
const app = 'ifc-lite';
|
|
452
|
+
const filename = 'created.ifc';
|
|
453
|
+
return `ISO-10303-21;
|
|
454
|
+
HEADER;
|
|
455
|
+
FILE_DESCRIPTION(('${esc(desc)}'),'2;1');
|
|
456
|
+
FILE_NAME('${filename}','${now}',('${esc(author)}'),('${esc(org)}'),'${app}','${app}','');
|
|
457
|
+
FILE_SCHEMA(('${this.schema}'));
|
|
458
|
+
ENDSEC;
|
|
459
|
+
`;
|
|
460
|
+
}
|
|
461
|
+
// ============================================================================
|
|
462
|
+
// Internal — Preamble (project, site, building, contexts, units, style)
|
|
463
|
+
// ============================================================================
|
|
464
|
+
buildPreamble(params) {
|
|
465
|
+
const personId = this.id();
|
|
466
|
+
this.line(personId, 'IFCPERSON', "$,$,'',$,$,$,$,$");
|
|
467
|
+
const orgId = this.id();
|
|
468
|
+
this.line(orgId, 'IFCORGANIZATION', `$,'${esc(params.Organization ?? 'ifc-lite')}',$,$,$`);
|
|
469
|
+
const personOrgId = this.id();
|
|
470
|
+
this.line(personOrgId, 'IFCPERSONANDORGANIZATION', `#${personId},#${orgId},$`);
|
|
471
|
+
const appId = this.id();
|
|
472
|
+
this.line(appId, 'IFCAPPLICATION', `#${orgId},'1.0','ifc-lite','ifc-lite'`);
|
|
473
|
+
this.ownerHistoryId = this.id();
|
|
474
|
+
const timestamp = Math.floor(Date.now() / 1000);
|
|
475
|
+
this.line(this.ownerHistoryId, 'IFCOWNERHISTORY', `#${personOrgId},#${appId},$,.NOCHANGE.,$,$,$,${timestamp}`);
|
|
476
|
+
// Shared geometry primitives
|
|
477
|
+
this.originId = this.addCartesianPoint([0, 0, 0]);
|
|
478
|
+
this.dirZ = this.addDirection([0, 0, 1]);
|
|
479
|
+
this.dirX = this.addDirection([1, 0, 0]);
|
|
480
|
+
// World coordinate placement
|
|
481
|
+
const worldAxisId = this.id();
|
|
482
|
+
this.line(worldAxisId, 'IFCAXIS2PLACEMENT3D', `#${this.originId},#${this.dirZ},#${this.dirX}`);
|
|
483
|
+
this.worldPlacementId = this.id();
|
|
484
|
+
this.line(this.worldPlacementId, 'IFCLOCALPLACEMENT', `$,#${worldAxisId}`);
|
|
485
|
+
// Geometric representation context
|
|
486
|
+
this.contextId = this.id();
|
|
487
|
+
this.line(this.contextId, 'IFCGEOMETRICREPRESENTATIONCONTEXT', `$,'Model',3,1.0E-5,#${worldAxisId},$`);
|
|
488
|
+
this.subContextBody = this.id();
|
|
489
|
+
this.line(this.subContextBody, 'IFCGEOMETRICREPRESENTATIONSUBCONTEXT', `$,'Body',*,*,*,*,#${this.contextId},$,.MODEL_VIEW.,$`);
|
|
490
|
+
this.subContextAxis = this.id();
|
|
491
|
+
this.line(this.subContextAxis, 'IFCGEOMETRICREPRESENTATIONSUBCONTEXT', `$,'Axis',*,*,*,*,#${this.contextId},$,.GRAPH_VIEW.,$`);
|
|
492
|
+
// Units
|
|
493
|
+
this.unitAssignmentId = this.buildUnits(params.LengthUnit ?? 'METRE');
|
|
494
|
+
// Default surface style — light grey with some specularity
|
|
495
|
+
this.defaultStyleId = this.buildDefaultStyle();
|
|
496
|
+
// IfcProject
|
|
497
|
+
this.projectId = this.id();
|
|
498
|
+
const projectGlobalId = newGlobalId();
|
|
499
|
+
const projectName = params.Name ?? 'Project';
|
|
500
|
+
const projectDesc = params.Description ? `'${esc(params.Description)}'` : '$';
|
|
501
|
+
this.line(this.projectId, 'IFCPROJECT', `'${projectGlobalId}',#${this.ownerHistoryId},'${esc(projectName)}',${projectDesc},$,$,$,(#${this.contextId}),#${this.unitAssignmentId}`);
|
|
502
|
+
this.entities.push({ expressId: this.projectId, type: 'IfcProject', Name: projectName });
|
|
503
|
+
// IfcSite
|
|
504
|
+
this.siteId = this.id();
|
|
505
|
+
const siteGlobalId = newGlobalId();
|
|
506
|
+
this.line(this.siteId, 'IFCSITE', `'${siteGlobalId}',#${this.ownerHistoryId},'Site',$,$,#${this.worldPlacementId},$,$,.ELEMENT.,$,$,$,$,$`);
|
|
507
|
+
this.entities.push({ expressId: this.siteId, type: 'IfcSite', Name: 'Site' });
|
|
508
|
+
// IfcBuilding
|
|
509
|
+
this.buildingId = this.id();
|
|
510
|
+
const buildingGlobalId = newGlobalId();
|
|
511
|
+
this.line(this.buildingId, 'IFCBUILDING', `'${buildingGlobalId}',#${this.ownerHistoryId},'Building',$,$,#${this.worldPlacementId},$,$,.ELEMENT.,$,$,$`);
|
|
512
|
+
this.entities.push({ expressId: this.buildingId, type: 'IfcBuilding', Name: 'Building' });
|
|
513
|
+
}
|
|
514
|
+
buildUnits(lengthUnit) {
|
|
515
|
+
const dimExpId = this.id();
|
|
516
|
+
this.line(dimExpId, 'IFCDIMENSIONALEXPONENTS', '0,0,0,0,0,0,0');
|
|
517
|
+
const siLengthId = this.id();
|
|
518
|
+
this.line(siLengthId, 'IFCSIUNIT', `*,.LENGTHUNIT.,$,.METRE.`);
|
|
519
|
+
let lengthUnitId = siLengthId;
|
|
520
|
+
if (lengthUnit === 'MILLIMETRE') {
|
|
521
|
+
const prefixId = this.id();
|
|
522
|
+
this.line(prefixId, 'IFCSIUNIT', `*,.LENGTHUNIT.,.MILLI.,.METRE.`);
|
|
523
|
+
lengthUnitId = prefixId;
|
|
524
|
+
}
|
|
525
|
+
const siAreaId = this.id();
|
|
526
|
+
this.line(siAreaId, 'IFCSIUNIT', `*,.AREAUNIT.,$,.SQUARE_METRE.`);
|
|
527
|
+
const siVolumeId = this.id();
|
|
528
|
+
this.line(siVolumeId, 'IFCSIUNIT', `*,.VOLUMEUNIT.,$,.CUBIC_METRE.`);
|
|
529
|
+
const siAngleId = this.id();
|
|
530
|
+
this.line(siAngleId, 'IFCSIUNIT', `*,.PLANEANGLEUNIT.,$,.RADIAN.`);
|
|
531
|
+
const assignmentId = this.id();
|
|
532
|
+
this.line(assignmentId, 'IFCUNITASSIGNMENT', `(#${lengthUnitId},#${siAreaId},#${siVolumeId},#${siAngleId})`);
|
|
533
|
+
return assignmentId;
|
|
534
|
+
}
|
|
535
|
+
/** Create a default IfcSurfaceStyle with a neutral colour (RGB 0.75, 0.73, 0.68) */
|
|
536
|
+
buildDefaultStyle() {
|
|
537
|
+
// IfcColourRgb — warm concrete grey
|
|
538
|
+
const colourId = this.id();
|
|
539
|
+
this.line(colourId, 'IFCCOLOURRGB', `$,0.75,0.73,0.68`);
|
|
540
|
+
// IfcSurfaceStyleRendering — surface + specular
|
|
541
|
+
const renderingId = this.id();
|
|
542
|
+
this.line(renderingId, 'IFCSURFACESTYLERENDERING', `#${colourId},0.,$,$,$,$,IFCNORMALISEDRATIOMEASURE(0.5),IFCSPECULAREXPONENT(64.),.NOTDEFINED.`);
|
|
543
|
+
// IfcSurfaceStyle
|
|
544
|
+
const styleId = this.id();
|
|
545
|
+
this.line(styleId, 'IFCSURFACESTYLE', `'Default',.BOTH.,(#${renderingId})`);
|
|
546
|
+
return styleId;
|
|
547
|
+
}
|
|
548
|
+
/** Create all IfcStyledItem entities — custom colour or default per element */
|
|
549
|
+
finalizeStyles() {
|
|
550
|
+
// Cache: colour key → styleId so identical colours share one style entity
|
|
551
|
+
const styleCache = new Map();
|
|
552
|
+
for (const [elementId, solidIds] of this.elementSolids) {
|
|
553
|
+
const color = this.elementColors.get(elementId);
|
|
554
|
+
let styleId;
|
|
555
|
+
if (color) {
|
|
556
|
+
const key = `${color.name}|${color.rgb.join(',')}`;
|
|
557
|
+
const cached = styleCache.get(key);
|
|
558
|
+
if (cached !== undefined) {
|
|
559
|
+
styleId = cached;
|
|
560
|
+
}
|
|
561
|
+
else {
|
|
562
|
+
styleId = this.buildColorStyle(color.name, color.rgb);
|
|
563
|
+
styleCache.set(key, styleId);
|
|
564
|
+
}
|
|
565
|
+
}
|
|
566
|
+
else {
|
|
567
|
+
styleId = this.defaultStyleId;
|
|
568
|
+
}
|
|
569
|
+
for (const solidId of solidIds) {
|
|
570
|
+
const styledItemId = this.id();
|
|
571
|
+
this.line(styledItemId, 'IFCSTYLEDITEM', `#${solidId},(#${styleId}),$`);
|
|
572
|
+
}
|
|
573
|
+
}
|
|
574
|
+
}
|
|
575
|
+
/** Create a named IfcSurfaceStyle with the given RGB colour */
|
|
576
|
+
buildColorStyle(name, rgb) {
|
|
577
|
+
const colourId = this.id();
|
|
578
|
+
this.line(colourId, 'IFCCOLOURRGB', `$,${num(rgb[0])},${num(rgb[1])},${num(rgb[2])}`);
|
|
579
|
+
const renderingId = this.id();
|
|
580
|
+
this.line(renderingId, 'IFCSURFACESTYLERENDERING', `#${colourId},0.,$,$,$,$,IFCNORMALISEDRATIOMEASURE(0.5),IFCSPECULAREXPONENT(64.),.NOTDEFINED.`);
|
|
581
|
+
const styleId = this.id();
|
|
582
|
+
this.line(styleId, 'IFCSURFACESTYLE', `'${esc(name)}',.BOTH.,(#${renderingId})`);
|
|
583
|
+
return styleId;
|
|
584
|
+
}
|
|
585
|
+
// ============================================================================
|
|
586
|
+
// Internal — Material helpers
|
|
587
|
+
// ============================================================================
|
|
588
|
+
/** Get or create a shared IfcMaterial entity (IFC4: Name, Description, Category) */
|
|
589
|
+
getOrCreateMaterial(name, category) {
|
|
590
|
+
const cached = this.materialCache.get(name);
|
|
591
|
+
if (cached !== undefined)
|
|
592
|
+
return cached;
|
|
593
|
+
const matId = this.id();
|
|
594
|
+
const cat = category ? `'${esc(category)}'` : '$';
|
|
595
|
+
this.line(matId, 'IFCMATERIAL', `'${esc(name)}',$,${cat}`);
|
|
596
|
+
this.materialCache.set(name, matId);
|
|
597
|
+
return matId;
|
|
598
|
+
}
|
|
599
|
+
/** Create IfcRelAssociatesMaterial entities — one per unique material ref (batched) */
|
|
600
|
+
finalizeMaterials() {
|
|
601
|
+
// Group elements by materialRefId so elements sharing a material get one rel
|
|
602
|
+
const groups = new Map();
|
|
603
|
+
for (const [elementId, materialRefId] of this.elementMaterials) {
|
|
604
|
+
const group = groups.get(materialRefId);
|
|
605
|
+
if (group)
|
|
606
|
+
group.push(elementId);
|
|
607
|
+
else
|
|
608
|
+
groups.set(materialRefId, [elementId]);
|
|
609
|
+
}
|
|
610
|
+
for (const [materialRefId, elementIds] of groups) {
|
|
611
|
+
const relId = this.id();
|
|
612
|
+
const globalId = newGlobalId();
|
|
613
|
+
const refs = elementIds.map(id => `#${id}`).join(',');
|
|
614
|
+
this.line(relId, 'IFCRELASSOCIATESMATERIAL', `'${globalId}',#${this.ownerHistoryId},$,$,(${refs}),#${materialRefId}`);
|
|
615
|
+
}
|
|
616
|
+
}
|
|
617
|
+
// ============================================================================
|
|
618
|
+
// Internal — Geometry helpers
|
|
619
|
+
// ============================================================================
|
|
620
|
+
addCartesianPoint(p) {
|
|
621
|
+
const id = this.id();
|
|
622
|
+
this.line(id, 'IFCCARTESIANPOINT', `(${num(p[0])},${num(p[1])},${num(p[2])})`);
|
|
623
|
+
return id;
|
|
624
|
+
}
|
|
625
|
+
addCartesianPoint2D(p) {
|
|
626
|
+
const id = this.id();
|
|
627
|
+
this.line(id, 'IFCCARTESIANPOINT', `(${num(p[0])},${num(p[1])})`);
|
|
628
|
+
return id;
|
|
629
|
+
}
|
|
630
|
+
addDirection(d) {
|
|
631
|
+
const id = this.id();
|
|
632
|
+
this.line(id, 'IFCDIRECTION', `(${num(d[0])},${num(d[1])},${num(d[2])})`);
|
|
633
|
+
return id;
|
|
634
|
+
}
|
|
635
|
+
addAxis2Placement3D(originId, axisId, refDirId) {
|
|
636
|
+
const id = this.id();
|
|
637
|
+
const axis = axisId ? `#${axisId}` : '$';
|
|
638
|
+
const refDir = refDirId ? `#${refDirId}` : '$';
|
|
639
|
+
this.line(id, 'IFCAXIS2PLACEMENT3D', `#${originId},${axis},${refDir}`);
|
|
640
|
+
return id;
|
|
641
|
+
}
|
|
642
|
+
addLocalPlacement(relativeTo, placement) {
|
|
643
|
+
const originId = this.addCartesianPoint(placement.Location);
|
|
644
|
+
let axisId;
|
|
645
|
+
let refDirId;
|
|
646
|
+
if (placement.Axis) {
|
|
647
|
+
axisId = this.addDirection(placement.Axis);
|
|
648
|
+
}
|
|
649
|
+
if (placement.RefDirection) {
|
|
650
|
+
refDirId = this.addDirection(placement.RefDirection);
|
|
651
|
+
}
|
|
652
|
+
const axis2Id = this.addAxis2Placement3D(originId, axisId, refDirId);
|
|
653
|
+
const id = this.id();
|
|
654
|
+
this.line(id, 'IFCLOCALPLACEMENT', `#${relativeTo},#${axis2Id}`);
|
|
655
|
+
return id;
|
|
656
|
+
}
|
|
657
|
+
/**
|
|
658
|
+
* Create a rectangle profile.
|
|
659
|
+
* @param xDim Width of rectangle
|
|
660
|
+
* @param yDim Height of rectangle
|
|
661
|
+
* @param center Optional 2D offset for the profile centre. Default [0,0] = centred at origin.
|
|
662
|
+
*/
|
|
663
|
+
addRectangleProfile(xDim, yDim, center) {
|
|
664
|
+
const cx = center?.[0] ?? 0;
|
|
665
|
+
const cy = center?.[1] ?? 0;
|
|
666
|
+
const profileOriginId = this.addCartesianPoint2D([cx, cy]);
|
|
667
|
+
const profileAxis2dId = this.id();
|
|
668
|
+
this.line(profileAxis2dId, 'IFCAXIS2PLACEMENT2D', `#${profileOriginId},$`);
|
|
669
|
+
const id = this.id();
|
|
670
|
+
this.line(id, 'IFCRECTANGLEPROFILEDEF', `.AREA.,$,#${profileAxis2dId},${num(xDim)},${num(yDim)}`);
|
|
671
|
+
return id;
|
|
672
|
+
}
|
|
673
|
+
addArbitraryProfile(points) {
|
|
674
|
+
const pointIds = points.map(p => this.addCartesianPoint2D(p));
|
|
675
|
+
if (points.length > 0) {
|
|
676
|
+
pointIds.push(pointIds[0]); // close the polyline
|
|
677
|
+
}
|
|
678
|
+
const refs = pointIds.map(id => `#${id}`).join(',');
|
|
679
|
+
const polylineId = this.id();
|
|
680
|
+
this.line(polylineId, 'IFCPOLYLINE', `(${refs})`);
|
|
681
|
+
const id = this.id();
|
|
682
|
+
this.line(id, 'IFCARBITRARYCLOSEDPROFILEDEF', `.AREA.,$,#${polylineId}`);
|
|
683
|
+
return id;
|
|
684
|
+
}
|
|
685
|
+
addExtrudedAreaSolid(profileId, depth, extrusionDir) {
|
|
686
|
+
const originId = this.addCartesianPoint([0, 0, 0]);
|
|
687
|
+
const axis2Id = this.addAxis2Placement3D(originId);
|
|
688
|
+
const dirRef = extrusionDir ?? this.dirZ;
|
|
689
|
+
const id = this.id();
|
|
690
|
+
this.line(id, 'IFCEXTRUDEDAREASOLID', `#${profileId},#${axis2Id},#${dirRef},${num(depth)}`);
|
|
691
|
+
return id;
|
|
692
|
+
}
|
|
693
|
+
addShapeRepresentation(repType, itemIds) {
|
|
694
|
+
const contextRef = repType === 'Axis' ? this.subContextAxis : this.subContextBody;
|
|
695
|
+
const refs = itemIds.map(id => `#${id}`).join(',');
|
|
696
|
+
const repId = this.id();
|
|
697
|
+
const repIdentifier = repType === 'Axis' ? 'Axis' : 'Body';
|
|
698
|
+
const repTypeName = itemIds.length > 1 ? 'SolidModel' : 'SweptSolid';
|
|
699
|
+
this.line(repId, 'IFCSHAPEREPRESENTATION', `#${contextRef},'${repIdentifier}','${repTypeName}',(${refs})`);
|
|
700
|
+
return repId;
|
|
701
|
+
}
|
|
702
|
+
addProductDefinitionShape(repIds) {
|
|
703
|
+
const refs = repIds.map(id => `#${id}`).join(',');
|
|
704
|
+
const id = this.id();
|
|
705
|
+
this.line(id, 'IFCPRODUCTDEFINITIONSHAPE', `$,$,(${refs})`);
|
|
706
|
+
return id;
|
|
707
|
+
}
|
|
708
|
+
// ============================================================================
|
|
709
|
+
// Internal — Openings
|
|
710
|
+
// ============================================================================
|
|
711
|
+
/**
|
|
712
|
+
* Add an opening in a wall.
|
|
713
|
+
* Opening Position: [distance_along_wall, 0, sill_height]
|
|
714
|
+
* The opening extrudes through the wall perpendicular to its face (local Y).
|
|
715
|
+
*/
|
|
716
|
+
addWallOpening(hostId, hostPlacementId, opening, wallThickness) {
|
|
717
|
+
// In wall local CS: X = along wall, Y = thickness, Z = up.
|
|
718
|
+
// Opening needs to cut through the wall in the Y direction.
|
|
719
|
+
// So we orient the opening's local Z = wall's local Y = [0,1,0],
|
|
720
|
+
// and opening's local X = wall's local X = [1,0,0].
|
|
721
|
+
// Opening local Y then = cross(Z,X) = cross([0,1,0],[1,0,0]) = [0,0,-1].
|
|
722
|
+
// To get Y pointing up, flip: Axis = [0,-1,0].
|
|
723
|
+
// Then: local X = [1,0,0], local Y = cross([0,-1,0],[1,0,0]) = [0,0,1], local Z = [0,-1,0].
|
|
724
|
+
// Profile XY: X = along wall (Width), Y = up (Height). Extrusion Z = through wall.
|
|
725
|
+
// Offset Y so extrusion starts just outside the +Y face and cuts clean through
|
|
726
|
+
const openingOriginId = this.addCartesianPoint([
|
|
727
|
+
opening.Position[0],
|
|
728
|
+
wallThickness / 2 + 0.05,
|
|
729
|
+
opening.Position[2],
|
|
730
|
+
]);
|
|
731
|
+
const openingAxisId = this.addDirection([0, -1, 0]);
|
|
732
|
+
const openingRefDirId = this.addDirection([1, 0, 0]);
|
|
733
|
+
const openingAxis2Id = this.addAxis2Placement3D(openingOriginId, openingAxisId, openingRefDirId);
|
|
734
|
+
const openingPlacementId = this.id();
|
|
735
|
+
this.line(openingPlacementId, 'IFCLOCALPLACEMENT', `#${hostPlacementId},#${openingAxis2Id}`);
|
|
736
|
+
// Profile: Width along wall (X), Height upward (Y), centered on position.
|
|
737
|
+
// Extrude through wall thickness + margin.
|
|
738
|
+
const profileId = this.addRectangleProfile(opening.Width, opening.Height, [0, opening.Height / 2]);
|
|
739
|
+
const extrusionDepth = wallThickness + 0.1; // enough to cut clean through
|
|
740
|
+
const solidId = this.addExtrudedAreaSolid(profileId, extrusionDepth);
|
|
741
|
+
const shapeId = this.addShapeRepresentation('Body', [solidId]);
|
|
742
|
+
const prodShapeId = this.addProductDefinitionShape([shapeId]);
|
|
743
|
+
const openingId = this.id();
|
|
744
|
+
const globalId = newGlobalId();
|
|
745
|
+
const name = opening.Name ?? 'Opening';
|
|
746
|
+
this.line(openingId, 'IFCOPENINGELEMENT', `'${globalId}',#${this.ownerHistoryId},'${esc(name)}',$,$,#${openingPlacementId},#${prodShapeId},$,.OPENING.`);
|
|
747
|
+
const relId = this.id();
|
|
748
|
+
const relGlobalId = newGlobalId();
|
|
749
|
+
this.line(relId, 'IFCRELVOIDSELEMENT', `'${relGlobalId}',#${this.ownerHistoryId},$,$,#${hostId},#${openingId}`);
|
|
750
|
+
this.entities.push({ expressId: openingId, type: 'IfcOpeningElement', Name: name });
|
|
751
|
+
return openingId;
|
|
752
|
+
}
|
|
753
|
+
/**
|
|
754
|
+
* Add an opening in a slab.
|
|
755
|
+
* Opening Position: [x_offset, y_offset, 0] relative to slab placement.
|
|
756
|
+
* The opening extrudes through the slab along Z.
|
|
757
|
+
*/
|
|
758
|
+
addSlabOpening(hostId, hostPlacementId, opening, slabThickness) {
|
|
759
|
+
const placementId = this.addLocalPlacement(hostPlacementId, {
|
|
760
|
+
Location: opening.Position,
|
|
761
|
+
});
|
|
762
|
+
const profileId = this.addRectangleProfile(opening.Width, opening.Height);
|
|
763
|
+
const extrusionDepth = slabThickness + 0.1; // enough to cut clean through
|
|
764
|
+
const solidId = this.addExtrudedAreaSolid(profileId, extrusionDepth);
|
|
765
|
+
const shapeId = this.addShapeRepresentation('Body', [solidId]);
|
|
766
|
+
const prodShapeId = this.addProductDefinitionShape([shapeId]);
|
|
767
|
+
const openingId = this.id();
|
|
768
|
+
const globalId = newGlobalId();
|
|
769
|
+
const name = opening.Name ?? 'Opening';
|
|
770
|
+
this.line(openingId, 'IFCOPENINGELEMENT', `'${globalId}',#${this.ownerHistoryId},'${esc(name)}',$,$,#${placementId},#${prodShapeId},$,.OPENING.`);
|
|
771
|
+
const relId = this.id();
|
|
772
|
+
const relGlobalId = newGlobalId();
|
|
773
|
+
this.line(relId, 'IFCRELVOIDSELEMENT', `'${relGlobalId}',#${this.ownerHistoryId},$,$,#${hostId},#${openingId}`);
|
|
774
|
+
this.entities.push({ expressId: openingId, type: 'IfcOpeningElement', Name: name });
|
|
775
|
+
return openingId;
|
|
776
|
+
}
|
|
777
|
+
// ============================================================================
|
|
778
|
+
// Internal — Property/quantity serialization
|
|
779
|
+
// ============================================================================
|
|
780
|
+
serializePropertyValue(prop) {
|
|
781
|
+
const val = prop.NominalValue;
|
|
782
|
+
if (typeof val === 'string') {
|
|
783
|
+
const typeName = prop.Type ?? 'IfcLabel';
|
|
784
|
+
return `${typeName.toUpperCase()}('${esc(val)}')`;
|
|
785
|
+
}
|
|
786
|
+
if (typeof val === 'number') {
|
|
787
|
+
const typeName = prop.Type ?? (Number.isInteger(val) ? 'IfcInteger' : 'IfcReal');
|
|
788
|
+
if (typeName === 'IfcInteger') {
|
|
789
|
+
return `IFCINTEGER(${Math.round(val)})`;
|
|
790
|
+
}
|
|
791
|
+
return `IFCREAL(${num(val)})`;
|
|
792
|
+
}
|
|
793
|
+
if (typeof val === 'boolean') {
|
|
794
|
+
return `IFCBOOLEAN(${val ? '.T.' : '.F.'})`;
|
|
795
|
+
}
|
|
796
|
+
return '$';
|
|
797
|
+
}
|
|
798
|
+
quantityValueField(qty) {
|
|
799
|
+
switch (qty.Kind) {
|
|
800
|
+
case 'IfcQuantityLength':
|
|
801
|
+
case 'IfcQuantityArea':
|
|
802
|
+
case 'IfcQuantityVolume':
|
|
803
|
+
case 'IfcQuantityWeight':
|
|
804
|
+
return `$,${num(qty.Value)}`;
|
|
805
|
+
case 'IfcQuantityCount':
|
|
806
|
+
return `$,${Math.round(qty.Value)}`;
|
|
807
|
+
default:
|
|
808
|
+
return `$,${num(qty.Value)}`;
|
|
809
|
+
}
|
|
810
|
+
}
|
|
811
|
+
// ============================================================================
|
|
812
|
+
// Internal — Relationship finalization
|
|
813
|
+
// ============================================================================
|
|
814
|
+
finalizeRelationships() {
|
|
815
|
+
this.addIfcRelAggregates(this.projectId, [this.siteId]);
|
|
816
|
+
this.addIfcRelAggregates(this.siteId, [this.buildingId]);
|
|
817
|
+
if (this.storeyIds.length > 0) {
|
|
818
|
+
this.addIfcRelAggregates(this.buildingId, this.storeyIds);
|
|
819
|
+
}
|
|
820
|
+
for (const [storeyId, elementIds] of this.storeyElements) {
|
|
821
|
+
if (elementIds.length > 0) {
|
|
822
|
+
this.addIfcRelContainedInSpatialStructure(storeyId, elementIds);
|
|
823
|
+
}
|
|
824
|
+
}
|
|
825
|
+
}
|
|
826
|
+
addIfcRelAggregates(relatingId, relatedIds) {
|
|
827
|
+
const relId = this.id();
|
|
828
|
+
const globalId = newGlobalId();
|
|
829
|
+
const refs = relatedIds.map(id => `#${id}`).join(',');
|
|
830
|
+
this.line(relId, 'IFCRELAGGREGATES', `'${globalId}',#${this.ownerHistoryId},$,$,#${relatingId},(${refs})`);
|
|
831
|
+
}
|
|
832
|
+
addIfcRelContainedInSpatialStructure(storeyId, elementIds) {
|
|
833
|
+
const relId = this.id();
|
|
834
|
+
const globalId = newGlobalId();
|
|
835
|
+
const refs = elementIds.map(id => `#${id}`).join(',');
|
|
836
|
+
this.line(relId, 'IFCRELCONTAINEDINSPATIALSTRUCTURE', `'${globalId}',#${this.ownerHistoryId},$,$,(${refs}),#${storeyId}`);
|
|
837
|
+
}
|
|
838
|
+
// ============================================================================
|
|
839
|
+
// Internal — Utilities
|
|
840
|
+
// ============================================================================
|
|
841
|
+
id() {
|
|
842
|
+
return this.nextId++;
|
|
843
|
+
}
|
|
844
|
+
line(id, type, args) {
|
|
845
|
+
this.lines.push(stepLine(id, type, args));
|
|
846
|
+
}
|
|
847
|
+
trackElement(storeyId, elementId) {
|
|
848
|
+
const elements = this.storeyElements.get(storeyId);
|
|
849
|
+
if (!elements) {
|
|
850
|
+
throw new Error(`Unknown storeyId #${storeyId} — call addIfcBuildingStorey() first`);
|
|
851
|
+
}
|
|
852
|
+
elements.push(elementId);
|
|
853
|
+
}
|
|
854
|
+
/** Compute a stable RefDirection perpendicular to a given Axis */
|
|
855
|
+
computeRefDirection(axis) {
|
|
856
|
+
const up = Math.abs(axis[2]) < 0.9 ? [0, 0, 1] : [1, 0, 0];
|
|
857
|
+
const cross = vecCross(up, axis);
|
|
858
|
+
return vecNorm(cross);
|
|
859
|
+
}
|
|
860
|
+
}
|
|
861
|
+
//# sourceMappingURL=ifc-creator.js.map
|