@pascal-app/editor 1.0.1 → 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 (498) hide show
  1. package/package.json +16 -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 +221 -128
  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 +37 -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 +536 -487
  27. package/src/components/editor/footprint-height-handle.tsx +204 -0
  28. package/src/components/editor/grid.tsx +31 -7
  29. package/src/components/editor/group-actions.ts +59 -11
  30. package/src/components/editor/group-floating-action-menu.tsx +19 -9
  31. package/src/components/editor/group-move-3d.ts +4 -6
  32. package/src/components/editor/group-rotate-handle.tsx +81 -35
  33. package/src/components/editor/group-selection-box-3d.tsx +25 -17
  34. package/src/components/editor/group-transform-shared.test.ts +126 -1
  35. package/src/components/editor/group-transform-shared.ts +79 -73
  36. package/src/components/editor/handles/handle-arrow.tsx +72 -2
  37. package/src/components/editor/handles/handle-hit-priority.test.tsx +63 -0
  38. package/src/components/editor/handles/handle-hit-priority.tsx +37 -0
  39. package/src/components/editor/handles/swallow-next-click.test.ts +28 -0
  40. package/src/components/editor/handles/use-handle-drag.ts +272 -131
  41. package/src/components/editor/index.tsx +316 -176
  42. package/src/components/editor/join-walls-pill.tsx +44 -0
  43. package/src/components/editor/node-action-menu.tsx +70 -70
  44. package/src/components/editor/node-arrow-handles.tsx +167 -14
  45. package/src/components/editor/open-wall-ends-3d-layer.tsx +273 -0
  46. package/src/components/editor/open-wall-ends-hint.tsx +89 -0
  47. package/src/components/editor/quick-measurement-hud.tsx +3 -2
  48. package/src/components/editor/registry-action-contributions.test.tsx +63 -0
  49. package/src/components/editor/registry-action-contributions.tsx +132 -0
  50. package/src/components/editor/room-controls.tsx +847 -0
  51. package/src/components/editor/room-floor-highlight.tsx +128 -0
  52. package/src/components/editor/room-handles-2d.tsx +354 -0
  53. package/src/components/editor/room-handles.tsx +675 -0
  54. package/src/components/editor/room-highlight.tsx +225 -0
  55. package/src/components/editor/room-labels.test.ts +357 -0
  56. package/src/components/editor/room-labels.tsx +990 -0
  57. package/src/components/editor/room-transform-ghost.tsx +256 -0
  58. package/src/components/editor/room-webgpu-materials.test.ts +27 -0
  59. package/src/components/editor/selection-manager.tsx +451 -317
  60. package/src/components/editor/slab-hole-highlights.tsx +3 -0
  61. package/src/components/editor/snapshot-capture-overlay.tsx +4 -0
  62. package/src/components/editor/snapshot-capture.test.ts +52 -4
  63. package/src/components/editor/snapshot-capture.ts +30 -5
  64. package/src/components/editor/thumbnail-generator.test.ts +308 -0
  65. package/src/components/editor/thumbnail-generator.tsx +1035 -229
  66. package/src/components/editor/use-floorplan-background-placement.ts +31 -11
  67. package/src/components/editor/vector-edge-extractor.tsx +121 -0
  68. package/src/components/editor/views/host-view.tsx +61 -0
  69. package/src/components/editor/views/plugins-view.tsx +670 -0
  70. package/src/components/editor/views/scene-region.tsx +59 -0
  71. package/src/components/editor/views/use-editor-views.ts +57 -0
  72. package/src/components/editor/views/view-bar.tsx +220 -0
  73. package/src/components/editor/wall-justification.test.ts +140 -0
  74. package/src/components/editor/wall-move-side-handles.tsx +114 -163
  75. package/src/components/editor/wall-opening-highlights.tsx +2 -3
  76. package/src/components/editor/wall-push-arrow.tsx +92 -0
  77. package/src/components/editor/wall-region-handles.tsx +509 -0
  78. package/src/components/editor/wall-snap-beacon-layer.tsx +3 -0
  79. package/src/components/editor-2d/floorplan-cursor-indicator-overlay.tsx +8 -4
  80. package/src/components/editor-2d/floorplan-group-move.tsx +17 -24
  81. package/src/components/editor-2d/floorplan-open-wall-ends-layer.test.tsx +86 -0
  82. package/src/components/editor-2d/floorplan-open-wall-ends-layer.tsx +290 -0
  83. package/src/components/editor-2d/floorplan-registered-tool-layer.tsx +33 -12
  84. package/src/components/editor-2d/floorplan-registry-action-menu.tsx +11 -1
  85. package/src/components/editor-2d/floorplan-registry-move-overlay.tsx +242 -126
  86. package/src/components/editor-2d/renderers/floorplan-dimension-edit-overlay.test.ts +156 -0
  87. package/src/components/editor-2d/renderers/floorplan-dimension-edit-overlay.tsx +278 -0
  88. package/src/components/editor-2d/renderers/floorplan-dimension-renderer.tsx +77 -0
  89. package/src/components/editor-2d/renderers/floorplan-draft-measurement.tsx +166 -0
  90. package/src/components/editor-2d/renderers/floorplan-geometry-renderer.tsx +4 -1
  91. package/src/components/editor-2d/renderers/floorplan-registry-layer.test.ts +87 -8
  92. package/src/components/editor-2d/renderers/floorplan-registry-layer.tsx +355 -245
  93. package/src/components/editor-2d/svg-paths.ts +12 -15
  94. package/src/components/procedural-items/inspector.tsx +40 -15
  95. package/src/components/systems/ceiling/ceiling-selection-affordance-system.test.ts +97 -14
  96. package/src/components/systems/ceiling/ceiling-selection-affordance-system.tsx +18 -18
  97. package/src/components/systems/zone/zone-system.test.ts +47 -0
  98. package/src/components/systems/zone/zone-system.tsx +31 -66
  99. package/src/components/tools/__fixtures__/hosting.ts +10 -0
  100. package/src/components/tools/item/item-surface-placement.test.ts +3 -9
  101. package/src/components/tools/item/item-surface-strategy-resolver.test.ts +3 -9
  102. package/src/components/tools/item/placement-history.test.tsx +474 -0
  103. package/src/components/tools/item/placement-strategies.ts +63 -47
  104. package/src/components/tools/item/shelf-surface-strategy-resolver.test.ts +3 -3
  105. package/src/components/tools/item/use-draft-node.test.tsx +432 -1
  106. package/src/components/tools/item/use-draft-node.ts +207 -97
  107. package/src/components/tools/item/use-placement-coordinator.tsx +251 -111
  108. package/src/components/tools/paint-region/use-paint-tint.ts +37 -0
  109. package/src/components/tools/paint-region/wall-paint-region-tool.tsx +466 -0
  110. package/src/components/tools/registry/item-surface-move.test.ts +10 -2
  111. package/src/components/tools/registry/item-surface-move.ts +46 -24
  112. package/src/components/tools/registry/item-surface-resolver.test.ts +2 -2
  113. package/src/components/tools/registry/move-registry-node-tool.tsx +26 -10
  114. package/src/components/tools/select/box-select-tool.tsx +2 -12
  115. package/src/components/tools/select/plane-box-select-tool.test.tsx +79 -0
  116. package/src/components/tools/select/plane-box-select-tool.tsx +35 -22
  117. package/src/components/tools/select/select-candidates.test.ts +37 -4
  118. package/src/components/tools/select/select-candidates.ts +15 -11
  119. package/src/components/tools/shared/affordance-dispatch.test.ts +24 -0
  120. package/src/components/tools/shared/affordance-dispatch.ts +13 -0
  121. package/src/components/tools/shared/cursor-sphere.tsx +36 -12
  122. package/src/components/tools/shared/draft-measurement-label.tsx +38 -0
  123. package/src/components/tools/shared/pointer-support-cap.test.ts +27 -0
  124. package/src/components/tools/shared/pointer-support-cap.ts +17 -5
  125. package/src/components/tools/shared/surface-rejection.test.tsx +5 -4
  126. package/src/components/tools/shared/surface-rejection.tsx +14 -0
  127. package/src/components/tools/site/site-boundary-editor.tsx +1 -2
  128. package/src/components/tools/stair/stair-defaults.ts +0 -3
  129. package/src/components/tools/stair/stair-tool.tsx +90 -42
  130. package/src/components/tools/tool-manager.tsx +82 -23
  131. package/src/components/tools/wall/wall-drafting.test.ts +19 -4
  132. package/src/components/tools/zone/zone-tool.tsx +4 -12
  133. package/src/components/ui/action-menu/action-button.tsx +3 -0
  134. package/src/components/ui/action-menu/camera-actions.tsx +2 -1
  135. package/src/components/ui/action-menu/control-modes.tsx +2 -35
  136. package/src/components/ui/action-menu/measurement-control.tsx +3 -10
  137. package/src/components/ui/build-panel/build-panel.tsx +235 -0
  138. package/src/components/ui/command-palette/editor-commands.tsx +4 -30
  139. package/src/components/ui/command-palette/index.tsx +5 -24
  140. package/src/components/ui/controls/finish-swatch.tsx +110 -0
  141. package/src/components/ui/controls/material-paint-panel.tsx +101 -91
  142. package/src/components/ui/controls/material-picker.tsx +8 -67
  143. package/src/components/ui/controls/scene-material-list.tsx +4 -32
  144. package/src/components/ui/controls/select-control.tsx +65 -0
  145. package/src/components/ui/controls/slider-control.tsx +76 -31
  146. package/src/components/ui/floating-level-selector.tsx +9 -14
  147. package/src/components/ui/helpers/building-helper.tsx +4 -1
  148. package/src/components/ui/helpers/contextual-helper-panel.tsx +154 -70
  149. package/src/components/ui/helpers/helper-manager.test.tsx +69 -0
  150. package/src/components/ui/helpers/helper-manager.tsx +348 -19
  151. package/src/components/ui/helpers/item-helper.tsx +8 -0
  152. package/src/components/ui/helpers/registered-tool-helper.tsx +10 -33
  153. package/src/components/ui/icon-ref.tsx +45 -1
  154. package/src/components/ui/item-catalog/item-catalog.tsx +5 -30
  155. package/src/components/ui/level-duplicate-dialog.tsx +1 -1
  156. package/src/components/ui/node-icon-image.tsx +16 -0
  157. package/src/components/ui/panels/collections/collections-popover.tsx +20 -14
  158. package/src/components/ui/panels/floor-foundation-panel.tsx +551 -0
  159. package/src/components/ui/panels/floor-opening-panel.tsx +143 -0
  160. package/src/components/ui/panels/mobile-panel-sheet.tsx +2 -2
  161. package/src/components/ui/panels/mobile-selection-bar.tsx +2 -2
  162. package/src/components/ui/panels/multi-field-value.test.ts +26 -3
  163. package/src/components/ui/panels/multi-field-value.ts +6 -3
  164. package/src/components/ui/panels/multi-height-mode.tsx +27 -21
  165. package/src/components/ui/panels/multi-parametric-inspector.tsx +52 -2
  166. package/src/components/ui/panels/multi-selection-panel.tsx +43 -4
  167. package/src/components/ui/panels/node-display.ts +8 -3
  168. package/src/components/ui/panels/panel-manager.tsx +103 -68
  169. package/src/components/ui/panels/panel-wrapper.tsx +78 -36
  170. package/src/components/ui/panels/parametric-field-control.tsx +1 -1
  171. package/src/components/ui/panels/parametric-inspector.tsx +7 -19
  172. package/src/components/ui/panels/reference-panel.tsx +2 -3
  173. package/src/components/ui/panels/room-construction-rows.tsx +774 -0
  174. package/src/components/ui/panels/room-name-options.test.ts +55 -0
  175. package/src/components/ui/panels/room-name-options.ts +47 -0
  176. package/src/components/ui/panels/room-openings-field.tsx +110 -0
  177. package/src/components/ui/panels/room-panel.tsx +725 -0
  178. package/src/components/ui/panels/shape-choice.tsx +58 -0
  179. package/src/components/ui/panels/wall-region-list.tsx +109 -0
  180. package/src/components/ui/primitives/color-dot.tsx +36 -24
  181. package/src/components/ui/primitives/shortcut-token.tsx +4 -0
  182. package/src/components/ui/primitives/sidebar.tsx +2 -0
  183. package/src/components/ui/right-stack.test.ts +29 -0
  184. package/src/components/ui/right-stack.tsx +145 -0
  185. package/src/components/ui/sidebar/icon-rail.tsx +5 -0
  186. package/src/components/ui/sidebar/panels/settings-panel/audio-settings-dialog.tsx +2 -2
  187. package/src/components/ui/sidebar/panels/settings-panel/index.tsx +615 -372
  188. package/src/components/ui/sidebar/panels/settings-panel/keyboard-shortcuts-dialog.tsx +7 -7
  189. package/src/components/ui/sidebar/panels/settings-panel/print-export-button.tsx +1 -1
  190. package/src/components/ui/sidebar/panels/site-panel/ceiling-tree-node.tsx +4 -4
  191. package/src/components/ui/sidebar/panels/site-panel/chimney-tree-node.tsx +1 -5
  192. package/src/components/ui/sidebar/panels/site-panel/collection-tree-node.tsx +197 -0
  193. package/src/components/ui/sidebar/panels/site-panel/column-tree-node.tsx +1 -5
  194. package/src/components/ui/sidebar/panels/site-panel/door-tree-node.tsx +1 -5
  195. package/src/components/ui/sidebar/panels/site-panel/dormer-tree-node.tsx +1 -5
  196. package/src/components/ui/sidebar/panels/site-panel/elevator-tree-node.tsx +1 -5
  197. package/src/components/ui/sidebar/panels/site-panel/fence-tree-node.tsx +24 -11
  198. package/src/components/ui/sidebar/panels/site-panel/gutter-tree-node.tsx +1 -5
  199. package/src/components/ui/sidebar/panels/site-panel/index.tsx +176 -211
  200. package/src/components/ui/sidebar/panels/site-panel/inline-rename-input.tsx +30 -8
  201. package/src/components/ui/sidebar/panels/site-panel/item-tree-node.tsx +1 -3
  202. package/src/components/ui/sidebar/panels/site-panel/level-tree-node.tsx +4 -1
  203. package/src/components/ui/sidebar/panels/site-panel/registry-tree-node.tsx +1 -3
  204. package/src/components/ui/sidebar/panels/site-panel/roof-tree-node.tsx +1 -4
  205. package/src/components/ui/sidebar/panels/site-panel/shelf-tree-node.tsx +1 -3
  206. package/src/components/ui/sidebar/panels/site-panel/slab-tree-node.tsx +21 -13
  207. package/src/components/ui/sidebar/panels/site-panel/solar-panel-tree-node.tsx +1 -5
  208. package/src/components/ui/sidebar/panels/site-panel/spawn-tree-node.tsx +1 -5
  209. package/src/components/ui/sidebar/panels/site-panel/stair-tree-node.tsx +1 -5
  210. package/src/components/ui/sidebar/panels/site-panel/tree-node.tsx +0 -27
  211. package/src/components/ui/sidebar/panels/site-panel/unit-tree-node.tsx +2 -3
  212. package/src/components/ui/sidebar/panels/site-panel/wall-tree-node.tsx +1 -5
  213. package/src/components/ui/sidebar/panels/site-panel/window-tree-node.tsx +1 -5
  214. package/src/components/ui/sidebar/panels/site-panel/zone-tree-node.tsx +9 -4
  215. package/src/components/ui/sidebar/panels/zone-panel/index.tsx +0 -2
  216. package/src/components/ui/sidebar/tab-bar.tsx +10 -0
  217. package/src/components/ui/sidebar/use-plugin-panels.tsx +15 -5
  218. package/src/components/viewer/floorplan-compass-button.tsx +9 -1
  219. package/src/components/viewer/floorplan-preview-visibility.test.ts +35 -0
  220. package/src/components/viewer/floorplan-preview-visibility.ts +19 -0
  221. package/src/components/viewer/floorplan-preview.tsx +6 -18
  222. package/src/components/viewer/viewer-hover-label.tsx +34 -0
  223. package/src/components/viewer/viewer-rooms.tsx +175 -0
  224. package/src/components/viewer/viewer-scene-header.tsx +181 -145
  225. package/src/components/viewer/viewer-selection-manager.tsx +124 -0
  226. package/src/components/viewer/viewer-stage.tsx +2 -2
  227. package/src/components/viewer-zone-system.tsx +9 -9
  228. package/src/hooks/use-auto-save.integration.test.tsx +311 -0
  229. package/src/hooks/use-auto-save.ts +55 -14
  230. package/src/hooks/use-ceiling-events.test.ts +1 -0
  231. package/src/hooks/use-ceiling-events.ts +2 -0
  232. package/src/hooks/use-grid-events.test.ts +2 -0
  233. package/src/hooks/use-grid-events.ts +16 -11
  234. package/src/hooks/use-keyboard.test.ts +182 -0
  235. package/src/hooks/use-keyboard.ts +122 -91
  236. package/src/hooks/use-open-wall-ends.test.ts +99 -0
  237. package/src/hooks/use-open-wall-ends.ts +238 -0
  238. package/src/hooks/use-placement-notice.test.ts +48 -0
  239. package/src/hooks/use-placement-notice.ts +42 -0
  240. package/src/hooks/use-selected-room.ts +143 -0
  241. package/src/index.tsx +306 -39
  242. package/src/lib/catalog-panel-model.ts +39 -0
  243. package/src/lib/catalog-selection.test.ts +27 -0
  244. package/src/lib/catalog-selection.ts +8 -0
  245. package/src/lib/ceiling-edit-session.test.ts +321 -0
  246. package/src/lib/ceiling-edit-session.ts +206 -0
  247. package/src/lib/ceiling-region-session.test.ts +179 -0
  248. package/src/lib/contextual-help.test.ts +27 -1
  249. package/src/lib/contextual-help.ts +16 -9
  250. package/src/lib/continuation.test.ts +19 -1
  251. package/src/lib/continuation.ts +25 -4
  252. package/src/lib/door-interaction.ts +14 -0
  253. package/src/lib/editor-level-display.test.ts +96 -0
  254. package/src/lib/editor-level-display.ts +48 -0
  255. package/src/lib/editor-views.ts +144 -0
  256. package/src/lib/export-geometry-wait.test.ts +90 -0
  257. package/src/lib/floor-edit-session.ts +153 -0
  258. package/src/lib/floor-footprints.test.ts +563 -0
  259. package/src/lib/floor-footprints.ts +395 -0
  260. package/src/lib/floor-foundation-support.test.ts +283 -0
  261. package/src/lib/floor-opening-draft.ts +218 -0
  262. package/src/lib/floor-openings.test.ts +216 -0
  263. package/src/lib/floor-region-geometry.ts +191 -0
  264. package/src/lib/floor-region-session.test.ts +485 -0
  265. package/src/lib/floor-region-session.ts +589 -0
  266. package/src/lib/floor-region-snap.ts +101 -0
  267. package/src/lib/floor-step-hover.test.ts +32 -0
  268. package/src/lib/floorplan/annotation-visibility.ts +17 -0
  269. package/src/lib/floorplan/dimension-drive.test.ts +360 -0
  270. package/src/lib/floorplan/dimension-drive.ts +479 -0
  271. package/src/lib/floorplan/floorplan-export.test.ts +15 -9
  272. package/src/lib/floorplan/floorplan-export.tsx +32 -8
  273. package/src/lib/floorplan/floorplan-extension.ts +48 -0
  274. package/src/lib/floorplan/floorplan-mode.test.ts +4 -0
  275. package/src/lib/floorplan/floorplan-mode.ts +2 -0
  276. package/src/lib/floorplan/floorplan-pdfkit-document.ts +339 -18
  277. package/src/lib/floorplan/floorplan-pdfkit-renderer.test.ts +138 -8
  278. package/src/lib/floorplan/floorplan-pdfkit-renderer.ts +56 -25
  279. package/src/lib/floorplan/floorplan-plugin-install.test.tsx +138 -0
  280. package/src/lib/floorplan/floorplan-readonly.ts +6 -0
  281. package/src/lib/floorplan/geometry.ts +13 -6
  282. package/src/lib/floorplan/index.ts +0 -9
  283. package/src/lib/floorplan/items.ts +5 -2
  284. package/src/lib/floorplan/marks.test.ts +213 -0
  285. package/src/lib/floorplan/marks.ts +238 -0
  286. package/src/lib/floorplan/open-wall-ends.test.ts +268 -0
  287. package/src/lib/floorplan/open-wall-ends.ts +162 -0
  288. package/src/lib/floorplan/pdf-fonts/GeistMono-Bold.ttf +0 -0
  289. package/src/lib/floorplan/pdf-fonts/GeistMono-Regular.ttf +0 -0
  290. package/src/lib/floorplan/pdf-fonts/LICENSE-GeistMono.txt +92 -0
  291. package/src/lib/floorplan/pdf-fonts/LICENSE-LiberationSans.txt +102 -0
  292. package/src/lib/floorplan/pdf-fonts/LiberationSans-Bold.ttf +0 -0
  293. package/src/lib/floorplan/pdf-fonts/LiberationSans-Regular.ttf +0 -0
  294. package/src/lib/floorplan/schedules.test.ts +189 -0
  295. package/src/lib/floorplan/schedules.ts +339 -0
  296. package/src/lib/floorplan/sheet-export.test.ts +100 -0
  297. package/src/lib/floorplan/sheet-export.ts +318 -0
  298. package/src/lib/floorplan/site-plan/build-site-plan-drawing.ts +779 -0
  299. package/src/lib/floorplan/site-plan/contours.test.ts +43 -0
  300. package/src/lib/floorplan/site-plan/contributors.ts +97 -0
  301. package/src/lib/floorplan/site-plan/coverage.ts +232 -0
  302. package/src/lib/floorplan/site-plan/drawing-type-switch.test.ts +15 -0
  303. package/src/lib/floorplan/site-plan/drawing-type-switch.tsx +86 -0
  304. package/src/lib/floorplan/site-plan/front-edge.test.ts +103 -0
  305. package/src/lib/floorplan/site-plan/front-edge.ts +156 -0
  306. package/src/lib/floorplan/site-plan/geometry.test.ts +275 -0
  307. package/src/lib/floorplan/site-plan/geometry.ts +557 -0
  308. package/src/lib/floorplan/site-plan/index.ts +71 -0
  309. package/src/lib/floorplan/site-plan/site-annotations.test.ts +366 -0
  310. package/src/lib/floorplan/site-plan/site-annotations.ts +1083 -0
  311. package/src/lib/floorplan/site-plan/site-parts.ts +379 -0
  312. package/src/lib/floorplan/site-plan/site-plan-layer.tsx +138 -0
  313. package/src/lib/floorplan/site-plan/street-name.test.ts +69 -0
  314. package/src/lib/floorplan/site-plan/street-name.ts +302 -0
  315. package/src/lib/floorplan/site-plan/yards.test.ts +69 -0
  316. package/src/lib/floorplan/types.ts +1 -27
  317. package/src/lib/fresh-planar-placement.test.ts +36 -0
  318. package/src/lib/fresh-planar-placement.ts +191 -36
  319. package/src/lib/geometry-script/author.ts +152 -0
  320. package/src/lib/geometry-script/client.ts +152 -0
  321. package/src/lib/geometry-script/protocol.ts +12 -0
  322. package/src/lib/gesture-lifecycle.test.ts +595 -0
  323. package/src/lib/gesture-lifecycle.ts +290 -0
  324. package/src/lib/glb-export-zone-holes.test.ts +41 -0
  325. package/src/lib/glb-export.test.ts +544 -16
  326. package/src/lib/glb-export.ts +256 -125
  327. package/src/lib/grid-frame.test.ts +19 -0
  328. package/src/lib/grid-frame.ts +18 -0
  329. package/src/lib/history.test.ts +6 -0
  330. package/src/lib/history.ts +7 -0
  331. package/src/lib/hud-title.ts +109 -0
  332. package/src/lib/hydration-gated-storage.ts +49 -0
  333. package/src/lib/ifc-export.test.ts +174 -0
  334. package/src/lib/ifc-export.ts +201 -0
  335. package/src/lib/ifc-parts.ts +15 -0
  336. package/src/lib/inspector-expanded.test.ts +44 -0
  337. package/src/lib/inspector-expanded.ts +36 -0
  338. package/src/lib/interaction/scope.ts +27 -1
  339. package/src/lib/keyboard-pan.test.ts +184 -0
  340. package/src/lib/keyboard-pan.ts +238 -0
  341. package/src/lib/live-gesture-round-trip.test.ts +360 -0
  342. package/src/lib/lot/__fixtures__/map-dossier-miami-shores.json +1 -0
  343. package/src/lib/lot/__fixtures__/map-dossier-stpete.json +1 -0
  344. package/src/lib/lot/clean-ring.test.ts +186 -0
  345. package/src/lib/lot/clean-ring.ts +217 -0
  346. package/src/lib/lot/dossier.test.ts +202 -0
  347. package/src/lib/lot/dossier.ts +498 -0
  348. package/src/lib/lot/drop-in.test.ts +326 -0
  349. package/src/lib/lot/drop-in.ts +407 -0
  350. package/src/lib/lot/index.ts +43 -0
  351. package/src/lib/lot/lot-patch.test.ts +340 -0
  352. package/src/lib/lot/lot-patch.ts +394 -0
  353. package/src/lib/lot/parcel-provider.ts +43 -0
  354. package/src/lib/lot/terrain.test.ts +161 -0
  355. package/src/lib/lot/terrain.ts +266 -0
  356. package/src/lib/material-catalog-model.ts +74 -0
  357. package/src/lib/material-paint-panel-model.ts +80 -0
  358. package/src/lib/material-paint.ts +4 -0
  359. package/src/lib/mezzanine-actions.test.ts +342 -0
  360. package/src/lib/mezzanine-draft.test.ts +501 -0
  361. package/src/lib/mezzanine-draft.ts +278 -0
  362. package/src/lib/mezzanine-messages.ts +26 -0
  363. package/src/lib/mezzanine-selection.test.ts +207 -0
  364. package/src/lib/mezzanine-stairs.ts +46 -0
  365. package/src/lib/model-export.ts +3 -1
  366. package/src/lib/node-action-movement.ts +17 -6
  367. package/src/lib/node-display-label.ts +37 -0
  368. package/src/lib/orbit-floor-clearance.test.ts +165 -0
  369. package/src/lib/orbit-floor-clearance.ts +106 -0
  370. package/src/lib/overlay-transform.test.ts +28 -0
  371. package/src/lib/overlay-transform.ts +12 -0
  372. package/src/lib/paint-commit-route.test.ts +37 -0
  373. package/src/lib/paint-commit-route.ts +17 -0
  374. package/src/lib/paint-eyedropper.test.tsx +165 -0
  375. package/src/lib/paint-eyedropper.ts +99 -0
  376. package/src/lib/paint-pass-through.test.ts +111 -0
  377. package/src/lib/paint-pass-through.ts +76 -0
  378. package/src/lib/paint-region-hover.ts +25 -0
  379. package/src/lib/paint-region-mode.ts +154 -0
  380. package/src/lib/paint-regions.test.ts +166 -0
  381. package/src/lib/paint-regions.ts +192 -0
  382. package/src/lib/paint-scope.test.ts +317 -35
  383. package/src/lib/paint-scope.ts +266 -25
  384. package/src/lib/panel-rows.ts +96 -0
  385. package/src/lib/panel-tool-options.test.ts +34 -0
  386. package/src/lib/panel-tool-options.ts +46 -0
  387. package/src/lib/parametric-node-update.ts +37 -0
  388. package/src/lib/plate-paint-affected.test.ts +300 -0
  389. package/src/lib/plate-paint-affected.ts +177 -0
  390. package/src/lib/plugin-chat.ts +50 -0
  391. package/src/lib/plugin-panels.test.ts +25 -0
  392. package/src/lib/plugin-panels.ts +94 -0
  393. package/src/lib/print-shell-compiler.ts +1 -0
  394. package/src/lib/registered-draft-snap.ts +12 -0
  395. package/src/lib/roof-duplication.ts +10 -2
  396. package/src/lib/room-assembly-overlay.test.ts +275 -0
  397. package/src/lib/room-assembly-overlay.ts +266 -0
  398. package/src/lib/room-built-on.test.ts +379 -0
  399. package/src/lib/room-built-on.ts +138 -0
  400. package/src/lib/room-construction-commands.ts +712 -0
  401. package/src/lib/room-construction.test.ts +746 -0
  402. package/src/lib/room-construction.ts +385 -0
  403. package/src/lib/room-controls-phase6.test.ts +630 -0
  404. package/src/lib/room-divide-session.ts +523 -0
  405. package/src/lib/room-handle-drag.ts +1068 -0
  406. package/src/lib/room-name-catalog.test.ts +90 -0
  407. package/src/lib/room-name-catalog.ts +168 -0
  408. package/src/lib/room-push-dimensions.test.ts +311 -0
  409. package/src/lib/room-push-dimensions.ts +251 -0
  410. package/src/lib/room-selection-commands.ts +62 -0
  411. package/src/lib/room-selection.test.ts +973 -0
  412. package/src/lib/room-selection.ts +417 -0
  413. package/src/lib/room-structure-commands.ts +94 -0
  414. package/src/lib/room-structure.test.ts +921 -0
  415. package/src/lib/room-transform-session.ts +570 -0
  416. package/src/lib/room-zone-routing.test.ts +294 -0
  417. package/src/lib/room-zone-routing.ts +197 -0
  418. package/src/lib/scene-clipboard.test.ts +364 -0
  419. package/src/lib/scene-clipboard.ts +229 -48
  420. package/src/lib/scene-material-refs.test.ts +79 -0
  421. package/src/lib/scene-material-refs.ts +79 -0
  422. package/src/lib/scene.test.ts +87 -0
  423. package/src/lib/scene.ts +39 -45
  424. package/src/lib/selection-routing.test.ts +129 -45
  425. package/src/lib/selection-routing.ts +53 -74
  426. package/src/lib/session-writes.test.ts +120 -0
  427. package/src/lib/session-writes.ts +205 -0
  428. package/src/lib/sidebar-panel.ts +62 -0
  429. package/src/lib/site-boundary.test.ts +4 -6
  430. package/src/lib/site-boundary.ts +1 -1
  431. package/src/lib/snapping-mode.test.ts +17 -2
  432. package/src/lib/snapping-mode.ts +19 -7
  433. package/src/lib/spatial-drag-plane.test.ts +53 -0
  434. package/src/lib/spatial-drag-plane.ts +19 -0
  435. package/src/lib/spatial-pointer-input.test.ts +72 -0
  436. package/src/lib/spatial-pointer-input.ts +62 -0
  437. package/src/lib/spatial-snap.test.ts +47 -0
  438. package/src/lib/terrace-draft.test.ts +145 -0
  439. package/src/lib/terrace-draft.ts +213 -0
  440. package/src/lib/terrain-panel-model.ts +117 -0
  441. package/src/lib/tool-cursor-icon.test.ts +16 -0
  442. package/src/lib/tool-cursor-icon.ts +11 -0
  443. package/src/lib/typing-target.ts +9 -0
  444. package/src/lib/units.ts +77 -39
  445. package/src/lib/vector-edges.test.ts +58 -0
  446. package/src/lib/vector-edges.ts +400 -0
  447. package/src/lib/viewer-selection.test.ts +577 -0
  448. package/src/lib/viewer-selection.ts +569 -0
  449. package/src/lib/wall-draw-variant.test.ts +71 -0
  450. package/src/lib/wall-draw-variant.ts +91 -0
  451. package/src/lib/wall-paint-region-session.ts +246 -0
  452. package/src/lib/wall-paint-region.test.ts +373 -0
  453. package/src/lib/wall-polygon-draft.test.ts +262 -0
  454. package/src/lib/wall-polygon-draft.ts +286 -0
  455. package/src/lib/wall-push.test.ts +453 -0
  456. package/src/lib/wall-region-face.ts +352 -0
  457. package/src/lib/wall-region-handles.test.ts +304 -0
  458. package/src/lib/wall-region-handles.ts +368 -0
  459. package/src/lib/wall-region-list.test.ts +119 -0
  460. package/src/lib/wall-region-list.ts +136 -0
  461. package/src/lib/wall-region-snap.ts +110 -0
  462. package/src/lib/window-interaction.ts +14 -0
  463. package/src/lib/world-grid-snap.test.ts +48 -0
  464. package/src/lib/world-grid-snap.ts +9 -5
  465. package/src/lib/zone-content.ts +4 -1
  466. package/src/lib/zone-name.test.ts +17 -0
  467. package/src/lib/zone-name.ts +17 -0
  468. package/src/store/opens-in-select-mode.test.ts +223 -0
  469. package/src/store/paint-mode.test.ts +188 -0
  470. package/src/store/rotation-snapping.test.ts +53 -0
  471. package/src/store/terrain-sculpt-mode.test.ts +19 -21
  472. package/src/store/tool-mode.test.ts +137 -6
  473. package/src/store/use-delete-confirmation.ts +28 -1
  474. package/src/store/use-drawing-view.test.ts +24 -3
  475. package/src/store/use-drawing-view.ts +29 -1
  476. package/src/store/use-editor.tsx +442 -323
  477. package/src/store/use-floorplan-draft-preview.ts +23 -1
  478. package/src/store/use-floorplan-marquee.ts +12 -2
  479. package/src/store/use-interaction-scope.ts +155 -6
  480. package/src/store/use-quick-measurement-hud.ts +3 -3
  481. package/src/store/use-stair-build-preview.ts +13 -5
  482. package/src/store/use-wall-move-ghosts.ts +4 -2
  483. package/src/store/use-wall-region-handles-selection.test.ts +222 -0
  484. package/src/store/use-wall-region-handles-selection.ts +139 -0
  485. package/src/store/view-layout.test.ts +175 -0
  486. package/src/store/view-layout.ts +196 -0
  487. package/src/test-utils/immediate-animation-frames.ts +16 -0
  488. package/src/test-utils/live-gesture-round-trip.ts +224 -0
  489. package/src/test-utils/selection-harness.ts +82 -0
  490. package/src/components/editor-2d/renderers/floorplan-stair-layer.tsx +0 -499
  491. package/src/components/systems/zone/zone-label-editor-system.tsx +0 -333
  492. package/src/components/tools/fence/fence-drafting.test.ts +0 -100
  493. package/src/components/tools/fence/fence-drafting.ts +0 -266
  494. package/src/components/ui/sidebar/panels/plugins-panel.tsx +0 -218
  495. package/src/components/viewer/viewer-units-panel.tsx +0 -116
  496. package/src/lib/floorplan/stairs.ts +0 -489
  497. package/src/lib/level-duplication.test.ts +0 -144
  498. package/src/lib/level-duplication.ts +0 -195
@@ -0,0 +1,44 @@
1
+ 'use client'
2
+
3
+ import { Link2 } from 'lucide-react'
4
+
5
+ /**
6
+ * The "Join walls" action on an open wall end — shared by the floor plan and
7
+ * the 3D view so both offer the same pill. A refusal ("A door or window is in
8
+ * the way") sits in front of the button.
9
+ */
10
+ export function JoinWallsPill({
11
+ onHoverEnd,
12
+ onHoverStart,
13
+ onJoin,
14
+ refusal,
15
+ }: {
16
+ onHoverEnd: () => void
17
+ onHoverStart: () => void
18
+ onJoin: () => void
19
+ refusal: string | null
20
+ }) {
21
+ return (
22
+ <div
23
+ className="pointer-events-auto flex items-center gap-1 whitespace-nowrap rounded-full border border-border bg-background/95 p-1 text-xs shadow-xl backdrop-blur-md"
24
+ data-open-wall-end=""
25
+ onPointerDown={(event) => event.stopPropagation()}
26
+ onPointerEnter={onHoverStart}
27
+ onPointerLeave={onHoverEnd}
28
+ onPointerUp={(event) => event.stopPropagation()}
29
+ >
30
+ {refusal ? <span className="px-2 text-muted-foreground">{refusal}</span> : null}
31
+ <button
32
+ className="flex items-center gap-1.5 rounded-full px-3 py-1.5 font-medium text-foreground transition-colors hover:bg-accent"
33
+ onClick={(event) => {
34
+ event.stopPropagation()
35
+ onJoin()
36
+ }}
37
+ type="button"
38
+ >
39
+ <Link2 className="h-3.5 w-3.5" />
40
+ Join walls
41
+ </button>
42
+ </div>
43
+ )
44
+ }
@@ -1,8 +1,21 @@
1
1
  'use client'
2
2
 
3
3
  import { Icon } from '@iconify/react'
4
- import { Copy, Group, Move, PencilRuler, Search, Spline, Trash2, Ungroup } from 'lucide-react'
5
- import type { MouseEventHandler, PointerEventHandler } from 'react'
4
+ import {
5
+ Copy,
6
+ Group,
7
+ Move,
8
+ PencilRuler,
9
+ RotateCcw,
10
+ RotateCw,
11
+ Search,
12
+ Spline,
13
+ Trash2,
14
+ Ungroup,
15
+ } from 'lucide-react'
16
+ import type { MouseEventHandler, PointerEventHandler, ReactNode } from 'react'
17
+ import { ActionMenuButton } from './action-menu-button'
18
+ import { RegistryActionContributions } from './registry-action-contributions'
6
19
 
7
20
  type NodeActionMenuProps = {
8
21
  onFind?: MouseEventHandler<HTMLButtonElement>
@@ -12,6 +25,9 @@ type NodeActionMenuProps = {
12
25
  onMove?: MouseEventHandler<HTMLButtonElement>
13
26
  onEditMesh?: MouseEventHandler<HTMLButtonElement>
14
27
  onCurve?: MouseEventHandler<HTMLButtonElement>
28
+ /** Quarter turns (a room); sit in the Curve slot (a room has no curve). */
29
+ onRotateLeft?: MouseEventHandler<HTMLButtonElement>
30
+ onRotateRight?: MouseEventHandler<HTMLButtonElement>
15
31
  /** Session group (Ctrl/Cmd+G) — multi-selection floating pill. */
16
32
  onGroup?: MouseEventHandler<HTMLButtonElement>
17
33
  /** Dissolve session group (Ctrl/Cmd+Shift+G). */
@@ -20,6 +36,16 @@ type NodeActionMenuProps = {
20
36
  onPointerUp?: PointerEventHandler<HTMLDivElement>
21
37
  onPointerEnter?: PointerEventHandler<HTMLDivElement>
22
38
  onPointerLeave?: PointerEventHandler<HTMLDivElement>
39
+ /**
40
+ * Buttons a caller contributes next to the registry contributions — for a
41
+ * selection that is not a scene-node selection (the room), whose actions the
42
+ * registry cannot find.
43
+ */
44
+ children?: ReactNode
45
+ /** Tooltip / accessible name of the delete button. */
46
+ deleteLabel?: string
47
+ /** Shows the delete button greyed out, its tooltip saying why. */
48
+ deleteDisabledReason?: string
23
49
  }
24
50
 
25
51
  export function NodeActionMenu({
@@ -30,12 +56,17 @@ export function NodeActionMenu({
30
56
  onMove,
31
57
  onEditMesh,
32
58
  onCurve,
59
+ onRotateLeft,
60
+ onRotateRight,
33
61
  onGroup,
34
62
  onUngroup,
35
63
  onPointerDown,
36
64
  onPointerUp,
37
65
  onPointerEnter,
38
66
  onPointerLeave,
67
+ children,
68
+ deleteLabel = 'Delete',
69
+ deleteDisabledReason,
39
70
  }: NodeActionMenuProps) {
40
71
  return (
41
72
  <div
@@ -46,103 +77,72 @@ export function NodeActionMenu({
46
77
  onPointerUp={onPointerUp}
47
78
  >
48
79
  {onFind && (
49
- <button
50
- aria-label="Find in catalog"
51
- className="tooltip-trigger rounded-md p-1.5 text-muted-foreground transition-colors hover:bg-accent hover:text-foreground"
52
- onClick={onFind}
53
- title="Find in catalog"
54
- type="button"
55
- >
80
+ <ActionMenuButton label="Find in catalog" onClick={onFind}>
56
81
  <Search className="h-4 w-4" />
57
- </button>
82
+ </ActionMenuButton>
58
83
  )}
59
84
  {onMove && (
60
- <button
61
- aria-label="Move"
62
- className="tooltip-trigger rounded-md p-1.5 text-muted-foreground transition-colors hover:bg-accent hover:text-foreground"
63
- onClick={onMove}
64
- title="Move"
65
- type="button"
66
- >
85
+ <ActionMenuButton label="Move" onClick={onMove}>
67
86
  <Move className="h-4 w-4" />
68
- </button>
87
+ </ActionMenuButton>
69
88
  )}
70
89
  {onEditMesh && (
71
- <button
72
- aria-label="Edit mesh"
73
- className="tooltip-trigger rounded-md p-1.5 text-muted-foreground transition-colors hover:bg-accent hover:text-foreground"
74
- onClick={onEditMesh}
75
- title="Edit mesh"
76
- type="button"
77
- >
90
+ <ActionMenuButton label="Edit mesh" onClick={onEditMesh}>
78
91
  <PencilRuler className="h-4 w-4" />
79
- </button>
92
+ </ActionMenuButton>
80
93
  )}
81
94
  {onGroup && (
82
- <button
83
- aria-label="Group selection"
84
- className="tooltip-trigger rounded-md p-1.5 text-muted-foreground transition-colors hover:bg-accent hover:text-foreground"
85
- onClick={onGroup}
86
- title="Group (Ctrl/Cmd+G)"
87
- type="button"
88
- >
95
+ <ActionMenuButton keys={['Cmd/Ctrl', 'G']} label="Group selection" onClick={onGroup}>
89
96
  <Group className="h-4 w-4" />
90
- </button>
97
+ </ActionMenuButton>
91
98
  )}
92
99
  {onUngroup && (
93
- <button
94
- aria-label="Ungroup selection"
95
- className="tooltip-trigger rounded-md p-1.5 text-muted-foreground transition-colors hover:bg-accent hover:text-foreground"
100
+ <ActionMenuButton
101
+ keys={['Cmd/Ctrl', 'Shift', 'G']}
102
+ label="Ungroup selection"
96
103
  onClick={onUngroup}
97
- title="Ungroup (Ctrl/Cmd+Shift+G)"
98
- type="button"
99
104
  >
100
105
  <Ungroup className="h-4 w-4" />
101
- </button>
106
+ </ActionMenuButton>
102
107
  )}
103
108
  {onCurve && (
104
- <button
105
- aria-label="Curve"
106
- className="tooltip-trigger rounded-md p-1.5 text-muted-foreground transition-colors hover:bg-accent hover:text-foreground"
107
- onClick={onCurve}
108
- title="Curve"
109
- type="button"
110
- >
109
+ <ActionMenuButton label="Curve" onClick={onCurve}>
111
110
  <Spline className="h-4 w-4" />
112
- </button>
111
+ </ActionMenuButton>
113
112
  )}
113
+ {onRotateLeft && (
114
+ <ActionMenuButton label="Rotate left" onClick={onRotateLeft}>
115
+ <RotateCcw className="h-4 w-4" />
116
+ </ActionMenuButton>
117
+ )}
118
+ {onRotateRight && (
119
+ <ActionMenuButton label="Rotate right" onClick={onRotateRight}>
120
+ <RotateCw className="h-4 w-4" />
121
+ </ActionMenuButton>
122
+ )}
123
+ <RegistryActionContributions />
124
+ {children}
114
125
  {onDuplicate && (
115
- <button
116
- aria-label="Duplicate"
117
- className="tooltip-trigger rounded-md p-1.5 text-muted-foreground transition-colors hover:bg-accent hover:text-foreground"
118
- onClick={onDuplicate}
119
- title="Duplicate"
120
- type="button"
121
- >
126
+ <ActionMenuButton label="Duplicate" onClick={onDuplicate}>
122
127
  <Copy className="h-4 w-4" />
123
- </button>
128
+ </ActionMenuButton>
124
129
  )}
125
130
  {onAddHole && (
126
- <button
127
- aria-label="Cut Out"
128
- className="tooltip-trigger rounded-md p-1.5 text-muted-foreground transition-colors hover:bg-accent hover:text-foreground"
129
- onClick={onAddHole}
130
- title="Cut Out"
131
- type="button"
132
- >
131
+ <ActionMenuButton label="Cut out" onClick={onAddHole}>
133
132
  <Icon height={16} icon="carbon:cut-out" width={16} />
134
- </button>
133
+ </ActionMenuButton>
135
134
  )}
136
135
  {onDelete && (
137
- <button
138
- aria-label="Delete"
139
- className="tooltip-trigger rounded-md p-1.5 text-muted-foreground transition-colors hover:bg-destructive/10 hover:text-destructive"
136
+ <ActionMenuButton
137
+ destructive
138
+ disabled={!!deleteDisabledReason}
139
+ disabledReason={deleteDisabledReason}
140
+ keys={['Delete / Backspace']}
141
+ label={deleteLabel}
140
142
  onClick={onDelete}
141
- title="Delete"
142
- type="button"
143
143
  >
144
144
  <Trash2 className="h-4 w-4" />
145
- </button>
145
+ </ActionMenuButton>
146
146
  )}
147
147
  </div>
148
148
  )
@@ -4,7 +4,9 @@ import {
4
4
  type AnyNode,
5
5
  type AnyNodeId,
6
6
  type ArcResizeHandle,
7
+ type CornerRadiusHandle,
7
8
  type Cursor,
9
+ createAngleAccumulator,
8
10
  createSceneApi,
9
11
  DEFAULT_ANGLE_STEP,
10
12
  type HandleDescriptor,
@@ -41,12 +43,17 @@ import {
41
43
  import { mergeGeometries } from 'three/examples/jsm/utils/BufferGeometryUtils.js'
42
44
 
43
45
  import { MeshBasicNodeMaterial } from 'three/webgpu'
46
+ import { useCeilingEditCeilingId } from '../../lib/ceiling-edit-session'
44
47
  import { EDITOR_LAYER } from '../../lib/constants'
45
48
  import { RESIZE_HANDLE_DRAG_LABEL, ROTATE_HANDLE_DRAG_LABEL } from '../../lib/contextual-help'
46
49
  import { createEditorApi } from '../../lib/editor-api'
47
50
  import { sfxEmitter } from '../../lib/sfx-bus'
48
51
  import useDirectManipulationFeedback from '../../store/use-direct-manipulation-feedback'
49
- import useEditor, { isGridSnapActive, isMagneticSnapActive } from '../../store/use-editor'
52
+ import useEditor, {
53
+ isAngleSnapActive,
54
+ isGridSnapActive,
55
+ isMagneticSnapActive,
56
+ } from '../../store/use-editor'
50
57
  import { useHandleGroup } from '../../store/use-handle-group'
51
58
  import useInteractionScope, {
52
59
  useEndpointReshape,
@@ -236,7 +243,12 @@ export function NodeArrowHandles() {
236
243
  const activeGroup = useHandleGroup((s) =>
237
244
  s.active && s.active.nodeId === node?.id ? s.active.group : null,
238
245
  )
239
- const def = node ? nodeRegistry.get(node.type) : null
246
+ // A ceiling's handles belong to its Edit ceiling session; a plain selection shows none.
247
+ const editCeilingId = useCeilingEditCeilingId()
248
+ const def =
249
+ node && (node.type !== 'ceiling' || node.id === editCeilingId)
250
+ ? nodeRegistry.get(node.type)
251
+ : null
240
252
  const descriptorSceneApi = useMemo(() => createSceneApi(useScene), [])
241
253
  const descriptors = useMemo(() => {
242
254
  if (!(node && def?.handles)) return null
@@ -574,6 +586,17 @@ function ArrowHandle({
574
586
  />
575
587
  )
576
588
  }
589
+ if (descriptor.kind === 'corner-radius') {
590
+ return (
591
+ <CornerRadiusKnob
592
+ descriptor={descriptor}
593
+ dragControls={dragControls}
594
+ handleIndex={handleIndex}
595
+ node={placementNode}
596
+ rideObject={rideObject}
597
+ />
598
+ )
599
+ }
577
600
  if (descriptor.kind === 'tap-action') {
578
601
  // Tap-action handles (fence side-move arrows, corner pickers) aren't
579
602
  // resize handles, so the freeze-at-pre-drag mechanism — which only
@@ -587,6 +610,119 @@ function ArrowHandle({
587
610
  return null
588
611
  }
589
612
 
613
+ const CORNER_RADIUS_DOT_INSET = 0.2
614
+ const CORNER_RADIUS_POSITION_FACTOR = Math.SQRT2 - 1
615
+
616
+ function CornerRadiusKnob({
617
+ descriptor,
618
+ node,
619
+ handleIndex,
620
+ dragControls,
621
+ rideObject,
622
+ }: {
623
+ descriptor: CornerRadiusHandle<AnyNode>
624
+ node: AnyNode
625
+ handleIndex: number
626
+ dragControls: HandleDragControls
627
+ rideObject: Object3D
628
+ }) {
629
+ const [isHovered, setIsHovered] = useState(false)
630
+ const [isDragging, setIsDragging] = useState(false)
631
+ const { camera } = useThree()
632
+ const zoom = camera instanceof OrthographicCamera ? 1 / camera.zoom : 1
633
+ const [sx, sy] = descriptor.corner
634
+ const radius = descriptor.currentValue(node)
635
+ const inset = CORNER_RADIUS_DOT_INSET + CORNER_RADIUS_POSITION_FACTOR * radius
636
+ const position: [number, number, number] = [
637
+ sx * (descriptor.width(node) / 2 - inset / Math.SQRT2),
638
+ sy * (descriptor.height(node) / 2 - inset / Math.SQRT2),
639
+ 0,
640
+ ]
641
+
642
+ const activate = useHandleDrag({
643
+ kind: 'drag',
644
+ cursor: 'grabbing',
645
+ dragControls,
646
+ handleIndex,
647
+ node,
648
+ rideObject,
649
+ setIsDragging,
650
+ onStart: ({ initialNode, nodeId, rideObject: dragRideObject, sceneApi: dragSceneApi }) => {
651
+ dragRideObject.updateMatrixWorld(true)
652
+ const worldMatrix = dragRideObject.matrixWorld.clone()
653
+ const inverseWorldMatrix = worldMatrix.clone().invert()
654
+ const center = new Vector3().setFromMatrixPosition(worldMatrix)
655
+ const normal = new Vector3().setFromMatrixColumn(worldMatrix, 2).normalize()
656
+ const plane = new Plane().setFromNormalAndCoplanarPoint(normal, center)
657
+ const maxRadius = resolveBound(
658
+ descriptor.max,
659
+ Number.POSITIVE_INFINITY,
660
+ initialNode,
661
+ dragSceneApi,
662
+ )
663
+ const preview = descriptor.createPreview?.(initialNode as never)
664
+
665
+ return {
666
+ markDirty: !preview,
667
+ commit: preview ? (patch) => preview.commit(patch as never) : undefined,
668
+ onBegin: () => {
669
+ useInteractionScope.getState().begin({
670
+ kind: 'handle-drag',
671
+ nodeId,
672
+ handle: 'Corner radius',
673
+ })
674
+ },
675
+ onCancel: () => preview?.cancel(),
676
+ onEnd: () => {
677
+ useInteractionScope.getState().endIf((scope) => scope.kind === 'handle-drag')
678
+ },
679
+ move: ({ event, modifiers, intersectPlane }) => {
680
+ const hit = new Vector3()
681
+ if (!intersectPlane(event.clientX, event.clientY, plane, hit)) return null
682
+ hit.applyMatrix4(inverseWorldMatrix)
683
+ const diagonalDistance =
684
+ (descriptor.width(initialNode) / 2 +
685
+ descriptor.height(initialNode) / 2 -
686
+ sx * hit.x -
687
+ sy * hit.y) /
688
+ Math.SQRT2
689
+ const next = Math.min(
690
+ maxRadius,
691
+ Math.max(
692
+ 0,
693
+ (diagonalDistance - CORNER_RADIUS_DOT_INSET) / CORNER_RADIUS_POSITION_FACTOR,
694
+ ),
695
+ )
696
+ const patch = descriptor.apply(initialNode as never, next, dragSceneApi, modifiers)
697
+ preview?.preview(patch as never)
698
+ return patch as Partial<AnyNode>
699
+ },
700
+ }
701
+ },
702
+ })
703
+
704
+ return (
705
+ <HandleArrow
706
+ activeCursor="grabbing"
707
+ cursor="grab"
708
+ hover={isHovered || isDragging}
709
+ hoverScale={1.3}
710
+ onHoverChange={setIsHovered}
711
+ onPointerDown={activate}
712
+ placement={{ position, rotation: [0, 0, 0], baseScale: zoom * 0.42 }}
713
+ round
714
+ shape="corner-picker"
715
+ >
716
+ {isHovered || isDragging ? (
717
+ <DimensionLabel
718
+ position={[0, 0.22, 0]}
719
+ text={formatDimension(radius, useViewer.getState().unit)}
720
+ />
721
+ ) : null}
722
+ </HandleArrow>
723
+ )
724
+ }
725
+
590
726
  function pickCursor(descriptor: LinearResizeHandle<AnyNode> | RadialResizeHandle<AnyNode>): Cursor {
591
727
  if (descriptor.kind === 'linear-resize' && descriptor.cursor) return descriptor.cursor
592
728
  return descriptor.axis === 'y' ? 'ns-resize' : 'ew-resize'
@@ -687,8 +823,16 @@ function LinearArrow({
687
823
  }) => {
688
824
  dragRideObject.matrixWorld.decompose(_resizePositionW, _resizeQuaternion, _resizeScale)
689
825
  _resizeOriginW.set(...position).applyMatrix4(dragRideObject.matrixWorld)
690
- axisVector(descriptor.axis, _resizeAxisW).applyQuaternion(_resizeQuaternion).normalize()
691
- const localToWorldScale = axisScale(descriptor.axis, _resizeScale)
826
+ const dragAxis =
827
+ descriptor.kind === 'linear-resize'
828
+ ? descriptor.dragAxis?.(initialNode as never, sceneApi)
829
+ : undefined
830
+ if (dragAxis) _resizeAxisW.set(...dragAxis).normalize()
831
+ else axisVector(descriptor.axis, _resizeAxisW)
832
+ const localToWorldScale = dragAxis
833
+ ? _resizeAxisW.clone().multiply(_resizeScale).length()
834
+ : axisScale(descriptor.axis, _resizeScale)
835
+ _resizeAxisW.applyQuaternion(_resizeQuaternion).normalize()
692
836
  if (Math.abs(localToWorldScale) < 1e-6 || _resizeAxisW.lengthSq() === 0) return null
693
837
 
694
838
  const initialPointer =
@@ -705,7 +849,10 @@ function LinearArrow({
705
849
  initialNode,
706
850
  nodeId,
707
851
  sceneApi,
708
- initialModifiers: { altKey: event.nativeEvent.altKey },
852
+ initialModifiers: {
853
+ altKey: event.nativeEvent.altKey,
854
+ shiftKey: event.nativeEvent.shiftKey,
855
+ },
709
856
  })
710
857
  : null
711
858
  const overrideId = linearBinding?.overrideId ?? nodeId
@@ -1218,11 +1365,9 @@ function ArcArrow({
1218
1365
  return null
1219
1366
  }
1220
1367
  const initialAngle = angleOf(hitWorld)
1368
+ const accumulateAngle = createAngleAccumulator(initialAngle)
1221
1369
 
1222
- // Advertise the rotate interaction so the contextual HUD can surface the
1223
- // Shift = free-rotation toggle (the angle-step bypass below). Resize
1224
- // handles route a measurement label here; rotate gets a sentinel label so
1225
- // the HUD shows the rotate hint, not a dimension pill.
1370
+ // A distinct label selects rotation snapping instead of resize measurements.
1226
1371
  if (isRotateShape) {
1227
1372
  useInteractionScope
1228
1373
  .getState()
@@ -1240,11 +1385,19 @@ function ArcArrow({
1240
1385
  const hit = new Vector3()
1241
1386
  if (!intersectMovePlane(moveEvent.clientX, moveEvent.clientY, plane, hit)) return null
1242
1387
  const currentAngle = angleOf(hit)
1243
- let delta = currentAngle - initialAngle
1244
- while (delta > Math.PI) delta -= 2 * Math.PI
1245
- while (delta < -Math.PI) delta += 2 * Math.PI
1246
-
1247
- if (!moveEvent.shiftKey && descriptor.shape === 'rotate') {
1388
+ let delta = isRotateShape
1389
+ ? Math.atan2(
1390
+ Math.sin(currentAngle - initialAngle),
1391
+ Math.cos(currentAngle - initialAngle),
1392
+ )
1393
+ : accumulateAngle(currentAngle)
1394
+
1395
+ if (
1396
+ !descriptor.continuous &&
1397
+ !moveEvent.altKey &&
1398
+ isAngleSnapActive() &&
1399
+ descriptor.shape === 'rotate'
1400
+ ) {
1248
1401
  delta = Math.round(delta / DEFAULT_ANGLE_STEP) * DEFAULT_ANGLE_STEP
1249
1402
  }
1250
1403