@pascal-app/editor 1.0.3 → 1.0.5

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 (624) hide show
  1. package/README.md +106 -0
  2. package/package.json +42 -38
  3. package/src/components/editor/__bench__/room-label-placement.bench.ts +108 -0
  4. package/src/components/editor/action-menu-button.tsx +83 -0
  5. package/src/components/editor/bake-thumbnail.tsx +307 -5
  6. package/src/components/editor/capture-level-picker.tsx +72 -0
  7. package/src/components/editor/construction-reveal-audio.test.tsx +150 -0
  8. package/src/components/editor/construction-reveal-audio.tsx +40 -0
  9. package/src/components/editor/custom-camera-controls.tsx +275 -57
  10. package/src/components/editor/delete-confirmation-dialog.tsx +297 -7
  11. package/src/components/editor/duplicate-node.ts +244 -0
  12. package/src/components/editor/editor-layout-mobile.tsx +37 -26
  13. package/src/components/editor/editor-layout-v2.tsx +132 -59
  14. package/src/components/editor/export-manager.tsx +47 -26
  15. package/src/components/editor/first-person/build-collider-world.test.ts +320 -0
  16. package/src/components/editor/first-person/build-collider-world.ts +28 -7
  17. package/src/components/editor/first-person/ground-holes.test.ts +128 -0
  18. package/src/components/editor/first-person/ground-holes.ts +130 -0
  19. package/src/components/editor/first-person/mechanism-targets.test.ts +162 -0
  20. package/src/components/editor/first-person/mechanism-targets.ts +88 -0
  21. package/src/components/editor/first-person-controls.tsx +220 -9
  22. package/src/components/editor/floating-action-menu.tsx +63 -207
  23. package/src/components/editor/floor-aware-camera-controls.test.ts +466 -0
  24. package/src/components/editor/floor-aware-camera-controls.ts +324 -0
  25. package/src/components/editor/floor-openings-overlay.tsx +243 -0
  26. package/src/components/editor/floor-region-controls.tsx +846 -0
  27. package/src/components/editor/floor-region-webgpu-materials.test.ts +12 -0
  28. package/src/components/editor/floor-surface.test.ts +165 -0
  29. package/src/components/editor/floor-surface.ts +182 -0
  30. package/src/components/editor/floorplan-background-selection.test.ts +11 -2
  31. package/src/components/editor/floorplan-background-selection.ts +1 -3
  32. package/src/components/editor/floorplan-mode-coordinator.tsx +2 -2
  33. package/src/components/editor/floorplan-panel.tsx +414 -363
  34. package/src/components/editor/follow-pascal.test.tsx +912 -0
  35. package/src/components/editor/follow-pascal.tsx +354 -0
  36. package/src/components/editor/footprint-height-handle.tsx +204 -0
  37. package/src/components/editor/group-actions.ts +56 -2
  38. package/src/components/editor/group-move-3d.ts +2 -0
  39. package/src/components/editor/group-rotate-handle.tsx +2 -0
  40. package/src/components/editor/group-transform-shared.test.ts +460 -0
  41. package/src/components/editor/handles/handle-hit-priority.test.tsx +63 -0
  42. package/src/components/editor/handles/handle-hit-priority.tsx +37 -0
  43. package/src/components/editor/handles/swallow-next-click.test.ts +28 -0
  44. package/src/components/editor/handles/use-handle-drag.ts +269 -197
  45. package/src/components/editor/index.tsx +409 -197
  46. package/src/components/editor/join-walls-pill.tsx +44 -0
  47. package/src/components/editor/node-action-menu.tsx +68 -70
  48. package/src/components/editor/node-arrow-handles.tsx +25 -6
  49. package/src/components/editor/open-wall-ends-3d-layer.tsx +273 -0
  50. package/src/components/editor/open-wall-ends-hint.tsx +89 -0
  51. package/src/components/editor/quick-measurement-hud.tsx +3 -2
  52. package/src/components/editor/registry-action-contributions.test.tsx +63 -0
  53. package/src/components/editor/registry-action-contributions.tsx +80 -1
  54. package/src/components/editor/riser-diagram-panel.tsx +14 -7
  55. package/src/components/editor/room-controls.tsx +847 -0
  56. package/src/components/editor/room-floor-highlight.tsx +128 -0
  57. package/src/components/editor/room-handles-2d.tsx +354 -0
  58. package/src/components/editor/room-handles.tsx +675 -0
  59. package/src/components/editor/room-highlight.tsx +233 -0
  60. package/src/components/editor/room-labels.test.ts +850 -0
  61. package/src/components/editor/room-labels.tsx +1890 -0
  62. package/src/components/editor/room-transform-ghost.tsx +256 -0
  63. package/src/components/editor/room-webgpu-materials.test.ts +27 -0
  64. package/src/components/editor/selection-manager.tsx +474 -419
  65. package/src/components/editor/slab-hole-highlights.tsx +5 -2
  66. package/src/components/editor/snapshot-capture-overlay.tsx +7 -2
  67. package/src/components/editor/snapshot-capture.test.ts +119 -60
  68. package/src/components/editor/snapshot-capture.ts +36 -5
  69. package/src/components/editor/thumbnail-generator.test.ts +654 -0
  70. package/src/components/editor/thumbnail-generator.tsx +1103 -223
  71. package/src/components/editor/use-floorplan-background-placement.ts +34 -12
  72. package/src/components/editor/vector-edge-extractor.tsx +121 -0
  73. package/src/components/editor/views/host-view.tsx +61 -0
  74. package/src/components/editor/views/plugins-view.tsx +663 -0
  75. package/src/components/editor/views/scene-region.tsx +59 -0
  76. package/src/components/editor/views/use-editor-views.ts +57 -0
  77. package/src/components/editor/views/view-bar.test.tsx +54 -0
  78. package/src/components/editor/views/view-bar.tsx +343 -0
  79. package/src/components/editor/wall-justification.test.ts +140 -0
  80. package/src/components/editor/wall-move-side-handles.tsx +43 -142
  81. package/src/components/editor/wall-push-arrow.tsx +92 -0
  82. package/src/components/editor/wall-region-handles.tsx +509 -0
  83. package/src/components/editor/wall-snap-beacon-layer.tsx +8 -4
  84. package/src/components/editor-2d/floorplan-cursor-indicator-overlay.tsx +8 -4
  85. package/src/components/editor-2d/floorplan-group-move.tsx +3 -0
  86. package/src/components/editor-2d/floorplan-open-wall-ends-layer.test.tsx +86 -0
  87. package/src/components/editor-2d/floorplan-open-wall-ends-layer.tsx +290 -0
  88. package/src/components/editor-2d/floorplan-registered-tool-layer.tsx +2 -2
  89. package/src/components/editor-2d/floorplan-registry-action-menu.tsx +11 -1
  90. package/src/components/editor-2d/floorplan-registry-move-overlay.tsx +116 -106
  91. package/src/components/editor-2d/renderers/floorplan-dimension-edit-overlay.test.ts +156 -0
  92. package/src/components/editor-2d/renderers/floorplan-dimension-edit-overlay.tsx +278 -0
  93. package/src/components/editor-2d/renderers/floorplan-dimension-renderer.tsx +77 -0
  94. package/src/components/editor-2d/renderers/{floorplan-draft-wall-measurement.tsx → floorplan-draft-measurement.tsx} +8 -8
  95. package/src/components/editor-2d/renderers/floorplan-geometry-renderer.tsx +4 -1
  96. package/src/components/editor-2d/renderers/floorplan-registry-layer.test.ts +38 -8
  97. package/src/components/editor-2d/renderers/floorplan-registry-layer.tsx +319 -233
  98. package/src/components/editor-2d/renderers/floorplan-reveal.test.tsx +254 -0
  99. package/src/components/editor-2d/renderers/floorplan-reveal.ts +141 -0
  100. package/src/components/editor-2d/svg-paths.ts +12 -15
  101. package/src/components/point-ask/bubble.tsx +538 -0
  102. package/src/components/point-ask/hover-chip.tsx +91 -0
  103. package/src/components/point-ask/mode-pill.tsx +92 -0
  104. package/src/components/point-ask/optional-motion.ts +22 -0
  105. package/src/components/point-ask/outline-corners.tsx +93 -0
  106. package/src/components/point-ask/pins.tsx +711 -0
  107. package/src/components/point-ask/point-ask-layer.tsx +83 -0
  108. package/src/components/point-ask/point-ask-styles.ts +105 -0
  109. package/src/components/point-ask/region-marquee-styles.ts +15 -0
  110. package/src/components/point-ask/region-marquee.tsx +64 -0
  111. package/src/components/procedural-items/inspector.tsx +40 -15
  112. package/src/components/systems/ceiling/ceiling-bracket-batch.test.ts +12 -2
  113. package/src/components/systems/ceiling/ceiling-selection-affordance-system.test.ts +97 -14
  114. package/src/components/systems/ceiling/ceiling-selection-affordance-system.tsx +18 -18
  115. package/src/components/systems/roof/roof-edit-system.tsx +1 -0
  116. package/src/components/systems/zone/zone-system.test.ts +47 -0
  117. package/src/components/systems/zone/zone-system.tsx +29 -68
  118. package/src/components/tools/distribution/connection-compatibility.test.ts +34 -0
  119. package/src/components/tools/distribution/connection-compatibility.ts +43 -0
  120. package/src/components/tools/distribution/distribution-public.ts +9 -0
  121. package/src/components/tools/distribution/distribution-run-contract.test.ts +26 -0
  122. package/src/components/tools/distribution/distribution-run-contract.ts +94 -0
  123. package/src/components/tools/distribution/distribution-run-tool.test.ts +241 -0
  124. package/src/components/tools/distribution/distribution-run-tool.test.tsx +125 -0
  125. package/src/components/tools/distribution/distribution-run-tool.tsx +1323 -0
  126. package/src/components/tools/distribution/draw-alignment.ts +35 -0
  127. package/src/components/tools/distribution/ports.test.ts +56 -0
  128. package/src/components/tools/distribution/ports.ts +378 -0
  129. package/src/components/tools/distribution/run-cursor.test.ts +135 -0
  130. package/src/components/tools/distribution/run-cursor.ts +36 -0
  131. package/src/components/tools/distribution/run-direction-feedback.test.ts +60 -0
  132. package/src/components/tools/distribution/run-direction-feedback.tsx +251 -0
  133. package/src/components/tools/distribution/run-port-snap.test.ts +79 -0
  134. package/src/components/tools/distribution/run-port-snap.ts +25 -0
  135. package/src/components/tools/distribution/run-screen-direction.test.ts +23 -0
  136. package/src/components/tools/distribution/run-screen-direction.ts +30 -0
  137. package/src/components/tools/elevator/elevator-tool.tsx +3 -6
  138. package/src/components/tools/elevator/move-elevator-tool.tsx +2 -3
  139. package/src/components/tools/item/item-surface-strategy-resolver.test.ts +67 -68
  140. package/src/components/tools/item/placement-history.test.tsx +512 -0
  141. package/src/components/tools/item/placement-strategies.test.ts +46 -1
  142. package/src/components/tools/item/placement-strategies.ts +11 -10
  143. package/src/components/tools/item/shelf-surface-strategy-resolver.test.ts +56 -56
  144. package/src/components/tools/item/use-draft-node.test.tsx +432 -1
  145. package/src/components/tools/item/use-draft-node.ts +166 -95
  146. package/src/components/tools/item/use-placement-coordinator.tsx +148 -95
  147. package/src/components/tools/paint-region/use-paint-tint.ts +37 -0
  148. package/src/components/tools/paint-region/wall-paint-region-tool.tsx +466 -0
  149. package/src/components/tools/registry/item-surface-move.test.ts +62 -63
  150. package/src/components/tools/registry/item-surface-move.ts +2 -0
  151. package/src/components/tools/registry/item-surface-resolver.test.ts +26 -26
  152. package/src/components/tools/registry/move-registry-node-tool.test.ts +268 -1
  153. package/src/components/tools/registry/move-registry-node-tool.tsx +66 -30
  154. package/src/components/tools/select/box-select-tool.tsx +2 -12
  155. package/src/components/tools/select/plane-box-select-tool.test.tsx +79 -0
  156. package/src/components/tools/select/plane-box-select-tool.tsx +35 -22
  157. package/src/components/tools/select/select-candidates.test.ts +37 -4
  158. package/src/components/tools/select/select-candidates.ts +15 -11
  159. package/src/components/tools/shared/cursor-sphere.tsx +36 -12
  160. package/src/components/tools/site/site-boundary-editor.tsx +1 -2
  161. package/src/components/tools/tool-manager.tsx +59 -24
  162. package/src/components/tools/wall/wall-drafting.test.ts +175 -47
  163. package/src/components/tools/zone/zone-tool.tsx +4 -12
  164. package/src/components/ui/action-menu/action-button.tsx +9 -1
  165. package/src/components/ui/action-menu/camera-actions.tsx +9 -4
  166. package/src/components/ui/action-menu/control-modes.tsx +15 -42
  167. package/src/components/ui/action-menu/follow-pascal-pill.tsx +32 -0
  168. package/src/components/ui/action-menu/follow-pascal-toggle.tsx +36 -0
  169. package/src/components/ui/action-menu/index.tsx +56 -33
  170. package/src/components/ui/action-menu/measurement-control.tsx +14 -19
  171. package/src/components/ui/action-menu/placement.ts +20 -0
  172. package/src/components/ui/action-menu/point-ask-button.tsx +52 -0
  173. package/src/components/ui/action-menu/view-toggles.tsx +47 -42
  174. package/src/components/ui/build-panel/build-panel.tsx +242 -0
  175. package/src/components/ui/command-palette/editor-commands.tsx +7 -42
  176. package/src/components/ui/command-palette/index.tsx +50 -39
  177. package/src/components/ui/command-palette/ranked-groups.test.ts +31 -0
  178. package/src/components/ui/controls/action-button.tsx +5 -2
  179. package/src/components/ui/controls/finish-swatch.tsx +110 -0
  180. package/src/components/ui/controls/material-paint-panel.tsx +97 -26
  181. package/src/components/ui/controls/material-picker.tsx +4 -1
  182. package/src/components/ui/controls/metric-control.tsx +1 -1
  183. package/src/components/ui/controls/scene-material-list.tsx +4 -32
  184. package/src/components/ui/controls/segmented-control.tsx +21 -8
  185. package/src/components/ui/controls/select-control.tsx +65 -0
  186. package/src/components/ui/controls/slider-control.tsx +1 -1
  187. package/src/components/ui/controls/toggle-control.tsx +3 -3
  188. package/src/components/ui/controls/tool-options-panel.tsx +1 -1
  189. package/src/components/ui/floating-level-selector.tsx +159 -59
  190. package/src/components/ui/helpers/building-helper.tsx +4 -1
  191. package/src/components/ui/helpers/contextual-helper-panel.tsx +181 -72
  192. package/src/components/ui/helpers/helper-manager.test.tsx +121 -0
  193. package/src/components/ui/helpers/helper-manager.tsx +331 -14
  194. package/src/components/ui/helpers/item-helper.tsx +17 -1
  195. package/src/components/ui/helpers/registered-tool-helper.tsx +8 -1
  196. package/src/components/ui/icon-ref.tsx +45 -1
  197. package/src/components/ui/level-duplicate-dialog.tsx +1 -1
  198. package/src/components/ui/node-icon-image.tsx +16 -0
  199. package/src/components/ui/panels/collections/collections-popover.tsx +28 -22
  200. package/src/components/ui/panels/floor-foundation-panel.tsx +551 -0
  201. package/src/components/ui/panels/floor-opening-panel.tsx +143 -0
  202. package/src/components/ui/panels/mobile-panel-sheet.tsx +4 -4
  203. package/src/components/ui/panels/mobile-selection-bar.tsx +8 -5
  204. package/src/components/ui/panels/multi-field-value.test.ts +26 -3
  205. package/src/components/ui/panels/multi-field-value.ts +6 -3
  206. package/src/components/ui/panels/multi-parametric-inspector.tsx +52 -2
  207. package/src/components/ui/panels/multi-selection-panel.tsx +161 -5
  208. package/src/components/ui/panels/node-display.ts +8 -3
  209. package/src/components/ui/panels/panel-manager.tsx +103 -68
  210. package/src/components/ui/panels/panel-wrapper.tsx +130 -62
  211. package/src/components/ui/panels/parametric-field-control.tsx +3 -3
  212. package/src/components/ui/panels/parametric-inspector.tsx +7 -7
  213. package/src/components/ui/panels/reference-panel.tsx +2 -3
  214. package/src/components/ui/panels/room-construction-rows.tsx +774 -0
  215. package/src/components/ui/panels/room-name-options.test.ts +55 -0
  216. package/src/components/ui/panels/room-name-options.ts +47 -0
  217. package/src/components/ui/panels/room-openings-field.tsx +110 -0
  218. package/src/components/ui/panels/room-panel.tsx +790 -0
  219. package/src/components/ui/panels/shape-choice.tsx +58 -0
  220. package/src/components/ui/panels/wall-region-list.tsx +109 -0
  221. package/src/components/ui/primitives/badge.tsx +8 -0
  222. package/src/components/ui/primitives/color-dot.tsx +36 -24
  223. package/src/components/ui/primitives/sidebar.tsx +3 -1
  224. package/src/components/ui/right-stack.test.ts +29 -0
  225. package/src/components/ui/right-stack.tsx +164 -0
  226. package/src/components/ui/sidebar/app-sidebar.tsx +1 -1
  227. package/src/components/ui/sidebar/icon-rail.tsx +8 -3
  228. package/src/components/ui/sidebar/panels/items-panel/function-tree-panel.tsx +1 -1
  229. package/src/components/ui/sidebar/panels/items-panel/index.tsx +6 -6
  230. package/src/components/ui/sidebar/panels/settings-panel/animation-speed-setting.tsx +40 -0
  231. package/src/components/ui/sidebar/panels/settings-panel/audio-settings-dialog.tsx +7 -8
  232. package/src/components/ui/sidebar/panels/settings-panel/construction-animation-setting.test.tsx +170 -0
  233. package/src/components/ui/sidebar/panels/settings-panel/construction-animation-setting.tsx +92 -0
  234. package/src/components/ui/sidebar/panels/settings-panel/index.tsx +138 -27
  235. package/src/components/ui/sidebar/panels/settings-panel/interface-theme.test.tsx +20 -0
  236. package/src/components/ui/sidebar/panels/settings-panel/keyboard-shortcuts-dialog.tsx +5 -5
  237. package/src/components/ui/sidebar/panels/settings-panel/print-export-button.tsx +4 -10
  238. package/src/components/ui/sidebar/panels/site-panel/building-tree-node.tsx +3 -11
  239. package/src/components/ui/sidebar/panels/site-panel/ceiling-tree-node.tsx +4 -4
  240. package/src/components/ui/sidebar/panels/site-panel/chimney-tree-node.tsx +1 -5
  241. package/src/components/ui/sidebar/panels/site-panel/collection-tree-node.tsx +197 -0
  242. package/src/components/ui/sidebar/panels/site-panel/column-tree-node.tsx +1 -5
  243. package/src/components/ui/sidebar/panels/site-panel/door-tree-node.tsx +1 -5
  244. package/src/components/ui/sidebar/panels/site-panel/dormer-tree-node.tsx +1 -5
  245. package/src/components/ui/sidebar/panels/site-panel/elevator-tree-node.tsx +1 -5
  246. package/src/components/ui/sidebar/panels/site-panel/fence-tree-node.tsx +24 -11
  247. package/src/components/ui/sidebar/panels/site-panel/gutter-tree-node.tsx +1 -5
  248. package/src/components/ui/sidebar/panels/site-panel/index.tsx +181 -223
  249. package/src/components/ui/sidebar/panels/site-panel/inline-rename-input.tsx +30 -8
  250. package/src/components/ui/sidebar/panels/site-panel/item-tree-node.tsx +1 -3
  251. package/src/components/ui/sidebar/panels/site-panel/level-tree-node.tsx +4 -1
  252. package/src/components/ui/sidebar/panels/site-panel/registry-tree-node.tsx +1 -3
  253. package/src/components/ui/sidebar/panels/site-panel/roof-tree-node.tsx +1 -4
  254. package/src/components/ui/sidebar/panels/site-panel/shelf-tree-node.tsx +1 -3
  255. package/src/components/ui/sidebar/panels/site-panel/slab-tree-node.tsx +21 -13
  256. package/src/components/ui/sidebar/panels/site-panel/solar-panel-tree-node.tsx +1 -5
  257. package/src/components/ui/sidebar/panels/site-panel/spawn-tree-node.tsx +1 -5
  258. package/src/components/ui/sidebar/panels/site-panel/stair-tree-node.tsx +1 -5
  259. package/src/components/ui/sidebar/panels/site-panel/tree-node.tsx +13 -37
  260. package/src/components/ui/sidebar/panels/site-panel/unit-tree-node.tsx +2 -3
  261. package/src/components/ui/sidebar/panels/site-panel/wall-tree-node.tsx +1 -5
  262. package/src/components/ui/sidebar/panels/site-panel/window-tree-node.tsx +1 -5
  263. package/src/components/ui/sidebar/panels/site-panel/zone-tree-node.tsx +9 -4
  264. package/src/components/ui/sidebar/panels/zone-panel/index.tsx +2 -4
  265. package/src/components/ui/sidebar/tab-bar.tsx +14 -4
  266. package/src/components/ui/sidebar/use-plugin-panels.tsx +15 -5
  267. package/src/components/viewer/floorplan-compass-button.tsx +9 -1
  268. package/src/components/viewer/floorplan-preview.tsx +5 -6
  269. package/src/components/viewer/viewer-controls-bar.tsx +18 -6
  270. package/src/components/viewer/viewer-hover-label.tsx +34 -0
  271. package/src/components/viewer/viewer-rooms.tsx +191 -0
  272. package/src/components/viewer/viewer-scene-header.tsx +181 -145
  273. package/src/components/viewer/viewer-selection-manager.tsx +124 -0
  274. package/src/components/viewer/viewer-stage.tsx +2 -2
  275. package/src/components/viewer-zone-system.tsx +3 -8
  276. package/src/hooks/use-auto-save.integration.test.tsx +215 -43
  277. package/src/hooks/use-auto-save.ts +60 -9
  278. package/src/hooks/use-ceiling-events.test.ts +123 -118
  279. package/src/hooks/use-grid-events.test.ts +105 -105
  280. package/src/hooks/use-keyboard.test.ts +243 -19
  281. package/src/hooks/use-keyboard.ts +118 -83
  282. package/src/hooks/use-open-wall-ends.test.ts +99 -0
  283. package/src/hooks/use-open-wall-ends.ts +238 -0
  284. package/src/hooks/use-placement-notice.test.ts +71 -0
  285. package/src/hooks/use-placement-notice.ts +73 -0
  286. package/src/hooks/use-selected-room.ts +143 -0
  287. package/src/index.tsx +312 -58
  288. package/src/lib/__fixtures__/owner-victor-seattle-parking.json +1 -0
  289. package/src/lib/__fixtures__/owner-villa-aurelia-basement.json +1 -0
  290. package/src/lib/add-storey.test.ts +177 -0
  291. package/src/lib/add-storey.ts +59 -0
  292. package/src/lib/below-grade.test.tsx +171 -0
  293. package/src/lib/below-grade.ts +133 -0
  294. package/src/lib/camera-facing.test.ts +24 -0
  295. package/src/lib/camera-facing.ts +24 -0
  296. package/src/lib/ceiling-edit-session.test.ts +321 -0
  297. package/src/lib/ceiling-edit-session.ts +206 -0
  298. package/src/lib/ceiling-region-session.test.ts +179 -0
  299. package/src/lib/continuation.test.ts +19 -1
  300. package/src/lib/continuation.ts +25 -4
  301. package/src/lib/direct-manipulation.ts +2 -0
  302. package/src/lib/door-interaction.ts +14 -0
  303. package/src/lib/download-project-file.ts +17 -0
  304. package/src/lib/editor-level-display.test.ts +96 -0
  305. package/src/lib/editor-level-display.ts +55 -0
  306. package/src/lib/editor-views.ts +178 -0
  307. package/src/lib/export-geometry-wait.test.ts +90 -0
  308. package/src/lib/export-texture-utils.ts +2 -4
  309. package/src/lib/floor-edit-session.ts +153 -0
  310. package/src/lib/floor-footprints.test.ts +563 -0
  311. package/src/lib/floor-footprints.ts +395 -0
  312. package/src/lib/floor-foundation-support.test.ts +284 -0
  313. package/src/lib/floor-opening-draft.ts +218 -0
  314. package/src/lib/floor-openings.test.ts +216 -0
  315. package/src/lib/floor-region-geometry.ts +191 -0
  316. package/src/lib/floor-region-session.test.ts +485 -0
  317. package/src/lib/floor-region-session.ts +589 -0
  318. package/src/lib/floor-region-snap.ts +101 -0
  319. package/src/lib/floor-step-hover.test.ts +32 -0
  320. package/src/lib/floorplan/annotation-visibility.ts +17 -0
  321. package/src/lib/floorplan/dimension-drive.test.ts +360 -0
  322. package/src/lib/floorplan/dimension-drive.ts +479 -0
  323. package/src/lib/floorplan/floorplan-export.test.ts +12 -8
  324. package/src/lib/floorplan/floorplan-export.tsx +32 -8
  325. package/src/lib/floorplan/floorplan-extension.ts +32 -0
  326. package/src/lib/floorplan/floorplan-mode.test.ts +4 -0
  327. package/src/lib/floorplan/floorplan-mode.ts +2 -0
  328. package/src/lib/floorplan/floorplan-pdfkit-document.ts +363 -27
  329. package/src/lib/floorplan/floorplan-pdfkit-renderer.test.ts +177 -8
  330. package/src/lib/floorplan/floorplan-pdfkit-renderer.ts +60 -25
  331. package/src/lib/floorplan/floorplan-plugin-install.test.tsx +138 -0
  332. package/src/lib/floorplan/floorplan-readonly.ts +6 -0
  333. package/src/lib/floorplan/geometry.ts +13 -6
  334. package/src/lib/floorplan/index.ts +0 -9
  335. package/src/lib/floorplan/items.ts +5 -2
  336. package/src/lib/floorplan/marks.test.ts +213 -0
  337. package/src/lib/floorplan/marks.ts +238 -0
  338. package/src/lib/floorplan/open-wall-ends.test.ts +268 -0
  339. package/src/lib/floorplan/open-wall-ends.ts +162 -0
  340. package/src/lib/floorplan/pdf-fonts/GeistMono-Bold.ttf +0 -0
  341. package/src/lib/floorplan/pdf-fonts/GeistMono-Regular.ttf +0 -0
  342. package/src/lib/floorplan/pdf-fonts/LICENSE-GeistMono.txt +92 -0
  343. package/src/lib/floorplan/pdf-fonts/LICENSE-LiberationSans.txt +102 -0
  344. package/src/lib/floorplan/pdf-fonts/LiberationSans-Bold.ttf +0 -0
  345. package/src/lib/floorplan/pdf-fonts/LiberationSans-Regular.ttf +0 -0
  346. package/src/lib/floorplan/pdfkit-standard-fonts.d.ts +5 -0
  347. package/src/lib/floorplan/schedules.test.ts +189 -0
  348. package/src/lib/floorplan/schedules.ts +339 -0
  349. package/src/lib/floorplan/sheet-export.test.ts +124 -0
  350. package/src/lib/floorplan/sheet-export.ts +318 -0
  351. package/src/lib/floorplan/site-plan/build-site-plan-drawing.ts +768 -0
  352. package/src/lib/floorplan/site-plan/contours.test.ts +43 -0
  353. package/src/lib/floorplan/site-plan/contributors.ts +97 -0
  354. package/src/lib/floorplan/site-plan/coverage.ts +230 -0
  355. package/src/lib/floorplan/site-plan/drawing-type-switch.test.ts +15 -0
  356. package/src/lib/floorplan/site-plan/drawing-type-switch.tsx +86 -0
  357. package/src/lib/floorplan/site-plan/front-edge.test.ts +103 -0
  358. package/src/lib/floorplan/site-plan/front-edge.ts +157 -0
  359. package/src/lib/floorplan/site-plan/geometry.test.ts +269 -0
  360. package/src/lib/floorplan/site-plan/geometry.ts +142 -0
  361. package/src/lib/floorplan/site-plan/index.ts +55 -0
  362. package/src/lib/floorplan/site-plan/site-annotations.test.ts +483 -0
  363. package/src/lib/floorplan/site-plan/site-annotations.ts +1084 -0
  364. package/src/lib/floorplan/site-plan/site-parts.ts +287 -0
  365. package/src/lib/floorplan/site-plan/site-plan-layer.tsx +138 -0
  366. package/src/lib/floorplan/site-plan/street-name.test.ts +69 -0
  367. package/src/lib/floorplan/site-plan/street-name.ts +302 -0
  368. package/src/lib/floorplan/site-plan/yards.test.ts +70 -0
  369. package/src/lib/floorplan/types.ts +1 -27
  370. package/src/lib/follow-pascal-exterior.test-fixture.ts +17 -0
  371. package/src/lib/follow-pascal-exterior.test.ts +171 -0
  372. package/src/lib/follow-pascal-exterior.ts +39 -0
  373. package/src/lib/follow-pascal-presentation.test.ts +203 -0
  374. package/src/lib/follow-pascal-presentation.ts +129 -0
  375. package/src/lib/follow-pascal-sight.ts +84 -0
  376. package/src/lib/follow-pascal-targets.test.ts +406 -0
  377. package/src/lib/follow-pascal-targets.ts +198 -0
  378. package/src/lib/follow-pascal.test.ts +350 -0
  379. package/src/lib/follow-pascal.ts +284 -0
  380. package/src/lib/fresh-planar-placement.test.ts +148 -0
  381. package/src/lib/fresh-planar-placement.ts +114 -37
  382. package/src/lib/geometry-script/author.ts +157 -0
  383. package/src/lib/geometry-script/client.ts +159 -0
  384. package/src/lib/geometry-script/finish-scripts.ts +127 -0
  385. package/src/lib/geometry-script/protocol.ts +13 -0
  386. package/src/lib/gesture-lifecycle.test.ts +595 -0
  387. package/src/lib/gesture-lifecycle.ts +290 -0
  388. package/src/lib/glb-export-zone-holes.test.ts +41 -0
  389. package/src/lib/glb-export.test.ts +636 -126
  390. package/src/lib/glb-export.ts +241 -120
  391. package/src/lib/history.test.ts +18 -7
  392. package/src/lib/history.ts +7 -0
  393. package/src/lib/hud-title.ts +109 -0
  394. package/src/lib/hydration-gated-storage.ts +49 -0
  395. package/src/lib/ifc-export.test.ts +174 -0
  396. package/src/lib/ifc-export.ts +201 -0
  397. package/src/lib/ifc-parts.ts +15 -0
  398. package/src/lib/inspector-expanded.test.ts +44 -0
  399. package/src/lib/inspector-expanded.ts +36 -0
  400. package/src/lib/interaction/scope.ts +22 -0
  401. package/src/lib/keyboard-pan.test.ts +129 -7
  402. package/src/lib/keyboard-pan.ts +161 -12
  403. package/src/lib/level-selection.test.ts +57 -0
  404. package/src/lib/level-selection.ts +16 -1
  405. package/src/lib/live-gesture-round-trip.test.ts +360 -0
  406. package/src/lib/lot/__fixtures__/map-dossier-miami-shores.json +1 -0
  407. package/src/lib/lot/__fixtures__/map-dossier-stpete.json +1 -0
  408. package/src/lib/lot/clean-ring.test.ts +187 -0
  409. package/src/lib/lot/clean-ring.ts +217 -0
  410. package/src/lib/lot/dossier.test.ts +202 -0
  411. package/src/lib/lot/dossier.ts +498 -0
  412. package/src/lib/lot/drop-in.test.ts +326 -0
  413. package/src/lib/lot/drop-in.ts +392 -0
  414. package/src/lib/lot/index.ts +43 -0
  415. package/src/lib/lot/lot-patch.test.ts +340 -0
  416. package/src/lib/lot/lot-patch.ts +394 -0
  417. package/src/lib/lot/parcel-provider.ts +43 -0
  418. package/src/lib/lot/terrain.test.ts +161 -0
  419. package/src/lib/lot/terrain.ts +266 -0
  420. package/src/lib/material-catalog-model.ts +3 -1
  421. package/src/lib/material-paint-panel-model.ts +9 -5
  422. package/src/lib/material-paint.ts +4 -0
  423. package/src/lib/measurement-kind.test.ts +6 -8
  424. package/src/lib/measurements.test.ts +23 -0
  425. package/src/lib/mezzanine-actions.test.ts +342 -0
  426. package/src/lib/mezzanine-draft.test.ts +501 -0
  427. package/src/lib/mezzanine-draft.ts +278 -0
  428. package/src/lib/mezzanine-messages.ts +26 -0
  429. package/src/lib/mezzanine-selection.test.ts +207 -0
  430. package/src/lib/mezzanine-stairs.ts +46 -0
  431. package/src/lib/model-export.ts +3 -1
  432. package/src/lib/node-action-movement.ts +17 -6
  433. package/src/lib/node-display-label.ts +37 -0
  434. package/src/lib/orbit-floor-clearance.test.ts +201 -0
  435. package/src/lib/orbit-floor-clearance.ts +154 -0
  436. package/src/lib/paint-commit-route.test.ts +37 -0
  437. package/src/lib/paint-commit-route.ts +17 -0
  438. package/src/lib/paint-eyedropper.test.tsx +165 -0
  439. package/src/lib/paint-eyedropper.ts +99 -0
  440. package/src/lib/paint-pass-through.test.ts +122 -0
  441. package/src/lib/paint-pass-through.ts +78 -0
  442. package/src/lib/paint-region-hover.ts +25 -0
  443. package/src/lib/paint-region-mode.ts +154 -0
  444. package/src/lib/paint-regions.test.ts +166 -0
  445. package/src/lib/paint-regions.ts +192 -0
  446. package/src/lib/paint-scope.test.ts +317 -35
  447. package/src/lib/paint-scope.ts +267 -25
  448. package/src/lib/panel-rows.ts +21 -0
  449. package/src/lib/parametric-node-update.ts +16 -7
  450. package/src/lib/plate-paint-affected.test.ts +300 -0
  451. package/src/lib/plate-paint-affected.ts +177 -0
  452. package/src/lib/plugin-chat.ts +65 -0
  453. package/src/lib/plugin-panels.test.ts +25 -0
  454. package/src/lib/plugin-panels.ts +94 -0
  455. package/src/lib/point-ask/__fixtures__/consumer-browser.tsx +76 -0
  456. package/src/lib/point-ask/__fixtures__/house.ts +81 -0
  457. package/src/lib/point-ask/ask-created.test.ts +195 -0
  458. package/src/lib/point-ask/ask-flow.test.ts +380 -0
  459. package/src/lib/point-ask/build-context.test.ts +173 -0
  460. package/src/lib/point-ask/build-context.ts +94 -0
  461. package/src/lib/point-ask/capture-crop.ts +129 -0
  462. package/src/lib/point-ask/choreography.ts +69 -0
  463. package/src/lib/point-ask/compare.test.ts +145 -0
  464. package/src/lib/point-ask/compare.ts +73 -0
  465. package/src/lib/point-ask/describe-target.test.ts +165 -0
  466. package/src/lib/point-ask/describe-target.ts +234 -0
  467. package/src/lib/point-ask/dictation.ts +79 -0
  468. package/src/lib/point-ask/edge-marker.test.ts +61 -0
  469. package/src/lib/point-ask/edge-marker.ts +27 -0
  470. package/src/lib/point-ask/frame-loop.ts +25 -0
  471. package/src/lib/point-ask/history-marks.test.ts +94 -0
  472. package/src/lib/point-ask/history-marks.ts +39 -0
  473. package/src/lib/point-ask/host-api.ts +32 -0
  474. package/src/lib/point-ask/outline-box.test.ts +30 -0
  475. package/src/lib/point-ask/outline-box.ts +11 -0
  476. package/src/lib/point-ask/overlay-metrics.ts +6 -0
  477. package/src/lib/point-ask/pick.test.ts +220 -0
  478. package/src/lib/point-ask/pick.ts +121 -0
  479. package/src/lib/point-ask/pin-machine.test.ts +494 -0
  480. package/src/lib/point-ask/pin-machine.ts +217 -0
  481. package/src/lib/point-ask/placement.test.ts +203 -0
  482. package/src/lib/point-ask/placement.ts +141 -0
  483. package/src/lib/point-ask/pointer-tracker.ts +34 -0
  484. package/src/lib/point-ask/projector.ts +83 -0
  485. package/src/lib/point-ask/region.test.ts +157 -0
  486. package/src/lib/point-ask/region.ts +136 -0
  487. package/src/lib/point-ask/retry.test.ts +217 -0
  488. package/src/lib/point-ask/retry.ts +37 -0
  489. package/src/lib/point-ask/scene-commit.test.ts +312 -0
  490. package/src/lib/point-ask/scene-commit.ts +32 -0
  491. package/src/lib/point-ask/session.ts +1391 -0
  492. package/src/lib/point-ask/spring-keyframes.test.ts +45 -0
  493. package/src/lib/point-ask/spring-keyframes.ts +38 -0
  494. package/src/lib/point-ask/spring.test.ts +63 -0
  495. package/src/lib/point-ask/spring.ts +107 -0
  496. package/src/lib/point-ask/suggestions.test.ts +80 -0
  497. package/src/lib/point-ask/suggestions.ts +125 -0
  498. package/src/lib/point-ask/target-scope.test.ts +622 -0
  499. package/src/lib/point-ask/target-scope.ts +338 -0
  500. package/src/lib/point-ask/types.ts +48 -0
  501. package/src/lib/portable-export.test.ts +34 -1
  502. package/src/lib/portable-export.ts +119 -53
  503. package/src/lib/print-feature-thickness.ts +10 -1
  504. package/src/lib/print-roof-solids.test.ts +39 -1
  505. package/src/lib/print-roof-solids.ts +43 -10
  506. package/src/lib/print-shell-compiler-manifold-core.ts +1 -1
  507. package/src/lib/print-shell-compiler.ts +1 -0
  508. package/src/lib/registered-draft-snap.ts +12 -0
  509. package/src/lib/roof-duplication.ts +10 -2
  510. package/src/lib/room-assembly-overlay.test.ts +275 -0
  511. package/src/lib/room-assembly-overlay.ts +265 -0
  512. package/src/lib/room-built-on.test.ts +379 -0
  513. package/src/lib/room-built-on.ts +138 -0
  514. package/src/lib/room-construction-commands.ts +712 -0
  515. package/src/lib/room-construction.test.ts +746 -0
  516. package/src/lib/room-construction.ts +385 -0
  517. package/src/lib/room-controls-phase6.test.ts +630 -0
  518. package/src/lib/room-divide-session.ts +523 -0
  519. package/src/lib/room-handle-drag.ts +1068 -0
  520. package/src/lib/room-name-catalog.test.ts +90 -0
  521. package/src/lib/room-name-catalog.ts +168 -0
  522. package/src/lib/room-push-dimensions.test.ts +311 -0
  523. package/src/lib/room-push-dimensions.ts +251 -0
  524. package/src/lib/room-selection-commands.ts +62 -0
  525. package/src/lib/room-selection.test.ts +1017 -0
  526. package/src/lib/room-selection.ts +417 -0
  527. package/src/lib/room-structure-commands.ts +94 -0
  528. package/src/lib/room-structure.test.ts +921 -0
  529. package/src/lib/room-transform-session.ts +570 -0
  530. package/src/lib/room-zone-routing.test.ts +294 -0
  531. package/src/lib/room-zone-routing.ts +197 -0
  532. package/src/lib/scene-clipboard.test.ts +610 -1
  533. package/src/lib/scene-clipboard.ts +269 -58
  534. package/src/lib/scene-material-refs.test.ts +79 -0
  535. package/src/lib/scene-material-refs.ts +79 -0
  536. package/src/lib/scene.test.ts +24 -36
  537. package/src/lib/scene.ts +33 -57
  538. package/src/lib/selection-routing.test.ts +129 -45
  539. package/src/lib/selection-routing.ts +54 -75
  540. package/src/lib/session-writes.test.ts +120 -0
  541. package/src/lib/session-writes.ts +205 -0
  542. package/src/lib/sfx-bus.ts +13 -0
  543. package/src/lib/sfx-player.test.ts +70 -9
  544. package/src/lib/sfx-player.ts +6 -0
  545. package/src/lib/sidebar-panel.ts +62 -0
  546. package/src/lib/site-boundary.test.ts +4 -6
  547. package/src/lib/site-boundary.ts +1 -1
  548. package/src/lib/snapping-mode.test.ts +4 -0
  549. package/src/lib/snapping-mode.ts +8 -1
  550. package/src/lib/stair-levels.test.ts +25 -0
  551. package/src/lib/stair-levels.ts +22 -3
  552. package/src/lib/terrace-draft.test.ts +145 -0
  553. package/src/lib/terrace-draft.ts +214 -0
  554. package/src/lib/tool-cursor-icon.test.ts +16 -0
  555. package/src/lib/tool-cursor-icon.ts +11 -0
  556. package/src/lib/typing-target.ts +9 -0
  557. package/src/lib/units.ts +77 -39
  558. package/src/lib/vector-edges.test.ts +58 -0
  559. package/src/lib/vector-edges.ts +400 -0
  560. package/src/lib/viewer-selection.test.ts +577 -0
  561. package/src/lib/viewer-selection.ts +580 -0
  562. package/src/lib/wall-draw-variant.test.ts +71 -0
  563. package/src/lib/wall-draw-variant.ts +91 -0
  564. package/src/lib/wall-paint-region-session.ts +246 -0
  565. package/src/lib/wall-paint-region.test.ts +373 -0
  566. package/src/lib/wall-placement-pointer.test.ts +42 -0
  567. package/src/lib/wall-placement-pointer.ts +57 -0
  568. package/src/lib/wall-polygon-draft.test.ts +262 -0
  569. package/src/lib/wall-polygon-draft.ts +286 -0
  570. package/src/lib/wall-push.test.ts +453 -0
  571. package/src/lib/wall-region-face.ts +352 -0
  572. package/src/lib/wall-region-handles.test.ts +304 -0
  573. package/src/lib/wall-region-handles.ts +368 -0
  574. package/src/lib/wall-region-list.test.ts +119 -0
  575. package/src/lib/wall-region-list.ts +136 -0
  576. package/src/lib/wall-region-snap.ts +110 -0
  577. package/src/lib/window-changes.ts +35 -0
  578. package/src/lib/window-interaction.ts +14 -0
  579. package/src/lib/zone-content.ts +4 -1
  580. package/src/lib/zone-name.test.ts +17 -0
  581. package/src/lib/zone-name.ts +17 -0
  582. package/src/store/opens-in-select-mode.test.ts +223 -0
  583. package/src/store/paint-mode.test.ts +188 -0
  584. package/src/store/terrain-sculpt-mode.test.ts +19 -21
  585. package/src/store/tool-mode.test.ts +137 -6
  586. package/src/store/use-audio.test.ts +146 -0
  587. package/src/store/use-audio.tsx +127 -3
  588. package/src/store/use-construction-reveal.test.ts +173 -0
  589. package/src/store/use-construction-reveal.ts +145 -0
  590. package/src/store/use-corner-snap-hint.test.ts +24 -0
  591. package/src/store/use-corner-snap-hint.ts +47 -0
  592. package/src/store/use-delete-confirmation.ts +28 -1
  593. package/src/store/use-drawing-view.test.ts +24 -3
  594. package/src/store/use-drawing-view.ts +29 -1
  595. package/src/store/use-editor.tsx +432 -321
  596. package/src/store/use-floorplan-draft-preview.ts +13 -0
  597. package/src/store/use-floorplan-marquee.ts +12 -2
  598. package/src/store/use-follow-pascal.test.ts +62 -0
  599. package/src/store/use-follow-pascal.ts +53 -0
  600. package/src/store/use-point-ask.ts +247 -0
  601. package/src/store/use-quick-measurement-hud.ts +3 -3
  602. package/src/store/use-stair-build-preview.ts +33 -5
  603. package/src/store/use-wall-move-ghosts.ts +4 -2
  604. package/src/store/use-wall-region-handles-selection.test.ts +222 -0
  605. package/src/store/use-wall-region-handles-selection.ts +139 -0
  606. package/src/store/view-layout.test.ts +201 -0
  607. package/src/store/view-layout.ts +199 -0
  608. package/src/test-utils/immediate-animation-frames.ts +16 -0
  609. package/src/test-utils/live-gesture-round-trip.ts +224 -0
  610. package/src/test-utils/render-current-markup.tsx +16 -0
  611. package/src/test-utils/selection-harness.ts +82 -0
  612. package/src/components/editor-2d/renderers/floorplan-stair-layer.tsx +0 -499
  613. package/src/components/systems/zone/zone-label-editor-system.tsx +0 -333
  614. package/src/components/tools/fence/fence-drafting.test.ts +0 -100
  615. package/src/components/tools/fence/fence-drafting.ts +0 -266
  616. package/src/components/tools/stair/stair-click-guard.test.ts +0 -58
  617. package/src/components/tools/stair/stair-click-guard.ts +0 -72
  618. package/src/components/tools/stair/stair-defaults.ts +0 -18
  619. package/src/components/tools/stair/stair-tool.tsx +0 -634
  620. package/src/components/ui/sidebar/panels/plugins-panel.tsx +0 -218
  621. package/src/components/viewer/viewer-units-panel.tsx +0 -116
  622. package/src/lib/floorplan/stairs.ts +0 -489
  623. package/src/lib/level-duplication.test.ts +0 -144
  624. package/src/lib/level-duplication.ts +0 -195
@@ -1,634 +0,0 @@
1
- import {
2
- type AnyNode,
3
- collectAlignmentAnchors,
4
- createDefaultStairSegment,
5
- createSurfaceOpeningPreviewController,
6
- DEFAULT_LEVEL_HEIGHT,
7
- emitter,
8
- type GridEvent,
9
- getFloorStackedPosition,
10
- getLevelFloorToFloorHeight,
11
- type LevelNode,
12
- movingAlignmentAnchors,
13
- type NodeEvent,
14
- resolveAlignment,
15
- resolveFrozenFloorPlacementPatch,
16
- resolveSupportSlabPatch,
17
- StairNode,
18
- type StairSegmentNode,
19
- syncAutoStairOpenings,
20
- useScene,
21
- } from '@pascal-app/core'
22
- import { useViewer } from '@pascal-app/viewer'
23
- import { useThree } from '@react-three/fiber'
24
- import { useEffect, useMemo, useRef } from 'react'
25
- import * as THREE from 'three'
26
- import { sfxEmitter } from '../../../lib/sfx-bus'
27
- import {
28
- resolveStairDestinationLevel,
29
- resolveStairPlacementLevelId,
30
- } from '../../../lib/stair-levels'
31
-
32
- import useAlignmentGuides from '../../../store/use-alignment-guides'
33
- import useEditor, {
34
- isAlignmentGuideActive,
35
- isGridSnapActive,
36
- isMagneticSnapActive,
37
- } from '../../../store/use-editor'
38
-
39
- import useFacingPose from '../../../store/use-facing-pose'
40
- import { useStairBuildPreview } from '../../../store/use-stair-build-preview'
41
- import { CursorSphere } from '../shared/cursor-sphere'
42
- import { getFloorStackPreviewPosition } from '../shared/floor-stack-preview'
43
- import {
44
- type PointerSupportSurface,
45
- resolvePointerSupportSurface,
46
- } from '../shared/pointer-support-cap'
47
- import { createStairCommitGate, swallowFollowUpBrowserClick } from './stair-click-guard'
48
- import {
49
- DEFAULT_CURVED_STAIR_INNER_RADIUS,
50
- DEFAULT_CURVED_STAIR_SWEEP_ANGLE,
51
- DEFAULT_SPIRAL_SHOW_CENTER_COLUMN,
52
- DEFAULT_SPIRAL_SHOW_STEP_SUPPORTS,
53
- DEFAULT_SPIRAL_TOP_LANDING_DEPTH,
54
- DEFAULT_SPIRAL_TOP_LANDING_MODE,
55
- DEFAULT_STAIR_ATTACHMENT_SIDE,
56
- DEFAULT_STAIR_FILL_TO_FLOOR,
57
- DEFAULT_STAIR_LENGTH,
58
- DEFAULT_STAIR_OPENING_OFFSET,
59
- DEFAULT_STAIR_RAILING_HEIGHT,
60
- DEFAULT_STAIR_RAILING_MODE,
61
- DEFAULT_STAIR_STEP_COUNT,
62
- DEFAULT_STAIR_THICKNESS,
63
- DEFAULT_STAIR_TYPE,
64
- DEFAULT_STAIR_WIDTH,
65
- } from './stair-defaults'
66
-
67
- const GRID_OFFSET = 0.02
68
- /** Figma-style alignment-snap threshold (meters), matching the move tools. */
69
- const ALIGNMENT_THRESHOLD_M = 0.08
70
- type ClickTriggerEvent = GridEvent | NodeEvent<AnyNode>
71
- type MoveTriggerEvent = GridEvent | NodeEvent<AnyNode>
72
-
73
- /**
74
- * Generates the step-profile geometry for the ghost preview.
75
- * Same algorithm as StairSystem's generateStairSegmentGeometry.
76
- */
77
- function createStairPreviewGeometry(rise: number): THREE.BufferGeometry {
78
- const riserHeight = rise / DEFAULT_STAIR_STEP_COUNT
79
- const treadDepth = DEFAULT_STAIR_LENGTH / DEFAULT_STAIR_STEP_COUNT
80
-
81
- const shape = new THREE.Shape()
82
- shape.moveTo(0, 0)
83
-
84
- for (let i = 0; i < DEFAULT_STAIR_STEP_COUNT; i++) {
85
- shape.lineTo(i * treadDepth, (i + 1) * riserHeight)
86
- shape.lineTo((i + 1) * treadDepth, (i + 1) * riserHeight)
87
- }
88
-
89
- // Fill to floor (absoluteHeight = 0)
90
- shape.lineTo(DEFAULT_STAIR_LENGTH, 0)
91
- shape.lineTo(0, 0)
92
-
93
- const geometry = new THREE.ExtrudeGeometry(shape, {
94
- steps: 1,
95
- depth: DEFAULT_STAIR_WIDTH,
96
- bevelEnabled: false,
97
- })
98
-
99
- // Rotate so extrusion is along X (width), shape profile in XZ plane
100
- const matrix = new THREE.Matrix4()
101
- matrix.makeRotationY(-Math.PI / 2)
102
- matrix.setPosition(DEFAULT_STAIR_WIDTH / 2, 0, 0)
103
- geometry.applyMatrix4(matrix)
104
-
105
- return geometry
106
- }
107
-
108
- /**
109
- * Creates a default straight stair segment climbing `rise` — the storey it is
110
- * dropped on, not a constant: the placed stair has no explicit `totalRise`, so
111
- * this is the height `syncStairRises` immediately converges it to anyway.
112
- */
113
- function resolvePlacedStairRise(
114
- nodes: Record<string, AnyNode>,
115
- levelId: LevelNode['id'],
116
- stair: StairNode,
117
- supportSurface: PointerSupportSurface | null,
118
- ): 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).
123
- const base = getFloorStackedPosition({
124
- node: stair,
125
- nodes,
126
- position: stair.position,
127
- rotation: stair.rotation,
128
- levelId,
129
- maxElevation: supportSurface?.elevation ?? null,
130
- })[1]
131
- return getLevelFloorToFloorHeight(levelId, nodes) - base
132
- }
133
-
134
- function createSeedStairSegment(rise: number) {
135
- return createDefaultStairSegment({
136
- width: DEFAULT_STAIR_WIDTH,
137
- length: DEFAULT_STAIR_LENGTH,
138
- height: rise,
139
- stepCount: DEFAULT_STAIR_STEP_COUNT,
140
- attachmentSide: DEFAULT_STAIR_ATTACHMENT_SIDE,
141
- fillToFloor: DEFAULT_STAIR_FILL_TO_FLOOR,
142
- thickness: DEFAULT_STAIR_THICKNESS,
143
- })
144
- }
145
-
146
- function createDefaultStairNode({
147
- name,
148
- levelId,
149
- nextLevelId,
150
- position,
151
- rotation,
152
- segmentId,
153
- }: {
154
- name: string
155
- levelId: LevelNode['id']
156
- nextLevelId: LevelNode['id']
157
- position: [number, number, number]
158
- rotation: number
159
- segmentId: StairSegmentNode['id']
160
- }) {
161
- return StairNode.parse({
162
- name,
163
- position,
164
- rotation,
165
- stairType: DEFAULT_STAIR_TYPE,
166
- fromLevelId: levelId,
167
- toLevelId: nextLevelId,
168
- slabOpeningMode: 'destination',
169
- openingOffset: DEFAULT_STAIR_OPENING_OFFSET,
170
- width: DEFAULT_STAIR_WIDTH,
171
- stepCount: DEFAULT_STAIR_STEP_COUNT,
172
- thickness: DEFAULT_STAIR_THICKNESS,
173
- fillToFloor: DEFAULT_STAIR_FILL_TO_FLOOR,
174
- innerRadius: DEFAULT_CURVED_STAIR_INNER_RADIUS,
175
- sweepAngle: DEFAULT_CURVED_STAIR_SWEEP_ANGLE,
176
- topLandingMode: DEFAULT_SPIRAL_TOP_LANDING_MODE,
177
- topLandingDepth: DEFAULT_SPIRAL_TOP_LANDING_DEPTH,
178
- showCenterColumn: DEFAULT_SPIRAL_SHOW_CENTER_COLUMN,
179
- showStepSupports: DEFAULT_SPIRAL_SHOW_STEP_SUPPORTS,
180
- railingHeight: DEFAULT_STAIR_RAILING_HEIGHT,
181
- railingMode: DEFAULT_STAIR_RAILING_MODE,
182
- children: [segmentId],
183
- })
184
- }
185
-
186
- /**
187
- * Creates a stair group with one default stair segment at the given position/rotation.
188
- */
189
- function commitStairPlacement(
190
- levelId: LevelNode['id'],
191
- position: [number, number, number],
192
- rotation: number,
193
- supportSurface: PointerSupportSurface | null,
194
- ): void {
195
- const { createNodes, nodes } = useScene.getState()
196
- const placementLevelId = resolveStairPlacementLevelId(
197
- nodes,
198
- levelId,
199
- useViewer.getState().selection.buildingId,
200
- )
201
- if (!placementLevelId) return
202
-
203
- const stairCount = Object.values(nodes).filter((n) => n.type === 'stair').length
204
- const name = `Staircase ${stairCount + 1}`
205
- const seed = createSeedStairSegment(getLevelFloorToFloorHeight(placementLevelId, nodes))
206
-
207
- const destinationPlan = resolveStairDestinationLevel({
208
- createMissing: true,
209
- fromLevelId: placementLevelId,
210
- nodes,
211
- })
212
- const nextLevelId = destinationPlan?.toLevel.id ?? placementLevelId
213
-
214
- const stair = StairNode.parse({
215
- ...createDefaultStairNode({
216
- name,
217
- levelId: placementLevelId,
218
- nextLevelId,
219
- position,
220
- rotation,
221
- segmentId: seed.id,
222
- }),
223
- parentId: placementLevelId,
224
- })
225
- const segment = {
226
- ...seed,
227
- height: resolvePlacedStairRise(nodes, placementLevelId, stair, supportSurface),
228
- }
229
- const prospectiveNodes = {
230
- ...nodes,
231
- [stair.id]: stair,
232
- [segment.id]: { ...segment, parentId: stair.id },
233
- } as Record<string, AnyNode>
234
- const placementPatch = supportSurface?.sourceNodeId
235
- ? resolveFrozenFloorPlacementPatch(stair, prospectiveNodes, {
236
- position,
237
- rotation,
238
- elevation: supportSurface.elevation,
239
- preferredSlabId: supportSurface.supportSlabId,
240
- })
241
- : {
242
- position,
243
- ...resolveSupportSlabPatch(stair, prospectiveNodes, {
244
- maxElevation: supportSurface?.elevation,
245
- }),
246
- }
247
- const committedStair = StairNode.parse({
248
- ...stair,
249
- ...placementPatch,
250
- })
251
-
252
- const createdLevel = destinationPlan?.createdLevel
253
- const levelCreateOps =
254
- createdLevel && destinationPlan.buildingId
255
- ? [{ node: createdLevel, parentId: destinationPlan.buildingId }]
256
- : []
257
-
258
- createNodes([
259
- ...levelCreateOps,
260
- { node: committedStair, parentId: placementLevelId },
261
- { node: segment, parentId: committedStair.id },
262
- ])
263
-
264
- sfxEmitter.emit('sfx:structure-build')
265
- }
266
-
267
- export const StairTool: React.FC = () => {
268
- const camera = useThree((state) => state.camera)
269
- const cameraRef = useRef(camera)
270
- cameraRef.current = camera
271
- const cursorRef = useRef<THREE.Group>(null)
272
- const previewRef = useRef<THREE.Group>(null)
273
- const rotationRef = useRef(0)
274
- const supportSurfaceRef = useRef<PointerSupportSurface | null>(null)
275
- const previousGridPosRef = useRef<[number, number] | null>(null)
276
- const lastCanonicalPositionRef = useRef<[number, number, number] | null>(null)
277
- const currentLevelId = useViewer((state) => state.selection.levelId)
278
-
279
- const previewRise = useScene((state) =>
280
- currentLevelId ? getLevelFloorToFloorHeight(currentLevelId, state.nodes) : DEFAULT_LEVEL_HEIGHT,
281
- )
282
- const previewRiseRef = useRef(previewRise)
283
- previewRiseRef.current = previewRise
284
- const previewGeometry = useMemo(() => createStairPreviewGeometry(previewRise), [previewRise])
285
- useEffect(() => () => previewGeometry.dispose(), [previewGeometry])
286
-
287
- useEffect(() => {
288
- if (!currentLevelId) return
289
-
290
- const openingPreview = createSurfaceOpeningPreviewController()
291
- // Refuses the duplicate commit triggers a single physical click produces
292
- // — see `stair-click-guard.ts`. Fresh per armed session.
293
- const commitGate = createStairCommitGate()
294
-
295
- // Reset rotation when tool activates
296
- rotationRef.current = 0
297
- useStairBuildPreview.getState().reset()
298
- if (previewRef.current) {
299
- previewRef.current.rotation.y = 0
300
- previewRef.current.scale.y = 1
301
- }
302
- lastCanonicalPositionRef.current = null
303
- supportSurfaceRef.current = null
304
-
305
- const buildPreviewScene = (
306
- position: [number, number, number],
307
- rotation: number,
308
- supportSurface: PointerSupportSurface | null,
309
- ) => {
310
- const nodes = useScene.getState().nodes
311
- const placementLevelId = resolveStairPlacementLevelId(
312
- nodes,
313
- currentLevelId,
314
- useViewer.getState().selection.buildingId,
315
- )
316
- if (!placementLevelId) return null
317
-
318
- const destinationPlan = resolveStairDestinationLevel({
319
- createMissing: true,
320
- fromLevelId: placementLevelId,
321
- nodes,
322
- })
323
- const nextLevelId = destinationPlan?.toLevel.id ?? placementLevelId
324
- const seed = createSeedStairSegment(getLevelFloorToFloorHeight(placementLevelId, nodes))
325
- const stair = createDefaultStairNode({
326
- name: 'Staircase Preview',
327
- levelId: placementLevelId,
328
- nextLevelId,
329
- position,
330
- rotation,
331
- segmentId: seed.id,
332
- })
333
- const segment = {
334
- ...seed,
335
- height: resolvePlacedStairRise(nodes, placementLevelId, stair, supportSurface),
336
- }
337
- const previewNodes = {
338
- ...nodes,
339
- ...(destinationPlan?.createdLevel
340
- ? { [destinationPlan.createdLevel.id]: destinationPlan.createdLevel }
341
- : {}),
342
- [stair.id]: { ...stair, parentId: placementLevelId },
343
- [segment.id]: { ...segment, parentId: stair.id },
344
- } as Record<string, AnyNode>
345
-
346
- return { placementLevelId, previewNodes, stair, rise: segment.height }
347
- }
348
-
349
- // The preview rebuild (full-scene copy + destination-level resolution +
350
- // auto-opening CSG) is expensive; `grid:move` fires it every pointer event
351
- // but the placed position is grid-snapped, so within a cell every rebuild
352
- // is identical. Dedupe on the snapped position + rotation so we rebuild
353
- // only when the staircase would actually land somewhere new — this is the
354
- // difference between a smooth and a stuttering stair tool (the elevator is
355
- // cheap because it has no opening sync).
356
- let lastPreviewKey: string | null = null
357
-
358
- const applyDraftPreview = (
359
- position: [number, number, number],
360
- rotation: number,
361
- supportSurface: PointerSupportSurface | null,
362
- ) => {
363
- const key = `${position[0].toFixed(3)},${position[2].toFixed(3)},${rotation.toFixed(4)},${supportSurface?.elevation.toFixed(3) ?? 'none'},${supportSurface?.sourceNodeId ?? 'floor'}`
364
- if (key === lastPreviewKey) return
365
- lastPreviewKey = key
366
- useStairBuildPreview.getState().setPreview([position[0], position[2]], rotation)
367
- const preview = buildPreviewScene(position, rotation, supportSurface)
368
- const frozenPatch =
369
- preview && supportSurface?.sourceNodeId
370
- ? resolveFrozenFloorPlacementPatch(preview.stair, preview.previewNodes, {
371
- position,
372
- rotation,
373
- elevation: supportSurface.elevation,
374
- preferredSlabId: supportSurface.supportSlabId,
375
- })
376
- : null
377
- const previewPosition = frozenPatch?.position ?? position
378
- const previewStair = frozenPatch
379
- ? ({ ...preview?.stair, ...frozenPatch } as AnyNode)
380
- : preview?.stair
381
- const visualPosition =
382
- preview && previewStair
383
- ? getFloorStackPreviewPosition({
384
- node: previewStair,
385
- position: previewPosition,
386
- rotation,
387
- levelId: preview.placementLevelId,
388
- nodes: preview.previewNodes,
389
- maxElevation: supportSurface?.sourceNodeId ? null : supportSurface?.elevation,
390
- })
391
- : previewPosition
392
- if (cursorRef.current) {
393
- cursorRef.current.position.set(
394
- visualPosition[0],
395
- visualPosition[1] + GRID_OFFSET,
396
- visualPosition[2],
397
- )
398
- }
399
-
400
- if (previewRef.current) {
401
- previewRef.current.position.set(...visualPosition)
402
- 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
406
- }
407
-
408
- // Forward-facing triangle (editor-side overlay). The run ascends along
409
- // local +Z from the entry at z≈0; the stair's front is the -Z entry side,
410
- // so `reversed` points the triangle out of the entry (where you approach
411
- // from), sitting just before it — not inside the footprint or at the
412
- // elevated far end. Centre is the footprint mid-run (origin is the entry).
413
- useFacingPose.getState().set({
414
- position: visualPosition,
415
- rotationY: rotation,
416
- depth: DEFAULT_STAIR_LENGTH,
417
- center: [0, DEFAULT_STAIR_LENGTH / 2],
418
- reversed: true,
419
- })
420
-
421
- if (!preview) {
422
- openingPreview.clear()
423
- return
424
- }
425
-
426
- openingPreview.apply(syncAutoStairOpenings(preview.previewNodes))
427
- }
428
-
429
- // Alignment candidates — anchors of every alignable object; refreshed
430
- // after each placement. The moving stair aligns by its footprint edges so
431
- // users can snap the run side against walls, slabs, elevators, or another
432
- // stair instead of only lining up the invisible origin point.
433
- let alignmentCandidates = collectAlignmentAnchors(useScene.getState().nodes, '', currentLevelId)
434
- const resolveStairFootprintAlignment = (
435
- x: number,
436
- z: number,
437
- rotation: number,
438
- ): ReturnType<typeof resolveAlignment> | null => {
439
- const preview = buildPreviewScene([x, 0, z], rotation, supportSurfaceRef.current)
440
- const moving = preview
441
- ? movingAlignmentAnchors(preview.stair, preview.previewNodes, x, z, rotation)
442
- : []
443
- if (moving.length === 0) return null
444
- return resolveAlignment({
445
- moving,
446
- candidates: alignmentCandidates,
447
- threshold: ALIGNMENT_THRESHOLD_M,
448
- })
449
- }
450
- // The probe is the RAW cursor, not the grid-snapped point: resolving
451
- // against the grid point would only catch anchors that happen to sit near
452
- // a grid line. Matched axes use the raw probe + snap delta; unmatched axes
453
- // keep the normal grid snap. Guides are published in every snapping mode
454
- // (including Off); the magnetic pull toward them (applySnap) applies only in
455
- // 'lines' mode.
456
- const alignPoint = (
457
- gridX: number,
458
- gridZ: number,
459
- rawX: number,
460
- rawZ: number,
461
- bypass: boolean,
462
- applySnap: boolean,
463
- ): [number, number] => {
464
- if (bypass || alignmentCandidates.length === 0) {
465
- useAlignmentGuides.getState().clear()
466
- return [gridX, gridZ]
467
- }
468
- const ar = resolveStairFootprintAlignment(rawX, rawZ, rotationRef.current)
469
- if (!ar || ar.guides.length === 0) {
470
- useAlignmentGuides.getState().clear()
471
- return [gridX, gridZ]
472
- }
473
- if (!applySnap) {
474
- useAlignmentGuides.getState().set(ar.guides)
475
- return [gridX, gridZ]
476
- }
477
- let x = gridX
478
- let z = gridZ
479
- if (ar.snap) {
480
- if (ar.guides.some((guide) => guide.axis === 'x')) x = rawX + ar.snap.dx
481
- if (ar.guides.some((guide) => guide.axis === 'z')) z = rawZ + ar.snap.dz
482
- }
483
- const finalAlignment = resolveStairFootprintAlignment(x, z, rotationRef.current)
484
- useAlignmentGuides.getState().set(finalAlignment?.guides ?? ar.guides)
485
- return [x, z]
486
- }
487
-
488
- const resolveStairPosition = (event: MoveTriggerEvent): [number, number, number] | null => {
489
- const pointed = resolvePointerSupportSurface(cameraRef.current, event.position, {
490
- includeNodeTopSurfaces: true,
491
- })
492
- supportSurfaceRef.current = pointed
493
- const fallbackPosition =
494
- 'node' in event ? lastCanonicalPositionRef.current : event.localPosition
495
- if (!pointed?.localPoint && !fallbackPosition) return null
496
- const rawX = pointed?.localPoint?.[0] ?? fallbackPosition![0]
497
- const rawZ = pointed?.localPoint?.[2] ?? fallbackPosition![2]
498
- // Grid snap follows the global mode (live step so the HUD chip is
499
- // honest); Off keeps the raw cursor. Shift cycles the mode centrally.
500
- const step = useEditor.getState().gridSnapStep
501
- const [gridX, gridZ] = alignPoint(
502
- isGridSnapActive() ? Math.round(rawX / step) * step : rawX,
503
- isGridSnapActive() ? Math.round(rawZ / step) * step : rawZ,
504
- rawX,
505
- rawZ,
506
- !isAlignmentGuideActive(),
507
- isMagneticSnapActive(),
508
- )
509
- return [gridX, 0, gridZ]
510
- }
511
-
512
- const onPointerMove = (event: MoveTriggerEvent) => {
513
- const position = resolveStairPosition(event)
514
- if (!position) return
515
- const [gridX, , gridZ] = position
516
- lastCanonicalPositionRef.current = position
517
- applyDraftPreview(position, rotationRef.current, supportSurfaceRef.current)
518
-
519
- if (
520
- (isGridSnapActive() || isMagneticSnapActive()) &&
521
- previousGridPosRef.current &&
522
- (gridX !== previousGridPosRef.current[0] || gridZ !== previousGridPosRef.current[1])
523
- ) {
524
- sfxEmitter.emit('sfx:grid-snap')
525
- }
526
-
527
- previousGridPosRef.current = [gridX, gridZ]
528
- }
529
-
530
- const commitAtCursor = (event: ClickTriggerEvent) => {
531
- if (!currentLevelId) return
532
- // One physical click can reach here twice (node click synthesized on
533
- // pointerup + the native browser click driving `grid:click`) — see
534
- // `stair-click-guard.ts`. The gate refuses anything after a single-
535
- // continuation commit; the swallow below eats the same gesture's
536
- // follow-up click while the tool stays armed (repeat continuation).
537
- if (!commitGate.shouldCommit()) return
538
- const nodeEvent = 'node' in event ? (event as NodeEvent<AnyNode>) : null
539
- if (nodeEvent) {
540
- nodeEvent.stopPropagation()
541
- nodeEvent.nativeEvent.stopPropagation()
542
- // The canvas-level `grid:click` listener is out of stopPropagation's
543
- // reach — without this, the browser click that follows this
544
- // pointerup-synthesized node click commits a second stair.
545
- swallowFollowUpBrowserClick()
546
- }
547
-
548
- const position = resolveStairPosition(event)
549
- if (!position) return
550
-
551
- commitStairPlacement(currentLevelId, position, rotationRef.current, supportSurfaceRef.current)
552
- openingPreview.clear()
553
- // Commit cleared the opening preview, so force the next hover (even on the
554
- // same cell) to rebuild rather than dedupe against the just-placed key.
555
- lastPreviewKey = null
556
- useAlignmentGuides.getState().clear()
557
-
558
- // Single by default; the C-toggle ('point' context, shared with every
559
- // other placement tool) opts into placing more. On single, drop the tool
560
- // and the facing triangle so we fall back to select after one stair.
561
- if (useEditor.getState().getContinuation('point') === 'repeat') {
562
- alignmentCandidates = collectAlignmentAnchors(useScene.getState().nodes, '', currentLevelId)
563
- } else {
564
- commitGate.markExited()
565
- useFacingPose.getState().clear()
566
- useEditor.getState().setTool(null)
567
- // Return to select mode explicitly (matches the spawn tool's exit).
568
- // The selection managers route node clicks only while
569
- // `mode === 'select'`; exiting with `mode: 'build'` + a null tool
570
- // left every click dead until the user pressed Escape.
571
- useEditor.getState().setMode('select')
572
- }
573
- }
574
-
575
- const onKeyDown = (event: KeyboardEvent) => {
576
- if (event.target instanceof HTMLInputElement || event.target instanceof HTMLTextAreaElement) {
577
- return
578
- }
579
-
580
- const ROTATION_STEP = Math.PI / 4
581
- let rotationDelta = 0
582
- if (event.key === 'r' || event.key === 'R') rotationDelta = ROTATION_STEP
583
- else if (event.key === 't' || event.key === 'T') rotationDelta = -ROTATION_STEP
584
-
585
- if (rotationDelta !== 0) {
586
- event.preventDefault()
587
- sfxEmitter.emit('sfx:item-rotate')
588
- rotationRef.current += rotationDelta
589
- if (lastCanonicalPositionRef.current) {
590
- applyDraftPreview(
591
- lastCanonicalPositionRef.current,
592
- rotationRef.current,
593
- supportSurfaceRef.current,
594
- )
595
- } else if (previewRef.current) {
596
- previewRef.current.rotation.y = rotationRef.current
597
- }
598
- }
599
- }
600
-
601
- emitter.on('grid:move', onPointerMove)
602
- emitter.on('grid:click', commitAtCursor)
603
- emitter.on('node:click', commitAtCursor)
604
- emitter.on('node:move', onPointerMove)
605
- window.addEventListener('keydown', onKeyDown)
606
-
607
- return () => {
608
- emitter.off('grid:move', onPointerMove)
609
- emitter.off('grid:click', commitAtCursor)
610
- emitter.off('node:click', commitAtCursor)
611
- emitter.off('node:move', onPointerMove)
612
- window.removeEventListener('keydown', onKeyDown)
613
- useAlignmentGuides.getState().clear()
614
- openingPreview.clear()
615
- useFacingPose.getState().clear()
616
- useStairBuildPreview.getState().reset()
617
- }
618
- }, [currentLevelId])
619
-
620
- return (
621
- <group>
622
- <CursorSphere ref={cursorRef} />
623
-
624
- {/* 3D ghost preview — position/rotation updated imperatively. The
625
- forward-facing triangle is drawn by the editor-side overlay from the
626
- pose published in `applyDraftPreview`. */}
627
- <group ref={previewRef}>
628
- <mesh castShadow geometry={previewGeometry}>
629
- <meshStandardMaterial color="#818cf8" depthWrite={false} opacity={0.35} transparent />
630
- </mesh>
631
- </group>
632
- </group>
633
- )
634
- }