@pascal-app/ifc-converter 1.0.6 → 1.1.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/README.md CHANGED
@@ -35,6 +35,7 @@ The importer writes intent for Pascal's room-first model; the scene load
35
35
  migrations (room adoption, floor plates) and the structure kernel turn it into
36
36
  rooms, floor plates and ceilings. `tests/room-first-load.test.ts` converts the
37
37
  reference files and runs that load chain.
38
+
38
39
  - **Walls.** Merged collinear fragments, then every straight wall end moves
39
40
  along its axis onto the centreline it meets (L and T joins; ends inside
40
41
  another wall's body reach its centreline, `IfcRelConnectsPathElements` widens
@@ -49,11 +49,17 @@ export declare class IfcModel {
49
49
  circleProfile(radius: number): StepRef;
50
50
  /** Vertical prism of `profile`, from local z = `bottom` upwards by `depth`. */
51
51
  extrusion(profile: StepRef, bottom: number, depth: number, color?: IfcColor): StepRef;
52
+ /**
53
+ * `solid` with everything above a plane cut away. The plane passes through `point` and rises
54
+ * `slope` per unit of local +X; its normal points up into the removed half-space, which an
55
+ * AgreementFlag of false places on the normal's side.
56
+ */
57
+ clipAboveSlope(solid: StepRef, point: Vec3, slope: number): StepRef;
52
58
  triangulatedFaceSet(set: TriangleSet): StepRef | null;
53
59
  private surfaceStyle;
54
60
  styleItem(item: StepRef, color: IfcColor): void;
55
61
  shape(representations: StepRef[]): StepRef;
56
- bodyRepresentation(type: 'SweptSolid' | 'Tessellation', items: StepRef[]): StepRef;
62
+ bodyRepresentation(type: 'SweptSolid' | 'Clipping' | 'Tessellation', items: StepRef[]): StepRef;
57
63
  axisRepresentation(curve: StepRef): StepRef;
58
64
  rel(type: string, seed: string, ...args: StepValue[]): StepRef;
59
65
  propertySet(seed: string, name: string, properties: Array<[string, StepValue]>, relatedObjects: StepRef[]): void;
@@ -1 +1 @@
1
- {"version":3,"file":"ifc-model.d.ts","sourceRoot":"","sources":["../../src/export/ifc-model.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"ifc-model.d.ts","sourceRoot":"","sources":["../../src/export/ifc-model.ts"],"names":[],"mappings":"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"}
@@ -102,6 +102,17 @@ export class IfcModel {
102
102
  this.styleItem(solid, color);
103
103
  return solid;
104
104
  }
105
+ /**
106
+ * `solid` with everything above a plane cut away. The plane passes through `point` and rises
107
+ * `slope` per unit of local +X; its normal points up into the removed half-space, which an
108
+ * AgreementFlag of false places on the normal's side.
109
+ */
110
+ clipAboveSlope(solid, point, slope) {
111
+ const length = Math.hypot(1, slope);
112
+ const plane = this.step.add('IFCPLANE', this.step.add('IFCAXIS2PLACEMENT3D', this.point3(point), this.direction([-slope / length, 0, 1 / length]), this.direction([1 / length, 0, slope / length])));
113
+ const halfSpace = this.step.add('IFCHALFSPACESOLID', plane, false);
114
+ return this.step.add('IFCBOOLEANCLIPPINGRESULT', enumValue('DIFFERENCE'), solid, halfSpace);
115
+ }
105
116
  triangulatedFaceSet(set) {
106
117
  const { positions, indices } = set;
107
118
  const vertexCount = Math.floor(positions.length / 3);
@@ -1 +1 @@
1
- {"version":3,"file":"scene-to-ifc.d.ts","sourceRoot":"","sources":["../../src/export/scene-to-ifc.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,KAAK,OAAO,EAIZ,KAAK,UAAU,EAoChB,MAAM,kBAAkB,CAAA;AAyBzB,0FAA0F;AAC1F,MAAM,WAAW,WAAW;IAC1B,SAAS,EAAE,SAAS,CAAC,MAAM,CAAC,CAAA;IAC5B,OAAO,CAAC,EAAE,SAAS,CAAC,MAAM,CAAC,CAAA;IAC3B,+BAA+B;IAC/B,KAAK,CAAC,EAAE,CAAC,MAAM,EAAE,MAAM,EAAE,MAAM,CAAC,CAAA;IAChC,OAAO,CAAC,EAAE,MAAM,CAAA;IAChB,iDAAiD;IACjD,IAAI,CAAC,EAAE,MAAM,CAAA;CACd;AAED,MAAM,WAAW,cAAc;IAC7B,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAA;IAC9B,WAAW,CAAC,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,UAAU,CAAC,CAAC,CAAA;IAClD,oGAAoG;IACpG,MAAM,CAAC,EAAE,WAAW,CAAC,MAAM,EAAE,WAAW,EAAE,CAAC,CAAA;IAC3C,WAAW,CAAC,EAAE,MAAM,CAAA;IACpB,MAAM,CAAC,EAAE,MAAM,CAAA;IACf,YAAY,CAAC,EAAE,MAAM,CAAA;IACrB,sDAAsD;IACtD,SAAS,CAAC,EAAE,IAAI,CAAA;IAChB,qFAAqF;IACrF,WAAW,CAAC,EAAE,OAAO,CAAA;IACrB,4DAA4D;IAC5D,iBAAiB,CAAC,EAAE,SAAS,MAAM,EAAE,CAAA;CACtC;AAED,MAAM,WAAW,aAAa;IAC5B,MAAM,EAAE,MAAM,CAAA;IACd,IAAI,EAAE,MAAM,CAAA;IACZ,MAAM,EAAE,aAAa,GAAG,YAAY,GAAG,UAAU,CAAA;CAClD;AAED,MAAM,WAAW,gBAAgB;IAC/B,2DAA2D;IAC3D,QAAQ,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAA;IAChC,OAAO,EAAE,aAAa,EAAE,CAAA;CACzB;AAED,MAAM,WAAW,eAAe;IAC9B,GAAG,EAAE,MAAM,CAAA;IACX,OAAO,EAAE,gBAAgB,CAAA;CAC1B;AAoMD,iFAAiF;AACjF,wBAAgB,cAAc,CAAC,KAAK,EAAE,cAAc,GAAG,eAAe,CAm1CrE;AAiCD,wDAAwD;AACxD,wBAAgB,gBAAgB,CAAC,KAAK,EAAE,cAAc,GAAG,MAAM,CAE9D"}
1
+ {"version":3,"file":"scene-to-ifc.d.ts","sourceRoot":"","sources":["../../src/export/scene-to-ifc.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,KAAK,OAAO,EAIZ,KAAK,UAAU,EAsChB,MAAM,kBAAkB,CAAA;AAyBzB,0FAA0F;AAC1F,MAAM,WAAW,WAAW;IAC1B,SAAS,EAAE,SAAS,CAAC,MAAM,CAAC,CAAA;IAC5B,OAAO,CAAC,EAAE,SAAS,CAAC,MAAM,CAAC,CAAA;IAC3B,+BAA+B;IAC/B,KAAK,CAAC,EAAE,CAAC,MAAM,EAAE,MAAM,EAAE,MAAM,CAAC,CAAA;IAChC,OAAO,CAAC,EAAE,MAAM,CAAA;IAChB,iDAAiD;IACjD,IAAI,CAAC,EAAE,MAAM,CAAA;CACd;AAED,MAAM,WAAW,cAAc;IAC7B,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAA;IAC9B,WAAW,CAAC,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,UAAU,CAAC,CAAC,CAAA;IAClD,oGAAoG;IACpG,MAAM,CAAC,EAAE,WAAW,CAAC,MAAM,EAAE,WAAW,EAAE,CAAC,CAAA;IAC3C,WAAW,CAAC,EAAE,MAAM,CAAA;IACpB,MAAM,CAAC,EAAE,MAAM,CAAA;IACf,YAAY,CAAC,EAAE,MAAM,CAAA;IACrB,sDAAsD;IACtD,SAAS,CAAC,EAAE,IAAI,CAAA;IAChB,qFAAqF;IACrF,WAAW,CAAC,EAAE,OAAO,CAAA;IACrB,4DAA4D;IAC5D,iBAAiB,CAAC,EAAE,SAAS,MAAM,EAAE,CAAA;CACtC;AAED,MAAM,WAAW,aAAa;IAC5B,MAAM,EAAE,MAAM,CAAA;IACd,IAAI,EAAE,MAAM,CAAA;IACZ,MAAM,EAAE,aAAa,GAAG,YAAY,GAAG,UAAU,CAAA;CAClD;AAED,MAAM,WAAW,gBAAgB;IAC/B,2DAA2D;IAC3D,QAAQ,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAA;IAChC,OAAO,EAAE,aAAa,EAAE,CAAA;CACzB;AAED,MAAM,WAAW,eAAe;IAC9B,GAAG,EAAE,MAAM,CAAA;IACX,OAAO,EAAE,gBAAgB,CAAA;CAC1B;AAoMD,iFAAiF;AACjF,wBAAgB,cAAc,CAAC,KAAK,EAAE,cAAc,GAAG,eAAe,CAy2CrE;AAiCD,wDAAwD;AACxD,wBAAgB,gBAAgB,CAAC,KAAK,EAAE,cAAc,GAAG,MAAM,CAE9D"}
@@ -1,4 +1,4 @@
1
- import { calculateLevelMiters, DEFAULT_WALL_THICKNESS, difference, getLevelElevations, getOpeningWallCut, getRenderableSlabPolygon, getWallAssemblyPreset, getWallBodyCenterOffset, getWallCurveFrameAt, getWallCurveLength, getWallFaceOffsets, getWallPlanFootprint, getWallTop, hidesDescendants, intersection, isCurvedWall, liftedManualSlab, measureStair, prepareSlabPolygonContext, resolveCeilingHeight, resolveWallExteriorSide, sampleWallCenterline, union, wallSupportForNodes, } from '@pascal-app/core';
1
+ import { calculateLevelMiters, DEFAULT_WALL_THICKNESS, difference, getDoorOpeningSize, getLevelElevations, getOpeningWallCut, getRenderableSlabPolygon, getWallAssemblyPreset, getWallBodyCenterOffset, getWallCurveFrameAt, getWallCurveLength, getWallFaceOffsets, getWallPlanFootprint, getWallTop, hidesDescendants, intersection, isCurvedWall, liftedManualSlab, measureStair, openingHostFacing, prepareSlabPolygonContext, resolveCeilingHeight, resolveWallExteriorSide, sampleWallCenterline, union, wallSupportForNodes, } from '@pascal-app/core';
2
2
  import { doorGlazingFraction, doorOperationForIfc } from './door-operation';
3
3
  import { bool, cleanRing, DERIVED, enumValue, frameToLocal, IDENTITY_FRAME, IfcModel, identifier, int, label, logical, ratio, relativeFrame, } from './ifc-model';
4
4
  import { typed } from './step';
@@ -577,7 +577,9 @@ export function buildIfcExport(input) {
577
577
  const chord = Math.hypot(endPlan[0] - startPlan[0], endPlan[1] - startPlan[1]);
578
578
  const support = wallSupportForNodes(wall, nodes);
579
579
  const base = support.elevation;
580
- const top = getWallTop(wall, levelId, nodes, base);
580
+ const startTop = getWallTop(wall, levelId, nodes, base, 0);
581
+ const endTop = getWallTop(wall, levelId, nodes, base, 1);
582
+ const top = Math.max(startTop, endTop);
581
583
  const cells = wallBaseCells(wall, support, top);
582
584
  if (chord < 1e-6 || cells.every((cell) => top - cell.bottom <= 1e-4)) {
583
585
  skipped.push({ nodeId: wall.id, type: wall.type, reason: 'degenerate' });
@@ -612,7 +614,7 @@ export function buildIfcExport(input) {
612
614
  }
613
615
  // A stepped base (runs on different supports, or faces on different
614
616
  // floors) becomes one prism per run, each clipped from the footprint.
615
- const solids = cells.length === 1
617
+ const prisms = cells.length === 1
616
618
  ? [
617
619
  model.extrusion(model.polygonProfile(footprint), cells[0].bottom - base, top - cells[0].bottom),
618
620
  ]
@@ -626,9 +628,15 @@ export function buildIfcExport(input) {
626
628
  ]
627
629
  : [];
628
630
  }));
631
+ // A sloped top is linear along the chord, the wall frame's +X: each prism rises to the
632
+ // higher end and the plane through both end tops cuts it down.
633
+ const sloped = Math.abs(endTop - startTop) > 1e-6;
634
+ const solids = sloped
635
+ ? prisms.map((prism) => model.clipAboveSlope(prism, [0, 0, startTop - base], (endTop - startTop) / chord))
636
+ : prisms;
629
637
  const shape = model.shape([
630
638
  model.axisRepresentation(model.polyline2(axisPoints, false)),
631
- model.bodyRepresentation('SweptSolid', solids),
639
+ model.bodyRepresentation(sloped ? 'Clipping' : 'SweptSolid', solids),
632
640
  ]);
633
641
  const wallPlacement = model.localPlacement(storey.placement, frame);
634
642
  const wallRef = emitElement({ entity: 'IFCWALL', tail: (tag) => [tag, enumValue('STANDARD')] }, wall.id, wall, nodeName(wall, 'Wall'), wallPlacement, shape);
@@ -682,10 +690,14 @@ export function buildIfcExport(input) {
682
690
  angle: Math.atan2(-curve.tangent.y, curve.tangent.x),
683
691
  };
684
692
  const cutBottom = (cut?.bottom ?? nominalBottom) - nominalBottom;
685
- const cutDepth = (cut?.top ?? nominalBottom + opening.height) - (cut?.bottom ?? nominalBottom);
693
+ const openingSize = getDoorOpeningSize(opening);
694
+ const cutDepth = (cut?.top ?? nominalBottom + openingSize.height) - (cut?.bottom ?? nominalBottom);
695
+ const cutOffset = isCurvedWall(wall)
696
+ ? 0
697
+ : openingSize.leafOffsetX * openingHostFacing(opening);
686
698
  let openingRef = null;
687
699
  let openingPlacement = null;
688
- if (cutDepth > 1e-4 && opening.width > 1e-4) {
700
+ if (cutDepth > 1e-4 && openingSize.width > 1e-4) {
689
701
  let profile;
690
702
  const band = isCurvedWall(wall) && cut
691
703
  ? cleanRing(cut.band.map(([x, z]) => {
@@ -697,8 +709,16 @@ export function buildIfcExport(input) {
697
709
  if (band.length > 0)
698
710
  profile = model.polygonProfile(band);
699
711
  else
700
- profile = model.rectangleProfile(opening.width, thickness + 0.2);
701
- openingPlacement = model.localPlacement(wallPlacement, relativeFrame(frame, openingFrame));
712
+ profile = model.rectangleProfile(openingSize.width, thickness + 0.2);
713
+ const cutFrame = {
714
+ ...openingFrame,
715
+ origin: [
716
+ openingFrame.origin[0] - cutOffset * curve.tangent.x,
717
+ openingFrame.origin[1] + cutOffset * curve.tangent.y,
718
+ nominalBottom,
719
+ ],
720
+ };
721
+ openingPlacement = model.localPlacement(wallPlacement, relativeFrame(frame, cutFrame));
702
722
  openingRef = step.add('IFCOPENINGELEMENT', model.guid(`${opening.id}:opening`), ownerHistory, `${nodeName(opening, opening.type === 'door' ? 'Door' : 'Window')} opening`, null, null, openingPlacement, model.shape([
703
723
  model.bodyRepresentation('SweptSolid', [
704
724
  model.extrusion(profile, cutBottom, cutDepth),
@@ -720,7 +740,7 @@ export function buildIfcExport(input) {
720
740
  ]);
721
741
  }
722
742
  const elementPlacement = openingPlacement
723
- ? model.localPlacement(openingPlacement, IDENTITY_FRAME)
743
+ ? model.localPlacement(openingPlacement, { origin: [cutOffset, 0, 0], angle: 0 })
724
744
  : model.localPlacement(storey.placement, openingFrame);
725
745
  const fill = emitElement(meshClassFor(opening), opening.id, opening, nodeName(opening, opening.type === 'door' ? 'Door' : 'Window'), elementPlacement, body);
726
746
  storey.contained.push(fill);
@@ -0,0 +1,45 @@
1
+ /**
2
+ * What a conversion could not read and left out or replaced, instead of failing the whole file
3
+ * on one bad number (a NaN vertex used to reject every imported mesh with
4
+ * "Could not build imported mesh node at primitives.0.positions.0").
5
+ */
6
+ export interface IfcConversionReport {
7
+ skippedGeometry: {
8
+ /** Placed geometries whose placement matrix had a non-finite entry. */
9
+ transforms: number;
10
+ /** Vertices with a non-finite position. */
11
+ vertices: number;
12
+ /** Triangles that used a skipped or missing vertex. */
13
+ triangles: number;
14
+ /** Mesh pieces left with no valid triangle. */
15
+ primitives: number;
16
+ /** Elements left out because none of their geometry could be read. */
17
+ elements: number;
18
+ };
19
+ /** Values that were not finite and fell back to a neutral default. */
20
+ replaced: {
21
+ unitFactor: boolean;
22
+ originOffset: boolean;
23
+ levelElevations: number;
24
+ /** Vertex normals recomputed from the faces around them: shading only, nothing lost. */
25
+ normals: number;
26
+ };
27
+ }
28
+ export declare function emptyConversionReport(): IfcConversionReport;
29
+ /** True when the report has anything to tell the person (a recomputed normal loses nothing). */
30
+ export declare function conversionReportHasIssues(report: IfcConversionReport): boolean;
31
+ export declare function isFiniteMatrix(matrix: ArrayLike<number> | null | undefined): boolean;
32
+ export interface MeshBuffers {
33
+ positions: number[];
34
+ normals: number[];
35
+ indices: number[];
36
+ }
37
+ /**
38
+ * Drops vertices with a non-finite position and every triangle that uses one (or an index past
39
+ * the vertex count), then compacts the vertex buffers to the vertices still used. A non-finite
40
+ * normal on a kept vertex is shading, not shape: it is recomputed from the kept triangles around
41
+ * it (zero when they have no area). Returns null when no triangle survives; a source with
42
+ * triangles that all fail counts as a skipped primitive.
43
+ */
44
+ export declare function keepValidTriangles(mesh: MeshBuffers, report: IfcConversionReport): MeshBuffers | null;
45
+ //# sourceMappingURL=geometry-validity.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"geometry-validity.d.ts","sourceRoot":"","sources":["../src/geometry-validity.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AACH,MAAM,WAAW,mBAAmB;IAClC,eAAe,EAAE;QACf,uEAAuE;QACvE,UAAU,EAAE,MAAM,CAAA;QAClB,2CAA2C;QAC3C,QAAQ,EAAE,MAAM,CAAA;QAChB,uDAAuD;QACvD,SAAS,EAAE,MAAM,CAAA;QACjB,+CAA+C;QAC/C,UAAU,EAAE,MAAM,CAAA;QAClB,sEAAsE;QACtE,QAAQ,EAAE,MAAM,CAAA;KACjB,CAAA;IACD,sEAAsE;IACtE,QAAQ,EAAE;QACR,UAAU,EAAE,OAAO,CAAA;QACnB,YAAY,EAAE,OAAO,CAAA;QACrB,eAAe,EAAE,MAAM,CAAA;QACvB,wFAAwF;QACxF,OAAO,EAAE,MAAM,CAAA;KAChB,CAAA;CACF;AAED,wBAAgB,qBAAqB,IAAI,mBAAmB,CAK3D;AAED,gGAAgG;AAChG,wBAAgB,yBAAyB,CAAC,MAAM,EAAE,mBAAmB,GAAG,OAAO,CAa9E;AAED,wBAAgB,cAAc,CAAC,MAAM,EAAE,SAAS,CAAC,MAAM,CAAC,GAAG,IAAI,GAAG,SAAS,GAAG,OAAO,CAIpF;AAED,MAAM,WAAW,WAAW;IAC1B,SAAS,EAAE,MAAM,EAAE,CAAA;IACnB,OAAO,EAAE,MAAM,EAAE,CAAA;IACjB,OAAO,EAAE,MAAM,EAAE,CAAA;CAClB;AAED;;;;;;GAMG;AACH,wBAAgB,kBAAkB,CAChC,IAAI,EAAE,WAAW,EACjB,MAAM,EAAE,mBAAmB,GAC1B,WAAW,GAAG,IAAI,CAqDpB"}
@@ -0,0 +1,138 @@
1
+ export function emptyConversionReport() {
2
+ return {
3
+ skippedGeometry: { transforms: 0, vertices: 0, triangles: 0, primitives: 0, elements: 0 },
4
+ replaced: { unitFactor: false, originOffset: false, levelElevations: 0, normals: 0 },
5
+ };
6
+ }
7
+ /** True when the report has anything to tell the person (a recomputed normal loses nothing). */
8
+ export function conversionReportHasIssues(report) {
9
+ const { skippedGeometry, replaced } = report;
10
+ return (skippedGeometry.transforms +
11
+ skippedGeometry.vertices +
12
+ skippedGeometry.triangles +
13
+ skippedGeometry.primitives +
14
+ skippedGeometry.elements +
15
+ replaced.levelElevations >
16
+ 0 ||
17
+ replaced.unitFactor ||
18
+ replaced.originOffset);
19
+ }
20
+ export function isFiniteMatrix(matrix) {
21
+ if (!matrix || matrix.length < 16)
22
+ return false;
23
+ for (let i = 0; i < 16; i++)
24
+ if (!Number.isFinite(matrix[i]))
25
+ return false;
26
+ return true;
27
+ }
28
+ /**
29
+ * Drops vertices with a non-finite position and every triangle that uses one (or an index past
30
+ * the vertex count), then compacts the vertex buffers to the vertices still used. A non-finite
31
+ * normal on a kept vertex is shading, not shape: it is recomputed from the kept triangles around
32
+ * it (zero when they have no area). Returns null when no triangle survives; a source with
33
+ * triangles that all fail counts as a skipped primitive.
34
+ */
35
+ export function keepValidTriangles(mesh, report) {
36
+ const skipped = report.skippedGeometry;
37
+ const triangleCount = Math.floor(mesh.indices.length / 3);
38
+ if (triangleCount === 0)
39
+ return null;
40
+ const vertexCount = Math.floor(mesh.positions.length / 3);
41
+ const valid = new Uint8Array(vertexCount);
42
+ for (let vertex = 0; vertex < vertexCount; vertex++) {
43
+ if (finiteTriple(mesh.positions, vertex))
44
+ valid[vertex] = 1;
45
+ else
46
+ skipped.vertices++;
47
+ }
48
+ const remap = new Int32Array(vertexCount).fill(-1);
49
+ const positions = [];
50
+ const normals = [];
51
+ const indices = [];
52
+ for (let triangle = 0; triangle < triangleCount; triangle++) {
53
+ const corners = [
54
+ mesh.indices[triangle * 3],
55
+ mesh.indices[triangle * 3 + 1],
56
+ mesh.indices[triangle * 3 + 2],
57
+ ];
58
+ if (!corners.every((index) => Number.isInteger(index) && index >= 0 && index < vertexCount && valid[index])) {
59
+ skipped.triangles++;
60
+ continue;
61
+ }
62
+ for (const index of corners) {
63
+ if (remap[index] === -1) {
64
+ remap[index] = positions.length / 3;
65
+ positions.push(mesh.positions[index * 3], mesh.positions[index * 3 + 1], mesh.positions[index * 3 + 2]);
66
+ normals.push(mesh.normals[index * 3], mesh.normals[index * 3 + 1], mesh.normals[index * 3 + 2]);
67
+ }
68
+ indices.push(remap[index]);
69
+ }
70
+ }
71
+ if (indices.length === 0) {
72
+ skipped.primitives++;
73
+ return null;
74
+ }
75
+ report.replaced.normals += recomputeNonFiniteNormals(positions, normals, indices);
76
+ return { positions, normals, indices };
77
+ }
78
+ function finiteTriple(values, vertex) {
79
+ return (Number.isFinite(values[vertex * 3]) &&
80
+ Number.isFinite(values[vertex * 3 + 1]) &&
81
+ Number.isFinite(values[vertex * 3 + 2]));
82
+ }
83
+ // A vertex whose faces have no area has no direction to take; a unit normal keeps lighting sane.
84
+ const FALLBACK_NORMAL = [0, 0, 1];
85
+ /**
86
+ * Each non-finite normal becomes the area-weighted normal of the triangles at its vertex, or the
87
+ * fallback when they have no area. Returns how many were recomputed. Accumulates in typed arrays:
88
+ * a whole mesh can need it (__bench__/normal-recompute.bench.ts).
89
+ */
90
+ function recomputeNonFiniteNormals(positions, normals, indices) {
91
+ const vertexCount = positions.length / 3;
92
+ const slotOf = new Int32Array(vertexCount).fill(-1);
93
+ let broken = 0;
94
+ for (let vertex = 0; vertex < vertexCount; vertex++)
95
+ if (!finiteTriple(normals, vertex))
96
+ slotOf[vertex] = broken++;
97
+ if (broken === 0)
98
+ return 0;
99
+ const sums = new Float64Array(broken * 3);
100
+ for (let t = 0; t + 2 < indices.length; t += 3) {
101
+ const a = indices[t];
102
+ const b = indices[t + 1];
103
+ const c = indices[t + 2];
104
+ if (slotOf[a] < 0 && slotOf[b] < 0 && slotOf[c] < 0)
105
+ continue;
106
+ const ux = positions[b * 3] - positions[a * 3];
107
+ const uy = positions[b * 3 + 1] - positions[a * 3 + 1];
108
+ const uz = positions[b * 3 + 2] - positions[a * 3 + 2];
109
+ const vx = positions[c * 3] - positions[a * 3];
110
+ const vy = positions[c * 3 + 1] - positions[a * 3 + 1];
111
+ const vz = positions[c * 3 + 2] - positions[a * 3 + 2];
112
+ const fx = uy * vz - uz * vy;
113
+ const fy = uz * vx - ux * vz;
114
+ const fz = ux * vy - uy * vx;
115
+ for (const corner of [a, b, c]) {
116
+ const slot = slotOf[corner];
117
+ if (slot < 0)
118
+ continue;
119
+ sums[slot * 3] += fx;
120
+ sums[slot * 3 + 1] += fy;
121
+ sums[slot * 3 + 2] += fz;
122
+ }
123
+ }
124
+ for (let vertex = 0; vertex < vertexCount; vertex++) {
125
+ const slot = slotOf[vertex];
126
+ if (slot < 0)
127
+ continue;
128
+ const x = sums[slot * 3];
129
+ const y = sums[slot * 3 + 1];
130
+ const z = sums[slot * 3 + 2];
131
+ // Coordinates past ~1e154 overflow the summed cross products: Infinity, not a direction.
132
+ const length = Math.hypot(x, y, z);
133
+ const normal = length > 0 && Number.isFinite(length) ? [x / length, y / length, z / length] : FALLBACK_NORMAL;
134
+ for (let axis = 0; axis < 3; axis++)
135
+ normals[vertex * 3 + axis] = Math.round(normal[axis] * 1000) / 1000 || 0;
136
+ }
137
+ return broken;
138
+ }
package/dist/index.d.ts CHANGED
@@ -1,6 +1,8 @@
1
1
  import { type AnyNode, type AnyNodeId, type Collection } from '@pascal-app/core';
2
2
  import { type IfcConversionSimplificationOptions } from './cleanup';
3
+ import { type IfcConversionReport } from './geometry-validity';
3
4
  export type { IfcConversionSimplificationOptions, IfcConversionSimplificationStats, } from './cleanup';
5
+ export { conversionReportHasIssues, type IfcConversionReport } from './geometry-validity';
4
6
  export type PascalNode = AnyNode;
5
7
  export interface PascalSceneGraph {
6
8
  nodes: Record<AnyNodeId, AnyNode>;
@@ -12,9 +14,20 @@ export interface ConversionOptions {
12
14
  extrusionDepthIsHeight?: boolean;
13
15
  swapProfileDimensions?: boolean;
14
16
  wasmPath?: string;
17
+ /**
18
+ * The URL to load one of web-ifc's wasm files from (`web-ifc.wasm`, or `web-ifc-mt.wasm` when
19
+ * the page is cross-origin isolated). Wins over `wasmPath`; lets a host serve a versioned or
20
+ * cache-busted URL.
21
+ */
22
+ locateWasmFile?: (fileName: string) => string;
15
23
  simplify?: boolean | IfcConversionSimplificationOptions;
16
24
  label?: string;
17
25
  }
18
26
  export declare const VARIANT_PRESETS: Record<string, ConversionOptions>;
19
27
  export declare function convertIfcToPascal(ifcData: Uint8Array, onProgress?: (message: string, percent: number) => void, options?: ConversionOptions): Promise<PascalSceneGraph>;
28
+ /** `convertIfcToPascal`, plus what the conversion had to skip or replace to finish. */
29
+ export declare function convertIfcToPascalWithReport(ifcData: Uint8Array, onProgress?: (message: string, percent: number) => void, options?: ConversionOptions): Promise<{
30
+ scene: PascalSceneGraph;
31
+ report: IfcConversionReport;
32
+ }>;
20
33
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,KAAK,OAAO,EACZ,KAAK,SAAS,EAId,KAAK,UAAU,EAmBhB,MAAM,kBAAkB,CAAA;AAGzB,OAAO,EAAE,KAAK,kCAAkC,EAA+B,MAAM,WAAW,CAAA;AAQhG,YAAY,EACV,kCAAkC,EAClC,gCAAgC,GACjC,MAAM,WAAW,CAAA;AAElB,MAAM,MAAM,UAAU,GAAG,OAAO,CAAA;AAEhC,MAAM,WAAW,gBAAgB;IAC/B,KAAK,EAAE,MAAM,CAAC,SAAS,EAAE,OAAO,CAAC,CAAA;IACjC,WAAW,EAAE,SAAS,EAAE,CAAA;IACxB,WAAW,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,UAAU,CAAC,CAAA;CACzC;AAiyBD,MAAM,WAAW,iBAAiB;IAChC,MAAM,CAAC,EAAE,OAAO,CAAA;IAChB,sBAAsB,CAAC,EAAE,OAAO,CAAA;IAChC,qBAAqB,CAAC,EAAE,OAAO,CAAA;IAC/B,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB,QAAQ,CAAC,EAAE,OAAO,GAAG,kCAAkC,CAAA;IACvD,KAAK,CAAC,EAAE,MAAM,CAAA;CACf;AAED,eAAO,MAAM,eAAe,EAAE,MAAM,CAAC,MAAM,EAAE,iBAAiB,CAa7D,CAAA;AAED,wBAAsB,kBAAkB,CACtC,OAAO,EAAE,UAAU,EACnB,UAAU,CAAC,EAAE,CAAC,OAAO,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,KAAK,IAAI,EACvD,OAAO,CAAC,EAAE,iBAAiB,GAC1B,OAAO,CAAC,gBAAgB,CAAC,CA8/D3B"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,KAAK,OAAO,EACZ,KAAK,SAAS,EAId,KAAK,UAAU,EAmBhB,MAAM,kBAAkB,CAAA;AAIzB,OAAO,EAAE,KAAK,kCAAkC,EAA+B,MAAM,WAAW,CAAA;AAGhG,OAAO,EAEL,KAAK,mBAAmB,EAGzB,MAAM,qBAAqB,CAAA;AAM5B,YAAY,EACV,kCAAkC,EAClC,gCAAgC,GACjC,MAAM,WAAW,CAAA;AAClB,OAAO,EAAE,yBAAyB,EAAE,KAAK,mBAAmB,EAAE,MAAM,qBAAqB,CAAA;AAEzF,MAAM,MAAM,UAAU,GAAG,OAAO,CAAA;AAEhC,MAAM,WAAW,gBAAgB;IAC/B,KAAK,EAAE,MAAM,CAAC,SAAS,EAAE,OAAO,CAAC,CAAA;IACjC,WAAW,EAAE,SAAS,EAAE,CAAA;IACxB,WAAW,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,UAAU,CAAC,CAAA;CACzC;AAuyBD,MAAM,WAAW,iBAAiB;IAChC,MAAM,CAAC,EAAE,OAAO,CAAA;IAChB,sBAAsB,CAAC,EAAE,OAAO,CAAA;IAChC,qBAAqB,CAAC,EAAE,OAAO,CAAA;IAC/B,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB;;;;OAIG;IACH,cAAc,CAAC,EAAE,CAAC,QAAQ,EAAE,MAAM,KAAK,MAAM,CAAA;IAC7C,QAAQ,CAAC,EAAE,OAAO,GAAG,kCAAkC,CAAA;IACvD,KAAK,CAAC,EAAE,MAAM,CAAA;CACf;AAED,eAAO,MAAM,eAAe,EAAE,MAAM,CAAC,MAAM,EAAE,iBAAiB,CAa7D,CAAA;AAED,wBAAsB,kBAAkB,CACtC,OAAO,EAAE,UAAU,EACnB,UAAU,CAAC,EAAE,CAAC,OAAO,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,KAAK,IAAI,EACvD,OAAO,CAAC,EAAE,iBAAiB,GAC1B,OAAO,CAAC,gBAAgB,CAAC,CAE3B;AAED,uFAAuF;AACvF,wBAAsB,4BAA4B,CAChD,OAAO,EAAE,UAAU,EACnB,UAAU,CAAC,EAAE,CAAC,OAAO,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,KAAK,IAAI,EACvD,OAAO,CAAC,EAAE,iBAAiB,GAC1B,OAAO,CAAC;IAAE,KAAK,EAAE,gBAAgB,CAAC;IAAC,MAAM,EAAE,mBAAmB,CAAA;CAAE,CAAC,CAshEnE"}
package/dist/index.js CHANGED
@@ -1,13 +1,16 @@
1
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';
2
+ import { NEW_DOOR_FRAME_THICKNESS } from '@pascal-app/core/building';
2
3
  import * as WebIFC from 'web-ifc';
3
4
  import { extractBeamGeometry } from './beam-geometry';
4
5
  import { simplifyConvertedSceneGraph } from './cleanup';
5
6
  import { importCollections } from './collections';
6
7
  import { doorGlazingStyle, doorStyleFromIfcOperation } from './door-semantics';
8
+ import { emptyConversionReport, isFiniteMatrix, keepValidTriangles, } from './geometry-validity';
7
9
  import { nextId, seedIds } from './ids';
8
10
  import { applyRoomFirstStructure } from './room-first';
9
11
  import { selectStoreyForElevation } from './storey-semantics';
10
12
  import { redundantWallIds } from './wall-joins';
13
+ export { conversionReportHasIssues } from './geometry-validity';
11
14
  function meta(node) {
12
15
  return (node?.metadata ?? {});
13
16
  }
@@ -534,7 +537,7 @@ function roundMeshNormal(value) {
534
537
  const rounded = Math.round(value * 1000) / 1000;
535
538
  return rounded === 0 ? 0 : rounded;
536
539
  }
537
- function extractImportedMeshPrimitives(ifcApi, modelID, expressID, unitFactor, originOffset, levelElevation, swapYZ) {
540
+ function extractImportedMeshPrimitives(ifcApi, modelID, expressID, unitFactor, originOffset, levelElevation, swapYZ, report) {
538
541
  let flatMesh;
539
542
  try {
540
543
  flatMesh = ifcApi.GetFlatMesh(modelID, expressID);
@@ -547,6 +550,10 @@ function extractImportedMeshPrimitives(ifcApi, modelID, expressID, unitFactor, o
547
550
  for (let geometryIndex = 0; geometryIndex < flatMesh.geometries.size(); geometryIndex++) {
548
551
  const placed = flatMesh.geometries.get(geometryIndex);
549
552
  const matrix = placed.flatTransformation;
553
+ if (!isFiniteMatrix(matrix)) {
554
+ report.skippedGeometry.transforms++;
555
+ continue;
556
+ }
550
557
  const geometry = ifcApi.GetGeometry(modelID, placed.geometryExpressID);
551
558
  try {
552
559
  const vertices = ifcApi.GetVertexArray(geometry.GetVertexData(), geometry.GetVertexDataSize());
@@ -594,12 +601,10 @@ function extractImportedMeshPrimitives(ifcApi, modelID, expressID, unitFactor, o
594
601
  const normalLength = Math.hypot(...mappedNormal) || 1;
595
602
  normals.push(roundMeshNormal(mappedNormal[0] / normalLength), roundMeshNormal(mappedNormal[1] / normalLength), roundMeshNormal(mappedNormal[2] / normalLength));
596
603
  }
597
- const indices = Array.from(sourceIndices);
598
- if (positions.length >= 9 && indices.length >= 3) {
604
+ const mesh = keepValidTriangles({ positions, normals, indices: Array.from(sourceIndices) }, report);
605
+ if (mesh) {
599
606
  primitives.push({
600
- positions,
601
- normals,
602
- indices,
607
+ ...mesh,
603
608
  color: sourceColor(placed.color),
604
609
  opacity: Math.max(0, Math.min(1, placed.color?.w ?? 1)),
605
610
  });
@@ -704,6 +709,11 @@ export const VARIANT_PRESETS = {
704
709
  },
705
710
  };
706
711
  export async function convertIfcToPascal(ifcData, onProgress, options) {
712
+ return (await convertIfcToPascalWithReport(ifcData, onProgress, options)).scene;
713
+ }
714
+ /** `convertIfcToPascal`, plus what the conversion had to skip or replace to finish. */
715
+ export async function convertIfcToPascalWithReport(ifcData, onProgress, options) {
716
+ const report = emptyConversionReport();
707
717
  const opts = {
708
718
  swapYZ: options?.swapYZ ?? true,
709
719
  extrusionDepthIsHeight: options?.extrusionDepthIsHeight ?? true,
@@ -722,7 +732,8 @@ export async function convertIfcToPascal(ifcData, onProgress, options) {
722
732
  progress('Initializing IFC parser...', 0);
723
733
  const ifcApi = new WebIFC.IfcAPI();
724
734
  ifcApi.SetWasmPath(opts.wasmPath, true);
725
- await ifcApi.Init();
735
+ const locateWasmFile = options?.locateWasmFile;
736
+ await ifcApi.Init(locateWasmFile ? (fileName) => locateWasmFile(fileName) : undefined);
726
737
  progress('Opening IFC model...', 10);
727
738
  const modelID = ifcApi.OpenModel(ifcData);
728
739
  seedIds(ifcData);
@@ -744,7 +755,9 @@ export async function convertIfcToPascal(ifcData, onProgress, options) {
744
755
  const expressIdToNodeId = new Map();
745
756
  progress('Analyzing spatial relationships...', 20);
746
757
  // Detect length unit → meters conversion factor
747
- const unitFactor = getLengthUnitFactor(ifcApi, modelID);
758
+ const detectedUnitFactor = getLengthUnitFactor(ifcApi, modelID);
759
+ report.replaced.unitFactor = !(Number.isFinite(detectedUnitFactor) && detectedUnitFactor > 0);
760
+ const unitFactor = report.replaced.unitFactor ? 1 : detectedUnitFactor;
748
761
  // Compute scene origin offset to center georeferenced models near (0,0,0)
749
762
  let originOffset = [0, 0, 0];
750
763
  try {
@@ -754,7 +767,11 @@ export async function convertIfcToPascal(ifcData, onProgress, options) {
754
767
  const anchor = ifcApi.GetLine(modelID, anchorId);
755
768
  if (anchor.ObjectPlacement?.value) {
756
769
  const mat = resolveWorldTransform(ifcApi, modelID, anchor.ObjectPlacement.value);
757
- originOffset = transformPoint3(mat, [0, 0, 0]);
770
+ const offset = transformPoint3(mat, [0, 0, 0]);
771
+ if (offset.every(Number.isFinite))
772
+ originOffset = offset;
773
+ else
774
+ report.replaced.originOffset = true;
758
775
  }
759
776
  }
760
777
  }
@@ -949,11 +966,21 @@ export async function convertIfcToPascal(ifcData, onProgress, options) {
949
966
  return storeyExpressId == null ? 0 : (storeyElevationByExpressId.get(storeyExpressId) ?? 0);
950
967
  }
951
968
  const importedPrimitivesByExpressId = new Map();
969
+ // Elements whose every piece of geometry was unreadable: omitted, never rebuilt as a native
970
+ // node from their placement data.
971
+ const unreadableGeometry = new Set();
952
972
  function importedMeshPrimitivesFor(expressId) {
953
973
  const cached = importedPrimitivesByExpressId.get(expressId);
954
974
  if (cached)
955
975
  return cached;
956
- const primitives = extractImportedMeshPrimitives(ifcApi, modelID, expressId, unitFactor, originOffset, elementLevelElevation(expressId), opts.swapYZ);
976
+ const { transforms, primitives: skippedPrimitives } = report.skippedGeometry;
977
+ const skippedBefore = transforms + skippedPrimitives;
978
+ const primitives = extractImportedMeshPrimitives(ifcApi, modelID, expressId, unitFactor, originOffset, elementLevelElevation(expressId), opts.swapYZ, report);
979
+ const skippedAfter = report.skippedGeometry.transforms + report.skippedGeometry.primitives;
980
+ if (primitives.length === 0 && skippedAfter > skippedBefore) {
981
+ unreadableGeometry.add(expressId);
982
+ report.skippedGeometry.elements++;
983
+ }
957
984
  importedPrimitivesByExpressId.set(expressId, primitives);
958
985
  return primitives;
959
986
  }
@@ -1129,6 +1156,11 @@ export async function convertIfcToPascal(ifcData, onProgress, options) {
1129
1156
  else {
1130
1157
  elevation *= unitFactor;
1131
1158
  }
1159
+ if (!Number.isFinite(elevation)) {
1160
+ report.replaced.levelElevations++;
1161
+ const explicit = (storey.Elevation?.value ?? 0) * unitFactor;
1162
+ elevation = Number.isFinite(explicit) ? explicit : 0;
1163
+ }
1132
1164
  storeyElevationByExpressId.set(storeyExpressID, elevation);
1133
1165
  const levelNode = tryParse(LevelNode, 'level', {
1134
1166
  object: 'node',
@@ -1575,6 +1607,7 @@ export async function convertIfcToPascal(ifcData, onProgress, options) {
1575
1607
  object: 'node',
1576
1608
  id: nodeId,
1577
1609
  type: 'door',
1610
+ frameThickness: NEW_DOOR_FRAME_THICKNESS,
1578
1611
  name: element.Name?.value || `Door`,
1579
1612
  parentId: wallNodeId,
1580
1613
  visible: true,
@@ -1756,6 +1789,7 @@ export async function convertIfcToPascal(ifcData, onProgress, options) {
1756
1789
  object: 'node',
1757
1790
  id: nodeId,
1758
1791
  type: 'door',
1792
+ frameThickness: NEW_DOOR_FRAME_THICKNESS,
1759
1793
  name: element.Name?.value || `Door`,
1760
1794
  parentId: parentNodeId,
1761
1795
  visible: true,
@@ -1824,11 +1858,12 @@ export async function convertIfcToPascal(ifcData, onProgress, options) {
1824
1858
  // Pascal's storey-wide wall-support calculation. Preserve roofs and stair
1825
1859
  // landings as exact meshes: roofs may be sloped, while a local landing is
1826
1860
  // not a storey floor and must not raise adjacent wall bases.
1861
+ const hasMesh = importedMeshPrimitivesFor(slabExpressID).length > 0;
1862
+ if (unreadableGeometry.has(slabExpressID))
1863
+ continue;
1827
1864
  const slabPredefinedType = String(slab.PredefinedType?.value ?? '').toUpperCase();
1828
- if ((slabPredefinedType === 'ROOF' || slabPredefinedType === 'LANDING') &&
1829
- importedMeshPrimitivesFor(slabExpressID).length > 0) {
1865
+ if ((slabPredefinedType === 'ROOF' || slabPredefinedType === 'LANDING') && hasMesh)
1830
1866
  continue;
1831
- }
1832
1867
  const levelElevation = elementLevelElevation(slabExpressID);
1833
1868
  let plan = null;
1834
1869
  try {
@@ -1838,7 +1873,7 @@ export async function convertIfcToPascal(ifcData, onProgress, options) {
1838
1873
  // mesh fallback below
1839
1874
  }
1840
1875
  // A sloped or tilted slab is not a Pascal floor; keep its exact mesh.
1841
- if (plan?.flat === false && importedMeshPrimitivesFor(slabExpressID).length > 0)
1876
+ if (plan?.flat === false && hasMesh)
1842
1877
  continue;
1843
1878
  if (!plan?.polygon)
1844
1879
  continue;
@@ -2680,8 +2715,11 @@ export async function convertIfcToPascal(ifcData, onProgress, options) {
2680
2715
  });
2681
2716
  progress('Complete!', 100);
2682
2717
  return {
2683
- nodes,
2684
- rootNodeIds: rootNodeIds,
2685
- collections,
2718
+ scene: {
2719
+ nodes,
2720
+ rootNodeIds: rootNodeIds,
2721
+ collections,
2722
+ },
2723
+ report,
2686
2724
  };
2687
2725
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pascal-app/ifc-converter",
3
- "version": "1.0.6",
3
+ "version": "1.1.0",
4
4
  "description": "IFC → Pascal scene-graph conversion. Pure logic — no DOM, no React.",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -19,21 +19,24 @@
19
19
  },
20
20
  "files": [
21
21
  "dist",
22
+ "!dist/__bench__",
22
23
  "README.md"
23
24
  ],
24
25
  "scripts": {
25
26
  "build": "tsc --build",
27
+ "check-types": "tsc -p tsconfig.bench.json --pretty false",
26
28
  "dev": "tsgo --build --watch",
27
29
  "test": "bun test tests",
30
+ "bench:normals": "bun run src/__bench__/normal-recompute.bench.ts",
28
31
  "prepublishOnly": "npm run build"
29
32
  },
30
33
  "dependencies": {
31
- "@pascal-app/core": "^1.0.6",
34
+ "@pascal-app/core": "^1.1.0",
32
35
  "web-ifc": "^0.0.78"
33
36
  },
34
37
  "devDependencies": {
35
38
  "@pascal/typescript-config": "*",
36
- "@types/bun": "^1.4.3",
39
+ "@types/bun": "^1.4.4",
37
40
  "typescript": "7.0.2"
38
41
  },
39
42
  "repository": {