@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
@@ -1,10 +1,16 @@
1
1
  import {
2
2
  type AnyNode,
3
3
  type AnyNodeId,
4
+ floorStepRole,
4
5
  generateSceneMaterialId,
6
+ getWallLevelZones,
7
+ getWallLocalFaceZ,
8
+ getWallZoneSpans,
5
9
  type ItemNode,
6
10
  type MaterialSchema,
7
11
  nodeRegistry,
12
+ parseFloorStepRole,
13
+ parseRoomFinishRole,
8
14
  pointInPolygon2D,
9
15
  resolveLevelId,
10
16
  type SceneMaterial,
@@ -15,6 +21,7 @@ import {
15
21
  toSceneMaterialRef,
16
22
  useScene,
17
23
  type WallNode,
24
+ wallRoomFinishRole,
18
25
  } from '@pascal-app/core'
19
26
 
20
27
  /**
@@ -36,6 +43,8 @@ export type PaintHoverInfo = {
36
43
  slotLabel: string
37
44
  /** Kind noun for the `'object'` label (e.g. "Whole shelf"). */
38
45
  nodeNoun: string
46
+ /** Labels a surface names itself, per scope, over the generic ones. */
47
+ labels?: Partial<Record<PaintScope, string>>
39
48
  }
40
49
 
41
50
  function nodeHasAsset(node: AnyNode): boolean {
@@ -46,13 +55,55 @@ function nodeOffersRoomScope(node: AnyNode): boolean {
46
55
  return nodeRegistry.get(node.type)?.capabilities?.paint?.roomScope === true
47
56
  }
48
57
 
58
+ const isFloorPlate = (node: AnyNode): boolean => node.type === 'slab' && node.boundary === 'auto'
59
+
60
+ const FOOTPRINT_FACE_LABELS: Record<string, string> = {
61
+ edge: 'Floor edge · all around this floor',
62
+ riser: 'Riser · all around this floor',
63
+ underside: 'Underside · all of this floor',
64
+ foundation: 'Foundation · all around this floor',
65
+ }
66
+
67
+ /**
68
+ * The scopes a generated floor plate offers for the surface under the cursor.
69
+ * A plate is not an object anyone paints whole: a room's floor is "this
70
+ * surface" (the painted part under the cursor, else the room floor) or the
71
+ * whole room; a step is its doorway or every step of the room; the footprint's
72
+ * faces are one finish all around the floor, so they have nothing to cycle.
73
+ */
74
+ export function platePaintScopes(
75
+ node: AnyNode,
76
+ role: string,
77
+ ): { scopes: PaintScope[]; labels: Partial<Record<PaintScope, string>> } | null {
78
+ if (node.type !== 'slab' || node.boundary !== 'auto') return null
79
+ if (parseFloorStepRole(role))
80
+ return {
81
+ scopes: ['single', 'room'],
82
+ labels: { single: 'This step', room: 'All steps in this room' },
83
+ }
84
+ if (parseRoomFinishRole(role))
85
+ return { scopes: ['single', 'room'], labels: { single: 'This surface', room: 'Whole room' } }
86
+ const fixed = node.plateRole === 'base' ? FOOTPRINT_FACE_LABELS[role] : undefined
87
+ return { scopes: ['single'], labels: fixed ? { single: fixed } : {} }
88
+ }
89
+
49
90
  /**
50
91
  * The scopes a hovered node offers, derived from the node itself: every node
51
92
  * paints `single`; > 1 slot adds `object`; an `asset` adds `matching`; a
52
93
  * `roomScope`-declaring kind adds `room`. `slotRoles` is the node's full slot set
53
- * (declared or mesh-derived), passed in by the caller.
94
+ * (declared or mesh-derived), passed in by the caller. A floor plate's set
95
+ * depends on the surface under the cursor (`role`).
54
96
  */
55
- export function availablePaintScopes(args: { node: AnyNode; slotRoles: string[] }): PaintScope[] {
97
+ export function availablePaintScopes(args: {
98
+ node: AnyNode
99
+ slotRoles: string[]
100
+ role?: string
101
+ }): PaintScope[] {
102
+ // A wall paints the face under the cursor, or its room's walls: "whole wall"
103
+ // (both faces of one wall, across two rooms) is nobody's surface.
104
+ if (args.node.type === 'wall') return ['single', 'room']
105
+ const plate = args.role === undefined ? null : platePaintScopes(args.node, args.role)
106
+ if (plate) return plate.scopes
56
107
  const scopes: PaintScope[] = ['single']
57
108
  if (args.slotRoles.length > 1) scopes.push('object')
58
109
  if (nodeHasAsset(args.node)) scopes.push('matching')
@@ -66,7 +117,24 @@ export function cyclePaintScope(scope: PaintScope, scopes: PaintScope[]): PaintS
66
117
  return list[(index + 1) % list.length] ?? 'single'
67
118
  }
68
119
 
120
+ /** The chip's hover info for `role` on `node`: its scopes and what each is called. */
121
+ export function paintHoverInfo(node: AnyNode, role: string, slotRoles: string[]): PaintHoverInfo {
122
+ return {
123
+ scopes: availablePaintScopes({ node, slotRoles, role }),
124
+ slotLabel: paintSurfaceLabel(node, role),
125
+ nodeNoun: node.type,
126
+ labels: platePaintScopes(node, role)?.labels,
127
+ }
128
+ }
129
+
130
+ /** The scope a click applies: the chosen one when the hovered surface offers it. */
131
+ export function effectivePaintScope(scope: PaintScope, scopes: PaintScope[]): PaintScope {
132
+ return scopes.includes(scope) ? scope : 'single'
133
+ }
134
+
69
135
  export function paintScopeLabel(scope: PaintScope, info: PaintHoverInfo): string {
136
+ const own = info.labels?.[scope]
137
+ if (own) return own
70
138
  switch (scope) {
71
139
  case 'object':
72
140
  return `Whole ${info.nodeNoun}`
@@ -91,13 +159,21 @@ export function nodeSlotRoles(node: AnyNode, meshSlotRoles: (node: AnyNode) => s
91
159
  return meshSlotRoles(node)
92
160
  }
93
161
 
162
+ /**
163
+ * What the paint HUD calls the surface under the cursor: a wall face is the
164
+ * "Face" (the narrow scope), a trim its own name, anything else its slot.
165
+ */
166
+ export function paintSurfaceLabel(node: AnyNode, role: string): string {
167
+ if (node.type === 'wall' && !WALL_TRIM_ROLE.test(role)) return 'Face'
168
+ return slotDisplayLabel(node, role)
169
+ }
170
+
94
171
  /** Display label for the hovered slot — declared label wins, else derived from the id. */
95
172
  export function slotDisplayLabel(node: AnyNode, role: string): string {
96
- const declared = nodeRegistry
97
- .get(node.type)
98
- ?.capabilities?.slots?.(node)
99
- ?.find((slot) => slot.slotId === role)
100
- return declared?.label ?? slotLabelFromId(role)
173
+ const capabilities = nodeRegistry.get(node.type)?.capabilities
174
+ const declared = capabilities?.slots?.(node)?.find((slot) => slot.slotId === role)
175
+ if (declared) return declared.label
176
+ return capabilities?.paint?.roleLabel?.(node, role) ?? slotLabelFromId(role)
101
177
  }
102
178
 
103
179
  // ── Fan-out resolution ──────────────────────────────────────────────────────
@@ -141,17 +217,87 @@ function distanceToPolyline(
141
217
  return distance
142
218
  }
143
219
 
144
- function wallRoleForRoomFace(role: string, wall: WallNode, face: 'front' | 'back'): string | null {
145
- const semantic = face === 'front' ? wall.frontSide : wall.backSide
146
- const fallback = face === 'front' ? 'interior' : 'exterior'
147
- const side = semantic === 'interior' || semantic === 'exterior' ? semantic : fallback
220
+ const WALL_TRIM_ROLE = /^([ab])(Skirting|Crown|ChairRail)$/
221
+ const WALL_ROOM_FACE_ROLE = /^room:(.+)\/([ab])$/
222
+
223
+ /** The same surface on a boundary face: face slots and trims follow the physical face. */
224
+ function wallRoleForRoomFace(role: string, face: 'front' | 'back'): string | null {
225
+ const physical = face === 'front' ? 'a' : 'b'
226
+ if (role === 'a' || role === 'b') return physical
227
+ const trim = WALL_TRIM_ROLE.exec(role)
228
+ return trim ? `${physical}${trim[2]}` : null
229
+ }
230
+
231
+ /** The face a wall paint role sits on, when the role names one. */
232
+ function wallRoleFace(role: string): 'a' | 'b' | null {
233
+ if (role === 'a' || role === 'b') return role
234
+ return (WALL_TRIM_ROLE.exec(role)?.[1] ?? WALL_ROOM_FACE_ROLE.exec(role)?.[2] ?? null) as
235
+ | 'a'
236
+ | 'b'
237
+ | null
238
+ }
239
+
240
+ /** The room (zone) a wall face borders at the hit point, from the zones' boundary spans. */
241
+ function zoneAtWallHit(
242
+ wall: WallNode,
243
+ face: 'a' | 'b',
244
+ point: readonly [number, number],
245
+ nodes: Record<string, AnyNode>,
246
+ ): string | null {
247
+ const dx = wall.end[0] - wall.start[0]
248
+ const dz = wall.end[1] - wall.start[1]
249
+ const lengthSquared = dx * dx + dz * dz
250
+ if (lengthSquared < 1e-12) return null
251
+ const t = ((point[0] - wall.start[0]) * dx + (point[1] - wall.start[1]) * dz) / lengthSquared
252
+ const spans = getWallZoneSpans(wall, getWallLevelZones(wall, nodes))
253
+ const span = spans.find(
254
+ (candidate) =>
255
+ candidate.face === face &&
256
+ t >= candidate.t0 - 1e-6 &&
257
+ (t < candidate.t1 || (candidate.t1 >= 1 && t <= 1 + 1e-6)),
258
+ )
259
+ return span?.zoneId ?? null
260
+ }
148
261
 
149
- if (role === 'interior' || role === 'exterior') return side
150
- if (role.endsWith('Interior'))
151
- return `${role.slice(0, -'Interior'.length)}${side === 'interior' ? 'Interior' : 'Exterior'}`
152
- if (role.endsWith('Exterior'))
153
- return `${role.slice(0, -'Exterior'.length)}${side === 'interior' ? 'Interior' : 'Exterior'}`
154
- return null
262
+ /**
263
+ * Room scope on a wall face inside a room paints the room's wall finish: one
264
+ * `room:<zoneId>` commit on the zone. Every wall bordering the room is listed so
265
+ * each previews the change; trims fan out to the face each wall turns to the room.
266
+ */
267
+ function wallRoomTargets(
268
+ wall: WallNode,
269
+ role: string,
270
+ zoneId: string,
271
+ nodes: Record<string, AnyNode>,
272
+ ): Array<{ nodeId: AnyNodeId; role: string }> {
273
+ const zone = nodes[zoneId]
274
+ if (zone?.type !== 'zone') return []
275
+ const walls = [
276
+ wall,
277
+ ...zone.boundaryWallIds
278
+ .map((id) => nodes[id])
279
+ .filter((node): node is WallNode => node?.type === 'wall' && node.id !== wall.id),
280
+ ]
281
+ const trim = WALL_TRIM_ROLE.exec(role)
282
+ if (trim) {
283
+ const targets = new Map<string, { nodeId: AnyNodeId; role: string }>()
284
+ for (const target of walls) {
285
+ for (const span of getWallZoneSpans(target, [zone])) {
286
+ const targetRole = `${span.face}${trim[2]}`
287
+ targets.set(`${target.id}:${targetRole}`, {
288
+ nodeId: target.id as AnyNodeId,
289
+ role: targetRole,
290
+ })
291
+ }
292
+ }
293
+ return [...targets.values()]
294
+ }
295
+ const roomRole = wallRoomFinishRole(zoneId)
296
+ const targets = walls.map((target) => ({ nodeId: target.id as AnyNodeId, role: roomRole }))
297
+ // A room bounded by one wall still commits through the fan-out path, which
298
+ // routes the room role to the zone; the zone target itself previews nothing.
299
+ if (targets.length === 1) targets.push({ nodeId: zone.id as AnyNodeId, role: roomRole })
300
+ return targets
155
301
  }
156
302
 
157
303
  function resolveWallPaintSpace(args: {
@@ -162,7 +308,13 @@ function resolveWallPaintSpace(args: {
162
308
  }): Space | null {
163
309
  const { wall, wallHit, nodes, spaces } = args
164
310
  const levelId = wall.parentId ?? resolveLevelId(wall, nodes)
165
- const tolerance = (wall.thickness ?? 0.2) / 2 + 0.08
311
+ const tolerance =
312
+ getWallLocalFaceZ(
313
+ { ...wall, thickness: wall.thickness ?? 0.2 },
314
+ wallHit.face === 'front' ? 'a' : 'b',
315
+ ) *
316
+ (wallHit.face === 'front' ? 1 : -1) +
317
+ 0.08
166
318
  let best: { space: Space; distance: number } | null = null
167
319
 
168
320
  for (const space of Object.values(spaces)) {
@@ -185,6 +337,7 @@ function boundaryPointKey(point: readonly [number, number]): string {
185
337
  function boundarySegmentKey(boundary: WallBoundaryFace): string {
186
338
  const forward = boundary.points.map(boundaryPointKey).join('|')
187
339
  const reverse = [...boundary.points].reverse().map(boundaryPointKey).join('|')
340
+ // biome-ignore lint/complexity/useMathMinMax: lexicographic string comparison
188
341
  return `${boundary.wallId}:${forward < reverse ? forward : reverse}`
189
342
  }
190
343
 
@@ -216,7 +369,13 @@ function connectedExteriorBoundaries(args: {
216
369
  if (!boundary) return []
217
370
  return [{ ...boundary, face: oppositeWallFace(boundary.face) }]
218
371
  })
219
- const tolerance = (wall.thickness ?? 0.2) / 2 + 0.08
372
+ const tolerance =
373
+ getWallLocalFaceZ(
374
+ { ...wall, thickness: wall.thickness ?? 0.2 },
375
+ wallHit.face === 'front' ? 'a' : 'b',
376
+ ) *
377
+ (wallHit.face === 'front' ? 1 : -1) +
378
+ 0.08
220
379
  const seed = exterior
221
380
  .filter((boundary) => boundary.wallId === wall.id && boundary.face === wallHit.face)
222
381
  .map((boundary) => ({ boundary, distance: distanceToPolyline(wallHit.point, boundary.points) }))
@@ -277,7 +436,7 @@ function wallTargetsForBoundaries(args: {
277
436
  ) {
278
437
  continue
279
438
  }
280
- const targetRole = wallRoleForRoomFace(role, targetWall, boundary.face)
439
+ const targetRole = wallRoleForRoomFace(role, boundary.face)
281
440
  if (!targetRole) continue
282
441
  const key = `${targetWall.id}:${targetRole}`
283
442
  targets.set(key, { nodeId: targetWall.id as AnyNodeId, role: targetRole })
@@ -298,6 +457,20 @@ function polygonCentroid(
298
457
  return [x / points.length, z / points.length]
299
458
  }
300
459
 
460
+ /**
461
+ * The role one paint click writes under `scope`: a floor step paints its own
462
+ * doorway, and in the room scope every step of its room (`step:<zoneId>`); a
463
+ * room's floor in the room scope is the whole room (`room:<zoneId>/*`).
464
+ * Hover outline, preview and commit all start from this role.
465
+ */
466
+ export function paintScopeRole(node: AnyNode, role: string, scope: PaintScope): string {
467
+ if (node.type !== 'slab' || scope !== 'room') return role
468
+ const step = parseFloorStepRole(role)
469
+ if (step) return floorStepRole(step.zoneId)
470
+ const floor = isFloorPlate(node) ? parseRoomFinishRole(role) : null
471
+ return floor ? `room:${floor.zoneId}/*` : role
472
+ }
473
+
301
474
  /**
302
475
  * Expand one paint hit (`node` + resolved `role`) into the full list of
303
476
  * (node, role) targets the current `scope` should paint. Returns just the
@@ -318,8 +491,21 @@ export function resolvePaintScopeTargets(args: {
318
491
  slotRolesOf: (node: AnyNode) => string[]
319
492
  wallHit?: WallPaintHit
320
493
  }): Array<{ nodeId: AnyNodeId; role: string }> {
321
- const { node, role, scope, nodes, spaces, slotRolesOf, wallHit } = args
494
+ const { node, role, nodes, spaces, slotRolesOf, wallHit } = args
495
+ const plateScopes = platePaintScopes(node, role)?.scopes
496
+ const scope = plateScopes ? effectivePaintScope(args.scope, plateScopes) : args.scope
322
497
  const single = [{ nodeId: node.id as AnyNodeId, role }]
498
+ if (node.type === 'slab' && parseFloorStepRole(role)) {
499
+ // A room's steps are drawn by whichever plates carry them.
500
+ const stepRole = paintScopeRole(node, role, scope)
501
+ const targets = Object.values(nodes)
502
+ .filter(
503
+ (other) =>
504
+ other.type === 'slab' && other.parentId === node.parentId && other.boundary === 'auto',
505
+ )
506
+ .map((other) => ({ nodeId: other.id, role: stepRole }))
507
+ return targets.length ? targets : [{ nodeId: node.id as AnyNodeId, role: stepRole }]
508
+ }
323
509
  if (scope === 'single') return single
324
510
 
325
511
  // Whole object: paint every slot of the clicked node. Generic across any
@@ -344,6 +530,13 @@ export function resolvePaintScopeTargets(args: {
344
530
  if (!wallHit) return single
345
531
  const levelId = wall.parentId ?? resolveLevelId(wall, nodes)
346
532
  if (!levelId) return single
533
+ const face = wallRoleFace(role) ?? (wallHit.face === 'front' ? 'a' : 'b')
534
+ const zoneId =
535
+ WALL_ROOM_FACE_ROLE.exec(role)?.[1] ?? zoneAtWallHit(wall, face, wallHit.point, nodes)
536
+ if (zoneId) {
537
+ const targets = wallRoomTargets(wall, role, zoneId, nodes)
538
+ if (targets.length > 0) return targets
539
+ }
347
540
  const space = resolveWallPaintSpace({ wall, wallHit, nodes, spaces })
348
541
  const boundaries = space
349
542
  ? space.boundaryFaces
@@ -353,6 +546,24 @@ export function resolvePaintScopeTargets(args: {
353
546
  }
354
547
 
355
548
  if (node.type === 'slab' && scope === 'room') {
549
+ // A room's floor in the room scope: every finish source of that room (its
550
+ // floor, painted parts, steps and edge) on every plate of the level, as one
551
+ // `room:<zoneId>/*` role the zone commits.
552
+ const floor = parseRoomFinishRole(role)
553
+ if (floor) {
554
+ const roomWide = `room:${floor.zoneId}/*`
555
+ const plates = Object.values(nodes).filter(
556
+ (other) =>
557
+ other.type === 'slab' && other.parentId === node.parentId && other.boundary === 'auto',
558
+ )
559
+ return (plates.length ? plates : [node]).map((plate) => ({
560
+ nodeId: plate.id as AnyNodeId,
561
+ role: roomWide,
562
+ }))
563
+ }
564
+ // A floor plate's own faces have no room scope; below is the hand-drawn
565
+ // slab's spread over the slabs of its room.
566
+ if (isFloorPlate(node)) return single
356
567
  const centroid = polygonCentroid((node as SlabNode).polygon)
357
568
  if (!centroid) return single
358
569
  // Space polygons are per-level footprints, and stacked storeys share a
@@ -386,7 +597,7 @@ function materialsEqual(a: unknown, b: unknown): boolean {
386
597
  if (Object.is(a, b)) return true
387
598
  if (typeof a !== typeof b || a === null || b === null) return false
388
599
  if (Array.isArray(a) || Array.isArray(b)) {
389
- if (!Array.isArray(a) || !Array.isArray(b) || a.length !== b.length) return false
600
+ if (!(Array.isArray(a) && Array.isArray(b)) || a.length !== b.length) return false
390
601
  return a.every((value, index) => materialsEqual(value, b[index]))
391
602
  }
392
603
  if (typeof a === 'object') {
@@ -401,18 +612,49 @@ function materialsEqual(a: unknown, b: unknown): boolean {
401
612
  return false
402
613
  }
403
614
 
615
+ /**
616
+ * Split a fan-out into slot writes and roles a kind routes elsewhere — a wall's
617
+ * `room:<zoneId>` paints the zone, through the wall's own commit, once per role.
618
+ * Targets without a paint capability (the zone listed for a one-wall room) only
619
+ * ride along for the preview.
620
+ */
621
+ function partitionFanout(targets: ReadonlyArray<{ nodeId: AnyNodeId; role: string }>) {
622
+ const nodes = useScene.getState().nodes
623
+ const routed = new Map<string, { nodeId: AnyNodeId; role: string }>()
624
+ const slots: Array<{ nodeId: AnyNodeId; role: string }> = []
625
+ for (const target of targets) {
626
+ const node = nodes[target.nodeId]
627
+ if (!node) continue
628
+ const capabilities = nodeRegistry.get(node.type)?.capabilities
629
+ const declared = capabilities?.slots?.(node)
630
+ const isSlot = declared?.length
631
+ ? declared.some((slot) => slot.slotId === target.role)
632
+ : !target.role.includes(':')
633
+ const key = target.role.includes(':') ? target.role : `${target.nodeId}:${target.role}`
634
+ if (isSlot) slots.push(target)
635
+ else if (capabilities?.paint?.commit && !routed.has(key)) routed.set(key, target)
636
+ }
637
+ return { routed: [...routed.values()], slots }
638
+ }
639
+
404
640
  /**
405
641
  * Apply one paint to many slot-model targets in a single undo step. Resolves
406
642
  * the slot ref ONCE — a one-off colour creates a single shared scene material
407
643
  * for the whole fan-out, not one per node — then writes every `node.slots[role]`
408
- * (or deletes it, for the eraser) in one `useScene.setState`. Only ever called
409
- * for item / wall / slab fan-outs, all of which use the unified slot model.
644
+ * (or deletes it, for the eraser) in one `useScene.setState`. Routed roles
645
+ * (see `partitionFanout`) commit through their kind instead.
410
646
  */
411
647
  export function commitPaintScopeFanout(
412
- targets: ReadonlyArray<{ nodeId: AnyNodeId; role: string }>,
648
+ fanout: ReadonlyArray<{ nodeId: AnyNodeId; role: string }>,
413
649
  material: MaterialSchema | undefined,
414
650
  materialPreset: string | undefined,
415
651
  ): void {
652
+ const { routed, slots: targets } = partitionFanout(fanout)
653
+ for (const { nodeId, role } of routed) {
654
+ const node = useScene.getState().nodes[nodeId]
655
+ const paint = node ? nodeRegistry.get(node.type)?.capabilities?.paint : undefined
656
+ if (node && paint?.commit) paint.commit({ node, role, material, materialPreset })
657
+ }
416
658
  if (targets.length === 0) return
417
659
  const state = useScene.getState()
418
660
 
@@ -62,12 +62,33 @@ export type PanelRow = { section?: string } & (
62
62
 
63
63
  export const PANEL_MODEL_EXTENSION = 'pascal:editor/panel-model'
64
64
 
65
+ /**
66
+ * A control the multi-selection inspector renders for every selected node of
67
+ * a kind at once (derived settings the raw parametrics can't express). `value`
68
+ * null means the selection is mixed: no option shows as active.
69
+ */
70
+ export type MultiPanelControl = {
71
+ id: string
72
+ kind: 'segmented'
73
+ /** The parametrics group title the control sits in. */
74
+ section: string
75
+ label: string
76
+ options: ReadonlyArray<{ value: string; label: string }>
77
+ value: string | null
78
+ /** Applies to every selected node in one undo step. */
79
+ onChange: (value: string) => void
80
+ }
81
+
65
82
  export type NodePanelModel<N extends AnyNode = AnyNode> = {
66
83
  rows: (context: {
67
84
  node: N
68
85
  nodes: Record<AnyNodeId, AnyNode>
69
86
  update: (patch: Partial<N>) => void
70
87
  }) => PanelRow[]
88
+ multiControls?: (context: {
89
+ selection: N[]
90
+ nodes: Record<AnyNodeId, AnyNode>
91
+ }) => MultiPanelControl[]
71
92
  }
72
93
 
73
94
  export function getNodePanelModel(definition: AnyNodeDefinition | undefined) {
@@ -6,13 +6,14 @@ import {
6
6
  useScene,
7
7
  } from '@pascal-app/core'
8
8
 
9
- export function commitParametricNodeFields(
9
+ /** A field edit with what its kind's parametrics derive and reconcile from it, as updates. */
10
+ export function parametricNodeUpdates(
10
11
  nodeId: AnyNodeId,
11
12
  requestedPatch: Partial<AnyNode>,
12
- ): void {
13
- const scene = useScene.getState()
14
- const node = scene.nodes[nodeId]
15
- if (!node) return
13
+ ): { id: AnyNodeId; data: Partial<AnyNode> }[] {
14
+ const nodes = useScene.getState().nodes
15
+ const node = nodes[nodeId]
16
+ if (!node) return []
16
17
 
17
18
  const parametrics = nodeRegistry.get(node.type)?.parametrics as
18
19
  | ParametricDescriptor<AnyNode>
@@ -31,7 +32,15 @@ export function commitParametricNodeFields(
31
32
  ]
32
33
  if (parametrics?.reconcile) {
33
34
  const next = { ...node, ...patch } as AnyNode
34
- updates.push(...parametrics.reconcile(node, next))
35
+ updates.push(...parametrics.reconcile(node, next, { ...nodes, [nodeId]: next }))
35
36
  }
36
- scene.updateNodes(updates)
37
+ return updates
38
+ }
39
+
40
+ export function commitParametricNodeFields(
41
+ nodeId: AnyNodeId,
42
+ requestedPatch: Partial<AnyNode>,
43
+ ): void {
44
+ const updates = parametricNodeUpdates(nodeId, requestedPatch)
45
+ if (updates.length) useScene.getState().updateNodes(updates)
37
46
  }