@pascal-app/editor 1.0.3 → 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 (440) hide show
  1. package/package.json +11 -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 +202 -57
  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 +36 -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 +344 -342
  27. package/src/components/editor/footprint-height-handle.tsx +204 -0
  28. package/src/components/editor/group-actions.ts +54 -2
  29. package/src/components/editor/handles/handle-hit-priority.test.tsx +63 -0
  30. package/src/components/editor/handles/handle-hit-priority.tsx +37 -0
  31. package/src/components/editor/handles/swallow-next-click.test.ts +28 -0
  32. package/src/components/editor/handles/use-handle-drag.ts +269 -197
  33. package/src/components/editor/index.tsx +294 -169
  34. package/src/components/editor/join-walls-pill.tsx +44 -0
  35. package/src/components/editor/node-action-menu.tsx +68 -70
  36. package/src/components/editor/node-arrow-handles.tsx +25 -6
  37. package/src/components/editor/open-wall-ends-3d-layer.tsx +273 -0
  38. package/src/components/editor/open-wall-ends-hint.tsx +89 -0
  39. package/src/components/editor/quick-measurement-hud.tsx +3 -2
  40. package/src/components/editor/registry-action-contributions.test.tsx +63 -0
  41. package/src/components/editor/registry-action-contributions.tsx +80 -1
  42. package/src/components/editor/room-controls.tsx +847 -0
  43. package/src/components/editor/room-floor-highlight.tsx +128 -0
  44. package/src/components/editor/room-handles-2d.tsx +354 -0
  45. package/src/components/editor/room-handles.tsx +675 -0
  46. package/src/components/editor/room-highlight.tsx +225 -0
  47. package/src/components/editor/room-labels.test.ts +357 -0
  48. package/src/components/editor/room-labels.tsx +990 -0
  49. package/src/components/editor/room-transform-ghost.tsx +256 -0
  50. package/src/components/editor/room-webgpu-materials.test.ts +27 -0
  51. package/src/components/editor/selection-manager.tsx +451 -317
  52. package/src/components/editor/slab-hole-highlights.tsx +3 -0
  53. package/src/components/editor/snapshot-capture-overlay.tsx +4 -0
  54. package/src/components/editor/snapshot-capture.test.ts +52 -4
  55. package/src/components/editor/snapshot-capture.ts +30 -5
  56. package/src/components/editor/thumbnail-generator.test.ts +308 -0
  57. package/src/components/editor/thumbnail-generator.tsx +1035 -229
  58. package/src/components/editor/use-floorplan-background-placement.ts +31 -11
  59. package/src/components/editor/vector-edge-extractor.tsx +121 -0
  60. package/src/components/editor/views/host-view.tsx +61 -0
  61. package/src/components/editor/views/plugins-view.tsx +670 -0
  62. package/src/components/editor/views/scene-region.tsx +59 -0
  63. package/src/components/editor/views/use-editor-views.ts +57 -0
  64. package/src/components/editor/views/view-bar.tsx +220 -0
  65. package/src/components/editor/wall-justification.test.ts +140 -0
  66. package/src/components/editor/wall-move-side-handles.tsx +43 -142
  67. package/src/components/editor/wall-push-arrow.tsx +92 -0
  68. package/src/components/editor/wall-region-handles.tsx +509 -0
  69. package/src/components/editor/wall-snap-beacon-layer.tsx +3 -0
  70. package/src/components/editor-2d/floorplan-cursor-indicator-overlay.tsx +8 -4
  71. package/src/components/editor-2d/floorplan-open-wall-ends-layer.test.tsx +86 -0
  72. package/src/components/editor-2d/floorplan-open-wall-ends-layer.tsx +290 -0
  73. package/src/components/editor-2d/floorplan-registry-action-menu.tsx +11 -1
  74. package/src/components/editor-2d/floorplan-registry-move-overlay.tsx +113 -102
  75. package/src/components/editor-2d/renderers/floorplan-dimension-edit-overlay.test.ts +156 -0
  76. package/src/components/editor-2d/renderers/floorplan-dimension-edit-overlay.tsx +278 -0
  77. package/src/components/editor-2d/renderers/floorplan-dimension-renderer.tsx +77 -0
  78. package/src/components/editor-2d/renderers/{floorplan-draft-wall-measurement.tsx → floorplan-draft-measurement.tsx} +8 -8
  79. package/src/components/editor-2d/renderers/floorplan-geometry-renderer.tsx +4 -1
  80. package/src/components/editor-2d/renderers/floorplan-registry-layer.test.ts +38 -8
  81. package/src/components/editor-2d/renderers/floorplan-registry-layer.tsx +291 -227
  82. package/src/components/editor-2d/svg-paths.ts +12 -15
  83. package/src/components/procedural-items/inspector.tsx +40 -15
  84. package/src/components/systems/ceiling/ceiling-selection-affordance-system.test.ts +97 -14
  85. package/src/components/systems/ceiling/ceiling-selection-affordance-system.tsx +18 -18
  86. package/src/components/systems/zone/zone-system.test.ts +47 -0
  87. package/src/components/systems/zone/zone-system.tsx +29 -68
  88. package/src/components/tools/item/placement-history.test.tsx +474 -0
  89. package/src/components/tools/item/placement-strategies.ts +8 -4
  90. package/src/components/tools/item/use-draft-node.test.tsx +432 -1
  91. package/src/components/tools/item/use-draft-node.ts +166 -95
  92. package/src/components/tools/item/use-placement-coordinator.tsx +93 -46
  93. package/src/components/tools/paint-region/use-paint-tint.ts +37 -0
  94. package/src/components/tools/paint-region/wall-paint-region-tool.tsx +466 -0
  95. package/src/components/tools/registry/move-registry-node-tool.tsx +3 -1
  96. package/src/components/tools/select/box-select-tool.tsx +2 -12
  97. package/src/components/tools/select/plane-box-select-tool.test.tsx +79 -0
  98. package/src/components/tools/select/plane-box-select-tool.tsx +35 -22
  99. package/src/components/tools/select/select-candidates.test.ts +37 -4
  100. package/src/components/tools/select/select-candidates.ts +15 -11
  101. package/src/components/tools/shared/cursor-sphere.tsx +36 -12
  102. package/src/components/tools/site/site-boundary-editor.tsx +1 -2
  103. package/src/components/tools/stair/stair-defaults.ts +0 -3
  104. package/src/components/tools/stair/stair-tool.tsx +90 -42
  105. package/src/components/tools/tool-manager.tsx +46 -20
  106. package/src/components/tools/wall/wall-drafting.test.ts +19 -4
  107. package/src/components/tools/zone/zone-tool.tsx +4 -12
  108. package/src/components/ui/action-menu/action-button.tsx +3 -0
  109. package/src/components/ui/action-menu/camera-actions.tsx +2 -1
  110. package/src/components/ui/action-menu/control-modes.tsx +2 -35
  111. package/src/components/ui/action-menu/measurement-control.tsx +3 -10
  112. package/src/components/ui/build-panel/build-panel.tsx +235 -0
  113. package/src/components/ui/command-palette/editor-commands.tsx +4 -30
  114. package/src/components/ui/command-palette/index.tsx +5 -24
  115. package/src/components/ui/controls/finish-swatch.tsx +110 -0
  116. package/src/components/ui/controls/material-paint-panel.tsx +97 -26
  117. package/src/components/ui/controls/material-picker.tsx +4 -1
  118. package/src/components/ui/controls/scene-material-list.tsx +4 -32
  119. package/src/components/ui/controls/select-control.tsx +65 -0
  120. package/src/components/ui/floating-level-selector.tsx +9 -14
  121. package/src/components/ui/helpers/building-helper.tsx +4 -1
  122. package/src/components/ui/helpers/contextual-helper-panel.tsx +154 -70
  123. package/src/components/ui/helpers/helper-manager.test.tsx +69 -0
  124. package/src/components/ui/helpers/helper-manager.tsx +325 -14
  125. package/src/components/ui/helpers/item-helper.tsx +8 -0
  126. package/src/components/ui/helpers/registered-tool-helper.tsx +8 -1
  127. package/src/components/ui/icon-ref.tsx +45 -1
  128. package/src/components/ui/level-duplicate-dialog.tsx +1 -1
  129. package/src/components/ui/node-icon-image.tsx +16 -0
  130. package/src/components/ui/panels/collections/collections-popover.tsx +20 -14
  131. package/src/components/ui/panels/floor-foundation-panel.tsx +551 -0
  132. package/src/components/ui/panels/floor-opening-panel.tsx +143 -0
  133. package/src/components/ui/panels/mobile-panel-sheet.tsx +2 -2
  134. package/src/components/ui/panels/mobile-selection-bar.tsx +2 -2
  135. package/src/components/ui/panels/multi-field-value.test.ts +26 -3
  136. package/src/components/ui/panels/multi-field-value.ts +6 -3
  137. package/src/components/ui/panels/multi-parametric-inspector.tsx +52 -2
  138. package/src/components/ui/panels/multi-selection-panel.tsx +43 -4
  139. package/src/components/ui/panels/node-display.ts +8 -3
  140. package/src/components/ui/panels/panel-manager.tsx +103 -68
  141. package/src/components/ui/panels/panel-wrapper.tsx +78 -36
  142. package/src/components/ui/panels/parametric-field-control.tsx +1 -1
  143. package/src/components/ui/panels/parametric-inspector.tsx +4 -4
  144. package/src/components/ui/panels/reference-panel.tsx +2 -3
  145. package/src/components/ui/panels/room-construction-rows.tsx +774 -0
  146. package/src/components/ui/panels/room-name-options.test.ts +55 -0
  147. package/src/components/ui/panels/room-name-options.ts +47 -0
  148. package/src/components/ui/panels/room-openings-field.tsx +110 -0
  149. package/src/components/ui/panels/room-panel.tsx +725 -0
  150. package/src/components/ui/panels/shape-choice.tsx +58 -0
  151. package/src/components/ui/panels/wall-region-list.tsx +109 -0
  152. package/src/components/ui/primitives/color-dot.tsx +36 -24
  153. package/src/components/ui/primitives/sidebar.tsx +2 -0
  154. package/src/components/ui/right-stack.test.ts +29 -0
  155. package/src/components/ui/right-stack.tsx +145 -0
  156. package/src/components/ui/sidebar/icon-rail.tsx +5 -0
  157. package/src/components/ui/sidebar/panels/settings-panel/index.tsx +31 -4
  158. package/src/components/ui/sidebar/panels/settings-panel/keyboard-shortcuts-dialog.tsx +5 -5
  159. package/src/components/ui/sidebar/panels/site-panel/ceiling-tree-node.tsx +4 -4
  160. package/src/components/ui/sidebar/panels/site-panel/chimney-tree-node.tsx +1 -5
  161. package/src/components/ui/sidebar/panels/site-panel/collection-tree-node.tsx +197 -0
  162. package/src/components/ui/sidebar/panels/site-panel/column-tree-node.tsx +1 -5
  163. package/src/components/ui/sidebar/panels/site-panel/door-tree-node.tsx +1 -5
  164. package/src/components/ui/sidebar/panels/site-panel/dormer-tree-node.tsx +1 -5
  165. package/src/components/ui/sidebar/panels/site-panel/elevator-tree-node.tsx +1 -5
  166. package/src/components/ui/sidebar/panels/site-panel/fence-tree-node.tsx +24 -11
  167. package/src/components/ui/sidebar/panels/site-panel/gutter-tree-node.tsx +1 -5
  168. package/src/components/ui/sidebar/panels/site-panel/index.tsx +176 -211
  169. package/src/components/ui/sidebar/panels/site-panel/inline-rename-input.tsx +30 -8
  170. package/src/components/ui/sidebar/panels/site-panel/item-tree-node.tsx +1 -3
  171. package/src/components/ui/sidebar/panels/site-panel/level-tree-node.tsx +4 -1
  172. package/src/components/ui/sidebar/panels/site-panel/registry-tree-node.tsx +1 -3
  173. package/src/components/ui/sidebar/panels/site-panel/roof-tree-node.tsx +1 -4
  174. package/src/components/ui/sidebar/panels/site-panel/shelf-tree-node.tsx +1 -3
  175. package/src/components/ui/sidebar/panels/site-panel/slab-tree-node.tsx +21 -13
  176. package/src/components/ui/sidebar/panels/site-panel/solar-panel-tree-node.tsx +1 -5
  177. package/src/components/ui/sidebar/panels/site-panel/spawn-tree-node.tsx +1 -5
  178. package/src/components/ui/sidebar/panels/site-panel/stair-tree-node.tsx +1 -5
  179. package/src/components/ui/sidebar/panels/site-panel/tree-node.tsx +0 -27
  180. package/src/components/ui/sidebar/panels/site-panel/unit-tree-node.tsx +2 -3
  181. package/src/components/ui/sidebar/panels/site-panel/wall-tree-node.tsx +1 -5
  182. package/src/components/ui/sidebar/panels/site-panel/window-tree-node.tsx +1 -5
  183. package/src/components/ui/sidebar/panels/site-panel/zone-tree-node.tsx +9 -4
  184. package/src/components/ui/sidebar/panels/zone-panel/index.tsx +0 -2
  185. package/src/components/ui/sidebar/tab-bar.tsx +10 -0
  186. package/src/components/ui/sidebar/use-plugin-panels.tsx +15 -5
  187. package/src/components/viewer/floorplan-compass-button.tsx +9 -1
  188. package/src/components/viewer/floorplan-preview.tsx +5 -6
  189. package/src/components/viewer/viewer-hover-label.tsx +34 -0
  190. package/src/components/viewer/viewer-rooms.tsx +175 -0
  191. package/src/components/viewer/viewer-scene-header.tsx +181 -145
  192. package/src/components/viewer/viewer-selection-manager.tsx +124 -0
  193. package/src/components/viewer/viewer-stage.tsx +2 -2
  194. package/src/components/viewer-zone-system.tsx +3 -8
  195. package/src/hooks/use-auto-save.integration.test.tsx +123 -9
  196. package/src/hooks/use-auto-save.ts +34 -8
  197. package/src/hooks/use-keyboard.test.ts +178 -3
  198. package/src/hooks/use-keyboard.ts +89 -71
  199. package/src/hooks/use-open-wall-ends.test.ts +99 -0
  200. package/src/hooks/use-open-wall-ends.ts +238 -0
  201. package/src/hooks/use-placement-notice.test.ts +48 -0
  202. package/src/hooks/use-placement-notice.ts +42 -0
  203. package/src/hooks/use-selected-room.ts +143 -0
  204. package/src/index.tsx +246 -39
  205. package/src/lib/ceiling-edit-session.test.ts +321 -0
  206. package/src/lib/ceiling-edit-session.ts +206 -0
  207. package/src/lib/ceiling-region-session.test.ts +179 -0
  208. package/src/lib/continuation.test.ts +19 -1
  209. package/src/lib/continuation.ts +25 -4
  210. package/src/lib/door-interaction.ts +14 -0
  211. package/src/lib/editor-level-display.test.ts +96 -0
  212. package/src/lib/editor-level-display.ts +48 -0
  213. package/src/lib/editor-views.ts +144 -0
  214. package/src/lib/export-geometry-wait.test.ts +90 -0
  215. package/src/lib/floor-edit-session.ts +153 -0
  216. package/src/lib/floor-footprints.test.ts +563 -0
  217. package/src/lib/floor-footprints.ts +395 -0
  218. package/src/lib/floor-foundation-support.test.ts +283 -0
  219. package/src/lib/floor-opening-draft.ts +218 -0
  220. package/src/lib/floor-openings.test.ts +216 -0
  221. package/src/lib/floor-region-geometry.ts +191 -0
  222. package/src/lib/floor-region-session.test.ts +485 -0
  223. package/src/lib/floor-region-session.ts +589 -0
  224. package/src/lib/floor-region-snap.ts +101 -0
  225. package/src/lib/floor-step-hover.test.ts +32 -0
  226. package/src/lib/floorplan/annotation-visibility.ts +17 -0
  227. package/src/lib/floorplan/dimension-drive.test.ts +360 -0
  228. package/src/lib/floorplan/dimension-drive.ts +479 -0
  229. package/src/lib/floorplan/floorplan-export.test.ts +12 -8
  230. package/src/lib/floorplan/floorplan-export.tsx +32 -8
  231. package/src/lib/floorplan/floorplan-extension.ts +32 -0
  232. package/src/lib/floorplan/floorplan-mode.test.ts +4 -0
  233. package/src/lib/floorplan/floorplan-mode.ts +2 -0
  234. package/src/lib/floorplan/floorplan-pdfkit-document.ts +339 -18
  235. package/src/lib/floorplan/floorplan-pdfkit-renderer.test.ts +138 -8
  236. package/src/lib/floorplan/floorplan-pdfkit-renderer.ts +56 -25
  237. package/src/lib/floorplan/floorplan-plugin-install.test.tsx +138 -0
  238. package/src/lib/floorplan/floorplan-readonly.ts +6 -0
  239. package/src/lib/floorplan/geometry.ts +13 -6
  240. package/src/lib/floorplan/index.ts +0 -9
  241. package/src/lib/floorplan/items.ts +5 -2
  242. package/src/lib/floorplan/marks.test.ts +213 -0
  243. package/src/lib/floorplan/marks.ts +238 -0
  244. package/src/lib/floorplan/open-wall-ends.test.ts +268 -0
  245. package/src/lib/floorplan/open-wall-ends.ts +162 -0
  246. package/src/lib/floorplan/pdf-fonts/GeistMono-Bold.ttf +0 -0
  247. package/src/lib/floorplan/pdf-fonts/GeistMono-Regular.ttf +0 -0
  248. package/src/lib/floorplan/pdf-fonts/LICENSE-GeistMono.txt +92 -0
  249. package/src/lib/floorplan/pdf-fonts/LICENSE-LiberationSans.txt +102 -0
  250. package/src/lib/floorplan/pdf-fonts/LiberationSans-Bold.ttf +0 -0
  251. package/src/lib/floorplan/pdf-fonts/LiberationSans-Regular.ttf +0 -0
  252. package/src/lib/floorplan/schedules.test.ts +189 -0
  253. package/src/lib/floorplan/schedules.ts +339 -0
  254. package/src/lib/floorplan/sheet-export.test.ts +100 -0
  255. package/src/lib/floorplan/sheet-export.ts +318 -0
  256. package/src/lib/floorplan/site-plan/build-site-plan-drawing.ts +779 -0
  257. package/src/lib/floorplan/site-plan/contours.test.ts +43 -0
  258. package/src/lib/floorplan/site-plan/contributors.ts +97 -0
  259. package/src/lib/floorplan/site-plan/coverage.ts +232 -0
  260. package/src/lib/floorplan/site-plan/drawing-type-switch.test.ts +15 -0
  261. package/src/lib/floorplan/site-plan/drawing-type-switch.tsx +86 -0
  262. package/src/lib/floorplan/site-plan/front-edge.test.ts +103 -0
  263. package/src/lib/floorplan/site-plan/front-edge.ts +156 -0
  264. package/src/lib/floorplan/site-plan/geometry.test.ts +275 -0
  265. package/src/lib/floorplan/site-plan/geometry.ts +557 -0
  266. package/src/lib/floorplan/site-plan/index.ts +71 -0
  267. package/src/lib/floorplan/site-plan/site-annotations.test.ts +366 -0
  268. package/src/lib/floorplan/site-plan/site-annotations.ts +1083 -0
  269. package/src/lib/floorplan/site-plan/site-parts.ts +379 -0
  270. package/src/lib/floorplan/site-plan/site-plan-layer.tsx +138 -0
  271. package/src/lib/floorplan/site-plan/street-name.test.ts +69 -0
  272. package/src/lib/floorplan/site-plan/street-name.ts +302 -0
  273. package/src/lib/floorplan/site-plan/yards.test.ts +69 -0
  274. package/src/lib/floorplan/types.ts +1 -27
  275. package/src/lib/fresh-planar-placement.test.ts +36 -0
  276. package/src/lib/fresh-planar-placement.ts +59 -36
  277. package/src/lib/geometry-script/author.ts +152 -0
  278. package/src/lib/geometry-script/client.ts +152 -0
  279. package/src/lib/geometry-script/protocol.ts +12 -0
  280. package/src/lib/gesture-lifecycle.test.ts +595 -0
  281. package/src/lib/gesture-lifecycle.ts +290 -0
  282. package/src/lib/glb-export-zone-holes.test.ts +41 -0
  283. package/src/lib/glb-export.test.ts +485 -0
  284. package/src/lib/glb-export.ts +237 -119
  285. package/src/lib/history.test.ts +6 -0
  286. package/src/lib/history.ts +7 -0
  287. package/src/lib/hud-title.ts +109 -0
  288. package/src/lib/hydration-gated-storage.ts +49 -0
  289. package/src/lib/ifc-export.test.ts +174 -0
  290. package/src/lib/ifc-export.ts +201 -0
  291. package/src/lib/ifc-parts.ts +15 -0
  292. package/src/lib/inspector-expanded.test.ts +44 -0
  293. package/src/lib/inspector-expanded.ts +36 -0
  294. package/src/lib/interaction/scope.ts +18 -0
  295. package/src/lib/keyboard-pan.test.ts +129 -7
  296. package/src/lib/keyboard-pan.ts +161 -12
  297. package/src/lib/live-gesture-round-trip.test.ts +360 -0
  298. package/src/lib/lot/__fixtures__/map-dossier-miami-shores.json +1 -0
  299. package/src/lib/lot/__fixtures__/map-dossier-stpete.json +1 -0
  300. package/src/lib/lot/clean-ring.test.ts +186 -0
  301. package/src/lib/lot/clean-ring.ts +217 -0
  302. package/src/lib/lot/dossier.test.ts +202 -0
  303. package/src/lib/lot/dossier.ts +498 -0
  304. package/src/lib/lot/drop-in.test.ts +326 -0
  305. package/src/lib/lot/drop-in.ts +407 -0
  306. package/src/lib/lot/index.ts +43 -0
  307. package/src/lib/lot/lot-patch.test.ts +340 -0
  308. package/src/lib/lot/lot-patch.ts +394 -0
  309. package/src/lib/lot/parcel-provider.ts +43 -0
  310. package/src/lib/lot/terrain.test.ts +161 -0
  311. package/src/lib/lot/terrain.ts +266 -0
  312. package/src/lib/material-catalog-model.ts +3 -1
  313. package/src/lib/material-paint-panel-model.ts +9 -5
  314. package/src/lib/material-paint.ts +4 -0
  315. package/src/lib/mezzanine-actions.test.ts +342 -0
  316. package/src/lib/mezzanine-draft.test.ts +501 -0
  317. package/src/lib/mezzanine-draft.ts +278 -0
  318. package/src/lib/mezzanine-messages.ts +26 -0
  319. package/src/lib/mezzanine-selection.test.ts +207 -0
  320. package/src/lib/mezzanine-stairs.ts +46 -0
  321. package/src/lib/model-export.ts +3 -1
  322. package/src/lib/node-action-movement.ts +17 -6
  323. package/src/lib/node-display-label.ts +37 -0
  324. package/src/lib/orbit-floor-clearance.test.ts +165 -0
  325. package/src/lib/orbit-floor-clearance.ts +106 -0
  326. package/src/lib/paint-commit-route.test.ts +37 -0
  327. package/src/lib/paint-commit-route.ts +17 -0
  328. package/src/lib/paint-eyedropper.test.tsx +165 -0
  329. package/src/lib/paint-eyedropper.ts +99 -0
  330. package/src/lib/paint-pass-through.test.ts +111 -0
  331. package/src/lib/paint-pass-through.ts +76 -0
  332. package/src/lib/paint-region-hover.ts +25 -0
  333. package/src/lib/paint-region-mode.ts +154 -0
  334. package/src/lib/paint-regions.test.ts +166 -0
  335. package/src/lib/paint-regions.ts +192 -0
  336. package/src/lib/paint-scope.test.ts +317 -35
  337. package/src/lib/paint-scope.ts +266 -25
  338. package/src/lib/panel-rows.ts +21 -0
  339. package/src/lib/parametric-node-update.ts +1 -1
  340. package/src/lib/plate-paint-affected.test.ts +300 -0
  341. package/src/lib/plate-paint-affected.ts +177 -0
  342. package/src/lib/plugin-chat.ts +50 -0
  343. package/src/lib/plugin-panels.test.ts +25 -0
  344. package/src/lib/plugin-panels.ts +94 -0
  345. package/src/lib/print-shell-compiler.ts +1 -0
  346. package/src/lib/registered-draft-snap.ts +12 -0
  347. package/src/lib/roof-duplication.ts +10 -2
  348. package/src/lib/room-assembly-overlay.test.ts +275 -0
  349. package/src/lib/room-assembly-overlay.ts +266 -0
  350. package/src/lib/room-built-on.test.ts +379 -0
  351. package/src/lib/room-built-on.ts +138 -0
  352. package/src/lib/room-construction-commands.ts +712 -0
  353. package/src/lib/room-construction.test.ts +746 -0
  354. package/src/lib/room-construction.ts +385 -0
  355. package/src/lib/room-controls-phase6.test.ts +630 -0
  356. package/src/lib/room-divide-session.ts +523 -0
  357. package/src/lib/room-handle-drag.ts +1068 -0
  358. package/src/lib/room-name-catalog.test.ts +90 -0
  359. package/src/lib/room-name-catalog.ts +168 -0
  360. package/src/lib/room-push-dimensions.test.ts +311 -0
  361. package/src/lib/room-push-dimensions.ts +251 -0
  362. package/src/lib/room-selection-commands.ts +62 -0
  363. package/src/lib/room-selection.test.ts +973 -0
  364. package/src/lib/room-selection.ts +417 -0
  365. package/src/lib/room-structure-commands.ts +94 -0
  366. package/src/lib/room-structure.test.ts +921 -0
  367. package/src/lib/room-transform-session.ts +570 -0
  368. package/src/lib/room-zone-routing.test.ts +294 -0
  369. package/src/lib/room-zone-routing.ts +197 -0
  370. package/src/lib/scene-clipboard.test.ts +364 -0
  371. package/src/lib/scene-clipboard.ts +229 -48
  372. package/src/lib/scene-material-refs.test.ts +79 -0
  373. package/src/lib/scene-material-refs.ts +79 -0
  374. package/src/lib/scene.test.ts +24 -36
  375. package/src/lib/scene.ts +33 -57
  376. package/src/lib/selection-routing.test.ts +129 -45
  377. package/src/lib/selection-routing.ts +53 -74
  378. package/src/lib/session-writes.test.ts +120 -0
  379. package/src/lib/session-writes.ts +205 -0
  380. package/src/lib/sidebar-panel.ts +62 -0
  381. package/src/lib/site-boundary.test.ts +4 -6
  382. package/src/lib/site-boundary.ts +1 -1
  383. package/src/lib/snapping-mode.test.ts +4 -0
  384. package/src/lib/snapping-mode.ts +8 -1
  385. package/src/lib/terrace-draft.test.ts +145 -0
  386. package/src/lib/terrace-draft.ts +213 -0
  387. package/src/lib/tool-cursor-icon.test.ts +16 -0
  388. package/src/lib/tool-cursor-icon.ts +11 -0
  389. package/src/lib/typing-target.ts +9 -0
  390. package/src/lib/units.ts +77 -39
  391. package/src/lib/vector-edges.test.ts +58 -0
  392. package/src/lib/vector-edges.ts +400 -0
  393. package/src/lib/viewer-selection.test.ts +577 -0
  394. package/src/lib/viewer-selection.ts +569 -0
  395. package/src/lib/wall-draw-variant.test.ts +71 -0
  396. package/src/lib/wall-draw-variant.ts +91 -0
  397. package/src/lib/wall-paint-region-session.ts +246 -0
  398. package/src/lib/wall-paint-region.test.ts +373 -0
  399. package/src/lib/wall-polygon-draft.test.ts +262 -0
  400. package/src/lib/wall-polygon-draft.ts +286 -0
  401. package/src/lib/wall-push.test.ts +453 -0
  402. package/src/lib/wall-region-face.ts +352 -0
  403. package/src/lib/wall-region-handles.test.ts +304 -0
  404. package/src/lib/wall-region-handles.ts +368 -0
  405. package/src/lib/wall-region-list.test.ts +119 -0
  406. package/src/lib/wall-region-list.ts +136 -0
  407. package/src/lib/wall-region-snap.ts +110 -0
  408. package/src/lib/window-interaction.ts +14 -0
  409. package/src/lib/zone-content.ts +4 -1
  410. package/src/lib/zone-name.test.ts +17 -0
  411. package/src/lib/zone-name.ts +17 -0
  412. package/src/store/opens-in-select-mode.test.ts +223 -0
  413. package/src/store/paint-mode.test.ts +188 -0
  414. package/src/store/terrain-sculpt-mode.test.ts +19 -21
  415. package/src/store/tool-mode.test.ts +137 -6
  416. package/src/store/use-delete-confirmation.ts +28 -1
  417. package/src/store/use-drawing-view.test.ts +24 -3
  418. package/src/store/use-drawing-view.ts +29 -1
  419. package/src/store/use-editor.tsx +432 -321
  420. package/src/store/use-floorplan-draft-preview.ts +13 -0
  421. package/src/store/use-floorplan-marquee.ts +12 -2
  422. package/src/store/use-quick-measurement-hud.ts +3 -3
  423. package/src/store/use-stair-build-preview.ts +13 -5
  424. package/src/store/use-wall-move-ghosts.ts +4 -2
  425. package/src/store/use-wall-region-handles-selection.test.ts +222 -0
  426. package/src/store/use-wall-region-handles-selection.ts +139 -0
  427. package/src/store/view-layout.test.ts +175 -0
  428. package/src/store/view-layout.ts +196 -0
  429. package/src/test-utils/immediate-animation-frames.ts +16 -0
  430. package/src/test-utils/live-gesture-round-trip.ts +224 -0
  431. package/src/test-utils/selection-harness.ts +82 -0
  432. package/src/components/editor-2d/renderers/floorplan-stair-layer.tsx +0 -499
  433. package/src/components/systems/zone/zone-label-editor-system.tsx +0 -333
  434. package/src/components/tools/fence/fence-drafting.test.ts +0 -100
  435. package/src/components/tools/fence/fence-drafting.ts +0 -266
  436. package/src/components/ui/sidebar/panels/plugins-panel.tsx +0 -218
  437. package/src/components/viewer/viewer-units-panel.tsx +0 -116
  438. package/src/lib/floorplan/stairs.ts +0 -489
  439. package/src/lib/level-duplication.test.ts +0 -144
  440. package/src/lib/level-duplication.ts +0 -195
@@ -1,14 +1,7 @@
1
1
  import '../../../three-types'
2
2
 
3
3
  import { Icon } from '@iconify/react'
4
- import {
5
- type AnyNodeId,
6
- emitter,
7
- type GridEvent,
8
- sceneRegistry,
9
- useScene,
10
- type ZoneNode,
11
- } from '@pascal-app/core'
4
+ import { type AnyNodeId, emitter, type GridEvent, sceneRegistry, useScene } from '@pascal-app/core'
12
5
  import { useViewer } from '@pascal-app/viewer'
13
6
  import { useThree } from '@react-three/fiber'
14
7
  import { useCallback, useEffect, useRef } from 'react'
@@ -26,9 +19,10 @@ import {
26
19
  Vector2,
27
20
  Vector3,
28
21
  } from 'three'
22
+ import { markToolCancelConsumed } from '../../../hooks/use-keyboard'
29
23
  import { EDITOR_LAYER } from '../../../lib/constants'
30
24
  import { sfxEmitter } from '../../../lib/sfx-bus'
31
- import useEditor from '../../../store/use-editor'
25
+ import useInteractionScope from '../../../store/use-interaction-scope'
32
26
  import { CursorSphere } from '../shared/cursor-sphere'
33
27
  import { isBoxSelectPointerSuppressed, markBoxSelectHandled } from './box-select-state'
34
28
  import { collectSelectableCandidateIds } from './select-candidates'
@@ -297,6 +291,7 @@ export const PlaneBoxSelectTool: React.FC = () => {
297
291
  const currentPoint = useRef(new Vector3())
298
292
  const pointerDown = useRef(false)
299
293
  const isDragging = useRef(false)
294
+ const ownedGesture = useRef<object | null>(null)
300
295
  const startClientX = useRef(0)
301
296
  const startClientY = useRef(0)
302
297
  const gridY = useRef(0)
@@ -320,11 +315,36 @@ export const PlaneBoxSelectTool: React.FC = () => {
320
315
  const resetDrag = useCallback(() => {
321
316
  pointerDown.current = false
322
317
  isDragging.current = false
323
- rectFillRef.current.visible = false
318
+ const gesture = ownedGesture.current
319
+ if (gesture)
320
+ useInteractionScope.getState().endIf(() => useInteractionScope.getState().gesture === gesture)
321
+ ownedGesture.current = null
322
+ previousGridPosition.current = null
323
+ if (rectFillRef.current) rectFillRef.current.visible = false
324
324
  outlineRef.current.visible = false
325
325
  syncPreviewSelectedIds([])
326
326
  }, [syncPreviewSelectedIds])
327
327
 
328
+ useEffect(() => {
329
+ const onCancel = () => {
330
+ if (!pointerDown.current) return
331
+ if (
332
+ (!ownedGesture.current && useInteractionScope.getState().scope.kind === 'idle') ||
333
+ (ownedGesture.current && useInteractionScope.getState().gesture === ownedGesture.current)
334
+ ) {
335
+ markToolCancelConsumed()
336
+ }
337
+ resetDrag()
338
+ }
339
+ emitter.on('tool:cancel', onCancel)
340
+ window.addEventListener('pointercancel', resetDrag)
341
+ return () => {
342
+ emitter.off('tool:cancel', onCancel)
343
+ window.removeEventListener('pointercancel', resetDrag)
344
+ resetDrag()
345
+ }
346
+ }, [resetDrag])
347
+
328
348
  const raycastToGround = useCallback(
329
349
  (event: PointerEvent): Vector3 | null => {
330
350
  const rect = gl.domElement.getBoundingClientRect()
@@ -342,12 +362,11 @@ export const PlaneBoxSelectTool: React.FC = () => {
342
362
  useEffect(() => {
343
363
  const outline = outlineRef.current
344
364
  return () => {
345
- previewSelectedIdsRef.current = []
346
- setPreviewSelectedIds([])
365
+ resetDrag()
347
366
  outline.geometry.dispose()
348
367
  ;(outline.material as LineBasicMaterial).dispose()
349
368
  }
350
- }, [setPreviewSelectedIds])
369
+ }, [resetDrag])
351
370
 
352
371
  useEffect(() => {
353
372
  const unsubscribe = useViewer.subscribe((state) => {
@@ -440,15 +459,7 @@ export const PlaneBoxSelectTool: React.FC = () => {
440
459
 
441
460
  const ids = collectNodeIdsInPlaneBounds(bounds)
442
461
  const shouldAppend = event.metaKey || event.ctrlKey || event.shiftKey
443
- const { phase, structureLayer } = useEditor.getState()
444
-
445
- if (phase === 'structure' && structureLayer === 'zones') {
446
- if (ids.length > 0) {
447
- useViewer.getState().setSelection({ zoneId: ids[0] as ZoneNode['id'] })
448
- } else if (!shouldAppend) {
449
- useViewer.getState().setSelection({ zoneId: null })
450
- }
451
- } else if (shouldAppend) {
462
+ if (shouldAppend) {
452
463
  const currentIds = useViewer.getState().selection.selectedIds
453
464
  useViewer.getState().setSelection({
454
465
  selectedIds: Array.from(new Set([...currentIds, ...ids])),
@@ -507,6 +518,8 @@ export const PlaneBoxSelectTool: React.FC = () => {
507
518
  const dy = nativeEvent.clientY - startClientY.current
508
519
  if (!isDragging.current && Math.hypot(dx, dy) >= DRAG_THRESHOLD_PX) {
509
520
  isDragging.current = true
521
+ useInteractionScope.getState().begin({ kind: 'box-select' })
522
+ ownedGesture.current = useInteractionScope.getState().gesture
510
523
  }
511
524
 
512
525
  if (isDragging.current && rectFillRef.current && outlineRef.current) {
@@ -47,10 +47,7 @@ describe('selectable candidates', () => {
47
47
  },
48
48
  previewSelectedIds: [],
49
49
  })
50
- useEditor.setState({
51
- phase: 'structure',
52
- structureLayer: 'elements',
53
- })
50
+ useEditor.setState({ phase: 'building' })
54
51
  })
55
52
 
56
53
  test('includes building-scoped elevators for the active level building', () => {
@@ -106,4 +103,40 @@ describe('selectable candidates', () => {
106
103
 
107
104
  expect(collectSelectableCandidateIds()).toContain('elevator_test')
108
105
  })
106
+
107
+ test('Building box selection includes structural and furnishing kinds on the active level', () => {
108
+ for (const category of ['structure', 'furnish'] as const) {
109
+ const kind = `box-select-test-${category}`
110
+ if (!nodeRegistry.has(kind)) {
111
+ registerNode({
112
+ kind,
113
+ schemaVersion: 1,
114
+ schema: z.object({ type: z.literal(kind) }) as never,
115
+ category,
116
+ defaults: () => ({}),
117
+ capabilities: { selectable: {} },
118
+ } as AnyNodeDefinition)
119
+ }
120
+ }
121
+ useScene.setState({
122
+ nodes: {
123
+ level_test: {
124
+ id: 'level_test',
125
+ type: 'level',
126
+ parentId: 'building_test',
127
+ children: ['wall_pick', 'item_pick', 'item_hidden'],
128
+ },
129
+ wall_pick: { id: 'wall_pick', type: 'box-select-test-structure', parentId: 'level_test' },
130
+ item_pick: { id: 'item_pick', type: 'box-select-test-furnish', parentId: 'level_test' },
131
+ item_hidden: {
132
+ id: 'item_hidden',
133
+ type: 'box-select-test-furnish',
134
+ parentId: 'level_test',
135
+ visible: false,
136
+ },
137
+ item_other: { id: 'item_other', type: 'box-select-test-furnish', parentId: 'level_other' },
138
+ } as unknown as Record<string, AnyNode>,
139
+ })
140
+ expect(collectSelectableCandidateIds().sort()).toEqual(['item_pick', 'wall_pick'])
141
+ })
109
142
  })
@@ -19,17 +19,29 @@ function isVisibleSelectableNode(node: AnyNode): boolean {
19
19
  export function collectSelectableCandidateIds(): string[] {
20
20
  const { levelId } = useViewer.getState().selection
21
21
  const { nodes } = useScene.getState()
22
- const { phase, structureLayer } = useEditor.getState()
22
+ const { phase } = useEditor.getState()
23
23
  const result: string[] = []
24
24
  const seen = new Set<string>()
25
25
  const addNode = (node: AnyNode | undefined) => {
26
- if (!node || seen.has(node.id) || (node as { visible?: boolean }).visible === false) return
26
+ if (
27
+ !node ||
28
+ node.type === 'zone' ||
29
+ seen.has(node.id) ||
30
+ (node as { visible?: boolean }).visible === false
31
+ )
32
+ return
27
33
  seen.add(node.id)
28
34
  result.push(node.id)
29
35
  }
30
36
  const visitLevelDescendant = (id: AnyNodeId) => {
31
37
  const node = nodes[id]
32
- if (!node || seen.has(node.id) || (node as { visible?: boolean }).visible === false) return
38
+ if (
39
+ !node ||
40
+ node.type === 'zone' ||
41
+ seen.has(node.id) ||
42
+ (node as { visible?: boolean }).visible === false
43
+ )
44
+ return
33
45
 
34
46
  if (isRegistrySelectable(node.type)) {
35
47
  addNode(node)
@@ -52,14 +64,6 @@ export function collectSelectableCandidateIds(): string[] {
52
64
  const levelNode = nodes[levelId as AnyNodeId] as LevelNode | undefined
53
65
  if (levelNode?.type !== 'level') return []
54
66
 
55
- if (phase === 'structure' && structureLayer === 'zones') {
56
- for (const childId of levelNode.children) {
57
- const node = nodes[childId as AnyNodeId]
58
- if (node?.type === 'zone') addNode(node)
59
- }
60
- return result
61
- }
62
-
63
67
  for (const childId of levelNode.children) {
64
68
  visitLevelDescendant(childId as AnyNodeId)
65
69
  }
@@ -1,10 +1,11 @@
1
1
  import { Html } from '@react-three/drei'
2
2
  import type { ThreeElements } from '@react-three/fiber'
3
3
  import { forwardRef } from 'react'
4
- import type { Group } from 'three'
4
+ import { CircleGeometry, CylinderGeometry, type Group, SphereGeometry } from 'three'
5
5
  import { furnishTools } from '../../../components/ui/action-menu/furnish-tools'
6
6
  import { tools } from '../../../components/ui/action-menu/structure-tools'
7
7
  import { EDITOR_LAYER } from '../../../lib/constants'
8
+ import { wallCursorIcon } from '../../../lib/tool-cursor-icon'
8
9
  import useEditor from '../../../store/use-editor'
9
10
  import useWallSnapIndicator from '../../../store/use-wall-snap-indicator'
10
11
 
@@ -25,6 +26,19 @@ interface CursorSphereProps extends Omit<ThreeElements['group'], 'ref'> {
25
26
  tooltipContent?: React.ReactNode
26
27
  }
27
28
 
29
+ // One geometry each for the life of the module: the vertical line used to
30
+ // be a `<cylinderGeometry args={[…, height, 8]}>` rebuilt on EVERY height
31
+ // change (the cursor's drop to the floor moves with the pointer), and the
32
+ // ring / dot / tip were built and disposed on every toggle — WebGPU then
33
+ // drew a frame whose vertex buffers had just been destroyed ("Vertex
34
+ // buffer slot 0 required by [RenderPipeline "renderPipeline_MeshBasicMaterial_…"]
35
+ // was not set … DrawIndexed(96 …)" — the 8-segment cylinder's 96 indices).
36
+ // The line is a unit cylinder scaled to `height` instead.
37
+ const DOT_GEOMETRY = new CircleGeometry(0.06, 32)
38
+ const RING_GEOMETRY = new CircleGeometry(0.2, 32)
39
+ const LINE_GEOMETRY = new CylinderGeometry(0.01, 0.01, 1, 8)
40
+ const TIP_GEOMETRY = new SphereGeometry(0.08, 20, 14)
41
+
28
42
  export const CursorSphere = forwardRef<Group, CursorSphereProps>(function CursorSphere(
29
43
  {
30
44
  color = '#818cf8',
@@ -45,11 +59,14 @@ export const CursorSphere = forwardRef<Group, CursorSphereProps>(function Cursor
45
59
  // (green square at a corner); hide the cursor's ground dot/ring so it
46
60
  // doesn't sit on top of that glyph.
47
61
  const isSnapping = useWallSnapIndicator((s) => s.point !== null)
62
+ const wallMode = useEditor((s) => s.continuationByContext.wall)
48
63
 
49
- // Find the icon for the current tool
50
- let activeToolConfig = null
64
+ // Find the icon for the current tool — the wall shows its Rooms variant.
65
+ let activeToolConfig: { label: string; iconSrc: string } | null | undefined = null
51
66
  if (mode === 'build' && tool) {
52
- if (tool === 'item' && catalogCategory) {
67
+ if (tool === 'wall') {
68
+ activeToolConfig = wallCursorIcon(wallMode)
69
+ } else if (tool === 'item' && catalogCategory) {
53
70
  activeToolConfig = furnishTools.find((t) => t.catalogCategory === catalogCategory)
54
71
  } else {
55
72
  activeToolConfig = tools.find((t) => t.id === tool)
@@ -67,8 +84,7 @@ export const CursorSphere = forwardRef<Group, CursorSphereProps>(function Cursor
67
84
  <group rotation={[-Math.PI / 2, 0, 0]}>
68
85
  {/* Center dot — at the ground unless the placement point is elevated */}
69
86
  {!dotAtTip && (
70
- <mesh layers={EDITOR_LAYER} renderOrder={2}>
71
- <circleGeometry args={[0.06, 32]} />
87
+ <mesh geometry={DOT_GEOMETRY} layers={EDITOR_LAYER} renderOrder={2}>
72
88
  <meshBasicMaterial
73
89
  color={color}
74
90
  depthTest={false}
@@ -80,8 +96,7 @@ export const CursorSphere = forwardRef<Group, CursorSphereProps>(function Cursor
80
96
  )}
81
97
 
82
98
  {/* Outer ring / glow */}
83
- <mesh layers={EDITOR_LAYER} renderOrder={2}>
84
- <circleGeometry args={[0.2, 32]} />
99
+ <mesh geometry={RING_GEOMETRY} layers={EDITOR_LAYER} renderOrder={2}>
85
100
  <meshBasicMaterial
86
101
  color={color}
87
102
  depthTest={false}
@@ -95,8 +110,13 @@ export const CursorSphere = forwardRef<Group, CursorSphereProps>(function Cursor
95
110
 
96
111
  {/* Vertical line */}
97
112
  {height > 0 && (
98
- <mesh layers={EDITOR_LAYER} position={[0, height / 2, 0]} renderOrder={2}>
99
- <cylinderGeometry args={[0.01, 0.01, height, 8]} />
113
+ <mesh
114
+ geometry={LINE_GEOMETRY}
115
+ layers={EDITOR_LAYER}
116
+ position={[0, height / 2, 0]}
117
+ renderOrder={2}
118
+ scale={[1, height, 1]}
119
+ >
100
120
  <meshBasicMaterial
101
121
  color={color}
102
122
  depthTest={false}
@@ -110,8 +130,12 @@ export const CursorSphere = forwardRef<Group, CursorSphereProps>(function Cursor
110
130
  {/* Bright marker dot at the tip of the line — the actual placement
111
131
  point, riding at the cursor while the line drops to the floor. */}
112
132
  {dotAtTip && height > 0 && (
113
- <mesh layers={EDITOR_LAYER} position={[0, height, 0]} renderOrder={2}>
114
- <sphereGeometry args={[0.08, 20, 14]} />
133
+ <mesh
134
+ geometry={TIP_GEOMETRY}
135
+ layers={EDITOR_LAYER}
136
+ position={[0, height, 0]}
137
+ renderOrder={2}
138
+ >
115
139
  <meshBasicMaterial color={color} depthTest={false} depthWrite={false} />
116
140
  </mesh>
117
141
  )}
@@ -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>