@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
@@ -1,7 +1,14 @@
1
- import { emitter, useScene, type ZoneNode } from '@pascal-app/core'
1
+ import {
2
+ type AnyNodeId,
3
+ emitter,
4
+ useScene,
5
+ type ZoneNode,
6
+ } from '@pascal-app/core'
2
7
  import { useViewer } from '@pascal-app/viewer'
3
- import { Camera, Hexagon, Trash2 } from 'lucide-react'
8
+ import { Camera, Hexagon, Save, Trash2 } from 'lucide-react'
4
9
  import { useState } from 'react'
10
+ import { sfxEmitter } from './../../../../../lib/sfx-bus'
11
+ import { collectZoneContentIds } from './../../../../../lib/zone-content'
5
12
  import { ColorDot } from './../../../../../components/ui/primitives/color-dot'
6
13
  import {
7
14
  Popover,
@@ -10,6 +17,8 @@ import {
10
17
  } from './../../../../../components/ui/primitives/popover'
11
18
  import { cn } from './../../../../../lib/utils'
12
19
  import useEditor from './../../../../../store/use-editor'
20
+ import { ActionButton } from '../../../controls/action-button'
21
+ import { PanelSection } from '../../../controls/panel-section'
13
22
 
14
23
  function ZoneItem({ zone }: { zone: ZoneNode }) {
15
24
  const [cameraPopoverOpen, setCameraPopoverOpen] = useState(false)
@@ -26,6 +35,7 @@ function ZoneItem({ zone }: { zone: ZoneNode }) {
26
35
 
27
36
  const handleDelete = (e: React.MouseEvent) => {
28
37
  e.stopPropagation()
38
+ sfxEmitter.emit('sfx:structure-delete')
29
39
  deleteNode(zone.id)
30
40
  if (isSelected) {
31
41
  setSelection({ zoneId: null })
@@ -125,6 +135,8 @@ function ZoneItem({ zone }: { zone: ZoneNode }) {
125
135
  export function ZonePanel() {
126
136
  const nodes = useScene((state) => state.nodes)
127
137
  const currentLevelId = useViewer((state) => state.selection.levelId)
138
+ const selectedZoneId = useViewer((state) => state.selection.zoneId)
139
+ const setSelection = useViewer((state) => state.setSelection)
128
140
  const setPhase = useEditor((state) => state.setPhase)
129
141
  const setMode = useEditor((state) => state.setMode)
130
142
  const setTool = useEditor((state) => state.setTool)
@@ -133,6 +145,7 @@ export function ZonePanel() {
133
145
  const levelZones = Object.values(nodes).filter(
134
146
  (node): node is ZoneNode => node.type === 'zone' && node.parentId === currentLevelId,
135
147
  )
148
+ const selectedZone = levelZones.find((zone) => zone.id === selectedZoneId)
136
149
 
137
150
  const handleAddZone = () => {
138
151
  if (currentLevelId) {
@@ -142,6 +155,17 @@ export function ZonePanel() {
142
155
  }
143
156
  }
144
157
 
158
+ const deleteSelectedZone = (withContent: boolean) => {
159
+ if (!selectedZone) return
160
+ const scene = useScene.getState()
161
+ const ids = withContent
162
+ ? [selectedZone.id as AnyNodeId, ...collectZoneContentIds(scene.nodes, selectedZone)]
163
+ : [selectedZone.id as AnyNodeId]
164
+ sfxEmitter.emit('sfx:structure-delete')
165
+ scene.deleteNodes(Array.from(new Set(ids)))
166
+ setSelection({ selectedIds: [], zoneId: null })
167
+ }
168
+
145
169
  if (!currentLevelId) {
146
170
  return (
147
171
  <div className="px-3 py-4 text-muted-foreground text-sm">
@@ -162,6 +186,31 @@ export function ZonePanel() {
162
186
  ) : (
163
187
  levelZones.map((zone) => <ZoneItem key={zone.id} zone={zone} />)
164
188
  )}
189
+ {selectedZone ? (
190
+ <PanelSection className="mt-2 border-t" title="Actions">
191
+ <ActionButton
192
+ className="w-full flex-none"
193
+ icon={<Save className="h-4 w-4" />}
194
+ label="Save to catalog"
195
+ onClick={() => emitter.emit('room-preset:create', { zoneId: selectedZone.id })}
196
+ type="button"
197
+ />
198
+ <ActionButton
199
+ className="w-full flex-none"
200
+ icon={<Trash2 className="h-4 w-4 text-red-400" />}
201
+ label="Delete"
202
+ onClick={() => deleteSelectedZone(false)}
203
+ type="button"
204
+ />
205
+ <ActionButton
206
+ className="w-full flex-none"
207
+ icon={<Trash2 className="h-4 w-4 text-red-400" />}
208
+ label="Delete with contents"
209
+ onClick={() => deleteSelectedZone(true)}
210
+ type="button"
211
+ />
212
+ </PanelSection>
213
+ ) : null}
165
214
  </div>
166
215
  )
167
216
  }
@@ -1,13 +1,18 @@
1
1
  'use client'
2
2
 
3
3
  import type { ReactNode } from 'react'
4
+ import { editorHostPanelRegistry } from '../../../lib/plugin-panels'
5
+ import { triggerSFX } from './../../../lib/sfx-bus'
4
6
  import { cn } from './../../../lib/utils'
7
+ import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '../primitives/tooltip'
5
8
 
6
9
  export type SidebarTab = {
7
10
  id: string
8
11
  label: string
9
12
  mobileDefaultSnap?: number
10
13
  mobileIcon?: ReactNode
14
+ /** Desktop icon shown in the vertical rail (v2 layout). */
15
+ icon?: ReactNode
11
16
  }
12
17
 
13
18
  interface TabBarProps {
@@ -30,7 +35,11 @@ export function TabBar({ tabs, activeTab, onTabChange }: TabBarProps) {
30
35
  : 'text-muted-foreground hover:bg-accent/50 hover:text-foreground',
31
36
  )}
32
37
  key={tab.id}
33
- onClick={() => onTabChange(tab.id)}
38
+ onClick={() => {
39
+ triggerSFX('sfx:menu-click')
40
+ onTabChange(tab.id)
41
+ }}
42
+ onMouseEnter={() => triggerSFX('sfx:menu-hover')}
34
43
  type="button"
35
44
  >
36
45
  {tab.label}
@@ -40,3 +49,68 @@ export function TabBar({ tabs, activeTab, onTabChange }: TabBarProps) {
40
49
  </div>
41
50
  )
42
51
  }
52
+
53
+ interface IconRailProps {
54
+ tabs: SidebarTab[]
55
+ /** Highlighted tab. Stays highlighted while the panel is collapsed. */
56
+ activeTab: string
57
+ /** True when the panel beside the rail is collapsed. */
58
+ collapsed: boolean
59
+ /** Clicking a rail icon: switch tab, or toggle the panel (see layout). */
60
+ onIconClick: (id: string) => void
61
+ }
62
+
63
+ /**
64
+ * Vertical icon rail for the v2 left column. Always visible (even when the
65
+ * panel is collapsed) so the user can reopen the panel by clicking an icon.
66
+ * The label renders as a hover tooltip on the right.
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
+
104
+ return (
105
+ <TooltipProvider delayDuration={0} disableHoverableContent>
106
+ <div className="flex h-full w-14 shrink-0 flex-col items-center gap-1 border-border/50 border-r py-2">
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
+ )}
113
+ </div>
114
+ </TooltipProvider>
115
+ )
116
+ }
@@ -0,0 +1,132 @@
1
+ 'use client'
2
+
3
+ import { Icon } from '@iconify/react'
4
+ import { type IconRef, useScene } from '@pascal-app/core'
5
+ import { Plus } from 'lucide-react'
6
+ import {
7
+ type ComponentType,
8
+ lazy,
9
+ type ReactNode,
10
+ Suspense,
11
+ useEffect,
12
+ useSyncExternalStore,
13
+ } from 'react'
14
+ import useEditor from '../../../store/use-editor'
15
+ import { editorHostPanelRegistry, type EditorHostPanel } from '../../../lib/plugin-panels'
16
+ import { ErrorBoundary } from '../primitives/error-boundary'
17
+ import type { ExtraPanel } from './icon-rail'
18
+ import { PluginsPanel } from './panels/plugins-panel'
19
+
20
+ const pluginsManagerPanel: ExtraPanel = {
21
+ id: 'plugins',
22
+ label: 'Plugins',
23
+ icon: <Plus className="h-5 w-5" />,
24
+ component: PluginsPanel,
25
+ }
26
+
27
+ /** Resolve a plugin's {@link IconRef} into a rail-sized React node. Mirrors the
28
+ * inspector's `renderIcon`, sized for the 24px icon-rail button. */
29
+ function renderIconRef(ref: IconRef): ReactNode {
30
+ if (ref.kind === 'url') {
31
+ return <img alt="" className="h-5 w-5 object-contain" src={ref.src} />
32
+ }
33
+ if (ref.kind === 'iconify') {
34
+ return <Icon height={20} icon={ref.name} width={20} />
35
+ }
36
+ if (ref.kind === 'svg') {
37
+ return (
38
+ <svg height={20} viewBox={ref.viewBox} width={20}>
39
+ <path d={ref.path} fill="currentColor" />
40
+ </svg>
41
+ )
42
+ }
43
+ const LazyIcon = lazy(ref.module)
44
+ return (
45
+ <Suspense fallback={null}>
46
+ <LazyIcon />
47
+ </Suspense>
48
+ )
49
+ }
50
+
51
+ function PluginPanelCrashed({ label }: { label: string }) {
52
+ return (
53
+ <div className="flex flex-col gap-2 p-4 text-sm">
54
+ <p className="font-medium text-sidebar-foreground">"{label}" plugin crashed</p>
55
+ <p className="text-sidebar-foreground/50 text-xs">
56
+ This panel hit an error and was unloaded for this session. The rest of the editor is
57
+ unaffected — reload to try again.
58
+ </p>
59
+ </div>
60
+ )
61
+ }
62
+
63
+ // `React.lazy` must be called once per loader so the resolved component keeps a
64
+ // stable identity across renders (otherwise switching panels remounts the tree
65
+ // every time the sidebar re-renders). Cache the wrapped component by its loader.
66
+ const wrappedPanelCache = new WeakMap<EditorHostPanel['component'], ComponentType>()
67
+
68
+ function resolvePanelComponent(panel: EditorHostPanel): ComponentType {
69
+ const cached = wrappedPanelCache.get(panel.component)
70
+ if (cached) return cached
71
+ const Lazy = lazy(panel.component)
72
+ const Wrapped: ComponentType = () => (
73
+ <ErrorBoundary fallback={<PluginPanelCrashed label={panel.label} />}>
74
+ <Suspense fallback={<div className="p-4 text-sidebar-foreground/50 text-sm">Loading…</div>}>
75
+ <Lazy />
76
+ </Suspense>
77
+ </ErrorBoundary>
78
+ )
79
+ Wrapped.displayName = `PluginPanel(${panel.id})`
80
+ wrappedPanelCache.set(panel.component, Wrapped)
81
+ return Wrapped
82
+ }
83
+
84
+ /**
85
+ * Merge host-registered panels (from the observable {@link editorHostPanelRegistry})
86
+ * with the host's `extraPanels`, returning the combined list the icon rail and
87
+ * panel-content area render. Host panels keep their leading order and win on id
88
+ * collisions. Subscribes to the registry so a panel that registers after the
89
+ * first render makes the rail re-render.
90
+ *
91
+ * Panels are filtered by the current workspace: a panel surfaces only in the
92
+ * workspaces it declares (`EditorHostPanel.workspaces`, default `['edit']`), so an
93
+ * authoring panel like Nature doesn't ride into the studio rail.
94
+ */
95
+ export function useHostPanels(hostPanels?: ExtraPanel[]): ExtraPanel[] {
96
+ const registered = useSyncExternalStore(
97
+ editorHostPanelRegistry.subscribe,
98
+ editorHostPanelRegistry.getSnapshot,
99
+ editorHostPanelRegistry.getSnapshot,
100
+ )
101
+ const workspaceMode = useEditor((s) => s.workspaceMode)
102
+ const installedPlugins = useScene((s) => s.installedPlugins)
103
+ const hostIds = new Set(hostPanels?.map((p) => p.id))
104
+
105
+ useEffect(() => {
106
+ const scene = useScene.getState()
107
+ if (scene.hasExplicitPluginInstallState) return
108
+ const defaults = editorHostPanelRegistry.getDefaultInstalledPluginIds()
109
+ if (defaults.every((pluginId) => scene.installedPlugins.includes(pluginId))) return
110
+ scene.setInstalledPlugins([...scene.installedPlugins, ...defaults], { explicit: false })
111
+ }, [registered])
112
+
113
+ const fromRegistry = registered
114
+ .filter(
115
+ (p) =>
116
+ !hostIds.has(p.id) &&
117
+ (p.workspaces ?? ['edit']).includes(workspaceMode) &&
118
+ (!p.pluginId || installedPlugins.includes(p.pluginId)),
119
+ )
120
+ .map(
121
+ (p): ExtraPanel => ({
122
+ id: p.id,
123
+ label: p.label,
124
+ icon: renderIconRef(p.icon),
125
+ component: resolvePanelComponent(p),
126
+ pluginId: p.pluginId,
127
+ }),
128
+ )
129
+ const manager =
130
+ workspaceMode === 'edit' && !hostIds.has(pluginsManagerPanel.id) ? [pluginsManagerPanel] : []
131
+ return [...(hostPanels ?? []), ...fromRegistry, ...manager]
132
+ }
@@ -0,0 +1,40 @@
1
+ import { describe, expect, test } from 'bun:test'
2
+ import type { AnyNode } from '@pascal-app/core'
3
+ import { resolveAssetSnapTarget, resolveNodeSnapTarget } from './snap-target-badge'
4
+
5
+ describe('resolveAssetSnapTarget', () => {
6
+ test('maps wall-hosted catalog assets to a wall badge', () => {
7
+ expect(resolveAssetSnapTarget('wall')).toBe('wall')
8
+ expect(resolveAssetSnapTarget('wall-side')).toBe('wall')
9
+ })
10
+
11
+ test('maps ceiling-hosted catalog assets to a ceiling badge', () => {
12
+ expect(resolveAssetSnapTarget('ceiling')).toBe('ceiling')
13
+ })
14
+
15
+ test('does not badge floor assets', () => {
16
+ expect(resolveAssetSnapTarget(undefined)).toBeNull()
17
+ })
18
+ })
19
+
20
+ describe('resolveNodeSnapTarget', () => {
21
+ test('prefers roof attachment when a node is hosted by a roof segment', () => {
22
+ const node = {
23
+ id: 'window_1',
24
+ type: 'window',
25
+ roofSegmentId: 'roof-segment_1',
26
+ } as unknown as AnyNode
27
+
28
+ expect(resolveNodeSnapTarget(node)).toBe('roof')
29
+ })
30
+
31
+ test('badges gutter-hosted downspouts as roof accessories', () => {
32
+ const node = {
33
+ id: 'downspout_1',
34
+ type: 'downspout',
35
+ gutterId: 'gutter_1',
36
+ } as unknown as AnyNode
37
+
38
+ expect(resolveNodeSnapTarget(node)).toBe('roof')
39
+ })
40
+ })
@@ -0,0 +1,88 @@
1
+ import type { AnyNode, AssetInput } from '@pascal-app/core'
2
+ import type { ReactNode } from 'react'
3
+ import { cn } from '../../lib/utils'
4
+
5
+ export type SnapTarget = 'wall' | 'ceiling' | 'roof'
6
+ export type SnapTargetBadgeSize = 'tile' | 'tree'
7
+
8
+ const SNAP_TARGET_ICONS: Record<SnapTarget, string> = {
9
+ wall: '/icons/wall.webp',
10
+ ceiling: '/icons/ceiling.webp',
11
+ roof: '/icons/roof.webp',
12
+ }
13
+
14
+ const SNAP_TARGET_LABELS: Record<SnapTarget, string> = {
15
+ wall: 'Wall attachment',
16
+ ceiling: 'Ceiling attachment',
17
+ roof: 'Roof attachment',
18
+ }
19
+
20
+ const SNAP_TARGET_BADGE_SIZE_CLASSES: Record<SnapTargetBadgeSize, string> = {
21
+ tile: 'h-6 w-6 rounded-md',
22
+ tree: 'h-3.5 w-3.5 rounded-[3px]',
23
+ }
24
+
25
+ const SNAP_TARGET_ICON_SIZE_CLASSES: Record<SnapTargetBadgeSize, string> = {
26
+ tile: 'h-[18px] w-[18px]',
27
+ tree: 'h-2.5 w-2.5',
28
+ }
29
+
30
+ export function resolveAssetSnapTarget(attachTo: AssetInput['attachTo']): SnapTarget | null {
31
+ if (attachTo === 'wall' || attachTo === 'wall-side') return 'wall'
32
+ if (attachTo === 'ceiling') return 'ceiling'
33
+ return null
34
+ }
35
+
36
+ export function resolveNodeSnapTarget(node: AnyNode | null | undefined): SnapTarget | null {
37
+ if (!node) return null
38
+ if ('roofSegmentId' in node && typeof node.roofSegmentId === 'string') return 'roof'
39
+ if (node.type === 'downspout') return 'roof'
40
+ if (node.type === 'door' || node.type === 'window') return 'wall'
41
+ if (node.type === 'item') return resolveAssetSnapTarget(node.asset?.attachTo)
42
+ return null
43
+ }
44
+
45
+ export function SnapTargetBadge({
46
+ className,
47
+ size = 'tile',
48
+ target,
49
+ }: {
50
+ className?: string
51
+ size?: SnapTargetBadgeSize
52
+ target: SnapTarget
53
+ }) {
54
+ return (
55
+ <span
56
+ className={cn(
57
+ 'flex items-center justify-center bg-black/65 ring-1 ring-white/20',
58
+ SNAP_TARGET_BADGE_SIZE_CLASSES[size],
59
+ className,
60
+ )}
61
+ >
62
+ <img
63
+ alt={SNAP_TARGET_LABELS[target]}
64
+ className={cn('object-contain', SNAP_TARGET_ICON_SIZE_CLASSES[size])}
65
+ src={SNAP_TARGET_ICONS[target]}
66
+ />
67
+ </span>
68
+ )
69
+ }
70
+
71
+ export function SnapTargetIcon({
72
+ children,
73
+ target,
74
+ }: {
75
+ children: ReactNode
76
+ target: SnapTarget
77
+ }) {
78
+ return (
79
+ <span className="relative inline-flex h-5 w-5 items-center justify-center">
80
+ {children}
81
+ <SnapTargetBadge
82
+ className="-right-1.5 -bottom-1.5 absolute"
83
+ size="tree"
84
+ target={target}
85
+ />
86
+ </span>
87
+ )
88
+ }