@pascal-app/core 0.7.0 → 0.9.1

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 (318) hide show
  1. package/dist/events/bus.d.ts +47 -18
  2. package/dist/events/bus.d.ts.map +1 -1
  3. package/dist/events/bus.js +1 -1
  4. package/dist/hooks/scene-registry/scene-registry.d.ts +6 -22
  5. package/dist/hooks/scene-registry/scene-registry.d.ts.map +1 -1
  6. package/dist/hooks/scene-registry/scene-registry.js +33 -26
  7. package/dist/hooks/spatial-grid/floor-placed-elevation.d.ts +14 -0
  8. package/dist/hooks/spatial-grid/floor-placed-elevation.d.ts.map +1 -0
  9. package/dist/hooks/spatial-grid/floor-placed-elevation.js +51 -0
  10. package/dist/hooks/spatial-grid/floor-placed-elevation.test.d.ts +2 -0
  11. package/dist/hooks/spatial-grid/floor-placed-elevation.test.d.ts.map +1 -0
  12. package/dist/hooks/spatial-grid/floor-placed-elevation.test.js +273 -0
  13. package/dist/hooks/spatial-grid/spatial-grid-manager.d.ts +13 -2
  14. package/dist/hooks/spatial-grid/spatial-grid-manager.d.ts.map +1 -1
  15. package/dist/hooks/spatial-grid/spatial-grid-manager.js +95 -9
  16. package/dist/hooks/spatial-grid/spatial-grid-sync.d.ts +28 -1
  17. package/dist/hooks/spatial-grid/spatial-grid-sync.d.ts.map +1 -1
  18. package/dist/hooks/spatial-grid/spatial-grid-sync.js +91 -18
  19. package/dist/index.d.ts +22 -4
  20. package/dist/index.d.ts.map +1 -1
  21. package/dist/index.js +19 -2
  22. package/dist/lib/polygon-geometry.d.ts.map +1 -1
  23. package/dist/lib/polygon-relations.d.ts +10 -0
  24. package/dist/lib/polygon-relations.d.ts.map +1 -0
  25. package/dist/lib/polygon-relations.js +73 -0
  26. package/dist/lib/space-detection-pause.test.d.ts +2 -0
  27. package/dist/lib/space-detection-pause.test.d.ts.map +1 -0
  28. package/dist/lib/space-detection-pause.test.js +42 -0
  29. package/dist/lib/space-detection.d.ts +30 -1
  30. package/dist/lib/space-detection.d.ts.map +1 -1
  31. package/dist/lib/space-detection.js +222 -41
  32. package/dist/lib/space-detection.test.d.ts +2 -0
  33. package/dist/lib/space-detection.test.d.ts.map +1 -0
  34. package/dist/lib/space-detection.test.js +77 -0
  35. package/dist/material-library.d.ts +1 -1
  36. package/dist/material-library.d.ts.map +1 -1
  37. package/dist/material-library.js +1957 -141
  38. package/dist/registry/__bench__/relations-resolver.bench.d.ts +15 -0
  39. package/dist/registry/__bench__/relations-resolver.bench.d.ts.map +1 -0
  40. package/dist/registry/__bench__/relations-resolver.bench.js +170 -0
  41. package/dist/registry/handles.d.ts +321 -0
  42. package/dist/registry/handles.d.ts.map +1 -0
  43. package/dist/registry/handles.js +24 -0
  44. package/dist/registry/index.d.ts +7 -0
  45. package/dist/registry/index.d.ts.map +1 -0
  46. package/dist/registry/index.js +4 -0
  47. package/dist/registry/registry.d.ts +93 -0
  48. package/dist/registry/registry.d.ts.map +1 -0
  49. package/dist/registry/registry.js +201 -0
  50. package/dist/registry/registry.test.d.ts +2 -0
  51. package/dist/registry/registry.test.d.ts.map +1 -0
  52. package/dist/registry/registry.test.js +183 -0
  53. package/dist/registry/relations-resolver.d.ts +42 -0
  54. package/dist/registry/relations-resolver.d.ts.map +1 -0
  55. package/dist/registry/relations-resolver.js +77 -0
  56. package/dist/registry/relations-resolver.test.d.ts +2 -0
  57. package/dist/registry/relations-resolver.test.d.ts.map +1 -0
  58. package/dist/registry/relations-resolver.test.js +183 -0
  59. package/dist/registry/scene-api.d.ts +44 -0
  60. package/dist/registry/scene-api.d.ts.map +1 -0
  61. package/dist/registry/scene-api.js +111 -0
  62. package/dist/registry/scene-api.test.d.ts +2 -0
  63. package/dist/registry/scene-api.test.d.ts.map +1 -0
  64. package/dist/registry/scene-api.test.js +183 -0
  65. package/dist/registry/subtree.d.ts +67 -0
  66. package/dist/registry/subtree.d.ts.map +1 -0
  67. package/dist/registry/subtree.js +119 -0
  68. package/dist/registry/subtree.test.d.ts +2 -0
  69. package/dist/registry/subtree.test.d.ts.map +1 -0
  70. package/dist/registry/subtree.test.js +133 -0
  71. package/dist/registry/types.d.ts +1420 -0
  72. package/dist/registry/types.d.ts.map +1 -0
  73. package/dist/registry/types.js +1 -0
  74. package/dist/schema/asset-url.test.js +0 -4
  75. package/dist/schema/index.d.ts +17 -2
  76. package/dist/schema/index.d.ts.map +1 -1
  77. package/dist/schema/index.js +17 -3
  78. package/dist/schema/material.d.ts +12 -2
  79. package/dist/schema/material.d.ts.map +1 -1
  80. package/dist/schema/material.js +10 -0
  81. package/dist/schema/nodes/box-vent.d.ts +74 -0
  82. package/dist/schema/nodes/box-vent.d.ts.map +1 -0
  83. package/dist/schema/nodes/box-vent.js +63 -0
  84. package/dist/schema/nodes/building.d.ts +1 -1
  85. package/dist/schema/nodes/building.d.ts.map +1 -1
  86. package/dist/schema/nodes/building.js +3 -2
  87. package/dist/schema/nodes/ceiling.d.ts +3 -1
  88. package/dist/schema/nodes/ceiling.d.ts.map +1 -1
  89. package/dist/schema/nodes/ceiling.js +1 -1
  90. package/dist/schema/nodes/chimney.d.ts +158 -0
  91. package/dist/schema/nodes/chimney.d.ts.map +1 -0
  92. package/dist/schema/nodes/chimney.js +67 -0
  93. package/dist/schema/nodes/column.d.ts +598 -6
  94. package/dist/schema/nodes/column.d.ts.map +1 -1
  95. package/dist/schema/nodes/column.js +586 -6
  96. package/dist/schema/nodes/cupola.d.ts +66 -0
  97. package/dist/schema/nodes/cupola.d.ts.map +1 -0
  98. package/dist/schema/nodes/cupola.js +28 -0
  99. package/dist/schema/nodes/door.d.ts +1 -1
  100. package/dist/schema/nodes/dormer.d.ts +228 -0
  101. package/dist/schema/nodes/dormer.d.ts.map +1 -0
  102. package/dist/schema/nodes/dormer.js +119 -0
  103. package/dist/schema/nodes/downspout.d.ts +75 -0
  104. package/dist/schema/nodes/downspout.d.ts.map +1 -0
  105. package/dist/schema/nodes/downspout.js +63 -0
  106. package/dist/schema/nodes/elevator.d.ts +105 -0
  107. package/dist/schema/nodes/elevator.d.ts.map +1 -0
  108. package/dist/schema/nodes/elevator.js +54 -0
  109. package/dist/schema/nodes/eyebrow-vent.d.ts +68 -0
  110. package/dist/schema/nodes/eyebrow-vent.d.ts.map +1 -0
  111. package/dist/schema/nodes/eyebrow-vent.js +40 -0
  112. package/dist/schema/nodes/fence.d.ts +3 -2
  113. package/dist/schema/nodes/fence.d.ts.map +1 -1
  114. package/dist/schema/nodes/fence.js +4 -2
  115. package/dist/schema/nodes/gutter.d.ts +84 -0
  116. package/dist/schema/nodes/gutter.d.ts.map +1 -0
  117. package/dist/schema/nodes/gutter.js +76 -0
  118. package/dist/schema/nodes/item.d.ts +16 -0
  119. package/dist/schema/nodes/item.d.ts.map +1 -1
  120. package/dist/schema/nodes/item.js +21 -0
  121. package/dist/schema/nodes/level.d.ts +1 -1
  122. package/dist/schema/nodes/level.d.ts.map +1 -1
  123. package/dist/schema/nodes/level.js +2 -0
  124. package/dist/schema/nodes/ridge-vent.d.ts +67 -0
  125. package/dist/schema/nodes/ridge-vent.d.ts.map +1 -0
  126. package/dist/schema/nodes/ridge-vent.js +30 -0
  127. package/dist/schema/nodes/roof-segment.d.ts +198 -4
  128. package/dist/schema/nodes/roof-segment.d.ts.map +1 -1
  129. package/dist/schema/nodes/roof-segment.js +254 -3
  130. package/dist/schema/nodes/roof.d.ts +6 -6
  131. package/dist/schema/nodes/roof.d.ts.map +1 -1
  132. package/dist/schema/nodes/roof.js +9 -21
  133. package/dist/schema/nodes/shelf.d.ts +105 -0
  134. package/dist/schema/nodes/shelf.d.ts.map +1 -0
  135. package/dist/schema/nodes/shelf.js +84 -0
  136. package/dist/schema/nodes/site.d.ts +1 -116
  137. package/dist/schema/nodes/site.d.ts.map +1 -1
  138. package/dist/schema/nodes/site.js +2 -6
  139. package/dist/schema/nodes/skylight.d.ts +279 -0
  140. package/dist/schema/nodes/skylight.d.ts.map +1 -0
  141. package/dist/schema/nodes/skylight.js +152 -0
  142. package/dist/schema/nodes/slab.d.ts +3 -1
  143. package/dist/schema/nodes/slab.d.ts.map +1 -1
  144. package/dist/schema/nodes/slab.js +1 -1
  145. package/dist/schema/nodes/solar-panel.d.ts +117 -0
  146. package/dist/schema/nodes/solar-panel.d.ts.map +1 -0
  147. package/dist/schema/nodes/solar-panel.js +47 -0
  148. package/dist/schema/nodes/stair-segment.d.ts +1 -1
  149. package/dist/schema/nodes/stair.d.ts +6 -6
  150. package/dist/schema/nodes/stair.d.ts.map +1 -1
  151. package/dist/schema/nodes/stair.js +9 -7
  152. package/dist/schema/nodes/surface-hole-metadata.d.ts +2 -0
  153. package/dist/schema/nodes/surface-hole-metadata.d.ts.map +1 -1
  154. package/dist/schema/nodes/surface-hole-metadata.js +3 -1
  155. package/dist/schema/nodes/turbine-vent.d.ts +68 -0
  156. package/dist/schema/nodes/turbine-vent.d.ts.map +1 -0
  157. package/dist/schema/nodes/turbine-vent.js +41 -0
  158. package/dist/schema/nodes/wall.d.ts +3 -3
  159. package/dist/schema/nodes/window.d.ts +1 -1
  160. package/dist/schema/types.d.ts +1448 -236
  161. package/dist/schema/types.d.ts.map +1 -1
  162. package/dist/schema/types.js +26 -0
  163. package/dist/services/alignment-anchors.d.ts +100 -0
  164. package/dist/services/alignment-anchors.d.ts.map +1 -0
  165. package/dist/services/alignment-anchors.js +283 -0
  166. package/dist/services/alignment-anchors.test.d.ts +2 -0
  167. package/dist/services/alignment-anchors.test.d.ts.map +1 -0
  168. package/dist/services/alignment-anchors.test.js +383 -0
  169. package/dist/services/alignment.d.ts +126 -0
  170. package/dist/services/alignment.d.ts.map +1 -0
  171. package/dist/services/alignment.js +169 -0
  172. package/dist/services/alignment.test.d.ts +2 -0
  173. package/dist/services/alignment.test.d.ts.map +1 -0
  174. package/dist/services/alignment.test.js +92 -0
  175. package/dist/services/drag-session.d.ts +47 -0
  176. package/dist/services/drag-session.d.ts.map +1 -0
  177. package/dist/services/drag-session.js +100 -0
  178. package/dist/services/drag-session.test.d.ts +2 -0
  179. package/dist/services/drag-session.test.d.ts.map +1 -0
  180. package/dist/services/drag-session.test.js +219 -0
  181. package/dist/services/hosting.d.ts +80 -0
  182. package/dist/services/hosting.d.ts.map +1 -0
  183. package/dist/services/hosting.js +122 -0
  184. package/dist/services/hosting.test.d.ts +2 -0
  185. package/dist/services/hosting.test.d.ts.map +1 -0
  186. package/dist/services/hosting.test.js +199 -0
  187. package/dist/services/index.d.ts +7 -0
  188. package/dist/services/index.d.ts.map +1 -0
  189. package/dist/services/index.js +6 -0
  190. package/dist/services/movement.d.ts +48 -0
  191. package/dist/services/movement.d.ts.map +1 -0
  192. package/dist/services/movement.js +65 -0
  193. package/dist/services/movement.test.d.ts +2 -0
  194. package/dist/services/movement.test.d.ts.map +1 -0
  195. package/dist/services/movement.test.js +133 -0
  196. package/dist/services/single-undo-dance.test.d.ts +2 -0
  197. package/dist/services/single-undo-dance.test.d.ts.map +1 -0
  198. package/dist/services/single-undo-dance.test.js +210 -0
  199. package/dist/services/snap.d.ts +72 -0
  200. package/dist/services/snap.d.ts.map +1 -0
  201. package/dist/services/snap.js +120 -0
  202. package/dist/services/snap.test.d.ts +2 -0
  203. package/dist/services/snap.test.d.ts.map +1 -0
  204. package/dist/services/snap.test.js +95 -0
  205. package/dist/solar-panel-presets.d.ts +17 -0
  206. package/dist/solar-panel-presets.d.ts.map +1 -0
  207. package/dist/solar-panel-presets.js +43 -0
  208. package/dist/store/actions/node-actions.d.ts +14 -2
  209. package/dist/store/actions/node-actions.d.ts.map +1 -1
  210. package/dist/store/actions/node-actions.js +499 -20
  211. package/dist/store/actions/node-mutation-sanitize.test.d.ts +2 -0
  212. package/dist/store/actions/node-mutation-sanitize.test.d.ts.map +1 -0
  213. package/dist/store/actions/node-mutation-sanitize.test.js +147 -0
  214. package/dist/store/actions/reparent.test.d.ts +2 -0
  215. package/dist/store/actions/reparent.test.d.ts.map +1 -0
  216. package/dist/store/actions/reparent.test.js +212 -0
  217. package/dist/store/use-interactive.d.ts +39 -0
  218. package/dist/store/use-interactive.d.ts.map +1 -1
  219. package/dist/store/use-interactive.js +75 -0
  220. package/dist/store/use-live-node-overrides.d.ts +22 -0
  221. package/dist/store/use-live-node-overrides.d.ts.map +1 -0
  222. package/dist/store/use-live-node-overrides.js +59 -0
  223. package/dist/store/use-scene-elevator-migration.test.d.ts +2 -0
  224. package/dist/store/use-scene-elevator-migration.test.d.ts.map +1 -0
  225. package/dist/store/use-scene-elevator-migration.test.js +109 -0
  226. package/dist/store/use-scene.d.ts +11 -0
  227. package/dist/store/use-scene.d.ts.map +1 -1
  228. package/dist/store/use-scene.js +207 -11
  229. package/dist/systems/elevator/elevator-dispatch.d.ts +12 -0
  230. package/dist/systems/elevator/elevator-dispatch.d.ts.map +1 -0
  231. package/dist/systems/elevator/elevator-dispatch.js +57 -0
  232. package/dist/systems/elevator/elevator-geometry.d.ts +15 -0
  233. package/dist/systems/elevator/elevator-geometry.d.ts.map +1 -0
  234. package/dist/systems/elevator/elevator-geometry.js +50 -0
  235. package/dist/systems/elevator/elevator-opening-sync.d.ts +6 -0
  236. package/dist/systems/elevator/elevator-opening-sync.d.ts.map +1 -0
  237. package/dist/systems/elevator/elevator-opening-sync.js +218 -0
  238. package/dist/systems/elevator/elevator-opening-sync.test.d.ts +2 -0
  239. package/dist/systems/elevator/elevator-opening-sync.test.d.ts.map +1 -0
  240. package/dist/systems/elevator/elevator-opening-sync.test.js +146 -0
  241. package/dist/systems/elevator/elevator-opening-system.d.ts +2 -0
  242. package/dist/systems/elevator/elevator-opening-system.d.ts.map +1 -0
  243. package/dist/systems/elevator/elevator-opening-system.js +48 -0
  244. package/dist/systems/elevator/elevator-runtime-system.d.ts +2 -0
  245. package/dist/systems/elevator/elevator-runtime-system.d.ts.map +1 -0
  246. package/dist/systems/elevator/elevator-runtime-system.js +8 -0
  247. package/dist/systems/elevator/elevator-runtime.d.ts +18 -0
  248. package/dist/systems/elevator/elevator-runtime.d.ts.map +1 -0
  249. package/dist/systems/elevator/elevator-runtime.js +225 -0
  250. package/dist/systems/elevator/elevator-runtime.test.d.ts +2 -0
  251. package/dist/systems/elevator/elevator-runtime.test.d.ts.map +1 -0
  252. package/dist/systems/elevator/elevator-runtime.test.js +64 -0
  253. package/dist/systems/elevator/elevator-service.d.ts +19 -0
  254. package/dist/systems/elevator/elevator-service.d.ts.map +1 -0
  255. package/dist/systems/elevator/elevator-service.js +111 -0
  256. package/dist/systems/stair/stair-footprint.d.ts +44 -0
  257. package/dist/systems/stair/stair-footprint.d.ts.map +1 -0
  258. package/dist/systems/stair/stair-footprint.js +168 -0
  259. package/dist/systems/stair/stair-opening-preview.d.ts +1777 -0
  260. package/dist/systems/stair/stair-opening-preview.d.ts.map +1 -0
  261. package/dist/systems/stair/stair-opening-preview.js +93 -0
  262. package/dist/systems/stair/stair-opening-preview.test.d.ts +2 -0
  263. package/dist/systems/stair/stair-opening-preview.test.d.ts.map +1 -0
  264. package/dist/systems/stair/stair-opening-preview.test.js +80 -0
  265. package/dist/systems/stair/stair-opening-sync.d.ts.map +1 -1
  266. package/dist/systems/stair/stair-opening-sync.js +183 -185
  267. package/dist/systems/stair/stair-opening-sync.test.js +385 -4
  268. package/dist/systems/stair/stair-opening-system.d.ts +2 -0
  269. package/dist/systems/stair/stair-opening-system.d.ts.map +1 -0
  270. package/dist/systems/stair/stair-opening-system.js +104 -0
  271. package/dist/systems/wall/wall-curve.d.ts +1 -1
  272. package/dist/systems/wall/wall-curve.d.ts.map +1 -1
  273. package/dist/systems/wall/wall-curve.js +1 -1
  274. package/dist/systems/wall/wall-mitering.d.ts.map +1 -1
  275. package/dist/systems/wall/wall-mitering.js +2 -6
  276. package/dist/systems/wall/wall-move.d.ts +48 -0
  277. package/dist/systems/wall/wall-move.d.ts.map +1 -0
  278. package/dist/systems/wall/wall-move.js +193 -0
  279. package/dist/utils/clone-scene-graph.d.ts +6 -4
  280. package/dist/utils/clone-scene-graph.d.ts.map +1 -1
  281. package/dist/utils/clone-scene-graph.js +6 -4
  282. package/dist/utils/clone-scene-graph.test.d.ts +2 -0
  283. package/dist/utils/clone-scene-graph.test.d.ts.map +1 -0
  284. package/dist/utils/clone-scene-graph.test.js +59 -0
  285. package/dist/validation/validate-build-json.d.ts +43 -0
  286. package/dist/validation/validate-build-json.d.ts.map +1 -0
  287. package/dist/validation/validate-build-json.js +227 -0
  288. package/package.json +11 -3
  289. package/dist/materials.d.ts +0 -10
  290. package/dist/materials.d.ts.map +0 -1
  291. package/dist/materials.js +0 -22
  292. package/dist/systems/ceiling/ceiling-system.d.ts +0 -8
  293. package/dist/systems/ceiling/ceiling-system.d.ts.map +0 -1
  294. package/dist/systems/ceiling/ceiling-system.js +0 -92
  295. package/dist/systems/door/door-system.d.ts +0 -2
  296. package/dist/systems/door/door-system.d.ts.map +0 -1
  297. package/dist/systems/door/door-system.js +0 -195
  298. package/dist/systems/fence/fence-system.d.ts +0 -2
  299. package/dist/systems/fence/fence-system.d.ts.map +0 -1
  300. package/dist/systems/fence/fence-system.js +0 -187
  301. package/dist/systems/item/item-system.d.ts +0 -2
  302. package/dist/systems/item/item-system.d.ts.map +0 -1
  303. package/dist/systems/item/item-system.js +0 -48
  304. package/dist/systems/roof/roof-system.d.ts +0 -16
  305. package/dist/systems/roof/roof-system.d.ts.map +0 -1
  306. package/dist/systems/roof/roof-system.js +0 -797
  307. package/dist/systems/slab/slab-system.d.ts +0 -8
  308. package/dist/systems/slab/slab-system.d.ts.map +0 -1
  309. package/dist/systems/slab/slab-system.js +0 -214
  310. package/dist/systems/stair/stair-system.d.ts +0 -2
  311. package/dist/systems/stair/stair-system.d.ts.map +0 -1
  312. package/dist/systems/stair/stair-system.js +0 -776
  313. package/dist/systems/wall/wall-system.d.ts +0 -12
  314. package/dist/systems/wall/wall-system.d.ts.map +0 -1
  315. package/dist/systems/wall/wall-system.js +0 -455
  316. package/dist/systems/window/window-system.d.ts +0 -2
  317. package/dist/systems/window/window-system.d.ts.map +0 -1
  318. package/dist/systems/window/window-system.js +0 -131
@@ -0,0 +1,42 @@
1
+ import { afterEach, beforeEach, describe, expect, test } from 'bun:test';
2
+ import { isSpaceDetectionPaused, pauseSpaceDetection, resumeSpaceDetection, } from './space-detection';
3
+ // The pause flag is module-level (matching the existing pauseSceneHistory
4
+ // refcount in store/history-control.ts). Reset it before/after each test so
5
+ // leftover depth from one case can't bleed into another.
6
+ function drain() {
7
+ for (let i = 0; i < 64 && isSpaceDetectionPaused(); i += 1) {
8
+ resumeSpaceDetection();
9
+ }
10
+ }
11
+ beforeEach(drain);
12
+ afterEach(drain);
13
+ describe('space-detection pause primitive', () => {
14
+ test('defaults to not paused', () => {
15
+ expect(isSpaceDetectionPaused()).toBe(false);
16
+ });
17
+ test('pauseSpaceDetection flips the flag, resumeSpaceDetection clears it', () => {
18
+ pauseSpaceDetection();
19
+ expect(isSpaceDetectionPaused()).toBe(true);
20
+ resumeSpaceDetection();
21
+ expect(isSpaceDetectionPaused()).toBe(false);
22
+ });
23
+ test('refcount — pause depth survives mismatched resumes from a second source', () => {
24
+ pauseSpaceDetection();
25
+ pauseSpaceDetection();
26
+ expect(isSpaceDetectionPaused()).toBe(true);
27
+ resumeSpaceDetection();
28
+ expect(isSpaceDetectionPaused()).toBe(true);
29
+ resumeSpaceDetection();
30
+ expect(isSpaceDetectionPaused()).toBe(false);
31
+ });
32
+ test('resume is a no-op when not currently paused', () => {
33
+ expect(isSpaceDetectionPaused()).toBe(false);
34
+ resumeSpaceDetection();
35
+ resumeSpaceDetection();
36
+ expect(isSpaceDetectionPaused()).toBe(false);
37
+ pauseSpaceDetection();
38
+ expect(isSpaceDetectionPaused()).toBe(true);
39
+ resumeSpaceDetection();
40
+ expect(isSpaceDetectionPaused()).toBe(false);
41
+ });
42
+ });
@@ -1,4 +1,4 @@
1
- import { type WallNode } from '../schema';
1
+ import { type CeilingNode as CeilingNodeType, type SlabNode as SlabNodeType, type WallNode } from '../schema';
2
2
  type Point2D = {
3
3
  x: number;
4
4
  y: number;
@@ -15,12 +15,41 @@ type WallSideUpdate = {
15
15
  frontSide: 'interior' | 'exterior' | 'unknown';
16
16
  backSide: 'interior' | 'exterior' | 'unknown';
17
17
  };
18
+ export type AutoSlabSyncPlan = {
19
+ create: SlabNodeType[];
20
+ update: Array<{
21
+ id: SlabNodeType['id'];
22
+ data: Partial<SlabNodeType>;
23
+ }>;
24
+ delete: Array<SlabNodeType['id']>;
25
+ };
26
+ export type AutoCeilingSyncPlan = {
27
+ create: CeilingNodeType[];
28
+ update: Array<{
29
+ id: CeilingNodeType['id'];
30
+ data: Partial<CeilingNodeType>;
31
+ }>;
32
+ delete: Array<CeilingNodeType['id']>;
33
+ };
34
+ export type AutoCeilingPlanningContext = {
35
+ walls?: WallNode[];
36
+ slabs?: SlabNodeType[];
37
+ };
18
38
  export declare function resolveWallSurfaceSides(wall: Pick<WallNode, 'start' | 'end' | 'thickness' | 'frontSide' | 'backSide'>, roomPolygons: Point2D[][]): Pick<WallSideUpdate, 'frontSide' | 'backSide'>;
39
+ export declare function planAutoSlabsForLevel(roomPolygons: Point2D[][], existingSlabs: SlabNodeType[]): AutoSlabSyncPlan;
40
+ export declare function projectAutoSlabsForPlan(existingSlabs: SlabNodeType[], plan: AutoSlabSyncPlan): SlabNodeType[];
41
+ export declare function planAutoCeilingsForLevel(roomPolygons: Point2D[][], existingCeilings: CeilingNodeType[], context?: AutoCeilingPlanningContext): AutoCeilingSyncPlan;
19
42
  export declare function detectSpacesForLevel(levelId: string, walls: WallNode[]): {
20
43
  roomPolygons: Point2D[][];
21
44
  spaces: Space[];
22
45
  wallUpdates: WallSideUpdate[];
23
46
  };
47
+ /** Pause the wall-driven auto slab/ceiling sync. Refcounted — pair with `resumeSpaceDetection`. */
48
+ export declare function pauseSpaceDetection(): void;
49
+ /** Resume the wall-driven auto slab/ceiling sync. No-op if not currently paused. */
50
+ export declare function resumeSpaceDetection(): void;
51
+ /** True iff the wall-driven auto slab/ceiling sync is currently paused. */
52
+ export declare function isSpaceDetectionPaused(): boolean;
24
53
  export declare function initSpaceDetectionSync(sceneStore: any, editorStore: any): () => void;
25
54
  export declare function wallTouchesOthers(wall: WallNode, otherWalls: WallNode[]): boolean;
26
55
  export {};
@@ -1 +1 @@
1
- {"version":3,"file":"space-detection.d.ts","sourceRoot":"","sources":["../../src/lib/space-detection.ts"],"names":[],"mappings":"AAKA,OAAO,EAA6F,KAAK,QAAQ,EAAE,MAAM,WAAW,CAAA;AAQpI,KAAK,OAAO,GAAG;IAAE,CAAC,EAAE,MAAM,CAAC;IAAC,CAAC,EAAE,MAAM,CAAA;CAAE,CAAA;AAEvC,MAAM,MAAM,KAAK,GAAG;IAClB,EAAE,EAAE,MAAM,CAAA;IACV,OAAO,EAAE,MAAM,CAAA;IACf,OAAO,EAAE,KAAK,CAAC,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,CAAA;IAChC,OAAO,EAAE,MAAM,EAAE,CAAA;IACjB,UAAU,EAAE,OAAO,CAAA;CACpB,CAAA;AAED,KAAK,cAAc,GAAG;IACpB,MAAM,EAAE,MAAM,CAAA;IACd,SAAS,EAAE,UAAU,GAAG,UAAU,GAAG,SAAS,CAAA;IAC9C,QAAQ,EAAE,UAAU,GAAG,UAAU,GAAG,SAAS,CAAA;CAC9C,CAAA;AA4VD,wBAAgB,uBAAuB,CACrC,IAAI,EAAE,IAAI,CAAC,QAAQ,EAAE,OAAO,GAAG,KAAK,GAAG,WAAW,GAAG,WAAW,GAAG,UAAU,CAAC,EAC9E,YAAY,EAAE,OAAO,EAAE,EAAE,GACxB,IAAI,CAAC,cAAc,EAAE,WAAW,GAAG,UAAU,CAAC,CA4ChD;AA0WD,wBAAgB,oBAAoB,CAAC,OAAO,EAAE,MAAM,EAAE,KAAK,EAAE,QAAQ,EAAE;;;;EAEtE;AAsED,wBAAgB,sBAAsB,CAAC,UAAU,EAAE,GAAG,EAAE,WAAW,EAAE,GAAG,GAAG,MAAM,IAAI,CAuDpF;AAED,wBAAgB,iBAAiB,CAAC,IAAI,EAAE,QAAQ,EAAE,UAAU,EAAE,QAAQ,EAAE,GAAG,OAAO,CAiBjF"}
1
+ {"version":3,"file":"space-detection.d.ts","sourceRoot":"","sources":["../../src/lib/space-detection.ts"],"names":[],"mappings":"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"}
@@ -1,12 +1,14 @@
1
- import { getClampedWallCurveOffset, getWallCurveFrameAt, isCurvedWall, } from '../systems/wall/wall-curve';
2
- import { CeilingNode, SlabNode } from '../schema';
1
+ import { CeilingNode, SlabNode, } from '../schema';
3
2
  import { getSceneHistoryPauseDepth, pauseSceneHistory, resumeSceneHistory, } from '../store/history-control';
3
+ import { getClampedWallCurveOffset, getWallCurveFrameAt, isCurvedWall, } from '../systems/wall/wall-curve';
4
4
  import { simplifyClosedPolygon } from './polygon-geometry';
5
5
  const DEFAULT_AUTO_SLAB_ELEVATION = 0.05;
6
6
  const DEFAULT_AUTO_CEILING_HEIGHT = 2.5;
7
+ const CEILING_HEIGHT_EPSILON = 1e-6;
7
8
  const ROOM_CURVE_TOLERANCE = 0.04;
8
9
  const MAX_CURVE_SUBDIVISION_DEPTH = 6;
9
10
  const AUTO_SLAB_POLYGON_SIMPLIFY_TOLERANCE = 0.08;
11
+ const WALL_ROOM_BOUNDARY_TOLERANCE = 0.08;
10
12
  function pointFromTuple(point) {
11
13
  return { x: point[0], y: point[1] };
12
14
  }
@@ -97,10 +99,10 @@ function polygonCentroid(points) {
97
99
  };
98
100
  }
99
101
  function bboxOf(points) {
100
- let minX = Infinity;
101
- let minY = Infinity;
102
- let maxX = -Infinity;
103
- let maxY = -Infinity;
102
+ let minX = Number.POSITIVE_INFINITY;
103
+ let minY = Number.POSITIVE_INFINITY;
104
+ let maxX = Number.NEGATIVE_INFINITY;
105
+ let maxY = Number.NEGATIVE_INFINITY;
104
106
  for (const point of points) {
105
107
  minX = Math.min(minX, point.x);
106
108
  minY = Math.min(minY, point.y);
@@ -114,6 +116,80 @@ function bboxOverlapArea(a, b) {
114
116
  const iy = Math.max(0, Math.min(a.maxY, b.maxY) - Math.max(a.minY, b.minY));
115
117
  return ix * iy;
116
118
  }
119
+ function pointDistanceToPolygonBoundary(point, polygon) {
120
+ let minDistance = Number.POSITIVE_INFINITY;
121
+ for (let index = 0; index < polygon.length; index += 1) {
122
+ const start = polygon[index];
123
+ const end = polygon[(index + 1) % polygon.length];
124
+ if (!(start && end))
125
+ continue;
126
+ minDistance = Math.min(minDistance, distanceToSegment(pointToTuple(point), pointToTuple(start), pointToTuple(end)));
127
+ }
128
+ return minDistance;
129
+ }
130
+ function wallBoundsRoom(wall, roomPolygon) {
131
+ const sampled = sampleWallPointsForRoomDetection(wall);
132
+ if (sampled.length === 0)
133
+ return false;
134
+ const candidates = sampled.length === 2
135
+ ? [
136
+ sampled[0],
137
+ {
138
+ x: (sampled[0].x + sampled[1].x) / 2,
139
+ y: (sampled[0].y + sampled[1].y) / 2,
140
+ },
141
+ sampled[1],
142
+ ]
143
+ : sampled;
144
+ const matchingPoints = candidates.filter((point) => pointDistanceToPolygonBoundary(point, roomPolygon) <= WALL_ROOM_BOUNDARY_TOLERANCE);
145
+ return matchingPoints.length >= 2;
146
+ }
147
+ function pointIsOnSlab(point, slab) {
148
+ if (slab.polygon.length < 3)
149
+ return false;
150
+ const slabPolygon = slab.polygon.map(pointFromTuple);
151
+ if (!pointInPolygon(point, slabPolygon))
152
+ return false;
153
+ for (const hole of slab.holes ?? []) {
154
+ if (hole.length >= 3 && pointInPolygon(point, hole.map(pointFromTuple))) {
155
+ return false;
156
+ }
157
+ }
158
+ return true;
159
+ }
160
+ function slabSupportsRoom(roomPolygon, slab) {
161
+ if (slab.polygon.length < 3)
162
+ return false;
163
+ if (polygonSignature(slab.polygon.map(pointFromTuple)) === polygonSignature(roomPolygon)) {
164
+ return true;
165
+ }
166
+ return pointIsOnSlab(polygonCentroid(roomPolygon), slab);
167
+ }
168
+ function resolveRoomSlabElevation(roomPolygon, slabs = []) {
169
+ let maxElevation = 0;
170
+ for (const slab of slabs) {
171
+ if (!slabSupportsRoom(roomPolygon, slab))
172
+ continue;
173
+ maxElevation = Math.max(maxElevation, slab.elevation ?? DEFAULT_AUTO_SLAB_ELEVATION);
174
+ }
175
+ return maxElevation;
176
+ }
177
+ function resolveRoomWallHeight(roomPolygon, walls = []) {
178
+ let maxHeight = 0;
179
+ for (const wall of walls) {
180
+ if (!wallBoundsRoom(wall, roomPolygon))
181
+ continue;
182
+ const height = wall.height ?? DEFAULT_AUTO_CEILING_HEIGHT;
183
+ if (Number.isFinite(height)) {
184
+ maxHeight = Math.max(maxHeight, height);
185
+ }
186
+ }
187
+ return maxHeight > 0 ? maxHeight : DEFAULT_AUTO_CEILING_HEIGHT;
188
+ }
189
+ function resolveAutoCeilingHeight(roomPolygon, context = {}) {
190
+ return (resolveRoomSlabElevation(roomPolygon, context.slabs) +
191
+ resolveRoomWallHeight(roomPolygon, context.walls));
192
+ }
117
193
  function getWallDirection(wall) {
118
194
  const dx = wall.end[0] - wall.start[0];
119
195
  const dy = wall.end[1] - wall.start[1];
@@ -266,7 +342,7 @@ function extractRoomPolygons(walls) {
266
342
  const signedArea = polygonArea(polygon);
267
343
  if (signedArea <= 0)
268
344
  continue;
269
- if (signedArea < 0.5 || signedArea > 10000)
345
+ if (signedArea < 0.5 || signedArea > 10_000)
270
346
  continue;
271
347
  const signature = polygonSignature(polygon);
272
348
  if (faces.some((face) => polygonSignature(face) === signature))
@@ -340,12 +416,48 @@ function wallGeometrySignature(wall) {
340
416
  wall.end[0].toFixed(4),
341
417
  wall.end[1].toFixed(4),
342
418
  (wall.thickness ?? 0.2).toFixed(4),
419
+ (wall.height ?? DEFAULT_AUTO_CEILING_HEIGHT).toFixed(4),
343
420
  getClampedWallCurveOffset(wall).toFixed(4),
344
421
  ].join('|');
345
422
  }
346
423
  function levelWallSnapshot(walls) {
347
424
  return walls.map(wallGeometrySignature).sort().join('||');
348
425
  }
426
+ function slabGeometrySignature(slab) {
427
+ const polygon = slab.polygon
428
+ .map((point) => `${point[0].toFixed(4)},${point[1].toFixed(4)}`)
429
+ .join(';');
430
+ const holes = (slab.holes ?? [])
431
+ .map((hole) => hole.map((point) => `${point[0].toFixed(4)},${point[1].toFixed(4)}`).join(';'))
432
+ .join('/');
433
+ return [slab.id, (slab.elevation ?? DEFAULT_AUTO_SLAB_ELEVATION).toFixed(4), polygon, holes].join('|');
434
+ }
435
+ function levelSlabSnapshot(slabs) {
436
+ return slabs.map(slabGeometrySignature).sort().join('||');
437
+ }
438
+ function levelStructureSnapshots(nodes) {
439
+ const byLevel = new Map();
440
+ const getEntry = (levelId) => {
441
+ const entry = byLevel.get(levelId) ?? { walls: [], slabs: [] };
442
+ byLevel.set(levelId, entry);
443
+ return entry;
444
+ };
445
+ for (const node of Object.values(nodes)) {
446
+ if (!(node && typeof node === 'object' && 'parentId' in node && node.parentId))
447
+ continue;
448
+ if (node.type === 'wall') {
449
+ getEntry(node.parentId).walls.push(node);
450
+ }
451
+ else if (node.type === 'slab') {
452
+ getEntry(node.parentId).slabs.push(SlabNode.parse(node));
453
+ }
454
+ }
455
+ const snapshots = new Map();
456
+ for (const [levelId, entry] of byLevel.entries()) {
457
+ snapshots.set(levelId, `${levelWallSnapshot(entry.walls)}##${levelSlabSnapshot(entry.slabs)}`);
458
+ }
459
+ return snapshots;
460
+ }
349
461
  function buildSpace(levelId, polygon) {
350
462
  const signature = polygonSignature(polygon);
351
463
  return {
@@ -356,7 +468,7 @@ function buildSpace(levelId, polygon) {
356
468
  isExterior: false,
357
469
  };
358
470
  }
359
- function syncAutoSlabsForLevel(levelId, roomPolygons, existingSlabs, sceneStore) {
471
+ export function planAutoSlabsForLevel(roomPolygons, existingSlabs) {
360
472
  const manualSlabs = existingSlabs.filter((slab) => !slab.autoFromWalls);
361
473
  const manualSignatures = new Set(manualSlabs.map((slab) => polygonSignature(slab.polygon.map(pointFromTuple))));
362
474
  const detected = roomPolygons
@@ -459,17 +571,42 @@ function syncAutoSlabsForLevel(levelId, roomPolygons, existingSlabs, sceneStore)
459
571
  autoFromWalls: true,
460
572
  }));
461
573
  }
462
- if (slabsToDelete.length > 0) {
463
- sceneStore.getState().deleteNodes(slabsToDelete);
574
+ return {
575
+ create: slabsToCreate,
576
+ update: slabsToUpdate,
577
+ delete: slabsToDelete,
578
+ };
579
+ }
580
+ function syncAutoSlabsForLevel(levelId, roomPolygons, existingSlabs, sceneStore) {
581
+ const plan = planAutoSlabsForLevel(roomPolygons, existingSlabs);
582
+ if (plan.delete.length > 0) {
583
+ sceneStore.getState().deleteNodes(plan.delete);
584
+ }
585
+ if (plan.update.length > 0) {
586
+ sceneStore.getState().updateNodes(plan.update);
464
587
  }
465
- if (slabsToUpdate.length > 0) {
466
- sceneStore.getState().updateNodes(slabsToUpdate);
588
+ if (plan.create.length > 0) {
589
+ sceneStore.getState().createNodes(plan.create.map((node) => ({ node, parentId: levelId })));
467
590
  }
468
- if (slabsToCreate.length > 0) {
469
- sceneStore.getState().createNodes(slabsToCreate.map((node) => ({ node, parentId: levelId })));
591
+ return plan;
592
+ }
593
+ export function projectAutoSlabsForPlan(existingSlabs, plan) {
594
+ const slabsById = new Map(existingSlabs.map((slab) => [slab.id, slab]));
595
+ for (const id of plan.delete) {
596
+ slabsById.delete(id);
597
+ }
598
+ for (const update of plan.update) {
599
+ const slab = slabsById.get(update.id);
600
+ if (!slab)
601
+ continue;
602
+ slabsById.set(update.id, SlabNode.parse({ ...slab, ...update.data }));
603
+ }
604
+ for (const slab of plan.create) {
605
+ slabsById.set(slab.id, slab);
470
606
  }
607
+ return [...slabsById.values()];
471
608
  }
472
- function syncAutoCeilingsForLevel(levelId, roomPolygons, existingCeilings, sceneStore) {
609
+ export function planAutoCeilingsForLevel(roomPolygons, existingCeilings, context = {}) {
473
610
  const manualCeilings = existingCeilings.filter((ceiling) => !ceiling.autoFromWalls);
474
611
  const manualSignatures = new Set(manualCeilings.map((ceiling) => polygonSignature(ceiling.polygon.map(pointFromTuple))));
475
612
  const detected = roomPolygons
@@ -486,6 +623,7 @@ function syncAutoCeilingsForLevel(levelId, roomPolygons, existingCeilings, scene
486
623
  centroid: polygonCentroid(room.poly),
487
624
  area: Math.abs(polygonArea(room.poly)),
488
625
  bbox: bboxOf(room.poly),
626
+ ceilingHeight: resolveAutoCeilingHeight(room.poly, context),
489
627
  }))
490
628
  .filter(({ sig }) => !manualSignatures.has(sig));
491
629
  const existingAuto = existingCeilings.filter((ceiling) => ceiling.autoFromWalls);
@@ -512,7 +650,10 @@ function syncAutoCeilingsForLevel(levelId, roomPolygons, existingCeilings, scene
512
650
  return;
513
651
  matchedDetectedIdx.add(index);
514
652
  matchedCeilingIds.add(existing.ceiling.id);
515
- updatesById.set(existing.ceiling.id, room.poly.map(pointToTuple));
653
+ updatesById.set(existing.ceiling.id, {
654
+ polygon: room.poly.map(pointToTuple),
655
+ height: room.ceilingHeight,
656
+ });
516
657
  });
517
658
  const remainingDetected = detected
518
659
  .map((room, index) => ({ room, index }))
@@ -541,7 +682,10 @@ function syncAutoCeilingsForLevel(levelId, roomPolygons, existingCeilings, scene
541
682
  continue;
542
683
  matchedDetectedIdx.add(index);
543
684
  matchedCeilingIds.add(bestMatch.entry.ceiling.id);
544
- updatesById.set(bestMatch.entry.ceiling.id, room.poly.map(pointToTuple));
685
+ updatesById.set(bestMatch.entry.ceiling.id, {
686
+ polygon: room.poly.map(pointToTuple),
687
+ height: room.ceilingHeight,
688
+ });
545
689
  }
546
690
  const ceilingsToDelete = existingAuto
547
691
  .filter((ceiling) => !updatesById.has(ceiling.id))
@@ -549,10 +693,18 @@ function syncAutoCeilingsForLevel(levelId, roomPolygons, existingCeilings, scene
549
693
  const ceilingsToUpdate = existingAuto
550
694
  .filter((ceiling) => updatesById.has(ceiling.id))
551
695
  .flatMap((ceiling) => {
552
- const polygon = updatesById.get(ceiling.id);
553
- if (!polygon)
696
+ const update = updatesById.get(ceiling.id);
697
+ if (!update)
554
698
  return [];
555
- return sameTuplePolygon(ceiling.polygon, polygon) ? [] : [{ id: ceiling.id, data: { polygon } }];
699
+ const data = {};
700
+ if (!sameTuplePolygon(ceiling.polygon, update.polygon)) {
701
+ data.polygon = update.polygon;
702
+ }
703
+ if (Math.abs((ceiling.height ?? DEFAULT_AUTO_CEILING_HEIGHT) - update.height) >
704
+ CEILING_HEIGHT_EPSILON) {
705
+ data.height = update.height;
706
+ }
707
+ return Object.keys(data).length === 0 ? [] : [{ id: ceiling.id, data }];
556
708
  });
557
709
  const plannedCeilingsForNaming = [...existingCeilings];
558
710
  const ceilingsToCreate = [];
@@ -568,18 +720,26 @@ function syncAutoCeilingsForLevel(levelId, roomPolygons, existingCeilings, scene
568
720
  name,
569
721
  polygon: room.poly.map(pointToTuple),
570
722
  holes: [],
571
- height: DEFAULT_AUTO_CEILING_HEIGHT,
723
+ height: room.ceilingHeight,
572
724
  autoFromWalls: true,
573
725
  }));
574
726
  }
575
- if (ceilingsToDelete.length > 0) {
576
- sceneStore.getState().deleteNodes(ceilingsToDelete);
727
+ return {
728
+ create: ceilingsToCreate,
729
+ update: ceilingsToUpdate,
730
+ delete: ceilingsToDelete,
731
+ };
732
+ }
733
+ function syncAutoCeilingsForLevel(levelId, roomPolygons, existingCeilings, sceneStore, context = {}) {
734
+ const plan = planAutoCeilingsForLevel(roomPolygons, existingCeilings, context);
735
+ if (plan.delete.length > 0) {
736
+ sceneStore.getState().deleteNodes(plan.delete);
577
737
  }
578
- if (ceilingsToUpdate.length > 0) {
579
- sceneStore.getState().updateNodes(ceilingsToUpdate);
738
+ if (plan.update.length > 0) {
739
+ sceneStore.getState().updateNodes(plan.update);
580
740
  }
581
- if (ceilingsToCreate.length > 0) {
582
- sceneStore.getState().createNodes(ceilingsToCreate.map((node) => ({ node, parentId: levelId })));
741
+ if (plan.create.length > 0) {
742
+ sceneStore.getState().createNodes(plan.create.map((node) => ({ node, parentId: levelId })));
583
743
  }
584
744
  }
585
745
  function detectSpacesFromWalls(levelId, walls) {
@@ -624,14 +784,36 @@ function runSpaceDetection(levelIds, sceneStore, editorStore, nodes) {
624
784
  },
625
785
  })));
626
786
  }
627
- syncAutoSlabsForLevel(levelId, roomPolygons, slabs.map((slab) => SlabNode.parse(slab)), sceneStore);
628
- syncAutoCeilingsForLevel(levelId, roomPolygons, ceilings.map((ceiling) => CeilingNode.parse(ceiling)), sceneStore);
787
+ const parsedSlabs = slabs.map((slab) => SlabNode.parse(slab));
788
+ const slabPlan = syncAutoSlabsForLevel(levelId, roomPolygons, parsedSlabs, sceneStore);
789
+ const projectedSlabs = projectAutoSlabsForPlan(parsedSlabs, slabPlan);
790
+ syncAutoCeilingsForLevel(levelId, roomPolygons, ceilings.map((ceiling) => CeilingNode.parse(ceiling)), sceneStore, { walls, slabs: projectedSlabs });
629
791
  for (const space of spaces) {
630
792
  nextSpaces[space.id] = space;
631
793
  }
632
794
  }
633
795
  editorStore.getState().setSpaces(nextSpaces);
634
796
  }
797
+ // Refcount of outstanding pause requests, matching the pauseSceneHistory
798
+ // pattern. The community editor flips this off while the AI is actively
799
+ // mutating the scene so the wall-driven auto slab/ceiling sync doesn't race
800
+ // `create_room`'s explicit slabs/ceilings (see plan
801
+ // `ai-pause-space-detection`).
802
+ let spaceDetectionPauseDepth = 0;
803
+ /** Pause the wall-driven auto slab/ceiling sync. Refcounted — pair with `resumeSpaceDetection`. */
804
+ export function pauseSpaceDetection() {
805
+ spaceDetectionPauseDepth += 1;
806
+ }
807
+ /** Resume the wall-driven auto slab/ceiling sync. No-op if not currently paused. */
808
+ export function resumeSpaceDetection() {
809
+ if (spaceDetectionPauseDepth === 0)
810
+ return;
811
+ spaceDetectionPauseDepth -= 1;
812
+ }
813
+ /** True iff the wall-driven auto slab/ceiling sync is currently paused. */
814
+ export function isSpaceDetectionPaused() {
815
+ return spaceDetectionPauseDepth > 0;
816
+ }
635
817
  export function initSpaceDetectionSync(sceneStore, editorStore) {
636
818
  const previousSnapshots = new Map();
637
819
  let isProcessing = false;
@@ -641,18 +823,16 @@ export function initSpaceDetectionSync(sceneStore, editorStore) {
641
823
  if (getSceneHistoryPauseDepth() > 0)
642
824
  return;
643
825
  const nodes = state.nodes;
644
- const wallsByLevel = new Map();
645
- for (const node of Object.values(nodes)) {
646
- if (node && node.type === 'wall' && node.parentId) {
647
- const levelId = node.parentId;
648
- const levelWalls = wallsByLevel.get(levelId) ?? [];
649
- levelWalls.push(node);
650
- wallsByLevel.set(levelId, levelWalls);
826
+ const currentSnapshots = levelStructureSnapshots(nodes);
827
+ // Paused: roll the snapshot forward so we don't backfill (and re-duplicate)
828
+ // every paused change once detection resumes. Whatever the AI built while
829
+ // paused becomes the new baseline; only future changes will reconcile.
830
+ if (spaceDetectionPauseDepth > 0) {
831
+ previousSnapshots.clear();
832
+ for (const [levelId, snapshot] of currentSnapshots.entries()) {
833
+ previousSnapshots.set(levelId, snapshot);
651
834
  }
652
- }
653
- const currentSnapshots = new Map();
654
- for (const [levelId, walls] of wallsByLevel.entries()) {
655
- currentSnapshots.set(levelId, levelWallSnapshot(walls));
835
+ return;
656
836
  }
657
837
  const levelsToUpdate = new Set();
658
838
  for (const levelId of new Set([...previousSnapshots.keys(), ...currentSnapshots.keys()])) {
@@ -675,7 +855,8 @@ export function initSpaceDetectionSync(sceneStore, editorStore) {
675
855
  finally {
676
856
  resumeSceneHistory(sceneStore);
677
857
  previousSnapshots.clear();
678
- for (const [levelId, snapshot] of currentSnapshots.entries()) {
858
+ const postRunSnapshots = levelStructureSnapshots(sceneStore.getState().nodes);
859
+ for (const [levelId, snapshot] of postRunSnapshots.entries()) {
679
860
  previousSnapshots.set(levelId, snapshot);
680
861
  }
681
862
  isProcessing = false;
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=space-detection.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"space-detection.test.d.ts","sourceRoot":"","sources":["../../src/lib/space-detection.test.ts"],"names":[],"mappings":""}
@@ -0,0 +1,77 @@
1
+ import { describe, expect, test } from 'bun:test';
2
+ import { CeilingNode, SlabNode, WallNode } from '../schema';
3
+ import { planAutoCeilingsForLevel } from './space-detection';
4
+ const square = [
5
+ [0, 0],
6
+ [4, 0],
7
+ [4, 3],
8
+ [0, 3],
9
+ ];
10
+ function roomPolygon() {
11
+ return square.map(([x, y]) => ({ x, y }));
12
+ }
13
+ function squareWalls(height = 2.5) {
14
+ return [
15
+ WallNode.parse({ start: [0, 0], end: [4, 0], height }),
16
+ WallNode.parse({ start: [4, 0], end: [4, 3], height }),
17
+ WallNode.parse({ start: [4, 3], end: [0, 3], height }),
18
+ WallNode.parse({ start: [0, 3], end: [0, 0], height }),
19
+ ];
20
+ }
21
+ function slab(elevation) {
22
+ return SlabNode.parse({
23
+ polygon: square,
24
+ elevation,
25
+ autoFromWalls: true,
26
+ });
27
+ }
28
+ describe('planAutoCeilingsForLevel', () => {
29
+ test('creates auto ceilings at the top of the room walls', () => {
30
+ const created = planAutoCeilingsForLevel([roomPolygon()], [], {
31
+ walls: squareWalls(),
32
+ slabs: [slab(0.05)],
33
+ }).create[0];
34
+ expect(created?.height).toBeCloseTo(2.55);
35
+ });
36
+ test('updates existing auto ceiling height when the slab elevation changes', () => {
37
+ const ceiling = CeilingNode.parse({
38
+ polygon: square,
39
+ height: 2.55,
40
+ autoFromWalls: true,
41
+ });
42
+ const plan = planAutoCeilingsForLevel([roomPolygon()], [ceiling], {
43
+ walls: squareWalls(),
44
+ slabs: [slab(0.4)],
45
+ });
46
+ expect(plan.update).toHaveLength(1);
47
+ expect(plan.update[0]?.id).toBe(ceiling.id);
48
+ expect(plan.update[0]?.data.polygon).toBeUndefined();
49
+ expect(plan.update[0]?.data.height).toBeCloseTo(2.9);
50
+ });
51
+ test('updates existing auto ceiling height when wall height changes', () => {
52
+ const ceiling = CeilingNode.parse({
53
+ polygon: square,
54
+ height: 2.55,
55
+ autoFromWalls: true,
56
+ });
57
+ const plan = planAutoCeilingsForLevel([roomPolygon()], [ceiling], {
58
+ walls: squareWalls(3),
59
+ slabs: [slab(0.05)],
60
+ });
61
+ expect(plan.update).toHaveLength(1);
62
+ expect(plan.update[0]?.data.height).toBeCloseTo(3.05);
63
+ });
64
+ test('does not replace a manual ceiling with an auto ceiling', () => {
65
+ const manualCeiling = CeilingNode.parse({
66
+ polygon: square,
67
+ height: 2.5,
68
+ autoFromWalls: false,
69
+ });
70
+ const plan = planAutoCeilingsForLevel([roomPolygon()], [manualCeiling], {
71
+ walls: squareWalls(),
72
+ slabs: [slab(0.4)],
73
+ });
74
+ expect(plan.create).toHaveLength(0);
75
+ expect(plan.update).toHaveLength(0);
76
+ });
77
+ });
@@ -8,7 +8,7 @@ export type MaterialCatalogItem = {
8
8
  previewColor?: string;
9
9
  preset: MaterialPresetPayload;
10
10
  };
11
- export declare const MATERIAL_CATEGORIES: readonly ["wood", "wallpaper", "parquet", "granite", "marble", "other"];
11
+ export declare const MATERIAL_CATEGORIES: readonly ["wood", "flooring", "roof", "other"];
12
12
  export type MaterialCategory = (typeof MATERIAL_CATEGORIES)[number];
13
13
  export declare const MATERIAL_CATALOG: MaterialCatalogItem[];
14
14
  export declare function getMaterialsForCategory(category: MaterialCategory): MaterialCatalogItem[];
@@ -1 +1 @@
1
- {"version":3,"file":"material-library.d.ts","sourceRoot":"","sources":["../src/material-library.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,KAAK,qBAAqB,EAC3B,MAAM,mBAAmB,CAAA;AAE1B,MAAM,MAAM,mBAAmB,GAAG;IAChC,EAAE,EAAE,MAAM,CAAA;IACV,KAAK,EAAE,MAAM,CAAA;IACb,QAAQ,EAAE,gBAAgB,CAAA;IAC1B,WAAW,CAAC,EAAE,MAAM,CAAA;IACpB,mBAAmB,CAAC,EAAE,MAAM,CAAA;IAC5B,YAAY,CAAC,EAAE,MAAM,CAAA;IACrB,MAAM,EAAE,qBAAqB,CAAA;CAC9B,CAAA;AAED,eAAO,MAAM,mBAAmB,yEAOtB,CAAA;AACV,MAAM,MAAM,gBAAgB,GAAG,CAAC,OAAO,mBAAmB,CAAC,CAAC,MAAM,CAAC,CAAA;AAEnE,eAAO,MAAM,gBAAgB,EAAE,mBAAmB,EAiiBjD,CAAA;AAED,wBAAgB,uBAAuB,CAAC,QAAQ,EAAE,gBAAgB,GAAG,mBAAmB,EAAE,CAEzF;AAED,wBAAgB,sBAAsB,CAAC,EAAE,CAAC,EAAE,MAAM,GAAG,mBAAmB,GAAG,SAAS,CAGnF;AAED,eAAO,MAAM,2BAA2B,aAAa,CAAA;AAErD,wBAAgB,oBAAoB,CAAC,EAAE,EAAE,MAAM,UAE9C;AAED,wBAAgB,2BAA2B,CAAC,WAAW,CAAC,EAAE,MAAM,GAAG,IAAI,iBAItE;AAED,wBAAgB,sBAAsB,CAAC,WAAW,CAAC,EAAE,MAAM,GAAG,IAAI,GAAG,qBAAqB,GAAG,IAAI,CAIhG"}
1
+ {"version":3,"file":"material-library.d.ts","sourceRoot":"","sources":["../src/material-library.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,KAAK,qBAAqB,EAG3B,MAAM,mBAAmB,CAAA;AAE1B,MAAM,MAAM,mBAAmB,GAAG;IAChC,EAAE,EAAE,MAAM,CAAA;IACV,KAAK,EAAE,MAAM,CAAA;IACb,QAAQ,EAAE,gBAAgB,CAAA;IAC1B,WAAW,CAAC,EAAE,MAAM,CAAA;IACpB,mBAAmB,CAAC,EAAE,MAAM,CAAA;IAC5B,YAAY,CAAC,EAAE,MAAM,CAAA;IACrB,MAAM,EAAE,qBAAqB,CAAA;CAC9B,CAAA;AAuCD,eAAO,MAAM,mBAAmB,gDAAiD,CAAA;AACjF,MAAM,MAAM,gBAAgB,GAAG,CAAC,OAAO,mBAAmB,CAAC,CAAC,MAAM,CAAC,CAAA;AAEnE,eAAO,MAAM,gBAAgB,EAAE,mBAAmB,EAqzEjD,CAAA;AAED,wBAAgB,uBAAuB,CAAC,QAAQ,EAAE,gBAAgB,GAAG,mBAAmB,EAAE,CAEzF;AAED,wBAAgB,sBAAsB,CAAC,EAAE,CAAC,EAAE,MAAM,GAAG,mBAAmB,GAAG,SAAS,CAGnF;AAED,eAAO,MAAM,2BAA2B,aAAa,CAAA;AAErD,wBAAgB,oBAAoB,CAAC,EAAE,EAAE,MAAM,UAE9C;AAED,wBAAgB,2BAA2B,CAAC,WAAW,CAAC,EAAE,MAAM,GAAG,IAAI,iBAItE;AAED,wBAAgB,sBAAsB,CAAC,WAAW,CAAC,EAAE,MAAM,GAAG,IAAI,GAAG,qBAAqB,GAAG,IAAI,CAIhG"}