@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,10 +1,19 @@
1
- import { type IconRef, nodeRegistry, type TerrainVerb } from '@pascal-app/core'
1
+ import {
2
+ type IconRef,
3
+ type MessageDescriptor,
4
+ nodeRegistry,
5
+ type TerrainVerb,
6
+ } from '@pascal-app/core'
7
+ import { nodeLabelDescriptor } from '../i18n/node-messages'
2
8
  import type { ContinuationMode } from './continuation'
3
9
  import { getWallDrawVariantInfo, wallDrawVariantOf } from './wall-draw-variant'
4
10
 
5
11
  /** The contextual helper panel's header: what is in hand, and its key. */
6
12
  export type HudTitle = {
7
- label: string
13
+ /** May use `{kind}`, filled with the translated, lower-cased name of `kind`. */
14
+ label: MessageDescriptor
15
+ /** The node kind whose display name the label refers to. */
16
+ kind?: { kind: string; english: string }
8
17
  icon?: IconRef
9
18
  shortcut?: string
10
19
  }
@@ -18,54 +27,63 @@ const TOOL_SHORTCUTS: Record<string, string> = {
18
27
  }
19
28
 
20
29
  export const SELECT_HUD_TITLE: HudTitle = {
21
- label: 'Select',
30
+ label: /* i18n */ { id: 'editor.lib.hudTitle.select', message: 'Select' },
22
31
  icon: url('/icons/select.webp'),
23
32
  shortcut: 'V',
24
33
  }
25
- export const PAINT_HUD_TITLE: HudTitle = { label: 'Paint', icon: url('/icons/paint.webp') }
34
+ export const PAINT_HUD_TITLE: HudTitle = {
35
+ label: /* i18n */ { id: 'editor.lib.hudTitle.paint', message: 'Paint' },
36
+ icon: url('/icons/paint.webp'),
37
+ }
26
38
  export const ERASE_HUD_TITLE: HudTitle = {
27
- label: 'Erase',
39
+ label: /* i18n */ { id: 'editor.lib.hudTitle.erase', message: 'Erase' },
28
40
  icon: { kind: 'iconify', name: 'lucide:eraser' },
29
41
  }
30
42
  export const PICK_MATERIAL_HUD_TITLE: HudTitle = {
31
- label: 'Pick material',
43
+ label: /* i18n */ { id: 'editor.lib.hudTitle.pickMaterial', message: 'Pick material' },
32
44
  icon: { kind: 'iconify', name: 'lucide:pipette' },
33
45
  }
34
- export const ROTATE_HUD_TITLE: HudTitle = { label: 'Rotate', icon: url('/icons/rotate.webp') }
46
+ export const ROTATE_HUD_TITLE: HudTitle = {
47
+ label: /* i18n */ { id: 'editor.lib.hudTitle.rotate', message: 'Rotate' },
48
+ icon: url('/icons/rotate.webp'),
49
+ }
35
50
  export const MOVE_SELECTION_HUD_TITLE: HudTitle = {
36
- label: 'Move selection',
51
+ label: /* i18n */ { id: 'editor.lib.hudTitle.moveSelection', message: 'Move selection' },
37
52
  icon: url('/icons/pan.webp'),
38
53
  }
39
- export const MOVE_ROOM_HUD_TITLE: HudTitle = { label: 'Move room', icon: url('/icons/room.webp') }
54
+ export const MOVE_ROOM_HUD_TITLE: HudTitle = {
55
+ label: /* i18n */ { id: 'editor.lib.hudTitle.moveRoom', message: 'Move room' },
56
+ icon: url('/icons/room.webp'),
57
+ }
40
58
  export const RESIZE_HUD_TITLE: HudTitle = {
41
- label: 'Resize',
59
+ label: /* i18n */ { id: 'editor.lib.hudTitle.resize', message: 'Resize' },
42
60
  icon: url('/icons/resize.webp'),
43
61
  }
44
62
  export const DIVIDE_ROOM_HUD_TITLE: HudTitle = {
45
- label: 'Divide room',
63
+ label: /* i18n */ { id: 'editor.lib.hudTitle.divideRoom', message: 'Divide room' },
46
64
  icon: url('/icons/divide-room.webp'),
47
65
  }
48
66
  export const MEZZANINE_HUD_TITLE: HudTitle = {
49
- label: 'Mezzanine',
67
+ label: /* i18n */ { id: 'editor.lib.hudTitle.mezzanine', message: 'Mezzanine' },
50
68
  icon: url('/icons/mezzanine.webp'),
51
69
  }
52
70
  export const OPENING_HUD_TITLE: HudTitle = {
53
- label: 'Cut opening',
71
+ label: /* i18n */ { id: 'editor.lib.hudTitle.cutOpening', message: 'Cut opening' },
54
72
  icon: { kind: 'iconify', name: 'lucide:square-dashed' },
55
73
  }
56
74
  export const TERRACE_HUD_TITLE: HudTitle = {
57
- label: 'Terrace',
75
+ label: /* i18n */ { id: 'editor.lib.hudTitle.terrace', message: 'Terrace' },
58
76
  icon: url('/icons/floor.webp'),
59
77
  }
60
78
  export const SMART_MEASURE_HUD_TITLE: HudTitle = {
61
- label: 'Smart measure',
79
+ label: /* i18n */ { id: 'editor.lib.hudTitle.smartMeasure', message: 'Smart measure' },
62
80
  icon: url('/icons/measure.webp'),
63
81
  shortcut: 'M',
64
82
  }
65
83
 
66
84
  const PAINT_REGION_LABELS = {
67
- rectangle: 'Paint a rectangle',
68
- polygon: 'Paint a shape',
85
+ rectangle: /* i18n */ { id: 'editor.lib.hudTitle.paintARectangle', message: 'Paint a rectangle' },
86
+ polygon: /* i18n */ { id: 'editor.lib.hudTitle.paintAShape', message: 'Paint a shape' },
69
87
  } as const
70
88
 
71
89
  export function paintRegionHudTitle(mode: keyof typeof PAINT_REGION_LABELS): HudTitle {
@@ -75,22 +93,28 @@ export function paintRegionHudTitle(mode: keyof typeof PAINT_REGION_LABELS): Hud
75
93
  export function terrainHudTitle(verb: TerrainVerb): HudTitle {
76
94
  const label =
77
95
  verb === 'raise'
78
- ? 'Raise terrain'
96
+ ? /* i18n */ { id: 'editor.lib.hudTitle.raiseTerrain', message: 'Raise terrain' }
79
97
  : verb === 'lower'
80
- ? 'Lower terrain'
98
+ ? /* i18n */ { id: 'editor.lib.hudTitle.lowerTerrain', message: 'Lower terrain' }
81
99
  : verb === 'flatten'
82
- ? 'Level terrain'
83
- : 'Smooth terrain'
100
+ ? /* i18n */ { id: 'editor.lib.hudTitle.levelTerrain', message: 'Level terrain' }
101
+ : /* i18n */ { id: 'editor.lib.hudTitle.smoothTerrain', message: 'Smooth terrain' }
84
102
  return { label, icon: url(`/icons/terrain-${verb}.webp`) }
85
103
  }
86
104
 
105
+ const KIND_VERBS = {
106
+ edit: /* i18n */ { id: 'editor.lib.hudTitle.editKind', message: 'Edit {kind}' },
107
+ move: /* i18n */ { id: 'editor.lib.hudTitle.moveKind', message: 'Move {kind}' },
108
+ split: /* i18n */ { id: 'editor.lib.hudTitle.splitKind', message: 'Split {kind}' },
109
+ } as const
110
+
87
111
  /** A kind's registry name and icon, optionally behind a verb ("Move door"). */
88
- export function nodeKindHudTitle(kind: string, verb?: string): HudTitle | null {
112
+ export function nodeKindHudTitle(kind: string, verb?: keyof typeof KIND_VERBS): HudTitle | null {
89
113
  const presentation = nodeRegistry.get(kind)?.presentation
90
114
  if (!presentation) return null
91
- const name = presentation.label
92
115
  return {
93
- label: verb ? `${verb} ${name.toLowerCase()}` : name,
116
+ label: verb ? KIND_VERBS[verb] : nodeLabelDescriptor(kind, presentation.label),
117
+ kind: verb ? { kind, english: presentation.label } : undefined,
94
118
  icon: presentation.icon,
95
119
  shortcut: verb ? undefined : TOOL_SHORTCUTS[kind],
96
120
  }
@@ -1,5 +1,5 @@
1
1
  import { describe, expect, test } from 'bun:test'
2
- import { type AnyNode, sceneRegistry } from '@pascal-app/core'
2
+ import { type AnyNode, DoorNode, LevelNode, sceneRegistry, WallNode } from '@pascal-app/core'
3
3
  import * as THREE from 'three'
4
4
  import { collectIfcMeshes, exportPreparedSceneToIfc, ifcFileName } from './ifc-export'
5
5
 
@@ -172,3 +172,43 @@ test('material groups retain their surface owner and semantic subpart', () => {
172
172
  geometry.dispose()
173
173
  material.dispose()
174
174
  })
175
+
176
+ for (const rotationY of [0, Math.PI]) {
177
+ test(`IFC opening includes fixed lights and preserves leaf placement facing ${rotationY}`, () => {
178
+ const level = LevelNode.parse({ id: 'level_entry' })
179
+ const wall = WallNode.parse({
180
+ id: 'wall_entry',
181
+ parentId: level.id,
182
+ start: [0, 0],
183
+ end: [5, 0],
184
+ height: 3,
185
+ thickness: 0.2,
186
+ })
187
+ const door = DoorNode.parse({
188
+ id: 'door_entry',
189
+ parentId: wall.id,
190
+ wallId: wall.id,
191
+ position: [2.5, 1.05, 0],
192
+ rotation: [0, rotationY, 0],
193
+ width: 1,
194
+ height: 2.1,
195
+ sidelightLeft: 0.3,
196
+ sidelightRight: 0.5,
197
+ transomHeight: 0.4,
198
+ })
199
+ const { data } = exportPreparedSceneToIfc(
200
+ new THREE.Group(),
201
+ { [level.id]: level, [wall.id]: wall, [door.id]: door },
202
+ {},
203
+ )
204
+ expect(data).toMatch(/IFCRECTANGLEPROFILEDEF\([^\n]*,1\.8,0\.4\)/)
205
+ expect(data).toMatch(/IFCEXTRUDEDAREASOLID\([^\n]*,2\.5\)/)
206
+ expect(data).toMatch(/IFCDOOR\([^\n]*,2\.1,1\.,\.DOOR\./)
207
+ expect(data).toContain(
208
+ rotationY === 0 ? 'IFCCARTESIANPOINT((2.6,0.,0.))' : 'IFCCARTESIANPOINT((2.4,0.,0.))',
209
+ )
210
+ expect(data).toContain(
211
+ rotationY === 0 ? 'IFCCARTESIANPOINT((-0.1,0.,0.))' : 'IFCCARTESIANPOINT((0.1,0.,0.))',
212
+ )
213
+ })
214
+ }
@@ -1,9 +1,9 @@
1
1
  import { describe, expect, test } from 'bun:test'
2
2
  import {
3
3
  type InspectorCardMode,
4
+ openExtension,
4
5
  resolveActiveExtension,
5
6
  toggleCard,
6
- toggleExtension,
7
7
  } from './inspector-card-mode'
8
8
 
9
9
  const collapsed: InspectorCardMode = { collapsed: true, activeExtensionId: null }
@@ -29,27 +29,6 @@ describe('toggleCard (chevron / header press)', () => {
29
29
  })
30
30
  })
31
31
 
32
- describe('toggleExtension (header icon press)', () => {
33
- test('folded card opens straight into extension-only mode', () => {
34
- expect(toggleExtension(collapsed, 'bones:eng')).toEqual(engineering)
35
- })
36
-
37
- test('regular expanded card swaps to extension-only mode', () => {
38
- expect(toggleExtension(regular, 'bones:eng')).toEqual(engineering)
39
- })
40
-
41
- test('pressing the ACTIVE extension icon again returns to regular mode', () => {
42
- expect(toggleExtension(engineering, 'bones:eng')).toEqual(regular)
43
- })
44
-
45
- test('pressing another extension icon switches extension modes directly', () => {
46
- expect(toggleExtension(engineering, 'other:ext')).toEqual({
47
- collapsed: false,
48
- activeExtensionId: 'other:ext',
49
- })
50
- })
51
- })
52
-
53
32
  describe('resolveActiveExtension', () => {
54
33
  const bones = { id: 'bones:eng' }
55
34
  const other = { id: 'other:ext' }
@@ -71,20 +50,13 @@ describe('resolveActiveExtension', () => {
71
50
  })
72
51
 
73
52
  describe('full user flows (QA script)', () => {
74
- test('chevron → regular only; fold; icon → extension only; icon again → regular', () => {
53
+ test('chevron → regular only; fold; extension action → extension only; chevron → regular', () => {
75
54
  let mode = collapsed
76
55
  mode = toggleCard(mode) // chevron
77
56
  expect(mode).toEqual(regular)
78
57
  mode = toggleCard(mode) // fold
79
58
  expect(mode).toEqual(collapsed)
80
- mode = toggleExtension(mode, 'bones:eng') // bones icon
81
- expect(mode).toEqual(engineering)
82
- mode = toggleExtension(mode, 'bones:eng') // bones icon again
83
- expect(mode).toEqual(regular)
84
- })
85
-
86
- test('extension mode exits via the chevron too', () => {
87
- let mode = toggleExtension(collapsed, 'bones:eng')
59
+ mode = openExtension('bones:eng') // the action-menu button
88
60
  expect(mode).toEqual(engineering)
89
61
  mode = toggleCard(mode) // chevron
90
62
  expect(mode).toEqual(regular)
@@ -14,9 +14,9 @@
14
14
  * - chevron / header press ({@link toggleCard}): collapsed → regular,
15
15
  * regular → collapsed, extension → regular (exit extension mode first,
16
16
  * stay expanded);
17
- * - extension icon press ({@link toggleExtension}): enters that
18
- * extension's mode from anywhere (expanding a folded card); pressing
19
- * the ACTIVE extension's icon again returns to regular.
17
+ * - an extension's action-menu button ({@link openExtension}): enters that
18
+ * extension's mode from anywhere (expanding a folded card); pressed while
19
+ * that extension is already showing, it keeps it showing.
20
20
  */
21
21
 
22
22
  export interface InspectorCardMode {
@@ -36,13 +36,8 @@ export function toggleCard(mode: InspectorCardMode): InspectorCardMode {
36
36
  return { collapsed: true, activeExtensionId: null }
37
37
  }
38
38
 
39
- /** Header extension-icon press. */
40
- export function toggleExtension(mode: InspectorCardMode, extensionId: string): InspectorCardMode {
41
- // The active extension's icon pressed again → back to the regular controls.
42
- if (!mode.collapsed && mode.activeExtensionId === extensionId) {
43
- return { collapsed: false, activeExtensionId: null }
44
- }
45
- // Anywhere else (folded, regular, another extension) → this extension only.
39
+ /** An extension's action-menu button: opens the card on that extension, or keeps it there. */
40
+ export function openExtension(extensionId: string): InspectorCardMode {
46
41
  return { collapsed: false, activeExtensionId: extensionId }
47
42
  }
48
43
 
@@ -42,3 +42,16 @@ test('without usable storage the panel opens folded and expanding still works',
42
42
  useInspectorExpanded.getState().setExpanded(true)
43
43
  expect(useInspectorExpanded.getState().expanded).toBe(true)
44
44
  })
45
+
46
+ test('an extension opened from the action menu expands the card; folding it forgets the extension', async () => {
47
+ stubStorage({ getItem: () => null, setItem: () => {} })
48
+ const { useInspectorExpanded } = await load()
49
+ useInspectorExpanded.getState().applyMode({ collapsed: false, activeExtensionId: 'bones:eng' })
50
+ expect(useInspectorExpanded.getState()).toMatchObject({
51
+ expanded: true,
52
+ activeExtensionId: 'bones:eng',
53
+ })
54
+ useInspectorExpanded.getState().setExpanded(false)
55
+ useInspectorExpanded.getState().setExpanded(true)
56
+ expect(useInspectorExpanded.getState().activeExtensionId).toBeNull()
57
+ })
@@ -1,4 +1,5 @@
1
1
  import { create } from 'zustand'
2
+ import type { InspectorCardMode } from './inspector-card-mode'
2
3
 
3
4
  // One choice for every right-side inspector panel (room, wall, item…): opened
4
5
  // expanded or folded to its header, kept per viewer until they fold it again.
@@ -26,11 +27,22 @@ function writeExpanded(expanded: boolean) {
26
27
 
27
28
  export const useInspectorExpanded = create<{
28
29
  expanded: boolean
30
+ /**
31
+ * The plugin inspector extension filling the expanded card (see `inspector-card-mode.ts`),
32
+ * shared so the action menu can open it. Not remembered: folding the card forgets it.
33
+ */
34
+ activeExtensionId: string | null
29
35
  setExpanded: (expanded: boolean) => void
36
+ applyMode: (mode: InspectorCardMode) => void
30
37
  }>((set) => ({
31
38
  expanded: readExpanded(),
39
+ activeExtensionId: null,
32
40
  setExpanded: (expanded) => {
33
41
  writeExpanded(expanded)
34
- set({ expanded })
42
+ set(expanded ? { expanded } : { expanded, activeExtensionId: null })
43
+ },
44
+ applyMode: ({ collapsed, activeExtensionId }) => {
45
+ writeExpanded(!collapsed)
46
+ set({ expanded: !collapsed, activeExtensionId: collapsed ? null : activeExtensionId })
35
47
  },
36
48
  }))
@@ -28,6 +28,7 @@ export type ReshapeKind =
28
28
  | 'tangent'
29
29
  // A kind's own reshape (wall 'split'): mounted from `def.affordanceTools[reshape]`.
30
30
  | 'split'
31
+ | 'trim'
31
32
 
32
33
  export type InteractionScope =
33
34
  | { kind: 'idle' }
@@ -111,6 +111,13 @@ test('modifier chords stay shortcuts, and typing in a field never pans', () => {
111
111
  expect(acceptsKeyboardPan(key({ target: new Input() }))).toBe(false)
112
112
  })
113
113
 
114
+ test('Shift with W, S or D arms a tool instead of panning; Shift with A still pans', () => {
115
+ for (const code of ['KeyW', 'KeyS', 'KeyD'])
116
+ expect(acceptsKeyboardPan(key({ code, shiftKey: true }))).toBe(false)
117
+ expect(acceptsKeyboardPan(key({ code: 'KeyA', shiftKey: true }))).toBe(true)
118
+ expect(acceptsKeyboardPan(key({ code: 'KeyD' }))).toBe(true)
119
+ })
120
+
114
121
  test('cruise speed scales with the visible width within fixed bounds', () => {
115
122
  expect(keyboardPanSpeed(0)).toBe(0.5)
116
123
  expect(keyboardPanSpeed(20)).toBeCloseTo(6)
@@ -2,6 +2,8 @@
2
2
  // move the same way. Keys match by physical position (`event.code`): the
3
3
  // cluster stays under the left hand on any layout (Z/Q/S/D on AZERTY).
4
4
 
5
+ import { claimsKeyboardPan } from './tool-shortcuts'
6
+
5
7
  export type KeyboardPanState = {
6
8
  forward: boolean
7
9
  backward: boolean
@@ -79,10 +81,15 @@ export function clearKeyboardPanKeys(state: KeyboardPanState) {
79
81
  state.right = false
80
82
  }
81
83
 
82
- /** Pan keys are ignored with a modifier held (shortcuts) or while typing. */
84
+ /**
85
+ * Pan keys are ignored with a modifier held (shortcuts), for a Shift chord that
86
+ * arms a tool (Shift+W window, Shift+S stairs, Shift+D door), and while typing.
87
+ */
83
88
  export function acceptsKeyboardPan(event: KeyboardEvent) {
84
89
  return (
85
- !(event.metaKey || event.ctrlKey || event.altKey) && !isEditableKeyboardTarget(event.target)
90
+ !(event.metaKey || event.ctrlKey || event.altKey) &&
91
+ !claimsKeyboardPan(event) &&
92
+ !isEditableKeyboardTarget(event.target)
86
93
  )
87
94
  }
88
95
 
@@ -0,0 +1,22 @@
1
+ import { expect, test } from 'bun:test'
2
+ import { ROOF_BUILD_UP_PRESETS, roofBuildUpDepths } from '@pascal-app/core'
3
+ import { formatLayerDepth, formatLayerThickness } from './layer-thickness'
4
+
5
+ const IN = 0.0254
6
+
7
+ // The roof layers read DECK 10-3/16" beside COVERING 0.54": a covering of shingle and underlayment
8
+ // is no exact 32nd, so it fell to decimals. Summed depths read as fractions, to the nearest 1/16".
9
+ test('summed depths read as fractions to the 1/16", like the deck beside them', () => {
10
+ expect(formatLayerDepth(0.54 * IN, 'imperial')).toBe('9/16"')
11
+ for (const preset of ROOF_BUILD_UP_PRESETS) {
12
+ const { deck, covering } = roofBuildUpDepths(preset.buildUp)
13
+ for (const depth of [deck, covering])
14
+ expect(formatLayerDepth(depth, 'imperial')).toMatch(/^(\d+|\d+\/\d+|\d+-\d+\/\d+)"$/)
15
+ }
16
+ expect(formatLayerDepth(0.0137, 'metric')).toBe('13.7 mm')
17
+ })
18
+
19
+ test('a typed thickness still reads back as typed', () => {
20
+ expect(formatLayerThickness((7 / 16) * IN, 'imperial')).toBe('7/16"')
21
+ expect(formatLayerThickness(0.0111, 'imperial')).toBe('0.437"')
22
+ })
@@ -0,0 +1,79 @@
1
+ import type { LinearUnit } from '@pascal-app/core'
2
+
3
+ const METRES_PER_INCH = 0.0254
4
+
5
+ /**
6
+ * Imperial-friendly thickness input. Accepts `5/8`, `5/8"`, `7/16 in`,
7
+ * `1-1/4"`, `1 1/2"`, `12mm`, `0.012m`. A bare number means INCHES in imperial
8
+ * display and MILLIMETRES in metric — layers are millimetre-scale and
9
+ * typing `0.0127` for half-inch board is nobody's idea of usable.
10
+ */
11
+ export function parseLayerThickness(raw: string, unit: LinearUnit): number | null {
12
+ const text = raw.trim().toLowerCase().replace(/["”]/g, ' in ')
13
+ if (!text) return null
14
+ const explicit = /(mm|cm|m|in|ft|')\s*$/.exec(text)
15
+ const suffix = explicit?.[1]
16
+ const body = (suffix ? text.slice(0, explicit?.index) : text).trim()
17
+
18
+ // `1-1/4` / `1 1/2` / `5/8` / `0.4375`
19
+ const mixed = /^(\d+(?:\.\d+)?)[\s-]+(\d+)\s*\/\s*(\d+)$/.exec(body)
20
+ const fraction = /^(\d+)\s*\/\s*(\d+)$/.exec(body)
21
+ let value: number
22
+ if (mixed) {
23
+ value = Number(mixed[1]) + Number(mixed[2]) / Number(mixed[3])
24
+ } else if (fraction) {
25
+ value = Number(fraction[1]) / Number(fraction[2])
26
+ } else {
27
+ value = Number(body)
28
+ }
29
+ if (!Number.isFinite(value) || value < 0) return null
30
+
31
+ switch (suffix) {
32
+ case 'mm':
33
+ return value / 1000
34
+ case 'cm':
35
+ return value / 100
36
+ case 'm':
37
+ return value
38
+ case 'in':
39
+ return value * METRES_PER_INCH
40
+ case 'ft':
41
+ case "'":
42
+ return value * 12 * METRES_PER_INCH
43
+ default:
44
+ return unit === 'imperial' ? value * METRES_PER_INCH : value / 1000
45
+ }
46
+ }
47
+
48
+ /**
49
+ * An exact 32nd of an inch as its fraction, so 7/16 in (0.0111125 m) reads back as `7/16"`; any
50
+ * other value as decimal inches (0.0111 m is `0.437"`).
51
+ */
52
+ export function formatLayerThickness(metres: number, unit: LinearUnit): string {
53
+ if (unit !== 'imperial') return `${Math.round(metres * 1000 * 10) / 10} mm`
54
+ const inchValue = metres / METRES_PER_INCH
55
+ const parts = Math.round(inchValue * 32)
56
+ if (Math.abs(inchValue * 32 - parts) > 1e-6) return `${Math.round(inchValue * 1000) / 1000}"`
57
+ const whole = Math.floor(parts / 32)
58
+ let numerator = parts % 32
59
+ let denominator = 32
60
+ while (numerator % 2 === 0 && numerator > 0) {
61
+ numerator /= 2
62
+ denominator /= 2
63
+ }
64
+ if (numerator === 0) return `${whole}"`
65
+ return whole > 0 ? `${whole}-${numerator}/${denominator}"` : `${numerator}/${denominator}"`
66
+ }
67
+
68
+ /**
69
+ * A summed depth to read, not type back (a plane's deck or covering, a ceiling's thickness): in
70
+ * imperial to the nearest 1/16" as a fraction, as the lumber and panels it adds up read, so a
71
+ * covering of 0.54" reads 9/16" beside a 10-3/16" deck.
72
+ */
73
+ export function formatLayerDepth(metres: number, unit: LinearUnit): string {
74
+ if (unit !== 'imperial') return formatLayerThickness(metres, unit)
75
+ return formatLayerThickness(
76
+ (Math.round((metres / METRES_PER_INCH) * 16) / 16) * METRES_PER_INCH,
77
+ unit,
78
+ )
79
+ }
@@ -3,6 +3,7 @@ import {
3
3
  type AnyNode,
4
4
  type AnyNodeId,
5
5
  BuildingNode,
6
+ ElevatorNode,
6
7
  LevelNode,
7
8
  RoofNode,
8
9
  useScene,
@@ -27,10 +28,14 @@ beforeEach(() => {
27
28
  metadata: { role: 'roof' },
28
29
  })
29
30
  const roof = RoofNode.parse({ id: 'roof_main', parentId: roofLevel.id })
30
- const building = BuildingNode.parse({ id: BUILDING, children: [GROUND, roofLevel.id] })
31
+ const elevator = ElevatorNode.parse({ id: 'elevator_lift', parentId: BUILDING })
32
+ const building = BuildingNode.parse({
33
+ id: BUILDING,
34
+ children: [GROUND, roofLevel.id, elevator.id],
35
+ })
31
36
  useScene.setState({
32
37
  nodes: Object.fromEntries(
33
- [building, ground, wall, roofLevel, roof].map((node) => [node.id, node as AnyNode]),
38
+ [building, ground, wall, roofLevel, roof, elevator].map((node) => [node.id, node as AnyNode]),
34
39
  ),
35
40
  rootNodeIds: [BUILDING],
36
41
  } as never)
@@ -54,4 +59,15 @@ describe('selectPlacedNode', () => {
54
59
  selectedIds: ['wall_a'],
55
60
  })
56
61
  })
62
+
63
+ test('a building-parented node keeps the floor in view and adopts its building', () => {
64
+ useViewer.getState().resetSelection()
65
+ useViewer.getState().setSelection({ levelId: GROUND })
66
+ selectPlacedNode('elevator_lift' as AnyNodeId)
67
+ expect(useViewer.getState().selection).toMatchObject({
68
+ buildingId: BUILDING,
69
+ levelId: GROUND,
70
+ selectedIds: ['elevator_lift'],
71
+ })
72
+ })
57
73
  })
@@ -1,4 +1,4 @@
1
- import type { AnyNodeId, BuildingNode, LevelNode } from '@pascal-app/core'
1
+ import type { AnyNode, AnyNodeId, BuildingNode, LevelNode } from '@pascal-app/core'
2
2
  import { findLevelAncestorId, useScene } from '@pascal-app/core'
3
3
  import { useViewer } from '@pascal-app/viewer'
4
4
 
@@ -30,17 +30,40 @@ export function deleteLevelWithFallbackSelection(levelId: AnyNodeId) {
30
30
  }
31
31
  }
32
32
 
33
+ function findBuildingAncestorId(
34
+ nodeId: AnyNodeId,
35
+ nodes: Record<string, AnyNode>,
36
+ ): BuildingNode['id'] | null {
37
+ let current: AnyNode | undefined = nodes[nodeId]
38
+ for (let guard = 0; current && guard < 16; guard++) {
39
+ if (current.type === 'building') return current.id
40
+ current = current.parentId ? nodes[current.parentId as AnyNodeId] : undefined
41
+ }
42
+ return null
43
+ }
44
+
33
45
  /**
34
46
  * Selects a node a tool just made, on its own level when that is not the one in view: a roof drawn
35
47
  * over a storey's walls lands on the level above, which the editor hides while the storey is
36
- * selected, so selecting it there alone would leave it out of sight.
48
+ * selected, so selecting it there alone would leave it out of sight. A node parented to the
49
+ * building itself (an elevator) has no level, so the floor in view stays active when it belongs
50
+ * to that building.
37
51
  */
38
52
  export function selectPlacedNode(nodeId: AnyNodeId) {
39
- const levelId = findLevelAncestorId(nodeId, useScene.getState().nodes)
53
+ const { nodes } = useScene.getState()
54
+ const levelId = findLevelAncestorId(nodeId, nodes) as LevelNode['id'] | null
55
+ const buildingId = findBuildingAncestorId(nodeId, nodes)
40
56
  const { selection, setSelection } = useViewer.getState()
41
- setSelection(
42
- levelId && levelId !== selection.levelId
43
- ? { levelId: levelId as LevelNode['id'], selectedIds: [nodeId] }
44
- : { selectedIds: [nodeId] },
45
- )
57
+ const updates: Partial<typeof selection> = { selectedIds: [nodeId] }
58
+ if (buildingId && buildingId !== selection.buildingId) {
59
+ updates.buildingId = buildingId
60
+ updates.levelId =
61
+ levelId ??
62
+ (selection.levelId && findBuildingAncestorId(selection.levelId, nodes) === buildingId
63
+ ? selection.levelId
64
+ : null)
65
+ } else if (levelId && levelId !== selection.levelId) {
66
+ updates.levelId = levelId
67
+ }
68
+ setSelection(updates)
46
69
  }
@@ -4,7 +4,7 @@ import {
4
4
  linearUnitToMeters,
5
5
  type MetricNotation,
6
6
  metersToLinearUnit,
7
- } from './measurements'
7
+ } from '@pascal-app/core'
8
8
 
9
9
  export function getLinearDisplay(
10
10
  unit: string,
@@ -6,6 +6,7 @@ import {
6
6
  createZoneDivisionContext,
7
7
  generateId,
8
8
  nodeRegistry,
9
+ roomFloorElevation,
9
10
  type SlabNode,
10
11
  useLiveNodeOverrides,
11
12
  useScene,
@@ -29,7 +30,6 @@ import { roomRelativeFloorHeight, stepRoomFloorElevation } from './room-construc
29
30
  import {
30
31
  commitRoomElevation,
31
32
  mezzanineEdgeHandles,
32
- roomFloorElevation,
33
33
  roomPushHandles,
34
34
  runFloorplanMezzanineEdge,
35
35
  runFloorplanRoomPush,