@pascal-app/core 0.9.1 → 1.0.0-beta.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 (378) hide show
  1. package/README.md +17 -10
  2. package/dist/events/bus.d.ts +49 -2
  3. package/dist/events/bus.d.ts.map +1 -1
  4. package/dist/hooks/scene-registry/item-clip-registry.d.ts +18 -0
  5. package/dist/hooks/scene-registry/item-clip-registry.d.ts.map +1 -0
  6. package/dist/hooks/scene-registry/item-clip-registry.js +10 -0
  7. package/dist/hooks/scene-registry/scene-registry.d.ts +8 -1
  8. package/dist/hooks/scene-registry/scene-registry.d.ts.map +1 -1
  9. package/dist/hooks/scene-registry/scene-registry.js +27 -1
  10. package/dist/hooks/spatial-grid/floor-placed-elevation.d.ts +17 -1
  11. package/dist/hooks/spatial-grid/floor-placed-elevation.d.ts.map +1 -1
  12. package/dist/hooks/spatial-grid/floor-placed-elevation.js +61 -8
  13. package/dist/hooks/spatial-grid/spatial-grid-manager.d.ts +135 -28
  14. package/dist/hooks/spatial-grid/spatial-grid-manager.d.ts.map +1 -1
  15. package/dist/hooks/spatial-grid/spatial-grid-manager.js +427 -271
  16. package/dist/hooks/spatial-grid/spatial-grid-sync.d.ts +46 -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 +208 -9
  19. package/dist/hooks/spatial-grid/support-host-id.d.ts +3 -0
  20. package/dist/hooks/spatial-grid/support-host-id.d.ts.map +1 -0
  21. package/dist/hooks/spatial-grid/support-host-id.js +2 -0
  22. package/dist/hooks/spatial-grid/support-host-patch.d.ts +51 -0
  23. package/dist/hooks/spatial-grid/support-host-patch.d.ts.map +1 -0
  24. package/dist/hooks/spatial-grid/support-host-patch.js +164 -0
  25. package/dist/index.d.ts +33 -11
  26. package/dist/index.d.ts.map +1 -1
  27. package/dist/index.js +32 -10
  28. package/dist/lib/level-name.d.ts +4 -0
  29. package/dist/lib/level-name.d.ts.map +1 -0
  30. package/dist/lib/level-name.js +10 -0
  31. package/dist/lib/measurement-geometry.d.ts +23 -0
  32. package/dist/lib/measurement-geometry.d.ts.map +1 -0
  33. package/dist/lib/measurement-geometry.js +249 -0
  34. package/dist/lib/polygon-geometry.d.ts +0 -1
  35. package/dist/lib/polygon-geometry.d.ts.map +1 -1
  36. package/dist/lib/polygon-geometry.js +0 -18
  37. package/dist/lib/selection-proxy.d.ts +4 -0
  38. package/dist/lib/selection-proxy.d.ts.map +1 -0
  39. package/dist/lib/selection-proxy.js +12 -0
  40. package/dist/lib/slab-polygon.d.ts +31 -2
  41. package/dist/lib/slab-polygon.d.ts.map +1 -1
  42. package/dist/lib/slab-polygon.js +557 -31
  43. package/dist/lib/slots.d.ts +14 -0
  44. package/dist/lib/slots.d.ts.map +1 -0
  45. package/dist/lib/slots.js +26 -0
  46. package/dist/lib/space-detection.d.ts +40 -6
  47. package/dist/lib/space-detection.d.ts.map +1 -1
  48. package/dist/lib/space-detection.js +574 -192
  49. package/dist/lib/terrain-brush.d.ts +163 -0
  50. package/dist/lib/terrain-brush.d.ts.map +1 -0
  51. package/dist/lib/terrain-brush.js +359 -0
  52. package/dist/lib/terrain-codec.d.ts +51 -0
  53. package/dist/lib/terrain-codec.d.ts.map +1 -0
  54. package/dist/lib/terrain-codec.js +167 -0
  55. package/dist/lib/terrain-field.d.ts +150 -0
  56. package/dist/lib/terrain-field.d.ts.map +1 -0
  57. package/dist/lib/terrain-field.js +248 -0
  58. package/dist/lib/terrain-raycast.d.ts +44 -0
  59. package/dist/lib/terrain-raycast.d.ts.map +1 -0
  60. package/dist/lib/terrain-raycast.js +188 -0
  61. package/dist/lib/terrain-source.d.ts +60 -0
  62. package/dist/lib/terrain-source.d.ts.map +1 -0
  63. package/dist/lib/terrain-source.js +90 -0
  64. package/dist/lib/terrain-support.d.ts +60 -0
  65. package/dist/lib/terrain-support.d.ts.map +1 -0
  66. package/dist/lib/terrain-support.js +150 -0
  67. package/dist/lib/wall-distance.d.ts +60 -0
  68. package/dist/lib/wall-distance.d.ts.map +1 -0
  69. package/dist/lib/wall-distance.js +85 -0
  70. package/dist/lib/zone-quantities.d.ts +21 -0
  71. package/dist/lib/zone-quantities.d.ts.map +1 -0
  72. package/dist/lib/zone-quantities.js +428 -0
  73. package/dist/material-library.d.ts +28 -1
  74. package/dist/material-library.d.ts.map +1 -1
  75. package/dist/material-library.js +2233 -441
  76. package/dist/registry/handles.d.ts +91 -3
  77. package/dist/registry/handles.d.ts.map +1 -1
  78. package/dist/registry/index.d.ts +3 -3
  79. package/dist/registry/index.d.ts.map +1 -1
  80. package/dist/registry/index.js +1 -1
  81. package/dist/registry/registry.d.ts +46 -3
  82. package/dist/registry/registry.d.ts.map +1 -1
  83. package/dist/registry/registry.js +82 -1
  84. package/dist/registry/subtree.d.ts.map +1 -1
  85. package/dist/registry/subtree.js +8 -1
  86. package/dist/registry/types.d.ts +781 -39
  87. package/dist/registry/types.d.ts.map +1 -1
  88. package/dist/schema/index.d.ts +27 -8
  89. package/dist/schema/index.d.ts.map +1 -1
  90. package/dist/schema/index.js +23 -6
  91. package/dist/schema/material.d.ts +3 -2
  92. package/dist/schema/material.d.ts.map +1 -1
  93. package/dist/schema/material.js +7 -5
  94. package/dist/schema/nodes/box-vent.d.ts +2 -2
  95. package/dist/schema/nodes/cabinet.d.ts +536 -0
  96. package/dist/schema/nodes/cabinet.d.ts.map +1 -0
  97. package/dist/schema/nodes/cabinet.js +139 -0
  98. package/dist/schema/nodes/ceiling.d.ts +4 -3
  99. package/dist/schema/nodes/ceiling.d.ts.map +1 -1
  100. package/dist/schema/nodes/ceiling.js +11 -1
  101. package/dist/schema/nodes/chimney.d.ts +4 -4
  102. package/dist/schema/nodes/column.d.ts +4 -2
  103. package/dist/schema/nodes/column.d.ts.map +1 -1
  104. package/dist/schema/nodes/column.js +5 -0
  105. package/dist/schema/nodes/construction-dimension.d.ts +195 -0
  106. package/dist/schema/nodes/construction-dimension.d.ts.map +1 -0
  107. package/dist/schema/nodes/construction-dimension.js +151 -0
  108. package/dist/schema/nodes/cupola.d.ts +2 -2
  109. package/dist/schema/nodes/door.d.ts +42 -5
  110. package/dist/schema/nodes/door.d.ts.map +1 -1
  111. package/dist/schema/nodes/door.js +32 -0
  112. package/dist/schema/nodes/dormer.d.ts +8 -8
  113. package/dist/schema/nodes/dormer.js +1 -1
  114. package/dist/schema/nodes/downspout.d.ts +2 -2
  115. package/dist/schema/nodes/duct-fitting.d.ts +91 -0
  116. package/dist/schema/nodes/duct-fitting.d.ts.map +1 -0
  117. package/dist/schema/nodes/duct-fitting.js +92 -0
  118. package/dist/schema/nodes/duct-segment.d.ts +61 -0
  119. package/dist/schema/nodes/duct-segment.d.ts.map +1 -0
  120. package/dist/schema/nodes/duct-segment.js +73 -0
  121. package/dist/schema/nodes/duct-terminal.d.ts +58 -0
  122. package/dist/schema/nodes/duct-terminal.d.ts.map +1 -0
  123. package/dist/schema/nodes/duct-terminal.js +53 -0
  124. package/dist/schema/nodes/elevator.d.ts +3 -2
  125. package/dist/schema/nodes/elevator.d.ts.map +1 -1
  126. package/dist/schema/nodes/elevator.js +3 -0
  127. package/dist/schema/nodes/eyebrow-vent.d.ts +2 -2
  128. package/dist/schema/nodes/fence.d.ts +20 -2
  129. package/dist/schema/nodes/fence.d.ts.map +1 -1
  130. package/dist/schema/nodes/fence.js +33 -2
  131. package/dist/schema/nodes/gutter.d.ts +2 -2
  132. package/dist/schema/nodes/hvac-equipment.d.ts +64 -0
  133. package/dist/schema/nodes/hvac-equipment.d.ts.map +1 -0
  134. package/dist/schema/nodes/hvac-equipment.js +57 -0
  135. package/dist/schema/nodes/item.d.ts +9 -0
  136. package/dist/schema/nodes/item.d.ts.map +1 -1
  137. package/dist/schema/nodes/item.js +24 -0
  138. package/dist/schema/nodes/level.d.ts +2 -1
  139. package/dist/schema/nodes/level.d.ts.map +1 -1
  140. package/dist/schema/nodes/level.js +12 -31
  141. package/dist/schema/nodes/lineset.d.ts +42 -0
  142. package/dist/schema/nodes/lineset.d.ts.map +1 -0
  143. package/dist/schema/nodes/lineset.js +38 -0
  144. package/dist/schema/nodes/liquid-line.d.ts +34 -0
  145. package/dist/schema/nodes/liquid-line.d.ts.map +1 -0
  146. package/dist/schema/nodes/liquid-line.js +24 -0
  147. package/dist/schema/nodes/measurement.d.ts +306 -0
  148. package/dist/schema/nodes/measurement.d.ts.map +1 -0
  149. package/dist/schema/nodes/measurement.js +87 -0
  150. package/dist/schema/nodes/pipe-fitting.d.ts +56 -0
  151. package/dist/schema/nodes/pipe-fitting.d.ts.map +1 -0
  152. package/dist/schema/nodes/pipe-fitting.js +45 -0
  153. package/dist/schema/nodes/pipe-segment.d.ts +48 -0
  154. package/dist/schema/nodes/pipe-segment.d.ts.map +1 -0
  155. package/dist/schema/nodes/pipe-segment.js +36 -0
  156. package/dist/schema/nodes/pipe-trap.d.ts +43 -0
  157. package/dist/schema/nodes/pipe-trap.d.ts.map +1 -0
  158. package/dist/schema/nodes/pipe-trap.js +36 -0
  159. package/dist/schema/nodes/ridge-vent.d.ts +18 -3
  160. package/dist/schema/nodes/ridge-vent.d.ts.map +1 -1
  161. package/dist/schema/nodes/ridge-vent.js +221 -3
  162. package/dist/schema/nodes/roof-segment-shape.d.ts +90 -0
  163. package/dist/schema/nodes/roof-segment-shape.d.ts.map +1 -0
  164. package/dist/schema/nodes/roof-segment-shape.js +318 -0
  165. package/dist/schema/nodes/roof-segment-walls.d.ts +101 -0
  166. package/dist/schema/nodes/roof-segment-walls.d.ts.map +1 -0
  167. package/dist/schema/nodes/roof-segment-walls.js +330 -0
  168. package/dist/schema/nodes/roof-segment.d.ts +84 -8
  169. package/dist/schema/nodes/roof-segment.d.ts.map +1 -1
  170. package/dist/schema/nodes/roof-segment.js +290 -5
  171. package/dist/schema/nodes/roof.d.ts +8 -8
  172. package/dist/schema/nodes/shelf.d.ts +4 -2
  173. package/dist/schema/nodes/shelf.d.ts.map +1 -1
  174. package/dist/schema/nodes/shelf.js +8 -0
  175. package/dist/schema/nodes/site.d.ts +9 -0
  176. package/dist/schema/nodes/site.d.ts.map +1 -1
  177. package/dist/schema/nodes/site.js +8 -6
  178. package/dist/schema/nodes/skylight.d.ts +10 -10
  179. package/dist/schema/nodes/slab.d.ts +8 -2
  180. package/dist/schema/nodes/slab.d.ts.map +1 -1
  181. package/dist/schema/nodes/slab.js +18 -2
  182. package/dist/schema/nodes/solar-panel.d.ts +4 -4
  183. package/dist/schema/nodes/spawn.d.ts +1 -0
  184. package/dist/schema/nodes/spawn.d.ts.map +1 -1
  185. package/dist/schema/nodes/spawn.js +2 -0
  186. package/dist/schema/nodes/stair-segment.d.ts +4 -4
  187. package/dist/schema/nodes/stair.d.ts +16 -13
  188. package/dist/schema/nodes/stair.d.ts.map +1 -1
  189. package/dist/schema/nodes/stair.js +11 -1
  190. package/dist/schema/nodes/structural-grid.d.ts +27 -0
  191. package/dist/schema/nodes/structural-grid.d.ts.map +1 -0
  192. package/dist/schema/nodes/structural-grid.js +17 -0
  193. package/dist/schema/nodes/turbine-vent.d.ts +2 -2
  194. package/dist/schema/nodes/wall.d.ts +248 -6
  195. package/dist/schema/nodes/wall.d.ts.map +1 -1
  196. package/dist/schema/nodes/wall.js +266 -0
  197. package/dist/schema/nodes/window.d.ts +40 -3
  198. package/dist/schema/nodes/window.d.ts.map +1 -1
  199. package/dist/schema/nodes/window.js +30 -0
  200. package/dist/schema/nodes/zone.d.ts +22 -0
  201. package/dist/schema/nodes/zone.d.ts.map +1 -1
  202. package/dist/schema/nodes/zone.js +21 -0
  203. package/dist/schema/scene-material.d.ts +41 -0
  204. package/dist/schema/scene-material.d.ts.map +1 -0
  205. package/dist/schema/scene-material.js +9 -0
  206. package/dist/schema/terrain.d.ts +28 -0
  207. package/dist/schema/terrain.d.ts.map +1 -0
  208. package/dist/schema/terrain.js +30 -0
  209. package/dist/schema/types.d.ts +1300 -97
  210. package/dist/schema/types.d.ts.map +1 -1
  211. package/dist/schema/types.js +27 -0
  212. package/dist/services/__fixtures__/wall-plane-top-boundary-repro.d.ts +80 -0
  213. package/dist/services/__fixtures__/wall-plane-top-boundary-repro.d.ts.map +1 -0
  214. package/dist/services/__fixtures__/wall-plane-top-boundary-repro.js +89 -0
  215. package/dist/services/alignment-anchors.d.ts.map +1 -1
  216. package/dist/services/alignment-anchors.js +30 -1
  217. package/dist/services/alignment.d.ts +5 -0
  218. package/dist/services/alignment.d.ts.map +1 -1
  219. package/dist/services/alignment.js +2 -0
  220. package/dist/services/hosting.d.ts +8 -0
  221. package/dist/services/hosting.d.ts.map +1 -1
  222. package/dist/services/hosting.js +13 -4
  223. package/dist/services/index.d.ts +9 -2
  224. package/dist/services/index.d.ts.map +1 -1
  225. package/dist/services/index.js +9 -2
  226. package/dist/services/level-height.d.ts +27 -0
  227. package/dist/services/level-height.d.ts.map +1 -0
  228. package/dist/services/level-height.js +94 -0
  229. package/dist/services/opening-guides.d.ts +141 -0
  230. package/dist/services/opening-guides.d.ts.map +1 -0
  231. package/dist/services/opening-guides.js +267 -0
  232. package/dist/services/port-connectivity.d.ts +115 -0
  233. package/dist/services/port-connectivity.d.ts.map +1 -0
  234. package/dist/services/port-connectivity.js +300 -0
  235. package/dist/services/riser-diagram.d.ts +40 -0
  236. package/dist/services/riser-diagram.d.ts.map +1 -0
  237. package/dist/services/riser-diagram.js +110 -0
  238. package/dist/services/snap.d.ts +9 -1
  239. package/dist/services/snap.d.ts.map +1 -1
  240. package/dist/services/snap.js +21 -1
  241. package/dist/services/storey.d.ts +103 -0
  242. package/dist/services/storey.d.ts.map +1 -0
  243. package/dist/services/storey.js +296 -0
  244. package/dist/services/system-graph.d.ts +30 -0
  245. package/dist/services/system-graph.d.ts.map +1 -0
  246. package/dist/services/system-graph.js +164 -0
  247. package/dist/services/validate-dwv.d.ts +32 -0
  248. package/dist/services/validate-dwv.d.ts.map +1 -0
  249. package/dist/services/validate-dwv.js +118 -0
  250. package/dist/store/actions/node-actions.d.ts.map +1 -1
  251. package/dist/store/actions/node-actions.js +141 -4
  252. package/dist/store/history-control.d.ts +31 -0
  253. package/dist/store/history-control.d.ts.map +1 -1
  254. package/dist/store/history-control.js +130 -0
  255. package/dist/store/use-live-terrain.d.ts +65 -0
  256. package/dist/store/use-live-terrain.d.ts.map +1 -0
  257. package/dist/store/use-live-terrain.js +31 -0
  258. package/dist/store/use-live-transforms.d.ts +8 -0
  259. package/dist/store/use-live-transforms.d.ts.map +1 -1
  260. package/dist/store/use-scene.d.ts +47 -3
  261. package/dist/store/use-scene.d.ts.map +1 -1
  262. package/dist/store/use-scene.js +992 -55
  263. package/dist/systems/elevator/elevator-opening-system.d.ts +1 -0
  264. package/dist/systems/elevator/elevator-opening-system.d.ts.map +1 -1
  265. package/dist/systems/elevator/elevator-opening-system.js +27 -21
  266. package/dist/systems/elevator/elevator-service.d.ts +0 -2
  267. package/dist/systems/elevator/elevator-service.d.ts.map +1 -1
  268. package/dist/systems/elevator/elevator-service.js +2 -24
  269. package/dist/systems/fence/fence-centerline.d.ts +12 -0
  270. package/dist/systems/fence/fence-centerline.d.ts.map +1 -0
  271. package/dist/systems/fence/fence-centerline.js +37 -0
  272. package/dist/systems/fence/fence-spline.d.ts +39 -0
  273. package/dist/systems/fence/fence-spline.d.ts.map +1 -0
  274. package/dist/systems/fence/fence-spline.js +196 -0
  275. package/dist/systems/slab/slab-placement.d.ts +11 -0
  276. package/dist/systems/slab/slab-placement.d.ts.map +1 -0
  277. package/dist/systems/slab/slab-placement.js +14 -0
  278. package/dist/systems/slab/slab-support.d.ts +137 -0
  279. package/dist/systems/slab/slab-support.d.ts.map +1 -0
  280. package/dist/systems/slab/slab-support.js +557 -0
  281. package/dist/systems/stair/stair-footprint.d.ts.map +1 -1
  282. package/dist/systems/stair/stair-footprint.js +21 -11
  283. package/dist/systems/stair/stair-opening-preview.d.ts +1300 -505
  284. package/dist/systems/stair/stair-opening-preview.d.ts.map +1 -1
  285. package/dist/systems/stair/stair-opening-sync.d.ts.map +1 -1
  286. package/dist/systems/stair/stair-opening-sync.js +19 -33
  287. package/dist/systems/stair/stair-opening-system.d.ts.map +1 -1
  288. package/dist/systems/stair/stair-opening-system.js +32 -4
  289. package/dist/systems/stair/stair-rise.d.ts +22 -0
  290. package/dist/systems/stair/stair-rise.d.ts.map +1 -0
  291. package/dist/systems/stair/stair-rise.js +79 -0
  292. package/dist/systems/wall/wall-curve.d.ts +10 -0
  293. package/dist/systems/wall/wall-curve.d.ts.map +1 -1
  294. package/dist/systems/wall/wall-curve.js +1 -1
  295. package/dist/systems/wall/wall-mitering.d.ts.map +1 -1
  296. package/dist/systems/wall/wall-mitering.js +44 -8
  297. package/dist/systems/wall/wall-top.d.ts +40 -0
  298. package/dist/systems/wall/wall-top.d.ts.map +1 -0
  299. package/dist/systems/wall/wall-top.js +46 -0
  300. package/dist/utils/clone-scene-graph.d.ts +1 -0
  301. package/dist/utils/clone-scene-graph.d.ts.map +1 -1
  302. package/dist/utils/clone-scene-graph.js +56 -5
  303. package/dist/utils/heal-scene-graph.d.ts +18 -0
  304. package/dist/utils/heal-scene-graph.d.ts.map +1 -0
  305. package/dist/utils/heal-scene-graph.js +106 -0
  306. package/dist/validation/validate-build-json.d.ts +3 -0
  307. package/dist/validation/validate-build-json.d.ts.map +1 -1
  308. package/dist/validation/validate-build-json.js +90 -5
  309. package/package.json +4 -4
  310. package/dist/hooks/spatial-grid/floor-placed-elevation.test.d.ts +0 -2
  311. package/dist/hooks/spatial-grid/floor-placed-elevation.test.d.ts.map +0 -1
  312. package/dist/hooks/spatial-grid/floor-placed-elevation.test.js +0 -273
  313. package/dist/lib/space-detection-pause.test.d.ts +0 -2
  314. package/dist/lib/space-detection-pause.test.d.ts.map +0 -1
  315. package/dist/lib/space-detection-pause.test.js +0 -42
  316. package/dist/lib/space-detection.test.d.ts +0 -2
  317. package/dist/lib/space-detection.test.d.ts.map +0 -1
  318. package/dist/lib/space-detection.test.js +0 -77
  319. package/dist/registry/registry.test.d.ts +0 -2
  320. package/dist/registry/registry.test.d.ts.map +0 -1
  321. package/dist/registry/registry.test.js +0 -183
  322. package/dist/registry/relations-resolver.test.d.ts +0 -2
  323. package/dist/registry/relations-resolver.test.d.ts.map +0 -1
  324. package/dist/registry/relations-resolver.test.js +0 -183
  325. package/dist/registry/scene-api.test.d.ts +0 -2
  326. package/dist/registry/scene-api.test.d.ts.map +0 -1
  327. package/dist/registry/scene-api.test.js +0 -183
  328. package/dist/registry/subtree.test.d.ts +0 -2
  329. package/dist/registry/subtree.test.d.ts.map +0 -1
  330. package/dist/registry/subtree.test.js +0 -133
  331. package/dist/schema/asset-url.test.d.ts +0 -2
  332. package/dist/schema/asset-url.test.d.ts.map +0 -1
  333. package/dist/schema/asset-url.test.js +0 -134
  334. package/dist/services/alignment-anchors.test.d.ts +0 -2
  335. package/dist/services/alignment-anchors.test.d.ts.map +0 -1
  336. package/dist/services/alignment-anchors.test.js +0 -383
  337. package/dist/services/alignment.test.d.ts +0 -2
  338. package/dist/services/alignment.test.d.ts.map +0 -1
  339. package/dist/services/alignment.test.js +0 -92
  340. package/dist/services/drag-session.test.d.ts +0 -2
  341. package/dist/services/drag-session.test.d.ts.map +0 -1
  342. package/dist/services/drag-session.test.js +0 -219
  343. package/dist/services/hosting.test.d.ts +0 -2
  344. package/dist/services/hosting.test.d.ts.map +0 -1
  345. package/dist/services/hosting.test.js +0 -199
  346. package/dist/services/movement.test.d.ts +0 -2
  347. package/dist/services/movement.test.d.ts.map +0 -1
  348. package/dist/services/movement.test.js +0 -133
  349. package/dist/services/single-undo-dance.test.d.ts +0 -2
  350. package/dist/services/single-undo-dance.test.d.ts.map +0 -1
  351. package/dist/services/single-undo-dance.test.js +0 -210
  352. package/dist/services/snap.test.d.ts +0 -2
  353. package/dist/services/snap.test.d.ts.map +0 -1
  354. package/dist/services/snap.test.js +0 -95
  355. package/dist/store/actions/node-mutation-sanitize.test.d.ts +0 -2
  356. package/dist/store/actions/node-mutation-sanitize.test.d.ts.map +0 -1
  357. package/dist/store/actions/node-mutation-sanitize.test.js +0 -147
  358. package/dist/store/actions/reparent.test.d.ts +0 -2
  359. package/dist/store/actions/reparent.test.d.ts.map +0 -1
  360. package/dist/store/actions/reparent.test.js +0 -212
  361. package/dist/store/use-scene-elevator-migration.test.d.ts +0 -2
  362. package/dist/store/use-scene-elevator-migration.test.d.ts.map +0 -1
  363. package/dist/store/use-scene-elevator-migration.test.js +0 -109
  364. package/dist/systems/elevator/elevator-opening-sync.test.d.ts +0 -2
  365. package/dist/systems/elevator/elevator-opening-sync.test.d.ts.map +0 -1
  366. package/dist/systems/elevator/elevator-opening-sync.test.js +0 -146
  367. package/dist/systems/elevator/elevator-runtime.test.d.ts +0 -2
  368. package/dist/systems/elevator/elevator-runtime.test.d.ts.map +0 -1
  369. package/dist/systems/elevator/elevator-runtime.test.js +0 -64
  370. package/dist/systems/stair/stair-opening-preview.test.d.ts +0 -2
  371. package/dist/systems/stair/stair-opening-preview.test.d.ts.map +0 -1
  372. package/dist/systems/stair/stair-opening-preview.test.js +0 -80
  373. package/dist/systems/stair/stair-opening-sync.test.d.ts +0 -2
  374. package/dist/systems/stair/stair-opening-sync.test.d.ts.map +0 -1
  375. package/dist/systems/stair/stair-opening-sync.test.js +0 -446
  376. package/dist/utils/clone-scene-graph.test.d.ts +0 -2
  377. package/dist/utils/clone-scene-graph.test.d.ts.map +0 -1
  378. package/dist/utils/clone-scene-graph.test.js +0 -59
@@ -1,210 +0,0 @@
1
- import { beforeEach, describe, expect, test } from 'bun:test';
2
- import { createSceneApi } from '../registry/scene-api';
3
- import useScene from '../store/use-scene';
4
- globalThis.requestAnimationFrame ??= ((cb) => {
5
- cb(0);
6
- return 0;
7
- });
8
- globalThis.cancelAnimationFrame ??=
9
- () => { };
10
- /**
11
- * Validates the "single-undo dance" pattern used by Stage D actions:
12
- *
13
- * action.commit:
14
- * scene.restoreAll() // revert via snapshot (paused → no zundo record)
15
- * scene.resumeHistory() // unpause zundo
16
- * scene.update(...) // re-apply final → zundo records one diff
17
- * return true
18
- *
19
- * After the dance, undo() should roll back ONLY the drag's commit —
20
- * never further back than that. The fence-bend regression that surfaced
21
- * after Phase 5 Stage D porting was reportedly losing the prior create
22
- * step on undo; this test pins the correct behavior.
23
- */
24
- const FENCE_ID = 'fence_test';
25
- function makeFence(curveOffset) {
26
- return {
27
- id: FENCE_ID,
28
- type: 'fence',
29
- parentId: null,
30
- object: 'node',
31
- visible: true,
32
- metadata: {},
33
- start: [0, 0],
34
- end: [3, 0],
35
- height: 1.8,
36
- thickness: 0.08,
37
- baseHeight: 0.22,
38
- postSpacing: 2,
39
- postSize: 0.1,
40
- topRailHeight: 0.04,
41
- groundClearance: 0,
42
- edgeInset: 0.015,
43
- baseStyle: 'grounded',
44
- showInfill: true,
45
- color: '#ffffff',
46
- style: 'slat',
47
- curveOffset,
48
- };
49
- }
50
- describe('Single-undo dance', () => {
51
- beforeEach(() => {
52
- useScene.setState({ nodes: {}, rootNodeIds: [] });
53
- useScene.temporal.getState().clear();
54
- });
55
- test('curve-style commit yields a single undo step', () => {
56
- // 1. Create the fence (recorded by zundo).
57
- useScene.getState().createNode(makeFence(0));
58
- const pastCountAfterCreate = useScene.temporal.getState().pastStates.length;
59
- // 2. Simulate the drag.
60
- const scene = createSceneApi(useScene);
61
- scene.pauseHistory();
62
- scene.update(FENCE_ID, { curveOffset: 0.2 });
63
- scene.update(FENCE_ID, { curveOffset: 0.5 });
64
- expect(useScene.getState().nodes[FENCE_ID].curveOffset).toBe(0.5);
65
- // 3. Commit dance.
66
- scene.restoreAll();
67
- expect(useScene.getState().nodes[FENCE_ID].curveOffset).toBe(0);
68
- scene.resumeHistory();
69
- scene.update(FENCE_ID, { curveOffset: 0.5 });
70
- const pastCountAfterDance = useScene.temporal.getState().pastStates.length;
71
- expect(pastCountAfterDance).toBe(pastCountAfterCreate + 1);
72
- expect(useScene.getState().nodes[FENCE_ID].curveOffset).toBe(0.5);
73
- // 4. Undo — should return fence to curveOffset 0, NOT delete it.
74
- useScene.temporal.getState().undo();
75
- const fenceAfterUndo = useScene.getState().nodes[FENCE_ID];
76
- expect(fenceAfterUndo).toBeDefined();
77
- expect(fenceAfterUndo.curveOffset).toBe(0);
78
- });
79
- test('StrictMode double-mount: dispose-then-restart preserves history', () => {
80
- useScene.getState().createNode(makeFence(0));
81
- const pastBeforeBend = useScene.temporal.getState().pastStates.length;
82
- // Simulate StrictMode: first mount → cleanup (dispose) → second mount → drag → commit.
83
- const scene = createSceneApi(useScene);
84
- // Mount 1.
85
- scene.pauseHistory();
86
- // Mount 1 cleanup (StrictMode): no apply happened yet. dispose-equivalent.
87
- scene.restoreAll(); // snapshot empty, no-op.
88
- scene.resumeHistory();
89
- // Mount 2.
90
- scene.pauseHistory();
91
- // Drag.
92
- scene.update(FENCE_ID, { curveOffset: 0.3 });
93
- scene.update(FENCE_ID, { curveOffset: 0.7 });
94
- // Commit dance.
95
- scene.restoreAll();
96
- scene.resumeHistory();
97
- scene.update(FENCE_ID, { curveOffset: 0.7 });
98
- const pastAfterBend = useScene.temporal.getState().pastStates.length;
99
- // Exactly one new entry: the pre-bend state. Not two (which would mean
100
- // StrictMode's first mount/cleanup polluted history).
101
- expect(pastAfterBend).toBe(pastBeforeBend + 1);
102
- useScene.temporal.getState().undo();
103
- expect(useScene.getState().nodes[FENCE_ID]).toBeDefined();
104
- expect(useScene.getState().nodes[FENCE_ID].curveOffset).toBe(0);
105
- });
106
- test('commit-returns-false (no change) does NOT consume the prior pastState', () => {
107
- // This is the suspected bend regression: when action.commit returns
108
- // false (draft.curveOffset === ctx.originalCurveOffset), session.commit
109
- // calls scene.restoreAll() but doesn't push to pastStates. Subsequent
110
- // undo pops the PRIOR action (e.g. fence creation), not the no-op
111
- // bend.
112
- useScene.getState().createNode(makeFence(0));
113
- const pastBeforeBend = useScene.temporal.getState().pastStates.length;
114
- const scene = createSceneApi(useScene);
115
- scene.pauseHistory();
116
- // Drag back to original — simulates no-op bend.
117
- scene.update(FENCE_ID, { curveOffset: 0.0 });
118
- // Cancel path (mimics session.commit → action.commit returns false → scene.restoreAll → terminate).
119
- scene.restoreAll();
120
- scene.resumeHistory();
121
- const pastAfterNoOp = useScene.temporal.getState().pastStates.length;
122
- expect(pastAfterNoOp).toBe(pastBeforeBend); // no entries added
123
- // Now undo — this should be a no-op (state unchanged), but pops the create.
124
- useScene.temporal.getState().undo();
125
- // ⚠️ Reproduces the bug — undo removes the fence:
126
- const fence = useScene.getState().nodes[FENCE_ID];
127
- if (fence === undefined) {
128
- // Bug reproduced. The "no-op bend" allowed Ctrl-Z to fall through
129
- // to the fence creation. Fix is in action.commit: don't return false
130
- // — push a no-op entry instead, or guard against the cancel path.
131
- expect(fence).toBeUndefined();
132
- }
133
- else {
134
- expect(fence.curveOffset).toBe(0);
135
- }
136
- });
137
- test('full session flow via createDragSession with real action.commit dance', async () => {
138
- // Reproduces the actual wrapper flow:
139
- // - createNode → session.start → moves → grid:click → session.commit.
140
- // The action.commit does the dance internally.
141
- const { createDragSession } = await import('./drag-session');
142
- useScene.getState().createNode(makeFence(0));
143
- const pastBeforeBend = useScene.temporal.getState().pastStates.length;
144
- const scene = createSceneApi(useScene);
145
- const action = {
146
- begin: () => ({ original: 0 }),
147
- preview: (_ctx, point) => ({ offset: point[0] }),
148
- apply: (draft, _ctx, s) => {
149
- s.update(FENCE_ID, { curveOffset: draft.offset });
150
- return [FENCE_ID];
151
- },
152
- commit: (draft, ctx, s) => {
153
- if (draft.offset === ctx.original)
154
- return false;
155
- s.restoreAll();
156
- s.resumeHistory();
157
- s.update(FENCE_ID, { curveOffset: draft.offset });
158
- return true;
159
- },
160
- cancel: () => { },
161
- };
162
- const session = createDragSession(action, scene);
163
- session.start({ point: [0, 0] });
164
- session.move([0.3, 0], { shift: false, alt: false, ctrl: false, meta: false });
165
- session.move([0.5, 0], { shift: false, alt: false, ctrl: false, meta: false });
166
- const okCommit = session.commit();
167
- expect(okCommit).toBe(true);
168
- const pastAfter = useScene.temporal.getState().pastStates.length;
169
- expect(pastAfter).toBe(pastBeforeBend + 1);
170
- expect(useScene.getState().nodes[FENCE_ID].curveOffset).toBe(0.5);
171
- useScene.temporal.getState().undo();
172
- const after = useScene.getState().nodes[FENCE_ID];
173
- expect(after).toBeDefined();
174
- expect(after.curveOffset).toBe(0);
175
- });
176
- test('REAL bend (draft != original): one Ctrl-Z undoes only the bend', () => {
177
- useScene.getState().createNode(makeFence(0));
178
- const stateAfterCreate = useScene.getState().nodes[FENCE_ID];
179
- expect(stateAfterCreate.curveOffset).toBe(0);
180
- const scene = createSceneApi(useScene);
181
- scene.pauseHistory();
182
- // Simulate a real drag: capture original, mutate to non-zero.
183
- scene.update(FENCE_ID, { curveOffset: 0.5 });
184
- expect(useScene.getState().nodes[FENCE_ID].curveOffset).toBe(0.5);
185
- // Dance.
186
- scene.restoreAll();
187
- expect(useScene.getState().nodes[FENCE_ID].curveOffset).toBe(0);
188
- scene.resumeHistory();
189
- scene.update(FENCE_ID, { curveOffset: 0.5 });
190
- expect(useScene.getState().nodes[FENCE_ID].curveOffset).toBe(0.5);
191
- // First Ctrl-Z should undo the bend.
192
- useScene.temporal.getState().undo();
193
- const afterFirstUndo = useScene.getState().nodes[FENCE_ID];
194
- expect(afterFirstUndo).toBeDefined();
195
- expect(afterFirstUndo?.curveOffset).toBe(0);
196
- });
197
- test('a SECOND undo rolls the create step back', () => {
198
- useScene.getState().createNode(makeFence(0));
199
- const scene = createSceneApi(useScene);
200
- scene.pauseHistory();
201
- scene.update(FENCE_ID, { curveOffset: 0.5 });
202
- scene.restoreAll();
203
- scene.resumeHistory();
204
- scene.update(FENCE_ID, { curveOffset: 0.5 });
205
- useScene.temporal.getState().undo();
206
- expect(useScene.getState().nodes[FENCE_ID]).toBeDefined();
207
- useScene.temporal.getState().undo();
208
- expect(useScene.getState().nodes[FENCE_ID]).toBeUndefined();
209
- });
210
- });
@@ -1,2 +0,0 @@
1
- export {};
2
- //# sourceMappingURL=snap.test.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"snap.test.d.ts","sourceRoot":"","sources":["../../src/services/snap.test.ts"],"names":[],"mappings":""}
@@ -1,95 +0,0 @@
1
- import { describe, expect, test } from 'bun:test';
2
- import { DEFAULT_ANGLE_STEP, DEFAULT_GRID_STEP, snapAngleToList, snapPointToAngle, snapPointToGrid, snapScalar, snapServices, snapVec3ToGrid, } from './snap';
3
- describe('snapScalar', () => {
4
- test('rounds to multiples of step', () => {
5
- expect(snapScalar(0.27, 0.25)).toBe(0.25);
6
- expect(snapScalar(0.13, 0.25)).toBe(0.25);
7
- expect(snapScalar(0.12, 0.25)).toBe(0);
8
- expect(snapScalar(1.7, 0.25)).toBeCloseTo(1.75);
9
- });
10
- test('returns input unchanged when step is non-positive', () => {
11
- expect(snapScalar(0.42, 0)).toBe(0.42);
12
- expect(snapScalar(0.42, -1)).toBe(0.42);
13
- });
14
- test('default step is 0.25m', () => {
15
- expect(snapScalar(0.3)).toBe(0.25);
16
- expect(snapScalar(0.4)).toBe(0.5);
17
- expect(DEFAULT_GRID_STEP).toBe(0.25);
18
- });
19
- });
20
- describe('snapPointToGrid', () => {
21
- test('snaps both components independently', () => {
22
- expect(snapPointToGrid([0.3, 0.6], 0.25)).toEqual([0.25, 0.5]);
23
- });
24
- test('preserves exact-grid points', () => {
25
- expect(snapPointToGrid([1, 2], 0.5)).toEqual([1, 2]);
26
- });
27
- });
28
- describe('snapVec3ToGrid', () => {
29
- test('snaps X and Z, leaves Y untouched', () => {
30
- expect(snapVec3ToGrid([0.3, 1.7, 0.6], 0.25)).toEqual([0.25, 1.7, 0.5]);
31
- });
32
- });
33
- describe('snapPointToAngle', () => {
34
- test('snaps to axis (0°) when cursor is near horizontal', () => {
35
- const from = [0, 0];
36
- const cursor = [1, 0.05]; // near 0°
37
- const snapped = snapPointToAngle(from, cursor, Math.PI / 4);
38
- expect(snapped[0]).toBeCloseTo(1, 1);
39
- expect(snapped[1]).toBeCloseTo(0, 5);
40
- });
41
- test('snaps to 45° at π/4 step', () => {
42
- const from = [0, 0];
43
- const cursor = [1, 0.9]; // near 45°
44
- const snapped = snapPointToAngle(from, cursor, Math.PI / 4);
45
- // distance preserved (≈ √(1² + 0.9²) ≈ 1.345), angle locked to 45°
46
- const expectedDist = Math.hypot(1, 0.9);
47
- expect(snapped[0]).toBeCloseTo(expectedDist * Math.cos(Math.PI / 4));
48
- expect(snapped[1]).toBeCloseTo(expectedDist * Math.sin(Math.PI / 4));
49
- });
50
- test('default angle step is π/12 (15°)', () => {
51
- expect(DEFAULT_ANGLE_STEP).toBeCloseTo(Math.PI / 12);
52
- });
53
- test('grid-snaps the projected point when gridStep is provided', () => {
54
- const from = [0, 0];
55
- const cursor = [1.05, 0.02]; // ~horizontal, slightly off grid
56
- const snapped = snapPointToAngle(from, cursor, Math.PI / 4, 0.25);
57
- // After 0° lock + 0.25m grid, X must be a 0.25 multiple.
58
- expect(snapped[0] / 0.25).toBeCloseTo(Math.round(snapped[0] / 0.25));
59
- });
60
- test('preserves distance from `from`', () => {
61
- const from = [2, 3];
62
- const cursor = [3, 4];
63
- const distance = Math.hypot(1, 1);
64
- const snapped = snapPointToAngle(from, cursor, Math.PI / 4);
65
- expect(Math.hypot(snapped[0] - 2, snapped[1] - 3)).toBeCloseTo(distance);
66
- });
67
- });
68
- describe('snapAngleToList', () => {
69
- test('snaps to the nearest entry within tolerance', () => {
70
- const targets = [0, Math.PI / 2, Math.PI, (3 * Math.PI) / 2];
71
- expect(snapAngleToList(0.05, targets, Math.PI / 36)).toBe(0);
72
- expect(snapAngleToList(Math.PI / 2 + 0.02, targets, Math.PI / 36)).toBe(Math.PI / 2);
73
- });
74
- test('returns original angle when no target is within tolerance', () => {
75
- const targets = [0, Math.PI / 2];
76
- expect(snapAngleToList(0.5, targets, Math.PI / 36)).toBe(0.5);
77
- });
78
- test('handles wrap-around near ±π', () => {
79
- const targets = [Math.PI];
80
- expect(snapAngleToList(-Math.PI + 0.01, targets, Math.PI / 36)).toBe(Math.PI);
81
- });
82
- });
83
- describe('snapServices facade', () => {
84
- test('grid.snap matches snapPointToGrid', () => {
85
- expect(snapServices.grid.snap([0.3, 0.6], 0.25)).toEqual(snapPointToGrid([0.3, 0.6], 0.25));
86
- });
87
- test('grid.snapScalar matches snapScalar', () => {
88
- expect(snapServices.grid.snapScalar(0.3, 0.25)).toBe(snapScalar(0.3, 0.25));
89
- });
90
- test('angle.snapTo matches snapPointToAngle', () => {
91
- const from = [0, 0];
92
- const cursor = [1, 0.9];
93
- expect(snapServices.angle.snapTo(from, cursor, Math.PI / 4)).toEqual(snapPointToAngle(from, cursor, Math.PI / 4));
94
- });
95
- });
@@ -1,2 +0,0 @@
1
- export {};
2
- //# sourceMappingURL=node-mutation-sanitize.test.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"node-mutation-sanitize.test.d.ts","sourceRoot":"","sources":["../../../src/store/actions/node-mutation-sanitize.test.ts"],"names":[],"mappings":""}
@@ -1,147 +0,0 @@
1
- import { beforeEach, describe, expect, test } from 'bun:test';
2
- import useScene from '../use-scene';
3
- globalThis.requestAnimationFrame ??= ((cb) => {
4
- cb(0);
5
- return 0;
6
- });
7
- globalThis.cancelAnimationFrame ??=
8
- () => { };
9
- const SHELF_ID = 'shelf_sanitize';
10
- const SOLAR_PANEL_ID = 'sp_x';
11
- function makeShelf(overrides = {}) {
12
- return {
13
- id: SHELF_ID,
14
- type: 'shelf',
15
- parentId: null,
16
- object: 'node',
17
- visible: true,
18
- name: 'Shelf',
19
- metadata: {},
20
- children: [],
21
- position: [0, 0, 0],
22
- rotation: [0, 0, 0],
23
- width: 1.2,
24
- depth: 0.3,
25
- thickness: 0.04,
26
- height: 0.9,
27
- style: 'wall-shelf',
28
- rows: 1,
29
- columns: 1,
30
- withBack: false,
31
- withSides: true,
32
- withBottom: false,
33
- bracketStyle: 'minimal',
34
- ...overrides,
35
- };
36
- }
37
- function makeSolarPanel() {
38
- return {
39
- id: SOLAR_PANEL_ID,
40
- type: 'solar-panel',
41
- parentId: null,
42
- object: 'node',
43
- visible: true,
44
- name: 'Panel',
45
- metadata: {},
46
- position: [0, 0, 0],
47
- rotation: 0,
48
- rows: 2,
49
- columns: 3,
50
- panelWidth: 1,
51
- panelHeight: 1.65,
52
- gapX: 0.02,
53
- gapY: 0.02,
54
- mountingType: 'flush',
55
- tiltAngle: 15,
56
- standoffHeight: 0.05,
57
- frameThickness: 0.04,
58
- frameDepth: 0.04,
59
- };
60
- }
61
- function shelf() {
62
- return useScene.getState().nodes[SHELF_ID];
63
- }
64
- describe('node mutation numeric sanitization', () => {
65
- beforeEach(() => {
66
- useScene.setState({
67
- nodes: {
68
- [SHELF_ID]: makeShelf(),
69
- [SOLAR_PANEL_ID]: makeSolarPanel(),
70
- },
71
- rootNodeIds: [SHELF_ID, SOLAR_PANEL_ID],
72
- dirtyNodes: new Set(),
73
- collections: {},
74
- readOnly: false,
75
- });
76
- useScene.temporal.getState().clear();
77
- });
78
- test('drops NaN numeric updates while preserving other fields in the patch', () => {
79
- useScene.getState().updateNode(SHELF_ID, {
80
- thickness: Number.NaN,
81
- name: 'Renamed after NaN',
82
- });
83
- expect(shelf().thickness).toBe(0.04);
84
- expect(Number.isFinite(shelf().thickness)).toBe(true);
85
- expect(shelf().name).toBe('Renamed after NaN');
86
- });
87
- test('drops Infinity numeric updates while preserving later normal updates', () => {
88
- useScene.getState().updateNode(SHELF_ID, {
89
- width: Infinity,
90
- name: 'Renamed after Infinity',
91
- });
92
- expect(shelf().width).toBe(1.2);
93
- expect(Number.isFinite(shelf().width)).toBe(true);
94
- expect(shelf().name).toBe('Renamed after Infinity');
95
- useScene.getState().updateNode(SHELF_ID, {
96
- name: 'Clean rename',
97
- });
98
- expect(shelf().name).toBe('Clean rename');
99
- });
100
- test('clamps out-of-range numeric updates to the node schema bounds', () => {
101
- useScene.getState().updateNode(SHELF_ID, {
102
- width: 99,
103
- thickness: -1,
104
- });
105
- expect(shelf().width).toBe(3);
106
- expect(shelf().thickness).toBe(0.01);
107
- });
108
- test('preserves extra fields while sanitizing numeric updates', () => {
109
- useScene.setState({
110
- nodes: {
111
- [SHELF_ID]: {
112
- ...makeShelf(),
113
- legacyField: 'current',
114
- },
115
- },
116
- rootNodeIds: [SHELF_ID],
117
- });
118
- useScene.getState().updateNode(SHELF_ID, {
119
- width: Infinity,
120
- legacyPatch: 'patch',
121
- });
122
- const node = useScene.getState().nodes[SHELF_ID];
123
- expect(node.width).toBe(1.2);
124
- expect(node.legacyField).toBe('current');
125
- expect(node.legacyPatch).toBe('patch');
126
- });
127
- test('allows non-canonical ids to receive updates', () => {
128
- useScene.getState().updateNode(SOLAR_PANEL_ID, {
129
- name: 'Updated panel',
130
- });
131
- const panel = useScene.getState().nodes[SOLAR_PANEL_ID];
132
- expect(panel.name).toBe('Updated panel');
133
- });
134
- test('sanitizes non-finite numeric values during create', () => {
135
- const createdId = 'shelf_created';
136
- useScene.getState().createNode(makeShelf({
137
- id: createdId,
138
- width: Infinity,
139
- thickness: Number.NaN,
140
- }));
141
- const created = useScene.getState().nodes[createdId];
142
- expect(created.width).toBe(1.2);
143
- expect(created.thickness).toBe(0.04);
144
- expect(Number.isFinite(created.width)).toBe(true);
145
- expect(Number.isFinite(created.thickness)).toBe(true);
146
- });
147
- });
@@ -1,2 +0,0 @@
1
- export {};
2
- //# sourceMappingURL=reparent.test.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"reparent.test.d.ts","sourceRoot":"","sources":["../../../src/store/actions/reparent.test.ts"],"names":[],"mappings":""}
@@ -1,212 +0,0 @@
1
- import { beforeEach, describe, expect, test } from 'bun:test';
2
- import useScene from '../use-scene';
3
- globalThis.requestAnimationFrame ??= ((cb) => {
4
- cb(0);
5
- return 0;
6
- });
7
- globalThis.cancelAnimationFrame ??=
8
- () => { };
9
- const ROOF_ID = 'roof_test';
10
- const SEG_A_ID = 'rseg_a';
11
- const SEG_B_ID = 'rseg_b';
12
- const VENT_ID = 'bvent_v1';
13
- function makeRoof() {
14
- return {
15
- id: ROOF_ID,
16
- type: 'roof',
17
- parentId: null,
18
- object: 'node',
19
- visible: true,
20
- name: '',
21
- metadata: {},
22
- position: [0, 0, 0],
23
- rotation: 0,
24
- children: [SEG_A_ID, SEG_B_ID],
25
- };
26
- }
27
- function makeSegment(id, children = []) {
28
- return {
29
- id,
30
- type: 'roof-segment',
31
- parentId: ROOF_ID,
32
- object: 'node',
33
- visible: true,
34
- name: '',
35
- metadata: {},
36
- position: [0, 0, 0],
37
- rotation: 0,
38
- roofType: 'gable',
39
- width: 8,
40
- depth: 6,
41
- wallHeight: 0.5,
42
- pitch: 40,
43
- wallThickness: 0.1,
44
- deckThickness: 0.1,
45
- overhang: 0.3,
46
- shingleThickness: 0.05,
47
- children,
48
- };
49
- }
50
- function makeVent(parentId) {
51
- return {
52
- id: VENT_ID,
53
- type: 'box-vent',
54
- parentId,
55
- roofSegmentId: parentId,
56
- object: 'node',
57
- visible: true,
58
- name: '',
59
- metadata: {},
60
- position: [0, 0, 0],
61
- rotation: 0,
62
- width: 0.4,
63
- depth: 0.4,
64
- height: 0.15,
65
- hoodOverhang: 0.04,
66
- topTaper: 0.4,
67
- capHeight: 0.07,
68
- capGap: 0,
69
- domeCurvature: 0.65,
70
- baseInset: 0.06,
71
- baseHeight: 0.04,
72
- cornerBevel: 0.012,
73
- style: 'cap',
74
- materialPreset: 'preset-white',
75
- };
76
- }
77
- function childrenOf(id) {
78
- const seg = useScene.getState().nodes[id];
79
- return (seg?.children ?? []);
80
- }
81
- function vent() {
82
- return useScene.getState().nodes[VENT_ID];
83
- }
84
- describe('node-actions reparent — repeated segment-hopping', () => {
85
- beforeEach(() => {
86
- useScene.setState({
87
- nodes: {
88
- [ROOF_ID]: makeRoof(),
89
- [SEG_A_ID]: makeSegment(SEG_A_ID, [VENT_ID]),
90
- [SEG_B_ID]: makeSegment(SEG_B_ID, []),
91
- [VENT_ID]: makeVent(SEG_A_ID),
92
- },
93
- rootNodeIds: [ROOF_ID],
94
- });
95
- useScene.temporal.getState().clear();
96
- });
97
- test('initial state: vent listed once in A.children, absent from B.children', () => {
98
- expect(childrenOf(SEG_A_ID)).toEqual([VENT_ID]);
99
- expect(childrenOf(SEG_B_ID)).toEqual([]);
100
- expect(vent().parentId).toBe(SEG_A_ID);
101
- });
102
- test('A→B: auto-reparent moves vent without leaving duplicates', () => {
103
- useScene
104
- .getState()
105
- .updateNode(VENT_ID, { parentId: SEG_B_ID, roofSegmentId: SEG_B_ID });
106
- expect(childrenOf(SEG_A_ID)).toEqual([]);
107
- expect(childrenOf(SEG_B_ID)).toEqual([VENT_ID]);
108
- expect(vent().parentId).toBe(SEG_B_ID);
109
- });
110
- test('A→B→A→B: three hops leave children lists clean (regression: vent crash)', () => {
111
- // Move 1: A → B
112
- useScene
113
- .getState()
114
- .updateNode(VENT_ID, { parentId: SEG_B_ID, roofSegmentId: SEG_B_ID });
115
- expect(childrenOf(SEG_A_ID)).toEqual([]);
116
- expect(childrenOf(SEG_B_ID)).toEqual([VENT_ID]);
117
- // Move 2: B → A
118
- useScene
119
- .getState()
120
- .updateNode(VENT_ID, { parentId: SEG_A_ID, roofSegmentId: SEG_A_ID });
121
- expect(childrenOf(SEG_A_ID)).toEqual([VENT_ID]);
122
- expect(childrenOf(SEG_B_ID)).toEqual([]);
123
- // Move 3: A → B (the move the user reports crashes)
124
- useScene
125
- .getState()
126
- .updateNode(VENT_ID, { parentId: SEG_B_ID, roofSegmentId: SEG_B_ID });
127
- expect(childrenOf(SEG_A_ID)).toEqual([]);
128
- expect(childrenOf(SEG_B_ID)).toEqual([VENT_ID]);
129
- expect(vent().parentId).toBe(SEG_B_ID);
130
- // Crucially: the vent must appear in EXACTLY ONE segment's children.
131
- // Duplicate listing is what makes the roof renderer mount two
132
- // <NodeRenderer key={ventId}> instances and crash the scene-registry
133
- // on unmount.
134
- const totalListings = childrenOf(SEG_A_ID).filter((id) => id === VENT_ID).length +
135
- childrenOf(SEG_B_ID).filter((id) => id === VENT_ID).length;
136
- expect(totalListings).toBe(1);
137
- });
138
- test.each([
139
- ['chimney', 'chmn_x'],
140
- ['skylight', 'sky_x'],
141
- ['dormer', 'dorm_x'],
142
- ['solar-panel', 'sp_x'],
143
- ['ridge-vent', 'rvent_x'],
144
- ])('A→B→A→B for %s: chimney-style auto-reparent leaves children clean', (type, idStr) => {
145
- const id = idStr;
146
- useScene.setState({
147
- nodes: {
148
- [ROOF_ID]: makeRoof(),
149
- [SEG_A_ID]: makeSegment(SEG_A_ID, [id]),
150
- [SEG_B_ID]: makeSegment(SEG_B_ID, []),
151
- [id]: {
152
- id,
153
- type,
154
- parentId: SEG_A_ID,
155
- roofSegmentId: SEG_A_ID,
156
- object: 'node',
157
- visible: true,
158
- name: '',
159
- metadata: {},
160
- position: [0, 0, 0],
161
- rotation: 0,
162
- },
163
- },
164
- rootNodeIds: [ROOF_ID],
165
- });
166
- const hop = (to) => useScene.getState().updateNode(id, { parentId: to, roofSegmentId: to });
167
- hop(SEG_B_ID);
168
- hop(SEG_A_ID);
169
- hop(SEG_B_ID);
170
- expect(childrenOf(SEG_A_ID)).toEqual([]);
171
- expect(childrenOf(SEG_B_ID)).toEqual([id]);
172
- const node = useScene.getState().nodes[id];
173
- expect(node.parentId).toBe(SEG_B_ID);
174
- });
175
- test('A→B→A→B with redundant manual children edits (vent move-tool pattern, pre-fix)', () => {
176
- // Reproduces the box-vent / ridge-vent move-tool order: manual
177
- // children edits FIRST, then updateNode({parentId}) which triggers
178
- // auto-reparent. The dual handling must not leave the vent listed
179
- // in both segments after three hops.
180
- const hop = (from, to) => {
181
- const st = useScene.getState();
182
- // 1. Manual: filter old children, push to new children — using
183
- // the snapshot (st.nodes) captured before any updates.
184
- const oldSeg = st.nodes[from];
185
- if (oldSeg) {
186
- st.updateNode(from, {
187
- children: (oldSeg.children ?? []).filter((id) => id !== VENT_ID),
188
- });
189
- }
190
- const newSeg = st.nodes[to];
191
- if (newSeg && !(newSeg.children ?? []).includes(VENT_ID)) {
192
- st.updateNode(to, {
193
- children: [...(newSeg.children ?? []), VENT_ID],
194
- });
195
- }
196
- // 2. Then the auto-reparent path via parentId.
197
- st.updateNode(VENT_ID, {
198
- parentId: to,
199
- roofSegmentId: to,
200
- });
201
- };
202
- hop(SEG_A_ID, SEG_B_ID);
203
- hop(SEG_B_ID, SEG_A_ID);
204
- hop(SEG_A_ID, SEG_B_ID);
205
- expect(childrenOf(SEG_A_ID)).toEqual([]);
206
- expect(childrenOf(SEG_B_ID)).toEqual([VENT_ID]);
207
- expect(vent().parentId).toBe(SEG_B_ID);
208
- const totalListings = childrenOf(SEG_A_ID).filter((id) => id === VENT_ID).length +
209
- childrenOf(SEG_B_ID).filter((id) => id === VENT_ID).length;
210
- expect(totalListings).toBe(1);
211
- });
212
- });