@pascal-app/editor 1.0.3 → 1.0.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (624) hide show
  1. package/README.md +106 -0
  2. package/package.json +42 -38
  3. package/src/components/editor/__bench__/room-label-placement.bench.ts +108 -0
  4. package/src/components/editor/action-menu-button.tsx +83 -0
  5. package/src/components/editor/bake-thumbnail.tsx +307 -5
  6. package/src/components/editor/capture-level-picker.tsx +72 -0
  7. package/src/components/editor/construction-reveal-audio.test.tsx +150 -0
  8. package/src/components/editor/construction-reveal-audio.tsx +40 -0
  9. package/src/components/editor/custom-camera-controls.tsx +275 -57
  10. package/src/components/editor/delete-confirmation-dialog.tsx +297 -7
  11. package/src/components/editor/duplicate-node.ts +244 -0
  12. package/src/components/editor/editor-layout-mobile.tsx +37 -26
  13. package/src/components/editor/editor-layout-v2.tsx +132 -59
  14. package/src/components/editor/export-manager.tsx +47 -26
  15. package/src/components/editor/first-person/build-collider-world.test.ts +320 -0
  16. package/src/components/editor/first-person/build-collider-world.ts +28 -7
  17. package/src/components/editor/first-person/ground-holes.test.ts +128 -0
  18. package/src/components/editor/first-person/ground-holes.ts +130 -0
  19. package/src/components/editor/first-person/mechanism-targets.test.ts +162 -0
  20. package/src/components/editor/first-person/mechanism-targets.ts +88 -0
  21. package/src/components/editor/first-person-controls.tsx +220 -9
  22. package/src/components/editor/floating-action-menu.tsx +63 -207
  23. package/src/components/editor/floor-aware-camera-controls.test.ts +466 -0
  24. package/src/components/editor/floor-aware-camera-controls.ts +324 -0
  25. package/src/components/editor/floor-openings-overlay.tsx +243 -0
  26. package/src/components/editor/floor-region-controls.tsx +846 -0
  27. package/src/components/editor/floor-region-webgpu-materials.test.ts +12 -0
  28. package/src/components/editor/floor-surface.test.ts +165 -0
  29. package/src/components/editor/floor-surface.ts +182 -0
  30. package/src/components/editor/floorplan-background-selection.test.ts +11 -2
  31. package/src/components/editor/floorplan-background-selection.ts +1 -3
  32. package/src/components/editor/floorplan-mode-coordinator.tsx +2 -2
  33. package/src/components/editor/floorplan-panel.tsx +414 -363
  34. package/src/components/editor/follow-pascal.test.tsx +912 -0
  35. package/src/components/editor/follow-pascal.tsx +354 -0
  36. package/src/components/editor/footprint-height-handle.tsx +204 -0
  37. package/src/components/editor/group-actions.ts +56 -2
  38. package/src/components/editor/group-move-3d.ts +2 -0
  39. package/src/components/editor/group-rotate-handle.tsx +2 -0
  40. package/src/components/editor/group-transform-shared.test.ts +460 -0
  41. package/src/components/editor/handles/handle-hit-priority.test.tsx +63 -0
  42. package/src/components/editor/handles/handle-hit-priority.tsx +37 -0
  43. package/src/components/editor/handles/swallow-next-click.test.ts +28 -0
  44. package/src/components/editor/handles/use-handle-drag.ts +269 -197
  45. package/src/components/editor/index.tsx +409 -197
  46. package/src/components/editor/join-walls-pill.tsx +44 -0
  47. package/src/components/editor/node-action-menu.tsx +68 -70
  48. package/src/components/editor/node-arrow-handles.tsx +25 -6
  49. package/src/components/editor/open-wall-ends-3d-layer.tsx +273 -0
  50. package/src/components/editor/open-wall-ends-hint.tsx +89 -0
  51. package/src/components/editor/quick-measurement-hud.tsx +3 -2
  52. package/src/components/editor/registry-action-contributions.test.tsx +63 -0
  53. package/src/components/editor/registry-action-contributions.tsx +80 -1
  54. package/src/components/editor/riser-diagram-panel.tsx +14 -7
  55. package/src/components/editor/room-controls.tsx +847 -0
  56. package/src/components/editor/room-floor-highlight.tsx +128 -0
  57. package/src/components/editor/room-handles-2d.tsx +354 -0
  58. package/src/components/editor/room-handles.tsx +675 -0
  59. package/src/components/editor/room-highlight.tsx +233 -0
  60. package/src/components/editor/room-labels.test.ts +850 -0
  61. package/src/components/editor/room-labels.tsx +1890 -0
  62. package/src/components/editor/room-transform-ghost.tsx +256 -0
  63. package/src/components/editor/room-webgpu-materials.test.ts +27 -0
  64. package/src/components/editor/selection-manager.tsx +474 -419
  65. package/src/components/editor/slab-hole-highlights.tsx +5 -2
  66. package/src/components/editor/snapshot-capture-overlay.tsx +7 -2
  67. package/src/components/editor/snapshot-capture.test.ts +119 -60
  68. package/src/components/editor/snapshot-capture.ts +36 -5
  69. package/src/components/editor/thumbnail-generator.test.ts +654 -0
  70. package/src/components/editor/thumbnail-generator.tsx +1103 -223
  71. package/src/components/editor/use-floorplan-background-placement.ts +34 -12
  72. package/src/components/editor/vector-edge-extractor.tsx +121 -0
  73. package/src/components/editor/views/host-view.tsx +61 -0
  74. package/src/components/editor/views/plugins-view.tsx +663 -0
  75. package/src/components/editor/views/scene-region.tsx +59 -0
  76. package/src/components/editor/views/use-editor-views.ts +57 -0
  77. package/src/components/editor/views/view-bar.test.tsx +54 -0
  78. package/src/components/editor/views/view-bar.tsx +343 -0
  79. package/src/components/editor/wall-justification.test.ts +140 -0
  80. package/src/components/editor/wall-move-side-handles.tsx +43 -142
  81. package/src/components/editor/wall-push-arrow.tsx +92 -0
  82. package/src/components/editor/wall-region-handles.tsx +509 -0
  83. package/src/components/editor/wall-snap-beacon-layer.tsx +8 -4
  84. package/src/components/editor-2d/floorplan-cursor-indicator-overlay.tsx +8 -4
  85. package/src/components/editor-2d/floorplan-group-move.tsx +3 -0
  86. package/src/components/editor-2d/floorplan-open-wall-ends-layer.test.tsx +86 -0
  87. package/src/components/editor-2d/floorplan-open-wall-ends-layer.tsx +290 -0
  88. package/src/components/editor-2d/floorplan-registered-tool-layer.tsx +2 -2
  89. package/src/components/editor-2d/floorplan-registry-action-menu.tsx +11 -1
  90. package/src/components/editor-2d/floorplan-registry-move-overlay.tsx +116 -106
  91. package/src/components/editor-2d/renderers/floorplan-dimension-edit-overlay.test.ts +156 -0
  92. package/src/components/editor-2d/renderers/floorplan-dimension-edit-overlay.tsx +278 -0
  93. package/src/components/editor-2d/renderers/floorplan-dimension-renderer.tsx +77 -0
  94. package/src/components/editor-2d/renderers/{floorplan-draft-wall-measurement.tsx → floorplan-draft-measurement.tsx} +8 -8
  95. package/src/components/editor-2d/renderers/floorplan-geometry-renderer.tsx +4 -1
  96. package/src/components/editor-2d/renderers/floorplan-registry-layer.test.ts +38 -8
  97. package/src/components/editor-2d/renderers/floorplan-registry-layer.tsx +319 -233
  98. package/src/components/editor-2d/renderers/floorplan-reveal.test.tsx +254 -0
  99. package/src/components/editor-2d/renderers/floorplan-reveal.ts +141 -0
  100. package/src/components/editor-2d/svg-paths.ts +12 -15
  101. package/src/components/point-ask/bubble.tsx +538 -0
  102. package/src/components/point-ask/hover-chip.tsx +91 -0
  103. package/src/components/point-ask/mode-pill.tsx +92 -0
  104. package/src/components/point-ask/optional-motion.ts +22 -0
  105. package/src/components/point-ask/outline-corners.tsx +93 -0
  106. package/src/components/point-ask/pins.tsx +711 -0
  107. package/src/components/point-ask/point-ask-layer.tsx +83 -0
  108. package/src/components/point-ask/point-ask-styles.ts +105 -0
  109. package/src/components/point-ask/region-marquee-styles.ts +15 -0
  110. package/src/components/point-ask/region-marquee.tsx +64 -0
  111. package/src/components/procedural-items/inspector.tsx +40 -15
  112. package/src/components/systems/ceiling/ceiling-bracket-batch.test.ts +12 -2
  113. package/src/components/systems/ceiling/ceiling-selection-affordance-system.test.ts +97 -14
  114. package/src/components/systems/ceiling/ceiling-selection-affordance-system.tsx +18 -18
  115. package/src/components/systems/roof/roof-edit-system.tsx +1 -0
  116. package/src/components/systems/zone/zone-system.test.ts +47 -0
  117. package/src/components/systems/zone/zone-system.tsx +29 -68
  118. package/src/components/tools/distribution/connection-compatibility.test.ts +34 -0
  119. package/src/components/tools/distribution/connection-compatibility.ts +43 -0
  120. package/src/components/tools/distribution/distribution-public.ts +9 -0
  121. package/src/components/tools/distribution/distribution-run-contract.test.ts +26 -0
  122. package/src/components/tools/distribution/distribution-run-contract.ts +94 -0
  123. package/src/components/tools/distribution/distribution-run-tool.test.ts +241 -0
  124. package/src/components/tools/distribution/distribution-run-tool.test.tsx +125 -0
  125. package/src/components/tools/distribution/distribution-run-tool.tsx +1323 -0
  126. package/src/components/tools/distribution/draw-alignment.ts +35 -0
  127. package/src/components/tools/distribution/ports.test.ts +56 -0
  128. package/src/components/tools/distribution/ports.ts +378 -0
  129. package/src/components/tools/distribution/run-cursor.test.ts +135 -0
  130. package/src/components/tools/distribution/run-cursor.ts +36 -0
  131. package/src/components/tools/distribution/run-direction-feedback.test.ts +60 -0
  132. package/src/components/tools/distribution/run-direction-feedback.tsx +251 -0
  133. package/src/components/tools/distribution/run-port-snap.test.ts +79 -0
  134. package/src/components/tools/distribution/run-port-snap.ts +25 -0
  135. package/src/components/tools/distribution/run-screen-direction.test.ts +23 -0
  136. package/src/components/tools/distribution/run-screen-direction.ts +30 -0
  137. package/src/components/tools/elevator/elevator-tool.tsx +3 -6
  138. package/src/components/tools/elevator/move-elevator-tool.tsx +2 -3
  139. package/src/components/tools/item/item-surface-strategy-resolver.test.ts +67 -68
  140. package/src/components/tools/item/placement-history.test.tsx +512 -0
  141. package/src/components/tools/item/placement-strategies.test.ts +46 -1
  142. package/src/components/tools/item/placement-strategies.ts +11 -10
  143. package/src/components/tools/item/shelf-surface-strategy-resolver.test.ts +56 -56
  144. package/src/components/tools/item/use-draft-node.test.tsx +432 -1
  145. package/src/components/tools/item/use-draft-node.ts +166 -95
  146. package/src/components/tools/item/use-placement-coordinator.tsx +148 -95
  147. package/src/components/tools/paint-region/use-paint-tint.ts +37 -0
  148. package/src/components/tools/paint-region/wall-paint-region-tool.tsx +466 -0
  149. package/src/components/tools/registry/item-surface-move.test.ts +62 -63
  150. package/src/components/tools/registry/item-surface-move.ts +2 -0
  151. package/src/components/tools/registry/item-surface-resolver.test.ts +26 -26
  152. package/src/components/tools/registry/move-registry-node-tool.test.ts +268 -1
  153. package/src/components/tools/registry/move-registry-node-tool.tsx +66 -30
  154. package/src/components/tools/select/box-select-tool.tsx +2 -12
  155. package/src/components/tools/select/plane-box-select-tool.test.tsx +79 -0
  156. package/src/components/tools/select/plane-box-select-tool.tsx +35 -22
  157. package/src/components/tools/select/select-candidates.test.ts +37 -4
  158. package/src/components/tools/select/select-candidates.ts +15 -11
  159. package/src/components/tools/shared/cursor-sphere.tsx +36 -12
  160. package/src/components/tools/site/site-boundary-editor.tsx +1 -2
  161. package/src/components/tools/tool-manager.tsx +59 -24
  162. package/src/components/tools/wall/wall-drafting.test.ts +175 -47
  163. package/src/components/tools/zone/zone-tool.tsx +4 -12
  164. package/src/components/ui/action-menu/action-button.tsx +9 -1
  165. package/src/components/ui/action-menu/camera-actions.tsx +9 -4
  166. package/src/components/ui/action-menu/control-modes.tsx +15 -42
  167. package/src/components/ui/action-menu/follow-pascal-pill.tsx +32 -0
  168. package/src/components/ui/action-menu/follow-pascal-toggle.tsx +36 -0
  169. package/src/components/ui/action-menu/index.tsx +56 -33
  170. package/src/components/ui/action-menu/measurement-control.tsx +14 -19
  171. package/src/components/ui/action-menu/placement.ts +20 -0
  172. package/src/components/ui/action-menu/point-ask-button.tsx +52 -0
  173. package/src/components/ui/action-menu/view-toggles.tsx +47 -42
  174. package/src/components/ui/build-panel/build-panel.tsx +242 -0
  175. package/src/components/ui/command-palette/editor-commands.tsx +7 -42
  176. package/src/components/ui/command-palette/index.tsx +50 -39
  177. package/src/components/ui/command-palette/ranked-groups.test.ts +31 -0
  178. package/src/components/ui/controls/action-button.tsx +5 -2
  179. package/src/components/ui/controls/finish-swatch.tsx +110 -0
  180. package/src/components/ui/controls/material-paint-panel.tsx +97 -26
  181. package/src/components/ui/controls/material-picker.tsx +4 -1
  182. package/src/components/ui/controls/metric-control.tsx +1 -1
  183. package/src/components/ui/controls/scene-material-list.tsx +4 -32
  184. package/src/components/ui/controls/segmented-control.tsx +21 -8
  185. package/src/components/ui/controls/select-control.tsx +65 -0
  186. package/src/components/ui/controls/slider-control.tsx +1 -1
  187. package/src/components/ui/controls/toggle-control.tsx +3 -3
  188. package/src/components/ui/controls/tool-options-panel.tsx +1 -1
  189. package/src/components/ui/floating-level-selector.tsx +159 -59
  190. package/src/components/ui/helpers/building-helper.tsx +4 -1
  191. package/src/components/ui/helpers/contextual-helper-panel.tsx +181 -72
  192. package/src/components/ui/helpers/helper-manager.test.tsx +121 -0
  193. package/src/components/ui/helpers/helper-manager.tsx +331 -14
  194. package/src/components/ui/helpers/item-helper.tsx +17 -1
  195. package/src/components/ui/helpers/registered-tool-helper.tsx +8 -1
  196. package/src/components/ui/icon-ref.tsx +45 -1
  197. package/src/components/ui/level-duplicate-dialog.tsx +1 -1
  198. package/src/components/ui/node-icon-image.tsx +16 -0
  199. package/src/components/ui/panels/collections/collections-popover.tsx +28 -22
  200. package/src/components/ui/panels/floor-foundation-panel.tsx +551 -0
  201. package/src/components/ui/panels/floor-opening-panel.tsx +143 -0
  202. package/src/components/ui/panels/mobile-panel-sheet.tsx +4 -4
  203. package/src/components/ui/panels/mobile-selection-bar.tsx +8 -5
  204. package/src/components/ui/panels/multi-field-value.test.ts +26 -3
  205. package/src/components/ui/panels/multi-field-value.ts +6 -3
  206. package/src/components/ui/panels/multi-parametric-inspector.tsx +52 -2
  207. package/src/components/ui/panels/multi-selection-panel.tsx +161 -5
  208. package/src/components/ui/panels/node-display.ts +8 -3
  209. package/src/components/ui/panels/panel-manager.tsx +103 -68
  210. package/src/components/ui/panels/panel-wrapper.tsx +130 -62
  211. package/src/components/ui/panels/parametric-field-control.tsx +3 -3
  212. package/src/components/ui/panels/parametric-inspector.tsx +7 -7
  213. package/src/components/ui/panels/reference-panel.tsx +2 -3
  214. package/src/components/ui/panels/room-construction-rows.tsx +774 -0
  215. package/src/components/ui/panels/room-name-options.test.ts +55 -0
  216. package/src/components/ui/panels/room-name-options.ts +47 -0
  217. package/src/components/ui/panels/room-openings-field.tsx +110 -0
  218. package/src/components/ui/panels/room-panel.tsx +790 -0
  219. package/src/components/ui/panels/shape-choice.tsx +58 -0
  220. package/src/components/ui/panels/wall-region-list.tsx +109 -0
  221. package/src/components/ui/primitives/badge.tsx +8 -0
  222. package/src/components/ui/primitives/color-dot.tsx +36 -24
  223. package/src/components/ui/primitives/sidebar.tsx +3 -1
  224. package/src/components/ui/right-stack.test.ts +29 -0
  225. package/src/components/ui/right-stack.tsx +164 -0
  226. package/src/components/ui/sidebar/app-sidebar.tsx +1 -1
  227. package/src/components/ui/sidebar/icon-rail.tsx +8 -3
  228. package/src/components/ui/sidebar/panels/items-panel/function-tree-panel.tsx +1 -1
  229. package/src/components/ui/sidebar/panels/items-panel/index.tsx +6 -6
  230. package/src/components/ui/sidebar/panels/settings-panel/animation-speed-setting.tsx +40 -0
  231. package/src/components/ui/sidebar/panels/settings-panel/audio-settings-dialog.tsx +7 -8
  232. package/src/components/ui/sidebar/panels/settings-panel/construction-animation-setting.test.tsx +170 -0
  233. package/src/components/ui/sidebar/panels/settings-panel/construction-animation-setting.tsx +92 -0
  234. package/src/components/ui/sidebar/panels/settings-panel/index.tsx +138 -27
  235. package/src/components/ui/sidebar/panels/settings-panel/interface-theme.test.tsx +20 -0
  236. package/src/components/ui/sidebar/panels/settings-panel/keyboard-shortcuts-dialog.tsx +5 -5
  237. package/src/components/ui/sidebar/panels/settings-panel/print-export-button.tsx +4 -10
  238. package/src/components/ui/sidebar/panels/site-panel/building-tree-node.tsx +3 -11
  239. package/src/components/ui/sidebar/panels/site-panel/ceiling-tree-node.tsx +4 -4
  240. package/src/components/ui/sidebar/panels/site-panel/chimney-tree-node.tsx +1 -5
  241. package/src/components/ui/sidebar/panels/site-panel/collection-tree-node.tsx +197 -0
  242. package/src/components/ui/sidebar/panels/site-panel/column-tree-node.tsx +1 -5
  243. package/src/components/ui/sidebar/panels/site-panel/door-tree-node.tsx +1 -5
  244. package/src/components/ui/sidebar/panels/site-panel/dormer-tree-node.tsx +1 -5
  245. package/src/components/ui/sidebar/panels/site-panel/elevator-tree-node.tsx +1 -5
  246. package/src/components/ui/sidebar/panels/site-panel/fence-tree-node.tsx +24 -11
  247. package/src/components/ui/sidebar/panels/site-panel/gutter-tree-node.tsx +1 -5
  248. package/src/components/ui/sidebar/panels/site-panel/index.tsx +181 -223
  249. package/src/components/ui/sidebar/panels/site-panel/inline-rename-input.tsx +30 -8
  250. package/src/components/ui/sidebar/panels/site-panel/item-tree-node.tsx +1 -3
  251. package/src/components/ui/sidebar/panels/site-panel/level-tree-node.tsx +4 -1
  252. package/src/components/ui/sidebar/panels/site-panel/registry-tree-node.tsx +1 -3
  253. package/src/components/ui/sidebar/panels/site-panel/roof-tree-node.tsx +1 -4
  254. package/src/components/ui/sidebar/panels/site-panel/shelf-tree-node.tsx +1 -3
  255. package/src/components/ui/sidebar/panels/site-panel/slab-tree-node.tsx +21 -13
  256. package/src/components/ui/sidebar/panels/site-panel/solar-panel-tree-node.tsx +1 -5
  257. package/src/components/ui/sidebar/panels/site-panel/spawn-tree-node.tsx +1 -5
  258. package/src/components/ui/sidebar/panels/site-panel/stair-tree-node.tsx +1 -5
  259. package/src/components/ui/sidebar/panels/site-panel/tree-node.tsx +13 -37
  260. package/src/components/ui/sidebar/panels/site-panel/unit-tree-node.tsx +2 -3
  261. package/src/components/ui/sidebar/panels/site-panel/wall-tree-node.tsx +1 -5
  262. package/src/components/ui/sidebar/panels/site-panel/window-tree-node.tsx +1 -5
  263. package/src/components/ui/sidebar/panels/site-panel/zone-tree-node.tsx +9 -4
  264. package/src/components/ui/sidebar/panels/zone-panel/index.tsx +2 -4
  265. package/src/components/ui/sidebar/tab-bar.tsx +14 -4
  266. package/src/components/ui/sidebar/use-plugin-panels.tsx +15 -5
  267. package/src/components/viewer/floorplan-compass-button.tsx +9 -1
  268. package/src/components/viewer/floorplan-preview.tsx +5 -6
  269. package/src/components/viewer/viewer-controls-bar.tsx +18 -6
  270. package/src/components/viewer/viewer-hover-label.tsx +34 -0
  271. package/src/components/viewer/viewer-rooms.tsx +191 -0
  272. package/src/components/viewer/viewer-scene-header.tsx +181 -145
  273. package/src/components/viewer/viewer-selection-manager.tsx +124 -0
  274. package/src/components/viewer/viewer-stage.tsx +2 -2
  275. package/src/components/viewer-zone-system.tsx +3 -8
  276. package/src/hooks/use-auto-save.integration.test.tsx +215 -43
  277. package/src/hooks/use-auto-save.ts +60 -9
  278. package/src/hooks/use-ceiling-events.test.ts +123 -118
  279. package/src/hooks/use-grid-events.test.ts +105 -105
  280. package/src/hooks/use-keyboard.test.ts +243 -19
  281. package/src/hooks/use-keyboard.ts +118 -83
  282. package/src/hooks/use-open-wall-ends.test.ts +99 -0
  283. package/src/hooks/use-open-wall-ends.ts +238 -0
  284. package/src/hooks/use-placement-notice.test.ts +71 -0
  285. package/src/hooks/use-placement-notice.ts +73 -0
  286. package/src/hooks/use-selected-room.ts +143 -0
  287. package/src/index.tsx +312 -58
  288. package/src/lib/__fixtures__/owner-victor-seattle-parking.json +1 -0
  289. package/src/lib/__fixtures__/owner-villa-aurelia-basement.json +1 -0
  290. package/src/lib/add-storey.test.ts +177 -0
  291. package/src/lib/add-storey.ts +59 -0
  292. package/src/lib/below-grade.test.tsx +171 -0
  293. package/src/lib/below-grade.ts +133 -0
  294. package/src/lib/camera-facing.test.ts +24 -0
  295. package/src/lib/camera-facing.ts +24 -0
  296. package/src/lib/ceiling-edit-session.test.ts +321 -0
  297. package/src/lib/ceiling-edit-session.ts +206 -0
  298. package/src/lib/ceiling-region-session.test.ts +179 -0
  299. package/src/lib/continuation.test.ts +19 -1
  300. package/src/lib/continuation.ts +25 -4
  301. package/src/lib/direct-manipulation.ts +2 -0
  302. package/src/lib/door-interaction.ts +14 -0
  303. package/src/lib/download-project-file.ts +17 -0
  304. package/src/lib/editor-level-display.test.ts +96 -0
  305. package/src/lib/editor-level-display.ts +55 -0
  306. package/src/lib/editor-views.ts +178 -0
  307. package/src/lib/export-geometry-wait.test.ts +90 -0
  308. package/src/lib/export-texture-utils.ts +2 -4
  309. package/src/lib/floor-edit-session.ts +153 -0
  310. package/src/lib/floor-footprints.test.ts +563 -0
  311. package/src/lib/floor-footprints.ts +395 -0
  312. package/src/lib/floor-foundation-support.test.ts +284 -0
  313. package/src/lib/floor-opening-draft.ts +218 -0
  314. package/src/lib/floor-openings.test.ts +216 -0
  315. package/src/lib/floor-region-geometry.ts +191 -0
  316. package/src/lib/floor-region-session.test.ts +485 -0
  317. package/src/lib/floor-region-session.ts +589 -0
  318. package/src/lib/floor-region-snap.ts +101 -0
  319. package/src/lib/floor-step-hover.test.ts +32 -0
  320. package/src/lib/floorplan/annotation-visibility.ts +17 -0
  321. package/src/lib/floorplan/dimension-drive.test.ts +360 -0
  322. package/src/lib/floorplan/dimension-drive.ts +479 -0
  323. package/src/lib/floorplan/floorplan-export.test.ts +12 -8
  324. package/src/lib/floorplan/floorplan-export.tsx +32 -8
  325. package/src/lib/floorplan/floorplan-extension.ts +32 -0
  326. package/src/lib/floorplan/floorplan-mode.test.ts +4 -0
  327. package/src/lib/floorplan/floorplan-mode.ts +2 -0
  328. package/src/lib/floorplan/floorplan-pdfkit-document.ts +363 -27
  329. package/src/lib/floorplan/floorplan-pdfkit-renderer.test.ts +177 -8
  330. package/src/lib/floorplan/floorplan-pdfkit-renderer.ts +60 -25
  331. package/src/lib/floorplan/floorplan-plugin-install.test.tsx +138 -0
  332. package/src/lib/floorplan/floorplan-readonly.ts +6 -0
  333. package/src/lib/floorplan/geometry.ts +13 -6
  334. package/src/lib/floorplan/index.ts +0 -9
  335. package/src/lib/floorplan/items.ts +5 -2
  336. package/src/lib/floorplan/marks.test.ts +213 -0
  337. package/src/lib/floorplan/marks.ts +238 -0
  338. package/src/lib/floorplan/open-wall-ends.test.ts +268 -0
  339. package/src/lib/floorplan/open-wall-ends.ts +162 -0
  340. package/src/lib/floorplan/pdf-fonts/GeistMono-Bold.ttf +0 -0
  341. package/src/lib/floorplan/pdf-fonts/GeistMono-Regular.ttf +0 -0
  342. package/src/lib/floorplan/pdf-fonts/LICENSE-GeistMono.txt +92 -0
  343. package/src/lib/floorplan/pdf-fonts/LICENSE-LiberationSans.txt +102 -0
  344. package/src/lib/floorplan/pdf-fonts/LiberationSans-Bold.ttf +0 -0
  345. package/src/lib/floorplan/pdf-fonts/LiberationSans-Regular.ttf +0 -0
  346. package/src/lib/floorplan/pdfkit-standard-fonts.d.ts +5 -0
  347. package/src/lib/floorplan/schedules.test.ts +189 -0
  348. package/src/lib/floorplan/schedules.ts +339 -0
  349. package/src/lib/floorplan/sheet-export.test.ts +124 -0
  350. package/src/lib/floorplan/sheet-export.ts +318 -0
  351. package/src/lib/floorplan/site-plan/build-site-plan-drawing.ts +768 -0
  352. package/src/lib/floorplan/site-plan/contours.test.ts +43 -0
  353. package/src/lib/floorplan/site-plan/contributors.ts +97 -0
  354. package/src/lib/floorplan/site-plan/coverage.ts +230 -0
  355. package/src/lib/floorplan/site-plan/drawing-type-switch.test.ts +15 -0
  356. package/src/lib/floorplan/site-plan/drawing-type-switch.tsx +86 -0
  357. package/src/lib/floorplan/site-plan/front-edge.test.ts +103 -0
  358. package/src/lib/floorplan/site-plan/front-edge.ts +157 -0
  359. package/src/lib/floorplan/site-plan/geometry.test.ts +269 -0
  360. package/src/lib/floorplan/site-plan/geometry.ts +142 -0
  361. package/src/lib/floorplan/site-plan/index.ts +55 -0
  362. package/src/lib/floorplan/site-plan/site-annotations.test.ts +483 -0
  363. package/src/lib/floorplan/site-plan/site-annotations.ts +1084 -0
  364. package/src/lib/floorplan/site-plan/site-parts.ts +287 -0
  365. package/src/lib/floorplan/site-plan/site-plan-layer.tsx +138 -0
  366. package/src/lib/floorplan/site-plan/street-name.test.ts +69 -0
  367. package/src/lib/floorplan/site-plan/street-name.ts +302 -0
  368. package/src/lib/floorplan/site-plan/yards.test.ts +70 -0
  369. package/src/lib/floorplan/types.ts +1 -27
  370. package/src/lib/follow-pascal-exterior.test-fixture.ts +17 -0
  371. package/src/lib/follow-pascal-exterior.test.ts +171 -0
  372. package/src/lib/follow-pascal-exterior.ts +39 -0
  373. package/src/lib/follow-pascal-presentation.test.ts +203 -0
  374. package/src/lib/follow-pascal-presentation.ts +129 -0
  375. package/src/lib/follow-pascal-sight.ts +84 -0
  376. package/src/lib/follow-pascal-targets.test.ts +406 -0
  377. package/src/lib/follow-pascal-targets.ts +198 -0
  378. package/src/lib/follow-pascal.test.ts +350 -0
  379. package/src/lib/follow-pascal.ts +284 -0
  380. package/src/lib/fresh-planar-placement.test.ts +148 -0
  381. package/src/lib/fresh-planar-placement.ts +114 -37
  382. package/src/lib/geometry-script/author.ts +157 -0
  383. package/src/lib/geometry-script/client.ts +159 -0
  384. package/src/lib/geometry-script/finish-scripts.ts +127 -0
  385. package/src/lib/geometry-script/protocol.ts +13 -0
  386. package/src/lib/gesture-lifecycle.test.ts +595 -0
  387. package/src/lib/gesture-lifecycle.ts +290 -0
  388. package/src/lib/glb-export-zone-holes.test.ts +41 -0
  389. package/src/lib/glb-export.test.ts +636 -126
  390. package/src/lib/glb-export.ts +241 -120
  391. package/src/lib/history.test.ts +18 -7
  392. package/src/lib/history.ts +7 -0
  393. package/src/lib/hud-title.ts +109 -0
  394. package/src/lib/hydration-gated-storage.ts +49 -0
  395. package/src/lib/ifc-export.test.ts +174 -0
  396. package/src/lib/ifc-export.ts +201 -0
  397. package/src/lib/ifc-parts.ts +15 -0
  398. package/src/lib/inspector-expanded.test.ts +44 -0
  399. package/src/lib/inspector-expanded.ts +36 -0
  400. package/src/lib/interaction/scope.ts +22 -0
  401. package/src/lib/keyboard-pan.test.ts +129 -7
  402. package/src/lib/keyboard-pan.ts +161 -12
  403. package/src/lib/level-selection.test.ts +57 -0
  404. package/src/lib/level-selection.ts +16 -1
  405. package/src/lib/live-gesture-round-trip.test.ts +360 -0
  406. package/src/lib/lot/__fixtures__/map-dossier-miami-shores.json +1 -0
  407. package/src/lib/lot/__fixtures__/map-dossier-stpete.json +1 -0
  408. package/src/lib/lot/clean-ring.test.ts +187 -0
  409. package/src/lib/lot/clean-ring.ts +217 -0
  410. package/src/lib/lot/dossier.test.ts +202 -0
  411. package/src/lib/lot/dossier.ts +498 -0
  412. package/src/lib/lot/drop-in.test.ts +326 -0
  413. package/src/lib/lot/drop-in.ts +392 -0
  414. package/src/lib/lot/index.ts +43 -0
  415. package/src/lib/lot/lot-patch.test.ts +340 -0
  416. package/src/lib/lot/lot-patch.ts +394 -0
  417. package/src/lib/lot/parcel-provider.ts +43 -0
  418. package/src/lib/lot/terrain.test.ts +161 -0
  419. package/src/lib/lot/terrain.ts +266 -0
  420. package/src/lib/material-catalog-model.ts +3 -1
  421. package/src/lib/material-paint-panel-model.ts +9 -5
  422. package/src/lib/material-paint.ts +4 -0
  423. package/src/lib/measurement-kind.test.ts +6 -8
  424. package/src/lib/measurements.test.ts +23 -0
  425. package/src/lib/mezzanine-actions.test.ts +342 -0
  426. package/src/lib/mezzanine-draft.test.ts +501 -0
  427. package/src/lib/mezzanine-draft.ts +278 -0
  428. package/src/lib/mezzanine-messages.ts +26 -0
  429. package/src/lib/mezzanine-selection.test.ts +207 -0
  430. package/src/lib/mezzanine-stairs.ts +46 -0
  431. package/src/lib/model-export.ts +3 -1
  432. package/src/lib/node-action-movement.ts +17 -6
  433. package/src/lib/node-display-label.ts +37 -0
  434. package/src/lib/orbit-floor-clearance.test.ts +201 -0
  435. package/src/lib/orbit-floor-clearance.ts +154 -0
  436. package/src/lib/paint-commit-route.test.ts +37 -0
  437. package/src/lib/paint-commit-route.ts +17 -0
  438. package/src/lib/paint-eyedropper.test.tsx +165 -0
  439. package/src/lib/paint-eyedropper.ts +99 -0
  440. package/src/lib/paint-pass-through.test.ts +122 -0
  441. package/src/lib/paint-pass-through.ts +78 -0
  442. package/src/lib/paint-region-hover.ts +25 -0
  443. package/src/lib/paint-region-mode.ts +154 -0
  444. package/src/lib/paint-regions.test.ts +166 -0
  445. package/src/lib/paint-regions.ts +192 -0
  446. package/src/lib/paint-scope.test.ts +317 -35
  447. package/src/lib/paint-scope.ts +267 -25
  448. package/src/lib/panel-rows.ts +21 -0
  449. package/src/lib/parametric-node-update.ts +16 -7
  450. package/src/lib/plate-paint-affected.test.ts +300 -0
  451. package/src/lib/plate-paint-affected.ts +177 -0
  452. package/src/lib/plugin-chat.ts +65 -0
  453. package/src/lib/plugin-panels.test.ts +25 -0
  454. package/src/lib/plugin-panels.ts +94 -0
  455. package/src/lib/point-ask/__fixtures__/consumer-browser.tsx +76 -0
  456. package/src/lib/point-ask/__fixtures__/house.ts +81 -0
  457. package/src/lib/point-ask/ask-created.test.ts +195 -0
  458. package/src/lib/point-ask/ask-flow.test.ts +380 -0
  459. package/src/lib/point-ask/build-context.test.ts +173 -0
  460. package/src/lib/point-ask/build-context.ts +94 -0
  461. package/src/lib/point-ask/capture-crop.ts +129 -0
  462. package/src/lib/point-ask/choreography.ts +69 -0
  463. package/src/lib/point-ask/compare.test.ts +145 -0
  464. package/src/lib/point-ask/compare.ts +73 -0
  465. package/src/lib/point-ask/describe-target.test.ts +165 -0
  466. package/src/lib/point-ask/describe-target.ts +234 -0
  467. package/src/lib/point-ask/dictation.ts +79 -0
  468. package/src/lib/point-ask/edge-marker.test.ts +61 -0
  469. package/src/lib/point-ask/edge-marker.ts +27 -0
  470. package/src/lib/point-ask/frame-loop.ts +25 -0
  471. package/src/lib/point-ask/history-marks.test.ts +94 -0
  472. package/src/lib/point-ask/history-marks.ts +39 -0
  473. package/src/lib/point-ask/host-api.ts +32 -0
  474. package/src/lib/point-ask/outline-box.test.ts +30 -0
  475. package/src/lib/point-ask/outline-box.ts +11 -0
  476. package/src/lib/point-ask/overlay-metrics.ts +6 -0
  477. package/src/lib/point-ask/pick.test.ts +220 -0
  478. package/src/lib/point-ask/pick.ts +121 -0
  479. package/src/lib/point-ask/pin-machine.test.ts +494 -0
  480. package/src/lib/point-ask/pin-machine.ts +217 -0
  481. package/src/lib/point-ask/placement.test.ts +203 -0
  482. package/src/lib/point-ask/placement.ts +141 -0
  483. package/src/lib/point-ask/pointer-tracker.ts +34 -0
  484. package/src/lib/point-ask/projector.ts +83 -0
  485. package/src/lib/point-ask/region.test.ts +157 -0
  486. package/src/lib/point-ask/region.ts +136 -0
  487. package/src/lib/point-ask/retry.test.ts +217 -0
  488. package/src/lib/point-ask/retry.ts +37 -0
  489. package/src/lib/point-ask/scene-commit.test.ts +312 -0
  490. package/src/lib/point-ask/scene-commit.ts +32 -0
  491. package/src/lib/point-ask/session.ts +1391 -0
  492. package/src/lib/point-ask/spring-keyframes.test.ts +45 -0
  493. package/src/lib/point-ask/spring-keyframes.ts +38 -0
  494. package/src/lib/point-ask/spring.test.ts +63 -0
  495. package/src/lib/point-ask/spring.ts +107 -0
  496. package/src/lib/point-ask/suggestions.test.ts +80 -0
  497. package/src/lib/point-ask/suggestions.ts +125 -0
  498. package/src/lib/point-ask/target-scope.test.ts +622 -0
  499. package/src/lib/point-ask/target-scope.ts +338 -0
  500. package/src/lib/point-ask/types.ts +48 -0
  501. package/src/lib/portable-export.test.ts +34 -1
  502. package/src/lib/portable-export.ts +119 -53
  503. package/src/lib/print-feature-thickness.ts +10 -1
  504. package/src/lib/print-roof-solids.test.ts +39 -1
  505. package/src/lib/print-roof-solids.ts +43 -10
  506. package/src/lib/print-shell-compiler-manifold-core.ts +1 -1
  507. package/src/lib/print-shell-compiler.ts +1 -0
  508. package/src/lib/registered-draft-snap.ts +12 -0
  509. package/src/lib/roof-duplication.ts +10 -2
  510. package/src/lib/room-assembly-overlay.test.ts +275 -0
  511. package/src/lib/room-assembly-overlay.ts +265 -0
  512. package/src/lib/room-built-on.test.ts +379 -0
  513. package/src/lib/room-built-on.ts +138 -0
  514. package/src/lib/room-construction-commands.ts +712 -0
  515. package/src/lib/room-construction.test.ts +746 -0
  516. package/src/lib/room-construction.ts +385 -0
  517. package/src/lib/room-controls-phase6.test.ts +630 -0
  518. package/src/lib/room-divide-session.ts +523 -0
  519. package/src/lib/room-handle-drag.ts +1068 -0
  520. package/src/lib/room-name-catalog.test.ts +90 -0
  521. package/src/lib/room-name-catalog.ts +168 -0
  522. package/src/lib/room-push-dimensions.test.ts +311 -0
  523. package/src/lib/room-push-dimensions.ts +251 -0
  524. package/src/lib/room-selection-commands.ts +62 -0
  525. package/src/lib/room-selection.test.ts +1017 -0
  526. package/src/lib/room-selection.ts +417 -0
  527. package/src/lib/room-structure-commands.ts +94 -0
  528. package/src/lib/room-structure.test.ts +921 -0
  529. package/src/lib/room-transform-session.ts +570 -0
  530. package/src/lib/room-zone-routing.test.ts +294 -0
  531. package/src/lib/room-zone-routing.ts +197 -0
  532. package/src/lib/scene-clipboard.test.ts +610 -1
  533. package/src/lib/scene-clipboard.ts +269 -58
  534. package/src/lib/scene-material-refs.test.ts +79 -0
  535. package/src/lib/scene-material-refs.ts +79 -0
  536. package/src/lib/scene.test.ts +24 -36
  537. package/src/lib/scene.ts +33 -57
  538. package/src/lib/selection-routing.test.ts +129 -45
  539. package/src/lib/selection-routing.ts +54 -75
  540. package/src/lib/session-writes.test.ts +120 -0
  541. package/src/lib/session-writes.ts +205 -0
  542. package/src/lib/sfx-bus.ts +13 -0
  543. package/src/lib/sfx-player.test.ts +70 -9
  544. package/src/lib/sfx-player.ts +6 -0
  545. package/src/lib/sidebar-panel.ts +62 -0
  546. package/src/lib/site-boundary.test.ts +4 -6
  547. package/src/lib/site-boundary.ts +1 -1
  548. package/src/lib/snapping-mode.test.ts +4 -0
  549. package/src/lib/snapping-mode.ts +8 -1
  550. package/src/lib/stair-levels.test.ts +25 -0
  551. package/src/lib/stair-levels.ts +22 -3
  552. package/src/lib/terrace-draft.test.ts +145 -0
  553. package/src/lib/terrace-draft.ts +214 -0
  554. package/src/lib/tool-cursor-icon.test.ts +16 -0
  555. package/src/lib/tool-cursor-icon.ts +11 -0
  556. package/src/lib/typing-target.ts +9 -0
  557. package/src/lib/units.ts +77 -39
  558. package/src/lib/vector-edges.test.ts +58 -0
  559. package/src/lib/vector-edges.ts +400 -0
  560. package/src/lib/viewer-selection.test.ts +577 -0
  561. package/src/lib/viewer-selection.ts +580 -0
  562. package/src/lib/wall-draw-variant.test.ts +71 -0
  563. package/src/lib/wall-draw-variant.ts +91 -0
  564. package/src/lib/wall-paint-region-session.ts +246 -0
  565. package/src/lib/wall-paint-region.test.ts +373 -0
  566. package/src/lib/wall-placement-pointer.test.ts +42 -0
  567. package/src/lib/wall-placement-pointer.ts +57 -0
  568. package/src/lib/wall-polygon-draft.test.ts +262 -0
  569. package/src/lib/wall-polygon-draft.ts +286 -0
  570. package/src/lib/wall-push.test.ts +453 -0
  571. package/src/lib/wall-region-face.ts +352 -0
  572. package/src/lib/wall-region-handles.test.ts +304 -0
  573. package/src/lib/wall-region-handles.ts +368 -0
  574. package/src/lib/wall-region-list.test.ts +119 -0
  575. package/src/lib/wall-region-list.ts +136 -0
  576. package/src/lib/wall-region-snap.ts +110 -0
  577. package/src/lib/window-changes.ts +35 -0
  578. package/src/lib/window-interaction.ts +14 -0
  579. package/src/lib/zone-content.ts +4 -1
  580. package/src/lib/zone-name.test.ts +17 -0
  581. package/src/lib/zone-name.ts +17 -0
  582. package/src/store/opens-in-select-mode.test.ts +223 -0
  583. package/src/store/paint-mode.test.ts +188 -0
  584. package/src/store/terrain-sculpt-mode.test.ts +19 -21
  585. package/src/store/tool-mode.test.ts +137 -6
  586. package/src/store/use-audio.test.ts +146 -0
  587. package/src/store/use-audio.tsx +127 -3
  588. package/src/store/use-construction-reveal.test.ts +173 -0
  589. package/src/store/use-construction-reveal.ts +145 -0
  590. package/src/store/use-corner-snap-hint.test.ts +24 -0
  591. package/src/store/use-corner-snap-hint.ts +47 -0
  592. package/src/store/use-delete-confirmation.ts +28 -1
  593. package/src/store/use-drawing-view.test.ts +24 -3
  594. package/src/store/use-drawing-view.ts +29 -1
  595. package/src/store/use-editor.tsx +432 -321
  596. package/src/store/use-floorplan-draft-preview.ts +13 -0
  597. package/src/store/use-floorplan-marquee.ts +12 -2
  598. package/src/store/use-follow-pascal.test.ts +62 -0
  599. package/src/store/use-follow-pascal.ts +53 -0
  600. package/src/store/use-point-ask.ts +247 -0
  601. package/src/store/use-quick-measurement-hud.ts +3 -3
  602. package/src/store/use-stair-build-preview.ts +33 -5
  603. package/src/store/use-wall-move-ghosts.ts +4 -2
  604. package/src/store/use-wall-region-handles-selection.test.ts +222 -0
  605. package/src/store/use-wall-region-handles-selection.ts +139 -0
  606. package/src/store/view-layout.test.ts +201 -0
  607. package/src/store/view-layout.ts +199 -0
  608. package/src/test-utils/immediate-animation-frames.ts +16 -0
  609. package/src/test-utils/live-gesture-round-trip.ts +224 -0
  610. package/src/test-utils/render-current-markup.tsx +16 -0
  611. package/src/test-utils/selection-harness.ts +82 -0
  612. package/src/components/editor-2d/renderers/floorplan-stair-layer.tsx +0 -499
  613. package/src/components/systems/zone/zone-label-editor-system.tsx +0 -333
  614. package/src/components/tools/fence/fence-drafting.test.ts +0 -100
  615. package/src/components/tools/fence/fence-drafting.ts +0 -266
  616. package/src/components/tools/stair/stair-click-guard.test.ts +0 -58
  617. package/src/components/tools/stair/stair-click-guard.ts +0 -72
  618. package/src/components/tools/stair/stair-defaults.ts +0 -18
  619. package/src/components/tools/stair/stair-tool.tsx +0 -634
  620. package/src/components/ui/sidebar/panels/plugins-panel.tsx +0 -218
  621. package/src/components/viewer/viewer-units-panel.tsx +0 -116
  622. package/src/lib/floorplan/stairs.ts +0 -489
  623. package/src/lib/level-duplication.test.ts +0 -144
  624. package/src/lib/level-duplication.ts +0 -195
@@ -3,6 +3,7 @@ import {
3
3
  type AlignmentAnchor,
4
4
  type AnyNode,
5
5
  type AnyNodeId,
6
+ beginSceneHistoryPauseSession,
6
7
  type CeilingEvent,
7
8
  canHostSurfaceChild,
8
9
  collectAlignmentAnchors,
@@ -10,6 +11,8 @@ import {
10
11
  findLevelAncestorId,
11
12
  type GridEvent,
12
13
  getScaledDimensions,
14
+ getWallBodyCenterOffset,
15
+ getWallLocalFaceZ,
13
16
  type ItemEvent,
14
17
  movingFootprintAnchors,
15
18
  type NodeEvent,
@@ -25,7 +28,6 @@ import {
25
28
  useScene,
26
29
  useSpatialQuery,
27
30
  type WallEvent,
28
- type WallNode,
29
31
  } from '@pascal-app/core'
30
32
  import { useViewer } from '@pascal-app/viewer'
31
33
  import { Html } from '@react-three/drei'
@@ -55,8 +57,10 @@ import { sfxEmitter } from '../../../lib/sfx-bus'
55
57
  import {
56
58
  surfaceAttachmentId,
57
59
  surfaceFramePose,
58
- updateSurfaceNode,
60
+ updateSurfaceNode as writeSurfaceNode,
59
61
  } from '../../../lib/surface-attachment'
62
+ import { isTypingTarget } from '../../../lib/typing-target'
63
+ import { createWallPointerTracker } from '../../../lib/wall-placement-pointer'
60
64
  import {
61
65
  projectAlignmentGuidesWorldToActiveBuildingLocal,
62
66
  resolveAlignmentForActiveBuilding,
@@ -68,6 +72,7 @@ import useEditor, {
68
72
  isMagneticSnapActive,
69
73
  } from '../../../store/use-editor'
70
74
  import useFacingPose from '../../../store/use-facing-pose'
75
+ import { getMovingNode } from '../../../store/use-interaction-scope'
71
76
  import usePlacementPreview from '../../../store/use-placement-preview'
72
77
  import {
73
78
  createItemSurfaceGridDispatch,
@@ -99,6 +104,7 @@ import {
99
104
  import {
100
105
  getDetachedAttachmentPreviewLift,
101
106
  getGridAlignedDimensions,
107
+ getSideFromNormal,
102
108
  snapToGrid,
103
109
  snapToHalf,
104
110
  snapUpToGridStep,
@@ -116,7 +122,7 @@ import {
116
122
  } from './placement-strategies'
117
123
  import { resolveItemPlacementSurfaceNormal } from './placement-surface'
118
124
  import type { PlacementState, TransitionResult } from './placement-types'
119
- import type { DraftNodeHandle } from './use-draft-node'
125
+ import { type DraftNodeHandle, type ItemCopy, pausedDraftWrite } from './use-draft-node'
120
126
 
121
127
  const DEFAULT_DIMENSIONS: [number, number, number] = [1, 1, 1]
122
128
 
@@ -262,20 +268,20 @@ basePlaneMaterial.opacityNode = radialOpacity
262
268
 
263
269
  export interface PlacementCoordinatorConfig {
264
270
  asset: AssetInput | null
271
+ /** Scale to use when lazily creating a draft (e.g. for wall/ceiling duplicates). Defaults to [1,1,1]. */
272
+ defaultScale?: [number, number, number]
265
273
  draftNode: DraftNodeHandle
266
274
  initDraft: (gridPosition: Vector3) => void
267
- onCommitted: () => boolean
268
- onCancel?: () => void
269
275
  initialState?: PlacementState
270
- /** Scale to use when lazily creating a draft (e.g. for wall/ceiling duplicates). Defaults to [1,1,1]. */
271
- defaultScale?: [number, number, number]
272
- /** Painted slot overrides to seed onto a lazily-created draft (wall/ceiling
273
- * duplicates) so the duplicate keeps its materials. */
274
- slots?: ItemNode['slots']
276
+ onCancel?: () => void
277
+ onCommitted: () => boolean
275
278
  /** Move-mode sessions keep the grabbed item offset from the first surface hit
276
279
  * (floor / wall / ceiling / item-surface / shelf) instead of snapping the
277
280
  * item's origin under the cursor. */
278
281
  preserveDragOffset?: boolean
282
+ /** The item a duplicate copies, for a lazily-created draft (wall/ceiling
283
+ * duplicates) to keep its materials and collections. */
284
+ copied?: ItemCopy
279
285
  }
280
286
 
281
287
  export function usePlacementCoordinator(config: PlacementCoordinatorConfig): React.ReactNode {
@@ -528,7 +534,14 @@ export function usePlacementCoordinator(config: PlacementCoordinatorConfig): Rea
528
534
 
529
535
  useEffect(() => {
530
536
  if (!asset) return
531
- useScene.temporal.getState().pause()
537
+ // History is paused only around the placement's own writes, never for the whole carry:
538
+ // the draft stays out of history snapshots (core's history-drafts), so writes others make
539
+ // mid-carry record as their own steps, with space detection. In split view the 2D move
540
+ // overlay co-owns the gesture (keyed by the moving node), and the drop's commitStep lifts
541
+ // its pause too.
542
+ const movingNodeId = getMovingNode()?.id
543
+ const updateSurfaceNode: typeof writeSurfaceNode = (...args) =>
544
+ pausedDraftWrite(() => writeSurfaceNode(...args))
532
545
 
533
546
  const validators = { canPlaceOnFloor, canPlaceOnWall, canPlaceOnCeiling }
534
547
 
@@ -596,6 +609,29 @@ export function usePlacementCoordinator(config: PlacementCoordinatorConfig): Rea
596
609
  if (mesh) reconcileDraftRaycast(mesh)
597
610
  }
598
611
 
612
+ // Eat the click the browser fires after a pointer-up that committed, so it
613
+ // neither commits a second time through a surface `:click` handler nor reaches
614
+ // the grid, in select mode by then, as a click on empty ground.
615
+ const swallowNextClick = () => {
616
+ const swallow = (e: Event) => {
617
+ e.stopPropagation()
618
+ e.preventDefault()
619
+ }
620
+ window.addEventListener('click', swallow, { capture: true, once: true })
621
+ setTimeout(() => window.removeEventListener('click', swallow, { capture: true }), 300)
622
+ }
623
+ // True between a press's pointer-up and the click the browser fires after it.
624
+ // Surface `:click` events are synthesized on pointer-up (use-node-events), so a
625
+ // commit they trigger still has that click to come; a `grid:click` commit is
626
+ // that click.
627
+ let releasing = false
628
+ const onPointerUpCapture = () => {
629
+ releasing = true
630
+ }
631
+ const onClickCapture = () => {
632
+ releasing = false
633
+ }
634
+
599
635
  const finishCommittedPlacement = (
600
636
  committedId: string | null,
601
637
  wasAdopted: boolean,
@@ -608,7 +644,6 @@ export function usePlacementCoordinator(config: PlacementCoordinatorConfig): Rea
608
644
  }
609
645
 
610
646
  useAlignmentGuides.getState().clear()
611
- useScene.temporal.getState().resume()
612
647
  if (committedId) {
613
648
  useViewer.getState().setSelection({ selectedIds: [committedId as AnyNodeId] })
614
649
  }
@@ -619,6 +654,7 @@ export function usePlacementCoordinator(config: PlacementCoordinatorConfig): Rea
619
654
  // lands on the just-placed (now selected) node instead of a tool-less build
620
655
  // limbo. Repeat placements took the early return above and stay armed.
621
656
  useEditor.getState().setMode('select')
657
+ if (releasing) swallowNextClick()
622
658
  }
623
659
 
624
660
  const commitDraft = (
@@ -631,14 +667,22 @@ export function usePlacementCoordinator(config: PlacementCoordinatorConfig): Rea
631
667
  ) => {
632
668
  const draftId = draftNode.current?.id ?? null
633
669
  const wasAdopted = draftNode.isAdopted
634
- const finalId = draftNode.commit(nodeUpdate, {
635
- ...options,
636
- onReject: (reason) => {
637
- feedback.reject(reason)
638
- edgeMaterial.color.setHex(0xef_44_44)
639
- basePlaneMaterial.color.setHex(0xef_44_44)
640
- },
641
- })
670
+ const drop = beginSceneHistoryPauseSession(useScene, { gesture: movingNodeId })
671
+ let finalId: string | null
672
+ try {
673
+ finalId = drop.commitStep(() =>
674
+ draftNode.commit(nodeUpdate, {
675
+ ...options,
676
+ onReject: (reason) => {
677
+ feedback.reject(reason)
678
+ edgeMaterial.color.setHex(0xef_44_44)
679
+ basePlaneMaterial.color.setHex(0xef_44_44)
680
+ },
681
+ }),
682
+ )
683
+ } finally {
684
+ drop.end()
685
+ }
642
686
  if (finalId && draftId) {
643
687
  useLiveTransforms.getState().clear(draftId)
644
688
  useLiveNodeOverrides.getState().clearFields(draftId, faceHostClearFields(draftNode.current))
@@ -780,10 +824,13 @@ export function usePlacementCoordinator(config: PlacementCoordinatorConfig): Rea
780
824
  // Alignment guides are floor-only; clear them when the cursor moves
781
825
  // onto a wall / ceiling / item surface (only those paths call this).
782
826
  useAlignmentGuides.getState().clear()
783
- // Roof faces carry no grab anchor, but landing on one still counts as
784
- // anchoring elsewhere — a later return to the grabbed wall must center
785
- // under the cursor, not restore the stale grab offset.
786
- if (result.stateUpdate.surface === 'roof-wall') grabForgotten = true
827
+ // A floor transition can be a hit gap; other hosted surfaces are a new anchor.
828
+ if (
829
+ result.stateUpdate.surface &&
830
+ result.stateUpdate.surface !== 'floor' &&
831
+ result.stateUpdate.surface !== 'wall'
832
+ )
833
+ grabForgotten = true
787
834
  Object.assign(placementState.current, result.stateUpdate)
788
835
  gridPosition.current.set(...result.gridPosition)
789
836
 
@@ -832,16 +879,15 @@ export function usePlacementCoordinator(config: PlacementCoordinatorConfig): Rea
832
879
  asset,
833
880
  initRotation,
834
881
  configRef.current.defaultScale,
835
- configRef.current.slots,
882
+ configRef.current.copied,
836
883
  )
837
884
 
838
885
  const draft = draftNode.current
839
886
  if (draft) {
840
887
  Object.assign(draft, result.nodeUpdate)
841
888
  // One-time setup: put node in the right parent so it renders correctly
842
- if (result.surfaceId !== undefined)
843
- draftNode.updateSurface(result.nodeUpdate, result.surfaceId)
844
- else updateSurfaceNode(draft.id, result.nodeUpdate)
889
+ if (result.surfaceId === undefined) updateSurfaceNode(draft.id, result.nodeUpdate)
890
+ else draftNode.updateSurface(result.nodeUpdate, result.surfaceId)
845
891
  disableDraftRaycastNow()
846
892
  }
847
893
 
@@ -908,15 +954,12 @@ export function usePlacementCoordinator(config: PlacementCoordinatorConfig): Rea
908
954
  // for a given host, then offsets every later move by
909
955
  // `start + (raw - anchor)` — mirroring the floor path and the door/window
910
956
  // move tools so the item tracks the grabbed point instead of teleporting
911
- // its origin under the cursor. Reset on host change (re-seeded from the
912
- // item's then-current position) by the surface leave handlers.
913
- let wallDragAnchor: {
914
- wallId: string
915
- rawX: number
916
- rawY: number
917
- startX: number
918
- startY: number
919
- } | null = null
957
+ // its origin under the cursor. Changing hosts or faces clears the offset.
958
+ const wallPointer = createWallPointerTracker(
959
+ preserveDragOffset && grabStartPosition
960
+ ? { wallId: grabWallId, side: draftNode.current?.side, position: grabStartPosition }
961
+ : undefined,
962
+ )
920
963
  let ceilingDragAnchor: {
921
964
  ceilingId: string
922
965
  rawX: number
@@ -1037,16 +1080,6 @@ export function usePlacementCoordinator(config: PlacementCoordinatorConfig): Rea
1037
1080
  // replays it. Captured once at setup — a fresh coordinator mounts per move.
1038
1081
  const dragMode = useEditor.getState().placementDragMode
1039
1082
  let releaseCommit: (() => void) | null = null
1040
- // Eat the click the browser fires after pointer-up so the surface
1041
- // `:click` handlers don't commit a second time.
1042
- const swallowNextClick = () => {
1043
- const swallow = (e: Event) => {
1044
- e.stopPropagation()
1045
- e.preventDefault()
1046
- }
1047
- window.addEventListener('click', swallow, { capture: true, once: true })
1048
- setTimeout(() => window.removeEventListener('click', swallow, { capture: true }), 300)
1049
- }
1050
1083
  const onReleaseCommit = () => {
1051
1084
  gridDispatch.flush()
1052
1085
  if (!releaseCommit) return
@@ -1134,6 +1167,8 @@ export function usePlacementCoordinator(config: PlacementCoordinatorConfig): Rea
1134
1167
  if (!cursorGroupRef.current) return
1135
1168
  const result = floorStrategy.move(getContext(), floorEvent)
1136
1169
  if (!result) return
1170
+ // Attached items cannot land on the floor; its hit can be a gap in their host.
1171
+ if (!asset.attachTo) grabForgotten = true
1137
1172
 
1138
1173
  // Figma-style alignment snap layered on top of the floor strategy's
1139
1174
  // grid snap: when the draft's edge lines up (on X or Z) with another
@@ -1276,7 +1311,7 @@ export function usePlacementCoordinator(config: PlacementCoordinatorConfig): Rea
1276
1311
  asset,
1277
1312
  currentRotation,
1278
1313
  configRef.current.defaultScale,
1279
- configRef.current.slots,
1314
+ configRef.current.copied,
1280
1315
  )
1281
1316
  const previewBounds = expandBoundsToGrid(
1282
1317
  getFallbackPreviewBounds(draftNode.current, asset, asset.attachTo),
@@ -1363,28 +1398,13 @@ export function usePlacementCoordinator(config: PlacementCoordinatorConfig): Rea
1363
1398
 
1364
1399
  let wallMoveEvent = event
1365
1400
  if (preserveDragOffset && draftNode.current) {
1366
- const rawX = event.localPosition[0]
1367
- const rawY = event.localPosition[1]
1368
- if (!wallDragAnchor || wallDragAnchor.wallId !== event.node.id) {
1369
- // Preserve the grab offset only on the wall the item was grabbed
1370
- // from (no teleport under the pointer at grab time). Any OTHER host
1371
- // centers the item under the cursor — a host change is already a
1372
- // jump, so tracking the pointer exactly is the expected feel, while
1373
- // re-seeding from the carried-over position (the old behavior)
1374
- // baked an arbitrary along-wall offset into the whole stay on that
1375
- // wall. Once anchored elsewhere the grab is forgotten for good, so
1376
- // re-entering the original wall centers under the cursor too.
1377
- const preserveGrab = preserveGrabOn(event.node.id, grabWallId)
1378
- wallDragAnchor = {
1379
- wallId: event.node.id,
1380
- rawX,
1381
- rawY,
1382
- startX: preserveGrab && grabStartPosition ? grabStartPosition[0] : rawX,
1383
- startY: preserveGrab && grabStartPosition ? grabStartPosition[1] : rawY,
1384
- }
1385
- }
1386
- const correctedX = wallDragAnchor.startX + (rawX - wallDragAnchor.rawX)
1387
- const correctedY = wallDragAnchor.startY + (rawY - wallDragAnchor.rawY)
1401
+ preserveGrabOn(event.node.id, grabWallId)
1402
+ if (grabForgotten) wallPointer.forget()
1403
+ const [correctedX, correctedY] = wallPointer.resolve(
1404
+ event.node.id,
1405
+ getSideFromNormal(event.normal),
1406
+ [event.localPosition[0], event.localPosition[1]],
1407
+ )
1388
1408
  wallMoveEvent = {
1389
1409
  ...event,
1390
1410
  localPosition: [correctedX, correctedY, event.localPosition[2]],
@@ -1427,12 +1447,13 @@ export function usePlacementCoordinator(config: PlacementCoordinatorConfig): Rea
1427
1447
  const rot = result.nodeUpdate?.rotation
1428
1448
  if (rot) mesh.rotation.y = rot[1]
1429
1449
 
1430
- // Push wall-side items out by half the parent wall's thickness
1431
- if (asset.attachTo === 'wall-side' && placementState.current.wallId) {
1450
+ if (placementState.current.wallId) {
1432
1451
  const parentWall = useScene.getState().nodes[placementState.current.wallId as AnyNodeId]
1433
1452
  if (parentWall?.type === 'wall') {
1434
- const wallThickness = (parentWall as WallNode).thickness ?? 0.1
1435
- mesh.position.z = (wallThickness / 2) * (draft.side === 'front' ? 1 : -1)
1453
+ mesh.position.z =
1454
+ asset.attachTo === 'wall-side'
1455
+ ? getWallLocalFaceZ(parentWall, draft.side === 'front' ? 'a' : 'b')
1456
+ : draft.position[2] + getWallBodyCenterOffset(parentWall)
1436
1457
  }
1437
1458
  }
1438
1459
  }
@@ -1486,7 +1507,7 @@ export function usePlacementCoordinator(config: PlacementCoordinatorConfig): Rea
1486
1507
  }
1487
1508
 
1488
1509
  const onWallLeave = (event: WallEvent) => {
1489
- wallDragAnchor = null
1510
+ wallPointer.leave()
1490
1511
  const result = wallStrategy.leave(getContext())
1491
1512
  if (!result) return
1492
1513
 
@@ -1600,7 +1621,7 @@ export function usePlacementCoordinator(config: PlacementCoordinatorConfig): Rea
1600
1621
  const segment =
1601
1622
  useScene.getState().nodes[placementState.current.roofSegmentId as AnyNodeId]
1602
1623
  if (segment?.type === 'roof-segment') {
1603
- mesh.position.z = (segment.wallThickness ?? 0.1) / 2
1624
+ mesh.position.z = getWallLocalFaceZ({ thickness: segment.wallThickness ?? 0.1 }, 'a')
1604
1625
  }
1605
1626
  }
1606
1627
  const rot = result.nodeUpdate?.rotation
@@ -1640,10 +1661,12 @@ export function usePlacementCoordinator(config: PlacementCoordinatorConfig): Rea
1640
1661
  applyTransition(result)
1641
1662
  const draft = draftNode.current
1642
1663
  if (draft) {
1643
- useScene.getState().updateNode(draft.id, {
1644
- parentId: result.nodeUpdate.parentId as string,
1645
- roofSegmentId: undefined,
1646
- })
1664
+ pausedDraftWrite(() =>
1665
+ useScene.getState().updateNode(draft.id, {
1666
+ parentId: result.nodeUpdate.parentId as string,
1667
+ roofSegmentId: undefined,
1668
+ }),
1669
+ )
1647
1670
  }
1648
1671
  } else {
1649
1672
  // Create mode: destroy transient and reset state
@@ -1758,7 +1781,8 @@ export function usePlacementCoordinator(config: PlacementCoordinatorConfig): Rea
1758
1781
  }
1759
1782
  if (draftNode.isAdopted) {
1760
1783
  applyTransition(result)
1761
- if (draft) useScene.getState().updateNode(draft.id, result.nodeUpdate)
1784
+ if (draft)
1785
+ pausedDraftWrite(() => useScene.getState().updateNode(draft.id, result.nodeUpdate))
1762
1786
  } else {
1763
1787
  draftNode.destroy()
1764
1788
  gridPosition.current.set(...result.gridPosition)
@@ -1863,6 +1887,8 @@ export function usePlacementCoordinator(config: PlacementCoordinatorConfig): Rea
1863
1887
  }
1864
1888
  if (event.node.id === draftNode.current?.id) return
1865
1889
  has3DPointerDrivenMoveRef.current = true
1890
+ // An authored object's underside takes ceiling items (a pendant under a vault).
1891
+ if (enterFaceHost(event)) return
1866
1892
  const result = itemSurfaceStrategy.enter(surfaceContext(event, generic), event)
1867
1893
  if (!result) return
1868
1894
  feedback.clear()
@@ -1880,6 +1906,17 @@ export function usePlacementCoordinator(config: PlacementCoordinatorConfig): Rea
1880
1906
 
1881
1907
  const onItemMove = (event: NodeEvent<AnyNode>, generic = false) => {
1882
1908
  if (event.node.id === draftNode.current?.id) return
1909
+ if (
1910
+ placementState.current.surface === 'block-face' &&
1911
+ placementState.current.blockId === event.node.id
1912
+ ) {
1913
+ onFaceHostMove(event)
1914
+ return
1915
+ }
1916
+ if (placementState.current.surface !== 'block-face' && enterFaceHost(event)) {
1917
+ releaseCommit = () => onFaceHostClick(event)
1918
+ return
1919
+ }
1883
1920
  releaseCommit = () => onItemClick(event)
1884
1921
  has3DPointerDrivenMoveRef.current = true
1885
1922
  if (!cursorGroupRef.current) return
@@ -1931,9 +1968,9 @@ export function usePlacementCoordinator(config: PlacementCoordinatorConfig): Rea
1931
1968
  }
1932
1969
 
1933
1970
  const surfaceWorld =
1934
- ctx.state.surfaceItemId !== null
1935
- ? resolveHostSurfaceWorld(ctx.state.surfaceItemId, event.position)
1936
- : null
1971
+ ctx.state.surfaceItemId === null
1972
+ ? null
1973
+ : resolveHostSurfaceWorld(ctx.state.surfaceItemId, event.position)
1937
1974
  const itemMoveEvent = surfaceWorld ? { ...event, position: surfaceWorld } : event
1938
1975
  lastRawPos.current.set(
1939
1976
  itemMoveEvent.position[0],
@@ -1980,6 +2017,13 @@ export function usePlacementCoordinator(config: PlacementCoordinatorConfig): Rea
1980
2017
  const onItemLeave = (event: NodeEvent<AnyNode>) => {
1981
2018
  if (event.node.type === 'cabinet') return
1982
2019
  if (event.node.id === draftNode.current?.id) return
2020
+ if (
2021
+ placementState.current.surface === 'block-face' &&
2022
+ placementState.current.blockId === event.node.id
2023
+ ) {
2024
+ onFaceHostLeave(event)
2025
+ return
2026
+ }
1983
2027
  if (placementState.current.surface !== 'item-surface') return
1984
2028
 
1985
2029
  event.stopPropagation()
@@ -1995,6 +2039,13 @@ export function usePlacementCoordinator(config: PlacementCoordinatorConfig): Rea
1995
2039
  const onItemClick = (event: NodeEvent<AnyNode>) => {
1996
2040
  gridDispatch.flush()
1997
2041
  if (feedback.reason) return
2042
+ if (
2043
+ placementState.current.surface === 'block-face' &&
2044
+ placementState.current.blockId === event.node.id
2045
+ ) {
2046
+ onFaceHostClick(event)
2047
+ return
2048
+ }
1998
2049
  // Click on the draft item itself. R3F dispatches click events to
1999
2050
  // the closest intersected mesh only — when the draft is hovering
2000
2051
  // on a host (shelf / table / etc.) the draft's mesh is *above*
@@ -2309,9 +2360,9 @@ export function usePlacementCoordinator(config: PlacementCoordinatorConfig): Rea
2309
2360
  return
2310
2361
  }
2311
2362
  const shelfWorld =
2312
- ctx.state.shelfId !== null
2313
- ? resolveHostSurfaceWorld(ctx.state.shelfId, event.position)
2314
- : null
2363
+ ctx.state.shelfId === null
2364
+ ? null
2365
+ : resolveHostSurfaceWorld(ctx.state.shelfId, event.position)
2315
2366
  const shelfMoveEvent = shelfWorld ? { ...event, position: shelfWorld } : event
2316
2367
  const result = shelfSurfaceStrategy.move(ctx, shelfMoveEvent)
2317
2368
  if (!result) return
@@ -2385,9 +2436,7 @@ export function usePlacementCoordinator(config: PlacementCoordinatorConfig): Rea
2385
2436
  }
2386
2437
 
2387
2438
  // Don't intercept keys when focus is inside a text input
2388
- if (event.target instanceof HTMLInputElement || event.target instanceof HTMLTextAreaElement) {
2389
- return
2390
- }
2439
+ if (isTypingTarget(event.target)) return
2391
2440
 
2392
2441
  const draft = draftNode.current
2393
2442
  if (!draft) return
@@ -2613,7 +2662,7 @@ export function usePlacementCoordinator(config: PlacementCoordinatorConfig): Rea
2613
2662
  if (event.button !== 2) return
2614
2663
  const down = rightDown
2615
2664
  rightDown = null
2616
- if (!down || !configRef.current.onCancel) return
2665
+ if (!(down && configRef.current.onCancel)) return
2617
2666
  const movedSq = (event.clientX - down.x) ** 2 + (event.clientY - down.y) ** 2
2618
2667
  const elapsed = performance.now() - down.t
2619
2668
  if (movedSq <= RIGHT_CLICK_CANCEL_MAX_MOVE_PX ** 2 && elapsed <= RIGHT_CLICK_CANCEL_MAX_MS) {
@@ -2662,8 +2711,7 @@ export function usePlacementCoordinator(config: PlacementCoordinatorConfig): Rea
2662
2711
  const draft = draftNode.current
2663
2712
  if (draft === null) return
2664
2713
  if (draft.id in useScene.getState().nodes) return
2665
- // Temporal is paused during placement, createNode won't be tracked
2666
- useScene.getState().createNode(draft, draft.parentId as AnyNodeId)
2714
+ pausedDraftWrite(() => useScene.getState().createNode(draft, draft.parentId as AnyNodeId))
2667
2715
  })
2668
2716
  })
2669
2717
 
@@ -2752,11 +2800,15 @@ export function usePlacementCoordinator(config: PlacementCoordinatorConfig): Rea
2752
2800
  }
2753
2801
  emitter.on('node:click', commitFloorOnSurfaceClick as never)
2754
2802
  if (dragMode) window.addEventListener('pointerup', onReleaseCommit)
2803
+ window.addEventListener('pointerup', onPointerUpCapture, true)
2804
+ window.addEventListener('click', onClickCapture, true)
2755
2805
 
2756
2806
  return () => {
2757
2807
  tearingDown = true
2758
2808
  feedback.clear()
2759
2809
  if (dragMode) window.removeEventListener('pointerup', onReleaseCommit)
2810
+ window.removeEventListener('pointerup', onPointerUpCapture, true)
2811
+ window.removeEventListener('click', onClickCapture, true)
2760
2812
  unsubDraftWatch()
2761
2813
  useAlignmentGuides.getState().clear()
2762
2814
  // Clear every live preview channel before restoring or deleting the draft.
@@ -2765,7 +2817,6 @@ export function usePlacementCoordinator(config: PlacementCoordinatorConfig): Rea
2765
2817
  useLiveNodeOverrides.getState().clearFields(draftNode.current.id, ['rotation'])
2766
2818
  }
2767
2819
  draftNode.destroy()
2768
- useScene.temporal.getState().resume()
2769
2820
  gridDispatch.cancel()
2770
2821
  emitter.off('grid:move', gridDispatch.schedule)
2771
2822
  emitter.off('grid:click', onGridClick)
@@ -2862,7 +2913,9 @@ export function usePlacementCoordinator(config: PlacementCoordinatorConfig): Rea
2862
2913
  )
2863
2914
  return
2864
2915
  draft.parentId = viewerLevelId
2865
- useScene.getState().updateNode(draft.id as AnyNodeId, { parentId: viewerLevelId })
2916
+ pausedDraftWrite(() =>
2917
+ useScene.getState().updateNode(draft.id as AnyNodeId, { parentId: viewerLevelId }),
2918
+ )
2866
2919
  }, [viewerLevelId, draftNode, asset])
2867
2920
 
2868
2921
  // Restore the draft mesh's raycast when the coordinator unmounts (tool change).
@@ -0,0 +1,37 @@
1
+ import {
2
+ getCatalogMaterialById,
3
+ parseMaterialRef,
4
+ type SceneMaterialId,
5
+ useScene,
6
+ } from '@pascal-app/core'
7
+ import { useMemo } from 'react'
8
+ import { usePaintRegionMode } from '../../../lib/paint-region-mode'
9
+ import useEditor from '../../../store/use-editor'
10
+
11
+ /**
12
+ * The paint material's colour for a region preview: a library preset's
13
+ * preview colour, a scene material's colour, or a plain material colour;
14
+ * `fallback` for the eraser or a material with no cheap colour.
15
+ */
16
+ export function usePaintTint(fallback: string): string {
17
+ const active = useEditor((state) => state.activePaintMaterial)
18
+ const eraser = usePaintRegionMode((state) => state.mode) === 'erase'
19
+ const materials = useScene((state) => state.materials)
20
+ return useMemo(() => {
21
+ if (eraser) return fallback
22
+ const parsed = parseMaterialRef(active?.materialPreset)
23
+ if (parsed?.kind === 'library') {
24
+ const color = getCatalogMaterialById(parsed.id)?.previewColor
25
+ if (color) return color
26
+ }
27
+ if (parsed?.kind === 'scene') {
28
+ const color = materials[parsed.id as SceneMaterialId]?.material.properties?.color
29
+ if (color) return color
30
+ }
31
+ return (
32
+ active?.material?.properties?.color ??
33
+ getCatalogMaterialById(active?.material?.id)?.previewColor ??
34
+ fallback
35
+ )
36
+ }, [active, eraser, materials, fallback])
37
+ }