@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 +1 -0
- package/dist/export/ifc-model.d.ts +7 -1
- package/dist/export/ifc-model.d.ts.map +1 -1
- package/dist/export/ifc-model.js +11 -0
- package/dist/export/scene-to-ifc.d.ts.map +1 -1
- package/dist/export/scene-to-ifc.js +29 -9
- package/dist/geometry-validity.d.ts +45 -0
- package/dist/geometry-validity.d.ts.map +1 -0
- package/dist/geometry-validity.js +138 -0
- package/dist/index.d.ts +13 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +55 -17
- package/package.json +6 -3
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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"}
|
package/dist/export/ifc-model.js
CHANGED
|
@@ -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,
|
|
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
|
|
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
|
|
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
|
|
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 &&
|
|
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(
|
|
701
|
-
|
|
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,
|
|
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
|
package/dist/index.d.ts.map
CHANGED
|
@@ -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;
|
|
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
|
|
598
|
-
if (
|
|
604
|
+
const mesh = keepValidTriangles({ positions, normals, indices: Array.from(sourceIndices) }, report);
|
|
605
|
+
if (mesh) {
|
|
599
606
|
primitives.push({
|
|
600
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
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 &&
|
|
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
|
-
|
|
2684
|
-
|
|
2685
|
-
|
|
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
|
|
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
|
|
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.
|
|
39
|
+
"@types/bun": "^1.4.4",
|
|
37
40
|
"typescript": "7.0.2"
|
|
38
41
|
},
|
|
39
42
|
"repository": {
|