@pascal-app/editor 1.0.3 → 1.0.4

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 (440) hide show
  1. package/package.json +11 -9
  2. package/src/components/editor/action-menu-button.tsx +83 -0
  3. package/src/components/editor/bake-thumbnail.tsx +315 -4
  4. package/src/components/editor/capture-level-picker.tsx +72 -0
  5. package/src/components/editor/custom-camera-controls.tsx +202 -57
  6. package/src/components/editor/delete-confirmation-dialog.tsx +297 -7
  7. package/src/components/editor/duplicate-node.ts +244 -0
  8. package/src/components/editor/editor-layout-mobile.tsx +36 -25
  9. package/src/components/editor/editor-layout-v2.tsx +131 -58
  10. package/src/components/editor/export-manager.tsx +34 -8
  11. package/src/components/editor/first-person/build-collider-world.test.ts +51 -0
  12. package/src/components/editor/first-person/build-collider-world.ts +6 -2
  13. package/src/components/editor/first-person/mechanism-targets.test.ts +75 -0
  14. package/src/components/editor/first-person/mechanism-targets.ts +36 -0
  15. package/src/components/editor/first-person-controls.tsx +205 -2
  16. package/src/components/editor/floating-action-menu.tsx +63 -207
  17. package/src/components/editor/floor-aware-camera-controls.test.ts +442 -0
  18. package/src/components/editor/floor-aware-camera-controls.ts +324 -0
  19. package/src/components/editor/floor-openings-overlay.tsx +243 -0
  20. package/src/components/editor/floor-region-controls.tsx +846 -0
  21. package/src/components/editor/floor-region-webgpu-materials.test.ts +12 -0
  22. package/src/components/editor/floor-surface.test.ts +165 -0
  23. package/src/components/editor/floor-surface.ts +182 -0
  24. package/src/components/editor/floorplan-background-selection.test.ts +11 -2
  25. package/src/components/editor/floorplan-background-selection.ts +1 -3
  26. package/src/components/editor/floorplan-panel.tsx +344 -342
  27. package/src/components/editor/footprint-height-handle.tsx +204 -0
  28. package/src/components/editor/group-actions.ts +54 -2
  29. package/src/components/editor/handles/handle-hit-priority.test.tsx +63 -0
  30. package/src/components/editor/handles/handle-hit-priority.tsx +37 -0
  31. package/src/components/editor/handles/swallow-next-click.test.ts +28 -0
  32. package/src/components/editor/handles/use-handle-drag.ts +269 -197
  33. package/src/components/editor/index.tsx +294 -169
  34. package/src/components/editor/join-walls-pill.tsx +44 -0
  35. package/src/components/editor/node-action-menu.tsx +68 -70
  36. package/src/components/editor/node-arrow-handles.tsx +25 -6
  37. package/src/components/editor/open-wall-ends-3d-layer.tsx +273 -0
  38. package/src/components/editor/open-wall-ends-hint.tsx +89 -0
  39. package/src/components/editor/quick-measurement-hud.tsx +3 -2
  40. package/src/components/editor/registry-action-contributions.test.tsx +63 -0
  41. package/src/components/editor/registry-action-contributions.tsx +80 -1
  42. package/src/components/editor/room-controls.tsx +847 -0
  43. package/src/components/editor/room-floor-highlight.tsx +128 -0
  44. package/src/components/editor/room-handles-2d.tsx +354 -0
  45. package/src/components/editor/room-handles.tsx +675 -0
  46. package/src/components/editor/room-highlight.tsx +225 -0
  47. package/src/components/editor/room-labels.test.ts +357 -0
  48. package/src/components/editor/room-labels.tsx +990 -0
  49. package/src/components/editor/room-transform-ghost.tsx +256 -0
  50. package/src/components/editor/room-webgpu-materials.test.ts +27 -0
  51. package/src/components/editor/selection-manager.tsx +451 -317
  52. package/src/components/editor/slab-hole-highlights.tsx +3 -0
  53. package/src/components/editor/snapshot-capture-overlay.tsx +4 -0
  54. package/src/components/editor/snapshot-capture.test.ts +52 -4
  55. package/src/components/editor/snapshot-capture.ts +30 -5
  56. package/src/components/editor/thumbnail-generator.test.ts +308 -0
  57. package/src/components/editor/thumbnail-generator.tsx +1035 -229
  58. package/src/components/editor/use-floorplan-background-placement.ts +31 -11
  59. package/src/components/editor/vector-edge-extractor.tsx +121 -0
  60. package/src/components/editor/views/host-view.tsx +61 -0
  61. package/src/components/editor/views/plugins-view.tsx +670 -0
  62. package/src/components/editor/views/scene-region.tsx +59 -0
  63. package/src/components/editor/views/use-editor-views.ts +57 -0
  64. package/src/components/editor/views/view-bar.tsx +220 -0
  65. package/src/components/editor/wall-justification.test.ts +140 -0
  66. package/src/components/editor/wall-move-side-handles.tsx +43 -142
  67. package/src/components/editor/wall-push-arrow.tsx +92 -0
  68. package/src/components/editor/wall-region-handles.tsx +509 -0
  69. package/src/components/editor/wall-snap-beacon-layer.tsx +3 -0
  70. package/src/components/editor-2d/floorplan-cursor-indicator-overlay.tsx +8 -4
  71. package/src/components/editor-2d/floorplan-open-wall-ends-layer.test.tsx +86 -0
  72. package/src/components/editor-2d/floorplan-open-wall-ends-layer.tsx +290 -0
  73. package/src/components/editor-2d/floorplan-registry-action-menu.tsx +11 -1
  74. package/src/components/editor-2d/floorplan-registry-move-overlay.tsx +113 -102
  75. package/src/components/editor-2d/renderers/floorplan-dimension-edit-overlay.test.ts +156 -0
  76. package/src/components/editor-2d/renderers/floorplan-dimension-edit-overlay.tsx +278 -0
  77. package/src/components/editor-2d/renderers/floorplan-dimension-renderer.tsx +77 -0
  78. package/src/components/editor-2d/renderers/{floorplan-draft-wall-measurement.tsx → floorplan-draft-measurement.tsx} +8 -8
  79. package/src/components/editor-2d/renderers/floorplan-geometry-renderer.tsx +4 -1
  80. package/src/components/editor-2d/renderers/floorplan-registry-layer.test.ts +38 -8
  81. package/src/components/editor-2d/renderers/floorplan-registry-layer.tsx +291 -227
  82. package/src/components/editor-2d/svg-paths.ts +12 -15
  83. package/src/components/procedural-items/inspector.tsx +40 -15
  84. package/src/components/systems/ceiling/ceiling-selection-affordance-system.test.ts +97 -14
  85. package/src/components/systems/ceiling/ceiling-selection-affordance-system.tsx +18 -18
  86. package/src/components/systems/zone/zone-system.test.ts +47 -0
  87. package/src/components/systems/zone/zone-system.tsx +29 -68
  88. package/src/components/tools/item/placement-history.test.tsx +474 -0
  89. package/src/components/tools/item/placement-strategies.ts +8 -4
  90. package/src/components/tools/item/use-draft-node.test.tsx +432 -1
  91. package/src/components/tools/item/use-draft-node.ts +166 -95
  92. package/src/components/tools/item/use-placement-coordinator.tsx +93 -46
  93. package/src/components/tools/paint-region/use-paint-tint.ts +37 -0
  94. package/src/components/tools/paint-region/wall-paint-region-tool.tsx +466 -0
  95. package/src/components/tools/registry/move-registry-node-tool.tsx +3 -1
  96. package/src/components/tools/select/box-select-tool.tsx +2 -12
  97. package/src/components/tools/select/plane-box-select-tool.test.tsx +79 -0
  98. package/src/components/tools/select/plane-box-select-tool.tsx +35 -22
  99. package/src/components/tools/select/select-candidates.test.ts +37 -4
  100. package/src/components/tools/select/select-candidates.ts +15 -11
  101. package/src/components/tools/shared/cursor-sphere.tsx +36 -12
  102. package/src/components/tools/site/site-boundary-editor.tsx +1 -2
  103. package/src/components/tools/stair/stair-defaults.ts +0 -3
  104. package/src/components/tools/stair/stair-tool.tsx +90 -42
  105. package/src/components/tools/tool-manager.tsx +46 -20
  106. package/src/components/tools/wall/wall-drafting.test.ts +19 -4
  107. package/src/components/tools/zone/zone-tool.tsx +4 -12
  108. package/src/components/ui/action-menu/action-button.tsx +3 -0
  109. package/src/components/ui/action-menu/camera-actions.tsx +2 -1
  110. package/src/components/ui/action-menu/control-modes.tsx +2 -35
  111. package/src/components/ui/action-menu/measurement-control.tsx +3 -10
  112. package/src/components/ui/build-panel/build-panel.tsx +235 -0
  113. package/src/components/ui/command-palette/editor-commands.tsx +4 -30
  114. package/src/components/ui/command-palette/index.tsx +5 -24
  115. package/src/components/ui/controls/finish-swatch.tsx +110 -0
  116. package/src/components/ui/controls/material-paint-panel.tsx +97 -26
  117. package/src/components/ui/controls/material-picker.tsx +4 -1
  118. package/src/components/ui/controls/scene-material-list.tsx +4 -32
  119. package/src/components/ui/controls/select-control.tsx +65 -0
  120. package/src/components/ui/floating-level-selector.tsx +9 -14
  121. package/src/components/ui/helpers/building-helper.tsx +4 -1
  122. package/src/components/ui/helpers/contextual-helper-panel.tsx +154 -70
  123. package/src/components/ui/helpers/helper-manager.test.tsx +69 -0
  124. package/src/components/ui/helpers/helper-manager.tsx +325 -14
  125. package/src/components/ui/helpers/item-helper.tsx +8 -0
  126. package/src/components/ui/helpers/registered-tool-helper.tsx +8 -1
  127. package/src/components/ui/icon-ref.tsx +45 -1
  128. package/src/components/ui/level-duplicate-dialog.tsx +1 -1
  129. package/src/components/ui/node-icon-image.tsx +16 -0
  130. package/src/components/ui/panels/collections/collections-popover.tsx +20 -14
  131. package/src/components/ui/panels/floor-foundation-panel.tsx +551 -0
  132. package/src/components/ui/panels/floor-opening-panel.tsx +143 -0
  133. package/src/components/ui/panels/mobile-panel-sheet.tsx +2 -2
  134. package/src/components/ui/panels/mobile-selection-bar.tsx +2 -2
  135. package/src/components/ui/panels/multi-field-value.test.ts +26 -3
  136. package/src/components/ui/panels/multi-field-value.ts +6 -3
  137. package/src/components/ui/panels/multi-parametric-inspector.tsx +52 -2
  138. package/src/components/ui/panels/multi-selection-panel.tsx +43 -4
  139. package/src/components/ui/panels/node-display.ts +8 -3
  140. package/src/components/ui/panels/panel-manager.tsx +103 -68
  141. package/src/components/ui/panels/panel-wrapper.tsx +78 -36
  142. package/src/components/ui/panels/parametric-field-control.tsx +1 -1
  143. package/src/components/ui/panels/parametric-inspector.tsx +4 -4
  144. package/src/components/ui/panels/reference-panel.tsx +2 -3
  145. package/src/components/ui/panels/room-construction-rows.tsx +774 -0
  146. package/src/components/ui/panels/room-name-options.test.ts +55 -0
  147. package/src/components/ui/panels/room-name-options.ts +47 -0
  148. package/src/components/ui/panels/room-openings-field.tsx +110 -0
  149. package/src/components/ui/panels/room-panel.tsx +725 -0
  150. package/src/components/ui/panels/shape-choice.tsx +58 -0
  151. package/src/components/ui/panels/wall-region-list.tsx +109 -0
  152. package/src/components/ui/primitives/color-dot.tsx +36 -24
  153. package/src/components/ui/primitives/sidebar.tsx +2 -0
  154. package/src/components/ui/right-stack.test.ts +29 -0
  155. package/src/components/ui/right-stack.tsx +145 -0
  156. package/src/components/ui/sidebar/icon-rail.tsx +5 -0
  157. package/src/components/ui/sidebar/panels/settings-panel/index.tsx +31 -4
  158. package/src/components/ui/sidebar/panels/settings-panel/keyboard-shortcuts-dialog.tsx +5 -5
  159. package/src/components/ui/sidebar/panels/site-panel/ceiling-tree-node.tsx +4 -4
  160. package/src/components/ui/sidebar/panels/site-panel/chimney-tree-node.tsx +1 -5
  161. package/src/components/ui/sidebar/panels/site-panel/collection-tree-node.tsx +197 -0
  162. package/src/components/ui/sidebar/panels/site-panel/column-tree-node.tsx +1 -5
  163. package/src/components/ui/sidebar/panels/site-panel/door-tree-node.tsx +1 -5
  164. package/src/components/ui/sidebar/panels/site-panel/dormer-tree-node.tsx +1 -5
  165. package/src/components/ui/sidebar/panels/site-panel/elevator-tree-node.tsx +1 -5
  166. package/src/components/ui/sidebar/panels/site-panel/fence-tree-node.tsx +24 -11
  167. package/src/components/ui/sidebar/panels/site-panel/gutter-tree-node.tsx +1 -5
  168. package/src/components/ui/sidebar/panels/site-panel/index.tsx +176 -211
  169. package/src/components/ui/sidebar/panels/site-panel/inline-rename-input.tsx +30 -8
  170. package/src/components/ui/sidebar/panels/site-panel/item-tree-node.tsx +1 -3
  171. package/src/components/ui/sidebar/panels/site-panel/level-tree-node.tsx +4 -1
  172. package/src/components/ui/sidebar/panels/site-panel/registry-tree-node.tsx +1 -3
  173. package/src/components/ui/sidebar/panels/site-panel/roof-tree-node.tsx +1 -4
  174. package/src/components/ui/sidebar/panels/site-panel/shelf-tree-node.tsx +1 -3
  175. package/src/components/ui/sidebar/panels/site-panel/slab-tree-node.tsx +21 -13
  176. package/src/components/ui/sidebar/panels/site-panel/solar-panel-tree-node.tsx +1 -5
  177. package/src/components/ui/sidebar/panels/site-panel/spawn-tree-node.tsx +1 -5
  178. package/src/components/ui/sidebar/panels/site-panel/stair-tree-node.tsx +1 -5
  179. package/src/components/ui/sidebar/panels/site-panel/tree-node.tsx +0 -27
  180. package/src/components/ui/sidebar/panels/site-panel/unit-tree-node.tsx +2 -3
  181. package/src/components/ui/sidebar/panels/site-panel/wall-tree-node.tsx +1 -5
  182. package/src/components/ui/sidebar/panels/site-panel/window-tree-node.tsx +1 -5
  183. package/src/components/ui/sidebar/panels/site-panel/zone-tree-node.tsx +9 -4
  184. package/src/components/ui/sidebar/panels/zone-panel/index.tsx +0 -2
  185. package/src/components/ui/sidebar/tab-bar.tsx +10 -0
  186. package/src/components/ui/sidebar/use-plugin-panels.tsx +15 -5
  187. package/src/components/viewer/floorplan-compass-button.tsx +9 -1
  188. package/src/components/viewer/floorplan-preview.tsx +5 -6
  189. package/src/components/viewer/viewer-hover-label.tsx +34 -0
  190. package/src/components/viewer/viewer-rooms.tsx +175 -0
  191. package/src/components/viewer/viewer-scene-header.tsx +181 -145
  192. package/src/components/viewer/viewer-selection-manager.tsx +124 -0
  193. package/src/components/viewer/viewer-stage.tsx +2 -2
  194. package/src/components/viewer-zone-system.tsx +3 -8
  195. package/src/hooks/use-auto-save.integration.test.tsx +123 -9
  196. package/src/hooks/use-auto-save.ts +34 -8
  197. package/src/hooks/use-keyboard.test.ts +178 -3
  198. package/src/hooks/use-keyboard.ts +89 -71
  199. package/src/hooks/use-open-wall-ends.test.ts +99 -0
  200. package/src/hooks/use-open-wall-ends.ts +238 -0
  201. package/src/hooks/use-placement-notice.test.ts +48 -0
  202. package/src/hooks/use-placement-notice.ts +42 -0
  203. package/src/hooks/use-selected-room.ts +143 -0
  204. package/src/index.tsx +246 -39
  205. package/src/lib/ceiling-edit-session.test.ts +321 -0
  206. package/src/lib/ceiling-edit-session.ts +206 -0
  207. package/src/lib/ceiling-region-session.test.ts +179 -0
  208. package/src/lib/continuation.test.ts +19 -1
  209. package/src/lib/continuation.ts +25 -4
  210. package/src/lib/door-interaction.ts +14 -0
  211. package/src/lib/editor-level-display.test.ts +96 -0
  212. package/src/lib/editor-level-display.ts +48 -0
  213. package/src/lib/editor-views.ts +144 -0
  214. package/src/lib/export-geometry-wait.test.ts +90 -0
  215. package/src/lib/floor-edit-session.ts +153 -0
  216. package/src/lib/floor-footprints.test.ts +563 -0
  217. package/src/lib/floor-footprints.ts +395 -0
  218. package/src/lib/floor-foundation-support.test.ts +283 -0
  219. package/src/lib/floor-opening-draft.ts +218 -0
  220. package/src/lib/floor-openings.test.ts +216 -0
  221. package/src/lib/floor-region-geometry.ts +191 -0
  222. package/src/lib/floor-region-session.test.ts +485 -0
  223. package/src/lib/floor-region-session.ts +589 -0
  224. package/src/lib/floor-region-snap.ts +101 -0
  225. package/src/lib/floor-step-hover.test.ts +32 -0
  226. package/src/lib/floorplan/annotation-visibility.ts +17 -0
  227. package/src/lib/floorplan/dimension-drive.test.ts +360 -0
  228. package/src/lib/floorplan/dimension-drive.ts +479 -0
  229. package/src/lib/floorplan/floorplan-export.test.ts +12 -8
  230. package/src/lib/floorplan/floorplan-export.tsx +32 -8
  231. package/src/lib/floorplan/floorplan-extension.ts +32 -0
  232. package/src/lib/floorplan/floorplan-mode.test.ts +4 -0
  233. package/src/lib/floorplan/floorplan-mode.ts +2 -0
  234. package/src/lib/floorplan/floorplan-pdfkit-document.ts +339 -18
  235. package/src/lib/floorplan/floorplan-pdfkit-renderer.test.ts +138 -8
  236. package/src/lib/floorplan/floorplan-pdfkit-renderer.ts +56 -25
  237. package/src/lib/floorplan/floorplan-plugin-install.test.tsx +138 -0
  238. package/src/lib/floorplan/floorplan-readonly.ts +6 -0
  239. package/src/lib/floorplan/geometry.ts +13 -6
  240. package/src/lib/floorplan/index.ts +0 -9
  241. package/src/lib/floorplan/items.ts +5 -2
  242. package/src/lib/floorplan/marks.test.ts +213 -0
  243. package/src/lib/floorplan/marks.ts +238 -0
  244. package/src/lib/floorplan/open-wall-ends.test.ts +268 -0
  245. package/src/lib/floorplan/open-wall-ends.ts +162 -0
  246. package/src/lib/floorplan/pdf-fonts/GeistMono-Bold.ttf +0 -0
  247. package/src/lib/floorplan/pdf-fonts/GeistMono-Regular.ttf +0 -0
  248. package/src/lib/floorplan/pdf-fonts/LICENSE-GeistMono.txt +92 -0
  249. package/src/lib/floorplan/pdf-fonts/LICENSE-LiberationSans.txt +102 -0
  250. package/src/lib/floorplan/pdf-fonts/LiberationSans-Bold.ttf +0 -0
  251. package/src/lib/floorplan/pdf-fonts/LiberationSans-Regular.ttf +0 -0
  252. package/src/lib/floorplan/schedules.test.ts +189 -0
  253. package/src/lib/floorplan/schedules.ts +339 -0
  254. package/src/lib/floorplan/sheet-export.test.ts +100 -0
  255. package/src/lib/floorplan/sheet-export.ts +318 -0
  256. package/src/lib/floorplan/site-plan/build-site-plan-drawing.ts +779 -0
  257. package/src/lib/floorplan/site-plan/contours.test.ts +43 -0
  258. package/src/lib/floorplan/site-plan/contributors.ts +97 -0
  259. package/src/lib/floorplan/site-plan/coverage.ts +232 -0
  260. package/src/lib/floorplan/site-plan/drawing-type-switch.test.ts +15 -0
  261. package/src/lib/floorplan/site-plan/drawing-type-switch.tsx +86 -0
  262. package/src/lib/floorplan/site-plan/front-edge.test.ts +103 -0
  263. package/src/lib/floorplan/site-plan/front-edge.ts +156 -0
  264. package/src/lib/floorplan/site-plan/geometry.test.ts +275 -0
  265. package/src/lib/floorplan/site-plan/geometry.ts +557 -0
  266. package/src/lib/floorplan/site-plan/index.ts +71 -0
  267. package/src/lib/floorplan/site-plan/site-annotations.test.ts +366 -0
  268. package/src/lib/floorplan/site-plan/site-annotations.ts +1083 -0
  269. package/src/lib/floorplan/site-plan/site-parts.ts +379 -0
  270. package/src/lib/floorplan/site-plan/site-plan-layer.tsx +138 -0
  271. package/src/lib/floorplan/site-plan/street-name.test.ts +69 -0
  272. package/src/lib/floorplan/site-plan/street-name.ts +302 -0
  273. package/src/lib/floorplan/site-plan/yards.test.ts +69 -0
  274. package/src/lib/floorplan/types.ts +1 -27
  275. package/src/lib/fresh-planar-placement.test.ts +36 -0
  276. package/src/lib/fresh-planar-placement.ts +59 -36
  277. package/src/lib/geometry-script/author.ts +152 -0
  278. package/src/lib/geometry-script/client.ts +152 -0
  279. package/src/lib/geometry-script/protocol.ts +12 -0
  280. package/src/lib/gesture-lifecycle.test.ts +595 -0
  281. package/src/lib/gesture-lifecycle.ts +290 -0
  282. package/src/lib/glb-export-zone-holes.test.ts +41 -0
  283. package/src/lib/glb-export.test.ts +485 -0
  284. package/src/lib/glb-export.ts +237 -119
  285. package/src/lib/history.test.ts +6 -0
  286. package/src/lib/history.ts +7 -0
  287. package/src/lib/hud-title.ts +109 -0
  288. package/src/lib/hydration-gated-storage.ts +49 -0
  289. package/src/lib/ifc-export.test.ts +174 -0
  290. package/src/lib/ifc-export.ts +201 -0
  291. package/src/lib/ifc-parts.ts +15 -0
  292. package/src/lib/inspector-expanded.test.ts +44 -0
  293. package/src/lib/inspector-expanded.ts +36 -0
  294. package/src/lib/interaction/scope.ts +18 -0
  295. package/src/lib/keyboard-pan.test.ts +129 -7
  296. package/src/lib/keyboard-pan.ts +161 -12
  297. package/src/lib/live-gesture-round-trip.test.ts +360 -0
  298. package/src/lib/lot/__fixtures__/map-dossier-miami-shores.json +1 -0
  299. package/src/lib/lot/__fixtures__/map-dossier-stpete.json +1 -0
  300. package/src/lib/lot/clean-ring.test.ts +186 -0
  301. package/src/lib/lot/clean-ring.ts +217 -0
  302. package/src/lib/lot/dossier.test.ts +202 -0
  303. package/src/lib/lot/dossier.ts +498 -0
  304. package/src/lib/lot/drop-in.test.ts +326 -0
  305. package/src/lib/lot/drop-in.ts +407 -0
  306. package/src/lib/lot/index.ts +43 -0
  307. package/src/lib/lot/lot-patch.test.ts +340 -0
  308. package/src/lib/lot/lot-patch.ts +394 -0
  309. package/src/lib/lot/parcel-provider.ts +43 -0
  310. package/src/lib/lot/terrain.test.ts +161 -0
  311. package/src/lib/lot/terrain.ts +266 -0
  312. package/src/lib/material-catalog-model.ts +3 -1
  313. package/src/lib/material-paint-panel-model.ts +9 -5
  314. package/src/lib/material-paint.ts +4 -0
  315. package/src/lib/mezzanine-actions.test.ts +342 -0
  316. package/src/lib/mezzanine-draft.test.ts +501 -0
  317. package/src/lib/mezzanine-draft.ts +278 -0
  318. package/src/lib/mezzanine-messages.ts +26 -0
  319. package/src/lib/mezzanine-selection.test.ts +207 -0
  320. package/src/lib/mezzanine-stairs.ts +46 -0
  321. package/src/lib/model-export.ts +3 -1
  322. package/src/lib/node-action-movement.ts +17 -6
  323. package/src/lib/node-display-label.ts +37 -0
  324. package/src/lib/orbit-floor-clearance.test.ts +165 -0
  325. package/src/lib/orbit-floor-clearance.ts +106 -0
  326. package/src/lib/paint-commit-route.test.ts +37 -0
  327. package/src/lib/paint-commit-route.ts +17 -0
  328. package/src/lib/paint-eyedropper.test.tsx +165 -0
  329. package/src/lib/paint-eyedropper.ts +99 -0
  330. package/src/lib/paint-pass-through.test.ts +111 -0
  331. package/src/lib/paint-pass-through.ts +76 -0
  332. package/src/lib/paint-region-hover.ts +25 -0
  333. package/src/lib/paint-region-mode.ts +154 -0
  334. package/src/lib/paint-regions.test.ts +166 -0
  335. package/src/lib/paint-regions.ts +192 -0
  336. package/src/lib/paint-scope.test.ts +317 -35
  337. package/src/lib/paint-scope.ts +266 -25
  338. package/src/lib/panel-rows.ts +21 -0
  339. package/src/lib/parametric-node-update.ts +1 -1
  340. package/src/lib/plate-paint-affected.test.ts +300 -0
  341. package/src/lib/plate-paint-affected.ts +177 -0
  342. package/src/lib/plugin-chat.ts +50 -0
  343. package/src/lib/plugin-panels.test.ts +25 -0
  344. package/src/lib/plugin-panels.ts +94 -0
  345. package/src/lib/print-shell-compiler.ts +1 -0
  346. package/src/lib/registered-draft-snap.ts +12 -0
  347. package/src/lib/roof-duplication.ts +10 -2
  348. package/src/lib/room-assembly-overlay.test.ts +275 -0
  349. package/src/lib/room-assembly-overlay.ts +266 -0
  350. package/src/lib/room-built-on.test.ts +379 -0
  351. package/src/lib/room-built-on.ts +138 -0
  352. package/src/lib/room-construction-commands.ts +712 -0
  353. package/src/lib/room-construction.test.ts +746 -0
  354. package/src/lib/room-construction.ts +385 -0
  355. package/src/lib/room-controls-phase6.test.ts +630 -0
  356. package/src/lib/room-divide-session.ts +523 -0
  357. package/src/lib/room-handle-drag.ts +1068 -0
  358. package/src/lib/room-name-catalog.test.ts +90 -0
  359. package/src/lib/room-name-catalog.ts +168 -0
  360. package/src/lib/room-push-dimensions.test.ts +311 -0
  361. package/src/lib/room-push-dimensions.ts +251 -0
  362. package/src/lib/room-selection-commands.ts +62 -0
  363. package/src/lib/room-selection.test.ts +973 -0
  364. package/src/lib/room-selection.ts +417 -0
  365. package/src/lib/room-structure-commands.ts +94 -0
  366. package/src/lib/room-structure.test.ts +921 -0
  367. package/src/lib/room-transform-session.ts +570 -0
  368. package/src/lib/room-zone-routing.test.ts +294 -0
  369. package/src/lib/room-zone-routing.ts +197 -0
  370. package/src/lib/scene-clipboard.test.ts +364 -0
  371. package/src/lib/scene-clipboard.ts +229 -48
  372. package/src/lib/scene-material-refs.test.ts +79 -0
  373. package/src/lib/scene-material-refs.ts +79 -0
  374. package/src/lib/scene.test.ts +24 -36
  375. package/src/lib/scene.ts +33 -57
  376. package/src/lib/selection-routing.test.ts +129 -45
  377. package/src/lib/selection-routing.ts +53 -74
  378. package/src/lib/session-writes.test.ts +120 -0
  379. package/src/lib/session-writes.ts +205 -0
  380. package/src/lib/sidebar-panel.ts +62 -0
  381. package/src/lib/site-boundary.test.ts +4 -6
  382. package/src/lib/site-boundary.ts +1 -1
  383. package/src/lib/snapping-mode.test.ts +4 -0
  384. package/src/lib/snapping-mode.ts +8 -1
  385. package/src/lib/terrace-draft.test.ts +145 -0
  386. package/src/lib/terrace-draft.ts +213 -0
  387. package/src/lib/tool-cursor-icon.test.ts +16 -0
  388. package/src/lib/tool-cursor-icon.ts +11 -0
  389. package/src/lib/typing-target.ts +9 -0
  390. package/src/lib/units.ts +77 -39
  391. package/src/lib/vector-edges.test.ts +58 -0
  392. package/src/lib/vector-edges.ts +400 -0
  393. package/src/lib/viewer-selection.test.ts +577 -0
  394. package/src/lib/viewer-selection.ts +569 -0
  395. package/src/lib/wall-draw-variant.test.ts +71 -0
  396. package/src/lib/wall-draw-variant.ts +91 -0
  397. package/src/lib/wall-paint-region-session.ts +246 -0
  398. package/src/lib/wall-paint-region.test.ts +373 -0
  399. package/src/lib/wall-polygon-draft.test.ts +262 -0
  400. package/src/lib/wall-polygon-draft.ts +286 -0
  401. package/src/lib/wall-push.test.ts +453 -0
  402. package/src/lib/wall-region-face.ts +352 -0
  403. package/src/lib/wall-region-handles.test.ts +304 -0
  404. package/src/lib/wall-region-handles.ts +368 -0
  405. package/src/lib/wall-region-list.test.ts +119 -0
  406. package/src/lib/wall-region-list.ts +136 -0
  407. package/src/lib/wall-region-snap.ts +110 -0
  408. package/src/lib/window-interaction.ts +14 -0
  409. package/src/lib/zone-content.ts +4 -1
  410. package/src/lib/zone-name.test.ts +17 -0
  411. package/src/lib/zone-name.ts +17 -0
  412. package/src/store/opens-in-select-mode.test.ts +223 -0
  413. package/src/store/paint-mode.test.ts +188 -0
  414. package/src/store/terrain-sculpt-mode.test.ts +19 -21
  415. package/src/store/tool-mode.test.ts +137 -6
  416. package/src/store/use-delete-confirmation.ts +28 -1
  417. package/src/store/use-drawing-view.test.ts +24 -3
  418. package/src/store/use-drawing-view.ts +29 -1
  419. package/src/store/use-editor.tsx +432 -321
  420. package/src/store/use-floorplan-draft-preview.ts +13 -0
  421. package/src/store/use-floorplan-marquee.ts +12 -2
  422. package/src/store/use-quick-measurement-hud.ts +3 -3
  423. package/src/store/use-stair-build-preview.ts +13 -5
  424. package/src/store/use-wall-move-ghosts.ts +4 -2
  425. package/src/store/use-wall-region-handles-selection.test.ts +222 -0
  426. package/src/store/use-wall-region-handles-selection.ts +139 -0
  427. package/src/store/view-layout.test.ts +175 -0
  428. package/src/store/view-layout.ts +196 -0
  429. package/src/test-utils/immediate-animation-frames.ts +16 -0
  430. package/src/test-utils/live-gesture-round-trip.ts +224 -0
  431. package/src/test-utils/selection-harness.ts +82 -0
  432. package/src/components/editor-2d/renderers/floorplan-stair-layer.tsx +0 -499
  433. package/src/components/systems/zone/zone-label-editor-system.tsx +0 -333
  434. package/src/components/tools/fence/fence-drafting.test.ts +0 -100
  435. package/src/components/tools/fence/fence-drafting.ts +0 -266
  436. package/src/components/ui/sidebar/panels/plugins-panel.tsx +0 -218
  437. package/src/components/viewer/viewer-units-panel.tsx +0 -116
  438. package/src/lib/floorplan/stairs.ts +0 -489
  439. package/src/lib/level-duplication.test.ts +0 -144
  440. package/src/lib/level-duplication.ts +0 -195
@@ -9,13 +9,16 @@ import {
9
9
  type SlabNode,
10
10
  useScene,
11
11
  type WallNode,
12
+ type ZoneNode,
12
13
  } from '@pascal-app/core'
13
14
  import { useViewer } from '@pascal-app/viewer'
14
15
  import { type ComponentType, lazy, Suspense, useMemo } from 'react'
15
16
  import { useRegisteredToolEnabled } from '../../hooks/use-registered-tool-enabled'
17
+ import { useCeilingEditCeilingId } from '../../lib/ceiling-edit-session'
16
18
  import type { ReshapeKind } from '../../lib/interaction/scope'
19
+ import { paintRegionTargets, usePaintRegionMode } from '../../lib/paint-region-mode'
17
20
  import { siteBoundaryHandlesEnabled } from '../../lib/site-boundary'
18
- import useEditor, { type Phase, type Tool } from '../../store/use-editor'
21
+ import useEditor, { type Tool } from '../../store/use-editor'
19
22
  import useInteractionScope, {
20
23
  useControlPointReshape,
21
24
  useEditingHole,
@@ -29,10 +32,14 @@ import useInteractionScope, {
29
32
  } from '../../store/use-interaction-scope'
30
33
  import { Alignment3DGuideLayer } from '../editor/alignment-3d-guide-layer'
31
34
  import { Elevation3DGuideLayer } from '../editor/elevation-3d-guide-layer'
35
+ import { FloorOpeningsOverlay3D } from '../editor/floor-openings-overlay'
36
+ import { FloorRegionControls3D } from '../editor/floor-region-controls'
37
+ import { OpenWallEnds3DLayer } from '../editor/open-wall-ends-3d-layer'
32
38
  import { OpeningGuides3DLayer } from '../editor/opening-guides-3d-layer'
33
39
  import { WallSnapBeaconLayer } from '../editor/wall-snap-beacon-layer'
34
40
  import { ElevatorTool } from './elevator/elevator-tool'
35
41
  import { MoveTool } from './item/move-tool'
42
+ import { WallPaintRegionTool } from './paint-region/wall-paint-region-tool'
36
43
  import { RegistryToolProvider } from './registry-tool-context'
37
44
  import {
38
45
  getRegistryAffordanceTool,
@@ -100,21 +107,17 @@ function getRegistryTool(tool: Tool | null): ComponentType | null {
100
107
  // Legacy tool fallbacks — kinds whose placement tools haven't migrated
101
108
  // to `def.tool` yet. Wall / fence / slab / ceiling / door / window /
102
109
  // item / shelf / spawn now go through the registry path above.
103
- const tools: Record<Phase, Partial<Record<Tool, React.FC>>> = {
104
- site: {
105
- 'property-line': SiteBoundaryEditor,
106
- },
107
- structure: {
108
- stair: StairTool,
109
- zone: ZoneTool,
110
- },
111
- furnish: {},
110
+ const tools: Partial<Record<Tool, React.FC>> = {
111
+ 'property-line': SiteBoundaryEditor,
112
+ stair: StairTool,
113
+ zone: ZoneTool,
112
114
  }
113
115
 
114
116
  export const ToolManager: React.FC = () => {
115
117
  const phase = useEditor((state) => state.phase)
116
118
  const mode = useEditor((state) => state.mode)
117
119
  const tool = useEditor((state) => state.tool)
120
+ const paintsWallRegions = usePaintRegionMode((state) => paintRegionTargets(state.mode).wall)
118
121
  const registeredToolEnabled = useRegisteredToolEnabled(tool)
119
122
  const movingNode = useMovingNode()
120
123
  const registryToolOwnsPlacement = useInteractionScope(
@@ -157,7 +160,18 @@ export const ToolManager: React.FC = () => {
157
160
  }
158
161
  }, [reshapingNode, tangentReshape])
159
162
  const editingHole = useEditingHole()
160
- const selectedZoneId = useViewer((state) => state.selection.zoneId)
163
+ const editCeilingId = useCeilingEditCeilingId()
164
+ // A zone picked as a zone, from the Scene list (zoneId) or its area or pill (sole selection).
165
+ const zoneSelectionId = useViewer((state) => state.selection.zoneId)
166
+ const soleSelectedId = useViewer((state) =>
167
+ state.selection.selectedIds.length === 1 ? state.selection.selectedIds[0] : undefined,
168
+ )
169
+ const soleSelectedZone = useScene((state) =>
170
+ soleSelectedId && state.nodes[soleSelectedId as AnyNodeId]?.type === 'zone'
171
+ ? (soleSelectedId as ZoneNode['id'])
172
+ : null,
173
+ )
174
+ const selectedZoneId = zoneSelectionId ?? soleSelectedZone
161
175
  const selectedIds = useViewer((state) => state.selection.selectedIds)
162
176
  const buildingId = useViewer((state) => state.selection.buildingId)
163
177
  const activeLevelId = useViewer((state) => state.selection.levelId)
@@ -213,12 +227,14 @@ export const ToolManager: React.FC = () => {
213
227
  // edge handles — mounts only for a sole selection.
214
228
  const isSoleSelection = selectedIds.length === 1
215
229
 
216
- // Show slab boundary editor when in structure/select mode with a slab selected (but not editing a hole)
230
+ // Show slab boundary editor in select mode with a slab selected (but not editing a hole)
217
231
  const showSlabBoundaryEditor =
218
- phase === 'structure' &&
219
232
  mode === 'select' &&
220
233
  isSoleSelection &&
221
234
  selectedSlabId !== undefined &&
235
+ // Derived plates (a footprint's floor, a room's raised plate) follow their
236
+ // rooms; only user-drawn slabs get the outline editor.
237
+ !selectedSlab?.plateRole &&
222
238
  !isFloorplanDrivenReshape &&
223
239
  !editingSlabHoleIsManual
224
240
 
@@ -230,26 +246,28 @@ export const ToolManager: React.FC = () => {
230
246
  !isFloorplanDrivenReshape &&
231
247
  editingSlabHoleIsManual
232
248
 
233
- // Show ceiling boundary editor when in structure/select mode with a ceiling selected (but not editing a hole)
249
+ // Show ceiling boundary editor only inside the ceiling's Edit ceiling session (not while editing a hole)
234
250
  const showCeilingBoundaryEditor =
235
- phase === 'structure' &&
236
251
  mode === 'select' &&
237
252
  isSoleSelection &&
238
253
  selectedCeilingId !== undefined &&
254
+ selectedCeilingId === editCeilingId &&
239
255
  !isFloorplanDrivenReshape &&
240
256
  (!editingHole || editingHole.nodeId !== selectedCeilingId)
241
257
 
242
- // Show ceiling hole editor when editing a hole on the selected ceiling
258
+ // Show ceiling hole editor when editing a manual hole inside the ceiling's Edit ceiling session
243
259
  const showCeilingHoleEditor =
244
260
  selectedCeilingId !== undefined &&
261
+ selectedCeilingId === editCeilingId &&
245
262
  editingHole !== null &&
246
263
  editingHole.nodeId === selectedCeilingId &&
264
+ ((nodes[selectedCeilingId as AnyNodeId] as CeilingNode).holeMetadata?.[editingHole.holeIndex]
265
+ ?.source ?? 'manual') === 'manual' &&
247
266
  !isFloorplanDrivenReshape
248
267
 
249
- // Show zone boundary editor when in structure/select mode with a zone selected
268
+ // Show zone boundary editor in select mode with a zone selected
250
269
  // Hide when editing a slab or ceiling to avoid overlapping handles
251
270
  const showZoneBoundaryEditor =
252
- phase === 'structure' &&
253
271
  mode === 'select' &&
254
272
  selectedZoneId !== null &&
255
273
  !isFloorplanDrivenReshape &&
@@ -274,7 +292,7 @@ export const ToolManager: React.FC = () => {
274
292
  const RegistryToolComponent = showBuildTool ? getRegistryTool(tool) : null
275
293
  const useRegistryTool = RegistryToolComponent != null
276
294
 
277
- const BuildToolComponent = showBuildTool && !useRegistryTool ? tools[phase]?.[tool] : null
295
+ const BuildToolComponent = showBuildTool && !useRegistryTool ? tools[tool] : null
278
296
  const handlePlacedNodeSelected = (nodeId: AnyNodeId) => {
279
297
  setSelection({ selectedIds: [nodeId] })
280
298
  }
@@ -298,10 +316,16 @@ export const ToolManager: React.FC = () => {
298
316
  <RegistryToolProvider value={registryToolContext}>
299
317
  {/* World-space tools: site boundary and building movement operate in world coordinates */}
300
318
  {showSiteBoundaryEditor && <SiteBoundaryEditor />}
301
- {/* Terrain sculpting is a mode rather than a `tools[phase][tool]` entry —
319
+ {/* Terrain sculpting is a mode rather than a `tools[tool]` entry —
302
320
  it places no node — so it gets its own gate here. World-space, because
303
321
  the ground is not building-local. */}
304
322
  {sculpting && <TerrainSculptTool />}
323
+ {/* "Paint part of the floor": level-space, follows the level matrix itself. */}
324
+ <FloorRegionControls3D />
325
+ {/* Floor and ceiling openings: level-space, follows the level matrix itself. */}
326
+ <FloorOpeningsOverlay3D />
327
+ {/* "Paint part of the wall": follows the wall mesh's own matrix. */}
328
+ {mode === 'material-paint' && paintsWallRegions && <WallPaintRegionTool />}
305
329
  {showMover && movingNode?.type === 'building' && (
306
330
  <MoveTool onNodeMoved={handlePlacedNodeSelected} onSpawnMoved={handlePlacedNodeSelected} />
307
331
  )}
@@ -454,6 +478,8 @@ export const ToolManager: React.FC = () => {
454
478
  <Elevation3DGuideLayer />
455
479
  {/* "Magnetic" beacon at the active wall-draft snap point. */}
456
480
  <WallSnapBeaconLayer />
481
+ {/* Wall ends that aren't joined, with a one-click join — twin of the floor plan's. */}
482
+ <OpenWallEnds3DLayer />
457
483
  </group>
458
484
  </RegistryToolProvider>
459
485
  )
@@ -22,6 +22,11 @@ import {
22
22
  type WallNode,
23
23
  WallNode as WallSchema,
24
24
  } from '@pascal-app/core'
25
+ import {
26
+ migrateCeilingRoomLinks,
27
+ migrateFloorPlates,
28
+ migrateRoomZones,
29
+ } from '@pascal-app/core/scene-migrations'
25
30
  import { useViewer } from '@pascal-app/viewer'
26
31
  import useEditor from '../../../store/use-editor'
27
32
  import useInteractionScope from '../../../store/use-interaction-scope'
@@ -721,7 +726,9 @@ describe('createWallOnCurrentLevel', () => {
721
726
  expect(useScene.temporal.getState().pastStates.length - before).toBe(1)
722
727
  })
723
728
 
724
- test('a room divider splits customized auto slabs and ceilings', () => {
729
+ test.each([
730
+ 0.05, 0.18,
731
+ ])('a room divider preserves the footprint floor at %s and splits ceilings', (elevation) => {
725
732
  const walls = [
726
733
  makeWall([0, 0], [8, 0], 'wall_bottom'),
727
734
  makeWall([8, 0], [8, 6], 'wall_right'),
@@ -737,7 +744,8 @@ describe('createWallOnCurrentLevel', () => {
737
744
  [8, 6],
738
745
  [0, 6],
739
746
  ],
740
- elevation: 0.18,
747
+ elevation,
748
+ floorHeight: elevation,
741
749
  thickness: 0.32,
742
750
  autoFromWalls: true,
743
751
  })
@@ -748,6 +756,11 @@ describe('createWallOnCurrentLevel', () => {
748
756
  autoFromWalls: true,
749
757
  })
750
758
  seedLevel(walls, [slab, ceiling])
759
+ useScene.setState({
760
+ nodes: migrateFloorPlates(
761
+ migrateCeilingRoomLinks(migrateRoomZones(useScene.getState().nodes).nodes).nodes,
762
+ ).nodes as Record<AnyNodeId, AnyNode>,
763
+ })
751
764
  const stopDetection = initSpaceDetectionSync(useScene, useEditor)
752
765
 
753
766
  try {
@@ -756,11 +769,13 @@ describe('createWallOnCurrentLevel', () => {
756
769
  const nodes = Object.values(useScene.getState().nodes)
757
770
  const slabs = nodes.filter((node) => node.type === 'slab')
758
771
  const ceilings = nodes.filter((node) => node.type === 'ceiling')
759
- expect(slabs).toHaveLength(2)
772
+ expect(slabs).toHaveLength(1)
773
+ expect(slabs[0]).toMatchObject({ plateRole: 'base', floorHeight: elevation })
774
+ expect(slabs[0]!.zoneIds).toHaveLength(2)
760
775
  expect(ceilings).toHaveLength(2)
761
776
  expect(
762
777
  slabs.every(
763
- (node) => node.autoFromWalls && node.elevation === 0.18 && node.thickness === 0.32,
778
+ (node) => node.autoFromWalls && node.elevation === elevation && node.thickness === 0.32,
764
779
  ),
765
780
  ).toBe(true)
766
781
  expect(ceilings.every((node) => node.autoFromWalls)).toBe(true)
@@ -3,10 +3,10 @@ import {
3
3
  emitter,
4
4
  type GridEvent,
5
5
  type LevelNode,
6
+ newZone,
6
7
  runAsSingleSceneHistoryStep,
7
8
  snapPointAlongAngleRay,
8
9
  useScene,
9
- ZoneNode,
10
10
  } from '@pascal-app/core'
11
11
  import { useViewer } from '@pascal-app/viewer'
12
12
  import { useEffect, useMemo, useRef, useState } from 'react'
@@ -19,6 +19,7 @@ import {
19
19
  } from './../../../lib/surface-plan-snap'
20
20
  import { focusedUnitNode } from './../../../lib/units'
21
21
  import { snapWorldXZForActiveBuilding } from './../../../lib/world-grid-snap'
22
+ import { nextZoneName } from '../../../lib/zone-name'
22
23
  import useEditor, { isAngleSnapActive, isGridSnapActive } from './../../../store/use-editor'
23
24
  import { useFloorplanDraftPreview } from './../../../store/use-floorplan-draft-preview'
24
25
  import { CursorSphere } from '../shared/cursor-sphere'
@@ -31,18 +32,9 @@ const Y_OFFSET = 0.02
31
32
  const commitZoneDrawing = (levelId: LevelNode['id'], points: Array<[number, number]>) => {
32
33
  const { createNode, updateNode, nodes } = useScene.getState()
33
34
 
34
- // Count existing zones for naming and color cycling
35
- const zoneCount = Object.values(nodes).filter((n) => n.type === 'zone').length
36
- const name = `Zone ${zoneCount + 1}`
35
+ const name = nextZoneName(nodes)
37
36
 
38
- // Default to blue, cycle through palette for subsequent zones
39
- const color = '#3b82f6'
40
-
41
- const zone = ZoneNode.parse({
42
- name,
43
- polygon: points,
44
- color,
45
- })
37
+ const zone = newZone({ name, polygon: points })
46
38
 
47
39
  const focusedUnit = focusedUnitNode()
48
40
 
@@ -36,6 +36,9 @@ export const ActionButton = React.forwardRef<HTMLButtonElement, ActionButtonProp
36
36
  <Tooltip>
37
37
  <TooltipTrigger asChild>
38
38
  <Button
39
+ // These are toggles, not commands: the armed/unarmed state has to be
40
+ // readable without decoding the active-state styling.
41
+ aria-pressed={isActive}
39
42
  className={cn('relative h-11 w-11 transition-all', className)}
40
43
  onClick={(event) => {
41
44
  triggerSFX('sfx:menu-click')
@@ -3,12 +3,13 @@
3
3
  import { emitter } from '@pascal-app/core'
4
4
  import Image from 'next/image'
5
5
  import useEditor from '../../../store/use-editor'
6
+ import { visibleScene } from '../../../store/view-layout'
6
7
  import { ActionButton } from './action-button'
7
8
 
8
9
  export function CameraActions({ hideOrbit = false }: { hideOrbit?: boolean }) {
9
10
  // Orbit stays useful in 2D-only (it spins the synced floorplan view), but
10
11
  // top view only tilts the hidden 3D camera — pointless without the canvas.
11
- const is2dOnly = useEditor((s) => s.viewMode === '2d')
12
+ const is2dOnly = useEditor((s) => visibleScene(s) === '2d')
12
13
 
13
14
  const goToTopView = () => {
14
15
  emitter.emit('camera-controls:top-view')
@@ -9,7 +9,7 @@ import useEditor from './../../../store/use-editor'
9
9
  import { ActionButton } from './action-button'
10
10
  import { MeasurementControl } from './measurement-control'
11
11
 
12
- type ControlId = 'select' | 'box-select' | 'zone' | 'delete'
12
+ type ControlId = 'select' | 'box-select' | 'delete'
13
13
 
14
14
  type ControlConfig = {
15
15
  id: ControlId
@@ -32,14 +32,6 @@ const controls: ControlConfig[] = [
32
32
  color: 'hover:bg-blue-500/20 hover:text-blue-400',
33
33
  activeColor: 'bg-blue-500/20 text-blue-400',
34
34
  },
35
- {
36
- id: 'zone',
37
- imageSrc: '/icons/zone.webp',
38
- label: 'Zone',
39
- shortcut: 'Z',
40
- color: 'hover:bg-green-500/20 hover:text-green-400',
41
- activeColor: 'bg-green-500/20 text-green-400',
42
- },
43
35
  {
44
36
  id: 'delete',
45
37
  icon: Trash2,
@@ -52,49 +44,23 @@ const controls: ControlConfig[] = [
52
44
 
53
45
  export function ControlModes() {
54
46
  const mode = useEditor((state) => state.mode)
55
- const phase = useEditor((state) => state.phase)
56
47
  const selectionTool = useEditor((state) => state.floorplanSelectionTool)
57
48
  const armToolMode = useEditor((state) => state.armToolMode)
58
- const setPhase = useEditor((state) => state.setPhase)
59
- const setStructureLayer = useEditor((state) => state.setStructureLayer)
60
49
  const setSelectionTool = useEditor((state) => state.setFloorplanSelectionTool)
61
50
 
62
- const isSiteEditing = phase === 'site'
63
-
64
- const structureLayer = useEditor((state) => state.structureLayer)
65
-
66
51
  const getIsActive = (id: ControlId): boolean => {
67
52
  if (id === 'select') return mode === 'select' && selectionTool === 'click'
68
53
  if (id === 'box-select') return mode === 'select' && selectionTool === 'marquee'
69
- if (id === 'zone')
70
- return mode === 'build' && phase === 'structure' && structureLayer === 'zones'
71
54
  return mode === id
72
55
  }
73
56
 
74
57
  const handleClick = (id: ControlId) => {
75
- // Exit site editing first if needed. Sculpting is a site-phase mode, so
76
- // leaving the phase is exactly the right way to leave the brush — but the
77
- // order matters: `setPhase` resets the mode, and setting the mode first
78
- // would have it overwritten below.
79
- if (isSiteEditing) {
80
- setPhase('structure')
81
- setStructureLayer('elements')
82
- }
83
-
84
58
  if (id === 'select') {
85
59
  armToolMode({ mode: 'select' })
86
60
  setSelectionTool('click')
87
61
  } else if (id === 'box-select') {
88
62
  armToolMode({ mode: 'select' })
89
63
  setSelectionTool('marquee')
90
- } else if (id === 'zone') {
91
- if (getIsActive('zone')) {
92
- armToolMode({ mode: 'select' })
93
- } else {
94
- setPhase('structure')
95
- setStructureLayer('zones')
96
- armToolMode({ mode: 'build', tool: 'zone' })
97
- }
98
64
  } else {
99
65
  armToolMode({ mode: id })
100
66
  }
@@ -121,6 +87,7 @@ export function ControlModes() {
121
87
  // A static hook for a host app that wants to point a first-run
122
88
  // tour at this button. Nothing here reads it.
123
89
  data-guide-target={c.id === 'select' ? 'mode-select' : undefined}
90
+ isActive={isActive}
124
91
  label={c.label}
125
92
  onClick={() => handleClick(c.id)}
126
93
  shortcut={c.shortcut}
@@ -22,6 +22,7 @@ import { useState } from 'react'
22
22
  import type { CreatableMeasurementKind } from '../../../lib/measurement-kind'
23
23
  import { cn } from '../../../lib/utils'
24
24
  import useEditor from '../../../store/use-editor'
25
+ import { VIEW_2D } from '../../../store/view-layout'
25
26
  import useFloorplanMode from '../../../store/use-floorplan-mode'
26
27
  import { Popover, PopoverContent, PopoverTrigger } from '../primitives/popover'
27
28
  import { ActionButton } from './action-button'
@@ -74,12 +75,10 @@ export function MeasurementControl() {
74
75
  (state) => state.toolDefaults['construction-dimension']?.mode,
75
76
  )
76
77
  const setMode = useEditor((state) => state.setMode)
77
- const setPhase = useEditor((state) => state.setPhase)
78
78
  const setLastMeasurementKind = useEditor((state) => state.setLastMeasurementKind)
79
- const setStructureLayer = useEditor((state) => state.setStructureLayer)
80
79
  const setTool = useEditor((state) => state.setTool)
81
80
  const setToolDefaults = useEditor((state) => state.setToolDefaults)
82
- const setViewMode = useEditor((state) => state.setViewMode)
81
+ const showView = useEditor((state) => state.showView)
83
82
 
84
83
  const selectedOption =
85
84
  measurementOptions.find((option) => option.kind === selectedKind) ?? measurementOptions[0]
@@ -104,8 +103,6 @@ export function MeasurementControl() {
104
103
  : selectedOption.label
105
104
 
106
105
  const activateMeasurement = (kind: CreatableMeasurementKind) => {
107
- setPhase('structure')
108
- setStructureLayer('elements')
109
106
  setLastMeasurementKind(kind)
110
107
  setToolDefaults('measurement', { kind })
111
108
  setMode('build')
@@ -121,8 +118,6 @@ export function MeasurementControl() {
121
118
  }
122
119
 
123
120
  const activateSmartMeasurement = () => {
124
- setPhase('structure')
125
- setStructureLayer('elements')
126
121
  setToolDefaults('measurement', { kind: 'smart' })
127
122
  setMode('build')
128
123
  setTool('measurement')
@@ -136,9 +131,7 @@ export function MeasurementControl() {
136
131
  useFloorplanMode.getState().showExpertModeNotice('Construction Dimension')
137
132
  return
138
133
  }
139
- setPhase('structure')
140
- setStructureLayer('elements')
141
- setViewMode('2d')
134
+ showView(VIEW_2D)
142
135
  setToolDefaults('construction-dimension', { chainMode, mode: dimensionMode })
143
136
  setMode('build')
144
137
  setTool('construction-dimension')
@@ -0,0 +1,235 @@
1
+ 'use client'
2
+
3
+ import { ChevronRight } from 'lucide-react'
4
+ import { type ComponentPropsWithoutRef, type ReactNode, useEffect, useState } from 'react'
5
+ import { cn } from '../../../lib/utils'
6
+ import {
7
+ WALL_DRAW_IDLE_CAPTION,
8
+ WALL_DRAW_VARIANTS,
9
+ type WallDrawVariant,
10
+ } from '../../../lib/wall-draw-variant'
11
+
12
+ /**
13
+ * The Build panel's grouped layout (Rooms / Add to rooms / Outdoor / Advanced),
14
+ * shared by the community and standalone editor Build tabs so both read the
15
+ * same. Hosts own what each tile arms; these only lay the groups out.
16
+ */
17
+ export function BuildPanelSection({
18
+ children,
19
+ hint,
20
+ id,
21
+ title,
22
+ }: {
23
+ children: ReactNode
24
+ hint?: string
25
+ id: string
26
+ title: string
27
+ }) {
28
+ return (
29
+ <section className="flex flex-col" data-build-group={id}>
30
+ <div className="mb-2 flex items-baseline justify-between gap-2 px-0.5">
31
+ <h3 className="font-medium text-foreground text-xs">{title}</h3>
32
+ {hint ? <span className="truncate text-[11px] text-muted-foreground">{hint}</span> : null}
33
+ </div>
34
+ {children}
35
+ </section>
36
+ )
37
+ }
38
+
39
+ const ADVANCED_OPEN_KEY = 'pascal-build-advanced-open'
40
+
41
+ function readAdvancedOpen(): boolean {
42
+ try {
43
+ return window.localStorage.getItem(ADVANCED_OPEN_KEY) === '1'
44
+ } catch {
45
+ return false
46
+ }
47
+ }
48
+
49
+ function writeAdvancedOpen(open: boolean) {
50
+ try {
51
+ window.localStorage.setItem(ADVANCED_OPEN_KEY, open ? '1' : '0')
52
+ } catch {
53
+ // Storage blocked (private window, preview): the section just starts closed.
54
+ }
55
+ }
56
+
57
+ /**
58
+ * The collapsible Advanced group. Remembers its open state per viewer, and
59
+ * opens itself when one of its tools becomes the armed one (e.g. by shortcut)
60
+ * so the lit tile is never hidden.
61
+ */
62
+ export function BuildPanelAdvancedSection({
63
+ children,
64
+ containsActiveTool,
65
+ description,
66
+ hint,
67
+ }: {
68
+ children: ReactNode
69
+ containsActiveTool: boolean
70
+ description?: string
71
+ hint?: string
72
+ }) {
73
+ const [open, setOpen] = useState(false)
74
+ useEffect(() => setOpen(readAdvancedOpen()), [])
75
+ useEffect(() => {
76
+ if (containsActiveTool) setOpen(true)
77
+ }, [containsActiveTool])
78
+
79
+ return (
80
+ <section className="flex flex-col" data-build-group="advanced">
81
+ <button
82
+ aria-expanded={open}
83
+ className="-mx-1.5 flex items-center gap-2 rounded-lg px-1.5 py-1.5 text-left transition-colors hover:bg-muted/60"
84
+ onClick={() => {
85
+ const next = !open
86
+ setOpen(next)
87
+ writeAdvancedOpen(next)
88
+ }}
89
+ type="button"
90
+ >
91
+ <ChevronRight
92
+ className={cn(
93
+ 'size-3 shrink-0 text-muted-foreground transition-transform duration-150',
94
+ open && 'rotate-90',
95
+ )}
96
+ />
97
+ <h3 className="font-medium text-foreground text-xs">Advanced</h3>
98
+ {hint ? (
99
+ <span className="ml-auto truncate text-[11px] text-muted-foreground">{hint}</span>
100
+ ) : null}
101
+ </button>
102
+ {open ? (
103
+ <div className="flex flex-col gap-2.5 pt-1.5">
104
+ {description ? (
105
+ <p className="px-0.5 text-[11px] text-muted-foreground leading-relaxed">
106
+ {description}
107
+ </p>
108
+ ) : null}
109
+ {children}
110
+ </div>
111
+ ) : null}
112
+ </section>
113
+ )
114
+ }
115
+
116
+ export function BuildToolGrid({ children, columns }: { children: ReactNode; columns: 3 | 4 }) {
117
+ return (
118
+ <div className={cn('grid gap-1.5', columns === 3 ? 'grid-cols-3' : 'grid-cols-4')}>
119
+ {children}
120
+ </div>
121
+ )
122
+ }
123
+
124
+ type BuildToolTileProps = Omit<ComponentPropsWithoutRef<'button'>, 'children' | 'type'> & {
125
+ active: boolean
126
+ /** Corner mark, e.g. the snap-target badge of wall-hosted tools. */
127
+ badge?: ReactNode
128
+ iconSrc: string
129
+ label: string
130
+ size?: 'lg' | 'sm'
131
+ }
132
+
133
+ /**
134
+ * One tool tile: the isometric icon over its name. Inactive icons stay grey and
135
+ * take their colour on hover; the armed tool keeps it, on the primary tint.
136
+ */
137
+ export function BuildToolTile({
138
+ active,
139
+ badge,
140
+ className,
141
+ iconSrc,
142
+ label,
143
+ size = 'sm',
144
+ ...buttonProps
145
+ }: BuildToolTileProps) {
146
+ return (
147
+ <button
148
+ aria-pressed={active}
149
+ className={cn(
150
+ 'group relative flex min-w-0 flex-col items-center justify-end rounded-xl transition-all duration-200',
151
+ // Small tiles grow to two label lines ("Spawn point"); a grid row's
152
+ // tiles stretch together, so a row keeps one height.
153
+ size === 'lg' ? 'h-[104px] gap-1.5 px-1.5 pt-2 pb-2.5' : 'min-h-[74px] gap-1 px-1 pt-1.5 pb-2',
154
+ active ? 'bg-primary/10 ring-1 ring-primary/50' : 'bg-muted/40 hover:bg-muted',
155
+ className,
156
+ )}
157
+ type="button"
158
+ {...buttonProps}
159
+ >
160
+ <img
161
+ alt=""
162
+ className={cn(
163
+ 'min-h-0 object-contain transition duration-200 group-hover:scale-110',
164
+ size === 'lg' ? 'size-14' : 'size-9',
165
+ !active && 'opacity-70 grayscale group-hover:opacity-100 group-hover:grayscale-0',
166
+ )}
167
+ draggable={false}
168
+ src={iconSrc}
169
+ />
170
+ <span
171
+ className={cn(
172
+ 'line-clamp-2 max-w-full text-center font-medium leading-tight',
173
+ size === 'lg' ? 'text-xs' : 'text-[11px]',
174
+ active ? 'text-foreground' : 'text-muted-foreground group-hover:text-foreground',
175
+ )}
176
+ >
177
+ {label}
178
+ </span>
179
+ {badge ? <span className="absolute top-1 right-1">{badge}</span> : null}
180
+ </button>
181
+ )
182
+ }
183
+
184
+ /** The line under the Rooms tiles: what the armed shape does, lead in bold. */
185
+ export function BuildPanelCaption({ lead, rest }: { lead?: string; rest: string }) {
186
+ return (
187
+ <p className="mt-2 min-h-[2lh] px-0.5 text-[11px] text-muted-foreground leading-relaxed">
188
+ {lead ? <span className="font-medium text-foreground">{lead} </span> : null}
189
+ {rest}
190
+ </p>
191
+ )
192
+ }
193
+
194
+ /**
195
+ * The Rooms group: one large tile per wall drawing variant, and the armed
196
+ * variant's caption. `activeVariant` is null while the wall tool is not armed.
197
+ */
198
+ export function BuildPanelRoomsSection({
199
+ activeVariant,
200
+ onHover,
201
+ onSelect,
202
+ tileProps,
203
+ }: {
204
+ activeVariant: WallDrawVariant | null
205
+ onHover?: (variant: WallDrawVariant) => void
206
+ onSelect: (variant: WallDrawVariant) => void
207
+ /** Extra attributes per tile (e.g. a host's guide-target hook). */
208
+ tileProps?: (variant: WallDrawVariant) => Record<string, string | undefined>
209
+ }) {
210
+ const active = WALL_DRAW_VARIANTS.find((variant) => variant.id === activeVariant)
211
+ return (
212
+ <BuildPanelSection hint="walls · floor · ceiling" id="rooms" title="Rooms">
213
+ <BuildToolGrid columns={3}>
214
+ {WALL_DRAW_VARIANTS.map((variant) => (
215
+ <BuildToolTile
216
+ active={variant.id === activeVariant}
217
+ data-build-tool={`room-${variant.id}`}
218
+ iconSrc={variant.iconSrc}
219
+ key={variant.id}
220
+ label={variant.label}
221
+ onClick={() => onSelect(variant.id)}
222
+ onMouseEnter={() => onHover?.(variant.id)}
223
+ size="lg"
224
+ title={variant.title}
225
+ {...tileProps?.(variant.id)}
226
+ />
227
+ ))}
228
+ </BuildToolGrid>
229
+ <BuildPanelCaption
230
+ lead={active?.caption.lead}
231
+ rest={active ? active.caption.rest : WALL_DRAW_IDLE_CAPTION}
232
+ />
233
+ </BuildPanelSection>
234
+ )
235
+ }