@pascal-app/editor 0.9.1 → 1.0.0-beta.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (366) hide show
  1. package/package.json +14 -9
  2. package/src/components/editor/alignment-3d-guide-layer.tsx +10 -2
  3. package/src/components/editor/bake-exporter.tsx +49 -0
  4. package/src/components/editor/bake-thumbnail.tsx +95 -0
  5. package/src/components/editor/camera-dragging-lifecycle.test.ts +49 -0
  6. package/src/components/editor/camera-dragging-lifecycle.ts +41 -0
  7. package/src/components/editor/custom-camera-controls.tsx +332 -199
  8. package/src/components/editor/delete-confirmation-dialog.tsx +54 -0
  9. package/src/components/editor/editor-layout-mobile.tsx +5 -0
  10. package/src/components/editor/editor-layout-v2.tsx +18 -0
  11. package/src/components/editor/elevation-3d-guide-layer.tsx +120 -0
  12. package/src/components/editor/export-manager.tsx +71 -63
  13. package/src/components/editor/fence-tangent-lines-3d.tsx +87 -0
  14. package/src/components/editor/first-person/build-collider-world.test.ts +102 -5
  15. package/src/components/editor/first-person/build-collider-world.ts +52 -5
  16. package/src/components/editor/first-person/terrain-collider.test.ts +132 -0
  17. package/src/components/editor/first-person/terrain-collider.ts +212 -0
  18. package/src/components/editor/first-person-controls.tsx +410 -136
  19. package/src/components/editor/floating-action-menu.tsx +435 -64
  20. package/src/components/editor/floating-building-action-menu.tsx +1 -1
  21. package/src/components/editor/floorplan-background-selection.test.ts +72 -0
  22. package/src/components/editor/floorplan-background-selection.ts +7 -20
  23. package/src/components/editor/floorplan-camera-sync.test.ts +249 -0
  24. package/src/components/editor/floorplan-camera-sync.ts +213 -0
  25. package/src/components/editor/floorplan-mode-coordinator.tsx +82 -0
  26. package/src/components/editor/floorplan-navigation-presentation.test.ts +142 -0
  27. package/src/components/editor/floorplan-navigation-presentation.ts +156 -0
  28. package/src/components/editor/floorplan-panel.tsx +2517 -1492
  29. package/src/components/editor/grid.tsx +166 -39
  30. package/src/components/editor/group-actions.ts +597 -0
  31. package/src/components/editor/group-floating-action-menu.tsx +130 -0
  32. package/src/components/editor/group-move-3d.ts +397 -0
  33. package/src/components/editor/group-rotate-handle.tsx +102 -64
  34. package/src/components/editor/group-selection-box-3d.tsx +173 -0
  35. package/src/components/editor/group-transform-shared.test.ts +192 -1
  36. package/src/components/editor/group-transform-shared.ts +240 -7
  37. package/src/components/editor/handles/handle-arrow.tsx +81 -23
  38. package/src/components/editor/handles/handle-drag-history.test.ts +96 -0
  39. package/src/components/editor/handles/handle-drag-history.ts +14 -0
  40. package/src/components/editor/handles/preview-overrides.test.ts +26 -0
  41. package/src/components/editor/handles/preview-overrides.ts +13 -0
  42. package/src/components/editor/handles/resize-snap.test.ts +52 -0
  43. package/src/components/editor/handles/resize-snap.ts +23 -0
  44. package/src/components/editor/handles/use-handle-drag.ts +49 -6
  45. package/src/components/editor/index.tsx +289 -35
  46. package/src/components/editor/measurement-pill.tsx +68 -23
  47. package/src/components/editor/node-action-menu.tsx +14 -1
  48. package/src/components/editor/node-arrow-handles.tsx +409 -180
  49. package/src/components/editor/opening-guides-3d-layer.tsx +144 -0
  50. package/src/components/editor/quick-measurement-card.tsx +76 -0
  51. package/src/components/editor/quick-measurement-hud.tsx +34 -0
  52. package/src/components/editor/riser-diagram-panel.tsx +137 -0
  53. package/src/components/editor/selection-manager.tsx +814 -466
  54. package/src/components/editor/site-edge-labels.tsx +28 -6
  55. package/src/components/editor/slab-hole-highlights.tsx +13 -5
  56. package/src/components/editor/snapshot-capture-overlay.tsx +258 -115
  57. package/src/components/editor/three-context-bridge.ts +22 -0
  58. package/src/components/editor/thumbnail-generator.tsx +118 -253
  59. package/src/components/editor/use-floorplan-background-placement.ts +113 -45
  60. package/src/components/editor/use-floorplan-hit-testing.test.ts +82 -0
  61. package/src/components/editor/use-floorplan-hit-testing.ts +144 -161
  62. package/src/components/editor/use-floorplan-scene-data.ts +5 -5
  63. package/src/components/editor/use-mesh-settle-epoch.ts +27 -0
  64. package/src/components/editor/wall-measurement-label.tsx +10 -28
  65. package/src/components/editor/wall-move-side-handles.tsx +229 -39
  66. package/src/components/editor/wall-opening-highlights.tsx +63 -32
  67. package/src/components/editor/wall-snap-beacon-layer.tsx +162 -6
  68. package/src/components/editor-2d/floorplan-action-menu-layer.tsx +9 -5
  69. package/src/components/editor-2d/floorplan-alignment-guide-layer.tsx +2 -1
  70. package/src/components/editor-2d/floorplan-cursor-indicator-overlay.tsx +40 -9
  71. package/src/components/editor-2d/floorplan-cursor-indicator-position.test.ts +31 -0
  72. package/src/components/editor-2d/floorplan-cursor-indicator-position.ts +39 -0
  73. package/src/components/editor-2d/floorplan-group-action-menu.tsx +87 -0
  74. package/src/components/editor-2d/floorplan-group-move.tsx +744 -0
  75. package/src/components/editor-2d/floorplan-measurement-tool-layer.test.ts +113 -0
  76. package/src/components/editor-2d/floorplan-measurement-tool-layer.tsx +1672 -0
  77. package/src/components/editor-2d/floorplan-quick-measure-layer.tsx +212 -0
  78. package/src/components/editor-2d/floorplan-registered-tool-layer.tsx +72 -0
  79. package/src/components/editor-2d/floorplan-registry-action-menu.tsx +219 -20
  80. package/src/components/editor-2d/floorplan-registry-move-overlay.tsx +327 -59
  81. package/src/components/editor-2d/floorplan-render-context.test.ts +27 -0
  82. package/src/components/editor-2d/floorplan-render-context.tsx +113 -8
  83. package/src/components/editor-2d/floorplan-snap-beacon-layer.tsx +5 -3
  84. package/src/components/editor-2d/renderers/floorplan-annotation-layout.test.ts +285 -0
  85. package/src/components/editor-2d/renderers/floorplan-annotation-layout.ts +516 -0
  86. package/src/components/editor-2d/renderers/floorplan-dimension-renderer.test.tsx +236 -0
  87. package/src/components/editor-2d/renderers/floorplan-dimension-renderer.tsx +618 -0
  88. package/src/components/editor-2d/renderers/floorplan-geometry-renderer.test.tsx +369 -0
  89. package/src/components/editor-2d/renderers/floorplan-geometry-renderer.tsx +431 -32
  90. package/src/components/editor-2d/renderers/floorplan-label-angle.test.ts +89 -0
  91. package/src/components/editor-2d/renderers/floorplan-label-angle.ts +106 -0
  92. package/src/components/editor-2d/renderers/floorplan-placement-preview-layer.tsx +94 -26
  93. package/src/components/editor-2d/renderers/floorplan-registry-layer.test.ts +587 -0
  94. package/src/components/editor-2d/renderers/floorplan-registry-layer.tsx +2280 -578
  95. package/src/components/editor-2d/renderers/floorplan-stair-layer.tsx +20 -10
  96. package/src/components/editor-2d/renderers/floorplan-voronoi-layer.tsx +128 -0
  97. package/src/components/systems/ceiling/ceiling-selection-affordance-system.tsx +425 -61
  98. package/src/components/systems/ceiling/ceiling-system.tsx +97 -4
  99. package/src/components/systems/roof/roof-edit-system.tsx +1768 -13
  100. package/src/components/systems/selection-affordance-manager.tsx +38 -0
  101. package/src/components/systems/zone/zone-label-editor-system.tsx +14 -1
  102. package/src/components/systems/zone/zone-system.tsx +42 -13
  103. package/src/components/tools/elevator/elevator-tool.tsx +34 -8
  104. package/src/components/tools/elevator/move-elevator-tool.tsx +13 -2
  105. package/src/components/tools/fence/fence-drafting.ts +107 -8
  106. package/src/components/tools/item/move-tool.tsx +17 -13
  107. package/src/components/tools/item/placement-math.test.ts +13 -1
  108. package/src/components/tools/item/placement-math.ts +28 -2
  109. package/src/components/tools/item/placement-strategies.ts +246 -5
  110. package/src/components/tools/item/placement-types.ts +13 -1
  111. package/src/components/tools/item/use-draft-node.ts +146 -57
  112. package/src/components/tools/item/use-placement-coordinator.tsx +813 -125
  113. package/src/components/tools/registry/move-registry-node-tool.tsx +636 -103
  114. package/src/components/tools/roof/roof-tool.tsx +336 -94
  115. package/src/components/tools/select/box-select-state.ts +18 -7
  116. package/src/components/tools/select/box-select-tool.tsx +176 -40
  117. package/src/components/tools/select/marquee-geometry.test.ts +161 -0
  118. package/src/components/tools/select/marquee-geometry.ts +155 -0
  119. package/src/components/tools/select/plane-box-select-tool.tsx +1 -8
  120. package/src/components/tools/select/select-candidates.ts +1 -1
  121. package/src/components/tools/shared/cursor-sphere.tsx +62 -26
  122. package/src/components/tools/shared/drag-bounding-box.tsx +28 -4
  123. package/src/components/tools/shared/facing-indicator.tsx +87 -0
  124. package/src/components/tools/shared/facing-pose-indicator.tsx +54 -0
  125. package/src/components/tools/shared/floor-stack-preview.ts +4 -0
  126. package/src/components/tools/shared/horizontal-construction-plane.ts +123 -0
  127. package/src/components/tools/shared/placement-box.tsx +183 -1
  128. package/src/components/tools/shared/pointer-support-cap.test.ts +98 -0
  129. package/src/components/tools/shared/pointer-support-cap.ts +273 -0
  130. package/src/components/tools/shared/polygon-editor.tsx +276 -35
  131. package/src/components/tools/site/site-boundary-editor.tsx +101 -42
  132. package/src/components/tools/site/stroke-pointer-ownership.test.ts +91 -0
  133. package/src/components/tools/site/stroke-pointer-ownership.ts +57 -0
  134. package/src/components/tools/site/terrain-brush-cursor.tsx +247 -0
  135. package/src/components/tools/site/terrain-sculpt-grid.tsx +192 -0
  136. package/src/components/tools/site/terrain-sculpt-tool.tsx +356 -0
  137. package/src/components/tools/stair/stair-click-guard.test.ts +58 -0
  138. package/src/components/tools/stair/stair-click-guard.ts +72 -0
  139. package/src/components/tools/stair/stair-defaults.ts +1 -0
  140. package/src/components/tools/stair/stair-tool.tsx +164 -42
  141. package/src/components/tools/tool-manager.tsx +149 -30
  142. package/src/components/tools/wall/wall-drafting.test.ts +447 -0
  143. package/src/components/tools/wall/wall-drafting.ts +304 -108
  144. package/src/components/tools/wall/wall-snap-geometry.test.ts +52 -0
  145. package/src/components/tools/wall/wall-snap-geometry.ts +137 -9
  146. package/src/components/tools/zone/zone-boundary-editor.tsx +5 -1
  147. package/src/components/tools/zone/zone-tool.tsx +98 -88
  148. package/src/components/ui/action-menu/camera-actions.tsx +24 -17
  149. package/src/components/ui/action-menu/control-modes.tsx +44 -94
  150. package/src/components/ui/action-menu/furnish-tools.tsx +5 -5
  151. package/src/components/ui/action-menu/index.tsx +2 -4
  152. package/src/components/ui/action-menu/measurement-control.tsx +267 -0
  153. package/src/components/ui/action-menu/structure-tools.tsx +22 -13
  154. package/src/components/ui/action-menu/view-toggles.tsx +42 -90
  155. package/src/components/ui/command-palette/editor-commands.tsx +16 -3
  156. package/src/components/ui/command-palette/index.tsx +4 -3
  157. package/src/components/ui/controls/material-paint-panel.tsx +93 -18
  158. package/src/components/ui/controls/material-picker.tsx +162 -152
  159. package/src/components/ui/controls/material-properties-editor.tsx +108 -0
  160. package/src/components/ui/controls/metric-control.tsx +118 -44
  161. package/src/components/ui/controls/scene-material-list.tsx +247 -0
  162. package/src/components/ui/controls/slider-control.tsx +88 -38
  163. package/src/components/ui/controls/terrain-sculpt-panel.tsx +193 -0
  164. package/src/components/ui/floating-level-selector.tsx +74 -20
  165. package/src/components/ui/helpers/building-helper.tsx +10 -23
  166. package/src/components/ui/helpers/contextual-helper-panel.tsx +428 -0
  167. package/src/components/ui/helpers/helper-manager.tsx +273 -14
  168. package/src/components/ui/helpers/item-helper.tsx +28 -32
  169. package/src/components/ui/helpers/registered-tool-helper.tsx +45 -11
  170. package/src/components/ui/helpers/roof-helper.tsx +10 -12
  171. package/src/components/ui/icon-ref.tsx +47 -0
  172. package/src/components/ui/item-catalog/catalog-items.tsx +39 -1
  173. package/src/components/ui/item-catalog/item-catalog.tsx +13 -36
  174. package/src/components/ui/level-duplicate-dialog.tsx +1 -1
  175. package/src/components/ui/panels/multi-selection-panel.tsx +57 -0
  176. package/src/components/ui/panels/node-display.ts +17 -17
  177. package/src/components/ui/panels/panel-manager.tsx +36 -14
  178. package/src/components/ui/panels/panel-wrapper.tsx +24 -3
  179. package/src/components/ui/panels/parametric-inspector.tsx +19 -4
  180. package/src/components/ui/panels/reference-panel.tsx +54 -19
  181. package/src/components/ui/panels/selection-breakdown.test.ts +24 -0
  182. package/src/components/ui/panels/selection-breakdown.ts +20 -0
  183. package/src/components/ui/primitives/shortcut-token.tsx +39 -3
  184. package/src/components/ui/primitives/sidebar.tsx +1 -0
  185. package/src/components/ui/sidebar/app-sidebar.tsx +5 -1
  186. package/src/components/ui/sidebar/icon-rail.tsx +50 -31
  187. package/src/components/ui/sidebar/panels/plugins-panel.tsx +218 -0
  188. package/src/components/ui/sidebar/panels/settings-panel/index.tsx +82 -41
  189. package/src/components/ui/sidebar/panels/settings-panel/keyboard-shortcuts-dialog.tsx +76 -6
  190. package/src/components/ui/sidebar/panels/settings-panel/load-build-dialog.tsx +12 -4
  191. package/src/components/ui/sidebar/panels/site-panel/building-tree-node.tsx +2 -1
  192. package/src/components/ui/sidebar/panels/site-panel/ceiling-tree-node.tsx +4 -3
  193. package/src/components/ui/sidebar/panels/site-panel/chimney-tree-node.tsx +10 -7
  194. package/src/components/ui/sidebar/panels/site-panel/column-tree-node.tsx +1 -1
  195. package/src/components/ui/sidebar/panels/site-panel/door-tree-node.tsx +7 -2
  196. package/src/components/ui/sidebar/panels/site-panel/dormer-tree-node.tsx +10 -7
  197. package/src/components/ui/sidebar/panels/site-panel/elevator-tree-node.tsx +1 -1
  198. package/src/components/ui/sidebar/panels/site-panel/fence-tree-node.tsx +1 -1
  199. package/src/components/ui/sidebar/panels/site-panel/gutter-tree-node.tsx +10 -7
  200. package/src/components/ui/sidebar/panels/site-panel/index.tsx +42 -17
  201. package/src/components/ui/sidebar/panels/site-panel/item-tree-node.tsx +31 -17
  202. package/src/components/ui/sidebar/panels/site-panel/level-tree-node.tsx +1 -1
  203. package/src/components/ui/sidebar/panels/site-panel/registry-tree-node.tsx +72 -25
  204. package/src/components/ui/sidebar/panels/site-panel/roof-tree-node.tsx +7 -3
  205. package/src/components/ui/sidebar/panels/site-panel/shelf-tree-node.tsx +12 -8
  206. package/src/components/ui/sidebar/panels/site-panel/slab-tree-node.tsx +4 -3
  207. package/src/components/ui/sidebar/panels/site-panel/solar-panel-tree-node.tsx +10 -7
  208. package/src/components/ui/sidebar/panels/site-panel/spawn-tree-node.tsx +1 -1
  209. package/src/components/ui/sidebar/panels/site-panel/stair-tree-node.tsx +2 -2
  210. package/src/components/ui/sidebar/panels/site-panel/tree-node.tsx +44 -3
  211. package/src/components/ui/sidebar/panels/site-panel/tree-structure.ts +36 -0
  212. package/src/components/ui/sidebar/panels/site-panel/wall-tree-node.tsx +1 -1
  213. package/src/components/ui/sidebar/panels/site-panel/window-tree-node.tsx +7 -2
  214. package/src/components/ui/sidebar/panels/site-panel/zone-tree-node.tsx +3 -3
  215. package/src/components/ui/sidebar/tab-bar.tsx +42 -28
  216. package/src/components/ui/sidebar/use-plugin-panels.tsx +132 -0
  217. package/src/components/ui/snap-target-badge.test.tsx +40 -0
  218. package/src/components/ui/snap-target-badge.tsx +88 -0
  219. package/src/components/viewer/viewer-controls-bar.tsx +466 -0
  220. package/src/components/viewer/viewer-scene-header.tsx +235 -0
  221. package/src/components/viewer-overlay.tsx +36 -596
  222. package/src/components/viewer-zone-system.tsx +6 -1
  223. package/src/components/walkthrough-hud.tsx +111 -0
  224. package/src/hooks/use-auto-save.test.ts +14 -0
  225. package/src/hooks/use-auto-save.ts +54 -7
  226. package/src/hooks/use-ceiling-events.ts +3 -2
  227. package/src/hooks/use-drag-action.ts +4 -1
  228. package/src/hooks/use-grid-events.ts +24 -0
  229. package/src/hooks/use-keyboard.ts +438 -70
  230. package/src/index.tsx +355 -9
  231. package/src/lib/active-placement-surface.test.ts +24 -0
  232. package/src/lib/active-placement-surface.ts +42 -0
  233. package/src/lib/camera-pose.test.ts +237 -0
  234. package/src/lib/camera-pose.ts +172 -0
  235. package/src/lib/ceiling-plan-snap.ts +24 -0
  236. package/src/lib/contextual-help.test.ts +112 -0
  237. package/src/lib/contextual-help.ts +144 -0
  238. package/src/lib/continuation.ts +64 -0
  239. package/src/lib/direct-manipulation.test.ts +275 -0
  240. package/src/lib/direct-manipulation.ts +118 -0
  241. package/src/lib/door-interaction.ts +1 -1
  242. package/src/lib/editor-api.ts +23 -35
  243. package/src/lib/elevation-guides.test.ts +107 -0
  244. package/src/lib/elevation-guides.ts +236 -0
  245. package/src/lib/floorplan/annotation-visibility.test.ts +327 -0
  246. package/src/lib/floorplan/annotation-visibility.ts +133 -0
  247. package/src/lib/floorplan/apply-alignment.test.ts +25 -0
  248. package/src/lib/floorplan/apply-alignment.ts +18 -12
  249. package/src/lib/floorplan/drawing-coordination.test.ts +46 -0
  250. package/src/lib/floorplan/drawing-coordination.ts +13 -0
  251. package/src/lib/floorplan/floorplan-export.test.ts +319 -0
  252. package/src/lib/floorplan/floorplan-export.tsx +978 -0
  253. package/src/lib/floorplan/floorplan-extension.test.ts +43 -0
  254. package/src/lib/floorplan/floorplan-extension.ts +181 -0
  255. package/src/lib/floorplan/floorplan-mode.test.ts +120 -0
  256. package/src/lib/floorplan/floorplan-mode.ts +73 -0
  257. package/src/lib/floorplan/floorplan-pdfkit-document.ts +194 -0
  258. package/src/lib/floorplan/floorplan-pdfkit-renderer.test.ts +107 -0
  259. package/src/lib/floorplan/floorplan-pdfkit-renderer.ts +680 -0
  260. package/src/lib/floorplan/geometry.ts +52 -0
  261. package/src/lib/floorplan/index.ts +3 -0
  262. package/src/lib/floorplan/items.ts +5 -2
  263. package/src/lib/floorplan/plan-coords.ts +21 -0
  264. package/src/lib/fresh-planar-placement.test.ts +283 -3
  265. package/src/lib/fresh-planar-placement.ts +85 -1
  266. package/src/lib/glb-export.test.ts +556 -0
  267. package/src/lib/glb-export.ts +1071 -0
  268. package/src/lib/ground-surface.ts +95 -0
  269. package/src/lib/history.test.ts +96 -0
  270. package/src/lib/history.ts +31 -0
  271. package/src/lib/interaction/hot-set.test.ts +133 -0
  272. package/src/lib/interaction/hot-set.ts +67 -0
  273. package/src/lib/interaction/overlay-policy.test.ts +52 -0
  274. package/src/lib/interaction/overlay-policy.ts +59 -0
  275. package/src/lib/interaction/scope.ts +243 -0
  276. package/src/lib/level-duplication.test.ts +2 -1
  277. package/src/lib/level-selection.ts +2 -2
  278. package/src/lib/material-paint.ts +36 -49
  279. package/src/lib/measurement-kind.test.ts +30 -0
  280. package/src/lib/measurement-kind.ts +19 -0
  281. package/src/lib/measurement-label.test.ts +47 -0
  282. package/src/lib/measurement-label.ts +68 -0
  283. package/src/lib/measurement-parser.ts +116 -0
  284. package/src/lib/measurements.test.ts +188 -0
  285. package/src/lib/measurements.ts +210 -0
  286. package/src/lib/paint-scope.test.ts +454 -0
  287. package/src/lib/paint-scope.ts +461 -0
  288. package/src/lib/placement-drag-release.ts +23 -0
  289. package/src/lib/planar-cursor-placement.test.ts +57 -0
  290. package/src/lib/plugin-panels.test.ts +19 -0
  291. package/src/lib/plugin-panels.ts +91 -0
  292. package/src/lib/quick-action-feedback.ts +33 -0
  293. package/src/lib/quick-action-nodes.test.ts +94 -0
  294. package/src/lib/quick-action-nodes.ts +47 -0
  295. package/src/lib/quick-measurement.test.ts +77 -0
  296. package/src/lib/quick-measurement.ts +109 -0
  297. package/src/lib/roof-duplication.ts +6 -6
  298. package/src/lib/roof-hover-outline-proxy.ts +22 -0
  299. package/src/lib/roof-wall-hit.ts +164 -0
  300. package/src/lib/scene-clipboard.test.ts +325 -0
  301. package/src/lib/scene-clipboard.ts +310 -21
  302. package/src/lib/scene.ts +28 -5
  303. package/src/lib/selection-routing.test.ts +313 -0
  304. package/src/lib/selection-routing.ts +180 -0
  305. package/src/lib/sfx/index.ts +1 -0
  306. package/src/lib/sfx/movement-tick.test.ts +65 -0
  307. package/src/lib/sfx/movement-tick.ts +18 -0
  308. package/src/lib/sfx-bus.ts +82 -14
  309. package/src/lib/sfx-player.test.ts +266 -0
  310. package/src/lib/sfx-player.ts +262 -69
  311. package/src/lib/site-boundary.test.ts +53 -0
  312. package/src/lib/site-boundary.ts +27 -0
  313. package/src/lib/slab-plan-snap.test.ts +93 -0
  314. package/src/lib/slab-plan-snap.ts +108 -0
  315. package/src/lib/snapping-mode.test.ts +165 -0
  316. package/src/lib/snapping-mode.ts +191 -0
  317. package/src/lib/stair-duplication.test.ts +103 -0
  318. package/src/lib/stair-duplication.ts +58 -86
  319. package/src/lib/stair-levels.ts +2 -0
  320. package/src/lib/surface-plan-snap.test.ts +71 -0
  321. package/src/lib/surface-plan-snap.ts +256 -0
  322. package/src/lib/terrain-sculpt.test.ts +429 -0
  323. package/src/lib/terrain-sculpt.ts +301 -0
  324. package/src/lib/terrain-verb-color.test.ts +51 -0
  325. package/src/lib/terrain-verb-color.ts +58 -0
  326. package/src/lib/touch-gesture-priority.test.ts +45 -0
  327. package/src/lib/touch-gesture-priority.ts +30 -0
  328. package/src/lib/use-linear-display.ts +40 -0
  329. package/src/lib/window-interaction.ts +1 -1
  330. package/src/lib/world-grid-snap.ts +37 -5
  331. package/src/store/camera-pose-store.test.ts +29 -0
  332. package/src/store/camera-pose-store.ts +27 -0
  333. package/src/store/live-draft-preview-stores.test.ts +136 -0
  334. package/src/store/navigation-sync-pose-store.test.ts +34 -0
  335. package/src/store/navigation-sync-pose-store.ts +27 -0
  336. package/src/store/terrain-sculpt-mode.test.ts +223 -0
  337. package/src/store/use-delete-confirmation.ts +27 -0
  338. package/src/store/use-direct-manipulation-feedback.ts +20 -0
  339. package/src/store/use-drawing-view.test.ts +23 -0
  340. package/src/store/use-drawing-view.ts +84 -0
  341. package/src/store/use-editor.tsx +665 -126
  342. package/src/store/use-elevation-guides.ts +40 -0
  343. package/src/store/use-facing-pose.ts +44 -0
  344. package/src/store/use-fence-curve-draft.ts +51 -0
  345. package/src/store/use-first-person-hud.ts +40 -0
  346. package/src/store/use-floorplan-annotation-visibility.ts +60 -0
  347. package/src/store/use-floorplan-draft-preview.ts +151 -0
  348. package/src/store/use-floorplan-marquee.ts +50 -0
  349. package/src/store/use-floorplan-mode.ts +110 -0
  350. package/src/store/use-interaction-scope.test.ts +206 -0
  351. package/src/store/use-interaction-scope.ts +140 -0
  352. package/src/store/use-measurement-draft.test.ts +559 -0
  353. package/src/store/use-measurement-draft.ts +570 -0
  354. package/src/store/use-opening-guides.ts +41 -0
  355. package/src/store/use-path-draft-preview.ts +90 -0
  356. package/src/store/use-placement-preview.ts +11 -3
  357. package/src/store/use-quick-measurement-hud.test.ts +53 -0
  358. package/src/store/use-quick-measurement-hud.ts +77 -0
  359. package/src/store/use-segment-draft-chain.ts +28 -0
  360. package/src/store/use-stair-build-preview.ts +53 -0
  361. package/src/store/use-wall-snap-indicator.ts +2 -0
  362. package/src/components/editor/first-person/bvh-ecctrl.tsx +0 -860
  363. package/src/components/editor/group-move-handle.tsx +0 -316
  364. package/src/components/ui/panels/paint-panel.tsx +0 -163
  365. package/src/lib/floorplan/selection-tool.ts +0 -271
  366. package/src/lib/level-name.ts +0 -11
@@ -9,13 +9,14 @@ import {
9
9
  movingAlignmentAnchors,
10
10
  type NodeEvent,
11
11
  resolveAlignment,
12
+ resolveSupportSlabPatch,
12
13
  StairNode,
13
14
  StairSegmentNode,
14
15
  syncAutoStairOpenings,
15
16
  useScene,
16
17
  } from '@pascal-app/core'
17
- import { useAlignmentGuides } from '@pascal-app/editor'
18
18
  import { useViewer } from '@pascal-app/viewer'
19
+ import { useThree } from '@react-three/fiber'
19
20
  import { useEffect, useMemo, useRef } from 'react'
20
21
  import * as THREE from 'three'
21
22
  import { sfxEmitter } from '../../../lib/sfx-bus'
@@ -23,8 +24,20 @@ import {
23
24
  resolveStairDestinationLevel,
24
25
  resolveStairPlacementLevelId,
25
26
  } from '../../../lib/stair-levels'
27
+
28
+ import useAlignmentGuides from '../../../store/use-alignment-guides'
29
+ import useEditor, {
30
+ isAlignmentGuideActive,
31
+ isGridSnapActive,
32
+ isMagneticSnapActive,
33
+ } from '../../../store/use-editor'
34
+
35
+ import useFacingPose from '../../../store/use-facing-pose'
36
+ import { useStairBuildPreview } from '../../../store/use-stair-build-preview'
26
37
  import { CursorSphere } from '../shared/cursor-sphere'
27
38
  import { getFloorStackPreviewPosition } from '../shared/floor-stack-preview'
39
+ import { resolvePointerSupportSurface } from '../shared/pointer-support-cap'
40
+ import { createStairCommitGate, swallowFollowUpBrowserClick } from './stair-click-guard'
28
41
  import {
29
42
  DEFAULT_CURVED_STAIR_INNER_RADIUS,
30
43
  DEFAULT_CURVED_STAIR_SWEEP_ANGLE,
@@ -36,6 +49,7 @@ import {
36
49
  DEFAULT_STAIR_FILL_TO_FLOOR,
37
50
  DEFAULT_STAIR_HEIGHT,
38
51
  DEFAULT_STAIR_LENGTH,
52
+ DEFAULT_STAIR_OPENING_OFFSET,
39
53
  DEFAULT_STAIR_RAILING_HEIGHT,
40
54
  DEFAULT_STAIR_RAILING_MODE,
41
55
  DEFAULT_STAIR_STEP_COUNT,
@@ -48,6 +62,7 @@ const GRID_OFFSET = 0.02
48
62
  /** Figma-style alignment-snap threshold (meters), matching the move tools. */
49
63
  const ALIGNMENT_THRESHOLD_M = 0.08
50
64
  type ClickTriggerEvent = GridEvent | NodeEvent<AnyNode>
65
+ type MoveTriggerEvent = GridEvent | NodeEvent<AnyNode>
51
66
 
52
67
  const CLICK_TRIGGER_KINDS = [
53
68
  'shelf',
@@ -138,9 +153,8 @@ function createDefaultStairNode({
138
153
  fromLevelId: levelId,
139
154
  toLevelId: nextLevelId,
140
155
  slabOpeningMode: 'destination',
141
- openingOffset: 0.08,
156
+ openingOffset: DEFAULT_STAIR_OPENING_OFFSET,
142
157
  width: DEFAULT_STAIR_WIDTH,
143
- totalRise: DEFAULT_STAIR_HEIGHT,
144
158
  stepCount: DEFAULT_STAIR_STEP_COUNT,
145
159
  thickness: DEFAULT_STAIR_THICKNESS,
146
160
  fillToFloor: DEFAULT_STAIR_FILL_TO_FLOOR,
@@ -163,6 +177,7 @@ function commitStairPlacement(
163
177
  levelId: LevelNode['id'],
164
178
  position: [number, number, number],
165
179
  rotation: number,
180
+ supportElevationCap: number | null,
166
181
  ): void {
167
182
  const { createNodes, nodes } = useScene.getState()
168
183
  const placementLevelId = resolveStairPlacementLevelId(
@@ -183,13 +198,27 @@ function commitStairPlacement(
183
198
  })
184
199
  const nextLevelId = destinationPlan?.toLevel.id ?? placementLevelId
185
200
 
186
- const stair = createDefaultStairNode({
187
- name,
188
- levelId: placementLevelId,
189
- nextLevelId,
190
- position,
191
- rotation,
192
- segmentId: segment.id,
201
+ const stair = StairNode.parse({
202
+ ...createDefaultStairNode({
203
+ name,
204
+ levelId: placementLevelId,
205
+ nextLevelId,
206
+ position,
207
+ rotation,
208
+ segmentId: segment.id,
209
+ }),
210
+ parentId: placementLevelId,
211
+ })
212
+ const prospectiveNodes = {
213
+ ...nodes,
214
+ [stair.id]: stair,
215
+ [segment.id]: { ...segment, parentId: stair.id },
216
+ } as Record<string, AnyNode>
217
+ const committedStair = StairNode.parse({
218
+ ...stair,
219
+ ...resolveSupportSlabPatch(stair, prospectiveNodes, {
220
+ maxElevation: supportElevationCap,
221
+ }),
193
222
  })
194
223
 
195
224
  const createdLevel = destinationPlan?.createdLevel
@@ -200,17 +229,21 @@ function commitStairPlacement(
200
229
 
201
230
  createNodes([
202
231
  ...levelCreateOps,
203
- { node: stair, parentId: placementLevelId },
204
- { node: segment, parentId: stair.id },
232
+ { node: committedStair, parentId: placementLevelId },
233
+ { node: segment, parentId: committedStair.id },
205
234
  ])
206
235
 
207
236
  sfxEmitter.emit('sfx:structure-build')
208
237
  }
209
238
 
210
239
  export const StairTool: React.FC = () => {
240
+ const camera = useThree((state) => state.camera)
241
+ const cameraRef = useRef(camera)
242
+ cameraRef.current = camera
211
243
  const cursorRef = useRef<THREE.Group>(null)
212
244
  const previewRef = useRef<THREE.Group>(null)
213
245
  const rotationRef = useRef(0)
246
+ const supportCapRef = useRef<number | null>(null)
214
247
  const previousGridPosRef = useRef<[number, number] | null>(null)
215
248
  const lastCanonicalPositionRef = useRef<[number, number, number] | null>(null)
216
249
  const currentLevelId = useViewer((state) => state.selection.levelId)
@@ -221,11 +254,16 @@ export const StairTool: React.FC = () => {
221
254
  if (!currentLevelId) return
222
255
 
223
256
  const openingPreview = createSurfaceOpeningPreviewController()
257
+ // Refuses the duplicate commit triggers a single physical click produces
258
+ // — see `stair-click-guard.ts`. Fresh per armed session.
259
+ const commitGate = createStairCommitGate()
224
260
 
225
261
  // Reset rotation when tool activates
226
262
  rotationRef.current = 0
263
+ useStairBuildPreview.getState().reset()
227
264
  if (previewRef.current) previewRef.current.rotation.y = 0
228
265
  lastCanonicalPositionRef.current = null
266
+ supportCapRef.current = null
229
267
 
230
268
  const buildPreviewScene = (position: [number, number, number], rotation: number) => {
231
269
  const nodes = useScene.getState().nodes
@@ -263,7 +301,24 @@ export const StairTool: React.FC = () => {
263
301
  return { placementLevelId, previewNodes, stair }
264
302
  }
265
303
 
266
- const applyDraftPreview = (position: [number, number, number], rotation: number) => {
304
+ // The preview rebuild (full-scene copy + destination-level resolution +
305
+ // auto-opening CSG) is expensive; `grid:move` fires it every pointer event
306
+ // but the placed position is grid-snapped, so within a cell every rebuild
307
+ // is identical. Dedupe on the snapped position + rotation so we rebuild
308
+ // only when the staircase would actually land somewhere new — this is the
309
+ // difference between a smooth and a stuttering stair tool (the elevator is
310
+ // cheap because it has no opening sync).
311
+ let lastPreviewKey: string | null = null
312
+
313
+ const applyDraftPreview = (
314
+ position: [number, number, number],
315
+ rotation: number,
316
+ supportElevationCap: number | null,
317
+ ) => {
318
+ const key = `${position[0].toFixed(3)},${position[2].toFixed(3)},${rotation.toFixed(4)},${supportElevationCap?.toFixed(3) ?? 'none'}`
319
+ if (key === lastPreviewKey) return
320
+ lastPreviewKey = key
321
+ useStairBuildPreview.getState().setPreview([position[0], position[2]], rotation)
267
322
  const preview = buildPreviewScene(position, rotation)
268
323
  const visualPosition = preview
269
324
  ? getFloorStackPreviewPosition({
@@ -272,6 +327,7 @@ export const StairTool: React.FC = () => {
272
327
  rotation,
273
328
  levelId: preview.placementLevelId,
274
329
  nodes: preview.previewNodes,
330
+ maxElevation: supportElevationCap,
275
331
  })
276
332
  : position
277
333
  if (cursorRef.current) {
@@ -287,6 +343,19 @@ export const StairTool: React.FC = () => {
287
343
  previewRef.current.rotation.y = rotation
288
344
  }
289
345
 
346
+ // Forward-facing triangle (editor-side overlay). The run ascends along
347
+ // local +Z from the entry at z≈0; the stair's front is the -Z entry side,
348
+ // so `reversed` points the triangle out of the entry (where you approach
349
+ // from), sitting just before it — not inside the footprint or at the
350
+ // elevated far end. Centre is the footprint mid-run (origin is the entry).
351
+ useFacingPose.getState().set({
352
+ position: visualPosition,
353
+ rotationY: rotation,
354
+ depth: DEFAULT_STAIR_LENGTH,
355
+ center: [0, DEFAULT_STAIR_LENGTH / 2],
356
+ reversed: true,
357
+ })
358
+
290
359
  if (!preview) {
291
360
  openingPreview.clear()
292
361
  return
@@ -319,13 +388,16 @@ export const StairTool: React.FC = () => {
319
388
  // The probe is the RAW cursor, not the grid-snapped point: resolving
320
389
  // against the grid point would only catch anchors that happen to sit near
321
390
  // a grid line. Matched axes use the raw probe + snap delta; unmatched axes
322
- // keep the normal grid snap. Alt bypasses.
391
+ // keep the normal grid snap. Guides are published in every snapping mode
392
+ // (including Off); the magnetic pull toward them (applySnap) applies only in
393
+ // 'lines' mode.
323
394
  const alignPoint = (
324
395
  gridX: number,
325
396
  gridZ: number,
326
397
  rawX: number,
327
398
  rawZ: number,
328
399
  bypass: boolean,
400
+ applySnap: boolean,
329
401
  ): [number, number] => {
330
402
  if (bypass || alignmentCandidates.length === 0) {
331
403
  useAlignmentGuides.getState().clear()
@@ -336,6 +408,10 @@ export const StairTool: React.FC = () => {
336
408
  useAlignmentGuides.getState().clear()
337
409
  return [gridX, gridZ]
338
410
  }
411
+ if (!applySnap) {
412
+ useAlignmentGuides.getState().set(ar.guides)
413
+ return [gridX, gridZ]
414
+ }
339
415
  let x = gridX
340
416
  let z = gridZ
341
417
  if (ar.snap) {
@@ -347,19 +423,37 @@ export const StairTool: React.FC = () => {
347
423
  return [x, z]
348
424
  }
349
425
 
350
- const onGridMove = (event: GridEvent) => {
426
+ const resolveStairPosition = (event: MoveTriggerEvent): [number, number, number] | null => {
427
+ const pointed = resolvePointerSupportSurface(cameraRef.current, event.position)
428
+ supportCapRef.current = pointed?.elevation ?? null
429
+ const fallbackPosition =
430
+ 'node' in event ? lastCanonicalPositionRef.current : event.localPosition
431
+ if (!pointed?.localPoint && !fallbackPosition) return null
432
+ const rawX = pointed?.localPoint?.[0] ?? fallbackPosition![0]
433
+ const rawZ = pointed?.localPoint?.[2] ?? fallbackPosition![2]
434
+ // Grid snap follows the global mode (live step so the HUD chip is
435
+ // honest); Off keeps the raw cursor. Shift cycles the mode centrally.
436
+ const step = useEditor.getState().gridSnapStep
351
437
  const [gridX, gridZ] = alignPoint(
352
- Math.round(event.localPosition[0] * 2) / 2,
353
- Math.round(event.localPosition[2] * 2) / 2,
354
- event.localPosition[0],
355
- event.localPosition[2],
356
- event.nativeEvent?.altKey === true,
438
+ isGridSnapActive() ? Math.round(rawX / step) * step : rawX,
439
+ isGridSnapActive() ? Math.round(rawZ / step) * step : rawZ,
440
+ rawX,
441
+ rawZ,
442
+ !isAlignmentGuideActive(),
443
+ isMagneticSnapActive(),
357
444
  )
358
- const position: [number, number, number] = [gridX, 0, gridZ]
445
+ return [gridX, 0, gridZ]
446
+ }
447
+
448
+ const onPointerMove = (event: MoveTriggerEvent) => {
449
+ const position = resolveStairPosition(event)
450
+ if (!position) return
451
+ const [gridX, , gridZ] = position
359
452
  lastCanonicalPositionRef.current = position
360
- applyDraftPreview(position, rotationRef.current)
453
+ applyDraftPreview(position, rotationRef.current, supportCapRef.current)
361
454
 
362
455
  if (
456
+ (isGridSnapActive() || isMagneticSnapActive()) &&
363
457
  previousGridPosRef.current &&
364
458
  (gridX !== previousGridPosRef.current[0] || gridZ !== previousGridPosRef.current[1])
365
459
  ) {
@@ -369,34 +463,49 @@ export const StairTool: React.FC = () => {
369
463
  previousGridPosRef.current = [gridX, gridZ]
370
464
  }
371
465
 
372
- const getAlignedGridPosition = (event: GridEvent): [number, number, number] => {
373
- const [gridX, gridZ] = alignPoint(
374
- Math.round(event.localPosition[0] * 2) / 2,
375
- Math.round(event.localPosition[2] * 2) / 2,
376
- event.localPosition[0],
377
- event.localPosition[2],
378
- event.nativeEvent?.altKey === true,
379
- )
380
- return [gridX, 0, gridZ]
381
- }
382
-
383
466
  const commitAtCursor = (event: ClickTriggerEvent) => {
384
467
  if (!currentLevelId) return
468
+ // One physical click can reach here twice (node click synthesized on
469
+ // pointerup + the native browser click driving `grid:click`) — see
470
+ // `stair-click-guard.ts`. The gate refuses anything after a single-
471
+ // continuation commit; the swallow below eats the same gesture's
472
+ // follow-up click while the tool stays armed (repeat continuation).
473
+ if (!commitGate.shouldCommit()) return
385
474
  const nodeEvent = 'node' in event ? (event as NodeEvent<AnyNode>) : null
386
475
  if (nodeEvent) {
387
476
  nodeEvent.stopPropagation()
388
477
  nodeEvent.nativeEvent.stopPropagation()
478
+ // The canvas-level `grid:click` listener is out of stopPropagation's
479
+ // reach — without this, the browser click that follows this
480
+ // pointerup-synthesized node click commits a second stair.
481
+ swallowFollowUpBrowserClick()
389
482
  }
390
483
 
391
- const position = nodeEvent
392
- ? lastCanonicalPositionRef.current
393
- : getAlignedGridPosition(event as GridEvent)
484
+ const position = resolveStairPosition(event)
394
485
  if (!position) return
395
486
 
396
- commitStairPlacement(currentLevelId, position, rotationRef.current)
487
+ commitStairPlacement(currentLevelId, position, rotationRef.current, supportCapRef.current)
397
488
  openingPreview.clear()
398
- alignmentCandidates = collectAlignmentAnchors(useScene.getState().nodes, '', currentLevelId)
489
+ // Commit cleared the opening preview, so force the next hover (even on the
490
+ // same cell) to rebuild rather than dedupe against the just-placed key.
491
+ lastPreviewKey = null
399
492
  useAlignmentGuides.getState().clear()
493
+
494
+ // Single by default; the C-toggle ('point' context, shared with every
495
+ // other placement tool) opts into placing more. On single, drop the tool
496
+ // and the facing triangle so we fall back to select after one stair.
497
+ if (useEditor.getState().getContinuation('point') === 'repeat') {
498
+ alignmentCandidates = collectAlignmentAnchors(useScene.getState().nodes, '', currentLevelId)
499
+ } else {
500
+ commitGate.markExited()
501
+ useFacingPose.getState().clear()
502
+ useEditor.getState().setTool(null)
503
+ // Return to select mode explicitly (matches the spawn tool's exit).
504
+ // The selection managers route node clicks only while
505
+ // `mode === 'select'`; exiting with `mode: 'build'` + a null tool
506
+ // left every click dead until the user pressed Escape.
507
+ useEditor.getState().setMode('select')
508
+ }
400
509
  }
401
510
 
402
511
  const onKeyDown = (event: KeyboardEvent) => {
@@ -414,33 +523,44 @@ export const StairTool: React.FC = () => {
414
523
  sfxEmitter.emit('sfx:item-rotate')
415
524
  rotationRef.current += rotationDelta
416
525
  if (lastCanonicalPositionRef.current) {
417
- applyDraftPreview(lastCanonicalPositionRef.current, rotationRef.current)
526
+ applyDraftPreview(
527
+ lastCanonicalPositionRef.current,
528
+ rotationRef.current,
529
+ supportCapRef.current,
530
+ )
418
531
  } else if (previewRef.current) {
419
532
  previewRef.current.rotation.y = rotationRef.current
420
533
  }
421
534
  }
422
535
  }
423
536
 
424
- emitter.on('grid:move', onGridMove)
537
+ emitter.on('grid:move', onPointerMove)
425
538
  emitter.on('grid:click', commitAtCursor)
426
539
  type SuffixedKey<K extends string> = `${K}:${EventSuffix}`
427
540
  type ClickKey = SuffixedKey<(typeof CLICK_TRIGGER_KINDS)[number]>
541
+ type MoveKey = SuffixedKey<(typeof CLICK_TRIGGER_KINDS)[number]>
428
542
  for (const kind of CLICK_TRIGGER_KINDS) {
429
543
  const key = `${kind}:click` as ClickKey
430
544
  emitter.on(key, commitAtCursor as never)
545
+ const moveKey = `${kind}:move` as MoveKey
546
+ emitter.on(moveKey, onPointerMove as never)
431
547
  }
432
548
  window.addEventListener('keydown', onKeyDown)
433
549
 
434
550
  return () => {
435
- emitter.off('grid:move', onGridMove)
551
+ emitter.off('grid:move', onPointerMove)
436
552
  emitter.off('grid:click', commitAtCursor)
437
553
  for (const kind of CLICK_TRIGGER_KINDS) {
438
554
  const key = `${kind}:click` as ClickKey
439
555
  emitter.off(key, commitAtCursor as never)
556
+ const moveKey = `${kind}:move` as MoveKey
557
+ emitter.off(moveKey, onPointerMove as never)
440
558
  }
441
559
  window.removeEventListener('keydown', onKeyDown)
442
560
  useAlignmentGuides.getState().clear()
443
561
  openingPreview.clear()
562
+ useFacingPose.getState().clear()
563
+ useStairBuildPreview.getState().reset()
444
564
  }
445
565
  }, [currentLevelId])
446
566
 
@@ -448,7 +568,9 @@ export const StairTool: React.FC = () => {
448
568
  <group>
449
569
  <CursorSphere ref={cursorRef} />
450
570
 
451
- {/* 3D ghost preview — position/rotation updated imperatively */}
571
+ {/* 3D ghost preview — position/rotation updated imperatively. The
572
+ forward-facing triangle is drawn by the editor-side overlay from the
573
+ pose published in `applyDraftPreview`. */}
452
574
  <group ref={previewRef}>
453
575
  <mesh castShadow geometry={previewGeometry}>
454
576
  <meshStandardMaterial color="#818cf8" depthWrite={false} opacity={0.35} transparent />