@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,132 @@
1
+ 'use client'
2
+
3
+ import { type AnyNodeId, type ShelfNode, useScene } from '@pascal-app/core'
4
+ import { useViewer } from '@pascal-app/viewer'
5
+ import Image from 'next/image'
6
+ import { memo, useCallback, useEffect, useState } from 'react'
7
+ import { useShallow } from 'zustand/react/shallow'
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
+
18
+ interface ShelfTreeNodeProps {
19
+ nodeId: ShelfNode['id']
20
+ depth: number
21
+ isLast?: boolean
22
+ }
23
+
24
+ /**
25
+ * Sidebar tree entry for shelf. Mirrors `item-tree-node`'s shape so the
26
+ * shelf's hosted items list as collapsible children — same pattern items
27
+ * use for their nested items. The shelf has its own `children: ItemNode[`id`]`
28
+ * field on the schema; items reparent into it via `def.surfaces` + the
29
+ * placement coordinator's shelf strategy.
30
+ */
31
+ export const ShelfTreeNode = memo(function ShelfTreeNode({
32
+ nodeId,
33
+ depth,
34
+ isLast,
35
+ }: ShelfTreeNodeProps) {
36
+ const [isEditing, setIsEditing] = useState(false)
37
+ const [expanded, setExpanded] = useState(true)
38
+ const isVisible = useScene((s) => s.nodes[nodeId]?.visible !== false)
39
+ const children = useScene(
40
+ useShallow((s) => (s.nodes[nodeId] as ShelfNode | undefined)?.children ?? []),
41
+ )
42
+ const node = useScene((s) => s.nodes[nodeId] as ShelfNode | undefined)
43
+ const isSelected = useViewer((state) => state.selection.selectedIds.includes(nodeId))
44
+ const isHovered = useViewer((state) => state.hoveredId === nodeId)
45
+ const setSelection = useViewer((state) => state.setSelection)
46
+ const setHoveredId = useViewer((state) => state.setHoveredId)
47
+
48
+ // Expand when a descendant is selected — same imperative subscription
49
+ // the item tree-node uses, so we don't re-render when unrelated
50
+ // selection-state ticks.
51
+ useEffect(() => {
52
+ return useViewer.subscribe((state) => {
53
+ const { selectedIds } = state.selection
54
+ if (selectedIds.length === 0) return
55
+ const nodes = useScene.getState().nodes
56
+ for (const id of selectedIds) {
57
+ let current = nodes[id as AnyNodeId]
58
+ while (current?.parentId) {
59
+ if (current.parentId === nodeId) {
60
+ setExpanded(true)
61
+ return
62
+ }
63
+ current = nodes[current.parentId as AnyNodeId]
64
+ }
65
+ }
66
+ })
67
+ }, [nodeId])
68
+
69
+ const handleClick = useCallback(
70
+ (e: React.MouseEvent) => {
71
+ e.stopPropagation()
72
+ handleTreeSelection(
73
+ e,
74
+ nodeId,
75
+ useViewer.getState().selection.selectedIds,
76
+ setSelection,
77
+ )
78
+ routeTreeSelectionToNode(node)
79
+ },
80
+ [node, nodeId, setSelection],
81
+ )
82
+
83
+ const handleDoubleClick = useCallback(() => focusTreeNode(nodeId), [nodeId])
84
+ const handleMouseEnter = useCallback(() => setHoveredId(nodeId), [nodeId, setHoveredId])
85
+ const handleMouseLeave = useCallback(() => setHoveredId(null), [setHoveredId])
86
+ const handleToggle = useCallback(() => setExpanded((prev) => !prev), [])
87
+ const handleStartEditing = useCallback(() => setIsEditing(true), [])
88
+ const handleStopEditing = useCallback(() => setIsEditing(false), [])
89
+
90
+ const hasChildren = children.length > 0
91
+
92
+ return (
93
+ <TreeNodeWrapper
94
+ actions={<TreeNodeActions nodeId={nodeId} />}
95
+ depth={depth}
96
+ expanded={expanded}
97
+ hasChildren={hasChildren}
98
+ icon={
99
+ <Image alt="" className="object-contain" height={14} src="/icons/shelf.webp" width={14} />
100
+ }
101
+ isHovered={isHovered}
102
+ isLast={isLast}
103
+ isSelected={isSelected}
104
+ isVisible={isVisible}
105
+ label={
106
+ <InlineRenameInput
107
+ defaultName="Shelf"
108
+ isEditing={isEditing}
109
+ nodeId={nodeId}
110
+ onStartEditing={handleStartEditing}
111
+ onStopEditing={handleStopEditing}
112
+ />
113
+ }
114
+ nodeId={nodeId}
115
+ onClick={handleClick}
116
+ onDoubleClick={handleDoubleClick}
117
+ onMouseEnter={handleMouseEnter}
118
+ onMouseLeave={handleMouseLeave}
119
+ onToggle={handleToggle}
120
+ >
121
+ {hasChildren &&
122
+ children.map((childId, index) => (
123
+ <TreeNode
124
+ depth={depth + 1}
125
+ isLast={index === children.length - 1}
126
+ key={childId}
127
+ nodeId={childId}
128
+ />
129
+ ))}
130
+ </TreeNodeWrapper>
131
+ )
132
+ })
@@ -2,6 +2,7 @@ import { type AnyNodeId, type SlabNode, useScene } from '@pascal-app/core'
2
2
  import { useViewer } from '@pascal-app/viewer'
3
3
  import Image from 'next/image'
4
4
  import { memo, useCallback, useState } from 'react'
5
+ import { formatAreaLabel } from './../../../../../lib/measurements'
5
6
  import useEditor from './../../../../../store/use-editor'
6
7
  import { InlineRenameInput } from './inline-rename-input'
7
8
  import { focusTreeNode, handleTreeSelection, TreeNodeWrapper } from './tree-node'
@@ -25,6 +26,7 @@ export const SlabTreeNode = memo(function SlabTreeNode({
25
26
  const isHovered = useViewer((state) => state.hoveredId === nodeId)
26
27
  const setSelection = useViewer((state) => state.setSelection)
27
28
  const setHoveredId = useViewer((state) => state.setHoveredId)
29
+ const unit = useViewer((state) => state.unit)
28
30
 
29
31
  const handleClick = useCallback(
30
32
  (e: React.MouseEvent) => {
@@ -45,8 +47,7 @@ export const SlabTreeNode = memo(function SlabTreeNode({
45
47
  const handleStartEditing = useCallback(() => setIsEditing(true), [])
46
48
  const handleStopEditing = useCallback(() => setIsEditing(false), [])
47
49
 
48
- const area = calculatePolygonArea(polygon).toFixed(1)
49
- const defaultName = `Slab (${area}m²)`
50
+ const defaultName = `Slab (${formatAreaLabel(calculatePolygonArea(polygon), unit)})`
50
51
 
51
52
  return (
52
53
  <TreeNodeWrapper
@@ -55,7 +56,7 @@ export const SlabTreeNode = memo(function SlabTreeNode({
55
56
  expanded={false}
56
57
  hasChildren={false}
57
58
  icon={
58
- <Image alt="" className="object-contain" height={14} src="/icons/floor.png" width={14} />
59
+ <Image alt="" className="object-contain" height={14} src="/icons/floor.webp" width={14} />
59
60
  }
60
61
  isHovered={isHovered}
61
62
  isLast={isLast}
@@ -91,8 +92,11 @@ function calculatePolygonArea(polygon: Array<[number, number]>): number {
91
92
 
92
93
  for (let i = 0; i < n; i++) {
93
94
  const j = (i + 1) % n
94
- area += polygon[i]?.[0] * polygon[j]?.[1]
95
- area -= polygon[j]?.[0] * polygon[i]?.[1]
95
+ const pi = polygon[i]
96
+ const pj = polygon[j]
97
+ if (!(pi && pj)) continue
98
+ area += pi[0] * pj[1]
99
+ area -= pj[0] * pi[1]
96
100
  }
97
101
 
98
102
  return Math.abs(area) / 2
@@ -0,0 +1,86 @@
1
+ import { type AnyNodeId, type SolarPanelNode, 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 SolarPanelTreeNodeProps {
12
+ nodeId: AnyNodeId
13
+ depth: number
14
+ isLast?: boolean
15
+ }
16
+
17
+ export const SolarPanelTreeNode = memo(function SolarPanelTreeNode({
18
+ nodeId,
19
+ depth,
20
+ isLast,
21
+ }: SolarPanelTreeNodeProps) {
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 SolarPanelNode | 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 || 'Solar Panel'
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
+ })
@@ -50,7 +50,13 @@ export const SpawnTreeNode = memo(function SpawnTreeNode({
50
50
  expanded={false}
51
51
  hasChildren={false}
52
52
  icon={
53
- <Image alt="" className="object-contain" height={14} src="/icons/site.png" width={14} />
53
+ <Image
54
+ alt=""
55
+ className="object-contain"
56
+ height={14}
57
+ src="/icons/spawn-point.webp"
58
+ width={14}
59
+ />
54
60
  }
55
61
  isHovered={isHovered}
56
62
  isLast={isLast}
@@ -98,7 +98,7 @@ export const StairTreeNode = memo(function StairTreeNode({
98
98
  expanded={expanded}
99
99
  hasChildren={segments.length > 0}
100
100
  icon={
101
- <Image alt="" className="object-contain" height={14} src="/icons/stairs.png" width={14} />
101
+ <Image alt="" className="object-contain" height={14} src="/icons/stairs.webp" width={14} />
102
102
  }
103
103
  isDropTarget={isValidDropTarget && isDropTarget}
104
104
  isHovered={isHovered || isDropTarget}
@@ -206,7 +206,7 @@ function StairSegmentTreeNode({
206
206
  alt=""
207
207
  className="object-contain opacity-60"
208
208
  height={14}
209
- src="/icons/stairs.png"
209
+ src="/icons/stairs.webp"
210
210
  width={14}
211
211
  />
212
212
  }
@@ -1,7 +1,10 @@
1
- import { type AnyNodeId, emitter, useScene } from '@pascal-app/core'
1
+ import { type AnyNode, type AnyNodeId, emitter, nodeRegistry, useScene } from '@pascal-app/core'
2
+ import { useViewer } from '@pascal-app/viewer'
2
3
  import { ChevronRight } from 'lucide-react'
3
4
  import { AnimatePresence, motion } from 'motion/react'
4
5
  import { forwardRef, memo, useEffect, useRef } from 'react'
6
+ import { resolveNodeSelectionTarget } from '../../../../../lib/selection-routing'
7
+ import useEditor from '../../../../../store/use-editor'
5
8
 
6
9
  export function handleTreeSelection(
7
10
  e: React.MouseEvent,
@@ -53,16 +56,48 @@ export function focusTreeNode(nodeId: AnyNodeId) {
53
56
  emitter.emit('camera-controls:focus', { nodeId })
54
57
  }
55
58
 
59
+ export function routeTreeSelectionToNode(node: AnyNode | null | undefined) {
60
+ const target = node ? resolveNodeSelectionTarget(node) : null
61
+ if (!target) return
62
+
63
+ const selectedIdsAfterClick = useViewer.getState().selection.selectedIds
64
+ const editor = useEditor.getState()
65
+ let didRoute = false
66
+
67
+ if (target.phase !== editor.phase) {
68
+ editor.setPhase(target.phase)
69
+ didRoute = true
70
+ }
71
+ if (
72
+ target.phase === 'structure' &&
73
+ target.structureLayer &&
74
+ target.structureLayer !== useEditor.getState().structureLayer
75
+ ) {
76
+ useEditor.getState().setStructureLayer(target.structureLayer)
77
+ didRoute = true
78
+ }
79
+ if (didRoute) {
80
+ useViewer.getState().setSelection({ selectedIds: selectedIdsAfterClick })
81
+ }
82
+ }
83
+
56
84
  import { cn } from '../../../../../lib/utils'
57
85
  import { BuildingTreeNode } from './building-tree-node'
58
86
  import { CeilingTreeNode } from './ceiling-tree-node'
87
+ import { ChimneyTreeNode } from './chimney-tree-node'
59
88
  import { ColumnTreeNode } from './column-tree-node'
60
89
  import { DoorTreeNode } from './door-tree-node'
90
+ import { DormerTreeNode } from './dormer-tree-node'
91
+ import { ElevatorTreeNode } from './elevator-tree-node'
61
92
  import { FenceTreeNode } from './fence-tree-node'
93
+ import { GutterTreeNode } from './gutter-tree-node'
62
94
  import { ItemTreeNode } from './item-tree-node'
63
95
  import { LevelTreeNode } from './level-tree-node'
96
+ import { RegistryTreeNode } from './registry-tree-node'
64
97
  import { RoofTreeNode } from './roof-tree-node'
98
+ import { ShelfTreeNode } from './shelf-tree-node'
65
99
  import { SlabTreeNode } from './slab-tree-node'
100
+ import { SolarPanelTreeNode } from './solar-panel-tree-node'
66
101
  import { SpawnTreeNode } from './spawn-tree-node'
67
102
  import { StairTreeNode } from './stair-tree-node'
68
103
  import { WallTreeNode } from './wall-tree-node'
@@ -75,45 +110,83 @@ interface TreeNodeProps {
75
110
  isLast?: boolean
76
111
  }
77
112
 
113
+ // Per-kind tree-node components keyed by `node.type`. Lookup replaces
114
+ // the legacy switch — adding a kind to this map is now the only edit
115
+ // needed in this file (the switch's `case '<kind>':` clauses were
116
+ // flagged by the Phase 6 grep gate as the last per-kind dispatch
117
+ // outside the registry; future work moves these to a
118
+ // `def.presentation`-driven generic tree-node and removes this map
119
+ // entirely).
120
+ const treeNodeByType: Record<
121
+ string,
122
+ React.ComponentType<{ depth: number; isLast?: boolean; nodeId: AnyNodeId }>
123
+ > = {
124
+ building: BuildingTreeNode as React.ComponentType<{
125
+ depth: number
126
+ isLast?: boolean
127
+ nodeId: AnyNodeId
128
+ }>,
129
+ cabinet: RegistryTreeNode,
130
+ 'cabinet-module': RegistryTreeNode,
131
+ 'box-vent': RegistryTreeNode,
132
+ ceiling: CeilingTreeNode,
133
+ chimney: ChimneyTreeNode,
134
+ dormer: DormerTreeNode,
135
+ downspout: RegistryTreeNode,
136
+ 'solar-panel': SolarPanelTreeNode,
137
+ column: ColumnTreeNode,
138
+ elevator: ElevatorTreeNode,
139
+ level: LevelTreeNode as React.ComponentType<{
140
+ depth: number
141
+ isLast?: boolean
142
+ nodeId: AnyNodeId
143
+ }>,
144
+ shelf: ShelfTreeNode as React.ComponentType<{
145
+ depth: number
146
+ isLast?: boolean
147
+ nodeId: AnyNodeId
148
+ }>,
149
+ slab: SlabTreeNode,
150
+ spawn: SpawnTreeNode as React.ComponentType<{
151
+ depth: number
152
+ isLast?: boolean
153
+ nodeId: AnyNodeId
154
+ }>,
155
+ wall: WallTreeNode,
156
+ fence: FenceTreeNode,
157
+ gutter: GutterTreeNode,
158
+ measurement: RegistryTreeNode,
159
+ 'ridge-vent': RegistryTreeNode,
160
+ 'turbine-vent': RegistryTreeNode,
161
+ cupola: RegistryTreeNode,
162
+ 'eyebrow-vent': RegistryTreeNode,
163
+ skylight: RegistryTreeNode,
164
+ roof: RoofTreeNode,
165
+ stair: StairTreeNode,
166
+ door: DoorTreeNode,
167
+ window: WindowTreeNode,
168
+ zone: ZoneTreeNode as React.ComponentType<{
169
+ depth: number
170
+ isLast?: boolean
171
+ nodeId: AnyNodeId
172
+ }>,
173
+ item: ItemTreeNode,
174
+ }
175
+
78
176
  export const TreeNode = memo(function TreeNode({ nodeId, depth = 0, isLast }: TreeNodeProps) {
177
+ // Registry-driven row hiding (`def.tree.hidden`) — primitive boolean
178
+ // selector so unrelated scene updates don't re-render every row.
179
+ const shouldHide = useScene((state) => {
180
+ const node = state.nodes[nodeId]
181
+ if (!node) return false
182
+ return nodeRegistry.get(node.type)?.tree?.hidden?.(node, state.nodes) ?? false
183
+ })
79
184
  const nodeType = useScene((state) => state.nodes[nodeId]?.type)
80
-
185
+ if (shouldHide) return null
81
186
  if (!nodeType) return null
82
-
83
- switch (nodeType) {
84
- case 'building':
85
- return (
86
- <BuildingTreeNode depth={depth} isLast={isLast} nodeId={nodeId as `building_${string}`} />
87
- )
88
- case 'ceiling':
89
- return <CeilingTreeNode depth={depth} isLast={isLast} nodeId={nodeId} />
90
- case 'column':
91
- return <ColumnTreeNode depth={depth} isLast={isLast} nodeId={nodeId} />
92
- case 'level':
93
- return <LevelTreeNode depth={depth} isLast={isLast} nodeId={nodeId as `level_${string}`} />
94
- case 'slab':
95
- return <SlabTreeNode depth={depth} isLast={isLast} nodeId={nodeId} />
96
- case 'spawn':
97
- return <SpawnTreeNode depth={depth} isLast={isLast} nodeId={nodeId as `spawn_${string}`} />
98
- case 'wall':
99
- return <WallTreeNode depth={depth} isLast={isLast} nodeId={nodeId} />
100
- case 'fence':
101
- return <FenceTreeNode depth={depth} isLast={isLast} nodeId={nodeId} />
102
- case 'roof':
103
- return <RoofTreeNode depth={depth} isLast={isLast} nodeId={nodeId} />
104
- case 'stair':
105
- return <StairTreeNode depth={depth} isLast={isLast} nodeId={nodeId} />
106
- case 'item':
107
- return <ItemTreeNode depth={depth} isLast={isLast} nodeId={nodeId} />
108
- case 'door':
109
- return <DoorTreeNode depth={depth} isLast={isLast} nodeId={nodeId} />
110
- case 'window':
111
- return <WindowTreeNode depth={depth} isLast={isLast} nodeId={nodeId} />
112
- case 'zone':
113
- return <ZoneTreeNode depth={depth} isLast={isLast} nodeId={nodeId as `zone_${string}`} />
114
- default:
115
- return null
116
- }
187
+ const Component = treeNodeByType[nodeType]
188
+ if (!Component) return null
189
+ return <Component depth={depth} isLast={isLast} nodeId={nodeId} />
117
190
  })
118
191
 
119
192
  interface TreeNodeWrapperProps {
@@ -234,10 +307,10 @@ export const TreeNodeWrapper = forwardRef<HTMLDivElement, TreeNodeWrapperProps>(
234
307
  </motion.div>
235
308
  ) : null}
236
309
  </button>
237
- <div className="flex min-w-0 flex-1 items-center gap-1.5">
310
+ <div className="flex min-w-0 flex-1 items-center gap-2">
238
311
  <span
239
312
  className={cn(
240
- 'flex h-4 w-4 shrink-0 items-center justify-center transition-all duration-200',
313
+ 'flex h-5 w-5 shrink-0 items-center justify-center transition-all duration-200',
241
314
  !isSelected && 'opacity-60 grayscale',
242
315
  )}
243
316
  >
@@ -0,0 +1,36 @@
1
+ import { type AnyNode, type AnyNodeId, nodeRegistry } from '@pascal-app/core'
2
+
3
+ /**
4
+ * Child ids the sidebar tree renders under a node: the kind's
5
+ * `def.tree.childIds` override when declared, otherwise the node's own
6
+ * `children`. Kind-agnostic — kinds that reshape their subtree (hidden
7
+ * derived nodes, flattened containers) do so via the registry hook.
8
+ */
9
+ export function resolveTreeChildIds(
10
+ nodeId: AnyNodeId,
11
+ nodes: Readonly<Partial<Record<AnyNodeId, AnyNode>>>,
12
+ ): AnyNodeId[] {
13
+ const node = nodes[nodeId]
14
+ if (!node) return []
15
+ const childIds = nodeRegistry.get(node.type)?.tree?.childIds
16
+ if (childIds) return childIds(node, nodes)
17
+ const children = (node as { children?: unknown }).children
18
+ return Array.isArray(children) ? (children as AnyNodeId[]) : []
19
+ }
20
+
21
+ /**
22
+ * Whether `targetId` appears anywhere under `nodeId` in the *rendered*
23
+ * sidebar tree (i.e. walking registry-overridden child ids, not the raw
24
+ * scene graph). Drives auto-expansion when a descendant gets selected.
25
+ */
26
+ export function treeContainsDescendant(
27
+ nodeId: AnyNodeId,
28
+ targetId: AnyNodeId,
29
+ nodes: Readonly<Partial<Record<AnyNodeId, AnyNode>>>,
30
+ ): boolean {
31
+ for (const childId of resolveTreeChildIds(nodeId, nodes)) {
32
+ if (childId === targetId) return true
33
+ if (treeContainsDescendant(childId, targetId, nodes)) return true
34
+ }
35
+ return false
36
+ }
@@ -79,7 +79,7 @@ export const WallTreeNode = memo(function WallTreeNode({
79
79
  expanded={expanded}
80
80
  hasChildren={children.length > 0}
81
81
  icon={
82
- <Image alt="" className="object-contain" height={14} src="/icons/wall.png" width={14} />
82
+ <Image alt="" className="object-contain" height={14} src="/icons/wall.webp" width={14} />
83
83
  }
84
84
  isHovered={isHovered}
85
85
  isLast={isLast}
@@ -1,9 +1,10 @@
1
1
  'use client'
2
2
 
3
- import { type AnyNodeId, useScene } from '@pascal-app/core'
3
+ import { type AnyNodeId, type WindowNode, useScene } from '@pascal-app/core'
4
4
  import { useViewer } from '@pascal-app/viewer'
5
5
  import Image from 'next/image'
6
6
  import { memo, useCallback, useState } from 'react'
7
+ import { resolveNodeSnapTarget, SnapTargetIcon } from '../../../snap-target-badge'
7
8
  import useEditor from './../../../../../store/use-editor'
8
9
  import { InlineRenameInput } from './inline-rename-input'
9
10
  import { focusTreeNode, handleTreeSelection, TreeNodeWrapper } from './tree-node'
@@ -22,6 +23,7 @@ export const WindowTreeNode = memo(function WindowTreeNode({
22
23
  }: WindowTreeNodeProps) {
23
24
  const [isEditing, setIsEditing] = useState(false)
24
25
  const isVisible = useScene((s) => s.nodes[nodeId as AnyNodeId]?.visible !== false)
26
+ const node = useScene((s) => s.nodes[nodeId] as WindowNode | undefined)
25
27
  const isSelected = useViewer((state) => state.selection.selectedIds.includes(nodeId))
26
28
  const isHovered = useViewer((state) => state.hoveredId === nodeId)
27
29
  const setSelection = useViewer((state) => state.setSelection)
@@ -45,6 +47,7 @@ export const WindowTreeNode = memo(function WindowTreeNode({
45
47
 
46
48
  const handleStartEditing = useCallback(() => setIsEditing(true), [])
47
49
  const handleStopEditing = useCallback(() => setIsEditing(false), [])
50
+ const snapTarget = resolveNodeSnapTarget(node) ?? 'wall'
48
51
 
49
52
  return (
50
53
  <TreeNodeWrapper
@@ -53,7 +56,9 @@ export const WindowTreeNode = memo(function WindowTreeNode({
53
56
  expanded={false}
54
57
  hasChildren={false}
55
58
  icon={
56
- <Image alt="" className="object-contain" height={14} src="/icons/window.png" width={14} />
59
+ <SnapTargetIcon target={snapTarget}>
60
+ <Image alt="" className="object-contain" height={14} src="/icons/window.webp" width={14} />
61
+ </SnapTargetIcon>
57
62
  }
58
63
  isHovered={isHovered}
59
64
  isLast={isLast}
@@ -2,6 +2,7 @@ import { useScene, type ZoneNode } from '@pascal-app/core'
2
2
  import { useViewer } from '@pascal-app/viewer'
3
3
  import { memo, useCallback, useState } from 'react'
4
4
  import { ColorDot } from './../../../../../components/ui/primitives/color-dot'
5
+ import { formatAreaLabel } from './../../../../../lib/measurements'
5
6
  import { InlineRenameInput } from './inline-rename-input'
6
7
  import { focusTreeNode, TreeNodeWrapper } from './tree-node'
7
8
  import { TreeNodeActions } from './tree-node-actions'
@@ -26,6 +27,7 @@ export const ZoneTreeNode = memo(function ZoneTreeNode({
26
27
  const isHovered = useViewer((state) => state.hoveredId === nodeId)
27
28
  const setSelection = useViewer((state) => state.setSelection)
28
29
  const setHoveredId = useViewer((state) => state.setHoveredId)
30
+ const unit = useViewer((state) => state.unit)
29
31
 
30
32
  const handleClick = useCallback(() => setSelection({ zoneId: nodeId }), [nodeId, setSelection])
31
33
  const handleDoubleClick = useCallback(() => focusTreeNode(nodeId), [nodeId])
@@ -34,9 +36,7 @@ export const ZoneTreeNode = memo(function ZoneTreeNode({
34
36
  const handleStartEditing = useCallback(() => setIsEditing(true), [])
35
37
  const handleStopEditing = useCallback(() => setIsEditing(false), [])
36
38
 
37
- // Calculate approximate area from polygon
38
- const area = calculatePolygonArea(polygon).toFixed(1)
39
- const defaultName = `Zone (${area}m²)`
39
+ const defaultName = `Zone (${formatAreaLabel(calculatePolygonArea(polygon), unit)})`
40
40
 
41
41
  return (
42
42
  <TreeNodeWrapper