@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,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
+ }
@@ -0,0 +1,466 @@
1
+ 'use client'
2
+
3
+ import {
4
+ type AnyNode,
5
+ type AnyNodeId,
6
+ emitter,
7
+ sceneRegistry,
8
+ useScene,
9
+ type WallFace,
10
+ type WallNode,
11
+ } from '@pascal-app/core'
12
+ import { getSceneTheme, setSurfaceRaycastLayers, useViewer } from '@pascal-app/viewer'
13
+ import { useFrame, useThree } from '@react-three/fiber'
14
+ import { useEffect, useMemo, useRef } from 'react'
15
+ import {
16
+ BufferGeometry,
17
+ type Camera,
18
+ DoubleSide,
19
+ Float32BufferAttribute,
20
+ type Group,
21
+ Matrix4,
22
+ type Mesh,
23
+ type Object3D,
24
+ Raycaster,
25
+ Vector2,
26
+ } from 'three'
27
+ import { LineBasicNodeMaterial, MeshBasicNodeMaterial } from 'three/webgpu'
28
+ import { markToolCancelConsumed } from '../../../hooks/use-keyboard'
29
+ import { EDITOR_LAYER } from '../../../lib/constants'
30
+ import { formatLinearMeasurement } from '../../../lib/measurements'
31
+ import { paintRegionTargets, usePaintRegionMode } from '../../../lib/paint-region-mode'
32
+ import { isTypingTarget } from '../../../lib/typing-target'
33
+ import {
34
+ activeWallRegionGesture,
35
+ activeWallRegionTarget,
36
+ cancelWallRegion,
37
+ dragWallRegion,
38
+ hoverWallRegion,
39
+ pressWallRegion,
40
+ releaseWallRegion,
41
+ useWallPaintRegionSession,
42
+ type WallRegionHit,
43
+ type WallRegionPreview,
44
+ } from '../../../lib/wall-paint-region-session'
45
+ import {
46
+ type FaceSpan,
47
+ facePoint,
48
+ faceSpanStrips,
49
+ faceStripOutline,
50
+ faceStripTriangles,
51
+ projectLocalRayToFace,
52
+ resolveWallFaceBase,
53
+ wallFaceExtent,
54
+ wallFaceFrame,
55
+ wallHitFaceUV,
56
+ } from '../../../lib/wall-region-face'
57
+ import {
58
+ DEFAULT_LINE_SNAP_TOLERANCE,
59
+ faceBaseAt,
60
+ resolveWallRegionSnapMode,
61
+ type WallRegionSnap,
62
+ } from '../../../lib/wall-region-snap'
63
+ import useEditor, { isGridSnapActive, isMagneticSnapActive } from '../../../store/use-editor'
64
+ import { DraftMeasurementLabel } from '../shared/draft-measurement-label'
65
+ import { usePaintTint } from './use-paint-tint'
66
+
67
+ const DRAFT_COLOR = '#818cf8'
68
+ const CORNER_HALF_SIZE = 0.04
69
+ const FILL_OFFSET = 0.004
70
+ const LINE_OFFSET = 0.006
71
+
72
+ const noRaycast = () => {}
73
+ const overlayMaterial = (opacity: number) =>
74
+ new MeshBasicNodeMaterial({
75
+ color: DRAFT_COLOR,
76
+ depthWrite: false,
77
+ opacity,
78
+ polygonOffset: true,
79
+ polygonOffsetFactor: -2,
80
+ polygonOffsetUnits: -2,
81
+ side: DoubleSide,
82
+ transparent: true,
83
+ })
84
+ const fillMaterial = overlayMaterial(0.35)
85
+ const lineMaterial = overlayMaterial(0.95)
86
+ const outlineMaterial = new LineBasicNodeMaterial({
87
+ color: DRAFT_COLOR,
88
+ depthWrite: false,
89
+ transparent: true,
90
+ })
91
+
92
+ function wallRegionActive(): boolean {
93
+ return paintRegionTargets(usePaintRegionMode.getState().mode).wall
94
+ }
95
+
96
+ function visibleSurface(object: Object3D) {
97
+ for (let node: Object3D | null = object; node; node = node.parent)
98
+ if (!node.visible || node.userData.wallHidden === true) return false
99
+ return true
100
+ }
101
+
102
+ type PointerSample = Pick<PointerEvent, 'clientX' | 'clientY' | 'target' | 'altKey'>
103
+
104
+ const sample = (event: PointerSample): PointerSample => ({
105
+ clientX: event.clientX,
106
+ clientY: event.clientY,
107
+ target: event.target,
108
+ altKey: event.altKey,
109
+ })
110
+
111
+ function pointerRaycaster(event: PointerSample, canvas: HTMLCanvasElement, camera: Camera) {
112
+ const bounds = canvas.getBoundingClientRect()
113
+ const raycaster = new Raycaster()
114
+ raycaster.setFromCamera(
115
+ new Vector2(
116
+ ((event.clientX - bounds.left) / bounds.width) * 2 - 1,
117
+ 1 - ((event.clientY - bounds.top) / bounds.height) * 2,
118
+ ),
119
+ camera,
120
+ )
121
+ setSurfaceRaycastLayers(raycaster.layers)
122
+ return raycaster
123
+ }
124
+
125
+ function regionHit(
126
+ wall: WallNode,
127
+ mesh: Mesh,
128
+ face: WallFace,
129
+ u: number,
130
+ v: number,
131
+ nodes: Readonly<Record<string, AnyNode | undefined>>,
132
+ ): WallRegionHit {
133
+ const faceBase = resolveWallFaceBase(wall, mesh.geometry, nodes)
134
+ return {
135
+ wallId: wall.id,
136
+ face,
137
+ u,
138
+ v,
139
+ length: Math.hypot(wall.end[0] - wall.start[0], wall.end[1] - wall.start[1]),
140
+ extent: wallFaceExtent(wall, face, mesh.geometry),
141
+ runs: faceBase?.[face] ?? null,
142
+ }
143
+ }
144
+
145
+ /** The nearest visible wall face of the current level under the pointer. */
146
+ function hoverHit(raycaster: Raycaster): WallRegionHit | null {
147
+ const levelId = useViewer.getState().selection.levelId
148
+ const nodes = useScene.getState().nodes
149
+ const level = levelId ? nodes[levelId as AnyNodeId] : undefined
150
+ if (level?.type !== 'level') return null
151
+ let best: {
152
+ wall: WallNode
153
+ mesh: Mesh
154
+ distance: number
155
+ hit: ReturnType<Raycaster['intersectObject']>[number]
156
+ } | null = null
157
+ for (const childId of level.children) {
158
+ const wall = nodes[childId as AnyNodeId]
159
+ if (wall?.type !== 'wall') continue
160
+ const mesh = sceneRegistry.nodes.get(wall.id) as Mesh | undefined
161
+ if (!(mesh?.isMesh && visibleSurface(mesh))) continue
162
+ const hit = raycaster.intersectObject(mesh, false)[0]
163
+ if (!hit || (best && hit.distance >= best.distance)) continue
164
+ best = { wall, mesh, distance: hit.distance, hit }
165
+ }
166
+ if (!best) return null
167
+ const local = best.mesh.worldToLocal(best.hit.point.clone())
168
+ const normal = best.hit.face?.normal
169
+ const faceBase = resolveWallFaceBase(best.wall, best.mesh.geometry, nodes)
170
+ const uv = wallHitFaceUV(
171
+ best.wall,
172
+ [local.x, local.y, local.z],
173
+ normal ? [normal.x, normal.y, normal.z] : undefined,
174
+ faceBase,
175
+ )
176
+ // Below the face base the ray has already passed through the floor, which
177
+ // the floor region gesture owns.
178
+ if (!uv || uv.v < -0.01) return null
179
+ return regionHit(best.wall, best.mesh, uv.face, uv.u, uv.v, nodes)
180
+ }
181
+
182
+ /** The pointer projected onto the pressed face, wherever it wanders. */
183
+ function dragHit(raycaster: Raycaster, target: { wallId: string; face: WallFace }) {
184
+ const nodes = useScene.getState().nodes
185
+ const wall = nodes[target.wallId as AnyNodeId]
186
+ const mesh = sceneRegistry.nodes.get(target.wallId) as Mesh | undefined
187
+ if (wall?.type !== 'wall' || !mesh) return null
188
+ mesh.updateWorldMatrix(true, false)
189
+ const ray = raycaster.ray.clone().applyMatrix4(new Matrix4().copy(mesh.matrixWorld).invert())
190
+ const point = projectLocalRayToFace(
191
+ wallFaceFrame(wall, target.face),
192
+ [ray.origin.x, ray.origin.y, ray.origin.z],
193
+ [ray.direction.x, ray.direction.y, ray.direction.z],
194
+ )
195
+ if (!point) return null
196
+ const runs = resolveWallFaceBase(wall, mesh.geometry, nodes)?.[target.face] ?? null
197
+ return regionHit(wall, mesh, target.face, point.u, point.y - faceBaseAt(runs, point.u), nodes)
198
+ }
199
+
200
+ function currentSnap(event: { altKey: boolean }): WallRegionSnap {
201
+ return {
202
+ mode: resolveWallRegionSnapMode({
203
+ grid: isGridSnapActive(),
204
+ magnetic: isMagneticSnapActive(),
205
+ alt: event.altKey,
206
+ }),
207
+ gridStep: useEditor.getState().gridSnapStep,
208
+ tolerance: DEFAULT_LINE_SNAP_TOLERANCE,
209
+ }
210
+ }
211
+
212
+ /**
213
+ * Pointer and keyboard wiring. Hover previews on every (frame-coalesced) move;
214
+ * a left press on a wall face claims the pointer, drags on window listeners so
215
+ * the gesture follows the pointer off the wall, and commits on release. Escape
216
+ * (and ⌘Z) mid-gesture cancel it through `tool:cancel` without a write.
217
+ */
218
+ function useWallRegionPointer(canvas: HTMLCanvasElement, camera: Camera) {
219
+ useEffect(() => {
220
+ let pressed: { pointerId: number } | null = null
221
+ let frame = 0
222
+ let pending: PointerEvent | null = null
223
+ let last: PointerSample | null = null
224
+ const dropPending = () => {
225
+ if (frame) cancelAnimationFrame(frame)
226
+ frame = 0
227
+ pending = null
228
+ }
229
+ const apply = (event: PointerSample) => {
230
+ last = event
231
+ if (!wallRegionActive()) return
232
+ const raycaster = pointerRaycaster(event, canvas, camera)
233
+ if (pressed) {
234
+ const target = activeWallRegionTarget()
235
+ const hit = target ? dragHit(raycaster, target) : null
236
+ if (hit) dragWallRegion(hit, currentSnap(event))
237
+ return
238
+ }
239
+ const idle = event.target === canvas && !useViewer.getState().cameraDragging
240
+ hoverWallRegion(idle ? hoverHit(raycaster) : null, currentSnap(event))
241
+ }
242
+ const flush = () => {
243
+ frame = 0
244
+ const event = pending
245
+ pending = null
246
+ if (event) apply(sample(event))
247
+ }
248
+ const onMove = (event: PointerEvent) => {
249
+ if (pressed && event.pointerId !== pressed.pointerId) return
250
+ pending = event
251
+ if (!frame) frame = requestAnimationFrame(flush) || 0
252
+ }
253
+ // Pointer capture is the gesture owner's; it releases it however the gesture ends.
254
+ const endPress = () => {
255
+ if (!pressed) return
256
+ pressed = null
257
+ dropPending()
258
+ // The click the browser synthesizes from this release belongs to the gesture.
259
+ setTimeout(() => useViewer.getState().setInputDragging(false), 0)
260
+ }
261
+ const onDown = (event: PointerEvent) => {
262
+ if (event.button !== 0 || event.target !== canvas || pressed) return
263
+ if (useViewer.getState().cameraDragging) return
264
+ if (!wallRegionActive()) return
265
+ const hit = hoverHit(pointerRaycaster(event, canvas, camera))
266
+ if (!hit) return
267
+ event.preventDefault()
268
+ event.stopPropagation()
269
+ dropPending()
270
+ if (!pressWallRegion(hit, currentSnap(event), endPress)) return
271
+ pressed = { pointerId: event.pointerId }
272
+ activeWallRegionGesture()?.capturePointer(canvas, event.pointerId)
273
+ useViewer.getState().setInputDragging(true)
274
+ }
275
+ const onUp = (event: PointerEvent) => {
276
+ if (!pressed || event.pointerId !== pressed.pointerId) return
277
+ const final = pending
278
+ dropPending()
279
+ if (final) apply(sample(final))
280
+ event.preventDefault()
281
+ event.stopPropagation()
282
+ endPress()
283
+ releaseWallRegion()
284
+ // Show what the next press would do without waiting for a move.
285
+ if (last) apply(last)
286
+ }
287
+ const onPointerCancel = (event: PointerEvent) => {
288
+ if (!pressed || event.pointerId !== pressed.pointerId) return
289
+ cancelWallRegion()
290
+ }
291
+ const onToolCancel = () => {
292
+ if (!cancelWallRegion()) return
293
+ markToolCancelConsumed()
294
+ }
295
+ // Alt frees the snap; pressing or letting go re-evaluates where the pointer is.
296
+ const onKey = (event: KeyboardEvent) => {
297
+ if (event.key !== 'Alt' || isTypingTarget(event.target) || !last) return
298
+ apply({ ...sample(last), altKey: event.type === 'keydown' })
299
+ }
300
+ canvas.addEventListener('pointerdown', onDown, true)
301
+ window.addEventListener('pointermove', onMove, true)
302
+ window.addEventListener('pointerup', onUp, true)
303
+ window.addEventListener('pointercancel', onPointerCancel, true)
304
+ window.addEventListener('keydown', onKey, true)
305
+ window.addEventListener('keyup', onKey, true)
306
+ emitter.on('tool:cancel', onToolCancel)
307
+ return () => {
308
+ cancelWallRegion()
309
+ dropPending()
310
+ useWallPaintRegionSession.setState({ preview: null })
311
+ canvas.removeEventListener('pointerdown', onDown, true)
312
+ window.removeEventListener('pointermove', onMove, true)
313
+ window.removeEventListener('pointerup', onUp, true)
314
+ window.removeEventListener('pointercancel', onPointerCancel, true)
315
+ window.removeEventListener('keydown', onKey, true)
316
+ window.removeEventListener('keyup', onKey, true)
317
+ emitter.off('tool:cancel', onToolCancel)
318
+ }
319
+ }, [canvas, camera])
320
+ }
321
+
322
+ function positions(array: Float32Array) {
323
+ const geometry = new BufferGeometry()
324
+ geometry.setAttribute('position', new Float32BufferAttribute(array, 3))
325
+ return geometry
326
+ }
327
+
328
+ type PreviewGeometry = {
329
+ fill: BufferGeometry | null
330
+ outline: BufferGeometry | null
331
+ corner: BufferGeometry | null
332
+ label: { position: [number, number, number]; values: number[] } | null
333
+ }
334
+
335
+ function buildPreviewGeometry(preview: WallRegionPreview, wall: WallNode): PreviewGeometry | null {
336
+ const mesh = sceneRegistry.nodes.get(wall.id) as Mesh | undefined
337
+ if (!mesh) return null
338
+ const nodes = useScene.getState().nodes
339
+ const frame = wallFaceFrame(wall, preview.face)
340
+ const other = wallFaceFrame(wall, preview.face === 'a' ? 'b' : 'a')
341
+ const extent = wallFaceExtent(wall, preview.face, mesh.geometry)
342
+ const runs = resolveWallFaceBase(wall, mesh.geometry, nodes)?.[preview.face] ?? null
343
+ const strips = (span: FaceSpan) => faceSpanStrips(frame, extent, runs, span)
344
+ const fillStrips = preview.bounds ? strips(preview.bounds) : []
345
+ // Where the box starts: the press point on hover, the pressed corner while dragging.
346
+ const [cu, cv] = preview.corner
347
+ const cornerStrips = strips({
348
+ u0: cu - CORNER_HALF_SIZE,
349
+ u1: cu + CORNER_HALF_SIZE,
350
+ v0: Math.max(0, cv - CORNER_HALF_SIZE),
351
+ v1: cv + CORNER_HALF_SIZE,
352
+ })
353
+ const { measure } = preview
354
+ return {
355
+ fill: fillStrips.length
356
+ ? positions(faceStripTriangles(frame, other, fillStrips, FILL_OFFSET))
357
+ : null,
358
+ outline: fillStrips.length
359
+ ? positions(faceStripOutline(frame, other, fillStrips, LINE_OFFSET))
360
+ : null,
361
+ corner: cornerStrips.length
362
+ ? positions(faceStripTriangles(frame, other, cornerStrips, LINE_OFFSET))
363
+ : null,
364
+ label: measure?.values.every((value) => value > 0.005)
365
+ ? {
366
+ position: facePoint(
367
+ frame,
368
+ other,
369
+ measure.u,
370
+ faceBaseAt(runs, measure.u) + measure.v,
371
+ 0.05,
372
+ ),
373
+ values: measure.values,
374
+ }
375
+ : null,
376
+ }
377
+ }
378
+
379
+ function WallRegionPreview3D() {
380
+ const preview = useWallPaintRegionSession((state) => state.preview)
381
+ const wall = useScene((state) => (preview ? state.nodes[preview.wallId as AnyNodeId] : undefined))
382
+ const tint = usePaintTint(DRAFT_COLOR)
383
+ const unit = useViewer((state) => state.unit)
384
+ const metricNotation = useViewer((state) => state.metricNotation)
385
+ const isDark = useViewer((state) => getSceneTheme(state.sceneTheme).appearance === 'dark')
386
+ const root = useRef<Group>(null)
387
+ const geometry = useMemo(
388
+ () => (preview && wall?.type === 'wall' ? buildPreviewGeometry(preview, wall) : null),
389
+ [preview, wall],
390
+ )
391
+ useEffect(
392
+ () => () => {
393
+ geometry?.fill?.dispose()
394
+ geometry?.outline?.dispose()
395
+ geometry?.corner?.dispose()
396
+ },
397
+ [geometry],
398
+ )
399
+ useEffect(() => {
400
+ fillMaterial.color.set(tint)
401
+ }, [tint])
402
+ useFrame(() => {
403
+ const mesh = preview ? sceneRegistry.nodes.get(preview.wallId) : undefined
404
+ if (!root.current) return
405
+ root.current.visible = !!mesh
406
+ if (mesh) {
407
+ mesh.updateWorldMatrix(true, false)
408
+ root.current.matrix.copy(mesh.matrixWorld)
409
+ }
410
+ })
411
+ if (!geometry) return null
412
+ const label = geometry.label
413
+ const text = label
414
+ ? label.values.map((value) => formatLinearMeasurement(value, unit, metricNotation)).join(' × ')
415
+ : null
416
+ return (
417
+ <group matrixAutoUpdate={false} ref={root}>
418
+ {geometry.fill && (
419
+ <mesh
420
+ geometry={geometry.fill}
421
+ layers={EDITOR_LAYER}
422
+ material={fillMaterial}
423
+ raycast={noRaycast}
424
+ renderOrder={100}
425
+ />
426
+ )}
427
+ {geometry.outline && (
428
+ <lineSegments
429
+ geometry={geometry.outline}
430
+ layers={EDITOR_LAYER}
431
+ material={outlineMaterial}
432
+ raycast={noRaycast}
433
+ renderOrder={101}
434
+ />
435
+ )}
436
+ {geometry.corner && (
437
+ <mesh
438
+ geometry={geometry.corner}
439
+ layers={EDITOR_LAYER}
440
+ material={lineMaterial}
441
+ raycast={noRaycast}
442
+ renderOrder={102}
443
+ />
444
+ )}
445
+ {label && text && (
446
+ <DraftMeasurementLabel
447
+ color={isDark ? '#ffffff' : '#111111'}
448
+ label={text}
449
+ position={label.position}
450
+ shadowColor={isDark ? '#111111' : '#ffffff'}
451
+ />
452
+ )}
453
+ </group>
454
+ )
455
+ }
456
+
457
+ /**
458
+ * "Paint part of a surface" in 3D: a rectangle pressed anywhere on a wall face
459
+ * of the current level and dragged to its opposite corner, while the paint
460
+ * tool's Rectangle sub-mode is on.
461
+ */
462
+ export function WallPaintRegionTool() {
463
+ const { gl, camera } = useThree()
464
+ useWallRegionPointer(gl.domElement, camera)
465
+ return <WallRegionPreview3D />
466
+ }
@@ -9,7 +9,6 @@ import {
9
9
  ItemNode,
10
10
  LevelNode,
11
11
  nodeRegistry,
12
- registerNode,
13
12
  resolveSupportSlabPatch,
14
13
  sceneRegistry,
15
14
  spatialGridManager,
@@ -23,6 +22,7 @@ import {
23
22
  } from '@pascal-app/core/procedural-items'
24
23
  import { Group, Vector3 } from 'three'
25
24
  import { commitFreshPlacementSubtree } from '../../../lib/fresh-planar-placement'
25
+ import { registerHostingTestNode } from '../__fixtures__/hosting'
26
26
  import {
27
27
  createItemSurfaceGridDispatch,
28
28
  createItemSurfacePointerArbitration,
@@ -73,7 +73,15 @@ beforeEach(() => {
73
73
  oldNodes = useScene.getState().nodes
74
74
  oldRoots = useScene.getState().rootNodeIds
75
75
  nodeRegistry._reset()
76
- registerNode({
76
+ registerHostingTestNode({
77
+ kind: 'item',
78
+ schemaVersion: 1,
79
+ schema: ItemNode,
80
+ category: 'furnish',
81
+ defaults: () => ({}),
82
+ capabilities: {},
83
+ })
84
+ registerHostingTestNode({
77
85
  kind: 'procedural-item',
78
86
  schemaVersion: 1,
79
87
  schema: ProceduralItemNode,
@@ -11,6 +11,7 @@ import {
11
11
  sceneRegistry,
12
12
  useLiveTransforms,
13
13
  useScene,
14
+ wouldCreateHostingCycle,
14
15
  } from '@pascal-app/core'
15
16
  import { boxCorners } from '@pascal-app/core/procedural-items'
16
17
  import { type Camera, Euler, Quaternion, Vector3 } from 'three'
@@ -23,7 +24,10 @@ import {
23
24
  import { snapToGrid, snapToHalf } from '../item/placement-math'
24
25
  import { createShelfStickiness } from '../shared/shelf-stickiness'
25
26
  import { itemEventToSurfaceHit } from '../shared/surface-hit'
26
- import { createSurfaceRejectionFeedback } from '../shared/surface-rejection'
27
+ import {
28
+ createSurfaceEventOwnership,
29
+ createSurfaceRejectionFeedback,
30
+ } from '../shared/surface-rejection'
27
31
 
28
32
  export function createItemSurfacePointerArbitration() {
29
33
  let hostId: string | null = null
@@ -114,6 +118,7 @@ export function createRegistryItemSurfaceMove(node: AnyNode) {
114
118
  const feedback = createSurfaceRejectionFeedback()
115
119
  const scene = createSceneApi(useScene)
116
120
  const pointer = createItemSurfacePointerArbitration()
121
+ const ownership = createSurfaceEventOwnership()
117
122
  const originalParent = original.parentId
118
123
  ? useScene.getState().nodes[original.parentId as AnyNodeId]
119
124
  : null
@@ -165,6 +170,14 @@ export function createRegistryItemSurfaceMove(node: AnyNode) {
165
170
  surfaceId,
166
171
  )
167
172
  useLiveTransforms.getState().clear(node.id)
173
+ // A floor move and host return can share a React batch with unchanged final props.
174
+ // Restore the local mesh pose too, so an imperative floor preview cannot survive it.
175
+ const mesh = sceneRegistry.nodes.get(node.id)
176
+ if (mesh) {
177
+ mesh.position.set(...position)
178
+ const angles = fullRotation ?? rotation(yaw)
179
+ mesh.rotation.set(...(typeof angles === 'number' ? ([0, angles, 0] as const) : angles))
180
+ }
168
181
  }
169
182
  const session = {
170
183
  get rejection() {
@@ -211,17 +224,14 @@ export function createRegistryItemSurfaceMove(node: AnyNode) {
211
224
  }
212
225
  },
213
226
  enter(event: NodeEvent<AnyNode>, dimensions: [number, number, number], yaw: number) {
227
+ if (!ownership.allows(event.node.id, pointerEventOf(event))) return null
214
228
  pointer.clear()
215
229
  feedback.clear()
216
230
  const live = liveNode()
217
231
  if (floorPlaced.applies && !floorPlaced.applies(live)) return null
218
232
  const host = useScene.getState().nodes[event.node.id]
219
233
  if (!host || NON_PHYSICAL_HOST_KINDS.includes(host.type)) return null
220
- let ancestor: AnyNode | undefined = host
221
- while (ancestor) {
222
- if (ancestor.id === node.id) return null
223
- ancestor = ancestor.parentId ? scene.get(ancestor.parentId as AnyNodeId) : undefined
224
- }
234
+ if (wouldCreateHostingCycle(node.id, host, scene)) return null
225
235
  const mesh = sceneRegistry.nodes.get(host.id)
226
236
  if (!mesh) return null
227
237
  const raw = mesh.worldToLocal(new Vector3(...event.position)).toArray()
@@ -262,6 +272,7 @@ export function createRegistryItemSurfaceMove(node: AnyNode) {
262
272
  const placement = resolveSurfacePlacement({
263
273
  host,
264
274
  childKind: node.type,
275
+ childId: node.id,
265
276
  childFootprint: {
266
277
  size: dimensions,
267
278
  rotationY: localYaw,
@@ -278,7 +289,23 @@ export function createRegistryItemSurfaceMove(node: AnyNode) {
278
289
  scene,
279
290
  snapScalar: host.type !== 'item' ? undefined : snapToGrid,
280
291
  checkFootprint: true,
281
- onReject: (reason) => feedback.reject(reason, pointerEventOf(event)),
292
+ onReject: (reason) => {
293
+ if (
294
+ live.parentId !== host.id &&
295
+ !counterHit &&
296
+ host.type !== 'item' &&
297
+ host.type !== 'shelf' &&
298
+ reason !== 'surface-cutout' &&
299
+ reason !== 'surface-occupied'
300
+ )
301
+ return
302
+ if (reason === 'surface-cutout' || reason === 'surface-occupied') {
303
+ ownership.claim(host.id, pointerEventOf(event))
304
+ pointer.hit(host.id, pointerEventOf(event))
305
+ event.stopPropagation()
306
+ }
307
+ feedback.reject(reason, pointerEventOf(event))
308
+ },
282
309
  })
283
310
  if (!placement) return null
284
311
  valid = true
@@ -291,6 +318,7 @@ export function createRegistryItemSurfaceMove(node: AnyNode) {
291
318
  worldPosition: mesh.localToWorld(new Vector3(...placement.position)).toArray(),
292
319
  }
293
320
  grab = corrected.grab
321
+ ownership.claim(host.id, pointerEventOf(event))
294
322
  pointer.hit(host.id, pointerEventOf(event))
295
323
  event.stopPropagation()
296
324
  write(
@@ -344,12 +372,7 @@ export function createRegistryItemSurfaceMove(node: AnyNode) {
344
372
  false,
345
373
  )
346
374
  if (host?.type === 'procedural-item') position.splice(0, 3, ...local.position)
347
- if (
348
- host?.type === 'cabinet' ||
349
- host?.type === 'shelf' ||
350
- host?.type === 'item' ||
351
- host?.type === 'procedural-item'
352
- ) {
375
+ if (host) {
353
376
  const footprint = floorPlaced.footprint?.(live, { nodes: scene.nodes() })
354
377
  const bounds:
355
378
  | { size: [number, number, number]; center?: [number, number, number] }
@@ -376,6 +399,7 @@ export function createRegistryItemSurfaceMove(node: AnyNode) {
376
399
  const placement = resolveSurfacePlacement({
377
400
  host,
378
401
  childKind: live.type,
402
+ childId: live.id,
379
403
  childFootprint: {
380
404
  size: bounds.size,
381
405
  rotationY: Array.isArray(localRotation) ? localRotation[1] : localRotation,
@@ -394,7 +418,14 @@ export function createRegistryItemSurfaceMove(node: AnyNode) {
394
418
  onReject: (reason) => feedback.reject(reason),
395
419
  })
396
420
  valid = !!placement
397
- if (placement && host.type === 'procedural-item') {
421
+ if (!placement) {
422
+ // Null means free rotation to the caller, so return the unchanged stored pose.
423
+ return {
424
+ position: [...live.position] as [number, number, number],
425
+ rotationY: Array.isArray(live.rotation) ? live.rotation[1] : live.rotation,
426
+ }
427
+ }
428
+ if (host.type === 'procedural-item') {
398
429
  const pose =
399
430
  placement.childFrame === 'surface-local'
400
431
  ? placement.surfaceLocal!
@@ -405,16 +436,7 @@ export function createRegistryItemSurfaceMove(node: AnyNode) {
405
436
  rotationY: pose.rotationY,
406
437
  }
407
438
  }
408
- if (placement) position[1] = placement.position[1]
409
- else if (
410
- feedback.reason === 'footprint-outside-surface' ||
411
- feedback.reason === 'footprint-exceeds-host' ||
412
- feedback.reason === 'no-surface'
413
- ) {
414
- const mesh = sceneRegistry.nodes.get(host.id)
415
- if (mesh)
416
- return session.detach(mesh.localToWorld(new Vector3(...position)).toArray(), yaw)
417
- }
439
+ position[1] = placement.position[1]
418
440
  }
419
441
  write(live.parentId, position, yaw)
420
442
  return { position, rotationY: yaw }