@pascal-app/editor 1.0.5 → 1.0.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (317) hide show
  1. package/package.json +20 -17
  2. package/src/components/editor/action-menu-button.tsx +4 -1
  3. package/src/components/editor/alignment-3d-guide-layer.tsx +102 -11
  4. package/src/components/editor/bake-thumbnail.tsx +3 -2
  5. package/src/components/editor/custom-camera-controls.tsx +20 -8
  6. package/src/components/editor/elevation-3d-guide-layer.tsx +1 -1
  7. package/src/components/editor/first-person-controls.tsx +72 -61
  8. package/src/components/editor/floating-action-menu.tsx +27 -9
  9. package/src/components/editor/floating-building-action-menu.tsx +1 -1
  10. package/src/components/editor/floorplan-panel.test.ts +220 -0
  11. package/src/components/editor/floorplan-panel.tsx +285 -132
  12. package/src/components/editor/footprint-height-handle.tsx +1 -1
  13. package/src/components/editor/grid.tsx +24 -15
  14. package/src/components/editor/group-actions.ts +19 -1
  15. package/src/components/editor/group-floating-action-menu.tsx +1 -1
  16. package/src/components/editor/group-move-3d.ts +27 -2
  17. package/src/components/editor/group-selection-box-3d.tsx +4 -1
  18. package/src/components/editor/group-transform-shared.ts +20 -0
  19. package/src/components/editor/index.tsx +130 -20
  20. package/src/components/editor/inspector-extension-actions.tsx +36 -0
  21. package/src/components/editor/localized-html.tsx +22 -0
  22. package/src/components/editor/node-action-menu.tsx +46 -12
  23. package/src/components/editor/node-arrow-handles.tsx +1 -2
  24. package/src/components/editor/open-wall-ends-3d-layer.tsx +1 -1
  25. package/src/components/editor/opening-guides-3d-layer.tsx +1 -1
  26. package/src/components/editor/registry-action-contributions.test.tsx +67 -6
  27. package/src/components/editor/registry-action-contributions.tsx +115 -15
  28. package/src/components/editor/riser-diagram-panel.tsx +59 -9
  29. package/src/components/editor/room-controls.tsx +1 -1
  30. package/src/components/editor/room-handles-2d.tsx +156 -6
  31. package/src/components/editor/room-handles.tsx +241 -51
  32. package/src/components/editor/room-highlight.tsx +2 -0
  33. package/src/components/editor/room-labels.tsx +29 -15
  34. package/src/components/editor/selection-manager.tsx +15 -102
  35. package/src/components/editor/separator-end-grip.tsx +74 -0
  36. package/src/components/editor/site-edge-labels.tsx +1 -1
  37. package/src/components/editor/thumbnail-generator.test.ts +15 -6
  38. package/src/components/editor/thumbnail-generator.tsx +26 -1
  39. package/src/components/editor/use-floorplan-background-placement.ts +55 -52
  40. package/src/components/editor/views/view-bar.tsx +111 -26
  41. package/src/components/editor/wall-measurement-label.tsx +1 -1
  42. package/src/components/editor/wall-move-side-handles.tsx +5 -1
  43. package/src/components/editor/wall-pull-grip.tsx +193 -0
  44. package/src/components/editor/wall-snap-beacon-layer.tsx +81 -1
  45. package/src/components/editor-2d/floorplan-adjacent-walls-layer.test.ts +76 -0
  46. package/src/components/editor-2d/floorplan-adjacent-walls-layer.tsx +186 -0
  47. package/src/components/editor-2d/floorplan-alignment-guide-layer.tsx +63 -3
  48. package/src/components/editor-2d/floorplan-group-move.tsx +31 -3
  49. package/src/components/editor-2d/floorplan-measurement-tool-layer.test.ts +6 -0
  50. package/src/components/editor-2d/floorplan-measurement-tool-layer.tsx +22 -13
  51. package/src/components/editor-2d/floorplan-open-wall-ends-layer.tsx +11 -3
  52. package/src/components/editor-2d/floorplan-registry-move-overlay.tsx +7 -1
  53. package/src/components/editor-2d/floorplan-snap-beacon-layer.tsx +96 -4
  54. package/src/components/editor-2d/renderers/floorplan-dimension-edit-overlay.tsx +52 -12
  55. package/src/components/editor-2d/renderers/floorplan-registry-layer.test.ts +12 -1
  56. package/src/components/editor-2d/renderers/floorplan-registry-layer.tsx +197 -37
  57. package/src/components/procedural-items/inspector.tsx +59 -12
  58. package/src/components/systems/ceiling/ceiling-bracket-batch.test.ts +2 -0
  59. package/src/components/systems/ceiling/ceiling-system.test.ts +141 -0
  60. package/src/components/systems/ceiling/ceiling-system.tsx +53 -18
  61. package/src/components/tools/distribution/distribution-run-tool.tsx +50 -26
  62. package/src/components/tools/item/placement-history.test.tsx +1 -1
  63. package/src/components/tools/item/placement-strategies.test.ts +64 -1
  64. package/src/components/tools/item/placement-strategies.ts +5 -9
  65. package/src/components/tools/item/use-placement-coordinator.tsx +30 -34
  66. package/src/components/tools/registry/item-surface-move.test.ts +11 -0
  67. package/src/components/tools/select/select-candidates.test.ts +45 -0
  68. package/src/components/tools/select/select-candidates.ts +13 -2
  69. package/src/components/tools/shared/cursor-sphere.tsx +10 -3
  70. package/src/components/tools/shared/draft-measurement-label.tsx +1 -1
  71. package/src/components/tools/shared/placement-box.tsx +1 -1
  72. package/src/components/tools/shared/placement-dimension-guides.tsx +1 -1
  73. package/src/components/tools/site/terrain-brush-cursor.tsx +1 -1
  74. package/src/components/tools/wall/wall-drafting.test.ts +299 -23
  75. package/src/components/tools/wall/wall-drafting.ts +132 -24
  76. package/src/components/tools/wall/wall-pull-snap.test.ts +274 -0
  77. package/src/components/tools/wall/wall-pull-snap.ts +563 -0
  78. package/src/components/tools/wall/wall-snap-geometry.test.ts +113 -1
  79. package/src/components/tools/wall/wall-snap-geometry.ts +137 -0
  80. package/src/components/tools/zone/zone-tool.tsx +3 -3
  81. package/src/components/ui/action-menu/action-button.tsx +2 -0
  82. package/src/components/ui/action-menu/camera-actions.tsx +8 -7
  83. package/src/components/ui/action-menu/control-modes.tsx +9 -5
  84. package/src/components/ui/action-menu/follow-pascal-pill.tsx +3 -1
  85. package/src/components/ui/action-menu/follow-pascal-toggle.tsx +7 -1
  86. package/src/components/ui/action-menu/furnish-tools.tsx +7 -6
  87. package/src/components/ui/action-menu/index.tsx +1 -0
  88. package/src/components/ui/action-menu/measurement-control.tsx +35 -29
  89. package/src/components/ui/action-menu/point-ask-button.tsx +4 -3
  90. package/src/components/ui/action-menu/structure-tools.tsx +25 -23
  91. package/src/components/ui/action-menu/view-toggles.test.ts +18 -0
  92. package/src/components/ui/action-menu/view-toggles.tsx +256 -51
  93. package/src/components/ui/build-panel/build-panel.tsx +163 -25
  94. package/src/components/ui/command-palette/editor-commands.tsx +38 -11
  95. package/src/components/ui/controls/finish-swatch.tsx +45 -19
  96. package/src/components/ui/controls/material-picker.tsx +24 -17
  97. package/src/components/ui/controls/metric-control.test.ts +52 -0
  98. package/src/components/ui/controls/metric-control.tsx +62 -24
  99. package/src/components/ui/controls/segmented-control.tsx +5 -2
  100. package/src/components/ui/controls/slider-control.test.tsx +23 -0
  101. package/src/components/ui/controls/slider-control.tsx +30 -14
  102. package/src/components/ui/floating-level-selector.tsx +100 -48
  103. package/src/components/ui/helpers/building-helper.tsx +5 -3
  104. package/src/components/ui/helpers/contextual-helper-panel.tsx +97 -37
  105. package/src/components/ui/helpers/helper-manager.test.tsx +53 -3
  106. package/src/components/ui/helpers/helper-manager.tsx +82 -57
  107. package/src/components/ui/helpers/item-helper.tsx +6 -4
  108. package/src/components/ui/helpers/registered-tool-helper.tsx +3 -1
  109. package/src/components/ui/level-duplicate-dialog.tsx +32 -23
  110. package/src/components/ui/level-wall-copy-prompt.tsx +124 -0
  111. package/src/components/ui/panels/collections/collections-popover.tsx +11 -11
  112. package/src/components/ui/panels/deck-boards-fields.tsx +136 -0
  113. package/src/components/ui/panels/floor-foundation-panel.tsx +113 -38
  114. package/src/components/ui/panels/floor-opening-panel.tsx +73 -25
  115. package/src/components/ui/panels/inspector-extensions.tsx +66 -0
  116. package/src/components/ui/panels/mobile-panel-sheet.tsx +3 -1
  117. package/src/components/ui/panels/mobile-selection-bar.tsx +8 -6
  118. package/src/components/ui/panels/multi-field-value.test.ts +27 -0
  119. package/src/components/ui/panels/multi-field-value.ts +20 -1
  120. package/src/components/ui/panels/multi-height-mode.tsx +8 -7
  121. package/src/components/ui/panels/multi-parametric-inspector.tsx +122 -34
  122. package/src/components/ui/panels/multi-selection-panel.tsx +75 -35
  123. package/src/components/ui/panels/node-display.ts +35 -24
  124. package/src/components/ui/panels/panel-manager.tsx +5 -3
  125. package/src/components/ui/panels/panel-wrapper.tsx +22 -109
  126. package/src/components/ui/panels/parametric-field-control.tsx +13 -10
  127. package/src/components/ui/panels/parametric-inspector.tsx +18 -12
  128. package/src/components/ui/panels/reference-panel.tsx +51 -35
  129. package/src/components/ui/panels/room-construction-rows.tsx +464 -65
  130. package/src/components/ui/panels/room-name-options.test.ts +36 -3
  131. package/src/components/ui/panels/room-name-options.ts +110 -11
  132. package/src/components/ui/panels/room-openings-field.tsx +21 -12
  133. package/src/components/ui/panels/room-panel.tsx +206 -99
  134. package/src/components/ui/panels/shape-choice.tsx +7 -5
  135. package/src/components/ui/panels/wall-region-list.tsx +7 -7
  136. package/src/components/ui/primitives/shortcut-token.tsx +10 -6
  137. package/src/components/ui/sidebar/icon-rail.tsx +11 -9
  138. package/src/components/ui/sidebar/panels/items-panel/function-tree-panel.tsx +10 -10
  139. package/src/components/ui/sidebar/panels/items-panel/index.tsx +10 -9
  140. package/src/components/ui/sidebar/panels/settings-panel/index.tsx +3 -0
  141. package/src/components/ui/sidebar/panels/settings-panel/keyboard-shortcuts-dialog.tsx +102 -32
  142. package/src/components/ui/sidebar/panels/site-panel/building-tree-node.tsx +4 -3
  143. package/src/components/ui/sidebar/panels/site-panel/ceiling-tree-node.tsx +4 -2
  144. package/src/components/ui/sidebar/panels/site-panel/chimney-tree-node.tsx +4 -2
  145. package/src/components/ui/sidebar/panels/site-panel/collection-tree-node.tsx +7 -5
  146. package/src/components/ui/sidebar/panels/site-panel/column-tree-node.tsx +4 -2
  147. package/src/components/ui/sidebar/panels/site-panel/default-names.ts +75 -0
  148. package/src/components/ui/sidebar/panels/site-panel/door-tree-node.tsx +4 -2
  149. package/src/components/ui/sidebar/panels/site-panel/dormer-tree-node.tsx +4 -2
  150. package/src/components/ui/sidebar/panels/site-panel/elevator-tree-node.tsx +4 -2
  151. package/src/components/ui/sidebar/panels/site-panel/fence-tree-node.tsx +4 -2
  152. package/src/components/ui/sidebar/panels/site-panel/gutter-tree-node.tsx +4 -2
  153. package/src/components/ui/sidebar/panels/site-panel/index.tsx +117 -64
  154. package/src/components/ui/sidebar/panels/site-panel/inline-rename-input.tsx +36 -3
  155. package/src/components/ui/sidebar/panels/site-panel/item-tree-node.tsx +4 -2
  156. package/src/components/ui/sidebar/panels/site-panel/level-tree-node.tsx +7 -4
  157. package/src/components/ui/sidebar/panels/site-panel/registry-tree-node.tsx +8 -2
  158. package/src/components/ui/sidebar/panels/site-panel/roof-tree-node.tsx +8 -5
  159. package/src/components/ui/sidebar/panels/site-panel/shelf-tree-node.tsx +4 -2
  160. package/src/components/ui/sidebar/panels/site-panel/slab-tree-node.tsx +8 -3
  161. package/src/components/ui/sidebar/panels/site-panel/solar-panel-tree-node.tsx +4 -2
  162. package/src/components/ui/sidebar/panels/site-panel/spawn-tree-node.tsx +4 -2
  163. package/src/components/ui/sidebar/panels/site-panel/stair-tree-node.tsx +8 -7
  164. package/src/components/ui/sidebar/panels/site-panel/tree-node-actions.tsx +7 -6
  165. package/src/components/ui/sidebar/panels/site-panel/tree-node.tsx +16 -1
  166. package/src/components/ui/sidebar/panels/site-panel/unit-tree-node.tsx +15 -8
  167. package/src/components/ui/sidebar/panels/site-panel/wall-tree-node.tsx +4 -2
  168. package/src/components/ui/sidebar/panels/site-panel/window-tree-node.tsx +4 -2
  169. package/src/components/ui/sidebar/panels/site-panel/zone-membership-checkbox.tsx +3 -1
  170. package/src/components/ui/sidebar/panels/site-panel/zone-tree-node.tsx +18 -8
  171. package/src/components/ui/sidebar/panels/zone-panel/index.tsx +25 -14
  172. package/src/components/ui/sidebar/tab-bar.tsx +4 -1
  173. package/src/components/ui/sidebar/use-plugin-panels.tsx +19 -10
  174. package/src/components/ui/snap-target-badge.tsx +7 -6
  175. package/src/components/ui/wall-type/wall-type-editor-dialog.tsx +721 -0
  176. package/src/components/ui/wall-type/wall-type-menu.tsx +167 -0
  177. package/src/components/viewer/floorplan-compass-button.tsx +12 -3
  178. package/src/components/viewer/floorplan-preview.tsx +27 -19
  179. package/src/components/viewer/render-settings-menu.tsx +294 -0
  180. package/src/components/viewer/sky-control.tsx +710 -0
  181. package/src/components/viewer/use-viewer-ui-sounds.test.ts +15 -0
  182. package/src/components/viewer/use-viewer-ui-sounds.ts +46 -0
  183. package/src/components/viewer/viewer-controls-bar.tsx +278 -260
  184. package/src/components/viewer/viewer-hover-label.tsx +12 -4
  185. package/src/components/viewer/viewer-rooms.tsx +61 -4
  186. package/src/components/viewer/viewer-scene-header.tsx +28 -17
  187. package/src/components/viewer/viewer-stage-switcher.tsx +4 -2
  188. package/src/components/viewer-overlay.tsx +8 -0
  189. package/src/components/walkthrough-hud.tsx +75 -2
  190. package/src/hooks/use-keyboard.test.ts +85 -4
  191. package/src/hooks/use-keyboard.ts +65 -7
  192. package/src/hooks/use-selected-room.ts +12 -1
  193. package/src/i18n/floor-footprint-name.ts +30 -0
  194. package/src/i18n/level-name.ts +39 -0
  195. package/src/i18n/locales/en/messages.po +15117 -0
  196. package/src/i18n/locales/en/messages.ts +1 -0
  197. package/src/i18n/locales/en-XA/messages.ts +1 -0
  198. package/src/i18n/locales/es/messages.po +37501 -0
  199. package/src/i18n/locales/es/messages.ts +1 -0
  200. package/src/i18n/locales/fr/messages.po +37501 -0
  201. package/src/i18n/locales/fr/messages.ts +1 -0
  202. package/src/i18n/locales/ja/messages.po +37501 -0
  203. package/src/i18n/locales/ja/messages.ts +1 -0
  204. package/src/i18n/locales/zh-CN/messages.po +37501 -0
  205. package/src/i18n/locales/zh-CN/messages.ts +1 -0
  206. package/src/i18n/locales.test.ts +63 -0
  207. package/src/i18n/locales.ts +32 -0
  208. package/src/i18n/message-slot.tsx +25 -0
  209. package/src/i18n/node-messages.ts +169 -0
  210. package/src/i18n/pseudo-locale.ts +17 -0
  211. package/src/i18n/published-entry.test.ts +17 -0
  212. package/src/i18n/scene-theme-name.ts +16 -0
  213. package/src/index.tsx +156 -4
  214. package/src/lib/add-storey.test.ts +3 -3
  215. package/src/lib/contextual-help.test.ts +20 -1
  216. package/src/lib/contextual-help.ts +95 -19
  217. package/src/lib/continuation.ts +38 -11
  218. package/src/lib/floor-footprints.test.ts +2 -1
  219. package/src/lib/floor-foundation-support.test.ts +7 -3
  220. package/src/lib/floorplan/dimension-drive.test.ts +36 -2
  221. package/src/lib/floorplan/dimension-drive.ts +68 -3
  222. package/src/lib/floorplan/floorplan-export.test.ts +16 -5
  223. package/src/lib/floorplan/floorplan-export.tsx +13 -16
  224. package/src/lib/floorplan/floorplan-extension.test.ts +11 -12
  225. package/src/lib/floorplan/floorplan-extension.ts +37 -18
  226. package/src/lib/floorplan/floorplan-mode.test.ts +13 -6
  227. package/src/lib/floorplan/floorplan-mode.ts +14 -12
  228. package/src/lib/floorplan/floorplan-plugin-install.test.tsx +1 -1
  229. package/src/lib/floorplan/floorplan-readonly.ts +3 -3
  230. package/src/lib/floorplan/schedules.ts +10 -6
  231. package/src/lib/floorplan/site-plan/site-annotations.test.ts +46 -1
  232. package/src/lib/floorplan/site-plan/site-annotations.ts +3 -0
  233. package/src/lib/floorplan/site-plan/site-parts.ts +6 -3
  234. package/src/lib/geometry-script/author.test.ts +114 -0
  235. package/src/lib/geometry-script/author.ts +55 -23
  236. package/src/lib/geometry-script/client.ts +3 -0
  237. package/src/lib/geometry-script/finish-scripts.ts +7 -7
  238. package/src/lib/glb-export.test.ts +327 -0
  239. package/src/lib/glb-export.ts +70 -2
  240. package/src/lib/grid-frame.test.ts +56 -3
  241. package/src/lib/grid-frame.ts +28 -1
  242. package/src/lib/history.test.ts +2 -1
  243. package/src/lib/hud-title.ts +48 -24
  244. package/src/lib/ifc-export.test.ts +41 -1
  245. package/src/lib/inspector-card-mode.test.ts +3 -31
  246. package/src/lib/inspector-card-mode.ts +5 -10
  247. package/src/lib/inspector-expanded.test.ts +13 -0
  248. package/src/lib/inspector-expanded.ts +13 -1
  249. package/src/lib/keyboard-pan.test.ts +7 -0
  250. package/src/lib/keyboard-pan.ts +9 -2
  251. package/src/lib/layer-thickness.test.ts +22 -0
  252. package/src/lib/layer-thickness.ts +79 -0
  253. package/src/lib/lot/drop-in.test.ts +59 -1
  254. package/src/lib/lot/drop-in.ts +42 -50
  255. package/src/lib/material-swatch-style.ts +24 -0
  256. package/src/lib/measurement-parser.ts +7 -3
  257. package/src/lib/measurements.test.ts +17 -0
  258. package/src/lib/measurements.ts +20 -5
  259. package/src/lib/mezzanine-selection.test.ts +3 -1
  260. package/src/lib/orbit-near-plane.test.ts +12 -0
  261. package/src/lib/orbit-near-plane.ts +13 -0
  262. package/src/lib/paint-regions.test.ts +8 -5
  263. package/src/lib/paint-scope.test.ts +53 -0
  264. package/src/lib/paint-scope.ts +18 -4
  265. package/src/lib/panel-rows.ts +17 -8
  266. package/src/lib/reference-scale.ts +83 -0
  267. package/src/lib/room-built-on.test.ts +26 -12
  268. package/src/lib/room-built-on.ts +20 -2
  269. package/src/lib/room-construction.test.ts +28 -1
  270. package/src/lib/room-construction.ts +14 -0
  271. package/src/lib/room-controls-phase6.test.ts +140 -0
  272. package/src/lib/room-handle-drag.ts +557 -42
  273. package/src/lib/room-name-catalog.test.ts +8 -0
  274. package/src/lib/room-name-catalog.ts +8 -1
  275. package/src/lib/room-name-messages.test.ts +132 -0
  276. package/src/lib/room-name-messages.ts +147 -0
  277. package/src/lib/room-name-storage.test.ts +334 -0
  278. package/src/lib/room-push-dimensions.test.ts +37 -0
  279. package/src/lib/room-push-dimensions.ts +226 -2
  280. package/src/lib/room-selection.test.ts +23 -6
  281. package/src/lib/scene-clipboard.test.ts +70 -49
  282. package/src/lib/scene-clipboard.ts +9 -4
  283. package/src/lib/selection-cycle.ts +63 -0
  284. package/src/lib/separator-edit.test.ts +431 -0
  285. package/src/lib/separator-edit.ts +179 -0
  286. package/src/lib/separator-lines.ts +38 -0
  287. package/src/lib/sfx-bus.ts +27 -1
  288. package/src/lib/sfx-player.ts +16 -0
  289. package/src/lib/tool-cursor-icon.ts +5 -1
  290. package/src/lib/tool-shortcuts.ts +130 -0
  291. package/src/lib/viewer-selection.test.ts +28 -24
  292. package/src/lib/viewer-selection.ts +20 -5
  293. package/src/lib/wall-draw-variant.test.ts +7 -6
  294. package/src/lib/wall-draw-variant.ts +107 -6
  295. package/src/lib/wall-nudge.test.ts +44 -0
  296. package/src/lib/wall-nudge.ts +131 -0
  297. package/src/lib/wall-parallel-snap.test.ts +83 -0
  298. package/src/lib/wall-parallel-snap.ts +176 -0
  299. package/src/lib/wall-placement-pointer.test.ts +23 -0
  300. package/src/lib/wall-placement-pointer.ts +18 -0
  301. package/src/lib/wall-pull.test.ts +245 -0
  302. package/src/lib/wall-pull.ts +209 -0
  303. package/src/lib/wall-push-line-up.test.ts +132 -0
  304. package/src/lib/wall-region-list.ts +14 -5
  305. package/src/lib/wall-run-snap.test.ts +188 -0
  306. package/src/lib/wall-run-snap.ts +83 -0
  307. package/src/lib/wall-type.test.ts +125 -0
  308. package/src/lib/wall-type.ts +119 -0
  309. package/src/lib/window-interaction.test.ts +42 -0
  310. package/src/lib/window-interaction.ts +47 -5
  311. package/src/lib/world-grid-snap.test.ts +139 -2
  312. package/src/lib/world-grid-snap.ts +102 -105
  313. package/src/store/use-alignment-guides.ts +27 -1
  314. package/src/store/use-editor.tsx +48 -0
  315. package/src/store/use-floorplan-annotation-visibility.test.ts +64 -0
  316. package/src/store/use-floorplan-annotation-visibility.ts +47 -22
  317. package/src/store/use-wall-snap-indicator.ts +15 -2
@@ -1,14 +1,15 @@
1
1
  'use client'
2
2
 
3
3
  import {
4
+ type AnyNodeId,
4
5
  clampRoomFloorHandle,
5
6
  type Point,
6
7
  roomDrawnFloor,
7
8
  sceneRegistry,
9
+ usePascalI18n,
8
10
  useScene,
9
11
  } from '@pascal-app/core'
10
12
  import { useViewer } from '@pascal-app/viewer'
11
- import { Html } from '@react-three/drei'
12
13
  import { type ThreeEvent, useFrame, useThree } from '@react-three/fiber'
13
14
  import { useEffect, useMemo, useRef, useState } from 'react'
14
15
  import {
@@ -33,6 +34,7 @@ import {
33
34
  publishStructuralElevationGuide,
34
35
  resolveStructuralElevationSnap,
35
36
  } from '../../lib/elevation-guides'
37
+ import { parseDimensionInput } from '../../lib/floorplan/dimension-drive'
36
38
  import { formatLinearMeasurement, MEASUREMENT_ACTIVE_COLOR } from '../../lib/measurements'
37
39
  import { commitRoomFloorElevation, roomFloorBase } from '../../lib/room-construction-commands'
38
40
  import {
@@ -51,10 +53,16 @@ import {
51
53
  import type { RoomDimension } from '../../lib/room-push-dimensions'
52
54
  import type { RoomSelectionRecord } from '../../lib/room-selection'
53
55
  import useEditor, { isGridSnapActive, isMagneticSnapActive } from '../../store/use-editor'
56
+ import {
57
+ commitDimensionValue,
58
+ useDimensionEditAllowed,
59
+ } from '../editor-2d/renderers/floorplan-dimension-edit-overlay'
54
60
  import { suppressBoxSelectForPointer } from '../tools/select/box-select-state'
55
61
  import { ARROW_COLOR, ARROW_SCALE, HandleArrow } from './handles/handle-arrow'
56
62
  import { resolveResizeSnapValue } from './handles/resize-snap'
63
+ import { Html } from './localized-html'
57
64
  import { formatMeasurement } from './measurement-pill'
65
+ import { useSelectedWallDimensions } from './room-handles-2d'
58
66
  import { WallPushArrow } from './wall-push-arrow'
59
67
 
60
68
  const PILL =
@@ -295,8 +303,9 @@ export function ElevationArrows({
295
303
 
296
304
  /**
297
305
  * A selected room's 3D handles: the floor elevation tracker at its centroid
298
- * and one push/pull arrow outside every boundary wall. Rendered in the level
299
- * frame by `RoomControls3D`, only while nothing else is in progress.
306
+ * and one push/pull arrow outside every boundary wall and every straight edge
307
+ * of invisible walls. Rendered in the level frame by `RoomControls3D`, only
308
+ * while nothing else is in progress.
300
309
  */
301
310
  export function RoomHandles3D({ room }: { room: RoomSelectionRecord }) {
302
311
  const nodes = useScene((s) => s.nodes)
@@ -307,7 +316,11 @@ export function RoomHandles3D({ room }: { room: RoomSelectionRecord }) {
307
316
  () => outlineCentroid([room.polygon as Point[], ...(room.holes as Point[][])]),
308
317
  [room.polygon, room.holes],
309
318
  )
310
- const handles = useMemo(() => roomPushHandles(nodes, room.spans), [nodes, room.spans])
319
+ // Its invisible walls' arrows lie on its floor, where their dashed lines are drawn.
320
+ const handles = useMemo(
321
+ () => roomPushHandles(nodes, room.spans, elevation),
322
+ [nodes, room.spans, elevation],
323
+ )
311
324
  // A mezzanine has no walls: its plate edges carry the push arrows instead.
312
325
  const edges = useMemo(
313
326
  () => (room.mezzanine ? mezzanineEdgeHandles(nodes, room.zoneId) : []),
@@ -414,9 +427,20 @@ const DIMENSION_LABEL_OUTLINE = [
414
427
  * above its room's floor and drawn over the walls, closed by cross ticks, with
415
428
  * the measurement's outlined label — apart from the moved-distance pill.
416
429
  */
417
- function RoomDimensionLines3D({ dimensions }: { dimensions: RoomDimension[] }) {
430
+ function RoomDimensionLines3D({
431
+ dimensions,
432
+ hidden = false,
433
+ }: {
434
+ /** With an `ownerId` (a selected wall's), a value can be typed to move that wall. */
435
+ dimensions: readonly (RoomDimension & { ownerId?: string })[]
436
+ /** Hides the labels while keeping their drei `Html` roots mounted. */
437
+ hidden?: boolean
438
+ }) {
439
+ const i18n = usePascalI18n()
418
440
  const unit = useViewer((s) => s.unit)
419
441
  const metricNotation = useViewer((s) => s.metricNotation)
442
+ const editAllowed = useDimensionEditAllowed()
443
+ const [editing, setEditing] = useState<string | null>(null)
420
444
  const geometry = useMemo(() => {
421
445
  if (!dimensions.length) return null
422
446
  const positions: number[] = []
@@ -441,27 +465,175 @@ function RoomDimensionLines3D({ dimensions }: { dimensions: RoomDimension[] }) {
441
465
  raycast={noRaycast}
442
466
  renderOrder={1001}
443
467
  />
444
- {dimensions.map((dimension) => (
445
- <Html
446
- center
447
- key={dimension.key}
448
- position={[
449
- (dimension.from[0] + dimension.to[0]) / 2,
450
- dimension.elevation,
451
- (dimension.from[1] + dimension.to[1]) / 2,
452
- ]}
453
- style={{ pointerEvents: 'none' }}
454
- zIndexRange={[20, 0]}
455
- >
456
- <div
457
- className="-translate-y-3 whitespace-nowrap font-medium text-base text-white"
458
- data-room-dimension-3d
459
- style={{ textShadow: DIMENSION_LABEL_OUTLINE }}
468
+ {dimensions.map(({ ownerId: wallId, ...dimension }) => {
469
+ const editable = editAllowed && !!wallId
470
+ return (
471
+ <Html
472
+ center
473
+ key={dimension.key}
474
+ position={[
475
+ (dimension.from[0] + dimension.to[0]) / 2,
476
+ dimension.elevation,
477
+ (dimension.from[1] + dimension.to[1]) / 2,
478
+ ]}
479
+ style={{ display: hidden ? 'none' : undefined, pointerEvents: 'none' }}
480
+ zIndexRange={[20, 0]}
460
481
  >
461
- {formatMeasurement(dimension.distance, unit, metricNotation)}
462
- </div>
463
- </Html>
464
- ))}
482
+ {editable && wallId && !hidden && editing === dimension.key ? (
483
+ <DimensionValueInput
484
+ dimension={dimension}
485
+ initial={formatMeasurement(dimension.distance, unit, metricNotation)}
486
+ onClose={() => setEditing(null)}
487
+ unit={unit}
488
+ wallId={wallId}
489
+ />
490
+ ) : (
491
+ <div
492
+ className={`-translate-y-3 whitespace-nowrap font-medium text-base text-white ${editable ? 'pointer-events-auto cursor-text' : ''}`}
493
+ data-room-dimension-3d
494
+ onClick={editable ? () => setEditing(dimension.key) : undefined}
495
+ onKeyDown={
496
+ editable
497
+ ? (event) => {
498
+ if (event.key === 'Enter') setEditing(dimension.key)
499
+ }
500
+ : undefined
501
+ }
502
+ onPointerDown={editable ? (event) => event.stopPropagation() : undefined}
503
+ role={editable ? 'button' : undefined}
504
+ style={{ textShadow: DIMENSION_LABEL_OUTLINE }}
505
+ tabIndex={editable ? 0 : undefined}
506
+ title={
507
+ editable
508
+ ? i18n._({
509
+ id: 'editor.roomHandles.typeASizeToMoveThe',
510
+ message: 'Type a size to move the wall',
511
+ })
512
+ : undefined
513
+ }
514
+ >
515
+ {formatMeasurement(dimension.distance, unit, metricNotation)}
516
+ </div>
517
+ )}
518
+ </Html>
519
+ )
520
+ })}
521
+ </group>
522
+ )
523
+ }
524
+
525
+ /**
526
+ * A temporary dimension's value, typed: Enter moves the selected wall so the
527
+ * dimension reads it (`commitDimensionValue`, one undo step), Esc or a click
528
+ * away leaves it. Feet-inches, metric and bare numbers parse as in the plan.
529
+ */
530
+ function DimensionValueInput({
531
+ dimension,
532
+ initial,
533
+ onClose,
534
+ unit,
535
+ wallId,
536
+ }: {
537
+ dimension: RoomDimension
538
+ initial: string
539
+ onClose: () => void
540
+ unit: 'metric' | 'imperial'
541
+ wallId: string
542
+ }) {
543
+ const i18n = usePascalI18n()
544
+ const [draft, setDraft] = useState(initial)
545
+ const [error, setError] = useState<string | null>(null)
546
+ const commit = () => {
547
+ const nextLength = parseDimensionInput(draft, unit)
548
+ if (nextLength === null) {
549
+ setError(
550
+ i18n._({
551
+ id: 'editor.roomHandles.cannotReadInput',
552
+ message: 'Cannot read "{input}"',
553
+ values: { input: draft.trim() },
554
+ }),
555
+ )
556
+ return
557
+ }
558
+ const outcome = commitDimensionValue({
559
+ target: {
560
+ ownerNodeId: wallId as AnyNodeId,
561
+ witnessStart: dimension.from,
562
+ witnessEnd: dimension.to,
563
+ value: dimension.distance,
564
+ },
565
+ nextLength,
566
+ unit,
567
+ })
568
+ if (outcome.ok) onClose()
569
+ else setError(outcome.reason)
570
+ }
571
+ return (
572
+ <div
573
+ className="pointer-events-auto -translate-y-3 w-24"
574
+ onPointerDown={(event) => event.stopPropagation()}
575
+ >
576
+ <input
577
+ // biome-ignore lint/a11y/noAutofocus: the box opens on a click to be typed in
578
+ autoFocus
579
+ aria-label={i18n._({ id: 'editor.roomHandles.dimensionValue', message: 'Dimension value' })}
580
+ className="w-full rounded-sm border border-primary bg-card px-1 py-0.5 text-center font-mono text-foreground text-xs shadow-sm outline-none"
581
+ onBlur={onClose}
582
+ onChange={(event) => {
583
+ setDraft(event.target.value)
584
+ setError(null)
585
+ }}
586
+ onFocus={(event) => event.target.select()}
587
+ onKeyDown={(event) => {
588
+ event.stopPropagation()
589
+ if (event.key === 'Enter') {
590
+ event.preventDefault()
591
+ commit()
592
+ } else if (event.key === 'Escape') {
593
+ event.preventDefault()
594
+ onClose()
595
+ }
596
+ }}
597
+ spellCheck={false}
598
+ value={draft}
599
+ />
600
+ {error ? (
601
+ <div className="mt-0.5 whitespace-nowrap rounded-sm bg-card px-1 text-[10px] text-destructive shadow-sm">
602
+ {error}
603
+ </div>
604
+ ) : null}
605
+ </div>
606
+ )
607
+ }
608
+
609
+ /**
610
+ * The selected walls' temporary dimensions in 3D, the plan's twin
611
+ * (`SelectedWallDimensions2D`): the room widths their pushes show and the
612
+ * outside sizes, drawn as the push draws them, in the walls' level frame.
613
+ */
614
+ export function SelectedWallDimensions3D() {
615
+ const selection = useSelectedWallDimensions()
616
+ const isFloorplanHovered = useEditor((s) => s.isFloorplanHovered)
617
+ const levelId = selection?.levelId ?? null
618
+ const root = useRef<Group>(null)
619
+ useFrame(() => {
620
+ const level = levelId ? sceneRegistry.nodes.get(levelId) : null
621
+ if (!root.current) return
622
+ root.current.visible = !!level && !isFloorplanHovered
623
+ if (level) {
624
+ level.updateWorldMatrix(true, false)
625
+ root.current.matrix.copy(level.matrixWorld)
626
+ }
627
+ })
628
+ // The labels are DOM (drei `Html`); with no document (a headless
629
+ // renderer) there is nothing to draw them into.
630
+ if (!selection || typeof document === 'undefined') return null
631
+ // Hovering the plan hides the dimensions rather than unmounting them: drei
632
+ // `Html` unmounts its DOM root synchronously, which React reports as an error
633
+ // when it happens inside a render.
634
+ return (
635
+ <group matrixAutoUpdate={false} ref={root}>
636
+ <RoomDimensionLines3D dimensions={selection.dimensions} hidden={isFloorplanHovered} />
465
637
  </group>
466
638
  )
467
639
  }
@@ -524,6 +696,7 @@ function MezzanineEdgeArrow({
524
696
  }
525
697
 
526
698
  export function RoomHandleDragPreview3D() {
699
+ const i18n = usePascalI18n()
527
700
  const drag = useRoomHandleDrag((s) => s.drag)
528
701
  const unit = useViewer((s) => s.unit)
529
702
  const metricNotation = useViewer((s) => s.metricNotation)
@@ -614,7 +787,9 @@ export function RoomHandleDragPreview3D() {
614
787
  />
615
788
  <Html center position={[ax, drag.value + 0.35, az]} zIndexRange={[25, 0]}>
616
789
  <div className={PILL}>
617
- <span className="font-medium text-foreground">Floor</span>
790
+ <span className="font-medium text-foreground">
791
+ {i18n._({ id: 'editor.roomHandles.floor', message: 'Floor' })}
792
+ </span>
618
793
  <span className="text-muted-foreground">
619
794
  {formatMeasurement(
620
795
  drag.value - roomFloorBase(useScene.getState().nodes, drag.zoneId),
@@ -627,6 +802,45 @@ export function RoomHandleDragPreview3D() {
627
802
  </group>
628
803
  )
629
804
  }
805
+ const ghosts = drag.ghosts.map((ghost, index) => {
806
+ const length = Math.hypot(ghost.end[0] - ghost.start[0], ghost.end[1] - ghost.start[1])
807
+ if (length < 1e-3) return null
808
+ return (
809
+ <mesh
810
+ key={index}
811
+ layers={EDITOR_LAYER}
812
+ material={materials.fill}
813
+ position={[
814
+ (ghost.start[0] + ghost.end[0]) / 2,
815
+ drag.base + ghost.height / 2,
816
+ (ghost.start[1] + ghost.end[1]) / 2,
817
+ ]}
818
+ raycast={noRaycast}
819
+ renderOrder={99}
820
+ rotation={[0, -Math.atan2(ghost.end[1] - ghost.start[1], ghost.end[0] - ghost.start[0]), 0]}
821
+ >
822
+ <boxGeometry args={[length, ghost.height, ghost.thickness]} />
823
+ </mesh>
824
+ )
825
+ })
826
+ if (drag.kind === 'wall-group-push')
827
+ return (
828
+ <group>
829
+ <RoomDimensionLines3D dimensions={drag.dimensions} />
830
+ {ghosts}
831
+ {drag.message && (
832
+ <Html
833
+ center
834
+ position={[drag.anchor[0], drag.height + 0.45, drag.anchor[1]]}
835
+ zIndexRange={[25, 0]}
836
+ >
837
+ <div className={PILL}>
838
+ <span className="font-medium text-red-500">{drag.message}</span>
839
+ </div>
840
+ </Html>
841
+ )}
842
+ </group>
843
+ )
630
844
  const at: Point = [
631
845
  drag.anchor[0] + drag.outward[0] * drag.distance,
632
846
  drag.anchor[1] + drag.outward[1] * drag.distance,
@@ -636,31 +850,7 @@ export function RoomHandleDragPreview3D() {
636
850
  return (
637
851
  <group>
638
852
  <RoomDimensionLines3D dimensions={drag.dimensions} />
639
- {drag.ghosts.map((ghost, index) => {
640
- const length = Math.hypot(ghost.end[0] - ghost.start[0], ghost.end[1] - ghost.start[1])
641
- if (length < 1e-3) return null
642
- return (
643
- <mesh
644
- key={index}
645
- layers={EDITOR_LAYER}
646
- material={materials.fill}
647
- position={[
648
- (ghost.start[0] + ghost.end[0]) / 2,
649
- drag.base + ghost.height / 2,
650
- (ghost.start[1] + ghost.end[1]) / 2,
651
- ]}
652
- raycast={noRaycast}
653
- renderOrder={99}
654
- rotation={[
655
- 0,
656
- -Math.atan2(ghost.end[1] - ghost.start[1], ghost.end[0] - ghost.start[0]),
657
- 0,
658
- ]}
659
- >
660
- <boxGeometry args={[length, ghost.height, ghost.thickness]} />
661
- </mesh>
662
- )
663
- })}
853
+ {ghosts}
664
854
  <Html center position={[at[0], drag.height + 0.45, at[1]]} zIndexRange={[25, 0]}>
665
855
  <div className={PILL}>
666
856
  {drag.message ? (
@@ -25,6 +25,7 @@ import { describeZoneOutline, type RoomSelectionRecord } from '../../lib/room-se
25
25
  import useEditor from '../../store/use-editor'
26
26
  import useInteractionScope from '../../store/use-interaction-scope'
27
27
  import { RoomControls2D, RoomControls3D } from './room-controls'
28
+ import { SelectedWallDimensions3D } from './room-handles'
28
29
 
29
30
  /**
30
31
  * A zone reached as a zone (hovered or selected through its area or pill, not
@@ -188,6 +189,7 @@ export function RoomHighlight3D() {
188
189
  return (
189
190
  <>
190
191
  <RoomControls3D />
192
+ <SelectedWallDimensions3D />
191
193
  {room ? <RoomMesh room={room} /> : null}
192
194
  </>
193
195
  )
@@ -7,15 +7,18 @@ import {
7
7
  containsPoint,
8
8
  DEFAULT_LEVEL_HEIGHT,
9
9
  emitter,
10
+ formatNumber,
11
+ type PascalI18n,
10
12
  type Polygon,
11
13
  polygonInteriorPoint,
12
14
  type Ring,
13
15
  sceneRegistry,
16
+ useLocale,
17
+ usePascalI18n,
14
18
  useScene,
15
19
  type ZoneNode,
16
20
  } from '@pascal-app/core'
17
21
  import { timeSpan, useViewer } from '@pascal-app/viewer'
18
- import { Html } from '@react-three/drei'
19
22
  import { useFrame, useThree } from '@react-three/fiber'
20
23
  import {
21
24
  createContext,
@@ -47,17 +50,14 @@ import { useShallow } from 'zustand/react/shallow'
47
50
  import { useRoomRecords } from '../../hooks/use-selected-room'
48
51
  import { getAreaUnitLabel, type LinearUnit, squareMetersToAreaUnit } from '../../lib/measurements'
49
52
  import { roomFloorElevation } from '../../lib/room-handle-drag'
53
+ import { displayRoomName } from '../../lib/room-name-messages'
50
54
  import type { RoomSelectionRecord } from '../../lib/room-selection'
51
- import {
52
- clickZoneArea,
53
- hoverZoneArea,
54
- leaveZoneArea,
55
- zoneKindLabel,
56
- } from '../../lib/room-zone-routing'
55
+ import { clickZoneArea, hoverZoneArea, leaveZoneArea } from '../../lib/room-zone-routing'
57
56
  import { selectionModifiersFromEvent } from '../../lib/selection-routing'
58
57
  import useEditor from '../../store/use-editor'
59
58
  import useInteractionScope from '../../store/use-interaction-scope'
60
59
  import { isViewVisible, VIEW_2D, VIEW_3D } from '../../store/view-layout'
60
+ import { Html } from './localized-html'
61
61
 
62
62
  const CONTEXT_TYPES = new Set(['site', 'building', 'level'])
63
63
 
@@ -92,8 +92,12 @@ export function roomLabelsVisible(state: RoomLabelVisibilityState): boolean {
92
92
  )
93
93
  }
94
94
 
95
- export function formatRoomArea(squareMeters: number, unit: LinearUnit): string {
96
- return `${squareMetersToAreaUnit(squareMeters, unit).toFixed(1)} ${getAreaUnitLabel(unit)}`
95
+ export function formatRoomArea(squareMeters: number, unit: LinearUnit, locale?: string): string {
96
+ return `${formatNumber(squareMetersToAreaUnit(squareMeters, unit), {
97
+ min: 1,
98
+ max: 1,
99
+ locale,
100
+ })} ${getAreaUnitLabel(unit)}`
97
101
  }
98
102
 
99
103
  function useRoomLabelsVisible() {
@@ -1169,11 +1173,12 @@ export function RoomPillChip({
1169
1173
  onLeave: () => void
1170
1174
  }) {
1171
1175
  const unit = useViewer((s) => s.unit)
1172
- const areaLabel = formatRoomArea(areaM2, unit)
1176
+ const locale = useLocale()
1177
+ const areaLabel = formatRoomArea(areaM2, unit, locale)
1173
1178
  // biome-ignore lint/correctness/useExhaustiveDependencies: A dot's border box does not resize when its hidden text changes.
1174
1179
  useLayoutEffect(() => {
1175
1180
  if (elements instanceof PillElements) elements.measure(zoneId)
1176
- }, [areaM2, elements, name, unit, zoneId])
1181
+ }, [areaM2, elements, locale, name, unit, zoneId])
1177
1182
  const register = useCallback(
1178
1183
  (element: HTMLElement | null) => {
1179
1184
  if (element) elements.set(zoneId, element)
@@ -1227,6 +1232,7 @@ export function RoomPillDetail({
1227
1232
  areaM2: number
1228
1233
  }) {
1229
1234
  const unit = useViewer((s) => s.unit)
1235
+ const locale = useLocale()
1230
1236
  return (
1231
1237
  <div
1232
1238
  aria-hidden
@@ -1234,7 +1240,7 @@ export function RoomPillDetail({
1234
1240
  data-room-label-detail={zoneId}
1235
1241
  data-room-label-view={view}
1236
1242
  >
1237
- <LabelParts areaLabel={formatRoomArea(areaM2, unit)} color={color} full name={name} />
1243
+ <LabelParts areaLabel={formatRoomArea(areaM2, unit, locale)} color={color} full name={name} />
1238
1244
  </div>
1239
1245
  )
1240
1246
  }
@@ -1244,7 +1250,13 @@ function useLabelZone(zoneId: string) {
1244
1250
  return zone && zone.visible !== false ? zone : null
1245
1251
  }
1246
1252
 
1247
- const labelName = (zone: ZoneNode) => zone.name?.trim() || zoneKindLabel(zone)
1253
+ const labelName = (zone: ZoneNode, i18n: PascalI18n) => {
1254
+ const name = zone.name?.trim()
1255
+ if (name) return displayRoomName(name, i18n)
1256
+ return zone.spaceRole === 'room'
1257
+ ? i18n._({ id: 'editor.roomLabel.room', message: 'Room' })
1258
+ : i18n._({ id: 'editor.roomLabel.zone', message: 'Zone' })
1259
+ }
1248
1260
 
1249
1261
  /**
1250
1262
  * One room's pill in the editor. Hover and click take the same rule as the
@@ -1260,6 +1272,7 @@ function RoomPill({
1260
1272
  view: '2d' | '3d'
1261
1273
  elements: Map<string, HTMLElement>
1262
1274
  }) {
1275
+ const i18n = usePascalI18n()
1263
1276
  const zone = useLabelZone(entry.zoneId)
1264
1277
  // A pill that disappears under the cursor (its click selected something)
1265
1278
  // fires no leave: end the hover it set.
@@ -1270,7 +1283,7 @@ function RoomPill({
1270
1283
  areaM2={entry.area}
1271
1284
  color={zone.color}
1272
1285
  elements={elements}
1273
- name={labelName(zone)}
1286
+ name={labelName(zone, i18n)}
1274
1287
  onClick={(event) => clickZoneArea(entry.zoneId, selectionModifiersFromEvent(event))}
1275
1288
  onHover={(event) => hoverZoneArea(entry.zoneId, selectionModifiersFromEvent(event))}
1276
1289
  onLeave={() => leaveZoneArea(entry.zoneId)}
@@ -1281,13 +1294,14 @@ function RoomPill({
1281
1294
  }
1282
1295
 
1283
1296
  function RoomDetail({ entry, view }: { entry: LabelEntry; view: '2d' | '3d' }) {
1297
+ const i18n = usePascalI18n()
1284
1298
  const zone = useLabelZone(entry.zoneId)
1285
1299
  if (!zone) return null
1286
1300
  return (
1287
1301
  <RoomPillDetail
1288
1302
  areaM2={entry.area}
1289
1303
  color={zone.color}
1290
- name={labelName(zone)}
1304
+ name={labelName(zone, i18n)}
1291
1305
  view={view}
1292
1306
  zoneId={entry.zoneId}
1293
1307
  />