@pascal-app/editor 0.8.0 → 0.9.2

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 (350) hide show
  1. package/package.json +15 -13
  2. package/src/components/editor/alignment-3d-guide-layer.tsx +168 -0
  3. package/src/components/editor/bake-exporter.tsx +47 -0
  4. package/src/components/editor/custom-camera-controls.tsx +795 -28
  5. package/src/components/editor/editor-layout-mobile.tsx +8 -3
  6. package/src/components/editor/editor-layout-v2.tsx +76 -51
  7. package/src/components/editor/export-manager.tsx +69 -32
  8. package/src/components/editor/fence-tangent-lines-3d.tsx +87 -0
  9. package/src/components/editor/first-person/build-collider-world.test.ts +182 -0
  10. package/src/components/editor/first-person/build-collider-world.ts +228 -83
  11. package/src/components/editor/first-person-controls.tsx +943 -27
  12. package/src/components/editor/floating-action-menu.tsx +676 -208
  13. package/src/components/editor/floating-building-action-menu.tsx +2 -2
  14. package/src/components/editor/floorplan-background-selection.test.ts +58 -0
  15. package/src/components/editor/floorplan-background-selection.ts +3 -2
  16. package/src/components/editor/floorplan-panel.tsx +6916 -12137
  17. package/src/components/editor/grid.tsx +197 -36
  18. package/src/components/editor/group-actions.ts +457 -0
  19. package/src/components/editor/group-floating-action-menu.tsx +130 -0
  20. package/src/components/editor/group-move-3d.ts +397 -0
  21. package/src/components/editor/group-rotate-handle.tsx +420 -0
  22. package/src/components/editor/group-selection-box-3d.tsx +173 -0
  23. package/src/components/editor/group-transform-shared.test.ts +396 -0
  24. package/src/components/editor/group-transform-shared.ts +489 -0
  25. package/src/components/editor/handles/handle-arrow.tsx +581 -0
  26. package/src/components/editor/handles/use-handle-drag.ts +246 -0
  27. package/src/components/editor/index.tsx +412 -162
  28. package/src/components/editor/measurement-pill.tsx +115 -0
  29. package/src/components/editor/node-action-menu.tsx +14 -1
  30. package/src/components/editor/node-arrow-handles.tsx +1646 -0
  31. package/src/components/editor/opening-guides-3d-layer.tsx +144 -0
  32. package/src/components/editor/quick-measurement-card.tsx +70 -0
  33. package/src/components/editor/quick-measurement-hud.tsx +28 -0
  34. package/src/components/editor/riser-diagram-panel.tsx +137 -0
  35. package/src/components/editor/selection-manager.tsx +1106 -483
  36. package/src/components/editor/site-edge-labels.tsx +45 -21
  37. package/src/components/editor/slab-hole-highlights.tsx +474 -0
  38. package/src/components/editor/snapshot-capture-overlay.tsx +369 -155
  39. package/src/components/editor/three-context-bridge.ts +22 -0
  40. package/src/components/editor/thumbnail-generator.tsx +140 -41
  41. package/src/components/editor/use-floorplan-background-placement.ts +190 -35
  42. package/src/components/editor/use-floorplan-hit-testing.ts +24 -0
  43. package/src/components/editor/use-floorplan-scene-data.ts +19 -7
  44. package/src/components/editor/use-mesh-settle-epoch.ts +26 -0
  45. package/src/components/editor/wall-measurement-label.tsx +14 -19
  46. package/src/components/editor/wall-move-side-handles.tsx +870 -0
  47. package/src/components/editor/wall-opening-highlights.tsx +181 -0
  48. package/src/components/editor/wall-snap-beacon-layer.tsx +304 -0
  49. package/src/components/editor-2d/floorplan-action-menu-layer.tsx +15 -6
  50. package/src/components/editor-2d/floorplan-alignment-guide-layer.tsx +142 -0
  51. package/src/components/editor-2d/floorplan-cursor-indicator-overlay.tsx +5 -3
  52. package/src/components/editor-2d/floorplan-group-action-menu.tsx +87 -0
  53. package/src/components/editor-2d/floorplan-group-move.tsx +701 -0
  54. package/src/components/editor-2d/floorplan-measurement-tool-layer.test.ts +113 -0
  55. package/src/components/editor-2d/floorplan-measurement-tool-layer.tsx +1656 -0
  56. package/src/components/editor-2d/floorplan-quick-measure-layer.tsx +212 -0
  57. package/src/components/editor-2d/floorplan-registry-action-menu.tsx +392 -0
  58. package/src/components/editor-2d/floorplan-registry-move-overlay.tsx +857 -0
  59. package/src/components/editor-2d/floorplan-render-context.tsx +62 -0
  60. package/src/components/editor-2d/floorplan-snap-beacon-layer.tsx +77 -0
  61. package/src/components/editor-2d/floorplan-wall-move-ghost-layer.tsx +47 -0
  62. package/src/components/editor-2d/renderers/floorplan-geometry-renderer.tsx +266 -0
  63. package/src/components/editor-2d/renderers/floorplan-label-angle.test.ts +15 -0
  64. package/src/components/editor-2d/renderers/floorplan-label-angle.ts +14 -0
  65. package/src/components/editor-2d/renderers/floorplan-measurements-layer.tsx +108 -66
  66. package/src/components/editor-2d/renderers/floorplan-placement-preview-layer.tsx +62 -0
  67. package/src/components/editor-2d/renderers/floorplan-registry-layer.test.ts +308 -0
  68. package/src/components/editor-2d/renderers/floorplan-registry-layer.tsx +3325 -0
  69. package/src/components/editor-2d/renderers/floorplan-stair-layer.tsx +24 -9
  70. package/src/components/editor-2d/renderers/floorplan-voronoi-layer.tsx +128 -0
  71. package/src/components/editor-2d/svg-paths.ts +10 -2
  72. package/src/components/systems/ceiling/ceiling-selection-affordance-system.tsx +439 -57
  73. package/src/components/systems/ceiling/ceiling-system.tsx +109 -12
  74. package/src/components/systems/roof/roof-edit-system.tsx +1851 -22
  75. package/src/components/systems/selection-affordance-manager.tsx +38 -0
  76. package/src/components/systems/stair/stair-edit-system.tsx +1 -1
  77. package/src/components/systems/zone/zone-label-editor-system.tsx +14 -1
  78. package/src/components/systems/zone/zone-system.tsx +42 -13
  79. package/src/components/tools/elevator/elevator-defaults.ts +8 -0
  80. package/src/components/tools/elevator/elevator-tool.tsx +375 -0
  81. package/src/components/tools/elevator/move-elevator-tool.tsx +263 -0
  82. package/src/components/tools/fence/fence-drafting.ts +141 -27
  83. package/src/components/tools/item/move-tool.tsx +45 -105
  84. package/src/components/tools/item/placement-math.test.ts +22 -0
  85. package/src/components/tools/item/placement-math.ts +59 -7
  86. package/src/components/tools/item/placement-strategies.ts +432 -16
  87. package/src/components/tools/item/placement-types.ts +22 -1
  88. package/src/components/tools/item/use-draft-node.ts +51 -2
  89. package/src/components/tools/item/use-placement-coordinator.tsx +1399 -256
  90. package/src/components/tools/registry/move-registry-node-tool.tsx +1064 -0
  91. package/src/components/tools/roof/roof-tool.tsx +350 -28
  92. package/src/components/tools/select/box-select-state.ts +88 -0
  93. package/src/components/tools/select/box-select-tool.tsx +442 -520
  94. package/src/components/tools/select/marquee-geometry.test.ts +161 -0
  95. package/src/components/tools/select/marquee-geometry.ts +155 -0
  96. package/src/components/tools/select/plane-box-select-tool.tsx +570 -0
  97. package/src/components/tools/select/screen-rectangle-selection.ts +84 -0
  98. package/src/components/tools/select/select-candidates.test.ts +109 -0
  99. package/src/components/tools/select/select-candidates.ts +85 -0
  100. package/src/components/tools/shared/affordance-dispatch.ts +30 -0
  101. package/src/components/tools/shared/cursor-sphere.tsx +62 -26
  102. package/src/components/tools/shared/drag-bounding-box.tsx +195 -0
  103. package/src/components/tools/shared/facing-indicator.tsx +87 -0
  104. package/src/components/tools/shared/facing-pose-indicator.tsx +54 -0
  105. package/src/components/tools/shared/floor-stack-preview.ts +25 -0
  106. package/src/components/tools/shared/fresh-placement-visibility.ts +61 -0
  107. package/src/components/tools/shared/placement-box-geometry.ts +123 -0
  108. package/src/components/tools/shared/placement-box.tsx +305 -0
  109. package/src/components/tools/shared/polygon-editor.tsx +866 -175
  110. package/src/components/tools/shared/segment-angle.ts +56 -0
  111. package/src/components/tools/site/site-boundary-editor.tsx +520 -16
  112. package/src/components/tools/site/site-flag-model.ts +18 -0
  113. package/src/components/tools/stair/stair-defaults.ts +2 -1
  114. package/src/components/tools/stair/stair-tool.tsx +342 -37
  115. package/src/components/tools/tool-manager.tsx +239 -62
  116. package/src/components/tools/wall/wall-drafting.test.ts +330 -0
  117. package/src/components/tools/wall/wall-drafting.ts +248 -171
  118. package/src/components/tools/wall/wall-snap-geometry.test.ts +123 -0
  119. package/src/components/tools/wall/wall-snap-geometry.ts +331 -0
  120. package/src/components/tools/zone/zone-boundary-editor.tsx +5 -1
  121. package/src/components/tools/zone/zone-tool.tsx +86 -82
  122. package/src/components/ui/action-menu/action-button.tsx +21 -1
  123. package/src/components/ui/action-menu/camera-actions.tsx +24 -17
  124. package/src/components/ui/action-menu/control-modes.tsx +40 -170
  125. package/src/components/ui/action-menu/furnish-tools.tsx +5 -5
  126. package/src/components/ui/action-menu/index.tsx +3 -117
  127. package/src/components/ui/action-menu/measurement-control.tsx +188 -0
  128. package/src/components/ui/action-menu/structure-tools.tsx +27 -93
  129. package/src/components/ui/action-menu/view-toggles.tsx +279 -90
  130. package/src/components/ui/command-palette/editor-commands.tsx +20 -15
  131. package/src/components/ui/command-palette/index.tsx +5 -3
  132. package/src/components/ui/controls/material-paint-panel.tsx +152 -0
  133. package/src/components/ui/controls/material-picker.tsx +87 -177
  134. package/src/components/ui/controls/material-properties-editor.tsx +108 -0
  135. package/src/components/ui/controls/metric-control.tsx +142 -46
  136. package/src/components/ui/controls/scene-material-list.tsx +247 -0
  137. package/src/components/ui/controls/slider-control.tsx +88 -38
  138. package/src/components/ui/floating-level-selector.tsx +47 -10
  139. package/src/components/ui/helpers/building-helper.tsx +10 -23
  140. package/src/components/ui/helpers/contextual-helper-panel.tsx +428 -0
  141. package/src/components/ui/helpers/helper-manager.tsx +235 -23
  142. package/src/components/ui/helpers/item-helper.tsx +28 -32
  143. package/src/components/ui/helpers/registered-tool-helper.tsx +59 -0
  144. package/src/components/ui/helpers/roof-helper.tsx +10 -12
  145. package/src/components/ui/icon-ref.tsx +47 -0
  146. package/src/components/ui/item-catalog/catalog-items.tsx +40 -1
  147. package/src/components/ui/item-catalog/item-catalog.tsx +16 -36
  148. package/src/components/ui/level-duplicate-dialog.tsx +2 -1
  149. package/src/components/ui/panels/node-display.ts +17 -16
  150. package/src/components/ui/panels/panel-manager.tsx +55 -58
  151. package/src/components/ui/panels/panel-wrapper.tsx +238 -12
  152. package/src/components/ui/panels/parametric-inspector.tsx +482 -0
  153. package/src/components/ui/panels/reference-panel.tsx +54 -19
  154. package/src/components/ui/primitives/dropdown-menu.tsx +12 -8
  155. package/src/components/ui/primitives/shortcut-token.tsx +39 -3
  156. package/src/components/ui/primitives/sidebar.tsx +1 -0
  157. package/src/components/ui/scene-loader.tsx +1 -3
  158. package/src/components/ui/sidebar/app-sidebar.tsx +5 -1
  159. package/src/components/ui/sidebar/icon-rail.tsx +50 -31
  160. package/src/components/ui/sidebar/panels/items-panel/function-tree-panel.tsx +263 -0
  161. package/src/components/ui/sidebar/panels/items-panel/index.tsx +82 -8
  162. package/src/components/ui/sidebar/panels/plugins-panel.tsx +218 -0
  163. package/src/components/ui/sidebar/panels/settings-panel/index.tsx +122 -41
  164. package/src/components/ui/sidebar/panels/settings-panel/keyboard-shortcuts-dialog.tsx +66 -7
  165. package/src/components/ui/sidebar/panels/settings-panel/load-build-dialog.tsx +267 -0
  166. package/src/components/ui/sidebar/panels/site-panel/building-tree-node.tsx +3 -1
  167. package/src/components/ui/sidebar/panels/site-panel/ceiling-tree-node.tsx +9 -5
  168. package/src/components/ui/sidebar/panels/site-panel/chimney-tree-node.tsx +86 -0
  169. package/src/components/ui/sidebar/panels/site-panel/column-tree-node.tsx +1 -1
  170. package/src/components/ui/sidebar/panels/site-panel/door-tree-node.tsx +7 -2
  171. package/src/components/ui/sidebar/panels/site-panel/dormer-tree-node.tsx +92 -0
  172. package/src/components/ui/sidebar/panels/site-panel/elevator-tree-node.tsx +75 -0
  173. package/src/components/ui/sidebar/panels/site-panel/fence-tree-node.tsx +3 -3
  174. package/src/components/ui/sidebar/panels/site-panel/gutter-tree-node.tsx +86 -0
  175. package/src/components/ui/sidebar/panels/site-panel/index.tsx +42 -21
  176. package/src/components/ui/sidebar/panels/site-panel/item-tree-node.tsx +31 -17
  177. package/src/components/ui/sidebar/panels/site-panel/level-tree-node.tsx +2 -1
  178. package/src/components/ui/sidebar/panels/site-panel/registry-tree-node.tsx +140 -0
  179. package/src/components/ui/sidebar/panels/site-panel/roof-tree-node.tsx +48 -8
  180. package/src/components/ui/sidebar/panels/site-panel/shelf-tree-node.tsx +132 -0
  181. package/src/components/ui/sidebar/panels/site-panel/slab-tree-node.tsx +9 -5
  182. package/src/components/ui/sidebar/panels/site-panel/solar-panel-tree-node.tsx +86 -0
  183. package/src/components/ui/sidebar/panels/site-panel/spawn-tree-node.tsx +7 -1
  184. package/src/components/ui/sidebar/panels/site-panel/stair-tree-node.tsx +2 -2
  185. package/src/components/ui/sidebar/panels/site-panel/tree-node.tsx +112 -39
  186. package/src/components/ui/sidebar/panels/site-panel/tree-structure.ts +36 -0
  187. package/src/components/ui/sidebar/panels/site-panel/wall-tree-node.tsx +1 -1
  188. package/src/components/ui/sidebar/panels/site-panel/window-tree-node.tsx +7 -2
  189. package/src/components/ui/sidebar/panels/site-panel/zone-tree-node.tsx +3 -3
  190. package/src/components/ui/sidebar/panels/zone-panel/index.tsx +51 -2
  191. package/src/components/ui/sidebar/tab-bar.tsx +75 -1
  192. package/src/components/ui/sidebar/use-plugin-panels.tsx +132 -0
  193. package/src/components/ui/snap-target-badge.test.tsx +40 -0
  194. package/src/components/ui/snap-target-badge.tsx +88 -0
  195. package/src/components/viewer-overlay.tsx +253 -61
  196. package/src/components/viewer-zone-system.tsx +7 -2
  197. package/src/hooks/use-auto-save.test.ts +14 -0
  198. package/src/hooks/use-auto-save.ts +64 -10
  199. package/src/hooks/use-ceiling-events.ts +176 -0
  200. package/src/hooks/use-drag-action.ts +127 -0
  201. package/src/hooks/use-keyboard.ts +493 -95
  202. package/src/hooks/use-selection.ts +64 -0
  203. package/src/index.tsx +451 -6
  204. package/src/lib/active-placement-surface.ts +35 -0
  205. package/src/lib/ceiling-plan-snap.ts +24 -0
  206. package/src/lib/constants.ts +6 -2
  207. package/src/lib/contextual-help.test.ts +112 -0
  208. package/src/lib/contextual-help.ts +144 -0
  209. package/src/lib/continuation.ts +64 -0
  210. package/src/lib/direct-manipulation.test.ts +192 -0
  211. package/src/lib/direct-manipulation.ts +109 -0
  212. package/src/lib/editor-api.ts +50 -0
  213. package/src/lib/elevator-support.ts +96 -0
  214. package/src/lib/floorplan/apply-alignment.test.ts +25 -0
  215. package/src/lib/floorplan/apply-alignment.ts +117 -0
  216. package/src/lib/floorplan/floorplan-export.tsx +364 -0
  217. package/src/lib/floorplan/geometry.ts +53 -0
  218. package/src/lib/floorplan/index.ts +10 -0
  219. package/src/lib/floorplan/items.ts +5 -2
  220. package/src/lib/floorplan/plan-coords.ts +21 -0
  221. package/src/lib/floorplan/selection-tool.ts +40 -0
  222. package/src/lib/floorplan/stairs.ts +12 -1
  223. package/src/lib/fresh-planar-placement.test.ts +339 -0
  224. package/src/lib/fresh-planar-placement.ts +130 -0
  225. package/src/lib/glb-export.test.ts +441 -0
  226. package/src/lib/glb-export.ts +874 -0
  227. package/src/lib/history.ts +11 -1
  228. package/src/lib/interaction/hot-set.test.ts +133 -0
  229. package/src/lib/interaction/hot-set.ts +67 -0
  230. package/src/lib/interaction/overlay-policy.test.ts +51 -0
  231. package/src/lib/interaction/overlay-policy.ts +59 -0
  232. package/src/lib/interaction/scope.ts +211 -0
  233. package/src/lib/level-duplication.ts +54 -42
  234. package/src/lib/level-selection.ts +2 -2
  235. package/src/lib/material-paint.ts +227 -48
  236. package/src/lib/measurement-kind.test.ts +30 -0
  237. package/src/lib/measurement-kind.ts +19 -0
  238. package/src/lib/measurement-label.test.ts +47 -0
  239. package/src/lib/measurement-label.ts +68 -0
  240. package/src/lib/measurement-parser.ts +116 -0
  241. package/src/lib/measurements.test.ts +183 -0
  242. package/src/lib/measurements.ts +199 -0
  243. package/src/lib/paint-scope.test.ts +454 -0
  244. package/src/lib/paint-scope.ts +461 -0
  245. package/src/lib/placement-drag-release.ts +23 -0
  246. package/src/lib/placement-metadata.ts +29 -0
  247. package/src/lib/planar-cursor-placement.test.ts +100 -0
  248. package/src/lib/planar-cursor-placement.ts +42 -0
  249. package/src/lib/plugin-panels.test.ts +19 -0
  250. package/src/lib/plugin-panels.ts +91 -0
  251. package/src/lib/quick-measurement.test.ts +77 -0
  252. package/src/lib/quick-measurement.ts +109 -0
  253. package/src/lib/roof-duplication.ts +7 -7
  254. package/src/lib/roof-hover-outline-proxy.ts +22 -0
  255. package/src/lib/roof-wall-hit.ts +164 -0
  256. package/src/lib/scene-clipboard.test.ts +196 -0
  257. package/src/lib/scene-clipboard.ts +332 -0
  258. package/src/lib/scene.ts +40 -8
  259. package/src/lib/selection-routing.test.ts +298 -0
  260. package/src/lib/selection-routing.ts +175 -0
  261. package/src/lib/sfx/index.ts +1 -0
  262. package/src/lib/sfx/movement-tick.test.ts +65 -0
  263. package/src/lib/sfx/movement-tick.ts +18 -0
  264. package/src/lib/sfx-bus.ts +69 -10
  265. package/src/lib/sfx-player.test.ts +124 -0
  266. package/src/lib/sfx-player.ts +148 -55
  267. package/src/lib/site-boundary.ts +1 -0
  268. package/src/lib/slab-plan-snap.test.ts +93 -0
  269. package/src/lib/slab-plan-snap.ts +108 -0
  270. package/src/lib/snapping-mode.test.ts +138 -0
  271. package/src/lib/snapping-mode.ts +179 -0
  272. package/src/lib/stair-duplication.ts +4 -4
  273. package/src/lib/stair-levels.test.ts +146 -0
  274. package/src/lib/stair-levels.ts +177 -0
  275. package/src/lib/surface-plan-snap.test.ts +71 -0
  276. package/src/lib/surface-plan-snap.ts +256 -0
  277. package/src/lib/use-linear-display.ts +40 -0
  278. package/src/lib/world-grid-snap.ts +231 -0
  279. package/src/lib/zone-content.ts +130 -0
  280. package/src/store/use-alignment-guides.ts +21 -0
  281. package/src/store/use-direct-manipulation-feedback.ts +20 -0
  282. package/src/store/use-editor.tsx +767 -114
  283. package/src/store/use-facing-pose.ts +44 -0
  284. package/src/store/use-fence-curve-draft.ts +21 -0
  285. package/src/store/use-floorplan-draft-preview.ts +101 -0
  286. package/src/store/use-floorplan-marquee.ts +50 -0
  287. package/src/store/use-interaction-scope.test.ts +186 -0
  288. package/src/store/use-interaction-scope.ts +132 -0
  289. package/src/store/use-measurement-draft.test.ts +530 -0
  290. package/src/store/use-measurement-draft.ts +543 -0
  291. package/src/store/use-opening-guides.ts +41 -0
  292. package/src/store/use-placement-preview.ts +39 -0
  293. package/src/store/use-quick-measurement-hud.test.ts +53 -0
  294. package/src/store/use-quick-measurement-hud.ts +77 -0
  295. package/src/store/use-segment-draft-chain.ts +28 -0
  296. package/src/store/use-stair-build-preview.ts +43 -0
  297. package/src/store/use-wall-move-ghosts.ts +36 -0
  298. package/src/store/use-wall-snap-indicator.ts +34 -0
  299. package/src/components/editor/first-person/bvh-ecctrl.tsx +0 -860
  300. package/src/components/editor/preset-thumbnail-generator.tsx +0 -125
  301. package/src/components/editor-2d/renderers/floorplan-roof-layer.tsx +0 -113
  302. package/src/components/tools/building/move-building-tool.tsx +0 -157
  303. package/src/components/tools/ceiling/ceiling-boundary-editor.tsx +0 -43
  304. package/src/components/tools/ceiling/ceiling-hole-editor.tsx +0 -49
  305. package/src/components/tools/ceiling/ceiling-tool.tsx +0 -465
  306. package/src/components/tools/ceiling/move-ceiling-tool.tsx +0 -264
  307. package/src/components/tools/column/column-tool.tsx +0 -97
  308. package/src/components/tools/column/move-column-tool.tsx +0 -105
  309. package/src/components/tools/door/door-math.ts +0 -110
  310. package/src/components/tools/door/door-tool.tsx +0 -324
  311. package/src/components/tools/door/move-door-tool.tsx +0 -412
  312. package/src/components/tools/fence/curve-fence-tool.tsx +0 -178
  313. package/src/components/tools/fence/fence-tool.tsx +0 -346
  314. package/src/components/tools/fence/move-fence-endpoint-tool.tsx +0 -441
  315. package/src/components/tools/fence/move-fence-tool.tsx +0 -302
  316. package/src/components/tools/item/item-tool.tsx +0 -26
  317. package/src/components/tools/roof/move-roof-tool.tsx +0 -364
  318. package/src/components/tools/slab/move-slab-tool.tsx +0 -182
  319. package/src/components/tools/slab/slab-boundary-editor.tsx +0 -43
  320. package/src/components/tools/slab/slab-hole-editor.tsx +0 -49
  321. package/src/components/tools/slab/slab-tool.tsx +0 -322
  322. package/src/components/tools/spawn/move-spawn-tool.tsx +0 -101
  323. package/src/components/tools/spawn/spawn-tool.tsx +0 -130
  324. package/src/components/tools/wall/curve-wall-tool.tsx +0 -178
  325. package/src/components/tools/wall/move-wall-endpoint-tool.tsx +0 -426
  326. package/src/components/tools/wall/move-wall-tool.tsx +0 -358
  327. package/src/components/tools/wall/wall-tool.tsx +0 -332
  328. package/src/components/tools/window/move-window-tool.tsx +0 -447
  329. package/src/components/tools/window/window-math.ts +0 -117
  330. package/src/components/tools/window/window-tool.tsx +0 -332
  331. package/src/components/ui/helpers/ceiling-helper.tsx +0 -20
  332. package/src/components/ui/helpers/slab-helper.tsx +0 -20
  333. package/src/components/ui/helpers/wall-helper.tsx +0 -20
  334. package/src/components/ui/panels/ceiling-panel.tsx +0 -249
  335. package/src/components/ui/panels/column-panel.tsx +0 -759
  336. package/src/components/ui/panels/door-panel.tsx +0 -1299
  337. package/src/components/ui/panels/fence-panel.tsx +0 -222
  338. package/src/components/ui/panels/item-panel.tsx +0 -306
  339. package/src/components/ui/panels/paint-panel.tsx +0 -163
  340. package/src/components/ui/panels/presets/presets-popover.tsx +0 -511
  341. package/src/components/ui/panels/roof-panel.tsx +0 -280
  342. package/src/components/ui/panels/roof-segment-panel.tsx +0 -311
  343. package/src/components/ui/panels/slab-panel.tsx +0 -250
  344. package/src/components/ui/panels/spawn-panel.tsx +0 -161
  345. package/src/components/ui/panels/stair-panel.tsx +0 -577
  346. package/src/components/ui/panels/stair-segment-panel.tsx +0 -325
  347. package/src/components/ui/panels/wall-panel.tsx +0 -273
  348. package/src/components/ui/panels/window-panel.tsx +0 -968
  349. package/src/contexts/presets-context.tsx +0 -121
  350. package/src/hooks/use-contextual-tools.ts +0 -61
@@ -5,35 +5,104 @@ import {
5
5
  emitter,
6
6
  resolveLevelId,
7
7
  sceneRegistry,
8
+ snapPointToGrid,
9
+ useLiveNodeOverrides,
8
10
  useScene,
9
11
  } from '@pascal-app/core'
10
12
  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'
13
+ import { createPortal, type ThreeEvent, useThree } from '@react-three/fiber'
14
+ import { useCallback, useEffect, useMemo, useRef, useState } from 'react'
15
+ import { BoxGeometry, type Group, type Object3D, Plane, Raycaster, Vector2, Vector3 } from 'three'
14
16
  import { useShallow } from 'zustand/react/shallow'
15
- import useEditor from '../../../store/use-editor'
17
+ import {
18
+ clearCeilingSnapFeedback,
19
+ resolveCeilingPlanPointSnap,
20
+ } from '../../../lib/ceiling-plan-snap'
21
+ import { sfxEmitter } from '../../../lib/sfx-bus'
22
+ import useEditor, { isGridSnapActive } from '../../../store/use-editor'
23
+ import useInteractionScope from '../../../store/use-interaction-scope'
24
+ import { suppressBoxSelectForPointer } from '../../tools/select/box-select-state'
16
25
 
17
26
  const BRACKET_THICKNESS = 0.04
18
27
  const BRACKET_HEIGHT = 0.04
19
28
  const BRACKET_Y_OFFSET = 0.035
20
29
  const HIT_BOX_SIZE: [number, number, number] = [0.28, 0.08, 0.28]
30
+ const HANDLE_COLOR = '#d4d4d4'
31
+ const HANDLE_HOVER_COLOR = '#818cf8'
32
+ const HANDLE_OPACITY = 0.72
33
+ const HANDLE_HOVER_OPACITY = 0.92
34
+ const HANDLE_DRAG_THRESHOLD_PX = 4
35
+ const SHARED_HANDLE_BOX_GEOMETRY = new BoxGeometry(1, 1, 1)
36
+ // Draw the corner handles after the ceiling surface so they read cleanly
37
+ // when unobstructed, while material depth testing still lets other scene
38
+ // geometry hide them.
39
+ const CORNER_RENDER_ORDER = 1000
21
40
 
22
41
  type CornerBracketData = {
23
42
  corner: [number, number]
43
+ index: number
44
+ incomingEdgeIndex: number
24
45
  incomingDirection: [number, number]
46
+ outgoingEdgeIndex: number
25
47
  outgoingDirection: [number, number]
26
48
  incomingLength: number
27
49
  outgoingLength: number
28
- cornerStrength: number
50
+ }
51
+
52
+ type CornerDragState = {
53
+ ceilingId: CeilingNode['id']
54
+ cornerIndex: number
55
+ didDrag: boolean
56
+ initialPolygon: Array<[number, number]>
57
+ inputDraggingSet: boolean
58
+ pointerId: number
59
+ previewPolygon: Array<[number, number]> | null
60
+ previousSnappedPosition: [number, number] | null
61
+ previousInputDragging: boolean
62
+ startClientX: number
63
+ startClientY: number
64
+ startPlanePosition: [number, number]
65
+ }
66
+
67
+ function stopHandlePointerDown(event: ThreeEvent<PointerEvent>) {
68
+ event.stopPropagation()
69
+ suppressBoxSelectForPointer(event, { markHandled: false })
70
+ }
71
+
72
+ function suppressNextClick() {
73
+ const suppressClick = (clickEvent: MouseEvent) => {
74
+ clickEvent.stopImmediatePropagation()
75
+ clickEvent.preventDefault()
76
+ window.removeEventListener('click', suppressClick, true)
77
+ }
78
+ window.addEventListener('click', suppressClick, true)
79
+ requestAnimationFrame(() => {
80
+ window.removeEventListener('click', suppressClick, true)
81
+ })
82
+ }
83
+
84
+ function clearCornerDragPreview(drag: CornerDragState) {
85
+ if (drag.didDrag) {
86
+ useLiveNodeOverrides.getState().clear(drag.ceilingId)
87
+ useScene.getState().markDirty(drag.ceilingId)
88
+ }
89
+ if (drag.inputDraggingSet) {
90
+ useViewer.getState().setInputDragging(drag.previousInputDragging)
91
+ }
92
+ clearCeilingSnapFeedback()
29
93
  }
30
94
 
31
95
  export const CeilingSelectionAffordanceSystem = () => {
32
96
  const phase = useEditor((state) => state.phase)
33
97
  const mode = useEditor((state) => state.mode)
34
98
  const structureLayer = useEditor((state) => state.structureLayer)
35
- const movingNode = useEditor((state) => state.movingNode)
36
- const curvingWall = useEditor((state) => state.curvingWall)
99
+ // ANY active interaction (moving/placing a node, reshaping a boundary or
100
+ // curve, dragging a handle) unmounts the brackets: their ceiling-height hit
101
+ // boxes would otherwise catch drag-time hover, set `hoveredId` to the
102
+ // ceiling, and flash the ceiling grid mid-gesture (e.g. while dragging a
103
+ // slab polygon vertex). The brackets' own corner drag doesn't begin a
104
+ // scope, so it can't unmount itself.
105
+ const scopeIdle = useInteractionScope((state) => state.scope.kind === 'idle')
37
106
  const currentLevelId = useViewer((state) => state.selection.levelId)
38
107
 
39
108
  const ceilings = useScene(
@@ -53,8 +122,7 @@ export const CeilingSelectionAffordanceSystem = () => {
53
122
  phase === 'structure' &&
54
123
  mode === 'select' &&
55
124
  structureLayer === 'elements' &&
56
- !movingNode &&
57
- !curvingWall &&
125
+ scopeIdle &&
58
126
  currentLevelId !== null
59
127
 
60
128
  if (!shouldRender) return null
@@ -75,11 +143,271 @@ const CeilingSelectionAffordance = ({
75
143
  ceiling: CeilingNode
76
144
  levelId: string
77
145
  }) => {
146
+ const { camera, gl } = useThree()
147
+ const liveOverride = useLiveNodeOverrides(
148
+ (state) => state.overrides.get(ceiling.id) as Partial<CeilingNode> | undefined,
149
+ )
150
+ const effectiveCeiling = useMemo(
151
+ () => (liveOverride ? ({ ...ceiling, ...liveOverride } as CeilingNode) : ceiling),
152
+ [ceiling, liveOverride],
153
+ )
78
154
  const [levelObject, setLevelObject] = useState<Object3D | null>(
79
155
  () => sceneRegistry.nodes.get(levelId) ?? null,
80
156
  )
157
+ const [hoveredCornerIndex, setHoveredCornerIndex] = useState<number | null>(null)
158
+ const [draggedCornerIndex, setDraggedCornerIndex] = useState<number | null>(null)
159
+ const [previewPolygon, setPreviewPolygon] = useState<Array<[number, number]> | null>(null)
160
+ const dragRef = useRef<CornerDragState | null>(null)
161
+ const bracketsRootRef = useRef<Group>(null)
162
+ const raycasterRef = useRef(new Raycaster())
163
+ const ndcRef = useRef(new Vector2())
164
+ const planeRef = useRef(new Plane())
165
+ const planePointRef = useRef(new Vector3())
166
+ const planeNormalRef = useRef(new Vector3())
167
+ const planeOriginRef = useRef(new Vector3())
168
+ const intersectionRef = useRef(new Vector3())
169
+ const localIntersectionRef = useRef(new Vector3())
170
+
171
+ const displayPolygon = previewPolygon ?? effectiveCeiling.polygon
172
+ const activeCornerIndex = draggedCornerIndex ?? hoveredCornerIndex
173
+ const corners = useMemo(() => buildCornerBrackets(displayPolygon), [displayPolygon])
174
+ const highlightedEdgeIndices = useMemo(() => {
175
+ const next = new Set<number>()
176
+ if (activeCornerIndex === null || displayPolygon.length < 2) return next
177
+ next.add(activeCornerIndex)
178
+ next.add((activeCornerIndex - 1 + displayPolygon.length) % displayPolygon.length)
179
+ return next
180
+ }, [activeCornerIndex, displayPolygon.length])
181
+ const highlightedCornerIndices = useMemo(() => {
182
+ const next = new Set<number>()
183
+ if (activeCornerIndex === null || displayPolygon.length < 2) return next
184
+ next.add(activeCornerIndex)
185
+ next.add((activeCornerIndex - 1 + displayPolygon.length) % displayPolygon.length)
186
+ next.add((activeCornerIndex + 1) % displayPolygon.length)
187
+ return next
188
+ }, [activeCornerIndex, displayPolygon.length])
189
+
190
+ useEffect(() => {
191
+ if (activeCornerIndex === null) return
192
+
193
+ useViewer.getState().setHoveredId(effectiveCeiling.id)
194
+ return () => {
195
+ if (useViewer.getState().hoveredId === effectiveCeiling.id) {
196
+ useViewer.getState().setHoveredId(null)
197
+ }
198
+ }
199
+ }, [activeCornerIndex, effectiveCeiling.id])
200
+
201
+ const selectCeilingForEdit = useCallback(() => {
202
+ const editor = useEditor.getState()
203
+ editor.setMovingNode(null)
204
+ useInteractionScope
205
+ .getState()
206
+ .endIf((sc) => sc.kind === 'reshaping' && sc.reshape === 'endpoint')
207
+ useInteractionScope.getState().endIf((sc) => sc.kind === 'reshaping' && sc.reshape === 'curve')
208
+ useInteractionScope.getState().endIf((sc) => sc.kind === 'reshaping' && sc.reshape === 'hole')
209
+ editor.setMode('select')
210
+ useViewer.getState().setSelection({ selectedIds: [effectiveCeiling.id] })
211
+ }, [effectiveCeiling.id])
212
+
213
+ const getHandlePlanePoint = useCallback(
214
+ (event: MouseEvent | PointerEvent): [number, number] | null => {
215
+ if (!levelObject) return null
216
+
217
+ const rect = gl.domElement.getBoundingClientRect()
218
+ ndcRef.current.set(
219
+ ((event.clientX - rect.left) / rect.width) * 2 - 1,
220
+ -((event.clientY - rect.top) / rect.height) * 2 + 1,
221
+ )
222
+ raycasterRef.current.setFromCamera(ndcRef.current, camera)
223
+
224
+ planePointRef.current.set(0, (effectiveCeiling.height ?? 2.5) + BRACKET_Y_OFFSET, 0)
225
+ levelObject.localToWorld(planePointRef.current)
226
+
227
+ planeOriginRef.current.set(0, 0, 0)
228
+ levelObject.localToWorld(planeOriginRef.current)
229
+ planeNormalRef.current.set(0, 1, 0)
230
+ levelObject.localToWorld(planeNormalRef.current)
231
+ planeNormalRef.current.sub(planeOriginRef.current).normalize()
232
+ planeRef.current.setFromNormalAndCoplanarPoint(planeNormalRef.current, planePointRef.current)
233
+
234
+ const hit = raycasterRef.current.ray.intersectPlane(planeRef.current, intersectionRef.current)
235
+ if (!hit) return null
236
+
237
+ localIntersectionRef.current.copy(intersectionRef.current)
238
+ levelObject.worldToLocal(localIntersectionRef.current)
239
+ return [localIntersectionRef.current.x, localIntersectionRef.current.z]
240
+ },
241
+ [camera, effectiveCeiling.height, gl.domElement, levelObject],
242
+ )
243
+
244
+ const handleCornerPointerDown = useCallback(
245
+ (corner: CornerBracketData, event: ThreeEvent<PointerEvent>) => {
246
+ if (event.button !== 0) return
247
+ stopHandlePointerDown(event)
248
+
249
+ const startPlanePosition = getHandlePlanePoint(event.nativeEvent)
250
+ if (!startPlanePosition) return
251
+ const initialCorner = effectiveCeiling.polygon[corner.index]
252
+ if (!initialCorner) return
253
+
254
+ dragRef.current = {
255
+ ceilingId: effectiveCeiling.id,
256
+ cornerIndex: corner.index,
257
+ didDrag: false,
258
+ initialPolygon: effectiveCeiling.polygon.map(([x, z]) => [x, z] as [number, number]),
259
+ inputDraggingSet: false,
260
+ pointerId: event.pointerId,
261
+ previewPolygon: null,
262
+ previousSnappedPosition: [initialCorner[0], initialCorner[1]],
263
+ previousInputDragging: useViewer.getState().inputDragging,
264
+ startClientX: event.nativeEvent.clientX,
265
+ startClientY: event.nativeEvent.clientY,
266
+ startPlanePosition,
267
+ }
268
+ },
269
+ [effectiveCeiling.id, effectiveCeiling.polygon, getHandlePlanePoint],
270
+ )
271
+
272
+ useEffect(() => {
273
+ const handlePointerMove = (event: PointerEvent) => {
274
+ const drag = dragRef.current
275
+ if (!drag || drag.ceilingId !== effectiveCeiling.id) return
276
+ if (event.pointerId !== drag.pointerId) return
277
+
278
+ const dragDistance = Math.hypot(
279
+ event.clientX - drag.startClientX,
280
+ event.clientY - drag.startClientY,
281
+ )
282
+
283
+ const planePosition = getHandlePlanePoint(event)
284
+ if (!planePosition) return
285
+
286
+ if (!drag.didDrag) {
287
+ if (dragDistance < HANDLE_DRAG_THRESHOLD_PX) return
288
+
289
+ drag.didDrag = true
290
+ drag.inputDraggingSet = true
291
+ useViewer.getState().setInputDragging(true)
292
+ setDraggedCornerIndex(drag.cornerIndex)
293
+ selectCeilingForEdit()
294
+ sfxEmitter.emit('sfx:item-pick')
295
+ }
296
+
297
+ const initialCorner = drag.initialPolygon[drag.cornerIndex]
298
+ if (!initialCorner) return
299
+
300
+ const rawNextPosition: [number, number] = [
301
+ initialCorner[0] + (planePosition[0] - drag.startPlanePosition[0]),
302
+ initialCorner[1] + (planePosition[1] - drag.startPlanePosition[1]),
303
+ ]
304
+ const rawDelta: [number, number] = [
305
+ planePosition[0] - drag.startPlanePosition[0],
306
+ planePosition[1] - drag.startPlanePosition[1],
307
+ ]
308
+ const gridStep = isGridSnapActive() ? useEditor.getState().gridSnapStep : 0
309
+ const snappedDelta = snapPointToGrid(rawDelta, gridStep)
310
+ const gridNextPosition: [number, number] = [
311
+ initialCorner[0] + snappedDelta[0],
312
+ initialCorner[1] + snappedDelta[1],
313
+ ]
314
+ const nextPosition = resolveCeilingPlanPointSnap({
315
+ rawPoint: rawNextPosition,
316
+ fallbackPoint: gridNextPosition,
317
+ levelId,
318
+ excludeId: drag.ceilingId,
319
+ }).point
320
+
321
+ const snapEpsilon = 1e-6
322
+ const alignmentSnapped =
323
+ Math.abs(nextPosition[0] - gridNextPosition[0]) > snapEpsilon ||
324
+ Math.abs(nextPosition[1] - gridNextPosition[1]) > snapEpsilon
325
+ if (
326
+ (gridStep > 0 || alignmentSnapped) &&
327
+ drag.previousSnappedPosition &&
328
+ (nextPosition[0] !== drag.previousSnappedPosition[0] ||
329
+ nextPosition[1] !== drag.previousSnappedPosition[1])
330
+ ) {
331
+ sfxEmitter.emit('sfx:grid-snap')
332
+ }
333
+ drag.previousSnappedPosition = nextPosition
81
334
 
82
- const corners = useMemo(() => buildCornerBrackets(ceiling.polygon), [ceiling.polygon])
335
+ const nextPolygon = drag.initialPolygon.map((polygonPoint, index) =>
336
+ index === drag.cornerIndex ? nextPosition : polygonPoint,
337
+ )
338
+
339
+ drag.previewPolygon = nextPolygon
340
+ setPreviewPolygon(nextPolygon)
341
+ useLiveNodeOverrides.getState().set(drag.ceilingId, { polygon: nextPolygon })
342
+ useScene.getState().markDirty(drag.ceilingId)
343
+ }
344
+
345
+ const finishDrag = (event: PointerEvent) => {
346
+ const drag = dragRef.current
347
+ if (!drag || event.pointerId !== drag.pointerId) return
348
+
349
+ dragRef.current = null
350
+ setDraggedCornerIndex(null)
351
+ setPreviewPolygon(null)
352
+
353
+ if (drag.didDrag) {
354
+ event.preventDefault()
355
+ suppressNextClick()
356
+
357
+ if (drag.previewPolygon) {
358
+ useScene.getState().updateNode(drag.ceilingId, { polygon: drag.previewPolygon })
359
+ useViewer.getState().setSelection({ selectedIds: [drag.ceilingId] })
360
+ }
361
+
362
+ sfxEmitter.emit('sfx:item-place')
363
+ }
364
+
365
+ clearCornerDragPreview(drag)
366
+ }
367
+
368
+ const cancelDrag = (event: PointerEvent) => {
369
+ const drag = dragRef.current
370
+ if (!drag || event.pointerId !== drag.pointerId) return
371
+
372
+ dragRef.current = null
373
+ setDraggedCornerIndex(null)
374
+ setPreviewPolygon(null)
375
+ clearCornerDragPreview(drag)
376
+ }
377
+
378
+ window.addEventListener('pointermove', handlePointerMove)
379
+ window.addEventListener('pointerup', finishDrag, true)
380
+ window.addEventListener('pointercancel', cancelDrag, true)
381
+ return () => {
382
+ window.removeEventListener('pointermove', handlePointerMove)
383
+ window.removeEventListener('pointerup', finishDrag, true)
384
+ window.removeEventListener('pointercancel', cancelDrag, true)
385
+
386
+ const drag = dragRef.current
387
+ if (!drag || drag.ceilingId !== effectiveCeiling.id) return
388
+ dragRef.current = null
389
+ clearCornerDragPreview(drag)
390
+ }
391
+ }, [effectiveCeiling.id, getHandlePlanePoint, levelId, selectCeilingForEdit])
392
+
393
+ // The brackets render on SCENE_LAYER (scene-depth occlusion), so unlike
394
+ // EDITOR_LAYER affordances the thumbnail camera can't filter them — hide
395
+ // them around captures via synchronous Object3D.visible mutation (the
396
+ // capture renders right after the emit), same as `site-boundary-editor.tsx`.
397
+ useEffect(() => {
398
+ const hideForCapture = () => {
399
+ if (bracketsRootRef.current) bracketsRootRef.current.visible = false
400
+ }
401
+ const restoreAfterCapture = () => {
402
+ if (bracketsRootRef.current) bracketsRootRef.current.visible = true
403
+ }
404
+ emitter.on('thumbnail:before-capture', hideForCapture)
405
+ emitter.on('thumbnail:after-capture', restoreAfterCapture)
406
+ return () => {
407
+ emitter.off('thumbnail:before-capture', hideForCapture)
408
+ emitter.off('thumbnail:after-capture', restoreAfterCapture)
409
+ }
410
+ }, [])
83
411
 
84
412
  useEffect(() => {
85
413
  let frameId = 0
@@ -110,9 +438,31 @@ const CeilingSelectionAffordance = ({
110
438
  if (!levelObject || corners.length === 0) return null
111
439
 
112
440
  return createPortal(
113
- <group position={[0, (ceiling.height ?? 2.5) + BRACKET_Y_OFFSET, 0]}>
441
+ <group
442
+ position={[0, (effectiveCeiling.height ?? 2.5) + BRACKET_Y_OFFSET, 0]}
443
+ ref={bracketsRootRef}
444
+ >
114
445
  {corners.map((corner, index) => (
115
- <CornerBracket ceiling={ceiling} corner={corner} key={`${ceiling.id}-corner-${index}`} />
446
+ <CornerBracket
447
+ ceiling={effectiveCeiling}
448
+ corner={corner}
449
+ highlightIncoming={highlightedEdgeIndices.has(corner.incomingEdgeIndex)}
450
+ highlightOutgoing={highlightedEdgeIndices.has(corner.outgoingEdgeIndex)}
451
+ isHovered={activeCornerIndex === corner.index}
452
+ isLinkedHovered={
453
+ activeCornerIndex !== null &&
454
+ activeCornerIndex !== corner.index &&
455
+ highlightedCornerIndices.has(corner.index)
456
+ }
457
+ key={`${ceiling.id}-corner-${index}`}
458
+ onHoverChange={(hovered) => {
459
+ setHoveredCornerIndex((current) => {
460
+ if (hovered) return corner.index
461
+ return current === corner.index ? null : current
462
+ })
463
+ }}
464
+ onPointerDown={(event) => handleCornerPointerDown(corner, event)}
465
+ />
116
466
  ))}
117
467
  </group>,
118
468
  levelObject,
@@ -122,25 +472,35 @@ const CeilingSelectionAffordance = ({
122
472
  const CornerBracket = ({
123
473
  ceiling,
124
474
  corner,
475
+ highlightIncoming,
476
+ highlightOutgoing,
477
+ isHovered,
478
+ isLinkedHovered,
479
+ onHoverChange,
480
+ onPointerDown,
125
481
  }: {
126
482
  ceiling: CeilingNode
127
483
  corner: CornerBracketData
484
+ highlightIncoming: boolean
485
+ highlightOutgoing: boolean
486
+ isHovered: boolean
487
+ isLinkedHovered: boolean
488
+ onHoverChange: (hovered: boolean) => void
489
+ onPointerDown: (event: ThreeEvent<PointerEvent>) => void
128
490
  }) => {
129
- const [isHovered, setIsHovered] = useState(false)
130
- const color = '#d4d4d4'
131
- const opacity = 0.72
132
- const cubeColor = isHovered ? '#818cf8' : '#d4d4d4'
133
- const cubeOpacity = isHovered ? 0.92 : 0.72
491
+ const cubeHighlighted = isHovered || isLinkedHovered
492
+ const cubeColor = cubeHighlighted ? HANDLE_HOVER_COLOR : HANDLE_COLOR
493
+ const cubeOpacity = cubeHighlighted ? HANDLE_HOVER_OPACITY : HANDLE_OPACITY
134
494
 
135
495
  const handleClick = (e: ThreeEvent<MouseEvent>) => {
136
496
  e.stopPropagation()
137
497
 
138
- const nodes = useScene.getState().nodes
139
-
140
498
  useEditor.getState().setMovingNode(null)
141
- useEditor.getState().setMovingWallEndpoint(null)
142
- useEditor.getState().setCurvingWall(null)
143
- useEditor.getState().setEditingHole(null)
499
+ useInteractionScope
500
+ .getState()
501
+ .endIf((sc) => sc.kind === 'reshaping' && sc.reshape === 'endpoint')
502
+ useInteractionScope.getState().endIf((sc) => sc.kind === 'reshaping' && sc.reshape === 'curve')
503
+ useInteractionScope.getState().endIf((sc) => sc.kind === 'reshaping' && sc.reshape === 'hole')
144
504
  useEditor.getState().setMode('select')
145
505
 
146
506
  emitter.emit('ceiling:click' as any, {
@@ -149,39 +509,53 @@ const CornerBracket = ({
149
509
  localPosition: [0, 0, 0],
150
510
  position: [corner.corner[0], ceiling.height ?? 2.5, corner.corner[1]],
151
511
  stopPropagation: () => e.stopPropagation(),
512
+ viaHandle: true,
152
513
  })
153
514
  }
154
515
 
155
516
  return (
156
517
  <group position={[corner.corner[0], 0, corner.corner[1]]}>
157
518
  <BracketLeg
158
- color={color}
519
+ color={highlightIncoming ? HANDLE_HOVER_COLOR : HANDLE_COLOR}
159
520
  direction={corner.incomingDirection}
521
+ highlighted={highlightIncoming}
160
522
  length={corner.incomingLength}
161
523
  onClick={handleClick}
162
- opacity={opacity}
524
+ onHoverChange={onHoverChange}
525
+ onPointerDown={onPointerDown}
163
526
  />
164
527
  <BracketLeg
165
- color={color}
528
+ color={highlightOutgoing ? HANDLE_HOVER_COLOR : HANDLE_COLOR}
166
529
  direction={corner.outgoingDirection}
530
+ highlighted={highlightOutgoing}
167
531
  length={corner.outgoingLength}
168
532
  onClick={handleClick}
169
- opacity={opacity}
533
+ onHoverChange={onHoverChange}
534
+ onPointerDown={onPointerDown}
170
535
  />
171
536
 
172
537
  <mesh
538
+ geometry={SHARED_HANDLE_BOX_GEOMETRY}
173
539
  onClick={handleClick}
540
+ onPointerDown={onPointerDown}
174
541
  onPointerEnter={(e) => {
175
542
  e.stopPropagation()
176
- setIsHovered(true)
543
+ onHoverChange(true)
177
544
  }}
178
545
  onPointerLeave={(e) => {
179
546
  e.stopPropagation()
180
- setIsHovered(false)
547
+ onHoverChange(false)
181
548
  }}
549
+ renderOrder={CORNER_RENDER_ORDER}
550
+ scale={HIT_BOX_SIZE}
182
551
  >
183
- <boxGeometry args={HIT_BOX_SIZE} />
184
- <meshBasicMaterial color={cubeColor} depthWrite={false} opacity={cubeOpacity} transparent />
552
+ <meshBasicMaterial
553
+ color={cubeColor}
554
+ depthTest
555
+ depthWrite={false}
556
+ opacity={cubeOpacity}
557
+ transparent
558
+ />
185
559
  </mesh>
186
560
  </group>
187
561
  )
@@ -191,16 +565,20 @@ const BracketLeg = ({
191
565
  direction,
192
566
  length,
193
567
  color,
568
+ highlighted,
194
569
  onClick,
195
- opacity,
570
+ onHoverChange,
571
+ onPointerDown,
196
572
  }: {
197
573
  direction: [number, number]
198
574
  length: number
199
575
  color: string
576
+ highlighted: boolean
200
577
  onClick: (e: ThreeEvent<MouseEvent>) => void
201
- opacity: number
578
+ onHoverChange: (hovered: boolean) => void
579
+ onPointerDown: (event: ThreeEvent<PointerEvent>) => void
202
580
  }) => {
203
- const angle = Math.atan2(direction[1], direction[0])
581
+ const angle = -Math.atan2(direction[1], direction[0])
204
582
  const position: [number, number, number] = [
205
583
  direction[0] * (length / 2),
206
584
  0,
@@ -208,9 +586,30 @@ const BracketLeg = ({
208
586
  ]
209
587
 
210
588
  return (
211
- <mesh onClick={onClick} position={position} rotation={[0, angle, 0]}>
212
- <boxGeometry args={[length, BRACKET_HEIGHT, BRACKET_THICKNESS]} />
213
- <meshBasicMaterial color={color} depthWrite={false} opacity={opacity} transparent />
589
+ <mesh
590
+ geometry={SHARED_HANDLE_BOX_GEOMETRY}
591
+ onClick={onClick}
592
+ onPointerDown={onPointerDown}
593
+ onPointerEnter={(e) => {
594
+ e.stopPropagation()
595
+ onHoverChange(true)
596
+ }}
597
+ onPointerLeave={(e) => {
598
+ e.stopPropagation()
599
+ onHoverChange(false)
600
+ }}
601
+ position={position}
602
+ renderOrder={CORNER_RENDER_ORDER}
603
+ rotation={[0, angle, 0]}
604
+ scale={[length, BRACKET_HEIGHT, BRACKET_THICKNESS]}
605
+ >
606
+ <meshBasicMaterial
607
+ color={color}
608
+ depthTest
609
+ depthWrite={false}
610
+ opacity={highlighted ? HANDLE_HOVER_OPACITY : HANDLE_OPACITY}
611
+ transparent
612
+ />
214
613
  </mesh>
215
614
  )
216
615
  }
@@ -218,7 +617,7 @@ const BracketLeg = ({
218
617
  function buildCornerBrackets(polygon: Array<[number, number]>): CornerBracketData[] {
219
618
  if (polygon.length < 3) return []
220
619
 
221
- const allCorners = polygon.map((corner, index) => {
620
+ return polygon.map((corner, index) => {
222
621
  const previous = polygon[(index - 1 + polygon.length) % polygon.length]!
223
622
  const next = polygon[(index + 1) % polygon.length]!
224
623
  const incomingVector = [previous[0] - corner[0], previous[1] - corner[1]] as [number, number]
@@ -228,35 +627,18 @@ function buildCornerBrackets(polygon: Array<[number, number]>): CornerBracketDat
228
627
 
229
628
  const incomingLength = Math.hypot(incomingVector[0], incomingVector[1])
230
629
  const outgoingLength = Math.hypot(outgoingVector[0], outgoingVector[1])
231
- const cornerStrength =
232
- 1 -
233
- Math.abs(
234
- incomingDirection[0] * outgoingDirection[0] + incomingDirection[1] * outgoingDirection[1],
235
- )
236
630
 
237
631
  return {
238
632
  corner,
633
+ index,
634
+ incomingEdgeIndex: (index - 1 + polygon.length) % polygon.length,
239
635
  incomingDirection,
636
+ outgoingEdgeIndex: index,
240
637
  outgoingDirection,
241
638
  incomingLength: getBracketLength(incomingLength),
242
639
  outgoingLength: getBracketLength(outgoingLength),
243
- cornerStrength,
244
640
  }
245
641
  })
246
-
247
- if (allCorners.length <= 4) {
248
- return allCorners
249
- }
250
-
251
- const selectedIndices = new Set(
252
- allCorners
253
- .map((corner, index) => ({ index, strength: corner.cornerStrength }))
254
- .sort((a, b) => b.strength - a.strength)
255
- .slice(0, 4)
256
- .map(({ index }) => index),
257
- )
258
-
259
- return allCorners.filter((_, index) => selectedIndices.has(index))
260
642
  }
261
643
 
262
644
  function normalize2D(vector: [number, number]): [number, number] {