@pascal-app/editor 0.9.1 → 1.0.0-beta.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (366) hide show
  1. package/package.json +14 -9
  2. package/src/components/editor/alignment-3d-guide-layer.tsx +10 -2
  3. package/src/components/editor/bake-exporter.tsx +49 -0
  4. package/src/components/editor/bake-thumbnail.tsx +95 -0
  5. package/src/components/editor/camera-dragging-lifecycle.test.ts +49 -0
  6. package/src/components/editor/camera-dragging-lifecycle.ts +41 -0
  7. package/src/components/editor/custom-camera-controls.tsx +332 -199
  8. package/src/components/editor/delete-confirmation-dialog.tsx +54 -0
  9. package/src/components/editor/editor-layout-mobile.tsx +5 -0
  10. package/src/components/editor/editor-layout-v2.tsx +18 -0
  11. package/src/components/editor/elevation-3d-guide-layer.tsx +120 -0
  12. package/src/components/editor/export-manager.tsx +71 -63
  13. package/src/components/editor/fence-tangent-lines-3d.tsx +87 -0
  14. package/src/components/editor/first-person/build-collider-world.test.ts +102 -5
  15. package/src/components/editor/first-person/build-collider-world.ts +52 -5
  16. package/src/components/editor/first-person/terrain-collider.test.ts +132 -0
  17. package/src/components/editor/first-person/terrain-collider.ts +212 -0
  18. package/src/components/editor/first-person-controls.tsx +410 -136
  19. package/src/components/editor/floating-action-menu.tsx +435 -64
  20. package/src/components/editor/floating-building-action-menu.tsx +1 -1
  21. package/src/components/editor/floorplan-background-selection.test.ts +72 -0
  22. package/src/components/editor/floorplan-background-selection.ts +7 -20
  23. package/src/components/editor/floorplan-camera-sync.test.ts +249 -0
  24. package/src/components/editor/floorplan-camera-sync.ts +213 -0
  25. package/src/components/editor/floorplan-mode-coordinator.tsx +82 -0
  26. package/src/components/editor/floorplan-navigation-presentation.test.ts +142 -0
  27. package/src/components/editor/floorplan-navigation-presentation.ts +156 -0
  28. package/src/components/editor/floorplan-panel.tsx +2517 -1492
  29. package/src/components/editor/grid.tsx +166 -39
  30. package/src/components/editor/group-actions.ts +597 -0
  31. package/src/components/editor/group-floating-action-menu.tsx +130 -0
  32. package/src/components/editor/group-move-3d.ts +397 -0
  33. package/src/components/editor/group-rotate-handle.tsx +102 -64
  34. package/src/components/editor/group-selection-box-3d.tsx +173 -0
  35. package/src/components/editor/group-transform-shared.test.ts +192 -1
  36. package/src/components/editor/group-transform-shared.ts +240 -7
  37. package/src/components/editor/handles/handle-arrow.tsx +81 -23
  38. package/src/components/editor/handles/handle-drag-history.test.ts +96 -0
  39. package/src/components/editor/handles/handle-drag-history.ts +14 -0
  40. package/src/components/editor/handles/preview-overrides.test.ts +26 -0
  41. package/src/components/editor/handles/preview-overrides.ts +13 -0
  42. package/src/components/editor/handles/resize-snap.test.ts +52 -0
  43. package/src/components/editor/handles/resize-snap.ts +23 -0
  44. package/src/components/editor/handles/use-handle-drag.ts +49 -6
  45. package/src/components/editor/index.tsx +289 -35
  46. package/src/components/editor/measurement-pill.tsx +68 -23
  47. package/src/components/editor/node-action-menu.tsx +14 -1
  48. package/src/components/editor/node-arrow-handles.tsx +409 -180
  49. package/src/components/editor/opening-guides-3d-layer.tsx +144 -0
  50. package/src/components/editor/quick-measurement-card.tsx +76 -0
  51. package/src/components/editor/quick-measurement-hud.tsx +34 -0
  52. package/src/components/editor/riser-diagram-panel.tsx +137 -0
  53. package/src/components/editor/selection-manager.tsx +814 -466
  54. package/src/components/editor/site-edge-labels.tsx +28 -6
  55. package/src/components/editor/slab-hole-highlights.tsx +13 -5
  56. package/src/components/editor/snapshot-capture-overlay.tsx +258 -115
  57. package/src/components/editor/three-context-bridge.ts +22 -0
  58. package/src/components/editor/thumbnail-generator.tsx +118 -253
  59. package/src/components/editor/use-floorplan-background-placement.ts +113 -45
  60. package/src/components/editor/use-floorplan-hit-testing.test.ts +82 -0
  61. package/src/components/editor/use-floorplan-hit-testing.ts +144 -161
  62. package/src/components/editor/use-floorplan-scene-data.ts +5 -5
  63. package/src/components/editor/use-mesh-settle-epoch.ts +27 -0
  64. package/src/components/editor/wall-measurement-label.tsx +10 -28
  65. package/src/components/editor/wall-move-side-handles.tsx +229 -39
  66. package/src/components/editor/wall-opening-highlights.tsx +63 -32
  67. package/src/components/editor/wall-snap-beacon-layer.tsx +162 -6
  68. package/src/components/editor-2d/floorplan-action-menu-layer.tsx +9 -5
  69. package/src/components/editor-2d/floorplan-alignment-guide-layer.tsx +2 -1
  70. package/src/components/editor-2d/floorplan-cursor-indicator-overlay.tsx +40 -9
  71. package/src/components/editor-2d/floorplan-cursor-indicator-position.test.ts +31 -0
  72. package/src/components/editor-2d/floorplan-cursor-indicator-position.ts +39 -0
  73. package/src/components/editor-2d/floorplan-group-action-menu.tsx +87 -0
  74. package/src/components/editor-2d/floorplan-group-move.tsx +744 -0
  75. package/src/components/editor-2d/floorplan-measurement-tool-layer.test.ts +113 -0
  76. package/src/components/editor-2d/floorplan-measurement-tool-layer.tsx +1672 -0
  77. package/src/components/editor-2d/floorplan-quick-measure-layer.tsx +212 -0
  78. package/src/components/editor-2d/floorplan-registered-tool-layer.tsx +72 -0
  79. package/src/components/editor-2d/floorplan-registry-action-menu.tsx +219 -20
  80. package/src/components/editor-2d/floorplan-registry-move-overlay.tsx +327 -59
  81. package/src/components/editor-2d/floorplan-render-context.test.ts +27 -0
  82. package/src/components/editor-2d/floorplan-render-context.tsx +113 -8
  83. package/src/components/editor-2d/floorplan-snap-beacon-layer.tsx +5 -3
  84. package/src/components/editor-2d/renderers/floorplan-annotation-layout.test.ts +285 -0
  85. package/src/components/editor-2d/renderers/floorplan-annotation-layout.ts +516 -0
  86. package/src/components/editor-2d/renderers/floorplan-dimension-renderer.test.tsx +236 -0
  87. package/src/components/editor-2d/renderers/floorplan-dimension-renderer.tsx +618 -0
  88. package/src/components/editor-2d/renderers/floorplan-geometry-renderer.test.tsx +369 -0
  89. package/src/components/editor-2d/renderers/floorplan-geometry-renderer.tsx +431 -32
  90. package/src/components/editor-2d/renderers/floorplan-label-angle.test.ts +89 -0
  91. package/src/components/editor-2d/renderers/floorplan-label-angle.ts +106 -0
  92. package/src/components/editor-2d/renderers/floorplan-placement-preview-layer.tsx +94 -26
  93. package/src/components/editor-2d/renderers/floorplan-registry-layer.test.ts +587 -0
  94. package/src/components/editor-2d/renderers/floorplan-registry-layer.tsx +2280 -578
  95. package/src/components/editor-2d/renderers/floorplan-stair-layer.tsx +20 -10
  96. package/src/components/editor-2d/renderers/floorplan-voronoi-layer.tsx +128 -0
  97. package/src/components/systems/ceiling/ceiling-selection-affordance-system.tsx +425 -61
  98. package/src/components/systems/ceiling/ceiling-system.tsx +97 -4
  99. package/src/components/systems/roof/roof-edit-system.tsx +1768 -13
  100. package/src/components/systems/selection-affordance-manager.tsx +38 -0
  101. package/src/components/systems/zone/zone-label-editor-system.tsx +14 -1
  102. package/src/components/systems/zone/zone-system.tsx +42 -13
  103. package/src/components/tools/elevator/elevator-tool.tsx +34 -8
  104. package/src/components/tools/elevator/move-elevator-tool.tsx +13 -2
  105. package/src/components/tools/fence/fence-drafting.ts +107 -8
  106. package/src/components/tools/item/move-tool.tsx +17 -13
  107. package/src/components/tools/item/placement-math.test.ts +13 -1
  108. package/src/components/tools/item/placement-math.ts +28 -2
  109. package/src/components/tools/item/placement-strategies.ts +246 -5
  110. package/src/components/tools/item/placement-types.ts +13 -1
  111. package/src/components/tools/item/use-draft-node.ts +146 -57
  112. package/src/components/tools/item/use-placement-coordinator.tsx +813 -125
  113. package/src/components/tools/registry/move-registry-node-tool.tsx +636 -103
  114. package/src/components/tools/roof/roof-tool.tsx +336 -94
  115. package/src/components/tools/select/box-select-state.ts +18 -7
  116. package/src/components/tools/select/box-select-tool.tsx +176 -40
  117. package/src/components/tools/select/marquee-geometry.test.ts +161 -0
  118. package/src/components/tools/select/marquee-geometry.ts +155 -0
  119. package/src/components/tools/select/plane-box-select-tool.tsx +1 -8
  120. package/src/components/tools/select/select-candidates.ts +1 -1
  121. package/src/components/tools/shared/cursor-sphere.tsx +62 -26
  122. package/src/components/tools/shared/drag-bounding-box.tsx +28 -4
  123. package/src/components/tools/shared/facing-indicator.tsx +87 -0
  124. package/src/components/tools/shared/facing-pose-indicator.tsx +54 -0
  125. package/src/components/tools/shared/floor-stack-preview.ts +4 -0
  126. package/src/components/tools/shared/horizontal-construction-plane.ts +123 -0
  127. package/src/components/tools/shared/placement-box.tsx +183 -1
  128. package/src/components/tools/shared/pointer-support-cap.test.ts +98 -0
  129. package/src/components/tools/shared/pointer-support-cap.ts +273 -0
  130. package/src/components/tools/shared/polygon-editor.tsx +276 -35
  131. package/src/components/tools/site/site-boundary-editor.tsx +101 -42
  132. package/src/components/tools/site/stroke-pointer-ownership.test.ts +91 -0
  133. package/src/components/tools/site/stroke-pointer-ownership.ts +57 -0
  134. package/src/components/tools/site/terrain-brush-cursor.tsx +247 -0
  135. package/src/components/tools/site/terrain-sculpt-grid.tsx +192 -0
  136. package/src/components/tools/site/terrain-sculpt-tool.tsx +356 -0
  137. package/src/components/tools/stair/stair-click-guard.test.ts +58 -0
  138. package/src/components/tools/stair/stair-click-guard.ts +72 -0
  139. package/src/components/tools/stair/stair-defaults.ts +1 -0
  140. package/src/components/tools/stair/stair-tool.tsx +164 -42
  141. package/src/components/tools/tool-manager.tsx +149 -30
  142. package/src/components/tools/wall/wall-drafting.test.ts +447 -0
  143. package/src/components/tools/wall/wall-drafting.ts +304 -108
  144. package/src/components/tools/wall/wall-snap-geometry.test.ts +52 -0
  145. package/src/components/tools/wall/wall-snap-geometry.ts +137 -9
  146. package/src/components/tools/zone/zone-boundary-editor.tsx +5 -1
  147. package/src/components/tools/zone/zone-tool.tsx +98 -88
  148. package/src/components/ui/action-menu/camera-actions.tsx +24 -17
  149. package/src/components/ui/action-menu/control-modes.tsx +44 -94
  150. package/src/components/ui/action-menu/furnish-tools.tsx +5 -5
  151. package/src/components/ui/action-menu/index.tsx +2 -4
  152. package/src/components/ui/action-menu/measurement-control.tsx +267 -0
  153. package/src/components/ui/action-menu/structure-tools.tsx +22 -13
  154. package/src/components/ui/action-menu/view-toggles.tsx +42 -90
  155. package/src/components/ui/command-palette/editor-commands.tsx +16 -3
  156. package/src/components/ui/command-palette/index.tsx +4 -3
  157. package/src/components/ui/controls/material-paint-panel.tsx +93 -18
  158. package/src/components/ui/controls/material-picker.tsx +162 -152
  159. package/src/components/ui/controls/material-properties-editor.tsx +108 -0
  160. package/src/components/ui/controls/metric-control.tsx +118 -44
  161. package/src/components/ui/controls/scene-material-list.tsx +247 -0
  162. package/src/components/ui/controls/slider-control.tsx +88 -38
  163. package/src/components/ui/controls/terrain-sculpt-panel.tsx +193 -0
  164. package/src/components/ui/floating-level-selector.tsx +74 -20
  165. package/src/components/ui/helpers/building-helper.tsx +10 -23
  166. package/src/components/ui/helpers/contextual-helper-panel.tsx +428 -0
  167. package/src/components/ui/helpers/helper-manager.tsx +273 -14
  168. package/src/components/ui/helpers/item-helper.tsx +28 -32
  169. package/src/components/ui/helpers/registered-tool-helper.tsx +45 -11
  170. package/src/components/ui/helpers/roof-helper.tsx +10 -12
  171. package/src/components/ui/icon-ref.tsx +47 -0
  172. package/src/components/ui/item-catalog/catalog-items.tsx +39 -1
  173. package/src/components/ui/item-catalog/item-catalog.tsx +13 -36
  174. package/src/components/ui/level-duplicate-dialog.tsx +1 -1
  175. package/src/components/ui/panels/multi-selection-panel.tsx +57 -0
  176. package/src/components/ui/panels/node-display.ts +17 -17
  177. package/src/components/ui/panels/panel-manager.tsx +36 -14
  178. package/src/components/ui/panels/panel-wrapper.tsx +24 -3
  179. package/src/components/ui/panels/parametric-inspector.tsx +19 -4
  180. package/src/components/ui/panels/reference-panel.tsx +54 -19
  181. package/src/components/ui/panels/selection-breakdown.test.ts +24 -0
  182. package/src/components/ui/panels/selection-breakdown.ts +20 -0
  183. package/src/components/ui/primitives/shortcut-token.tsx +39 -3
  184. package/src/components/ui/primitives/sidebar.tsx +1 -0
  185. package/src/components/ui/sidebar/app-sidebar.tsx +5 -1
  186. package/src/components/ui/sidebar/icon-rail.tsx +50 -31
  187. package/src/components/ui/sidebar/panels/plugins-panel.tsx +218 -0
  188. package/src/components/ui/sidebar/panels/settings-panel/index.tsx +82 -41
  189. package/src/components/ui/sidebar/panels/settings-panel/keyboard-shortcuts-dialog.tsx +76 -6
  190. package/src/components/ui/sidebar/panels/settings-panel/load-build-dialog.tsx +12 -4
  191. package/src/components/ui/sidebar/panels/site-panel/building-tree-node.tsx +2 -1
  192. package/src/components/ui/sidebar/panels/site-panel/ceiling-tree-node.tsx +4 -3
  193. package/src/components/ui/sidebar/panels/site-panel/chimney-tree-node.tsx +10 -7
  194. package/src/components/ui/sidebar/panels/site-panel/column-tree-node.tsx +1 -1
  195. package/src/components/ui/sidebar/panels/site-panel/door-tree-node.tsx +7 -2
  196. package/src/components/ui/sidebar/panels/site-panel/dormer-tree-node.tsx +10 -7
  197. package/src/components/ui/sidebar/panels/site-panel/elevator-tree-node.tsx +1 -1
  198. package/src/components/ui/sidebar/panels/site-panel/fence-tree-node.tsx +1 -1
  199. package/src/components/ui/sidebar/panels/site-panel/gutter-tree-node.tsx +10 -7
  200. package/src/components/ui/sidebar/panels/site-panel/index.tsx +42 -17
  201. package/src/components/ui/sidebar/panels/site-panel/item-tree-node.tsx +31 -17
  202. package/src/components/ui/sidebar/panels/site-panel/level-tree-node.tsx +1 -1
  203. package/src/components/ui/sidebar/panels/site-panel/registry-tree-node.tsx +72 -25
  204. package/src/components/ui/sidebar/panels/site-panel/roof-tree-node.tsx +7 -3
  205. package/src/components/ui/sidebar/panels/site-panel/shelf-tree-node.tsx +12 -8
  206. package/src/components/ui/sidebar/panels/site-panel/slab-tree-node.tsx +4 -3
  207. package/src/components/ui/sidebar/panels/site-panel/solar-panel-tree-node.tsx +10 -7
  208. package/src/components/ui/sidebar/panels/site-panel/spawn-tree-node.tsx +1 -1
  209. package/src/components/ui/sidebar/panels/site-panel/stair-tree-node.tsx +2 -2
  210. package/src/components/ui/sidebar/panels/site-panel/tree-node.tsx +44 -3
  211. package/src/components/ui/sidebar/panels/site-panel/tree-structure.ts +36 -0
  212. package/src/components/ui/sidebar/panels/site-panel/wall-tree-node.tsx +1 -1
  213. package/src/components/ui/sidebar/panels/site-panel/window-tree-node.tsx +7 -2
  214. package/src/components/ui/sidebar/panels/site-panel/zone-tree-node.tsx +3 -3
  215. package/src/components/ui/sidebar/tab-bar.tsx +42 -28
  216. package/src/components/ui/sidebar/use-plugin-panels.tsx +132 -0
  217. package/src/components/ui/snap-target-badge.test.tsx +40 -0
  218. package/src/components/ui/snap-target-badge.tsx +88 -0
  219. package/src/components/viewer/viewer-controls-bar.tsx +466 -0
  220. package/src/components/viewer/viewer-scene-header.tsx +235 -0
  221. package/src/components/viewer-overlay.tsx +36 -596
  222. package/src/components/viewer-zone-system.tsx +6 -1
  223. package/src/components/walkthrough-hud.tsx +111 -0
  224. package/src/hooks/use-auto-save.test.ts +14 -0
  225. package/src/hooks/use-auto-save.ts +54 -7
  226. package/src/hooks/use-ceiling-events.ts +3 -2
  227. package/src/hooks/use-drag-action.ts +4 -1
  228. package/src/hooks/use-grid-events.ts +24 -0
  229. package/src/hooks/use-keyboard.ts +438 -70
  230. package/src/index.tsx +355 -9
  231. package/src/lib/active-placement-surface.test.ts +24 -0
  232. package/src/lib/active-placement-surface.ts +42 -0
  233. package/src/lib/camera-pose.test.ts +237 -0
  234. package/src/lib/camera-pose.ts +172 -0
  235. package/src/lib/ceiling-plan-snap.ts +24 -0
  236. package/src/lib/contextual-help.test.ts +112 -0
  237. package/src/lib/contextual-help.ts +144 -0
  238. package/src/lib/continuation.ts +64 -0
  239. package/src/lib/direct-manipulation.test.ts +275 -0
  240. package/src/lib/direct-manipulation.ts +118 -0
  241. package/src/lib/door-interaction.ts +1 -1
  242. package/src/lib/editor-api.ts +23 -35
  243. package/src/lib/elevation-guides.test.ts +107 -0
  244. package/src/lib/elevation-guides.ts +236 -0
  245. package/src/lib/floorplan/annotation-visibility.test.ts +327 -0
  246. package/src/lib/floorplan/annotation-visibility.ts +133 -0
  247. package/src/lib/floorplan/apply-alignment.test.ts +25 -0
  248. package/src/lib/floorplan/apply-alignment.ts +18 -12
  249. package/src/lib/floorplan/drawing-coordination.test.ts +46 -0
  250. package/src/lib/floorplan/drawing-coordination.ts +13 -0
  251. package/src/lib/floorplan/floorplan-export.test.ts +319 -0
  252. package/src/lib/floorplan/floorplan-export.tsx +978 -0
  253. package/src/lib/floorplan/floorplan-extension.test.ts +43 -0
  254. package/src/lib/floorplan/floorplan-extension.ts +181 -0
  255. package/src/lib/floorplan/floorplan-mode.test.ts +120 -0
  256. package/src/lib/floorplan/floorplan-mode.ts +73 -0
  257. package/src/lib/floorplan/floorplan-pdfkit-document.ts +194 -0
  258. package/src/lib/floorplan/floorplan-pdfkit-renderer.test.ts +107 -0
  259. package/src/lib/floorplan/floorplan-pdfkit-renderer.ts +680 -0
  260. package/src/lib/floorplan/geometry.ts +52 -0
  261. package/src/lib/floorplan/index.ts +3 -0
  262. package/src/lib/floorplan/items.ts +5 -2
  263. package/src/lib/floorplan/plan-coords.ts +21 -0
  264. package/src/lib/fresh-planar-placement.test.ts +283 -3
  265. package/src/lib/fresh-planar-placement.ts +85 -1
  266. package/src/lib/glb-export.test.ts +556 -0
  267. package/src/lib/glb-export.ts +1071 -0
  268. package/src/lib/ground-surface.ts +95 -0
  269. package/src/lib/history.test.ts +96 -0
  270. package/src/lib/history.ts +31 -0
  271. package/src/lib/interaction/hot-set.test.ts +133 -0
  272. package/src/lib/interaction/hot-set.ts +67 -0
  273. package/src/lib/interaction/overlay-policy.test.ts +52 -0
  274. package/src/lib/interaction/overlay-policy.ts +59 -0
  275. package/src/lib/interaction/scope.ts +243 -0
  276. package/src/lib/level-duplication.test.ts +2 -1
  277. package/src/lib/level-selection.ts +2 -2
  278. package/src/lib/material-paint.ts +36 -49
  279. package/src/lib/measurement-kind.test.ts +30 -0
  280. package/src/lib/measurement-kind.ts +19 -0
  281. package/src/lib/measurement-label.test.ts +47 -0
  282. package/src/lib/measurement-label.ts +68 -0
  283. package/src/lib/measurement-parser.ts +116 -0
  284. package/src/lib/measurements.test.ts +188 -0
  285. package/src/lib/measurements.ts +210 -0
  286. package/src/lib/paint-scope.test.ts +454 -0
  287. package/src/lib/paint-scope.ts +461 -0
  288. package/src/lib/placement-drag-release.ts +23 -0
  289. package/src/lib/planar-cursor-placement.test.ts +57 -0
  290. package/src/lib/plugin-panels.test.ts +19 -0
  291. package/src/lib/plugin-panels.ts +91 -0
  292. package/src/lib/quick-action-feedback.ts +33 -0
  293. package/src/lib/quick-action-nodes.test.ts +94 -0
  294. package/src/lib/quick-action-nodes.ts +47 -0
  295. package/src/lib/quick-measurement.test.ts +77 -0
  296. package/src/lib/quick-measurement.ts +109 -0
  297. package/src/lib/roof-duplication.ts +6 -6
  298. package/src/lib/roof-hover-outline-proxy.ts +22 -0
  299. package/src/lib/roof-wall-hit.ts +164 -0
  300. package/src/lib/scene-clipboard.test.ts +325 -0
  301. package/src/lib/scene-clipboard.ts +310 -21
  302. package/src/lib/scene.ts +28 -5
  303. package/src/lib/selection-routing.test.ts +313 -0
  304. package/src/lib/selection-routing.ts +180 -0
  305. package/src/lib/sfx/index.ts +1 -0
  306. package/src/lib/sfx/movement-tick.test.ts +65 -0
  307. package/src/lib/sfx/movement-tick.ts +18 -0
  308. package/src/lib/sfx-bus.ts +82 -14
  309. package/src/lib/sfx-player.test.ts +266 -0
  310. package/src/lib/sfx-player.ts +262 -69
  311. package/src/lib/site-boundary.test.ts +53 -0
  312. package/src/lib/site-boundary.ts +27 -0
  313. package/src/lib/slab-plan-snap.test.ts +93 -0
  314. package/src/lib/slab-plan-snap.ts +108 -0
  315. package/src/lib/snapping-mode.test.ts +165 -0
  316. package/src/lib/snapping-mode.ts +191 -0
  317. package/src/lib/stair-duplication.test.ts +103 -0
  318. package/src/lib/stair-duplication.ts +58 -86
  319. package/src/lib/stair-levels.ts +2 -0
  320. package/src/lib/surface-plan-snap.test.ts +71 -0
  321. package/src/lib/surface-plan-snap.ts +256 -0
  322. package/src/lib/terrain-sculpt.test.ts +429 -0
  323. package/src/lib/terrain-sculpt.ts +301 -0
  324. package/src/lib/terrain-verb-color.test.ts +51 -0
  325. package/src/lib/terrain-verb-color.ts +58 -0
  326. package/src/lib/touch-gesture-priority.test.ts +45 -0
  327. package/src/lib/touch-gesture-priority.ts +30 -0
  328. package/src/lib/use-linear-display.ts +40 -0
  329. package/src/lib/window-interaction.ts +1 -1
  330. package/src/lib/world-grid-snap.ts +37 -5
  331. package/src/store/camera-pose-store.test.ts +29 -0
  332. package/src/store/camera-pose-store.ts +27 -0
  333. package/src/store/live-draft-preview-stores.test.ts +136 -0
  334. package/src/store/navigation-sync-pose-store.test.ts +34 -0
  335. package/src/store/navigation-sync-pose-store.ts +27 -0
  336. package/src/store/terrain-sculpt-mode.test.ts +223 -0
  337. package/src/store/use-delete-confirmation.ts +27 -0
  338. package/src/store/use-direct-manipulation-feedback.ts +20 -0
  339. package/src/store/use-drawing-view.test.ts +23 -0
  340. package/src/store/use-drawing-view.ts +84 -0
  341. package/src/store/use-editor.tsx +665 -126
  342. package/src/store/use-elevation-guides.ts +40 -0
  343. package/src/store/use-facing-pose.ts +44 -0
  344. package/src/store/use-fence-curve-draft.ts +51 -0
  345. package/src/store/use-first-person-hud.ts +40 -0
  346. package/src/store/use-floorplan-annotation-visibility.ts +60 -0
  347. package/src/store/use-floorplan-draft-preview.ts +151 -0
  348. package/src/store/use-floorplan-marquee.ts +50 -0
  349. package/src/store/use-floorplan-mode.ts +110 -0
  350. package/src/store/use-interaction-scope.test.ts +206 -0
  351. package/src/store/use-interaction-scope.ts +140 -0
  352. package/src/store/use-measurement-draft.test.ts +559 -0
  353. package/src/store/use-measurement-draft.ts +570 -0
  354. package/src/store/use-opening-guides.ts +41 -0
  355. package/src/store/use-path-draft-preview.ts +90 -0
  356. package/src/store/use-placement-preview.ts +11 -3
  357. package/src/store/use-quick-measurement-hud.test.ts +53 -0
  358. package/src/store/use-quick-measurement-hud.ts +77 -0
  359. package/src/store/use-segment-draft-chain.ts +28 -0
  360. package/src/store/use-stair-build-preview.ts +53 -0
  361. package/src/store/use-wall-snap-indicator.ts +2 -0
  362. package/src/components/editor/first-person/bvh-ecctrl.tsx +0 -860
  363. package/src/components/editor/group-move-handle.tsx +0 -316
  364. package/src/components/ui/panels/paint-panel.tsx +0 -163
  365. package/src/lib/floorplan/selection-tool.ts +0 -271
  366. package/src/lib/level-name.ts +0 -11
@@ -1,11 +1,20 @@
1
+ import { Icon as IconifyIcon } from '@iconify/react'
1
2
  import { type AnyNodeId, nodeRegistry, useScene } from '@pascal-app/core'
2
3
  import { useViewer } from '@pascal-app/viewer'
3
4
  import Image from 'next/image'
4
- import { memo, useCallback, useState } from 'react'
5
- import useEditor from './../../../../../store/use-editor'
5
+ import { memo, useCallback, useEffect, useState } from 'react'
6
+ import { useShallow } from 'zustand/react/shallow'
7
+ import { resolveNodeSnapTarget, SnapTargetIcon } from '../../../snap-target-badge'
6
8
  import { InlineRenameInput } from './inline-rename-input'
7
- import { focusTreeNode, handleTreeSelection, TreeNodeWrapper } from './tree-node'
9
+ import {
10
+ focusTreeNode,
11
+ handleTreeSelection,
12
+ routeTreeSelectionToNode,
13
+ TreeNode,
14
+ TreeNodeWrapper,
15
+ } from './tree-node'
8
16
  import { TreeNodeActions } from './tree-node-actions'
17
+ import { resolveTreeChildIds, treeContainsDescendant } from './tree-structure'
9
18
 
10
19
  interface RegistryTreeNodeProps {
11
20
  nodeId: AnyNodeId
@@ -14,11 +23,11 @@ interface RegistryTreeNodeProps {
14
23
  }
15
24
 
16
25
  /**
17
- * Generic, leaf tree-node row driven entirely by the kind's
18
- * `def.presentation` (icon + label). Replaces the per-kind boilerplate
26
+ * Generic, registry-driven tree-node row powered by `def.presentation` and
27
+ * `def.tree`. Replaces the per-kind boilerplate
19
28
  * components that differed only in their default name and icon — today the
20
- * roof vents (box / ridge / turbine / cupola / eyebrow). Register a kind in
21
- * `treeNodeByType` against this component instead of authoring another copy.
29
+ * roof vents plus cabinet rows. Register a kind in `treeNodeByType` against
30
+ * this component instead of authoring another copy.
22
31
  */
23
32
  export const RegistryTreeNode = memo(function RegistryTreeNode({
24
33
  nodeId,
@@ -26,48 +35,76 @@ export const RegistryTreeNode = memo(function RegistryTreeNode({
26
35
  isLast,
27
36
  }: RegistryTreeNodeProps) {
28
37
  const [isEditing, setIsEditing] = useState(false)
38
+ const [expanded, setExpanded] = useState(true)
29
39
  const isVisible = useScene((s) => s.nodes[nodeId]?.visible !== false)
30
40
  const node = useScene((s) => s.nodes[nodeId])
41
+ const children = useScene(useShallow((s) => resolveTreeChildIds(nodeId, s.nodes)))
31
42
  const isSelected = useViewer((state) => state.selection.selectedIds.includes(nodeId))
32
43
  const isHovered = useViewer((state) => state.hoveredId === nodeId)
33
44
  const setSelection = useViewer((state) => state.setSelection)
34
45
  const setHoveredId = useViewer((state) => state.setHoveredId)
35
46
 
36
47
  const presentation = node ? nodeRegistry.get(node.type)?.presentation : undefined
48
+ const tree = node ? nodeRegistry.get(node.type)?.tree : undefined
37
49
  const icon = presentation?.icon
38
- const iconSrc = icon?.kind === 'url' ? icon.src : '/icons/roof.png'
39
- const defaultName = node?.name || presentation?.label || 'Node'
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])
40
81
 
41
82
  const handleClick = useCallback(
42
83
  (e: React.MouseEvent) => {
43
84
  e.stopPropagation()
44
- const handled = handleTreeSelection(
85
+ handleTreeSelection(
45
86
  e,
46
87
  nodeId,
47
88
  useViewer.getState().selection.selectedIds,
48
89
  setSelection,
49
90
  )
50
- if (!handled && useEditor.getState().phase === 'furnish') {
51
- useEditor.getState().setPhase('structure')
52
- }
91
+ routeTreeSelectionToNode(node)
53
92
  },
54
- [nodeId, setSelection],
93
+ [node, nodeId, setSelection],
55
94
  )
56
95
 
57
96
  return (
58
97
  <TreeNodeWrapper
59
98
  actions={<TreeNodeActions nodeId={nodeId} />}
60
99
  depth={depth}
61
- expanded={false}
62
- hasChildren={false}
100
+ expanded={expanded}
101
+ hasChildren={hasChildren}
63
102
  icon={
64
- <Image
65
- alt=""
66
- className="object-contain opacity-60"
67
- height={14}
68
- src={iconSrc}
69
- width={14}
70
- />
103
+ snapTarget ? (
104
+ <SnapTargetIcon target={snapTarget}>{iconElement}</SnapTargetIcon>
105
+ ) : (
106
+ iconElement
107
+ )
71
108
  }
72
109
  isHovered={isHovered}
73
110
  isLast={isLast}
@@ -87,7 +124,17 @@ export const RegistryTreeNode = memo(function RegistryTreeNode({
87
124
  onDoubleClick={() => focusTreeNode(nodeId)}
88
125
  onMouseEnter={() => setHoveredId(nodeId)}
89
126
  onMouseLeave={() => setHoveredId(null)}
90
- onToggle={() => {}}
91
- />
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>
92
139
  )
93
140
  })
@@ -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}
@@ -230,7 +234,7 @@ function RoofSegmentTreeNode({
230
234
  alt=""
231
235
  className="object-contain opacity-60"
232
236
  height={14}
233
- src="/icons/roof.png"
237
+ src="/icons/roof.webp"
234
238
  width={14}
235
239
  />
236
240
  }
@@ -5,9 +5,14 @@ import { useViewer } from '@pascal-app/viewer'
5
5
  import Image from 'next/image'
6
6
  import { memo, useCallback, useEffect, useState } from 'react'
7
7
  import { useShallow } from 'zustand/react/shallow'
8
- import useEditor from './../../../../../store/use-editor'
9
8
  import { InlineRenameInput } from './inline-rename-input'
10
- import { focusTreeNode, handleTreeSelection, TreeNode, TreeNodeWrapper } from './tree-node'
9
+ import {
10
+ focusTreeNode,
11
+ handleTreeSelection,
12
+ routeTreeSelectionToNode,
13
+ TreeNode,
14
+ TreeNodeWrapper,
15
+ } from './tree-node'
11
16
  import { TreeNodeActions } from './tree-node-actions'
12
17
 
13
18
  interface ShelfTreeNodeProps {
@@ -34,6 +39,7 @@ export const ShelfTreeNode = memo(function ShelfTreeNode({
34
39
  const children = useScene(
35
40
  useShallow((s) => (s.nodes[nodeId] as ShelfNode | undefined)?.children ?? []),
36
41
  )
42
+ const node = useScene((s) => s.nodes[nodeId] as ShelfNode | undefined)
37
43
  const isSelected = useViewer((state) => state.selection.selectedIds.includes(nodeId))
38
44
  const isHovered = useViewer((state) => state.hoveredId === nodeId)
39
45
  const setSelection = useViewer((state) => state.setSelection)
@@ -63,17 +69,15 @@ export const ShelfTreeNode = memo(function ShelfTreeNode({
63
69
  const handleClick = useCallback(
64
70
  (e: React.MouseEvent) => {
65
71
  e.stopPropagation()
66
- const handled = handleTreeSelection(
72
+ handleTreeSelection(
67
73
  e,
68
74
  nodeId,
69
75
  useViewer.getState().selection.selectedIds,
70
76
  setSelection,
71
77
  )
72
- if (!handled && useEditor.getState().phase === 'furnish') {
73
- useEditor.getState().setPhase('structure')
74
- }
78
+ routeTreeSelectionToNode(node)
75
79
  },
76
- [nodeId, setSelection],
80
+ [node, nodeId, setSelection],
77
81
  )
78
82
 
79
83
  const handleDoubleClick = useCallback(() => focusTreeNode(nodeId), [nodeId])
@@ -92,7 +96,7 @@ export const ShelfTreeNode = memo(function ShelfTreeNode({
92
96
  expanded={expanded}
93
97
  hasChildren={hasChildren}
94
98
  icon={
95
- <Image alt="" className="object-contain" height={14} src="/icons/shelf.png" width={14} />
99
+ <Image alt="" className="object-contain" height={14} src="/icons/shelf.webp" width={14} />
96
100
  }
97
101
  isHovered={isHovered}
98
102
  isLast={isLast}
@@ -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}
@@ -2,6 +2,7 @@ import { type AnyNodeId, type SolarPanelNode, 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 { SnapTargetIcon } from '../../../snap-target-badge'
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'
@@ -51,13 +52,15 @@ export const SolarPanelTreeNode = memo(function SolarPanelTreeNode({
51
52
  expanded={false}
52
53
  hasChildren={false}
53
54
  icon={
54
- <Image
55
- alt=""
56
- className="object-contain opacity-60"
57
- height={14}
58
- src="/icons/roof.png"
59
- width={14}
60
- />
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>
61
64
  }
62
65
  isHovered={isHovered}
63
66
  isLast={isLast}
@@ -54,7 +54,7 @@ export const SpawnTreeNode = memo(function SpawnTreeNode({
54
54
  alt=""
55
55
  className="object-contain"
56
56
  height={14}
57
- src="/icons/spawn-point.png"
57
+ src="/icons/spawn-point.webp"
58
58
  width={14}
59
59
  />
60
60
  }
@@ -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,6 +56,31 @@ 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'
@@ -98,10 +126,13 @@ const treeNodeByType: Record<
98
126
  isLast?: boolean
99
127
  nodeId: AnyNodeId
100
128
  }>,
129
+ cabinet: RegistryTreeNode,
130
+ 'cabinet-module': RegistryTreeNode,
101
131
  'box-vent': RegistryTreeNode,
102
132
  ceiling: CeilingTreeNode,
103
133
  chimney: ChimneyTreeNode,
104
134
  dormer: DormerTreeNode,
135
+ downspout: RegistryTreeNode,
105
136
  'solar-panel': SolarPanelTreeNode,
106
137
  column: ColumnTreeNode,
107
138
  elevator: ElevatorTreeNode,
@@ -124,10 +155,12 @@ const treeNodeByType: Record<
124
155
  wall: WallTreeNode,
125
156
  fence: FenceTreeNode,
126
157
  gutter: GutterTreeNode,
158
+ measurement: RegistryTreeNode,
127
159
  'ridge-vent': RegistryTreeNode,
128
160
  'turbine-vent': RegistryTreeNode,
129
161
  cupola: RegistryTreeNode,
130
162
  'eyebrow-vent': RegistryTreeNode,
163
+ skylight: RegistryTreeNode,
131
164
  roof: RoofTreeNode,
132
165
  stair: StairTreeNode,
133
166
  door: DoorTreeNode,
@@ -141,7 +174,15 @@ const treeNodeByType: Record<
141
174
  }
142
175
 
143
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
+ })
144
184
  const nodeType = useScene((state) => state.nodes[nodeId]?.type)
185
+ if (shouldHide) return null
145
186
  if (!nodeType) return null
146
187
  const Component = treeNodeByType[nodeType]
147
188
  if (!Component) return null
@@ -266,10 +307,10 @@ export const TreeNodeWrapper = forwardRef<HTMLDivElement, TreeNodeWrapperProps>(
266
307
  </motion.div>
267
308
  ) : null}
268
309
  </button>
269
- <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">
270
311
  <span
271
312
  className={cn(
272
- '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',
273
314
  !isSelected && 'opacity-60 grayscale',
274
315
  )}
275
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
@@ -1,6 +1,7 @@
1
1
  'use client'
2
2
 
3
3
  import type { ReactNode } from 'react'
4
+ import { editorHostPanelRegistry } from '../../../lib/plugin-panels'
4
5
  import { triggerSFX } from './../../../lib/sfx-bus'
5
6
  import { cn } from './../../../lib/utils'
6
7
  import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '../primitives/tooltip'
@@ -65,37 +66,50 @@ interface IconRailProps {
65
66
  * The label renders as a hover tooltip on the right.
66
67
  */
67
68
  export function IconRail({ tabs, activeTab, collapsed, onIconClick }: IconRailProps) {
69
+ const pluginPanelIds = new Set(
70
+ editorHostPanelRegistry.getSnapshot().flatMap((panel) =>
71
+ panel.pluginId ? [panel.id] : [],
72
+ ),
73
+ )
74
+ const defaultTabs = tabs.filter((tab) => !pluginPanelIds.has(tab.id) && tab.id !== 'plugins')
75
+ const pluginTabs = tabs.filter((tab) => pluginPanelIds.has(tab.id) || tab.id === 'plugins')
76
+
77
+ const renderTab = (tab: SidebarTab) => {
78
+ const showActive = activeTab === tab.id && !collapsed
79
+ return (
80
+ <Tooltip key={tab.id}>
81
+ <TooltipTrigger asChild>
82
+ <button
83
+ className={cn(
84
+ 'group flex h-11 w-11 items-center justify-center rounded-xl transition-all duration-200 [&_img]:transition-[opacity,filter] [&_img]:duration-200',
85
+ showActive
86
+ ? 'bg-accent text-foreground shadow-sm [&_img]:opacity-100 [&_img]:grayscale-0'
87
+ : 'text-muted-foreground hover:bg-accent/50 hover:text-foreground [&_img]:opacity-60 [&_img]:grayscale hover:[&_img]:opacity-100 hover:[&_img]:grayscale-0',
88
+ )}
89
+ onClick={() => {
90
+ triggerSFX('sfx:menu-click')
91
+ onIconClick(tab.id)
92
+ }}
93
+ onMouseEnter={() => triggerSFX('sfx:menu-hover')}
94
+ type="button"
95
+ >
96
+ {tab.icon ?? tab.label.charAt(0)}
97
+ </button>
98
+ </TooltipTrigger>
99
+ <TooltipContent side="right">{tab.label}</TooltipContent>
100
+ </Tooltip>
101
+ )
102
+ }
103
+
68
104
  return (
69
105
  <TooltipProvider delayDuration={0} disableHoverableContent>
70
106
  <div className="flex h-full w-14 shrink-0 flex-col items-center gap-1 border-border/50 border-r py-2">
71
- {tabs.map((tab) => {
72
- // Only show the active highlight while the panel is open. When
73
- // collapsed nothing is "open", so every icon reads as unselected.
74
- const showActive = activeTab === tab.id && !collapsed
75
- return (
76
- <Tooltip key={tab.id}>
77
- <TooltipTrigger asChild>
78
- <button
79
- className={cn(
80
- 'group flex h-11 w-11 items-center justify-center rounded-xl transition-all duration-200 [&_img]:transition-[opacity,filter] [&_img]:duration-200',
81
- showActive
82
- ? 'bg-accent text-foreground shadow-sm [&_img]:opacity-100 [&_img]:grayscale-0'
83
- : 'text-muted-foreground hover:bg-accent/50 hover:text-foreground [&_img]:opacity-60 [&_img]:grayscale hover:[&_img]:opacity-100 hover:[&_img]:grayscale-0',
84
- )}
85
- onClick={() => {
86
- triggerSFX('sfx:menu-click')
87
- onIconClick(tab.id)
88
- }}
89
- onMouseEnter={() => triggerSFX('sfx:menu-hover')}
90
- type="button"
91
- >
92
- {tab.icon ?? tab.label.charAt(0)}
93
- </button>
94
- </TooltipTrigger>
95
- <TooltipContent side="right">{tab.label}</TooltipContent>
96
- </Tooltip>
97
- )
98
- })}
107
+ {defaultTabs.map(renderTab)}
108
+ {pluginTabs.length > 0 && (
109
+ <div className="mt-1 flex w-11 flex-col items-center gap-1 border-border/70 border-t pt-2">
110
+ {pluginTabs.map(renderTab)}
111
+ </div>
112
+ )}
99
113
  </div>
100
114
  </TooltipProvider>
101
115
  )