@pascal-app/editor 1.0.1 → 1.0.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (498) hide show
  1. package/package.json +16 -9
  2. package/src/components/editor/action-menu-button.tsx +83 -0
  3. package/src/components/editor/bake-thumbnail.tsx +315 -4
  4. package/src/components/editor/capture-level-picker.tsx +72 -0
  5. package/src/components/editor/custom-camera-controls.tsx +221 -128
  6. package/src/components/editor/delete-confirmation-dialog.tsx +297 -7
  7. package/src/components/editor/duplicate-node.ts +244 -0
  8. package/src/components/editor/editor-layout-mobile.tsx +37 -25
  9. package/src/components/editor/editor-layout-v2.tsx +131 -58
  10. package/src/components/editor/export-manager.tsx +34 -8
  11. package/src/components/editor/first-person/build-collider-world.test.ts +51 -0
  12. package/src/components/editor/first-person/build-collider-world.ts +6 -2
  13. package/src/components/editor/first-person/mechanism-targets.test.ts +75 -0
  14. package/src/components/editor/first-person/mechanism-targets.ts +36 -0
  15. package/src/components/editor/first-person-controls.tsx +205 -2
  16. package/src/components/editor/floating-action-menu.tsx +63 -207
  17. package/src/components/editor/floor-aware-camera-controls.test.ts +442 -0
  18. package/src/components/editor/floor-aware-camera-controls.ts +324 -0
  19. package/src/components/editor/floor-openings-overlay.tsx +243 -0
  20. package/src/components/editor/floor-region-controls.tsx +846 -0
  21. package/src/components/editor/floor-region-webgpu-materials.test.ts +12 -0
  22. package/src/components/editor/floor-surface.test.ts +165 -0
  23. package/src/components/editor/floor-surface.ts +182 -0
  24. package/src/components/editor/floorplan-background-selection.test.ts +11 -2
  25. package/src/components/editor/floorplan-background-selection.ts +1 -3
  26. package/src/components/editor/floorplan-panel.tsx +536 -487
  27. package/src/components/editor/footprint-height-handle.tsx +204 -0
  28. package/src/components/editor/grid.tsx +31 -7
  29. package/src/components/editor/group-actions.ts +59 -11
  30. package/src/components/editor/group-floating-action-menu.tsx +19 -9
  31. package/src/components/editor/group-move-3d.ts +4 -6
  32. package/src/components/editor/group-rotate-handle.tsx +81 -35
  33. package/src/components/editor/group-selection-box-3d.tsx +25 -17
  34. package/src/components/editor/group-transform-shared.test.ts +126 -1
  35. package/src/components/editor/group-transform-shared.ts +79 -73
  36. package/src/components/editor/handles/handle-arrow.tsx +72 -2
  37. package/src/components/editor/handles/handle-hit-priority.test.tsx +63 -0
  38. package/src/components/editor/handles/handle-hit-priority.tsx +37 -0
  39. package/src/components/editor/handles/swallow-next-click.test.ts +28 -0
  40. package/src/components/editor/handles/use-handle-drag.ts +272 -131
  41. package/src/components/editor/index.tsx +316 -176
  42. package/src/components/editor/join-walls-pill.tsx +44 -0
  43. package/src/components/editor/node-action-menu.tsx +70 -70
  44. package/src/components/editor/node-arrow-handles.tsx +167 -14
  45. package/src/components/editor/open-wall-ends-3d-layer.tsx +273 -0
  46. package/src/components/editor/open-wall-ends-hint.tsx +89 -0
  47. package/src/components/editor/quick-measurement-hud.tsx +3 -2
  48. package/src/components/editor/registry-action-contributions.test.tsx +63 -0
  49. package/src/components/editor/registry-action-contributions.tsx +132 -0
  50. package/src/components/editor/room-controls.tsx +847 -0
  51. package/src/components/editor/room-floor-highlight.tsx +128 -0
  52. package/src/components/editor/room-handles-2d.tsx +354 -0
  53. package/src/components/editor/room-handles.tsx +675 -0
  54. package/src/components/editor/room-highlight.tsx +225 -0
  55. package/src/components/editor/room-labels.test.ts +357 -0
  56. package/src/components/editor/room-labels.tsx +990 -0
  57. package/src/components/editor/room-transform-ghost.tsx +256 -0
  58. package/src/components/editor/room-webgpu-materials.test.ts +27 -0
  59. package/src/components/editor/selection-manager.tsx +451 -317
  60. package/src/components/editor/slab-hole-highlights.tsx +3 -0
  61. package/src/components/editor/snapshot-capture-overlay.tsx +4 -0
  62. package/src/components/editor/snapshot-capture.test.ts +52 -4
  63. package/src/components/editor/snapshot-capture.ts +30 -5
  64. package/src/components/editor/thumbnail-generator.test.ts +308 -0
  65. package/src/components/editor/thumbnail-generator.tsx +1035 -229
  66. package/src/components/editor/use-floorplan-background-placement.ts +31 -11
  67. package/src/components/editor/vector-edge-extractor.tsx +121 -0
  68. package/src/components/editor/views/host-view.tsx +61 -0
  69. package/src/components/editor/views/plugins-view.tsx +670 -0
  70. package/src/components/editor/views/scene-region.tsx +59 -0
  71. package/src/components/editor/views/use-editor-views.ts +57 -0
  72. package/src/components/editor/views/view-bar.tsx +220 -0
  73. package/src/components/editor/wall-justification.test.ts +140 -0
  74. package/src/components/editor/wall-move-side-handles.tsx +114 -163
  75. package/src/components/editor/wall-opening-highlights.tsx +2 -3
  76. package/src/components/editor/wall-push-arrow.tsx +92 -0
  77. package/src/components/editor/wall-region-handles.tsx +509 -0
  78. package/src/components/editor/wall-snap-beacon-layer.tsx +3 -0
  79. package/src/components/editor-2d/floorplan-cursor-indicator-overlay.tsx +8 -4
  80. package/src/components/editor-2d/floorplan-group-move.tsx +17 -24
  81. package/src/components/editor-2d/floorplan-open-wall-ends-layer.test.tsx +86 -0
  82. package/src/components/editor-2d/floorplan-open-wall-ends-layer.tsx +290 -0
  83. package/src/components/editor-2d/floorplan-registered-tool-layer.tsx +33 -12
  84. package/src/components/editor-2d/floorplan-registry-action-menu.tsx +11 -1
  85. package/src/components/editor-2d/floorplan-registry-move-overlay.tsx +242 -126
  86. package/src/components/editor-2d/renderers/floorplan-dimension-edit-overlay.test.ts +156 -0
  87. package/src/components/editor-2d/renderers/floorplan-dimension-edit-overlay.tsx +278 -0
  88. package/src/components/editor-2d/renderers/floorplan-dimension-renderer.tsx +77 -0
  89. package/src/components/editor-2d/renderers/floorplan-draft-measurement.tsx +166 -0
  90. package/src/components/editor-2d/renderers/floorplan-geometry-renderer.tsx +4 -1
  91. package/src/components/editor-2d/renderers/floorplan-registry-layer.test.ts +87 -8
  92. package/src/components/editor-2d/renderers/floorplan-registry-layer.tsx +355 -245
  93. package/src/components/editor-2d/svg-paths.ts +12 -15
  94. package/src/components/procedural-items/inspector.tsx +40 -15
  95. package/src/components/systems/ceiling/ceiling-selection-affordance-system.test.ts +97 -14
  96. package/src/components/systems/ceiling/ceiling-selection-affordance-system.tsx +18 -18
  97. package/src/components/systems/zone/zone-system.test.ts +47 -0
  98. package/src/components/systems/zone/zone-system.tsx +31 -66
  99. package/src/components/tools/__fixtures__/hosting.ts +10 -0
  100. package/src/components/tools/item/item-surface-placement.test.ts +3 -9
  101. package/src/components/tools/item/item-surface-strategy-resolver.test.ts +3 -9
  102. package/src/components/tools/item/placement-history.test.tsx +474 -0
  103. package/src/components/tools/item/placement-strategies.ts +63 -47
  104. package/src/components/tools/item/shelf-surface-strategy-resolver.test.ts +3 -3
  105. package/src/components/tools/item/use-draft-node.test.tsx +432 -1
  106. package/src/components/tools/item/use-draft-node.ts +207 -97
  107. package/src/components/tools/item/use-placement-coordinator.tsx +251 -111
  108. package/src/components/tools/paint-region/use-paint-tint.ts +37 -0
  109. package/src/components/tools/paint-region/wall-paint-region-tool.tsx +466 -0
  110. package/src/components/tools/registry/item-surface-move.test.ts +10 -2
  111. package/src/components/tools/registry/item-surface-move.ts +46 -24
  112. package/src/components/tools/registry/item-surface-resolver.test.ts +2 -2
  113. package/src/components/tools/registry/move-registry-node-tool.tsx +26 -10
  114. package/src/components/tools/select/box-select-tool.tsx +2 -12
  115. package/src/components/tools/select/plane-box-select-tool.test.tsx +79 -0
  116. package/src/components/tools/select/plane-box-select-tool.tsx +35 -22
  117. package/src/components/tools/select/select-candidates.test.ts +37 -4
  118. package/src/components/tools/select/select-candidates.ts +15 -11
  119. package/src/components/tools/shared/affordance-dispatch.test.ts +24 -0
  120. package/src/components/tools/shared/affordance-dispatch.ts +13 -0
  121. package/src/components/tools/shared/cursor-sphere.tsx +36 -12
  122. package/src/components/tools/shared/draft-measurement-label.tsx +38 -0
  123. package/src/components/tools/shared/pointer-support-cap.test.ts +27 -0
  124. package/src/components/tools/shared/pointer-support-cap.ts +17 -5
  125. package/src/components/tools/shared/surface-rejection.test.tsx +5 -4
  126. package/src/components/tools/shared/surface-rejection.tsx +14 -0
  127. package/src/components/tools/site/site-boundary-editor.tsx +1 -2
  128. package/src/components/tools/stair/stair-defaults.ts +0 -3
  129. package/src/components/tools/stair/stair-tool.tsx +90 -42
  130. package/src/components/tools/tool-manager.tsx +82 -23
  131. package/src/components/tools/wall/wall-drafting.test.ts +19 -4
  132. package/src/components/tools/zone/zone-tool.tsx +4 -12
  133. package/src/components/ui/action-menu/action-button.tsx +3 -0
  134. package/src/components/ui/action-menu/camera-actions.tsx +2 -1
  135. package/src/components/ui/action-menu/control-modes.tsx +2 -35
  136. package/src/components/ui/action-menu/measurement-control.tsx +3 -10
  137. package/src/components/ui/build-panel/build-panel.tsx +235 -0
  138. package/src/components/ui/command-palette/editor-commands.tsx +4 -30
  139. package/src/components/ui/command-palette/index.tsx +5 -24
  140. package/src/components/ui/controls/finish-swatch.tsx +110 -0
  141. package/src/components/ui/controls/material-paint-panel.tsx +101 -91
  142. package/src/components/ui/controls/material-picker.tsx +8 -67
  143. package/src/components/ui/controls/scene-material-list.tsx +4 -32
  144. package/src/components/ui/controls/select-control.tsx +65 -0
  145. package/src/components/ui/controls/slider-control.tsx +76 -31
  146. package/src/components/ui/floating-level-selector.tsx +9 -14
  147. package/src/components/ui/helpers/building-helper.tsx +4 -1
  148. package/src/components/ui/helpers/contextual-helper-panel.tsx +154 -70
  149. package/src/components/ui/helpers/helper-manager.test.tsx +69 -0
  150. package/src/components/ui/helpers/helper-manager.tsx +348 -19
  151. package/src/components/ui/helpers/item-helper.tsx +8 -0
  152. package/src/components/ui/helpers/registered-tool-helper.tsx +10 -33
  153. package/src/components/ui/icon-ref.tsx +45 -1
  154. package/src/components/ui/item-catalog/item-catalog.tsx +5 -30
  155. package/src/components/ui/level-duplicate-dialog.tsx +1 -1
  156. package/src/components/ui/node-icon-image.tsx +16 -0
  157. package/src/components/ui/panels/collections/collections-popover.tsx +20 -14
  158. package/src/components/ui/panels/floor-foundation-panel.tsx +551 -0
  159. package/src/components/ui/panels/floor-opening-panel.tsx +143 -0
  160. package/src/components/ui/panels/mobile-panel-sheet.tsx +2 -2
  161. package/src/components/ui/panels/mobile-selection-bar.tsx +2 -2
  162. package/src/components/ui/panels/multi-field-value.test.ts +26 -3
  163. package/src/components/ui/panels/multi-field-value.ts +6 -3
  164. package/src/components/ui/panels/multi-height-mode.tsx +27 -21
  165. package/src/components/ui/panels/multi-parametric-inspector.tsx +52 -2
  166. package/src/components/ui/panels/multi-selection-panel.tsx +43 -4
  167. package/src/components/ui/panels/node-display.ts +8 -3
  168. package/src/components/ui/panels/panel-manager.tsx +103 -68
  169. package/src/components/ui/panels/panel-wrapper.tsx +78 -36
  170. package/src/components/ui/panels/parametric-field-control.tsx +1 -1
  171. package/src/components/ui/panels/parametric-inspector.tsx +7 -19
  172. package/src/components/ui/panels/reference-panel.tsx +2 -3
  173. package/src/components/ui/panels/room-construction-rows.tsx +774 -0
  174. package/src/components/ui/panels/room-name-options.test.ts +55 -0
  175. package/src/components/ui/panels/room-name-options.ts +47 -0
  176. package/src/components/ui/panels/room-openings-field.tsx +110 -0
  177. package/src/components/ui/panels/room-panel.tsx +725 -0
  178. package/src/components/ui/panels/shape-choice.tsx +58 -0
  179. package/src/components/ui/panels/wall-region-list.tsx +109 -0
  180. package/src/components/ui/primitives/color-dot.tsx +36 -24
  181. package/src/components/ui/primitives/shortcut-token.tsx +4 -0
  182. package/src/components/ui/primitives/sidebar.tsx +2 -0
  183. package/src/components/ui/right-stack.test.ts +29 -0
  184. package/src/components/ui/right-stack.tsx +145 -0
  185. package/src/components/ui/sidebar/icon-rail.tsx +5 -0
  186. package/src/components/ui/sidebar/panels/settings-panel/audio-settings-dialog.tsx +2 -2
  187. package/src/components/ui/sidebar/panels/settings-panel/index.tsx +615 -372
  188. package/src/components/ui/sidebar/panels/settings-panel/keyboard-shortcuts-dialog.tsx +7 -7
  189. package/src/components/ui/sidebar/panels/settings-panel/print-export-button.tsx +1 -1
  190. package/src/components/ui/sidebar/panels/site-panel/ceiling-tree-node.tsx +4 -4
  191. package/src/components/ui/sidebar/panels/site-panel/chimney-tree-node.tsx +1 -5
  192. package/src/components/ui/sidebar/panels/site-panel/collection-tree-node.tsx +197 -0
  193. package/src/components/ui/sidebar/panels/site-panel/column-tree-node.tsx +1 -5
  194. package/src/components/ui/sidebar/panels/site-panel/door-tree-node.tsx +1 -5
  195. package/src/components/ui/sidebar/panels/site-panel/dormer-tree-node.tsx +1 -5
  196. package/src/components/ui/sidebar/panels/site-panel/elevator-tree-node.tsx +1 -5
  197. package/src/components/ui/sidebar/panels/site-panel/fence-tree-node.tsx +24 -11
  198. package/src/components/ui/sidebar/panels/site-panel/gutter-tree-node.tsx +1 -5
  199. package/src/components/ui/sidebar/panels/site-panel/index.tsx +176 -211
  200. package/src/components/ui/sidebar/panels/site-panel/inline-rename-input.tsx +30 -8
  201. package/src/components/ui/sidebar/panels/site-panel/item-tree-node.tsx +1 -3
  202. package/src/components/ui/sidebar/panels/site-panel/level-tree-node.tsx +4 -1
  203. package/src/components/ui/sidebar/panels/site-panel/registry-tree-node.tsx +1 -3
  204. package/src/components/ui/sidebar/panels/site-panel/roof-tree-node.tsx +1 -4
  205. package/src/components/ui/sidebar/panels/site-panel/shelf-tree-node.tsx +1 -3
  206. package/src/components/ui/sidebar/panels/site-panel/slab-tree-node.tsx +21 -13
  207. package/src/components/ui/sidebar/panels/site-panel/solar-panel-tree-node.tsx +1 -5
  208. package/src/components/ui/sidebar/panels/site-panel/spawn-tree-node.tsx +1 -5
  209. package/src/components/ui/sidebar/panels/site-panel/stair-tree-node.tsx +1 -5
  210. package/src/components/ui/sidebar/panels/site-panel/tree-node.tsx +0 -27
  211. package/src/components/ui/sidebar/panels/site-panel/unit-tree-node.tsx +2 -3
  212. package/src/components/ui/sidebar/panels/site-panel/wall-tree-node.tsx +1 -5
  213. package/src/components/ui/sidebar/panels/site-panel/window-tree-node.tsx +1 -5
  214. package/src/components/ui/sidebar/panels/site-panel/zone-tree-node.tsx +9 -4
  215. package/src/components/ui/sidebar/panels/zone-panel/index.tsx +0 -2
  216. package/src/components/ui/sidebar/tab-bar.tsx +10 -0
  217. package/src/components/ui/sidebar/use-plugin-panels.tsx +15 -5
  218. package/src/components/viewer/floorplan-compass-button.tsx +9 -1
  219. package/src/components/viewer/floorplan-preview-visibility.test.ts +35 -0
  220. package/src/components/viewer/floorplan-preview-visibility.ts +19 -0
  221. package/src/components/viewer/floorplan-preview.tsx +6 -18
  222. package/src/components/viewer/viewer-hover-label.tsx +34 -0
  223. package/src/components/viewer/viewer-rooms.tsx +175 -0
  224. package/src/components/viewer/viewer-scene-header.tsx +181 -145
  225. package/src/components/viewer/viewer-selection-manager.tsx +124 -0
  226. package/src/components/viewer/viewer-stage.tsx +2 -2
  227. package/src/components/viewer-zone-system.tsx +9 -9
  228. package/src/hooks/use-auto-save.integration.test.tsx +311 -0
  229. package/src/hooks/use-auto-save.ts +55 -14
  230. package/src/hooks/use-ceiling-events.test.ts +1 -0
  231. package/src/hooks/use-ceiling-events.ts +2 -0
  232. package/src/hooks/use-grid-events.test.ts +2 -0
  233. package/src/hooks/use-grid-events.ts +16 -11
  234. package/src/hooks/use-keyboard.test.ts +182 -0
  235. package/src/hooks/use-keyboard.ts +122 -91
  236. package/src/hooks/use-open-wall-ends.test.ts +99 -0
  237. package/src/hooks/use-open-wall-ends.ts +238 -0
  238. package/src/hooks/use-placement-notice.test.ts +48 -0
  239. package/src/hooks/use-placement-notice.ts +42 -0
  240. package/src/hooks/use-selected-room.ts +143 -0
  241. package/src/index.tsx +306 -39
  242. package/src/lib/catalog-panel-model.ts +39 -0
  243. package/src/lib/catalog-selection.test.ts +27 -0
  244. package/src/lib/catalog-selection.ts +8 -0
  245. package/src/lib/ceiling-edit-session.test.ts +321 -0
  246. package/src/lib/ceiling-edit-session.ts +206 -0
  247. package/src/lib/ceiling-region-session.test.ts +179 -0
  248. package/src/lib/contextual-help.test.ts +27 -1
  249. package/src/lib/contextual-help.ts +16 -9
  250. package/src/lib/continuation.test.ts +19 -1
  251. package/src/lib/continuation.ts +25 -4
  252. package/src/lib/door-interaction.ts +14 -0
  253. package/src/lib/editor-level-display.test.ts +96 -0
  254. package/src/lib/editor-level-display.ts +48 -0
  255. package/src/lib/editor-views.ts +144 -0
  256. package/src/lib/export-geometry-wait.test.ts +90 -0
  257. package/src/lib/floor-edit-session.ts +153 -0
  258. package/src/lib/floor-footprints.test.ts +563 -0
  259. package/src/lib/floor-footprints.ts +395 -0
  260. package/src/lib/floor-foundation-support.test.ts +283 -0
  261. package/src/lib/floor-opening-draft.ts +218 -0
  262. package/src/lib/floor-openings.test.ts +216 -0
  263. package/src/lib/floor-region-geometry.ts +191 -0
  264. package/src/lib/floor-region-session.test.ts +485 -0
  265. package/src/lib/floor-region-session.ts +589 -0
  266. package/src/lib/floor-region-snap.ts +101 -0
  267. package/src/lib/floor-step-hover.test.ts +32 -0
  268. package/src/lib/floorplan/annotation-visibility.ts +17 -0
  269. package/src/lib/floorplan/dimension-drive.test.ts +360 -0
  270. package/src/lib/floorplan/dimension-drive.ts +479 -0
  271. package/src/lib/floorplan/floorplan-export.test.ts +15 -9
  272. package/src/lib/floorplan/floorplan-export.tsx +32 -8
  273. package/src/lib/floorplan/floorplan-extension.ts +48 -0
  274. package/src/lib/floorplan/floorplan-mode.test.ts +4 -0
  275. package/src/lib/floorplan/floorplan-mode.ts +2 -0
  276. package/src/lib/floorplan/floorplan-pdfkit-document.ts +339 -18
  277. package/src/lib/floorplan/floorplan-pdfkit-renderer.test.ts +138 -8
  278. package/src/lib/floorplan/floorplan-pdfkit-renderer.ts +56 -25
  279. package/src/lib/floorplan/floorplan-plugin-install.test.tsx +138 -0
  280. package/src/lib/floorplan/floorplan-readonly.ts +6 -0
  281. package/src/lib/floorplan/geometry.ts +13 -6
  282. package/src/lib/floorplan/index.ts +0 -9
  283. package/src/lib/floorplan/items.ts +5 -2
  284. package/src/lib/floorplan/marks.test.ts +213 -0
  285. package/src/lib/floorplan/marks.ts +238 -0
  286. package/src/lib/floorplan/open-wall-ends.test.ts +268 -0
  287. package/src/lib/floorplan/open-wall-ends.ts +162 -0
  288. package/src/lib/floorplan/pdf-fonts/GeistMono-Bold.ttf +0 -0
  289. package/src/lib/floorplan/pdf-fonts/GeistMono-Regular.ttf +0 -0
  290. package/src/lib/floorplan/pdf-fonts/LICENSE-GeistMono.txt +92 -0
  291. package/src/lib/floorplan/pdf-fonts/LICENSE-LiberationSans.txt +102 -0
  292. package/src/lib/floorplan/pdf-fonts/LiberationSans-Bold.ttf +0 -0
  293. package/src/lib/floorplan/pdf-fonts/LiberationSans-Regular.ttf +0 -0
  294. package/src/lib/floorplan/schedules.test.ts +189 -0
  295. package/src/lib/floorplan/schedules.ts +339 -0
  296. package/src/lib/floorplan/sheet-export.test.ts +100 -0
  297. package/src/lib/floorplan/sheet-export.ts +318 -0
  298. package/src/lib/floorplan/site-plan/build-site-plan-drawing.ts +779 -0
  299. package/src/lib/floorplan/site-plan/contours.test.ts +43 -0
  300. package/src/lib/floorplan/site-plan/contributors.ts +97 -0
  301. package/src/lib/floorplan/site-plan/coverage.ts +232 -0
  302. package/src/lib/floorplan/site-plan/drawing-type-switch.test.ts +15 -0
  303. package/src/lib/floorplan/site-plan/drawing-type-switch.tsx +86 -0
  304. package/src/lib/floorplan/site-plan/front-edge.test.ts +103 -0
  305. package/src/lib/floorplan/site-plan/front-edge.ts +156 -0
  306. package/src/lib/floorplan/site-plan/geometry.test.ts +275 -0
  307. package/src/lib/floorplan/site-plan/geometry.ts +557 -0
  308. package/src/lib/floorplan/site-plan/index.ts +71 -0
  309. package/src/lib/floorplan/site-plan/site-annotations.test.ts +366 -0
  310. package/src/lib/floorplan/site-plan/site-annotations.ts +1083 -0
  311. package/src/lib/floorplan/site-plan/site-parts.ts +379 -0
  312. package/src/lib/floorplan/site-plan/site-plan-layer.tsx +138 -0
  313. package/src/lib/floorplan/site-plan/street-name.test.ts +69 -0
  314. package/src/lib/floorplan/site-plan/street-name.ts +302 -0
  315. package/src/lib/floorplan/site-plan/yards.test.ts +69 -0
  316. package/src/lib/floorplan/types.ts +1 -27
  317. package/src/lib/fresh-planar-placement.test.ts +36 -0
  318. package/src/lib/fresh-planar-placement.ts +191 -36
  319. package/src/lib/geometry-script/author.ts +152 -0
  320. package/src/lib/geometry-script/client.ts +152 -0
  321. package/src/lib/geometry-script/protocol.ts +12 -0
  322. package/src/lib/gesture-lifecycle.test.ts +595 -0
  323. package/src/lib/gesture-lifecycle.ts +290 -0
  324. package/src/lib/glb-export-zone-holes.test.ts +41 -0
  325. package/src/lib/glb-export.test.ts +544 -16
  326. package/src/lib/glb-export.ts +256 -125
  327. package/src/lib/grid-frame.test.ts +19 -0
  328. package/src/lib/grid-frame.ts +18 -0
  329. package/src/lib/history.test.ts +6 -0
  330. package/src/lib/history.ts +7 -0
  331. package/src/lib/hud-title.ts +109 -0
  332. package/src/lib/hydration-gated-storage.ts +49 -0
  333. package/src/lib/ifc-export.test.ts +174 -0
  334. package/src/lib/ifc-export.ts +201 -0
  335. package/src/lib/ifc-parts.ts +15 -0
  336. package/src/lib/inspector-expanded.test.ts +44 -0
  337. package/src/lib/inspector-expanded.ts +36 -0
  338. package/src/lib/interaction/scope.ts +27 -1
  339. package/src/lib/keyboard-pan.test.ts +184 -0
  340. package/src/lib/keyboard-pan.ts +238 -0
  341. package/src/lib/live-gesture-round-trip.test.ts +360 -0
  342. package/src/lib/lot/__fixtures__/map-dossier-miami-shores.json +1 -0
  343. package/src/lib/lot/__fixtures__/map-dossier-stpete.json +1 -0
  344. package/src/lib/lot/clean-ring.test.ts +186 -0
  345. package/src/lib/lot/clean-ring.ts +217 -0
  346. package/src/lib/lot/dossier.test.ts +202 -0
  347. package/src/lib/lot/dossier.ts +498 -0
  348. package/src/lib/lot/drop-in.test.ts +326 -0
  349. package/src/lib/lot/drop-in.ts +407 -0
  350. package/src/lib/lot/index.ts +43 -0
  351. package/src/lib/lot/lot-patch.test.ts +340 -0
  352. package/src/lib/lot/lot-patch.ts +394 -0
  353. package/src/lib/lot/parcel-provider.ts +43 -0
  354. package/src/lib/lot/terrain.test.ts +161 -0
  355. package/src/lib/lot/terrain.ts +266 -0
  356. package/src/lib/material-catalog-model.ts +74 -0
  357. package/src/lib/material-paint-panel-model.ts +80 -0
  358. package/src/lib/material-paint.ts +4 -0
  359. package/src/lib/mezzanine-actions.test.ts +342 -0
  360. package/src/lib/mezzanine-draft.test.ts +501 -0
  361. package/src/lib/mezzanine-draft.ts +278 -0
  362. package/src/lib/mezzanine-messages.ts +26 -0
  363. package/src/lib/mezzanine-selection.test.ts +207 -0
  364. package/src/lib/mezzanine-stairs.ts +46 -0
  365. package/src/lib/model-export.ts +3 -1
  366. package/src/lib/node-action-movement.ts +17 -6
  367. package/src/lib/node-display-label.ts +37 -0
  368. package/src/lib/orbit-floor-clearance.test.ts +165 -0
  369. package/src/lib/orbit-floor-clearance.ts +106 -0
  370. package/src/lib/overlay-transform.test.ts +28 -0
  371. package/src/lib/overlay-transform.ts +12 -0
  372. package/src/lib/paint-commit-route.test.ts +37 -0
  373. package/src/lib/paint-commit-route.ts +17 -0
  374. package/src/lib/paint-eyedropper.test.tsx +165 -0
  375. package/src/lib/paint-eyedropper.ts +99 -0
  376. package/src/lib/paint-pass-through.test.ts +111 -0
  377. package/src/lib/paint-pass-through.ts +76 -0
  378. package/src/lib/paint-region-hover.ts +25 -0
  379. package/src/lib/paint-region-mode.ts +154 -0
  380. package/src/lib/paint-regions.test.ts +166 -0
  381. package/src/lib/paint-regions.ts +192 -0
  382. package/src/lib/paint-scope.test.ts +317 -35
  383. package/src/lib/paint-scope.ts +266 -25
  384. package/src/lib/panel-rows.ts +96 -0
  385. package/src/lib/panel-tool-options.test.ts +34 -0
  386. package/src/lib/panel-tool-options.ts +46 -0
  387. package/src/lib/parametric-node-update.ts +37 -0
  388. package/src/lib/plate-paint-affected.test.ts +300 -0
  389. package/src/lib/plate-paint-affected.ts +177 -0
  390. package/src/lib/plugin-chat.ts +50 -0
  391. package/src/lib/plugin-panels.test.ts +25 -0
  392. package/src/lib/plugin-panels.ts +94 -0
  393. package/src/lib/print-shell-compiler.ts +1 -0
  394. package/src/lib/registered-draft-snap.ts +12 -0
  395. package/src/lib/roof-duplication.ts +10 -2
  396. package/src/lib/room-assembly-overlay.test.ts +275 -0
  397. package/src/lib/room-assembly-overlay.ts +266 -0
  398. package/src/lib/room-built-on.test.ts +379 -0
  399. package/src/lib/room-built-on.ts +138 -0
  400. package/src/lib/room-construction-commands.ts +712 -0
  401. package/src/lib/room-construction.test.ts +746 -0
  402. package/src/lib/room-construction.ts +385 -0
  403. package/src/lib/room-controls-phase6.test.ts +630 -0
  404. package/src/lib/room-divide-session.ts +523 -0
  405. package/src/lib/room-handle-drag.ts +1068 -0
  406. package/src/lib/room-name-catalog.test.ts +90 -0
  407. package/src/lib/room-name-catalog.ts +168 -0
  408. package/src/lib/room-push-dimensions.test.ts +311 -0
  409. package/src/lib/room-push-dimensions.ts +251 -0
  410. package/src/lib/room-selection-commands.ts +62 -0
  411. package/src/lib/room-selection.test.ts +973 -0
  412. package/src/lib/room-selection.ts +417 -0
  413. package/src/lib/room-structure-commands.ts +94 -0
  414. package/src/lib/room-structure.test.ts +921 -0
  415. package/src/lib/room-transform-session.ts +570 -0
  416. package/src/lib/room-zone-routing.test.ts +294 -0
  417. package/src/lib/room-zone-routing.ts +197 -0
  418. package/src/lib/scene-clipboard.test.ts +364 -0
  419. package/src/lib/scene-clipboard.ts +229 -48
  420. package/src/lib/scene-material-refs.test.ts +79 -0
  421. package/src/lib/scene-material-refs.ts +79 -0
  422. package/src/lib/scene.test.ts +87 -0
  423. package/src/lib/scene.ts +39 -45
  424. package/src/lib/selection-routing.test.ts +129 -45
  425. package/src/lib/selection-routing.ts +53 -74
  426. package/src/lib/session-writes.test.ts +120 -0
  427. package/src/lib/session-writes.ts +205 -0
  428. package/src/lib/sidebar-panel.ts +62 -0
  429. package/src/lib/site-boundary.test.ts +4 -6
  430. package/src/lib/site-boundary.ts +1 -1
  431. package/src/lib/snapping-mode.test.ts +17 -2
  432. package/src/lib/snapping-mode.ts +19 -7
  433. package/src/lib/spatial-drag-plane.test.ts +53 -0
  434. package/src/lib/spatial-drag-plane.ts +19 -0
  435. package/src/lib/spatial-pointer-input.test.ts +72 -0
  436. package/src/lib/spatial-pointer-input.ts +62 -0
  437. package/src/lib/spatial-snap.test.ts +47 -0
  438. package/src/lib/terrace-draft.test.ts +145 -0
  439. package/src/lib/terrace-draft.ts +213 -0
  440. package/src/lib/terrain-panel-model.ts +117 -0
  441. package/src/lib/tool-cursor-icon.test.ts +16 -0
  442. package/src/lib/tool-cursor-icon.ts +11 -0
  443. package/src/lib/typing-target.ts +9 -0
  444. package/src/lib/units.ts +77 -39
  445. package/src/lib/vector-edges.test.ts +58 -0
  446. package/src/lib/vector-edges.ts +400 -0
  447. package/src/lib/viewer-selection.test.ts +577 -0
  448. package/src/lib/viewer-selection.ts +569 -0
  449. package/src/lib/wall-draw-variant.test.ts +71 -0
  450. package/src/lib/wall-draw-variant.ts +91 -0
  451. package/src/lib/wall-paint-region-session.ts +246 -0
  452. package/src/lib/wall-paint-region.test.ts +373 -0
  453. package/src/lib/wall-polygon-draft.test.ts +262 -0
  454. package/src/lib/wall-polygon-draft.ts +286 -0
  455. package/src/lib/wall-push.test.ts +453 -0
  456. package/src/lib/wall-region-face.ts +352 -0
  457. package/src/lib/wall-region-handles.test.ts +304 -0
  458. package/src/lib/wall-region-handles.ts +368 -0
  459. package/src/lib/wall-region-list.test.ts +119 -0
  460. package/src/lib/wall-region-list.ts +136 -0
  461. package/src/lib/wall-region-snap.ts +110 -0
  462. package/src/lib/window-interaction.ts +14 -0
  463. package/src/lib/world-grid-snap.test.ts +48 -0
  464. package/src/lib/world-grid-snap.ts +9 -5
  465. package/src/lib/zone-content.ts +4 -1
  466. package/src/lib/zone-name.test.ts +17 -0
  467. package/src/lib/zone-name.ts +17 -0
  468. package/src/store/opens-in-select-mode.test.ts +223 -0
  469. package/src/store/paint-mode.test.ts +188 -0
  470. package/src/store/rotation-snapping.test.ts +53 -0
  471. package/src/store/terrain-sculpt-mode.test.ts +19 -21
  472. package/src/store/tool-mode.test.ts +137 -6
  473. package/src/store/use-delete-confirmation.ts +28 -1
  474. package/src/store/use-drawing-view.test.ts +24 -3
  475. package/src/store/use-drawing-view.ts +29 -1
  476. package/src/store/use-editor.tsx +442 -323
  477. package/src/store/use-floorplan-draft-preview.ts +23 -1
  478. package/src/store/use-floorplan-marquee.ts +12 -2
  479. package/src/store/use-interaction-scope.ts +155 -6
  480. package/src/store/use-quick-measurement-hud.ts +3 -3
  481. package/src/store/use-stair-build-preview.ts +13 -5
  482. package/src/store/use-wall-move-ghosts.ts +4 -2
  483. package/src/store/use-wall-region-handles-selection.test.ts +222 -0
  484. package/src/store/use-wall-region-handles-selection.ts +139 -0
  485. package/src/store/view-layout.test.ts +175 -0
  486. package/src/store/view-layout.ts +196 -0
  487. package/src/test-utils/immediate-animation-frames.ts +16 -0
  488. package/src/test-utils/live-gesture-round-trip.ts +224 -0
  489. package/src/test-utils/selection-harness.ts +82 -0
  490. package/src/components/editor-2d/renderers/floorplan-stair-layer.tsx +0 -499
  491. package/src/components/systems/zone/zone-label-editor-system.tsx +0 -333
  492. package/src/components/tools/fence/fence-drafting.test.ts +0 -100
  493. package/src/components/tools/fence/fence-drafting.ts +0 -266
  494. package/src/components/ui/sidebar/panels/plugins-panel.tsx +0 -218
  495. package/src/components/viewer/viewer-units-panel.tsx +0 -116
  496. package/src/lib/floorplan/stairs.ts +0 -489
  497. package/src/lib/level-duplication.test.ts +0 -144
  498. package/src/lib/level-duplication.ts +0 -195
@@ -0,0 +1,300 @@
1
+ import { describe, expect, test } from 'bun:test'
2
+ import {
3
+ type AnyNode,
4
+ DoorNode,
5
+ LevelNode,
6
+ reconcileSceneStructure,
7
+ SlabNode,
8
+ WallNode,
9
+ ZoneNode,
10
+ } from '@pascal-app/core'
11
+ import { paintScopeRole, resolvePaintScopeTargets } from './paint-scope'
12
+ import {
13
+ mergePaintSurfaces,
14
+ paintSurfaceMeshes,
15
+ plateAffectedSurfaces,
16
+ platePreviewSurfaces,
17
+ } from './plate-paint-affected'
18
+
19
+ const square = (x: number): [number, number][] => [
20
+ [x, 0],
21
+ [x + 4, 0],
22
+ [x + 4, 4],
23
+ [x, 4],
24
+ ]
25
+ const level = LevelNode.parse({ id: 'level_paint' })
26
+ const kitchen = ZoneNode.parse({
27
+ id: 'zone_kitchen',
28
+ name: 'Kitchen',
29
+ parentId: level.id,
30
+ spaceRole: 'room',
31
+ polygon: square(0),
32
+ })
33
+ const hall = ZoneNode.parse({
34
+ id: 'zone_hall',
35
+ name: 'Hall',
36
+ parentId: level.id,
37
+ spaceRole: 'room',
38
+ polygon: square(4),
39
+ floorStepFinish: 'library:oak',
40
+ floorEdgeFinish: 'library:stone',
41
+ })
42
+ const base = SlabNode.parse({
43
+ id: 'slab_base',
44
+ parentId: level.id,
45
+ boundary: 'auto',
46
+ plateRole: 'base',
47
+ polygon: [
48
+ [0, 0],
49
+ [8, 0],
50
+ [8, 4],
51
+ [0, 4],
52
+ ],
53
+ zoneIds: [kitchen.id, hall.id],
54
+ })
55
+ const platform = SlabNode.parse({
56
+ id: 'slab_platform',
57
+ parentId: level.id,
58
+ boundary: 'auto',
59
+ plateRole: 'platform',
60
+ polygon: square(4),
61
+ zoneIds: [hall.id],
62
+ })
63
+ const manual = SlabNode.parse({ id: 'slab_manual', parentId: level.id, polygon: square(20) })
64
+ const nodes = Object.fromEntries(
65
+ [level, kitchen, hall, base, platform, manual].map((n) => [n.id, n]),
66
+ ) as Record<string, AnyNode>
67
+
68
+ const keys = (surfaces: ReturnType<typeof plateAffectedSurfaces>) =>
69
+ (surfaces ?? []).map((s) => `${s.nodeId}:${s.role}`).sort()
70
+
71
+ describe('what one paint click changes on the floor plates (point 7)', () => {
72
+ test('a room’s steps are one finish on every plate of the level', () => {
73
+ expect(keys(plateAffectedSurfaces(nodes, base, 'step:zone_kitchen'))).toEqual([
74
+ 'slab_base:step:zone_kitchen',
75
+ 'slab_platform:step:zone_kitchen',
76
+ ])
77
+ })
78
+
79
+ test('a room floor with painted steps or a region paint stands alone', () => {
80
+ expect(keys(plateAffectedSurfaces(nodes, platform, 'room:zone_hall'))).toEqual([
81
+ 'slab_base:room:zone_hall',
82
+ 'slab_platform:room:zone_hall',
83
+ ])
84
+ expect(keys(plateAffectedSurfaces(nodes, base, 'room:zone_kitchen/region_1'))).toEqual([
85
+ 'slab_base:room:zone_kitchen/region_1',
86
+ 'slab_platform:room:zone_kitchen/region_1',
87
+ ])
88
+ })
89
+
90
+ test('the footprint edge band carries the edges of its rooms that have no edge finish', () => {
91
+ expect(keys(plateAffectedSurfaces(nodes, base, 'edge'))).toEqual([
92
+ 'slab_base:edge',
93
+ 'slab_base:edge:zone_kitchen',
94
+ 'slab_platform:edge:zone_kitchen',
95
+ ])
96
+ })
97
+
98
+ test('other plate faces are just themselves; a hand-drawn slab keeps its own path', () => {
99
+ expect(keys(plateAffectedSurfaces(nodes, base, 'riser'))).toEqual(['slab_base:riser'])
100
+ expect(plateAffectedSurfaces(nodes, manual, 'surface')).toBeNull()
101
+ })
102
+ })
103
+
104
+ /** A | B | C in a row, a door in each dividing wall, B raised: one step per door. */
105
+ function doorwayRow(): Record<string, AnyNode> {
106
+ const ring = (x0: number, x1: number): [number, number][] => [
107
+ [x0, 0],
108
+ [x1, 0],
109
+ [x1, 4],
110
+ [x0, 4],
111
+ ]
112
+ const outline = ring(0, 12)
113
+ const walls = outline.map((start, i) =>
114
+ WallNode.parse({ parentId: 'level_row', start, end: outline[(i + 1) % 4], thickness: 0.2 }),
115
+ )
116
+ const doors = ['door_ab', 'door_bc'].map((id) =>
117
+ DoorNode.parse({
118
+ id,
119
+ parentId: `wall_${id}`,
120
+ wallId: `wall_${id}`,
121
+ width: 1,
122
+ height: 2,
123
+ position: [2, 1, 0],
124
+ }),
125
+ )
126
+ const dividers = [4, 8].map((x, i) =>
127
+ WallNode.parse({
128
+ id: `wall_${doors[i]!.id}`,
129
+ parentId: 'level_row',
130
+ start: [x, 0],
131
+ end: [x, 4],
132
+ thickness: 0.2,
133
+ children: [doors[i]!.id],
134
+ }),
135
+ )
136
+ const zones = ['a', 'b', 'c'].map((name, i) =>
137
+ ZoneNode.parse({
138
+ id: `zone_${name}`,
139
+ parentId: 'level_row',
140
+ name,
141
+ spaceRole: 'room',
142
+ enclosureStatus: 'enclosed',
143
+ polygon: ring(i * 4, (i + 1) * 4),
144
+ floor: { elevation: i === 1 ? 0.3 : 0.05 },
145
+ }),
146
+ )
147
+ const children = [...walls, ...dividers, ...zones]
148
+ const level = LevelNode.parse({ id: 'level_row', height: 3, children: children.map((n) => n.id) })
149
+ let minted = 0
150
+ return reconcileSceneStructure({
151
+ nodes: Object.fromEntries([level, ...children, ...doors].map((n) => [n.id, n])),
152
+ mintId: (kind) => `${kind}_row${++minted}`,
153
+ }).nodes as Record<string, AnyNode>
154
+ }
155
+
156
+ // B is raised between A and C, a door on each side: B owns one step per door.
157
+ describe('doorway steps: hover, preview and commit are the same set', () => {
158
+ const row = doorwayRow()
159
+ const plates = Object.values(row).filter(
160
+ (node): node is SlabNode => node.type === 'slab' && node.boundary === 'auto',
161
+ )
162
+ const plate = plates.find((slab) => slab.plateRole === 'platform')!
163
+ const onPlates = (...roles: string[]) =>
164
+ plates.flatMap((slab) => roles.map((role) => `${slab.id}:${role}`)).sort()
165
+ type Obj = {
166
+ userData: Record<string, unknown>
167
+ isMesh?: boolean
168
+ traverse: (visit: (o: Obj) => void) => void
169
+ }
170
+ const mesh = (paintRole: string): Obj => ({
171
+ isMesh: true,
172
+ userData: { paintRole, __fromGeometry: true },
173
+ traverse(visit) {
174
+ visit(this)
175
+ },
176
+ })
177
+ const floorB = mesh('room:zone_b')
178
+ const stepAB = mesh('step:zone_b/door_ab')
179
+ const stepBC = mesh('step:zone_b/door_bc')
180
+ const root: Obj = {
181
+ userData: {},
182
+ traverse(visit) {
183
+ for (const child of [floorB, stepAB, stepBC]) visit(child)
184
+ },
185
+ }
186
+ const outlined = (surfaces: ReturnType<typeof plateAffectedSurfaces>) =>
187
+ paintSurfaceMeshes(surfaces!, (id) => (id === plate.id ? root : null))
188
+ const targets = (role: string, scope: 'single' | 'room') =>
189
+ resolvePaintScopeTargets({
190
+ node: plate,
191
+ role,
192
+ scope,
193
+ nodes: row,
194
+ spaces: {},
195
+ slotRolesOf: () => [],
196
+ })
197
+ .map((target) => `${target.nodeId}:${target.role}`)
198
+ .sort()
199
+
200
+ test('one step: that doorway only, on every plate', () => {
201
+ const role = 'step:zone_b/door_bc'
202
+ expect(paintScopeRole(plate, role, 'single')).toBe(role)
203
+ expect(targets(role, 'single')).toEqual(onPlates(role))
204
+ const affected = plateAffectedSurfaces(row, plate, role)
205
+ expect(keys(affected)).toEqual(onPlates(role))
206
+ expect(outlined(affected)).toEqual([stepBC])
207
+ })
208
+
209
+ test('the room scope: every step of the room', () => {
210
+ const role = paintScopeRole(plate, 'step:zone_b/door_bc', 'room')
211
+ expect(role).toBe('step:zone_b')
212
+ expect(targets('step:zone_b/door_bc', 'room')).toEqual(onPlates(role))
213
+ expect(outlined(plateAffectedSurfaces(row, plate, role))).toEqual([stepAB, stepBC])
214
+ })
215
+
216
+ test('a floor paint carries only the steps that follow the floor', () => {
217
+ expect(keys(plateAffectedSurfaces(row, plate, 'room:zone_b'))).toEqual(
218
+ onPlates('room:zone_b', 'step:zone_b/door_ab', 'step:zone_b/door_bc'),
219
+ )
220
+ const painted = {
221
+ ...row,
222
+ zone_b: { ...row.zone_b!, floorStepOverrides: [{ key: 'door_bc', finish: 'library:x' }] },
223
+ } as Record<string, AnyNode>
224
+ const affected = plateAffectedSurfaces(painted, plate, 'room:zone_b')
225
+ expect(keys(affected)).toEqual(onPlates('room:zone_b', 'step:zone_b/door_ab'))
226
+ expect(outlined(affected)).toEqual([floorB, stepAB])
227
+ // The preview is the click targets plus the fallbacks, once each.
228
+ expect(mergePaintSurfaces([{ nodeId: plate.id, role: 'room:zone_b' }], affected)).toHaveLength(
229
+ affected!.length,
230
+ )
231
+ })
232
+ })
233
+
234
+ describe('the whole room: what it paints, erases, and where it previews', () => {
235
+ const row = doorwayRow()
236
+ const plates = Object.values(row).filter(
237
+ (node): node is SlabNode => node.type === 'slab' && node.boundary === 'auto',
238
+ )
239
+ const plate = plates.find((slab) => slab.plateRole === 'platform')!
240
+ const onPlates = (...roles: string[]) =>
241
+ plates.flatMap((slab) => roles.map((role) => `${slab.id}:${role}`)).sort()
242
+ const zoneB = row.zone_b as ZoneNode
243
+ const withB = (patch: Partial<ZoneNode>) =>
244
+ ({ ...row, zone_b: { ...zoneB, ...patch } }) as Record<string, AnyNode>
245
+
246
+ test('painted, the whole room is its floor, its painted parts and the steps that follow it', () => {
247
+ const rug = withB({
248
+ floor: {
249
+ ...zoneB.floor,
250
+ regions: [{ id: 'rug', polygon: square(4), finish: 'library:red' }],
251
+ },
252
+ } as Partial<ZoneNode>)
253
+ expect(keys(plateAffectedSurfaces(rug, plate, 'room:zone_b/*'))).toEqual(
254
+ onPlates('room:zone_b', 'room:zone_b/rug', 'step:zone_b/door_ab', 'step:zone_b/door_bc'),
255
+ )
256
+ const nodes = withB({ floorStepOverrides: [{ key: 'door_bc', finish: 'library:x' }] })
257
+ expect(keys(plateAffectedSurfaces(nodes, plate, 'room:zone_b/*'))).toEqual(
258
+ onPlates('room:zone_b', 'step:zone_b/door_ab'),
259
+ )
260
+ // A room step finish holds the steps: the floor goes alone.
261
+ expect(
262
+ keys(plateAffectedSurfaces(withB({ floorStepFinish: 'library:y' }), plate, 'room:zone_b/*')),
263
+ ).toEqual(onPlates('room:zone_b'))
264
+ })
265
+
266
+ test('erased, it also clears its painted parts and its edge; doorway paint stays', () => {
267
+ const nodes = withB({
268
+ floorStepFinish: 'library:y',
269
+ floorEdgeFinish: 'library:stone',
270
+ floorStepOverrides: [{ key: 'door_bc', finish: 'library:x' }],
271
+ floor: {
272
+ ...zoneB.floor,
273
+ finish: 'library:oak',
274
+ regions: [{ id: 'rug', polygon: square(4), finish: 'library:red' }],
275
+ },
276
+ } as Partial<ZoneNode>)
277
+ expect(keys(plateAffectedSurfaces(nodes, plate, 'room:zone_b/*', { erasing: true }))).toEqual(
278
+ onPlates('edge:zone_b', 'room:zone_b', 'room:zone_b/rug', 'step:zone_b/door_ab'),
279
+ )
280
+ })
281
+
282
+ test('a floor previews on every plate; its steps are drawn by that preview', () => {
283
+ expect(keys(platePreviewSurfaces(row, plate, 'room:zone_b'))).toEqual(onPlates('room:zone_b'))
284
+ expect(keys(platePreviewSurfaces(row, plate, 'room:zone_b/*'))).toEqual(
285
+ onPlates('room:zone_b/*'),
286
+ )
287
+ expect(keys(platePreviewSurfaces(row, plate, 'step:zone_b'))).toEqual(onPlates('step:zone_b'))
288
+ })
289
+ })
290
+
291
+ describe("a footprint's edge band previews where its rooms' edges are drawn", () => {
292
+ test('the band itself, and the plates drawing the edges that carry it', () => {
293
+ expect(keys(platePreviewSurfaces(nodes, base, 'edge'))).toEqual([
294
+ 'slab_base:edge',
295
+ 'slab_platform:edge',
296
+ ])
297
+ expect(keys(platePreviewSurfaces(nodes, base, 'foundation'))).toEqual(['slab_base:foundation'])
298
+ expect(platePreviewSurfaces(nodes, manual, 'surface')).toBeNull()
299
+ })
300
+ })
@@ -0,0 +1,177 @@
1
+ import {
2
+ type AnyNode,
3
+ type AnyNodeId,
4
+ floorStepKeysOf,
5
+ floorStepOverrideFor,
6
+ floorStepRole,
7
+ floorStepRoleCovers,
8
+ parseCeilingRegionRole,
9
+ parseFloorStepRole,
10
+ parseRoomFinishRole,
11
+ roomFinishRole,
12
+ type SlabNode,
13
+ type ZoneNode,
14
+ } from '@pascal-app/core'
15
+ import { create } from 'zustand'
16
+ import { roomFootprint } from './floor-footprints'
17
+
18
+ export type PaintSurface = { nodeId: AnyNodeId; role: string }
19
+
20
+ const isFloorPlate = (node: AnyNode | undefined): node is SlabNode =>
21
+ node?.type === 'slab' && node.boundary === 'auto'
22
+
23
+ /**
24
+ * Every floor-plate surface one paint click changes: the painted surface and
25
+ * the surfaces whose finish falls back to it. A room's floor carries its steps
26
+ * until they are painted on their own (room-wide or per doorway); the
27
+ * footprint's edge band carries the edges of the rooms on it that have no edge
28
+ * finish; a step paints its doorway, or in the room scope (`step:<zoneId>`)
29
+ * every step of the room, wherever they are drawn. A region paint changes only
30
+ * the region. The whole room (`room:<zoneId>/*`) paints its floor, its painted
31
+ * parts and the steps that follow it; erased, it also clears its steps and edge.
32
+ *
33
+ * This set is the hover outline; `platePreviewSurfaces` previews it, so what
34
+ * lights up is exactly what the click changes. Null for anything but a floor
35
+ * plate.
36
+ */
37
+ export function plateAffectedSurfaces(
38
+ nodes: Readonly<Record<string, AnyNode>>,
39
+ node: AnyNode,
40
+ role: string,
41
+ options: { erasing?: boolean } = {},
42
+ ): PaintSurface[] | null {
43
+ if (!isFloorPlate(node)) return null
44
+ const plates = Object.values(nodes).filter(
45
+ (other): other is SlabNode => isFloorPlate(other) && other.parentId === node.parentId,
46
+ )
47
+ const onEveryPlate = (paintRole: string) =>
48
+ plates.map((plate) => ({ nodeId: plate.id as AnyNodeId, role: paintRole }))
49
+ const zoneOf = (zoneId: string) => {
50
+ const zone = nodes[zoneId]
51
+ return zone?.type === 'zone' ? zone : null
52
+ }
53
+ // The room's steps that no doorway paint holds: the ones its floor carries.
54
+ const followingSteps = (zone: ZoneNode) => {
55
+ const zones = Object.values(nodes).filter(
56
+ (other): other is ZoneNode => other.type === 'zone' && other.parentId === zone.parentId,
57
+ )
58
+ return floorStepKeysOf(nodes, zone.parentId ?? '', zone.id)
59
+ .filter((key) => !floorStepOverrideFor(zone, key, null, zones))
60
+ .flatMap((key) => onEveryPlate(floorStepRole(zone.id, key)))
61
+ }
62
+
63
+ if (parseFloorStepRole(role)) return onEveryPlate(role)
64
+
65
+ const floor = parseRoomFinishRole(role)
66
+ if (floor) {
67
+ const zone = zoneOf(floor.zoneId)
68
+ if (floor.regionId === '*') {
69
+ const surfaces = onEveryPlate(roomFinishRole(floor.zoneId))
70
+ if (!zone) return surfaces
71
+ for (const region of zone.floor?.regions ?? [])
72
+ surfaces.push(...onEveryPlate(roomFinishRole(zone.id, region.id)))
73
+ if (!options.erasing)
74
+ return zone.floorStepFinish ? surfaces : [...surfaces, ...followingSteps(zone)]
75
+ surfaces.push(...followingSteps(zone))
76
+ if (zone.floorEdgeFinish) surfaces.push(...onEveryPlate(`edge:${zone.id}`))
77
+ return surfaces
78
+ }
79
+ const surfaces = onEveryPlate(role)
80
+ if (floor.regionId || !zone || zone.floorStepFinish) return surfaces
81
+ return [...surfaces, ...followingSteps(zone)]
82
+ }
83
+
84
+ if (role === 'edge' && node.plateRole === 'base') {
85
+ const surfaces: PaintSurface[] = [{ nodeId: node.id as AnyNodeId, role }]
86
+ for (const zone of Object.values(nodes)) {
87
+ if (zone.type !== 'zone' || zone.parentId !== node.parentId || zone.floorEdgeFinish) continue
88
+ if (roomFootprint(nodes as Record<string, AnyNode>, zone.id)?.id !== node.id) continue
89
+ surfaces.push(...onEveryPlate(`edge:${zone.id}`))
90
+ }
91
+ return surfaces
92
+ }
93
+
94
+ return [{ nodeId: node.id as AnyNodeId, role }]
95
+ }
96
+
97
+ /**
98
+ * The (plate, role) pairs whose paint preview draws `plateAffectedSurfaces`.
99
+ * A surface that follows another (a room's unpainted steps, the room edges on
100
+ * a footprint's band) is drawn by the preview of the surface it follows on the
101
+ * same plate: only that preview knows what the follower shows once its leader
102
+ * is erased. So a floor and a whole room preview on every plate, and a
103
+ * footprint's edge band on itself and on the plates drawing its rooms' edges.
104
+ */
105
+ export function platePreviewSurfaces(
106
+ nodes: Readonly<Record<string, AnyNode>>,
107
+ node: AnyNode,
108
+ role: string,
109
+ ): PaintSurface[] | null {
110
+ if (!isFloorPlate(node)) return null
111
+ if (parseFloorStepRole(role) || parseRoomFinishRole(role))
112
+ return Object.values(nodes)
113
+ .filter((other) => isFloorPlate(other) && other.parentId === node.parentId)
114
+ .map((plate) => ({ nodeId: plate.id as AnyNodeId, role }))
115
+ if (role === 'edge' && node.plateRole === 'base')
116
+ return mergePaintSurfaces(
117
+ [{ nodeId: node.id as AnyNodeId, role }],
118
+ (plateAffectedSurfaces(nodes, node, role) ?? []).map((surface) => ({
119
+ nodeId: surface.nodeId,
120
+ role,
121
+ })),
122
+ )
123
+ return [{ nodeId: node.id as AnyNodeId, role }]
124
+ }
125
+
126
+ /** A ceiling's painted part: the click changes that region and nothing else. */
127
+ export function ceilingAffectedSurfaces(node: AnyNode, role: string): PaintSurface[] | null {
128
+ return node.type === 'ceiling' && parseCeilingRegionRole(role)
129
+ ? [{ nodeId: node.id as AnyNodeId, role }]
130
+ : null
131
+ }
132
+
133
+ /** The targets a preview paints: the click's own targets plus what falls back to them. */
134
+ export function mergePaintSurfaces(
135
+ first: readonly PaintSurface[],
136
+ second: readonly PaintSurface[] | null,
137
+ ): PaintSurface[] {
138
+ const merged = new Map<string, PaintSurface>()
139
+ for (const surface of [...first, ...(second ?? [])])
140
+ merged.set(`${surface.nodeId}:${surface.role}`, surface)
141
+ return [...merged.values()]
142
+ }
143
+
144
+ /**
145
+ * The meshes of `surfaces` under their registered roots: what the hover
146
+ * outline draws instead of the whole plate.
147
+ */
148
+ export function paintSurfaceMeshes<
149
+ T extends { userData: Record<string, unknown>; traverse: (visit: (object: T) => void) => void },
150
+ >(surfaces: readonly PaintSurface[], rootOf: (nodeId: AnyNodeId) => T | null | undefined): T[] {
151
+ const byNode = new Map<AnyNodeId, Set<string>>()
152
+ for (const { nodeId, role } of surfaces) {
153
+ const roles = byNode.get(nodeId) ?? new Set<string>()
154
+ roles.add(role)
155
+ byNode.set(nodeId, roles)
156
+ }
157
+ const meshes: T[] = []
158
+ for (const [nodeId, roles] of byNode) {
159
+ rootOf(nodeId)?.traverse((object) => {
160
+ const { paintRole, slotId, __fromGeometry } = object.userData as {
161
+ paintRole?: string
162
+ slotId?: string | null
163
+ __fromGeometry?: boolean
164
+ }
165
+ if ((object as { isMesh?: boolean }).isMesh !== true || __fromGeometry !== true) return
166
+ const role = paintRole ?? slotId
167
+ if (role && (roles.has(role) || [...roles].some((wide) => floorStepRoleCovers(wide, role))))
168
+ meshes.push(object)
169
+ })
170
+ }
171
+ return meshes
172
+ }
173
+
174
+ /** The floor-plate surfaces the paint hover outlines (null: outline the hovered node as usual). */
175
+ export const usePaintOutline = create<{ surfaces: PaintSurface[] | null }>(() => ({
176
+ surfaces: null,
177
+ }))
@@ -0,0 +1,50 @@
1
+ import type { ComponentType } from 'react'
2
+
3
+ /**
4
+ * What a plugin's tool answers in a chat. Images travel by reference (a host-stored asset), never
5
+ * as bytes, so a transcript stays small; a refusal is `error` with its stable `code`.
6
+ */
7
+ export type ChatToolOutput = {
8
+ text?: string
9
+ data?: Record<string, unknown>
10
+ images?: readonly { ref: string; alt?: string }[]
11
+ error?: string
12
+ code?: string
13
+ }
14
+
15
+ /**
16
+ * What a plugin's card renders for one of its tool calls, from the persisted call alone. `actions`
17
+ * are the host's: open one of the editor's views, select nodes, send a prompt to the chat.
18
+ */
19
+ export type ChatToolCardProps = {
20
+ toolName: string
21
+ input: unknown
22
+ output: ChatToolOutput | undefined
23
+ state: 'running' | 'done' | 'error' | 'cancelled'
24
+ actions: {
25
+ openView(viewId: string, options?: { beside?: boolean }): void
26
+ select(nodeIds: readonly string[]): void
27
+ send(prompt: string): void
28
+ }
29
+ }
30
+
31
+ /**
32
+ * A plugin's contribution to a host's chat, exported from its browser entry beside its panels and
33
+ * views; its tool contracts are its `./agent-tools` export. Which tools a turn offers, and to whom,
34
+ * is the host's decision.
35
+ */
36
+ export type PluginChatExtension = {
37
+ pluginId: string
38
+ /** Runs the plugin's `runsIn: 'editor'` tools in the editor tab. */
39
+ execute?: (
40
+ name: string,
41
+ input: unknown,
42
+ context: { projectId: string; signal: AbortSignal },
43
+ ) => Promise<ChatToolOutput>
44
+ /** A card per tool name, loaded lazily; a tool without one shows as a plain tool row. */
45
+ cards?: Readonly<Record<string, () => Promise<{ default: ComponentType<ChatToolCardProps> }>>>
46
+ /** Entries for the composer's `/` menu: picking one sends `prompt`. */
47
+ actions?: readonly { id: string; label: string; hint: string; prompt: string }[]
48
+ /** File extensions or MIME types the plugin's tools read, which the composer then accepts. */
49
+ accepts?: readonly string[]
50
+ }
@@ -3,7 +3,9 @@ import {
3
3
  type EditorHostPanel,
4
4
  editorHostPanelRegistry,
5
5
  managedPluginIds,
6
+ pluginInstallLocks,
6
7
  registerEditorHostPanel,
8
+ setPluginInstallLocks,
7
9
  showsPluginManager,
8
10
  } from './plugin-panels'
9
11
 
@@ -108,3 +110,26 @@ describe('showsPluginManager', () => {
108
110
  }
109
111
  })
110
112
  })
113
+
114
+ describe('pluginInstallLocks', () => {
115
+ afterEach(() => setPluginInstallLocks({}))
116
+
117
+ test('replaces the whole lock set and notifies subscribers with a new snapshot', () => {
118
+ let notified = 0
119
+ const unsubscribe = pluginInstallLocks.subscribe(() => {
120
+ notified += 1
121
+ })
122
+ const before = pluginInstallLocks.getSnapshot()
123
+ setPluginInstallLocks({
124
+ 'pascal:architect': { actionLabel: 'Upgrade', onAction: () => {}, reason: 'Pro plan' },
125
+ })
126
+ const after = pluginInstallLocks.getSnapshot()
127
+
128
+ expect(after).not.toBe(before)
129
+ expect(Object.keys(after)).toEqual(['pascal:architect'])
130
+ setPluginInstallLocks({})
131
+ expect(pluginInstallLocks.getSnapshot()).toEqual({})
132
+ expect(notified).toBe(2)
133
+ unsubscribe()
134
+ })
135
+ })
@@ -17,6 +17,48 @@ export type EditorHostPanel = {
17
17
  }
18
18
  pluginUrl?: string
19
19
  defaultInstalled?: boolean
20
+ /** Short label shown next to the plugin name in the manager, e.g. "Pro". */
21
+ badge?: string
22
+ /** Directory grouping, e.g. "Outdoor & site". Uncategorised plugins list under "Other". */
23
+ category?: string
24
+ /** One line for the directory card; `description` is the full text on its detail. */
25
+ summary?: string
26
+ /** What the plugin reaches beyond the editor — the host's policy, shown before install. */
27
+ access?: readonly PluginAccess[]
28
+ }
29
+
30
+ /** One thing a plugin can reach once installed: a host service, an outside account, shared data. */
31
+ export type PluginAccess = {
32
+ /** Picks the glyph: AI calls, media models, data pulls, an outside account, sharing. */
33
+ kind: 'ai' | 'media' | 'data' | 'account' | 'sharing'
34
+ label: string
35
+ detail: string
36
+ /** Calls spend the project's credits; the card sums these into one "Uses credits" chip. */
37
+ usesCredits?: boolean
38
+ /** Card chip for an access that spends no credits, e.g. "Mint account". Defaults to `label`. */
39
+ chip?: string
40
+ }
41
+
42
+ /**
43
+ * Host-owned facts about the open project that the plugin directory shows: its
44
+ * name, the credits plugin calls spend, and the house rules under the
45
+ * categories ("Installing is free", …).
46
+ */
47
+ export type PluginDirectoryContext = {
48
+ projectName?: string
49
+ credits?: { remaining: number } | { unlimited: true } | null
50
+ notes?: readonly string[]
51
+ }
52
+
53
+ /**
54
+ * A host-owned reason why a plugin cannot be installed right now (a plan the
55
+ * account doesn't have, for instance). The manager swaps its Install button
56
+ * for `actionLabel`; uninstalling is never locked.
57
+ */
58
+ export type PluginInstallLock = {
59
+ reason: string
60
+ actionLabel: string
61
+ onAction: () => void
20
62
  }
21
63
 
22
64
  function isDevMode(): boolean {
@@ -86,6 +128,58 @@ class EditorHostPanelRegistryImpl {
86
128
 
87
129
  export const editorHostPanelRegistry = new EditorHostPanelRegistryImpl()
88
130
 
131
+ class PluginInstallLocksImpl {
132
+ private locks: Readonly<Record<string, PluginInstallLock>> = {}
133
+ private readonly listeners = new Set<() => void>()
134
+
135
+ subscribe = (onChange: () => void): (() => void) => {
136
+ this.listeners.add(onChange)
137
+ return () => {
138
+ this.listeners.delete(onChange)
139
+ }
140
+ }
141
+
142
+ getSnapshot = (): Readonly<Record<string, PluginInstallLock>> => this.locks
143
+
144
+ set(locks: Record<string, PluginInstallLock>): void {
145
+ this.locks = { ...locks }
146
+ for (const listener of this.listeners) listener()
147
+ }
148
+ }
149
+
150
+ export const pluginInstallLocks = new PluginInstallLocksImpl()
151
+
152
+ class PluginDirectoryContextImpl {
153
+ private context: PluginDirectoryContext = {}
154
+ private readonly listeners = new Set<() => void>()
155
+
156
+ subscribe = (onChange: () => void): (() => void) => {
157
+ this.listeners.add(onChange)
158
+ return () => {
159
+ this.listeners.delete(onChange)
160
+ }
161
+ }
162
+
163
+ getSnapshot = (): PluginDirectoryContext => this.context
164
+
165
+ set(context: PluginDirectoryContext): void {
166
+ this.context = context
167
+ for (const listener of this.listeners) listener()
168
+ }
169
+ }
170
+
171
+ export const pluginDirectoryContext = new PluginDirectoryContextImpl()
172
+
173
+ /** Replaces what the plugin directory knows about the open project. */
174
+ export function setPluginDirectoryContext(context: PluginDirectoryContext): void {
175
+ pluginDirectoryContext.set(context)
176
+ }
177
+
178
+ /** Replaces every install lock, keyed by `pluginId`. Pass `{}` to clear. */
179
+ export function setPluginInstallLocks(locks: Record<string, PluginInstallLock>): void {
180
+ pluginInstallLocks.set(locks)
181
+ }
182
+
89
183
  export function registerEditorHostPanel(panel: EditorHostPanel): void {
90
184
  editorHostPanelRegistry.registerPanel(panel)
91
185
  }
@@ -131,6 +131,7 @@ function preparedWallHeight(
131
131
  node.supportSlabId ?? null,
132
132
  undefined,
133
133
  node.supportOffset,
134
+ node.justification,
134
135
  )
135
136
  const hasDisplacedBase =
136
137
  Math.abs(support.baseElevation - support.elevation) > 1e-5 ||
@@ -0,0 +1,12 @@
1
+ import { nodeRegistry } from '@pascal-app/core'
2
+
3
+ export const REGISTERED_DRAFT_SNAP_EXTENSION = 'pascal:editor/draft-snap'
4
+
5
+ export function snapRegisteredDraftPoint<TArgs, TResult>(
6
+ kind: string,
7
+ args: TArgs,
8
+ fallback: TResult,
9
+ ): TResult {
10
+ const snap = nodeRegistry.get(kind)?.extensions?.[REGISTERED_DRAFT_SNAP_EXTENSION]
11
+ return typeof snap === 'function' ? (snap as (args: TArgs) => TResult)(args) : fallback
12
+ }