@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
@@ -1,21 +1,30 @@
1
1
  import {
2
2
  type AnyNodeId,
3
3
  DEFAULT_ANGLE_STEP,
4
+ finishWallEdit,
5
+ generateId,
6
+ planSeparatorInsertion,
4
7
  planWallInsertion,
5
8
  planWallSplitAtPoint,
9
+ railingWallDefaults,
6
10
  resolveWallConstruction,
7
11
  runAsSingleSceneHistoryStep,
12
+ type SeparatorNode,
13
+ sameWallConstruction,
8
14
  snapPointAlongAngleRay,
9
15
  useScene,
10
16
  type WallConstructionOptions,
11
17
  type WallNode,
18
+ wallsRunAlong,
12
19
  } from '@pascal-app/core'
13
20
  import { useViewer } from '@pascal-app/viewer'
14
21
  import { sfxEmitter } from '../../../lib/sfx-bus'
15
22
  import { resolveSnapFlags } from '../../../lib/snapping-mode'
23
+ import { getWallLineKind } from '../../../lib/wall-draw-variant'
16
24
  import useEditor, { getActiveSnappingMode, isMagneticSnapActive } from '../../../store/use-editor'
17
25
  import {
18
26
  distanceSquared,
27
+ findWallBodySnapTarget,
19
28
  findWallSnapTarget,
20
29
  findWallSpecialPointSnap,
21
30
  WALL_CONNECT_SNAP_RADIUS,
@@ -109,14 +118,30 @@ type SnapWallDraftArgs = {
109
118
  */
110
119
  magnetic?: boolean
111
120
  /**
112
- * Optional grid-snap override. Lets the caller route grid snapping
113
- * through a world-XZ aligned snap (so a rotated building's draft
114
- * lands on the visible grid). When omitted, falls back to the
115
- * local-axis grid at `step`.
121
+ * Optional grid-snap override, in the caller's plan frame. When omitted,
122
+ * falls back to the building-local grid at `step` — the lattice the wall
123
+ * tools draft on, square to the house however the building is turned.
116
124
  */
117
125
  gridSnap?: (point: WallPlanPoint) => WallPlanPoint
118
126
  /** Optional magnetic snap radii. Omitted means wall tools keep their defaults. */
119
127
  snapRadii?: WallSnapRadii
128
+ /**
129
+ * The point is a wall end (drawn or dragged): in the non-magnetic modes an
130
+ * end aimed into another wall's drawn body, or within the connect radius of
131
+ * either face, lands on that wall's reference line — where the wall
132
+ * topology T-joins it — ahead of the grid / angle placement, keeping the
133
+ * segment's direction from `start` (see `findWallBodySnapTarget`). Without
134
+ * it the grid quantises an end aimed at a face to the nearest lattice point,
135
+ * a fraction short of the face, and the end stays open with a gap.
136
+ */
137
+ captureWallBodies?: boolean
138
+ /**
139
+ * The walls `captureWallBodies` may join an end into; `walls` when omitted.
140
+ * A body capture T-joins the wall, so a caller that adds reference walls it
141
+ * can't join (the floor below) keeps them out of this list while they still
142
+ * serve the corner and edge snaps.
143
+ */
144
+ bodyWalls?: WallNode[]
120
145
  }
121
146
 
122
147
  export function snapWallDraftPointDetailed(args: SnapWallDraftArgs): WallDraftSnapResult {
@@ -131,6 +156,8 @@ export function snapWallDraftPointDetailed(args: SnapWallDraftArgs): WallDraftSn
131
156
  magnetic = true,
132
157
  gridSnap,
133
158
  snapRadii,
159
+ captureWallBodies = false,
160
+ bodyWalls = walls,
134
161
  } = args
135
162
 
136
163
  if (bypassSnap) return { point, snap: null, targetWallIds: [] }
@@ -144,9 +171,8 @@ export function snapWallDraftPointDetailed(args: SnapWallDraftArgs): WallDraftSn
144
171
  }
145
172
 
146
173
  const step = overrideStep ?? getSegmentGridStep()
147
- // The angle path snaps the distance ALONG the 15° ray — a scalar, the
148
- // same in world and local frames — so the `gridSnap` world-grid override
149
- // only applies when the angle lock is off.
174
+ // The angle path snaps the distance ALONG the 15° ray from `start`, so the
175
+ // `gridSnap` override only applies when the angle lock is off.
150
176
  const basePoint: WallPlanPoint =
151
177
  start && angleSnap
152
178
  ? [...snapPointAlongAngleRay(start, point, DEFAULT_ANGLE_STEP, step)]
@@ -182,6 +208,30 @@ export function snapWallDraftPointDetailed(args: SnapWallDraftArgs): WallDraftSn
182
208
  }
183
209
  const connectSpecial = findWallSpecialPointSnap(basePoint, walls, ignoreWallIds, connectRadii)
184
210
  if (connectSpecial) return connectSpecial
211
+ // A wall end aimed into a wall's body joins it, whichever lattice point or
212
+ // ray station the mode picked near its face.
213
+ const bodySnap = captureWallBodies
214
+ ? findWallBodySnapTarget(point, bodyWalls, {
215
+ ignoreWallIds,
216
+ placed: basePoint,
217
+ start,
218
+ margin: WALL_CONNECT_SNAP_RADIUS,
219
+ })
220
+ : null
221
+ if (bodySnap) {
222
+ // An end aimed into a corner block joins the corner, and reads as one.
223
+ const atCorner = bodyWalls.some(
224
+ (wall) =>
225
+ !ignoreWallIds?.includes(wall.id) &&
226
+ (distanceSquared(bodySnap, wall.start) <= 1e-12 ||
227
+ distanceSquared(bodySnap, wall.end) <= 1e-12),
228
+ )
229
+ return {
230
+ point: bodySnap,
231
+ snap: atCorner ? 'endpoint' : 'wall',
232
+ targetWallIds: wallIdsAtSnapPoint(bodySnap, walls, ignoreWallIds),
233
+ }
234
+ }
185
235
  const connectWall = findWallSnapTarget(basePoint, walls, {
186
236
  ignoreWallIds,
187
237
  radius: WALL_CONNECT_SNAP_RADIUS,
@@ -205,6 +255,36 @@ export function isSegmentLongEnough(start: WallPlanPoint, end: WallPlanPoint): b
205
255
  return distanceSquared(start, end) >= WALL_MIN_LENGTH * WALL_MIN_LENGTH
206
256
  }
207
257
 
258
+ /**
259
+ * An invisible wall drawn `start` → `end` on the current level (the Walls variant's Invisible, a room
260
+ * divider): room separators, each end joined to a wall it lands on and broken
261
+ * either side of a wall it crosses (`planSeparatorInsertion`), as one undo step. Rooms close on
262
+ * it; a room it closes off outside the house becomes a porch. The last piece, for the run to go
263
+ * on from its end; null when it would be too short.
264
+ */
265
+ export function createInvisibleWallOnCurrentLevel(
266
+ start: WallPlanPoint,
267
+ end: WallPlanPoint,
268
+ ): SeparatorNode | null {
269
+ const currentLevelId = useViewer.getState().selection.levelId
270
+ const { nodes, applyNodeChanges } = useScene.getState()
271
+ if (!(currentLevelId && isSegmentLongEnough(start, end))) return null
272
+ const joinRadius = isMagneticSnapActive() ? WALL_JOIN_SNAP_RADIUS : WALL_CONNECT_SNAP_RADIUS
273
+ return runAsSingleSceneHistoryStep(useScene, () => {
274
+ const plan = planSeparatorInsertion(nodes, {
275
+ levelId: currentLevelId as AnyNodeId,
276
+ start,
277
+ end,
278
+ joinRadius,
279
+ mintId: () => generateId('separator'),
280
+ })
281
+ if (!plan) return null
282
+ applyNodeChanges(plan.changes)
283
+ sfxEmitter.emit('sfx:structure-build')
284
+ return plan.separators.at(-1) ?? null
285
+ })
286
+ }
287
+
208
288
  export function createWallOnCurrentLevel(
209
289
  start: WallPlanPoint,
210
290
  end: WallPlanPoint,
@@ -220,13 +300,33 @@ export function createWallOnCurrentLevel(
220
300
  const joinRadius = isMagneticSnapActive() ? WALL_JOIN_SNAP_RADIUS : WALL_CONNECT_SNAP_RADIUS
221
301
 
222
302
  return runAsSingleSceneHistoryStep(useScene, () => {
223
- const result = planWallInsertion(nodes, {
303
+ // The Walls variant's Railing draws railings: a guard of their own.
304
+ const insertion = {
224
305
  levelId: currentLevelId as AnyNodeId,
225
306
  start,
226
307
  end,
227
308
  joinRadius,
228
- wallDefaults: useEditor.getState().toolDefaults.wall ?? {},
229
- })
309
+ wallDefaults:
310
+ getWallLineKind() === 'railing'
311
+ ? railingWallDefaults()
312
+ : (useEditor.getState().toolDefaults.wall ?? {}),
313
+ }
314
+ let result = planWallInsertion(nodes, insertion)
315
+ if (!result.ok && result.reason === 'covered-existing-wall') {
316
+ // Drawn over walls built another way (an outside wall over an inside
317
+ // jog), the new wall takes their stretch over; over walls built the
318
+ // same, it would change nothing, so it stays refused.
319
+ const replacing = planWallInsertion(nodes, { ...insertion, replaceCovered: true })
320
+ if (
321
+ replacing.ok &&
322
+ replacing.plan.insertedWalls.some((wall) =>
323
+ wallsRunAlong(nodes, { ...wall, parentId: currentLevelId } as WallNode).some(
324
+ (covered) => !sameWallConstruction(covered, wall),
325
+ ),
326
+ )
327
+ )
328
+ result = replacing
329
+ }
230
330
  if (!result.ok) return null
231
331
  const { plan } = result
232
332
 
@@ -237,20 +337,28 @@ export function createWallOnCurrentLevel(
237
337
  const sourceAlreadyUpdated = sourceUpdate
238
338
  ? plan.changes.update.some((operation) => operation.id === sourceUpdate.id)
239
339
  : false
240
- applyNodeChanges({
241
- ...plan.changes,
242
- update: plan.changes.update
243
- .map((operation) =>
244
- sourceUpdate?.id === operation.id
245
- ? { ...operation, data: { ...operation.data, ...sourceUpdate.data } }
246
- : operation,
247
- )
248
- .concat(sourceUpdate && !sourceAlreadyUpdated ? [sourceUpdate] : []),
249
- create: plan.changes.create.map((operation) => ({
250
- ...operation,
251
- node: finalizedWallsById.get(operation.node.id as WallNode['id']) ?? operation.node,
252
- })),
253
- })
340
+ // A wall drawn along part of a parallel wall takes that stretch over, so
341
+ // drawing on from a wall never doubles the one it runs along.
342
+ applyNodeChanges(
343
+ finishWallEdit(
344
+ nodes,
345
+ {
346
+ ...plan.changes,
347
+ update: plan.changes.update
348
+ .map((operation) =>
349
+ sourceUpdate?.id === operation.id
350
+ ? { ...operation, data: { ...operation.data, ...sourceUpdate.data } }
351
+ : operation,
352
+ )
353
+ .concat(sourceUpdate && !sourceAlreadyUpdated ? [sourceUpdate] : []),
354
+ create: plan.changes.create.map((operation) => ({
355
+ ...operation,
356
+ node: finalizedWallsById.get(operation.node.id as WallNode['id']) ?? operation.node,
357
+ })),
358
+ },
359
+ { winnerIds: finalizedWalls.map((wall) => wall.id), mintId: () => generateId('wall') },
360
+ ),
361
+ )
254
362
  sfxEmitter.emit('sfx:structure-build')
255
363
 
256
364
  const terminalWall = finalizedWalls.at(-1)!
@@ -0,0 +1,274 @@
1
+ import { describe, expect, test } from 'bun:test'
2
+ import type { WallNode } from '@pascal-app/core'
3
+ import { lockWallPullDirection, snapWallPullPoint, type WallPullSnapInput } from './wall-pull-snap'
4
+ import type { WallPlanPoint } from './wall-snap-geometry'
5
+
6
+ function makeWall(start: WallPlanPoint, end: WallPlanPoint, id?: string): WallNode {
7
+ return {
8
+ object: 'node',
9
+ id: (id ?? `wall_${start.join('_')}_${end.join('_')}`) as WallNode['id'],
10
+ type: 'wall',
11
+ name: 'Wall',
12
+ parentId: null,
13
+ visible: true,
14
+ metadata: {},
15
+ children: [],
16
+ start,
17
+ end,
18
+ thickness: 0.1,
19
+ frontSide: 'unknown',
20
+ backSide: 'unknown',
21
+ } as unknown as WallNode
22
+ }
23
+
24
+ const deg = (degrees: number) => (degrees * Math.PI) / 180
25
+ const polar = (from: WallPlanPoint, degrees: number, length: number): WallPlanPoint => [
26
+ from[0] + Math.cos(deg(degrees)) * length,
27
+ from[1] + Math.sin(deg(degrees)) * length,
28
+ ]
29
+
30
+ function expectPoint(actual: WallPlanPoint, expected: WallPlanPoint) {
31
+ expect(actual[0]).toBeCloseTo(expected[0], 6)
32
+ expect(actual[1]).toBeCloseTo(expected[1], 6)
33
+ }
34
+
35
+ // A 4 × 3 m room; the pull starts at the south wall's east end.
36
+ const ROOM = [
37
+ makeWall([0, 0], [4, 0], 'south'),
38
+ makeWall([4, 0], [4, 3], 'east'),
39
+ makeWall([4, 3], [0, 3], 'north'),
40
+ makeWall([0, 3], [0, 0], 'west'),
41
+ ]
42
+
43
+ function pullFromRoom(point: WallPlanPoint, extra: Partial<WallPullSnapInput> = {}) {
44
+ return snapWallPullPoint({
45
+ point,
46
+ start: [4, 0],
47
+ runStart: [4, 0],
48
+ reference: [1, 0],
49
+ walls: ROOM,
50
+ gridStep: 0.5,
51
+ ...extra,
52
+ })
53
+ }
54
+
55
+ describe('lockWallPullDirection', () => {
56
+ test('holds straight on from the wall it comes from within about 7°', () => {
57
+ expect(lockWallPullDirection(polar([0, 0], 6, 3), [1, 0])).toEqual({
58
+ direction: [1, 0],
59
+ lock: 'straight',
60
+ })
61
+ expect(lockWallPullDirection(polar([0, 0], 8, 3), [1, 0]).lock).toBe('angle')
62
+ })
63
+
64
+ test('holds square to the wall it comes from, either side', () => {
65
+ const left = lockWallPullDirection(polar([0, 0], 85, 3), [1, 0])
66
+ expect(left.lock).toBe('square')
67
+ expectPoint(left.direction, [0, 1])
68
+ const right = lockWallPullDirection(polar([0, 0], -96, 3), [1, 0])
69
+ expect(right.lock).toBe('square')
70
+ expectPoint(right.direction, [0, -1])
71
+ })
72
+
73
+ test('holds straight on to a skewed wall, and the house axes beside it', () => {
74
+ const skew: WallPlanPoint = [Math.cos(deg(20)), Math.sin(deg(20))]
75
+ const straight = lockWallPullDirection(polar([0, 0], 23, 3), skew)
76
+ expect(straight.lock).toBe('straight')
77
+ expectPoint(straight.direction, skew)
78
+ const axis = lockWallPullDirection(polar([0, 0], 4, 3), skew)
79
+ expect(axis.lock).toBe('axis')
80
+ expectPoint(axis.direction, [1, 0])
81
+ })
82
+
83
+ test('past the lock it steps by 15° on the house axes', () => {
84
+ for (const [cursor, expected] of [
85
+ [40, 45],
86
+ [28, 30],
87
+ [52, 45],
88
+ [-118, -120],
89
+ ] as const) {
90
+ const { direction, lock } = lockWallPullDirection(polar([0, 0], cursor, 3), [1, 0])
91
+ expect(lock).toBe('angle')
92
+ expectPoint(direction, polar([0, 0], expected, 1))
93
+ }
94
+ })
95
+ })
96
+
97
+ describe('snapWallPullPoint', () => {
98
+ test('a straight pull steps its length on the grid', () => {
99
+ const result = pullFromRoom([6.6, 0.2])
100
+ expect(result.lock).toBe('straight')
101
+ expect(result.snap).toBe('grid')
102
+ expect(result.label).toBe('Grid')
103
+ expectPoint(result.point, [6.5, 0])
104
+ })
105
+
106
+ test('a square pull holds 90° to the pulled wall', () => {
107
+ const result = pullFromRoom([4.25, -2.6])
108
+ expect(result.lock).toBe('square')
109
+ expectPoint(result.point, [4, -2.5])
110
+ })
111
+
112
+ test('further round it snaps to 15° steps, with a grid length along them', () => {
113
+ const result = pullFromRoom(polar([4, 0], -40, 3))
114
+ expect(result.lock).toBe('angle')
115
+ // 3 m at 40° is 2.99 m along 45°: 3 m on the 0.5 m grid.
116
+ expectPoint(result.point, polar([4, 0], -45, 3))
117
+ })
118
+
119
+ test('Alt is fully free', () => {
120
+ const cursor = polar([4, 0], -40, 3.13)
121
+ const result = pullFromRoom(cursor, { free: true })
122
+ expect(result.lock).toBe('free')
123
+ expect(result.snap).toBeNull()
124
+ expectPoint(result.point, cursor)
125
+ })
126
+
127
+ test('the extension of the pulled wall is always drawn', () => {
128
+ const result = pullFromRoom([5.3, -1.9])
129
+ const extension = result.guides.find((guide) => guide.kind === 'extension')
130
+ expect(extension?.from).toEqual([4, 0])
131
+ expect(extension?.to[1]).toBeCloseTo(0, 9)
132
+ expect(extension?.to[0] ?? 0).toBeGreaterThan(5.3)
133
+ })
134
+
135
+ test('an existing wall end wins over the held direction', () => {
136
+ const result = snapWallPullPoint({
137
+ point: [4.15, 3.2],
138
+ start: [7, 3],
139
+ runStart: [4, 0],
140
+ reference: [0, 1],
141
+ walls: [...ROOM, makeWall([4, 0], [7, 0], 'p1'), makeWall([7, 0], [7, 3], 'p2')],
142
+ gridStep: 0.5,
143
+ })
144
+ expect(result.snap).toBe('endpoint')
145
+ expect(result.label).toBe('Endpoint')
146
+ expect(result.point).toEqual([4, 3])
147
+ expect(result.targetWallIds.sort()).toEqual(['east', 'north'])
148
+ })
149
+
150
+ test('a held direction running into a wall lands on its line, square', () => {
151
+ // From the east of the room, back west at the north wall's height: the
152
+ // wall runs into the east wall at (4, 1.5), square to it.
153
+ const result = snapWallPullPoint({
154
+ point: [4.2, 1.52],
155
+ start: [7, 1.5],
156
+ runStart: [7, -1],
157
+ reference: [0, 1],
158
+ walls: ROOM,
159
+ gridStep: 0.5,
160
+ })
161
+ expect(result.snap).toBe('perpendicular')
162
+ expect(result.label).toBe('Perpendicular')
163
+ expectPoint(result.point, [4, 1.5])
164
+ expect(result.targetWallIds).toEqual(['east'])
165
+ })
166
+
167
+ test('a run lines up with its start, then closes on it', () => {
168
+ // Pulled east out of a lone wall's end S = (0, 0): east to (5, 0), north
169
+ // to (5, 4), then west — the run's start guide catches x = 0 …
170
+ const source = makeWall([-4, 0], [0, 0], 'source')
171
+ const walls = [source, makeWall([0, 0], [5, 0], 'r1'), makeWall([5, 0], [5, 4], 'r2')]
172
+ const across = snapWallPullPoint({
173
+ point: [0.1, 4.05],
174
+ start: [5, 4],
175
+ runStart: [0, 0],
176
+ reference: [0, 1],
177
+ walls,
178
+ gridStep: 0,
179
+ })
180
+ expect(across.snap).toBe('alignment')
181
+ expect(across.label).toBe('In line with start')
182
+ expectPoint(across.point, [0, 4])
183
+ const startGuide = across.guides.find((guide) => guide.kind === 'run-start')
184
+ expect(startGuide?.from).toEqual([0, 0])
185
+ expect(startGuide?.to[0]).toBeCloseTo(0, 9)
186
+ expect(startGuide?.to[1] ?? 0).toBeGreaterThan(4)
187
+ // … and the last wall closes square back onto it.
188
+ const close = snapWallPullPoint({
189
+ point: [0.12, 0.2],
190
+ start: [0, 4],
191
+ runStart: [0, 0],
192
+ reference: [-1, 0],
193
+ walls: [...walls, makeWall([5, 4], [0, 4], 'r3')],
194
+ gridStep: 0.5,
195
+ })
196
+ expect(close.snap).toBe('endpoint')
197
+ expect(close.point).toEqual([0, 0])
198
+ })
199
+
200
+ test('guides cross near the cursor: the crossing is marked and snaps', () => {
201
+ // The run's start S = (0, 0) and another wall's end P = (2, 6): S's
202
+ // vertical and P's horizontal cross at (0, 6).
203
+ const walls = [
204
+ makeWall([-4, 0], [0, 0], 'source'),
205
+ makeWall([0, 0], [5, 0], 'r1'),
206
+ makeWall([5, 0], [5, 4], 'r2'),
207
+ makeWall([2, 6], [2, 9], 'p'),
208
+ ]
209
+ const near = snapWallPullPoint({
210
+ point: [0.45, 5.6],
211
+ start: [5, 4],
212
+ runStart: [0, 0],
213
+ reference: [0, 1],
214
+ walls,
215
+ gridStep: 0.5,
216
+ })
217
+ expect(near.intersections.some((point) => point[0] === 0 && point[1] === 6)).toBe(true)
218
+ expect(near.snap).not.toBe('intersection')
219
+
220
+ const on = snapWallPullPoint({
221
+ point: [0.1, 5.9],
222
+ start: [5, 4],
223
+ runStart: [0, 0],
224
+ reference: [0, 1],
225
+ walls,
226
+ gridStep: 0.5,
227
+ })
228
+ expect(on.snap).toBe('intersection')
229
+ expect(on.label).toBe('Intersection')
230
+ expect(on.point).toEqual([0, 6])
231
+ expect(on.guides.map((guide) => guide.kind).sort()).toEqual([
232
+ 'alignment',
233
+ 'extension',
234
+ 'run-start',
235
+ ])
236
+ const vertical = on.guides.find((guide) => guide.kind === 'run-start')
237
+ expect(vertical?.from).toEqual([0, 0])
238
+ const horizontal = on.guides.find((guide) => guide.kind === 'alignment')
239
+ expect(horizontal?.from).toEqual([2, 6])
240
+ })
241
+
242
+ test('a wall middle and a wall crossing snap too', () => {
243
+ const midpoint = pullFromRoom([2.1, 3.15], { start: [4, -2], reference: [0, -1] })
244
+ expect(midpoint.snap).toBe('midpoint')
245
+ expect(midpoint.point).toEqual([2, 3])
246
+ })
247
+
248
+ test("on a turned building the lock follows the house's axes, not the world's", () => {
249
+ // The house is turned 30° on its lot. Plan points are in its own frame,
250
+ // as both views hand them to the tools; a world cursor along the house's
251
+ // east side comes in along the house's X, and the held wall goes back out
252
+ // along it.
253
+ const yaw = deg(30)
254
+ const origin: WallPlanPoint = [12, -7]
255
+ const toWorld = ([x, z]: WallPlanPoint): WallPlanPoint => [
256
+ origin[0] + x * Math.cos(yaw) + z * Math.sin(yaw),
257
+ origin[1] - x * Math.sin(yaw) + z * Math.cos(yaw),
258
+ ]
259
+ const toLocal = ([x, z]: WallPlanPoint): WallPlanPoint => {
260
+ const dx = x - origin[0]
261
+ const dz = z - origin[1]
262
+ return [dx * Math.cos(yaw) - dz * Math.sin(yaw), dx * Math.sin(yaw) + dz * Math.cos(yaw)]
263
+ }
264
+ const worldCursor = toWorld([6.6, 0.25])
265
+ const result = pullFromRoom(toLocal(worldCursor))
266
+ expect(result.lock).toBe('straight')
267
+ expectPoint(result.point, [6.5, 0])
268
+ const worldStart = toWorld([4, 0])
269
+ const worldEnd = toWorld(result.point)
270
+ const bearing = Math.atan2(worldEnd[1] - worldStart[1], worldEnd[0] - worldStart[0])
271
+ // Along the house's X, which sits 30° off the world's.
272
+ expect(bearing).toBeCloseTo(-yaw, 9)
273
+ })
274
+ })