@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,5 +1,6 @@
1
1
  'use client'
2
2
 
3
+ import { type MessageDescriptor, type PascalI18n, usePascalI18n } from '@pascal-app/core'
3
4
  import { type GroundMode, useViewer } from '@pascal-app/viewer'
4
5
  import { ArrowLeftRight, Check, Columns2, MoreHorizontal, Pin, PinOff, X } from 'lucide-react'
5
6
  import { type ReactNode, useEffect, useRef } from 'react'
@@ -7,7 +8,7 @@ import { useIsMobile } from '../../../hooks/use-mobile'
7
8
  import { type EditorViewDescriptor, isSceneView, isViewPinned } from '../../../lib/editor-views'
8
9
  import { cn } from '../../../lib/utils'
9
10
  import useEditor from '../../../store/use-editor'
10
- import type { ViewPaneIndex } from '../../../store/view-layout'
11
+ import { VIEW_2D, VIEW_3D, type ViewPaneIndex } from '../../../store/view-layout'
11
12
  import { IconRefGlyph } from '../../ui/icon-ref'
12
13
  import {
13
14
  DropdownMenu,
@@ -30,7 +31,16 @@ const IDLE =
30
31
  'text-muted-foreground/70 hover:bg-black/5 dark:hover:bg-white/8 hover:text-muted-foreground'
31
32
 
32
33
  /** The views of one pane: its pinned views, the one it shows, and a menu with the rest. */
34
+ /** Built-in views are named by the editor; host and plugin views bring their own label. */
35
+ function viewLabel(view: EditorViewDescriptor, i18n: PascalI18n): string {
36
+ if (view.id === VIEW_3D) return i18n._({ id: 'editor.views.view3d', message: '3D' })
37
+ if (view.id === VIEW_2D) return i18n._({ id: 'editor.views.view2d', message: '2D' })
38
+ if (view.id === PLUGINS_VIEW_ID) return i18n._({ id: 'editor.views.plugins', message: 'Plugins' })
39
+ return view.label
40
+ }
41
+
33
42
  export function ViewTabs({ pane, className }: { pane: ViewPaneIndex; className?: string }) {
43
+ const i18n = usePascalI18n()
34
44
  const layout = useActiveViewLayout()
35
45
  const views = useEditorViews()
36
46
  const pinnedViews = useEditor((s) => s.pinnedViews)
@@ -58,7 +68,11 @@ export function ViewTabs({ pane, className }: { pane: ViewPaneIndex; className?:
58
68
  return (
59
69
  <div className={cn(GROUP, 'min-w-0 shrink', className)}>
60
70
  <div
61
- aria-label={`Pane ${pane + 1} views`}
71
+ aria-label={i18n._({
72
+ id: 'editor.views.viewBar.paneViews',
73
+ message: 'Pane {pane} views',
74
+ values: { pane: pane + 1 },
75
+ })}
62
76
  className="flex min-w-0 overflow-x-auto [scrollbar-width:none]"
63
77
  role="tablist"
64
78
  ref={tabList}
@@ -102,16 +116,20 @@ export function ViewTabs({ pane, className }: { pane: ViewPaneIndex; className?:
102
116
  }
103
117
  }}
104
118
  tabIndex={view.id === current ? 0 : -1}
105
- title={view.label}
119
+ title={viewLabel(view, i18n)}
106
120
  role="tab"
107
121
  type="button"
108
122
  >
109
123
  <IconRefGlyph icon={view.icon} size={14} />
110
- <span className="max-w-36 truncate">{view.label}</span>
124
+ <span className="max-w-36 truncate">{viewLabel(view, i18n)}</span>
111
125
  </button>
112
126
  {view.onClose && (
113
127
  <button
114
- aria-label={`Close ${view.label}`}
128
+ aria-label={i18n._({
129
+ id: 'editor.views.viewBar.closeLabel',
130
+ message: 'Close {label}',
131
+ values: { label: viewLabel(view, i18n) },
132
+ })}
115
133
  className="mr-1 flex size-6 shrink-0 items-center justify-center rounded-full text-muted-foreground hover:bg-accent hover:text-foreground focus-visible:ring-2 focus-visible:ring-ring"
116
134
  onClick={view.onClose}
117
135
  type="button"
@@ -128,9 +146,27 @@ export function ViewTabs({ pane, className }: { pane: ViewPaneIndex; className?:
128
146
  }
129
147
 
130
148
  const GROUND_MODES = [
131
- { mode: 'auto', label: 'Auto', title: 'Veil the ground over a level below grade' },
132
- { mode: 'solid', label: 'Solid', title: 'Keep the ground solid' },
133
- ] as const satisfies ReadonlyArray<{ mode: GroundMode; label: string; title: string }>
149
+ {
150
+ mode: 'auto',
151
+ label: /* i18n */ { id: 'editor.views.viewBar.auto', message: 'Auto' },
152
+ title: /* i18n */ {
153
+ id: 'editor.views.viewBar.veilTheGroundOverALevel',
154
+ message: 'Veil the ground over a level below grade',
155
+ },
156
+ },
157
+ {
158
+ mode: 'solid',
159
+ label: /* i18n */ { id: 'editor.views.viewBar.solid', message: 'Solid' },
160
+ title: /* i18n */ {
161
+ id: 'editor.views.viewBar.keepTheGroundSolid',
162
+ message: 'Keep the ground solid',
163
+ },
164
+ },
165
+ ] as const satisfies ReadonlyArray<{
166
+ mode: GroundMode
167
+ label: MessageDescriptor
168
+ title: MessageDescriptor
169
+ }>
134
170
 
135
171
  /**
136
172
  * Over a level below grade the ground is veiled so its rooms show, and the bar says so: the
@@ -139,6 +175,7 @@ const GROUND_MODES = [
139
175
  * a level at grade or only partly below it.
140
176
  */
141
177
  export function GroundModeToggle() {
178
+ const i18n = usePascalI18n()
142
179
  const belowGrade = useViewer((s) => s.belowGrade)
143
180
  const gradeStanding = useViewer((s) => s.gradeStanding)
144
181
  const groundWindowVeiled = useViewer((s) => s.groundWindowVeiled)
@@ -147,15 +184,31 @@ export function GroundModeToggle() {
147
184
  const veiled =
148
185
  groundMode === 'auto' &&
149
186
  ((belowGrade && gradeStanding !== 'partly-below-grade') || groundWindowVeiled)
150
- const ground = veiled ? 'veiled' : 'solid'
151
- const label =
187
+ const standing =
152
188
  gradeStanding === 'partly-below-grade'
153
- ? `Partly below grade · ground ${ground}`
189
+ ? 'partly'
154
190
  : gradeStanding === 'below-grade'
155
- ? `Below grade · ground ${ground}`
156
- : `Ground ${ground}`
191
+ ? 'below'
192
+ : 'other'
193
+ const label = veiled
194
+ ? i18n._({
195
+ id: 'editor.views.viewBar.groundVeiled',
196
+ message:
197
+ '{standing, select, partly {Partly below grade · ground veiled} below {Below grade · ground veiled} other {Ground veiled}}',
198
+ values: { standing },
199
+ })
200
+ : i18n._({
201
+ id: 'editor.views.viewBar.groundSolid',
202
+ message:
203
+ '{standing, select, partly {Partly below grade · ground solid} below {Below grade · ground solid} other {Ground solid}}',
204
+ values: { standing },
205
+ })
157
206
  return (
158
- <div aria-label="Ground" className={GROUP} role="group">
207
+ <div
208
+ aria-label={i18n._({ id: 'editor.views.viewBar.ground', message: 'Ground' })}
209
+ className={GROUP}
210
+ role="group"
211
+ >
159
212
  <span className="flex items-center px-2.5 text-muted-foreground text-xs">{label}</span>
160
213
  {GROUND_MODES.map(({ mode, label, title }) => (
161
214
  <button
@@ -163,10 +216,10 @@ export function GroundModeToggle() {
163
216
  className={cn(SEGMENT, groundMode === mode ? ACTIVE : IDLE)}
164
217
  key={mode}
165
218
  onClick={() => useViewer.getState().setGroundMode(mode)}
166
- title={title}
219
+ title={i18n._(title)}
167
220
  type="button"
168
221
  >
169
- {label}
222
+ {i18n._(label)}
170
223
  </button>
171
224
  ))}
172
225
  </div>
@@ -175,15 +228,16 @@ export function GroundModeToggle() {
175
228
 
176
229
  /** Split toggle and, split, the swap — at the right end of the last pane's bar. */
177
230
  function SplitControls() {
231
+ const i18n = usePascalI18n()
178
232
  const split = useActiveViewLayout().split
179
233
  return (
180
234
  <div className={GROUP}>
181
235
  {split && (
182
236
  <button
183
- aria-label="Swap panes"
237
+ aria-label={i18n._({ id: 'editor.views.viewBar.swapPanes', message: 'Swap panes' })}
184
238
  className={cn(SEGMENT, IDLE, 'px-2')}
185
239
  onClick={() => useEditor.getState().swapPanes()}
186
- title="Swap panes"
240
+ title={i18n._({ id: 'editor.views.viewBar.swapPanes', message: 'Swap panes' })}
187
241
  type="button"
188
242
  >
189
243
  <ArrowLeftRight className="h-3 w-3" />
@@ -193,11 +247,15 @@ function SplitControls() {
193
247
  aria-pressed={split}
194
248
  className={cn(SEGMENT, split ? ACTIVE : IDLE)}
195
249
  onClick={() => useEditor.getState().toggleSplit()}
196
- title={split ? 'Close split (\\)' : 'Split (\\)'}
250
+ title={
251
+ split
252
+ ? i18n._({ id: 'editor.views.viewBar.closeSplit', message: 'Close split (\\)' })
253
+ : i18n._({ id: 'editor.views.viewBar.split', message: 'Split (\\)' })
254
+ }
197
255
  type="button"
198
256
  >
199
257
  <Columns2 className="h-3 w-3" />
200
- <span>Split</span>
258
+ <span>{i18n._({ id: 'editor.views.viewBar.split2', message: 'Split' })}</span>
201
259
  </button>
202
260
  </div>
203
261
  )
@@ -272,6 +330,7 @@ function MoreViewsMenu({
272
330
  current: string
273
331
  views: EditorViewDescriptor[]
274
332
  }) {
333
+ const i18n = usePascalI18n()
275
334
  const pinnedViews = useEditor((s) => s.pinnedViews)
276
335
  const setPaneView = useEditor((s) => s.setPaneView)
277
336
  const setViewPinned = useEditor((s) => s.setViewPinned)
@@ -282,7 +341,7 @@ function MoreViewsMenu({
282
341
  <DropdownMenu>
283
342
  <DropdownMenuTrigger asChild>
284
343
  <button
285
- aria-label="More views"
344
+ aria-label={i18n._({ id: 'editor.views.viewBar.moreViews', message: 'More views' })}
286
345
  className={cn(SEGMENT, IDLE, 'shrink-0 px-2')}
287
346
  type="button"
288
347
  >
@@ -290,7 +349,9 @@ function MoreViewsMenu({
290
349
  </button>
291
350
  </DropdownMenuTrigger>
292
351
  <DropdownMenuContent align="start" className="w-64">
293
- <DropdownMenuLabel className="text-muted-foreground text-xs">Views</DropdownMenuLabel>
352
+ <DropdownMenuLabel className="text-muted-foreground text-xs">
353
+ {i18n._({ id: 'editor.views.viewBar.views', message: 'Views' })}
354
+ </DropdownMenuLabel>
294
355
  {listed.map((view) => {
295
356
  const pinned = isViewPinned(view, pinnedViews)
296
357
  return (
@@ -300,11 +361,23 @@ function MoreViewsMenu({
300
361
  onSelect={() => setPaneView(pane, view.id)}
301
362
  >
302
363
  <IconRefGlyph icon={view.icon} size={16} />
303
- <span className="min-w-0 flex-1 truncate">{view.label}</span>
364
+ <span className="min-w-0 flex-1 truncate">{viewLabel(view, i18n)}</span>
304
365
  {view.id === current && <Check className="h-3.5 w-3.5 text-muted-foreground" />}
305
366
  {!isSceneView(view.id) && !view.onClose && !view.alwaysPinned && (
306
367
  <button
307
- aria-label={pinned ? `Unpin ${view.label}` : `Pin ${view.label}`}
368
+ aria-label={
369
+ pinned
370
+ ? i18n._({
371
+ id: 'editor.views.viewBar.unpinLabel',
372
+ message: 'Unpin {label}',
373
+ values: { label: viewLabel(view, i18n) },
374
+ })
375
+ : i18n._({
376
+ id: 'editor.views.viewBar.pinLabel',
377
+ message: 'Pin {label}',
378
+ values: { label: viewLabel(view, i18n) },
379
+ })
380
+ }
308
381
  aria-pressed={pinned}
309
382
  className={cn(
310
383
  'flex h-6 w-6 items-center justify-center rounded-md transition-colors hover:bg-black/6 dark:hover:bg-white/10',
@@ -316,7 +389,17 @@ function MoreViewsMenu({
316
389
  setViewPinned(view.id, !pinned)
317
390
  }}
318
391
  onPointerDown={(event) => event.stopPropagation()}
319
- title={pinned ? 'Unpin from the view bar' : 'Pin to the view bar'}
392
+ title={
393
+ pinned
394
+ ? i18n._({
395
+ id: 'editor.views.viewBar.unpinFromTheViewBar',
396
+ message: 'Unpin from the view bar',
397
+ })
398
+ : i18n._({
399
+ id: 'editor.views.viewBar.pinToTheViewBar',
400
+ message: 'Pin to the view bar',
401
+ })
402
+ }
320
403
  type="button"
321
404
  >
322
405
  {pinned ? <PinOff className="h-3.5 w-3.5" /> : <Pin className="h-3.5 w-3.5" />}
@@ -333,7 +416,9 @@ function MoreViewsMenu({
333
416
  onSelect={() => setPaneView(pane, PLUGINS_VIEW_ID)}
334
417
  >
335
418
  <img alt="" className="h-4 w-4 object-contain" src="/icons/plugins.webp" />
336
- <span>Browse plugins</span>
419
+ <span>
420
+ {i18n._({ id: 'editor.views.viewBar.browsePlugins', message: 'Browse plugins' })}
421
+ </span>
337
422
  </DropdownMenuItem>
338
423
  </>
339
424
  )}
@@ -20,11 +20,11 @@ import {
20
20
  type WallNode,
21
21
  } from '@pascal-app/core'
22
22
  import { getSceneTheme, useViewer } from '@pascal-app/viewer'
23
- import { Html } from '@react-three/drei'
24
23
  import { createPortal, useFrame } from '@react-three/fiber'
25
24
  import { useMemo, useState } from 'react'
26
25
  import * as THREE from 'three'
27
26
  import { formatLinearMeasurement } from '../../lib/measurements'
27
+ import { Html } from './localized-html'
28
28
 
29
29
  const GUIDE_Y_OFFSET = 0.08
30
30
  const LABEL_LIFT = 0.08
@@ -19,7 +19,6 @@ import {
19
19
  type WallNode,
20
20
  } from '@pascal-app/core'
21
21
  import { useViewer } from '@pascal-app/viewer'
22
- import { Html } from '@react-three/drei'
23
22
  import { createPortal, type ThreeEvent, useFrame, useThree } from '@react-three/fiber'
24
23
  import { useEffect, useMemo, useRef, useState } from 'react'
25
24
  import {
@@ -61,6 +60,7 @@ import useInteractionScope, {
61
60
  import { suppressBoxSelectForPointer } from '../tools/select/box-select-state'
62
61
  import { resolveResizeSnapValue } from './handles/resize-snap'
63
62
  import { type HandleDragControls, useHandleDrag } from './handles/use-handle-drag'
63
+ import { Html } from './localized-html'
64
64
  import { MeasurementPill } from './measurement-pill'
65
65
  import {
66
66
  createArrowHitAreaGeometry,
@@ -70,6 +70,7 @@ import {
70
70
  NO_RAYCAST,
71
71
  useInvisibleHitAreaMaterial,
72
72
  } from './node-arrow-handles'
73
+ import { WallPullGrip } from './wall-pull-grip'
73
74
  import { WallPushArrow } from './wall-push-arrow'
74
75
 
75
76
  const HANDLE_OFFSET = 0.27
@@ -251,6 +252,9 @@ function WallMoveSideHandlesForWall({ wall }: { wall: WallNode }) {
251
252
  />
252
253
  <WallCornerLeaderHandle endpoint="start" wall={effectiveWall} />
253
254
  <WallCornerLeaderHandle endpoint="end" wall={effectiveWall} />
255
+ {/* Past each end, the grip that pulls the next wall out of it. */}
256
+ <WallPullGrip endpoint="start" wall={effectiveWall} />
257
+ <WallPullGrip endpoint="end" wall={effectiveWall} />
254
258
  </group>
255
259
  <WallBaseElevationHandle
256
260
  baseElevation={baseElevation}
@@ -0,0 +1,193 @@
1
+ 'use client'
2
+
3
+ import { nodeRegistry, type WallNode } from '@pascal-app/core'
4
+ import { type ThreeEvent, useThree } from '@react-three/fiber'
5
+ import { useEffect, useMemo, useState } from 'react'
6
+ import { Color, DoubleSide, ExtrudeGeometry, OrthographicCamera, Shape } from 'three'
7
+ import { MeshBasicNodeMaterial } from 'three/webgpu'
8
+ import { EDITOR_LAYER } from '../../lib/constants'
9
+ import { sfxEmitter } from '../../lib/sfx-bus'
10
+ import { startWallPull, WALL_PULL_GRIP_TITLE, wallPullSeed } from '../../lib/wall-pull'
11
+ import { suppressBoxSelectForPointer } from '../tools/select/box-select-state'
12
+ import {
13
+ createEndpointHitAreaGeometry,
14
+ InvisibleHandleHitArea,
15
+ NO_RAYCAST,
16
+ useInvisibleHitAreaMaterial,
17
+ } from './handles/handle-arrow'
18
+
19
+ const PULL_FILL = '#10b981' // emerald-500 — the 2D grip's colour
20
+ const PULL_HOVER_FILL = '#34d399' // emerald-400
21
+ // Out from the wall end (m, before zoom): clear of the corner picker's hex.
22
+ const PULL_ARROW_OFFSET = 0.2
23
+ const PULL_ARROW_LENGTH = 0.34
24
+ const PULL_ARROW_HALF_WIDTH = 0.11
25
+ const PULL_ARROW_SHAFT_HALF_WIDTH = 0.04
26
+ const PULL_HIT_RADIUS = 0.17
27
+ const PULL_FLOOR_LIFT = 0.02
28
+
29
+ // The grip's tooltip: one plain DOM element beside the pointer, so it never
30
+ // lives in (or unmounts with) the R3F tree.
31
+ let tooltip: HTMLDivElement | null = null
32
+
33
+ function showGripTooltip(clientX: number, clientY: number) {
34
+ if (typeof document === 'undefined') return
35
+ if (!tooltip) {
36
+ tooltip = document.createElement('div')
37
+ tooltip.textContent = WALL_PULL_GRIP_TITLE
38
+ tooltip.setAttribute('role', 'tooltip')
39
+ Object.assign(tooltip.style, {
40
+ position: 'fixed',
41
+ zIndex: '60',
42
+ pointerEvents: 'none',
43
+ whiteSpace: 'nowrap',
44
+ borderRadius: '6px',
45
+ background: 'rgba(24, 24, 27, 0.92)',
46
+ color: '#fff',
47
+ font: '500 11px/1.4 system-ui, -apple-system, sans-serif',
48
+ padding: '4px 8px',
49
+ boxShadow: '0 1px 3px rgba(0, 0, 0, 0.25)',
50
+ })
51
+ }
52
+ tooltip.style.left = `${clientX + 14}px`
53
+ tooltip.style.top = `${clientY + 18}px`
54
+ if (!tooltip.isConnected) document.body.appendChild(tooltip)
55
+ }
56
+
57
+ function hideGripTooltip() {
58
+ tooltip?.remove()
59
+ }
60
+
61
+ /** A flat outward arrow (tip along +X), lying in the XZ plane from x = 0. */
62
+ function createPullArrowGeometry() {
63
+ const head = PULL_ARROW_LENGTH * 0.45
64
+ const shape = new Shape()
65
+ shape.moveTo(0, -PULL_ARROW_SHAFT_HALF_WIDTH)
66
+ shape.lineTo(PULL_ARROW_LENGTH - head, -PULL_ARROW_SHAFT_HALF_WIDTH)
67
+ shape.lineTo(PULL_ARROW_LENGTH - head, -PULL_ARROW_HALF_WIDTH)
68
+ shape.lineTo(PULL_ARROW_LENGTH, 0)
69
+ shape.lineTo(PULL_ARROW_LENGTH - head, PULL_ARROW_HALF_WIDTH)
70
+ shape.lineTo(PULL_ARROW_LENGTH - head, PULL_ARROW_SHAFT_HALF_WIDTH)
71
+ shape.lineTo(0, PULL_ARROW_SHAFT_HALF_WIDTH)
72
+ shape.lineTo(0, -PULL_ARROW_SHAFT_HALF_WIDTH)
73
+ const geometry = new ExtrudeGeometry(shape, {
74
+ depth: 0.03,
75
+ bevelEnabled: true,
76
+ bevelThickness: 0.01,
77
+ bevelSize: 0.012,
78
+ bevelSegments: 4,
79
+ })
80
+ geometry.translate(0, 0, -0.015)
81
+ geometry.rotateX(-Math.PI / 2)
82
+ geometry.computeVertexNormals()
83
+ geometry.computeBoundingSphere()
84
+ return geometry
85
+ }
86
+
87
+ /**
88
+ * A selected wall end's "pull to continue" grip in 3D — the twin of the floor
89
+ * plan's: an emerald arrow lying on the floor just past the end, pointing out
90
+ * along the wall (along its tangent when curved), titled "Drag to draw the next
91
+ * wall" while the pointer rests on it. Pressing it starts
92
+ * `startWallPull`: the wall tool's Walls run opens at that end, the drag shows
93
+ * the next wall, and the release (the canvas click) places it. Screen-sized
94
+ * like the corner picker beside it; rendered in the wall's level frame at its
95
+ * base, by `WallMoveSideHandles`.
96
+ */
97
+ export function WallPullGrip({ wall, endpoint }: { wall: WallNode; endpoint: 'start' | 'end' }) {
98
+ const [hover, setHover] = useState(false)
99
+ const { camera } = useThree()
100
+ const zoom = camera instanceof OrthographicCamera ? 1 / camera.zoom : 1
101
+ const seed = wallPullSeed(wall, endpoint)
102
+ const geometry = useMemo(() => createPullArrowGeometry(), [])
103
+ const hitGeometry = useMemo(() => createEndpointHitAreaGeometry(PULL_HIT_RADIUS), [])
104
+ const hitMaterial = useInvisibleHitAreaMaterial()
105
+ const material = useMemo(
106
+ () =>
107
+ new MeshBasicNodeMaterial({
108
+ color: new Color(PULL_FILL),
109
+ side: DoubleSide,
110
+ transparent: true,
111
+ opacity: 0.95,
112
+ depthTest: false,
113
+ depthWrite: false,
114
+ toneMapped: false,
115
+ }),
116
+ [],
117
+ )
118
+ useEffect(() => {
119
+ material.color.set(hover ? PULL_HOVER_FILL : PULL_FILL)
120
+ }, [hover, material])
121
+ useEffect(() => () => geometry.dispose(), [geometry])
122
+ useEffect(() => () => hitGeometry.dispose(), [hitGeometry])
123
+ useEffect(() => () => material.dispose(), [material])
124
+ // Warm the wall tool's module so the press starts drawing at once.
125
+ useEffect(() => {
126
+ void nodeRegistry
127
+ .get('wall')
128
+ ?.tool?.()
129
+ .catch(() => {})
130
+ }, [])
131
+ useEffect(
132
+ () => () => {
133
+ hideGripTooltip()
134
+ if (document.body.style.cursor === 'grab') document.body.style.cursor = ''
135
+ },
136
+ [],
137
+ )
138
+
139
+ if (!seed) return null
140
+ const [dx, dz] = seed.reference
141
+ const offset = PULL_ARROW_OFFSET * zoom
142
+ const position: [number, number, number] = [
143
+ seed.start[0] + dx * offset,
144
+ PULL_FLOOR_LIFT,
145
+ seed.start[1] + dz * offset,
146
+ ]
147
+ const rotationY = Math.atan2(-dz, dx)
148
+
149
+ const onPointerDown = (event: ThreeEvent<PointerEvent>) => {
150
+ if (event.button !== 0) return
151
+ event.stopPropagation()
152
+ suppressBoxSelectForPointer(event)
153
+ sfxEmitter.emit('sfx:item-pick')
154
+ setHover(false)
155
+ hideGripTooltip()
156
+ if (document.body.style.cursor === 'grab') document.body.style.cursor = ''
157
+ startWallPull({ wallId: wall.id, endpoint, origin: '3d' })
158
+ }
159
+
160
+ return (
161
+ <group position={position} rotation={[0, rotationY, 0]} scale={zoom}>
162
+ <group position={[PULL_ARROW_LENGTH / 2, 0, 0]} rotation={[-Math.PI / 2, 0, 0]}>
163
+ <InvisibleHandleHitArea
164
+ geometry={hitGeometry}
165
+ material={hitMaterial}
166
+ onPointerDown={onPointerDown}
167
+ onPointerEnter={(event) => {
168
+ event.stopPropagation()
169
+ setHover(true)
170
+ showGripTooltip(event.nativeEvent.clientX, event.nativeEvent.clientY)
171
+ document.body.style.cursor = 'grab'
172
+ }}
173
+ onPointerLeave={(event) => {
174
+ event.stopPropagation()
175
+ setHover(false)
176
+ hideGripTooltip()
177
+ if (document.body.style.cursor === 'grab') document.body.style.cursor = ''
178
+ }}
179
+ scale={1}
180
+ />
181
+ </group>
182
+ <mesh
183
+ frustumCulled={false}
184
+ geometry={geometry}
185
+ layers={EDITOR_LAYER}
186
+ material={material}
187
+ raycast={NO_RAYCAST}
188
+ renderOrder={1003}
189
+ scale={hover ? 1.15 : 1}
190
+ />
191
+ </group>
192
+ )
193
+ }
@@ -16,6 +16,7 @@ import {
16
16
  } from '@pascal-app/core'
17
17
  import { useWallSnapIndicator, type WallSnapKind } from '@pascal-app/editor'
18
18
  import { useViewer } from '@pascal-app/viewer'
19
+ import { Html } from '@react-three/drei'
19
20
  import { useFrame } from '@react-three/fiber'
20
21
  import { memo, useMemo, useRef } from 'react'
21
22
  import { BoxGeometry, CircleGeometry, CylinderGeometry, type Group } from 'three'
@@ -31,10 +32,14 @@ import { EDITOR_LAYER } from '../../lib/constants'
31
32
  *
32
33
  * endpoint (corner) → square midpoint → triangle
33
34
  * intersection → ✕ cross wall body (edge) → circle
35
+ * perpendicular → ⊥ corner projection guide → diamond
36
+ * grid length → small +
34
37
  *
35
38
  * Subscribes to the shared `useWallSnapIndicator` store (published by the wall
36
39
  * draft + endpoint-move tools). The vertical mouse pillar and the corner
37
- * (endpoint) square are green; the other floor glyphs are indigo.
40
+ * (endpoint) square are green; the other floor glyphs are indigo. A point that
41
+ * carries a `label` (a wall pulled from a wall end) also names what it caught
42
+ * in a pill beside the foot of the pillar.
38
43
  *
39
44
  * The point carries only XZ (building-local plan coords); like the alignment
40
45
  * guides it's lifted to the active level's building-local Y each frame so it
@@ -135,6 +140,23 @@ export const WallSnapBeaconLayer = memo(function WallSnapBeaconLayer() {
135
140
  renderOrder={1002}
136
141
  />
137
142
  <SnapMarker kind={point.kind} x={point.x} z={point.z} />
143
+ {point.label ? (
144
+ <Html
145
+ position={[point.x, 0.05, point.z]}
146
+ style={{ pointerEvents: 'none', userSelect: 'none' }}
147
+ zIndexRange={[24, 0]}
148
+ >
149
+ <div
150
+ className="whitespace-nowrap rounded-[3px] px-[5px] py-[2px] font-sans font-semibold text-[11px] text-white"
151
+ style={{
152
+ backgroundColor: point.kind === 'endpoint' ? '#16a34a' : '#6366f1',
153
+ transform: 'translate(12px, 8px)',
154
+ }}
155
+ >
156
+ {point.label}
157
+ </div>
158
+ </Html>
159
+ ) : null}
138
160
  </group>
139
161
  )
140
162
  })
@@ -295,6 +317,64 @@ function SnapMarker({ kind, x, z }: { kind: WallSnapKind; x: number; z: number }
295
317
  </>
296
318
  )
297
319
  }
320
+ if (kind === 'perpendicular') {
321
+ // ⊥ — a base bar with an upright from its middle.
322
+ return (
323
+ <>
324
+ <mesh
325
+ geometry={FLAT_BOX_GEOMETRY}
326
+ layers={EDITOR_LAYER}
327
+ material={beaconMaterial}
328
+ position={[x, y, z]}
329
+ renderOrder={1001}
330
+ scale={[MARKER * 2.6, 1, MARKER * 0.45]}
331
+ />
332
+ <mesh
333
+ geometry={FLAT_BOX_GEOMETRY}
334
+ layers={EDITOR_LAYER}
335
+ material={beaconMaterial}
336
+ position={[x, y, z - MARKER * 0.8]}
337
+ renderOrder={1001}
338
+ scale={[MARKER * 0.45, 1, MARKER * 1.6]}
339
+ />
340
+ </>
341
+ )
342
+ }
343
+ if (kind === 'alignment') {
344
+ return (
345
+ <mesh
346
+ geometry={FLAT_BOX_GEOMETRY}
347
+ layers={EDITOR_LAYER}
348
+ material={beaconMaterial}
349
+ position={[x, y, z]}
350
+ renderOrder={1001}
351
+ rotation={[0, Math.PI / 4, 0]}
352
+ scale={[MARKER * 1.4, 1, MARKER * 1.4]}
353
+ />
354
+ )
355
+ }
356
+ if (kind === 'grid') {
357
+ return (
358
+ <>
359
+ <mesh
360
+ geometry={FLAT_BOX_GEOMETRY}
361
+ layers={EDITOR_LAYER}
362
+ material={beaconMaterial}
363
+ position={[x, y, z]}
364
+ renderOrder={1001}
365
+ scale={[MARKER * 1.8, 1, MARKER * 0.35]}
366
+ />
367
+ <mesh
368
+ geometry={FLAT_BOX_GEOMETRY}
369
+ layers={EDITOR_LAYER}
370
+ material={beaconMaterial}
371
+ position={[x, y, z]}
372
+ renderOrder={1001}
373
+ scale={[MARKER * 0.35, 1, MARKER * 1.8]}
374
+ />
375
+ </>
376
+ )
377
+ }
298
378
  // 'wall' (edge / along-wall) → circle
299
379
  return (
300
380
  <mesh