@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
@@ -1,5 +1,6 @@
1
1
  'use client'
2
2
 
3
+ import { Plus } from 'lucide-react'
3
4
  import type { ComponentType, ReactNode } from 'react'
4
5
  import {
5
6
  Tooltip,
@@ -10,7 +11,13 @@ import { cn } from './../../../lib/utils'
10
11
 
11
12
  export type PanelId = string
12
13
 
13
- export type ExtraPanel = { id: string; icon: ReactNode; label: string; component: ComponentType }
14
+ export type ExtraPanel = {
15
+ id: string
16
+ icon: ReactNode
17
+ label: string
18
+ component: ComponentType
19
+ pluginId?: string
20
+ }
14
21
 
15
22
  interface IconRailProps {
16
23
  activePanel: PanelId
@@ -22,13 +29,13 @@ interface IconRailProps {
22
29
 
23
30
  const sitePanel: { id: PanelId; iconSrc: string; label: string } = {
24
31
  id: 'site',
25
- iconSrc: '/icons/level.png',
32
+ iconSrc: '/icons/level.webp',
26
33
  label: 'Site',
27
34
  }
28
35
 
29
36
  const settingsPanel: { id: PanelId; iconSrc: string; label: string } = {
30
37
  id: 'settings',
31
- iconSrc: '/icons/settings.png',
38
+ iconSrc: '/icons/settings.webp',
32
39
  label: 'Settings',
33
40
  }
34
41
 
@@ -41,6 +48,38 @@ export function IconRail({
41
48
  extraPanels,
42
49
  className,
43
50
  }: IconRailProps) {
51
+ const regularExtraPanels = extraPanels?.filter((panel) => !panel.pluginId && panel.id !== 'plugins')
52
+ const pluginPanels = extraPanels?.filter((panel) => panel.pluginId)
53
+ const pluginsPanel = extraPanels?.find((panel) => panel.id === 'plugins')
54
+
55
+ const renderExtraPanel = (panel: ExtraPanel) => {
56
+ const isActive = activePanel === panel.id
57
+ return (
58
+ <Tooltip key={panel.id}>
59
+ <TooltipTrigger asChild>
60
+ <button
61
+ className={cn(
62
+ 'flex h-9 w-9 items-center justify-center rounded-lg transition-all',
63
+ isActive ? 'bg-accent' : 'hover:bg-accent',
64
+ )}
65
+ onClick={() => onPanelChange(panel.id)}
66
+ type="button"
67
+ >
68
+ <span
69
+ className={cn(
70
+ 'flex h-6 w-6 items-center justify-center transition-all',
71
+ !isActive && 'opacity-50',
72
+ )}
73
+ >
74
+ {panel.id === 'plugins' ? <Plus className="h-5 w-5" /> : panel.icon}
75
+ </span>
76
+ </button>
77
+ </TooltipTrigger>
78
+ <TooltipContent side="right">{panel.label}</TooltipContent>
79
+ </Tooltip>
80
+ )
81
+ }
82
+
44
83
  return (
45
84
  <div
46
85
  className={cn(
@@ -83,34 +122,7 @@ export function IconRail({
83
122
  )
84
123
  })}
85
124
 
86
- {/* Extra panels (injected between site and settings) */}
87
- {extraPanels?.map((panel) => {
88
- const isActive = activePanel === panel.id
89
- return (
90
- <Tooltip key={panel.id}>
91
- <TooltipTrigger asChild>
92
- <button
93
- className={cn(
94
- 'flex h-9 w-9 items-center justify-center rounded-lg transition-all',
95
- isActive ? 'bg-accent' : 'hover:bg-accent',
96
- )}
97
- onClick={() => onPanelChange(panel.id)}
98
- type="button"
99
- >
100
- <span
101
- className={cn(
102
- 'flex h-6 w-6 items-center justify-center transition-all',
103
- !isActive && 'opacity-50',
104
- )}
105
- >
106
- {panel.icon}
107
- </span>
108
- </button>
109
- </TooltipTrigger>
110
- <TooltipContent side="right">{panel.label}</TooltipContent>
111
- </Tooltip>
112
- )
113
- })}
125
+ {regularExtraPanels?.map(renderExtraPanel)}
114
126
 
115
127
  {/* Settings panel */}
116
128
  {[settingsPanel].map((panel) => {
@@ -140,6 +152,13 @@ export function IconRail({
140
152
  </Tooltip>
141
153
  )
142
154
  })}
155
+
156
+ {(pluginPanels?.length || pluginsPanel) && (
157
+ <div className="mt-1 flex w-9 flex-col items-center gap-1 border-border/70 border-t pt-2">
158
+ {pluginPanels?.map(renderExtraPanel)}
159
+ {pluginsPanel && renderExtraPanel(pluginsPanel)}
160
+ </div>
161
+ )}
143
162
  </div>
144
163
  )
145
164
  }
@@ -0,0 +1,218 @@
1
+ 'use client'
2
+
3
+ import { Icon } from '@iconify/react'
4
+ import { type IconRef, useScene } from '@pascal-app/core'
5
+ import { ChevronLeft, ChevronRight, ExternalLink, Puzzle } from 'lucide-react'
6
+ import { lazy, type ReactNode, Suspense, useState, useSyncExternalStore } from 'react'
7
+ import { editorHostPanelRegistry } from '../../../../lib/plugin-panels'
8
+ import { Button } from '../../primitives/button'
9
+
10
+ const PLUGIN_AUTHORING_URL =
11
+ 'https://editor.pascal.app/docs/developers/plugins'
12
+
13
+ function renderPluginIcon(ref: IconRef): ReactNode {
14
+ if (ref.kind === 'url') {
15
+ return <img alt="" className="h-8 w-8 object-contain" src={ref.src} />
16
+ }
17
+ if (ref.kind === 'iconify') {
18
+ return <Icon height={28} icon={ref.name} width={28} />
19
+ }
20
+ if (ref.kind === 'svg') {
21
+ return (
22
+ <svg height={28} viewBox={ref.viewBox} width={28}>
23
+ <path d={ref.path} fill="currentColor" />
24
+ </svg>
25
+ )
26
+ }
27
+ const LazyIcon = lazy(ref.module)
28
+ return (
29
+ <Suspense fallback={<Puzzle className="h-7 w-7" />}>
30
+ <LazyIcon />
31
+ </Suspense>
32
+ )
33
+ }
34
+
35
+ export function PluginsPanel() {
36
+ const [selectedPluginId, setSelectedPluginId] = useState<string | null>(null)
37
+ const panels = useSyncExternalStore(
38
+ editorHostPanelRegistry.subscribe,
39
+ editorHostPanelRegistry.getSnapshot,
40
+ editorHostPanelRegistry.getSnapshot,
41
+ )
42
+ const installedPlugins = useScene((state) => state.installedPlugins)
43
+ const setInstalledPlugins = useScene((state) => state.setInstalledPlugins)
44
+ const readOnly = useScene((state) => state.readOnly)
45
+ const plugins = Array.from(
46
+ new Map(
47
+ panels
48
+ .filter((panel) => panel.pluginId)
49
+ .map((panel) => [panel.pluginId as string, panel]),
50
+ ).entries(),
51
+ )
52
+ const selectedPlugin = selectedPluginId
53
+ ? plugins.find(([pluginId]) => pluginId === selectedPluginId)
54
+ : undefined
55
+
56
+ if (selectedPlugin) {
57
+ const [pluginId, panel] = selectedPlugin
58
+ const installed = installedPlugins.includes(pluginId)
59
+
60
+ return (
61
+ <div className="flex h-full flex-col overflow-y-auto p-4">
62
+ <div>
63
+ <Button
64
+ className="rounded-full"
65
+ onClick={() => setSelectedPluginId(null)}
66
+ size="sm"
67
+ variant="ghost"
68
+ >
69
+ <ChevronLeft className="h-4 w-4" />
70
+ All plugins
71
+ </Button>
72
+
73
+ <div className="mt-5 flex items-start gap-4">
74
+ <div className="flex h-16 w-16 shrink-0 items-center justify-center rounded-2xl bg-background/60">
75
+ {renderPluginIcon(panel.icon)}
76
+ </div>
77
+ <div className="min-w-0 pt-1">
78
+ <h2 className="font-semibold text-lg text-sidebar-foreground">{panel.label}</h2>
79
+ <p className="text-sidebar-foreground/50 text-sm">
80
+ {installed ? 'Installed' : 'Not installed'}
81
+ </p>
82
+ </div>
83
+ </div>
84
+
85
+ <p className="mt-5 text-sidebar-foreground/70 text-sm">
86
+ {panel.description ?? 'Adds a new tool panel to the editor.'}
87
+ </p>
88
+
89
+ <dl className="mt-6 divide-y divide-border/50 rounded-xl border border-border/60">
90
+ <div className="p-3">
91
+ <dt className="text-sidebar-foreground/50 text-xs">Plugin ID</dt>
92
+ <dd className="mt-1 break-all text-sidebar-foreground text-sm">{pluginId}</dd>
93
+ </div>
94
+ {panel.creator && (
95
+ <div className="p-3">
96
+ <dt className="text-sidebar-foreground/50 text-xs">Creator</dt>
97
+ <dd className="mt-1 text-sm">
98
+ {panel.creator.url ? (
99
+ <a
100
+ className="inline-flex items-center gap-1 text-sidebar-foreground underline-offset-4 hover:underline"
101
+ href={panel.creator.url}
102
+ rel="noreferrer"
103
+ target="_blank"
104
+ >
105
+ {panel.creator.name}
106
+ <ExternalLink className="h-3 w-3" />
107
+ </a>
108
+ ) : (
109
+ panel.creator.name
110
+ )}
111
+ </dd>
112
+ </div>
113
+ )}
114
+ {panel.pluginUrl && (
115
+ <div className="p-3">
116
+ <dt className="text-sidebar-foreground/50 text-xs">Plugin</dt>
117
+ <dd className="mt-1 text-sm">
118
+ <a
119
+ className="inline-flex items-center gap-1 text-sidebar-foreground underline-offset-4 hover:underline"
120
+ href={panel.pluginUrl}
121
+ rel="noreferrer"
122
+ target="_blank"
123
+ >
124
+ View plugin
125
+ <ExternalLink className="h-3 w-3" />
126
+ </a>
127
+ </dd>
128
+ </div>
129
+ )}
130
+ </dl>
131
+
132
+ <Button
133
+ className="mt-5 rounded-full"
134
+ disabled={readOnly}
135
+ onClick={() => {
136
+ const next = installed
137
+ ? installedPlugins.filter((id) => id !== pluginId)
138
+ : [...installedPlugins, pluginId]
139
+ setInstalledPlugins(next, { explicit: true })
140
+ }}
141
+ variant={installed ? 'outline' : 'default'}
142
+ >
143
+ {installed ? 'Uninstall' : 'Install'}
144
+ </Button>
145
+ </div>
146
+
147
+ <div className="mt-auto pt-6">
148
+ <a
149
+ className="inline-flex items-center gap-1.5 text-sidebar-foreground/70 text-sm underline-offset-4 hover:text-sidebar-foreground hover:underline"
150
+ href={PLUGIN_AUTHORING_URL}
151
+ rel="noreferrer"
152
+ target="_blank"
153
+ >
154
+ Create a Pascal plugin
155
+ <ExternalLink className="h-3.5 w-3.5" />
156
+ </a>
157
+ </div>
158
+ </div>
159
+ )
160
+ }
161
+
162
+ return (
163
+ <div className="flex h-full flex-col overflow-y-auto p-4">
164
+ <div className="mb-5">
165
+ <h2 className="font-semibold text-lg text-sidebar-foreground">Plugins</h2>
166
+ <p className="mt-1 text-sidebar-foreground/60 text-sm">
167
+ Add focused tools and content to this project.
168
+ </p>
169
+ </div>
170
+
171
+ <div className="flex flex-col gap-3">
172
+ {plugins.map(([pluginId, panel]) => {
173
+ const installed = installedPlugins.includes(pluginId)
174
+ return (
175
+ <button
176
+ className="w-full rounded-xl border border-border/60 bg-accent/20 p-3 text-left transition-colors hover:bg-accent/40"
177
+ key={pluginId}
178
+ onClick={() => setSelectedPluginId(pluginId)}
179
+ type="button"
180
+ >
181
+ <div className="flex items-start gap-3">
182
+ <div className="flex h-11 w-11 shrink-0 items-center justify-center rounded-xl bg-background/60">
183
+ {renderPluginIcon(panel.icon)}
184
+ </div>
185
+ <div className="min-w-0 flex-1">
186
+ <div className="flex items-center justify-between gap-3">
187
+ <div>
188
+ <h3 className="font-medium text-sidebar-foreground">{panel.label}</h3>
189
+ <p className="text-sidebar-foreground/50 text-xs">
190
+ {installed ? 'Installed' : 'Not installed'}
191
+ </p>
192
+ </div>
193
+ <ChevronRight className="h-4 w-4 shrink-0 text-sidebar-foreground/50" />
194
+ </div>
195
+ <p className="mt-2 text-sidebar-foreground/60 text-sm">
196
+ {panel.description ?? 'Adds a new tool panel to the editor.'}
197
+ </p>
198
+ </div>
199
+ </div>
200
+ </button>
201
+ )
202
+ })}
203
+ </div>
204
+
205
+ <div className="mt-auto pt-6">
206
+ <a
207
+ className="inline-flex items-center gap-1.5 text-sidebar-foreground/70 text-sm underline-offset-4 hover:text-sidebar-foreground hover:underline"
208
+ href={PLUGIN_AUTHORING_URL}
209
+ rel="noreferrer"
210
+ target="_blank"
211
+ >
212
+ Create a Pascal plugin
213
+ <ExternalLink className="h-3.5 w-3.5" />
214
+ </a>
215
+ </div>
216
+ </div>
217
+ )
218
+ }
@@ -1,7 +1,12 @@
1
- import { emitter, useScene, validateBuildJson } from '@pascal-app/core'
1
+ import {
2
+ clearSceneHistory,
3
+ emitter,
4
+ useScene,
5
+ validateBuildJson,
6
+ } from '@pascal-app/core'
2
7
  import { useViewer } from '@pascal-app/viewer'
3
8
  import { TreeView, VisualJson } from '@visual-json/react'
4
- import { Camera, Download, Save, Trash2, Upload } from 'lucide-react'
9
+ import { Camera, Download, Map as MapIcon, Save, Trash2, Upload } from 'lucide-react'
5
10
  import {
6
11
  type KeyboardEvent,
7
12
  type SyntheticEvent,
@@ -10,6 +15,7 @@ import {
10
15
  useRef,
11
16
  useState,
12
17
  } from 'react'
18
+ import { exportFloorplanPdf } from '../../../../../lib/floorplan/floorplan-export'
13
19
  import { Button } from './../../../../../components/ui/primitives/button'
14
20
  import {
15
21
  Dialog,
@@ -19,6 +25,7 @@ import {
19
25
  } from './../../../../../components/ui/primitives/dialog'
20
26
  import { Switch } from './../../../../../components/ui/primitives/switch'
21
27
  import useEditor, { selectDefaultBuildingAndLevel } from './../../../../../store/use-editor'
28
+ import useFloorplanMode from './../../../../../store/use-floorplan-mode'
22
29
  import { AudioSettingsDialog } from './audio-settings-dialog'
23
30
  import { KeyboardShortcutsDialog } from './keyboard-shortcuts-dialog'
24
31
  import { LoadBuildDialog, type PendingImport } from './load-build-dialog'
@@ -179,13 +186,14 @@ export function SettingsPanel({
179
186
  const fileInputRef = useRef<HTMLInputElement>(null)
180
187
  const nodes = useScene((state) => state.nodes)
181
188
  const rootNodeIds = useScene((state) => state.rootNodeIds)
189
+ const installedPlugins = useScene((state) => state.installedPlugins)
182
190
  const setScene = useScene((state) => state.setScene)
183
191
  const clearScene = useScene((state) => state.clearScene)
184
192
  const resetSelection = useViewer((state) => state.resetSelection)
185
193
  const exportScene = useViewer((state) => state.exportScene)
186
- const showGrid = useViewer((state) => state.showGrid)
187
194
  const shadows = useViewer((state) => state.shadows)
188
195
  const setPhase = useEditor((state) => state.setPhase)
196
+ const floorplanMode = useFloorplanMode((state) => state.mode)
189
197
  const [isGeneratingThumbnail, setIsGeneratingThumbnail] = useState(false)
190
198
  const [pendingImport, setPendingImport] = useState<PendingImport | null>(null)
191
199
  const sceneGraphValue = useMemo(
@@ -206,7 +214,7 @@ export function SettingsPanel({
206
214
  const isLocalProject = false // Props-based; only show cloud sections when projectId provided
207
215
 
208
216
  const handleSaveBuild = () => {
209
- const sceneData = { nodes, rootNodeIds }
217
+ const sceneData = { nodes, rootNodeIds, installedPlugins }
210
218
  const json = JSON.stringify(sceneData, null, 2)
211
219
  const blob = new Blob([json], { type: 'application/json' })
212
220
  const url = URL.createObjectURL(blob)
@@ -235,7 +243,7 @@ export function SettingsPanel({
235
243
  result: {
236
244
  ok: false,
237
245
  parsed: null,
238
- stats: { total: 0, byType: {}, unknownTypes: {}, floorAreaM2: 0 },
246
+ stats: { total: 0, byType: {}, pluginTypes: {}, unknownTypes: {}, floorAreaM2: 0 },
239
247
  errors: [
240
248
  {
241
249
  severity: 'error',
@@ -262,11 +270,24 @@ export function SettingsPanel({
262
270
  e.target.value = ''
263
271
  }
264
272
 
265
- const handleConfirmImport = (parsed: { nodes: Record<string, unknown>; rootNodeIds: string[] }) => {
273
+ const handleConfirmImport = (parsed: {
274
+ nodes: Record<string, unknown>
275
+ rootNodeIds: string[]
276
+ installedPlugins?: string[]
277
+ }) => {
278
+ const currentScene = useScene.getState()
266
279
  setScene(
267
280
  parsed.nodes as Parameters<typeof setScene>[0],
268
281
  parsed.rootNodeIds as Parameters<typeof setScene>[1],
282
+ {
283
+ installedPlugins: parsed.installedPlugins ?? currentScene.installedPlugins,
284
+ hasExplicitPluginInstallState:
285
+ parsed.installedPlugins !== undefined || currentScene.hasExplicitPluginInstallState,
286
+ },
269
287
  )
288
+ // An import is a scene load: it becomes the undo floor. Without this,
289
+ // undo could step back into the pre-import scene state.
290
+ clearSceneHistory()
270
291
  resetSelection()
271
292
  setPhase('site')
272
293
  setPendingImport(null)
@@ -274,6 +295,9 @@ export function SettingsPanel({
274
295
 
275
296
  const handleResetToDefault = () => {
276
297
  clearScene()
298
+ // Same floor rule as import — undo after a reset must not resurrect the
299
+ // old scene (or land on the empty intermediate `unloadScene` state).
300
+ clearSceneHistory()
277
301
  resetSelection()
278
302
  setPhase('structure')
279
303
  selectDefaultBuildingAndLevel()
@@ -331,16 +355,6 @@ export function SettingsPanel({
331
355
  onCheckedChange={(checked) => handleVisibilityChange('showGuidesPublic', checked)}
332
356
  />
333
357
  </div>
334
- <div className="flex items-center justify-between">
335
- <div>
336
- <div className="font-medium text-sm">Show Grid</div>
337
- <div className="text-muted-foreground text-xs">Visible only in the editor</div>
338
- </div>
339
- <Switch
340
- checked={showGrid}
341
- onCheckedChange={(checked) => useViewer.getState().setShowGrid(checked)}
342
- />
343
- </div>
344
358
  <div className="flex items-center justify-between">
345
359
  <div>
346
360
  <div className="font-medium text-sm">Shadows</div>
@@ -355,32 +369,59 @@ export function SettingsPanel({
355
369
  )}
356
370
 
357
371
  {/* Export Section */}
358
- <div className="space-y-2">
372
+ <div className="space-y-4">
359
373
  <label className="font-medium text-muted-foreground text-xs uppercase">Export</label>
360
- <Button
361
- className="w-full justify-start gap-2"
362
- onClick={() => exportScene?.('glb')}
363
- variant="outline"
364
- >
365
- <Download className="size-4" />
366
- Export GLB
367
- </Button>
368
- <Button
369
- className="w-full justify-start gap-2"
370
- onClick={() => exportScene?.('stl')}
371
- variant="outline"
372
- >
373
- <Download className="size-4" />
374
- Export STL
375
- </Button>
376
- <Button
377
- className="w-full justify-start gap-2"
378
- onClick={() => exportScene?.('obj')}
379
- variant="outline"
380
- >
381
- <Download className="size-4" />
382
- Export OBJ
383
- </Button>
374
+
375
+ <div className="space-y-2">
376
+ <div className="font-medium text-muted-foreground text-xs">3D model</div>
377
+ <Button
378
+ className="w-full justify-start gap-2"
379
+ onClick={() => exportScene?.('glb')}
380
+ variant="outline"
381
+ >
382
+ <Download className="size-4" />
383
+ Export GLB
384
+ </Button>
385
+ <Button
386
+ className="w-full justify-start gap-2"
387
+ onClick={() => exportScene?.('stl')}
388
+ variant="outline"
389
+ >
390
+ <Download className="size-4" />
391
+ Export STL
392
+ </Button>
393
+ <Button
394
+ className="w-full justify-start gap-2"
395
+ onClick={() => exportScene?.('obj')}
396
+ variant="outline"
397
+ >
398
+ <Download className="size-4" />
399
+ Export OBJ
400
+ </Button>
401
+ </div>
402
+
403
+ <div className="space-y-2">
404
+ <div className="flex items-center justify-between font-medium text-muted-foreground text-xs">
405
+ <span>Floor plan</span>
406
+ <span>{floorplanMode === 'default' ? 'Default mode' : 'Expert mode'}</span>
407
+ </div>
408
+ <Button
409
+ className="w-full justify-start gap-2"
410
+ onClick={() => exportFloorplanPdf('full')}
411
+ variant="outline"
412
+ >
413
+ <MapIcon className="size-4" />
414
+ Full floor plan
415
+ </Button>
416
+ <Button
417
+ className="w-full justify-start gap-2"
418
+ onClick={() => exportFloorplanPdf('structure')}
419
+ variant="outline"
420
+ >
421
+ <MapIcon className="size-4" />
422
+ Structure only
423
+ </Button>
424
+ </div>
384
425
  </div>
385
426
 
386
427
  {/* Thumbnail Section (only for cloud projects) */}
@@ -55,6 +55,7 @@ const SHORTCUT_CATEGORIES: ShortcutCategory[] = [
55
55
  shortcuts: [
56
56
  { keys: ['V'], action: 'Switch to Select mode' },
57
57
  { keys: ['B'], action: 'Switch to Build mode' },
58
+ { keys: ['M'], action: 'Activate the last measurement tool' },
58
59
  { keys: ['X'], action: 'Switch to Delete mode' },
59
60
  {
60
61
  keys: ['Esc'],
@@ -68,10 +69,67 @@ const SHORTCUT_CATEGORIES: ShortcutCategory[] = [
68
69
  {
69
70
  title: 'Selection',
70
71
  shortcuts: [
72
+ {
73
+ keys: ['Cmd/Ctrl', 'C'],
74
+ action: 'Copy the selected objects',
75
+ note: 'The copied selection can be pasted into another level, project, or browser tab.',
76
+ },
77
+ {
78
+ keys: ['Cmd/Ctrl', 'X'],
79
+ action: 'Cut the selected objects',
80
+ note: 'Copies the selection to the clipboard, then removes it from this scene.',
81
+ },
82
+ {
83
+ keys: ['Cmd/Ctrl', 'V'],
84
+ action: 'Paste and place copied objects',
85
+ note:
86
+ 'Carries a preview under the cursor. Click to place it, or press Escape to cancel.',
87
+ },
71
88
  {
72
89
  keys: ['Cmd/Ctrl', 'Left click'],
73
90
  action: 'Add or remove an object from multi-selection',
74
- note: 'Works while in Select mode.',
91
+ note: 'Works in Select mode on the 3D canvas, the 2D floor plan, and the scene graph.',
92
+ },
93
+ {
94
+ keys: ['Shift', 'Left click'],
95
+ action: 'Add or remove an object from canvas multi-selection',
96
+ note: 'In the scene graph, Shift-click selects the visible range like a file browser.',
97
+ },
98
+ {
99
+ keys: ['Left click'],
100
+ action: 'Move the whole multi-selection',
101
+ note:
102
+ 'With 2+ objects selected, in 2D and 3D alike: drag the selection (or its dashed box) to slide it; click it to pick it up and place with the next click.',
103
+ },
104
+ {
105
+ keys: ['R', 'T'],
106
+ action: 'Rotate a multi-selection ±45° around its center',
107
+ note: 'Also works mid-move while carrying the selection.',
108
+ },
109
+ {
110
+ keys: ['Esc'],
111
+ action: 'Clear the selection',
112
+ note: 'Clicking empty space does the same.',
113
+ },
114
+ ],
115
+ },
116
+ {
117
+ title: 'Direct Manipulation',
118
+ shortcuts: [
119
+ {
120
+ keys: ['Cmd/Ctrl', 'Left click'],
121
+ action: 'Move the selected movable object under the cursor',
122
+ note: 'Drag in Select mode with a single object selected. Guided snapping and guides are enabled by default.',
123
+ },
124
+ {
125
+ keys: ['Cmd/Ctrl', 'Right click'],
126
+ action: 'Rotate the selected object under the cursor',
127
+ note: 'Drag left or right in Select mode with a single object selected. Rotation snaps to 15° increments by default.',
128
+ },
129
+ {
130
+ keys: ['Cmd/Ctrl', 'Shift', 'Right click'],
131
+ action: 'Rotate freely',
132
+ note: 'Hold Shift during the drag to bypass the 15° rotation increment.',
75
133
  },
76
134
  ],
77
135
  },
@@ -80,16 +138,27 @@ const SHORTCUT_CATEGORIES: ShortcutCategory[] = [
80
138
  shortcuts: [
81
139
  {
82
140
  keys: ['Shift'],
83
- action: 'Temporarily disable angle snapping while drawing walls, slabs, and ceilings',
84
- note: 'Hold while drawing.',
141
+ action: 'Bypass guided snapping and angle constraints',
142
+ note: 'Hold during the active gesture. Passive guide or measurement feedback may stay visible.',
143
+ },
144
+ {
145
+ keys: ['Shift'],
146
+ action: 'Rotate freely, bypassing the default 15° rotation snap',
147
+ note: 'Hold while dragging a rotate handle or direct-rotation gesture.',
85
148
  },
86
149
  ],
87
150
  },
88
151
  {
89
152
  title: 'Item Placement',
90
153
  shortcuts: [
91
- { keys: ['R'], action: 'Rotate item clockwise, or toggle selected door open/closed' },
92
- { keys: ['T'], action: 'Rotate item counter-clockwise, or close selected door' },
154
+ {
155
+ keys: ['R', 'T'],
156
+ action: 'Rotate item; with a door selected, R toggles open/closed and T closes',
157
+ },
158
+ {
159
+ keys: ['E'],
160
+ action: 'Operate the selected node — doors, windows, and cabinet doors/drawers animate open/closed',
161
+ },
93
162
  {
94
163
  keys: ['Shift'],
95
164
  action: 'Temporarily bypass placement validation constraints',
@@ -157,7 +226,8 @@ export function KeyboardShortcutsDialog() {
157
226
  <DialogHeader className="shrink-0 border-b px-6 py-4">
158
227
  <DialogTitle>Keyboard Shortcuts</DialogTitle>
159
228
  <DialogDescription>
160
- Shortcuts are context-aware and depend on the current phase or tool.
229
+ Shortcuts are context-aware. Guided constraints are enabled by default; hold Shift
230
+ during an active gesture to build freely.
161
231
  </DialogDescription>
162
232
  </DialogHeader>
163
233