@pascal-app/editor 1.0.1 → 1.0.4

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 (498) hide show
  1. package/package.json +16 -9
  2. package/src/components/editor/action-menu-button.tsx +83 -0
  3. package/src/components/editor/bake-thumbnail.tsx +315 -4
  4. package/src/components/editor/capture-level-picker.tsx +72 -0
  5. package/src/components/editor/custom-camera-controls.tsx +221 -128
  6. package/src/components/editor/delete-confirmation-dialog.tsx +297 -7
  7. package/src/components/editor/duplicate-node.ts +244 -0
  8. package/src/components/editor/editor-layout-mobile.tsx +37 -25
  9. package/src/components/editor/editor-layout-v2.tsx +131 -58
  10. package/src/components/editor/export-manager.tsx +34 -8
  11. package/src/components/editor/first-person/build-collider-world.test.ts +51 -0
  12. package/src/components/editor/first-person/build-collider-world.ts +6 -2
  13. package/src/components/editor/first-person/mechanism-targets.test.ts +75 -0
  14. package/src/components/editor/first-person/mechanism-targets.ts +36 -0
  15. package/src/components/editor/first-person-controls.tsx +205 -2
  16. package/src/components/editor/floating-action-menu.tsx +63 -207
  17. package/src/components/editor/floor-aware-camera-controls.test.ts +442 -0
  18. package/src/components/editor/floor-aware-camera-controls.ts +324 -0
  19. package/src/components/editor/floor-openings-overlay.tsx +243 -0
  20. package/src/components/editor/floor-region-controls.tsx +846 -0
  21. package/src/components/editor/floor-region-webgpu-materials.test.ts +12 -0
  22. package/src/components/editor/floor-surface.test.ts +165 -0
  23. package/src/components/editor/floor-surface.ts +182 -0
  24. package/src/components/editor/floorplan-background-selection.test.ts +11 -2
  25. package/src/components/editor/floorplan-background-selection.ts +1 -3
  26. package/src/components/editor/floorplan-panel.tsx +536 -487
  27. package/src/components/editor/footprint-height-handle.tsx +204 -0
  28. package/src/components/editor/grid.tsx +31 -7
  29. package/src/components/editor/group-actions.ts +59 -11
  30. package/src/components/editor/group-floating-action-menu.tsx +19 -9
  31. package/src/components/editor/group-move-3d.ts +4 -6
  32. package/src/components/editor/group-rotate-handle.tsx +81 -35
  33. package/src/components/editor/group-selection-box-3d.tsx +25 -17
  34. package/src/components/editor/group-transform-shared.test.ts +126 -1
  35. package/src/components/editor/group-transform-shared.ts +79 -73
  36. package/src/components/editor/handles/handle-arrow.tsx +72 -2
  37. package/src/components/editor/handles/handle-hit-priority.test.tsx +63 -0
  38. package/src/components/editor/handles/handle-hit-priority.tsx +37 -0
  39. package/src/components/editor/handles/swallow-next-click.test.ts +28 -0
  40. package/src/components/editor/handles/use-handle-drag.ts +272 -131
  41. package/src/components/editor/index.tsx +316 -176
  42. package/src/components/editor/join-walls-pill.tsx +44 -0
  43. package/src/components/editor/node-action-menu.tsx +70 -70
  44. package/src/components/editor/node-arrow-handles.tsx +167 -14
  45. package/src/components/editor/open-wall-ends-3d-layer.tsx +273 -0
  46. package/src/components/editor/open-wall-ends-hint.tsx +89 -0
  47. package/src/components/editor/quick-measurement-hud.tsx +3 -2
  48. package/src/components/editor/registry-action-contributions.test.tsx +63 -0
  49. package/src/components/editor/registry-action-contributions.tsx +132 -0
  50. package/src/components/editor/room-controls.tsx +847 -0
  51. package/src/components/editor/room-floor-highlight.tsx +128 -0
  52. package/src/components/editor/room-handles-2d.tsx +354 -0
  53. package/src/components/editor/room-handles.tsx +675 -0
  54. package/src/components/editor/room-highlight.tsx +225 -0
  55. package/src/components/editor/room-labels.test.ts +357 -0
  56. package/src/components/editor/room-labels.tsx +990 -0
  57. package/src/components/editor/room-transform-ghost.tsx +256 -0
  58. package/src/components/editor/room-webgpu-materials.test.ts +27 -0
  59. package/src/components/editor/selection-manager.tsx +451 -317
  60. package/src/components/editor/slab-hole-highlights.tsx +3 -0
  61. package/src/components/editor/snapshot-capture-overlay.tsx +4 -0
  62. package/src/components/editor/snapshot-capture.test.ts +52 -4
  63. package/src/components/editor/snapshot-capture.ts +30 -5
  64. package/src/components/editor/thumbnail-generator.test.ts +308 -0
  65. package/src/components/editor/thumbnail-generator.tsx +1035 -229
  66. package/src/components/editor/use-floorplan-background-placement.ts +31 -11
  67. package/src/components/editor/vector-edge-extractor.tsx +121 -0
  68. package/src/components/editor/views/host-view.tsx +61 -0
  69. package/src/components/editor/views/plugins-view.tsx +670 -0
  70. package/src/components/editor/views/scene-region.tsx +59 -0
  71. package/src/components/editor/views/use-editor-views.ts +57 -0
  72. package/src/components/editor/views/view-bar.tsx +220 -0
  73. package/src/components/editor/wall-justification.test.ts +140 -0
  74. package/src/components/editor/wall-move-side-handles.tsx +114 -163
  75. package/src/components/editor/wall-opening-highlights.tsx +2 -3
  76. package/src/components/editor/wall-push-arrow.tsx +92 -0
  77. package/src/components/editor/wall-region-handles.tsx +509 -0
  78. package/src/components/editor/wall-snap-beacon-layer.tsx +3 -0
  79. package/src/components/editor-2d/floorplan-cursor-indicator-overlay.tsx +8 -4
  80. package/src/components/editor-2d/floorplan-group-move.tsx +17 -24
  81. package/src/components/editor-2d/floorplan-open-wall-ends-layer.test.tsx +86 -0
  82. package/src/components/editor-2d/floorplan-open-wall-ends-layer.tsx +290 -0
  83. package/src/components/editor-2d/floorplan-registered-tool-layer.tsx +33 -12
  84. package/src/components/editor-2d/floorplan-registry-action-menu.tsx +11 -1
  85. package/src/components/editor-2d/floorplan-registry-move-overlay.tsx +242 -126
  86. package/src/components/editor-2d/renderers/floorplan-dimension-edit-overlay.test.ts +156 -0
  87. package/src/components/editor-2d/renderers/floorplan-dimension-edit-overlay.tsx +278 -0
  88. package/src/components/editor-2d/renderers/floorplan-dimension-renderer.tsx +77 -0
  89. package/src/components/editor-2d/renderers/floorplan-draft-measurement.tsx +166 -0
  90. package/src/components/editor-2d/renderers/floorplan-geometry-renderer.tsx +4 -1
  91. package/src/components/editor-2d/renderers/floorplan-registry-layer.test.ts +87 -8
  92. package/src/components/editor-2d/renderers/floorplan-registry-layer.tsx +355 -245
  93. package/src/components/editor-2d/svg-paths.ts +12 -15
  94. package/src/components/procedural-items/inspector.tsx +40 -15
  95. package/src/components/systems/ceiling/ceiling-selection-affordance-system.test.ts +97 -14
  96. package/src/components/systems/ceiling/ceiling-selection-affordance-system.tsx +18 -18
  97. package/src/components/systems/zone/zone-system.test.ts +47 -0
  98. package/src/components/systems/zone/zone-system.tsx +31 -66
  99. package/src/components/tools/__fixtures__/hosting.ts +10 -0
  100. package/src/components/tools/item/item-surface-placement.test.ts +3 -9
  101. package/src/components/tools/item/item-surface-strategy-resolver.test.ts +3 -9
  102. package/src/components/tools/item/placement-history.test.tsx +474 -0
  103. package/src/components/tools/item/placement-strategies.ts +63 -47
  104. package/src/components/tools/item/shelf-surface-strategy-resolver.test.ts +3 -3
  105. package/src/components/tools/item/use-draft-node.test.tsx +432 -1
  106. package/src/components/tools/item/use-draft-node.ts +207 -97
  107. package/src/components/tools/item/use-placement-coordinator.tsx +251 -111
  108. package/src/components/tools/paint-region/use-paint-tint.ts +37 -0
  109. package/src/components/tools/paint-region/wall-paint-region-tool.tsx +466 -0
  110. package/src/components/tools/registry/item-surface-move.test.ts +10 -2
  111. package/src/components/tools/registry/item-surface-move.ts +46 -24
  112. package/src/components/tools/registry/item-surface-resolver.test.ts +2 -2
  113. package/src/components/tools/registry/move-registry-node-tool.tsx +26 -10
  114. package/src/components/tools/select/box-select-tool.tsx +2 -12
  115. package/src/components/tools/select/plane-box-select-tool.test.tsx +79 -0
  116. package/src/components/tools/select/plane-box-select-tool.tsx +35 -22
  117. package/src/components/tools/select/select-candidates.test.ts +37 -4
  118. package/src/components/tools/select/select-candidates.ts +15 -11
  119. package/src/components/tools/shared/affordance-dispatch.test.ts +24 -0
  120. package/src/components/tools/shared/affordance-dispatch.ts +13 -0
  121. package/src/components/tools/shared/cursor-sphere.tsx +36 -12
  122. package/src/components/tools/shared/draft-measurement-label.tsx +38 -0
  123. package/src/components/tools/shared/pointer-support-cap.test.ts +27 -0
  124. package/src/components/tools/shared/pointer-support-cap.ts +17 -5
  125. package/src/components/tools/shared/surface-rejection.test.tsx +5 -4
  126. package/src/components/tools/shared/surface-rejection.tsx +14 -0
  127. package/src/components/tools/site/site-boundary-editor.tsx +1 -2
  128. package/src/components/tools/stair/stair-defaults.ts +0 -3
  129. package/src/components/tools/stair/stair-tool.tsx +90 -42
  130. package/src/components/tools/tool-manager.tsx +82 -23
  131. package/src/components/tools/wall/wall-drafting.test.ts +19 -4
  132. package/src/components/tools/zone/zone-tool.tsx +4 -12
  133. package/src/components/ui/action-menu/action-button.tsx +3 -0
  134. package/src/components/ui/action-menu/camera-actions.tsx +2 -1
  135. package/src/components/ui/action-menu/control-modes.tsx +2 -35
  136. package/src/components/ui/action-menu/measurement-control.tsx +3 -10
  137. package/src/components/ui/build-panel/build-panel.tsx +235 -0
  138. package/src/components/ui/command-palette/editor-commands.tsx +4 -30
  139. package/src/components/ui/command-palette/index.tsx +5 -24
  140. package/src/components/ui/controls/finish-swatch.tsx +110 -0
  141. package/src/components/ui/controls/material-paint-panel.tsx +101 -91
  142. package/src/components/ui/controls/material-picker.tsx +8 -67
  143. package/src/components/ui/controls/scene-material-list.tsx +4 -32
  144. package/src/components/ui/controls/select-control.tsx +65 -0
  145. package/src/components/ui/controls/slider-control.tsx +76 -31
  146. package/src/components/ui/floating-level-selector.tsx +9 -14
  147. package/src/components/ui/helpers/building-helper.tsx +4 -1
  148. package/src/components/ui/helpers/contextual-helper-panel.tsx +154 -70
  149. package/src/components/ui/helpers/helper-manager.test.tsx +69 -0
  150. package/src/components/ui/helpers/helper-manager.tsx +348 -19
  151. package/src/components/ui/helpers/item-helper.tsx +8 -0
  152. package/src/components/ui/helpers/registered-tool-helper.tsx +10 -33
  153. package/src/components/ui/icon-ref.tsx +45 -1
  154. package/src/components/ui/item-catalog/item-catalog.tsx +5 -30
  155. package/src/components/ui/level-duplicate-dialog.tsx +1 -1
  156. package/src/components/ui/node-icon-image.tsx +16 -0
  157. package/src/components/ui/panels/collections/collections-popover.tsx +20 -14
  158. package/src/components/ui/panels/floor-foundation-panel.tsx +551 -0
  159. package/src/components/ui/panels/floor-opening-panel.tsx +143 -0
  160. package/src/components/ui/panels/mobile-panel-sheet.tsx +2 -2
  161. package/src/components/ui/panels/mobile-selection-bar.tsx +2 -2
  162. package/src/components/ui/panels/multi-field-value.test.ts +26 -3
  163. package/src/components/ui/panels/multi-field-value.ts +6 -3
  164. package/src/components/ui/panels/multi-height-mode.tsx +27 -21
  165. package/src/components/ui/panels/multi-parametric-inspector.tsx +52 -2
  166. package/src/components/ui/panels/multi-selection-panel.tsx +43 -4
  167. package/src/components/ui/panels/node-display.ts +8 -3
  168. package/src/components/ui/panels/panel-manager.tsx +103 -68
  169. package/src/components/ui/panels/panel-wrapper.tsx +78 -36
  170. package/src/components/ui/panels/parametric-field-control.tsx +1 -1
  171. package/src/components/ui/panels/parametric-inspector.tsx +7 -19
  172. package/src/components/ui/panels/reference-panel.tsx +2 -3
  173. package/src/components/ui/panels/room-construction-rows.tsx +774 -0
  174. package/src/components/ui/panels/room-name-options.test.ts +55 -0
  175. package/src/components/ui/panels/room-name-options.ts +47 -0
  176. package/src/components/ui/panels/room-openings-field.tsx +110 -0
  177. package/src/components/ui/panels/room-panel.tsx +725 -0
  178. package/src/components/ui/panels/shape-choice.tsx +58 -0
  179. package/src/components/ui/panels/wall-region-list.tsx +109 -0
  180. package/src/components/ui/primitives/color-dot.tsx +36 -24
  181. package/src/components/ui/primitives/shortcut-token.tsx +4 -0
  182. package/src/components/ui/primitives/sidebar.tsx +2 -0
  183. package/src/components/ui/right-stack.test.ts +29 -0
  184. package/src/components/ui/right-stack.tsx +145 -0
  185. package/src/components/ui/sidebar/icon-rail.tsx +5 -0
  186. package/src/components/ui/sidebar/panels/settings-panel/audio-settings-dialog.tsx +2 -2
  187. package/src/components/ui/sidebar/panels/settings-panel/index.tsx +615 -372
  188. package/src/components/ui/sidebar/panels/settings-panel/keyboard-shortcuts-dialog.tsx +7 -7
  189. package/src/components/ui/sidebar/panels/settings-panel/print-export-button.tsx +1 -1
  190. package/src/components/ui/sidebar/panels/site-panel/ceiling-tree-node.tsx +4 -4
  191. package/src/components/ui/sidebar/panels/site-panel/chimney-tree-node.tsx +1 -5
  192. package/src/components/ui/sidebar/panels/site-panel/collection-tree-node.tsx +197 -0
  193. package/src/components/ui/sidebar/panels/site-panel/column-tree-node.tsx +1 -5
  194. package/src/components/ui/sidebar/panels/site-panel/door-tree-node.tsx +1 -5
  195. package/src/components/ui/sidebar/panels/site-panel/dormer-tree-node.tsx +1 -5
  196. package/src/components/ui/sidebar/panels/site-panel/elevator-tree-node.tsx +1 -5
  197. package/src/components/ui/sidebar/panels/site-panel/fence-tree-node.tsx +24 -11
  198. package/src/components/ui/sidebar/panels/site-panel/gutter-tree-node.tsx +1 -5
  199. package/src/components/ui/sidebar/panels/site-panel/index.tsx +176 -211
  200. package/src/components/ui/sidebar/panels/site-panel/inline-rename-input.tsx +30 -8
  201. package/src/components/ui/sidebar/panels/site-panel/item-tree-node.tsx +1 -3
  202. package/src/components/ui/sidebar/panels/site-panel/level-tree-node.tsx +4 -1
  203. package/src/components/ui/sidebar/panels/site-panel/registry-tree-node.tsx +1 -3
  204. package/src/components/ui/sidebar/panels/site-panel/roof-tree-node.tsx +1 -4
  205. package/src/components/ui/sidebar/panels/site-panel/shelf-tree-node.tsx +1 -3
  206. package/src/components/ui/sidebar/panels/site-panel/slab-tree-node.tsx +21 -13
  207. package/src/components/ui/sidebar/panels/site-panel/solar-panel-tree-node.tsx +1 -5
  208. package/src/components/ui/sidebar/panels/site-panel/spawn-tree-node.tsx +1 -5
  209. package/src/components/ui/sidebar/panels/site-panel/stair-tree-node.tsx +1 -5
  210. package/src/components/ui/sidebar/panels/site-panel/tree-node.tsx +0 -27
  211. package/src/components/ui/sidebar/panels/site-panel/unit-tree-node.tsx +2 -3
  212. package/src/components/ui/sidebar/panels/site-panel/wall-tree-node.tsx +1 -5
  213. package/src/components/ui/sidebar/panels/site-panel/window-tree-node.tsx +1 -5
  214. package/src/components/ui/sidebar/panels/site-panel/zone-tree-node.tsx +9 -4
  215. package/src/components/ui/sidebar/panels/zone-panel/index.tsx +0 -2
  216. package/src/components/ui/sidebar/tab-bar.tsx +10 -0
  217. package/src/components/ui/sidebar/use-plugin-panels.tsx +15 -5
  218. package/src/components/viewer/floorplan-compass-button.tsx +9 -1
  219. package/src/components/viewer/floorplan-preview-visibility.test.ts +35 -0
  220. package/src/components/viewer/floorplan-preview-visibility.ts +19 -0
  221. package/src/components/viewer/floorplan-preview.tsx +6 -18
  222. package/src/components/viewer/viewer-hover-label.tsx +34 -0
  223. package/src/components/viewer/viewer-rooms.tsx +175 -0
  224. package/src/components/viewer/viewer-scene-header.tsx +181 -145
  225. package/src/components/viewer/viewer-selection-manager.tsx +124 -0
  226. package/src/components/viewer/viewer-stage.tsx +2 -2
  227. package/src/components/viewer-zone-system.tsx +9 -9
  228. package/src/hooks/use-auto-save.integration.test.tsx +311 -0
  229. package/src/hooks/use-auto-save.ts +55 -14
  230. package/src/hooks/use-ceiling-events.test.ts +1 -0
  231. package/src/hooks/use-ceiling-events.ts +2 -0
  232. package/src/hooks/use-grid-events.test.ts +2 -0
  233. package/src/hooks/use-grid-events.ts +16 -11
  234. package/src/hooks/use-keyboard.test.ts +182 -0
  235. package/src/hooks/use-keyboard.ts +122 -91
  236. package/src/hooks/use-open-wall-ends.test.ts +99 -0
  237. package/src/hooks/use-open-wall-ends.ts +238 -0
  238. package/src/hooks/use-placement-notice.test.ts +48 -0
  239. package/src/hooks/use-placement-notice.ts +42 -0
  240. package/src/hooks/use-selected-room.ts +143 -0
  241. package/src/index.tsx +306 -39
  242. package/src/lib/catalog-panel-model.ts +39 -0
  243. package/src/lib/catalog-selection.test.ts +27 -0
  244. package/src/lib/catalog-selection.ts +8 -0
  245. package/src/lib/ceiling-edit-session.test.ts +321 -0
  246. package/src/lib/ceiling-edit-session.ts +206 -0
  247. package/src/lib/ceiling-region-session.test.ts +179 -0
  248. package/src/lib/contextual-help.test.ts +27 -1
  249. package/src/lib/contextual-help.ts +16 -9
  250. package/src/lib/continuation.test.ts +19 -1
  251. package/src/lib/continuation.ts +25 -4
  252. package/src/lib/door-interaction.ts +14 -0
  253. package/src/lib/editor-level-display.test.ts +96 -0
  254. package/src/lib/editor-level-display.ts +48 -0
  255. package/src/lib/editor-views.ts +144 -0
  256. package/src/lib/export-geometry-wait.test.ts +90 -0
  257. package/src/lib/floor-edit-session.ts +153 -0
  258. package/src/lib/floor-footprints.test.ts +563 -0
  259. package/src/lib/floor-footprints.ts +395 -0
  260. package/src/lib/floor-foundation-support.test.ts +283 -0
  261. package/src/lib/floor-opening-draft.ts +218 -0
  262. package/src/lib/floor-openings.test.ts +216 -0
  263. package/src/lib/floor-region-geometry.ts +191 -0
  264. package/src/lib/floor-region-session.test.ts +485 -0
  265. package/src/lib/floor-region-session.ts +589 -0
  266. package/src/lib/floor-region-snap.ts +101 -0
  267. package/src/lib/floor-step-hover.test.ts +32 -0
  268. package/src/lib/floorplan/annotation-visibility.ts +17 -0
  269. package/src/lib/floorplan/dimension-drive.test.ts +360 -0
  270. package/src/lib/floorplan/dimension-drive.ts +479 -0
  271. package/src/lib/floorplan/floorplan-export.test.ts +15 -9
  272. package/src/lib/floorplan/floorplan-export.tsx +32 -8
  273. package/src/lib/floorplan/floorplan-extension.ts +48 -0
  274. package/src/lib/floorplan/floorplan-mode.test.ts +4 -0
  275. package/src/lib/floorplan/floorplan-mode.ts +2 -0
  276. package/src/lib/floorplan/floorplan-pdfkit-document.ts +339 -18
  277. package/src/lib/floorplan/floorplan-pdfkit-renderer.test.ts +138 -8
  278. package/src/lib/floorplan/floorplan-pdfkit-renderer.ts +56 -25
  279. package/src/lib/floorplan/floorplan-plugin-install.test.tsx +138 -0
  280. package/src/lib/floorplan/floorplan-readonly.ts +6 -0
  281. package/src/lib/floorplan/geometry.ts +13 -6
  282. package/src/lib/floorplan/index.ts +0 -9
  283. package/src/lib/floorplan/items.ts +5 -2
  284. package/src/lib/floorplan/marks.test.ts +213 -0
  285. package/src/lib/floorplan/marks.ts +238 -0
  286. package/src/lib/floorplan/open-wall-ends.test.ts +268 -0
  287. package/src/lib/floorplan/open-wall-ends.ts +162 -0
  288. package/src/lib/floorplan/pdf-fonts/GeistMono-Bold.ttf +0 -0
  289. package/src/lib/floorplan/pdf-fonts/GeistMono-Regular.ttf +0 -0
  290. package/src/lib/floorplan/pdf-fonts/LICENSE-GeistMono.txt +92 -0
  291. package/src/lib/floorplan/pdf-fonts/LICENSE-LiberationSans.txt +102 -0
  292. package/src/lib/floorplan/pdf-fonts/LiberationSans-Bold.ttf +0 -0
  293. package/src/lib/floorplan/pdf-fonts/LiberationSans-Regular.ttf +0 -0
  294. package/src/lib/floorplan/schedules.test.ts +189 -0
  295. package/src/lib/floorplan/schedules.ts +339 -0
  296. package/src/lib/floorplan/sheet-export.test.ts +100 -0
  297. package/src/lib/floorplan/sheet-export.ts +318 -0
  298. package/src/lib/floorplan/site-plan/build-site-plan-drawing.ts +779 -0
  299. package/src/lib/floorplan/site-plan/contours.test.ts +43 -0
  300. package/src/lib/floorplan/site-plan/contributors.ts +97 -0
  301. package/src/lib/floorplan/site-plan/coverage.ts +232 -0
  302. package/src/lib/floorplan/site-plan/drawing-type-switch.test.ts +15 -0
  303. package/src/lib/floorplan/site-plan/drawing-type-switch.tsx +86 -0
  304. package/src/lib/floorplan/site-plan/front-edge.test.ts +103 -0
  305. package/src/lib/floorplan/site-plan/front-edge.ts +156 -0
  306. package/src/lib/floorplan/site-plan/geometry.test.ts +275 -0
  307. package/src/lib/floorplan/site-plan/geometry.ts +557 -0
  308. package/src/lib/floorplan/site-plan/index.ts +71 -0
  309. package/src/lib/floorplan/site-plan/site-annotations.test.ts +366 -0
  310. package/src/lib/floorplan/site-plan/site-annotations.ts +1083 -0
  311. package/src/lib/floorplan/site-plan/site-parts.ts +379 -0
  312. package/src/lib/floorplan/site-plan/site-plan-layer.tsx +138 -0
  313. package/src/lib/floorplan/site-plan/street-name.test.ts +69 -0
  314. package/src/lib/floorplan/site-plan/street-name.ts +302 -0
  315. package/src/lib/floorplan/site-plan/yards.test.ts +69 -0
  316. package/src/lib/floorplan/types.ts +1 -27
  317. package/src/lib/fresh-planar-placement.test.ts +36 -0
  318. package/src/lib/fresh-planar-placement.ts +191 -36
  319. package/src/lib/geometry-script/author.ts +152 -0
  320. package/src/lib/geometry-script/client.ts +152 -0
  321. package/src/lib/geometry-script/protocol.ts +12 -0
  322. package/src/lib/gesture-lifecycle.test.ts +595 -0
  323. package/src/lib/gesture-lifecycle.ts +290 -0
  324. package/src/lib/glb-export-zone-holes.test.ts +41 -0
  325. package/src/lib/glb-export.test.ts +544 -16
  326. package/src/lib/glb-export.ts +256 -125
  327. package/src/lib/grid-frame.test.ts +19 -0
  328. package/src/lib/grid-frame.ts +18 -0
  329. package/src/lib/history.test.ts +6 -0
  330. package/src/lib/history.ts +7 -0
  331. package/src/lib/hud-title.ts +109 -0
  332. package/src/lib/hydration-gated-storage.ts +49 -0
  333. package/src/lib/ifc-export.test.ts +174 -0
  334. package/src/lib/ifc-export.ts +201 -0
  335. package/src/lib/ifc-parts.ts +15 -0
  336. package/src/lib/inspector-expanded.test.ts +44 -0
  337. package/src/lib/inspector-expanded.ts +36 -0
  338. package/src/lib/interaction/scope.ts +27 -1
  339. package/src/lib/keyboard-pan.test.ts +184 -0
  340. package/src/lib/keyboard-pan.ts +238 -0
  341. package/src/lib/live-gesture-round-trip.test.ts +360 -0
  342. package/src/lib/lot/__fixtures__/map-dossier-miami-shores.json +1 -0
  343. package/src/lib/lot/__fixtures__/map-dossier-stpete.json +1 -0
  344. package/src/lib/lot/clean-ring.test.ts +186 -0
  345. package/src/lib/lot/clean-ring.ts +217 -0
  346. package/src/lib/lot/dossier.test.ts +202 -0
  347. package/src/lib/lot/dossier.ts +498 -0
  348. package/src/lib/lot/drop-in.test.ts +326 -0
  349. package/src/lib/lot/drop-in.ts +407 -0
  350. package/src/lib/lot/index.ts +43 -0
  351. package/src/lib/lot/lot-patch.test.ts +340 -0
  352. package/src/lib/lot/lot-patch.ts +394 -0
  353. package/src/lib/lot/parcel-provider.ts +43 -0
  354. package/src/lib/lot/terrain.test.ts +161 -0
  355. package/src/lib/lot/terrain.ts +266 -0
  356. package/src/lib/material-catalog-model.ts +74 -0
  357. package/src/lib/material-paint-panel-model.ts +80 -0
  358. package/src/lib/material-paint.ts +4 -0
  359. package/src/lib/mezzanine-actions.test.ts +342 -0
  360. package/src/lib/mezzanine-draft.test.ts +501 -0
  361. package/src/lib/mezzanine-draft.ts +278 -0
  362. package/src/lib/mezzanine-messages.ts +26 -0
  363. package/src/lib/mezzanine-selection.test.ts +207 -0
  364. package/src/lib/mezzanine-stairs.ts +46 -0
  365. package/src/lib/model-export.ts +3 -1
  366. package/src/lib/node-action-movement.ts +17 -6
  367. package/src/lib/node-display-label.ts +37 -0
  368. package/src/lib/orbit-floor-clearance.test.ts +165 -0
  369. package/src/lib/orbit-floor-clearance.ts +106 -0
  370. package/src/lib/overlay-transform.test.ts +28 -0
  371. package/src/lib/overlay-transform.ts +12 -0
  372. package/src/lib/paint-commit-route.test.ts +37 -0
  373. package/src/lib/paint-commit-route.ts +17 -0
  374. package/src/lib/paint-eyedropper.test.tsx +165 -0
  375. package/src/lib/paint-eyedropper.ts +99 -0
  376. package/src/lib/paint-pass-through.test.ts +111 -0
  377. package/src/lib/paint-pass-through.ts +76 -0
  378. package/src/lib/paint-region-hover.ts +25 -0
  379. package/src/lib/paint-region-mode.ts +154 -0
  380. package/src/lib/paint-regions.test.ts +166 -0
  381. package/src/lib/paint-regions.ts +192 -0
  382. package/src/lib/paint-scope.test.ts +317 -35
  383. package/src/lib/paint-scope.ts +266 -25
  384. package/src/lib/panel-rows.ts +96 -0
  385. package/src/lib/panel-tool-options.test.ts +34 -0
  386. package/src/lib/panel-tool-options.ts +46 -0
  387. package/src/lib/parametric-node-update.ts +37 -0
  388. package/src/lib/plate-paint-affected.test.ts +300 -0
  389. package/src/lib/plate-paint-affected.ts +177 -0
  390. package/src/lib/plugin-chat.ts +50 -0
  391. package/src/lib/plugin-panels.test.ts +25 -0
  392. package/src/lib/plugin-panels.ts +94 -0
  393. package/src/lib/print-shell-compiler.ts +1 -0
  394. package/src/lib/registered-draft-snap.ts +12 -0
  395. package/src/lib/roof-duplication.ts +10 -2
  396. package/src/lib/room-assembly-overlay.test.ts +275 -0
  397. package/src/lib/room-assembly-overlay.ts +266 -0
  398. package/src/lib/room-built-on.test.ts +379 -0
  399. package/src/lib/room-built-on.ts +138 -0
  400. package/src/lib/room-construction-commands.ts +712 -0
  401. package/src/lib/room-construction.test.ts +746 -0
  402. package/src/lib/room-construction.ts +385 -0
  403. package/src/lib/room-controls-phase6.test.ts +630 -0
  404. package/src/lib/room-divide-session.ts +523 -0
  405. package/src/lib/room-handle-drag.ts +1068 -0
  406. package/src/lib/room-name-catalog.test.ts +90 -0
  407. package/src/lib/room-name-catalog.ts +168 -0
  408. package/src/lib/room-push-dimensions.test.ts +311 -0
  409. package/src/lib/room-push-dimensions.ts +251 -0
  410. package/src/lib/room-selection-commands.ts +62 -0
  411. package/src/lib/room-selection.test.ts +973 -0
  412. package/src/lib/room-selection.ts +417 -0
  413. package/src/lib/room-structure-commands.ts +94 -0
  414. package/src/lib/room-structure.test.ts +921 -0
  415. package/src/lib/room-transform-session.ts +570 -0
  416. package/src/lib/room-zone-routing.test.ts +294 -0
  417. package/src/lib/room-zone-routing.ts +197 -0
  418. package/src/lib/scene-clipboard.test.ts +364 -0
  419. package/src/lib/scene-clipboard.ts +229 -48
  420. package/src/lib/scene-material-refs.test.ts +79 -0
  421. package/src/lib/scene-material-refs.ts +79 -0
  422. package/src/lib/scene.test.ts +87 -0
  423. package/src/lib/scene.ts +39 -45
  424. package/src/lib/selection-routing.test.ts +129 -45
  425. package/src/lib/selection-routing.ts +53 -74
  426. package/src/lib/session-writes.test.ts +120 -0
  427. package/src/lib/session-writes.ts +205 -0
  428. package/src/lib/sidebar-panel.ts +62 -0
  429. package/src/lib/site-boundary.test.ts +4 -6
  430. package/src/lib/site-boundary.ts +1 -1
  431. package/src/lib/snapping-mode.test.ts +17 -2
  432. package/src/lib/snapping-mode.ts +19 -7
  433. package/src/lib/spatial-drag-plane.test.ts +53 -0
  434. package/src/lib/spatial-drag-plane.ts +19 -0
  435. package/src/lib/spatial-pointer-input.test.ts +72 -0
  436. package/src/lib/spatial-pointer-input.ts +62 -0
  437. package/src/lib/spatial-snap.test.ts +47 -0
  438. package/src/lib/terrace-draft.test.ts +145 -0
  439. package/src/lib/terrace-draft.ts +213 -0
  440. package/src/lib/terrain-panel-model.ts +117 -0
  441. package/src/lib/tool-cursor-icon.test.ts +16 -0
  442. package/src/lib/tool-cursor-icon.ts +11 -0
  443. package/src/lib/typing-target.ts +9 -0
  444. package/src/lib/units.ts +77 -39
  445. package/src/lib/vector-edges.test.ts +58 -0
  446. package/src/lib/vector-edges.ts +400 -0
  447. package/src/lib/viewer-selection.test.ts +577 -0
  448. package/src/lib/viewer-selection.ts +569 -0
  449. package/src/lib/wall-draw-variant.test.ts +71 -0
  450. package/src/lib/wall-draw-variant.ts +91 -0
  451. package/src/lib/wall-paint-region-session.ts +246 -0
  452. package/src/lib/wall-paint-region.test.ts +373 -0
  453. package/src/lib/wall-polygon-draft.test.ts +262 -0
  454. package/src/lib/wall-polygon-draft.ts +286 -0
  455. package/src/lib/wall-push.test.ts +453 -0
  456. package/src/lib/wall-region-face.ts +352 -0
  457. package/src/lib/wall-region-handles.test.ts +304 -0
  458. package/src/lib/wall-region-handles.ts +368 -0
  459. package/src/lib/wall-region-list.test.ts +119 -0
  460. package/src/lib/wall-region-list.ts +136 -0
  461. package/src/lib/wall-region-snap.ts +110 -0
  462. package/src/lib/window-interaction.ts +14 -0
  463. package/src/lib/world-grid-snap.test.ts +48 -0
  464. package/src/lib/world-grid-snap.ts +9 -5
  465. package/src/lib/zone-content.ts +4 -1
  466. package/src/lib/zone-name.test.ts +17 -0
  467. package/src/lib/zone-name.ts +17 -0
  468. package/src/store/opens-in-select-mode.test.ts +223 -0
  469. package/src/store/paint-mode.test.ts +188 -0
  470. package/src/store/rotation-snapping.test.ts +53 -0
  471. package/src/store/terrain-sculpt-mode.test.ts +19 -21
  472. package/src/store/tool-mode.test.ts +137 -6
  473. package/src/store/use-delete-confirmation.ts +28 -1
  474. package/src/store/use-drawing-view.test.ts +24 -3
  475. package/src/store/use-drawing-view.ts +29 -1
  476. package/src/store/use-editor.tsx +442 -323
  477. package/src/store/use-floorplan-draft-preview.ts +23 -1
  478. package/src/store/use-floorplan-marquee.ts +12 -2
  479. package/src/store/use-interaction-scope.ts +155 -6
  480. package/src/store/use-quick-measurement-hud.ts +3 -3
  481. package/src/store/use-stair-build-preview.ts +13 -5
  482. package/src/store/use-wall-move-ghosts.ts +4 -2
  483. package/src/store/use-wall-region-handles-selection.test.ts +222 -0
  484. package/src/store/use-wall-region-handles-selection.ts +139 -0
  485. package/src/store/view-layout.test.ts +175 -0
  486. package/src/store/view-layout.ts +196 -0
  487. package/src/test-utils/immediate-animation-frames.ts +16 -0
  488. package/src/test-utils/live-gesture-round-trip.ts +224 -0
  489. package/src/test-utils/selection-harness.ts +82 -0
  490. package/src/components/editor-2d/renderers/floorplan-stair-layer.tsx +0 -499
  491. package/src/components/systems/zone/zone-label-editor-system.tsx +0 -333
  492. package/src/components/tools/fence/fence-drafting.test.ts +0 -100
  493. package/src/components/tools/fence/fence-drafting.ts +0 -266
  494. package/src/components/ui/sidebar/panels/plugins-panel.tsx +0 -218
  495. package/src/components/viewer/viewer-units-panel.tsx +0 -116
  496. package/src/lib/floorplan/stairs.ts +0 -489
  497. package/src/lib/level-duplication.test.ts +0 -144
  498. package/src/lib/level-duplication.ts +0 -195
@@ -19,6 +19,7 @@ import {
19
19
  MeshBasicMaterial,
20
20
  OrthographicCamera,
21
21
  PerspectiveCamera,
22
+ Ray,
22
23
  Vector3,
23
24
  } from 'three'
24
25
  import { z } from 'zod'
@@ -161,6 +162,32 @@ describe('resolvePointerSupportSurface node tops', () => {
161
162
  expect(support?.worldPoint).toEqual([0, 2, 0])
162
163
  })
163
164
 
165
+ test('XR floor drawing uses the controller ray even when the camera sees a raised surface', () => {
166
+ addPluginPlatform(2, [4, 2, 2])
167
+ const camera = new PerspectiveCamera()
168
+ camera.position.set(0, 5, 5)
169
+ camera.updateMatrixWorld(true)
170
+ const controllerRay = new Ray(new Vector3(0, 1.5, 0), new Vector3(0, -1, 0))
171
+ const desktop = resolvePointerSupportSurface(camera, [0, 0, 0], {
172
+ includeNodeTopSurfaces: true,
173
+ })
174
+ expect(desktop?.sourceNodeId).toBe(PLATFORM_ID)
175
+ const xr = resolvePointerSupportSurface(camera, [0, 0, 0], {
176
+ includeNodeTopSurfaces: true,
177
+ pointerRay: controllerRay,
178
+ })
179
+ expect(xr?.sourceNodeId).toBeNull()
180
+ expect(xr?.worldPoint).toEqual([0, 0, 0])
181
+ camera.position.set(12, 8, -10)
182
+ camera.updateMatrixWorld(true)
183
+ expect(
184
+ resolvePointerSupportSurface(camera, [0, 0, 0], {
185
+ includeNodeTopSurfaces: true,
186
+ pointerRay: controllerRay,
187
+ })?.worldPoint,
188
+ ).toEqual([0, 0, 0])
189
+ })
190
+
164
191
  test('keeps an unhovered item top above a batched slab across batch transitions', () => {
165
192
  const restoreRegistry = nodeRegistry._snapshot()
166
193
  const item = ItemNode.parse({
@@ -11,7 +11,7 @@ import {
11
11
  useScene,
12
12
  } from '@pascal-app/core'
13
13
  import { setSurfaceRaycastLayers, useViewer } from '@pascal-app/viewer'
14
- import { type Camera, type Object3D, Raycaster, Vector3 } from 'three'
14
+ import { type Camera, type Object3D, type Ray, Raycaster, Vector3 } from 'three'
15
15
  import { resolveTerrainGroundHit } from '../../../lib/ground-surface'
16
16
  import { scopeNodeId } from '../../../lib/interaction/scope'
17
17
  import useInteractionScope from '../../../store/use-interaction-scope'
@@ -79,16 +79,22 @@ export type PointerSupportSurface = {
79
79
  export function resolvePointerSupportSurface(
80
80
  camera: Camera,
81
81
  worldHit: readonly [number, number, number],
82
- options?: { includeNodeTopSurfaces?: boolean },
82
+ options?: { includeNodeTopSurfaces?: boolean; pointerRay?: Ray },
83
83
  ): PointerSupportSurface | null {
84
84
  const levelId = useViewer.getState().selection.levelId
85
85
  if (!levelId) return null
86
86
 
87
87
  // The world ray, kept before the level conversion below: the terrain field is
88
88
  // world-space (site geometry, not level-local), so the march needs this frame.
89
- camera.getWorldPosition(worldRayOrigin)
89
+ if (options?.pointerRay) worldRayOrigin.copy(options.pointerRay.origin)
90
+ else camera.getWorldPosition(worldRayOrigin)
91
+ const isOrthographic =
92
+ !options?.pointerRay &&
93
+ (camera as Camera & { isOrthographicCamera?: boolean }).isOrthographicCamera === true
90
94
  const cameraToHit = hitScratch.set(worldHit[0], worldHit[1], worldHit[2]).sub(worldRayOrigin)
91
- if ((camera as Camera & { isOrthographicCamera?: boolean }).isOrthographicCamera) {
95
+ if (options?.pointerRay) {
96
+ worldRayDirection.copy(options.pointerRay.direction).normalize()
97
+ } else if (isOrthographic) {
92
98
  // For an orthographic camera every screen pixel has the same direction. The
93
99
  // hit point is offset from the camera along the view plane, so using
94
100
  // `camera.position -> hit` tilts the ray toward the screen centre and makes
@@ -102,7 +108,13 @@ export function resolvePointerSupportSurface(
102
108
  }
103
109
 
104
110
  originScratch.copy(worldRayOrigin)
105
- hitScratch.set(worldHit[0], worldHit[1], worldHit[2])
111
+ // Second world point defining the ray fed to the surface solve. In the plain
112
+ // perspective case `worldHit` already lies on the true pointer ray, so using
113
+ // it keeps `t === 1` (and the returned plan point exact) whenever the pointed
114
+ // surface IS the event plane — the pointer-ray and orthographic branches have
115
+ // no such hit, so they step one unit along the resolved direction.
116
+ if (options?.pointerRay || isOrthographic) hitScratch.copy(worldRayOrigin).add(worldRayDirection)
117
+ else hitScratch.set(worldHit[0], worldHit[1], worldHit[2])
106
118
  // Slab polygons/elevations live in the level frame; the level mesh
107
119
  // carries the storey Y offset and any building rotation.
108
120
  const levelMesh = sceneRegistry.nodes.get(levelId as AnyNodeId)
@@ -3,12 +3,12 @@ import {
3
3
  type AnyNode,
4
4
  ItemNode,
5
5
  nodeRegistry,
6
- registerNode,
7
6
  resolveSurfacePlacement,
8
7
  type SceneApi,
9
8
  type SurfaceProvider,
10
9
  type SurfaceRejectReason,
11
10
  } from '@pascal-app/core'
11
+ import { registerHostingTestNode } from '../__fixtures__/hosting'
12
12
  import { createSurfaceRejectionFeedback } from './surface-rejection'
13
13
 
14
14
  let restore: () => void
@@ -27,7 +27,7 @@ for (const reason of [
27
27
  'no-surface',
28
28
  'invalid-hit',
29
29
  ] as const) {
30
- test(`${reason} remains available from onReject without user-facing text`, () => {
30
+ test(`${reason}: eligible-host diagnostics remain available; ineligible hosts defer silently`, () => {
31
31
  const provider: SurfaceProvider = {
32
32
  childFrame: 'host-local',
33
33
  resolveHit: () =>
@@ -56,7 +56,7 @@ for (const reason of [
56
56
  },
57
57
  accepts: () => reason !== 'child-not-accepted',
58
58
  }
59
- registerNode({
59
+ registerHostingTestNode({
60
60
  kind: 'test-host',
61
61
  schemaVersion: 1,
62
62
  schema: ItemNode,
@@ -75,6 +75,7 @@ for (const reason of [
75
75
  type: reason === 'host-not-eligible' ? 'guide' : 'test-host',
76
76
  } as unknown as AnyNode,
77
77
  childKind: 'item',
78
+ childId: 'item_existing',
78
79
  childFootprint: {
79
80
  size: reason === 'footprint-outside-surface' ? [3, 1, 3] : [0.3, 0.3, 0.3],
80
81
  rotationY: 0,
@@ -87,7 +88,7 @@ for (const reason of [
87
88
  onReject: (r) => feedback.reject(r),
88
89
  }),
89
90
  ).toBeNull()
90
- expect(feedback.reason).toBe(reason)
91
+ expect(feedback.reason).toBe(reason === 'host-not-eligible' ? null : reason)
91
92
  })
92
93
  }
93
94
 
@@ -27,3 +27,17 @@ export function createSurfaceRejectionFeedback(
27
27
  },
28
28
  }
29
29
  }
30
+
31
+ // R3F reuses one native event across enter/move dispatch and bubbled ancestor hits.
32
+ export function createSurfaceEventOwnership() {
33
+ const owners = new WeakMap<object, string>()
34
+ return {
35
+ allows(hostId: string, event: object) {
36
+ const owner = owners.get(event)
37
+ return owner === undefined || owner === hostId
38
+ },
39
+ claim(hostId: string, event: object) {
40
+ if (!owners.has(event)) owners.set(event, hostId)
41
+ },
42
+ }
43
+ }
@@ -458,8 +458,7 @@ export const SiteBoundaryEditor: React.FC = () => {
458
458
 
459
459
  const exitSiteEditing = useCallback(() => {
460
460
  const editor = useEditor.getState()
461
- editor.setPhase('structure')
462
- editor.setStructureLayer('elements')
461
+ editor.setPhase('building')
463
462
  editor.setMode('select')
464
463
  }, [])
465
464
 
@@ -1,8 +1,5 @@
1
1
  export const DEFAULT_STAIR_TYPE = 'straight' as const
2
2
  export const DEFAULT_STAIR_WIDTH = 1.0
3
- export const DEFAULT_STAIR_LENGTH = 3.0
4
- export const DEFAULT_STAIR_HEIGHT = 2.5
5
- export const DEFAULT_STAIR_STEP_COUNT = 10
6
3
  export const DEFAULT_STAIR_ATTACHMENT_SIDE = 'front' as const
7
4
  export const DEFAULT_STAIR_FILL_TO_FLOOR = true
8
5
  export const DEFAULT_STAIR_THICKNESS = 0.25
@@ -1,7 +1,7 @@
1
1
  import {
2
2
  type AnyNode,
3
3
  collectAlignmentAnchors,
4
- createDefaultStairSegment,
4
+ createSizedStairFlight,
5
5
  createSurfaceOpeningPreviewController,
6
6
  DEFAULT_LEVEL_HEIGHT,
7
7
  emitter,
@@ -11,6 +11,8 @@ import {
11
11
  type LevelNode,
12
12
  movingAlignmentAnchors,
13
13
  type NodeEvent,
14
+ planStairCreation,
15
+ planStairSizing,
14
16
  resolveAlignment,
15
17
  resolveFrozenFloorPlacementPatch,
16
18
  resolveSupportSlabPatch,
@@ -23,6 +25,7 @@ import { useViewer } from '@pascal-app/viewer'
23
25
  import { useThree } from '@react-three/fiber'
24
26
  import { useEffect, useMemo, useRef } from 'react'
25
27
  import * as THREE from 'three'
28
+ import { EDITOR_LAYER } from '../../../lib/constants'
26
29
  import { sfxEmitter } from '../../../lib/sfx-bus'
27
30
  import {
28
31
  resolveStairDestinationLevel,
@@ -54,11 +57,9 @@ import {
54
57
  DEFAULT_SPIRAL_TOP_LANDING_MODE,
55
58
  DEFAULT_STAIR_ATTACHMENT_SIDE,
56
59
  DEFAULT_STAIR_FILL_TO_FLOOR,
57
- DEFAULT_STAIR_LENGTH,
58
60
  DEFAULT_STAIR_OPENING_OFFSET,
59
61
  DEFAULT_STAIR_RAILING_HEIGHT,
60
62
  DEFAULT_STAIR_RAILING_MODE,
61
- DEFAULT_STAIR_STEP_COUNT,
62
63
  DEFAULT_STAIR_THICKNESS,
63
64
  DEFAULT_STAIR_TYPE,
64
65
  DEFAULT_STAIR_WIDTH,
@@ -75,19 +76,18 @@ type MoveTriggerEvent = GridEvent | NodeEvent<AnyNode>
75
76
  * Same algorithm as StairSystem's generateStairSegmentGeometry.
76
77
  */
77
78
  function createStairPreviewGeometry(rise: number): THREE.BufferGeometry {
78
- const riserHeight = rise / DEFAULT_STAIR_STEP_COUNT
79
- const treadDepth = DEFAULT_STAIR_LENGTH / DEFAULT_STAIR_STEP_COUNT
79
+ const { stepCount, length, riserHeight, going: treadDepth } = planStairSizing(rise)
80
80
 
81
81
  const shape = new THREE.Shape()
82
82
  shape.moveTo(0, 0)
83
83
 
84
- for (let i = 0; i < DEFAULT_STAIR_STEP_COUNT; i++) {
84
+ for (let i = 0; i < stepCount; i++) {
85
85
  shape.lineTo(i * treadDepth, (i + 1) * riserHeight)
86
86
  shape.lineTo((i + 1) * treadDepth, (i + 1) * riserHeight)
87
87
  }
88
88
 
89
89
  // Fill to floor (absoluteHeight = 0)
90
- shape.lineTo(DEFAULT_STAIR_LENGTH, 0)
90
+ shape.lineTo(length, 0)
91
91
  shape.lineTo(0, 0)
92
92
 
93
93
  const geometry = new THREE.ExtrudeGeometry(shape, {
@@ -110,33 +110,28 @@ function createStairPreviewGeometry(rise: number): THREE.BufferGeometry {
110
110
  * dropped on, not a constant: the placed stair has no explicit `totalRise`, so
111
111
  * this is the height `syncStairRises` immediately converges it to anyway.
112
112
  */
113
- function resolvePlacedStairRise(
113
+ function resolvePlacedStairBase(
114
114
  nodes: Record<string, AnyNode>,
115
115
  levelId: LevelNode['id'],
116
116
  stair: StairNode,
117
117
  supportSurface: PointerSupportSurface | null,
118
+ seed: StairSegmentNode,
118
119
  ): number {
119
- // Same contract as `resolveStairTotalRise` for a stair that is not in the
120
- // scene yet: the storey height minus whatever slab lifts the drop point,
121
- // capped by the surface the pointer actually aims at (a floor under an
122
- // overlapping deck must not elect the deck).
120
+ // Keep the pointer-selected base when an overlapping deck is above the cursor.
123
121
  const base = getFloorStackedPosition({
124
122
  node: stair,
125
- nodes,
123
+ nodes: { ...nodes, [seed.id]: { ...seed, parentId: stair.id } },
126
124
  position: stair.position,
127
125
  rotation: stair.rotation,
128
126
  levelId,
129
127
  maxElevation: supportSurface?.elevation ?? null,
130
128
  })[1]
131
- return getLevelFloorToFloorHeight(levelId, nodes) - base
129
+ return base
132
130
  }
133
131
 
134
132
  function createSeedStairSegment(rise: number) {
135
- return createDefaultStairSegment({
133
+ return createSizedStairFlight(rise, {
136
134
  width: DEFAULT_STAIR_WIDTH,
137
- length: DEFAULT_STAIR_LENGTH,
138
- height: rise,
139
- stepCount: DEFAULT_STAIR_STEP_COUNT,
140
135
  attachmentSide: DEFAULT_STAIR_ATTACHMENT_SIDE,
141
136
  fillToFloor: DEFAULT_STAIR_FILL_TO_FLOOR,
142
137
  thickness: DEFAULT_STAIR_THICKNESS,
@@ -160,15 +155,16 @@ function createDefaultStairNode({
160
155
  }) {
161
156
  return StairNode.parse({
162
157
  name,
158
+ parentId: levelId,
163
159
  position,
164
160
  rotation,
165
161
  stairType: DEFAULT_STAIR_TYPE,
162
+ uniformRisers: true,
166
163
  fromLevelId: levelId,
167
164
  toLevelId: nextLevelId,
168
165
  slabOpeningMode: 'destination',
169
166
  openingOffset: DEFAULT_STAIR_OPENING_OFFSET,
170
167
  width: DEFAULT_STAIR_WIDTH,
171
- stepCount: DEFAULT_STAIR_STEP_COUNT,
172
168
  thickness: DEFAULT_STAIR_THICKNESS,
173
169
  fillToFloor: DEFAULT_STAIR_FILL_TO_FLOOR,
174
170
  innerRadius: DEFAULT_CURVED_STAIR_INNER_RADIUS,
@@ -202,7 +198,9 @@ function commitStairPlacement(
202
198
 
203
199
  const stairCount = Object.values(nodes).filter((n) => n.type === 'stair').length
204
200
  const name = `Staircase ${stairCount + 1}`
205
- const seed = createSeedStairSegment(getLevelFloorToFloorHeight(placementLevelId, nodes))
201
+ const storeyRise = getLevelFloorToFloorHeight(placementLevelId, nodes)
202
+ if (!(storeyRise > 0)) return
203
+ const seed = createSeedStairSegment(storeyRise)
206
204
 
207
205
  const destinationPlan = resolveStairDestinationLevel({
208
206
  createMissing: true,
@@ -222,10 +220,28 @@ function commitStairPlacement(
222
220
  }),
223
221
  parentId: placementLevelId,
224
222
  })
225
- const segment = {
226
- ...seed,
227
- height: resolvePlacedStairRise(nodes, placementLevelId, stair, supportSurface),
223
+ const base = resolvePlacedStairBase(nodes, placementLevelId, stair, supportSurface, seed)
224
+ let segment: StairSegmentNode
225
+ try {
226
+ segment = {
227
+ ...planStairCreation(
228
+ stair,
229
+ nodes,
230
+ {
231
+ width: seed.width,
232
+ attachmentSide: seed.attachmentSide,
233
+ fillToFloor: seed.fillToFloor,
234
+ thickness: seed.thickness,
235
+ },
236
+ base,
237
+ ).flight,
238
+ id: seed.id,
239
+ }
240
+ } catch (error) {
241
+ if (error instanceof RangeError) return
242
+ throw error
228
243
  }
244
+ stair.stepCount = segment.stepCount
229
245
  const prospectiveNodes = {
230
246
  ...nodes,
231
247
  [stair.id]: stair,
@@ -270,6 +286,7 @@ export const StairTool: React.FC = () => {
270
286
  cameraRef.current = camera
271
287
  const cursorRef = useRef<THREE.Group>(null)
272
288
  const previewRef = useRef<THREE.Group>(null)
289
+ const previewMeshRef = useRef<THREE.Mesh>(null)
273
290
  const rotationRef = useRef(0)
274
291
  const supportSurfaceRef = useRef<PointerSupportSurface | null>(null)
275
292
  const previousGridPosRef = useRef<[number, number] | null>(null)
@@ -281,8 +298,10 @@ export const StairTool: React.FC = () => {
281
298
  )
282
299
  const previewRiseRef = useRef(previewRise)
283
300
  previewRiseRef.current = previewRise
284
- const previewGeometry = useMemo(() => createStairPreviewGeometry(previewRise), [previewRise])
285
- useEffect(() => () => previewGeometry.dispose(), [previewGeometry])
301
+ const previewGeometry = useMemo(() => createStairPreviewGeometry(DEFAULT_LEVEL_HEIGHT), [])
302
+ const ownedGeometryRef = useRef(previewGeometry)
303
+ const geometryRiseRef = useRef(DEFAULT_LEVEL_HEIGHT)
304
+ useEffect(() => () => ownedGeometryRef.current.dispose(), [])
286
305
 
287
306
  useEffect(() => {
288
307
  if (!currentLevelId) return
@@ -321,7 +340,9 @@ export const StairTool: React.FC = () => {
321
340
  nodes,
322
341
  })
323
342
  const nextLevelId = destinationPlan?.toLevel.id ?? placementLevelId
324
- const seed = createSeedStairSegment(getLevelFloorToFloorHeight(placementLevelId, nodes))
343
+ const storeyRise = getLevelFloorToFloorHeight(placementLevelId, nodes)
344
+ if (!(storeyRise > 0)) return null
345
+ const seed = createSeedStairSegment(storeyRise)
325
346
  const stair = createDefaultStairNode({
326
347
  name: 'Staircase Preview',
327
348
  levelId: placementLevelId,
@@ -330,10 +351,28 @@ export const StairTool: React.FC = () => {
330
351
  rotation,
331
352
  segmentId: seed.id,
332
353
  })
333
- const segment = {
334
- ...seed,
335
- height: resolvePlacedStairRise(nodes, placementLevelId, stair, supportSurface),
354
+ const base = resolvePlacedStairBase(nodes, placementLevelId, stair, supportSurface, seed)
355
+ let segment: StairSegmentNode
356
+ try {
357
+ segment = {
358
+ ...planStairCreation(
359
+ stair,
360
+ nodes,
361
+ {
362
+ width: seed.width,
363
+ attachmentSide: seed.attachmentSide,
364
+ fillToFloor: seed.fillToFloor,
365
+ thickness: seed.thickness,
366
+ },
367
+ base,
368
+ ).flight,
369
+ id: seed.id,
370
+ }
371
+ } catch (error) {
372
+ if (error instanceof RangeError) return null
373
+ throw error
336
374
  }
375
+ stair.stepCount = segment.stepCount
337
376
  const previewNodes = {
338
377
  ...nodes,
339
378
  ...(destinationPlan?.createdLevel
@@ -360,11 +399,13 @@ export const StairTool: React.FC = () => {
360
399
  rotation: number,
361
400
  supportSurface: PointerSupportSurface | null,
362
401
  ) => {
363
- const key = `${position[0].toFixed(3)},${position[2].toFixed(3)},${rotation.toFixed(4)},${supportSurface?.elevation.toFixed(3) ?? 'none'},${supportSurface?.sourceNodeId ?? 'floor'}`
402
+ const key = `${position[0].toFixed(3)},${position[2].toFixed(3)},${rotation.toFixed(4)},${supportSurface?.elevation.toFixed(3) ?? 'none'},${supportSurface?.sourceNodeId ?? 'floor'},${previewRiseRef.current}`
364
403
  if (key === lastPreviewKey) return
365
404
  lastPreviewKey = key
366
- useStairBuildPreview.getState().setPreview([position[0], position[2]], rotation)
367
405
  const preview = buildPreviewScene(position, rotation, supportSurface)
406
+ useStairBuildPreview
407
+ .getState()
408
+ .setPreview(preview ? [position[0], position[2]] : null, rotation, preview?.rise ?? null)
368
409
  const frozenPatch =
369
410
  preview && supportSurface?.sourceNodeId
370
411
  ? resolveFrozenFloorPlacementPatch(preview.stair, preview.previewNodes, {
@@ -398,11 +439,17 @@ export const StairTool: React.FC = () => {
398
439
  }
399
440
 
400
441
  if (previewRef.current) {
442
+ previewRef.current.visible = Boolean(preview)
401
443
  previewRef.current.position.set(...visualPosition)
402
444
  previewRef.current.rotation.y = rotation
403
- // The ghost geometry is built for the storey height; squash it to the
404
- // rise the placed flight will get on this surface.
405
- previewRef.current.scale.y = preview ? preview.rise / previewRiseRef.current : 1
445
+ if (preview && previewMeshRef.current && preview.rise !== geometryRiseRef.current) {
446
+ const previous = previewMeshRef.current.geometry
447
+ previewMeshRef.current.geometry = createStairPreviewGeometry(preview.rise)
448
+ previous.dispose()
449
+ ownedGeometryRef.current = previewMeshRef.current.geometry
450
+ geometryRiseRef.current = preview.rise
451
+ }
452
+ previewRef.current.scale.y = 1
406
453
  }
407
454
 
408
455
  // Forward-facing triangle (editor-side overlay). The run ascends along
@@ -410,19 +457,20 @@ export const StairTool: React.FC = () => {
410
457
  // so `reversed` points the triangle out of the entry (where you approach
411
458
  // from), sitting just before it — not inside the footprint or at the
412
459
  // elevated far end. Centre is the footprint mid-run (origin is the entry).
460
+ if (!preview) {
461
+ useFacingPose.getState().clear()
462
+ openingPreview.clear()
463
+ return
464
+ }
465
+ const sizing = planStairSizing(preview.rise)
413
466
  useFacingPose.getState().set({
414
467
  position: visualPosition,
415
468
  rotationY: rotation,
416
- depth: DEFAULT_STAIR_LENGTH,
417
- center: [0, DEFAULT_STAIR_LENGTH / 2],
469
+ depth: sizing.length,
470
+ center: [0, sizing.length / 2],
418
471
  reversed: true,
419
472
  })
420
473
 
421
- if (!preview) {
422
- openingPreview.clear()
423
- return
424
- }
425
-
426
474
  openingPreview.apply(syncAutoStairOpenings(preview.previewNodes))
427
475
  }
428
476
 
@@ -625,7 +673,7 @@ export const StairTool: React.FC = () => {
625
673
  forward-facing triangle is drawn by the editor-side overlay from the
626
674
  pose published in `applyDraftPreview`. */}
627
675
  <group ref={previewRef}>
628
- <mesh castShadow geometry={previewGeometry}>
676
+ <mesh dispose={null} geometry={previewGeometry} layers={EDITOR_LAYER} ref={previewMeshRef}>
629
677
  <meshStandardMaterial color="#818cf8" depthWrite={false} opacity={0.35} transparent />
630
678
  </mesh>
631
679
  </group>