@pascal-app/editor 1.0.3 → 1.0.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (624) hide show
  1. package/README.md +106 -0
  2. package/package.json +42 -38
  3. package/src/components/editor/__bench__/room-label-placement.bench.ts +108 -0
  4. package/src/components/editor/action-menu-button.tsx +83 -0
  5. package/src/components/editor/bake-thumbnail.tsx +307 -5
  6. package/src/components/editor/capture-level-picker.tsx +72 -0
  7. package/src/components/editor/construction-reveal-audio.test.tsx +150 -0
  8. package/src/components/editor/construction-reveal-audio.tsx +40 -0
  9. package/src/components/editor/custom-camera-controls.tsx +275 -57
  10. package/src/components/editor/delete-confirmation-dialog.tsx +297 -7
  11. package/src/components/editor/duplicate-node.ts +244 -0
  12. package/src/components/editor/editor-layout-mobile.tsx +37 -26
  13. package/src/components/editor/editor-layout-v2.tsx +132 -59
  14. package/src/components/editor/export-manager.tsx +47 -26
  15. package/src/components/editor/first-person/build-collider-world.test.ts +320 -0
  16. package/src/components/editor/first-person/build-collider-world.ts +28 -7
  17. package/src/components/editor/first-person/ground-holes.test.ts +128 -0
  18. package/src/components/editor/first-person/ground-holes.ts +130 -0
  19. package/src/components/editor/first-person/mechanism-targets.test.ts +162 -0
  20. package/src/components/editor/first-person/mechanism-targets.ts +88 -0
  21. package/src/components/editor/first-person-controls.tsx +220 -9
  22. package/src/components/editor/floating-action-menu.tsx +63 -207
  23. package/src/components/editor/floor-aware-camera-controls.test.ts +466 -0
  24. package/src/components/editor/floor-aware-camera-controls.ts +324 -0
  25. package/src/components/editor/floor-openings-overlay.tsx +243 -0
  26. package/src/components/editor/floor-region-controls.tsx +846 -0
  27. package/src/components/editor/floor-region-webgpu-materials.test.ts +12 -0
  28. package/src/components/editor/floor-surface.test.ts +165 -0
  29. package/src/components/editor/floor-surface.ts +182 -0
  30. package/src/components/editor/floorplan-background-selection.test.ts +11 -2
  31. package/src/components/editor/floorplan-background-selection.ts +1 -3
  32. package/src/components/editor/floorplan-mode-coordinator.tsx +2 -2
  33. package/src/components/editor/floorplan-panel.tsx +414 -363
  34. package/src/components/editor/follow-pascal.test.tsx +912 -0
  35. package/src/components/editor/follow-pascal.tsx +354 -0
  36. package/src/components/editor/footprint-height-handle.tsx +204 -0
  37. package/src/components/editor/group-actions.ts +56 -2
  38. package/src/components/editor/group-move-3d.ts +2 -0
  39. package/src/components/editor/group-rotate-handle.tsx +2 -0
  40. package/src/components/editor/group-transform-shared.test.ts +460 -0
  41. package/src/components/editor/handles/handle-hit-priority.test.tsx +63 -0
  42. package/src/components/editor/handles/handle-hit-priority.tsx +37 -0
  43. package/src/components/editor/handles/swallow-next-click.test.ts +28 -0
  44. package/src/components/editor/handles/use-handle-drag.ts +269 -197
  45. package/src/components/editor/index.tsx +409 -197
  46. package/src/components/editor/join-walls-pill.tsx +44 -0
  47. package/src/components/editor/node-action-menu.tsx +68 -70
  48. package/src/components/editor/node-arrow-handles.tsx +25 -6
  49. package/src/components/editor/open-wall-ends-3d-layer.tsx +273 -0
  50. package/src/components/editor/open-wall-ends-hint.tsx +89 -0
  51. package/src/components/editor/quick-measurement-hud.tsx +3 -2
  52. package/src/components/editor/registry-action-contributions.test.tsx +63 -0
  53. package/src/components/editor/registry-action-contributions.tsx +80 -1
  54. package/src/components/editor/riser-diagram-panel.tsx +14 -7
  55. package/src/components/editor/room-controls.tsx +847 -0
  56. package/src/components/editor/room-floor-highlight.tsx +128 -0
  57. package/src/components/editor/room-handles-2d.tsx +354 -0
  58. package/src/components/editor/room-handles.tsx +675 -0
  59. package/src/components/editor/room-highlight.tsx +233 -0
  60. package/src/components/editor/room-labels.test.ts +850 -0
  61. package/src/components/editor/room-labels.tsx +1890 -0
  62. package/src/components/editor/room-transform-ghost.tsx +256 -0
  63. package/src/components/editor/room-webgpu-materials.test.ts +27 -0
  64. package/src/components/editor/selection-manager.tsx +474 -419
  65. package/src/components/editor/slab-hole-highlights.tsx +5 -2
  66. package/src/components/editor/snapshot-capture-overlay.tsx +7 -2
  67. package/src/components/editor/snapshot-capture.test.ts +119 -60
  68. package/src/components/editor/snapshot-capture.ts +36 -5
  69. package/src/components/editor/thumbnail-generator.test.ts +654 -0
  70. package/src/components/editor/thumbnail-generator.tsx +1103 -223
  71. package/src/components/editor/use-floorplan-background-placement.ts +34 -12
  72. package/src/components/editor/vector-edge-extractor.tsx +121 -0
  73. package/src/components/editor/views/host-view.tsx +61 -0
  74. package/src/components/editor/views/plugins-view.tsx +663 -0
  75. package/src/components/editor/views/scene-region.tsx +59 -0
  76. package/src/components/editor/views/use-editor-views.ts +57 -0
  77. package/src/components/editor/views/view-bar.test.tsx +54 -0
  78. package/src/components/editor/views/view-bar.tsx +343 -0
  79. package/src/components/editor/wall-justification.test.ts +140 -0
  80. package/src/components/editor/wall-move-side-handles.tsx +43 -142
  81. package/src/components/editor/wall-push-arrow.tsx +92 -0
  82. package/src/components/editor/wall-region-handles.tsx +509 -0
  83. package/src/components/editor/wall-snap-beacon-layer.tsx +8 -4
  84. package/src/components/editor-2d/floorplan-cursor-indicator-overlay.tsx +8 -4
  85. package/src/components/editor-2d/floorplan-group-move.tsx +3 -0
  86. package/src/components/editor-2d/floorplan-open-wall-ends-layer.test.tsx +86 -0
  87. package/src/components/editor-2d/floorplan-open-wall-ends-layer.tsx +290 -0
  88. package/src/components/editor-2d/floorplan-registered-tool-layer.tsx +2 -2
  89. package/src/components/editor-2d/floorplan-registry-action-menu.tsx +11 -1
  90. package/src/components/editor-2d/floorplan-registry-move-overlay.tsx +116 -106
  91. package/src/components/editor-2d/renderers/floorplan-dimension-edit-overlay.test.ts +156 -0
  92. package/src/components/editor-2d/renderers/floorplan-dimension-edit-overlay.tsx +278 -0
  93. package/src/components/editor-2d/renderers/floorplan-dimension-renderer.tsx +77 -0
  94. package/src/components/editor-2d/renderers/{floorplan-draft-wall-measurement.tsx → floorplan-draft-measurement.tsx} +8 -8
  95. package/src/components/editor-2d/renderers/floorplan-geometry-renderer.tsx +4 -1
  96. package/src/components/editor-2d/renderers/floorplan-registry-layer.test.ts +38 -8
  97. package/src/components/editor-2d/renderers/floorplan-registry-layer.tsx +319 -233
  98. package/src/components/editor-2d/renderers/floorplan-reveal.test.tsx +254 -0
  99. package/src/components/editor-2d/renderers/floorplan-reveal.ts +141 -0
  100. package/src/components/editor-2d/svg-paths.ts +12 -15
  101. package/src/components/point-ask/bubble.tsx +538 -0
  102. package/src/components/point-ask/hover-chip.tsx +91 -0
  103. package/src/components/point-ask/mode-pill.tsx +92 -0
  104. package/src/components/point-ask/optional-motion.ts +22 -0
  105. package/src/components/point-ask/outline-corners.tsx +93 -0
  106. package/src/components/point-ask/pins.tsx +711 -0
  107. package/src/components/point-ask/point-ask-layer.tsx +83 -0
  108. package/src/components/point-ask/point-ask-styles.ts +105 -0
  109. package/src/components/point-ask/region-marquee-styles.ts +15 -0
  110. package/src/components/point-ask/region-marquee.tsx +64 -0
  111. package/src/components/procedural-items/inspector.tsx +40 -15
  112. package/src/components/systems/ceiling/ceiling-bracket-batch.test.ts +12 -2
  113. package/src/components/systems/ceiling/ceiling-selection-affordance-system.test.ts +97 -14
  114. package/src/components/systems/ceiling/ceiling-selection-affordance-system.tsx +18 -18
  115. package/src/components/systems/roof/roof-edit-system.tsx +1 -0
  116. package/src/components/systems/zone/zone-system.test.ts +47 -0
  117. package/src/components/systems/zone/zone-system.tsx +29 -68
  118. package/src/components/tools/distribution/connection-compatibility.test.ts +34 -0
  119. package/src/components/tools/distribution/connection-compatibility.ts +43 -0
  120. package/src/components/tools/distribution/distribution-public.ts +9 -0
  121. package/src/components/tools/distribution/distribution-run-contract.test.ts +26 -0
  122. package/src/components/tools/distribution/distribution-run-contract.ts +94 -0
  123. package/src/components/tools/distribution/distribution-run-tool.test.ts +241 -0
  124. package/src/components/tools/distribution/distribution-run-tool.test.tsx +125 -0
  125. package/src/components/tools/distribution/distribution-run-tool.tsx +1323 -0
  126. package/src/components/tools/distribution/draw-alignment.ts +35 -0
  127. package/src/components/tools/distribution/ports.test.ts +56 -0
  128. package/src/components/tools/distribution/ports.ts +378 -0
  129. package/src/components/tools/distribution/run-cursor.test.ts +135 -0
  130. package/src/components/tools/distribution/run-cursor.ts +36 -0
  131. package/src/components/tools/distribution/run-direction-feedback.test.ts +60 -0
  132. package/src/components/tools/distribution/run-direction-feedback.tsx +251 -0
  133. package/src/components/tools/distribution/run-port-snap.test.ts +79 -0
  134. package/src/components/tools/distribution/run-port-snap.ts +25 -0
  135. package/src/components/tools/distribution/run-screen-direction.test.ts +23 -0
  136. package/src/components/tools/distribution/run-screen-direction.ts +30 -0
  137. package/src/components/tools/elevator/elevator-tool.tsx +3 -6
  138. package/src/components/tools/elevator/move-elevator-tool.tsx +2 -3
  139. package/src/components/tools/item/item-surface-strategy-resolver.test.ts +67 -68
  140. package/src/components/tools/item/placement-history.test.tsx +512 -0
  141. package/src/components/tools/item/placement-strategies.test.ts +46 -1
  142. package/src/components/tools/item/placement-strategies.ts +11 -10
  143. package/src/components/tools/item/shelf-surface-strategy-resolver.test.ts +56 -56
  144. package/src/components/tools/item/use-draft-node.test.tsx +432 -1
  145. package/src/components/tools/item/use-draft-node.ts +166 -95
  146. package/src/components/tools/item/use-placement-coordinator.tsx +148 -95
  147. package/src/components/tools/paint-region/use-paint-tint.ts +37 -0
  148. package/src/components/tools/paint-region/wall-paint-region-tool.tsx +466 -0
  149. package/src/components/tools/registry/item-surface-move.test.ts +62 -63
  150. package/src/components/tools/registry/item-surface-move.ts +2 -0
  151. package/src/components/tools/registry/item-surface-resolver.test.ts +26 -26
  152. package/src/components/tools/registry/move-registry-node-tool.test.ts +268 -1
  153. package/src/components/tools/registry/move-registry-node-tool.tsx +66 -30
  154. package/src/components/tools/select/box-select-tool.tsx +2 -12
  155. package/src/components/tools/select/plane-box-select-tool.test.tsx +79 -0
  156. package/src/components/tools/select/plane-box-select-tool.tsx +35 -22
  157. package/src/components/tools/select/select-candidates.test.ts +37 -4
  158. package/src/components/tools/select/select-candidates.ts +15 -11
  159. package/src/components/tools/shared/cursor-sphere.tsx +36 -12
  160. package/src/components/tools/site/site-boundary-editor.tsx +1 -2
  161. package/src/components/tools/tool-manager.tsx +59 -24
  162. package/src/components/tools/wall/wall-drafting.test.ts +175 -47
  163. package/src/components/tools/zone/zone-tool.tsx +4 -12
  164. package/src/components/ui/action-menu/action-button.tsx +9 -1
  165. package/src/components/ui/action-menu/camera-actions.tsx +9 -4
  166. package/src/components/ui/action-menu/control-modes.tsx +15 -42
  167. package/src/components/ui/action-menu/follow-pascal-pill.tsx +32 -0
  168. package/src/components/ui/action-menu/follow-pascal-toggle.tsx +36 -0
  169. package/src/components/ui/action-menu/index.tsx +56 -33
  170. package/src/components/ui/action-menu/measurement-control.tsx +14 -19
  171. package/src/components/ui/action-menu/placement.ts +20 -0
  172. package/src/components/ui/action-menu/point-ask-button.tsx +52 -0
  173. package/src/components/ui/action-menu/view-toggles.tsx +47 -42
  174. package/src/components/ui/build-panel/build-panel.tsx +242 -0
  175. package/src/components/ui/command-palette/editor-commands.tsx +7 -42
  176. package/src/components/ui/command-palette/index.tsx +50 -39
  177. package/src/components/ui/command-palette/ranked-groups.test.ts +31 -0
  178. package/src/components/ui/controls/action-button.tsx +5 -2
  179. package/src/components/ui/controls/finish-swatch.tsx +110 -0
  180. package/src/components/ui/controls/material-paint-panel.tsx +97 -26
  181. package/src/components/ui/controls/material-picker.tsx +4 -1
  182. package/src/components/ui/controls/metric-control.tsx +1 -1
  183. package/src/components/ui/controls/scene-material-list.tsx +4 -32
  184. package/src/components/ui/controls/segmented-control.tsx +21 -8
  185. package/src/components/ui/controls/select-control.tsx +65 -0
  186. package/src/components/ui/controls/slider-control.tsx +1 -1
  187. package/src/components/ui/controls/toggle-control.tsx +3 -3
  188. package/src/components/ui/controls/tool-options-panel.tsx +1 -1
  189. package/src/components/ui/floating-level-selector.tsx +159 -59
  190. package/src/components/ui/helpers/building-helper.tsx +4 -1
  191. package/src/components/ui/helpers/contextual-helper-panel.tsx +181 -72
  192. package/src/components/ui/helpers/helper-manager.test.tsx +121 -0
  193. package/src/components/ui/helpers/helper-manager.tsx +331 -14
  194. package/src/components/ui/helpers/item-helper.tsx +17 -1
  195. package/src/components/ui/helpers/registered-tool-helper.tsx +8 -1
  196. package/src/components/ui/icon-ref.tsx +45 -1
  197. package/src/components/ui/level-duplicate-dialog.tsx +1 -1
  198. package/src/components/ui/node-icon-image.tsx +16 -0
  199. package/src/components/ui/panels/collections/collections-popover.tsx +28 -22
  200. package/src/components/ui/panels/floor-foundation-panel.tsx +551 -0
  201. package/src/components/ui/panels/floor-opening-panel.tsx +143 -0
  202. package/src/components/ui/panels/mobile-panel-sheet.tsx +4 -4
  203. package/src/components/ui/panels/mobile-selection-bar.tsx +8 -5
  204. package/src/components/ui/panels/multi-field-value.test.ts +26 -3
  205. package/src/components/ui/panels/multi-field-value.ts +6 -3
  206. package/src/components/ui/panels/multi-parametric-inspector.tsx +52 -2
  207. package/src/components/ui/panels/multi-selection-panel.tsx +161 -5
  208. package/src/components/ui/panels/node-display.ts +8 -3
  209. package/src/components/ui/panels/panel-manager.tsx +103 -68
  210. package/src/components/ui/panels/panel-wrapper.tsx +130 -62
  211. package/src/components/ui/panels/parametric-field-control.tsx +3 -3
  212. package/src/components/ui/panels/parametric-inspector.tsx +7 -7
  213. package/src/components/ui/panels/reference-panel.tsx +2 -3
  214. package/src/components/ui/panels/room-construction-rows.tsx +774 -0
  215. package/src/components/ui/panels/room-name-options.test.ts +55 -0
  216. package/src/components/ui/panels/room-name-options.ts +47 -0
  217. package/src/components/ui/panels/room-openings-field.tsx +110 -0
  218. package/src/components/ui/panels/room-panel.tsx +790 -0
  219. package/src/components/ui/panels/shape-choice.tsx +58 -0
  220. package/src/components/ui/panels/wall-region-list.tsx +109 -0
  221. package/src/components/ui/primitives/badge.tsx +8 -0
  222. package/src/components/ui/primitives/color-dot.tsx +36 -24
  223. package/src/components/ui/primitives/sidebar.tsx +3 -1
  224. package/src/components/ui/right-stack.test.ts +29 -0
  225. package/src/components/ui/right-stack.tsx +164 -0
  226. package/src/components/ui/sidebar/app-sidebar.tsx +1 -1
  227. package/src/components/ui/sidebar/icon-rail.tsx +8 -3
  228. package/src/components/ui/sidebar/panels/items-panel/function-tree-panel.tsx +1 -1
  229. package/src/components/ui/sidebar/panels/items-panel/index.tsx +6 -6
  230. package/src/components/ui/sidebar/panels/settings-panel/animation-speed-setting.tsx +40 -0
  231. package/src/components/ui/sidebar/panels/settings-panel/audio-settings-dialog.tsx +7 -8
  232. package/src/components/ui/sidebar/panels/settings-panel/construction-animation-setting.test.tsx +170 -0
  233. package/src/components/ui/sidebar/panels/settings-panel/construction-animation-setting.tsx +92 -0
  234. package/src/components/ui/sidebar/panels/settings-panel/index.tsx +138 -27
  235. package/src/components/ui/sidebar/panels/settings-panel/interface-theme.test.tsx +20 -0
  236. package/src/components/ui/sidebar/panels/settings-panel/keyboard-shortcuts-dialog.tsx +5 -5
  237. package/src/components/ui/sidebar/panels/settings-panel/print-export-button.tsx +4 -10
  238. package/src/components/ui/sidebar/panels/site-panel/building-tree-node.tsx +3 -11
  239. package/src/components/ui/sidebar/panels/site-panel/ceiling-tree-node.tsx +4 -4
  240. package/src/components/ui/sidebar/panels/site-panel/chimney-tree-node.tsx +1 -5
  241. package/src/components/ui/sidebar/panels/site-panel/collection-tree-node.tsx +197 -0
  242. package/src/components/ui/sidebar/panels/site-panel/column-tree-node.tsx +1 -5
  243. package/src/components/ui/sidebar/panels/site-panel/door-tree-node.tsx +1 -5
  244. package/src/components/ui/sidebar/panels/site-panel/dormer-tree-node.tsx +1 -5
  245. package/src/components/ui/sidebar/panels/site-panel/elevator-tree-node.tsx +1 -5
  246. package/src/components/ui/sidebar/panels/site-panel/fence-tree-node.tsx +24 -11
  247. package/src/components/ui/sidebar/panels/site-panel/gutter-tree-node.tsx +1 -5
  248. package/src/components/ui/sidebar/panels/site-panel/index.tsx +181 -223
  249. package/src/components/ui/sidebar/panels/site-panel/inline-rename-input.tsx +30 -8
  250. package/src/components/ui/sidebar/panels/site-panel/item-tree-node.tsx +1 -3
  251. package/src/components/ui/sidebar/panels/site-panel/level-tree-node.tsx +4 -1
  252. package/src/components/ui/sidebar/panels/site-panel/registry-tree-node.tsx +1 -3
  253. package/src/components/ui/sidebar/panels/site-panel/roof-tree-node.tsx +1 -4
  254. package/src/components/ui/sidebar/panels/site-panel/shelf-tree-node.tsx +1 -3
  255. package/src/components/ui/sidebar/panels/site-panel/slab-tree-node.tsx +21 -13
  256. package/src/components/ui/sidebar/panels/site-panel/solar-panel-tree-node.tsx +1 -5
  257. package/src/components/ui/sidebar/panels/site-panel/spawn-tree-node.tsx +1 -5
  258. package/src/components/ui/sidebar/panels/site-panel/stair-tree-node.tsx +1 -5
  259. package/src/components/ui/sidebar/panels/site-panel/tree-node.tsx +13 -37
  260. package/src/components/ui/sidebar/panels/site-panel/unit-tree-node.tsx +2 -3
  261. package/src/components/ui/sidebar/panels/site-panel/wall-tree-node.tsx +1 -5
  262. package/src/components/ui/sidebar/panels/site-panel/window-tree-node.tsx +1 -5
  263. package/src/components/ui/sidebar/panels/site-panel/zone-tree-node.tsx +9 -4
  264. package/src/components/ui/sidebar/panels/zone-panel/index.tsx +2 -4
  265. package/src/components/ui/sidebar/tab-bar.tsx +14 -4
  266. package/src/components/ui/sidebar/use-plugin-panels.tsx +15 -5
  267. package/src/components/viewer/floorplan-compass-button.tsx +9 -1
  268. package/src/components/viewer/floorplan-preview.tsx +5 -6
  269. package/src/components/viewer/viewer-controls-bar.tsx +18 -6
  270. package/src/components/viewer/viewer-hover-label.tsx +34 -0
  271. package/src/components/viewer/viewer-rooms.tsx +191 -0
  272. package/src/components/viewer/viewer-scene-header.tsx +181 -145
  273. package/src/components/viewer/viewer-selection-manager.tsx +124 -0
  274. package/src/components/viewer/viewer-stage.tsx +2 -2
  275. package/src/components/viewer-zone-system.tsx +3 -8
  276. package/src/hooks/use-auto-save.integration.test.tsx +215 -43
  277. package/src/hooks/use-auto-save.ts +60 -9
  278. package/src/hooks/use-ceiling-events.test.ts +123 -118
  279. package/src/hooks/use-grid-events.test.ts +105 -105
  280. package/src/hooks/use-keyboard.test.ts +243 -19
  281. package/src/hooks/use-keyboard.ts +118 -83
  282. package/src/hooks/use-open-wall-ends.test.ts +99 -0
  283. package/src/hooks/use-open-wall-ends.ts +238 -0
  284. package/src/hooks/use-placement-notice.test.ts +71 -0
  285. package/src/hooks/use-placement-notice.ts +73 -0
  286. package/src/hooks/use-selected-room.ts +143 -0
  287. package/src/index.tsx +312 -58
  288. package/src/lib/__fixtures__/owner-victor-seattle-parking.json +1 -0
  289. package/src/lib/__fixtures__/owner-villa-aurelia-basement.json +1 -0
  290. package/src/lib/add-storey.test.ts +177 -0
  291. package/src/lib/add-storey.ts +59 -0
  292. package/src/lib/below-grade.test.tsx +171 -0
  293. package/src/lib/below-grade.ts +133 -0
  294. package/src/lib/camera-facing.test.ts +24 -0
  295. package/src/lib/camera-facing.ts +24 -0
  296. package/src/lib/ceiling-edit-session.test.ts +321 -0
  297. package/src/lib/ceiling-edit-session.ts +206 -0
  298. package/src/lib/ceiling-region-session.test.ts +179 -0
  299. package/src/lib/continuation.test.ts +19 -1
  300. package/src/lib/continuation.ts +25 -4
  301. package/src/lib/direct-manipulation.ts +2 -0
  302. package/src/lib/door-interaction.ts +14 -0
  303. package/src/lib/download-project-file.ts +17 -0
  304. package/src/lib/editor-level-display.test.ts +96 -0
  305. package/src/lib/editor-level-display.ts +55 -0
  306. package/src/lib/editor-views.ts +178 -0
  307. package/src/lib/export-geometry-wait.test.ts +90 -0
  308. package/src/lib/export-texture-utils.ts +2 -4
  309. package/src/lib/floor-edit-session.ts +153 -0
  310. package/src/lib/floor-footprints.test.ts +563 -0
  311. package/src/lib/floor-footprints.ts +395 -0
  312. package/src/lib/floor-foundation-support.test.ts +284 -0
  313. package/src/lib/floor-opening-draft.ts +218 -0
  314. package/src/lib/floor-openings.test.ts +216 -0
  315. package/src/lib/floor-region-geometry.ts +191 -0
  316. package/src/lib/floor-region-session.test.ts +485 -0
  317. package/src/lib/floor-region-session.ts +589 -0
  318. package/src/lib/floor-region-snap.ts +101 -0
  319. package/src/lib/floor-step-hover.test.ts +32 -0
  320. package/src/lib/floorplan/annotation-visibility.ts +17 -0
  321. package/src/lib/floorplan/dimension-drive.test.ts +360 -0
  322. package/src/lib/floorplan/dimension-drive.ts +479 -0
  323. package/src/lib/floorplan/floorplan-export.test.ts +12 -8
  324. package/src/lib/floorplan/floorplan-export.tsx +32 -8
  325. package/src/lib/floorplan/floorplan-extension.ts +32 -0
  326. package/src/lib/floorplan/floorplan-mode.test.ts +4 -0
  327. package/src/lib/floorplan/floorplan-mode.ts +2 -0
  328. package/src/lib/floorplan/floorplan-pdfkit-document.ts +363 -27
  329. package/src/lib/floorplan/floorplan-pdfkit-renderer.test.ts +177 -8
  330. package/src/lib/floorplan/floorplan-pdfkit-renderer.ts +60 -25
  331. package/src/lib/floorplan/floorplan-plugin-install.test.tsx +138 -0
  332. package/src/lib/floorplan/floorplan-readonly.ts +6 -0
  333. package/src/lib/floorplan/geometry.ts +13 -6
  334. package/src/lib/floorplan/index.ts +0 -9
  335. package/src/lib/floorplan/items.ts +5 -2
  336. package/src/lib/floorplan/marks.test.ts +213 -0
  337. package/src/lib/floorplan/marks.ts +238 -0
  338. package/src/lib/floorplan/open-wall-ends.test.ts +268 -0
  339. package/src/lib/floorplan/open-wall-ends.ts +162 -0
  340. package/src/lib/floorplan/pdf-fonts/GeistMono-Bold.ttf +0 -0
  341. package/src/lib/floorplan/pdf-fonts/GeistMono-Regular.ttf +0 -0
  342. package/src/lib/floorplan/pdf-fonts/LICENSE-GeistMono.txt +92 -0
  343. package/src/lib/floorplan/pdf-fonts/LICENSE-LiberationSans.txt +102 -0
  344. package/src/lib/floorplan/pdf-fonts/LiberationSans-Bold.ttf +0 -0
  345. package/src/lib/floorplan/pdf-fonts/LiberationSans-Regular.ttf +0 -0
  346. package/src/lib/floorplan/pdfkit-standard-fonts.d.ts +5 -0
  347. package/src/lib/floorplan/schedules.test.ts +189 -0
  348. package/src/lib/floorplan/schedules.ts +339 -0
  349. package/src/lib/floorplan/sheet-export.test.ts +124 -0
  350. package/src/lib/floorplan/sheet-export.ts +318 -0
  351. package/src/lib/floorplan/site-plan/build-site-plan-drawing.ts +768 -0
  352. package/src/lib/floorplan/site-plan/contours.test.ts +43 -0
  353. package/src/lib/floorplan/site-plan/contributors.ts +97 -0
  354. package/src/lib/floorplan/site-plan/coverage.ts +230 -0
  355. package/src/lib/floorplan/site-plan/drawing-type-switch.test.ts +15 -0
  356. package/src/lib/floorplan/site-plan/drawing-type-switch.tsx +86 -0
  357. package/src/lib/floorplan/site-plan/front-edge.test.ts +103 -0
  358. package/src/lib/floorplan/site-plan/front-edge.ts +157 -0
  359. package/src/lib/floorplan/site-plan/geometry.test.ts +269 -0
  360. package/src/lib/floorplan/site-plan/geometry.ts +142 -0
  361. package/src/lib/floorplan/site-plan/index.ts +55 -0
  362. package/src/lib/floorplan/site-plan/site-annotations.test.ts +483 -0
  363. package/src/lib/floorplan/site-plan/site-annotations.ts +1084 -0
  364. package/src/lib/floorplan/site-plan/site-parts.ts +287 -0
  365. package/src/lib/floorplan/site-plan/site-plan-layer.tsx +138 -0
  366. package/src/lib/floorplan/site-plan/street-name.test.ts +69 -0
  367. package/src/lib/floorplan/site-plan/street-name.ts +302 -0
  368. package/src/lib/floorplan/site-plan/yards.test.ts +70 -0
  369. package/src/lib/floorplan/types.ts +1 -27
  370. package/src/lib/follow-pascal-exterior.test-fixture.ts +17 -0
  371. package/src/lib/follow-pascal-exterior.test.ts +171 -0
  372. package/src/lib/follow-pascal-exterior.ts +39 -0
  373. package/src/lib/follow-pascal-presentation.test.ts +203 -0
  374. package/src/lib/follow-pascal-presentation.ts +129 -0
  375. package/src/lib/follow-pascal-sight.ts +84 -0
  376. package/src/lib/follow-pascal-targets.test.ts +406 -0
  377. package/src/lib/follow-pascal-targets.ts +198 -0
  378. package/src/lib/follow-pascal.test.ts +350 -0
  379. package/src/lib/follow-pascal.ts +284 -0
  380. package/src/lib/fresh-planar-placement.test.ts +148 -0
  381. package/src/lib/fresh-planar-placement.ts +114 -37
  382. package/src/lib/geometry-script/author.ts +157 -0
  383. package/src/lib/geometry-script/client.ts +159 -0
  384. package/src/lib/geometry-script/finish-scripts.ts +127 -0
  385. package/src/lib/geometry-script/protocol.ts +13 -0
  386. package/src/lib/gesture-lifecycle.test.ts +595 -0
  387. package/src/lib/gesture-lifecycle.ts +290 -0
  388. package/src/lib/glb-export-zone-holes.test.ts +41 -0
  389. package/src/lib/glb-export.test.ts +636 -126
  390. package/src/lib/glb-export.ts +241 -120
  391. package/src/lib/history.test.ts +18 -7
  392. package/src/lib/history.ts +7 -0
  393. package/src/lib/hud-title.ts +109 -0
  394. package/src/lib/hydration-gated-storage.ts +49 -0
  395. package/src/lib/ifc-export.test.ts +174 -0
  396. package/src/lib/ifc-export.ts +201 -0
  397. package/src/lib/ifc-parts.ts +15 -0
  398. package/src/lib/inspector-expanded.test.ts +44 -0
  399. package/src/lib/inspector-expanded.ts +36 -0
  400. package/src/lib/interaction/scope.ts +22 -0
  401. package/src/lib/keyboard-pan.test.ts +129 -7
  402. package/src/lib/keyboard-pan.ts +161 -12
  403. package/src/lib/level-selection.test.ts +57 -0
  404. package/src/lib/level-selection.ts +16 -1
  405. package/src/lib/live-gesture-round-trip.test.ts +360 -0
  406. package/src/lib/lot/__fixtures__/map-dossier-miami-shores.json +1 -0
  407. package/src/lib/lot/__fixtures__/map-dossier-stpete.json +1 -0
  408. package/src/lib/lot/clean-ring.test.ts +187 -0
  409. package/src/lib/lot/clean-ring.ts +217 -0
  410. package/src/lib/lot/dossier.test.ts +202 -0
  411. package/src/lib/lot/dossier.ts +498 -0
  412. package/src/lib/lot/drop-in.test.ts +326 -0
  413. package/src/lib/lot/drop-in.ts +392 -0
  414. package/src/lib/lot/index.ts +43 -0
  415. package/src/lib/lot/lot-patch.test.ts +340 -0
  416. package/src/lib/lot/lot-patch.ts +394 -0
  417. package/src/lib/lot/parcel-provider.ts +43 -0
  418. package/src/lib/lot/terrain.test.ts +161 -0
  419. package/src/lib/lot/terrain.ts +266 -0
  420. package/src/lib/material-catalog-model.ts +3 -1
  421. package/src/lib/material-paint-panel-model.ts +9 -5
  422. package/src/lib/material-paint.ts +4 -0
  423. package/src/lib/measurement-kind.test.ts +6 -8
  424. package/src/lib/measurements.test.ts +23 -0
  425. package/src/lib/mezzanine-actions.test.ts +342 -0
  426. package/src/lib/mezzanine-draft.test.ts +501 -0
  427. package/src/lib/mezzanine-draft.ts +278 -0
  428. package/src/lib/mezzanine-messages.ts +26 -0
  429. package/src/lib/mezzanine-selection.test.ts +207 -0
  430. package/src/lib/mezzanine-stairs.ts +46 -0
  431. package/src/lib/model-export.ts +3 -1
  432. package/src/lib/node-action-movement.ts +17 -6
  433. package/src/lib/node-display-label.ts +37 -0
  434. package/src/lib/orbit-floor-clearance.test.ts +201 -0
  435. package/src/lib/orbit-floor-clearance.ts +154 -0
  436. package/src/lib/paint-commit-route.test.ts +37 -0
  437. package/src/lib/paint-commit-route.ts +17 -0
  438. package/src/lib/paint-eyedropper.test.tsx +165 -0
  439. package/src/lib/paint-eyedropper.ts +99 -0
  440. package/src/lib/paint-pass-through.test.ts +122 -0
  441. package/src/lib/paint-pass-through.ts +78 -0
  442. package/src/lib/paint-region-hover.ts +25 -0
  443. package/src/lib/paint-region-mode.ts +154 -0
  444. package/src/lib/paint-regions.test.ts +166 -0
  445. package/src/lib/paint-regions.ts +192 -0
  446. package/src/lib/paint-scope.test.ts +317 -35
  447. package/src/lib/paint-scope.ts +267 -25
  448. package/src/lib/panel-rows.ts +21 -0
  449. package/src/lib/parametric-node-update.ts +16 -7
  450. package/src/lib/plate-paint-affected.test.ts +300 -0
  451. package/src/lib/plate-paint-affected.ts +177 -0
  452. package/src/lib/plugin-chat.ts +65 -0
  453. package/src/lib/plugin-panels.test.ts +25 -0
  454. package/src/lib/plugin-panels.ts +94 -0
  455. package/src/lib/point-ask/__fixtures__/consumer-browser.tsx +76 -0
  456. package/src/lib/point-ask/__fixtures__/house.ts +81 -0
  457. package/src/lib/point-ask/ask-created.test.ts +195 -0
  458. package/src/lib/point-ask/ask-flow.test.ts +380 -0
  459. package/src/lib/point-ask/build-context.test.ts +173 -0
  460. package/src/lib/point-ask/build-context.ts +94 -0
  461. package/src/lib/point-ask/capture-crop.ts +129 -0
  462. package/src/lib/point-ask/choreography.ts +69 -0
  463. package/src/lib/point-ask/compare.test.ts +145 -0
  464. package/src/lib/point-ask/compare.ts +73 -0
  465. package/src/lib/point-ask/describe-target.test.ts +165 -0
  466. package/src/lib/point-ask/describe-target.ts +234 -0
  467. package/src/lib/point-ask/dictation.ts +79 -0
  468. package/src/lib/point-ask/edge-marker.test.ts +61 -0
  469. package/src/lib/point-ask/edge-marker.ts +27 -0
  470. package/src/lib/point-ask/frame-loop.ts +25 -0
  471. package/src/lib/point-ask/history-marks.test.ts +94 -0
  472. package/src/lib/point-ask/history-marks.ts +39 -0
  473. package/src/lib/point-ask/host-api.ts +32 -0
  474. package/src/lib/point-ask/outline-box.test.ts +30 -0
  475. package/src/lib/point-ask/outline-box.ts +11 -0
  476. package/src/lib/point-ask/overlay-metrics.ts +6 -0
  477. package/src/lib/point-ask/pick.test.ts +220 -0
  478. package/src/lib/point-ask/pick.ts +121 -0
  479. package/src/lib/point-ask/pin-machine.test.ts +494 -0
  480. package/src/lib/point-ask/pin-machine.ts +217 -0
  481. package/src/lib/point-ask/placement.test.ts +203 -0
  482. package/src/lib/point-ask/placement.ts +141 -0
  483. package/src/lib/point-ask/pointer-tracker.ts +34 -0
  484. package/src/lib/point-ask/projector.ts +83 -0
  485. package/src/lib/point-ask/region.test.ts +157 -0
  486. package/src/lib/point-ask/region.ts +136 -0
  487. package/src/lib/point-ask/retry.test.ts +217 -0
  488. package/src/lib/point-ask/retry.ts +37 -0
  489. package/src/lib/point-ask/scene-commit.test.ts +312 -0
  490. package/src/lib/point-ask/scene-commit.ts +32 -0
  491. package/src/lib/point-ask/session.ts +1391 -0
  492. package/src/lib/point-ask/spring-keyframes.test.ts +45 -0
  493. package/src/lib/point-ask/spring-keyframes.ts +38 -0
  494. package/src/lib/point-ask/spring.test.ts +63 -0
  495. package/src/lib/point-ask/spring.ts +107 -0
  496. package/src/lib/point-ask/suggestions.test.ts +80 -0
  497. package/src/lib/point-ask/suggestions.ts +125 -0
  498. package/src/lib/point-ask/target-scope.test.ts +622 -0
  499. package/src/lib/point-ask/target-scope.ts +338 -0
  500. package/src/lib/point-ask/types.ts +48 -0
  501. package/src/lib/portable-export.test.ts +34 -1
  502. package/src/lib/portable-export.ts +119 -53
  503. package/src/lib/print-feature-thickness.ts +10 -1
  504. package/src/lib/print-roof-solids.test.ts +39 -1
  505. package/src/lib/print-roof-solids.ts +43 -10
  506. package/src/lib/print-shell-compiler-manifold-core.ts +1 -1
  507. package/src/lib/print-shell-compiler.ts +1 -0
  508. package/src/lib/registered-draft-snap.ts +12 -0
  509. package/src/lib/roof-duplication.ts +10 -2
  510. package/src/lib/room-assembly-overlay.test.ts +275 -0
  511. package/src/lib/room-assembly-overlay.ts +265 -0
  512. package/src/lib/room-built-on.test.ts +379 -0
  513. package/src/lib/room-built-on.ts +138 -0
  514. package/src/lib/room-construction-commands.ts +712 -0
  515. package/src/lib/room-construction.test.ts +746 -0
  516. package/src/lib/room-construction.ts +385 -0
  517. package/src/lib/room-controls-phase6.test.ts +630 -0
  518. package/src/lib/room-divide-session.ts +523 -0
  519. package/src/lib/room-handle-drag.ts +1068 -0
  520. package/src/lib/room-name-catalog.test.ts +90 -0
  521. package/src/lib/room-name-catalog.ts +168 -0
  522. package/src/lib/room-push-dimensions.test.ts +311 -0
  523. package/src/lib/room-push-dimensions.ts +251 -0
  524. package/src/lib/room-selection-commands.ts +62 -0
  525. package/src/lib/room-selection.test.ts +1017 -0
  526. package/src/lib/room-selection.ts +417 -0
  527. package/src/lib/room-structure-commands.ts +94 -0
  528. package/src/lib/room-structure.test.ts +921 -0
  529. package/src/lib/room-transform-session.ts +570 -0
  530. package/src/lib/room-zone-routing.test.ts +294 -0
  531. package/src/lib/room-zone-routing.ts +197 -0
  532. package/src/lib/scene-clipboard.test.ts +610 -1
  533. package/src/lib/scene-clipboard.ts +269 -58
  534. package/src/lib/scene-material-refs.test.ts +79 -0
  535. package/src/lib/scene-material-refs.ts +79 -0
  536. package/src/lib/scene.test.ts +24 -36
  537. package/src/lib/scene.ts +33 -57
  538. package/src/lib/selection-routing.test.ts +129 -45
  539. package/src/lib/selection-routing.ts +54 -75
  540. package/src/lib/session-writes.test.ts +120 -0
  541. package/src/lib/session-writes.ts +205 -0
  542. package/src/lib/sfx-bus.ts +13 -0
  543. package/src/lib/sfx-player.test.ts +70 -9
  544. package/src/lib/sfx-player.ts +6 -0
  545. package/src/lib/sidebar-panel.ts +62 -0
  546. package/src/lib/site-boundary.test.ts +4 -6
  547. package/src/lib/site-boundary.ts +1 -1
  548. package/src/lib/snapping-mode.test.ts +4 -0
  549. package/src/lib/snapping-mode.ts +8 -1
  550. package/src/lib/stair-levels.test.ts +25 -0
  551. package/src/lib/stair-levels.ts +22 -3
  552. package/src/lib/terrace-draft.test.ts +145 -0
  553. package/src/lib/terrace-draft.ts +214 -0
  554. package/src/lib/tool-cursor-icon.test.ts +16 -0
  555. package/src/lib/tool-cursor-icon.ts +11 -0
  556. package/src/lib/typing-target.ts +9 -0
  557. package/src/lib/units.ts +77 -39
  558. package/src/lib/vector-edges.test.ts +58 -0
  559. package/src/lib/vector-edges.ts +400 -0
  560. package/src/lib/viewer-selection.test.ts +577 -0
  561. package/src/lib/viewer-selection.ts +580 -0
  562. package/src/lib/wall-draw-variant.test.ts +71 -0
  563. package/src/lib/wall-draw-variant.ts +91 -0
  564. package/src/lib/wall-paint-region-session.ts +246 -0
  565. package/src/lib/wall-paint-region.test.ts +373 -0
  566. package/src/lib/wall-placement-pointer.test.ts +42 -0
  567. package/src/lib/wall-placement-pointer.ts +57 -0
  568. package/src/lib/wall-polygon-draft.test.ts +262 -0
  569. package/src/lib/wall-polygon-draft.ts +286 -0
  570. package/src/lib/wall-push.test.ts +453 -0
  571. package/src/lib/wall-region-face.ts +352 -0
  572. package/src/lib/wall-region-handles.test.ts +304 -0
  573. package/src/lib/wall-region-handles.ts +368 -0
  574. package/src/lib/wall-region-list.test.ts +119 -0
  575. package/src/lib/wall-region-list.ts +136 -0
  576. package/src/lib/wall-region-snap.ts +110 -0
  577. package/src/lib/window-changes.ts +35 -0
  578. package/src/lib/window-interaction.ts +14 -0
  579. package/src/lib/zone-content.ts +4 -1
  580. package/src/lib/zone-name.test.ts +17 -0
  581. package/src/lib/zone-name.ts +17 -0
  582. package/src/store/opens-in-select-mode.test.ts +223 -0
  583. package/src/store/paint-mode.test.ts +188 -0
  584. package/src/store/terrain-sculpt-mode.test.ts +19 -21
  585. package/src/store/tool-mode.test.ts +137 -6
  586. package/src/store/use-audio.test.ts +146 -0
  587. package/src/store/use-audio.tsx +127 -3
  588. package/src/store/use-construction-reveal.test.ts +173 -0
  589. package/src/store/use-construction-reveal.ts +145 -0
  590. package/src/store/use-corner-snap-hint.test.ts +24 -0
  591. package/src/store/use-corner-snap-hint.ts +47 -0
  592. package/src/store/use-delete-confirmation.ts +28 -1
  593. package/src/store/use-drawing-view.test.ts +24 -3
  594. package/src/store/use-drawing-view.ts +29 -1
  595. package/src/store/use-editor.tsx +432 -321
  596. package/src/store/use-floorplan-draft-preview.ts +13 -0
  597. package/src/store/use-floorplan-marquee.ts +12 -2
  598. package/src/store/use-follow-pascal.test.ts +62 -0
  599. package/src/store/use-follow-pascal.ts +53 -0
  600. package/src/store/use-point-ask.ts +247 -0
  601. package/src/store/use-quick-measurement-hud.ts +3 -3
  602. package/src/store/use-stair-build-preview.ts +33 -5
  603. package/src/store/use-wall-move-ghosts.ts +4 -2
  604. package/src/store/use-wall-region-handles-selection.test.ts +222 -0
  605. package/src/store/use-wall-region-handles-selection.ts +139 -0
  606. package/src/store/view-layout.test.ts +201 -0
  607. package/src/store/view-layout.ts +199 -0
  608. package/src/test-utils/immediate-animation-frames.ts +16 -0
  609. package/src/test-utils/live-gesture-round-trip.ts +224 -0
  610. package/src/test-utils/render-current-markup.tsx +16 -0
  611. package/src/test-utils/selection-harness.ts +82 -0
  612. package/src/components/editor-2d/renderers/floorplan-stair-layer.tsx +0 -499
  613. package/src/components/systems/zone/zone-label-editor-system.tsx +0 -333
  614. package/src/components/tools/fence/fence-drafting.test.ts +0 -100
  615. package/src/components/tools/fence/fence-drafting.ts +0 -266
  616. package/src/components/tools/stair/stair-click-guard.test.ts +0 -58
  617. package/src/components/tools/stair/stair-click-guard.ts +0 -72
  618. package/src/components/tools/stair/stair-defaults.ts +0 -18
  619. package/src/components/tools/stair/stair-tool.tsx +0 -634
  620. package/src/components/ui/sidebar/panels/plugins-panel.tsx +0 -218
  621. package/src/components/viewer/viewer-units-panel.tsx +0 -116
  622. package/src/lib/floorplan/stairs.ts +0 -489
  623. package/src/lib/level-duplication.test.ts +0 -144
  624. package/src/lib/level-duplication.ts +0 -195
@@ -0,0 +1,354 @@
1
+ 'use client'
2
+
3
+ import {
4
+ type AnyNodeId,
5
+ type BaseNode,
6
+ type BuildingNode,
7
+ type CameraPose,
8
+ emitter,
9
+ type LevelNode,
10
+ resolveBuildingForLevel,
11
+ useScene,
12
+ type ZoneNode,
13
+ } from '@pascal-app/core'
14
+ import {
15
+ groundVeiled,
16
+ type RevealEvent,
17
+ revealChangedNodeIds,
18
+ subscribeRevealEvents,
19
+ useViewer,
20
+ } from '@pascal-app/viewer'
21
+ import { useFrame, useThree } from '@react-three/fiber'
22
+ import { useEffect, useMemo, useRef } from 'react'
23
+ import { Vector3 } from 'three'
24
+ import {
25
+ clearFrame,
26
+ Follower,
27
+ type FollowWant,
28
+ frameOf,
29
+ OrbitSpring,
30
+ type OrbitValue,
31
+ } from '../../lib/follow-pascal'
32
+ import { anyExterior } from '../../lib/follow-pascal-exterior'
33
+ import { FollowPresentation, type PresentationPort } from '../../lib/follow-pascal-presentation'
34
+ import { FollowSight } from '../../lib/follow-pascal-sight'
35
+ import { stayOnLevel, wantFromEvent } from '../../lib/follow-pascal-targets'
36
+ import useEditor from '../../store/use-editor'
37
+ import useFollowPascal from '../../store/use-follow-pascal'
38
+ import { usePointAsk } from '../../store/use-point-ask'
39
+
40
+ /**
41
+ * Follow Pascal: while a build plays, the camera eases to what the current step is working on, framed
42
+ * at the distance its scale needs. It listens to the reveal's own words (`subscribeRevealEvents`), so
43
+ * a step and the camera share one rhythm, and moves like a critically damped spring on an orbit about
44
+ * what it looks at, so the horizon stays level. Touching the camera, or choosing a view, pauses it; the
45
+ * person resumes it from the pill, or the next build takes it again. Under reduced motion it cuts.
46
+ *
47
+ * To see a build it changes the presentation: it cuts the walls away when no eye sees the target,
48
+ * selects the floor being built and hides the floors above it. An edit or a move seen from outside
49
+ * (an exterior wall bent, a window moved on one, a roof, the ground) is looked at from outdoors with
50
+ * the walls up instead: a cutaway would hide the very wall that moves. A commit with one such change
51
+ * in it takes the outside view for all of it. A build is followed as it always was. When Follow ends (switched off, the person takes the
52
+ * camera, the build is over and no turn is running) it puts back what it changed, and leaves what
53
+ * the person changed since (`FollowPresentation`).
54
+ */
55
+
56
+ /** A build is over this long after its last word; the pill goes with it. */
57
+ const BUILD_OVER_MS = 4000
58
+ /** Close enough to its goal that the flight is over. */
59
+ const ARRIVED = { position: 0.01, angle: 0.002 }
60
+
61
+ /** Every way a person takes the camera. */
62
+ const TAKEN = [
63
+ 'camera-controls:interaction-start',
64
+ 'camera-controls:view',
65
+ 'camera-controls:focus',
66
+ 'camera-controls:top-view',
67
+ 'camera-controls:orbit-cw',
68
+ 'camera-controls:orbit-ccw',
69
+ 'camera-controls:fit-scene',
70
+ ] as const
71
+
72
+ /** The viewer and the editor as `FollowPresentation` reads and writes them. */
73
+ const viewPort: PresentationPort = {
74
+ read: () => {
75
+ const viewer = useViewer.getState()
76
+ const { selection } = viewer
77
+ return {
78
+ wall: viewer.wallMode,
79
+ levels: viewer.levelMode,
80
+ hideAbove: viewer.hideLevelsAboveSelection,
81
+ floor: {
82
+ buildingId: selection.buildingId,
83
+ levelId: selection.levelId,
84
+ zoneId: selection.zoneId,
85
+ selectedIds: selection.selectedIds,
86
+ room: useEditor.getState().room,
87
+ },
88
+ }
89
+ },
90
+ setWall: (mode) => useViewer.getState().setWallMode(mode),
91
+ setLevels: (mode) => useViewer.getState().setLevelMode(mode),
92
+ setHideAbove: (hide) => useViewer.setState({ hideLevelsAboveSelection: hide }),
93
+ setFloor: ({ buildingId, levelId }) => {
94
+ useViewer.getState().setSelection({
95
+ ...(buildingId ? { buildingId: buildingId as BuildingNode['id'] } : {}),
96
+ levelId: levelId as LevelNode['id'],
97
+ })
98
+ useEditor.getState().clearRoom()
99
+ },
100
+ putFloor: (floor) => {
101
+ const nodes = useScene.getState().nodes
102
+ const exists = (id: string | null) => id !== null && !!nodes[id as AnyNodeId]
103
+ useViewer.getState().setSelection({
104
+ buildingId: exists(floor.buildingId) ? (floor.buildingId as BuildingNode['id']) : null,
105
+ levelId: exists(floor.levelId) ? (floor.levelId as LevelNode['id']) : null,
106
+ zoneId: exists(floor.zoneId) ? (floor.zoneId as ZoneNode['id']) : null,
107
+ selectedIds: floor.selectedIds.filter(exists) as BaseNode['id'][],
108
+ })
109
+ const { room } = floor
110
+ useEditor.setState({
111
+ room: room && exists(room.zoneId) && exists(room.levelId) ? room : null,
112
+ hoveredRoom: null,
113
+ })
114
+ },
115
+ }
116
+
117
+ const prefersReducedMotion = () =>
118
+ typeof window !== 'undefined' &&
119
+ typeof window.matchMedia === 'function' &&
120
+ window.matchMedia('(prefers-reduced-motion: reduce)').matches
121
+
122
+ /** The canvas dips and comes back, so a cut reads as a cross-fade rather than a jump. */
123
+ function dipCanvas() {
124
+ const canvas = document.querySelector<HTMLElement>('[data-pascal-viewer-3d]')
125
+ canvas?.animate([{ opacity: 0.35 }, { opacity: 1 }], { duration: 240, easing: 'ease-out' })
126
+ }
127
+
128
+ const arrived = (value: OrbitValue, goal: OrbitValue) =>
129
+ Math.abs(value.x - goal.x) < ARRIVED.position &&
130
+ Math.abs(value.y - goal.y) < ARRIVED.position &&
131
+ Math.abs(value.z - goal.z) < ARRIVED.position &&
132
+ Math.abs(value.r - goal.r) < ARRIVED.position &&
133
+ Math.abs(value.az - goal.az) < ARRIVED.angle &&
134
+ Math.abs(value.el - goal.el) < ARRIVED.angle
135
+
136
+ export function FollowPascal() {
137
+ const camera = useThree((state) => state.camera)
138
+ const scene = useThree((state) => state.scene)
139
+ const sight = useMemo(() => new FollowSight(scene), [scene])
140
+ const presentation = useMemo(() => new FollowPresentation(viewPort), [])
141
+ const controls = useThree((state) => state.controls) as {
142
+ getTarget?: (out: Vector3) => Vector3
143
+ } | null
144
+ const size = useThree((state) => state.size)
145
+ const follower = useRef(new Follower())
146
+ const spring = useRef<OrbitSpring | null>(null)
147
+ const clock = useRef(0)
148
+ const flying = useRef(false)
149
+ const synced = useRef(false)
150
+ const last = useRef<FollowWant | null>(null)
151
+ const resuming = useRef(false)
152
+ const over = useRef<ReturnType<typeof setTimeout> | null>(null)
153
+ const scratch = useRef(new Vector3())
154
+ const nextSightCheck = useRef(0)
155
+
156
+ /** The camera as it is now, so a flight starts from the view the person has. */
157
+ const currentPose = (): CameraPose => {
158
+ const target = controls?.getTarget?.(scratch.current) ?? scratch.current.set(0, 0, 0)
159
+ return {
160
+ position: [camera.position.x, camera.position.y, camera.position.z],
161
+ target: [target.x, target.y, target.z],
162
+ projection: 'perspective',
163
+ }
164
+ }
165
+
166
+ /** The build in hand has a change seen from outside in it: all of it is looked at from outdoors. */
167
+ const outsideBuild = useRef(false)
168
+ /** The last word was that the play is over: the next edit is another commit's. */
169
+ const played = useRef(false)
170
+
171
+ const unobstructed = (want: FollowWant, goal: OrbitValue): OrbitValue => {
172
+ sight.refresh(want)
173
+ const clear = clearFrame(goal, (pose) => sight.clear(pose))
174
+ if (clear) return clear
175
+ // Seen from outside the walls stay up: a cutaway would take the wall that changes away.
176
+ if (want.exterior || outsideBuild.current) {
177
+ presentation.wall('up')
178
+ return goal
179
+ }
180
+ presentation.wall('cutaway')
181
+ return goal
182
+ }
183
+
184
+ const moveTo = (goal: OrbitValue) => {
185
+ const orbit = spring.current!
186
+ if (prefersReducedMotion()) {
187
+ orbit.snap(goal)
188
+ emitter.emit('camera-controls:apply-pose', orbit.pose())
189
+ dipCanvas()
190
+ flying.current = false
191
+ return
192
+ }
193
+ orbit.to(goal)
194
+ flying.current = true
195
+ }
196
+
197
+ const fly = (asked: FollowWant) => {
198
+ const nodes = useScene.getState().nodes
199
+ // A level below grade is shown, its ground open over it: the look at the whole house that follows
200
+ // furniture landing, and every finished commit, stays on that level. Showing the floors above would
201
+ // close the ground, and the next piece on the basement open it again, once for every commit.
202
+ const { selection, hideLevelsAboveSelection } = useViewer.getState()
203
+ const staying = hideLevelsAboveSelection && groundVeiled(useViewer.getState())
204
+ const want =
205
+ asked.kind === 'group' && !asked.levelId && staying && selection.levelId
206
+ ? stayOnLevel(asked, nodes, selection.levelId)
207
+ : asked
208
+ last.current = want
209
+ const level = want.levelId ? nodes[want.levelId as AnyNodeId] : undefined
210
+ if (level?.type === 'level') {
211
+ const buildingId = resolveBuildingForLevel(level.id as AnyNodeId, nodes)
212
+ const building = buildingId ? nodes[buildingId] : undefined
213
+ presentation.floor({
214
+ buildingId: building?.type === 'building' ? building.id : null,
215
+ levelId: level.id,
216
+ })
217
+ presentation.levels('stacked')
218
+ presentation.hideAbove(true)
219
+ } else if (want.kind === 'group' && !want.levelId) {
220
+ // The finished house is looked at whole: its upper floors are not hidden.
221
+ presentation.levels('stacked')
222
+ presentation.hideAbove(false)
223
+ }
224
+ // A change seen from outside: the walls up for it, even if an earlier step cut them away.
225
+ if (want.exterior || outsideBuild.current) presentation.wall('up')
226
+ if (!spring.current) {
227
+ spring.current = new OrbitSpring({ x: 0, y: 1, z: 0, r: 20, az: 0.6, el: 0.5 })
228
+ synced.current = false
229
+ }
230
+ if (!synced.current) {
231
+ spring.current.fromPose(currentPose())
232
+ synced.current = true
233
+ }
234
+ const perspective = camera as { fov?: number }
235
+ const goal = frameOf(want, {
236
+ fov: perspective.fov ?? 45,
237
+ aspect: size.width / Math.max(1, size.height),
238
+ azimuth: spring.current.value().az,
239
+ })
240
+ moveTo(unobstructed(want, goal))
241
+ nextSightCheck.current = clock.current + 0.5
242
+ }
243
+
244
+ // What the build says it is doing: where to look, and when a build begins and ends.
245
+ useEffect(() => {
246
+ const hear = (event: RevealEvent) => {
247
+ const store = useFollowPascal.getState()
248
+ if (over.current) clearTimeout(over.current)
249
+ if (!store.building) {
250
+ store.beginBuild()
251
+ follower.current.reset()
252
+ synced.current = false
253
+ outsideBuild.current = false
254
+ }
255
+ // An edit or a move is read before its first piece plays: one change seen from outside takes the
256
+ // whole commit outdoors, until the play is over. What is waiting in the queue belongs to a later
257
+ // commit, so a build, a deletion, or the edit that follows a finished play never inherits it.
258
+ const edit = 'changed' in event && event.changed === true
259
+ if (!edit) outsideBuild.current = false
260
+ else {
261
+ if (played.current) outsideBuild.current = false
262
+ const changed = outsideBuild.current ? [] : revealChangedNodeIds()
263
+ if (changed.length && anyExterior(useScene.getState().nodes, changed))
264
+ outsideBuild.current = true
265
+ }
266
+ played.current = event.type === 'complete'
267
+ over.current = setTimeout(() => useFollowPascal.getState().endBuild(), BUILD_OVER_MS)
268
+ if (event.type === 'complete') return
269
+ const want = wantFromEvent(event, useScene.getState().nodes, clock.current)
270
+ if (want) follower.current.want(want)
271
+ }
272
+ const stop = subscribeRevealEvents(hear)
273
+ return () => {
274
+ stop()
275
+ if (over.current) clearTimeout(over.current)
276
+ }
277
+ }, [])
278
+
279
+ // The person takes the camera: it stays theirs, and the next flight starts from where they leave it.
280
+ useEffect(() => {
281
+ const take = () => {
282
+ flying.current = false
283
+ synced.current = false
284
+ useFollowPascal.getState().pause()
285
+ }
286
+ for (const name of TAKEN) emitter.on(name, take)
287
+ return () => {
288
+ for (const name of TAKEN) emitter.off(name, take)
289
+ }
290
+ }, [])
291
+
292
+ // Resuming flies back to what it was last looking at; switching it off ends a flight.
293
+ // Follow ends when it is switched off and when the person takes the camera: what it changed to see
294
+ // the build goes back. A build being over ends it only when no turn is running: a turn runs several
295
+ // programs, and the view holds steady between them (the host's busy flag, below).
296
+ useEffect(
297
+ () =>
298
+ useFollowPascal.subscribe((state, before) => {
299
+ if (before.paused && !state.paused && state.on) resuming.current = true
300
+ if (before.on && !state.on) flying.current = false
301
+ if ((before.on && !state.on) || (!before.paused && state.paused)) presentation.restore()
302
+ // The build is over: that ends it when no turn is running to build more.
303
+ if (before.building && !state.building && !usePointAsk.getState().busy)
304
+ presentation.restore()
305
+ }),
306
+ [presentation],
307
+ )
308
+
309
+ // The turn ends, or the person stops it: the host's busy flag is the editor's word on it. What the
310
+ // reveal still plays after that is still followed, so it ends with the build, not with the turn.
311
+ useEffect(
312
+ () =>
313
+ usePointAsk.subscribe((state, before) => {
314
+ if (before.busy && !state.busy && !useFollowPascal.getState().building)
315
+ presentation.restore()
316
+ }),
317
+ [presentation],
318
+ )
319
+
320
+ // Leaving the editor ends it too.
321
+ useEffect(() => () => presentation.restore(), [presentation])
322
+
323
+ useFrame((_, delta) => {
324
+ clock.current += Math.min(delta, 0.1)
325
+ const { on, paused, building } = useFollowPascal.getState()
326
+ if (!on || paused) return
327
+ if (useViewer.getState().cameraMode !== 'perspective' || useEditor.getState().isFirstPersonMode)
328
+ return
329
+ if (resuming.current) {
330
+ resuming.current = false
331
+ if (last.current) fly(last.current)
332
+ }
333
+ const due = follower.current.due(clock.current)
334
+ if (due) fly(due)
335
+ const orbit = spring.current
336
+ // Recheck as geometry lands, even after arrival; retain a clear orbit instead of hunting each frame.
337
+ if (
338
+ orbit &&
339
+ last.current &&
340
+ (building || flying.current) &&
341
+ clock.current >= nextSightCheck.current
342
+ ) {
343
+ nextSightCheck.current = clock.current + 0.5
344
+ const goal = unobstructed(last.current, orbit.destination())
345
+ if (!arrived(goal, orbit.destination())) moveTo(goal)
346
+ }
347
+ if (!(flying.current && orbit)) return
348
+ orbit.step(Math.min(delta, 0.05))
349
+ emitter.emit('camera-controls:apply-pose', orbit.pose())
350
+ if (arrived(orbit.value(), orbit.destination())) flying.current = false
351
+ })
352
+
353
+ return null
354
+ }
@@ -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
+ }
@@ -18,13 +18,17 @@ 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'
31
+ import { isTypingTarget } from '../../lib/typing-target'
28
32
  import useAlignmentGuides from '../../store/use-alignment-guides'
29
33
  import useDeleteConfirmation from '../../store/use-delete-confirmation'
30
34
  import useEditor, {
@@ -33,6 +37,7 @@ import useEditor, {
33
37
  isGridSnapActive,
34
38
  isMagneticSnapActive,
35
39
  } from '../../store/use-editor'
40
+ import useFloorplanMode from '../../store/use-floorplan-mode'
36
41
  import useInteractionScope from '../../store/use-interaction-scope'
37
42
  import { useFloorplanGroupDrag } from '../editor-2d/floorplan-group-move'
38
43
  import {
@@ -366,6 +371,7 @@ export function startGroupPickUp(
366
371
  }
367
372
 
368
373
  const onKeyDown = (e: KeyboardEvent) => {
374
+ if (isTypingTarget(e.target)) return
369
375
  const key = e.key.toLowerCase()
370
376
  if ((e.metaKey || e.ctrlKey) && (key === 'c' || key === 'v' || key === 'x')) {
371
377
  // A clipboard chord replaces the current carry. Let the global keyboard
@@ -456,12 +462,39 @@ function removeUnusedPasteMaterials(materialIds: SceneMaterialId[]) {
456
462
  }
457
463
  }
458
464
 
465
+ const REFUSED_PASTE_REASONS: Record<PasteRefusal, [one: string, many: string]> = {
466
+ 'no-access': [
467
+ 'you no longer have access to the project it came from.',
468
+ 'you no longer have access to the project they came from.',
469
+ ],
470
+ 'no-copies': [
471
+ 'this version is not available to copy from the source project.',
472
+ 'these versions are not available to copy from the source project.',
473
+ ],
474
+ failed: ["it couldn't be copied. Try again.", "they couldn't be copied. Try again."],
475
+ }
476
+
477
+ /** Why scripted objects were left out of a paste, or of a build loaded from a file. */
478
+ export function refusedObjectsNotice(
479
+ count: number,
480
+ refusal: PasteRefusal,
481
+ action: 'pasted' | 'loaded' = 'pasted',
482
+ ) {
483
+ const [one, many] = REFUSED_PASTE_REASONS[refusal]
484
+ return count === 1
485
+ ? `1 object wasn't ${action}: ${one}`
486
+ : `${count} objects weren't ${action}: ${many}`
487
+ }
488
+
489
+ let pastePending = false
490
+
459
491
  /**
460
492
  * Paste the Pascal scene payload from the browser clipboard onto the active
461
493
  * level, then carry the clones under the cursor until click-to-place. Escape
462
494
  * removes the uncommitted clones and any scene materials imported with them.
463
495
  */
464
496
  export async function pasteSelectionAndPickUp(targetLevelId?: AnyNodeId): Promise<boolean> {
497
+ if (pastePending) return false
465
498
  const activeScope = useInteractionScope.getState().scope
466
499
  if (activeScope.kind === 'placing' || activeScope.kind === 'moving') {
467
500
  emitter.emit('tool:cancel')
@@ -476,7 +509,18 @@ export async function pasteSelectionAndPickUp(targetLevelId?: AnyNodeId): Promis
476
509
  // above there is nothing to abandon.
477
510
  if (isBrushMode(useEditor.getState().mode)) useEditor.getState().setMode('select')
478
511
 
479
- const result = await pasteSystemEditorClipboardToLevel(targetLevelId)
512
+ pastePending = true
513
+ let result: Awaited<ReturnType<typeof pasteSystemEditorClipboardToLevel>>
514
+ try {
515
+ result = await pasteSystemEditorClipboardToLevel(targetLevelId)
516
+ } finally {
517
+ pastePending = false
518
+ }
519
+ if (result?.refusal) {
520
+ useFloorplanMode
521
+ .getState()
522
+ .showNotice(refusedObjectsNotice(result.refusedIds.length, result.refusal))
523
+ }
480
524
  if (!result || result.pastedIds.length === 0) return false
481
525
 
482
526
  const discardPaste = () => {
@@ -547,7 +591,16 @@ export function cutSelectionToEditorClipboard(): boolean {
547
591
  */
548
592
  export function deleteSelection(): boolean {
549
593
  const selectedIds = useViewer.getState().selection.selectedIds as AnyNodeId[]
550
- if (selectedIds.length === 0) return false
594
+ if (selectedIds.length === 0) {
595
+ const room = useEditor.getState().room
596
+ if (!room) return false
597
+ requestRoomDeletion(room.zoneId)
598
+ return true
599
+ }
600
+ // A drilled piece of a room (a wall, a separator, its ceiling) deleted: back to the room.
601
+ const origin = captureElementActionOrigin(selectedIds)
602
+ if (selectedIds.length === 1 && useScene.getState().nodes[selectedIds[0]!]?.type === 'separator')
603
+ return deleteSelectedSeparator(selectedIds[0]!, origin)
551
604
 
552
605
  const commitDelete = () => {
553
606
  const detail =
@@ -562,6 +615,7 @@ export function deleteSelection(): boolean {
562
615
  }
563
616
  useScene.getState().deleteNodes(selectedIds)
564
617
  useViewer.getState().setSelection({ selectedIds: [] })
618
+ completeElementAction(origin)
565
619
  }
566
620
 
567
621
  if (selectedIds.length >= BULK_DELETE_THRESHOLD) {
@@ -17,6 +17,7 @@ import { type Camera, Plane, type Raycaster, Vector2, Vector3 } from 'three'
17
17
  import { GROUP_MOVE_DRAG_LABEL } from '../../lib/contextual-help'
18
18
  import { isHistoryShortcut } from '../../lib/history'
19
19
  import { sfxEmitter } from '../../lib/sfx-bus'
20
+ import { isTypingTarget } from '../../lib/typing-target'
20
21
  import useAlignmentGuides from '../../store/use-alignment-guides'
21
22
  import useEditor, {
22
23
  isAlignmentGuideActive,
@@ -380,6 +381,7 @@ export function armGroupMove3d(args: {
380
381
  // `use-keyboard` arms, which would otherwise act on stale store state
381
382
  // (or clear the multi-selection) mid-session.
382
383
  const onKeyDown = (e: KeyboardEvent) => {
384
+ if (isTypingTarget(e.target)) return
383
385
  const key = e.key.toLowerCase()
384
386
  if ((key === 'r' || key === 't') && !e.metaKey && !e.ctrlKey && !e.altKey && !e.shiftKey) {
385
387
  // Armed but still under the drag threshold: engage first (exactly what