@pascal-app/editor 1.0.5 → 1.0.7

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 (317) hide show
  1. package/package.json +20 -17
  2. package/src/components/editor/action-menu-button.tsx +4 -1
  3. package/src/components/editor/alignment-3d-guide-layer.tsx +102 -11
  4. package/src/components/editor/bake-thumbnail.tsx +3 -2
  5. package/src/components/editor/custom-camera-controls.tsx +20 -8
  6. package/src/components/editor/elevation-3d-guide-layer.tsx +1 -1
  7. package/src/components/editor/first-person-controls.tsx +72 -61
  8. package/src/components/editor/floating-action-menu.tsx +27 -9
  9. package/src/components/editor/floating-building-action-menu.tsx +1 -1
  10. package/src/components/editor/floorplan-panel.test.ts +220 -0
  11. package/src/components/editor/floorplan-panel.tsx +285 -132
  12. package/src/components/editor/footprint-height-handle.tsx +1 -1
  13. package/src/components/editor/grid.tsx +24 -15
  14. package/src/components/editor/group-actions.ts +19 -1
  15. package/src/components/editor/group-floating-action-menu.tsx +1 -1
  16. package/src/components/editor/group-move-3d.ts +27 -2
  17. package/src/components/editor/group-selection-box-3d.tsx +4 -1
  18. package/src/components/editor/group-transform-shared.ts +20 -0
  19. package/src/components/editor/index.tsx +130 -20
  20. package/src/components/editor/inspector-extension-actions.tsx +36 -0
  21. package/src/components/editor/localized-html.tsx +22 -0
  22. package/src/components/editor/node-action-menu.tsx +46 -12
  23. package/src/components/editor/node-arrow-handles.tsx +1 -2
  24. package/src/components/editor/open-wall-ends-3d-layer.tsx +1 -1
  25. package/src/components/editor/opening-guides-3d-layer.tsx +1 -1
  26. package/src/components/editor/registry-action-contributions.test.tsx +67 -6
  27. package/src/components/editor/registry-action-contributions.tsx +115 -15
  28. package/src/components/editor/riser-diagram-panel.tsx +59 -9
  29. package/src/components/editor/room-controls.tsx +1 -1
  30. package/src/components/editor/room-handles-2d.tsx +156 -6
  31. package/src/components/editor/room-handles.tsx +241 -51
  32. package/src/components/editor/room-highlight.tsx +2 -0
  33. package/src/components/editor/room-labels.tsx +29 -15
  34. package/src/components/editor/selection-manager.tsx +15 -102
  35. package/src/components/editor/separator-end-grip.tsx +74 -0
  36. package/src/components/editor/site-edge-labels.tsx +1 -1
  37. package/src/components/editor/thumbnail-generator.test.ts +15 -6
  38. package/src/components/editor/thumbnail-generator.tsx +26 -1
  39. package/src/components/editor/use-floorplan-background-placement.ts +55 -52
  40. package/src/components/editor/views/view-bar.tsx +111 -26
  41. package/src/components/editor/wall-measurement-label.tsx +1 -1
  42. package/src/components/editor/wall-move-side-handles.tsx +5 -1
  43. package/src/components/editor/wall-pull-grip.tsx +193 -0
  44. package/src/components/editor/wall-snap-beacon-layer.tsx +81 -1
  45. package/src/components/editor-2d/floorplan-adjacent-walls-layer.test.ts +76 -0
  46. package/src/components/editor-2d/floorplan-adjacent-walls-layer.tsx +186 -0
  47. package/src/components/editor-2d/floorplan-alignment-guide-layer.tsx +63 -3
  48. package/src/components/editor-2d/floorplan-group-move.tsx +31 -3
  49. package/src/components/editor-2d/floorplan-measurement-tool-layer.test.ts +6 -0
  50. package/src/components/editor-2d/floorplan-measurement-tool-layer.tsx +22 -13
  51. package/src/components/editor-2d/floorplan-open-wall-ends-layer.tsx +11 -3
  52. package/src/components/editor-2d/floorplan-registry-move-overlay.tsx +7 -1
  53. package/src/components/editor-2d/floorplan-snap-beacon-layer.tsx +96 -4
  54. package/src/components/editor-2d/renderers/floorplan-dimension-edit-overlay.tsx +52 -12
  55. package/src/components/editor-2d/renderers/floorplan-registry-layer.test.ts +12 -1
  56. package/src/components/editor-2d/renderers/floorplan-registry-layer.tsx +197 -37
  57. package/src/components/procedural-items/inspector.tsx +59 -12
  58. package/src/components/systems/ceiling/ceiling-bracket-batch.test.ts +2 -0
  59. package/src/components/systems/ceiling/ceiling-system.test.ts +141 -0
  60. package/src/components/systems/ceiling/ceiling-system.tsx +53 -18
  61. package/src/components/tools/distribution/distribution-run-tool.tsx +50 -26
  62. package/src/components/tools/item/placement-history.test.tsx +1 -1
  63. package/src/components/tools/item/placement-strategies.test.ts +64 -1
  64. package/src/components/tools/item/placement-strategies.ts +5 -9
  65. package/src/components/tools/item/use-placement-coordinator.tsx +30 -34
  66. package/src/components/tools/registry/item-surface-move.test.ts +11 -0
  67. package/src/components/tools/select/select-candidates.test.ts +45 -0
  68. package/src/components/tools/select/select-candidates.ts +13 -2
  69. package/src/components/tools/shared/cursor-sphere.tsx +10 -3
  70. package/src/components/tools/shared/draft-measurement-label.tsx +1 -1
  71. package/src/components/tools/shared/placement-box.tsx +1 -1
  72. package/src/components/tools/shared/placement-dimension-guides.tsx +1 -1
  73. package/src/components/tools/site/terrain-brush-cursor.tsx +1 -1
  74. package/src/components/tools/wall/wall-drafting.test.ts +299 -23
  75. package/src/components/tools/wall/wall-drafting.ts +132 -24
  76. package/src/components/tools/wall/wall-pull-snap.test.ts +274 -0
  77. package/src/components/tools/wall/wall-pull-snap.ts +563 -0
  78. package/src/components/tools/wall/wall-snap-geometry.test.ts +113 -1
  79. package/src/components/tools/wall/wall-snap-geometry.ts +137 -0
  80. package/src/components/tools/zone/zone-tool.tsx +3 -3
  81. package/src/components/ui/action-menu/action-button.tsx +2 -0
  82. package/src/components/ui/action-menu/camera-actions.tsx +8 -7
  83. package/src/components/ui/action-menu/control-modes.tsx +9 -5
  84. package/src/components/ui/action-menu/follow-pascal-pill.tsx +3 -1
  85. package/src/components/ui/action-menu/follow-pascal-toggle.tsx +7 -1
  86. package/src/components/ui/action-menu/furnish-tools.tsx +7 -6
  87. package/src/components/ui/action-menu/index.tsx +1 -0
  88. package/src/components/ui/action-menu/measurement-control.tsx +35 -29
  89. package/src/components/ui/action-menu/point-ask-button.tsx +4 -3
  90. package/src/components/ui/action-menu/structure-tools.tsx +25 -23
  91. package/src/components/ui/action-menu/view-toggles.test.ts +18 -0
  92. package/src/components/ui/action-menu/view-toggles.tsx +256 -51
  93. package/src/components/ui/build-panel/build-panel.tsx +163 -25
  94. package/src/components/ui/command-palette/editor-commands.tsx +38 -11
  95. package/src/components/ui/controls/finish-swatch.tsx +45 -19
  96. package/src/components/ui/controls/material-picker.tsx +24 -17
  97. package/src/components/ui/controls/metric-control.test.ts +52 -0
  98. package/src/components/ui/controls/metric-control.tsx +62 -24
  99. package/src/components/ui/controls/segmented-control.tsx +5 -2
  100. package/src/components/ui/controls/slider-control.test.tsx +23 -0
  101. package/src/components/ui/controls/slider-control.tsx +30 -14
  102. package/src/components/ui/floating-level-selector.tsx +100 -48
  103. package/src/components/ui/helpers/building-helper.tsx +5 -3
  104. package/src/components/ui/helpers/contextual-helper-panel.tsx +97 -37
  105. package/src/components/ui/helpers/helper-manager.test.tsx +53 -3
  106. package/src/components/ui/helpers/helper-manager.tsx +82 -57
  107. package/src/components/ui/helpers/item-helper.tsx +6 -4
  108. package/src/components/ui/helpers/registered-tool-helper.tsx +3 -1
  109. package/src/components/ui/level-duplicate-dialog.tsx +32 -23
  110. package/src/components/ui/level-wall-copy-prompt.tsx +124 -0
  111. package/src/components/ui/panels/collections/collections-popover.tsx +11 -11
  112. package/src/components/ui/panels/deck-boards-fields.tsx +136 -0
  113. package/src/components/ui/panels/floor-foundation-panel.tsx +113 -38
  114. package/src/components/ui/panels/floor-opening-panel.tsx +73 -25
  115. package/src/components/ui/panels/inspector-extensions.tsx +66 -0
  116. package/src/components/ui/panels/mobile-panel-sheet.tsx +3 -1
  117. package/src/components/ui/panels/mobile-selection-bar.tsx +8 -6
  118. package/src/components/ui/panels/multi-field-value.test.ts +27 -0
  119. package/src/components/ui/panels/multi-field-value.ts +20 -1
  120. package/src/components/ui/panels/multi-height-mode.tsx +8 -7
  121. package/src/components/ui/panels/multi-parametric-inspector.tsx +122 -34
  122. package/src/components/ui/panels/multi-selection-panel.tsx +75 -35
  123. package/src/components/ui/panels/node-display.ts +35 -24
  124. package/src/components/ui/panels/panel-manager.tsx +5 -3
  125. package/src/components/ui/panels/panel-wrapper.tsx +22 -109
  126. package/src/components/ui/panels/parametric-field-control.tsx +13 -10
  127. package/src/components/ui/panels/parametric-inspector.tsx +18 -12
  128. package/src/components/ui/panels/reference-panel.tsx +51 -35
  129. package/src/components/ui/panels/room-construction-rows.tsx +464 -65
  130. package/src/components/ui/panels/room-name-options.test.ts +36 -3
  131. package/src/components/ui/panels/room-name-options.ts +110 -11
  132. package/src/components/ui/panels/room-openings-field.tsx +21 -12
  133. package/src/components/ui/panels/room-panel.tsx +206 -99
  134. package/src/components/ui/panels/shape-choice.tsx +7 -5
  135. package/src/components/ui/panels/wall-region-list.tsx +7 -7
  136. package/src/components/ui/primitives/shortcut-token.tsx +10 -6
  137. package/src/components/ui/sidebar/icon-rail.tsx +11 -9
  138. package/src/components/ui/sidebar/panels/items-panel/function-tree-panel.tsx +10 -10
  139. package/src/components/ui/sidebar/panels/items-panel/index.tsx +10 -9
  140. package/src/components/ui/sidebar/panels/settings-panel/index.tsx +3 -0
  141. package/src/components/ui/sidebar/panels/settings-panel/keyboard-shortcuts-dialog.tsx +102 -32
  142. package/src/components/ui/sidebar/panels/site-panel/building-tree-node.tsx +4 -3
  143. package/src/components/ui/sidebar/panels/site-panel/ceiling-tree-node.tsx +4 -2
  144. package/src/components/ui/sidebar/panels/site-panel/chimney-tree-node.tsx +4 -2
  145. package/src/components/ui/sidebar/panels/site-panel/collection-tree-node.tsx +7 -5
  146. package/src/components/ui/sidebar/panels/site-panel/column-tree-node.tsx +4 -2
  147. package/src/components/ui/sidebar/panels/site-panel/default-names.ts +75 -0
  148. package/src/components/ui/sidebar/panels/site-panel/door-tree-node.tsx +4 -2
  149. package/src/components/ui/sidebar/panels/site-panel/dormer-tree-node.tsx +4 -2
  150. package/src/components/ui/sidebar/panels/site-panel/elevator-tree-node.tsx +4 -2
  151. package/src/components/ui/sidebar/panels/site-panel/fence-tree-node.tsx +4 -2
  152. package/src/components/ui/sidebar/panels/site-panel/gutter-tree-node.tsx +4 -2
  153. package/src/components/ui/sidebar/panels/site-panel/index.tsx +117 -64
  154. package/src/components/ui/sidebar/panels/site-panel/inline-rename-input.tsx +36 -3
  155. package/src/components/ui/sidebar/panels/site-panel/item-tree-node.tsx +4 -2
  156. package/src/components/ui/sidebar/panels/site-panel/level-tree-node.tsx +7 -4
  157. package/src/components/ui/sidebar/panels/site-panel/registry-tree-node.tsx +8 -2
  158. package/src/components/ui/sidebar/panels/site-panel/roof-tree-node.tsx +8 -5
  159. package/src/components/ui/sidebar/panels/site-panel/shelf-tree-node.tsx +4 -2
  160. package/src/components/ui/sidebar/panels/site-panel/slab-tree-node.tsx +8 -3
  161. package/src/components/ui/sidebar/panels/site-panel/solar-panel-tree-node.tsx +4 -2
  162. package/src/components/ui/sidebar/panels/site-panel/spawn-tree-node.tsx +4 -2
  163. package/src/components/ui/sidebar/panels/site-panel/stair-tree-node.tsx +8 -7
  164. package/src/components/ui/sidebar/panels/site-panel/tree-node-actions.tsx +7 -6
  165. package/src/components/ui/sidebar/panels/site-panel/tree-node.tsx +16 -1
  166. package/src/components/ui/sidebar/panels/site-panel/unit-tree-node.tsx +15 -8
  167. package/src/components/ui/sidebar/panels/site-panel/wall-tree-node.tsx +4 -2
  168. package/src/components/ui/sidebar/panels/site-panel/window-tree-node.tsx +4 -2
  169. package/src/components/ui/sidebar/panels/site-panel/zone-membership-checkbox.tsx +3 -1
  170. package/src/components/ui/sidebar/panels/site-panel/zone-tree-node.tsx +18 -8
  171. package/src/components/ui/sidebar/panels/zone-panel/index.tsx +25 -14
  172. package/src/components/ui/sidebar/tab-bar.tsx +4 -1
  173. package/src/components/ui/sidebar/use-plugin-panels.tsx +19 -10
  174. package/src/components/ui/snap-target-badge.tsx +7 -6
  175. package/src/components/ui/wall-type/wall-type-editor-dialog.tsx +721 -0
  176. package/src/components/ui/wall-type/wall-type-menu.tsx +167 -0
  177. package/src/components/viewer/floorplan-compass-button.tsx +12 -3
  178. package/src/components/viewer/floorplan-preview.tsx +27 -19
  179. package/src/components/viewer/render-settings-menu.tsx +294 -0
  180. package/src/components/viewer/sky-control.tsx +710 -0
  181. package/src/components/viewer/use-viewer-ui-sounds.test.ts +15 -0
  182. package/src/components/viewer/use-viewer-ui-sounds.ts +46 -0
  183. package/src/components/viewer/viewer-controls-bar.tsx +278 -260
  184. package/src/components/viewer/viewer-hover-label.tsx +12 -4
  185. package/src/components/viewer/viewer-rooms.tsx +61 -4
  186. package/src/components/viewer/viewer-scene-header.tsx +28 -17
  187. package/src/components/viewer/viewer-stage-switcher.tsx +4 -2
  188. package/src/components/viewer-overlay.tsx +8 -0
  189. package/src/components/walkthrough-hud.tsx +75 -2
  190. package/src/hooks/use-keyboard.test.ts +85 -4
  191. package/src/hooks/use-keyboard.ts +65 -7
  192. package/src/hooks/use-selected-room.ts +12 -1
  193. package/src/i18n/floor-footprint-name.ts +30 -0
  194. package/src/i18n/level-name.ts +39 -0
  195. package/src/i18n/locales/en/messages.po +15117 -0
  196. package/src/i18n/locales/en/messages.ts +1 -0
  197. package/src/i18n/locales/en-XA/messages.ts +1 -0
  198. package/src/i18n/locales/es/messages.po +37501 -0
  199. package/src/i18n/locales/es/messages.ts +1 -0
  200. package/src/i18n/locales/fr/messages.po +37501 -0
  201. package/src/i18n/locales/fr/messages.ts +1 -0
  202. package/src/i18n/locales/ja/messages.po +37501 -0
  203. package/src/i18n/locales/ja/messages.ts +1 -0
  204. package/src/i18n/locales/zh-CN/messages.po +37501 -0
  205. package/src/i18n/locales/zh-CN/messages.ts +1 -0
  206. package/src/i18n/locales.test.ts +63 -0
  207. package/src/i18n/locales.ts +32 -0
  208. package/src/i18n/message-slot.tsx +25 -0
  209. package/src/i18n/node-messages.ts +169 -0
  210. package/src/i18n/pseudo-locale.ts +17 -0
  211. package/src/i18n/published-entry.test.ts +17 -0
  212. package/src/i18n/scene-theme-name.ts +16 -0
  213. package/src/index.tsx +156 -4
  214. package/src/lib/add-storey.test.ts +3 -3
  215. package/src/lib/contextual-help.test.ts +20 -1
  216. package/src/lib/contextual-help.ts +95 -19
  217. package/src/lib/continuation.ts +38 -11
  218. package/src/lib/floor-footprints.test.ts +2 -1
  219. package/src/lib/floor-foundation-support.test.ts +7 -3
  220. package/src/lib/floorplan/dimension-drive.test.ts +36 -2
  221. package/src/lib/floorplan/dimension-drive.ts +68 -3
  222. package/src/lib/floorplan/floorplan-export.test.ts +16 -5
  223. package/src/lib/floorplan/floorplan-export.tsx +13 -16
  224. package/src/lib/floorplan/floorplan-extension.test.ts +11 -12
  225. package/src/lib/floorplan/floorplan-extension.ts +37 -18
  226. package/src/lib/floorplan/floorplan-mode.test.ts +13 -6
  227. package/src/lib/floorplan/floorplan-mode.ts +14 -12
  228. package/src/lib/floorplan/floorplan-plugin-install.test.tsx +1 -1
  229. package/src/lib/floorplan/floorplan-readonly.ts +3 -3
  230. package/src/lib/floorplan/schedules.ts +10 -6
  231. package/src/lib/floorplan/site-plan/site-annotations.test.ts +46 -1
  232. package/src/lib/floorplan/site-plan/site-annotations.ts +3 -0
  233. package/src/lib/floorplan/site-plan/site-parts.ts +6 -3
  234. package/src/lib/geometry-script/author.test.ts +114 -0
  235. package/src/lib/geometry-script/author.ts +55 -23
  236. package/src/lib/geometry-script/client.ts +3 -0
  237. package/src/lib/geometry-script/finish-scripts.ts +7 -7
  238. package/src/lib/glb-export.test.ts +327 -0
  239. package/src/lib/glb-export.ts +70 -2
  240. package/src/lib/grid-frame.test.ts +56 -3
  241. package/src/lib/grid-frame.ts +28 -1
  242. package/src/lib/history.test.ts +2 -1
  243. package/src/lib/hud-title.ts +48 -24
  244. package/src/lib/ifc-export.test.ts +41 -1
  245. package/src/lib/inspector-card-mode.test.ts +3 -31
  246. package/src/lib/inspector-card-mode.ts +5 -10
  247. package/src/lib/inspector-expanded.test.ts +13 -0
  248. package/src/lib/inspector-expanded.ts +13 -1
  249. package/src/lib/keyboard-pan.test.ts +7 -0
  250. package/src/lib/keyboard-pan.ts +9 -2
  251. package/src/lib/layer-thickness.test.ts +22 -0
  252. package/src/lib/layer-thickness.ts +79 -0
  253. package/src/lib/lot/drop-in.test.ts +59 -1
  254. package/src/lib/lot/drop-in.ts +42 -50
  255. package/src/lib/material-swatch-style.ts +24 -0
  256. package/src/lib/measurement-parser.ts +7 -3
  257. package/src/lib/measurements.test.ts +17 -0
  258. package/src/lib/measurements.ts +20 -5
  259. package/src/lib/mezzanine-selection.test.ts +3 -1
  260. package/src/lib/orbit-near-plane.test.ts +12 -0
  261. package/src/lib/orbit-near-plane.ts +13 -0
  262. package/src/lib/paint-regions.test.ts +8 -5
  263. package/src/lib/paint-scope.test.ts +53 -0
  264. package/src/lib/paint-scope.ts +18 -4
  265. package/src/lib/panel-rows.ts +17 -8
  266. package/src/lib/reference-scale.ts +83 -0
  267. package/src/lib/room-built-on.test.ts +26 -12
  268. package/src/lib/room-built-on.ts +20 -2
  269. package/src/lib/room-construction.test.ts +28 -1
  270. package/src/lib/room-construction.ts +14 -0
  271. package/src/lib/room-controls-phase6.test.ts +140 -0
  272. package/src/lib/room-handle-drag.ts +557 -42
  273. package/src/lib/room-name-catalog.test.ts +8 -0
  274. package/src/lib/room-name-catalog.ts +8 -1
  275. package/src/lib/room-name-messages.test.ts +132 -0
  276. package/src/lib/room-name-messages.ts +147 -0
  277. package/src/lib/room-name-storage.test.ts +334 -0
  278. package/src/lib/room-push-dimensions.test.ts +37 -0
  279. package/src/lib/room-push-dimensions.ts +226 -2
  280. package/src/lib/room-selection.test.ts +23 -6
  281. package/src/lib/scene-clipboard.test.ts +70 -49
  282. package/src/lib/scene-clipboard.ts +9 -4
  283. package/src/lib/selection-cycle.ts +63 -0
  284. package/src/lib/separator-edit.test.ts +431 -0
  285. package/src/lib/separator-edit.ts +179 -0
  286. package/src/lib/separator-lines.ts +38 -0
  287. package/src/lib/sfx-bus.ts +27 -1
  288. package/src/lib/sfx-player.ts +16 -0
  289. package/src/lib/tool-cursor-icon.ts +5 -1
  290. package/src/lib/tool-shortcuts.ts +130 -0
  291. package/src/lib/viewer-selection.test.ts +28 -24
  292. package/src/lib/viewer-selection.ts +20 -5
  293. package/src/lib/wall-draw-variant.test.ts +7 -6
  294. package/src/lib/wall-draw-variant.ts +107 -6
  295. package/src/lib/wall-nudge.test.ts +44 -0
  296. package/src/lib/wall-nudge.ts +131 -0
  297. package/src/lib/wall-parallel-snap.test.ts +83 -0
  298. package/src/lib/wall-parallel-snap.ts +176 -0
  299. package/src/lib/wall-placement-pointer.test.ts +23 -0
  300. package/src/lib/wall-placement-pointer.ts +18 -0
  301. package/src/lib/wall-pull.test.ts +245 -0
  302. package/src/lib/wall-pull.ts +209 -0
  303. package/src/lib/wall-push-line-up.test.ts +132 -0
  304. package/src/lib/wall-region-list.ts +14 -5
  305. package/src/lib/wall-run-snap.test.ts +188 -0
  306. package/src/lib/wall-run-snap.ts +83 -0
  307. package/src/lib/wall-type.test.ts +125 -0
  308. package/src/lib/wall-type.ts +119 -0
  309. package/src/lib/window-interaction.test.ts +42 -0
  310. package/src/lib/window-interaction.ts +47 -5
  311. package/src/lib/world-grid-snap.test.ts +139 -2
  312. package/src/lib/world-grid-snap.ts +102 -105
  313. package/src/store/use-alignment-guides.ts +27 -1
  314. package/src/store/use-editor.tsx +48 -0
  315. package/src/store/use-floorplan-annotation-visibility.test.ts +64 -0
  316. package/src/store/use-floorplan-annotation-visibility.ts +47 -22
  317. package/src/store/use-wall-snap-indicator.ts +15 -2
@@ -1,15 +1,20 @@
1
1
  'use client'
2
2
 
3
- import { useScene } from '@pascal-app/core'
3
+ import { usePascalI18n, useScene } from '@pascal-app/core'
4
4
  import { useViewer } from '@pascal-app/viewer'
5
5
  import { nodeDisplayLabel } from '../../lib/node-display-label'
6
+ import type { DescriptorTranslator } from '../../lib/room-name-messages'
6
7
  import { type ViewerNodes, zoneLabel } from '../../lib/viewer-selection'
7
8
 
8
9
  /** The hovered node's name, for the bottom pill. A room reads as the room. */
9
- export function viewerHoverLabel(hoveredId: string | null, nodes: ViewerNodes): string | null {
10
+ export function viewerHoverLabel(
11
+ hoveredId: string | null,
12
+ nodes: ViewerNodes,
13
+ i18n: DescriptorTranslator,
14
+ ): string | null {
10
15
  const node = hoveredId ? nodes[hoveredId] : undefined
11
16
  if (!node) return null
12
- return node.type === 'zone' ? zoneLabel(node) : nodeDisplayLabel(node)
17
+ return node.type === 'zone' ? zoneLabel(node, i18n) : nodeDisplayLabel(node)
13
18
  }
14
19
 
15
20
  /**
@@ -19,9 +24,12 @@ export function viewerHoverLabel(hoveredId: string | null, nodes: ViewerNodes):
19
24
  * the parametric scene store unless the surface passes its own graph.
20
25
  */
21
26
  export function ViewerHoverLabel({ nodes: nodesProp }: { nodes?: ViewerNodes }) {
27
+ const i18n = usePascalI18n()
22
28
  const sceneNodes = useScene((s) => s.nodes)
23
29
  const nodes = nodesProp ?? sceneNodes
24
- const label = useViewer((s) => (s.walkthroughMode ? null : viewerHoverLabel(s.hoveredId, nodes)))
30
+ const label = useViewer((s) =>
31
+ s.walkthroughMode ? null : viewerHoverLabel(s.hoveredId, nodes, i18n),
32
+ )
25
33
  if (!label) return null
26
34
  return (
27
35
  <div
@@ -3,12 +3,16 @@
3
3
  import {
4
4
  type AnyNode,
5
5
  buildUnitReport,
6
+ polygonInteriorPoint,
6
7
  sceneRegistry,
8
+ useLocale,
9
+ usePascalI18n,
7
10
  useScene,
8
11
  type ZoneNode,
9
12
  } from '@pascal-app/core'
10
- import { useViewer } from '@pascal-app/viewer'
11
- import { useEffect, useMemo } from 'react'
13
+ import { SelectionCallout, useViewer } from '@pascal-app/viewer'
14
+ import { useCallback, useEffect, useMemo } from 'react'
15
+ import { Vector3 } from 'three'
12
16
  import { useShallow } from 'zustand/react/shallow'
13
17
  import { resolveRoomAssemblyHeights } from '../../lib/room-assembly-overlay'
14
18
  import { roomFloorElevation } from '../../lib/room-handle-drag'
@@ -21,7 +25,9 @@ import {
21
25
  } from '../../lib/viewer-selection'
22
26
  import { RoomAssemblyMesh } from '../editor/room-highlight'
23
27
  import {
28
+ formatRoomArea,
24
29
  RoomLabelAnchor3D,
30
+ type RoomLabelEntry,
25
31
  RoomPillChip,
26
32
  RoomPillDetail,
27
33
  RoomPillsLayer3D,
@@ -38,6 +44,7 @@ import {
38
44
  * mounted at all, so they never catch a click.
39
45
  */
40
46
  export function ViewerRooms({ source }: { source: ViewerSource }) {
47
+ const i18n = usePascalI18n()
41
48
  useEffect(() => {
42
49
  const before = useViewer.getState().showZones
43
50
  useViewer.getState().setShowZones(false)
@@ -119,7 +126,7 @@ export function ViewerRooms({ source }: { source: ViewerSource }) {
119
126
  <RoomPillDetail
120
127
  areaM2={entry.area}
121
128
  color={zone.color}
122
- name={zoneLabel(zone)}
129
+ name={zoneLabel(zone, i18n)}
123
130
  view="3d"
124
131
  zoneId={zone.id}
125
132
  />
@@ -138,7 +145,7 @@ export function ViewerRooms({ source }: { source: ViewerSource }) {
138
145
  areaM2={entry.area}
139
146
  color={zone.color}
140
147
  elements={elements}
141
- name={zoneLabel(zone)}
148
+ name={zoneLabel(zone, i18n)}
142
149
  onClick={() => selectViewerRoom(zone.id, source)}
143
150
  onHover={() => useViewer.getState().setHoveredId(zone.id)}
144
151
  onLeave={() => {
@@ -157,10 +164,60 @@ export function ViewerRooms({ source }: { source: ViewerSource }) {
157
164
  {highlighted.map((room) => (
158
165
  <ViewerRoomHighlight key={room.zoneId} room={room} source={source} />
159
166
  ))}
167
+ {zoneId && levelId && !walkthrough && selectedIds.length === 0
168
+ ? (() => {
169
+ const entry = entries.find((candidate) => candidate.zoneId === zoneId)
170
+ const zone = nodes[zoneId]
171
+ const level = source.objectFor(levelId)
172
+ return entry && zone?.type === 'zone' && level ? (
173
+ <ViewerRoomCallout
174
+ entry={entry}
175
+ floorY={roomFloorElevation(nodes as Record<string, AnyNode>, zoneId)}
176
+ key={zoneId}
177
+ level={level}
178
+ name={zoneLabel(zone, i18n)}
179
+ />
180
+ ) : null
181
+ })()
182
+ : null}
160
183
  </>
161
184
  )
162
185
  }
163
186
 
187
+ /** The selected room's callout: its name and area at a point inside it, and the close button. */
188
+ function ViewerRoomCallout({
189
+ entry,
190
+ floorY,
191
+ level,
192
+ name,
193
+ }: {
194
+ entry: RoomLabelEntry
195
+ floorY: number
196
+ level: NonNullable<ReturnType<ViewerSource['objectFor']>>
197
+ name: string
198
+ }) {
199
+ const unit = useViewer((s) => s.unit)
200
+ const locale = useLocale()
201
+ const anchor = useMemo(() => {
202
+ const [x, z] = polygonInteriorPoint({ polygon: entry.polygon, holes: entry.holes })
203
+ return new Vector3(x, floorY + 0.05, z)
204
+ }, [entry.polygon, entry.holes, floorY])
205
+ const dismiss = useCallback(() => {
206
+ const { selection, setSelection } = useViewer.getState()
207
+ if (selection.zoneId === entry.zoneId) setSelection({ zoneId: null })
208
+ }, [entry.zoneId])
209
+ return (
210
+ <SelectionCallout
211
+ actions={[]}
212
+ anchor={anchor}
213
+ object={level}
214
+ onDismiss={dismiss}
215
+ subtitle={formatRoomArea(entry.area, unit, locale)}
216
+ title={name}
217
+ />
218
+ )
219
+ }
220
+
164
221
  const parametricObject = (id: string) => sceneRegistry.nodes.get(id)
165
222
 
166
223
  /** `ViewerRooms` over the parametric scene store. */
@@ -1,10 +1,18 @@
1
1
  'use client'
2
2
 
3
- import { getLevelDisplayName, type LevelNode, sceneRegistry, useScene } from '@pascal-app/core'
3
+ import {
4
+ type LevelNode,
5
+ type PascalI18n,
6
+ sceneRegistry,
7
+ useLocale,
8
+ usePascalI18n,
9
+ useScene,
10
+ } from '@pascal-app/core'
4
11
  import { markPerfAction, useViewer } from '@pascal-app/viewer'
5
12
  import { ArrowLeft, ChevronRight, Layers } from 'lucide-react'
6
13
  import Link from 'next/link'
7
14
  import { type ReactNode, useMemo } from 'react'
15
+ import { localizedLevelName } from '../../i18n/level-name'
8
16
  import { cn } from '../../lib/utils'
9
17
  import {
10
18
  applyViewerClick,
@@ -36,10 +44,10 @@ export type ViewerSceneHeaderProps = {
36
44
  source?: ViewerSource
37
45
  }
38
46
 
39
- const SECTION_TITLES: Record<ViewerSidebarSection['kind'], string> = {
40
- units: 'Units',
41
- rooms: 'Rooms',
42
- zones: 'Zones',
47
+ function sectionTitle(kind: ViewerSidebarSection['kind'], i18n: PascalI18n): string {
48
+ if (kind === 'units') return i18n._({ id: 'viewer.header.units', message: 'Units' })
49
+ if (kind === 'rooms') return i18n._({ id: 'viewer.header.rooms', message: 'Rooms' })
50
+ return i18n._({ id: 'viewer.header.zones', message: 'Zones' })
43
51
  }
44
52
 
45
53
  const parametricObject = (id: string) => sceneRegistry.nodes.get(id)
@@ -58,6 +66,9 @@ export const ViewerSceneHeader = ({
58
66
  stats,
59
67
  source: sourceProp,
60
68
  }: ViewerSceneHeaderProps) => {
69
+ const i18n = usePascalI18n()
70
+ const catalogVersion = i18n.getVersion()
71
+ const locale = useLocale()
61
72
  const sceneNodes = useScene((s) => s.nodes)
62
73
  const source = useMemo<ViewerSource>(
63
74
  () => sourceProp ?? { nodes: sceneNodes, objectFor: parametricObject },
@@ -70,10 +81,10 @@ export const ViewerSceneHeader = ({
70
81
 
71
82
  const crumbs = useMemo(
72
83
  () =>
73
- viewerBreadcrumb(selection, focusedUnitId, nodes, (nodeId, levelId) =>
84
+ viewerBreadcrumb(selection, focusedUnitId, nodes, i18n, (nodeId, levelId) =>
74
85
  roomOfObject(nodeId, levelId, source),
75
86
  ),
76
- [selection, focusedUnitId, nodes, source],
87
+ [selection, focusedUnitId, nodes, source, i18n, locale, catalogVersion],
77
88
  )
78
89
  const buildingId = crumbs.find((crumb) => crumb.kind === 'building')?.id ?? null
79
90
  // Highest first so the list reads top-down like a building section.
@@ -86,8 +97,8 @@ export const ViewerSceneHeader = ({
86
97
  .sort((a, b) => b.level - a.level)
87
98
  }, [buildingId, nodes])
88
99
  const sections = useMemo(
89
- () => (selection.levelId ? viewerSidebarSections(selection.levelId, nodes) : []),
90
- [selection.levelId, nodes],
100
+ () => (selection.levelId ? viewerSidebarSections(selection.levelId, nodes, i18n) : []),
101
+ [selection.levelId, nodes, i18n, locale, catalogVersion],
91
102
  )
92
103
 
93
104
  const handleCrumb = (crumb: ViewerCrumb) => {
@@ -113,7 +124,7 @@ export const ViewerSceneHeader = ({
113
124
  <div className="flex items-center gap-3 px-3 py-2.5">
114
125
  {onBack ? (
115
126
  <button
116
- aria-label="Back"
127
+ aria-label={i18n._({ id: 'viewer.header.back', message: 'Back' })}
117
128
  className="flex h-7 w-7 shrink-0 items-center justify-center rounded-md transition-colors hover:bg-white/10"
118
129
  onClick={onBack}
119
130
  type="button"
@@ -122,7 +133,7 @@ export const ViewerSceneHeader = ({
122
133
  </button>
123
134
  ) : backHref === null ? null : (
124
135
  <Link
125
- aria-label="Back"
136
+ aria-label={i18n._({ id: 'viewer.header.back', message: 'Back' })}
126
137
  className="flex h-7 w-7 shrink-0 items-center justify-center rounded-md transition-colors hover:bg-white/10"
127
138
  href={backHref}
128
139
  prefetch={false}
@@ -132,7 +143,7 @@ export const ViewerSceneHeader = ({
132
143
  )}
133
144
  <div className="min-w-0 flex-1">
134
145
  <div className="truncate font-medium text-foreground text-sm">
135
- {projectName || 'Untitled'}
146
+ {projectName || i18n._({ id: 'viewer.header.untitled', message: 'Untitled' })}
136
147
  </div>
137
148
  {owner?.username && (
138
149
  <Link
@@ -151,7 +162,7 @@ export const ViewerSceneHeader = ({
151
162
 
152
163
  {crumbs.length > 0 && (
153
164
  <nav
154
- aria-label="Selection"
165
+ aria-label={i18n._({ id: 'viewer.header.selection', message: 'Selection' })}
155
166
  className="flex flex-wrap items-center gap-x-1.5 gap-y-1 border-border/40 border-t px-3 py-2 text-xs"
156
167
  data-testid="viewer-breadcrumb"
157
168
  >
@@ -186,7 +197,7 @@ export const ViewerSceneHeader = ({
186
197
  {levels.length > 0 && (
187
198
  <div className="corner-smooth pointer-events-auto flex min-h-0 w-56 flex-col overflow-hidden rounded-2xl border border-border/40 bg-background/95 py-1 shadow-elevation-4 backdrop-blur-xl transition-colors duration-200 ease-out">
188
199
  <span className="px-3 py-2 font-medium text-[10px] text-muted-foreground uppercase tracking-wider">
189
- Levels
200
+ {i18n._({ id: 'viewer.header.levels', message: 'Levels' })}
190
201
  </span>
191
202
  <div className="flex min-h-0 flex-col overflow-y-auto">
192
203
  {levels.map((lvl) => {
@@ -216,7 +227,7 @@ export const ViewerSceneHeader = ({
216
227
  <Layers className="h-3.5 w-3.5" />
217
228
  </span>
218
229
  <div className="min-w-0 flex-1 truncate text-left">
219
- {getLevelDisplayName(lvl)}
230
+ {localizedLevelName(lvl, i18n, locale)}
220
231
  </div>
221
232
  </div>
222
233
  </button>
@@ -228,7 +239,7 @@ export const ViewerSceneHeader = ({
228
239
  key={section.kind}
229
240
  >
230
241
  <span className="block px-3 pt-2 pb-1 pl-9 font-medium text-[10px] text-muted-foreground uppercase tracking-wider">
231
- {SECTION_TITLES[section.kind]}
242
+ {sectionTitle(section.kind, i18n)}
232
243
  </span>
233
244
  {section.rows.map((row) => {
234
245
  const active =
@@ -257,7 +268,7 @@ export const ViewerSceneHeader = ({
257
268
  />
258
269
  <span className="min-w-0 flex-1 truncate">{row.label}</span>
259
270
  <span className="shrink-0 font-mono text-[11px] text-muted-foreground tabular-nums">
260
- {formatRoomArea(row.areaM2, unit)}
271
+ {formatRoomArea(row.areaM2, unit, locale)}
261
272
  </span>
262
273
  </button>
263
274
  )
@@ -1,5 +1,6 @@
1
1
  'use client'
2
2
 
3
+ import { usePascalI18n } from '@pascal-app/core'
3
4
  import { Box, Columns2, Map as MapIcon } from 'lucide-react'
4
5
  import type { ReactNode } from 'react'
5
6
  import { cn } from '../../lib/utils'
@@ -22,11 +23,12 @@ export function ViewerStageSwitcher({
22
23
  modes,
23
24
  onChange,
24
25
  }: ViewerStageSwitcherProps) {
26
+ const i18n = usePascalI18n()
25
27
  const enabledModes = normalizeViewerStageModes(modes)
26
28
 
27
29
  return (
28
30
  <div
29
- aria-label="Viewer layout"
31
+ aria-label={i18n._({ id: 'viewer.stage.layout', message: 'Viewer layout' })}
30
32
  className={cn(
31
33
  'dark absolute top-4 left-1/2 z-30 flex -translate-x-1/2 items-center gap-1 rounded-full border border-white/10 bg-neutral-950/82 p-1 text-white shadow-elevation-4 backdrop-blur-xl',
32
34
  className,
@@ -54,7 +56,7 @@ export function ViewerStageSwitcher({
54
56
  active={mode === 'split'}
55
57
  className={hideSplitOnMobile && enabledModes.length > 1 ? 'hidden md:flex' : undefined}
56
58
  icon={<Columns2 />}
57
- label="Split"
59
+ label={i18n._({ id: 'viewer.stage.split', message: 'Split' })}
58
60
  onClick={() => onChange('split')}
59
61
  />
60
62
  ) : null}
@@ -36,6 +36,14 @@ export const ViewerOverlay = ({
36
36
  <ViewerControlsBar
37
37
  canShowGuides={canShowGuides}
38
38
  canShowScans={canShowScans}
39
+ onDroneToggle={() => {
40
+ // Arm drone before first person so the controller never builds the walk's collider.
41
+ flushSync(() => {
42
+ useEditor.getState().setFirstPersonMovementMode('drone')
43
+ useEditor.getState().setFirstPersonMode(true)
44
+ })
45
+ requestWalkthroughPointerLock()
46
+ }}
39
47
  onWalkthroughToggle={() => {
40
48
  flushSync(() => useEditor.getState().setFirstPersonMode(true))
41
49
  requestWalkthroughPointerLock()
@@ -1,15 +1,22 @@
1
1
  'use client'
2
2
 
3
- import type { ReactNode } from 'react'
3
+ import { usePascalI18n } from '@pascal-app/core'
4
+ import { type ReactNode, useMemo } from 'react'
5
+ import { displayRoomName } from '../lib/room-name-messages'
4
6
  import { cn } from '../lib/utils'
5
7
  import type { WalkthroughInteract } from '../store/use-first-person-hud'
6
8
 
7
9
  export type { WalkthroughInteract } from '../store/use-first-person-hud'
8
10
 
11
+ /** A control shown in the HUD's key strip: its keys (as printed on the keycaps) and what they do. */
12
+ export type WalkthroughHint = { keys: readonly string[]; action: string }
13
+
9
14
  export type WalkthroughHudProps = {
10
15
  floorLabel?: string | null
11
16
  zoneLabel?: string | null
12
17
  interact?: WalkthroughInteract
18
+ /** Movement controls listed before the cursor and exit pills. */
19
+ hints?: readonly WalkthroughHint[]
13
20
  /** Pointer lock temporarily released (OS screenshot) — the pill flips to
14
21
  * "Click to resume" and lets clicks fall through to the canvas. */
15
22
  suspended?: boolean
@@ -39,6 +46,7 @@ export function WalkthroughHud({
39
46
  floorLabel,
40
47
  zoneLabel,
41
48
  interact = null,
49
+ hints,
42
50
  suspended = false,
43
51
  onExit,
44
52
  children,
@@ -63,7 +71,7 @@ export function WalkthroughHud({
63
71
  )}
64
72
  {zoneLabel && (
65
73
  <div className="corner-smooth rounded-full border border-border/40 bg-background/80 px-3 py-1 font-medium text-sm shadow-elevation-3 backdrop-blur-xl">
66
- {zoneLabel}
74
+ <WalkthroughZoneName name={zoneLabel} />
67
75
  </div>
68
76
  )}
69
77
  {children}
@@ -83,6 +91,22 @@ export function WalkthroughHud({
83
91
  </div>
84
92
  ) : (
85
93
  <>
94
+ {hints && hints.length > 0 ? (
95
+ <div className={pillClass}>
96
+ {hints.map(({ keys, action }, index) => (
97
+ <span className="flex items-center gap-1.5" key={action}>
98
+ {index > 0 && <span className="text-muted-foreground/60">·</span>}
99
+ {keys.map((key, keyIndex) => (
100
+ <span className="flex items-center gap-1" key={key}>
101
+ {keyIndex > 0 && <span className="text-muted-foreground/60">/</span>}
102
+ <kbd className={kbdClass}>{key}</kbd>
103
+ </span>
104
+ ))}
105
+ {action}
106
+ </span>
107
+ ))}
108
+ </div>
109
+ ) : null}
86
110
  <div className={pillClass}>
87
111
  <kbd className={kbdClass}>P</kbd>
88
112
  free cursor
@@ -118,3 +142,52 @@ export function WalkthroughHud({
118
142
  </div>
119
143
  )
120
144
  }
145
+
146
+ /** The drone's key strip: the movement controls of the viewer's `DroneController`. */
147
+ function useDroneHints(): readonly WalkthroughHint[] {
148
+ const i18n = usePascalI18n()
149
+ return useMemo(
150
+ () => [
151
+ {
152
+ keys: ['WASD'],
153
+ action: i18n._({ id: 'editor.walkthroughHud.drone.move', message: 'move' }),
154
+ },
155
+ {
156
+ keys: ['Space', 'E'],
157
+ action: i18n._({ id: 'editor.walkthroughHud.drone.up', message: 'up' }),
158
+ },
159
+ {
160
+ keys: ['Q'],
161
+ action: i18n._({ id: 'editor.walkthroughHud.drone.down', message: 'down' }),
162
+ },
163
+ {
164
+ keys: ['Shift'],
165
+ action: i18n._({ id: 'editor.walkthroughHud.drone.boost', message: 'boost' }),
166
+ },
167
+ {
168
+ keys: ['Alt'],
169
+ action: i18n._({ id: 'editor.walkthroughHud.drone.slow', message: 'slow' }),
170
+ },
171
+ ],
172
+ [i18n],
173
+ )
174
+ }
175
+
176
+ /** The walkthrough HUD for the drone: the plain pointer dot and the flight keys, no room or door prompts. */
177
+ export function DroneHud({
178
+ suspended,
179
+ onExit,
180
+ children,
181
+ }: Pick<WalkthroughHudProps, 'suspended' | 'onExit' | 'children'>) {
182
+ return (
183
+ <WalkthroughHud hints={useDroneHints()} onExit={onExit} suspended={suspended}>
184
+ {children}
185
+ </WalkthroughHud>
186
+ )
187
+ }
188
+
189
+ /** The room the walker is in, in the UI language (the scene keeps its stored name). */
190
+ function WalkthroughZoneName({ name }: { name: string }) {
191
+ const i18n = usePascalI18n()
192
+ return displayRoomName(name, i18n)
193
+ }
@@ -18,7 +18,9 @@ import { useSidebarStore } from '../components/ui/primitives/sidebar'
18
18
  import { runRedo, runUndo } from '../lib/history'
19
19
  import { meshEditScope } from '../lib/interaction/scope'
20
20
  import { setSidebarTabIds } from '../lib/sidebar-panel'
21
- import useEditor from '../store/use-editor'
21
+ import { armToolShortcut, toolShortcutForKey, withToolShortcut } from '../lib/tool-shortcuts'
22
+ import { getWallDrawVariant, selectWallDrawVariant } from '../lib/wall-draw-variant'
23
+ import useEditor, { normalizePersistedEditorLayoutState } from '../store/use-editor'
22
24
  import useInteractionScope from '../store/use-interaction-scope'
23
25
  import {
24
26
  armPaintFromShortcut,
@@ -164,6 +166,14 @@ describe('rotation shortcut ownership', () => {
164
166
  expect(isToolOwnedRotation()).toBe(true)
165
167
  })
166
168
 
169
+ test('leaves R and T, and so Shift+R, to the stair and elevator placement tools', () => {
170
+ useEditor.getState().armToolMode({ mode: 'build', tool: 'stair' })
171
+ expect(isToolOwnedRotation()).toBe(true)
172
+
173
+ useEditor.getState().armToolMode({ mode: 'build', tool: 'elevator' })
174
+ expect(isToolOwnedRotation()).toBe(true)
175
+ })
176
+
167
177
  test('does not reserve R for the wall tool: its shape is picked in the Build panel', () => {
168
178
  useEditor.getState().armToolMode({ mode: 'build', tool: 'wall' })
169
179
 
@@ -418,20 +428,35 @@ describe('B and P open their panel', () => {
418
428
  expect(useSidebarStore.getState().isCollapsed).toBe(false)
419
429
  })
420
430
 
421
- test('B arms the wall tool on Rectangle and shows the Build panel', () => {
431
+ test('B arms the wall tool on the Rooms shape picked last and shows the Build panel', () => {
422
432
  setSidebarTabIds(['site', 'build', 'paint', 'items'])
423
433
  useEditor.getState().setActiveSidebarPanel('items')
424
434
  useSidebarStore.getState().setIsCollapsed(true)
425
- useEditor.getState().setContinuation('wall', 'single')
435
+ selectWallDrawVariant('walls')
436
+ useEditor.getState().armToolMode({ mode: 'select' })
426
437
 
427
438
  armWallToolFromShortcut()
428
439
 
429
440
  const editor = useEditor.getState()
430
441
  expect(editor.mode).toBe('build')
431
442
  expect(editor.tool).toBe('wall')
432
- expect(editor.getContinuation('wall')).toBe('rectangle')
443
+ expect(getWallDrawVariant()).toBe('walls')
433
444
  expect(editor.activeSidebarPanel).toBe('build')
434
445
  expect(useSidebarStore.getState().isCollapsed).toBe(false)
446
+ // The pick is a preference: it comes back with the saved editor layout.
447
+ expect(
448
+ normalizePersistedEditorLayoutState({ continuationByContext: { wall: 'single' } } as never)
449
+ .continuationByContext.wall,
450
+ ).toBe('single')
451
+ })
452
+
453
+ test('B draws a Rectangle until a Rooms shape is picked', () => {
454
+ expect(normalizePersistedEditorLayoutState({}).continuationByContext.wall).toBe('rectangle')
455
+ useEditor.setState({
456
+ continuationByContext: normalizePersistedEditorLayoutState({}).continuationByContext,
457
+ })
458
+ armWallToolFromShortcut()
459
+ expect(getWallDrawVariant()).toBe('rectangle')
435
460
  })
436
461
 
437
462
  // A host may drop the armed tool when the panel changes (the community does,
@@ -473,3 +498,59 @@ describe('B and P open their panel', () => {
473
498
  expect(useEditor.getState().activeSidebarPanel).toBe('build')
474
499
  })
475
500
  })
501
+
502
+ describe('drafting tool shortcuts', () => {
503
+ const press = (code: string, mods: Partial<KeyboardEvent> = {}) =>
504
+ toolShortcutForKey({
505
+ code,
506
+ shiftKey: false,
507
+ metaKey: false,
508
+ ctrlKey: false,
509
+ altKey: false,
510
+ repeat: false,
511
+ ...mods,
512
+ })
513
+
514
+ afterEach(() => {
515
+ setSidebarTabIds([])
516
+ useInteractionScope.getState().endIf(() => true)
517
+ useEditor.getState().setContinuation('wall', 'room')
518
+ useEditor.getState().armToolMode({ mode: 'select' })
519
+ })
520
+
521
+ test('keep the bare keys they would collide with: pan, rotate and operate', () => {
522
+ expect(press('KeyD', { shiftKey: true })).toBe('door')
523
+ expect(press('KeyW', { shiftKey: true })).toBe('window')
524
+ expect(press('KeyS', { shiftKey: true })).toBe('stair')
525
+ expect(press('KeyR', { shiftKey: true })).toBe('roof')
526
+ expect(press('KeyE', { shiftKey: true })).toBeNull()
527
+ for (const code of ['KeyD', 'KeyW', 'KeyS', 'KeyR', 'KeyE', 'KeyI', 'KeyA'])
528
+ expect(press(code)).toBeNull()
529
+ expect(press('KeyD', { shiftKey: true, ctrlKey: true })).toBeNull()
530
+ expect(press('KeyD', { shiftKey: true, repeat: true })).toBeNull()
531
+ })
532
+
533
+ test('stand down while a gesture holds Shift', () => {
534
+ useInteractionScope.getState().begin({ kind: 'box-select' })
535
+ expect(press('KeyD', { shiftKey: true })).toBeNull()
536
+ })
537
+
538
+ test('arm their tool with the Build panel showing, and name their key in tooltips', () => {
539
+ setSidebarTabIds(['build', 'items'])
540
+ useEditor.getState().setActiveSidebarPanel('items')
541
+ for (const [id, tool] of [
542
+ ['door', 'door'],
543
+ ['window', 'window'],
544
+ ['stair', 'stair'],
545
+ ['roof', 'roof'],
546
+ ] as const) {
547
+ armToolShortcut(id)
548
+ expect(useEditor.getState().mode).toBe('build')
549
+ expect(useEditor.getState().tool).toBe(tool)
550
+ expect(useEditor.getState().activeSidebarPanel).toBe('build')
551
+ }
552
+ expect(withToolShortcut('Door', 'door')).toBe('Door (Shift+D)')
553
+ expect(withToolShortcut('Wall', 'wall')).toBe('Wall (B)')
554
+ expect(withToolShortcut('Fence', 'fence')).toBe('Fence')
555
+ })
556
+ })