@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,266 @@
1
+ /**
2
+ * Lot terrain from USGS elevations: an N×N grid of points over the lot's
3
+ * bounding box (padded a touch so the ground runs past the lines) goes to
4
+ * the parcel provider's `elevation` (USGS EPQS, feet), the readings come
5
+ * back relative to a DATUM — the ground at the lot's centre, so the site
6
+ * plane y = 0 is the ground where the house will stand — and are written
7
+ * into the site's heightfield (`site.terrain`, the same field the sculpt
8
+ * tool edits and every placement / raycast / drape reads).
9
+ *
10
+ * Honesty: a web-service DEM is preliminary — the sample record on the
11
+ * site (`metadata.terrainSample`: source, grid, holes, datum, relief) says
12
+ * what was read; a lot flatter than `MIN_RELIEF_M` writes NO terrain and
13
+ * keeps the flat-ground fast path; every failure returns a reason and
14
+ * writes nothing.
15
+ */
16
+ import {
17
+ commitTerrainField,
18
+ createTerrainField,
19
+ quantize,
20
+ type TerrainData,
21
+ type TerrainField,
22
+ } from '@pascal-app/core'
23
+ import { getParcelProvider, NO_PARCEL_SERVICE, type ParcelProvider } from './parcel-provider'
24
+
25
+ export type Pt = readonly [number, number]
26
+
27
+ /** apps/editor/lib/parcel/project.ts — the plan frame's degree scale. */
28
+ export const FEET_PER_DEG_LAT = 364000
29
+ const METRES_PER_FOOT = 0.3048
30
+ const DEG2RAD = Math.PI / 180
31
+ /** Grid points per side (81 points — well under the route's 256 cap). */
32
+ export const DEFAULT_GRID_N = 9
33
+ /** The lot bbox is padded by this fraction so the mesh runs past the lines. */
34
+ export const PAD_FRAC = 0.08
35
+ /** A lot with less fall than this across its samples is flat: no terrain written. */
36
+ export const MIN_RELIEF_M = 0.15
37
+ /** Heightfield resolution: the bbox split into ~64 cells per side, clamped. */
38
+ const FIELD_CELLS = 64
39
+ const MIN_SPACING_M = 0.5
40
+ const MAX_SPACING_M = 4
41
+
42
+ export interface TerrainSampleSummary {
43
+ source: 'USGS EPQS'
44
+ /** Grid side (n × n points). */
45
+ grid: number
46
+ /** Points with a reading / without one. */
47
+ sampled: number
48
+ holes: number
49
+ /** Absolute elevation of the site plane (the datum), feet. */
50
+ datumFt: number
51
+ /** Highest − lowest reading, feet. */
52
+ reliefFt: number
53
+ /** True when the lot read flat and no heightfield was written. */
54
+ flat: boolean
55
+ at: string
56
+ }
57
+
58
+ export interface TerrainSampleResult {
59
+ ok: boolean
60
+ reason?: string
61
+ /** The heightfield to write to `site.terrain` (absent when flat). */
62
+ terrain?: TerrainData
63
+ summary?: TerrainSampleSummary
64
+ }
65
+
66
+ export interface SampleGrid {
67
+ n: number
68
+ x0: number
69
+ z0: number
70
+ x1: number
71
+ z1: number
72
+ /** Row-major, `r * n + c`. */
73
+ points: Pt[]
74
+ }
75
+
76
+ /** Local plan metres (the site frame) → `{ lat, lng }`, the inverse of `ringsToPlanFeet`. */
77
+ export function localMetresToLngLat(
78
+ p: Pt,
79
+ originLngLat: readonly [number, number],
80
+ ): { lat: number; lng: number } {
81
+ const [oLng, oLat] = originLngLat
82
+ const ftPerDegLng = FEET_PER_DEG_LAT * Math.cos(oLat * DEG2RAD)
83
+ const xFt = p[0] / METRES_PER_FOOT
84
+ const zFt = p[1] / METRES_PER_FOOT
85
+ return { lng: oLng + xFt / ftPerDegLng, lat: oLat - zFt / FEET_PER_DEG_LAT }
86
+ }
87
+
88
+ /** The n × n sample grid over the ring's padded bounding box. */
89
+ export function gridOver(ring: readonly Pt[], n = DEFAULT_GRID_N, pad = PAD_FRAC): SampleGrid {
90
+ let minX = Number.POSITIVE_INFINITY
91
+ let minZ = Number.POSITIVE_INFINITY
92
+ let maxX = Number.NEGATIVE_INFINITY
93
+ let maxZ = Number.NEGATIVE_INFINITY
94
+ for (const p of ring) {
95
+ minX = Math.min(minX, p[0])
96
+ maxX = Math.max(maxX, p[0])
97
+ minZ = Math.min(minZ, p[1])
98
+ maxZ = Math.max(maxZ, p[1])
99
+ }
100
+ const padX = (maxX - minX) * pad
101
+ const padZ = (maxZ - minZ) * pad
102
+ const x0 = minX - padX
103
+ const x1 = maxX + padX
104
+ const z0 = minZ - padZ
105
+ const z1 = maxZ + padZ
106
+ const side = Math.max(2, Math.floor(n))
107
+ const points: Pt[] = []
108
+ for (let r = 0; r < side; r++) {
109
+ const z = z0 + ((z1 - z0) * r) / (side - 1)
110
+ for (let c = 0; c < side; c++) points.push([x0 + ((x1 - x0) * c) / (side - 1), z])
111
+ }
112
+ return { n: side, x0, z0, x1, z1, points }
113
+ }
114
+
115
+ /**
116
+ * Bilinear read of the coarse grid at a plan point; holes (null readings)
117
+ * read as `fill`. Clamped to the grid — the field never extrapolates.
118
+ */
119
+ export function coarseHeightAt(
120
+ grid: SampleGrid,
121
+ elev: readonly (number | null)[],
122
+ x: number,
123
+ z: number,
124
+ fill: number,
125
+ ): number {
126
+ const n = grid.n
127
+ const fx = grid.x1 === grid.x0 ? 0 : ((x - grid.x0) / (grid.x1 - grid.x0)) * (n - 1)
128
+ const fz = grid.z1 === grid.z0 ? 0 : ((z - grid.z0) / (grid.z1 - grid.z0)) * (n - 1)
129
+ const gx = Math.max(0, Math.min(n - 1, fx))
130
+ const gz = Math.max(0, Math.min(n - 1, fz))
131
+ const c0 = Math.min(n - 2, Math.floor(gx))
132
+ const r0 = Math.min(n - 2, Math.floor(gz))
133
+ const tx = gx - c0
134
+ const tz = gz - r0
135
+ const at = (r: number, c: number): number => {
136
+ const v = elev[r * n + c]
137
+ return v === null || v === undefined || !Number.isFinite(v) ? fill : v
138
+ }
139
+ const top = at(r0, c0) * (1 - tx) + at(r0, c0 + 1) * tx
140
+ const bottom = at(r0 + 1, c0) * (1 - tx) + at(r0 + 1, c0 + 1) * tx
141
+ return top * (1 - tz) + bottom * tz
142
+ }
143
+
144
+ /** A heightfield over the grid's extent, metres above the datum, bilinear from the coarse samples. */
145
+ export function fieldFromSamples(
146
+ grid: SampleGrid,
147
+ elevM: readonly (number | null)[],
148
+ datumM: number,
149
+ fillM: number,
150
+ ): TerrainField {
151
+ const spanX = grid.x1 - grid.x0
152
+ const spanZ = grid.z1 - grid.z0
153
+ const spacing = Math.min(
154
+ MAX_SPACING_M,
155
+ Math.max(MIN_SPACING_M, Math.max(spanX, spanZ) / FIELD_CELLS),
156
+ )
157
+ const cols = Math.max(2, Math.ceil(spanX / spacing) + 1)
158
+ const rows = Math.max(2, Math.ceil(spanZ / spacing) + 1)
159
+ const field = createTerrainField({ origin: [grid.x0, grid.z0], spacing, cols, rows })
160
+ for (let r = 0; r < rows; r++) {
161
+ for (let c = 0; c < cols; c++) {
162
+ const x = grid.x0 + c * spacing
163
+ const z = grid.z0 + r * spacing
164
+ field.heights[r * cols + c] = quantize(
165
+ field,
166
+ coarseHeightAt(grid, elevM, x, z, fillM) - datumM,
167
+ )
168
+ }
169
+ }
170
+ return field
171
+ }
172
+
173
+ type ElevationResponse = {
174
+ ok?: boolean
175
+ error?: string
176
+ results?: { lat?: number; lng?: number; elevation?: number | null }[]
177
+ }
178
+
179
+ export interface SampleOptions {
180
+ /** Default = the provider the host set. */
181
+ provider?: ParcelProvider
182
+ /** Grid side, default 9. */
183
+ n?: number
184
+ /** Passed to the route (USGS is slow; the route caps it). */
185
+ deadlineMs?: number
186
+ /** Where the datum is read (site metres); default = the ring's centroid. */
187
+ datumAt?: Pt
188
+ now?: () => string
189
+ }
190
+
191
+ /**
192
+ * Sample the lot. Resolves `{ ok: false, reason }` on every failure path and
193
+ * never throws; `ok: true` with no `terrain` means the lot read flat.
194
+ */
195
+ export async function sampleLotTerrain(
196
+ ring: readonly Pt[],
197
+ originLngLat: readonly [number, number],
198
+ options: SampleOptions = {},
199
+ ): Promise<TerrainSampleResult> {
200
+ if (ring.length < 3) return { ok: false, reason: 'no lot ring' }
201
+ const provider = options.provider ?? getParcelProvider()
202
+ if (!provider) return { ok: false, reason: NO_PARCEL_SERVICE }
203
+ const grid = gridOver(ring, options.n ?? DEFAULT_GRID_N)
204
+ const points = grid.points.map((p) => localMetresToLngLat(p, originLngLat))
205
+ let body: ElevationResponse
206
+ try {
207
+ body = (await provider('elevation', {
208
+ points,
209
+ deadlineMs: options.deadlineMs ?? 12000,
210
+ })) as ElevationResponse
211
+ } catch (error) {
212
+ return { ok: false, reason: error instanceof Error ? error.message : 'elevation lookup failed' }
213
+ }
214
+ if (!body.ok || !Array.isArray(body.results))
215
+ return { ok: false, reason: body.error ?? 'no elevations' }
216
+ const elevFt: (number | null)[] = grid.points.map((_, i) => {
217
+ const e = body.results?.[i]?.elevation
218
+ return typeof e === 'number' && Number.isFinite(e) ? e : null
219
+ })
220
+ const valid = elevFt.filter((e): e is number => e !== null)
221
+ if (valid.length < 4)
222
+ return { ok: false, reason: `too few elevations (${valid.length} of ${elevFt.length})` }
223
+ const mean = valid.reduce((s, v) => s + v, 0) / valid.length
224
+ const datumAt = options.datumAt ?? centroidOf(ring)
225
+ const datumFt = coarseHeightAt(grid, elevFt, datumAt[0], datumAt[1], mean)
226
+ const reliefFt = Math.max(...valid) - Math.min(...valid)
227
+ const summary: TerrainSampleSummary = {
228
+ source: 'USGS EPQS',
229
+ grid: grid.n,
230
+ sampled: valid.length,
231
+ holes: elevFt.length - valid.length,
232
+ datumFt,
233
+ reliefFt,
234
+ flat: reliefFt * METRES_PER_FOOT < MIN_RELIEF_M,
235
+ at: (options.now ?? (() => new Date().toISOString()))(),
236
+ }
237
+ if (summary.flat) return { ok: true, summary }
238
+ const elevM = elevFt.map((e) => (e === null ? null : e * METRES_PER_FOOT))
239
+ const field = fieldFromSamples(grid, elevM, datumFt * METRES_PER_FOOT, mean * METRES_PER_FOOT)
240
+ return { ok: true, terrain: commitTerrainField(field), summary }
241
+ }
242
+
243
+ function centroidOf(ring: readonly Pt[]): Pt {
244
+ let x = 0
245
+ let z = 0
246
+ for (const p of ring) {
247
+ x += p[0]
248
+ z += p[1]
249
+ }
250
+ return [x / ring.length, z / ring.length]
251
+ }
252
+
253
+ /** One status fragment for the drop-in message. */
254
+ export function describeTerrainSample(
255
+ summary: TerrainSampleSummary | null,
256
+ failure: string,
257
+ ): string {
258
+ if (summary) {
259
+ if (summary.flat) return `ground flat within ${Math.round(summary.reliefFt * 12)}" (USGS)`
260
+ const holes = summary.holes > 0 ? `, ${summary.holes} unread` : ''
261
+ return `terrain: ${summary.reliefFt.toFixed(1)}' of fall across the lot (USGS, ${summary.sampled} pts${holes})`
262
+ }
263
+ if (failure && failure !== 'skipped')
264
+ return `terrain read failed (${failure}) — the ground is flat until it is read`
265
+ return ''
266
+ }
@@ -0,0 +1,74 @@
1
+ 'use client'
2
+
3
+ import {
4
+ getCatalogMaterialById,
5
+ getDynamicLibraryMaterials,
6
+ getLibraryMaterialIdFromRef,
7
+ getLibraryMaterialsVersion,
8
+ getMaterialsForCategory,
9
+ MATERIAL_CATEGORIES,
10
+ type MaterialSource,
11
+ subscribeLibraryMaterials,
12
+ toLibraryMaterialRef,
13
+ } from '@pascal-app/core'
14
+ import { useEffect, useState, useSyncExternalStore } from 'react'
15
+
16
+ export type MaterialSourceFilter = MaterialSource
17
+ const SOURCE_FILTERS: { id: MaterialSourceFilter; label: string }[] = [
18
+ { id: 'pascal', label: 'Pascal' },
19
+ { id: 'mine', label: 'Mine' },
20
+ { id: 'workspace', label: 'Workspace' },
21
+ { id: 'community', label: 'Community' },
22
+ ]
23
+
24
+ export function useMaterialCatalogModel(
25
+ selectedMaterialPreset?: string,
26
+ onSelectMaterialPreset?: (ref: string) => void,
27
+ disabled = false,
28
+ /** Paint mode arms the tab's first material; a finish row only writes on a tile click. */
29
+ selectOnCategoryChange = true,
30
+ ) {
31
+ const [selectedCategory, setCategory] = useState<(typeof MATERIAL_CATEGORIES)[number]>(
32
+ MATERIAL_CATEGORIES[0],
33
+ )
34
+ const [sourceFilter, setSourceFilter] = useState<MaterialSourceFilter>('pascal')
35
+ useSyncExternalStore(
36
+ subscribeLibraryMaterials,
37
+ getLibraryMaterialsVersion,
38
+ getLibraryMaterialsVersion,
39
+ )
40
+ const availableCategories = MATERIAL_CATEGORIES.filter(
41
+ (category) => getMaterialsForCategory(category).length > 0,
42
+ )
43
+ const visibleSourceFilters = SOURCE_FILTERS.filter(
44
+ (filter) =>
45
+ filter.id !== 'workspace' ||
46
+ getDynamicLibraryMaterials().some((item) => item.source === 'workspace'),
47
+ )
48
+ const itemsFor = (category: typeof selectedCategory) =>
49
+ getMaterialsForCategory(category).filter((item) => (item.source ?? 'pascal') === sourceFilter)
50
+ useEffect(() => {
51
+ const entry = getCatalogMaterialById(
52
+ getLibraryMaterialIdFromRef(selectedMaterialPreset) ?? undefined,
53
+ )
54
+ if (entry?.category) setCategory(entry.category)
55
+ }, [selectedMaterialPreset])
56
+ const select = (id: string) => {
57
+ if (!disabled) onSelectMaterialPreset?.(toLibraryMaterialRef(id))
58
+ }
59
+ const setSelectedCategory = (category: typeof selectedCategory) => {
60
+ setCategory(category)
61
+ const first = itemsFor(category)[0]
62
+ if (first && selectOnCategoryChange) select(first.id)
63
+ }
64
+ return {
65
+ selectedCategory,
66
+ setSelectedCategory,
67
+ sourceFilter,
68
+ setSourceFilter,
69
+ visibleSourceFilters,
70
+ availableCategories,
71
+ catalogItems: itemsFor(selectedCategory),
72
+ select,
73
+ }
74
+ }
@@ -0,0 +1,80 @@
1
+ 'use client'
2
+
3
+ import {
4
+ type AnyNodeId,
5
+ generateSceneMaterialId,
6
+ toSceneMaterialRef,
7
+ useScene,
8
+ } from '@pascal-app/core'
9
+ import { useViewer } from '@pascal-app/viewer'
10
+ import { useEffect } from 'react'
11
+ import useEditor from '../store/use-editor'
12
+ import { buildResetSurfaceMaterialUpdates, resolvePaintTargetFromSelection } from './material-paint'
13
+ import { usePaintRegionMode } from './paint-region-mode'
14
+
15
+ export function useMaterialPaintPanelModel(enabled = true) {
16
+ const activePaintMaterial = useEditor((state) => state.activePaintMaterial)
17
+ const activePaintTarget = useEditor((state) => state.activePaintTarget)
18
+ const setActivePaintMaterial = useEditor((state) => state.armMaterialPaint)
19
+ const setActivePaintTarget = useEditor((state) => state.setActivePaintTarget)
20
+ const selectedId = useViewer((state) =>
21
+ state.selection.selectedIds.length === 1 ? (state.selection.selectedIds[0] ?? null) : null,
22
+ )
23
+ const paintTarget = useScene((state) =>
24
+ resolvePaintTargetFromSelection({ nodes: state.nodes, selectedId }),
25
+ )
26
+ const materials = useScene((state) => state.materials)
27
+ const paintEraser = usePaintRegionMode((state) => state.mode === 'erase')
28
+ const setPaintEraser = (on: boolean) => {
29
+ const paintMode = usePaintRegionMode.getState()
30
+ paintMode.setMode(on ? 'erase' : paintMode.drawMode)
31
+ }
32
+ const canResetSelection = paintTarget !== null
33
+ const resetSelection = () => {
34
+ const scene = useScene.getState()
35
+ const node = selectedId ? scene.nodes[selectedId as AnyNodeId] : undefined
36
+ if (node) scene.updateNodes(buildResetSurfaceMaterialUpdates(scene.nodes, node))
37
+ }
38
+ useEffect(() => {
39
+ if (enabled && paintTarget) setActivePaintTarget(paintTarget)
40
+ }, [enabled, paintTarget, setActivePaintTarget])
41
+ const createCustomMaterial = () => {
42
+ const scene = useScene.getState()
43
+ const id = generateSceneMaterialId()
44
+ scene.addSceneMaterial({
45
+ id,
46
+ name: `Material ${Object.keys(scene.materials).length + 1}`,
47
+ material: {
48
+ preset: 'custom',
49
+ properties: {
50
+ color: '#ffffff',
51
+ roughness: 0.5,
52
+ metalness: 0,
53
+ opacity: 1,
54
+ transparent: false,
55
+ side: 'front',
56
+ },
57
+ },
58
+ })
59
+ setActivePaintMaterial({
60
+ materialPreset: toSceneMaterialRef(id),
61
+ sourceTarget: useEditor.getState().activePaintTarget,
62
+ })
63
+ return id
64
+ }
65
+ const selectMaterial = (materialPreset: string) =>
66
+ setActivePaintMaterial({ materialPreset, sourceTarget: useEditor.getState().activePaintTarget })
67
+ return {
68
+ activePaintMaterial,
69
+ activePaintTarget,
70
+ setActivePaintMaterial,
71
+ paintEraser,
72
+ setPaintEraser,
73
+ materials,
74
+ materialCount: Object.keys(materials).length,
75
+ canResetSelection,
76
+ resetSelection,
77
+ createCustomMaterial,
78
+ selectMaterial,
79
+ }
80
+ }
@@ -21,6 +21,8 @@ import {
21
21
  type RoofSegmentNode,
22
22
  type RoofSegmentSurfaceMaterialRole,
23
23
  type RoofSurfaceMaterialRole,
24
+ type SceneMaterial,
25
+ type SceneMaterialId,
24
26
  type ShelfNode,
25
27
  type SlabNode,
26
28
  type StairNode,
@@ -243,6 +245,7 @@ export function getEffectiveChimneyMaterial(
243
245
  }
244
246
 
245
247
  export function resolveActivePaintMaterialFromSelection(params: {
248
+ materials?: Record<SceneMaterialId, SceneMaterial>
246
249
  nodes: Record<string, any>
247
250
  selectedId: string | null
248
251
  selectedMaterialTarget: {
@@ -274,6 +277,7 @@ export function resolveActivePaintMaterialFromSelection(params: {
274
277
  node: selectedNode,
275
278
  role: selectedMaterialTarget.role as string,
276
279
  nodes,
280
+ materials: params.materials,
277
281
  })
278
282
  if (surface) {
279
283
  const sourceTarget = (paintCap.materialTarget ?? selectedNode.type) as PaintableMaterialTarget