@pascal-app/ifc-converter 1.0.0-beta.5 → 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/dist/cleanup.d.ts.map +1 -1
- package/dist/cleanup.js +24 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +18 -13
- package/package.json +3 -3
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Pascal Group Inc.
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/dist/cleanup.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"cleanup.d.ts","sourceRoot":"","sources":["../src/cleanup.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,KAAK,OAAO,EAMb,MAAM,kBAAkB,CAAA;AAEzB,KAAK,UAAU,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAA;AAGzC,MAAM,MAAM,kCAAkC,GAAG;IAC/C,OAAO,CAAC,EAAE,OAAO,CAAA;IACjB,cAAc,CAAC,EAAE,MAAM,CAAA;CACxB,CAAA;AAED,MAAM,MAAM,gCAAgC,GAAG;IAC7C,KAAK,EAAE;QACL,KAAK,EAAE,MAAM,CAAA;QACb,KAAK,EAAE,MAAM,CAAA;QACb,OAAO,EAAE,MAAM,CAAA;KAChB,CAAA;IACD,MAAM,EAAE;QACN,KAAK,EAAE,MAAM,CAAA;QACb,KAAK,EAAE,MAAM,CAAA;QACb,OAAO,EAAE,MAAM,CAAA;KAChB,CAAA;IACD,gBAAgB,EAAE,MAAM,CAAA;IACxB,gBAAgB,EAAE,MAAM,CAAA;IACxB,kBAAkB,EAAE,MAAM,CAAA;IAC1B,wBAAwB,EAAE,MAAM,CAAA;CACjC,CAAA;
|
|
1
|
+
{"version":3,"file":"cleanup.d.ts","sourceRoot":"","sources":["../src/cleanup.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,KAAK,OAAO,EAMb,MAAM,kBAAkB,CAAA;AAEzB,KAAK,UAAU,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAA;AAGzC,MAAM,MAAM,kCAAkC,GAAG;IAC/C,OAAO,CAAC,EAAE,OAAO,CAAA;IACjB,cAAc,CAAC,EAAE,MAAM,CAAA;CACxB,CAAA;AAED,MAAM,MAAM,gCAAgC,GAAG;IAC7C,KAAK,EAAE;QACL,KAAK,EAAE,MAAM,CAAA;QACb,KAAK,EAAE,MAAM,CAAA;QACb,OAAO,EAAE,MAAM,CAAA;KAChB,CAAA;IACD,MAAM,EAAE;QACN,KAAK,EAAE,MAAM,CAAA;QACb,KAAK,EAAE,MAAM,CAAA;QACb,OAAO,EAAE,MAAM,CAAA;KAChB,CAAA;IACD,gBAAgB,EAAE,MAAM,CAAA;IACxB,gBAAgB,EAAE,MAAM,CAAA;IACxB,kBAAkB,EAAE,MAAM,CAAA;IAC1B,wBAAwB,EAAE,MAAM,CAAA;CACjC,CAAA;AAseD,wBAAgB,2BAA2B,CACzC,KAAK,EAAE,UAAU,EACjB,OAAO,GAAE,kCAAuC,GAC/C,gCAAgC,CAoBlC"}
|
package/dist/cleanup.js
CHANGED
|
@@ -142,11 +142,32 @@ function toWallSegment(wall) {
|
|
|
142
142
|
};
|
|
143
143
|
}
|
|
144
144
|
function wallLineTolerance(a, b) {
|
|
145
|
-
|
|
145
|
+
// Fragments must share essentially the same centerline. A tolerance based on
|
|
146
|
+
// half the wall thickness can collapse adjacent walls whose faces merely meet.
|
|
147
|
+
return Math.max(0.005, Math.min(0.025, Math.max(a.thickness, b.thickness) * 0.1));
|
|
146
148
|
}
|
|
147
149
|
function wallHeightCompatible(a, b) {
|
|
148
150
|
return Math.abs(a.height - b.height) <= WALL_HEIGHT_TOLERANCE;
|
|
149
151
|
}
|
|
152
|
+
function wallMaterialSignature(segment) {
|
|
153
|
+
const metadata = segment.wall.metadata;
|
|
154
|
+
const material = typeof metadata?.material === 'string' ? metadata.material : null;
|
|
155
|
+
const layers = Array.isArray(metadata?.materialLayers)
|
|
156
|
+
? metadata.materialLayers.map((layer) => {
|
|
157
|
+
const value = layer;
|
|
158
|
+
return [
|
|
159
|
+
typeof value.name === 'string' ? value.name : null,
|
|
160
|
+
typeof value.thickness === 'number' ? value.thickness : null,
|
|
161
|
+
];
|
|
162
|
+
})
|
|
163
|
+
: [];
|
|
164
|
+
if (material === null && layers.length === 0)
|
|
165
|
+
return null;
|
|
166
|
+
return JSON.stringify({ material, layers });
|
|
167
|
+
}
|
|
168
|
+
function wallMaterialCompatible(a, b) {
|
|
169
|
+
return wallMaterialSignature(a) === wallMaterialSignature(b);
|
|
170
|
+
}
|
|
150
171
|
function wallIntervalsCompatible(a, b, maxJoinGap) {
|
|
151
172
|
const gap = Math.max(a.t0, b.t0) - Math.min(a.t1, b.t1);
|
|
152
173
|
if (gap <= maxJoinGap)
|
|
@@ -165,6 +186,8 @@ function wallsCanMerge(a, b, maxJoinGap) {
|
|
|
165
186
|
return false;
|
|
166
187
|
if (!wallHeightCompatible(a, b))
|
|
167
188
|
return false;
|
|
189
|
+
if (!wallMaterialCompatible(a, b))
|
|
190
|
+
return false;
|
|
168
191
|
return wallIntervalsCompatible(a, b, maxJoinGap);
|
|
169
192
|
}
|
|
170
193
|
function find(parent, index) {
|
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,EAaf,MAAM,kBAAkB,CAAA;AAGzB,OAAO,EAAE,KAAK,kCAAkC,EAA+B,MAAM,WAAW,CAAA;AAEhG,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,OAAO,CAAC,CAAA;CACtC;AAimBD,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,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,
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,KAAK,OAAO,EACZ,KAAK,SAAS,EAaf,MAAM,kBAAkB,CAAA;AAGzB,OAAO,EAAE,KAAK,kCAAkC,EAA+B,MAAM,WAAW,CAAA;AAEhG,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,OAAO,CAAC,CAAA;CACtC;AAimBD,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,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,CA65C3B"}
|
package/dist/index.js
CHANGED
|
@@ -5,11 +5,10 @@ import { simplifyConvertedSceneGraph } from './cleanup';
|
|
|
5
5
|
function meta(node) {
|
|
6
6
|
return (node?.metadata ?? {});
|
|
7
7
|
}
|
|
8
|
-
//
|
|
9
|
-
//
|
|
10
|
-
//
|
|
11
|
-
//
|
|
12
|
-
// schemas happy without spraying `?? null` through every assignment.
|
|
8
|
+
// The converter pulls many metadata fields from optional IFC properties
|
|
9
|
+
// that often return `undefined`. Those keys vanish the moment the graph is
|
|
10
|
+
// serialized, so stripping them here keeps the in-memory scene identical to
|
|
11
|
+
// the persisted one instead of spraying `?? null` through every assignment.
|
|
13
12
|
function buildMetadata(input) {
|
|
14
13
|
const out = {};
|
|
15
14
|
for (const [key, value] of Object.entries(input)) {
|
|
@@ -621,9 +620,10 @@ export async function convertIfcToPascal(ifcData, onProgress, options) {
|
|
|
621
620
|
parentMap.set(childID, parentExpressID);
|
|
622
621
|
});
|
|
623
622
|
if (!childrenMap.has(parentExpressID)) {
|
|
624
|
-
childrenMap.set(parentExpressID,
|
|
623
|
+
childrenMap.set(parentExpressID, new Set());
|
|
625
624
|
}
|
|
626
|
-
|
|
625
|
+
for (const childID of children)
|
|
626
|
+
childrenMap.get(parentExpressID).add(childID);
|
|
627
627
|
}
|
|
628
628
|
}
|
|
629
629
|
// Second pass: collect spatial containment
|
|
@@ -638,9 +638,10 @@ export async function convertIfcToPascal(ifcData, onProgress, options) {
|
|
|
638
638
|
parentMap.set(childID, parentExpressID);
|
|
639
639
|
});
|
|
640
640
|
if (!childrenMap.has(parentExpressID)) {
|
|
641
|
-
childrenMap.set(parentExpressID,
|
|
641
|
+
childrenMap.set(parentExpressID, new Set());
|
|
642
642
|
}
|
|
643
|
-
|
|
643
|
+
for (const childID of children)
|
|
644
|
+
childrenMap.get(parentExpressID).add(childID);
|
|
644
645
|
}
|
|
645
646
|
}
|
|
646
647
|
// Resolve containing storey for an element by walking the parent chain
|
|
@@ -973,6 +974,10 @@ export async function convertIfcToPascal(ifcData, onProgress, options) {
|
|
|
973
974
|
const fillId = openingToFill.get(openingId);
|
|
974
975
|
if (!fillId)
|
|
975
976
|
continue;
|
|
977
|
+
// IFC fills belong to at most one opening, which voids one host element.
|
|
978
|
+
// Repeated or conflicting relationships must not emit another node.
|
|
979
|
+
if (expressIdToNodeId.has(fillId))
|
|
980
|
+
continue;
|
|
976
981
|
const isDoor = doorExpressIds.has(fillId);
|
|
977
982
|
const isWindow = windowExpressIds.has(fillId);
|
|
978
983
|
if (!isDoor && !isWindow)
|
|
@@ -1035,7 +1040,6 @@ export async function convertIfcToPascal(ifcData, onProgress, options) {
|
|
|
1035
1040
|
}
|
|
1036
1041
|
if (isDoor) {
|
|
1037
1042
|
const nodeId = generateId('door');
|
|
1038
|
-
expressIdToNodeId.set(fillId, nodeId);
|
|
1039
1043
|
// Vertical centering is now handled: door center Y = height/2 so the
|
|
1040
1044
|
// opening sits at the correct position. Remaining caveat: door bottom
|
|
1041
1045
|
// is assumed at floor y=0 (i.e. the door starts at the wall's base).
|
|
@@ -1059,11 +1063,11 @@ export async function convertIfcToPascal(ifcData, onProgress, options) {
|
|
|
1059
1063
|
}),
|
|
1060
1064
|
});
|
|
1061
1065
|
nodes[nodeId] = doorNode;
|
|
1066
|
+
expressIdToNodeId.set(fillId, nodeId);
|
|
1062
1067
|
wallNode.children.push(nodeId);
|
|
1063
1068
|
}
|
|
1064
1069
|
else {
|
|
1065
1070
|
const nodeId = generateId('window');
|
|
1066
|
-
expressIdToNodeId.set(fillId, nodeId);
|
|
1067
1071
|
// TODO(ifc-fix): same scalar-vs-tuple position issue as door above.
|
|
1068
1072
|
// sillHeight stays read-only metadata until we resolve the window
|
|
1069
1073
|
// schema (Pascal's WindowNode doesn't have sillHeight today —
|
|
@@ -1092,6 +1096,7 @@ export async function convertIfcToPascal(ifcData, onProgress, options) {
|
|
|
1092
1096
|
}),
|
|
1093
1097
|
});
|
|
1094
1098
|
nodes[nodeId] = windowNode;
|
|
1099
|
+
expressIdToNodeId.set(fillId, nodeId);
|
|
1095
1100
|
wallNode.children.push(nodeId);
|
|
1096
1101
|
}
|
|
1097
1102
|
}
|
|
@@ -1209,7 +1214,6 @@ export async function convertIfcToPascal(ifcData, onProgress, options) {
|
|
|
1209
1214
|
if (isDoor) {
|
|
1210
1215
|
const h = height ?? 2.1;
|
|
1211
1216
|
const nodeId = generateId('door');
|
|
1212
|
-
expressIdToNodeId.set(fillId, nodeId);
|
|
1213
1217
|
const doorNode = tryParse(DoorNode, 'door', {
|
|
1214
1218
|
object: 'node',
|
|
1215
1219
|
id: nodeId,
|
|
@@ -1230,6 +1234,7 @@ export async function convertIfcToPascal(ifcData, onProgress, options) {
|
|
|
1230
1234
|
}),
|
|
1231
1235
|
});
|
|
1232
1236
|
nodes[nodeId] = doorNode;
|
|
1237
|
+
expressIdToNodeId.set(fillId, nodeId);
|
|
1233
1238
|
if (parentNodeId && nodes[parentNodeId]) {
|
|
1234
1239
|
;
|
|
1235
1240
|
nodes[parentNodeId].children?.push(nodeId);
|
|
@@ -1239,7 +1244,6 @@ export async function convertIfcToPascal(ifcData, onProgress, options) {
|
|
|
1239
1244
|
const h = height ?? 1.2;
|
|
1240
1245
|
const sill = hosted && scene ? Math.max(0, scene[2] - hosted.info.baseY) : 0;
|
|
1241
1246
|
const nodeId = generateId('window');
|
|
1242
|
-
expressIdToNodeId.set(fillId, nodeId);
|
|
1243
1247
|
const windowNode = tryParse(WindowNode, 'window', {
|
|
1244
1248
|
object: 'node',
|
|
1245
1249
|
id: nodeId,
|
|
@@ -1259,6 +1263,7 @@ export async function convertIfcToPascal(ifcData, onProgress, options) {
|
|
|
1259
1263
|
}),
|
|
1260
1264
|
});
|
|
1261
1265
|
nodes[nodeId] = windowNode;
|
|
1266
|
+
expressIdToNodeId.set(fillId, nodeId);
|
|
1262
1267
|
if (parentNodeId && nodes[parentNodeId]) {
|
|
1263
1268
|
;
|
|
1264
1269
|
nodes[parentNodeId].children?.push(nodeId);
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@pascal-app/ifc-converter",
|
|
3
|
-
"version": "1.0.0
|
|
4
|
-
"description": "IFC
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "IFC \u2192 Pascal scene-graph conversion. Pure logic \u2014 no DOM, no React.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|
|
7
7
|
"types": "./dist/index.d.ts",
|
|
@@ -23,7 +23,7 @@
|
|
|
23
23
|
"prepublishOnly": "npm run build"
|
|
24
24
|
},
|
|
25
25
|
"dependencies": {
|
|
26
|
-
"@pascal-app/core": "
|
|
26
|
+
"@pascal-app/core": "^1.0.0",
|
|
27
27
|
"nanoid": "^5.1.6",
|
|
28
28
|
"web-ifc": "^0.0.77"
|
|
29
29
|
},
|