@pascal-app/editor 1.0.3 → 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 (440) hide show
  1. package/package.json +11 -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 +202 -57
  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 +36 -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 +344 -342
  27. package/src/components/editor/footprint-height-handle.tsx +204 -0
  28. package/src/components/editor/group-actions.ts +54 -2
  29. package/src/components/editor/handles/handle-hit-priority.test.tsx +63 -0
  30. package/src/components/editor/handles/handle-hit-priority.tsx +37 -0
  31. package/src/components/editor/handles/swallow-next-click.test.ts +28 -0
  32. package/src/components/editor/handles/use-handle-drag.ts +269 -197
  33. package/src/components/editor/index.tsx +294 -169
  34. package/src/components/editor/join-walls-pill.tsx +44 -0
  35. package/src/components/editor/node-action-menu.tsx +68 -70
  36. package/src/components/editor/node-arrow-handles.tsx +25 -6
  37. package/src/components/editor/open-wall-ends-3d-layer.tsx +273 -0
  38. package/src/components/editor/open-wall-ends-hint.tsx +89 -0
  39. package/src/components/editor/quick-measurement-hud.tsx +3 -2
  40. package/src/components/editor/registry-action-contributions.test.tsx +63 -0
  41. package/src/components/editor/registry-action-contributions.tsx +80 -1
  42. package/src/components/editor/room-controls.tsx +847 -0
  43. package/src/components/editor/room-floor-highlight.tsx +128 -0
  44. package/src/components/editor/room-handles-2d.tsx +354 -0
  45. package/src/components/editor/room-handles.tsx +675 -0
  46. package/src/components/editor/room-highlight.tsx +225 -0
  47. package/src/components/editor/room-labels.test.ts +357 -0
  48. package/src/components/editor/room-labels.tsx +990 -0
  49. package/src/components/editor/room-transform-ghost.tsx +256 -0
  50. package/src/components/editor/room-webgpu-materials.test.ts +27 -0
  51. package/src/components/editor/selection-manager.tsx +451 -317
  52. package/src/components/editor/slab-hole-highlights.tsx +3 -0
  53. package/src/components/editor/snapshot-capture-overlay.tsx +4 -0
  54. package/src/components/editor/snapshot-capture.test.ts +52 -4
  55. package/src/components/editor/snapshot-capture.ts +30 -5
  56. package/src/components/editor/thumbnail-generator.test.ts +308 -0
  57. package/src/components/editor/thumbnail-generator.tsx +1035 -229
  58. package/src/components/editor/use-floorplan-background-placement.ts +31 -11
  59. package/src/components/editor/vector-edge-extractor.tsx +121 -0
  60. package/src/components/editor/views/host-view.tsx +61 -0
  61. package/src/components/editor/views/plugins-view.tsx +670 -0
  62. package/src/components/editor/views/scene-region.tsx +59 -0
  63. package/src/components/editor/views/use-editor-views.ts +57 -0
  64. package/src/components/editor/views/view-bar.tsx +220 -0
  65. package/src/components/editor/wall-justification.test.ts +140 -0
  66. package/src/components/editor/wall-move-side-handles.tsx +43 -142
  67. package/src/components/editor/wall-push-arrow.tsx +92 -0
  68. package/src/components/editor/wall-region-handles.tsx +509 -0
  69. package/src/components/editor/wall-snap-beacon-layer.tsx +3 -0
  70. package/src/components/editor-2d/floorplan-cursor-indicator-overlay.tsx +8 -4
  71. package/src/components/editor-2d/floorplan-open-wall-ends-layer.test.tsx +86 -0
  72. package/src/components/editor-2d/floorplan-open-wall-ends-layer.tsx +290 -0
  73. package/src/components/editor-2d/floorplan-registry-action-menu.tsx +11 -1
  74. package/src/components/editor-2d/floorplan-registry-move-overlay.tsx +113 -102
  75. package/src/components/editor-2d/renderers/floorplan-dimension-edit-overlay.test.ts +156 -0
  76. package/src/components/editor-2d/renderers/floorplan-dimension-edit-overlay.tsx +278 -0
  77. package/src/components/editor-2d/renderers/floorplan-dimension-renderer.tsx +77 -0
  78. package/src/components/editor-2d/renderers/{floorplan-draft-wall-measurement.tsx → floorplan-draft-measurement.tsx} +8 -8
  79. package/src/components/editor-2d/renderers/floorplan-geometry-renderer.tsx +4 -1
  80. package/src/components/editor-2d/renderers/floorplan-registry-layer.test.ts +38 -8
  81. package/src/components/editor-2d/renderers/floorplan-registry-layer.tsx +291 -227
  82. package/src/components/editor-2d/svg-paths.ts +12 -15
  83. package/src/components/procedural-items/inspector.tsx +40 -15
  84. package/src/components/systems/ceiling/ceiling-selection-affordance-system.test.ts +97 -14
  85. package/src/components/systems/ceiling/ceiling-selection-affordance-system.tsx +18 -18
  86. package/src/components/systems/zone/zone-system.test.ts +47 -0
  87. package/src/components/systems/zone/zone-system.tsx +29 -68
  88. package/src/components/tools/item/placement-history.test.tsx +474 -0
  89. package/src/components/tools/item/placement-strategies.ts +8 -4
  90. package/src/components/tools/item/use-draft-node.test.tsx +432 -1
  91. package/src/components/tools/item/use-draft-node.ts +166 -95
  92. package/src/components/tools/item/use-placement-coordinator.tsx +93 -46
  93. package/src/components/tools/paint-region/use-paint-tint.ts +37 -0
  94. package/src/components/tools/paint-region/wall-paint-region-tool.tsx +466 -0
  95. package/src/components/tools/registry/move-registry-node-tool.tsx +3 -1
  96. package/src/components/tools/select/box-select-tool.tsx +2 -12
  97. package/src/components/tools/select/plane-box-select-tool.test.tsx +79 -0
  98. package/src/components/tools/select/plane-box-select-tool.tsx +35 -22
  99. package/src/components/tools/select/select-candidates.test.ts +37 -4
  100. package/src/components/tools/select/select-candidates.ts +15 -11
  101. package/src/components/tools/shared/cursor-sphere.tsx +36 -12
  102. package/src/components/tools/site/site-boundary-editor.tsx +1 -2
  103. package/src/components/tools/stair/stair-defaults.ts +0 -3
  104. package/src/components/tools/stair/stair-tool.tsx +90 -42
  105. package/src/components/tools/tool-manager.tsx +46 -20
  106. package/src/components/tools/wall/wall-drafting.test.ts +19 -4
  107. package/src/components/tools/zone/zone-tool.tsx +4 -12
  108. package/src/components/ui/action-menu/action-button.tsx +3 -0
  109. package/src/components/ui/action-menu/camera-actions.tsx +2 -1
  110. package/src/components/ui/action-menu/control-modes.tsx +2 -35
  111. package/src/components/ui/action-menu/measurement-control.tsx +3 -10
  112. package/src/components/ui/build-panel/build-panel.tsx +235 -0
  113. package/src/components/ui/command-palette/editor-commands.tsx +4 -30
  114. package/src/components/ui/command-palette/index.tsx +5 -24
  115. package/src/components/ui/controls/finish-swatch.tsx +110 -0
  116. package/src/components/ui/controls/material-paint-panel.tsx +97 -26
  117. package/src/components/ui/controls/material-picker.tsx +4 -1
  118. package/src/components/ui/controls/scene-material-list.tsx +4 -32
  119. package/src/components/ui/controls/select-control.tsx +65 -0
  120. package/src/components/ui/floating-level-selector.tsx +9 -14
  121. package/src/components/ui/helpers/building-helper.tsx +4 -1
  122. package/src/components/ui/helpers/contextual-helper-panel.tsx +154 -70
  123. package/src/components/ui/helpers/helper-manager.test.tsx +69 -0
  124. package/src/components/ui/helpers/helper-manager.tsx +325 -14
  125. package/src/components/ui/helpers/item-helper.tsx +8 -0
  126. package/src/components/ui/helpers/registered-tool-helper.tsx +8 -1
  127. package/src/components/ui/icon-ref.tsx +45 -1
  128. package/src/components/ui/level-duplicate-dialog.tsx +1 -1
  129. package/src/components/ui/node-icon-image.tsx +16 -0
  130. package/src/components/ui/panels/collections/collections-popover.tsx +20 -14
  131. package/src/components/ui/panels/floor-foundation-panel.tsx +551 -0
  132. package/src/components/ui/panels/floor-opening-panel.tsx +143 -0
  133. package/src/components/ui/panels/mobile-panel-sheet.tsx +2 -2
  134. package/src/components/ui/panels/mobile-selection-bar.tsx +2 -2
  135. package/src/components/ui/panels/multi-field-value.test.ts +26 -3
  136. package/src/components/ui/panels/multi-field-value.ts +6 -3
  137. package/src/components/ui/panels/multi-parametric-inspector.tsx +52 -2
  138. package/src/components/ui/panels/multi-selection-panel.tsx +43 -4
  139. package/src/components/ui/panels/node-display.ts +8 -3
  140. package/src/components/ui/panels/panel-manager.tsx +103 -68
  141. package/src/components/ui/panels/panel-wrapper.tsx +78 -36
  142. package/src/components/ui/panels/parametric-field-control.tsx +1 -1
  143. package/src/components/ui/panels/parametric-inspector.tsx +4 -4
  144. package/src/components/ui/panels/reference-panel.tsx +2 -3
  145. package/src/components/ui/panels/room-construction-rows.tsx +774 -0
  146. package/src/components/ui/panels/room-name-options.test.ts +55 -0
  147. package/src/components/ui/panels/room-name-options.ts +47 -0
  148. package/src/components/ui/panels/room-openings-field.tsx +110 -0
  149. package/src/components/ui/panels/room-panel.tsx +725 -0
  150. package/src/components/ui/panels/shape-choice.tsx +58 -0
  151. package/src/components/ui/panels/wall-region-list.tsx +109 -0
  152. package/src/components/ui/primitives/color-dot.tsx +36 -24
  153. package/src/components/ui/primitives/sidebar.tsx +2 -0
  154. package/src/components/ui/right-stack.test.ts +29 -0
  155. package/src/components/ui/right-stack.tsx +145 -0
  156. package/src/components/ui/sidebar/icon-rail.tsx +5 -0
  157. package/src/components/ui/sidebar/panels/settings-panel/index.tsx +31 -4
  158. package/src/components/ui/sidebar/panels/settings-panel/keyboard-shortcuts-dialog.tsx +5 -5
  159. package/src/components/ui/sidebar/panels/site-panel/ceiling-tree-node.tsx +4 -4
  160. package/src/components/ui/sidebar/panels/site-panel/chimney-tree-node.tsx +1 -5
  161. package/src/components/ui/sidebar/panels/site-panel/collection-tree-node.tsx +197 -0
  162. package/src/components/ui/sidebar/panels/site-panel/column-tree-node.tsx +1 -5
  163. package/src/components/ui/sidebar/panels/site-panel/door-tree-node.tsx +1 -5
  164. package/src/components/ui/sidebar/panels/site-panel/dormer-tree-node.tsx +1 -5
  165. package/src/components/ui/sidebar/panels/site-panel/elevator-tree-node.tsx +1 -5
  166. package/src/components/ui/sidebar/panels/site-panel/fence-tree-node.tsx +24 -11
  167. package/src/components/ui/sidebar/panels/site-panel/gutter-tree-node.tsx +1 -5
  168. package/src/components/ui/sidebar/panels/site-panel/index.tsx +176 -211
  169. package/src/components/ui/sidebar/panels/site-panel/inline-rename-input.tsx +30 -8
  170. package/src/components/ui/sidebar/panels/site-panel/item-tree-node.tsx +1 -3
  171. package/src/components/ui/sidebar/panels/site-panel/level-tree-node.tsx +4 -1
  172. package/src/components/ui/sidebar/panels/site-panel/registry-tree-node.tsx +1 -3
  173. package/src/components/ui/sidebar/panels/site-panel/roof-tree-node.tsx +1 -4
  174. package/src/components/ui/sidebar/panels/site-panel/shelf-tree-node.tsx +1 -3
  175. package/src/components/ui/sidebar/panels/site-panel/slab-tree-node.tsx +21 -13
  176. package/src/components/ui/sidebar/panels/site-panel/solar-panel-tree-node.tsx +1 -5
  177. package/src/components/ui/sidebar/panels/site-panel/spawn-tree-node.tsx +1 -5
  178. package/src/components/ui/sidebar/panels/site-panel/stair-tree-node.tsx +1 -5
  179. package/src/components/ui/sidebar/panels/site-panel/tree-node.tsx +0 -27
  180. package/src/components/ui/sidebar/panels/site-panel/unit-tree-node.tsx +2 -3
  181. package/src/components/ui/sidebar/panels/site-panel/wall-tree-node.tsx +1 -5
  182. package/src/components/ui/sidebar/panels/site-panel/window-tree-node.tsx +1 -5
  183. package/src/components/ui/sidebar/panels/site-panel/zone-tree-node.tsx +9 -4
  184. package/src/components/ui/sidebar/panels/zone-panel/index.tsx +0 -2
  185. package/src/components/ui/sidebar/tab-bar.tsx +10 -0
  186. package/src/components/ui/sidebar/use-plugin-panels.tsx +15 -5
  187. package/src/components/viewer/floorplan-compass-button.tsx +9 -1
  188. package/src/components/viewer/floorplan-preview.tsx +5 -6
  189. package/src/components/viewer/viewer-hover-label.tsx +34 -0
  190. package/src/components/viewer/viewer-rooms.tsx +175 -0
  191. package/src/components/viewer/viewer-scene-header.tsx +181 -145
  192. package/src/components/viewer/viewer-selection-manager.tsx +124 -0
  193. package/src/components/viewer/viewer-stage.tsx +2 -2
  194. package/src/components/viewer-zone-system.tsx +3 -8
  195. package/src/hooks/use-auto-save.integration.test.tsx +123 -9
  196. package/src/hooks/use-auto-save.ts +34 -8
  197. package/src/hooks/use-keyboard.test.ts +178 -3
  198. package/src/hooks/use-keyboard.ts +89 -71
  199. package/src/hooks/use-open-wall-ends.test.ts +99 -0
  200. package/src/hooks/use-open-wall-ends.ts +238 -0
  201. package/src/hooks/use-placement-notice.test.ts +48 -0
  202. package/src/hooks/use-placement-notice.ts +42 -0
  203. package/src/hooks/use-selected-room.ts +143 -0
  204. package/src/index.tsx +246 -39
  205. package/src/lib/ceiling-edit-session.test.ts +321 -0
  206. package/src/lib/ceiling-edit-session.ts +206 -0
  207. package/src/lib/ceiling-region-session.test.ts +179 -0
  208. package/src/lib/continuation.test.ts +19 -1
  209. package/src/lib/continuation.ts +25 -4
  210. package/src/lib/door-interaction.ts +14 -0
  211. package/src/lib/editor-level-display.test.ts +96 -0
  212. package/src/lib/editor-level-display.ts +48 -0
  213. package/src/lib/editor-views.ts +144 -0
  214. package/src/lib/export-geometry-wait.test.ts +90 -0
  215. package/src/lib/floor-edit-session.ts +153 -0
  216. package/src/lib/floor-footprints.test.ts +563 -0
  217. package/src/lib/floor-footprints.ts +395 -0
  218. package/src/lib/floor-foundation-support.test.ts +283 -0
  219. package/src/lib/floor-opening-draft.ts +218 -0
  220. package/src/lib/floor-openings.test.ts +216 -0
  221. package/src/lib/floor-region-geometry.ts +191 -0
  222. package/src/lib/floor-region-session.test.ts +485 -0
  223. package/src/lib/floor-region-session.ts +589 -0
  224. package/src/lib/floor-region-snap.ts +101 -0
  225. package/src/lib/floor-step-hover.test.ts +32 -0
  226. package/src/lib/floorplan/annotation-visibility.ts +17 -0
  227. package/src/lib/floorplan/dimension-drive.test.ts +360 -0
  228. package/src/lib/floorplan/dimension-drive.ts +479 -0
  229. package/src/lib/floorplan/floorplan-export.test.ts +12 -8
  230. package/src/lib/floorplan/floorplan-export.tsx +32 -8
  231. package/src/lib/floorplan/floorplan-extension.ts +32 -0
  232. package/src/lib/floorplan/floorplan-mode.test.ts +4 -0
  233. package/src/lib/floorplan/floorplan-mode.ts +2 -0
  234. package/src/lib/floorplan/floorplan-pdfkit-document.ts +339 -18
  235. package/src/lib/floorplan/floorplan-pdfkit-renderer.test.ts +138 -8
  236. package/src/lib/floorplan/floorplan-pdfkit-renderer.ts +56 -25
  237. package/src/lib/floorplan/floorplan-plugin-install.test.tsx +138 -0
  238. package/src/lib/floorplan/floorplan-readonly.ts +6 -0
  239. package/src/lib/floorplan/geometry.ts +13 -6
  240. package/src/lib/floorplan/index.ts +0 -9
  241. package/src/lib/floorplan/items.ts +5 -2
  242. package/src/lib/floorplan/marks.test.ts +213 -0
  243. package/src/lib/floorplan/marks.ts +238 -0
  244. package/src/lib/floorplan/open-wall-ends.test.ts +268 -0
  245. package/src/lib/floorplan/open-wall-ends.ts +162 -0
  246. package/src/lib/floorplan/pdf-fonts/GeistMono-Bold.ttf +0 -0
  247. package/src/lib/floorplan/pdf-fonts/GeistMono-Regular.ttf +0 -0
  248. package/src/lib/floorplan/pdf-fonts/LICENSE-GeistMono.txt +92 -0
  249. package/src/lib/floorplan/pdf-fonts/LICENSE-LiberationSans.txt +102 -0
  250. package/src/lib/floorplan/pdf-fonts/LiberationSans-Bold.ttf +0 -0
  251. package/src/lib/floorplan/pdf-fonts/LiberationSans-Regular.ttf +0 -0
  252. package/src/lib/floorplan/schedules.test.ts +189 -0
  253. package/src/lib/floorplan/schedules.ts +339 -0
  254. package/src/lib/floorplan/sheet-export.test.ts +100 -0
  255. package/src/lib/floorplan/sheet-export.ts +318 -0
  256. package/src/lib/floorplan/site-plan/build-site-plan-drawing.ts +779 -0
  257. package/src/lib/floorplan/site-plan/contours.test.ts +43 -0
  258. package/src/lib/floorplan/site-plan/contributors.ts +97 -0
  259. package/src/lib/floorplan/site-plan/coverage.ts +232 -0
  260. package/src/lib/floorplan/site-plan/drawing-type-switch.test.ts +15 -0
  261. package/src/lib/floorplan/site-plan/drawing-type-switch.tsx +86 -0
  262. package/src/lib/floorplan/site-plan/front-edge.test.ts +103 -0
  263. package/src/lib/floorplan/site-plan/front-edge.ts +156 -0
  264. package/src/lib/floorplan/site-plan/geometry.test.ts +275 -0
  265. package/src/lib/floorplan/site-plan/geometry.ts +557 -0
  266. package/src/lib/floorplan/site-plan/index.ts +71 -0
  267. package/src/lib/floorplan/site-plan/site-annotations.test.ts +366 -0
  268. package/src/lib/floorplan/site-plan/site-annotations.ts +1083 -0
  269. package/src/lib/floorplan/site-plan/site-parts.ts +379 -0
  270. package/src/lib/floorplan/site-plan/site-plan-layer.tsx +138 -0
  271. package/src/lib/floorplan/site-plan/street-name.test.ts +69 -0
  272. package/src/lib/floorplan/site-plan/street-name.ts +302 -0
  273. package/src/lib/floorplan/site-plan/yards.test.ts +69 -0
  274. package/src/lib/floorplan/types.ts +1 -27
  275. package/src/lib/fresh-planar-placement.test.ts +36 -0
  276. package/src/lib/fresh-planar-placement.ts +59 -36
  277. package/src/lib/geometry-script/author.ts +152 -0
  278. package/src/lib/geometry-script/client.ts +152 -0
  279. package/src/lib/geometry-script/protocol.ts +12 -0
  280. package/src/lib/gesture-lifecycle.test.ts +595 -0
  281. package/src/lib/gesture-lifecycle.ts +290 -0
  282. package/src/lib/glb-export-zone-holes.test.ts +41 -0
  283. package/src/lib/glb-export.test.ts +485 -0
  284. package/src/lib/glb-export.ts +237 -119
  285. package/src/lib/history.test.ts +6 -0
  286. package/src/lib/history.ts +7 -0
  287. package/src/lib/hud-title.ts +109 -0
  288. package/src/lib/hydration-gated-storage.ts +49 -0
  289. package/src/lib/ifc-export.test.ts +174 -0
  290. package/src/lib/ifc-export.ts +201 -0
  291. package/src/lib/ifc-parts.ts +15 -0
  292. package/src/lib/inspector-expanded.test.ts +44 -0
  293. package/src/lib/inspector-expanded.ts +36 -0
  294. package/src/lib/interaction/scope.ts +18 -0
  295. package/src/lib/keyboard-pan.test.ts +129 -7
  296. package/src/lib/keyboard-pan.ts +161 -12
  297. package/src/lib/live-gesture-round-trip.test.ts +360 -0
  298. package/src/lib/lot/__fixtures__/map-dossier-miami-shores.json +1 -0
  299. package/src/lib/lot/__fixtures__/map-dossier-stpete.json +1 -0
  300. package/src/lib/lot/clean-ring.test.ts +186 -0
  301. package/src/lib/lot/clean-ring.ts +217 -0
  302. package/src/lib/lot/dossier.test.ts +202 -0
  303. package/src/lib/lot/dossier.ts +498 -0
  304. package/src/lib/lot/drop-in.test.ts +326 -0
  305. package/src/lib/lot/drop-in.ts +407 -0
  306. package/src/lib/lot/index.ts +43 -0
  307. package/src/lib/lot/lot-patch.test.ts +340 -0
  308. package/src/lib/lot/lot-patch.ts +394 -0
  309. package/src/lib/lot/parcel-provider.ts +43 -0
  310. package/src/lib/lot/terrain.test.ts +161 -0
  311. package/src/lib/lot/terrain.ts +266 -0
  312. package/src/lib/material-catalog-model.ts +3 -1
  313. package/src/lib/material-paint-panel-model.ts +9 -5
  314. package/src/lib/material-paint.ts +4 -0
  315. package/src/lib/mezzanine-actions.test.ts +342 -0
  316. package/src/lib/mezzanine-draft.test.ts +501 -0
  317. package/src/lib/mezzanine-draft.ts +278 -0
  318. package/src/lib/mezzanine-messages.ts +26 -0
  319. package/src/lib/mezzanine-selection.test.ts +207 -0
  320. package/src/lib/mezzanine-stairs.ts +46 -0
  321. package/src/lib/model-export.ts +3 -1
  322. package/src/lib/node-action-movement.ts +17 -6
  323. package/src/lib/node-display-label.ts +37 -0
  324. package/src/lib/orbit-floor-clearance.test.ts +165 -0
  325. package/src/lib/orbit-floor-clearance.ts +106 -0
  326. package/src/lib/paint-commit-route.test.ts +37 -0
  327. package/src/lib/paint-commit-route.ts +17 -0
  328. package/src/lib/paint-eyedropper.test.tsx +165 -0
  329. package/src/lib/paint-eyedropper.ts +99 -0
  330. package/src/lib/paint-pass-through.test.ts +111 -0
  331. package/src/lib/paint-pass-through.ts +76 -0
  332. package/src/lib/paint-region-hover.ts +25 -0
  333. package/src/lib/paint-region-mode.ts +154 -0
  334. package/src/lib/paint-regions.test.ts +166 -0
  335. package/src/lib/paint-regions.ts +192 -0
  336. package/src/lib/paint-scope.test.ts +317 -35
  337. package/src/lib/paint-scope.ts +266 -25
  338. package/src/lib/panel-rows.ts +21 -0
  339. package/src/lib/parametric-node-update.ts +1 -1
  340. package/src/lib/plate-paint-affected.test.ts +300 -0
  341. package/src/lib/plate-paint-affected.ts +177 -0
  342. package/src/lib/plugin-chat.ts +50 -0
  343. package/src/lib/plugin-panels.test.ts +25 -0
  344. package/src/lib/plugin-panels.ts +94 -0
  345. package/src/lib/print-shell-compiler.ts +1 -0
  346. package/src/lib/registered-draft-snap.ts +12 -0
  347. package/src/lib/roof-duplication.ts +10 -2
  348. package/src/lib/room-assembly-overlay.test.ts +275 -0
  349. package/src/lib/room-assembly-overlay.ts +266 -0
  350. package/src/lib/room-built-on.test.ts +379 -0
  351. package/src/lib/room-built-on.ts +138 -0
  352. package/src/lib/room-construction-commands.ts +712 -0
  353. package/src/lib/room-construction.test.ts +746 -0
  354. package/src/lib/room-construction.ts +385 -0
  355. package/src/lib/room-controls-phase6.test.ts +630 -0
  356. package/src/lib/room-divide-session.ts +523 -0
  357. package/src/lib/room-handle-drag.ts +1068 -0
  358. package/src/lib/room-name-catalog.test.ts +90 -0
  359. package/src/lib/room-name-catalog.ts +168 -0
  360. package/src/lib/room-push-dimensions.test.ts +311 -0
  361. package/src/lib/room-push-dimensions.ts +251 -0
  362. package/src/lib/room-selection-commands.ts +62 -0
  363. package/src/lib/room-selection.test.ts +973 -0
  364. package/src/lib/room-selection.ts +417 -0
  365. package/src/lib/room-structure-commands.ts +94 -0
  366. package/src/lib/room-structure.test.ts +921 -0
  367. package/src/lib/room-transform-session.ts +570 -0
  368. package/src/lib/room-zone-routing.test.ts +294 -0
  369. package/src/lib/room-zone-routing.ts +197 -0
  370. package/src/lib/scene-clipboard.test.ts +364 -0
  371. package/src/lib/scene-clipboard.ts +229 -48
  372. package/src/lib/scene-material-refs.test.ts +79 -0
  373. package/src/lib/scene-material-refs.ts +79 -0
  374. package/src/lib/scene.test.ts +24 -36
  375. package/src/lib/scene.ts +33 -57
  376. package/src/lib/selection-routing.test.ts +129 -45
  377. package/src/lib/selection-routing.ts +53 -74
  378. package/src/lib/session-writes.test.ts +120 -0
  379. package/src/lib/session-writes.ts +205 -0
  380. package/src/lib/sidebar-panel.ts +62 -0
  381. package/src/lib/site-boundary.test.ts +4 -6
  382. package/src/lib/site-boundary.ts +1 -1
  383. package/src/lib/snapping-mode.test.ts +4 -0
  384. package/src/lib/snapping-mode.ts +8 -1
  385. package/src/lib/terrace-draft.test.ts +145 -0
  386. package/src/lib/terrace-draft.ts +213 -0
  387. package/src/lib/tool-cursor-icon.test.ts +16 -0
  388. package/src/lib/tool-cursor-icon.ts +11 -0
  389. package/src/lib/typing-target.ts +9 -0
  390. package/src/lib/units.ts +77 -39
  391. package/src/lib/vector-edges.test.ts +58 -0
  392. package/src/lib/vector-edges.ts +400 -0
  393. package/src/lib/viewer-selection.test.ts +577 -0
  394. package/src/lib/viewer-selection.ts +569 -0
  395. package/src/lib/wall-draw-variant.test.ts +71 -0
  396. package/src/lib/wall-draw-variant.ts +91 -0
  397. package/src/lib/wall-paint-region-session.ts +246 -0
  398. package/src/lib/wall-paint-region.test.ts +373 -0
  399. package/src/lib/wall-polygon-draft.test.ts +262 -0
  400. package/src/lib/wall-polygon-draft.ts +286 -0
  401. package/src/lib/wall-push.test.ts +453 -0
  402. package/src/lib/wall-region-face.ts +352 -0
  403. package/src/lib/wall-region-handles.test.ts +304 -0
  404. package/src/lib/wall-region-handles.ts +368 -0
  405. package/src/lib/wall-region-list.test.ts +119 -0
  406. package/src/lib/wall-region-list.ts +136 -0
  407. package/src/lib/wall-region-snap.ts +110 -0
  408. package/src/lib/window-interaction.ts +14 -0
  409. package/src/lib/zone-content.ts +4 -1
  410. package/src/lib/zone-name.test.ts +17 -0
  411. package/src/lib/zone-name.ts +17 -0
  412. package/src/store/opens-in-select-mode.test.ts +223 -0
  413. package/src/store/paint-mode.test.ts +188 -0
  414. package/src/store/terrain-sculpt-mode.test.ts +19 -21
  415. package/src/store/tool-mode.test.ts +137 -6
  416. package/src/store/use-delete-confirmation.ts +28 -1
  417. package/src/store/use-drawing-view.test.ts +24 -3
  418. package/src/store/use-drawing-view.ts +29 -1
  419. package/src/store/use-editor.tsx +432 -321
  420. package/src/store/use-floorplan-draft-preview.ts +13 -0
  421. package/src/store/use-floorplan-marquee.ts +12 -2
  422. package/src/store/use-quick-measurement-hud.ts +3 -3
  423. package/src/store/use-stair-build-preview.ts +13 -5
  424. package/src/store/use-wall-move-ghosts.ts +4 -2
  425. package/src/store/use-wall-region-handles-selection.test.ts +222 -0
  426. package/src/store/use-wall-region-handles-selection.ts +139 -0
  427. package/src/store/view-layout.test.ts +175 -0
  428. package/src/store/view-layout.ts +196 -0
  429. package/src/test-utils/immediate-animation-frames.ts +16 -0
  430. package/src/test-utils/live-gesture-round-trip.ts +224 -0
  431. package/src/test-utils/selection-harness.ts +82 -0
  432. package/src/components/editor-2d/renderers/floorplan-stair-layer.tsx +0 -499
  433. package/src/components/systems/zone/zone-label-editor-system.tsx +0 -333
  434. package/src/components/tools/fence/fence-drafting.test.ts +0 -100
  435. package/src/components/tools/fence/fence-drafting.ts +0 -266
  436. package/src/components/ui/sidebar/panels/plugins-panel.tsx +0 -218
  437. package/src/components/viewer/viewer-units-panel.tsx +0 -116
  438. package/src/lib/floorplan/stairs.ts +0 -489
  439. package/src/lib/level-duplication.test.ts +0 -144
  440. package/src/lib/level-duplication.ts +0 -195
@@ -1,7 +1,17 @@
1
1
  'use client'
2
2
 
3
- import { useEffect } from 'react'
4
- import useDeleteConfirmation from '../../store/use-delete-confirmation'
3
+ import {
4
+ type AnyNode,
5
+ type DeleteZonePayload,
6
+ type StructureNodes,
7
+ useScene,
8
+ } from '@pascal-app/core'
9
+ import { useEffect, useMemo, useRef } from 'react'
10
+ import { cn } from '../../lib/utils'
11
+ import useDeleteConfirmation, {
12
+ type DeleteConfirmationRequest,
13
+ type RoomConstructionChange,
14
+ } from '../../store/use-delete-confirmation'
5
15
  import {
6
16
  Dialog,
7
17
  DialogContent,
@@ -11,10 +21,268 @@ import {
11
21
  DialogTitle,
12
22
  } from '../ui/primitives/dialog'
13
23
 
24
+ const count = (n: number, one: string, many = `${one}s`) => `${n} ${n === 1 ? one : many}`
25
+
26
+ function sentenceList(parts: string[]) {
27
+ if (parts.length <= 1) return parts.join('')
28
+ return `${parts.slice(0, -1).join(', ')} and ${parts.at(-1)}`
29
+ }
30
+
31
+ /**
32
+ * A mezzanine takes its own plate (with its railing) and the ceiling core
33
+ * derived over it; kept items come down to the floor below.
34
+ */
35
+ function mezzanineDeletionDescription(room: DeleteZonePayload, nodes: StructureNodes) {
36
+ const hasCeiling = Object.values(nodes).some(
37
+ (node) => node.type === 'ceiling' && node.zoneId === room.zoneId,
38
+ )
39
+ const sentences = [
40
+ hasCeiling
41
+ ? 'Removes the mezzanine floor, its railing and the ceiling over it.'
42
+ : 'Removes the mezzanine floor and its railing.',
43
+ ]
44
+ const items = room.itemIds.length
45
+ if (items)
46
+ sentences.push(
47
+ `${count(items, 'item')} ${items === 1 ? 'is' : 'are'} on it. Keep ${items === 1 ? 'it' : 'them'} on the floor below, or delete ${items === 1 ? 'it' : 'them'} too.`,
48
+ )
49
+ return sentences.join(' ')
50
+ }
51
+
52
+ /**
53
+ * An area Divide made goes back into the room across its dividing line: the
54
+ * line and the area's own finish go, the walls and anything standing in it stay.
55
+ */
56
+ function mergeDescription(room: DeleteZonePayload, nodes: StructureNodes) {
57
+ const into = nodes[room.mergedIntoZoneId ?? '']?.name?.trim()
58
+ const lines = room.separatorIds.length === 1 ? 'dividing line' : 'dividing lines'
59
+ const sentences = [
60
+ `Merges it back into ${into || 'the room next to it'}: removes the ${lines} and its floor finish. The walls stay.`,
61
+ ]
62
+ const items = room.itemIds.length
63
+ if (items)
64
+ sentences.push(
65
+ `${count(items, 'item')} inside ${items === 1 ? 'stays where it is' : 'stay where they are'}.`,
66
+ )
67
+ return sentences.join(' ')
68
+ }
69
+
70
+ function roomDeletionDescription(room: DeleteZonePayload, nodes: StructureNodes) {
71
+ if (isMezzanine(room, nodes)) return mezzanineDeletionDescription(room, nodes)
72
+ if (room.mode === 'merge') return mergeDescription(room, nodes)
73
+ const sentences: string[] = []
74
+ const typeCount = (type: string) =>
75
+ room.openingIds.filter((id) => nodes[id]?.type === type).length
76
+ const removed = [
77
+ room.wallIds.length ? count(room.wallIds.length, 'wall') : '',
78
+ room.separatorIds.length ? count(room.separatorIds.length, 'separator') : '',
79
+ typeCount('door') ? count(typeCount('door'), 'door') : '',
80
+ typeCount('window') ? count(typeCount('window'), 'window') : '',
81
+ ].filter(Boolean)
82
+ sentences.push(
83
+ removed.length
84
+ ? `Removes ${sentenceList(removed)}, with the floor finish and ceiling.`
85
+ : 'Removes the floor finish and ceiling.',
86
+ )
87
+ if (room.keptSharedWallIds.length + room.keptSharedSeparatorIds.length > 0)
88
+ sentences.push('Walls shared with other rooms stay.')
89
+ if (room.opensZoneIds.length) {
90
+ const names = room.opensZoneIds.map((id) => nodes[id]?.name?.trim() || 'the next room')
91
+ sentences.push(`Opens into ${sentenceList(names)}.`)
92
+ }
93
+ const items = room.itemIds.length
94
+ if (items)
95
+ sentences.push(
96
+ `${count(items, 'item')} ${items === 1 ? 'is' : 'are'} inside. Keep ${items === 1 ? 'it' : 'them'} where ${items === 1 ? 'it is' : 'they are'}, or delete ${items === 1 ? 'it' : 'them'} too.`,
97
+ )
98
+ return sentences.join(' ')
99
+ }
100
+
101
+ /** A hosted node's kind from its type and structure — never from its name. */
102
+ function hostedKind(node: AnyNode | undefined) {
103
+ if (node?.type === 'door' || node?.type === 'window') return node.type
104
+ if (node?.type === 'item' && node.asset.interactive?.effects.some((e) => e.kind === 'light'))
105
+ return 'light'
106
+ return 'item'
107
+ }
108
+
109
+ function hostedName(node: AnyNode | undefined) {
110
+ return (node?.name || (node?.type === 'item' ? node.asset.name : '') || '').trim()
111
+ }
112
+
113
+ function shownNames(names: string[]) {
114
+ const named = [...new Set(names.filter(Boolean))]
115
+ return named.slice(0, 3).join(', ') + (named.length > 3 ? ', …' : '')
116
+ }
117
+
118
+ /** "1 door (Front door), 2 windows and 1 light": counts per kind, a few names, no ids. */
119
+ export function hostedSummary(ids: readonly string[], nodes: StructureNodes) {
120
+ const groups = new Map<string, string[]>()
121
+ for (const id of ids) {
122
+ const kind = hostedKind(nodes[id])
123
+ groups.set(kind, [...(groups.get(kind) ?? []), hostedName(nodes[id])])
124
+ }
125
+ return sentenceList(
126
+ [...groups].map(([kind, names]) => {
127
+ const shown = shownNames(names)
128
+ return `${count(names.length, kind)}${shown ? ` (${shown})` : ''}`
129
+ }),
130
+ )
131
+ }
132
+
133
+ const pronouns = (one: boolean) =>
134
+ one
135
+ ? { it: 'it', is: 'is', its: 'its', verb: (v: string) => `${v}s` }
136
+ : { it: 'them', is: 'are', its: 'their', verb: (v: string) => v }
137
+
138
+ function ceilingContent(
139
+ change: RoomConstructionChange,
140
+ nodes: StructureNodes,
141
+ room: string,
142
+ canKeep: boolean,
143
+ ): DeleteConfirmationContent {
144
+ const ids = change.hostedIds
145
+ const p = pronouns(ids.length === 1)
146
+ const manual = change.manualCeilings ?? []
147
+ const sentences: string[] = []
148
+ if (manual.length) {
149
+ const names = shownNames(manual)
150
+ sentences.push(
151
+ manual.length === 1
152
+ ? `Removes the hand-drawn ceiling${names ? ` (${names})` : ''}.`
153
+ : `Removes ${manual.length} hand-drawn ceilings${names ? ` (${names})` : ''}.`,
154
+ )
155
+ if (change.alsoCovers?.length)
156
+ sentences.push(
157
+ `${manual.length === 1 ? 'It also covers' : 'They also cover'} ${sentenceList(change.alsoCovers)}.`,
158
+ )
159
+ }
160
+ // Lights are known from their light effect; anything else reads as an item.
161
+ const lights = ids.length > 0 && ids.every((id) => hostedKind(nodes[id]) === 'light')
162
+ if (ids.length) {
163
+ const names = shownNames(ids.map((id) => hostedName(nodes[id])))
164
+ sentences.push(
165
+ lights
166
+ ? `${hostedSummary(ids, nodes)} ${p.verb('hang')} from it.`
167
+ : `${count(ids.length, 'item')} ${p.is} on the ceiling${names ? ` (${names})` : ''}.`,
168
+ `Keep ${p.it} in place, or delete ${p.it} too.`,
169
+ )
170
+ }
171
+ return {
172
+ title: `Remove the ceiling from ${room}?`,
173
+ description: sentences.length ? sentences.join(' ') : 'Removes the ceiling.',
174
+ keepLabel: ids.length && canKeep ? (lights ? 'Keep lights' : 'Keep items') : null,
175
+ confirmLabel: ids.length ? 'Remove all' : 'Remove',
176
+ destructive: true,
177
+ }
178
+ }
179
+
180
+ function constructionContent(
181
+ change: RoomConstructionChange,
182
+ nodes: StructureNodes,
183
+ canKeep: boolean,
184
+ ): DeleteConfirmationContent {
185
+ const room = change.roomName.trim() || 'this room'
186
+ const hosted = hostedSummary(change.hostedIds, nodes)
187
+ const p = pronouns(change.hostedIds.length === 1)
188
+ if (change.part === 'walls' && change.action === 'add') {
189
+ return {
190
+ title: `Add walls to ${room}?`,
191
+ description: `Adds ${count(change.wallCount ?? 0, 'wall')} · the room loses ${(change.areaLoss ?? 0).toFixed(2)} m² of floor.`,
192
+ keepLabel: null,
193
+ confirmLabel: 'Add walls',
194
+ destructive: false,
195
+ }
196
+ }
197
+ if (change.part === 'walls') {
198
+ const walls = count(change.wallCount ?? 0, 'wall')
199
+ return {
200
+ title: `Remove walls from ${room}?`,
201
+ description: [
202
+ hosted
203
+ ? `Turns ${walls} into separators and removes ${hosted}.`
204
+ : `Turns ${walls} into separators.`,
205
+ change.sharedWalls ? 'Shared walls remain.' : '',
206
+ ]
207
+ .filter(Boolean)
208
+ .join(' '),
209
+ keepLabel: null,
210
+ confirmLabel: 'Remove walls',
211
+ destructive: true,
212
+ }
213
+ }
214
+ if (change.part === 'floor') {
215
+ return {
216
+ title: `Remove the floor from ${room}?`,
217
+ description: hosted
218
+ ? `${hosted} ${p.verb('stand')} on it. Keep ${p.it} at ${p.its} height, or delete ${p.it} too.`
219
+ : 'Removes the floor.',
220
+ keepLabel: hosted && canKeep ? 'Keep items' : null,
221
+ confirmLabel: hosted ? 'Delete items' : 'Remove',
222
+ destructive: true,
223
+ }
224
+ }
225
+ return ceilingContent(change, nodes, room, canKeep)
226
+ }
227
+
228
+ export type DeleteConfirmationContent = {
229
+ title: string
230
+ description: string
231
+ /** Delete the room but leave its items in place — offered only when it has items. */
232
+ keepLabel: string | null
233
+ confirmLabel: string
234
+ /** Red confirm for removals; adding walls confirms in the neutral style. */
235
+ destructive: boolean
236
+ }
237
+
238
+ /** The words and choices the confirmation offers, from the request alone. */
239
+ function isMezzanine(room: DeleteZonePayload, nodes: StructureNodes) {
240
+ const zone = nodes[room.zoneId]
241
+ return zone?.type === 'zone' && zone.floor?.support === 'open'
242
+ }
243
+
244
+ export function deleteConfirmationContent(
245
+ request: DeleteConfirmationRequest,
246
+ nodes: StructureNodes,
247
+ ): DeleteConfirmationContent {
248
+ if (request.construction)
249
+ return constructionContent(request.construction, nodes, !!request.onKeepContents)
250
+ const room = request.room
251
+ if (!room)
252
+ return {
253
+ title: `Delete ${count(request.count, 'element')}?`,
254
+ description:
255
+ 'This removes every selected element. You can undo the deletion while it remains in the editor history.',
256
+ keepLabel: null,
257
+ confirmLabel: 'Delete',
258
+ destructive: true,
259
+ }
260
+ const merge = room.mode === 'merge'
261
+ // A merged area's items always stay, so there is nothing to choose.
262
+ const hasItems = room.itemIds.length > 0 && !merge
263
+ return {
264
+ title: `Delete ${room.name.trim() || (isMezzanine(room, nodes) ? 'this mezzanine' : merge ? 'this area' : 'this room')}?`,
265
+ description: roomDeletionDescription(room, nodes),
266
+ keepLabel: hasItems && request.onKeepContents ? 'Keep items' : null,
267
+ confirmLabel: hasItems ? 'Delete all' : 'Delete',
268
+ destructive: true,
269
+ }
270
+ }
271
+
14
272
  export function DeleteConfirmationDialog() {
15
273
  const request = useDeleteConfirmation((state) => state.request)
16
274
  const cancel = useDeleteConfirmation((state) => state.cancel)
17
275
  const confirm = useDeleteConfirmation((state) => state.confirm)
276
+ const nodes = useScene((state) => (request ? state.nodes : null))
277
+ const live = useMemo(
278
+ () => (request && nodes ? deleteConfirmationContent(request, nodes) : null),
279
+ [request, nodes],
280
+ )
281
+ // Hold the last words through the close animation so the dialog never empties.
282
+ const shown = useRef(live)
283
+ if (live) shown.current = live
284
+ const content = shown.current
285
+ const blocked = !!request?.conflict
18
286
 
19
287
  useEffect(() => cancel, [cancel])
20
288
 
@@ -26,10 +294,12 @@ export function DeleteConfirmationDialog() {
26
294
  showCloseButton={false}
27
295
  >
28
296
  <DialogHeader>
29
- <DialogTitle>Delete {request?.count ?? 0} elements?</DialogTitle>
297
+ <DialogTitle>{content?.title}</DialogTitle>
30
298
  <DialogDescription>
31
- This removes every selected element. You can undo the deletion while it remains in the
32
- editor history.
299
+ {content?.description}
300
+ {request?.conflict && (
301
+ <span className="mt-2 block text-destructive">{request.conflict}</span>
302
+ )}
33
303
  </DialogDescription>
34
304
  </DialogHeader>
35
305
  <DialogFooter>
@@ -40,12 +310,32 @@ export function DeleteConfirmationDialog() {
40
310
  >
41
311
  Cancel
42
312
  </button>
313
+ {content?.keepLabel && (
314
+ <button
315
+ className="rounded-full border border-border px-4 py-2 text-sm transition-colors hover:bg-accent disabled:pointer-events-none disabled:opacity-50"
316
+ disabled={blocked}
317
+ onClick={() => {
318
+ const keep = request?.onKeepContents
319
+ cancel()
320
+ keep?.()
321
+ }}
322
+ type="button"
323
+ >
324
+ {content.keepLabel}
325
+ </button>
326
+ )}
43
327
  <button
44
- className="rounded-full bg-red-600 px-4 py-2 text-sm text-white transition-colors hover:bg-red-700"
328
+ className={cn(
329
+ 'rounded-full px-4 py-2 text-sm transition-colors disabled:pointer-events-none disabled:opacity-50',
330
+ content?.destructive === false
331
+ ? 'bg-primary text-primary-foreground hover:bg-primary/90'
332
+ : 'bg-red-600 text-white hover:bg-red-700',
333
+ )}
334
+ disabled={blocked}
45
335
  onClick={confirm}
46
336
  type="button"
47
337
  >
48
- Delete
338
+ {content?.confirmLabel}
49
339
  </button>
50
340
  </DialogFooter>
51
341
  </DialogContent>
@@ -0,0 +1,244 @@
1
+ import {
2
+ type AnyNode,
3
+ type AnyNodeId,
4
+ type CollectionId,
5
+ ColumnNode,
6
+ collectionIdsOf,
7
+ DoorNode,
8
+ ElevatorNode,
9
+ FenceNode,
10
+ generateId,
11
+ ItemNode,
12
+ nodeRegistry,
13
+ RoofSegmentNode,
14
+ runSceneHistoryDraftWrite,
15
+ SpawnNode,
16
+ StairSegmentNode,
17
+ useScene,
18
+ WallNode,
19
+ WindowNode,
20
+ } from '@pascal-app/core'
21
+ import { useViewer } from '@pascal-app/viewer'
22
+ import {
23
+ createFreshPlacementSubtree,
24
+ duplicatesAsFreshSubtree,
25
+ prepareFreshPlacementRootDuplicate,
26
+ } from '../../lib/fresh-planar-placement'
27
+ import { duplicateWithoutMove, registryMoveDisabled } from '../../lib/node-action-movement'
28
+ import { duplicateRoofSubtree } from '../../lib/roof-duplication'
29
+ import { sfxEmitter } from '../../lib/sfx-bus'
30
+ import useEditor from '../../store/use-editor'
31
+ import { startZoneRoomTransform } from './room-controls'
32
+
33
+ let carriedCopy: { id: AnyNodeId; sourceId: AnyNodeId } | null = null
34
+
35
+ /**
36
+ * The collections a copy being placed joins when its move tool creates it:
37
+ * its draft's when the copy is in the scene, else those of the node it copies.
38
+ */
39
+ export function copyCollectionIds(copy: AnyNode): CollectionId[] {
40
+ const { collections, nodes } = useScene.getState()
41
+ if (nodes[copy.id as AnyNodeId]) return collectionIdsOf(collections, copy.id as AnyNodeId)
42
+ if (carriedCopy?.id !== copy.id) return []
43
+ return collectionIdsOf(collections, carriedCopy.sourceId)
44
+ }
45
+
46
+ /**
47
+ * Duplicate one node and hand the copy to its move tool: the toolbar's and
48
+ * every inspector's Duplicate. The copy is in the collections its source is in.
49
+ */
50
+ export function duplicateNodeAndPickUp(node: AnyNode) {
51
+ if (!node.parentId) return
52
+ if (startZoneRoomTransform(node, 'duplicate')) return
53
+ sfxEmitter.emit('sfx:item-pick')
54
+ const { setMovingNode } = useEditor.getState()
55
+ const { setSelection } = useViewer.getState()
56
+
57
+ if (registryMoveDisabled(node)) {
58
+ try {
59
+ const id = duplicateWithoutMove(node)
60
+ if (id) setSelection({ selectedIds: [id] })
61
+ } catch (error) {
62
+ console.error('Failed to duplicate node', error)
63
+ }
64
+ return
65
+ }
66
+
67
+ if (node.type === 'roof') {
68
+ try {
69
+ duplicateRoofSubtree(node.id as AnyNodeId, { mode: 'move' })
70
+ } catch (error) {
71
+ console.error('Failed to duplicate roof', error)
72
+ }
73
+ return
74
+ }
75
+
76
+ runSceneHistoryDraftWrite(() => {
77
+ if (duplicatesAsFreshSubtree(node)) {
78
+ let draftId: AnyNodeId | null = null
79
+ try {
80
+ draftId = createFreshPlacementSubtree(node.id as AnyNodeId)
81
+ const draft = draftId ? useScene.getState().nodes[draftId] : null
82
+ if (draft) {
83
+ setMovingNode(draft)
84
+ setSelection({ selectedIds: [] })
85
+ return
86
+ }
87
+ } catch (error) {
88
+ if (draftId && useScene.getState().nodes[draftId]) {
89
+ useScene.getState().deleteNode(draftId)
90
+ }
91
+ console.error('Failed to duplicate node subtree', error)
92
+ }
93
+ return
94
+ }
95
+
96
+ const duplicateInfo = prepareFreshPlacementRootDuplicate(node) as any
97
+
98
+ let duplicate: AnyNode | null = null
99
+ try {
100
+ if (node.type === 'door') {
101
+ duplicate = DoorNode.parse(duplicateInfo)
102
+ } else if (node.type === 'window') {
103
+ duplicate = WindowNode.parse(duplicateInfo)
104
+ } else if (node.type === 'item') {
105
+ duplicate = ItemNode.parse(duplicateInfo)
106
+ } else if (node.type === 'elevator') {
107
+ duplicate = ElevatorNode.parse(duplicateInfo)
108
+ } else if (node.type === 'column') {
109
+ duplicate = ColumnNode.parse(duplicateInfo)
110
+ } else if (node.type === 'wall') {
111
+ duplicate = WallNode.parse(duplicateInfo)
112
+ } else if (node.type === 'fence') {
113
+ duplicate = FenceNode.parse(duplicateInfo)
114
+ duplicate.start = [duplicate.start[0] + 1, duplicate.start[1] + 1]
115
+ duplicate.end = [duplicate.end[0] + 1, duplicate.end[1] + 1]
116
+ } else if (node.type === 'roof-segment') {
117
+ duplicateInfo.id = generateId('rseg')
118
+ duplicate = RoofSegmentNode.parse(duplicateInfo)
119
+ } else if (node.type === 'stair-segment') {
120
+ duplicate = StairSegmentNode.parse(duplicateInfo)
121
+ } else if (node.type === 'spawn') {
122
+ duplicate = SpawnNode.parse(duplicateInfo)
123
+ }
124
+
125
+ // Registry-driven fallback: any kind with a NodeDefinition can be
126
+ // duplicated through its schema's parse(). Future built-in kinds
127
+ // get duplicate for free.
128
+ if (!duplicate) {
129
+ const def = nodeRegistry.get(node.type)
130
+ if (def) {
131
+ duplicate = def.schema.parse(duplicateInfo) as AnyNode
132
+ }
133
+ }
134
+ } catch (error) {
135
+ console.error('Failed to parse duplicate', error)
136
+ return
137
+ }
138
+
139
+ if (!duplicate) {
140
+ return
141
+ }
142
+
143
+ const copy = duplicate
144
+ // The copy is in the collections its source is in, from its draft on.
145
+ const createCopy = () =>
146
+ useScene.getState().createNodes([
147
+ {
148
+ node: copy,
149
+ parentId: copy.parentId as AnyNodeId,
150
+ collectionIds: collectionIdsOf(useScene.getState().collections, node.id),
151
+ },
152
+ ])
153
+ if (
154
+ duplicate.type === 'door' ||
155
+ duplicate.type === 'window' ||
156
+ duplicate.type === 'elevator' ||
157
+ duplicate.type === 'wall' ||
158
+ duplicate.type === 'fence'
159
+ ) {
160
+ createCopy()
161
+ } else if (duplicate.type === 'roof-segment' || duplicate.type === 'stair-segment') {
162
+ // Add small offset to make it visible
163
+ if ('position' in duplicate) {
164
+ duplicate.position = [
165
+ duplicate.position[0] + 1,
166
+ duplicate.position[1],
167
+ duplicate.position[2] + 1,
168
+ ]
169
+ }
170
+ createCopy()
171
+ } else if (
172
+ duplicate.type === 'item' ||
173
+ duplicate.type === 'chimney' ||
174
+ duplicate.type === 'dormer'
175
+ ) {
176
+ // Items, chimneys & dormers use pure drag-to-place: NO node is
177
+ // inserted into the scene until the user clicks to commit. The
178
+ // `setMovingNode` call below hands the clone (with
179
+ // `metadata.isNew = true` + no id) to its move tool —
180
+ // `MoveItemTool` / `MoveChimneyTool` / `MoveDormerTool` — which
181
+ // create a draft and call `createNode` on the commit click.
182
+ // Pre-creating here would drop a second copy into the scene
183
+ // before any click — the furnish-tab "duplicate auto-places an
184
+ // item without clicking" bug. (Item has its own
185
+ // draft-committing move tool, so it must skip the generic
186
+ // registry auto-create branch below.)
187
+ carriedCopy = { id: duplicate.id as AnyNodeId, sourceId: node.id as AnyNodeId }
188
+ } else if (
189
+ duplicate.type === 'duct-segment' ||
190
+ duplicate.type === 'duct-fitting' ||
191
+ duplicate.type === 'pipe-segment' ||
192
+ duplicate.type === 'lineset' ||
193
+ duplicate.type === 'liquid-line'
194
+ ) {
195
+ // Duct runs & fittings, DWV pipe runs, and refrigerant linesets use
196
+ // pure drag-to-place: NO node is inserted into the scene until the
197
+ // commit click. `setMovingNode` below hands the clone (with
198
+ // `metadata.isNew`) to its ghost tool (`MoveDuctSegmentTool` /
199
+ // `MoveDuctFittingTool` / `MovePipeSegmentTool` / `MoveLinesetTool`),
200
+ // which previews a translucent copy inside a footprint bounding box
201
+ // on the cursor and calls `createNode` on the drop click.
202
+ // Pre-creating here would drop a copy before any click — the
203
+ // "auto-places it" bug.
204
+ carriedCopy = { id: duplicate.id as AnyNodeId, sourceId: node.id as AnyNodeId }
205
+ } else if (nodeRegistry.has(duplicate.type)) {
206
+ // Registry-driven kinds: offset slightly so the duplicate doesn't
207
+ // overlap exactly, then create + hand to the move tool. Mirrors the
208
+ // roof-segment / stair-segment behavior.
209
+ if ('position' in duplicate && Array.isArray((duplicate as any).position)) {
210
+ const pos = (duplicate as { position: [number, number, number] }).position
211
+ ;(duplicate as { position: [number, number, number] }).position = [
212
+ pos[0] + 1,
213
+ pos[1],
214
+ pos[2] + 1,
215
+ ]
216
+ } else if ('path' in duplicate && Array.isArray((duplicate as any).path)) {
217
+ // Other polyline kinds (pipe / lineset) carry a `path`, not a
218
+ // `position`. Create the copy HIDDEN so nothing is auto-placed:
219
+ // their shared path mover reveals it as a cursor-following preview
220
+ // on the first mouse move and commits on the next click.
221
+ ;(duplicate as { visible?: boolean }).visible = false
222
+ }
223
+ createCopy()
224
+ }
225
+ if (
226
+ duplicate.type === 'item' ||
227
+ duplicate.type === 'elevator' ||
228
+ duplicate.type === 'column' ||
229
+ duplicate.type === 'wall' ||
230
+ duplicate.type === 'fence' ||
231
+ duplicate.type === 'window' ||
232
+ duplicate.type === 'door' ||
233
+ duplicate.type === 'roof-segment' ||
234
+ duplicate.type === 'spawn' ||
235
+ duplicate.type === 'stair-segment' ||
236
+ // Registry-driven kinds get picked up by MoveTool's generic
237
+ // fallback (MoveRegistryNodeTool) so the user can reposition.
238
+ nodeRegistry.has(duplicate.type)
239
+ ) {
240
+ setMovingNode(duplicate)
241
+ }
242
+ setSelection({ selectedIds: [] })
243
+ })
244
+ }