@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
@@ -0,0 +1,581 @@
1
+ 'use client'
2
+
3
+ import { type Cursor, emitter } from '@pascal-app/core'
4
+ import type { ThreeEvent } from '@react-three/fiber'
5
+ import { type ReactNode, useEffect, useMemo, useRef } from 'react'
6
+ import {
7
+ BoxGeometry,
8
+ type BufferGeometry,
9
+ CircleGeometry,
10
+ Color,
11
+ CylinderGeometry,
12
+ DoubleSide,
13
+ ExtrudeGeometry,
14
+ type Group,
15
+ type Intersection,
16
+ Mesh,
17
+ type Raycaster,
18
+ Shape,
19
+ TorusGeometry,
20
+ } from 'three'
21
+ import { mergeGeometries } from 'three/examples/jsm/utils/BufferGeometryUtils.js'
22
+ import { MeshBasicNodeMaterial } from 'three/webgpu'
23
+ import { EDITOR_LAYER } from '../../../lib/constants'
24
+ import useEditor from '../../../store/use-editor'
25
+
26
+ // While a press-drag move is in flight (`placementDragMode`), the move tool
27
+ // owns the pointer and the handle rig rides the moving node — so a handle hit
28
+ // area would sit under the cursor and starve the tool's surface raycast
29
+ // (`wall:move` for openings, `grid:move` for free movers), freezing the drag.
30
+ // Make every handle hit area inert for the duration; the indicator mesh still
31
+ // renders (it's already NO_RAYCAST + depthTest off) so the grip stays visible.
32
+ function hitAreaRaycast(this: Mesh, raycaster: Raycaster, intersects: Intersection[]): void {
33
+ if (useEditor.getState().placementDragMode) return
34
+ Mesh.prototype.raycast.call(this, raycaster, intersects)
35
+ }
36
+
37
+ export const ARROW_SCALE = 0.65
38
+ export const ARROW_COLOR = '#8381ed'
39
+ export const ARROW_HOVER_COLOR = '#a5b4fc'
40
+ export const NO_RAYCAST = () => null
41
+ export const HIT_AREA_MARGIN = 0.035
42
+
43
+ const HIT_AREA_RENDER_ORDER = 1011
44
+ const HIT_AREA_THICKNESS = 0.08
45
+ const CHEVRON_MIN_X = -0.2
46
+ const CHEVRON_MAX_X = 0.22
47
+ const CHEVRON_HALF_WIDTH = 0.12
48
+ const CHEVRON_NOTCH_X = -0.04
49
+ const CHEVRON_SHAFT_HALF_WIDTH = 0.035
50
+ const CHEVRON_DEPTH = 0.08
51
+ const CHEVRON_BEVEL_THICKNESS = 0.035
52
+ const CHEVRON_BEVEL_SIZE = 0.03
53
+ const CHEVRON_BEVEL_SEGMENTS = 10
54
+ // Slimmer extrude profile matching the legacy wall side handles
55
+ // (`wall-move-side-handles.tsx`) — opt-in via the `thin` prop so the chunkier
56
+ // default is preserved for every other handle that uses the shared chevron.
57
+ const CHEVRON_THIN_DEPTH = 0.045
58
+ const CHEVRON_THIN_BEVEL_THICKNESS = 0.018
59
+ const CHEVRON_THIN_BEVEL_SIZE = 0.02
60
+ const CHEVRON_THIN_BEVEL_SEGMENTS = 8
61
+ const MOVE_CROSS_HALF_LENGTH = 0.36
62
+ const MOVE_CROSS_SHAFT_HALF_WIDTH = 0.03
63
+ const MOVE_CROSS_HEAD_HALF_WIDTH = 0.12
64
+ const MOVE_CROSS_HEAD_INSET = 0.2
65
+ const MOVE_CROSS_DEPTH = 0.06
66
+ const MOVE_CROSS_BEVEL_THICKNESS = 0.018
67
+ const MOVE_CROSS_BEVEL_SIZE = 0.012
68
+ const MOVE_CROSS_BEVEL_SEGMENTS = 6
69
+ const ROTATE_HANDLE_RADIUS = 0.2
70
+ const ROTATE_HANDLE_HALF_SWEEP = Math.PI / 3
71
+ const ROTATE_RIBBON_HALF_WIDTH = 0.02
72
+ const ROTATE_HEAD_HALF_WIDTH = 0.045
73
+ const TRACKER_CUBE_SIZE = 0.16
74
+ export const CORNER_HEX_RADIUS = 0.11
75
+
76
+ export type HandleArrowShape = 'chevron' | 'cross' | 'curved-arrow' | 'tracker' | 'corner-picker'
77
+ export type HandleArrowInputShape = HandleArrowShape | 'arrow' | 'move-cross'
78
+
79
+ export type HandleArrowPlacement = {
80
+ position: readonly [number, number, number]
81
+ rotation?: readonly [number, number, number]
82
+ baseScale: number
83
+ }
84
+
85
+ type PointerHandler = (event: ThreeEvent<PointerEvent>) => void
86
+
87
+ export type HandleArrowProps = {
88
+ shape: HandleArrowInputShape
89
+ placement: HandleArrowPlacement
90
+ hover: boolean
91
+ cursor: Cursor
92
+ onHoverChange: (hovered: boolean) => void
93
+ onPointerDown: PointerHandler
94
+ activeCursor?: Cursor
95
+ children?: ReactNode
96
+ hoverScale?: number
97
+ indicatorRotation?: readonly [number, number, number]
98
+ onPointerEnter?: PointerHandler
99
+ onPointerLeave?: PointerHandler
100
+ // Extrude the slimmer wall-handle chevron profile (chevron shape only).
101
+ thin?: boolean
102
+ // Render the corner-picker disc as a smooth circle instead of a hexagon.
103
+ round?: boolean
104
+ }
105
+
106
+ function normalizeHandleArrowShape(shape: HandleArrowInputShape, cursor: Cursor): HandleArrowShape {
107
+ if (shape === 'arrow') return 'chevron'
108
+ if (shape === 'move-cross') return 'cross'
109
+ if (shape === 'chevron' && cursor === 'move') return 'cross'
110
+ return shape
111
+ }
112
+
113
+ // Two-headed curved-arrow silhouette for whole-node rotation handles
114
+ // (today: the elevator's corner rotate gizmo). Symmetric arc centred on
115
+ // +X with sweeps to +/-halfSweep, arrowhead wings + tangentially-extended
116
+ // tips at each end. Drawn in 2D then extruded and rotated to lie in the
117
+ // XZ plane - same final-orientation contract as the chevron, so the
118
+ // outer rotation Y and inner-rotation chain in the renderer are reused
119
+ // unchanged.
120
+ export function createRotateArrowHandleGeometry() {
121
+ const R = 0.2
122
+ const ribbonHalfWidth = 0.02
123
+ const halfSweep = Math.PI / 3
124
+ const headHalfWidth = 0.045
125
+ const headOvershoot = 0.075
126
+ const rIn = R - ribbonHalfWidth
127
+ const rOut = R + ribbonHalfWidth
128
+ const a1 = halfSweep
129
+ const a2 = -halfSweep
130
+
131
+ const tip1: [number, number] = [
132
+ R * Math.cos(a1) - headOvershoot * Math.sin(a1),
133
+ R * Math.sin(a1) + headOvershoot * Math.cos(a1),
134
+ ]
135
+ const tip2: [number, number] = [
136
+ R * Math.cos(a2) + headOvershoot * Math.sin(a2),
137
+ R * Math.sin(a2) - headOvershoot * Math.cos(a2),
138
+ ]
139
+ const innerWing1: [number, number] = [
140
+ (rIn - headHalfWidth) * Math.cos(a1),
141
+ (rIn - headHalfWidth) * Math.sin(a1),
142
+ ]
143
+ const outerWing1: [number, number] = [
144
+ (rOut + headHalfWidth) * Math.cos(a1),
145
+ (rOut + headHalfWidth) * Math.sin(a1),
146
+ ]
147
+ const innerWing2: [number, number] = [
148
+ (rIn - headHalfWidth) * Math.cos(a2),
149
+ (rIn - headHalfWidth) * Math.sin(a2),
150
+ ]
151
+ const outerWing2: [number, number] = [
152
+ (rOut + headHalfWidth) * Math.cos(a2),
153
+ (rOut + headHalfWidth) * Math.sin(a2),
154
+ ]
155
+ const innerCorner1: [number, number] = [rIn * Math.cos(a1), rIn * Math.sin(a1)]
156
+ const outerCorner1: [number, number] = [rOut * Math.cos(a1), rOut * Math.sin(a1)]
157
+ const innerCorner2: [number, number] = [rIn * Math.cos(a2), rIn * Math.sin(a2)]
158
+ const outerCorner2: [number, number] = [rOut * Math.cos(a2), rOut * Math.sin(a2)]
159
+
160
+ const shape = new Shape()
161
+ shape.moveTo(innerCorner1[0], innerCorner1[1])
162
+ shape.lineTo(innerWing1[0], innerWing1[1])
163
+ shape.lineTo(tip1[0], tip1[1])
164
+ shape.lineTo(outerWing1[0], outerWing1[1])
165
+ shape.lineTo(outerCorner1[0], outerCorner1[1])
166
+ shape.absarc(0, 0, rOut, a1, a2, true)
167
+ shape.lineTo(outerWing2[0], outerWing2[1])
168
+ shape.lineTo(tip2[0], tip2[1])
169
+ shape.lineTo(innerWing2[0], innerWing2[1])
170
+ shape.lineTo(innerCorner2[0], innerCorner2[1])
171
+ shape.absarc(0, 0, rIn, a2, a1, false)
172
+ shape.closePath()
173
+
174
+ const geometry = new ExtrudeGeometry(shape, {
175
+ depth: 0.06,
176
+ bevelEnabled: true,
177
+ bevelThickness: 0.018,
178
+ bevelSize: 0.012,
179
+ bevelOffset: 0,
180
+ bevelSegments: 6,
181
+ curveSegments: 24,
182
+ steps: 1,
183
+ })
184
+ geometry.translate(0, 0, -0.03)
185
+ geometry.rotateX(-Math.PI / 2)
186
+ geometry.computeVertexNormals()
187
+ geometry.computeBoundingSphere()
188
+ return geometry
189
+ }
190
+
191
+ // Reused chevron+shaft silhouette. The chevron points along +X by default;
192
+ // callers rotate it around Y for Z-axis handles and into a vertical frame for
193
+ // Y-axis handles. `thin` extrudes the slimmer wall-handle profile.
194
+ export function createArrowHandleGeometry(thin = false) {
195
+ const depth = thin ? CHEVRON_THIN_DEPTH : CHEVRON_DEPTH
196
+ const shape = new Shape()
197
+ shape.moveTo(CHEVRON_MAX_X, 0)
198
+ shape.lineTo(CHEVRON_NOTCH_X, CHEVRON_HALF_WIDTH)
199
+ shape.lineTo(CHEVRON_NOTCH_X, CHEVRON_SHAFT_HALF_WIDTH)
200
+ shape.lineTo(CHEVRON_MIN_X, CHEVRON_SHAFT_HALF_WIDTH)
201
+ shape.lineTo(CHEVRON_MIN_X, -CHEVRON_SHAFT_HALF_WIDTH)
202
+ shape.lineTo(CHEVRON_NOTCH_X, -CHEVRON_SHAFT_HALF_WIDTH)
203
+ shape.lineTo(CHEVRON_NOTCH_X, -CHEVRON_HALF_WIDTH)
204
+ shape.lineTo(CHEVRON_MAX_X, 0)
205
+ const geometry = new ExtrudeGeometry(shape, {
206
+ depth,
207
+ bevelEnabled: true,
208
+ bevelThickness: thin ? CHEVRON_THIN_BEVEL_THICKNESS : CHEVRON_BEVEL_THICKNESS,
209
+ bevelSize: thin ? CHEVRON_THIN_BEVEL_SIZE : CHEVRON_BEVEL_SIZE,
210
+ bevelOffset: 0,
211
+ bevelSegments: thin ? CHEVRON_THIN_BEVEL_SEGMENTS : CHEVRON_BEVEL_SEGMENTS,
212
+ curveSegments: 16,
213
+ steps: 1,
214
+ })
215
+ geometry.translate(0, 0, -depth / 2)
216
+ geometry.rotateX(-Math.PI / 2)
217
+ geometry.computeVertexNormals()
218
+ geometry.computeBoundingSphere()
219
+ return geometry
220
+ }
221
+
222
+ function createDoubleArrowShape(): Shape {
223
+ const shape = new Shape()
224
+ shape.moveTo(MOVE_CROSS_HALF_LENGTH, 0)
225
+ shape.lineTo(MOVE_CROSS_HEAD_INSET, MOVE_CROSS_HEAD_HALF_WIDTH)
226
+ shape.lineTo(MOVE_CROSS_HEAD_INSET, MOVE_CROSS_SHAFT_HALF_WIDTH)
227
+ shape.lineTo(-MOVE_CROSS_HEAD_INSET, MOVE_CROSS_SHAFT_HALF_WIDTH)
228
+ shape.lineTo(-MOVE_CROSS_HEAD_INSET, MOVE_CROSS_HEAD_HALF_WIDTH)
229
+ shape.lineTo(-MOVE_CROSS_HALF_LENGTH, 0)
230
+ shape.lineTo(-MOVE_CROSS_HEAD_INSET, -MOVE_CROSS_HEAD_HALF_WIDTH)
231
+ shape.lineTo(-MOVE_CROSS_HEAD_INSET, -MOVE_CROSS_SHAFT_HALF_WIDTH)
232
+ shape.lineTo(MOVE_CROSS_HEAD_INSET, -MOVE_CROSS_SHAFT_HALF_WIDTH)
233
+ shape.lineTo(MOVE_CROSS_HEAD_INSET, -MOVE_CROSS_HEAD_HALF_WIDTH)
234
+ shape.closePath()
235
+ return shape
236
+ }
237
+
238
+ export function createMoveCrossHandleGeometry() {
239
+ const shape = createDoubleArrowShape()
240
+ const extrudeOpts = {
241
+ depth: MOVE_CROSS_DEPTH,
242
+ bevelEnabled: true,
243
+ bevelThickness: MOVE_CROSS_BEVEL_THICKNESS,
244
+ bevelSize: MOVE_CROSS_BEVEL_SIZE,
245
+ bevelOffset: 0,
246
+ bevelSegments: MOVE_CROSS_BEVEL_SEGMENTS,
247
+ curveSegments: 8,
248
+ steps: 1,
249
+ }
250
+ const armX = new ExtrudeGeometry(shape, extrudeOpts)
251
+ armX.translate(0, 0, -MOVE_CROSS_DEPTH / 2)
252
+ armX.rotateX(-Math.PI / 2)
253
+ const armZ = armX.clone()
254
+ armZ.rotateY(Math.PI / 2)
255
+ const merged = mergeGeometries([armX, armZ], false)
256
+ if (!merged) {
257
+ armZ.dispose()
258
+ armX.computeVertexNormals()
259
+ armX.computeBoundingSphere()
260
+ return armX
261
+ }
262
+ armX.dispose()
263
+ armZ.dispose()
264
+ merged.computeVertexNormals()
265
+ merged.computeBoundingSphere()
266
+ return merged
267
+ }
268
+
269
+ export function createArrowHitAreaGeometry() {
270
+ const length = CHEVRON_MAX_X - CHEVRON_MIN_X + HIT_AREA_MARGIN * 2
271
+ const centerX = (CHEVRON_MIN_X + CHEVRON_MAX_X) / 2
272
+ const geometry = new CylinderGeometry(
273
+ CHEVRON_HALF_WIDTH + HIT_AREA_MARGIN,
274
+ CHEVRON_HALF_WIDTH + HIT_AREA_MARGIN,
275
+ length,
276
+ 16,
277
+ )
278
+ geometry.rotateZ(-Math.PI / 2)
279
+ geometry.translate(centerX, 0, 0)
280
+ geometry.computeBoundingSphere()
281
+ return geometry
282
+ }
283
+
284
+ // The move cross is a plus, not a disk. A disk-shaped hit area fills the four
285
+ // corner gaps between the arms, so a neighbouring node sitting next to the
286
+ // selected node (a lamp by a door, a slab beside a wall) gets swallowed by the
287
+ // invisible grip and can't be picked. Wrap the visible arms instead: two flat
288
+ // arm boxes (length/width + margin) merged into a plus, leaving the corners
289
+ // empty so co-located neighbours stay selectable while the grip stays grabbable.
290
+ function createMoveCrossHitAreaGeometry() {
291
+ const armLength = (MOVE_CROSS_HALF_LENGTH + HIT_AREA_MARGIN) * 2
292
+ const armWidth = (MOVE_CROSS_HEAD_HALF_WIDTH + HIT_AREA_MARGIN) * 2
293
+ const armX = new BoxGeometry(armLength, HIT_AREA_THICKNESS, armWidth)
294
+ const armZ = new BoxGeometry(armWidth, HIT_AREA_THICKNESS, armLength)
295
+ const merged = mergeGeometries([armX, armZ], false)
296
+ if (!merged) {
297
+ armZ.dispose()
298
+ armX.computeBoundingSphere()
299
+ return armX
300
+ }
301
+ armX.dispose()
302
+ armZ.dispose()
303
+ merged.computeBoundingSphere()
304
+ return merged
305
+ }
306
+
307
+ export function createRotateArrowHitAreaGeometry() {
308
+ const halfSweep = ROTATE_HANDLE_HALF_SWEEP + HIT_AREA_MARGIN / ROTATE_HANDLE_RADIUS
309
+ const geometry = new TorusGeometry(
310
+ ROTATE_HANDLE_RADIUS,
311
+ ROTATE_RIBBON_HALF_WIDTH + ROTATE_HEAD_HALF_WIDTH + HIT_AREA_MARGIN,
312
+ 10,
313
+ 64,
314
+ halfSweep * 2,
315
+ )
316
+ geometry.rotateZ(-halfSweep)
317
+ geometry.rotateX(-Math.PI / 2)
318
+ geometry.computeBoundingSphere()
319
+ return geometry
320
+ }
321
+
322
+ function createTrackerHitAreaGeometry() {
323
+ const size = TRACKER_CUBE_SIZE + HIT_AREA_MARGIN * 2
324
+ const geometry = new BoxGeometry(size, size, size)
325
+ geometry.computeBoundingSphere()
326
+ return geometry
327
+ }
328
+
329
+ export function createEndpointHitAreaGeometry(radius: number) {
330
+ const geometry = new CylinderGeometry(
331
+ radius + HIT_AREA_MARGIN,
332
+ radius + HIT_AREA_MARGIN,
333
+ HIT_AREA_THICKNESS,
334
+ 24,
335
+ )
336
+ geometry.rotateX(Math.PI / 2)
337
+ geometry.computeBoundingSphere()
338
+ return geometry
339
+ }
340
+
341
+ // Hexagon (6 segments) by default; a smooth circle (32 segments) when `round`.
342
+ const CORNER_DISC_SEGMENTS = 6
343
+ const CORNER_DISC_ROUND_SEGMENTS = 32
344
+
345
+ function createHandleArrowGeometry(shape: HandleArrowShape, thin = false, round = false) {
346
+ if (shape === 'chevron') return createArrowHandleGeometry(thin)
347
+ if (shape === 'cross') return createMoveCrossHandleGeometry()
348
+ if (shape === 'curved-arrow') return createRotateArrowHandleGeometry()
349
+ if (shape === 'tracker') {
350
+ const geometry = new BoxGeometry(TRACKER_CUBE_SIZE, TRACKER_CUBE_SIZE, TRACKER_CUBE_SIZE)
351
+ geometry.computeBoundingSphere()
352
+ return geometry
353
+ }
354
+ const geometry = new CircleGeometry(
355
+ CORNER_HEX_RADIUS,
356
+ round ? CORNER_DISC_ROUND_SEGMENTS : CORNER_DISC_SEGMENTS,
357
+ )
358
+ geometry.computeBoundingSphere()
359
+ return geometry
360
+ }
361
+
362
+ function createHandleArrowHitGeometry(shape: HandleArrowShape, round = false) {
363
+ if (shape === 'chevron') return createArrowHitAreaGeometry()
364
+ if (shape === 'cross') return createMoveCrossHitAreaGeometry()
365
+ if (shape === 'curved-arrow') return createRotateArrowHitAreaGeometry()
366
+ if (shape === 'tracker') return createTrackerHitAreaGeometry()
367
+ const geometry = new CircleGeometry(
368
+ CORNER_HEX_RADIUS,
369
+ round ? CORNER_DISC_ROUND_SEGMENTS : CORNER_DISC_SEGMENTS,
370
+ )
371
+ geometry.computeBoundingSphere()
372
+ return geometry
373
+ }
374
+
375
+ let sharedHitAreaMaterial: MeshBasicNodeMaterial | null = null
376
+ let sharedHitAreaMaterialRefs = 0
377
+
378
+ function createInvisibleHitAreaMaterial() {
379
+ return new MeshBasicNodeMaterial({
380
+ color: new Color('#ffffff'),
381
+ colorWrite: false,
382
+ depthTest: false,
383
+ depthWrite: false,
384
+ opacity: 0,
385
+ side: DoubleSide,
386
+ transparent: true,
387
+ })
388
+ }
389
+
390
+ export function useInvisibleHitAreaMaterial(): MeshBasicNodeMaterial {
391
+ const materialRef = useRef<MeshBasicNodeMaterial | null>(null)
392
+ if (!materialRef.current) {
393
+ sharedHitAreaMaterial ??= createInvisibleHitAreaMaterial()
394
+ materialRef.current = sharedHitAreaMaterial
395
+ }
396
+ useEffect(() => {
397
+ sharedHitAreaMaterialRefs += 1
398
+ return () => {
399
+ sharedHitAreaMaterialRefs -= 1
400
+ if (sharedHitAreaMaterialRefs <= 0 && sharedHitAreaMaterial) {
401
+ sharedHitAreaMaterial.dispose()
402
+ sharedHitAreaMaterial = null
403
+ sharedHitAreaMaterialRefs = 0
404
+ }
405
+ }
406
+ }, [])
407
+ return materialRef.current
408
+ }
409
+
410
+ export function InvisibleHandleHitArea({
411
+ geometry,
412
+ material,
413
+ onPointerDown,
414
+ onPointerEnter,
415
+ onPointerLeave,
416
+ scale,
417
+ }: {
418
+ geometry: BufferGeometry
419
+ material: MeshBasicNodeMaterial
420
+ onPointerDown: PointerHandler
421
+ onPointerEnter: PointerHandler
422
+ onPointerLeave: PointerHandler
423
+ scale: number
424
+ }) {
425
+ return (
426
+ <mesh
427
+ frustumCulled={false}
428
+ geometry={geometry}
429
+ layers={EDITOR_LAYER}
430
+ material={material}
431
+ onPointerDown={onPointerDown}
432
+ onPointerEnter={onPointerEnter}
433
+ onPointerLeave={onPointerLeave}
434
+ raycast={hitAreaRaycast}
435
+ renderOrder={HIT_AREA_RENDER_ORDER}
436
+ scale={scale}
437
+ />
438
+ )
439
+ }
440
+
441
+ export function useArrowMaterial(): MeshBasicNodeMaterial {
442
+ return useMemo(
443
+ () =>
444
+ new MeshBasicNodeMaterial({
445
+ color: new Color(ARROW_COLOR),
446
+ side: DoubleSide,
447
+ // `depthTest: false` keeps the chevron drawing on top of any
448
+ // geometry under it; `depthWrite: true` puts the chevron's depth
449
+ // into the scenePass buffer so the ink-edge shader's depth
450
+ // Laplacian fires on its silhouette from every angle. Without
451
+ // depthWrite, only the normal-discontinuity branch can detect
452
+ // the chevron, and that signal collapses when the arrow's faces
453
+ // happen to align with whatever sits behind them in screen space
454
+ // - which is why the lines used to drop out depending on the view.
455
+ depthTest: false,
456
+ depthWrite: true,
457
+ transparent: true,
458
+ opacity: 1,
459
+ }),
460
+ [],
461
+ )
462
+ }
463
+
464
+ function useHandleArrowMaterial(shape: HandleArrowShape): MeshBasicNodeMaterial {
465
+ return useMemo(
466
+ () =>
467
+ new MeshBasicNodeMaterial({
468
+ color: new Color(ARROW_COLOR),
469
+ side: DoubleSide,
470
+ transparent: true,
471
+ opacity: shape === 'corner-picker' ? 0.95 : 1,
472
+ depthTest: false,
473
+ depthWrite: shape !== 'corner-picker',
474
+ }),
475
+ [shape],
476
+ )
477
+ }
478
+
479
+ function indicatorRenderOrder(shape: HandleArrowShape) {
480
+ return shape === 'tracker' || shape === 'corner-picker' ? 1003 : 1010
481
+ }
482
+
483
+ export function HandleArrow({
484
+ shape,
485
+ placement,
486
+ hover,
487
+ cursor,
488
+ activeCursor,
489
+ children,
490
+ hoverScale = 1.12,
491
+ indicatorRotation,
492
+ onHoverChange,
493
+ onPointerDown,
494
+ onPointerEnter,
495
+ onPointerLeave,
496
+ thin = false,
497
+ round = false,
498
+ }: HandleArrowProps) {
499
+ const visualShape = normalizeHandleArrowShape(shape, cursor)
500
+ const geometry = useMemo(
501
+ () => createHandleArrowGeometry(visualShape, thin, round),
502
+ [visualShape, thin, round],
503
+ )
504
+ const hitGeometry = useMemo(
505
+ () => createHandleArrowHitGeometry(visualShape, round),
506
+ [visualShape, round],
507
+ )
508
+ const indicatorMaterial = useHandleArrowMaterial(visualShape)
509
+ const hitMaterial = useInvisibleHitAreaMaterial()
510
+ const rootRef = useRef<Group>(null)
511
+ const rotation: [number, number, number] = placement.rotation
512
+ ? [placement.rotation[0], placement.rotation[1], placement.rotation[2]]
513
+ : [0, 0, 0]
514
+ const localRotation: [number, number, number] = indicatorRotation
515
+ ? [indicatorRotation[0], indicatorRotation[1], indicatorRotation[2]]
516
+ : [0, 0, 0]
517
+ const scale = (hover ? hoverScale : 1) * placement.baseScale
518
+ const hitScale = visualShape === 'corner-picker' ? scale : placement.baseScale
519
+
520
+ useEffect(() => {
521
+ indicatorMaterial.color.set(hover ? ARROW_HOVER_COLOR : ARROW_COLOR)
522
+ }, [indicatorMaterial, hover])
523
+ useEffect(() => {
524
+ const hideForCapture = () => {
525
+ if (rootRef.current) rootRef.current.visible = false
526
+ }
527
+ const restoreAfterCapture = () => {
528
+ if (rootRef.current) rootRef.current.visible = true
529
+ }
530
+ emitter.on('thumbnail:before-capture', hideForCapture)
531
+ emitter.on('thumbnail:after-capture', restoreAfterCapture)
532
+ return () => {
533
+ emitter.off('thumbnail:before-capture', hideForCapture)
534
+ emitter.off('thumbnail:after-capture', restoreAfterCapture)
535
+ }
536
+ }, [])
537
+ useEffect(() => () => geometry.dispose(), [geometry])
538
+ useEffect(() => () => hitGeometry.dispose(), [hitGeometry])
539
+ useEffect(() => () => indicatorMaterial.dispose(), [indicatorMaterial])
540
+
541
+ const handleEnter: PointerHandler = (event) => {
542
+ event.stopPropagation()
543
+ onHoverChange(true)
544
+ if (!activeCursor || document.body.style.cursor !== activeCursor) {
545
+ document.body.style.cursor = cursor
546
+ }
547
+ onPointerEnter?.(event)
548
+ }
549
+ const handleLeave: PointerHandler = (event) => {
550
+ event.stopPropagation()
551
+ onHoverChange(false)
552
+ if (document.body.style.cursor === cursor) {
553
+ document.body.style.cursor = ''
554
+ }
555
+ onPointerLeave?.(event)
556
+ }
557
+
558
+ return (
559
+ <group position={placement.position} ref={rootRef} rotation={rotation}>
560
+ {children}
561
+ <group rotation={localRotation}>
562
+ <InvisibleHandleHitArea
563
+ geometry={hitGeometry}
564
+ material={hitMaterial}
565
+ onPointerDown={onPointerDown}
566
+ onPointerEnter={handleEnter}
567
+ onPointerLeave={handleLeave}
568
+ scale={hitScale}
569
+ />
570
+ <mesh
571
+ frustumCulled={false}
572
+ geometry={geometry}
573
+ material={indicatorMaterial}
574
+ raycast={NO_RAYCAST}
575
+ renderOrder={indicatorRenderOrder(visualShape)}
576
+ scale={scale}
577
+ />
578
+ </group>
579
+ </group>
580
+ )
581
+ }