@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,466 @@
1
+ import { SpatialGrid } from './spatial-grid';
2
+ import { WallSpatialGrid } from './wall-spatial-grid';
3
+ // ============================================================================
4
+ // GEOMETRY HELPERS
5
+ // ============================================================================
6
+ /**
7
+ * Point-in-polygon test using ray casting algorithm.
8
+ */
9
+ export function pointInPolygon(px, pz, polygon) {
10
+ let inside = false;
11
+ const n = polygon.length;
12
+ for (let i = 0, j = n - 1; i < n; j = i++) {
13
+ const xi = polygon[i][0], zi = polygon[i][1];
14
+ const xj = polygon[j][0], zj = polygon[j][1];
15
+ if ((zi > pz) !== (zj > pz) && px < ((xj - xi) * (pz - zi)) / (zj - zi) + xi) {
16
+ inside = !inside;
17
+ }
18
+ }
19
+ return inside;
20
+ }
21
+ /**
22
+ * Compute the 4 XZ footprint corners of an item given its position, dimensions, and Y rotation.
23
+ */
24
+ function getItemFootprint(position, dimensions, rotation, inset = 0) {
25
+ const [x, , z] = position;
26
+ const [w, , d] = dimensions;
27
+ const yRot = rotation[1];
28
+ const halfW = Math.max(0, w / 2 - inset);
29
+ const halfD = Math.max(0, d / 2 - inset);
30
+ const cos = Math.cos(yRot);
31
+ const sin = Math.sin(yRot);
32
+ return [
33
+ [x + (-halfW * cos + halfD * sin), z + (-halfW * sin - halfD * cos)],
34
+ [x + (halfW * cos + halfD * sin), z + (halfW * sin - halfD * cos)],
35
+ [x + (halfW * cos - halfD * sin), z + (halfW * sin + halfD * cos)],
36
+ [x + (-halfW * cos - halfD * sin), z + (-halfW * sin + halfD * cos)],
37
+ ];
38
+ }
39
+ /**
40
+ * Test if two line segments (a1->a2) and (b1->b2) intersect.
41
+ */
42
+ function segmentsIntersect(ax1, az1, ax2, az2, bx1, bz1, bx2, bz2) {
43
+ const cross = (ox, oz, ax, az, bx, bz) => (ax - ox) * (bz - oz) - (az - oz) * (bx - ox);
44
+ const d1 = cross(bx1, bz1, bx2, bz2, ax1, az1);
45
+ const d2 = cross(bx1, bz1, bx2, bz2, ax2, az2);
46
+ const d3 = cross(ax1, az1, ax2, az2, bx1, bz1);
47
+ const d4 = cross(ax1, az1, ax2, az2, bx2, bz2);
48
+ if (((d1 > 0 && d2 < 0) || (d1 < 0 && d2 > 0)) &&
49
+ ((d3 > 0 && d4 < 0) || (d3 < 0 && d4 > 0))) {
50
+ return true;
51
+ }
52
+ // Collinear touching cases
53
+ const onSeg = (px, pz, qx, qz, rx, rz) => Math.min(px, qx) <= rx && rx <= Math.max(px, qx) &&
54
+ Math.min(pz, qz) <= rz && rz <= Math.max(pz, qz);
55
+ if (d1 === 0 && onSeg(bx1, bz1, bx2, bz2, ax1, az1))
56
+ return true;
57
+ if (d2 === 0 && onSeg(bx1, bz1, bx2, bz2, ax2, az2))
58
+ return true;
59
+ if (d3 === 0 && onSeg(ax1, az1, ax2, az2, bx1, bz1))
60
+ return true;
61
+ if (d4 === 0 && onSeg(ax1, az1, ax2, az2, bx2, bz2))
62
+ return true;
63
+ return false;
64
+ }
65
+ /**
66
+ * Test if a line segment intersects any edge of a polygon.
67
+ */
68
+ function segmentIntersectsPolygon(sx1, sz1, sx2, sz2, polygon) {
69
+ const n = polygon.length;
70
+ for (let i = 0; i < n; i++) {
71
+ const j = (i + 1) % n;
72
+ if (segmentsIntersect(sx1, sz1, sx2, sz2, polygon[i][0], polygon[i][1], polygon[j][0], polygon[j][1])) {
73
+ return true;
74
+ }
75
+ }
76
+ return false;
77
+ }
78
+ /**
79
+ * Test if an item's footprint overlaps with a polygon.
80
+ * Checks: any item corner inside polygon, or any polygon vertex inside item AABB, or edges intersect.
81
+ */
82
+ export function itemOverlapsPolygon(position, dimensions, rotation, polygon, inset = 0) {
83
+ const corners = getItemFootprint(position, dimensions, rotation, inset);
84
+ // Check if any item corner is inside the polygon
85
+ for (const [cx, cz] of corners) {
86
+ if (pointInPolygon(cx, cz, polygon))
87
+ return true;
88
+ }
89
+ // Check if any polygon vertex is inside the item footprint
90
+ // (handles case where slab is fully inside a large item)
91
+ for (const [px, pz] of polygon) {
92
+ if (pointInPolygon(px, pz, corners))
93
+ return true;
94
+ }
95
+ // Check if any item edge intersects any polygon edge
96
+ for (let i = 0; i < 4; i++) {
97
+ const j = (i + 1) % 4;
98
+ if (segmentIntersectsPolygon(corners[i][0], corners[i][1], corners[j][0], corners[j][1], polygon))
99
+ return true;
100
+ }
101
+ return false;
102
+ }
103
+ /**
104
+ * Check if wall segment (a) is substantially on polygon edge segment (b).
105
+ * Returns true only if BOTH endpoints of the wall are on or very close to the edge.
106
+ * This prevents walls that just touch one point from being detected.
107
+ */
108
+ function segmentsCollinearAndOverlap(ax1, az1, ax2, az2, bx1, bz1, bx2, bz2) {
109
+ const EPSILON = 1e-6;
110
+ // Cross product to check collinearity
111
+ const cross1 = (ax2 - ax1) * (bz1 - az1) - (az2 - az1) * (bx1 - ax1);
112
+ const cross2 = (ax2 - ax1) * (bz2 - az1) - (az2 - az1) * (bx2 - ax1);
113
+ if (Math.abs(cross1) > EPSILON || Math.abs(cross2) > EPSILON) {
114
+ return false; // Not collinear
115
+ }
116
+ // Check if a point is on segment b
117
+ const onSegment = (px, pz, qx, qz, rx, rz) => Math.min(px, qx) - EPSILON <= rx && rx <= Math.max(px, qx) + EPSILON &&
118
+ Math.min(pz, qz) - EPSILON <= rz && rz <= Math.max(pz, qz) + EPSILON;
119
+ // BOTH endpoints of wall (a) must be on edge (b) for substantial overlap
120
+ const a1OnB = onSegment(bx1, bz1, bx2, bz2, ax1, az1);
121
+ const a2OnB = onSegment(bx1, bz1, bx2, bz2, ax2, az2);
122
+ return a1OnB && a2OnB;
123
+ }
124
+ /**
125
+ * Test if a wall segment overlaps with a polygon.
126
+ * A wall is considered to overlap if:
127
+ * - Its midpoint is inside the polygon (wall crosses through)
128
+ * - At least one endpoint is inside (wall partially or fully in slab)
129
+ * - It's collinear with and overlaps a polygon edge (wall on slab boundary)
130
+ *
131
+ * Note: A wall with just one endpoint touching the edge but the rest outside
132
+ * is NOT considered overlapping (adjacent only).
133
+ */
134
+ export function wallOverlapsPolygon(start, end, polygon) {
135
+ const startInside = pointInPolygon(start[0], start[1], polygon);
136
+ const endInside = pointInPolygon(end[0], end[1], polygon);
137
+ // At least one endpoint strictly inside the polygon
138
+ if (startInside || endInside)
139
+ return true;
140
+ // Check if midpoint is inside (catches walls crossing through)
141
+ const midX = (start[0] + end[0]) / 2;
142
+ const midZ = (start[1] + end[1]) / 2;
143
+ if (pointInPolygon(midX, midZ, polygon))
144
+ return true;
145
+ // Check if the wall is collinear with and overlaps any polygon edge
146
+ const n = polygon.length;
147
+ for (let i = 0; i < n; i++) {
148
+ const j = (i + 1) % n;
149
+ const [p1x, p1z] = polygon[i];
150
+ const [p2x, p2z] = polygon[j];
151
+ if (segmentsCollinearAndOverlap(start[0], start[1], end[0], end[1], p1x, p1z, p2x, p2z)) {
152
+ return true;
153
+ }
154
+ }
155
+ return false;
156
+ }
157
+ export class SpatialGridManager {
158
+ cellSize;
159
+ floorGrids = new Map(); // levelId -> grid
160
+ wallGrids = new Map(); // levelId -> wall grid
161
+ walls = new Map(); // wallId -> wall data (for length calculations)
162
+ slabsByLevel = new Map(); // levelId -> (slabId -> slab)
163
+ ceilingGrids = new Map(); // ceilingId -> grid
164
+ ceilings = new Map(); // ceilingId -> ceiling data
165
+ itemCeilingMap = new Map(); // itemId -> ceilingId (reverse lookup)
166
+ constructor(cellSize = 0.5) {
167
+ this.cellSize = cellSize;
168
+ }
169
+ getFloorGrid(levelId) {
170
+ if (!this.floorGrids.has(levelId)) {
171
+ this.floorGrids.set(levelId, new SpatialGrid({ cellSize: this.cellSize }));
172
+ }
173
+ return this.floorGrids.get(levelId);
174
+ }
175
+ getWallGrid(levelId) {
176
+ if (!this.wallGrids.has(levelId)) {
177
+ this.wallGrids.set(levelId, new WallSpatialGrid());
178
+ }
179
+ return this.wallGrids.get(levelId);
180
+ }
181
+ getWallLength(wallId) {
182
+ const wall = this.walls.get(wallId);
183
+ if (!wall)
184
+ return 0;
185
+ const dx = wall.end[0] - wall.start[0];
186
+ const dy = wall.end[1] - wall.start[1];
187
+ return Math.sqrt(dx * dx + dy * dy);
188
+ }
189
+ getWallHeight(wallId) {
190
+ const wall = this.walls.get(wallId);
191
+ return wall?.height ?? 2.5; // Default wall height
192
+ }
193
+ getCeilingGrid(ceilingId) {
194
+ if (!this.ceilingGrids.has(ceilingId)) {
195
+ this.ceilingGrids.set(ceilingId, new SpatialGrid({ cellSize: this.cellSize }));
196
+ }
197
+ return this.ceilingGrids.get(ceilingId);
198
+ }
199
+ getSlabMap(levelId) {
200
+ if (!this.slabsByLevel.has(levelId)) {
201
+ this.slabsByLevel.set(levelId, new Map());
202
+ }
203
+ return this.slabsByLevel.get(levelId);
204
+ }
205
+ // Called when nodes change
206
+ handleNodeCreated(node, levelId) {
207
+ if (node.type === 'slab') {
208
+ this.getSlabMap(levelId).set(node.id, node);
209
+ }
210
+ else if (node.type === 'ceiling') {
211
+ this.ceilings.set(node.id, node);
212
+ }
213
+ else if (node.type === 'wall') {
214
+ const wall = node;
215
+ this.walls.set(wall.id, wall);
216
+ }
217
+ else if (node.type === 'item') {
218
+ const item = node;
219
+ if (item.asset.attachTo === 'wall' || item.asset.attachTo === 'wall-side') {
220
+ // Wall-attached item - use parentId as the wall ID
221
+ const wallId = item.parentId;
222
+ if (wallId && this.walls.has(wallId)) {
223
+ const wallLength = this.getWallLength(wallId);
224
+ if (wallLength > 0) {
225
+ const [width, height] = item.asset.dimensions;
226
+ const halfW = width / wallLength / 2;
227
+ // Calculate t from local X position (position[0] is distance along wall)
228
+ const t = item.position[0] / wallLength;
229
+ // position[1] is the bottom of the item
230
+ this.getWallGrid(levelId).insert({
231
+ itemId: item.id,
232
+ wallId: wallId,
233
+ tStart: t - halfW,
234
+ tEnd: t + halfW,
235
+ yStart: item.position[1],
236
+ yEnd: item.position[1] + height,
237
+ attachType: item.asset.attachTo,
238
+ side: item.side,
239
+ });
240
+ }
241
+ }
242
+ }
243
+ else if (item.asset.attachTo === 'ceiling') {
244
+ // Ceiling item - use parentId as the ceiling ID
245
+ const ceilingId = item.parentId;
246
+ if (ceilingId && this.ceilings.has(ceilingId)) {
247
+ this.getCeilingGrid(ceilingId).insert(item.id, item.position, item.asset.dimensions, item.rotation);
248
+ this.itemCeilingMap.set(item.id, ceilingId);
249
+ }
250
+ }
251
+ else if (!item.asset.attachTo) {
252
+ // Floor item
253
+ this.getFloorGrid(levelId).insert(item.id, item.position, item.asset.dimensions, item.rotation);
254
+ }
255
+ }
256
+ }
257
+ handleNodeUpdated(node, levelId) {
258
+ if (node.type === 'slab') {
259
+ this.getSlabMap(levelId).set(node.id, node);
260
+ }
261
+ else if (node.type === 'ceiling') {
262
+ this.ceilings.set(node.id, node);
263
+ }
264
+ else if (node.type === 'wall') {
265
+ const wall = node;
266
+ this.walls.set(wall.id, wall);
267
+ }
268
+ else if (node.type === 'item') {
269
+ const item = node;
270
+ if (item.asset.attachTo === 'wall' || item.asset.attachTo === 'wall-side') {
271
+ // Remove old placement and re-insert
272
+ this.getWallGrid(levelId).removeByItemId(item.id);
273
+ const wallId = item.parentId;
274
+ if (wallId && this.walls.has(wallId)) {
275
+ const wallLength = this.getWallLength(wallId);
276
+ if (wallLength > 0) {
277
+ const [width, height] = item.asset.dimensions;
278
+ const halfW = width / wallLength / 2;
279
+ // Calculate t from local X position (position[0] is distance along wall)
280
+ const t = item.position[0] / wallLength;
281
+ // position[1] is the bottom of the item
282
+ this.getWallGrid(levelId).insert({
283
+ itemId: item.id,
284
+ wallId: wallId,
285
+ tStart: t - halfW,
286
+ tEnd: t + halfW,
287
+ yStart: item.position[1],
288
+ yEnd: item.position[1] + height,
289
+ attachType: item.asset.attachTo,
290
+ side: item.side,
291
+ });
292
+ }
293
+ }
294
+ }
295
+ else if (item.asset.attachTo === 'ceiling') {
296
+ // Remove from old ceiling grid
297
+ const oldCeilingId = this.itemCeilingMap.get(item.id);
298
+ if (oldCeilingId) {
299
+ this.getCeilingGrid(oldCeilingId).remove(item.id);
300
+ this.itemCeilingMap.delete(item.id);
301
+ }
302
+ // Insert into new ceiling grid
303
+ const ceilingId = item.parentId;
304
+ if (ceilingId && this.ceilings.has(ceilingId)) {
305
+ this.getCeilingGrid(ceilingId).insert(item.id, item.position, item.asset.dimensions, item.rotation);
306
+ this.itemCeilingMap.set(item.id, ceilingId);
307
+ }
308
+ }
309
+ else if (!item.asset.attachTo) {
310
+ this.getFloorGrid(levelId).update(item.id, item.position, item.asset.dimensions, item.rotation);
311
+ }
312
+ }
313
+ }
314
+ handleNodeDeleted(nodeId, nodeType, levelId) {
315
+ if (nodeType === 'slab') {
316
+ this.getSlabMap(levelId).delete(nodeId);
317
+ }
318
+ else if (nodeType === 'ceiling') {
319
+ this.ceilings.delete(nodeId);
320
+ this.ceilingGrids.delete(nodeId);
321
+ }
322
+ else if (nodeType === 'wall') {
323
+ this.walls.delete(nodeId);
324
+ // Remove all items attached to this wall from the spatial grid
325
+ const removedItemIds = this.getWallGrid(levelId).removeWall(nodeId);
326
+ return removedItemIds; // Caller can use this to delete the items from scene
327
+ }
328
+ else if (nodeType === 'item') {
329
+ this.getFloorGrid(levelId).remove(nodeId);
330
+ this.getWallGrid(levelId).removeByItemId(nodeId);
331
+ // Also clean up ceiling grid
332
+ const oldCeilingId = this.itemCeilingMap.get(nodeId);
333
+ if (oldCeilingId) {
334
+ this.getCeilingGrid(oldCeilingId).remove(nodeId);
335
+ this.itemCeilingMap.delete(nodeId);
336
+ }
337
+ }
338
+ return [];
339
+ }
340
+ // Query methods
341
+ canPlaceOnFloor(levelId, position, dimensions, rotation, ignoreIds) {
342
+ const grid = this.getFloorGrid(levelId);
343
+ return grid.canPlace(position, dimensions, rotation, ignoreIds);
344
+ }
345
+ /**
346
+ * Check if an item can be placed on a wall
347
+ * @param levelId - the level containing the wall
348
+ * @param wallId - the wall to check
349
+ * @param localX - X position in wall-local space (distance from wall start)
350
+ * @param localY - Y position (height from floor)
351
+ * @param dimensions - item dimensions [width, height, depth]
352
+ * @param attachType - 'wall' (needs both sides) or 'wall-side' (needs one side)
353
+ * @param side - which side for 'wall-side' items
354
+ * @param ignoreIds - item IDs to ignore in collision check
355
+ */
356
+ canPlaceOnWall(levelId, wallId, localX, localY, dimensions, attachType = 'wall', side, ignoreIds) {
357
+ const wallLength = this.getWallLength(wallId);
358
+ if (wallLength === 0) {
359
+ return { valid: false, conflictIds: [] };
360
+ }
361
+ const wallHeight = this.getWallHeight(wallId);
362
+ // Convert local X position to parametric t (0-1)
363
+ const tCenter = localX / wallLength;
364
+ const [itemWidth, itemHeight] = dimensions;
365
+ return this.getWallGrid(levelId).canPlaceOnWall(wallId, wallLength, wallHeight, tCenter, itemWidth, localY, itemHeight, attachType, side, ignoreIds);
366
+ }
367
+ getWallForItem(levelId, itemId) {
368
+ return this.getWallGrid(levelId).getWallForItem(itemId);
369
+ }
370
+ /**
371
+ * Get the total slab elevation at a given (x, z) position on a level.
372
+ * Returns the highest slab elevation if the point is inside any slab polygon, otherwise 0.
373
+ */
374
+ getSlabElevationAt(levelId, x, z) {
375
+ const slabMap = this.slabsByLevel.get(levelId);
376
+ if (!slabMap)
377
+ return 0;
378
+ let maxElevation = 0;
379
+ for (const slab of slabMap.values()) {
380
+ if (slab.polygon.length >= 3 && pointInPolygon(x, z, slab.polygon)) {
381
+ const elevation = slab.elevation ?? 0.05;
382
+ if (elevation > maxElevation) {
383
+ maxElevation = elevation;
384
+ }
385
+ }
386
+ }
387
+ return maxElevation;
388
+ }
389
+ /**
390
+ * Get the slab elevation for an item using its full footprint (bounding box).
391
+ * Checks if any part of the item's rotated footprint overlaps with any slab polygon.
392
+ * Returns the highest overlapping slab elevation, or 0 if none.
393
+ */
394
+ getSlabElevationForItem(levelId, position, dimensions, rotation) {
395
+ const slabMap = this.slabsByLevel.get(levelId);
396
+ if (!slabMap)
397
+ return 0;
398
+ let maxElevation = -Infinity;
399
+ for (const slab of slabMap.values()) {
400
+ if (slab.polygon.length >= 3 && itemOverlapsPolygon(position, dimensions, rotation, slab.polygon, 0.01)) {
401
+ const elevation = slab.elevation ?? 0.05;
402
+ if (elevation > maxElevation) {
403
+ maxElevation = elevation;
404
+ }
405
+ }
406
+ }
407
+ return maxElevation === -Infinity ? 0 : maxElevation;
408
+ }
409
+ /**
410
+ * Get the slab elevation for a wall by checking if it overlaps with any slab polygon.
411
+ * Uses wallOverlapsPolygon which handles edge cases (points on boundary, collinear segments).
412
+ * Returns the highest slab elevation found, or 0 if none.
413
+ */
414
+ getSlabElevationForWall(levelId, start, end) {
415
+ const slabMap = this.slabsByLevel.get(levelId);
416
+ if (!slabMap)
417
+ return 0;
418
+ let maxElevation = -Infinity;
419
+ for (const slab of slabMap.values()) {
420
+ if (slab.polygon.length < 3)
421
+ continue;
422
+ if (wallOverlapsPolygon(start, end, slab.polygon)) {
423
+ const elevation = slab.elevation ?? 0.05;
424
+ if (elevation > maxElevation) {
425
+ maxElevation = elevation;
426
+ }
427
+ }
428
+ }
429
+ return maxElevation === -Infinity ? 0 : maxElevation;
430
+ }
431
+ /**
432
+ * Check if an item can be placed on a ceiling.
433
+ * Validates that the footprint is within the ceiling polygon and doesn't overlap other ceiling items.
434
+ */
435
+ canPlaceOnCeiling(ceilingId, position, dimensions, rotation, ignoreIds) {
436
+ const ceiling = this.ceilings.get(ceilingId);
437
+ if (!ceiling || ceiling.polygon.length < 3) {
438
+ return { valid: false, conflictIds: [] };
439
+ }
440
+ // Check that the item footprint is entirely within the ceiling polygon
441
+ const corners = getItemFootprint(position, dimensions, rotation);
442
+ for (const [cx, cz] of corners) {
443
+ if (!pointInPolygon(cx, cz, ceiling.polygon)) {
444
+ return { valid: false, conflictIds: [] };
445
+ }
446
+ }
447
+ // Check for overlaps with other ceiling items
448
+ return this.getCeilingGrid(ceilingId).canPlace(position, dimensions, rotation, ignoreIds);
449
+ }
450
+ clearLevel(levelId) {
451
+ this.floorGrids.delete(levelId);
452
+ this.wallGrids.delete(levelId);
453
+ this.slabsByLevel.delete(levelId);
454
+ }
455
+ clear() {
456
+ this.floorGrids.clear();
457
+ this.wallGrids.clear();
458
+ this.walls.clear();
459
+ this.slabsByLevel.clear();
460
+ this.ceilingGrids.clear();
461
+ this.ceilings.clear();
462
+ this.itemCeilingMap.clear();
463
+ }
464
+ }
465
+ // Singleton instance
466
+ export const spatialGridManager = new SpatialGridManager();
@@ -0,0 +1,4 @@
1
+ import type { AnyNode } from '../../schema';
2
+ export declare function resolveLevelId(node: AnyNode, nodes: Record<string, AnyNode>): string;
3
+ export declare function initSpatialGridSync(): void;
4
+ //# sourceMappingURL=spatial-grid-sync.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"spatial-grid-sync.d.ts","sourceRoot":"","sources":["../../../src/hooks/spatial-grid/spatial-grid-sync.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,OAAO,EAA2C,MAAM,cAAc,CAAA;AAIpF,wBAAgB,cAAc,CAAC,IAAI,EAAE,OAAO,EAAE,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,MAAM,CAmBpF;AAGD,wBAAgB,mBAAmB,SAmElC"}
@@ -0,0 +1,115 @@
1
+ import useScene from '../../store/use-scene';
2
+ import { itemOverlapsPolygon, spatialGridManager, wallOverlapsPolygon } from './spatial-grid-manager';
3
+ export function resolveLevelId(node, nodes) {
4
+ // If the node itself is a level
5
+ if (node.type === 'level')
6
+ return node.id;
7
+ // Walk up parent chain to find level
8
+ // This assumes you track parentId or can derive it
9
+ let current = node;
10
+ while (current) {
11
+ if (current.type === 'level')
12
+ return current.id;
13
+ // Find parent (you might need to add parentId to your schema or derive it)
14
+ if (!current.parentId) {
15
+ current = undefined;
16
+ }
17
+ else {
18
+ current = nodes[current.parentId];
19
+ }
20
+ }
21
+ return 'default'; // fallback for orphaned items
22
+ }
23
+ // Call this once at app initialization
24
+ export function initSpatialGridSync() {
25
+ const store = useScene;
26
+ // 1. Initial sync - process all existing nodes
27
+ const state = store.getState();
28
+ for (const node of Object.values(state.nodes)) {
29
+ const levelId = resolveLevelId(node, state.nodes);
30
+ spatialGridManager.handleNodeCreated(node, levelId);
31
+ }
32
+ // 2. Then subscribe to future changes
33
+ const markDirty = (id) => store.getState().markDirty(id);
34
+ // Subscribe to all changes
35
+ store.subscribe((state, prevState) => {
36
+ // Detect added nodes
37
+ for (const [id, node] of Object.entries(state.nodes)) {
38
+ if (!prevState.nodes[id]) {
39
+ const levelId = resolveLevelId(node, state.nodes);
40
+ spatialGridManager.handleNodeCreated(node, levelId);
41
+ // When a slab is added, mark overlapping items/walls dirty
42
+ if (node.type === 'slab') {
43
+ markNodesOverlappingSlab(node, state.nodes, markDirty);
44
+ }
45
+ }
46
+ }
47
+ // Detect removed nodes
48
+ for (const [id, node] of Object.entries(prevState.nodes)) {
49
+ if (!state.nodes[id]) {
50
+ const levelId = resolveLevelId(node, prevState.nodes);
51
+ spatialGridManager.handleNodeDeleted(id, node.type, levelId);
52
+ // When a slab is removed, mark items/walls that were on it dirty (using current state)
53
+ if (node.type === 'slab') {
54
+ markNodesOverlappingSlab(node, state.nodes, markDirty);
55
+ }
56
+ }
57
+ }
58
+ // Detect updated nodes (items with position/rotation/parentId/side changes, slabs with polygon/elevation changes)
59
+ for (const [id, node] of Object.entries(state.nodes)) {
60
+ const prev = prevState.nodes[id];
61
+ if (!prev)
62
+ continue;
63
+ if (node.type === 'item' && prev.type === 'item') {
64
+ if (!arraysEqual(node.position, prev.position) ||
65
+ !arraysEqual(node.rotation, prev.rotation) ||
66
+ node.parentId !== prev.parentId ||
67
+ node.side !== prev.side) {
68
+ const levelId = resolveLevelId(node, state.nodes);
69
+ spatialGridManager.handleNodeUpdated(node, levelId);
70
+ }
71
+ }
72
+ else if (node.type === 'slab' && prev.type === 'slab') {
73
+ if (node.polygon !== prev.polygon || node.elevation !== prev.elevation) {
74
+ const levelId = resolveLevelId(node, state.nodes);
75
+ spatialGridManager.handleNodeUpdated(node, levelId);
76
+ // Mark nodes overlapping old polygon and new polygon as dirty
77
+ markNodesOverlappingSlab(prev, state.nodes, markDirty);
78
+ markNodesOverlappingSlab(node, state.nodes, markDirty);
79
+ }
80
+ }
81
+ }
82
+ });
83
+ }
84
+ function arraysEqual(a, b) {
85
+ return a.length === b.length && a.every((v, i) => v === b[i]);
86
+ }
87
+ /**
88
+ * Mark all floor items and walls that overlap a slab polygon as dirty.
89
+ */
90
+ function markNodesOverlappingSlab(slab, nodes, markDirty) {
91
+ if (slab.polygon.length < 3)
92
+ return;
93
+ const slabLevelId = resolveLevelId(slab, nodes);
94
+ for (const node of Object.values(nodes)) {
95
+ if (node.type === 'item') {
96
+ const item = node;
97
+ // Only floor items are affected by slabs
98
+ if (item.asset.attachTo)
99
+ continue;
100
+ if (resolveLevelId(node, nodes) !== slabLevelId)
101
+ continue;
102
+ if (itemOverlapsPolygon(item.position, item.asset.dimensions, item.rotation, slab.polygon, 0.01)) {
103
+ markDirty(node.id);
104
+ }
105
+ }
106
+ else if (node.type === 'wall') {
107
+ const wall = node;
108
+ if (resolveLevelId(node, nodes) !== slabLevelId)
109
+ continue;
110
+ if (wallOverlapsPolygon(wall.start, wall.end, slab.polygon)) {
111
+ markDirty(node.id);
112
+ }
113
+ }
114
+ }
115
+ }
@@ -0,0 +1,23 @@
1
+ interface SpatialGridConfig {
2
+ cellSize: number;
3
+ }
4
+ export declare class SpatialGrid {
5
+ private config;
6
+ private cells;
7
+ private itemCells;
8
+ constructor(config: SpatialGridConfig);
9
+ private posToCell;
10
+ private cellKey;
11
+ private getItemCells;
12
+ insert(itemId: string, position: [number, number, number], dimensions: [number, number, number], rotation: [number, number, number]): void;
13
+ remove(itemId: string): void;
14
+ update(itemId: string, position: [number, number, number], dimensions: [number, number, number], rotation: [number, number, number]): void;
15
+ canPlace(position: [number, number, number], dimensions: [number, number, number], rotation: [number, number, number], ignoreIds?: string[]): {
16
+ valid: boolean;
17
+ conflictIds: string[];
18
+ };
19
+ queryRadius(x: number, z: number, radius: number): string[];
20
+ getItemCount(): number;
21
+ }
22
+ export {};
23
+ //# sourceMappingURL=spatial-grid.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"spatial-grid.d.ts","sourceRoot":"","sources":["../../../src/hooks/spatial-grid/spatial-grid.ts"],"names":[],"mappings":"AAMA,UAAU,iBAAiB;IACzB,QAAQ,EAAE,MAAM,CAAA;CACjB;AAED,qBAAa,WAAW;IAIV,OAAO,CAAC,MAAM;IAH1B,OAAO,CAAC,KAAK,CAA+B;IAC5C,OAAO,CAAC,SAAS,CAAkC;gBAE/B,MAAM,EAAE,iBAAiB;IAE7C,OAAO,CAAC,SAAS;IAIjB,OAAO,CAAC,OAAO;IAKf,OAAO,CAAC,YAAY;IAsCpB,MAAM,CACJ,MAAM,EAAE,MAAM,EACd,QAAQ,EAAE,CAAC,MAAM,EAAE,MAAM,EAAE,MAAM,CAAC,EAClC,UAAU,EAAE,CAAC,MAAM,EAAE,MAAM,EAAE,MAAM,CAAC,EACpC,QAAQ,EAAE,CAAC,MAAM,EAAE,MAAM,EAAE,MAAM,CAAC;IAepC,MAAM,CAAC,MAAM,EAAE,MAAM;IAiBrB,MAAM,CACJ,MAAM,EAAE,MAAM,EACd,QAAQ,EAAE,CAAC,MAAM,EAAE,MAAM,EAAE,MAAM,CAAC,EAClC,UAAU,EAAE,CAAC,MAAM,EAAE,MAAM,EAAE,MAAM,CAAC,EACpC,QAAQ,EAAE,CAAC,MAAM,EAAE,MAAM,EAAE,MAAM,CAAC;IAOpC,QAAQ,CACN,QAAQ,EAAE,CAAC,MAAM,EAAE,MAAM,EAAE,MAAM,CAAC,EAClC,UAAU,EAAE,CAAC,MAAM,EAAE,MAAM,EAAE,MAAM,CAAC,EACpC,QAAQ,EAAE,CAAC,MAAM,EAAE,MAAM,EAAE,MAAM,CAAC,EAClC,SAAS,GAAE,MAAM,EAAO,GACvB;QAAE,KAAK,EAAE,OAAO,CAAC;QAAC,WAAW,EAAE,MAAM,EAAE,CAAA;KAAE;IAuB5C,WAAW,CAAC,CAAC,EAAE,MAAM,EAAE,CAAC,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,MAAM,EAAE;IAkB3D,YAAY,IAAI,MAAM;CAGvB"}