@pascal-app/ifc-converter 1.0.3 → 1.0.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +63 -4
- package/dist/beam-geometry.d.ts.map +1 -1
- package/dist/beam-geometry.js +3 -2
- package/dist/cleanup.d.ts +8 -1
- package/dist/cleanup.d.ts.map +1 -1
- package/dist/cleanup.js +13 -4
- package/dist/collections.d.ts +4 -0
- package/dist/collections.d.ts.map +1 -0
- package/dist/collections.js +66 -0
- package/dist/export/door-operation.d.ts +13 -0
- package/dist/export/door-operation.d.ts.map +1 -0
- package/dist/export/door-operation.js +45 -0
- package/dist/export/guid.d.ts +15 -0
- package/dist/export/guid.d.ts.map +1 -0
- package/dist/export/guid.js +80 -0
- package/dist/export/ifc-model.d.ts +91 -0
- package/dist/export/ifc-model.d.ts.map +1 -0
- package/dist/export/ifc-model.js +184 -0
- package/dist/export/index.d.ts +3 -0
- package/dist/export/index.d.ts.map +1 -0
- package/dist/export/index.js +2 -0
- package/dist/export/scene-to-ifc.d.ts +45 -0
- package/dist/export/scene-to-ifc.d.ts.map +1 -0
- package/dist/export/scene-to-ifc.js +1175 -0
- package/dist/export/step.d.ts +64 -0
- package/dist/export/step.d.ts.map +1 -0
- package/dist/export/step.js +158 -0
- package/dist/export/wall-base.d.ts +25 -0
- package/dist/export/wall-base.d.ts.map +1 -0
- package/dist/export/wall-base.js +91 -0
- package/dist/ids.d.ts +3 -0
- package/dist/ids.d.ts.map +1 -0
- package/dist/ids.js +35 -0
- package/dist/index.d.ts +2 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +570 -151
- package/dist/room-first.d.ts +35 -0
- package/dist/room-first.d.ts.map +1 -0
- package/dist/room-first.js +885 -0
- package/dist/wall-joins.d.ts +21 -0
- package/dist/wall-joins.d.ts.map +1 -0
- package/dist/wall-joins.js +367 -0
- package/package.json +10 -6
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import type { AnyNode, WallNode } from '@pascal-app/core';
|
|
2
|
+
type SceneNodes = Record<string, AnyNode>;
|
|
3
|
+
/**
|
|
4
|
+
* Walls that only line or hide inside another wall: tiles, skirtings and thin
|
|
5
|
+
* facings exported as IfcWall along a real wall's face, shorter walls laid
|
|
6
|
+
* face to face along or running through a longer one, and walls whose body
|
|
7
|
+
* lies entirely inside a thicker parallel wall. As Pascal walls they carve
|
|
8
|
+
* sliver rooms or keep corners from closing, so they stay exact meshes.
|
|
9
|
+
*/
|
|
10
|
+
export declare function redundantWallIds(walls: readonly WallNode[]): Map<string, WallNode>;
|
|
11
|
+
export declare function joinWallEnds(nodes: SceneNodes, connections?: readonly (readonly [number, number])[],
|
|
12
|
+
/** Host wall expressID → thickness of a lining removed from its face (see redundantWallIds). */
|
|
13
|
+
linings?: ReadonlyMap<number, number>): number;
|
|
14
|
+
/**
|
|
15
|
+
* Room faces only close at wall junctions: a wall crossing another mid-span
|
|
16
|
+
* (an X, not a T) splits in two at the crossing so both halves meet the other
|
|
17
|
+
* wall's centreline. The thinner wall of the pair is split.
|
|
18
|
+
*/
|
|
19
|
+
export declare function splitCrossingWalls(nodes: SceneNodes): number;
|
|
20
|
+
export {};
|
|
21
|
+
//# sourceMappingURL=wall-joins.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"wall-joins.d.ts","sourceRoot":"","sources":["../src/wall-joins.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,OAAO,EAAE,QAAQ,EAAE,MAAM,kBAAkB,CAAA;AAIzD,KAAK,UAAU,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAA;AAwFzC;;;;;;GAMG;AACH,wBAAgB,gBAAgB,CAAC,KAAK,EAAE,SAAS,QAAQ,EAAE,GAAG,GAAG,CAAC,MAAM,EAAE,QAAQ,CAAC,CAiDlF;AAED,wBAAgB,YAAY,CAC1B,KAAK,EAAE,UAAU,EACjB,WAAW,GAAE,SAAS,CAAC,SAAS,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,EAAO;AACxD,gGAAgG;AAChG,OAAO,GAAE,WAAW,CAAC,MAAM,EAAE,MAAM,CAAa,GAC/C,MAAM,CAqFR;AAuFD;;;;GAIG;AACH,wBAAgB,kBAAkB,CAAC,KAAK,EAAE,UAAU,GAAG,MAAM,CA4B5D"}
|
|
@@ -0,0 +1,367 @@
|
|
|
1
|
+
import { isPascalAuthored } from './cleanup';
|
|
2
|
+
import { nextId } from './ids';
|
|
3
|
+
/**
|
|
4
|
+
* Revit (and most IFC exporters) stop a wall axis at the neighbour's face, not
|
|
5
|
+
* its centreline, so Pascal's wall graph sees open corners and rooms never
|
|
6
|
+
* close. Each straight wall end moves along its own axis to the nearest
|
|
7
|
+
* neighbour centreline it clearly meant to meet (L and T joins).
|
|
8
|
+
*/
|
|
9
|
+
/** Largest along-axis move for an end IfcRelConnectsPathElements joins. */
|
|
10
|
+
const MAX_CONNECTED_GAP = 0.6;
|
|
11
|
+
/** Slack on top of the neighbour thickness for an unconnected end. */
|
|
12
|
+
const GAP_TOLERANCE = 0.05;
|
|
13
|
+
/** Walls closer to parallel than this never join end-to-side. */
|
|
14
|
+
const MIN_JOIN_SINE = Math.sin((15 * Math.PI) / 180);
|
|
15
|
+
const MIN_JOINED_LENGTH = 0.1;
|
|
16
|
+
const cross = (a, b) => a[0] * b[1] - a[1] * b[0];
|
|
17
|
+
function toSegment(wall) {
|
|
18
|
+
if (wall.curveOffset !== undefined && Math.abs(wall.curveOffset) > 1e-6)
|
|
19
|
+
return null;
|
|
20
|
+
const dx = wall.end[0] - wall.start[0];
|
|
21
|
+
const dy = wall.end[1] - wall.start[1];
|
|
22
|
+
const length = Math.hypot(dx, dy);
|
|
23
|
+
if (length < MIN_JOINED_LENGTH)
|
|
24
|
+
return null;
|
|
25
|
+
return {
|
|
26
|
+
wall,
|
|
27
|
+
start: wall.start,
|
|
28
|
+
end: wall.end,
|
|
29
|
+
dir: [dx / length, dy / length],
|
|
30
|
+
length,
|
|
31
|
+
thickness: wall.thickness ?? 0.1,
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
function wallExpressIds(wall) {
|
|
35
|
+
const metadata = (wall.metadata ?? {});
|
|
36
|
+
const merged = metadata.ifcSimplification?.mergedExpressIDs;
|
|
37
|
+
return [metadata.expressID, ...(Array.isArray(merged) ? merged : [])].filter((id) => typeof id === 'number');
|
|
38
|
+
}
|
|
39
|
+
/** Slack around a wall body within which a touching end counts as inside it. */
|
|
40
|
+
const BODY_TOLERANCE = 0.03;
|
|
41
|
+
function insideBody(point, other, slack = 0) {
|
|
42
|
+
const offset = [point[0] - other.start[0], point[1] - other.start[1]];
|
|
43
|
+
const along = offset[0] * other.dir[0] + offset[1] * other.dir[1];
|
|
44
|
+
return (Math.abs(cross(offset, other.dir)) <= other.thickness / 2 + BODY_TOLERANCE + slack &&
|
|
45
|
+
along >= -BODY_TOLERANCE &&
|
|
46
|
+
along <= other.length + BODY_TOLERANCE);
|
|
47
|
+
}
|
|
48
|
+
/** Signed move of `segment`'s end along its outward axis onto `other`'s centreline. */
|
|
49
|
+
function joinMove(segment, atEnd, other, reach) {
|
|
50
|
+
const point = atEnd ? segment.end : segment.start;
|
|
51
|
+
const out = atEnd ? segment.dir : [-segment.dir[0], -segment.dir[1]];
|
|
52
|
+
const denominator = cross(out, other.dir);
|
|
53
|
+
if (Math.abs(denominator) < MIN_JOIN_SINE)
|
|
54
|
+
return null;
|
|
55
|
+
const toOther = [other.start[0] - point[0], other.start[1] - point[1]];
|
|
56
|
+
const move = cross(toOther, other.dir) / denominator;
|
|
57
|
+
const along = cross(toOther, out) / denominator;
|
|
58
|
+
// The meeting point must lie on the neighbour, or just past an end of it
|
|
59
|
+
// that is itself short of this wall's centreline.
|
|
60
|
+
if (along < -reach || along > other.length + reach)
|
|
61
|
+
return null;
|
|
62
|
+
if (segment.length + move < MIN_JOINED_LENGTH)
|
|
63
|
+
return null;
|
|
64
|
+
return move;
|
|
65
|
+
}
|
|
66
|
+
/** Thickest IFC wall still read as cladding (tiles, skirting) when it lines another wall. */
|
|
67
|
+
const MAX_CLADDING_THICKNESS = 0.035;
|
|
68
|
+
/**
|
|
69
|
+
* Walls that only line or hide inside another wall: tiles, skirtings and thin
|
|
70
|
+
* facings exported as IfcWall along a real wall's face, shorter walls laid
|
|
71
|
+
* face to face along or running through a longer one, and walls whose body
|
|
72
|
+
* lies entirely inside a thicker parallel wall. As Pascal walls they carve
|
|
73
|
+
* sliver rooms or keep corners from closing, so they stay exact meshes.
|
|
74
|
+
*/
|
|
75
|
+
export function redundantWallIds(walls) {
|
|
76
|
+
const hosts = new Map();
|
|
77
|
+
const segments = walls.map(toSegment);
|
|
78
|
+
for (const [index, segment] of segments.entries()) {
|
|
79
|
+
if (!segment)
|
|
80
|
+
continue;
|
|
81
|
+
const cladding = segment.thickness <= MAX_CLADDING_THICKNESS;
|
|
82
|
+
let lined = 0;
|
|
83
|
+
let host;
|
|
84
|
+
for (const [otherIndex, other] of segments.entries()) {
|
|
85
|
+
if (!other || otherIndex === index || hosts.has(other.wall.id))
|
|
86
|
+
continue;
|
|
87
|
+
if (other.wall.parentId !== segment.wall.parentId)
|
|
88
|
+
continue;
|
|
89
|
+
if (Math.abs(cross(segment.dir, other.dir)) > 0.05)
|
|
90
|
+
continue;
|
|
91
|
+
const offset = Math.abs(cross([segment.start[0] - other.start[0], segment.start[1] - other.start[1]], other.dir));
|
|
92
|
+
const project = (point) => (point[0] - other.start[0]) * other.dir[0] + (point[1] - other.start[1]) * other.dir[1];
|
|
93
|
+
const [a, b] = [project(segment.start), project(segment.end)].sort((x, y) => x - y);
|
|
94
|
+
const overlap = Math.max(0, Math.min(b, other.length) - Math.max(a, 0));
|
|
95
|
+
const embedded = other.thickness > segment.thickness &&
|
|
96
|
+
offset + segment.thickness / 2 <= other.thickness / 2 + 0.01 &&
|
|
97
|
+
overlap >= segment.length - 0.05;
|
|
98
|
+
// A shorter wall laid face to face along a longer one (a double
|
|
99
|
+
// partition) would leave a zero-width room between the two bodies.
|
|
100
|
+
// A shorter wall running through another's body (a thickened stretch
|
|
101
|
+
// over a partition) makes two overlapping room boundaries.
|
|
102
|
+
const lining = segment.length < other.length &&
|
|
103
|
+
(Math.abs(offset - (other.thickness + segment.thickness) / 2) <= 0.01 ||
|
|
104
|
+
offset < Math.max(other.thickness, segment.thickness) / 2) &&
|
|
105
|
+
overlap >= segment.length * 0.9;
|
|
106
|
+
if (embedded || lining) {
|
|
107
|
+
lined = segment.length;
|
|
108
|
+
host = other;
|
|
109
|
+
break;
|
|
110
|
+
}
|
|
111
|
+
if (cladding &&
|
|
112
|
+
other.thickness > 2 * segment.thickness &&
|
|
113
|
+
offset <= (other.thickness + segment.thickness) / 2 + 0.02) {
|
|
114
|
+
lined += overlap;
|
|
115
|
+
host ??= other;
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
if (host && lined >= segment.length * 0.5)
|
|
119
|
+
hosts.set(segment.wall.id, host.wall);
|
|
120
|
+
}
|
|
121
|
+
return hosts;
|
|
122
|
+
}
|
|
123
|
+
export function joinWallEnds(nodes, connections = [],
|
|
124
|
+
/** Host wall expressID → thickness of a lining removed from its face (see redundantWallIds). */
|
|
125
|
+
linings = new Map()) {
|
|
126
|
+
const segments = Object.values(nodes)
|
|
127
|
+
.filter((node) => node.type === 'wall')
|
|
128
|
+
.map(toSegment)
|
|
129
|
+
.filter((segment) => segment !== null);
|
|
130
|
+
const wallByExpressId = new Map();
|
|
131
|
+
for (const segment of segments)
|
|
132
|
+
for (const id of wallExpressIds(segment.wall))
|
|
133
|
+
wallByExpressId.set(id, segment.wall.id);
|
|
134
|
+
const connected = new Set();
|
|
135
|
+
for (const [a, b] of connections) {
|
|
136
|
+
const wallA = wallByExpressId.get(a);
|
|
137
|
+
const wallB = wallByExpressId.get(b);
|
|
138
|
+
if (!wallA || !wallB || wallA === wallB)
|
|
139
|
+
continue;
|
|
140
|
+
connected.add(`${wallA}|${wallB}`);
|
|
141
|
+
connected.add(`${wallB}|${wallA}`);
|
|
142
|
+
}
|
|
143
|
+
const byLevel = new Map();
|
|
144
|
+
for (const segment of segments) {
|
|
145
|
+
const key = segment.wall.parentId ?? null;
|
|
146
|
+
byLevel.set(key, [...(byLevel.get(key) ?? []), segment]);
|
|
147
|
+
}
|
|
148
|
+
// Moves are measured on the original geometry: a wall extending along its
|
|
149
|
+
// own axis never changes the centreline its neighbours join onto.
|
|
150
|
+
const moves = [];
|
|
151
|
+
for (const level of byLevel.values()) {
|
|
152
|
+
for (const segment of level) {
|
|
153
|
+
if (isPascalAuthored(segment.wall))
|
|
154
|
+
continue;
|
|
155
|
+
for (const atEnd of [false, true]) {
|
|
156
|
+
const point = atEnd ? segment.end : segment.start;
|
|
157
|
+
let best = null;
|
|
158
|
+
// An end inside another wall's body belongs on that wall's centreline,
|
|
159
|
+
// even past a corner it already makes (a furring lining a party wall).
|
|
160
|
+
let embedded = null;
|
|
161
|
+
for (const other of level) {
|
|
162
|
+
if (other === segment)
|
|
163
|
+
continue;
|
|
164
|
+
const linked = connected.has(`${segment.wall.id}|${other.wall.id}`);
|
|
165
|
+
const limit = linked
|
|
166
|
+
? MAX_CONNECTED_GAP
|
|
167
|
+
: Math.min(MAX_CONNECTED_GAP, other.thickness + GAP_TOLERANCE);
|
|
168
|
+
const reach = linked
|
|
169
|
+
? MAX_CONNECTED_GAP
|
|
170
|
+
: Math.min(MAX_CONNECTED_GAP, Math.max(segment.thickness, other.thickness) + GAP_TOLERANCE);
|
|
171
|
+
const move = joinMove(segment, atEnd, other, reach);
|
|
172
|
+
if (move === null || Math.abs(move) > limit)
|
|
173
|
+
continue;
|
|
174
|
+
const slack = Math.max(0, ...wallExpressIds(other.wall).map((id) => linings.get(id) ?? 0));
|
|
175
|
+
// Ends reach through to the farthest body they touch; an end
|
|
176
|
+
// overshooting every body it touches trims back to the nearest.
|
|
177
|
+
if (insideBody(point, other, slack) &&
|
|
178
|
+
(!embedded ||
|
|
179
|
+
(move > 0 ? move > embedded.move : embedded.move <= 0 && move > embedded.move)))
|
|
180
|
+
embedded = { move };
|
|
181
|
+
// An end already on a centreline stays there; IFC-connected
|
|
182
|
+
// neighbours win ties.
|
|
183
|
+
const score = Math.abs(move) - (linked ? 1e-3 : 0);
|
|
184
|
+
if (!best || score < best.score)
|
|
185
|
+
best = { move, score };
|
|
186
|
+
}
|
|
187
|
+
const chosen = embedded ?? best;
|
|
188
|
+
if (chosen && Math.abs(chosen.move) > 1e-4)
|
|
189
|
+
moves.push({ segment, atEnd, move: chosen.move });
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
for (const { segment, atEnd, move } of moves) {
|
|
194
|
+
const wall = nodes[segment.wall.id];
|
|
195
|
+
if (atEnd) {
|
|
196
|
+
wall.end = [wall.end[0] + segment.dir[0] * move, wall.end[1] + segment.dir[1] * move];
|
|
197
|
+
continue;
|
|
198
|
+
}
|
|
199
|
+
wall.start = [wall.start[0] - segment.dir[0] * move, wall.start[1] - segment.dir[1] * move];
|
|
200
|
+
// Openings are placed by their distance from the wall start.
|
|
201
|
+
for (const childId of wall.children) {
|
|
202
|
+
const child = nodes[childId];
|
|
203
|
+
if (child?.type !== 'door' && child?.type !== 'window')
|
|
204
|
+
continue;
|
|
205
|
+
child.position = [child.position[0] + move, child.position[1], child.position[2]];
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
return moves.length + joinInLineEnds(nodes, segments);
|
|
209
|
+
}
|
|
210
|
+
/** A wall's plan frame: x along start → end, z to its left (wall-local +z). */
|
|
211
|
+
function wallFrame(wall) {
|
|
212
|
+
const length = Math.hypot(wall.end[0] - wall.start[0], wall.end[1] - wall.start[1]) || 1;
|
|
213
|
+
const dir = [
|
|
214
|
+
(wall.end[0] - wall.start[0]) / length,
|
|
215
|
+
(wall.end[1] - wall.start[1]) / length,
|
|
216
|
+
];
|
|
217
|
+
const left = [-dir[1], dir[0]];
|
|
218
|
+
const origin = [wall.start[0], wall.start[1]];
|
|
219
|
+
return {
|
|
220
|
+
toWorld: (along, across) => [
|
|
221
|
+
origin[0] + dir[0] * along + left[0] * across,
|
|
222
|
+
origin[1] + dir[1] * along + left[1] * across,
|
|
223
|
+
],
|
|
224
|
+
toLocal: (point) => {
|
|
225
|
+
const offset = [point[0] - origin[0], point[1] - origin[1]];
|
|
226
|
+
return [offset[0] * dir[0] + offset[1] * dir[1], offset[0] * left[0] + offset[1] * left[1]];
|
|
227
|
+
},
|
|
228
|
+
};
|
|
229
|
+
}
|
|
230
|
+
/**
|
|
231
|
+
* A thinner wall continuing a thicker one flush with one face leaves its end
|
|
232
|
+
* beside the other's centreline, where no L or T can close. The end moves
|
|
233
|
+
* sideways onto the other wall's end; the wall turns by a degree or two and
|
|
234
|
+
* its doors and windows keep their plan position.
|
|
235
|
+
*/
|
|
236
|
+
function joinInLineEnds(nodes, original) {
|
|
237
|
+
const walls = original.map((segment) => nodes[segment.wall.id]);
|
|
238
|
+
const current = walls.map(toSegment);
|
|
239
|
+
const onCentreline = (point, self) => current.some((other) => {
|
|
240
|
+
if (!other || other === self || other.wall.parentId !== self.wall.parentId)
|
|
241
|
+
return false;
|
|
242
|
+
const offset = [point[0] - other.start[0], point[1] - other.start[1]];
|
|
243
|
+
const along = offset[0] * other.dir[0] + offset[1] * other.dir[1];
|
|
244
|
+
return (Math.abs(cross(offset, other.dir)) <= 1e-3 && along >= -1e-3 && along <= other.length + 1e-3);
|
|
245
|
+
});
|
|
246
|
+
let joined = 0;
|
|
247
|
+
for (const segment of current) {
|
|
248
|
+
// Turning a wall would carry its doors and windows off their IFC place.
|
|
249
|
+
if (!segment || isPascalAuthored(segment.wall))
|
|
250
|
+
continue;
|
|
251
|
+
for (const atEnd of [false, true]) {
|
|
252
|
+
const point = atEnd ? segment.end : segment.start;
|
|
253
|
+
if (onCentreline(point, segment))
|
|
254
|
+
continue;
|
|
255
|
+
let best;
|
|
256
|
+
for (const other of current) {
|
|
257
|
+
if (!other || other === segment || other.wall.parentId !== segment.wall.parentId)
|
|
258
|
+
continue;
|
|
259
|
+
if (Math.abs(cross(segment.dir, other.dir)) > 0.03)
|
|
260
|
+
continue;
|
|
261
|
+
const lateral = Math.abs(cross([point[0] - other.start[0], point[1] - other.start[1]], other.dir));
|
|
262
|
+
if (lateral > Math.max(segment.thickness, other.thickness) / 2 + 0.01)
|
|
263
|
+
continue;
|
|
264
|
+
for (const end of [other.start, other.end]) {
|
|
265
|
+
const distance = Math.hypot(end[0] - point[0], end[1] - point[1]);
|
|
266
|
+
const gap = Math.abs((end[0] - point[0]) * other.dir[0] + (end[1] - point[1]) * other.dir[1]);
|
|
267
|
+
if (gap > 0.1 || distance < 1e-4 || distance > segment.length * 0.1)
|
|
268
|
+
continue;
|
|
269
|
+
if (!best || distance < best.distance)
|
|
270
|
+
best = { end, distance };
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
if (!best)
|
|
274
|
+
continue;
|
|
275
|
+
const wall = nodes[segment.wall.id];
|
|
276
|
+
const before = wallFrame(wall);
|
|
277
|
+
if (atEnd)
|
|
278
|
+
wall.end = [...best.end];
|
|
279
|
+
else
|
|
280
|
+
wall.start = [...best.end];
|
|
281
|
+
const after = wallFrame(wall);
|
|
282
|
+
// Doors and windows keep their place: re-expressed in the turned wall.
|
|
283
|
+
for (const id of wall.children) {
|
|
284
|
+
const child = nodes[id];
|
|
285
|
+
if (child?.type !== 'door' && child?.type !== 'window')
|
|
286
|
+
continue;
|
|
287
|
+
const world = before.toWorld(child.position[0], child.position[2]);
|
|
288
|
+
const [along, across] = after.toLocal(world);
|
|
289
|
+
child.position = [along, child.position[1], across];
|
|
290
|
+
}
|
|
291
|
+
segment.start = wall.start;
|
|
292
|
+
segment.end = wall.end;
|
|
293
|
+
joined++;
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
return joined;
|
|
297
|
+
}
|
|
298
|
+
/**
|
|
299
|
+
* Room faces only close at wall junctions: a wall crossing another mid-span
|
|
300
|
+
* (an X, not a T) splits in two at the crossing so both halves meet the other
|
|
301
|
+
* wall's centreline. The thinner wall of the pair is split.
|
|
302
|
+
*/
|
|
303
|
+
export function splitCrossingWalls(nodes) {
|
|
304
|
+
let split = 0;
|
|
305
|
+
for (let changed = true; changed;) {
|
|
306
|
+
changed = false;
|
|
307
|
+
const segments = Object.values(nodes)
|
|
308
|
+
.filter((node) => node.type === 'wall')
|
|
309
|
+
.map(toSegment)
|
|
310
|
+
.filter((segment) => segment !== null);
|
|
311
|
+
outer: for (const a of segments)
|
|
312
|
+
for (const b of segments) {
|
|
313
|
+
if (a === b || a.wall.parentId !== b.wall.parentId || isPascalAuthored(a.wall))
|
|
314
|
+
continue;
|
|
315
|
+
if (a.thickness > b.thickness || (a.thickness === b.thickness && a.wall.id > b.wall.id))
|
|
316
|
+
continue;
|
|
317
|
+
const denominator = cross(a.dir, b.dir);
|
|
318
|
+
if (Math.abs(denominator) < MIN_JOIN_SINE)
|
|
319
|
+
continue;
|
|
320
|
+
const toB = [b.start[0] - a.start[0], b.start[1] - a.start[1]];
|
|
321
|
+
const alongA = cross(toB, b.dir) / denominator;
|
|
322
|
+
const alongB = cross(toB, a.dir) / denominator;
|
|
323
|
+
const margin = Math.max(a.thickness, b.thickness) / 2 + 0.05;
|
|
324
|
+
if (alongA < margin || alongA > a.length - margin)
|
|
325
|
+
continue;
|
|
326
|
+
if (alongB < margin || alongB > b.length - margin)
|
|
327
|
+
continue;
|
|
328
|
+
splitWallAt(nodes, a, alongA);
|
|
329
|
+
split++;
|
|
330
|
+
changed = true;
|
|
331
|
+
break outer;
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
return split;
|
|
335
|
+
}
|
|
336
|
+
function splitWallAt(nodes, segment, along) {
|
|
337
|
+
const wall = nodes[segment.wall.id];
|
|
338
|
+
const point = [
|
|
339
|
+
segment.start[0] + segment.dir[0] * along,
|
|
340
|
+
segment.start[1] + segment.dir[1] * along,
|
|
341
|
+
];
|
|
342
|
+
const second = {
|
|
343
|
+
...wall,
|
|
344
|
+
id: nextId('wall'),
|
|
345
|
+
start: point,
|
|
346
|
+
end: [...wall.end],
|
|
347
|
+
children: [],
|
|
348
|
+
metadata: { ...(wall.metadata ?? {}) },
|
|
349
|
+
};
|
|
350
|
+
wall.end = point;
|
|
351
|
+
for (const childId of [...wall.children]) {
|
|
352
|
+
const child = nodes[childId];
|
|
353
|
+
if (child?.type !== 'door' && child?.type !== 'window')
|
|
354
|
+
continue;
|
|
355
|
+
if (child.position[0] <= along)
|
|
356
|
+
continue;
|
|
357
|
+
child.position = [child.position[0] - along, child.position[1], child.position[2]];
|
|
358
|
+
child.parentId = second.id;
|
|
359
|
+
child.wallId = second.id;
|
|
360
|
+
wall.children = wall.children.filter((id) => id !== childId);
|
|
361
|
+
second.children.push(childId);
|
|
362
|
+
}
|
|
363
|
+
nodes[second.id] = second;
|
|
364
|
+
const parent = wall.parentId ? nodes[wall.parentId] : undefined;
|
|
365
|
+
if (parent && 'children' in parent && Array.isArray(parent.children))
|
|
366
|
+
parent.children.push(second.id);
|
|
367
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@pascal-app/ifc-converter",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.5",
|
|
4
4
|
"description": "IFC → Pascal scene-graph conversion. Pure logic — no DOM, no React.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|
|
@@ -10,6 +10,11 @@
|
|
|
10
10
|
"types": "./dist/index.d.ts",
|
|
11
11
|
"import": "./dist/index.js",
|
|
12
12
|
"default": "./dist/index.js"
|
|
13
|
+
},
|
|
14
|
+
"./export": {
|
|
15
|
+
"types": "./dist/export/index.d.ts",
|
|
16
|
+
"import": "./dist/export/index.js",
|
|
17
|
+
"default": "./dist/export/index.js"
|
|
13
18
|
}
|
|
14
19
|
},
|
|
15
20
|
"files": [
|
|
@@ -23,14 +28,13 @@
|
|
|
23
28
|
"prepublishOnly": "npm run build"
|
|
24
29
|
},
|
|
25
30
|
"dependencies": {
|
|
26
|
-
"@pascal-app/core": "^1.0.
|
|
27
|
-
"
|
|
28
|
-
"web-ifc": "^0.0.77"
|
|
31
|
+
"@pascal-app/core": "^1.0.5",
|
|
32
|
+
"web-ifc": "^0.0.78"
|
|
29
33
|
},
|
|
30
34
|
"devDependencies": {
|
|
31
35
|
"@pascal/typescript-config": "*",
|
|
32
|
-
"@types/bun": "^1.3
|
|
33
|
-
"typescript": "
|
|
36
|
+
"@types/bun": "^1.4.3",
|
|
37
|
+
"typescript": "7.0.2"
|
|
34
38
|
},
|
|
35
39
|
"repository": {
|
|
36
40
|
"type": "git",
|