@pascal-app/editor 1.0.7 → 1.1.1

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 (336) hide show
  1. package/package.json +9 -15
  2. package/src/components/editor/action-menu-button.tsx +1 -1
  3. package/src/components/editor/alignment-3d-guide-layer.tsx +1 -1
  4. package/src/components/editor/custom-camera-controls.tsx +101 -45
  5. package/src/components/editor/duplicate-node.ts +2 -1
  6. package/src/components/editor/editor-layout-v2.tsx +1 -1
  7. package/src/components/editor/elevation-3d-guide-layer.tsx +1 -1
  8. package/src/components/editor/first-person-controls.tsx +20 -1983
  9. package/src/components/editor/fit-scene-framing.test.ts +318 -0
  10. package/src/components/editor/fit-scene-framing.ts +142 -0
  11. package/src/components/editor/floating-action-menu.tsx +5 -3
  12. package/src/components/editor/floating-building-action-menu.tsx +1 -1
  13. package/src/components/editor/floor-openings-overlay.tsx +1 -1
  14. package/src/components/editor/floor-region-controls.tsx +8 -4
  15. package/src/components/editor/floorplan-navigation-presentation.test.ts +0 -40
  16. package/src/components/editor/floorplan-navigation-presentation.ts +0 -66
  17. package/src/components/editor/floorplan-panel.tsx +199 -30
  18. package/src/components/editor/floorplan-wall-typing-keys.test.tsx +152 -0
  19. package/src/components/editor/floorplan-wall-typing-keys.ts +79 -0
  20. package/src/components/editor/follow-pascal.test.tsx +1 -2
  21. package/src/components/editor/follow-pascal.tsx +1 -1
  22. package/src/components/editor/footprint-height-handle.tsx +2 -2
  23. package/src/components/editor/group-actions.ts +1 -2
  24. package/src/components/editor/group-floating-action-menu.tsx +1 -1
  25. package/src/components/editor/group-move-3d.ts +1 -2
  26. package/src/components/editor/group-rotate-handle.tsx +1 -2
  27. package/src/components/editor/group-transform-shared.ts +3 -1
  28. package/src/components/editor/handles/use-handle-drag.ts +1 -2
  29. package/src/components/editor/index.tsx +43 -13
  30. package/src/components/editor/inspector-extension-actions.tsx +1 -1
  31. package/src/components/editor/measurement-pill.tsx +1 -1
  32. package/src/components/editor/node-arrow-handles.tsx +2 -2
  33. package/src/components/editor/open-wall-ends-3d-layer.tsx +2 -3
  34. package/src/components/editor/open-wall-ends-hint.tsx +1 -1
  35. package/src/components/editor/opening-guides-3d-layer.tsx +1 -1
  36. package/src/components/editor/quick-measurement-card.tsx +1 -1
  37. package/src/components/editor/room-controls.tsx +10 -6
  38. package/src/components/editor/room-handles-2d.tsx +3 -3
  39. package/src/components/editor/room-handles.tsx +4 -5
  40. package/src/components/editor/room-highlight.tsx +6 -105
  41. package/src/components/editor/room-labels.test.ts +8 -822
  42. package/src/components/editor/room-labels.tsx +41 -1670
  43. package/src/components/editor/room-webgpu-materials.test.ts +1 -1
  44. package/src/components/editor/selection-manager.tsx +5 -3
  45. package/src/components/editor/site-edge-labels.tsx +7 -3
  46. package/src/components/editor/snapshot-capture-overlay.tsx +2 -5
  47. package/src/components/editor/use-floorplan-background-placement.ts +2 -1
  48. package/src/components/editor/views/plugins-view.tsx +1 -1
  49. package/src/components/editor/views/view-bar.tsx +9 -9
  50. package/src/components/editor/wall-measurement-label.tsx +31 -15
  51. package/src/components/editor/wall-move-side-handles.tsx +11 -7
  52. package/src/components/editor/wall-pull-grip.tsx +2 -2
  53. package/src/components/editor/wall-region-handles.tsx +2 -2
  54. package/src/components/editor/wall-snap-beacon-layer.tsx +53 -32
  55. package/src/components/editor-2d/floorplan-group-move.tsx +3 -4
  56. package/src/components/editor-2d/floorplan-measurement-tool-layer.tsx +13 -13
  57. package/src/components/editor-2d/floorplan-open-wall-ends-layer.tsx +2 -3
  58. package/src/components/editor-2d/floorplan-registry-action-menu.tsx +2 -1
  59. package/src/components/editor-2d/floorplan-registry-move-overlay.tsx +23 -6
  60. package/src/components/editor-2d/renderers/floorplan-annotation-layout.test.ts +0 -37
  61. package/src/components/editor-2d/renderers/floorplan-annotation-layout.ts +0 -30
  62. package/src/components/editor-2d/renderers/floorplan-dimension-edit-overlay.tsx +13 -3
  63. package/src/components/editor-2d/renderers/floorplan-draft-measurement.tsx +7 -1
  64. package/src/components/editor-2d/renderers/floorplan-placement-preview-layer.tsx +14 -3
  65. package/src/components/editor-2d/renderers/floorplan-placement-typing-hud.test.tsx +35 -0
  66. package/src/components/editor-2d/renderers/floorplan-placement-typing-hud.tsx +65 -0
  67. package/src/components/editor-2d/renderers/floorplan-registry-layer.test.ts +30 -10
  68. package/src/components/editor-2d/renderers/floorplan-registry-layer.tsx +39 -35
  69. package/src/components/systems/roof/roof-edit-system.test.tsx +15 -1
  70. package/src/components/systems/roof/roof-edit-system.tsx +33 -21
  71. package/src/components/tools/distribution/distribution-run-tool.test.tsx +371 -3
  72. package/src/components/tools/distribution/distribution-run-tool.tsx +151 -33
  73. package/src/components/tools/item/move-tool.tsx +1 -6
  74. package/src/components/tools/item/placement-history.test.tsx +1 -1
  75. package/src/components/tools/item/use-placement-coordinator.tsx +3 -4
  76. package/src/components/tools/paint-region/wall-paint-region-tool.tsx +7 -3
  77. package/src/components/tools/registry/move-registry-node-tool.test.ts +42 -1
  78. package/src/components/tools/registry/move-registry-node-tool.tsx +39 -16
  79. package/src/components/tools/shared/cursor-sphere.tsx +1 -1
  80. package/src/components/tools/shared/draft-measurement-label.tsx +1 -1
  81. package/src/components/tools/shared/placement-box.tsx +2 -6
  82. package/src/components/tools/shared/placement-coordinate-input.tsx +94 -0
  83. package/src/components/tools/shared/placement-dimension-guides.tsx +1 -1
  84. package/src/components/tools/shared/segment-angle.ts +0 -4
  85. package/src/components/tools/site/terrain-brush-cursor.tsx +1 -1
  86. package/src/components/tools/tool-manager.tsx +1 -24
  87. package/src/components/tools/wall/wall-typed-entry.test.ts +99 -0
  88. package/src/components/tools/wall/wall-typed-entry.ts +70 -0
  89. package/src/components/ui/action-menu/camera-actions.tsx +2 -2
  90. package/src/components/ui/action-menu/control-modes.tsx +1 -1
  91. package/src/components/ui/action-menu/follow-pascal-pill.tsx +1 -1
  92. package/src/components/ui/action-menu/index.tsx +3 -3
  93. package/src/components/ui/action-menu/measurement-control.tsx +3 -3
  94. package/src/components/ui/action-menu/placement.ts +0 -18
  95. package/src/components/ui/action-menu/point-ask-button.tsx +1 -1
  96. package/src/components/ui/action-menu/view-toggles.tsx +5 -5
  97. package/src/components/ui/controls/finish-swatch.tsx +1 -1
  98. package/src/components/ui/controls/material-paint-panel.tsx +2 -2
  99. package/src/components/ui/controls/scene-material-list.tsx +2 -2
  100. package/src/components/ui/controls/select-control.tsx +1 -1
  101. package/src/components/ui/controls/slider-control.tsx +18 -6
  102. package/src/components/ui/controls/terrain-sculpt-panel.tsx +1 -1
  103. package/src/components/ui/controls/tool-options-panel.tsx +74 -44
  104. package/src/components/ui/floating-level-selector.tsx +3 -3
  105. package/src/components/ui/helpers/contextual-helper-panel.tsx +1 -1
  106. package/src/components/ui/helpers/helper-manager.test.tsx +54 -1
  107. package/src/components/ui/helpers/helper-manager.tsx +55 -1
  108. package/src/components/ui/level-duplicate-dialog.tsx +1 -1
  109. package/src/components/ui/panels/collections/collections-popover.tsx +2 -2
  110. package/src/components/ui/panels/floor-foundation-panel.tsx +3 -3
  111. package/src/components/ui/panels/floor-opening-panel.tsx +5 -5
  112. package/src/components/ui/panels/mobile-selection-bar.tsx +28 -24
  113. package/src/components/ui/panels/multi-height-mode.tsx +3 -3
  114. package/src/components/ui/panels/multi-selection-panel.tsx +1 -1
  115. package/src/components/ui/panels/node-display.ts +1 -1
  116. package/src/components/ui/panels/panel-manager.tsx +5 -3
  117. package/src/components/ui/panels/panel-wrapper.tsx +42 -19
  118. package/src/components/ui/panels/parametric-inspector.tsx +1 -1
  119. package/src/components/ui/panels/room-construction-rows.tsx +7 -7
  120. package/src/components/ui/panels/room-name-options.ts +1 -5
  121. package/src/components/ui/panels/room-openings-field.tsx +1 -1
  122. package/src/components/ui/panels/room-panel.tsx +7 -9
  123. package/src/components/ui/primitives/color-dot.tsx +2 -2
  124. package/src/components/ui/primitives/opacity-control.tsx +2 -2
  125. package/src/components/ui/primitives/sidebar.tsx +3 -3
  126. package/src/components/ui/right-stack.test.ts +13 -6
  127. package/src/components/ui/right-stack.tsx +22 -11
  128. package/src/components/ui/sidebar/icon-rail.tsx +1 -1
  129. package/src/components/ui/sidebar/panels/items-panel/function-tree-panel.tsx +1 -1
  130. package/src/components/ui/sidebar/panels/settings-panel/audio-settings-dialog.tsx +2 -2
  131. package/src/components/ui/sidebar/panels/settings-panel/construction-animation-setting.tsx +1 -1
  132. package/src/components/ui/sidebar/panels/settings-panel/index.tsx +2 -2
  133. package/src/components/ui/sidebar/panels/settings-panel/keyboard-shortcuts-dialog.tsx +1 -1
  134. package/src/components/ui/sidebar/panels/settings-panel/load-build-dialog.tsx +2 -6
  135. package/src/components/ui/sidebar/panels/settings-panel/print-export-button.tsx +1 -1
  136. package/src/components/ui/sidebar/panels/site-panel/building-tree-node.tsx +1 -1
  137. package/src/components/ui/sidebar/panels/site-panel/ceiling-tree-node.tsx +1 -1
  138. package/src/components/ui/sidebar/panels/site-panel/index.tsx +3 -10
  139. package/src/components/ui/sidebar/panels/site-panel/inline-rename-input.tsx +1 -1
  140. package/src/components/ui/sidebar/panels/site-panel/level-tree-node.tsx +1 -1
  141. package/src/components/ui/sidebar/panels/site-panel/roof-tree-node.tsx +46 -7
  142. package/src/components/ui/sidebar/panels/site-panel/slab-tree-node.tsx +1 -1
  143. package/src/components/ui/sidebar/panels/site-panel/tree-node-actions.tsx +1 -1
  144. package/src/components/ui/sidebar/panels/site-panel/tree-node-drag.tsx +14 -5
  145. package/src/components/ui/sidebar/panels/site-panel/unit-tree-node.tsx +2 -2
  146. package/src/components/ui/sidebar/panels/site-panel/zone-tree-node.tsx +1 -1
  147. package/src/components/ui/sidebar/panels/zone-panel/index.tsx +3 -3
  148. package/src/components/ui/sidebar/tab-bar.tsx +1 -1
  149. package/src/components/ui/wall-type/wall-type-editor-dialog.tsx +2 -2
  150. package/src/components/ui/wall-type/wall-type-menu.tsx +1 -1
  151. package/src/components/viewer-overlay.tsx +1 -2
  152. package/src/hooks/use-auto-frame.ts +1 -2
  153. package/src/hooks/use-keyboard.ts +1 -2
  154. package/src/hooks/use-selected-room.ts +5 -7
  155. package/src/i18n/floor-footprint-name.ts +1 -1
  156. package/src/i18n/locales/en/messages.po +1036 -306
  157. package/src/i18n/locales/en/messages.ts +1 -1
  158. package/src/i18n/locales/en-XA/messages.ts +1 -1
  159. package/src/i18n/locales/es/messages.po +2267 -439
  160. package/src/i18n/locales/fr/messages.po +2267 -439
  161. package/src/i18n/locales/ja/messages.po +2267 -439
  162. package/src/i18n/locales/zh-CN/messages.po +2267 -439
  163. package/src/i18n/locales.test.ts +1 -1
  164. package/src/index.tsx +183 -178
  165. package/src/lib/below-grade.test.tsx +4 -4
  166. package/src/lib/camera-facing.test.ts +1 -1
  167. package/src/lib/camera-facing.ts +2 -3
  168. package/src/lib/contextual-help.ts +6 -0
  169. package/src/lib/editor-level-display.ts +1 -1
  170. package/src/lib/elevation-guides.ts +21 -6
  171. package/src/lib/elevator-support.ts +1 -72
  172. package/src/lib/floor-footprints.test.ts +3 -3
  173. package/src/lib/floor-footprints.ts +1 -9
  174. package/src/lib/floor-foundation-support.test.ts +8 -3
  175. package/src/lib/floor-opening-draft.ts +2 -2
  176. package/src/lib/floor-region-session.ts +1 -1
  177. package/src/lib/floorplan/annotation-visibility.test.ts +1 -1
  178. package/src/lib/floorplan/annotation-visibility.ts +1 -1
  179. package/src/lib/floorplan/floorplan-export.test.ts +1 -1
  180. package/src/lib/floorplan/floorplan-export.tsx +8 -11
  181. package/src/lib/floorplan/floorplan-extension.ts +4 -157
  182. package/src/lib/floorplan/floorplan-mode.ts +2 -1
  183. package/src/lib/floorplan/floorplan-pdfkit-renderer.test.ts +1 -1
  184. package/src/lib/floorplan/floorplan-pdfkit-renderer.ts +8 -6
  185. package/src/lib/floorplan/floorplan-plugin-install.test.tsx +3 -4
  186. package/src/lib/floorplan/geometry.ts +2 -112
  187. package/src/lib/floorplan/index.ts +0 -9
  188. package/src/lib/floorplan/items.ts +2 -1
  189. package/src/lib/floorplan/open-wall-ends.ts +2 -1
  190. package/src/lib/floorplan/schedules.ts +6 -3
  191. package/src/lib/floorplan/sheet-export.ts +7 -3
  192. package/src/lib/floorplan/site-plan/site-parts.ts +7 -1
  193. package/src/lib/floorplan/site-plan/site-plan-layer.tsx +1 -1
  194. package/src/lib/follow-pascal-presentation.ts +1 -1
  195. package/src/lib/gesture-lifecycle.ts +3 -1
  196. package/src/lib/glb-export.ts +1 -1
  197. package/src/lib/interaction/scope.ts +1 -0
  198. package/src/lib/layer-thickness.ts +1 -1
  199. package/src/lib/level-selection.test.ts +18 -2
  200. package/src/lib/level-selection.ts +31 -8
  201. package/src/lib/linear-display.ts +1 -1
  202. package/src/lib/live-gesture-round-trip.test.ts +1 -1
  203. package/src/lib/material-paint.ts +1 -1
  204. package/src/lib/measurement-label.test.ts +1 -1
  205. package/src/lib/measurements.test.ts +7 -5
  206. package/src/lib/mezzanine-selection.test.ts +1 -1
  207. package/src/lib/node-action-movement.test.tsx +115 -0
  208. package/src/lib/node-action-movement.ts +2 -0
  209. package/src/lib/paint-region-mode.ts +1 -1
  210. package/src/lib/placement-metadata.ts +4 -0
  211. package/src/lib/point-ask/describe-target.ts +6 -8
  212. package/src/lib/point-ask/region.ts +1 -1
  213. package/src/lib/point-ask/session.ts +1 -2
  214. package/src/lib/print-shell-compiler.ts +12 -1
  215. package/src/lib/print-wall-solids.test.ts +11 -0
  216. package/src/lib/print-wall-solids.ts +9 -0
  217. package/src/lib/roof-duplication.test.ts +100 -0
  218. package/src/lib/roof-duplication.ts +30 -5
  219. package/src/lib/room-built-on.test.ts +2 -5
  220. package/src/lib/room-built-on.ts +1 -15
  221. package/src/lib/room-construction-commands.ts +3 -2
  222. package/src/lib/room-controls-phase6.test.ts +1 -1
  223. package/src/lib/room-handle-drag.ts +8 -37
  224. package/src/lib/room-name-catalog.test.ts +2 -1
  225. package/src/lib/room-name-messages.test.ts +2 -1
  226. package/src/lib/room-name-messages.ts +1 -118
  227. package/src/lib/room-name-storage.test.ts +2 -1
  228. package/src/lib/room-push-dimensions.test.ts +1 -1
  229. package/src/lib/room-selection-commands.ts +2 -2
  230. package/src/lib/room-selection.test.ts +6 -6
  231. package/src/lib/room-transform-session.ts +1 -2
  232. package/src/lib/room-zone-routing.ts +2 -2
  233. package/src/lib/scene-clipboard.ts +2 -1
  234. package/src/lib/selection-routing.test.ts +0 -28
  235. package/src/lib/selection-routing.ts +1 -40
  236. package/src/lib/sfx-bus.ts +8 -32
  237. package/src/lib/sfx-player.test.ts +72 -1
  238. package/src/lib/sfx-player.ts +49 -337
  239. package/src/lib/snapping-mode.test.ts +23 -1
  240. package/src/lib/viewer-selection.test.ts +38 -10
  241. package/src/lib/wall-pull.test.ts +1 -1
  242. package/src/lib/wall-pull.ts +1 -27
  243. package/src/lib/wall-region-list.ts +3 -1
  244. package/src/lib/window-interaction.test.ts +4 -2
  245. package/src/store/use-editor.tsx +22 -26
  246. package/src/store/use-floorplan-annotation-visibility.ts +5 -5
  247. package/src/store/use-placement-typing.test.ts +165 -0
  248. package/src/store/use-placement-typing.ts +186 -0
  249. package/src/store/use-wall-region-handles-selection.ts +1 -2
  250. package/src/components/editor/__bench__/room-label-placement.bench.ts +0 -108
  251. package/src/components/editor/first-person/build-collider-world.test.ts +0 -596
  252. package/src/components/editor/first-person/build-collider-world.ts +0 -549
  253. package/src/components/editor/first-person/ground-holes.test.ts +0 -128
  254. package/src/components/editor/first-person/ground-holes.ts +0 -130
  255. package/src/components/editor/first-person/mechanism-targets.test.ts +0 -162
  256. package/src/components/editor/first-person/mechanism-targets.ts +0 -88
  257. package/src/components/editor/first-person/terrain-collider.test.ts +0 -132
  258. package/src/components/editor/first-person/terrain-collider.ts +0 -212
  259. package/src/components/editor/floorplan-camera-sync.test.ts +0 -249
  260. package/src/components/editor/floorplan-camera-sync.ts +0 -213
  261. package/src/components/editor/localized-html.tsx +0 -22
  262. package/src/components/editor-2d/renderers/floorplan-dimension-renderer.test.tsx +0 -236
  263. package/src/components/editor-2d/renderers/floorplan-dimension-renderer.tsx +0 -708
  264. package/src/components/editor-2d/renderers/floorplan-geometry-renderer.test.tsx +0 -385
  265. package/src/components/editor-2d/renderers/floorplan-geometry-renderer.tsx +0 -670
  266. package/src/components/editor-2d/renderers/floorplan-label-angle.test.ts +0 -89
  267. package/src/components/editor-2d/renderers/floorplan-label-angle.ts +0 -106
  268. package/src/components/editor-2d/svg-paths.ts +0 -124
  269. package/src/components/tools/elevator/elevator-defaults.ts +0 -8
  270. package/src/components/tools/elevator/elevator-tool.tsx +0 -372
  271. package/src/components/tools/elevator/move-elevator-tool.tsx +0 -262
  272. package/src/components/ui/action-menu/action-button.tsx +0 -89
  273. package/src/components/ui/action-menu/follow-pascal-toggle.tsx +0 -42
  274. package/src/components/ui/primitives/button.tsx +0 -69
  275. package/src/components/ui/primitives/dropdown-menu.tsx +0 -232
  276. package/src/components/ui/primitives/popover.tsx +0 -42
  277. package/src/components/ui/primitives/switch.tsx +0 -29
  278. package/src/components/ui/primitives/tooltip.tsx +0 -57
  279. package/src/components/ui/scene-loader.tsx +0 -73
  280. package/src/components/viewer/floorplan-compass-button.tsx +0 -67
  281. package/src/components/viewer/floorplan-preview-geometry.test.ts +0 -107
  282. package/src/components/viewer/floorplan-preview-geometry.ts +0 -281
  283. package/src/components/viewer/floorplan-preview-navigation.test.ts +0 -43
  284. package/src/components/viewer/floorplan-preview-navigation.ts +0 -62
  285. package/src/components/viewer/floorplan-preview-visibility.test.ts +0 -35
  286. package/src/components/viewer/floorplan-preview-visibility.ts +0 -19
  287. package/src/components/viewer/floorplan-preview.tsx +0 -1065
  288. package/src/components/viewer/render-settings-menu.tsx +0 -294
  289. package/src/components/viewer/sky-control.tsx +0 -710
  290. package/src/components/viewer/use-viewer-camera-navigation-sync.ts +0 -140
  291. package/src/components/viewer/use-viewer-ui-sounds.test.ts +0 -15
  292. package/src/components/viewer/use-viewer-ui-sounds.ts +0 -46
  293. package/src/components/viewer/viewer-controls-bar.tsx +0 -496
  294. package/src/components/viewer/viewer-hover-label.tsx +0 -42
  295. package/src/components/viewer/viewer-rooms.tsx +0 -248
  296. package/src/components/viewer/viewer-scene-header.tsx +0 -286
  297. package/src/components/viewer/viewer-selection-manager.tsx +0 -124
  298. package/src/components/viewer/viewer-stage-modes.test.ts +0 -38
  299. package/src/components/viewer/viewer-stage-modes.ts +0 -49
  300. package/src/components/viewer/viewer-stage-switcher.tsx +0 -97
  301. package/src/components/viewer/viewer-stage.test.tsx +0 -47
  302. package/src/components/viewer/viewer-stage.tsx +0 -221
  303. package/src/components/walkthrough-hud.tsx +0 -193
  304. package/src/hooks/use-mobile.ts +0 -19
  305. package/src/i18n/level-name.ts +0 -39
  306. package/src/i18n/scene-theme-name.ts +0 -16
  307. package/src/lib/below-grade.ts +0 -133
  308. package/src/lib/camera-pose.test.ts +0 -237
  309. package/src/lib/camera-pose.ts +0 -172
  310. package/src/lib/door-interaction.ts +0 -102
  311. package/src/lib/floorplan/floorplan-extension.test.ts +0 -42
  312. package/src/lib/floorplan/floorplan-readonly.test.ts +0 -47
  313. package/src/lib/floorplan/floorplan-readonly.ts +0 -95
  314. package/src/lib/floorplan/marks.test.ts +0 -213
  315. package/src/lib/floorplan/marks.ts +0 -238
  316. package/src/lib/measurement-label.ts +0 -68
  317. package/src/lib/measurements.ts +0 -225
  318. package/src/lib/node-display-label.ts +0 -37
  319. package/src/lib/room-assembly-overlay.test.ts +0 -275
  320. package/src/lib/room-assembly-overlay.ts +0 -265
  321. package/src/lib/room-selection.ts +0 -417
  322. package/src/lib/scene-bounds.test.ts +0 -183
  323. package/src/lib/scene-bounds.ts +0 -169
  324. package/src/lib/typing-target.ts +0 -9
  325. package/src/lib/viewer-selection.ts +0 -595
  326. package/src/lib/window-interaction.ts +0 -142
  327. package/src/lib/zone-content.ts +0 -133
  328. package/src/store/camera-pose-store.test.ts +0 -29
  329. package/src/store/camera-pose-store.ts +0 -27
  330. package/src/store/navigation-sync-pose-store.test.ts +0 -34
  331. package/src/store/navigation-sync-pose-store.ts +0 -27
  332. package/src/store/use-audio.test.ts +0 -146
  333. package/src/store/use-audio.tsx +0 -169
  334. package/src/store/use-first-person-hud.ts +0 -40
  335. package/src/store/use-follow-pascal.test.ts +0 -62
  336. package/src/store/use-follow-pascal.ts +0 -53
@@ -1,89 +0,0 @@
1
- import { describe, expect, test } from 'bun:test'
2
- import {
3
- resolveFloorplanAnnotationLabelTransform,
4
- resolveFloorplanAnnotationUpdate,
5
- resolveFloorplanLabelAngle,
6
- shouldUpdateFloorplanLabelRotation,
7
- updateSvgFloorplanLabelOrientations,
8
- } from './floorplan-label-angle'
9
-
10
- describe('resolveFloorplanLabelAngle', () => {
11
- test('keeps segment labels readable while preserving their screen direction', () => {
12
- expect(resolveFloorplanLabelAngle(0, 0)).toBe(0)
13
- expect(resolveFloorplanLabelAngle(Math.PI, 0)).toBe(0)
14
- expect(resolveFloorplanLabelAngle(Math.PI / 2, 90)).toBe(-90)
15
- })
16
-
17
- test('counter-rotates aggregate labels to remain horizontal', () => {
18
- expect(resolveFloorplanLabelAngle(0, 90, true)).toBe(-90)
19
- expect(resolveFloorplanLabelAngle(Math.PI / 3, -35, true)).toBe(35)
20
- })
21
-
22
- test('ignores sub-degree rotation changes for annotation layout', () => {
23
- expect(shouldUpdateFloorplanLabelRotation(30, 30.9)).toBe(false)
24
- expect(shouldUpdateFloorplanLabelRotation(30, 31)).toBe(true)
25
- expect(shouldUpdateFloorplanLabelRotation(359.5, 0.25)).toBe(false)
26
- })
27
-
28
- test('updates only label orientation while preserving its layout shift', () => {
29
- expect(
30
- resolveFloorplanAnnotationLabelTransform({
31
- afterRotation: 'translate(0 -0.2)',
32
- angleRadians: 0,
33
- beforeRotation: 'translate(4 6)',
34
- layoutDx: 0.5,
35
- layoutDy: -0.25,
36
- sceneRotationDeg: 90,
37
- screenUpright: true,
38
- }),
39
- ).toEqual({
40
- defaultTransform: 'translate(4 6) rotate(-90) translate(0 -0.2)',
41
- transform: 'translate(4 6) rotate(-90) translate(0 -0.2) translate(0.5 -0.25)',
42
- })
43
- })
44
-
45
- test('keeps a rotation-only update out of the full collision layout path', () => {
46
- expect(
47
- resolveFloorplanAnnotationUpdate({
48
- layoutInputsChanged: false,
49
- nextRotationDeg: 45,
50
- previousRotationDeg: 30,
51
- }),
52
- ).toEqual({
53
- resolveCollisions: false,
54
- updateLabelPresentation: true,
55
- })
56
- })
57
-
58
- test('runs collision layout when floor-plan scene inputs change', () => {
59
- expect(
60
- resolveFloorplanAnnotationUpdate({
61
- layoutInputsChanged: true,
62
- nextRotationDeg: 30,
63
- previousRotationDeg: 30,
64
- }),
65
- ).toEqual({
66
- resolveCollisions: true,
67
- updateLabelPresentation: true,
68
- })
69
- })
70
-
71
- test('updates captured label references without rediscovering the DOM', () => {
72
- const attributes = new Map<string, string>([['transform', 'translate(4 6) rotate(0)']])
73
- const label = {
74
- dataset: {
75
- floorplanAnnotationAngleRadians: '0',
76
- floorplanAnnotationLayoutDx: '0.5',
77
- floorplanAnnotationLayoutDy: '-0.25',
78
- floorplanAnnotationScreenUpright: 'true',
79
- floorplanAnnotationTransformAfterRotation: '',
80
- floorplanAnnotationTransformBeforeRotation: 'translate(4 6)',
81
- },
82
- getAttribute: (name: string) => attributes.get(name) ?? null,
83
- setAttribute: (name: string, value: string) => attributes.set(name, value),
84
- } as unknown as SVGGElement
85
-
86
- expect(updateSvgFloorplanLabelOrientations([label], 90)).toBe(1)
87
- expect(attributes.get('transform')).toBe('translate(4 6) rotate(-90) translate(0.5 -0.25)')
88
- })
89
- })
@@ -1,106 +0,0 @@
1
- export function resolveFloorplanLabelAngle(
2
- angleRadians: number,
3
- sceneRotationDeg: number,
4
- screenUpright = false,
5
- ): number {
6
- if (screenUpright) return -sceneRotationDeg
7
-
8
- let localAngleDeg = (angleRadians * 180) / Math.PI
9
- let screenAngleDeg = localAngleDeg + sceneRotationDeg
10
- screenAngleDeg = ((((screenAngleDeg + 180) % 360) + 360) % 360) - 180
11
- if (screenAngleDeg > 90) localAngleDeg -= 180
12
- else if (screenAngleDeg <= -90) localAngleDeg += 180
13
- return ((((localAngleDeg + 180) % 360) + 360) % 360) - 180
14
- }
15
-
16
- export function shouldUpdateFloorplanLabelRotation(
17
- previousRotationDeg: number | null,
18
- nextRotationDeg: number,
19
- minimumDeltaDeg = 1,
20
- ): boolean {
21
- if (previousRotationDeg === null) return true
22
- const deltaDeg = ((((nextRotationDeg - previousRotationDeg + 180) % 360) + 360) % 360) - 180
23
- return Math.abs(deltaDeg) >= minimumDeltaDeg
24
- }
25
-
26
- export function resolveFloorplanAnnotationUpdate({
27
- layoutInputsChanged,
28
- previousRotationDeg,
29
- nextRotationDeg,
30
- }: {
31
- layoutInputsChanged: boolean
32
- previousRotationDeg: number | null
33
- nextRotationDeg: number
34
- }): {
35
- resolveCollisions: boolean
36
- updateLabelPresentation: boolean
37
- } {
38
- return {
39
- resolveCollisions: layoutInputsChanged,
40
- updateLabelPresentation:
41
- layoutInputsChanged ||
42
- shouldUpdateFloorplanLabelRotation(previousRotationDeg, nextRotationDeg),
43
- }
44
- }
45
-
46
- export function resolveFloorplanAnnotationLabelTransform({
47
- angleRadians,
48
- sceneRotationDeg,
49
- screenUpright,
50
- beforeRotation,
51
- afterRotation,
52
- layoutDx = 0,
53
- layoutDy = 0,
54
- }: {
55
- angleRadians: number
56
- sceneRotationDeg: number
57
- screenUpright: boolean
58
- beforeRotation: string
59
- afterRotation: string
60
- layoutDx?: number
61
- layoutDy?: number
62
- }): {
63
- defaultTransform: string
64
- transform: string
65
- } {
66
- const degrees = resolveFloorplanLabelAngle(angleRadians, sceneRotationDeg, screenUpright)
67
- const defaultTransform = [beforeRotation, `rotate(${degrees})`, afterRotation]
68
- .filter(Boolean)
69
- .join(' ')
70
- return {
71
- defaultTransform,
72
- transform:
73
- layoutDx === 0 && layoutDy === 0
74
- ? defaultTransform
75
- : `${defaultTransform} translate(${layoutDx} ${layoutDy})`,
76
- }
77
- }
78
-
79
- export function updateSvgFloorplanLabelOrientations(
80
- labels: Iterable<SVGGElement>,
81
- sceneRotationDeg: number,
82
- ): number {
83
- let updated = 0
84
-
85
- for (const label of labels) {
86
- const angleRadians = Number(label.dataset.floorplanAnnotationAngleRadians)
87
- if (!Number.isFinite(angleRadians)) continue
88
-
89
- const { defaultTransform, transform } = resolveFloorplanAnnotationLabelTransform({
90
- angleRadians,
91
- sceneRotationDeg,
92
- screenUpright: label.dataset.floorplanAnnotationScreenUpright === 'true',
93
- beforeRotation: label.dataset.floorplanAnnotationTransformBeforeRotation ?? '',
94
- afterRotation: label.dataset.floorplanAnnotationTransformAfterRotation ?? '',
95
- layoutDx: Number(label.dataset.floorplanAnnotationLayoutDx ?? 0),
96
- layoutDy: Number(label.dataset.floorplanAnnotationLayoutDy ?? 0),
97
- })
98
- if (label.getAttribute('transform') === transform) continue
99
-
100
- label.dataset.floorplanAnnotationDefaultTransform = defaultTransform
101
- label.setAttribute('transform', transform)
102
- updated += 1
103
- }
104
-
105
- return updated
106
- }
@@ -1,124 +0,0 @@
1
- 'use client'
2
-
3
- import type { Point2D } from '@pascal-app/core'
4
-
5
- function toSvgX(value: number) {
6
- return value
7
- }
8
-
9
- function toSvgY(value: number) {
10
- return value
11
- }
12
-
13
- function toSvgPoint(point: Point2D) {
14
- return {
15
- x: toSvgX(point.x),
16
- y: toSvgY(point.y),
17
- }
18
- }
19
-
20
- export function formatPolygonPath(points: Point2D[], holes: Point2D[][] = []) {
21
- const formatSubpath = (subpathPoints: Point2D[]) => {
22
- const [firstPoint, ...restPoints] = subpathPoints
23
- if (!firstPoint) {
24
- return null
25
- }
26
-
27
- const firstSvgPoint = toSvgPoint(firstPoint)
28
-
29
- return [
30
- `M ${firstSvgPoint.x} ${firstSvgPoint.y}`,
31
- ...restPoints.map((point) => {
32
- const svgPoint = toSvgPoint(point)
33
- return `L ${svgPoint.x} ${svgPoint.y}`
34
- }),
35
- 'Z',
36
- ].join(' ')
37
- }
38
-
39
- return [points, ...holes].map(formatSubpath).filter(Boolean).join(' ')
40
- }
41
-
42
- export function buildSvgPolylinePath(points: Point2D[]) {
43
- if (points.length < 2) {
44
- return null
45
- }
46
-
47
- return points
48
- .map((point, index) => {
49
- const svgPoint = toSvgPoint(point)
50
- return `${index === 0 ? 'M' : 'L'} ${svgPoint.x} ${svgPoint.y}`
51
- })
52
- .join(' ')
53
- }
54
-
55
- export function getArcPlanPoint(center: Point2D, radius: number, angle: number): Point2D {
56
- return {
57
- x: center.x + Math.cos(angle) * radius,
58
- y: center.y + Math.sin(angle) * radius,
59
- }
60
- }
61
-
62
- export function buildSvgArcPath(
63
- center: Point2D,
64
- radius: number,
65
- startAngle: number,
66
- endAngle: number,
67
- ) {
68
- const start = getArcPlanPoint(center, radius, startAngle)
69
- return `M ${toSvgX(start.x)} ${toSvgY(start.y)} ${svgArcCommands(center, radius, startAngle, endAngle)}`
70
- }
71
-
72
- export function buildSvgAnnularSectorPath(
73
- center: Point2D,
74
- innerRadius: number,
75
- outerRadius: number,
76
- startAngle: number,
77
- endAngle: number,
78
- ) {
79
- const outerStart = getArcPlanPoint(center, outerRadius, startAngle)
80
- const innerEnd = getArcPlanPoint(center, innerRadius, endAngle)
81
- return [
82
- `M ${toSvgX(outerStart.x)} ${toSvgY(outerStart.y)}`,
83
- svgArcCommands(center, outerRadius, startAngle, endAngle),
84
- `L ${toSvgX(innerEnd.x)} ${toSvgY(innerEnd.y)}`,
85
- svgArcCommands(center, innerRadius, endAngle, startAngle),
86
- 'Z',
87
- ].join(' ')
88
- }
89
-
90
- function svgArcCommands(center: Point2D, radius: number, startAngle: number, endAngle: number) {
91
- const delta = endAngle - startAngle
92
- const count = Math.max(1, Math.ceil(Math.abs(delta) / Math.PI))
93
- return Array.from({ length: count }, (_, index) => {
94
- const end = getArcPlanPoint(center, radius, startAngle + (delta * (index + 1)) / count)
95
- return `A ${radius} ${radius} 0 0 ${delta >= 0 ? 1 : 0} ${toSvgX(end.x)} ${toSvgY(end.y)}`
96
- }).join(' ')
97
- }
98
-
99
- export function formatSvgPolygonPoints(points: Point2D[]) {
100
- return points.map((point) => `${toSvgX(point.x)},${toSvgY(point.y)}`).join(' ')
101
- }
102
-
103
- /**
104
- * Three points defining an arrow head — tip + two trailing barbs.
105
- * Returned as plain `Point2D` objects so consumers can either feed them
106
- * straight into `formatSvgPolygonPoints` (for SVG `points=""`) or push
107
- * them onto a `FloorplanGeometry.polygon.points` array. Mixing both
108
- * downstream paths through a string-returning helper was awkward — see
109
- * `nodes/src/stair/floorplan.ts` which needs the points as objects.
110
- */
111
- export function buildSvgArrowHeadPoints(point: Point2D, angle: number, size: number): Point2D[] {
112
- const left = {
113
- x: point.x - size * Math.cos(angle - Math.PI / 6),
114
- y: point.y - size * Math.sin(angle - Math.PI / 6),
115
- }
116
- const right = {
117
- x: point.x - size * Math.cos(angle + Math.PI / 6),
118
- y: point.y - size * Math.sin(angle + Math.PI / 6),
119
- }
120
-
121
- return [point, left, right]
122
- }
123
-
124
- export { toSvgPoint, toSvgX, toSvgY }
@@ -1,8 +0,0 @@
1
- export const DEFAULT_ELEVATOR_WIDTH = 1.84
2
- export const DEFAULT_ELEVATOR_DEPTH = 1.84
3
- export const DEFAULT_ELEVATOR_CAB_HEIGHT = 2.35
4
- export const DEFAULT_ELEVATOR_DOOR_WIDTH = 0.95
5
- export const DEFAULT_ELEVATOR_DOOR_HEIGHT = 2.1
6
- export const DEFAULT_ELEVATOR_SPEED = 2.2
7
- export const DEFAULT_ELEVATOR_DOOR_DURATION_MS = 900
8
- export const DEFAULT_ELEVATOR_DWELL_MS = 1400
@@ -1,372 +0,0 @@
1
- import {
2
- type AnyNodeId,
3
- type BuildingNode,
4
- collectAlignmentAnchors,
5
- ElevatorNode,
6
- emitter,
7
- type GridEvent,
8
- type LevelNode,
9
- resolveAlignment,
10
- useScene,
11
- } from '@pascal-app/core'
12
- import { useEffect, useMemo, useRef } from 'react'
13
- import * as THREE from 'three'
14
- import { resolveCurrentBuildingId, resolveElevatorSupportY } from '../../../lib/elevator-support'
15
- import { sfxEmitter } from '../../../lib/sfx-bus'
16
- import { isTypingTarget } from '../../../lib/typing-target'
17
- import useAlignmentGuides from '../../../store/use-alignment-guides'
18
- import useEditor, {
19
- isAlignmentGuideActive,
20
- isGridSnapActive,
21
- isMagneticSnapActive,
22
- } from '../../../store/use-editor'
23
- import usePlacementPreview from '../../../store/use-placement-preview'
24
- import { CursorSphere } from '../shared/cursor-sphere'
25
- import {
26
- DEFAULT_ELEVATOR_CAB_HEIGHT,
27
- DEFAULT_ELEVATOR_DEPTH,
28
- DEFAULT_ELEVATOR_DOOR_DURATION_MS,
29
- DEFAULT_ELEVATOR_DOOR_HEIGHT,
30
- DEFAULT_ELEVATOR_DOOR_WIDTH,
31
- DEFAULT_ELEVATOR_DWELL_MS,
32
- DEFAULT_ELEVATOR_SPEED,
33
- DEFAULT_ELEVATOR_WIDTH,
34
- } from './elevator-defaults'
35
-
36
- const GRID_OFFSET = 0.02
37
- /** Figma-style alignment-snap threshold (meters), matching the move tools. */
38
- const ALIGNMENT_THRESHOLD_M = 0.08
39
-
40
- type ElevatorToolProps = {
41
- buildingId: BuildingNode['id'] | null
42
- levelId: LevelNode['id'] | null
43
- onPlaced?: (elevatorId: AnyNodeId, buildingId: BuildingNode['id']) => void
44
- }
45
-
46
- function resolveDefaultServiceRange(
47
- buildingId: BuildingNode['id'],
48
- selectedLevelId: LevelNode['id'] | null,
49
- ): {
50
- defaultLevelId: LevelNode['id'] | null
51
- fromLevelId: LevelNode['id'] | null
52
- toLevelId: LevelNode['id'] | null
53
- } {
54
- const nodes = useScene.getState().nodes
55
- const building = nodes[buildingId as AnyNodeId]
56
- if (building?.type !== 'building') {
57
- return { defaultLevelId: null, fromLevelId: null, toLevelId: null }
58
- }
59
-
60
- const levels = building.children
61
- .map((childId) => nodes[childId as AnyNodeId])
62
- .filter((node): node is LevelNode => node?.type === 'level')
63
- .sort((left, right) => left.level - right.level)
64
- const selectedLevelIndex = levels.findIndex((level) => level.id === selectedLevelId)
65
- const fromIndex = Math.max(selectedLevelIndex, 0)
66
- const fromLevel = levels[fromIndex]
67
- const toLevel = levels[Math.min(fromIndex + 1, levels.length - 1)] ?? fromLevel
68
-
69
- return {
70
- defaultLevelId: fromLevel?.id ?? null,
71
- fromLevelId: fromLevel?.id ?? null,
72
- toLevelId: toLevel?.id ?? fromLevel?.id ?? null,
73
- }
74
- }
75
-
76
- function createElevatorPreviewGeometry(): THREE.BufferGeometry {
77
- return new THREE.BoxGeometry(
78
- DEFAULT_ELEVATOR_WIDTH,
79
- DEFAULT_ELEVATOR_CAB_HEIGHT,
80
- DEFAULT_ELEVATOR_DEPTH,
81
- )
82
- }
83
-
84
- function createElevatorPreviewEdgeGeometry(): THREE.BufferGeometry {
85
- return new THREE.EdgesGeometry(createElevatorPreviewGeometry())
86
- }
87
-
88
- function commitElevatorPlacement(
89
- buildingId: BuildingNode['id'],
90
- selectedLevelId: LevelNode['id'] | null,
91
- x: number,
92
- z: number,
93
- rotation: number,
94
- onPlaced: ElevatorToolProps['onPlaced'],
95
- ): void {
96
- const { createNode, nodes } = useScene.getState()
97
- const elevatorCount = Object.values(nodes).filter((node) => node.type === 'elevator').length
98
- const serviceRange = resolveDefaultServiceRange(buildingId, selectedLevelId)
99
- const supportY = resolveElevatorSupportY({
100
- buildingId,
101
- preferredLevelId: serviceRange.fromLevelId ?? serviceRange.defaultLevelId,
102
- x,
103
- z,
104
- })
105
- const elevator = ElevatorNode.parse({
106
- name: `Elevator ${elevatorCount + 1}`,
107
- parentId: buildingId,
108
- position: [x, supportY, z],
109
- rotation,
110
- width: DEFAULT_ELEVATOR_WIDTH,
111
- depth: DEFAULT_ELEVATOR_DEPTH,
112
- cabHeight: DEFAULT_ELEVATOR_CAB_HEIGHT,
113
- doorWidth: DEFAULT_ELEVATOR_DOOR_WIDTH,
114
- doorHeight: DEFAULT_ELEVATOR_DOOR_HEIGHT,
115
- ...serviceRange,
116
- speed: DEFAULT_ELEVATOR_SPEED,
117
- doorDurationMs: DEFAULT_ELEVATOR_DOOR_DURATION_MS,
118
- dwellMs: DEFAULT_ELEVATOR_DWELL_MS,
119
- })
120
-
121
- createNode(elevator, buildingId)
122
- onPlaced?.(elevator.id as AnyNodeId, buildingId)
123
- sfxEmitter.emit('sfx:structure-build')
124
- }
125
-
126
- export const ElevatorTool: React.FC<ElevatorToolProps> = ({ buildingId, levelId, onPlaced }) => {
127
- const cursorRef = useRef<THREE.Group>(null)
128
- const previewRef = useRef<THREE.Group>(null)
129
- const rotationRef = useRef(0)
130
- const previousGridPosRef = useRef<[number, number] | null>(null)
131
- const previewGeometry = useMemo(() => createElevatorPreviewGeometry(), [])
132
- const previewEdgeGeometry = useMemo(() => createElevatorPreviewEdgeGeometry(), [])
133
-
134
- // Default-shaped elevator for the 2D floor-plan placement ghost. The 3D
135
- // preview meshes below are hidden in 2D (canvas `display:none`), so this
136
- // feeds `usePlacementPreview` → `FloorplanPlacementPreviewLayer`, which
137
- // renders the elevator's footprint following the cursor.
138
- const floorplanPreviewNode = useMemo(
139
- () =>
140
- ElevatorNode.parse({
141
- name: 'Elevator',
142
- position: [0, 0, 0],
143
- rotation: 0,
144
- width: DEFAULT_ELEVATOR_WIDTH,
145
- depth: DEFAULT_ELEVATOR_DEPTH,
146
- cabHeight: DEFAULT_ELEVATOR_CAB_HEIGHT,
147
- doorWidth: DEFAULT_ELEVATOR_DOOR_WIDTH,
148
- doorHeight: DEFAULT_ELEVATOR_DOOR_HEIGHT,
149
- }),
150
- [],
151
- )
152
-
153
- useEffect(() => {
154
- const currentBuildingId = resolveCurrentBuildingId({
155
- buildingId,
156
- levelId,
157
- nodes: useScene.getState().nodes,
158
- })
159
- if (!currentBuildingId) return
160
-
161
- rotationRef.current = 0
162
- if (previewRef.current) previewRef.current.rotation.y = 0
163
-
164
- // Alignment candidates — anchors of every alignable object; refreshed
165
- // after each placement. The elevator aligns by its ORIGIN point.
166
- let alignmentCandidates = collectAlignmentAnchors(useScene.getState().nodes, '', levelId)
167
- // Snap the elevator origin onto another object's nearest real anchor and
168
- // publish the guide. The probe is the RAW cursor, NOT the 0.5m-grid-snapped
169
- // point: resolving against the grid point would only ever catch anchors
170
- // that happen to sit on a grid line, so off-grid items (furniture, angled
171
- // walls) would never surface a guide. The matched axis locks exactly to the
172
- // candidate's coordinate; the other axis keeps its grid snap. Guides are
173
- // published in every snapping mode (including Off); the magnetic pull toward
174
- // them (applySnap) applies only in 'lines' mode.
175
- const alignPoint = (
176
- gridX: number,
177
- gridZ: number,
178
- rawX: number,
179
- rawZ: number,
180
- bypass: boolean,
181
- applySnap: boolean,
182
- ): [number, number] => {
183
- if (bypass || alignmentCandidates.length === 0) {
184
- useAlignmentGuides.getState().clear()
185
- return [gridX, gridZ]
186
- }
187
- const ar = resolveAlignment({
188
- moving: [{ nodeId: '__elevator-draft__', kind: 'corner', x: rawX, z: rawZ }],
189
- candidates: alignmentCandidates,
190
- threshold: ALIGNMENT_THRESHOLD_M,
191
- })
192
- if (ar.guides.length === 0) {
193
- useAlignmentGuides.getState().clear()
194
- return [gridX, gridZ]
195
- }
196
- useAlignmentGuides.getState().set(ar.guides)
197
- if (!applySnap) return [gridX, gridZ]
198
- let x = gridX
199
- let z = gridZ
200
- for (const guide of ar.guides) {
201
- if (guide.axis === 'x') x = guide.coord
202
- else z = guide.coord
203
- }
204
- return [x, z]
205
- }
206
-
207
- const onGridMove = (event: GridEvent) => {
208
- // Grid snap follows the global mode (live step so the HUD chip is
209
- // honest); Off keeps the raw cursor. Shift cycles the mode centrally.
210
- const step = useEditor.getState().gridSnapStep
211
- const [gridX, gridZ] = alignPoint(
212
- isGridSnapActive()
213
- ? Math.round(event.localPosition[0] / step) * step
214
- : event.localPosition[0],
215
- isGridSnapActive()
216
- ? Math.round(event.localPosition[2] / step) * step
217
- : event.localPosition[2],
218
- event.localPosition[0],
219
- event.localPosition[2],
220
- !isAlignmentGuideActive(),
221
- isMagneticSnapActive(),
222
- )
223
- const supportY = resolveElevatorSupportY({
224
- buildingId: currentBuildingId,
225
- preferredLevelId: levelId as LevelNode['id'] | null,
226
- x: gridX,
227
- z: gridZ,
228
- })
229
-
230
- cursorRef.current?.position.set(gridX, supportY + GRID_OFFSET, gridZ)
231
- previewRef.current?.position.set(gridX, supportY + DEFAULT_ELEVATOR_CAB_HEIGHT / 2, gridZ)
232
-
233
- // Publish the 2D floor-plan ghost at the snapped/aligned cursor.
234
- usePlacementPreview.getState().set({
235
- ...floorplanPreviewNode,
236
- position: [gridX, supportY, gridZ],
237
- rotation: rotationRef.current,
238
- })
239
-
240
- if (
241
- (isGridSnapActive() || isMagneticSnapActive()) &&
242
- previousGridPosRef.current &&
243
- (gridX !== previousGridPosRef.current[0] || gridZ !== previousGridPosRef.current[1])
244
- ) {
245
- sfxEmitter.emit('sfx:grid-snap')
246
- }
247
-
248
- previousGridPosRef.current = [gridX, gridZ]
249
- }
250
-
251
- const onGridClick = (event: GridEvent) => {
252
- const latestBuildingId = resolveCurrentBuildingId({
253
- buildingId,
254
- levelId,
255
- nodes: useScene.getState().nodes,
256
- })
257
- if (!latestBuildingId) return
258
-
259
- const step = useEditor.getState().gridSnapStep
260
- const [gridX, gridZ] = alignPoint(
261
- isGridSnapActive()
262
- ? Math.round(event.localPosition[0] / step) * step
263
- : event.localPosition[0],
264
- isGridSnapActive()
265
- ? Math.round(event.localPosition[2] / step) * step
266
- : event.localPosition[2],
267
- event.localPosition[0],
268
- event.localPosition[2],
269
- !isAlignmentGuideActive(),
270
- isMagneticSnapActive(),
271
- )
272
- commitElevatorPlacement(
273
- latestBuildingId,
274
- levelId,
275
- gridX,
276
- gridZ,
277
- rotationRef.current,
278
- onPlaced,
279
- )
280
- alignmentCandidates = collectAlignmentAnchors(useScene.getState().nodes, '', levelId)
281
- useAlignmentGuides.getState().clear()
282
- // The placed elevator's footprint now renders for real; drop the ghost
283
- // (the next grid:move re-publishes it for the following placement).
284
- usePlacementPreview.getState().clear()
285
- }
286
-
287
- const onKeyDown = (event: KeyboardEvent) => {
288
- if (isTypingTarget(event.target)) return
289
-
290
- const ROTATION_STEP = Math.PI / 4
291
- let rotationDelta = 0
292
- if (event.key === 'r' || event.key === 'R') rotationDelta = ROTATION_STEP
293
- else if (event.key === 't' || event.key === 'T') rotationDelta = -ROTATION_STEP
294
-
295
- if (rotationDelta !== 0) {
296
- event.preventDefault()
297
- sfxEmitter.emit('sfx:item-rotate')
298
- rotationRef.current += rotationDelta
299
- if (previewRef.current) previewRef.current.rotation.y = rotationRef.current
300
- // Reflect the rotation in the 2D ghost immediately (no pointer move
301
- // needed) by republishing at the last snapped cursor position.
302
- const last = previousGridPosRef.current
303
- if (last) {
304
- usePlacementPreview.getState().set({
305
- ...floorplanPreviewNode,
306
- position: [last[0], 0, last[1]],
307
- rotation: rotationRef.current,
308
- })
309
- }
310
- }
311
- }
312
-
313
- emitter.on('grid:move', onGridMove)
314
- emitter.on('grid:click', onGridClick)
315
- window.addEventListener('keydown', onKeyDown)
316
-
317
- return () => {
318
- emitter.off('grid:move', onGridMove)
319
- emitter.off('grid:click', onGridClick)
320
- window.removeEventListener('keydown', onKeyDown)
321
- useAlignmentGuides.getState().clear()
322
- usePlacementPreview.getState().clear()
323
- }
324
- }, [buildingId, levelId, onPlaced, floorplanPreviewNode])
325
-
326
- return (
327
- <group>
328
- <CursorSphere ref={cursorRef} />
329
- <group ref={previewRef}>
330
- <mesh geometry={previewGeometry}>
331
- <meshStandardMaterial color="#38bdf8" depthWrite={false} opacity={0.12} transparent />
332
- </mesh>
333
- <lineSegments geometry={previewEdgeGeometry}>
334
- <lineBasicMaterial color="#38bdf8" opacity={0.9} transparent />
335
- </lineSegments>
336
- <mesh
337
- position={[0, -DEFAULT_ELEVATOR_CAB_HEIGHT / 2 + 0.015, 0]}
338
- rotation={[-Math.PI / 2, 0, 0]}
339
- >
340
- <planeGeometry args={[DEFAULT_ELEVATOR_WIDTH, DEFAULT_ELEVATOR_DEPTH]} />
341
- <meshBasicMaterial color="#38bdf8" opacity={0.16} transparent />
342
- </mesh>
343
- <mesh position={[0, 0, -DEFAULT_ELEVATOR_DEPTH / 2 - 0.01]}>
344
- <planeGeometry args={[DEFAULT_ELEVATOR_WIDTH, DEFAULT_ELEVATOR_CAB_HEIGHT]} />
345
- <meshBasicMaterial color="#e0f2fe" opacity={0.18} transparent />
346
- </mesh>
347
- <mesh
348
- position={[
349
- 0,
350
- -DEFAULT_ELEVATOR_CAB_HEIGHT / 2 + DEFAULT_ELEVATOR_DOOR_HEIGHT / 2,
351
- -DEFAULT_ELEVATOR_DEPTH / 2 - 0.03,
352
- ]}
353
- >
354
- <boxGeometry args={[DEFAULT_ELEVATOR_DOOR_WIDTH, DEFAULT_ELEVATOR_DOOR_HEIGHT, 0.035]} />
355
- <meshStandardMaterial color="#e5e7eb" depthWrite={false} opacity={0.45} transparent />
356
- </mesh>
357
- <mesh
358
- position={[
359
- 0,
360
- -DEFAULT_ELEVATOR_CAB_HEIGHT / 2 + DEFAULT_ELEVATOR_DOOR_HEIGHT / 2,
361
- -DEFAULT_ELEVATOR_DEPTH / 2 - 0.049,
362
- ]}
363
- >
364
- <boxGeometry
365
- args={[DEFAULT_ELEVATOR_DOOR_WIDTH + 0.14, DEFAULT_ELEVATOR_DOOR_HEIGHT + 0.14, 0.01]}
366
- />
367
- <meshBasicMaterial color="#38bdf8" opacity={0.5} transparent />
368
- </mesh>
369
- </group>
370
- </group>
371
- )
372
- }