@pascal-app/core 0.1.3
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 +42 -0
- package/dist/events/bus.d.ts.map +1 -0
- package/dist/events/bus.js +13 -0
- package/dist/hooks/scene-registry/scene-registry.d.ts +18 -0
- package/dist/hooks/scene-registry/scene-registry.d.ts.map +1 -0
- package/dist/hooks/scene-registry/scene-registry.js +35 -0
- package/dist/hooks/spatial-grid/spatial-grid-manager.d.ts +90 -0
- package/dist/hooks/spatial-grid/spatial-grid-manager.d.ts.map +1 -0
- package/dist/hooks/spatial-grid/spatial-grid-manager.js +466 -0
- package/dist/hooks/spatial-grid/spatial-grid-sync.d.ts +4 -0
- package/dist/hooks/spatial-grid/spatial-grid-sync.d.ts.map +1 -0
- package/dist/hooks/spatial-grid/spatial-grid-sync.js +115 -0
- package/dist/hooks/spatial-grid/spatial-grid.d.ts +23 -0
- package/dist/hooks/spatial-grid/spatial-grid.d.ts.map +1 -0
- package/dist/hooks/spatial-grid/spatial-grid.js +115 -0
- package/dist/hooks/spatial-grid/use-spatial-query.d.ts +16 -0
- package/dist/hooks/spatial-grid/use-spatial-query.d.ts.map +1 -0
- package/dist/hooks/spatial-grid/use-spatial-query.js +14 -0
- package/dist/hooks/spatial-grid/wall-spatial-grid.d.ts +47 -0
- package/dist/hooks/spatial-grid/wall-spatial-grid.d.ts.map +1 -0
- package/dist/hooks/spatial-grid/wall-spatial-grid.js +113 -0
- package/dist/index.d.ts +17 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +22 -0
- package/dist/lib/asset-storage.d.ts +11 -0
- package/dist/lib/asset-storage.d.ts.map +1 -0
- package/dist/lib/asset-storage.js +48 -0
- package/dist/lib/space-detection.d.ts +34 -0
- package/dist/lib/space-detection.d.ts.map +1 -0
- package/dist/lib/space-detection.js +499 -0
- package/dist/schema/base.d.ts +30 -0
- package/dist/schema/base.d.ts.map +1 -0
- package/dist/schema/base.js +25 -0
- package/dist/schema/camera.d.ts +13 -0
- package/dist/schema/camera.d.ts.map +1 -0
- package/dist/schema/camera.js +9 -0
- package/dist/schema/index.d.ts +17 -0
- package/dist/schema/index.d.ts.map +1 -0
- package/dist/schema/index.js +18 -0
- package/dist/schema/nodes/building.d.ts +25 -0
- package/dist/schema/nodes/building.d.ts.map +1 -0
- package/dist/schema/nodes/building.js +16 -0
- package/dist/schema/nodes/ceiling.d.ts +25 -0
- package/dist/schema/nodes/ceiling.d.ts.map +1 -0
- package/dist/schema/nodes/ceiling.js +16 -0
- package/dist/schema/nodes/guide.d.ts +27 -0
- package/dist/schema/nodes/guide.d.ts.map +1 -0
- package/dist/schema/nodes/guide.js +11 -0
- package/dist/schema/nodes/item.d.ts +65 -0
- package/dist/schema/nodes/item.d.ts.map +1 -0
- package/dist/schema/nodes/item.js +38 -0
- package/dist/schema/nodes/level.d.ts +24 -0
- package/dist/schema/nodes/level.d.ts.map +1 -0
- package/dist/schema/nodes/level.js +21 -0
- package/dist/schema/nodes/roof.d.ts +28 -0
- package/dist/schema/nodes/roof.d.ts.map +1 -0
- package/dist/schema/nodes/roof.js +28 -0
- package/dist/schema/nodes/scan.d.ts +27 -0
- package/dist/schema/nodes/scan.d.ts.map +1 -0
- package/dist/schema/nodes/scan.js +11 -0
- package/dist/schema/nodes/site.d.ts +90 -0
- package/dist/schema/nodes/site.d.ts.map +1 -0
- package/dist/schema/nodes/site.js +39 -0
- package/dist/schema/nodes/slab.d.ts +24 -0
- package/dist/schema/nodes/slab.d.ts.map +1 -0
- package/dist/schema/nodes/slab.js +15 -0
- package/dist/schema/nodes/wall.d.ts +37 -0
- package/dist/schema/nodes/wall.d.ts.map +1 -0
- package/dist/schema/nodes/wall.js +30 -0
- package/dist/schema/nodes/zone.d.ts +24 -0
- package/dist/schema/nodes/zone.d.ts.map +1 -0
- package/dist/schema/nodes/zone.js +22 -0
- package/dist/schema/types.d.ts +339 -0
- package/dist/schema/types.d.ts.map +1 -0
- package/dist/schema/types.js +25 -0
- package/dist/store/actions/node-actions.d.ts +12 -0
- package/dist/store/actions/node-actions.d.ts.map +1 -0
- package/dist/store/actions/node-actions.js +121 -0
- package/dist/store/use-scene.d.ts +31 -0
- package/dist/store/use-scene.d.ts.map +1 -0
- package/dist/store/use-scene.js +127 -0
- package/dist/systems/ceiling/ceiling-system.d.ts +8 -0
- package/dist/systems/ceiling/ceiling-system.d.ts.map +1 -0
- package/dist/systems/ceiling/ceiling-system.js +65 -0
- package/dist/systems/item/item-system.d.ts +2 -0
- package/dist/systems/item/item-system.d.ts.map +1 -0
- package/dist/systems/item/item-system.js +43 -0
- package/dist/systems/roof/roof-system.d.ts +8 -0
- package/dist/systems/roof/roof-system.d.ts.map +1 -0
- package/dist/systems/roof/roof-system.js +254 -0
- package/dist/systems/slab/slab-system.d.ts +8 -0
- package/dist/systems/slab/slab-system.d.ts.map +1 -0
- package/dist/systems/slab/slab-system.js +117 -0
- package/dist/systems/wall/wall-mitering.d.ts +32 -0
- package/dist/systems/wall/wall-mitering.d.ts.map +1 -0
- package/dist/systems/wall/wall-mitering.js +214 -0
- package/dist/systems/wall/wall-system.d.ts +12 -0
- package/dist/systems/wall/wall-system.d.ts.map +1 -0
- package/dist/systems/wall/wall-system.js +286 -0
- package/dist/utils/types.d.ts +6 -0
- package/dist/utils/types.d.ts.map +1 -0
- package/dist/utils/types.js +7 -0
- package/package.json +58 -0
|
@@ -0,0 +1,499 @@
|
|
|
1
|
+
// ============================================================================
|
|
2
|
+
// SYNC INITIALIZATION
|
|
3
|
+
// ============================================================================
|
|
4
|
+
/**
|
|
5
|
+
* Initializes space detection sync with scene and editor stores
|
|
6
|
+
* Call this once during app initialization
|
|
7
|
+
*/
|
|
8
|
+
export function initSpaceDetectionSync(sceneStore, // useScene store
|
|
9
|
+
editorStore) {
|
|
10
|
+
const prevWallsByLevel = new Map();
|
|
11
|
+
let isProcessing = false; // Prevent re-entrant calls
|
|
12
|
+
// Subscribe to scene changes (standard Zustand subscribe, not selector-based)
|
|
13
|
+
const unsubscribe = sceneStore.subscribe((state) => {
|
|
14
|
+
// Skip if already processing to avoid infinite loops
|
|
15
|
+
if (isProcessing)
|
|
16
|
+
return;
|
|
17
|
+
const nodes = state.nodes;
|
|
18
|
+
const currentWallsByLevel = new Map();
|
|
19
|
+
// Group walls by level
|
|
20
|
+
for (const node of Object.values(nodes)) {
|
|
21
|
+
if (node.type === 'wall' && node.parentId) {
|
|
22
|
+
const levelId = node.parentId;
|
|
23
|
+
if (!currentWallsByLevel.has(levelId)) {
|
|
24
|
+
currentWallsByLevel.set(levelId, new Set());
|
|
25
|
+
}
|
|
26
|
+
currentWallsByLevel.get(levelId).add(node.id);
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
// Check each level for changes
|
|
30
|
+
const levelsToUpdate = new Set();
|
|
31
|
+
// Check for new walls (created)
|
|
32
|
+
for (const [levelId, wallIds] of currentWallsByLevel.entries()) {
|
|
33
|
+
const prevWallIds = prevWallsByLevel.get(levelId);
|
|
34
|
+
if (!prevWallIds) {
|
|
35
|
+
// New level with walls - run detection if there are multiple walls
|
|
36
|
+
if (wallIds.size > 1) {
|
|
37
|
+
levelsToUpdate.add(levelId);
|
|
38
|
+
}
|
|
39
|
+
continue;
|
|
40
|
+
}
|
|
41
|
+
// Find newly added walls
|
|
42
|
+
for (const wallId of wallIds) {
|
|
43
|
+
if (!prevWallIds.has(wallId)) {
|
|
44
|
+
// Wall was added - check if it touches other walls
|
|
45
|
+
const wall = nodes[wallId];
|
|
46
|
+
const otherWalls = Array.from(wallIds)
|
|
47
|
+
.filter((id) => id !== wallId)
|
|
48
|
+
.map((id) => nodes[id])
|
|
49
|
+
.filter(Boolean);
|
|
50
|
+
if (wallTouchesOthers(wall, otherWalls)) {
|
|
51
|
+
levelsToUpdate.add(levelId);
|
|
52
|
+
break;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
// Check for deleted walls
|
|
58
|
+
for (const [levelId, prevWallIds] of prevWallsByLevel.entries()) {
|
|
59
|
+
const currentWallIds = currentWallsByLevel.get(levelId);
|
|
60
|
+
if (!currentWallIds) {
|
|
61
|
+
// All walls deleted from level - clear spaces
|
|
62
|
+
if (prevWallIds.size > 0) {
|
|
63
|
+
levelsToUpdate.add(levelId);
|
|
64
|
+
}
|
|
65
|
+
continue;
|
|
66
|
+
}
|
|
67
|
+
// Check if any walls were deleted
|
|
68
|
+
for (const wallId of prevWallIds) {
|
|
69
|
+
if (!currentWallIds.has(wallId)) {
|
|
70
|
+
// Wall was deleted - run detection
|
|
71
|
+
levelsToUpdate.add(levelId);
|
|
72
|
+
break;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
// Run detection for affected levels
|
|
77
|
+
if (levelsToUpdate.size > 0) {
|
|
78
|
+
isProcessing = true;
|
|
79
|
+
try {
|
|
80
|
+
runSpaceDetection(Array.from(levelsToUpdate), sceneStore, editorStore, nodes);
|
|
81
|
+
}
|
|
82
|
+
finally {
|
|
83
|
+
isProcessing = false;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
// Update previous walls reference
|
|
87
|
+
prevWallsByLevel.clear();
|
|
88
|
+
for (const [levelId, wallIds] of currentWallsByLevel.entries()) {
|
|
89
|
+
prevWallsByLevel.set(levelId, wallIds);
|
|
90
|
+
}
|
|
91
|
+
});
|
|
92
|
+
return unsubscribe;
|
|
93
|
+
}
|
|
94
|
+
/**
|
|
95
|
+
* Runs space detection for the given levels
|
|
96
|
+
* Updates wall nodes and editor spaces
|
|
97
|
+
*/
|
|
98
|
+
function runSpaceDetection(levelIds, sceneStore, editorStore, nodes) {
|
|
99
|
+
const { updateNode } = sceneStore.getState();
|
|
100
|
+
const { setSpaces } = editorStore.getState();
|
|
101
|
+
const allSpaces = {};
|
|
102
|
+
for (const levelId of levelIds) {
|
|
103
|
+
// Get walls for this level
|
|
104
|
+
const walls = Object.values(nodes).filter((node) => node.type === 'wall' && node.parentId === levelId);
|
|
105
|
+
if (walls.length === 0) {
|
|
106
|
+
// No walls - clear any spaces for this level
|
|
107
|
+
continue;
|
|
108
|
+
}
|
|
109
|
+
// Run detection
|
|
110
|
+
const { wallUpdates, spaces } = detectSpacesForLevel(levelId, walls);
|
|
111
|
+
// Update wall nodes (only if values changed to avoid infinite loop)
|
|
112
|
+
for (const update of wallUpdates) {
|
|
113
|
+
const wall = nodes[update.wallId];
|
|
114
|
+
if (wall.frontSide !== update.frontSide || wall.backSide !== update.backSide) {
|
|
115
|
+
updateNode(update.wallId, {
|
|
116
|
+
frontSide: update.frontSide,
|
|
117
|
+
backSide: update.backSide,
|
|
118
|
+
});
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
// Store spaces
|
|
122
|
+
for (const space of spaces) {
|
|
123
|
+
allSpaces[space.id] = space;
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
// Update editor spaces
|
|
127
|
+
setSpaces(allSpaces);
|
|
128
|
+
}
|
|
129
|
+
// ============================================================================
|
|
130
|
+
// MAIN DETECTION FUNCTION
|
|
131
|
+
// ============================================================================
|
|
132
|
+
/**
|
|
133
|
+
* Detects spaces for a level by flood-filling a grid from the edges
|
|
134
|
+
* Returns wall side updates and detected spaces
|
|
135
|
+
*/
|
|
136
|
+
export function detectSpacesForLevel(levelId, walls, gridResolution = 0.5) {
|
|
137
|
+
if (walls.length === 0) {
|
|
138
|
+
return { wallUpdates: [], spaces: [] };
|
|
139
|
+
}
|
|
140
|
+
// Build grid from walls
|
|
141
|
+
const grid = buildGrid(walls, gridResolution);
|
|
142
|
+
// Flood fill from edges to mark exterior
|
|
143
|
+
floodFillFromEdges(grid);
|
|
144
|
+
// Find interior spaces
|
|
145
|
+
const interiorSpaces = findInteriorSpaces(grid, levelId);
|
|
146
|
+
// Assign wall sides
|
|
147
|
+
const wallUpdates = assignWallSides(walls, grid);
|
|
148
|
+
return {
|
|
149
|
+
wallUpdates,
|
|
150
|
+
spaces: interiorSpaces,
|
|
151
|
+
};
|
|
152
|
+
}
|
|
153
|
+
// ============================================================================
|
|
154
|
+
// GRID BUILDING
|
|
155
|
+
// ============================================================================
|
|
156
|
+
/**
|
|
157
|
+
* Builds a discrete grid and marks cells occupied by walls
|
|
158
|
+
*/
|
|
159
|
+
function buildGrid(walls, resolution) {
|
|
160
|
+
// Find bounds
|
|
161
|
+
let minX = Number.POSITIVE_INFINITY;
|
|
162
|
+
let minZ = Number.POSITIVE_INFINITY;
|
|
163
|
+
let maxX = Number.NEGATIVE_INFINITY;
|
|
164
|
+
let maxZ = Number.NEGATIVE_INFINITY;
|
|
165
|
+
for (const wall of walls) {
|
|
166
|
+
minX = Math.min(minX, wall.start[0], wall.end[0]);
|
|
167
|
+
minZ = Math.min(minZ, wall.start[1], wall.end[1]);
|
|
168
|
+
maxX = Math.max(maxX, wall.start[0], wall.end[0]);
|
|
169
|
+
maxZ = Math.max(maxZ, wall.start[1], wall.end[1]);
|
|
170
|
+
}
|
|
171
|
+
// Add padding around bounds
|
|
172
|
+
const padding = 2; // meters
|
|
173
|
+
minX -= padding;
|
|
174
|
+
minZ -= padding;
|
|
175
|
+
maxX += padding;
|
|
176
|
+
maxZ += padding;
|
|
177
|
+
const width = Math.ceil((maxX - minX) / resolution);
|
|
178
|
+
const height = Math.ceil((maxZ - minZ) / resolution);
|
|
179
|
+
const grid = {
|
|
180
|
+
cells: new Map(),
|
|
181
|
+
resolution,
|
|
182
|
+
minX,
|
|
183
|
+
minZ,
|
|
184
|
+
maxX,
|
|
185
|
+
maxZ,
|
|
186
|
+
width,
|
|
187
|
+
height,
|
|
188
|
+
};
|
|
189
|
+
// Mark wall cells
|
|
190
|
+
for (const wall of walls) {
|
|
191
|
+
markWallCells(grid, wall);
|
|
192
|
+
}
|
|
193
|
+
return grid;
|
|
194
|
+
}
|
|
195
|
+
/**
|
|
196
|
+
* Marks all grid cells occupied by a wall using line rasterization
|
|
197
|
+
* Uses denser sampling to ensure continuous barriers
|
|
198
|
+
*/
|
|
199
|
+
function markWallCells(grid, wall) {
|
|
200
|
+
const thickness = wall.thickness ?? 0.2;
|
|
201
|
+
const halfThickness = thickness / 2;
|
|
202
|
+
const [x1, z1] = wall.start;
|
|
203
|
+
const [x2, z2] = wall.end;
|
|
204
|
+
// Wall direction vector
|
|
205
|
+
const dx = x2 - x1;
|
|
206
|
+
const dz = z2 - z1;
|
|
207
|
+
const len = Math.sqrt(dx * dx + dz * dz);
|
|
208
|
+
if (len < 0.001)
|
|
209
|
+
return;
|
|
210
|
+
// Normalized direction and perpendicular
|
|
211
|
+
const dirX = dx / len;
|
|
212
|
+
const dirZ = dz / len;
|
|
213
|
+
const perpX = -dirZ;
|
|
214
|
+
const perpZ = dirX;
|
|
215
|
+
// Denser sampling along wall length (at least 2x resolution)
|
|
216
|
+
const steps = Math.max(Math.ceil(len / (grid.resolution * 0.5)), 2);
|
|
217
|
+
for (let i = 0; i <= steps; i++) {
|
|
218
|
+
const t = i / steps;
|
|
219
|
+
const x = x1 + dx * t;
|
|
220
|
+
const z = z1 + dz * t;
|
|
221
|
+
// Denser sampling across wall thickness
|
|
222
|
+
const thicknessSteps = Math.max(Math.ceil(thickness / (grid.resolution * 0.5)), 2);
|
|
223
|
+
for (let j = 0; j <= thicknessSteps; j++) {
|
|
224
|
+
const offset = (j / thicknessSteps - 0.5) * thickness;
|
|
225
|
+
const wx = x + perpX * offset;
|
|
226
|
+
const wz = z + perpZ * offset;
|
|
227
|
+
const key = getCellKey(grid, wx, wz);
|
|
228
|
+
if (key) {
|
|
229
|
+
grid.cells.set(key, 'wall');
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
// ============================================================================
|
|
235
|
+
// FLOOD FILL
|
|
236
|
+
// ============================================================================
|
|
237
|
+
/**
|
|
238
|
+
* Flood fills from all edge cells to mark exterior space
|
|
239
|
+
*/
|
|
240
|
+
function floodFillFromEdges(grid) {
|
|
241
|
+
const queue = [];
|
|
242
|
+
// Add all edge cells to queue
|
|
243
|
+
for (let x = 0; x < grid.width; x++) {
|
|
244
|
+
for (let z = 0; z < grid.height; z++) {
|
|
245
|
+
// Only process edge cells
|
|
246
|
+
if (x === 0 || x === grid.width - 1 || z === 0 || z === grid.height - 1) {
|
|
247
|
+
const key = getCellKeyFromIndex(x, z, grid.width);
|
|
248
|
+
const cell = grid.cells.get(key);
|
|
249
|
+
if (cell !== 'wall') {
|
|
250
|
+
grid.cells.set(key, 'exterior');
|
|
251
|
+
queue.push(key);
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
// Flood fill
|
|
257
|
+
while (queue.length > 0) {
|
|
258
|
+
const key = queue.shift();
|
|
259
|
+
const [x, z] = parseCellKey(key);
|
|
260
|
+
// Check 4 neighbors
|
|
261
|
+
const neighbors = [
|
|
262
|
+
[x + 1, z],
|
|
263
|
+
[x - 1, z],
|
|
264
|
+
[x, z + 1],
|
|
265
|
+
[x, z - 1],
|
|
266
|
+
];
|
|
267
|
+
for (const [nx, nz] of neighbors) {
|
|
268
|
+
if (nx < 0 || nx >= grid.width || nz < 0 || nz >= grid.height)
|
|
269
|
+
continue;
|
|
270
|
+
const nKey = getCellKeyFromIndex(nx, nz, grid.width);
|
|
271
|
+
const cell = grid.cells.get(nKey);
|
|
272
|
+
if (cell !== 'wall' && cell !== 'exterior') {
|
|
273
|
+
grid.cells.set(nKey, 'exterior');
|
|
274
|
+
queue.push(nKey);
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
// ============================================================================
|
|
280
|
+
// INTERIOR SPACE DETECTION
|
|
281
|
+
// ============================================================================
|
|
282
|
+
/**
|
|
283
|
+
* Finds all interior spaces (connected regions not marked as exterior or wall)
|
|
284
|
+
*/
|
|
285
|
+
function findInteriorSpaces(grid, levelId) {
|
|
286
|
+
const spaces = [];
|
|
287
|
+
const visited = new Set();
|
|
288
|
+
// Scan grid for interior cells
|
|
289
|
+
for (let x = 0; x < grid.width; x++) {
|
|
290
|
+
for (let z = 0; z < grid.height; z++) {
|
|
291
|
+
const key = getCellKeyFromIndex(x, z, grid.width);
|
|
292
|
+
if (visited.has(key))
|
|
293
|
+
continue;
|
|
294
|
+
const cell = grid.cells.get(key);
|
|
295
|
+
if (cell === 'wall' || cell === 'exterior') {
|
|
296
|
+
visited.add(key);
|
|
297
|
+
continue;
|
|
298
|
+
}
|
|
299
|
+
// Found interior cell - flood fill to find full space
|
|
300
|
+
const spaceCells = new Set();
|
|
301
|
+
const queue = [key];
|
|
302
|
+
visited.add(key);
|
|
303
|
+
spaceCells.add(key);
|
|
304
|
+
// Mark the seed cell as interior in the grid
|
|
305
|
+
grid.cells.set(key, 'interior');
|
|
306
|
+
while (queue.length > 0) {
|
|
307
|
+
const curKey = queue.shift();
|
|
308
|
+
const [cx, cz] = parseCellKey(curKey);
|
|
309
|
+
const neighbors = [
|
|
310
|
+
[cx + 1, cz],
|
|
311
|
+
[cx - 1, cz],
|
|
312
|
+
[cx, cz + 1],
|
|
313
|
+
[cx, cz - 1],
|
|
314
|
+
];
|
|
315
|
+
for (const [nx, nz] of neighbors) {
|
|
316
|
+
if (nx < 0 || nx >= grid.width || nz < 0 || nz >= grid.height)
|
|
317
|
+
continue;
|
|
318
|
+
const nKey = getCellKeyFromIndex(nx, nz, grid.width);
|
|
319
|
+
if (visited.has(nKey))
|
|
320
|
+
continue;
|
|
321
|
+
const nCell = grid.cells.get(nKey);
|
|
322
|
+
if (nCell === 'wall' || nCell === 'exterior') {
|
|
323
|
+
visited.add(nKey);
|
|
324
|
+
continue;
|
|
325
|
+
}
|
|
326
|
+
visited.add(nKey);
|
|
327
|
+
spaceCells.add(nKey);
|
|
328
|
+
// Mark as interior in grid
|
|
329
|
+
grid.cells.set(nKey, 'interior');
|
|
330
|
+
queue.push(nKey);
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
// Create space from cells
|
|
334
|
+
const polygon = extractPolygonFromCells(spaceCells, grid);
|
|
335
|
+
spaces.push({
|
|
336
|
+
id: `space-${spaces.length}`,
|
|
337
|
+
levelId,
|
|
338
|
+
polygon,
|
|
339
|
+
wallIds: [],
|
|
340
|
+
isExterior: false,
|
|
341
|
+
});
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
return spaces;
|
|
345
|
+
}
|
|
346
|
+
/**
|
|
347
|
+
* Extracts a simplified polygon from a set of grid cells
|
|
348
|
+
* Returns bounding box for now (can be improved to trace actual boundary)
|
|
349
|
+
*/
|
|
350
|
+
function extractPolygonFromCells(cells, grid) {
|
|
351
|
+
let minX = Number.POSITIVE_INFINITY;
|
|
352
|
+
let minZ = Number.POSITIVE_INFINITY;
|
|
353
|
+
let maxX = Number.NEGATIVE_INFINITY;
|
|
354
|
+
let maxZ = Number.NEGATIVE_INFINITY;
|
|
355
|
+
for (const key of cells) {
|
|
356
|
+
const [x, z] = parseCellKey(key);
|
|
357
|
+
const worldX = grid.minX + x * grid.resolution;
|
|
358
|
+
const worldZ = grid.minZ + z * grid.resolution;
|
|
359
|
+
minX = Math.min(minX, worldX);
|
|
360
|
+
minZ = Math.min(minZ, worldZ);
|
|
361
|
+
maxX = Math.max(maxX, worldX);
|
|
362
|
+
maxZ = Math.max(maxZ, worldZ);
|
|
363
|
+
}
|
|
364
|
+
// Return bounding box as polygon
|
|
365
|
+
return [
|
|
366
|
+
[minX, minZ],
|
|
367
|
+
[maxX, minZ],
|
|
368
|
+
[maxX, maxZ],
|
|
369
|
+
[minX, maxZ],
|
|
370
|
+
];
|
|
371
|
+
}
|
|
372
|
+
// ============================================================================
|
|
373
|
+
// WALL SIDE ASSIGNMENT
|
|
374
|
+
// ============================================================================
|
|
375
|
+
/**
|
|
376
|
+
* Assigns front/back side classification to each wall based on grid
|
|
377
|
+
*/
|
|
378
|
+
function assignWallSides(walls, grid) {
|
|
379
|
+
const updates = [];
|
|
380
|
+
for (const wall of walls) {
|
|
381
|
+
const thickness = wall.thickness ?? 0.2;
|
|
382
|
+
const [x1, z1] = wall.start;
|
|
383
|
+
const [x2, z2] = wall.end;
|
|
384
|
+
// Wall direction and perpendicular
|
|
385
|
+
const dx = x2 - x1;
|
|
386
|
+
const dz = z2 - z1;
|
|
387
|
+
const len = Math.sqrt(dx * dx + dz * dz);
|
|
388
|
+
if (len < 0.001)
|
|
389
|
+
continue;
|
|
390
|
+
const perpX = -dz / len;
|
|
391
|
+
const perpZ = dx / len;
|
|
392
|
+
// Sample point on front side (perpendicular direction)
|
|
393
|
+
const midX = (x1 + x2) / 2;
|
|
394
|
+
const midZ = (z1 + z2) / 2;
|
|
395
|
+
// Sample beyond wall thickness + one full grid cell to ensure we're in the next cell
|
|
396
|
+
const offset = thickness / 2 + grid.resolution;
|
|
397
|
+
const frontX = midX + perpX * offset;
|
|
398
|
+
const frontZ = midZ + perpZ * offset;
|
|
399
|
+
const backX = midX - perpX * offset;
|
|
400
|
+
const backZ = midZ - perpZ * offset;
|
|
401
|
+
// Check what space each side faces
|
|
402
|
+
const frontKey = getCellKey(grid, frontX, frontZ);
|
|
403
|
+
const backKey = getCellKey(grid, backX, backZ);
|
|
404
|
+
const frontCell = frontKey ? grid.cells.get(frontKey) : undefined;
|
|
405
|
+
const backCell = backKey ? grid.cells.get(backKey) : undefined;
|
|
406
|
+
const frontSide = classifySide(frontCell);
|
|
407
|
+
const backSide = classifySide(backCell);
|
|
408
|
+
updates.push({
|
|
409
|
+
wallId: wall.id,
|
|
410
|
+
frontSide,
|
|
411
|
+
backSide,
|
|
412
|
+
});
|
|
413
|
+
}
|
|
414
|
+
return updates;
|
|
415
|
+
}
|
|
416
|
+
/**
|
|
417
|
+
* Classifies a cell as interior, exterior, or unknown
|
|
418
|
+
*/
|
|
419
|
+
function classifySide(cell) {
|
|
420
|
+
if (cell === 'exterior')
|
|
421
|
+
return 'exterior';
|
|
422
|
+
if (cell === 'interior')
|
|
423
|
+
return 'interior';
|
|
424
|
+
// Wall cells or out-of-bounds (undefined) are unknown
|
|
425
|
+
return 'unknown';
|
|
426
|
+
}
|
|
427
|
+
// ============================================================================
|
|
428
|
+
// GRID UTILITIES
|
|
429
|
+
// ============================================================================
|
|
430
|
+
/**
|
|
431
|
+
* Gets grid cell key from world coordinates
|
|
432
|
+
*/
|
|
433
|
+
function getCellKey(grid, x, z) {
|
|
434
|
+
const cellX = Math.floor((x - grid.minX) / grid.resolution);
|
|
435
|
+
const cellZ = Math.floor((z - grid.minZ) / grid.resolution);
|
|
436
|
+
if (cellX < 0 || cellX >= grid.width || cellZ < 0 || cellZ >= grid.height) {
|
|
437
|
+
return null;
|
|
438
|
+
}
|
|
439
|
+
return `${cellX},${cellZ}`;
|
|
440
|
+
}
|
|
441
|
+
/**
|
|
442
|
+
* Gets cell key from grid indices
|
|
443
|
+
*/
|
|
444
|
+
function getCellKeyFromIndex(x, z, width) {
|
|
445
|
+
return `${x},${z}`;
|
|
446
|
+
}
|
|
447
|
+
/**
|
|
448
|
+
* Parses cell key back to indices
|
|
449
|
+
*/
|
|
450
|
+
function parseCellKey(key) {
|
|
451
|
+
const parts = key.split(',');
|
|
452
|
+
return [Number.parseInt(parts[0], 10), Number.parseInt(parts[1], 10)];
|
|
453
|
+
}
|
|
454
|
+
// ============================================================================
|
|
455
|
+
// WALL CONNECTIVITY DETECTION
|
|
456
|
+
// ============================================================================
|
|
457
|
+
/**
|
|
458
|
+
* Checks if a wall touches any other walls
|
|
459
|
+
* Used to determine if space detection should run
|
|
460
|
+
*/
|
|
461
|
+
export function wallTouchesOthers(wall, otherWalls) {
|
|
462
|
+
const threshold = 0.1; // 10cm connection threshold
|
|
463
|
+
for (const other of otherWalls) {
|
|
464
|
+
if (other.id === wall.id)
|
|
465
|
+
continue;
|
|
466
|
+
// Check if any endpoint of wall is close to any endpoint or segment of other
|
|
467
|
+
if (distanceToSegment(wall.start, other.start, other.end) < threshold ||
|
|
468
|
+
distanceToSegment(wall.end, other.start, other.end) < threshold ||
|
|
469
|
+
distanceToSegment(other.start, wall.start, wall.end) < threshold ||
|
|
470
|
+
distanceToSegment(other.end, wall.start, wall.end) < threshold) {
|
|
471
|
+
return true;
|
|
472
|
+
}
|
|
473
|
+
}
|
|
474
|
+
return false;
|
|
475
|
+
}
|
|
476
|
+
/**
|
|
477
|
+
* Distance from point to line segment
|
|
478
|
+
*/
|
|
479
|
+
function distanceToSegment(point, segStart, segEnd) {
|
|
480
|
+
const [px, pz] = point;
|
|
481
|
+
const [x1, z1] = segStart;
|
|
482
|
+
const [x2, z2] = segEnd;
|
|
483
|
+
const dx = x2 - x1;
|
|
484
|
+
const dz = z2 - z1;
|
|
485
|
+
const lenSq = dx * dx + dz * dz;
|
|
486
|
+
if (lenSq < 0.0001) {
|
|
487
|
+
// Segment is a point
|
|
488
|
+
const dpx = px - x1;
|
|
489
|
+
const dpz = pz - z1;
|
|
490
|
+
return Math.sqrt(dpx * dpx + dpz * dpz);
|
|
491
|
+
}
|
|
492
|
+
// Project point onto line
|
|
493
|
+
const t = Math.max(0, Math.min(1, ((px - x1) * dx + (pz - z1) * dz) / lenSq));
|
|
494
|
+
const projX = x1 + t * dx;
|
|
495
|
+
const projZ = z1 + t * dz;
|
|
496
|
+
const distX = px - projX;
|
|
497
|
+
const distZ = pz - projZ;
|
|
498
|
+
return Math.sqrt(distX * distX + distZ * distZ);
|
|
499
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
/**
|
|
3
|
+
* Material preset name reference
|
|
4
|
+
* @example 'white', 'brick', 'wood', 'glass', 'preview-valid'
|
|
5
|
+
*/
|
|
6
|
+
export declare const Material: z.ZodOptional<z.ZodString>;
|
|
7
|
+
export declare const generateId: <T extends string>(prefix: T) => `${T}_${string}`;
|
|
8
|
+
export declare const objectId: <T extends string>(prefix: T) => z.ZodDefault<z.ZodTemplateLiteral<`${`${T}_` extends infer T_1 ? T_1 extends `${T}_` ? T_1 extends string | number | bigint | boolean | null | undefined ? `${T_1 extends infer T_2 ? T_2 extends T_1 ? T_2 extends undefined ? "" : T_2 : never : never}` : T_1 extends z.core.$ZodType<unknown, unknown, z.core.$ZodTypeInternals<unknown, unknown>> ? `${z.core.output<T_1> extends infer T_3 extends string | number | bigint | boolean | null | undefined ? T_3 extends undefined ? "" : T_3 : never}` : never : never : never}${string}`>>;
|
|
9
|
+
export declare const nodeType: <T extends string>(type: T) => z.ZodDefault<z.ZodLiteral<T>>;
|
|
10
|
+
export declare const BaseNode: z.ZodObject<{
|
|
11
|
+
object: z.ZodDefault<z.ZodLiteral<"node">>;
|
|
12
|
+
id: z.ZodString;
|
|
13
|
+
type: z.ZodDefault<z.ZodLiteral<"node">>;
|
|
14
|
+
name: z.ZodOptional<z.ZodString>;
|
|
15
|
+
parentId: z.ZodDefault<z.ZodNullable<z.ZodString>>;
|
|
16
|
+
visible: z.ZodDefault<z.ZodOptional<z.ZodBoolean>>;
|
|
17
|
+
camera: z.ZodOptional<z.ZodObject<{
|
|
18
|
+
position: z.ZodTuple<[z.ZodNumber, z.ZodNumber, z.ZodNumber], null>;
|
|
19
|
+
target: z.ZodTuple<[z.ZodNumber, z.ZodNumber, z.ZodNumber], null>;
|
|
20
|
+
mode: z.ZodDefault<z.ZodEnum<{
|
|
21
|
+
perspective: "perspective";
|
|
22
|
+
orthographic: "orthographic";
|
|
23
|
+
}>>;
|
|
24
|
+
fov: z.ZodOptional<z.ZodNumber>;
|
|
25
|
+
zoom: z.ZodOptional<z.ZodNumber>;
|
|
26
|
+
}, z.core.$strip>>;
|
|
27
|
+
metadata: z.ZodDefault<z.ZodOptional<z.ZodJSONSchema>>;
|
|
28
|
+
}, z.core.$strip>;
|
|
29
|
+
export type BaseNode = z.infer<typeof BaseNode>;
|
|
30
|
+
//# sourceMappingURL=base.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"base.d.ts","sourceRoot":"","sources":["../../src/schema/base.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAA;AAKvB;;;GAGG;AACH,eAAO,MAAM,QAAQ,4BAAwB,CAAA;AAC7C,eAAO,MAAM,UAAU,GAAI,CAAC,SAAS,MAAM,EAAE,QAAQ,CAAC,KAAG,GAAG,CAAC,IAAI,MAAM,EACxB,CAAA;AAC/C,eAAO,MAAM,QAAQ,GAAI,CAAC,SAAS,MAAM,EAAE,QAAQ,CAAC,qhBAInD,CAAA;AACD,eAAO,MAAM,QAAQ,GAAI,CAAC,SAAS,MAAM,EAAE,MAAM,CAAC,kCAAkC,CAAA;AAEpF,eAAO,MAAM,QAAQ;;;;;;;;;;;;;;;;;;iBASnB,CAAA;AAEF,MAAM,MAAM,QAAQ,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,QAAQ,CAAC,CAAA"}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { customAlphabet } from 'nanoid';
|
|
2
|
+
import { z } from 'zod';
|
|
3
|
+
import { CameraSchema } from './camera';
|
|
4
|
+
const customId = customAlphabet('0123456789abcdefghijklmnopqrstuvwxyz', 16);
|
|
5
|
+
/**
|
|
6
|
+
* Material preset name reference
|
|
7
|
+
* @example 'white', 'brick', 'wood', 'glass', 'preview-valid'
|
|
8
|
+
*/
|
|
9
|
+
export const Material = z.string().optional();
|
|
10
|
+
export const generateId = (prefix) => `${prefix}_${customId()}`;
|
|
11
|
+
export const objectId = (prefix) => {
|
|
12
|
+
const schema = z.templateLiteral([`${prefix}_`, z.string()]);
|
|
13
|
+
return schema.default(() => generateId(prefix));
|
|
14
|
+
};
|
|
15
|
+
export const nodeType = (type) => z.literal(type).default(type);
|
|
16
|
+
export const BaseNode = z.object({
|
|
17
|
+
object: z.literal('node').default('node'),
|
|
18
|
+
id: z.string(), // objectId('node'), @Aymericr: Thing is if we specify objectId here, when using BaseNode.extend, TS complains that the id is not assignable to the more specific type in the extended node
|
|
19
|
+
type: nodeType('node'),
|
|
20
|
+
name: z.string().optional(),
|
|
21
|
+
parentId: z.string().nullable().default(null),
|
|
22
|
+
visible: z.boolean().optional().default(true),
|
|
23
|
+
camera: CameraSchema.optional(),
|
|
24
|
+
metadata: z.json().optional().default({}),
|
|
25
|
+
});
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
export declare const CameraSchema: z.ZodObject<{
|
|
3
|
+
position: z.ZodTuple<[z.ZodNumber, z.ZodNumber, z.ZodNumber], null>;
|
|
4
|
+
target: z.ZodTuple<[z.ZodNumber, z.ZodNumber, z.ZodNumber], null>;
|
|
5
|
+
mode: z.ZodDefault<z.ZodEnum<{
|
|
6
|
+
perspective: "perspective";
|
|
7
|
+
orthographic: "orthographic";
|
|
8
|
+
}>>;
|
|
9
|
+
fov: z.ZodOptional<z.ZodNumber>;
|
|
10
|
+
zoom: z.ZodOptional<z.ZodNumber>;
|
|
11
|
+
}, z.core.$strip>;
|
|
12
|
+
export type Camera = z.infer<typeof CameraSchema>;
|
|
13
|
+
//# sourceMappingURL=camera.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"camera.d.ts","sourceRoot":"","sources":["../../src/schema/camera.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAA;AAIvB,eAAO,MAAM,YAAY;;;;;;;;;iBAMvB,CAAA;AAEF,MAAM,MAAM,MAAM,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,YAAY,CAAC,CAAA"}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
const Vector3Schema = z.tuple([z.number(), z.number(), z.number()]);
|
|
3
|
+
export const CameraSchema = z.object({
|
|
4
|
+
position: Vector3Schema,
|
|
5
|
+
target: Vector3Schema,
|
|
6
|
+
mode: z.enum(['perspective', 'orthographic']).default('perspective'),
|
|
7
|
+
fov: z.number().optional(), // For perspective
|
|
8
|
+
zoom: z.number().optional(), // For orthographic
|
|
9
|
+
});
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
export { BaseNode, generateId, Material, nodeType, objectId } from './base';
|
|
2
|
+
export { CameraSchema } from './camera';
|
|
3
|
+
export type { AssetInput } from './nodes/item';
|
|
4
|
+
export { ItemNode } from './nodes/item';
|
|
5
|
+
export { LevelNode } from './nodes/level';
|
|
6
|
+
export { SiteNode } from './nodes/site';
|
|
7
|
+
export { SlabNode } from './nodes/slab';
|
|
8
|
+
export { WallNode } from './nodes/wall';
|
|
9
|
+
export { BuildingNode } from './nodes/building';
|
|
10
|
+
export { CeilingNode } from './nodes/ceiling';
|
|
11
|
+
export { ZoneNode } from './nodes/zone';
|
|
12
|
+
export { RoofNode } from './nodes/roof';
|
|
13
|
+
export { ScanNode } from './nodes/scan';
|
|
14
|
+
export { GuideNode } from './nodes/guide';
|
|
15
|
+
export type { AnyNodeId, AnyNodeType } from './types';
|
|
16
|
+
export { AnyNode } from './types';
|
|
17
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/schema/index.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,QAAQ,EAAE,UAAU,EAAE,QAAQ,EAAE,QAAQ,EAAE,QAAQ,EAAE,MAAM,QAAQ,CAAA;AAE3E,OAAO,EAAE,YAAY,EAAE,MAAM,UAAU,CAAA;AACvC,YAAY,EAAE,UAAU,EAAE,MAAM,cAAc,CAAA;AAC9C,OAAO,EAAE,QAAQ,EAAE,MAAM,cAAc,CAAA;AACvC,OAAO,EAAE,SAAS,EAAE,MAAM,eAAe,CAAA;AAEzC,OAAO,EAAE,QAAQ,EAAE,MAAM,cAAc,CAAA;AACvC,OAAO,EAAE,QAAQ,EAAE,MAAM,cAAc,CAAA;AACvC,OAAO,EAAE,QAAQ,EAAE,MAAM,cAAc,CAAA;AACvC,OAAO,EAAE,YAAY,EAAE,MAAM,kBAAkB,CAAA;AAC/C,OAAO,EAAE,WAAW,EAAE,MAAM,iBAAiB,CAAA;AAE7C,OAAO,EAAE,QAAQ,EAAE,MAAM,cAAc,CAAA;AACvC,OAAO,EAAE,QAAQ,EAAE,MAAM,cAAc,CAAA;AACvC,OAAO,EAAE,QAAQ,EAAE,MAAM,cAAc,CAAA;AACvC,OAAO,EAAE,SAAS,EAAE,MAAM,eAAe,CAAA;AACzC,YAAY,EAAE,SAAS,EAAE,WAAW,EAAE,MAAM,SAAS,CAAA;AAErD,OAAO,EAAE,OAAO,EAAE,MAAM,SAAS,CAAA"}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
// Base
|
|
2
|
+
export { BaseNode, generateId, Material, nodeType, objectId } from './base';
|
|
3
|
+
// Camera
|
|
4
|
+
export { CameraSchema } from './camera';
|
|
5
|
+
export { ItemNode } from './nodes/item';
|
|
6
|
+
export { LevelNode } from './nodes/level';
|
|
7
|
+
// Nodes
|
|
8
|
+
export { SiteNode } from './nodes/site';
|
|
9
|
+
export { SlabNode } from './nodes/slab';
|
|
10
|
+
export { WallNode } from './nodes/wall';
|
|
11
|
+
export { BuildingNode } from './nodes/building';
|
|
12
|
+
export { CeilingNode } from './nodes/ceiling';
|
|
13
|
+
export { ZoneNode } from './nodes/zone';
|
|
14
|
+
export { RoofNode } from './nodes/roof';
|
|
15
|
+
export { ScanNode } from './nodes/scan';
|
|
16
|
+
export { GuideNode } from './nodes/guide';
|
|
17
|
+
// Union types
|
|
18
|
+
export { AnyNode } from './types';
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
export declare const BuildingNode: z.ZodObject<{
|
|
3
|
+
object: z.ZodDefault<z.ZodLiteral<"node">>;
|
|
4
|
+
name: z.ZodOptional<z.ZodString>;
|
|
5
|
+
parentId: z.ZodDefault<z.ZodNullable<z.ZodString>>;
|
|
6
|
+
visible: z.ZodDefault<z.ZodOptional<z.ZodBoolean>>;
|
|
7
|
+
camera: z.ZodOptional<z.ZodObject<{
|
|
8
|
+
position: z.ZodTuple<[z.ZodNumber, z.ZodNumber, z.ZodNumber], null>;
|
|
9
|
+
target: z.ZodTuple<[z.ZodNumber, z.ZodNumber, z.ZodNumber], null>;
|
|
10
|
+
mode: z.ZodDefault<z.ZodEnum<{
|
|
11
|
+
perspective: "perspective";
|
|
12
|
+
orthographic: "orthographic";
|
|
13
|
+
}>>;
|
|
14
|
+
fov: z.ZodOptional<z.ZodNumber>;
|
|
15
|
+
zoom: z.ZodOptional<z.ZodNumber>;
|
|
16
|
+
}, z.core.$strip>>;
|
|
17
|
+
metadata: z.ZodDefault<z.ZodOptional<z.ZodJSONSchema>>;
|
|
18
|
+
id: z.ZodDefault<z.ZodTemplateLiteral<`building_${string}`>>;
|
|
19
|
+
type: z.ZodDefault<z.ZodLiteral<"building">>;
|
|
20
|
+
children: z.ZodDefault<z.ZodArray<z.ZodDefault<z.ZodTemplateLiteral<`level_${string}`>>>>;
|
|
21
|
+
position: z.ZodDefault<z.ZodTuple<[z.ZodNumber, z.ZodNumber, z.ZodNumber], null>>;
|
|
22
|
+
rotation: z.ZodDefault<z.ZodTuple<[z.ZodNumber, z.ZodNumber, z.ZodNumber], null>>;
|
|
23
|
+
}, z.core.$strip>;
|
|
24
|
+
export type BuildingNode = z.infer<typeof BuildingNode>;
|
|
25
|
+
//# sourceMappingURL=building.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"building.d.ts","sourceRoot":"","sources":["../../../src/schema/nodes/building.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAA;AAIvB,eAAO,MAAM,YAAY;;;;;;;;;;;;;;;;;;;;;iBAaxB,CAAA;AAED,MAAM,MAAM,YAAY,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,YAAY,CAAC,CAAA"}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import dedent from 'dedent';
|
|
2
|
+
import { z } from 'zod';
|
|
3
|
+
import { BaseNode, nodeType, objectId } from '../base';
|
|
4
|
+
import { LevelNode } from './level';
|
|
5
|
+
export const BuildingNode = BaseNode.extend({
|
|
6
|
+
id: objectId('building'),
|
|
7
|
+
type: nodeType('building'),
|
|
8
|
+
children: z.array(LevelNode.shape.id).default([]),
|
|
9
|
+
position: z.tuple([z.number(), z.number(), z.number()]).default([0, 0, 0]),
|
|
10
|
+
rotation: z.tuple([z.number(), z.number(), z.number()]).default([0, 0, 0]),
|
|
11
|
+
}).describe(dedent `
|
|
12
|
+
Building node - used to represent a building
|
|
13
|
+
- position: position in site coordinate system
|
|
14
|
+
- rotation: rotation in site coordinate system
|
|
15
|
+
- children: array of level nodes (each level is a tree of floor and wall nodes)
|
|
16
|
+
`);
|