@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,109 @@
1
+ import { beforeEach, describe, expect, test } from 'bun:test';
2
+ import useScene from './use-scene';
3
+ describe('scene elevator migrations', () => {
4
+ beforeEach(() => {
5
+ useScene.setState({
6
+ nodes: {},
7
+ rootNodeIds: [],
8
+ dirtyNodes: new Set(),
9
+ collections: {},
10
+ });
11
+ useScene.temporal.getState().clear();
12
+ });
13
+ test('normalizes legacy level-parented elevators into building-scoped nodes', () => {
14
+ useScene.getState().setScene({
15
+ site_test: {
16
+ object: 'node',
17
+ id: 'site_test',
18
+ type: 'site',
19
+ parentId: null,
20
+ visible: true,
21
+ metadata: {},
22
+ children: ['building_test'],
23
+ },
24
+ building_test: {
25
+ object: 'node',
26
+ id: 'building_test',
27
+ type: 'building',
28
+ parentId: 'site_test',
29
+ visible: true,
30
+ metadata: {},
31
+ children: ['level_test'],
32
+ },
33
+ level_test: {
34
+ object: 'node',
35
+ id: 'level_test',
36
+ type: 'level',
37
+ parentId: 'building_test',
38
+ visible: true,
39
+ metadata: {},
40
+ children: ['elevator_test'],
41
+ level: 0,
42
+ },
43
+ elevator_test: {
44
+ object: 'node',
45
+ id: 'elevator_test',
46
+ type: 'elevator',
47
+ parentId: 'level_test',
48
+ visible: true,
49
+ metadata: {},
50
+ },
51
+ }, ['site_test']);
52
+ const nodes = useScene.getState().nodes;
53
+ const elevator = nodes.elevator_test;
54
+ const level = nodes.level_test;
55
+ const building = nodes.building_test;
56
+ expect(elevator.parentId).toBe('building_test');
57
+ expect(elevator.position).toEqual([0, 0, 0]);
58
+ expect(elevator.rotation).toBe(0);
59
+ expect(level.children).not.toContain('elevator_test');
60
+ expect(building.children).toContain('elevator_test');
61
+ });
62
+ test('migrates level-parented elevators when the level parentId is missing', () => {
63
+ useScene.getState().setScene({
64
+ site_test: {
65
+ object: 'node',
66
+ id: 'site_test',
67
+ type: 'site',
68
+ parentId: null,
69
+ visible: true,
70
+ metadata: {},
71
+ children: ['building_test'],
72
+ },
73
+ building_test: {
74
+ object: 'node',
75
+ id: 'building_test',
76
+ type: 'building',
77
+ parentId: 'site_test',
78
+ visible: true,
79
+ metadata: {},
80
+ children: ['level_test'],
81
+ },
82
+ level_test: {
83
+ object: 'node',
84
+ id: 'level_test',
85
+ type: 'level',
86
+ parentId: null,
87
+ visible: true,
88
+ metadata: {},
89
+ children: ['elevator_test'],
90
+ level: 0,
91
+ },
92
+ elevator_test: {
93
+ object: 'node',
94
+ id: 'elevator_test',
95
+ type: 'elevator',
96
+ parentId: 'level_test',
97
+ visible: true,
98
+ metadata: {},
99
+ },
100
+ }, ['site_test']);
101
+ const nodes = useScene.getState().nodes;
102
+ const elevator = nodes.elevator_test;
103
+ const level = nodes.level_test;
104
+ const building = nodes.building_test;
105
+ expect(elevator.parentId).toBe('building_test');
106
+ expect(level.children).not.toContain('elevator_test');
107
+ expect(building.children).toContain('elevator_test');
108
+ });
109
+ });
@@ -20,6 +20,17 @@ export type SceneState = {
20
20
  node: AnyNode;
21
21
  parentId?: AnyNodeId;
22
22
  }[]) => void;
23
+ applyNodeChanges: (changes: {
24
+ create?: {
25
+ node: AnyNode;
26
+ parentId?: AnyNodeId;
27
+ }[];
28
+ update?: {
29
+ id: AnyNodeId;
30
+ data: Partial<AnyNode>;
31
+ }[];
32
+ delete?: AnyNodeId[];
33
+ }) => void;
23
34
  updateNode: (id: AnyNodeId, data: Partial<AnyNode>) => void;
24
35
  updateNodes: (updates: {
25
36
  id: AnyNodeId;
@@ -1 +1 @@
1
- {"version":3,"file":"use-scene.d.ts","sourceRoot":"","sources":["../../src/store/use-scene.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,OAAO,CAAA;AAE1C,OAAO,EAAU,KAAK,QAAQ,EAAE,KAAK,aAAa,EAAE,MAAM,SAAS,CAAA;AAEnE,OAAO,KAAK,EAAE,UAAU,EAAE,YAAY,EAAE,MAAM,uBAAuB,CAAA;AAMrE,OAAO,KAAK,EAAE,OAAO,EAAE,SAAS,EAAE,MAAM,iBAAiB,CAAA;AAgZzD,MAAM,MAAM,UAAU,GAAG;IAEvB,KAAK,EAAE,MAAM,CAAC,SAAS,EAAE,OAAO,CAAC,CAAA;IAGjC,WAAW,EAAE,SAAS,EAAE,CAAA;IAGxB,UAAU,EAAE,GAAG,CAAC,SAAS,CAAC,CAAA;IAG1B,WAAW,EAAE,MAAM,CAAC,YAAY,EAAE,UAAU,CAAC,CAAA;IAG7C,QAAQ,EAAE,OAAO,CAAA;IACjB,WAAW,EAAE,CAAC,QAAQ,EAAE,OAAO,KAAK,IAAI,CAAA;IAGxC,SAAS,EAAE,MAAM,IAAI,CAAA;IACrB,UAAU,EAAE,MAAM,IAAI,CAAA;IACtB,WAAW,EAAE,MAAM,IAAI,CAAA;IACvB,QAAQ,EAAE,CAAC,KAAK,EAAE,MAAM,CAAC,SAAS,EAAE,OAAO,CAAC,EAAE,WAAW,EAAE,SAAS,EAAE,KAAK,IAAI,CAAA;IAE/E,SAAS,EAAE,CAAC,EAAE,EAAE,SAAS,KAAK,IAAI,CAAA;IAClC,UAAU,EAAE,CAAC,EAAE,EAAE,SAAS,KAAK,IAAI,CAAA;IAEnC,UAAU,EAAE,CAAC,IAAI,EAAE,OAAO,EAAE,QAAQ,CAAC,EAAE,SAAS,KAAK,IAAI,CAAA;IACzD,WAAW,EAAE,CAAC,GAAG,EAAE;QAAE,IAAI,EAAE,OAAO,CAAC;QAAC,QAAQ,CAAC,EAAE,SAAS,CAAA;KAAE,EAAE,KAAK,IAAI,CAAA;IAErE,UAAU,EAAE,CAAC,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,OAAO,CAAC,OAAO,CAAC,KAAK,IAAI,CAAA;IAC3D,WAAW,EAAE,CAAC,OAAO,EAAE;QAAE,EAAE,EAAE,SAAS,CAAC;QAAC,IAAI,EAAE,OAAO,CAAC,OAAO,CAAC,CAAA;KAAE,EAAE,KAAK,IAAI,CAAA;IAE3E,UAAU,EAAE,CAAC,EAAE,EAAE,SAAS,KAAK,IAAI,CAAA;IACnC,WAAW,EAAE,CAAC,GAAG,EAAE,SAAS,EAAE,KAAK,IAAI,CAAA;IAGvC,gBAAgB,EAAE,CAAC,IAAI,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,SAAS,EAAE,KAAK,YAAY,CAAA;IACvE,gBAAgB,EAAE,CAAC,EAAE,EAAE,YAAY,KAAK,IAAI,CAAA;IAC5C,gBAAgB,EAAE,CAAC,EAAE,EAAE,YAAY,EAAE,IAAI,EAAE,OAAO,CAAC,IAAI,CAAC,UAAU,EAAE,IAAI,CAAC,CAAC,KAAK,IAAI,CAAA;IACnF,eAAe,EAAE,CAAC,EAAE,EAAE,YAAY,EAAE,MAAM,EAAE,SAAS,KAAK,IAAI,CAAA;IAC9D,oBAAoB,EAAE,CAAC,EAAE,EAAE,YAAY,EAAE,MAAM,EAAE,SAAS,KAAK,IAAI,CAAA;CACpE,CAAA;AAID,KAAK,aAAa,GAAG,aAAa,CAAC,QAAQ,CAAC,UAAU,CAAC,CAAC,GAAG;IACzD,QAAQ,EAAE,QAAQ,CAAC,aAAa,CAAC,IAAI,CAAC,UAAU,EAAE,OAAO,GAAG,aAAa,GAAG,aAAa,CAAC,CAAC,CAAC,CAAA;CAC7F,CAAA;AAED,QAAA,MAAM,QAAQ,EAAE,aAwOf,CAAA;AAED,eAAe,QAAQ,CAAA;AAOvB,wBAAgB,iBAAiB,SAMhC"}
1
+ {"version":3,"file":"use-scene.d.ts","sourceRoot":"","sources":["../../src/store/use-scene.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,OAAO,CAAA;AAE1C,OAAO,EAAU,KAAK,QAAQ,EAAE,KAAK,aAAa,EAAE,MAAM,SAAS,CAAA;AAEnE,OAAO,KAAK,EAAE,UAAU,EAAE,YAAY,EAAE,MAAM,uBAAuB,CAAA;AAcrE,OAAO,KAAK,EAAE,OAAO,EAAE,SAAS,EAAE,MAAM,iBAAiB,CAAA;AAqmBzD,MAAM,MAAM,UAAU,GAAG;IAEvB,KAAK,EAAE,MAAM,CAAC,SAAS,EAAE,OAAO,CAAC,CAAA;IAGjC,WAAW,EAAE,SAAS,EAAE,CAAA;IAGxB,UAAU,EAAE,GAAG,CAAC,SAAS,CAAC,CAAA;IAG1B,WAAW,EAAE,MAAM,CAAC,YAAY,EAAE,UAAU,CAAC,CAAA;IAG7C,QAAQ,EAAE,OAAO,CAAA;IACjB,WAAW,EAAE,CAAC,QAAQ,EAAE,OAAO,KAAK,IAAI,CAAA;IAGxC,SAAS,EAAE,MAAM,IAAI,CAAA;IACrB,UAAU,EAAE,MAAM,IAAI,CAAA;IACtB,WAAW,EAAE,MAAM,IAAI,CAAA;IACvB,QAAQ,EAAE,CAAC,KAAK,EAAE,MAAM,CAAC,SAAS,EAAE,OAAO,CAAC,EAAE,WAAW,EAAE,SAAS,EAAE,KAAK,IAAI,CAAA;IAE/E,SAAS,EAAE,CAAC,EAAE,EAAE,SAAS,KAAK,IAAI,CAAA;IAClC,UAAU,EAAE,CAAC,EAAE,EAAE,SAAS,KAAK,IAAI,CAAA;IAEnC,UAAU,EAAE,CAAC,IAAI,EAAE,OAAO,EAAE,QAAQ,CAAC,EAAE,SAAS,KAAK,IAAI,CAAA;IACzD,WAAW,EAAE,CAAC,GAAG,EAAE;QAAE,IAAI,EAAE,OAAO,CAAC;QAAC,QAAQ,CAAC,EAAE,SAAS,CAAA;KAAE,EAAE,KAAK,IAAI,CAAA;IACrE,gBAAgB,EAAE,CAAC,OAAO,EAAE;QAC1B,MAAM,CAAC,EAAE;YAAE,IAAI,EAAE,OAAO,CAAC;YAAC,QAAQ,CAAC,EAAE,SAAS,CAAA;SAAE,EAAE,CAAA;QAClD,MAAM,CAAC,EAAE;YAAE,EAAE,EAAE,SAAS,CAAC;YAAC,IAAI,EAAE,OAAO,CAAC,OAAO,CAAC,CAAA;SAAE,EAAE,CAAA;QACpD,MAAM,CAAC,EAAE,SAAS,EAAE,CAAA;KACrB,KAAK,IAAI,CAAA;IAEV,UAAU,EAAE,CAAC,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,OAAO,CAAC,OAAO,CAAC,KAAK,IAAI,CAAA;IAC3D,WAAW,EAAE,CAAC,OAAO,EAAE;QAAE,EAAE,EAAE,SAAS,CAAC;QAAC,IAAI,EAAE,OAAO,CAAC,OAAO,CAAC,CAAA;KAAE,EAAE,KAAK,IAAI,CAAA;IAE3E,UAAU,EAAE,CAAC,EAAE,EAAE,SAAS,KAAK,IAAI,CAAA;IACnC,WAAW,EAAE,CAAC,GAAG,EAAE,SAAS,EAAE,KAAK,IAAI,CAAA;IAGvC,gBAAgB,EAAE,CAAC,IAAI,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,SAAS,EAAE,KAAK,YAAY,CAAA;IACvE,gBAAgB,EAAE,CAAC,EAAE,EAAE,YAAY,KAAK,IAAI,CAAA;IAC5C,gBAAgB,EAAE,CAAC,EAAE,EAAE,YAAY,EAAE,IAAI,EAAE,OAAO,CAAC,IAAI,CAAC,UAAU,EAAE,IAAI,CAAC,CAAC,KAAK,IAAI,CAAA;IACnF,eAAe,EAAE,CAAC,EAAE,EAAE,YAAY,EAAE,MAAM,EAAE,SAAS,KAAK,IAAI,CAAA;IAC9D,oBAAoB,EAAE,CAAC,EAAE,EAAE,YAAY,EAAE,MAAM,EAAE,SAAS,KAAK,IAAI,CAAA;CACpE,CAAA;AAID,KAAK,aAAa,GAAG,aAAa,CAAC,QAAQ,CAAC,UAAU,CAAC,CAAC,GAAG;IACzD,QAAQ,EAAE,QAAQ,CAAC,aAAa,CAAC,IAAI,CAAC,UAAU,EAAE,OAAO,GAAG,aAAa,GAAG,aAAa,CAAC,CAAC,CAAC,CAAA;CAC7F,CAAA;AAED,QAAA,MAAM,QAAQ,EAAE,aAgPf,CAAA;AAED,eAAe,QAAQ,CAAA;AAOvB,wBAAgB,iBAAiB,SAMhC"}
@@ -3,7 +3,11 @@ import { temporal } from 'zundo';
3
3
  import { create } from 'zustand';
4
4
  import { BuildingNode } from '../schema';
5
5
  import { generateCollectionId } from '../schema/collections';
6
+ import { DoorNode as DoorNodeSchema } from '../schema/nodes/door';
7
+ import { ElevatorNode as ElevatorNodeSchema } from '../schema/nodes/elevator';
6
8
  import { LevelNode } from '../schema/nodes/level';
9
+ import { getPitchFromActiveRoofHeight, } from '../schema/nodes/roof-segment';
10
+ import { ShelfNode as ShelfNodeSchema } from '../schema/nodes/shelf';
7
11
  import { SiteNode } from '../schema/nodes/site';
8
12
  import { StairNode as StairNodeSchema } from '../schema/nodes/stair';
9
13
  import { StairSegmentNode as StairSegmentNodeSchema } from '../schema/nodes/stair-segment';
@@ -12,6 +16,10 @@ import { resetSceneHistoryPauseDepth } from './history-control';
12
16
  function getFiniteNumber(value, fallback) {
13
17
  return typeof value === 'number' && Number.isFinite(value) ? value : fallback;
14
18
  }
19
+ function getFiniteNumberInRange(value, fallback, min, max) {
20
+ const finite = getFiniteNumber(value, fallback);
21
+ return Math.min(Math.max(finite, min), max);
22
+ }
15
23
  function getBoolean(value, fallback) {
16
24
  return typeof value === 'boolean' ? value : fallback;
17
25
  }
@@ -19,7 +27,7 @@ function getEnumValue(value, allowed, fallback) {
19
27
  return typeof value === 'string' && allowed.includes(value) ? value : fallback;
20
28
  }
21
29
  function getNullableString(value) {
22
- return typeof value === 'string' ? value : null;
30
+ return typeof value === 'string' && value.length > 0 ? value : null;
23
31
  }
24
32
  function getStringArray(value) {
25
33
  return Array.isArray(value)
@@ -81,6 +89,99 @@ function normalizeStairSegmentNode(node) {
81
89
  const parsed = StairSegmentNodeSchema.safeParse(sanitized);
82
90
  return parsed.success ? parsed.data : null;
83
91
  }
92
+ function normalizeDoorNode(node) {
93
+ const parsed = DoorNodeSchema.safeParse(node);
94
+ return parsed.success ? { ...node, ...parsed.data } : null;
95
+ }
96
+ function normalizeShelfNode(node) {
97
+ const sanitized = {
98
+ ...node,
99
+ children: getStringArray(node.children),
100
+ position: getVector3(node.position, [0, 0, 0]),
101
+ rotation: getVector3(node.rotation, [0, 0, 0]),
102
+ width: getFiniteNumberInRange(node.width, 1.2, 0.3, 3.0),
103
+ depth: getFiniteNumberInRange(node.depth, 0.3, 0.1, 1.0),
104
+ thickness: getFiniteNumberInRange(node.thickness, 0.04, 0.01, 0.1),
105
+ height: getFiniteNumberInRange(node.height, 0.9, 0.05, 2.5),
106
+ rows: Math.round(getFiniteNumberInRange(node.rows, 1, 1, 8)),
107
+ columns: Math.round(getFiniteNumberInRange(node.columns, 1, 1, 6)),
108
+ style: getEnumValue(node.style, ['wall-shelf', 'bookshelf', 'open-rack', 'cubby'], 'wall-shelf'),
109
+ withBack: getBoolean(node.withBack, false),
110
+ withSides: getBoolean(node.withSides, true),
111
+ withBottom: getBoolean(node.withBottom, false),
112
+ bracketStyle: getEnumValue(node.bracketStyle, ['minimal', 'industrial', 'hidden'], 'minimal'),
113
+ };
114
+ const parsed = ShelfNodeSchema.safeParse(sanitized);
115
+ return parsed.success ? parsed.data : null;
116
+ }
117
+ function normalizeElevatorNode(node) {
118
+ const sanitized = {
119
+ ...node,
120
+ position: getVector3(node.position, [0, 0, 0]),
121
+ rotation: getFiniteNumber(node.rotation, 0),
122
+ width: getFiniteNumber(node.width, 1.84),
123
+ depth: getFiniteNumber(node.depth, 1.84),
124
+ shaftWidth: node.shaftWidth === undefined ? undefined : getFiniteNumber(node.shaftWidth, 1.84),
125
+ shaftDepth: node.shaftDepth === undefined ? undefined : getFiniteNumber(node.shaftDepth, 1.84),
126
+ shaftWallThickness: getFiniteNumber(node.shaftWallThickness, 0.09),
127
+ cabHeight: getFiniteNumber(node.cabHeight, 2.35),
128
+ doorWidth: getFiniteNumber(node.doorWidth, 0.95),
129
+ doorHeight: getFiniteNumber(node.doorHeight, 2.1),
130
+ fromLevelId: getNullableString(node.fromLevelId),
131
+ toLevelId: getNullableString(node.toLevelId),
132
+ servedLevelIds: node.servedLevelIds === undefined ? undefined : getStringArray(node.servedLevelIds),
133
+ disabledLevelIds: getStringArray(node.disabledLevelIds),
134
+ serviceOnlyLevelIds: getStringArray(node.serviceOnlyLevelIds),
135
+ defaultLevelId: getNullableString(node.defaultLevelId),
136
+ speed: getFiniteNumber(node.speed, 2.2),
137
+ doorDurationMs: getFiniteNumber(node.doorDurationMs, 900),
138
+ dwellMs: getFiniteNumber(node.dwellMs, 1400),
139
+ };
140
+ const parsed = ElevatorNodeSchema.safeParse(sanitized);
141
+ return parsed.success ? parsed.data : null;
142
+ }
143
+ function findBuildingIdForLevel(levelId, nodes) {
144
+ const level = nodes[levelId];
145
+ const directBuildingId = typeof level?.parentId === 'string' ? level.parentId : null;
146
+ if (directBuildingId && nodes[directBuildingId]?.type === 'building') {
147
+ return directBuildingId;
148
+ }
149
+ for (const [candidateId, candidate] of Object.entries(nodes)) {
150
+ if (candidate?.type !== 'building')
151
+ continue;
152
+ if (getStringArray(candidate.children).includes(levelId)) {
153
+ return candidateId;
154
+ }
155
+ }
156
+ return null;
157
+ }
158
+ function migrateElevatorParent(id, node, nodes) {
159
+ const parentId = typeof node.parentId === 'string' ? node.parentId : null;
160
+ if (!parentId)
161
+ return node;
162
+ const parent = parentId ? nodes[parentId] : null;
163
+ if (parent?.type !== 'level')
164
+ return node;
165
+ const buildingId = findBuildingIdForLevel(parentId, nodes);
166
+ if (!buildingId)
167
+ return node;
168
+ const building = buildingId ? nodes[buildingId] : null;
169
+ if (building?.type !== 'building')
170
+ return node;
171
+ nodes[parentId] = {
172
+ ...parent,
173
+ children: getStringArray(parent.children).filter((childId) => childId !== id),
174
+ };
175
+ const buildingChildren = getStringArray(building.children);
176
+ nodes[buildingId] = {
177
+ ...building,
178
+ children: buildingChildren.includes(id) ? buildingChildren : [...buildingChildren, id],
179
+ };
180
+ return {
181
+ ...node,
182
+ parentId: buildingId,
183
+ };
184
+ }
84
185
  function migrateWallSurfaceMaterials(node) {
85
186
  const hasInterior = node.interiorMaterial !== undefined || typeof node.interiorMaterialPreset === 'string';
86
187
  const hasExterior = node.exteriorMaterial !== undefined || typeof node.exteriorMaterialPreset === 'string';
@@ -88,7 +189,7 @@ function migrateWallSurfaceMaterials(node) {
88
189
  material: node.material,
89
190
  materialPreset: typeof node.materialPreset === 'string' ? node.materialPreset : undefined,
90
191
  };
91
- if (!hasInterior && !hasExterior) {
192
+ if (!(hasInterior || hasExterior)) {
92
193
  if (legacyFinish.material === undefined && legacyFinish.materialPreset === undefined) {
93
194
  return node;
94
195
  }
@@ -139,7 +240,7 @@ function migrateStairSurfaceMaterials(node) {
139
240
  }
140
241
  return legacyFinish;
141
242
  };
142
- if (!hasRailing && !hasTread && !hasSide) {
243
+ if (!(hasRailing || hasTread || hasSide)) {
143
244
  if (legacyFinish.material === undefined && legacyFinish.materialPreset === undefined) {
144
245
  return node;
145
246
  }
@@ -189,7 +290,7 @@ function migrateRoofSurfaceMaterials(node) {
189
290
  material: node.material,
190
291
  materialPreset: typeof node.materialPreset === 'string' ? node.materialPreset : undefined,
191
292
  };
192
- if (!hasTop && !hasEdge && !hasWall) {
293
+ if (!(hasTop || hasEdge || hasWall)) {
193
294
  if (legacyFinish.material === undefined && legacyFinish.materialPreset === undefined) {
194
295
  return node;
195
296
  }
@@ -244,6 +345,9 @@ function migrateNodes(nodes) {
244
345
  const oldRoof = node;
245
346
  const suffix = id.includes('_') ? id.split('_')[1] : Math.random().toString(36).slice(2);
246
347
  const segmentId = `rseg_${suffix}`;
348
+ const segWidth = oldRoof.length ?? 8;
349
+ const segDepth = (oldRoof.leftWidth ?? 2.2) + (oldRoof.rightWidth ?? 2.2);
350
+ const legacyRoofHeight = oldRoof.height ?? 2.5;
247
351
  const segment = {
248
352
  object: 'node',
249
353
  id: segmentId,
@@ -254,10 +358,18 @@ function migrateNodes(nodes) {
254
358
  position: [0, 0, 0],
255
359
  rotation: 0,
256
360
  roofType: 'gable',
257
- width: oldRoof.length ?? 8,
258
- depth: (oldRoof.leftWidth ?? 2.2) + (oldRoof.rightWidth ?? 2.2),
259
- wallHeight: 0,
260
- roofHeight: oldRoof.height ?? 2.5,
361
+ width: segWidth,
362
+ depth: segDepth,
363
+ // Schema default (0.5), NOT 0: a zero-height wall builds a flat,
364
+ // degenerate CSG brush → "Coplanar clip not handled" + NaN geometry, so
365
+ // the migrated legacy roof never renders. New roofs use 0.5 too.
366
+ wallHeight: 0.5,
367
+ pitch: getPitchFromActiveRoofHeight({
368
+ roofType: 'gable',
369
+ width: segWidth,
370
+ depth: segDepth,
371
+ roofHeight: legacyRoofHeight,
372
+ }),
261
373
  wallThickness: 0.1,
262
374
  deckThickness: 0.1,
263
375
  overhang: 0.3,
@@ -269,6 +381,35 @@ function migrateNodes(nodes) {
269
381
  children: [segmentId],
270
382
  };
271
383
  }
384
+ // 2b. roof-segment: guarantee a valid positive pitch (degrees).
385
+ // Saved scenes wrote `roofHeight` in metres; the schema now stores `pitch`
386
+ // in degrees. Convert the legacy field when present, and — crucially — fall
387
+ // back to the schema default for any segment that carries no usable pitch or
388
+ // roofHeight (older/partial saves). Without this, the slope-frame guard
389
+ // resolves a missing pitch to a FLAT frame, so the roof renders as a slab.
390
+ // The migration result is cast, not zod-parsed, so the schema default never
391
+ // applies on its own — this branch is the only place it lands.
392
+ if (node.type === 'roof-segment') {
393
+ const currentPitch = node.pitch;
394
+ const hasValidPitch = typeof currentPitch === 'number' && currentPitch > 0;
395
+ if (!hasValidPitch) {
396
+ const { roofHeight, ...rest } = node;
397
+ const width = typeof node.width === 'number' ? node.width : 8;
398
+ const depth = typeof node.depth === 'number' ? node.depth : 6;
399
+ const roofType = (typeof node.roofType === 'string' ? node.roofType : 'gable');
400
+ const derived = typeof roofHeight === 'number' && roofHeight > 0
401
+ ? getPitchFromActiveRoofHeight({ roofType, width, depth, roofHeight })
402
+ : 0;
403
+ // 40° matches the RoofSegmentNode schema default.
404
+ patchedNodes[id] = { ...rest, pitch: derived > 0 ? derived : 40 };
405
+ }
406
+ }
407
+ if (node.type === 'door') {
408
+ const normalized = normalizeDoorNode(node);
409
+ if (normalized) {
410
+ patchedNodes[id] = normalized;
411
+ }
412
+ }
272
413
  if (node.type === 'stair') {
273
414
  const normalized = normalizeStairNode(migrateStairSurfaceMaterials(node));
274
415
  if (normalized) {
@@ -284,14 +425,62 @@ function migrateNodes(nodes) {
284
425
  if (node.type === 'wall') {
285
426
  patchedNodes[id] = migrateWallSurfaceMaterials(patchedNodes[id]);
286
427
  }
428
+ if (node.type === 'shelf') {
429
+ const normalized = normalizeShelfNode(node);
430
+ if (normalized) {
431
+ patchedNodes[id] = normalized;
432
+ }
433
+ }
434
+ if (node.type === 'elevator') {
435
+ const parentMigrated = migrateElevatorParent(id, node, patchedNodes);
436
+ const normalized = normalizeElevatorNode(parentMigrated);
437
+ if (normalized) {
438
+ patchedNodes[id] = normalized;
439
+ }
440
+ }
441
+ // Roof-segment hosting was added in this migration cycle (the same
442
+ // pattern as shelf above). Older segments saved before the schema
443
+ // gained `children` need the field initialised so
444
+ // `createNode(chimney, segmentId)` finds an array to append to —
445
+ // without this every "Add Element" click on the roof panel results
446
+ // in an orphaned accessory (parented in scene state but never
447
+ // appended to `seg.children`, so the renderer's recursive
448
+ // `<NodeRenderer>` mount never sees it).
449
+ if (node.type === 'roof-segment' && !Array.isArray(node.children)) {
450
+ patchedNodes[id] = { ...node, children: [] };
451
+ }
287
452
  if (node.type === 'roof') {
288
453
  patchedNodes[id] = migrateRoofSurfaceMaterials(patchedNodes[id]);
289
454
  }
455
+ // Legacy: site.children used to hold nested BuildingNode / ItemNode
456
+ // objects (see the SiteNode schema before the children-as-ids fix).
457
+ // Flatten any leftover nested children into ids, and absorb the
458
+ // embedded nodes into the flat map so the rest of the loader can
459
+ // treat the site like every other parent.
460
+ if (node.type === 'site' && Array.isArray(node.children)) {
461
+ let needsFlatten = false;
462
+ const flattened = [];
463
+ for (const child of node.children) {
464
+ if (typeof child === 'string') {
465
+ flattened.push(child);
466
+ }
467
+ else if (child && typeof child === 'object' && typeof child.id === 'string') {
468
+ needsFlatten = true;
469
+ flattened.push(child.id);
470
+ if (!patchedNodes[child.id]) {
471
+ patchedNodes[child.id] = { ...child, parentId: id };
472
+ }
473
+ }
474
+ }
475
+ if (needsFlatten) {
476
+ patchedNodes[id] = { ...node, children: flattened };
477
+ }
478
+ }
290
479
  }
291
480
  return patchedNodes;
292
481
  }
293
482
  function getNodeChildIds(node) {
294
- if (!('children' in node) || !Array.isArray(node.children)) {
483
+ if (!('children' in node && Array.isArray(node.children))) {
295
484
  return [];
296
485
  }
297
486
  return node.children
@@ -373,6 +562,12 @@ const useScene = create()(temporal((set, get) => ({
373
562
  delete cleanedNodes[node.id];
374
563
  }
375
564
  }
565
+ set({
566
+ nodes: cleanedNodes,
567
+ rootNodeIds,
568
+ dirtyNodes: new Set(),
569
+ collections: {},
570
+ });
376
571
  const normalizedRootNodeIds = normalizeRootNodeIds(cleanedNodes, rootNodeIds);
377
572
  const reachableNodeIds = collectReachableNodeIds(cleanedNodes, normalizedRootNodeIds);
378
573
  if (normalizedRootNodeIds.length > 0) {
@@ -411,7 +606,7 @@ const useScene = create()(temporal((set, get) => ({
411
606
  children: [level0.id],
412
607
  });
413
608
  const site = SiteNode.parse({
414
- children: [building],
609
+ children: [building.id],
415
610
  });
416
611
  // Define all nodes flat
417
612
  const nodes = {
@@ -431,6 +626,7 @@ const useScene = create()(temporal((set, get) => ({
431
626
  },
432
627
  createNodes: (ops) => nodeActions.createNodesAction(set, get, ops),
433
628
  createNode: (node, parentId) => nodeActions.createNodesAction(set, get, [{ node, parentId }]),
629
+ applyNodeChanges: (changes) => nodeActions.applyNodeChangesAction(set, get, changes),
434
630
  updateNodes: (updates) => nodeActions.updateNodesAction(set, get, updates),
435
631
  updateNode: (id, data) => nodeActions.updateNodesAction(set, get, [{ id, data }]),
436
632
  // --- DELETE ---
@@ -547,8 +743,8 @@ let prevPastLength = 0;
547
743
  let prevFutureLength = 0;
548
744
  let prevNodesSnapshot = null;
549
745
  export function clearSceneHistory() {
550
- useScene.temporal.getState().clear();
551
746
  resetSceneHistoryPauseDepth();
747
+ useScene.temporal.getState().clear();
552
748
  prevPastLength = 0;
553
749
  prevFutureLength = 0;
554
750
  prevNodesSnapshot = null;
@@ -0,0 +1,12 @@
1
+ import type { AnyNode, AnyNodeId } from '../../schema';
2
+ import type { ElevatorInteractiveState } from '../../store/use-interactive';
3
+ type ElevatorRuntimeMap = Record<AnyNodeId, ElevatorInteractiveState>;
4
+ type ResolveElevatorDispatchTargetArgs = {
5
+ elevators: ElevatorRuntimeMap;
6
+ levelId: AnyNodeId;
7
+ nodes: Record<string, AnyNode>;
8
+ requestedElevatorId: AnyNodeId;
9
+ };
10
+ export declare function resolveElevatorDispatchTarget({ elevators, levelId, nodes, requestedElevatorId, }: ResolveElevatorDispatchTargetArgs): AnyNodeId;
11
+ export {};
12
+ //# sourceMappingURL=elevator-dispatch.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"elevator-dispatch.d.ts","sourceRoot":"","sources":["../../../src/systems/elevator/elevator-dispatch.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,OAAO,EAAE,SAAS,EAAgB,MAAM,cAAc,CAAA;AACpE,OAAO,KAAK,EAAE,wBAAwB,EAAE,MAAM,6BAA6B,CAAA;AAG3E,KAAK,kBAAkB,GAAG,MAAM,CAAC,SAAS,EAAE,wBAAwB,CAAC,CAAA;AAErE,KAAK,iCAAiC,GAAG;IACvC,SAAS,EAAE,kBAAkB,CAAA;IAC7B,OAAO,EAAE,SAAS,CAAA;IAClB,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAA;IAC9B,mBAAmB,EAAE,SAAS,CAAA;CAC/B,CAAA;AAsCD,wBAAgB,6BAA6B,CAAC,EAC5C,SAAS,EACT,OAAO,EACP,KAAK,EACL,mBAAmB,GACpB,EAAE,iCAAiC,GAAG,SAAS,CAmC/C"}
@@ -0,0 +1,57 @@
1
+ import { resolveElevatorServiceLevels } from './elevator-service';
2
+ function getRuntimeLevelId(runtime, elevator) {
3
+ return runtime?.currentLevelId ?? elevator.defaultLevelId ?? elevator.fromLevelId ?? null;
4
+ }
5
+ function scoreDispatchCandidate({ elevator, elevators, levelId, nodes, }) {
6
+ if ((elevator.disabledLevelIds ?? []).includes(levelId))
7
+ return null;
8
+ if ((elevator.serviceOnlyLevelIds ?? []).includes(levelId))
9
+ return null;
10
+ const serviceLevels = resolveElevatorServiceLevels(elevator, nodes);
11
+ const targetIndex = serviceLevels.findIndex((level) => level.id === levelId);
12
+ if (targetIndex < 0)
13
+ return null;
14
+ const runtime = elevators[elevator.id];
15
+ const runtimeLevelId = getRuntimeLevelId(runtime, elevator);
16
+ const currentIndex = runtimeLevelId
17
+ ? serviceLevels.findIndex((level) => level.id === runtimeLevelId)
18
+ : -1;
19
+ const resolvedCurrentIndex = currentIndex >= 0 ? currentIndex : 0;
20
+ const distance = Math.abs(targetIndex - resolvedCurrentIndex);
21
+ const queuePenalty = (runtime?.queue.length ?? 0) * 2 + (runtime?.targetLevelId ? 1 : 0);
22
+ const motionPenalty = runtime?.phase === 'moving' || runtime?.phase === 'closing' ? 0.5 : 0;
23
+ const openPenalty = runtime?.phase === 'open' || runtime?.phase === 'opening' ? 0.2 : 0;
24
+ return distance + queuePenalty + motionPenalty + openPenalty;
25
+ }
26
+ export function resolveElevatorDispatchTarget({ elevators, levelId, nodes, requestedElevatorId, }) {
27
+ const requestedElevator = nodes[requestedElevatorId];
28
+ if (!(requestedElevator?.type === 'elevator' && requestedElevator.parentId)) {
29
+ return requestedElevatorId;
30
+ }
31
+ const building = nodes[requestedElevator.parentId];
32
+ if (building?.type !== 'building') {
33
+ return requestedElevatorId;
34
+ }
35
+ let bestElevatorId = null;
36
+ let bestScore = Number.POSITIVE_INFINITY;
37
+ for (const childId of building.children) {
38
+ const candidate = nodes[childId];
39
+ if (!(candidate?.type === 'elevator' && candidate.visible !== false))
40
+ continue;
41
+ const score = scoreDispatchCandidate({
42
+ elevator: candidate,
43
+ elevators,
44
+ levelId,
45
+ nodes,
46
+ });
47
+ if (score === null)
48
+ continue;
49
+ const tieBreaker = candidate.id === requestedElevatorId ? -0.01 : 0;
50
+ const finalScore = score + tieBreaker;
51
+ if (finalScore < bestScore) {
52
+ bestScore = finalScore;
53
+ bestElevatorId = candidate.id;
54
+ }
55
+ }
56
+ return bestElevatorId ?? requestedElevatorId;
57
+ }
@@ -0,0 +1,15 @@
1
+ import type { ElevatorDoorPanelStyle, ElevatorDoorStyle, ElevatorNode, ElevatorShaftStyle } from '../../schema';
2
+ export type ElevatorDoorSide = 'left' | 'right';
3
+ export declare function getResolvedElevatorDoorStyle(doorStyle: ElevatorNode['doorStyle'] | undefined): ElevatorDoorStyle;
4
+ export declare function getResolvedElevatorDoorPanelStyle(doorPanelStyle: ElevatorNode['doorPanelStyle'] | undefined): ElevatorDoorPanelStyle;
5
+ export declare function getResolvedElevatorShaftStyle(shaftStyle: ElevatorNode['shaftStyle'] | undefined): ElevatorShaftStyle;
6
+ export declare function getElevatorDoorLeafSides(doorStyle: ElevatorNode['doorStyle'] | undefined): ElevatorDoorSide[];
7
+ export declare function getElevatorDoorLeafX(side: ElevatorDoorSide, openingWidth: number, doorOpen: number, doorStyle: ElevatorNode['doorStyle'] | undefined): number;
8
+ export declare function getElevatorDoorLeafWidth(openingWidth: number, doorStyle: ElevatorNode['doorStyle'] | undefined): number;
9
+ export declare function getElevatorCabWidth(node: ElevatorNode): number;
10
+ export declare function getElevatorCabDepth(node: ElevatorNode): number;
11
+ export declare function getElevatorShaftWallThickness(node: ElevatorNode): number;
12
+ export declare function getElevatorShaftWidth(node: ElevatorNode, cabWidth?: number): number;
13
+ export declare function getElevatorShaftDepth(node: ElevatorNode, cabDepth?: number): number;
14
+ export declare function getElevatorCabCenterZ(node: ElevatorNode): number;
15
+ //# sourceMappingURL=elevator-geometry.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"elevator-geometry.d.ts","sourceRoot":"","sources":["../../../src/systems/elevator/elevator-geometry.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACV,sBAAsB,EACtB,iBAAiB,EACjB,YAAY,EACZ,kBAAkB,EACnB,MAAM,cAAc,CAAA;AAErB,MAAM,MAAM,gBAAgB,GAAG,MAAM,GAAG,OAAO,CAAA;AAI/C,wBAAgB,4BAA4B,CAC1C,SAAS,EAAE,YAAY,CAAC,WAAW,CAAC,GAAG,SAAS,GAC/C,iBAAiB,CAEnB;AAED,wBAAgB,iCAAiC,CAC/C,cAAc,EAAE,YAAY,CAAC,gBAAgB,CAAC,GAAG,SAAS,GACzD,sBAAsB,CAExB;AAED,wBAAgB,6BAA6B,CAC3C,UAAU,EAAE,YAAY,CAAC,YAAY,CAAC,GAAG,SAAS,GACjD,kBAAkB,CAEpB;AAED,wBAAgB,wBAAwB,CACtC,SAAS,EAAE,YAAY,CAAC,WAAW,CAAC,GAAG,SAAS,GAC/C,gBAAgB,EAAE,CAKpB;AAED,wBAAgB,oBAAoB,CAClC,IAAI,EAAE,gBAAgB,EACtB,YAAY,EAAE,MAAM,EACpB,QAAQ,EAAE,MAAM,EAChB,SAAS,EAAE,YAAY,CAAC,WAAW,CAAC,GAAG,SAAS,UAUjD;AAED,wBAAgB,wBAAwB,CACtC,YAAY,EAAE,MAAM,EACpB,SAAS,EAAE,YAAY,CAAC,WAAW,CAAC,GAAG,SAAS,UAKjD;AAED,wBAAgB,mBAAmB,CAAC,IAAI,EAAE,YAAY,UAErD;AAED,wBAAgB,mBAAmB,CAAC,IAAI,EAAE,YAAY,UAErD;AAED,wBAAgB,6BAA6B,CAAC,IAAI,EAAE,YAAY,UAE/D;AAED,wBAAgB,qBAAqB,CAAC,IAAI,EAAE,YAAY,EAAE,QAAQ,SAA4B,UAE7F;AAED,wBAAgB,qBAAqB,CAAC,IAAI,EAAE,YAAY,EAAE,QAAQ,SAA4B,UAE7F;AAED,wBAAgB,qBAAqB,CAAC,IAAI,EAAE,YAAY,UAEvD"}
@@ -0,0 +1,50 @@
1
+ const DEFAULT_ELEVATOR_SHAFT_WALL_THICKNESS = 0.09;
2
+ export function getResolvedElevatorDoorStyle(doorStyle) {
3
+ return doorStyle ?? 'center-opening';
4
+ }
5
+ export function getResolvedElevatorDoorPanelStyle(doorPanelStyle) {
6
+ return doorPanelStyle ?? 'glass-frame';
7
+ }
8
+ export function getResolvedElevatorShaftStyle(shaftStyle) {
9
+ return shaftStyle ?? 'solid';
10
+ }
11
+ export function getElevatorDoorLeafSides(doorStyle) {
12
+ const resolvedDoorStyle = getResolvedElevatorDoorStyle(doorStyle);
13
+ if (resolvedDoorStyle === 'single-left')
14
+ return ['left'];
15
+ if (resolvedDoorStyle === 'single-right')
16
+ return ['right'];
17
+ return ['left', 'right'];
18
+ }
19
+ export function getElevatorDoorLeafX(side, openingWidth, doorOpen, doorStyle) {
20
+ const resolvedDoorStyle = getResolvedElevatorDoorStyle(doorStyle);
21
+ if (resolvedDoorStyle === 'center-opening') {
22
+ const direction = side === 'left' ? -1 : 1;
23
+ return direction * (openingWidth / 4 + doorOpen * openingWidth * 0.34);
24
+ }
25
+ const direction = resolvedDoorStyle === 'single-left' ? -1 : 1;
26
+ return direction * doorOpen * openingWidth * 0.68;
27
+ }
28
+ export function getElevatorDoorLeafWidth(openingWidth, doorStyle) {
29
+ return getResolvedElevatorDoorStyle(doorStyle) === 'center-opening'
30
+ ? Math.max(openingWidth / 2 - 0.018, 0.12)
31
+ : Math.max(openingWidth - 0.018, 0.18);
32
+ }
33
+ export function getElevatorCabWidth(node) {
34
+ return Math.max(node.width, 0.8);
35
+ }
36
+ export function getElevatorCabDepth(node) {
37
+ return Math.max(node.depth, 0.8);
38
+ }
39
+ export function getElevatorShaftWallThickness(node) {
40
+ return Math.max(node.shaftWallThickness ?? DEFAULT_ELEVATOR_SHAFT_WALL_THICKNESS, 0.04);
41
+ }
42
+ export function getElevatorShaftWidth(node, cabWidth = getElevatorCabWidth(node)) {
43
+ return Math.max(node.shaftWidth ?? cabWidth, cabWidth, 0.8);
44
+ }
45
+ export function getElevatorShaftDepth(node, cabDepth = getElevatorCabDepth(node)) {
46
+ return Math.max(node.shaftDepth ?? cabDepth, cabDepth, 0.8);
47
+ }
48
+ export function getElevatorCabCenterZ(node) {
49
+ return -getElevatorShaftDepth(node) / 2 + getElevatorCabDepth(node) / 2;
50
+ }
@@ -0,0 +1,6 @@
1
+ import type { AnyNode, AnyNodeId, CeilingNode, SlabNode } from '../../schema';
2
+ export declare function syncAutoElevatorOpenings(nodes: Record<string, AnyNode>): {
3
+ id: AnyNodeId;
4
+ data: Partial<SlabNode | CeilingNode>;
5
+ }[];
6
+ //# sourceMappingURL=elevator-opening-sync.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"elevator-opening-sync.d.ts","sourceRoot":"","sources":["../../../src/systems/elevator/elevator-opening-sync.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EACV,OAAO,EACP,SAAS,EACT,WAAW,EAEX,QAAQ,EAET,MAAM,cAAc,CAAA;AA8JrB,wBAAgB,wBAAwB,CAAC,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC;QAQ1C,SAAS;UAAQ,OAAO,CAAC,QAAQ,GAAG,WAAW,CAAC;IAmG5E"}