@pascal-app/editor 1.0.3 → 1.0.4

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 (440) hide show
  1. package/package.json +11 -9
  2. package/src/components/editor/action-menu-button.tsx +83 -0
  3. package/src/components/editor/bake-thumbnail.tsx +315 -4
  4. package/src/components/editor/capture-level-picker.tsx +72 -0
  5. package/src/components/editor/custom-camera-controls.tsx +202 -57
  6. package/src/components/editor/delete-confirmation-dialog.tsx +297 -7
  7. package/src/components/editor/duplicate-node.ts +244 -0
  8. package/src/components/editor/editor-layout-mobile.tsx +36 -25
  9. package/src/components/editor/editor-layout-v2.tsx +131 -58
  10. package/src/components/editor/export-manager.tsx +34 -8
  11. package/src/components/editor/first-person/build-collider-world.test.ts +51 -0
  12. package/src/components/editor/first-person/build-collider-world.ts +6 -2
  13. package/src/components/editor/first-person/mechanism-targets.test.ts +75 -0
  14. package/src/components/editor/first-person/mechanism-targets.ts +36 -0
  15. package/src/components/editor/first-person-controls.tsx +205 -2
  16. package/src/components/editor/floating-action-menu.tsx +63 -207
  17. package/src/components/editor/floor-aware-camera-controls.test.ts +442 -0
  18. package/src/components/editor/floor-aware-camera-controls.ts +324 -0
  19. package/src/components/editor/floor-openings-overlay.tsx +243 -0
  20. package/src/components/editor/floor-region-controls.tsx +846 -0
  21. package/src/components/editor/floor-region-webgpu-materials.test.ts +12 -0
  22. package/src/components/editor/floor-surface.test.ts +165 -0
  23. package/src/components/editor/floor-surface.ts +182 -0
  24. package/src/components/editor/floorplan-background-selection.test.ts +11 -2
  25. package/src/components/editor/floorplan-background-selection.ts +1 -3
  26. package/src/components/editor/floorplan-panel.tsx +344 -342
  27. package/src/components/editor/footprint-height-handle.tsx +204 -0
  28. package/src/components/editor/group-actions.ts +54 -2
  29. package/src/components/editor/handles/handle-hit-priority.test.tsx +63 -0
  30. package/src/components/editor/handles/handle-hit-priority.tsx +37 -0
  31. package/src/components/editor/handles/swallow-next-click.test.ts +28 -0
  32. package/src/components/editor/handles/use-handle-drag.ts +269 -197
  33. package/src/components/editor/index.tsx +294 -169
  34. package/src/components/editor/join-walls-pill.tsx +44 -0
  35. package/src/components/editor/node-action-menu.tsx +68 -70
  36. package/src/components/editor/node-arrow-handles.tsx +25 -6
  37. package/src/components/editor/open-wall-ends-3d-layer.tsx +273 -0
  38. package/src/components/editor/open-wall-ends-hint.tsx +89 -0
  39. package/src/components/editor/quick-measurement-hud.tsx +3 -2
  40. package/src/components/editor/registry-action-contributions.test.tsx +63 -0
  41. package/src/components/editor/registry-action-contributions.tsx +80 -1
  42. package/src/components/editor/room-controls.tsx +847 -0
  43. package/src/components/editor/room-floor-highlight.tsx +128 -0
  44. package/src/components/editor/room-handles-2d.tsx +354 -0
  45. package/src/components/editor/room-handles.tsx +675 -0
  46. package/src/components/editor/room-highlight.tsx +225 -0
  47. package/src/components/editor/room-labels.test.ts +357 -0
  48. package/src/components/editor/room-labels.tsx +990 -0
  49. package/src/components/editor/room-transform-ghost.tsx +256 -0
  50. package/src/components/editor/room-webgpu-materials.test.ts +27 -0
  51. package/src/components/editor/selection-manager.tsx +451 -317
  52. package/src/components/editor/slab-hole-highlights.tsx +3 -0
  53. package/src/components/editor/snapshot-capture-overlay.tsx +4 -0
  54. package/src/components/editor/snapshot-capture.test.ts +52 -4
  55. package/src/components/editor/snapshot-capture.ts +30 -5
  56. package/src/components/editor/thumbnail-generator.test.ts +308 -0
  57. package/src/components/editor/thumbnail-generator.tsx +1035 -229
  58. package/src/components/editor/use-floorplan-background-placement.ts +31 -11
  59. package/src/components/editor/vector-edge-extractor.tsx +121 -0
  60. package/src/components/editor/views/host-view.tsx +61 -0
  61. package/src/components/editor/views/plugins-view.tsx +670 -0
  62. package/src/components/editor/views/scene-region.tsx +59 -0
  63. package/src/components/editor/views/use-editor-views.ts +57 -0
  64. package/src/components/editor/views/view-bar.tsx +220 -0
  65. package/src/components/editor/wall-justification.test.ts +140 -0
  66. package/src/components/editor/wall-move-side-handles.tsx +43 -142
  67. package/src/components/editor/wall-push-arrow.tsx +92 -0
  68. package/src/components/editor/wall-region-handles.tsx +509 -0
  69. package/src/components/editor/wall-snap-beacon-layer.tsx +3 -0
  70. package/src/components/editor-2d/floorplan-cursor-indicator-overlay.tsx +8 -4
  71. package/src/components/editor-2d/floorplan-open-wall-ends-layer.test.tsx +86 -0
  72. package/src/components/editor-2d/floorplan-open-wall-ends-layer.tsx +290 -0
  73. package/src/components/editor-2d/floorplan-registry-action-menu.tsx +11 -1
  74. package/src/components/editor-2d/floorplan-registry-move-overlay.tsx +113 -102
  75. package/src/components/editor-2d/renderers/floorplan-dimension-edit-overlay.test.ts +156 -0
  76. package/src/components/editor-2d/renderers/floorplan-dimension-edit-overlay.tsx +278 -0
  77. package/src/components/editor-2d/renderers/floorplan-dimension-renderer.tsx +77 -0
  78. package/src/components/editor-2d/renderers/{floorplan-draft-wall-measurement.tsx → floorplan-draft-measurement.tsx} +8 -8
  79. package/src/components/editor-2d/renderers/floorplan-geometry-renderer.tsx +4 -1
  80. package/src/components/editor-2d/renderers/floorplan-registry-layer.test.ts +38 -8
  81. package/src/components/editor-2d/renderers/floorplan-registry-layer.tsx +291 -227
  82. package/src/components/editor-2d/svg-paths.ts +12 -15
  83. package/src/components/procedural-items/inspector.tsx +40 -15
  84. package/src/components/systems/ceiling/ceiling-selection-affordance-system.test.ts +97 -14
  85. package/src/components/systems/ceiling/ceiling-selection-affordance-system.tsx +18 -18
  86. package/src/components/systems/zone/zone-system.test.ts +47 -0
  87. package/src/components/systems/zone/zone-system.tsx +29 -68
  88. package/src/components/tools/item/placement-history.test.tsx +474 -0
  89. package/src/components/tools/item/placement-strategies.ts +8 -4
  90. package/src/components/tools/item/use-draft-node.test.tsx +432 -1
  91. package/src/components/tools/item/use-draft-node.ts +166 -95
  92. package/src/components/tools/item/use-placement-coordinator.tsx +93 -46
  93. package/src/components/tools/paint-region/use-paint-tint.ts +37 -0
  94. package/src/components/tools/paint-region/wall-paint-region-tool.tsx +466 -0
  95. package/src/components/tools/registry/move-registry-node-tool.tsx +3 -1
  96. package/src/components/tools/select/box-select-tool.tsx +2 -12
  97. package/src/components/tools/select/plane-box-select-tool.test.tsx +79 -0
  98. package/src/components/tools/select/plane-box-select-tool.tsx +35 -22
  99. package/src/components/tools/select/select-candidates.test.ts +37 -4
  100. package/src/components/tools/select/select-candidates.ts +15 -11
  101. package/src/components/tools/shared/cursor-sphere.tsx +36 -12
  102. package/src/components/tools/site/site-boundary-editor.tsx +1 -2
  103. package/src/components/tools/stair/stair-defaults.ts +0 -3
  104. package/src/components/tools/stair/stair-tool.tsx +90 -42
  105. package/src/components/tools/tool-manager.tsx +46 -20
  106. package/src/components/tools/wall/wall-drafting.test.ts +19 -4
  107. package/src/components/tools/zone/zone-tool.tsx +4 -12
  108. package/src/components/ui/action-menu/action-button.tsx +3 -0
  109. package/src/components/ui/action-menu/camera-actions.tsx +2 -1
  110. package/src/components/ui/action-menu/control-modes.tsx +2 -35
  111. package/src/components/ui/action-menu/measurement-control.tsx +3 -10
  112. package/src/components/ui/build-panel/build-panel.tsx +235 -0
  113. package/src/components/ui/command-palette/editor-commands.tsx +4 -30
  114. package/src/components/ui/command-palette/index.tsx +5 -24
  115. package/src/components/ui/controls/finish-swatch.tsx +110 -0
  116. package/src/components/ui/controls/material-paint-panel.tsx +97 -26
  117. package/src/components/ui/controls/material-picker.tsx +4 -1
  118. package/src/components/ui/controls/scene-material-list.tsx +4 -32
  119. package/src/components/ui/controls/select-control.tsx +65 -0
  120. package/src/components/ui/floating-level-selector.tsx +9 -14
  121. package/src/components/ui/helpers/building-helper.tsx +4 -1
  122. package/src/components/ui/helpers/contextual-helper-panel.tsx +154 -70
  123. package/src/components/ui/helpers/helper-manager.test.tsx +69 -0
  124. package/src/components/ui/helpers/helper-manager.tsx +325 -14
  125. package/src/components/ui/helpers/item-helper.tsx +8 -0
  126. package/src/components/ui/helpers/registered-tool-helper.tsx +8 -1
  127. package/src/components/ui/icon-ref.tsx +45 -1
  128. package/src/components/ui/level-duplicate-dialog.tsx +1 -1
  129. package/src/components/ui/node-icon-image.tsx +16 -0
  130. package/src/components/ui/panels/collections/collections-popover.tsx +20 -14
  131. package/src/components/ui/panels/floor-foundation-panel.tsx +551 -0
  132. package/src/components/ui/panels/floor-opening-panel.tsx +143 -0
  133. package/src/components/ui/panels/mobile-panel-sheet.tsx +2 -2
  134. package/src/components/ui/panels/mobile-selection-bar.tsx +2 -2
  135. package/src/components/ui/panels/multi-field-value.test.ts +26 -3
  136. package/src/components/ui/panels/multi-field-value.ts +6 -3
  137. package/src/components/ui/panels/multi-parametric-inspector.tsx +52 -2
  138. package/src/components/ui/panels/multi-selection-panel.tsx +43 -4
  139. package/src/components/ui/panels/node-display.ts +8 -3
  140. package/src/components/ui/panels/panel-manager.tsx +103 -68
  141. package/src/components/ui/panels/panel-wrapper.tsx +78 -36
  142. package/src/components/ui/panels/parametric-field-control.tsx +1 -1
  143. package/src/components/ui/panels/parametric-inspector.tsx +4 -4
  144. package/src/components/ui/panels/reference-panel.tsx +2 -3
  145. package/src/components/ui/panels/room-construction-rows.tsx +774 -0
  146. package/src/components/ui/panels/room-name-options.test.ts +55 -0
  147. package/src/components/ui/panels/room-name-options.ts +47 -0
  148. package/src/components/ui/panels/room-openings-field.tsx +110 -0
  149. package/src/components/ui/panels/room-panel.tsx +725 -0
  150. package/src/components/ui/panels/shape-choice.tsx +58 -0
  151. package/src/components/ui/panels/wall-region-list.tsx +109 -0
  152. package/src/components/ui/primitives/color-dot.tsx +36 -24
  153. package/src/components/ui/primitives/sidebar.tsx +2 -0
  154. package/src/components/ui/right-stack.test.ts +29 -0
  155. package/src/components/ui/right-stack.tsx +145 -0
  156. package/src/components/ui/sidebar/icon-rail.tsx +5 -0
  157. package/src/components/ui/sidebar/panels/settings-panel/index.tsx +31 -4
  158. package/src/components/ui/sidebar/panels/settings-panel/keyboard-shortcuts-dialog.tsx +5 -5
  159. package/src/components/ui/sidebar/panels/site-panel/ceiling-tree-node.tsx +4 -4
  160. package/src/components/ui/sidebar/panels/site-panel/chimney-tree-node.tsx +1 -5
  161. package/src/components/ui/sidebar/panels/site-panel/collection-tree-node.tsx +197 -0
  162. package/src/components/ui/sidebar/panels/site-panel/column-tree-node.tsx +1 -5
  163. package/src/components/ui/sidebar/panels/site-panel/door-tree-node.tsx +1 -5
  164. package/src/components/ui/sidebar/panels/site-panel/dormer-tree-node.tsx +1 -5
  165. package/src/components/ui/sidebar/panels/site-panel/elevator-tree-node.tsx +1 -5
  166. package/src/components/ui/sidebar/panels/site-panel/fence-tree-node.tsx +24 -11
  167. package/src/components/ui/sidebar/panels/site-panel/gutter-tree-node.tsx +1 -5
  168. package/src/components/ui/sidebar/panels/site-panel/index.tsx +176 -211
  169. package/src/components/ui/sidebar/panels/site-panel/inline-rename-input.tsx +30 -8
  170. package/src/components/ui/sidebar/panels/site-panel/item-tree-node.tsx +1 -3
  171. package/src/components/ui/sidebar/panels/site-panel/level-tree-node.tsx +4 -1
  172. package/src/components/ui/sidebar/panels/site-panel/registry-tree-node.tsx +1 -3
  173. package/src/components/ui/sidebar/panels/site-panel/roof-tree-node.tsx +1 -4
  174. package/src/components/ui/sidebar/panels/site-panel/shelf-tree-node.tsx +1 -3
  175. package/src/components/ui/sidebar/panels/site-panel/slab-tree-node.tsx +21 -13
  176. package/src/components/ui/sidebar/panels/site-panel/solar-panel-tree-node.tsx +1 -5
  177. package/src/components/ui/sidebar/panels/site-panel/spawn-tree-node.tsx +1 -5
  178. package/src/components/ui/sidebar/panels/site-panel/stair-tree-node.tsx +1 -5
  179. package/src/components/ui/sidebar/panels/site-panel/tree-node.tsx +0 -27
  180. package/src/components/ui/sidebar/panels/site-panel/unit-tree-node.tsx +2 -3
  181. package/src/components/ui/sidebar/panels/site-panel/wall-tree-node.tsx +1 -5
  182. package/src/components/ui/sidebar/panels/site-panel/window-tree-node.tsx +1 -5
  183. package/src/components/ui/sidebar/panels/site-panel/zone-tree-node.tsx +9 -4
  184. package/src/components/ui/sidebar/panels/zone-panel/index.tsx +0 -2
  185. package/src/components/ui/sidebar/tab-bar.tsx +10 -0
  186. package/src/components/ui/sidebar/use-plugin-panels.tsx +15 -5
  187. package/src/components/viewer/floorplan-compass-button.tsx +9 -1
  188. package/src/components/viewer/floorplan-preview.tsx +5 -6
  189. package/src/components/viewer/viewer-hover-label.tsx +34 -0
  190. package/src/components/viewer/viewer-rooms.tsx +175 -0
  191. package/src/components/viewer/viewer-scene-header.tsx +181 -145
  192. package/src/components/viewer/viewer-selection-manager.tsx +124 -0
  193. package/src/components/viewer/viewer-stage.tsx +2 -2
  194. package/src/components/viewer-zone-system.tsx +3 -8
  195. package/src/hooks/use-auto-save.integration.test.tsx +123 -9
  196. package/src/hooks/use-auto-save.ts +34 -8
  197. package/src/hooks/use-keyboard.test.ts +178 -3
  198. package/src/hooks/use-keyboard.ts +89 -71
  199. package/src/hooks/use-open-wall-ends.test.ts +99 -0
  200. package/src/hooks/use-open-wall-ends.ts +238 -0
  201. package/src/hooks/use-placement-notice.test.ts +48 -0
  202. package/src/hooks/use-placement-notice.ts +42 -0
  203. package/src/hooks/use-selected-room.ts +143 -0
  204. package/src/index.tsx +246 -39
  205. package/src/lib/ceiling-edit-session.test.ts +321 -0
  206. package/src/lib/ceiling-edit-session.ts +206 -0
  207. package/src/lib/ceiling-region-session.test.ts +179 -0
  208. package/src/lib/continuation.test.ts +19 -1
  209. package/src/lib/continuation.ts +25 -4
  210. package/src/lib/door-interaction.ts +14 -0
  211. package/src/lib/editor-level-display.test.ts +96 -0
  212. package/src/lib/editor-level-display.ts +48 -0
  213. package/src/lib/editor-views.ts +144 -0
  214. package/src/lib/export-geometry-wait.test.ts +90 -0
  215. package/src/lib/floor-edit-session.ts +153 -0
  216. package/src/lib/floor-footprints.test.ts +563 -0
  217. package/src/lib/floor-footprints.ts +395 -0
  218. package/src/lib/floor-foundation-support.test.ts +283 -0
  219. package/src/lib/floor-opening-draft.ts +218 -0
  220. package/src/lib/floor-openings.test.ts +216 -0
  221. package/src/lib/floor-region-geometry.ts +191 -0
  222. package/src/lib/floor-region-session.test.ts +485 -0
  223. package/src/lib/floor-region-session.ts +589 -0
  224. package/src/lib/floor-region-snap.ts +101 -0
  225. package/src/lib/floor-step-hover.test.ts +32 -0
  226. package/src/lib/floorplan/annotation-visibility.ts +17 -0
  227. package/src/lib/floorplan/dimension-drive.test.ts +360 -0
  228. package/src/lib/floorplan/dimension-drive.ts +479 -0
  229. package/src/lib/floorplan/floorplan-export.test.ts +12 -8
  230. package/src/lib/floorplan/floorplan-export.tsx +32 -8
  231. package/src/lib/floorplan/floorplan-extension.ts +32 -0
  232. package/src/lib/floorplan/floorplan-mode.test.ts +4 -0
  233. package/src/lib/floorplan/floorplan-mode.ts +2 -0
  234. package/src/lib/floorplan/floorplan-pdfkit-document.ts +339 -18
  235. package/src/lib/floorplan/floorplan-pdfkit-renderer.test.ts +138 -8
  236. package/src/lib/floorplan/floorplan-pdfkit-renderer.ts +56 -25
  237. package/src/lib/floorplan/floorplan-plugin-install.test.tsx +138 -0
  238. package/src/lib/floorplan/floorplan-readonly.ts +6 -0
  239. package/src/lib/floorplan/geometry.ts +13 -6
  240. package/src/lib/floorplan/index.ts +0 -9
  241. package/src/lib/floorplan/items.ts +5 -2
  242. package/src/lib/floorplan/marks.test.ts +213 -0
  243. package/src/lib/floorplan/marks.ts +238 -0
  244. package/src/lib/floorplan/open-wall-ends.test.ts +268 -0
  245. package/src/lib/floorplan/open-wall-ends.ts +162 -0
  246. package/src/lib/floorplan/pdf-fonts/GeistMono-Bold.ttf +0 -0
  247. package/src/lib/floorplan/pdf-fonts/GeistMono-Regular.ttf +0 -0
  248. package/src/lib/floorplan/pdf-fonts/LICENSE-GeistMono.txt +92 -0
  249. package/src/lib/floorplan/pdf-fonts/LICENSE-LiberationSans.txt +102 -0
  250. package/src/lib/floorplan/pdf-fonts/LiberationSans-Bold.ttf +0 -0
  251. package/src/lib/floorplan/pdf-fonts/LiberationSans-Regular.ttf +0 -0
  252. package/src/lib/floorplan/schedules.test.ts +189 -0
  253. package/src/lib/floorplan/schedules.ts +339 -0
  254. package/src/lib/floorplan/sheet-export.test.ts +100 -0
  255. package/src/lib/floorplan/sheet-export.ts +318 -0
  256. package/src/lib/floorplan/site-plan/build-site-plan-drawing.ts +779 -0
  257. package/src/lib/floorplan/site-plan/contours.test.ts +43 -0
  258. package/src/lib/floorplan/site-plan/contributors.ts +97 -0
  259. package/src/lib/floorplan/site-plan/coverage.ts +232 -0
  260. package/src/lib/floorplan/site-plan/drawing-type-switch.test.ts +15 -0
  261. package/src/lib/floorplan/site-plan/drawing-type-switch.tsx +86 -0
  262. package/src/lib/floorplan/site-plan/front-edge.test.ts +103 -0
  263. package/src/lib/floorplan/site-plan/front-edge.ts +156 -0
  264. package/src/lib/floorplan/site-plan/geometry.test.ts +275 -0
  265. package/src/lib/floorplan/site-plan/geometry.ts +557 -0
  266. package/src/lib/floorplan/site-plan/index.ts +71 -0
  267. package/src/lib/floorplan/site-plan/site-annotations.test.ts +366 -0
  268. package/src/lib/floorplan/site-plan/site-annotations.ts +1083 -0
  269. package/src/lib/floorplan/site-plan/site-parts.ts +379 -0
  270. package/src/lib/floorplan/site-plan/site-plan-layer.tsx +138 -0
  271. package/src/lib/floorplan/site-plan/street-name.test.ts +69 -0
  272. package/src/lib/floorplan/site-plan/street-name.ts +302 -0
  273. package/src/lib/floorplan/site-plan/yards.test.ts +69 -0
  274. package/src/lib/floorplan/types.ts +1 -27
  275. package/src/lib/fresh-planar-placement.test.ts +36 -0
  276. package/src/lib/fresh-planar-placement.ts +59 -36
  277. package/src/lib/geometry-script/author.ts +152 -0
  278. package/src/lib/geometry-script/client.ts +152 -0
  279. package/src/lib/geometry-script/protocol.ts +12 -0
  280. package/src/lib/gesture-lifecycle.test.ts +595 -0
  281. package/src/lib/gesture-lifecycle.ts +290 -0
  282. package/src/lib/glb-export-zone-holes.test.ts +41 -0
  283. package/src/lib/glb-export.test.ts +485 -0
  284. package/src/lib/glb-export.ts +237 -119
  285. package/src/lib/history.test.ts +6 -0
  286. package/src/lib/history.ts +7 -0
  287. package/src/lib/hud-title.ts +109 -0
  288. package/src/lib/hydration-gated-storage.ts +49 -0
  289. package/src/lib/ifc-export.test.ts +174 -0
  290. package/src/lib/ifc-export.ts +201 -0
  291. package/src/lib/ifc-parts.ts +15 -0
  292. package/src/lib/inspector-expanded.test.ts +44 -0
  293. package/src/lib/inspector-expanded.ts +36 -0
  294. package/src/lib/interaction/scope.ts +18 -0
  295. package/src/lib/keyboard-pan.test.ts +129 -7
  296. package/src/lib/keyboard-pan.ts +161 -12
  297. package/src/lib/live-gesture-round-trip.test.ts +360 -0
  298. package/src/lib/lot/__fixtures__/map-dossier-miami-shores.json +1 -0
  299. package/src/lib/lot/__fixtures__/map-dossier-stpete.json +1 -0
  300. package/src/lib/lot/clean-ring.test.ts +186 -0
  301. package/src/lib/lot/clean-ring.ts +217 -0
  302. package/src/lib/lot/dossier.test.ts +202 -0
  303. package/src/lib/lot/dossier.ts +498 -0
  304. package/src/lib/lot/drop-in.test.ts +326 -0
  305. package/src/lib/lot/drop-in.ts +407 -0
  306. package/src/lib/lot/index.ts +43 -0
  307. package/src/lib/lot/lot-patch.test.ts +340 -0
  308. package/src/lib/lot/lot-patch.ts +394 -0
  309. package/src/lib/lot/parcel-provider.ts +43 -0
  310. package/src/lib/lot/terrain.test.ts +161 -0
  311. package/src/lib/lot/terrain.ts +266 -0
  312. package/src/lib/material-catalog-model.ts +3 -1
  313. package/src/lib/material-paint-panel-model.ts +9 -5
  314. package/src/lib/material-paint.ts +4 -0
  315. package/src/lib/mezzanine-actions.test.ts +342 -0
  316. package/src/lib/mezzanine-draft.test.ts +501 -0
  317. package/src/lib/mezzanine-draft.ts +278 -0
  318. package/src/lib/mezzanine-messages.ts +26 -0
  319. package/src/lib/mezzanine-selection.test.ts +207 -0
  320. package/src/lib/mezzanine-stairs.ts +46 -0
  321. package/src/lib/model-export.ts +3 -1
  322. package/src/lib/node-action-movement.ts +17 -6
  323. package/src/lib/node-display-label.ts +37 -0
  324. package/src/lib/orbit-floor-clearance.test.ts +165 -0
  325. package/src/lib/orbit-floor-clearance.ts +106 -0
  326. package/src/lib/paint-commit-route.test.ts +37 -0
  327. package/src/lib/paint-commit-route.ts +17 -0
  328. package/src/lib/paint-eyedropper.test.tsx +165 -0
  329. package/src/lib/paint-eyedropper.ts +99 -0
  330. package/src/lib/paint-pass-through.test.ts +111 -0
  331. package/src/lib/paint-pass-through.ts +76 -0
  332. package/src/lib/paint-region-hover.ts +25 -0
  333. package/src/lib/paint-region-mode.ts +154 -0
  334. package/src/lib/paint-regions.test.ts +166 -0
  335. package/src/lib/paint-regions.ts +192 -0
  336. package/src/lib/paint-scope.test.ts +317 -35
  337. package/src/lib/paint-scope.ts +266 -25
  338. package/src/lib/panel-rows.ts +21 -0
  339. package/src/lib/parametric-node-update.ts +1 -1
  340. package/src/lib/plate-paint-affected.test.ts +300 -0
  341. package/src/lib/plate-paint-affected.ts +177 -0
  342. package/src/lib/plugin-chat.ts +50 -0
  343. package/src/lib/plugin-panels.test.ts +25 -0
  344. package/src/lib/plugin-panels.ts +94 -0
  345. package/src/lib/print-shell-compiler.ts +1 -0
  346. package/src/lib/registered-draft-snap.ts +12 -0
  347. package/src/lib/roof-duplication.ts +10 -2
  348. package/src/lib/room-assembly-overlay.test.ts +275 -0
  349. package/src/lib/room-assembly-overlay.ts +266 -0
  350. package/src/lib/room-built-on.test.ts +379 -0
  351. package/src/lib/room-built-on.ts +138 -0
  352. package/src/lib/room-construction-commands.ts +712 -0
  353. package/src/lib/room-construction.test.ts +746 -0
  354. package/src/lib/room-construction.ts +385 -0
  355. package/src/lib/room-controls-phase6.test.ts +630 -0
  356. package/src/lib/room-divide-session.ts +523 -0
  357. package/src/lib/room-handle-drag.ts +1068 -0
  358. package/src/lib/room-name-catalog.test.ts +90 -0
  359. package/src/lib/room-name-catalog.ts +168 -0
  360. package/src/lib/room-push-dimensions.test.ts +311 -0
  361. package/src/lib/room-push-dimensions.ts +251 -0
  362. package/src/lib/room-selection-commands.ts +62 -0
  363. package/src/lib/room-selection.test.ts +973 -0
  364. package/src/lib/room-selection.ts +417 -0
  365. package/src/lib/room-structure-commands.ts +94 -0
  366. package/src/lib/room-structure.test.ts +921 -0
  367. package/src/lib/room-transform-session.ts +570 -0
  368. package/src/lib/room-zone-routing.test.ts +294 -0
  369. package/src/lib/room-zone-routing.ts +197 -0
  370. package/src/lib/scene-clipboard.test.ts +364 -0
  371. package/src/lib/scene-clipboard.ts +229 -48
  372. package/src/lib/scene-material-refs.test.ts +79 -0
  373. package/src/lib/scene-material-refs.ts +79 -0
  374. package/src/lib/scene.test.ts +24 -36
  375. package/src/lib/scene.ts +33 -57
  376. package/src/lib/selection-routing.test.ts +129 -45
  377. package/src/lib/selection-routing.ts +53 -74
  378. package/src/lib/session-writes.test.ts +120 -0
  379. package/src/lib/session-writes.ts +205 -0
  380. package/src/lib/sidebar-panel.ts +62 -0
  381. package/src/lib/site-boundary.test.ts +4 -6
  382. package/src/lib/site-boundary.ts +1 -1
  383. package/src/lib/snapping-mode.test.ts +4 -0
  384. package/src/lib/snapping-mode.ts +8 -1
  385. package/src/lib/terrace-draft.test.ts +145 -0
  386. package/src/lib/terrace-draft.ts +213 -0
  387. package/src/lib/tool-cursor-icon.test.ts +16 -0
  388. package/src/lib/tool-cursor-icon.ts +11 -0
  389. package/src/lib/typing-target.ts +9 -0
  390. package/src/lib/units.ts +77 -39
  391. package/src/lib/vector-edges.test.ts +58 -0
  392. package/src/lib/vector-edges.ts +400 -0
  393. package/src/lib/viewer-selection.test.ts +577 -0
  394. package/src/lib/viewer-selection.ts +569 -0
  395. package/src/lib/wall-draw-variant.test.ts +71 -0
  396. package/src/lib/wall-draw-variant.ts +91 -0
  397. package/src/lib/wall-paint-region-session.ts +246 -0
  398. package/src/lib/wall-paint-region.test.ts +373 -0
  399. package/src/lib/wall-polygon-draft.test.ts +262 -0
  400. package/src/lib/wall-polygon-draft.ts +286 -0
  401. package/src/lib/wall-push.test.ts +453 -0
  402. package/src/lib/wall-region-face.ts +352 -0
  403. package/src/lib/wall-region-handles.test.ts +304 -0
  404. package/src/lib/wall-region-handles.ts +368 -0
  405. package/src/lib/wall-region-list.test.ts +119 -0
  406. package/src/lib/wall-region-list.ts +136 -0
  407. package/src/lib/wall-region-snap.ts +110 -0
  408. package/src/lib/window-interaction.ts +14 -0
  409. package/src/lib/zone-content.ts +4 -1
  410. package/src/lib/zone-name.test.ts +17 -0
  411. package/src/lib/zone-name.ts +17 -0
  412. package/src/store/opens-in-select-mode.test.ts +223 -0
  413. package/src/store/paint-mode.test.ts +188 -0
  414. package/src/store/terrain-sculpt-mode.test.ts +19 -21
  415. package/src/store/tool-mode.test.ts +137 -6
  416. package/src/store/use-delete-confirmation.ts +28 -1
  417. package/src/store/use-drawing-view.test.ts +24 -3
  418. package/src/store/use-drawing-view.ts +29 -1
  419. package/src/store/use-editor.tsx +432 -321
  420. package/src/store/use-floorplan-draft-preview.ts +13 -0
  421. package/src/store/use-floorplan-marquee.ts +12 -2
  422. package/src/store/use-quick-measurement-hud.ts +3 -3
  423. package/src/store/use-stair-build-preview.ts +13 -5
  424. package/src/store/use-wall-move-ghosts.ts +4 -2
  425. package/src/store/use-wall-region-handles-selection.test.ts +222 -0
  426. package/src/store/use-wall-region-handles-selection.ts +139 -0
  427. package/src/store/view-layout.test.ts +175 -0
  428. package/src/store/view-layout.ts +196 -0
  429. package/src/test-utils/immediate-animation-frames.ts +16 -0
  430. package/src/test-utils/live-gesture-round-trip.ts +224 -0
  431. package/src/test-utils/selection-harness.ts +82 -0
  432. package/src/components/editor-2d/renderers/floorplan-stair-layer.tsx +0 -499
  433. package/src/components/systems/zone/zone-label-editor-system.tsx +0 -333
  434. package/src/components/tools/fence/fence-drafting.test.ts +0 -100
  435. package/src/components/tools/fence/fence-drafting.ts +0 -266
  436. package/src/components/ui/sidebar/panels/plugins-panel.tsx +0 -218
  437. package/src/components/viewer/viewer-units-panel.tsx +0 -116
  438. package/src/lib/floorplan/stairs.ts +0 -489
  439. package/src/lib/level-duplication.test.ts +0 -144
  440. package/src/lib/level-duplication.ts +0 -195
@@ -1,15 +1,23 @@
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,
8
10
  emitter,
9
11
  sceneRegistry,
10
12
  useScene,
11
13
  } from '@pascal-app/core'
12
- import { GRID_LAYER, getLevelPresentationY, useViewer, ZONE_LAYER } from '@pascal-app/viewer'
14
+ import {
15
+ GRID_LAYER,
16
+ getLevelPresentationY,
17
+ useSceneGroundReplacement,
18
+ useViewer,
19
+ ZONE_LAYER,
20
+ } from '@pascal-app/viewer'
13
21
  import { CameraControls, CameraControlsImpl } from '@react-three/drei'
14
22
  import { useFrame, useThree } from '@react-three/fiber'
15
23
  import { useCallback, useEffect, useLayoutEffect, useMemo, useRef } from 'react'
@@ -33,24 +41,35 @@ import {
33
41
  import { EDITOR_LAYER } from '../../lib/constants'
34
42
  import {
35
43
  acceptsKeyboardPan,
44
+ advanceKeyboardPanMotion,
36
45
  clearKeyboardPanKeys,
46
+ createKeyboardPanMotion,
37
47
  hasKeyboardPanInput,
38
48
  isEditableKeyboardTarget,
39
49
  isKeyboardPanKey,
40
50
  type KeyboardPanState,
41
- keyboardPanDirection,
42
51
  keyboardPanSpeed,
43
52
  setKeyboardPanKey,
53
+ syncKeyboardPanMotion,
44
54
  } from '../../lib/keyboard-pan'
55
+ import {
56
+ MAX_ORBIT_POLAR_ANGLE,
57
+ ORBIT_FLOOR_CLEARANCE,
58
+ ORBIT_TARGET_HEIGHT,
59
+ } from '../../lib/orbit-floor-clearance'
45
60
  import { editorOwnsOneFingerDrag } from '../../lib/touch-gesture-priority'
61
+ import { frameViewerCamera } from '../../lib/viewer-selection'
46
62
  import { publishCameraPose } from '../../store/camera-pose-store'
47
- import useEditor from '../../store/use-editor'
63
+ import useEditor, { takePreviewCameraRestore } from '../../store/use-editor'
48
64
  import {
49
65
  useActiveHandleDrag,
50
66
  useEndpointReshape,
51
67
  useMovingNode,
52
68
  } from '../../store/use-interaction-scope'
69
+ import { visibleScene } from '../../store/view-layout'
53
70
  import { createCameraDraggingLifecycle } from './camera-dragging-lifecycle'
71
+ import { FloorAwareCameraControls } from './floor-aware-camera-controls'
72
+ import { FloorSurface } from './floor-surface'
54
73
 
55
74
  const currentTarget = new Vector3()
56
75
  const tempBox = new Box3()
@@ -62,11 +81,37 @@ const tempTarget = new Vector3()
62
81
  const transitionFreezePosition = new Vector3()
63
82
  const transitionFreezeTarget = new Vector3()
64
83
  const keyboardPanSpherical = new Spherical()
84
+ /** Mouse/touch orbit speed (camera-controls default 1): slower for precise looks. */
85
+ const ORBIT_ROTATE_SPEED = 0.6
65
86
  // In 2D-only view the canvas is paused, so the floor plan drives WASD, orbit and
66
87
  // top view itself (`floorplan-panel.tsx`) and the camera stands down.
67
- const planOwnsNavigation = () => useEditor.getState().viewMode === '2d'
88
+ const planOwnsNavigation = () => visibleScene(useEditor.getState()) === '2d'
68
89
  const DEFAULT_MAX_POLAR_ANGLE = Math.PI / 2 - 0.1
69
- const DEBUG_MAX_POLAR_ANGLE = Math.PI - 0.05
90
+ // Focusing one of these aims at its floor (raised to the orbit's pivot
91
+ // height), not at the middle of its bounding box.
92
+ const GROUND_FOCUS_TYPES = new Set(['site', 'building', 'level', 'zone', 'slab'])
93
+
94
+ type LevelMode = ReturnType<typeof useViewer.getState>['levelMode']
95
+
96
+ /** Where the orbit pivot rests on a level: its floor plus the pivot height. */
97
+ function orbitGroundY(
98
+ levelId: string | null | undefined,
99
+ levelMode: LevelMode = useViewer.getState().levelMode,
100
+ ) {
101
+ const floorY = levelId ? getLevelPresentationY(levelId, useScene.getState().nodes, levelMode) : 0
102
+ return floorY + ORBIT_TARGET_HEIGHT
103
+ }
104
+
105
+ function levelOf(nodeId: string): string | null {
106
+ const nodes = useScene.getState().nodes
107
+ let node: AnyNode | undefined = nodes[nodeId as AnyNodeId]
108
+ while (node) {
109
+ if (node.type === 'level') return node.id
110
+ node = node.parentId ? nodes[node.parentId as AnyNodeId] : undefined
111
+ }
112
+ return null
113
+ }
114
+
70
115
  type CameraMode = ReturnType<typeof useViewer.getState>['cameraMode']
71
116
  type CameraPoseSnapshot = {
72
117
  mode: CameraMode
@@ -311,7 +356,7 @@ function useFirstPersonCameraPoseRestore(
311
356
  }
312
357
 
313
358
  export const CustomCameraControls = ({ paused = false }: { paused?: boolean }) => {
314
- const controls = useRef<CameraControlsImpl | null>(null)
359
+ const controls = useRef<FloorAwareCameraControls | null>(null)
315
360
  const pendingAppliedPose = useRef<CameraPoseApplicationPlan | null>(null)
316
361
  const activePoseInterpolation = useRef<{
317
362
  camera: Camera
@@ -325,6 +370,7 @@ export const CustomCameraControls = ({ paused = false }: { paused?: boolean }) =
325
370
  left: false,
326
371
  right: false,
327
372
  })
373
+ const keyboardPanMotion = useRef(createKeyboardPanMotion())
328
374
  const isPreviewMode = useEditor((s) => s.isPreviewMode)
329
375
  const isFirstPersonMode = useEditor((s) => s.isFirstPersonMode)
330
376
  const allowUndergroundCamera = useEditor((s) => s.allowUndergroundCamera)
@@ -337,12 +383,24 @@ export const CustomCameraControls = ({ paused = false }: { paused?: boolean }) =
337
383
  isFirstPersonMode,
338
384
  cameraMode,
339
385
  )
340
- const currentLevelId = selection.levelId
386
+ // Selecting the building or site clears the level; the camera keeps
387
+ // standing on the last level rather than dropping to the ground floor.
388
+ const lastLevelId = useRef<string | null>(null)
389
+ if (selection.levelId) lastLevelId.current = selection.levelId
390
+ const currentLevelId = lastLevelId.current
341
391
  const firstLoad = useRef(true)
392
+ const floorY = useScene((state) =>
393
+ currentLevelId ? getLevelPresentationY(currentLevelId, state.nodes, levelMode) : 0,
394
+ )
395
+ const allowUnderground = !isPreviewMode && allowUndergroundCamera
396
+ const floorMinY = allowUnderground ? null : floorY + ORBIT_FLOOR_CLEARANCE
342
397
  const maxPolarAngle =
343
- !isPreviewMode && allowUndergroundCamera ? DEBUG_MAX_POLAR_ANGLE : DEFAULT_MAX_POLAR_ANGLE
398
+ allowUnderground || cameraMode === 'perspective'
399
+ ? MAX_ORBIT_POLAR_ANGLE
400
+ : DEFAULT_MAX_POLAR_ANGLE
344
401
 
345
402
  const camera = useThree((state) => state.camera)
403
+ const scene = useThree((state) => state.scene)
346
404
  const gl = useThree((state) => state.gl)
347
405
  const raycaster = useThree((state) => state.raycaster)
348
406
  const viewportSize = useThree((state) => state.size)
@@ -381,7 +439,10 @@ export const CustomCameraControls = ({ paused = false }: { paused?: boolean }) =
381
439
  const beginLocalCameraInteraction = useCallback(
382
440
  ({ dragging = true }: { dragging?: boolean } = {}) => {
383
441
  cancelPoseApplication()
384
- if (dragging) cameraDraggingLifecycle.begin()
442
+ if (dragging) {
443
+ controls.current?.releaseWrittenPose()
444
+ cameraDraggingLifecycle.begin()
445
+ }
385
446
  emitter.emit('camera-controls:interaction-start', undefined)
386
447
  },
387
448
  [cameraDraggingLifecycle, cancelPoseApplication],
@@ -469,11 +530,21 @@ export const CustomCameraControls = ({ paused = false }: { paused?: boolean }) =
469
530
 
470
531
  emitter.on('camera-controls:apply-pose', handleAppliedPose)
471
532
  emitter.on('camera-controls:cancel-pose', cancelPoseApplication)
533
+ if (!isPreviewMode) {
534
+ const restore = takePreviewCameraRestore()
535
+ if (restore) handleAppliedPose(restore)
536
+ }
472
537
  return () => {
473
538
  emitter.off('camera-controls:apply-pose', handleAppliedPose)
474
539
  emitter.off('camera-controls:cancel-pose', cancelPoseApplication)
475
540
  }
476
- }, [applyPendingPose, cancelPoseApplication, freezeActivePoseInterpolation, isFirstPersonMode])
541
+ }, [
542
+ applyPendingPose,
543
+ cancelPoseApplication,
544
+ freezeActivePoseInterpolation,
545
+ isFirstPersonMode,
546
+ isPreviewMode,
547
+ ])
477
548
 
478
549
  useEffect(() => cancelPoseApplication, [cancelPoseApplication])
479
550
 
@@ -484,33 +555,60 @@ export const CustomCameraControls = ({ paused = false }: { paused?: boolean }) =
484
555
  if (process.env.NODE_ENV !== 'development') return
485
556
  const w = window as typeof window & {
486
557
  __pascalCameraControls?: (() => CameraControlsImpl | null) | null
558
+ __pascalScene?: (() => unknown) | null
559
+ __pascalCapture?: (() => Promise<string>) | null
487
560
  }
488
561
  w.__pascalCameraControls = () => controls.current
562
+ // the live three.js scene, for the same tooling (a probe can walk the
563
+ // meshes for an empty geometry the WebGPU validator complains about)
564
+ w.__pascalScene = () => scene
565
+ // a fresh frame read back as a PNG data URL: the canvas is cleared after
566
+ // each presented frame, so a capture has to render and read in one task
567
+ w.__pascalCapture = async () => {
568
+ const r = gl as unknown as {
569
+ renderAsync?: (s: unknown, c: unknown) => Promise<void>
570
+ render: (s: unknown, c: unknown) => void
571
+ domElement: HTMLCanvasElement
572
+ }
573
+ if (r.renderAsync) await r.renderAsync(scene, camera)
574
+ else r.render(scene, camera)
575
+ return r.domElement.toDataURL('image/png')
576
+ }
489
577
  return () => {
490
578
  w.__pascalCameraControls = null
579
+ w.__pascalScene = null
580
+ w.__pascalCapture = null
491
581
  }
492
- }, [])
582
+ }, [scene, gl, camera])
493
583
 
584
+ // `camera` too: drei builds a fresh controls instance (pivot at the origin)
585
+ // when the default camera mounts or changes; it is rested on the floor too.
586
+ // biome-ignore lint/correctness/useExhaustiveDependencies: camera re-runs this on a new instance
494
587
  useEffect(() => {
495
588
  if (isPreviewMode || isFirstPersonMode || isRestoringFirstPersonPose()) return
496
589
  // Analytic destination, not `sceneRegistry` mesh position: a level created
497
590
  // this frame still sits at y=0 (LevelSystem lerps it later), and a mode
498
591
  // switch leaves every level mid-lerp — the camera must pan to where the
499
592
  // level will settle, in the CURRENT presentation mode.
500
- const targetY = currentLevelId
501
- ? getLevelPresentationY(currentLevelId, useScene.getState().nodes, levelMode)
502
- : 0
593
+ const targetY = orbitGroundY(currentLevelId, levelMode)
503
594
  if (!controls.current) return
504
595
  if (firstLoad.current) {
505
596
  firstLoad.current = false
506
- controls.current.setLookAt(20, 20, 20, 0, 0, 0, true)
597
+ controls.current.setLookAt(20, 20 + targetY, 20, 0, targetY, 0, true)
507
598
  }
508
- controls.current.getTarget(currentTarget)
599
+ controls.current.getOrbitTarget(currentTarget)
509
600
  // Idempotence guard: skip when already there — also swallows the thumbnail
510
601
  // generator's synchronous stacked→restore levelMode round-trip.
511
602
  if (Math.abs(currentTarget.y - targetY) < 1e-3) return
512
603
  controls.current.moveTo(currentTarget.x, targetY, currentTarget.z, true)
513
- }, [currentLevelId, levelMode, isPreviewMode, isFirstPersonMode, isRestoringFirstPersonPose])
604
+ }, [
605
+ camera,
606
+ currentLevelId,
607
+ levelMode,
608
+ isPreviewMode,
609
+ isFirstPersonMode,
610
+ isRestoringFirstPersonPose,
611
+ ])
514
612
 
515
613
  useEffect(() => {
516
614
  if (isFirstPersonMode || !controls.current) return
@@ -523,6 +621,15 @@ export const CustomCameraControls = ({ paused = false }: { paused?: boolean }) =
523
621
  }
524
622
  }, [isFirstPersonMode, maxPolarAngle])
525
623
 
624
+ const floorSurface = useMemo(() => new FloorSurface(), [])
625
+ // biome-ignore lint/correctness/useExhaustiveDependencies: drei builds a new controls instance per camera
626
+ useLayoutEffect(() => {
627
+ floorSurface.levelId = currentLevelId ?? null
628
+ if (!controls.current) return
629
+ controls.current.floorMinY = floorMinY
630
+ controls.current.floorSurface = floorSurface
631
+ }, [camera, currentLevelId, floorMinY, floorSurface, isFirstPersonMode])
632
+
526
633
  const focusNode = useCallback(
527
634
  (nodeId: string) => {
528
635
  if (isPreviewMode || isFirstPersonMode || !controls.current) return
@@ -530,17 +637,34 @@ export const CustomCameraControls = ({ paused = false }: { paused?: boolean }) =
530
637
  const object3D = sceneRegistry.nodes.get(nodeId)
531
638
  if (!object3D) return
532
639
 
640
+ const node = useScene.getState().nodes[nodeId as AnyNodeId]
641
+ const groundFocus = node !== undefined && GROUND_FOCUS_TYPES.has(node.type)
533
642
  tempBox.setFromObject(object3D)
534
- if (tempBox.isEmpty()) return
535
-
536
- tempBox.getCenter(tempCenter)
643
+ if (!tempBox.isEmpty()) {
644
+ tempBox.getCenter(tempCenter)
645
+ } else if (groundFocus && node.type === 'zone' && node.polygon.length > 0) {
646
+ // A room draws nothing measurable in some display modes: aim at the
647
+ // middle of its outline instead.
648
+ tempBox.makeEmpty()
649
+ for (const [x, z] of node.polygon) tempBox.expandByPoint(tempCenter.set(x, 0, z))
650
+ tempBox.getCenter(tempCenter)
651
+ object3D.updateWorldMatrix(true, false)
652
+ tempCenter.applyMatrix4(object3D.matrixWorld)
653
+ } else {
654
+ return
655
+ }
656
+ if (groundFocus) {
657
+ tempCenter.y = orbitGroundY(levelOf(nodeId) ?? lastLevelId.current)
658
+ }
537
659
  controls.current.getPosition(tempPosition)
538
660
  controls.current.getTarget(tempTarget)
539
661
  tempDelta.copy(tempCenter).sub(tempTarget)
540
662
 
663
+ const focusedY = tempPosition.y + tempDelta.y
664
+ const floorMinY = isPerspectiveCamera(camera) ? controls.current.floorLimitY : null
541
665
  controls.current.setLookAt(
542
666
  tempPosition.x + tempDelta.x,
543
- tempPosition.y + tempDelta.y,
667
+ floorMinY === null ? focusedY : Math.max(focusedY, floorMinY),
544
668
  tempPosition.z + tempDelta.z,
545
669
  tempCenter.x,
546
670
  tempCenter.y,
@@ -548,7 +672,7 @@ export const CustomCameraControls = ({ paused = false }: { paused?: boolean }) =
548
672
  true,
549
673
  )
550
674
  },
551
- [isPreviewMode, isFirstPersonMode],
675
+ [camera, isPreviewMode, isFirstPersonMode],
552
676
  )
553
677
 
554
678
  const publishCurrentPose = useCallback(() => {
@@ -630,18 +754,18 @@ export const CustomCameraControls = ({ paused = false }: { paused?: boolean }) =
630
754
  }
631
755
  }
632
756
 
633
- const { horizontal, vertical } = keyboardPanDirection(keyboardPanKeys.current)
634
- if (horizontal === 0 && vertical === 0) return
757
+ const motion = keyboardPanMotion.current
758
+ if (!advanceKeyboardPanMotion(motion, keyboardPanKeys.current, performance.now())) return
635
759
 
636
760
  const control = controls.current
637
761
 
638
762
  control.getSpherical(keyboardPanSpherical, false)
639
763
  const viewWidth = getCameraViewWidth(camera, keyboardPanSpherical.radius, viewportSize)
640
764
  const speed = keyboardPanSpeed(viewWidth)
641
- const step = (speed * Math.min(delta, 0.05)) / Math.hypot(horizontal, vertical)
642
765
 
643
- if (horizontal !== 0) control.truck(horizontal * step, 0, true)
644
- if (vertical !== 0) control.forward(vertical * step, true)
766
+ // The motion already eases; the controls' own smoothing would add a glide on top.
767
+ if (motion.stepX !== 0) control.truck(motion.stepX * speed, 0, false)
768
+ if (motion.stepY !== 0) control.forward(motion.stepY * speed, false)
645
769
  }, 0)
646
770
 
647
771
  // Configure mouse buttons based on control mode and camera mode
@@ -652,13 +776,14 @@ export const CustomCameraControls = ({ paused = false }: { paused?: boolean }) =
652
776
  ? CameraControlsImpl.ACTION.ZOOM
653
777
  : CameraControlsImpl.ACTION.DOLLY
654
778
 
779
+ // Left click selects, in Preview as in the published viewer.
655
780
  return {
656
- left: isPreviewMode ? CameraControlsImpl.ACTION.SCREEN_PAN : CameraControlsImpl.ACTION.NONE,
781
+ left: CameraControlsImpl.ACTION.NONE,
657
782
  middle: CameraControlsImpl.ACTION.SCREEN_PAN,
658
783
  right: CameraControlsImpl.ACTION.ROTATE,
659
784
  wheel: wheelAction,
660
785
  }
661
- }, [cameraMode, isPreviewMode])
786
+ }, [cameraMode])
662
787
 
663
788
  // Touch gestures (mobile / trackpad).
664
789
  // - One finger drag → rotate by default (much easier on a phone), but
@@ -774,10 +899,7 @@ export const CustomCameraControls = ({ paused = false }: { paused?: boolean }) =
774
899
  controls.current.mouseButtons.wheel = wheelAction
775
900
  controls.current.mouseButtons.middle = CameraControlsImpl.ACTION.SCREEN_PAN
776
901
  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) {
902
+ if (space) {
781
903
  controls.current.mouseButtons.left = CameraControlsImpl.ACTION.SCREEN_PAN
782
904
  } else {
783
905
  controls.current.mouseButtons.left = CameraControlsImpl.ACTION.NONE
@@ -787,6 +909,11 @@ export const CustomCameraControls = ({ paused = false }: { paused?: boolean }) =
787
909
  const onKeyDown = (event: KeyboardEvent) => {
788
910
  if (isKeyboardPanKey(event.code)) {
789
911
  if (acceptsKeyboardPan(event) && !planOwnsNavigation()) {
912
+ syncKeyboardPanMotion(
913
+ keyboardPanMotion.current,
914
+ keyboardPanKeys.current,
915
+ performance.now(),
916
+ )
790
917
  const changed = setKeyboardPanKey(keyboardPanKeys.current, event.code, true)
791
918
  if (changed) beginLocalCameraInteraction()
792
919
  event.preventDefault()
@@ -818,6 +945,7 @@ export const CustomCameraControls = ({ paused = false }: { paused?: boolean }) =
818
945
 
819
946
  const onKeyUp = (event: KeyboardEvent) => {
820
947
  if (isKeyboardPanKey(event.code)) {
948
+ syncKeyboardPanMotion(keyboardPanMotion.current, keyboardPanKeys.current, performance.now())
821
949
  const changed = setKeyboardPanKey(keyboardPanKeys.current, event.code, false)
822
950
  if (changed) {
823
951
  if (!hasKeyboardPanInput(keyboardPanKeys.current)) {
@@ -907,14 +1035,7 @@ export const CustomCameraControls = ({ paused = false }: { paused?: boolean }) =
907
1035
  clearNavigationCursor()
908
1036
  cameraDraggingLifecycle.end()
909
1037
  }
910
- }, [
911
- beginLocalCameraInteraction,
912
- cameraDraggingLifecycle,
913
- cameraMode,
914
- gl,
915
- isPreviewMode,
916
- isFirstPersonMode,
917
- ])
1038
+ }, [beginLocalCameraInteraction, cameraDraggingLifecycle, cameraMode, gl, isFirstPersonMode])
918
1039
 
919
1040
  // `controlstart` fires only for user pointer interactions. Pointerdowns
920
1041
  // mapped to ACTION.NONE must not flag the camera as dragging because no
@@ -927,18 +1048,18 @@ export const CustomCameraControls = ({ paused = false }: { paused?: boolean }) =
927
1048
  })
928
1049
  }, [beginLocalCameraInteraction])
929
1050
 
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
-
1051
+ // Preview opens on the editor's floor (or building, or site) framed the
1052
+ // viewer's way; afterwards only the viewer navigation moves the camera
1053
+ // (`camera-controls:frame`), never a canvas selection.
935
1054
  useEffect(() => {
936
1055
  if (!(isPreviewMode && controls.current) || isFirstPersonMode) return
937
1056
 
938
1057
  const nodes = useScene.getState().nodes
939
- let node = previewTargetNodeId ? nodes[previewTargetNodeId] : null
1058
+ const { selection: opening } = useViewer.getState()
1059
+ const openingNodeId = opening.levelId ?? opening.buildingId
1060
+ let node = openingNodeId ? nodes[openingNodeId as AnyNodeId] : null
940
1061
 
941
- if (!previewTargetNodeId) {
1062
+ if (!openingNodeId) {
942
1063
  const site = Object.values(nodes).find((n) => n.type === 'site')
943
1064
  node = site || null
944
1065
  }
@@ -971,10 +1092,10 @@ export const CustomCameraControls = ({ paused = false }: { paused?: boolean }) =
971
1092
  return
972
1093
  }
973
1094
 
974
- if (!previewTargetNodeId) return
1095
+ if (!openingNodeId) return
975
1096
 
976
1097
  // Calculate camera position from bounding box
977
- const object3D = sceneRegistry.nodes.get(previewTargetNodeId)
1098
+ const object3D = sceneRegistry.nodes.get(openingNodeId)
978
1099
  if (!object3D) return
979
1100
 
980
1101
  tempBox.setFromObject(object3D)
@@ -993,7 +1114,7 @@ export const CustomCameraControls = ({ paused = false }: { paused?: boolean }) =
993
1114
  tempCenter.z,
994
1115
  true,
995
1116
  )
996
- }, [isPreviewMode, isFirstPersonMode, previewTargetNodeId])
1117
+ }, [isPreviewMode, isFirstPersonMode])
997
1118
 
998
1119
  // Preset capture auto-framing — when `setCaptureMode({ mode: 'preset',
999
1120
  // isolated })` fires, fly the camera to a pose that fits the union
@@ -1159,7 +1280,7 @@ export const CustomCameraControls = ({ paused = false }: { paused?: boolean }) =
1159
1280
  if (isFirstPersonMode || !controls.current || planOwnsNavigation()) return
1160
1281
 
1161
1282
  const currentAzimuth = controls.current.azimuthAngle
1162
- const currentPolar = controls.current.polarAngle
1283
+ const currentPolar = controls.current.orbitPolarAngle
1163
1284
  // Round to nearest 90° increment, then rotate 90° clockwise
1164
1285
  const rounded = Math.round(currentAzimuth / (Math.PI / 2)) * (Math.PI / 2)
1165
1286
  const target = rounded - Math.PI / 2
@@ -1171,7 +1292,7 @@ export const CustomCameraControls = ({ paused = false }: { paused?: boolean }) =
1171
1292
  if (isFirstPersonMode || !controls.current || planOwnsNavigation()) return
1172
1293
 
1173
1294
  const currentAzimuth = controls.current.azimuthAngle
1174
- const currentPolar = controls.current.polarAngle
1295
+ const currentPolar = controls.current.orbitPolarAngle
1175
1296
  // Round to nearest 90° increment, then rotate 90° counter-clockwise
1176
1297
  const rounded = Math.round(currentAzimuth / (Math.PI / 2)) * (Math.PI / 2)
1177
1298
  const target = rounded + Math.PI / 2
@@ -1183,11 +1304,18 @@ export const CustomCameraControls = ({ paused = false }: { paused?: boolean }) =
1183
1304
  focusNode(nodeId)
1184
1305
  }
1185
1306
 
1307
+ const handleFrame = (event: CameraControlFrameEvent) => {
1308
+ if (isFirstPersonMode || !controls.current) return
1309
+ cancelPoseApplication()
1310
+ frameViewerCamera(controls.current, camera, event)
1311
+ }
1312
+
1186
1313
  const handleFitScene = ({ bounds }: CameraControlFitSceneEvent) => {
1187
1314
  if (isFirstPersonMode || !controls.current || isPreviewMode) return
1315
+ const groundY = orbitGroundY(lastLevelId.current)
1188
1316
  if (!bounds) {
1189
1317
  // Restore default framing pose when no bounds were computed.
1190
- controls.current.setLookAt(20, 20, 20, 0, 0, 0, true)
1318
+ controls.current.setLookAt(20, 20 + groundY, 20, 0, groundY, 0, true)
1191
1319
  return
1192
1320
  }
1193
1321
  const [cx, cz] = bounds.center
@@ -1197,11 +1325,20 @@ export const CustomCameraControls = ({ paused = false }: { paused?: boolean }) =
1197
1325
  const maxExtent = Math.max(w, d)
1198
1326
  const distance = Math.max(maxExtent * 1.4, 15)
1199
1327
  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)
1328
+ controls.current.setLookAt(
1329
+ cx + distance * 0.7,
1330
+ height + groundY,
1331
+ cz + distance * 0.7,
1332
+ cx,
1333
+ groundY,
1334
+ cz,
1335
+ true,
1336
+ )
1201
1337
  }
1202
1338
 
1203
1339
  emitter.on('camera-controls:capture', handleNodeCapture)
1204
1340
  emitter.on('camera-controls:focus', handleNodeFocus)
1341
+ emitter.on('camera-controls:frame', handleFrame)
1205
1342
  emitter.on('camera-controls:view', handleNodeView)
1206
1343
  emitter.on('camera-controls:top-view', handleTopView)
1207
1344
  emitter.on('camera-controls:orbit-cw', handleOrbitCW)
@@ -1211,13 +1348,14 @@ export const CustomCameraControls = ({ paused = false }: { paused?: boolean }) =
1211
1348
  return () => {
1212
1349
  emitter.off('camera-controls:capture', handleNodeCapture)
1213
1350
  emitter.off('camera-controls:focus', handleNodeFocus)
1351
+ emitter.off('camera-controls:frame', handleFrame)
1214
1352
  emitter.off('camera-controls:view', handleNodeView)
1215
1353
  emitter.off('camera-controls:top-view', handleTopView)
1216
1354
  emitter.off('camera-controls:orbit-cw', handleOrbitCW)
1217
1355
  emitter.off('camera-controls:orbit-ccw', handleOrbitCCW)
1218
1356
  emitter.off('camera-controls:fit-scene', handleFitScene)
1219
1357
  }
1220
- }, [focusNode, isPreviewMode, isFirstPersonMode])
1358
+ }, [camera, cancelPoseApplication, focusNode, isPreviewMode, isFirstPersonMode])
1221
1359
 
1222
1360
  const onTransitionStart = useCallback(() => {
1223
1361
  cameraDraggingLifecycle.begin()
@@ -1248,6 +1386,10 @@ export const CustomCameraControls = ({ paused = false }: { paused?: boolean }) =
1248
1386
  // navigation guardrails.
1249
1387
  const isPresetCapture = captureMode.mode === 'preset'
1250
1388
  const minDistance = isPresetCapture ? 0.5 : 2
1389
+ // Over modeled or mapped surroundings (a neighbourhood instead of the
1390
+ // horizon disc) the camera may pull back far enough to take it all in.
1391
+ const groundReplaced = useSceneGroundReplacement()
1392
+ const maxDistance = groundReplaced ? 1500 : 100
1251
1393
 
1252
1394
  if (isFirstPersonMode) {
1253
1395
  return null
@@ -1255,8 +1397,10 @@ export const CustomCameraControls = ({ paused = false }: { paused?: boolean }) =
1255
1397
 
1256
1398
  return (
1257
1399
  <CameraControls
1400
+ azimuthRotateSpeed={ORBIT_ROTATE_SPEED}
1258
1401
  makeDefault
1259
- maxDistance={100}
1402
+ maxDistance={maxDistance}
1403
+ impl={FloorAwareCameraControls}
1260
1404
  maxPolarAngle={maxPolarAngle}
1261
1405
  minDistance={minDistance}
1262
1406
  minPolarAngle={0}
@@ -1265,6 +1409,7 @@ export const CustomCameraControls = ({ paused = false }: { paused?: boolean }) =
1265
1409
  onControlStart={handleControlStart}
1266
1410
  onUpdate={handleCameraUpdate}
1267
1411
  onRest={onRest}
1412
+ polarRotateSpeed={ORBIT_ROTATE_SPEED}
1268
1413
  onSleep={onRest}
1269
1414
  onTransitionStart={onTransitionStart}
1270
1415
  ref={controls}