@pascal-app/editor 0.8.0 → 0.9.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (350) hide show
  1. package/package.json +15 -13
  2. package/src/components/editor/alignment-3d-guide-layer.tsx +168 -0
  3. package/src/components/editor/bake-exporter.tsx +47 -0
  4. package/src/components/editor/custom-camera-controls.tsx +795 -28
  5. package/src/components/editor/editor-layout-mobile.tsx +8 -3
  6. package/src/components/editor/editor-layout-v2.tsx +76 -51
  7. package/src/components/editor/export-manager.tsx +69 -32
  8. package/src/components/editor/fence-tangent-lines-3d.tsx +87 -0
  9. package/src/components/editor/first-person/build-collider-world.test.ts +182 -0
  10. package/src/components/editor/first-person/build-collider-world.ts +228 -83
  11. package/src/components/editor/first-person-controls.tsx +943 -27
  12. package/src/components/editor/floating-action-menu.tsx +676 -208
  13. package/src/components/editor/floating-building-action-menu.tsx +2 -2
  14. package/src/components/editor/floorplan-background-selection.test.ts +58 -0
  15. package/src/components/editor/floorplan-background-selection.ts +3 -2
  16. package/src/components/editor/floorplan-panel.tsx +6916 -12137
  17. package/src/components/editor/grid.tsx +197 -36
  18. package/src/components/editor/group-actions.ts +457 -0
  19. package/src/components/editor/group-floating-action-menu.tsx +130 -0
  20. package/src/components/editor/group-move-3d.ts +397 -0
  21. package/src/components/editor/group-rotate-handle.tsx +420 -0
  22. package/src/components/editor/group-selection-box-3d.tsx +173 -0
  23. package/src/components/editor/group-transform-shared.test.ts +396 -0
  24. package/src/components/editor/group-transform-shared.ts +489 -0
  25. package/src/components/editor/handles/handle-arrow.tsx +581 -0
  26. package/src/components/editor/handles/use-handle-drag.ts +246 -0
  27. package/src/components/editor/index.tsx +412 -162
  28. package/src/components/editor/measurement-pill.tsx +115 -0
  29. package/src/components/editor/node-action-menu.tsx +14 -1
  30. package/src/components/editor/node-arrow-handles.tsx +1646 -0
  31. package/src/components/editor/opening-guides-3d-layer.tsx +144 -0
  32. package/src/components/editor/quick-measurement-card.tsx +70 -0
  33. package/src/components/editor/quick-measurement-hud.tsx +28 -0
  34. package/src/components/editor/riser-diagram-panel.tsx +137 -0
  35. package/src/components/editor/selection-manager.tsx +1106 -483
  36. package/src/components/editor/site-edge-labels.tsx +45 -21
  37. package/src/components/editor/slab-hole-highlights.tsx +474 -0
  38. package/src/components/editor/snapshot-capture-overlay.tsx +369 -155
  39. package/src/components/editor/three-context-bridge.ts +22 -0
  40. package/src/components/editor/thumbnail-generator.tsx +140 -41
  41. package/src/components/editor/use-floorplan-background-placement.ts +190 -35
  42. package/src/components/editor/use-floorplan-hit-testing.ts +24 -0
  43. package/src/components/editor/use-floorplan-scene-data.ts +19 -7
  44. package/src/components/editor/use-mesh-settle-epoch.ts +26 -0
  45. package/src/components/editor/wall-measurement-label.tsx +14 -19
  46. package/src/components/editor/wall-move-side-handles.tsx +870 -0
  47. package/src/components/editor/wall-opening-highlights.tsx +181 -0
  48. package/src/components/editor/wall-snap-beacon-layer.tsx +304 -0
  49. package/src/components/editor-2d/floorplan-action-menu-layer.tsx +15 -6
  50. package/src/components/editor-2d/floorplan-alignment-guide-layer.tsx +142 -0
  51. package/src/components/editor-2d/floorplan-cursor-indicator-overlay.tsx +5 -3
  52. package/src/components/editor-2d/floorplan-group-action-menu.tsx +87 -0
  53. package/src/components/editor-2d/floorplan-group-move.tsx +701 -0
  54. package/src/components/editor-2d/floorplan-measurement-tool-layer.test.ts +113 -0
  55. package/src/components/editor-2d/floorplan-measurement-tool-layer.tsx +1656 -0
  56. package/src/components/editor-2d/floorplan-quick-measure-layer.tsx +212 -0
  57. package/src/components/editor-2d/floorplan-registry-action-menu.tsx +392 -0
  58. package/src/components/editor-2d/floorplan-registry-move-overlay.tsx +857 -0
  59. package/src/components/editor-2d/floorplan-render-context.tsx +62 -0
  60. package/src/components/editor-2d/floorplan-snap-beacon-layer.tsx +77 -0
  61. package/src/components/editor-2d/floorplan-wall-move-ghost-layer.tsx +47 -0
  62. package/src/components/editor-2d/renderers/floorplan-geometry-renderer.tsx +266 -0
  63. package/src/components/editor-2d/renderers/floorplan-label-angle.test.ts +15 -0
  64. package/src/components/editor-2d/renderers/floorplan-label-angle.ts +14 -0
  65. package/src/components/editor-2d/renderers/floorplan-measurements-layer.tsx +108 -66
  66. package/src/components/editor-2d/renderers/floorplan-placement-preview-layer.tsx +62 -0
  67. package/src/components/editor-2d/renderers/floorplan-registry-layer.test.ts +308 -0
  68. package/src/components/editor-2d/renderers/floorplan-registry-layer.tsx +3325 -0
  69. package/src/components/editor-2d/renderers/floorplan-stair-layer.tsx +24 -9
  70. package/src/components/editor-2d/renderers/floorplan-voronoi-layer.tsx +128 -0
  71. package/src/components/editor-2d/svg-paths.ts +10 -2
  72. package/src/components/systems/ceiling/ceiling-selection-affordance-system.tsx +439 -57
  73. package/src/components/systems/ceiling/ceiling-system.tsx +109 -12
  74. package/src/components/systems/roof/roof-edit-system.tsx +1851 -22
  75. package/src/components/systems/selection-affordance-manager.tsx +38 -0
  76. package/src/components/systems/stair/stair-edit-system.tsx +1 -1
  77. package/src/components/systems/zone/zone-label-editor-system.tsx +14 -1
  78. package/src/components/systems/zone/zone-system.tsx +42 -13
  79. package/src/components/tools/elevator/elevator-defaults.ts +8 -0
  80. package/src/components/tools/elevator/elevator-tool.tsx +375 -0
  81. package/src/components/tools/elevator/move-elevator-tool.tsx +263 -0
  82. package/src/components/tools/fence/fence-drafting.ts +141 -27
  83. package/src/components/tools/item/move-tool.tsx +45 -105
  84. package/src/components/tools/item/placement-math.test.ts +22 -0
  85. package/src/components/tools/item/placement-math.ts +59 -7
  86. package/src/components/tools/item/placement-strategies.ts +432 -16
  87. package/src/components/tools/item/placement-types.ts +22 -1
  88. package/src/components/tools/item/use-draft-node.ts +51 -2
  89. package/src/components/tools/item/use-placement-coordinator.tsx +1399 -256
  90. package/src/components/tools/registry/move-registry-node-tool.tsx +1064 -0
  91. package/src/components/tools/roof/roof-tool.tsx +350 -28
  92. package/src/components/tools/select/box-select-state.ts +88 -0
  93. package/src/components/tools/select/box-select-tool.tsx +442 -520
  94. package/src/components/tools/select/marquee-geometry.test.ts +161 -0
  95. package/src/components/tools/select/marquee-geometry.ts +155 -0
  96. package/src/components/tools/select/plane-box-select-tool.tsx +570 -0
  97. package/src/components/tools/select/screen-rectangle-selection.ts +84 -0
  98. package/src/components/tools/select/select-candidates.test.ts +109 -0
  99. package/src/components/tools/select/select-candidates.ts +85 -0
  100. package/src/components/tools/shared/affordance-dispatch.ts +30 -0
  101. package/src/components/tools/shared/cursor-sphere.tsx +62 -26
  102. package/src/components/tools/shared/drag-bounding-box.tsx +195 -0
  103. package/src/components/tools/shared/facing-indicator.tsx +87 -0
  104. package/src/components/tools/shared/facing-pose-indicator.tsx +54 -0
  105. package/src/components/tools/shared/floor-stack-preview.ts +25 -0
  106. package/src/components/tools/shared/fresh-placement-visibility.ts +61 -0
  107. package/src/components/tools/shared/placement-box-geometry.ts +123 -0
  108. package/src/components/tools/shared/placement-box.tsx +305 -0
  109. package/src/components/tools/shared/polygon-editor.tsx +866 -175
  110. package/src/components/tools/shared/segment-angle.ts +56 -0
  111. package/src/components/tools/site/site-boundary-editor.tsx +520 -16
  112. package/src/components/tools/site/site-flag-model.ts +18 -0
  113. package/src/components/tools/stair/stair-defaults.ts +2 -1
  114. package/src/components/tools/stair/stair-tool.tsx +342 -37
  115. package/src/components/tools/tool-manager.tsx +239 -62
  116. package/src/components/tools/wall/wall-drafting.test.ts +330 -0
  117. package/src/components/tools/wall/wall-drafting.ts +248 -171
  118. package/src/components/tools/wall/wall-snap-geometry.test.ts +123 -0
  119. package/src/components/tools/wall/wall-snap-geometry.ts +331 -0
  120. package/src/components/tools/zone/zone-boundary-editor.tsx +5 -1
  121. package/src/components/tools/zone/zone-tool.tsx +86 -82
  122. package/src/components/ui/action-menu/action-button.tsx +21 -1
  123. package/src/components/ui/action-menu/camera-actions.tsx +24 -17
  124. package/src/components/ui/action-menu/control-modes.tsx +40 -170
  125. package/src/components/ui/action-menu/furnish-tools.tsx +5 -5
  126. package/src/components/ui/action-menu/index.tsx +3 -117
  127. package/src/components/ui/action-menu/measurement-control.tsx +188 -0
  128. package/src/components/ui/action-menu/structure-tools.tsx +27 -93
  129. package/src/components/ui/action-menu/view-toggles.tsx +279 -90
  130. package/src/components/ui/command-palette/editor-commands.tsx +20 -15
  131. package/src/components/ui/command-palette/index.tsx +5 -3
  132. package/src/components/ui/controls/material-paint-panel.tsx +152 -0
  133. package/src/components/ui/controls/material-picker.tsx +87 -177
  134. package/src/components/ui/controls/material-properties-editor.tsx +108 -0
  135. package/src/components/ui/controls/metric-control.tsx +142 -46
  136. package/src/components/ui/controls/scene-material-list.tsx +247 -0
  137. package/src/components/ui/controls/slider-control.tsx +88 -38
  138. package/src/components/ui/floating-level-selector.tsx +47 -10
  139. package/src/components/ui/helpers/building-helper.tsx +10 -23
  140. package/src/components/ui/helpers/contextual-helper-panel.tsx +428 -0
  141. package/src/components/ui/helpers/helper-manager.tsx +235 -23
  142. package/src/components/ui/helpers/item-helper.tsx +28 -32
  143. package/src/components/ui/helpers/registered-tool-helper.tsx +59 -0
  144. package/src/components/ui/helpers/roof-helper.tsx +10 -12
  145. package/src/components/ui/icon-ref.tsx +47 -0
  146. package/src/components/ui/item-catalog/catalog-items.tsx +40 -1
  147. package/src/components/ui/item-catalog/item-catalog.tsx +16 -36
  148. package/src/components/ui/level-duplicate-dialog.tsx +2 -1
  149. package/src/components/ui/panels/node-display.ts +17 -16
  150. package/src/components/ui/panels/panel-manager.tsx +55 -58
  151. package/src/components/ui/panels/panel-wrapper.tsx +238 -12
  152. package/src/components/ui/panels/parametric-inspector.tsx +482 -0
  153. package/src/components/ui/panels/reference-panel.tsx +54 -19
  154. package/src/components/ui/primitives/dropdown-menu.tsx +12 -8
  155. package/src/components/ui/primitives/shortcut-token.tsx +39 -3
  156. package/src/components/ui/primitives/sidebar.tsx +1 -0
  157. package/src/components/ui/scene-loader.tsx +1 -3
  158. package/src/components/ui/sidebar/app-sidebar.tsx +5 -1
  159. package/src/components/ui/sidebar/icon-rail.tsx +50 -31
  160. package/src/components/ui/sidebar/panels/items-panel/function-tree-panel.tsx +263 -0
  161. package/src/components/ui/sidebar/panels/items-panel/index.tsx +82 -8
  162. package/src/components/ui/sidebar/panels/plugins-panel.tsx +218 -0
  163. package/src/components/ui/sidebar/panels/settings-panel/index.tsx +122 -41
  164. package/src/components/ui/sidebar/panels/settings-panel/keyboard-shortcuts-dialog.tsx +66 -7
  165. package/src/components/ui/sidebar/panels/settings-panel/load-build-dialog.tsx +267 -0
  166. package/src/components/ui/sidebar/panels/site-panel/building-tree-node.tsx +3 -1
  167. package/src/components/ui/sidebar/panels/site-panel/ceiling-tree-node.tsx +9 -5
  168. package/src/components/ui/sidebar/panels/site-panel/chimney-tree-node.tsx +86 -0
  169. package/src/components/ui/sidebar/panels/site-panel/column-tree-node.tsx +1 -1
  170. package/src/components/ui/sidebar/panels/site-panel/door-tree-node.tsx +7 -2
  171. package/src/components/ui/sidebar/panels/site-panel/dormer-tree-node.tsx +92 -0
  172. package/src/components/ui/sidebar/panels/site-panel/elevator-tree-node.tsx +75 -0
  173. package/src/components/ui/sidebar/panels/site-panel/fence-tree-node.tsx +3 -3
  174. package/src/components/ui/sidebar/panels/site-panel/gutter-tree-node.tsx +86 -0
  175. package/src/components/ui/sidebar/panels/site-panel/index.tsx +42 -21
  176. package/src/components/ui/sidebar/panels/site-panel/item-tree-node.tsx +31 -17
  177. package/src/components/ui/sidebar/panels/site-panel/level-tree-node.tsx +2 -1
  178. package/src/components/ui/sidebar/panels/site-panel/registry-tree-node.tsx +140 -0
  179. package/src/components/ui/sidebar/panels/site-panel/roof-tree-node.tsx +48 -8
  180. package/src/components/ui/sidebar/panels/site-panel/shelf-tree-node.tsx +132 -0
  181. package/src/components/ui/sidebar/panels/site-panel/slab-tree-node.tsx +9 -5
  182. package/src/components/ui/sidebar/panels/site-panel/solar-panel-tree-node.tsx +86 -0
  183. package/src/components/ui/sidebar/panels/site-panel/spawn-tree-node.tsx +7 -1
  184. package/src/components/ui/sidebar/panels/site-panel/stair-tree-node.tsx +2 -2
  185. package/src/components/ui/sidebar/panels/site-panel/tree-node.tsx +112 -39
  186. package/src/components/ui/sidebar/panels/site-panel/tree-structure.ts +36 -0
  187. package/src/components/ui/sidebar/panels/site-panel/wall-tree-node.tsx +1 -1
  188. package/src/components/ui/sidebar/panels/site-panel/window-tree-node.tsx +7 -2
  189. package/src/components/ui/sidebar/panels/site-panel/zone-tree-node.tsx +3 -3
  190. package/src/components/ui/sidebar/panels/zone-panel/index.tsx +51 -2
  191. package/src/components/ui/sidebar/tab-bar.tsx +75 -1
  192. package/src/components/ui/sidebar/use-plugin-panels.tsx +132 -0
  193. package/src/components/ui/snap-target-badge.test.tsx +40 -0
  194. package/src/components/ui/snap-target-badge.tsx +88 -0
  195. package/src/components/viewer-overlay.tsx +253 -61
  196. package/src/components/viewer-zone-system.tsx +7 -2
  197. package/src/hooks/use-auto-save.test.ts +14 -0
  198. package/src/hooks/use-auto-save.ts +64 -10
  199. package/src/hooks/use-ceiling-events.ts +176 -0
  200. package/src/hooks/use-drag-action.ts +127 -0
  201. package/src/hooks/use-keyboard.ts +493 -95
  202. package/src/hooks/use-selection.ts +64 -0
  203. package/src/index.tsx +451 -6
  204. package/src/lib/active-placement-surface.ts +35 -0
  205. package/src/lib/ceiling-plan-snap.ts +24 -0
  206. package/src/lib/constants.ts +6 -2
  207. package/src/lib/contextual-help.test.ts +112 -0
  208. package/src/lib/contextual-help.ts +144 -0
  209. package/src/lib/continuation.ts +64 -0
  210. package/src/lib/direct-manipulation.test.ts +192 -0
  211. package/src/lib/direct-manipulation.ts +109 -0
  212. package/src/lib/editor-api.ts +50 -0
  213. package/src/lib/elevator-support.ts +96 -0
  214. package/src/lib/floorplan/apply-alignment.test.ts +25 -0
  215. package/src/lib/floorplan/apply-alignment.ts +117 -0
  216. package/src/lib/floorplan/floorplan-export.tsx +364 -0
  217. package/src/lib/floorplan/geometry.ts +53 -0
  218. package/src/lib/floorplan/index.ts +10 -0
  219. package/src/lib/floorplan/items.ts +5 -2
  220. package/src/lib/floorplan/plan-coords.ts +21 -0
  221. package/src/lib/floorplan/selection-tool.ts +40 -0
  222. package/src/lib/floorplan/stairs.ts +12 -1
  223. package/src/lib/fresh-planar-placement.test.ts +339 -0
  224. package/src/lib/fresh-planar-placement.ts +130 -0
  225. package/src/lib/glb-export.test.ts +441 -0
  226. package/src/lib/glb-export.ts +874 -0
  227. package/src/lib/history.ts +11 -1
  228. package/src/lib/interaction/hot-set.test.ts +133 -0
  229. package/src/lib/interaction/hot-set.ts +67 -0
  230. package/src/lib/interaction/overlay-policy.test.ts +51 -0
  231. package/src/lib/interaction/overlay-policy.ts +59 -0
  232. package/src/lib/interaction/scope.ts +211 -0
  233. package/src/lib/level-duplication.ts +54 -42
  234. package/src/lib/level-selection.ts +2 -2
  235. package/src/lib/material-paint.ts +227 -48
  236. package/src/lib/measurement-kind.test.ts +30 -0
  237. package/src/lib/measurement-kind.ts +19 -0
  238. package/src/lib/measurement-label.test.ts +47 -0
  239. package/src/lib/measurement-label.ts +68 -0
  240. package/src/lib/measurement-parser.ts +116 -0
  241. package/src/lib/measurements.test.ts +183 -0
  242. package/src/lib/measurements.ts +199 -0
  243. package/src/lib/paint-scope.test.ts +454 -0
  244. package/src/lib/paint-scope.ts +461 -0
  245. package/src/lib/placement-drag-release.ts +23 -0
  246. package/src/lib/placement-metadata.ts +29 -0
  247. package/src/lib/planar-cursor-placement.test.ts +100 -0
  248. package/src/lib/planar-cursor-placement.ts +42 -0
  249. package/src/lib/plugin-panels.test.ts +19 -0
  250. package/src/lib/plugin-panels.ts +91 -0
  251. package/src/lib/quick-measurement.test.ts +77 -0
  252. package/src/lib/quick-measurement.ts +109 -0
  253. package/src/lib/roof-duplication.ts +7 -7
  254. package/src/lib/roof-hover-outline-proxy.ts +22 -0
  255. package/src/lib/roof-wall-hit.ts +164 -0
  256. package/src/lib/scene-clipboard.test.ts +196 -0
  257. package/src/lib/scene-clipboard.ts +332 -0
  258. package/src/lib/scene.ts +40 -8
  259. package/src/lib/selection-routing.test.ts +298 -0
  260. package/src/lib/selection-routing.ts +175 -0
  261. package/src/lib/sfx/index.ts +1 -0
  262. package/src/lib/sfx/movement-tick.test.ts +65 -0
  263. package/src/lib/sfx/movement-tick.ts +18 -0
  264. package/src/lib/sfx-bus.ts +69 -10
  265. package/src/lib/sfx-player.test.ts +124 -0
  266. package/src/lib/sfx-player.ts +148 -55
  267. package/src/lib/site-boundary.ts +1 -0
  268. package/src/lib/slab-plan-snap.test.ts +93 -0
  269. package/src/lib/slab-plan-snap.ts +108 -0
  270. package/src/lib/snapping-mode.test.ts +138 -0
  271. package/src/lib/snapping-mode.ts +179 -0
  272. package/src/lib/stair-duplication.ts +4 -4
  273. package/src/lib/stair-levels.test.ts +146 -0
  274. package/src/lib/stair-levels.ts +177 -0
  275. package/src/lib/surface-plan-snap.test.ts +71 -0
  276. package/src/lib/surface-plan-snap.ts +256 -0
  277. package/src/lib/use-linear-display.ts +40 -0
  278. package/src/lib/world-grid-snap.ts +231 -0
  279. package/src/lib/zone-content.ts +130 -0
  280. package/src/store/use-alignment-guides.ts +21 -0
  281. package/src/store/use-direct-manipulation-feedback.ts +20 -0
  282. package/src/store/use-editor.tsx +767 -114
  283. package/src/store/use-facing-pose.ts +44 -0
  284. package/src/store/use-fence-curve-draft.ts +21 -0
  285. package/src/store/use-floorplan-draft-preview.ts +101 -0
  286. package/src/store/use-floorplan-marquee.ts +50 -0
  287. package/src/store/use-interaction-scope.test.ts +186 -0
  288. package/src/store/use-interaction-scope.ts +132 -0
  289. package/src/store/use-measurement-draft.test.ts +530 -0
  290. package/src/store/use-measurement-draft.ts +543 -0
  291. package/src/store/use-opening-guides.ts +41 -0
  292. package/src/store/use-placement-preview.ts +39 -0
  293. package/src/store/use-quick-measurement-hud.test.ts +53 -0
  294. package/src/store/use-quick-measurement-hud.ts +77 -0
  295. package/src/store/use-segment-draft-chain.ts +28 -0
  296. package/src/store/use-stair-build-preview.ts +43 -0
  297. package/src/store/use-wall-move-ghosts.ts +36 -0
  298. package/src/store/use-wall-snap-indicator.ts +34 -0
  299. package/src/components/editor/first-person/bvh-ecctrl.tsx +0 -860
  300. package/src/components/editor/preset-thumbnail-generator.tsx +0 -125
  301. package/src/components/editor-2d/renderers/floorplan-roof-layer.tsx +0 -113
  302. package/src/components/tools/building/move-building-tool.tsx +0 -157
  303. package/src/components/tools/ceiling/ceiling-boundary-editor.tsx +0 -43
  304. package/src/components/tools/ceiling/ceiling-hole-editor.tsx +0 -49
  305. package/src/components/tools/ceiling/ceiling-tool.tsx +0 -465
  306. package/src/components/tools/ceiling/move-ceiling-tool.tsx +0 -264
  307. package/src/components/tools/column/column-tool.tsx +0 -97
  308. package/src/components/tools/column/move-column-tool.tsx +0 -105
  309. package/src/components/tools/door/door-math.ts +0 -110
  310. package/src/components/tools/door/door-tool.tsx +0 -324
  311. package/src/components/tools/door/move-door-tool.tsx +0 -412
  312. package/src/components/tools/fence/curve-fence-tool.tsx +0 -178
  313. package/src/components/tools/fence/fence-tool.tsx +0 -346
  314. package/src/components/tools/fence/move-fence-endpoint-tool.tsx +0 -441
  315. package/src/components/tools/fence/move-fence-tool.tsx +0 -302
  316. package/src/components/tools/item/item-tool.tsx +0 -26
  317. package/src/components/tools/roof/move-roof-tool.tsx +0 -364
  318. package/src/components/tools/slab/move-slab-tool.tsx +0 -182
  319. package/src/components/tools/slab/slab-boundary-editor.tsx +0 -43
  320. package/src/components/tools/slab/slab-hole-editor.tsx +0 -49
  321. package/src/components/tools/slab/slab-tool.tsx +0 -322
  322. package/src/components/tools/spawn/move-spawn-tool.tsx +0 -101
  323. package/src/components/tools/spawn/spawn-tool.tsx +0 -130
  324. package/src/components/tools/wall/curve-wall-tool.tsx +0 -178
  325. package/src/components/tools/wall/move-wall-endpoint-tool.tsx +0 -426
  326. package/src/components/tools/wall/move-wall-tool.tsx +0 -358
  327. package/src/components/tools/wall/wall-tool.tsx +0 -332
  328. package/src/components/tools/window/move-window-tool.tsx +0 -447
  329. package/src/components/tools/window/window-math.ts +0 -117
  330. package/src/components/tools/window/window-tool.tsx +0 -332
  331. package/src/components/ui/helpers/ceiling-helper.tsx +0 -20
  332. package/src/components/ui/helpers/slab-helper.tsx +0 -20
  333. package/src/components/ui/helpers/wall-helper.tsx +0 -20
  334. package/src/components/ui/panels/ceiling-panel.tsx +0 -249
  335. package/src/components/ui/panels/column-panel.tsx +0 -759
  336. package/src/components/ui/panels/door-panel.tsx +0 -1299
  337. package/src/components/ui/panels/fence-panel.tsx +0 -222
  338. package/src/components/ui/panels/item-panel.tsx +0 -306
  339. package/src/components/ui/panels/paint-panel.tsx +0 -163
  340. package/src/components/ui/panels/presets/presets-popover.tsx +0 -511
  341. package/src/components/ui/panels/roof-panel.tsx +0 -280
  342. package/src/components/ui/panels/roof-segment-panel.tsx +0 -311
  343. package/src/components/ui/panels/slab-panel.tsx +0 -250
  344. package/src/components/ui/panels/spawn-panel.tsx +0 -161
  345. package/src/components/ui/panels/stair-panel.tsx +0 -577
  346. package/src/components/ui/panels/stair-segment-panel.tsx +0 -325
  347. package/src/components/ui/panels/wall-panel.tsx +0 -273
  348. package/src/components/ui/panels/window-panel.tsx +0 -968
  349. package/src/contexts/presets-context.tsx +0 -121
  350. package/src/hooks/use-contextual-tools.ts +0 -61
@@ -0,0 +1,109 @@
1
+ import { beforeAll, beforeEach, describe, expect, test } from 'bun:test'
2
+ import {
3
+ type AnyNode,
4
+ type AnyNodeDefinition,
5
+ nodeRegistry,
6
+ registerNode,
7
+ useScene,
8
+ } from '@pascal-app/core'
9
+ import { useViewer } from '@pascal-app/viewer'
10
+ import { z } from 'zod'
11
+ import useEditor from '../../../store/use-editor'
12
+ import { collectSelectableCandidateIds } from './select-candidates'
13
+
14
+ function registerSelectableElevatorTestKind() {
15
+ if (nodeRegistry.has('elevator')) return
16
+
17
+ registerNode({
18
+ kind: 'elevator',
19
+ schemaVersion: 1,
20
+ schema: z.object({ type: z.literal('elevator') }) as never,
21
+ category: 'structure',
22
+ defaults: () => ({}),
23
+ capabilities: { selectable: {} },
24
+ floorplanScope: 'building',
25
+ renderer: { kind: 'parametric', module: async () => ({ default: () => null }) },
26
+ } as AnyNodeDefinition)
27
+ }
28
+
29
+ describe('selectable candidates', () => {
30
+ beforeAll(() => {
31
+ registerSelectableElevatorTestKind()
32
+ })
33
+
34
+ beforeEach(() => {
35
+ useScene.setState({
36
+ nodes: {},
37
+ rootNodeIds: [],
38
+ dirtyNodes: new Set(),
39
+ collections: {},
40
+ } as never)
41
+ useViewer.setState({
42
+ selection: {
43
+ buildingId: 'building_test',
44
+ levelId: 'level_test',
45
+ zoneId: null,
46
+ selectedIds: [],
47
+ },
48
+ previewSelectedIds: [],
49
+ })
50
+ useEditor.setState({
51
+ phase: 'structure',
52
+ structureLayer: 'elements',
53
+ })
54
+ })
55
+
56
+ test('includes building-scoped elevators for the active level building', () => {
57
+ useScene.setState({
58
+ nodes: {
59
+ building_test: {
60
+ id: 'building_test',
61
+ type: 'building',
62
+ children: ['level_test', 'elevator_test'],
63
+ },
64
+ level_test: {
65
+ id: 'level_test',
66
+ type: 'level',
67
+ parentId: 'building_test',
68
+ children: [],
69
+ },
70
+ elevator_test: {
71
+ id: 'elevator_test',
72
+ type: 'elevator',
73
+ parentId: 'building_test',
74
+ position: [1, 0, 2],
75
+ rotation: 0,
76
+ },
77
+ } as unknown as Record<string, AnyNode>,
78
+ } as never)
79
+
80
+ expect(collectSelectableCandidateIds()).toContain('elevator_test')
81
+ })
82
+
83
+ test('includes legacy level-parented elevators already loaded in the editor', () => {
84
+ useScene.setState({
85
+ nodes: {
86
+ building_test: {
87
+ id: 'building_test',
88
+ type: 'building',
89
+ children: ['level_test'],
90
+ },
91
+ level_test: {
92
+ id: 'level_test',
93
+ type: 'level',
94
+ parentId: 'building_test',
95
+ children: ['elevator_test'],
96
+ },
97
+ elevator_test: {
98
+ id: 'elevator_test',
99
+ type: 'elevator',
100
+ parentId: 'level_test',
101
+ position: [1, 0, 2],
102
+ rotation: 0,
103
+ },
104
+ } as unknown as Record<string, AnyNode>,
105
+ } as never)
106
+
107
+ expect(collectSelectableCandidateIds()).toContain('elevator_test')
108
+ })
109
+ })
@@ -0,0 +1,85 @@
1
+ import {
2
+ type AnyNode,
3
+ type AnyNodeId,
4
+ isRegistrySelectable,
5
+ type LevelNode,
6
+ nodeRegistry,
7
+ resolveBuildingForLevel,
8
+ resolveLevelId,
9
+ useScene,
10
+ } from '@pascal-app/core'
11
+ import { useViewer } from '@pascal-app/viewer'
12
+ import useEditor from '../../../store/use-editor'
13
+
14
+ function isVisibleSelectableNode(node: AnyNode): boolean {
15
+ if ((node as { visible?: boolean }).visible === false) return false
16
+ return isRegistrySelectable(node.type)
17
+ }
18
+
19
+ export function collectSelectableCandidateIds(): string[] {
20
+ const { levelId } = useViewer.getState().selection
21
+ const { nodes } = useScene.getState()
22
+ const { phase, structureLayer } = useEditor.getState()
23
+ const result: string[] = []
24
+ const seen = new Set<string>()
25
+ const addNode = (node: AnyNode | undefined) => {
26
+ if (!node || seen.has(node.id) || (node as { visible?: boolean }).visible === false) return
27
+ seen.add(node.id)
28
+ result.push(node.id)
29
+ }
30
+ const visitLevelDescendant = (id: AnyNodeId) => {
31
+ const node = nodes[id]
32
+ if (!node || seen.has(node.id) || (node as { visible?: boolean }).visible === false) return
33
+
34
+ if (isRegistrySelectable(node.type)) {
35
+ addNode(node)
36
+ }
37
+
38
+ const children = 'children' in node && Array.isArray(node.children) ? node.children : []
39
+ for (const childId of children) {
40
+ visitLevelDescendant(childId as AnyNodeId)
41
+ }
42
+ }
43
+
44
+ if (phase === 'site') {
45
+ for (const node of Object.values(nodes)) {
46
+ if (node.type === 'building') addNode(node)
47
+ }
48
+ return result
49
+ }
50
+
51
+ if (!levelId) return []
52
+ const levelNode = nodes[levelId as AnyNodeId] as LevelNode | undefined
53
+ if (levelNode?.type !== 'level') return []
54
+
55
+ if (phase === 'structure' && structureLayer === 'zones') {
56
+ for (const childId of levelNode.children) {
57
+ const node = nodes[childId as AnyNodeId]
58
+ if (node?.type === 'zone') addNode(node)
59
+ }
60
+ return result
61
+ }
62
+
63
+ for (const childId of levelNode.children) {
64
+ visitLevelDescendant(childId as AnyNodeId)
65
+ }
66
+
67
+ const buildingId = resolveBuildingForLevel(levelId as AnyNodeId, nodes)
68
+ for (const node of Object.values(nodes)) {
69
+ if (!node || node.type === 'level' || !isVisibleSelectableNode(node)) continue
70
+
71
+ const def = nodeRegistry.get(node.type)
72
+ const isBuildingScoped = def?.floorplanScope === 'building'
73
+ const parentId = (node as { parentId?: AnyNodeId | null }).parentId
74
+ if (isBuildingScoped && buildingId && parentId === buildingId) {
75
+ addNode(node)
76
+ continue
77
+ }
78
+
79
+ if (!isBuildingScoped && resolveLevelId(node, nodes) === levelId) {
80
+ addNode(node)
81
+ }
82
+ }
83
+
84
+ return result
85
+ }
@@ -0,0 +1,30 @@
1
+ import { nodeRegistry } from '@pascal-app/core'
2
+ import { type ComponentType, lazy } from 'react'
3
+
4
+ /**
5
+ * Phase 5 Stage D — runtime lazy-load of a kind's affordance tool.
6
+ *
7
+ * The editor can't statically import from `@pascal-app/nodes` (the
8
+ * nodes package depends on editor — static imports would cycle). The
9
+ * kind declares its drag-affordance components in
10
+ * `def.affordanceTools[<key>]: () => import('./<name>-tool')`; this
11
+ * helper resolves that to a `React.lazy` component at the call site.
12
+ *
13
+ * Returns null when the kind doesn't declare the affordance — callers
14
+ * mount the legacy fallback in that case.
15
+ */
16
+ const lazyToolCache = new WeakMap<() => Promise<unknown>, ComponentType>()
17
+
18
+ export function getRegistryAffordanceTool(
19
+ kind: string,
20
+ affordance: string,
21
+ ): ComponentType<any> | null {
22
+ const def = nodeRegistry.get(kind)
23
+ const loader = def?.affordanceTools?.[affordance]
24
+ if (!loader) return null
25
+ const cached = lazyToolCache.get(loader)
26
+ if (cached) return cached
27
+ const Comp = lazy(loader as () => Promise<{ default: ComponentType<any> }>)
28
+ lazyToolCache.set(loader, Comp as unknown as ComponentType)
29
+ return Comp as unknown as ComponentType<any>
30
+ }
@@ -6,24 +6,45 @@ import { furnishTools } from '../../../components/ui/action-menu/furnish-tools'
6
6
  import { tools } from '../../../components/ui/action-menu/structure-tools'
7
7
  import { EDITOR_LAYER } from '../../../lib/constants'
8
8
  import useEditor from '../../../store/use-editor'
9
+ import useWallSnapIndicator from '../../../store/use-wall-snap-indicator'
9
10
 
10
11
  interface CursorSphereProps extends Omit<ThreeElements['group'], 'ref'> {
11
12
  color?: string
12
13
  depthWrite?: boolean
13
14
  showTooltip?: boolean
14
15
  height?: number
16
+ /**
17
+ * Put the bright marker dot at the TIP of the vertical line (y = height)
18
+ * instead of on the ground ring. Used when the point being placed hangs
19
+ * above the floor (e.g. duct drawn against the ceiling): the dot rides at
20
+ * the cursor / placement point while the line drops to a floor ring that
21
+ * keeps the plan position readable.
22
+ */
23
+ dotAtTip?: boolean
15
24
  /** Custom tooltip content — overrides the auto-detected build tool icon */
16
25
  tooltipContent?: React.ReactNode
17
26
  }
18
27
 
19
28
  export const CursorSphere = forwardRef<Group, CursorSphereProps>(function CursorSphere(
20
- { color = '#818cf8', showTooltip = true, height = 2.5, visible = true, tooltipContent, ...props },
29
+ {
30
+ color = '#818cf8',
31
+ showTooltip = true,
32
+ height = 2.5,
33
+ dotAtTip = false,
34
+ visible = true,
35
+ tooltipContent,
36
+ ...props
37
+ },
21
38
  ref,
22
39
  ) {
23
40
  const tool = useEditor((s) => s.tool)
24
41
  const mode = useEditor((s) => s.mode)
25
42
  const catalogCategory = useEditor((s) => s.catalogCategory)
26
43
  const isFloorplanHovered = useEditor((s) => s.isFloorplanHovered)
44
+ // While a wall snap is active the beacon layer already marks the point
45
+ // (green square at a corner); hide the cursor's ground dot/ring so it
46
+ // doesn't sit on top of that glyph.
47
+ const isSnapping = useWallSnapIndicator((s) => s.point !== null)
27
48
 
28
49
  // Find the icon for the current tool
29
50
  let activeToolConfig = null
@@ -39,32 +60,38 @@ export const CursorSphere = forwardRef<Group, CursorSphereProps>(function Cursor
39
60
 
40
61
  return (
41
62
  <group ref={ref} {...props} visible={isVisible}>
42
- {/* Flat marker on the ground */}
43
- <group rotation={[-Math.PI / 2, 0, 0]}>
44
- {/* Center dot */}
45
- <mesh layers={EDITOR_LAYER} renderOrder={2}>
46
- <circleGeometry args={[0.06, 32]} />
47
- <meshBasicMaterial
48
- color={color}
49
- depthTest={false}
50
- depthWrite={false}
51
- opacity={0.9}
52
- transparent
53
- />
54
- </mesh>
63
+ {/* Flat marker on the ground. The bright center dot moves to the tip
64
+ of the line in `dotAtTip` mode (the placement point hangs above the
65
+ floor), leaving a faint ring here so the plan position stays read. */}
66
+ {!isSnapping && (
67
+ <group rotation={[-Math.PI / 2, 0, 0]}>
68
+ {/* Center dot — at the ground unless the placement point is elevated */}
69
+ {!dotAtTip && (
70
+ <mesh layers={EDITOR_LAYER} renderOrder={2}>
71
+ <circleGeometry args={[0.06, 32]} />
72
+ <meshBasicMaterial
73
+ color={color}
74
+ depthTest={false}
75
+ depthWrite={false}
76
+ opacity={0.9}
77
+ transparent
78
+ />
79
+ </mesh>
80
+ )}
55
81
 
56
- {/* Outer ring / glow */}
57
- <mesh layers={EDITOR_LAYER} renderOrder={2}>
58
- <circleGeometry args={[0.2, 32]} />
59
- <meshBasicMaterial
60
- color={color}
61
- depthTest={false}
62
- depthWrite={false}
63
- opacity={0.25}
64
- transparent
65
- />
66
- </mesh>
67
- </group>
82
+ {/* Outer ring / glow */}
83
+ <mesh layers={EDITOR_LAYER} renderOrder={2}>
84
+ <circleGeometry args={[0.2, 32]} />
85
+ <meshBasicMaterial
86
+ color={color}
87
+ depthTest={false}
88
+ depthWrite={false}
89
+ opacity={dotAtTip ? 0.2 : 0.25}
90
+ transparent
91
+ />
92
+ </mesh>
93
+ </group>
94
+ )}
68
95
 
69
96
  {/* Vertical line */}
70
97
  {height > 0 && (
@@ -80,6 +107,15 @@ export const CursorSphere = forwardRef<Group, CursorSphereProps>(function Cursor
80
107
  </mesh>
81
108
  )}
82
109
 
110
+ {/* Bright marker dot at the tip of the line — the actual placement
111
+ point, riding at the cursor while the line drops to the floor. */}
112
+ {dotAtTip && height > 0 && (
113
+ <mesh layers={EDITOR_LAYER} position={[0, height, 0]} renderOrder={2}>
114
+ <sphereGeometry args={[0.08, 20, 14]} />
115
+ <meshBasicMaterial color={color} depthTest={false} depthWrite={false} />
116
+ </mesh>
117
+ )}
118
+
83
119
  {/* Tool Icon Tooltip at the top of the line */}
84
120
  {isVisible && showTooltip && (activeToolConfig || tooltipContent) && (
85
121
  <Html
@@ -0,0 +1,195 @@
1
+ 'use client'
2
+
3
+ import { type AnyNodeId, resolveFacingIndicator, sceneRegistry, useScene } from '@pascal-app/core'
4
+ import { useEffect, useMemo } from 'react'
5
+ import {
6
+ Box3,
7
+ BoxGeometry,
8
+ EdgesGeometry,
9
+ Matrix4,
10
+ type Mesh,
11
+ type Object3D,
12
+ PlaneGeometry,
13
+ Vector3,
14
+ } from 'three'
15
+ import { distance, smoothstep, uv, vec2 } from 'three/tsl'
16
+ import { LineBasicNodeMaterial, MeshBasicNodeMaterial } from 'three/webgpu'
17
+ import { EDITOR_LAYER } from '../../../lib/constants'
18
+ import useFacingPose from '../../../store/use-facing-pose'
19
+
20
+ const NO_RAYCAST = () => null
21
+
22
+ /** green-500 — matches the item placement box's "placeable" state. */
23
+ const DEFAULT_COLOR = 0x22_c5_5e
24
+
25
+ type LocalBounds = { size: [number, number, number]; center: [number, number, number] }
26
+
27
+ /**
28
+ * The node's bounding box in its OWN, unrotated frame — measured from the
29
+ * rendered geometry so it captures the full extent (base, cap, overhang),
30
+ * not just the declared width/height/depth. World position + rotation are
31
+ * cancelled out (invert the root's world matrix), so the result is stable
32
+ * regardless of where the live drag has moved the node; the caller re-applies
33
+ * the current Y rotation on the group. Returns null when nothing measurable.
34
+ */
35
+ function measureLocalBounds(obj: Object3D): LocalBounds | null {
36
+ obj.updateWorldMatrix(true, true)
37
+ const inverseRoot = new Matrix4().copy(obj.matrixWorld).invert()
38
+ const box = new Box3()
39
+ const meshBox = new Box3()
40
+ const toLocal = new Matrix4()
41
+ let measured = false
42
+ obj.traverse((child) => {
43
+ const mesh = child as Mesh
44
+ if (!mesh.isMesh || !mesh.geometry) return
45
+ if (!mesh.geometry.boundingBox) mesh.geometry.computeBoundingBox()
46
+ const bb = mesh.geometry.boundingBox
47
+ if (!bb) return
48
+ toLocal.copy(inverseRoot).multiply(mesh.matrixWorld)
49
+ meshBox.copy(bb).applyMatrix4(toLocal)
50
+ box.union(meshBox)
51
+ measured = true
52
+ })
53
+ if (!measured || box.isEmpty()) return null
54
+ const size = box.getSize(new Vector3())
55
+ const center = box.getCenter(new Vector3())
56
+ return { size: [size.x, size.y, size.z], center: [center.x, center.y, center.z] }
57
+ }
58
+
59
+ interface DragBoundingBoxProps {
60
+ /** Node whose rendered geometry is measured for the box extents. */
61
+ nodeId: string
62
+ /** Footprint origin on the floor, `[x, 0, z]` — the node's live position. */
63
+ position: [number, number, number]
64
+ /** Y rotation (radians) applied to the box, matching the dragged node. */
65
+ rotationY?: number
66
+ /** Declared `[width, height, depth]`, used until/if the mesh can't be measured. */
67
+ fallbackSize?: [number, number, number]
68
+ /**
69
+ * Hard override for the box extents — wins over both mesh measurement and
70
+ * `fallbackSize`. Use when the rendered mesh contains extras the user
71
+ * wouldn't read as "the thing being dragged" (e.g. an elevator whose mesh
72
+ * includes per-level landings outside the shaft footprint).
73
+ */
74
+ size?: [number, number, number]
75
+ /** Local center of `size`. Defaults to the node origin plus half-height. */
76
+ center?: [number, number, number]
77
+ /** Y center of the box in the node's local frame. Defaults to `size[1] / 2`. */
78
+ centerY?: number
79
+ color?: number
80
+ }
81
+
82
+ /**
83
+ * Footprint box drawn around a node while it is being dragged — the same
84
+ * affordance items get during placement: a wireframe cube spanning the node's
85
+ * full measured extent plus a ground plane with a radial opacity gradient
86
+ * (transparent in the centre, opaque toward the edges). Overlay layer +
87
+ * `depthTest: false` keep it drawn on top of scene geometry throughout the
88
+ * drag, and the box visualises the bounds that drive alignment snapping.
89
+ */
90
+ export function DragBoundingBox({
91
+ nodeId,
92
+ position,
93
+ rotationY = 0,
94
+ fallbackSize = [0, 0, 0],
95
+ size,
96
+ center,
97
+ centerY,
98
+ color = DEFAULT_COLOR,
99
+ }: DragBoundingBoxProps) {
100
+ const nodeType = useScene((state) => state.nodes[nodeId as AnyNodeId]?.type)
101
+ const facing = nodeType ? resolveFacingIndicator(nodeType) : null
102
+
103
+ const measured = useMemo(() => {
104
+ if (size) return null
105
+ const obj = sceneRegistry.nodes.get(nodeId)
106
+ return obj ? measureLocalBounds(obj) : null
107
+ }, [nodeId, size])
108
+
109
+ const [w, h, d] = size ?? measured?.size ?? fallbackSize
110
+ const [cx, cy, cz] = size
111
+ ? (center ?? [0, centerY ?? size[1] / 2, 0])
112
+ : (measured?.center ?? [0, fallbackSize[1] / 2, 0])
113
+ const minY = cy - h / 2
114
+ const groundY = minY + 0.01
115
+
116
+ const edgeGeometry = useMemo(() => {
117
+ const box = new BoxGeometry(w, h, d)
118
+ const edges = new EdgesGeometry(box)
119
+ box.dispose()
120
+ return edges
121
+ }, [w, h, d])
122
+
123
+ // Flat on the ground (XZ) at the box's base, nudged up 0.01m to avoid
124
+ // z-fighting with slabs.
125
+ const planeGeometry = useMemo(() => {
126
+ const plane = new PlaneGeometry(w, d)
127
+ plane.rotateX(-Math.PI / 2)
128
+ plane.translate(cx, groundY, cz)
129
+ return plane
130
+ }, [w, d, cx, groundY, cz])
131
+
132
+ const edgeMaterial = useMemo(
133
+ () => new LineBasicNodeMaterial({ color, linewidth: 3, depthTest: false, depthWrite: false }),
134
+ [color],
135
+ )
136
+
137
+ const planeMaterial = useMemo(() => {
138
+ const material = new MeshBasicNodeMaterial({
139
+ color,
140
+ transparent: true,
141
+ depthTest: false,
142
+ depthWrite: false,
143
+ })
144
+ material.opacityNode = smoothstep(0, 0.7, distance(uv(), vec2(0.5, 0.5))).mul(0.6)
145
+ return material
146
+ }, [color])
147
+
148
+ useEffect(
149
+ () => () => {
150
+ edgeGeometry.dispose()
151
+ planeGeometry.dispose()
152
+ edgeMaterial.dispose()
153
+ planeMaterial.dispose()
154
+ },
155
+ [edgeGeometry, planeGeometry, edgeMaterial, planeMaterial],
156
+ )
157
+
158
+ // Publish the facing pose to the editor-side overlay (the single triangle
159
+ // renderer) rather than drawing it here. The node origin is `position`; the
160
+ // footprint centre is `[cx, cz]` in the node's local frame. Runs each drag
161
+ // frame so the triangle follows; a separate mount/unmount effect clears it.
162
+ useEffect(() => {
163
+ if (!facing || d <= 0) return
164
+ useFacingPose.getState().set({
165
+ position: [position[0], position[1] + groundY, position[2]],
166
+ rotationY,
167
+ depth: d,
168
+ center: [cx, cz],
169
+ reversed: facing.reversed,
170
+ })
171
+ }, [facing, position, rotationY, d, cx, cz, groundY])
172
+ useEffect(() => () => useFacingPose.getState().clear(), [])
173
+
174
+ if (w <= 0 || h <= 0 || d <= 0) return null
175
+
176
+ return (
177
+ <group position={position} rotation={[0, rotationY, 0]}>
178
+ <mesh
179
+ geometry={planeGeometry}
180
+ layers={EDITOR_LAYER}
181
+ material={planeMaterial}
182
+ raycast={NO_RAYCAST}
183
+ renderOrder={999}
184
+ />
185
+ <lineSegments
186
+ geometry={edgeGeometry}
187
+ layers={EDITOR_LAYER}
188
+ material={edgeMaterial}
189
+ position={[cx, cy, cz]}
190
+ raycast={NO_RAYCAST}
191
+ renderOrder={999}
192
+ />
193
+ </group>
194
+ )
195
+ }
@@ -0,0 +1,87 @@
1
+ import { useEffect, useMemo } from 'react'
2
+ import { BufferGeometry, DoubleSide, Float32BufferAttribute } from 'three'
3
+ import { MeshBasicNodeMaterial } from 'three/webgpu'
4
+ import { EDITOR_LAYER } from '../../../lib/constants'
5
+
6
+ // A flat forward-pointing triangle drawn on the floor just in front of a
7
+ // placement ghost, so the direction the node will face is obvious. Tip at local
8
+ // +Z (every kind's forward face); render it inside the ghost's rotated group so
9
+ // it inherits the node's yaw. Matches the item coordinator / drag-bounding-box
10
+ // indicators (same size, colour, double-sided so it shows from above).
11
+ const FACING_INDICATOR_WIDTH = 0.4
12
+ const FACING_INDICATOR_LENGTH = 0.46
13
+ const FACING_INDICATOR_GAP = 0.45
14
+
15
+ /**
16
+ * @param depth bbox depth (along local Z) of the ghost — positions the
17
+ * triangle just past the front edge.
18
+ * @param center optional [x, z] of the bbox centre in the ghost's local frame.
19
+ * @param reversed point along local -Z (the front is the -Z side, e.g. a stair
20
+ * entry) instead of +Z.
21
+ * @param y small lift off the floor to avoid z-fighting.
22
+ */
23
+ export function FacingIndicator({
24
+ depth,
25
+ center = [0, 0],
26
+ reversed = false,
27
+ y = 0.02,
28
+ }: {
29
+ depth: number
30
+ center?: [number, number]
31
+ reversed?: boolean
32
+ y?: number
33
+ }) {
34
+ const dir = reversed ? -1 : 1
35
+ // Per-instance geometry/material (not module singletons) so this works no
36
+ // matter which package mounts it (the tools live in `nodes`, imported via
37
+ // `@pascal-app/editor`). Disposed on unmount.
38
+ const geometry = useMemo(() => {
39
+ const g = new BufferGeometry()
40
+ g.setAttribute(
41
+ 'position',
42
+ new Float32BufferAttribute(
43
+ [
44
+ 0,
45
+ 0,
46
+ dir * FACING_INDICATOR_LENGTH,
47
+ FACING_INDICATOR_WIDTH / 2,
48
+ 0,
49
+ 0,
50
+ -FACING_INDICATOR_WIDTH / 2,
51
+ 0,
52
+ 0,
53
+ ],
54
+ 3,
55
+ ),
56
+ )
57
+ return g
58
+ }, [dir])
59
+ const material = useMemo(
60
+ () =>
61
+ new MeshBasicNodeMaterial({
62
+ color: 0x22_c5_5e, // green-500 (forward)
63
+ depthTest: false,
64
+ depthWrite: false,
65
+ side: DoubleSide,
66
+ }),
67
+ [],
68
+ )
69
+ useEffect(
70
+ () => () => {
71
+ geometry.dispose()
72
+ material.dispose()
73
+ },
74
+ [geometry, material],
75
+ )
76
+
77
+ return (
78
+ <mesh
79
+ frustumCulled={false}
80
+ geometry={geometry}
81
+ layers={EDITOR_LAYER}
82
+ material={material}
83
+ position={[center[0], y, center[1] + dir * (depth / 2 + FACING_INDICATOR_GAP)]}
84
+ renderOrder={1001}
85
+ />
86
+ )
87
+ }
@@ -0,0 +1,54 @@
1
+ import { useEffect, useRef, useState } from 'react'
2
+ import type { Group } from 'three'
3
+ import useFacingPose, { type FacingPose } from '../../../store/use-facing-pose'
4
+ import { FacingIndicator } from './facing-indicator'
5
+
6
+ // The single editor-side renderer for the placement/move facing triangle.
7
+ // Mounted once inside ToolManager's building-local group; every tool publishes
8
+ // its ghost pose to `useFacingPose` and this draws the triangle. The pose
9
+ // (position/yaw) is applied imperatively to a ref so the per-frame cursor
10
+ // updates don't re-render React — only a change in footprint shape (depth /
11
+ // centre), which is constant per tool session, triggers a re-render.
12
+ export function FacingPoseIndicator() {
13
+ const groupRef = useRef<Group>(null)
14
+ const [shape, setShape] = useState<Pick<FacingPose, 'depth' | 'center' | 'reversed'> | null>(null)
15
+
16
+ useEffect(() => {
17
+ const apply = (pose: FacingPose | null) => {
18
+ const group = groupRef.current
19
+ if (group) {
20
+ if (pose) {
21
+ group.visible = true
22
+ group.position.set(...pose.position)
23
+ group.rotation.y = pose.rotationY
24
+ } else {
25
+ group.visible = false
26
+ }
27
+ }
28
+ setShape((prev) => {
29
+ if (!pose) return null
30
+ const center = pose.center ?? [0, 0]
31
+ if (
32
+ prev &&
33
+ prev.depth === pose.depth &&
34
+ prev.reversed === pose.reversed &&
35
+ (prev.center ?? [0, 0])[0] === center[0] &&
36
+ (prev.center ?? [0, 0])[1] === center[1]
37
+ ) {
38
+ return prev
39
+ }
40
+ return { depth: pose.depth, center, reversed: pose.reversed }
41
+ })
42
+ }
43
+ apply(useFacingPose.getState().pose)
44
+ return useFacingPose.subscribe((state) => apply(state.pose))
45
+ }, [])
46
+
47
+ return (
48
+ <group ref={groupRef} visible={false}>
49
+ {shape ? (
50
+ <FacingIndicator center={shape.center} depth={shape.depth} reversed={shape.reversed} />
51
+ ) : null}
52
+ </group>
53
+ )
54
+ }