@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
@@ -31,7 +31,6 @@ import {
31
31
  Redo2,
32
32
  Sparkles,
33
33
  Square,
34
- SquareStack,
35
34
  Sun,
36
35
  Trash2,
37
36
  Undo2,
@@ -40,6 +39,7 @@ import {
40
39
  import { useEffect } from 'react'
41
40
  import { getHistoryCommandState, runRedo, runUndo } from '../../../lib/history'
42
41
  import { deleteLevelWithFallbackSelection } from '../../../lib/level-selection'
42
+ import { getSavedSceneDocument } from '../../../lib/scene'
43
43
  import { createUnitInBuilding, enterUnitFocus, leaveUnitFocus } from '../../../lib/units'
44
44
  import { useCommandRegistry } from '../../../store/use-command-registry'
45
45
  import type { StructureTool } from '../../../store/use-editor'
@@ -50,10 +50,8 @@ export function EditorCommands() {
50
50
  const register = useCommandRegistry((s) => s.register)
51
51
  const { navigateTo, setInputValue, setOpen } = useCommandPalette()
52
52
 
53
- const setPhase = useEditor((s) => s.setPhase)
54
53
  const armToolMode = useEditor((s) => s.armToolMode)
55
54
  const armMaterialPaint = useEditor((s) => s.armMaterialPaint)
56
- const setStructureLayer = useEditor((s) => s.setStructureLayer)
57
55
  const isPreviewMode = useEditor((s) => s.isPreviewMode)
58
56
  const setPreviewMode = useEditor((s) => s.setPreviewMode)
59
57
 
@@ -79,12 +77,7 @@ export function EditorCommands() {
79
77
  )
80
78
 
81
79
  const activateTool = (tool: StructureTool) => {
82
- run(() => {
83
- setPhase('structure')
84
- if (tool === 'zone') setStructureLayer('zones')
85
- else setStructureLayer('elements')
86
- armToolMode({ mode: 'build', tool })
87
- })
80
+ run(() => armToolMode({ mode: 'build', tool }))
88
81
  }
89
82
 
90
83
  return register([
@@ -175,11 +168,7 @@ export function EditorCommands() {
175
168
  keywords: ['paint', 'material', 'texture', 'bucket', 'surface'],
176
169
  shortcut: ['P'],
177
170
  execute: () =>
178
- run(() => {
179
- setPhase('structure')
180
- setStructureLayer('elements')
181
- armMaterialPaint()
182
- }),
171
+ run(() => armMaterialPaint()),
183
172
  },
184
173
  {
185
174
  id: 'editor.mode.terrain-sculpt',
@@ -315,19 +304,6 @@ export function EditorCommands() {
315
304
  navigate: true,
316
305
  execute: () => navigateTo('wall-mode'),
317
306
  },
318
- {
319
- id: 'editor.viewer.level-mode',
320
- label: 'Level Mode',
321
- group: 'Viewer Controls',
322
- icon: <SquareStack className="h-4 w-4" />,
323
- keywords: ['level', 'floor', 'exploded', 'stacked', 'solo'],
324
- badge: () => {
325
- const mode = useViewer.getState().levelMode
326
- return { manual: 'Manual', stacked: 'Stacked', exploded: 'Exploded', solo: 'Solo' }[mode]
327
- },
328
- navigate: true,
329
- execute: () => navigateTo('level-mode'),
330
- },
331
307
  {
332
308
  id: 'editor.viewer.camera-mode',
333
309
  label: () => {
@@ -422,7 +398,7 @@ export function EditorCommands() {
422
398
  keywords: ['export', 'download', 'json', 'save', 'data'],
423
399
  execute: () =>
424
400
  run(() => {
425
- const { nodes, rootNodeIds } = useScene.getState()
401
+ const { nodes, rootNodeIds } = getSavedSceneDocument()
426
402
  const blob = new Blob([JSON.stringify({ nodes, rootNodeIds }, null, 2)], {
427
403
  type: 'application/json',
428
404
  })
@@ -476,10 +452,8 @@ export function EditorCommands() {
476
452
  navigateTo,
477
453
  setInputValue,
478
454
  setOpen,
479
- setPhase,
480
455
  armToolMode,
481
456
  armMaterialPaint,
482
- setStructureLayer,
483
457
  isPreviewMode,
484
458
  setPreviewMode,
485
459
  exportScene,
@@ -149,7 +149,6 @@ function OptionItem({
149
149
  // ---------------------------------------------------------------------------
150
150
  const PAGE_LABEL: Record<string, string> = {
151
151
  'wall-mode': 'Wall Mode',
152
- 'level-mode': 'Level Mode',
153
152
  'rename-level': 'Rename Level',
154
153
  'goto-level': 'Go to Level',
155
154
  }
@@ -204,8 +203,6 @@ export function CommandPalette({ emptyAction }: { emptyAction?: CommandPaletteEm
204
203
 
205
204
  const wallMode = useViewer((s) => s.wallMode)
206
205
  const setWallMode = useViewer((s) => s.setWallMode)
207
- const levelMode = useViewer((s) => s.levelMode)
208
- const setLevelMode = useViewer((s) => s.setLevelMode)
209
206
 
210
207
  const allLevels = useScene(
211
208
  useShallow((s) =>
@@ -250,12 +247,6 @@ export function CommandPalette({ emptyAction }: { emptyAction?: CommandPaletteEm
250
247
  down: 'Down',
251
248
  translucent: 'Translucent',
252
249
  }
253
- const levelModeLabel: Record<'manual' | 'stacked' | 'exploded' | 'solo', string> = {
254
- manual: 'Manual',
255
- stacked: 'Stacked',
256
- exploded: 'Exploded',
257
- solo: 'Solo',
258
- }
259
250
 
260
251
  const confirmRename = () => {
261
252
  if (!(activeLevelId && inputValue.trim())) return
@@ -364,7 +355,11 @@ export function CommandPalette({ emptyAction }: { emptyAction?: CommandPaletteEm
364
355
  keywords={action.keywords}
365
356
  label={action.label}
366
357
  navigate={action.navigate}
367
- onSelect={() => action.execute()}
358
+ // A registered command that does not navigate within the
359
+ // palette closes it, like the built-ins — otherwise the
360
+ // dialog overlay keeps swallowing clicks under a full-screen
361
+ // surface the command opened (Sheets).
362
+ onSelect={() => (action.navigate ? action.execute() : run(() => action.execute()))}
368
363
  shortcut={action.shortcut}
369
364
  />
370
365
  ))}
@@ -385,20 +380,6 @@ export function CommandPalette({ emptyAction }: { emptyAction?: CommandPaletteEm
385
380
  </Command.Group>
386
381
  )}
387
382
 
388
- {/* ── Level Mode sub-page ───────────────────────────────────── */}
389
- {page === 'level-mode' && (
390
- <Command.Group heading="Level Mode">
391
- {(['stacked', 'exploded', 'solo'] as const).map((mode) => (
392
- <OptionItem
393
- isActive={levelMode === mode}
394
- key={mode}
395
- label={levelModeLabel[mode]}
396
- onSelect={() => run(() => setLevelMode(mode))}
397
- />
398
- ))}
399
- </Command.Group>
400
- )}
401
-
402
383
  {/* ── Go to Level sub-page ──────────────────────────────────── */}
403
384
  {page === 'goto-level' && (
404
385
  <Command.Group heading="Go to Level">
@@ -0,0 +1,110 @@
1
+ 'use client'
2
+
3
+ import {
4
+ type AnyNode,
5
+ getCatalogMaterialById,
6
+ getLibraryMaterialIdFromRef,
7
+ getSceneMaterialIdFromRef,
8
+ nodeRegistry,
9
+ useScene,
10
+ } from '@pascal-app/core'
11
+ import { useState } from 'react'
12
+ import { Popover, PopoverContent, PopoverTrigger } from '../primitives/popover'
13
+ import { MaterialPicker } from './material-picker'
14
+
15
+ type Swatch = { label: string; color: string | null; image: string | null }
16
+
17
+ /** How a finish reference reads in a panel: its catalog tile, its colour, or its scene material. */
18
+ function useFinishSwatch(ref: string | undefined, fallback: string): Swatch {
19
+ const materials = useScene((state) => state.materials)
20
+ if (!ref) return { ...describe(fallback, materials), label: 'Default' }
21
+ return describe(ref, materials)
22
+ }
23
+
24
+ function describe(ref: string, materials: ReturnType<typeof useScene.getState>['materials']) {
25
+ const library = getCatalogMaterialById(getLibraryMaterialIdFromRef(ref) ?? undefined)
26
+ if (library)
27
+ return {
28
+ label: library.label,
29
+ color: library.previewColor ?? null,
30
+ image: library.previewThumbnailUrl ?? null,
31
+ }
32
+ const sceneId = getSceneMaterialIdFromRef(ref)
33
+ const scene = sceneId ? materials[sceneId as keyof typeof materials] : undefined
34
+ if (scene) return { label: scene.name, color: scene.material.properties?.color ?? null, image: null }
35
+ if (ref.startsWith('#')) return { label: ref.toUpperCase(), color: ref, image: null }
36
+ return { label: 'Default', color: null, image: null }
37
+ }
38
+
39
+ /**
40
+ * Paints one surface of a node from its panel, exactly as the paint tool does:
41
+ * the kind's own paint commit for that role (same write, one undo step).
42
+ */
43
+ export function paintSurface(node: AnyNode, role: string, materialPreset: string) {
44
+ const paint = nodeRegistry.get(node.type)?.capabilities?.paint
45
+ if (!paint) return
46
+ const args = { node, role, material: undefined, materialPreset }
47
+ if (paint.commit) paint.commit(args)
48
+ else useScene.getState().updateNode(node.id, paint.buildPatch(args) as Partial<AnyNode>)
49
+ }
50
+
51
+ /**
52
+ * A finish row: the swatch and its name. A click opens the paint tool's
53
+ * material picker; a pick paints the surface through the paint tool's path.
54
+ */
55
+ export function FinishSwatch({
56
+ label,
57
+ node,
58
+ role,
59
+ value,
60
+ fallback,
61
+ hint,
62
+ }: {
63
+ label: string
64
+ node: AnyNode
65
+ /** The paint role of the surface (`edge`, `foundation`, …). */
66
+ role: string
67
+ /** The authored finish reference; absent shows `fallback` as "Default". */
68
+ value: string | undefined
69
+ fallback: string
70
+ /** Tooltip on the row. */
71
+ hint?: string
72
+ }) {
73
+ const [open, setOpen] = useState(false)
74
+ const swatch = useFinishSwatch(value, fallback)
75
+ return (
76
+ <div className="flex min-h-8 items-center justify-between gap-3" title={hint}>
77
+ <span className="text-muted-foreground text-xs">{label}</span>
78
+ <Popover onOpenChange={setOpen} open={open}>
79
+ <PopoverTrigger asChild>
80
+ <button
81
+ aria-label={`${label}: ${swatch.label}`}
82
+ className="flex min-w-0 items-center gap-2 rounded-full border border-border/50 py-0.5 pr-2.5 pl-1 text-xs transition-colors hover:bg-accent"
83
+ data-finish-swatch={label}
84
+ type="button"
85
+ >
86
+ <span
87
+ aria-hidden
88
+ className="size-4 shrink-0 overflow-hidden rounded-full border border-border/50"
89
+ style={{ backgroundColor: swatch.color ?? 'transparent' }}
90
+ >
91
+ {swatch.image && <img alt="" className="size-full object-cover" src={swatch.image} />}
92
+ </span>
93
+ <span className="truncate">{swatch.label}</span>
94
+ </button>
95
+ </PopoverTrigger>
96
+ <PopoverContent align="end" className="flex h-96 w-80 flex-col p-2" side="left">
97
+ <MaterialPicker
98
+ onSelectMaterialPreset={(ref) => {
99
+ const live = useScene.getState().nodes[node.id as keyof ReturnType<typeof useScene.getState>['nodes']]
100
+ if (live) paintSurface(live, role, ref)
101
+ setOpen(false)
102
+ }}
103
+ selectOnCategoryChange={false}
104
+ selectedMaterialPreset={value}
105
+ />
106
+ </PopoverContent>
107
+ </Popover>
108
+ </div>
109
+ )
110
+ }
@@ -1,19 +1,22 @@
1
1
  'use client'
2
2
 
3
3
  import { type SceneMaterialId } from '@pascal-app/core'
4
- import { Eraser, Plus, RotateCcw } from 'lucide-react'
4
+ import { Eraser, PaintBucket, Pentagon, Pipette, Plus, Square } from 'lucide-react'
5
5
  import { useState } from 'react'
6
6
  import { useMaterialPaintPanelModel } from '../../../lib/material-paint-panel-model'
7
+ import { type PaintMode, usePaintRegionMode } from '../../../lib/paint-region-mode'
8
+ import useEditor, { armMaterialPaint } from '../../../store/use-editor'
7
9
  import { Button } from '../primitives/button'
8
10
  import { Tooltip, TooltipContent, TooltipTrigger } from '../primitives/tooltip'
9
11
  import { MaterialPicker } from './material-picker'
10
12
  import { SceneMaterialList } from './scene-material-list'
13
+ import { SegmentedControl } from './segmented-control'
11
14
 
12
15
  /**
13
16
  * Material picker for paint mode. Embedders render this wherever paint controls
14
17
  * belong (the community editor places it in the Build sidebar while paint mode
15
18
  * is active). It fills its container's height and lays out as three bands: a
16
- * fixed control/category header, a single scrolling catalog grid, and a fixed
19
+ * fixed mode header, a single scrolling catalog grid, and a fixed
17
20
  * scene-material footer (always visible, with a `+` to add a custom material).
18
21
  */
19
22
  export type MaterialPaintPanelProps = {
@@ -21,37 +24,103 @@ export type MaterialPaintPanelProps = {
21
24
  onCreateMaterialRequest?: () => void
22
25
  }
23
26
 
27
+ const PAINT_MODE_OPTIONS: { value: PaintMode; label: string; icon: typeof PaintBucket }[] = [
28
+ { value: 'surface', label: 'Whole surface: a click paints the surface under it', icon: PaintBucket },
29
+ {
30
+ value: 'rectangle',
31
+ label: 'Rectangle: press anywhere on a wall, floor or ceiling, drag to the opposite corner',
32
+ icon: Square,
33
+ },
34
+ {
35
+ value: 'polygon',
36
+ label: 'Polygon: click the points of a shape on a floor or ceiling',
37
+ icon: Pentagon,
38
+ },
39
+ {
40
+ value: 'erase',
41
+ label: 'Erase: click a painted part to remove it, or a painted surface to reset it',
42
+ icon: Eraser,
43
+ },
44
+ {
45
+ value: 'pick',
46
+ label: 'Pick: click a surface to paint with its material (or hold Alt / Option)',
47
+ icon: Pipette,
48
+ },
49
+ ]
50
+
51
+ /**
52
+ * Paint the whole surface, draw part of a wall, floor or ceiling first, erase, or
53
+ * pick a material off a surface. Erase
54
+ * is the eraser the paint path already knows (a click writes "no finish"), so a
55
+ * painted region goes and a whole-surface paint returns to its default. The row
56
+ * shows a choice only while paint mode is on; any pick (erase included) turns it
57
+ * on, so nothing here needs a colour first.
58
+ */
59
+ function PaintModeControl() {
60
+ const painting = useEditor((s) => s.mode === 'material-paint')
61
+ const mode = usePaintRegionMode((s) => s.mode)
62
+ const select = (next: PaintMode) => {
63
+ if (painting) usePaintRegionMode.getState().setMode(next)
64
+ else armMaterialPaint(undefined, next)
65
+ }
66
+ return (
67
+ <div
68
+ className="shrink-0 space-y-1.5 pb-2"
69
+ data-paint-mode={painting ? mode : 'off'}
70
+ // A clicked option must not keep focus: the next shortcut (V) would light
71
+ // its focus ring and it would read as still chosen after paint mode ends.
72
+ onPointerUp={(event) => (event.target as HTMLElement).closest('button')?.blur()}
73
+ >
74
+ <div className="px-0.5 font-medium text-muted-foreground text-xs">
75
+ {painting && mode === 'erase' ? 'Erase' : painting && mode === 'pick' ? 'Pick material' : 'Paint'}
76
+ </div>
77
+ <SegmentedControl
78
+ mixed={!painting}
79
+ onChange={select}
80
+ options={PAINT_MODE_OPTIONS.map(({ value, label, icon: Icon }) => ({
81
+ value,
82
+ label: (
83
+ <Tooltip>
84
+ <TooltipTrigger asChild>
85
+ <span aria-label={label} className="flex items-center justify-center">
86
+ <Icon className="h-3.5 w-3.5" />
87
+ </span>
88
+ </TooltipTrigger>
89
+ <TooltipContent>{label}</TooltipContent>
90
+ </Tooltip>
91
+ ),
92
+ }))}
93
+ value={mode}
94
+ />
95
+ </div>
96
+ )
97
+ }
98
+
24
99
  export function MaterialPaintPanel({ onCreateMaterialRequest }: MaterialPaintPanelProps) {
25
- const { activePaintMaterial, paintEraser, setPaintEraser, canResetSelection, resetSelection, materialCount, selectMaterial, createCustomMaterial: createMaterial } = useMaterialPaintPanelModel()
100
+ const painting = useEditor((s) => s.mode === 'material-paint')
101
+ const erasing = usePaintRegionMode((s) => s.mode) === 'erase' && painting
102
+ const {
103
+ activePaintMaterial,
104
+ materialCount,
105
+ selectMaterial,
106
+ createCustomMaterial: createMaterial,
107
+ } = useMaterialPaintPanelModel()
26
108
  const [autoEditMaterialId, setAutoEditMaterialId] = useState<SceneMaterialId | null>(null)
27
109
  const createCustomMaterial = () => setAutoEditMaterialId(createMaterial())
28
110
 
29
111
  return (
30
112
  <div className="flex h-full min-h-0 w-full flex-col">
31
- {/* Fixed: eraser / reset. */}
32
- <div className="flex shrink-0 items-center gap-2 pb-2">
33
- <Button
34
- aria-pressed={paintEraser}
35
- className="flex-1"
36
- onClick={() => setPaintEraser(!paintEraser)}
37
- size="sm"
38
- variant={paintEraser ? 'default' : 'outline'}
39
- >
40
- <Eraser />
41
- Erase
42
- </Button>
43
- <Button
44
- className="flex-1"
45
- disabled={!canResetSelection}
46
- onClick={resetSelection}
47
- size="sm"
48
- variant="outline"
49
- >
50
- <RotateCcw />
51
- Reset all
52
- </Button>
53
- </div>
113
+ {/* Fixed: whole surface, part of a surface, or erase. */}
114
+ <PaintModeControl />
54
115
 
116
+ {/* Erasing paints nothing: no swatches to pick from, just what a click does. */}
117
+ {erasing ? (
118
+ <p className="px-0.5 py-1 text-muted-foreground text-xs" data-paint-erase-note>
119
+ Click a painted part of a wall, floor or ceiling to remove it, or a painted surface to reset it to
120
+ its default.
121
+ </p>
122
+ ) : (
123
+ <>
55
124
  {/* Scrolls: category tabs (fixed inside) + catalog grid (the scroll). */}
56
125
  {/* A stable hook for host-app onboarding to point at. Static, and read
57
126
  only from outside: nothing here depends on it. */}
@@ -94,6 +163,8 @@ export function MaterialPaintPanel({ onCreateMaterialRequest }: MaterialPaintPan
94
163
  )}
95
164
  </div>
96
165
  </div>
166
+ </>
167
+ )}
97
168
  </div>
98
169
  )
99
170
  }
@@ -14,6 +14,8 @@ export type MaterialPickerProps = {
14
14
  nodeType?: MaterialTarget
15
15
  hideSideControl?: boolean
16
16
  onCreateMaterialRequest?: () => void
17
+ /** Whether switching category tabs selects the tab's first material (paint mode does). */
18
+ selectOnCategoryChange?: boolean
17
19
  }
18
20
 
19
21
  function getCategoryLabel(category: string) {
@@ -31,8 +33,9 @@ export function MaterialPicker({
31
33
  onSelectMaterialPreset,
32
34
  disabled = false,
33
35
  onCreateMaterialRequest,
36
+ selectOnCategoryChange = true,
34
37
  }: MaterialPickerProps) {
35
- const { selectedCategory, setSelectedCategory, sourceFilter, setSourceFilter, visibleSourceFilters, availableCategories, catalogItems, select: handleCatalogSelect } = useMaterialCatalogModel(selectedMaterialPreset, onSelectMaterialPreset, disabled)
38
+ const { selectedCategory, setSelectedCategory, sourceFilter, setSourceFilter, visibleSourceFilters, availableCategories, catalogItems, select: handleCatalogSelect } = useMaterialCatalogModel(selectedMaterialPreset, onSelectMaterialPreset, disabled, selectOnCategoryChange)
36
39
 
37
40
  return (
38
41
  <div
@@ -10,21 +10,13 @@ import {
10
10
  } from '@pascal-app/core'
11
11
  import { Copy, Paintbrush, Pencil, Trash2 } from 'lucide-react'
12
12
  import { useEffect, useMemo, useState } from 'react'
13
+ import { sceneMaterialUsageCounts } from '../../../lib/scene-material-refs'
13
14
  import useEditor from '../../../store/use-editor'
14
15
  import { Button } from '../primitives/button'
15
16
  import { Input } from '../primitives/input'
16
17
  import { Tooltip, TooltipContent, TooltipTrigger } from '../primitives/tooltip'
17
18
  import { MaterialPropertiesEditor } from './material-properties-editor'
18
19
 
19
- type SlotRecord = Record<string, string | undefined>
20
-
21
- function getSlotRecord(node: unknown): SlotRecord | null {
22
- if (!node || typeof node !== 'object' || !('slots' in node)) return null
23
- const slots = (node as { slots?: unknown }).slots
24
- if (!slots || typeof slots !== 'object' || Array.isArray(slots)) return null
25
- return slots as SlotRecord
26
- }
27
-
28
20
  export function SceneMaterialList({ autoEditId }: { autoEditId?: SceneMaterialId | null }) {
29
21
  const materials = useScene((state) => state.materials)
30
22
  const nodes = useScene((state) => state.nodes)
@@ -40,29 +32,9 @@ export function SceneMaterialList({ autoEditId }: { autoEditId?: SceneMaterialId
40
32
  [materials],
41
33
  )
42
34
 
43
- const usageCounts = useMemo(() => {
44
- const counts = new Map<SceneMaterialId, number>()
45
- const refToId = new Map<string, SceneMaterialId>()
46
-
47
- for (const [id] of materialEntries) {
48
- counts.set(id, 0)
49
- refToId.set(toSceneMaterialRef(id), id)
50
- }
51
-
52
- for (const node of Object.values(nodes)) {
53
- const slots = getSlotRecord(node)
54
- if (!slots) continue
55
-
56
- for (const value of Object.values(slots)) {
57
- if (typeof value !== 'string') continue
58
- const materialId = refToId.get(value)
59
- if (!materialId) continue
60
- counts.set(materialId, (counts.get(materialId) ?? 0) + 1)
61
- }
62
- }
63
-
64
- return counts
65
- }, [materialEntries, nodes])
35
+ // Every place a scene material can be used — slots and the room, wall and
36
+ // footprint finishes — counted by the same walker saving and pasting use.
37
+ const usageCounts = useMemo(() => sceneMaterialUsageCounts(nodes), [nodes])
66
38
 
67
39
  return (
68
40
  <div className="space-y-2">
@@ -0,0 +1,65 @@
1
+ 'use client'
2
+
3
+ import { ChevronDown } from 'lucide-react'
4
+ import { cn } from '../../../lib/utils'
5
+ import {
6
+ DropdownMenu,
7
+ DropdownMenuContent,
8
+ DropdownMenuRadioGroup,
9
+ DropdownMenuRadioItem,
10
+ DropdownMenuTrigger,
11
+ } from '../primitives/dropdown-menu'
12
+
13
+ interface SelectControlProps<T extends string> {
14
+ label: string
15
+ value: T
16
+ onChange: (value: T) => void
17
+ options: { label: string; value: T }[]
18
+ className?: string
19
+ mixed?: boolean
20
+ }
21
+
22
+ export function SelectControl<T extends string>({
23
+ label,
24
+ value,
25
+ onChange,
26
+ options,
27
+ className,
28
+ mixed = false,
29
+ }: SelectControlProps<T>) {
30
+ const current = mixed ? 'Mixed' : options.find((option) => option.value === value)?.label
31
+ return (
32
+ <DropdownMenu>
33
+ <DropdownMenuTrigger asChild>
34
+ <button
35
+ aria-label={label}
36
+ className={cn(
37
+ 'group flex h-10 w-full items-center justify-between gap-2 rounded-lg border border-border/50 bg-[#2C2C2E] px-3 text-sm transition-colors hover:bg-[#3e3e3e] data-[state=open]:bg-[#3e3e3e]',
38
+ className,
39
+ )}
40
+ type="button"
41
+ >
42
+ <span className="select-none text-muted-foreground transition-colors group-hover:text-foreground">
43
+ {label}
44
+ </span>
45
+ <span className="flex min-w-0 items-center gap-1.5 text-foreground">
46
+ <span className="truncate">{current}</span>
47
+ <ChevronDown className="size-3.5 shrink-0 text-muted-foreground" />
48
+ </span>
49
+ </button>
50
+ </DropdownMenuTrigger>
51
+ <DropdownMenuContent align="end" className="min-w-(--radix-dropdown-menu-trigger-width)">
52
+ <DropdownMenuRadioGroup
53
+ onValueChange={(next) => onChange(next as T)}
54
+ value={mixed ? '' : value}
55
+ >
56
+ {options.map((option) => (
57
+ <DropdownMenuRadioItem key={option.value} value={option.value}>
58
+ {option.label}
59
+ </DropdownMenuRadioItem>
60
+ ))}
61
+ </DropdownMenuRadioGroup>
62
+ </DropdownMenuContent>
63
+ </DropdownMenu>
64
+ )
65
+ }
@@ -50,12 +50,13 @@ import { pasteSelectionAndPickUp } from '../editor/group-actions'
50
50
  import {
51
51
  buildLevelDuplicateCreateOps,
52
52
  type LevelDuplicatePreset,
53
- } from '../../lib/level-duplication'
53
+ } from '@pascal-app/core/building'
54
54
  import { getDefaultLevelName, getLevelDisplayName } from '@pascal-app/core'
55
55
  import { deleteLevelWithFallbackSelection } from '../../lib/level-selection'
56
56
  import { unitMemberLevels, leaveUnitFocus } from '../../lib/units'
57
57
  import { useLinearDisplay } from '../../lib/use-linear-display'
58
58
  import { cn } from '../../lib/utils'
59
+ import { useIsMobile } from '../../hooks/use-mobile'
59
60
  import { ActionButton } from './controls/action-button'
60
61
  import { SliderControl } from './controls/slider-control'
61
62
  import { LevelDuplicateDialog } from './level-duplicate-dialog'
@@ -448,11 +449,13 @@ function UnitFocusChip() {
448
449
  // ── Main component ──────────────────────────────────────────────────────────
449
450
 
450
451
  export function FloatingLevelSelector() {
452
+ // Desktop's view bar is a row above the stage; mobile's floats over it.
453
+ const isMobile = useIsMobile()
451
454
  const selectedBuildingId = useViewer((s) => s.selection.buildingId)
452
455
  const levelId = useViewer((s) => s.selection.levelId)
453
456
  const setSelection = useViewer((s) => s.setSelection)
454
457
  const createNode = useScene((s) => s.createNode)
455
- const createNodes = useScene((s) => s.createNodes)
458
+ const applyNodeChanges = useScene((s) => s.applyNodeChanges)
456
459
  const updateNodes = useScene((s) => s.updateNodes)
457
460
 
458
461
  const [deletingLevel, setDeletingLevel] = useState<LevelNode | null>(null)
@@ -560,29 +563,21 @@ export function FloatingLevelSelector() {
560
563
 
561
564
  const handleDuplicateLevel = useCallback(
562
565
  (level: LevelNode, preset: LevelDuplicatePreset = 'everything') => {
563
- const { createOps, newLevelId, shiftedLevels } = buildLevelDuplicateCreateOps({
566
+ const { createOps, newLevelId, updateOps } = buildLevelDuplicateCreateOps({
564
567
  nodes: useScene.getState().nodes,
565
568
  level,
566
569
  levels,
567
570
  preset,
568
571
  })
569
572
 
570
- if (shiftedLevels.length > 0) {
571
- updateNodes(
572
- shiftedLevels.map((shiftedLevel) => ({
573
- id: shiftedLevel.id as AnyNodeId,
574
- data: { level: shiftedLevel.level } as Partial<AnyNode>,
575
- })),
576
- )
577
- }
578
- createNodes(createOps)
573
+ applyNodeChanges({ create: createOps, update: updateOps })
579
574
 
580
575
  setSelection({
581
576
  buildingId: resolvedBuildingId ?? undefined,
582
577
  levelId: newLevelId as LevelNode['id'],
583
578
  })
584
579
  },
585
- [createNodes, levels, resolvedBuildingId, setSelection, updateNodes],
580
+ [applyNodeChanges, levels, resolvedBuildingId, setSelection],
586
581
  )
587
582
 
588
583
  const handlePasteToLevel = useCallback((level: LevelNode) => {
@@ -641,7 +636,7 @@ export function FloatingLevelSelector() {
641
636
 
642
637
  return (
643
638
  <>
644
- <div className="pointer-events-auto absolute top-14 left-3 z-20">
639
+ <div className={cn('pointer-events-auto absolute left-3 z-20', isMobile ? 'top-14' : 'top-3')}>
645
640
  <div className="relative">
646
641
  {/* Floating + at top edge */}
647
642
  {!draggingLevelId && (
@@ -1,14 +1,17 @@
1
+ import type { HudTitle } from '../../../lib/hud-title'
1
2
  import { ContextualHelperPanel } from './contextual-helper-panel'
2
3
 
3
4
  interface BuildingHelperProps {
4
5
  showRotate?: boolean
6
+ title?: HudTitle | null
5
7
  }
6
8
 
7
9
  // Rotate is one hint with both keys (R / T) — never two separate
8
10
  // counterclockwise / clockwise rows — to match every other placement helper.
9
- export function BuildingHelper({ showRotate }: BuildingHelperProps) {
11
+ export function BuildingHelper({ showRotate, title = null }: BuildingHelperProps) {
10
12
  return (
11
13
  <ContextualHelperPanel
14
+ title={title}
12
15
  hints={[
13
16
  { keys: ['Left click'], label: 'Place building' },
14
17
  ...(showRotate ? [{ keys: ['R', 'T'], label: 'Rotate' }] : []),