@pascal-app/editor 1.0.6 → 1.0.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (317) hide show
  1. package/package.json +18 -15
  2. package/src/components/editor/action-menu-button.tsx +4 -1
  3. package/src/components/editor/alignment-3d-guide-layer.tsx +102 -11
  4. package/src/components/editor/bake-thumbnail.tsx +3 -2
  5. package/src/components/editor/custom-camera-controls.tsx +20 -8
  6. package/src/components/editor/elevation-3d-guide-layer.tsx +1 -1
  7. package/src/components/editor/first-person-controls.tsx +72 -61
  8. package/src/components/editor/floating-action-menu.tsx +27 -9
  9. package/src/components/editor/floating-building-action-menu.tsx +1 -1
  10. package/src/components/editor/floorplan-panel.test.ts +220 -0
  11. package/src/components/editor/floorplan-panel.tsx +285 -132
  12. package/src/components/editor/footprint-height-handle.tsx +1 -1
  13. package/src/components/editor/grid.tsx +24 -15
  14. package/src/components/editor/group-actions.ts +19 -1
  15. package/src/components/editor/group-floating-action-menu.tsx +1 -1
  16. package/src/components/editor/group-move-3d.ts +27 -2
  17. package/src/components/editor/group-selection-box-3d.tsx +4 -1
  18. package/src/components/editor/group-transform-shared.ts +20 -0
  19. package/src/components/editor/index.tsx +130 -20
  20. package/src/components/editor/inspector-extension-actions.tsx +36 -0
  21. package/src/components/editor/localized-html.tsx +22 -0
  22. package/src/components/editor/node-action-menu.tsx +46 -12
  23. package/src/components/editor/node-arrow-handles.tsx +1 -2
  24. package/src/components/editor/open-wall-ends-3d-layer.tsx +1 -1
  25. package/src/components/editor/opening-guides-3d-layer.tsx +1 -1
  26. package/src/components/editor/registry-action-contributions.test.tsx +67 -6
  27. package/src/components/editor/registry-action-contributions.tsx +115 -15
  28. package/src/components/editor/riser-diagram-panel.tsx +59 -9
  29. package/src/components/editor/room-controls.tsx +1 -1
  30. package/src/components/editor/room-handles-2d.tsx +156 -6
  31. package/src/components/editor/room-handles.tsx +241 -51
  32. package/src/components/editor/room-highlight.tsx +2 -0
  33. package/src/components/editor/room-labels.tsx +29 -15
  34. package/src/components/editor/selection-manager.tsx +15 -102
  35. package/src/components/editor/separator-end-grip.tsx +74 -0
  36. package/src/components/editor/site-edge-labels.tsx +1 -1
  37. package/src/components/editor/thumbnail-generator.test.ts +15 -6
  38. package/src/components/editor/thumbnail-generator.tsx +26 -1
  39. package/src/components/editor/use-floorplan-background-placement.ts +55 -52
  40. package/src/components/editor/views/view-bar.tsx +111 -26
  41. package/src/components/editor/wall-measurement-label.tsx +1 -1
  42. package/src/components/editor/wall-move-side-handles.tsx +5 -1
  43. package/src/components/editor/wall-pull-grip.tsx +193 -0
  44. package/src/components/editor/wall-snap-beacon-layer.tsx +81 -1
  45. package/src/components/editor-2d/floorplan-adjacent-walls-layer.test.ts +76 -0
  46. package/src/components/editor-2d/floorplan-adjacent-walls-layer.tsx +186 -0
  47. package/src/components/editor-2d/floorplan-alignment-guide-layer.tsx +63 -3
  48. package/src/components/editor-2d/floorplan-group-move.tsx +31 -3
  49. package/src/components/editor-2d/floorplan-measurement-tool-layer.test.ts +6 -0
  50. package/src/components/editor-2d/floorplan-measurement-tool-layer.tsx +22 -13
  51. package/src/components/editor-2d/floorplan-open-wall-ends-layer.tsx +11 -3
  52. package/src/components/editor-2d/floorplan-registry-move-overlay.tsx +7 -1
  53. package/src/components/editor-2d/floorplan-snap-beacon-layer.tsx +96 -4
  54. package/src/components/editor-2d/renderers/floorplan-dimension-edit-overlay.tsx +52 -12
  55. package/src/components/editor-2d/renderers/floorplan-registry-layer.test.ts +12 -1
  56. package/src/components/editor-2d/renderers/floorplan-registry-layer.tsx +197 -37
  57. package/src/components/procedural-items/inspector.tsx +59 -12
  58. package/src/components/systems/ceiling/ceiling-bracket-batch.test.ts +2 -0
  59. package/src/components/systems/ceiling/ceiling-system.test.ts +141 -0
  60. package/src/components/systems/ceiling/ceiling-system.tsx +53 -18
  61. package/src/components/tools/distribution/distribution-run-tool.tsx +50 -26
  62. package/src/components/tools/item/placement-history.test.tsx +1 -1
  63. package/src/components/tools/item/placement-strategies.test.ts +64 -1
  64. package/src/components/tools/item/placement-strategies.ts +5 -9
  65. package/src/components/tools/item/use-placement-coordinator.tsx +30 -34
  66. package/src/components/tools/registry/item-surface-move.test.ts +11 -0
  67. package/src/components/tools/select/select-candidates.test.ts +45 -0
  68. package/src/components/tools/select/select-candidates.ts +13 -2
  69. package/src/components/tools/shared/cursor-sphere.tsx +10 -3
  70. package/src/components/tools/shared/draft-measurement-label.tsx +1 -1
  71. package/src/components/tools/shared/placement-box.tsx +1 -1
  72. package/src/components/tools/shared/placement-dimension-guides.tsx +1 -1
  73. package/src/components/tools/site/terrain-brush-cursor.tsx +1 -1
  74. package/src/components/tools/wall/wall-drafting.test.ts +299 -23
  75. package/src/components/tools/wall/wall-drafting.ts +132 -24
  76. package/src/components/tools/wall/wall-pull-snap.test.ts +274 -0
  77. package/src/components/tools/wall/wall-pull-snap.ts +563 -0
  78. package/src/components/tools/wall/wall-snap-geometry.test.ts +113 -1
  79. package/src/components/tools/wall/wall-snap-geometry.ts +137 -0
  80. package/src/components/tools/zone/zone-tool.tsx +3 -3
  81. package/src/components/ui/action-menu/action-button.tsx +2 -0
  82. package/src/components/ui/action-menu/camera-actions.tsx +8 -7
  83. package/src/components/ui/action-menu/control-modes.tsx +9 -5
  84. package/src/components/ui/action-menu/follow-pascal-pill.tsx +3 -1
  85. package/src/components/ui/action-menu/follow-pascal-toggle.tsx +7 -1
  86. package/src/components/ui/action-menu/furnish-tools.tsx +7 -6
  87. package/src/components/ui/action-menu/index.tsx +1 -0
  88. package/src/components/ui/action-menu/measurement-control.tsx +35 -29
  89. package/src/components/ui/action-menu/point-ask-button.tsx +4 -3
  90. package/src/components/ui/action-menu/structure-tools.tsx +25 -23
  91. package/src/components/ui/action-menu/view-toggles.test.ts +18 -0
  92. package/src/components/ui/action-menu/view-toggles.tsx +256 -51
  93. package/src/components/ui/build-panel/build-panel.tsx +163 -25
  94. package/src/components/ui/command-palette/editor-commands.tsx +38 -11
  95. package/src/components/ui/controls/finish-swatch.tsx +45 -19
  96. package/src/components/ui/controls/material-picker.tsx +24 -17
  97. package/src/components/ui/controls/metric-control.test.ts +52 -0
  98. package/src/components/ui/controls/metric-control.tsx +62 -24
  99. package/src/components/ui/controls/segmented-control.tsx +5 -2
  100. package/src/components/ui/controls/slider-control.test.tsx +23 -0
  101. package/src/components/ui/controls/slider-control.tsx +30 -14
  102. package/src/components/ui/floating-level-selector.tsx +100 -48
  103. package/src/components/ui/helpers/building-helper.tsx +5 -3
  104. package/src/components/ui/helpers/contextual-helper-panel.tsx +97 -37
  105. package/src/components/ui/helpers/helper-manager.test.tsx +53 -3
  106. package/src/components/ui/helpers/helper-manager.tsx +82 -57
  107. package/src/components/ui/helpers/item-helper.tsx +6 -4
  108. package/src/components/ui/helpers/registered-tool-helper.tsx +3 -1
  109. package/src/components/ui/level-duplicate-dialog.tsx +32 -23
  110. package/src/components/ui/level-wall-copy-prompt.tsx +124 -0
  111. package/src/components/ui/panels/collections/collections-popover.tsx +11 -11
  112. package/src/components/ui/panels/deck-boards-fields.tsx +136 -0
  113. package/src/components/ui/panels/floor-foundation-panel.tsx +113 -38
  114. package/src/components/ui/panels/floor-opening-panel.tsx +73 -25
  115. package/src/components/ui/panels/inspector-extensions.tsx +66 -0
  116. package/src/components/ui/panels/mobile-panel-sheet.tsx +3 -1
  117. package/src/components/ui/panels/mobile-selection-bar.tsx +8 -6
  118. package/src/components/ui/panels/multi-field-value.test.ts +27 -0
  119. package/src/components/ui/panels/multi-field-value.ts +20 -1
  120. package/src/components/ui/panels/multi-height-mode.tsx +8 -7
  121. package/src/components/ui/panels/multi-parametric-inspector.tsx +122 -34
  122. package/src/components/ui/panels/multi-selection-panel.tsx +75 -35
  123. package/src/components/ui/panels/node-display.ts +35 -24
  124. package/src/components/ui/panels/panel-manager.tsx +5 -3
  125. package/src/components/ui/panels/panel-wrapper.tsx +22 -109
  126. package/src/components/ui/panels/parametric-field-control.tsx +13 -10
  127. package/src/components/ui/panels/parametric-inspector.tsx +18 -12
  128. package/src/components/ui/panels/reference-panel.tsx +51 -35
  129. package/src/components/ui/panels/room-construction-rows.tsx +464 -65
  130. package/src/components/ui/panels/room-name-options.test.ts +36 -3
  131. package/src/components/ui/panels/room-name-options.ts +110 -11
  132. package/src/components/ui/panels/room-openings-field.tsx +21 -12
  133. package/src/components/ui/panels/room-panel.tsx +206 -99
  134. package/src/components/ui/panels/shape-choice.tsx +7 -5
  135. package/src/components/ui/panels/wall-region-list.tsx +7 -7
  136. package/src/components/ui/primitives/shortcut-token.tsx +10 -6
  137. package/src/components/ui/sidebar/icon-rail.tsx +11 -9
  138. package/src/components/ui/sidebar/panels/items-panel/function-tree-panel.tsx +10 -10
  139. package/src/components/ui/sidebar/panels/items-panel/index.tsx +10 -9
  140. package/src/components/ui/sidebar/panels/settings-panel/index.tsx +3 -0
  141. package/src/components/ui/sidebar/panels/settings-panel/keyboard-shortcuts-dialog.tsx +102 -32
  142. package/src/components/ui/sidebar/panels/site-panel/building-tree-node.tsx +4 -3
  143. package/src/components/ui/sidebar/panels/site-panel/ceiling-tree-node.tsx +4 -2
  144. package/src/components/ui/sidebar/panels/site-panel/chimney-tree-node.tsx +4 -2
  145. package/src/components/ui/sidebar/panels/site-panel/collection-tree-node.tsx +7 -5
  146. package/src/components/ui/sidebar/panels/site-panel/column-tree-node.tsx +4 -2
  147. package/src/components/ui/sidebar/panels/site-panel/default-names.ts +75 -0
  148. package/src/components/ui/sidebar/panels/site-panel/door-tree-node.tsx +4 -2
  149. package/src/components/ui/sidebar/panels/site-panel/dormer-tree-node.tsx +4 -2
  150. package/src/components/ui/sidebar/panels/site-panel/elevator-tree-node.tsx +4 -2
  151. package/src/components/ui/sidebar/panels/site-panel/fence-tree-node.tsx +4 -2
  152. package/src/components/ui/sidebar/panels/site-panel/gutter-tree-node.tsx +4 -2
  153. package/src/components/ui/sidebar/panels/site-panel/index.tsx +117 -64
  154. package/src/components/ui/sidebar/panels/site-panel/inline-rename-input.tsx +36 -3
  155. package/src/components/ui/sidebar/panels/site-panel/item-tree-node.tsx +4 -2
  156. package/src/components/ui/sidebar/panels/site-panel/level-tree-node.tsx +7 -4
  157. package/src/components/ui/sidebar/panels/site-panel/registry-tree-node.tsx +8 -2
  158. package/src/components/ui/sidebar/panels/site-panel/roof-tree-node.tsx +8 -5
  159. package/src/components/ui/sidebar/panels/site-panel/shelf-tree-node.tsx +4 -2
  160. package/src/components/ui/sidebar/panels/site-panel/slab-tree-node.tsx +8 -3
  161. package/src/components/ui/sidebar/panels/site-panel/solar-panel-tree-node.tsx +4 -2
  162. package/src/components/ui/sidebar/panels/site-panel/spawn-tree-node.tsx +4 -2
  163. package/src/components/ui/sidebar/panels/site-panel/stair-tree-node.tsx +8 -7
  164. package/src/components/ui/sidebar/panels/site-panel/tree-node-actions.tsx +7 -6
  165. package/src/components/ui/sidebar/panels/site-panel/tree-node.tsx +16 -1
  166. package/src/components/ui/sidebar/panels/site-panel/unit-tree-node.tsx +15 -8
  167. package/src/components/ui/sidebar/panels/site-panel/wall-tree-node.tsx +4 -2
  168. package/src/components/ui/sidebar/panels/site-panel/window-tree-node.tsx +4 -2
  169. package/src/components/ui/sidebar/panels/site-panel/zone-membership-checkbox.tsx +3 -1
  170. package/src/components/ui/sidebar/panels/site-panel/zone-tree-node.tsx +18 -8
  171. package/src/components/ui/sidebar/panels/zone-panel/index.tsx +25 -14
  172. package/src/components/ui/sidebar/tab-bar.tsx +4 -1
  173. package/src/components/ui/sidebar/use-plugin-panels.tsx +19 -10
  174. package/src/components/ui/snap-target-badge.tsx +7 -6
  175. package/src/components/ui/wall-type/wall-type-editor-dialog.tsx +721 -0
  176. package/src/components/ui/wall-type/wall-type-menu.tsx +167 -0
  177. package/src/components/viewer/floorplan-compass-button.tsx +12 -3
  178. package/src/components/viewer/floorplan-preview.tsx +27 -19
  179. package/src/components/viewer/render-settings-menu.tsx +294 -0
  180. package/src/components/viewer/sky-control.tsx +710 -0
  181. package/src/components/viewer/use-viewer-ui-sounds.test.ts +15 -0
  182. package/src/components/viewer/use-viewer-ui-sounds.ts +46 -0
  183. package/src/components/viewer/viewer-controls-bar.tsx +278 -260
  184. package/src/components/viewer/viewer-hover-label.tsx +12 -4
  185. package/src/components/viewer/viewer-rooms.tsx +61 -4
  186. package/src/components/viewer/viewer-scene-header.tsx +28 -17
  187. package/src/components/viewer/viewer-stage-switcher.tsx +4 -2
  188. package/src/components/viewer-overlay.tsx +8 -0
  189. package/src/components/walkthrough-hud.tsx +75 -2
  190. package/src/hooks/use-keyboard.test.ts +85 -4
  191. package/src/hooks/use-keyboard.ts +65 -7
  192. package/src/hooks/use-selected-room.ts +12 -1
  193. package/src/i18n/floor-footprint-name.ts +30 -0
  194. package/src/i18n/level-name.ts +39 -0
  195. package/src/i18n/locales/en/messages.po +15117 -0
  196. package/src/i18n/locales/en/messages.ts +1 -0
  197. package/src/i18n/locales/en-XA/messages.ts +1 -0
  198. package/src/i18n/locales/es/messages.po +37501 -0
  199. package/src/i18n/locales/es/messages.ts +1 -0
  200. package/src/i18n/locales/fr/messages.po +37501 -0
  201. package/src/i18n/locales/fr/messages.ts +1 -0
  202. package/src/i18n/locales/ja/messages.po +37501 -0
  203. package/src/i18n/locales/ja/messages.ts +1 -0
  204. package/src/i18n/locales/zh-CN/messages.po +37501 -0
  205. package/src/i18n/locales/zh-CN/messages.ts +1 -0
  206. package/src/i18n/locales.test.ts +63 -0
  207. package/src/i18n/locales.ts +32 -0
  208. package/src/i18n/message-slot.tsx +25 -0
  209. package/src/i18n/node-messages.ts +169 -0
  210. package/src/i18n/pseudo-locale.ts +17 -0
  211. package/src/i18n/published-entry.test.ts +17 -0
  212. package/src/i18n/scene-theme-name.ts +16 -0
  213. package/src/index.tsx +156 -4
  214. package/src/lib/add-storey.test.ts +3 -3
  215. package/src/lib/contextual-help.test.ts +20 -1
  216. package/src/lib/contextual-help.ts +95 -19
  217. package/src/lib/continuation.ts +38 -11
  218. package/src/lib/floor-footprints.test.ts +2 -1
  219. package/src/lib/floor-foundation-support.test.ts +7 -3
  220. package/src/lib/floorplan/dimension-drive.test.ts +36 -2
  221. package/src/lib/floorplan/dimension-drive.ts +68 -3
  222. package/src/lib/floorplan/floorplan-export.test.ts +16 -5
  223. package/src/lib/floorplan/floorplan-export.tsx +13 -16
  224. package/src/lib/floorplan/floorplan-extension.test.ts +11 -12
  225. package/src/lib/floorplan/floorplan-extension.ts +37 -18
  226. package/src/lib/floorplan/floorplan-mode.test.ts +13 -6
  227. package/src/lib/floorplan/floorplan-mode.ts +14 -12
  228. package/src/lib/floorplan/floorplan-plugin-install.test.tsx +1 -1
  229. package/src/lib/floorplan/floorplan-readonly.ts +3 -3
  230. package/src/lib/floorplan/schedules.ts +10 -6
  231. package/src/lib/floorplan/site-plan/site-annotations.test.ts +46 -1
  232. package/src/lib/floorplan/site-plan/site-annotations.ts +3 -0
  233. package/src/lib/floorplan/site-plan/site-parts.ts +6 -3
  234. package/src/lib/geometry-script/author.test.ts +114 -0
  235. package/src/lib/geometry-script/author.ts +55 -23
  236. package/src/lib/geometry-script/client.ts +3 -0
  237. package/src/lib/geometry-script/finish-scripts.ts +7 -7
  238. package/src/lib/glb-export.test.ts +327 -0
  239. package/src/lib/glb-export.ts +70 -2
  240. package/src/lib/grid-frame.test.ts +56 -3
  241. package/src/lib/grid-frame.ts +28 -1
  242. package/src/lib/history.test.ts +2 -1
  243. package/src/lib/hud-title.ts +48 -24
  244. package/src/lib/ifc-export.test.ts +41 -1
  245. package/src/lib/inspector-card-mode.test.ts +3 -31
  246. package/src/lib/inspector-card-mode.ts +5 -10
  247. package/src/lib/inspector-expanded.test.ts +13 -0
  248. package/src/lib/inspector-expanded.ts +13 -1
  249. package/src/lib/keyboard-pan.test.ts +7 -0
  250. package/src/lib/keyboard-pan.ts +9 -2
  251. package/src/lib/layer-thickness.test.ts +22 -0
  252. package/src/lib/layer-thickness.ts +79 -0
  253. package/src/lib/lot/drop-in.test.ts +59 -1
  254. package/src/lib/lot/drop-in.ts +42 -50
  255. package/src/lib/material-swatch-style.ts +24 -0
  256. package/src/lib/measurement-parser.ts +7 -3
  257. package/src/lib/measurements.test.ts +17 -0
  258. package/src/lib/measurements.ts +20 -5
  259. package/src/lib/mezzanine-selection.test.ts +3 -1
  260. package/src/lib/orbit-near-plane.test.ts +12 -0
  261. package/src/lib/orbit-near-plane.ts +13 -0
  262. package/src/lib/paint-regions.test.ts +8 -5
  263. package/src/lib/paint-scope.test.ts +53 -0
  264. package/src/lib/paint-scope.ts +18 -4
  265. package/src/lib/panel-rows.ts +17 -8
  266. package/src/lib/reference-scale.ts +83 -0
  267. package/src/lib/room-built-on.test.ts +26 -12
  268. package/src/lib/room-built-on.ts +20 -2
  269. package/src/lib/room-construction.test.ts +28 -1
  270. package/src/lib/room-construction.ts +14 -0
  271. package/src/lib/room-controls-phase6.test.ts +140 -0
  272. package/src/lib/room-handle-drag.ts +557 -42
  273. package/src/lib/room-name-catalog.test.ts +8 -0
  274. package/src/lib/room-name-catalog.ts +8 -1
  275. package/src/lib/room-name-messages.test.ts +132 -0
  276. package/src/lib/room-name-messages.ts +147 -0
  277. package/src/lib/room-name-storage.test.ts +334 -0
  278. package/src/lib/room-push-dimensions.test.ts +37 -0
  279. package/src/lib/room-push-dimensions.ts +226 -2
  280. package/src/lib/room-selection.test.ts +23 -6
  281. package/src/lib/scene-clipboard.test.ts +70 -49
  282. package/src/lib/scene-clipboard.ts +9 -4
  283. package/src/lib/selection-cycle.ts +63 -0
  284. package/src/lib/separator-edit.test.ts +431 -0
  285. package/src/lib/separator-edit.ts +179 -0
  286. package/src/lib/separator-lines.ts +38 -0
  287. package/src/lib/sfx-bus.ts +27 -1
  288. package/src/lib/sfx-player.ts +16 -0
  289. package/src/lib/tool-cursor-icon.ts +5 -1
  290. package/src/lib/tool-shortcuts.ts +130 -0
  291. package/src/lib/viewer-selection.test.ts +28 -24
  292. package/src/lib/viewer-selection.ts +20 -5
  293. package/src/lib/wall-draw-variant.test.ts +7 -6
  294. package/src/lib/wall-draw-variant.ts +107 -6
  295. package/src/lib/wall-nudge.test.ts +44 -0
  296. package/src/lib/wall-nudge.ts +131 -0
  297. package/src/lib/wall-parallel-snap.test.ts +83 -0
  298. package/src/lib/wall-parallel-snap.ts +176 -0
  299. package/src/lib/wall-placement-pointer.test.ts +23 -0
  300. package/src/lib/wall-placement-pointer.ts +18 -0
  301. package/src/lib/wall-pull.test.ts +245 -0
  302. package/src/lib/wall-pull.ts +209 -0
  303. package/src/lib/wall-push-line-up.test.ts +132 -0
  304. package/src/lib/wall-region-list.ts +14 -5
  305. package/src/lib/wall-run-snap.test.ts +188 -0
  306. package/src/lib/wall-run-snap.ts +83 -0
  307. package/src/lib/wall-type.test.ts +125 -0
  308. package/src/lib/wall-type.ts +119 -0
  309. package/src/lib/window-interaction.test.ts +42 -0
  310. package/src/lib/window-interaction.ts +47 -5
  311. package/src/lib/world-grid-snap.test.ts +139 -2
  312. package/src/lib/world-grid-snap.ts +102 -105
  313. package/src/store/use-alignment-guides.ts +27 -1
  314. package/src/store/use-editor.tsx +48 -0
  315. package/src/store/use-floorplan-annotation-visibility.test.ts +64 -0
  316. package/src/store/use-floorplan-annotation-visibility.ts +47 -22
  317. package/src/store/use-wall-snap-indicator.ts +15 -2
@@ -10,7 +10,6 @@ import {
10
10
  useScene,
11
11
  } from '@pascal-app/core'
12
12
  import { useViewer } from '@pascal-app/viewer'
13
- import { Html } from '@react-three/drei'
14
13
  import { type ThreeEvent, useFrame, useThree } from '@react-three/fiber'
15
14
  import { useRef, useState } from 'react'
16
15
  import { type Group, Plane, Vector2, Vector3 } from 'three'
@@ -26,6 +25,7 @@ import { runRoomHandleDrag } from '../../lib/room-handle-drag'
26
25
  import useEditor, { isGridSnapActive } from '../../store/use-editor'
27
26
  import useInteractionScope from '../../store/use-interaction-scope'
28
27
  import { suppressBoxSelectForPointer } from '../tools/select/box-select-state'
28
+ import { Html } from './localized-html'
29
29
  import { ElevationArrows, outlineCentroid, useHandleScale } from './room-handles'
30
30
 
31
31
  export const FOOTPRINT_HEIGHT_DRAG_LABEL = 'footprint-height'
@@ -15,7 +15,8 @@ import { MeshBasicNodeMaterial } from 'three/webgpu'
15
15
  import { useCeilingEvents } from '../../hooks/use-ceiling-events'
16
16
  import { useGridEvents } from '../../hooks/use-grid-events'
17
17
  import { getPlacementSurface, usesOrientedPlacementPlane } from '../../lib/active-placement-surface'
18
- import { gridLocalNormal, gridLocalPoint } from '../../lib/grid-frame'
18
+ import { gridLocalNormal, gridLocalPoint, layHorizontalGrid } from '../../lib/grid-frame'
19
+ import { getActiveGridPose } from '../../lib/world-grid-snap'
19
20
  import useEditor, { getActiveSnapContext, isGridSnapActive } from '../../store/use-editor'
20
21
  import { getMovingNode } from '../../store/use-interaction-scope'
21
22
 
@@ -27,10 +28,9 @@ export const EDITOR_GRID_INPUT_NAME = 'pascal-editor-grid-input'
27
28
  const PLACEMENT_REVEAL_RADIUS = 12
28
29
 
29
30
  const UP = new Vector3(0, 1, 0)
30
- // PlaneGeometry faces +Z; this is the orientation that lays it flat (its normal
31
- // → world +Y), equivalent to the old `rotation-x={-π/2}`.
31
+ // PlaneGeometry faces +Z; the oriented (wall-plane) lattice turns this normal
32
+ // into the host's. The flat lattice is laid by `layHorizontalGrid`.
32
33
  const PLANE_LOCAL_NORMAL = new Vector3(0, 0, 1)
33
- const HORIZONTAL_QUATERNION = new Quaternion().setFromUnitVectors(PLANE_LOCAL_NORMAL, UP)
34
34
 
35
35
  export const Grid = ({
36
36
  cellSize = 0.5,
@@ -184,10 +184,10 @@ export const Grid = ({
184
184
  useCeilingEvents()
185
185
 
186
186
  // Track the last world-space cursor hit. The reveal-fade shader reads
187
- // `positionLocal.xy` (vertex position on the un-transformed plane), and the
188
- // laid-flat orientation maps `positionLocal.y` to world `-Z` relative to the
189
- // mesh origin. The cursor is recomputed every frame from the stored world hit
190
- // so the reveal stays put regardless of where the mesh origin sits.
187
+ // `positionLocal.xy` (vertex position on the un-transformed plane), so the
188
+ // cursor is taken into the plane's frame every frame from the stored world
189
+ // hit — the reveal stays put wherever the mesh origin sits and however it
190
+ // is turned.
191
191
  const lastWorldCursorRef = useRef<Vector3 | null>(null)
192
192
  useEffect(() => {
193
193
  const onGridMove = (event: GridEvent) => {
@@ -279,20 +279,29 @@ export const Grid = ({
279
279
  setGridY(surfacePoint.y)
280
280
  }
281
281
  } else {
282
- // Horizontal: keep the lattice anchored to world XZ (0,0); only the Y
283
- // origin follows the surface height (floor / shelf top). Snap directly —
284
- // the old lerp made the grid visibly drift up to a new floor height.
285
- // Cursor uniform tracks the world cursor (mirrored on Z for the flat plane).
282
+ // Horizontal: lay the lattice on the frame the tools snap in — the
283
+ // active building's yaw and offset, so a turned house's walls sit square
284
+ // on it, or world XZ (0,0) for site work; only the Y origin follows the
285
+ // surface height (floor / shelf top). Snap directly — the old lerp made
286
+ // the grid visibly drift up to a new floor height.
286
287
  const targetY = surfacePoint ? surfacePoint.y : levelY
287
288
  // Visual mesh rides 1mm above the surface: at exactly the surface Y the
288
289
  // lattice is coplanar with the slab top and z-fights it. The event plane
289
290
  // (`gridY` → useGridEvents) stays at the true surface height.
290
- gridMesh.position.set(0, targetY + 0.001, 0)
291
- gridMesh.quaternion.copy(HORIZONTAL_QUATERNION)
291
+ layHorizontalGrid(
292
+ getActiveGridPose(),
293
+ targetY + 0.001,
294
+ gridMesh.position,
295
+ gridMesh.quaternion,
296
+ )
297
+ // Cursor uniform → plane-local XY, as for the wall plane above.
292
298
  const world = lastWorldCursorRef.current
293
299
  if (world) {
300
+ invQuatRef.current.copy(gridMesh.quaternion).invert()
294
301
  const local = gridLocalPoint(gridMesh.parent, world, localCursorRef.current)
295
- cursorPositionRef.current.set(local.x, -local.z)
302
+ .sub(gridMesh.position)
303
+ .applyQuaternion(invQuatRef.current)
304
+ cursorPositionRef.current.set(local.x, local.y)
296
305
  }
297
306
  if (lastGridYRef.current !== targetY) {
298
307
  lastGridYRef.current = targetY
@@ -41,6 +41,7 @@ import useFloorplanMode from '../../store/use-floorplan-mode'
41
41
  import useInteractionScope from '../../store/use-interaction-scope'
42
42
  import { useFloorplanGroupDrag } from '../editor-2d/floorplan-group-move'
43
43
  import {
44
+ beginGroupWallPush,
44
45
  classifyParticipant,
45
46
  collectParticipants,
46
47
  computeGroupBox,
@@ -185,6 +186,9 @@ export function startGroupPickUp(
185
186
  handle: GROUP_MOVE_DRAG_LABEL,
186
187
  })
187
188
  document.body.style.cursor = 'grabbing'
189
+ // Walls alone move as their push arrows do; copies (paste, duplicate) are
190
+ // carried rigidly, joined to nothing yet.
191
+ const push = opts.scopeToSelection ? null : beginGroupWallPush(starts, levelId)
188
192
 
189
193
  const applyMove = (e: PointerEvent) => {
190
194
  const plan = resolvePlanPoint(e)
@@ -202,7 +206,8 @@ export function startGroupPickUp(
202
206
  let dx = snap ? Math.round(rawDx / step) * step : rawDx
203
207
  let dz = snap ? Math.round(rawDz / step) * step : rawDz
204
208
 
205
- if (isAlignmentGuideActive() && candidates.length > 0 && restAnchors.length > 0) {
209
+ // Pushed walls don't move as one footprint, so they have no box to align.
210
+ if (!push && isAlignmentGuideActive() && candidates.length > 0 && restAnchors.length > 0) {
206
211
  const result = resolveAlignment({
207
212
  moving: restAnchors.map((a) => ({ ...a, x: a.x + dx, z: a.z + dz })),
208
213
  candidates,
@@ -225,6 +230,10 @@ export function startGroupPickUp(
225
230
  sfxEmitter.emit('sfx:grid-snap')
226
231
  lastDelta = [dx, dz]
227
232
  }
233
+ if (push) {
234
+ push.preview([dx, dz])
235
+ return
236
+ }
228
237
 
229
238
  const entries = translateGroupPatches(starts, links, dx, dz)
230
239
  const patchById = new Map(entries)
@@ -252,6 +261,8 @@ export function startGroupPickUp(
252
261
  // the current delta — the carried group turns exactly like the idle
253
262
  // keyboard rotate, and the placement stays a single updateNodes.
254
263
  const rotateCarried = (direction: 1 | -1) => {
264
+ // Pushed walls keep their bearing: there is nothing to turn.
265
+ if (push) return
255
266
  const pivot = { x: restCenter[0], z: restCenter[1] }
256
267
  const delta = -direction * (Math.PI / 4)
257
268
  const rotated = rotateGroupSnapshots(starts, links, pivot, delta)
@@ -271,6 +282,7 @@ export function startGroupPickUp(
271
282
  }
272
283
 
273
284
  const clearLivePreviews = () => {
285
+ push?.cancel()
274
286
  const overrides = useLiveNodeOverrides.getState()
275
287
  const liveTransforms = useLiveTransforms.getState()
276
288
  for (const id of affectedIds) {
@@ -303,6 +315,12 @@ export function startGroupPickUp(
303
315
  const commit = () => {
304
316
  swallowNextClick()
305
317
  sfxEmitter.emit('sfx:item-place')
318
+ if (push) {
319
+ resumeSceneHistory(useScene)
320
+ push.commit()
321
+ teardown()
322
+ return
323
+ }
306
324
  const overrides = useLiveNodeOverrides.getState()
307
325
  const updates: { id: AnyNodeId; data: Partial<AnyNode> }[] = []
308
326
  for (const id of affectedIds) {
@@ -2,7 +2,6 @@
2
2
 
3
3
  import { type AnyNodeId, useScene } from '@pascal-app/core'
4
4
  import { useViewer } from '@pascal-app/viewer'
5
- import { Html } from '@react-three/drei'
6
5
  import { useFrame } from '@react-three/fiber'
7
6
  import { useCallback, useMemo, useRef } from 'react'
8
7
  import * as THREE from 'three'
@@ -21,6 +20,7 @@ import {
21
20
  computeGroupPlanBox,
22
21
  levelFrame,
23
22
  } from './group-transform-shared'
23
+ import { Html } from './localized-html'
24
24
  import { NodeActionMenu } from './node-action-menu'
25
25
  import { useMeshSettleEpoch } from './use-mesh-settle-epoch'
26
26
 
@@ -16,6 +16,7 @@ import { useViewer } from '@pascal-app/viewer'
16
16
  import { type Camera, Plane, type Raycaster, Vector2, Vector3 } from 'three'
17
17
  import { GROUP_MOVE_DRAG_LABEL } from '../../lib/contextual-help'
18
18
  import { isHistoryShortcut } from '../../lib/history'
19
+ import type { WallGroupPush } from '../../lib/room-handle-drag'
19
20
  import { sfxEmitter } from '../../lib/sfx-bus'
20
21
  import { isTypingTarget } from '../../lib/typing-target'
21
22
  import useAlignmentGuides from '../../store/use-alignment-guides'
@@ -29,6 +30,7 @@ import { useFloorplanGroupDrag } from '../editor-2d/floorplan-group-move'
29
30
  import { suppressBoxSelectForPointer } from '../tools/select/box-select-state'
30
31
  import { startGroupPickUp } from './group-actions'
31
32
  import {
33
+ beginGroupWallPush,
32
34
  classifyParticipant,
33
35
  collectParticipants,
34
36
  computeGroupBox,
@@ -99,6 +101,8 @@ export function armGroupMove3d(args: {
99
101
  startLocal: Vector3
100
102
  frameInv: ReturnType<typeof levelFrame>['inverse']
101
103
  lastDelta: Vec2 | null
104
+ /** Walls alone: each moves as its push arrow does instead of the rigid slide. */
105
+ push: WallGroupPush | null
102
106
  }
103
107
  let session: Session | null = null
104
108
 
@@ -179,6 +183,7 @@ export function armGroupMove3d(args: {
179
183
  startLocal,
180
184
  frameInv,
181
185
  lastDelta: null,
186
+ push: beginGroupWallPush(starts, levelId),
182
187
  }
183
188
  }
184
189
 
@@ -202,8 +207,14 @@ export function armGroupMove3d(args: {
202
207
  : moveLocal.z - s.startLocal.z
203
208
 
204
209
  // Figma-style alignment layered on top: guides display in every mode
205
- // except Off; the magnetic pull applies only in 'lines' mode.
206
- if (isAlignmentGuideActive() && s.candidates.length > 0 && s.restAnchors.length > 0) {
210
+ // except Off; the magnetic pull applies only in 'lines' mode. Pushed walls
211
+ // don't move as one footprint, so they have no box to align.
212
+ if (
213
+ !s.push &&
214
+ isAlignmentGuideActive() &&
215
+ s.candidates.length > 0 &&
216
+ s.restAnchors.length > 0
217
+ ) {
207
218
  const result = resolveAlignment({
208
219
  moving: s.restAnchors.map((a) => ({ ...a, x: a.x + dx, z: a.z + dz })),
209
220
  candidates: s.candidates,
@@ -227,6 +238,10 @@ export function armGroupMove3d(args: {
227
238
  sfxEmitter.emit('sfx:grid-snap')
228
239
  s.lastDelta = [dx, dz]
229
240
  }
241
+ if (s.push) {
242
+ s.push.preview([dx, dz])
243
+ return
244
+ }
230
245
 
231
246
  const entries = translateGroupPatches(s.starts, s.links, dx, dz)
232
247
  const patchById = new Map(entries)
@@ -255,6 +270,8 @@ export function armGroupMove3d(args: {
255
270
  // current delta — the carried group turns exactly like the idle keyboard
256
271
  // rotate, and the commit stays a single updateNodes.
257
272
  const rotateSession = (s: Session, direction: 1 | -1) => {
273
+ // Pushed walls keep their bearing: there is nothing to turn.
274
+ if (s.push) return
258
275
  const pivot = { x: s.restCenter[0], z: s.restCenter[1] }
259
276
  const delta = -direction * (Math.PI / 4)
260
277
  const rotated = rotateGroupSnapshots(s.starts, s.links, pivot, delta)
@@ -276,6 +293,7 @@ export function armGroupMove3d(args: {
276
293
  }
277
294
 
278
295
  const clearLivePreviews = (s: Session) => {
296
+ s.push?.cancel()
279
297
  const overrides = useLiveNodeOverrides.getState()
280
298
  const liveTransforms = useLiveTransforms.getState()
281
299
  for (const id of s.affectedIds) {
@@ -342,6 +360,13 @@ export function armGroupMove3d(args: {
342
360
  }
343
361
  swallowNextClick()
344
362
  sfxEmitter.emit('sfx:item-place')
363
+ if (session.push) {
364
+ resumeSceneHistory(useScene)
365
+ session.push.commit()
366
+ session = null
367
+ teardown()
368
+ return
369
+ }
345
370
  const overrides = useLiveNodeOverrides.getState()
346
371
  const updates: { id: AnyNodeId; data: Partial<AnyNode> }[] = []
347
372
  for (const id of session.affectedIds) {
@@ -5,6 +5,7 @@ import { useViewer } from '@pascal-app/viewer'
5
5
  import { type ThreeEvent, useThree } from '@react-three/fiber'
6
6
  import { useEffect, useMemo } from 'react'
7
7
  import * as THREE from 'three'
8
+ import { useRoomHandleDrag } from '../../lib/room-handle-drag'
8
9
  import useEditor from '../../store/use-editor'
9
10
  import { useMovingNode } from '../../store/use-interaction-scope'
10
11
  import { useFloorplanGroupDrag } from '../editor-2d/floorplan-group-move'
@@ -38,6 +39,8 @@ export function GroupSelectionBox3D() {
38
39
  const mode = useEditor((s) => s.mode)
39
40
  const movingNode = useMovingNode()
40
41
  const delta = useFloorplanGroupDrag((s) => s.delta)
42
+ // Pushed walls don't move as one box: the room widths they show take its place.
43
+ const wallPush = useRoomHandleDrag((s) => s.drag?.kind === 'wall-group-push')
41
44
  const { camera, raycaster, gl } = useThree()
42
45
 
43
46
  const participantIds = useMemo(
@@ -118,7 +121,7 @@ export function GroupSelectionBox3D() {
118
121
  return new THREE.Vector3(delta[0], 0, delta[1]).applyMatrix4(matrix).sub(origin)
119
122
  }, [delta, levelId])
120
123
 
121
- if (!box || !edges || movingNode || mode !== 'select') return null
124
+ if (!box || !edges || movingNode || wallPush || mode !== 'select') return null
122
125
 
123
126
  const handlePointerDown = (event: ThreeEvent<PointerEvent>) => {
124
127
  const native = event.nativeEvent
@@ -10,6 +10,7 @@ import {
10
10
  type WallNode,
11
11
  } from '@pascal-app/core'
12
12
  import { Box3, type BufferGeometry, Matrix4, type Object3D, Vector3 } from 'three'
13
+ import { beginWallGroupPush, type WallGroupPush } from '../../lib/room-handle-drag'
13
14
 
14
15
  // Shared plumbing for the group transform gizmos (rotate + move). Both operate
15
16
  // on the same multi-selection: classify each participant by how its placement
@@ -486,6 +487,25 @@ export function rotatePlanBounds(
486
487
  return { minX, minZ, maxX, maxZ }
487
488
  }
488
489
 
490
+ /**
491
+ * A group move of walls alone moves each wall as its push arrow does
492
+ * (`planWallGroupPush`) rather than sliding the selection rigidly: null for
493
+ * any other selection, which keeps the rigid slide. Doors and windows are not
494
+ * participants (they ride their wall), so they never make a selection mixed.
495
+ */
496
+ export function beginGroupWallPush(
497
+ starts: readonly ParticipantStart[],
498
+ levelId: string | null,
499
+ ): WallGroupPush | null {
500
+ const nodes = useScene.getState().nodes
501
+ if (!levelId || starts.length === 0) return null
502
+ if (!starts.every((s) => s.kind === 'endpoint' && nodes[s.id]?.type === 'wall')) return null
503
+ return beginWallGroupPush(
504
+ starts.map((s) => s.id),
505
+ levelId,
506
+ )
507
+ }
508
+
489
509
  // Rigid group slide: shift every participant (and each linked neighbour's
490
510
  // shared endpoint) by the same level-frame XZ delta. Y is untouched; the
491
511
  // snapshot's rotation rides along because a mid-gesture R/T turns the