@pascal-app/editor 1.0.3 → 1.0.5

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 (624) hide show
  1. package/README.md +106 -0
  2. package/package.json +42 -38
  3. package/src/components/editor/__bench__/room-label-placement.bench.ts +108 -0
  4. package/src/components/editor/action-menu-button.tsx +83 -0
  5. package/src/components/editor/bake-thumbnail.tsx +307 -5
  6. package/src/components/editor/capture-level-picker.tsx +72 -0
  7. package/src/components/editor/construction-reveal-audio.test.tsx +150 -0
  8. package/src/components/editor/construction-reveal-audio.tsx +40 -0
  9. package/src/components/editor/custom-camera-controls.tsx +275 -57
  10. package/src/components/editor/delete-confirmation-dialog.tsx +297 -7
  11. package/src/components/editor/duplicate-node.ts +244 -0
  12. package/src/components/editor/editor-layout-mobile.tsx +37 -26
  13. package/src/components/editor/editor-layout-v2.tsx +132 -59
  14. package/src/components/editor/export-manager.tsx +47 -26
  15. package/src/components/editor/first-person/build-collider-world.test.ts +320 -0
  16. package/src/components/editor/first-person/build-collider-world.ts +28 -7
  17. package/src/components/editor/first-person/ground-holes.test.ts +128 -0
  18. package/src/components/editor/first-person/ground-holes.ts +130 -0
  19. package/src/components/editor/first-person/mechanism-targets.test.ts +162 -0
  20. package/src/components/editor/first-person/mechanism-targets.ts +88 -0
  21. package/src/components/editor/first-person-controls.tsx +220 -9
  22. package/src/components/editor/floating-action-menu.tsx +63 -207
  23. package/src/components/editor/floor-aware-camera-controls.test.ts +466 -0
  24. package/src/components/editor/floor-aware-camera-controls.ts +324 -0
  25. package/src/components/editor/floor-openings-overlay.tsx +243 -0
  26. package/src/components/editor/floor-region-controls.tsx +846 -0
  27. package/src/components/editor/floor-region-webgpu-materials.test.ts +12 -0
  28. package/src/components/editor/floor-surface.test.ts +165 -0
  29. package/src/components/editor/floor-surface.ts +182 -0
  30. package/src/components/editor/floorplan-background-selection.test.ts +11 -2
  31. package/src/components/editor/floorplan-background-selection.ts +1 -3
  32. package/src/components/editor/floorplan-mode-coordinator.tsx +2 -2
  33. package/src/components/editor/floorplan-panel.tsx +414 -363
  34. package/src/components/editor/follow-pascal.test.tsx +912 -0
  35. package/src/components/editor/follow-pascal.tsx +354 -0
  36. package/src/components/editor/footprint-height-handle.tsx +204 -0
  37. package/src/components/editor/group-actions.ts +56 -2
  38. package/src/components/editor/group-move-3d.ts +2 -0
  39. package/src/components/editor/group-rotate-handle.tsx +2 -0
  40. package/src/components/editor/group-transform-shared.test.ts +460 -0
  41. package/src/components/editor/handles/handle-hit-priority.test.tsx +63 -0
  42. package/src/components/editor/handles/handle-hit-priority.tsx +37 -0
  43. package/src/components/editor/handles/swallow-next-click.test.ts +28 -0
  44. package/src/components/editor/handles/use-handle-drag.ts +269 -197
  45. package/src/components/editor/index.tsx +409 -197
  46. package/src/components/editor/join-walls-pill.tsx +44 -0
  47. package/src/components/editor/node-action-menu.tsx +68 -70
  48. package/src/components/editor/node-arrow-handles.tsx +25 -6
  49. package/src/components/editor/open-wall-ends-3d-layer.tsx +273 -0
  50. package/src/components/editor/open-wall-ends-hint.tsx +89 -0
  51. package/src/components/editor/quick-measurement-hud.tsx +3 -2
  52. package/src/components/editor/registry-action-contributions.test.tsx +63 -0
  53. package/src/components/editor/registry-action-contributions.tsx +80 -1
  54. package/src/components/editor/riser-diagram-panel.tsx +14 -7
  55. package/src/components/editor/room-controls.tsx +847 -0
  56. package/src/components/editor/room-floor-highlight.tsx +128 -0
  57. package/src/components/editor/room-handles-2d.tsx +354 -0
  58. package/src/components/editor/room-handles.tsx +675 -0
  59. package/src/components/editor/room-highlight.tsx +233 -0
  60. package/src/components/editor/room-labels.test.ts +850 -0
  61. package/src/components/editor/room-labels.tsx +1890 -0
  62. package/src/components/editor/room-transform-ghost.tsx +256 -0
  63. package/src/components/editor/room-webgpu-materials.test.ts +27 -0
  64. package/src/components/editor/selection-manager.tsx +474 -419
  65. package/src/components/editor/slab-hole-highlights.tsx +5 -2
  66. package/src/components/editor/snapshot-capture-overlay.tsx +7 -2
  67. package/src/components/editor/snapshot-capture.test.ts +119 -60
  68. package/src/components/editor/snapshot-capture.ts +36 -5
  69. package/src/components/editor/thumbnail-generator.test.ts +654 -0
  70. package/src/components/editor/thumbnail-generator.tsx +1103 -223
  71. package/src/components/editor/use-floorplan-background-placement.ts +34 -12
  72. package/src/components/editor/vector-edge-extractor.tsx +121 -0
  73. package/src/components/editor/views/host-view.tsx +61 -0
  74. package/src/components/editor/views/plugins-view.tsx +663 -0
  75. package/src/components/editor/views/scene-region.tsx +59 -0
  76. package/src/components/editor/views/use-editor-views.ts +57 -0
  77. package/src/components/editor/views/view-bar.test.tsx +54 -0
  78. package/src/components/editor/views/view-bar.tsx +343 -0
  79. package/src/components/editor/wall-justification.test.ts +140 -0
  80. package/src/components/editor/wall-move-side-handles.tsx +43 -142
  81. package/src/components/editor/wall-push-arrow.tsx +92 -0
  82. package/src/components/editor/wall-region-handles.tsx +509 -0
  83. package/src/components/editor/wall-snap-beacon-layer.tsx +8 -4
  84. package/src/components/editor-2d/floorplan-cursor-indicator-overlay.tsx +8 -4
  85. package/src/components/editor-2d/floorplan-group-move.tsx +3 -0
  86. package/src/components/editor-2d/floorplan-open-wall-ends-layer.test.tsx +86 -0
  87. package/src/components/editor-2d/floorplan-open-wall-ends-layer.tsx +290 -0
  88. package/src/components/editor-2d/floorplan-registered-tool-layer.tsx +2 -2
  89. package/src/components/editor-2d/floorplan-registry-action-menu.tsx +11 -1
  90. package/src/components/editor-2d/floorplan-registry-move-overlay.tsx +116 -106
  91. package/src/components/editor-2d/renderers/floorplan-dimension-edit-overlay.test.ts +156 -0
  92. package/src/components/editor-2d/renderers/floorplan-dimension-edit-overlay.tsx +278 -0
  93. package/src/components/editor-2d/renderers/floorplan-dimension-renderer.tsx +77 -0
  94. package/src/components/editor-2d/renderers/{floorplan-draft-wall-measurement.tsx → floorplan-draft-measurement.tsx} +8 -8
  95. package/src/components/editor-2d/renderers/floorplan-geometry-renderer.tsx +4 -1
  96. package/src/components/editor-2d/renderers/floorplan-registry-layer.test.ts +38 -8
  97. package/src/components/editor-2d/renderers/floorplan-registry-layer.tsx +319 -233
  98. package/src/components/editor-2d/renderers/floorplan-reveal.test.tsx +254 -0
  99. package/src/components/editor-2d/renderers/floorplan-reveal.ts +141 -0
  100. package/src/components/editor-2d/svg-paths.ts +12 -15
  101. package/src/components/point-ask/bubble.tsx +538 -0
  102. package/src/components/point-ask/hover-chip.tsx +91 -0
  103. package/src/components/point-ask/mode-pill.tsx +92 -0
  104. package/src/components/point-ask/optional-motion.ts +22 -0
  105. package/src/components/point-ask/outline-corners.tsx +93 -0
  106. package/src/components/point-ask/pins.tsx +711 -0
  107. package/src/components/point-ask/point-ask-layer.tsx +83 -0
  108. package/src/components/point-ask/point-ask-styles.ts +105 -0
  109. package/src/components/point-ask/region-marquee-styles.ts +15 -0
  110. package/src/components/point-ask/region-marquee.tsx +64 -0
  111. package/src/components/procedural-items/inspector.tsx +40 -15
  112. package/src/components/systems/ceiling/ceiling-bracket-batch.test.ts +12 -2
  113. package/src/components/systems/ceiling/ceiling-selection-affordance-system.test.ts +97 -14
  114. package/src/components/systems/ceiling/ceiling-selection-affordance-system.tsx +18 -18
  115. package/src/components/systems/roof/roof-edit-system.tsx +1 -0
  116. package/src/components/systems/zone/zone-system.test.ts +47 -0
  117. package/src/components/systems/zone/zone-system.tsx +29 -68
  118. package/src/components/tools/distribution/connection-compatibility.test.ts +34 -0
  119. package/src/components/tools/distribution/connection-compatibility.ts +43 -0
  120. package/src/components/tools/distribution/distribution-public.ts +9 -0
  121. package/src/components/tools/distribution/distribution-run-contract.test.ts +26 -0
  122. package/src/components/tools/distribution/distribution-run-contract.ts +94 -0
  123. package/src/components/tools/distribution/distribution-run-tool.test.ts +241 -0
  124. package/src/components/tools/distribution/distribution-run-tool.test.tsx +125 -0
  125. package/src/components/tools/distribution/distribution-run-tool.tsx +1323 -0
  126. package/src/components/tools/distribution/draw-alignment.ts +35 -0
  127. package/src/components/tools/distribution/ports.test.ts +56 -0
  128. package/src/components/tools/distribution/ports.ts +378 -0
  129. package/src/components/tools/distribution/run-cursor.test.ts +135 -0
  130. package/src/components/tools/distribution/run-cursor.ts +36 -0
  131. package/src/components/tools/distribution/run-direction-feedback.test.ts +60 -0
  132. package/src/components/tools/distribution/run-direction-feedback.tsx +251 -0
  133. package/src/components/tools/distribution/run-port-snap.test.ts +79 -0
  134. package/src/components/tools/distribution/run-port-snap.ts +25 -0
  135. package/src/components/tools/distribution/run-screen-direction.test.ts +23 -0
  136. package/src/components/tools/distribution/run-screen-direction.ts +30 -0
  137. package/src/components/tools/elevator/elevator-tool.tsx +3 -6
  138. package/src/components/tools/elevator/move-elevator-tool.tsx +2 -3
  139. package/src/components/tools/item/item-surface-strategy-resolver.test.ts +67 -68
  140. package/src/components/tools/item/placement-history.test.tsx +512 -0
  141. package/src/components/tools/item/placement-strategies.test.ts +46 -1
  142. package/src/components/tools/item/placement-strategies.ts +11 -10
  143. package/src/components/tools/item/shelf-surface-strategy-resolver.test.ts +56 -56
  144. package/src/components/tools/item/use-draft-node.test.tsx +432 -1
  145. package/src/components/tools/item/use-draft-node.ts +166 -95
  146. package/src/components/tools/item/use-placement-coordinator.tsx +148 -95
  147. package/src/components/tools/paint-region/use-paint-tint.ts +37 -0
  148. package/src/components/tools/paint-region/wall-paint-region-tool.tsx +466 -0
  149. package/src/components/tools/registry/item-surface-move.test.ts +62 -63
  150. package/src/components/tools/registry/item-surface-move.ts +2 -0
  151. package/src/components/tools/registry/item-surface-resolver.test.ts +26 -26
  152. package/src/components/tools/registry/move-registry-node-tool.test.ts +268 -1
  153. package/src/components/tools/registry/move-registry-node-tool.tsx +66 -30
  154. package/src/components/tools/select/box-select-tool.tsx +2 -12
  155. package/src/components/tools/select/plane-box-select-tool.test.tsx +79 -0
  156. package/src/components/tools/select/plane-box-select-tool.tsx +35 -22
  157. package/src/components/tools/select/select-candidates.test.ts +37 -4
  158. package/src/components/tools/select/select-candidates.ts +15 -11
  159. package/src/components/tools/shared/cursor-sphere.tsx +36 -12
  160. package/src/components/tools/site/site-boundary-editor.tsx +1 -2
  161. package/src/components/tools/tool-manager.tsx +59 -24
  162. package/src/components/tools/wall/wall-drafting.test.ts +175 -47
  163. package/src/components/tools/zone/zone-tool.tsx +4 -12
  164. package/src/components/ui/action-menu/action-button.tsx +9 -1
  165. package/src/components/ui/action-menu/camera-actions.tsx +9 -4
  166. package/src/components/ui/action-menu/control-modes.tsx +15 -42
  167. package/src/components/ui/action-menu/follow-pascal-pill.tsx +32 -0
  168. package/src/components/ui/action-menu/follow-pascal-toggle.tsx +36 -0
  169. package/src/components/ui/action-menu/index.tsx +56 -33
  170. package/src/components/ui/action-menu/measurement-control.tsx +14 -19
  171. package/src/components/ui/action-menu/placement.ts +20 -0
  172. package/src/components/ui/action-menu/point-ask-button.tsx +52 -0
  173. package/src/components/ui/action-menu/view-toggles.tsx +47 -42
  174. package/src/components/ui/build-panel/build-panel.tsx +242 -0
  175. package/src/components/ui/command-palette/editor-commands.tsx +7 -42
  176. package/src/components/ui/command-palette/index.tsx +50 -39
  177. package/src/components/ui/command-palette/ranked-groups.test.ts +31 -0
  178. package/src/components/ui/controls/action-button.tsx +5 -2
  179. package/src/components/ui/controls/finish-swatch.tsx +110 -0
  180. package/src/components/ui/controls/material-paint-panel.tsx +97 -26
  181. package/src/components/ui/controls/material-picker.tsx +4 -1
  182. package/src/components/ui/controls/metric-control.tsx +1 -1
  183. package/src/components/ui/controls/scene-material-list.tsx +4 -32
  184. package/src/components/ui/controls/segmented-control.tsx +21 -8
  185. package/src/components/ui/controls/select-control.tsx +65 -0
  186. package/src/components/ui/controls/slider-control.tsx +1 -1
  187. package/src/components/ui/controls/toggle-control.tsx +3 -3
  188. package/src/components/ui/controls/tool-options-panel.tsx +1 -1
  189. package/src/components/ui/floating-level-selector.tsx +159 -59
  190. package/src/components/ui/helpers/building-helper.tsx +4 -1
  191. package/src/components/ui/helpers/contextual-helper-panel.tsx +181 -72
  192. package/src/components/ui/helpers/helper-manager.test.tsx +121 -0
  193. package/src/components/ui/helpers/helper-manager.tsx +331 -14
  194. package/src/components/ui/helpers/item-helper.tsx +17 -1
  195. package/src/components/ui/helpers/registered-tool-helper.tsx +8 -1
  196. package/src/components/ui/icon-ref.tsx +45 -1
  197. package/src/components/ui/level-duplicate-dialog.tsx +1 -1
  198. package/src/components/ui/node-icon-image.tsx +16 -0
  199. package/src/components/ui/panels/collections/collections-popover.tsx +28 -22
  200. package/src/components/ui/panels/floor-foundation-panel.tsx +551 -0
  201. package/src/components/ui/panels/floor-opening-panel.tsx +143 -0
  202. package/src/components/ui/panels/mobile-panel-sheet.tsx +4 -4
  203. package/src/components/ui/panels/mobile-selection-bar.tsx +8 -5
  204. package/src/components/ui/panels/multi-field-value.test.ts +26 -3
  205. package/src/components/ui/panels/multi-field-value.ts +6 -3
  206. package/src/components/ui/panels/multi-parametric-inspector.tsx +52 -2
  207. package/src/components/ui/panels/multi-selection-panel.tsx +161 -5
  208. package/src/components/ui/panels/node-display.ts +8 -3
  209. package/src/components/ui/panels/panel-manager.tsx +103 -68
  210. package/src/components/ui/panels/panel-wrapper.tsx +130 -62
  211. package/src/components/ui/panels/parametric-field-control.tsx +3 -3
  212. package/src/components/ui/panels/parametric-inspector.tsx +7 -7
  213. package/src/components/ui/panels/reference-panel.tsx +2 -3
  214. package/src/components/ui/panels/room-construction-rows.tsx +774 -0
  215. package/src/components/ui/panels/room-name-options.test.ts +55 -0
  216. package/src/components/ui/panels/room-name-options.ts +47 -0
  217. package/src/components/ui/panels/room-openings-field.tsx +110 -0
  218. package/src/components/ui/panels/room-panel.tsx +790 -0
  219. package/src/components/ui/panels/shape-choice.tsx +58 -0
  220. package/src/components/ui/panels/wall-region-list.tsx +109 -0
  221. package/src/components/ui/primitives/badge.tsx +8 -0
  222. package/src/components/ui/primitives/color-dot.tsx +36 -24
  223. package/src/components/ui/primitives/sidebar.tsx +3 -1
  224. package/src/components/ui/right-stack.test.ts +29 -0
  225. package/src/components/ui/right-stack.tsx +164 -0
  226. package/src/components/ui/sidebar/app-sidebar.tsx +1 -1
  227. package/src/components/ui/sidebar/icon-rail.tsx +8 -3
  228. package/src/components/ui/sidebar/panels/items-panel/function-tree-panel.tsx +1 -1
  229. package/src/components/ui/sidebar/panels/items-panel/index.tsx +6 -6
  230. package/src/components/ui/sidebar/panels/settings-panel/animation-speed-setting.tsx +40 -0
  231. package/src/components/ui/sidebar/panels/settings-panel/audio-settings-dialog.tsx +7 -8
  232. package/src/components/ui/sidebar/panels/settings-panel/construction-animation-setting.test.tsx +170 -0
  233. package/src/components/ui/sidebar/panels/settings-panel/construction-animation-setting.tsx +92 -0
  234. package/src/components/ui/sidebar/panels/settings-panel/index.tsx +138 -27
  235. package/src/components/ui/sidebar/panels/settings-panel/interface-theme.test.tsx +20 -0
  236. package/src/components/ui/sidebar/panels/settings-panel/keyboard-shortcuts-dialog.tsx +5 -5
  237. package/src/components/ui/sidebar/panels/settings-panel/print-export-button.tsx +4 -10
  238. package/src/components/ui/sidebar/panels/site-panel/building-tree-node.tsx +3 -11
  239. package/src/components/ui/sidebar/panels/site-panel/ceiling-tree-node.tsx +4 -4
  240. package/src/components/ui/sidebar/panels/site-panel/chimney-tree-node.tsx +1 -5
  241. package/src/components/ui/sidebar/panels/site-panel/collection-tree-node.tsx +197 -0
  242. package/src/components/ui/sidebar/panels/site-panel/column-tree-node.tsx +1 -5
  243. package/src/components/ui/sidebar/panels/site-panel/door-tree-node.tsx +1 -5
  244. package/src/components/ui/sidebar/panels/site-panel/dormer-tree-node.tsx +1 -5
  245. package/src/components/ui/sidebar/panels/site-panel/elevator-tree-node.tsx +1 -5
  246. package/src/components/ui/sidebar/panels/site-panel/fence-tree-node.tsx +24 -11
  247. package/src/components/ui/sidebar/panels/site-panel/gutter-tree-node.tsx +1 -5
  248. package/src/components/ui/sidebar/panels/site-panel/index.tsx +181 -223
  249. package/src/components/ui/sidebar/panels/site-panel/inline-rename-input.tsx +30 -8
  250. package/src/components/ui/sidebar/panels/site-panel/item-tree-node.tsx +1 -3
  251. package/src/components/ui/sidebar/panels/site-panel/level-tree-node.tsx +4 -1
  252. package/src/components/ui/sidebar/panels/site-panel/registry-tree-node.tsx +1 -3
  253. package/src/components/ui/sidebar/panels/site-panel/roof-tree-node.tsx +1 -4
  254. package/src/components/ui/sidebar/panels/site-panel/shelf-tree-node.tsx +1 -3
  255. package/src/components/ui/sidebar/panels/site-panel/slab-tree-node.tsx +21 -13
  256. package/src/components/ui/sidebar/panels/site-panel/solar-panel-tree-node.tsx +1 -5
  257. package/src/components/ui/sidebar/panels/site-panel/spawn-tree-node.tsx +1 -5
  258. package/src/components/ui/sidebar/panels/site-panel/stair-tree-node.tsx +1 -5
  259. package/src/components/ui/sidebar/panels/site-panel/tree-node.tsx +13 -37
  260. package/src/components/ui/sidebar/panels/site-panel/unit-tree-node.tsx +2 -3
  261. package/src/components/ui/sidebar/panels/site-panel/wall-tree-node.tsx +1 -5
  262. package/src/components/ui/sidebar/panels/site-panel/window-tree-node.tsx +1 -5
  263. package/src/components/ui/sidebar/panels/site-panel/zone-tree-node.tsx +9 -4
  264. package/src/components/ui/sidebar/panels/zone-panel/index.tsx +2 -4
  265. package/src/components/ui/sidebar/tab-bar.tsx +14 -4
  266. package/src/components/ui/sidebar/use-plugin-panels.tsx +15 -5
  267. package/src/components/viewer/floorplan-compass-button.tsx +9 -1
  268. package/src/components/viewer/floorplan-preview.tsx +5 -6
  269. package/src/components/viewer/viewer-controls-bar.tsx +18 -6
  270. package/src/components/viewer/viewer-hover-label.tsx +34 -0
  271. package/src/components/viewer/viewer-rooms.tsx +191 -0
  272. package/src/components/viewer/viewer-scene-header.tsx +181 -145
  273. package/src/components/viewer/viewer-selection-manager.tsx +124 -0
  274. package/src/components/viewer/viewer-stage.tsx +2 -2
  275. package/src/components/viewer-zone-system.tsx +3 -8
  276. package/src/hooks/use-auto-save.integration.test.tsx +215 -43
  277. package/src/hooks/use-auto-save.ts +60 -9
  278. package/src/hooks/use-ceiling-events.test.ts +123 -118
  279. package/src/hooks/use-grid-events.test.ts +105 -105
  280. package/src/hooks/use-keyboard.test.ts +243 -19
  281. package/src/hooks/use-keyboard.ts +118 -83
  282. package/src/hooks/use-open-wall-ends.test.ts +99 -0
  283. package/src/hooks/use-open-wall-ends.ts +238 -0
  284. package/src/hooks/use-placement-notice.test.ts +71 -0
  285. package/src/hooks/use-placement-notice.ts +73 -0
  286. package/src/hooks/use-selected-room.ts +143 -0
  287. package/src/index.tsx +312 -58
  288. package/src/lib/__fixtures__/owner-victor-seattle-parking.json +1 -0
  289. package/src/lib/__fixtures__/owner-villa-aurelia-basement.json +1 -0
  290. package/src/lib/add-storey.test.ts +177 -0
  291. package/src/lib/add-storey.ts +59 -0
  292. package/src/lib/below-grade.test.tsx +171 -0
  293. package/src/lib/below-grade.ts +133 -0
  294. package/src/lib/camera-facing.test.ts +24 -0
  295. package/src/lib/camera-facing.ts +24 -0
  296. package/src/lib/ceiling-edit-session.test.ts +321 -0
  297. package/src/lib/ceiling-edit-session.ts +206 -0
  298. package/src/lib/ceiling-region-session.test.ts +179 -0
  299. package/src/lib/continuation.test.ts +19 -1
  300. package/src/lib/continuation.ts +25 -4
  301. package/src/lib/direct-manipulation.ts +2 -0
  302. package/src/lib/door-interaction.ts +14 -0
  303. package/src/lib/download-project-file.ts +17 -0
  304. package/src/lib/editor-level-display.test.ts +96 -0
  305. package/src/lib/editor-level-display.ts +55 -0
  306. package/src/lib/editor-views.ts +178 -0
  307. package/src/lib/export-geometry-wait.test.ts +90 -0
  308. package/src/lib/export-texture-utils.ts +2 -4
  309. package/src/lib/floor-edit-session.ts +153 -0
  310. package/src/lib/floor-footprints.test.ts +563 -0
  311. package/src/lib/floor-footprints.ts +395 -0
  312. package/src/lib/floor-foundation-support.test.ts +284 -0
  313. package/src/lib/floor-opening-draft.ts +218 -0
  314. package/src/lib/floor-openings.test.ts +216 -0
  315. package/src/lib/floor-region-geometry.ts +191 -0
  316. package/src/lib/floor-region-session.test.ts +485 -0
  317. package/src/lib/floor-region-session.ts +589 -0
  318. package/src/lib/floor-region-snap.ts +101 -0
  319. package/src/lib/floor-step-hover.test.ts +32 -0
  320. package/src/lib/floorplan/annotation-visibility.ts +17 -0
  321. package/src/lib/floorplan/dimension-drive.test.ts +360 -0
  322. package/src/lib/floorplan/dimension-drive.ts +479 -0
  323. package/src/lib/floorplan/floorplan-export.test.ts +12 -8
  324. package/src/lib/floorplan/floorplan-export.tsx +32 -8
  325. package/src/lib/floorplan/floorplan-extension.ts +32 -0
  326. package/src/lib/floorplan/floorplan-mode.test.ts +4 -0
  327. package/src/lib/floorplan/floorplan-mode.ts +2 -0
  328. package/src/lib/floorplan/floorplan-pdfkit-document.ts +363 -27
  329. package/src/lib/floorplan/floorplan-pdfkit-renderer.test.ts +177 -8
  330. package/src/lib/floorplan/floorplan-pdfkit-renderer.ts +60 -25
  331. package/src/lib/floorplan/floorplan-plugin-install.test.tsx +138 -0
  332. package/src/lib/floorplan/floorplan-readonly.ts +6 -0
  333. package/src/lib/floorplan/geometry.ts +13 -6
  334. package/src/lib/floorplan/index.ts +0 -9
  335. package/src/lib/floorplan/items.ts +5 -2
  336. package/src/lib/floorplan/marks.test.ts +213 -0
  337. package/src/lib/floorplan/marks.ts +238 -0
  338. package/src/lib/floorplan/open-wall-ends.test.ts +268 -0
  339. package/src/lib/floorplan/open-wall-ends.ts +162 -0
  340. package/src/lib/floorplan/pdf-fonts/GeistMono-Bold.ttf +0 -0
  341. package/src/lib/floorplan/pdf-fonts/GeistMono-Regular.ttf +0 -0
  342. package/src/lib/floorplan/pdf-fonts/LICENSE-GeistMono.txt +92 -0
  343. package/src/lib/floorplan/pdf-fonts/LICENSE-LiberationSans.txt +102 -0
  344. package/src/lib/floorplan/pdf-fonts/LiberationSans-Bold.ttf +0 -0
  345. package/src/lib/floorplan/pdf-fonts/LiberationSans-Regular.ttf +0 -0
  346. package/src/lib/floorplan/pdfkit-standard-fonts.d.ts +5 -0
  347. package/src/lib/floorplan/schedules.test.ts +189 -0
  348. package/src/lib/floorplan/schedules.ts +339 -0
  349. package/src/lib/floorplan/sheet-export.test.ts +124 -0
  350. package/src/lib/floorplan/sheet-export.ts +318 -0
  351. package/src/lib/floorplan/site-plan/build-site-plan-drawing.ts +768 -0
  352. package/src/lib/floorplan/site-plan/contours.test.ts +43 -0
  353. package/src/lib/floorplan/site-plan/contributors.ts +97 -0
  354. package/src/lib/floorplan/site-plan/coverage.ts +230 -0
  355. package/src/lib/floorplan/site-plan/drawing-type-switch.test.ts +15 -0
  356. package/src/lib/floorplan/site-plan/drawing-type-switch.tsx +86 -0
  357. package/src/lib/floorplan/site-plan/front-edge.test.ts +103 -0
  358. package/src/lib/floorplan/site-plan/front-edge.ts +157 -0
  359. package/src/lib/floorplan/site-plan/geometry.test.ts +269 -0
  360. package/src/lib/floorplan/site-plan/geometry.ts +142 -0
  361. package/src/lib/floorplan/site-plan/index.ts +55 -0
  362. package/src/lib/floorplan/site-plan/site-annotations.test.ts +483 -0
  363. package/src/lib/floorplan/site-plan/site-annotations.ts +1084 -0
  364. package/src/lib/floorplan/site-plan/site-parts.ts +287 -0
  365. package/src/lib/floorplan/site-plan/site-plan-layer.tsx +138 -0
  366. package/src/lib/floorplan/site-plan/street-name.test.ts +69 -0
  367. package/src/lib/floorplan/site-plan/street-name.ts +302 -0
  368. package/src/lib/floorplan/site-plan/yards.test.ts +70 -0
  369. package/src/lib/floorplan/types.ts +1 -27
  370. package/src/lib/follow-pascal-exterior.test-fixture.ts +17 -0
  371. package/src/lib/follow-pascal-exterior.test.ts +171 -0
  372. package/src/lib/follow-pascal-exterior.ts +39 -0
  373. package/src/lib/follow-pascal-presentation.test.ts +203 -0
  374. package/src/lib/follow-pascal-presentation.ts +129 -0
  375. package/src/lib/follow-pascal-sight.ts +84 -0
  376. package/src/lib/follow-pascal-targets.test.ts +406 -0
  377. package/src/lib/follow-pascal-targets.ts +198 -0
  378. package/src/lib/follow-pascal.test.ts +350 -0
  379. package/src/lib/follow-pascal.ts +284 -0
  380. package/src/lib/fresh-planar-placement.test.ts +148 -0
  381. package/src/lib/fresh-planar-placement.ts +114 -37
  382. package/src/lib/geometry-script/author.ts +157 -0
  383. package/src/lib/geometry-script/client.ts +159 -0
  384. package/src/lib/geometry-script/finish-scripts.ts +127 -0
  385. package/src/lib/geometry-script/protocol.ts +13 -0
  386. package/src/lib/gesture-lifecycle.test.ts +595 -0
  387. package/src/lib/gesture-lifecycle.ts +290 -0
  388. package/src/lib/glb-export-zone-holes.test.ts +41 -0
  389. package/src/lib/glb-export.test.ts +636 -126
  390. package/src/lib/glb-export.ts +241 -120
  391. package/src/lib/history.test.ts +18 -7
  392. package/src/lib/history.ts +7 -0
  393. package/src/lib/hud-title.ts +109 -0
  394. package/src/lib/hydration-gated-storage.ts +49 -0
  395. package/src/lib/ifc-export.test.ts +174 -0
  396. package/src/lib/ifc-export.ts +201 -0
  397. package/src/lib/ifc-parts.ts +15 -0
  398. package/src/lib/inspector-expanded.test.ts +44 -0
  399. package/src/lib/inspector-expanded.ts +36 -0
  400. package/src/lib/interaction/scope.ts +22 -0
  401. package/src/lib/keyboard-pan.test.ts +129 -7
  402. package/src/lib/keyboard-pan.ts +161 -12
  403. package/src/lib/level-selection.test.ts +57 -0
  404. package/src/lib/level-selection.ts +16 -1
  405. package/src/lib/live-gesture-round-trip.test.ts +360 -0
  406. package/src/lib/lot/__fixtures__/map-dossier-miami-shores.json +1 -0
  407. package/src/lib/lot/__fixtures__/map-dossier-stpete.json +1 -0
  408. package/src/lib/lot/clean-ring.test.ts +187 -0
  409. package/src/lib/lot/clean-ring.ts +217 -0
  410. package/src/lib/lot/dossier.test.ts +202 -0
  411. package/src/lib/lot/dossier.ts +498 -0
  412. package/src/lib/lot/drop-in.test.ts +326 -0
  413. package/src/lib/lot/drop-in.ts +392 -0
  414. package/src/lib/lot/index.ts +43 -0
  415. package/src/lib/lot/lot-patch.test.ts +340 -0
  416. package/src/lib/lot/lot-patch.ts +394 -0
  417. package/src/lib/lot/parcel-provider.ts +43 -0
  418. package/src/lib/lot/terrain.test.ts +161 -0
  419. package/src/lib/lot/terrain.ts +266 -0
  420. package/src/lib/material-catalog-model.ts +3 -1
  421. package/src/lib/material-paint-panel-model.ts +9 -5
  422. package/src/lib/material-paint.ts +4 -0
  423. package/src/lib/measurement-kind.test.ts +6 -8
  424. package/src/lib/measurements.test.ts +23 -0
  425. package/src/lib/mezzanine-actions.test.ts +342 -0
  426. package/src/lib/mezzanine-draft.test.ts +501 -0
  427. package/src/lib/mezzanine-draft.ts +278 -0
  428. package/src/lib/mezzanine-messages.ts +26 -0
  429. package/src/lib/mezzanine-selection.test.ts +207 -0
  430. package/src/lib/mezzanine-stairs.ts +46 -0
  431. package/src/lib/model-export.ts +3 -1
  432. package/src/lib/node-action-movement.ts +17 -6
  433. package/src/lib/node-display-label.ts +37 -0
  434. package/src/lib/orbit-floor-clearance.test.ts +201 -0
  435. package/src/lib/orbit-floor-clearance.ts +154 -0
  436. package/src/lib/paint-commit-route.test.ts +37 -0
  437. package/src/lib/paint-commit-route.ts +17 -0
  438. package/src/lib/paint-eyedropper.test.tsx +165 -0
  439. package/src/lib/paint-eyedropper.ts +99 -0
  440. package/src/lib/paint-pass-through.test.ts +122 -0
  441. package/src/lib/paint-pass-through.ts +78 -0
  442. package/src/lib/paint-region-hover.ts +25 -0
  443. package/src/lib/paint-region-mode.ts +154 -0
  444. package/src/lib/paint-regions.test.ts +166 -0
  445. package/src/lib/paint-regions.ts +192 -0
  446. package/src/lib/paint-scope.test.ts +317 -35
  447. package/src/lib/paint-scope.ts +267 -25
  448. package/src/lib/panel-rows.ts +21 -0
  449. package/src/lib/parametric-node-update.ts +16 -7
  450. package/src/lib/plate-paint-affected.test.ts +300 -0
  451. package/src/lib/plate-paint-affected.ts +177 -0
  452. package/src/lib/plugin-chat.ts +65 -0
  453. package/src/lib/plugin-panels.test.ts +25 -0
  454. package/src/lib/plugin-panels.ts +94 -0
  455. package/src/lib/point-ask/__fixtures__/consumer-browser.tsx +76 -0
  456. package/src/lib/point-ask/__fixtures__/house.ts +81 -0
  457. package/src/lib/point-ask/ask-created.test.ts +195 -0
  458. package/src/lib/point-ask/ask-flow.test.ts +380 -0
  459. package/src/lib/point-ask/build-context.test.ts +173 -0
  460. package/src/lib/point-ask/build-context.ts +94 -0
  461. package/src/lib/point-ask/capture-crop.ts +129 -0
  462. package/src/lib/point-ask/choreography.ts +69 -0
  463. package/src/lib/point-ask/compare.test.ts +145 -0
  464. package/src/lib/point-ask/compare.ts +73 -0
  465. package/src/lib/point-ask/describe-target.test.ts +165 -0
  466. package/src/lib/point-ask/describe-target.ts +234 -0
  467. package/src/lib/point-ask/dictation.ts +79 -0
  468. package/src/lib/point-ask/edge-marker.test.ts +61 -0
  469. package/src/lib/point-ask/edge-marker.ts +27 -0
  470. package/src/lib/point-ask/frame-loop.ts +25 -0
  471. package/src/lib/point-ask/history-marks.test.ts +94 -0
  472. package/src/lib/point-ask/history-marks.ts +39 -0
  473. package/src/lib/point-ask/host-api.ts +32 -0
  474. package/src/lib/point-ask/outline-box.test.ts +30 -0
  475. package/src/lib/point-ask/outline-box.ts +11 -0
  476. package/src/lib/point-ask/overlay-metrics.ts +6 -0
  477. package/src/lib/point-ask/pick.test.ts +220 -0
  478. package/src/lib/point-ask/pick.ts +121 -0
  479. package/src/lib/point-ask/pin-machine.test.ts +494 -0
  480. package/src/lib/point-ask/pin-machine.ts +217 -0
  481. package/src/lib/point-ask/placement.test.ts +203 -0
  482. package/src/lib/point-ask/placement.ts +141 -0
  483. package/src/lib/point-ask/pointer-tracker.ts +34 -0
  484. package/src/lib/point-ask/projector.ts +83 -0
  485. package/src/lib/point-ask/region.test.ts +157 -0
  486. package/src/lib/point-ask/region.ts +136 -0
  487. package/src/lib/point-ask/retry.test.ts +217 -0
  488. package/src/lib/point-ask/retry.ts +37 -0
  489. package/src/lib/point-ask/scene-commit.test.ts +312 -0
  490. package/src/lib/point-ask/scene-commit.ts +32 -0
  491. package/src/lib/point-ask/session.ts +1391 -0
  492. package/src/lib/point-ask/spring-keyframes.test.ts +45 -0
  493. package/src/lib/point-ask/spring-keyframes.ts +38 -0
  494. package/src/lib/point-ask/spring.test.ts +63 -0
  495. package/src/lib/point-ask/spring.ts +107 -0
  496. package/src/lib/point-ask/suggestions.test.ts +80 -0
  497. package/src/lib/point-ask/suggestions.ts +125 -0
  498. package/src/lib/point-ask/target-scope.test.ts +622 -0
  499. package/src/lib/point-ask/target-scope.ts +338 -0
  500. package/src/lib/point-ask/types.ts +48 -0
  501. package/src/lib/portable-export.test.ts +34 -1
  502. package/src/lib/portable-export.ts +119 -53
  503. package/src/lib/print-feature-thickness.ts +10 -1
  504. package/src/lib/print-roof-solids.test.ts +39 -1
  505. package/src/lib/print-roof-solids.ts +43 -10
  506. package/src/lib/print-shell-compiler-manifold-core.ts +1 -1
  507. package/src/lib/print-shell-compiler.ts +1 -0
  508. package/src/lib/registered-draft-snap.ts +12 -0
  509. package/src/lib/roof-duplication.ts +10 -2
  510. package/src/lib/room-assembly-overlay.test.ts +275 -0
  511. package/src/lib/room-assembly-overlay.ts +265 -0
  512. package/src/lib/room-built-on.test.ts +379 -0
  513. package/src/lib/room-built-on.ts +138 -0
  514. package/src/lib/room-construction-commands.ts +712 -0
  515. package/src/lib/room-construction.test.ts +746 -0
  516. package/src/lib/room-construction.ts +385 -0
  517. package/src/lib/room-controls-phase6.test.ts +630 -0
  518. package/src/lib/room-divide-session.ts +523 -0
  519. package/src/lib/room-handle-drag.ts +1068 -0
  520. package/src/lib/room-name-catalog.test.ts +90 -0
  521. package/src/lib/room-name-catalog.ts +168 -0
  522. package/src/lib/room-push-dimensions.test.ts +311 -0
  523. package/src/lib/room-push-dimensions.ts +251 -0
  524. package/src/lib/room-selection-commands.ts +62 -0
  525. package/src/lib/room-selection.test.ts +1017 -0
  526. package/src/lib/room-selection.ts +417 -0
  527. package/src/lib/room-structure-commands.ts +94 -0
  528. package/src/lib/room-structure.test.ts +921 -0
  529. package/src/lib/room-transform-session.ts +570 -0
  530. package/src/lib/room-zone-routing.test.ts +294 -0
  531. package/src/lib/room-zone-routing.ts +197 -0
  532. package/src/lib/scene-clipboard.test.ts +610 -1
  533. package/src/lib/scene-clipboard.ts +269 -58
  534. package/src/lib/scene-material-refs.test.ts +79 -0
  535. package/src/lib/scene-material-refs.ts +79 -0
  536. package/src/lib/scene.test.ts +24 -36
  537. package/src/lib/scene.ts +33 -57
  538. package/src/lib/selection-routing.test.ts +129 -45
  539. package/src/lib/selection-routing.ts +54 -75
  540. package/src/lib/session-writes.test.ts +120 -0
  541. package/src/lib/session-writes.ts +205 -0
  542. package/src/lib/sfx-bus.ts +13 -0
  543. package/src/lib/sfx-player.test.ts +70 -9
  544. package/src/lib/sfx-player.ts +6 -0
  545. package/src/lib/sidebar-panel.ts +62 -0
  546. package/src/lib/site-boundary.test.ts +4 -6
  547. package/src/lib/site-boundary.ts +1 -1
  548. package/src/lib/snapping-mode.test.ts +4 -0
  549. package/src/lib/snapping-mode.ts +8 -1
  550. package/src/lib/stair-levels.test.ts +25 -0
  551. package/src/lib/stair-levels.ts +22 -3
  552. package/src/lib/terrace-draft.test.ts +145 -0
  553. package/src/lib/terrace-draft.ts +214 -0
  554. package/src/lib/tool-cursor-icon.test.ts +16 -0
  555. package/src/lib/tool-cursor-icon.ts +11 -0
  556. package/src/lib/typing-target.ts +9 -0
  557. package/src/lib/units.ts +77 -39
  558. package/src/lib/vector-edges.test.ts +58 -0
  559. package/src/lib/vector-edges.ts +400 -0
  560. package/src/lib/viewer-selection.test.ts +577 -0
  561. package/src/lib/viewer-selection.ts +580 -0
  562. package/src/lib/wall-draw-variant.test.ts +71 -0
  563. package/src/lib/wall-draw-variant.ts +91 -0
  564. package/src/lib/wall-paint-region-session.ts +246 -0
  565. package/src/lib/wall-paint-region.test.ts +373 -0
  566. package/src/lib/wall-placement-pointer.test.ts +42 -0
  567. package/src/lib/wall-placement-pointer.ts +57 -0
  568. package/src/lib/wall-polygon-draft.test.ts +262 -0
  569. package/src/lib/wall-polygon-draft.ts +286 -0
  570. package/src/lib/wall-push.test.ts +453 -0
  571. package/src/lib/wall-region-face.ts +352 -0
  572. package/src/lib/wall-region-handles.test.ts +304 -0
  573. package/src/lib/wall-region-handles.ts +368 -0
  574. package/src/lib/wall-region-list.test.ts +119 -0
  575. package/src/lib/wall-region-list.ts +136 -0
  576. package/src/lib/wall-region-snap.ts +110 -0
  577. package/src/lib/window-changes.ts +35 -0
  578. package/src/lib/window-interaction.ts +14 -0
  579. package/src/lib/zone-content.ts +4 -1
  580. package/src/lib/zone-name.test.ts +17 -0
  581. package/src/lib/zone-name.ts +17 -0
  582. package/src/store/opens-in-select-mode.test.ts +223 -0
  583. package/src/store/paint-mode.test.ts +188 -0
  584. package/src/store/terrain-sculpt-mode.test.ts +19 -21
  585. package/src/store/tool-mode.test.ts +137 -6
  586. package/src/store/use-audio.test.ts +146 -0
  587. package/src/store/use-audio.tsx +127 -3
  588. package/src/store/use-construction-reveal.test.ts +173 -0
  589. package/src/store/use-construction-reveal.ts +145 -0
  590. package/src/store/use-corner-snap-hint.test.ts +24 -0
  591. package/src/store/use-corner-snap-hint.ts +47 -0
  592. package/src/store/use-delete-confirmation.ts +28 -1
  593. package/src/store/use-drawing-view.test.ts +24 -3
  594. package/src/store/use-drawing-view.ts +29 -1
  595. package/src/store/use-editor.tsx +432 -321
  596. package/src/store/use-floorplan-draft-preview.ts +13 -0
  597. package/src/store/use-floorplan-marquee.ts +12 -2
  598. package/src/store/use-follow-pascal.test.ts +62 -0
  599. package/src/store/use-follow-pascal.ts +53 -0
  600. package/src/store/use-point-ask.ts +247 -0
  601. package/src/store/use-quick-measurement-hud.ts +3 -3
  602. package/src/store/use-stair-build-preview.ts +33 -5
  603. package/src/store/use-wall-move-ghosts.ts +4 -2
  604. package/src/store/use-wall-region-handles-selection.test.ts +222 -0
  605. package/src/store/use-wall-region-handles-selection.ts +139 -0
  606. package/src/store/view-layout.test.ts +201 -0
  607. package/src/store/view-layout.ts +199 -0
  608. package/src/test-utils/immediate-animation-frames.ts +16 -0
  609. package/src/test-utils/live-gesture-round-trip.ts +224 -0
  610. package/src/test-utils/render-current-markup.tsx +16 -0
  611. package/src/test-utils/selection-harness.ts +82 -0
  612. package/src/components/editor-2d/renderers/floorplan-stair-layer.tsx +0 -499
  613. package/src/components/systems/zone/zone-label-editor-system.tsx +0 -333
  614. package/src/components/tools/fence/fence-drafting.test.ts +0 -100
  615. package/src/components/tools/fence/fence-drafting.ts +0 -266
  616. package/src/components/tools/stair/stair-click-guard.test.ts +0 -58
  617. package/src/components/tools/stair/stair-click-guard.ts +0 -72
  618. package/src/components/tools/stair/stair-defaults.ts +0 -18
  619. package/src/components/tools/stair/stair-tool.tsx +0 -634
  620. package/src/components/ui/sidebar/panels/plugins-panel.tsx +0 -218
  621. package/src/components/viewer/viewer-units-panel.tsx +0 -116
  622. package/src/lib/floorplan/stairs.ts +0 -489
  623. package/src/lib/level-duplication.test.ts +0 -144
  624. package/src/lib/level-duplication.ts +0 -195
@@ -1,15 +1,26 @@
1
1
  'use client'
2
2
 
3
3
  import {
4
+ type AnyNode,
4
5
  type AnyNodeId,
5
6
  type CameraControlEvent,
6
7
  type CameraControlFitSceneEvent,
8
+ type CameraControlFrameEvent,
7
9
  type CameraPose,
10
+ deepestDrawnY,
8
11
  emitter,
12
+ groundHeightAt,
13
+ lowestGroundY,
9
14
  sceneRegistry,
10
15
  useScene,
11
16
  } from '@pascal-app/core'
12
- import { GRID_LAYER, getLevelPresentationY, useViewer, ZONE_LAYER } from '@pascal-app/viewer'
17
+ import {
18
+ GRID_LAYER,
19
+ getLevelPresentationY,
20
+ useSceneGroundReplacement,
21
+ useViewer,
22
+ ZONE_LAYER,
23
+ } from '@pascal-app/viewer'
13
24
  import { CameraControls, CameraControlsImpl } from '@react-three/drei'
14
25
  import { useFrame, useThree } from '@react-three/fiber'
15
26
  import { useCallback, useEffect, useLayoutEffect, useMemo, useRef } from 'react'
@@ -33,24 +44,37 @@ import {
33
44
  import { EDITOR_LAYER } from '../../lib/constants'
34
45
  import {
35
46
  acceptsKeyboardPan,
47
+ advanceKeyboardPanMotion,
36
48
  clearKeyboardPanKeys,
49
+ createKeyboardPanMotion,
37
50
  hasKeyboardPanInput,
38
51
  isEditableKeyboardTarget,
39
52
  isKeyboardPanKey,
40
53
  type KeyboardPanState,
41
- keyboardPanDirection,
42
54
  keyboardPanSpeed,
43
55
  setKeyboardPanKey,
56
+ syncKeyboardPanMotion,
44
57
  } from '../../lib/keyboard-pan'
58
+ import {
59
+ diveFloorY,
60
+ MAX_ORBIT_POLAR_ANGLE,
61
+ ORBIT_TARGET_HEIGHT,
62
+ orbitFloorMinY,
63
+ overviewEyeY,
64
+ } from '../../lib/orbit-floor-clearance'
45
65
  import { editorOwnsOneFingerDrag } from '../../lib/touch-gesture-priority'
66
+ import { frameViewerCamera } from '../../lib/viewer-selection'
46
67
  import { publishCameraPose } from '../../store/camera-pose-store'
47
- import useEditor from '../../store/use-editor'
68
+ import useEditor, { takePreviewCameraRestore } from '../../store/use-editor'
48
69
  import {
49
70
  useActiveHandleDrag,
50
71
  useEndpointReshape,
51
72
  useMovingNode,
52
73
  } from '../../store/use-interaction-scope'
74
+ import { visibleScene } from '../../store/view-layout'
53
75
  import { createCameraDraggingLifecycle } from './camera-dragging-lifecycle'
76
+ import { FloorAwareCameraControls } from './floor-aware-camera-controls'
77
+ import { FloorSurface } from './floor-surface'
54
78
 
55
79
  const currentTarget = new Vector3()
56
80
  const tempBox = new Box3()
@@ -62,11 +86,49 @@ const tempTarget = new Vector3()
62
86
  const transitionFreezePosition = new Vector3()
63
87
  const transitionFreezeTarget = new Vector3()
64
88
  const keyboardPanSpherical = new Spherical()
89
+ /** Mouse/touch orbit speed (camera-controls default 1): slower for precise looks. */
90
+ const ORBIT_ROTATE_SPEED = 0.6
65
91
  // In 2D-only view the canvas is paused, so the floor plan drives WASD, orbit and
66
92
  // top view itself (`floorplan-panel.tsx`) and the camera stands down.
67
- const planOwnsNavigation = () => useEditor.getState().viewMode === '2d'
93
+ const planOwnsNavigation = () => visibleScene(useEditor.getState()) === '2d'
68
94
  const DEFAULT_MAX_POLAR_ANGLE = Math.PI / 2 - 0.1
69
- const DEBUG_MAX_POLAR_ANGLE = Math.PI - 0.05
95
+ // Focusing one of these aims at its floor (raised to the orbit's pivot
96
+ // height), not at the middle of its bounding box.
97
+ const GROUND_FOCUS_TYPES = new Set(['site', 'building', 'level', 'zone', 'slab'])
98
+
99
+ type LevelMode = ReturnType<typeof useViewer.getState>['levelMode']
100
+
101
+ /** Where the orbit pivot rests on a level: its floor plus the pivot height. */
102
+ function orbitGroundY(
103
+ levelId: string | null | undefined,
104
+ levelMode: LevelMode = useViewer.getState().levelMode,
105
+ ) {
106
+ const floorY = levelId ? getLevelPresentationY(levelId, useScene.getState().nodes, levelMode) : 0
107
+ return floorY + ORBIT_TARGET_HEIGHT
108
+ }
109
+
110
+ /**
111
+ * `eyeY` for an overview eye at (x, z) whose pivot is being sent to another level: kept over the
112
+ * ground it was over (a deep level shown is looked at through the hole, not from under the lid),
113
+ * unless the person has dived.
114
+ */
115
+ function keepEyeOverGround(x: number, eyeY: number, z: number, previousEyeY = eyeY): number {
116
+ const { nodes } = useScene.getState()
117
+ const siteId = sceneRegistry.byType.site?.values().next().value as AnyNodeId | undefined
118
+ const groundY = groundHeightAt(siteId ? nodes[siteId] : null, x, z)
119
+ return overviewEyeY(eyeY, previousEyeY, groundY, useViewer.getState().eyeUnderGround)
120
+ }
121
+
122
+ function levelOf(nodeId: string): string | null {
123
+ const nodes = useScene.getState().nodes
124
+ let node: AnyNode | undefined = nodes[nodeId as AnyNodeId]
125
+ while (node) {
126
+ if (node.type === 'level') return node.id
127
+ node = node.parentId ? nodes[node.parentId as AnyNodeId] : undefined
128
+ }
129
+ return null
130
+ }
131
+
70
132
  type CameraMode = ReturnType<typeof useViewer.getState>['cameraMode']
71
133
  type CameraPoseSnapshot = {
72
134
  mode: CameraMode
@@ -311,7 +373,7 @@ function useFirstPersonCameraPoseRestore(
311
373
  }
312
374
 
313
375
  export const CustomCameraControls = ({ paused = false }: { paused?: boolean }) => {
314
- const controls = useRef<CameraControlsImpl | null>(null)
376
+ const controls = useRef<FloorAwareCameraControls | null>(null)
315
377
  const pendingAppliedPose = useRef<CameraPoseApplicationPlan | null>(null)
316
378
  const activePoseInterpolation = useRef<{
317
379
  camera: Camera
@@ -325,6 +387,7 @@ export const CustomCameraControls = ({ paused = false }: { paused?: boolean }) =
325
387
  left: false,
326
388
  right: false,
327
389
  })
390
+ const keyboardPanMotion = useRef(createKeyboardPanMotion())
328
391
  const isPreviewMode = useEditor((s) => s.isPreviewMode)
329
392
  const isFirstPersonMode = useEditor((s) => s.isFirstPersonMode)
330
393
  const allowUndergroundCamera = useEditor((s) => s.allowUndergroundCamera)
@@ -337,12 +400,40 @@ export const CustomCameraControls = ({ paused = false }: { paused?: boolean }) =
337
400
  isFirstPersonMode,
338
401
  cameraMode,
339
402
  )
340
- const currentLevelId = selection.levelId
403
+ // Selecting the building or site clears the level; the camera keeps
404
+ // standing on the last level rather than dropping to the ground floor.
405
+ const lastLevelId = useRef<string | null>(null)
406
+ if (selection.levelId) lastLevelId.current = selection.levelId
407
+ const currentLevelId = lastLevelId.current
341
408
  const firstLoad = useRef(true)
409
+ const floorY = useScene((state) =>
410
+ currentLevelId ? getLevelPresentationY(currentLevelId, state.nodes, levelMode) : 0,
411
+ )
412
+ const allowUnderground = !isPreviewMode && allowUndergroundCamera
413
+ // With the ground on Auto the eye may dive under any building, down to the dive's floor; Solid
414
+ // keeps the eye over the ground.
415
+ const groundMode = useViewer((state) => state.groundMode)
416
+ const diveFloor = useScene((state) =>
417
+ groundMode === 'auto'
418
+ ? diveFloorY(lowestGroundY(state.nodes), deepestDrawnY(state.nodes))
419
+ : null,
420
+ )
421
+ const floorMinY = allowUnderground ? null : orbitFloorMinY(floorY, diveFloor)
422
+ // The viewer mounts the eye's fill light before the first dive, so crossing the ground compiles nothing.
423
+ const mayDive = allowUnderground || diveFloor !== null
424
+ useEffect(() => {
425
+ useViewer.setState({ diveAvailable: mayDive })
426
+ return () => {
427
+ useViewer.setState({ diveAvailable: false })
428
+ }
429
+ }, [mayDive])
342
430
  const maxPolarAngle =
343
- !isPreviewMode && allowUndergroundCamera ? DEBUG_MAX_POLAR_ANGLE : DEFAULT_MAX_POLAR_ANGLE
431
+ allowUnderground || cameraMode === 'perspective'
432
+ ? MAX_ORBIT_POLAR_ANGLE
433
+ : DEFAULT_MAX_POLAR_ANGLE
344
434
 
345
435
  const camera = useThree((state) => state.camera)
436
+ const scene = useThree((state) => state.scene)
346
437
  const gl = useThree((state) => state.gl)
347
438
  const raycaster = useThree((state) => state.raycaster)
348
439
  const viewportSize = useThree((state) => state.size)
@@ -381,7 +472,10 @@ export const CustomCameraControls = ({ paused = false }: { paused?: boolean }) =
381
472
  const beginLocalCameraInteraction = useCallback(
382
473
  ({ dragging = true }: { dragging?: boolean } = {}) => {
383
474
  cancelPoseApplication()
384
- if (dragging) cameraDraggingLifecycle.begin()
475
+ if (dragging) {
476
+ controls.current?.releaseWrittenPose()
477
+ cameraDraggingLifecycle.begin()
478
+ }
385
479
  emitter.emit('camera-controls:interaction-start', undefined)
386
480
  },
387
481
  [cameraDraggingLifecycle, cancelPoseApplication],
@@ -469,11 +563,21 @@ export const CustomCameraControls = ({ paused = false }: { paused?: boolean }) =
469
563
 
470
564
  emitter.on('camera-controls:apply-pose', handleAppliedPose)
471
565
  emitter.on('camera-controls:cancel-pose', cancelPoseApplication)
566
+ if (!isPreviewMode) {
567
+ const restore = takePreviewCameraRestore()
568
+ if (restore) handleAppliedPose(restore)
569
+ }
472
570
  return () => {
473
571
  emitter.off('camera-controls:apply-pose', handleAppliedPose)
474
572
  emitter.off('camera-controls:cancel-pose', cancelPoseApplication)
475
573
  }
476
- }, [applyPendingPose, cancelPoseApplication, freezeActivePoseInterpolation, isFirstPersonMode])
574
+ }, [
575
+ applyPendingPose,
576
+ cancelPoseApplication,
577
+ freezeActivePoseInterpolation,
578
+ isFirstPersonMode,
579
+ isPreviewMode,
580
+ ])
477
581
 
478
582
  useEffect(() => cancelPoseApplication, [cancelPoseApplication])
479
583
 
@@ -484,33 +588,87 @@ export const CustomCameraControls = ({ paused = false }: { paused?: boolean }) =
484
588
  if (process.env.NODE_ENV !== 'development') return
485
589
  const w = window as typeof window & {
486
590
  __pascalCameraControls?: (() => CameraControlsImpl | null) | null
591
+ __pascalScene?: (() => unknown) | null
592
+ __pascalCapture?: (() => Promise<string>) | null
487
593
  }
488
594
  w.__pascalCameraControls = () => controls.current
595
+ // the live three.js scene, for the same tooling (a probe can walk the
596
+ // meshes for an empty geometry the WebGPU validator complains about)
597
+ w.__pascalScene = () => scene
598
+ // a fresh frame read back as a PNG data URL: the canvas is cleared after
599
+ // each presented frame, so a capture has to render and read in one task
600
+ w.__pascalCapture = async () => {
601
+ const r = gl as unknown as {
602
+ renderAsync?: (s: unknown, c: unknown) => Promise<void>
603
+ render: (s: unknown, c: unknown) => void
604
+ domElement: HTMLCanvasElement
605
+ }
606
+ if (r.renderAsync) await r.renderAsync(scene, camera)
607
+ else r.render(scene, camera)
608
+ return r.domElement.toDataURL('image/png')
609
+ }
489
610
  return () => {
490
611
  w.__pascalCameraControls = null
612
+ w.__pascalScene = null
613
+ w.__pascalCapture = null
491
614
  }
492
- }, [])
615
+ }, [scene, gl, camera])
493
616
 
617
+ // `camera` too: drei builds a fresh controls instance (pivot at the origin)
618
+ // when the default camera mounts or changes; it is rested on the floor too.
619
+ // biome-ignore lint/correctness/useExhaustiveDependencies: camera re-runs this on a new instance
494
620
  useEffect(() => {
495
621
  if (isPreviewMode || isFirstPersonMode || isRestoringFirstPersonPose()) return
496
622
  // Analytic destination, not `sceneRegistry` mesh position: a level created
497
623
  // this frame still sits at y=0 (LevelSystem lerps it later), and a mode
498
624
  // switch leaves every level mid-lerp — the camera must pan to where the
499
625
  // level will settle, in the CURRENT presentation mode.
500
- const targetY = currentLevelId
501
- ? getLevelPresentationY(currentLevelId, useScene.getState().nodes, levelMode)
502
- : 0
626
+ const targetY = orbitGroundY(currentLevelId, levelMode)
503
627
  if (!controls.current) return
504
628
  if (firstLoad.current) {
505
629
  firstLoad.current = false
506
- controls.current.setLookAt(20, 20, 20, 0, 0, 0, true)
630
+ controls.current.setLookAt(
631
+ 20,
632
+ keepEyeOverGround(20, 20 + targetY, 20, 20),
633
+ 20,
634
+ 0,
635
+ targetY,
636
+ 0,
637
+ true,
638
+ )
507
639
  }
508
- controls.current.getTarget(currentTarget)
640
+ controls.current.getOrbitTarget(currentTarget)
509
641
  // Idempotence guard: skip when already there — also swallows the thumbnail
510
642
  // generator's synchronous stacked→restore levelMode round-trip.
511
643
  if (Math.abs(currentTarget.y - targetY) < 1e-3) return
644
+ // The pivot goes down with the level; the eye follows it unless that would sink it under the
645
+ // ground (a basement four storeys down): then it stays over the ground, looking down.
646
+ controls.current.getPosition(tempPosition, false)
647
+ const movedEyeY = tempPosition.y + targetY - currentTarget.y
648
+ const eyeY = isPerspectiveCamera(camera)
649
+ ? keepEyeOverGround(tempPosition.x, movedEyeY, tempPosition.z, tempPosition.y)
650
+ : movedEyeY
651
+ if (eyeY > movedEyeY) {
652
+ controls.current.setLookAt(
653
+ tempPosition.x,
654
+ eyeY,
655
+ tempPosition.z,
656
+ currentTarget.x,
657
+ targetY,
658
+ currentTarget.z,
659
+ true,
660
+ )
661
+ return
662
+ }
512
663
  controls.current.moveTo(currentTarget.x, targetY, currentTarget.z, true)
513
- }, [currentLevelId, levelMode, isPreviewMode, isFirstPersonMode, isRestoringFirstPersonPose])
664
+ }, [
665
+ camera,
666
+ currentLevelId,
667
+ levelMode,
668
+ isPreviewMode,
669
+ isFirstPersonMode,
670
+ isRestoringFirstPersonPose,
671
+ ])
514
672
 
515
673
  useEffect(() => {
516
674
  if (isFirstPersonMode || !controls.current) return
@@ -523,6 +681,16 @@ export const CustomCameraControls = ({ paused = false }: { paused?: boolean }) =
523
681
  }
524
682
  }, [isFirstPersonMode, maxPolarAngle])
525
683
 
684
+ const floorSurface = useMemo(() => new FloorSurface(), [])
685
+ // biome-ignore lint/correctness/useExhaustiveDependencies: drei builds a new controls instance per camera
686
+ useLayoutEffect(() => {
687
+ floorSurface.levelId = currentLevelId ?? null
688
+ if (!controls.current) return
689
+ controls.current.floorMinY = floorMinY
690
+ // The level's floor and the ground hold the eye over them; the dive goes through both.
691
+ controls.current.floorSurface = diveFloor === null ? floorSurface : null
692
+ }, [camera, currentLevelId, diveFloor, floorMinY, floorSurface, isFirstPersonMode])
693
+
526
694
  const focusNode = useCallback(
527
695
  (nodeId: string) => {
528
696
  if (isPreviewMode || isFirstPersonMode || !controls.current) return
@@ -530,17 +698,38 @@ export const CustomCameraControls = ({ paused = false }: { paused?: boolean }) =
530
698
  const object3D = sceneRegistry.nodes.get(nodeId)
531
699
  if (!object3D) return
532
700
 
701
+ const node = useScene.getState().nodes[nodeId as AnyNodeId]
702
+ const groundFocus = node !== undefined && GROUND_FOCUS_TYPES.has(node.type)
533
703
  tempBox.setFromObject(object3D)
534
- if (tempBox.isEmpty()) return
535
-
536
- tempBox.getCenter(tempCenter)
704
+ if (!tempBox.isEmpty()) {
705
+ tempBox.getCenter(tempCenter)
706
+ } else if (groundFocus && node.type === 'zone' && node.polygon.length > 0) {
707
+ // A room draws nothing measurable in some display modes: aim at the
708
+ // middle of its outline instead.
709
+ tempBox.makeEmpty()
710
+ for (const [x, z] of node.polygon) tempBox.expandByPoint(tempCenter.set(x, 0, z))
711
+ tempBox.getCenter(tempCenter)
712
+ object3D.updateWorldMatrix(true, false)
713
+ tempCenter.applyMatrix4(object3D.matrixWorld)
714
+ } else {
715
+ return
716
+ }
717
+ if (groundFocus) {
718
+ tempCenter.y = orbitGroundY(levelOf(nodeId) ?? lastLevelId.current)
719
+ }
537
720
  controls.current.getPosition(tempPosition)
538
721
  controls.current.getTarget(tempTarget)
539
722
  tempDelta.copy(tempCenter).sub(tempTarget)
540
723
 
724
+ const focusedX = tempPosition.x + tempDelta.x
725
+ const focusedZ = tempPosition.z + tempDelta.z
726
+ const focusedY = isPerspectiveCamera(camera)
727
+ ? keepEyeOverGround(focusedX, tempPosition.y + tempDelta.y, focusedZ, tempPosition.y)
728
+ : tempPosition.y + tempDelta.y
729
+ const floorMinY = isPerspectiveCamera(camera) ? controls.current.floorLimitY : null
541
730
  controls.current.setLookAt(
542
731
  tempPosition.x + tempDelta.x,
543
- tempPosition.y + tempDelta.y,
732
+ floorMinY === null ? focusedY : Math.max(focusedY, floorMinY),
544
733
  tempPosition.z + tempDelta.z,
545
734
  tempCenter.x,
546
735
  tempCenter.y,
@@ -548,7 +737,7 @@ export const CustomCameraControls = ({ paused = false }: { paused?: boolean }) =
548
737
  true,
549
738
  )
550
739
  },
551
- [isPreviewMode, isFirstPersonMode],
740
+ [camera, isPreviewMode, isFirstPersonMode],
552
741
  )
553
742
 
554
743
  const publishCurrentPose = useCallback(() => {
@@ -630,18 +819,18 @@ export const CustomCameraControls = ({ paused = false }: { paused?: boolean }) =
630
819
  }
631
820
  }
632
821
 
633
- const { horizontal, vertical } = keyboardPanDirection(keyboardPanKeys.current)
634
- if (horizontal === 0 && vertical === 0) return
822
+ const motion = keyboardPanMotion.current
823
+ if (!advanceKeyboardPanMotion(motion, keyboardPanKeys.current, performance.now())) return
635
824
 
636
825
  const control = controls.current
637
826
 
638
827
  control.getSpherical(keyboardPanSpherical, false)
639
828
  const viewWidth = getCameraViewWidth(camera, keyboardPanSpherical.radius, viewportSize)
640
829
  const speed = keyboardPanSpeed(viewWidth)
641
- const step = (speed * Math.min(delta, 0.05)) / Math.hypot(horizontal, vertical)
642
830
 
643
- if (horizontal !== 0) control.truck(horizontal * step, 0, true)
644
- if (vertical !== 0) control.forward(vertical * step, true)
831
+ // The motion already eases; the controls' own smoothing would add a glide on top.
832
+ if (motion.stepX !== 0) control.truck(motion.stepX * speed, 0, false)
833
+ if (motion.stepY !== 0) control.forward(motion.stepY * speed, false)
645
834
  }, 0)
646
835
 
647
836
  // Configure mouse buttons based on control mode and camera mode
@@ -652,13 +841,14 @@ export const CustomCameraControls = ({ paused = false }: { paused?: boolean }) =
652
841
  ? CameraControlsImpl.ACTION.ZOOM
653
842
  : CameraControlsImpl.ACTION.DOLLY
654
843
 
844
+ // Left click selects, in Preview as in the published viewer.
655
845
  return {
656
- left: isPreviewMode ? CameraControlsImpl.ACTION.SCREEN_PAN : CameraControlsImpl.ACTION.NONE,
846
+ left: CameraControlsImpl.ACTION.NONE,
657
847
  middle: CameraControlsImpl.ACTION.SCREEN_PAN,
658
848
  right: CameraControlsImpl.ACTION.ROTATE,
659
849
  wheel: wheelAction,
660
850
  }
661
- }, [cameraMode, isPreviewMode])
851
+ }, [cameraMode])
662
852
 
663
853
  // Touch gestures (mobile / trackpad).
664
854
  // - One finger drag → rotate by default (much easier on a phone), but
@@ -774,10 +964,7 @@ export const CustomCameraControls = ({ paused = false }: { paused?: boolean }) =
774
964
  controls.current.mouseButtons.wheel = wheelAction
775
965
  controls.current.mouseButtons.middle = CameraControlsImpl.ACTION.SCREEN_PAN
776
966
  controls.current.mouseButtons.right = CameraControlsImpl.ACTION.ROTATE
777
- if (isPreviewMode) {
778
- // In preview mode, left-click is always pan (viewer-style)
779
- controls.current.mouseButtons.left = CameraControlsImpl.ACTION.SCREEN_PAN
780
- } else if (space) {
967
+ if (space) {
781
968
  controls.current.mouseButtons.left = CameraControlsImpl.ACTION.SCREEN_PAN
782
969
  } else {
783
970
  controls.current.mouseButtons.left = CameraControlsImpl.ACTION.NONE
@@ -787,6 +974,11 @@ export const CustomCameraControls = ({ paused = false }: { paused?: boolean }) =
787
974
  const onKeyDown = (event: KeyboardEvent) => {
788
975
  if (isKeyboardPanKey(event.code)) {
789
976
  if (acceptsKeyboardPan(event) && !planOwnsNavigation()) {
977
+ syncKeyboardPanMotion(
978
+ keyboardPanMotion.current,
979
+ keyboardPanKeys.current,
980
+ performance.now(),
981
+ )
790
982
  const changed = setKeyboardPanKey(keyboardPanKeys.current, event.code, true)
791
983
  if (changed) beginLocalCameraInteraction()
792
984
  event.preventDefault()
@@ -818,6 +1010,7 @@ export const CustomCameraControls = ({ paused = false }: { paused?: boolean }) =
818
1010
 
819
1011
  const onKeyUp = (event: KeyboardEvent) => {
820
1012
  if (isKeyboardPanKey(event.code)) {
1013
+ syncKeyboardPanMotion(keyboardPanMotion.current, keyboardPanKeys.current, performance.now())
821
1014
  const changed = setKeyboardPanKey(keyboardPanKeys.current, event.code, false)
822
1015
  if (changed) {
823
1016
  if (!hasKeyboardPanInput(keyboardPanKeys.current)) {
@@ -907,14 +1100,7 @@ export const CustomCameraControls = ({ paused = false }: { paused?: boolean }) =
907
1100
  clearNavigationCursor()
908
1101
  cameraDraggingLifecycle.end()
909
1102
  }
910
- }, [
911
- beginLocalCameraInteraction,
912
- cameraDraggingLifecycle,
913
- cameraMode,
914
- gl,
915
- isPreviewMode,
916
- isFirstPersonMode,
917
- ])
1103
+ }, [beginLocalCameraInteraction, cameraDraggingLifecycle, cameraMode, gl, isFirstPersonMode])
918
1104
 
919
1105
  // `controlstart` fires only for user pointer interactions. Pointerdowns
920
1106
  // mapped to ACTION.NONE must not flag the camera as dragging because no
@@ -927,18 +1113,18 @@ export const CustomCameraControls = ({ paused = false }: { paused?: boolean }) =
927
1113
  })
928
1114
  }, [beginLocalCameraInteraction])
929
1115
 
930
- // Preview mode: auto-navigate camera to selected node (viewer behavior)
931
- const previewTargetNodeId = isPreviewMode
932
- ? (selection.zoneId ?? selection.levelId ?? selection.buildingId)
933
- : null
934
-
1116
+ // Preview opens on the editor's floor (or building, or site) framed the
1117
+ // viewer's way; afterwards only the viewer navigation moves the camera
1118
+ // (`camera-controls:frame`), never a canvas selection.
935
1119
  useEffect(() => {
936
1120
  if (!(isPreviewMode && controls.current) || isFirstPersonMode) return
937
1121
 
938
1122
  const nodes = useScene.getState().nodes
939
- let node = previewTargetNodeId ? nodes[previewTargetNodeId] : null
1123
+ const { selection: opening } = useViewer.getState()
1124
+ const openingNodeId = opening.levelId ?? opening.buildingId
1125
+ let node = openingNodeId ? nodes[openingNodeId as AnyNodeId] : null
940
1126
 
941
- if (!previewTargetNodeId) {
1127
+ if (!openingNodeId) {
942
1128
  const site = Object.values(nodes).find((n) => n.type === 'site')
943
1129
  node = site || null
944
1130
  }
@@ -971,10 +1157,10 @@ export const CustomCameraControls = ({ paused = false }: { paused?: boolean }) =
971
1157
  return
972
1158
  }
973
1159
 
974
- if (!previewTargetNodeId) return
1160
+ if (!openingNodeId) return
975
1161
 
976
1162
  // Calculate camera position from bounding box
977
- const object3D = sceneRegistry.nodes.get(previewTargetNodeId)
1163
+ const object3D = sceneRegistry.nodes.get(openingNodeId)
978
1164
  if (!object3D) return
979
1165
 
980
1166
  tempBox.setFromObject(object3D)
@@ -993,7 +1179,7 @@ export const CustomCameraControls = ({ paused = false }: { paused?: boolean }) =
993
1179
  tempCenter.z,
994
1180
  true,
995
1181
  )
996
- }, [isPreviewMode, isFirstPersonMode, previewTargetNodeId])
1182
+ }, [isPreviewMode, isFirstPersonMode])
997
1183
 
998
1184
  // Preset capture auto-framing — when `setCaptureMode({ mode: 'preset',
999
1185
  // isolated })` fires, fly the camera to a pose that fits the union
@@ -1159,7 +1345,7 @@ export const CustomCameraControls = ({ paused = false }: { paused?: boolean }) =
1159
1345
  if (isFirstPersonMode || !controls.current || planOwnsNavigation()) return
1160
1346
 
1161
1347
  const currentAzimuth = controls.current.azimuthAngle
1162
- const currentPolar = controls.current.polarAngle
1348
+ const currentPolar = controls.current.orbitPolarAngle
1163
1349
  // Round to nearest 90° increment, then rotate 90° clockwise
1164
1350
  const rounded = Math.round(currentAzimuth / (Math.PI / 2)) * (Math.PI / 2)
1165
1351
  const target = rounded - Math.PI / 2
@@ -1171,7 +1357,7 @@ export const CustomCameraControls = ({ paused = false }: { paused?: boolean }) =
1171
1357
  if (isFirstPersonMode || !controls.current || planOwnsNavigation()) return
1172
1358
 
1173
1359
  const currentAzimuth = controls.current.azimuthAngle
1174
- const currentPolar = controls.current.polarAngle
1360
+ const currentPolar = controls.current.orbitPolarAngle
1175
1361
  // Round to nearest 90° increment, then rotate 90° counter-clockwise
1176
1362
  const rounded = Math.round(currentAzimuth / (Math.PI / 2)) * (Math.PI / 2)
1177
1363
  const target = rounded + Math.PI / 2
@@ -1183,11 +1369,26 @@ export const CustomCameraControls = ({ paused = false }: { paused?: boolean }) =
1183
1369
  focusNode(nodeId)
1184
1370
  }
1185
1371
 
1372
+ const handleFrame = (event: CameraControlFrameEvent) => {
1373
+ if (isFirstPersonMode || !controls.current) return
1374
+ cancelPoseApplication()
1375
+ frameViewerCamera(controls.current, camera, event)
1376
+ }
1377
+
1186
1378
  const handleFitScene = ({ bounds }: CameraControlFitSceneEvent) => {
1187
1379
  if (isFirstPersonMode || !controls.current || isPreviewMode) return
1380
+ const groundY = orbitGroundY(lastLevelId.current)
1188
1381
  if (!bounds) {
1189
1382
  // Restore default framing pose when no bounds were computed.
1190
- controls.current.setLookAt(20, 20, 20, 0, 0, 0, true)
1383
+ controls.current.setLookAt(
1384
+ 20,
1385
+ keepEyeOverGround(20, 20 + groundY, 20, 20),
1386
+ 20,
1387
+ 0,
1388
+ groundY,
1389
+ 0,
1390
+ true,
1391
+ )
1191
1392
  return
1192
1393
  }
1193
1394
  const [cx, cz] = bounds.center
@@ -1197,11 +1398,20 @@ export const CustomCameraControls = ({ paused = false }: { paused?: boolean }) =
1197
1398
  const maxExtent = Math.max(w, d)
1198
1399
  const distance = Math.max(maxExtent * 1.4, 15)
1199
1400
  const height = Math.max(maxExtent * 0.8, 10)
1200
- controls.current.setLookAt(cx + distance * 0.7, height, cz + distance * 0.7, cx, 0, cz, true)
1401
+ controls.current.setLookAt(
1402
+ cx + distance * 0.7,
1403
+ keepEyeOverGround(cx + distance * 0.7, height + groundY, cz + distance * 0.7, height),
1404
+ cz + distance * 0.7,
1405
+ cx,
1406
+ groundY,
1407
+ cz,
1408
+ true,
1409
+ )
1201
1410
  }
1202
1411
 
1203
1412
  emitter.on('camera-controls:capture', handleNodeCapture)
1204
1413
  emitter.on('camera-controls:focus', handleNodeFocus)
1414
+ emitter.on('camera-controls:frame', handleFrame)
1205
1415
  emitter.on('camera-controls:view', handleNodeView)
1206
1416
  emitter.on('camera-controls:top-view', handleTopView)
1207
1417
  emitter.on('camera-controls:orbit-cw', handleOrbitCW)
@@ -1211,13 +1421,14 @@ export const CustomCameraControls = ({ paused = false }: { paused?: boolean }) =
1211
1421
  return () => {
1212
1422
  emitter.off('camera-controls:capture', handleNodeCapture)
1213
1423
  emitter.off('camera-controls:focus', handleNodeFocus)
1424
+ emitter.off('camera-controls:frame', handleFrame)
1214
1425
  emitter.off('camera-controls:view', handleNodeView)
1215
1426
  emitter.off('camera-controls:top-view', handleTopView)
1216
1427
  emitter.off('camera-controls:orbit-cw', handleOrbitCW)
1217
1428
  emitter.off('camera-controls:orbit-ccw', handleOrbitCCW)
1218
1429
  emitter.off('camera-controls:fit-scene', handleFitScene)
1219
1430
  }
1220
- }, [focusNode, isPreviewMode, isFirstPersonMode])
1431
+ }, [camera, cancelPoseApplication, focusNode, isPreviewMode, isFirstPersonMode])
1221
1432
 
1222
1433
  const onTransitionStart = useCallback(() => {
1223
1434
  cameraDraggingLifecycle.begin()
@@ -1248,6 +1459,10 @@ export const CustomCameraControls = ({ paused = false }: { paused?: boolean }) =
1248
1459
  // navigation guardrails.
1249
1460
  const isPresetCapture = captureMode.mode === 'preset'
1250
1461
  const minDistance = isPresetCapture ? 0.5 : 2
1462
+ // Over modeled or mapped surroundings (a neighbourhood instead of the
1463
+ // horizon disc) the camera may pull back far enough to take it all in.
1464
+ const groundReplaced = useSceneGroundReplacement()
1465
+ const maxDistance = groundReplaced ? 1500 : 100
1251
1466
 
1252
1467
  if (isFirstPersonMode) {
1253
1468
  return null
@@ -1255,8 +1470,10 @@ export const CustomCameraControls = ({ paused = false }: { paused?: boolean }) =
1255
1470
 
1256
1471
  return (
1257
1472
  <CameraControls
1473
+ azimuthRotateSpeed={ORBIT_ROTATE_SPEED}
1258
1474
  makeDefault
1259
- maxDistance={100}
1475
+ maxDistance={maxDistance}
1476
+ impl={FloorAwareCameraControls}
1260
1477
  maxPolarAngle={maxPolarAngle}
1261
1478
  minDistance={minDistance}
1262
1479
  minPolarAngle={0}
@@ -1265,6 +1482,7 @@ export const CustomCameraControls = ({ paused = false }: { paused?: boolean }) =
1265
1482
  onControlStart={handleControlStart}
1266
1483
  onUpdate={handleCameraUpdate}
1267
1484
  onRest={onRest}
1485
+ polarRotateSpeed={ORBIT_ROTATE_SPEED}
1268
1486
  onSleep={onRest}
1269
1487
  onTransitionStart={onTransitionStart}
1270
1488
  ref={controls}