@pascal-app/nodes 1.0.0-beta.5 → 1.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (929) hide show
  1. package/LICENSE +21 -0
  2. package/dist/block/commands.d.ts +68 -0
  3. package/dist/block/commands.d.ts.map +1 -0
  4. package/dist/block/commands.js +1144 -0
  5. package/dist/block/contextual-help.d.ts +3 -0
  6. package/dist/block/contextual-help.d.ts.map +1 -0
  7. package/dist/block/contextual-help.js +38 -0
  8. package/dist/block/definition.d.ts +8 -0
  9. package/dist/block/definition.d.ts.map +1 -0
  10. package/dist/block/definition.js +126 -0
  11. package/dist/block/edit-session.d.ts +16 -0
  12. package/dist/block/edit-session.d.ts.map +1 -0
  13. package/dist/block/edit-session.js +32 -0
  14. package/dist/block/face-host.d.ts +3 -0
  15. package/dist/block/face-host.d.ts.map +1 -0
  16. package/dist/block/face-host.js +178 -0
  17. package/dist/block/floorplan.d.ts +3 -0
  18. package/dist/block/floorplan.d.ts.map +1 -0
  19. package/dist/block/floorplan.js +43 -0
  20. package/dist/block/geometry-snap.d.ts +18 -0
  21. package/dist/block/geometry-snap.d.ts.map +1 -0
  22. package/dist/block/geometry-snap.js +137 -0
  23. package/dist/block/geometry.d.ts +11 -0
  24. package/dist/block/geometry.d.ts.map +1 -0
  25. package/dist/block/geometry.js +146 -0
  26. package/dist/block/gesture-wheel.d.ts +8 -0
  27. package/dist/block/gesture-wheel.d.ts.map +1 -0
  28. package/dist/block/gesture-wheel.js +7 -0
  29. package/dist/block/interaction-sfx.d.ts +3 -0
  30. package/dist/block/interaction-sfx.d.ts.map +1 -0
  31. package/dist/block/interaction-sfx.js +16 -0
  32. package/dist/block/last-operation.d.ts +48 -0
  33. package/dist/block/last-operation.d.ts.map +1 -0
  34. package/dist/block/last-operation.js +120 -0
  35. package/dist/block/loop-cut-interaction.d.ts +5 -0
  36. package/dist/block/loop-cut-interaction.d.ts.map +1 -0
  37. package/dist/block/loop-cut-interaction.js +13 -0
  38. package/dist/block/material-slots.d.ts +61 -0
  39. package/dist/block/material-slots.d.ts.map +1 -0
  40. package/dist/block/material-slots.js +131 -0
  41. package/dist/block/modal-face-operation.d.ts +13 -0
  42. package/dist/block/modal-face-operation.d.ts.map +1 -0
  43. package/dist/block/modal-face-operation.js +40 -0
  44. package/dist/block/modal-session.d.ts +15 -0
  45. package/dist/block/modal-session.d.ts.map +1 -0
  46. package/dist/block/modal-session.js +40 -0
  47. package/dist/block/modal-transform.d.ts +30 -0
  48. package/dist/block/modal-transform.d.ts.map +1 -0
  49. package/dist/block/modal-transform.js +144 -0
  50. package/dist/block/paint.d.ts +3 -0
  51. package/dist/block/paint.d.ts.map +1 -0
  52. package/dist/block/paint.js +136 -0
  53. package/dist/block/panel.d.ts +2 -0
  54. package/dist/block/panel.d.ts.map +1 -0
  55. package/dist/block/panel.js +234 -0
  56. package/dist/block/parametrics.d.ts +4 -0
  57. package/dist/block/parametrics.d.ts.map +1 -0
  58. package/dist/block/parametrics.js +9 -0
  59. package/dist/block/preview.d.ts +6 -0
  60. package/dist/block/preview.d.ts.map +1 -0
  61. package/dist/block/preview.js +46 -0
  62. package/dist/block/rotation-drag.d.ts +5 -0
  63. package/dist/block/rotation-drag.d.ts.map +1 -0
  64. package/dist/block/rotation-drag.js +18 -0
  65. package/dist/block/schema.d.ts +2 -0
  66. package/dist/block/schema.d.ts.map +1 -0
  67. package/dist/block/schema.js +1 -0
  68. package/dist/block/selection-geometry.d.ts +9 -0
  69. package/dist/block/selection-geometry.d.ts.map +1 -0
  70. package/dist/block/selection-geometry.js +31 -0
  71. package/dist/block/selection-model.d.ts +17 -0
  72. package/dist/block/selection-model.d.ts.map +1 -0
  73. package/dist/block/selection-model.js +84 -0
  74. package/dist/block/selection.d.ts +4 -0
  75. package/dist/block/selection.d.ts.map +1 -0
  76. package/dist/block/selection.js +2570 -0
  77. package/dist/block/slots.d.ts +4 -0
  78. package/dist/block/slots.d.ts.map +1 -0
  79. package/dist/block/slots.js +17 -0
  80. package/dist/block/tool.d.ts +3 -0
  81. package/dist/block/tool.d.ts.map +1 -0
  82. package/dist/block/tool.js +174 -0
  83. package/dist/block/toolbar-state.d.ts +46 -0
  84. package/dist/block/toolbar-state.d.ts.map +1 -0
  85. package/dist/block/toolbar-state.js +67 -0
  86. package/dist/block/use-block-face-operation.d.ts +39 -0
  87. package/dist/block/use-block-face-operation.d.ts.map +1 -0
  88. package/dist/block/use-block-face-operation.js +218 -0
  89. package/dist/box-vent/definition.d.ts.map +1 -1
  90. package/dist/box-vent/definition.js +8 -5
  91. package/dist/box-vent/geometry.d.ts +4 -0
  92. package/dist/box-vent/geometry.d.ts.map +1 -1
  93. package/dist/box-vent/geometry.js +76 -44
  94. package/dist/box-vent/paint.d.ts +4 -0
  95. package/dist/box-vent/paint.d.ts.map +1 -0
  96. package/dist/box-vent/paint.js +40 -0
  97. package/dist/box-vent/panel.js +4 -4
  98. package/dist/box-vent/parametrics.js +3 -3
  99. package/dist/box-vent/renderer.d.ts.map +1 -1
  100. package/dist/box-vent/renderer.js +29 -10
  101. package/dist/cabinet/appliance-layout.d.ts +26 -0
  102. package/dist/cabinet/appliance-layout.d.ts.map +1 -0
  103. package/dist/cabinet/appliance-layout.js +52 -0
  104. package/dist/cabinet/compartment-card.d.ts.map +1 -1
  105. package/dist/cabinet/compartment-card.js +11 -3
  106. package/dist/cabinet/continuous-placement.d.ts +3 -0
  107. package/dist/cabinet/continuous-placement.d.ts.map +1 -1
  108. package/dist/cabinet/continuous-placement.js +6 -0
  109. package/dist/cabinet/countertop-layout.d.ts +173 -0
  110. package/dist/cabinet/countertop-layout.d.ts.map +1 -0
  111. package/dist/cabinet/countertop-layout.js +80 -0
  112. package/dist/cabinet/definition.d.ts.map +1 -1
  113. package/dist/cabinet/definition.js +414 -61
  114. package/dist/cabinet/floorplan-move.d.ts.map +1 -1
  115. package/dist/cabinet/floorplan-move.js +91 -22
  116. package/dist/cabinet/geometry/cooktop.d.ts.map +1 -1
  117. package/dist/cabinet/geometry/cooktop.js +2 -2
  118. package/dist/cabinet/geometry/fridge.d.ts.map +1 -1
  119. package/dist/cabinet/geometry/fridge.js +25 -1
  120. package/dist/cabinet/geometry/fronts.d.ts +4 -0
  121. package/dist/cabinet/geometry/fronts.d.ts.map +1 -1
  122. package/dist/cabinet/geometry/fronts.js +1 -1
  123. package/dist/cabinet/geometry/run.d.ts.map +1 -1
  124. package/dist/cabinet/geometry/run.js +28 -71
  125. package/dist/cabinet/geometry/sink.d.ts +2 -12
  126. package/dist/cabinet/geometry/sink.d.ts.map +1 -1
  127. package/dist/cabinet/geometry/sink.js +8 -39
  128. package/dist/cabinet/geometry.d.ts.map +1 -1
  129. package/dist/cabinet/geometry.js +63 -1
  130. package/dist/cabinet/hosted-resize.d.ts +8 -0
  131. package/dist/cabinet/hosted-resize.d.ts.map +1 -0
  132. package/dist/cabinet/hosted-resize.js +158 -0
  133. package/dist/cabinet/index.d.ts +1 -0
  134. package/dist/cabinet/index.d.ts.map +1 -1
  135. package/dist/cabinet/index.js +1 -0
  136. package/dist/cabinet/insertion.d.ts +19 -0
  137. package/dist/cabinet/insertion.d.ts.map +1 -0
  138. package/dist/cabinet/insertion.js +49 -0
  139. package/dist/cabinet/move-frame.d.ts.map +1 -1
  140. package/dist/cabinet/move-frame.js +68 -2
  141. package/dist/cabinet/panel-context.d.ts +7 -0
  142. package/dist/cabinet/panel-context.d.ts.map +1 -0
  143. package/dist/cabinet/panel-context.js +16 -0
  144. package/dist/cabinet/panel-visibility.d.ts +9 -0
  145. package/dist/cabinet/panel-visibility.d.ts.map +1 -0
  146. package/dist/cabinet/panel-visibility.js +26 -0
  147. package/dist/cabinet/panel.d.ts.map +1 -1
  148. package/dist/cabinet/panel.js +221 -37
  149. package/dist/cabinet/placement-dimensions.d.ts +42 -0
  150. package/dist/cabinet/placement-dimensions.d.ts.map +1 -0
  151. package/dist/cabinet/placement-dimensions.js +227 -0
  152. package/dist/cabinet/placement-snap.d.ts +21 -1
  153. package/dist/cabinet/placement-snap.d.ts.map +1 -1
  154. package/dist/cabinet/placement-snap.js +48 -6
  155. package/dist/cabinet/preset-width-debt.d.ts +7 -0
  156. package/dist/cabinet/preset-width-debt.d.ts.map +1 -0
  157. package/dist/cabinet/preset-width-debt.js +45 -0
  158. package/dist/cabinet/presets.d.ts.map +1 -1
  159. package/dist/cabinet/presets.js +14 -13
  160. package/dist/cabinet/profiles.d.ts +14 -0
  161. package/dist/cabinet/profiles.d.ts.map +1 -0
  162. package/dist/cabinet/profiles.js +30 -0
  163. package/dist/cabinet/quick-actions.d.ts.map +1 -1
  164. package/dist/cabinet/quick-actions.js +8 -24
  165. package/dist/cabinet/reveals.d.ts +37 -0
  166. package/dist/cabinet/reveals.d.ts.map +1 -0
  167. package/dist/cabinet/reveals.js +13 -0
  168. package/dist/cabinet/run-layout.d.ts +98 -5
  169. package/dist/cabinet/run-layout.d.ts.map +1 -1
  170. package/dist/cabinet/run-layout.js +430 -37
  171. package/dist/cabinet/run-ops.d.ts +204 -12
  172. package/dist/cabinet/run-ops.d.ts.map +1 -1
  173. package/dist/cabinet/run-ops.js +523 -36
  174. package/dist/cabinet/run-panel.d.ts +6 -2
  175. package/dist/cabinet/run-panel.d.ts.map +1 -1
  176. package/dist/cabinet/run-panel.js +294 -108
  177. package/dist/cabinet/stack-transitions.d.ts.map +1 -1
  178. package/dist/cabinet/stack-transitions.js +35 -24
  179. package/dist/cabinet/stack.d.ts +169 -1
  180. package/dist/cabinet/stack.d.ts.map +1 -1
  181. package/dist/cabinet/stack.js +57 -18
  182. package/dist/cabinet/surfaces.d.ts +4 -0
  183. package/dist/cabinet/surfaces.d.ts.map +1 -0
  184. package/dist/cabinet/surfaces.js +101 -0
  185. package/dist/cabinet/tool.d.ts.map +1 -1
  186. package/dist/cabinet/tool.js +615 -91
  187. package/dist/cabinet/tree-structure.d.ts.map +1 -1
  188. package/dist/cabinet/tree-structure.js +10 -9
  189. package/dist/cabinet/validation.d.ts +27 -0
  190. package/dist/cabinet/validation.d.ts.map +1 -0
  191. package/dist/cabinet/validation.js +56 -0
  192. package/dist/cabinet/wall-height-presets.d.ts +12 -0
  193. package/dist/cabinet/wall-height-presets.d.ts.map +1 -0
  194. package/dist/cabinet/wall-height-presets.js +15 -0
  195. package/dist/cabinet/wall-snap.d.ts +27 -3
  196. package/dist/cabinet/wall-snap.d.ts.map +1 -1
  197. package/dist/cabinet/wall-snap.js +278 -62
  198. package/dist/cabinet/widths.d.ts +10 -0
  199. package/dist/cabinet/widths.d.ts.map +1 -0
  200. package/dist/cabinet/widths.js +13 -0
  201. package/dist/ceiling/definition.d.ts.map +1 -1
  202. package/dist/ceiling/definition.js +8 -2
  203. package/dist/ceiling/floorplan-affordances.d.ts +12 -12
  204. package/dist/ceiling/materials.d.ts.map +1 -1
  205. package/dist/ceiling/materials.js +0 -1
  206. package/dist/ceiling/panel.js +1 -1
  207. package/dist/ceiling/parametrics.js +1 -1
  208. package/dist/chimney/definition.js +1 -1
  209. package/dist/chimney/panel.js +9 -9
  210. package/dist/chimney/parametrics.js +3 -3
  211. package/dist/column/definition.d.ts.map +1 -1
  212. package/dist/column/definition.js +22 -4
  213. package/dist/column/panel.d.ts.map +1 -1
  214. package/dist/column/panel.js +37 -7
  215. package/dist/column/parametrics.d.ts.map +1 -1
  216. package/dist/column/parametrics.js +5 -3
  217. package/dist/column/tool.d.ts.map +1 -1
  218. package/dist/column/tool.js +50 -14
  219. package/dist/cupola/definition.d.ts.map +1 -1
  220. package/dist/cupola/definition.js +15 -6
  221. package/dist/cupola/geometry.d.ts +6 -0
  222. package/dist/cupola/geometry.d.ts.map +1 -1
  223. package/dist/cupola/geometry.js +60 -13
  224. package/dist/cupola/paint.d.ts +4 -0
  225. package/dist/cupola/paint.d.ts.map +1 -0
  226. package/dist/cupola/paint.js +39 -0
  227. package/dist/cupola/panel.js +1 -1
  228. package/dist/cupola/parametrics.js +3 -3
  229. package/dist/cupola/renderer.d.ts.map +1 -1
  230. package/dist/cupola/renderer.js +27 -8
  231. package/dist/door/floorplan-move.d.ts.map +1 -1
  232. package/dist/door/floorplan-move.js +1 -1
  233. package/dist/door/move-tool.d.ts.map +1 -1
  234. package/dist/door/move-tool.js +86 -32
  235. package/dist/door/panel.d.ts.map +1 -1
  236. package/dist/door/panel.js +12 -12
  237. package/dist/door/parametrics.js +2 -2
  238. package/dist/door/tool.d.ts.map +1 -1
  239. package/dist/door/tool.js +16 -4
  240. package/dist/dormer/csg-geometry.d.ts +6 -42
  241. package/dist/dormer/csg-geometry.d.ts.map +1 -1
  242. package/dist/dormer/csg-geometry.js +38 -209
  243. package/dist/dormer/definition.d.ts +4 -12
  244. package/dist/dormer/definition.d.ts.map +1 -1
  245. package/dist/dormer/definition.js +18 -193
  246. package/dist/dormer/floorplan.d.ts +3 -3
  247. package/dist/dormer/floorplan.d.ts.map +1 -1
  248. package/dist/dormer/floorplan.js +6 -17
  249. package/dist/dormer/geometry.d.ts +5 -24
  250. package/dist/dormer/geometry.d.ts.map +1 -1
  251. package/dist/dormer/geometry.js +64 -41
  252. package/dist/dormer/index.d.ts +1 -1
  253. package/dist/dormer/index.d.ts.map +1 -1
  254. package/dist/dormer/index.js +1 -1
  255. package/dist/dormer/move-tool.d.ts.map +1 -1
  256. package/dist/dormer/move-tool.js +31 -3
  257. package/dist/dormer/panel-position-section.js +2 -2
  258. package/dist/dormer/panel-windows-section.d.ts +9 -0
  259. package/dist/dormer/panel-windows-section.d.ts.map +1 -0
  260. package/dist/dormer/panel-windows-section.js +7 -0
  261. package/dist/dormer/panel.d.ts.map +1 -1
  262. package/dist/dormer/panel.js +100 -18
  263. package/dist/dormer/parametrics.d.ts.map +1 -1
  264. package/dist/dormer/parametrics.js +8 -77
  265. package/dist/dormer/preview.d.ts.map +1 -1
  266. package/dist/dormer/preview.js +9 -1
  267. package/dist/dormer/renderer.d.ts.map +1 -1
  268. package/dist/dormer/renderer.js +24 -26
  269. package/dist/dormer/tool.d.ts.map +1 -1
  270. package/dist/dormer/tool.js +3 -1
  271. package/dist/dormer/window-layout.d.ts +15 -0
  272. package/dist/dormer/window-layout.d.ts.map +1 -0
  273. package/dist/dormer/window-layout.js +59 -0
  274. package/dist/downspout/definition.d.ts.map +1 -1
  275. package/dist/downspout/definition.js +17 -4
  276. package/dist/downspout/geometry.d.ts.map +1 -1
  277. package/dist/downspout/geometry.js +10 -0
  278. package/dist/downspout/inspector-editors.d.ts.map +1 -1
  279. package/dist/downspout/inspector-editors.js +9 -1
  280. package/dist/downspout/parametrics.d.ts.map +1 -1
  281. package/dist/downspout/parametrics.js +2 -1
  282. package/dist/downspout/renderer.d.ts.map +1 -1
  283. package/dist/downspout/renderer.js +19 -3
  284. package/dist/downspout/system.d.ts +7 -0
  285. package/dist/downspout/system.d.ts.map +1 -0
  286. package/dist/downspout/system.js +114 -0
  287. package/dist/duct-fitting/accessory-geometry.d.ts +4 -0
  288. package/dist/duct-fitting/accessory-geometry.d.ts.map +1 -0
  289. package/dist/duct-fitting/accessory-geometry.js +47 -0
  290. package/dist/duct-fitting/definition.d.ts.map +1 -1
  291. package/dist/duct-fitting/definition.js +11 -1
  292. package/dist/duct-fitting/floorplan.d.ts.map +1 -1
  293. package/dist/duct-fitting/floorplan.js +8 -0
  294. package/dist/duct-fitting/geometry.d.ts.map +1 -1
  295. package/dist/duct-fitting/geometry.js +52 -38
  296. package/dist/duct-fitting/parametrics.d.ts.map +1 -1
  297. package/dist/duct-fitting/parametrics.js +78 -15
  298. package/dist/duct-fitting/ports.d.ts +1 -0
  299. package/dist/duct-fitting/ports.d.ts.map +1 -1
  300. package/dist/duct-fitting/ports.js +46 -0
  301. package/dist/duct-fitting/tool.d.ts +19 -0
  302. package/dist/duct-fitting/tool.d.ts.map +1 -1
  303. package/dist/duct-fitting/tool.js +126 -34
  304. package/dist/duct-segment/continuation.d.ts +21 -0
  305. package/dist/duct-segment/continuation.d.ts.map +1 -0
  306. package/dist/duct-segment/continuation.js +167 -0
  307. package/dist/duct-segment/definition.d.ts.map +1 -1
  308. package/dist/duct-segment/definition.js +19 -18
  309. package/dist/duct-segment/floorplan.d.ts.map +1 -1
  310. package/dist/duct-segment/floorplan.js +43 -0
  311. package/dist/duct-segment/geometry.d.ts.map +1 -1
  312. package/dist/duct-segment/geometry.js +3 -0
  313. package/dist/duct-segment/move-tool.d.ts.map +1 -1
  314. package/dist/duct-segment/move-tool.js +33 -6
  315. package/dist/duct-segment/parametrics.d.ts.map +1 -1
  316. package/dist/duct-segment/parametrics.js +49 -1
  317. package/dist/duct-segment/selection.d.ts +3 -4
  318. package/dist/duct-segment/selection.d.ts.map +1 -1
  319. package/dist/duct-segment/selection.js +186 -158
  320. package/dist/duct-segment/tool.d.ts +38 -0
  321. package/dist/duct-segment/tool.d.ts.map +1 -1
  322. package/dist/duct-segment/tool.js +306 -677
  323. package/dist/duct-terminal/parametrics.js +2 -2
  324. package/dist/duct-terminal/tool.d.ts.map +1 -1
  325. package/dist/duct-terminal/tool.js +37 -7
  326. package/dist/elevator/panel.d.ts.map +1 -1
  327. package/dist/elevator/panel.js +7 -7
  328. package/dist/elevator/system.d.ts.map +1 -1
  329. package/dist/elevator/system.js +10 -1
  330. package/dist/eyebrow-vent/definition.d.ts.map +1 -1
  331. package/dist/eyebrow-vent/definition.js +8 -5
  332. package/dist/eyebrow-vent/geometry.d.ts +4 -0
  333. package/dist/eyebrow-vent/geometry.d.ts.map +1 -1
  334. package/dist/eyebrow-vent/geometry.js +60 -15
  335. package/dist/eyebrow-vent/paint.d.ts +4 -0
  336. package/dist/eyebrow-vent/paint.d.ts.map +1 -0
  337. package/dist/eyebrow-vent/paint.js +33 -0
  338. package/dist/eyebrow-vent/panel.js +1 -1
  339. package/dist/eyebrow-vent/parametrics.js +3 -3
  340. package/dist/eyebrow-vent/renderer.d.ts.map +1 -1
  341. package/dist/eyebrow-vent/renderer.js +27 -8
  342. package/dist/fence/definition.d.ts.map +1 -1
  343. package/dist/fence/definition.js +2 -1
  344. package/dist/fence/floorplan-affordances.d.ts +1 -0
  345. package/dist/fence/floorplan-affordances.d.ts.map +1 -1
  346. package/dist/fence/floorplan-affordances.js +30 -1
  347. package/dist/fence/floorplan.d.ts.map +1 -1
  348. package/dist/fence/floorplan.js +14 -0
  349. package/dist/fence/inspector-editors.js +1 -1
  350. package/dist/fence/parametrics.js +3 -3
  351. package/dist/fence/tool.d.ts.map +1 -1
  352. package/dist/fence/tool.js +47 -20
  353. package/dist/gutter/corner-mitre.d.ts.map +1 -1
  354. package/dist/gutter/corner-mitre.js +47 -7
  355. package/dist/gutter/definition.d.ts.map +1 -1
  356. package/dist/gutter/definition.js +23 -13
  357. package/dist/gutter/eave-align.d.ts.map +1 -1
  358. package/dist/gutter/eave-align.js +10 -0
  359. package/dist/gutter/eave-snap.d.ts +1 -1
  360. package/dist/gutter/eave-snap.d.ts.map +1 -1
  361. package/dist/gutter/eave-snap.js +10 -23
  362. package/dist/gutter/geometry.d.ts.map +1 -1
  363. package/dist/gutter/geometry.js +126 -8
  364. package/dist/gutter/outlet-lookup.d.ts.map +1 -1
  365. package/dist/gutter/outlet-lookup.js +22 -1
  366. package/dist/gutter/paint.d.ts +3 -0
  367. package/dist/gutter/paint.d.ts.map +1 -0
  368. package/dist/gutter/paint.js +6 -0
  369. package/dist/gutter/parametrics.d.ts.map +1 -1
  370. package/dist/gutter/parametrics.js +4 -1
  371. package/dist/gutter/renderer.d.ts.map +1 -1
  372. package/dist/gutter/renderer.js +63 -7
  373. package/dist/gutter/trim-clip.d.ts +3 -0
  374. package/dist/gutter/trim-clip.d.ts.map +1 -0
  375. package/dist/gutter/trim-clip.js +5 -0
  376. package/dist/hvac-equipment/parametrics.js +3 -3
  377. package/dist/hvac-equipment/tool.d.ts.map +1 -1
  378. package/dist/hvac-equipment/tool.js +12 -2
  379. package/dist/imported-mesh/definition.d.ts +5 -0
  380. package/dist/imported-mesh/definition.d.ts.map +1 -0
  381. package/dist/imported-mesh/definition.js +38 -0
  382. package/dist/imported-mesh/floorplan.d.ts +4 -0
  383. package/dist/imported-mesh/floorplan.d.ts.map +1 -0
  384. package/dist/imported-mesh/floorplan.js +34 -0
  385. package/dist/imported-mesh/geometry.d.ts +5 -0
  386. package/dist/imported-mesh/geometry.d.ts.map +1 -0
  387. package/dist/imported-mesh/geometry.js +34 -0
  388. package/dist/imported-mesh/index.d.ts +4 -0
  389. package/dist/imported-mesh/index.d.ts.map +1 -0
  390. package/dist/imported-mesh/index.js +3 -0
  391. package/dist/imported-mesh/schema.d.ts +2 -0
  392. package/dist/imported-mesh/schema.d.ts.map +1 -0
  393. package/dist/imported-mesh/schema.js +1 -0
  394. package/dist/index.d.ts +7 -2
  395. package/dist/index.d.ts.map +1 -1
  396. package/dist/index.js +17 -2
  397. package/dist/item/definition.d.ts.map +1 -1
  398. package/dist/item/definition.js +10 -2
  399. package/dist/item/floorplan-move.d.ts +9 -1
  400. package/dist/item/floorplan-move.d.ts.map +1 -1
  401. package/dist/item/floorplan-move.js +74 -2
  402. package/dist/item/floorplan.d.ts.map +1 -1
  403. package/dist/item/floorplan.js +33 -1
  404. package/dist/item/move-tool.d.ts +28 -1
  405. package/dist/item/move-tool.d.ts.map +1 -1
  406. package/dist/item/move-tool.js +15 -3
  407. package/dist/item/panel.js +1 -1
  408. package/dist/item/renderer.d.ts.map +1 -1
  409. package/dist/item/renderer.js +16 -6
  410. package/dist/item/system.d.ts +5 -0
  411. package/dist/item/system.d.ts.map +1 -1
  412. package/dist/item/system.js +7 -1
  413. package/dist/lean-to-extension/arc.d.ts +14 -0
  414. package/dist/lean-to-extension/arc.d.ts.map +1 -0
  415. package/dist/lean-to-extension/arc.js +56 -0
  416. package/dist/lean-to-extension/assembly.d.ts +61 -0
  417. package/dist/lean-to-extension/assembly.d.ts.map +1 -0
  418. package/dist/lean-to-extension/assembly.js +761 -0
  419. package/dist/lean-to-extension/canopy-joint.d.ts +35 -0
  420. package/dist/lean-to-extension/canopy-joint.d.ts.map +1 -0
  421. package/dist/lean-to-extension/canopy-joint.js +234 -0
  422. package/dist/lean-to-extension/conical-host.d.ts +15 -0
  423. package/dist/lean-to-extension/conical-host.d.ts.map +1 -0
  424. package/dist/lean-to-extension/conical-host.js +113 -0
  425. package/dist/lean-to-extension/corner-joint.d.ts +29 -0
  426. package/dist/lean-to-extension/corner-joint.d.ts.map +1 -0
  427. package/dist/lean-to-extension/corner-joint.js +1202 -0
  428. package/dist/lean-to-extension/definition.d.ts +4 -0
  429. package/dist/lean-to-extension/definition.d.ts.map +1 -0
  430. package/dist/lean-to-extension/definition.js +433 -0
  431. package/dist/lean-to-extension/floorplan-affordances.d.ts +4 -0
  432. package/dist/lean-to-extension/floorplan-affordances.d.ts.map +1 -0
  433. package/dist/lean-to-extension/floorplan-affordances.js +144 -0
  434. package/dist/lean-to-extension/floorplan-move.d.ts +3 -0
  435. package/dist/lean-to-extension/floorplan-move.d.ts.map +1 -0
  436. package/dist/lean-to-extension/floorplan-move.js +157 -0
  437. package/dist/lean-to-extension/floorplan-tool.d.ts +4 -0
  438. package/dist/lean-to-extension/floorplan-tool.d.ts.map +1 -0
  439. package/dist/lean-to-extension/floorplan-tool.js +385 -0
  440. package/dist/lean-to-extension/floorplan.d.ts +3 -0
  441. package/dist/lean-to-extension/floorplan.d.ts.map +1 -0
  442. package/dist/lean-to-extension/floorplan.js +373 -0
  443. package/dist/lean-to-extension/geometry.d.ts +7 -0
  444. package/dist/lean-to-extension/geometry.d.ts.map +1 -0
  445. package/dist/lean-to-extension/geometry.js +662 -0
  446. package/dist/lean-to-extension/index.d.ts +8 -0
  447. package/dist/lean-to-extension/index.d.ts.map +1 -0
  448. package/dist/lean-to-extension/index.js +7 -0
  449. package/dist/lean-to-extension/layout.d.ts +108 -0
  450. package/dist/lean-to-extension/layout.d.ts.map +1 -0
  451. package/dist/lean-to-extension/layout.js +477 -0
  452. package/dist/lean-to-extension/managed-preview.d.ts +3 -0
  453. package/dist/lean-to-extension/managed-preview.d.ts.map +1 -0
  454. package/dist/lean-to-extension/managed-preview.js +93 -0
  455. package/dist/lean-to-extension/move-tool.d.ts +8 -0
  456. package/dist/lean-to-extension/move-tool.d.ts.map +1 -0
  457. package/dist/lean-to-extension/move-tool.js +202 -0
  458. package/dist/lean-to-extension/paint.d.ts +2 -0
  459. package/dist/lean-to-extension/paint.d.ts.map +1 -0
  460. package/dist/lean-to-extension/paint.js +16 -0
  461. package/dist/lean-to-extension/parametrics.d.ts +4 -0
  462. package/dist/lean-to-extension/parametrics.d.ts.map +1 -0
  463. package/dist/lean-to-extension/parametrics.js +570 -0
  464. package/dist/lean-to-extension/placement-scope.d.ts +3 -0
  465. package/dist/lean-to-extension/placement-scope.d.ts.map +1 -0
  466. package/dist/lean-to-extension/placement-scope.js +4 -0
  467. package/dist/lean-to-extension/placement-validation.d.ts +4 -0
  468. package/dist/lean-to-extension/placement-validation.d.ts.map +1 -0
  469. package/dist/lean-to-extension/placement-validation.js +324 -0
  470. package/dist/lean-to-extension/placement.d.ts +59 -0
  471. package/dist/lean-to-extension/placement.d.ts.map +1 -0
  472. package/dist/lean-to-extension/placement.js +331 -0
  473. package/dist/lean-to-extension/plan-footprint.d.ts +10 -0
  474. package/dist/lean-to-extension/plan-footprint.d.ts.map +1 -0
  475. package/dist/lean-to-extension/plan-footprint.js +27 -0
  476. package/dist/lean-to-extension/preview-geometry.d.ts +7 -0
  477. package/dist/lean-to-extension/preview-geometry.d.ts.map +1 -0
  478. package/dist/lean-to-extension/preview-geometry.js +40 -0
  479. package/dist/lean-to-extension/preview.d.ts +7 -0
  480. package/dist/lean-to-extension/preview.d.ts.map +1 -0
  481. package/dist/lean-to-extension/preview.js +20 -0
  482. package/dist/lean-to-extension/renderer.d.ts +6 -0
  483. package/dist/lean-to-extension/renderer.d.ts.map +1 -0
  484. package/dist/lean-to-extension/renderer.js +34 -0
  485. package/dist/lean-to-extension/roof-attachment.d.ts +28 -0
  486. package/dist/lean-to-extension/roof-attachment.d.ts.map +1 -0
  487. package/dist/lean-to-extension/roof-attachment.js +376 -0
  488. package/dist/lean-to-extension/schema.d.ts +2 -0
  489. package/dist/lean-to-extension/schema.d.ts.map +1 -0
  490. package/dist/lean-to-extension/schema.js +1 -0
  491. package/dist/lean-to-extension/slots.d.ts +5 -0
  492. package/dist/lean-to-extension/slots.d.ts.map +1 -0
  493. package/dist/lean-to-extension/slots.js +15 -0
  494. package/dist/lean-to-extension/system.d.ts +7 -0
  495. package/dist/lean-to-extension/system.d.ts.map +1 -0
  496. package/dist/lean-to-extension/system.js +726 -0
  497. package/dist/lean-to-extension/tool.d.ts +3 -0
  498. package/dist/lean-to-extension/tool.d.ts.map +1 -0
  499. package/dist/lean-to-extension/tool.js +580 -0
  500. package/dist/lean-to-extension/wall-target.d.ts +10 -0
  501. package/dist/lean-to-extension/wall-target.d.ts.map +1 -0
  502. package/dist/lean-to-extension/wall-target.js +29 -0
  503. package/dist/lineset/definition.d.ts.map +1 -1
  504. package/dist/lineset/definition.js +1 -0
  505. package/dist/liquid-line/definition.d.ts.map +1 -1
  506. package/dist/liquid-line/definition.js +1 -0
  507. package/dist/pipe-fitting/definition.d.ts.map +1 -1
  508. package/dist/pipe-fitting/definition.js +16 -2
  509. package/dist/pipe-fitting/floorplan.d.ts.map +1 -1
  510. package/dist/pipe-fitting/floorplan.js +4 -0
  511. package/dist/pipe-fitting/geometry.d.ts +3 -5
  512. package/dist/pipe-fitting/geometry.d.ts.map +1 -1
  513. package/dist/pipe-fitting/geometry.js +154 -21
  514. package/dist/pipe-fitting/inline-insertion.d.ts +13 -0
  515. package/dist/pipe-fitting/inline-insertion.d.ts.map +1 -0
  516. package/dist/pipe-fitting/inline-insertion.js +96 -0
  517. package/dist/pipe-fitting/parametrics.d.ts.map +1 -1
  518. package/dist/pipe-fitting/parametrics.js +17 -3
  519. package/dist/pipe-fitting/ports.d.ts.map +1 -1
  520. package/dist/pipe-fitting/ports.js +8 -1
  521. package/dist/pipe-fitting/tool.d.ts +26 -0
  522. package/dist/pipe-fitting/tool.d.ts.map +1 -1
  523. package/dist/pipe-fitting/tool.js +182 -37
  524. package/dist/pipe-segment/continuation.d.ts +22 -0
  525. package/dist/pipe-segment/continuation.d.ts.map +1 -0
  526. package/dist/pipe-segment/continuation.js +146 -0
  527. package/dist/pipe-segment/definition.d.ts.map +1 -1
  528. package/dist/pipe-segment/definition.js +23 -7
  529. package/dist/pipe-segment/floorplan.d.ts.map +1 -1
  530. package/dist/pipe-segment/floorplan.js +42 -0
  531. package/dist/pipe-segment/geometry.d.ts +2 -1
  532. package/dist/pipe-segment/geometry.d.ts.map +1 -1
  533. package/dist/pipe-segment/geometry.js +4 -1
  534. package/dist/pipe-segment/move-tool.d.ts.map +1 -1
  535. package/dist/pipe-segment/move-tool.js +25 -5
  536. package/dist/pipe-segment/parametrics.d.ts.map +1 -1
  537. package/dist/pipe-segment/parametrics.js +49 -0
  538. package/dist/pipe-segment/selection.d.ts +3 -4
  539. package/dist/pipe-segment/selection.d.ts.map +1 -1
  540. package/dist/pipe-segment/selection.js +169 -105
  541. package/dist/pipe-segment/slope.d.ts +5 -0
  542. package/dist/pipe-segment/slope.d.ts.map +1 -0
  543. package/dist/pipe-segment/slope.js +10 -0
  544. package/dist/pipe-segment/tool.d.ts.map +1 -1
  545. package/dist/pipe-segment/tool.js +325 -545
  546. package/dist/pipe-trap/tool.d.ts.map +1 -1
  547. package/dist/pipe-trap/tool.js +15 -2
  548. package/dist/procedural-item/definition.d.ts +4 -0
  549. package/dist/procedural-item/definition.d.ts.map +1 -0
  550. package/dist/procedural-item/definition.js +254 -0
  551. package/dist/procedural-item/geometry.d.ts +31 -0
  552. package/dist/procedural-item/geometry.d.ts.map +1 -0
  553. package/dist/procedural-item/geometry.js +88 -0
  554. package/dist/procedural-item/index.d.ts +3 -0
  555. package/dist/procedural-item/index.d.ts.map +1 -0
  556. package/dist/procedural-item/index.js +2 -0
  557. package/dist/procedural-item/move-session.d.ts +15 -0
  558. package/dist/procedural-item/move-session.d.ts.map +1 -0
  559. package/dist/procedural-item/move-session.js +197 -0
  560. package/dist/procedural-item/move-tool.d.ts +5 -0
  561. package/dist/procedural-item/move-tool.d.ts.map +1 -0
  562. package/dist/procedural-item/move-tool.js +193 -0
  563. package/dist/procedural-item/renderer.d.ts +5 -0
  564. package/dist/procedural-item/renderer.d.ts.map +1 -0
  565. package/dist/procedural-item/renderer.js +66 -0
  566. package/dist/ridge-vent/definition.js +1 -1
  567. package/dist/ridge-vent/panel.d.ts.map +1 -1
  568. package/dist/ridge-vent/panel.js +4 -4
  569. package/dist/ridge-vent/parametrics.js +1 -1
  570. package/dist/roof/conical-roof-placement.d.ts +36 -0
  571. package/dist/roof/conical-roof-placement.d.ts.map +1 -0
  572. package/dist/roof/conical-roof-placement.js +134 -0
  573. package/dist/roof/conical-roof.d.ts +3 -0
  574. package/dist/roof/conical-roof.d.ts.map +1 -0
  575. package/dist/roof/conical-roof.js +53 -0
  576. package/dist/roof/definition.d.ts +3 -10
  577. package/dist/roof/definition.d.ts.map +1 -1
  578. package/dist/roof/definition.js +83 -13
  579. package/dist/roof/floorplan.d.ts +3 -10
  580. package/dist/roof/floorplan.d.ts.map +1 -1
  581. package/dist/roof/floorplan.js +139 -165
  582. package/dist/roof/index.d.ts +1 -0
  583. package/dist/roof/index.d.ts.map +1 -1
  584. package/dist/roof/index.js +1 -0
  585. package/dist/roof/panel.d.ts.map +1 -1
  586. package/dist/roof/panel.js +26 -25
  587. package/dist/roof/renderer.d.ts.map +1 -1
  588. package/dist/roof/roof-footprint-source.d.ts +16 -0
  589. package/dist/roof/roof-footprint-source.d.ts.map +1 -0
  590. package/dist/roof/roof-footprint-source.js +14 -0
  591. package/dist/roof/roof-footprint.d.ts +25 -0
  592. package/dist/roof/roof-footprint.d.ts.map +1 -0
  593. package/dist/roof/roof-footprint.js +94 -0
  594. package/dist/roof/roof-placement-mode.d.ts +18 -0
  595. package/dist/roof/roof-placement-mode.d.ts.map +1 -0
  596. package/dist/roof/roof-placement-mode.js +24 -0
  597. package/dist/roof/tool.d.ts +10 -0
  598. package/dist/roof/tool.d.ts.map +1 -0
  599. package/dist/roof/tool.js +922 -0
  600. package/dist/roof-segment/definition.d.ts.map +1 -1
  601. package/dist/roof-segment/definition.js +52 -8
  602. package/dist/roof-segment/floorplan-affordances.d.ts +5 -6
  603. package/dist/roof-segment/floorplan-affordances.d.ts.map +1 -1
  604. package/dist/roof-segment/floorplan-affordances.js +64 -18
  605. package/dist/roof-segment/floorplan.d.ts +3 -2
  606. package/dist/roof-segment/floorplan.d.ts.map +1 -1
  607. package/dist/roof-segment/floorplan.js +95 -46
  608. package/dist/roof-segment/panel.d.ts.map +1 -1
  609. package/dist/roof-segment/panel.js +122 -27
  610. package/dist/scan/definition.d.ts +2 -3
  611. package/dist/scan/definition.d.ts.map +1 -1
  612. package/dist/scan/definition.js +9 -7
  613. package/dist/scan/parametrics.d.ts.map +1 -1
  614. package/dist/scan/parametrics.js +13 -1
  615. package/dist/scan/renderer.d.ts.map +1 -1
  616. package/dist/scan/renderer.js +8 -2
  617. package/dist/shared/accessory-cursor.d.ts +7 -0
  618. package/dist/shared/accessory-cursor.d.ts.map +1 -0
  619. package/dist/shared/accessory-cursor.js +83 -0
  620. package/dist/shared/accessory-floorplan.d.ts +4 -0
  621. package/dist/shared/accessory-floorplan.d.ts.map +1 -0
  622. package/dist/shared/accessory-floorplan.js +51 -0
  623. package/dist/shared/accessory-geometry.d.ts +7 -0
  624. package/dist/shared/accessory-geometry.d.ts.map +1 -0
  625. package/dist/shared/accessory-geometry.js +79 -0
  626. package/dist/shared/accessory-placement.d.ts +10 -0
  627. package/dist/shared/accessory-placement.d.ts.map +1 -0
  628. package/dist/shared/accessory-placement.js +126 -0
  629. package/dist/shared/accessory-snapping.d.ts +5 -0
  630. package/dist/shared/accessory-snapping.d.ts.map +1 -0
  631. package/dist/shared/accessory-snapping.js +40 -0
  632. package/dist/shared/automatic-run-end-cap.d.ts +12 -0
  633. package/dist/shared/automatic-run-end-cap.d.ts.map +1 -0
  634. package/dist/shared/automatic-run-end-cap.js +169 -0
  635. package/dist/shared/block-face-host.d.ts +15 -0
  636. package/dist/shared/block-face-host.d.ts.map +1 -0
  637. package/dist/shared/block-face-host.js +36 -0
  638. package/dist/shared/connection-compatibility.d.ts +8 -0
  639. package/dist/shared/connection-compatibility.d.ts.map +1 -0
  640. package/dist/shared/connection-compatibility.js +28 -0
  641. package/dist/shared/connection-feedback.d.ts +10 -0
  642. package/dist/shared/connection-feedback.d.ts.map +1 -0
  643. package/dist/shared/connection-feedback.js +17 -0
  644. package/dist/shared/distribution-run-contract.d.ts +49 -0
  645. package/dist/shared/distribution-run-contract.d.ts.map +1 -0
  646. package/dist/shared/distribution-run-contract.js +30 -0
  647. package/dist/shared/distribution-run-tool.d.ts +117 -0
  648. package/dist/shared/distribution-run-tool.d.ts.map +1 -0
  649. package/dist/shared/distribution-run-tool.js +986 -0
  650. package/dist/shared/dormer-wall-opening-placement.d.ts +21 -0
  651. package/dist/shared/dormer-wall-opening-placement.d.ts.map +1 -0
  652. package/dist/shared/dormer-wall-opening-placement.js +101 -0
  653. package/dist/shared/duct-adapter.d.ts +10 -0
  654. package/dist/shared/duct-adapter.d.ts.map +1 -0
  655. package/dist/shared/duct-adapter.js +43 -0
  656. package/dist/shared/elbow-branch-continuation.d.ts +20 -0
  657. package/dist/shared/elbow-branch-continuation.d.ts.map +1 -0
  658. package/dist/shared/elbow-branch-continuation.js +200 -0
  659. package/dist/shared/fitting-clearance.d.ts +5 -0
  660. package/dist/shared/fitting-clearance.d.ts.map +1 -0
  661. package/dist/shared/fitting-clearance.js +11 -0
  662. package/dist/shared/fitting-deletion-cleanup.d.ts +14 -0
  663. package/dist/shared/fitting-deletion-cleanup.d.ts.map +1 -0
  664. package/dist/shared/fitting-deletion-cleanup.js +297 -0
  665. package/dist/shared/fitting-surface-support.d.ts +5 -0
  666. package/dist/shared/fitting-surface-support.d.ts.map +1 -0
  667. package/dist/shared/fitting-surface-support.js +42 -0
  668. package/dist/shared/fitting-tool-options.d.ts +4 -0
  669. package/dist/shared/fitting-tool-options.d.ts.map +1 -0
  670. package/dist/shared/fitting-tool-options.js +175 -0
  671. package/dist/shared/floor-placement.d.ts +1 -1
  672. package/dist/shared/floor-placement.d.ts.map +1 -1
  673. package/dist/shared/floor-placement.js +13 -33
  674. package/dist/shared/ghost-materials.d.ts.map +1 -1
  675. package/dist/shared/ghost-materials.js +2 -1
  676. package/dist/shared/lean-to-post-omissions.d.ts +8 -0
  677. package/dist/shared/lean-to-post-omissions.d.ts.map +1 -0
  678. package/dist/shared/lean-to-post-omissions.js +51 -0
  679. package/dist/shared/mep-fitting-actions.d.ts +5 -0
  680. package/dist/shared/mep-fitting-actions.d.ts.map +1 -0
  681. package/dist/shared/mep-fitting-actions.js +16 -0
  682. package/dist/shared/mep-presets.d.ts +11 -0
  683. package/dist/shared/mep-presets.d.ts.map +1 -0
  684. package/dist/shared/mep-presets.js +34 -0
  685. package/dist/shared/node-batch/candidates.d.ts +29 -0
  686. package/dist/shared/node-batch/candidates.d.ts.map +1 -0
  687. package/dist/shared/node-batch/candidates.js +260 -0
  688. package/dist/shared/node-batch/store.d.ts +26 -0
  689. package/dist/shared/node-batch/store.d.ts.map +1 -0
  690. package/dist/shared/node-batch/store.js +319 -0
  691. package/dist/shared/node-batch/system.d.ts +8 -0
  692. package/dist/shared/node-batch/system.d.ts.map +1 -0
  693. package/dist/shared/node-batch/system.js +569 -0
  694. package/dist/shared/node-batch/types.d.ts +99 -0
  695. package/dist/shared/node-batch/types.d.ts.map +1 -0
  696. package/dist/shared/node-batch/types.js +4 -0
  697. package/dist/shared/opening-move-history.d.ts +47 -0
  698. package/dist/shared/opening-move-history.d.ts.map +1 -0
  699. package/dist/shared/opening-move-history.js +18 -0
  700. package/dist/shared/opening-move-wall-gate.d.ts +55 -0
  701. package/dist/shared/opening-move-wall-gate.d.ts.map +1 -0
  702. package/dist/shared/opening-move-wall-gate.js +44 -0
  703. package/dist/shared/panel-fields.d.ts +15 -0
  704. package/dist/shared/panel-fields.d.ts.map +1 -0
  705. package/dist/shared/panel-fields.js +31 -0
  706. package/dist/shared/path-point-affordance.d.ts +2 -2
  707. package/dist/shared/path-point-affordance.d.ts.map +1 -1
  708. package/dist/shared/path-point-affordance.js +30 -6
  709. package/dist/shared/placeholder-geometry.d.ts.map +1 -1
  710. package/dist/shared/placeholder-geometry.js +4 -0
  711. package/dist/shared/ports.d.ts +20 -3
  712. package/dist/shared/ports.d.ts.map +1 -1
  713. package/dist/shared/ports.js +148 -7
  714. package/dist/shared/primitive-uv.d.ts +17 -0
  715. package/dist/shared/primitive-uv.d.ts.map +1 -0
  716. package/dist/shared/primitive-uv.js +107 -0
  717. package/dist/shared/reducer-size.d.ts +2 -0
  718. package/dist/shared/reducer-size.d.ts.map +1 -0
  719. package/dist/shared/reducer-size.js +12 -0
  720. package/dist/shared/resting-surface-plan.d.ts +3 -0
  721. package/dist/shared/resting-surface-plan.d.ts.map +1 -0
  722. package/dist/shared/resting-surface-plan.js +28 -0
  723. package/dist/shared/ridge-snap.js +1 -1
  724. package/dist/shared/roof-surface.d.ts.map +1 -1
  725. package/dist/shared/roof-surface.js +17 -2
  726. package/dist/shared/run-cursor.d.ts +22 -0
  727. package/dist/shared/run-cursor.d.ts.map +1 -0
  728. package/dist/shared/run-cursor.js +19 -0
  729. package/dist/shared/run-direction-feedback.d.ts +20 -0
  730. package/dist/shared/run-direction-feedback.d.ts.map +1 -0
  731. package/dist/shared/run-direction-feedback.js +139 -0
  732. package/dist/shared/run-hanger-controls.d.ts +7 -0
  733. package/dist/shared/run-hanger-controls.d.ts.map +1 -0
  734. package/dist/shared/run-hanger-controls.js +44 -0
  735. package/dist/shared/run-hanger-inspector.d.ts +5 -0
  736. package/dist/shared/run-hanger-inspector.d.ts.map +1 -0
  737. package/dist/shared/run-hanger-inspector.js +43 -0
  738. package/dist/shared/run-hanger-mode.d.ts +11 -0
  739. package/dist/shared/run-hanger-mode.d.ts.map +1 -0
  740. package/dist/shared/run-hanger-mode.js +32 -0
  741. package/dist/shared/run-hanger-system.d.ts +3 -0
  742. package/dist/shared/run-hanger-system.d.ts.map +1 -0
  743. package/dist/shared/run-hanger-system.js +39 -0
  744. package/dist/shared/run-hangers.d.ts +25 -0
  745. package/dist/shared/run-hangers.d.ts.map +1 -0
  746. package/dist/shared/run-hangers.js +261 -0
  747. package/dist/shared/run-port-snap.d.ts +11 -0
  748. package/dist/shared/run-port-snap.d.ts.map +1 -0
  749. package/dist/shared/run-port-snap.js +19 -0
  750. package/dist/shared/run-screen-direction.d.ts +5 -0
  751. package/dist/shared/run-screen-direction.d.ts.map +1 -0
  752. package/dist/shared/run-screen-direction.js +24 -0
  753. package/dist/shared/selection-handles.d.ts +4 -0
  754. package/dist/shared/selection-handles.d.ts.map +1 -1
  755. package/dist/shared/selection-handles.js +10 -0
  756. package/dist/shared/slot-paint.d.ts +8 -1
  757. package/dist/shared/slot-paint.d.ts.map +1 -1
  758. package/dist/shared/slot-paint.js +120 -27
  759. package/dist/shared/surface-paint.d.ts +1 -2
  760. package/dist/shared/surface-paint.d.ts.map +1 -1
  761. package/dist/shared/surface-paint.js +9 -20
  762. package/dist/shared/system-check-panel.d.ts +6 -0
  763. package/dist/shared/system-check-panel.d.ts.map +1 -0
  764. package/dist/shared/system-check-panel.js +62 -0
  765. package/dist/shared/system-checks.d.ts +9 -0
  766. package/dist/shared/system-checks.d.ts.map +1 -0
  767. package/dist/shared/system-checks.js +131 -0
  768. package/dist/shared/wall-attach-target.d.ts +14 -2
  769. package/dist/shared/wall-attach-target.d.ts.map +1 -1
  770. package/dist/shared/wall-attach-target.js +108 -4
  771. package/dist/shared/wall-opening-clearance.d.ts +19 -0
  772. package/dist/shared/wall-opening-clearance.d.ts.map +1 -0
  773. package/dist/shared/wall-opening-clearance.js +28 -0
  774. package/dist/shared/wall-run-move.d.ts +17 -0
  775. package/dist/shared/wall-run-move.d.ts.map +1 -0
  776. package/dist/shared/wall-run-move.js +56 -0
  777. package/dist/shelf/definition.d.ts.map +1 -1
  778. package/dist/shelf/definition.js +2 -1
  779. package/dist/shelf/floorplan-affordances.d.ts.map +1 -1
  780. package/dist/shelf/floorplan-affordances.js +5 -3
  781. package/dist/shelf/floorplan-move.d.ts.map +1 -1
  782. package/dist/shelf/floorplan-move.js +3 -2
  783. package/dist/shelf/geometry.d.ts.map +1 -1
  784. package/dist/shelf/geometry.js +2 -10
  785. package/dist/shelf/parametrics.js +4 -4
  786. package/dist/site/renderer.d.ts.map +1 -1
  787. package/dist/site/renderer.js +40 -28
  788. package/dist/site/terrain-geometry.d.ts +1 -1
  789. package/dist/site/terrain-geometry.d.ts.map +1 -1
  790. package/dist/site/terrain-geometry.js +35 -9
  791. package/dist/site/terrain-mesh.d.ts +5 -0
  792. package/dist/site/terrain-mesh.d.ts.map +1 -1
  793. package/dist/site/terrain-mesh.js +63 -34
  794. package/dist/skylight/definition.js +1 -1
  795. package/dist/skylight/panel.js +6 -6
  796. package/dist/skylight/parametrics.js +2 -2
  797. package/dist/slab/definition.d.ts.map +1 -1
  798. package/dist/slab/definition.js +6 -1
  799. package/dist/slab/dependency-tracker.d.ts +3 -0
  800. package/dist/slab/dependency-tracker.d.ts.map +1 -0
  801. package/dist/slab/dependency-tracker.js +104 -0
  802. package/dist/slab/floorplan-affordances.d.ts +8 -8
  803. package/dist/slab/geometry.d.ts.map +1 -1
  804. package/dist/slab/geometry.js +16 -2
  805. package/dist/slab/panel.js +1 -1
  806. package/dist/slab/parametrics.js +1 -1
  807. package/dist/slab/system.d.ts.map +1 -1
  808. package/dist/slab/system.js +8 -64
  809. package/dist/solar-panel/definition.js +1 -1
  810. package/dist/solar-panel/panel.js +2 -2
  811. package/dist/solar-panel/parametrics.js +2 -2
  812. package/dist/spawn/panel.js +1 -1
  813. package/dist/stair/panel.d.ts.map +1 -1
  814. package/dist/stair/panel.js +23 -16
  815. package/dist/stair/stair-type.d.ts +18 -0
  816. package/dist/stair/stair-type.d.ts.map +1 -0
  817. package/dist/stair/stair-type.js +25 -0
  818. package/dist/stair-segment/panel.d.ts.map +1 -1
  819. package/dist/stair-segment/panel.js +45 -6
  820. package/dist/turbine-vent/definition.d.ts.map +1 -1
  821. package/dist/turbine-vent/definition.js +8 -5
  822. package/dist/turbine-vent/geometry.d.ts.map +1 -1
  823. package/dist/turbine-vent/geometry.js +48 -18
  824. package/dist/turbine-vent/paint.d.ts +4 -0
  825. package/dist/turbine-vent/paint.d.ts.map +1 -0
  826. package/dist/turbine-vent/paint.js +35 -0
  827. package/dist/turbine-vent/panel.js +1 -1
  828. package/dist/turbine-vent/parametrics.js +2 -2
  829. package/dist/turbine-vent/renderer.d.ts.map +1 -1
  830. package/dist/turbine-vent/renderer.js +31 -9
  831. package/dist/unit/definition.d.ts +10 -0
  832. package/dist/unit/definition.d.ts.map +1 -0
  833. package/dist/unit/definition.js +38 -0
  834. package/dist/unit/index.d.ts +2 -0
  835. package/dist/unit/index.d.ts.map +1 -0
  836. package/dist/unit/index.js +1 -0
  837. package/dist/unit/parametrics.d.ts +3 -0
  838. package/dist/unit/parametrics.d.ts.map +1 -0
  839. package/dist/unit/parametrics.js +4 -0
  840. package/dist/unit/schema.d.ts +2 -0
  841. package/dist/unit/schema.d.ts.map +1 -0
  842. package/dist/unit/schema.js +1 -0
  843. package/dist/unit/unit-panel.d.ts +5 -0
  844. package/dist/unit/unit-panel.d.ts.map +1 -0
  845. package/dist/unit/unit-panel.js +61 -0
  846. package/dist/wall/curve-eligibility.d.ts +3 -0
  847. package/dist/wall/curve-eligibility.d.ts.map +1 -0
  848. package/dist/wall/curve-eligibility.js +10 -0
  849. package/dist/wall/curve-tool.d.ts.map +1 -1
  850. package/dist/wall/curve-tool.js +8 -11
  851. package/dist/wall/definition.d.ts +1 -1
  852. package/dist/wall/definition.d.ts.map +1 -1
  853. package/dist/wall/definition.js +36 -13
  854. package/dist/wall/floorplan-affordances.d.ts +1 -0
  855. package/dist/wall/floorplan-affordances.d.ts.map +1 -1
  856. package/dist/wall/floorplan-affordances.js +46 -7
  857. package/dist/wall/floorplan-move.d.ts +3 -4
  858. package/dist/wall/floorplan-move.d.ts.map +1 -1
  859. package/dist/wall/floorplan-move.js +122 -9
  860. package/dist/wall/floorplan.d.ts.map +1 -1
  861. package/dist/wall/floorplan.js +19 -23
  862. package/dist/wall/move-shared.d.ts.map +1 -1
  863. package/dist/wall/move-shared.js +10 -7
  864. package/dist/wall/paint.d.ts.map +1 -1
  865. package/dist/wall/paint.js +2 -0
  866. package/dist/wall/panel.d.ts.map +1 -1
  867. package/dist/wall/panel.js +10 -17
  868. package/dist/wall/parametrics.js +2 -2
  869. package/dist/wall/pointer-transparency.d.ts +128 -0
  870. package/dist/wall/pointer-transparency.d.ts.map +1 -0
  871. package/dist/wall/pointer-transparency.js +147 -0
  872. package/dist/wall/renderer.d.ts.map +1 -1
  873. package/dist/wall/renderer.js +65 -8
  874. package/dist/wall/selection-hit-owner.d.ts +53 -0
  875. package/dist/wall/selection-hit-owner.d.ts.map +1 -0
  876. package/dist/wall/selection-hit-owner.js +79 -0
  877. package/dist/wall/system.d.ts +2 -0
  878. package/dist/wall/system.d.ts.map +1 -1
  879. package/dist/wall/system.js +66 -25
  880. package/dist/wall/tool.d.ts.map +1 -1
  881. package/dist/wall/tool.js +5 -0
  882. package/dist/wall/treatment-level-data.d.ts +6 -1
  883. package/dist/wall/treatment-level-data.d.ts.map +1 -1
  884. package/dist/wall/treatment-level-data.js +82 -5
  885. package/dist/wall/treatments.d.ts +1 -0
  886. package/dist/wall/treatments.d.ts.map +1 -1
  887. package/dist/wall/treatments.js +5 -0
  888. package/dist/wall/wall-batch-system.d.ts +38 -6
  889. package/dist/wall/wall-batch-system.d.ts.map +1 -1
  890. package/dist/wall/wall-batch-system.js +122 -25
  891. package/dist/wall/wall-batch.js +2 -2
  892. package/dist/window/definition.d.ts +2 -1
  893. package/dist/window/definition.d.ts.map +1 -1
  894. package/dist/window/definition.js +51 -24
  895. package/dist/window/floorplan-move.d.ts.map +1 -1
  896. package/dist/window/floorplan-move.js +1 -1
  897. package/dist/window/move-tool.d.ts +7 -5
  898. package/dist/window/move-tool.d.ts.map +1 -1
  899. package/dist/window/move-tool.js +344 -58
  900. package/dist/window/panel.d.ts.map +1 -1
  901. package/dist/window/panel.js +8 -6
  902. package/dist/window/parametrics.js +2 -2
  903. package/dist/window/renderer.d.ts.map +1 -1
  904. package/dist/window/renderer.js +5 -7
  905. package/dist/window/tool.d.ts.map +1 -1
  906. package/dist/window/tool.js +227 -13
  907. package/dist/zone/definition.d.ts.map +1 -1
  908. package/dist/zone/definition.js +7 -1
  909. package/dist/zone/floorplan-affordances.d.ts +12 -12
  910. package/dist/zone/floorplan.d.ts +1 -3
  911. package/dist/zone/floorplan.d.ts.map +1 -1
  912. package/dist/zone/floorplan.js +43 -28
  913. package/dist/zone/quantities-panel.d.ts.map +1 -1
  914. package/dist/zone/quantities-panel.js +33 -29
  915. package/dist/zone/renderer.d.ts.map +1 -1
  916. package/dist/zone/renderer.js +20 -6
  917. package/dist/zone/unit-membership.d.ts +12 -0
  918. package/dist/zone/unit-membership.d.ts.map +1 -0
  919. package/dist/zone/unit-membership.js +22 -0
  920. package/package.json +16 -8
  921. package/dist/dormer/panel-window-section.d.ts +0 -14
  922. package/dist/dormer/panel-window-section.d.ts.map +0 -1
  923. package/dist/dormer/panel-window-section.js +0 -59
  924. package/dist/dormer/window-assembly.d.ts +0 -22
  925. package/dist/dormer/window-assembly.d.ts.map +0 -1
  926. package/dist/dormer/window-assembly.js +0 -111
  927. package/dist/dormer/window-frame.d.ts +0 -24
  928. package/dist/dormer/window-frame.d.ts.map +0 -1
  929. package/dist/dormer/window-frame.js +0 -100
@@ -0,0 +1,2570 @@
1
+ 'use client';
2
+ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
3
+ import { emitter, sceneRegistry, useLiveNodeOverrides, } from '@pascal-app/core';
4
+ import { cn, EDITOR_LAYER, getFloatingMenuScale, isAngleSnapActive, isGridSnapActive, isMagneticSnapActive, markToolCancelConsumed, meshEditScope, NodeActionMenu, swallowNextClick, triggerSFX, useEditor, useInteractionScope, } from '@pascal-app/editor';
5
+ import { Html } from '@react-three/drei';
6
+ import { createPortal, useFrame, useThree } from '@react-three/fiber';
7
+ import { ArrowUpFromLine, Check, ChevronDown, CircleDot, Ellipsis, Eye, EyeOff, Move3D, Rotate3D, Rows3, Scaling, ScanLine, Square, Trash2, X as XIcon, } from 'lucide-react';
8
+ import { useCallback, useEffect, useMemo, useRef, useState, } from 'react';
9
+ import { BufferGeometry, Color, ConeGeometry, CylinderGeometry, DoubleSide, Float32BufferAttribute, LineSegments, Mesh, Plane, PlaneGeometry, Quaternion, Raycaster, SphereGeometry, TorusGeometry, Vector2, Vector3, } from 'three';
10
+ import { LineBasicNodeMaterial, MeshBasicNodeMaterial } from 'three/webgpu';
11
+ import { applyBlockCommand, blockFaceCentroid, blockFaceNormal, blockLoopCutSegments, } from './commands';
12
+ import useBlockEditSession from './edit-session';
13
+ import { triangulateBlockFace } from './geometry';
14
+ import { blockGeometrySnapThreshold, resolveBlockGeometrySnap } from './geometry-snap';
15
+ import { BLOCK_WHEEL_OPTIONS, consumeBlockGestureWheel } from './gesture-wheel';
16
+ import { blockSfx } from './interaction-sfx';
17
+ import { commitBlockOperation, repeatCommittedBlockOperation, replaceCommittedBlockOperation, } from './last-operation';
18
+ import { resolveLoopCutPointerAction, resolveLoopCutSlideFactor } from './loop-cut-interaction';
19
+ import { BLOCK_BODY_SLOT_ID, unpaintedBlockMaterialSlotIds } from './material-slots';
20
+ import { blockModalFaceOperationStatus, } from './modal-face-operation';
21
+ import { beginBlockModalSession } from './modal-session';
22
+ import { blockAxisDelta, blockAxisVisualState, blockConstrainTranslationDelta, blockModalTransformStatus, blockNumericDeltaForConstraint, blockPlaneVisualState, blockPointerDistanceForAxis, blockRotationPointerAngle, blockScaleFactorsForConstraint, blockTransformConstraintFromKey, blockTransformDisplayValue, blockTransformNumericInputFromKey, blockTransformNumericValue, } from './modal-transform';
23
+ import { lockedRotationAngleFromHits, signedAngleAroundAxis, unwrapRotationDelta, } from './rotation-drag';
24
+ import { blockTopologyClientExtent, blockLocalPointToClient as localPointToClient, blockSelectionCentroid as selectionCentroid, } from './selection-geometry';
25
+ import { blockSelectionChanged, clearBlockSelection, convertBlockSelection, invertBlockSelection, selectAllBlockComponents, selectBlockComponent, } from './selection-model';
26
+ import { blockBevelWidthFromDrag, blockComponentStatus, blockGizmoDimensions, blockGizmoHitDimensions, blockOperationAvailability, blockScaleFactorFromDrag, blockScaleFactors, blockToolbarOffset, formatBlockSelectionStatus, } from './toolbar-state';
27
+ import { useBlockFaceOperation } from './use-block-face-operation';
28
+ const AXIS_VECTORS = {
29
+ x: [1, 0, 0],
30
+ y: [0, 1, 0],
31
+ z: [0, 0, 1],
32
+ };
33
+ const AXIS_COLORS = {
34
+ x: '#ff2060',
35
+ y: '#20df80',
36
+ z: '#2080ff',
37
+ };
38
+ const PIVOT_HOVERED_COLOR = '#ffff40';
39
+ const GIZMO_RENDER_ORDER = 1300;
40
+ const GIZMO_HIT_RENDER_ORDER = GIZMO_RENDER_ORDER + 1;
41
+ const PLANE_NORMAL = {
42
+ xy: 'z',
43
+ xz: 'y',
44
+ yz: 'x',
45
+ };
46
+ const COMPONENT_ACTIVE_COLOR = '#ff9a24';
47
+ const COMPONENT_SELECTED_COLOR = '#ff6d00';
48
+ const COMPONENT_HOVER_COLOR = '#ffb020';
49
+ const COMPONENT_IDLE_COLOR = '#737982';
50
+ const DEFAULT_BEVEL_SEGMENTS = 6;
51
+ const ROTATION_SNAP_ANGLE_DEGREES = 15;
52
+ const EMPTY_COMPONENT_IDS = [];
53
+ const FLOATING_PANEL_CLASS = 'pointer-events-auto flex items-center gap-1 rounded-lg border border-border bg-background/95 p-1 shadow-xl backdrop-blur-md';
54
+ const TOOLBAR_POPOVER_CLASS = 'absolute top-[calc(100%+10px)] left-1/2 z-50 w-72 -translate-x-1/2 rounded-xl border border-border/50 bg-background/98 p-2 shadow-elevation-4 backdrop-blur-xl';
55
+ const OPERATION_INPUT_CLASS = 'h-6 w-12 rounded-md border border-border/50 bg-accent/25 px-1 text-right font-mono text-[10px] text-foreground tabular-nums outline-none hover:border-border/80 focus:border-ring disabled:opacity-35';
56
+ const playBlockSfx = (action) => triggerSFX(blockSfx(action));
57
+ function isAxisConstraint(constraint) {
58
+ return constraint === 'x' || constraint === 'y' || constraint === 'z';
59
+ }
60
+ function isPlaneConstraint(constraint) {
61
+ return constraint === 'xy' || constraint === 'xz' || constraint === 'yz';
62
+ }
63
+ function preferredFace(topology) {
64
+ return (topology.faces
65
+ .map((face) => ({
66
+ face,
67
+ normal: blockFaceNormal(topology, face),
68
+ centroid: blockFaceCentroid(topology, face),
69
+ }))
70
+ .filter((entry) => entry.normal && entry.centroid)
71
+ .sort((a, b) => b.normal[1] - a.normal[1] || b.centroid[1] - a.centroid[1])[0]?.face ??
72
+ null);
73
+ }
74
+ function topologyVertexMap(topology) {
75
+ return new Map(topology.vertices.map((vertex) => [vertex.id, vertex.position]));
76
+ }
77
+ function topologyExtent(topology) {
78
+ const axes = [0, 1, 2];
79
+ return Math.max(0.5, ...axes.map((axis) => {
80
+ const values = topology.vertices.map((vertex) => vertex.position[axis]);
81
+ return Math.max(...values) - Math.min(...values);
82
+ }));
83
+ }
84
+ function closestAxisParameterToRay(axisOrigin, axisDirection, ray) {
85
+ const originToRay = axisOrigin.clone().sub(ray.origin);
86
+ const b = axisDirection.dot(ray.direction);
87
+ const d = axisDirection.dot(originToRay);
88
+ const e = ray.direction.dot(originToRay);
89
+ const denominator = 1 - b * b;
90
+ if (Math.abs(denominator) < 1e-6)
91
+ return -d;
92
+ const axisParameter = (b * e - d) / denominator;
93
+ return e + b * axisParameter < 0 ? -d : axisParameter;
94
+ }
95
+ function geometrySnapThreshold(camera, worldPoint, target, canvas, extent) {
96
+ target.updateWorldMatrix(true, false);
97
+ const screenThreshold = blockGeometrySnapThreshold(camera, worldPoint, canvas.getBoundingClientRect().height, target.getWorldScale(new Vector3()));
98
+ return Math.min(extent * 0.15, Math.max(0.02, screenThreshold));
99
+ }
100
+ function VertexHandle({ id, position, radius, selected, active, xray, onSelect, }) {
101
+ const [hovered, setHovered] = useState(false);
102
+ const visibleGeometry = useMemo(() => new SphereGeometry(radius, 16, 12), [radius]);
103
+ const hitGeometry = useMemo(() => new SphereGeometry(radius * 4.2, 12, 8), [radius]);
104
+ const visibleMaterial = useMemo(() => new MeshBasicNodeMaterial({ depthTest: !xray, depthWrite: false }), [xray]);
105
+ const hitMaterial = useMemo(() => new MeshBasicNodeMaterial({
106
+ color: '#ffffff',
107
+ transparent: true,
108
+ opacity: 0,
109
+ depthTest: false,
110
+ depthWrite: false,
111
+ }), []);
112
+ useEffect(() => {
113
+ visibleMaterial.color.set(active
114
+ ? COMPONENT_ACTIVE_COLOR
115
+ : selected
116
+ ? COMPONENT_SELECTED_COLOR
117
+ : hovered
118
+ ? COMPONENT_HOVER_COLOR
119
+ : COMPONENT_IDLE_COLOR);
120
+ }, [active, hovered, selected, visibleMaterial]);
121
+ useEffect(() => () => {
122
+ visibleGeometry.dispose();
123
+ hitGeometry.dispose();
124
+ visibleMaterial.dispose();
125
+ hitMaterial.dispose();
126
+ }, [hitGeometry, hitMaterial, visibleGeometry, visibleMaterial]);
127
+ return (_jsxs("group", { position: position, children: [_jsx("mesh", { frustumCulled: false, geometry: visibleGeometry, layers: EDITOR_LAYER, material: visibleMaterial, raycast: () => { }, renderOrder: 1200 }), _jsx("mesh", { frustumCulled: false, geometry: hitGeometry, layers: EDITOR_LAYER, material: hitMaterial, onClick: (event) => {
128
+ event.stopPropagation();
129
+ onSelect(id, event.nativeEvent.shiftKey, event);
130
+ }, onPointerEnter: (event) => {
131
+ event.stopPropagation();
132
+ setHovered(true);
133
+ document.body.style.cursor = 'pointer';
134
+ }, onPointerLeave: () => {
135
+ setHovered(false);
136
+ if (document.body.style.cursor === 'pointer')
137
+ document.body.style.cursor = '';
138
+ }, renderOrder: 1201 })] }));
139
+ }
140
+ function EdgeHandle({ id, start, end, radius, selected, active, xray, onSelect, onPointerDown, }) {
141
+ const [hovered, setHovered] = useState(false);
142
+ const hoverCursor = onPointerDown ? 'ew-resize' : 'pointer';
143
+ const placement = useMemo(() => {
144
+ const a = new Vector3(...start);
145
+ const b = new Vector3(...end);
146
+ const direction = b.clone().sub(a);
147
+ const length = direction.length();
148
+ return {
149
+ length,
150
+ position: a.add(b).multiplyScalar(0.5),
151
+ quaternion: new Quaternion().setFromUnitVectors(new Vector3(0, 1, 0), direction.normalize()),
152
+ };
153
+ }, [end, start]);
154
+ const visibleGeometry = useMemo(() => {
155
+ const geometry = new BufferGeometry();
156
+ geometry.setAttribute('position', new Float32BufferAttribute([...start, ...end], 3));
157
+ return geometry;
158
+ }, [end, start]);
159
+ const hitGeometry = useMemo(() => new CylinderGeometry(radius * 3.2, radius * 3.2, placement.length, 8), [placement.length, radius]);
160
+ const emphasisGeometry = useMemo(() => new CylinderGeometry(radius * 1.35, radius * 1.35, placement.length, 12), [placement.length, radius]);
161
+ const visibleMaterial = useMemo(() => new LineBasicNodeMaterial({
162
+ transparent: true,
163
+ depthTest: !xray,
164
+ depthWrite: false,
165
+ }), [xray]);
166
+ const hitMaterial = useMemo(() => new MeshBasicNodeMaterial({
167
+ color: '#ffffff',
168
+ transparent: true,
169
+ opacity: 0,
170
+ depthTest: false,
171
+ depthWrite: false,
172
+ }), []);
173
+ const emphasisMaterial = useMemo(() => new MeshBasicNodeMaterial({
174
+ transparent: true,
175
+ depthTest: !xray,
176
+ depthWrite: false,
177
+ }), [xray]);
178
+ useEffect(() => {
179
+ const color = active
180
+ ? COMPONENT_ACTIVE_COLOR
181
+ : selected
182
+ ? COMPONENT_SELECTED_COLOR
183
+ : hovered
184
+ ? COMPONENT_HOVER_COLOR
185
+ : COMPONENT_IDLE_COLOR;
186
+ visibleMaterial.color.set(color);
187
+ visibleMaterial.opacity = active || selected || hovered ? 1 : 0.65;
188
+ emphasisMaterial.color.set(color);
189
+ emphasisMaterial.opacity = active ? 1 : selected ? 0.96 : hovered ? 0.82 : 0;
190
+ }, [active, emphasisMaterial, hovered, selected, visibleMaterial]);
191
+ const visibleLine = useMemo(() => {
192
+ const line = new LineSegments(visibleGeometry, visibleMaterial);
193
+ line.frustumCulled = false;
194
+ line.layers.set(EDITOR_LAYER);
195
+ line.raycast = () => { };
196
+ line.renderOrder = 1200;
197
+ return line;
198
+ }, [visibleGeometry, visibleMaterial]);
199
+ useEffect(() => () => {
200
+ visibleGeometry.dispose();
201
+ hitGeometry.dispose();
202
+ emphasisGeometry.dispose();
203
+ visibleMaterial.dispose();
204
+ hitMaterial.dispose();
205
+ emphasisMaterial.dispose();
206
+ }, [
207
+ emphasisGeometry,
208
+ emphasisMaterial,
209
+ hitGeometry,
210
+ hitMaterial,
211
+ visibleGeometry,
212
+ visibleMaterial,
213
+ ]);
214
+ return (_jsxs(_Fragment, { children: [_jsx("primitive", { object: visibleLine }), _jsxs("group", { position: placement.position, quaternion: placement.quaternion, children: [_jsx("mesh", { frustumCulled: false, geometry: emphasisGeometry, layers: EDITOR_LAYER, material: emphasisMaterial, raycast: () => { }, renderOrder: 1202, visible: active || selected || hovered }), _jsx("mesh", { frustumCulled: false, geometry: hitGeometry, layers: EDITOR_LAYER, material: hitMaterial, onClick: (event) => {
215
+ event.stopPropagation();
216
+ if (onPointerDown)
217
+ return;
218
+ onSelect(id, event.nativeEvent.shiftKey, event);
219
+ }, onPointerDown: onPointerDown
220
+ ? (event) => {
221
+ event.stopPropagation();
222
+ event.nativeEvent.stopImmediatePropagation();
223
+ swallowNextClick();
224
+ onPointerDown(id, event);
225
+ }
226
+ : undefined, onPointerEnter: (event) => {
227
+ event.stopPropagation();
228
+ setHovered(true);
229
+ document.body.style.cursor = hoverCursor;
230
+ }, onPointerLeave: () => {
231
+ setHovered(false);
232
+ if (document.body.style.cursor === hoverCursor)
233
+ document.body.style.cursor = '';
234
+ }, renderOrder: 1201 })] })] }));
235
+ }
236
+ function FaceHandle({ face, topology, selected, active, xray, interactive = true, onSelect, }) {
237
+ const [hovered, setHovered] = useState(false);
238
+ const geometries = useMemo(() => {
239
+ const triangulated = triangulateBlockFace(topology, face);
240
+ if (!triangulated)
241
+ return null;
242
+ const fill = new BufferGeometry();
243
+ fill.setAttribute('position', new Float32BufferAttribute(triangulated.triangles.flatMap((triangle) => triangle.flat()), 3));
244
+ const vertexById = topologyVertexMap(topology);
245
+ const outlinePositions = [];
246
+ for (let index = 0; index < face.vertexIds.length; index += 1) {
247
+ const start = vertexById.get(face.vertexIds[index]);
248
+ const end = vertexById.get(face.vertexIds[(index + 1) % face.vertexIds.length]);
249
+ if (start && end)
250
+ outlinePositions.push(...start, ...end);
251
+ }
252
+ const outline = new BufferGeometry();
253
+ outline.setAttribute('position', new Float32BufferAttribute(outlinePositions, 3));
254
+ return { fill, outline };
255
+ }, [face, topology]);
256
+ const fillMaterial = useMemo(() => new MeshBasicNodeMaterial({
257
+ transparent: true,
258
+ depthTest: !xray,
259
+ depthWrite: false,
260
+ polygonOffset: true,
261
+ polygonOffsetFactor: -4,
262
+ side: DoubleSide,
263
+ }), [xray]);
264
+ const outlineMaterial = useMemo(() => new LineBasicNodeMaterial({
265
+ transparent: true,
266
+ depthTest: !xray,
267
+ depthWrite: false,
268
+ }), [xray]);
269
+ useEffect(() => {
270
+ fillMaterial.color.set(active
271
+ ? COMPONENT_ACTIVE_COLOR
272
+ : selected
273
+ ? COMPONENT_SELECTED_COLOR
274
+ : hovered
275
+ ? COMPONENT_HOVER_COLOR
276
+ : COMPONENT_IDLE_COLOR);
277
+ fillMaterial.opacity = active ? 0.46 : selected ? 0.38 : hovered ? 0.18 : 0.001;
278
+ outlineMaterial.color.set(active
279
+ ? COMPONENT_ACTIVE_COLOR
280
+ : selected
281
+ ? COMPONENT_SELECTED_COLOR
282
+ : hovered
283
+ ? COMPONENT_HOVER_COLOR
284
+ : COMPONENT_IDLE_COLOR);
285
+ outlineMaterial.opacity = active || selected ? 1 : hovered ? 0.9 : 0.28;
286
+ }, [active, fillMaterial, hovered, outlineMaterial, selected]);
287
+ const outline = useMemo(() => {
288
+ if (!geometries)
289
+ return null;
290
+ const line = new LineSegments(geometries.outline, outlineMaterial);
291
+ line.layers.set(EDITOR_LAYER);
292
+ line.raycast = () => { };
293
+ line.renderOrder = 1201;
294
+ return line;
295
+ }, [geometries, outlineMaterial]);
296
+ useEffect(() => () => {
297
+ geometries?.fill.dispose();
298
+ geometries?.outline.dispose();
299
+ fillMaterial.dispose();
300
+ outlineMaterial.dispose();
301
+ }, [fillMaterial, geometries, outlineMaterial]);
302
+ if (!geometries)
303
+ return null;
304
+ return (_jsxs("group", { children: [_jsx("mesh", { frustumCulled: false, geometry: geometries.fill, layers: EDITOR_LAYER, material: fillMaterial, onClick: interactive
305
+ ? (event) => {
306
+ event.stopPropagation();
307
+ onSelect(face.id, event.nativeEvent.shiftKey, event);
308
+ }
309
+ : undefined, onPointerEnter: interactive
310
+ ? (event) => {
311
+ event.stopPropagation();
312
+ setHovered(true);
313
+ document.body.style.cursor = 'pointer';
314
+ }
315
+ : undefined, onPointerLeave: interactive
316
+ ? () => {
317
+ setHovered(false);
318
+ if (document.body.style.cursor === 'pointer')
319
+ document.body.style.cursor = '';
320
+ }
321
+ : undefined, raycast: interactive ? undefined : () => { }, renderOrder: 1200 }), outline ? _jsx("primitive", { object: outline }) : null] }));
322
+ }
323
+ function AxisTransformHandle({ axis, length, radius, moveHitRadius, scaleHitRadius, moveState, scaleState, disabled, onMovePointerDown, onScalePointerDown, }) {
324
+ const [hovered, setHovered] = useState(null);
325
+ const shaftGeometry = useMemo(() => new CylinderGeometry(radius * 0.35, radius * 0.35, length * 0.8, 10), [length, radius]);
326
+ const arrowGeometry = useMemo(() => new ConeGeometry(radius * 1.6, length * 0.2, 24), [length, radius]);
327
+ const moveHitGeometry = useMemo(() => new CylinderGeometry(moveHitRadius, moveHitRadius, length, 8), [length, moveHitRadius]);
328
+ const scaleGeometry = useMemo(() => new SphereGeometry(radius * 1.3, 12, 12), [radius]);
329
+ const scaleHitGeometry = useMemo(() => new SphereGeometry(scaleHitRadius, 12, 8), [scaleHitRadius]);
330
+ const moveMaterial = useMemo(() => new MeshBasicNodeMaterial({
331
+ transparent: true,
332
+ opacity: 1,
333
+ depthTest: false,
334
+ depthWrite: false,
335
+ }), []);
336
+ const scaleMaterial = useMemo(() => new MeshBasicNodeMaterial({
337
+ transparent: true,
338
+ opacity: 1,
339
+ depthTest: false,
340
+ depthWrite: false,
341
+ }), []);
342
+ const hitMaterial = useMemo(() => new MeshBasicNodeMaterial({
343
+ color: AXIS_COLORS[axis],
344
+ transparent: true,
345
+ opacity: 0,
346
+ depthTest: false,
347
+ depthWrite: false,
348
+ }), [axis]);
349
+ useEffect(() => {
350
+ moveMaterial.color.set(hovered === 'translate' && moveState !== 'faded' ? PIVOT_HOVERED_COLOR : AXIS_COLORS[axis]);
351
+ moveMaterial.opacity = moveState === 'faded' ? 0.14 : 1;
352
+ scaleMaterial.color.set(hovered === 'scale' && scaleState !== 'faded' ? PIVOT_HOVERED_COLOR : AXIS_COLORS[axis]);
353
+ scaleMaterial.opacity = scaleState === 'faded' ? 0.14 : 1;
354
+ }, [axis, hovered, moveMaterial, moveState, scaleMaterial, scaleState]);
355
+ useEffect(() => () => {
356
+ shaftGeometry.dispose();
357
+ arrowGeometry.dispose();
358
+ moveHitGeometry.dispose();
359
+ scaleGeometry.dispose();
360
+ scaleHitGeometry.dispose();
361
+ moveMaterial.dispose();
362
+ scaleMaterial.dispose();
363
+ hitMaterial.dispose();
364
+ }, [
365
+ arrowGeometry,
366
+ hitMaterial,
367
+ moveHitGeometry,
368
+ moveMaterial,
369
+ scaleGeometry,
370
+ scaleHitGeometry,
371
+ scaleMaterial,
372
+ shaftGeometry,
373
+ ]);
374
+ const rotation = axis === 'x' ? [0, 0, -Math.PI / 2] : axis === 'z' ? [Math.PI / 2, 0, 0] : [0, 0, 0];
375
+ const scalePosition = length * 1.2;
376
+ return (_jsxs("group", { rotation: rotation, children: [_jsx("mesh", { geometry: shaftGeometry, layers: EDITOR_LAYER, material: moveMaterial, position: [0, length * 0.4, 0], raycast: () => { }, renderOrder: GIZMO_RENDER_ORDER }), _jsx("mesh", { geometry: arrowGeometry, layers: EDITOR_LAYER, material: moveMaterial, position: [0, length * 0.9, 0], raycast: () => { }, renderOrder: GIZMO_RENDER_ORDER }), _jsx("mesh", { geometry: moveHitGeometry, layers: EDITOR_LAYER, material: hitMaterial, onPointerDown: (event) => {
377
+ if (disabled)
378
+ return;
379
+ event.stopPropagation();
380
+ event.nativeEvent.stopImmediatePropagation();
381
+ swallowNextClick();
382
+ onMovePointerDown(axis, event);
383
+ }, onPointerEnter: (event) => {
384
+ if (disabled)
385
+ return;
386
+ event.stopPropagation();
387
+ setHovered('translate');
388
+ document.body.style.cursor = 'grab';
389
+ }, onPointerLeave: () => {
390
+ setHovered(null);
391
+ if (document.body.style.cursor === 'grab')
392
+ document.body.style.cursor = '';
393
+ }, position: [0, length * 0.5, 0], renderOrder: GIZMO_HIT_RENDER_ORDER }), _jsx("mesh", { geometry: scaleGeometry, layers: EDITOR_LAYER, material: scaleMaterial, position: [0, scalePosition, 0], raycast: () => { }, renderOrder: GIZMO_RENDER_ORDER }), _jsx("mesh", { geometry: scaleHitGeometry, layers: EDITOR_LAYER, material: hitMaterial, onPointerDown: (event) => {
394
+ if (disabled)
395
+ return;
396
+ event.stopPropagation();
397
+ event.nativeEvent.stopImmediatePropagation();
398
+ swallowNextClick();
399
+ onScalePointerDown(axis, event);
400
+ }, onPointerEnter: (event) => {
401
+ if (disabled)
402
+ return;
403
+ event.stopPropagation();
404
+ setHovered('scale');
405
+ document.body.style.cursor = 'grab';
406
+ }, onPointerLeave: () => {
407
+ setHovered(null);
408
+ if (document.body.style.cursor === 'grab')
409
+ document.body.style.cursor = '';
410
+ }, position: [0, scalePosition, 0], renderOrder: GIZMO_HIT_RENDER_ORDER })] }));
411
+ }
412
+ function PlaneMoveHandle({ plane, offset, size, hitSize, state, disabled, onPointerDown, }) {
413
+ const [hovered, setHovered] = useState(false);
414
+ const geometry = useMemo(() => new PlaneGeometry(size, size), [size]);
415
+ const hitGeometry = useMemo(() => new PlaneGeometry(hitSize, hitSize), [hitSize]);
416
+ const normalAxis = PLANE_NORMAL[plane];
417
+ const material = useMemo(() => new MeshBasicNodeMaterial({
418
+ color: AXIS_COLORS[normalAxis],
419
+ depthTest: false,
420
+ depthWrite: false,
421
+ side: DoubleSide,
422
+ transparent: true,
423
+ opacity: 1,
424
+ }), [normalAxis]);
425
+ const hitMaterial = useMemo(() => new MeshBasicNodeMaterial({
426
+ color: '#ffffff',
427
+ depthTest: false,
428
+ depthWrite: false,
429
+ side: DoubleSide,
430
+ transparent: true,
431
+ opacity: 0,
432
+ }), []);
433
+ useEffect(() => {
434
+ material.color.set(hovered && state !== 'faded' ? PIVOT_HOVERED_COLOR : AXIS_COLORS[normalAxis]);
435
+ material.opacity = state === 'faded' ? 0.1 : 1;
436
+ }, [hovered, material, normalAxis, state]);
437
+ useEffect(() => () => {
438
+ geometry.dispose();
439
+ hitGeometry.dispose();
440
+ material.dispose();
441
+ hitMaterial.dispose();
442
+ }, [geometry, hitGeometry, hitMaterial, material]);
443
+ const position = plane === 'xy'
444
+ ? [offset, offset, 0]
445
+ : plane === 'xz'
446
+ ? [offset, 0, offset]
447
+ : [0, offset, offset];
448
+ const rotation = plane === 'xz' ? [-Math.PI / 2, 0, 0] : plane === 'yz' ? [0, Math.PI / 2, 0] : [0, 0, 0];
449
+ return (_jsxs("group", { position: position, rotation: rotation, children: [_jsx("mesh", { geometry: geometry, layers: EDITOR_LAYER, material: material, raycast: () => { }, renderOrder: GIZMO_RENDER_ORDER }), _jsx("mesh", { geometry: hitGeometry, layers: EDITOR_LAYER, material: hitMaterial, onPointerDown: (event) => {
450
+ if (disabled)
451
+ return;
452
+ event.stopPropagation();
453
+ event.nativeEvent.stopImmediatePropagation();
454
+ swallowNextClick();
455
+ onPointerDown(plane, event);
456
+ }, onPointerEnter: (event) => {
457
+ if (disabled)
458
+ return;
459
+ event.stopPropagation();
460
+ setHovered(true);
461
+ document.body.style.cursor = 'move';
462
+ }, onPointerLeave: () => {
463
+ setHovered(false);
464
+ if (document.body.style.cursor === 'move')
465
+ document.body.style.cursor = '';
466
+ }, renderOrder: GIZMO_HIT_RENDER_ORDER })] }));
467
+ }
468
+ function RotationHandle({ axis, radius, tube, hitTube, arc, start, state, disabled, onPointerDown, }) {
469
+ const [hovered, setHovered] = useState(false);
470
+ const ringGeometry = useMemo(() => new TorusGeometry(radius, tube * 0.35, 8, 32, arc), [arc, radius, tube]);
471
+ const hitGeometry = useMemo(() => new TorusGeometry(radius, hitTube, 8, 32, arc), [arc, hitTube, radius]);
472
+ const material = useMemo(() => new MeshBasicNodeMaterial({
473
+ transparent: true,
474
+ opacity: 1,
475
+ depthTest: false,
476
+ depthWrite: false,
477
+ }), []);
478
+ const hitMaterial = useMemo(() => new MeshBasicNodeMaterial({
479
+ color: AXIS_COLORS[axis],
480
+ transparent: true,
481
+ opacity: 0,
482
+ depthTest: false,
483
+ depthWrite: false,
484
+ }), [axis]);
485
+ useEffect(() => {
486
+ material.color.set(hovered && state !== 'faded' ? PIVOT_HOVERED_COLOR : AXIS_COLORS[axis]);
487
+ material.opacity = state === 'faded' ? 0.14 : 1;
488
+ }, [axis, hovered, material, state]);
489
+ useEffect(() => () => {
490
+ ringGeometry.dispose();
491
+ hitGeometry.dispose();
492
+ material.dispose();
493
+ hitMaterial.dispose();
494
+ }, [hitGeometry, hitMaterial, material, ringGeometry]);
495
+ const rotation = axis === 'x' ? [0, -Math.PI / 2, 0] : axis === 'y' ? [Math.PI / 2, 0, 0] : [0, 0, 0];
496
+ return (_jsxs("group", { rotation: rotation, children: [_jsx("mesh", { geometry: ringGeometry, layers: EDITOR_LAYER, material: material, raycast: () => { }, renderOrder: GIZMO_RENDER_ORDER, rotation: [0, 0, start] }), _jsx("mesh", { geometry: hitGeometry, layers: EDITOR_LAYER, material: hitMaterial, onPointerDown: (event) => {
497
+ if (disabled)
498
+ return;
499
+ event.stopPropagation();
500
+ event.nativeEvent.stopImmediatePropagation();
501
+ swallowNextClick();
502
+ onPointerDown(axis, event);
503
+ }, onPointerEnter: (event) => {
504
+ if (disabled)
505
+ return;
506
+ event.stopPropagation();
507
+ setHovered(true);
508
+ document.body.style.cursor = 'grab';
509
+ }, onPointerLeave: () => {
510
+ setHovered(false);
511
+ if (document.body.style.cursor === 'grab')
512
+ document.body.style.cursor = '';
513
+ }, renderOrder: GIZMO_HIT_RENDER_ORDER, rotation: [0, 0, start] })] }));
514
+ }
515
+ function LoopCutTarget({ edgeId, start, end, radius, onHover, onPointerDown, }) {
516
+ const placement = useMemo(() => {
517
+ const from = new Vector3(...start);
518
+ const to = new Vector3(...end);
519
+ const direction = to.clone().sub(from);
520
+ return {
521
+ length: direction.length(),
522
+ position: from.add(to).multiplyScalar(0.5),
523
+ quaternion: new Quaternion().setFromUnitVectors(new Vector3(0, 1, 0), direction.normalize()),
524
+ };
525
+ }, [end, start]);
526
+ const geometry = useMemo(() => new CylinderGeometry(radius, radius, placement.length, 8), [placement.length, radius]);
527
+ const material = useMemo(() => new MeshBasicNodeMaterial({
528
+ color: '#ffffff',
529
+ transparent: true,
530
+ opacity: 0,
531
+ depthTest: false,
532
+ depthWrite: false,
533
+ }), []);
534
+ useEffect(() => () => {
535
+ geometry.dispose();
536
+ material.dispose();
537
+ if (document.body.style.cursor === 'crosshair')
538
+ document.body.style.cursor = '';
539
+ }, [geometry, material]);
540
+ return (_jsx("group", { position: placement.position, quaternion: placement.quaternion, children: _jsx("mesh", { geometry: geometry, layers: EDITOR_LAYER, material: material, onPointerDown: (event) => {
541
+ event.stopPropagation();
542
+ event.nativeEvent.stopImmediatePropagation();
543
+ swallowNextClick();
544
+ onPointerDown(edgeId, event);
545
+ }, onPointerEnter: (event) => {
546
+ event.stopPropagation();
547
+ onHover(edgeId);
548
+ document.body.style.cursor = 'crosshair';
549
+ }, onPointerLeave: () => {
550
+ onHover(null);
551
+ if (document.body.style.cursor === 'crosshair')
552
+ document.body.style.cursor = '';
553
+ }, renderOrder: 1221 }) }));
554
+ }
555
+ function LoopCutPreview({ segments }) {
556
+ const geometry = useMemo(() => {
557
+ const next = new BufferGeometry();
558
+ next.setAttribute('position', new Float32BufferAttribute(segments.flatMap(([from, to]) => [...from, ...to]), 3));
559
+ return next;
560
+ }, [segments]);
561
+ const material = useMemo(() => new LineBasicNodeMaterial({
562
+ color: '#facc15',
563
+ depthTest: false,
564
+ depthWrite: false,
565
+ }), []);
566
+ const line = useMemo(() => {
567
+ const next = new LineSegments(geometry, material);
568
+ next.frustumCulled = false;
569
+ next.layers.set(EDITOR_LAYER);
570
+ next.raycast = () => { };
571
+ next.renderOrder = 1220;
572
+ return next;
573
+ }, [geometry, material]);
574
+ useEffect(() => () => {
575
+ geometry.dispose();
576
+ material.dispose();
577
+ }, [geometry, material]);
578
+ return _jsx("primitive", { object: line });
579
+ }
580
+ function ToolbarButton({ label, active = false, disabled = false, destructive = false, sound = 'tool-select', onClick, children, }) {
581
+ return (_jsxs("span", { className: "group relative inline-flex", children: [_jsx("button", { "aria-label": label, className: cn('flex items-center justify-center rounded-md p-1.5 text-muted-foreground transition-colors', active && 'bg-accent text-foreground hover:bg-accent/80', !active && !destructive && 'hover:bg-accent hover:text-foreground', destructive && 'hover:bg-destructive/10 hover:text-destructive', 'disabled:cursor-not-allowed disabled:opacity-35'), disabled: disabled, onClick: (event) => {
582
+ event.stopPropagation();
583
+ if (!onClick)
584
+ return;
585
+ if (sound)
586
+ playBlockSfx(sound);
587
+ onClick();
588
+ }, type: "button", children: children }), _jsx("span", { "aria-hidden": "true", className: "pointer-events-none absolute bottom-full left-1/2 z-50 mb-2 -translate-x-1/2 whitespace-nowrap rounded-md bg-foreground px-2.5 py-1.5 font-medium text-[11px] text-background opacity-0 shadow-elevation-3 transition-opacity delay-150 group-hover:opacity-100 group-focus-within:opacity-100", children: label })] }));
589
+ }
590
+ function ToolbarMenuItem({ label, shortcut, active = false, disabled = false, destructive = false, sound = 'tool-select', onClick, children, }) {
591
+ return (_jsxs("button", { "aria-pressed": active || undefined, className: cn('flex h-9 w-full items-center gap-2 rounded-lg px-2.5 text-left text-xs transition-colors', active
592
+ ? 'bg-accent text-foreground'
593
+ : 'text-muted-foreground hover:bg-accent/70 hover:text-foreground', destructive && 'hover:bg-destructive/10 hover:text-destructive', 'disabled:cursor-not-allowed disabled:opacity-35 disabled:hover:bg-transparent disabled:hover:text-muted-foreground'), disabled: disabled, onClick: (event) => {
594
+ event.stopPropagation();
595
+ if (sound)
596
+ playBlockSfx(sound);
597
+ onClick();
598
+ }, type: "button", children: [children, _jsx("span", { children: label }), shortcut ? (_jsx("kbd", { className: "ml-auto font-mono text-[10px] text-muted-foreground/70", children: shortcut })) : null] }));
599
+ }
600
+ function ToolbarOperationItem({ label, shortcut, active = false, disabled = false, controls, onClick, children, }) {
601
+ return (_jsxs("div", { className: cn('flex h-8 items-center rounded-md transition-colors', active ? 'bg-accent' : 'hover:bg-accent/70', disabled && 'opacity-35'), children: [_jsxs("button", { className: "flex h-full min-w-0 flex-1 items-center gap-1.5 px-2 text-left text-[11px] text-muted-foreground hover:text-foreground disabled:cursor-not-allowed", disabled: disabled, onClick: (event) => {
602
+ event.stopPropagation();
603
+ onClick();
604
+ }, type: "button", children: [children, _jsx("span", { className: "whitespace-nowrap", children: label }), shortcut ? (_jsx("kbd", { className: "ml-auto font-mono text-[9px] text-muted-foreground/70", children: shortcut })) : null] }), controls ? _jsx("div", { className: "flex shrink-0 items-center gap-1 pr-1", children: controls }) : null] }));
605
+ }
606
+ function ToolbarPanelFrame({ label, className, children, }) {
607
+ return (_jsx("div", { "aria-label": label, className: cn(TOOLBAR_POPOVER_CLASS, className), role: "dialog", children: children }));
608
+ }
609
+ function LastOperationControls({ operation, onChange, }) {
610
+ const command = operation.command;
611
+ const input = (label, value, update, options = {}) => (_jsxs("label", { className: "flex items-center justify-between gap-3 text-[11px] text-muted-foreground", children: [_jsx("span", { children: label }), _jsx("input", { "aria-label": label, className: cn(OPERATION_INPUT_CLASS, 'w-20'), max: options.max, min: options.min, onChange: (event) => onChange(update(Number(event.target.value))), step: options.step ?? 0.01, type: "number", value: Math.round(value * 1000) / 1000 })] }, label));
612
+ switch (command.type) {
613
+ case 'translate-components':
614
+ return (_jsx("div", { className: "space-y-1", children: ['X', 'Y', 'Z'].map((axis, index) => input(`${axis} distance`, command.delta[index], (value) => ({
615
+ ...command,
616
+ delta: command.delta.map((current, currentIndex) => currentIndex === index ? value : current),
617
+ }))) }));
618
+ case 'rotate-components':
619
+ return input('Angle', (command.angle * 180) / Math.PI, (value) => ({ ...command, angle: (value * Math.PI) / 180 }), { step: 1 });
620
+ case 'scale-components':
621
+ return (_jsx("div", { className: "space-y-1", children: ['X', 'Y', 'Z'].map((axis, index) => input(`${axis} scale`, command.factors[index], (value) => ({
622
+ ...command,
623
+ factors: command.factors.map((current, currentIndex) => currentIndex === index ? value : current),
624
+ }))) }));
625
+ case 'extrude-faces':
626
+ return input('Distance', command.distance, (distance) => ({ ...command, distance }));
627
+ case 'inset-faces':
628
+ return input('Amount', command.amount, (amount) => ({ ...command, amount }), {
629
+ min: 0,
630
+ max: 0.95,
631
+ });
632
+ case 'bevel-edges':
633
+ return (_jsxs("div", { className: "space-y-1", children: [input('Width', command.width, (width) => ({ ...command, width }), { min: 0 }), input('Segments', command.segments, (segments) => ({ ...command, segments: Math.min(12, Math.max(1, segments)) }), { min: 1, max: 12, step: 1 })] }));
634
+ case 'loop-cut':
635
+ return (_jsxs("div", { className: "space-y-1", children: [input('Position', command.factor, (factor) => ({ ...command, factor }), {
636
+ min: 0.02,
637
+ max: 0.98,
638
+ }), input('Cuts', command.cuts ?? 1, (cuts) => ({ ...command, cuts: Math.min(32, Math.max(1, cuts)) }), { min: 1, max: 32, step: 1 })] }));
639
+ default:
640
+ return null;
641
+ }
642
+ }
643
+ function LastOperationPanel({ operation, onChange, onClose, onRepeat, }) {
644
+ return (_jsxs("div", { "aria-label": `Adjust ${operation.label}`, className: "pointer-events-auto absolute bottom-16 left-4 w-64 max-w-[calc(100%-2rem)] rounded-xl border border-border/50 bg-background/98 p-2 shadow-elevation-4 backdrop-blur-xl", onContextMenu: (event) => event.stopPropagation(), onPointerDown: (event) => event.stopPropagation(), onPointerUp: (event) => event.stopPropagation(), role: "dialog", children: [_jsxs("div", { className: "mb-2 flex items-center justify-between gap-3", children: [_jsxs("div", { children: [_jsx("div", { className: "font-medium text-xs", children: operation.label }), _jsx("div", { className: "text-[10px] text-muted-foreground", children: "Adjust Last Operation \u00B7 F9" })] }), _jsx("button", { "aria-label": "Close last operation panel", className: "rounded p-1 text-muted-foreground hover:bg-accent hover:text-foreground", onClick: onClose, type: "button", children: _jsx(XIcon, { className: "h-3.5 w-3.5" }) })] }), _jsx(LastOperationControls, { onChange: onChange, operation: operation }), _jsxs("button", { className: "mt-2 flex h-7 w-full items-center justify-between rounded-md bg-accent/50 px-2 text-[11px] text-foreground hover:bg-accent", onClick: onRepeat, type: "button", children: [_jsxs("span", { children: ["Repeat ", operation.label] }), _jsx("kbd", { className: "font-mono text-[9px] text-muted-foreground", children: "Shift+R" })] })] }));
645
+ }
646
+ function BlockEditor({ historyApi, interactionApi, node, readOnly, sceneApi, target, mirrorTarget, }) {
647
+ const { camera, gl } = useThree();
648
+ const outerRef = useRef(null);
649
+ const menuScaleRef = useRef(null);
650
+ const menuWorldPositionRef = useRef(new Vector3());
651
+ const editing = useInteractionScope((state) => state.scope.kind === 'mesh-editing' && state.scope.nodeId === node.id);
652
+ const mode = useBlockEditSession((state) => state.nodeId === node.id ? state.selection.mode : 'face');
653
+ const selectedIds = useBlockEditSession((state) => state.nodeId === node.id ? state.selection.ids : EMPTY_COMPONENT_IDS);
654
+ const activeId = useBlockEditSession((state) => state.nodeId === node.id ? state.selection.activeId : null);
655
+ const lastOperation = useBlockEditSession((state) => state.nodeId === node.id ? state.lastOperation : null);
656
+ const [transformTool, setTransformTool] = useState('transform');
657
+ const [xray, setXray] = useState(false);
658
+ const [previewTopology, setPreviewTopology] = useState(null);
659
+ const [activeTransform, setActiveTransform] = useState(null);
660
+ const [transformNumericInput, setTransformNumericInput] = useState('');
661
+ const [modalFeedbackMode, setModalFeedbackMode] = useState('free');
662
+ const [activeFaceOperation, setActiveFaceOperation] = useState(null);
663
+ const [faceOperationAxis, setFaceOperationAxis] = useState('normal');
664
+ const [faceOperationValue, setFaceOperationValue] = useState('');
665
+ const [loopCutSegments, setLoopCutSegments] = useState(null);
666
+ const [loopCutEdgeId, setLoopCutEdgeId] = useState(null);
667
+ const [loopCutSliding, setLoopCutSliding] = useState(false);
668
+ const [loopCutCount, setLoopCutCount] = useState(1);
669
+ const [loopCutFactor, setLoopCutFactor] = useState(0.5);
670
+ const [bevelSegments, setBevelSegments] = useState(DEFAULT_BEVEL_SEGMENTS);
671
+ const [bevelWidth, setBevelWidth] = useState(0);
672
+ const [lastOperationPanelOpen, setLastOperationPanelOpen] = useState(false);
673
+ const [toolbarPanel, setToolbarPanel] = useState(null);
674
+ const [error, setError] = useState(null);
675
+ const cancelDragRef = useRef(null);
676
+ const lastPointerClientRef = useRef(null);
677
+ const operationServices = useMemo(() => ({ historyApi, readOnly, sceneApi }), [historyApi, readOnly, sceneApi]);
678
+ const displayTopology = previewTopology ?? node.topology;
679
+ const selectedSet = useMemo(() => new Set(selectedIds), [selectedIds]);
680
+ const selection = useMemo(() => ({ mode, ids: selectedIds }), [mode, selectedIds]);
681
+ const extent = topologyExtent(displayTopology);
682
+ const componentRadius = Math.min(0.055, Math.max(0.022, extent * 0.011));
683
+ const gizmoOrigin = selectionCentroid(displayTopology, selection);
684
+ const gizmoDimensions = blockGizmoDimensions(extent);
685
+ const gizmoLength = gizmoDimensions.length;
686
+ const gizmoRadius = gizmoDimensions.radius;
687
+ const rotationGizmoRadius = gizmoDimensions.rotationRadius;
688
+ const planeHandleSize = gizmoDimensions.planeHandleSize;
689
+ const planeHandleOffset = gizmoDimensions.planeHandleOffset;
690
+ const gizmoHitDimensions = blockGizmoHitDimensions(gizmoRadius, planeHandleSize);
691
+ const vertexById = useMemo(() => topologyVertexMap(displayTopology), [displayTopology]);
692
+ const menuAnchor = useMemo(() => {
693
+ const xs = displayTopology.vertices.map((vertex) => vertex.position[0]);
694
+ const ys = displayTopology.vertices.map((vertex) => vertex.position[1]);
695
+ const zs = displayTopology.vertices.map((vertex) => vertex.position[2]);
696
+ return [
697
+ (Math.min(...xs) + Math.max(...xs)) / 2,
698
+ Math.max(...ys) + blockToolbarOffset(extent, gizmoLength),
699
+ (Math.min(...zs) + Math.max(...zs)) / 2,
700
+ ];
701
+ }, [displayTopology, extent, gizmoLength]);
702
+ useFrame((state) => {
703
+ const outer = outerRef.current;
704
+ if (!outer)
705
+ return;
706
+ if (mirrorTarget) {
707
+ outer.position.copy(target.position);
708
+ outer.quaternion.copy(target.quaternion);
709
+ outer.scale.copy(target.scale);
710
+ }
711
+ if (menuScaleRef.current) {
712
+ const menuWorldPosition = menuWorldPositionRef.current.set(...menuAnchor);
713
+ outer.localToWorld(menuWorldPosition);
714
+ menuScaleRef.current.style.transform = `scale(${getFloatingMenuScale(state.camera, menuWorldPosition)})`;
715
+ }
716
+ });
717
+ const ownsEditSession = useCallback(() => {
718
+ const scope = useInteractionScope.getState().scope;
719
+ return scope.kind === 'mesh-editing' && scope.nodeId === node.id;
720
+ }, [node.id]);
721
+ const endOwnedScope = useCallback(() => {
722
+ useInteractionScope
723
+ .getState()
724
+ .endIf((scope) => scope.kind === 'mesh-editing' && scope.nodeId === node.id);
725
+ }, [node.id]);
726
+ const exitEditMode = useCallback(() => {
727
+ cancelDragRef.current?.();
728
+ cancelDragRef.current = null;
729
+ useLiveNodeOverrides.getState().clear(node.id);
730
+ sceneApi.markDirty(node.id);
731
+ endOwnedScope();
732
+ useBlockEditSession.getState().end(node.id);
733
+ setPreviewTopology(null);
734
+ setTransformTool('transform');
735
+ setActiveTransform(null);
736
+ setTransformNumericInput('');
737
+ setModalFeedbackMode('free');
738
+ setActiveFaceOperation(null);
739
+ setFaceOperationValue('');
740
+ setLoopCutSegments(null);
741
+ setLoopCutEdgeId(null);
742
+ setLoopCutSliding(false);
743
+ setToolbarPanel(null);
744
+ setError(null);
745
+ playBlockSfx('finish');
746
+ }, [endOwnedScope, node.id, sceneApi.markDirty]);
747
+ useEffect(() => () => {
748
+ cancelDragRef.current?.();
749
+ useLiveNodeOverrides.getState().clear(node.id);
750
+ sceneApi.markDirty(node.id);
751
+ endOwnedScope();
752
+ useBlockEditSession.getState().end(node.id);
753
+ if (document.body.style.cursor === 'grabbing')
754
+ document.body.style.cursor = '';
755
+ }, [endOwnedScope, node.id, sceneApi.markDirty]);
756
+ useEffect(() => {
757
+ if (editing)
758
+ return;
759
+ cancelDragRef.current?.();
760
+ cancelDragRef.current = null;
761
+ useLiveNodeOverrides.getState().clear(node.id);
762
+ sceneApi.markDirty(node.id);
763
+ setPreviewTopology(null);
764
+ setToolbarPanel(null);
765
+ setLoopCutSegments(null);
766
+ setLoopCutEdgeId(null);
767
+ setLoopCutSliding(false);
768
+ setActiveTransform(null);
769
+ setTransformNumericInput('');
770
+ setModalFeedbackMode('free');
771
+ setActiveFaceOperation(null);
772
+ setFaceOperationValue('');
773
+ useBlockEditSession.getState().end(node.id);
774
+ }, [editing, node.id, sceneApi.markDirty]);
775
+ useEffect(() => {
776
+ if (!editing)
777
+ return;
778
+ const unpaintedSlotIds = new Set(unpaintedBlockMaterialSlotIds(node.topology, node.slots, node.slotNames));
779
+ if (unpaintedSlotIds.size === 0)
780
+ return;
781
+ const restores = [];
782
+ const ownedMaterials = [];
783
+ target.traverse((child) => {
784
+ if (!(child instanceof Mesh))
785
+ return;
786
+ const slotIds = Array.isArray(child.userData.slotIds)
787
+ ? child.userData.slotIds
788
+ : [];
789
+ if (!slotIds.some((slotId) => unpaintedSlotIds.has(slotId)))
790
+ return;
791
+ const previousMaterial = child.material;
792
+ const sourceMaterials = Array.isArray(previousMaterial)
793
+ ? previousMaterial
794
+ : [previousMaterial];
795
+ const nextMaterials = sourceMaterials.map((material, index) => {
796
+ const slotId = slotIds[index];
797
+ if (!(slotId && slotId !== BLOCK_BODY_SLOT_ID && unpaintedSlotIds.has(slotId))) {
798
+ return material;
799
+ }
800
+ const tinted = material.clone();
801
+ if ('color' in tinted && tinted.color instanceof Color)
802
+ tinted.color.set('#7768d8');
803
+ ownedMaterials.push(tinted);
804
+ return tinted;
805
+ });
806
+ restores.push({ mesh: child, material: previousMaterial });
807
+ child.material = Array.isArray(previousMaterial) ? nextMaterials : nextMaterials[0];
808
+ });
809
+ return () => {
810
+ for (const restore of restores)
811
+ restore.mesh.material = restore.material;
812
+ for (const material of ownedMaterials)
813
+ material.dispose();
814
+ };
815
+ }, [editing, node.slotNames, node.slots, node.topology, target]);
816
+ useEffect(() => {
817
+ if (!editing)
818
+ return;
819
+ const onToolCancel = () => {
820
+ markToolCancelConsumed();
821
+ if (toolbarPanel) {
822
+ setToolbarPanel(null);
823
+ playBlockSfx('cancel');
824
+ }
825
+ else if (cancelDragRef.current)
826
+ cancelDragRef.current();
827
+ else if (transformTool === 'loop-cut') {
828
+ setTransformTool('transform');
829
+ setLoopCutEdgeId(null);
830
+ setLoopCutSegments(null);
831
+ setError(null);
832
+ playBlockSfx('cancel');
833
+ }
834
+ else
835
+ exitEditMode();
836
+ };
837
+ emitter.on('tool:cancel', onToolCancel);
838
+ return () => emitter.off('tool:cancel', onToolCancel);
839
+ }, [editing, exitEditMode, toolbarPanel, transformTool]);
840
+ useEffect(() => {
841
+ if (!(editing && toolbarPanel))
842
+ return;
843
+ const closePanel = (event) => {
844
+ const targetElement = event.target;
845
+ if (targetElement instanceof Node && menuScaleRef.current?.contains(targetElement))
846
+ return;
847
+ setToolbarPanel(null);
848
+ };
849
+ window.addEventListener('pointerdown', closePanel, true);
850
+ return () => window.removeEventListener('pointerdown', closePanel, true);
851
+ }, [editing, toolbarPanel]);
852
+ useEffect(() => {
853
+ if (!editing)
854
+ return;
855
+ const trackPointer = (event) => {
856
+ lastPointerClientRef.current = new Vector2(event.clientX, event.clientY);
857
+ };
858
+ window.addEventListener('pointermove', trackPointer, true);
859
+ return () => window.removeEventListener('pointermove', trackPointer, true);
860
+ }, [editing]);
861
+ useEffect(() => {
862
+ if (!editing)
863
+ return;
864
+ const onGridClick = () => {
865
+ const scope = useInteractionScope.getState().scope;
866
+ if (scope.kind !== 'mesh-editing' || scope.nodeId !== node.id || cancelDragRef.current)
867
+ return;
868
+ const session = useBlockEditSession.getState();
869
+ const next = { mode, ids: [], activeId: null };
870
+ if (!blockSelectionChanged(session.selection, next))
871
+ return;
872
+ session.setSelection(node.id, next);
873
+ setError(null);
874
+ playBlockSfx('component-select');
875
+ };
876
+ emitter.on('grid:click', onGridClick);
877
+ return () => emitter.off('grid:click', onGridClick);
878
+ }, [editing, mode, node.id]);
879
+ useEffect(() => {
880
+ const onKeyDown = (event) => {
881
+ const element = event.target;
882
+ if (element?.tagName === 'INPUT' ||
883
+ element?.tagName === 'TEXTAREA' ||
884
+ element?.isContentEditable)
885
+ return;
886
+ if (event.key === 'Tab') {
887
+ event.preventDefault();
888
+ event.stopImmediatePropagation();
889
+ if (cancelDragRef.current)
890
+ return;
891
+ if (editing) {
892
+ exitEditMode();
893
+ }
894
+ else if (useInteractionScope.getState().scope.kind === 'idle') {
895
+ const face = preferredFace(node.topology);
896
+ useBlockEditSession.getState().begin(node.id, {
897
+ mode: 'face',
898
+ ids: face ? [face.id] : [],
899
+ activeId: face?.id ?? null,
900
+ });
901
+ setTransformTool('transform');
902
+ setToolbarPanel(null);
903
+ setError(null);
904
+ useInteractionScope.getState().begin(meshEditScope(node.id));
905
+ triggerSFX('sfx:item-pick');
906
+ }
907
+ return;
908
+ }
909
+ if (!editing)
910
+ return;
911
+ const nextMode = event.key === '1'
912
+ ? 'vertex'
913
+ : event.key === '2'
914
+ ? 'edge'
915
+ : event.key === '3'
916
+ ? 'face'
917
+ : null;
918
+ if (!nextMode || cancelDragRef.current)
919
+ return;
920
+ event.preventDefault();
921
+ event.stopImmediatePropagation();
922
+ const converted = convertBlockSelection(node.topology, {
923
+ mode,
924
+ ids: selectedIds,
925
+ activeId,
926
+ }, nextMode);
927
+ useBlockEditSession.getState().setSelection(node.id, converted);
928
+ setError(null);
929
+ playBlockSfx('tool-select');
930
+ };
931
+ window.addEventListener('keydown', onKeyDown, true);
932
+ return () => window.removeEventListener('keydown', onKeyDown, true);
933
+ }, [activeId, editing, exitEditMode, mode, node.id, node.topology, selectedIds]);
934
+ useEffect(() => {
935
+ useBlockEditSession.getState().reconcileSelection(node.id, node.topology);
936
+ }, [node.id, node.topology]);
937
+ const enterEditMode = (event) => {
938
+ event.stopPropagation();
939
+ const face = preferredFace(node.topology);
940
+ useBlockEditSession.getState().begin(node.id, {
941
+ mode: 'face',
942
+ ids: face ? [face.id] : [],
943
+ activeId: face?.id ?? null,
944
+ });
945
+ setTransformTool('transform');
946
+ setToolbarPanel(null);
947
+ setError(null);
948
+ useInteractionScope.getState().begin(meshEditScope(node.id));
949
+ triggerSFX('sfx:item-pick');
950
+ };
951
+ const componentIsVisible = useCallback((id, event) => {
952
+ if (xray)
953
+ return true;
954
+ target.updateWorldMatrix(true, true);
955
+ const raycaster = new Raycaster();
956
+ raycaster.ray.copy(event.ray);
957
+ const nearestSurface = raycaster.intersectObject(target, true)[0];
958
+ if (!nearestSurface)
959
+ return true;
960
+ let worldPoint = null;
961
+ if (mode === 'vertex') {
962
+ const vertex = displayTopology.vertices.find((entry) => entry.id === id);
963
+ if (vertex)
964
+ worldPoint = target.localToWorld(new Vector3(...vertex.position));
965
+ }
966
+ else if (mode === 'edge') {
967
+ const edge = displayTopology.edges.find((entry) => entry.id === id);
968
+ const vertices = topologyVertexMap(displayTopology);
969
+ const start = edge ? vertices.get(edge.vertexIds[0]) : null;
970
+ const end = edge ? vertices.get(edge.vertexIds[1]) : null;
971
+ if (start && end) {
972
+ const worldStart = target.localToWorld(new Vector3(...start));
973
+ const worldEnd = target.localToWorld(new Vector3(...end));
974
+ worldPoint = new Vector3();
975
+ event.ray.distanceSqToSegment(worldStart, worldEnd, undefined, worldPoint);
976
+ }
977
+ }
978
+ else {
979
+ worldPoint = event.point.clone();
980
+ }
981
+ if (!worldPoint)
982
+ return false;
983
+ const scale = target.getWorldScale(new Vector3());
984
+ const tolerance = componentRadius * Math.max(scale.x, scale.y, scale.z) * 1.5;
985
+ return event.ray.origin.distanceTo(worldPoint) <= nearestSurface.distance + tolerance;
986
+ }, [componentRadius, displayTopology, mode, target, xray]);
987
+ const selectComponent = useCallback((id, additive, event) => {
988
+ if (!componentIsVisible(id, event))
989
+ return;
990
+ const next = selectBlockComponent({ mode, ids: selectedIds, activeId }, id, additive);
991
+ if (!blockSelectionChanged({ mode, ids: selectedIds, activeId }, next))
992
+ return;
993
+ useBlockEditSession.getState().setSelection(node.id, next);
994
+ setError(null);
995
+ playBlockSfx('component-select');
996
+ }, [activeId, componentIsVisible, mode, node.id, selectedIds]);
997
+ const switchMode = (nextMode) => {
998
+ if (cancelDragRef.current)
999
+ return;
1000
+ const converted = convertBlockSelection(displayTopology, { mode, ids: selectedIds, activeId }, nextMode);
1001
+ useBlockEditSession.getState().setSelection(node.id, converted);
1002
+ setToolbarPanel(null);
1003
+ setError(null);
1004
+ };
1005
+ const makeRay = useCallback((clientX, clientY) => {
1006
+ const rect = gl.domElement.getBoundingClientRect();
1007
+ const pointer = new Vector2(((clientX - rect.left) / rect.width) * 2 - 1, -((clientY - rect.top) / rect.height) * 2 + 1);
1008
+ const raycaster = new Raycaster();
1009
+ raycaster.setFromCamera(pointer, camera);
1010
+ return raycaster.ray;
1011
+ }, [camera, gl.domElement]);
1012
+ const commitAdjustableOperation = useCallback((baseTopology, command, label) => {
1013
+ const committed = commitBlockOperation(operationServices, node.id, label, baseTopology, command);
1014
+ if (!committed.ok) {
1015
+ setError(committed.error);
1016
+ return false;
1017
+ }
1018
+ if (!committed.changed) {
1019
+ setError(null);
1020
+ return false;
1021
+ }
1022
+ const session = useBlockEditSession.getState();
1023
+ session.setSelection(node.id, {
1024
+ ...committed.result.selection,
1025
+ activeId: committed.result.selection.ids.at(-1) ?? null,
1026
+ });
1027
+ session.setLastOperation(node.id, committed.operation);
1028
+ setLastOperationPanelOpen(true);
1029
+ setError(null);
1030
+ return true;
1031
+ }, [node.id, operationServices]);
1032
+ const beginKeyboardTransformModal = useCallback((operation) => {
1033
+ if (!ownsEditSession() || selectedIds.length === 0 || cancelDragRef.current)
1034
+ return false;
1035
+ const origin = selectionCentroid(displayTopology, selection);
1036
+ if (!origin)
1037
+ return false;
1038
+ const pivotClient = localPointToClient(origin, target, camera, gl.domElement);
1039
+ if (!pivotClient)
1040
+ return false;
1041
+ target.updateWorldMatrix(true, false);
1042
+ const originLocal = new Vector3(...origin);
1043
+ const worldOrigin = target.localToWorld(originLocal.clone());
1044
+ const startPointer = lastPointerClientRef.current?.clone() ?? pivotClient.clone().add(new Vector2(80, 0));
1045
+ const startRay = makeRay(startPointer.x, startPointer.y);
1046
+ const viewAxisWorld = camera.getWorldDirection(new Vector3()).normalize();
1047
+ const viewPlane = new Plane().setFromNormalAndCoplanarPoint(viewAxisWorld, worldOrigin);
1048
+ const startPlaneHit = startRay.intersectPlane(viewPlane, new Vector3()) ?? worldOrigin.clone();
1049
+ const targetWorldQuaternion = target.getWorldQuaternion(new Quaternion());
1050
+ const freeRotationAxis = viewAxisWorld
1051
+ .clone()
1052
+ .applyQuaternion(targetWorldQuaternion.clone().invert())
1053
+ .normalize();
1054
+ const baseTopology = displayTopology;
1055
+ const baseSelection = selection;
1056
+ let activeConstraint = null;
1057
+ let latestTopology = null;
1058
+ let latestCommand = null;
1059
+ let latestMagnitude = 0;
1060
+ let previousWrappedAngle = 0;
1061
+ let accumulatedAngle = 0;
1062
+ let lockedRotationInitialHit = null;
1063
+ let lockedRotationPlane = null;
1064
+ let lockedRotationWorldAxis = null;
1065
+ let lockedTranslationInitialHit = null;
1066
+ let lockedTranslationPlane = null;
1067
+ let lastClientX = startPointer.x;
1068
+ let lastClientY = startPointer.y;
1069
+ let lastAltKey = false;
1070
+ let lastSnapValue = null;
1071
+ let typedInput = '';
1072
+ const worldAxisFor = (axis) => target
1073
+ .localToWorld(originLocal.clone().add(new Vector3(...AXIS_VECTORS[axis])))
1074
+ .sub(worldOrigin)
1075
+ .normalize();
1076
+ const updatePreview = (clientX, clientY, altKey) => {
1077
+ lastClientX = clientX;
1078
+ lastClientY = clientY;
1079
+ lastAltKey = altKey;
1080
+ const ray = makeRay(clientX, clientY);
1081
+ const numericValue = blockTransformNumericValue(typedInput, operation);
1082
+ let command;
1083
+ let snapValue;
1084
+ if (operation === 'translate') {
1085
+ let delta;
1086
+ if (activeConstraint && isAxisConstraint(activeConstraint)) {
1087
+ const worldAxis = worldAxisFor(activeConstraint);
1088
+ const startParameter = closestAxisParameterToRay(worldOrigin, worldAxis, startRay);
1089
+ const currentParameter = closestAxisParameterToRay(worldOrigin, worldAxis, ray);
1090
+ const localPoint = target.worldToLocal(worldOrigin.clone().addScaledVector(worldAxis, currentParameter - startParameter));
1091
+ const axisIndex = activeConstraint === 'x' ? 0 : activeConstraint === 'y' ? 1 : 2;
1092
+ delta = blockAxisDelta(activeConstraint, blockPointerDistanceForAxis(activeConstraint, localPoint.getComponent(axisIndex) - origin[axisIndex]));
1093
+ }
1094
+ else if (activeConstraint &&
1095
+ isPlaneConstraint(activeConstraint) &&
1096
+ lockedTranslationInitialHit &&
1097
+ lockedTranslationPlane) {
1098
+ const currentHit = ray.intersectPlane(lockedTranslationPlane, new Vector3());
1099
+ if (!currentHit)
1100
+ return;
1101
+ const localPoint = target.worldToLocal(worldOrigin.clone().add(currentHit.sub(lockedTranslationInitialHit)));
1102
+ delta = blockConstrainTranslationDelta([localPoint.x - origin[0], localPoint.y - origin[1], localPoint.z - origin[2]], activeConstraint);
1103
+ }
1104
+ else {
1105
+ const currentHit = ray.intersectPlane(viewPlane, new Vector3());
1106
+ if (!currentHit)
1107
+ return;
1108
+ const localPoint = target.worldToLocal(worldOrigin.clone().add(currentHit.clone().sub(startPlaneHit)));
1109
+ delta = [localPoint.x - origin[0], localPoint.y - origin[1], localPoint.z - origin[2]];
1110
+ }
1111
+ if (numericValue !== null) {
1112
+ delta = blockNumericDeltaForConstraint(activeConstraint ?? 'free', delta, numericValue);
1113
+ }
1114
+ const snapping = numericValue === null && isGridSnapActive() && !altKey;
1115
+ if (snapping) {
1116
+ const step = useEditor.getState().gridSnapStep;
1117
+ if (step > 0)
1118
+ delta = delta.map((value) => Math.round(value / step) * step);
1119
+ }
1120
+ const geometrySnap = numericValue === null && isMagneticSnapActive() && !altKey
1121
+ ? resolveBlockGeometrySnap(baseTopology, baseSelection, delta, activeConstraint ?? 'free', geometrySnapThreshold(camera, worldOrigin, target, gl.domElement, extent))
1122
+ : null;
1123
+ if (geometrySnap)
1124
+ delta = geometrySnap.delta;
1125
+ latestMagnitude = Math.hypot(...delta);
1126
+ const signedDistance = activeConstraint && isAxisConstraint(activeConstraint)
1127
+ ? delta[activeConstraint === 'x' ? 0 : activeConstraint === 'y' ? 1 : 2]
1128
+ : latestMagnitude;
1129
+ setTransformNumericInput(typedInput || blockTransformDisplayValue('translate', signedDistance));
1130
+ setModalFeedbackMode(typedInput ? 'exact' : geometrySnap ? 'geometry' : snapping ? 'grid' : 'free');
1131
+ snapValue = geometrySnap
1132
+ ? `${geometrySnap.kind}:${geometrySnap.targetId}`
1133
+ : delta.join(':');
1134
+ if ((snapping || geometrySnap) && latestMagnitude > 1e-6 && snapValue !== lastSnapValue) {
1135
+ playBlockSfx('move-step');
1136
+ }
1137
+ command = { type: 'translate-components', selection: baseSelection, delta };
1138
+ }
1139
+ else {
1140
+ let wrappedAngle;
1141
+ if (activeConstraint?.length === 1 &&
1142
+ lockedRotationInitialHit &&
1143
+ lockedRotationPlane &&
1144
+ lockedRotationWorldAxis) {
1145
+ const currentHit = ray.intersectPlane(lockedRotationPlane, new Vector3());
1146
+ if (!currentHit)
1147
+ return;
1148
+ const lockedAngle = lockedRotationAngleFromHits(worldOrigin, lockedRotationInitialHit, currentHit, lockedRotationWorldAxis);
1149
+ if (lockedAngle === null) {
1150
+ if (currentHit.distanceToSquared(worldOrigin) > 1e-6) {
1151
+ lockedRotationInitialHit = currentHit.clone();
1152
+ }
1153
+ wrappedAngle = 0;
1154
+ }
1155
+ else {
1156
+ wrappedAngle = lockedAngle;
1157
+ }
1158
+ }
1159
+ else {
1160
+ wrappedAngle = blockRotationPointerAngle(pivotClient, startPointer, new Vector2(clientX, clientY));
1161
+ }
1162
+ accumulatedAngle += unwrapRotationDelta(previousWrappedAngle, wrappedAngle);
1163
+ previousWrappedAngle = wrappedAngle;
1164
+ let angle = accumulatedAngle;
1165
+ if (numericValue !== null)
1166
+ angle = numericValue;
1167
+ const snapping = numericValue === null && isAngleSnapActive() && !altKey;
1168
+ if (snapping) {
1169
+ const step = (ROTATION_SNAP_ANGLE_DEGREES * Math.PI) / 180;
1170
+ angle = Math.round(angle / step) * step;
1171
+ }
1172
+ latestMagnitude = Math.abs(angle);
1173
+ setTransformNumericInput(typedInput || blockTransformDisplayValue('rotate', angle));
1174
+ setModalFeedbackMode(typedInput ? 'exact' : snapping ? 'angle' : 'free');
1175
+ snapValue = angle;
1176
+ if (snapping && latestMagnitude > 1e-6 && snapValue !== lastSnapValue) {
1177
+ playBlockSfx('rotate-step');
1178
+ }
1179
+ command = {
1180
+ type: 'rotate-components',
1181
+ selection: baseSelection,
1182
+ pivot: origin,
1183
+ axis: activeConstraint && isAxisConstraint(activeConstraint)
1184
+ ? AXIS_VECTORS[activeConstraint]
1185
+ : freeRotationAxis.toArray(),
1186
+ angle,
1187
+ };
1188
+ }
1189
+ lastSnapValue = snapValue;
1190
+ const result = applyBlockCommand(baseTopology, command);
1191
+ if (!result.ok) {
1192
+ setError(result.error);
1193
+ return;
1194
+ }
1195
+ latestTopology = result.topology;
1196
+ latestCommand = command;
1197
+ setPreviewTopology(result.topology);
1198
+ useLiveNodeOverrides.getState().set(node.id, { topology: result.topology });
1199
+ sceneApi.markDirty(node.id);
1200
+ setError(null);
1201
+ };
1202
+ const complete = (commit) => {
1203
+ useLiveNodeOverrides.getState().clear(node.id);
1204
+ sceneApi.markDirty(node.id);
1205
+ setPreviewTopology(null);
1206
+ setActiveTransform(null);
1207
+ setTransformNumericInput('');
1208
+ setModalFeedbackMode('free');
1209
+ if (commit && latestTopology && latestCommand && latestMagnitude > 1e-6) {
1210
+ commitAdjustableOperation(baseTopology, latestCommand, operation === 'translate' ? 'Move' : 'Rotate');
1211
+ playBlockSfx('finish');
1212
+ }
1213
+ else if (!commit) {
1214
+ playBlockSfx('cancel');
1215
+ }
1216
+ if (ownsEditSession())
1217
+ useInteractionScope.getState().begin(meshEditScope(node.id));
1218
+ swallowNextClick();
1219
+ };
1220
+ const onMove = (pointerEvent) => {
1221
+ lastPointerClientRef.current = new Vector2(pointerEvent.clientX, pointerEvent.clientY);
1222
+ updatePreview(pointerEvent.clientX, pointerEvent.clientY, pointerEvent.altKey);
1223
+ };
1224
+ const onPointerDown = (pointerEvent, finish) => {
1225
+ if (pointerEvent.button !== 0 && pointerEvent.button !== 2)
1226
+ return;
1227
+ pointerEvent.preventDefault();
1228
+ pointerEvent.stopImmediatePropagation();
1229
+ finish(pointerEvent.button === 0);
1230
+ };
1231
+ const onKeyDown = (keyboardEvent, finish) => {
1232
+ const element = keyboardEvent.target;
1233
+ if (element?.tagName === 'INPUT' ||
1234
+ element?.tagName === 'TEXTAREA' ||
1235
+ element?.isContentEditable)
1236
+ return;
1237
+ const constraint = blockTransformConstraintFromKey(keyboardEvent.key, operation === 'translate' && keyboardEvent.shiftKey);
1238
+ if (constraint) {
1239
+ keyboardEvent.preventDefault();
1240
+ keyboardEvent.stopImmediatePropagation();
1241
+ activeConstraint = constraint;
1242
+ if (operation === 'rotate') {
1243
+ const axis = constraint;
1244
+ lockedRotationWorldAxis = worldAxisFor(axis);
1245
+ lockedRotationPlane = new Plane().setFromNormalAndCoplanarPoint(lockedRotationWorldAxis, worldOrigin);
1246
+ lockedRotationInitialHit = makeRay(lastClientX, lastClientY).intersectPlane(lockedRotationPlane, new Vector3());
1247
+ previousWrappedAngle = 0;
1248
+ accumulatedAngle = 0;
1249
+ }
1250
+ else if (isPlaneConstraint(constraint)) {
1251
+ const normalAxis = PLANE_NORMAL[constraint];
1252
+ lockedTranslationPlane = new Plane().setFromNormalAndCoplanarPoint(worldAxisFor(normalAxis), worldOrigin);
1253
+ lockedTranslationInitialHit = startRay.intersectPlane(lockedTranslationPlane, new Vector3());
1254
+ }
1255
+ else {
1256
+ lockedTranslationPlane = null;
1257
+ lockedTranslationInitialHit = null;
1258
+ }
1259
+ setActiveTransform({ operation, constraint });
1260
+ lastSnapValue = null;
1261
+ updatePreview(lastClientX, lastClientY, lastAltKey);
1262
+ }
1263
+ else {
1264
+ const nextInput = blockTransformNumericInputFromKey(typedInput, keyboardEvent.key);
1265
+ if (nextInput !== null) {
1266
+ keyboardEvent.preventDefault();
1267
+ keyboardEvent.stopImmediatePropagation();
1268
+ typedInput = nextInput;
1269
+ setTransformNumericInput(nextInput);
1270
+ lastSnapValue = null;
1271
+ updatePreview(lastClientX, lastClientY, lastAltKey);
1272
+ }
1273
+ else if (keyboardEvent.key === 'Enter') {
1274
+ keyboardEvent.preventDefault();
1275
+ keyboardEvent.stopImmediatePropagation();
1276
+ finish(true);
1277
+ }
1278
+ else if (keyboardEvent.key === 'Escape') {
1279
+ keyboardEvent.preventDefault();
1280
+ keyboardEvent.stopImmediatePropagation();
1281
+ finish(false);
1282
+ }
1283
+ }
1284
+ };
1285
+ useInteractionScope.getState().begin(meshEditScope(node.id, 'operating', operation));
1286
+ playBlockSfx('drag-start');
1287
+ setTransformTool('transform');
1288
+ setToolbarPanel(null);
1289
+ setActiveTransform({ operation, constraint: 'free' });
1290
+ setTransformNumericInput('');
1291
+ setModalFeedbackMode('free');
1292
+ setError(null);
1293
+ beginBlockModalSession({
1294
+ beginInputDrag: interactionApi.beginInputDrag,
1295
+ cancelRef: cancelDragRef,
1296
+ cursor: operation === 'translate' ? 'move' : 'crosshair',
1297
+ onFinish: complete,
1298
+ onKeyDown,
1299
+ onPointerDown,
1300
+ onPointerMove: onMove,
1301
+ });
1302
+ return true;
1303
+ }, [
1304
+ camera,
1305
+ commitAdjustableOperation,
1306
+ displayTopology,
1307
+ extent,
1308
+ gl.domElement,
1309
+ interactionApi.beginInputDrag,
1310
+ makeRay,
1311
+ node.id,
1312
+ ownsEditSession,
1313
+ selectedIds.length,
1314
+ selection,
1315
+ target,
1316
+ sceneApi.markDirty,
1317
+ ]);
1318
+ const beginTranslationDrag = useCallback((constraint, event) => {
1319
+ if (!ownsEditSession() || selectedIds.length === 0 || cancelDragRef.current)
1320
+ return;
1321
+ const origin = selectionCentroid(displayTopology, selection);
1322
+ if (!origin)
1323
+ return;
1324
+ target.updateWorldMatrix(true, false);
1325
+ const originLocal = new Vector3(...origin);
1326
+ const worldOrigin = target.localToWorld(originLocal.clone());
1327
+ const normalAxis = isPlaneConstraint(constraint) ? PLANE_NORMAL[constraint] : constraint;
1328
+ const localAxis = new Vector3(...AXIS_VECTORS[normalAxis]);
1329
+ const worldAxis = target
1330
+ .localToWorld(originLocal.clone().add(localAxis))
1331
+ .sub(worldOrigin)
1332
+ .normalize();
1333
+ const dragPlane = isPlaneConstraint(constraint)
1334
+ ? new Plane().setFromNormalAndCoplanarPoint(worldAxis, worldOrigin)
1335
+ : null;
1336
+ const initialPlaneHit = dragPlane ? event.ray.intersectPlane(dragPlane, new Vector3()) : null;
1337
+ if (dragPlane && !initialPlaneHit)
1338
+ return;
1339
+ const initialParameter = isAxisConstraint(constraint)
1340
+ ? closestAxisParameterToRay(worldOrigin, worldAxis, event.ray)
1341
+ : 0;
1342
+ const axisIndex = normalAxis === 'x' ? 0 : normalAxis === 'y' ? 1 : 2;
1343
+ const baseTopology = displayTopology;
1344
+ const baseSelection = selection;
1345
+ const restoreInputDragging = interactionApi.beginInputDrag();
1346
+ const previousCursor = document.body.style.cursor;
1347
+ let latestTopology = null;
1348
+ let latestDelta = [0, 0, 0];
1349
+ let lastSnapDelta = null;
1350
+ let finished = false;
1351
+ useInteractionScope.getState().begin(meshEditScope(node.id, 'operating', 'translate'));
1352
+ playBlockSfx('drag-start');
1353
+ setActiveTransform({ operation: 'translate', constraint });
1354
+ setTransformNumericInput('0');
1355
+ setModalFeedbackMode('free');
1356
+ document.body.style.cursor = 'grabbing';
1357
+ const onMove = (pointerEvent) => {
1358
+ const ray = makeRay(pointerEvent.clientX, pointerEvent.clientY);
1359
+ let delta;
1360
+ if (dragPlane && initialPlaneHit) {
1361
+ const currentHit = ray.intersectPlane(dragPlane, new Vector3());
1362
+ if (!currentHit)
1363
+ return;
1364
+ const localPoint = target.worldToLocal(worldOrigin.clone().add(currentHit.sub(initialPlaneHit)));
1365
+ delta = [localPoint.x - origin[0], localPoint.y - origin[1], localPoint.z - origin[2]];
1366
+ delta[axisIndex] = 0;
1367
+ }
1368
+ else {
1369
+ delta = [0, 0, 0];
1370
+ const parameter = closestAxisParameterToRay(worldOrigin, worldAxis, ray);
1371
+ const worldPoint = worldOrigin
1372
+ .clone()
1373
+ .addScaledVector(worldAxis, parameter - initialParameter);
1374
+ const localPoint = target.worldToLocal(worldPoint);
1375
+ delta[axisIndex] = blockPointerDistanceForAxis(normalAxis, localPoint.getComponent(axisIndex) - originLocal.getComponent(axisIndex));
1376
+ }
1377
+ const snapping = isGridSnapActive() && !pointerEvent.altKey;
1378
+ if (snapping) {
1379
+ const step = useEditor.getState().gridSnapStep;
1380
+ if (step > 0) {
1381
+ delta = delta.map((value) => Math.round(value / step) * step);
1382
+ }
1383
+ }
1384
+ const geometrySnap = isMagneticSnapActive() && !pointerEvent.altKey
1385
+ ? resolveBlockGeometrySnap(baseTopology, baseSelection, delta, constraint, geometrySnapThreshold(camera, worldOrigin, target, gl.domElement, extent))
1386
+ : null;
1387
+ if (geometrySnap)
1388
+ delta.splice(0, 3, ...geometrySnap.delta);
1389
+ const snapDelta = delta.join(':');
1390
+ const magnitude = Math.hypot(...delta);
1391
+ setTransformNumericInput(blockTransformDisplayValue('translate', isAxisConstraint(constraint) ? delta[axisIndex] : Math.hypot(...delta)));
1392
+ setModalFeedbackMode(geometrySnap ? 'geometry' : snapping ? 'grid' : 'free');
1393
+ const activeSnap = geometrySnap
1394
+ ? `${geometrySnap.kind}:${geometrySnap.targetId}`
1395
+ : snapDelta;
1396
+ if ((snapping || geometrySnap) && magnitude > 1e-6 && activeSnap !== lastSnapDelta) {
1397
+ lastSnapDelta = activeSnap;
1398
+ playBlockSfx('move-step');
1399
+ }
1400
+ else if (!(snapping || geometrySnap)) {
1401
+ lastSnapDelta = null;
1402
+ }
1403
+ const result = applyBlockCommand(baseTopology, {
1404
+ type: 'translate-components',
1405
+ selection: baseSelection,
1406
+ delta,
1407
+ });
1408
+ if (!result.ok) {
1409
+ setError(result.error);
1410
+ return;
1411
+ }
1412
+ latestDelta = delta;
1413
+ latestTopology = result.topology;
1414
+ setPreviewTopology(result.topology);
1415
+ useLiveNodeOverrides.getState().set(node.id, { topology: result.topology });
1416
+ sceneApi.markDirty(node.id);
1417
+ };
1418
+ const finish = (commit) => {
1419
+ if (finished)
1420
+ return;
1421
+ finished = true;
1422
+ window.removeEventListener('pointermove', onMove);
1423
+ window.removeEventListener('pointerup', onPointerUp);
1424
+ window.removeEventListener('pointercancel', onPointerCancel);
1425
+ window.removeEventListener('blur', onPointerCancel);
1426
+ cancelDragRef.current = null;
1427
+ useLiveNodeOverrides.getState().clear(node.id);
1428
+ sceneApi.markDirty(node.id);
1429
+ restoreInputDragging();
1430
+ document.body.style.cursor = previousCursor;
1431
+ setPreviewTopology(null);
1432
+ setActiveTransform(null);
1433
+ setTransformNumericInput('');
1434
+ setModalFeedbackMode('free');
1435
+ if (commit && latestTopology && Math.hypot(...latestDelta) > 1e-6) {
1436
+ commitAdjustableOperation(baseTopology, { type: 'translate-components', selection: baseSelection, delta: latestDelta }, 'Move');
1437
+ playBlockSfx('finish');
1438
+ }
1439
+ else if (!commit) {
1440
+ playBlockSfx('cancel');
1441
+ }
1442
+ if (ownsEditSession()) {
1443
+ useInteractionScope.getState().begin(meshEditScope(node.id));
1444
+ }
1445
+ swallowNextClick();
1446
+ };
1447
+ const onPointerUp = () => finish(true);
1448
+ const onPointerCancel = () => finish(false);
1449
+ cancelDragRef.current = onPointerCancel;
1450
+ window.addEventListener('pointermove', onMove);
1451
+ window.addEventListener('pointerup', onPointerUp, { once: true });
1452
+ window.addEventListener('pointercancel', onPointerCancel, { once: true });
1453
+ window.addEventListener('blur', onPointerCancel, { once: true });
1454
+ }, [
1455
+ camera,
1456
+ commitAdjustableOperation,
1457
+ displayTopology,
1458
+ extent,
1459
+ gl.domElement,
1460
+ interactionApi.beginInputDrag,
1461
+ makeRay,
1462
+ node.id,
1463
+ ownsEditSession,
1464
+ selectedIds.length,
1465
+ selection,
1466
+ target,
1467
+ sceneApi.markDirty,
1468
+ ]);
1469
+ const beginRotationDrag = useCallback((axis, event) => {
1470
+ if (!ownsEditSession() || selectedIds.length === 0 || cancelDragRef.current)
1471
+ return;
1472
+ const origin = selectionCentroid(displayTopology, selection);
1473
+ if (!origin)
1474
+ return;
1475
+ target.updateWorldMatrix(true, false);
1476
+ const originLocal = new Vector3(...origin);
1477
+ const worldOrigin = target.localToWorld(originLocal.clone());
1478
+ const localAxis = new Vector3(...AXIS_VECTORS[axis]);
1479
+ const worldAxis = target
1480
+ .localToWorld(originLocal.clone().add(localAxis))
1481
+ .sub(worldOrigin)
1482
+ .normalize();
1483
+ const initialVector = event.point
1484
+ .clone()
1485
+ .sub(worldOrigin)
1486
+ .projectOnPlane(worldAxis)
1487
+ .normalize();
1488
+ if (initialVector.lengthSq() < 1e-6)
1489
+ return;
1490
+ const rotationPlane = new Plane().setFromNormalAndCoplanarPoint(worldAxis, worldOrigin);
1491
+ const baseTopology = displayTopology;
1492
+ const baseSelection = selection;
1493
+ const restoreInputDragging = interactionApi.beginInputDrag();
1494
+ const previousCursor = document.body.style.cursor;
1495
+ let previousWrappedAngle = 0;
1496
+ let accumulatedAngle = 0;
1497
+ let latestAngle = 0;
1498
+ let lastSnapAngle = null;
1499
+ let latestTopology = null;
1500
+ let finished = false;
1501
+ useInteractionScope.getState().begin(meshEditScope(node.id, 'operating', 'rotate'));
1502
+ playBlockSfx('drag-start');
1503
+ setActiveTransform({ operation: 'rotate', constraint: axis });
1504
+ setTransformNumericInput('0');
1505
+ setModalFeedbackMode('free');
1506
+ document.body.style.cursor = 'grabbing';
1507
+ const onMove = (pointerEvent) => {
1508
+ const hit = makeRay(pointerEvent.clientX, pointerEvent.clientY).intersectPlane(rotationPlane, new Vector3());
1509
+ if (!hit)
1510
+ return;
1511
+ const currentVector = hit.sub(worldOrigin).projectOnPlane(worldAxis);
1512
+ if (currentVector.lengthSq() < 1e-6)
1513
+ return;
1514
+ currentVector.normalize();
1515
+ const wrappedAngle = signedAngleAroundAxis(initialVector, currentVector, worldAxis);
1516
+ accumulatedAngle += unwrapRotationDelta(previousWrappedAngle, wrappedAngle);
1517
+ previousWrappedAngle = wrappedAngle;
1518
+ let angle = accumulatedAngle;
1519
+ const snapping = !pointerEvent.altKey && isAngleSnapActive();
1520
+ if (snapping) {
1521
+ const step = (ROTATION_SNAP_ANGLE_DEGREES * Math.PI) / 180;
1522
+ angle = Math.round(angle / step) * step;
1523
+ }
1524
+ if (snapping && Math.abs(angle) > 1e-6 && angle !== lastSnapAngle) {
1525
+ lastSnapAngle = angle;
1526
+ playBlockSfx('rotate-step');
1527
+ }
1528
+ else if (!snapping) {
1529
+ lastSnapAngle = null;
1530
+ }
1531
+ setTransformNumericInput(blockTransformDisplayValue('rotate', angle));
1532
+ setModalFeedbackMode(snapping ? 'angle' : 'free');
1533
+ const result = applyBlockCommand(baseTopology, {
1534
+ type: 'rotate-components',
1535
+ selection: baseSelection,
1536
+ pivot: origin,
1537
+ axis: AXIS_VECTORS[axis],
1538
+ angle,
1539
+ });
1540
+ if (!result.ok) {
1541
+ setError(result.error);
1542
+ return;
1543
+ }
1544
+ latestAngle = angle;
1545
+ latestTopology = result.topology;
1546
+ setPreviewTopology(result.topology);
1547
+ useLiveNodeOverrides.getState().set(node.id, { topology: result.topology });
1548
+ sceneApi.markDirty(node.id);
1549
+ };
1550
+ const finish = (commit) => {
1551
+ if (finished)
1552
+ return;
1553
+ finished = true;
1554
+ window.removeEventListener('pointermove', onMove);
1555
+ window.removeEventListener('pointerup', onPointerUp);
1556
+ window.removeEventListener('pointercancel', onPointerCancel);
1557
+ window.removeEventListener('blur', onPointerCancel);
1558
+ cancelDragRef.current = null;
1559
+ useLiveNodeOverrides.getState().clear(node.id);
1560
+ sceneApi.markDirty(node.id);
1561
+ restoreInputDragging();
1562
+ document.body.style.cursor = previousCursor;
1563
+ setPreviewTopology(null);
1564
+ setActiveTransform(null);
1565
+ setTransformNumericInput('');
1566
+ setModalFeedbackMode('free');
1567
+ if (commit && latestTopology && Math.abs(latestAngle) > 1e-6) {
1568
+ commitAdjustableOperation(baseTopology, {
1569
+ type: 'rotate-components',
1570
+ selection: baseSelection,
1571
+ pivot: origin,
1572
+ axis: AXIS_VECTORS[axis],
1573
+ angle: latestAngle,
1574
+ }, 'Rotate');
1575
+ playBlockSfx('finish');
1576
+ }
1577
+ else if (!commit) {
1578
+ playBlockSfx('cancel');
1579
+ }
1580
+ if (ownsEditSession()) {
1581
+ useInteractionScope.getState().begin(meshEditScope(node.id));
1582
+ }
1583
+ swallowNextClick();
1584
+ };
1585
+ const onPointerUp = () => finish(true);
1586
+ const onPointerCancel = () => finish(false);
1587
+ cancelDragRef.current = onPointerCancel;
1588
+ window.addEventListener('pointermove', onMove);
1589
+ window.addEventListener('pointerup', onPointerUp, { once: true });
1590
+ window.addEventListener('pointercancel', onPointerCancel, { once: true });
1591
+ window.addEventListener('blur', onPointerCancel, { once: true });
1592
+ }, [
1593
+ commitAdjustableOperation,
1594
+ displayTopology,
1595
+ interactionApi.beginInputDrag,
1596
+ makeRay,
1597
+ node.id,
1598
+ ownsEditSession,
1599
+ selectedIds.length,
1600
+ selection,
1601
+ target,
1602
+ sceneApi.markDirty,
1603
+ ]);
1604
+ const beginScaleDrag = useCallback((axis, event) => {
1605
+ if (!ownsEditSession() || selectedIds.length === 0 || cancelDragRef.current)
1606
+ return;
1607
+ const origin = selectionCentroid(displayTopology, selection);
1608
+ if (!origin)
1609
+ return;
1610
+ target.updateWorldMatrix(true, false);
1611
+ const originLocal = new Vector3(...origin);
1612
+ const worldOrigin = target.localToWorld(originLocal.clone());
1613
+ const localAxis = new Vector3(...AXIS_VECTORS[axis]);
1614
+ const worldAxis = target
1615
+ .localToWorld(originLocal.clone().add(localAxis))
1616
+ .sub(worldOrigin)
1617
+ .normalize();
1618
+ const initialParameter = closestAxisParameterToRay(worldOrigin, worldAxis, event.ray);
1619
+ const axisIndex = axis === 'x' ? 0 : axis === 'y' ? 1 : 2;
1620
+ const baseTopology = displayTopology;
1621
+ const baseSelection = selection;
1622
+ const restoreInputDragging = interactionApi.beginInputDrag();
1623
+ const previousCursor = document.body.style.cursor;
1624
+ let latestFactor = 1;
1625
+ let lastSnapFactor = null;
1626
+ let latestTopology = null;
1627
+ let finished = false;
1628
+ useInteractionScope.getState().begin(meshEditScope(node.id, 'operating', 'scale'));
1629
+ playBlockSfx('drag-start');
1630
+ setActiveTransform({ operation: 'scale', constraint: axis });
1631
+ setTransformNumericInput('1');
1632
+ setModalFeedbackMode('free');
1633
+ document.body.style.cursor = 'grabbing';
1634
+ const onMove = (pointerEvent) => {
1635
+ const parameter = closestAxisParameterToRay(worldOrigin, worldAxis, makeRay(pointerEvent.clientX, pointerEvent.clientY));
1636
+ const worldPoint = worldOrigin
1637
+ .clone()
1638
+ .addScaledVector(worldAxis, parameter - initialParameter);
1639
+ const localPoint = target.worldToLocal(worldPoint);
1640
+ const distance = localPoint.getComponent(axisIndex) - originLocal.getComponent(axisIndex);
1641
+ const snapStep = !pointerEvent.altKey && isGridSnapActive() ? useEditor.getState().gridSnapStep : 0;
1642
+ const factor = blockScaleFactorFromDrag(distance, gizmoLength, snapStep);
1643
+ setTransformNumericInput(blockTransformDisplayValue('scale', factor));
1644
+ setModalFeedbackMode(snapStep > 0 ? 'grid' : 'free');
1645
+ if (snapStep > 0 && Math.abs(factor - 1) > 1e-6 && factor !== lastSnapFactor) {
1646
+ lastSnapFactor = factor;
1647
+ playBlockSfx('resize-step');
1648
+ }
1649
+ else if (snapStep === 0) {
1650
+ lastSnapFactor = null;
1651
+ }
1652
+ const result = applyBlockCommand(baseTopology, {
1653
+ type: 'scale-components',
1654
+ selection: baseSelection,
1655
+ pivot: origin,
1656
+ factors: blockScaleFactors(axis, factor),
1657
+ });
1658
+ if (!result.ok) {
1659
+ setError(result.error);
1660
+ return;
1661
+ }
1662
+ latestFactor = factor;
1663
+ latestTopology = result.topology;
1664
+ setPreviewTopology(result.topology);
1665
+ useLiveNodeOverrides.getState().set(node.id, { topology: result.topology });
1666
+ sceneApi.markDirty(node.id);
1667
+ setError(null);
1668
+ };
1669
+ const finish = (commit) => {
1670
+ if (finished)
1671
+ return;
1672
+ finished = true;
1673
+ window.removeEventListener('pointermove', onMove);
1674
+ window.removeEventListener('pointerup', onPointerUp);
1675
+ window.removeEventListener('pointercancel', onPointerCancel);
1676
+ window.removeEventListener('blur', onPointerCancel);
1677
+ cancelDragRef.current = null;
1678
+ useLiveNodeOverrides.getState().clear(node.id);
1679
+ sceneApi.markDirty(node.id);
1680
+ restoreInputDragging();
1681
+ document.body.style.cursor = previousCursor;
1682
+ setPreviewTopology(null);
1683
+ setActiveTransform(null);
1684
+ setTransformNumericInput('');
1685
+ setModalFeedbackMode('free');
1686
+ if (commit && latestTopology && Math.abs(latestFactor - 1) > 1e-6) {
1687
+ commitAdjustableOperation(baseTopology, {
1688
+ type: 'scale-components',
1689
+ selection: baseSelection,
1690
+ pivot: origin,
1691
+ factors: blockScaleFactors(axis, latestFactor),
1692
+ }, 'Scale');
1693
+ playBlockSfx('finish');
1694
+ }
1695
+ else if (!commit) {
1696
+ playBlockSfx('cancel');
1697
+ }
1698
+ if (ownsEditSession()) {
1699
+ useInteractionScope.getState().begin(meshEditScope(node.id));
1700
+ }
1701
+ swallowNextClick();
1702
+ };
1703
+ const onPointerUp = () => finish(true);
1704
+ const onPointerCancel = () => finish(false);
1705
+ cancelDragRef.current = onPointerCancel;
1706
+ window.addEventListener('pointermove', onMove);
1707
+ window.addEventListener('pointerup', onPointerUp, { once: true });
1708
+ window.addEventListener('pointercancel', onPointerCancel, { once: true });
1709
+ window.addEventListener('blur', onPointerCancel, { once: true });
1710
+ }, [
1711
+ displayTopology,
1712
+ commitAdjustableOperation,
1713
+ gizmoLength,
1714
+ interactionApi.beginInputDrag,
1715
+ makeRay,
1716
+ node.id,
1717
+ ownsEditSession,
1718
+ selectedIds.length,
1719
+ selection,
1720
+ target,
1721
+ sceneApi.markDirty,
1722
+ ]);
1723
+ const beginUniformScaleModal = useCallback(() => {
1724
+ if (!ownsEditSession() || selectedIds.length === 0 || cancelDragRef.current)
1725
+ return false;
1726
+ const origin = selectionCentroid(displayTopology, selection);
1727
+ if (!origin)
1728
+ return false;
1729
+ const pivotClient = localPointToClient(origin, target, camera, gl.domElement);
1730
+ if (!pivotClient)
1731
+ return false;
1732
+ const fallbackDistance = Math.max(80, gizmoLength * 96);
1733
+ const startPointer = lastPointerClientRef.current?.clone() ??
1734
+ pivotClient.clone().add(new Vector2(fallbackDistance, 0));
1735
+ const initialDistance = Math.max(24, pivotClient.distanceTo(startPointer));
1736
+ const baseTopology = displayTopology;
1737
+ const baseSelection = selection;
1738
+ let latestFactor = 1;
1739
+ let lastSnapFactor = null;
1740
+ let latestTopology = null;
1741
+ let activeConstraint = 'uniform';
1742
+ let typedInput = '';
1743
+ let lastClientX = startPointer.x;
1744
+ let lastClientY = startPointer.y;
1745
+ let lastAltKey = false;
1746
+ const updatePreview = (clientX, clientY, altKey) => {
1747
+ lastClientX = clientX;
1748
+ lastClientY = clientY;
1749
+ lastAltKey = altKey;
1750
+ const pointer = new Vector2(clientX, clientY);
1751
+ const distance = pointer.distanceTo(pivotClient) - initialDistance;
1752
+ const numericValue = blockTransformNumericValue(typedInput, 'scale');
1753
+ const snapStep = numericValue === null && !altKey && isGridSnapActive()
1754
+ ? useEditor.getState().gridSnapStep
1755
+ : 0;
1756
+ const factor = numericValue ?? blockScaleFactorFromDrag(distance, initialDistance, snapStep);
1757
+ if (snapStep > 0 && Math.abs(factor - 1) > 1e-6 && factor !== lastSnapFactor) {
1758
+ lastSnapFactor = factor;
1759
+ playBlockSfx('resize-step');
1760
+ }
1761
+ else if (snapStep === 0) {
1762
+ lastSnapFactor = null;
1763
+ }
1764
+ const result = applyBlockCommand(baseTopology, {
1765
+ type: 'scale-components',
1766
+ selection: baseSelection,
1767
+ pivot: origin,
1768
+ factors: blockScaleFactorsForConstraint(activeConstraint, factor),
1769
+ });
1770
+ if (!result.ok) {
1771
+ setError(result.error);
1772
+ return;
1773
+ }
1774
+ latestFactor = factor;
1775
+ setTransformNumericInput(typedInput || blockTransformDisplayValue('scale', factor));
1776
+ setModalFeedbackMode(typedInput ? 'exact' : snapStep > 0 ? 'grid' : 'free');
1777
+ latestTopology = result.topology;
1778
+ setPreviewTopology(result.topology);
1779
+ useLiveNodeOverrides.getState().set(node.id, { topology: result.topology });
1780
+ sceneApi.markDirty(node.id);
1781
+ setError(null);
1782
+ };
1783
+ const complete = (commit) => {
1784
+ useLiveNodeOverrides.getState().clear(node.id);
1785
+ sceneApi.markDirty(node.id);
1786
+ setPreviewTopology(null);
1787
+ setActiveTransform(null);
1788
+ setTransformNumericInput('');
1789
+ setModalFeedbackMode('free');
1790
+ if (commit && latestTopology && Math.abs(latestFactor - 1) > 1e-6) {
1791
+ commitAdjustableOperation(baseTopology, {
1792
+ type: 'scale-components',
1793
+ selection: baseSelection,
1794
+ pivot: origin,
1795
+ factors: blockScaleFactorsForConstraint(activeConstraint, latestFactor),
1796
+ }, 'Scale');
1797
+ playBlockSfx('finish');
1798
+ }
1799
+ else if (!commit) {
1800
+ playBlockSfx('cancel');
1801
+ }
1802
+ if (ownsEditSession()) {
1803
+ useInteractionScope.getState().begin(meshEditScope(node.id));
1804
+ }
1805
+ swallowNextClick();
1806
+ };
1807
+ const onMove = (pointerEvent) => {
1808
+ lastPointerClientRef.current = new Vector2(pointerEvent.clientX, pointerEvent.clientY);
1809
+ updatePreview(pointerEvent.clientX, pointerEvent.clientY, pointerEvent.altKey);
1810
+ };
1811
+ const onPointerDown = (pointerEvent, finish) => {
1812
+ pointerEvent.preventDefault();
1813
+ pointerEvent.stopImmediatePropagation();
1814
+ finish(pointerEvent.button !== 2);
1815
+ };
1816
+ const onKeyDown = (keyboardEvent, finish) => {
1817
+ const element = keyboardEvent.target;
1818
+ if (element?.tagName === 'INPUT' ||
1819
+ element?.tagName === 'TEXTAREA' ||
1820
+ element?.isContentEditable)
1821
+ return;
1822
+ const nextInput = blockTransformNumericInputFromKey(typedInput, keyboardEvent.key);
1823
+ const constraint = blockTransformConstraintFromKey(keyboardEvent.key, keyboardEvent.shiftKey);
1824
+ if (constraint) {
1825
+ keyboardEvent.preventDefault();
1826
+ keyboardEvent.stopImmediatePropagation();
1827
+ activeConstraint = constraint;
1828
+ setActiveTransform({ operation: 'scale', constraint });
1829
+ lastSnapFactor = null;
1830
+ updatePreview(lastClientX, lastClientY, lastAltKey);
1831
+ }
1832
+ else if (nextInput !== null) {
1833
+ keyboardEvent.preventDefault();
1834
+ keyboardEvent.stopImmediatePropagation();
1835
+ typedInput = nextInput;
1836
+ setTransformNumericInput(nextInput);
1837
+ lastSnapFactor = null;
1838
+ updatePreview(lastClientX, lastClientY, lastAltKey);
1839
+ }
1840
+ else if (keyboardEvent.key === 'Enter') {
1841
+ keyboardEvent.preventDefault();
1842
+ keyboardEvent.stopImmediatePropagation();
1843
+ finish(true);
1844
+ }
1845
+ else if (keyboardEvent.key === 'Escape') {
1846
+ keyboardEvent.preventDefault();
1847
+ keyboardEvent.stopImmediatePropagation();
1848
+ finish(false);
1849
+ }
1850
+ };
1851
+ useInteractionScope.getState().begin(meshEditScope(node.id, 'operating', 'scale'));
1852
+ playBlockSfx('drag-start');
1853
+ setTransformTool('transform');
1854
+ setToolbarPanel(null);
1855
+ setActiveTransform({ operation: 'scale', constraint: 'uniform' });
1856
+ setTransformNumericInput('');
1857
+ setModalFeedbackMode('free');
1858
+ setError(null);
1859
+ beginBlockModalSession({
1860
+ beginInputDrag: interactionApi.beginInputDrag,
1861
+ cancelRef: cancelDragRef,
1862
+ cursor: 'nwse-resize',
1863
+ onFinish: complete,
1864
+ onKeyDown,
1865
+ onPointerDown,
1866
+ onPointerMove: onMove,
1867
+ });
1868
+ return true;
1869
+ }, [
1870
+ camera,
1871
+ commitAdjustableOperation,
1872
+ displayTopology,
1873
+ gizmoLength,
1874
+ gl.domElement,
1875
+ interactionApi.beginInputDrag,
1876
+ node.id,
1877
+ ownsEditSession,
1878
+ selectedIds.length,
1879
+ selection,
1880
+ target,
1881
+ sceneApi.markDirty,
1882
+ ]);
1883
+ const beginBevelDrag = useCallback((edgeId, event) => {
1884
+ if (event.nativeEvent.button !== 0 || !ownsEditSession() || cancelDragRef.current)
1885
+ return;
1886
+ if (!displayTopology.edges.some((edge) => edge.id === edgeId))
1887
+ return;
1888
+ const edgeIds = mode === 'edge' && selectedIds.includes(edgeId) ? [...selectedIds] : [edgeId];
1889
+ const baseTopology = displayTopology;
1890
+ const projectedExtentPixels = blockTopologyClientExtent(baseTopology, target, camera, gl.domElement);
1891
+ if (!projectedExtentPixels)
1892
+ return;
1893
+ const startClientX = event.nativeEvent.clientX;
1894
+ const startClientY = event.nativeEvent.clientY;
1895
+ const restoreInputDragging = interactionApi.beginInputDrag();
1896
+ const previousCursor = document.body.style.cursor;
1897
+ let activeSegments = bevelSegments;
1898
+ let latestWidth = 0;
1899
+ let lastWidthStep = 0;
1900
+ let latestTopology = null;
1901
+ let latestSelection = null;
1902
+ let finished = false;
1903
+ useBlockEditSession.getState().setSelection(node.id, {
1904
+ mode: 'edge',
1905
+ ids: edgeIds,
1906
+ activeId: edgeId,
1907
+ });
1908
+ setToolbarPanel(null);
1909
+ setError(null);
1910
+ setBevelWidth(0);
1911
+ useInteractionScope.getState().begin(meshEditScope(node.id, 'operating', 'bevel'));
1912
+ playBlockSfx('operation-start');
1913
+ document.body.style.cursor = 'ew-resize';
1914
+ const updatePreview = (width, segments = activeSegments) => {
1915
+ if (width <= 1e-6)
1916
+ return false;
1917
+ const result = applyBlockCommand(baseTopology, {
1918
+ type: 'bevel-edges',
1919
+ edgeIds,
1920
+ width,
1921
+ segments,
1922
+ profile: 0.5,
1923
+ clampOverlap: true,
1924
+ });
1925
+ if (!result.ok) {
1926
+ setError(result.error);
1927
+ return false;
1928
+ }
1929
+ activeSegments = segments;
1930
+ latestWidth = width;
1931
+ setBevelWidth(width);
1932
+ latestTopology = result.topology;
1933
+ latestSelection = result.selection;
1934
+ setPreviewTopology(result.topology);
1935
+ useLiveNodeOverrides.getState().set(node.id, { topology: result.topology });
1936
+ sceneApi.markDirty(node.id);
1937
+ setError(null);
1938
+ return true;
1939
+ };
1940
+ const onMove = (pointerEvent) => {
1941
+ const deltaX = pointerEvent.clientX - startClientX;
1942
+ const deltaY = pointerEvent.clientY - startClientY;
1943
+ if (Math.hypot(deltaX, deltaY) < 2)
1944
+ return;
1945
+ const width = blockBevelWidthFromDrag(deltaX, deltaY, {
1946
+ topologyExtent: extent,
1947
+ projectedExtentPixels,
1948
+ });
1949
+ const widthStep = Math.floor(width / Math.max(0.01, extent * 0.025));
1950
+ if (widthStep > 0 && widthStep !== lastWidthStep) {
1951
+ lastWidthStep = widthStep;
1952
+ playBlockSfx('resize-step');
1953
+ }
1954
+ updatePreview(width, activeSegments);
1955
+ };
1956
+ const onWheel = (wheelEvent) => {
1957
+ const direction = consumeBlockGestureWheel(wheelEvent);
1958
+ if (direction === 0)
1959
+ return;
1960
+ const segments = Math.min(12, Math.max(1, activeSegments + direction));
1961
+ if (segments === activeSegments)
1962
+ return;
1963
+ activeSegments = segments;
1964
+ setBevelSegments(segments);
1965
+ playBlockSfx('resize-step');
1966
+ if (latestWidth > 0)
1967
+ updatePreview(latestWidth, segments);
1968
+ };
1969
+ const finish = (commit) => {
1970
+ if (finished)
1971
+ return;
1972
+ finished = true;
1973
+ window.removeEventListener('pointermove', onMove);
1974
+ window.removeEventListener('pointerup', onPointerUp);
1975
+ window.removeEventListener('wheel', onWheel, BLOCK_WHEEL_OPTIONS);
1976
+ window.removeEventListener('pointercancel', onPointerCancel);
1977
+ window.removeEventListener('blur', onPointerCancel);
1978
+ cancelDragRef.current = null;
1979
+ useLiveNodeOverrides.getState().clear(node.id);
1980
+ sceneApi.markDirty(node.id);
1981
+ restoreInputDragging();
1982
+ document.body.style.cursor = previousCursor;
1983
+ setPreviewTopology(null);
1984
+ if (commit && latestTopology && latestSelection && latestWidth > 1e-6) {
1985
+ commitAdjustableOperation(baseTopology, {
1986
+ type: 'bevel-edges',
1987
+ edgeIds,
1988
+ width: latestWidth,
1989
+ segments: activeSegments,
1990
+ profile: 0.5,
1991
+ clampOverlap: true,
1992
+ }, 'Bevel');
1993
+ playBlockSfx('operation-commit');
1994
+ }
1995
+ else if (!commit) {
1996
+ playBlockSfx('cancel');
1997
+ }
1998
+ if (ownsEditSession()) {
1999
+ useInteractionScope.getState().begin(meshEditScope(node.id));
2000
+ }
2001
+ swallowNextClick();
2002
+ };
2003
+ const onPointerUp = () => finish(true);
2004
+ const onPointerCancel = () => finish(false);
2005
+ cancelDragRef.current = onPointerCancel;
2006
+ window.addEventListener('pointermove', onMove);
2007
+ window.addEventListener('pointerup', onPointerUp, { once: true });
2008
+ window.addEventListener('wheel', onWheel, BLOCK_WHEEL_OPTIONS);
2009
+ window.addEventListener('pointercancel', onPointerCancel, { once: true });
2010
+ window.addEventListener('blur', onPointerCancel, { once: true });
2011
+ }, [
2012
+ bevelSegments,
2013
+ camera,
2014
+ commitAdjustableOperation,
2015
+ displayTopology,
2016
+ extent,
2017
+ gl.domElement,
2018
+ interactionApi.beginInputDrag,
2019
+ mode,
2020
+ node.id,
2021
+ ownsEditSession,
2022
+ selectedIds,
2023
+ target,
2024
+ sceneApi.markDirty,
2025
+ ]);
2026
+ const previewLoopCut = useCallback((edgeId) => {
2027
+ if (cancelDragRef.current)
2028
+ return;
2029
+ setLoopCutEdgeId(edgeId);
2030
+ }, []);
2031
+ useEffect(() => {
2032
+ if (!(editing && transformTool === 'loop-cut') || loopCutSliding)
2033
+ return;
2034
+ if (!loopCutEdgeId) {
2035
+ setLoopCutSegments(null);
2036
+ setError(null);
2037
+ return;
2038
+ }
2039
+ const segments = blockLoopCutSegments(node.topology, loopCutEdgeId, 0.5, loopCutCount);
2040
+ setLoopCutSegments(segments);
2041
+ setLoopCutFactor(0.5);
2042
+ setError(segments ? null : 'Loop cut requires a connected ring of quad faces');
2043
+ }, [editing, loopCutCount, loopCutEdgeId, loopCutSliding, node.topology, transformTool]);
2044
+ useEffect(() => {
2045
+ if (transformTool === 'loop-cut' || loopCutSliding)
2046
+ return;
2047
+ setLoopCutEdgeId(null);
2048
+ setLoopCutSegments(null);
2049
+ setLoopCutFactor(0.5);
2050
+ }, [loopCutSliding, transformTool]);
2051
+ useEffect(() => {
2052
+ if (!(editing && transformTool === 'loop-cut' && !loopCutSliding))
2053
+ return;
2054
+ const onWheel = (event) => {
2055
+ const direction = consumeBlockGestureWheel(event);
2056
+ if (direction === 0)
2057
+ return;
2058
+ setLoopCutCount((current) => {
2059
+ const next = Math.min(32, Math.max(1, current + direction));
2060
+ if (next !== current)
2061
+ playBlockSfx('resize-step');
2062
+ return next;
2063
+ });
2064
+ };
2065
+ const onPointerDown = (event) => {
2066
+ if (resolveLoopCutPointerAction('choosing-ring', event.button) !== 'cancel')
2067
+ return;
2068
+ event.preventDefault();
2069
+ event.stopImmediatePropagation();
2070
+ setTransformTool('transform');
2071
+ setLoopCutEdgeId(null);
2072
+ setLoopCutSegments(null);
2073
+ setError(null);
2074
+ playBlockSfx('cancel');
2075
+ swallowNextClick();
2076
+ };
2077
+ window.addEventListener('wheel', onWheel, BLOCK_WHEEL_OPTIONS);
2078
+ window.addEventListener('pointerdown', onPointerDown, true);
2079
+ return () => {
2080
+ window.removeEventListener('wheel', onWheel, BLOCK_WHEEL_OPTIONS);
2081
+ window.removeEventListener('pointerdown', onPointerDown, true);
2082
+ };
2083
+ }, [editing, loopCutSliding, transformTool]);
2084
+ const beginLoopCutSlide = useCallback((edgeId, event) => {
2085
+ if (resolveLoopCutPointerAction('choosing-ring', event.nativeEvent.button) !== 'begin-slide' ||
2086
+ !ownsEditSession() ||
2087
+ cancelDragRef.current)
2088
+ return;
2089
+ const edge = node.topology.edges.find((entry) => entry.id === edgeId);
2090
+ const vertices = topologyVertexMap(node.topology);
2091
+ const start = edge ? vertices.get(edge.vertexIds[0]) : null;
2092
+ const end = edge ? vertices.get(edge.vertexIds[1]) : null;
2093
+ if (!(edge && start && end))
2094
+ return;
2095
+ target.updateWorldMatrix(true, false);
2096
+ const worldStart = target.localToWorld(new Vector3(...start));
2097
+ const worldEnd = target.localToWorld(new Vector3(...end));
2098
+ const worldDirection = worldEnd.clone().sub(worldStart);
2099
+ const worldLength = worldDirection.length();
2100
+ if (worldLength < 1e-6)
2101
+ return;
2102
+ const worldAxis = worldDirection.normalize();
2103
+ const initialParameter = closestAxisParameterToRay(worldStart, worldAxis, event.ray);
2104
+ const baseTopology = node.topology;
2105
+ const restoreInputDragging = interactionApi.beginInputDrag();
2106
+ const previousCursor = document.body.style.cursor;
2107
+ let latestTopology = null;
2108
+ let latestSelection = null;
2109
+ let latestFactor = 0.5;
2110
+ const activeCuts = loopCutCount;
2111
+ let lastSnapFactor = null;
2112
+ let finished = false;
2113
+ let confirmationAttached = false;
2114
+ const updatePreview = (factor) => {
2115
+ const effectiveFactor = resolveLoopCutSlideFactor(activeCuts, factor);
2116
+ const result = applyBlockCommand(baseTopology, {
2117
+ type: 'loop-cut',
2118
+ edgeId,
2119
+ factor: effectiveFactor,
2120
+ cuts: activeCuts,
2121
+ });
2122
+ const segments = blockLoopCutSegments(baseTopology, edgeId, effectiveFactor, activeCuts);
2123
+ if (!result.ok || !segments) {
2124
+ setError(result.ok ? 'Could not preview loop cut' : result.error);
2125
+ return false;
2126
+ }
2127
+ latestFactor = effectiveFactor;
2128
+ latestTopology = result.topology;
2129
+ latestSelection = result.selection;
2130
+ setPreviewTopology(result.topology);
2131
+ setLoopCutSegments(segments);
2132
+ setLoopCutFactor(effectiveFactor);
2133
+ useLiveNodeOverrides.getState().set(node.id, { topology: result.topology });
2134
+ sceneApi.markDirty(node.id);
2135
+ setError(null);
2136
+ return true;
2137
+ };
2138
+ if (!updatePreview(0.5))
2139
+ return;
2140
+ useInteractionScope.getState().begin(meshEditScope(node.id, 'operating', 'loop-cut'));
2141
+ playBlockSfx('operation-start');
2142
+ setLoopCutSliding(true);
2143
+ document.body.style.cursor = 'ew-resize';
2144
+ const onMove = (pointerEvent) => {
2145
+ if (activeCuts > 1)
2146
+ return;
2147
+ const parameter = closestAxisParameterToRay(worldStart, worldAxis, makeRay(pointerEvent.clientX, pointerEvent.clientY));
2148
+ let factor = Math.min(0.98, Math.max(0.02, 0.5 + (parameter - initialParameter) / worldLength));
2149
+ const snapping = isGridSnapActive() && !pointerEvent.altKey;
2150
+ if (snapping) {
2151
+ const step = useEditor.getState().gridSnapStep;
2152
+ if (step > 0)
2153
+ factor = Math.min(0.98, Math.max(0.02, (Math.round((factor * worldLength) / step) * step) / worldLength));
2154
+ }
2155
+ if (snapping && factor !== lastSnapFactor) {
2156
+ lastSnapFactor = factor;
2157
+ playBlockSfx('move-step');
2158
+ }
2159
+ else if (!snapping) {
2160
+ lastSnapFactor = null;
2161
+ }
2162
+ updatePreview(factor);
2163
+ };
2164
+ const finish = (outcome) => {
2165
+ if (finished)
2166
+ return;
2167
+ if (outcome === 'commit-centered' && !updatePreview(0.5))
2168
+ outcome = 'cancel';
2169
+ finished = true;
2170
+ window.removeEventListener('pointermove', onMove);
2171
+ if (confirmationAttached)
2172
+ window.removeEventListener('pointerdown', onConfirm, true);
2173
+ window.removeEventListener('contextmenu', onContextMenu, true);
2174
+ window.removeEventListener('pointercancel', onPointerCancel);
2175
+ window.removeEventListener('blur', onPointerCancel);
2176
+ cancelDragRef.current = null;
2177
+ useLiveNodeOverrides.getState().clear(node.id);
2178
+ sceneApi.markDirty(node.id);
2179
+ restoreInputDragging();
2180
+ document.body.style.cursor = previousCursor;
2181
+ setPreviewTopology(null);
2182
+ setLoopCutSegments(null);
2183
+ setLoopCutEdgeId(null);
2184
+ setLoopCutSliding(false);
2185
+ if (outcome !== 'cancel' && latestTopology && latestSelection && latestFactor > 0) {
2186
+ commitAdjustableOperation(baseTopology, {
2187
+ type: 'loop-cut',
2188
+ edgeId,
2189
+ factor: latestFactor,
2190
+ cuts: activeCuts,
2191
+ }, 'Loop Cut');
2192
+ playBlockSfx('operation-commit');
2193
+ }
2194
+ else if (outcome === 'cancel') {
2195
+ playBlockSfx('cancel');
2196
+ }
2197
+ if (ownsEditSession()) {
2198
+ useInteractionScope.getState().begin(meshEditScope(node.id));
2199
+ }
2200
+ swallowNextClick();
2201
+ };
2202
+ const onConfirm = (pointerEvent) => {
2203
+ const action = resolveLoopCutPointerAction('sliding', pointerEvent.button);
2204
+ if (action !== 'commit-current' && action !== 'commit-centered')
2205
+ return;
2206
+ pointerEvent.preventDefault();
2207
+ pointerEvent.stopImmediatePropagation();
2208
+ finish(action);
2209
+ };
2210
+ const onContextMenu = (contextEvent) => {
2211
+ contextEvent.preventDefault();
2212
+ contextEvent.stopImmediatePropagation();
2213
+ };
2214
+ const onPointerCancel = () => finish('cancel');
2215
+ cancelDragRef.current = onPointerCancel;
2216
+ window.addEventListener('pointermove', onMove);
2217
+ window.addEventListener('contextmenu', onContextMenu, true);
2218
+ window.addEventListener('pointercancel', onPointerCancel, { once: true });
2219
+ window.addEventListener('blur', onPointerCancel, { once: true });
2220
+ queueMicrotask(() => {
2221
+ if (finished)
2222
+ return;
2223
+ confirmationAttached = true;
2224
+ window.addEventListener('pointerdown', onConfirm, true);
2225
+ });
2226
+ }, [
2227
+ commitAdjustableOperation,
2228
+ interactionApi.beginInputDrag,
2229
+ loopCutCount,
2230
+ makeRay,
2231
+ node.id,
2232
+ node.topology,
2233
+ ownsEditSession,
2234
+ target,
2235
+ sceneApi.markDirty,
2236
+ ]);
2237
+ const beginFaceOperationModal = useBlockFaceOperation({
2238
+ camera,
2239
+ cancelRef: cancelDragRef,
2240
+ canvas: gl.domElement,
2241
+ closeToolbar: () => setToolbarPanel(null),
2242
+ commit: commitAdjustableOperation,
2243
+ displayTopology,
2244
+ extent,
2245
+ lastPointerClientRef,
2246
+ mode,
2247
+ nodeId: node.id,
2248
+ ownsEditSession,
2249
+ playSfx: playBlockSfx,
2250
+ beginInputDrag: interactionApi.beginInputDrag,
2251
+ sceneApi,
2252
+ selectedIds,
2253
+ selection,
2254
+ setActiveFaceOperation,
2255
+ setError,
2256
+ setFaceOperationAxis,
2257
+ setFaceOperationValue,
2258
+ setModalFeedbackMode,
2259
+ setPreviewTopology,
2260
+ setTransformNumericInput,
2261
+ target,
2262
+ });
2263
+ const commitCommand = (command, operator) => {
2264
+ if (cancelDragRef.current)
2265
+ return;
2266
+ useInteractionScope.getState().begin(meshEditScope(node.id, 'operating', operator));
2267
+ const result = applyBlockCommand(node.topology, command);
2268
+ if (!result.ok) {
2269
+ useInteractionScope.getState().begin(meshEditScope(node.id));
2270
+ setError(result.error);
2271
+ return;
2272
+ }
2273
+ sceneApi.update(node.id, { topology: result.topology });
2274
+ const session = useBlockEditSession.getState();
2275
+ session.setSelection(node.id, {
2276
+ ...result.selection,
2277
+ activeId: result.selection.ids.at(-1) ?? null,
2278
+ });
2279
+ session.setLastOperation(node.id, null);
2280
+ setLastOperationPanelOpen(false);
2281
+ setToolbarPanel(null);
2282
+ setError(null);
2283
+ if (ownsEditSession())
2284
+ useInteractionScope.getState().begin(meshEditScope(node.id));
2285
+ playBlockSfx(operator === 'delete' ? 'delete' : 'operation-commit');
2286
+ };
2287
+ const extrudeSelectedFace = () => beginFaceOperationModal('extrude');
2288
+ const insetSelectedFace = () => beginFaceOperationModal('inset');
2289
+ const deleteSelection = () => {
2290
+ if (selectedIds.length === 0)
2291
+ return;
2292
+ commitCommand({ type: 'delete-components', selection }, 'delete');
2293
+ };
2294
+ const mergeSelection = () => {
2295
+ if (mode !== 'vertex' || selectedIds.length < 2)
2296
+ return;
2297
+ commitCommand({ type: 'merge-vertices', vertexIds: selectedIds }, 'merge');
2298
+ };
2299
+ const dissolveSelection = () => {
2300
+ if (mode === 'edge' && selectedIds.length > 0) {
2301
+ commitCommand({ type: 'dissolve-edges', edgeIds: selectedIds }, 'dissolve');
2302
+ }
2303
+ else if (mode === 'face' && selectedIds.length > 1) {
2304
+ commitCommand({ type: 'dissolve-faces', faceIds: selectedIds }, 'dissolve');
2305
+ }
2306
+ };
2307
+ const adjustLastOperation = (command) => {
2308
+ if (!lastOperation || cancelDragRef.current)
2309
+ return;
2310
+ const replacement = replaceCommittedBlockOperation(operationServices, lastOperation, command);
2311
+ if (!replacement.ok) {
2312
+ setError(replacement.error);
2313
+ setLastOperationPanelOpen(false);
2314
+ return;
2315
+ }
2316
+ const session = useBlockEditSession.getState();
2317
+ session.setLastOperation(node.id, replacement.operation);
2318
+ session.setSelection(node.id, {
2319
+ ...replacement.operation.resultSelection,
2320
+ activeId: replacement.operation.resultSelection.ids.at(-1) ?? null,
2321
+ });
2322
+ setError(null);
2323
+ playBlockSfx('resize-step');
2324
+ };
2325
+ const repeatLastOperation = () => {
2326
+ if (!lastOperation || cancelDragRef.current)
2327
+ return;
2328
+ const repeated = repeatCommittedBlockOperation(operationServices, lastOperation, {
2329
+ mode,
2330
+ ids: selectedIds,
2331
+ activeId,
2332
+ });
2333
+ if (!repeated.ok) {
2334
+ setError(repeated.error);
2335
+ return;
2336
+ }
2337
+ const session = useBlockEditSession.getState();
2338
+ session.setLastOperation(node.id, repeated.operation);
2339
+ session.setSelection(node.id, {
2340
+ ...repeated.operation.resultSelection,
2341
+ activeId: repeated.operation.resultSelection.ids.at(-1) ?? null,
2342
+ });
2343
+ setLastOperationPanelOpen(true);
2344
+ setError(null);
2345
+ playBlockSfx('operation-commit');
2346
+ };
2347
+ const updateSelection = (next) => {
2348
+ if (!blockSelectionChanged({ mode, ids: selectedIds, activeId }, next))
2349
+ return;
2350
+ useBlockEditSession.getState().setSelection(node.id, next);
2351
+ setError(null);
2352
+ playBlockSfx('component-select');
2353
+ };
2354
+ const selectAll = () => updateSelection(selectAllBlockComponents(displayTopology, { mode, ids: selectedIds, activeId }));
2355
+ const invertSelection = () => updateSelection(invertBlockSelection(displayTopology, { mode, ids: selectedIds, activeId }));
2356
+ const clearSelection = () => updateSelection(clearBlockSelection({ mode, ids: selectedIds, activeId }));
2357
+ const keyboardActionsRef = useRef({
2358
+ beginKeyboardTransformModal,
2359
+ beginUniformScaleModal,
2360
+ canBevel: mode === 'edge',
2361
+ clearSelection,
2362
+ deleteSelection,
2363
+ dissolveSelection,
2364
+ extrudeSelectedFace,
2365
+ hasSelection: selectedIds.length > 0,
2366
+ hasLastOperation: Boolean(lastOperation),
2367
+ insetSelectedFace,
2368
+ invertSelection,
2369
+ mergeSelection,
2370
+ selectAll,
2371
+ repeatLastOperation,
2372
+ showLastOperation: () => setLastOperationPanelOpen(true),
2373
+ });
2374
+ keyboardActionsRef.current = {
2375
+ beginKeyboardTransformModal,
2376
+ beginUniformScaleModal,
2377
+ canBevel: mode === 'edge',
2378
+ clearSelection,
2379
+ deleteSelection,
2380
+ dissolveSelection,
2381
+ extrudeSelectedFace,
2382
+ hasSelection: selectedIds.length > 0,
2383
+ hasLastOperation: Boolean(lastOperation),
2384
+ insetSelectedFace,
2385
+ invertSelection,
2386
+ mergeSelection,
2387
+ selectAll,
2388
+ repeatLastOperation,
2389
+ showLastOperation: () => setLastOperationPanelOpen(true),
2390
+ };
2391
+ useEffect(() => {
2392
+ if (!editing)
2393
+ return;
2394
+ const onKeyDown = (event) => {
2395
+ const element = event.target;
2396
+ if (element?.tagName === 'INPUT' ||
2397
+ element?.tagName === 'TEXTAREA' ||
2398
+ element?.isContentEditable ||
2399
+ cancelDragRef.current)
2400
+ return;
2401
+ const key = event.key.toLowerCase();
2402
+ const actions = keyboardActionsRef.current;
2403
+ let handled = true;
2404
+ if (event.key === 'F9') {
2405
+ if (actions.hasLastOperation)
2406
+ actions.showLastOperation();
2407
+ else
2408
+ handled = false;
2409
+ }
2410
+ else if (key === 'b' && (event.ctrlKey || event.metaKey)) {
2411
+ if (actions.canBevel) {
2412
+ playBlockSfx('tool-select');
2413
+ setBevelSegments(DEFAULT_BEVEL_SEGMENTS);
2414
+ setTransformTool('bevel');
2415
+ setToolbarPanel(null);
2416
+ }
2417
+ }
2418
+ else if (key === 'a') {
2419
+ if (event.altKey)
2420
+ actions.clearSelection();
2421
+ else
2422
+ actions.selectAll();
2423
+ }
2424
+ else if (key === 'i' && (event.ctrlKey || event.metaKey)) {
2425
+ actions.invertSelection();
2426
+ }
2427
+ else if (key === 'g') {
2428
+ if (actions.hasSelection)
2429
+ actions.beginKeyboardTransformModal('translate');
2430
+ }
2431
+ else if (key === 'e') {
2432
+ actions.extrudeSelectedFace();
2433
+ }
2434
+ else if (key === 'i') {
2435
+ actions.insetSelectedFace();
2436
+ }
2437
+ else if (key === 'r' && event.shiftKey) {
2438
+ if (actions.hasLastOperation)
2439
+ actions.repeatLastOperation();
2440
+ else
2441
+ handled = false;
2442
+ }
2443
+ else if (key === 'r') {
2444
+ if (event.ctrlKey || event.metaKey) {
2445
+ playBlockSfx('tool-select');
2446
+ setTransformTool('loop-cut');
2447
+ setToolbarPanel(null);
2448
+ }
2449
+ else if (actions.hasSelection) {
2450
+ actions.beginKeyboardTransformModal('rotate');
2451
+ }
2452
+ }
2453
+ else if (key === 's' && !(event.ctrlKey || event.metaKey)) {
2454
+ if (actions.hasSelection) {
2455
+ if (!actions.beginUniformScaleModal()) {
2456
+ playBlockSfx('tool-select');
2457
+ setTransformTool('transform');
2458
+ }
2459
+ }
2460
+ }
2461
+ else if (key === 'm') {
2462
+ actions.mergeSelection();
2463
+ }
2464
+ else if (key === 'd') {
2465
+ actions.dissolveSelection();
2466
+ }
2467
+ else if (event.key === 'Delete' || key === 'x') {
2468
+ actions.deleteSelection();
2469
+ }
2470
+ else {
2471
+ handled = false;
2472
+ }
2473
+ if (!handled)
2474
+ return;
2475
+ event.preventDefault();
2476
+ event.stopImmediatePropagation();
2477
+ };
2478
+ window.addEventListener('keydown', onKeyDown, true);
2479
+ return () => window.removeEventListener('keydown', onKeyDown, true);
2480
+ }, [editing]);
2481
+ const moveNode = (event) => {
2482
+ event.stopPropagation();
2483
+ useEditor.getState().setMovingNode(node);
2484
+ interactionApi.clearSelection();
2485
+ triggerSFX('sfx:item-pick');
2486
+ };
2487
+ const deleteNode = (event) => {
2488
+ event.stopPropagation();
2489
+ interactionApi.clearSelection();
2490
+ sceneApi.delete(node.id);
2491
+ playBlockSfx('delete');
2492
+ };
2493
+ const selectionStatus = formatBlockSelectionStatus(mode, selectedIds.length);
2494
+ const operationAvailability = blockOperationAvailability(mode, selectedIds.length);
2495
+ const loopCutActive = transformTool === 'loop-cut';
2496
+ const bevelActive = transformTool === 'bevel';
2497
+ const gizmoTransform = activeTransform ??
2498
+ (activeFaceOperation && faceOperationAxis !== 'normal'
2499
+ ? { operation: 'translate', constraint: faceOperationAxis }
2500
+ : null);
2501
+ const gizmoDisabled = Boolean(activeTransform || activeFaceOperation);
2502
+ const componentStatus = activeFaceOperation
2503
+ ? blockModalFaceOperationStatus(activeFaceOperation, faceOperationValue || '0', modalFeedbackMode, faceOperationAxis)
2504
+ : activeTransform
2505
+ ? blockModalTransformStatus(activeTransform, transformNumericInput, modalFeedbackMode)
2506
+ : blockComponentStatus({
2507
+ mode,
2508
+ selectedCount: selectedIds.length,
2509
+ tool: transformTool,
2510
+ loopCutCount,
2511
+ loopCutFactor,
2512
+ bevelSegments,
2513
+ bevelWidth,
2514
+ });
2515
+ return (_jsxs("group", { ref: outerRef, children: [editing ? (_jsxs(_Fragment, { children: [mode === 'vertex'
2516
+ ? displayTopology.vertices.map((vertex) => (_jsx(VertexHandle, { active: activeId === vertex.id, id: vertex.id, onSelect: selectComponent, position: vertex.position, radius: componentRadius, selected: selectedSet.has(vertex.id), xray: xray }, vertex.id)))
2517
+ : null, mode === 'edge'
2518
+ ? displayTopology.edges.map((edge) => {
2519
+ const start = vertexById.get(edge.vertexIds[0]);
2520
+ const end = vertexById.get(edge.vertexIds[1]);
2521
+ return start && end ? (_jsx(EdgeHandle, { active: activeId === edge.id, end: end, id: edge.id, onPointerDown: transformTool === 'bevel' ? beginBevelDrag : undefined, onSelect: selectComponent, radius: componentRadius * 0.42, selected: selectedSet.has(edge.id), start: start, xray: xray }, edge.id)) : null;
2522
+ })
2523
+ : null, mode === 'face'
2524
+ ? displayTopology.faces.map((face) => {
2525
+ const center = blockFaceCentroid(displayTopology, face);
2526
+ return (_jsxs("group", { children: [_jsx(FaceHandle, { active: activeId === face.id, face: face, interactive: !xray, onSelect: selectComponent, selected: selectedSet.has(face.id), topology: displayTopology, xray: xray }), xray && center ? (_jsx(VertexHandle, { active: activeId === face.id, id: face.id, onSelect: selectComponent, position: center, radius: componentRadius * 0.72, selected: selectedSet.has(face.id), xray: true })) : null] }, face.id));
2527
+ })
2528
+ : null, gizmoOrigin && transformTool === 'transform' ? (_jsxs("group", { position: gizmoOrigin, children: [['x', 'y', 'z'].map((axis) => (_jsx(AxisTransformHandle, { axis: axis, disabled: gizmoDisabled, length: gizmoLength, moveHitRadius: gizmoHitDimensions.axisRadius, moveState: blockAxisVisualState(gizmoTransform, 'translate', axis), onMovePointerDown: beginTranslationDrag, onScalePointerDown: beginScaleDrag, radius: gizmoRadius, scaleHitRadius: gizmoHitDimensions.scaleRadius, scaleState: blockAxisVisualState(gizmoTransform, 'scale', axis) }, axis))), Object.keys(PLANE_NORMAL).map((plane) => (_jsx(PlaneMoveHandle, { disabled: gizmoDisabled, hitSize: gizmoHitDimensions.planeSize, offset: planeHandleOffset, onPointerDown: beginTranslationDrag, plane: plane, size: planeHandleSize, state: blockPlaneVisualState(gizmoTransform, plane) }, plane))), ['x', 'y', 'z'].map((axis) => (_jsx(RotationHandle, { arc: gizmoHitDimensions.rotationArc, axis: axis, disabled: gizmoDisabled, hitTube: gizmoHitDimensions.rotationTube, onPointerDown: beginRotationDrag, radius: rotationGizmoRadius, state: blockAxisVisualState(gizmoTransform, 'rotate', axis), start: gizmoHitDimensions.rotationStart, tube: gizmoRadius }, `rotate-${axis}`)))] })) : null, transformTool === 'loop-cut' && !loopCutSliding
2529
+ ? displayTopology.edges.map((edge) => {
2530
+ const start = vertexById.get(edge.vertexIds[0]);
2531
+ const end = vertexById.get(edge.vertexIds[1]);
2532
+ return start && end ? (_jsx(LoopCutTarget, { edgeId: edge.id, end: end, onHover: previewLoopCut, onPointerDown: beginLoopCutSlide, radius: componentRadius * 3.2, start: start }, edge.id)) : null;
2533
+ })
2534
+ : null, loopCutSegments ? _jsx(LoopCutPreview, { segments: loopCutSegments }) : null] })) : null, _jsx(Html, { center: true, position: menuAnchor, style: { pointerEvents: 'auto', touchAction: 'none', userSelect: 'none' }, zIndexRange: [70, 0], children: _jsxs("div", { className: "flex flex-col items-center gap-1", onContextMenu: (event) => event.stopPropagation(), onPointerDown: (event) => event.stopPropagation(), onPointerUp: (event) => event.stopPropagation(), ref: menuScaleRef, style: { transformOrigin: 'center center' }, children: [editing ? (_jsxs("div", { className: cn(FLOATING_PANEL_CLASS, 'relative'), children: [_jsx(ToolbarButton, { active: transformTool === 'transform', disabled: Boolean(selectedIds.length === 0 || cancelDragRef.current), label: "Transform selected components (G / R / S)", onClick: () => setTransformTool('transform'), children: _jsx(Move3D, { className: "h-4 w-4" }) }), _jsx(ToolbarButton, { active: mode === 'vertex', disabled: Boolean(cancelDragRef.current), label: "Vertex select (1)", onClick: () => switchMode('vertex'), children: _jsx(CircleDot, { className: "h-4 w-4" }) }), _jsx(ToolbarButton, { active: mode === 'edge', disabled: Boolean(cancelDragRef.current), label: "Edge select (2)", onClick: () => switchMode('edge'), children: _jsx(ScanLine, { className: "h-4 w-4" }) }), _jsx(ToolbarButton, { active: mode === 'face', disabled: Boolean(cancelDragRef.current), label: "Face select (3)", onClick: () => switchMode('face'), children: _jsx(Square, { className: "h-4 w-4" }) }), _jsx("span", { className: "min-w-14 whitespace-nowrap px-1.5 text-center font-mono text-[10px] text-foreground tracking-[0.08em]", children: selectionStatus }), _jsxs("div", { className: "relative", children: [_jsxs("button", { "aria-expanded": toolbarPanel === 'operations', "aria-haspopup": "dialog", className: cn('flex h-7 min-w-24 items-center justify-center gap-1.5 rounded-md px-2 text-xs transition-colors disabled:opacity-35', toolbarPanel === 'operations'
2535
+ ? 'bg-accent text-foreground'
2536
+ : 'text-muted-foreground hover:bg-accent hover:text-foreground'), onClick: (event) => {
2537
+ event.stopPropagation();
2538
+ playBlockSfx('tool-select');
2539
+ setToolbarPanel((current) => (current === 'operations' ? null : 'operations'));
2540
+ }, type: "button", children: [loopCutActive ? _jsx(Rows3, { className: "h-4 w-4" }) : null, bevelActive ? _jsx(Scaling, { className: "h-4 w-4" }) : null, _jsx("span", { children: loopCutActive ? 'LOOP CUT' : bevelActive ? 'BEVEL' : 'Operations' }), _jsx(ChevronDown, { className: "h-3.5 w-3.5" })] }), toolbarPanel === 'operations' ? (_jsx(ToolbarPanelFrame, { label: "Mesh operations", className: "w-80 p-1.5", children: _jsxs("div", { className: "space-y-0.5", children: [_jsx(ToolbarOperationItem, { disabled: selectedIds.length === 0, label: "Move selection", onClick: () => beginKeyboardTransformModal('translate'), shortcut: "G", children: _jsx(Move3D, { className: "h-4 w-4" }) }), _jsx(ToolbarOperationItem, { disabled: selectedIds.length === 0, label: "Rotate selection", onClick: () => beginKeyboardTransformModal('rotate'), shortcut: "R", children: _jsx(Rotate3D, { className: "h-4 w-4" }) }), _jsx(ToolbarOperationItem, { disabled: !operationAvailability.extrude, label: "Extrude selected faces", onClick: extrudeSelectedFace, shortcut: "E", children: _jsx(ArrowUpFromLine, { className: "h-4 w-4" }) }), _jsx(ToolbarOperationItem, { disabled: !operationAvailability.inset, label: "Inset selected faces", onClick: insetSelectedFace, shortcut: "I", children: _jsx(Square, { className: "h-4 w-4" }) }), _jsx(ToolbarOperationItem, { active: loopCutActive, controls: _jsx("input", { "aria-label": "Loop cut count", className: OPERATION_INPUT_CLASS, max: "32", min: "1", onChange: (event) => setLoopCutCount(Math.min(32, Math.max(1, Number(event.target.value) || 1))), step: "1", type: "number", value: loopCutCount }), label: "Loop Cut and Slide", onClick: () => {
2541
+ playBlockSfx('tool-select');
2542
+ setTransformTool('loop-cut');
2543
+ setToolbarPanel(null);
2544
+ }, shortcut: "Ctrl+R", children: _jsx(Rows3, { className: "h-4 w-4" }) }), _jsx(ToolbarOperationItem, { disabled: !operationAvailability.merge, label: "Merge vertices", onClick: mergeSelection, shortcut: "M", children: _jsx(CircleDot, { className: "h-4 w-4" }) }), _jsx(ToolbarOperationItem, { disabled: !operationAvailability.dissolve, label: "Dissolve selection", onClick: dissolveSelection, shortcut: "D", children: _jsx(ScanLine, { className: "h-4 w-4" }) }), _jsx(ToolbarOperationItem, { active: bevelActive, disabled: !operationAvailability.bevel, label: "Bevel selected edges", onClick: () => {
2545
+ playBlockSfx('tool-select');
2546
+ setBevelSegments(DEFAULT_BEVEL_SEGMENTS);
2547
+ setTransformTool('bevel');
2548
+ setToolbarPanel(null);
2549
+ }, shortcut: "Ctrl+B", children: _jsx(Scaling, { className: "h-4 w-4" }) })] }) })) : null] }), lastOperation ? (_jsx(ToolbarButton, { active: lastOperationPanelOpen, label: `Adjust ${lastOperation.label} (F9)`, onClick: () => setLastOperationPanelOpen((open) => !open), children: _jsx(Rotate3D, { className: "h-4 w-4" }) })) : null, _jsx(ToolbarButton, { label: "Finish edit mode (Tab)", onClick: exitEditMode, sound: false, children: _jsx(Check, { className: "h-4 w-4" }) }), _jsxs("div", { className: "relative", children: [_jsx(ToolbarButton, { active: toolbarPanel === 'selection', label: "Selection and more", onClick: () => setToolbarPanel((current) => (current === 'selection' ? null : 'selection')), children: _jsx(Ellipsis, { className: "h-4 w-4" }) }), toolbarPanel === 'selection' ? (_jsx(ToolbarPanelFrame, { label: "Selection actions", className: "right-0 left-auto w-60 translate-x-0", children: _jsxs("div", { className: "space-y-1", children: [_jsx(ToolbarMenuItem, { label: "Select all", onClick: selectAll, shortcut: "A", sound: false, children: _jsx(CircleDot, { className: "h-4 w-4" }) }), _jsx(ToolbarMenuItem, { label: "Invert selection", onClick: invertSelection, shortcut: "Ctrl+I", sound: false, children: _jsx(ScanLine, { className: "h-4 w-4" }) }), _jsx(ToolbarMenuItem, { disabled: selectedIds.length === 0, label: "Clear selection", onClick: clearSelection, shortcut: "Alt+A", sound: false, children: _jsx(XIcon, { className: "h-4 w-4" }) }), _jsx(ToolbarMenuItem, { active: xray, label: "X-ray selection", onClick: () => setXray((value) => !value), children: xray ? _jsx(Eye, { className: "h-4 w-4" }) : _jsx(EyeOff, { className: "h-4 w-4" }) }), _jsx("div", { className: "my-1 h-px bg-border/50" }), _jsx(ToolbarMenuItem, { destructive: true, disabled: selectedIds.length === 0, label: "Delete components", onClick: deleteSelection, shortcut: "X", sound: false, children: _jsx(Trash2, { className: "h-4 w-4" }) })] }) })) : null] })] })) : (_jsx(NodeActionMenu, { onDelete: deleteNode, onEditMesh: enterEditMode, onMove: moveNode })), editing && (error || componentStatus) ? (_jsx("div", { className: cn('whitespace-nowrap rounded-full border border-border/50 bg-background/90 px-3 py-1 font-medium text-[10px] shadow-sm backdrop-blur-md', error ? 'text-destructive' : 'text-muted-foreground'), children: error ?? componentStatus })) : null] }) }), editing && lastOperation && lastOperationPanelOpen ? (_jsx(Html, { calculatePosition: (_object, _camera, size) => [size.width / 2, size.height / 2], fullscreen: true, style: { pointerEvents: 'none' }, zIndexRange: [80, 0], children: _jsx(LastOperationPanel, { onChange: adjustLastOperation, onClose: () => setLastOperationPanelOpen(false), onRepeat: repeatLastOperation, operation: lastOperation }) })) : null] }));
2550
+ }
2551
+ const BlockSelectionAffordance = ({ historyApi, interactionApi, node, readOnly, sceneApi, }) => {
2552
+ const blockNode = node.type === 'block' ? node : null;
2553
+ const [target, setTarget] = useState(null);
2554
+ const targetRef = useRef(null);
2555
+ const nodeId = blockNode?.id ?? null;
2556
+ const scopeAllowsAffordance = useInteractionScope((state) => state.scope.kind === 'idle' ||
2557
+ (state.scope.kind === 'mesh-editing' && state.scope.nodeId === nodeId));
2558
+ useFrame(() => {
2559
+ const next = nodeId ? (sceneRegistry.nodes.get(nodeId) ?? null) : null;
2560
+ if (targetRef.current === next)
2561
+ return;
2562
+ targetRef.current = next;
2563
+ setTarget(next);
2564
+ });
2565
+ if (!blockNode || !target || !scopeAllowsAffordance)
2566
+ return null;
2567
+ const mount = target.parent ?? target;
2568
+ return createPortal(_jsx(BlockEditor, { historyApi: historyApi, interactionApi: interactionApi, mirrorTarget: mount !== target, node: blockNode, readOnly: readOnly, sceneApi: sceneApi, target: target }), mount, undefined);
2569
+ };
2570
+ export default BlockSelectionAffordance;