@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
@@ -0,0 +1,204 @@
1
+ 'use client'
2
+
3
+ import {
4
+ type AnyNodeId,
5
+ getStoredLevelHeight,
6
+ type Point,
7
+ type SlabNode,
8
+ sceneRegistry,
9
+ upperFloorHeightControl,
10
+ useScene,
11
+ } from '@pascal-app/core'
12
+ import { useViewer } from '@pascal-app/viewer'
13
+ import { Html } from '@react-three/drei'
14
+ import { type ThreeEvent, useFrame, useThree } from '@react-three/fiber'
15
+ import { useRef, useState } from 'react'
16
+ import { type Group, Plane, Vector2, Vector3 } from 'three'
17
+ import {
18
+ beginFootprintHeightPreview,
19
+ footprintHeightMinimum,
20
+ footprintHeightValue,
21
+ THICK_FLOOR_HINT,
22
+ thickFloorAdvice,
23
+ } from '../../lib/floor-footprints'
24
+ import { formatLinearMeasurement } from '../../lib/measurements'
25
+ import { runRoomHandleDrag } from '../../lib/room-handle-drag'
26
+ import useEditor, { isGridSnapActive } from '../../store/use-editor'
27
+ import useInteractionScope from '../../store/use-interaction-scope'
28
+ import { suppressBoxSelectForPointer } from '../tools/select/box-select-state'
29
+ import { ElevationArrows, outlineCentroid, useHandleScale } from './room-handles'
30
+
31
+ export const FOOTPRINT_HEIGHT_DRAG_LABEL = 'footprint-height'
32
+
33
+ const PILL =
34
+ 'pointer-events-none flex items-center gap-2 whitespace-nowrap rounded-full border border-border/60 bg-background/90 px-4 py-1.5 text-xs tabular-nums shadow-sm backdrop-blur'
35
+ const NOTICE =
36
+ 'pointer-events-none max-w-72 rounded-full border border-border/60 bg-background/95 px-3 py-1 text-center text-muted-foreground text-xs shadow-md backdrop-blur-md'
37
+
38
+ /** The selected footprint floor (base plate), when it is the one selection in select mode. */
39
+ function useSelectedFootprint(): SlabNode | null {
40
+ const selectedId = useViewer((s) =>
41
+ s.selection.selectedIds.length === 1 ? s.selection.selectedIds[0] : null,
42
+ )
43
+ const enabled = useEditor((s) => s.mode === 'select')
44
+ const idle = useInteractionScope(
45
+ (s) =>
46
+ s.scope.kind === 'idle' ||
47
+ (s.scope.kind === 'handle-drag' && s.scope.handle === FOOTPRINT_HEIGHT_DRAG_LABEL),
48
+ )
49
+ const plate = useScene((s) => {
50
+ const node = selectedId ? s.nodes[selectedId as AnyNodeId] : undefined
51
+ return node?.type === 'slab' && node.plateRole === 'base' ? node : null
52
+ })
53
+ return enabled && idle ? plate : null
54
+ }
55
+
56
+ /**
57
+ * A selected footprint floor's one control in 3D: the rooms' arrow–cube–arrow
58
+ * at the middle of the footprint, dragging its height above the ground. The
59
+ * building follows live; the release lands as one undo step. Refused heights
60
+ * (core's conflict, e.g. an upper floor over another footprint) show beside it.
61
+ */
62
+ export function FootprintHeightHandle() {
63
+ const plate = useSelectedFootprint()
64
+ const root = useRef<Group>(null)
65
+ useFrame(() => {
66
+ const level = plate?.parentId ? sceneRegistry.nodes.get(plate.parentId) : null
67
+ if (!root.current) return
68
+ root.current.visible = !!level
69
+ if (level) {
70
+ level.updateWorldMatrix(true, false)
71
+ root.current.matrix.copy(level.matrixWorld)
72
+ }
73
+ })
74
+ if (!plate) return null
75
+ return (
76
+ <group matrixAutoUpdate={false} ref={root}>
77
+ <FootprintHeightArrows plate={plate} />
78
+ </group>
79
+ )
80
+ }
81
+
82
+ function FootprintHeightArrows({ plate }: { plate: SlabNode }) {
83
+ const { camera, raycaster, gl } = useThree()
84
+ const zoom = useHandleScale()
85
+ const unit = useViewer((s) => s.unit)
86
+ const metricNotation = useViewer((s) => s.metricNotation)
87
+ const [hover, setHover] = useState(false)
88
+ const [dragValue, setDragValue] = useState<number | null>(null)
89
+ const [notice, setNotice] = useState<string | null>(null)
90
+ const [hint, setHint] = useState<string | null>(null)
91
+ const upper = useScene((s) => upperFloorHeightControl(s.nodes, plate) !== null)
92
+ const anchor: Point = outlineCentroid([plate.polygon as Point[], ...(plate.holes as Point[][])])
93
+ const elevation = plate.elevation
94
+
95
+ const onPointerDown = (event: ThreeEvent<PointerEvent>) => {
96
+ if (event.button !== 0) return
97
+ event.stopPropagation()
98
+ suppressBoxSelectForPointer(event)
99
+ const levelId = plate.parentId
100
+ const level = levelId ? sceneRegistry.nodes.get(levelId) : null
101
+ if (!(levelId && level)) return
102
+ level.updateWorldMatrix(true, false)
103
+ const toLocal = level.matrixWorld.clone().invert()
104
+ const world = new Vector3(anchor[0], elevation, anchor[1]).applyMatrix4(level.matrixWorld)
105
+ const normal = camera.getWorldPosition(new Vector3()).sub(world).setY(0)
106
+ if (normal.lengthSq() === 0) return
107
+ const plane = new Plane().setFromNormalAndCoplanarPoint(normal.normalize(), world)
108
+ const localY = (clientX: number, clientY: number) => {
109
+ const rect = gl.domElement.getBoundingClientRect()
110
+ raycaster.setFromCamera(
111
+ new Vector2(
112
+ ((clientX - rect.left) / rect.width) * 2 - 1,
113
+ -((clientY - rect.top) / rect.height) * 2 + 1,
114
+ ),
115
+ camera,
116
+ )
117
+ const hit = raycaster.ray.intersectPlane(plane, new Vector3())
118
+ return hit ? hit.applyMatrix4(toLocal).y : null
119
+ }
120
+ const startY = localY(event.nativeEvent.clientX, event.nativeEvent.clientY)
121
+ if (startY === null) return
122
+ const nodes = () => useScene.getState().nodes
123
+ // Heights are read against the plate as the drag found it (the preview moves it).
124
+ const startNodes = nodes()
125
+ const start = footprintHeightValue(startNodes, plate)
126
+ const min = footprintHeightMinimum(startNodes, plate)
127
+ const levelNode = nodes()[levelId as AnyNodeId]
128
+ const max = levelNode?.type === 'level' ? getStoredLevelHeight(levelNode) - 0.5 : 3
129
+ const step = isGridSnapActive() ? 0.05 : 0.01
130
+ let value = start
131
+ const preview = beginFootprintHeightPreview(plate.id)
132
+ setNotice(null)
133
+ setDragValue(start)
134
+ runRoomHandleDrag({
135
+ label: FOOTPRINT_HEIGHT_DRAG_LABEL,
136
+ nodeId: plate.id,
137
+ levelId,
138
+ requires: [plate.id],
139
+ sample: (x, y) => {
140
+ const pointerY = localY(x, y)
141
+ if (pointerY === null) return null
142
+ const raw = start + pointerY - startY
143
+ return Math.max(min, Math.min(max, Math.round(raw / step) * step))
144
+ },
145
+ onValue: (next) => {
146
+ if (Math.abs(next - value) < 1e-6) return
147
+ // Live: the building follows, outside history until the release.
148
+ const refused = preview.preview(next)
149
+ setNotice(refused)
150
+ setHint(thickFloorAdvice(startNodes, plate, next) ? THICK_FLOOR_HINT : null)
151
+ if (!refused) value = next
152
+ setDragValue(next)
153
+ },
154
+ onCommit: () => {
155
+ setNotice(preview.commit(value))
156
+ setDragValue(null)
157
+ setHint(null)
158
+ },
159
+ onCancel: () => {
160
+ preview.cancel()
161
+ setDragValue(null)
162
+ setHint(null)
163
+ },
164
+ })
165
+ }
166
+
167
+ const shown = dragValue ?? null
168
+ return (
169
+ <group>
170
+ <ElevationArrows
171
+ anchor={[anchor[0], elevation, anchor[1]]}
172
+ hover={hover || shown !== null}
173
+ onHoverChange={setHover}
174
+ onPointerDown={onPointerDown}
175
+ zoom={zoom}
176
+ />
177
+ {(shown !== null || notice || hint) && (
178
+ <Html
179
+ center
180
+ position={[anchor[0], elevation + 0.75 * zoom, anchor[1]]}
181
+ zIndexRange={[25, 0]}
182
+ >
183
+ <div className="flex flex-col items-center gap-1" data-footprint-height-pill>
184
+ {shown !== null && (
185
+ <div className={PILL}>
186
+ <span className="font-medium text-foreground">
187
+ {upper ? 'Floor top' : 'Foundation'}
188
+ </span>
189
+ <span className="text-muted-foreground">
190
+ {formatLinearMeasurement(shown, unit, metricNotation)}
191
+ </span>
192
+ </div>
193
+ )}
194
+ {(notice ?? hint) && (
195
+ <div className={NOTICE} data-footprint-height-notice role="status">
196
+ {notice ?? hint}
197
+ </div>
198
+ )}
199
+ </div>
200
+ </Html>
201
+ )}
202
+ </group>
203
+ )
204
+ }
@@ -18,10 +18,13 @@ import { markPerfAction, useViewer } from '@pascal-app/viewer'
18
18
  import { Plane, Vector2, Vector3 } from 'three'
19
19
  import { GROUP_MOVE_DRAG_LABEL } from '../../lib/contextual-help'
20
20
  import { clientToPlan } from '../../lib/floorplan/plan-coords'
21
+ import { deleteSelectedSeparator, requestRoomDeletion } from '../../lib/room-structure-commands'
22
+ import { captureElementActionOrigin, completeElementAction } from '../../lib/room-zone-routing'
21
23
  import {
22
24
  copySelectedNodesToEditorClipboard,
23
25
  duplicateNodesToLevel,
24
26
  getEditorClipboardSnapshot,
27
+ type PasteRefusal,
25
28
  pasteSystemEditorClipboardToLevel,
26
29
  } from '../../lib/scene-clipboard'
27
30
  import { emitDeleteSFX, sfxEmitter } from '../../lib/sfx-bus'
@@ -33,6 +36,7 @@ import useEditor, {
33
36
  isGridSnapActive,
34
37
  isMagneticSnapActive,
35
38
  } from '../../store/use-editor'
39
+ import useFloorplanMode from '../../store/use-floorplan-mode'
36
40
  import useInteractionScope from '../../store/use-interaction-scope'
37
41
  import { useFloorplanGroupDrag } from '../editor-2d/floorplan-group-move'
38
42
  import {
@@ -456,12 +460,39 @@ function removeUnusedPasteMaterials(materialIds: SceneMaterialId[]) {
456
460
  }
457
461
  }
458
462
 
463
+ const REFUSED_PASTE_REASONS: Record<PasteRefusal, [one: string, many: string]> = {
464
+ 'no-access': [
465
+ 'you no longer have access to the project it came from.',
466
+ 'you no longer have access to the project they came from.',
467
+ ],
468
+ 'no-copies': [
469
+ 'this version is not available to copy from the source project.',
470
+ 'these versions are not available to copy from the source project.',
471
+ ],
472
+ failed: ["it couldn't be copied. Try again.", "they couldn't be copied. Try again."],
473
+ }
474
+
475
+ /** Why scripted objects were left out of a paste, or of a build loaded from a file. */
476
+ export function refusedObjectsNotice(
477
+ count: number,
478
+ refusal: PasteRefusal,
479
+ action: 'pasted' | 'loaded' = 'pasted',
480
+ ) {
481
+ const [one, many] = REFUSED_PASTE_REASONS[refusal]
482
+ return count === 1
483
+ ? `1 object wasn't ${action}: ${one}`
484
+ : `${count} objects weren't ${action}: ${many}`
485
+ }
486
+
487
+ let pastePending = false
488
+
459
489
  /**
460
490
  * Paste the Pascal scene payload from the browser clipboard onto the active
461
491
  * level, then carry the clones under the cursor until click-to-place. Escape
462
492
  * removes the uncommitted clones and any scene materials imported with them.
463
493
  */
464
494
  export async function pasteSelectionAndPickUp(targetLevelId?: AnyNodeId): Promise<boolean> {
495
+ if (pastePending) return false
465
496
  const activeScope = useInteractionScope.getState().scope
466
497
  if (activeScope.kind === 'placing' || activeScope.kind === 'moving') {
467
498
  emitter.emit('tool:cancel')
@@ -476,7 +507,18 @@ export async function pasteSelectionAndPickUp(targetLevelId?: AnyNodeId): Promis
476
507
  // above there is nothing to abandon.
477
508
  if (isBrushMode(useEditor.getState().mode)) useEditor.getState().setMode('select')
478
509
 
479
- const result = await pasteSystemEditorClipboardToLevel(targetLevelId)
510
+ pastePending = true
511
+ let result: Awaited<ReturnType<typeof pasteSystemEditorClipboardToLevel>>
512
+ try {
513
+ result = await pasteSystemEditorClipboardToLevel(targetLevelId)
514
+ } finally {
515
+ pastePending = false
516
+ }
517
+ if (result?.refusal) {
518
+ useFloorplanMode
519
+ .getState()
520
+ .showNotice(refusedObjectsNotice(result.refusedIds.length, result.refusal))
521
+ }
480
522
  if (!result || result.pastedIds.length === 0) return false
481
523
 
482
524
  const discardPaste = () => {
@@ -547,7 +589,16 @@ export function cutSelectionToEditorClipboard(): boolean {
547
589
  */
548
590
  export function deleteSelection(): boolean {
549
591
  const selectedIds = useViewer.getState().selection.selectedIds as AnyNodeId[]
550
- if (selectedIds.length === 0) return false
592
+ if (selectedIds.length === 0) {
593
+ const room = useEditor.getState().room
594
+ if (!room) return false
595
+ requestRoomDeletion(room.zoneId)
596
+ return true
597
+ }
598
+ // A drilled piece of a room (a wall, a separator, its ceiling) deleted: back to the room.
599
+ const origin = captureElementActionOrigin(selectedIds)
600
+ if (selectedIds.length === 1 && useScene.getState().nodes[selectedIds[0]!]?.type === 'separator')
601
+ return deleteSelectedSeparator(selectedIds[0]!, origin)
551
602
 
552
603
  const commitDelete = () => {
553
604
  const detail =
@@ -562,6 +613,7 @@ export function deleteSelection(): boolean {
562
613
  }
563
614
  useScene.getState().deleteNodes(selectedIds)
564
615
  useViewer.getState().setSelection({ selectedIds: [] })
616
+ completeElementAction(origin)
565
617
  }
566
618
 
567
619
  if (selectedIds.length >= BULK_DELETE_THRESHOLD) {
@@ -0,0 +1,63 @@
1
+ import { expect, test } from 'bun:test'
2
+ import { type RootState, useThree } from '@react-three/fiber'
3
+ import { create } from '@react-three/test-renderer'
4
+ import { useLayoutEffect } from 'react'
5
+ import { type Intersection, Object3D } from 'three'
6
+ import { EDITOR_HANDLE_HIT_AREA_USER_DATA_KEY } from '../../../lib/direct-manipulation'
7
+ import { EditorHandleHitPriority, prioritizeEditorHandleHits } from './handle-hit-priority'
8
+
9
+ function hit(name: string, distance: number, handle = false): Intersection {
10
+ const object = new Object3D()
11
+ object.name = name
12
+ if (handle) object.userData[EDITOR_HANDLE_HIT_AREA_USER_DATA_KEY] = true
13
+ return { object, distance, point: undefined as never }
14
+ }
15
+ const names = (hits: Intersection[]) => hits.map((entry) => entry.object.name)
16
+
17
+ test('a handle behind a wall comes first, so the hover goes where the press goes', () => {
18
+ const hits = [hit('wall', 3), hit('floor', 5), hit('up', 6, true), hit('cube', 6.2, true)]
19
+ expect(names(prioritizeEditorHandleHits(hits))).toEqual(['up', 'cube', 'wall', 'floor'])
20
+ })
21
+
22
+ test('without a handle on the ray the order is untouched', () => {
23
+ const hits = [hit('wall', 3), hit('floor', 5)]
24
+ expect(prioritizeEditorHandleHits(hits)).toBe(hits)
25
+ })
26
+
27
+ test('mounted, it is the canvas event filter; unmounted, the previous one comes back', async () => {
28
+ let state: RootState | null = null
29
+ const Probe = () => {
30
+ state = useThree((root) => root)
31
+ return null
32
+ }
33
+ const previous = (hits: Intersection[]) => hits.filter((entry) => entry.object.name !== 'grid')
34
+ const Previous = () => {
35
+ const setEvents = useThree((root) => root.setEvents)
36
+ useLayoutEffect(() => setEvents({ filter: previous }), [setEvents])
37
+ return null
38
+ }
39
+ const before = await create(
40
+ <>
41
+ <Probe />
42
+ <Previous />
43
+ </>,
44
+ )
45
+ await before.update(
46
+ <>
47
+ <Probe />
48
+ <Previous />
49
+ <EditorHandleHitPriority />
50
+ </>,
51
+ )
52
+ const hits = [hit('grid', 1), hit('wall', 3), hit('cube', 6, true)]
53
+ const filter = (state as RootState | null)?.events.filter
54
+ expect(names(filter!(hits, state!))).toEqual(['cube', 'wall'])
55
+ await before.update(
56
+ <>
57
+ <Probe />
58
+ <Previous />
59
+ </>,
60
+ )
61
+ expect((state as RootState | null)?.events.filter).toBe(previous)
62
+ await before.unmount()
63
+ })
@@ -0,0 +1,37 @@
1
+ 'use client'
2
+
3
+ import { useThree } from '@react-three/fiber'
4
+ import { useEffect } from 'react'
5
+ import type { Intersection, Object3D } from 'three'
6
+ import { EDITOR_HANDLE_HIT_AREA_USER_DATA_KEY } from '../../../lib/direct-manipulation'
7
+
8
+ const isHandleHit = (hit: { object: Object3D }) =>
9
+ hit.object.userData?.[EDITOR_HANDLE_HIT_AREA_USER_DATA_KEY] === true
10
+
11
+ /**
12
+ * Editor handles first, everything else after, each in distance order. Handles
13
+ * draw on top of walls and a handle anywhere along the ray already owns the
14
+ * press; ordering them first gives them the hover too. Distance order alone
15
+ * lets a wall in front win the hover for good: once its enter stops
16
+ * propagation, R3F keeps stopping every later move at it, so a handle behind
17
+ * it never hears its enter however the pointer reaches it.
18
+ */
19
+ export function prioritizeEditorHandleHits<T extends { object: Object3D }>(hits: T[]): T[] {
20
+ if (!hits.some(isHandleHit)) return hits
21
+ return [...hits.filter(isHandleHit), ...hits.filter((hit) => !isHandleHit(hit))]
22
+ }
23
+
24
+ /** Installs `prioritizeEditorHandleHits` as the canvas's pointer-event filter while mounted. */
25
+ export function EditorHandleHitPriority() {
26
+ const get = useThree((state) => state.get)
27
+ const setEvents = useThree((state) => state.setEvents)
28
+ useEffect(() => {
29
+ const previous = get().events.filter
30
+ setEvents({
31
+ filter: (hits: Intersection[], state) =>
32
+ prioritizeEditorHandleHits(previous ? previous(hits, state) : hits),
33
+ })
34
+ return () => setEvents({ filter: previous })
35
+ }, [get, setEvents])
36
+ return null
37
+ }
@@ -0,0 +1,28 @@
1
+ import { expect, spyOn, test } from 'bun:test'
2
+ import { swallowNextClick } from './use-handle-drag'
3
+
4
+ test('deferred click cleanup removes the listener from its original window', () => {
5
+ const originalWindow = Object.getOwnPropertyDescriptor(globalThis, 'window')
6
+ const target = new EventTarget()
7
+ const remove = spyOn(target, 'removeEventListener')
8
+ let cleanup: (() => void) | undefined
9
+ const timer = spyOn(globalThis, 'setTimeout').mockImplementation(((
10
+ callback: Parameters<typeof setTimeout>[0],
11
+ ) => {
12
+ cleanup = callback as () => void
13
+ return undefined as unknown as ReturnType<typeof setTimeout>
14
+ }) as typeof setTimeout)
15
+ try {
16
+ Object.defineProperty(globalThis, 'window', { configurable: true, value: target })
17
+ swallowNextClick()
18
+ Reflect.deleteProperty(globalThis, 'window')
19
+ expect(cleanup).toBeDefined()
20
+ expect(() => cleanup?.()).not.toThrow()
21
+ expect(remove).toHaveBeenCalledTimes(1)
22
+ } finally {
23
+ timer.mockRestore()
24
+ remove.mockRestore()
25
+ if (originalWindow) Object.defineProperty(globalThis, 'window', originalWindow)
26
+ else Reflect.deleteProperty(globalThis, 'window')
27
+ }
28
+ })