@pascal-app/editor 1.0.3 → 1.0.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (440) hide show
  1. package/package.json +11 -9
  2. package/src/components/editor/action-menu-button.tsx +83 -0
  3. package/src/components/editor/bake-thumbnail.tsx +315 -4
  4. package/src/components/editor/capture-level-picker.tsx +72 -0
  5. package/src/components/editor/custom-camera-controls.tsx +202 -57
  6. package/src/components/editor/delete-confirmation-dialog.tsx +297 -7
  7. package/src/components/editor/duplicate-node.ts +244 -0
  8. package/src/components/editor/editor-layout-mobile.tsx +36 -25
  9. package/src/components/editor/editor-layout-v2.tsx +131 -58
  10. package/src/components/editor/export-manager.tsx +34 -8
  11. package/src/components/editor/first-person/build-collider-world.test.ts +51 -0
  12. package/src/components/editor/first-person/build-collider-world.ts +6 -2
  13. package/src/components/editor/first-person/mechanism-targets.test.ts +75 -0
  14. package/src/components/editor/first-person/mechanism-targets.ts +36 -0
  15. package/src/components/editor/first-person-controls.tsx +205 -2
  16. package/src/components/editor/floating-action-menu.tsx +63 -207
  17. package/src/components/editor/floor-aware-camera-controls.test.ts +442 -0
  18. package/src/components/editor/floor-aware-camera-controls.ts +324 -0
  19. package/src/components/editor/floor-openings-overlay.tsx +243 -0
  20. package/src/components/editor/floor-region-controls.tsx +846 -0
  21. package/src/components/editor/floor-region-webgpu-materials.test.ts +12 -0
  22. package/src/components/editor/floor-surface.test.ts +165 -0
  23. package/src/components/editor/floor-surface.ts +182 -0
  24. package/src/components/editor/floorplan-background-selection.test.ts +11 -2
  25. package/src/components/editor/floorplan-background-selection.ts +1 -3
  26. package/src/components/editor/floorplan-panel.tsx +344 -342
  27. package/src/components/editor/footprint-height-handle.tsx +204 -0
  28. package/src/components/editor/group-actions.ts +54 -2
  29. package/src/components/editor/handles/handle-hit-priority.test.tsx +63 -0
  30. package/src/components/editor/handles/handle-hit-priority.tsx +37 -0
  31. package/src/components/editor/handles/swallow-next-click.test.ts +28 -0
  32. package/src/components/editor/handles/use-handle-drag.ts +269 -197
  33. package/src/components/editor/index.tsx +294 -169
  34. package/src/components/editor/join-walls-pill.tsx +44 -0
  35. package/src/components/editor/node-action-menu.tsx +68 -70
  36. package/src/components/editor/node-arrow-handles.tsx +25 -6
  37. package/src/components/editor/open-wall-ends-3d-layer.tsx +273 -0
  38. package/src/components/editor/open-wall-ends-hint.tsx +89 -0
  39. package/src/components/editor/quick-measurement-hud.tsx +3 -2
  40. package/src/components/editor/registry-action-contributions.test.tsx +63 -0
  41. package/src/components/editor/registry-action-contributions.tsx +80 -1
  42. package/src/components/editor/room-controls.tsx +847 -0
  43. package/src/components/editor/room-floor-highlight.tsx +128 -0
  44. package/src/components/editor/room-handles-2d.tsx +354 -0
  45. package/src/components/editor/room-handles.tsx +675 -0
  46. package/src/components/editor/room-highlight.tsx +225 -0
  47. package/src/components/editor/room-labels.test.ts +357 -0
  48. package/src/components/editor/room-labels.tsx +990 -0
  49. package/src/components/editor/room-transform-ghost.tsx +256 -0
  50. package/src/components/editor/room-webgpu-materials.test.ts +27 -0
  51. package/src/components/editor/selection-manager.tsx +451 -317
  52. package/src/components/editor/slab-hole-highlights.tsx +3 -0
  53. package/src/components/editor/snapshot-capture-overlay.tsx +4 -0
  54. package/src/components/editor/snapshot-capture.test.ts +52 -4
  55. package/src/components/editor/snapshot-capture.ts +30 -5
  56. package/src/components/editor/thumbnail-generator.test.ts +308 -0
  57. package/src/components/editor/thumbnail-generator.tsx +1035 -229
  58. package/src/components/editor/use-floorplan-background-placement.ts +31 -11
  59. package/src/components/editor/vector-edge-extractor.tsx +121 -0
  60. package/src/components/editor/views/host-view.tsx +61 -0
  61. package/src/components/editor/views/plugins-view.tsx +670 -0
  62. package/src/components/editor/views/scene-region.tsx +59 -0
  63. package/src/components/editor/views/use-editor-views.ts +57 -0
  64. package/src/components/editor/views/view-bar.tsx +220 -0
  65. package/src/components/editor/wall-justification.test.ts +140 -0
  66. package/src/components/editor/wall-move-side-handles.tsx +43 -142
  67. package/src/components/editor/wall-push-arrow.tsx +92 -0
  68. package/src/components/editor/wall-region-handles.tsx +509 -0
  69. package/src/components/editor/wall-snap-beacon-layer.tsx +3 -0
  70. package/src/components/editor-2d/floorplan-cursor-indicator-overlay.tsx +8 -4
  71. package/src/components/editor-2d/floorplan-open-wall-ends-layer.test.tsx +86 -0
  72. package/src/components/editor-2d/floorplan-open-wall-ends-layer.tsx +290 -0
  73. package/src/components/editor-2d/floorplan-registry-action-menu.tsx +11 -1
  74. package/src/components/editor-2d/floorplan-registry-move-overlay.tsx +113 -102
  75. package/src/components/editor-2d/renderers/floorplan-dimension-edit-overlay.test.ts +156 -0
  76. package/src/components/editor-2d/renderers/floorplan-dimension-edit-overlay.tsx +278 -0
  77. package/src/components/editor-2d/renderers/floorplan-dimension-renderer.tsx +77 -0
  78. package/src/components/editor-2d/renderers/{floorplan-draft-wall-measurement.tsx → floorplan-draft-measurement.tsx} +8 -8
  79. package/src/components/editor-2d/renderers/floorplan-geometry-renderer.tsx +4 -1
  80. package/src/components/editor-2d/renderers/floorplan-registry-layer.test.ts +38 -8
  81. package/src/components/editor-2d/renderers/floorplan-registry-layer.tsx +291 -227
  82. package/src/components/editor-2d/svg-paths.ts +12 -15
  83. package/src/components/procedural-items/inspector.tsx +40 -15
  84. package/src/components/systems/ceiling/ceiling-selection-affordance-system.test.ts +97 -14
  85. package/src/components/systems/ceiling/ceiling-selection-affordance-system.tsx +18 -18
  86. package/src/components/systems/zone/zone-system.test.ts +47 -0
  87. package/src/components/systems/zone/zone-system.tsx +29 -68
  88. package/src/components/tools/item/placement-history.test.tsx +474 -0
  89. package/src/components/tools/item/placement-strategies.ts +8 -4
  90. package/src/components/tools/item/use-draft-node.test.tsx +432 -1
  91. package/src/components/tools/item/use-draft-node.ts +166 -95
  92. package/src/components/tools/item/use-placement-coordinator.tsx +93 -46
  93. package/src/components/tools/paint-region/use-paint-tint.ts +37 -0
  94. package/src/components/tools/paint-region/wall-paint-region-tool.tsx +466 -0
  95. package/src/components/tools/registry/move-registry-node-tool.tsx +3 -1
  96. package/src/components/tools/select/box-select-tool.tsx +2 -12
  97. package/src/components/tools/select/plane-box-select-tool.test.tsx +79 -0
  98. package/src/components/tools/select/plane-box-select-tool.tsx +35 -22
  99. package/src/components/tools/select/select-candidates.test.ts +37 -4
  100. package/src/components/tools/select/select-candidates.ts +15 -11
  101. package/src/components/tools/shared/cursor-sphere.tsx +36 -12
  102. package/src/components/tools/site/site-boundary-editor.tsx +1 -2
  103. package/src/components/tools/stair/stair-defaults.ts +0 -3
  104. package/src/components/tools/stair/stair-tool.tsx +90 -42
  105. package/src/components/tools/tool-manager.tsx +46 -20
  106. package/src/components/tools/wall/wall-drafting.test.ts +19 -4
  107. package/src/components/tools/zone/zone-tool.tsx +4 -12
  108. package/src/components/ui/action-menu/action-button.tsx +3 -0
  109. package/src/components/ui/action-menu/camera-actions.tsx +2 -1
  110. package/src/components/ui/action-menu/control-modes.tsx +2 -35
  111. package/src/components/ui/action-menu/measurement-control.tsx +3 -10
  112. package/src/components/ui/build-panel/build-panel.tsx +235 -0
  113. package/src/components/ui/command-palette/editor-commands.tsx +4 -30
  114. package/src/components/ui/command-palette/index.tsx +5 -24
  115. package/src/components/ui/controls/finish-swatch.tsx +110 -0
  116. package/src/components/ui/controls/material-paint-panel.tsx +97 -26
  117. package/src/components/ui/controls/material-picker.tsx +4 -1
  118. package/src/components/ui/controls/scene-material-list.tsx +4 -32
  119. package/src/components/ui/controls/select-control.tsx +65 -0
  120. package/src/components/ui/floating-level-selector.tsx +9 -14
  121. package/src/components/ui/helpers/building-helper.tsx +4 -1
  122. package/src/components/ui/helpers/contextual-helper-panel.tsx +154 -70
  123. package/src/components/ui/helpers/helper-manager.test.tsx +69 -0
  124. package/src/components/ui/helpers/helper-manager.tsx +325 -14
  125. package/src/components/ui/helpers/item-helper.tsx +8 -0
  126. package/src/components/ui/helpers/registered-tool-helper.tsx +8 -1
  127. package/src/components/ui/icon-ref.tsx +45 -1
  128. package/src/components/ui/level-duplicate-dialog.tsx +1 -1
  129. package/src/components/ui/node-icon-image.tsx +16 -0
  130. package/src/components/ui/panels/collections/collections-popover.tsx +20 -14
  131. package/src/components/ui/panels/floor-foundation-panel.tsx +551 -0
  132. package/src/components/ui/panels/floor-opening-panel.tsx +143 -0
  133. package/src/components/ui/panels/mobile-panel-sheet.tsx +2 -2
  134. package/src/components/ui/panels/mobile-selection-bar.tsx +2 -2
  135. package/src/components/ui/panels/multi-field-value.test.ts +26 -3
  136. package/src/components/ui/panels/multi-field-value.ts +6 -3
  137. package/src/components/ui/panels/multi-parametric-inspector.tsx +52 -2
  138. package/src/components/ui/panels/multi-selection-panel.tsx +43 -4
  139. package/src/components/ui/panels/node-display.ts +8 -3
  140. package/src/components/ui/panels/panel-manager.tsx +103 -68
  141. package/src/components/ui/panels/panel-wrapper.tsx +78 -36
  142. package/src/components/ui/panels/parametric-field-control.tsx +1 -1
  143. package/src/components/ui/panels/parametric-inspector.tsx +4 -4
  144. package/src/components/ui/panels/reference-panel.tsx +2 -3
  145. package/src/components/ui/panels/room-construction-rows.tsx +774 -0
  146. package/src/components/ui/panels/room-name-options.test.ts +55 -0
  147. package/src/components/ui/panels/room-name-options.ts +47 -0
  148. package/src/components/ui/panels/room-openings-field.tsx +110 -0
  149. package/src/components/ui/panels/room-panel.tsx +725 -0
  150. package/src/components/ui/panels/shape-choice.tsx +58 -0
  151. package/src/components/ui/panels/wall-region-list.tsx +109 -0
  152. package/src/components/ui/primitives/color-dot.tsx +36 -24
  153. package/src/components/ui/primitives/sidebar.tsx +2 -0
  154. package/src/components/ui/right-stack.test.ts +29 -0
  155. package/src/components/ui/right-stack.tsx +145 -0
  156. package/src/components/ui/sidebar/icon-rail.tsx +5 -0
  157. package/src/components/ui/sidebar/panels/settings-panel/index.tsx +31 -4
  158. package/src/components/ui/sidebar/panels/settings-panel/keyboard-shortcuts-dialog.tsx +5 -5
  159. package/src/components/ui/sidebar/panels/site-panel/ceiling-tree-node.tsx +4 -4
  160. package/src/components/ui/sidebar/panels/site-panel/chimney-tree-node.tsx +1 -5
  161. package/src/components/ui/sidebar/panels/site-panel/collection-tree-node.tsx +197 -0
  162. package/src/components/ui/sidebar/panels/site-panel/column-tree-node.tsx +1 -5
  163. package/src/components/ui/sidebar/panels/site-panel/door-tree-node.tsx +1 -5
  164. package/src/components/ui/sidebar/panels/site-panel/dormer-tree-node.tsx +1 -5
  165. package/src/components/ui/sidebar/panels/site-panel/elevator-tree-node.tsx +1 -5
  166. package/src/components/ui/sidebar/panels/site-panel/fence-tree-node.tsx +24 -11
  167. package/src/components/ui/sidebar/panels/site-panel/gutter-tree-node.tsx +1 -5
  168. package/src/components/ui/sidebar/panels/site-panel/index.tsx +176 -211
  169. package/src/components/ui/sidebar/panels/site-panel/inline-rename-input.tsx +30 -8
  170. package/src/components/ui/sidebar/panels/site-panel/item-tree-node.tsx +1 -3
  171. package/src/components/ui/sidebar/panels/site-panel/level-tree-node.tsx +4 -1
  172. package/src/components/ui/sidebar/panels/site-panel/registry-tree-node.tsx +1 -3
  173. package/src/components/ui/sidebar/panels/site-panel/roof-tree-node.tsx +1 -4
  174. package/src/components/ui/sidebar/panels/site-panel/shelf-tree-node.tsx +1 -3
  175. package/src/components/ui/sidebar/panels/site-panel/slab-tree-node.tsx +21 -13
  176. package/src/components/ui/sidebar/panels/site-panel/solar-panel-tree-node.tsx +1 -5
  177. package/src/components/ui/sidebar/panels/site-panel/spawn-tree-node.tsx +1 -5
  178. package/src/components/ui/sidebar/panels/site-panel/stair-tree-node.tsx +1 -5
  179. package/src/components/ui/sidebar/panels/site-panel/tree-node.tsx +0 -27
  180. package/src/components/ui/sidebar/panels/site-panel/unit-tree-node.tsx +2 -3
  181. package/src/components/ui/sidebar/panels/site-panel/wall-tree-node.tsx +1 -5
  182. package/src/components/ui/sidebar/panels/site-panel/window-tree-node.tsx +1 -5
  183. package/src/components/ui/sidebar/panels/site-panel/zone-tree-node.tsx +9 -4
  184. package/src/components/ui/sidebar/panels/zone-panel/index.tsx +0 -2
  185. package/src/components/ui/sidebar/tab-bar.tsx +10 -0
  186. package/src/components/ui/sidebar/use-plugin-panels.tsx +15 -5
  187. package/src/components/viewer/floorplan-compass-button.tsx +9 -1
  188. package/src/components/viewer/floorplan-preview.tsx +5 -6
  189. package/src/components/viewer/viewer-hover-label.tsx +34 -0
  190. package/src/components/viewer/viewer-rooms.tsx +175 -0
  191. package/src/components/viewer/viewer-scene-header.tsx +181 -145
  192. package/src/components/viewer/viewer-selection-manager.tsx +124 -0
  193. package/src/components/viewer/viewer-stage.tsx +2 -2
  194. package/src/components/viewer-zone-system.tsx +3 -8
  195. package/src/hooks/use-auto-save.integration.test.tsx +123 -9
  196. package/src/hooks/use-auto-save.ts +34 -8
  197. package/src/hooks/use-keyboard.test.ts +178 -3
  198. package/src/hooks/use-keyboard.ts +89 -71
  199. package/src/hooks/use-open-wall-ends.test.ts +99 -0
  200. package/src/hooks/use-open-wall-ends.ts +238 -0
  201. package/src/hooks/use-placement-notice.test.ts +48 -0
  202. package/src/hooks/use-placement-notice.ts +42 -0
  203. package/src/hooks/use-selected-room.ts +143 -0
  204. package/src/index.tsx +246 -39
  205. package/src/lib/ceiling-edit-session.test.ts +321 -0
  206. package/src/lib/ceiling-edit-session.ts +206 -0
  207. package/src/lib/ceiling-region-session.test.ts +179 -0
  208. package/src/lib/continuation.test.ts +19 -1
  209. package/src/lib/continuation.ts +25 -4
  210. package/src/lib/door-interaction.ts +14 -0
  211. package/src/lib/editor-level-display.test.ts +96 -0
  212. package/src/lib/editor-level-display.ts +48 -0
  213. package/src/lib/editor-views.ts +144 -0
  214. package/src/lib/export-geometry-wait.test.ts +90 -0
  215. package/src/lib/floor-edit-session.ts +153 -0
  216. package/src/lib/floor-footprints.test.ts +563 -0
  217. package/src/lib/floor-footprints.ts +395 -0
  218. package/src/lib/floor-foundation-support.test.ts +283 -0
  219. package/src/lib/floor-opening-draft.ts +218 -0
  220. package/src/lib/floor-openings.test.ts +216 -0
  221. package/src/lib/floor-region-geometry.ts +191 -0
  222. package/src/lib/floor-region-session.test.ts +485 -0
  223. package/src/lib/floor-region-session.ts +589 -0
  224. package/src/lib/floor-region-snap.ts +101 -0
  225. package/src/lib/floor-step-hover.test.ts +32 -0
  226. package/src/lib/floorplan/annotation-visibility.ts +17 -0
  227. package/src/lib/floorplan/dimension-drive.test.ts +360 -0
  228. package/src/lib/floorplan/dimension-drive.ts +479 -0
  229. package/src/lib/floorplan/floorplan-export.test.ts +12 -8
  230. package/src/lib/floorplan/floorplan-export.tsx +32 -8
  231. package/src/lib/floorplan/floorplan-extension.ts +32 -0
  232. package/src/lib/floorplan/floorplan-mode.test.ts +4 -0
  233. package/src/lib/floorplan/floorplan-mode.ts +2 -0
  234. package/src/lib/floorplan/floorplan-pdfkit-document.ts +339 -18
  235. package/src/lib/floorplan/floorplan-pdfkit-renderer.test.ts +138 -8
  236. package/src/lib/floorplan/floorplan-pdfkit-renderer.ts +56 -25
  237. package/src/lib/floorplan/floorplan-plugin-install.test.tsx +138 -0
  238. package/src/lib/floorplan/floorplan-readonly.ts +6 -0
  239. package/src/lib/floorplan/geometry.ts +13 -6
  240. package/src/lib/floorplan/index.ts +0 -9
  241. package/src/lib/floorplan/items.ts +5 -2
  242. package/src/lib/floorplan/marks.test.ts +213 -0
  243. package/src/lib/floorplan/marks.ts +238 -0
  244. package/src/lib/floorplan/open-wall-ends.test.ts +268 -0
  245. package/src/lib/floorplan/open-wall-ends.ts +162 -0
  246. package/src/lib/floorplan/pdf-fonts/GeistMono-Bold.ttf +0 -0
  247. package/src/lib/floorplan/pdf-fonts/GeistMono-Regular.ttf +0 -0
  248. package/src/lib/floorplan/pdf-fonts/LICENSE-GeistMono.txt +92 -0
  249. package/src/lib/floorplan/pdf-fonts/LICENSE-LiberationSans.txt +102 -0
  250. package/src/lib/floorplan/pdf-fonts/LiberationSans-Bold.ttf +0 -0
  251. package/src/lib/floorplan/pdf-fonts/LiberationSans-Regular.ttf +0 -0
  252. package/src/lib/floorplan/schedules.test.ts +189 -0
  253. package/src/lib/floorplan/schedules.ts +339 -0
  254. package/src/lib/floorplan/sheet-export.test.ts +100 -0
  255. package/src/lib/floorplan/sheet-export.ts +318 -0
  256. package/src/lib/floorplan/site-plan/build-site-plan-drawing.ts +779 -0
  257. package/src/lib/floorplan/site-plan/contours.test.ts +43 -0
  258. package/src/lib/floorplan/site-plan/contributors.ts +97 -0
  259. package/src/lib/floorplan/site-plan/coverage.ts +232 -0
  260. package/src/lib/floorplan/site-plan/drawing-type-switch.test.ts +15 -0
  261. package/src/lib/floorplan/site-plan/drawing-type-switch.tsx +86 -0
  262. package/src/lib/floorplan/site-plan/front-edge.test.ts +103 -0
  263. package/src/lib/floorplan/site-plan/front-edge.ts +156 -0
  264. package/src/lib/floorplan/site-plan/geometry.test.ts +275 -0
  265. package/src/lib/floorplan/site-plan/geometry.ts +557 -0
  266. package/src/lib/floorplan/site-plan/index.ts +71 -0
  267. package/src/lib/floorplan/site-plan/site-annotations.test.ts +366 -0
  268. package/src/lib/floorplan/site-plan/site-annotations.ts +1083 -0
  269. package/src/lib/floorplan/site-plan/site-parts.ts +379 -0
  270. package/src/lib/floorplan/site-plan/site-plan-layer.tsx +138 -0
  271. package/src/lib/floorplan/site-plan/street-name.test.ts +69 -0
  272. package/src/lib/floorplan/site-plan/street-name.ts +302 -0
  273. package/src/lib/floorplan/site-plan/yards.test.ts +69 -0
  274. package/src/lib/floorplan/types.ts +1 -27
  275. package/src/lib/fresh-planar-placement.test.ts +36 -0
  276. package/src/lib/fresh-planar-placement.ts +59 -36
  277. package/src/lib/geometry-script/author.ts +152 -0
  278. package/src/lib/geometry-script/client.ts +152 -0
  279. package/src/lib/geometry-script/protocol.ts +12 -0
  280. package/src/lib/gesture-lifecycle.test.ts +595 -0
  281. package/src/lib/gesture-lifecycle.ts +290 -0
  282. package/src/lib/glb-export-zone-holes.test.ts +41 -0
  283. package/src/lib/glb-export.test.ts +485 -0
  284. package/src/lib/glb-export.ts +237 -119
  285. package/src/lib/history.test.ts +6 -0
  286. package/src/lib/history.ts +7 -0
  287. package/src/lib/hud-title.ts +109 -0
  288. package/src/lib/hydration-gated-storage.ts +49 -0
  289. package/src/lib/ifc-export.test.ts +174 -0
  290. package/src/lib/ifc-export.ts +201 -0
  291. package/src/lib/ifc-parts.ts +15 -0
  292. package/src/lib/inspector-expanded.test.ts +44 -0
  293. package/src/lib/inspector-expanded.ts +36 -0
  294. package/src/lib/interaction/scope.ts +18 -0
  295. package/src/lib/keyboard-pan.test.ts +129 -7
  296. package/src/lib/keyboard-pan.ts +161 -12
  297. package/src/lib/live-gesture-round-trip.test.ts +360 -0
  298. package/src/lib/lot/__fixtures__/map-dossier-miami-shores.json +1 -0
  299. package/src/lib/lot/__fixtures__/map-dossier-stpete.json +1 -0
  300. package/src/lib/lot/clean-ring.test.ts +186 -0
  301. package/src/lib/lot/clean-ring.ts +217 -0
  302. package/src/lib/lot/dossier.test.ts +202 -0
  303. package/src/lib/lot/dossier.ts +498 -0
  304. package/src/lib/lot/drop-in.test.ts +326 -0
  305. package/src/lib/lot/drop-in.ts +407 -0
  306. package/src/lib/lot/index.ts +43 -0
  307. package/src/lib/lot/lot-patch.test.ts +340 -0
  308. package/src/lib/lot/lot-patch.ts +394 -0
  309. package/src/lib/lot/parcel-provider.ts +43 -0
  310. package/src/lib/lot/terrain.test.ts +161 -0
  311. package/src/lib/lot/terrain.ts +266 -0
  312. package/src/lib/material-catalog-model.ts +3 -1
  313. package/src/lib/material-paint-panel-model.ts +9 -5
  314. package/src/lib/material-paint.ts +4 -0
  315. package/src/lib/mezzanine-actions.test.ts +342 -0
  316. package/src/lib/mezzanine-draft.test.ts +501 -0
  317. package/src/lib/mezzanine-draft.ts +278 -0
  318. package/src/lib/mezzanine-messages.ts +26 -0
  319. package/src/lib/mezzanine-selection.test.ts +207 -0
  320. package/src/lib/mezzanine-stairs.ts +46 -0
  321. package/src/lib/model-export.ts +3 -1
  322. package/src/lib/node-action-movement.ts +17 -6
  323. package/src/lib/node-display-label.ts +37 -0
  324. package/src/lib/orbit-floor-clearance.test.ts +165 -0
  325. package/src/lib/orbit-floor-clearance.ts +106 -0
  326. package/src/lib/paint-commit-route.test.ts +37 -0
  327. package/src/lib/paint-commit-route.ts +17 -0
  328. package/src/lib/paint-eyedropper.test.tsx +165 -0
  329. package/src/lib/paint-eyedropper.ts +99 -0
  330. package/src/lib/paint-pass-through.test.ts +111 -0
  331. package/src/lib/paint-pass-through.ts +76 -0
  332. package/src/lib/paint-region-hover.ts +25 -0
  333. package/src/lib/paint-region-mode.ts +154 -0
  334. package/src/lib/paint-regions.test.ts +166 -0
  335. package/src/lib/paint-regions.ts +192 -0
  336. package/src/lib/paint-scope.test.ts +317 -35
  337. package/src/lib/paint-scope.ts +266 -25
  338. package/src/lib/panel-rows.ts +21 -0
  339. package/src/lib/parametric-node-update.ts +1 -1
  340. package/src/lib/plate-paint-affected.test.ts +300 -0
  341. package/src/lib/plate-paint-affected.ts +177 -0
  342. package/src/lib/plugin-chat.ts +50 -0
  343. package/src/lib/plugin-panels.test.ts +25 -0
  344. package/src/lib/plugin-panels.ts +94 -0
  345. package/src/lib/print-shell-compiler.ts +1 -0
  346. package/src/lib/registered-draft-snap.ts +12 -0
  347. package/src/lib/roof-duplication.ts +10 -2
  348. package/src/lib/room-assembly-overlay.test.ts +275 -0
  349. package/src/lib/room-assembly-overlay.ts +266 -0
  350. package/src/lib/room-built-on.test.ts +379 -0
  351. package/src/lib/room-built-on.ts +138 -0
  352. package/src/lib/room-construction-commands.ts +712 -0
  353. package/src/lib/room-construction.test.ts +746 -0
  354. package/src/lib/room-construction.ts +385 -0
  355. package/src/lib/room-controls-phase6.test.ts +630 -0
  356. package/src/lib/room-divide-session.ts +523 -0
  357. package/src/lib/room-handle-drag.ts +1068 -0
  358. package/src/lib/room-name-catalog.test.ts +90 -0
  359. package/src/lib/room-name-catalog.ts +168 -0
  360. package/src/lib/room-push-dimensions.test.ts +311 -0
  361. package/src/lib/room-push-dimensions.ts +251 -0
  362. package/src/lib/room-selection-commands.ts +62 -0
  363. package/src/lib/room-selection.test.ts +973 -0
  364. package/src/lib/room-selection.ts +417 -0
  365. package/src/lib/room-structure-commands.ts +94 -0
  366. package/src/lib/room-structure.test.ts +921 -0
  367. package/src/lib/room-transform-session.ts +570 -0
  368. package/src/lib/room-zone-routing.test.ts +294 -0
  369. package/src/lib/room-zone-routing.ts +197 -0
  370. package/src/lib/scene-clipboard.test.ts +364 -0
  371. package/src/lib/scene-clipboard.ts +229 -48
  372. package/src/lib/scene-material-refs.test.ts +79 -0
  373. package/src/lib/scene-material-refs.ts +79 -0
  374. package/src/lib/scene.test.ts +24 -36
  375. package/src/lib/scene.ts +33 -57
  376. package/src/lib/selection-routing.test.ts +129 -45
  377. package/src/lib/selection-routing.ts +53 -74
  378. package/src/lib/session-writes.test.ts +120 -0
  379. package/src/lib/session-writes.ts +205 -0
  380. package/src/lib/sidebar-panel.ts +62 -0
  381. package/src/lib/site-boundary.test.ts +4 -6
  382. package/src/lib/site-boundary.ts +1 -1
  383. package/src/lib/snapping-mode.test.ts +4 -0
  384. package/src/lib/snapping-mode.ts +8 -1
  385. package/src/lib/terrace-draft.test.ts +145 -0
  386. package/src/lib/terrace-draft.ts +213 -0
  387. package/src/lib/tool-cursor-icon.test.ts +16 -0
  388. package/src/lib/tool-cursor-icon.ts +11 -0
  389. package/src/lib/typing-target.ts +9 -0
  390. package/src/lib/units.ts +77 -39
  391. package/src/lib/vector-edges.test.ts +58 -0
  392. package/src/lib/vector-edges.ts +400 -0
  393. package/src/lib/viewer-selection.test.ts +577 -0
  394. package/src/lib/viewer-selection.ts +569 -0
  395. package/src/lib/wall-draw-variant.test.ts +71 -0
  396. package/src/lib/wall-draw-variant.ts +91 -0
  397. package/src/lib/wall-paint-region-session.ts +246 -0
  398. package/src/lib/wall-paint-region.test.ts +373 -0
  399. package/src/lib/wall-polygon-draft.test.ts +262 -0
  400. package/src/lib/wall-polygon-draft.ts +286 -0
  401. package/src/lib/wall-push.test.ts +453 -0
  402. package/src/lib/wall-region-face.ts +352 -0
  403. package/src/lib/wall-region-handles.test.ts +304 -0
  404. package/src/lib/wall-region-handles.ts +368 -0
  405. package/src/lib/wall-region-list.test.ts +119 -0
  406. package/src/lib/wall-region-list.ts +136 -0
  407. package/src/lib/wall-region-snap.ts +110 -0
  408. package/src/lib/window-interaction.ts +14 -0
  409. package/src/lib/zone-content.ts +4 -1
  410. package/src/lib/zone-name.test.ts +17 -0
  411. package/src/lib/zone-name.ts +17 -0
  412. package/src/store/opens-in-select-mode.test.ts +223 -0
  413. package/src/store/paint-mode.test.ts +188 -0
  414. package/src/store/terrain-sculpt-mode.test.ts +19 -21
  415. package/src/store/tool-mode.test.ts +137 -6
  416. package/src/store/use-delete-confirmation.ts +28 -1
  417. package/src/store/use-drawing-view.test.ts +24 -3
  418. package/src/store/use-drawing-view.ts +29 -1
  419. package/src/store/use-editor.tsx +432 -321
  420. package/src/store/use-floorplan-draft-preview.ts +13 -0
  421. package/src/store/use-floorplan-marquee.ts +12 -2
  422. package/src/store/use-quick-measurement-hud.ts +3 -3
  423. package/src/store/use-stair-build-preview.ts +13 -5
  424. package/src/store/use-wall-move-ghosts.ts +4 -2
  425. package/src/store/use-wall-region-handles-selection.test.ts +222 -0
  426. package/src/store/use-wall-region-handles-selection.ts +139 -0
  427. package/src/store/view-layout.test.ts +175 -0
  428. package/src/store/view-layout.ts +196 -0
  429. package/src/test-utils/immediate-animation-frames.ts +16 -0
  430. package/src/test-utils/live-gesture-round-trip.ts +224 -0
  431. package/src/test-utils/selection-harness.ts +82 -0
  432. package/src/components/editor-2d/renderers/floorplan-stair-layer.tsx +0 -499
  433. package/src/components/systems/zone/zone-label-editor-system.tsx +0 -333
  434. package/src/components/tools/fence/fence-drafting.test.ts +0 -100
  435. package/src/components/tools/fence/fence-drafting.ts +0 -266
  436. package/src/components/ui/sidebar/panels/plugins-panel.tsx +0 -218
  437. package/src/components/viewer/viewer-units-panel.tsx +0 -116
  438. package/src/lib/floorplan/stairs.ts +0 -489
  439. package/src/lib/level-duplication.test.ts +0 -144
  440. package/src/lib/level-duplication.ts +0 -195
@@ -1,7 +1,6 @@
1
1
  'use client'
2
2
 
3
3
  import {
4
- type AnyNode,
5
4
  type AnyNodeId,
6
5
  type FenceNode,
7
6
  getFenceCenterlineFrameAt,
@@ -10,6 +9,7 @@ import {
10
9
  getWallCurveFrameAt,
11
10
  getWallCurveLength,
12
11
  getWallEffectiveHeightForNodes,
12
+ getWallLocalFaceZ,
13
13
  getWallThickness,
14
14
  isCurvedWall,
15
15
  MIN_WALL_HEIGHT,
@@ -48,6 +48,7 @@ import {
48
48
  } from '../../lib/elevation-guides'
49
49
  import { isHistoryShortcut } from '../../lib/history'
50
50
  import { endpointReshapeScope } from '../../lib/interaction/scope'
51
+ import { useRoomHandleDrag, wallPushHandles } from '../../lib/room-handle-drag'
51
52
  import { sfxEmitter } from '../../lib/sfx-bus'
52
53
  import { intersectSpatialDragPlane, spatialDragLocalY } from '../../lib/spatial-drag-plane'
53
54
  import { getSpatialPointerId, spatialPointerInput } from '../../lib/spatial-pointer-input'
@@ -69,6 +70,7 @@ import {
69
70
  NO_RAYCAST,
70
71
  useInvisibleHitAreaMaterial,
71
72
  } from './node-arrow-handles'
73
+ import { WallPushArrow } from './wall-push-arrow'
72
74
 
73
75
  const HANDLE_OFFSET = 0.27
74
76
  const HANDLE_MIN_OFFSET = 0.33
@@ -149,6 +151,7 @@ export function WallMoveSideHandles() {
149
151
  const movingNode = useMovingNode()
150
152
  const endpointReshape = useEndpointReshape()
151
153
  const isCurveReshape = useIsCurveReshape()
154
+ const isPushing = useRoomHandleDrag((state) => state.drag?.kind === 'push')
152
155
 
153
156
  const selectedId = selectedIds.length === 1 ? selectedIds[0] : null
154
157
  // Walls still use this legacy handle renderer. Fences retain their registry
@@ -164,9 +167,10 @@ export function WallMoveSideHandles() {
164
167
  mode !== 'delete' &&
165
168
  !movingNode &&
166
169
  !endpointReshape &&
167
- !isCurveReshape
170
+ !isCurveReshape &&
171
+ !isPushing
168
172
 
169
- if (!shouldRender || !selectedNode) return null
173
+ if (!(shouldRender && selectedNode)) return null
170
174
 
171
175
  return selectedNode.type === 'wall' ? (
172
176
  <WallMoveSideHandlesForWall wall={selectedNode} />
@@ -221,16 +225,17 @@ function WallMoveSideHandlesForWall({ wall }: { wall: WallNode }) {
221
225
  }, [wall.parentId])
222
226
 
223
227
  const baseElevation = getWallBaseElevationForNodes(effectiveWall, nodes)
224
- const handles = useMemo(() => getWallMoveHandles(effectiveWall, nodes), [effectiveWall, nodes])
228
+ const handles = useMemo(() => wallPushHandles(effectiveWall, nodes), [effectiveWall, nodes])
225
229
 
226
- if (!levelObject || handles.length === 0) return null
230
+ const levelId = wall.parentId
231
+ if (!(levelObject && levelId) || handles.length === 0) return null
227
232
 
228
233
  return createPortal(
229
234
  <>
235
+ {handles.map((handle) => (
236
+ <WallPushArrow handle={handle} key={handle.key} levelId={levelId} />
237
+ ))}
230
238
  <group position={[0, baseElevation, 0]}>
231
- {handles.map((handle) => (
232
- <WallMoveArrowHandle handle={handle} key={handle.key} wall={effectiveWall} />
233
- ))}
234
239
  <WallHeightArrowHandle wall={effectiveWall} />
235
240
  <StructureThicknessHandle
236
241
  baseElevation={baseElevation}
@@ -294,7 +299,7 @@ function StructureThicknessHandle({
294
299
  ? (node.height ?? 1.8)
295
300
  : getWallEffectiveHeightForNodes(node, useScene.getState().nodes)
296
301
  const outward = new Vector2(frame.normal.x * side, frame.normal.y * side)
297
- const faceOffset = thickness / 2 + 0.006
302
+ const faceOffset = getWallLocalFaceZ({ ...node, thickness }, side > 0 ? 'a' : 'b') * side + 0.006
298
303
  const position: [number, number, number] = [
299
304
  frame.point.x + outward.x * faceOffset,
300
305
  structureHeight / 2,
@@ -371,11 +376,18 @@ function StructureThicknessHandle({
371
376
  initialNode.type === 'fence'
372
377
  ? (initialNode.thickness ?? 0.08)
373
378
  : getWallThickness(initialNode)
379
+ const initialFaceOffset =
380
+ getWallLocalFaceZ(
381
+ { ...initialNode, thickness: initialStructureThickness },
382
+ side > 0 ? 'a' : 'b',
383
+ ) *
384
+ side +
385
+ 0.006
374
386
  const minimumThickness = initialNode.type === 'fence' ? 0.03 : MIN_WALL_THICKNESS
375
387
  const localOrigin = new Vector3(
376
- initialFrame.point.x + localAxis.x * (initialStructureThickness / 2 + 0.006),
388
+ initialFrame.point.x + localAxis.x * initialFaceOffset,
377
389
  baseElevation + initialStructureHeight / 2,
378
- initialFrame.point.y + localAxis.z * (initialStructureThickness / 2 + 0.006),
390
+ initialFrame.point.y + localAxis.z * initialFaceOffset,
379
391
  )
380
392
  const worldOrigin = localOrigin.clone().applyMatrix4(rideObject.matrixWorld)
381
393
  const worldAxisEnd = localOrigin.clone().add(localAxis).applyMatrix4(rideObject.matrixWorld)
@@ -641,13 +653,14 @@ function WallCornerLeaderHandle({ wall, endpoint }: { wall: WallNode; endpoint:
641
653
  }
642
654
  })
643
655
 
644
- useEffect(() => {
645
- return () => {
656
+ useEffect(
657
+ () => () => {
646
658
  if (document.body.style.cursor === 'grab' || document.body.style.cursor === 'grabbing') {
647
659
  document.body.style.cursor = ''
648
660
  }
649
- }
650
- }, [])
661
+ },
662
+ [],
663
+ )
651
664
 
652
665
  const activateEndpointMove = (event: ThreeEvent<PointerEvent>) => {
653
666
  if (event.button !== 0) return
@@ -877,14 +890,15 @@ function WallHeightArrowHandle({ wall }: { wall: WallNode }) {
877
890
  arrowMaterial.color.set(isHovered ? ARROW_HOVER_COLOR : ARROW_COLOR)
878
891
  }, [arrowMaterial, isHovered])
879
892
 
880
- useEffect(() => {
881
- return () => {
893
+ useEffect(
894
+ () => () => {
882
895
  if (document.body.style.cursor === 'ns-resize') {
883
896
  document.body.style.cursor = ''
884
897
  }
885
898
  dragCleanupRef.current?.()
886
- }
887
- }, [])
899
+ },
900
+ [],
901
+ )
888
902
 
889
903
  useEffect(() => () => arrowGeometry.dispose(), [arrowGeometry])
890
904
  useEffect(() => () => hitGeometry.dispose(), [hitGeometry])
@@ -893,7 +907,7 @@ function WallHeightArrowHandle({ wall }: { wall: WallNode }) {
893
907
  // Sit on the visual centre of the wall — for curved walls that's the
894
908
  // arc apex at t=0.5, not the chord midpoint. Use the curve tangent for
895
909
  // the yaw so the arrow's local frame matches the wall direction at the
896
- // apex, consistent with `getWallMoveHandles`.
910
+ // apex, consistent with `wallPushHandles`.
897
911
  const curveFrame = isCurvedWall(wall) ? getWallCurveFrameAt(wall, 0.5) : null
898
912
  const midX = curveFrame ? curveFrame.point.x : (wall.start[0] + wall.end[0]) / 2
899
913
  const midZ = curveFrame ? curveFrame.point.y : (wall.start[1] + wall.end[1]) / 2
@@ -1085,7 +1099,10 @@ function WallHeightArrowHandle({ wall }: { wall: WallNode }) {
1085
1099
  scale={baseScale}
1086
1100
  />
1087
1101
  <mesh
1088
- // Geometry-as-prop + frustumCulled={false} — see WallMoveArrowHandle.
1102
+ // Pass geometry as a prop (not `<primitive attach="geometry">`): with
1103
+ // `frustumCulled={false}` the attach path draws R3F's empty default
1104
+ // geometry on the first frame and WebGPU flags "Vertex buffer slot 0
1105
+ // ... was not set".
1089
1106
  frustumCulled={false}
1090
1107
  geometry={arrowGeometry}
1091
1108
  material={arrowMaterial}
@@ -1098,7 +1115,7 @@ function WallHeightArrowHandle({ wall }: { wall: WallNode }) {
1098
1115
  )
1099
1116
  }
1100
1117
 
1101
- function WallMoveArrowHandle({ wall, handle }: { wall: WallNode; handle: WallMoveHandle }) {
1118
+ function FenceMoveArrowHandle({ fence, handle }: { fence: FenceNode; handle: WallMoveHandle }) {
1102
1119
  const [isHovered, setIsHovered] = useState(false)
1103
1120
  const arrowGeometry = useMemo(() => createArrowHandleGeometry(), [])
1104
1121
  const hitGeometry = useMemo(() => createArrowHitAreaGeometry(), [])
@@ -1118,7 +1135,6 @@ function WallMoveArrowHandle({ wall, handle }: { wall: WallNode; handle: WallMov
1118
1135
  const { camera } = useThree()
1119
1136
 
1120
1137
  const zoom = camera instanceof OrthographicCamera ? 1 / camera.zoom : 1
1121
-
1122
1138
  const baseScale = zoom * ARROW_SCALE
1123
1139
  const scale = (isHovered ? 1.12 : 1) * baseScale
1124
1140
 
@@ -1126,103 +1142,14 @@ function WallMoveArrowHandle({ wall, handle }: { wall: WallNode; handle: WallMov
1126
1142
  arrowMaterial.color.set(isHovered ? ARROW_HOVER_COLOR : ARROW_COLOR)
1127
1143
  }, [arrowMaterial, isHovered])
1128
1144
 
1129
- useEffect(() => {
1130
- return () => {
1145
+ useEffect(
1146
+ () => () => {
1131
1147
  if (document.body.style.cursor === 'grab' || document.body.style.cursor === 'grabbing') {
1132
1148
  document.body.style.cursor = ''
1133
1149
  }
1134
- }
1135
- }, [])
1136
-
1137
- useEffect(() => () => arrowGeometry.dispose(), [arrowGeometry])
1138
- useEffect(() => () => hitGeometry.dispose(), [hitGeometry])
1139
- useEffect(() => () => arrowMaterial.dispose(), [arrowMaterial])
1140
-
1141
- const activateWallMove = (event: ThreeEvent<PointerEvent>) => {
1142
- if (event.button !== 0) return
1143
- event.stopPropagation()
1144
- suppressBoxSelectForPointer(event)
1145
- document.body.style.cursor = 'grabbing'
1146
-
1147
- sfxEmitter.emit('sfx:item-pick')
1148
- useEditor.getState().setMovingNode(wall)
1149
- useInteractionScope.getState().endIf((s) => s.kind === 'reshaping' && s.reshape === 'endpoint')
1150
- useInteractionScope.getState().endIf((s) => s.kind === 'reshaping' && s.reshape === 'curve')
1151
- // Keep the wall selected so it stays the active item once the move
1152
- // commits; the `!movingNode` guard on the handles hides them mid-drag.
1153
- }
1154
-
1155
- return (
1156
- <group position={handle.position} rotation={[0, handle.rotationY, 0]}>
1157
- <InvisibleHandleHitArea
1158
- geometry={hitGeometry}
1159
- material={hitMaterial}
1160
- onPointerDown={activateWallMove}
1161
- onPointerEnter={(event) => {
1162
- event.stopPropagation()
1163
- setIsHovered(true)
1164
- document.body.style.cursor = 'grab'
1165
- }}
1166
- onPointerLeave={(event) => {
1167
- event.stopPropagation()
1168
- setIsHovered(false)
1169
- if (document.body.style.cursor === 'grab') {
1170
- document.body.style.cursor = ''
1171
- }
1172
- }}
1173
- scale={baseScale}
1174
- />
1175
- <mesh
1176
- // Pass geometry as a prop (not `<primitive attach="geometry">`)
1177
- // so the mesh is never rendered with R3F's default empty
1178
- // `BufferGeometry`. Combined with `frustumCulled={false}`, the
1179
- // primitive-attach path emits a `Draw(0, 1, 0, 0)` on the first
1180
- // frame and WebGPU flags "Vertex buffer slot 0 ... was not set".
1181
- frustumCulled={false}
1182
- geometry={arrowGeometry}
1183
- material={arrowMaterial}
1184
- raycast={NO_RAYCAST}
1185
- renderOrder={1002}
1186
- scale={scale}
1187
- />
1188
- </group>
1189
- )
1190
- }
1191
-
1192
- function FenceMoveArrowHandle({ fence, handle }: { fence: FenceNode; handle: WallMoveHandle }) {
1193
- const [isHovered, setIsHovered] = useState(false)
1194
- const arrowGeometry = useMemo(() => createArrowHandleGeometry(), [])
1195
- const hitGeometry = useMemo(() => createArrowHitAreaGeometry(), [])
1196
- const hitMaterial = useInvisibleHitAreaMaterial()
1197
- const arrowMaterial = useMemo(
1198
- () =>
1199
- new MeshBasicNodeMaterial({
1200
- color: new Color(ARROW_COLOR),
1201
- side: DoubleSide,
1202
- depthTest: false,
1203
- depthWrite: false,
1204
- transparent: true,
1205
- opacity: 1,
1206
- }),
1150
+ },
1207
1151
  [],
1208
1152
  )
1209
- const { camera } = useThree()
1210
-
1211
- const zoom = camera instanceof OrthographicCamera ? 1 / camera.zoom : 1
1212
- const baseScale = zoom * ARROW_SCALE
1213
- const scale = (isHovered ? 1.12 : 1) * baseScale
1214
-
1215
- useEffect(() => {
1216
- arrowMaterial.color.set(isHovered ? ARROW_HOVER_COLOR : ARROW_COLOR)
1217
- }, [arrowMaterial, isHovered])
1218
-
1219
- useEffect(() => {
1220
- return () => {
1221
- if (document.body.style.cursor === 'grab' || document.body.style.cursor === 'grabbing') {
1222
- document.body.style.cursor = ''
1223
- }
1224
- }
1225
- }, [])
1226
1153
 
1227
1154
  useEffect(() => () => arrowGeometry.dispose(), [arrowGeometry])
1228
1155
  useEffect(() => () => hitGeometry.dispose(), [hitGeometry])
@@ -1262,7 +1189,7 @@ function FenceMoveArrowHandle({ fence, handle }: { fence: FenceNode; handle: Wal
1262
1189
  scale={baseScale}
1263
1190
  />
1264
1191
  <mesh
1265
- // Pass geometry as a prop — see WallMoveArrowHandle for the
1192
+ // Pass geometry as a prop — see WallHeightArrowHandle for the
1266
1193
  // WebGPU "Vertex buffer slot 0 ... was not set" rationale.
1267
1194
  frustumCulled={false}
1268
1195
  geometry={arrowGeometry}
@@ -1275,32 +1202,6 @@ function FenceMoveArrowHandle({ fence, handle }: { fence: FenceNode; handle: Wal
1275
1202
  )
1276
1203
  }
1277
1204
 
1278
- function getWallMoveHandles(wall: WallNode, nodes: Record<string, AnyNode>): WallMoveHandle[] {
1279
- const dx = wall.end[0] - wall.start[0]
1280
- const dz = wall.end[1] - wall.start[1]
1281
- const length = Math.hypot(dx, dz)
1282
-
1283
- if (length < 1e-6) {
1284
- return []
1285
- }
1286
-
1287
- const frame = isCurvedWall(wall) ? getWallCurveFrameAt(wall, 0.5) : null
1288
- const normal: [number, number] = frame
1289
- ? [frame.normal.x, frame.normal.y]
1290
- : [-dz / length, dx / length]
1291
- const midpoint: [number, number] = frame
1292
- ? [frame.point.x, frame.point.y]
1293
- : [(wall.start[0] + wall.end[0]) / 2, (wall.start[1] + wall.end[1]) / 2]
1294
- const wallHeight = getWallEffectiveHeightForNodes(wall, nodes)
1295
- const handleHeight = Math.max(wallHeight - HANDLE_TOP_INSET, HANDLE_MIN_HEIGHT)
1296
- const offset = Math.max(getWallThickness(wall) / 2 + HANDLE_OFFSET, HANDLE_MIN_OFFSET)
1297
-
1298
- return [
1299
- buildWallMoveHandle('front', midpoint, normal, offset, handleHeight),
1300
- buildWallMoveHandle('back', midpoint, [-normal[0], -normal[1]], offset, handleHeight),
1301
- ]
1302
- }
1303
-
1304
1205
  function WallMoveSideHandlesForFence({ fence }: { fence: FenceNode }) {
1305
1206
  const [levelObject, setLevelObject] = useState<Object3D | null>(() =>
1306
1207
  fence.parentId ? (sceneRegistry.nodes.get(fence.parentId) ?? null) : null,
@@ -0,0 +1,92 @@
1
+ 'use client'
2
+
3
+ import { sceneRegistry } from '@pascal-app/core'
4
+ import { type ThreeEvent, useThree } from '@react-three/fiber'
5
+ import { useState } from 'react'
6
+ import { OrthographicCamera, Plane, type Ray, Vector2, Vector3 } from 'three'
7
+ import { runWallPushDrag, type WallPushHandle } from '../../lib/room-handle-drag'
8
+ import { getSpatialPointerId } from '../../lib/spatial-pointer-input'
9
+ import { suppressBoxSelectForPointer } from '../tools/select/box-select-state'
10
+ import { ARROW_SCALE, HandleArrow } from './handles/handle-arrow'
11
+
12
+ /**
13
+ * The push/pull arrow of a wall stretch — a selected wall's side arrows and a
14
+ * selected room's boundary arrows are this one component, driven by the one
15
+ * `runWallPushDrag`. Rendered in the level frame. `zoneId` binds a room's
16
+ * arrow to its room.
17
+ */
18
+ export function WallPushArrow({
19
+ handle,
20
+ levelId,
21
+ zoneId,
22
+ }: {
23
+ handle: WallPushHandle
24
+ levelId: string
25
+ zoneId?: string
26
+ }) {
27
+ const [hover, setHover] = useState(false)
28
+ const { camera, raycaster, gl } = useThree()
29
+ const zoom = camera instanceof OrthographicCamera ? 1 / camera.zoom : 1
30
+ const onPointerDown = (event: ThreeEvent<PointerEvent>) => {
31
+ if (event.button !== 0) return
32
+ event.stopPropagation()
33
+ suppressBoxSelectForPointer(event)
34
+ const level = sceneRegistry.nodes.get(levelId)
35
+ if (!level) return
36
+ level.updateWorldMatrix(true, false)
37
+ const toLocal = level.matrixWorld.clone().invert()
38
+ const origin = new Vector3(handle.position[0], handle.height, handle.position[1]).applyMatrix4(
39
+ level.matrixWorld,
40
+ )
41
+ const up = new Vector3(0, 1, 0).transformDirection(level.matrixWorld)
42
+ const plane = new Plane().setFromNormalAndCoplanarPoint(up, origin)
43
+ const along = (ray: Ray) => {
44
+ const hit = ray.intersectPlane(plane, new Vector3())
45
+ if (!hit) return null
46
+ const local = hit.applyMatrix4(toLocal)
47
+ return (
48
+ (local.x - handle.position[0]) * handle.outward[0] +
49
+ (local.z - handle.position[1]) * handle.outward[1]
50
+ )
51
+ }
52
+ const rayAt = (clientX: number, clientY: number) => {
53
+ const rect = gl.domElement.getBoundingClientRect()
54
+ raycaster.setFromCamera(
55
+ new Vector2(
56
+ ((clientX - rect.left) / rect.width) * 2 - 1,
57
+ -((clientY - rect.top) / rect.height) * 2 + 1,
58
+ ),
59
+ camera,
60
+ )
61
+ return raycaster.ray
62
+ }
63
+ const spatialPointerId = getSpatialPointerId(event.nativeEvent)
64
+ const from = along(
65
+ spatialPointerId ? event.ray : rayAt(event.nativeEvent.clientX, event.nativeEvent.clientY),
66
+ )
67
+ if (from === null) return
68
+ runWallPushDrag({
69
+ handle,
70
+ levelId,
71
+ zoneId,
72
+ from,
73
+ along: (clientX, clientY) => along(rayAt(clientX, clientY)),
74
+ spatial: spatialPointerId ? { pointerId: spatialPointerId, along } : undefined,
75
+ })
76
+ }
77
+ return (
78
+ <HandleArrow
79
+ cursor="grab"
80
+ hover={hover}
81
+ onHoverChange={setHover}
82
+ onPointerDown={onPointerDown}
83
+ placement={{
84
+ position: [handle.position[0], handle.height, handle.position[1]],
85
+ rotation: [0, Math.atan2(-handle.outward[1], handle.outward[0]), 0],
86
+ baseScale: zoom * ARROW_SCALE,
87
+ }}
88
+ shape="chevron"
89
+ thin
90
+ />
91
+ )
92
+ }