@pascal-app/editor 0.9.1 → 1.0.0-beta.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 (366) hide show
  1. package/package.json +14 -9
  2. package/src/components/editor/alignment-3d-guide-layer.tsx +10 -2
  3. package/src/components/editor/bake-exporter.tsx +49 -0
  4. package/src/components/editor/bake-thumbnail.tsx +95 -0
  5. package/src/components/editor/camera-dragging-lifecycle.test.ts +49 -0
  6. package/src/components/editor/camera-dragging-lifecycle.ts +41 -0
  7. package/src/components/editor/custom-camera-controls.tsx +332 -199
  8. package/src/components/editor/delete-confirmation-dialog.tsx +54 -0
  9. package/src/components/editor/editor-layout-mobile.tsx +5 -0
  10. package/src/components/editor/editor-layout-v2.tsx +18 -0
  11. package/src/components/editor/elevation-3d-guide-layer.tsx +120 -0
  12. package/src/components/editor/export-manager.tsx +71 -63
  13. package/src/components/editor/fence-tangent-lines-3d.tsx +87 -0
  14. package/src/components/editor/first-person/build-collider-world.test.ts +102 -5
  15. package/src/components/editor/first-person/build-collider-world.ts +52 -5
  16. package/src/components/editor/first-person/terrain-collider.test.ts +132 -0
  17. package/src/components/editor/first-person/terrain-collider.ts +212 -0
  18. package/src/components/editor/first-person-controls.tsx +410 -136
  19. package/src/components/editor/floating-action-menu.tsx +435 -64
  20. package/src/components/editor/floating-building-action-menu.tsx +1 -1
  21. package/src/components/editor/floorplan-background-selection.test.ts +72 -0
  22. package/src/components/editor/floorplan-background-selection.ts +7 -20
  23. package/src/components/editor/floorplan-camera-sync.test.ts +249 -0
  24. package/src/components/editor/floorplan-camera-sync.ts +213 -0
  25. package/src/components/editor/floorplan-mode-coordinator.tsx +82 -0
  26. package/src/components/editor/floorplan-navigation-presentation.test.ts +142 -0
  27. package/src/components/editor/floorplan-navigation-presentation.ts +156 -0
  28. package/src/components/editor/floorplan-panel.tsx +2517 -1492
  29. package/src/components/editor/grid.tsx +166 -39
  30. package/src/components/editor/group-actions.ts +597 -0
  31. package/src/components/editor/group-floating-action-menu.tsx +130 -0
  32. package/src/components/editor/group-move-3d.ts +397 -0
  33. package/src/components/editor/group-rotate-handle.tsx +102 -64
  34. package/src/components/editor/group-selection-box-3d.tsx +173 -0
  35. package/src/components/editor/group-transform-shared.test.ts +192 -1
  36. package/src/components/editor/group-transform-shared.ts +240 -7
  37. package/src/components/editor/handles/handle-arrow.tsx +81 -23
  38. package/src/components/editor/handles/handle-drag-history.test.ts +96 -0
  39. package/src/components/editor/handles/handle-drag-history.ts +14 -0
  40. package/src/components/editor/handles/preview-overrides.test.ts +26 -0
  41. package/src/components/editor/handles/preview-overrides.ts +13 -0
  42. package/src/components/editor/handles/resize-snap.test.ts +52 -0
  43. package/src/components/editor/handles/resize-snap.ts +23 -0
  44. package/src/components/editor/handles/use-handle-drag.ts +49 -6
  45. package/src/components/editor/index.tsx +289 -35
  46. package/src/components/editor/measurement-pill.tsx +68 -23
  47. package/src/components/editor/node-action-menu.tsx +14 -1
  48. package/src/components/editor/node-arrow-handles.tsx +409 -180
  49. package/src/components/editor/opening-guides-3d-layer.tsx +144 -0
  50. package/src/components/editor/quick-measurement-card.tsx +76 -0
  51. package/src/components/editor/quick-measurement-hud.tsx +34 -0
  52. package/src/components/editor/riser-diagram-panel.tsx +137 -0
  53. package/src/components/editor/selection-manager.tsx +814 -466
  54. package/src/components/editor/site-edge-labels.tsx +28 -6
  55. package/src/components/editor/slab-hole-highlights.tsx +13 -5
  56. package/src/components/editor/snapshot-capture-overlay.tsx +258 -115
  57. package/src/components/editor/three-context-bridge.ts +22 -0
  58. package/src/components/editor/thumbnail-generator.tsx +118 -253
  59. package/src/components/editor/use-floorplan-background-placement.ts +113 -45
  60. package/src/components/editor/use-floorplan-hit-testing.test.ts +82 -0
  61. package/src/components/editor/use-floorplan-hit-testing.ts +144 -161
  62. package/src/components/editor/use-floorplan-scene-data.ts +5 -5
  63. package/src/components/editor/use-mesh-settle-epoch.ts +27 -0
  64. package/src/components/editor/wall-measurement-label.tsx +10 -28
  65. package/src/components/editor/wall-move-side-handles.tsx +229 -39
  66. package/src/components/editor/wall-opening-highlights.tsx +63 -32
  67. package/src/components/editor/wall-snap-beacon-layer.tsx +162 -6
  68. package/src/components/editor-2d/floorplan-action-menu-layer.tsx +9 -5
  69. package/src/components/editor-2d/floorplan-alignment-guide-layer.tsx +2 -1
  70. package/src/components/editor-2d/floorplan-cursor-indicator-overlay.tsx +40 -9
  71. package/src/components/editor-2d/floorplan-cursor-indicator-position.test.ts +31 -0
  72. package/src/components/editor-2d/floorplan-cursor-indicator-position.ts +39 -0
  73. package/src/components/editor-2d/floorplan-group-action-menu.tsx +87 -0
  74. package/src/components/editor-2d/floorplan-group-move.tsx +744 -0
  75. package/src/components/editor-2d/floorplan-measurement-tool-layer.test.ts +113 -0
  76. package/src/components/editor-2d/floorplan-measurement-tool-layer.tsx +1672 -0
  77. package/src/components/editor-2d/floorplan-quick-measure-layer.tsx +212 -0
  78. package/src/components/editor-2d/floorplan-registered-tool-layer.tsx +72 -0
  79. package/src/components/editor-2d/floorplan-registry-action-menu.tsx +219 -20
  80. package/src/components/editor-2d/floorplan-registry-move-overlay.tsx +327 -59
  81. package/src/components/editor-2d/floorplan-render-context.test.ts +27 -0
  82. package/src/components/editor-2d/floorplan-render-context.tsx +113 -8
  83. package/src/components/editor-2d/floorplan-snap-beacon-layer.tsx +5 -3
  84. package/src/components/editor-2d/renderers/floorplan-annotation-layout.test.ts +285 -0
  85. package/src/components/editor-2d/renderers/floorplan-annotation-layout.ts +516 -0
  86. package/src/components/editor-2d/renderers/floorplan-dimension-renderer.test.tsx +236 -0
  87. package/src/components/editor-2d/renderers/floorplan-dimension-renderer.tsx +618 -0
  88. package/src/components/editor-2d/renderers/floorplan-geometry-renderer.test.tsx +369 -0
  89. package/src/components/editor-2d/renderers/floorplan-geometry-renderer.tsx +431 -32
  90. package/src/components/editor-2d/renderers/floorplan-label-angle.test.ts +89 -0
  91. package/src/components/editor-2d/renderers/floorplan-label-angle.ts +106 -0
  92. package/src/components/editor-2d/renderers/floorplan-placement-preview-layer.tsx +94 -26
  93. package/src/components/editor-2d/renderers/floorplan-registry-layer.test.ts +587 -0
  94. package/src/components/editor-2d/renderers/floorplan-registry-layer.tsx +2280 -578
  95. package/src/components/editor-2d/renderers/floorplan-stair-layer.tsx +20 -10
  96. package/src/components/editor-2d/renderers/floorplan-voronoi-layer.tsx +128 -0
  97. package/src/components/systems/ceiling/ceiling-selection-affordance-system.tsx +425 -61
  98. package/src/components/systems/ceiling/ceiling-system.tsx +97 -4
  99. package/src/components/systems/roof/roof-edit-system.tsx +1768 -13
  100. package/src/components/systems/selection-affordance-manager.tsx +38 -0
  101. package/src/components/systems/zone/zone-label-editor-system.tsx +14 -1
  102. package/src/components/systems/zone/zone-system.tsx +42 -13
  103. package/src/components/tools/elevator/elevator-tool.tsx +34 -8
  104. package/src/components/tools/elevator/move-elevator-tool.tsx +13 -2
  105. package/src/components/tools/fence/fence-drafting.ts +107 -8
  106. package/src/components/tools/item/move-tool.tsx +17 -13
  107. package/src/components/tools/item/placement-math.test.ts +13 -1
  108. package/src/components/tools/item/placement-math.ts +28 -2
  109. package/src/components/tools/item/placement-strategies.ts +246 -5
  110. package/src/components/tools/item/placement-types.ts +13 -1
  111. package/src/components/tools/item/use-draft-node.ts +146 -57
  112. package/src/components/tools/item/use-placement-coordinator.tsx +813 -125
  113. package/src/components/tools/registry/move-registry-node-tool.tsx +636 -103
  114. package/src/components/tools/roof/roof-tool.tsx +336 -94
  115. package/src/components/tools/select/box-select-state.ts +18 -7
  116. package/src/components/tools/select/box-select-tool.tsx +176 -40
  117. package/src/components/tools/select/marquee-geometry.test.ts +161 -0
  118. package/src/components/tools/select/marquee-geometry.ts +155 -0
  119. package/src/components/tools/select/plane-box-select-tool.tsx +1 -8
  120. package/src/components/tools/select/select-candidates.ts +1 -1
  121. package/src/components/tools/shared/cursor-sphere.tsx +62 -26
  122. package/src/components/tools/shared/drag-bounding-box.tsx +28 -4
  123. package/src/components/tools/shared/facing-indicator.tsx +87 -0
  124. package/src/components/tools/shared/facing-pose-indicator.tsx +54 -0
  125. package/src/components/tools/shared/floor-stack-preview.ts +4 -0
  126. package/src/components/tools/shared/horizontal-construction-plane.ts +123 -0
  127. package/src/components/tools/shared/placement-box.tsx +183 -1
  128. package/src/components/tools/shared/pointer-support-cap.test.ts +98 -0
  129. package/src/components/tools/shared/pointer-support-cap.ts +273 -0
  130. package/src/components/tools/shared/polygon-editor.tsx +276 -35
  131. package/src/components/tools/site/site-boundary-editor.tsx +101 -42
  132. package/src/components/tools/site/stroke-pointer-ownership.test.ts +91 -0
  133. package/src/components/tools/site/stroke-pointer-ownership.ts +57 -0
  134. package/src/components/tools/site/terrain-brush-cursor.tsx +247 -0
  135. package/src/components/tools/site/terrain-sculpt-grid.tsx +192 -0
  136. package/src/components/tools/site/terrain-sculpt-tool.tsx +356 -0
  137. package/src/components/tools/stair/stair-click-guard.test.ts +58 -0
  138. package/src/components/tools/stair/stair-click-guard.ts +72 -0
  139. package/src/components/tools/stair/stair-defaults.ts +1 -0
  140. package/src/components/tools/stair/stair-tool.tsx +164 -42
  141. package/src/components/tools/tool-manager.tsx +149 -30
  142. package/src/components/tools/wall/wall-drafting.test.ts +447 -0
  143. package/src/components/tools/wall/wall-drafting.ts +304 -108
  144. package/src/components/tools/wall/wall-snap-geometry.test.ts +52 -0
  145. package/src/components/tools/wall/wall-snap-geometry.ts +137 -9
  146. package/src/components/tools/zone/zone-boundary-editor.tsx +5 -1
  147. package/src/components/tools/zone/zone-tool.tsx +98 -88
  148. package/src/components/ui/action-menu/camera-actions.tsx +24 -17
  149. package/src/components/ui/action-menu/control-modes.tsx +44 -94
  150. package/src/components/ui/action-menu/furnish-tools.tsx +5 -5
  151. package/src/components/ui/action-menu/index.tsx +2 -4
  152. package/src/components/ui/action-menu/measurement-control.tsx +267 -0
  153. package/src/components/ui/action-menu/structure-tools.tsx +22 -13
  154. package/src/components/ui/action-menu/view-toggles.tsx +42 -90
  155. package/src/components/ui/command-palette/editor-commands.tsx +16 -3
  156. package/src/components/ui/command-palette/index.tsx +4 -3
  157. package/src/components/ui/controls/material-paint-panel.tsx +93 -18
  158. package/src/components/ui/controls/material-picker.tsx +162 -152
  159. package/src/components/ui/controls/material-properties-editor.tsx +108 -0
  160. package/src/components/ui/controls/metric-control.tsx +118 -44
  161. package/src/components/ui/controls/scene-material-list.tsx +247 -0
  162. package/src/components/ui/controls/slider-control.tsx +88 -38
  163. package/src/components/ui/controls/terrain-sculpt-panel.tsx +193 -0
  164. package/src/components/ui/floating-level-selector.tsx +74 -20
  165. package/src/components/ui/helpers/building-helper.tsx +10 -23
  166. package/src/components/ui/helpers/contextual-helper-panel.tsx +428 -0
  167. package/src/components/ui/helpers/helper-manager.tsx +273 -14
  168. package/src/components/ui/helpers/item-helper.tsx +28 -32
  169. package/src/components/ui/helpers/registered-tool-helper.tsx +45 -11
  170. package/src/components/ui/helpers/roof-helper.tsx +10 -12
  171. package/src/components/ui/icon-ref.tsx +47 -0
  172. package/src/components/ui/item-catalog/catalog-items.tsx +39 -1
  173. package/src/components/ui/item-catalog/item-catalog.tsx +13 -36
  174. package/src/components/ui/level-duplicate-dialog.tsx +1 -1
  175. package/src/components/ui/panels/multi-selection-panel.tsx +57 -0
  176. package/src/components/ui/panels/node-display.ts +17 -17
  177. package/src/components/ui/panels/panel-manager.tsx +36 -14
  178. package/src/components/ui/panels/panel-wrapper.tsx +24 -3
  179. package/src/components/ui/panels/parametric-inspector.tsx +19 -4
  180. package/src/components/ui/panels/reference-panel.tsx +54 -19
  181. package/src/components/ui/panels/selection-breakdown.test.ts +24 -0
  182. package/src/components/ui/panels/selection-breakdown.ts +20 -0
  183. package/src/components/ui/primitives/shortcut-token.tsx +39 -3
  184. package/src/components/ui/primitives/sidebar.tsx +1 -0
  185. package/src/components/ui/sidebar/app-sidebar.tsx +5 -1
  186. package/src/components/ui/sidebar/icon-rail.tsx +50 -31
  187. package/src/components/ui/sidebar/panels/plugins-panel.tsx +218 -0
  188. package/src/components/ui/sidebar/panels/settings-panel/index.tsx +82 -41
  189. package/src/components/ui/sidebar/panels/settings-panel/keyboard-shortcuts-dialog.tsx +76 -6
  190. package/src/components/ui/sidebar/panels/settings-panel/load-build-dialog.tsx +12 -4
  191. package/src/components/ui/sidebar/panels/site-panel/building-tree-node.tsx +2 -1
  192. package/src/components/ui/sidebar/panels/site-panel/ceiling-tree-node.tsx +4 -3
  193. package/src/components/ui/sidebar/panels/site-panel/chimney-tree-node.tsx +10 -7
  194. package/src/components/ui/sidebar/panels/site-panel/column-tree-node.tsx +1 -1
  195. package/src/components/ui/sidebar/panels/site-panel/door-tree-node.tsx +7 -2
  196. package/src/components/ui/sidebar/panels/site-panel/dormer-tree-node.tsx +10 -7
  197. package/src/components/ui/sidebar/panels/site-panel/elevator-tree-node.tsx +1 -1
  198. package/src/components/ui/sidebar/panels/site-panel/fence-tree-node.tsx +1 -1
  199. package/src/components/ui/sidebar/panels/site-panel/gutter-tree-node.tsx +10 -7
  200. package/src/components/ui/sidebar/panels/site-panel/index.tsx +42 -17
  201. package/src/components/ui/sidebar/panels/site-panel/item-tree-node.tsx +31 -17
  202. package/src/components/ui/sidebar/panels/site-panel/level-tree-node.tsx +1 -1
  203. package/src/components/ui/sidebar/panels/site-panel/registry-tree-node.tsx +72 -25
  204. package/src/components/ui/sidebar/panels/site-panel/roof-tree-node.tsx +7 -3
  205. package/src/components/ui/sidebar/panels/site-panel/shelf-tree-node.tsx +12 -8
  206. package/src/components/ui/sidebar/panels/site-panel/slab-tree-node.tsx +4 -3
  207. package/src/components/ui/sidebar/panels/site-panel/solar-panel-tree-node.tsx +10 -7
  208. package/src/components/ui/sidebar/panels/site-panel/spawn-tree-node.tsx +1 -1
  209. package/src/components/ui/sidebar/panels/site-panel/stair-tree-node.tsx +2 -2
  210. package/src/components/ui/sidebar/panels/site-panel/tree-node.tsx +44 -3
  211. package/src/components/ui/sidebar/panels/site-panel/tree-structure.ts +36 -0
  212. package/src/components/ui/sidebar/panels/site-panel/wall-tree-node.tsx +1 -1
  213. package/src/components/ui/sidebar/panels/site-panel/window-tree-node.tsx +7 -2
  214. package/src/components/ui/sidebar/panels/site-panel/zone-tree-node.tsx +3 -3
  215. package/src/components/ui/sidebar/tab-bar.tsx +42 -28
  216. package/src/components/ui/sidebar/use-plugin-panels.tsx +132 -0
  217. package/src/components/ui/snap-target-badge.test.tsx +40 -0
  218. package/src/components/ui/snap-target-badge.tsx +88 -0
  219. package/src/components/viewer/viewer-controls-bar.tsx +466 -0
  220. package/src/components/viewer/viewer-scene-header.tsx +235 -0
  221. package/src/components/viewer-overlay.tsx +36 -596
  222. package/src/components/viewer-zone-system.tsx +6 -1
  223. package/src/components/walkthrough-hud.tsx +111 -0
  224. package/src/hooks/use-auto-save.test.ts +14 -0
  225. package/src/hooks/use-auto-save.ts +54 -7
  226. package/src/hooks/use-ceiling-events.ts +3 -2
  227. package/src/hooks/use-drag-action.ts +4 -1
  228. package/src/hooks/use-grid-events.ts +24 -0
  229. package/src/hooks/use-keyboard.ts +438 -70
  230. package/src/index.tsx +355 -9
  231. package/src/lib/active-placement-surface.test.ts +24 -0
  232. package/src/lib/active-placement-surface.ts +42 -0
  233. package/src/lib/camera-pose.test.ts +237 -0
  234. package/src/lib/camera-pose.ts +172 -0
  235. package/src/lib/ceiling-plan-snap.ts +24 -0
  236. package/src/lib/contextual-help.test.ts +112 -0
  237. package/src/lib/contextual-help.ts +144 -0
  238. package/src/lib/continuation.ts +64 -0
  239. package/src/lib/direct-manipulation.test.ts +275 -0
  240. package/src/lib/direct-manipulation.ts +118 -0
  241. package/src/lib/door-interaction.ts +1 -1
  242. package/src/lib/editor-api.ts +23 -35
  243. package/src/lib/elevation-guides.test.ts +107 -0
  244. package/src/lib/elevation-guides.ts +236 -0
  245. package/src/lib/floorplan/annotation-visibility.test.ts +327 -0
  246. package/src/lib/floorplan/annotation-visibility.ts +133 -0
  247. package/src/lib/floorplan/apply-alignment.test.ts +25 -0
  248. package/src/lib/floorplan/apply-alignment.ts +18 -12
  249. package/src/lib/floorplan/drawing-coordination.test.ts +46 -0
  250. package/src/lib/floorplan/drawing-coordination.ts +13 -0
  251. package/src/lib/floorplan/floorplan-export.test.ts +319 -0
  252. package/src/lib/floorplan/floorplan-export.tsx +978 -0
  253. package/src/lib/floorplan/floorplan-extension.test.ts +43 -0
  254. package/src/lib/floorplan/floorplan-extension.ts +181 -0
  255. package/src/lib/floorplan/floorplan-mode.test.ts +120 -0
  256. package/src/lib/floorplan/floorplan-mode.ts +73 -0
  257. package/src/lib/floorplan/floorplan-pdfkit-document.ts +194 -0
  258. package/src/lib/floorplan/floorplan-pdfkit-renderer.test.ts +107 -0
  259. package/src/lib/floorplan/floorplan-pdfkit-renderer.ts +680 -0
  260. package/src/lib/floorplan/geometry.ts +52 -0
  261. package/src/lib/floorplan/index.ts +3 -0
  262. package/src/lib/floorplan/items.ts +5 -2
  263. package/src/lib/floorplan/plan-coords.ts +21 -0
  264. package/src/lib/fresh-planar-placement.test.ts +283 -3
  265. package/src/lib/fresh-planar-placement.ts +85 -1
  266. package/src/lib/glb-export.test.ts +556 -0
  267. package/src/lib/glb-export.ts +1071 -0
  268. package/src/lib/ground-surface.ts +95 -0
  269. package/src/lib/history.test.ts +96 -0
  270. package/src/lib/history.ts +31 -0
  271. package/src/lib/interaction/hot-set.test.ts +133 -0
  272. package/src/lib/interaction/hot-set.ts +67 -0
  273. package/src/lib/interaction/overlay-policy.test.ts +52 -0
  274. package/src/lib/interaction/overlay-policy.ts +59 -0
  275. package/src/lib/interaction/scope.ts +243 -0
  276. package/src/lib/level-duplication.test.ts +2 -1
  277. package/src/lib/level-selection.ts +2 -2
  278. package/src/lib/material-paint.ts +36 -49
  279. package/src/lib/measurement-kind.test.ts +30 -0
  280. package/src/lib/measurement-kind.ts +19 -0
  281. package/src/lib/measurement-label.test.ts +47 -0
  282. package/src/lib/measurement-label.ts +68 -0
  283. package/src/lib/measurement-parser.ts +116 -0
  284. package/src/lib/measurements.test.ts +188 -0
  285. package/src/lib/measurements.ts +210 -0
  286. package/src/lib/paint-scope.test.ts +454 -0
  287. package/src/lib/paint-scope.ts +461 -0
  288. package/src/lib/placement-drag-release.ts +23 -0
  289. package/src/lib/planar-cursor-placement.test.ts +57 -0
  290. package/src/lib/plugin-panels.test.ts +19 -0
  291. package/src/lib/plugin-panels.ts +91 -0
  292. package/src/lib/quick-action-feedback.ts +33 -0
  293. package/src/lib/quick-action-nodes.test.ts +94 -0
  294. package/src/lib/quick-action-nodes.ts +47 -0
  295. package/src/lib/quick-measurement.test.ts +77 -0
  296. package/src/lib/quick-measurement.ts +109 -0
  297. package/src/lib/roof-duplication.ts +6 -6
  298. package/src/lib/roof-hover-outline-proxy.ts +22 -0
  299. package/src/lib/roof-wall-hit.ts +164 -0
  300. package/src/lib/scene-clipboard.test.ts +325 -0
  301. package/src/lib/scene-clipboard.ts +310 -21
  302. package/src/lib/scene.ts +28 -5
  303. package/src/lib/selection-routing.test.ts +313 -0
  304. package/src/lib/selection-routing.ts +180 -0
  305. package/src/lib/sfx/index.ts +1 -0
  306. package/src/lib/sfx/movement-tick.test.ts +65 -0
  307. package/src/lib/sfx/movement-tick.ts +18 -0
  308. package/src/lib/sfx-bus.ts +82 -14
  309. package/src/lib/sfx-player.test.ts +266 -0
  310. package/src/lib/sfx-player.ts +262 -69
  311. package/src/lib/site-boundary.test.ts +53 -0
  312. package/src/lib/site-boundary.ts +27 -0
  313. package/src/lib/slab-plan-snap.test.ts +93 -0
  314. package/src/lib/slab-plan-snap.ts +108 -0
  315. package/src/lib/snapping-mode.test.ts +165 -0
  316. package/src/lib/snapping-mode.ts +191 -0
  317. package/src/lib/stair-duplication.test.ts +103 -0
  318. package/src/lib/stair-duplication.ts +58 -86
  319. package/src/lib/stair-levels.ts +2 -0
  320. package/src/lib/surface-plan-snap.test.ts +71 -0
  321. package/src/lib/surface-plan-snap.ts +256 -0
  322. package/src/lib/terrain-sculpt.test.ts +429 -0
  323. package/src/lib/terrain-sculpt.ts +301 -0
  324. package/src/lib/terrain-verb-color.test.ts +51 -0
  325. package/src/lib/terrain-verb-color.ts +58 -0
  326. package/src/lib/touch-gesture-priority.test.ts +45 -0
  327. package/src/lib/touch-gesture-priority.ts +30 -0
  328. package/src/lib/use-linear-display.ts +40 -0
  329. package/src/lib/window-interaction.ts +1 -1
  330. package/src/lib/world-grid-snap.ts +37 -5
  331. package/src/store/camera-pose-store.test.ts +29 -0
  332. package/src/store/camera-pose-store.ts +27 -0
  333. package/src/store/live-draft-preview-stores.test.ts +136 -0
  334. package/src/store/navigation-sync-pose-store.test.ts +34 -0
  335. package/src/store/navigation-sync-pose-store.ts +27 -0
  336. package/src/store/terrain-sculpt-mode.test.ts +223 -0
  337. package/src/store/use-delete-confirmation.ts +27 -0
  338. package/src/store/use-direct-manipulation-feedback.ts +20 -0
  339. package/src/store/use-drawing-view.test.ts +23 -0
  340. package/src/store/use-drawing-view.ts +84 -0
  341. package/src/store/use-editor.tsx +665 -126
  342. package/src/store/use-elevation-guides.ts +40 -0
  343. package/src/store/use-facing-pose.ts +44 -0
  344. package/src/store/use-fence-curve-draft.ts +51 -0
  345. package/src/store/use-first-person-hud.ts +40 -0
  346. package/src/store/use-floorplan-annotation-visibility.ts +60 -0
  347. package/src/store/use-floorplan-draft-preview.ts +151 -0
  348. package/src/store/use-floorplan-marquee.ts +50 -0
  349. package/src/store/use-floorplan-mode.ts +110 -0
  350. package/src/store/use-interaction-scope.test.ts +206 -0
  351. package/src/store/use-interaction-scope.ts +140 -0
  352. package/src/store/use-measurement-draft.test.ts +559 -0
  353. package/src/store/use-measurement-draft.ts +570 -0
  354. package/src/store/use-opening-guides.ts +41 -0
  355. package/src/store/use-path-draft-preview.ts +90 -0
  356. package/src/store/use-placement-preview.ts +11 -3
  357. package/src/store/use-quick-measurement-hud.test.ts +53 -0
  358. package/src/store/use-quick-measurement-hud.ts +77 -0
  359. package/src/store/use-segment-draft-chain.ts +28 -0
  360. package/src/store/use-stair-build-preview.ts +53 -0
  361. package/src/store/use-wall-snap-indicator.ts +2 -0
  362. package/src/components/editor/first-person/bvh-ecctrl.tsx +0 -860
  363. package/src/components/editor/group-move-handle.tsx +0 -316
  364. package/src/components/ui/panels/paint-panel.tsx +0 -163
  365. package/src/lib/floorplan/selection-tool.ts +0 -271
  366. package/src/lib/level-name.ts +0 -11
@@ -2,15 +2,131 @@
2
2
 
3
3
  import '../../../three-types'
4
4
 
5
+ import { Html } from '@react-three/drei'
5
6
  import { useEffect, useMemo } from 'react'
6
7
  import { PlaneGeometry } from 'three'
7
8
  import { distance, smoothstep, uv, vec2 } from 'three/tsl'
8
9
  import { LineBasicNodeMaterial, MeshBasicNodeMaterial } from 'three/webgpu'
9
10
  import { EDITOR_LAYER } from '../../../lib/constants'
11
+ import { formatLinearMeasurement, type LinearUnit } from '../../../lib/measurements'
10
12
  import { createLineGeometry, getBoxEdgePoints } from './placement-box-geometry'
11
13
 
12
14
  const VALID_COLOR = 0x22_c5_5e // green-500
13
15
  const INVALID_COLOR = 0xef_44_44 // red-500
16
+ const MEASUREMENT_COLOR = 0x0f_17_2a
17
+
18
+ type PlacementBoxMeasurements = {
19
+ unit: LinearUnit
20
+ }
21
+
22
+ function getMeasurementGuidePoints(width: number, height: number, depth: number) {
23
+ const minX = -width / 2
24
+ const maxX = width / 2
25
+ const minZ = -depth / 2
26
+ const maxZ = depth / 2
27
+ const guideOffset = 0.18
28
+ const tick = 0.08
29
+ const y = 0.02
30
+
31
+ return {
32
+ depth: [
33
+ maxX + guideOffset,
34
+ y,
35
+ minZ,
36
+ maxX + guideOffset,
37
+ y,
38
+ maxZ,
39
+
40
+ maxX + guideOffset - tick,
41
+ y,
42
+ minZ,
43
+ maxX + guideOffset + tick,
44
+ y,
45
+ minZ,
46
+
47
+ maxX + guideOffset - tick,
48
+ y,
49
+ maxZ,
50
+ maxX + guideOffset + tick,
51
+ y,
52
+ maxZ,
53
+ ],
54
+ height: [
55
+ minX - guideOffset,
56
+ 0,
57
+ minZ,
58
+ minX - guideOffset,
59
+ height,
60
+ minZ,
61
+
62
+ minX - guideOffset - tick,
63
+ 0,
64
+ minZ,
65
+ minX - guideOffset + tick,
66
+ 0,
67
+ minZ,
68
+
69
+ minX - guideOffset - tick,
70
+ height,
71
+ minZ,
72
+ minX - guideOffset + tick,
73
+ height,
74
+ minZ,
75
+ ],
76
+ width: [
77
+ minX,
78
+ y,
79
+ maxZ + guideOffset,
80
+ maxX,
81
+ y,
82
+ maxZ + guideOffset,
83
+
84
+ minX,
85
+ y,
86
+ maxZ + guideOffset - tick,
87
+ minX,
88
+ y,
89
+ maxZ + guideOffset + tick,
90
+
91
+ maxX,
92
+ y,
93
+ maxZ + guideOffset - tick,
94
+ maxX,
95
+ y,
96
+ maxZ + guideOffset + tick,
97
+ ],
98
+ }
99
+ }
100
+
101
+ function MeasurementPill({
102
+ label,
103
+ position,
104
+ }: {
105
+ label: string
106
+ position: [number, number, number]
107
+ }) {
108
+ return (
109
+ <Html center position={position} style={{ pointerEvents: 'none' }}>
110
+ <div
111
+ style={{
112
+ background: 'rgba(15, 23, 42, 0.86)',
113
+ border: '1px solid rgba(15, 23, 42, 0.65)',
114
+ borderRadius: '999px',
115
+ color: '#f8fafc',
116
+ fontFamily: 'ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace',
117
+ fontSize: '11px',
118
+ fontWeight: 600,
119
+ lineHeight: 1,
120
+ padding: '4px 8px',
121
+ pointerEvents: 'none',
122
+ whiteSpace: 'nowrap',
123
+ }}
124
+ >
125
+ {label}
126
+ </div>
127
+ </Html>
128
+ )
129
+ }
14
130
 
15
131
  /**
16
132
  * Green/red placement footprint shown while a node follows the cursor — the
@@ -27,12 +143,15 @@ const INVALID_COLOR = 0xef_44_44 // red-500
27
143
  */
28
144
  export function PlacementBox({
29
145
  dimensions,
146
+ measurements,
30
147
  position,
31
148
  rotationY = 0,
32
149
  valid,
33
150
  }: {
34
151
  /** Footprint extent `[width, height, depth]` (unrotated). */
35
152
  dimensions: [number, number, number]
153
+ /** Optional dimension guide labels matching the GLB item placement cursor. */
154
+ measurements?: PlacementBoxMeasurements
36
155
  /** World-plan position of the footprint centre (floor level). */
37
156
  position: [number, number, number]
38
157
  /** Y-rotation in radians, applied to the whole box. */
@@ -61,6 +180,14 @@ export function PlacementBox({
61
180
  geometry.translate(0, 0.01, 0)
62
181
  return geometry
63
182
  }, [width, depth])
183
+ const measurementGuideGeometries = useMemo(() => {
184
+ const points = getMeasurementGuidePoints(width, height, depth)
185
+ return {
186
+ depth: createLineGeometry(points.depth),
187
+ height: createLineGeometry(points.height),
188
+ width: createLineGeometry(points.width),
189
+ }
190
+ }, [width, height, depth])
64
191
 
65
192
  const edgeMaterial = useMemo(
66
193
  () => new LineBasicNodeMaterial({ linewidth: 3, depthTest: false, depthWrite: false }),
@@ -77,6 +204,16 @@ export function PlacementBox({
77
204
  material.opacityNode = smoothstep(0, 0.7, distance(uv(), vec2(0.5, 0.5))).mul(0.6)
78
205
  return material
79
206
  }, [])
207
+ const measurementMaterial = useMemo(
208
+ () =>
209
+ new LineBasicNodeMaterial({
210
+ color: MEASUREMENT_COLOR,
211
+ linewidth: 2,
212
+ depthTest: false,
213
+ depthWrite: false,
214
+ }),
215
+ [],
216
+ )
80
217
 
81
218
  useEffect(() => {
82
219
  const color = valid ? VALID_COLOR : INVALID_COLOR
@@ -90,6 +227,14 @@ export function PlacementBox({
90
227
  },
91
228
  [edgeGeometry],
92
229
  )
230
+ useEffect(
231
+ () => () => {
232
+ measurementGuideGeometries.depth.dispose()
233
+ measurementGuideGeometries.height.dispose()
234
+ measurementGuideGeometries.width.dispose()
235
+ },
236
+ [measurementGuideGeometries],
237
+ )
93
238
  useEffect(
94
239
  () => () => {
95
240
  basePlaneGeometry.dispose()
@@ -100,10 +245,46 @@ export function PlacementBox({
100
245
  () => () => {
101
246
  edgeMaterial.dispose()
102
247
  basePlaneMaterial.dispose()
248
+ measurementMaterial.dispose()
103
249
  },
104
- [edgeMaterial, basePlaneMaterial],
250
+ [edgeMaterial, basePlaneMaterial, measurementMaterial],
105
251
  )
106
252
 
253
+ const measurementContent = measurements ? (
254
+ <>
255
+ <lineSegments
256
+ geometry={measurementGuideGeometries.width}
257
+ layers={EDITOR_LAYER}
258
+ material={measurementMaterial}
259
+ renderOrder={998}
260
+ />
261
+ <lineSegments
262
+ geometry={measurementGuideGeometries.depth}
263
+ layers={EDITOR_LAYER}
264
+ material={measurementMaterial}
265
+ renderOrder={998}
266
+ />
267
+ <lineSegments
268
+ geometry={measurementGuideGeometries.height}
269
+ layers={EDITOR_LAYER}
270
+ material={measurementMaterial}
271
+ renderOrder={998}
272
+ />
273
+ <MeasurementPill
274
+ label={formatLinearMeasurement(width, measurements.unit)}
275
+ position={[0, 0.04, depth / 2 + 0.24]}
276
+ />
277
+ <MeasurementPill
278
+ label={formatLinearMeasurement(depth, measurements.unit)}
279
+ position={[width / 2 + 0.24, 0.04, 0]}
280
+ />
281
+ <MeasurementPill
282
+ label={formatLinearMeasurement(height, measurements.unit)}
283
+ position={[-width / 2 - 0.24, height / 2, -depth / 2]}
284
+ />
285
+ </>
286
+ ) : null
287
+
107
288
  return (
108
289
  <group position={position} rotation={[0, rotationY, 0]}>
109
290
  <lineSegments
@@ -112,6 +293,7 @@ export function PlacementBox({
112
293
  material={edgeMaterial}
113
294
  renderOrder={999}
114
295
  />
296
+ {measurementContent}
115
297
  <mesh
116
298
  geometry={basePlaneGeometry}
117
299
  layers={EDITOR_LAYER}
@@ -0,0 +1,98 @@
1
+ import { afterEach, beforeEach, describe, expect, test } from 'bun:test'
2
+ import {
3
+ type AnyNode,
4
+ type AnyNodeId,
5
+ sceneRegistry,
6
+ spatialGridManager,
7
+ useScene,
8
+ } from '@pascal-app/core'
9
+ import { useViewer } from '@pascal-app/viewer'
10
+ import { BoxGeometry, Mesh, MeshBasicMaterial, PerspectiveCamera } from 'three'
11
+ import { resolvePointerSupportSurface } from './pointer-support-cap'
12
+
13
+ const LEVEL_ID = 'level_test' as AnyNodeId
14
+ const WALL_ID = 'wall_test' as AnyNodeId
15
+
16
+ describe('resolvePointerSupportSurface node tops', () => {
17
+ beforeEach(() => {
18
+ spatialGridManager.clear()
19
+ sceneRegistry.clear()
20
+ useViewer.setState({
21
+ selection: {
22
+ buildingId: null,
23
+ levelId: LEVEL_ID,
24
+ zoneId: null,
25
+ selectedIds: [],
26
+ },
27
+ } as never)
28
+ useScene.setState({
29
+ nodes: {
30
+ [LEVEL_ID]: {
31
+ id: LEVEL_ID,
32
+ type: 'level',
33
+ object: 'node',
34
+ parentId: null,
35
+ visible: true,
36
+ metadata: {},
37
+ children: [WALL_ID],
38
+ level: 0,
39
+ } as AnyNode,
40
+ [WALL_ID]: {
41
+ id: WALL_ID,
42
+ type: 'wall',
43
+ object: 'node',
44
+ parentId: LEVEL_ID,
45
+ visible: true,
46
+ metadata: {},
47
+ children: [],
48
+ assemblyLayers: [],
49
+ start: [-1, 0],
50
+ end: [1, 0],
51
+ frontSide: 'unknown',
52
+ backSide: 'unknown',
53
+ } as AnyNode,
54
+ },
55
+ })
56
+ })
57
+
58
+ afterEach(() => {
59
+ sceneRegistry.clear()
60
+ })
61
+
62
+ test('prefers the nearest upward-facing registered node surface over the ground', () => {
63
+ const wallMesh = new Mesh(new BoxGeometry(2, 2, 0.2), new MeshBasicMaterial())
64
+ wallMesh.position.y = 1
65
+ wallMesh.updateMatrixWorld(true)
66
+ sceneRegistry.nodes.set(WALL_ID, wallMesh)
67
+ sceneRegistry.byType.wall!.add(WALL_ID)
68
+
69
+ const camera = new PerspectiveCamera()
70
+ camera.position.set(0, 5, 0)
71
+ camera.updateMatrixWorld(true)
72
+
73
+ const support = resolvePointerSupportSurface(camera, [0, 0, 0], {
74
+ includeNodeTopSurfaces: true,
75
+ })
76
+
77
+ expect(support?.sourceNodeId).toBe(WALL_ID)
78
+ expect(support?.elevation).toBeCloseTo(2)
79
+ expect(support?.worldPoint).toEqual([0, 2, 0])
80
+ })
81
+
82
+ test('keeps the ground result when node-top surfaces are not requested', () => {
83
+ const wallMesh = new Mesh(new BoxGeometry(2, 2, 0.2), new MeshBasicMaterial())
84
+ wallMesh.position.y = 1
85
+ wallMesh.updateMatrixWorld(true)
86
+ sceneRegistry.nodes.set(WALL_ID, wallMesh)
87
+ sceneRegistry.byType.wall!.add(WALL_ID)
88
+
89
+ const camera = new PerspectiveCamera()
90
+ camera.position.set(0, 5, 0)
91
+ camera.updateMatrixWorld(true)
92
+
93
+ const support = resolvePointerSupportSurface(camera, [0, 0, 0])
94
+
95
+ expect(support?.sourceNodeId).toBeNull()
96
+ expect(support?.elevation).toBe(0)
97
+ })
98
+ })
@@ -0,0 +1,273 @@
1
+ import {
2
+ type AnyNodeId,
3
+ canHostOnTop,
4
+ GROUND_SUPPORT_ID,
5
+ type ItemNode,
6
+ isLowProfileItemSurface,
7
+ sceneRegistry,
8
+ spatialGridManager,
9
+ useScene,
10
+ } from '@pascal-app/core'
11
+ import { useViewer } from '@pascal-app/viewer'
12
+ import { type Camera, Matrix3, type Object3D, Raycaster, Vector3 } from 'three'
13
+ import { resolveTerrainGroundHit } from '../../../lib/ground-surface'
14
+
15
+ const originScratch = new Vector3()
16
+ const hitScratch = new Vector3()
17
+ const worldScratch = new Vector3()
18
+ const pointScratch = new Vector3()
19
+ const worldRayOrigin = new Vector3()
20
+ const worldRayDirection = new Vector3()
21
+ const nodeTopRaycaster = new Raycaster()
22
+ const nodeTopNormal = new Vector3()
23
+ const nodeTopNormalMatrix = new Matrix3()
24
+
25
+ const NODE_TOP_SURFACE_KINDS = ['wall', 'item', 'column'] as const
26
+
27
+ export type PointerSupportSurface = {
28
+ /** Level-local elevation of the pointed surface — the election cap. */
29
+ elevation: number
30
+ /** Slab host, or the explicit ground sentinel when the level base won. */
31
+ supportSlabId: string | null
32
+ /** Node whose upward-facing geometry supplied this surface, when applicable. */
33
+ sourceNodeId: AnyNodeId | null
34
+ /** World-space Y of the same surface, for grid-plane / preview placement. */
35
+ worldY: number
36
+ /**
37
+ * World-space point where the pointer ray meets the pointed surface's
38
+ * plane, or null when the ray never reaches it. Unlike the grid event's
39
+ * own hit — whose XZ is perspective-skewed whenever the event plane
40
+ * rides at a different storey than the aimed-at surface — this point
41
+ * depends only on the ray and the pointed surface, so a preview /
42
+ * election fed from it cannot flip with the event plane's height.
43
+ */
44
+ worldPoint: [number, number, number] | null
45
+ /** {@link PointerSupportSurface.worldPoint} in the grid event's
46
+ * `localPosition` (building-local) frame — a drop-in replacement for
47
+ * the event's plane-hit XZ. */
48
+ localPoint: [number, number, number] | null
49
+ }
50
+
51
+ /**
52
+ * The walking surface the pointer actually points at: its level-local
53
+ * elevation (for use as the slab-support election cap, `maxElevation`),
54
+ * its world-space Y (for riding the grid event plane / draw preview on
55
+ * it), and the ray's crossing of that surface's plane (the plan point the
56
+ * cursor indicates).
57
+ *
58
+ * The grid event plane rides at the ghost's last height, so its hit point
59
+ * alone can't be trusted (that feedback loop is what made a ghost under an
60
+ * elevated deck blink between the deck top and the ground). But camera →
61
+ * hit reconstructs the true pointer ray regardless of the plane height,
62
+ * and the nearest slab plane that ray crosses inside its rendered polygon
63
+ * IS the surface under the cursor — the deck top when aiming at the deck,
64
+ * the floor/ground when aiming underneath it. The same reasoning applies
65
+ * to the cursor XZ: the event plane's hit is skewed along the ray whenever
66
+ * the plane sits on a different storey than the pointed surface, so
67
+ * callers should place at `localPoint` / `worldPoint`, not the event hit.
68
+ *
69
+ * When the ray crosses no slab, the surface is the level base — and on the
70
+ * storey that sits on the ground, that base is the sculpted terrain rather
71
+ * than a plane, so `elevation` varies with XZ. Everything downstream already
72
+ * treats it as a varying scalar, which is why terrain needs no new axis here.
73
+ *
74
+ * Returns null when no level is active (callers fall back to the
75
+ * uncapped max election and the raw event hit).
76
+ */
77
+ export function resolvePointerSupportSurface(
78
+ camera: Camera,
79
+ worldHit: readonly [number, number, number],
80
+ options?: { includeNodeTopSurfaces?: boolean },
81
+ ): PointerSupportSurface | null {
82
+ const levelId = useViewer.getState().selection.levelId
83
+ if (!levelId) return null
84
+
85
+ // The world ray, kept before the level conversion below: the terrain field is
86
+ // world-space (site geometry, not level-local), so the march needs this frame.
87
+ camera.getWorldPosition(worldRayOrigin)
88
+ worldRayDirection.set(worldHit[0], worldHit[1], worldHit[2]).sub(worldRayOrigin)
89
+
90
+ originScratch.copy(worldRayOrigin)
91
+ hitScratch.set(worldHit[0], worldHit[1], worldHit[2])
92
+ // Slab polygons/elevations live in the level frame; the level mesh
93
+ // carries the storey Y offset and any building rotation.
94
+ const levelMesh = sceneRegistry.nodes.get(levelId as AnyNodeId)
95
+ if (levelMesh) {
96
+ levelMesh.worldToLocal(originScratch)
97
+ levelMesh.worldToLocal(hitScratch)
98
+ }
99
+ hitScratch.sub(originScratch)
100
+ if (hitScratch.lengthSq() < 1e-12) return null
101
+
102
+ const pointed = spatialGridManager.getPointedSupportSurface(
103
+ levelId,
104
+ [originScratch.x, originScratch.y, originScratch.z],
105
+ [hitScratch.x, hitScratch.y, hitScratch.z],
106
+ )
107
+ let elevation = pointed.elevation
108
+ let point = pointed.point
109
+
110
+ // The level base is the second flat plane terrain has to displace (the first is
111
+ // `useGridEvents`'). The manager solves `t = -oy / dy` for it — a plane — which
112
+ // is still right for every storey whose base is a built floor, and wrong for the
113
+ // storey sitting on the ground, where the base IS the terrain.
114
+ //
115
+ // Substituted here rather than inside the manager for two reasons: the
116
+ // discrimination ("is this plane the site ground?") needs the level's *world*
117
+ // height, and this function is what owns the world↔level conversion; and
118
+ // `ground-surface` is meant to hold that rule once, shared with `useGridEvents`,
119
+ // rather than have `core` grow a terrain dependency it cannot express.
120
+ //
121
+ // Only the no-slab branch is replaced. A ray that crossed a slab inside its
122
+ // rendered polygon is aimed at a built floor, and terrain does not compete with
123
+ // one — that is the flat-floor invariant.
124
+ if (pointed.slabId === null) {
125
+ const baseWorldY = levelMesh ? levelMesh.localToWorld(worldScratch.set(0, 0, 0)).y : 0
126
+ const groundHit = resolveTerrainGroundHit(
127
+ [worldRayOrigin.x, worldRayOrigin.y, worldRayOrigin.z],
128
+ [worldRayDirection.x, worldRayDirection.y, worldRayDirection.z],
129
+ baseWorldY,
130
+ )
131
+ if (groundHit) {
132
+ pointScratch.set(groundHit.x, groundHit.y, groundHit.z)
133
+ if (levelMesh) levelMesh.worldToLocal(pointScratch)
134
+ elevation = pointScratch.y
135
+ point = [pointScratch.x, pointScratch.z]
136
+ }
137
+ }
138
+
139
+ const worldY = levelMesh ? levelMesh.localToWorld(worldScratch.set(0, elevation, 0)).y : elevation
140
+
141
+ let worldPoint: [number, number, number] | null = null
142
+ let localPoint: [number, number, number] | null = null
143
+ if (point) {
144
+ pointScratch.set(point[0], elevation, point[1])
145
+ if (levelMesh) levelMesh.localToWorld(pointScratch)
146
+ worldPoint = [pointScratch.x, pointScratch.y, pointScratch.z]
147
+ // Same frame the grid events report `localPosition` in (use-grid-events).
148
+ const buildingId = useViewer.getState().selection.buildingId
149
+ const buildingMesh = buildingId ? sceneRegistry.nodes.get(buildingId as AnyNodeId) : null
150
+ if (buildingMesh) buildingMesh.worldToLocal(pointScratch)
151
+ localPoint = [pointScratch.x, pointScratch.y, pointScratch.z]
152
+ }
153
+
154
+ if (options?.includeNodeTopSurfaces) {
155
+ nodeTopRaycaster.set(worldRayOrigin, worldRayDirection.clone().normalize())
156
+ const nodes = useScene.getState().nodes
157
+ const registeredOwners = new Map(
158
+ [...sceneRegistry.nodes.entries()].map(([nodeId, object]) => [object, nodeId as AnyNodeId]),
159
+ )
160
+ const belongsToActiveLevel = (nodeId: AnyNodeId) => {
161
+ let current = nodes[nodeId]
162
+ const visited = new Set<AnyNodeId>()
163
+ while (current && !visited.has(current.id)) {
164
+ if (current.id === levelId) return true
165
+ visited.add(current.id)
166
+ current = current.parentId ? nodes[current.parentId as AnyNodeId] : undefined
167
+ }
168
+ return false
169
+ }
170
+
171
+ const pointedDistance = worldPoint
172
+ ? worldRayOrigin.distanceTo(pointScratch.set(...worldPoint))
173
+ : Number.POSITIVE_INFINITY
174
+ let nearest:
175
+ | {
176
+ distance: number
177
+ nodeId: AnyNodeId
178
+ point: Vector3
179
+ }
180
+ | undefined
181
+
182
+ for (const kind of NODE_TOP_SURFACE_KINDS) {
183
+ for (const rawId of sceneRegistry.byType[kind] ?? []) {
184
+ const nodeId = rawId as AnyNodeId
185
+ const node = nodes[nodeId]
186
+ const object = sceneRegistry.nodes.get(nodeId)
187
+ if (!(node?.visible && object?.visible && belongsToActiveLevel(nodeId))) continue
188
+ if (
189
+ node.type === 'item' &&
190
+ (!canHostOnTop(node) || isLowProfileItemSurface(node as ItemNode))
191
+ ) {
192
+ continue
193
+ }
194
+
195
+ for (const intersection of nodeTopRaycaster.intersectObject(object, true)) {
196
+ if (
197
+ intersection.distance >= (nearest?.distance ?? pointedDistance) ||
198
+ !intersection.face
199
+ ) {
200
+ continue
201
+ }
202
+ let ownerObject: Object3D | null = intersection.object
203
+ let belongsToNestedNode = false
204
+ while (ownerObject && ownerObject !== object) {
205
+ const ownerId = registeredOwners.get(ownerObject)
206
+ if (ownerId && ownerId !== nodeId) {
207
+ belongsToNestedNode = true
208
+ break
209
+ }
210
+ ownerObject = ownerObject.parent
211
+ }
212
+ if (belongsToNestedNode) continue
213
+ nodeTopNormal
214
+ .copy(intersection.face.normal)
215
+ .applyNormalMatrix(nodeTopNormalMatrix.getNormalMatrix(intersection.object.matrixWorld))
216
+ .normalize()
217
+ if (nodeTopNormal.y < 0.75) continue
218
+ nearest = {
219
+ distance: intersection.distance,
220
+ nodeId,
221
+ point: intersection.point.clone(),
222
+ }
223
+ }
224
+ }
225
+ }
226
+
227
+ if (nearest) {
228
+ const sourceNode = nodes[nearest.nodeId]
229
+ pointScratch.copy(nearest.point)
230
+ const nodeWorldPoint: [number, number, number] = [
231
+ pointScratch.x,
232
+ pointScratch.y,
233
+ pointScratch.z,
234
+ ]
235
+ if (levelMesh) levelMesh.worldToLocal(pointScratch)
236
+ elevation = pointScratch.y
237
+
238
+ pointScratch.copy(nearest.point)
239
+ const buildingId = useViewer.getState().selection.buildingId
240
+ const buildingMesh = buildingId ? sceneRegistry.nodes.get(buildingId as AnyNodeId) : null
241
+ if (buildingMesh) buildingMesh.worldToLocal(pointScratch)
242
+
243
+ return {
244
+ elevation,
245
+ supportSlabId:
246
+ sourceNode && 'supportSlabId' in sourceNode
247
+ ? ((sourceNode.supportSlabId as string | undefined) ?? null)
248
+ : null,
249
+ sourceNodeId: nearest.nodeId,
250
+ worldY: nearest.point.y,
251
+ worldPoint: nodeWorldPoint,
252
+ localPoint: [pointScratch.x, pointScratch.y, pointScratch.z],
253
+ }
254
+ }
255
+ }
256
+
257
+ return {
258
+ elevation,
259
+ supportSlabId: pointed.slabId ?? GROUND_SUPPORT_ID,
260
+ sourceNodeId: null,
261
+ worldY,
262
+ worldPoint,
263
+ localPoint,
264
+ }
265
+ }
266
+
267
+ /** {@link resolvePointerSupportSurface}, elevation only — the election cap. */
268
+ export function resolvePointerSupportElevation(
269
+ camera: Camera,
270
+ worldHit: readonly [number, number, number],
271
+ ): number | null {
272
+ return resolvePointerSupportSurface(camera, worldHit)?.elevation ?? null
273
+ }