@pascal-app/editor 0.8.0 → 0.9.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (350) hide show
  1. package/package.json +15 -13
  2. package/src/components/editor/alignment-3d-guide-layer.tsx +168 -0
  3. package/src/components/editor/bake-exporter.tsx +47 -0
  4. package/src/components/editor/custom-camera-controls.tsx +795 -28
  5. package/src/components/editor/editor-layout-mobile.tsx +8 -3
  6. package/src/components/editor/editor-layout-v2.tsx +76 -51
  7. package/src/components/editor/export-manager.tsx +69 -32
  8. package/src/components/editor/fence-tangent-lines-3d.tsx +87 -0
  9. package/src/components/editor/first-person/build-collider-world.test.ts +182 -0
  10. package/src/components/editor/first-person/build-collider-world.ts +228 -83
  11. package/src/components/editor/first-person-controls.tsx +943 -27
  12. package/src/components/editor/floating-action-menu.tsx +676 -208
  13. package/src/components/editor/floating-building-action-menu.tsx +2 -2
  14. package/src/components/editor/floorplan-background-selection.test.ts +58 -0
  15. package/src/components/editor/floorplan-background-selection.ts +3 -2
  16. package/src/components/editor/floorplan-panel.tsx +6916 -12137
  17. package/src/components/editor/grid.tsx +197 -36
  18. package/src/components/editor/group-actions.ts +457 -0
  19. package/src/components/editor/group-floating-action-menu.tsx +130 -0
  20. package/src/components/editor/group-move-3d.ts +397 -0
  21. package/src/components/editor/group-rotate-handle.tsx +420 -0
  22. package/src/components/editor/group-selection-box-3d.tsx +173 -0
  23. package/src/components/editor/group-transform-shared.test.ts +396 -0
  24. package/src/components/editor/group-transform-shared.ts +489 -0
  25. package/src/components/editor/handles/handle-arrow.tsx +581 -0
  26. package/src/components/editor/handles/use-handle-drag.ts +246 -0
  27. package/src/components/editor/index.tsx +412 -162
  28. package/src/components/editor/measurement-pill.tsx +115 -0
  29. package/src/components/editor/node-action-menu.tsx +14 -1
  30. package/src/components/editor/node-arrow-handles.tsx +1646 -0
  31. package/src/components/editor/opening-guides-3d-layer.tsx +144 -0
  32. package/src/components/editor/quick-measurement-card.tsx +70 -0
  33. package/src/components/editor/quick-measurement-hud.tsx +28 -0
  34. package/src/components/editor/riser-diagram-panel.tsx +137 -0
  35. package/src/components/editor/selection-manager.tsx +1106 -483
  36. package/src/components/editor/site-edge-labels.tsx +45 -21
  37. package/src/components/editor/slab-hole-highlights.tsx +474 -0
  38. package/src/components/editor/snapshot-capture-overlay.tsx +369 -155
  39. package/src/components/editor/three-context-bridge.ts +22 -0
  40. package/src/components/editor/thumbnail-generator.tsx +140 -41
  41. package/src/components/editor/use-floorplan-background-placement.ts +190 -35
  42. package/src/components/editor/use-floorplan-hit-testing.ts +24 -0
  43. package/src/components/editor/use-floorplan-scene-data.ts +19 -7
  44. package/src/components/editor/use-mesh-settle-epoch.ts +26 -0
  45. package/src/components/editor/wall-measurement-label.tsx +14 -19
  46. package/src/components/editor/wall-move-side-handles.tsx +870 -0
  47. package/src/components/editor/wall-opening-highlights.tsx +181 -0
  48. package/src/components/editor/wall-snap-beacon-layer.tsx +304 -0
  49. package/src/components/editor-2d/floorplan-action-menu-layer.tsx +15 -6
  50. package/src/components/editor-2d/floorplan-alignment-guide-layer.tsx +142 -0
  51. package/src/components/editor-2d/floorplan-cursor-indicator-overlay.tsx +5 -3
  52. package/src/components/editor-2d/floorplan-group-action-menu.tsx +87 -0
  53. package/src/components/editor-2d/floorplan-group-move.tsx +701 -0
  54. package/src/components/editor-2d/floorplan-measurement-tool-layer.test.ts +113 -0
  55. package/src/components/editor-2d/floorplan-measurement-tool-layer.tsx +1656 -0
  56. package/src/components/editor-2d/floorplan-quick-measure-layer.tsx +212 -0
  57. package/src/components/editor-2d/floorplan-registry-action-menu.tsx +392 -0
  58. package/src/components/editor-2d/floorplan-registry-move-overlay.tsx +857 -0
  59. package/src/components/editor-2d/floorplan-render-context.tsx +62 -0
  60. package/src/components/editor-2d/floorplan-snap-beacon-layer.tsx +77 -0
  61. package/src/components/editor-2d/floorplan-wall-move-ghost-layer.tsx +47 -0
  62. package/src/components/editor-2d/renderers/floorplan-geometry-renderer.tsx +266 -0
  63. package/src/components/editor-2d/renderers/floorplan-label-angle.test.ts +15 -0
  64. package/src/components/editor-2d/renderers/floorplan-label-angle.ts +14 -0
  65. package/src/components/editor-2d/renderers/floorplan-measurements-layer.tsx +108 -66
  66. package/src/components/editor-2d/renderers/floorplan-placement-preview-layer.tsx +62 -0
  67. package/src/components/editor-2d/renderers/floorplan-registry-layer.test.ts +308 -0
  68. package/src/components/editor-2d/renderers/floorplan-registry-layer.tsx +3325 -0
  69. package/src/components/editor-2d/renderers/floorplan-stair-layer.tsx +24 -9
  70. package/src/components/editor-2d/renderers/floorplan-voronoi-layer.tsx +128 -0
  71. package/src/components/editor-2d/svg-paths.ts +10 -2
  72. package/src/components/systems/ceiling/ceiling-selection-affordance-system.tsx +439 -57
  73. package/src/components/systems/ceiling/ceiling-system.tsx +109 -12
  74. package/src/components/systems/roof/roof-edit-system.tsx +1851 -22
  75. package/src/components/systems/selection-affordance-manager.tsx +38 -0
  76. package/src/components/systems/stair/stair-edit-system.tsx +1 -1
  77. package/src/components/systems/zone/zone-label-editor-system.tsx +14 -1
  78. package/src/components/systems/zone/zone-system.tsx +42 -13
  79. package/src/components/tools/elevator/elevator-defaults.ts +8 -0
  80. package/src/components/tools/elevator/elevator-tool.tsx +375 -0
  81. package/src/components/tools/elevator/move-elevator-tool.tsx +263 -0
  82. package/src/components/tools/fence/fence-drafting.ts +141 -27
  83. package/src/components/tools/item/move-tool.tsx +45 -105
  84. package/src/components/tools/item/placement-math.test.ts +22 -0
  85. package/src/components/tools/item/placement-math.ts +59 -7
  86. package/src/components/tools/item/placement-strategies.ts +432 -16
  87. package/src/components/tools/item/placement-types.ts +22 -1
  88. package/src/components/tools/item/use-draft-node.ts +51 -2
  89. package/src/components/tools/item/use-placement-coordinator.tsx +1399 -256
  90. package/src/components/tools/registry/move-registry-node-tool.tsx +1064 -0
  91. package/src/components/tools/roof/roof-tool.tsx +350 -28
  92. package/src/components/tools/select/box-select-state.ts +88 -0
  93. package/src/components/tools/select/box-select-tool.tsx +442 -520
  94. package/src/components/tools/select/marquee-geometry.test.ts +161 -0
  95. package/src/components/tools/select/marquee-geometry.ts +155 -0
  96. package/src/components/tools/select/plane-box-select-tool.tsx +570 -0
  97. package/src/components/tools/select/screen-rectangle-selection.ts +84 -0
  98. package/src/components/tools/select/select-candidates.test.ts +109 -0
  99. package/src/components/tools/select/select-candidates.ts +85 -0
  100. package/src/components/tools/shared/affordance-dispatch.ts +30 -0
  101. package/src/components/tools/shared/cursor-sphere.tsx +62 -26
  102. package/src/components/tools/shared/drag-bounding-box.tsx +195 -0
  103. package/src/components/tools/shared/facing-indicator.tsx +87 -0
  104. package/src/components/tools/shared/facing-pose-indicator.tsx +54 -0
  105. package/src/components/tools/shared/floor-stack-preview.ts +25 -0
  106. package/src/components/tools/shared/fresh-placement-visibility.ts +61 -0
  107. package/src/components/tools/shared/placement-box-geometry.ts +123 -0
  108. package/src/components/tools/shared/placement-box.tsx +305 -0
  109. package/src/components/tools/shared/polygon-editor.tsx +866 -175
  110. package/src/components/tools/shared/segment-angle.ts +56 -0
  111. package/src/components/tools/site/site-boundary-editor.tsx +520 -16
  112. package/src/components/tools/site/site-flag-model.ts +18 -0
  113. package/src/components/tools/stair/stair-defaults.ts +2 -1
  114. package/src/components/tools/stair/stair-tool.tsx +342 -37
  115. package/src/components/tools/tool-manager.tsx +239 -62
  116. package/src/components/tools/wall/wall-drafting.test.ts +330 -0
  117. package/src/components/tools/wall/wall-drafting.ts +248 -171
  118. package/src/components/tools/wall/wall-snap-geometry.test.ts +123 -0
  119. package/src/components/tools/wall/wall-snap-geometry.ts +331 -0
  120. package/src/components/tools/zone/zone-boundary-editor.tsx +5 -1
  121. package/src/components/tools/zone/zone-tool.tsx +86 -82
  122. package/src/components/ui/action-menu/action-button.tsx +21 -1
  123. package/src/components/ui/action-menu/camera-actions.tsx +24 -17
  124. package/src/components/ui/action-menu/control-modes.tsx +40 -170
  125. package/src/components/ui/action-menu/furnish-tools.tsx +5 -5
  126. package/src/components/ui/action-menu/index.tsx +3 -117
  127. package/src/components/ui/action-menu/measurement-control.tsx +188 -0
  128. package/src/components/ui/action-menu/structure-tools.tsx +27 -93
  129. package/src/components/ui/action-menu/view-toggles.tsx +279 -90
  130. package/src/components/ui/command-palette/editor-commands.tsx +20 -15
  131. package/src/components/ui/command-palette/index.tsx +5 -3
  132. package/src/components/ui/controls/material-paint-panel.tsx +152 -0
  133. package/src/components/ui/controls/material-picker.tsx +87 -177
  134. package/src/components/ui/controls/material-properties-editor.tsx +108 -0
  135. package/src/components/ui/controls/metric-control.tsx +142 -46
  136. package/src/components/ui/controls/scene-material-list.tsx +247 -0
  137. package/src/components/ui/controls/slider-control.tsx +88 -38
  138. package/src/components/ui/floating-level-selector.tsx +47 -10
  139. package/src/components/ui/helpers/building-helper.tsx +10 -23
  140. package/src/components/ui/helpers/contextual-helper-panel.tsx +428 -0
  141. package/src/components/ui/helpers/helper-manager.tsx +235 -23
  142. package/src/components/ui/helpers/item-helper.tsx +28 -32
  143. package/src/components/ui/helpers/registered-tool-helper.tsx +59 -0
  144. package/src/components/ui/helpers/roof-helper.tsx +10 -12
  145. package/src/components/ui/icon-ref.tsx +47 -0
  146. package/src/components/ui/item-catalog/catalog-items.tsx +40 -1
  147. package/src/components/ui/item-catalog/item-catalog.tsx +16 -36
  148. package/src/components/ui/level-duplicate-dialog.tsx +2 -1
  149. package/src/components/ui/panels/node-display.ts +17 -16
  150. package/src/components/ui/panels/panel-manager.tsx +55 -58
  151. package/src/components/ui/panels/panel-wrapper.tsx +238 -12
  152. package/src/components/ui/panels/parametric-inspector.tsx +482 -0
  153. package/src/components/ui/panels/reference-panel.tsx +54 -19
  154. package/src/components/ui/primitives/dropdown-menu.tsx +12 -8
  155. package/src/components/ui/primitives/shortcut-token.tsx +39 -3
  156. package/src/components/ui/primitives/sidebar.tsx +1 -0
  157. package/src/components/ui/scene-loader.tsx +1 -3
  158. package/src/components/ui/sidebar/app-sidebar.tsx +5 -1
  159. package/src/components/ui/sidebar/icon-rail.tsx +50 -31
  160. package/src/components/ui/sidebar/panels/items-panel/function-tree-panel.tsx +263 -0
  161. package/src/components/ui/sidebar/panels/items-panel/index.tsx +82 -8
  162. package/src/components/ui/sidebar/panels/plugins-panel.tsx +218 -0
  163. package/src/components/ui/sidebar/panels/settings-panel/index.tsx +122 -41
  164. package/src/components/ui/sidebar/panels/settings-panel/keyboard-shortcuts-dialog.tsx +66 -7
  165. package/src/components/ui/sidebar/panels/settings-panel/load-build-dialog.tsx +267 -0
  166. package/src/components/ui/sidebar/panels/site-panel/building-tree-node.tsx +3 -1
  167. package/src/components/ui/sidebar/panels/site-panel/ceiling-tree-node.tsx +9 -5
  168. package/src/components/ui/sidebar/panels/site-panel/chimney-tree-node.tsx +86 -0
  169. package/src/components/ui/sidebar/panels/site-panel/column-tree-node.tsx +1 -1
  170. package/src/components/ui/sidebar/panels/site-panel/door-tree-node.tsx +7 -2
  171. package/src/components/ui/sidebar/panels/site-panel/dormer-tree-node.tsx +92 -0
  172. package/src/components/ui/sidebar/panels/site-panel/elevator-tree-node.tsx +75 -0
  173. package/src/components/ui/sidebar/panels/site-panel/fence-tree-node.tsx +3 -3
  174. package/src/components/ui/sidebar/panels/site-panel/gutter-tree-node.tsx +86 -0
  175. package/src/components/ui/sidebar/panels/site-panel/index.tsx +42 -21
  176. package/src/components/ui/sidebar/panels/site-panel/item-tree-node.tsx +31 -17
  177. package/src/components/ui/sidebar/panels/site-panel/level-tree-node.tsx +2 -1
  178. package/src/components/ui/sidebar/panels/site-panel/registry-tree-node.tsx +140 -0
  179. package/src/components/ui/sidebar/panels/site-panel/roof-tree-node.tsx +48 -8
  180. package/src/components/ui/sidebar/panels/site-panel/shelf-tree-node.tsx +132 -0
  181. package/src/components/ui/sidebar/panels/site-panel/slab-tree-node.tsx +9 -5
  182. package/src/components/ui/sidebar/panels/site-panel/solar-panel-tree-node.tsx +86 -0
  183. package/src/components/ui/sidebar/panels/site-panel/spawn-tree-node.tsx +7 -1
  184. package/src/components/ui/sidebar/panels/site-panel/stair-tree-node.tsx +2 -2
  185. package/src/components/ui/sidebar/panels/site-panel/tree-node.tsx +112 -39
  186. package/src/components/ui/sidebar/panels/site-panel/tree-structure.ts +36 -0
  187. package/src/components/ui/sidebar/panels/site-panel/wall-tree-node.tsx +1 -1
  188. package/src/components/ui/sidebar/panels/site-panel/window-tree-node.tsx +7 -2
  189. package/src/components/ui/sidebar/panels/site-panel/zone-tree-node.tsx +3 -3
  190. package/src/components/ui/sidebar/panels/zone-panel/index.tsx +51 -2
  191. package/src/components/ui/sidebar/tab-bar.tsx +75 -1
  192. package/src/components/ui/sidebar/use-plugin-panels.tsx +132 -0
  193. package/src/components/ui/snap-target-badge.test.tsx +40 -0
  194. package/src/components/ui/snap-target-badge.tsx +88 -0
  195. package/src/components/viewer-overlay.tsx +253 -61
  196. package/src/components/viewer-zone-system.tsx +7 -2
  197. package/src/hooks/use-auto-save.test.ts +14 -0
  198. package/src/hooks/use-auto-save.ts +64 -10
  199. package/src/hooks/use-ceiling-events.ts +176 -0
  200. package/src/hooks/use-drag-action.ts +127 -0
  201. package/src/hooks/use-keyboard.ts +493 -95
  202. package/src/hooks/use-selection.ts +64 -0
  203. package/src/index.tsx +451 -6
  204. package/src/lib/active-placement-surface.ts +35 -0
  205. package/src/lib/ceiling-plan-snap.ts +24 -0
  206. package/src/lib/constants.ts +6 -2
  207. package/src/lib/contextual-help.test.ts +112 -0
  208. package/src/lib/contextual-help.ts +144 -0
  209. package/src/lib/continuation.ts +64 -0
  210. package/src/lib/direct-manipulation.test.ts +192 -0
  211. package/src/lib/direct-manipulation.ts +109 -0
  212. package/src/lib/editor-api.ts +50 -0
  213. package/src/lib/elevator-support.ts +96 -0
  214. package/src/lib/floorplan/apply-alignment.test.ts +25 -0
  215. package/src/lib/floorplan/apply-alignment.ts +117 -0
  216. package/src/lib/floorplan/floorplan-export.tsx +364 -0
  217. package/src/lib/floorplan/geometry.ts +53 -0
  218. package/src/lib/floorplan/index.ts +10 -0
  219. package/src/lib/floorplan/items.ts +5 -2
  220. package/src/lib/floorplan/plan-coords.ts +21 -0
  221. package/src/lib/floorplan/selection-tool.ts +40 -0
  222. package/src/lib/floorplan/stairs.ts +12 -1
  223. package/src/lib/fresh-planar-placement.test.ts +339 -0
  224. package/src/lib/fresh-planar-placement.ts +130 -0
  225. package/src/lib/glb-export.test.ts +441 -0
  226. package/src/lib/glb-export.ts +874 -0
  227. package/src/lib/history.ts +11 -1
  228. package/src/lib/interaction/hot-set.test.ts +133 -0
  229. package/src/lib/interaction/hot-set.ts +67 -0
  230. package/src/lib/interaction/overlay-policy.test.ts +51 -0
  231. package/src/lib/interaction/overlay-policy.ts +59 -0
  232. package/src/lib/interaction/scope.ts +211 -0
  233. package/src/lib/level-duplication.ts +54 -42
  234. package/src/lib/level-selection.ts +2 -2
  235. package/src/lib/material-paint.ts +227 -48
  236. package/src/lib/measurement-kind.test.ts +30 -0
  237. package/src/lib/measurement-kind.ts +19 -0
  238. package/src/lib/measurement-label.test.ts +47 -0
  239. package/src/lib/measurement-label.ts +68 -0
  240. package/src/lib/measurement-parser.ts +116 -0
  241. package/src/lib/measurements.test.ts +183 -0
  242. package/src/lib/measurements.ts +199 -0
  243. package/src/lib/paint-scope.test.ts +454 -0
  244. package/src/lib/paint-scope.ts +461 -0
  245. package/src/lib/placement-drag-release.ts +23 -0
  246. package/src/lib/placement-metadata.ts +29 -0
  247. package/src/lib/planar-cursor-placement.test.ts +100 -0
  248. package/src/lib/planar-cursor-placement.ts +42 -0
  249. package/src/lib/plugin-panels.test.ts +19 -0
  250. package/src/lib/plugin-panels.ts +91 -0
  251. package/src/lib/quick-measurement.test.ts +77 -0
  252. package/src/lib/quick-measurement.ts +109 -0
  253. package/src/lib/roof-duplication.ts +7 -7
  254. package/src/lib/roof-hover-outline-proxy.ts +22 -0
  255. package/src/lib/roof-wall-hit.ts +164 -0
  256. package/src/lib/scene-clipboard.test.ts +196 -0
  257. package/src/lib/scene-clipboard.ts +332 -0
  258. package/src/lib/scene.ts +40 -8
  259. package/src/lib/selection-routing.test.ts +298 -0
  260. package/src/lib/selection-routing.ts +175 -0
  261. package/src/lib/sfx/index.ts +1 -0
  262. package/src/lib/sfx/movement-tick.test.ts +65 -0
  263. package/src/lib/sfx/movement-tick.ts +18 -0
  264. package/src/lib/sfx-bus.ts +69 -10
  265. package/src/lib/sfx-player.test.ts +124 -0
  266. package/src/lib/sfx-player.ts +148 -55
  267. package/src/lib/site-boundary.ts +1 -0
  268. package/src/lib/slab-plan-snap.test.ts +93 -0
  269. package/src/lib/slab-plan-snap.ts +108 -0
  270. package/src/lib/snapping-mode.test.ts +138 -0
  271. package/src/lib/snapping-mode.ts +179 -0
  272. package/src/lib/stair-duplication.ts +4 -4
  273. package/src/lib/stair-levels.test.ts +146 -0
  274. package/src/lib/stair-levels.ts +177 -0
  275. package/src/lib/surface-plan-snap.test.ts +71 -0
  276. package/src/lib/surface-plan-snap.ts +256 -0
  277. package/src/lib/use-linear-display.ts +40 -0
  278. package/src/lib/world-grid-snap.ts +231 -0
  279. package/src/lib/zone-content.ts +130 -0
  280. package/src/store/use-alignment-guides.ts +21 -0
  281. package/src/store/use-direct-manipulation-feedback.ts +20 -0
  282. package/src/store/use-editor.tsx +767 -114
  283. package/src/store/use-facing-pose.ts +44 -0
  284. package/src/store/use-fence-curve-draft.ts +21 -0
  285. package/src/store/use-floorplan-draft-preview.ts +101 -0
  286. package/src/store/use-floorplan-marquee.ts +50 -0
  287. package/src/store/use-interaction-scope.test.ts +186 -0
  288. package/src/store/use-interaction-scope.ts +132 -0
  289. package/src/store/use-measurement-draft.test.ts +530 -0
  290. package/src/store/use-measurement-draft.ts +543 -0
  291. package/src/store/use-opening-guides.ts +41 -0
  292. package/src/store/use-placement-preview.ts +39 -0
  293. package/src/store/use-quick-measurement-hud.test.ts +53 -0
  294. package/src/store/use-quick-measurement-hud.ts +77 -0
  295. package/src/store/use-segment-draft-chain.ts +28 -0
  296. package/src/store/use-stair-build-preview.ts +43 -0
  297. package/src/store/use-wall-move-ghosts.ts +36 -0
  298. package/src/store/use-wall-snap-indicator.ts +34 -0
  299. package/src/components/editor/first-person/bvh-ecctrl.tsx +0 -860
  300. package/src/components/editor/preset-thumbnail-generator.tsx +0 -125
  301. package/src/components/editor-2d/renderers/floorplan-roof-layer.tsx +0 -113
  302. package/src/components/tools/building/move-building-tool.tsx +0 -157
  303. package/src/components/tools/ceiling/ceiling-boundary-editor.tsx +0 -43
  304. package/src/components/tools/ceiling/ceiling-hole-editor.tsx +0 -49
  305. package/src/components/tools/ceiling/ceiling-tool.tsx +0 -465
  306. package/src/components/tools/ceiling/move-ceiling-tool.tsx +0 -264
  307. package/src/components/tools/column/column-tool.tsx +0 -97
  308. package/src/components/tools/column/move-column-tool.tsx +0 -105
  309. package/src/components/tools/door/door-math.ts +0 -110
  310. package/src/components/tools/door/door-tool.tsx +0 -324
  311. package/src/components/tools/door/move-door-tool.tsx +0 -412
  312. package/src/components/tools/fence/curve-fence-tool.tsx +0 -178
  313. package/src/components/tools/fence/fence-tool.tsx +0 -346
  314. package/src/components/tools/fence/move-fence-endpoint-tool.tsx +0 -441
  315. package/src/components/tools/fence/move-fence-tool.tsx +0 -302
  316. package/src/components/tools/item/item-tool.tsx +0 -26
  317. package/src/components/tools/roof/move-roof-tool.tsx +0 -364
  318. package/src/components/tools/slab/move-slab-tool.tsx +0 -182
  319. package/src/components/tools/slab/slab-boundary-editor.tsx +0 -43
  320. package/src/components/tools/slab/slab-hole-editor.tsx +0 -49
  321. package/src/components/tools/slab/slab-tool.tsx +0 -322
  322. package/src/components/tools/spawn/move-spawn-tool.tsx +0 -101
  323. package/src/components/tools/spawn/spawn-tool.tsx +0 -130
  324. package/src/components/tools/wall/curve-wall-tool.tsx +0 -178
  325. package/src/components/tools/wall/move-wall-endpoint-tool.tsx +0 -426
  326. package/src/components/tools/wall/move-wall-tool.tsx +0 -358
  327. package/src/components/tools/wall/wall-tool.tsx +0 -332
  328. package/src/components/tools/window/move-window-tool.tsx +0 -447
  329. package/src/components/tools/window/window-math.ts +0 -117
  330. package/src/components/tools/window/window-tool.tsx +0 -332
  331. package/src/components/ui/helpers/ceiling-helper.tsx +0 -20
  332. package/src/components/ui/helpers/slab-helper.tsx +0 -20
  333. package/src/components/ui/helpers/wall-helper.tsx +0 -20
  334. package/src/components/ui/panels/ceiling-panel.tsx +0 -249
  335. package/src/components/ui/panels/column-panel.tsx +0 -759
  336. package/src/components/ui/panels/door-panel.tsx +0 -1299
  337. package/src/components/ui/panels/fence-panel.tsx +0 -222
  338. package/src/components/ui/panels/item-panel.tsx +0 -306
  339. package/src/components/ui/panels/paint-panel.tsx +0 -163
  340. package/src/components/ui/panels/presets/presets-popover.tsx +0 -511
  341. package/src/components/ui/panels/roof-panel.tsx +0 -280
  342. package/src/components/ui/panels/roof-segment-panel.tsx +0 -311
  343. package/src/components/ui/panels/slab-panel.tsx +0 -250
  344. package/src/components/ui/panels/spawn-panel.tsx +0 -161
  345. package/src/components/ui/panels/stair-panel.tsx +0 -577
  346. package/src/components/ui/panels/stair-segment-panel.tsx +0 -325
  347. package/src/components/ui/panels/wall-panel.tsx +0 -273
  348. package/src/components/ui/panels/window-panel.tsx +0 -968
  349. package/src/contexts/presets-context.tsx +0 -121
  350. package/src/hooks/use-contextual-tools.ts +0 -61
@@ -1,17 +1,38 @@
1
1
  import {
2
2
  type AnyNode,
3
+ collectAlignmentAnchors,
4
+ createSurfaceOpeningPreviewController,
5
+ type EventSuffix,
3
6
  emitter,
4
7
  type GridEvent,
5
8
  type LevelNode,
9
+ movingAlignmentAnchors,
10
+ type NodeEvent,
11
+ resolveAlignment,
6
12
  StairNode,
7
13
  StairSegmentNode,
14
+ syncAutoStairOpenings,
8
15
  useScene,
9
16
  } from '@pascal-app/core'
10
17
  import { useViewer } from '@pascal-app/viewer'
11
18
  import { useEffect, useMemo, useRef } from 'react'
12
19
  import * as THREE from 'three'
13
20
  import { sfxEmitter } from '../../../lib/sfx-bus'
21
+ import {
22
+ resolveStairDestinationLevel,
23
+ resolveStairPlacementLevelId,
24
+ } from '../../../lib/stair-levels'
25
+
26
+ import useAlignmentGuides from '../../../store/use-alignment-guides'
27
+ import useEditor, {
28
+ isAlignmentGuideActive,
29
+ isGridSnapActive,
30
+ isMagneticSnapActive,
31
+ } from '../../../store/use-editor'
32
+
33
+ import useFacingPose from '../../../store/use-facing-pose'
14
34
  import { CursorSphere } from '../shared/cursor-sphere'
35
+ import { getFloorStackPreviewPosition } from '../shared/floor-stack-preview'
15
36
  import {
16
37
  DEFAULT_CURVED_STAIR_INNER_RADIUS,
17
38
  DEFAULT_CURVED_STAIR_SWEEP_ANGLE,
@@ -23,6 +44,7 @@ import {
23
44
  DEFAULT_STAIR_FILL_TO_FLOOR,
24
45
  DEFAULT_STAIR_HEIGHT,
25
46
  DEFAULT_STAIR_LENGTH,
47
+ DEFAULT_STAIR_OPENING_OFFSET,
26
48
  DEFAULT_STAIR_RAILING_HEIGHT,
27
49
  DEFAULT_STAIR_RAILING_MODE,
28
50
  DEFAULT_STAIR_STEP_COUNT,
@@ -32,6 +54,23 @@ import {
32
54
  } from './stair-defaults'
33
55
 
34
56
  const GRID_OFFSET = 0.02
57
+ /** Figma-style alignment-snap threshold (meters), matching the move tools. */
58
+ const ALIGNMENT_THRESHOLD_M = 0.08
59
+ type ClickTriggerEvent = GridEvent | NodeEvent<AnyNode>
60
+
61
+ const CLICK_TRIGGER_KINDS = [
62
+ 'shelf',
63
+ 'item',
64
+ 'slab',
65
+ 'ceiling',
66
+ 'wall',
67
+ 'fence',
68
+ 'column',
69
+ 'roof',
70
+ 'roof-segment',
71
+ 'stair',
72
+ 'stair-segment',
73
+ ] as const
35
74
 
36
75
  /**
37
76
  * Generates the step-profile geometry for the ghost preview.
@@ -69,19 +108,10 @@ function createStairPreviewGeometry(): THREE.BufferGeometry {
69
108
  }
70
109
 
71
110
  /**
72
- * Creates a stair group with one default stair segment at the given position/rotation.
111
+ * Creates a default straight stair segment.
73
112
  */
74
- function commitStairPlacement(
75
- levelId: LevelNode['id'],
76
- position: [number, number, number],
77
- rotation: number,
78
- ): void {
79
- const { createNodes, nodes } = useScene.getState()
80
-
81
- const stairCount = Object.values(nodes).filter((n) => n.type === 'stair').length
82
- const name = `Staircase ${stairCount + 1}`
83
-
84
- const segment = StairSegmentNode.parse({
113
+ function createDefaultStairSegment() {
114
+ return StairSegmentNode.parse({
85
115
  segmentType: 'stair',
86
116
  width: DEFAULT_STAIR_WIDTH,
87
117
  length: DEFAULT_STAIR_LENGTH,
@@ -92,14 +122,24 @@ function commitStairPlacement(
92
122
  thickness: DEFAULT_STAIR_THICKNESS,
93
123
  position: [0, 0, 0],
94
124
  })
125
+ }
95
126
 
96
- const sortedLevels = Object.values(nodes)
97
- .filter((node): node is LevelNode => node.type === 'level')
98
- .sort((left, right) => left.level - right.level)
99
- const currentLevelIndex = sortedLevels.findIndex((level) => level.id === levelId)
100
- const nextLevelId = sortedLevels[currentLevelIndex + 1]?.id ?? levelId
101
-
102
- const stair = StairNode.parse({
127
+ function createDefaultStairNode({
128
+ name,
129
+ levelId,
130
+ nextLevelId,
131
+ position,
132
+ rotation,
133
+ segmentId,
134
+ }: {
135
+ name: string
136
+ levelId: LevelNode['id']
137
+ nextLevelId: LevelNode['id']
138
+ position: [number, number, number]
139
+ rotation: number
140
+ segmentId: StairSegmentNode['id']
141
+ }) {
142
+ return StairNode.parse({
103
143
  name,
104
144
  position,
105
145
  rotation,
@@ -107,7 +147,7 @@ function commitStairPlacement(
107
147
  fromLevelId: levelId,
108
148
  toLevelId: nextLevelId,
109
149
  slabOpeningMode: 'destination',
110
- openingOffset: 0.08,
150
+ openingOffset: DEFAULT_STAIR_OPENING_OFFSET,
111
151
  width: DEFAULT_STAIR_WIDTH,
112
152
  totalRise: DEFAULT_STAIR_HEIGHT,
113
153
  stepCount: DEFAULT_STAIR_STEP_COUNT,
@@ -121,11 +161,55 @@ function commitStairPlacement(
121
161
  showStepSupports: DEFAULT_SPIRAL_SHOW_STEP_SUPPORTS,
122
162
  railingHeight: DEFAULT_STAIR_RAILING_HEIGHT,
123
163
  railingMode: DEFAULT_STAIR_RAILING_MODE,
124
- children: [segment.id],
164
+ children: [segmentId],
165
+ })
166
+ }
167
+
168
+ /**
169
+ * Creates a stair group with one default stair segment at the given position/rotation.
170
+ */
171
+ function commitStairPlacement(
172
+ levelId: LevelNode['id'],
173
+ position: [number, number, number],
174
+ rotation: number,
175
+ ): void {
176
+ const { createNodes, nodes } = useScene.getState()
177
+ const placementLevelId = resolveStairPlacementLevelId(
178
+ nodes,
179
+ levelId,
180
+ useViewer.getState().selection.buildingId,
181
+ )
182
+ if (!placementLevelId) return
183
+
184
+ const stairCount = Object.values(nodes).filter((n) => n.type === 'stair').length
185
+ const name = `Staircase ${stairCount + 1}`
186
+ const segment = createDefaultStairSegment()
187
+
188
+ const destinationPlan = resolveStairDestinationLevel({
189
+ createMissing: true,
190
+ fromLevelId: placementLevelId,
191
+ nodes,
125
192
  })
193
+ const nextLevelId = destinationPlan?.toLevel.id ?? placementLevelId
194
+
195
+ const stair = createDefaultStairNode({
196
+ name,
197
+ levelId: placementLevelId,
198
+ nextLevelId,
199
+ position,
200
+ rotation,
201
+ segmentId: segment.id,
202
+ })
203
+
204
+ const createdLevel = destinationPlan?.createdLevel
205
+ const levelCreateOps =
206
+ createdLevel && destinationPlan.buildingId
207
+ ? [{ node: createdLevel, parentId: destinationPlan.buildingId }]
208
+ : []
126
209
 
127
210
  createNodes([
128
- { node: stair, parentId: levelId },
211
+ ...levelCreateOps,
212
+ { node: stair, parentId: placementLevelId },
129
213
  { node: segment, parentId: stair.id },
130
214
  ])
131
215
 
@@ -137,6 +221,7 @@ export const StairTool: React.FC = () => {
137
221
  const previewRef = useRef<THREE.Group>(null)
138
222
  const rotationRef = useRef(0)
139
223
  const previousGridPosRef = useRef<[number, number] | null>(null)
224
+ const lastCanonicalPositionRef = useRef<[number, number, number] | null>(null)
140
225
  const currentLevelId = useViewer((state) => state.selection.levelId)
141
226
 
142
227
  const previewGeometry = useMemo(() => createStairPreviewGeometry(), [])
@@ -144,24 +229,187 @@ export const StairTool: React.FC = () => {
144
229
  useEffect(() => {
145
230
  if (!currentLevelId) return
146
231
 
232
+ const openingPreview = createSurfaceOpeningPreviewController()
233
+
147
234
  // Reset rotation when tool activates
148
235
  rotationRef.current = 0
149
236
  if (previewRef.current) previewRef.current.rotation.y = 0
237
+ lastCanonicalPositionRef.current = null
238
+
239
+ const buildPreviewScene = (position: [number, number, number], rotation: number) => {
240
+ const nodes = useScene.getState().nodes
241
+ const placementLevelId = resolveStairPlacementLevelId(
242
+ nodes,
243
+ currentLevelId,
244
+ useViewer.getState().selection.buildingId,
245
+ )
246
+ if (!placementLevelId) return null
247
+
248
+ const destinationPlan = resolveStairDestinationLevel({
249
+ createMissing: true,
250
+ fromLevelId: placementLevelId,
251
+ nodes,
252
+ })
253
+ const nextLevelId = destinationPlan?.toLevel.id ?? placementLevelId
254
+ const segment = createDefaultStairSegment()
255
+ const stair = createDefaultStairNode({
256
+ name: 'Staircase Preview',
257
+ levelId: placementLevelId,
258
+ nextLevelId,
259
+ position,
260
+ rotation,
261
+ segmentId: segment.id,
262
+ })
263
+ const previewNodes = {
264
+ ...nodes,
265
+ ...(destinationPlan?.createdLevel
266
+ ? { [destinationPlan.createdLevel.id]: destinationPlan.createdLevel }
267
+ : {}),
268
+ [stair.id]: { ...stair, parentId: placementLevelId },
269
+ [segment.id]: { ...segment, parentId: stair.id },
270
+ } as Record<string, AnyNode>
271
+
272
+ return { placementLevelId, previewNodes, stair }
273
+ }
150
274
 
151
- const onGridMove = (event: GridEvent) => {
152
- const gridX = Math.round(event.localPosition[0] * 2) / 2
153
- const gridZ = Math.round(event.localPosition[2] * 2) / 2
154
- const y = event.localPosition[1]
155
-
275
+ // The preview rebuild (full-scene copy + destination-level resolution +
276
+ // auto-opening CSG) is expensive; `grid:move` fires it every pointer event
277
+ // but the placed position is grid-snapped, so within a cell every rebuild
278
+ // is identical. Dedupe on the snapped position + rotation so we rebuild
279
+ // only when the staircase would actually land somewhere new — this is the
280
+ // difference between a smooth and a stuttering stair tool (the elevator is
281
+ // cheap because it has no opening sync).
282
+ let lastPreviewKey: string | null = null
283
+
284
+ const applyDraftPreview = (position: [number, number, number], rotation: number) => {
285
+ const key = `${position[0].toFixed(3)},${position[2].toFixed(3)},${rotation.toFixed(4)}`
286
+ if (key === lastPreviewKey) return
287
+ lastPreviewKey = key
288
+ const preview = buildPreviewScene(position, rotation)
289
+ const visualPosition = preview
290
+ ? getFloorStackPreviewPosition({
291
+ node: preview.stair,
292
+ position,
293
+ rotation,
294
+ levelId: preview.placementLevelId,
295
+ nodes: preview.previewNodes,
296
+ })
297
+ : position
156
298
  if (cursorRef.current) {
157
- cursorRef.current.position.set(gridX, y + GRID_OFFSET, gridZ)
299
+ cursorRef.current.position.set(
300
+ visualPosition[0],
301
+ visualPosition[1] + GRID_OFFSET,
302
+ visualPosition[2],
303
+ )
158
304
  }
159
305
 
160
306
  if (previewRef.current) {
161
- previewRef.current.position.set(gridX, y, gridZ)
307
+ previewRef.current.position.set(...visualPosition)
308
+ previewRef.current.rotation.y = rotation
309
+ }
310
+
311
+ // Forward-facing triangle (editor-side overlay). The run ascends along
312
+ // local +Z from the entry at z≈0; the stair's front is the -Z entry side,
313
+ // so `reversed` points the triangle out of the entry (where you approach
314
+ // from), sitting just before it — not inside the footprint or at the
315
+ // elevated far end. Centre is the footprint mid-run (origin is the entry).
316
+ useFacingPose.getState().set({
317
+ position: visualPosition,
318
+ rotationY: rotation,
319
+ depth: DEFAULT_STAIR_LENGTH,
320
+ center: [0, DEFAULT_STAIR_LENGTH / 2],
321
+ reversed: true,
322
+ })
323
+
324
+ if (!preview) {
325
+ openingPreview.clear()
326
+ return
327
+ }
328
+
329
+ openingPreview.apply(syncAutoStairOpenings(preview.previewNodes))
330
+ }
331
+
332
+ // Alignment candidates — anchors of every alignable object; refreshed
333
+ // after each placement. The moving stair aligns by its footprint edges so
334
+ // users can snap the run side against walls, slabs, elevators, or another
335
+ // stair instead of only lining up the invisible origin point.
336
+ let alignmentCandidates = collectAlignmentAnchors(useScene.getState().nodes, '', currentLevelId)
337
+ const resolveStairFootprintAlignment = (
338
+ x: number,
339
+ z: number,
340
+ rotation: number,
341
+ ): ReturnType<typeof resolveAlignment> | null => {
342
+ const preview = buildPreviewScene([x, 0, z], rotation)
343
+ const moving = preview
344
+ ? movingAlignmentAnchors(preview.stair, preview.previewNodes, x, z, rotation)
345
+ : []
346
+ if (moving.length === 0) return null
347
+ return resolveAlignment({
348
+ moving,
349
+ candidates: alignmentCandidates,
350
+ threshold: ALIGNMENT_THRESHOLD_M,
351
+ })
352
+ }
353
+ // The probe is the RAW cursor, not the grid-snapped point: resolving
354
+ // against the grid point would only catch anchors that happen to sit near
355
+ // a grid line. Matched axes use the raw probe + snap delta; unmatched axes
356
+ // keep the normal grid snap. Guides are published in every snapping mode
357
+ // (including Off); the magnetic pull toward them (applySnap) applies only in
358
+ // 'lines' mode.
359
+ const alignPoint = (
360
+ gridX: number,
361
+ gridZ: number,
362
+ rawX: number,
363
+ rawZ: number,
364
+ bypass: boolean,
365
+ applySnap: boolean,
366
+ ): [number, number] => {
367
+ if (bypass || alignmentCandidates.length === 0) {
368
+ useAlignmentGuides.getState().clear()
369
+ return [gridX, gridZ]
370
+ }
371
+ const ar = resolveStairFootprintAlignment(rawX, rawZ, rotationRef.current)
372
+ if (!ar || ar.guides.length === 0) {
373
+ useAlignmentGuides.getState().clear()
374
+ return [gridX, gridZ]
162
375
  }
376
+ if (!applySnap) {
377
+ useAlignmentGuides.getState().set(ar.guides)
378
+ return [gridX, gridZ]
379
+ }
380
+ let x = gridX
381
+ let z = gridZ
382
+ if (ar.snap) {
383
+ if (ar.guides.some((guide) => guide.axis === 'x')) x = rawX + ar.snap.dx
384
+ if (ar.guides.some((guide) => guide.axis === 'z')) z = rawZ + ar.snap.dz
385
+ }
386
+ const finalAlignment = resolveStairFootprintAlignment(x, z, rotationRef.current)
387
+ useAlignmentGuides.getState().set(finalAlignment?.guides ?? ar.guides)
388
+ return [x, z]
389
+ }
390
+
391
+ const onGridMove = (event: GridEvent) => {
392
+ // Grid snap follows the global mode (live step so the HUD chip is
393
+ // honest); Off keeps the raw cursor. Shift cycles the mode centrally.
394
+ const step = useEditor.getState().gridSnapStep
395
+ const [gridX, gridZ] = alignPoint(
396
+ isGridSnapActive()
397
+ ? Math.round(event.localPosition[0] / step) * step
398
+ : event.localPosition[0],
399
+ isGridSnapActive()
400
+ ? Math.round(event.localPosition[2] / step) * step
401
+ : event.localPosition[2],
402
+ event.localPosition[0],
403
+ event.localPosition[2],
404
+ !isAlignmentGuideActive(),
405
+ isMagneticSnapActive(),
406
+ )
407
+ const position: [number, number, number] = [gridX, 0, gridZ]
408
+ lastCanonicalPositionRef.current = position
409
+ applyDraftPreview(position, rotationRef.current)
163
410
 
164
411
  if (
412
+ (isGridSnapActive() || isMagneticSnapActive()) &&
165
413
  previousGridPosRef.current &&
166
414
  (gridX !== previousGridPosRef.current[0] || gridZ !== previousGridPosRef.current[1])
167
415
  ) {
@@ -171,12 +419,52 @@ export const StairTool: React.FC = () => {
171
419
  previousGridPosRef.current = [gridX, gridZ]
172
420
  }
173
421
 
174
- const onGridClick = (event: GridEvent) => {
422
+ const getAlignedGridPosition = (event: GridEvent): [number, number, number] => {
423
+ const step = useEditor.getState().gridSnapStep
424
+ const [gridX, gridZ] = alignPoint(
425
+ isGridSnapActive()
426
+ ? Math.round(event.localPosition[0] / step) * step
427
+ : event.localPosition[0],
428
+ isGridSnapActive()
429
+ ? Math.round(event.localPosition[2] / step) * step
430
+ : event.localPosition[2],
431
+ event.localPosition[0],
432
+ event.localPosition[2],
433
+ !isAlignmentGuideActive(),
434
+ isMagneticSnapActive(),
435
+ )
436
+ return [gridX, 0, gridZ]
437
+ }
438
+
439
+ const commitAtCursor = (event: ClickTriggerEvent) => {
175
440
  if (!currentLevelId) return
441
+ const nodeEvent = 'node' in event ? (event as NodeEvent<AnyNode>) : null
442
+ if (nodeEvent) {
443
+ nodeEvent.stopPropagation()
444
+ nodeEvent.nativeEvent.stopPropagation()
445
+ }
176
446
 
177
- const gridX = Math.round(event.localPosition[0] * 2) / 2
178
- const gridZ = Math.round(event.localPosition[2] * 2) / 2
179
- commitStairPlacement(currentLevelId, [gridX, 0, gridZ], rotationRef.current)
447
+ const position = nodeEvent
448
+ ? lastCanonicalPositionRef.current
449
+ : getAlignedGridPosition(event as GridEvent)
450
+ if (!position) return
451
+
452
+ commitStairPlacement(currentLevelId, position, rotationRef.current)
453
+ openingPreview.clear()
454
+ // Commit cleared the opening preview, so force the next hover (even on the
455
+ // same cell) to rebuild rather than dedupe against the just-placed key.
456
+ lastPreviewKey = null
457
+ useAlignmentGuides.getState().clear()
458
+
459
+ // Single by default; the C-toggle ('point' context, shared with every
460
+ // other placement tool) opts into placing more. On single, drop the tool
461
+ // and the facing triangle so we fall back to select after one stair.
462
+ if (useEditor.getState().getContinuation('point') === 'repeat') {
463
+ alignmentCandidates = collectAlignmentAnchors(useScene.getState().nodes, '', currentLevelId)
464
+ } else {
465
+ useFacingPose.getState().clear()
466
+ useEditor.getState().setTool(null)
467
+ }
180
468
  }
181
469
 
182
470
  const onKeyDown = (event: KeyboardEvent) => {
@@ -193,20 +481,35 @@ export const StairTool: React.FC = () => {
193
481
  event.preventDefault()
194
482
  sfxEmitter.emit('sfx:item-rotate')
195
483
  rotationRef.current += rotationDelta
196
- if (previewRef.current) {
484
+ if (lastCanonicalPositionRef.current) {
485
+ applyDraftPreview(lastCanonicalPositionRef.current, rotationRef.current)
486
+ } else if (previewRef.current) {
197
487
  previewRef.current.rotation.y = rotationRef.current
198
488
  }
199
489
  }
200
490
  }
201
491
 
202
492
  emitter.on('grid:move', onGridMove)
203
- emitter.on('grid:click', onGridClick)
493
+ emitter.on('grid:click', commitAtCursor)
494
+ type SuffixedKey<K extends string> = `${K}:${EventSuffix}`
495
+ type ClickKey = SuffixedKey<(typeof CLICK_TRIGGER_KINDS)[number]>
496
+ for (const kind of CLICK_TRIGGER_KINDS) {
497
+ const key = `${kind}:click` as ClickKey
498
+ emitter.on(key, commitAtCursor as never)
499
+ }
204
500
  window.addEventListener('keydown', onKeyDown)
205
501
 
206
502
  return () => {
207
503
  emitter.off('grid:move', onGridMove)
208
- emitter.off('grid:click', onGridClick)
504
+ emitter.off('grid:click', commitAtCursor)
505
+ for (const kind of CLICK_TRIGGER_KINDS) {
506
+ const key = `${kind}:click` as ClickKey
507
+ emitter.off(key, commitAtCursor as never)
508
+ }
209
509
  window.removeEventListener('keydown', onKeyDown)
510
+ useAlignmentGuides.getState().clear()
511
+ openingPreview.clear()
512
+ useFacingPose.getState().clear()
210
513
  }
211
514
  }, [currentLevelId])
212
515
 
@@ -214,7 +517,9 @@ export const StairTool: React.FC = () => {
214
517
  <group>
215
518
  <CursorSphere ref={cursorRef} />
216
519
 
217
- {/* 3D ghost preview — position/rotation updated imperatively */}
520
+ {/* 3D ghost preview — position/rotation updated imperatively. The
521
+ forward-facing triangle is drawn by the editor-side overlay from the
522
+ pose published in `applyDraftPreview`. */}
218
523
  <group ref={previewRef}>
219
524
  <mesh castShadow geometry={previewGeometry}>
220
525
  <meshStandardMaterial color="#818cf8" depthWrite={false} opacity={0.35} transparent />