@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,267 @@
1
+ import type { BuildStats, SchemaIssue, ValidateBuildJsonResult } from '@pascal-app/core'
2
+ import { useViewer } from '@pascal-app/viewer'
3
+ import {
4
+ AlertTriangle,
5
+ AppWindow,
6
+ Box,
7
+ Building2,
8
+ CheckCircle2,
9
+ DoorOpen,
10
+ Layers,
11
+ MapPin,
12
+ Scan,
13
+ Square,
14
+ XCircle,
15
+ } from 'lucide-react'
16
+ import { useState } from 'react'
17
+ import { Button } from '../../../../../components/ui/primitives/button'
18
+ import {
19
+ Dialog,
20
+ DialogContent,
21
+ DialogDescription,
22
+ DialogFooter,
23
+ DialogHeader,
24
+ DialogTitle,
25
+ } from '../../../../../components/ui/primitives/dialog'
26
+ import {
27
+ getAreaUnitLabel,
28
+ type LinearUnit,
29
+ squareMetersToAreaUnit,
30
+ } from '../../../../../lib/measurements'
31
+
32
+ export type PendingImport = {
33
+ fileName: string
34
+ fileSizeBytes: number
35
+ result: ValidateBuildJsonResult
36
+ }
37
+
38
+ type Props = {
39
+ pending: PendingImport | null
40
+ onCancel: () => void
41
+ onConfirm: (parsed: NonNullable<ValidateBuildJsonResult['parsed']>) => void
42
+ }
43
+
44
+ type StatRow = {
45
+ icon: typeof Building2
46
+ label: string
47
+ count: number
48
+ }
49
+
50
+ function statsRows(stats: BuildStats): StatRow[] {
51
+ return (
52
+ [
53
+ { icon: MapPin, label: 'Sites', count: stats.byType.site ?? 0 },
54
+ { icon: Building2, label: 'Buildings', count: stats.byType.building ?? 0 },
55
+ { icon: Layers, label: 'Levels', count: stats.byType.level ?? 0 },
56
+ { icon: Square, label: 'Walls', count: stats.byType.wall ?? 0 },
57
+ { icon: DoorOpen, label: 'Doors', count: stats.byType.door ?? 0 },
58
+ { icon: AppWindow, label: 'Windows', count: stats.byType.window ?? 0 },
59
+ { icon: Box, label: 'Items', count: stats.byType.item ?? 0 },
60
+ { icon: Square, label: 'Slabs', count: stats.byType.slab ?? 0 },
61
+ { icon: Square, label: 'Ceilings', count: stats.byType.ceiling ?? 0 },
62
+ { icon: Square, label: 'Zones', count: stats.byType.zone ?? 0 },
63
+ { icon: Scan, label: 'Scans', count: stats.byType.scan ?? 0 },
64
+ ] satisfies StatRow[]
65
+ ).filter((row) => row.count > 0)
66
+ }
67
+
68
+ function groupSchemaIssuesByType(
69
+ issues: SchemaIssue[],
70
+ ): { type: string; issues: SchemaIssue[] }[] {
71
+ const groups = new Map<string, SchemaIssue[]>()
72
+ for (const issue of issues) {
73
+ const existing = groups.get(issue.nodeType)
74
+ if (existing) {
75
+ existing.push(issue)
76
+ } else {
77
+ groups.set(issue.nodeType, [issue])
78
+ }
79
+ }
80
+ return Array.from(groups, ([type, list]) => ({ type, issues: list }))
81
+ }
82
+
83
+ function formatFileSize(bytes: number): string {
84
+ if (bytes < 1024) return `${bytes} B`
85
+ if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`
86
+ return `${(bytes / (1024 * 1024)).toFixed(1)} MB`
87
+ }
88
+
89
+ function formatFloorArea(m2: number, unit: LinearUnit): string {
90
+ if (m2 === 0) return '—'
91
+ const value = squareMetersToAreaUnit(m2, unit)
92
+ const label = getAreaUnitLabel(unit)
93
+ return `${value < 10 ? value.toFixed(2) : value.toFixed(1)} ${label}`
94
+ }
95
+
96
+ export function LoadBuildDialog({ pending, onCancel, onConfirm }: Props) {
97
+ const [showAllWarnings, setShowAllWarnings] = useState(false)
98
+ const [showSchemaIssues, setShowSchemaIssues] = useState(false)
99
+ const unit = useViewer((state) => state.unit)
100
+
101
+ if (!pending) return null
102
+
103
+ const { fileName, fileSizeBytes, result } = pending
104
+ const { ok, parsed, stats, errors, warnings, schemaIssues, schemaIssueCount } = result
105
+ const rows = statsRows(stats)
106
+ const visibleWarnings = showAllWarnings ? warnings : warnings.slice(0, 3)
107
+ const hiddenWarningCount = warnings.length - visibleWarnings.length
108
+ const schemaIssuesByType = groupSchemaIssuesByType(schemaIssues)
109
+
110
+ return (
111
+ <Dialog
112
+ open
113
+ onOpenChange={(open) => {
114
+ if (!open) onCancel()
115
+ }}
116
+ >
117
+ <DialogContent className="sm:max-w-lg">
118
+ <DialogHeader>
119
+ <DialogTitle className="flex items-center gap-2">
120
+ {ok ? (
121
+ <CheckCircle2 className="size-5 text-emerald-600" />
122
+ ) : (
123
+ <XCircle className="size-5 text-red-600" />
124
+ )}
125
+ {ok ? 'Ready to import' : 'Cannot import this file'}
126
+ </DialogTitle>
127
+ <DialogDescription>
128
+ {fileName} · {formatFileSize(fileSizeBytes)} · {stats.total} node
129
+ {stats.total === 1 ? '' : 's'}
130
+ </DialogDescription>
131
+ </DialogHeader>
132
+
133
+ <div className="max-h-[60vh] space-y-4 overflow-y-auto py-2">
134
+ {errors.length > 0 && (
135
+ <div className="space-y-2 rounded-md border border-red-200 bg-red-50 p-3">
136
+ <div className="flex items-center gap-2 font-medium text-red-800 text-sm">
137
+ <XCircle className="size-4" />
138
+ {errors.length} error{errors.length === 1 ? '' : 's'}
139
+ </div>
140
+ <ul className="space-y-1 text-red-700 text-xs">
141
+ {errors.map((e) => (
142
+ <li key={`${e.code}-${e.nodeId ?? ''}`}>· {e.message}</li>
143
+ ))}
144
+ </ul>
145
+ </div>
146
+ )}
147
+
148
+ {stats.total > 0 && (
149
+ <div className="rounded-md border bg-card">
150
+ <div className="border-b px-3 py-2 font-medium text-muted-foreground text-xs uppercase">
151
+ Structure
152
+ </div>
153
+ {rows.length > 0 ? (
154
+ <div>
155
+ {rows.map((row, i) => {
156
+ const Icon = row.icon
157
+ return (
158
+ <div
159
+ className={`flex items-center justify-between px-3 py-2 ${
160
+ i === rows.length - 1 ? '' : 'border-b'
161
+ }`}
162
+ key={row.label}
163
+ >
164
+ <div className="flex items-center gap-2">
165
+ <Icon className="size-4 text-muted-foreground" />
166
+ <span className="text-sm">{row.label}</span>
167
+ </div>
168
+ <span className="font-medium text-sm">{row.count}</span>
169
+ </div>
170
+ )
171
+ })}
172
+ {stats.floorAreaM2 > 0 && (
173
+ <div className="flex items-center justify-between border-t px-3 py-2">
174
+ <span className="text-muted-foreground text-sm">Floor area</span>
175
+ <span className="font-medium text-sm">
176
+ {formatFloorArea(stats.floorAreaM2, unit)}
177
+ </span>
178
+ </div>
179
+ )}
180
+ </div>
181
+ ) : (
182
+ <div className="px-3 py-4 text-center text-muted-foreground text-xs">
183
+ The file contains no recognised nodes.
184
+ </div>
185
+ )}
186
+ </div>
187
+ )}
188
+
189
+ {warnings.length > 0 && (
190
+ <div className="space-y-2 rounded-md border border-amber-200 bg-amber-50 p-3">
191
+ <div className="flex items-center gap-2 font-medium text-amber-800 text-sm">
192
+ <AlertTriangle className="size-4" />
193
+ {warnings.length} warning{warnings.length === 1 ? '' : 's'}
194
+ </div>
195
+ <ul className="space-y-1 text-amber-700 text-xs">
196
+ {visibleWarnings.map((w, i) => (
197
+ <li key={`${w.code}-${w.nodeId ?? ''}-${i}`}>· {w.message}</li>
198
+ ))}
199
+ </ul>
200
+ {hiddenWarningCount > 0 && (
201
+ <button
202
+ className="text-amber-800 text-xs underline hover:no-underline"
203
+ onClick={() => setShowAllWarnings(true)}
204
+ type="button"
205
+ >
206
+ Show {hiddenWarningCount} more
207
+ </button>
208
+ )}
209
+ </div>
210
+ )}
211
+
212
+ {schemaIssues.length > 0 && (
213
+ <div className="space-y-2 rounded-md border bg-card p-3">
214
+ <button
215
+ className="flex w-full items-center justify-between text-left"
216
+ onClick={() => setShowSchemaIssues((v) => !v)}
217
+ type="button"
218
+ >
219
+ <span className="font-medium text-muted-foreground text-xs uppercase">
220
+ Schema details ({schemaIssueCount} node
221
+ {schemaIssueCount === 1 ? '' : 's'})
222
+ </span>
223
+ <span className="text-muted-foreground text-xs">
224
+ {showSchemaIssues ? 'Hide' : 'Show'}
225
+ </span>
226
+ </button>
227
+ {showSchemaIssues && (
228
+ <div className="space-y-3 pt-1">
229
+ {schemaIssuesByType.map(({ type, issues }) => (
230
+ <div className="space-y-1" key={type}>
231
+ <div className="font-medium text-xs">
232
+ {type} · {issues.length}
233
+ </div>
234
+ <ul className="space-y-0.5 text-muted-foreground text-xs">
235
+ {issues.map((issue) => (
236
+ <li className="font-mono" key={issue.nodeId}>
237
+ <span className="text-foreground">{issue.nodeId}</span>
238
+ {issue.path && <span> · {issue.path}</span>}
239
+ <span> — {issue.message}</span>
240
+ </li>
241
+ ))}
242
+ </ul>
243
+ </div>
244
+ ))}
245
+ </div>
246
+ )}
247
+ </div>
248
+ )}
249
+ </div>
250
+
251
+ <DialogFooter>
252
+ <Button onClick={onCancel} variant="outline">
253
+ Cancel
254
+ </Button>
255
+ <Button
256
+ disabled={!ok || !parsed}
257
+ onClick={() => {
258
+ if (parsed) onConfirm(parsed)
259
+ }}
260
+ >
261
+ Replace current scene
262
+ </Button>
263
+ </DialogFooter>
264
+ </DialogContent>
265
+ </Dialog>
266
+ )
267
+ }
@@ -39,8 +39,10 @@ export const BuildingTreeNode = memo(function BuildingTreeNode({
39
39
 
40
40
  const handleAddLevel = (e: React.MouseEvent) => {
41
41
  e.stopPropagation()
42
+ const nodes = useScene.getState().nodes
43
+ const levelCount = children.filter((childId) => nodes[childId]?.type === 'level').length
42
44
  const newLevel = LevelNode.parse({
43
- level: children.length,
45
+ level: levelCount,
44
46
  children: [],
45
47
  parentId: nodeId,
46
48
  })
@@ -3,6 +3,7 @@ import { useViewer } from '@pascal-app/viewer'
3
3
  import Image from 'next/image'
4
4
  import { memo, useCallback, useEffect, useState } from 'react'
5
5
  import { useShallow } from 'zustand/react/shallow'
6
+ import { formatAreaLabel } from './../../../../../lib/measurements'
6
7
  import useEditor from './../../../../../store/use-editor'
7
8
  import { InlineRenameInput } from './inline-rename-input'
8
9
  import { focusTreeNode, handleTreeSelection, TreeNode, TreeNodeWrapper } from './tree-node'
@@ -32,6 +33,7 @@ export const CeilingTreeNode = memo(function CeilingTreeNode({
32
33
  const isHovered = useViewer((state) => state.hoveredId === nodeId)
33
34
  const setSelection = useViewer((state) => state.setSelection)
34
35
  const setHoveredId = useViewer((state) => state.setHoveredId)
36
+ const unit = useViewer((state) => state.unit)
35
37
 
36
38
  // Expand when a descendant is selected — imperative to avoid subscribing to the full selectedIds array
37
39
  useEffect(() => {
@@ -75,8 +77,7 @@ export const CeilingTreeNode = memo(function CeilingTreeNode({
75
77
  const handleStartEditing = useCallback(() => setIsEditing(true), [])
76
78
  const handleStopEditing = useCallback(() => setIsEditing(false), [])
77
79
 
78
- const area = calculatePolygonArea(polygon).toFixed(1)
79
- const defaultName = `Ceiling (${area}m²)`
80
+ const defaultName = `Ceiling (${formatAreaLabel(calculatePolygonArea(polygon), unit)})`
80
81
 
81
82
  return (
82
83
  <TreeNodeWrapper
@@ -85,7 +86,7 @@ export const CeilingTreeNode = memo(function CeilingTreeNode({
85
86
  expanded={expanded}
86
87
  hasChildren={children.length > 0}
87
88
  icon={
88
- <Image alt="" className="object-contain" height={14} src="/icons/ceiling.png" width={14} />
89
+ <Image alt="" className="object-contain" height={14} src="/icons/ceiling.webp" width={14} />
89
90
  }
90
91
  isHovered={isHovered}
91
92
  isLast={isLast}
@@ -130,8 +131,11 @@ function calculatePolygonArea(polygon: Array<[number, number]>): number {
130
131
 
131
132
  for (let i = 0; i < n; i++) {
132
133
  const j = (i + 1) % n
133
- area += polygon[i]?.[0] * polygon[j]?.[1]
134
- area -= polygon[j]?.[0] * polygon[i]?.[1]
134
+ const pi = polygon[i]
135
+ const pj = polygon[j]
136
+ if (!(pi && pj)) continue
137
+ area += pi[0] * pj[1]
138
+ area -= pj[0] * pi[1]
135
139
  }
136
140
 
137
141
  return Math.abs(area) / 2
@@ -0,0 +1,86 @@
1
+ import { type AnyNodeId, type ChimneyNode, 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 ChimneyTreeNodeProps {
12
+ nodeId: AnyNodeId
13
+ depth: number
14
+ isLast?: boolean
15
+ }
16
+
17
+ export const ChimneyTreeNode = memo(function ChimneyTreeNode({
18
+ nodeId,
19
+ depth,
20
+ isLast,
21
+ }: ChimneyTreeNodeProps) {
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 ChimneyNode | 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 || 'Chimney'
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
+ })
@@ -51,7 +51,7 @@ export const ColumnTreeNode = memo(function ColumnTreeNode({
51
51
  expanded={false}
52
52
  hasChildren={false}
53
53
  icon={
54
- <Image alt="" className="object-contain" height={14} src="/icons/column.png" width={14} />
54
+ <Image alt="" className="object-contain" height={14} src="/icons/column.webp" width={14} />
55
55
  }
56
56
  isHovered={isHovered}
57
57
  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 DoorNode, 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 DoorTreeNode = memo(function DoorTreeNode({
22
23
  }: DoorTreeNodeProps) {
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 DoorNode | 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 DoorTreeNode = memo(function DoorTreeNode({
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 DoorTreeNode = memo(function DoorTreeNode({
53
56
  expanded={false}
54
57
  hasChildren={false}
55
58
  icon={
56
- <Image alt="" className="object-contain" height={14} src="/icons/door.png" width={14} />
59
+ <SnapTargetIcon target={snapTarget}>
60
+ <Image alt="" className="object-contain" height={14} src="/icons/door.webp" width={14} />
61
+ </SnapTargetIcon>
57
62
  }
58
63
  isHovered={isHovered}
59
64
  isLast={isLast}
@@ -0,0 +1,92 @@
1
+ import { type AnyNodeId, type DormerNode, 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 DormerTreeNodeProps {
12
+ nodeId: AnyNodeId
13
+ depth: number
14
+ isLast?: boolean
15
+ }
16
+
17
+ /**
18
+ * Sidebar tree-node entry for a dormer. Mirrors `ChimneyTreeNode`
19
+ * exactly — dormers are leaf entries (no children) parented under
20
+ * their host roof segment. The `roof.png` icon matches the rest of
21
+ * the roof-accessory kinds.
22
+ */
23
+ export const DormerTreeNode = memo(function DormerTreeNode({
24
+ nodeId,
25
+ depth,
26
+ isLast,
27
+ }: DormerTreeNodeProps) {
28
+ const [isEditing, setIsEditing] = useState(false)
29
+ const isVisible = useScene((s) => s.nodes[nodeId]?.visible !== false)
30
+ const node = useScene((s) => s.nodes[nodeId] as DormerNode | undefined)
31
+ const isSelected = useViewer((state) => state.selection.selectedIds.includes(nodeId))
32
+ const isHovered = useViewer((state) => state.hoveredId === nodeId)
33
+ const setSelection = useViewer((state) => state.setSelection)
34
+ const setHoveredId = useViewer((state) => state.setHoveredId)
35
+
36
+ const handleClick = useCallback(
37
+ (e: React.MouseEvent) => {
38
+ e.stopPropagation()
39
+ const handled = handleTreeSelection(
40
+ e,
41
+ nodeId,
42
+ useViewer.getState().selection.selectedIds,
43
+ setSelection,
44
+ )
45
+ if (!handled && useEditor.getState().phase === 'furnish') {
46
+ useEditor.getState().setPhase('structure')
47
+ }
48
+ },
49
+ [nodeId, setSelection],
50
+ )
51
+
52
+ const defaultName = node?.name || 'Dormer'
53
+
54
+ return (
55
+ <TreeNodeWrapper
56
+ actions={<TreeNodeActions nodeId={nodeId} />}
57
+ depth={depth}
58
+ expanded={false}
59
+ hasChildren={false}
60
+ icon={
61
+ <SnapTargetIcon target="roof">
62
+ <Image
63
+ alt=""
64
+ className="object-contain opacity-60"
65
+ height={14}
66
+ src="/icons/roof.webp"
67
+ width={14}
68
+ />
69
+ </SnapTargetIcon>
70
+ }
71
+ isHovered={isHovered}
72
+ isLast={isLast}
73
+ isSelected={isSelected}
74
+ isVisible={isVisible}
75
+ label={
76
+ <InlineRenameInput
77
+ defaultName={defaultName}
78
+ isEditing={isEditing}
79
+ nodeId={nodeId}
80
+ onStartEditing={() => setIsEditing(true)}
81
+ onStopEditing={() => setIsEditing(false)}
82
+ />
83
+ }
84
+ nodeId={nodeId}
85
+ onClick={handleClick}
86
+ onDoubleClick={() => focusTreeNode(nodeId)}
87
+ onMouseEnter={() => setHoveredId(nodeId)}
88
+ onMouseLeave={() => setHoveredId(null)}
89
+ onToggle={() => {}}
90
+ />
91
+ )
92
+ })
@@ -0,0 +1,75 @@
1
+ import { type AnyNodeId, type ElevatorNode, 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 useEditor from './../../../../../store/use-editor'
6
+ import { InlineRenameInput } from './inline-rename-input'
7
+ import { focusTreeNode, handleTreeSelection, TreeNodeWrapper } from './tree-node'
8
+ import { TreeNodeActions } from './tree-node-actions'
9
+
10
+ interface ElevatorTreeNodeProps {
11
+ nodeId: AnyNodeId
12
+ depth: number
13
+ isLast?: boolean
14
+ }
15
+
16
+ export const ElevatorTreeNode = memo(function ElevatorTreeNode({
17
+ nodeId,
18
+ depth,
19
+ isLast,
20
+ }: ElevatorTreeNodeProps) {
21
+ const [isEditing, setIsEditing] = useState(false)
22
+ const isVisible = useScene((s) => s.nodes[nodeId]?.visible !== false)
23
+ const node = useScene((s) => s.nodes[nodeId] as ElevatorNode | undefined)
24
+ const isSelected = useViewer((state) => state.selection.selectedIds.includes(nodeId))
25
+ const isHovered = useViewer((state) => state.hoveredId === nodeId)
26
+ const setSelection = useViewer((state) => state.setSelection)
27
+ const setHoveredId = useViewer((state) => state.setHoveredId)
28
+
29
+ const handleClick = useCallback(
30
+ (event: React.MouseEvent) => {
31
+ event.stopPropagation()
32
+ const handled = handleTreeSelection(
33
+ event,
34
+ nodeId,
35
+ useViewer.getState().selection.selectedIds,
36
+ setSelection,
37
+ )
38
+ if (!handled && useEditor.getState().phase === 'furnish') {
39
+ useEditor.getState().setPhase('structure')
40
+ }
41
+ },
42
+ [nodeId, setSelection],
43
+ )
44
+
45
+ return (
46
+ <TreeNodeWrapper
47
+ actions={<TreeNodeActions nodeId={nodeId} />}
48
+ depth={depth}
49
+ expanded={false}
50
+ hasChildren={false}
51
+ icon={
52
+ <Image alt="" className="object-contain" height={14} src="/icons/elevator.webp" width={14} />
53
+ }
54
+ isHovered={isHovered}
55
+ isLast={isLast}
56
+ isSelected={isSelected}
57
+ isVisible={isVisible}
58
+ label={
59
+ <InlineRenameInput
60
+ defaultName={node?.name || 'Elevator'}
61
+ isEditing={isEditing}
62
+ nodeId={nodeId}
63
+ onStartEditing={() => setIsEditing(true)}
64
+ onStopEditing={() => setIsEditing(false)}
65
+ />
66
+ }
67
+ nodeId={nodeId}
68
+ onClick={handleClick}
69
+ onDoubleClick={() => focusTreeNode(nodeId)}
70
+ onMouseEnter={() => setHoveredId(nodeId)}
71
+ onMouseLeave={() => setHoveredId(null)}
72
+ onToggle={() => {}}
73
+ />
74
+ )
75
+ })
@@ -38,12 +38,12 @@ export const FenceTreeNode = memo(function FenceTreeNode({
38
38
 
39
39
  return (
40
40
  <TreeNodeWrapper
41
- actions={<TreeNodeActions node={node} />}
41
+ actions={<TreeNodeActions nodeId={node.id} />}
42
42
  depth={depth}
43
43
  expanded={false}
44
44
  hasChildren={false}
45
45
  icon={
46
- <Image alt="" className="object-contain" height={14} src="/icons/fence.png" width={14} />
46
+ <Image alt="" className="object-contain" height={14} src="/icons/fence.webp" width={14} />
47
47
  }
48
48
  isHovered={isHovered}
49
49
  isLast={isLast}
@@ -53,7 +53,7 @@ export const FenceTreeNode = memo(function FenceTreeNode({
53
53
  <InlineRenameInput
54
54
  defaultName="Fence"
55
55
  isEditing={isEditing}
56
- node={node}
56
+ nodeId={node.id}
57
57
  onStartEditing={() => setIsEditing(true)}
58
58
  onStopEditing={() => setIsEditing(false)}
59
59
  />