@pascal-app/editor 1.0.6 → 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 +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 +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
@@ -3,6 +3,7 @@ import {
3
3
  type AnyNode,
4
4
  BuildingNode,
5
5
  clearSceneHistory,
6
+ createI18n,
6
7
  createZone,
7
8
  generateId,
8
9
  initSpaceDetectionSync,
@@ -67,6 +68,7 @@ const scene = (...nodes: AnyNode[]): Record<string, AnyNode> =>
67
68
  const living = zone('zone_living', 'Living', rect(0, 0, 4, 4))
68
69
  const lanai = zone('zone_lanai', 'Lanai', rect(4, 0, 6, 4))
69
70
  const keyedLanai = zone('zone_lanai', 'Lanai', rect(4, 0, 6, 4), 'floor_lanai')
71
+ const english = createI18n()
70
72
  const length = (meters: number) => `${meters.toFixed(2)} m`
71
73
 
72
74
  describe('Built on: when the picker shows and what it offers', () => {
@@ -116,7 +118,11 @@ describe('Built on: when the picker shows and what it offers', () => {
116
118
  expect(model.shared).toBeNull()
117
119
  expect(separateFloorOffset(nodes, keyedLanai.id)).toBeNull()
118
120
  expect(
119
- separateFloorSummary({ ...model, offset: separateFloorOffset(nodes, keyedLanai.id) }, length),
121
+ separateFloorSummary(
122
+ { ...model, offset: separateFloorOffset(nodes, keyedLanai.id) },
123
+ length,
124
+ english,
125
+ ),
120
126
  ).toBe('Lanai floor')
121
127
  })
122
128
 
@@ -159,18 +165,22 @@ describe('Built on: the Floor row summary', () => {
159
165
  const lanaiFloor = { name: 'Lanai floor' }
160
166
  test('says how a separate floor sits against the shared floor', () => {
161
167
  const shared = { name: 'Shared floor' }
162
- expect(separateFloorSummary({ current: lanaiFloor, shared, offset: -0.15 }, length)).toBe(
163
- 'Lanai floor · 0.15 m below Shared floor',
164
- )
165
168
  expect(
166
- separateFloorSummary({ current: lanaiFloor, shared: { name: 'Cabin' }, offset: 0.3 }, length),
169
+ separateFloorSummary({ current: lanaiFloor, shared, offset: -0.15 }, length, english),
170
+ ).toBe('Lanai floor · 0.15 m below Shared floor')
171
+ expect(
172
+ separateFloorSummary(
173
+ { current: lanaiFloor, shared: { name: 'Cabin' }, offset: 0.3 },
174
+ length,
175
+ english,
176
+ ),
167
177
  ).toBe('Lanai floor · 0.30 m above Cabin')
168
- expect(separateFloorSummary({ current: lanaiFloor, shared, offset: 0.002 }, length)).toBe(
169
- 'Lanai floor · level with Shared floor',
170
- )
171
- expect(separateFloorSummary({ current: lanaiFloor, shared: null, offset: null }, length)).toBe(
172
- 'Lanai floor',
173
- )
178
+ expect(
179
+ separateFloorSummary({ current: lanaiFloor, shared, offset: 0.002 }, length, english),
180
+ ).toBe('Lanai floor · level with Shared floor')
181
+ expect(
182
+ separateFloorSummary({ current: lanaiFloor, shared: null, offset: null }, length, english),
183
+ ).toBe('Lanai floor')
174
184
  })
175
185
 
176
186
  test("compares the room's walking surface with the shared-floor room beside it, not plate with plate", () => {
@@ -188,7 +198,11 @@ describe('Built on: the Floor row summary', () => {
188
198
  expect(separateFloorOffset(nodes, keyedLanai.id)).toBeCloseTo(-0.3)
189
199
  const model = roomBuiltOn(nodes, keyedLanai.id)!
190
200
  expect(
191
- separateFloorSummary({ ...model, offset: separateFloorOffset(nodes, keyedLanai.id) }, length),
201
+ separateFloorSummary(
202
+ { ...model, offset: separateFloorOffset(nodes, keyedLanai.id) },
203
+ length,
204
+ english,
205
+ ),
192
206
  ).toBe('Lanai floor · 0.30 m below Living floor')
193
207
  // Nothing shared across a wall or open side: the shared plate's top.
194
208
  expect(separateFloorOffset(scene(raised, keyedLanai, house, own), keyedLanai.id)).toBeCloseTo(
@@ -1,6 +1,7 @@
1
1
  import {
2
2
  type AnyNode,
3
3
  roomFloorChoices as coreRoomFloorChoices,
4
+ type PascalI18n,
4
5
  roomDrawnFloor,
5
6
  type SlabNode,
6
7
  type ZoneNode,
@@ -130,9 +131,26 @@ export function roomBuiltOn(nodes: Nodes, zoneId: string): RoomBuiltOnModel | nu
130
131
  export function separateFloorSummary(
131
132
  model: { current: { name: string }; shared: { name: string } | null; offset: number | null },
132
133
  length: (meters: number) => string,
134
+ i18n: PascalI18n,
133
135
  ): string {
134
136
  const { current, shared, offset } = model
135
137
  if (!shared || offset === null) return current.name
136
- if (Math.abs(offset) < 0.005) return `${current.name} · level with ${shared.name}`
137
- return `${current.name} · ${length(Math.abs(offset))} ${offset > 0 ? 'above' : 'below'} ${shared.name}`
138
+ const values = { current: current.name, shared: shared.name, distance: length(Math.abs(offset)) }
139
+ if (Math.abs(offset) < 0.005)
140
+ return i18n._({
141
+ id: 'editor.room.builtOn.levelWith',
142
+ message: '{current} · level with {shared}',
143
+ values,
144
+ })
145
+ return offset > 0
146
+ ? i18n._({
147
+ id: 'editor.room.builtOn.above',
148
+ message: '{current} · {distance} above {shared}',
149
+ values,
150
+ })
151
+ : i18n._({
152
+ id: 'editor.room.builtOn.below',
153
+ message: '{current} · {distance} below {shared}',
154
+ values,
155
+ })
138
156
  }
@@ -4,6 +4,7 @@ import {
4
4
  area,
5
5
  BuildingNode,
6
6
  CeilingNode,
7
+ ColumnNode,
7
8
  clearSceneHistory,
8
9
  createZone,
9
10
  DoorNode,
@@ -25,7 +26,12 @@ import { deleteConfirmationContent } from '../components/editor/delete-confirmat
25
26
  import { constructionActions, wallsSwitch } from '../components/ui/panels/room-construction-rows'
26
27
  import useDeleteConfirmation from '../store/use-delete-confirmation'
27
28
  import useEditor from '../store/use-editor'
28
- import { roomClearPolygon, roomConstructionState, roomFloorContents } from './room-construction'
29
+ import {
30
+ roomClearPolygon,
31
+ roomConstructionState,
32
+ roomFloorContents,
33
+ roomPorchFramingCounts,
34
+ } from './room-construction'
29
35
  import {
30
36
  addRoomConstruction,
31
37
  adoptExistingCeiling,
@@ -177,6 +183,27 @@ describe('room construction state', () => {
177
183
  expect(constructionActions(room, 'floor')).toEqual({ add: false, remove: true })
178
184
  })
179
185
 
186
+ test('porch framing counts the room’s generated parts by marker, across node kinds', () => {
187
+ const zone = nodes()[zoneId as AnyNodeId] as ZoneNode
188
+ const parts = [
189
+ ColumnNode.parse({ metadata: { porchOf: zoneId, porchPart: 'corner:0' } }),
190
+ ItemNode.parse({
191
+ ...item('item_side_post', 'Post', LEVEL, [0, 0, 0]),
192
+ metadata: { porchOf: zoneId, porchPart: 'side:0:1' },
193
+ }),
194
+ ColumnNode.parse({ metadata: { porchOf: zoneId, porchPart: 'beam:0' } }),
195
+ ColumnNode.parse({ metadata: { porchOf: 'zone_other', porchPart: 'corner:0' } }),
196
+ ColumnNode.parse({}),
197
+ ]
198
+ const level = nodes()[LEVEL as AnyNodeId] as LevelNode
199
+ const scene = {
200
+ ...nodes(),
201
+ ...Object.fromEntries(parts.map((part) => [part.id, part])),
202
+ [level.id]: { ...level, children: [...level.children, ...parts.map((part) => part.id)] },
203
+ }
204
+ expect(roomPorchFramingCounts(scene, zone)).toEqual({ posts: 2, beams: 1 })
205
+ })
206
+
180
207
  test('memoized per nodes identity', () => {
181
208
  expect(roomConstructionState(nodes(), zoneId)).toBe(roomConstructionState(nodes(), zoneId))
182
209
  expect(roomConstructionState(nodes(), 'zone_missing')).toBeNull()
@@ -383,3 +383,17 @@ export function roomConstructionState(
383
383
  memo.states.set(zoneId, state)
384
384
  return state
385
385
  }
386
+
387
+ export function roomPorchFramingCounts(nodes: StructureNodes, zone: ZoneNode) {
388
+ const level = zone.parentId ? nodes[zone.parentId] : undefined
389
+ let posts = 0
390
+ let beams = 0
391
+ for (const id of level?.type === 'level' ? level.children : []) {
392
+ const metadata = nodes[id]?.metadata
393
+ const part = metadata?.porchPart
394
+ if (metadata?.porchOf !== zone.id || typeof part !== 'string') continue
395
+ if (/^(corner|side):/.test(part)) posts++
396
+ else if (part.startsWith('beam:')) beams++
397
+ }
398
+ return { posts, beams }
399
+ }
@@ -17,6 +17,7 @@ import {
17
17
  import { useViewer } from '@pascal-app/viewer'
18
18
  import { createElement } from 'react'
19
19
  import { roomTransformSource } from '../components/editor/room-controls'
20
+ import { commitDimensionValue } from '../components/editor-2d/renderers/floorplan-dimension-edit-overlay'
20
21
  import { getRoomSelectionIndex } from '../hooks/use-selected-room'
21
22
  import useEditor from '../store/use-editor'
22
23
  import useInteractionScope from '../store/use-interaction-scope'
@@ -24,6 +25,7 @@ import { installImmediateAnimationFrames } from '../test-utils/immediate-animati
24
25
  import { withSelectionHarness } from '../test-utils/selection-harness'
25
26
  import { useGestureLifecycleOwner } from './gesture-lifecycle'
26
27
  import {
28
+ beginWallGroupPush,
27
29
  cancelRoomHandleDrag,
28
30
  commitRoomElevation,
29
31
  commitRoomPush,
@@ -34,7 +36,10 @@ import {
34
36
  roomFloorElevation,
35
37
  roomPushHandles,
36
38
  runRoomHandleDrag,
39
+ selectedWallDimensions,
40
+ useRoomHandleDrag,
37
41
  } from './room-handle-drag'
42
+ import { mergeSelectionDimensions } from './room-push-dimensions'
38
43
  import { applyRoomPlan, renameRoom } from './room-structure-commands'
39
44
  import {
40
45
  cancelRoomTransform,
@@ -376,6 +381,45 @@ describe('room handles (elevation, push/pull)', () => {
376
381
  expect(useScene.temporal.getState().pastStates).toHaveLength(1)
377
382
  })
378
383
 
384
+ test('a selected wall shows its room width and outside size, and typing either moves the wall', () => {
385
+ const east = () =>
386
+ Object.values(useScene.getState().nodes).find(
387
+ (n): n is WallNode => n.type === 'wall' && n.start[0] === n.end[0] && n.start[0] > 3,
388
+ )!
389
+ const wall = east()
390
+ const thickness = wall.thickness ?? 0.1
391
+ const dimensions = selectedWallDimensions(useScene.getState().nodes, wall, 'both')
392
+ const interior = dimensions.find((d) => d.faces === 'interior')!
393
+ const exterior = dimensions.find((d) => d.faces === 'exterior')!
394
+ // The 6 m room between centrelines: face to face inside, outer face to outer face outside.
395
+ expect(interior.distance).toBeCloseTo(6 - thickness)
396
+ expect(exterior.distance).toBeCloseTo(6 + thickness)
397
+ expect(selectedWallDimensions(useScene.getState().nodes, wall, 'interior')).toEqual([interior])
398
+ expect(selectedWallDimensions(useScene.getState().nodes, wall, 'exterior')).toEqual([exterior])
399
+
400
+ // Type 10' ID: the wall moves out until the room is exactly that wide, one undo step.
401
+ const target = (d: typeof interior) => ({
402
+ ownerNodeId: wall.id as AnyNodeId,
403
+ witnessStart: d.from,
404
+ witnessEnd: d.to,
405
+ value: d.distance,
406
+ })
407
+ expect(commitDimensionValue({ target: target(interior), nextLength: 3.048 })).toEqual({
408
+ ok: true,
409
+ })
410
+ // The west face sits half a thickness inside x = 0; the east centreline half outside its face.
411
+ expect(east().start[0]).toBeCloseTo(3.048 + thickness)
412
+ expect(useScene.temporal.getState().pastStates).toHaveLength(1)
413
+ const after = selectedWallDimensions(useScene.getState().nodes, east(), 'both')
414
+ expect(after.find((d) => d.faces === 'interior')!.distance).toBeCloseTo(3.048)
415
+
416
+ // Type the outside size: the same wall moves so the building reads it.
417
+ const outside = after.find((d) => d.faces === 'exterior')!
418
+ expect(commitDimensionValue({ target: target(outside), nextLength: 5 })).toEqual({ ok: true })
419
+ expect(east().start[0]).toBeCloseTo(5 - thickness)
420
+ expect(useScene.temporal.getState().pastStates).toHaveLength(2)
421
+ })
422
+
379
423
  test('a partial span splits off and moves only the room-facing piece, openings stay put', () => {
380
424
  // A 10 m north wall, of which this 6 m room faces only 0..0.6; a door
381
425
  // on the far stretch must stay where it is.
@@ -452,6 +496,102 @@ describe('room handles (elevation, push/pull)', () => {
452
496
  expect(host.start[1]).toBeCloseTo(4)
453
497
  expect(useScene.temporal.getState().pastStates).toHaveLength(1)
454
498
  })
499
+
500
+ const roomWalls = () => {
501
+ const walls = Object.values(useScene.getState().nodes).filter(
502
+ (n): n is WallNode => n.type === 'wall',
503
+ )
504
+ const at = (axis: 0 | 1, value: number) =>
505
+ walls.find(
506
+ (w) => Math.abs(w.start[axis] - value) < 1e-6 && Math.abs(w.end[axis] - value) < 1e-6,
507
+ )!
508
+ return { east: at(0, 6), west: at(0, 0), north: at(1, 4), south: at(1, 0) }
509
+ }
510
+
511
+ test('two selected walls move as their push arrows: parallel, still joined, one undo step', () => {
512
+ const { east, north, west, south } = roomWalls()
513
+ const push = beginWallGroupPush([east.id, north.id], LEVEL)
514
+ const nodes = useScene.getState().nodes
515
+ // A diagonal drag: each wall takes only the part along its own normal.
516
+ push.preview([1, 0.5])
517
+ expect(useLiveNodeOverrides.getState().overrides.get(east.id as AnyNodeId)).toBeDefined()
518
+ expect(useScene.getState().nodes).toBe(nodes)
519
+ expect(push.commit()).toBe(true)
520
+ const after = useScene.getState().nodes
521
+ const wall = (id: string) => after[id as AnyNodeId] as WallNode
522
+ for (const point of [wall(east.id).start, wall(east.id).end]) expect(point[0]).toBeCloseTo(7)
523
+ for (const point of [wall(north.id).start, wall(north.id).end])
524
+ expect(point[1]).toBeCloseTo(4.5)
525
+ const corner = (a: WallNode, b: WallNode) =>
526
+ [a.start, a.end].some((p) =>
527
+ [b.start, b.end].some((q) => Math.hypot(p[0] - q[0], p[1] - q[1]) < 1e-6),
528
+ )
529
+ // Their own corner, and the walls they meet, stay joined; the others keep their line.
530
+ expect(corner(wall(east.id), wall(north.id))).toBe(true)
531
+ expect(corner(wall(east.id), wall(south.id))).toBe(true)
532
+ expect(corner(wall(north.id), wall(west.id))).toBe(true)
533
+ for (const point of [wall(west.id).start, wall(west.id).end]) expect(point[0]).toBeCloseTo(0)
534
+ for (const point of [wall(south.id).start, wall(south.id).end]) expect(point[1]).toBeCloseTo(0)
535
+ expect(useScene.temporal.getState().pastStates).toHaveLength(1)
536
+ })
537
+
538
+ test("several selected walls show each one's dimensions, a room between two of them once", () => {
539
+ const { east, west, north } = roomWalls()
540
+ const thickness = east.thickness ?? 0.1
541
+ const nodes = useScene.getState().nodes
542
+ const dimensions = mergeSelectionDimensions(
543
+ [east, west, north].map((wall) => ({
544
+ ownerId: wall.id,
545
+ dimensions: selectedWallDimensions(nodes, wall, 'both'),
546
+ })),
547
+ )
548
+ const shown = (faces: 'interior' | 'exterior') =>
549
+ dimensions
550
+ .filter((d) => d.faces === faces)
551
+ .map((d) => [d.ownerId, Number(d.distance.toFixed(4))])
552
+ // East and west both measure the 6 m room and the building across it: once each, on east.
553
+ expect(shown('interior')).toEqual([
554
+ [east.id, Number((6 - thickness).toFixed(4))],
555
+ [north.id, Number((4 - thickness).toFixed(4))],
556
+ ])
557
+ expect(shown('exterior')).toEqual([
558
+ [east.id, Number((6 + thickness).toFixed(4))],
559
+ [north.id, Number((4 + thickness).toFixed(4))],
560
+ ])
561
+ expect(new Set(dimensions.map((d) => d.key)).size).toBe(dimensions.length)
562
+
563
+ // Typing one moves the wall it runs from.
564
+ const depth = dimensions.find((d) => d.ownerId === north.id && d.faces === 'interior')!
565
+ expect(
566
+ commitDimensionValue({
567
+ target: {
568
+ ownerNodeId: north.id as AnyNodeId,
569
+ witnessStart: depth.from,
570
+ witnessEnd: depth.to,
571
+ value: depth.distance,
572
+ },
573
+ nextLength: 5,
574
+ }),
575
+ ).toEqual({ ok: true })
576
+ const moved = useScene.getState().nodes[north.id as AnyNodeId] as WallNode
577
+ expect(moved.start[1]).toBeCloseTo(5 + thickness)
578
+ })
579
+
580
+ test('a multi-wall move shows the room widths live, as a single wall push does', () => {
581
+ const { east, north, west } = roomWalls()
582
+ const thickness = east.thickness ?? 0.1
583
+ const push = beginWallGroupPush([east.id, west.id, north.id], LEVEL)
584
+ push.preview([1, 0.5])
585
+ const drag = useRoomHandleDrag.getState().drag
586
+ expect(drag?.kind).toBe('wall-group-push')
587
+ const widths = (drag?.kind === 'wall-group-push' ? drag.dimensions : []).map((d) => d.distance)
588
+ // East and west both moved 1 m: the 6 m room is still 6 m wide, shown once; north went 0.5 m out.
589
+ expect(widths.filter((w) => Math.abs(w - (6 - thickness)) < 1e-6)).toHaveLength(1)
590
+ expect(widths.some((w) => Math.abs(w - (4.5 - thickness)) < 1e-6)).toBe(true)
591
+ push.cancel()
592
+ expect(useRoomHandleDrag.getState().drag).toBeNull()
593
+ expect(useLiveNodeOverrides.getState().overrides.size).toBe(0)
594
+ })
455
595
  })
456
596
 
457
597
  describe('room gesture ownership', () => {