@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
@@ -14,11 +14,22 @@ import {
14
14
  import { useViewer } from '@pascal-app/viewer'
15
15
  import { type ThreeEvent, useThree } from '@react-three/fiber'
16
16
  import { useEffect, useRef } from 'react'
17
- import { type Camera, type Object3D, type Plane, type Ray, Vector2, type Vector3 } from 'three'
17
+ import {
18
+ type Camera,
19
+ type Object3D,
20
+ type Plane,
21
+ type Ray,
22
+ type Raycaster,
23
+ Vector2,
24
+ type Vector3,
25
+ } from 'three'
26
+ import { beginGesture, scopeMatcher } from '../../../lib/gesture-lifecycle'
18
27
  import { isHistoryShortcut } from '../../../lib/history'
19
28
  import { sfxEmitter } from '../../../lib/sfx-bus'
20
29
  import { intersectSpatialDragPlane } from '../../../lib/spatial-drag-plane'
21
30
  import { getSpatialPointerId, spatialPointerInput } from '../../../lib/spatial-pointer-input'
31
+ import { isTypingTarget } from '../../../lib/typing-target'
32
+ import useInteractionScope from '../../../store/use-interaction-scope'
22
33
  import { suppressBoxSelectForPointer } from '../../tools/select/box-select-state'
23
34
  import { commitHandleDragPatch } from './handle-drag-history'
24
35
 
@@ -82,13 +93,14 @@ type UseHandleDragArgs =
82
93
  }
83
94
 
84
95
  export function swallowNextClick() {
96
+ const clickWindow = window
85
97
  const swallow = (clickEvent: Event) => {
86
98
  clickEvent.stopPropagation()
87
99
  clickEvent.preventDefault()
88
100
  }
89
- window.addEventListener('click', swallow, { capture: true, once: true })
101
+ clickWindow.addEventListener('click', swallow, { capture: true, once: true })
90
102
  setTimeout(() => {
91
- window.removeEventListener('click', swallow, { capture: true })
103
+ clickWindow.removeEventListener('click', swallow, { capture: true })
92
104
  }, 300)
93
105
  }
94
106
 
@@ -119,137 +131,168 @@ export function useHandleDrag(args: UseHandleDragArgs) {
119
131
 
120
132
  useEffect(() => () => dragCleanupRef.current?.(), [])
121
133
 
122
- return (event: ThreeEvent<PointerEvent>) => {
123
- // Only the primary button starts a handle gesture — right/middle-drag
124
- // belongs to the camera, so let it propagate untouched.
125
- if (event.button !== 0) return
126
- event.stopPropagation()
127
- suppressBoxSelectForPointer(event)
128
- const spatialPointerId = getSpatialPointerId(event.nativeEvent)
129
-
130
- if (args.kind === 'tap') {
131
- const restoreInputDragging = suppressInputDraggingUntilPointerRelease(event.pointerId)
132
- if (spatialPointerId) {
133
- spatialPointerInput.capture(spatialPointerId, {
134
- onMove: () => undefined,
135
- onRelease: restoreInputDragging,
136
- onCancel: restoreInputDragging,
137
- })
138
- }
139
- swallowNextClick()
140
- sfxEmitter.emit('sfx:item-pick')
141
- document.body.style.cursor = ''
142
- args.onTap(event)
143
- return
144
- }
134
+ return (event: ThreeEvent<PointerEvent>) =>
135
+ startHandleDrag(args, event, { camera, raycaster, gl }, dragCleanupRef)
136
+ }
145
137
 
146
- const { cursor, dragControls, handleIndex, node, rideObject, setIsDragging } = args
147
- rideObject.updateMatrixWorld()
148
- const spatialRay = spatialPointerId ? event.ray.clone() : null
149
-
150
- const ndc = new Vector2()
151
- const setPointerRay = (clientX: number, clientY: number) => {
152
- const rect = gl.domElement.getBoundingClientRect()
153
- ndc.set(
154
- ((clientX - rect.left) / rect.width) * 2 - 1,
155
- -((clientY - rect.top) / rect.height) * 2 + 1,
156
- )
157
- raycaster.setFromCamera(ndc, camera)
158
- }
159
- const getPointerRay: GetPointerRay = (clientX, clientY, target) => {
160
- if (spatialRay) return target.copy(spatialRay)
161
- setPointerRay(clientX, clientY)
162
- return target.copy(raycaster.ray)
163
- }
164
- const intersectPlane: IntersectPlane = (clientX, clientY, plane, target) => {
165
- if (spatialRay) return intersectSpatialDragPlane(spatialRay, plane, target)
166
- setPointerRay(clientX, clientY)
167
- return raycaster.ray.intersectPlane(plane, target)
138
+ type HandleDragThree = {
139
+ camera: Camera
140
+ raycaster: Raycaster
141
+ gl: { domElement: Pick<HTMLElement, 'getBoundingClientRect'> }
142
+ }
143
+
144
+ /**
145
+ * One press on a handle: a tap, or a drag registered with the gesture
146
+ * lifecycle owner (mode / level / selection change, its scope replaced, its
147
+ * node deleted, any history command, unmount → cancel). Release commits; the
148
+ * cleanup runs exactly once and in `finally`, so a throwing commit cannot
149
+ * leave the drag live. Exported for tests; components use `useHandleDrag`.
150
+ */
151
+ export function startHandleDrag(
152
+ args: UseHandleDragArgs,
153
+ event: ThreeEvent<PointerEvent>,
154
+ { camera, raycaster, gl }: HandleDragThree,
155
+ dragCleanupRef: { current: (() => void) | null },
156
+ ) {
157
+ // Only the primary button starts a handle gesture — right/middle-drag
158
+ // belongs to the camera, so let it propagate untouched.
159
+ if (event.button !== 0) return
160
+ event.stopPropagation()
161
+ suppressBoxSelectForPointer(event)
162
+ const spatialPointerId = getSpatialPointerId(event.nativeEvent)
163
+
164
+ if (args.kind === 'tap') {
165
+ const restoreInputDragging = suppressInputDraggingUntilPointerRelease(event.pointerId)
166
+ if (spatialPointerId) {
167
+ spatialPointerInput.capture(spatialPointerId, {
168
+ onMove: () => undefined,
169
+ onRelease: restoreInputDragging,
170
+ onCancel: restoreInputDragging,
171
+ })
168
172
  }
173
+ swallowNextClick()
174
+ sfxEmitter.emit('sfx:item-pick')
175
+ document.body.style.cursor = ''
176
+ args.onTap(event)
177
+ return
178
+ }
179
+
180
+ const { cursor, dragControls, handleIndex, node, rideObject, setIsDragging } = args
181
+ rideObject.updateMatrixWorld()
182
+ const spatialRay = spatialPointerId ? event.ray.clone() : null
169
183
 
170
- const nodeId = node.id as AnyNodeId
171
- const sceneApi = createSceneApi(useScene)
172
- const initialNode = (sceneApi.get(nodeId) ?? node) as AnyNode
173
- const session = args.onStart({
174
- event,
175
- camera,
184
+ const ndc = new Vector2()
185
+ const setPointerRay = (clientX: number, clientY: number) => {
186
+ const rect = gl.domElement.getBoundingClientRect()
187
+ ndc.set(
188
+ ((clientX - rect.left) / rect.width) * 2 - 1,
189
+ -((clientY - rect.top) / rect.height) * 2 + 1,
190
+ )
191
+ raycaster.setFromCamera(ndc, camera)
192
+ }
193
+ const getPointerRay: GetPointerRay = (clientX, clientY, target) => {
194
+ if (spatialRay) return target.copy(spatialRay)
195
+ setPointerRay(clientX, clientY)
196
+ return target.copy(raycaster.ray)
197
+ }
198
+ const intersectPlane: IntersectPlane = (clientX, clientY, plane, target) => {
199
+ if (spatialRay) return intersectSpatialDragPlane(spatialRay, plane, target)
200
+ setPointerRay(clientX, clientY)
201
+ return raycaster.ray.intersectPlane(plane, target)
202
+ }
203
+
204
+ const nodeId = node.id as AnyNodeId
205
+ const sceneApi = createSceneApi(useScene)
206
+ const initialNode = (sceneApi.get(nodeId) ?? node) as AnyNode
207
+ const session = args.onStart({
208
+ event,
209
+ camera,
210
+ getPointerRay,
211
+ intersectPlane,
212
+ initialNode,
213
+ node,
214
+ nodeId,
215
+ rideObject,
216
+ sceneApi,
217
+ })
218
+ if (!session) return
219
+
220
+ const overrideId = session.overrideId ?? nodeId
221
+ const markDirty = session.markDirty !== false
222
+ const scopeBefore = useInteractionScope.getState().scope
223
+ document.body.style.cursor = cursor
224
+ sfxEmitter.emit('sfx:item-pick')
225
+ useViewer.getState().setInputDragging(true)
226
+ useScene.temporal.getState().pause()
227
+ setIsDragging(true)
228
+ dragControls.onStart(handleIndex, initialNode)
229
+ session.onBegin?.()
230
+ // The scope the handle's own callbacks began is the one whose loss cancels.
231
+ const scopeAfter = useInteractionScope.getState().scope
232
+ const nodeInScene = !!useScene.getState().nodes[nodeId]
233
+
234
+ let lastPatch: Partial<AnyNode> | null = null
235
+ let historyPaused = true
236
+ let releaseSpatialCapture: (() => void) | null = null
237
+ let altKey = event.nativeEvent.altKey
238
+ let shiftKey = event.nativeEvent.shiftKey
239
+ let pendingMoveEvent: PointerEvent | null = null
240
+ let moveFrame = 0
241
+
242
+ const resumeHistory = () => {
243
+ if (!historyPaused) return
244
+ historyPaused = false
245
+ useScene.temporal.getState().resume()
246
+ }
247
+
248
+ const processMove = (moveEvent: PointerEvent) => {
249
+ const patch = session.move({
250
+ event: moveEvent,
251
+ modifiers: { altKey, shiftKey },
176
252
  getPointerRay,
177
253
  intersectPlane,
178
- initialNode,
179
- node,
180
- nodeId,
181
- rideObject,
182
- sceneApi,
183
254
  })
184
- if (!session) return
185
-
186
- const overrideId = session.overrideId ?? nodeId
187
- const markDirty = session.markDirty !== false
188
- document.body.style.cursor = cursor
189
- sfxEmitter.emit('sfx:item-pick')
190
- useViewer.getState().setInputDragging(true)
191
- useScene.temporal.getState().pause()
192
- setIsDragging(true)
193
- dragControls.onStart(handleIndex, initialNode)
194
- session.onBegin?.()
195
-
196
- let lastPatch: Partial<AnyNode> | null = null
197
- let historyPaused = true
198
- let releaseSpatialCapture: (() => void) | null = null
199
- let altKey = event.nativeEvent.altKey
200
- let shiftKey = event.nativeEvent.shiftKey
201
- let pendingMoveEvent: PointerEvent | null = null
202
- let moveFrame = 0
203
-
204
- const resumeHistory = () => {
205
- if (!historyPaused) return
206
- historyPaused = false
207
- useScene.temporal.getState().resume()
208
- }
209
-
210
- const processMove = (moveEvent: PointerEvent) => {
211
- const patch = session.move({
212
- event: moveEvent,
213
- modifiers: { altKey, shiftKey },
214
- getPointerRay,
215
- intersectPlane,
216
- })
217
- if (!patch) return
218
- lastPatch = patch
219
- useLiveNodeOverrides.getState().set(overrideId, patch as Record<string, unknown>)
220
- if (markDirty) {
221
- for (const id of cascadeDirty(overrideId, { scene: createSceneApi(useScene) }))
222
- useScene.getState().markDirty(id)
223
- }
255
+ if (!patch) return
256
+ lastPatch = patch
257
+ useLiveNodeOverrides.getState().set(overrideId, patch as Record<string, unknown>)
258
+ if (markDirty) {
259
+ for (const id of cascadeDirty(overrideId, { scene: createSceneApi(useScene) }))
260
+ useScene.getState().markDirty(id)
224
261
  }
262
+ }
225
263
 
226
- // Coalesce high-frequency pointer events so live React/store updates happen
227
- // at most once per animation frame while retaining the newest position.
228
- const flushMove = () => {
229
- moveFrame = 0
230
- const moveEvent = pendingMoveEvent
231
- pendingMoveEvent = null
232
- if (moveEvent) processMove(moveEvent)
233
- }
234
- const onMove = (moveEvent: PointerEvent) => {
235
- pendingMoveEvent = moveEvent
236
- if (moveFrame !== 0) return
237
- if (typeof window.requestAnimationFrame === 'function') {
238
- moveFrame = window.requestAnimationFrame(flushMove)
239
- } else {
240
- flushMove()
241
- }
264
+ // Coalesce high-frequency pointer events so live React/store updates happen
265
+ // at most once per animation frame while retaining the newest position.
266
+ const flushMove = () => {
267
+ moveFrame = 0
268
+ const moveEvent = pendingMoveEvent
269
+ pendingMoveEvent = null
270
+ if (moveEvent) processMove(moveEvent)
271
+ }
272
+ const onMove = (moveEvent: PointerEvent) => {
273
+ pendingMoveEvent = moveEvent
274
+ if (moveFrame !== 0) return
275
+ if (typeof window.requestAnimationFrame === 'function') {
276
+ moveFrame = window.requestAnimationFrame(flushMove)
277
+ } else {
278
+ flushMove()
242
279
  }
280
+ }
243
281
 
244
- const cleanup = () => {
245
- if (moveFrame !== 0) window.cancelAnimationFrame?.(moveFrame)
246
- moveFrame = 0
247
- pendingMoveEvent = null
248
- window.removeEventListener('pointermove', onMove)
249
- window.removeEventListener('pointerup', onUp)
250
- window.removeEventListener('pointercancel', onCancel)
251
- window.removeEventListener('keydown', onKeyDown, true)
252
- window.removeEventListener('keyup', onKeyUp, true)
282
+ let done = false
283
+ const cleanup = () => {
284
+ if (done) return
285
+ done = true
286
+ if (moveFrame !== 0) window.cancelAnimationFrame?.(moveFrame)
287
+ moveFrame = 0
288
+ pendingMoveEvent = null
289
+ window.removeEventListener('pointermove', onMove)
290
+ window.removeEventListener('pointerup', onUp)
291
+ window.removeEventListener('pointercancel', onCancel)
292
+ window.removeEventListener('keydown', onKeyDown, true)
293
+ window.removeEventListener('keyup', onKeyUp, true)
294
+ dragCleanupRef.current = null
295
+ try {
253
296
  releaseSpatialCapture?.()
254
297
  releaseSpatialCapture = null
255
298
  if (document.body.style.cursor === cursor) {
@@ -258,88 +301,117 @@ export function useHandleDrag(args: UseHandleDragArgs) {
258
301
  resumeHistory()
259
302
  useViewer.getState().setInputDragging(false)
260
303
  setIsDragging(false)
261
- session.onEnd?.()
262
- dragControls.onEnd()
263
- dragCleanupRef.current = null
264
- }
265
-
266
- const clearOverride = () => {
267
- useLiveNodeOverrides.getState().clear(overrideId)
268
- if (markDirty) {
269
- for (const id of cascadeDirty(overrideId, { scene: createSceneApi(useScene) }))
270
- useScene.getState().markDirty(id)
304
+ } finally {
305
+ try {
306
+ session.onEnd?.()
307
+ } finally {
308
+ dragControls.onEnd()
271
309
  }
272
310
  }
311
+ }
273
312
 
274
- const onUp = () => {
275
- if (moveFrame !== 0) window.cancelAnimationFrame?.(moveFrame)
276
- flushMove()
277
- swallowNextClick()
278
- sfxEmitter.emit('sfx:item-place')
279
- if (lastPatch) {
280
- commitHandleDragPatch({
281
- patch: lastPatch,
282
- resumeHistory,
283
- runAsSingleHistoryStep: (run) => runAsSingleSceneHistoryStep(useScene, run),
284
- commit: session.commit ?? ((patch) => sceneApi.update(overrideId, patch)),
285
- })
286
- }
287
- clearOverride()
288
- cleanup()
289
- }
290
-
291
- const onCancel = () => {
292
- session.onCancel?.()
293
- clearOverride()
294
- cleanup()
313
+ const clearOverride = () => {
314
+ useLiveNodeOverrides.getState().clear(overrideId)
315
+ if (markDirty) {
316
+ for (const id of cascadeDirty(overrideId, { scene: createSceneApi(useScene) }))
317
+ useScene.getState().markDirty(id)
295
318
  }
319
+ }
296
320
 
297
- // Escape / ⌘Z abort the drag — capture phase so they win over the global
298
- // use-keyboard arms (⌘Z must never history-jump under a live pointer).
299
- const onKeyDown = (e: KeyboardEvent) => {
300
- if (e.key === 'Alt') {
301
- altKey = true
302
- return
321
+ const gesture = beginGesture({
322
+ kind: 'handle-drag',
323
+ onCancel: () => {
324
+ try {
325
+ session.onCancel?.()
326
+ } finally {
327
+ try {
328
+ clearOverride()
329
+ } finally {
330
+ cleanup()
331
+ }
303
332
  }
304
- if (e.key === 'Shift') {
305
- shiftKey = true
306
- return
333
+ },
334
+ ownsScope:
335
+ scopeAfter !== scopeBefore && scopeAfter.kind !== 'idle'
336
+ ? scopeMatcher(scopeAfter)
337
+ : undefined,
338
+ // A draft handle's node is not in the scene yet; only a real node can go stale.
339
+ stale: nodeInScene ? () => !useScene.getState().nodes[nodeId] : undefined,
340
+ })
341
+
342
+ const onUp = () => {
343
+ if (moveFrame !== 0) window.cancelAnimationFrame?.(moveFrame)
344
+ flushMove()
345
+ swallowNextClick()
346
+ sfxEmitter.emit('sfx:item-place')
347
+ gesture.finish(() => {
348
+ try {
349
+ if (lastPatch) {
350
+ commitHandleDragPatch({
351
+ patch: lastPatch,
352
+ resumeHistory,
353
+ runAsSingleHistoryStep: (run) => runAsSingleSceneHistoryStep(useScene, run),
354
+ commit: session.commit ?? ((patch) => sceneApi.update(overrideId, patch)),
355
+ })
356
+ }
357
+ } finally {
358
+ try {
359
+ clearOverride()
360
+ } finally {
361
+ cleanup()
362
+ }
307
363
  }
308
- if (e.key !== 'Escape' && !isHistoryShortcut(e)) return
309
- e.preventDefault()
310
- e.stopPropagation()
311
- swallowNextClick()
312
- onCancel()
364
+ })
365
+ }
366
+
367
+ const onCancel = () => gesture.cancel('cancel')
368
+
369
+ // Escape / ⌘Z abort the drag — capture phase so they win over the global
370
+ // use-keyboard arms (⌘Z must never history-jump under a live pointer).
371
+ const onKeyDown = (e: KeyboardEvent) => {
372
+ if (e.key === 'Alt') {
373
+ altKey = true
374
+ return
313
375
  }
314
- const onKeyUp = (e: KeyboardEvent) => {
315
- if (e.key === 'Alt') altKey = false
316
- if (e.key === 'Shift') shiftKey = false
376
+ if (e.key === 'Shift') {
377
+ shiftKey = true
378
+ return
317
379
  }
380
+ if (e.key !== 'Escape' && !isHistoryShortcut(e)) return
381
+ if (isTypingTarget(e.target)) return
382
+ e.preventDefault()
383
+ e.stopPropagation()
384
+ swallowNextClick()
385
+ onCancel()
386
+ }
387
+ const onKeyUp = (e: KeyboardEvent) => {
388
+ if (e.key === 'Alt') altKey = false
389
+ if (e.key === 'Shift') shiftKey = false
390
+ }
318
391
 
319
- dragCleanupRef.current = onCancel
320
- if (spatialPointerId && spatialRay) {
321
- releaseSpatialCapture = spatialPointerInput.capture(spatialPointerId, {
322
- onMove: (ray) => {
323
- spatialRay.copy(ray)
324
- processMove(
325
- new PointerEvent('pointermove', {
326
- button: 0,
327
- buttons: 1,
328
- altKey,
329
- pointerId: event.pointerId,
330
- pointerType: 'xr',
331
- }),
332
- )
333
- },
334
- onRelease: onUp,
335
- onCancel,
336
- })
337
- } else {
338
- window.addEventListener('pointermove', onMove)
339
- window.addEventListener('pointerup', onUp)
340
- window.addEventListener('pointercancel', onCancel)
341
- }
342
- window.addEventListener('keydown', onKeyDown, true)
343
- window.addEventListener('keyup', onKeyUp, true)
392
+ dragCleanupRef.current = onCancel
393
+ if (spatialPointerId && spatialRay) {
394
+ releaseSpatialCapture = spatialPointerInput.capture(spatialPointerId, {
395
+ onMove: (ray) => {
396
+ spatialRay.copy(ray)
397
+ processMove(
398
+ new PointerEvent('pointermove', {
399
+ button: 0,
400
+ buttons: 1,
401
+ altKey,
402
+ pointerId: event.pointerId,
403
+ pointerType: 'xr',
404
+ }),
405
+ )
406
+ },
407
+ onRelease: onUp,
408
+ onCancel,
409
+ })
410
+ } else {
411
+ window.addEventListener('pointermove', onMove)
412
+ window.addEventListener('pointerup', onUp)
413
+ window.addEventListener('pointercancel', onCancel)
344
414
  }
415
+ window.addEventListener('keydown', onKeyDown, true)
416
+ window.addEventListener('keyup', onKeyUp, true)
345
417
  }