@pascal-app/ifc-converter 1.0.3 → 1.0.5
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/README.md +63 -4
- package/dist/beam-geometry.d.ts.map +1 -1
- package/dist/beam-geometry.js +3 -2
- package/dist/cleanup.d.ts +8 -1
- package/dist/cleanup.d.ts.map +1 -1
- package/dist/cleanup.js +13 -4
- package/dist/collections.d.ts +4 -0
- package/dist/collections.d.ts.map +1 -0
- package/dist/collections.js +66 -0
- package/dist/export/door-operation.d.ts +13 -0
- package/dist/export/door-operation.d.ts.map +1 -0
- package/dist/export/door-operation.js +45 -0
- package/dist/export/guid.d.ts +15 -0
- package/dist/export/guid.d.ts.map +1 -0
- package/dist/export/guid.js +80 -0
- package/dist/export/ifc-model.d.ts +91 -0
- package/dist/export/ifc-model.d.ts.map +1 -0
- package/dist/export/ifc-model.js +184 -0
- package/dist/export/index.d.ts +3 -0
- package/dist/export/index.d.ts.map +1 -0
- package/dist/export/index.js +2 -0
- package/dist/export/scene-to-ifc.d.ts +45 -0
- package/dist/export/scene-to-ifc.d.ts.map +1 -0
- package/dist/export/scene-to-ifc.js +1175 -0
- package/dist/export/step.d.ts +64 -0
- package/dist/export/step.d.ts.map +1 -0
- package/dist/export/step.js +158 -0
- package/dist/export/wall-base.d.ts +25 -0
- package/dist/export/wall-base.d.ts.map +1 -0
- package/dist/export/wall-base.js +91 -0
- package/dist/ids.d.ts +3 -0
- package/dist/ids.d.ts.map +1 -0
- package/dist/ids.js +35 -0
- package/dist/index.d.ts +2 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +570 -151
- package/dist/room-first.d.ts +35 -0
- package/dist/room-first.d.ts.map +1 -0
- package/dist/room-first.js +885 -0
- package/dist/wall-joins.d.ts +21 -0
- package/dist/wall-joins.d.ts.map +1 -0
- package/dist/wall-joins.js +367 -0
- package/package.json +10 -6
package/dist/index.js
CHANGED
|
@@ -1,10 +1,13 @@
|
|
|
1
|
-
import { BlockNode, BuildingNode, ColumnNode, DEFAULT_LEVEL_HEIGHT, DEFAULT_WALL_HEIGHT, DEFAULT_WALL_THICKNESS, DoorNode, GROUND_SUPPORT_ID, ImportedMeshNode, LevelNode, RoofNode, SiteNode, SlabNode, WallNode, WindowNode, ZoneNode, } from '@pascal-app/core';
|
|
2
|
-
import { customAlphabet } from 'nanoid';
|
|
1
|
+
import { BlockNode, BuildingNode, CeilingNode, ColumnNode, containsPoint, DEFAULT_LEVEL_HEIGHT, DEFAULT_WALL_HEIGHT, DEFAULT_WALL_THICKNESS, DoorNode, GROUND_SUPPORT_ID, ImportedMeshNode, LevelNode, polygonInteriorPoint, RoofNode, SiteNode, SlabNode, WallNode, WindowNode, ZoneNode, zoneColorForSeed, } from '@pascal-app/core';
|
|
3
2
|
import * as WebIFC from 'web-ifc';
|
|
4
3
|
import { extractBeamGeometry } from './beam-geometry';
|
|
5
4
|
import { simplifyConvertedSceneGraph } from './cleanup';
|
|
5
|
+
import { importCollections } from './collections';
|
|
6
6
|
import { doorGlazingStyle, doorStyleFromIfcOperation } from './door-semantics';
|
|
7
|
+
import { nextId, seedIds } from './ids';
|
|
8
|
+
import { applyRoomFirstStructure } from './room-first';
|
|
7
9
|
import { selectStoreyForElevation } from './storey-semantics';
|
|
10
|
+
import { redundantWallIds } from './wall-joins';
|
|
8
11
|
function meta(node) {
|
|
9
12
|
return (node?.metadata ?? {});
|
|
10
13
|
}
|
|
@@ -42,10 +45,6 @@ function tryParse(schema, kind, input) {
|
|
|
42
45
|
throw err;
|
|
43
46
|
}
|
|
44
47
|
}
|
|
45
|
-
const nanoid = customAlphabet('0123456789abcdefghijklmnopqrstuvwxyz', 16);
|
|
46
|
-
function generateId(prefix) {
|
|
47
|
-
return `${prefix}_${nanoid()}`;
|
|
48
|
-
}
|
|
49
48
|
// --- Unit detection ---
|
|
50
49
|
function getLengthUnitFactor(ifcApi, modelID) {
|
|
51
50
|
const prefixFactors = {
|
|
@@ -271,6 +270,36 @@ function getAxisPolyline(ifcApi, modelID, element) {
|
|
|
271
270
|
}
|
|
272
271
|
return null;
|
|
273
272
|
}
|
|
273
|
+
const DEFAULT_SLAB_THICKNESS = 0.05;
|
|
274
|
+
/** Deepest covering mesh read as flat when it has no extrusion to check. */
|
|
275
|
+
const MAX_FLAT_COVERING_DEPTH = 0.3;
|
|
276
|
+
function readCurvePoints(ifcApi, modelID, curveRef) {
|
|
277
|
+
const curve = ifcApi.GetLine(modelID, curveRef);
|
|
278
|
+
const pts = [];
|
|
279
|
+
if (!curve.Points)
|
|
280
|
+
return pts;
|
|
281
|
+
// IFCPOLYLINE — Points is array of CartesianPoint refs
|
|
282
|
+
if (Array.isArray(curve.Points) && curve.Points.length > 0 && curve.Points[0]?.value != null) {
|
|
283
|
+
for (const ptRef of curve.Points) {
|
|
284
|
+
const pt = ifcApi.GetLine(modelID, ptRef.value);
|
|
285
|
+
const coords = pt.Coordinates.map((c) => (typeof c === 'number' ? c : (c?.value ?? 0)));
|
|
286
|
+
pts.push([coords[0] ?? 0, coords[1] ?? 0]);
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
// IFCINDEXEDPOLYCURVE — Points is a reference to a point list
|
|
290
|
+
else if (curve.Points?.value) {
|
|
291
|
+
const ptList = ifcApi.GetLine(modelID, curve.Points.value);
|
|
292
|
+
if (ptList.CoordList) {
|
|
293
|
+
for (const coords of ptList.CoordList) {
|
|
294
|
+
const c = Array.isArray(coords)
|
|
295
|
+
? coords.map((v) => (typeof v === 'number' ? v : (v?.value ?? 0)))
|
|
296
|
+
: [];
|
|
297
|
+
pts.push([c[0] ?? 0, c[1] ?? 0]);
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
return pts;
|
|
302
|
+
}
|
|
274
303
|
function extractFromExtrusionItem(ifcApi, modelID, item, result) {
|
|
275
304
|
// Unwrap BooleanClippingResult → follow FirstOperand chain to find extrusion
|
|
276
305
|
let current = item;
|
|
@@ -287,6 +316,12 @@ function extractFromExtrusionItem(ifcApi, modelID, item, result) {
|
|
|
287
316
|
if (current.Depth?.value) {
|
|
288
317
|
result.depth = current.Depth.value;
|
|
289
318
|
}
|
|
319
|
+
if (current.ExtrudedDirection?.value) {
|
|
320
|
+
const direction = ifcApi.GetLine(modelID, current.ExtrudedDirection.value);
|
|
321
|
+
const ratios = (direction.DirectionRatios ?? []).map((c) => typeof c === 'number' ? c : (c?.value ?? 0));
|
|
322
|
+
if (ratios.length >= 2)
|
|
323
|
+
result.direction = [ratios[0] ?? 0, ratios[1] ?? 0, ratios[2] ?? 0];
|
|
324
|
+
}
|
|
290
325
|
if (current.SweptArea?.value) {
|
|
291
326
|
const profile = ifcApi.GetLine(modelID, current.SweptArea.value);
|
|
292
327
|
// Detect the profile shape by the fields present rather than the IFC
|
|
@@ -305,34 +340,16 @@ function extractFromExtrusionItem(ifcApi, modelID, item, result) {
|
|
|
305
340
|
// Extract profile points — OuterCurve for ArbitraryClosedProfileDef
|
|
306
341
|
const curveRef = profile.OuterCurve?.value;
|
|
307
342
|
if (curveRef) {
|
|
308
|
-
const
|
|
309
|
-
if (
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
pts.push([coords[0] ?? 0, coords[1] ?? 0]);
|
|
319
|
-
}
|
|
320
|
-
}
|
|
321
|
-
// IFCINDEXEDPOLYCURVE — Points is a reference to a point list
|
|
322
|
-
else if (curve.Points?.value) {
|
|
323
|
-
const ptList = ifcApi.GetLine(modelID, curve.Points.value);
|
|
324
|
-
if (ptList.CoordList) {
|
|
325
|
-
for (const coords of ptList.CoordList) {
|
|
326
|
-
const c = Array.isArray(coords)
|
|
327
|
-
? coords.map((v) => (typeof v === 'number' ? v : (v?.value ?? 0)))
|
|
328
|
-
: [];
|
|
329
|
-
pts.push([c[0] ?? 0, c[1] ?? 0]);
|
|
330
|
-
}
|
|
331
|
-
}
|
|
332
|
-
}
|
|
333
|
-
if (pts.length >= 3)
|
|
334
|
-
result.profilePoints = pts;
|
|
335
|
-
}
|
|
343
|
+
const pts = readCurvePoints(ifcApi, modelID, curveRef);
|
|
344
|
+
if (pts.length >= 3)
|
|
345
|
+
result.profilePoints = pts;
|
|
346
|
+
}
|
|
347
|
+
for (const innerRef of Array.isArray(profile.InnerCurves) ? profile.InnerCurves : []) {
|
|
348
|
+
if (!innerRef?.value)
|
|
349
|
+
continue;
|
|
350
|
+
const pts = readCurvePoints(ifcApi, modelID, innerRef.value);
|
|
351
|
+
if (pts.length >= 3)
|
|
352
|
+
result.innerCurves.push(pts);
|
|
336
353
|
}
|
|
337
354
|
}
|
|
338
355
|
return result.depth !== null;
|
|
@@ -345,6 +362,8 @@ function getBodyExtrusionData(ifcApi, modelID, element) {
|
|
|
345
362
|
profilePoints: null,
|
|
346
363
|
profileShape: null,
|
|
347
364
|
radius: null,
|
|
365
|
+
innerCurves: [],
|
|
366
|
+
direction: null,
|
|
348
367
|
};
|
|
349
368
|
try {
|
|
350
369
|
if (!element.Representation?.value)
|
|
@@ -406,9 +425,9 @@ function findExtrusionPosition(ifcApi, modelID, item) {
|
|
|
406
425
|
// world AABB. A world AABB conflates length and thickness for any wall
|
|
407
426
|
// the placement rotates (a 37°-rotated 0.2m wall would read ~1.9m
|
|
408
427
|
// thick); projecting onto the actual axis is rotation-invariant.
|
|
409
|
-
// (axisX, axisY) is the unit wall direction in
|
|
410
|
-
//
|
|
411
|
-
//
|
|
428
|
+
// (axisX, axisY) is the unit wall direction in IFC world XY. web-ifc
|
|
429
|
+
// meshes come in (X, Z, -Y), so a vertex's IFC plan point is (wx, -wz)
|
|
430
|
+
// and its height is wy; origin and scale cancel in extents.
|
|
412
431
|
// Returns extents in the geometry's native units (caller resolves
|
|
413
432
|
// scale), or null on any failure.
|
|
414
433
|
function measureWallLocalExtents(ifcApi, modelID, expressID, axisX, axisY) {
|
|
@@ -445,8 +464,8 @@ function measureWallLocalExtents(ifcApi, modelID, expressID, axisX, axisY) {
|
|
|
445
464
|
const wx = m[0] * x + m[4] * y + m[8] * z + m[12];
|
|
446
465
|
const wy = m[1] * x + m[5] * y + m[9] * z + m[13];
|
|
447
466
|
const wz = m[2] * x + m[6] * y + m[10] * z + m[14];
|
|
448
|
-
const a = wx * axisX
|
|
449
|
-
const p = wx * perpX
|
|
467
|
+
const a = wx * axisX - wz * axisY;
|
|
468
|
+
const p = wx * perpX - wz * perpY;
|
|
450
469
|
if (a < minA)
|
|
451
470
|
minA = a;
|
|
452
471
|
if (a > maxA)
|
|
@@ -455,10 +474,10 @@ function measureWallLocalExtents(ifcApi, modelID, expressID, axisX, axisY) {
|
|
|
455
474
|
minP = p;
|
|
456
475
|
if (p > maxP)
|
|
457
476
|
maxP = p;
|
|
458
|
-
if (
|
|
459
|
-
minV =
|
|
460
|
-
if (
|
|
461
|
-
maxV =
|
|
477
|
+
if (wy < minV)
|
|
478
|
+
minV = wy;
|
|
479
|
+
if (wy > maxV)
|
|
480
|
+
maxV = wy;
|
|
462
481
|
any = true;
|
|
463
482
|
}
|
|
464
483
|
}
|
|
@@ -545,15 +564,15 @@ function extractImportedMeshPrimitives(ifcApi, modelID, expressID, unitFactor, o
|
|
|
545
564
|
];
|
|
546
565
|
// `GetFlatMesh` does not use the same axes as the STEP placement
|
|
547
566
|
// data read by `resolveWorldTransform`: web-ifc has already mapped
|
|
548
|
-
// IFC Z-up coordinates to
|
|
549
|
-
//
|
|
550
|
-
//
|
|
551
|
-
//
|
|
567
|
+
// IFC Z-up coordinates to (X, Z, -Y), which is Pascal's Y-up
|
|
568
|
+
// right-handed frame. The default preset only removes the origin
|
|
569
|
+
// offset and level elevation; negating the third axis again would
|
|
570
|
+
// mirror the mesh.
|
|
552
571
|
const mappedPosition = swapYZ
|
|
553
572
|
? [
|
|
554
573
|
world[0] - originOffset[0] * unitFactor,
|
|
555
574
|
world[1] - originOffset[2] * unitFactor - levelElevation,
|
|
556
|
-
|
|
575
|
+
world[2] + originOffset[1] * unitFactor,
|
|
557
576
|
]
|
|
558
577
|
: [
|
|
559
578
|
world[0] - originOffset[0] * unitFactor,
|
|
@@ -570,19 +589,12 @@ function extractImportedMeshPrimitives(ifcApi, modelID, expressID, unitFactor, o
|
|
|
570
589
|
matrix[2] * nx + matrix[6] * ny + matrix[10] * nz,
|
|
571
590
|
];
|
|
572
591
|
const mappedNormal = swapYZ
|
|
573
|
-
?
|
|
592
|
+
? worldNormal
|
|
574
593
|
: [worldNormal[0], -worldNormal[2], worldNormal[1]];
|
|
575
594
|
const normalLength = Math.hypot(...mappedNormal) || 1;
|
|
576
595
|
normals.push(roundMeshNormal(mappedNormal[0] / normalLength), roundMeshNormal(mappedNormal[1] / normalLength), roundMeshNormal(mappedNormal[2] / normalLength));
|
|
577
596
|
}
|
|
578
597
|
const indices = Array.from(sourceIndices);
|
|
579
|
-
if (swapYZ) {
|
|
580
|
-
for (let index = 0; index + 2 < indices.length; index += 3) {
|
|
581
|
-
const second = indices[index + 1];
|
|
582
|
-
indices[index + 1] = indices[index + 2];
|
|
583
|
-
indices[index + 2] = second;
|
|
584
|
-
}
|
|
585
|
-
}
|
|
586
598
|
if (positions.length >= 9 && indices.length >= 3) {
|
|
587
599
|
primitives.push({
|
|
588
600
|
positions,
|
|
@@ -713,6 +725,7 @@ export async function convertIfcToPascal(ifcData, onProgress, options) {
|
|
|
713
725
|
await ifcApi.Init();
|
|
714
726
|
progress('Opening IFC model...', 10);
|
|
715
727
|
const modelID = ifcApi.OpenModel(ifcData);
|
|
728
|
+
seedIds(ifcData);
|
|
716
729
|
console.log(`[IFC→Pascal] Model opened, ID: ${modelID}, File size: ${(ifcData.length / 1024).toFixed(1)} KB`);
|
|
717
730
|
const nodes = {};
|
|
718
731
|
const rootNodeIds = [];
|
|
@@ -748,10 +761,16 @@ export async function convertIfcToPascal(ifcData, onProgress, options) {
|
|
|
748
761
|
catch {
|
|
749
762
|
/* keep zero offset */
|
|
750
763
|
}
|
|
764
|
+
// Scene points keep IFC's axis order (plan [0] and [1], vertical [2]), but
|
|
765
|
+
// the plan's second axis is Pascal z. Pascal is Y-up right-handed, so seen
|
|
766
|
+
// from above IFC north (+Y) is Pascal -Z: the default preset negates IFC Y
|
|
767
|
+
// here, once, for every placement-derived wall, opening, slab, roof, space
|
|
768
|
+
// and column. Mapping +Y to +Z mirrors the whole model.
|
|
769
|
+
const planDepthSign = opts.swapYZ ? -1 : 1;
|
|
751
770
|
function worldToScene(worldPt) {
|
|
752
771
|
return [
|
|
753
772
|
(worldPt[0] - originOffset[0]) * unitFactor,
|
|
754
|
-
(worldPt[1] - originOffset[1]) * unitFactor,
|
|
773
|
+
planDepthSign * (worldPt[1] - originOffset[1]) * unitFactor,
|
|
755
774
|
(worldPt[2] - originOffset[2]) * unitFactor,
|
|
756
775
|
];
|
|
757
776
|
}
|
|
@@ -801,6 +820,59 @@ export async function convertIfcToPascal(ifcData, onProgress, options) {
|
|
|
801
820
|
childrenMap.get(parentExpressID).add(childID);
|
|
802
821
|
}
|
|
803
822
|
}
|
|
823
|
+
// Elements a Pascal export wrote carry their node identity in a `Pascal`
|
|
824
|
+
// property set; storeys carry their height as the GrossHeight quantity.
|
|
825
|
+
const pascalIdentity = new Map();
|
|
826
|
+
const grossHeightByExpressId = new Map();
|
|
827
|
+
try {
|
|
828
|
+
const relDefines = ifcApi.GetLineIDsWithType(modelID, WebIFC.IFCRELDEFINESBYPROPERTIES);
|
|
829
|
+
for (let i = 0; i < relDefines.size(); i++) {
|
|
830
|
+
try {
|
|
831
|
+
const rel = ifcApi.GetLine(modelID, relDefines.get(i));
|
|
832
|
+
if (!rel.RelatedObjects || !rel.RelatingPropertyDefinition?.value)
|
|
833
|
+
continue;
|
|
834
|
+
const definition = ifcApi.GetLine(modelID, rel.RelatingPropertyDefinition.value);
|
|
835
|
+
if (definition.Name?.value === 'Pascal' && definition.HasProperties) {
|
|
836
|
+
const values = {};
|
|
837
|
+
for (const ref of definition.HasProperties) {
|
|
838
|
+
const property = ifcApi.GetLine(modelID, ref.value);
|
|
839
|
+
if (property.Name?.value)
|
|
840
|
+
values[property.Name.value] = property.NominalValue?.value;
|
|
841
|
+
}
|
|
842
|
+
if (typeof values.NodeId !== 'string' || typeof values.NodeType !== 'string')
|
|
843
|
+
continue;
|
|
844
|
+
for (const object of rel.RelatedObjects)
|
|
845
|
+
pascalIdentity.set(object.value, { id: values.NodeId, type: values.NodeType });
|
|
846
|
+
}
|
|
847
|
+
for (const ref of definition.Quantities ?? []) {
|
|
848
|
+
const quantity = ifcApi.GetLine(modelID, ref.value);
|
|
849
|
+
if (quantity.Name?.value !== 'GrossHeight' || !(quantity.LengthValue?.value > 0))
|
|
850
|
+
continue;
|
|
851
|
+
for (const object of rel.RelatedObjects)
|
|
852
|
+
grossHeightByExpressId.set(object.value, quantity.LengthValue.value * unitFactor);
|
|
853
|
+
}
|
|
854
|
+
}
|
|
855
|
+
catch {
|
|
856
|
+
/* skip rel */
|
|
857
|
+
}
|
|
858
|
+
}
|
|
859
|
+
}
|
|
860
|
+
catch {
|
|
861
|
+
/* no property rels */
|
|
862
|
+
}
|
|
863
|
+
// Re-importing a Pascal export keeps each node's id, so the scene keeps its
|
|
864
|
+
// identity (links, selections, history) across the round trip.
|
|
865
|
+
const usedPascalIds = new Set();
|
|
866
|
+
function nodeIdFor(expressId, kind, type = kind) {
|
|
867
|
+
const identity = pascalIdentity.get(expressId);
|
|
868
|
+
if (identity?.type === type &&
|
|
869
|
+
identity.id.startsWith(`${kind}_`) &&
|
|
870
|
+
!usedPascalIds.has(identity.id)) {
|
|
871
|
+
usedPascalIds.add(identity.id);
|
|
872
|
+
return identity.id;
|
|
873
|
+
}
|
|
874
|
+
return nextId(kind);
|
|
875
|
+
}
|
|
804
876
|
// Resolve containing storey for an element by walking the parent chain
|
|
805
877
|
function findStoreyForElement(expressId) {
|
|
806
878
|
let current = expressId;
|
|
@@ -885,6 +957,85 @@ export async function convertIfcToPascal(ifcData, onProgress, options) {
|
|
|
885
957
|
importedPrimitivesByExpressId.set(expressId, primitives);
|
|
886
958
|
return primitives;
|
|
887
959
|
}
|
|
960
|
+
// Level-local vertical extent of the element's triangles.
|
|
961
|
+
function meshHeightRange(expressId) {
|
|
962
|
+
const heightAxis = opts.swapYZ ? 1 : 2;
|
|
963
|
+
let min = Number.POSITIVE_INFINITY;
|
|
964
|
+
let max = Number.NEGATIVE_INFINITY;
|
|
965
|
+
for (const primitive of importedMeshPrimitivesFor(expressId)) {
|
|
966
|
+
for (let index = heightAxis; index < primitive.positions.length; index += 3) {
|
|
967
|
+
const value = primitive.positions[index];
|
|
968
|
+
if (value < min)
|
|
969
|
+
min = value;
|
|
970
|
+
if (value > max)
|
|
971
|
+
max = value;
|
|
972
|
+
}
|
|
973
|
+
}
|
|
974
|
+
return min <= max ? { min, max } : null;
|
|
975
|
+
}
|
|
976
|
+
// Plan outline, holes and level-local vertical extent of an extruded
|
|
977
|
+
// element (slab, covering, opening, site). `flat` is false when the
|
|
978
|
+
// profile or extrusion is not horizontal/vertical.
|
|
979
|
+
function extrudedPlan(element, levelElevation) {
|
|
980
|
+
const worldMat = element.ObjectPlacement?.value
|
|
981
|
+
? resolveWorldTransform(ifcApi, modelID, element.ObjectPlacement.value)
|
|
982
|
+
: identity();
|
|
983
|
+
const body = getBodyExtrusionData(ifcApi, modelID, element);
|
|
984
|
+
const extrusionMat = getExtrusionPosition(ifcApi, modelID, element);
|
|
985
|
+
const combinedMat = extrusionMat ? multiply(worldMat, extrusionMat) : worldMat;
|
|
986
|
+
const toPlan = (points) => {
|
|
987
|
+
const ring = points.map((point) => {
|
|
988
|
+
const scene = worldToScene(transformPoint3(combinedMat, [point[0], point[1], 0]));
|
|
989
|
+
return [scene[0], scene[1]];
|
|
990
|
+
});
|
|
991
|
+
const first = ring[0];
|
|
992
|
+
const last = ring.at(-1);
|
|
993
|
+
if (ring.length > 3 &&
|
|
994
|
+
first &&
|
|
995
|
+
last &&
|
|
996
|
+
Math.abs(first[0] - last[0]) < 1e-6 &&
|
|
997
|
+
Math.abs(first[1] - last[1]) < 1e-6)
|
|
998
|
+
ring.pop();
|
|
999
|
+
return ring;
|
|
1000
|
+
};
|
|
1001
|
+
let polygon = null;
|
|
1002
|
+
if (body.profilePoints && body.profilePoints.length >= 3) {
|
|
1003
|
+
polygon = toPlan(body.profilePoints);
|
|
1004
|
+
}
|
|
1005
|
+
else if (body.xDim && body.yDim) {
|
|
1006
|
+
const hw = body.xDim / 2;
|
|
1007
|
+
const hh = body.yDim / 2;
|
|
1008
|
+
polygon = toPlan([
|
|
1009
|
+
[-hw, -hh],
|
|
1010
|
+
[hw, -hh],
|
|
1011
|
+
[hw, hh],
|
|
1012
|
+
[-hw, hh],
|
|
1013
|
+
]);
|
|
1014
|
+
}
|
|
1015
|
+
if (polygon && polygon.length < 3)
|
|
1016
|
+
polygon = null;
|
|
1017
|
+
let top = null;
|
|
1018
|
+
let bottom = null;
|
|
1019
|
+
let flat = null;
|
|
1020
|
+
if (body.depth) {
|
|
1021
|
+
const direction = body.direction ?? [0, 0, 1];
|
|
1022
|
+
const length = Math.hypot(direction[0], direction[1], direction[2]) || 1;
|
|
1023
|
+
const tip = direction.map((value) => (value / length) * body.depth);
|
|
1024
|
+
const z0 = worldToScene(transformPoint3(combinedMat, [0, 0, 0]))[2];
|
|
1025
|
+
const z1 = worldToScene(transformPoint3(combinedMat, tip))[2];
|
|
1026
|
+
top = Math.max(z0, z1) - levelElevation;
|
|
1027
|
+
bottom = Math.min(z0, z1) - levelElevation;
|
|
1028
|
+
flat =
|
|
1029
|
+
Math.abs(combinedMat[10]) > 0.999 && Math.abs(z1 - z0) >= body.depth * unitFactor * 0.999;
|
|
1030
|
+
}
|
|
1031
|
+
return {
|
|
1032
|
+
polygon,
|
|
1033
|
+
holes: polygon ? body.innerCurves.map(toPlan).filter((ring) => ring.length >= 3) : [],
|
|
1034
|
+
top,
|
|
1035
|
+
bottom,
|
|
1036
|
+
flat,
|
|
1037
|
+
};
|
|
1038
|
+
}
|
|
888
1039
|
progress('Processing sites...', 30);
|
|
889
1040
|
// Process sites
|
|
890
1041
|
const sites = ifcApi.GetLineIDsWithType(modelID, WebIFC.IFCSITE);
|
|
@@ -892,7 +1043,7 @@ export async function convertIfcToPascal(ifcData, onProgress, options) {
|
|
|
892
1043
|
for (let i = 0; i < sites.size(); i++) {
|
|
893
1044
|
const siteExpressID = sites.get(i);
|
|
894
1045
|
const site = ifcApi.GetLine(modelID, siteExpressID);
|
|
895
|
-
const nodeId =
|
|
1046
|
+
const nodeId = nodeIdFor(siteExpressID, 'site');
|
|
896
1047
|
expressIdToNodeId.set(siteExpressID, nodeId);
|
|
897
1048
|
rootNodeIds.push(nodeId);
|
|
898
1049
|
const siteNode = tryParse(SiteNode, 'site', {
|
|
@@ -903,10 +1054,8 @@ export async function convertIfcToPascal(ifcData, onProgress, options) {
|
|
|
903
1054
|
parentId: null,
|
|
904
1055
|
visible: true,
|
|
905
1056
|
polygon: {
|
|
906
|
-
//
|
|
907
|
-
//
|
|
908
|
-
// editor's default 30x30 square here.
|
|
909
|
-
// TODO(ifc-fix): derive from IfcSite.SiteAddress or building footprints.
|
|
1057
|
+
// Placeholder until the whole model is read: fitSitePolygons below
|
|
1058
|
+
// replaces it with the site footprint or the imported extent.
|
|
910
1059
|
type: 'polygon',
|
|
911
1060
|
points: [
|
|
912
1061
|
[-15, -15],
|
|
@@ -931,7 +1080,7 @@ export async function convertIfcToPascal(ifcData, onProgress, options) {
|
|
|
931
1080
|
for (let i = 0; i < buildings.size(); i++) {
|
|
932
1081
|
const buildingExpressID = buildings.get(i);
|
|
933
1082
|
const building = ifcApi.GetLine(modelID, buildingExpressID);
|
|
934
|
-
const nodeId =
|
|
1083
|
+
const nodeId = nodeIdFor(buildingExpressID, 'building');
|
|
935
1084
|
expressIdToNodeId.set(buildingExpressID, nodeId);
|
|
936
1085
|
const parentExpressID = parentMap.get(buildingExpressID);
|
|
937
1086
|
const parentNodeId = parentExpressID ? expressIdToNodeId.get(parentExpressID) : null;
|
|
@@ -961,7 +1110,7 @@ export async function convertIfcToPascal(ifcData, onProgress, options) {
|
|
|
961
1110
|
for (let i = 0; i < storeys.size(); i++) {
|
|
962
1111
|
const storeyExpressID = storeys.get(i);
|
|
963
1112
|
const storey = ifcApi.GetLine(modelID, storeyExpressID);
|
|
964
|
-
const nodeId =
|
|
1113
|
+
const nodeId = nodeIdFor(storeyExpressID, 'level');
|
|
965
1114
|
expressIdToNodeId.set(storeyExpressID, nodeId);
|
|
966
1115
|
const parentExpressID = parentMap.get(storeyExpressID);
|
|
967
1116
|
const parentNodeId = parentExpressID ? expressIdToNodeId.get(parentExpressID) : null;
|
|
@@ -1022,24 +1171,24 @@ export async function convertIfcToPascal(ifcData, onProgress, options) {
|
|
|
1022
1171
|
const height = nextExpressId
|
|
1023
1172
|
? (storeyElevationByExpressId.get(nextExpressId) ?? 0) -
|
|
1024
1173
|
(storeyElevationByExpressId.get(expressId) ?? 0)
|
|
1025
|
-
: DEFAULT_LEVEL_HEIGHT;
|
|
1174
|
+
: (grossHeightByExpressId.get(expressId) ?? DEFAULT_LEVEL_HEIGHT);
|
|
1026
1175
|
level.level = index;
|
|
1027
1176
|
level.height = height > 0.1 ? height : DEFAULT_LEVEL_HEIGHT;
|
|
1028
1177
|
}
|
|
1029
1178
|
}
|
|
1030
1179
|
progress('Processing walls...', 60);
|
|
1031
|
-
// Build void/fill relationship maps for doors and
|
|
1032
|
-
// IFCRELVOIDSELEMENT: wall (RelatingBuildingElement) → opening (RelatedOpeningElement)
|
|
1033
|
-
const
|
|
1180
|
+
// Build void/fill relationship maps for doors, windows and slab holes
|
|
1181
|
+
// IFCRELVOIDSELEMENT: wall/slab (RelatingBuildingElement) → opening (RelatedOpeningElement)
|
|
1182
|
+
const hostToOpenings = new Map();
|
|
1034
1183
|
const relVoids = ifcApi.GetLineIDsWithType(modelID, WebIFC.IFCRELVOIDSELEMENT);
|
|
1035
1184
|
for (let i = 0; i < relVoids.size(); i++) {
|
|
1036
1185
|
const rel = ifcApi.GetLine(modelID, relVoids.get(i));
|
|
1037
|
-
const
|
|
1186
|
+
const hostId = rel.RelatingBuildingElement?.value;
|
|
1038
1187
|
const openingId = rel.RelatedOpeningElement?.value;
|
|
1039
|
-
if (
|
|
1040
|
-
if (!
|
|
1041
|
-
|
|
1042
|
-
|
|
1188
|
+
if (hostId && openingId) {
|
|
1189
|
+
if (!hostToOpenings.has(hostId))
|
|
1190
|
+
hostToOpenings.set(hostId, []);
|
|
1191
|
+
hostToOpenings.get(hostId).push(openingId);
|
|
1043
1192
|
}
|
|
1044
1193
|
}
|
|
1045
1194
|
// IFCRELFILLSELEMENT: opening (RelatingOpeningElement) → door/window (RelatedBuildingElement)
|
|
@@ -1066,6 +1215,69 @@ export async function convertIfcToPascal(ifcData, onProgress, options) {
|
|
|
1066
1215
|
for (let i = 0; i < ids.size(); i++)
|
|
1067
1216
|
windowExpressIds.add(ids.get(i));
|
|
1068
1217
|
}
|
|
1218
|
+
// IfcMaterialLayerSetUsage places the wall body relative to its reference
|
|
1219
|
+
// line: across [low, high] on the wall's local Y (model units).
|
|
1220
|
+
const layerUsageByElement = new Map();
|
|
1221
|
+
try {
|
|
1222
|
+
const relMaterials = ifcApi.GetLineIDsWithType(modelID, WebIFC.IFCRELASSOCIATESMATERIAL);
|
|
1223
|
+
for (let i = 0; i < relMaterials.size(); i++) {
|
|
1224
|
+
try {
|
|
1225
|
+
const rel = ifcApi.GetLine(modelID, relMaterials.get(i));
|
|
1226
|
+
if (!rel.RelatingMaterial?.value || !rel.RelatedObjects)
|
|
1227
|
+
continue;
|
|
1228
|
+
const usage = ifcApi.GetLine(modelID, rel.RelatingMaterial.value);
|
|
1229
|
+
if (usage.type !== WebIFC.IFCMATERIALLAYERSETUSAGE || !usage.ForLayerSet?.value)
|
|
1230
|
+
continue;
|
|
1231
|
+
if (String(usage.LayerSetDirection?.value ?? 'AXIS2').toUpperCase() !== 'AXIS2')
|
|
1232
|
+
continue;
|
|
1233
|
+
const layerSet = ifcApi.GetLine(modelID, usage.ForLayerSet.value);
|
|
1234
|
+
let total = 0;
|
|
1235
|
+
for (const layerRef of layerSet.MaterialLayers ?? []) {
|
|
1236
|
+
total += Number(ifcApi.GetLine(modelID, layerRef.value).LayerThickness?.value ?? 0);
|
|
1237
|
+
}
|
|
1238
|
+
const offset = Number(usage.OffsetFromReferenceLine?.value ?? 0);
|
|
1239
|
+
if (!(total > 0) || !Number.isFinite(offset))
|
|
1240
|
+
continue;
|
|
1241
|
+
const negative = String(usage.DirectionSense?.value ?? '').toUpperCase() === 'NEGATIVE';
|
|
1242
|
+
const extent = negative
|
|
1243
|
+
? { low: offset - total, high: offset }
|
|
1244
|
+
: { low: offset, high: offset + total };
|
|
1245
|
+
for (const object of rel.RelatedObjects)
|
|
1246
|
+
layerUsageByElement.set(object.value, extent);
|
|
1247
|
+
}
|
|
1248
|
+
catch {
|
|
1249
|
+
/* skip rel */
|
|
1250
|
+
}
|
|
1251
|
+
}
|
|
1252
|
+
}
|
|
1253
|
+
catch {
|
|
1254
|
+
/* no material rels */
|
|
1255
|
+
}
|
|
1256
|
+
// Wall node id → plan unit vector from the IFC reference line into the body.
|
|
1257
|
+
const wallBodySides = new Map();
|
|
1258
|
+
// IfcRelConnectsPathElements: walls joined at an end (ATSTART/ATEND).
|
|
1259
|
+
const wallConnections = [];
|
|
1260
|
+
try {
|
|
1261
|
+
const relPaths = ifcApi.GetLineIDsWithType(modelID, WebIFC.IFCRELCONNECTSPATHELEMENTS);
|
|
1262
|
+
for (let i = 0; i < relPaths.size(); i++) {
|
|
1263
|
+
try {
|
|
1264
|
+
const rel = ifcApi.GetLine(modelID, relPaths.get(i));
|
|
1265
|
+
const ends = [rel.RelatingConnectionType?.value, rel.RelatedConnectionType?.value].map((value) => String(value ?? '').toUpperCase());
|
|
1266
|
+
if (!ends.some((value) => value === 'ATSTART' || value === 'ATEND'))
|
|
1267
|
+
continue;
|
|
1268
|
+
const a = rel.RelatingElement?.value;
|
|
1269
|
+
const b = rel.RelatedElement?.value;
|
|
1270
|
+
if (a && b)
|
|
1271
|
+
wallConnections.push([a, b]);
|
|
1272
|
+
}
|
|
1273
|
+
catch {
|
|
1274
|
+
/* skip rel */
|
|
1275
|
+
}
|
|
1276
|
+
}
|
|
1277
|
+
}
|
|
1278
|
+
catch {
|
|
1279
|
+
/* no connection rels */
|
|
1280
|
+
}
|
|
1069
1281
|
// Process walls (both IFCWALL and IFCWALLSTANDARDCASE)
|
|
1070
1282
|
const wallTypes = [WebIFC.IFCWALL, WebIFC.IFCWALLSTANDARDCASE];
|
|
1071
1283
|
for (const wallType of wallTypes) {
|
|
@@ -1075,13 +1287,14 @@ export async function convertIfcToPascal(ifcData, onProgress, options) {
|
|
|
1075
1287
|
if (expressIdToNodeId.has(wallExpressID))
|
|
1076
1288
|
continue;
|
|
1077
1289
|
const wall = ifcApi.GetLine(modelID, wallExpressID);
|
|
1078
|
-
const nodeId =
|
|
1290
|
+
const nodeId = nodeIdFor(wallExpressID, 'wall');
|
|
1079
1291
|
expressIdToNodeId.set(wallExpressID, nodeId);
|
|
1080
1292
|
const parentNodeId = resolveElementParent(wallExpressID);
|
|
1081
1293
|
let start = [0, 0];
|
|
1082
1294
|
let end = null;
|
|
1083
1295
|
let thickness;
|
|
1084
1296
|
let height;
|
|
1297
|
+
let onReferenceLine = false;
|
|
1085
1298
|
try {
|
|
1086
1299
|
// Resolve world placement
|
|
1087
1300
|
const worldMat = wall.ObjectPlacement?.value
|
|
@@ -1090,6 +1303,7 @@ export async function convertIfcToPascal(ifcData, onProgress, options) {
|
|
|
1090
1303
|
// Try to get axis polyline (local 2D line along wall)
|
|
1091
1304
|
const axisPts = getAxisPolyline(ifcApi, modelID, wall);
|
|
1092
1305
|
if (axisPts && axisPts.length >= 2) {
|
|
1306
|
+
onReferenceLine = true;
|
|
1093
1307
|
const s0 = worldToScene(transformPoint3(worldMat, axisPts[0]));
|
|
1094
1308
|
const s1 = worldToScene(transformPoint3(worldMat, axisPts[axisPts.length - 1]));
|
|
1095
1309
|
start = [s0[0], s0[1]];
|
|
@@ -1175,7 +1389,8 @@ export async function convertIfcToPascal(ifcData, onProgress, options) {
|
|
|
1175
1389
|
// IFC ground plane, which is also the mapping used for the
|
|
1176
1390
|
// wall's start/end above.
|
|
1177
1391
|
const axisX = (end[0] - start[0]) / wallLenM;
|
|
1178
|
-
|
|
1392
|
+
// Back to IFC world XY, the frame measureWallLocalExtents projects in.
|
|
1393
|
+
const axisY = (planDepthSign * (end[1] - start[1])) / wallLenM;
|
|
1179
1394
|
const extents = measureWallLocalExtents(ifcApi, modelID, wallExpressID, axisX, axisY);
|
|
1180
1395
|
const geom = extents
|
|
1181
1396
|
? wallHeightThicknessFromExtents(extents, wallLenM, unitFactor)
|
|
@@ -1192,6 +1407,29 @@ export async function convertIfcToPascal(ifcData, onProgress, options) {
|
|
|
1192
1407
|
height = DEFAULT_WALL_HEIGHT;
|
|
1193
1408
|
if (thickness === undefined)
|
|
1194
1409
|
thickness = DEFAULT_WALL_THICKNESS;
|
|
1410
|
+
// Every later pass (merging, corner joins, room faces) works on body
|
|
1411
|
+
// centrelines; a face reference line is restored at the end as
|
|
1412
|
+
// `justification` (see applyWallReferenceLines).
|
|
1413
|
+
const layers = layerUsageByElement.get(wallExpressID);
|
|
1414
|
+
if (layers && onReferenceLine && wall.ObjectPlacement?.value) {
|
|
1415
|
+
const worldMat = resolveWorldTransform(ifcApi, modelID, wall.ObjectPlacement.value);
|
|
1416
|
+
const sideX = worldMat[1];
|
|
1417
|
+
const sideY = planDepthSign * worldMat[5];
|
|
1418
|
+
const sideLength = Math.hypot(sideX, sideY);
|
|
1419
|
+
const low = layers.low * unitFactor;
|
|
1420
|
+
const high = layers.high * unitFactor;
|
|
1421
|
+
const centre = (low + high) / 2;
|
|
1422
|
+
if (sideLength > 1e-6 && Math.abs(centre) > Math.max(0.001, thickness * 0.02)) {
|
|
1423
|
+
const side = [sideX / sideLength, sideY / sideLength];
|
|
1424
|
+
start = [start[0] + side[0] * centre, start[1] + side[1] * centre];
|
|
1425
|
+
end = [end[0] + side[0] * centre, end[1] + side[1] * centre];
|
|
1426
|
+
const faceTolerance = Math.max(0.002, thickness * 0.02);
|
|
1427
|
+
if (Math.abs(low) <= faceTolerance)
|
|
1428
|
+
wallBodySides.set(nodeId, side);
|
|
1429
|
+
else if (Math.abs(high) <= faceTolerance)
|
|
1430
|
+
wallBodySides.set(nodeId, [-side[0], -side[1]]);
|
|
1431
|
+
}
|
|
1432
|
+
}
|
|
1195
1433
|
const wallNode = tryParse(WallNode, 'wall', {
|
|
1196
1434
|
object: 'node',
|
|
1197
1435
|
id: nodeId,
|
|
@@ -1216,8 +1454,28 @@ export async function convertIfcToPascal(ifcData, onProgress, options) {
|
|
|
1216
1454
|
attachNodeToGraph(nodeId, parentNodeId);
|
|
1217
1455
|
}
|
|
1218
1456
|
}
|
|
1457
|
+
// Cladding, linings and embedded walls fall through to the imported-mesh
|
|
1458
|
+
// pass. Ends that met a removed lining still reach its host wall's body.
|
|
1459
|
+
const wallLinings = new Map();
|
|
1460
|
+
for (const [wallNodeId, host] of redundantWallIds(Object.values(nodes).filter((node) => node.type === 'wall'))) {
|
|
1461
|
+
const expressId = meta(nodes[wallNodeId]).expressID;
|
|
1462
|
+
if (expressId === undefined || hostToOpenings.has(expressId) || pascalIdentity.has(expressId))
|
|
1463
|
+
continue;
|
|
1464
|
+
const hostExpressId = meta(host).expressID;
|
|
1465
|
+
const thickness = nodes[wallNodeId].thickness ?? 0;
|
|
1466
|
+
if (hostExpressId !== undefined)
|
|
1467
|
+
wallLinings.set(hostExpressId, Math.max(wallLinings.get(hostExpressId) ?? 0, thickness));
|
|
1468
|
+
for (const node of Object.values(nodes)) {
|
|
1469
|
+
if (node.children?.includes(wallNodeId))
|
|
1470
|
+
node.children = node.children.filter((id) => id !== wallNodeId);
|
|
1471
|
+
}
|
|
1472
|
+
if (rootNodeIds.includes(wallNodeId))
|
|
1473
|
+
rootNodeIds.splice(rootNodeIds.indexOf(wallNodeId), 1);
|
|
1474
|
+
delete nodes[wallNodeId];
|
|
1475
|
+
expressIdToNodeId.delete(expressId);
|
|
1476
|
+
}
|
|
1219
1477
|
// Process doors and windows via void/fill relationships
|
|
1220
|
-
for (const [wallExpressID, openingIds] of
|
|
1478
|
+
for (const [wallExpressID, openingIds] of hostToOpenings) {
|
|
1221
1479
|
const wallNodeId = expressIdToNodeId.get(wallExpressID);
|
|
1222
1480
|
if (!wallNodeId)
|
|
1223
1481
|
continue;
|
|
@@ -1307,7 +1565,7 @@ export async function convertIfcToPascal(ifcData, onProgress, options) {
|
|
|
1307
1565
|
}
|
|
1308
1566
|
}
|
|
1309
1567
|
if (isDoor) {
|
|
1310
|
-
const nodeId =
|
|
1568
|
+
const nodeId = nodeIdFor(fillId, 'door');
|
|
1311
1569
|
// Vertical centering is now handled: door center Y = height/2 so the
|
|
1312
1570
|
// opening sits at the correct position. Remaining caveat: door bottom
|
|
1313
1571
|
// is assumed at floor y=0 (i.e. the door starts at the wall's base).
|
|
@@ -1323,6 +1581,8 @@ export async function convertIfcToPascal(ifcData, onProgress, options) {
|
|
|
1323
1581
|
width: width ?? 0.9,
|
|
1324
1582
|
height: height ?? 2.1,
|
|
1325
1583
|
position: doorPosition,
|
|
1584
|
+
wallId: wallNodeId,
|
|
1585
|
+
floorThresholdVersion: 1,
|
|
1326
1586
|
...doorStyleFromIfcOperation(element.OperationType?.value),
|
|
1327
1587
|
metadata: buildMetadata({
|
|
1328
1588
|
ifcType: 'IFCDOOR',
|
|
@@ -1337,11 +1597,9 @@ export async function convertIfcToPascal(ifcData, onProgress, options) {
|
|
|
1337
1597
|
wallNode.children.push(nodeId);
|
|
1338
1598
|
}
|
|
1339
1599
|
else {
|
|
1340
|
-
const nodeId =
|
|
1341
|
-
//
|
|
1342
|
-
//
|
|
1343
|
-
// schema (Pascal's WindowNode doesn't have sillHeight today —
|
|
1344
|
-
// moved to metadata for now so we don't lose the value).
|
|
1600
|
+
const nodeId = nodeIdFor(fillId, 'window');
|
|
1601
|
+
// WindowNode has no sill field: the sill sets the centre height in
|
|
1602
|
+
// position[1]; the IFC value is kept in metadata as well.
|
|
1345
1603
|
const windowPosition = [
|
|
1346
1604
|
position ?? 0,
|
|
1347
1605
|
(sillHeight ?? 0) + (height ?? 1.2) / 2,
|
|
@@ -1357,6 +1615,8 @@ export async function convertIfcToPascal(ifcData, onProgress, options) {
|
|
|
1357
1615
|
width: width ?? 1.0,
|
|
1358
1616
|
height: height ?? 1.2,
|
|
1359
1617
|
position: windowPosition,
|
|
1618
|
+
wallId: wallNodeId,
|
|
1619
|
+
floorThresholdVersion: 1,
|
|
1360
1620
|
metadata: buildMetadata({
|
|
1361
1621
|
ifcType: 'IFCWINDOW',
|
|
1362
1622
|
expressID: fillId,
|
|
@@ -1491,7 +1751,7 @@ export async function convertIfcToPascal(ifcData, onProgress, options) {
|
|
|
1491
1751
|
const parentNodeId = hosted.info.nodeId;
|
|
1492
1752
|
if (isDoor) {
|
|
1493
1753
|
const h = height ?? 2.1;
|
|
1494
|
-
const nodeId =
|
|
1754
|
+
const nodeId = nodeIdFor(fillId, 'door');
|
|
1495
1755
|
const doorNode = tryParse(DoorNode, 'door', {
|
|
1496
1756
|
object: 'node',
|
|
1497
1757
|
id: nodeId,
|
|
@@ -1503,6 +1763,7 @@ export async function convertIfcToPascal(ifcData, onProgress, options) {
|
|
|
1503
1763
|
height: h,
|
|
1504
1764
|
position: [hosted.along, h / 2, 0],
|
|
1505
1765
|
wallId: hosted.info.nodeId,
|
|
1766
|
+
floorThresholdVersion: 1,
|
|
1506
1767
|
...doorStyleFromIfcOperation(element.OperationType?.value),
|
|
1507
1768
|
metadata: buildMetadata({
|
|
1508
1769
|
ifcType: 'IFCDOOR',
|
|
@@ -1521,7 +1782,7 @@ export async function convertIfcToPascal(ifcData, onProgress, options) {
|
|
|
1521
1782
|
else {
|
|
1522
1783
|
const h = height ?? 1.2;
|
|
1523
1784
|
const sill = scene ? Math.max(0, scene[2] - hosted.info.baseY) : 0;
|
|
1524
|
-
const nodeId =
|
|
1785
|
+
const nodeId = nodeIdFor(fillId, 'window');
|
|
1525
1786
|
const windowNode = tryParse(WindowNode, 'window', {
|
|
1526
1787
|
object: 'node',
|
|
1527
1788
|
id: nodeId,
|
|
@@ -1533,6 +1794,7 @@ export async function convertIfcToPascal(ifcData, onProgress, options) {
|
|
|
1533
1794
|
height: h,
|
|
1534
1795
|
position: [hosted.along, sill + h / 2, 0],
|
|
1535
1796
|
wallId: hosted.info.nodeId,
|
|
1797
|
+
floorThresholdVersion: 1,
|
|
1536
1798
|
metadata: buildMetadata({
|
|
1537
1799
|
ifcType: 'IFCWINDOW',
|
|
1538
1800
|
expressID: fillId,
|
|
@@ -1567,66 +1829,45 @@ export async function convertIfcToPascal(ifcData, onProgress, options) {
|
|
|
1567
1829
|
importedMeshPrimitivesFor(slabExpressID).length > 0) {
|
|
1568
1830
|
continue;
|
|
1569
1831
|
}
|
|
1570
|
-
const
|
|
1571
|
-
|
|
1572
|
-
const parentNodeId = resolveElementParent(slabExpressID);
|
|
1573
|
-
let polygon = null;
|
|
1574
|
-
let elevation = 0;
|
|
1575
|
-
let thickness;
|
|
1832
|
+
const levelElevation = elementLevelElevation(slabExpressID);
|
|
1833
|
+
let plan = null;
|
|
1576
1834
|
try {
|
|
1577
|
-
|
|
1578
|
-
const worldMat = slab.ObjectPlacement?.value
|
|
1579
|
-
? resolveWorldTransform(ifcApi, modelID, slab.ObjectPlacement.value)
|
|
1580
|
-
: identity();
|
|
1581
|
-
// Get elevation from placement Z
|
|
1582
|
-
const s = worldToScene(transformPoint3(worldMat, [0, 0, 0]));
|
|
1583
|
-
elevation = s[2] - elementLevelElevation(slabExpressID);
|
|
1584
|
-
// Get body extrusion data
|
|
1585
|
-
const body = getBodyExtrusionData(ifcApi, modelID, slab);
|
|
1586
|
-
// Extrusion depth is slab thickness
|
|
1587
|
-
if (body.depth) {
|
|
1588
|
-
thickness = body.depth * unitFactor;
|
|
1589
|
-
}
|
|
1590
|
-
// Extrusion Position provides an additional local offset for the profile
|
|
1591
|
-
const extrusionMat = getExtrusionPosition(ifcApi, modelID, slab);
|
|
1592
|
-
if (body.profilePoints && body.profilePoints.length >= 3) {
|
|
1593
|
-
const combinedMat = extrusionMat ? multiply(worldMat, extrusionMat) : worldMat;
|
|
1594
|
-
polygon = body.profilePoints.map((pt) => {
|
|
1595
|
-
const sc = worldToScene(transformPoint3(combinedMat, [pt[0], pt[1], 0]));
|
|
1596
|
-
return [sc[0], sc[1]];
|
|
1597
|
-
});
|
|
1598
|
-
const first = polygon[0];
|
|
1599
|
-
const last = polygon[polygon.length - 1];
|
|
1600
|
-
if (polygon.length > 3 &&
|
|
1601
|
-
Math.abs(first[0] - last[0]) < 1e-6 &&
|
|
1602
|
-
Math.abs(first[1] - last[1]) < 1e-6) {
|
|
1603
|
-
polygon.pop();
|
|
1604
|
-
}
|
|
1605
|
-
}
|
|
1606
|
-
else if (body.xDim && body.yDim) {
|
|
1607
|
-
const hw = body.xDim / 2;
|
|
1608
|
-
const hh = body.yDim / 2;
|
|
1609
|
-
const corners = [
|
|
1610
|
-
[-hw, -hh, 0],
|
|
1611
|
-
[hw, -hh, 0],
|
|
1612
|
-
[hw, hh, 0],
|
|
1613
|
-
[-hw, hh, 0],
|
|
1614
|
-
];
|
|
1615
|
-
const combinedMat = extrusionMat ? multiply(worldMat, extrusionMat) : worldMat;
|
|
1616
|
-
polygon = corners.map((c) => {
|
|
1617
|
-
const sc = worldToScene(transformPoint3(combinedMat, c));
|
|
1618
|
-
return [sc[0], sc[1]];
|
|
1619
|
-
});
|
|
1620
|
-
}
|
|
1835
|
+
plan = extrudedPlan(slab, levelElevation);
|
|
1621
1836
|
}
|
|
1622
1837
|
catch {
|
|
1623
|
-
//
|
|
1838
|
+
// mesh fallback below
|
|
1624
1839
|
}
|
|
1625
|
-
//
|
|
1626
|
-
if (
|
|
1627
|
-
expressIdToNodeId.delete(slabExpressID);
|
|
1840
|
+
// A sloped or tilted slab is not a Pascal floor; keep its exact mesh.
|
|
1841
|
+
if (plan?.flat === false && importedMeshPrimitivesFor(slabExpressID).length > 0)
|
|
1628
1842
|
continue;
|
|
1629
|
-
|
|
1843
|
+
if (!plan?.polygon)
|
|
1844
|
+
continue;
|
|
1845
|
+
// Revit extrudes floors down from their top, other tools up from their
|
|
1846
|
+
// bottom: the top is wherever the extrusion ends highest. The mesh wins
|
|
1847
|
+
// when the body is more than that one extrusion (extra items, clipping).
|
|
1848
|
+
const meshRange = meshHeightRange(slabExpressID);
|
|
1849
|
+
const meshAgrees = !meshRange ||
|
|
1850
|
+
(plan.top !== null &&
|
|
1851
|
+
Math.abs(meshRange.max - plan.top) <= 0.01 &&
|
|
1852
|
+
Math.abs(meshRange.min - (plan.bottom ?? plan.top)) <= 0.01);
|
|
1853
|
+
const top = (meshAgrees ? plan.top : meshRange?.max) ?? 0;
|
|
1854
|
+
const bottom = meshAgrees ? (plan.bottom ?? undefined) : meshRange?.min;
|
|
1855
|
+
const thickness = bottom !== undefined && top - bottom >= 0.005 ? top - bottom : DEFAULT_SLAB_THICKNESS;
|
|
1856
|
+
const holes = [
|
|
1857
|
+
...plan.holes,
|
|
1858
|
+
...(hostToOpenings.get(slabExpressID) ?? []).flatMap((openingId) => {
|
|
1859
|
+
try {
|
|
1860
|
+
const opening = extrudedPlan(ifcApi.GetLine(modelID, openingId), levelElevation);
|
|
1861
|
+
return opening?.polygon ? [opening.polygon] : [];
|
|
1862
|
+
}
|
|
1863
|
+
catch {
|
|
1864
|
+
return [];
|
|
1865
|
+
}
|
|
1866
|
+
}),
|
|
1867
|
+
];
|
|
1868
|
+
const parentNodeId = resolveElementParent(slabExpressID);
|
|
1869
|
+
const nodeId = nodeIdFor(slabExpressID, 'slab');
|
|
1870
|
+
expressIdToNodeId.set(slabExpressID, nodeId);
|
|
1630
1871
|
const slabNode = tryParse(SlabNode, 'slab', {
|
|
1631
1872
|
object: 'node',
|
|
1632
1873
|
id: nodeId,
|
|
@@ -1634,22 +1875,117 @@ export async function convertIfcToPascal(ifcData, onProgress, options) {
|
|
|
1634
1875
|
name: slab.Name?.value || `Slab ${i + 1}`,
|
|
1635
1876
|
parentId: parentNodeId || null,
|
|
1636
1877
|
visible: true,
|
|
1637
|
-
polygon,
|
|
1638
|
-
holes
|
|
1639
|
-
|
|
1640
|
-
|
|
1641
|
-
|
|
1878
|
+
polygon: plan.polygon,
|
|
1879
|
+
holes,
|
|
1880
|
+
holeMetadata: holes.map(() => ({ source: 'manual' })),
|
|
1881
|
+
elevation: top,
|
|
1882
|
+
thickness,
|
|
1642
1883
|
metadata: buildMetadata({
|
|
1643
1884
|
ifcType: 'IFCSLAB',
|
|
1644
1885
|
expressID: slabExpressID,
|
|
1645
1886
|
globalId: slab.GlobalId?.value,
|
|
1646
1887
|
predefinedType: slab.PredefinedType?.value,
|
|
1647
|
-
|
|
1888
|
+
sourceColor: importedMeshPrimitivesFor(slabExpressID)[0]?.color,
|
|
1648
1889
|
}),
|
|
1649
1890
|
});
|
|
1650
1891
|
nodes[nodeId] = slabNode;
|
|
1651
1892
|
attachNodeToGraph(nodeId, parentNodeId);
|
|
1652
1893
|
}
|
|
1894
|
+
// Ceiling and flooring coverings become Pascal ceilings and floor finishes
|
|
1895
|
+
// (see room-first.ts). Everything else stays an exact imported mesh.
|
|
1896
|
+
const coveringIds = ifcApi.GetLineIDsWithType(modelID, WebIFC.IFCCOVERING);
|
|
1897
|
+
for (let i = 0; i < coveringIds.size(); i++) {
|
|
1898
|
+
const coveringExpressID = coveringIds.get(i);
|
|
1899
|
+
try {
|
|
1900
|
+
const covering = ifcApi.GetLine(modelID, coveringExpressID);
|
|
1901
|
+
const role = String(covering.PredefinedType?.value ?? '').toUpperCase();
|
|
1902
|
+
if (role !== 'CEILING' && role !== 'FLOORING')
|
|
1903
|
+
continue;
|
|
1904
|
+
const levelElevation = elementLevelElevation(coveringExpressID);
|
|
1905
|
+
const range = meshHeightRange(coveringExpressID);
|
|
1906
|
+
if (!range)
|
|
1907
|
+
continue;
|
|
1908
|
+
let plan = null;
|
|
1909
|
+
try {
|
|
1910
|
+
plan = extrudedPlan(covering, levelElevation);
|
|
1911
|
+
}
|
|
1912
|
+
catch {
|
|
1913
|
+
// hull fallback below
|
|
1914
|
+
}
|
|
1915
|
+
// A sloped covering is no Pascal ceiling or floor: it keeps its exact mesh.
|
|
1916
|
+
if (plan?.flat === false)
|
|
1917
|
+
continue;
|
|
1918
|
+
let polygon = plan?.polygon ?? null;
|
|
1919
|
+
if (!polygon) {
|
|
1920
|
+
// Without an extrusion to read, only a thin mesh is known to be flat.
|
|
1921
|
+
if (range.max - range.min > MAX_FLAT_COVERING_DEPTH)
|
|
1922
|
+
continue;
|
|
1923
|
+
polygon = meshFootprint(importedMeshPrimitivesFor(coveringExpressID), opts.swapYZ);
|
|
1924
|
+
}
|
|
1925
|
+
if (!polygon || polygon.length < 3)
|
|
1926
|
+
continue;
|
|
1927
|
+
const holes = [
|
|
1928
|
+
...(plan?.polygon ? plan.holes : []),
|
|
1929
|
+
...(hostToOpenings.get(coveringExpressID) ?? []).flatMap((openingId) => {
|
|
1930
|
+
try {
|
|
1931
|
+
const opening = extrudedPlan(ifcApi.GetLine(modelID, openingId), levelElevation);
|
|
1932
|
+
return opening?.polygon ? [opening.polygon] : [];
|
|
1933
|
+
}
|
|
1934
|
+
catch {
|
|
1935
|
+
return [];
|
|
1936
|
+
}
|
|
1937
|
+
}),
|
|
1938
|
+
];
|
|
1939
|
+
const holeMetadata = holes.map(() => ({ source: 'manual' }));
|
|
1940
|
+
const parentNodeId = resolveElementParent(coveringExpressID);
|
|
1941
|
+
if (!parentNodeId)
|
|
1942
|
+
continue;
|
|
1943
|
+
const color = importedMeshPrimitivesFor(coveringExpressID)[0]?.color;
|
|
1944
|
+
const metadata = buildMetadata({
|
|
1945
|
+
ifcType: 'IFCCOVERING',
|
|
1946
|
+
expressID: coveringExpressID,
|
|
1947
|
+
globalId: covering.GlobalId?.value,
|
|
1948
|
+
predefinedType: role,
|
|
1949
|
+
objectType: covering.ObjectType?.value,
|
|
1950
|
+
sourceColor: color,
|
|
1951
|
+
});
|
|
1952
|
+
const name = covering.Name?.value || (role === 'CEILING' ? 'Ceiling' : 'Flooring');
|
|
1953
|
+
const node = role === 'CEILING'
|
|
1954
|
+
? tryParse(CeilingNode, 'ceiling', {
|
|
1955
|
+
object: 'node',
|
|
1956
|
+
id: nodeIdFor(coveringExpressID, 'ceiling'),
|
|
1957
|
+
type: 'ceiling',
|
|
1958
|
+
name,
|
|
1959
|
+
parentId: parentNodeId,
|
|
1960
|
+
visible: true,
|
|
1961
|
+
polygon,
|
|
1962
|
+
holes,
|
|
1963
|
+
holeMetadata,
|
|
1964
|
+
height: range.min,
|
|
1965
|
+
metadata,
|
|
1966
|
+
})
|
|
1967
|
+
: tryParse(SlabNode, 'slab', {
|
|
1968
|
+
object: 'node',
|
|
1969
|
+
id: nodeIdFor(coveringExpressID, 'slab'),
|
|
1970
|
+
type: 'slab',
|
|
1971
|
+
name,
|
|
1972
|
+
parentId: parentNodeId,
|
|
1973
|
+
visible: true,
|
|
1974
|
+
polygon,
|
|
1975
|
+
holes,
|
|
1976
|
+
holeMetadata,
|
|
1977
|
+
elevation: range.max,
|
|
1978
|
+
thickness: Math.max(0.005, range.max - range.min),
|
|
1979
|
+
metadata,
|
|
1980
|
+
});
|
|
1981
|
+
expressIdToNodeId.set(coveringExpressID, node.id);
|
|
1982
|
+
nodes[node.id] = node;
|
|
1983
|
+
attachNodeToGraph(node.id, parentNodeId);
|
|
1984
|
+
}
|
|
1985
|
+
catch {
|
|
1986
|
+
// keep it for the imported-mesh fallback
|
|
1987
|
+
}
|
|
1988
|
+
}
|
|
1653
1989
|
// Process roofs
|
|
1654
1990
|
const roofs = ifcApi.GetLineIDsWithType(modelID, WebIFC.IFCROOF);
|
|
1655
1991
|
for (let i = 0; i < roofs.size(); i++) {
|
|
@@ -1657,7 +1993,7 @@ export async function convertIfcToPascal(ifcData, onProgress, options) {
|
|
|
1657
1993
|
if (expressIdToNodeId.has(roofExpressID))
|
|
1658
1994
|
continue;
|
|
1659
1995
|
const roof = ifcApi.GetLine(modelID, roofExpressID);
|
|
1660
|
-
const nodeId =
|
|
1996
|
+
const nodeId = nodeIdFor(roofExpressID, 'roof');
|
|
1661
1997
|
expressIdToNodeId.set(roofExpressID, nodeId);
|
|
1662
1998
|
const parentNodeId = resolveElementParent(roofExpressID);
|
|
1663
1999
|
let polygon;
|
|
@@ -1750,7 +2086,7 @@ export async function convertIfcToPascal(ifcData, onProgress, options) {
|
|
|
1750
2086
|
if (expressIdToNodeId.has(colExpressID))
|
|
1751
2087
|
continue;
|
|
1752
2088
|
const col = ifcApi.GetLine(modelID, colExpressID);
|
|
1753
|
-
const nodeId =
|
|
2089
|
+
const nodeId = nodeIdFor(colExpressID, 'column');
|
|
1754
2090
|
expressIdToNodeId.set(colExpressID, nodeId);
|
|
1755
2091
|
const parentNodeId = resolveElementParent(colExpressID);
|
|
1756
2092
|
let position = [0, 0, 0];
|
|
@@ -1892,15 +2228,18 @@ export async function convertIfcToPascal(ifcData, onProgress, options) {
|
|
|
1892
2228
|
const longName = space.LongName?.value;
|
|
1893
2229
|
const roomNumberCandidate = longName && spaceName !== longName ? (spaceName ?? '').trim() : '';
|
|
1894
2230
|
const roomNumber = roomNumberCandidate.length <= 32 ? roomNumberCandidate : '';
|
|
1895
|
-
const nodeId =
|
|
2231
|
+
const nodeId = nodeIdFor(spaceExpressId, 'zone');
|
|
1896
2232
|
const zone = tryParse(ZoneNode, 'zone', {
|
|
1897
2233
|
object: 'node',
|
|
1898
2234
|
id: nodeId,
|
|
1899
2235
|
type: 'zone',
|
|
2236
|
+
color: zoneColorForSeed(nodeId),
|
|
1900
2237
|
name: longName || spaceName || `Space ${i + 1}`,
|
|
1901
2238
|
parentId: parentNodeId,
|
|
1902
2239
|
visible: true,
|
|
1903
2240
|
polygon,
|
|
2241
|
+
// The room keeps this identity point through adoption and edits.
|
|
2242
|
+
seed: polygonInteriorPoint({ polygon }),
|
|
1904
2243
|
spaceRole: 'room',
|
|
1905
2244
|
roomNumber,
|
|
1906
2245
|
ceilingHeight: Math.max(0.1, ceilingHeight),
|
|
@@ -1951,6 +2290,7 @@ export async function convertIfcToPascal(ifcData, onProgress, options) {
|
|
|
1951
2290
|
if (!geometry)
|
|
1952
2291
|
throw new Error('No renderable beam geometry');
|
|
1953
2292
|
const beamNode = tryParse(BlockNode, 'beam', {
|
|
2293
|
+
id: nodeIdFor(beamExpressID, 'block'),
|
|
1954
2294
|
name: beam.Name?.value || `Beam ${i + 1}`,
|
|
1955
2295
|
parentId: parentNodeId ?? null,
|
|
1956
2296
|
...geometry,
|
|
@@ -1998,6 +2338,7 @@ export async function convertIfcToPascal(ifcData, onProgress, options) {
|
|
|
1998
2338
|
addFallbackType(WebIFC.IFCPLATE, 'IFCPLATE');
|
|
1999
2339
|
addFallbackType(WebIFC.IFCMEMBER, 'IFCMEMBER');
|
|
2000
2340
|
addFallbackType(WebIFC.IFCFOOTING, 'IFCFOOTING');
|
|
2341
|
+
addFallbackType(WebIFC.IFCGEOGRAPHICELEMENT, 'IFCGEOGRAPHICELEMENT');
|
|
2001
2342
|
addFallbackType(WebIFC.IFCSTAIRFLIGHT, 'IFCSTAIRFLIGHT');
|
|
2002
2343
|
addFallbackType(WebIFC.IFCSPACE, 'IFCSPACE');
|
|
2003
2344
|
addFallbackType(WebIFC.IFCWALL, 'IFCWALL');
|
|
@@ -2025,7 +2366,7 @@ export async function convertIfcToPascal(ifcData, onProgress, options) {
|
|
|
2025
2366
|
const primitives = importedMeshPrimitivesFor(expressId);
|
|
2026
2367
|
if (primitives.length === 0)
|
|
2027
2368
|
continue;
|
|
2028
|
-
const nodeId =
|
|
2369
|
+
const nodeId = nodeIdFor(expressId, 'imesh', 'imported-mesh');
|
|
2029
2370
|
const importedMesh = tryParse(ImportedMeshNode, 'imported mesh', {
|
|
2030
2371
|
object: 'node',
|
|
2031
2372
|
id: nodeId,
|
|
@@ -2060,6 +2401,13 @@ export async function convertIfcToPascal(ifcData, onProgress, options) {
|
|
|
2060
2401
|
m.levelId = expressIdToNodeId.get(storeyExpId) ?? undefined;
|
|
2061
2402
|
}
|
|
2062
2403
|
}
|
|
2404
|
+
// Pascal-authored nodes are already Pascal geometry: cleanup leaves them be.
|
|
2405
|
+
for (const node of Object.values(nodes)) {
|
|
2406
|
+
const m = meta(node);
|
|
2407
|
+
const identity = m.expressID === undefined ? undefined : pascalIdentity.get(m.expressID);
|
|
2408
|
+
if (identity)
|
|
2409
|
+
m.pascalNodeId = identity.id;
|
|
2410
|
+
}
|
|
2063
2411
|
// Post-process: extract property sets and materials
|
|
2064
2412
|
const elementExpressIds = new Set();
|
|
2065
2413
|
const expressIdToNode = new Map();
|
|
@@ -2233,13 +2581,83 @@ export async function convertIfcToPascal(ifcData, onProgress, options) {
|
|
|
2233
2581
|
catch {
|
|
2234
2582
|
/* no type rels */
|
|
2235
2583
|
}
|
|
2584
|
+
// The site's property line: the IfcSite footprint when it holds the whole
|
|
2585
|
+
// model, else the imported extent plus a margin.
|
|
2586
|
+
function fitSitePolygons() {
|
|
2587
|
+
let minX = Number.POSITIVE_INFINITY;
|
|
2588
|
+
let minZ = Number.POSITIVE_INFINITY;
|
|
2589
|
+
let maxX = Number.NEGATIVE_INFINITY;
|
|
2590
|
+
let maxZ = Number.NEGATIVE_INFINITY;
|
|
2591
|
+
const include = (x, z, pad = 0) => {
|
|
2592
|
+
minX = Math.min(minX, x - pad);
|
|
2593
|
+
minZ = Math.min(minZ, z - pad);
|
|
2594
|
+
maxX = Math.max(maxX, x + pad);
|
|
2595
|
+
maxZ = Math.max(maxZ, z + pad);
|
|
2596
|
+
};
|
|
2597
|
+
const secondPlanAxis = opts.swapYZ ? 2 : 1;
|
|
2598
|
+
for (const node of Object.values(nodes)) {
|
|
2599
|
+
if (node.type === 'wall') {
|
|
2600
|
+
for (const point of [node.start, node.end])
|
|
2601
|
+
include(point[0], point[1], node.thickness ?? 0);
|
|
2602
|
+
}
|
|
2603
|
+
else if (node.type === 'slab' || node.type === 'zone' || node.type === 'ceiling') {
|
|
2604
|
+
for (const point of node.polygon)
|
|
2605
|
+
include(point[0], point[1]);
|
|
2606
|
+
}
|
|
2607
|
+
else if (node.type === 'column') {
|
|
2608
|
+
include(node.position[0], node.position[2], Math.max(node.width ?? 0, node.depth ?? 0));
|
|
2609
|
+
}
|
|
2610
|
+
else if (node.type === 'imported-mesh') {
|
|
2611
|
+
for (const primitive of node.primitives)
|
|
2612
|
+
for (let index = 0; index + 2 < primitive.positions.length; index += 3)
|
|
2613
|
+
include(primitive.positions[index], primitive.positions[index + secondPlanAxis]);
|
|
2614
|
+
}
|
|
2615
|
+
}
|
|
2616
|
+
if (!(minX <= maxX))
|
|
2617
|
+
return;
|
|
2618
|
+
const SITE_MARGIN = 5;
|
|
2619
|
+
const extent = [
|
|
2620
|
+
[minX - SITE_MARGIN, minZ - SITE_MARGIN],
|
|
2621
|
+
[maxX + SITE_MARGIN, minZ - SITE_MARGIN],
|
|
2622
|
+
[maxX + SITE_MARGIN, maxZ + SITE_MARGIN],
|
|
2623
|
+
[minX - SITE_MARGIN, maxZ + SITE_MARGIN],
|
|
2624
|
+
];
|
|
2625
|
+
for (const node of Object.values(nodes)) {
|
|
2626
|
+
if (node.type !== 'site')
|
|
2627
|
+
continue;
|
|
2628
|
+
let footprint = null;
|
|
2629
|
+
try {
|
|
2630
|
+
const site = ifcApi.GetLine(modelID, Number(meta(node).expressID));
|
|
2631
|
+
footprint = extrudedPlan(site, 0)?.polygon ?? null;
|
|
2632
|
+
}
|
|
2633
|
+
catch {
|
|
2634
|
+
footprint = null;
|
|
2635
|
+
}
|
|
2636
|
+
const holds = footprint !== null &&
|
|
2637
|
+
[
|
|
2638
|
+
[minX, minZ],
|
|
2639
|
+
[maxX, minZ],
|
|
2640
|
+
[maxX, maxZ],
|
|
2641
|
+
[minX, maxZ],
|
|
2642
|
+
].every((point) => containsPoint([{ outer: footprint, holes: [] }], point));
|
|
2643
|
+
node.polygon = { type: 'polygon', points: holds ? footprint : extent };
|
|
2644
|
+
}
|
|
2645
|
+
}
|
|
2236
2646
|
progress('Simplifying converted scene...', 94);
|
|
2237
|
-
const simplificationStats = simplifyConvertedSceneGraph(nodes, simplificationOptions
|
|
2647
|
+
const simplificationStats = simplifyConvertedSceneGraph(nodes, simplificationOptions, {
|
|
2648
|
+
wallConnections,
|
|
2649
|
+
wallLinings,
|
|
2650
|
+
});
|
|
2238
2651
|
if (simplificationStats.removedTinyWalls > 0 ||
|
|
2239
2652
|
simplificationStats.removedMergedWalls > 0 ||
|
|
2240
|
-
simplificationStats.removedDuplicateOpenings > 0
|
|
2653
|
+
simplificationStats.removedDuplicateOpenings > 0 ||
|
|
2654
|
+
simplificationStats.joinedWallEnds > 0) {
|
|
2241
2655
|
console.log('[IFC→Pascal] Simplification:', simplificationStats);
|
|
2242
2656
|
}
|
|
2657
|
+
const roomFirstStats = applyRoomFirstStructure(nodes, wallBodySides);
|
|
2658
|
+
console.log('[IFC→Pascal] Room-first structure:', roomFirstStats);
|
|
2659
|
+
fitSitePolygons();
|
|
2660
|
+
const collections = importCollections(ifcApi, modelID, nodes, expressIdToNodeId);
|
|
2243
2661
|
ifcApi.CloseModel(modelID);
|
|
2244
2662
|
progress('Building scene graph...', 95);
|
|
2245
2663
|
const totalNodes = Object.keys(nodes).length;
|
|
@@ -2264,5 +2682,6 @@ export async function convertIfcToPascal(ifcData, onProgress, options) {
|
|
|
2264
2682
|
return {
|
|
2265
2683
|
nodes,
|
|
2266
2684
|
rootNodeIds: rootNodeIds,
|
|
2685
|
+
collections,
|
|
2267
2686
|
};
|
|
2268
2687
|
}
|