@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.
Files changed (103) hide show
  1. package/dist/events/bus.d.ts +42 -0
  2. package/dist/events/bus.d.ts.map +1 -0
  3. package/dist/events/bus.js +13 -0
  4. package/dist/hooks/scene-registry/scene-registry.d.ts +18 -0
  5. package/dist/hooks/scene-registry/scene-registry.d.ts.map +1 -0
  6. package/dist/hooks/scene-registry/scene-registry.js +35 -0
  7. package/dist/hooks/spatial-grid/spatial-grid-manager.d.ts +90 -0
  8. package/dist/hooks/spatial-grid/spatial-grid-manager.d.ts.map +1 -0
  9. package/dist/hooks/spatial-grid/spatial-grid-manager.js +466 -0
  10. package/dist/hooks/spatial-grid/spatial-grid-sync.d.ts +4 -0
  11. package/dist/hooks/spatial-grid/spatial-grid-sync.d.ts.map +1 -0
  12. package/dist/hooks/spatial-grid/spatial-grid-sync.js +115 -0
  13. package/dist/hooks/spatial-grid/spatial-grid.d.ts +23 -0
  14. package/dist/hooks/spatial-grid/spatial-grid.d.ts.map +1 -0
  15. package/dist/hooks/spatial-grid/spatial-grid.js +115 -0
  16. package/dist/hooks/spatial-grid/use-spatial-query.d.ts +16 -0
  17. package/dist/hooks/spatial-grid/use-spatial-query.d.ts.map +1 -0
  18. package/dist/hooks/spatial-grid/use-spatial-query.js +14 -0
  19. package/dist/hooks/spatial-grid/wall-spatial-grid.d.ts +47 -0
  20. package/dist/hooks/spatial-grid/wall-spatial-grid.d.ts.map +1 -0
  21. package/dist/hooks/spatial-grid/wall-spatial-grid.js +113 -0
  22. package/dist/index.d.ts +17 -0
  23. package/dist/index.d.ts.map +1 -0
  24. package/dist/index.js +22 -0
  25. package/dist/lib/asset-storage.d.ts +11 -0
  26. package/dist/lib/asset-storage.d.ts.map +1 -0
  27. package/dist/lib/asset-storage.js +48 -0
  28. package/dist/lib/space-detection.d.ts +34 -0
  29. package/dist/lib/space-detection.d.ts.map +1 -0
  30. package/dist/lib/space-detection.js +499 -0
  31. package/dist/schema/base.d.ts +30 -0
  32. package/dist/schema/base.d.ts.map +1 -0
  33. package/dist/schema/base.js +25 -0
  34. package/dist/schema/camera.d.ts +13 -0
  35. package/dist/schema/camera.d.ts.map +1 -0
  36. package/dist/schema/camera.js +9 -0
  37. package/dist/schema/index.d.ts +17 -0
  38. package/dist/schema/index.d.ts.map +1 -0
  39. package/dist/schema/index.js +18 -0
  40. package/dist/schema/nodes/building.d.ts +25 -0
  41. package/dist/schema/nodes/building.d.ts.map +1 -0
  42. package/dist/schema/nodes/building.js +16 -0
  43. package/dist/schema/nodes/ceiling.d.ts +25 -0
  44. package/dist/schema/nodes/ceiling.d.ts.map +1 -0
  45. package/dist/schema/nodes/ceiling.js +16 -0
  46. package/dist/schema/nodes/guide.d.ts +27 -0
  47. package/dist/schema/nodes/guide.d.ts.map +1 -0
  48. package/dist/schema/nodes/guide.js +11 -0
  49. package/dist/schema/nodes/item.d.ts +65 -0
  50. package/dist/schema/nodes/item.d.ts.map +1 -0
  51. package/dist/schema/nodes/item.js +38 -0
  52. package/dist/schema/nodes/level.d.ts +24 -0
  53. package/dist/schema/nodes/level.d.ts.map +1 -0
  54. package/dist/schema/nodes/level.js +21 -0
  55. package/dist/schema/nodes/roof.d.ts +28 -0
  56. package/dist/schema/nodes/roof.d.ts.map +1 -0
  57. package/dist/schema/nodes/roof.js +28 -0
  58. package/dist/schema/nodes/scan.d.ts +27 -0
  59. package/dist/schema/nodes/scan.d.ts.map +1 -0
  60. package/dist/schema/nodes/scan.js +11 -0
  61. package/dist/schema/nodes/site.d.ts +90 -0
  62. package/dist/schema/nodes/site.d.ts.map +1 -0
  63. package/dist/schema/nodes/site.js +39 -0
  64. package/dist/schema/nodes/slab.d.ts +24 -0
  65. package/dist/schema/nodes/slab.d.ts.map +1 -0
  66. package/dist/schema/nodes/slab.js +15 -0
  67. package/dist/schema/nodes/wall.d.ts +37 -0
  68. package/dist/schema/nodes/wall.d.ts.map +1 -0
  69. package/dist/schema/nodes/wall.js +30 -0
  70. package/dist/schema/nodes/zone.d.ts +24 -0
  71. package/dist/schema/nodes/zone.d.ts.map +1 -0
  72. package/dist/schema/nodes/zone.js +22 -0
  73. package/dist/schema/types.d.ts +339 -0
  74. package/dist/schema/types.d.ts.map +1 -0
  75. package/dist/schema/types.js +25 -0
  76. package/dist/store/actions/node-actions.d.ts +12 -0
  77. package/dist/store/actions/node-actions.d.ts.map +1 -0
  78. package/dist/store/actions/node-actions.js +121 -0
  79. package/dist/store/use-scene.d.ts +31 -0
  80. package/dist/store/use-scene.d.ts.map +1 -0
  81. package/dist/store/use-scene.js +127 -0
  82. package/dist/systems/ceiling/ceiling-system.d.ts +8 -0
  83. package/dist/systems/ceiling/ceiling-system.d.ts.map +1 -0
  84. package/dist/systems/ceiling/ceiling-system.js +65 -0
  85. package/dist/systems/item/item-system.d.ts +2 -0
  86. package/dist/systems/item/item-system.d.ts.map +1 -0
  87. package/dist/systems/item/item-system.js +43 -0
  88. package/dist/systems/roof/roof-system.d.ts +8 -0
  89. package/dist/systems/roof/roof-system.d.ts.map +1 -0
  90. package/dist/systems/roof/roof-system.js +254 -0
  91. package/dist/systems/slab/slab-system.d.ts +8 -0
  92. package/dist/systems/slab/slab-system.d.ts.map +1 -0
  93. package/dist/systems/slab/slab-system.js +117 -0
  94. package/dist/systems/wall/wall-mitering.d.ts +32 -0
  95. package/dist/systems/wall/wall-mitering.d.ts.map +1 -0
  96. package/dist/systems/wall/wall-mitering.js +214 -0
  97. package/dist/systems/wall/wall-system.d.ts +12 -0
  98. package/dist/systems/wall/wall-system.d.ts.map +1 -0
  99. package/dist/systems/wall/wall-system.js +286 -0
  100. package/dist/utils/types.d.ts +6 -0
  101. package/dist/utils/types.d.ts.map +1 -0
  102. package/dist/utils/types.js +7 -0
  103. 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
+ `);