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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (366) hide show
  1. package/package.json +14 -9
  2. package/src/components/editor/alignment-3d-guide-layer.tsx +10 -2
  3. package/src/components/editor/bake-exporter.tsx +49 -0
  4. package/src/components/editor/bake-thumbnail.tsx +95 -0
  5. package/src/components/editor/camera-dragging-lifecycle.test.ts +49 -0
  6. package/src/components/editor/camera-dragging-lifecycle.ts +41 -0
  7. package/src/components/editor/custom-camera-controls.tsx +332 -199
  8. package/src/components/editor/delete-confirmation-dialog.tsx +54 -0
  9. package/src/components/editor/editor-layout-mobile.tsx +5 -0
  10. package/src/components/editor/editor-layout-v2.tsx +18 -0
  11. package/src/components/editor/elevation-3d-guide-layer.tsx +120 -0
  12. package/src/components/editor/export-manager.tsx +71 -63
  13. package/src/components/editor/fence-tangent-lines-3d.tsx +87 -0
  14. package/src/components/editor/first-person/build-collider-world.test.ts +102 -5
  15. package/src/components/editor/first-person/build-collider-world.ts +52 -5
  16. package/src/components/editor/first-person/terrain-collider.test.ts +132 -0
  17. package/src/components/editor/first-person/terrain-collider.ts +212 -0
  18. package/src/components/editor/first-person-controls.tsx +410 -136
  19. package/src/components/editor/floating-action-menu.tsx +435 -64
  20. package/src/components/editor/floating-building-action-menu.tsx +1 -1
  21. package/src/components/editor/floorplan-background-selection.test.ts +72 -0
  22. package/src/components/editor/floorplan-background-selection.ts +7 -20
  23. package/src/components/editor/floorplan-camera-sync.test.ts +249 -0
  24. package/src/components/editor/floorplan-camera-sync.ts +213 -0
  25. package/src/components/editor/floorplan-mode-coordinator.tsx +82 -0
  26. package/src/components/editor/floorplan-navigation-presentation.test.ts +142 -0
  27. package/src/components/editor/floorplan-navigation-presentation.ts +156 -0
  28. package/src/components/editor/floorplan-panel.tsx +2517 -1492
  29. package/src/components/editor/grid.tsx +166 -39
  30. package/src/components/editor/group-actions.ts +597 -0
  31. package/src/components/editor/group-floating-action-menu.tsx +130 -0
  32. package/src/components/editor/group-move-3d.ts +397 -0
  33. package/src/components/editor/group-rotate-handle.tsx +102 -64
  34. package/src/components/editor/group-selection-box-3d.tsx +173 -0
  35. package/src/components/editor/group-transform-shared.test.ts +192 -1
  36. package/src/components/editor/group-transform-shared.ts +240 -7
  37. package/src/components/editor/handles/handle-arrow.tsx +81 -23
  38. package/src/components/editor/handles/handle-drag-history.test.ts +96 -0
  39. package/src/components/editor/handles/handle-drag-history.ts +14 -0
  40. package/src/components/editor/handles/preview-overrides.test.ts +26 -0
  41. package/src/components/editor/handles/preview-overrides.ts +13 -0
  42. package/src/components/editor/handles/resize-snap.test.ts +52 -0
  43. package/src/components/editor/handles/resize-snap.ts +23 -0
  44. package/src/components/editor/handles/use-handle-drag.ts +49 -6
  45. package/src/components/editor/index.tsx +289 -35
  46. package/src/components/editor/measurement-pill.tsx +68 -23
  47. package/src/components/editor/node-action-menu.tsx +14 -1
  48. package/src/components/editor/node-arrow-handles.tsx +409 -180
  49. package/src/components/editor/opening-guides-3d-layer.tsx +144 -0
  50. package/src/components/editor/quick-measurement-card.tsx +76 -0
  51. package/src/components/editor/quick-measurement-hud.tsx +34 -0
  52. package/src/components/editor/riser-diagram-panel.tsx +137 -0
  53. package/src/components/editor/selection-manager.tsx +814 -466
  54. package/src/components/editor/site-edge-labels.tsx +28 -6
  55. package/src/components/editor/slab-hole-highlights.tsx +13 -5
  56. package/src/components/editor/snapshot-capture-overlay.tsx +258 -115
  57. package/src/components/editor/three-context-bridge.ts +22 -0
  58. package/src/components/editor/thumbnail-generator.tsx +118 -253
  59. package/src/components/editor/use-floorplan-background-placement.ts +113 -45
  60. package/src/components/editor/use-floorplan-hit-testing.test.ts +82 -0
  61. package/src/components/editor/use-floorplan-hit-testing.ts +144 -161
  62. package/src/components/editor/use-floorplan-scene-data.ts +5 -5
  63. package/src/components/editor/use-mesh-settle-epoch.ts +27 -0
  64. package/src/components/editor/wall-measurement-label.tsx +10 -28
  65. package/src/components/editor/wall-move-side-handles.tsx +229 -39
  66. package/src/components/editor/wall-opening-highlights.tsx +63 -32
  67. package/src/components/editor/wall-snap-beacon-layer.tsx +162 -6
  68. package/src/components/editor-2d/floorplan-action-menu-layer.tsx +9 -5
  69. package/src/components/editor-2d/floorplan-alignment-guide-layer.tsx +2 -1
  70. package/src/components/editor-2d/floorplan-cursor-indicator-overlay.tsx +40 -9
  71. package/src/components/editor-2d/floorplan-cursor-indicator-position.test.ts +31 -0
  72. package/src/components/editor-2d/floorplan-cursor-indicator-position.ts +39 -0
  73. package/src/components/editor-2d/floorplan-group-action-menu.tsx +87 -0
  74. package/src/components/editor-2d/floorplan-group-move.tsx +744 -0
  75. package/src/components/editor-2d/floorplan-measurement-tool-layer.test.ts +113 -0
  76. package/src/components/editor-2d/floorplan-measurement-tool-layer.tsx +1672 -0
  77. package/src/components/editor-2d/floorplan-quick-measure-layer.tsx +212 -0
  78. package/src/components/editor-2d/floorplan-registered-tool-layer.tsx +72 -0
  79. package/src/components/editor-2d/floorplan-registry-action-menu.tsx +219 -20
  80. package/src/components/editor-2d/floorplan-registry-move-overlay.tsx +327 -59
  81. package/src/components/editor-2d/floorplan-render-context.test.ts +27 -0
  82. package/src/components/editor-2d/floorplan-render-context.tsx +113 -8
  83. package/src/components/editor-2d/floorplan-snap-beacon-layer.tsx +5 -3
  84. package/src/components/editor-2d/renderers/floorplan-annotation-layout.test.ts +285 -0
  85. package/src/components/editor-2d/renderers/floorplan-annotation-layout.ts +516 -0
  86. package/src/components/editor-2d/renderers/floorplan-dimension-renderer.test.tsx +236 -0
  87. package/src/components/editor-2d/renderers/floorplan-dimension-renderer.tsx +618 -0
  88. package/src/components/editor-2d/renderers/floorplan-geometry-renderer.test.tsx +369 -0
  89. package/src/components/editor-2d/renderers/floorplan-geometry-renderer.tsx +431 -32
  90. package/src/components/editor-2d/renderers/floorplan-label-angle.test.ts +89 -0
  91. package/src/components/editor-2d/renderers/floorplan-label-angle.ts +106 -0
  92. package/src/components/editor-2d/renderers/floorplan-placement-preview-layer.tsx +94 -26
  93. package/src/components/editor-2d/renderers/floorplan-registry-layer.test.ts +587 -0
  94. package/src/components/editor-2d/renderers/floorplan-registry-layer.tsx +2280 -578
  95. package/src/components/editor-2d/renderers/floorplan-stair-layer.tsx +20 -10
  96. package/src/components/editor-2d/renderers/floorplan-voronoi-layer.tsx +128 -0
  97. package/src/components/systems/ceiling/ceiling-selection-affordance-system.tsx +425 -61
  98. package/src/components/systems/ceiling/ceiling-system.tsx +97 -4
  99. package/src/components/systems/roof/roof-edit-system.tsx +1768 -13
  100. package/src/components/systems/selection-affordance-manager.tsx +38 -0
  101. package/src/components/systems/zone/zone-label-editor-system.tsx +14 -1
  102. package/src/components/systems/zone/zone-system.tsx +42 -13
  103. package/src/components/tools/elevator/elevator-tool.tsx +34 -8
  104. package/src/components/tools/elevator/move-elevator-tool.tsx +13 -2
  105. package/src/components/tools/fence/fence-drafting.ts +107 -8
  106. package/src/components/tools/item/move-tool.tsx +17 -13
  107. package/src/components/tools/item/placement-math.test.ts +13 -1
  108. package/src/components/tools/item/placement-math.ts +28 -2
  109. package/src/components/tools/item/placement-strategies.ts +246 -5
  110. package/src/components/tools/item/placement-types.ts +13 -1
  111. package/src/components/tools/item/use-draft-node.ts +146 -57
  112. package/src/components/tools/item/use-placement-coordinator.tsx +813 -125
  113. package/src/components/tools/registry/move-registry-node-tool.tsx +636 -103
  114. package/src/components/tools/roof/roof-tool.tsx +336 -94
  115. package/src/components/tools/select/box-select-state.ts +18 -7
  116. package/src/components/tools/select/box-select-tool.tsx +176 -40
  117. package/src/components/tools/select/marquee-geometry.test.ts +161 -0
  118. package/src/components/tools/select/marquee-geometry.ts +155 -0
  119. package/src/components/tools/select/plane-box-select-tool.tsx +1 -8
  120. package/src/components/tools/select/select-candidates.ts +1 -1
  121. package/src/components/tools/shared/cursor-sphere.tsx +62 -26
  122. package/src/components/tools/shared/drag-bounding-box.tsx +28 -4
  123. package/src/components/tools/shared/facing-indicator.tsx +87 -0
  124. package/src/components/tools/shared/facing-pose-indicator.tsx +54 -0
  125. package/src/components/tools/shared/floor-stack-preview.ts +4 -0
  126. package/src/components/tools/shared/horizontal-construction-plane.ts +123 -0
  127. package/src/components/tools/shared/placement-box.tsx +183 -1
  128. package/src/components/tools/shared/pointer-support-cap.test.ts +98 -0
  129. package/src/components/tools/shared/pointer-support-cap.ts +273 -0
  130. package/src/components/tools/shared/polygon-editor.tsx +276 -35
  131. package/src/components/tools/site/site-boundary-editor.tsx +101 -42
  132. package/src/components/tools/site/stroke-pointer-ownership.test.ts +91 -0
  133. package/src/components/tools/site/stroke-pointer-ownership.ts +57 -0
  134. package/src/components/tools/site/terrain-brush-cursor.tsx +247 -0
  135. package/src/components/tools/site/terrain-sculpt-grid.tsx +192 -0
  136. package/src/components/tools/site/terrain-sculpt-tool.tsx +356 -0
  137. package/src/components/tools/stair/stair-click-guard.test.ts +58 -0
  138. package/src/components/tools/stair/stair-click-guard.ts +72 -0
  139. package/src/components/tools/stair/stair-defaults.ts +1 -0
  140. package/src/components/tools/stair/stair-tool.tsx +164 -42
  141. package/src/components/tools/tool-manager.tsx +149 -30
  142. package/src/components/tools/wall/wall-drafting.test.ts +447 -0
  143. package/src/components/tools/wall/wall-drafting.ts +304 -108
  144. package/src/components/tools/wall/wall-snap-geometry.test.ts +52 -0
  145. package/src/components/tools/wall/wall-snap-geometry.ts +137 -9
  146. package/src/components/tools/zone/zone-boundary-editor.tsx +5 -1
  147. package/src/components/tools/zone/zone-tool.tsx +98 -88
  148. package/src/components/ui/action-menu/camera-actions.tsx +24 -17
  149. package/src/components/ui/action-menu/control-modes.tsx +44 -94
  150. package/src/components/ui/action-menu/furnish-tools.tsx +5 -5
  151. package/src/components/ui/action-menu/index.tsx +2 -4
  152. package/src/components/ui/action-menu/measurement-control.tsx +267 -0
  153. package/src/components/ui/action-menu/structure-tools.tsx +22 -13
  154. package/src/components/ui/action-menu/view-toggles.tsx +42 -90
  155. package/src/components/ui/command-palette/editor-commands.tsx +16 -3
  156. package/src/components/ui/command-palette/index.tsx +4 -3
  157. package/src/components/ui/controls/material-paint-panel.tsx +93 -18
  158. package/src/components/ui/controls/material-picker.tsx +162 -152
  159. package/src/components/ui/controls/material-properties-editor.tsx +108 -0
  160. package/src/components/ui/controls/metric-control.tsx +118 -44
  161. package/src/components/ui/controls/scene-material-list.tsx +247 -0
  162. package/src/components/ui/controls/slider-control.tsx +88 -38
  163. package/src/components/ui/controls/terrain-sculpt-panel.tsx +193 -0
  164. package/src/components/ui/floating-level-selector.tsx +74 -20
  165. package/src/components/ui/helpers/building-helper.tsx +10 -23
  166. package/src/components/ui/helpers/contextual-helper-panel.tsx +428 -0
  167. package/src/components/ui/helpers/helper-manager.tsx +273 -14
  168. package/src/components/ui/helpers/item-helper.tsx +28 -32
  169. package/src/components/ui/helpers/registered-tool-helper.tsx +45 -11
  170. package/src/components/ui/helpers/roof-helper.tsx +10 -12
  171. package/src/components/ui/icon-ref.tsx +47 -0
  172. package/src/components/ui/item-catalog/catalog-items.tsx +39 -1
  173. package/src/components/ui/item-catalog/item-catalog.tsx +13 -36
  174. package/src/components/ui/level-duplicate-dialog.tsx +1 -1
  175. package/src/components/ui/panels/multi-selection-panel.tsx +57 -0
  176. package/src/components/ui/panels/node-display.ts +17 -17
  177. package/src/components/ui/panels/panel-manager.tsx +36 -14
  178. package/src/components/ui/panels/panel-wrapper.tsx +24 -3
  179. package/src/components/ui/panels/parametric-inspector.tsx +19 -4
  180. package/src/components/ui/panels/reference-panel.tsx +54 -19
  181. package/src/components/ui/panels/selection-breakdown.test.ts +24 -0
  182. package/src/components/ui/panels/selection-breakdown.ts +20 -0
  183. package/src/components/ui/primitives/shortcut-token.tsx +39 -3
  184. package/src/components/ui/primitives/sidebar.tsx +1 -0
  185. package/src/components/ui/sidebar/app-sidebar.tsx +5 -1
  186. package/src/components/ui/sidebar/icon-rail.tsx +50 -31
  187. package/src/components/ui/sidebar/panels/plugins-panel.tsx +218 -0
  188. package/src/components/ui/sidebar/panels/settings-panel/index.tsx +82 -41
  189. package/src/components/ui/sidebar/panels/settings-panel/keyboard-shortcuts-dialog.tsx +76 -6
  190. package/src/components/ui/sidebar/panels/settings-panel/load-build-dialog.tsx +12 -4
  191. package/src/components/ui/sidebar/panels/site-panel/building-tree-node.tsx +2 -1
  192. package/src/components/ui/sidebar/panels/site-panel/ceiling-tree-node.tsx +4 -3
  193. package/src/components/ui/sidebar/panels/site-panel/chimney-tree-node.tsx +10 -7
  194. package/src/components/ui/sidebar/panels/site-panel/column-tree-node.tsx +1 -1
  195. package/src/components/ui/sidebar/panels/site-panel/door-tree-node.tsx +7 -2
  196. package/src/components/ui/sidebar/panels/site-panel/dormer-tree-node.tsx +10 -7
  197. package/src/components/ui/sidebar/panels/site-panel/elevator-tree-node.tsx +1 -1
  198. package/src/components/ui/sidebar/panels/site-panel/fence-tree-node.tsx +1 -1
  199. package/src/components/ui/sidebar/panels/site-panel/gutter-tree-node.tsx +10 -7
  200. package/src/components/ui/sidebar/panels/site-panel/index.tsx +42 -17
  201. package/src/components/ui/sidebar/panels/site-panel/item-tree-node.tsx +31 -17
  202. package/src/components/ui/sidebar/panels/site-panel/level-tree-node.tsx +1 -1
  203. package/src/components/ui/sidebar/panels/site-panel/registry-tree-node.tsx +72 -25
  204. package/src/components/ui/sidebar/panels/site-panel/roof-tree-node.tsx +7 -3
  205. package/src/components/ui/sidebar/panels/site-panel/shelf-tree-node.tsx +12 -8
  206. package/src/components/ui/sidebar/panels/site-panel/slab-tree-node.tsx +4 -3
  207. package/src/components/ui/sidebar/panels/site-panel/solar-panel-tree-node.tsx +10 -7
  208. package/src/components/ui/sidebar/panels/site-panel/spawn-tree-node.tsx +1 -1
  209. package/src/components/ui/sidebar/panels/site-panel/stair-tree-node.tsx +2 -2
  210. package/src/components/ui/sidebar/panels/site-panel/tree-node.tsx +44 -3
  211. package/src/components/ui/sidebar/panels/site-panel/tree-structure.ts +36 -0
  212. package/src/components/ui/sidebar/panels/site-panel/wall-tree-node.tsx +1 -1
  213. package/src/components/ui/sidebar/panels/site-panel/window-tree-node.tsx +7 -2
  214. package/src/components/ui/sidebar/panels/site-panel/zone-tree-node.tsx +3 -3
  215. package/src/components/ui/sidebar/tab-bar.tsx +42 -28
  216. package/src/components/ui/sidebar/use-plugin-panels.tsx +132 -0
  217. package/src/components/ui/snap-target-badge.test.tsx +40 -0
  218. package/src/components/ui/snap-target-badge.tsx +88 -0
  219. package/src/components/viewer/viewer-controls-bar.tsx +466 -0
  220. package/src/components/viewer/viewer-scene-header.tsx +235 -0
  221. package/src/components/viewer-overlay.tsx +36 -596
  222. package/src/components/viewer-zone-system.tsx +6 -1
  223. package/src/components/walkthrough-hud.tsx +111 -0
  224. package/src/hooks/use-auto-save.test.ts +14 -0
  225. package/src/hooks/use-auto-save.ts +54 -7
  226. package/src/hooks/use-ceiling-events.ts +3 -2
  227. package/src/hooks/use-drag-action.ts +4 -1
  228. package/src/hooks/use-grid-events.ts +24 -0
  229. package/src/hooks/use-keyboard.ts +438 -70
  230. package/src/index.tsx +355 -9
  231. package/src/lib/active-placement-surface.test.ts +24 -0
  232. package/src/lib/active-placement-surface.ts +42 -0
  233. package/src/lib/camera-pose.test.ts +237 -0
  234. package/src/lib/camera-pose.ts +172 -0
  235. package/src/lib/ceiling-plan-snap.ts +24 -0
  236. package/src/lib/contextual-help.test.ts +112 -0
  237. package/src/lib/contextual-help.ts +144 -0
  238. package/src/lib/continuation.ts +64 -0
  239. package/src/lib/direct-manipulation.test.ts +275 -0
  240. package/src/lib/direct-manipulation.ts +118 -0
  241. package/src/lib/door-interaction.ts +1 -1
  242. package/src/lib/editor-api.ts +23 -35
  243. package/src/lib/elevation-guides.test.ts +107 -0
  244. package/src/lib/elevation-guides.ts +236 -0
  245. package/src/lib/floorplan/annotation-visibility.test.ts +327 -0
  246. package/src/lib/floorplan/annotation-visibility.ts +133 -0
  247. package/src/lib/floorplan/apply-alignment.test.ts +25 -0
  248. package/src/lib/floorplan/apply-alignment.ts +18 -12
  249. package/src/lib/floorplan/drawing-coordination.test.ts +46 -0
  250. package/src/lib/floorplan/drawing-coordination.ts +13 -0
  251. package/src/lib/floorplan/floorplan-export.test.ts +319 -0
  252. package/src/lib/floorplan/floorplan-export.tsx +978 -0
  253. package/src/lib/floorplan/floorplan-extension.test.ts +43 -0
  254. package/src/lib/floorplan/floorplan-extension.ts +181 -0
  255. package/src/lib/floorplan/floorplan-mode.test.ts +120 -0
  256. package/src/lib/floorplan/floorplan-mode.ts +73 -0
  257. package/src/lib/floorplan/floorplan-pdfkit-document.ts +194 -0
  258. package/src/lib/floorplan/floorplan-pdfkit-renderer.test.ts +107 -0
  259. package/src/lib/floorplan/floorplan-pdfkit-renderer.ts +680 -0
  260. package/src/lib/floorplan/geometry.ts +52 -0
  261. package/src/lib/floorplan/index.ts +3 -0
  262. package/src/lib/floorplan/items.ts +5 -2
  263. package/src/lib/floorplan/plan-coords.ts +21 -0
  264. package/src/lib/fresh-planar-placement.test.ts +283 -3
  265. package/src/lib/fresh-planar-placement.ts +85 -1
  266. package/src/lib/glb-export.test.ts +556 -0
  267. package/src/lib/glb-export.ts +1071 -0
  268. package/src/lib/ground-surface.ts +95 -0
  269. package/src/lib/history.test.ts +96 -0
  270. package/src/lib/history.ts +31 -0
  271. package/src/lib/interaction/hot-set.test.ts +133 -0
  272. package/src/lib/interaction/hot-set.ts +67 -0
  273. package/src/lib/interaction/overlay-policy.test.ts +52 -0
  274. package/src/lib/interaction/overlay-policy.ts +59 -0
  275. package/src/lib/interaction/scope.ts +243 -0
  276. package/src/lib/level-duplication.test.ts +2 -1
  277. package/src/lib/level-selection.ts +2 -2
  278. package/src/lib/material-paint.ts +36 -49
  279. package/src/lib/measurement-kind.test.ts +30 -0
  280. package/src/lib/measurement-kind.ts +19 -0
  281. package/src/lib/measurement-label.test.ts +47 -0
  282. package/src/lib/measurement-label.ts +68 -0
  283. package/src/lib/measurement-parser.ts +116 -0
  284. package/src/lib/measurements.test.ts +188 -0
  285. package/src/lib/measurements.ts +210 -0
  286. package/src/lib/paint-scope.test.ts +454 -0
  287. package/src/lib/paint-scope.ts +461 -0
  288. package/src/lib/placement-drag-release.ts +23 -0
  289. package/src/lib/planar-cursor-placement.test.ts +57 -0
  290. package/src/lib/plugin-panels.test.ts +19 -0
  291. package/src/lib/plugin-panels.ts +91 -0
  292. package/src/lib/quick-action-feedback.ts +33 -0
  293. package/src/lib/quick-action-nodes.test.ts +94 -0
  294. package/src/lib/quick-action-nodes.ts +47 -0
  295. package/src/lib/quick-measurement.test.ts +77 -0
  296. package/src/lib/quick-measurement.ts +109 -0
  297. package/src/lib/roof-duplication.ts +6 -6
  298. package/src/lib/roof-hover-outline-proxy.ts +22 -0
  299. package/src/lib/roof-wall-hit.ts +164 -0
  300. package/src/lib/scene-clipboard.test.ts +325 -0
  301. package/src/lib/scene-clipboard.ts +310 -21
  302. package/src/lib/scene.ts +28 -5
  303. package/src/lib/selection-routing.test.ts +313 -0
  304. package/src/lib/selection-routing.ts +180 -0
  305. package/src/lib/sfx/index.ts +1 -0
  306. package/src/lib/sfx/movement-tick.test.ts +65 -0
  307. package/src/lib/sfx/movement-tick.ts +18 -0
  308. package/src/lib/sfx-bus.ts +82 -14
  309. package/src/lib/sfx-player.test.ts +266 -0
  310. package/src/lib/sfx-player.ts +262 -69
  311. package/src/lib/site-boundary.test.ts +53 -0
  312. package/src/lib/site-boundary.ts +27 -0
  313. package/src/lib/slab-plan-snap.test.ts +93 -0
  314. package/src/lib/slab-plan-snap.ts +108 -0
  315. package/src/lib/snapping-mode.test.ts +165 -0
  316. package/src/lib/snapping-mode.ts +191 -0
  317. package/src/lib/stair-duplication.test.ts +103 -0
  318. package/src/lib/stair-duplication.ts +58 -86
  319. package/src/lib/stair-levels.ts +2 -0
  320. package/src/lib/surface-plan-snap.test.ts +71 -0
  321. package/src/lib/surface-plan-snap.ts +256 -0
  322. package/src/lib/terrain-sculpt.test.ts +429 -0
  323. package/src/lib/terrain-sculpt.ts +301 -0
  324. package/src/lib/terrain-verb-color.test.ts +51 -0
  325. package/src/lib/terrain-verb-color.ts +58 -0
  326. package/src/lib/touch-gesture-priority.test.ts +45 -0
  327. package/src/lib/touch-gesture-priority.ts +30 -0
  328. package/src/lib/use-linear-display.ts +40 -0
  329. package/src/lib/window-interaction.ts +1 -1
  330. package/src/lib/world-grid-snap.ts +37 -5
  331. package/src/store/camera-pose-store.test.ts +29 -0
  332. package/src/store/camera-pose-store.ts +27 -0
  333. package/src/store/live-draft-preview-stores.test.ts +136 -0
  334. package/src/store/navigation-sync-pose-store.test.ts +34 -0
  335. package/src/store/navigation-sync-pose-store.ts +27 -0
  336. package/src/store/terrain-sculpt-mode.test.ts +223 -0
  337. package/src/store/use-delete-confirmation.ts +27 -0
  338. package/src/store/use-direct-manipulation-feedback.ts +20 -0
  339. package/src/store/use-drawing-view.test.ts +23 -0
  340. package/src/store/use-drawing-view.ts +84 -0
  341. package/src/store/use-editor.tsx +665 -126
  342. package/src/store/use-elevation-guides.ts +40 -0
  343. package/src/store/use-facing-pose.ts +44 -0
  344. package/src/store/use-fence-curve-draft.ts +51 -0
  345. package/src/store/use-first-person-hud.ts +40 -0
  346. package/src/store/use-floorplan-annotation-visibility.ts +60 -0
  347. package/src/store/use-floorplan-draft-preview.ts +151 -0
  348. package/src/store/use-floorplan-marquee.ts +50 -0
  349. package/src/store/use-floorplan-mode.ts +110 -0
  350. package/src/store/use-interaction-scope.test.ts +206 -0
  351. package/src/store/use-interaction-scope.ts +140 -0
  352. package/src/store/use-measurement-draft.test.ts +559 -0
  353. package/src/store/use-measurement-draft.ts +570 -0
  354. package/src/store/use-opening-guides.ts +41 -0
  355. package/src/store/use-path-draft-preview.ts +90 -0
  356. package/src/store/use-placement-preview.ts +11 -3
  357. package/src/store/use-quick-measurement-hud.test.ts +53 -0
  358. package/src/store/use-quick-measurement-hud.ts +77 -0
  359. package/src/store/use-segment-draft-chain.ts +28 -0
  360. package/src/store/use-stair-build-preview.ts +53 -0
  361. package/src/store/use-wall-snap-indicator.ts +2 -0
  362. package/src/components/editor/first-person/bvh-ecctrl.tsx +0 -860
  363. package/src/components/editor/group-move-handle.tsx +0 -316
  364. package/src/components/ui/panels/paint-panel.tsx +0 -163
  365. package/src/lib/floorplan/selection-tool.ts +0 -271
  366. package/src/lib/level-name.ts +0 -11
@@ -2,6 +2,12 @@
2
2
 
3
3
  import { useScene } from '@pascal-app/core'
4
4
  import { useCallback, useEffect, useRef, useState } from 'react'
5
+ import {
6
+ lingoUnitSpec,
7
+ measurementHint,
8
+ parseMeasurement,
9
+ } from '../../../lib/measurement-parser'
10
+ import { useLinearDisplay } from '../../../lib/use-linear-display'
5
11
  import { cn } from '../../../lib/utils'
6
12
 
7
13
  interface SliderControlProps {
@@ -59,10 +65,17 @@ export function SliderControl({
59
65
  unit = '',
60
66
  restoreOnCommit = true,
61
67
  }: SliderControlProps) {
68
+ // Display/storage conversion so the value honors the metric/imperial toggle.
69
+ // `value`, `onChange`, `onCommit`, `min`/`max`/`clamp` are always in the
70
+ // stored unit (meters for `unit === 'm'`); the step, drag deltas, text field
71
+ // and rendered number are in the DISPLAY unit (feet when imperial). For
72
+ // metric and non-length units these conversions are the identity.
73
+ const { isImperial, displayUnit, toDisplay, toStored } = useLinearDisplay(unit, precision)
74
+
62
75
  const [isEditing, setIsEditing] = useState(false)
63
76
  const [isDragging, setIsDragging] = useState(false)
64
77
  const [isHovered, setIsHovered] = useState(false)
65
- const [inputValue, setInputValue] = useState(value.toFixed(precision))
78
+ const [inputValue, setInputValue] = useState(toDisplay(value).toFixed(precision))
66
79
 
67
80
  const dragRef = useRef<{
68
81
  // Original value at drag start — preserved across modifier re-anchors so
@@ -80,12 +93,23 @@ export function SliderControl({
80
93
  valueRef.current = value
81
94
 
82
95
  const clamp = useCallback((val: number) => Math.min(Math.max(val, min), max), [min, max])
96
+ // Apply a signed display-unit delta to a stored value, rounding in the
97
+ // display unit and clamping in the stored unit.
98
+ const applyDisplayDelta = useCallback(
99
+ (storedValue: number, displayDelta: number, displayStep: number) =>
100
+ clamp(
101
+ toStored(
102
+ Number.parseFloat((toDisplay(storedValue) + displayDelta).toFixed(stepPrecision(displayStep))),
103
+ ),
104
+ ),
105
+ [clamp, toDisplay, toStored],
106
+ )
83
107
 
84
108
  useEffect(() => {
85
109
  if (!isEditing) {
86
- setInputValue(value.toFixed(precision))
110
+ setInputValue(toDisplay(value).toFixed(precision))
87
111
  }
88
- }, [value, precision, isEditing])
112
+ }, [value, precision, isEditing, toDisplay])
89
113
 
90
114
  // Wheel support on the label
91
115
  useEffect(() => {
@@ -96,14 +120,13 @@ export function SliderControl({
96
120
  e.preventDefault()
97
121
  const direction = e.deltaY < 0 ? 1 : -1
98
122
  const s = getAdjustedStep(step, e)
99
- const newValue = clamp(valueRef.current + direction * s)
100
- const final = Number.parseFloat(newValue.toFixed(stepPrecision(s)))
123
+ const final = applyDisplayDelta(valueRef.current, direction * s, s)
101
124
  if (final !== valueRef.current) onChange(final)
102
125
  onCommit?.(final)
103
126
  }
104
127
  el.addEventListener('wheel', handleWheel, { passive: false })
105
128
  return () => el.removeEventListener('wheel', handleWheel)
106
- }, [isEditing, step, clamp, onChange, onCommit])
129
+ }, [isEditing, step, applyDisplayDelta, onChange, onCommit])
107
130
 
108
131
  // Arrow key support while hovered
109
132
  useEffect(() => {
@@ -115,15 +138,14 @@ export function SliderControl({
115
138
  if (direction !== 0) {
116
139
  e.preventDefault()
117
140
  const s = getAdjustedStep(step, e)
118
- const newValue = clamp(valueRef.current + direction * s)
119
- const final = Number.parseFloat(newValue.toFixed(stepPrecision(s)))
141
+ const final = applyDisplayDelta(valueRef.current, direction * s, s)
120
142
  if (final !== valueRef.current) onChange(final)
121
143
  onCommit?.(final)
122
144
  }
123
145
  }
124
146
  window.addEventListener('keydown', handleKeyDown)
125
147
  return () => window.removeEventListener('keydown', handleKeyDown)
126
- }, [isHovered, isEditing, step, clamp, onChange, onCommit])
148
+ }, [isHovered, isEditing, step, applyDisplayDelta, onChange, onCommit])
127
149
 
128
150
  const handleLabelPointerDown = useCallback(
129
151
  (e: React.PointerEvent<HTMLDivElement>) => {
@@ -160,15 +182,13 @@ export function SliderControl({
160
182
  const dx = e.clientX - anchorX
161
183
  const s = step * multiplier
162
184
  // 4 px per step at default sensitivity
163
- const newValue = clamp(
164
- Number.parseFloat((anchorValue + (dx / 4) * s).toFixed(stepPrecision(s))),
165
- )
185
+ const newValue = applyDisplayDelta(anchorValue, (dx / 4) * s, s)
166
186
  if (newValue !== valueRef.current) {
167
187
  valueRef.current = newValue
168
188
  onChange(newValue)
169
189
  }
170
190
  },
171
- [step, clamp, onChange],
191
+ [step, applyDisplayDelta, onChange],
172
192
  )
173
193
 
174
194
  const handleLabelPointerUp = useCallback(
@@ -195,49 +215,65 @@ export function SliderControl({
195
215
 
196
216
  const handleValueClick = useCallback(() => {
197
217
  setIsEditing(true)
198
- setInputValue(value.toFixed(precision))
199
- }, [value, precision])
218
+ setInputValue(toDisplay(value).toFixed(precision))
219
+ }, [value, precision, toDisplay])
200
220
 
201
221
  const submitValue = useCallback(() => {
202
- const numValue = Number.parseFloat(inputValue)
203
- if (Number.isNaN(numValue)) {
204
- setInputValue(value.toFixed(precision))
222
+ const spec = lingoUnitSpec(unit)
223
+ let stored = spec
224
+ ? parseMeasurement(inputValue, spec, {
225
+ bareUnit: isImperial ? 'ft' : spec.unitId,
226
+ system: isImperial ? 'us' : 'metric',
227
+ })
228
+ : null
229
+ if (stored === null) {
230
+ // Fallback: a bare number typed in the DISPLAY unit → convert to stored.
231
+ const numValue = Number.parseFloat(inputValue)
232
+ stored = Number.isFinite(numValue) ? toStored(numValue) : null
233
+ }
234
+ if (stored === null) {
235
+ setInputValue(toDisplay(value).toFixed(precision))
205
236
  } else {
206
- const nextValue = clamp(Number.parseFloat(numValue.toFixed(precision)))
237
+ const nextValue = clamp(toStored(Number.parseFloat(toDisplay(stored).toFixed(precision))))
207
238
  onChange(nextValue)
208
239
  onCommit?.(nextValue)
209
240
  }
210
241
  setIsEditing(false)
211
- }, [inputValue, onChange, onCommit, clamp, precision, value])
242
+ }, [inputValue, unit, isImperial, onChange, onCommit, clamp, precision, value, toDisplay, toStored])
243
+
244
+ const spec = lingoUnitSpec(unit)
245
+ const hint =
246
+ isEditing && spec
247
+ ? measurementHint(inputValue, spec, {
248
+ bareUnit: isImperial ? 'ft' : spec.unitId,
249
+ system: isImperial ? 'us' : 'metric',
250
+ displayUnit: isImperial ? 'ft' : spec.unitId,
251
+ precision,
252
+ clamp,
253
+ })
254
+ : null
212
255
 
213
256
  const handleInputKeyDown = useCallback(
214
257
  (e: React.KeyboardEvent<HTMLInputElement>) => {
215
258
  if (e.key === 'Enter') {
216
259
  submitValue()
217
260
  } else if (e.key === 'Escape') {
218
- setInputValue(value.toFixed(precision))
261
+ setInputValue(toDisplay(value).toFixed(precision))
219
262
  setIsEditing(false)
220
- } else if (e.key === 'ArrowUp') {
263
+ } else if (e.key === 'ArrowUp' || e.key === 'ArrowDown') {
221
264
  e.preventDefault()
265
+ const direction = e.key === 'ArrowUp' ? 1 : -1
222
266
  const adjustedStep = getAdjustedStep(step, e)
223
- const newV = clamp(
224
- Number.parseFloat((value + adjustedStep).toFixed(stepPrecision(adjustedStep))),
225
- )
267
+ const newV = applyDisplayDelta(value, direction * adjustedStep, adjustedStep)
226
268
  onChange(newV)
227
- setInputValue(newV.toFixed(precision))
228
- } else if (e.key === 'ArrowDown') {
229
- e.preventDefault()
230
- const adjustedStep = getAdjustedStep(step, e)
231
- const newV = clamp(
232
- Number.parseFloat((value - adjustedStep).toFixed(stepPrecision(adjustedStep))),
233
- )
234
- onChange(newV)
235
- setInputValue(newV.toFixed(precision))
269
+ setInputValue(toDisplay(newV).toFixed(precision))
236
270
  }
237
271
  },
238
- [submitValue, value, precision, step, clamp, onChange],
272
+ [submitValue, value, precision, step, applyDisplayDelta, onChange, toDisplay],
239
273
  )
240
274
 
275
+ const displayValue = toDisplay(value)
276
+
241
277
  return (
242
278
  <div
243
279
  className={cn(
@@ -279,6 +315,11 @@ export function SliderControl({
279
315
  <div className="flex items-center text-xs">
280
316
  {isEditing ? (
281
317
  <>
318
+ {hint && (
319
+ <span className="mr-1 shrink-0 whitespace-nowrap text-[10px] text-muted-foreground/50 tabular-nums">
320
+ {hint}
321
+ </span>
322
+ )}
282
323
  <input
283
324
  autoFocus
284
325
  className="w-14 bg-transparent p-0 text-right font-mono text-foreground outline-none selection:bg-primary/30"
@@ -288,7 +329,16 @@ export function SliderControl({
288
329
  type="text"
289
330
  value={inputValue}
290
331
  />
291
- {unit && <span className="ml-[1px] text-muted-foreground">{unit}</span>}
332
+ {displayUnit && (
333
+ <span
334
+ className={cn(
335
+ 'ml-[1px] transition-opacity duration-150',
336
+ hint ? 'opacity-0' : 'text-muted-foreground/40',
337
+ )}
338
+ >
339
+ {displayUnit}
340
+ </span>
341
+ )}
292
342
  </>
293
343
  ) : (
294
344
  <div
@@ -296,9 +346,9 @@ export function SliderControl({
296
346
  onClick={handleValueClick}
297
347
  >
298
348
  <span className="font-mono tabular-nums tracking-tight" suppressHydrationWarning>
299
- {Number(value.toFixed(precision)).toFixed(precision)}
349
+ {Number(displayValue.toFixed(precision)).toFixed(precision)}
300
350
  </span>
301
- {unit && <span className="ml-[1px] text-muted-foreground">{unit}</span>}
351
+ {displayUnit && <span className="ml-[1px] text-muted-foreground">{displayUnit}</span>}
302
352
  </div>
303
353
  )}
304
354
  </div>
@@ -0,0 +1,193 @@
1
+ 'use client'
2
+
3
+ import {
4
+ type AnyNodeId,
5
+ RAISE_METRES_PER_STROKE,
6
+ type SiteNode,
7
+ type TerrainVerb,
8
+ useScene,
9
+ } from '@pascal-app/core'
10
+ import { Mountain, Pipette } from 'lucide-react'
11
+ import { brushRadiusRange, flattenSite, resetSiteTerrain } from '../../../lib/terrain-sculpt'
12
+ import useEditor from '../../../store/use-editor'
13
+ import { Button } from '../primitives/button'
14
+ import { SegmentedControl } from './segmented-control'
15
+ import { SliderControl } from './slider-control'
16
+
17
+ const VERB_OPTIONS: Array<{ value: TerrainVerb; iconSrc: string; hint: string }> = [
18
+ { value: 'raise', iconSrc: '/icons/terrain-raise.webp', hint: 'Raise' },
19
+ { value: 'lower', iconSrc: '/icons/terrain-lower.webp', hint: 'Lower' },
20
+ { value: 'flatten', iconSrc: '/icons/terrain-flatten.webp', hint: 'Flatten' },
21
+ { value: 'smooth', iconSrc: '/icons/terrain-smooth.webp', hint: 'Smooth' },
22
+ ]
23
+
24
+ const VERB_HINTS: Record<TerrainVerb, string> = {
25
+ raise: `Drag to raise the ground. One pass moves it up to ${RAISE_METRES_PER_STROKE} m — release and drag again to go further.`,
26
+ lower: `Drag to lower the ground. One pass moves it down to ${RAISE_METRES_PER_STROKE} m.`,
27
+ flatten: 'Drag to level the ground toward the target height. It never overshoots.',
28
+ smooth: 'Drag to soften slopes and remove ridges. Flat ground stays flat.',
29
+ }
30
+
31
+ /**
32
+ * Sculpt controls for terrain mode — verb, brush, and the two lot-wide actions.
33
+ *
34
+ * A panel rather than a floating HUD because the brush settings are the sort of
35
+ * thing a user adjusts between strokes and then leaves alone, and because
36
+ * sculpting already owns the whole viewport pointer: putting controls over the
37
+ * canvas would put them over the surface being sculpted.
38
+ *
39
+ * Embedders mount this wherever their sculpt controls belong (the community
40
+ * editor puts it in the Build sidebar while sculpt mode is active), exactly like
41
+ * `MaterialPaintPanel`.
42
+ */
43
+ export function TerrainSculptPanel() {
44
+ const verb = useEditor((state) => state.terrainVerb)
45
+ const setTerrainVerb = useEditor((state) => state.setTerrainVerb)
46
+ const brush = useEditor((state) => state.terrainBrush)
47
+ const setTerrainBrush = useEditor((state) => state.setTerrainBrush)
48
+ const flattenTarget = useEditor((state) => state.terrainFlattenTarget)
49
+ const setTerrainFlattenTarget = useEditor((state) => state.setTerrainFlattenTarget)
50
+ const sampling = useEditor((state) => state.terrainSampling)
51
+ const setTerrainSampling = useEditor((state) => state.setTerrainSampling)
52
+
53
+ const nodes = useScene((state) => state.nodes)
54
+ const rootNodeIds = useScene((state) => state.rootNodeIds)
55
+ const siteId = rootNodeIds[0]
56
+ const siteNode = siteId ? nodes[siteId as AnyNodeId] : undefined
57
+ const site = siteNode?.type === 'site' ? (siteNode as SiteNode) : null
58
+ const [minRadius, maxRadius] = brushRadiusRange(site)
59
+
60
+ return (
61
+ <div className="flex flex-col gap-3">
62
+ <div className="flex flex-col gap-1.5">
63
+ <SegmentedControl
64
+ className="h-14"
65
+ onChange={(next) => setTerrainVerb(next)}
66
+ options={VERB_OPTIONS.map(({ value, iconSrc, hint }) => ({
67
+ value,
68
+ label: (
69
+ <span className="flex flex-col items-center gap-0.5">
70
+ <img
71
+ alt=""
72
+ aria-hidden
73
+ className="size-7 object-contain"
74
+ draggable={false}
75
+ height={28}
76
+ src={iconSrc}
77
+ width={28}
78
+ />
79
+ <span className="text-[9px] leading-none">{hint}</span>
80
+ </span>
81
+ ),
82
+ }))}
83
+ value={verb}
84
+ />
85
+ <p className="px-0.5 text-muted-foreground text-xs">{VERB_HINTS[verb]}</p>
86
+ </div>
87
+
88
+ <div className="flex flex-col gap-1.5">
89
+ {/*
90
+ Range from `brushRadiusRange`, shared with the `[`/`]` keys. The low end
91
+ is not a preference: it tracks the field's sample spacing, and a brush
92
+ under it lands between samples and paints nothing at all.
93
+ */}
94
+ <SliderControl
95
+ label="Size"
96
+ max={maxRadius}
97
+ min={minRadius}
98
+ onChange={(radius) => setTerrainBrush({ radius })}
99
+ precision={1}
100
+ step={0.5}
101
+ unit="m"
102
+ value={brush.radius}
103
+ />
104
+ <SliderControl
105
+ label="Strength"
106
+ max={1}
107
+ min={0.05}
108
+ onChange={(strength) => setTerrainBrush({ strength })}
109
+ precision={2}
110
+ step={0.05}
111
+ value={brush.strength}
112
+ />
113
+ <SliderControl
114
+ label="Softness"
115
+ max={1}
116
+ min={0}
117
+ onChange={(falloff) => setTerrainBrush({ falloff })}
118
+ precision={2}
119
+ step={0.05}
120
+ value={brush.falloff}
121
+ />
122
+ <SegmentedControl
123
+ onChange={(shape) => setTerrainBrush({ shape })}
124
+ options={[
125
+ { value: 'round', label: 'Round' },
126
+ { value: 'square', label: 'Square' },
127
+ ]}
128
+ value={brush.shape}
129
+ />
130
+ </div>
131
+
132
+ {verb === 'flatten' && (
133
+ <div className="flex flex-col gap-1.5 border-border/60 border-t pt-3">
134
+ <div className="flex items-center gap-2">
135
+ <div className="min-w-0 flex-1">
136
+ <SliderControl
137
+ label="Target"
138
+ max={50}
139
+ min={-50}
140
+ onChange={setTerrainFlattenTarget}
141
+ precision={2}
142
+ step={0.1}
143
+ unit="m"
144
+ value={flattenTarget ?? 0}
145
+ />
146
+ </div>
147
+ <Button
148
+ aria-label="Pick target height from the ground"
149
+ aria-pressed={sampling}
150
+ onClick={() => setTerrainSampling(!sampling)}
151
+ size="icon-sm"
152
+ type="button"
153
+ variant={sampling ? 'default' : 'outline'}
154
+ >
155
+ <Pipette />
156
+ </Button>
157
+ </div>
158
+ <p className="px-0.5 text-muted-foreground text-xs">
159
+ {sampling
160
+ ? 'Click the ground to pick its height as the target.'
161
+ : flattenTarget === null
162
+ ? 'No target yet — the first click samples the ground under it.'
163
+ : 'Every flatten stroke levels toward this height.'}
164
+ </p>
165
+ </div>
166
+ )}
167
+
168
+ <div className="flex items-center gap-2 border-border/60 border-t pt-3">
169
+ <Button
170
+ className="flex-1"
171
+ disabled={!site}
172
+ onClick={() => site && flattenSite(site, flattenTarget ?? 0)}
173
+ size="sm"
174
+ type="button"
175
+ variant="outline"
176
+ >
177
+ <Mountain />
178
+ Level lot
179
+ </Button>
180
+ <Button
181
+ className="flex-1"
182
+ disabled={!site?.terrain}
183
+ onClick={() => site && resetSiteTerrain(site)}
184
+ size="sm"
185
+ type="button"
186
+ variant="outline"
187
+ >
188
+ Clear terrain
189
+ </Button>
190
+ </div>
191
+ </div>
192
+ )
193
+ }
@@ -22,6 +22,8 @@ import {
22
22
  type AnyNode,
23
23
  type AnyNodeId,
24
24
  type BuildingNode,
25
+ DEFAULT_LEVEL_HEIGHT,
26
+ getStoredLevelHeight,
25
27
  LevelNode,
26
28
  useScene,
27
29
  } from '@pascal-app/core'
@@ -34,22 +36,19 @@ import {
34
36
  useEffect,
35
37
  useRef,
36
38
  useState,
37
- useSyncExternalStore,
38
39
  } from 'react'
39
40
  import { useShallow } from 'zustand/react/shallow'
41
+ import { pasteSelectionAndPickUp } from '../editor/group-actions'
40
42
  import {
41
43
  buildLevelDuplicateCreateOps,
42
44
  type LevelDuplicatePreset,
43
45
  } from '../../lib/level-duplication'
44
- import { getDefaultLevelName, getLevelDisplayName } from '../../lib/level-name'
46
+ import { getDefaultLevelName, getLevelDisplayName } from '@pascal-app/core'
45
47
  import { deleteLevelWithFallbackSelection } from '../../lib/level-selection'
46
- import {
47
- getEditorClipboardSnapshot,
48
- pasteEditorClipboardToLevel,
49
- subscribeEditorClipboard,
50
- } from '../../lib/scene-clipboard'
51
- import { sfxEmitter } from '../../lib/sfx-bus'
48
+ import { useLinearDisplay } from '../../lib/use-linear-display'
52
49
  import { cn } from '../../lib/utils'
50
+ import { ActionButton } from './controls/action-button'
51
+ import { SliderControl } from './controls/slider-control'
53
52
  import { LevelDuplicateDialog } from './level-duplicate-dialog'
54
53
  import {
55
54
  Dialog,
@@ -145,6 +144,26 @@ function LevelRow({
145
144
  }) {
146
145
  const [duplicateDialogOpen, setDuplicateDialogOpen] = useState(false)
147
146
  const [isEditing, setIsEditing] = useState(false)
147
+ const updateNode = useScene((s) => s.updateNode)
148
+ const { isImperial, toDisplay, displayUnit } = useLinearDisplay('m', 2)
149
+
150
+ const storeyHeight = getStoredLevelHeight(level)
151
+ // toFixed(2) + strip one trailing zero: "2.50" → "2.5", "2.75" stays.
152
+ const storeyHeightLabel = `${toDisplay(storeyHeight).toFixed(2).replace(/0$/, '')} ${displayUnit}`
153
+
154
+ // Clean preset values per display system; imperial stores exact meters
155
+ // for whole-foot storey heights.
156
+ const heightPresets = isImperial
157
+ ? [
158
+ { label: '8 ft', height: 2.4384 },
159
+ { label: '9 ft', height: 2.7432 },
160
+ { label: '10 ft', height: 3.048 },
161
+ ]
162
+ : [
163
+ { label: '2.5 m', height: 2.5 },
164
+ { label: '3.0 m', height: 3.0 },
165
+ { label: '3.5 m', height: 3.5 },
166
+ ]
148
167
 
149
168
  return (
150
169
  <div className="group/level">
@@ -195,6 +214,48 @@ function LevelRow({
195
214
  <span className="truncate">{getLevelDisplayName(level)}</span>
196
215
  </button>
197
216
 
217
+ {/* Storey height badge — opens the height popover */}
218
+ <Popover>
219
+ <PopoverTrigger asChild>
220
+ <button
221
+ className="mr-0.5 shrink-0 whitespace-nowrap rounded px-1 py-0.5 font-mono text-[10px] text-muted-foreground/50 tabular-nums transition-colors hover:bg-white/5 hover:text-foreground"
222
+ onClick={(e) => e.stopPropagation()}
223
+ title="Level height"
224
+ type="button"
225
+ >
226
+ {storeyHeightLabel}
227
+ </button>
228
+ </PopoverTrigger>
229
+ <PopoverContent
230
+ align="start"
231
+ className="w-56 p-2"
232
+ onClick={(e) => e.stopPropagation()}
233
+ side="right"
234
+ sideOffset={8}
235
+ >
236
+ <SliderControl
237
+ label="Level height"
238
+ max={6}
239
+ min={1}
240
+ onChange={(v) => updateNode(level.id, { height: v })}
241
+ precision={3}
242
+ step={0.1}
243
+ unit="m"
244
+ value={Math.round(storeyHeight * 1000) / 1000}
245
+ />
246
+ <div className="mt-1.5 grid grid-cols-3 gap-1.5">
247
+ {heightPresets.map((preset) => (
248
+ <ActionButton
249
+ className="h-7 px-2"
250
+ key={preset.label}
251
+ label={preset.label}
252
+ onClick={() => updateNode(level.id, { height: preset.height })}
253
+ />
254
+ ))}
255
+ </div>
256
+ </PopoverContent>
257
+ </Popover>
258
+
198
259
  {/* Vertical three-dot menu — inside the pill */}
199
260
  <Popover>
200
261
  <PopoverTrigger asChild>
@@ -332,11 +393,6 @@ export function FloatingLevelSelector() {
332
393
 
333
394
  const [deletingLevel, setDeletingLevel] = useState<LevelNode | null>(null)
334
395
  const [draggingLevelId, setDraggingLevelId] = useState<string | null>(null)
335
- const clipboardSnapshot = useSyncExternalStore(
336
- subscribeEditorClipboard,
337
- getEditorClipboardSnapshot,
338
- getEditorClipboardSnapshot,
339
- )
340
396
  const sensors = useSensors(
341
397
  useSensor(PointerSensor, {
342
398
  activationConstraint: { distance: 4 },
@@ -371,6 +427,7 @@ export function FloatingLevelSelector() {
371
427
  const maxLevel = levels.length > 0 ? Math.max(...levels.map((l) => l.level)) : -1
372
428
  const newLevel = LevelNode.parse({
373
429
  level: maxLevel + 1,
430
+ height: DEFAULT_LEVEL_HEIGHT,
374
431
  children: [],
375
432
  parentId: resolvedBuildingId,
376
433
  })
@@ -383,6 +440,7 @@ export function FloatingLevelSelector() {
383
440
  const minLevel = levels.length > 0 ? Math.min(...levels.map((l) => l.level)) : 1
384
441
  const newLevel = LevelNode.parse({
385
442
  level: minLevel - 1,
443
+ height: DEFAULT_LEVEL_HEIGHT,
386
444
  children: [],
387
445
  parentId: resolvedBuildingId,
388
446
  })
@@ -409,6 +467,7 @@ export function FloatingLevelSelector() {
409
467
 
410
468
  const newLevel = LevelNode.parse({
411
469
  level: newLevelNumber,
470
+ height: DEFAULT_LEVEL_HEIGHT,
412
471
  children: [],
413
472
  parentId: resolvedBuildingId,
414
473
  })
@@ -452,10 +511,7 @@ export function FloatingLevelSelector() {
452
511
  )
453
512
 
454
513
  const handlePasteToLevel = useCallback((level: LevelNode) => {
455
- const result = pasteEditorClipboardToLevel(level.id)
456
- if (result?.pastedIds.length) {
457
- sfxEmitter.emit('sfx:item-place')
458
- }
514
+ void pasteSelectionAndPickUp(level.id)
459
515
  }, [])
460
516
 
461
517
  const handleDragStart = useCallback((event: DragStartEvent) => {
@@ -557,9 +613,7 @@ export function FloatingLevelSelector() {
557
613
  isSelected={isSelected}
558
614
  level={level}
559
615
  onDuplicate={(preset) => handleDuplicateLevel(level, preset)}
560
- onPaste={
561
- clipboardSnapshot ? () => handlePasteToLevel(level) : undefined
562
- }
616
+ onPaste={() => handlePasteToLevel(level)}
563
617
  onRequestDelete={() => setDeletingLevel(level)}
564
618
  onSelect={() =>
565
619
  setSelection(
@@ -1,32 +1,19 @@
1
- import { ShortcutToken } from '../primitives/shortcut-token'
1
+ import { ContextualHelperPanel } from './contextual-helper-panel'
2
2
 
3
3
  interface BuildingHelperProps {
4
4
  showRotate?: boolean
5
5
  }
6
6
 
7
+ // Rotate is one hint with both keys (R / T) — never two separate
8
+ // counterclockwise / clockwise rows — to match every other placement helper.
7
9
  export function BuildingHelper({ showRotate }: BuildingHelperProps) {
8
10
  return (
9
- <div className="pointer-events-none fixed top-1/2 right-4 z-40 flex -translate-y-1/2 flex-col gap-2 rounded-lg border border-border bg-background/95 px-4 py-3 shadow-lg backdrop-blur-md">
10
- <div className="flex items-center gap-2 text-sm">
11
- <ShortcutToken value="Left click" />
12
- <span className="text-muted-foreground">Place building</span>
13
- </div>
14
- {showRotate && (
15
- <>
16
- <div className="flex items-center gap-2 text-sm">
17
- <ShortcutToken value="R" />
18
- <span className="text-muted-foreground">Rotate counterclockwise</span>
19
- </div>
20
- <div className="flex items-center gap-2 text-sm">
21
- <ShortcutToken value="T" />
22
- <span className="text-muted-foreground">Rotate clockwise</span>
23
- </div>
24
- </>
25
- )}
26
- <div className="flex items-center gap-2 text-sm">
27
- <ShortcutToken value="Esc" />
28
- <span className="text-muted-foreground">Cancel</span>
29
- </div>
30
- </div>
11
+ <ContextualHelperPanel
12
+ hints={[
13
+ { keys: ['Left click'], label: 'Place building' },
14
+ ...(showRotate ? [{ keys: ['R', 'T'], label: 'Rotate' }] : []),
15
+ { keys: ['Esc'], label: 'Cancel' },
16
+ ]}
17
+ />
31
18
  )
32
19
  }