@ifc-lite/create 1.15.0 → 1.16.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/dist/ifc-creator.js +1 -1
- package/dist/ifc-creator.js.map +1 -1
- package/dist/in-store/_emit-helpers.d.ts +8 -2
- package/dist/in-store/_emit-helpers.d.ts.map +1 -1
- package/dist/in-store/_emit-helpers.js +10 -2
- package/dist/in-store/_emit-helpers.js.map +1 -1
- package/dist/in-store/anchor.d.ts +23 -2
- package/dist/in-store/anchor.d.ts.map +1 -1
- package/dist/in-store/anchor.js +11 -1
- package/dist/in-store/anchor.js.map +1 -1
- package/dist/in-store/auto-space-detect.d.ts.map +1 -1
- package/dist/in-store/auto-space-detect.js +150 -57
- package/dist/in-store/auto-space-detect.js.map +1 -1
- package/dist/in-store/beam.d.ts.map +1 -1
- package/dist/in-store/beam.js +3 -2
- package/dist/in-store/beam.js.map +1 -1
- package/dist/in-store/column.d.ts.map +1 -1
- package/dist/in-store/column.js +4 -3
- package/dist/in-store/column.js.map +1 -1
- package/dist/in-store/extract-walls.d.ts +28 -1
- package/dist/in-store/extract-walls.d.ts.map +1 -1
- package/dist/in-store/extract-walls.js +400 -65
- package/dist/in-store/extract-walls.js.map +1 -1
- package/dist/in-store/generate-spaces-all.d.ts +72 -0
- package/dist/in-store/generate-spaces-all.d.ts.map +1 -0
- package/dist/in-store/generate-spaces-all.js +84 -0
- package/dist/in-store/generate-spaces-all.js.map +1 -0
- package/dist/in-store/generate-spaces.d.ts +33 -1
- package/dist/in-store/generate-spaces.d.ts.map +1 -1
- package/dist/in-store/generate-spaces.js +258 -3
- package/dist/in-store/generate-spaces.js.map +1 -1
- package/dist/in-store/resolve-anchor.d.ts.map +1 -1
- package/dist/in-store/resolve-anchor.js +58 -6
- package/dist/in-store/resolve-anchor.js.map +1 -1
- package/dist/in-store/slab.d.ts.map +1 -1
- package/dist/in-store/slab.js +3 -2
- package/dist/in-store/slab.js.map +1 -1
- package/dist/in-store/space.d.ts +28 -1
- package/dist/in-store/space.d.ts.map +1 -1
- package/dist/in-store/space.js +91 -8
- package/dist/in-store/space.js.map +1 -1
- package/dist/in-store/wall.d.ts.map +1 -1
- package/dist/in-store/wall.js +10 -7
- package/dist/in-store/wall.js.map +1 -1
- package/dist/index.d.ts +3 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3 -2
- package/dist/index.js.map +1 -1
- package/package.json +8 -8
|
@@ -28,7 +28,7 @@
|
|
|
28
28
|
* callers (and the viewer's Auto Spaces UI) can surface why a wall
|
|
29
29
|
* didn't contribute to the planar graph.
|
|
30
30
|
*/
|
|
31
|
-
import { EntityExtractor, extractLengthUnitScale, } from '@ifc-lite/parser';
|
|
31
|
+
import { EntityExtractor, extractLengthUnitScale, extractMaterialsOnDemand, } from '@ifc-lite/parser';
|
|
32
32
|
/**
|
|
33
33
|
* Element types treated as "wall-like" dividers by default. Extends
|
|
34
34
|
* the obvious walls with curtain walls (often the only divider on a
|
|
@@ -51,6 +51,7 @@ const AXIS_EPS = 1e-6;
|
|
|
51
51
|
export function extractWallSegmentsForStorey(store, storeyExpressId, overlay, options = {}) {
|
|
52
52
|
const segments = [];
|
|
53
53
|
const contributing = [];
|
|
54
|
+
const wallThicknesses = [];
|
|
54
55
|
const skipped = [];
|
|
55
56
|
const debug = !!options.debug;
|
|
56
57
|
const log = debug ? (...args) => console.debug('[extract-walls]', ...args) : () => { };
|
|
@@ -72,7 +73,7 @@ export function extractWallSegmentsForStorey(store, storeyExpressId, overlay, op
|
|
|
72
73
|
log(`length unit scale = ${lengthUnitScale} (raw → metres)`);
|
|
73
74
|
if (!store.source) {
|
|
74
75
|
log('no source bytes on data store — extraction cannot run');
|
|
75
|
-
return { segments, contributingWallIds: contributing, skipped, considered: 0, lengthUnitScale };
|
|
76
|
+
return { segments, contributingWallIds: contributing, wallThicknesses, skipped, considered: 0, lengthUnitScale };
|
|
76
77
|
}
|
|
77
78
|
const dividerTypes = new Set(DEFAULT_DIVIDER_TYPES);
|
|
78
79
|
if (options.extraDividerTypes) {
|
|
@@ -80,13 +81,14 @@ export function extractWallSegmentsForStorey(store, storeyExpressId, overlay, op
|
|
|
80
81
|
dividerTypes.add(t.toLowerCase());
|
|
81
82
|
}
|
|
82
83
|
const extractor = new EntityExtractor(store.source);
|
|
83
|
-
const dividerIds = collectDividerIdsOnStorey(store, storeyExpressId, dividerTypes, log);
|
|
84
|
+
const dividerIds = collectDividerIdsOnStorey(store, extractor, storeyExpressId, dividerTypes, log);
|
|
84
85
|
log(`storey #${storeyExpressId}: ${dividerIds.length} contained divider element(s)`);
|
|
85
86
|
for (const id of dividerIds) {
|
|
86
87
|
const result = extractWallAxisFromSource(store, extractor, id, log);
|
|
87
88
|
if (result.segment) {
|
|
88
89
|
segments.push(scaleSegment(result.segment, lengthUnitScale));
|
|
89
90
|
contributing.push(id);
|
|
91
|
+
wallThicknesses.push(wallThicknessFromMaterial(store, id));
|
|
90
92
|
}
|
|
91
93
|
else {
|
|
92
94
|
skipped.push({ wallId: id, reason: result.reason ?? 'no-axis-or-rect-profile' });
|
|
@@ -104,6 +106,7 @@ export function extractWallSegmentsForStorey(store, storeyExpressId, overlay, op
|
|
|
104
106
|
// metre coords — don't double-scale.
|
|
105
107
|
segments.push(result.segment);
|
|
106
108
|
contributing.push(ent.expressId);
|
|
109
|
+
wallThicknesses.push(undefined); // overlay walls carry no material yet
|
|
107
110
|
}
|
|
108
111
|
else {
|
|
109
112
|
skipped.push({ wallId: ent.expressId, reason: result.reason ?? 'no-axis-or-rect-profile' });
|
|
@@ -126,11 +129,27 @@ export function extractWallSegmentsForStorey(store, storeyExpressId, overlay, op
|
|
|
126
129
|
return {
|
|
127
130
|
segments,
|
|
128
131
|
contributingWallIds: contributing,
|
|
132
|
+
wallThicknesses,
|
|
129
133
|
skipped,
|
|
130
134
|
considered: dividerIds.length + overlayCount,
|
|
131
135
|
lengthUnitScale,
|
|
132
136
|
};
|
|
133
137
|
}
|
|
138
|
+
/**
|
|
139
|
+
* Total thickness (metres) of a wall's material layer set, or `undefined` when
|
|
140
|
+
* the wall has no resolvable layers. The material resolver already scales layer
|
|
141
|
+
* thicknesses to metres, so no further unit conversion is applied.
|
|
142
|
+
*/
|
|
143
|
+
function wallThicknessFromMaterial(store, wallId) {
|
|
144
|
+
const info = extractMaterialsOnDemand(store, wallId);
|
|
145
|
+
const layers = info?.layers;
|
|
146
|
+
if (!layers || layers.length === 0)
|
|
147
|
+
return undefined;
|
|
148
|
+
let total = 0;
|
|
149
|
+
for (const layer of layers)
|
|
150
|
+
total += layer.thickness ?? 0;
|
|
151
|
+
return total > 0 ? total : undefined;
|
|
152
|
+
}
|
|
134
153
|
function scaleSegment(seg, scale) {
|
|
135
154
|
if (scale === 1)
|
|
136
155
|
return seg;
|
|
@@ -142,12 +161,18 @@ function scaleSegment(seg, scale) {
|
|
|
142
161
|
function isDividerType(type, dividerTypes) {
|
|
143
162
|
return dividerTypes.has(type.toLowerCase());
|
|
144
163
|
}
|
|
145
|
-
function collectDividerIdsOnStorey(store, storeyId, dividerTypes, log) {
|
|
164
|
+
function collectDividerIdsOnStorey(store, extractor, storeyId, dividerTypes, log) {
|
|
146
165
|
const ids = [];
|
|
147
166
|
const seen = new Set();
|
|
148
167
|
if (!store.source)
|
|
149
168
|
return ids;
|
|
150
|
-
|
|
169
|
+
// Build relating → related-children indices ONCE, then walk in O(1) per
|
|
170
|
+
// parent. Previously each `descendAggregate` / `walkContainmentInto`
|
|
171
|
+
// call walked every IfcRelAggregates / IfcRelContainedInSpatialStructure
|
|
172
|
+
// entity in the file looking for one with the right relating id —
|
|
173
|
+
// O(R·N) total in the number of rels and parents visited.
|
|
174
|
+
const aggregateChildren = buildRelatingChildrenIndex(store, extractor, 'IFCRELAGGREGATES', 4, 5);
|
|
175
|
+
const containmentChildren = buildRelatingChildrenIndex(store, extractor, 'IFCRELCONTAINEDINSPATIALSTRUCTURE', 5, 4);
|
|
151
176
|
const visitMember = (memberId) => {
|
|
152
177
|
if (seen.has(memberId))
|
|
153
178
|
return;
|
|
@@ -169,80 +194,112 @@ function collectDividerIdsOnStorey(store, storeyId, dividerTypes, log) {
|
|
|
169
194
|
seen.add(parentId);
|
|
170
195
|
// Anything `IfcRelContainedInSpatialStructure`-anchored to this
|
|
171
196
|
// sub-structure should still be reachable.
|
|
172
|
-
|
|
197
|
+
const contained = containmentChildren.get(parentId);
|
|
198
|
+
if (contained)
|
|
199
|
+
for (const m of contained)
|
|
200
|
+
visitMember(m);
|
|
173
201
|
// And recurse through aggregation.
|
|
174
|
-
const
|
|
175
|
-
|
|
176
|
-
const
|
|
177
|
-
|
|
178
|
-
continue;
|
|
179
|
-
const rel = extractor.extractEntity(ref);
|
|
180
|
-
if (!rel)
|
|
181
|
-
continue;
|
|
182
|
-
const relating = rel.attributes[4];
|
|
183
|
-
if (typeof relating !== 'number' || relating !== parentId)
|
|
184
|
-
continue;
|
|
185
|
-
const related = rel.attributes[5];
|
|
186
|
-
if (!Array.isArray(related))
|
|
187
|
-
continue;
|
|
188
|
-
for (const child of related) {
|
|
189
|
-
if (typeof child !== 'number')
|
|
190
|
-
continue;
|
|
191
|
-
visitMember(child);
|
|
192
|
-
}
|
|
193
|
-
}
|
|
194
|
-
};
|
|
195
|
-
const walkContainmentInto = (parentId) => {
|
|
196
|
-
const containedRels = store.entityIndex.byType.get('IFCRELCONTAINEDINSPATIALSTRUCTURE') ?? [];
|
|
197
|
-
for (const relId of containedRels) {
|
|
198
|
-
const ref = store.entityIndex.byId.get(relId);
|
|
199
|
-
if (!ref)
|
|
200
|
-
continue;
|
|
201
|
-
const rel = extractor.extractEntity(ref);
|
|
202
|
-
if (!rel)
|
|
203
|
-
continue;
|
|
204
|
-
const relating = rel.attributes[5];
|
|
205
|
-
if (typeof relating !== 'number' || relating !== parentId)
|
|
206
|
-
continue;
|
|
207
|
-
const related = rel.attributes[4];
|
|
208
|
-
if (!Array.isArray(related))
|
|
209
|
-
continue;
|
|
210
|
-
for (const member of related) {
|
|
211
|
-
if (typeof member !== 'number')
|
|
212
|
-
continue;
|
|
213
|
-
visitMember(member);
|
|
214
|
-
}
|
|
215
|
-
}
|
|
202
|
+
const agg = aggregateChildren.get(parentId);
|
|
203
|
+
if (agg)
|
|
204
|
+
for (const c of agg)
|
|
205
|
+
visitMember(c);
|
|
216
206
|
};
|
|
217
207
|
// Mark the storey itself as seen but DON'T push it (we want its
|
|
218
208
|
// children, not the storey id).
|
|
219
209
|
seen.add(storeyId);
|
|
220
|
-
|
|
210
|
+
const storeyContained = containmentChildren.get(storeyId);
|
|
211
|
+
if (storeyContained)
|
|
212
|
+
for (const m of storeyContained)
|
|
213
|
+
visitMember(m);
|
|
221
214
|
// Some authoring tools attach elements via IfcRelAggregates to the
|
|
222
215
|
// storey instead of containment (or in addition). Walk both
|
|
223
216
|
// unconditionally to keep coverage broad.
|
|
224
|
-
const
|
|
225
|
-
|
|
217
|
+
const storeyAgg = aggregateChildren.get(storeyId);
|
|
218
|
+
if (storeyAgg)
|
|
219
|
+
for (const c of storeyAgg)
|
|
220
|
+
visitMember(c);
|
|
221
|
+
log(`collected ${ids.length} divider candidate(s) — types: ${[...dividerTypes].join(', ')}`);
|
|
222
|
+
return ids;
|
|
223
|
+
}
|
|
224
|
+
/**
|
|
225
|
+
* Footprint polygons (model-local metres, same frame as the extracted wall
|
|
226
|
+
* segments) of existing `IfcSpace` per storey — so generation can skip *only*
|
|
227
|
+
* the new rooms that overlap an already-present space (per-space dedup), while
|
|
228
|
+
* still adding rooms an empty part of the floor lacks. Keyed by storey
|
|
229
|
+
* expressId; storeys with no resolvable space footprints are omitted.
|
|
230
|
+
*/
|
|
231
|
+
export function existingSpaceFootprintsByStorey(store) {
|
|
232
|
+
const out = new Map();
|
|
233
|
+
if (!store.source)
|
|
234
|
+
return out;
|
|
235
|
+
const extractor = new EntityExtractor(store.source);
|
|
236
|
+
const scale = extractLengthUnitScale(store.source, store.entityIndex) ?? 1;
|
|
237
|
+
const aggregated = buildRelatingChildrenIndex(store, extractor, 'IFCRELAGGREGATES', 4, 5);
|
|
238
|
+
const contained = buildRelatingChildrenIndex(store, extractor, 'IFCRELCONTAINEDINSPATIALSTRUCTURE', 5, 4);
|
|
239
|
+
for (const st of store.getEntitiesByType('IfcBuildingStorey')) {
|
|
240
|
+
const kids = [...(aggregated.get(st.expressId) ?? []), ...(contained.get(st.expressId) ?? [])];
|
|
241
|
+
const footprints = [];
|
|
242
|
+
for (const id of kids) {
|
|
243
|
+
if ((store.entities.getTypeName(id) ?? '').toUpperCase() !== 'IFCSPACE')
|
|
244
|
+
continue;
|
|
245
|
+
const ref = store.entityIndex.byId.get(id);
|
|
246
|
+
if (!ref)
|
|
247
|
+
continue;
|
|
248
|
+
const ent = extractor.extractEntity(ref);
|
|
249
|
+
if (!ent)
|
|
250
|
+
continue;
|
|
251
|
+
const placementId = numericAttr(ent.attributes[5]); // ObjectPlacement
|
|
252
|
+
const representationId = numericAttr(ent.attributes[6]); // Representation
|
|
253
|
+
if (placementId === null || representationId === null)
|
|
254
|
+
continue;
|
|
255
|
+
const frame = readPlacementFrame(store, extractor, undefined, placementId);
|
|
256
|
+
const localPts = gatherBodyFootprintPoints(store, extractor, undefined, representationId);
|
|
257
|
+
if (!frame || !localPts || localPts.length < 3)
|
|
258
|
+
continue;
|
|
259
|
+
footprints.push(localPts.map((p) => {
|
|
260
|
+
const w = applyFrame(frame, p);
|
|
261
|
+
return [w[0] * scale, w[1] * scale];
|
|
262
|
+
}));
|
|
263
|
+
}
|
|
264
|
+
if (footprints.length)
|
|
265
|
+
out.set(st.expressId, footprints);
|
|
266
|
+
}
|
|
267
|
+
return out;
|
|
268
|
+
}
|
|
269
|
+
/**
|
|
270
|
+
* Index every relationship of `relType` by its "relating" attribute, so a
|
|
271
|
+
* lookup of "what's anchored to id X" becomes O(1) instead of an O(R)
|
|
272
|
+
* scan of every relationship.
|
|
273
|
+
*/
|
|
274
|
+
function buildRelatingChildrenIndex(store, extractor, relType, relatingIdx, relatedIdx) {
|
|
275
|
+
const out = new Map();
|
|
276
|
+
const relIds = store.entityIndex.byType.get(relType);
|
|
277
|
+
if (!relIds)
|
|
278
|
+
return out;
|
|
279
|
+
for (const relId of relIds) {
|
|
226
280
|
const ref = store.entityIndex.byId.get(relId);
|
|
227
281
|
if (!ref)
|
|
228
282
|
continue;
|
|
229
283
|
const rel = extractor.extractEntity(ref);
|
|
230
284
|
if (!rel)
|
|
231
285
|
continue;
|
|
232
|
-
const relating = rel.attributes[
|
|
233
|
-
if (typeof relating !== 'number'
|
|
286
|
+
const relating = rel.attributes[relatingIdx];
|
|
287
|
+
if (typeof relating !== 'number')
|
|
234
288
|
continue;
|
|
235
|
-
const related = rel.attributes[
|
|
289
|
+
const related = rel.attributes[relatedIdx];
|
|
236
290
|
if (!Array.isArray(related))
|
|
237
291
|
continue;
|
|
292
|
+
let bucket = out.get(relating);
|
|
293
|
+
if (!bucket) {
|
|
294
|
+
bucket = [];
|
|
295
|
+
out.set(relating, bucket);
|
|
296
|
+
}
|
|
238
297
|
for (const child of related) {
|
|
239
|
-
if (typeof child
|
|
240
|
-
|
|
241
|
-
visitMember(child);
|
|
298
|
+
if (typeof child === 'number')
|
|
299
|
+
bucket.push(child);
|
|
242
300
|
}
|
|
243
301
|
}
|
|
244
|
-
|
|
245
|
-
return ids;
|
|
302
|
+
return out;
|
|
246
303
|
}
|
|
247
304
|
function extractWallAxisFromSource(store, extractor, wallId, log) {
|
|
248
305
|
const ref = store.entityIndex.byId.get(wallId);
|
|
@@ -299,9 +356,278 @@ function computeWallSegment(store, extractor, placementId, representationId, ove
|
|
|
299
356
|
];
|
|
300
357
|
return finaliseSegment(start, end, wallId, log, 'rect-profile');
|
|
301
358
|
}
|
|
302
|
-
|
|
359
|
+
// Strategy 3 — body-footprint centreline. For walls with only a meshed
|
|
360
|
+
// body (IfcTriangulatedFaceSet / IfcPolygonalFaceSet) and no Axis/rect
|
|
361
|
+
// profile, project the body vertices to the local ground plane (drop the
|
|
362
|
+
// local vertical Z) and take the principal axis of that footprint as the
|
|
363
|
+
// wall centreline. Covers tessellated exports that the two shape-specific
|
|
364
|
+
// strategies miss.
|
|
365
|
+
const footprint = gatherBodyFootprintPoints(store, extractor, overlay, representationId);
|
|
366
|
+
if (footprint && footprint.length >= 3) {
|
|
367
|
+
const centreline = principalAxisCentreline(footprint);
|
|
368
|
+
if (centreline) {
|
|
369
|
+
const start = applyFrame(frame, centreline[0]);
|
|
370
|
+
const end = applyFrame(frame, centreline[1]);
|
|
371
|
+
return finaliseSegment(start, end, wallId, log, 'body-footprint');
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
log(`wall #${wallId}: no Axis representation, no IfcRectangleProfileDef body, no meshed footprint — skipping`);
|
|
303
375
|
return { segment: null, reason: 'no-axis-or-rect-profile' };
|
|
304
376
|
}
|
|
377
|
+
/**
|
|
378
|
+
* Gather the wall body's ground-plane footprint points (local frame, raw
|
|
379
|
+
* units) from a meshed representation. Reads the vertex list of every
|
|
380
|
+
* `IfcTriangulatedFaceSet` / `IfcPolygonalFaceSet` item in a non-`Axis`
|
|
381
|
+
* representation and drops the local vertical (Z), which is up in the
|
|
382
|
+
* wall-local frame before placement. Returns null when there's no mesh body.
|
|
383
|
+
*/
|
|
384
|
+
function gatherBodyFootprintPoints(store, extractor, overlay, representationId) {
|
|
385
|
+
const productShape = readEntity(store, extractor, overlay, representationId);
|
|
386
|
+
if (!productShape)
|
|
387
|
+
return null;
|
|
388
|
+
const reps = productShape.attributes[2];
|
|
389
|
+
if (!Array.isArray(reps))
|
|
390
|
+
return null;
|
|
391
|
+
const pts = [];
|
|
392
|
+
for (const repRef of reps) {
|
|
393
|
+
const repId = numericAttr(repRef);
|
|
394
|
+
if (repId === null)
|
|
395
|
+
continue;
|
|
396
|
+
const rep = readEntity(store, extractor, overlay, repId);
|
|
397
|
+
if (!rep)
|
|
398
|
+
continue;
|
|
399
|
+
const identifier = stringAttr(rep.attributes[1]);
|
|
400
|
+
if (identifier && identifier.toLowerCase() === 'axis')
|
|
401
|
+
continue; // handled by Strategy 1
|
|
402
|
+
const items = rep.attributes[3];
|
|
403
|
+
if (!Array.isArray(items))
|
|
404
|
+
continue;
|
|
405
|
+
for (const itemRef of items) {
|
|
406
|
+
const itemId = numericAttr(itemRef);
|
|
407
|
+
if (itemId === null)
|
|
408
|
+
continue;
|
|
409
|
+
const item = readEntity(store, extractor, overlay, itemId);
|
|
410
|
+
if (!item)
|
|
411
|
+
continue;
|
|
412
|
+
const itemType = resolveEntityTypeName(store, item, itemId);
|
|
413
|
+
if (itemType === 'ifctriangulatedfaceset' || itemType === 'ifcpolygonalfaceset') {
|
|
414
|
+
// Vertices live in an IfcCartesianPointList3D at attribute 0; its
|
|
415
|
+
// attribute 0 is the flat list of [x, y, z] triples (rep frame).
|
|
416
|
+
const coordId = numericAttr(item.attributes[0]);
|
|
417
|
+
if (coordId === null)
|
|
418
|
+
continue;
|
|
419
|
+
const coordList = readEntity(store, extractor, overlay, coordId);
|
|
420
|
+
if (!coordList || !Array.isArray(coordList.attributes[0]))
|
|
421
|
+
continue;
|
|
422
|
+
for (const p of coordList.attributes[0]) {
|
|
423
|
+
const v = readVec3(p);
|
|
424
|
+
if (v)
|
|
425
|
+
pts.push([v[0], v[1]]);
|
|
426
|
+
}
|
|
427
|
+
}
|
|
428
|
+
else if (itemType === 'ifcextrudedareasolid') {
|
|
429
|
+
gatherExtrudedFootprint(store, extractor, overlay, item, pts);
|
|
430
|
+
}
|
|
431
|
+
else if (itemType === 'ifcfacetedbrep') {
|
|
432
|
+
gatherBrepFootprint(store, extractor, overlay, item, pts);
|
|
433
|
+
}
|
|
434
|
+
}
|
|
435
|
+
}
|
|
436
|
+
return pts.length >= 3 ? pts : null;
|
|
437
|
+
}
|
|
438
|
+
const IDENTITY2 = { o: [0, 0], x: [1, 0], y: [0, 1] };
|
|
439
|
+
const apply2 = (f, p) => [
|
|
440
|
+
f.o[0] + f.x[0] * p[0] + f.y[0] * p[1],
|
|
441
|
+
f.o[1] + f.x[1] * p[0] + f.y[1] * p[1],
|
|
442
|
+
];
|
|
443
|
+
/** Read an IfcAxis2Placement2D/3D into its XY affine frame (Z ignored). */
|
|
444
|
+
function readPlacement2(store, extractor, overlay, ref) {
|
|
445
|
+
const id = numericAttr(ref);
|
|
446
|
+
if (id === null)
|
|
447
|
+
return IDENTITY2;
|
|
448
|
+
const ent = readEntity(store, extractor, overlay, id);
|
|
449
|
+
if (!ent)
|
|
450
|
+
return IDENTITY2;
|
|
451
|
+
const o = readCartesianPoint2D(store, extractor, overlay, ent.attributes[0]) ?? [0, 0];
|
|
452
|
+
// RefDirection: attr 1 on the 2D placement, attr 2 on the 3D placement.
|
|
453
|
+
const is3d = resolveEntityTypeName(store, ent, id) === 'ifcaxis2placement3d';
|
|
454
|
+
const refDir = readCartesianPoint2D(store, extractor, overlay, ent.attributes[is3d ? 2 : 1]);
|
|
455
|
+
let xx = 1, xy = 0;
|
|
456
|
+
if (refDir) {
|
|
457
|
+
const l = Math.hypot(refDir[0], refDir[1]) || 1;
|
|
458
|
+
xx = refDir[0] / l;
|
|
459
|
+
xy = refDir[1] / l;
|
|
460
|
+
}
|
|
461
|
+
return { o, x: [xx, xy], y: [-xy, xx] };
|
|
462
|
+
}
|
|
463
|
+
/**
|
|
464
|
+
* Footprint of a vertically-extruded solid = its swept profile placed into the
|
|
465
|
+
* rep frame. Handles rectangle and arbitrary-(polyline-)closed profiles; skips
|
|
466
|
+
* non-vertical extrusions (the profile wouldn't be a plan footprint then).
|
|
467
|
+
*/
|
|
468
|
+
function gatherExtrudedFootprint(store, extractor, overlay, solid, out) {
|
|
469
|
+
// IfcExtrudedAreaSolid = SweptArea(0), Position(1), ExtrudedDirection(2), Depth(3).
|
|
470
|
+
// The swept profile is a *plan* footprint only when the extrusion ends up
|
|
471
|
+
// world-vertical: ExtrudedDirection must be the solid's local Z AND the
|
|
472
|
+
// solid's Position must keep that Z world-up. Multi-layer walls (e.g. #218)
|
|
473
|
+
// extrude along their thickness via a Position rotated to a horizontal axis —
|
|
474
|
+
// their profile is an elevation, not a footprint, so skip rather than emit a
|
|
475
|
+
// bogus centreline.
|
|
476
|
+
const dirId = numericAttr(solid.attributes[2]);
|
|
477
|
+
if (dirId !== null) {
|
|
478
|
+
const d = readVec3(readEntity(store, extractor, overlay, dirId)?.attributes[0]);
|
|
479
|
+
if (d && Math.abs(d[2]) < 0.9)
|
|
480
|
+
return;
|
|
481
|
+
}
|
|
482
|
+
const posId = numericAttr(solid.attributes[1]);
|
|
483
|
+
if (posId !== null) {
|
|
484
|
+
const posEnt = readEntity(store, extractor, overlay, posId);
|
|
485
|
+
const axisId = posEnt ? numericAttr(posEnt.attributes[1]) : null;
|
|
486
|
+
if (axisId !== null) {
|
|
487
|
+
const az = readVec3(readEntity(store, extractor, overlay, axisId)?.attributes[0]);
|
|
488
|
+
if (az && Math.abs(az[2]) < 0.9)
|
|
489
|
+
return; // solid's local Z isn't world-up
|
|
490
|
+
}
|
|
491
|
+
}
|
|
492
|
+
const solidPos = readPlacement2(store, extractor, overlay, solid.attributes[1]);
|
|
493
|
+
const profileId = numericAttr(solid.attributes[0]);
|
|
494
|
+
if (profileId === null)
|
|
495
|
+
return;
|
|
496
|
+
const profile = readEntity(store, extractor, overlay, profileId);
|
|
497
|
+
if (!profile)
|
|
498
|
+
return;
|
|
499
|
+
const ptype = resolveEntityTypeName(store, profile, profileId);
|
|
500
|
+
if (ptype === 'ifcrectangleprofiledef') {
|
|
501
|
+
const ppos = readPlacement2(store, extractor, overlay, profile.attributes[2]);
|
|
502
|
+
const xd = numericAttr(profile.attributes[3]) ?? 0;
|
|
503
|
+
const yd = numericAttr(profile.attributes[4]) ?? 0;
|
|
504
|
+
if (xd <= 0 || yd <= 0)
|
|
505
|
+
return;
|
|
506
|
+
const corners = [[-xd / 2, -yd / 2], [xd / 2, -yd / 2], [xd / 2, yd / 2], [-xd / 2, yd / 2]];
|
|
507
|
+
for (const c of corners)
|
|
508
|
+
out.push(apply2(solidPos, apply2(ppos, c)));
|
|
509
|
+
}
|
|
510
|
+
else if (ptype === 'ifcarbitraryclosedprofiledef' || ptype === 'ifcarbitraryprofiledefwithvoids') {
|
|
511
|
+
// OuterCurve at attr 2 for both; read its points as the footprint cloud
|
|
512
|
+
// (PCA only needs the cloud, not edge order). Polyline or IndexedPolyCurve.
|
|
513
|
+
const ocId = numericAttr(profile.attributes[2]);
|
|
514
|
+
if (ocId === null)
|
|
515
|
+
return;
|
|
516
|
+
const oc = readEntity(store, extractor, overlay, ocId);
|
|
517
|
+
if (!oc)
|
|
518
|
+
return;
|
|
519
|
+
const octype = resolveEntityTypeName(store, oc, ocId);
|
|
520
|
+
if (octype === 'ifcpolyline' && Array.isArray(oc.attributes[0])) {
|
|
521
|
+
for (const ptRef of oc.attributes[0]) {
|
|
522
|
+
const v = readCartesianPoint2D(store, extractor, overlay, ptRef);
|
|
523
|
+
if (v)
|
|
524
|
+
out.push(apply2(solidPos, v));
|
|
525
|
+
}
|
|
526
|
+
}
|
|
527
|
+
else if (octype === 'ifcindexedpolycurve') {
|
|
528
|
+
// IfcIndexedPolyCurve.Points → IfcCartesianPointList2D (flat [x,y] list).
|
|
529
|
+
const listId = numericAttr(oc.attributes[0]);
|
|
530
|
+
if (listId === null)
|
|
531
|
+
return;
|
|
532
|
+
const list = readEntity(store, extractor, overlay, listId);
|
|
533
|
+
if (!list || !Array.isArray(list.attributes[0]))
|
|
534
|
+
return;
|
|
535
|
+
for (const p of list.attributes[0]) {
|
|
536
|
+
const v = readVec3(p);
|
|
537
|
+
if (v)
|
|
538
|
+
out.push(apply2(solidPos, [v[0], v[1]]));
|
|
539
|
+
}
|
|
540
|
+
}
|
|
541
|
+
}
|
|
542
|
+
}
|
|
543
|
+
/**
|
|
544
|
+
* Footprint of an IfcFacetedBrep = all its shell vertices projected to XY.
|
|
545
|
+
* Walks Outer (IfcClosedShell) → faces → bounds → IfcPolyLoop → points. The
|
|
546
|
+
* vertices are already in the rep frame, so no extra placement is needed.
|
|
547
|
+
*/
|
|
548
|
+
function gatherBrepFootprint(store, extractor, overlay, brep, out) {
|
|
549
|
+
const shellId = numericAttr(brep.attributes[0]);
|
|
550
|
+
if (shellId === null)
|
|
551
|
+
return;
|
|
552
|
+
const shell = readEntity(store, extractor, overlay, shellId);
|
|
553
|
+
if (!shell || !Array.isArray(shell.attributes[0]))
|
|
554
|
+
return;
|
|
555
|
+
for (const faceRef of shell.attributes[0]) {
|
|
556
|
+
const faceId = numericAttr(faceRef);
|
|
557
|
+
if (faceId === null)
|
|
558
|
+
continue;
|
|
559
|
+
const face = readEntity(store, extractor, overlay, faceId);
|
|
560
|
+
if (!face || !Array.isArray(face.attributes[0]))
|
|
561
|
+
continue;
|
|
562
|
+
for (const boundRef of face.attributes[0]) {
|
|
563
|
+
const boundId = numericAttr(boundRef);
|
|
564
|
+
if (boundId === null)
|
|
565
|
+
continue;
|
|
566
|
+
const bound = readEntity(store, extractor, overlay, boundId);
|
|
567
|
+
if (!bound)
|
|
568
|
+
continue;
|
|
569
|
+
const loopId = numericAttr(bound.attributes[0]);
|
|
570
|
+
if (loopId === null)
|
|
571
|
+
continue;
|
|
572
|
+
const loop = readEntity(store, extractor, overlay, loopId);
|
|
573
|
+
if (!loop || !Array.isArray(loop.attributes[0]))
|
|
574
|
+
continue;
|
|
575
|
+
for (const ptRef of loop.attributes[0]) {
|
|
576
|
+
const v = readCartesianPoint2D(store, extractor, overlay, ptRef);
|
|
577
|
+
if (v)
|
|
578
|
+
out.push(v);
|
|
579
|
+
}
|
|
580
|
+
}
|
|
581
|
+
}
|
|
582
|
+
}
|
|
583
|
+
/**
|
|
584
|
+
* Principal-axis centreline of a 2D point cloud (PCA). A wall footprint is a
|
|
585
|
+
* thin elongated rectangle, so the largest-eigenvalue direction is the wall's
|
|
586
|
+
* length and the centroid sits on the centreline — exactly the axis we want.
|
|
587
|
+
* Returns the centroid-anchored segment spanning the footprint's extent along
|
|
588
|
+
* that axis, or null if it collapses to a point.
|
|
589
|
+
*/
|
|
590
|
+
function principalAxisCentreline(points) {
|
|
591
|
+
const n = points.length;
|
|
592
|
+
let cx = 0, cy = 0;
|
|
593
|
+
for (const [x, y] of points) {
|
|
594
|
+
cx += x;
|
|
595
|
+
cy += y;
|
|
596
|
+
}
|
|
597
|
+
cx /= n;
|
|
598
|
+
cy /= n;
|
|
599
|
+
let sxx = 0, sxy = 0, syy = 0;
|
|
600
|
+
for (const [x, y] of points) {
|
|
601
|
+
const dx = x - cx, dy = y - cy;
|
|
602
|
+
sxx += dx * dx;
|
|
603
|
+
sxy += dx * dy;
|
|
604
|
+
syy += dy * dy;
|
|
605
|
+
}
|
|
606
|
+
const tr = sxx + syy;
|
|
607
|
+
const disc = Math.sqrt(Math.max(0, (tr * tr) / 4 - (sxx * syy - sxy * sxy)));
|
|
608
|
+
const lambda = tr / 2 + disc; // largest eigenvalue
|
|
609
|
+
let ex = sxy, ey = lambda - sxx;
|
|
610
|
+
if (Math.hypot(ex, ey) < 1e-12) {
|
|
611
|
+
ex = lambda - syy;
|
|
612
|
+
ey = sxy;
|
|
613
|
+
}
|
|
614
|
+
const elen = Math.hypot(ex, ey);
|
|
615
|
+
if (elen < 1e-12)
|
|
616
|
+
return null;
|
|
617
|
+
ex /= elen;
|
|
618
|
+
ey /= elen;
|
|
619
|
+
let tmin = Infinity, tmax = -Infinity;
|
|
620
|
+
for (const [x, y] of points) {
|
|
621
|
+
const t = (x - cx) * ex + (y - cy) * ey;
|
|
622
|
+
if (t < tmin)
|
|
623
|
+
tmin = t;
|
|
624
|
+
if (t > tmax)
|
|
625
|
+
tmax = t;
|
|
626
|
+
}
|
|
627
|
+
if (tmax - tmin < AXIS_EPS)
|
|
628
|
+
return null;
|
|
629
|
+
return [[cx + ex * tmin, cy + ey * tmin], [cx + ex * tmax, cy + ey * tmax]];
|
|
630
|
+
}
|
|
305
631
|
function finaliseSegment(start, end, wallId, log, source) {
|
|
306
632
|
const dx = end[0] - start[0];
|
|
307
633
|
const dy = end[1] - start[1];
|
|
@@ -402,7 +728,7 @@ function readAxisRepresentationEndpoints(store, extractor, overlay, representati
|
|
|
402
728
|
const item = readEntity(store, extractor, overlay, itemId);
|
|
403
729
|
if (!item)
|
|
404
730
|
continue;
|
|
405
|
-
const itemType = (store
|
|
731
|
+
const itemType = resolveEntityTypeName(store, item, itemId);
|
|
406
732
|
if (itemType !== 'ifcpolyline')
|
|
407
733
|
continue;
|
|
408
734
|
// IfcPolyline.Points = list of IfcCartesianPoint refs (attribute 0).
|
|
@@ -463,7 +789,7 @@ function readWallLength(store, extractor, overlay, representationId) {
|
|
|
463
789
|
const profile = readEntity(store, extractor, overlay, profileId);
|
|
464
790
|
if (!profile)
|
|
465
791
|
continue;
|
|
466
|
-
const profileType =
|
|
792
|
+
const profileType = resolveEntityTypeName(store, profile, profileId);
|
|
467
793
|
if (profileType !== 'ifcrectangleprofiledef')
|
|
468
794
|
continue;
|
|
469
795
|
// IfcRectangleProfileDef.XDim = attribute index 3.
|
|
@@ -474,9 +800,18 @@ function readWallLength(store, extractor, overlay, representationId) {
|
|
|
474
800
|
}
|
|
475
801
|
return null;
|
|
476
802
|
}
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
803
|
+
/**
|
|
804
|
+
* Robust IFC type-name for an entity read from source. The columnar entity
|
|
805
|
+
* table only indexes products/named entities, so `getTypeName` returns
|
|
806
|
+
* `'Unknown'` for geometry primitives (IfcPolyline, IfcCartesianPoint,
|
|
807
|
+
* profiles, solids). The extractor's `entity.type` is always reliable — so
|
|
808
|
+
* prefer the table only when it actually resolved, otherwise fall back to the
|
|
809
|
+
* extractor type. (Reading the table's `'Unknown'` literally was what made
|
|
810
|
+
* every `Curve2D` Axis polyline — e.g. all of AC20-FZK-Haus — get skipped.)
|
|
811
|
+
*/
|
|
812
|
+
export function resolveEntityTypeName(store, entity, entityId) {
|
|
813
|
+
const fromTable = store.entities.getTypeName(entityId);
|
|
814
|
+
const name = (fromTable && fromTable !== 'Unknown' ? fromTable : entity.type) ?? '';
|
|
480
815
|
return name.toLowerCase();
|
|
481
816
|
}
|
|
482
817
|
function readEntity(store, extractor, overlay, expressId) {
|