@pascal-app/core 0.5.1 → 0.7.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/dist/events/bus.d.ts +74 -4
- package/dist/events/bus.d.ts.map +1 -1
- package/dist/events/bus.js +1 -1
- package/dist/hooks/scene-registry/scene-registry.d.ts +2 -0
- package/dist/hooks/scene-registry/scene-registry.d.ts.map +1 -1
- package/dist/hooks/scene-registry/scene-registry.js +2 -0
- package/dist/hooks/spatial-grid/spatial-grid-manager.d.ts.map +1 -1
- package/dist/hooks/spatial-grid/spatial-grid-manager.js +164 -6
- package/dist/hooks/spatial-grid/spatial-grid.d.ts +2 -0
- package/dist/hooks/spatial-grid/spatial-grid.d.ts.map +1 -1
- package/dist/hooks/spatial-grid/spatial-grid.js +43 -20
- package/dist/index.d.ts +9 -13
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +7 -11
- package/dist/lib/door-operation.d.ts +7 -0
- package/dist/lib/door-operation.d.ts.map +1 -0
- package/dist/lib/door-operation.js +25 -0
- package/dist/lib/polygon-geometry.d.ts +3 -0
- package/dist/lib/polygon-geometry.d.ts.map +1 -0
- package/dist/lib/polygon-geometry.js +90 -0
- package/dist/lib/slab-polygon.d.ts +3 -0
- package/dist/lib/slab-polygon.d.ts.map +1 -0
- package/dist/lib/slab-polygon.js +58 -0
- package/dist/lib/space-detection.d.ts +10 -17
- package/dist/lib/space-detection.d.ts.map +1 -1
- package/dist/lib/space-detection.js +666 -453
- package/dist/material-library.d.ts +20 -0
- package/dist/material-library.d.ts.map +1 -0
- package/dist/material-library.js +580 -0
- package/dist/schema/asset-url.d.ts +34 -0
- package/dist/schema/asset-url.d.ts.map +1 -0
- package/dist/schema/asset-url.js +79 -0
- package/dist/schema/asset-url.test.d.ts +2 -0
- package/dist/schema/asset-url.test.d.ts.map +1 -0
- package/dist/schema/asset-url.test.js +138 -0
- package/dist/schema/index.d.ts +14 -7
- package/dist/schema/index.d.ts.map +1 -1
- package/dist/schema/index.js +10 -7
- package/dist/schema/material.d.ts +112 -2
- package/dist/schema/material.d.ts.map +1 -1
- package/dist/schema/material.js +55 -1
- package/dist/schema/nodes/ceiling.d.ts +11 -1
- package/dist/schema/nodes/ceiling.d.ts.map +1 -1
- package/dist/schema/nodes/ceiling.js +6 -0
- package/dist/schema/nodes/column.d.ts +520 -0
- package/dist/schema/nodes/column.d.ts.map +1 -0
- package/dist/schema/nodes/column.js +385 -0
- package/dist/schema/nodes/door.d.ts +74 -1
- package/dist/schema/nodes/door.d.ts.map +1 -1
- package/dist/schema/nodes/door.js +39 -2
- package/dist/schema/nodes/fence.d.ts +34 -0
- package/dist/schema/nodes/fence.d.ts.map +1 -1
- package/dist/schema/nodes/fence.js +5 -0
- package/dist/schema/nodes/guide.d.ts +17 -0
- package/dist/schema/nodes/guide.d.ts.map +1 -1
- package/dist/schema/nodes/guide.js +11 -1
- package/dist/schema/nodes/item.d.ts +10 -2
- package/dist/schema/nodes/item.d.ts.map +1 -1
- package/dist/schema/nodes/item.js +18 -1
- package/dist/schema/nodes/level.d.ts +1 -1
- package/dist/schema/nodes/level.d.ts.map +1 -1
- package/dist/schema/nodes/level.js +6 -0
- package/dist/schema/nodes/roof-segment.d.ts +3 -1
- package/dist/schema/nodes/roof-segment.d.ts.map +1 -1
- package/dist/schema/nodes/roof-segment.js +1 -0
- package/dist/schema/nodes/roof.d.ts +108 -0
- package/dist/schema/nodes/roof.d.ts.map +1 -1
- package/dist/schema/nodes/roof.js +58 -2
- package/dist/schema/nodes/scan.d.ts.map +1 -1
- package/dist/schema/nodes/scan.js +2 -1
- package/dist/schema/nodes/site.d.ts +2 -1
- package/dist/schema/nodes/site.d.ts.map +1 -1
- package/dist/schema/nodes/slab.d.ts +11 -1
- package/dist/schema/nodes/slab.d.ts.map +1 -1
- package/dist/schema/nodes/slab.js +7 -0
- package/dist/schema/nodes/spawn.d.ts +24 -0
- package/dist/schema/nodes/spawn.d.ts.map +1 -0
- package/dist/schema/nodes/spawn.js +8 -0
- package/dist/schema/nodes/stair-segment.d.ts +3 -1
- package/dist/schema/nodes/stair-segment.d.ts.map +1 -1
- package/dist/schema/nodes/stair-segment.js +1 -0
- package/dist/schema/nodes/stair.d.ts +122 -2
- package/dist/schema/nodes/stair.d.ts.map +1 -1
- package/dist/schema/nodes/stair.js +72 -2
- package/dist/schema/nodes/surface-hole-metadata.d.ts +10 -0
- package/dist/schema/nodes/surface-hole-metadata.d.ts.map +1 -0
- package/dist/schema/nodes/surface-hole-metadata.js +5 -0
- package/dist/schema/nodes/wall.d.ts +87 -1
- package/dist/schema/nodes/wall.d.ts.map +1 -1
- package/dist/schema/nodes/wall.js +45 -4
- package/dist/schema/nodes/window.d.ts +57 -1
- package/dist/schema/nodes/window.d.ts.map +1 -1
- package/dist/schema/nodes/window.js +29 -0
- package/dist/schema/types.d.ts +653 -12
- package/dist/schema/types.d.ts.map +1 -1
- package/dist/schema/types.js +4 -0
- package/dist/store/actions/node-actions.d.ts +1 -1
- package/dist/store/actions/node-actions.d.ts.map +1 -1
- package/dist/store/actions/node-actions.js +181 -5
- package/dist/store/history-control.d.ts +14 -0
- package/dist/store/history-control.d.ts.map +1 -0
- package/dist/store/history-control.js +22 -0
- package/dist/store/use-interactive.d.ts +43 -0
- package/dist/store/use-interactive.d.ts.map +1 -1
- package/dist/store/use-interactive.js +66 -0
- package/dist/store/use-scene.d.ts.map +1 -1
- package/dist/store/use-scene.js +307 -3
- package/dist/systems/ceiling/ceiling-system.d.ts.map +1 -1
- package/dist/systems/ceiling/ceiling-system.js +7 -0
- package/dist/systems/fence/fence-system.d.ts.map +1 -1
- package/dist/systems/fence/fence-system.js +106 -39
- package/dist/systems/roof/roof-system.d.ts.map +1 -1
- package/dist/systems/roof/roof-system.js +31 -1
- package/dist/systems/slab/slab-system.d.ts.map +1 -1
- package/dist/systems/slab/slab-system.js +45 -8
- package/dist/systems/stair/stair-opening-sync.d.ts +6 -0
- package/dist/systems/stair/stair-opening-sync.d.ts.map +1 -0
- package/dist/systems/stair/stair-opening-sync.js +576 -0
- package/dist/systems/stair/stair-opening-sync.test.d.ts +2 -0
- package/dist/systems/stair/stair-opening-sync.test.d.ts.map +1 -0
- package/dist/systems/stair/stair-opening-sync.test.js +65 -0
- package/dist/systems/stair/stair-system.d.ts.map +1 -1
- package/dist/systems/stair/stair-system.js +119 -2
- package/dist/systems/wall/wall-curve.d.ts +43 -0
- package/dist/systems/wall/wall-curve.d.ts.map +1 -0
- package/dist/systems/wall/wall-curve.js +176 -0
- package/dist/systems/wall/wall-footprint.d.ts.map +1 -1
- package/dist/systems/wall/wall-footprint.js +16 -2
- package/dist/systems/wall/wall-mitering.d.ts +7 -0
- package/dist/systems/wall/wall-mitering.d.ts.map +1 -1
- package/dist/systems/wall/wall-mitering.js +76 -3
- package/dist/systems/wall/wall-system.d.ts.map +1 -1
- package/dist/systems/wall/wall-system.js +202 -2
- package/package.json +33 -5
|
@@ -0,0 +1,576 @@
|
|
|
1
|
+
import { resolveLevelId } from '../../hooks/spatial-grid/spatial-grid-sync';
|
|
2
|
+
import { DEFAULT_WALL_HEIGHT } from '../wall/wall-footprint';
|
|
3
|
+
const CURVED_STAIR_SLAB_OPENING_RATIO = 0.9;
|
|
4
|
+
const STRAIGHT_STAIR_TARGET_THRESHOLD_MIN = 0.35;
|
|
5
|
+
const STAIR_SLAB_OPENING_TIGHTENING = 0;
|
|
6
|
+
const CURVED_STAIR_OPENING_STEP_PADDING = 3;
|
|
7
|
+
function clamp(value, min, max) {
|
|
8
|
+
return Math.min(max, Math.max(min, value));
|
|
9
|
+
}
|
|
10
|
+
function pointsEqual(a, b, tolerance = 1e-5) {
|
|
11
|
+
const dx = a[0] - b[0];
|
|
12
|
+
const dz = a[1] - b[1];
|
|
13
|
+
return dx * dx + dz * dz <= tolerance * tolerance;
|
|
14
|
+
}
|
|
15
|
+
function polygonsEqual(left, right) {
|
|
16
|
+
if (left.length !== right.length)
|
|
17
|
+
return false;
|
|
18
|
+
return left.every((polygon, polygonIndex) => {
|
|
19
|
+
const other = right[polygonIndex];
|
|
20
|
+
if (!(other && polygon.length === other.length))
|
|
21
|
+
return false;
|
|
22
|
+
return polygon.every((point, pointIndex) => {
|
|
23
|
+
const otherPoint = other[pointIndex];
|
|
24
|
+
if (!otherPoint)
|
|
25
|
+
return false;
|
|
26
|
+
return pointsEqual(point, otherPoint);
|
|
27
|
+
});
|
|
28
|
+
});
|
|
29
|
+
}
|
|
30
|
+
function metadataEqual(left, right) {
|
|
31
|
+
if (left.length !== right.length)
|
|
32
|
+
return false;
|
|
33
|
+
return left.every((entry, index) => entry.source === right[index]?.source &&
|
|
34
|
+
(entry.stairId ?? null) === (right[index]?.stairId ?? null));
|
|
35
|
+
}
|
|
36
|
+
function normalizeExistingMetadata(holes, metadata) {
|
|
37
|
+
return holes.map((_, index) => metadata?.[index] ?? { source: 'manual' });
|
|
38
|
+
}
|
|
39
|
+
function expandPolygonFromCentroid(polygon, offset) {
|
|
40
|
+
if (Math.abs(offset) < 1e-6) {
|
|
41
|
+
return polygon.map(([x, z]) => [x, z]);
|
|
42
|
+
}
|
|
43
|
+
const centroid = polygon.reduce((acc, [x, z]) => {
|
|
44
|
+
acc.x += x;
|
|
45
|
+
acc.z += z;
|
|
46
|
+
return acc;
|
|
47
|
+
}, { x: 0, z: 0 });
|
|
48
|
+
centroid.x /= Math.max(polygon.length, 1);
|
|
49
|
+
centroid.z /= Math.max(polygon.length, 1);
|
|
50
|
+
return polygon.map(([x, z]) => {
|
|
51
|
+
const dx = x - centroid.x;
|
|
52
|
+
const dz = z - centroid.z;
|
|
53
|
+
const length = Math.hypot(dx, dz);
|
|
54
|
+
if (length < 1e-6) {
|
|
55
|
+
return [x, z];
|
|
56
|
+
}
|
|
57
|
+
const scale = Math.max(0.1, (length + offset) / length);
|
|
58
|
+
return [centroid.x + dx * scale, centroid.z + dz * scale];
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
function rotateXZ(x, z, angle) {
|
|
62
|
+
const cos = Math.cos(angle);
|
|
63
|
+
const sin = Math.sin(angle);
|
|
64
|
+
return [x * cos + z * sin, -x * sin + z * cos];
|
|
65
|
+
}
|
|
66
|
+
function computeSegmentTransforms(segments) {
|
|
67
|
+
const transforms = [];
|
|
68
|
+
let currentX = 0;
|
|
69
|
+
let currentY = 0;
|
|
70
|
+
let currentZ = 0;
|
|
71
|
+
let currentRot = 0;
|
|
72
|
+
for (let index = 0; index < segments.length; index++) {
|
|
73
|
+
const segment = segments[index];
|
|
74
|
+
if (!segment)
|
|
75
|
+
continue;
|
|
76
|
+
if (index === 0) {
|
|
77
|
+
transforms.push({
|
|
78
|
+
position: [currentX, currentY, currentZ],
|
|
79
|
+
rotation: currentRot,
|
|
80
|
+
});
|
|
81
|
+
continue;
|
|
82
|
+
}
|
|
83
|
+
const previous = segments[index - 1];
|
|
84
|
+
if (!previous)
|
|
85
|
+
continue;
|
|
86
|
+
let attachX = 0;
|
|
87
|
+
let attachZ = 0;
|
|
88
|
+
let rotationDelta = 0;
|
|
89
|
+
switch (segment.attachmentSide) {
|
|
90
|
+
case 'front':
|
|
91
|
+
attachX = 0;
|
|
92
|
+
attachZ = previous.length;
|
|
93
|
+
break;
|
|
94
|
+
case 'left':
|
|
95
|
+
attachX = previous.width / 2;
|
|
96
|
+
attachZ = previous.length / 2;
|
|
97
|
+
rotationDelta = Math.PI / 2;
|
|
98
|
+
break;
|
|
99
|
+
case 'right':
|
|
100
|
+
attachX = -previous.width / 2;
|
|
101
|
+
attachZ = previous.length / 2;
|
|
102
|
+
rotationDelta = -Math.PI / 2;
|
|
103
|
+
break;
|
|
104
|
+
}
|
|
105
|
+
const [deltaX, deltaZ] = rotateXZ(attachX, attachZ, currentRot);
|
|
106
|
+
currentX += deltaX;
|
|
107
|
+
currentY += previous.height;
|
|
108
|
+
currentZ += deltaZ;
|
|
109
|
+
currentRot += rotationDelta;
|
|
110
|
+
transforms.push({
|
|
111
|
+
position: [currentX, currentY, currentZ],
|
|
112
|
+
rotation: currentRot,
|
|
113
|
+
});
|
|
114
|
+
}
|
|
115
|
+
return transforms;
|
|
116
|
+
}
|
|
117
|
+
function getLevelNumber(levelId, nodes) {
|
|
118
|
+
if (!levelId)
|
|
119
|
+
return undefined;
|
|
120
|
+
const node = nodes[levelId];
|
|
121
|
+
return node?.type === 'level' ? node.level : undefined;
|
|
122
|
+
}
|
|
123
|
+
function getResolvedStairLevelIds(stair, nodes) {
|
|
124
|
+
const parentLevelId = resolveLevelId(stair, nodes);
|
|
125
|
+
const fromLevelId = stair.fromLevelId ?? parentLevelId;
|
|
126
|
+
const toLevelId = stair.toLevelId ?? fromLevelId;
|
|
127
|
+
return { fromLevelId, toLevelId };
|
|
128
|
+
}
|
|
129
|
+
function resolveStraightSegments(stair, nodes) {
|
|
130
|
+
return (stair.children ?? [])
|
|
131
|
+
.map((childId) => nodes[childId])
|
|
132
|
+
.filter((segment) => segment?.type === 'stair-segment' && segment.visible !== false);
|
|
133
|
+
}
|
|
134
|
+
function toWorldPlanPoint(stair, localX, localZ) {
|
|
135
|
+
const [worldX, worldZ] = rotateXZ(localX, localZ, stair.rotation ?? 0);
|
|
136
|
+
return [stair.position[0] + worldX, stair.position[2] + worldZ];
|
|
137
|
+
}
|
|
138
|
+
function getStraightStairLayouts(stair, nodes) {
|
|
139
|
+
const segments = resolveStraightSegments(stair, nodes);
|
|
140
|
+
const transforms = computeSegmentTransforms(segments);
|
|
141
|
+
return segments.map((segment, index) => {
|
|
142
|
+
const transform = transforms[index] ?? {
|
|
143
|
+
position: [0, 0, 0],
|
|
144
|
+
rotation: 0,
|
|
145
|
+
};
|
|
146
|
+
return {
|
|
147
|
+
segment,
|
|
148
|
+
transform,
|
|
149
|
+
topElevation: transform.position[1] + (segment.segmentType === 'stair' ? segment.height : 0),
|
|
150
|
+
};
|
|
151
|
+
});
|
|
152
|
+
}
|
|
153
|
+
function getStraightSegmentFootprintPolygon(stair, layout) {
|
|
154
|
+
return getStraightSegmentSlicePolygon(stair, layout, 0, layout.segment.length);
|
|
155
|
+
}
|
|
156
|
+
function getStraightSegmentLocalSlicePolygon(layout, startAlong, endAlong) {
|
|
157
|
+
const { segment, transform } = layout;
|
|
158
|
+
const clampedStart = clamp(startAlong, 0, segment.length);
|
|
159
|
+
const clampedEnd = clamp(endAlong, clampedStart, segment.length);
|
|
160
|
+
const sliceLength = Math.max(clampedEnd - clampedStart, 1e-4);
|
|
161
|
+
const sliceCenterAlong = clampedStart + sliceLength / 2;
|
|
162
|
+
const [centerOffsetX, centerOffsetZ] = rotateXZ(0, sliceCenterAlong, transform.rotation);
|
|
163
|
+
const centerX = transform.position[0] + centerOffsetX;
|
|
164
|
+
const centerZ = transform.position[2] + centerOffsetZ;
|
|
165
|
+
const halfWidth = segment.width / 2;
|
|
166
|
+
const halfLength = sliceLength / 2;
|
|
167
|
+
const corners = [
|
|
168
|
+
[-halfWidth, -halfLength],
|
|
169
|
+
[halfWidth, -halfLength],
|
|
170
|
+
[halfWidth, halfLength],
|
|
171
|
+
[-halfWidth, halfLength],
|
|
172
|
+
];
|
|
173
|
+
return corners.map(([localWidth, localLength]) => {
|
|
174
|
+
const [offsetX, offsetZ] = rotateXZ(localWidth, localLength, transform.rotation);
|
|
175
|
+
return [centerX + offsetX, centerZ + offsetZ];
|
|
176
|
+
});
|
|
177
|
+
}
|
|
178
|
+
function getStraightSegmentSlicePolygon(stair, layout, startAlong, endAlong) {
|
|
179
|
+
return getStraightSegmentLocalSlicePolygon(layout, startAlong, endAlong).map(([x, z]) => toWorldPlanPoint(stair, x, z));
|
|
180
|
+
}
|
|
181
|
+
function getStraightFlightOpeningDepth(stair, segment) {
|
|
182
|
+
const treadDepth = Math.max(0.2, segment.length / Math.max(segment.stepCount || stair.stepCount || 10, 1));
|
|
183
|
+
return Math.min(segment.length, Math.max(treadDepth * 6, segment.length * 0.62, 1.8));
|
|
184
|
+
}
|
|
185
|
+
function polygonArea(points) {
|
|
186
|
+
let area = 0;
|
|
187
|
+
for (let index = 0; index < points.length; index += 1) {
|
|
188
|
+
const current = points[index];
|
|
189
|
+
const next = points[(index + 1) % points.length];
|
|
190
|
+
if (!current || !next)
|
|
191
|
+
continue;
|
|
192
|
+
area += current[0] * next[1] - next[0] * current[1];
|
|
193
|
+
}
|
|
194
|
+
return area / 2;
|
|
195
|
+
}
|
|
196
|
+
function pointOnSegment(point, a, b, tolerance = 1e-6) {
|
|
197
|
+
const cross = (point[1] - a[1]) * (b[0] - a[0]) - (point[0] - a[0]) * (b[1] - a[1]);
|
|
198
|
+
if (Math.abs(cross) > tolerance)
|
|
199
|
+
return false;
|
|
200
|
+
const dot = (point[0] - a[0]) * (b[0] - a[0]) + (point[1] - a[1]) * (b[1] - a[1]);
|
|
201
|
+
if (dot < -tolerance)
|
|
202
|
+
return false;
|
|
203
|
+
const lenSq = (b[0] - a[0]) ** 2 + (b[1] - a[1]) ** 2;
|
|
204
|
+
return dot <= lenSq + tolerance;
|
|
205
|
+
}
|
|
206
|
+
function pointInPolygon(point, polygon) {
|
|
207
|
+
if (polygon.length < 3)
|
|
208
|
+
return false;
|
|
209
|
+
let inside = false;
|
|
210
|
+
const [x, z] = point;
|
|
211
|
+
for (let i = 0, j = polygon.length - 1; i < polygon.length; j = i++) {
|
|
212
|
+
const a = polygon[i];
|
|
213
|
+
const b = polygon[j];
|
|
214
|
+
if (pointOnSegment(point, a, b))
|
|
215
|
+
return true;
|
|
216
|
+
const intersects = a[1] > z !== b[1] > z && x < ((b[0] - a[0]) * (z - a[1])) / (b[1] - a[1]) + a[0];
|
|
217
|
+
if (intersects)
|
|
218
|
+
inside = !inside;
|
|
219
|
+
}
|
|
220
|
+
return inside;
|
|
221
|
+
}
|
|
222
|
+
function polygonContainsPolygon(outer, inner) {
|
|
223
|
+
return inner.every((point) => pointInPolygon(point, outer));
|
|
224
|
+
}
|
|
225
|
+
function getAxisAlignedRectFromPolygon(polygon) {
|
|
226
|
+
if (polygon.length < 4)
|
|
227
|
+
return null;
|
|
228
|
+
const xs = polygon.map(([x]) => x);
|
|
229
|
+
const zs = polygon.map(([, z]) => z);
|
|
230
|
+
const minX = Math.min(...xs);
|
|
231
|
+
const maxX = Math.max(...xs);
|
|
232
|
+
const minZ = Math.min(...zs);
|
|
233
|
+
const maxZ = Math.max(...zs);
|
|
234
|
+
if (!(maxX > minX && maxZ > minZ))
|
|
235
|
+
return null;
|
|
236
|
+
return { minX, maxX, minZ, maxZ };
|
|
237
|
+
}
|
|
238
|
+
function expandRect(rect, offset) {
|
|
239
|
+
if (offset <= 1e-6) {
|
|
240
|
+
return rect;
|
|
241
|
+
}
|
|
242
|
+
return {
|
|
243
|
+
minX: rect.minX - offset,
|
|
244
|
+
maxX: rect.maxX + offset,
|
|
245
|
+
minZ: rect.minZ - offset,
|
|
246
|
+
maxZ: rect.maxZ + offset,
|
|
247
|
+
};
|
|
248
|
+
}
|
|
249
|
+
function buildUnionPolygonsFromRects(rects) {
|
|
250
|
+
if (rects.length === 0)
|
|
251
|
+
return [];
|
|
252
|
+
const xs = Array.from(new Set(rects.flatMap((rect) => [rect.minX, rect.maxX]).map((value) => Number(value.toFixed(6))))).sort((a, b) => a - b);
|
|
253
|
+
const zs = Array.from(new Set(rects.flatMap((rect) => [rect.minZ, rect.maxZ]).map((value) => Number(value.toFixed(6))))).sort((a, b) => a - b);
|
|
254
|
+
if (xs.length < 2 || zs.length < 2)
|
|
255
|
+
return [];
|
|
256
|
+
const occupied = new Set();
|
|
257
|
+
for (let xi = 0; xi < xs.length - 1; xi += 1) {
|
|
258
|
+
for (let zi = 0; zi < zs.length - 1; zi += 1) {
|
|
259
|
+
const cx = (xs[xi] + xs[xi + 1]) / 2;
|
|
260
|
+
const cz = (zs[zi] + zs[zi + 1]) / 2;
|
|
261
|
+
if (rects.some((rect) => cx > rect.minX && cx < rect.maxX && cz > rect.minZ && cz < rect.maxZ)) {
|
|
262
|
+
occupied.add(`${xi}:${zi}`);
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
const edgeMap = new Map();
|
|
267
|
+
const addEdge = (start, end) => {
|
|
268
|
+
edgeMap.set(`${start[0]},${start[1]}`, end);
|
|
269
|
+
};
|
|
270
|
+
for (let xi = 0; xi < xs.length - 1; xi += 1) {
|
|
271
|
+
for (let zi = 0; zi < zs.length - 1; zi += 1) {
|
|
272
|
+
if (!occupied.has(`${xi}:${zi}`))
|
|
273
|
+
continue;
|
|
274
|
+
const x0 = xs[xi];
|
|
275
|
+
const x1 = xs[xi + 1];
|
|
276
|
+
const z0 = zs[zi];
|
|
277
|
+
const z1 = zs[zi + 1];
|
|
278
|
+
if (!occupied.has(`${xi}:${zi - 1}`))
|
|
279
|
+
addEdge([x0, z0], [x1, z0]);
|
|
280
|
+
if (!occupied.has(`${xi + 1}:${zi}`))
|
|
281
|
+
addEdge([x1, z0], [x1, z1]);
|
|
282
|
+
if (!occupied.has(`${xi}:${zi + 1}`))
|
|
283
|
+
addEdge([x1, z1], [x0, z1]);
|
|
284
|
+
if (!occupied.has(`${xi - 1}:${zi}`))
|
|
285
|
+
addEdge([x0, z1], [x0, z0]);
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
const polygons = [];
|
|
289
|
+
while (edgeMap.size > 0) {
|
|
290
|
+
const firstEntry = edgeMap.entries().next().value;
|
|
291
|
+
if (!firstEntry)
|
|
292
|
+
break;
|
|
293
|
+
const [startKey] = firstEntry;
|
|
294
|
+
const startParts = startKey.split(',').map(Number);
|
|
295
|
+
const sx = startParts[0];
|
|
296
|
+
const sz = startParts[1];
|
|
297
|
+
if (sx === undefined || sz === undefined) {
|
|
298
|
+
edgeMap.delete(startKey);
|
|
299
|
+
continue;
|
|
300
|
+
}
|
|
301
|
+
const start = [sx, sz];
|
|
302
|
+
const polygon = [start];
|
|
303
|
+
let current = start;
|
|
304
|
+
while (true) {
|
|
305
|
+
const currentKey = `${current[0]},${current[1]}`;
|
|
306
|
+
const next = edgeMap.get(currentKey);
|
|
307
|
+
if (!next)
|
|
308
|
+
break;
|
|
309
|
+
edgeMap.delete(currentKey);
|
|
310
|
+
if (pointsEqual(next, start)) {
|
|
311
|
+
break;
|
|
312
|
+
}
|
|
313
|
+
polygon.push(next);
|
|
314
|
+
current = next;
|
|
315
|
+
}
|
|
316
|
+
if (polygon.length >= 3) {
|
|
317
|
+
polygons.push(polygonArea(polygon) < 0 ? [...polygon].reverse() : polygon);
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
return polygons;
|
|
321
|
+
}
|
|
322
|
+
function getCurvedOpeningStepCount(stair, innerRadius, outerRadius, totalSweep) {
|
|
323
|
+
const stepCount = Math.max(2, Math.round(stair.stepCount ?? 10));
|
|
324
|
+
const stepSweep = Math.abs(totalSweep) / stepCount;
|
|
325
|
+
const midRadius = Math.max((innerRadius + outerRadius) * 0.5, 0.01);
|
|
326
|
+
const treadDepth = Math.max(stepSweep * midRadius, 0.2);
|
|
327
|
+
return Math.min(stepCount, Math.max(1, Math.ceil(1.8 / treadDepth), Math.ceil(stepCount * CURVED_STAIR_SLAB_OPENING_RATIO)));
|
|
328
|
+
}
|
|
329
|
+
function buildArcOpeningPolygon(stair, innerRadius, outerRadius, startAngle, endAngle) {
|
|
330
|
+
const sweep = endAngle - startAngle;
|
|
331
|
+
const segmentCount = Math.max(10, Math.min(32, Math.ceil(Math.abs(sweep) / (Math.PI / 24) + Math.max(stair.stepCount ?? 1, 1) * 0.5)));
|
|
332
|
+
const outerPoints = [];
|
|
333
|
+
const innerPoints = [];
|
|
334
|
+
for (let index = 0; index <= segmentCount; index++) {
|
|
335
|
+
const t = index / segmentCount;
|
|
336
|
+
const angle = startAngle + sweep * t;
|
|
337
|
+
outerPoints.push(toWorldPlanPoint(stair, Math.cos(angle) * outerRadius, Math.sin(angle) * outerRadius));
|
|
338
|
+
}
|
|
339
|
+
for (let index = segmentCount; index >= 0; index--) {
|
|
340
|
+
const t = index / segmentCount;
|
|
341
|
+
const angle = startAngle + sweep * t;
|
|
342
|
+
innerPoints.push(toWorldPlanPoint(stair, Math.cos(angle) * innerRadius, Math.sin(angle) * innerRadius));
|
|
343
|
+
}
|
|
344
|
+
return [...outerPoints, ...innerPoints];
|
|
345
|
+
}
|
|
346
|
+
function getCurvedOpeningPolygon(stair, targetElevation) {
|
|
347
|
+
const width = Math.max(stair.width ?? 1, 0.4);
|
|
348
|
+
const innerRadius = Math.max(0.2, stair.innerRadius ?? 0.9);
|
|
349
|
+
const outerRadius = innerRadius + width;
|
|
350
|
+
const totalSweep = stair.sweepAngle ?? Math.PI / 2;
|
|
351
|
+
const stepCount = Math.max(2, Math.round(stair.stepCount ?? 10));
|
|
352
|
+
const stepHeight = Math.max(stair.totalRise ?? 2.5, 0.1) / stepCount;
|
|
353
|
+
const stepSweep = totalSweep / stepCount;
|
|
354
|
+
const targetThreshold = Math.max(stepHeight * 2, STRAIGHT_STAIR_TARGET_THRESHOLD_MIN);
|
|
355
|
+
const endAngle = totalSweep / 2;
|
|
356
|
+
const fallbackStartStepIndex = Math.max(0, stepCount - getCurvedOpeningStepCount(stair, innerRadius, outerRadius, totalSweep));
|
|
357
|
+
let startStepIndex = fallbackStartStepIndex;
|
|
358
|
+
if (typeof targetElevation === 'number') {
|
|
359
|
+
for (let index = 0; index < stepCount; index += 1) {
|
|
360
|
+
const stepTopElevation = stepHeight * (index + 1);
|
|
361
|
+
if (stepTopElevation >= targetElevation - targetThreshold) {
|
|
362
|
+
startStepIndex = Math.max(0, Math.min(fallbackStartStepIndex, index - CURVED_STAIR_OPENING_STEP_PADDING));
|
|
363
|
+
break;
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
const startAngle = -totalSweep / 2 + stepSweep * startStepIndex;
|
|
368
|
+
return buildArcOpeningPolygon(stair, innerRadius, outerRadius, startAngle, endAngle);
|
|
369
|
+
}
|
|
370
|
+
function getSpiralOpeningPolygon(stair) {
|
|
371
|
+
const radius = Math.max(0.05, stair.innerRadius ?? 0.9) + Math.max(stair.width ?? 1, 0.4);
|
|
372
|
+
const segmentCount = 48;
|
|
373
|
+
return Array.from({ length: segmentCount }).map((_, index) => {
|
|
374
|
+
const angle = (index / segmentCount) * Math.PI * 2;
|
|
375
|
+
return toWorldPlanPoint(stair, Math.cos(angle) * radius, Math.sin(angle) * radius);
|
|
376
|
+
});
|
|
377
|
+
}
|
|
378
|
+
function getStraightOpeningPolygonsForSurface(stair, nodes, targetElevation) {
|
|
379
|
+
const layouts = getStraightStairLayouts(stair, nodes);
|
|
380
|
+
if (layouts.length === 0)
|
|
381
|
+
return [];
|
|
382
|
+
const riserHeight = (stair.totalRise ?? 2.5) / Math.max(stair.stepCount ?? 10, 1);
|
|
383
|
+
const targetThreshold = Math.max(riserHeight * 2, STRAIGHT_STAIR_TARGET_THRESHOLD_MIN);
|
|
384
|
+
const openingOffset = Math.max(stair.openingOffset ?? 0, 0);
|
|
385
|
+
const openingRects = [];
|
|
386
|
+
for (let index = 0; index < layouts.length; index += 1) {
|
|
387
|
+
const layout = layouts[index];
|
|
388
|
+
if (!layout)
|
|
389
|
+
continue;
|
|
390
|
+
const { segment, transform } = layout;
|
|
391
|
+
const segmentStartElevation = transform.position[1];
|
|
392
|
+
const segmentTopElevation = layout.topElevation;
|
|
393
|
+
if (segment.segmentType === 'stair') {
|
|
394
|
+
if (Math.abs(targetElevation - segmentTopElevation) <= targetThreshold) {
|
|
395
|
+
const openingDepth = getStraightFlightOpeningDepth(stair, segment);
|
|
396
|
+
const flightRect = getAxisAlignedRectFromPolygon(getStraightSegmentLocalSlicePolygon(layout, Math.max(0, segment.length - openingDepth), segment.length));
|
|
397
|
+
if (flightRect)
|
|
398
|
+
openingRects.push(expandRect(flightRect, openingOffset));
|
|
399
|
+
}
|
|
400
|
+
continue;
|
|
401
|
+
}
|
|
402
|
+
if (Math.abs(targetElevation - segmentStartElevation) > targetThreshold) {
|
|
403
|
+
continue;
|
|
404
|
+
}
|
|
405
|
+
const landingRects = [];
|
|
406
|
+
const landingRect = getAxisAlignedRectFromPolygon(getStraightSegmentLocalSlicePolygon(layout, 0, layout.segment.length));
|
|
407
|
+
if (landingRect)
|
|
408
|
+
landingRects.push(expandRect(landingRect, openingOffset));
|
|
409
|
+
const previous = layouts[index - 1];
|
|
410
|
+
if (previous?.segment.segmentType === 'stair') {
|
|
411
|
+
const previousTopElevation = previous.topElevation;
|
|
412
|
+
if (Math.abs(targetElevation - previousTopElevation) <= targetThreshold) {
|
|
413
|
+
const previousDepth = getStraightFlightOpeningDepth(stair, previous.segment);
|
|
414
|
+
const previousRect = getAxisAlignedRectFromPolygon(getStraightSegmentLocalSlicePolygon(previous, Math.max(0, previous.segment.length - previousDepth), previous.segment.length));
|
|
415
|
+
if (previousRect)
|
|
416
|
+
landingRects.push(expandRect(previousRect, openingOffset));
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
openingRects.push(...landingRects);
|
|
420
|
+
}
|
|
421
|
+
if (openingRects.length > 0) {
|
|
422
|
+
const unionPolygons = buildUnionPolygonsFromRects(openingRects).map((polygon) => polygon.map(([x, z]) => toWorldPlanPoint(stair, x, z)));
|
|
423
|
+
if (unionPolygons.length > 0) {
|
|
424
|
+
return unionPolygons;
|
|
425
|
+
}
|
|
426
|
+
}
|
|
427
|
+
let fallbackLayout = layouts[layouts.length - 1];
|
|
428
|
+
for (let index = layouts.length - 1; index >= 0; index -= 1) {
|
|
429
|
+
const layout = layouts[index];
|
|
430
|
+
if (layout?.segment.segmentType === 'stair') {
|
|
431
|
+
fallbackLayout = layout;
|
|
432
|
+
break;
|
|
433
|
+
}
|
|
434
|
+
}
|
|
435
|
+
return fallbackLayout ? [getStraightSegmentFootprintPolygon(stair, fallbackLayout)] : [];
|
|
436
|
+
}
|
|
437
|
+
function getStairOpeningPolygons(stair, nodes, targetElevation) {
|
|
438
|
+
if ((stair.slabOpeningMode ?? 'none') !== 'destination') {
|
|
439
|
+
return [];
|
|
440
|
+
}
|
|
441
|
+
if (stair.stairType === 'curved') {
|
|
442
|
+
return [getCurvedOpeningPolygon(stair, targetElevation)];
|
|
443
|
+
}
|
|
444
|
+
if (stair.stairType === 'spiral') {
|
|
445
|
+
return [getSpiralOpeningPolygon(stair)];
|
|
446
|
+
}
|
|
447
|
+
if (typeof targetElevation === 'number') {
|
|
448
|
+
return getStraightOpeningPolygonsForSurface(stair, nodes, targetElevation);
|
|
449
|
+
}
|
|
450
|
+
return getStraightOpeningPolygonsForSurface(stair, nodes, Math.max(...getStraightStairLayouts(stair, nodes).map((layout) => layout.topElevation), 0));
|
|
451
|
+
}
|
|
452
|
+
function getTargetSlabElevationForStair(stair, slab, slabLevelId, nodes) {
|
|
453
|
+
const { fromLevelId } = getResolvedStairLevelIds(stair, nodes);
|
|
454
|
+
const fromLevel = getLevelNumber(fromLevelId, nodes);
|
|
455
|
+
const slabLevel = getLevelNumber(slabLevelId, nodes);
|
|
456
|
+
if (fromLevel === undefined || slabLevel === undefined) {
|
|
457
|
+
return slab.elevation ?? 0.05;
|
|
458
|
+
}
|
|
459
|
+
return ((slabLevel - fromLevel) * DEFAULT_WALL_HEIGHT +
|
|
460
|
+
(slab.elevation ?? 0.05) -
|
|
461
|
+
(stair.position[1] ?? 0));
|
|
462
|
+
}
|
|
463
|
+
function getTargetCeilingElevationForStair(stair, ceiling, ceilingLevelId, nodes) {
|
|
464
|
+
const { fromLevelId } = getResolvedStairLevelIds(stair, nodes);
|
|
465
|
+
const fromLevel = getLevelNumber(fromLevelId, nodes);
|
|
466
|
+
const ceilingLevel = getLevelNumber(ceilingLevelId, nodes);
|
|
467
|
+
if (fromLevel === undefined || ceilingLevel === undefined) {
|
|
468
|
+
return ceiling.height ?? DEFAULT_WALL_HEIGHT;
|
|
469
|
+
}
|
|
470
|
+
return ((ceilingLevel - fromLevel) * DEFAULT_WALL_HEIGHT +
|
|
471
|
+
(ceiling.height ?? DEFAULT_WALL_HEIGHT) -
|
|
472
|
+
(stair.position[1] ?? 0));
|
|
473
|
+
}
|
|
474
|
+
function shouldApplyStairToSlab(stair, slabLevelId, nodes) {
|
|
475
|
+
const { fromLevelId, toLevelId } = getResolvedStairLevelIds(stair, nodes);
|
|
476
|
+
const fromLevel = getLevelNumber(fromLevelId, nodes);
|
|
477
|
+
const toLevel = getLevelNumber(toLevelId, nodes);
|
|
478
|
+
const slabLevel = getLevelNumber(slabLevelId, nodes);
|
|
479
|
+
if (slabLevel === undefined) {
|
|
480
|
+
return toLevelId === slabLevelId;
|
|
481
|
+
}
|
|
482
|
+
if (fromLevel === undefined || toLevel === undefined) {
|
|
483
|
+
return toLevelId === slabLevelId;
|
|
484
|
+
}
|
|
485
|
+
const minLevel = Math.min(fromLevel, toLevel);
|
|
486
|
+
const maxLevel = Math.max(fromLevel, toLevel);
|
|
487
|
+
return slabLevel > minLevel && slabLevel <= maxLevel;
|
|
488
|
+
}
|
|
489
|
+
function shouldApplyStairToCeiling(stair, ceilingLevelId, nodes) {
|
|
490
|
+
const { fromLevelId, toLevelId } = getResolvedStairLevelIds(stair, nodes);
|
|
491
|
+
const fromLevel = getLevelNumber(fromLevelId, nodes);
|
|
492
|
+
const toLevel = getLevelNumber(toLevelId, nodes);
|
|
493
|
+
const ceilingLevel = getLevelNumber(ceilingLevelId, nodes);
|
|
494
|
+
if (ceilingLevel === undefined) {
|
|
495
|
+
return fromLevelId === ceilingLevelId;
|
|
496
|
+
}
|
|
497
|
+
if (fromLevel === undefined || toLevel === undefined) {
|
|
498
|
+
return fromLevelId === ceilingLevelId;
|
|
499
|
+
}
|
|
500
|
+
const minLevel = Math.min(fromLevel, toLevel);
|
|
501
|
+
const maxLevel = Math.max(fromLevel, toLevel);
|
|
502
|
+
return ceilingLevel >= minLevel && ceilingLevel < maxLevel;
|
|
503
|
+
}
|
|
504
|
+
export function syncAutoStairOpenings(nodes) {
|
|
505
|
+
const stairs = Object.values(nodes).filter((node) => node.type === 'stair' && node.visible !== false);
|
|
506
|
+
const slabs = Object.values(nodes).filter((node) => node.type === 'slab');
|
|
507
|
+
const ceilings = Object.values(nodes).filter((node) => node.type === 'ceiling');
|
|
508
|
+
const updates = [];
|
|
509
|
+
for (const slab of slabs) {
|
|
510
|
+
const slabLevelId = resolveLevelId(slab, nodes);
|
|
511
|
+
const existingHoles = slab.holes ?? [];
|
|
512
|
+
const existingMetadata = normalizeExistingMetadata(existingHoles, slab.holeMetadata);
|
|
513
|
+
const manualHoles = existingHoles.filter((_hole, index) => existingMetadata[index]?.source !== 'stair');
|
|
514
|
+
const manualMetadata = existingMetadata
|
|
515
|
+
.filter((entry) => entry.source !== 'stair')
|
|
516
|
+
.map((entry) => ({ ...entry }));
|
|
517
|
+
const stairHoles = stairs
|
|
518
|
+
.filter((stair) => shouldApplyStairToSlab(stair, slabLevelId, nodes))
|
|
519
|
+
.flatMap((stair) => getStairOpeningPolygons(stair, nodes, getTargetSlabElevationForStair(stair, slab, slabLevelId, nodes)).map((polygon) => ({
|
|
520
|
+
polygon: stair.stairType === 'straight'
|
|
521
|
+
? polygon
|
|
522
|
+
: expandPolygonFromCentroid(polygon, Math.max((stair.openingOffset ?? 0) - STAIR_SLAB_OPENING_TIGHTENING, 0)),
|
|
523
|
+
metadata: {
|
|
524
|
+
source: 'stair',
|
|
525
|
+
stairId: stair.id,
|
|
526
|
+
},
|
|
527
|
+
})))
|
|
528
|
+
.filter((hole) => polygonContainsPolygon(slab.polygon, hole.polygon));
|
|
529
|
+
const nextHoles = [...manualHoles, ...stairHoles.map((hole) => hole.polygon)];
|
|
530
|
+
const nextMetadata = [...manualMetadata, ...stairHoles.map((hole) => hole.metadata)];
|
|
531
|
+
if (!polygonsEqual(existingHoles, nextHoles) ||
|
|
532
|
+
!metadataEqual(existingMetadata, nextMetadata)) {
|
|
533
|
+
updates.push({
|
|
534
|
+
id: slab.id,
|
|
535
|
+
data: {
|
|
536
|
+
holes: nextHoles,
|
|
537
|
+
holeMetadata: nextMetadata,
|
|
538
|
+
},
|
|
539
|
+
});
|
|
540
|
+
}
|
|
541
|
+
}
|
|
542
|
+
for (const ceiling of ceilings) {
|
|
543
|
+
const ceilingLevelId = resolveLevelId(ceiling, nodes);
|
|
544
|
+
const existingHoles = ceiling.holes ?? [];
|
|
545
|
+
const existingMetadata = normalizeExistingMetadata(existingHoles, ceiling.holeMetadata);
|
|
546
|
+
const manualHoles = existingHoles.filter((_hole, index) => existingMetadata[index]?.source !== 'stair');
|
|
547
|
+
const manualMetadata = existingMetadata
|
|
548
|
+
.filter((entry) => entry.source !== 'stair')
|
|
549
|
+
.map((entry) => ({ ...entry }));
|
|
550
|
+
const stairHoles = stairs
|
|
551
|
+
.filter((stair) => shouldApplyStairToCeiling(stair, ceilingLevelId, nodes))
|
|
552
|
+
.flatMap((stair) => getStairOpeningPolygons(stair, nodes, getTargetCeilingElevationForStair(stair, ceiling, ceilingLevelId, nodes)).map((polygon) => ({
|
|
553
|
+
polygon: stair.stairType === 'straight'
|
|
554
|
+
? polygon
|
|
555
|
+
: expandPolygonFromCentroid(polygon, Math.max((stair.openingOffset ?? 0) - STAIR_SLAB_OPENING_TIGHTENING, 0)),
|
|
556
|
+
metadata: {
|
|
557
|
+
source: 'stair',
|
|
558
|
+
stairId: stair.id,
|
|
559
|
+
},
|
|
560
|
+
})))
|
|
561
|
+
.filter((hole) => polygonContainsPolygon(ceiling.polygon, hole.polygon));
|
|
562
|
+
const nextHoles = [...manualHoles, ...stairHoles.map((hole) => hole.polygon)];
|
|
563
|
+
const nextMetadata = [...manualMetadata, ...stairHoles.map((hole) => hole.metadata)];
|
|
564
|
+
if (!polygonsEqual(existingHoles, nextHoles) ||
|
|
565
|
+
!metadataEqual(existingMetadata, nextMetadata)) {
|
|
566
|
+
updates.push({
|
|
567
|
+
id: ceiling.id,
|
|
568
|
+
data: {
|
|
569
|
+
holes: nextHoles,
|
|
570
|
+
holeMetadata: nextMetadata,
|
|
571
|
+
},
|
|
572
|
+
});
|
|
573
|
+
}
|
|
574
|
+
}
|
|
575
|
+
return updates;
|
|
576
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"stair-opening-sync.test.d.ts","sourceRoot":"","sources":["../../../src/systems/stair/stair-opening-sync.test.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
// @ts-expect-error — bun:test is provided by the Bun runtime; core does not
|
|
2
|
+
// depend on @types/bun so the import type is unresolved at compile time.
|
|
3
|
+
import { describe, expect, test } from 'bun:test';
|
|
4
|
+
import { BuildingNode, LevelNode, SlabNode, StairNode, StairSegmentNode } from '../../schema';
|
|
5
|
+
import { syncAutoStairOpenings } from './stair-opening-sync';
|
|
6
|
+
describe('syncAutoStairOpenings', () => {
|
|
7
|
+
test('only applies stair holes to destination slabs that contain the opening', () => {
|
|
8
|
+
const building = BuildingNode.parse({ name: 'Building' });
|
|
9
|
+
const ground = LevelNode.parse({ name: 'Ground', level: 0, parentId: building.id });
|
|
10
|
+
const upper = LevelNode.parse({ name: 'Upper', level: 1, parentId: building.id });
|
|
11
|
+
const landingSlab = SlabNode.parse({
|
|
12
|
+
name: 'Landing Slab',
|
|
13
|
+
parentId: upper.id,
|
|
14
|
+
polygon: [
|
|
15
|
+
[0, 0],
|
|
16
|
+
[4, 0],
|
|
17
|
+
[4, 3],
|
|
18
|
+
[0, 3],
|
|
19
|
+
],
|
|
20
|
+
});
|
|
21
|
+
const bedroomSlab = SlabNode.parse({
|
|
22
|
+
name: 'Bedroom Slab',
|
|
23
|
+
parentId: upper.id,
|
|
24
|
+
polygon: [
|
|
25
|
+
[4, 0],
|
|
26
|
+
[8, 0],
|
|
27
|
+
[8, 3],
|
|
28
|
+
[4, 3],
|
|
29
|
+
],
|
|
30
|
+
});
|
|
31
|
+
const segment = StairSegmentNode.parse({
|
|
32
|
+
parentId: 'stair_main',
|
|
33
|
+
width: 1,
|
|
34
|
+
length: 2.6,
|
|
35
|
+
height: 2.5,
|
|
36
|
+
stepCount: 12,
|
|
37
|
+
});
|
|
38
|
+
const stair = StairNode.parse({
|
|
39
|
+
id: 'stair_main',
|
|
40
|
+
name: 'Main Stair',
|
|
41
|
+
parentId: ground.id,
|
|
42
|
+
position: [2, 0, 0.2],
|
|
43
|
+
stairType: 'straight',
|
|
44
|
+
fromLevelId: ground.id,
|
|
45
|
+
toLevelId: upper.id,
|
|
46
|
+
slabOpeningMode: 'destination',
|
|
47
|
+
children: [segment.id],
|
|
48
|
+
});
|
|
49
|
+
const nodes = Object.fromEntries([
|
|
50
|
+
building,
|
|
51
|
+
ground,
|
|
52
|
+
upper,
|
|
53
|
+
landingSlab,
|
|
54
|
+
bedroomSlab,
|
|
55
|
+
stair,
|
|
56
|
+
{ ...segment, parentId: stair.id },
|
|
57
|
+
].map((node) => [node.id, node]));
|
|
58
|
+
const updates = syncAutoStairOpenings(nodes);
|
|
59
|
+
const landingUpdate = updates.find((update) => update.id === landingSlab.id);
|
|
60
|
+
const bedroomUpdate = updates.find((update) => update.id === bedroomSlab.id);
|
|
61
|
+
expect(landingUpdate?.data.holes).toHaveLength(1);
|
|
62
|
+
expect(landingUpdate?.data.holeMetadata).toEqual([{ source: 'stair', stairId: stair.id }]);
|
|
63
|
+
expect(bedroomUpdate).toBeUndefined();
|
|
64
|
+
});
|
|
65
|
+
});
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"stair-system.d.ts","sourceRoot":"","sources":["../../../src/systems/stair/stair-system.tsx"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"stair-system.d.ts","sourceRoot":"","sources":["../../../src/systems/stair/stair-system.tsx"],"names":[],"mappings":"AAuBA,eAAO,MAAM,WAAW,YAgHvB,CAAA"}
|