@pascal-app/editor 0.8.0 → 0.9.2

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 (350) hide show
  1. package/package.json +15 -13
  2. package/src/components/editor/alignment-3d-guide-layer.tsx +168 -0
  3. package/src/components/editor/bake-exporter.tsx +47 -0
  4. package/src/components/editor/custom-camera-controls.tsx +795 -28
  5. package/src/components/editor/editor-layout-mobile.tsx +8 -3
  6. package/src/components/editor/editor-layout-v2.tsx +76 -51
  7. package/src/components/editor/export-manager.tsx +69 -32
  8. package/src/components/editor/fence-tangent-lines-3d.tsx +87 -0
  9. package/src/components/editor/first-person/build-collider-world.test.ts +182 -0
  10. package/src/components/editor/first-person/build-collider-world.ts +228 -83
  11. package/src/components/editor/first-person-controls.tsx +943 -27
  12. package/src/components/editor/floating-action-menu.tsx +676 -208
  13. package/src/components/editor/floating-building-action-menu.tsx +2 -2
  14. package/src/components/editor/floorplan-background-selection.test.ts +58 -0
  15. package/src/components/editor/floorplan-background-selection.ts +3 -2
  16. package/src/components/editor/floorplan-panel.tsx +6916 -12137
  17. package/src/components/editor/grid.tsx +197 -36
  18. package/src/components/editor/group-actions.ts +457 -0
  19. package/src/components/editor/group-floating-action-menu.tsx +130 -0
  20. package/src/components/editor/group-move-3d.ts +397 -0
  21. package/src/components/editor/group-rotate-handle.tsx +420 -0
  22. package/src/components/editor/group-selection-box-3d.tsx +173 -0
  23. package/src/components/editor/group-transform-shared.test.ts +396 -0
  24. package/src/components/editor/group-transform-shared.ts +489 -0
  25. package/src/components/editor/handles/handle-arrow.tsx +581 -0
  26. package/src/components/editor/handles/use-handle-drag.ts +246 -0
  27. package/src/components/editor/index.tsx +412 -162
  28. package/src/components/editor/measurement-pill.tsx +115 -0
  29. package/src/components/editor/node-action-menu.tsx +14 -1
  30. package/src/components/editor/node-arrow-handles.tsx +1646 -0
  31. package/src/components/editor/opening-guides-3d-layer.tsx +144 -0
  32. package/src/components/editor/quick-measurement-card.tsx +70 -0
  33. package/src/components/editor/quick-measurement-hud.tsx +28 -0
  34. package/src/components/editor/riser-diagram-panel.tsx +137 -0
  35. package/src/components/editor/selection-manager.tsx +1106 -483
  36. package/src/components/editor/site-edge-labels.tsx +45 -21
  37. package/src/components/editor/slab-hole-highlights.tsx +474 -0
  38. package/src/components/editor/snapshot-capture-overlay.tsx +369 -155
  39. package/src/components/editor/three-context-bridge.ts +22 -0
  40. package/src/components/editor/thumbnail-generator.tsx +140 -41
  41. package/src/components/editor/use-floorplan-background-placement.ts +190 -35
  42. package/src/components/editor/use-floorplan-hit-testing.ts +24 -0
  43. package/src/components/editor/use-floorplan-scene-data.ts +19 -7
  44. package/src/components/editor/use-mesh-settle-epoch.ts +26 -0
  45. package/src/components/editor/wall-measurement-label.tsx +14 -19
  46. package/src/components/editor/wall-move-side-handles.tsx +870 -0
  47. package/src/components/editor/wall-opening-highlights.tsx +181 -0
  48. package/src/components/editor/wall-snap-beacon-layer.tsx +304 -0
  49. package/src/components/editor-2d/floorplan-action-menu-layer.tsx +15 -6
  50. package/src/components/editor-2d/floorplan-alignment-guide-layer.tsx +142 -0
  51. package/src/components/editor-2d/floorplan-cursor-indicator-overlay.tsx +5 -3
  52. package/src/components/editor-2d/floorplan-group-action-menu.tsx +87 -0
  53. package/src/components/editor-2d/floorplan-group-move.tsx +701 -0
  54. package/src/components/editor-2d/floorplan-measurement-tool-layer.test.ts +113 -0
  55. package/src/components/editor-2d/floorplan-measurement-tool-layer.tsx +1656 -0
  56. package/src/components/editor-2d/floorplan-quick-measure-layer.tsx +212 -0
  57. package/src/components/editor-2d/floorplan-registry-action-menu.tsx +392 -0
  58. package/src/components/editor-2d/floorplan-registry-move-overlay.tsx +857 -0
  59. package/src/components/editor-2d/floorplan-render-context.tsx +62 -0
  60. package/src/components/editor-2d/floorplan-snap-beacon-layer.tsx +77 -0
  61. package/src/components/editor-2d/floorplan-wall-move-ghost-layer.tsx +47 -0
  62. package/src/components/editor-2d/renderers/floorplan-geometry-renderer.tsx +266 -0
  63. package/src/components/editor-2d/renderers/floorplan-label-angle.test.ts +15 -0
  64. package/src/components/editor-2d/renderers/floorplan-label-angle.ts +14 -0
  65. package/src/components/editor-2d/renderers/floorplan-measurements-layer.tsx +108 -66
  66. package/src/components/editor-2d/renderers/floorplan-placement-preview-layer.tsx +62 -0
  67. package/src/components/editor-2d/renderers/floorplan-registry-layer.test.ts +308 -0
  68. package/src/components/editor-2d/renderers/floorplan-registry-layer.tsx +3325 -0
  69. package/src/components/editor-2d/renderers/floorplan-stair-layer.tsx +24 -9
  70. package/src/components/editor-2d/renderers/floorplan-voronoi-layer.tsx +128 -0
  71. package/src/components/editor-2d/svg-paths.ts +10 -2
  72. package/src/components/systems/ceiling/ceiling-selection-affordance-system.tsx +439 -57
  73. package/src/components/systems/ceiling/ceiling-system.tsx +109 -12
  74. package/src/components/systems/roof/roof-edit-system.tsx +1851 -22
  75. package/src/components/systems/selection-affordance-manager.tsx +38 -0
  76. package/src/components/systems/stair/stair-edit-system.tsx +1 -1
  77. package/src/components/systems/zone/zone-label-editor-system.tsx +14 -1
  78. package/src/components/systems/zone/zone-system.tsx +42 -13
  79. package/src/components/tools/elevator/elevator-defaults.ts +8 -0
  80. package/src/components/tools/elevator/elevator-tool.tsx +375 -0
  81. package/src/components/tools/elevator/move-elevator-tool.tsx +263 -0
  82. package/src/components/tools/fence/fence-drafting.ts +141 -27
  83. package/src/components/tools/item/move-tool.tsx +45 -105
  84. package/src/components/tools/item/placement-math.test.ts +22 -0
  85. package/src/components/tools/item/placement-math.ts +59 -7
  86. package/src/components/tools/item/placement-strategies.ts +432 -16
  87. package/src/components/tools/item/placement-types.ts +22 -1
  88. package/src/components/tools/item/use-draft-node.ts +51 -2
  89. package/src/components/tools/item/use-placement-coordinator.tsx +1399 -256
  90. package/src/components/tools/registry/move-registry-node-tool.tsx +1064 -0
  91. package/src/components/tools/roof/roof-tool.tsx +350 -28
  92. package/src/components/tools/select/box-select-state.ts +88 -0
  93. package/src/components/tools/select/box-select-tool.tsx +442 -520
  94. package/src/components/tools/select/marquee-geometry.test.ts +161 -0
  95. package/src/components/tools/select/marquee-geometry.ts +155 -0
  96. package/src/components/tools/select/plane-box-select-tool.tsx +570 -0
  97. package/src/components/tools/select/screen-rectangle-selection.ts +84 -0
  98. package/src/components/tools/select/select-candidates.test.ts +109 -0
  99. package/src/components/tools/select/select-candidates.ts +85 -0
  100. package/src/components/tools/shared/affordance-dispatch.ts +30 -0
  101. package/src/components/tools/shared/cursor-sphere.tsx +62 -26
  102. package/src/components/tools/shared/drag-bounding-box.tsx +195 -0
  103. package/src/components/tools/shared/facing-indicator.tsx +87 -0
  104. package/src/components/tools/shared/facing-pose-indicator.tsx +54 -0
  105. package/src/components/tools/shared/floor-stack-preview.ts +25 -0
  106. package/src/components/tools/shared/fresh-placement-visibility.ts +61 -0
  107. package/src/components/tools/shared/placement-box-geometry.ts +123 -0
  108. package/src/components/tools/shared/placement-box.tsx +305 -0
  109. package/src/components/tools/shared/polygon-editor.tsx +866 -175
  110. package/src/components/tools/shared/segment-angle.ts +56 -0
  111. package/src/components/tools/site/site-boundary-editor.tsx +520 -16
  112. package/src/components/tools/site/site-flag-model.ts +18 -0
  113. package/src/components/tools/stair/stair-defaults.ts +2 -1
  114. package/src/components/tools/stair/stair-tool.tsx +342 -37
  115. package/src/components/tools/tool-manager.tsx +239 -62
  116. package/src/components/tools/wall/wall-drafting.test.ts +330 -0
  117. package/src/components/tools/wall/wall-drafting.ts +248 -171
  118. package/src/components/tools/wall/wall-snap-geometry.test.ts +123 -0
  119. package/src/components/tools/wall/wall-snap-geometry.ts +331 -0
  120. package/src/components/tools/zone/zone-boundary-editor.tsx +5 -1
  121. package/src/components/tools/zone/zone-tool.tsx +86 -82
  122. package/src/components/ui/action-menu/action-button.tsx +21 -1
  123. package/src/components/ui/action-menu/camera-actions.tsx +24 -17
  124. package/src/components/ui/action-menu/control-modes.tsx +40 -170
  125. package/src/components/ui/action-menu/furnish-tools.tsx +5 -5
  126. package/src/components/ui/action-menu/index.tsx +3 -117
  127. package/src/components/ui/action-menu/measurement-control.tsx +188 -0
  128. package/src/components/ui/action-menu/structure-tools.tsx +27 -93
  129. package/src/components/ui/action-menu/view-toggles.tsx +279 -90
  130. package/src/components/ui/command-palette/editor-commands.tsx +20 -15
  131. package/src/components/ui/command-palette/index.tsx +5 -3
  132. package/src/components/ui/controls/material-paint-panel.tsx +152 -0
  133. package/src/components/ui/controls/material-picker.tsx +87 -177
  134. package/src/components/ui/controls/material-properties-editor.tsx +108 -0
  135. package/src/components/ui/controls/metric-control.tsx +142 -46
  136. package/src/components/ui/controls/scene-material-list.tsx +247 -0
  137. package/src/components/ui/controls/slider-control.tsx +88 -38
  138. package/src/components/ui/floating-level-selector.tsx +47 -10
  139. package/src/components/ui/helpers/building-helper.tsx +10 -23
  140. package/src/components/ui/helpers/contextual-helper-panel.tsx +428 -0
  141. package/src/components/ui/helpers/helper-manager.tsx +235 -23
  142. package/src/components/ui/helpers/item-helper.tsx +28 -32
  143. package/src/components/ui/helpers/registered-tool-helper.tsx +59 -0
  144. package/src/components/ui/helpers/roof-helper.tsx +10 -12
  145. package/src/components/ui/icon-ref.tsx +47 -0
  146. package/src/components/ui/item-catalog/catalog-items.tsx +40 -1
  147. package/src/components/ui/item-catalog/item-catalog.tsx +16 -36
  148. package/src/components/ui/level-duplicate-dialog.tsx +2 -1
  149. package/src/components/ui/panels/node-display.ts +17 -16
  150. package/src/components/ui/panels/panel-manager.tsx +55 -58
  151. package/src/components/ui/panels/panel-wrapper.tsx +238 -12
  152. package/src/components/ui/panels/parametric-inspector.tsx +482 -0
  153. package/src/components/ui/panels/reference-panel.tsx +54 -19
  154. package/src/components/ui/primitives/dropdown-menu.tsx +12 -8
  155. package/src/components/ui/primitives/shortcut-token.tsx +39 -3
  156. package/src/components/ui/primitives/sidebar.tsx +1 -0
  157. package/src/components/ui/scene-loader.tsx +1 -3
  158. package/src/components/ui/sidebar/app-sidebar.tsx +5 -1
  159. package/src/components/ui/sidebar/icon-rail.tsx +50 -31
  160. package/src/components/ui/sidebar/panels/items-panel/function-tree-panel.tsx +263 -0
  161. package/src/components/ui/sidebar/panels/items-panel/index.tsx +82 -8
  162. package/src/components/ui/sidebar/panels/plugins-panel.tsx +218 -0
  163. package/src/components/ui/sidebar/panels/settings-panel/index.tsx +122 -41
  164. package/src/components/ui/sidebar/panels/settings-panel/keyboard-shortcuts-dialog.tsx +66 -7
  165. package/src/components/ui/sidebar/panels/settings-panel/load-build-dialog.tsx +267 -0
  166. package/src/components/ui/sidebar/panels/site-panel/building-tree-node.tsx +3 -1
  167. package/src/components/ui/sidebar/panels/site-panel/ceiling-tree-node.tsx +9 -5
  168. package/src/components/ui/sidebar/panels/site-panel/chimney-tree-node.tsx +86 -0
  169. package/src/components/ui/sidebar/panels/site-panel/column-tree-node.tsx +1 -1
  170. package/src/components/ui/sidebar/panels/site-panel/door-tree-node.tsx +7 -2
  171. package/src/components/ui/sidebar/panels/site-panel/dormer-tree-node.tsx +92 -0
  172. package/src/components/ui/sidebar/panels/site-panel/elevator-tree-node.tsx +75 -0
  173. package/src/components/ui/sidebar/panels/site-panel/fence-tree-node.tsx +3 -3
  174. package/src/components/ui/sidebar/panels/site-panel/gutter-tree-node.tsx +86 -0
  175. package/src/components/ui/sidebar/panels/site-panel/index.tsx +42 -21
  176. package/src/components/ui/sidebar/panels/site-panel/item-tree-node.tsx +31 -17
  177. package/src/components/ui/sidebar/panels/site-panel/level-tree-node.tsx +2 -1
  178. package/src/components/ui/sidebar/panels/site-panel/registry-tree-node.tsx +140 -0
  179. package/src/components/ui/sidebar/panels/site-panel/roof-tree-node.tsx +48 -8
  180. package/src/components/ui/sidebar/panels/site-panel/shelf-tree-node.tsx +132 -0
  181. package/src/components/ui/sidebar/panels/site-panel/slab-tree-node.tsx +9 -5
  182. package/src/components/ui/sidebar/panels/site-panel/solar-panel-tree-node.tsx +86 -0
  183. package/src/components/ui/sidebar/panels/site-panel/spawn-tree-node.tsx +7 -1
  184. package/src/components/ui/sidebar/panels/site-panel/stair-tree-node.tsx +2 -2
  185. package/src/components/ui/sidebar/panels/site-panel/tree-node.tsx +112 -39
  186. package/src/components/ui/sidebar/panels/site-panel/tree-structure.ts +36 -0
  187. package/src/components/ui/sidebar/panels/site-panel/wall-tree-node.tsx +1 -1
  188. package/src/components/ui/sidebar/panels/site-panel/window-tree-node.tsx +7 -2
  189. package/src/components/ui/sidebar/panels/site-panel/zone-tree-node.tsx +3 -3
  190. package/src/components/ui/sidebar/panels/zone-panel/index.tsx +51 -2
  191. package/src/components/ui/sidebar/tab-bar.tsx +75 -1
  192. package/src/components/ui/sidebar/use-plugin-panels.tsx +132 -0
  193. package/src/components/ui/snap-target-badge.test.tsx +40 -0
  194. package/src/components/ui/snap-target-badge.tsx +88 -0
  195. package/src/components/viewer-overlay.tsx +253 -61
  196. package/src/components/viewer-zone-system.tsx +7 -2
  197. package/src/hooks/use-auto-save.test.ts +14 -0
  198. package/src/hooks/use-auto-save.ts +64 -10
  199. package/src/hooks/use-ceiling-events.ts +176 -0
  200. package/src/hooks/use-drag-action.ts +127 -0
  201. package/src/hooks/use-keyboard.ts +493 -95
  202. package/src/hooks/use-selection.ts +64 -0
  203. package/src/index.tsx +451 -6
  204. package/src/lib/active-placement-surface.ts +35 -0
  205. package/src/lib/ceiling-plan-snap.ts +24 -0
  206. package/src/lib/constants.ts +6 -2
  207. package/src/lib/contextual-help.test.ts +112 -0
  208. package/src/lib/contextual-help.ts +144 -0
  209. package/src/lib/continuation.ts +64 -0
  210. package/src/lib/direct-manipulation.test.ts +192 -0
  211. package/src/lib/direct-manipulation.ts +109 -0
  212. package/src/lib/editor-api.ts +50 -0
  213. package/src/lib/elevator-support.ts +96 -0
  214. package/src/lib/floorplan/apply-alignment.test.ts +25 -0
  215. package/src/lib/floorplan/apply-alignment.ts +117 -0
  216. package/src/lib/floorplan/floorplan-export.tsx +364 -0
  217. package/src/lib/floorplan/geometry.ts +53 -0
  218. package/src/lib/floorplan/index.ts +10 -0
  219. package/src/lib/floorplan/items.ts +5 -2
  220. package/src/lib/floorplan/plan-coords.ts +21 -0
  221. package/src/lib/floorplan/selection-tool.ts +40 -0
  222. package/src/lib/floorplan/stairs.ts +12 -1
  223. package/src/lib/fresh-planar-placement.test.ts +339 -0
  224. package/src/lib/fresh-planar-placement.ts +130 -0
  225. package/src/lib/glb-export.test.ts +441 -0
  226. package/src/lib/glb-export.ts +874 -0
  227. package/src/lib/history.ts +11 -1
  228. package/src/lib/interaction/hot-set.test.ts +133 -0
  229. package/src/lib/interaction/hot-set.ts +67 -0
  230. package/src/lib/interaction/overlay-policy.test.ts +51 -0
  231. package/src/lib/interaction/overlay-policy.ts +59 -0
  232. package/src/lib/interaction/scope.ts +211 -0
  233. package/src/lib/level-duplication.ts +54 -42
  234. package/src/lib/level-selection.ts +2 -2
  235. package/src/lib/material-paint.ts +227 -48
  236. package/src/lib/measurement-kind.test.ts +30 -0
  237. package/src/lib/measurement-kind.ts +19 -0
  238. package/src/lib/measurement-label.test.ts +47 -0
  239. package/src/lib/measurement-label.ts +68 -0
  240. package/src/lib/measurement-parser.ts +116 -0
  241. package/src/lib/measurements.test.ts +183 -0
  242. package/src/lib/measurements.ts +199 -0
  243. package/src/lib/paint-scope.test.ts +454 -0
  244. package/src/lib/paint-scope.ts +461 -0
  245. package/src/lib/placement-drag-release.ts +23 -0
  246. package/src/lib/placement-metadata.ts +29 -0
  247. package/src/lib/planar-cursor-placement.test.ts +100 -0
  248. package/src/lib/planar-cursor-placement.ts +42 -0
  249. package/src/lib/plugin-panels.test.ts +19 -0
  250. package/src/lib/plugin-panels.ts +91 -0
  251. package/src/lib/quick-measurement.test.ts +77 -0
  252. package/src/lib/quick-measurement.ts +109 -0
  253. package/src/lib/roof-duplication.ts +7 -7
  254. package/src/lib/roof-hover-outline-proxy.ts +22 -0
  255. package/src/lib/roof-wall-hit.ts +164 -0
  256. package/src/lib/scene-clipboard.test.ts +196 -0
  257. package/src/lib/scene-clipboard.ts +332 -0
  258. package/src/lib/scene.ts +40 -8
  259. package/src/lib/selection-routing.test.ts +298 -0
  260. package/src/lib/selection-routing.ts +175 -0
  261. package/src/lib/sfx/index.ts +1 -0
  262. package/src/lib/sfx/movement-tick.test.ts +65 -0
  263. package/src/lib/sfx/movement-tick.ts +18 -0
  264. package/src/lib/sfx-bus.ts +69 -10
  265. package/src/lib/sfx-player.test.ts +124 -0
  266. package/src/lib/sfx-player.ts +148 -55
  267. package/src/lib/site-boundary.ts +1 -0
  268. package/src/lib/slab-plan-snap.test.ts +93 -0
  269. package/src/lib/slab-plan-snap.ts +108 -0
  270. package/src/lib/snapping-mode.test.ts +138 -0
  271. package/src/lib/snapping-mode.ts +179 -0
  272. package/src/lib/stair-duplication.ts +4 -4
  273. package/src/lib/stair-levels.test.ts +146 -0
  274. package/src/lib/stair-levels.ts +177 -0
  275. package/src/lib/surface-plan-snap.test.ts +71 -0
  276. package/src/lib/surface-plan-snap.ts +256 -0
  277. package/src/lib/use-linear-display.ts +40 -0
  278. package/src/lib/world-grid-snap.ts +231 -0
  279. package/src/lib/zone-content.ts +130 -0
  280. package/src/store/use-alignment-guides.ts +21 -0
  281. package/src/store/use-direct-manipulation-feedback.ts +20 -0
  282. package/src/store/use-editor.tsx +767 -114
  283. package/src/store/use-facing-pose.ts +44 -0
  284. package/src/store/use-fence-curve-draft.ts +21 -0
  285. package/src/store/use-floorplan-draft-preview.ts +101 -0
  286. package/src/store/use-floorplan-marquee.ts +50 -0
  287. package/src/store/use-interaction-scope.test.ts +186 -0
  288. package/src/store/use-interaction-scope.ts +132 -0
  289. package/src/store/use-measurement-draft.test.ts +530 -0
  290. package/src/store/use-measurement-draft.ts +543 -0
  291. package/src/store/use-opening-guides.ts +41 -0
  292. package/src/store/use-placement-preview.ts +39 -0
  293. package/src/store/use-quick-measurement-hud.test.ts +53 -0
  294. package/src/store/use-quick-measurement-hud.ts +77 -0
  295. package/src/store/use-segment-draft-chain.ts +28 -0
  296. package/src/store/use-stair-build-preview.ts +43 -0
  297. package/src/store/use-wall-move-ghosts.ts +36 -0
  298. package/src/store/use-wall-snap-indicator.ts +34 -0
  299. package/src/components/editor/first-person/bvh-ecctrl.tsx +0 -860
  300. package/src/components/editor/preset-thumbnail-generator.tsx +0 -125
  301. package/src/components/editor-2d/renderers/floorplan-roof-layer.tsx +0 -113
  302. package/src/components/tools/building/move-building-tool.tsx +0 -157
  303. package/src/components/tools/ceiling/ceiling-boundary-editor.tsx +0 -43
  304. package/src/components/tools/ceiling/ceiling-hole-editor.tsx +0 -49
  305. package/src/components/tools/ceiling/ceiling-tool.tsx +0 -465
  306. package/src/components/tools/ceiling/move-ceiling-tool.tsx +0 -264
  307. package/src/components/tools/column/column-tool.tsx +0 -97
  308. package/src/components/tools/column/move-column-tool.tsx +0 -105
  309. package/src/components/tools/door/door-math.ts +0 -110
  310. package/src/components/tools/door/door-tool.tsx +0 -324
  311. package/src/components/tools/door/move-door-tool.tsx +0 -412
  312. package/src/components/tools/fence/curve-fence-tool.tsx +0 -178
  313. package/src/components/tools/fence/fence-tool.tsx +0 -346
  314. package/src/components/tools/fence/move-fence-endpoint-tool.tsx +0 -441
  315. package/src/components/tools/fence/move-fence-tool.tsx +0 -302
  316. package/src/components/tools/item/item-tool.tsx +0 -26
  317. package/src/components/tools/roof/move-roof-tool.tsx +0 -364
  318. package/src/components/tools/slab/move-slab-tool.tsx +0 -182
  319. package/src/components/tools/slab/slab-boundary-editor.tsx +0 -43
  320. package/src/components/tools/slab/slab-hole-editor.tsx +0 -49
  321. package/src/components/tools/slab/slab-tool.tsx +0 -322
  322. package/src/components/tools/spawn/move-spawn-tool.tsx +0 -101
  323. package/src/components/tools/spawn/spawn-tool.tsx +0 -130
  324. package/src/components/tools/wall/curve-wall-tool.tsx +0 -178
  325. package/src/components/tools/wall/move-wall-endpoint-tool.tsx +0 -426
  326. package/src/components/tools/wall/move-wall-tool.tsx +0 -358
  327. package/src/components/tools/wall/wall-tool.tsx +0 -332
  328. package/src/components/tools/window/move-window-tool.tsx +0 -447
  329. package/src/components/tools/window/window-math.ts +0 -117
  330. package/src/components/tools/window/window-tool.tsx +0 -332
  331. package/src/components/ui/helpers/ceiling-helper.tsx +0 -20
  332. package/src/components/ui/helpers/slab-helper.tsx +0 -20
  333. package/src/components/ui/helpers/wall-helper.tsx +0 -20
  334. package/src/components/ui/panels/ceiling-panel.tsx +0 -249
  335. package/src/components/ui/panels/column-panel.tsx +0 -759
  336. package/src/components/ui/panels/door-panel.tsx +0 -1299
  337. package/src/components/ui/panels/fence-panel.tsx +0 -222
  338. package/src/components/ui/panels/item-panel.tsx +0 -306
  339. package/src/components/ui/panels/paint-panel.tsx +0 -163
  340. package/src/components/ui/panels/presets/presets-popover.tsx +0 -511
  341. package/src/components/ui/panels/roof-panel.tsx +0 -280
  342. package/src/components/ui/panels/roof-segment-panel.tsx +0 -311
  343. package/src/components/ui/panels/slab-panel.tsx +0 -250
  344. package/src/components/ui/panels/spawn-panel.tsx +0 -161
  345. package/src/components/ui/panels/stair-panel.tsx +0 -577
  346. package/src/components/ui/panels/stair-segment-panel.tsx +0 -325
  347. package/src/components/ui/panels/wall-panel.tsx +0 -273
  348. package/src/components/ui/panels/window-panel.tsx +0 -968
  349. package/src/contexts/presets-context.tsx +0 -121
  350. package/src/hooks/use-contextual-tools.ts +0 -61
@@ -12,6 +12,8 @@ const FLOORPLAN_MEASUREMENT_END_TICK = 0.18
12
12
  export type LinearMeasurementOverlay = {
13
13
  dashedExtensions?: boolean
14
14
  id: string
15
+ dimensionPathEnd?: string | null
16
+ dimensionPathStart?: string | null
15
17
  dimensionLineEnd: { x1: number; y1: number; x2: number; y2: number }
16
18
  dimensionLineStart: { x1: number; y1: number; x2: number; y2: number }
17
19
  extensionStart: { x1: number; y1: number; x2: number; y2: number }
@@ -34,6 +36,7 @@ type FloorplanMeasurementPalette = {
34
36
  type FloorplanMeasurementLineProps = {
35
37
  palette: FloorplanMeasurementPalette
36
38
  segment: { x1: number; y1: number; x2: number; y2: number }
39
+ path?: string | null
37
40
  isSelected?: boolean
38
41
  dashed?: boolean
39
42
  stroke?: string
@@ -42,6 +45,7 @@ type FloorplanMeasurementLineProps = {
42
45
  function FloorplanMeasurementLine({
43
46
  palette,
44
47
  segment,
48
+ path,
45
49
  isSelected,
46
50
  dashed = false,
47
51
  stroke,
@@ -50,7 +54,20 @@ function FloorplanMeasurementLine({
50
54
  ? FLOORPLAN_MEASUREMENT_LINE_OPACITY
51
55
  : FLOORPLAN_MEASUREMENT_LINE_OPACITY * 0.4
52
56
 
53
- return (
57
+ return path ? (
58
+ <path
59
+ d={path}
60
+ fill="none"
61
+ shapeRendering="geometricPrecision"
62
+ stroke={stroke ?? palette.measurementStroke}
63
+ strokeDasharray={dashed ? FLOORPLAN_MEASUREMENT_EXTENSION_DASH : undefined}
64
+ strokeLinecap="round"
65
+ strokeLinejoin="round"
66
+ strokeOpacity={lineOpacity}
67
+ strokeWidth={FLOORPLAN_MEASUREMENT_LINE_WIDTH}
68
+ vectorEffect="non-scaling-stroke"
69
+ />
70
+ ) : (
54
71
  <line
55
72
  shapeRendering="geometricPrecision"
56
73
  stroke={stroke ?? palette.measurementStroke}
@@ -111,12 +128,26 @@ type FloorplanMeasurementsLayerProps = {
111
128
  className: string
112
129
  measurements: LinearMeasurementOverlay[]
113
130
  palette: FloorplanMeasurementPalette
131
+ sceneRotationDeg?: number
132
+ }
133
+
134
+ function normalizeReadableScreenAngle(angleDeg: number) {
135
+ let normalized = ((((angleDeg + 180) % 360) + 360) % 360) - 180
136
+
137
+ if (normalized > 90) {
138
+ normalized -= 180
139
+ } else if (normalized <= -90) {
140
+ normalized += 180
141
+ }
142
+
143
+ return normalized
114
144
  }
115
145
 
116
146
  export const FloorplanMeasurementsLayer = memo(function FloorplanMeasurementsLayer({
117
147
  className,
118
148
  measurements,
119
149
  palette,
150
+ sceneRotationDeg = 0,
120
151
  }: FloorplanMeasurementsLayerProps) {
121
152
  if (measurements.length === 0) {
122
153
  return null
@@ -124,79 +155,90 @@ export const FloorplanMeasurementsLayer = memo(function FloorplanMeasurementsLay
124
155
 
125
156
  return (
126
157
  <>
127
- {measurements.map((measurement) => (
128
- <g
129
- className={className}
130
- key={measurement.id}
131
- pointerEvents="none"
132
- style={{ userSelect: 'none' }}
133
- >
134
- <FloorplanMeasurementLine
135
- dashed={measurement.dashedExtensions ?? true}
136
- isSelected={measurement.isSelected}
137
- palette={palette}
138
- segment={measurement.extensionStart}
139
- stroke={measurement.extensionStroke}
140
- />
141
- <FloorplanMeasurementLine
142
- isSelected={measurement.isSelected}
143
- palette={palette}
144
- segment={measurement.dimensionLineStart}
145
- stroke={measurement.stroke}
146
- />
147
- <FloorplanMeasurementLine
148
- isSelected={measurement.isSelected}
149
- palette={palette}
150
- segment={measurement.dimensionLineEnd}
151
- stroke={measurement.stroke}
152
- />
153
- <FloorplanMeasurementLine
154
- dashed={measurement.dashedExtensions ?? true}
155
- isSelected={measurement.isSelected}
156
- palette={palette}
157
- segment={measurement.extensionEnd}
158
- stroke={measurement.extensionStroke}
159
- />
160
- {measurement.showTicks !== false ? (
161
- <>
162
- <FloorplanMeasurementTick
163
- angleDeg={measurement.labelAngleDeg}
158
+ {measurements.map((measurement) =>
159
+ (() => {
160
+ const screenLabelAngleDeg = normalizeReadableScreenAngle(
161
+ measurement.labelAngleDeg + sceneRotationDeg,
162
+ )
163
+ const localLabelAngleDeg = screenLabelAngleDeg - sceneRotationDeg
164
+
165
+ return (
166
+ <g
167
+ className={className}
168
+ key={measurement.id}
169
+ pointerEvents="none"
170
+ style={{ userSelect: 'none' }}
171
+ >
172
+ <FloorplanMeasurementLine
173
+ dashed={measurement.dashedExtensions ?? true}
174
+ isSelected={measurement.isSelected}
175
+ palette={palette}
176
+ segment={measurement.extensionStart}
177
+ stroke={measurement.extensionStroke}
178
+ />
179
+ <FloorplanMeasurementLine
164
180
  isSelected={measurement.isSelected}
165
181
  palette={palette}
182
+ path={measurement.dimensionPathStart}
183
+ segment={measurement.dimensionLineStart}
166
184
  stroke={measurement.stroke}
167
- x={measurement.dimensionLineStart.x1}
168
- y={measurement.dimensionLineStart.y1}
169
185
  />
170
- <FloorplanMeasurementTick
171
- angleDeg={measurement.labelAngleDeg}
186
+ <FloorplanMeasurementLine
172
187
  isSelected={measurement.isSelected}
173
188
  palette={palette}
189
+ path={measurement.dimensionPathEnd}
190
+ segment={measurement.dimensionLineEnd}
174
191
  stroke={measurement.stroke}
175
- x={measurement.dimensionLineEnd.x2}
176
- y={measurement.dimensionLineEnd.y2}
177
192
  />
178
- </>
179
- ) : null}
180
- <text
181
- dominantBaseline="central"
182
- fill={measurement.labelFill ?? palette.measurementStroke}
183
- fillOpacity={
184
- measurement.isSelected
185
- ? FLOORPLAN_MEASUREMENT_LABEL_OPACITY
186
- : FLOORPLAN_MEASUREMENT_LABEL_OPACITY * 0.4
187
- }
188
- fontFamily="ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace"
189
- fontSize={FLOORPLAN_MEASUREMENT_LABEL_FONT_SIZE}
190
- fontWeight="600"
191
- textAnchor="middle"
192
- transform={`rotate(${measurement.labelAngleDeg} ${measurement.labelX} ${measurement.labelY}) translate(0, -0.04)`}
193
- x={measurement.labelX}
194
- y={measurement.labelY}
195
- >
196
- {measurement.label}
197
- </text>
198
- </g>
199
- ))}
193
+ <FloorplanMeasurementLine
194
+ dashed={measurement.dashedExtensions ?? true}
195
+ isSelected={measurement.isSelected}
196
+ palette={palette}
197
+ segment={measurement.extensionEnd}
198
+ stroke={measurement.extensionStroke}
199
+ />
200
+ {measurement.showTicks !== false ? (
201
+ <>
202
+ <FloorplanMeasurementTick
203
+ angleDeg={localLabelAngleDeg}
204
+ isSelected={measurement.isSelected}
205
+ palette={palette}
206
+ stroke={measurement.stroke}
207
+ x={measurement.dimensionLineStart.x1}
208
+ y={measurement.dimensionLineStart.y1}
209
+ />
210
+ <FloorplanMeasurementTick
211
+ angleDeg={localLabelAngleDeg}
212
+ isSelected={measurement.isSelected}
213
+ palette={palette}
214
+ stroke={measurement.stroke}
215
+ x={measurement.dimensionLineEnd.x2}
216
+ y={measurement.dimensionLineEnd.y2}
217
+ />
218
+ </>
219
+ ) : null}
220
+ <text
221
+ dominantBaseline="central"
222
+ fill={measurement.labelFill ?? palette.measurementStroke}
223
+ fillOpacity={
224
+ measurement.isSelected
225
+ ? FLOORPLAN_MEASUREMENT_LABEL_OPACITY
226
+ : FLOORPLAN_MEASUREMENT_LABEL_OPACITY * 0.4
227
+ }
228
+ fontFamily="ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace"
229
+ fontSize={FLOORPLAN_MEASUREMENT_LABEL_FONT_SIZE}
230
+ fontWeight="600"
231
+ textAnchor="middle"
232
+ transform={`rotate(${localLabelAngleDeg} ${measurement.labelX} ${measurement.labelY}) translate(0, -0.04)`}
233
+ x={measurement.labelX}
234
+ y={measurement.labelY}
235
+ >
236
+ {measurement.label}
237
+ </text>
238
+ </g>
239
+ )
240
+ })(),
241
+ )}
200
242
  </>
201
243
  )
202
244
  })
@@ -0,0 +1,62 @@
1
+ 'use client'
2
+
3
+ import {
4
+ type AnyNode,
5
+ type AnyNodeId,
6
+ type FloorplanGeometry,
7
+ type GeometryContext,
8
+ nodeRegistry,
9
+ useScene,
10
+ } from '@pascal-app/core'
11
+ import { memo } from 'react'
12
+ import usePlacementPreview from '../../../store/use-placement-preview'
13
+ import { FloorplanGeometryRenderer } from './floorplan-geometry-renderer'
14
+
15
+ /**
16
+ * Renders a faint, non-interactive ghost of the node being placed by a
17
+ * registry placement tool (e.g. column), following the cursor in the floor
18
+ * plan. The 3D view shows a translucent mesh preview; in 2D that mesh is
19
+ * hidden (canvas `display:none`), so without this the user only saw the grid
20
+ * cursor dot + alignment guides — no sense of the footprint they were about
21
+ * to drop. The placement tool publishes a transient, already-positioned +
22
+ * aligned node to `usePlacementPreview`; we build its `def.floorplan`
23
+ * footprint with a minimal (unselected) context and render it.
24
+ *
25
+ * Mounted inside the floor-plan scene `<g>` so the geometry's level-local
26
+ * meters get the same world→SVG transform every other entry does.
27
+ */
28
+ export const FloorplanPlacementPreviewLayer = memo(function FloorplanPlacementPreviewLayer() {
29
+ const node = usePlacementPreview((s) => s.node)
30
+ const parentNode = usePlacementPreview((s) => s.parentNode)
31
+ if (!node) return null
32
+
33
+ const builder = nodeRegistry.get(node.type)?.floorplan
34
+ if (!builder) return null
35
+
36
+ // Minimal, unselected context — preview never shows selection chrome
37
+ // (move handles / resize arrows / hatch live behind `viewState.selected`).
38
+ // `resolve` reads the scene lazily (a builder rarely calls it for a ghost,
39
+ // and `parent: null` short-circuits the elevator's level walk) so the layer
40
+ // never subscribes to / bulk-reads the nodes map during render.
41
+ // `parentNode` is the synthetic wall for an off-wall door/window ghost so
42
+ // its builder draws the real swing-arc / pane symbol (see use-placement-preview).
43
+ const ctx = {
44
+ resolve: (id: AnyNodeId) => useScene.getState().nodes[id],
45
+ children: [],
46
+ siblings: [],
47
+ parent: parentNode ?? null,
48
+ viewState: undefined,
49
+ } as unknown as GeometryContext
50
+
51
+ const geometry = (builder as (n: AnyNode, c: GeometryContext) => FloorplanGeometry | null)(
52
+ node,
53
+ ctx,
54
+ )
55
+ if (!geometry) return null
56
+
57
+ return (
58
+ <g data-floorplan-placement-preview opacity={0.5} pointerEvents="none">
59
+ <FloorplanGeometryRenderer geometry={geometry} />
60
+ </g>
61
+ )
62
+ })
@@ -0,0 +1,308 @@
1
+ import { beforeEach, describe, expect, mock, test } from 'bun:test'
2
+ import type {
3
+ AnyNode,
4
+ AnyNodeId,
5
+ FloorplanAffordanceSession,
6
+ LiveNodeOverrides,
7
+ } from '@pascal-app/core'
8
+ import { type AnyNodeDefinition, emitter, nodeRegistry, registerNode } from '@pascal-app/core'
9
+ import { z } from 'zod'
10
+ import {
11
+ cancelFloorplanAffordanceDrag,
12
+ collectFloorplanDependencyNodes,
13
+ computeAffectedSiblingIds,
14
+ floorplanHandleDoubleClickAffordance,
15
+ subscribeFloorplanAffordanceToolCancel,
16
+ } from './floorplan-registry-layer'
17
+
18
+ function cabinetRun(id: string, children: string[] = [], parentId: string | null = 'level_test') {
19
+ return {
20
+ id,
21
+ type: 'cabinet',
22
+ object: 'node',
23
+ parentId,
24
+ visible: true,
25
+ metadata: {},
26
+ children,
27
+ position: [0, 0, 0],
28
+ rotation: 0,
29
+ width: 1.2,
30
+ depth: 0.58,
31
+ carcassHeight: 0.72,
32
+ plinthHeight: 0.1,
33
+ showPlinth: true,
34
+ withCountertop: true,
35
+ countertopThickness: 0.02,
36
+ } as AnyNode
37
+ }
38
+
39
+ function cabinetModule(id: string, parentId: string, children: string[] = []) {
40
+ return {
41
+ id,
42
+ type: 'cabinet-module',
43
+ object: 'node',
44
+ parentId,
45
+ visible: true,
46
+ metadata: {},
47
+ children,
48
+ position: [0, 0.1, 0],
49
+ rotation: 0,
50
+ width: 0.6,
51
+ depth: 0.58,
52
+ carcassHeight: 0.72,
53
+ plinthHeight: 0.1,
54
+ showPlinth: true,
55
+ countertopThickness: 0.02,
56
+ } as AnyNode
57
+ }
58
+
59
+ function isCabinetNode(node: AnyNode | undefined): boolean {
60
+ return node?.type === 'cabinet' || node?.type === 'cabinet-module'
61
+ }
62
+
63
+ function childIdsOf(node: AnyNode | undefined): AnyNodeId[] {
64
+ return Array.isArray((node as { children?: unknown } | undefined)?.children)
65
+ ? ((node as { children: AnyNodeId[] }).children ?? [])
66
+ : []
67
+ }
68
+
69
+ function cabinetAffectedIds({
70
+ node,
71
+ nodes,
72
+ liveOverrides,
73
+ }: {
74
+ node: AnyNode
75
+ nodes: Record<AnyNodeId, AnyNode>
76
+ liveOverrides: Map<string, Record<string, unknown>>
77
+ }): readonly AnyNodeId[] {
78
+ const affected = new Set<AnyNodeId>()
79
+ const visited = new Set<AnyNodeId>()
80
+ const queue: AnyNodeId[] = [node.id as AnyNodeId]
81
+
82
+ while (queue.length > 0) {
83
+ const id = queue.pop()!
84
+ if (visited.has(id)) continue
85
+ visited.add(id)
86
+ const current = nodes[id]
87
+ if (!isCabinetNode(current)) continue
88
+ affected.add(id)
89
+
90
+ const parentIds = [
91
+ current?.parentId as AnyNodeId | undefined,
92
+ (liveOverrides.get(id) as { parentId?: AnyNodeId } | undefined)?.parentId,
93
+ ]
94
+ for (const parentId of parentIds) {
95
+ const parent = parentId ? nodes[parentId] : undefined
96
+ if (parentId && isCabinetNode(parent)) queue.push(parentId)
97
+ }
98
+ for (const childId of childIdsOf(current)) {
99
+ if (isCabinetNode(nodes[childId])) queue.push(childId)
100
+ }
101
+ }
102
+
103
+ return Array.from(affected)
104
+ }
105
+
106
+ function registerCabinetFloorplanDefinition(kind: 'cabinet' | 'cabinet-module') {
107
+ registerNode({
108
+ kind,
109
+ schemaVersion: 1,
110
+ schema: z.object({ type: z.literal(kind) }) as never,
111
+ category: 'utility',
112
+ defaults: () => ({}) as never,
113
+ floorplanAffectedIds: cabinetAffectedIds,
114
+ } as unknown as AnyNodeDefinition)
115
+ }
116
+
117
+ describe('floorplan affordance cancellation', () => {
118
+ test('tool:cancel reverts the drag and makes a later pointerup inert', () => {
119
+ const releasePointerCapture = mock(() => {})
120
+ const commit = mock(() => {})
121
+ const session: FloorplanAffordanceSession = {
122
+ affectedIds: ['wall_a', 'wall_b'],
123
+ apply: () => {},
124
+ canCommit: () => true,
125
+ commit,
126
+ }
127
+ const snapshots = [{ id: 'wall_a' as AnyNodeId, data: { width: 1 } }]
128
+ const drag = {
129
+ pointerId: 7,
130
+ captureTarget: {
131
+ hasPointerCapture: mock(() => true),
132
+ releasePointerCapture,
133
+ } as unknown as Element,
134
+ handleId: 'wall_a:endpoint',
135
+ session,
136
+ snapshots,
137
+ historyPaused: true,
138
+ lastPlanPoint: [0, 0] as [number, number],
139
+ }
140
+ const dragRef = { current: drag }
141
+ const restoreSnapshots = mock(() => {})
142
+ const resumeHistory = mock(() => {})
143
+ const clearPreview = mock(() => {})
144
+ const clearSnapFeedback = mock(() => {})
145
+ const endReshapeScope = mock(() => {})
146
+ const clearDragFeedback = mock(() => {})
147
+ const consumeToolCancel = mock(() => {})
148
+
149
+ const unsubscribe = subscribeFloorplanAffordanceToolCancel(
150
+ () =>
151
+ cancelFloorplanAffordanceDrag(dragRef, {
152
+ restoreSnapshots,
153
+ resumeHistory,
154
+ clearPreview,
155
+ clearSnapFeedback,
156
+ endReshapeScope,
157
+ clearDragFeedback,
158
+ }),
159
+ consumeToolCancel,
160
+ )
161
+
162
+ try {
163
+ emitter.emit('tool:cancel')
164
+ emitter.emit('tool:cancel')
165
+ } finally {
166
+ unsubscribe()
167
+ }
168
+
169
+ expect(dragRef.current).toBeNull()
170
+ expect(releasePointerCapture).toHaveBeenCalledWith(7)
171
+ expect(restoreSnapshots).toHaveBeenCalledWith(snapshots)
172
+ expect(resumeHistory).toHaveBeenCalledTimes(1)
173
+ expect(clearPreview).toHaveBeenCalledTimes(2)
174
+ expect(clearPreview).toHaveBeenNthCalledWith(1, 'wall_a')
175
+ expect(clearPreview).toHaveBeenNthCalledWith(2, 'wall_b')
176
+ expect(clearSnapFeedback).toHaveBeenCalledTimes(1)
177
+ expect(endReshapeScope).toHaveBeenCalledWith(drag)
178
+ expect(clearDragFeedback).toHaveBeenCalledTimes(1)
179
+ expect(consumeToolCancel).toHaveBeenCalledTimes(1)
180
+ expect(drag.historyPaused).toBe(false)
181
+
182
+ const activeDrag = dragRef.current
183
+ if (activeDrag?.pointerId === 7) activeDrag.session.commit?.()
184
+ expect(commit).toHaveBeenCalledTimes(0)
185
+ })
186
+ })
187
+
188
+ describe('floorplan vertex double-click routing', () => {
189
+ test('routes polygon vertex handles to the kind-owned delete affordance', () => {
190
+ expect(
191
+ floorplanHandleDoubleClickAffordance({
192
+ kind: 'endpoint-handle',
193
+ point: [1, 2],
194
+ state: 'idle',
195
+ affordance: 'move-vertex',
196
+ payload: { vertexIndex: 2 },
197
+ }),
198
+ ).toBe('delete-vertex')
199
+
200
+ expect(
201
+ floorplanHandleDoubleClickAffordance({
202
+ kind: 'endpoint-handle',
203
+ point: [1, 2],
204
+ state: 'idle',
205
+ affordance: 'move-endpoint',
206
+ payload: { endpoint: 'end' },
207
+ }),
208
+ ).toBeNull()
209
+ })
210
+ })
211
+
212
+ describe('computeAffectedSiblingIds', () => {
213
+ beforeEach(() => {
214
+ nodeRegistry._reset()
215
+ registerCabinetFloorplanDefinition('cabinet')
216
+ registerCabinetFloorplanDefinition('cabinet-module')
217
+ })
218
+
219
+ test('propagates cabinet live overrides through the cabinet family', () => {
220
+ const run = cabinetRun('cabinet_run', ['cabinet-module_main', 'cabinet-module_corner'])
221
+ const module = cabinetModule('cabinet-module_main', run.id)
222
+ const cornerModule = cabinetModule('cabinet-module_corner', run.id, ['cabinet_child-run'])
223
+ const childRun = cabinetRun('cabinet_child-run', ['cabinet-module_child'], cornerModule.id)
224
+ const childModule = cabinetModule('cabinet-module_child', childRun.id)
225
+ const nodes = {
226
+ [run.id]: run,
227
+ [module.id]: module,
228
+ [cornerModule.id]: cornerModule,
229
+ [childRun.id]: childRun,
230
+ [childModule.id]: childModule,
231
+ } as Record<string, AnyNode>
232
+
233
+ const affected = computeAffectedSiblingIds(
234
+ [run.id as AnyNodeId],
235
+ nodes,
236
+ new Map([[run.id, { position: [2, 0, 3] }]]),
237
+ )
238
+
239
+ expect(affected).toEqual(
240
+ new Set([run.id, module.id, cornerModule.id, childRun.id, childModule.id] as AnyNodeId[]),
241
+ )
242
+ })
243
+
244
+ test('propagates a live-moving cabinet module back to its owning run', () => {
245
+ const run = cabinetRun('cabinet_run', ['cabinet-module_main', 'cabinet-module_child'])
246
+ const module = cabinetModule('cabinet-module_main', run.id)
247
+ const sibling = cabinetModule('cabinet-module_child', run.id)
248
+ const nodes = {
249
+ [run.id]: run,
250
+ [module.id]: module,
251
+ [sibling.id]: sibling,
252
+ } as Record<string, AnyNode>
253
+
254
+ const affected = computeAffectedSiblingIds(
255
+ [module.id as AnyNodeId],
256
+ nodes,
257
+ new Map([[module.id, { position: [1.2, 0.1, 0.3] }]]),
258
+ )
259
+
260
+ expect(affected).toEqual(new Set([module.id, run.id, sibling.id] as AnyNodeId[]))
261
+ })
262
+ })
263
+
264
+ describe('collectFloorplanDependencyNodes', () => {
265
+ test('includes referenced hosts and their transform-owning parents', () => {
266
+ const level = {
267
+ id: 'level_test',
268
+ type: 'level',
269
+ parentId: null,
270
+ children: ['roof_test'],
271
+ } as unknown as AnyNode
272
+ const roof = {
273
+ id: 'roof_test',
274
+ type: 'roof',
275
+ parentId: level.id,
276
+ children: [],
277
+ position: [0, 0, 0],
278
+ rotation: 0,
279
+ } as unknown as AnyNode
280
+ const measurement = {
281
+ id: 'measurement_test',
282
+ type: 'measurement',
283
+ parentId: level.id,
284
+ } as unknown as AnyNode
285
+ const definition = {
286
+ floorplanDependencies: () => [roof.id],
287
+ } as unknown as AnyNodeDefinition
288
+
289
+ expect(
290
+ collectFloorplanDependencyNodes(
291
+ definition,
292
+ measurement,
293
+ {
294
+ [level.id]: level,
295
+ [roof.id]: roof,
296
+ [measurement.id]: measurement,
297
+ },
298
+ new Map<string, LiveNodeOverrides>([
299
+ [roof.id, { position: [3, 0, 2] }],
300
+ [level.id, { visible: false }],
301
+ ]),
302
+ ),
303
+ ).toEqual([
304
+ expect.objectContaining({ id: roof.id, position: [3, 0, 2] }),
305
+ expect.objectContaining({ id: level.id, visible: false }),
306
+ ])
307
+ })
308
+ })