@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
@@ -0,0 +1,86 @@
1
+ import { type AnyNodeId, type GutterNode, useScene } from '@pascal-app/core'
2
+ import { useViewer } from '@pascal-app/viewer'
3
+ import Image from 'next/image'
4
+ import { memo, useCallback, useState } from 'react'
5
+ import { SnapTargetIcon } from '../../../snap-target-badge'
6
+ import useEditor from './../../../../../store/use-editor'
7
+ import { InlineRenameInput } from './inline-rename-input'
8
+ import { focusTreeNode, handleTreeSelection, TreeNodeWrapper } from './tree-node'
9
+ import { TreeNodeActions } from './tree-node-actions'
10
+
11
+ interface GutterTreeNodeProps {
12
+ nodeId: AnyNodeId
13
+ depth: number
14
+ isLast?: boolean
15
+ }
16
+
17
+ export const GutterTreeNode = memo(function GutterTreeNode({
18
+ nodeId,
19
+ depth,
20
+ isLast,
21
+ }: GutterTreeNodeProps) {
22
+ const [isEditing, setIsEditing] = useState(false)
23
+ const isVisible = useScene((s) => s.nodes[nodeId]?.visible !== false)
24
+ const node = useScene((s) => s.nodes[nodeId] as GutterNode | undefined)
25
+ const isSelected = useViewer((state) => state.selection.selectedIds.includes(nodeId))
26
+ const isHovered = useViewer((state) => state.hoveredId === nodeId)
27
+ const setSelection = useViewer((state) => state.setSelection)
28
+ const setHoveredId = useViewer((state) => state.setHoveredId)
29
+
30
+ const handleClick = useCallback(
31
+ (e: React.MouseEvent) => {
32
+ e.stopPropagation()
33
+ const handled = handleTreeSelection(
34
+ e,
35
+ nodeId,
36
+ useViewer.getState().selection.selectedIds,
37
+ setSelection,
38
+ )
39
+ if (!handled && useEditor.getState().phase === 'furnish') {
40
+ useEditor.getState().setPhase('structure')
41
+ }
42
+ },
43
+ [nodeId, setSelection],
44
+ )
45
+
46
+ const defaultName = node?.name || 'Gutter'
47
+
48
+ return (
49
+ <TreeNodeWrapper
50
+ actions={<TreeNodeActions nodeId={nodeId} />}
51
+ depth={depth}
52
+ expanded={false}
53
+ hasChildren={false}
54
+ icon={
55
+ <SnapTargetIcon target="roof">
56
+ <Image
57
+ alt=""
58
+ className="object-contain opacity-60"
59
+ height={14}
60
+ src="/icons/roof.webp"
61
+ width={14}
62
+ />
63
+ </SnapTargetIcon>
64
+ }
65
+ isHovered={isHovered}
66
+ isLast={isLast}
67
+ isSelected={isSelected}
68
+ isVisible={isVisible}
69
+ label={
70
+ <InlineRenameInput
71
+ defaultName={defaultName}
72
+ isEditing={isEditing}
73
+ nodeId={nodeId}
74
+ onStartEditing={() => setIsEditing(true)}
75
+ onStopEditing={() => setIsEditing(false)}
76
+ />
77
+ }
78
+ nodeId={nodeId}
79
+ onClick={handleClick}
80
+ onDoubleClick={() => focusTreeNode(nodeId)}
81
+ onMouseEnter={() => setHoveredId(nodeId)}
82
+ onMouseLeave={() => setHoveredId(null)}
83
+ onToggle={() => {}}
84
+ />
85
+ )
86
+ })
@@ -36,7 +36,16 @@ import {
36
36
  buildLevelDuplicateCreateOps,
37
37
  type LevelDuplicatePreset,
38
38
  } from './../../../../../lib/level-duplication'
39
+ import { getDefaultLevelName } from '@pascal-app/core'
39
40
  import { deleteLevelWithFallbackSelection } from './../../../../../lib/level-selection'
41
+ import {
42
+ formatAreaLabel,
43
+ getAreaUnitLabel,
44
+ getLinearUnitLabel,
45
+ linearUnitToMeters,
46
+ metersToLinearUnit,
47
+ squareMetersToAreaUnit,
48
+ } from './../../../../../lib/measurements'
40
49
  import { createLocalGuideImage } from './../../../../../lib/local-guide-image'
41
50
  import { cn } from './../../../../../lib/utils'
42
51
  import useEditor from './../../../../../store/use-editor'
@@ -92,6 +101,7 @@ const PropertyLineSection = memo(function PropertyLineSection() {
92
101
  const updateNode = useScene((state) => state.updateNode)
93
102
  const mode = useEditor((state) => state.mode)
94
103
  const setMode = useEditor((state) => state.setMode)
104
+ const viewerUnit = useViewer((state) => state.unit)
95
105
 
96
106
  if (!siteNode) return null
97
107
 
@@ -100,6 +110,14 @@ const PropertyLineSection = memo(function PropertyLineSection() {
100
110
  const perimeter = calculatePerimeter(points)
101
111
  const isEditing = mode === 'edit'
102
112
 
113
+ // Property-line coordinates and readouts follow the metric/imperial toggle.
114
+ const isImperial = viewerUnit === 'imperial'
115
+ const linearLabel = getLinearUnitLabel(viewerUnit)
116
+ const toDisplayLinear = (meters: number) => metersToLinearUnit(meters, viewerUnit)
117
+ const toStoredLinear = (display: number) => linearUnitToMeters(display, viewerUnit)
118
+ const displayArea = squareMetersToAreaUnit(area, viewerUnit)
119
+ const displayPerimeter = toDisplayLinear(perimeter)
120
+
103
121
  const handleToggleEdit = () => {
104
122
  setMode(isEditing ? 'select' : 'edit')
105
123
  }
@@ -165,10 +183,16 @@ const PropertyLineSection = memo(function PropertyLineSection() {
165
183
  {/* Measurements */}
166
184
  <div className="relative flex gap-3 pr-3 pb-2 pl-10">
167
185
  <div className="text-muted-foreground text-xs">
168
- Area: <span className="text-foreground">{area.toFixed(1)} m²</span>
186
+ Area:{' '}
187
+ <span className="text-foreground">
188
+ {displayArea.toFixed(1)} {getAreaUnitLabel(viewerUnit)}
189
+ </span>
169
190
  </div>
170
191
  <div className="text-muted-foreground text-xs">
171
- Perimeter: <span className="text-foreground">{perimeter.toFixed(1)} m</span>
192
+ Perimeter:{' '}
193
+ <span className="text-foreground">
194
+ {displayPerimeter.toFixed(1)} {linearLabel}
195
+ </span>
172
196
  </div>
173
197
  </div>
174
198
 
@@ -183,21 +207,21 @@ const PropertyLineSection = memo(function PropertyLineSection() {
183
207
  <input
184
208
  className="w-16 rounded border border-border/50 bg-accent/50 px-1.5 py-0.5 text-foreground text-xs focus:border-primary focus:outline-none"
185
209
  onChange={(e) =>
186
- handlePointChange(index, 0, Number.parseFloat(e.target.value) || 0)
210
+ handlePointChange(index, 0, toStoredLinear(Number.parseFloat(e.target.value) || 0))
187
211
  }
188
212
  step={0.5}
189
213
  type="number"
190
- value={point[0]}
214
+ value={Number(toDisplayLinear(point[0]).toFixed(2))}
191
215
  />
192
216
  <label className="shrink-0 text-muted-foreground">Z</label>
193
217
  <input
194
218
  className="w-16 rounded border border-border/50 bg-accent/50 px-1.5 py-0.5 text-foreground text-xs focus:border-primary focus:outline-none"
195
219
  onChange={(e) =>
196
- handlePointChange(index, 1, Number.parseFloat(e.target.value) || 0)
220
+ handlePointChange(index, 1, toStoredLinear(Number.parseFloat(e.target.value) || 0))
197
221
  }
198
222
  step={0.5}
199
223
  type="number"
200
- value={point[1]}
224
+ value={Number(toDisplayLinear(point[1]).toFixed(2))}
201
225
  />
202
226
  <button
203
227
  className={cn(
@@ -355,13 +379,13 @@ const ReferenceItem = memo(function ReferenceItem({
355
379
  <img
356
380
  alt="Scan"
357
381
  className="h-3.5 w-3.5 shrink-0 object-contain opacity-70 transition-opacity group-hover/ref:opacity-100"
358
- src="/icons/mesh.png"
382
+ src="/icons/mesh.webp"
359
383
  />
360
384
  ) : (
361
385
  <img
362
386
  alt="Guide"
363
387
  className="h-3.5 w-3.5 shrink-0 object-contain opacity-70 transition-opacity group-hover/ref:opacity-100"
364
- src="/icons/floorplan.png"
388
+ src="/icons/floorplan.webp"
365
389
  />
366
390
  )}
367
391
  <InlineRenameInput
@@ -720,10 +744,10 @@ const LevelItem = memo(function LevelItem({
720
744
  'h-4 w-4 shrink-0 object-contain transition-all duration-200',
721
745
  !isSelected && 'opacity-60 grayscale',
722
746
  )}
723
- src="/icons/level.png"
747
+ src="/icons/level.webp"
724
748
  />
725
749
  <InlineRenameInput
726
- defaultName={`Level ${level.level}`}
750
+ defaultName={getDefaultLevelName(level.level)}
727
751
  isEditing={isEditing}
728
752
  nodeId={level.id}
729
753
  onStartEditing={() => setIsEditing(true)}
@@ -998,7 +1022,7 @@ const LayerToggle = memo(function LayerToggle() {
998
1022
  'mb-1 h-6 w-6 transition-all',
999
1023
  activeTab !== 'structure' && 'opacity-50 grayscale',
1000
1024
  )}
1001
- src="/icons/room.png"
1025
+ src="/icons/room.webp"
1002
1026
  />
1003
1027
  Structure
1004
1028
  </div>
@@ -1034,7 +1058,7 @@ const LayerToggle = memo(function LayerToggle() {
1034
1058
  'mb-1 h-6 w-6 transition-all',
1035
1059
  activeTab !== 'furnish' && 'opacity-50 grayscale',
1036
1060
  )}
1037
- src="/icons/couch.png"
1061
+ src="/icons/couch.webp"
1038
1062
  />
1039
1063
  Furnish
1040
1064
  </div>
@@ -1071,7 +1095,7 @@ const LayerToggle = memo(function LayerToggle() {
1071
1095
  'mb-1 h-6 w-6 transition-all',
1072
1096
  activeTab !== 'zones' && 'opacity-50 grayscale',
1073
1097
  )}
1074
- src="/icons/kitchen.png"
1098
+ src="/icons/kitchen.webp"
1075
1099
  />
1076
1100
  Zones
1077
1101
  </div>
@@ -1096,6 +1120,7 @@ const ZoneItem = memo(function ZoneItem({ zone, isLast }: { zone: ZoneNode; isLa
1096
1120
  const setHoveredId = useViewer((state) => state.setHoveredId)
1097
1121
  const setPhase = useEditor((state) => state.setPhase)
1098
1122
  const setMode = useEditor((state) => state.setMode)
1123
+ const unit = useViewer((state) => state.unit)
1099
1124
 
1100
1125
  const isSelected = selectedZoneId === zone.id
1101
1126
  const isHovered = hoveredId === zone.id
@@ -1108,8 +1133,7 @@ const ZoneItem = memo(function ZoneItem({ zone, isLast }: { zone: ZoneNode; isLa
1108
1133
  }
1109
1134
  }, [isSelected])
1110
1135
 
1111
- const area = calculatePolygonArea(zone.polygon).toFixed(1)
1112
- const defaultName = `Zone (${area}m²)`
1136
+ const defaultName = `Zone (${formatAreaLabel(calculatePolygonArea(zone.polygon), unit)})`
1113
1137
 
1114
1138
  const handleClick = () => {
1115
1139
  setSelection({ zoneId: zone.id })
@@ -1412,7 +1436,7 @@ const BuildingItem = memo(function BuildingItem({
1412
1436
  'h-5 w-5 object-contain transition-all',
1413
1437
  !isBuildingActive && 'opacity-60 grayscale',
1414
1438
  )}
1415
- src="/icons/building.png"
1439
+ src="/icons/building.webp"
1416
1440
  />
1417
1441
  <span className="truncate font-medium text-sm">{building.name || 'Building'}</span>
1418
1442
  </div>
@@ -1541,10 +1565,7 @@ export function SitePanel({ projectId, onUploadAsset, onDeleteAsset }: SitePanel
1541
1565
  useShallow((s) => {
1542
1566
  if (!siteNode) return []
1543
1567
  return siteNode.children
1544
- .map((child) => {
1545
- const id = typeof child === 'string' ? child : child.id
1546
- return s.nodes[id] as BuildingNode | undefined
1547
- })
1568
+ .map((childId) => s.nodes[childId as AnyNodeId] as BuildingNode | undefined)
1548
1569
  .filter((node): node is BuildingNode => node?.type === 'building')
1549
1570
  }),
1550
1571
  )
@@ -1571,7 +1592,7 @@ export function SitePanel({ projectId, onUploadAsset, onDeleteAsset }: SitePanel
1571
1592
  'h-5 w-5 object-contain transition-all',
1572
1593
  phase !== 'site' && 'opacity-60 grayscale',
1573
1594
  )}
1574
- src="/icons/site.png"
1595
+ src="/icons/site-flag.webp"
1575
1596
  />
1576
1597
  <span className="font-medium text-sm">{siteNode.name || 'Site'}</span>
1577
1598
  </div>
@@ -3,19 +3,25 @@ import { useViewer } from '@pascal-app/viewer'
3
3
  import Image from 'next/image'
4
4
  import { memo, useCallback, useEffect, useState } from 'react'
5
5
  import { useShallow } from 'zustand/react/shallow'
6
- import useEditor from './../../../../../store/use-editor'
6
+ import { resolveNodeSnapTarget, SnapTargetIcon } from '../../../snap-target-badge'
7
7
  import { InlineRenameInput } from './inline-rename-input'
8
- import { focusTreeNode, handleTreeSelection, TreeNode, TreeNodeWrapper } from './tree-node'
8
+ import {
9
+ focusTreeNode,
10
+ handleTreeSelection,
11
+ routeTreeSelectionToNode,
12
+ TreeNode,
13
+ TreeNodeWrapper,
14
+ } from './tree-node'
9
15
  import { TreeNodeActions } from './tree-node-actions'
10
16
 
11
17
  const CATEGORY_ICONS: Record<string, string> = {
12
- door: '/icons/door.png',
13
- window: '/icons/window.png',
14
- furniture: '/icons/couch.png',
15
- appliance: '/icons/appliance.png',
16
- kitchen: '/icons/kitchen.png',
17
- bathroom: '/icons/bathroom.png',
18
- outdoor: '/icons/tree.png',
18
+ door: '/icons/door.webp',
19
+ window: '/icons/window.webp',
20
+ furniture: '/icons/couch.webp',
21
+ appliance: '/icons/appliance.webp',
22
+ kitchen: '/icons/kitchen.webp',
23
+ bathroom: '/icons/bathroom.webp',
24
+ outdoor: '/icons/tree.webp',
19
25
  }
20
26
 
21
27
  interface ItemTreeNodeProps {
@@ -35,7 +41,8 @@ export const ItemTreeNode = memo(function ItemTreeNode({
35
41
  const children = useScene(
36
42
  useShallow((s) => (s.nodes[nodeId] as ItemNode | undefined)?.children ?? []),
37
43
  )
38
- const asset = useScene((s) => (s.nodes[nodeId] as ItemNode | undefined)?.asset)
44
+ const node = useScene((s) => s.nodes[nodeId] as ItemNode | undefined)
45
+ const asset = node?.asset
39
46
  const isSelected = useViewer((state) => state.selection.selectedIds.includes(nodeId))
40
47
  const isHovered = useViewer((state) => state.hoveredId === nodeId)
41
48
  const setSelection = useViewer((state) => state.setSelection)
@@ -63,17 +70,15 @@ export const ItemTreeNode = memo(function ItemTreeNode({
63
70
  const handleClick = useCallback(
64
71
  (e: React.MouseEvent) => {
65
72
  e.stopPropagation()
66
- const handled = handleTreeSelection(
73
+ handleTreeSelection(
67
74
  e,
68
75
  nodeId,
69
76
  useViewer.getState().selection.selectedIds,
70
77
  setSelection,
71
78
  )
72
- if (!handled && useEditor.getState().phase === 'structure') {
73
- useEditor.getState().setPhase('furnish')
74
- }
79
+ routeTreeSelectionToNode(node)
75
80
  },
76
- [nodeId, setSelection],
81
+ [node, nodeId, setSelection],
77
82
  )
78
83
 
79
84
  const handleDoubleClick = useCallback(() => focusTreeNode(nodeId), [nodeId])
@@ -83,7 +88,8 @@ export const ItemTreeNode = memo(function ItemTreeNode({
83
88
  const handleStartEditing = useCallback(() => setIsEditing(true), [])
84
89
  const handleStopEditing = useCallback(() => setIsEditing(false), [])
85
90
 
86
- const iconSrc = CATEGORY_ICONS[asset?.category ?? ''] || '/icons/couch.png'
91
+ const iconSrc = CATEGORY_ICONS[asset?.category ?? ''] || '/icons/couch.webp'
92
+ const snapTarget = resolveNodeSnapTarget(node)
87
93
  const defaultName = asset?.name || 'Item'
88
94
  const hasChildren = children.length > 0
89
95
 
@@ -93,7 +99,15 @@ export const ItemTreeNode = memo(function ItemTreeNode({
93
99
  depth={depth}
94
100
  expanded={expanded}
95
101
  hasChildren={hasChildren}
96
- icon={<Image alt="" className="object-contain" height={14} src={iconSrc} width={14} />}
102
+ icon={
103
+ snapTarget ? (
104
+ <SnapTargetIcon target={snapTarget}>
105
+ <Image alt="" className="object-contain" height={14} src={iconSrc} width={14} />
106
+ </SnapTargetIcon>
107
+ ) : (
108
+ <Image alt="" className="object-contain" height={14} src={iconSrc} width={14} />
109
+ )
110
+ }
97
111
  isHovered={isHovered}
98
112
  isLast={isLast}
99
113
  isSelected={isSelected}
@@ -3,6 +3,7 @@ import { useViewer } from '@pascal-app/viewer'
3
3
  import { Layers } from 'lucide-react'
4
4
  import { memo, useCallback, useState } from 'react'
5
5
  import { useShallow } from 'zustand/react/shallow'
6
+ import { getDefaultLevelName } from '@pascal-app/core'
6
7
  import { InlineRenameInput } from './inline-rename-input'
7
8
  import { focusTreeNode, TreeNode, TreeNodeWrapper } from './tree-node'
8
9
  import { TreeNodeActions } from './tree-node-actions'
@@ -35,7 +36,7 @@ export const LevelTreeNode = memo(function LevelTreeNode({
35
36
  const handleStartEditing = useCallback(() => setIsEditing(true), [])
36
37
  const handleStopEditing = useCallback(() => setIsEditing(false), [])
37
38
 
38
- const defaultName = `Level ${level}`
39
+ const defaultName = getDefaultLevelName(level)
39
40
 
40
41
  return (
41
42
  <TreeNodeWrapper
@@ -0,0 +1,140 @@
1
+ import { Icon as IconifyIcon } from '@iconify/react'
2
+ import { type AnyNodeId, nodeRegistry, useScene } from '@pascal-app/core'
3
+ import { useViewer } from '@pascal-app/viewer'
4
+ import Image from 'next/image'
5
+ import { memo, useCallback, useEffect, useState } from 'react'
6
+ import { useShallow } from 'zustand/react/shallow'
7
+ import { resolveNodeSnapTarget, SnapTargetIcon } from '../../../snap-target-badge'
8
+ import { InlineRenameInput } from './inline-rename-input'
9
+ import {
10
+ focusTreeNode,
11
+ handleTreeSelection,
12
+ routeTreeSelectionToNode,
13
+ TreeNode,
14
+ TreeNodeWrapper,
15
+ } from './tree-node'
16
+ import { TreeNodeActions } from './tree-node-actions'
17
+ import { resolveTreeChildIds, treeContainsDescendant } from './tree-structure'
18
+
19
+ interface RegistryTreeNodeProps {
20
+ nodeId: AnyNodeId
21
+ depth: number
22
+ isLast?: boolean
23
+ }
24
+
25
+ /**
26
+ * Generic, registry-driven tree-node row powered by `def.presentation` and
27
+ * `def.tree`. Replaces the per-kind boilerplate
28
+ * components that differed only in their default name and icon — today the
29
+ * roof vents plus cabinet rows. Register a kind in `treeNodeByType` against
30
+ * this component instead of authoring another copy.
31
+ */
32
+ export const RegistryTreeNode = memo(function RegistryTreeNode({
33
+ nodeId,
34
+ depth,
35
+ isLast,
36
+ }: RegistryTreeNodeProps) {
37
+ const [isEditing, setIsEditing] = useState(false)
38
+ const [expanded, setExpanded] = useState(true)
39
+ const isVisible = useScene((s) => s.nodes[nodeId]?.visible !== false)
40
+ const node = useScene((s) => s.nodes[nodeId])
41
+ const children = useScene(useShallow((s) => resolveTreeChildIds(nodeId, s.nodes)))
42
+ const isSelected = useViewer((state) => state.selection.selectedIds.includes(nodeId))
43
+ const isHovered = useViewer((state) => state.hoveredId === nodeId)
44
+ const setSelection = useViewer((state) => state.setSelection)
45
+ const setHoveredId = useViewer((state) => state.setHoveredId)
46
+
47
+ const presentation = node ? nodeRegistry.get(node.type)?.presentation : undefined
48
+ const tree = node ? nodeRegistry.get(node.type)?.tree : undefined
49
+ const icon = presentation?.icon
50
+ const iconSrc = icon?.kind === 'url' ? icon.src : '/icons/roof.webp'
51
+ const iconElement =
52
+ icon?.kind === 'iconify' ? (
53
+ <IconifyIcon className="opacity-60" height={14} icon={icon.name} width={14} />
54
+ ) : (
55
+ <Image
56
+ alt=""
57
+ className="object-contain opacity-60"
58
+ height={14}
59
+ src={iconSrc}
60
+ width={14}
61
+ />
62
+ )
63
+ const snapTarget = resolveNodeSnapTarget(node)
64
+ const defaultName =
65
+ node ? tree?.label?.(node, useScene.getState().nodes) || node.name || presentation?.label || 'Node' : 'Node'
66
+ const hasChildren = children.length > 0
67
+
68
+ useEffect(() => {
69
+ return useViewer.subscribe((state) => {
70
+ const { selectedIds } = state.selection
71
+ if (selectedIds.length === 0) return
72
+ const nodes = useScene.getState().nodes
73
+ for (const id of selectedIds) {
74
+ if (treeContainsDescendant(nodeId, id as AnyNodeId, nodes)) {
75
+ setExpanded(true)
76
+ return
77
+ }
78
+ }
79
+ })
80
+ }, [nodeId])
81
+
82
+ const handleClick = useCallback(
83
+ (e: React.MouseEvent) => {
84
+ e.stopPropagation()
85
+ handleTreeSelection(
86
+ e,
87
+ nodeId,
88
+ useViewer.getState().selection.selectedIds,
89
+ setSelection,
90
+ )
91
+ routeTreeSelectionToNode(node)
92
+ },
93
+ [node, nodeId, setSelection],
94
+ )
95
+
96
+ return (
97
+ <TreeNodeWrapper
98
+ actions={<TreeNodeActions nodeId={nodeId} />}
99
+ depth={depth}
100
+ expanded={expanded}
101
+ hasChildren={hasChildren}
102
+ icon={
103
+ snapTarget ? (
104
+ <SnapTargetIcon target={snapTarget}>{iconElement}</SnapTargetIcon>
105
+ ) : (
106
+ iconElement
107
+ )
108
+ }
109
+ isHovered={isHovered}
110
+ isLast={isLast}
111
+ isSelected={isSelected}
112
+ isVisible={isVisible}
113
+ label={
114
+ <InlineRenameInput
115
+ defaultName={defaultName}
116
+ isEditing={isEditing}
117
+ nodeId={nodeId}
118
+ onStartEditing={() => setIsEditing(true)}
119
+ onStopEditing={() => setIsEditing(false)}
120
+ />
121
+ }
122
+ nodeId={nodeId}
123
+ onClick={handleClick}
124
+ onDoubleClick={() => focusTreeNode(nodeId)}
125
+ onMouseEnter={() => setHoveredId(nodeId)}
126
+ onMouseLeave={() => setHoveredId(null)}
127
+ onToggle={() => setExpanded((prev) => !prev)}
128
+ >
129
+ {hasChildren &&
130
+ children.map((childId, index) => (
131
+ <TreeNode
132
+ depth={depth + 1}
133
+ isLast={index === children.length - 1}
134
+ key={childId}
135
+ nodeId={childId as AnyNodeId}
136
+ />
137
+ ))}
138
+ </TreeNodeWrapper>
139
+ )
140
+ })
@@ -6,7 +6,7 @@ import { memo, useCallback, useEffect, useState } from 'react'
6
6
  import { useShallow } from 'zustand/react/shallow'
7
7
  import useEditor from '../../../../../store/use-editor'
8
8
  import { InlineRenameInput } from './inline-rename-input'
9
- import { focusTreeNode, handleTreeSelection, TreeNodeWrapper } from './tree-node'
9
+ import { focusTreeNode, handleTreeSelection, TreeNode, TreeNodeWrapper } from './tree-node'
10
10
  import { TreeNodeActions } from './tree-node-actions'
11
11
  import { DropIndicatorLine, useTreeNodeDrag } from './tree-node-drag'
12
12
 
@@ -28,6 +28,7 @@ export const RoofTreeNode = memo(function RoofTreeNode({
28
28
  const isHovered = useViewer((state) => state.hoveredId === nodeId)
29
29
  const setSelection = useViewer((state) => state.setSelection)
30
30
  const setHoveredId = useViewer((state) => state.setHoveredId)
31
+ const setRoofHostDragArmedId = useEditor((state) => state.setRoofHostDragArmedId)
31
32
  const { drag, dropTarget } = useTreeNodeDrag()
32
33
 
33
34
  const segments = useScene(
@@ -57,8 +58,11 @@ export const RoofTreeNode = memo(function RoofTreeNode({
57
58
  if (!handled && useEditor.getState().phase === 'furnish') {
58
59
  useEditor.getState().setPhase('structure')
59
60
  }
61
+ if (!e.metaKey && !e.ctrlKey && !e.shiftKey) {
62
+ setRoofHostDragArmedId(nodeId)
63
+ }
60
64
  },
61
- [nodeId, setSelection],
65
+ [nodeId, setRoofHostDragArmedId, setSelection],
62
66
  )
63
67
 
64
68
  const handleDoubleClick = useCallback(() => focusTreeNode(nodeId), [nodeId])
@@ -98,7 +102,7 @@ export const RoofTreeNode = memo(function RoofTreeNode({
98
102
  expanded={expanded}
99
103
  hasChildren={segments.length > 0}
100
104
  icon={
101
- <Image alt="" className="object-contain" height={14} src="/icons/roof.png" width={14} />
105
+ <Image alt="" className="object-contain" height={14} src="/icons/roof.webp" width={14} />
102
106
  }
103
107
  isDropTarget={isValidDropTarget && isDropTarget}
104
108
  isHovered={isHovered || isDropTarget}
@@ -163,12 +167,36 @@ function RoofSegmentTreeNode({
163
167
  isLast?: boolean
164
168
  }) {
165
169
  const [isEditing, setIsEditing] = useState(false)
170
+ const [expanded, setExpanded] = useState(true)
171
+ const accessoryIds = useScene(
172
+ useShallow(
173
+ (s) => (s.nodes[node.id] as RoofSegmentNode | undefined)?.children ?? ([] as string[]),
174
+ ),
175
+ )
166
176
  const isSelected = useViewer((state) => state.selection.selectedIds.includes(node.id))
167
177
  const isHovered = useViewer((state) => state.hoveredId === node.id)
168
178
  const setSelection = useViewer((state) => state.setSelection)
169
179
  const setHoveredId = useViewer((state) => state.setHoveredId)
170
180
  const { startDrag, isDragging } = useTreeNodeDrag()
171
181
 
182
+ useEffect(() => {
183
+ return useViewer.subscribe((state) => {
184
+ const { selectedIds } = state.selection
185
+ if (selectedIds.length === 0) return
186
+ const nodes = useScene.getState().nodes
187
+ for (const id of selectedIds) {
188
+ let current = nodes[id as AnyNodeId]
189
+ while (current?.parentId) {
190
+ if (current.parentId === node.id) {
191
+ setExpanded(true)
192
+ return
193
+ }
194
+ current = nodes[current.parentId as AnyNodeId]
195
+ }
196
+ }
197
+ })
198
+ }, [node.id])
199
+
172
200
  const handleClick = useCallback(
173
201
  (e: React.MouseEvent) => {
174
202
  if (isDragging) return
@@ -192,19 +220,21 @@ function RoofSegmentTreeNode({
192
220
 
193
221
  const defaultName = `${node.roofType.charAt(0).toUpperCase() + node.roofType.slice(1)} (${node.width.toFixed(1)}x${node.depth.toFixed(1)}m)`
194
222
 
223
+ const hasAccessories = accessoryIds.length > 0
224
+
195
225
  return (
196
226
  <div data-drop-child={node.id}>
197
227
  <TreeNodeWrapper
198
228
  actions={<TreeNodeActions nodeId={node.id} />}
199
229
  depth={depth}
200
- expanded={false}
201
- hasChildren={false}
230
+ expanded={expanded}
231
+ hasChildren={hasAccessories}
202
232
  icon={
203
233
  <Image
204
234
  alt=""
205
235
  className="object-contain opacity-60"
206
236
  height={14}
207
- src="/icons/roof.png"
237
+ src="/icons/roof.webp"
208
238
  width={14}
209
239
  />
210
240
  }
@@ -228,8 +258,18 @@ function RoofSegmentTreeNode({
228
258
  onMouseEnter={() => setHoveredId(node.id)}
229
259
  onMouseLeave={() => setHoveredId(null)}
230
260
  onPointerDown={handlePointerDown}
231
- onToggle={() => {}}
232
- />
261
+ onToggle={() => setExpanded((prev) => !prev)}
262
+ >
263
+ {hasAccessories &&
264
+ accessoryIds.map((childId, index) => (
265
+ <TreeNode
266
+ depth={depth + 1}
267
+ isLast={index === accessoryIds.length - 1}
268
+ key={childId}
269
+ nodeId={childId as AnyNodeId}
270
+ />
271
+ ))}
272
+ </TreeNodeWrapper>
233
273
  </div>
234
274
  )
235
275
  }