@pascal-app/editor 1.0.6 → 1.1.0

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 (382) hide show
  1. package/package.json +18 -15
  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 +110 -42
  6. package/src/components/editor/duplicate-node.ts +2 -1
  7. package/src/components/editor/elevation-3d-guide-layer.tsx +1 -1
  8. package/src/components/editor/first-person-controls.tsx +77 -68
  9. package/src/components/editor/fit-scene-framing.test.ts +318 -0
  10. package/src/components/editor/fit-scene-framing.ts +142 -0
  11. package/src/components/editor/floating-action-menu.tsx +31 -11
  12. package/src/components/editor/floating-building-action-menu.tsx +1 -1
  13. package/src/components/editor/floor-openings-overlay.tsx +1 -1
  14. package/src/components/editor/floor-region-controls.tsx +2 -2
  15. package/src/components/editor/floorplan-panel.test.ts +220 -0
  16. package/src/components/editor/floorplan-panel.tsx +458 -140
  17. package/src/components/editor/floorplan-wall-typing-keys.test.tsx +152 -0
  18. package/src/components/editor/floorplan-wall-typing-keys.ts +79 -0
  19. package/src/components/editor/footprint-height-handle.tsx +2 -2
  20. package/src/components/editor/grid.tsx +24 -15
  21. package/src/components/editor/group-actions.ts +19 -1
  22. package/src/components/editor/group-floating-action-menu.tsx +1 -1
  23. package/src/components/editor/group-move-3d.ts +27 -2
  24. package/src/components/editor/group-selection-box-3d.tsx +4 -1
  25. package/src/components/editor/group-transform-shared.ts +23 -1
  26. package/src/components/editor/index.tsx +150 -20
  27. package/src/components/editor/inspector-extension-actions.tsx +36 -0
  28. package/src/components/editor/localized-html.tsx +22 -0
  29. package/src/components/editor/measurement-pill.tsx +1 -1
  30. package/src/components/editor/node-action-menu.tsx +46 -12
  31. package/src/components/editor/node-arrow-handles.tsx +2 -3
  32. package/src/components/editor/open-wall-ends-3d-layer.tsx +2 -3
  33. package/src/components/editor/open-wall-ends-hint.tsx +1 -1
  34. package/src/components/editor/opening-guides-3d-layer.tsx +1 -1
  35. package/src/components/editor/quick-measurement-card.tsx +1 -1
  36. package/src/components/editor/registry-action-contributions.test.tsx +67 -6
  37. package/src/components/editor/registry-action-contributions.tsx +115 -15
  38. package/src/components/editor/riser-diagram-panel.tsx +59 -9
  39. package/src/components/editor/room-controls.tsx +4 -3
  40. package/src/components/editor/room-handles-2d.tsx +158 -7
  41. package/src/components/editor/room-handles.tsx +244 -54
  42. package/src/components/editor/room-highlight.tsx +2 -0
  43. package/src/components/editor/room-labels.tsx +33 -17
  44. package/src/components/editor/selection-manager.tsx +15 -102
  45. package/src/components/editor/separator-end-grip.tsx +74 -0
  46. package/src/components/editor/site-edge-labels.tsx +7 -3
  47. package/src/components/editor/thumbnail-generator.test.ts +15 -6
  48. package/src/components/editor/thumbnail-generator.tsx +26 -1
  49. package/src/components/editor/use-floorplan-background-placement.ts +55 -52
  50. package/src/components/editor/views/view-bar.tsx +111 -26
  51. package/src/components/editor/wall-measurement-label.tsx +31 -15
  52. package/src/components/editor/wall-move-side-handles.tsx +15 -7
  53. package/src/components/editor/wall-pull-grip.tsx +193 -0
  54. package/src/components/editor/wall-region-handles.tsx +2 -2
  55. package/src/components/editor/wall-snap-beacon-layer.tsx +134 -33
  56. package/src/components/editor-2d/floorplan-adjacent-walls-layer.test.ts +76 -0
  57. package/src/components/editor-2d/floorplan-adjacent-walls-layer.tsx +186 -0
  58. package/src/components/editor-2d/floorplan-alignment-guide-layer.tsx +63 -3
  59. package/src/components/editor-2d/floorplan-group-move.tsx +32 -4
  60. package/src/components/editor-2d/floorplan-measurement-tool-layer.test.ts +6 -0
  61. package/src/components/editor-2d/floorplan-measurement-tool-layer.tsx +33 -24
  62. package/src/components/editor-2d/floorplan-open-wall-ends-layer.tsx +12 -5
  63. package/src/components/editor-2d/floorplan-registry-action-menu.tsx +2 -1
  64. package/src/components/editor-2d/floorplan-registry-move-overlay.tsx +29 -5
  65. package/src/components/editor-2d/floorplan-snap-beacon-layer.tsx +96 -4
  66. package/src/components/editor-2d/renderers/floorplan-dimension-edit-overlay.tsx +52 -12
  67. package/src/components/editor-2d/renderers/floorplan-draft-measurement.tsx +7 -1
  68. package/src/components/editor-2d/renderers/floorplan-placement-preview-layer.tsx +13 -1
  69. package/src/components/editor-2d/renderers/floorplan-placement-typing-hud.test.tsx +35 -0
  70. package/src/components/editor-2d/renderers/floorplan-placement-typing-hud.tsx +65 -0
  71. package/src/components/editor-2d/renderers/floorplan-registry-layer.test.ts +34 -1
  72. package/src/components/editor-2d/renderers/floorplan-registry-layer.tsx +211 -39
  73. package/src/components/procedural-items/inspector.tsx +59 -12
  74. package/src/components/systems/ceiling/ceiling-bracket-batch.test.ts +2 -0
  75. package/src/components/systems/ceiling/ceiling-system.test.ts +141 -0
  76. package/src/components/systems/ceiling/ceiling-system.tsx +53 -18
  77. package/src/components/systems/roof/roof-edit-system.test.tsx +15 -1
  78. package/src/components/systems/roof/roof-edit-system.tsx +30 -19
  79. package/src/components/tools/distribution/distribution-run-tool.test.tsx +371 -3
  80. package/src/components/tools/distribution/distribution-run-tool.tsx +200 -58
  81. package/src/components/tools/item/move-tool.tsx +1 -6
  82. package/src/components/tools/item/placement-history.test.tsx +1 -1
  83. package/src/components/tools/item/placement-strategies.test.ts +64 -1
  84. package/src/components/tools/item/placement-strategies.ts +5 -9
  85. package/src/components/tools/item/use-placement-coordinator.tsx +31 -35
  86. package/src/components/tools/paint-region/wall-paint-region-tool.tsx +1 -1
  87. package/src/components/tools/registry/item-surface-move.test.ts +11 -0
  88. package/src/components/tools/registry/move-registry-node-tool.test.ts +42 -1
  89. package/src/components/tools/registry/move-registry-node-tool.tsx +38 -14
  90. package/src/components/tools/select/select-candidates.test.ts +45 -0
  91. package/src/components/tools/select/select-candidates.ts +13 -2
  92. package/src/components/tools/shared/cursor-sphere.tsx +10 -3
  93. package/src/components/tools/shared/draft-measurement-label.tsx +1 -1
  94. package/src/components/tools/shared/placement-box.tsx +2 -6
  95. package/src/components/tools/shared/placement-coordinate-input.tsx +94 -0
  96. package/src/components/tools/shared/placement-dimension-guides.tsx +1 -1
  97. package/src/components/tools/shared/segment-angle.ts +0 -4
  98. package/src/components/tools/site/terrain-brush-cursor.tsx +1 -1
  99. package/src/components/tools/tool-manager.tsx +1 -24
  100. package/src/components/tools/wall/wall-drafting.test.ts +299 -23
  101. package/src/components/tools/wall/wall-drafting.ts +132 -24
  102. package/src/components/tools/wall/wall-pull-snap.test.ts +274 -0
  103. package/src/components/tools/wall/wall-pull-snap.ts +563 -0
  104. package/src/components/tools/wall/wall-snap-geometry.test.ts +113 -1
  105. package/src/components/tools/wall/wall-snap-geometry.ts +137 -0
  106. package/src/components/tools/wall/wall-typed-entry.test.ts +99 -0
  107. package/src/components/tools/wall/wall-typed-entry.ts +70 -0
  108. package/src/components/tools/zone/zone-tool.tsx +3 -3
  109. package/src/components/ui/action-menu/action-button.tsx +2 -0
  110. package/src/components/ui/action-menu/camera-actions.tsx +8 -7
  111. package/src/components/ui/action-menu/control-modes.tsx +9 -5
  112. package/src/components/ui/action-menu/follow-pascal-pill.tsx +3 -1
  113. package/src/components/ui/action-menu/follow-pascal-toggle.tsx +7 -1
  114. package/src/components/ui/action-menu/furnish-tools.tsx +7 -6
  115. package/src/components/ui/action-menu/index.tsx +1 -0
  116. package/src/components/ui/action-menu/measurement-control.tsx +35 -29
  117. package/src/components/ui/action-menu/point-ask-button.tsx +4 -3
  118. package/src/components/ui/action-menu/structure-tools.tsx +25 -23
  119. package/src/components/ui/action-menu/view-toggles.test.ts +18 -0
  120. package/src/components/ui/action-menu/view-toggles.tsx +256 -51
  121. package/src/components/ui/build-panel/build-panel.tsx +163 -25
  122. package/src/components/ui/command-palette/editor-commands.tsx +38 -11
  123. package/src/components/ui/controls/finish-swatch.tsx +45 -19
  124. package/src/components/ui/controls/material-picker.tsx +24 -17
  125. package/src/components/ui/controls/metric-control.test.ts +52 -0
  126. package/src/components/ui/controls/metric-control.tsx +62 -24
  127. package/src/components/ui/controls/segmented-control.tsx +5 -2
  128. package/src/components/ui/controls/slider-control.test.tsx +23 -0
  129. package/src/components/ui/controls/slider-control.tsx +47 -19
  130. package/src/components/ui/controls/tool-options-panel.tsx +74 -44
  131. package/src/components/ui/floating-level-selector.tsx +100 -48
  132. package/src/components/ui/helpers/building-helper.tsx +5 -3
  133. package/src/components/ui/helpers/contextual-helper-panel.tsx +97 -37
  134. package/src/components/ui/helpers/helper-manager.test.tsx +106 -3
  135. package/src/components/ui/helpers/helper-manager.tsx +136 -57
  136. package/src/components/ui/helpers/item-helper.tsx +6 -4
  137. package/src/components/ui/helpers/registered-tool-helper.tsx +3 -1
  138. package/src/components/ui/level-duplicate-dialog.tsx +32 -23
  139. package/src/components/ui/level-wall-copy-prompt.tsx +124 -0
  140. package/src/components/ui/panels/collections/collections-popover.tsx +11 -11
  141. package/src/components/ui/panels/deck-boards-fields.tsx +136 -0
  142. package/src/components/ui/panels/floor-foundation-panel.tsx +114 -39
  143. package/src/components/ui/panels/floor-opening-panel.tsx +74 -26
  144. package/src/components/ui/panels/inspector-extensions.tsx +66 -0
  145. package/src/components/ui/panels/mobile-panel-sheet.tsx +3 -1
  146. package/src/components/ui/panels/mobile-selection-bar.tsx +34 -28
  147. package/src/components/ui/panels/multi-field-value.test.ts +27 -0
  148. package/src/components/ui/panels/multi-field-value.ts +20 -1
  149. package/src/components/ui/panels/multi-height-mode.tsx +11 -10
  150. package/src/components/ui/panels/multi-parametric-inspector.tsx +122 -34
  151. package/src/components/ui/panels/multi-selection-panel.tsx +76 -36
  152. package/src/components/ui/panels/node-display.ts +35 -24
  153. package/src/components/ui/panels/panel-manager.tsx +9 -5
  154. package/src/components/ui/panels/panel-wrapper.tsx +63 -127
  155. package/src/components/ui/panels/parametric-field-control.tsx +13 -10
  156. package/src/components/ui/panels/parametric-inspector.tsx +18 -12
  157. package/src/components/ui/panels/reference-panel.tsx +51 -35
  158. package/src/components/ui/panels/room-construction-rows.tsx +465 -66
  159. package/src/components/ui/panels/room-name-options.test.ts +36 -3
  160. package/src/components/ui/panels/room-name-options.ts +110 -11
  161. package/src/components/ui/panels/room-openings-field.tsx +22 -13
  162. package/src/components/ui/panels/room-panel.tsx +207 -100
  163. package/src/components/ui/panels/shape-choice.tsx +7 -5
  164. package/src/components/ui/panels/wall-region-list.tsx +7 -7
  165. package/src/components/ui/primitives/shortcut-token.tsx +10 -6
  166. package/src/components/ui/right-stack.test.ts +13 -6
  167. package/src/components/ui/right-stack.tsx +21 -10
  168. package/src/components/ui/sidebar/icon-rail.tsx +11 -9
  169. package/src/components/ui/sidebar/panels/items-panel/function-tree-panel.tsx +10 -10
  170. package/src/components/ui/sidebar/panels/items-panel/index.tsx +10 -9
  171. package/src/components/ui/sidebar/panels/settings-panel/index.tsx +3 -0
  172. package/src/components/ui/sidebar/panels/settings-panel/keyboard-shortcuts-dialog.tsx +102 -32
  173. package/src/components/ui/sidebar/panels/settings-panel/load-build-dialog.tsx +1 -5
  174. package/src/components/ui/sidebar/panels/site-panel/building-tree-node.tsx +4 -3
  175. package/src/components/ui/sidebar/panels/site-panel/ceiling-tree-node.tsx +5 -3
  176. package/src/components/ui/sidebar/panels/site-panel/chimney-tree-node.tsx +4 -2
  177. package/src/components/ui/sidebar/panels/site-panel/collection-tree-node.tsx +7 -5
  178. package/src/components/ui/sidebar/panels/site-panel/column-tree-node.tsx +4 -2
  179. package/src/components/ui/sidebar/panels/site-panel/default-names.ts +75 -0
  180. package/src/components/ui/sidebar/panels/site-panel/door-tree-node.tsx +4 -2
  181. package/src/components/ui/sidebar/panels/site-panel/dormer-tree-node.tsx +4 -2
  182. package/src/components/ui/sidebar/panels/site-panel/elevator-tree-node.tsx +4 -2
  183. package/src/components/ui/sidebar/panels/site-panel/fence-tree-node.tsx +4 -2
  184. package/src/components/ui/sidebar/panels/site-panel/gutter-tree-node.tsx +4 -2
  185. package/src/components/ui/sidebar/panels/site-panel/index.tsx +118 -72
  186. package/src/components/ui/sidebar/panels/site-panel/inline-rename-input.tsx +36 -3
  187. package/src/components/ui/sidebar/panels/site-panel/item-tree-node.tsx +4 -2
  188. package/src/components/ui/sidebar/panels/site-panel/level-tree-node.tsx +7 -4
  189. package/src/components/ui/sidebar/panels/site-panel/registry-tree-node.tsx +8 -2
  190. package/src/components/ui/sidebar/panels/site-panel/roof-tree-node.tsx +8 -5
  191. package/src/components/ui/sidebar/panels/site-panel/shelf-tree-node.tsx +4 -2
  192. package/src/components/ui/sidebar/panels/site-panel/slab-tree-node.tsx +9 -4
  193. package/src/components/ui/sidebar/panels/site-panel/solar-panel-tree-node.tsx +4 -2
  194. package/src/components/ui/sidebar/panels/site-panel/spawn-tree-node.tsx +4 -2
  195. package/src/components/ui/sidebar/panels/site-panel/stair-tree-node.tsx +8 -7
  196. package/src/components/ui/sidebar/panels/site-panel/tree-node-actions.tsx +7 -6
  197. package/src/components/ui/sidebar/panels/site-panel/tree-node.tsx +16 -1
  198. package/src/components/ui/sidebar/panels/site-panel/unit-tree-node.tsx +15 -8
  199. package/src/components/ui/sidebar/panels/site-panel/wall-tree-node.tsx +4 -2
  200. package/src/components/ui/sidebar/panels/site-panel/window-tree-node.tsx +4 -2
  201. package/src/components/ui/sidebar/panels/site-panel/zone-membership-checkbox.tsx +3 -1
  202. package/src/components/ui/sidebar/panels/site-panel/zone-tree-node.tsx +19 -9
  203. package/src/components/ui/sidebar/panels/zone-panel/index.tsx +25 -14
  204. package/src/components/ui/sidebar/tab-bar.tsx +4 -1
  205. package/src/components/ui/sidebar/use-plugin-panels.tsx +19 -10
  206. package/src/components/ui/snap-target-badge.tsx +7 -6
  207. package/src/components/ui/wall-type/wall-type-editor-dialog.tsx +721 -0
  208. package/src/components/ui/wall-type/wall-type-menu.tsx +167 -0
  209. package/src/components/viewer/floorplan-compass-button.tsx +12 -3
  210. package/src/components/viewer/floorplan-preview.tsx +27 -19
  211. package/src/components/viewer/render-settings-menu.tsx +294 -0
  212. package/src/components/viewer/sky-control.tsx +710 -0
  213. package/src/components/viewer/use-viewer-ui-sounds.test.ts +15 -0
  214. package/src/components/viewer/use-viewer-ui-sounds.ts +46 -0
  215. package/src/components/viewer/viewer-controls-bar.tsx +278 -260
  216. package/src/components/viewer/viewer-hover-label.tsx +12 -4
  217. package/src/components/viewer/viewer-rooms.tsx +62 -5
  218. package/src/components/viewer/viewer-scene-header.tsx +28 -17
  219. package/src/components/viewer/viewer-stage-switcher.tsx +4 -2
  220. package/src/components/viewer-overlay.tsx +8 -0
  221. package/src/components/walkthrough-hud.tsx +75 -2
  222. package/src/hooks/use-keyboard.test.ts +85 -4
  223. package/src/hooks/use-keyboard.ts +65 -7
  224. package/src/hooks/use-selected-room.ts +12 -1
  225. package/src/i18n/floor-footprint-name.ts +30 -0
  226. package/src/i18n/level-name.ts +39 -0
  227. package/src/i18n/locales/en/messages.po +15849 -0
  228. package/src/i18n/locales/en/messages.ts +1 -0
  229. package/src/i18n/locales/en-XA/messages.ts +1 -0
  230. package/src/i18n/locales/es/messages.po +39331 -0
  231. package/src/i18n/locales/es/messages.ts +1 -0
  232. package/src/i18n/locales/fr/messages.po +39331 -0
  233. package/src/i18n/locales/fr/messages.ts +1 -0
  234. package/src/i18n/locales/ja/messages.po +39331 -0
  235. package/src/i18n/locales/ja/messages.ts +1 -0
  236. package/src/i18n/locales/zh-CN/messages.po +39331 -0
  237. package/src/i18n/locales/zh-CN/messages.ts +1 -0
  238. package/src/i18n/locales.test.ts +63 -0
  239. package/src/i18n/locales.ts +32 -0
  240. package/src/i18n/message-slot.tsx +25 -0
  241. package/src/i18n/node-messages.ts +169 -0
  242. package/src/i18n/pseudo-locale.ts +17 -0
  243. package/src/i18n/published-entry.test.ts +17 -0
  244. package/src/i18n/scene-theme-name.ts +16 -0
  245. package/src/index.tsx +224 -45
  246. package/src/lib/add-storey.test.ts +3 -3
  247. package/src/lib/contextual-help.test.ts +20 -1
  248. package/src/lib/contextual-help.ts +101 -19
  249. package/src/lib/continuation.ts +38 -11
  250. package/src/lib/elevation-guides.ts +21 -6
  251. package/src/lib/elevator-support.ts +1 -72
  252. package/src/lib/floor-footprints.test.ts +3 -2
  253. package/src/lib/floor-footprints.ts +1 -9
  254. package/src/lib/floor-foundation-support.test.ts +15 -6
  255. package/src/lib/floor-opening-draft.ts +1 -1
  256. package/src/lib/floor-region-session.ts +1 -1
  257. package/src/lib/floorplan/dimension-drive.test.ts +36 -2
  258. package/src/lib/floorplan/dimension-drive.ts +68 -3
  259. package/src/lib/floorplan/floorplan-export.test.ts +16 -5
  260. package/src/lib/floorplan/floorplan-export.tsx +13 -16
  261. package/src/lib/floorplan/floorplan-extension.test.ts +11 -12
  262. package/src/lib/floorplan/floorplan-extension.ts +37 -18
  263. package/src/lib/floorplan/floorplan-mode.test.ts +13 -6
  264. package/src/lib/floorplan/floorplan-mode.ts +14 -12
  265. package/src/lib/floorplan/floorplan-plugin-install.test.tsx +1 -1
  266. package/src/lib/floorplan/floorplan-readonly.ts +3 -3
  267. package/src/lib/floorplan/open-wall-ends.ts +2 -1
  268. package/src/lib/floorplan/schedules.ts +10 -6
  269. package/src/lib/floorplan/site-plan/site-annotations.test.ts +46 -1
  270. package/src/lib/floorplan/site-plan/site-annotations.ts +3 -0
  271. package/src/lib/floorplan/site-plan/site-parts.ts +6 -3
  272. package/src/lib/geometry-script/author.test.ts +114 -0
  273. package/src/lib/geometry-script/author.ts +55 -23
  274. package/src/lib/geometry-script/client.ts +3 -0
  275. package/src/lib/geometry-script/finish-scripts.ts +7 -7
  276. package/src/lib/gesture-lifecycle.ts +3 -1
  277. package/src/lib/glb-export.test.ts +327 -0
  278. package/src/lib/glb-export.ts +70 -2
  279. package/src/lib/grid-frame.test.ts +56 -3
  280. package/src/lib/grid-frame.ts +28 -1
  281. package/src/lib/history.test.ts +2 -1
  282. package/src/lib/hud-title.ts +48 -24
  283. package/src/lib/ifc-export.test.ts +41 -1
  284. package/src/lib/inspector-card-mode.test.ts +3 -31
  285. package/src/lib/inspector-card-mode.ts +5 -10
  286. package/src/lib/inspector-expanded.test.ts +13 -0
  287. package/src/lib/inspector-expanded.ts +13 -1
  288. package/src/lib/interaction/scope.ts +1 -0
  289. package/src/lib/keyboard-pan.test.ts +7 -0
  290. package/src/lib/keyboard-pan.ts +9 -2
  291. package/src/lib/layer-thickness.test.ts +22 -0
  292. package/src/lib/layer-thickness.ts +79 -0
  293. package/src/lib/level-selection.test.ts +18 -2
  294. package/src/lib/level-selection.ts +31 -8
  295. package/src/lib/linear-display.ts +1 -1
  296. package/src/lib/live-gesture-round-trip.test.ts +1 -1
  297. package/src/lib/lot/drop-in.test.ts +59 -1
  298. package/src/lib/lot/drop-in.ts +42 -50
  299. package/src/lib/material-swatch-style.ts +24 -0
  300. package/src/lib/measurement-label.test.ts +1 -1
  301. package/src/lib/measurement-parser.ts +7 -3
  302. package/src/lib/measurements.test.ts +24 -6
  303. package/src/lib/measurements.ts +1 -203
  304. package/src/lib/mezzanine-selection.test.ts +3 -1
  305. package/src/lib/node-action-movement.test.tsx +115 -0
  306. package/src/lib/node-action-movement.ts +2 -0
  307. package/src/lib/orbit-near-plane.test.ts +12 -0
  308. package/src/lib/orbit-near-plane.ts +13 -0
  309. package/src/lib/paint-regions.test.ts +8 -5
  310. package/src/lib/paint-scope.test.ts +53 -0
  311. package/src/lib/paint-scope.ts +18 -4
  312. package/src/lib/panel-rows.ts +17 -8
  313. package/src/lib/placement-metadata.ts +4 -0
  314. package/src/lib/point-ask/describe-target.ts +6 -8
  315. package/src/lib/point-ask/region.ts +1 -1
  316. package/src/lib/print-shell-compiler.ts +12 -1
  317. package/src/lib/print-wall-solids.test.ts +11 -0
  318. package/src/lib/print-wall-solids.ts +9 -0
  319. package/src/lib/reference-scale.ts +83 -0
  320. package/src/lib/room-assembly-overlay.ts +1 -1
  321. package/src/lib/room-built-on.test.ts +28 -17
  322. package/src/lib/room-built-on.ts +21 -17
  323. package/src/lib/room-construction-commands.ts +3 -2
  324. package/src/lib/room-construction.test.ts +28 -1
  325. package/src/lib/room-construction.ts +14 -0
  326. package/src/lib/room-controls-phase6.test.ts +141 -1
  327. package/src/lib/room-handle-drag.ts +554 -58
  328. package/src/lib/room-name-catalog.test.ts +8 -0
  329. package/src/lib/room-name-catalog.ts +8 -1
  330. package/src/lib/room-name-messages.test.ts +132 -0
  331. package/src/lib/room-name-messages.ts +147 -0
  332. package/src/lib/room-name-storage.test.ts +334 -0
  333. package/src/lib/room-push-dimensions.test.ts +38 -1
  334. package/src/lib/room-push-dimensions.ts +226 -2
  335. package/src/lib/room-selection.test.ts +23 -6
  336. package/src/lib/scene-bounds.test.ts +144 -0
  337. package/src/lib/scene-bounds.ts +79 -15
  338. package/src/lib/scene-clipboard.test.ts +70 -49
  339. package/src/lib/scene-clipboard.ts +11 -5
  340. package/src/lib/selection-cycle.ts +63 -0
  341. package/src/lib/separator-edit.test.ts +431 -0
  342. package/src/lib/separator-edit.ts +179 -0
  343. package/src/lib/separator-lines.ts +38 -0
  344. package/src/lib/sfx-bus.ts +27 -1
  345. package/src/lib/sfx-player.ts +16 -0
  346. package/src/lib/snapping-mode.test.ts +23 -1
  347. package/src/lib/tool-cursor-icon.ts +5 -1
  348. package/src/lib/tool-shortcuts.ts +130 -0
  349. package/src/lib/viewer-selection.test.ts +28 -24
  350. package/src/lib/viewer-selection.ts +21 -6
  351. package/src/lib/wall-draw-variant.test.ts +7 -6
  352. package/src/lib/wall-draw-variant.ts +107 -6
  353. package/src/lib/wall-nudge.test.ts +44 -0
  354. package/src/lib/wall-nudge.ts +131 -0
  355. package/src/lib/wall-parallel-snap.test.ts +83 -0
  356. package/src/lib/wall-parallel-snap.ts +176 -0
  357. package/src/lib/wall-placement-pointer.test.ts +23 -0
  358. package/src/lib/wall-placement-pointer.ts +18 -0
  359. package/src/lib/wall-pull.test.ts +245 -0
  360. package/src/lib/wall-pull.ts +209 -0
  361. package/src/lib/wall-push-line-up.test.ts +132 -0
  362. package/src/lib/wall-region-list.ts +17 -6
  363. package/src/lib/wall-run-snap.test.ts +188 -0
  364. package/src/lib/wall-run-snap.ts +83 -0
  365. package/src/lib/wall-type.test.ts +125 -0
  366. package/src/lib/wall-type.ts +119 -0
  367. package/src/lib/window-interaction.test.ts +44 -0
  368. package/src/lib/world-grid-snap.test.ts +139 -2
  369. package/src/lib/world-grid-snap.ts +102 -105
  370. package/src/store/use-alignment-guides.ts +27 -1
  371. package/src/store/use-editor.tsx +53 -0
  372. package/src/store/use-floorplan-annotation-visibility.test.ts +64 -0
  373. package/src/store/use-floorplan-annotation-visibility.ts +47 -22
  374. package/src/store/use-placement-typing.test.ts +165 -0
  375. package/src/store/use-placement-typing.ts +186 -0
  376. package/src/store/use-wall-snap-indicator.ts +15 -2
  377. package/src/components/tools/elevator/elevator-defaults.ts +0 -8
  378. package/src/components/tools/elevator/elevator-tool.tsx +0 -372
  379. package/src/components/tools/elevator/move-elevator-tool.tsx +0 -262
  380. package/src/lib/door-interaction.ts +0 -102
  381. package/src/lib/measurement-label.ts +0 -68
  382. package/src/lib/window-interaction.ts +0 -100
@@ -1,12 +1,14 @@
1
+ import type { MessageDescriptor } from '@pascal-app/core'
2
+
1
3
  export type ContextualShortcutHint = {
2
4
  // A combo of keys pressed together (rendered joined by "+"). An entry may
3
5
  // itself be an array of alternatives (rendered joined by "/"), e.g.
4
6
  // [['Cmd/Ctrl', 'Shift'], 'Left click'] → "⌘ / ⇧ + click".
5
7
  keys: Array<string | string[]>
6
- label: string
8
+ label: MessageDescriptor | string
7
9
  // Optional secondary line under the label for a terser qualifier
8
10
  // (e.g. "disable 15° snap"). The HUD wraps both lines rather than truncating.
9
- subtitle?: string
11
+ subtitle?: MessageDescriptor | string
10
12
  active?: boolean
11
13
  }
12
14
 
@@ -29,11 +31,25 @@ export const RESIZE_HANDLE_DRAG_LABEL = 'resize-handle'
29
31
  export const GROUP_MOVE_DRAG_LABEL = 'group-move-handle'
30
32
  export const GROUP_ROTATE_DRAG_LABEL = 'group-rotate-handle'
31
33
 
34
+ // `activeHandleDrag.label`s for the MEP handles, in 3D and on the plan: one
35
+ // point of a run, where Alt detaches the joint, and a fitting, a run or one
36
+ // run segment slid along an axis. Their snap context is the kind's own.
37
+ export const PATH_POINT_DRAG_LABEL = 'path-point'
38
+ export const AXIS_MOVE_DRAG_LABEL = 'axis-move'
39
+
32
40
  export function resolveRotateHandleHelpHints(altPressed: boolean): ContextualShortcutHint[] {
33
41
  return [
34
42
  {
35
43
  keys: [ALT_KEY],
36
- label: altPressed ? 'Rotating freely (no angle step)' : 'Hold to rotate freely',
44
+ label: altPressed
45
+ ? /* i18n */ {
46
+ id: 'editor.lib.contextualHelp.rotatingFreely',
47
+ message: 'Rotating freely (no angle step)',
48
+ }
49
+ : /* i18n */ {
50
+ id: 'editor.lib.contextualHelp.holdToRotateFreely',
51
+ message: 'Hold to rotate freely',
52
+ },
37
53
  active: altPressed,
38
54
  },
39
55
  ]
@@ -77,7 +93,10 @@ export function resolveSelectModeHelpHints({
77
93
 
78
94
  hints.push({
79
95
  keys: [[COMMAND_KEY, SHIFT_KEY], LEFT_CLICK],
80
- label: 'Add or remove objects from the selection',
96
+ label: /* i18n */ {
97
+ id: 'editor.lib.contextualHelp.addOrRemoveObjectsFromThe',
98
+ message: 'Add or remove objects from the selection',
99
+ },
81
100
  active: true,
82
101
  })
83
102
  return hints
@@ -89,23 +108,47 @@ export function resolveSelectModeHelpHints({
89
108
  if (selectedCount > 1) {
90
109
  hints.push({
91
110
  keys: [LEFT_CLICK],
92
- label: 'Click or drag the selection to move it as one',
111
+ label: /* i18n */ {
112
+ id: 'editor.lib.contextualHelp.clickOrDragTheSelectionTo',
113
+ message: 'Click or drag the selection to move it as one',
114
+ },
115
+ })
116
+ hints.push({
117
+ keys: [ROTATE_KEYS],
118
+ label: /* i18n */ {
119
+ id: 'editor.lib.contextualHelp.rotateTheSelection45',
120
+ message: 'Rotate the selection ±45°',
121
+ },
93
122
  })
94
- hints.push({ keys: [ROTATE_KEYS], label: 'Rotate the selection ±45°' })
95
123
  hints.push({
96
124
  keys: [COMMAND_KEY, 'G'],
97
- label: 'Group selection (session only)',
125
+ label: /* i18n */ {
126
+ id: 'editor.lib.contextualHelp.groupSelectionSessionOnly',
127
+ message: 'Group selection (session only)',
128
+ },
98
129
  })
99
130
  hints.push({
100
131
  keys: [COMMAND_KEY, SHIFT_KEY, 'G'],
101
- label: 'Ungroup session selection',
132
+ label: /* i18n */ {
133
+ id: 'editor.lib.contextualHelp.ungroupSessionSelection',
134
+ message: 'Ungroup session selection',
135
+ },
102
136
  })
103
137
  hints.push({
104
138
  keys: [[COMMAND_KEY, SHIFT_KEY], LEFT_CLICK],
105
- label: 'Add or remove objects from the selection',
139
+ label: /* i18n */ {
140
+ id: 'editor.lib.contextualHelp.addOrRemoveObjectsFromThe',
141
+ message: 'Add or remove objects from the selection',
142
+ },
106
143
  active: commandPressed || shiftPressed,
107
144
  })
108
- hints.push({ keys: [ESC_KEY], label: 'Clear the selection (or click outside)' })
145
+ hints.push({
146
+ keys: [ESC_KEY],
147
+ label: /* i18n */ {
148
+ id: 'editor.lib.contextualHelp.clearTheSelectionOrClickOutside',
149
+ message: 'Clear the selection (or click outside)',
150
+ },
151
+ })
109
152
  return hints
110
153
  }
111
154
 
@@ -115,12 +158,39 @@ export function resolveSelectModeHelpHints({
115
158
  // detaches the joint mid-drag; a fitting's cluster adds rotate arcs, with
116
159
  // R / T (and Alt to switch axis) for keyboard rotation.
117
160
  if (mepSelection === 'run') {
118
- hints.push({ keys: [CLICK], label: 'Click a handle dot to show move arrows' })
119
- hints.push({ keys: [ALT_KEY], label: 'Detach the joint while dragging an arrow' })
161
+ hints.push({
162
+ keys: [CLICK],
163
+ label: /* i18n */ {
164
+ id: 'editor.lib.contextualHelp.clickAHandleDotToShow',
165
+ message: 'Click a handle dot to show move arrows',
166
+ },
167
+ })
168
+ hints.push({
169
+ keys: [ALT_KEY],
170
+ label: /* i18n */ {
171
+ id: 'editor.lib.contextualHelp.detachTheJointWhileDraggingAn',
172
+ message: 'Detach the joint while dragging an arrow',
173
+ },
174
+ })
120
175
  } else if (mepSelection === 'fitting') {
121
- hints.push({ keys: [CLICK], label: 'Click the handle dot to show move + rotate handles' })
122
- hints.push({ keys: [ROTATE_KEYS], label: 'Rotate ±45°' })
123
- hints.push({ keys: [ALT_KEY], label: 'Switch the rotation axis (Y → X → Z)' })
176
+ hints.push({
177
+ keys: [CLICK],
178
+ label: /* i18n */ {
179
+ id: 'editor.lib.contextualHelp.clickTheHandleDotToShow',
180
+ message: 'Click the handle dot to show move + rotate handles',
181
+ },
182
+ })
183
+ hints.push({
184
+ keys: [ROTATE_KEYS],
185
+ label: /* i18n */ { id: 'editor.lib.contextualHelp.rotate45', message: 'Rotate ±45°' },
186
+ })
187
+ hints.push({
188
+ keys: [ALT_KEY],
189
+ label: /* i18n */ {
190
+ id: 'editor.lib.contextualHelp.switchTheRotationAxisYX',
191
+ message: 'Switch the rotation axis (Y → X → Z)',
192
+ },
193
+ })
124
194
  }
125
195
 
126
196
  // The rows are the same whatever modifier is held — guides/snapping are
@@ -130,28 +200,40 @@ export function resolveSelectModeHelpHints({
130
200
  if (hasMovableSelection) {
131
201
  hints.push({
132
202
  keys: [LEFT_CLICK],
133
- label: 'Drag selected movable object',
203
+ label: /* i18n */ {
204
+ id: 'editor.lib.contextualHelp.dragSelectedMovableObject',
205
+ message: 'Drag selected movable object',
206
+ },
134
207
  })
135
208
  }
136
209
 
137
210
  if (hasRotatableSelection) {
138
211
  hints.push({
139
212
  keys: [COMMAND_KEY, RIGHT_CLICK],
140
- label: 'Drag left or right to rotate selected object',
213
+ label: /* i18n */ {
214
+ id: 'editor.lib.contextualHelp.dragLeftOrRightToRotate',
215
+ message: 'Drag left or right to rotate selected object',
216
+ },
141
217
  })
142
218
  }
143
219
 
144
220
  if (hasOpeningRadiusSelection) {
145
221
  hints.push({
146
222
  keys: [SHIFT_KEY],
147
- label: 'Adjust one side’s corner radius',
223
+ label: /* i18n */ {
224
+ id: 'editor.lib.contextualHelp.adjustOneSideSCornerRadius',
225
+ message: 'Adjust one side’s corner radius',
226
+ },
148
227
  active: shiftPressed,
149
228
  })
150
229
  }
151
230
 
152
231
  hints.push({
153
232
  keys: [[COMMAND_KEY, SHIFT_KEY], LEFT_CLICK],
154
- label: 'Add or remove objects from the selection',
233
+ label: /* i18n */ {
234
+ id: 'editor.lib.contextualHelp.addOrRemoveObjectsFromThe',
235
+ message: 'Add or remove objects from the selection',
236
+ },
155
237
  active: commandPressed || shiftPressed,
156
238
  })
157
239
 
@@ -1,3 +1,4 @@
1
+ import type { MessageDescriptor } from '@pascal-app/core'
1
2
  export type ContinuationContext = 'wall' | 'fence' | 'point' | 'cabinet' | 'canopy'
2
3
  export type ContinuationMode = string
3
4
 
@@ -6,7 +7,7 @@ export const CONTINUATION_PROFILES: Record<
6
7
  {
7
8
  options: ContinuationMode[]
8
9
  default: ContinuationMode
9
- labels: Record<string, string>
10
+ labels: Record<string, MessageDescriptor>
10
11
  icons: Record<string, string>
11
12
  /**
12
13
  * Picked once from the Build panel before drawing (`wall-draw-variant.ts`),
@@ -17,11 +18,19 @@ export const CONTINUATION_PROFILES: Record<
17
18
  > = {
18
19
  // The wall tool's drawing mode: a chain that closes into a room ('room', the
19
20
  // Build panel's Polygon), one wall per two clicks ('single', Walls), or a
20
- // two-corner box of four walls ('rectangle', Rectangle).
21
+ // two-corner box of four walls ('rectangle', Rectangle). The last one picked
22
+ // is what B arms; Rectangle until one is.
21
23
  wall: {
22
24
  options: ['room', 'single', 'rectangle'],
23
- default: 'room',
24
- labels: { room: 'Polygon room', single: 'Walls', rectangle: 'Rectangle room' },
25
+ default: 'rectangle',
26
+ labels: {
27
+ room: /* i18n */ { id: 'editor.lib.continuation.polygonRoom', message: 'Polygon room' },
28
+ single: /* i18n */ { id: 'editor.lib.continuation.walls', message: 'Walls' },
29
+ rectangle: /* i18n */ {
30
+ id: 'editor.lib.continuation.rectangleRoom',
31
+ message: 'Rectangle room',
32
+ },
33
+ },
25
34
  icons: { room: 'lucide:pentagon', single: 'lucide:minus', rectangle: 'lucide:square' },
26
35
  chosenInPanel: true,
27
36
  },
@@ -29,10 +38,13 @@ export const CONTINUATION_PROFILES: Record<
29
38
  options: ['single', 'continuous', 'curved', 'freehand'],
30
39
  default: 'continuous',
31
40
  labels: {
32
- continuous: 'Continuous',
33
- single: 'Single fence',
34
- curved: 'Curved fence',
35
- freehand: 'Freehand fence',
41
+ continuous: /* i18n */ { id: 'editor.lib.continuation.continuous', message: 'Continuous' },
42
+ single: /* i18n */ { id: 'editor.lib.continuation.singleFence', message: 'Single fence' },
43
+ curved: /* i18n */ { id: 'editor.lib.continuation.curvedFence', message: 'Curved fence' },
44
+ freehand: /* i18n */ {
45
+ id: 'editor.lib.continuation.freehandFence',
46
+ message: 'Freehand fence',
47
+ },
36
48
  },
37
49
  icons: {
38
50
  continuous: 'lucide:waypoints',
@@ -44,19 +56,34 @@ export const CONTINUATION_PROFILES: Record<
44
56
  point: {
45
57
  options: ['once', 'repeat'],
46
58
  default: 'once',
47
- labels: { once: 'Place once', repeat: 'Place multiple' },
59
+ labels: {
60
+ once: /* i18n */ { id: 'editor.lib.continuation.placeOnce', message: 'Place once' },
61
+ repeat: /* i18n */ { id: 'editor.lib.continuation.placeMultiple', message: 'Place multiple' },
62
+ },
48
63
  icons: { once: 'lucide:target', repeat: 'lucide:copy-plus' },
49
64
  },
50
65
  cabinet: {
51
66
  options: ['single', 'continuous'],
52
67
  default: 'single',
53
- labels: { single: 'Single cabinet', continuous: 'Continuous run' },
68
+ labels: {
69
+ single: /* i18n */ { id: 'editor.lib.continuation.singleCabinet', message: 'Single cabinet' },
70
+ continuous: /* i18n */ {
71
+ id: 'editor.lib.continuation.continuousRun',
72
+ message: 'Continuous run',
73
+ },
74
+ },
54
75
  icons: { single: 'lucide:minus', continuous: 'lucide:waypoints' },
55
76
  },
56
77
  canopy: {
57
78
  options: ['single', 'continuous'],
58
79
  default: 'single',
59
- labels: { single: 'Single canopy', continuous: 'Continuous canopy' },
80
+ labels: {
81
+ single: /* i18n */ { id: 'editor.lib.continuation.singleCanopy', message: 'Single canopy' },
82
+ continuous: /* i18n */ {
83
+ id: 'editor.lib.continuation.continuousCanopy',
84
+ message: 'Continuous canopy',
85
+ },
86
+ },
60
87
  icons: { single: 'lucide:minus', continuous: 'lucide:waypoints' },
61
88
  },
62
89
  }
@@ -154,12 +154,27 @@ export function collectElevationSnapTargets(
154
154
  anchor: center,
155
155
  label: 'Wall base',
156
156
  })
157
- targets.push({
158
- id: `${node.id}:top`,
159
- elevation: base + getWallEffectiveHeightForNodes(node, nodes),
160
- anchor: center,
161
- label: 'Wall top',
162
- })
157
+ if (node.endHeightOffset) {
158
+ targets.push({
159
+ id: `${node.id}:top-start`,
160
+ elevation: base + getWallEffectiveHeightForNodes(node, nodes, 0),
161
+ anchor: node.start,
162
+ label: 'Wall top start',
163
+ })
164
+ targets.push({
165
+ id: `${node.id}:top-end`,
166
+ elevation: base + getWallEffectiveHeightForNodes(node, nodes, 1),
167
+ anchor: node.end,
168
+ label: 'Wall top end',
169
+ })
170
+ } else {
171
+ targets.push({
172
+ id: `${node.id}:top`,
173
+ elevation: base + getWallEffectiveHeightForNodes(node, nodes, 0.5),
174
+ anchor: center,
175
+ label: 'Wall top',
176
+ })
177
+ }
163
178
  continue
164
179
  }
165
180
 
@@ -1,26 +1,4 @@
1
- import {
2
- type AnyNode,
3
- type AnyNodeId,
4
- type BuildingNode,
5
- type ElevatorNode,
6
- type LevelNode,
7
- spatialGridManager,
8
- useScene,
9
- } from '@pascal-app/core'
10
-
11
- function getBuildingLevels(
12
- buildingId: string | null | undefined,
13
- nodes: Record<string, AnyNode>,
14
- ): LevelNode[] {
15
- if (!buildingId) return []
16
- const building = nodes[buildingId as AnyNodeId]
17
- if (building?.type !== 'building') return []
18
-
19
- return building.children
20
- .map((childId) => nodes[childId as AnyNodeId])
21
- .filter((entry): entry is LevelNode => entry?.type === 'level')
22
- .sort((left, right) => left.level - right.level)
23
- }
1
+ import type { AnyNode, AnyNodeId, BuildingNode, LevelNode } from '@pascal-app/core'
24
2
 
25
3
  export function resolveCurrentBuildingId({
26
4
  buildingId,
@@ -45,52 +23,3 @@ export function resolveCurrentBuildingId({
45
23
 
46
24
  return null
47
25
  }
48
-
49
- export function resolveElevatorSupportLevelId({
50
- buildingId,
51
- preferredLevelId,
52
- }: {
53
- buildingId: string | null | undefined
54
- preferredLevelId?: string | null
55
- }): LevelNode['id'] | null {
56
- const nodes = useScene.getState().nodes
57
- const preferred = preferredLevelId ? nodes[preferredLevelId as AnyNodeId] : undefined
58
- const levels = getBuildingLevels(buildingId, nodes)
59
- const preferredInBuilding = preferredLevelId
60
- ? levels.find((level) => level.id === preferredLevelId)
61
- : undefined
62
-
63
- if (preferredInBuilding) return preferredInBuilding.id
64
- if (levels.length === 0) return preferred?.type === 'level' ? preferred.id : null
65
-
66
- return levels[0]?.id ?? null
67
- }
68
-
69
- export function resolveElevatorSupportY({
70
- buildingId,
71
- preferredLevelId,
72
- x,
73
- z,
74
- }: {
75
- buildingId: string | null | undefined
76
- preferredLevelId?: string | null
77
- x: number
78
- z: number
79
- }): number {
80
- const levelId = resolveElevatorSupportLevelId({ buildingId, preferredLevelId })
81
- if (!levelId) return 0
82
-
83
- return Math.max(0, spatialGridManager.getSlabElevationAt(levelId, x, z))
84
- }
85
-
86
- export function resolveElevatorNodeSupportY(
87
- node: ElevatorNode,
88
- position: [number, number, number] = node.position,
89
- ): number {
90
- return resolveElevatorSupportY({
91
- buildingId: node.parentId,
92
- preferredLevelId: node.fromLevelId ?? node.defaultLevelId,
93
- x: position[0],
94
- z: position[2],
95
- })
96
- }
@@ -4,12 +4,14 @@ import {
4
4
  applySceneOperationPatch,
5
5
  BuildingNode,
6
6
  clearSceneHistory,
7
+ createI18n,
7
8
  createZone,
8
9
  floorFootprintName,
9
10
  generateId,
10
11
  ItemNode,
11
12
  initSpaceDetectionSync,
12
13
  LevelNode,
14
+ levelFootprints,
13
15
  type SceneCommit,
14
16
  type SlabNode,
15
17
  SlabNode as SlabSchema,
@@ -28,7 +30,6 @@ import {
28
30
  footprintsToMoveTogether,
29
31
  foundationHeight,
30
32
  isFootprintConstructionHit,
31
- levelFootprints,
32
33
  presetPatch,
33
34
  roomFloorOwner,
34
35
  roomFootprint,
@@ -418,7 +419,7 @@ describe('floor footprints', () => {
418
419
  // What the notice offers instead: both footprints, together, in one step.
419
420
  const together = footprintsToMoveTogether(nodes(), house.id, patch)
420
421
  expect(together).toEqual([house.id, shed.id])
421
- expect(moveTogetherLabel(together!.length, false)).toBe('Raise both')
422
+ expect(moveTogetherLabel(together!.length, false, createI18n())).toBe('Raise both')
422
423
  const steps = history()
423
424
  expect(applyFloorFoundation(together!, patch)).toBeNull()
424
425
  expect(history()).toBe(steps + 1)
@@ -10,6 +10,7 @@ import {
10
10
  getRoomBaseElevation,
11
11
  groundFloorConstruction,
12
12
  intersection,
13
+ levelFootprints,
13
14
  roomDrawnFloor,
14
15
  type SlabNode,
15
16
  setFloorFoundation,
@@ -39,15 +40,6 @@ const ON_GROUND = 0.0005
39
40
  const isBase = (node: AnyNode | undefined): node is SlabNode =>
40
41
  node?.type === 'slab' && node.plateRole === 'base'
41
42
 
42
- const footprintArea = (plate: SlabNode) => area([{ outer: plate.polygon, holes: plate.holes }])
43
-
44
- /** The level's footprints, largest first (ties by id, so the order is stable). */
45
- export function levelFootprints(nodes: Record<string, AnyNode>, levelId: string): SlabNode[] {
46
- return Object.values(nodes)
47
- .filter((node): node is SlabNode => isBase(node) && node.parentId === levelId)
48
- .sort((a, b) => footprintArea(b) - footprintArea(a) || a.id.localeCompare(b.id))
49
- }
50
-
51
43
  /**
52
44
  * The drawn slab a room's floor is: the one it visibly stands on (a legacy
53
45
  * floor can show a hand-drawn slab over the one it was taken from), else the
@@ -3,6 +3,7 @@ import {
3
3
  type AnyNode,
4
4
  type AnyNodeId,
5
5
  BuildingNode,
6
+ createI18n,
6
7
  createZone,
7
8
  floorFootprintSupportClass,
8
9
  generateId,
@@ -10,6 +11,7 @@ import {
10
11
  getLevelElevations,
11
12
  initSpaceDetectionSync,
12
13
  LevelNode,
14
+ levelFootprints,
13
15
  type SlabNode,
14
16
  upperFloorHeightControl,
15
17
  useScene,
@@ -29,13 +31,14 @@ import {
29
31
  footprintHeightMinimum,
30
32
  footprintHeightPatch,
31
33
  footprintHeightValue,
32
- levelFootprints,
33
34
  presetPatch,
34
35
  setFootprintHeight,
35
36
  thickFloorAdvice,
36
37
  } from './floor-footprints'
37
38
  import { applyRoomPlan } from './room-structure-commands'
38
39
 
40
+ const english = createI18n()
41
+
39
42
  const GROUND = 'level_support_ground'
40
43
  const UPPER = 'level_support_upper'
41
44
  let stop = () => {}
@@ -98,8 +101,10 @@ describe('Floor & foundation support class (point 5)', () => {
98
101
  })
99
102
 
100
103
  test('the kicker says where the plate is and what it rests on', () => {
101
- expect(floorPlateKicker(nodes(), plateOf(GROUND, 'Living'))).toBe('Floor plate · Ground floor')
102
- expect(floorPlateKicker(nodes(), plateOf(UPPER, 'Bedroom'))).toBe(
104
+ expect(floorPlateKicker(nodes(), plateOf(GROUND, 'Living'), english, 'en')).toBe(
105
+ 'Floor plate · Ground floor',
106
+ )
107
+ expect(floorPlateKicker(nodes(), plateOf(UPPER, 'Bedroom'), english, 'en')).toBe(
103
108
  'Floor plate · Floor 1 · rests on Ground floor',
104
109
  )
105
110
  })
@@ -164,7 +169,6 @@ describe('Upper-floor height drag release (round-final item 6)', () => {
164
169
  test(`a drag of ${delta} m lands on release`, () => {
165
170
  const bedroom = plateOf(UPPER, 'Bedroom')
166
171
  const start = footprintHeightValue(nodes(), bedroom)
167
- const past = useScene.temporal.getState().pastStates.length
168
172
  const preview = beginFootprintHeightPreview(bedroom.id)
169
173
  expect(preview.preview(start + delta / 2)).toBeNull()
170
174
  expect(preview.preview(start + delta)).toBeNull()
@@ -172,7 +176,13 @@ describe('Upper-floor height drag release (round-final item 6)', () => {
172
176
  const after = nodes()[bedroom.id] as SlabNode
173
177
  expect(after.thickness).toBeCloseTo(bedroom.thickness + delta, 6)
174
178
  expect(footprintHeightValue(nodes(), after)).toBeCloseTo(start + delta, 6)
175
- expect(useScene.temporal.getState().pastStates.length).toBe(past + 1)
179
+ useScene.temporal.getState().undo()
180
+ expect(footprintHeightValue(nodes(), nodes()[bedroom.id] as SlabNode)).toBeCloseTo(start, 6)
181
+ useScene.temporal.getState().redo()
182
+ expect(footprintHeightValue(nodes(), nodes()[bedroom.id] as SlabNode)).toBeCloseTo(
183
+ start + delta,
184
+ 6,
185
+ )
176
186
  })
177
187
  }
178
188
  })
@@ -183,7 +193,6 @@ describe('Floor & foundation panel (point 5)', () => {
183
193
  expect(ground).toMatchObject({ upper: false, preset: 'ground', underFloor: true, minHeight: 0 })
184
194
  const upper = floorFoundationModel(nodes(), plateOf(UPPER, 'Bedroom'))
185
195
  expect(upper).toMatchObject({ upper: true, underFloor: false, thickFloor: false })
186
- expect(upper.kicker).toBe('Floor plate · Floor 1 · rests on Ground floor')
187
196
  })
188
197
 
189
198
  test('on the ground the two inputs are slab thickness and foundation height; the top follows', () => {
@@ -7,6 +7,7 @@ import {
7
7
  generateId,
8
8
  type LevelNode,
9
9
  type MultiPolygon,
10
+ roomFloorElevation,
10
11
  type StructurePlan,
11
12
  structureChangeBatch,
12
13
  useScene,
@@ -31,7 +32,6 @@ import {
31
32
  import { type FloorRegionPoint, floorRegionSnapTargets } from './floor-region-snap'
32
33
  import { beginGesture, type GestureHandle } from './gesture-lifecycle'
33
34
  import { resolveRoomAssemblyHeights } from './room-assembly-overlay'
34
- import { roomFloorElevation } from './room-handle-drag'
35
35
  import { sfxEmitter } from './sfx-bus'
36
36
 
37
37
  // "Cut opening": draws an opening on a room's floor (or its ceiling) with the
@@ -7,6 +7,7 @@ import {
7
7
  ceilingRegionsOwner,
8
8
  type MultiPolygon,
9
9
  resolveCeilingHeight,
10
+ roomFloorElevation,
10
11
  union,
11
12
  useScene,
12
13
  } from '@pascal-app/core'
@@ -32,7 +33,6 @@ import { beginGesture, type GestureHandle } from './gesture-lifecycle'
32
33
  import { hasActivePaintMaterial } from './material-paint'
33
34
  import { isPaintErasing, usePaintRegionMode } from './paint-region-mode'
34
35
  import { addCeilingRegion, addFloorRegion } from './paint-regions'
35
- import { roomFloorElevation } from './room-handle-drag'
36
36
 
37
37
  // An outline drawn on one room's floor: a rectangle (press, drag, release; a
38
38
  // persistent tool also takes two clicks) or a polygon (click points, close on
@@ -26,6 +26,11 @@ describe('parseDimensionInput', () => {
26
26
  expect(parseDimensionInput('12.5', 'imperial')).toBeCloseTo(12.5 * FOOT, 9)
27
27
  })
28
28
 
29
+ test('decimal commas follow the scoped locale', () => {
30
+ expect(parseDimensionInput('2,75', 'metric', 'fr')).toBe(2.75)
31
+ expect(parseDimensionInput('2,75', 'metric', 'en')).toBeNull()
32
+ })
33
+
29
34
  test('rejects garbage and non-positive values', () => {
30
35
  expect(parseDimensionInput('')).toBeNull()
31
36
  expect(parseDimensionInput('abc')).toBeNull()
@@ -152,7 +157,7 @@ describe('driving a wall length', () => {
152
157
  expect(plan?.updates[0]?.data).toEqual({ start: [0, 0], end: [3.5, 0] })
153
158
  })
154
159
 
155
- test('a dimension across the wall is not drivable', () => {
160
+ test('a dimension across the wall that ends on neither face is not drivable', () => {
156
161
  const nodes = scene()
157
162
  const resolution = resolveDimensionDrive({
158
163
  nodes,
@@ -160,7 +165,10 @@ describe('driving a wall length', () => {
160
165
  start: [1.5, -1],
161
166
  end: [1.5, 1],
162
167
  })
163
- expect(resolution).toEqual({ drivable: false, reason: 'dimension is not along the wall' })
168
+ expect(resolution).toEqual({
169
+ drivable: false,
170
+ reason: 'dimension does not start or end on this wall',
171
+ })
164
172
  })
165
173
 
166
174
  test('a locked wall refuses to drive', () => {
@@ -358,3 +366,29 @@ describe('an opening width tag resizes the opening', () => {
358
366
  expect(plan?.updates).toEqual([{ id: 'win_1' as AnyNodeId, data: { width: 1.2192 } }])
359
367
  })
360
368
  })
369
+
370
+ describe('a dimension across a wall slides the wall', () => {
371
+ test('a room width from the wall face drives the wall along its normal, away from the far end', () => {
372
+ // wall_a runs east along z = 0; the dimension leaves its south face for a wall 3 m south.
373
+ const resolution = resolveDimensionDrive({
374
+ nodes: scene(),
375
+ ownerNodeId: 'wall_a' as AnyNodeId,
376
+ start: [1.5, 0.075],
377
+ end: [1.5, 3],
378
+ })
379
+ expect(resolution).toEqual({
380
+ drivable: true,
381
+ target: { kind: 'wall-offset', wallId: 'wall_a' as AnyNodeId, direction: [0, -1] },
382
+ })
383
+ })
384
+
385
+ test('the wall end of the dimension may be either witness point', () => {
386
+ const resolution = resolveDimensionDrive({
387
+ nodes: scene(),
388
+ ownerNodeId: 'wall_a' as AnyNodeId,
389
+ start: [1.5, -4],
390
+ end: [1.5, -0.075],
391
+ })
392
+ expect(resolution).toMatchObject({ drivable: true, target: { direction: [0, 1] } })
393
+ })
394
+ })