@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,204 @@
1
+ 'use client'
2
+
3
+ import {
4
+ type AnyNodeId,
5
+ getStoredLevelHeight,
6
+ type Point,
7
+ type SlabNode,
8
+ sceneRegistry,
9
+ upperFloorHeightControl,
10
+ useScene,
11
+ } from '@pascal-app/core'
12
+ import { useViewer } from '@pascal-app/viewer'
13
+ import { Html } from '@react-three/drei'
14
+ import { type ThreeEvent, useFrame, useThree } from '@react-three/fiber'
15
+ import { useRef, useState } from 'react'
16
+ import { type Group, Plane, Vector2, Vector3 } from 'three'
17
+ import {
18
+ beginFootprintHeightPreview,
19
+ footprintHeightMinimum,
20
+ footprintHeightValue,
21
+ THICK_FLOOR_HINT,
22
+ thickFloorAdvice,
23
+ } from '../../lib/floor-footprints'
24
+ import { formatLinearMeasurement } from '../../lib/measurements'
25
+ import { runRoomHandleDrag } from '../../lib/room-handle-drag'
26
+ import useEditor, { isGridSnapActive } from '../../store/use-editor'
27
+ import useInteractionScope from '../../store/use-interaction-scope'
28
+ import { suppressBoxSelectForPointer } from '../tools/select/box-select-state'
29
+ import { ElevationArrows, outlineCentroid, useHandleScale } from './room-handles'
30
+
31
+ export const FOOTPRINT_HEIGHT_DRAG_LABEL = 'footprint-height'
32
+
33
+ const PILL =
34
+ 'pointer-events-none flex items-center gap-2 whitespace-nowrap rounded-full border border-border/60 bg-background/90 px-4 py-1.5 text-xs tabular-nums shadow-sm backdrop-blur'
35
+ const NOTICE =
36
+ 'pointer-events-none max-w-72 rounded-full border border-border/60 bg-background/95 px-3 py-1 text-center text-muted-foreground text-xs shadow-md backdrop-blur-md'
37
+
38
+ /** The selected footprint floor (base plate), when it is the one selection in select mode. */
39
+ function useSelectedFootprint(): SlabNode | null {
40
+ const selectedId = useViewer((s) =>
41
+ s.selection.selectedIds.length === 1 ? s.selection.selectedIds[0] : null,
42
+ )
43
+ const enabled = useEditor((s) => s.mode === 'select')
44
+ const idle = useInteractionScope(
45
+ (s) =>
46
+ s.scope.kind === 'idle' ||
47
+ (s.scope.kind === 'handle-drag' && s.scope.handle === FOOTPRINT_HEIGHT_DRAG_LABEL),
48
+ )
49
+ const plate = useScene((s) => {
50
+ const node = selectedId ? s.nodes[selectedId as AnyNodeId] : undefined
51
+ return node?.type === 'slab' && node.plateRole === 'base' ? node : null
52
+ })
53
+ return enabled && idle ? plate : null
54
+ }
55
+
56
+ /**
57
+ * A selected footprint floor's one control in 3D: the rooms' arrow–cube–arrow
58
+ * at the middle of the footprint, dragging its height above the ground. The
59
+ * building follows live; the release lands as one undo step. Refused heights
60
+ * (core's conflict, e.g. an upper floor over another footprint) show beside it.
61
+ */
62
+ export function FootprintHeightHandle() {
63
+ const plate = useSelectedFootprint()
64
+ const root = useRef<Group>(null)
65
+ useFrame(() => {
66
+ const level = plate?.parentId ? sceneRegistry.nodes.get(plate.parentId) : null
67
+ if (!root.current) return
68
+ root.current.visible = !!level
69
+ if (level) {
70
+ level.updateWorldMatrix(true, false)
71
+ root.current.matrix.copy(level.matrixWorld)
72
+ }
73
+ })
74
+ if (!plate) return null
75
+ return (
76
+ <group matrixAutoUpdate={false} ref={root}>
77
+ <FootprintHeightArrows plate={plate} />
78
+ </group>
79
+ )
80
+ }
81
+
82
+ function FootprintHeightArrows({ plate }: { plate: SlabNode }) {
83
+ const { camera, raycaster, gl } = useThree()
84
+ const zoom = useHandleScale()
85
+ const unit = useViewer((s) => s.unit)
86
+ const metricNotation = useViewer((s) => s.metricNotation)
87
+ const [hover, setHover] = useState(false)
88
+ const [dragValue, setDragValue] = useState<number | null>(null)
89
+ const [notice, setNotice] = useState<string | null>(null)
90
+ const [hint, setHint] = useState<string | null>(null)
91
+ const upper = useScene((s) => upperFloorHeightControl(s.nodes, plate) !== null)
92
+ const anchor: Point = outlineCentroid([plate.polygon as Point[], ...(plate.holes as Point[][])])
93
+ const elevation = plate.elevation
94
+
95
+ const onPointerDown = (event: ThreeEvent<PointerEvent>) => {
96
+ if (event.button !== 0) return
97
+ event.stopPropagation()
98
+ suppressBoxSelectForPointer(event)
99
+ const levelId = plate.parentId
100
+ const level = levelId ? sceneRegistry.nodes.get(levelId) : null
101
+ if (!(levelId && level)) return
102
+ level.updateWorldMatrix(true, false)
103
+ const toLocal = level.matrixWorld.clone().invert()
104
+ const world = new Vector3(anchor[0], elevation, anchor[1]).applyMatrix4(level.matrixWorld)
105
+ const normal = camera.getWorldPosition(new Vector3()).sub(world).setY(0)
106
+ if (normal.lengthSq() === 0) return
107
+ const plane = new Plane().setFromNormalAndCoplanarPoint(normal.normalize(), world)
108
+ const localY = (clientX: number, clientY: number) => {
109
+ const rect = gl.domElement.getBoundingClientRect()
110
+ raycaster.setFromCamera(
111
+ new Vector2(
112
+ ((clientX - rect.left) / rect.width) * 2 - 1,
113
+ -((clientY - rect.top) / rect.height) * 2 + 1,
114
+ ),
115
+ camera,
116
+ )
117
+ const hit = raycaster.ray.intersectPlane(plane, new Vector3())
118
+ return hit ? hit.applyMatrix4(toLocal).y : null
119
+ }
120
+ const startY = localY(event.nativeEvent.clientX, event.nativeEvent.clientY)
121
+ if (startY === null) return
122
+ const nodes = () => useScene.getState().nodes
123
+ // Heights are read against the plate as the drag found it (the preview moves it).
124
+ const startNodes = nodes()
125
+ const start = footprintHeightValue(startNodes, plate)
126
+ const min = footprintHeightMinimum(startNodes, plate)
127
+ const levelNode = nodes()[levelId as AnyNodeId]
128
+ const max = levelNode?.type === 'level' ? getStoredLevelHeight(levelNode) - 0.5 : 3
129
+ const step = isGridSnapActive() ? 0.05 : 0.01
130
+ let value = start
131
+ const preview = beginFootprintHeightPreview(plate.id)
132
+ setNotice(null)
133
+ setDragValue(start)
134
+ runRoomHandleDrag({
135
+ label: FOOTPRINT_HEIGHT_DRAG_LABEL,
136
+ nodeId: plate.id,
137
+ levelId,
138
+ requires: [plate.id],
139
+ sample: (x, y) => {
140
+ const pointerY = localY(x, y)
141
+ if (pointerY === null) return null
142
+ const raw = start + pointerY - startY
143
+ return Math.max(min, Math.min(max, Math.round(raw / step) * step))
144
+ },
145
+ onValue: (next) => {
146
+ if (Math.abs(next - value) < 1e-6) return
147
+ // Live: the building follows, outside history until the release.
148
+ const refused = preview.preview(next)
149
+ setNotice(refused)
150
+ setHint(thickFloorAdvice(startNodes, plate, next) ? THICK_FLOOR_HINT : null)
151
+ if (!refused) value = next
152
+ setDragValue(next)
153
+ },
154
+ onCommit: () => {
155
+ setNotice(preview.commit(value))
156
+ setDragValue(null)
157
+ setHint(null)
158
+ },
159
+ onCancel: () => {
160
+ preview.cancel()
161
+ setDragValue(null)
162
+ setHint(null)
163
+ },
164
+ })
165
+ }
166
+
167
+ const shown = dragValue ?? null
168
+ return (
169
+ <group>
170
+ <ElevationArrows
171
+ anchor={[anchor[0], elevation, anchor[1]]}
172
+ hover={hover || shown !== null}
173
+ onHoverChange={setHover}
174
+ onPointerDown={onPointerDown}
175
+ zoom={zoom}
176
+ />
177
+ {(shown !== null || notice || hint) && (
178
+ <Html
179
+ center
180
+ position={[anchor[0], elevation + 0.75 * zoom, anchor[1]]}
181
+ zIndexRange={[25, 0]}
182
+ >
183
+ <div className="flex flex-col items-center gap-1" data-footprint-height-pill>
184
+ {shown !== null && (
185
+ <div className={PILL}>
186
+ <span className="font-medium text-foreground">
187
+ {upper ? 'Floor top' : 'Foundation'}
188
+ </span>
189
+ <span className="text-muted-foreground">
190
+ {formatLinearMeasurement(shown, unit, metricNotation)}
191
+ </span>
192
+ </div>
193
+ )}
194
+ {(notice ?? hint) && (
195
+ <div className={NOTICE} data-footprint-height-notice role="status">
196
+ {notice ?? hint}
197
+ </div>
198
+ )}
199
+ </div>
200
+ </Html>
201
+ )}
202
+ </group>
203
+ )
204
+ }
@@ -1,7 +1,12 @@
1
1
  'use client'
2
2
 
3
3
  import { type AnyNodeId, emitter, type GridEvent, sceneRegistry } from '@pascal-app/core'
4
- import { GRID_LAYER, getSceneTheme, useViewer } from '@pascal-app/viewer'
4
+ import {
5
+ GRID_LAYER,
6
+ getSceneTheme,
7
+ useImmersiveXRPresentation,
8
+ useViewer,
9
+ } from '@pascal-app/viewer'
5
10
  import { useFrame } from '@react-three/fiber'
6
11
  import { useEffect, useMemo, useRef, useState } from 'react'
7
12
  import { DoubleSide, type Mesh, PlaneGeometry, Quaternion, Vector2, Vector3 } from 'three'
@@ -10,9 +15,12 @@ import { MeshBasicNodeMaterial } from 'three/webgpu'
10
15
  import { useCeilingEvents } from '../../hooks/use-ceiling-events'
11
16
  import { useGridEvents } from '../../hooks/use-grid-events'
12
17
  import { getPlacementSurface, usesOrientedPlacementPlane } from '../../lib/active-placement-surface'
13
- import useEditor, { isGridSnapActive } from '../../store/use-editor'
18
+ import { gridLocalNormal, gridLocalPoint } from '../../lib/grid-frame'
19
+ import useEditor, { getActiveSnapContext, isGridSnapActive } from '../../store/use-editor'
14
20
  import { getMovingNode } from '../../store/use-interaction-scope'
15
21
 
22
+ export const EDITOR_GRID_INPUT_NAME = 'pascal-editor-grid-input'
23
+
16
24
  // Reveal radius (m) of the cursor-local grid patch shown while placing/moving in
17
25
  // grid-snap mode — much tighter than the idle reveal so only the area you're
18
26
  // about to snap into lights up.
@@ -45,6 +53,7 @@ export const Grid = ({
45
53
  fadeStrength?: number
46
54
  revealRadius?: number
47
55
  }) => {
56
+ const immersive = useImmersiveXRPresentation()
48
57
  const isDark = useViewer((state) => getSceneTheme(state.sceneTheme).appearance === 'dark')
49
58
 
50
59
  // Use slightly lighter colors for dark themes' grid to make it apparent
@@ -60,6 +69,9 @@ export const Grid = ({
60
69
  const wallCursorRef = useRef(new Vector3())
61
70
  // Scratch for deriving a moving wall-item's host normal from its mesh.
62
71
  const wallNormalRef = useRef(new Vector3())
72
+ const localSurfaceRef = useRef(new Vector3())
73
+ const localNormalRef = useRef(new Vector3())
74
+ const localCursorRef = useRef(new Vector3())
63
75
  // Last Y pushed to `gridY` state, so the per-frame surface follow only triggers
64
76
  // a React re-render when the height actually changes (not every frame).
65
77
  const lastGridYRef = useRef<number | null>(null)
@@ -176,10 +188,11 @@ export const Grid = ({
176
188
  // laid-flat orientation maps `positionLocal.y` to world `-Z` relative to the
177
189
  // mesh origin. The cursor is recomputed every frame from the stored world hit
178
190
  // so the reveal stays put regardless of where the mesh origin sits.
179
- const lastWorldCursorRef = useRef<{ x: number; z: number } | null>(null)
191
+ const lastWorldCursorRef = useRef<Vector3 | null>(null)
180
192
  useEffect(() => {
181
193
  const onGridMove = (event: GridEvent) => {
182
- lastWorldCursorRef.current = { x: event.position[0], z: event.position[2] }
194
+ lastWorldCursorRef.current ??= new Vector3()
195
+ lastWorldCursorRef.current.set(...event.position)
183
196
  }
184
197
 
185
198
  emitter.on('grid:move', onGridMove)
@@ -194,7 +207,12 @@ export const Grid = ({
194
207
  if (levelId) {
195
208
  const levelMesh = sceneRegistry.nodes.get(levelId)
196
209
  if (levelMesh) {
197
- levelY = levelMesh.position.y
210
+ levelMesh.getWorldPosition(worldPosRef.current)
211
+ levelY = gridLocalPoint(
212
+ gridRef.current.parent,
213
+ worldPosRef.current,
214
+ localSurfaceRef.current,
215
+ ).y
198
216
  }
199
217
  }
200
218
 
@@ -229,6 +247,10 @@ export const Grid = ({
229
247
  }
230
248
 
231
249
  const gridMesh = gridRef.current
250
+ if (surfacePoint) {
251
+ surfacePoint = gridLocalPoint(gridMesh.parent, surfacePoint, localSurfaceRef.current)
252
+ surfaceNormal = gridLocalNormal(gridMesh.parent, surfaceNormal, localNormalRef.current)
253
+ }
232
254
  const onOrientedPlane = surfacePoint != null && usesOrientedPlacementPlane(surfaceNormal)
233
255
  if (onOrientedPlane && surfacePoint) {
234
256
  // Surface-anchored lattice: orient the plane into the host and pin the mesh to
@@ -269,7 +291,8 @@ export const Grid = ({
269
291
  gridMesh.quaternion.copy(HORIZONTAL_QUATERNION)
270
292
  const world = lastWorldCursorRef.current
271
293
  if (world) {
272
- cursorPositionRef.current.set(world.x, -world.z)
294
+ const local = gridLocalPoint(gridMesh.parent, world, localCursorRef.current)
295
+ cursorPositionRef.current.set(local.x, -local.z)
273
296
  }
274
297
  if (lastGridYRef.current !== targetY) {
275
298
  lastGridYRef.current = targetY
@@ -293,7 +316,7 @@ export const Grid = ({
293
316
  // from the interaction scope OR the armed build tool and is true only when
294
317
  // that context resolves to grid, so it IS the gate. (Previously this also
295
318
  // required a ghost in flight, so a merely-armed draft tool showed nothing.)
296
- const snapPatchVisible = isGridSnapActive()
319
+ const snapPatchVisible = isGridSnapActive() || (immersive && getActiveSnapContext() !== null)
297
320
  revealRadiusUniform.value = PLACEMENT_REVEAL_RADIUS
298
321
  baseAlphaUniform.value = 0
299
322
  cellSizeUniform.value = useEditor.getState().gridSnapStep
@@ -320,6 +343,7 @@ export const Grid = ({
320
343
  geometry={geometry}
321
344
  layers={GRID_LAYER}
322
345
  material={material}
346
+ name={EDITOR_GRID_INPUT_NAME}
323
347
  ref={gridRef}
324
348
  renderOrder={1}
325
349
  />
@@ -18,10 +18,13 @@ import { markPerfAction, useViewer } from '@pascal-app/viewer'
18
18
  import { Plane, Vector2, Vector3 } from 'three'
19
19
  import { GROUP_MOVE_DRAG_LABEL } from '../../lib/contextual-help'
20
20
  import { clientToPlan } from '../../lib/floorplan/plan-coords'
21
+ import { deleteSelectedSeparator, requestRoomDeletion } from '../../lib/room-structure-commands'
22
+ import { captureElementActionOrigin, completeElementAction } from '../../lib/room-zone-routing'
21
23
  import {
22
24
  copySelectedNodesToEditorClipboard,
23
25
  duplicateNodesToLevel,
24
26
  getEditorClipboardSnapshot,
27
+ type PasteRefusal,
25
28
  pasteSystemEditorClipboardToLevel,
26
29
  } from '../../lib/scene-clipboard'
27
30
  import { emitDeleteSFX, sfxEmitter } from '../../lib/sfx-bus'
@@ -33,13 +36,13 @@ import useEditor, {
33
36
  isGridSnapActive,
34
37
  isMagneticSnapActive,
35
38
  } from '../../store/use-editor'
39
+ import useFloorplanMode from '../../store/use-floorplan-mode'
36
40
  import useInteractionScope from '../../store/use-interaction-scope'
37
41
  import { useFloorplanGroupDrag } from '../editor-2d/floorplan-group-move'
38
42
  import {
39
43
  classifyParticipant,
40
44
  collectParticipants,
41
45
  computeGroupBox,
42
- expandToComponent,
43
46
  groupPlanBounds,
44
47
  levelFrame,
45
48
  planBoundsCenter,
@@ -83,8 +86,8 @@ export function canGroupPickUp(): boolean {
83
86
  * until a click commits, mirroring the single-node `movingNode` flow. Returns
84
87
  * false when the selection holds no transformable participants.
85
88
  *
86
- * `scopeToSelection` limits the moving set to the selected participants —
87
- * no connected-component expansion and no welded-neighbor endpoints. The
89
+ * `scopeToSelection` drops the welded-neighbor endpoints, so connected walls
90
+ * don't stretch along with the move. The
88
91
  * Duplicate flow needs this: its clones sit EXACTLY on the originals, so
89
92
  * junction coincidence would otherwise weld the originals into the pick-up
90
93
  * and drag them along with the copies.
@@ -96,10 +99,7 @@ export function startGroupPickUp(
96
99
  const participantIds = groupParticipantIds()
97
100
  if (participantIds.length === 0) return false
98
101
  const nodes = useScene.getState().nodes
99
- const fullIds = opts.scopeToSelection
100
- ? participantIds
101
- : expandToComponent(participantIds, nodes, levelId)
102
- const collected = collectParticipants(fullIds, nodes, levelId)
102
+ const collected = collectParticipants(participantIds, nodes, levelId)
103
103
  // Mutable: mid-carry R/T rotates these snapshots in place.
104
104
  let starts = collected.starts
105
105
  let links = opts.scopeToSelection ? [] : collected.links
@@ -107,8 +107,8 @@ export function startGroupPickUp(
107
107
  const affectedIds: AnyNodeId[] = [...starts.map((s) => s.id), ...links.map((l) => l.id)]
108
108
 
109
109
  const { inverse: frameInv } = levelFrame(levelId)
110
- const restBox = computeGroupBox(fullIds)
111
- const startBounds = groupPlanBounds(restBox, starts, frameInv)
110
+ const restBox = computeGroupBox(participantIds)
111
+ const startBounds = groupPlanBounds(starts, frameInv)
112
112
  if (!startBounds) return false
113
113
  // Mutable: mid-carry R/T re-seeds the footprint around the same pivot.
114
114
  let restBounds = startBounds
@@ -460,12 +460,39 @@ function removeUnusedPasteMaterials(materialIds: SceneMaterialId[]) {
460
460
  }
461
461
  }
462
462
 
463
+ const REFUSED_PASTE_REASONS: Record<PasteRefusal, [one: string, many: string]> = {
464
+ 'no-access': [
465
+ 'you no longer have access to the project it came from.',
466
+ 'you no longer have access to the project they came from.',
467
+ ],
468
+ 'no-copies': [
469
+ 'this version is not available to copy from the source project.',
470
+ 'these versions are not available to copy from the source project.',
471
+ ],
472
+ failed: ["it couldn't be copied. Try again.", "they couldn't be copied. Try again."],
473
+ }
474
+
475
+ /** Why scripted objects were left out of a paste, or of a build loaded from a file. */
476
+ export function refusedObjectsNotice(
477
+ count: number,
478
+ refusal: PasteRefusal,
479
+ action: 'pasted' | 'loaded' = 'pasted',
480
+ ) {
481
+ const [one, many] = REFUSED_PASTE_REASONS[refusal]
482
+ return count === 1
483
+ ? `1 object wasn't ${action}: ${one}`
484
+ : `${count} objects weren't ${action}: ${many}`
485
+ }
486
+
487
+ let pastePending = false
488
+
463
489
  /**
464
490
  * Paste the Pascal scene payload from the browser clipboard onto the active
465
491
  * level, then carry the clones under the cursor until click-to-place. Escape
466
492
  * removes the uncommitted clones and any scene materials imported with them.
467
493
  */
468
494
  export async function pasteSelectionAndPickUp(targetLevelId?: AnyNodeId): Promise<boolean> {
495
+ if (pastePending) return false
469
496
  const activeScope = useInteractionScope.getState().scope
470
497
  if (activeScope.kind === 'placing' || activeScope.kind === 'moving') {
471
498
  emitter.emit('tool:cancel')
@@ -480,7 +507,18 @@ export async function pasteSelectionAndPickUp(targetLevelId?: AnyNodeId): Promis
480
507
  // above there is nothing to abandon.
481
508
  if (isBrushMode(useEditor.getState().mode)) useEditor.getState().setMode('select')
482
509
 
483
- const result = await pasteSystemEditorClipboardToLevel(targetLevelId)
510
+ pastePending = true
511
+ let result: Awaited<ReturnType<typeof pasteSystemEditorClipboardToLevel>>
512
+ try {
513
+ result = await pasteSystemEditorClipboardToLevel(targetLevelId)
514
+ } finally {
515
+ pastePending = false
516
+ }
517
+ if (result?.refusal) {
518
+ useFloorplanMode
519
+ .getState()
520
+ .showNotice(refusedObjectsNotice(result.refusedIds.length, result.refusal))
521
+ }
484
522
  if (!result || result.pastedIds.length === 0) return false
485
523
 
486
524
  const discardPaste = () => {
@@ -551,7 +589,16 @@ export function cutSelectionToEditorClipboard(): boolean {
551
589
  */
552
590
  export function deleteSelection(): boolean {
553
591
  const selectedIds = useViewer.getState().selection.selectedIds as AnyNodeId[]
554
- if (selectedIds.length === 0) return false
592
+ if (selectedIds.length === 0) {
593
+ const room = useEditor.getState().room
594
+ if (!room) return false
595
+ requestRoomDeletion(room.zoneId)
596
+ return true
597
+ }
598
+ // A drilled piece of a room (a wall, a separator, its ceiling) deleted: back to the room.
599
+ const origin = captureElementActionOrigin(selectedIds)
600
+ if (selectedIds.length === 1 && useScene.getState().nodes[selectedIds[0]!]?.type === 'separator')
601
+ return deleteSelectedSeparator(selectedIds[0]!, origin)
555
602
 
556
603
  const commitDelete = () => {
557
604
  const detail =
@@ -566,6 +613,7 @@ export function deleteSelection(): boolean {
566
613
  }
567
614
  useScene.getState().deleteNodes(selectedIds)
568
615
  useViewer.getState().setSelection({ selectedIds: [] })
616
+ completeElementAction(origin)
569
617
  }
570
618
 
571
619
  if (selectedIds.length >= BULK_DELETE_THRESHOLD) {
@@ -15,7 +15,12 @@ import useSessionGroups, {
15
15
  ungroupCurrentSelection,
16
16
  } from '../../store/use-session-groups'
17
17
  import { deleteSelection, duplicateSelectionAndPickUp, startGroupPickUp } from './group-actions'
18
- import { classifyParticipant, computeGroupBox, expandToComponent } from './group-transform-shared'
18
+ import {
19
+ classifyParticipant,
20
+ computeGroupBox,
21
+ computeGroupPlanBox,
22
+ levelFrame,
23
+ } from './group-transform-shared'
19
24
  import { NodeActionMenu } from './node-action-menu'
20
25
  import { useMeshSettleEpoch } from './use-mesh-settle-epoch'
21
26
 
@@ -69,15 +74,20 @@ export function GroupFloatingActionMenu() {
69
74
  const anchor = useMemo(() => {
70
75
  void meshEpoch
71
76
  if (participantIds.length === 0) return null
72
- const fullIds = expandToComponent(participantIds, nodes, levelId)
73
- const box = computeGroupBox(fullIds)
77
+ const box = computeGroupBox(participantIds)
74
78
  if (!box) return null
75
- return new THREE.Vector3(
76
- (box.min.x + box.max.x) / 2,
77
- box.max.y + MENU_Y_OFFSET,
78
- (box.min.z + box.max.z) / 2,
79
- )
80
- }, [participantIds, nodes, levelId, meshEpoch])
79
+ // Over the level-frame box's centre (the dashed boxes' and gizmo's), so the
80
+ // pill stays on the box under a rotated building; the mesh box gives the top.
81
+ const levelId = useViewer.getState().selection.levelId
82
+ const plan = computeGroupPlanBox(participantIds, levelId)
83
+ const anchor = plan
84
+ ? new THREE.Vector3((plan.minX + plan.maxX) / 2, 0, (plan.minZ + plan.maxZ) / 2).applyMatrix4(
85
+ levelFrame(levelId).matrix,
86
+ )
87
+ : new THREE.Vector3((box.min.x + box.max.x) / 2, 0, (box.min.z + box.max.z) / 2)
88
+ anchor.y = box.max.y + MENU_Y_OFFSET
89
+ return anchor
90
+ }, [participantIds, meshEpoch])
81
91
 
82
92
  useFrame((state) => {
83
93
  if (!(menuScaleRef.current && groupRef.current)) return
@@ -31,7 +31,6 @@ import {
31
31
  classifyParticipant,
32
32
  collectParticipants,
33
33
  computeGroupBox,
34
- expandToComponent,
35
34
  type GroupPlanBounds,
36
35
  groupPlanBounds,
37
36
  levelFrame,
@@ -107,16 +106,15 @@ export function armGroupMove3d(args: {
107
106
  const participantIds = selectedIds.filter(
108
107
  (id) => classifyParticipant(nodes[id as AnyNodeId], levelId, nodes) !== null,
109
108
  )
110
- // Move the full connected wall/fence component, mirroring the 2D session.
111
- const fullIds = expandToComponent(participantIds, nodes, levelId)
112
- const { starts, links } = collectParticipants(fullIds, nodes, levelId)
109
+ // Only the selection moves; connected walls stretch through `links`, as in 2D.
110
+ const { starts, links } = collectParticipants(participantIds, nodes, levelId)
113
111
  if (starts.length === 0) return null
114
112
  const affectedIds: AnyNodeId[] = [...starts.map((s) => s.id), ...links.map((l) => l.id)]
115
113
 
116
114
  // Horizontal drag plane at the group's base; placements live in the level
117
115
  // frame, so world-space plane hits convert through it (a rotated building
118
116
  // would otherwise drift off-axis from the cursor).
119
- const restBox = computeGroupBox(fullIds)
117
+ const restBox = computeGroupBox(participantIds)
120
118
  if (!restBox) return null
121
119
  const plane = new Plane(new Vector3(0, 1, 0), -restBox.min.y)
122
120
  const { inverse: frameInv } = levelFrame(levelId)
@@ -136,7 +134,7 @@ export function armGroupMove3d(args: {
136
134
  if (n && !movingIdSet.has(nid)) staticNodes[nid] = n
137
135
  }
138
136
  const candidates = collectAlignmentAnchors(staticNodes, '', levelId)
139
- const restBounds = groupPlanBounds(restBox, starts, frameInv)
137
+ const restBounds = groupPlanBounds(starts, frameInv)
140
138
  if (!restBounds) return null
141
139
  const restAnchors = bboxCornerAnchors(
142
140
  'group-move',