@pascal-app/editor 0.9.1 → 1.0.0-beta.1

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 (366) hide show
  1. package/package.json +14 -9
  2. package/src/components/editor/alignment-3d-guide-layer.tsx +10 -2
  3. package/src/components/editor/bake-exporter.tsx +49 -0
  4. package/src/components/editor/bake-thumbnail.tsx +95 -0
  5. package/src/components/editor/camera-dragging-lifecycle.test.ts +49 -0
  6. package/src/components/editor/camera-dragging-lifecycle.ts +41 -0
  7. package/src/components/editor/custom-camera-controls.tsx +332 -199
  8. package/src/components/editor/delete-confirmation-dialog.tsx +54 -0
  9. package/src/components/editor/editor-layout-mobile.tsx +5 -0
  10. package/src/components/editor/editor-layout-v2.tsx +18 -0
  11. package/src/components/editor/elevation-3d-guide-layer.tsx +120 -0
  12. package/src/components/editor/export-manager.tsx +71 -63
  13. package/src/components/editor/fence-tangent-lines-3d.tsx +87 -0
  14. package/src/components/editor/first-person/build-collider-world.test.ts +102 -5
  15. package/src/components/editor/first-person/build-collider-world.ts +52 -5
  16. package/src/components/editor/first-person/terrain-collider.test.ts +132 -0
  17. package/src/components/editor/first-person/terrain-collider.ts +212 -0
  18. package/src/components/editor/first-person-controls.tsx +410 -136
  19. package/src/components/editor/floating-action-menu.tsx +435 -64
  20. package/src/components/editor/floating-building-action-menu.tsx +1 -1
  21. package/src/components/editor/floorplan-background-selection.test.ts +72 -0
  22. package/src/components/editor/floorplan-background-selection.ts +7 -20
  23. package/src/components/editor/floorplan-camera-sync.test.ts +249 -0
  24. package/src/components/editor/floorplan-camera-sync.ts +213 -0
  25. package/src/components/editor/floorplan-mode-coordinator.tsx +82 -0
  26. package/src/components/editor/floorplan-navigation-presentation.test.ts +142 -0
  27. package/src/components/editor/floorplan-navigation-presentation.ts +156 -0
  28. package/src/components/editor/floorplan-panel.tsx +2517 -1492
  29. package/src/components/editor/grid.tsx +166 -39
  30. package/src/components/editor/group-actions.ts +597 -0
  31. package/src/components/editor/group-floating-action-menu.tsx +130 -0
  32. package/src/components/editor/group-move-3d.ts +397 -0
  33. package/src/components/editor/group-rotate-handle.tsx +102 -64
  34. package/src/components/editor/group-selection-box-3d.tsx +173 -0
  35. package/src/components/editor/group-transform-shared.test.ts +192 -1
  36. package/src/components/editor/group-transform-shared.ts +240 -7
  37. package/src/components/editor/handles/handle-arrow.tsx +81 -23
  38. package/src/components/editor/handles/handle-drag-history.test.ts +96 -0
  39. package/src/components/editor/handles/handle-drag-history.ts +14 -0
  40. package/src/components/editor/handles/preview-overrides.test.ts +26 -0
  41. package/src/components/editor/handles/preview-overrides.ts +13 -0
  42. package/src/components/editor/handles/resize-snap.test.ts +52 -0
  43. package/src/components/editor/handles/resize-snap.ts +23 -0
  44. package/src/components/editor/handles/use-handle-drag.ts +49 -6
  45. package/src/components/editor/index.tsx +289 -35
  46. package/src/components/editor/measurement-pill.tsx +68 -23
  47. package/src/components/editor/node-action-menu.tsx +14 -1
  48. package/src/components/editor/node-arrow-handles.tsx +409 -180
  49. package/src/components/editor/opening-guides-3d-layer.tsx +144 -0
  50. package/src/components/editor/quick-measurement-card.tsx +76 -0
  51. package/src/components/editor/quick-measurement-hud.tsx +34 -0
  52. package/src/components/editor/riser-diagram-panel.tsx +137 -0
  53. package/src/components/editor/selection-manager.tsx +814 -466
  54. package/src/components/editor/site-edge-labels.tsx +28 -6
  55. package/src/components/editor/slab-hole-highlights.tsx +13 -5
  56. package/src/components/editor/snapshot-capture-overlay.tsx +258 -115
  57. package/src/components/editor/three-context-bridge.ts +22 -0
  58. package/src/components/editor/thumbnail-generator.tsx +118 -253
  59. package/src/components/editor/use-floorplan-background-placement.ts +113 -45
  60. package/src/components/editor/use-floorplan-hit-testing.test.ts +82 -0
  61. package/src/components/editor/use-floorplan-hit-testing.ts +144 -161
  62. package/src/components/editor/use-floorplan-scene-data.ts +5 -5
  63. package/src/components/editor/use-mesh-settle-epoch.ts +27 -0
  64. package/src/components/editor/wall-measurement-label.tsx +10 -28
  65. package/src/components/editor/wall-move-side-handles.tsx +229 -39
  66. package/src/components/editor/wall-opening-highlights.tsx +63 -32
  67. package/src/components/editor/wall-snap-beacon-layer.tsx +162 -6
  68. package/src/components/editor-2d/floorplan-action-menu-layer.tsx +9 -5
  69. package/src/components/editor-2d/floorplan-alignment-guide-layer.tsx +2 -1
  70. package/src/components/editor-2d/floorplan-cursor-indicator-overlay.tsx +40 -9
  71. package/src/components/editor-2d/floorplan-cursor-indicator-position.test.ts +31 -0
  72. package/src/components/editor-2d/floorplan-cursor-indicator-position.ts +39 -0
  73. package/src/components/editor-2d/floorplan-group-action-menu.tsx +87 -0
  74. package/src/components/editor-2d/floorplan-group-move.tsx +744 -0
  75. package/src/components/editor-2d/floorplan-measurement-tool-layer.test.ts +113 -0
  76. package/src/components/editor-2d/floorplan-measurement-tool-layer.tsx +1672 -0
  77. package/src/components/editor-2d/floorplan-quick-measure-layer.tsx +212 -0
  78. package/src/components/editor-2d/floorplan-registered-tool-layer.tsx +72 -0
  79. package/src/components/editor-2d/floorplan-registry-action-menu.tsx +219 -20
  80. package/src/components/editor-2d/floorplan-registry-move-overlay.tsx +327 -59
  81. package/src/components/editor-2d/floorplan-render-context.test.ts +27 -0
  82. package/src/components/editor-2d/floorplan-render-context.tsx +113 -8
  83. package/src/components/editor-2d/floorplan-snap-beacon-layer.tsx +5 -3
  84. package/src/components/editor-2d/renderers/floorplan-annotation-layout.test.ts +285 -0
  85. package/src/components/editor-2d/renderers/floorplan-annotation-layout.ts +516 -0
  86. package/src/components/editor-2d/renderers/floorplan-dimension-renderer.test.tsx +236 -0
  87. package/src/components/editor-2d/renderers/floorplan-dimension-renderer.tsx +618 -0
  88. package/src/components/editor-2d/renderers/floorplan-geometry-renderer.test.tsx +369 -0
  89. package/src/components/editor-2d/renderers/floorplan-geometry-renderer.tsx +431 -32
  90. package/src/components/editor-2d/renderers/floorplan-label-angle.test.ts +89 -0
  91. package/src/components/editor-2d/renderers/floorplan-label-angle.ts +106 -0
  92. package/src/components/editor-2d/renderers/floorplan-placement-preview-layer.tsx +94 -26
  93. package/src/components/editor-2d/renderers/floorplan-registry-layer.test.ts +587 -0
  94. package/src/components/editor-2d/renderers/floorplan-registry-layer.tsx +2280 -578
  95. package/src/components/editor-2d/renderers/floorplan-stair-layer.tsx +20 -10
  96. package/src/components/editor-2d/renderers/floorplan-voronoi-layer.tsx +128 -0
  97. package/src/components/systems/ceiling/ceiling-selection-affordance-system.tsx +425 -61
  98. package/src/components/systems/ceiling/ceiling-system.tsx +97 -4
  99. package/src/components/systems/roof/roof-edit-system.tsx +1768 -13
  100. package/src/components/systems/selection-affordance-manager.tsx +38 -0
  101. package/src/components/systems/zone/zone-label-editor-system.tsx +14 -1
  102. package/src/components/systems/zone/zone-system.tsx +42 -13
  103. package/src/components/tools/elevator/elevator-tool.tsx +34 -8
  104. package/src/components/tools/elevator/move-elevator-tool.tsx +13 -2
  105. package/src/components/tools/fence/fence-drafting.ts +107 -8
  106. package/src/components/tools/item/move-tool.tsx +17 -13
  107. package/src/components/tools/item/placement-math.test.ts +13 -1
  108. package/src/components/tools/item/placement-math.ts +28 -2
  109. package/src/components/tools/item/placement-strategies.ts +246 -5
  110. package/src/components/tools/item/placement-types.ts +13 -1
  111. package/src/components/tools/item/use-draft-node.ts +146 -57
  112. package/src/components/tools/item/use-placement-coordinator.tsx +813 -125
  113. package/src/components/tools/registry/move-registry-node-tool.tsx +636 -103
  114. package/src/components/tools/roof/roof-tool.tsx +336 -94
  115. package/src/components/tools/select/box-select-state.ts +18 -7
  116. package/src/components/tools/select/box-select-tool.tsx +176 -40
  117. package/src/components/tools/select/marquee-geometry.test.ts +161 -0
  118. package/src/components/tools/select/marquee-geometry.ts +155 -0
  119. package/src/components/tools/select/plane-box-select-tool.tsx +1 -8
  120. package/src/components/tools/select/select-candidates.ts +1 -1
  121. package/src/components/tools/shared/cursor-sphere.tsx +62 -26
  122. package/src/components/tools/shared/drag-bounding-box.tsx +28 -4
  123. package/src/components/tools/shared/facing-indicator.tsx +87 -0
  124. package/src/components/tools/shared/facing-pose-indicator.tsx +54 -0
  125. package/src/components/tools/shared/floor-stack-preview.ts +4 -0
  126. package/src/components/tools/shared/horizontal-construction-plane.ts +123 -0
  127. package/src/components/tools/shared/placement-box.tsx +183 -1
  128. package/src/components/tools/shared/pointer-support-cap.test.ts +98 -0
  129. package/src/components/tools/shared/pointer-support-cap.ts +273 -0
  130. package/src/components/tools/shared/polygon-editor.tsx +276 -35
  131. package/src/components/tools/site/site-boundary-editor.tsx +101 -42
  132. package/src/components/tools/site/stroke-pointer-ownership.test.ts +91 -0
  133. package/src/components/tools/site/stroke-pointer-ownership.ts +57 -0
  134. package/src/components/tools/site/terrain-brush-cursor.tsx +247 -0
  135. package/src/components/tools/site/terrain-sculpt-grid.tsx +192 -0
  136. package/src/components/tools/site/terrain-sculpt-tool.tsx +356 -0
  137. package/src/components/tools/stair/stair-click-guard.test.ts +58 -0
  138. package/src/components/tools/stair/stair-click-guard.ts +72 -0
  139. package/src/components/tools/stair/stair-defaults.ts +1 -0
  140. package/src/components/tools/stair/stair-tool.tsx +164 -42
  141. package/src/components/tools/tool-manager.tsx +149 -30
  142. package/src/components/tools/wall/wall-drafting.test.ts +447 -0
  143. package/src/components/tools/wall/wall-drafting.ts +304 -108
  144. package/src/components/tools/wall/wall-snap-geometry.test.ts +52 -0
  145. package/src/components/tools/wall/wall-snap-geometry.ts +137 -9
  146. package/src/components/tools/zone/zone-boundary-editor.tsx +5 -1
  147. package/src/components/tools/zone/zone-tool.tsx +98 -88
  148. package/src/components/ui/action-menu/camera-actions.tsx +24 -17
  149. package/src/components/ui/action-menu/control-modes.tsx +44 -94
  150. package/src/components/ui/action-menu/furnish-tools.tsx +5 -5
  151. package/src/components/ui/action-menu/index.tsx +2 -4
  152. package/src/components/ui/action-menu/measurement-control.tsx +267 -0
  153. package/src/components/ui/action-menu/structure-tools.tsx +22 -13
  154. package/src/components/ui/action-menu/view-toggles.tsx +42 -90
  155. package/src/components/ui/command-palette/editor-commands.tsx +16 -3
  156. package/src/components/ui/command-palette/index.tsx +4 -3
  157. package/src/components/ui/controls/material-paint-panel.tsx +93 -18
  158. package/src/components/ui/controls/material-picker.tsx +162 -152
  159. package/src/components/ui/controls/material-properties-editor.tsx +108 -0
  160. package/src/components/ui/controls/metric-control.tsx +118 -44
  161. package/src/components/ui/controls/scene-material-list.tsx +247 -0
  162. package/src/components/ui/controls/slider-control.tsx +88 -38
  163. package/src/components/ui/controls/terrain-sculpt-panel.tsx +193 -0
  164. package/src/components/ui/floating-level-selector.tsx +74 -20
  165. package/src/components/ui/helpers/building-helper.tsx +10 -23
  166. package/src/components/ui/helpers/contextual-helper-panel.tsx +428 -0
  167. package/src/components/ui/helpers/helper-manager.tsx +273 -14
  168. package/src/components/ui/helpers/item-helper.tsx +28 -32
  169. package/src/components/ui/helpers/registered-tool-helper.tsx +45 -11
  170. package/src/components/ui/helpers/roof-helper.tsx +10 -12
  171. package/src/components/ui/icon-ref.tsx +47 -0
  172. package/src/components/ui/item-catalog/catalog-items.tsx +39 -1
  173. package/src/components/ui/item-catalog/item-catalog.tsx +13 -36
  174. package/src/components/ui/level-duplicate-dialog.tsx +1 -1
  175. package/src/components/ui/panels/multi-selection-panel.tsx +57 -0
  176. package/src/components/ui/panels/node-display.ts +17 -17
  177. package/src/components/ui/panels/panel-manager.tsx +36 -14
  178. package/src/components/ui/panels/panel-wrapper.tsx +24 -3
  179. package/src/components/ui/panels/parametric-inspector.tsx +19 -4
  180. package/src/components/ui/panels/reference-panel.tsx +54 -19
  181. package/src/components/ui/panels/selection-breakdown.test.ts +24 -0
  182. package/src/components/ui/panels/selection-breakdown.ts +20 -0
  183. package/src/components/ui/primitives/shortcut-token.tsx +39 -3
  184. package/src/components/ui/primitives/sidebar.tsx +1 -0
  185. package/src/components/ui/sidebar/app-sidebar.tsx +5 -1
  186. package/src/components/ui/sidebar/icon-rail.tsx +50 -31
  187. package/src/components/ui/sidebar/panels/plugins-panel.tsx +218 -0
  188. package/src/components/ui/sidebar/panels/settings-panel/index.tsx +82 -41
  189. package/src/components/ui/sidebar/panels/settings-panel/keyboard-shortcuts-dialog.tsx +76 -6
  190. package/src/components/ui/sidebar/panels/settings-panel/load-build-dialog.tsx +12 -4
  191. package/src/components/ui/sidebar/panels/site-panel/building-tree-node.tsx +2 -1
  192. package/src/components/ui/sidebar/panels/site-panel/ceiling-tree-node.tsx +4 -3
  193. package/src/components/ui/sidebar/panels/site-panel/chimney-tree-node.tsx +10 -7
  194. package/src/components/ui/sidebar/panels/site-panel/column-tree-node.tsx +1 -1
  195. package/src/components/ui/sidebar/panels/site-panel/door-tree-node.tsx +7 -2
  196. package/src/components/ui/sidebar/panels/site-panel/dormer-tree-node.tsx +10 -7
  197. package/src/components/ui/sidebar/panels/site-panel/elevator-tree-node.tsx +1 -1
  198. package/src/components/ui/sidebar/panels/site-panel/fence-tree-node.tsx +1 -1
  199. package/src/components/ui/sidebar/panels/site-panel/gutter-tree-node.tsx +10 -7
  200. package/src/components/ui/sidebar/panels/site-panel/index.tsx +42 -17
  201. package/src/components/ui/sidebar/panels/site-panel/item-tree-node.tsx +31 -17
  202. package/src/components/ui/sidebar/panels/site-panel/level-tree-node.tsx +1 -1
  203. package/src/components/ui/sidebar/panels/site-panel/registry-tree-node.tsx +72 -25
  204. package/src/components/ui/sidebar/panels/site-panel/roof-tree-node.tsx +7 -3
  205. package/src/components/ui/sidebar/panels/site-panel/shelf-tree-node.tsx +12 -8
  206. package/src/components/ui/sidebar/panels/site-panel/slab-tree-node.tsx +4 -3
  207. package/src/components/ui/sidebar/panels/site-panel/solar-panel-tree-node.tsx +10 -7
  208. package/src/components/ui/sidebar/panels/site-panel/spawn-tree-node.tsx +1 -1
  209. package/src/components/ui/sidebar/panels/site-panel/stair-tree-node.tsx +2 -2
  210. package/src/components/ui/sidebar/panels/site-panel/tree-node.tsx +44 -3
  211. package/src/components/ui/sidebar/panels/site-panel/tree-structure.ts +36 -0
  212. package/src/components/ui/sidebar/panels/site-panel/wall-tree-node.tsx +1 -1
  213. package/src/components/ui/sidebar/panels/site-panel/window-tree-node.tsx +7 -2
  214. package/src/components/ui/sidebar/panels/site-panel/zone-tree-node.tsx +3 -3
  215. package/src/components/ui/sidebar/tab-bar.tsx +42 -28
  216. package/src/components/ui/sidebar/use-plugin-panels.tsx +132 -0
  217. package/src/components/ui/snap-target-badge.test.tsx +40 -0
  218. package/src/components/ui/snap-target-badge.tsx +88 -0
  219. package/src/components/viewer/viewer-controls-bar.tsx +466 -0
  220. package/src/components/viewer/viewer-scene-header.tsx +235 -0
  221. package/src/components/viewer-overlay.tsx +36 -596
  222. package/src/components/viewer-zone-system.tsx +6 -1
  223. package/src/components/walkthrough-hud.tsx +111 -0
  224. package/src/hooks/use-auto-save.test.ts +14 -0
  225. package/src/hooks/use-auto-save.ts +54 -7
  226. package/src/hooks/use-ceiling-events.ts +3 -2
  227. package/src/hooks/use-drag-action.ts +4 -1
  228. package/src/hooks/use-grid-events.ts +24 -0
  229. package/src/hooks/use-keyboard.ts +438 -70
  230. package/src/index.tsx +355 -9
  231. package/src/lib/active-placement-surface.test.ts +24 -0
  232. package/src/lib/active-placement-surface.ts +42 -0
  233. package/src/lib/camera-pose.test.ts +237 -0
  234. package/src/lib/camera-pose.ts +172 -0
  235. package/src/lib/ceiling-plan-snap.ts +24 -0
  236. package/src/lib/contextual-help.test.ts +112 -0
  237. package/src/lib/contextual-help.ts +144 -0
  238. package/src/lib/continuation.ts +64 -0
  239. package/src/lib/direct-manipulation.test.ts +275 -0
  240. package/src/lib/direct-manipulation.ts +118 -0
  241. package/src/lib/door-interaction.ts +1 -1
  242. package/src/lib/editor-api.ts +23 -35
  243. package/src/lib/elevation-guides.test.ts +107 -0
  244. package/src/lib/elevation-guides.ts +236 -0
  245. package/src/lib/floorplan/annotation-visibility.test.ts +327 -0
  246. package/src/lib/floorplan/annotation-visibility.ts +133 -0
  247. package/src/lib/floorplan/apply-alignment.test.ts +25 -0
  248. package/src/lib/floorplan/apply-alignment.ts +18 -12
  249. package/src/lib/floorplan/drawing-coordination.test.ts +46 -0
  250. package/src/lib/floorplan/drawing-coordination.ts +13 -0
  251. package/src/lib/floorplan/floorplan-export.test.ts +319 -0
  252. package/src/lib/floorplan/floorplan-export.tsx +978 -0
  253. package/src/lib/floorplan/floorplan-extension.test.ts +43 -0
  254. package/src/lib/floorplan/floorplan-extension.ts +181 -0
  255. package/src/lib/floorplan/floorplan-mode.test.ts +120 -0
  256. package/src/lib/floorplan/floorplan-mode.ts +73 -0
  257. package/src/lib/floorplan/floorplan-pdfkit-document.ts +194 -0
  258. package/src/lib/floorplan/floorplan-pdfkit-renderer.test.ts +107 -0
  259. package/src/lib/floorplan/floorplan-pdfkit-renderer.ts +680 -0
  260. package/src/lib/floorplan/geometry.ts +52 -0
  261. package/src/lib/floorplan/index.ts +3 -0
  262. package/src/lib/floorplan/items.ts +5 -2
  263. package/src/lib/floorplan/plan-coords.ts +21 -0
  264. package/src/lib/fresh-planar-placement.test.ts +283 -3
  265. package/src/lib/fresh-planar-placement.ts +85 -1
  266. package/src/lib/glb-export.test.ts +556 -0
  267. package/src/lib/glb-export.ts +1071 -0
  268. package/src/lib/ground-surface.ts +95 -0
  269. package/src/lib/history.test.ts +96 -0
  270. package/src/lib/history.ts +31 -0
  271. package/src/lib/interaction/hot-set.test.ts +133 -0
  272. package/src/lib/interaction/hot-set.ts +67 -0
  273. package/src/lib/interaction/overlay-policy.test.ts +52 -0
  274. package/src/lib/interaction/overlay-policy.ts +59 -0
  275. package/src/lib/interaction/scope.ts +243 -0
  276. package/src/lib/level-duplication.test.ts +2 -1
  277. package/src/lib/level-selection.ts +2 -2
  278. package/src/lib/material-paint.ts +36 -49
  279. package/src/lib/measurement-kind.test.ts +30 -0
  280. package/src/lib/measurement-kind.ts +19 -0
  281. package/src/lib/measurement-label.test.ts +47 -0
  282. package/src/lib/measurement-label.ts +68 -0
  283. package/src/lib/measurement-parser.ts +116 -0
  284. package/src/lib/measurements.test.ts +188 -0
  285. package/src/lib/measurements.ts +210 -0
  286. package/src/lib/paint-scope.test.ts +454 -0
  287. package/src/lib/paint-scope.ts +461 -0
  288. package/src/lib/placement-drag-release.ts +23 -0
  289. package/src/lib/planar-cursor-placement.test.ts +57 -0
  290. package/src/lib/plugin-panels.test.ts +19 -0
  291. package/src/lib/plugin-panels.ts +91 -0
  292. package/src/lib/quick-action-feedback.ts +33 -0
  293. package/src/lib/quick-action-nodes.test.ts +94 -0
  294. package/src/lib/quick-action-nodes.ts +47 -0
  295. package/src/lib/quick-measurement.test.ts +77 -0
  296. package/src/lib/quick-measurement.ts +109 -0
  297. package/src/lib/roof-duplication.ts +6 -6
  298. package/src/lib/roof-hover-outline-proxy.ts +22 -0
  299. package/src/lib/roof-wall-hit.ts +164 -0
  300. package/src/lib/scene-clipboard.test.ts +325 -0
  301. package/src/lib/scene-clipboard.ts +310 -21
  302. package/src/lib/scene.ts +28 -5
  303. package/src/lib/selection-routing.test.ts +313 -0
  304. package/src/lib/selection-routing.ts +180 -0
  305. package/src/lib/sfx/index.ts +1 -0
  306. package/src/lib/sfx/movement-tick.test.ts +65 -0
  307. package/src/lib/sfx/movement-tick.ts +18 -0
  308. package/src/lib/sfx-bus.ts +82 -14
  309. package/src/lib/sfx-player.test.ts +266 -0
  310. package/src/lib/sfx-player.ts +262 -69
  311. package/src/lib/site-boundary.test.ts +53 -0
  312. package/src/lib/site-boundary.ts +27 -0
  313. package/src/lib/slab-plan-snap.test.ts +93 -0
  314. package/src/lib/slab-plan-snap.ts +108 -0
  315. package/src/lib/snapping-mode.test.ts +165 -0
  316. package/src/lib/snapping-mode.ts +191 -0
  317. package/src/lib/stair-duplication.test.ts +103 -0
  318. package/src/lib/stair-duplication.ts +58 -86
  319. package/src/lib/stair-levels.ts +2 -0
  320. package/src/lib/surface-plan-snap.test.ts +71 -0
  321. package/src/lib/surface-plan-snap.ts +256 -0
  322. package/src/lib/terrain-sculpt.test.ts +429 -0
  323. package/src/lib/terrain-sculpt.ts +301 -0
  324. package/src/lib/terrain-verb-color.test.ts +51 -0
  325. package/src/lib/terrain-verb-color.ts +58 -0
  326. package/src/lib/touch-gesture-priority.test.ts +45 -0
  327. package/src/lib/touch-gesture-priority.ts +30 -0
  328. package/src/lib/use-linear-display.ts +40 -0
  329. package/src/lib/window-interaction.ts +1 -1
  330. package/src/lib/world-grid-snap.ts +37 -5
  331. package/src/store/camera-pose-store.test.ts +29 -0
  332. package/src/store/camera-pose-store.ts +27 -0
  333. package/src/store/live-draft-preview-stores.test.ts +136 -0
  334. package/src/store/navigation-sync-pose-store.test.ts +34 -0
  335. package/src/store/navigation-sync-pose-store.ts +27 -0
  336. package/src/store/terrain-sculpt-mode.test.ts +223 -0
  337. package/src/store/use-delete-confirmation.ts +27 -0
  338. package/src/store/use-direct-manipulation-feedback.ts +20 -0
  339. package/src/store/use-drawing-view.test.ts +23 -0
  340. package/src/store/use-drawing-view.ts +84 -0
  341. package/src/store/use-editor.tsx +665 -126
  342. package/src/store/use-elevation-guides.ts +40 -0
  343. package/src/store/use-facing-pose.ts +44 -0
  344. package/src/store/use-fence-curve-draft.ts +51 -0
  345. package/src/store/use-first-person-hud.ts +40 -0
  346. package/src/store/use-floorplan-annotation-visibility.ts +60 -0
  347. package/src/store/use-floorplan-draft-preview.ts +151 -0
  348. package/src/store/use-floorplan-marquee.ts +50 -0
  349. package/src/store/use-floorplan-mode.ts +110 -0
  350. package/src/store/use-interaction-scope.test.ts +206 -0
  351. package/src/store/use-interaction-scope.ts +140 -0
  352. package/src/store/use-measurement-draft.test.ts +559 -0
  353. package/src/store/use-measurement-draft.ts +570 -0
  354. package/src/store/use-opening-guides.ts +41 -0
  355. package/src/store/use-path-draft-preview.ts +90 -0
  356. package/src/store/use-placement-preview.ts +11 -3
  357. package/src/store/use-quick-measurement-hud.test.ts +53 -0
  358. package/src/store/use-quick-measurement-hud.ts +77 -0
  359. package/src/store/use-segment-draft-chain.ts +28 -0
  360. package/src/store/use-stair-build-preview.ts +53 -0
  361. package/src/store/use-wall-snap-indicator.ts +2 -0
  362. package/src/components/editor/first-person/bvh-ecctrl.tsx +0 -860
  363. package/src/components/editor/group-move-handle.tsx +0 -316
  364. package/src/components/ui/panels/paint-panel.tsx +0 -163
  365. package/src/lib/floorplan/selection-tool.ts +0 -271
  366. package/src/lib/level-name.ts +0 -11
@@ -1,16 +1,19 @@
1
1
  import { emitter, type GridEvent, sceneRegistry } from '@pascal-app/core'
2
2
  import { SCENE_LAYER, useViewer } from '@pascal-app/viewer'
3
- import { createPortal, type ThreeEvent } from '@react-three/fiber'
3
+ import { createPortal, type ThreeEvent, useThree } from '@react-three/fiber'
4
4
  import { Fragment, useCallback, useEffect, useMemo, useRef, useState } from 'react'
5
5
  import {
6
+ BoxGeometry,
6
7
  BufferGeometry,
7
8
  Color,
8
9
  CylinderGeometry,
10
+ DoubleSide,
9
11
  ExtrudeGeometry,
10
12
  Float32BufferAttribute,
11
13
  type Line,
12
14
  type Object3D,
13
15
  Shape,
16
+ Vector3,
14
17
  } from 'three'
15
18
  import { MeshBasicNodeMaterial } from 'three/webgpu'
16
19
  import { EDITOR_LAYER } from '../../../lib/constants'
@@ -20,10 +23,10 @@ import {
20
23
  ARROW_COLOR as EDGE_ARROW_COLOR,
21
24
  ARROW_HOVER_COLOR as EDGE_ARROW_HOVER_COLOR,
22
25
  ARROW_SCALE as EDGE_ARROW_SCALE,
23
- useArrowMaterial,
24
26
  useInvisibleHitAreaMaterial,
25
27
  } from '../../editor/node-arrow-handles'
26
28
  import { snapToHalf } from '../item/placement-math'
29
+ import { suppressBoxSelectForPointer } from '../select/box-select-state'
27
30
 
28
31
  const Y_OFFSET = 0.02
29
32
  // Per-side resize arrows: indigo chevrons that match the registry arrow
@@ -39,6 +42,19 @@ const EDGE_ARROW_OFFSET = 0.34
39
42
  // the `NO_RAYCAST` sentinel in node-arrow-handles.tsx.
40
43
  const NO_RAYCAST = () => null
41
44
 
45
+ // Maps the screen-space direction of an edge's outward normal to the closest
46
+ // directional resize cursor, so an edge arrow advertises the axis it will
47
+ // actually drag along regardless of camera orbit. Input is an NDC delta
48
+ // (y up); the angle is folded to [0°, 180°) since resize cursors are
49
+ // bidirectional.
50
+ function resizeCursorForScreenDirection(dx: number, dy: number): string {
51
+ const angle = ((((Math.atan2(dy, dx) * 180) / Math.PI) % 180) + 180) % 180
52
+ if (angle < 22.5 || angle >= 157.5) return 'ew-resize'
53
+ if (angle < 67.5) return 'nesw-resize'
54
+ if (angle < 112.5) return 'ns-resize'
55
+ return 'nwse-resize'
56
+ }
57
+
42
58
  function createEdgeArrowGeometry() {
43
59
  const shape = new Shape()
44
60
  shape.moveTo(0.22, 0)
@@ -77,6 +93,17 @@ type DragState = {
77
93
  pointerId: number
78
94
  }
79
95
 
96
+ export type PolygonEditorPlanPointSnapContext = {
97
+ rawPoint: [number, number]
98
+ gridPoint: [number, number]
99
+ mode: DragState['mode']
100
+ vertexIndex: number | null
101
+ edgeIndex?: number
102
+ initialPosition: [number, number]
103
+ initialPolygon: Array<[number, number]>
104
+ nativeEvent?: GridEvent['nativeEvent']
105
+ }
106
+
80
107
  export interface PolygonEditorProps {
81
108
  polygon: Array<[number, number]>
82
109
  color?: string
@@ -104,6 +131,8 @@ export interface PolygonEditorProps {
104
131
  onVertexHoverChange?: (vertexIndex: number | null) => void
105
132
  /** Called when a midpoint add-vertex handle enters or leaves hover. */
106
133
  onMidpointHoverChange?: (edgeIndex: number | null) => void
134
+ /** Called when an edge move handle enters or leaves hover. */
135
+ onEdgeHoverChange?: (edgeIndex: number | null) => void
107
136
  /** Called when any polygon drag starts or ends. */
108
137
  onDragStateChange?: (isDragging: boolean) => void
109
138
  /** Called once when a polygon drag starts. */
@@ -114,6 +143,10 @@ export interface PolygonEditorProps {
114
143
  showBorderLine?: boolean
115
144
  /** Whether midpoint handles can add new vertices. */
116
145
  showMidpointHandles?: boolean
146
+ /** Whether hovering a handle should also tint its connected edges and endpoint handles. */
147
+ highlightConnectedHandles?: boolean
148
+ /** Optional host-owned point snapper. Defaults to the existing half-grid snap. */
149
+ resolvePlanPoint?: (context: PolygonEditorPlanPointSnapContext) => [number, number]
117
150
  /** Optional vertex handle renderer for host-specific affordances. */
118
151
  renderVertexHandle?: PolygonVertexHandleRenderer
119
152
  /** Optional midpoint handle renderer for host-specific add-vertex affordances. */
@@ -127,6 +160,7 @@ export interface PolygonEditorProps {
127
160
  const MIN_HANDLE_HEIGHT = 0.15
128
161
  const EDGE_HANDLE_HEIGHT = 0.06
129
162
  const EDGE_HANDLE_THICKNESS = 0.12
163
+ const EDGE_HANDLE_GEOMETRY = new BoxGeometry(1, 1, 1)
130
164
 
131
165
  function getEdgeNormal(start: [number, number], end: [number, number]): [number, number] | null {
132
166
  const dx = end[0] - start[0]
@@ -137,6 +171,11 @@ function getEdgeNormal(start: [number, number], end: [number, number]): [number,
137
171
  return [-dz / length, dx / length]
138
172
  }
139
173
 
174
+ function stopHandlePointerDown(event: ThreeEvent<PointerEvent>) {
175
+ event.stopPropagation()
176
+ suppressBoxSelectForPointer(event, { markHandled: false })
177
+ }
178
+
140
179
  type HandleClickHandler = (event: ThreeEvent<MouseEvent>) => void
141
180
  type HandlePointerHandler = (event: ThreeEvent<PointerEvent>) => void
142
181
 
@@ -176,12 +215,16 @@ export type PolygonMidpointHandleRenderer = (
176
215
  props: PolygonMidpointHandleRenderProps,
177
216
  ) => React.ReactNode
178
217
 
179
- function usePolygonNodeMaterial(color: string, opacity = 1): MeshBasicNodeMaterial {
218
+ function usePolygonNodeMaterial(
219
+ color: string,
220
+ opacity = 1,
221
+ depthTest = true,
222
+ ): MeshBasicNodeMaterial {
180
223
  const material = useMemo(
181
224
  () =>
182
225
  new MeshBasicNodeMaterial({
183
226
  color: new Color('#ffffff'),
184
- depthTest: false,
227
+ depthTest: true,
185
228
  depthWrite: true,
186
229
  opacity: 1,
187
230
  transparent: true,
@@ -192,17 +235,31 @@ function usePolygonNodeMaterial(color: string, opacity = 1): MeshBasicNodeMateri
192
235
  useEffect(() => {
193
236
  material.color.set(color)
194
237
  material.opacity = opacity
195
- }, [color, material, opacity])
238
+ material.depthTest = depthTest
239
+ }, [color, depthTest, material, opacity])
196
240
  useEffect(() => () => material.dispose(), [material])
197
241
 
198
242
  return material
199
243
  }
200
244
 
245
+ function usePolygonArrowMaterial(): MeshBasicNodeMaterial {
246
+ return useMemo(
247
+ () =>
248
+ new MeshBasicNodeMaterial({
249
+ color: new Color(EDGE_ARROW_COLOR),
250
+ depthTest: false,
251
+ depthWrite: true,
252
+ opacity: 1,
253
+ side: DoubleSide,
254
+ transparent: true,
255
+ }),
256
+ [],
257
+ )
258
+ }
259
+
201
260
  // One mesh per handle: lives on SCENE_LAYER with a node material so the
202
- // post-processing ink-edge pass outlines it, and carries the pointer handlers
203
- // directly so it stays grabbable matching the registry arrow gizmos in
204
- // node-arrow-handles.tsx. No paired hit mesh is needed; the R3F event
205
- // raycaster picks SCENE_LAYER meshes too.
261
+ // post-processing ink-edge pass outlines it. Ignores scene depth like the
262
+ // edge arrows so the handle stays visible through walls and slabs.
206
263
  function OutlinedCylinderHandle({
207
264
  radius,
208
265
  height,
@@ -218,7 +275,7 @@ function OutlinedCylinderHandle({
218
275
  position: [number, number, number]
219
276
  } & PolygonHandleHandlers) {
220
277
  const geometry = useMemo(() => new CylinderGeometry(radius, radius, height, 16), [height, radius])
221
- const material = usePolygonNodeMaterial(color, opacity)
278
+ const material = usePolygonNodeMaterial(color, opacity, false)
222
279
  useEffect(() => () => geometry.dispose(), [geometry])
223
280
 
224
281
  return (
@@ -290,7 +347,7 @@ function OutlinedEdgeArrowHandle({
290
347
  rotationY: number
291
348
  scale: number
292
349
  } & PolygonHandleHandlers) {
293
- const material = useArrowMaterial()
350
+ const material = usePolygonArrowMaterial()
294
351
  useEffect(() => {
295
352
  material.color.set(color)
296
353
  }, [color, material])
@@ -311,6 +368,47 @@ function OutlinedEdgeArrowHandle({
311
368
  )
312
369
  }
313
370
 
371
+ function HighlightedEdgeSegment({
372
+ end,
373
+ start,
374
+ y,
375
+ }: {
376
+ end: [number, number]
377
+ start: [number, number]
378
+ y: number
379
+ }) {
380
+ const geometry = useMemo(() => {
381
+ const nextGeometry = new BufferGeometry()
382
+ nextGeometry.setAttribute(
383
+ 'position',
384
+ new Float32BufferAttribute([start[0], y, start[1], end[0], y, end[1]], 3),
385
+ )
386
+ return nextGeometry
387
+ }, [end, start, y])
388
+
389
+ useEffect(() => () => geometry.dispose(), [geometry])
390
+
391
+ return (
392
+ <line
393
+ // @ts-expect-error R3F <line> element conflicts with SVG <line> type
394
+ frustumCulled={false}
395
+ geometry={geometry}
396
+ layers={EDITOR_LAYER}
397
+ raycast={NO_RAYCAST}
398
+ renderOrder={12}
399
+ >
400
+ <lineBasicNodeMaterial
401
+ color={EDGE_ARROW_HOVER_COLOR}
402
+ depthTest
403
+ depthWrite={false}
404
+ linewidth={4}
405
+ opacity={0.95}
406
+ transparent
407
+ />
408
+ </line>
409
+ )
410
+ }
411
+
314
412
  export const PolygonEditor: React.FC<PolygonEditorProps> = ({
315
413
  polygon,
316
414
  color = '#3b82f6',
@@ -324,11 +422,14 @@ export const PolygonEditor: React.FC<PolygonEditorProps> = ({
324
422
  onBeforeVertexDrag,
325
423
  onVertexHoverChange,
326
424
  onMidpointHoverChange,
425
+ onEdgeHoverChange,
327
426
  onDragStateChange,
328
427
  onDragStart,
329
428
  onDragCommit,
330
429
  showBorderLine = true,
331
430
  showMidpointHandles = true,
431
+ highlightConnectedHandles = false,
432
+ resolvePlanPoint,
332
433
  renderMidpointHandle,
333
434
  renderVertexHandle,
334
435
  }) => {
@@ -371,6 +472,39 @@ export const PolygonEditor: React.FC<PolygonEditorProps> = ({
371
472
  // When not using portal, edit at world origin
372
473
  const editY = levelNode ? Y_OFFSET : 0
373
474
 
475
+ const camera = useThree((state) => state.camera)
476
+
477
+ // Handle-hover cursor, applied to document.body like the registry arrow
478
+ // handles (see handle-arrow.tsx). Tracked in a ref so we only ever clear a
479
+ // cursor we set ourselves.
480
+ const activeBodyCursorRef = useRef<string | null>(null)
481
+ const setBodyCursor = useCallback((cursor: string) => {
482
+ activeBodyCursorRef.current = cursor
483
+ document.body.style.cursor = cursor
484
+ }, [])
485
+ const clearBodyCursor = useCallback(() => {
486
+ if (activeBodyCursorRef.current && document.body.style.cursor === activeBodyCursorRef.current) {
487
+ document.body.style.cursor = ''
488
+ }
489
+ activeBodyCursorRef.current = null
490
+ }, [])
491
+ useEffect(() => () => clearBodyCursor(), [clearBodyCursor])
492
+
493
+ const edgeResizeCursor = useCallback(
494
+ (midpoint: [number, number], outwardNormal: [number, number]) => {
495
+ const from = new Vector3(midpoint[0], editY, midpoint[1])
496
+ const to = new Vector3(midpoint[0] + outwardNormal[0], editY, midpoint[1] + outwardNormal[1])
497
+ if (levelNode) {
498
+ levelNode.localToWorld(from)
499
+ levelNode.localToWorld(to)
500
+ }
501
+ from.project(camera)
502
+ to.project(camera)
503
+ return resizeCursorForScreenDirection(to.x - from.x, to.y - from.y)
504
+ },
505
+ [camera, editY, levelNode],
506
+ )
507
+
374
508
  // Local state for dragging
375
509
  const [dragState, setDragState] = useState<DragState | null>(null)
376
510
  const [previewPolygon, setPreviewPolygon] = useState<Array<[number, number]> | null>(null)
@@ -429,6 +563,12 @@ export const PolygonEditor: React.FC<PolygonEditorProps> = ({
429
563
 
430
564
  useEffect(() => () => onMidpointHoverChange?.(null), [onMidpointHoverChange])
431
565
 
566
+ useEffect(() => {
567
+ onEdgeHoverChange?.(hoveredEdge)
568
+ }, [hoveredEdge, onEdgeHoverChange])
569
+
570
+ useEffect(() => () => onEdgeHoverChange?.(null), [onEdgeHoverChange])
571
+
432
572
  const lineRef = useRef<Line>(null!)
433
573
  const previousPositionRef = useRef<[number, number] | null>(null)
434
574
 
@@ -556,6 +696,47 @@ export const PolygonEditor: React.FC<PolygonEditorProps> = ({
556
696
  })
557
697
  }, [displayPolygon])
558
698
 
699
+ const activeVertexIndex = dragState?.mode === 'vertex' ? dragState.vertexIndex : hoveredVertex
700
+ const activeEdgeIndex = dragState?.mode === 'edge' ? dragState.edgeIndex : hoveredEdge
701
+
702
+ const highlightedEdgeIndices = useMemo(() => {
703
+ const next = new Set<number>()
704
+ const edgeCount = displayPolygon.length
705
+ if (!highlightConnectedHandles || edgeCount < 2) return next
706
+
707
+ if (activeVertexIndex !== null && activeVertexIndex !== undefined) {
708
+ next.add(activeVertexIndex)
709
+ next.add((activeVertexIndex - 1 + edgeCount) % edgeCount)
710
+ }
711
+ if (hoveredMidpoint !== null) {
712
+ next.add(hoveredMidpoint)
713
+ }
714
+ if (activeEdgeIndex !== null && activeEdgeIndex !== undefined) {
715
+ next.add(activeEdgeIndex)
716
+ }
717
+
718
+ return next
719
+ }, [
720
+ activeEdgeIndex,
721
+ activeVertexIndex,
722
+ displayPolygon.length,
723
+ highlightConnectedHandles,
724
+ hoveredMidpoint,
725
+ ])
726
+
727
+ const isVertexLinkedHighlighted = useCallback(
728
+ (index: number) => {
729
+ if (!highlightConnectedHandles || highlightedEdgeIndices.size === 0) return false
730
+ const edgeCount = displayPolygon.length
731
+ if (edgeCount < 2) return false
732
+ return (
733
+ highlightedEdgeIndices.has(index) ||
734
+ highlightedEdgeIndices.has((index - 1 + edgeCount) % edgeCount)
735
+ )
736
+ },
737
+ [displayPolygon.length, highlightConnectedHandles, highlightedEdgeIndices],
738
+ )
739
+
559
740
  const arrowGeometry = useMemo(() => createEdgeArrowGeometry(), [])
560
741
  useEffect(() => () => arrowGeometry.dispose(), [arrowGeometry])
561
742
 
@@ -578,7 +759,8 @@ export const PolygonEditor: React.FC<PolygonEditorProps> = ({
578
759
  onDragCommitRef.current?.()
579
760
  updatePreviewPolygon(null)
580
761
  setDragState(null)
581
- }, [onPolygonChange, updatePreviewPolygon])
762
+ clearBodyCursor()
763
+ }, [clearBodyCursor, onPolygonChange, updatePreviewPolygon])
582
764
 
583
765
  // Handle adding a new vertex at midpoint
584
766
  const handleAddVertex = useCallback(
@@ -616,9 +798,23 @@ export const PolygonEditor: React.FC<PolygonEditorProps> = ({
616
798
  useEffect(() => {
617
799
  const onGridMove = (event: GridEvent) => {
618
800
  const point = levelNode ? event.localPosition : event.position
619
- const gridX = snapToHalf(point[0])
620
- const gridZ = snapToHalf(point[2])
621
- const newPosition: [number, number] = [gridX, gridZ]
801
+ const rawPoint: [number, number] = [point[0], point[2]]
802
+ // Snapping follows the active mode (snapToHalf returns raw in Off / non-grid);
803
+ // no Shift bypass Shift cycles the mode, Off is the bypass.
804
+ const gridPoint: [number, number] = [snapToHalf(rawPoint[0]), snapToHalf(rawPoint[1])]
805
+ const newPosition =
806
+ dragState?.isDragging && resolvePlanPoint
807
+ ? resolvePlanPoint({
808
+ rawPoint,
809
+ gridPoint,
810
+ mode: dragState.mode,
811
+ vertexIndex: dragState.vertexIndex,
812
+ edgeIndex: dragState.edgeIndex,
813
+ initialPosition: dragState.initialPosition,
814
+ initialPolygon: dragState.initialPolygon,
815
+ nativeEvent: event.nativeEvent,
816
+ })
817
+ : gridPoint
622
818
 
623
819
  // Play snap sound when cursor moves to a new grid cell during drag
624
820
  if (
@@ -673,7 +869,7 @@ export const PolygonEditor: React.FC<PolygonEditorProps> = ({
673
869
  return () => {
674
870
  emitter.off('grid:move', onGridMove)
675
871
  }
676
- }, [dragState, handleVertexDrag, levelNode, updatePreviewPolygon])
872
+ }, [dragState, handleVertexDrag, levelNode, resolvePlanPoint, updatePreviewPolygon])
677
873
 
678
874
  // Set up pointer up listener for ending drag
679
875
  useEffect(() => {
@@ -741,9 +937,10 @@ export const PolygonEditor: React.FC<PolygonEditorProps> = ({
741
937
  const handleHeight = Math.max(MIN_HANDLE_HEIGHT, surfaceHeight + 0.02)
742
938
  const edgeHandleY = editY + handleHeight - EDGE_HANDLE_HEIGHT / 2
743
939
 
744
- // Interactive handles are single SCENE_LAYER node-material meshes (like the
745
- // registry arrow gizmos) so the ink-edge pass outlines them while they stay
746
- // grabbable. The edge BAR and border line stay on EDITOR_LAYER, visual-only
940
+ // Interactive handles are SCENE_LAYER node-material meshes so the ink-edge
941
+ // pass outlines them. Edge arrows ignore scene depth so the active resize
942
+ // affordance remains visible through walls and slabs. The edge BAR and border
943
+ // line stay on EDITOR_LAYER, visual-only
747
944
  // (raycast disabled) so they never steal clicks from the vertex/midpoint
748
945
  // handles overlapping them — edge dragging starts from the chevron arrow
749
946
  // outside the polygon edge.
@@ -771,11 +968,32 @@ export const PolygonEditor: React.FC<PolygonEditorProps> = ({
771
968
  </line>
772
969
  )}
773
970
 
774
- {/* Vertex handles - blue cylinders that match surface height */}
971
+ {highlightConnectedHandles &&
972
+ highlightedEdgeIndices.size > 0 &&
973
+ Array.from(highlightedEdgeIndices).map((edgeIndex) => {
974
+ const start = displayPolygon[edgeIndex]
975
+ const end = displayPolygon[(edgeIndex + 1) % displayPolygon.length]
976
+ if (!(start && end)) return null
977
+ return (
978
+ <HighlightedEdgeSegment
979
+ end={end}
980
+ key={`highlight-edge-${edgeIndex}`}
981
+ start={start}
982
+ y={edgeHandleY}
983
+ />
984
+ )
985
+ })}
986
+
987
+ {/* Vertex handles - blue cylinders that match surface height. During a
988
+ drag only the active handle stays mounted so the gesture reads
989
+ clearly (same rule for the cross / edge handles below). */}
775
990
  {displayPolygon.map(([x, z], index) => {
776
991
  const isHovered = hoveredVertex === index
777
992
  const isDragging = dragState?.mode === 'vertex' && dragState.vertexIndex === index
778
- const radius = 0.1
993
+ if (dragState && !isDragging) return null
994
+ const isLinkedHighlighted = isVertexLinkedHighlighted(index)
995
+ const isHighlighted = isDragging || isHovered || isLinkedHighlighted
996
+ const radius = 0.08
779
997
  const height = handleHeight
780
998
  const point: [number, number] = [x!, z!]
781
999
  const position: [number, number, number] = [x!, editY + height / 2, z!]
@@ -793,7 +1011,7 @@ export const PolygonEditor: React.FC<PolygonEditorProps> = ({
793
1011
  },
794
1012
  onPointerDown: (e) => {
795
1013
  if (e.button !== 0) return
796
- e.stopPropagation()
1014
+ stopHandlePointerDown(e)
797
1015
  setHoveredEdge(null)
798
1016
  onBeforeVertexDrag?.(index, point)
799
1017
  startDrag({
@@ -808,10 +1026,14 @@ export const PolygonEditor: React.FC<PolygonEditorProps> = ({
808
1026
  onPointerEnter: (e) => {
809
1027
  e.stopPropagation()
810
1028
  setHoveredVertex(index)
1029
+ setBodyCursor('move')
811
1030
  },
812
1031
  onPointerLeave: (e) => {
813
1032
  e.stopPropagation()
814
1033
  setHoveredVertex(null)
1034
+ // Keep the cursor for the whole gesture when the pointer slips
1035
+ // off the handle mid-drag; commit clears it.
1036
+ if (!dragState?.isDragging) clearBodyCursor()
815
1037
  },
816
1038
  }
817
1039
 
@@ -835,7 +1057,7 @@ export const PolygonEditor: React.FC<PolygonEditorProps> = ({
835
1057
 
836
1058
  return (
837
1059
  <OutlinedCylinderHandle
838
- color={isDragging || isHovered ? EDGE_ARROW_HOVER_COLOR : EDGE_ARROW_COLOR}
1060
+ color={isHighlighted ? EDGE_ARROW_HOVER_COLOR : EDGE_ARROW_COLOR}
839
1061
  height={height}
840
1062
  key={`vertex-${index}`}
841
1063
  {...handleProps}
@@ -845,7 +1067,7 @@ export const PolygonEditor: React.FC<PolygonEditorProps> = ({
845
1067
  )
846
1068
  })}
847
1069
 
848
- {allowPolygonMove && (
1070
+ {allowPolygonMove && (!dragState || dragState.mode === 'polygon') && (
849
1071
  <OutlinedCrossHandle
850
1072
  color={dragState?.mode === 'polygon' ? EDGE_ARROW_HOVER_COLOR : EDGE_ARROW_COLOR}
851
1073
  onClick={(e) => {
@@ -854,7 +1076,7 @@ export const PolygonEditor: React.FC<PolygonEditorProps> = ({
854
1076
  }}
855
1077
  onPointerDown={(e) => {
856
1078
  if (e.button !== 0) return
857
- e.stopPropagation()
1079
+ stopHandlePointerDown(e)
858
1080
  setHoveredEdge(null)
859
1081
  startDrag({
860
1082
  isDragging: true,
@@ -865,14 +1087,24 @@ export const PolygonEditor: React.FC<PolygonEditorProps> = ({
865
1087
  pointerId: e.pointerId,
866
1088
  })
867
1089
  }}
1090
+ onPointerEnter={(e) => {
1091
+ e.stopPropagation()
1092
+ setBodyCursor('move')
1093
+ }}
1094
+ onPointerLeave={(e) => {
1095
+ e.stopPropagation()
1096
+ if (!dragState?.isDragging) clearBodyCursor()
1097
+ }}
868
1098
  position={[polygonCenter[0], editY + handleHeight + 0.08, polygonCenter[1]]}
869
1099
  />
870
1100
  )}
871
-
872
1101
  {allowEdgeMove &&
873
1102
  edgeHandles.map(({ index, length, midpoint, rotationY, outwardNormal, outwardAngle }) => {
874
1103
  const isHovered = hoveredEdge === index
875
1104
  const isDragging = dragState?.mode === 'edge' && dragState.edgeIndex === index
1105
+ if (dragState && !isDragging) return null
1106
+ const isLinkedHighlighted = highlightedEdgeIndices.has(index)
1107
+ const isHighlighted = isDragging || isHovered || isLinkedHighlighted
876
1108
  const arrowX = midpoint[0] + outwardNormal[0] * EDGE_ARROW_OFFSET
877
1109
  const arrowZ = midpoint[1] + outwardNormal[1] * EDGE_ARROW_OFFSET
878
1110
 
@@ -906,15 +1138,16 @@ export const PolygonEditor: React.FC<PolygonEditorProps> = ({
906
1138
  which sits outside the polygon and never overlaps a
907
1139
  vertex/midpoint handle. */}
908
1140
  <mesh
1141
+ geometry={EDGE_HANDLE_GEOMETRY}
909
1142
  layers={EDITOR_LAYER}
910
1143
  position={[midpoint[0], edgeHandleY, midpoint[1]]}
911
1144
  raycast={NO_RAYCAST}
912
1145
  rotation={[0, rotationY, 0]}
1146
+ scale={[length, EDGE_HANDLE_HEIGHT, EDGE_HANDLE_THICKNESS]}
913
1147
  >
914
- <boxGeometry args={[length, EDGE_HANDLE_HEIGHT, EDGE_HANDLE_THICKNESS]} />
915
1148
  <meshBasicMaterial
916
- color={isDragging ? EDGE_ARROW_HOVER_COLOR : EDGE_ARROW_COLOR}
917
- opacity={isDragging ? 0.5 : isHovered ? 0.38 : 0.14}
1149
+ color={isHighlighted ? EDGE_ARROW_HOVER_COLOR : EDGE_ARROW_COLOR}
1150
+ opacity={isDragging ? 0.5 : isHighlighted ? 0.38 : 0.14}
918
1151
  transparent
919
1152
  />
920
1153
  </mesh>
@@ -922,7 +1155,7 @@ export const PolygonEditor: React.FC<PolygonEditorProps> = ({
922
1155
  Points outward from the edge; dragging it translates only this
923
1156
  edge's two vertices along the outward normal. */}
924
1157
  <OutlinedEdgeArrowHandle
925
- color={isDragging || isHovered ? EDGE_ARROW_HOVER_COLOR : EDGE_ARROW_COLOR}
1158
+ color={isHighlighted ? EDGE_ARROW_HOVER_COLOR : EDGE_ARROW_COLOR}
926
1159
  geometry={arrowGeometry}
927
1160
  onClick={(e) => {
928
1161
  if (e.button !== 0) return
@@ -930,16 +1163,18 @@ export const PolygonEditor: React.FC<PolygonEditorProps> = ({
930
1163
  }}
931
1164
  onPointerDown={(e) => {
932
1165
  if (e.button !== 0) return
933
- e.stopPropagation()
1166
+ stopHandlePointerDown(e)
934
1167
  beginEdgeDrag(e)
935
1168
  }}
936
1169
  onPointerEnter={(e) => {
937
1170
  e.stopPropagation()
938
1171
  setHoveredEdge(index)
1172
+ setBodyCursor(edgeResizeCursor(midpoint, outwardNormal))
939
1173
  }}
940
1174
  onPointerLeave={(e) => {
941
1175
  e.stopPropagation()
942
1176
  setHoveredEdge(null)
1177
+ if (!dragState?.isDragging) clearBodyCursor()
943
1178
  }}
944
1179
  position={[arrowX, edgeHandleY, arrowZ]}
945
1180
  rotationY={outwardAngle}
@@ -954,7 +1189,9 @@ export const PolygonEditor: React.FC<PolygonEditorProps> = ({
954
1189
  !dragState &&
955
1190
  midpoints.map(([x, z], index) => {
956
1191
  const isHovered = hoveredMidpoint === index
957
- const radius = 0.06
1192
+ const isLinkedHighlighted = highlightedEdgeIndices.has(index)
1193
+ const isHighlighted = isHovered || isLinkedHighlighted
1194
+ const radius = 0.05
958
1195
  const height = handleHeight
959
1196
  const point: [number, number] = [x!, z!]
960
1197
  const position: [number, number, number] = [x!, editY + height / 2, z!]
@@ -965,7 +1202,7 @@ export const PolygonEditor: React.FC<PolygonEditorProps> = ({
965
1202
  },
966
1203
  onPointerDown: (e) => {
967
1204
  if (e.button !== 0) return
968
- e.stopPropagation()
1205
+ stopHandlePointerDown(e)
969
1206
  onBeforeVertexDrag?.(index + 1, point)
970
1207
  const insertedVertex = handleAddVertex(index, point)
971
1208
  if (insertedVertex.vertexIndex >= 0) {
@@ -983,10 +1220,12 @@ export const PolygonEditor: React.FC<PolygonEditorProps> = ({
983
1220
  onPointerEnter: (e) => {
984
1221
  e.stopPropagation()
985
1222
  setHoveredMidpoint(index)
1223
+ setBodyCursor('move')
986
1224
  },
987
1225
  onPointerLeave: (e) => {
988
1226
  e.stopPropagation()
989
1227
  setHoveredMidpoint(null)
1228
+ clearBodyCursor()
990
1229
  },
991
1230
  }
992
1231
 
@@ -1008,11 +1247,13 @@ export const PolygonEditor: React.FC<PolygonEditorProps> = ({
1008
1247
 
1009
1248
  return (
1010
1249
  <OutlinedCylinderHandle
1011
- color={isHovered ? EDGE_ARROW_HOVER_COLOR : EDGE_ARROW_COLOR}
1250
+ // Midpoints read as secondary add-points: the brighter hover
1251
+ // shade at rest distinguishes them from the vertex handles.
1252
+ color={EDGE_ARROW_HOVER_COLOR}
1012
1253
  height={height}
1013
1254
  key={`midpoint-${index}`}
1014
1255
  {...handleProps}
1015
- opacity={isHovered ? 1 : 0.7}
1256
+ opacity={isHighlighted ? 1 : 0.7}
1016
1257
  position={position}
1017
1258
  radius={radius}
1018
1259
  />