@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
@@ -3,41 +3,107 @@
3
3
  import {
4
4
  type CeilingNode,
5
5
  emitter,
6
+ resolveCeilingHeight,
6
7
  resolveLevelId,
7
8
  sceneRegistry,
9
+ snapPointToGrid,
10
+ useLiveNodeOverrides,
8
11
  useScene,
9
12
  } from '@pascal-app/core'
10
13
  import { useViewer } from '@pascal-app/viewer'
11
- import { createPortal, type ThreeEvent } from '@react-three/fiber'
12
- import { useEffect, useMemo, useState } from 'react'
13
- import type { Object3D } from 'three'
14
+ import { createPortal, type ThreeEvent, useThree } from '@react-three/fiber'
15
+ import { useCallback, useEffect, useMemo, useRef, useState } from 'react'
16
+ import { BoxGeometry, type Group, type Object3D, Plane, Raycaster, Vector2, Vector3 } from 'three'
14
17
  import { useShallow } from 'zustand/react/shallow'
15
- import useEditor from '../../../store/use-editor'
18
+ import {
19
+ clearCeilingSnapFeedback,
20
+ resolveCeilingPlanPointSnap,
21
+ } from '../../../lib/ceiling-plan-snap'
22
+ import { sfxEmitter } from '../../../lib/sfx-bus'
23
+ import useEditor, { isGridSnapActive } from '../../../store/use-editor'
24
+ import useInteractionScope from '../../../store/use-interaction-scope'
25
+ import { suppressBoxSelectForPointer } from '../../tools/select/box-select-state'
16
26
 
17
27
  const BRACKET_THICKNESS = 0.04
18
28
  const BRACKET_HEIGHT = 0.04
19
29
  const BRACKET_Y_OFFSET = 0.035
20
30
  const HIT_BOX_SIZE: [number, number, number] = [0.28, 0.08, 0.28]
21
- // Draw the corner handles after everything else and with depth testing
22
- // off (see materials below) so they stay visible — and clickable — even
23
- // when a wall, roof, or the ceiling itself would otherwise occlude them.
31
+ const HANDLE_COLOR = '#d4d4d4'
32
+ const HANDLE_HOVER_COLOR = '#818cf8'
33
+ const HANDLE_OPACITY = 0.72
34
+ const HANDLE_HOVER_OPACITY = 0.92
35
+ const HANDLE_DRAG_THRESHOLD_PX = 4
36
+ const SHARED_HANDLE_BOX_GEOMETRY = new BoxGeometry(1, 1, 1)
37
+ // Draw the corner handles after the ceiling surface so they read cleanly
38
+ // when unobstructed, while material depth testing still lets other scene
39
+ // geometry hide them.
24
40
  const CORNER_RENDER_ORDER = 1000
25
41
 
26
42
  type CornerBracketData = {
27
43
  corner: [number, number]
44
+ index: number
45
+ incomingEdgeIndex: number
28
46
  incomingDirection: [number, number]
47
+ outgoingEdgeIndex: number
29
48
  outgoingDirection: [number, number]
30
49
  incomingLength: number
31
50
  outgoingLength: number
32
- cornerStrength: number
51
+ }
52
+
53
+ type CornerDragState = {
54
+ ceilingId: CeilingNode['id']
55
+ cornerIndex: number
56
+ didDrag: boolean
57
+ initialPolygon: Array<[number, number]>
58
+ inputDraggingSet: boolean
59
+ pointerId: number
60
+ previewPolygon: Array<[number, number]> | null
61
+ previousSnappedPosition: [number, number] | null
62
+ previousInputDragging: boolean
63
+ startClientX: number
64
+ startClientY: number
65
+ startPlanePosition: [number, number]
66
+ }
67
+
68
+ function stopHandlePointerDown(event: ThreeEvent<PointerEvent>) {
69
+ event.stopPropagation()
70
+ suppressBoxSelectForPointer(event, { markHandled: false })
71
+ }
72
+
73
+ function suppressNextClick() {
74
+ const suppressClick = (clickEvent: MouseEvent) => {
75
+ clickEvent.stopImmediatePropagation()
76
+ clickEvent.preventDefault()
77
+ window.removeEventListener('click', suppressClick, true)
78
+ }
79
+ window.addEventListener('click', suppressClick, true)
80
+ requestAnimationFrame(() => {
81
+ window.removeEventListener('click', suppressClick, true)
82
+ })
83
+ }
84
+
85
+ function clearCornerDragPreview(drag: CornerDragState) {
86
+ if (drag.didDrag) {
87
+ useLiveNodeOverrides.getState().clear(drag.ceilingId)
88
+ useScene.getState().markDirty(drag.ceilingId)
89
+ }
90
+ if (drag.inputDraggingSet) {
91
+ useViewer.getState().setInputDragging(drag.previousInputDragging)
92
+ }
93
+ clearCeilingSnapFeedback()
33
94
  }
34
95
 
35
96
  export const CeilingSelectionAffordanceSystem = () => {
36
97
  const phase = useEditor((state) => state.phase)
37
98
  const mode = useEditor((state) => state.mode)
38
99
  const structureLayer = useEditor((state) => state.structureLayer)
39
- const movingNode = useEditor((state) => state.movingNode)
40
- const curvingWall = useEditor((state) => state.curvingWall)
100
+ // ANY active interaction (moving/placing a node, reshaping a boundary or
101
+ // curve, dragging a handle) unmounts the brackets: their ceiling-height hit
102
+ // boxes would otherwise catch drag-time hover, set `hoveredId` to the
103
+ // ceiling, and flash the ceiling grid mid-gesture (e.g. while dragging a
104
+ // slab polygon vertex). The brackets' own corner drag doesn't begin a
105
+ // scope, so it can't unmount itself.
106
+ const scopeIdle = useInteractionScope((state) => state.scope.kind === 'idle')
41
107
  const currentLevelId = useViewer((state) => state.selection.levelId)
42
108
 
43
109
  const ceilings = useScene(
@@ -57,8 +123,7 @@ export const CeilingSelectionAffordanceSystem = () => {
57
123
  phase === 'structure' &&
58
124
  mode === 'select' &&
59
125
  structureLayer === 'elements' &&
60
- !movingNode &&
61
- !curvingWall &&
126
+ scopeIdle &&
62
127
  currentLevelId !== null
63
128
 
64
129
  if (!shouldRender) return null
@@ -79,11 +144,274 @@ const CeilingSelectionAffordance = ({
79
144
  ceiling: CeilingNode
80
145
  levelId: string
81
146
  }) => {
147
+ const { camera, gl } = useThree()
148
+ const liveOverride = useLiveNodeOverrides(
149
+ (state) => state.overrides.get(ceiling.id) as Partial<CeilingNode> | undefined,
150
+ )
151
+ const effectiveCeiling = useMemo(
152
+ () => (liveOverride ? ({ ...ceiling, ...liveOverride } as CeilingNode) : ceiling),
153
+ [ceiling, liveOverride],
154
+ )
155
+ // Explicit height when stored, else the live level-top bound the ceiling
156
+ // follows (primitive selector — re-render-safe).
157
+ const resolvedHeight = useScene((s) => resolveCeilingHeight(effectiveCeiling, s.nodes))
82
158
  const [levelObject, setLevelObject] = useState<Object3D | null>(
83
159
  () => sceneRegistry.nodes.get(levelId) ?? null,
84
160
  )
161
+ const [hoveredCornerIndex, setHoveredCornerIndex] = useState<number | null>(null)
162
+ const [draggedCornerIndex, setDraggedCornerIndex] = useState<number | null>(null)
163
+ const [previewPolygon, setPreviewPolygon] = useState<Array<[number, number]> | null>(null)
164
+ const dragRef = useRef<CornerDragState | null>(null)
165
+ const bracketsRootRef = useRef<Group>(null)
166
+ const raycasterRef = useRef(new Raycaster())
167
+ const ndcRef = useRef(new Vector2())
168
+ const planeRef = useRef(new Plane())
169
+ const planePointRef = useRef(new Vector3())
170
+ const planeNormalRef = useRef(new Vector3())
171
+ const planeOriginRef = useRef(new Vector3())
172
+ const intersectionRef = useRef(new Vector3())
173
+ const localIntersectionRef = useRef(new Vector3())
174
+
175
+ const displayPolygon = previewPolygon ?? effectiveCeiling.polygon
176
+ const activeCornerIndex = draggedCornerIndex ?? hoveredCornerIndex
177
+ const corners = useMemo(() => buildCornerBrackets(displayPolygon), [displayPolygon])
178
+ const highlightedEdgeIndices = useMemo(() => {
179
+ const next = new Set<number>()
180
+ if (activeCornerIndex === null || displayPolygon.length < 2) return next
181
+ next.add(activeCornerIndex)
182
+ next.add((activeCornerIndex - 1 + displayPolygon.length) % displayPolygon.length)
183
+ return next
184
+ }, [activeCornerIndex, displayPolygon.length])
185
+ const highlightedCornerIndices = useMemo(() => {
186
+ const next = new Set<number>()
187
+ if (activeCornerIndex === null || displayPolygon.length < 2) return next
188
+ next.add(activeCornerIndex)
189
+ next.add((activeCornerIndex - 1 + displayPolygon.length) % displayPolygon.length)
190
+ next.add((activeCornerIndex + 1) % displayPolygon.length)
191
+ return next
192
+ }, [activeCornerIndex, displayPolygon.length])
85
193
 
86
- const corners = useMemo(() => buildCornerBrackets(ceiling.polygon), [ceiling.polygon])
194
+ useEffect(() => {
195
+ if (activeCornerIndex === null) return
196
+
197
+ useViewer.getState().setHoveredId(effectiveCeiling.id)
198
+ return () => {
199
+ if (useViewer.getState().hoveredId === effectiveCeiling.id) {
200
+ useViewer.getState().setHoveredId(null)
201
+ }
202
+ }
203
+ }, [activeCornerIndex, effectiveCeiling.id])
204
+
205
+ const selectCeilingForEdit = useCallback(() => {
206
+ const editor = useEditor.getState()
207
+ editor.setMovingNode(null)
208
+ useInteractionScope
209
+ .getState()
210
+ .endIf((sc) => sc.kind === 'reshaping' && sc.reshape === 'endpoint')
211
+ useInteractionScope.getState().endIf((sc) => sc.kind === 'reshaping' && sc.reshape === 'curve')
212
+ useInteractionScope.getState().endIf((sc) => sc.kind === 'reshaping' && sc.reshape === 'hole')
213
+ editor.setMode('select')
214
+ useViewer.getState().setSelection({ selectedIds: [effectiveCeiling.id] })
215
+ }, [effectiveCeiling.id])
216
+
217
+ const getHandlePlanePoint = useCallback(
218
+ (event: MouseEvent | PointerEvent): [number, number] | null => {
219
+ if (!levelObject) return null
220
+
221
+ const rect = gl.domElement.getBoundingClientRect()
222
+ ndcRef.current.set(
223
+ ((event.clientX - rect.left) / rect.width) * 2 - 1,
224
+ -((event.clientY - rect.top) / rect.height) * 2 + 1,
225
+ )
226
+ raycasterRef.current.setFromCamera(ndcRef.current, camera)
227
+
228
+ planePointRef.current.set(0, resolvedHeight + BRACKET_Y_OFFSET, 0)
229
+ levelObject.localToWorld(planePointRef.current)
230
+
231
+ planeOriginRef.current.set(0, 0, 0)
232
+ levelObject.localToWorld(planeOriginRef.current)
233
+ planeNormalRef.current.set(0, 1, 0)
234
+ levelObject.localToWorld(planeNormalRef.current)
235
+ planeNormalRef.current.sub(planeOriginRef.current).normalize()
236
+ planeRef.current.setFromNormalAndCoplanarPoint(planeNormalRef.current, planePointRef.current)
237
+
238
+ const hit = raycasterRef.current.ray.intersectPlane(planeRef.current, intersectionRef.current)
239
+ if (!hit) return null
240
+
241
+ localIntersectionRef.current.copy(intersectionRef.current)
242
+ levelObject.worldToLocal(localIntersectionRef.current)
243
+ return [localIntersectionRef.current.x, localIntersectionRef.current.z]
244
+ },
245
+ [camera, resolvedHeight, gl.domElement, levelObject],
246
+ )
247
+
248
+ const handleCornerPointerDown = useCallback(
249
+ (corner: CornerBracketData, event: ThreeEvent<PointerEvent>) => {
250
+ if (event.button !== 0) return
251
+ stopHandlePointerDown(event)
252
+
253
+ const startPlanePosition = getHandlePlanePoint(event.nativeEvent)
254
+ if (!startPlanePosition) return
255
+ const initialCorner = effectiveCeiling.polygon[corner.index]
256
+ if (!initialCorner) return
257
+
258
+ dragRef.current = {
259
+ ceilingId: effectiveCeiling.id,
260
+ cornerIndex: corner.index,
261
+ didDrag: false,
262
+ initialPolygon: effectiveCeiling.polygon.map(([x, z]) => [x, z] as [number, number]),
263
+ inputDraggingSet: false,
264
+ pointerId: event.pointerId,
265
+ previewPolygon: null,
266
+ previousSnappedPosition: [initialCorner[0], initialCorner[1]],
267
+ previousInputDragging: useViewer.getState().inputDragging,
268
+ startClientX: event.nativeEvent.clientX,
269
+ startClientY: event.nativeEvent.clientY,
270
+ startPlanePosition,
271
+ }
272
+ },
273
+ [effectiveCeiling.id, effectiveCeiling.polygon, getHandlePlanePoint],
274
+ )
275
+
276
+ useEffect(() => {
277
+ const handlePointerMove = (event: PointerEvent) => {
278
+ const drag = dragRef.current
279
+ if (!drag || drag.ceilingId !== effectiveCeiling.id) return
280
+ if (event.pointerId !== drag.pointerId) return
281
+
282
+ const dragDistance = Math.hypot(
283
+ event.clientX - drag.startClientX,
284
+ event.clientY - drag.startClientY,
285
+ )
286
+
287
+ const planePosition = getHandlePlanePoint(event)
288
+ if (!planePosition) return
289
+
290
+ if (!drag.didDrag) {
291
+ if (dragDistance < HANDLE_DRAG_THRESHOLD_PX) return
292
+
293
+ drag.didDrag = true
294
+ drag.inputDraggingSet = true
295
+ useViewer.getState().setInputDragging(true)
296
+ setDraggedCornerIndex(drag.cornerIndex)
297
+ selectCeilingForEdit()
298
+ sfxEmitter.emit('sfx:item-pick')
299
+ }
300
+
301
+ const initialCorner = drag.initialPolygon[drag.cornerIndex]
302
+ if (!initialCorner) return
303
+
304
+ const rawNextPosition: [number, number] = [
305
+ initialCorner[0] + (planePosition[0] - drag.startPlanePosition[0]),
306
+ initialCorner[1] + (planePosition[1] - drag.startPlanePosition[1]),
307
+ ]
308
+ const rawDelta: [number, number] = [
309
+ planePosition[0] - drag.startPlanePosition[0],
310
+ planePosition[1] - drag.startPlanePosition[1],
311
+ ]
312
+ const gridStep = isGridSnapActive() ? useEditor.getState().gridSnapStep : 0
313
+ const snappedDelta = snapPointToGrid(rawDelta, gridStep)
314
+ const gridNextPosition: [number, number] = [
315
+ initialCorner[0] + snappedDelta[0],
316
+ initialCorner[1] + snappedDelta[1],
317
+ ]
318
+ const nextPosition = resolveCeilingPlanPointSnap({
319
+ rawPoint: rawNextPosition,
320
+ fallbackPoint: gridNextPosition,
321
+ levelId,
322
+ excludeId: drag.ceilingId,
323
+ }).point
324
+
325
+ const snapEpsilon = 1e-6
326
+ const alignmentSnapped =
327
+ Math.abs(nextPosition[0] - gridNextPosition[0]) > snapEpsilon ||
328
+ Math.abs(nextPosition[1] - gridNextPosition[1]) > snapEpsilon
329
+ if (
330
+ (gridStep > 0 || alignmentSnapped) &&
331
+ drag.previousSnappedPosition &&
332
+ (nextPosition[0] !== drag.previousSnappedPosition[0] ||
333
+ nextPosition[1] !== drag.previousSnappedPosition[1])
334
+ ) {
335
+ sfxEmitter.emit('sfx:grid-snap')
336
+ }
337
+ drag.previousSnappedPosition = nextPosition
338
+
339
+ const nextPolygon = drag.initialPolygon.map((polygonPoint, index) =>
340
+ index === drag.cornerIndex ? nextPosition : polygonPoint,
341
+ )
342
+
343
+ drag.previewPolygon = nextPolygon
344
+ setPreviewPolygon(nextPolygon)
345
+ useLiveNodeOverrides.getState().set(drag.ceilingId, { polygon: nextPolygon })
346
+ useScene.getState().markDirty(drag.ceilingId)
347
+ }
348
+
349
+ const finishDrag = (event: PointerEvent) => {
350
+ const drag = dragRef.current
351
+ if (!drag || event.pointerId !== drag.pointerId) return
352
+
353
+ dragRef.current = null
354
+ setDraggedCornerIndex(null)
355
+ setPreviewPolygon(null)
356
+
357
+ if (drag.didDrag) {
358
+ event.preventDefault()
359
+ suppressNextClick()
360
+
361
+ if (drag.previewPolygon) {
362
+ useScene.getState().updateNode(drag.ceilingId, { polygon: drag.previewPolygon })
363
+ useViewer.getState().setSelection({ selectedIds: [drag.ceilingId] })
364
+ }
365
+
366
+ sfxEmitter.emit('sfx:item-place')
367
+ }
368
+
369
+ clearCornerDragPreview(drag)
370
+ }
371
+
372
+ const cancelDrag = (event: PointerEvent) => {
373
+ const drag = dragRef.current
374
+ if (!drag || event.pointerId !== drag.pointerId) return
375
+
376
+ dragRef.current = null
377
+ setDraggedCornerIndex(null)
378
+ setPreviewPolygon(null)
379
+ clearCornerDragPreview(drag)
380
+ }
381
+
382
+ window.addEventListener('pointermove', handlePointerMove)
383
+ window.addEventListener('pointerup', finishDrag, true)
384
+ window.addEventListener('pointercancel', cancelDrag, true)
385
+ return () => {
386
+ window.removeEventListener('pointermove', handlePointerMove)
387
+ window.removeEventListener('pointerup', finishDrag, true)
388
+ window.removeEventListener('pointercancel', cancelDrag, true)
389
+
390
+ const drag = dragRef.current
391
+ if (!drag || drag.ceilingId !== effectiveCeiling.id) return
392
+ dragRef.current = null
393
+ clearCornerDragPreview(drag)
394
+ }
395
+ }, [effectiveCeiling.id, getHandlePlanePoint, levelId, selectCeilingForEdit])
396
+
397
+ // The brackets render on SCENE_LAYER (scene-depth occlusion), so unlike
398
+ // EDITOR_LAYER affordances the thumbnail camera can't filter them — hide
399
+ // them around captures via synchronous Object3D.visible mutation (the
400
+ // capture renders right after the emit), same as `site-boundary-editor.tsx`.
401
+ useEffect(() => {
402
+ const hideForCapture = () => {
403
+ if (bracketsRootRef.current) bracketsRootRef.current.visible = false
404
+ }
405
+ const restoreAfterCapture = () => {
406
+ if (bracketsRootRef.current) bracketsRootRef.current.visible = true
407
+ }
408
+ emitter.on('thumbnail:before-capture', hideForCapture)
409
+ emitter.on('thumbnail:after-capture', restoreAfterCapture)
410
+ return () => {
411
+ emitter.off('thumbnail:before-capture', hideForCapture)
412
+ emitter.off('thumbnail:after-capture', restoreAfterCapture)
413
+ }
414
+ }, [])
87
415
 
88
416
  useEffect(() => {
89
417
  let frameId = 0
@@ -114,9 +442,28 @@ const CeilingSelectionAffordance = ({
114
442
  if (!levelObject || corners.length === 0) return null
115
443
 
116
444
  return createPortal(
117
- <group position={[0, (ceiling.height ?? 2.5) + BRACKET_Y_OFFSET, 0]}>
445
+ <group position={[0, resolvedHeight + BRACKET_Y_OFFSET, 0]} ref={bracketsRootRef}>
118
446
  {corners.map((corner, index) => (
119
- <CornerBracket ceiling={ceiling} corner={corner} key={`${ceiling.id}-corner-${index}`} />
447
+ <CornerBracket
448
+ ceiling={effectiveCeiling}
449
+ corner={corner}
450
+ highlightIncoming={highlightedEdgeIndices.has(corner.incomingEdgeIndex)}
451
+ highlightOutgoing={highlightedEdgeIndices.has(corner.outgoingEdgeIndex)}
452
+ isHovered={activeCornerIndex === corner.index}
453
+ isLinkedHovered={
454
+ activeCornerIndex !== null &&
455
+ activeCornerIndex !== corner.index &&
456
+ highlightedCornerIndices.has(corner.index)
457
+ }
458
+ key={`${ceiling.id}-corner-${index}`}
459
+ onHoverChange={(hovered) => {
460
+ setHoveredCornerIndex((current) => {
461
+ if (hovered) return corner.index
462
+ return current === corner.index ? null : current
463
+ })
464
+ }}
465
+ onPointerDown={(event) => handleCornerPointerDown(corner, event)}
466
+ />
120
467
  ))}
121
468
  </group>,
122
469
  levelObject,
@@ -126,32 +473,46 @@ const CeilingSelectionAffordance = ({
126
473
  const CornerBracket = ({
127
474
  ceiling,
128
475
  corner,
476
+ highlightIncoming,
477
+ highlightOutgoing,
478
+ isHovered,
479
+ isLinkedHovered,
480
+ onHoverChange,
481
+ onPointerDown,
129
482
  }: {
130
483
  ceiling: CeilingNode
131
484
  corner: CornerBracketData
485
+ highlightIncoming: boolean
486
+ highlightOutgoing: boolean
487
+ isHovered: boolean
488
+ isLinkedHovered: boolean
489
+ onHoverChange: (hovered: boolean) => void
490
+ onPointerDown: (event: ThreeEvent<PointerEvent>) => void
132
491
  }) => {
133
- const [isHovered, setIsHovered] = useState(false)
134
- const color = '#d4d4d4'
135
- const opacity = 0.72
136
- const cubeColor = isHovered ? '#818cf8' : '#d4d4d4'
137
- const cubeOpacity = isHovered ? 0.92 : 0.72
492
+ const cubeHighlighted = isHovered || isLinkedHovered
493
+ const cubeColor = cubeHighlighted ? HANDLE_HOVER_COLOR : HANDLE_COLOR
494
+ const cubeOpacity = cubeHighlighted ? HANDLE_HOVER_OPACITY : HANDLE_OPACITY
138
495
 
139
496
  const handleClick = (e: ThreeEvent<MouseEvent>) => {
140
497
  e.stopPropagation()
141
498
 
142
- const nodes = useScene.getState().nodes
143
-
144
499
  useEditor.getState().setMovingNode(null)
145
- useEditor.getState().setMovingWallEndpoint(null)
146
- useEditor.getState().setCurvingWall(null)
147
- useEditor.getState().setEditingHole(null)
500
+ useInteractionScope
501
+ .getState()
502
+ .endIf((sc) => sc.kind === 'reshaping' && sc.reshape === 'endpoint')
503
+ useInteractionScope.getState().endIf((sc) => sc.kind === 'reshaping' && sc.reshape === 'curve')
504
+ useInteractionScope.getState().endIf((sc) => sc.kind === 'reshaping' && sc.reshape === 'hole')
148
505
  useEditor.getState().setMode('select')
149
506
 
150
507
  emitter.emit('ceiling:click' as any, {
151
508
  node: ceiling,
152
509
  nativeEvent: e.nativeEvent,
153
510
  localPosition: [0, 0, 0],
154
- position: [corner.corner[0], ceiling.height ?? 2.5, corner.corner[1]],
511
+ position: [
512
+ corner.corner[0],
513
+ resolveCeilingHeight(ceiling, useScene.getState().nodes),
514
+ corner.corner[1],
515
+ ],
155
516
  stopPropagation: () => e.stopPropagation(),
156
517
  viaHandle: true,
157
518
  })
@@ -160,36 +521,42 @@ const CornerBracket = ({
160
521
  return (
161
522
  <group position={[corner.corner[0], 0, corner.corner[1]]}>
162
523
  <BracketLeg
163
- color={color}
524
+ color={highlightIncoming ? HANDLE_HOVER_COLOR : HANDLE_COLOR}
164
525
  direction={corner.incomingDirection}
526
+ highlighted={highlightIncoming}
165
527
  length={corner.incomingLength}
166
528
  onClick={handleClick}
167
- opacity={opacity}
529
+ onHoverChange={onHoverChange}
530
+ onPointerDown={onPointerDown}
168
531
  />
169
532
  <BracketLeg
170
- color={color}
533
+ color={highlightOutgoing ? HANDLE_HOVER_COLOR : HANDLE_COLOR}
171
534
  direction={corner.outgoingDirection}
535
+ highlighted={highlightOutgoing}
172
536
  length={corner.outgoingLength}
173
537
  onClick={handleClick}
174
- opacity={opacity}
538
+ onHoverChange={onHoverChange}
539
+ onPointerDown={onPointerDown}
175
540
  />
176
541
 
177
542
  <mesh
543
+ geometry={SHARED_HANDLE_BOX_GEOMETRY}
178
544
  onClick={handleClick}
545
+ onPointerDown={onPointerDown}
179
546
  onPointerEnter={(e) => {
180
547
  e.stopPropagation()
181
- setIsHovered(true)
548
+ onHoverChange(true)
182
549
  }}
183
550
  onPointerLeave={(e) => {
184
551
  e.stopPropagation()
185
- setIsHovered(false)
552
+ onHoverChange(false)
186
553
  }}
187
554
  renderOrder={CORNER_RENDER_ORDER}
555
+ scale={HIT_BOX_SIZE}
188
556
  >
189
- <boxGeometry args={HIT_BOX_SIZE} />
190
557
  <meshBasicMaterial
191
558
  color={cubeColor}
192
- depthTest={false}
559
+ depthTest
193
560
  depthWrite={false}
194
561
  opacity={cubeOpacity}
195
562
  transparent
@@ -203,16 +570,20 @@ const BracketLeg = ({
203
570
  direction,
204
571
  length,
205
572
  color,
573
+ highlighted,
206
574
  onClick,
207
- opacity,
575
+ onHoverChange,
576
+ onPointerDown,
208
577
  }: {
209
578
  direction: [number, number]
210
579
  length: number
211
580
  color: string
581
+ highlighted: boolean
212
582
  onClick: (e: ThreeEvent<MouseEvent>) => void
213
- opacity: number
583
+ onHoverChange: (hovered: boolean) => void
584
+ onPointerDown: (event: ThreeEvent<PointerEvent>) => void
214
585
  }) => {
215
- const angle = Math.atan2(direction[1], direction[0])
586
+ const angle = -Math.atan2(direction[1], direction[0])
216
587
  const position: [number, number, number] = [
217
588
  direction[0] * (length / 2),
218
589
  0,
@@ -221,17 +592,27 @@ const BracketLeg = ({
221
592
 
222
593
  return (
223
594
  <mesh
595
+ geometry={SHARED_HANDLE_BOX_GEOMETRY}
224
596
  onClick={onClick}
597
+ onPointerDown={onPointerDown}
598
+ onPointerEnter={(e) => {
599
+ e.stopPropagation()
600
+ onHoverChange(true)
601
+ }}
602
+ onPointerLeave={(e) => {
603
+ e.stopPropagation()
604
+ onHoverChange(false)
605
+ }}
225
606
  position={position}
226
607
  renderOrder={CORNER_RENDER_ORDER}
227
608
  rotation={[0, angle, 0]}
609
+ scale={[length, BRACKET_HEIGHT, BRACKET_THICKNESS]}
228
610
  >
229
- <boxGeometry args={[length, BRACKET_HEIGHT, BRACKET_THICKNESS]} />
230
611
  <meshBasicMaterial
231
612
  color={color}
232
- depthTest={false}
613
+ depthTest
233
614
  depthWrite={false}
234
- opacity={opacity}
615
+ opacity={highlighted ? HANDLE_HOVER_OPACITY : HANDLE_OPACITY}
235
616
  transparent
236
617
  />
237
618
  </mesh>
@@ -241,7 +622,7 @@ const BracketLeg = ({
241
622
  function buildCornerBrackets(polygon: Array<[number, number]>): CornerBracketData[] {
242
623
  if (polygon.length < 3) return []
243
624
 
244
- const allCorners = polygon.map((corner, index) => {
625
+ return polygon.map((corner, index) => {
245
626
  const previous = polygon[(index - 1 + polygon.length) % polygon.length]!
246
627
  const next = polygon[(index + 1) % polygon.length]!
247
628
  const incomingVector = [previous[0] - corner[0], previous[1] - corner[1]] as [number, number]
@@ -251,35 +632,18 @@ function buildCornerBrackets(polygon: Array<[number, number]>): CornerBracketDat
251
632
 
252
633
  const incomingLength = Math.hypot(incomingVector[0], incomingVector[1])
253
634
  const outgoingLength = Math.hypot(outgoingVector[0], outgoingVector[1])
254
- const cornerStrength =
255
- 1 -
256
- Math.abs(
257
- incomingDirection[0] * outgoingDirection[0] + incomingDirection[1] * outgoingDirection[1],
258
- )
259
635
 
260
636
  return {
261
637
  corner,
638
+ index,
639
+ incomingEdgeIndex: (index - 1 + polygon.length) % polygon.length,
262
640
  incomingDirection,
641
+ outgoingEdgeIndex: index,
263
642
  outgoingDirection,
264
643
  incomingLength: getBracketLength(incomingLength),
265
644
  outgoingLength: getBracketLength(outgoingLength),
266
- cornerStrength,
267
645
  }
268
646
  })
269
-
270
- if (allCorners.length <= 4) {
271
- return allCorners
272
- }
273
-
274
- const selectedIndices = new Set(
275
- allCorners
276
- .map((corner, index) => ({ index, strength: corner.cornerStrength }))
277
- .sort((a, b) => b.strength - a.strength)
278
- .slice(0, 4)
279
- .map(({ index }) => index),
280
- )
281
-
282
- return allCorners.filter((_, index) => selectedIndices.has(index))
283
647
  }
284
648
 
285
649
  function normalize2D(vector: [number, number]): [number, number] {