@pascal-app/ifc-converter 0.1.0 → 0.1.1

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.
@@ -0,0 +1,25 @@
1
+ import { type AnyNode } from '@pascal-app/core';
2
+ type SceneNodes = Record<string, AnyNode>;
3
+ export type IfcConversionSimplificationOptions = {
4
+ enabled?: boolean;
5
+ maxWallJoinGap?: number;
6
+ };
7
+ export type IfcConversionSimplificationStats = {
8
+ input: {
9
+ walls: number;
10
+ doors: number;
11
+ windows: number;
12
+ };
13
+ output: {
14
+ walls: number;
15
+ doors: number;
16
+ windows: number;
17
+ };
18
+ removedTinyWalls: number;
19
+ mergedWallGroups: number;
20
+ removedMergedWalls: number;
21
+ removedDuplicateOpenings: number;
22
+ };
23
+ export declare function simplifyConvertedSceneGraph(nodes: SceneNodes, options?: IfcConversionSimplificationOptions): IfcConversionSimplificationStats;
24
+ export {};
25
+ //# sourceMappingURL=cleanup.d.ts.map
@@ -0,0 +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;AA6cD,wBAAgB,2BAA2B,CACzC,KAAK,EAAE,UAAU,EACjB,OAAO,GAAE,kCAAuC,GAC/C,gCAAgC,CAoBlC"}
@@ -0,0 +1,434 @@
1
+ import { DEFAULT_WALL_HEIGHT, DEFAULT_WALL_THICKNESS, } from '@pascal-app/core';
2
+ const MIN_WALL_LENGTH = 0.08;
3
+ const WALL_ANGLE_BUCKET_RAD = Math.PI / 180;
4
+ const DEFAULT_MAX_WALL_JOIN_GAP = 1.25;
5
+ const WALL_HEIGHT_TOLERANCE = 0.35;
6
+ const OPENING_DUPLICATE_TOLERANCE = 0.05;
7
+ function countNodes(nodes, type) {
8
+ return Object.values(nodes).filter((node) => node.type === type).length;
9
+ }
10
+ function getInitialStats(nodes) {
11
+ return {
12
+ input: {
13
+ walls: countNodes(nodes, 'wall'),
14
+ doors: countNodes(nodes, 'door'),
15
+ windows: countNodes(nodes, 'window'),
16
+ },
17
+ output: {
18
+ walls: 0,
19
+ doors: 0,
20
+ windows: 0,
21
+ },
22
+ removedTinyWalls: 0,
23
+ mergedWallGroups: 0,
24
+ removedMergedWalls: 0,
25
+ removedDuplicateOpenings: 0,
26
+ };
27
+ }
28
+ function finishStats(nodes, stats) {
29
+ stats.output = {
30
+ walls: countNodes(nodes, 'wall'),
31
+ doors: countNodes(nodes, 'door'),
32
+ windows: countNodes(nodes, 'window'),
33
+ };
34
+ }
35
+ function isOpeningNode(node) {
36
+ return node?.type === 'door' || node?.type === 'window';
37
+ }
38
+ function getOpeningWallId(opening, nodes) {
39
+ if (opening.wallId && nodes[opening.wallId]?.type === 'wall')
40
+ return opening.wallId;
41
+ if (opening.parentId && nodes[opening.parentId]?.type === 'wall')
42
+ return opening.parentId;
43
+ return null;
44
+ }
45
+ function uniqueExistingChildren(children, nodes) {
46
+ const next = [];
47
+ const seen = new Set();
48
+ for (const childId of children ?? []) {
49
+ if (!(childId in nodes) || seen.has(childId))
50
+ continue;
51
+ seen.add(childId);
52
+ next.push(childId);
53
+ }
54
+ return next;
55
+ }
56
+ function normalizeChildren(nodes) {
57
+ for (const node of Object.values(nodes)) {
58
+ const withChildren = node;
59
+ if (Array.isArray(withChildren.children)) {
60
+ withChildren.children = uniqueExistingChildren(withChildren.children, nodes);
61
+ }
62
+ }
63
+ }
64
+ function syncWallOpeningChildren(nodes) {
65
+ for (const node of Object.values(nodes)) {
66
+ if (node.type !== 'wall')
67
+ continue;
68
+ node.children = uniqueExistingChildren(node.children, nodes);
69
+ }
70
+ for (const node of Object.values(nodes)) {
71
+ if (!isOpeningNode(node))
72
+ continue;
73
+ const wallId = getOpeningWallId(node, nodes);
74
+ const wall = wallId ? nodes[wallId] : undefined;
75
+ if (wall?.type !== 'wall')
76
+ continue;
77
+ if (!wall.children.includes(node.id)) {
78
+ wall.children.push(node.id);
79
+ }
80
+ }
81
+ }
82
+ function removeNodeFromParents(nodes, nodeId) {
83
+ for (const node of Object.values(nodes)) {
84
+ const withChildren = node;
85
+ if (!Array.isArray(withChildren.children))
86
+ continue;
87
+ withChildren.children = withChildren.children.filter((childId) => childId !== nodeId);
88
+ }
89
+ }
90
+ function wallLength(wall) {
91
+ return Math.hypot(wall.end[0] - wall.start[0], wall.end[1] - wall.start[1]);
92
+ }
93
+ function pruneTinyWalls(nodes, stats) {
94
+ for (const node of Object.values(nodes)) {
95
+ if (node.type !== 'wall')
96
+ continue;
97
+ if (wallLength(node) >= MIN_WALL_LENGTH)
98
+ continue;
99
+ if (node.children.length > 0)
100
+ continue;
101
+ delete nodes[node.id];
102
+ removeNodeFromParents(nodes, node.id);
103
+ stats.removedTinyWalls += 1;
104
+ }
105
+ }
106
+ function toWallSegment(wall) {
107
+ if (wall.curveOffset !== undefined && Math.abs(wall.curveOffset) > 1e-6)
108
+ return null;
109
+ const dx = wall.end[0] - wall.start[0];
110
+ const dy = wall.end[1] - wall.start[1];
111
+ const length = Math.hypot(dx, dy);
112
+ if (length < MIN_WALL_LENGTH)
113
+ return null;
114
+ let axisX = dx / length;
115
+ let axisY = dy / length;
116
+ if (axisX < -1e-6 || (Math.abs(axisX) <= 1e-6 && axisY < 0)) {
117
+ axisX = -axisX;
118
+ axisY = -axisY;
119
+ }
120
+ const normalX = -axisY;
121
+ const normalY = axisX;
122
+ const startT = wall.start[0] * axisX + wall.start[1] * axisY;
123
+ const endT = wall.end[0] * axisX + wall.end[1] * axisY;
124
+ const startOffset = wall.start[0] * normalX + wall.start[1] * normalY;
125
+ const endOffset = wall.end[0] * normalX + wall.end[1] * normalY;
126
+ const angle = Math.atan2(axisY, axisX);
127
+ return {
128
+ id: wall.id,
129
+ wall,
130
+ parentId: wall.parentId ?? null,
131
+ axisX,
132
+ axisY,
133
+ normalX,
134
+ normalY,
135
+ offset: (startOffset + endOffset) / 2,
136
+ t0: Math.min(startT, endT),
137
+ t1: Math.max(startT, endT),
138
+ length,
139
+ height: wall.height ?? DEFAULT_WALL_HEIGHT,
140
+ thickness: wall.thickness ?? DEFAULT_WALL_THICKNESS,
141
+ angleBucket: Math.round(angle / WALL_ANGLE_BUCKET_RAD),
142
+ };
143
+ }
144
+ function wallLineTolerance(a, b) {
145
+ return Math.max(0.06, Math.min(0.14, Math.max(a.thickness, b.thickness) * 0.5));
146
+ }
147
+ function wallHeightCompatible(a, b) {
148
+ return Math.abs(a.height - b.height) <= WALL_HEIGHT_TOLERANCE;
149
+ }
150
+ function wallIntervalsCompatible(a, b, maxJoinGap) {
151
+ const gap = Math.max(a.t0, b.t0) - Math.min(a.t1, b.t1);
152
+ if (gap <= maxJoinGap)
153
+ return true;
154
+ const overlap = Math.min(a.t1, b.t1) - Math.max(a.t0, b.t0);
155
+ if (overlap <= 0)
156
+ return false;
157
+ return overlap / Math.min(a.length, b.length) >= 0.5;
158
+ }
159
+ function wallsCanMerge(a, b, maxJoinGap) {
160
+ if (a.parentId !== b.parentId)
161
+ return false;
162
+ if (Math.abs(a.angleBucket - b.angleBucket) > 1)
163
+ return false;
164
+ if (Math.abs(a.offset - b.offset) > wallLineTolerance(a, b))
165
+ return false;
166
+ if (!wallHeightCompatible(a, b))
167
+ return false;
168
+ return wallIntervalsCompatible(a, b, maxJoinGap);
169
+ }
170
+ function find(parent, index) {
171
+ let current = index;
172
+ while (parent[current] !== current) {
173
+ parent[current] = parent[parent[current]];
174
+ current = parent[current];
175
+ }
176
+ return current;
177
+ }
178
+ function union(parent, a, b) {
179
+ const rootA = find(parent, a);
180
+ const rootB = find(parent, b);
181
+ if (rootA !== rootB)
182
+ parent[rootB] = rootA;
183
+ }
184
+ function openingWorldPosition(opening, wall) {
185
+ const length = wallLength(wall);
186
+ if (length < 1e-6)
187
+ return [wall.start[0], wall.start[1]];
188
+ const axisX = (wall.end[0] - wall.start[0]) / length;
189
+ const axisY = (wall.end[1] - wall.start[1]) / length;
190
+ const normalX = -axisY;
191
+ const normalY = axisX;
192
+ const [localX, , localZ] = opening.position;
193
+ return [
194
+ wall.start[0] + axisX * localX + normalX * localZ,
195
+ wall.start[1] + axisY * localX + normalY * localZ,
196
+ ];
197
+ }
198
+ function clampOpeningAlongWall(opening, along, wallLengthValue) {
199
+ const width = opening.width ?? (opening.type === 'door' ? 0.9 : 1.0);
200
+ const half = width / 2;
201
+ const lo = Math.min(half, wallLengthValue / 2);
202
+ const hi = Math.max(wallLengthValue - half, wallLengthValue / 2);
203
+ return Math.max(lo, Math.min(hi, along));
204
+ }
205
+ function rehostOpeningToWall(opening, oldWall, newWall) {
206
+ const [worldX, worldY] = openingWorldPosition(opening, oldWall);
207
+ const newLength = wallLength(newWall);
208
+ if (newLength < 1e-6)
209
+ return;
210
+ const axisX = (newWall.end[0] - newWall.start[0]) / newLength;
211
+ const axisY = (newWall.end[1] - newWall.start[1]) / newLength;
212
+ const normalX = -axisY;
213
+ const normalY = axisX;
214
+ const relX = worldX - newWall.start[0];
215
+ const relY = worldY - newWall.start[1];
216
+ const along = clampOpeningAlongWall(opening, relX * axisX + relY * axisY, newLength);
217
+ const across = relX * normalX + relY * normalY;
218
+ opening.parentId = newWall.id;
219
+ opening.wallId = newWall.id;
220
+ opening.position = [along, opening.position[1], across];
221
+ }
222
+ function pointOnLine(segment, t, offset) {
223
+ return [
224
+ segment.axisX * t + segment.normalX * offset,
225
+ segment.axisY * t + segment.normalY * offset,
226
+ ];
227
+ }
228
+ function chooseKeptSegment(cluster) {
229
+ return cluster.reduce((best, current) => {
230
+ if (current.wall.children.length !== best.wall.children.length) {
231
+ return current.wall.children.length > best.wall.children.length ? current : best;
232
+ }
233
+ return current.length > best.length ? current : best;
234
+ }, cluster[0]);
235
+ }
236
+ function collectOpeningIdsForWalls(nodes, wallIds) {
237
+ const childIds = new Set();
238
+ const childOriginWall = new Map();
239
+ for (const wallId of wallIds) {
240
+ const wall = nodes[wallId];
241
+ if (wall?.type !== 'wall')
242
+ continue;
243
+ for (const childId of wall.children) {
244
+ childIds.add(childId);
245
+ childOriginWall.set(childId, wallId);
246
+ }
247
+ }
248
+ for (const node of Object.values(nodes)) {
249
+ if (!isOpeningNode(node))
250
+ continue;
251
+ const wallId = getOpeningWallId(node, nodes);
252
+ if (!wallId || !wallIds.has(wallId))
253
+ continue;
254
+ childIds.add(node.id);
255
+ childOriginWall.set(node.id, wallId);
256
+ }
257
+ return { childIds, childOriginWall };
258
+ }
259
+ function mergeWallCluster(nodes, cluster, stats) {
260
+ const kept = chooseKeptSegment(cluster);
261
+ const keptWall = kept.wall;
262
+ const wallIds = new Set(cluster.map((segment) => segment.id));
263
+ const originalWalls = new Map(cluster.map((segment) => [
264
+ segment.id,
265
+ {
266
+ ...segment.wall,
267
+ start: [...segment.wall.start],
268
+ end: [...segment.wall.end],
269
+ children: [...segment.wall.children],
270
+ },
271
+ ]));
272
+ const { childIds, childOriginWall } = collectOpeningIdsForWalls(nodes, wallIds);
273
+ const t0 = Math.min(...cluster.map((segment) => segment.t0));
274
+ const t1 = Math.max(...cluster.map((segment) => segment.t1));
275
+ const crossMin = Math.min(...cluster.map((segment) => segment.offset - segment.thickness / 2));
276
+ const crossMax = Math.max(...cluster.map((segment) => segment.offset + segment.thickness / 2));
277
+ const offset = (crossMin + crossMax) / 2;
278
+ const thickness = Math.max(...cluster.map((segment) => segment.thickness), crossMax - crossMin);
279
+ const height = Math.max(...cluster.map((segment) => segment.height));
280
+ const mergedExpressIds = cluster
281
+ .map((segment) => segment.wall.metadata?.expressID)
282
+ .filter((expressID) => typeof expressID === 'number');
283
+ keptWall.start = pointOnLine(kept, t0, offset);
284
+ keptWall.end = pointOnLine(kept, t1, offset);
285
+ keptWall.thickness = thickness;
286
+ keptWall.height = height;
287
+ keptWall.metadata = {
288
+ ...(keptWall.metadata ?? {}),
289
+ ifcSimplification: {
290
+ mergedWallCount: cluster.length,
291
+ mergedExpressIDs: mergedExpressIds,
292
+ },
293
+ };
294
+ const nextChildren = new Set();
295
+ for (const childId of childIds) {
296
+ const child = nodes[childId];
297
+ if (!child)
298
+ continue;
299
+ if (isOpeningNode(child)) {
300
+ const originWallId = childOriginWall.get(childId);
301
+ const originWall = originWallId ? originalWalls.get(originWallId) : undefined;
302
+ if (originWall) {
303
+ rehostOpeningToWall(child, originWall, keptWall);
304
+ }
305
+ else {
306
+ child.parentId = keptWall.id;
307
+ child.wallId = keptWall.id;
308
+ }
309
+ }
310
+ else {
311
+ child.parentId = keptWall.id;
312
+ }
313
+ nextChildren.add(childId);
314
+ }
315
+ keptWall.children = Array.from(nextChildren);
316
+ for (const segment of cluster) {
317
+ if (segment.id === keptWall.id)
318
+ continue;
319
+ delete nodes[segment.id];
320
+ removeNodeFromParents(nodes, segment.id);
321
+ }
322
+ const parent = keptWall.parentId ? nodes[keptWall.parentId] : undefined;
323
+ const parentWithChildren = parent;
324
+ if (parentWithChildren?.children && !parentWithChildren.children.includes(keptWall.id)) {
325
+ parentWithChildren.children.push(keptWall.id);
326
+ }
327
+ stats.mergedWallGroups += 1;
328
+ stats.removedMergedWalls += cluster.length - 1;
329
+ }
330
+ function mergeWallFragments(nodes, stats, options) {
331
+ const segments = Object.values(nodes)
332
+ .filter((node) => node.type === 'wall')
333
+ .map(toWallSegment)
334
+ .filter((segment) => segment !== null);
335
+ const groups = new Map();
336
+ for (const segment of segments) {
337
+ const key = `${segment.parentId ?? 'root'}:${segment.angleBucket}`;
338
+ const group = groups.get(key);
339
+ if (group)
340
+ group.push(segment);
341
+ else
342
+ groups.set(key, [segment]);
343
+ }
344
+ for (const group of groups.values()) {
345
+ if (group.length < 2)
346
+ continue;
347
+ const parent = group.map((_, index) => index);
348
+ for (let i = 0; i < group.length; i++) {
349
+ for (let j = i + 1; j < group.length; j++) {
350
+ if (wallsCanMerge(group[i], group[j], options.maxWallJoinGap)) {
351
+ union(parent, i, j);
352
+ }
353
+ }
354
+ }
355
+ const clusters = new Map();
356
+ for (let i = 0; i < group.length; i++) {
357
+ const root = find(parent, i);
358
+ const cluster = clusters.get(root);
359
+ if (cluster)
360
+ cluster.push(group[i]);
361
+ else
362
+ clusters.set(root, [group[i]]);
363
+ }
364
+ for (const cluster of clusters.values()) {
365
+ if (cluster.length < 2)
366
+ continue;
367
+ mergeWallCluster(nodes, cluster, stats);
368
+ }
369
+ }
370
+ }
371
+ function signatureNumber(value, tolerance) {
372
+ return Math.round((value ?? 0) / tolerance);
373
+ }
374
+ function openingSignature(opening) {
375
+ const [x, y, z] = opening.position;
376
+ const family = opening.type === 'door'
377
+ ? `${opening.openingKind}:${opening.openingShape}:${opening.doorType}`
378
+ : `${opening.openingKind}:${opening.openingShape}:${opening.windowType}`;
379
+ return [
380
+ opening.type,
381
+ family,
382
+ signatureNumber(x, OPENING_DUPLICATE_TOLERANCE),
383
+ signatureNumber(y, OPENING_DUPLICATE_TOLERANCE),
384
+ signatureNumber(z, OPENING_DUPLICATE_TOLERANCE),
385
+ signatureNumber(opening.width, OPENING_DUPLICATE_TOLERANCE),
386
+ signatureNumber(opening.height, OPENING_DUPLICATE_TOLERANCE),
387
+ ].join(':');
388
+ }
389
+ function dedupeOpenings(nodes, stats) {
390
+ const byWall = new Map();
391
+ for (const node of Object.values(nodes)) {
392
+ if (!isOpeningNode(node))
393
+ continue;
394
+ const wallId = getOpeningWallId(node, nodes);
395
+ if (!wallId)
396
+ continue;
397
+ const openings = byWall.get(wallId);
398
+ if (openings)
399
+ openings.push(node);
400
+ else
401
+ byWall.set(wallId, [node]);
402
+ }
403
+ for (const openings of byWall.values()) {
404
+ const seen = new Set();
405
+ for (const opening of openings) {
406
+ const signature = openingSignature(opening);
407
+ if (!seen.has(signature)) {
408
+ seen.add(signature);
409
+ continue;
410
+ }
411
+ delete nodes[opening.id];
412
+ removeNodeFromParents(nodes, opening.id);
413
+ stats.removedDuplicateOpenings += 1;
414
+ }
415
+ }
416
+ }
417
+ export function simplifyConvertedSceneGraph(nodes, options = {}) {
418
+ const stats = getInitialStats(nodes);
419
+ if (options.enabled === false) {
420
+ finishStats(nodes, stats);
421
+ return stats;
422
+ }
423
+ const resolvedOptions = {
424
+ enabled: true,
425
+ maxWallJoinGap: options.maxWallJoinGap ?? DEFAULT_MAX_WALL_JOIN_GAP,
426
+ };
427
+ pruneTinyWalls(nodes, stats);
428
+ mergeWallFragments(nodes, stats, resolvedOptions);
429
+ syncWallOpeningChildren(nodes);
430
+ dedupeOpenings(nodes, stats);
431
+ normalizeChildren(nodes);
432
+ finishStats(nodes, stats);
433
+ return stats;
434
+ }
package/dist/index.d.ts CHANGED
@@ -1,4 +1,6 @@
1
1
  import { type AnyNode, type AnyNodeId } from '@pascal-app/core';
2
+ import { type IfcConversionSimplificationOptions } from './cleanup';
3
+ export type { IfcConversionSimplificationOptions, IfcConversionSimplificationStats, } from './cleanup';
2
4
  export type PascalNode = AnyNode;
3
5
  export interface PascalSceneGraph {
4
6
  nodes: Record<AnyNodeId, AnyNode>;
@@ -9,6 +11,7 @@ export interface ConversionOptions {
9
11
  swapYZ?: boolean;
10
12
  extrusionDepthIsHeight?: boolean;
11
13
  swapProfileDimensions?: boolean;
14
+ simplify?: boolean | IfcConversionSimplificationOptions;
12
15
  label?: string;
13
16
  }
14
17
  export declare const VARIANT_PRESETS: Record<string, ConversionOptions>;
@@ -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;AAIzB,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,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,CA04C3B"}
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,CA05C3B"}
package/dist/index.js CHANGED
@@ -1,6 +1,7 @@
1
1
  import { BuildingNode, ColumnNode, DEFAULT_WALL_HEIGHT, DEFAULT_WALL_THICKNESS, DoorNode, LevelNode, RoofNode, SiteNode, SlabNode, StairNode, WallNode, WindowNode, } from '@pascal-app/core';
2
2
  import { customAlphabet } from 'nanoid';
3
3
  import * as WebIFC from 'web-ifc';
4
+ import { simplifyConvertedSceneGraph } from './cleanup';
4
5
  function meta(node) {
5
6
  return (node?.metadata ?? {});
6
7
  }
@@ -554,6 +555,11 @@ export async function convertIfcToPascal(ifcData, onProgress, options) {
554
555
  extrusionDepthIsHeight: options?.extrusionDepthIsHeight ?? true,
555
556
  swapProfileDimensions: options?.swapProfileDimensions ?? false,
556
557
  };
558
+ const simplificationOptions = options?.simplify === false
559
+ ? { enabled: false }
560
+ : typeof options?.simplify === 'object'
561
+ ? options.simplify
562
+ : undefined;
557
563
  const progress = (msg, pct) => {
558
564
  console.log(`[IFC→Pascal] ${msg} (${pct}%)`);
559
565
  onProgress?.(msg, pct);
@@ -1873,6 +1879,13 @@ export async function convertIfcToPascal(ifcData, onProgress, options) {
1873
1879
  catch {
1874
1880
  /* no type rels */
1875
1881
  }
1882
+ progress('Simplifying converted scene...', 94);
1883
+ const simplificationStats = simplifyConvertedSceneGraph(nodes, simplificationOptions);
1884
+ if (simplificationStats.removedTinyWalls > 0 ||
1885
+ simplificationStats.removedMergedWalls > 0 ||
1886
+ simplificationStats.removedDuplicateOpenings > 0) {
1887
+ console.log('[IFC→Pascal] Simplification:', simplificationStats);
1888
+ }
1876
1889
  ifcApi.CloseModel(modelID);
1877
1890
  progress('Building scene graph...', 95);
1878
1891
  const totalNodes = Object.keys(nodes).length;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pascal-app/ifc-converter",
3
- "version": "0.1.0",
3
+ "version": "0.1.1",
4
4
  "description": "IFC → Pascal scene-graph conversion. Pure logic — no DOM, no React.",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -29,7 +29,7 @@
29
29
  "devDependencies": {
30
30
  "@pascal/typescript-config": "*",
31
31
  "@types/bun": "^1.3.0",
32
- "typescript": "6.0.2"
32
+ "typescript": "6.0.3"
33
33
  },
34
34
  "repository": {
35
35
  "type": "git",