@pascal-app/editor 1.0.3 → 1.0.5

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 (624) hide show
  1. package/README.md +106 -0
  2. package/package.json +42 -38
  3. package/src/components/editor/__bench__/room-label-placement.bench.ts +108 -0
  4. package/src/components/editor/action-menu-button.tsx +83 -0
  5. package/src/components/editor/bake-thumbnail.tsx +307 -5
  6. package/src/components/editor/capture-level-picker.tsx +72 -0
  7. package/src/components/editor/construction-reveal-audio.test.tsx +150 -0
  8. package/src/components/editor/construction-reveal-audio.tsx +40 -0
  9. package/src/components/editor/custom-camera-controls.tsx +275 -57
  10. package/src/components/editor/delete-confirmation-dialog.tsx +297 -7
  11. package/src/components/editor/duplicate-node.ts +244 -0
  12. package/src/components/editor/editor-layout-mobile.tsx +37 -26
  13. package/src/components/editor/editor-layout-v2.tsx +132 -59
  14. package/src/components/editor/export-manager.tsx +47 -26
  15. package/src/components/editor/first-person/build-collider-world.test.ts +320 -0
  16. package/src/components/editor/first-person/build-collider-world.ts +28 -7
  17. package/src/components/editor/first-person/ground-holes.test.ts +128 -0
  18. package/src/components/editor/first-person/ground-holes.ts +130 -0
  19. package/src/components/editor/first-person/mechanism-targets.test.ts +162 -0
  20. package/src/components/editor/first-person/mechanism-targets.ts +88 -0
  21. package/src/components/editor/first-person-controls.tsx +220 -9
  22. package/src/components/editor/floating-action-menu.tsx +63 -207
  23. package/src/components/editor/floor-aware-camera-controls.test.ts +466 -0
  24. package/src/components/editor/floor-aware-camera-controls.ts +324 -0
  25. package/src/components/editor/floor-openings-overlay.tsx +243 -0
  26. package/src/components/editor/floor-region-controls.tsx +846 -0
  27. package/src/components/editor/floor-region-webgpu-materials.test.ts +12 -0
  28. package/src/components/editor/floor-surface.test.ts +165 -0
  29. package/src/components/editor/floor-surface.ts +182 -0
  30. package/src/components/editor/floorplan-background-selection.test.ts +11 -2
  31. package/src/components/editor/floorplan-background-selection.ts +1 -3
  32. package/src/components/editor/floorplan-mode-coordinator.tsx +2 -2
  33. package/src/components/editor/floorplan-panel.tsx +414 -363
  34. package/src/components/editor/follow-pascal.test.tsx +912 -0
  35. package/src/components/editor/follow-pascal.tsx +354 -0
  36. package/src/components/editor/footprint-height-handle.tsx +204 -0
  37. package/src/components/editor/group-actions.ts +56 -2
  38. package/src/components/editor/group-move-3d.ts +2 -0
  39. package/src/components/editor/group-rotate-handle.tsx +2 -0
  40. package/src/components/editor/group-transform-shared.test.ts +460 -0
  41. package/src/components/editor/handles/handle-hit-priority.test.tsx +63 -0
  42. package/src/components/editor/handles/handle-hit-priority.tsx +37 -0
  43. package/src/components/editor/handles/swallow-next-click.test.ts +28 -0
  44. package/src/components/editor/handles/use-handle-drag.ts +269 -197
  45. package/src/components/editor/index.tsx +409 -197
  46. package/src/components/editor/join-walls-pill.tsx +44 -0
  47. package/src/components/editor/node-action-menu.tsx +68 -70
  48. package/src/components/editor/node-arrow-handles.tsx +25 -6
  49. package/src/components/editor/open-wall-ends-3d-layer.tsx +273 -0
  50. package/src/components/editor/open-wall-ends-hint.tsx +89 -0
  51. package/src/components/editor/quick-measurement-hud.tsx +3 -2
  52. package/src/components/editor/registry-action-contributions.test.tsx +63 -0
  53. package/src/components/editor/registry-action-contributions.tsx +80 -1
  54. package/src/components/editor/riser-diagram-panel.tsx +14 -7
  55. package/src/components/editor/room-controls.tsx +847 -0
  56. package/src/components/editor/room-floor-highlight.tsx +128 -0
  57. package/src/components/editor/room-handles-2d.tsx +354 -0
  58. package/src/components/editor/room-handles.tsx +675 -0
  59. package/src/components/editor/room-highlight.tsx +233 -0
  60. package/src/components/editor/room-labels.test.ts +850 -0
  61. package/src/components/editor/room-labels.tsx +1890 -0
  62. package/src/components/editor/room-transform-ghost.tsx +256 -0
  63. package/src/components/editor/room-webgpu-materials.test.ts +27 -0
  64. package/src/components/editor/selection-manager.tsx +474 -419
  65. package/src/components/editor/slab-hole-highlights.tsx +5 -2
  66. package/src/components/editor/snapshot-capture-overlay.tsx +7 -2
  67. package/src/components/editor/snapshot-capture.test.ts +119 -60
  68. package/src/components/editor/snapshot-capture.ts +36 -5
  69. package/src/components/editor/thumbnail-generator.test.ts +654 -0
  70. package/src/components/editor/thumbnail-generator.tsx +1103 -223
  71. package/src/components/editor/use-floorplan-background-placement.ts +34 -12
  72. package/src/components/editor/vector-edge-extractor.tsx +121 -0
  73. package/src/components/editor/views/host-view.tsx +61 -0
  74. package/src/components/editor/views/plugins-view.tsx +663 -0
  75. package/src/components/editor/views/scene-region.tsx +59 -0
  76. package/src/components/editor/views/use-editor-views.ts +57 -0
  77. package/src/components/editor/views/view-bar.test.tsx +54 -0
  78. package/src/components/editor/views/view-bar.tsx +343 -0
  79. package/src/components/editor/wall-justification.test.ts +140 -0
  80. package/src/components/editor/wall-move-side-handles.tsx +43 -142
  81. package/src/components/editor/wall-push-arrow.tsx +92 -0
  82. package/src/components/editor/wall-region-handles.tsx +509 -0
  83. package/src/components/editor/wall-snap-beacon-layer.tsx +8 -4
  84. package/src/components/editor-2d/floorplan-cursor-indicator-overlay.tsx +8 -4
  85. package/src/components/editor-2d/floorplan-group-move.tsx +3 -0
  86. package/src/components/editor-2d/floorplan-open-wall-ends-layer.test.tsx +86 -0
  87. package/src/components/editor-2d/floorplan-open-wall-ends-layer.tsx +290 -0
  88. package/src/components/editor-2d/floorplan-registered-tool-layer.tsx +2 -2
  89. package/src/components/editor-2d/floorplan-registry-action-menu.tsx +11 -1
  90. package/src/components/editor-2d/floorplan-registry-move-overlay.tsx +116 -106
  91. package/src/components/editor-2d/renderers/floorplan-dimension-edit-overlay.test.ts +156 -0
  92. package/src/components/editor-2d/renderers/floorplan-dimension-edit-overlay.tsx +278 -0
  93. package/src/components/editor-2d/renderers/floorplan-dimension-renderer.tsx +77 -0
  94. package/src/components/editor-2d/renderers/{floorplan-draft-wall-measurement.tsx → floorplan-draft-measurement.tsx} +8 -8
  95. package/src/components/editor-2d/renderers/floorplan-geometry-renderer.tsx +4 -1
  96. package/src/components/editor-2d/renderers/floorplan-registry-layer.test.ts +38 -8
  97. package/src/components/editor-2d/renderers/floorplan-registry-layer.tsx +319 -233
  98. package/src/components/editor-2d/renderers/floorplan-reveal.test.tsx +254 -0
  99. package/src/components/editor-2d/renderers/floorplan-reveal.ts +141 -0
  100. package/src/components/editor-2d/svg-paths.ts +12 -15
  101. package/src/components/point-ask/bubble.tsx +538 -0
  102. package/src/components/point-ask/hover-chip.tsx +91 -0
  103. package/src/components/point-ask/mode-pill.tsx +92 -0
  104. package/src/components/point-ask/optional-motion.ts +22 -0
  105. package/src/components/point-ask/outline-corners.tsx +93 -0
  106. package/src/components/point-ask/pins.tsx +711 -0
  107. package/src/components/point-ask/point-ask-layer.tsx +83 -0
  108. package/src/components/point-ask/point-ask-styles.ts +105 -0
  109. package/src/components/point-ask/region-marquee-styles.ts +15 -0
  110. package/src/components/point-ask/region-marquee.tsx +64 -0
  111. package/src/components/procedural-items/inspector.tsx +40 -15
  112. package/src/components/systems/ceiling/ceiling-bracket-batch.test.ts +12 -2
  113. package/src/components/systems/ceiling/ceiling-selection-affordance-system.test.ts +97 -14
  114. package/src/components/systems/ceiling/ceiling-selection-affordance-system.tsx +18 -18
  115. package/src/components/systems/roof/roof-edit-system.tsx +1 -0
  116. package/src/components/systems/zone/zone-system.test.ts +47 -0
  117. package/src/components/systems/zone/zone-system.tsx +29 -68
  118. package/src/components/tools/distribution/connection-compatibility.test.ts +34 -0
  119. package/src/components/tools/distribution/connection-compatibility.ts +43 -0
  120. package/src/components/tools/distribution/distribution-public.ts +9 -0
  121. package/src/components/tools/distribution/distribution-run-contract.test.ts +26 -0
  122. package/src/components/tools/distribution/distribution-run-contract.ts +94 -0
  123. package/src/components/tools/distribution/distribution-run-tool.test.ts +241 -0
  124. package/src/components/tools/distribution/distribution-run-tool.test.tsx +125 -0
  125. package/src/components/tools/distribution/distribution-run-tool.tsx +1323 -0
  126. package/src/components/tools/distribution/draw-alignment.ts +35 -0
  127. package/src/components/tools/distribution/ports.test.ts +56 -0
  128. package/src/components/tools/distribution/ports.ts +378 -0
  129. package/src/components/tools/distribution/run-cursor.test.ts +135 -0
  130. package/src/components/tools/distribution/run-cursor.ts +36 -0
  131. package/src/components/tools/distribution/run-direction-feedback.test.ts +60 -0
  132. package/src/components/tools/distribution/run-direction-feedback.tsx +251 -0
  133. package/src/components/tools/distribution/run-port-snap.test.ts +79 -0
  134. package/src/components/tools/distribution/run-port-snap.ts +25 -0
  135. package/src/components/tools/distribution/run-screen-direction.test.ts +23 -0
  136. package/src/components/tools/distribution/run-screen-direction.ts +30 -0
  137. package/src/components/tools/elevator/elevator-tool.tsx +3 -6
  138. package/src/components/tools/elevator/move-elevator-tool.tsx +2 -3
  139. package/src/components/tools/item/item-surface-strategy-resolver.test.ts +67 -68
  140. package/src/components/tools/item/placement-history.test.tsx +512 -0
  141. package/src/components/tools/item/placement-strategies.test.ts +46 -1
  142. package/src/components/tools/item/placement-strategies.ts +11 -10
  143. package/src/components/tools/item/shelf-surface-strategy-resolver.test.ts +56 -56
  144. package/src/components/tools/item/use-draft-node.test.tsx +432 -1
  145. package/src/components/tools/item/use-draft-node.ts +166 -95
  146. package/src/components/tools/item/use-placement-coordinator.tsx +148 -95
  147. package/src/components/tools/paint-region/use-paint-tint.ts +37 -0
  148. package/src/components/tools/paint-region/wall-paint-region-tool.tsx +466 -0
  149. package/src/components/tools/registry/item-surface-move.test.ts +62 -63
  150. package/src/components/tools/registry/item-surface-move.ts +2 -0
  151. package/src/components/tools/registry/item-surface-resolver.test.ts +26 -26
  152. package/src/components/tools/registry/move-registry-node-tool.test.ts +268 -1
  153. package/src/components/tools/registry/move-registry-node-tool.tsx +66 -30
  154. package/src/components/tools/select/box-select-tool.tsx +2 -12
  155. package/src/components/tools/select/plane-box-select-tool.test.tsx +79 -0
  156. package/src/components/tools/select/plane-box-select-tool.tsx +35 -22
  157. package/src/components/tools/select/select-candidates.test.ts +37 -4
  158. package/src/components/tools/select/select-candidates.ts +15 -11
  159. package/src/components/tools/shared/cursor-sphere.tsx +36 -12
  160. package/src/components/tools/site/site-boundary-editor.tsx +1 -2
  161. package/src/components/tools/tool-manager.tsx +59 -24
  162. package/src/components/tools/wall/wall-drafting.test.ts +175 -47
  163. package/src/components/tools/zone/zone-tool.tsx +4 -12
  164. package/src/components/ui/action-menu/action-button.tsx +9 -1
  165. package/src/components/ui/action-menu/camera-actions.tsx +9 -4
  166. package/src/components/ui/action-menu/control-modes.tsx +15 -42
  167. package/src/components/ui/action-menu/follow-pascal-pill.tsx +32 -0
  168. package/src/components/ui/action-menu/follow-pascal-toggle.tsx +36 -0
  169. package/src/components/ui/action-menu/index.tsx +56 -33
  170. package/src/components/ui/action-menu/measurement-control.tsx +14 -19
  171. package/src/components/ui/action-menu/placement.ts +20 -0
  172. package/src/components/ui/action-menu/point-ask-button.tsx +52 -0
  173. package/src/components/ui/action-menu/view-toggles.tsx +47 -42
  174. package/src/components/ui/build-panel/build-panel.tsx +242 -0
  175. package/src/components/ui/command-palette/editor-commands.tsx +7 -42
  176. package/src/components/ui/command-palette/index.tsx +50 -39
  177. package/src/components/ui/command-palette/ranked-groups.test.ts +31 -0
  178. package/src/components/ui/controls/action-button.tsx +5 -2
  179. package/src/components/ui/controls/finish-swatch.tsx +110 -0
  180. package/src/components/ui/controls/material-paint-panel.tsx +97 -26
  181. package/src/components/ui/controls/material-picker.tsx +4 -1
  182. package/src/components/ui/controls/metric-control.tsx +1 -1
  183. package/src/components/ui/controls/scene-material-list.tsx +4 -32
  184. package/src/components/ui/controls/segmented-control.tsx +21 -8
  185. package/src/components/ui/controls/select-control.tsx +65 -0
  186. package/src/components/ui/controls/slider-control.tsx +1 -1
  187. package/src/components/ui/controls/toggle-control.tsx +3 -3
  188. package/src/components/ui/controls/tool-options-panel.tsx +1 -1
  189. package/src/components/ui/floating-level-selector.tsx +159 -59
  190. package/src/components/ui/helpers/building-helper.tsx +4 -1
  191. package/src/components/ui/helpers/contextual-helper-panel.tsx +181 -72
  192. package/src/components/ui/helpers/helper-manager.test.tsx +121 -0
  193. package/src/components/ui/helpers/helper-manager.tsx +331 -14
  194. package/src/components/ui/helpers/item-helper.tsx +17 -1
  195. package/src/components/ui/helpers/registered-tool-helper.tsx +8 -1
  196. package/src/components/ui/icon-ref.tsx +45 -1
  197. package/src/components/ui/level-duplicate-dialog.tsx +1 -1
  198. package/src/components/ui/node-icon-image.tsx +16 -0
  199. package/src/components/ui/panels/collections/collections-popover.tsx +28 -22
  200. package/src/components/ui/panels/floor-foundation-panel.tsx +551 -0
  201. package/src/components/ui/panels/floor-opening-panel.tsx +143 -0
  202. package/src/components/ui/panels/mobile-panel-sheet.tsx +4 -4
  203. package/src/components/ui/panels/mobile-selection-bar.tsx +8 -5
  204. package/src/components/ui/panels/multi-field-value.test.ts +26 -3
  205. package/src/components/ui/panels/multi-field-value.ts +6 -3
  206. package/src/components/ui/panels/multi-parametric-inspector.tsx +52 -2
  207. package/src/components/ui/panels/multi-selection-panel.tsx +161 -5
  208. package/src/components/ui/panels/node-display.ts +8 -3
  209. package/src/components/ui/panels/panel-manager.tsx +103 -68
  210. package/src/components/ui/panels/panel-wrapper.tsx +130 -62
  211. package/src/components/ui/panels/parametric-field-control.tsx +3 -3
  212. package/src/components/ui/panels/parametric-inspector.tsx +7 -7
  213. package/src/components/ui/panels/reference-panel.tsx +2 -3
  214. package/src/components/ui/panels/room-construction-rows.tsx +774 -0
  215. package/src/components/ui/panels/room-name-options.test.ts +55 -0
  216. package/src/components/ui/panels/room-name-options.ts +47 -0
  217. package/src/components/ui/panels/room-openings-field.tsx +110 -0
  218. package/src/components/ui/panels/room-panel.tsx +790 -0
  219. package/src/components/ui/panels/shape-choice.tsx +58 -0
  220. package/src/components/ui/panels/wall-region-list.tsx +109 -0
  221. package/src/components/ui/primitives/badge.tsx +8 -0
  222. package/src/components/ui/primitives/color-dot.tsx +36 -24
  223. package/src/components/ui/primitives/sidebar.tsx +3 -1
  224. package/src/components/ui/right-stack.test.ts +29 -0
  225. package/src/components/ui/right-stack.tsx +164 -0
  226. package/src/components/ui/sidebar/app-sidebar.tsx +1 -1
  227. package/src/components/ui/sidebar/icon-rail.tsx +8 -3
  228. package/src/components/ui/sidebar/panels/items-panel/function-tree-panel.tsx +1 -1
  229. package/src/components/ui/sidebar/panels/items-panel/index.tsx +6 -6
  230. package/src/components/ui/sidebar/panels/settings-panel/animation-speed-setting.tsx +40 -0
  231. package/src/components/ui/sidebar/panels/settings-panel/audio-settings-dialog.tsx +7 -8
  232. package/src/components/ui/sidebar/panels/settings-panel/construction-animation-setting.test.tsx +170 -0
  233. package/src/components/ui/sidebar/panels/settings-panel/construction-animation-setting.tsx +92 -0
  234. package/src/components/ui/sidebar/panels/settings-panel/index.tsx +138 -27
  235. package/src/components/ui/sidebar/panels/settings-panel/interface-theme.test.tsx +20 -0
  236. package/src/components/ui/sidebar/panels/settings-panel/keyboard-shortcuts-dialog.tsx +5 -5
  237. package/src/components/ui/sidebar/panels/settings-panel/print-export-button.tsx +4 -10
  238. package/src/components/ui/sidebar/panels/site-panel/building-tree-node.tsx +3 -11
  239. package/src/components/ui/sidebar/panels/site-panel/ceiling-tree-node.tsx +4 -4
  240. package/src/components/ui/sidebar/panels/site-panel/chimney-tree-node.tsx +1 -5
  241. package/src/components/ui/sidebar/panels/site-panel/collection-tree-node.tsx +197 -0
  242. package/src/components/ui/sidebar/panels/site-panel/column-tree-node.tsx +1 -5
  243. package/src/components/ui/sidebar/panels/site-panel/door-tree-node.tsx +1 -5
  244. package/src/components/ui/sidebar/panels/site-panel/dormer-tree-node.tsx +1 -5
  245. package/src/components/ui/sidebar/panels/site-panel/elevator-tree-node.tsx +1 -5
  246. package/src/components/ui/sidebar/panels/site-panel/fence-tree-node.tsx +24 -11
  247. package/src/components/ui/sidebar/panels/site-panel/gutter-tree-node.tsx +1 -5
  248. package/src/components/ui/sidebar/panels/site-panel/index.tsx +181 -223
  249. package/src/components/ui/sidebar/panels/site-panel/inline-rename-input.tsx +30 -8
  250. package/src/components/ui/sidebar/panels/site-panel/item-tree-node.tsx +1 -3
  251. package/src/components/ui/sidebar/panels/site-panel/level-tree-node.tsx +4 -1
  252. package/src/components/ui/sidebar/panels/site-panel/registry-tree-node.tsx +1 -3
  253. package/src/components/ui/sidebar/panels/site-panel/roof-tree-node.tsx +1 -4
  254. package/src/components/ui/sidebar/panels/site-panel/shelf-tree-node.tsx +1 -3
  255. package/src/components/ui/sidebar/panels/site-panel/slab-tree-node.tsx +21 -13
  256. package/src/components/ui/sidebar/panels/site-panel/solar-panel-tree-node.tsx +1 -5
  257. package/src/components/ui/sidebar/panels/site-panel/spawn-tree-node.tsx +1 -5
  258. package/src/components/ui/sidebar/panels/site-panel/stair-tree-node.tsx +1 -5
  259. package/src/components/ui/sidebar/panels/site-panel/tree-node.tsx +13 -37
  260. package/src/components/ui/sidebar/panels/site-panel/unit-tree-node.tsx +2 -3
  261. package/src/components/ui/sidebar/panels/site-panel/wall-tree-node.tsx +1 -5
  262. package/src/components/ui/sidebar/panels/site-panel/window-tree-node.tsx +1 -5
  263. package/src/components/ui/sidebar/panels/site-panel/zone-tree-node.tsx +9 -4
  264. package/src/components/ui/sidebar/panels/zone-panel/index.tsx +2 -4
  265. package/src/components/ui/sidebar/tab-bar.tsx +14 -4
  266. package/src/components/ui/sidebar/use-plugin-panels.tsx +15 -5
  267. package/src/components/viewer/floorplan-compass-button.tsx +9 -1
  268. package/src/components/viewer/floorplan-preview.tsx +5 -6
  269. package/src/components/viewer/viewer-controls-bar.tsx +18 -6
  270. package/src/components/viewer/viewer-hover-label.tsx +34 -0
  271. package/src/components/viewer/viewer-rooms.tsx +191 -0
  272. package/src/components/viewer/viewer-scene-header.tsx +181 -145
  273. package/src/components/viewer/viewer-selection-manager.tsx +124 -0
  274. package/src/components/viewer/viewer-stage.tsx +2 -2
  275. package/src/components/viewer-zone-system.tsx +3 -8
  276. package/src/hooks/use-auto-save.integration.test.tsx +215 -43
  277. package/src/hooks/use-auto-save.ts +60 -9
  278. package/src/hooks/use-ceiling-events.test.ts +123 -118
  279. package/src/hooks/use-grid-events.test.ts +105 -105
  280. package/src/hooks/use-keyboard.test.ts +243 -19
  281. package/src/hooks/use-keyboard.ts +118 -83
  282. package/src/hooks/use-open-wall-ends.test.ts +99 -0
  283. package/src/hooks/use-open-wall-ends.ts +238 -0
  284. package/src/hooks/use-placement-notice.test.ts +71 -0
  285. package/src/hooks/use-placement-notice.ts +73 -0
  286. package/src/hooks/use-selected-room.ts +143 -0
  287. package/src/index.tsx +312 -58
  288. package/src/lib/__fixtures__/owner-victor-seattle-parking.json +1 -0
  289. package/src/lib/__fixtures__/owner-villa-aurelia-basement.json +1 -0
  290. package/src/lib/add-storey.test.ts +177 -0
  291. package/src/lib/add-storey.ts +59 -0
  292. package/src/lib/below-grade.test.tsx +171 -0
  293. package/src/lib/below-grade.ts +133 -0
  294. package/src/lib/camera-facing.test.ts +24 -0
  295. package/src/lib/camera-facing.ts +24 -0
  296. package/src/lib/ceiling-edit-session.test.ts +321 -0
  297. package/src/lib/ceiling-edit-session.ts +206 -0
  298. package/src/lib/ceiling-region-session.test.ts +179 -0
  299. package/src/lib/continuation.test.ts +19 -1
  300. package/src/lib/continuation.ts +25 -4
  301. package/src/lib/direct-manipulation.ts +2 -0
  302. package/src/lib/door-interaction.ts +14 -0
  303. package/src/lib/download-project-file.ts +17 -0
  304. package/src/lib/editor-level-display.test.ts +96 -0
  305. package/src/lib/editor-level-display.ts +55 -0
  306. package/src/lib/editor-views.ts +178 -0
  307. package/src/lib/export-geometry-wait.test.ts +90 -0
  308. package/src/lib/export-texture-utils.ts +2 -4
  309. package/src/lib/floor-edit-session.ts +153 -0
  310. package/src/lib/floor-footprints.test.ts +563 -0
  311. package/src/lib/floor-footprints.ts +395 -0
  312. package/src/lib/floor-foundation-support.test.ts +284 -0
  313. package/src/lib/floor-opening-draft.ts +218 -0
  314. package/src/lib/floor-openings.test.ts +216 -0
  315. package/src/lib/floor-region-geometry.ts +191 -0
  316. package/src/lib/floor-region-session.test.ts +485 -0
  317. package/src/lib/floor-region-session.ts +589 -0
  318. package/src/lib/floor-region-snap.ts +101 -0
  319. package/src/lib/floor-step-hover.test.ts +32 -0
  320. package/src/lib/floorplan/annotation-visibility.ts +17 -0
  321. package/src/lib/floorplan/dimension-drive.test.ts +360 -0
  322. package/src/lib/floorplan/dimension-drive.ts +479 -0
  323. package/src/lib/floorplan/floorplan-export.test.ts +12 -8
  324. package/src/lib/floorplan/floorplan-export.tsx +32 -8
  325. package/src/lib/floorplan/floorplan-extension.ts +32 -0
  326. package/src/lib/floorplan/floorplan-mode.test.ts +4 -0
  327. package/src/lib/floorplan/floorplan-mode.ts +2 -0
  328. package/src/lib/floorplan/floorplan-pdfkit-document.ts +363 -27
  329. package/src/lib/floorplan/floorplan-pdfkit-renderer.test.ts +177 -8
  330. package/src/lib/floorplan/floorplan-pdfkit-renderer.ts +60 -25
  331. package/src/lib/floorplan/floorplan-plugin-install.test.tsx +138 -0
  332. package/src/lib/floorplan/floorplan-readonly.ts +6 -0
  333. package/src/lib/floorplan/geometry.ts +13 -6
  334. package/src/lib/floorplan/index.ts +0 -9
  335. package/src/lib/floorplan/items.ts +5 -2
  336. package/src/lib/floorplan/marks.test.ts +213 -0
  337. package/src/lib/floorplan/marks.ts +238 -0
  338. package/src/lib/floorplan/open-wall-ends.test.ts +268 -0
  339. package/src/lib/floorplan/open-wall-ends.ts +162 -0
  340. package/src/lib/floorplan/pdf-fonts/GeistMono-Bold.ttf +0 -0
  341. package/src/lib/floorplan/pdf-fonts/GeistMono-Regular.ttf +0 -0
  342. package/src/lib/floorplan/pdf-fonts/LICENSE-GeistMono.txt +92 -0
  343. package/src/lib/floorplan/pdf-fonts/LICENSE-LiberationSans.txt +102 -0
  344. package/src/lib/floorplan/pdf-fonts/LiberationSans-Bold.ttf +0 -0
  345. package/src/lib/floorplan/pdf-fonts/LiberationSans-Regular.ttf +0 -0
  346. package/src/lib/floorplan/pdfkit-standard-fonts.d.ts +5 -0
  347. package/src/lib/floorplan/schedules.test.ts +189 -0
  348. package/src/lib/floorplan/schedules.ts +339 -0
  349. package/src/lib/floorplan/sheet-export.test.ts +124 -0
  350. package/src/lib/floorplan/sheet-export.ts +318 -0
  351. package/src/lib/floorplan/site-plan/build-site-plan-drawing.ts +768 -0
  352. package/src/lib/floorplan/site-plan/contours.test.ts +43 -0
  353. package/src/lib/floorplan/site-plan/contributors.ts +97 -0
  354. package/src/lib/floorplan/site-plan/coverage.ts +230 -0
  355. package/src/lib/floorplan/site-plan/drawing-type-switch.test.ts +15 -0
  356. package/src/lib/floorplan/site-plan/drawing-type-switch.tsx +86 -0
  357. package/src/lib/floorplan/site-plan/front-edge.test.ts +103 -0
  358. package/src/lib/floorplan/site-plan/front-edge.ts +157 -0
  359. package/src/lib/floorplan/site-plan/geometry.test.ts +269 -0
  360. package/src/lib/floorplan/site-plan/geometry.ts +142 -0
  361. package/src/lib/floorplan/site-plan/index.ts +55 -0
  362. package/src/lib/floorplan/site-plan/site-annotations.test.ts +483 -0
  363. package/src/lib/floorplan/site-plan/site-annotations.ts +1084 -0
  364. package/src/lib/floorplan/site-plan/site-parts.ts +287 -0
  365. package/src/lib/floorplan/site-plan/site-plan-layer.tsx +138 -0
  366. package/src/lib/floorplan/site-plan/street-name.test.ts +69 -0
  367. package/src/lib/floorplan/site-plan/street-name.ts +302 -0
  368. package/src/lib/floorplan/site-plan/yards.test.ts +70 -0
  369. package/src/lib/floorplan/types.ts +1 -27
  370. package/src/lib/follow-pascal-exterior.test-fixture.ts +17 -0
  371. package/src/lib/follow-pascal-exterior.test.ts +171 -0
  372. package/src/lib/follow-pascal-exterior.ts +39 -0
  373. package/src/lib/follow-pascal-presentation.test.ts +203 -0
  374. package/src/lib/follow-pascal-presentation.ts +129 -0
  375. package/src/lib/follow-pascal-sight.ts +84 -0
  376. package/src/lib/follow-pascal-targets.test.ts +406 -0
  377. package/src/lib/follow-pascal-targets.ts +198 -0
  378. package/src/lib/follow-pascal.test.ts +350 -0
  379. package/src/lib/follow-pascal.ts +284 -0
  380. package/src/lib/fresh-planar-placement.test.ts +148 -0
  381. package/src/lib/fresh-planar-placement.ts +114 -37
  382. package/src/lib/geometry-script/author.ts +157 -0
  383. package/src/lib/geometry-script/client.ts +159 -0
  384. package/src/lib/geometry-script/finish-scripts.ts +127 -0
  385. package/src/lib/geometry-script/protocol.ts +13 -0
  386. package/src/lib/gesture-lifecycle.test.ts +595 -0
  387. package/src/lib/gesture-lifecycle.ts +290 -0
  388. package/src/lib/glb-export-zone-holes.test.ts +41 -0
  389. package/src/lib/glb-export.test.ts +636 -126
  390. package/src/lib/glb-export.ts +241 -120
  391. package/src/lib/history.test.ts +18 -7
  392. package/src/lib/history.ts +7 -0
  393. package/src/lib/hud-title.ts +109 -0
  394. package/src/lib/hydration-gated-storage.ts +49 -0
  395. package/src/lib/ifc-export.test.ts +174 -0
  396. package/src/lib/ifc-export.ts +201 -0
  397. package/src/lib/ifc-parts.ts +15 -0
  398. package/src/lib/inspector-expanded.test.ts +44 -0
  399. package/src/lib/inspector-expanded.ts +36 -0
  400. package/src/lib/interaction/scope.ts +22 -0
  401. package/src/lib/keyboard-pan.test.ts +129 -7
  402. package/src/lib/keyboard-pan.ts +161 -12
  403. package/src/lib/level-selection.test.ts +57 -0
  404. package/src/lib/level-selection.ts +16 -1
  405. package/src/lib/live-gesture-round-trip.test.ts +360 -0
  406. package/src/lib/lot/__fixtures__/map-dossier-miami-shores.json +1 -0
  407. package/src/lib/lot/__fixtures__/map-dossier-stpete.json +1 -0
  408. package/src/lib/lot/clean-ring.test.ts +187 -0
  409. package/src/lib/lot/clean-ring.ts +217 -0
  410. package/src/lib/lot/dossier.test.ts +202 -0
  411. package/src/lib/lot/dossier.ts +498 -0
  412. package/src/lib/lot/drop-in.test.ts +326 -0
  413. package/src/lib/lot/drop-in.ts +392 -0
  414. package/src/lib/lot/index.ts +43 -0
  415. package/src/lib/lot/lot-patch.test.ts +340 -0
  416. package/src/lib/lot/lot-patch.ts +394 -0
  417. package/src/lib/lot/parcel-provider.ts +43 -0
  418. package/src/lib/lot/terrain.test.ts +161 -0
  419. package/src/lib/lot/terrain.ts +266 -0
  420. package/src/lib/material-catalog-model.ts +3 -1
  421. package/src/lib/material-paint-panel-model.ts +9 -5
  422. package/src/lib/material-paint.ts +4 -0
  423. package/src/lib/measurement-kind.test.ts +6 -8
  424. package/src/lib/measurements.test.ts +23 -0
  425. package/src/lib/mezzanine-actions.test.ts +342 -0
  426. package/src/lib/mezzanine-draft.test.ts +501 -0
  427. package/src/lib/mezzanine-draft.ts +278 -0
  428. package/src/lib/mezzanine-messages.ts +26 -0
  429. package/src/lib/mezzanine-selection.test.ts +207 -0
  430. package/src/lib/mezzanine-stairs.ts +46 -0
  431. package/src/lib/model-export.ts +3 -1
  432. package/src/lib/node-action-movement.ts +17 -6
  433. package/src/lib/node-display-label.ts +37 -0
  434. package/src/lib/orbit-floor-clearance.test.ts +201 -0
  435. package/src/lib/orbit-floor-clearance.ts +154 -0
  436. package/src/lib/paint-commit-route.test.ts +37 -0
  437. package/src/lib/paint-commit-route.ts +17 -0
  438. package/src/lib/paint-eyedropper.test.tsx +165 -0
  439. package/src/lib/paint-eyedropper.ts +99 -0
  440. package/src/lib/paint-pass-through.test.ts +122 -0
  441. package/src/lib/paint-pass-through.ts +78 -0
  442. package/src/lib/paint-region-hover.ts +25 -0
  443. package/src/lib/paint-region-mode.ts +154 -0
  444. package/src/lib/paint-regions.test.ts +166 -0
  445. package/src/lib/paint-regions.ts +192 -0
  446. package/src/lib/paint-scope.test.ts +317 -35
  447. package/src/lib/paint-scope.ts +267 -25
  448. package/src/lib/panel-rows.ts +21 -0
  449. package/src/lib/parametric-node-update.ts +16 -7
  450. package/src/lib/plate-paint-affected.test.ts +300 -0
  451. package/src/lib/plate-paint-affected.ts +177 -0
  452. package/src/lib/plugin-chat.ts +65 -0
  453. package/src/lib/plugin-panels.test.ts +25 -0
  454. package/src/lib/plugin-panels.ts +94 -0
  455. package/src/lib/point-ask/__fixtures__/consumer-browser.tsx +76 -0
  456. package/src/lib/point-ask/__fixtures__/house.ts +81 -0
  457. package/src/lib/point-ask/ask-created.test.ts +195 -0
  458. package/src/lib/point-ask/ask-flow.test.ts +380 -0
  459. package/src/lib/point-ask/build-context.test.ts +173 -0
  460. package/src/lib/point-ask/build-context.ts +94 -0
  461. package/src/lib/point-ask/capture-crop.ts +129 -0
  462. package/src/lib/point-ask/choreography.ts +69 -0
  463. package/src/lib/point-ask/compare.test.ts +145 -0
  464. package/src/lib/point-ask/compare.ts +73 -0
  465. package/src/lib/point-ask/describe-target.test.ts +165 -0
  466. package/src/lib/point-ask/describe-target.ts +234 -0
  467. package/src/lib/point-ask/dictation.ts +79 -0
  468. package/src/lib/point-ask/edge-marker.test.ts +61 -0
  469. package/src/lib/point-ask/edge-marker.ts +27 -0
  470. package/src/lib/point-ask/frame-loop.ts +25 -0
  471. package/src/lib/point-ask/history-marks.test.ts +94 -0
  472. package/src/lib/point-ask/history-marks.ts +39 -0
  473. package/src/lib/point-ask/host-api.ts +32 -0
  474. package/src/lib/point-ask/outline-box.test.ts +30 -0
  475. package/src/lib/point-ask/outline-box.ts +11 -0
  476. package/src/lib/point-ask/overlay-metrics.ts +6 -0
  477. package/src/lib/point-ask/pick.test.ts +220 -0
  478. package/src/lib/point-ask/pick.ts +121 -0
  479. package/src/lib/point-ask/pin-machine.test.ts +494 -0
  480. package/src/lib/point-ask/pin-machine.ts +217 -0
  481. package/src/lib/point-ask/placement.test.ts +203 -0
  482. package/src/lib/point-ask/placement.ts +141 -0
  483. package/src/lib/point-ask/pointer-tracker.ts +34 -0
  484. package/src/lib/point-ask/projector.ts +83 -0
  485. package/src/lib/point-ask/region.test.ts +157 -0
  486. package/src/lib/point-ask/region.ts +136 -0
  487. package/src/lib/point-ask/retry.test.ts +217 -0
  488. package/src/lib/point-ask/retry.ts +37 -0
  489. package/src/lib/point-ask/scene-commit.test.ts +312 -0
  490. package/src/lib/point-ask/scene-commit.ts +32 -0
  491. package/src/lib/point-ask/session.ts +1391 -0
  492. package/src/lib/point-ask/spring-keyframes.test.ts +45 -0
  493. package/src/lib/point-ask/spring-keyframes.ts +38 -0
  494. package/src/lib/point-ask/spring.test.ts +63 -0
  495. package/src/lib/point-ask/spring.ts +107 -0
  496. package/src/lib/point-ask/suggestions.test.ts +80 -0
  497. package/src/lib/point-ask/suggestions.ts +125 -0
  498. package/src/lib/point-ask/target-scope.test.ts +622 -0
  499. package/src/lib/point-ask/target-scope.ts +338 -0
  500. package/src/lib/point-ask/types.ts +48 -0
  501. package/src/lib/portable-export.test.ts +34 -1
  502. package/src/lib/portable-export.ts +119 -53
  503. package/src/lib/print-feature-thickness.ts +10 -1
  504. package/src/lib/print-roof-solids.test.ts +39 -1
  505. package/src/lib/print-roof-solids.ts +43 -10
  506. package/src/lib/print-shell-compiler-manifold-core.ts +1 -1
  507. package/src/lib/print-shell-compiler.ts +1 -0
  508. package/src/lib/registered-draft-snap.ts +12 -0
  509. package/src/lib/roof-duplication.ts +10 -2
  510. package/src/lib/room-assembly-overlay.test.ts +275 -0
  511. package/src/lib/room-assembly-overlay.ts +265 -0
  512. package/src/lib/room-built-on.test.ts +379 -0
  513. package/src/lib/room-built-on.ts +138 -0
  514. package/src/lib/room-construction-commands.ts +712 -0
  515. package/src/lib/room-construction.test.ts +746 -0
  516. package/src/lib/room-construction.ts +385 -0
  517. package/src/lib/room-controls-phase6.test.ts +630 -0
  518. package/src/lib/room-divide-session.ts +523 -0
  519. package/src/lib/room-handle-drag.ts +1068 -0
  520. package/src/lib/room-name-catalog.test.ts +90 -0
  521. package/src/lib/room-name-catalog.ts +168 -0
  522. package/src/lib/room-push-dimensions.test.ts +311 -0
  523. package/src/lib/room-push-dimensions.ts +251 -0
  524. package/src/lib/room-selection-commands.ts +62 -0
  525. package/src/lib/room-selection.test.ts +1017 -0
  526. package/src/lib/room-selection.ts +417 -0
  527. package/src/lib/room-structure-commands.ts +94 -0
  528. package/src/lib/room-structure.test.ts +921 -0
  529. package/src/lib/room-transform-session.ts +570 -0
  530. package/src/lib/room-zone-routing.test.ts +294 -0
  531. package/src/lib/room-zone-routing.ts +197 -0
  532. package/src/lib/scene-clipboard.test.ts +610 -1
  533. package/src/lib/scene-clipboard.ts +269 -58
  534. package/src/lib/scene-material-refs.test.ts +79 -0
  535. package/src/lib/scene-material-refs.ts +79 -0
  536. package/src/lib/scene.test.ts +24 -36
  537. package/src/lib/scene.ts +33 -57
  538. package/src/lib/selection-routing.test.ts +129 -45
  539. package/src/lib/selection-routing.ts +54 -75
  540. package/src/lib/session-writes.test.ts +120 -0
  541. package/src/lib/session-writes.ts +205 -0
  542. package/src/lib/sfx-bus.ts +13 -0
  543. package/src/lib/sfx-player.test.ts +70 -9
  544. package/src/lib/sfx-player.ts +6 -0
  545. package/src/lib/sidebar-panel.ts +62 -0
  546. package/src/lib/site-boundary.test.ts +4 -6
  547. package/src/lib/site-boundary.ts +1 -1
  548. package/src/lib/snapping-mode.test.ts +4 -0
  549. package/src/lib/snapping-mode.ts +8 -1
  550. package/src/lib/stair-levels.test.ts +25 -0
  551. package/src/lib/stair-levels.ts +22 -3
  552. package/src/lib/terrace-draft.test.ts +145 -0
  553. package/src/lib/terrace-draft.ts +214 -0
  554. package/src/lib/tool-cursor-icon.test.ts +16 -0
  555. package/src/lib/tool-cursor-icon.ts +11 -0
  556. package/src/lib/typing-target.ts +9 -0
  557. package/src/lib/units.ts +77 -39
  558. package/src/lib/vector-edges.test.ts +58 -0
  559. package/src/lib/vector-edges.ts +400 -0
  560. package/src/lib/viewer-selection.test.ts +577 -0
  561. package/src/lib/viewer-selection.ts +580 -0
  562. package/src/lib/wall-draw-variant.test.ts +71 -0
  563. package/src/lib/wall-draw-variant.ts +91 -0
  564. package/src/lib/wall-paint-region-session.ts +246 -0
  565. package/src/lib/wall-paint-region.test.ts +373 -0
  566. package/src/lib/wall-placement-pointer.test.ts +42 -0
  567. package/src/lib/wall-placement-pointer.ts +57 -0
  568. package/src/lib/wall-polygon-draft.test.ts +262 -0
  569. package/src/lib/wall-polygon-draft.ts +286 -0
  570. package/src/lib/wall-push.test.ts +453 -0
  571. package/src/lib/wall-region-face.ts +352 -0
  572. package/src/lib/wall-region-handles.test.ts +304 -0
  573. package/src/lib/wall-region-handles.ts +368 -0
  574. package/src/lib/wall-region-list.test.ts +119 -0
  575. package/src/lib/wall-region-list.ts +136 -0
  576. package/src/lib/wall-region-snap.ts +110 -0
  577. package/src/lib/window-changes.ts +35 -0
  578. package/src/lib/window-interaction.ts +14 -0
  579. package/src/lib/zone-content.ts +4 -1
  580. package/src/lib/zone-name.test.ts +17 -0
  581. package/src/lib/zone-name.ts +17 -0
  582. package/src/store/opens-in-select-mode.test.ts +223 -0
  583. package/src/store/paint-mode.test.ts +188 -0
  584. package/src/store/terrain-sculpt-mode.test.ts +19 -21
  585. package/src/store/tool-mode.test.ts +137 -6
  586. package/src/store/use-audio.test.ts +146 -0
  587. package/src/store/use-audio.tsx +127 -3
  588. package/src/store/use-construction-reveal.test.ts +173 -0
  589. package/src/store/use-construction-reveal.ts +145 -0
  590. package/src/store/use-corner-snap-hint.test.ts +24 -0
  591. package/src/store/use-corner-snap-hint.ts +47 -0
  592. package/src/store/use-delete-confirmation.ts +28 -1
  593. package/src/store/use-drawing-view.test.ts +24 -3
  594. package/src/store/use-drawing-view.ts +29 -1
  595. package/src/store/use-editor.tsx +432 -321
  596. package/src/store/use-floorplan-draft-preview.ts +13 -0
  597. package/src/store/use-floorplan-marquee.ts +12 -2
  598. package/src/store/use-follow-pascal.test.ts +62 -0
  599. package/src/store/use-follow-pascal.ts +53 -0
  600. package/src/store/use-point-ask.ts +247 -0
  601. package/src/store/use-quick-measurement-hud.ts +3 -3
  602. package/src/store/use-stair-build-preview.ts +33 -5
  603. package/src/store/use-wall-move-ghosts.ts +4 -2
  604. package/src/store/use-wall-region-handles-selection.test.ts +222 -0
  605. package/src/store/use-wall-region-handles-selection.ts +139 -0
  606. package/src/store/view-layout.test.ts +201 -0
  607. package/src/store/view-layout.ts +199 -0
  608. package/src/test-utils/immediate-animation-frames.ts +16 -0
  609. package/src/test-utils/live-gesture-round-trip.ts +224 -0
  610. package/src/test-utils/render-current-markup.tsx +16 -0
  611. package/src/test-utils/selection-harness.ts +82 -0
  612. package/src/components/editor-2d/renderers/floorplan-stair-layer.tsx +0 -499
  613. package/src/components/systems/zone/zone-label-editor-system.tsx +0 -333
  614. package/src/components/tools/fence/fence-drafting.test.ts +0 -100
  615. package/src/components/tools/fence/fence-drafting.ts +0 -266
  616. package/src/components/tools/stair/stair-click-guard.test.ts +0 -58
  617. package/src/components/tools/stair/stair-click-guard.ts +0 -72
  618. package/src/components/tools/stair/stair-defaults.ts +0 -18
  619. package/src/components/tools/stair/stair-tool.tsx +0 -634
  620. package/src/components/ui/sidebar/panels/plugins-panel.tsx +0 -218
  621. package/src/components/viewer/viewer-units-panel.tsx +0 -116
  622. package/src/lib/floorplan/stairs.ts +0 -489
  623. package/src/lib/level-duplication.test.ts +0 -144
  624. package/src/lib/level-duplication.ts +0 -195
@@ -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
@@ -310,7 +310,7 @@ export function MetricControl({
310
310
  <div
311
311
  className={cn(
312
312
  'group flex h-10 w-full items-center justify-between rounded-lg border border-border/50 px-3 text-sm transition-colors',
313
- isDragging ? 'bg-[#3e3e3e]' : 'bg-[#2C2C2E] hover:bg-[#3e3e3e]',
313
+ isDragging ? 'bg-black/8 dark:bg-[#3e3e3e]' : 'bg-black/4 dark:bg-[#2C2C2E] hover:bg-black/8 dark:hover:bg-[#3e3e3e]',
314
314
  className,
315
315
  )}
316
316
  onMouseEnter={() => setIsHovered(true)}
@@ -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">
@@ -5,7 +5,11 @@ import { cn } from '../../../lib/utils'
5
5
  interface SegmentedControlProps<T extends string> {
6
6
  value: T
7
7
  onChange: (value: T) => void
8
- options: { label: React.ReactNode; value: T }[]
8
+ /**
9
+ * `disabled` greys one option and ignores its click; it stays hoverable, so a tooltip in its
10
+ * `label` can say why.
11
+ */
12
+ options: { label: React.ReactNode; value: T; disabled?: boolean }[]
9
13
  className?: string
10
14
  disabled?: boolean
11
15
  mixed?: boolean
@@ -22,28 +26,37 @@ export function SegmentedControl<T extends string>({
22
26
  return (
23
27
  <div
24
28
  className={cn(
25
- 'flex h-9 w-full items-center rounded-lg border border-border/50 bg-[#2C2C2E] p-[3px]',
29
+ 'flex min-h-9 w-full items-stretch rounded-lg border border-border/50 bg-black/4 dark:bg-[#2C2C2E] p-[3px]',
26
30
  disabled && 'opacity-60',
27
31
  className,
28
32
  )}
29
33
  >
30
34
  {options.map((option) => {
31
35
  const isSelected = !mixed && value === option.value
36
+ const isDisabled = disabled || option.disabled === true
32
37
  return (
33
38
  <button
39
+ aria-disabled={option.disabled || undefined}
40
+ aria-pressed={isSelected}
34
41
  className={cn(
35
- 'relative flex h-full flex-1 items-center justify-center rounded-md font-medium text-xs transition-all duration-200',
42
+ 'relative flex min-h-7 min-w-0 flex-1 items-center justify-center rounded-md px-1.5 py-1 font-medium text-xs leading-4 transition-colors duration-200',
36
43
  isSelected
37
- ? 'bg-[#3e3e3e] text-foreground shadow-sm ring-1 ring-border/50'
38
- : 'text-muted-foreground hover:bg-white/5 hover:text-foreground',
39
- disabled && 'cursor-not-allowed hover:bg-transparent hover:text-muted-foreground',
44
+ ? 'bg-white dark:bg-[#3e3e3e] text-foreground shadow-sm ring-1 ring-border/50'
45
+ : 'text-muted-foreground hover:bg-black/4 dark:hover:bg-white/5 hover:text-foreground',
46
+ isDisabled &&
47
+ 'cursor-not-allowed hover:bg-transparent hover:text-muted-foreground dark:hover:bg-transparent',
48
+ option.disabled && 'opacity-40',
40
49
  )}
41
50
  disabled={disabled}
42
51
  key={option.value}
43
- onClick={() => onChange(option.value)}
52
+ onClick={() => {
53
+ if (!option.disabled) onChange(option.value)
54
+ }}
44
55
  type="button"
45
56
  >
46
- <span className="relative z-10 flex items-center gap-1.5">{option.label}</span>
57
+ <span className="relative z-10 flex min-w-0 items-center justify-center gap-1.5 whitespace-normal text-center [&>svg]:shrink-0">
58
+ {option.label}
59
+ </span>
47
60
  </button>
48
61
  )
49
62
  })}
@@ -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-black/4 dark:bg-[#2C2C2E] px-3 text-sm transition-colors hover:bg-black/8 dark:hover:bg-[#3e3e3e] data-[state=open]:bg-black/8 dark: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
+ }
@@ -329,7 +329,7 @@ export function SliderControl({
329
329
  <div
330
330
  className={cn(
331
331
  'group flex h-7 w-full select-none items-center rounded-lg px-2 transition-colors',
332
- isDragging ? 'bg-white/5' : 'hover:bg-white/5',
332
+ isDragging ? 'bg-black/4 dark:bg-white/5' : 'hover:bg-black/4 dark:hover:bg-white/5',
333
333
  className,
334
334
  )}
335
335
  onMouseEnter={() => setIsHovered(true)}
@@ -21,7 +21,7 @@ export function ToggleControl({
21
21
  return (
22
22
  <div
23
23
  className={cn(
24
- 'group flex h-10 w-full cursor-pointer items-center justify-between rounded-lg border border-border/50 bg-[#2C2C2E] px-3 text-sm transition-colors hover:bg-[#3e3e3e]',
24
+ 'group flex h-10 w-full cursor-pointer items-center justify-between rounded-lg border border-border/50 bg-black/4 dark:bg-[#2C2C2E] px-3 text-sm transition-colors hover:bg-black/8 dark:hover:bg-[#3e3e3e]',
25
25
  className,
26
26
  )}
27
27
  onClick={() => onChange(!checked)}
@@ -34,10 +34,10 @@ export function ToggleControl({
34
34
  className={cn(
35
35
  'flex h-5 w-5 items-center justify-center rounded-[4px] border transition-all duration-200',
36
36
  mixed
37
- ? 'border-muted-foreground/70 bg-black/20 text-muted-foreground'
37
+ ? 'border-muted-foreground/70 bg-black/5 dark:bg-black/20 text-muted-foreground'
38
38
  : checked
39
39
  ? 'border-primary bg-primary text-primary-foreground'
40
- : 'border-border bg-black/20 text-transparent group-hover:border-muted-foreground',
40
+ : 'border-border bg-black/5 dark:bg-black/20 text-transparent group-hover:border-muted-foreground',
41
41
  )}
42
42
  >
43
43
  {mixed ? (
@@ -50,7 +50,7 @@ function ToolOptionRow({
50
50
  'rounded-lg px-2 py-2 text-center font-medium text-xs transition-colors',
51
51
  selected
52
52
  ? 'bg-primary/10 text-primary ring-1 ring-primary/50'
53
- : 'bg-muted/40 text-muted-foreground hover:bg-muted hover:text-foreground',
53
+ : 'bg-black/4 text-muted-foreground hover:bg-black/8 hover:text-foreground dark:bg-muted/40 dark:hover:bg-muted',
54
54
  )}
55
55
  key={choice.value}
56
56
  onClick={() => {