@pascal-app/editor 1.0.1 → 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 (498) hide show
  1. package/package.json +16 -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 +221 -128
  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 +37 -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 +536 -487
  27. package/src/components/editor/footprint-height-handle.tsx +204 -0
  28. package/src/components/editor/grid.tsx +31 -7
  29. package/src/components/editor/group-actions.ts +59 -11
  30. package/src/components/editor/group-floating-action-menu.tsx +19 -9
  31. package/src/components/editor/group-move-3d.ts +4 -6
  32. package/src/components/editor/group-rotate-handle.tsx +81 -35
  33. package/src/components/editor/group-selection-box-3d.tsx +25 -17
  34. package/src/components/editor/group-transform-shared.test.ts +126 -1
  35. package/src/components/editor/group-transform-shared.ts +79 -73
  36. package/src/components/editor/handles/handle-arrow.tsx +72 -2
  37. package/src/components/editor/handles/handle-hit-priority.test.tsx +63 -0
  38. package/src/components/editor/handles/handle-hit-priority.tsx +37 -0
  39. package/src/components/editor/handles/swallow-next-click.test.ts +28 -0
  40. package/src/components/editor/handles/use-handle-drag.ts +272 -131
  41. package/src/components/editor/index.tsx +316 -176
  42. package/src/components/editor/join-walls-pill.tsx +44 -0
  43. package/src/components/editor/node-action-menu.tsx +70 -70
  44. package/src/components/editor/node-arrow-handles.tsx +167 -14
  45. package/src/components/editor/open-wall-ends-3d-layer.tsx +273 -0
  46. package/src/components/editor/open-wall-ends-hint.tsx +89 -0
  47. package/src/components/editor/quick-measurement-hud.tsx +3 -2
  48. package/src/components/editor/registry-action-contributions.test.tsx +63 -0
  49. package/src/components/editor/registry-action-contributions.tsx +132 -0
  50. package/src/components/editor/room-controls.tsx +847 -0
  51. package/src/components/editor/room-floor-highlight.tsx +128 -0
  52. package/src/components/editor/room-handles-2d.tsx +354 -0
  53. package/src/components/editor/room-handles.tsx +675 -0
  54. package/src/components/editor/room-highlight.tsx +225 -0
  55. package/src/components/editor/room-labels.test.ts +357 -0
  56. package/src/components/editor/room-labels.tsx +990 -0
  57. package/src/components/editor/room-transform-ghost.tsx +256 -0
  58. package/src/components/editor/room-webgpu-materials.test.ts +27 -0
  59. package/src/components/editor/selection-manager.tsx +451 -317
  60. package/src/components/editor/slab-hole-highlights.tsx +3 -0
  61. package/src/components/editor/snapshot-capture-overlay.tsx +4 -0
  62. package/src/components/editor/snapshot-capture.test.ts +52 -4
  63. package/src/components/editor/snapshot-capture.ts +30 -5
  64. package/src/components/editor/thumbnail-generator.test.ts +308 -0
  65. package/src/components/editor/thumbnail-generator.tsx +1035 -229
  66. package/src/components/editor/use-floorplan-background-placement.ts +31 -11
  67. package/src/components/editor/vector-edge-extractor.tsx +121 -0
  68. package/src/components/editor/views/host-view.tsx +61 -0
  69. package/src/components/editor/views/plugins-view.tsx +670 -0
  70. package/src/components/editor/views/scene-region.tsx +59 -0
  71. package/src/components/editor/views/use-editor-views.ts +57 -0
  72. package/src/components/editor/views/view-bar.tsx +220 -0
  73. package/src/components/editor/wall-justification.test.ts +140 -0
  74. package/src/components/editor/wall-move-side-handles.tsx +114 -163
  75. package/src/components/editor/wall-opening-highlights.tsx +2 -3
  76. package/src/components/editor/wall-push-arrow.tsx +92 -0
  77. package/src/components/editor/wall-region-handles.tsx +509 -0
  78. package/src/components/editor/wall-snap-beacon-layer.tsx +3 -0
  79. package/src/components/editor-2d/floorplan-cursor-indicator-overlay.tsx +8 -4
  80. package/src/components/editor-2d/floorplan-group-move.tsx +17 -24
  81. package/src/components/editor-2d/floorplan-open-wall-ends-layer.test.tsx +86 -0
  82. package/src/components/editor-2d/floorplan-open-wall-ends-layer.tsx +290 -0
  83. package/src/components/editor-2d/floorplan-registered-tool-layer.tsx +33 -12
  84. package/src/components/editor-2d/floorplan-registry-action-menu.tsx +11 -1
  85. package/src/components/editor-2d/floorplan-registry-move-overlay.tsx +242 -126
  86. package/src/components/editor-2d/renderers/floorplan-dimension-edit-overlay.test.ts +156 -0
  87. package/src/components/editor-2d/renderers/floorplan-dimension-edit-overlay.tsx +278 -0
  88. package/src/components/editor-2d/renderers/floorplan-dimension-renderer.tsx +77 -0
  89. package/src/components/editor-2d/renderers/floorplan-draft-measurement.tsx +166 -0
  90. package/src/components/editor-2d/renderers/floorplan-geometry-renderer.tsx +4 -1
  91. package/src/components/editor-2d/renderers/floorplan-registry-layer.test.ts +87 -8
  92. package/src/components/editor-2d/renderers/floorplan-registry-layer.tsx +355 -245
  93. package/src/components/editor-2d/svg-paths.ts +12 -15
  94. package/src/components/procedural-items/inspector.tsx +40 -15
  95. package/src/components/systems/ceiling/ceiling-selection-affordance-system.test.ts +97 -14
  96. package/src/components/systems/ceiling/ceiling-selection-affordance-system.tsx +18 -18
  97. package/src/components/systems/zone/zone-system.test.ts +47 -0
  98. package/src/components/systems/zone/zone-system.tsx +31 -66
  99. package/src/components/tools/__fixtures__/hosting.ts +10 -0
  100. package/src/components/tools/item/item-surface-placement.test.ts +3 -9
  101. package/src/components/tools/item/item-surface-strategy-resolver.test.ts +3 -9
  102. package/src/components/tools/item/placement-history.test.tsx +474 -0
  103. package/src/components/tools/item/placement-strategies.ts +63 -47
  104. package/src/components/tools/item/shelf-surface-strategy-resolver.test.ts +3 -3
  105. package/src/components/tools/item/use-draft-node.test.tsx +432 -1
  106. package/src/components/tools/item/use-draft-node.ts +207 -97
  107. package/src/components/tools/item/use-placement-coordinator.tsx +251 -111
  108. package/src/components/tools/paint-region/use-paint-tint.ts +37 -0
  109. package/src/components/tools/paint-region/wall-paint-region-tool.tsx +466 -0
  110. package/src/components/tools/registry/item-surface-move.test.ts +10 -2
  111. package/src/components/tools/registry/item-surface-move.ts +46 -24
  112. package/src/components/tools/registry/item-surface-resolver.test.ts +2 -2
  113. package/src/components/tools/registry/move-registry-node-tool.tsx +26 -10
  114. package/src/components/tools/select/box-select-tool.tsx +2 -12
  115. package/src/components/tools/select/plane-box-select-tool.test.tsx +79 -0
  116. package/src/components/tools/select/plane-box-select-tool.tsx +35 -22
  117. package/src/components/tools/select/select-candidates.test.ts +37 -4
  118. package/src/components/tools/select/select-candidates.ts +15 -11
  119. package/src/components/tools/shared/affordance-dispatch.test.ts +24 -0
  120. package/src/components/tools/shared/affordance-dispatch.ts +13 -0
  121. package/src/components/tools/shared/cursor-sphere.tsx +36 -12
  122. package/src/components/tools/shared/draft-measurement-label.tsx +38 -0
  123. package/src/components/tools/shared/pointer-support-cap.test.ts +27 -0
  124. package/src/components/tools/shared/pointer-support-cap.ts +17 -5
  125. package/src/components/tools/shared/surface-rejection.test.tsx +5 -4
  126. package/src/components/tools/shared/surface-rejection.tsx +14 -0
  127. package/src/components/tools/site/site-boundary-editor.tsx +1 -2
  128. package/src/components/tools/stair/stair-defaults.ts +0 -3
  129. package/src/components/tools/stair/stair-tool.tsx +90 -42
  130. package/src/components/tools/tool-manager.tsx +82 -23
  131. package/src/components/tools/wall/wall-drafting.test.ts +19 -4
  132. package/src/components/tools/zone/zone-tool.tsx +4 -12
  133. package/src/components/ui/action-menu/action-button.tsx +3 -0
  134. package/src/components/ui/action-menu/camera-actions.tsx +2 -1
  135. package/src/components/ui/action-menu/control-modes.tsx +2 -35
  136. package/src/components/ui/action-menu/measurement-control.tsx +3 -10
  137. package/src/components/ui/build-panel/build-panel.tsx +235 -0
  138. package/src/components/ui/command-palette/editor-commands.tsx +4 -30
  139. package/src/components/ui/command-palette/index.tsx +5 -24
  140. package/src/components/ui/controls/finish-swatch.tsx +110 -0
  141. package/src/components/ui/controls/material-paint-panel.tsx +101 -91
  142. package/src/components/ui/controls/material-picker.tsx +8 -67
  143. package/src/components/ui/controls/scene-material-list.tsx +4 -32
  144. package/src/components/ui/controls/select-control.tsx +65 -0
  145. package/src/components/ui/controls/slider-control.tsx +76 -31
  146. package/src/components/ui/floating-level-selector.tsx +9 -14
  147. package/src/components/ui/helpers/building-helper.tsx +4 -1
  148. package/src/components/ui/helpers/contextual-helper-panel.tsx +154 -70
  149. package/src/components/ui/helpers/helper-manager.test.tsx +69 -0
  150. package/src/components/ui/helpers/helper-manager.tsx +348 -19
  151. package/src/components/ui/helpers/item-helper.tsx +8 -0
  152. package/src/components/ui/helpers/registered-tool-helper.tsx +10 -33
  153. package/src/components/ui/icon-ref.tsx +45 -1
  154. package/src/components/ui/item-catalog/item-catalog.tsx +5 -30
  155. package/src/components/ui/level-duplicate-dialog.tsx +1 -1
  156. package/src/components/ui/node-icon-image.tsx +16 -0
  157. package/src/components/ui/panels/collections/collections-popover.tsx +20 -14
  158. package/src/components/ui/panels/floor-foundation-panel.tsx +551 -0
  159. package/src/components/ui/panels/floor-opening-panel.tsx +143 -0
  160. package/src/components/ui/panels/mobile-panel-sheet.tsx +2 -2
  161. package/src/components/ui/panels/mobile-selection-bar.tsx +2 -2
  162. package/src/components/ui/panels/multi-field-value.test.ts +26 -3
  163. package/src/components/ui/panels/multi-field-value.ts +6 -3
  164. package/src/components/ui/panels/multi-height-mode.tsx +27 -21
  165. package/src/components/ui/panels/multi-parametric-inspector.tsx +52 -2
  166. package/src/components/ui/panels/multi-selection-panel.tsx +43 -4
  167. package/src/components/ui/panels/node-display.ts +8 -3
  168. package/src/components/ui/panels/panel-manager.tsx +103 -68
  169. package/src/components/ui/panels/panel-wrapper.tsx +78 -36
  170. package/src/components/ui/panels/parametric-field-control.tsx +1 -1
  171. package/src/components/ui/panels/parametric-inspector.tsx +7 -19
  172. package/src/components/ui/panels/reference-panel.tsx +2 -3
  173. package/src/components/ui/panels/room-construction-rows.tsx +774 -0
  174. package/src/components/ui/panels/room-name-options.test.ts +55 -0
  175. package/src/components/ui/panels/room-name-options.ts +47 -0
  176. package/src/components/ui/panels/room-openings-field.tsx +110 -0
  177. package/src/components/ui/panels/room-panel.tsx +725 -0
  178. package/src/components/ui/panels/shape-choice.tsx +58 -0
  179. package/src/components/ui/panels/wall-region-list.tsx +109 -0
  180. package/src/components/ui/primitives/color-dot.tsx +36 -24
  181. package/src/components/ui/primitives/shortcut-token.tsx +4 -0
  182. package/src/components/ui/primitives/sidebar.tsx +2 -0
  183. package/src/components/ui/right-stack.test.ts +29 -0
  184. package/src/components/ui/right-stack.tsx +145 -0
  185. package/src/components/ui/sidebar/icon-rail.tsx +5 -0
  186. package/src/components/ui/sidebar/panels/settings-panel/audio-settings-dialog.tsx +2 -2
  187. package/src/components/ui/sidebar/panels/settings-panel/index.tsx +615 -372
  188. package/src/components/ui/sidebar/panels/settings-panel/keyboard-shortcuts-dialog.tsx +7 -7
  189. package/src/components/ui/sidebar/panels/settings-panel/print-export-button.tsx +1 -1
  190. package/src/components/ui/sidebar/panels/site-panel/ceiling-tree-node.tsx +4 -4
  191. package/src/components/ui/sidebar/panels/site-panel/chimney-tree-node.tsx +1 -5
  192. package/src/components/ui/sidebar/panels/site-panel/collection-tree-node.tsx +197 -0
  193. package/src/components/ui/sidebar/panels/site-panel/column-tree-node.tsx +1 -5
  194. package/src/components/ui/sidebar/panels/site-panel/door-tree-node.tsx +1 -5
  195. package/src/components/ui/sidebar/panels/site-panel/dormer-tree-node.tsx +1 -5
  196. package/src/components/ui/sidebar/panels/site-panel/elevator-tree-node.tsx +1 -5
  197. package/src/components/ui/sidebar/panels/site-panel/fence-tree-node.tsx +24 -11
  198. package/src/components/ui/sidebar/panels/site-panel/gutter-tree-node.tsx +1 -5
  199. package/src/components/ui/sidebar/panels/site-panel/index.tsx +176 -211
  200. package/src/components/ui/sidebar/panels/site-panel/inline-rename-input.tsx +30 -8
  201. package/src/components/ui/sidebar/panels/site-panel/item-tree-node.tsx +1 -3
  202. package/src/components/ui/sidebar/panels/site-panel/level-tree-node.tsx +4 -1
  203. package/src/components/ui/sidebar/panels/site-panel/registry-tree-node.tsx +1 -3
  204. package/src/components/ui/sidebar/panels/site-panel/roof-tree-node.tsx +1 -4
  205. package/src/components/ui/sidebar/panels/site-panel/shelf-tree-node.tsx +1 -3
  206. package/src/components/ui/sidebar/panels/site-panel/slab-tree-node.tsx +21 -13
  207. package/src/components/ui/sidebar/panels/site-panel/solar-panel-tree-node.tsx +1 -5
  208. package/src/components/ui/sidebar/panels/site-panel/spawn-tree-node.tsx +1 -5
  209. package/src/components/ui/sidebar/panels/site-panel/stair-tree-node.tsx +1 -5
  210. package/src/components/ui/sidebar/panels/site-panel/tree-node.tsx +0 -27
  211. package/src/components/ui/sidebar/panels/site-panel/unit-tree-node.tsx +2 -3
  212. package/src/components/ui/sidebar/panels/site-panel/wall-tree-node.tsx +1 -5
  213. package/src/components/ui/sidebar/panels/site-panel/window-tree-node.tsx +1 -5
  214. package/src/components/ui/sidebar/panels/site-panel/zone-tree-node.tsx +9 -4
  215. package/src/components/ui/sidebar/panels/zone-panel/index.tsx +0 -2
  216. package/src/components/ui/sidebar/tab-bar.tsx +10 -0
  217. package/src/components/ui/sidebar/use-plugin-panels.tsx +15 -5
  218. package/src/components/viewer/floorplan-compass-button.tsx +9 -1
  219. package/src/components/viewer/floorplan-preview-visibility.test.ts +35 -0
  220. package/src/components/viewer/floorplan-preview-visibility.ts +19 -0
  221. package/src/components/viewer/floorplan-preview.tsx +6 -18
  222. package/src/components/viewer/viewer-hover-label.tsx +34 -0
  223. package/src/components/viewer/viewer-rooms.tsx +175 -0
  224. package/src/components/viewer/viewer-scene-header.tsx +181 -145
  225. package/src/components/viewer/viewer-selection-manager.tsx +124 -0
  226. package/src/components/viewer/viewer-stage.tsx +2 -2
  227. package/src/components/viewer-zone-system.tsx +9 -9
  228. package/src/hooks/use-auto-save.integration.test.tsx +311 -0
  229. package/src/hooks/use-auto-save.ts +55 -14
  230. package/src/hooks/use-ceiling-events.test.ts +1 -0
  231. package/src/hooks/use-ceiling-events.ts +2 -0
  232. package/src/hooks/use-grid-events.test.ts +2 -0
  233. package/src/hooks/use-grid-events.ts +16 -11
  234. package/src/hooks/use-keyboard.test.ts +182 -0
  235. package/src/hooks/use-keyboard.ts +122 -91
  236. package/src/hooks/use-open-wall-ends.test.ts +99 -0
  237. package/src/hooks/use-open-wall-ends.ts +238 -0
  238. package/src/hooks/use-placement-notice.test.ts +48 -0
  239. package/src/hooks/use-placement-notice.ts +42 -0
  240. package/src/hooks/use-selected-room.ts +143 -0
  241. package/src/index.tsx +306 -39
  242. package/src/lib/catalog-panel-model.ts +39 -0
  243. package/src/lib/catalog-selection.test.ts +27 -0
  244. package/src/lib/catalog-selection.ts +8 -0
  245. package/src/lib/ceiling-edit-session.test.ts +321 -0
  246. package/src/lib/ceiling-edit-session.ts +206 -0
  247. package/src/lib/ceiling-region-session.test.ts +179 -0
  248. package/src/lib/contextual-help.test.ts +27 -1
  249. package/src/lib/contextual-help.ts +16 -9
  250. package/src/lib/continuation.test.ts +19 -1
  251. package/src/lib/continuation.ts +25 -4
  252. package/src/lib/door-interaction.ts +14 -0
  253. package/src/lib/editor-level-display.test.ts +96 -0
  254. package/src/lib/editor-level-display.ts +48 -0
  255. package/src/lib/editor-views.ts +144 -0
  256. package/src/lib/export-geometry-wait.test.ts +90 -0
  257. package/src/lib/floor-edit-session.ts +153 -0
  258. package/src/lib/floor-footprints.test.ts +563 -0
  259. package/src/lib/floor-footprints.ts +395 -0
  260. package/src/lib/floor-foundation-support.test.ts +283 -0
  261. package/src/lib/floor-opening-draft.ts +218 -0
  262. package/src/lib/floor-openings.test.ts +216 -0
  263. package/src/lib/floor-region-geometry.ts +191 -0
  264. package/src/lib/floor-region-session.test.ts +485 -0
  265. package/src/lib/floor-region-session.ts +589 -0
  266. package/src/lib/floor-region-snap.ts +101 -0
  267. package/src/lib/floor-step-hover.test.ts +32 -0
  268. package/src/lib/floorplan/annotation-visibility.ts +17 -0
  269. package/src/lib/floorplan/dimension-drive.test.ts +360 -0
  270. package/src/lib/floorplan/dimension-drive.ts +479 -0
  271. package/src/lib/floorplan/floorplan-export.test.ts +15 -9
  272. package/src/lib/floorplan/floorplan-export.tsx +32 -8
  273. package/src/lib/floorplan/floorplan-extension.ts +48 -0
  274. package/src/lib/floorplan/floorplan-mode.test.ts +4 -0
  275. package/src/lib/floorplan/floorplan-mode.ts +2 -0
  276. package/src/lib/floorplan/floorplan-pdfkit-document.ts +339 -18
  277. package/src/lib/floorplan/floorplan-pdfkit-renderer.test.ts +138 -8
  278. package/src/lib/floorplan/floorplan-pdfkit-renderer.ts +56 -25
  279. package/src/lib/floorplan/floorplan-plugin-install.test.tsx +138 -0
  280. package/src/lib/floorplan/floorplan-readonly.ts +6 -0
  281. package/src/lib/floorplan/geometry.ts +13 -6
  282. package/src/lib/floorplan/index.ts +0 -9
  283. package/src/lib/floorplan/items.ts +5 -2
  284. package/src/lib/floorplan/marks.test.ts +213 -0
  285. package/src/lib/floorplan/marks.ts +238 -0
  286. package/src/lib/floorplan/open-wall-ends.test.ts +268 -0
  287. package/src/lib/floorplan/open-wall-ends.ts +162 -0
  288. package/src/lib/floorplan/pdf-fonts/GeistMono-Bold.ttf +0 -0
  289. package/src/lib/floorplan/pdf-fonts/GeistMono-Regular.ttf +0 -0
  290. package/src/lib/floorplan/pdf-fonts/LICENSE-GeistMono.txt +92 -0
  291. package/src/lib/floorplan/pdf-fonts/LICENSE-LiberationSans.txt +102 -0
  292. package/src/lib/floorplan/pdf-fonts/LiberationSans-Bold.ttf +0 -0
  293. package/src/lib/floorplan/pdf-fonts/LiberationSans-Regular.ttf +0 -0
  294. package/src/lib/floorplan/schedules.test.ts +189 -0
  295. package/src/lib/floorplan/schedules.ts +339 -0
  296. package/src/lib/floorplan/sheet-export.test.ts +100 -0
  297. package/src/lib/floorplan/sheet-export.ts +318 -0
  298. package/src/lib/floorplan/site-plan/build-site-plan-drawing.ts +779 -0
  299. package/src/lib/floorplan/site-plan/contours.test.ts +43 -0
  300. package/src/lib/floorplan/site-plan/contributors.ts +97 -0
  301. package/src/lib/floorplan/site-plan/coverage.ts +232 -0
  302. package/src/lib/floorplan/site-plan/drawing-type-switch.test.ts +15 -0
  303. package/src/lib/floorplan/site-plan/drawing-type-switch.tsx +86 -0
  304. package/src/lib/floorplan/site-plan/front-edge.test.ts +103 -0
  305. package/src/lib/floorplan/site-plan/front-edge.ts +156 -0
  306. package/src/lib/floorplan/site-plan/geometry.test.ts +275 -0
  307. package/src/lib/floorplan/site-plan/geometry.ts +557 -0
  308. package/src/lib/floorplan/site-plan/index.ts +71 -0
  309. package/src/lib/floorplan/site-plan/site-annotations.test.ts +366 -0
  310. package/src/lib/floorplan/site-plan/site-annotations.ts +1083 -0
  311. package/src/lib/floorplan/site-plan/site-parts.ts +379 -0
  312. package/src/lib/floorplan/site-plan/site-plan-layer.tsx +138 -0
  313. package/src/lib/floorplan/site-plan/street-name.test.ts +69 -0
  314. package/src/lib/floorplan/site-plan/street-name.ts +302 -0
  315. package/src/lib/floorplan/site-plan/yards.test.ts +69 -0
  316. package/src/lib/floorplan/types.ts +1 -27
  317. package/src/lib/fresh-planar-placement.test.ts +36 -0
  318. package/src/lib/fresh-planar-placement.ts +191 -36
  319. package/src/lib/geometry-script/author.ts +152 -0
  320. package/src/lib/geometry-script/client.ts +152 -0
  321. package/src/lib/geometry-script/protocol.ts +12 -0
  322. package/src/lib/gesture-lifecycle.test.ts +595 -0
  323. package/src/lib/gesture-lifecycle.ts +290 -0
  324. package/src/lib/glb-export-zone-holes.test.ts +41 -0
  325. package/src/lib/glb-export.test.ts +544 -16
  326. package/src/lib/glb-export.ts +256 -125
  327. package/src/lib/grid-frame.test.ts +19 -0
  328. package/src/lib/grid-frame.ts +18 -0
  329. package/src/lib/history.test.ts +6 -0
  330. package/src/lib/history.ts +7 -0
  331. package/src/lib/hud-title.ts +109 -0
  332. package/src/lib/hydration-gated-storage.ts +49 -0
  333. package/src/lib/ifc-export.test.ts +174 -0
  334. package/src/lib/ifc-export.ts +201 -0
  335. package/src/lib/ifc-parts.ts +15 -0
  336. package/src/lib/inspector-expanded.test.ts +44 -0
  337. package/src/lib/inspector-expanded.ts +36 -0
  338. package/src/lib/interaction/scope.ts +27 -1
  339. package/src/lib/keyboard-pan.test.ts +184 -0
  340. package/src/lib/keyboard-pan.ts +238 -0
  341. package/src/lib/live-gesture-round-trip.test.ts +360 -0
  342. package/src/lib/lot/__fixtures__/map-dossier-miami-shores.json +1 -0
  343. package/src/lib/lot/__fixtures__/map-dossier-stpete.json +1 -0
  344. package/src/lib/lot/clean-ring.test.ts +186 -0
  345. package/src/lib/lot/clean-ring.ts +217 -0
  346. package/src/lib/lot/dossier.test.ts +202 -0
  347. package/src/lib/lot/dossier.ts +498 -0
  348. package/src/lib/lot/drop-in.test.ts +326 -0
  349. package/src/lib/lot/drop-in.ts +407 -0
  350. package/src/lib/lot/index.ts +43 -0
  351. package/src/lib/lot/lot-patch.test.ts +340 -0
  352. package/src/lib/lot/lot-patch.ts +394 -0
  353. package/src/lib/lot/parcel-provider.ts +43 -0
  354. package/src/lib/lot/terrain.test.ts +161 -0
  355. package/src/lib/lot/terrain.ts +266 -0
  356. package/src/lib/material-catalog-model.ts +74 -0
  357. package/src/lib/material-paint-panel-model.ts +80 -0
  358. package/src/lib/material-paint.ts +4 -0
  359. package/src/lib/mezzanine-actions.test.ts +342 -0
  360. package/src/lib/mezzanine-draft.test.ts +501 -0
  361. package/src/lib/mezzanine-draft.ts +278 -0
  362. package/src/lib/mezzanine-messages.ts +26 -0
  363. package/src/lib/mezzanine-selection.test.ts +207 -0
  364. package/src/lib/mezzanine-stairs.ts +46 -0
  365. package/src/lib/model-export.ts +3 -1
  366. package/src/lib/node-action-movement.ts +17 -6
  367. package/src/lib/node-display-label.ts +37 -0
  368. package/src/lib/orbit-floor-clearance.test.ts +165 -0
  369. package/src/lib/orbit-floor-clearance.ts +106 -0
  370. package/src/lib/overlay-transform.test.ts +28 -0
  371. package/src/lib/overlay-transform.ts +12 -0
  372. package/src/lib/paint-commit-route.test.ts +37 -0
  373. package/src/lib/paint-commit-route.ts +17 -0
  374. package/src/lib/paint-eyedropper.test.tsx +165 -0
  375. package/src/lib/paint-eyedropper.ts +99 -0
  376. package/src/lib/paint-pass-through.test.ts +111 -0
  377. package/src/lib/paint-pass-through.ts +76 -0
  378. package/src/lib/paint-region-hover.ts +25 -0
  379. package/src/lib/paint-region-mode.ts +154 -0
  380. package/src/lib/paint-regions.test.ts +166 -0
  381. package/src/lib/paint-regions.ts +192 -0
  382. package/src/lib/paint-scope.test.ts +317 -35
  383. package/src/lib/paint-scope.ts +266 -25
  384. package/src/lib/panel-rows.ts +96 -0
  385. package/src/lib/panel-tool-options.test.ts +34 -0
  386. package/src/lib/panel-tool-options.ts +46 -0
  387. package/src/lib/parametric-node-update.ts +37 -0
  388. package/src/lib/plate-paint-affected.test.ts +300 -0
  389. package/src/lib/plate-paint-affected.ts +177 -0
  390. package/src/lib/plugin-chat.ts +50 -0
  391. package/src/lib/plugin-panels.test.ts +25 -0
  392. package/src/lib/plugin-panels.ts +94 -0
  393. package/src/lib/print-shell-compiler.ts +1 -0
  394. package/src/lib/registered-draft-snap.ts +12 -0
  395. package/src/lib/roof-duplication.ts +10 -2
  396. package/src/lib/room-assembly-overlay.test.ts +275 -0
  397. package/src/lib/room-assembly-overlay.ts +266 -0
  398. package/src/lib/room-built-on.test.ts +379 -0
  399. package/src/lib/room-built-on.ts +138 -0
  400. package/src/lib/room-construction-commands.ts +712 -0
  401. package/src/lib/room-construction.test.ts +746 -0
  402. package/src/lib/room-construction.ts +385 -0
  403. package/src/lib/room-controls-phase6.test.ts +630 -0
  404. package/src/lib/room-divide-session.ts +523 -0
  405. package/src/lib/room-handle-drag.ts +1068 -0
  406. package/src/lib/room-name-catalog.test.ts +90 -0
  407. package/src/lib/room-name-catalog.ts +168 -0
  408. package/src/lib/room-push-dimensions.test.ts +311 -0
  409. package/src/lib/room-push-dimensions.ts +251 -0
  410. package/src/lib/room-selection-commands.ts +62 -0
  411. package/src/lib/room-selection.test.ts +973 -0
  412. package/src/lib/room-selection.ts +417 -0
  413. package/src/lib/room-structure-commands.ts +94 -0
  414. package/src/lib/room-structure.test.ts +921 -0
  415. package/src/lib/room-transform-session.ts +570 -0
  416. package/src/lib/room-zone-routing.test.ts +294 -0
  417. package/src/lib/room-zone-routing.ts +197 -0
  418. package/src/lib/scene-clipboard.test.ts +364 -0
  419. package/src/lib/scene-clipboard.ts +229 -48
  420. package/src/lib/scene-material-refs.test.ts +79 -0
  421. package/src/lib/scene-material-refs.ts +79 -0
  422. package/src/lib/scene.test.ts +87 -0
  423. package/src/lib/scene.ts +39 -45
  424. package/src/lib/selection-routing.test.ts +129 -45
  425. package/src/lib/selection-routing.ts +53 -74
  426. package/src/lib/session-writes.test.ts +120 -0
  427. package/src/lib/session-writes.ts +205 -0
  428. package/src/lib/sidebar-panel.ts +62 -0
  429. package/src/lib/site-boundary.test.ts +4 -6
  430. package/src/lib/site-boundary.ts +1 -1
  431. package/src/lib/snapping-mode.test.ts +17 -2
  432. package/src/lib/snapping-mode.ts +19 -7
  433. package/src/lib/spatial-drag-plane.test.ts +53 -0
  434. package/src/lib/spatial-drag-plane.ts +19 -0
  435. package/src/lib/spatial-pointer-input.test.ts +72 -0
  436. package/src/lib/spatial-pointer-input.ts +62 -0
  437. package/src/lib/spatial-snap.test.ts +47 -0
  438. package/src/lib/terrace-draft.test.ts +145 -0
  439. package/src/lib/terrace-draft.ts +213 -0
  440. package/src/lib/terrain-panel-model.ts +117 -0
  441. package/src/lib/tool-cursor-icon.test.ts +16 -0
  442. package/src/lib/tool-cursor-icon.ts +11 -0
  443. package/src/lib/typing-target.ts +9 -0
  444. package/src/lib/units.ts +77 -39
  445. package/src/lib/vector-edges.test.ts +58 -0
  446. package/src/lib/vector-edges.ts +400 -0
  447. package/src/lib/viewer-selection.test.ts +577 -0
  448. package/src/lib/viewer-selection.ts +569 -0
  449. package/src/lib/wall-draw-variant.test.ts +71 -0
  450. package/src/lib/wall-draw-variant.ts +91 -0
  451. package/src/lib/wall-paint-region-session.ts +246 -0
  452. package/src/lib/wall-paint-region.test.ts +373 -0
  453. package/src/lib/wall-polygon-draft.test.ts +262 -0
  454. package/src/lib/wall-polygon-draft.ts +286 -0
  455. package/src/lib/wall-push.test.ts +453 -0
  456. package/src/lib/wall-region-face.ts +352 -0
  457. package/src/lib/wall-region-handles.test.ts +304 -0
  458. package/src/lib/wall-region-handles.ts +368 -0
  459. package/src/lib/wall-region-list.test.ts +119 -0
  460. package/src/lib/wall-region-list.ts +136 -0
  461. package/src/lib/wall-region-snap.ts +110 -0
  462. package/src/lib/window-interaction.ts +14 -0
  463. package/src/lib/world-grid-snap.test.ts +48 -0
  464. package/src/lib/world-grid-snap.ts +9 -5
  465. package/src/lib/zone-content.ts +4 -1
  466. package/src/lib/zone-name.test.ts +17 -0
  467. package/src/lib/zone-name.ts +17 -0
  468. package/src/store/opens-in-select-mode.test.ts +223 -0
  469. package/src/store/paint-mode.test.ts +188 -0
  470. package/src/store/rotation-snapping.test.ts +53 -0
  471. package/src/store/terrain-sculpt-mode.test.ts +19 -21
  472. package/src/store/tool-mode.test.ts +137 -6
  473. package/src/store/use-delete-confirmation.ts +28 -1
  474. package/src/store/use-drawing-view.test.ts +24 -3
  475. package/src/store/use-drawing-view.ts +29 -1
  476. package/src/store/use-editor.tsx +442 -323
  477. package/src/store/use-floorplan-draft-preview.ts +23 -1
  478. package/src/store/use-floorplan-marquee.ts +12 -2
  479. package/src/store/use-interaction-scope.ts +155 -6
  480. package/src/store/use-quick-measurement-hud.ts +3 -3
  481. package/src/store/use-stair-build-preview.ts +13 -5
  482. package/src/store/use-wall-move-ghosts.ts +4 -2
  483. package/src/store/use-wall-region-handles-selection.test.ts +222 -0
  484. package/src/store/use-wall-region-handles-selection.ts +139 -0
  485. package/src/store/view-layout.test.ts +175 -0
  486. package/src/store/view-layout.ts +196 -0
  487. package/src/test-utils/immediate-animation-frames.ts +16 -0
  488. package/src/test-utils/live-gesture-round-trip.ts +224 -0
  489. package/src/test-utils/selection-harness.ts +82 -0
  490. package/src/components/editor-2d/renderers/floorplan-stair-layer.tsx +0 -499
  491. package/src/components/systems/zone/zone-label-editor-system.tsx +0 -333
  492. package/src/components/tools/fence/fence-drafting.test.ts +0 -100
  493. package/src/components/tools/fence/fence-drafting.ts +0 -266
  494. package/src/components/ui/sidebar/panels/plugins-panel.tsx +0 -218
  495. package/src/components/viewer/viewer-units-panel.tsx +0 -116
  496. package/src/lib/floorplan/stairs.ts +0 -489
  497. package/src/lib/level-duplication.test.ts +0 -144
  498. package/src/lib/level-duplication.ts +0 -195
@@ -0,0 +1,442 @@
1
+ import { afterAll, beforeAll, expect, test } from 'bun:test'
2
+ import { CameraControlsImpl } from '@react-three/drei'
3
+ import * as THREE from 'three'
4
+ import {
5
+ LOOK_UP_PITCH_GAIN,
6
+ LOOK_UP_PUSH_HALF_ANGLE,
7
+ MAX_ORBIT_POLAR_ANGLE,
8
+ ORBIT_FLOOR_CLEARANCE,
9
+ ORBIT_TARGET_HEIGHT,
10
+ } from '../../lib/orbit-floor-clearance'
11
+ import { FloorAwareCameraControls } from './floor-aware-camera-controls'
12
+
13
+ // No DOM in this runner: camera-controls only constructs an empty DOMRect.
14
+ const scope = globalThis as Record<string, unknown>
15
+ const stubDomRect = !scope.DOMRect
16
+ beforeAll(() => {
17
+ if (stubDomRect) scope.DOMRect = class {}
18
+ CameraControlsImpl.install({ THREE })
19
+ })
20
+ afterAll(() => {
21
+ if (stubDomRect) delete scope.DOMRect
22
+ })
23
+
24
+ const GROUND_MIN_Y = ORBIT_FLOOR_CLEARANCE
25
+ const ROOM_CENTRE = new THREE.Vector3(-2.25, 0, 3.25)
26
+ const degrees = (value: number) => (value * Math.PI) / 180
27
+
28
+ function groundFloorControls(
29
+ camera: THREE.Camera = new THREE.PerspectiveCamera(50, 1.5, 0.1, 500),
30
+ ) {
31
+ const controls = new FloorAwareCameraControls(camera as THREE.PerspectiveCamera)
32
+ controls.floorMinY = GROUND_MIN_Y
33
+ controls.maxPolarAngle = Math.PI - 0.05
34
+ // 6 m from the room centre, 45° down.
35
+ controls.setLookAt(0.75, 4.2, 6.25, ROOM_CENTRE.x, ROOM_CENTRE.y, ROOM_CENTRE.z, false)
36
+ settle(controls)
37
+ return controls
38
+ }
39
+
40
+ function settle(controls: CameraControlsImpl) {
41
+ for (let frame = 0; frame < 4; frame++) controls.update(1 / 60)
42
+ }
43
+
44
+ function pose(controls: CameraControlsImpl) {
45
+ return {
46
+ position: controls.getPosition(new THREE.Vector3(), false),
47
+ target: controls.getTarget(new THREE.Vector3(), false),
48
+ drawn: controls.camera.position.clone(),
49
+ }
50
+ }
51
+
52
+ function viewDirection(p: { position: THREE.Vector3; target: THREE.Vector3 }) {
53
+ return p.target.clone().sub(p.position).normalize()
54
+ }
55
+
56
+ function expectVectorClose(actual: THREE.Vector3, expected: THREE.Vector3, digits = 6) {
57
+ expect(actual.x).toBeCloseTo(expected.x, digits)
58
+ expect(actual.y).toBeCloseTo(expected.y, digits)
59
+ expect(actual.z).toBeCloseTo(expected.z, digits)
60
+ }
61
+
62
+ // A drag: relative, like the pointer, to the orbit's own polar angle.
63
+ function orbitTo(controls: FloorAwareCameraControls, polarDegrees: number) {
64
+ controls.rotate(0, degrees(polarDegrees) - controls.orbitPolarAngle, false)
65
+ settle(controls)
66
+ }
67
+
68
+ test('orbiting past the horizon pitches the view up fast while the camera closes in gently', () => {
69
+ const controls = groundFloorControls()
70
+ const before = pose(controls)
71
+ const startPolarDegrees = (controls.polarAngle * 180) / Math.PI
72
+ const contactPolar = Math.acos(GROUND_MIN_Y / controls.distance)
73
+ const contactReach = Math.sqrt(controls.distance ** 2 - GROUND_MIN_Y ** 2)
74
+ const straightUp = (MAX_ORBIT_POLAR_ANGLE - contactPolar) / LOOK_UP_PITCH_GAIN
75
+
76
+ let previousPitch = -Math.PI
77
+ for (const polar of [95, 100, 110, 120, 130, 145, 160, 176]) {
78
+ orbitTo(controls, polar)
79
+ const grounded = pose(controls)
80
+ const past = Math.min(degrees(polar) - contactPolar, straightUp)
81
+ expect(grounded.drawn.y).toBeCloseTo(GROUND_MIN_Y, 6)
82
+ // Pitch: LOOK_UP_PITCH_GAIN times the drag, stopping at straight up.
83
+ const pitch = Math.asin(viewDirection(grounded).y)
84
+ expect(pitch).toBeCloseTo(contactPolar + past * LOOK_UP_PITCH_GAIN - Math.PI / 2, 6)
85
+ expect(pitch).toBeGreaterThanOrEqual(previousPitch - 1e-9)
86
+ previousPitch = pitch
87
+ // Push: gentle, at the half-angle rate.
88
+ const reach = Math.hypot(grounded.drawn.x - ROOM_CENTRE.x, grounded.drawn.z - ROOM_CENTRE.z)
89
+ expect(reach).toBeCloseTo(contactReach * 0.5 ** (past / LOOK_UP_PUSH_HALF_ANGLE), 6)
90
+ // Every reader agrees with the drawn camera, so pan speed is calibrated on it.
91
+ expectVectorClose(grounded.position, grounded.drawn)
92
+ expect(grounded.position.distanceTo(grounded.target)).toBeCloseTo(controls.distance, 6)
93
+ }
94
+ // Straight up within ~80° of drag past the floor, and no further.
95
+ expect(previousPitch).toBeCloseTo(MAX_ORBIT_POLAR_ANGLE - Math.PI / 2, 6)
96
+
97
+ orbitTo(controls, startPolarDegrees)
98
+ const back = pose(controls)
99
+ expectVectorClose(back.position, before.position)
100
+ expectVectorClose(back.target, before.target)
101
+ })
102
+
103
+ test('a captured grounded view replays as the same view', () => {
104
+ const controls = groundFloorControls()
105
+ orbitTo(controls, 135)
106
+ const saved = pose(controls)
107
+
108
+ const replay = groundFloorControls()
109
+ replay.setLookAt(
110
+ saved.position.x,
111
+ saved.position.y,
112
+ saved.position.z,
113
+ saved.target.x,
114
+ saved.target.y,
115
+ saved.target.z,
116
+ false,
117
+ )
118
+ settle(replay)
119
+ const replayed = pose(replay)
120
+ expectVectorClose(replayed.drawn, saved.drawn)
121
+ expectVectorClose(viewDirection(replayed), viewDirection(saved))
122
+ })
123
+
124
+ test('a pose written under the clearance is shown exactly, then held without a jump', () => {
125
+ const controls = groundFloorControls()
126
+ controls.setLookAt(0, 0.1, 6, 0, 2, 0, false)
127
+ settle(controls)
128
+ expectVectorClose(controls.camera.position, new THREE.Vector3(0, 0.1, 6))
129
+
130
+ // Orbiting a little lower from there does not jump up to the clearance.
131
+ controls.rotate(0, degrees(1), false)
132
+ settle(controls)
133
+ expect(controls.camera.position.y).toBeLessThan(0.1 + 1e-9)
134
+ expect(controls.camera.position.y).toBeGreaterThan(0.05)
135
+
136
+ // Rising past the clearance and restoring the same saved view honours it again.
137
+ controls.rotateTo(controls.azimuthAngle, degrees(30), false)
138
+ settle(controls)
139
+ expect(controls.camera.position.y).toBeGreaterThan(GROUND_MIN_Y)
140
+ controls.setLookAt(0, 0.1, 6, 0, 2, 0, false)
141
+ settle(controls)
142
+ expectVectorClose(controls.camera.position, new THREE.Vector3(0, 0.1, 6))
143
+ })
144
+
145
+ test('a held low pose does not survive moving to another floor', () => {
146
+ const controls = groundFloorControls()
147
+ controls.setLookAt(0, 0.1, 6, 0, 2, 0, false)
148
+ settle(controls)
149
+
150
+ // The upper level is selected: its floor is 2.71 and the target follows it.
151
+ controls.floorMinY = 2.71 + ORBIT_FLOOR_CLEARANCE
152
+ const target = controls.getTarget(new THREE.Vector3())
153
+ controls.moveTo(target.x, target.y + 2.71, target.z, false)
154
+ settle(controls)
155
+ expect(controls.camera.position.y).toBeCloseTo(2.71 + ORBIT_FLOOR_CLEARANCE, 6)
156
+ })
157
+
158
+ test('an orthographic camera is never moved by the floor', () => {
159
+ const controls = groundFloorControls(new THREE.OrthographicCamera(-5, 5, 5, -5, 0.1, 500))
160
+ orbitTo(controls, 135)
161
+ const orbit = pose(controls)
162
+ expect(orbit.drawn.y).toBeLessThan(0)
163
+ expectVectorClose(orbit.target, ROOM_CENTRE)
164
+ })
165
+
166
+ test('update listeners observe the grounded pose throughout damping and smooth time changes', () => {
167
+ const controls = groundFloorControls()
168
+ const before = pose(controls)
169
+ const startPolar = controls.polarAngle
170
+ orbitTo(controls, 135)
171
+ let published = pose(controls)
172
+ controls.addEventListener('update', () => {
173
+ published = pose(controls)
174
+ })
175
+ controls.smoothTime = 0.5
176
+ controls.rotatePolarTo(degrees(160), true)
177
+ for (let frame = 0; frame < 180; frame++) {
178
+ if (frame === 20) controls.smoothTime = 0.08
179
+ controls.update(1 / 60)
180
+ expectVectorClose(published.position, controls.camera.position)
181
+ expect(published.position.y).toBeGreaterThanOrEqual(GROUND_MIN_Y - 1e-9)
182
+ }
183
+ controls.rotatePolarTo(startPolar, true)
184
+ for (let frame = 0; frame < 180; frame++) controls.update(1 / 60)
185
+ expectVectorClose(controls.camera.position, before.drawn)
186
+ expectVectorClose(controls.getTarget(new THREE.Vector3(), false), before.target)
187
+ })
188
+
189
+ test('changing the floor while idle publishes the corrected pose in the same update', () => {
190
+ const controls = groundFloorControls()
191
+ orbitTo(controls, 135)
192
+ let published: THREE.Vector3 | null = null
193
+ controls.addEventListener('update', () => {
194
+ published = controls.getPosition(new THREE.Vector3(), false)
195
+ })
196
+ controls.floorMinY = 3.91
197
+ expect(controls.update(1 / 60)).toBe(true)
198
+ expect(published).not.toBeNull()
199
+ expectVectorClose(published!, controls.camera.position)
200
+ expect(controls.camera.position.y).toBeCloseTo(3.91, 6)
201
+ })
202
+
203
+ test('fitting a box replaces a grounded target and keeps the box centred when orbiting back', () => {
204
+ const controls = groundFloorControls()
205
+ orbitTo(controls, 110)
206
+ const box = new THREE.Box3(new THREE.Vector3(-3, 0, 2), new THREE.Vector3(-1, 3, 4))
207
+ const centre = box.getCenter(new THREE.Vector3())
208
+ controls.fitToBox(box, false)
209
+ settle(controls)
210
+ expectVectorClose(controls.getTarget(new THREE.Vector3(), false), centre)
211
+ controls.rotatePolarTo(degrees(45), false)
212
+ settle(controls)
213
+ expectVectorClose(controls.getTarget(new THREE.Vector3(), false), centre)
214
+ })
215
+
216
+ test('relative navigation while grounded preserves its translation when orbiting back', () => {
217
+ const controls = groundFloorControls()
218
+ const startPolar = controls.polarAngle
219
+ const expected = groundFloorControls()
220
+ orbitTo(controls, 135)
221
+ controls.truck(1, 0, false)
222
+ controls.forward(2, false)
223
+ controls.elevate(0.5, false)
224
+ settle(controls)
225
+ expected.truck(1, 0, false)
226
+ expected.forward(2, false)
227
+ expected.elevate(0.5, false)
228
+ settle(expected)
229
+ controls.rotatePolarTo(startPolar, false)
230
+ settle(controls)
231
+ expectVectorClose(controls.camera.position, expected.camera.position)
232
+ expectVectorClose(
233
+ controls.getTarget(new THREE.Vector3(), false),
234
+ expected.getTarget(new THREE.Vector3(), false),
235
+ )
236
+ })
237
+
238
+ test('switching floors from a grounded orbit preserves horizontal framing on the round trip', () => {
239
+ const controls = groundFloorControls()
240
+ orbitTo(controls, 135)
241
+ const before = pose(controls)
242
+ const target = new THREE.Vector3()
243
+ for (const floorY of [2.71, 0]) {
244
+ controls.floorMinY = floorY + GROUND_MIN_Y
245
+ controls.getOrbitTarget(target)
246
+ controls.moveTo(target.x, floorY, target.z, true)
247
+ for (let frame = 0; frame < 180; frame++) controls.update(1 / 60)
248
+ expect(controls.camera.position.y).toBeCloseTo(floorY + GROUND_MIN_Y, 6)
249
+ expect(controls.camera.position.x).toBeCloseTo(before.drawn.x, 6)
250
+ expect(controls.camera.position.z).toBeCloseTo(before.drawn.z, 6)
251
+ }
252
+ expectVectorClose(controls.getTarget(new THREE.Vector3(), false), before.target)
253
+ })
254
+
255
+ test('turning a quarter around the orbit while looking up keeps the look-up pitch', () => {
256
+ const controls = groundFloorControls()
257
+ orbitTo(controls, 110)
258
+ const pitch = Math.asin(viewDirection(pose(controls)).y)
259
+ // The 90° orbit shortcut: an absolute azimuth with the orbit's own polar angle.
260
+ controls.rotateTo(controls.azimuthAngle + Math.PI / 2, controls.orbitPolarAngle, false)
261
+ settle(controls)
262
+ expect(Math.asin(viewDirection(pose(controls)).y)).toBeCloseTo(pitch, 6)
263
+ expect(controls.camera.position.y).toBeCloseTo(GROUND_MIN_Y, 6)
264
+ })
265
+
266
+ test('a pivot at standing height: zoomed in, the orbit looks around at eye level and pans level', () => {
267
+ const controls = groundFloorControls()
268
+ const pivotY = ORBIT_TARGET_HEIGHT
269
+ controls.setLookAt(-2.25, pivotY, 3.25 + 2, ROOM_CENTRE.x, pivotY, ROOM_CENTRE.z, false)
270
+ settle(controls)
271
+ // Turning around the room at eye height: the camera stays on its orbit.
272
+ for (const azimuth of [0, Math.PI / 2, Math.PI]) {
273
+ controls.rotateTo(azimuth, Math.PI / 2, false)
274
+ settle(controls)
275
+ expect(controls.camera.position.y).toBeCloseTo(pivotY, 6)
276
+ expect(controls.camera.position.distanceTo(pose(controls).target)).toBeCloseTo(2, 6)
277
+ }
278
+ // Tilting to look 15° up keeps the camera above the clearance, unpushed.
279
+ orbitTo(controls, 105)
280
+ expect(controls.camera.position.y).toBeGreaterThan(GROUND_MIN_Y)
281
+ expect(Math.asin(viewDirection(pose(controls)).y)).toBeCloseTo(degrees(15), 6)
282
+ // Panning (the editor's level pan: truck sideways, forward) keeps the pivot height.
283
+ controls.truck(1.5, 0, false)
284
+ controls.forward(2, false)
285
+ settle(controls)
286
+ expect(controls.getOrbitTarget(new THREE.Vector3()).y).toBeCloseTo(pivotY, 6)
287
+ })
288
+
289
+ // A raised platform (top at 0.45) over the room's z > 3.5 half, floor elsewhere.
290
+ function platformSurface() {
291
+ const surface = {
292
+ revision: 0,
293
+ rebuilding: false,
294
+ topAt(_x: number, z: number) {
295
+ if (surface.rebuilding) return null
296
+ return z > 3.5 ? 0.45 : 0.05
297
+ },
298
+ }
299
+ return surface
300
+ }
301
+
302
+ function settleLong(controls: CameraControlsImpl, frames = 90) {
303
+ for (let frame = 0; frame < frames; frame++) controls.update(1 / 60)
304
+ }
305
+
306
+ test('the floor limit follows a raised slab under the camera, gliding at its edge', () => {
307
+ const surface = platformSurface()
308
+ const controls = groundFloorControls()
309
+ controls.floorSurface = surface
310
+ // Low over the platform, toward the wall on the +z side.
311
+ controls.setLookAt(-2.25, 1.1, 5.25, -2.25, 1.1, 3.25, false)
312
+ settleLong(controls)
313
+ orbitTo(controls, 150)
314
+ settleLong(controls)
315
+ expect(controls.camera.position.y).toBeCloseTo(0.45 + GROUND_MIN_Y, 4)
316
+
317
+ // Swing round to over the plain floor: the limit glides down, no pop.
318
+ let previous = controls.camera.position.y
319
+ controls.rotate(Math.PI, 0, false)
320
+ for (let frame = 0; frame < 120; frame++) {
321
+ controls.update(1 / 60)
322
+ expect(Math.abs(controls.camera.position.y - previous)).toBeLessThan(0.07)
323
+ previous = controls.camera.position.y
324
+ }
325
+ expect(controls.camera.position.z).toBeLessThan(3)
326
+ expect(controls.camera.position.y).toBeCloseTo(0.05 + GROUND_MIN_Y, 4)
327
+ })
328
+
329
+ test('a slab missing for a moment while it is rebuilt does not drop the camera through it', () => {
330
+ const surface = platformSurface()
331
+ const controls = groundFloorControls()
332
+ controls.floorSurface = surface
333
+ controls.setLookAt(-2.25, 1.1, 5.25, -2.25, 1.1, 3.25, false)
334
+ settleLong(controls)
335
+ orbitTo(controls, 150)
336
+ settleLong(controls)
337
+ const y = controls.camera.position.y
338
+ surface.rebuilding = true
339
+ surface.revision += 1
340
+ for (let frame = 0; frame < 4; frame++) {
341
+ controls.update(1 / 60)
342
+ expect(controls.camera.position.y).toBeCloseTo(y, 6)
343
+ }
344
+ surface.rebuilding = false
345
+ settleLong(controls, 4)
346
+ expect(controls.camera.position.y).toBeCloseTo(y, 6)
347
+ })
348
+
349
+ test('with nothing under the camera the limit is the level floor plus the clearance, never 0', () => {
350
+ const controls = groundFloorControls()
351
+ controls.floorMinY = 2.71 + GROUND_MIN_Y
352
+ controls.floorSurface = { revision: 0, topAt: () => null }
353
+ controls.setLookAt(-2.25, 3.81, 5.25, -2.25, 3.81, 3.25, false)
354
+ settleLong(controls)
355
+ orbitTo(controls, 150)
356
+ settleLong(controls)
357
+ expect(controls.camera.position.y).toBeCloseTo(2.71 + GROUND_MIN_Y, 6)
358
+ })
359
+
360
+ test('a pose written low over a raised slab is lifted out of the slab, then orbits above it', () => {
361
+ const surface = platformSurface()
362
+ const controls = groundFloorControls()
363
+ controls.floorSurface = surface
364
+ controls.setLookAt(-2.25, 1.1, 5.25, -2.25, 1.1, 3.25, false)
365
+ settleLong(controls)
366
+ // As a focus clamped to the level's floor would write it: under the platform top.
367
+ controls.setLookAt(-2.25, GROUND_MIN_Y, 5.25, -2.25, 1.1, 3.25, false)
368
+ settleLong(controls)
369
+ expect(controls.camera.position.y).toBeGreaterThanOrEqual(0.45 - 1e-6)
370
+ orbitTo(controls, 150)
371
+ settleLong(controls)
372
+ expect(controls.camera.position.y).toBeGreaterThanOrEqual(0.45 - 1e-6)
373
+ })
374
+
375
+ test('a camera that drops from high above straight under a raised floor is held above it at once', () => {
376
+ const controls = groundFloorControls()
377
+ // A 1 m platform everywhere: the level's own floor limit would be inside it.
378
+ controls.floorSurface = { revision: 0, topAt: () => 1 }
379
+ controls.setLookAt(-2.25, 9, 5.25, -2.25, 1.1, 3.25, false)
380
+ settleLong(controls)
381
+ // Every pose published, from the very first frame of one fast motion down.
382
+ const published: number[] = []
383
+ controls.addEventListener('update', () => published.push(controls.camera.position.y))
384
+ controls.rotate(0, degrees(120), false)
385
+ controls.dollyTo(2, false)
386
+ controls.update(1 / 60)
387
+ expect(published.length).toBeGreaterThan(0)
388
+ for (const y of published) expect(y).toBeGreaterThan(1)
389
+ expect(controls.camera.position.y).toBeGreaterThan(1)
390
+ settleLong(controls)
391
+ expect(controls.camera.position.y).toBeCloseTo(1 + GROUND_MIN_Y, 4)
392
+ })
393
+
394
+ test('over a hole in the floor the camera keeps the floor it came from, not the level below', () => {
395
+ // A stair void over z < 3; a 0.5 m floor elsewhere.
396
+ const controls = groundFloorControls()
397
+ controls.floorSurface = { revision: 0, topAt: (_x: number, z: number) => (z < 3 ? null : 0.5) }
398
+ controls.setLookAt(-2.25, 1.6, 5.25, -2.25, 1.6, 3.25, false)
399
+ settleLong(controls)
400
+ orbitTo(controls, 150)
401
+ settleLong(controls)
402
+ expect(controls.camera.position.y).toBeCloseTo(0.5 + GROUND_MIN_Y, 4)
403
+ // Swing round over the void.
404
+ controls.rotate(Math.PI, 0, false)
405
+ settleLong(controls, 120)
406
+ expect(controls.camera.position.z).toBeLessThan(3)
407
+ expect(controls.camera.position.y).toBeCloseTo(0.5 + GROUND_MIN_Y, 4)
408
+ })
409
+
410
+ test('the orbit never aims below the level floor: a destination under it is lifted smoothly', () => {
411
+ const controls = groundFloorControls()
412
+ controls.floorMinY = 2.71 + GROUND_MIN_Y
413
+ controls.moveTo(-2.25, 2.71 + 1.1, 3.25, false)
414
+ settleLong(controls)
415
+ // A move aimed through the floor, down to the level below.
416
+ controls.moveTo(-2.25, 1.1, 3.25, true)
417
+ let previous = controls.camera.position.y
418
+ for (let frame = 0; frame < 120; frame++) {
419
+ controls.update(1 / 60)
420
+ // The destination is held at the floor; the drawn camera moves smoothly.
421
+ expect(controls.getOrbitTarget(new THREE.Vector3()).y).toBeGreaterThanOrEqual(2.71 - 1e-6)
422
+ expect(Math.abs(controls.camera.position.y - previous)).toBeLessThan(0.2)
423
+ previous = controls.camera.position.y
424
+ }
425
+ expect(controls.camera.position.y).toBeGreaterThanOrEqual(2.71 + GROUND_MIN_Y - 1e-6)
426
+ })
427
+
428
+ test('once the user moves the camera, a written low pose eases up to the clearance', () => {
429
+ const controls = groundFloorControls()
430
+ controls.floorSurface = { revision: 0, topAt: () => 0.05 }
431
+ controls.setLookAt(-2.25, 0.15, 5.25, -2.25, 1.1, 3.25, false)
432
+ settleLong(controls)
433
+ expect(controls.camera.position.y).toBeCloseTo(0.15, 6)
434
+ controls.releaseWrittenPose()
435
+ let previous = controls.camera.position.y
436
+ for (let frame = 0; frame < 90; frame++) {
437
+ controls.update(1 / 60)
438
+ expect(controls.camera.position.y - previous).toBeLessThan(0.07)
439
+ previous = controls.camera.position.y
440
+ }
441
+ expect(controls.camera.position.y).toBeCloseTo(0.05 + GROUND_MIN_Y, 4)
442
+ })