@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
@@ -4,6 +4,7 @@ import {
4
4
  applySceneOperationPatch,
5
5
  BuildingNode,
6
6
  clearSceneHistory,
7
+ createI18n,
7
8
  createZone,
8
9
  floorFootprintName,
9
10
  generateId,
@@ -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)
@@ -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,
@@ -36,6 +37,8 @@ import {
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
  })
@@ -183,7 +188,6 @@ describe('Floor & foundation panel (point 5)', () => {
183
188
  expect(ground).toMatchObject({ upper: false, preset: 'ground', underFloor: true, minHeight: 0 })
184
189
  const upper = floorFoundationModel(nodes(), plateOf(UPPER, 'Bedroom'))
185
190
  expect(upper).toMatchObject({ upper: true, underFloor: false, thickFloor: false })
186
- expect(upper.kicker).toBe('Floor plate · Floor 1 · rests on Ground floor')
187
191
  })
188
192
 
189
193
  test('on the ground the two inputs are slab thickness and foundation height; the top follows', () => {
@@ -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
+ })
@@ -1,5 +1,5 @@
1
1
  import type { AnyNode, AnyNodeId, DoorNode, WallNode, WindowNode } from '@pascal-app/core'
2
- import { getLinkedWallUpdates } from '@pascal-app/core'
2
+ import { getLinkedWallUpdates, normalizeDecimalInput } from '@pascal-app/core'
3
3
 
4
4
  /**
5
5
  * Driving dimensions — WS3.
@@ -34,8 +34,9 @@ const INCHES_PER_METER = 1 / 0.0254
34
34
  export function parseDimensionInput(
35
35
  raw: string,
36
36
  unit: 'metric' | 'imperial' = 'metric',
37
+ locale = 'en',
37
38
  ): number | null {
38
- const text = raw.trim().toLowerCase().replace(/\s+/g, ' ')
39
+ const text = normalizeDecimalInput(raw, locale).trim().toLowerCase().replace(/\s+/g, ' ')
39
40
  if (!text) return null
40
41
 
41
42
  // Explicit metric suffix.
@@ -119,6 +120,17 @@ export type DimensionDriveTarget =
119
120
  openingId: AnyNodeId
120
121
  wallId: AnyNodeId
121
122
  }
123
+ | {
124
+ /**
125
+ * The dimension runs across the wall, from one of its faces to another
126
+ * wall (a room width, the outside size): the whole wall slides along
127
+ * its normal, its neighbours following, as a push of the wall does.
128
+ */
129
+ kind: 'wall-offset'
130
+ wallId: AnyNodeId
131
+ /** Unit vector the wall travels along when the value grows. */
132
+ direction: DimensionPlanPoint
133
+ }
122
134
 
123
135
  export type DimensionDriveResolution =
124
136
  | { drivable: true; target: DimensionDriveTarget }
@@ -173,7 +185,7 @@ function resolveWallDrive(
173
185
  const wallDirection = unit(subtract(wall.end, wall.start))
174
186
  if (!wallDirection) return { drivable: false, reason: 'degenerate wall' }
175
187
  if (Math.abs(cross(wallDirection, dimDirection)) > PARALLEL_TOLERANCE) {
176
- return { drivable: false, reason: 'dimension is not along the wall' }
188
+ return resolveWallOffsetDrive(query, wall, wallDirection, dimDirection)
177
189
  }
178
190
  if ((wall as { curvature?: number }).curvature) {
179
191
  return { drivable: false, reason: 'curved walls are not drivable yet' }
@@ -209,6 +221,56 @@ function resolveWallDrive(
209
221
  }
210
222
  }
211
223
 
224
+ /** How far off a wall's face a witness point may sit and still be on that wall. */
225
+ const FACE_MATCH_TOLERANCE_M = 0.05
226
+
227
+ /**
228
+ * A dimension square to the wall, with one end on it: typing a value slides
229
+ * the wall along its normal, away from the other end when the value grows.
230
+ */
231
+ function resolveWallOffsetDrive(
232
+ query: DimensionDriveQuery,
233
+ wall: WallNode,
234
+ wallDirection: DimensionPlanPoint,
235
+ dimDirection: DimensionPlanPoint,
236
+ ): DimensionDriveResolution {
237
+ if (Math.abs(dot(wallDirection, dimDirection)) > PARALLEL_TOLERANCE) {
238
+ return { drivable: false, reason: 'dimension is neither along nor across the wall' }
239
+ }
240
+ if (wall.curveOffset) return { drivable: false, reason: 'curved walls are not drivable yet' }
241
+ const reach = (wall.thickness ?? 0.1) + FACE_MATCH_TOLERANCE_M
242
+ const onWall = (point: DimensionPlanPoint) =>
243
+ distanceToSegment(point, wall.start as DimensionPlanPoint, wall.end as DimensionPlanPoint) <=
244
+ reach
245
+ const startOn = onWall(query.start)
246
+ const endOn = onWall(query.end)
247
+ if (startOn === endOn) {
248
+ return { drivable: false, reason: 'dimension does not start or end on this wall' }
249
+ }
250
+ const direction = startOn
251
+ ? unit(subtract(query.start, query.end))
252
+ : unit(subtract(query.end, query.start))
253
+ if (!direction) return { drivable: false, reason: 'degenerate dimension' }
254
+ return {
255
+ drivable: true,
256
+ target: { kind: 'wall-offset', wallId: wall.id as AnyNodeId, direction },
257
+ }
258
+ }
259
+
260
+ function distanceToSegment(
261
+ point: DimensionPlanPoint,
262
+ start: DimensionPlanPoint,
263
+ end: DimensionPlanPoint,
264
+ ): number {
265
+ const segment = subtract(end, start)
266
+ const lengthSq = dot(segment, segment)
267
+ const t =
268
+ lengthSq <= 1e-12
269
+ ? 0
270
+ : Math.max(0, Math.min(1, dot(subtract(point, start), segment) / lengthSq))
271
+ return length(subtract(point, [start[0] + segment[0] * t, start[1] + segment[1] * t]))
272
+ }
273
+
212
274
  function resolveOpeningDrive(
213
275
  query: DimensionDriveQuery,
214
276
  opening: DoorNode | WindowNode,
@@ -359,6 +421,9 @@ export function planDimensionDrive(args: {
359
421
  }): DimensionDrivePlan | null {
360
422
  const delta = args.nextLength - args.currentLength
361
423
  if (!Number.isFinite(delta) || Math.abs(delta) < 1e-9) return { updates: [], delta: 0 }
424
+ // A wall offset moves the wall with its junctions re-planned, bridges and
425
+ // all: that is the wall-push planner's job (`commitDimensionValue`).
426
+ if (args.target.kind === 'wall-offset') return null
362
427
 
363
428
  if (args.target.kind === 'opening-width') {
364
429
  const opening = args.nodes[args.target.openingId]
@@ -311,16 +311,27 @@ describe('floor plan export policy', () => {
311
311
  stairAnnotations: true,
312
312
  }
313
313
 
314
- expect(resolveFloorplanExportAnnotationVisibility('expert', liveVisibility)).toEqual(
314
+ expect(resolveFloorplanExportAnnotationVisibility('expert', liveVisibility, true)).toEqual(
315
315
  liveVisibility,
316
316
  )
317
317
  })
318
318
 
319
+ test('exports no automatic dimensions while Display → Dimensions is off', () => {
320
+ expect(
321
+ resolveFloorplanExportAnnotationVisibility(
322
+ 'expert',
323
+ DEFAULT_FLOORPLAN_ANNOTATION_VISIBILITY,
324
+ false,
325
+ ),
326
+ ).toEqual({ ...DEFAULT_FLOORPLAN_ANNOTATION_VISIBILITY, automaticDimensions: false })
327
+ })
328
+
319
329
  test('exports only model geometry and room labels in Default', () => {
320
330
  expect(
321
331
  resolveFloorplanExportAnnotationVisibility(
322
332
  'default',
323
333
  DEFAULT_FLOORPLAN_ANNOTATION_VISIBILITY,
334
+ true,
324
335
  ),
325
336
  ).toEqual({
326
337
  automaticDimensions: false,
@@ -700,7 +711,7 @@ describe('collectFloorplanGeometry', () => {
700
711
  'meters',
701
712
  DEFAULT_FLOORPLAN_ANNOTATION_VISIBILITY,
702
713
  'floor-plan',
703
- 'finished-faces',
714
+ 'both',
704
715
  [enabledPluginId],
705
716
  )
706
717
 
@@ -758,7 +769,7 @@ describe('collectFloorplanGeometry', () => {
758
769
  'meters',
759
770
  DEFAULT_FLOORPLAN_ANNOTATION_VISIBILITY,
760
771
  'floor-plan',
761
- 'finished-faces',
772
+ 'both',
762
773
  [enabledPluginId],
763
774
  )
764
775
  expect(upper.map(({ id }) => id)).toEqual(['site_overlay', 'level_upper_architecture'])
@@ -775,7 +786,7 @@ describe('collectFloorplanGeometry', () => {
775
786
  'meters',
776
787
  DEFAULT_FLOORPLAN_ANNOTATION_VISIBILITY,
777
788
  'floor-plan',
778
- 'finished-faces',
789
+ 'both',
779
790
  [enabledPluginId],
780
791
  )
781
792
  // A hidden Site hides only its own ground and boundary; the site-scoped
@@ -790,7 +801,7 @@ describe('collectFloorplanGeometry', () => {
790
801
  'meters',
791
802
  DEFAULT_FLOORPLAN_ANNOTATION_VISIBILITY,
792
803
  'floor-plan',
793
- 'finished-faces',
804
+ 'both',
794
805
  [enabledPluginId],
795
806
  )
796
807
  expect(structure.map(({ id }) => id)).toEqual(['level_architecture'])
@@ -44,15 +44,11 @@ import { resolveNodeForDrawingType } from './drawing-coordination'
44
44
  import {
45
45
  type FloorplanMetricNotation,
46
46
  type FloorplanSchedule,
47
- type FloorplanWallDimensionReference,
47
+ type FloorplanWallDimensionFaces,
48
48
  getFloorplanNodeExtension,
49
49
  readFloorplanGeometryMetadata,
50
50
  } from './floorplan-extension'
51
- import {
52
- type FloorplanMode,
53
- resolveFloorplanAnnotationVisibility,
54
- resolveFloorplanWallDimensionReference,
55
- } from './floorplan-mode'
51
+ import { type FloorplanMode, resolveFloorplanAnnotationVisibility } from './floorplan-mode'
56
52
  import { createFloorplanPdfDocument, type FloorplanPdfDocument } from './floorplan-pdfkit-document'
57
53
  import { renderFloorplanGeometryToPdfKit } from './floorplan-pdfkit-renderer'
58
54
  import { FLOORPLAN_VIEW_ROTATION_DEG } from './geometry'
@@ -131,7 +127,7 @@ const NEUTRAL_VIEW_STATE = {
131
127
  export function resolveFloorplanExportViewState(
132
128
  unit: 'metric' | 'imperial',
133
129
  metricNotation: FloorplanMetricNotation,
134
- wallDimensionReference?: FloorplanWallDimensionReference,
130
+ wallDimensionFaces?: FloorplanWallDimensionFaces,
135
131
  automaticDimensions = true,
136
132
  ) {
137
133
  return {
@@ -139,7 +135,7 @@ export function resolveFloorplanExportViewState(
139
135
  automaticDimensions,
140
136
  unit,
141
137
  metricNotation,
142
- wallDimensionReference,
138
+ wallDimensionFaces,
143
139
  }
144
140
  }
145
141
 
@@ -195,10 +191,7 @@ export async function exportFloorplanPdf(scope: FloorplanExportScope): Promise<v
195
191
  const annotationVisibility = resolveFloorplanExportAnnotationVisibility(
196
192
  floorplanMode,
197
193
  expertAnnotationState.visibility,
198
- )
199
- const wallDimensionReference = resolveFloorplanWallDimensionReference(
200
- floorplanMode,
201
- expertAnnotationState.wallDimensionReference,
194
+ expertAnnotationState.showDimensions,
202
195
  )
203
196
  const navigationAzimuth = useEditor.getState().navigationSyncPose?.azimuth
204
197
  const drawingType = useDrawingView.getState().drawingType
@@ -232,7 +225,7 @@ export async function exportFloorplanPdf(scope: FloorplanExportScope): Promise<v
232
225
  metricNotation,
233
226
  annotationVisibility,
234
227
  drawingType,
235
- wallDimensionReference,
228
+ expertAnnotationState.wallDimensionFaces,
236
229
  installedPlugins,
237
230
  )
238
231
  const schedules = collectFloorplanSchedules(nodes, level.id, unit, scope)
@@ -604,8 +597,12 @@ export function resolveFloorplanScreenUnitsPerPixel(
604
597
  export function resolveFloorplanExportAnnotationVisibility(
605
598
  mode: FloorplanMode,
606
599
  liveVisibility: FloorplanAnnotationVisibility,
600
+ dimensions: boolean,
607
601
  ): FloorplanAnnotationVisibility {
608
- return resolveFloorplanAnnotationVisibility(mode, liveVisibility, { target: 'export' })
602
+ return resolveFloorplanAnnotationVisibility(mode, liveVisibility, {
603
+ target: 'export',
604
+ dimensions,
605
+ })
609
606
  }
610
607
 
611
608
  export function resolveFloorplanExportRotationDeg(
@@ -785,7 +782,7 @@ export function collectFloorplanGeometry(
785
782
  metricNotation: FloorplanMetricNotation,
786
783
  annotationVisibility: FloorplanAnnotationVisibility,
787
784
  drawingType: ConstructionDrawingType,
788
- wallDimensionReference: FloorplanWallDimensionReference,
785
+ wallDimensionFaces: FloorplanWallDimensionFaces,
789
786
  /** `undefined` = legacy scene without install state: every loaded kind draws. */
790
787
  installedPlugins: readonly string[] | undefined,
791
788
  ): ExportGeometry[] {
@@ -907,7 +904,7 @@ export function collectFloorplanGeometry(
907
904
  resolveFloorplanExportViewState(
908
905
  unit,
909
906
  metricNotation,
910
- wallDimensionReference,
907
+ wallDimensionFaces,
911
908
  annotationVisibility.automaticDimensions,
912
909
  ),
913
910
  levelData,
@@ -2,7 +2,7 @@ import { describe, expect, test } from 'bun:test'
2
2
  import type { GeometryContext } from '@pascal-app/core'
3
3
  import {
4
4
  createFloorplanContextExtensions,
5
- normalizeFloorplanWallDimensionReference,
5
+ normalizeFloorplanWallDimensionFaces,
6
6
  readFloorplanContext,
7
7
  } from './floorplan-extension'
8
8
 
@@ -17,22 +17,21 @@ function context(extensions?: Readonly<Record<string, unknown>>): GeometryContex
17
17
  }
18
18
 
19
19
  describe('floor-plan context extensions', () => {
20
- test('defaults wall dimensions to finished faces', () => {
21
- expect(readFloorplanContext(context()).wallDimensionReference).toBe('finished-faces')
20
+ test('measures walls to both faces by default', () => {
21
+ expect(readFloorplanContext(context()).wallDimensionFaces).toBe('both')
22
22
  })
23
23
 
24
- test('carries the selected centerline or stud-face reference', () => {
25
- for (const wallDimensionReference of ['centerline', 'stud-faces'] as const) {
26
- const extensions = createFloorplanContextExtensions({ wallDimensionReference })
27
- expect(readFloorplanContext(context(extensions)).wallDimensionReference).toBe(
28
- wallDimensionReference,
29
- )
24
+ test('carries the Measure walls choice', () => {
25
+ for (const wallDimensionFaces of ['interior', 'exterior', 'both'] as const) {
26
+ const extensions = createFloorplanContextExtensions({ wallDimensionFaces })
27
+ expect(readFloorplanContext(context(extensions)).wallDimensionFaces).toBe(wallDimensionFaces)
30
28
  }
31
29
  })
32
30
 
33
- test('normalizes stale persisted references to the finished-face default', () => {
34
- expect(normalizeFloorplanWallDimensionReference('unknown')).toBe('finished-faces')
35
- expect(normalizeFloorplanWallDimensionReference(null)).toBe('finished-faces')
31
+ test('normalizes stale persisted values, including the retired references, to Both', () => {
32
+ for (const stale of ['finished-faces', 'centerline', 'stud-faces', 'unknown', null]) {
33
+ expect(normalizeFloorplanWallDimensionFaces(stale)).toBe('both')
34
+ }
36
35
  })
37
36
 
38
37
  test('lets presentation code suppress automatic annotation construction', () => {
@@ -8,6 +8,7 @@ import type {
8
8
  SceneApi,
9
9
  } from '@pascal-app/core'
10
10
  import type { ComponentType } from 'react'
11
+ import type { WallSelectionDimension } from '../room-push-dimensions'
11
12
 
12
13
  export const FLOORPLAN_NODE_EXTENSION_KEY = 'pascal:editor/floorplan'
13
14
  export const FLOORPLAN_GEOMETRY_METADATA_KEY = 'pascal:editor/floorplan'
@@ -16,8 +17,20 @@ export const FLOORPLAN_CONTEXT_EXTENSION_KEY = 'pascal:editor/floorplan'
16
17
  export type FloorplanRenderPurpose = 'edit' | 'document'
17
18
  export type FloorplanMetricNotation = 'meters' | 'millimeters'
18
19
  export type FloorplanToolMode = 'default' | 'expert'
19
- export type FloorplanWallDimensionReference = 'finished-faces' | 'centerline' | 'stud-faces'
20
- export const DEFAULT_FLOORPLAN_WALL_DIMENSION_REFERENCE = 'finished-faces'
20
+ /**
21
+ * Display → Measure walls: which finished faces wall dimensions measure to —
22
+ * the interior faces (ID, the room sizes), the exterior faces (OD, the
23
+ * outside size) or both. Read by a selected wall's temporary dimensions in 2D
24
+ * and 3D and by the Expert plan's automatic dimension strings.
25
+ */
26
+ export type FloorplanWallDimensionFaces = 'interior' | 'exterior' | 'both'
27
+ export const DEFAULT_FLOORPLAN_WALL_DIMENSION_FACES: FloorplanWallDimensionFaces = 'both'
28
+
29
+ export function normalizeFloorplanWallDimensionFaces(value: unknown): FloorplanWallDimensionFaces {
30
+ return value === 'interior' || value === 'exterior'
31
+ ? value
32
+ : DEFAULT_FLOORPLAN_WALL_DIMENSION_FACES
33
+ }
21
34
  export type FloorplanAnnotationRole =
22
35
  | 'automatic-dimension'
23
36
  | 'contextual-dimension'
@@ -67,6 +80,24 @@ export type FloorplanNodeExtension<N extends AnyNode = AnyNode> = {
67
80
  preferredView?: '2d' | '3d'
68
81
  referencedSelectionAnnotationRole?: FloorplanAnnotationRole
69
82
  contextualDimensions?: (node: N, ctx: GeometryContext) => FloorplanGeometry | null
83
+ /**
84
+ * A plan hit on this kind selects the node itself, not the room it bounds
85
+ * (3D stays room-first). For thin boundary bands whose rooms are clickable
86
+ * on either side of them.
87
+ */
88
+ planHitSelectsNode?: boolean
89
+ /**
90
+ * Temporary dimensions shown while this node is selected in Select mode,
91
+ * alone or with others, in 2D and 3D (level-local plan points). Its
92
+ * `contextualDimensions` also stay in a multi-selection. Each is typeable:
93
+ * `commitDimensionValue` drives the node so it reads the typed value.
94
+ * Recomputed when the node or its level's rooms change.
95
+ */
96
+ selectionDimensions?: (args: {
97
+ node: N
98
+ nodes: Readonly<Record<AnyNodeId, AnyNode>>
99
+ faces: FloorplanWallDimensionFaces
100
+ }) => readonly WallSelectionDimension[]
70
101
  actionMenu?: {
71
102
  canCurve?: (args: { node: N; nodes: Readonly<Record<AnyNodeId, AnyNode>> }) => boolean
72
103
  /** Buttons the kind adds to the selection's action menu (2D and 3D); each decides its own visibility. */
@@ -130,15 +161,7 @@ type FloorplanContextExtension = {
130
161
  */
131
162
  roomLabelOverlay: boolean
132
163
  metricNotation: FloorplanMetricNotation
133
- wallDimensionReference: FloorplanWallDimensionReference
134
- }
135
-
136
- export function normalizeFloorplanWallDimensionReference(
137
- value: unknown,
138
- ): FloorplanWallDimensionReference {
139
- return value === 'centerline' || value === 'stud-faces'
140
- ? value
141
- : DEFAULT_FLOORPLAN_WALL_DIMENSION_REFERENCE
164
+ wallDimensionFaces: FloorplanWallDimensionFaces
142
165
  }
143
166
 
144
167
  export function getFloorplanNodeExtension(
@@ -185,9 +208,7 @@ export function createFloorplanContextExtensions(
185
208
  drafting: values.drafting === true,
186
209
  roomLabelOverlay: values.roomLabelOverlay === true,
187
210
  metricNotation: values.metricNotation === 'millimeters' ? 'millimeters' : 'meters',
188
- wallDimensionReference: normalizeFloorplanWallDimensionReference(
189
- values.wallDimensionReference,
190
- ),
211
+ wallDimensionFaces: normalizeFloorplanWallDimensionFaces(values.wallDimensionFaces),
191
212
  } satisfies FloorplanContextExtension,
192
213
  }
193
214
  }
@@ -202,9 +223,7 @@ export function readFloorplanContext(ctx: GeometryContext): FloorplanContextExte
202
223
  drafting: extension.drafting === true,
203
224
  roomLabelOverlay: extension.roomLabelOverlay === true,
204
225
  metricNotation: extension.metricNotation === 'millimeters' ? 'millimeters' : 'meters',
205
- wallDimensionReference: normalizeFloorplanWallDimensionReference(
206
- extension.wallDimensionReference,
207
- ),
226
+ wallDimensionFaces: normalizeFloorplanWallDimensionFaces(extension.wallDimensionFaces),
208
227
  }
209
228
  }
210
229
  return {
@@ -213,7 +232,7 @@ export function readFloorplanContext(ctx: GeometryContext): FloorplanContextExte
213
232
  drafting: false,
214
233
  roomLabelOverlay: false,
215
234
  metricNotation: 'meters',
216
- wallDimensionReference: DEFAULT_FLOORPLAN_WALL_DIMENSION_REFERENCE,
235
+ wallDimensionFaces: DEFAULT_FLOORPLAN_WALL_DIMENSION_FACES,
217
236
  }
218
237
  }
219
238
 
@@ -5,7 +5,6 @@ import {
5
5
  normalizeFloorplanMode,
6
6
  normalizeFloorplanModesByProject,
7
7
  resolveFloorplanAnnotationVisibility,
8
- resolveFloorplanWallDimensionReference,
9
8
  } from './floorplan-mode'
10
9
 
11
10
  describe('floor-plan mode', () => {
@@ -115,10 +114,18 @@ describe('floor-plan mode', () => {
115
114
  })
116
115
  })
117
116
 
118
- test('uses centerline dimensions only in Default', () => {
119
- expect(resolveFloorplanWallDimensionReference('default', 'finished-faces')).toBe('centerline')
120
- expect(resolveFloorplanWallDimensionReference('expert', 'finished-faces')).toBe(
121
- 'finished-faces',
122
- )
117
+ test('Dimensions off hides automatic and contextual dimensions in both modes', () => {
118
+ for (const mode of ['default', 'expert'] as const) {
119
+ const visibility = resolveFloorplanAnnotationVisibility(
120
+ mode,
121
+ { ...DEFAULT_FLOORPLAN_ANNOTATION_VISIBILITY, contextualDimensions: true },
122
+ { selected: true, target: 'editor', dimensions: false },
123
+ )
124
+ expect(visibility.automaticDimensions).toBe(false)
125
+ expect(visibility.contextualDimensions).toBe(false)
126
+ // Drawn dimensions and measurements are the user's own: they stay.
127
+ expect(visibility.manualDimensions).toBe(true)
128
+ expect(visibility.measurements).toBe(true)
129
+ }
123
130
  })
124
131
  })
@@ -2,11 +2,7 @@ import {
2
2
  type FloorplanAnnotationVisibility,
3
3
  revealFloorplanAnnotationRole,
4
4
  } from './annotation-visibility'
5
- import type {
6
- FloorplanAnnotationRole,
7
- FloorplanToolMode,
8
- FloorplanWallDimensionReference,
9
- } from './floorplan-extension'
5
+ import type { FloorplanAnnotationRole, FloorplanToolMode } from './floorplan-extension'
10
6
 
11
7
  export const FLOORPLAN_MODES = ['default', 'expert'] as const
12
8
 
@@ -18,6 +14,8 @@ export type FloorplanPresentationContext = {
18
14
  referencedAnnotationRole?: FloorplanAnnotationRole
19
15
  selected?: boolean
20
16
  target: 'editor' | 'export'
17
+ /** Display → Dimensions. Off hides the automatic and contextual dimensions in either mode. */
18
+ dimensions?: boolean
21
19
  }
22
20
 
23
21
  export function normalizeFloorplanMode(value: unknown): FloorplanMode {
@@ -45,6 +43,17 @@ export function resolveFloorplanAnnotationVisibility(
45
43
  mode: FloorplanMode,
46
44
  expertVisibility: FloorplanAnnotationVisibility,
47
45
  context: FloorplanPresentationContext,
46
+ ): FloorplanAnnotationVisibility {
47
+ const visibility = resolveModeAnnotationVisibility(mode, expertVisibility, context)
48
+ if (context.dimensions !== false) return visibility
49
+ if (!visibility.automaticDimensions && !visibility.contextualDimensions) return visibility
50
+ return { ...visibility, automaticDimensions: false, contextualDimensions: false }
51
+ }
52
+
53
+ function resolveModeAnnotationVisibility(
54
+ mode: FloorplanMode,
55
+ expertVisibility: FloorplanAnnotationVisibility,
56
+ context: FloorplanPresentationContext,
48
57
  ): FloorplanAnnotationVisibility {
49
58
  if (mode === 'expert') return expertVisibility
50
59
 
@@ -66,10 +75,3 @@ export function resolveFloorplanAnnotationVisibility(
66
75
  ? revealFloorplanAnnotationRole(visibility, context.referencedAnnotationRole)
67
76
  : visibility
68
77
  }
69
-
70
- export function resolveFloorplanWallDimensionReference(
71
- mode: FloorplanMode,
72
- expertReference: FloorplanWallDimensionReference,
73
- ): FloorplanWallDimensionReference {
74
- return mode === 'default' ? 'centerline' : expertReference
75
- }
@@ -90,7 +90,7 @@ function exportedIds(installedPlugins: readonly string[] | undefined) {
90
90
  'meters',
91
91
  DEFAULT_FLOORPLAN_ANNOTATION_VISIBILITY,
92
92
  'floor-plan',
93
- 'finished-faces',
93
+ 'both',
94
94
  installedPlugins,
95
95
  ).map(({ id }) => id)
96
96
  }
@@ -1,7 +1,7 @@
1
1
  import type { AnyNode, AnyNodeId, FloorplanPalette, GeometryContext } from '@pascal-app/core'
2
2
  import {
3
3
  createFloorplanContextExtensions,
4
- type FloorplanWallDimensionReference,
4
+ type FloorplanWallDimensionFaces,
5
5
  } from './floorplan-extension'
6
6
 
7
7
  export type FloorplanViewState = {
@@ -14,7 +14,7 @@ export type FloorplanViewState = {
14
14
  drafting?: boolean
15
15
  /** Room labels drawn as screen pills (see `FloorplanContextExtension.roomLabelOverlay`). */
16
16
  roomLabelOverlay?: boolean
17
- wallDimensionReference?: FloorplanWallDimensionReference
17
+ wallDimensionFaces?: FloorplanWallDimensionFaces
18
18
  highlighted: boolean
19
19
  hovered: boolean
20
20
  moving: boolean
@@ -65,7 +65,7 @@ export function buildFloorplanContext(
65
65
  purpose: viewState.purpose ?? 'edit',
66
66
  drafting: viewState.drafting === true,
67
67
  roomLabelOverlay: viewState.roomLabelOverlay === true,
68
- wallDimensionReference: viewState.wallDimensionReference,
68
+ wallDimensionFaces: viewState.wallDimensionFaces,
69
69
  }),
70
70
  viewState: viewState.palette
71
71
  ? {