@pascal-app/editor 0.8.0 → 0.9.2

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 (350) hide show
  1. package/package.json +15 -13
  2. package/src/components/editor/alignment-3d-guide-layer.tsx +168 -0
  3. package/src/components/editor/bake-exporter.tsx +47 -0
  4. package/src/components/editor/custom-camera-controls.tsx +795 -28
  5. package/src/components/editor/editor-layout-mobile.tsx +8 -3
  6. package/src/components/editor/editor-layout-v2.tsx +76 -51
  7. package/src/components/editor/export-manager.tsx +69 -32
  8. package/src/components/editor/fence-tangent-lines-3d.tsx +87 -0
  9. package/src/components/editor/first-person/build-collider-world.test.ts +182 -0
  10. package/src/components/editor/first-person/build-collider-world.ts +228 -83
  11. package/src/components/editor/first-person-controls.tsx +943 -27
  12. package/src/components/editor/floating-action-menu.tsx +676 -208
  13. package/src/components/editor/floating-building-action-menu.tsx +2 -2
  14. package/src/components/editor/floorplan-background-selection.test.ts +58 -0
  15. package/src/components/editor/floorplan-background-selection.ts +3 -2
  16. package/src/components/editor/floorplan-panel.tsx +6916 -12137
  17. package/src/components/editor/grid.tsx +197 -36
  18. package/src/components/editor/group-actions.ts +457 -0
  19. package/src/components/editor/group-floating-action-menu.tsx +130 -0
  20. package/src/components/editor/group-move-3d.ts +397 -0
  21. package/src/components/editor/group-rotate-handle.tsx +420 -0
  22. package/src/components/editor/group-selection-box-3d.tsx +173 -0
  23. package/src/components/editor/group-transform-shared.test.ts +396 -0
  24. package/src/components/editor/group-transform-shared.ts +489 -0
  25. package/src/components/editor/handles/handle-arrow.tsx +581 -0
  26. package/src/components/editor/handles/use-handle-drag.ts +246 -0
  27. package/src/components/editor/index.tsx +412 -162
  28. package/src/components/editor/measurement-pill.tsx +115 -0
  29. package/src/components/editor/node-action-menu.tsx +14 -1
  30. package/src/components/editor/node-arrow-handles.tsx +1646 -0
  31. package/src/components/editor/opening-guides-3d-layer.tsx +144 -0
  32. package/src/components/editor/quick-measurement-card.tsx +70 -0
  33. package/src/components/editor/quick-measurement-hud.tsx +28 -0
  34. package/src/components/editor/riser-diagram-panel.tsx +137 -0
  35. package/src/components/editor/selection-manager.tsx +1106 -483
  36. package/src/components/editor/site-edge-labels.tsx +45 -21
  37. package/src/components/editor/slab-hole-highlights.tsx +474 -0
  38. package/src/components/editor/snapshot-capture-overlay.tsx +369 -155
  39. package/src/components/editor/three-context-bridge.ts +22 -0
  40. package/src/components/editor/thumbnail-generator.tsx +140 -41
  41. package/src/components/editor/use-floorplan-background-placement.ts +190 -35
  42. package/src/components/editor/use-floorplan-hit-testing.ts +24 -0
  43. package/src/components/editor/use-floorplan-scene-data.ts +19 -7
  44. package/src/components/editor/use-mesh-settle-epoch.ts +26 -0
  45. package/src/components/editor/wall-measurement-label.tsx +14 -19
  46. package/src/components/editor/wall-move-side-handles.tsx +870 -0
  47. package/src/components/editor/wall-opening-highlights.tsx +181 -0
  48. package/src/components/editor/wall-snap-beacon-layer.tsx +304 -0
  49. package/src/components/editor-2d/floorplan-action-menu-layer.tsx +15 -6
  50. package/src/components/editor-2d/floorplan-alignment-guide-layer.tsx +142 -0
  51. package/src/components/editor-2d/floorplan-cursor-indicator-overlay.tsx +5 -3
  52. package/src/components/editor-2d/floorplan-group-action-menu.tsx +87 -0
  53. package/src/components/editor-2d/floorplan-group-move.tsx +701 -0
  54. package/src/components/editor-2d/floorplan-measurement-tool-layer.test.ts +113 -0
  55. package/src/components/editor-2d/floorplan-measurement-tool-layer.tsx +1656 -0
  56. package/src/components/editor-2d/floorplan-quick-measure-layer.tsx +212 -0
  57. package/src/components/editor-2d/floorplan-registry-action-menu.tsx +392 -0
  58. package/src/components/editor-2d/floorplan-registry-move-overlay.tsx +857 -0
  59. package/src/components/editor-2d/floorplan-render-context.tsx +62 -0
  60. package/src/components/editor-2d/floorplan-snap-beacon-layer.tsx +77 -0
  61. package/src/components/editor-2d/floorplan-wall-move-ghost-layer.tsx +47 -0
  62. package/src/components/editor-2d/renderers/floorplan-geometry-renderer.tsx +266 -0
  63. package/src/components/editor-2d/renderers/floorplan-label-angle.test.ts +15 -0
  64. package/src/components/editor-2d/renderers/floorplan-label-angle.ts +14 -0
  65. package/src/components/editor-2d/renderers/floorplan-measurements-layer.tsx +108 -66
  66. package/src/components/editor-2d/renderers/floorplan-placement-preview-layer.tsx +62 -0
  67. package/src/components/editor-2d/renderers/floorplan-registry-layer.test.ts +308 -0
  68. package/src/components/editor-2d/renderers/floorplan-registry-layer.tsx +3325 -0
  69. package/src/components/editor-2d/renderers/floorplan-stair-layer.tsx +24 -9
  70. package/src/components/editor-2d/renderers/floorplan-voronoi-layer.tsx +128 -0
  71. package/src/components/editor-2d/svg-paths.ts +10 -2
  72. package/src/components/systems/ceiling/ceiling-selection-affordance-system.tsx +439 -57
  73. package/src/components/systems/ceiling/ceiling-system.tsx +109 -12
  74. package/src/components/systems/roof/roof-edit-system.tsx +1851 -22
  75. package/src/components/systems/selection-affordance-manager.tsx +38 -0
  76. package/src/components/systems/stair/stair-edit-system.tsx +1 -1
  77. package/src/components/systems/zone/zone-label-editor-system.tsx +14 -1
  78. package/src/components/systems/zone/zone-system.tsx +42 -13
  79. package/src/components/tools/elevator/elevator-defaults.ts +8 -0
  80. package/src/components/tools/elevator/elevator-tool.tsx +375 -0
  81. package/src/components/tools/elevator/move-elevator-tool.tsx +263 -0
  82. package/src/components/tools/fence/fence-drafting.ts +141 -27
  83. package/src/components/tools/item/move-tool.tsx +45 -105
  84. package/src/components/tools/item/placement-math.test.ts +22 -0
  85. package/src/components/tools/item/placement-math.ts +59 -7
  86. package/src/components/tools/item/placement-strategies.ts +432 -16
  87. package/src/components/tools/item/placement-types.ts +22 -1
  88. package/src/components/tools/item/use-draft-node.ts +51 -2
  89. package/src/components/tools/item/use-placement-coordinator.tsx +1399 -256
  90. package/src/components/tools/registry/move-registry-node-tool.tsx +1064 -0
  91. package/src/components/tools/roof/roof-tool.tsx +350 -28
  92. package/src/components/tools/select/box-select-state.ts +88 -0
  93. package/src/components/tools/select/box-select-tool.tsx +442 -520
  94. package/src/components/tools/select/marquee-geometry.test.ts +161 -0
  95. package/src/components/tools/select/marquee-geometry.ts +155 -0
  96. package/src/components/tools/select/plane-box-select-tool.tsx +570 -0
  97. package/src/components/tools/select/screen-rectangle-selection.ts +84 -0
  98. package/src/components/tools/select/select-candidates.test.ts +109 -0
  99. package/src/components/tools/select/select-candidates.ts +85 -0
  100. package/src/components/tools/shared/affordance-dispatch.ts +30 -0
  101. package/src/components/tools/shared/cursor-sphere.tsx +62 -26
  102. package/src/components/tools/shared/drag-bounding-box.tsx +195 -0
  103. package/src/components/tools/shared/facing-indicator.tsx +87 -0
  104. package/src/components/tools/shared/facing-pose-indicator.tsx +54 -0
  105. package/src/components/tools/shared/floor-stack-preview.ts +25 -0
  106. package/src/components/tools/shared/fresh-placement-visibility.ts +61 -0
  107. package/src/components/tools/shared/placement-box-geometry.ts +123 -0
  108. package/src/components/tools/shared/placement-box.tsx +305 -0
  109. package/src/components/tools/shared/polygon-editor.tsx +866 -175
  110. package/src/components/tools/shared/segment-angle.ts +56 -0
  111. package/src/components/tools/site/site-boundary-editor.tsx +520 -16
  112. package/src/components/tools/site/site-flag-model.ts +18 -0
  113. package/src/components/tools/stair/stair-defaults.ts +2 -1
  114. package/src/components/tools/stair/stair-tool.tsx +342 -37
  115. package/src/components/tools/tool-manager.tsx +239 -62
  116. package/src/components/tools/wall/wall-drafting.test.ts +330 -0
  117. package/src/components/tools/wall/wall-drafting.ts +248 -171
  118. package/src/components/tools/wall/wall-snap-geometry.test.ts +123 -0
  119. package/src/components/tools/wall/wall-snap-geometry.ts +331 -0
  120. package/src/components/tools/zone/zone-boundary-editor.tsx +5 -1
  121. package/src/components/tools/zone/zone-tool.tsx +86 -82
  122. package/src/components/ui/action-menu/action-button.tsx +21 -1
  123. package/src/components/ui/action-menu/camera-actions.tsx +24 -17
  124. package/src/components/ui/action-menu/control-modes.tsx +40 -170
  125. package/src/components/ui/action-menu/furnish-tools.tsx +5 -5
  126. package/src/components/ui/action-menu/index.tsx +3 -117
  127. package/src/components/ui/action-menu/measurement-control.tsx +188 -0
  128. package/src/components/ui/action-menu/structure-tools.tsx +27 -93
  129. package/src/components/ui/action-menu/view-toggles.tsx +279 -90
  130. package/src/components/ui/command-palette/editor-commands.tsx +20 -15
  131. package/src/components/ui/command-palette/index.tsx +5 -3
  132. package/src/components/ui/controls/material-paint-panel.tsx +152 -0
  133. package/src/components/ui/controls/material-picker.tsx +87 -177
  134. package/src/components/ui/controls/material-properties-editor.tsx +108 -0
  135. package/src/components/ui/controls/metric-control.tsx +142 -46
  136. package/src/components/ui/controls/scene-material-list.tsx +247 -0
  137. package/src/components/ui/controls/slider-control.tsx +88 -38
  138. package/src/components/ui/floating-level-selector.tsx +47 -10
  139. package/src/components/ui/helpers/building-helper.tsx +10 -23
  140. package/src/components/ui/helpers/contextual-helper-panel.tsx +428 -0
  141. package/src/components/ui/helpers/helper-manager.tsx +235 -23
  142. package/src/components/ui/helpers/item-helper.tsx +28 -32
  143. package/src/components/ui/helpers/registered-tool-helper.tsx +59 -0
  144. package/src/components/ui/helpers/roof-helper.tsx +10 -12
  145. package/src/components/ui/icon-ref.tsx +47 -0
  146. package/src/components/ui/item-catalog/catalog-items.tsx +40 -1
  147. package/src/components/ui/item-catalog/item-catalog.tsx +16 -36
  148. package/src/components/ui/level-duplicate-dialog.tsx +2 -1
  149. package/src/components/ui/panels/node-display.ts +17 -16
  150. package/src/components/ui/panels/panel-manager.tsx +55 -58
  151. package/src/components/ui/panels/panel-wrapper.tsx +238 -12
  152. package/src/components/ui/panels/parametric-inspector.tsx +482 -0
  153. package/src/components/ui/panels/reference-panel.tsx +54 -19
  154. package/src/components/ui/primitives/dropdown-menu.tsx +12 -8
  155. package/src/components/ui/primitives/shortcut-token.tsx +39 -3
  156. package/src/components/ui/primitives/sidebar.tsx +1 -0
  157. package/src/components/ui/scene-loader.tsx +1 -3
  158. package/src/components/ui/sidebar/app-sidebar.tsx +5 -1
  159. package/src/components/ui/sidebar/icon-rail.tsx +50 -31
  160. package/src/components/ui/sidebar/panels/items-panel/function-tree-panel.tsx +263 -0
  161. package/src/components/ui/sidebar/panels/items-panel/index.tsx +82 -8
  162. package/src/components/ui/sidebar/panels/plugins-panel.tsx +218 -0
  163. package/src/components/ui/sidebar/panels/settings-panel/index.tsx +122 -41
  164. package/src/components/ui/sidebar/panels/settings-panel/keyboard-shortcuts-dialog.tsx +66 -7
  165. package/src/components/ui/sidebar/panels/settings-panel/load-build-dialog.tsx +267 -0
  166. package/src/components/ui/sidebar/panels/site-panel/building-tree-node.tsx +3 -1
  167. package/src/components/ui/sidebar/panels/site-panel/ceiling-tree-node.tsx +9 -5
  168. package/src/components/ui/sidebar/panels/site-panel/chimney-tree-node.tsx +86 -0
  169. package/src/components/ui/sidebar/panels/site-panel/column-tree-node.tsx +1 -1
  170. package/src/components/ui/sidebar/panels/site-panel/door-tree-node.tsx +7 -2
  171. package/src/components/ui/sidebar/panels/site-panel/dormer-tree-node.tsx +92 -0
  172. package/src/components/ui/sidebar/panels/site-panel/elevator-tree-node.tsx +75 -0
  173. package/src/components/ui/sidebar/panels/site-panel/fence-tree-node.tsx +3 -3
  174. package/src/components/ui/sidebar/panels/site-panel/gutter-tree-node.tsx +86 -0
  175. package/src/components/ui/sidebar/panels/site-panel/index.tsx +42 -21
  176. package/src/components/ui/sidebar/panels/site-panel/item-tree-node.tsx +31 -17
  177. package/src/components/ui/sidebar/panels/site-panel/level-tree-node.tsx +2 -1
  178. package/src/components/ui/sidebar/panels/site-panel/registry-tree-node.tsx +140 -0
  179. package/src/components/ui/sidebar/panels/site-panel/roof-tree-node.tsx +48 -8
  180. package/src/components/ui/sidebar/panels/site-panel/shelf-tree-node.tsx +132 -0
  181. package/src/components/ui/sidebar/panels/site-panel/slab-tree-node.tsx +9 -5
  182. package/src/components/ui/sidebar/panels/site-panel/solar-panel-tree-node.tsx +86 -0
  183. package/src/components/ui/sidebar/panels/site-panel/spawn-tree-node.tsx +7 -1
  184. package/src/components/ui/sidebar/panels/site-panel/stair-tree-node.tsx +2 -2
  185. package/src/components/ui/sidebar/panels/site-panel/tree-node.tsx +112 -39
  186. package/src/components/ui/sidebar/panels/site-panel/tree-structure.ts +36 -0
  187. package/src/components/ui/sidebar/panels/site-panel/wall-tree-node.tsx +1 -1
  188. package/src/components/ui/sidebar/panels/site-panel/window-tree-node.tsx +7 -2
  189. package/src/components/ui/sidebar/panels/site-panel/zone-tree-node.tsx +3 -3
  190. package/src/components/ui/sidebar/panels/zone-panel/index.tsx +51 -2
  191. package/src/components/ui/sidebar/tab-bar.tsx +75 -1
  192. package/src/components/ui/sidebar/use-plugin-panels.tsx +132 -0
  193. package/src/components/ui/snap-target-badge.test.tsx +40 -0
  194. package/src/components/ui/snap-target-badge.tsx +88 -0
  195. package/src/components/viewer-overlay.tsx +253 -61
  196. package/src/components/viewer-zone-system.tsx +7 -2
  197. package/src/hooks/use-auto-save.test.ts +14 -0
  198. package/src/hooks/use-auto-save.ts +64 -10
  199. package/src/hooks/use-ceiling-events.ts +176 -0
  200. package/src/hooks/use-drag-action.ts +127 -0
  201. package/src/hooks/use-keyboard.ts +493 -95
  202. package/src/hooks/use-selection.ts +64 -0
  203. package/src/index.tsx +451 -6
  204. package/src/lib/active-placement-surface.ts +35 -0
  205. package/src/lib/ceiling-plan-snap.ts +24 -0
  206. package/src/lib/constants.ts +6 -2
  207. package/src/lib/contextual-help.test.ts +112 -0
  208. package/src/lib/contextual-help.ts +144 -0
  209. package/src/lib/continuation.ts +64 -0
  210. package/src/lib/direct-manipulation.test.ts +192 -0
  211. package/src/lib/direct-manipulation.ts +109 -0
  212. package/src/lib/editor-api.ts +50 -0
  213. package/src/lib/elevator-support.ts +96 -0
  214. package/src/lib/floorplan/apply-alignment.test.ts +25 -0
  215. package/src/lib/floorplan/apply-alignment.ts +117 -0
  216. package/src/lib/floorplan/floorplan-export.tsx +364 -0
  217. package/src/lib/floorplan/geometry.ts +53 -0
  218. package/src/lib/floorplan/index.ts +10 -0
  219. package/src/lib/floorplan/items.ts +5 -2
  220. package/src/lib/floorplan/plan-coords.ts +21 -0
  221. package/src/lib/floorplan/selection-tool.ts +40 -0
  222. package/src/lib/floorplan/stairs.ts +12 -1
  223. package/src/lib/fresh-planar-placement.test.ts +339 -0
  224. package/src/lib/fresh-planar-placement.ts +130 -0
  225. package/src/lib/glb-export.test.ts +441 -0
  226. package/src/lib/glb-export.ts +874 -0
  227. package/src/lib/history.ts +11 -1
  228. package/src/lib/interaction/hot-set.test.ts +133 -0
  229. package/src/lib/interaction/hot-set.ts +67 -0
  230. package/src/lib/interaction/overlay-policy.test.ts +51 -0
  231. package/src/lib/interaction/overlay-policy.ts +59 -0
  232. package/src/lib/interaction/scope.ts +211 -0
  233. package/src/lib/level-duplication.ts +54 -42
  234. package/src/lib/level-selection.ts +2 -2
  235. package/src/lib/material-paint.ts +227 -48
  236. package/src/lib/measurement-kind.test.ts +30 -0
  237. package/src/lib/measurement-kind.ts +19 -0
  238. package/src/lib/measurement-label.test.ts +47 -0
  239. package/src/lib/measurement-label.ts +68 -0
  240. package/src/lib/measurement-parser.ts +116 -0
  241. package/src/lib/measurements.test.ts +183 -0
  242. package/src/lib/measurements.ts +199 -0
  243. package/src/lib/paint-scope.test.ts +454 -0
  244. package/src/lib/paint-scope.ts +461 -0
  245. package/src/lib/placement-drag-release.ts +23 -0
  246. package/src/lib/placement-metadata.ts +29 -0
  247. package/src/lib/planar-cursor-placement.test.ts +100 -0
  248. package/src/lib/planar-cursor-placement.ts +42 -0
  249. package/src/lib/plugin-panels.test.ts +19 -0
  250. package/src/lib/plugin-panels.ts +91 -0
  251. package/src/lib/quick-measurement.test.ts +77 -0
  252. package/src/lib/quick-measurement.ts +109 -0
  253. package/src/lib/roof-duplication.ts +7 -7
  254. package/src/lib/roof-hover-outline-proxy.ts +22 -0
  255. package/src/lib/roof-wall-hit.ts +164 -0
  256. package/src/lib/scene-clipboard.test.ts +196 -0
  257. package/src/lib/scene-clipboard.ts +332 -0
  258. package/src/lib/scene.ts +40 -8
  259. package/src/lib/selection-routing.test.ts +298 -0
  260. package/src/lib/selection-routing.ts +175 -0
  261. package/src/lib/sfx/index.ts +1 -0
  262. package/src/lib/sfx/movement-tick.test.ts +65 -0
  263. package/src/lib/sfx/movement-tick.ts +18 -0
  264. package/src/lib/sfx-bus.ts +69 -10
  265. package/src/lib/sfx-player.test.ts +124 -0
  266. package/src/lib/sfx-player.ts +148 -55
  267. package/src/lib/site-boundary.ts +1 -0
  268. package/src/lib/slab-plan-snap.test.ts +93 -0
  269. package/src/lib/slab-plan-snap.ts +108 -0
  270. package/src/lib/snapping-mode.test.ts +138 -0
  271. package/src/lib/snapping-mode.ts +179 -0
  272. package/src/lib/stair-duplication.ts +4 -4
  273. package/src/lib/stair-levels.test.ts +146 -0
  274. package/src/lib/stair-levels.ts +177 -0
  275. package/src/lib/surface-plan-snap.test.ts +71 -0
  276. package/src/lib/surface-plan-snap.ts +256 -0
  277. package/src/lib/use-linear-display.ts +40 -0
  278. package/src/lib/world-grid-snap.ts +231 -0
  279. package/src/lib/zone-content.ts +130 -0
  280. package/src/store/use-alignment-guides.ts +21 -0
  281. package/src/store/use-direct-manipulation-feedback.ts +20 -0
  282. package/src/store/use-editor.tsx +767 -114
  283. package/src/store/use-facing-pose.ts +44 -0
  284. package/src/store/use-fence-curve-draft.ts +21 -0
  285. package/src/store/use-floorplan-draft-preview.ts +101 -0
  286. package/src/store/use-floorplan-marquee.ts +50 -0
  287. package/src/store/use-interaction-scope.test.ts +186 -0
  288. package/src/store/use-interaction-scope.ts +132 -0
  289. package/src/store/use-measurement-draft.test.ts +530 -0
  290. package/src/store/use-measurement-draft.ts +543 -0
  291. package/src/store/use-opening-guides.ts +41 -0
  292. package/src/store/use-placement-preview.ts +39 -0
  293. package/src/store/use-quick-measurement-hud.test.ts +53 -0
  294. package/src/store/use-quick-measurement-hud.ts +77 -0
  295. package/src/store/use-segment-draft-chain.ts +28 -0
  296. package/src/store/use-stair-build-preview.ts +43 -0
  297. package/src/store/use-wall-move-ghosts.ts +36 -0
  298. package/src/store/use-wall-snap-indicator.ts +34 -0
  299. package/src/components/editor/first-person/bvh-ecctrl.tsx +0 -860
  300. package/src/components/editor/preset-thumbnail-generator.tsx +0 -125
  301. package/src/components/editor-2d/renderers/floorplan-roof-layer.tsx +0 -113
  302. package/src/components/tools/building/move-building-tool.tsx +0 -157
  303. package/src/components/tools/ceiling/ceiling-boundary-editor.tsx +0 -43
  304. package/src/components/tools/ceiling/ceiling-hole-editor.tsx +0 -49
  305. package/src/components/tools/ceiling/ceiling-tool.tsx +0 -465
  306. package/src/components/tools/ceiling/move-ceiling-tool.tsx +0 -264
  307. package/src/components/tools/column/column-tool.tsx +0 -97
  308. package/src/components/tools/column/move-column-tool.tsx +0 -105
  309. package/src/components/tools/door/door-math.ts +0 -110
  310. package/src/components/tools/door/door-tool.tsx +0 -324
  311. package/src/components/tools/door/move-door-tool.tsx +0 -412
  312. package/src/components/tools/fence/curve-fence-tool.tsx +0 -178
  313. package/src/components/tools/fence/fence-tool.tsx +0 -346
  314. package/src/components/tools/fence/move-fence-endpoint-tool.tsx +0 -441
  315. package/src/components/tools/fence/move-fence-tool.tsx +0 -302
  316. package/src/components/tools/item/item-tool.tsx +0 -26
  317. package/src/components/tools/roof/move-roof-tool.tsx +0 -364
  318. package/src/components/tools/slab/move-slab-tool.tsx +0 -182
  319. package/src/components/tools/slab/slab-boundary-editor.tsx +0 -43
  320. package/src/components/tools/slab/slab-hole-editor.tsx +0 -49
  321. package/src/components/tools/slab/slab-tool.tsx +0 -322
  322. package/src/components/tools/spawn/move-spawn-tool.tsx +0 -101
  323. package/src/components/tools/spawn/spawn-tool.tsx +0 -130
  324. package/src/components/tools/wall/curve-wall-tool.tsx +0 -178
  325. package/src/components/tools/wall/move-wall-endpoint-tool.tsx +0 -426
  326. package/src/components/tools/wall/move-wall-tool.tsx +0 -358
  327. package/src/components/tools/wall/wall-tool.tsx +0 -332
  328. package/src/components/tools/window/move-window-tool.tsx +0 -447
  329. package/src/components/tools/window/window-math.ts +0 -117
  330. package/src/components/tools/window/window-tool.tsx +0 -332
  331. package/src/components/ui/helpers/ceiling-helper.tsx +0 -20
  332. package/src/components/ui/helpers/slab-helper.tsx +0 -20
  333. package/src/components/ui/helpers/wall-helper.tsx +0 -20
  334. package/src/components/ui/panels/ceiling-panel.tsx +0 -249
  335. package/src/components/ui/panels/column-panel.tsx +0 -759
  336. package/src/components/ui/panels/door-panel.tsx +0 -1299
  337. package/src/components/ui/panels/fence-panel.tsx +0 -222
  338. package/src/components/ui/panels/item-panel.tsx +0 -306
  339. package/src/components/ui/panels/paint-panel.tsx +0 -163
  340. package/src/components/ui/panels/presets/presets-popover.tsx +0 -511
  341. package/src/components/ui/panels/roof-panel.tsx +0 -280
  342. package/src/components/ui/panels/roof-segment-panel.tsx +0 -311
  343. package/src/components/ui/panels/slab-panel.tsx +0 -250
  344. package/src/components/ui/panels/spawn-panel.tsx +0 -161
  345. package/src/components/ui/panels/stair-panel.tsx +0 -577
  346. package/src/components/ui/panels/stair-segment-panel.tsx +0 -325
  347. package/src/components/ui/panels/wall-panel.tsx +0 -273
  348. package/src/components/ui/panels/window-panel.tsx +0 -968
  349. package/src/contexts/presets-context.tsx +0 -121
  350. package/src/hooks/use-contextual-tools.ts +0 -61
@@ -1,19 +1,32 @@
1
1
  'use client'
2
2
 
3
3
  import {
4
+ type AnyNodeId,
4
5
  type CameraControlEvent,
5
6
  type CameraControlFitSceneEvent,
6
7
  emitter,
7
8
  sceneRegistry,
8
9
  useScene,
9
10
  } from '@pascal-app/core'
10
- import { useViewer, WalkthroughControls, ZONE_LAYER } from '@pascal-app/viewer'
11
+ import { GRID_LAYER, useViewer, ZONE_LAYER } from '@pascal-app/viewer'
11
12
  import { CameraControls, CameraControlsImpl } from '@react-three/drei'
12
- import { useThree } from '@react-three/fiber'
13
+ import { useFrame, useThree } from '@react-three/fiber'
13
14
  import { useCallback, useEffect, useMemo, useRef } from 'react'
14
- import { Box3, Vector3 } from 'three'
15
+ import {
16
+ Box3,
17
+ type Camera,
18
+ type OrthographicCamera,
19
+ type PerspectiveCamera,
20
+ Spherical,
21
+ Vector3,
22
+ } from 'three'
15
23
  import { EDITOR_LAYER } from '../../lib/constants'
16
24
  import useEditor from '../../store/use-editor'
25
+ import {
26
+ useActiveHandleDrag,
27
+ useEndpointReshape,
28
+ useMovingNode,
29
+ } from '../../store/use-interaction-scope'
17
30
 
18
31
  const currentTarget = new Vector3()
19
32
  const tempBox = new Box3()
@@ -22,30 +35,378 @@ const tempDelta = new Vector3()
22
35
  const tempPosition = new Vector3()
23
36
  const tempSize = new Vector3()
24
37
  const tempTarget = new Vector3()
38
+ const syncTarget = new Vector3()
39
+ const syncSpherical = new Spherical()
40
+ const keyboardPanSpherical = new Spherical()
25
41
  const DEFAULT_MAX_POLAR_ANGLE = Math.PI / 2 - 0.1
26
42
  const DEBUG_MAX_POLAR_ANGLE = Math.PI - 0.05
43
+ const NAVIGATION_SYNC_POSITION_EPSILON = 0.001
44
+ const NAVIGATION_SYNC_AZIMUTH_EPSILON = 0.0005
45
+ const NAVIGATION_SYNC_VIEW_WIDTH_EPSILON = 0.001
46
+ const KEYBOARD_PAN_VIEW_WIDTH_PER_SECOND = 0.65
47
+ const KEYBOARD_PAN_MIN_SPEED = 2
48
+ const KEYBOARD_PAN_MAX_SPEED = 55
49
+ type CameraMode = ReturnType<typeof useViewer.getState>['cameraMode']
50
+ type CameraPoseSnapshot = {
51
+ mode: CameraMode
52
+ position: [number, number, number]
53
+ target: [number, number, number]
54
+ }
55
+ type NavigationCameraPoseSnapshot = {
56
+ target: [number, number, number]
57
+ azimuth: number
58
+ viewWidth: number
59
+ }
60
+ type PendingNavigationCameraPoseSnapshot = NavigationCameraPoseSnapshot & {
61
+ publishOnComplete: boolean
62
+ }
63
+ type CameraViewWidthUpdate =
64
+ | { type: 'distance'; distance: number; viewWidth: number }
65
+ | { type: 'zoom'; viewWidth: number; zoom: number }
66
+ | { type: 'none'; viewWidth: number }
67
+
68
+ function writeVectorTuple(tuple: [number, number, number], vector: Vector3) {
69
+ tuple[0] = vector.x
70
+ tuple[1] = vector.y
71
+ tuple[2] = vector.z
72
+ }
73
+
74
+ function saveCameraPose(
75
+ control: CameraControlsImpl,
76
+ mode: CameraMode,
77
+ pose: CameraPoseSnapshot,
78
+ position: Vector3,
79
+ target: Vector3,
80
+ ) {
81
+ control.getPosition(position)
82
+ control.getTarget(target)
83
+ pose.mode = mode
84
+ writeVectorTuple(pose.position, position)
85
+ writeVectorTuple(pose.target, target)
86
+ }
87
+
88
+ function restoreCameraPose(control: CameraControlsImpl, pose: CameraPoseSnapshot) {
89
+ control.setLookAt(
90
+ pose.position[0],
91
+ pose.position[1],
92
+ pose.position[2],
93
+ pose.target[0],
94
+ pose.target[1],
95
+ pose.target[2],
96
+ false,
97
+ )
98
+ }
99
+
100
+ function isEditableKeyboardTarget(target: EventTarget | null) {
101
+ return (
102
+ target instanceof HTMLInputElement ||
103
+ target instanceof HTMLTextAreaElement ||
104
+ target instanceof HTMLSelectElement ||
105
+ (target instanceof HTMLElement && target.isContentEditable)
106
+ )
107
+ }
108
+
109
+ type KeyboardPanState = {
110
+ forward: boolean
111
+ backward: boolean
112
+ left: boolean
113
+ right: boolean
114
+ }
115
+
116
+ function setKeyboardPanKey(state: KeyboardPanState, code: string, pressed: boolean): boolean {
117
+ if (code === 'KeyW') {
118
+ const changed = state.forward !== pressed
119
+ state.forward = pressed
120
+ return changed
121
+ }
122
+ if (code === 'KeyS') {
123
+ const changed = state.backward !== pressed
124
+ state.backward = pressed
125
+ return changed
126
+ }
127
+ if (code === 'KeyA') {
128
+ const changed = state.left !== pressed
129
+ state.left = pressed
130
+ return changed
131
+ }
132
+ if (code === 'KeyD') {
133
+ const changed = state.right !== pressed
134
+ state.right = pressed
135
+ return changed
136
+ }
137
+ return false
138
+ }
139
+
140
+ function isKeyboardPanKey(code: string): boolean {
141
+ return code === 'KeyW' || code === 'KeyA' || code === 'KeyS' || code === 'KeyD'
142
+ }
143
+
144
+ type CameraViewportSize = {
145
+ width: number
146
+ height: number
147
+ }
148
+
149
+ function isPerspectiveCamera(camera: Camera): camera is PerspectiveCamera {
150
+ return (camera as PerspectiveCamera).isPerspectiveCamera === true
151
+ }
152
+
153
+ function isOrthographicCamera(camera: Camera): camera is OrthographicCamera {
154
+ return (camera as OrthographicCamera).isOrthographicCamera === true
155
+ }
156
+
157
+ function getCameraViewAspect(size: CameraViewportSize) {
158
+ return Math.max(size.width, 1) / Math.max(size.height, 1)
159
+ }
160
+
161
+ function getCameraViewWidth(camera: Camera, distance: number, size: CameraViewportSize) {
162
+ if (isPerspectiveCamera(camera)) {
163
+ const fovRadians = (camera.getEffectiveFOV() * Math.PI) / 180
164
+ return Math.max(0.001, 2 * distance * Math.tan(fovRadians / 2) * getCameraViewAspect(size))
165
+ }
166
+
167
+ if (isOrthographicCamera(camera)) {
168
+ return Math.max(0.001, (camera.right - camera.left) / camera.zoom)
169
+ }
170
+
171
+ return Math.max(0.001, distance)
172
+ }
173
+
174
+ function getAngleDeltaRadians(a: number, b: number) {
175
+ return Math.atan2(Math.sin(a - b), Math.cos(a - b))
176
+ }
177
+
178
+ function nearestEquivalentRadians(angle: number, reference: number) {
179
+ return reference + getAngleDeltaRadians(angle, reference)
180
+ }
181
+
182
+ function clampFinite(value: number, min: number, max: number) {
183
+ const resolvedMin = Number.isFinite(min) ? min : Number.NEGATIVE_INFINITY
184
+ const resolvedMax = Number.isFinite(max) ? max : Number.POSITIVE_INFINITY
185
+ return Math.min(Math.max(value, resolvedMin), resolvedMax)
186
+ }
187
+
188
+ function clampCameraControlDistance(control: CameraControlsImpl, distance: number) {
189
+ const bounds = control as { minDistance?: number; maxDistance?: number }
190
+ return clampFinite(
191
+ distance,
192
+ bounds.minDistance ?? Number.NEGATIVE_INFINITY,
193
+ bounds.maxDistance ?? Number.POSITIVE_INFINITY,
194
+ )
195
+ }
196
+
197
+ function clampCameraControlZoom(control: CameraControlsImpl, zoom: number) {
198
+ const bounds = control as { minZoom?: number; maxZoom?: number }
199
+ return clampFinite(
200
+ zoom,
201
+ bounds.minZoom ?? Number.NEGATIVE_INFINITY,
202
+ bounds.maxZoom ?? Number.POSITIVE_INFINITY,
203
+ )
204
+ }
205
+
206
+ function isCameraAtNavigationPose(
207
+ pose: NavigationCameraPoseSnapshot,
208
+ target: Vector3,
209
+ azimuth: number,
210
+ viewWidth: number,
211
+ ) {
212
+ return (
213
+ Math.abs(pose.target[0] - target.x) < NAVIGATION_SYNC_POSITION_EPSILON &&
214
+ Math.abs(pose.target[1] - target.y) < NAVIGATION_SYNC_POSITION_EPSILON &&
215
+ Math.abs(pose.target[2] - target.z) < NAVIGATION_SYNC_POSITION_EPSILON &&
216
+ Math.abs(getAngleDeltaRadians(pose.azimuth, azimuth)) < NAVIGATION_SYNC_AZIMUTH_EPSILON &&
217
+ Math.abs(pose.viewWidth - viewWidth) < NAVIGATION_SYNC_VIEW_WIDTH_EPSILON
218
+ )
219
+ }
220
+
221
+ function getCameraDistanceForViewWidth(
222
+ camera: Camera,
223
+ viewWidth: number,
224
+ size: CameraViewportSize,
225
+ ) {
226
+ if (!isPerspectiveCamera(camera)) {
227
+ return null
228
+ }
229
+
230
+ const fovRadians = (camera.getEffectiveFOV() * Math.PI) / 180
231
+ const denominator = 2 * Math.tan(fovRadians / 2) * getCameraViewAspect(size)
232
+
233
+ return denominator > 0 ? Math.max(0.001, viewWidth / denominator) : null
234
+ }
235
+
236
+ function getCameraZoomForViewWidth(camera: Camera, viewWidth: number) {
237
+ if (!isOrthographicCamera(camera)) {
238
+ return null
239
+ }
240
+
241
+ return viewWidth > 0 ? Math.max(0.001, (camera.right - camera.left) / viewWidth) : null
242
+ }
243
+
244
+ function resolveCameraViewWidthUpdate(
245
+ control: CameraControlsImpl,
246
+ camera: Camera,
247
+ viewWidth: number,
248
+ size: CameraViewportSize,
249
+ ): CameraViewWidthUpdate {
250
+ const nextDistance = getCameraDistanceForViewWidth(camera, viewWidth, size)
251
+ if (nextDistance !== null) {
252
+ const appliedDistance = clampCameraControlDistance(control, nextDistance)
253
+ return {
254
+ type: 'distance',
255
+ distance: appliedDistance,
256
+ viewWidth: getCameraViewWidth(camera, appliedDistance, size),
257
+ }
258
+ }
259
+
260
+ const nextZoom = getCameraZoomForViewWidth(camera, viewWidth)
261
+ if (nextZoom !== null) {
262
+ const appliedZoom = clampCameraControlZoom(control, nextZoom)
263
+ if (isOrthographicCamera(camera)) {
264
+ return {
265
+ type: 'zoom',
266
+ zoom: appliedZoom,
267
+ viewWidth: Math.max(0.001, (camera.right - camera.left) / Math.max(appliedZoom, 0.001)),
268
+ }
269
+ }
270
+ }
271
+
272
+ return { type: 'none', viewWidth }
273
+ }
274
+
275
+ function applyCameraViewWidth(control: CameraControlsImpl, update: CameraViewWidthUpdate) {
276
+ if (update.type === 'distance') {
277
+ control.dollyTo(update.distance, true)
278
+ return
279
+ }
280
+
281
+ if (update.type === 'zoom') {
282
+ control.zoomTo(update.zoom, true)
283
+ }
284
+ }
285
+
286
+ function useFirstPersonCameraPoseRestore(
287
+ controls: { current: CameraControlsImpl | null },
288
+ isFirstPersonMode: boolean,
289
+ cameraMode: CameraMode,
290
+ ) {
291
+ const restorePose = useRef<CameraPoseSnapshot>({
292
+ mode: cameraMode,
293
+ position: [0, 0, 0],
294
+ target: [0, 0, 0],
295
+ })
296
+ const hasRestorePose = useRef(false)
297
+ const isRestoring = useRef(false)
298
+ const wasFirstPersonMode = useRef(isFirstPersonMode)
299
+ const snapshotPosition = useRef(new Vector3())
300
+ const snapshotTarget = useRef(new Vector3())
301
+
302
+ useFrame(() => {
303
+ if (isFirstPersonMode || isRestoring.current) return
304
+ const control = controls.current
305
+ if (!control) return
306
+
307
+ saveCameraPose(
308
+ control,
309
+ cameraMode,
310
+ restorePose.current,
311
+ snapshotPosition.current,
312
+ snapshotTarget.current,
313
+ )
314
+ hasRestorePose.current = true
315
+ })
316
+
317
+ useEffect(() => {
318
+ const wasFirstPerson = wasFirstPersonMode.current
319
+ wasFirstPersonMode.current = isFirstPersonMode
320
+
321
+ if (isFirstPersonMode) {
322
+ return
323
+ }
324
+
325
+ if (!wasFirstPerson || !hasRestorePose.current) return
326
+
327
+ const pose = restorePose.current
328
+ isRestoring.current = true
329
+ useViewer.getState().setCameraMode(pose.mode)
330
+
331
+ const restoreFrame = requestAnimationFrame(() => {
332
+ const currentControls = controls.current
333
+ if (currentControls) {
334
+ restoreCameraPose(currentControls, pose)
335
+ }
336
+ isRestoring.current = false
337
+ })
338
+
339
+ return () => {
340
+ cancelAnimationFrame(restoreFrame)
341
+ isRestoring.current = false
342
+ }
343
+ }, [controls, isFirstPersonMode])
344
+
345
+ return useCallback(() => isRestoring.current, [])
346
+ }
27
347
 
28
348
  export const CustomCameraControls = () => {
29
- const controls = useRef<CameraControlsImpl>(null!)
349
+ const controls = useRef<CameraControlsImpl | null>(null)
350
+ const keyboardPanKeys = useRef<KeyboardPanState>({
351
+ forward: false,
352
+ backward: false,
353
+ left: false,
354
+ right: false,
355
+ })
30
356
  const isPreviewMode = useEditor((s) => s.isPreviewMode)
31
357
  const isFirstPersonMode = useEditor((s) => s.isFirstPersonMode)
32
358
  const allowUndergroundCamera = useEditor((s) => s.allowUndergroundCamera)
359
+ const isFloorplanOpen = useEditor((s) => s.isFloorplanOpen)
33
360
  const selection = useViewer((s) => s.selection)
361
+ const cameraMode = useViewer((state) => state.cameraMode)
362
+ const isRestoringFirstPersonPose = useFirstPersonCameraPoseRestore(
363
+ controls,
364
+ isFirstPersonMode,
365
+ cameraMode,
366
+ )
34
367
  const currentLevelId = selection.levelId
35
368
  const firstLoad = useRef(true)
369
+ const lastPublishedNavigationSync = useRef<NavigationCameraPoseSnapshot | null>(null)
370
+ const pendingFloorplanNavigationPose = useRef<PendingNavigationCameraPoseSnapshot | null>(null)
371
+ const lastApplied2dNavigationRevision = useRef(0)
372
+ const savedSmoothTimeRef = useRef<number | null>(null)
36
373
  const maxPolarAngle =
37
374
  !isPreviewMode && allowUndergroundCamera ? DEBUG_MAX_POLAR_ANGLE : DEFAULT_MAX_POLAR_ANGLE
375
+ const clearPendingFloorplanNavigationPose = useCallback(() => {
376
+ pendingFloorplanNavigationPose.current = null
377
+ if (savedSmoothTimeRef.current !== null && controls.current) {
378
+ controls.current.smoothTime = savedSmoothTimeRef.current
379
+ savedSmoothTimeRef.current = null
380
+ }
381
+ }, [])
38
382
 
39
383
  const camera = useThree((state) => state.camera)
384
+ const gl = useThree((state) => state.gl)
40
385
  const raycaster = useThree((state) => state.raycaster)
386
+ const viewportSize = useThree((state) => state.size)
41
387
  useEffect(() => {
42
388
  camera.layers.enable(EDITOR_LAYER)
389
+ camera.layers.enable(GRID_LAYER)
43
390
  raycaster.layers.enable(EDITOR_LAYER)
44
391
  raycaster.layers.enable(ZONE_LAYER)
45
392
  }, [camera, raycaster])
46
393
 
47
394
  useEffect(() => {
48
- if (isPreviewMode) return // Preview mode uses auto-navigate instead
395
+ // Dev-only: deterministic camera poses for screenshot/automation tooling.
396
+ // A getter, not a snapshot — drei recreates the impl when the default
397
+ // camera changes, so a captured instance goes stale.
398
+ if (process.env.NODE_ENV !== 'development') return
399
+ const w = window as typeof window & {
400
+ __pascalCameraControls?: (() => CameraControlsImpl | null) | null
401
+ }
402
+ w.__pascalCameraControls = () => controls.current
403
+ return () => {
404
+ w.__pascalCameraControls = null
405
+ }
406
+ }, [])
407
+
408
+ useEffect(() => {
409
+ if (isPreviewMode || isFirstPersonMode || isRestoringFirstPersonPose()) return
49
410
  let targetY = 0
50
411
  if (currentLevelId) {
51
412
  const levelMesh = sceneRegistry.nodes.get(currentLevelId)
@@ -56,14 +417,22 @@ export const CustomCameraControls = () => {
56
417
  if (!controls.current) return
57
418
  if (firstLoad.current) {
58
419
  firstLoad.current = false
420
+ clearPendingFloorplanNavigationPose()
59
421
  controls.current.setLookAt(20, 20, 20, 0, 0, 0, true)
60
422
  }
61
423
  controls.current.getTarget(currentTarget)
424
+ clearPendingFloorplanNavigationPose()
62
425
  controls.current.moveTo(currentTarget.x, targetY, currentTarget.z, true)
63
- }, [currentLevelId, isPreviewMode])
426
+ }, [
427
+ clearPendingFloorplanNavigationPose,
428
+ currentLevelId,
429
+ isPreviewMode,
430
+ isFirstPersonMode,
431
+ isRestoringFirstPersonPose,
432
+ ])
64
433
 
65
434
  useEffect(() => {
66
- if (!controls.current) return
435
+ if (isFirstPersonMode || !controls.current) return
67
436
 
68
437
  controls.current.maxPolarAngle = maxPolarAngle
69
438
  controls.current.minPolarAngle = 0
@@ -71,11 +440,11 @@ export const CustomCameraControls = () => {
71
440
  if (controls.current.polarAngle > maxPolarAngle) {
72
441
  controls.current.rotateTo(controls.current.azimuthAngle, maxPolarAngle, true)
73
442
  }
74
- }, [maxPolarAngle])
443
+ }, [isFirstPersonMode, maxPolarAngle])
75
444
 
76
445
  const focusNode = useCallback(
77
446
  (nodeId: string) => {
78
- if (isPreviewMode || !controls.current) return
447
+ if (isPreviewMode || isFirstPersonMode || !controls.current) return
79
448
 
80
449
  const object3D = sceneRegistry.nodes.get(nodeId)
81
450
  if (!object3D) return
@@ -88,6 +457,7 @@ export const CustomCameraControls = () => {
88
457
  controls.current.getTarget(tempTarget)
89
458
  tempDelta.copy(tempCenter).sub(tempTarget)
90
459
 
460
+ clearPendingFloorplanNavigationPose()
91
461
  controls.current.setLookAt(
92
462
  tempPosition.x + tempDelta.x,
93
463
  tempPosition.y + tempDelta.y,
@@ -98,11 +468,143 @@ export const CustomCameraControls = () => {
98
468
  true,
99
469
  )
100
470
  },
101
- [isPreviewMode],
471
+ [clearPendingFloorplanNavigationPose, isPreviewMode, isFirstPersonMode],
102
472
  )
103
473
 
474
+ useEffect(() => {
475
+ if (isFirstPersonMode) return
476
+
477
+ return useEditor.subscribe((state) => {
478
+ const pose = state.navigationSyncPose
479
+ if (pose?.source !== '2d' || pose.revision === lastApplied2dNavigationRevision.current) return
480
+
481
+ const control = controls.current
482
+ if (!control) return
483
+
484
+ lastApplied2dNavigationRevision.current = pose.revision
485
+ const targetAzimuth = nearestEquivalentRadians(pose.azimuth, control.azimuthAngle)
486
+ const viewWidthUpdate = resolveCameraViewWidthUpdate(
487
+ control,
488
+ camera,
489
+ pose.viewWidth,
490
+ viewportSize,
491
+ )
492
+ pendingFloorplanNavigationPose.current = {
493
+ target: [...pose.target],
494
+ azimuth: targetAzimuth,
495
+ viewWidth: viewWidthUpdate.viewWidth,
496
+ publishOnComplete:
497
+ Math.abs(viewWidthUpdate.viewWidth - pose.viewWidth) >=
498
+ NAVIGATION_SYNC_VIEW_WIDTH_EPSILON,
499
+ }
500
+ // Match 3D settle time to 2D exponential decay (τ=90ms). SmoothDamp's
501
+ // effective time constant is smoothTime/2, so smoothTime=0.18 gives
502
+ // τ≈90ms and visual convergence in ~350-400ms, matching the 2D panel.
503
+ if (savedSmoothTimeRef.current === null) {
504
+ savedSmoothTimeRef.current = control.smoothTime
505
+ }
506
+ control.smoothTime = 0.18
507
+ control.moveTo(pose.target[0], pose.target[1], pose.target[2], true)
508
+ control.rotateTo(targetAzimuth, control.polarAngle, true)
509
+ applyCameraViewWidth(control, viewWidthUpdate)
510
+ })
511
+ }, [camera, isFirstPersonMode, viewportSize])
512
+
513
+ const publishCurrentNavigationPose = useCallback(() => {
514
+ if (isFirstPersonMode || !controls.current) return
515
+
516
+ controls.current.getTarget(syncTarget, false)
517
+ controls.current.getSpherical(syncSpherical, false)
518
+ const viewWidth = getCameraViewWidth(camera, syncSpherical.radius, viewportSize)
519
+
520
+ const pendingFloorplanPose = pendingFloorplanNavigationPose.current
521
+ if (pendingFloorplanPose) {
522
+ // The camera is still damping toward a 2D-originated pose; do not echo
523
+ // intermediate 3D poses back into the floorplan.
524
+ if (
525
+ isCameraAtNavigationPose(pendingFloorplanPose, syncTarget, syncSpherical.theta, viewWidth)
526
+ ) {
527
+ lastPublishedNavigationSync.current = pendingFloorplanPose
528
+ clearPendingFloorplanNavigationPose()
529
+ if (pendingFloorplanPose.publishOnComplete) {
530
+ useEditor.getState().publishNavigationSyncPose({
531
+ source: '3d',
532
+ target: [
533
+ pendingFloorplanPose.target[0],
534
+ pendingFloorplanPose.target[1],
535
+ pendingFloorplanPose.target[2],
536
+ ],
537
+ azimuth: pendingFloorplanPose.azimuth,
538
+ viewWidth: pendingFloorplanPose.viewWidth,
539
+ })
540
+ }
541
+ }
542
+ return
543
+ }
544
+
545
+ const previous = lastPublishedNavigationSync.current
546
+ if (
547
+ previous &&
548
+ Math.abs(previous.target[0] - syncTarget.x) < NAVIGATION_SYNC_POSITION_EPSILON &&
549
+ Math.abs(previous.target[1] - syncTarget.y) < NAVIGATION_SYNC_POSITION_EPSILON &&
550
+ Math.abs(previous.target[2] - syncTarget.z) < NAVIGATION_SYNC_POSITION_EPSILON &&
551
+ Math.abs(getAngleDeltaRadians(previous.azimuth, syncSpherical.theta)) <
552
+ NAVIGATION_SYNC_AZIMUTH_EPSILON &&
553
+ Math.abs(previous.viewWidth - viewWidth) < NAVIGATION_SYNC_VIEW_WIDTH_EPSILON
554
+ ) {
555
+ return
556
+ }
557
+
558
+ lastPublishedNavigationSync.current = {
559
+ target: [syncTarget.x, syncTarget.y, syncTarget.z],
560
+ azimuth: syncSpherical.theta,
561
+ viewWidth,
562
+ }
563
+ useEditor.getState().publishNavigationSyncPose({
564
+ source: '3d',
565
+ target: [syncTarget.x, syncTarget.y, syncTarget.z],
566
+ azimuth: syncSpherical.theta,
567
+ viewWidth,
568
+ })
569
+ }, [camera, clearPendingFloorplanNavigationPose, isFirstPersonMode, viewportSize])
570
+
571
+ useEffect(() => {
572
+ if (isFirstPersonMode || (!isFloorplanOpen && currentLevelId === null)) return
573
+
574
+ const frame = requestAnimationFrame(() => {
575
+ lastPublishedNavigationSync.current = null
576
+ publishCurrentNavigationPose()
577
+ })
578
+
579
+ return () => {
580
+ cancelAnimationFrame(frame)
581
+ }
582
+ }, [currentLevelId, isFirstPersonMode, isFloorplanOpen, publishCurrentNavigationPose])
583
+
584
+ useFrame((_, delta) => {
585
+ if (isFirstPersonMode || !controls.current) return
586
+
587
+ const panKeys = keyboardPanKeys.current
588
+ const horizontal = (panKeys.right ? 1 : 0) - (panKeys.left ? 1 : 0)
589
+ const vertical = (panKeys.forward ? 1 : 0) - (panKeys.backward ? 1 : 0)
590
+ if (horizontal === 0 && vertical === 0) return
591
+
592
+ const control = controls.current
593
+
594
+ control.getSpherical(keyboardPanSpherical, false)
595
+ const viewWidth = getCameraViewWidth(camera, keyboardPanSpherical.radius, viewportSize)
596
+ const speed = Math.min(
597
+ Math.max(viewWidth * KEYBOARD_PAN_VIEW_WIDTH_PER_SECOND, KEYBOARD_PAN_MIN_SPEED),
598
+ KEYBOARD_PAN_MAX_SPEED,
599
+ )
600
+ const step = (speed * Math.min(delta, 0.05)) / Math.hypot(horizontal, vertical)
601
+
602
+ clearPendingFloorplanNavigationPose()
603
+ if (horizontal !== 0) control.truck(horizontal * step, 0, true)
604
+ if (vertical !== 0) control.forward(vertical * step, true)
605
+ })
606
+
104
607
  // Configure mouse buttons based on control mode and camera mode
105
- const cameraMode = useViewer((state) => state.cameraMode)
106
608
  const mouseButtons = useMemo(() => {
107
609
  // Use ZOOM for orthographic camera, DOLLY for perspective camera
108
610
  const wheelAction =
@@ -135,12 +637,12 @@ export const CustomCameraControls = () => {
135
637
  const tool = useEditor((s) => s.tool)
136
638
  const mode = useEditor((s) => s.mode)
137
639
  const selectionTool = useEditor((s) => s.floorplanSelectionTool)
138
- const movingNode = useEditor((s) => s.movingNode)
139
- const movingWallEndpoint = useEditor((s) => s.movingWallEndpoint)
140
- const movingFenceEndpoint = useEditor((s) => s.movingFenceEndpoint)
640
+ const movingNode = useMovingNode()
641
+ const endpointReshape = useEndpointReshape()
642
+ const activeHandleDrag = useActiveHandleDrag()
141
643
  const isBoxSelectActive = mode === 'select' && selectionTool === 'marquee'
142
644
  const isInteracting = Boolean(
143
- tool || movingNode || movingWallEndpoint || movingFenceEndpoint || isBoxSelectActive,
645
+ tool || movingNode || endpointReshape || activeHandleDrag || isBoxSelectActive,
144
646
  )
145
647
  const touches = useMemo(() => {
146
648
  const twoFingerAction =
@@ -162,6 +664,8 @@ export const CustomCameraControls = () => {
162
664
  }, [cameraMode, isPreviewMode, isInteracting])
163
665
 
164
666
  useEffect(() => {
667
+ if (isFirstPersonMode) return
668
+
165
669
  const keyState = {
166
670
  shiftRight: false,
167
671
  shiftLeft: false,
@@ -169,6 +673,52 @@ export const CustomCameraControls = () => {
169
673
  controlLeft: false,
170
674
  space: false,
171
675
  }
676
+ let ownsNavigationCursor = false
677
+ let panPointerId: number | null = null
678
+ let panPointerButton: number | null = null
679
+
680
+ const clearKeyboardPanKeys = () => {
681
+ keyboardPanKeys.current.forward = false
682
+ keyboardPanKeys.current.backward = false
683
+ keyboardPanKeys.current.left = false
684
+ keyboardPanKeys.current.right = false
685
+ }
686
+
687
+ const setNavigationCursor = (cursor: 'grab' | 'grabbing') => {
688
+ document.body.style.cursor = cursor
689
+ gl.domElement.style.cursor = cursor
690
+ ownsNavigationCursor = true
691
+ }
692
+
693
+ const clearNavigationCursor = () => {
694
+ if (
695
+ ownsNavigationCursor &&
696
+ (document.body.style.cursor === 'grab' || document.body.style.cursor === 'grabbing')
697
+ ) {
698
+ document.body.style.cursor = ''
699
+ }
700
+ if (ownsNavigationCursor && gl.domElement.style.cursor === 'grab') {
701
+ gl.domElement.style.cursor = ''
702
+ }
703
+ if (ownsNavigationCursor && gl.domElement.style.cursor === 'grabbing') {
704
+ gl.domElement.style.cursor = ''
705
+ }
706
+ ownsNavigationCursor = false
707
+ }
708
+
709
+ const updateNavigationCursor = () => {
710
+ if (panPointerId !== null) {
711
+ setNavigationCursor('grabbing')
712
+ return
713
+ }
714
+
715
+ if (keyState.space) {
716
+ setNavigationCursor('grab')
717
+ return
718
+ }
719
+
720
+ clearNavigationCursor()
721
+ }
172
722
 
173
723
  const updateConfig = () => {
174
724
  if (!controls.current) return
@@ -195,9 +745,24 @@ export const CustomCameraControls = () => {
195
745
  }
196
746
 
197
747
  const onKeyDown = (event: KeyboardEvent) => {
748
+ if (isKeyboardPanKey(event.code)) {
749
+ if (
750
+ !(event.metaKey || event.ctrlKey || event.altKey) &&
751
+ !isEditableKeyboardTarget(event.target)
752
+ ) {
753
+ setKeyboardPanKey(keyboardPanKeys.current, event.code, true)
754
+ clearPendingFloorplanNavigationPose()
755
+ event.preventDefault()
756
+ event.stopPropagation()
757
+ }
758
+ return
759
+ }
760
+
198
761
  if (event.code === 'Space') {
762
+ if (isEditableKeyboardTarget(event.target)) return
763
+ event.preventDefault()
199
764
  keyState.space = true
200
- document.body.style.cursor = 'grab'
765
+ updateNavigationCursor()
201
766
  }
202
767
  if (event.code === 'ShiftRight') {
203
768
  keyState.shiftRight = true
@@ -215,9 +780,22 @@ export const CustomCameraControls = () => {
215
780
  }
216
781
 
217
782
  const onKeyUp = (event: KeyboardEvent) => {
783
+ if (isKeyboardPanKey(event.code)) {
784
+ const changed = setKeyboardPanKey(keyboardPanKeys.current, event.code, false)
785
+ if (changed) {
786
+ event.preventDefault()
787
+ event.stopPropagation()
788
+ }
789
+ return
790
+ }
791
+
218
792
  if (event.code === 'Space') {
219
793
  keyState.space = false
220
- document.body.style.cursor = ''
794
+ if (panPointerButton === 0) {
795
+ panPointerId = null
796
+ panPointerButton = null
797
+ }
798
+ updateNavigationCursor()
221
799
  }
222
800
  if (event.code === 'ShiftRight') {
223
801
  keyState.shiftRight = false
@@ -234,15 +812,78 @@ export const CustomCameraControls = () => {
234
812
  updateConfig()
235
813
  }
236
814
 
815
+ const onPointerDown = (event: PointerEvent) => {
816
+ if (!(event.target instanceof Node) || !gl.domElement.contains(event.target)) return
817
+ clearPendingFloorplanNavigationPose()
818
+ if (event.button !== 1 && !(event.button === 0 && keyState.space)) return
819
+
820
+ panPointerId = event.pointerId
821
+ panPointerButton = event.button
822
+ updateNavigationCursor()
823
+ }
824
+
825
+ const onWheel = () => {
826
+ clearPendingFloorplanNavigationPose()
827
+ }
828
+
829
+ const onPointerUp = (event: PointerEvent) => {
830
+ if (panPointerId === null) return
831
+ if (event.type !== 'pointercancel' && event.pointerId !== panPointerId) return
832
+ if (event.type !== 'pointercancel' && event.button !== panPointerButton) return
833
+
834
+ panPointerId = null
835
+ panPointerButton = null
836
+ updateNavigationCursor()
837
+ }
838
+
839
+ const onBlur = () => {
840
+ keyState.space = false
841
+ clearKeyboardPanKeys()
842
+ panPointerId = null
843
+ panPointerButton = null
844
+ clearNavigationCursor()
845
+ updateConfig()
846
+ }
847
+
237
848
  document.addEventListener('keydown', onKeyDown)
238
849
  document.addEventListener('keyup', onKeyUp)
850
+ window.addEventListener('pointerdown', onPointerDown, true)
851
+ window.addEventListener('pointerup', onPointerUp, true)
852
+ window.addEventListener('pointercancel', onPointerUp, true)
853
+ window.addEventListener('blur', onBlur)
854
+ gl.domElement.addEventListener('wheel', onWheel, { passive: true })
239
855
  updateConfig()
240
856
 
241
857
  return () => {
242
858
  document.removeEventListener('keydown', onKeyDown)
243
859
  document.removeEventListener('keyup', onKeyUp)
860
+ window.removeEventListener('pointerdown', onPointerDown, true)
861
+ window.removeEventListener('pointerup', onPointerUp, true)
862
+ window.removeEventListener('pointercancel', onPointerUp, true)
863
+ window.removeEventListener('blur', onBlur)
864
+ gl.domElement.removeEventListener('wheel', onWheel)
865
+ clearKeyboardPanKeys()
866
+ clearNavigationCursor()
244
867
  }
245
- }, [cameraMode, isPreviewMode])
868
+ }, [cameraMode, gl, isPreviewMode, isFirstPersonMode, clearPendingFloorplanNavigationPose])
869
+
870
+ // Cancel any in-progress 2D-origin navigation pose when the user starts
871
+ // dragging (right-click orbit, middle-click pan, touch). `controlstart`
872
+ // fires only for user pointer interactions — not for programmatic
873
+ // moveTo/rotateTo which emit `transitionstart` instead.
874
+ useEffect(() => {
875
+ if (isFirstPersonMode) return
876
+ const control = controls.current
877
+ if (!control) return
878
+
879
+ const onControlStart = () => {
880
+ clearPendingFloorplanNavigationPose()
881
+ }
882
+ control.addEventListener('controlstart', onControlStart)
883
+ return () => {
884
+ control.removeEventListener('controlstart', onControlStart)
885
+ }
886
+ }, [isFirstPersonMode, clearPendingFloorplanNavigationPose])
246
887
 
247
888
  // Preview mode: auto-navigate camera to selected node (viewer behavior)
248
889
  const previewTargetNodeId = isPreviewMode
@@ -250,7 +891,7 @@ export const CustomCameraControls = () => {
250
891
  : null
251
892
 
252
893
  useEffect(() => {
253
- if (!(isPreviewMode && controls.current)) return
894
+ if (!(isPreviewMode && controls.current) || isFirstPersonMode) return
254
895
 
255
896
  const nodes = useScene.getState().nodes
256
897
  let node = previewTargetNodeId ? nodes[previewTargetNodeId] : null
@@ -310,11 +951,122 @@ export const CustomCameraControls = () => {
310
951
  tempCenter.z,
311
952
  true,
312
953
  )
313
- }, [isPreviewMode, previewTargetNodeId])
954
+ }, [isPreviewMode, isFirstPersonMode, previewTargetNodeId])
314
955
 
956
+ // Preset capture auto-framing — when `setCaptureMode({ mode: 'preset',
957
+ // isolated })` fires, fly the camera to a pose that fits the union
958
+ // bounds of the isolated subtree inside the locked square crop. The
959
+ // user can still pan / orbit / zoom from there; we only set the
960
+ // initial pose. On exit (`mode: 'idle'`), we restore the previous
961
+ // pose so the user lands back exactly where they were before the
962
+ // modal opened.
963
+ const captureMode = useEditor((s) => s.captureMode)
315
964
  useEffect(() => {
316
- const handleNodeCapture = ({ nodeId }: CameraControlEvent) => {
965
+ if (isFirstPersonMode) return
966
+ if (!controls.current) return
967
+ if (captureMode.mode !== 'preset') return
968
+ const ids = captureMode.isolated
969
+ if (ids.length === 0) return
970
+
971
+ // Stash the pre-capture pose so we can restore it on exit. Using
972
+ // a ref keeps the value across the cleanup phase without
973
+ // re-renders.
974
+ const restorePos = new Vector3()
975
+ const restoreTarget = new Vector3()
976
+ controls.current.getPosition(restorePos)
977
+ controls.current.getTarget(restoreTarget)
978
+
979
+ // Union the bounds of every isolated subtree root. `setFromObject`
980
+ // walks the Three.js descendants automatically, so this picks up
981
+ // synthesized children (door/window cutouts under a wall, etc.).
982
+ tempBox.makeEmpty()
983
+ for (const id of ids) {
984
+ const obj = sceneRegistry.nodes.get(id)
985
+ if (!obj) continue
986
+ const sub = new Box3().setFromObject(obj)
987
+ if (!sub.isEmpty()) tempBox.union(sub)
988
+ }
989
+ if (captureMode.framingBounds) {
990
+ const { center, max, min, size } = captureMode.framingBounds
991
+ const fallbackHeight = Math.max(Math.max(size[0], size[1]) * 0.35, 2.5)
992
+ const minY = tempBox.isEmpty() ? 0 : tempBox.min.y
993
+ const maxY = tempBox.isEmpty() ? fallbackHeight : tempBox.max.y
994
+ tempBox.min.set(min[0], minY, min[1])
995
+ tempBox.max.set(max[0], Math.max(maxY, minY + 0.1), max[1])
996
+ tempCenter.set(center[0], (tempBox.min.y + tempBox.max.y) / 2, center[1])
997
+ tempSize.set(size[0], tempBox.max.y - tempBox.min.y, size[1])
998
+ } else {
999
+ if (tempBox.isEmpty()) return
1000
+
1001
+ tempBox.getCenter(tempCenter)
1002
+ tempBox.getSize(tempSize)
1003
+ }
1004
+
1005
+ // Distance heuristic: fit the subject inside the 75%-of-shorter-
1006
+ // side square crop with comfortable padding. Multiplier 2.4 leaves
1007
+ // ~25-30% margin around the bounds so the user can frame without
1008
+ // immediately needing to zoom out, but isn't so far away that the
1009
+ // subject reads as small in the thumbnail.
1010
+ const maxDim = Math.max(tempSize.x, tempSize.y, tempSize.z)
1011
+ const distance = Math.max(maxDim * 2.4, 4)
1012
+
1013
+ // Frame the subject from a 3/4 view of its front face. The node's
1014
+ // local +Z is its forward axis in this scene's authoring convention
1015
+ // (the face the user sets up to be photographed). When a single
1016
+ // subtree is isolated we read its yaw and rotate the camera around
1017
+ // the bounds center so the framing follows the user's authored
1018
+ // orientation; for multi-isolate sets we fall back to world +Z.
1019
+ // The 3/4 view offsets the camera by 35° to the right of dead-on
1020
+ // so both the front face and a side are visible — the "nice angle"
1021
+ // that reads as a product shot rather than a flat elevation. ~25°
1022
+ // elevation keeps the top visible without going isometric.
1023
+ const SIDE_OFFSET_RAD = (35 * Math.PI) / 180
1024
+ const ELEVATION_RAD = (25 * Math.PI) / 180
1025
+ let yaw = 0
1026
+ if (ids.length === 1) {
1027
+ const node = useScene.getState().nodes[ids[0] as AnyNodeId]
1028
+ if (node && 'rotation' in node) {
1029
+ const r = (node as { rotation?: unknown }).rotation
1030
+ if (typeof r === 'number') yaw = r
1031
+ else if (Array.isArray(r)) yaw = (r as [number, number, number])[1] ?? 0
1032
+ }
1033
+ }
1034
+ // World-space direction the camera should sit *along* relative to
1035
+ // bounds center: in front (object's local +Z under yaw) + a right
1036
+ // offset around Y for the 3/4 read.
1037
+ const viewAngle = yaw + SIDE_OFFSET_RAD
1038
+ const horizontal = distance * Math.cos(ELEVATION_RAD)
1039
+ const elevation = distance * Math.sin(ELEVATION_RAD)
1040
+ controls.current.setLookAt(
1041
+ tempCenter.x + Math.sin(viewAngle) * horizontal,
1042
+ tempCenter.y + elevation,
1043
+ tempCenter.z + Math.cos(viewAngle) * horizontal,
1044
+ tempCenter.x,
1045
+ tempCenter.y,
1046
+ tempCenter.z,
1047
+ true,
1048
+ )
1049
+
1050
+ return () => {
1051
+ // Cleanup runs on captureMode change *or* unmount. Restore the
1052
+ // pre-capture pose only if the controls are still around (during
1053
+ // unmount they might be torn down already).
317
1054
  if (!controls.current) return
1055
+ controls.current.setLookAt(
1056
+ restorePos.x,
1057
+ restorePos.y,
1058
+ restorePos.z,
1059
+ restoreTarget.x,
1060
+ restoreTarget.y,
1061
+ restoreTarget.z,
1062
+ true,
1063
+ )
1064
+ }
1065
+ }, [captureMode, isFirstPersonMode])
1066
+
1067
+ useEffect(() => {
1068
+ const handleNodeCapture = ({ nodeId }: CameraControlEvent) => {
1069
+ if (isFirstPersonMode || !controls.current) return
318
1070
 
319
1071
  const position = new Vector3()
320
1072
  const target = new Vector3()
@@ -332,12 +1084,13 @@ export const CustomCameraControls = () => {
332
1084
  })
333
1085
  }
334
1086
  const handleNodeView = ({ nodeId }: CameraControlEvent) => {
335
- if (!controls.current) return
1087
+ if (isFirstPersonMode || !controls.current) return
336
1088
 
337
1089
  const node = useScene.getState().nodes[nodeId]
338
1090
  if (!node?.camera) return
339
1091
  const { position, target } = node.camera
340
1092
 
1093
+ clearPendingFloorplanNavigationPose()
341
1094
  controls.current.setLookAt(
342
1095
  position[0],
343
1096
  position[1],
@@ -350,7 +1103,7 @@ export const CustomCameraControls = () => {
350
1103
  }
351
1104
 
352
1105
  const handleTopView = () => {
353
- if (!controls.current) return
1106
+ if (isFirstPersonMode || !controls.current) return
354
1107
 
355
1108
  const currentPolarAngle = controls.current.polarAngle
356
1109
 
@@ -358,11 +1111,12 @@ export const CustomCameraControls = () => {
358
1111
  // Otherwise, go to top view (0°)
359
1112
  const targetAngle = currentPolarAngle < 0.1 ? Math.PI / 4 : 0
360
1113
 
1114
+ clearPendingFloorplanNavigationPose()
361
1115
  controls.current.rotatePolarTo(targetAngle, true)
362
1116
  }
363
1117
 
364
1118
  const handleOrbitCW = () => {
365
- if (!controls.current) return
1119
+ if (isFirstPersonMode || !controls.current) return
366
1120
 
367
1121
  const currentAzimuth = controls.current.azimuthAngle
368
1122
  const currentPolar = controls.current.polarAngle
@@ -370,11 +1124,12 @@ export const CustomCameraControls = () => {
370
1124
  const rounded = Math.round(currentAzimuth / (Math.PI / 2)) * (Math.PI / 2)
371
1125
  const target = rounded - Math.PI / 2
372
1126
 
1127
+ clearPendingFloorplanNavigationPose()
373
1128
  controls.current.rotateTo(target, currentPolar, true)
374
1129
  }
375
1130
 
376
1131
  const handleOrbitCCW = () => {
377
- if (!controls.current) return
1132
+ if (isFirstPersonMode || !controls.current) return
378
1133
 
379
1134
  const currentAzimuth = controls.current.azimuthAngle
380
1135
  const currentPolar = controls.current.polarAngle
@@ -382,6 +1137,7 @@ export const CustomCameraControls = () => {
382
1137
  const rounded = Math.round(currentAzimuth / (Math.PI / 2)) * (Math.PI / 2)
383
1138
  const target = rounded + Math.PI / 2
384
1139
 
1140
+ clearPendingFloorplanNavigationPose()
385
1141
  controls.current.rotateTo(target, currentPolar, true)
386
1142
  }
387
1143
 
@@ -390,9 +1146,10 @@ export const CustomCameraControls = () => {
390
1146
  }
391
1147
 
392
1148
  const handleFitScene = ({ bounds }: CameraControlFitSceneEvent) => {
393
- if (!controls.current || isPreviewMode) return
1149
+ if (isFirstPersonMode || !controls.current || isPreviewMode) return
394
1150
  if (!bounds) {
395
1151
  // Restore default framing pose when no bounds were computed.
1152
+ clearPendingFloorplanNavigationPose()
396
1153
  controls.current.setLookAt(20, 20, 20, 0, 0, 0, true)
397
1154
  return
398
1155
  }
@@ -403,6 +1160,7 @@ export const CustomCameraControls = () => {
403
1160
  const maxExtent = Math.max(w, d)
404
1161
  const distance = Math.max(maxExtent * 1.4, 15)
405
1162
  const height = Math.max(maxExtent * 0.8, 10)
1163
+ clearPendingFloorplanNavigationPose()
406
1164
  controls.current.setLookAt(cx + distance * 0.7, height, cz + distance * 0.7, cx, 0, cz, true)
407
1165
  }
408
1166
 
@@ -423,7 +1181,7 @@ export const CustomCameraControls = () => {
423
1181
  emitter.off('camera-controls:orbit-ccw', handleOrbitCCW)
424
1182
  emitter.off('camera-controls:fit-scene', handleFitScene)
425
1183
  }
426
- }, [focusNode, isPreviewMode])
1184
+ }, [clearPendingFloorplanNavigationPose, focusNode, isPreviewMode, isFirstPersonMode])
427
1185
 
428
1186
  const onTransitionStart = useCallback(() => {
429
1187
  useViewer.getState().setCameraDragging(true)
@@ -433,6 +1191,14 @@ export const CustomCameraControls = () => {
433
1191
  useViewer.getState().setCameraDragging(false)
434
1192
  }, [])
435
1193
 
1194
+ // Preset capture mode frames a single subtree (often a 0.3–2m preset),
1195
+ // so the default 2m minDistance prevents the user from getting close
1196
+ // enough to compose a good thumbnail. Relax the clamp to 0.5m while
1197
+ // capturing presets; reset on exit so general editing keeps the looser
1198
+ // navigation guardrails.
1199
+ const isPresetCapture = captureMode.mode === 'preset'
1200
+ const minDistance = isPresetCapture ? 0.5 : 2
1201
+
436
1202
  if (isFirstPersonMode) {
437
1203
  return null
438
1204
  }
@@ -442,9 +1208,10 @@ export const CustomCameraControls = () => {
442
1208
  makeDefault
443
1209
  maxDistance={100}
444
1210
  maxPolarAngle={maxPolarAngle}
445
- minDistance={10}
1211
+ minDistance={minDistance}
446
1212
  minPolarAngle={0}
447
1213
  mouseButtons={mouseButtons}
1214
+ onUpdate={publishCurrentNavigationPose}
448
1215
  onRest={onRest}
449
1216
  onSleep={onRest}
450
1217
  onTransitionStart={onTransitionStart}