@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,184 @@
1
+ import { afterAll, beforeAll, expect, test } from 'bun:test'
2
+ import {
3
+ acceptsKeyboardPan,
4
+ advanceKeyboardPanMotion,
5
+ clearKeyboardPanKeys,
6
+ createKeyboardPanMotion,
7
+ hasKeyboardPanInput,
8
+ isKeyboardPanKey,
9
+ KEYBOARD_PAN_RAMP_SECONDS,
10
+ KEYBOARD_PAN_START_FRACTION,
11
+ KEYBOARD_PAN_STOP_SECONDS,
12
+ type KeyboardPanState,
13
+ keyboardPanSpeed,
14
+ setKeyboardPanKey,
15
+ syncKeyboardPanMotion,
16
+ } from './keyboard-pan'
17
+
18
+ const idle = (): KeyboardPanState => ({
19
+ forward: false,
20
+ backward: false,
21
+ left: false,
22
+ right: false,
23
+ })
24
+
25
+ /**
26
+ * Frames every `1000 / hz` ms from `phaseMs`; `codes` go down at `pressMs` and
27
+ * up `holdMs` later, synced at the edges like the views do. Returns the total
28
+ * travel in cruise-seconds and how long the glide lasted.
29
+ */
30
+ function simulate({
31
+ codes = ['KeyD'],
32
+ holdMs,
33
+ hz = 60,
34
+ phaseMs = 0,
35
+ pressMs = 1000,
36
+ }: {
37
+ codes?: string[]
38
+ holdMs: number
39
+ hz?: number
40
+ phaseMs?: number
41
+ pressMs?: number
42
+ }) {
43
+ const motion = createKeyboardPanMotion()
44
+ const keys = idle()
45
+ const releaseMs = pressMs + holdMs
46
+ let x = 0
47
+ let y = 0
48
+ let pressed = false
49
+ let released = false
50
+ let restMs = releaseMs
51
+ for (let now = phaseMs; now < releaseMs + 2000; now += 1000 / hz) {
52
+ if (!pressed && now >= pressMs) {
53
+ syncKeyboardPanMotion(motion, keys, pressMs)
54
+ for (const code of codes) setKeyboardPanKey(keys, code, true)
55
+ pressed = true
56
+ }
57
+ if (!released && now >= releaseMs) {
58
+ syncKeyboardPanMotion(motion, keys, releaseMs)
59
+ clearKeyboardPanKeys(keys)
60
+ released = true
61
+ }
62
+ if (advanceKeyboardPanMotion(motion, keys, now)) restMs = now
63
+ x += motion.stepX
64
+ y += motion.stepY
65
+ }
66
+ return { x, y, glideMs: restMs - releaseMs, motion }
67
+ }
68
+
69
+ const key = (init: Partial<KeyboardEvent>) => ({ target: null, ...init }) as KeyboardEvent
70
+
71
+ // No DOM in this runner: stand in for the element classes the editable check reads.
72
+ const DOM_CLASSES = ['HTMLElement', 'HTMLInputElement', 'HTMLTextAreaElement', 'HTMLSelectElement']
73
+ const stubbed: string[] = []
74
+ beforeAll(() => {
75
+ const scope = globalThis as Record<string, unknown>
76
+ for (const name of DOM_CLASSES) {
77
+ if (scope[name]) continue
78
+ scope[name] = class {}
79
+ stubbed.push(name)
80
+ }
81
+ })
82
+ afterAll(() => {
83
+ for (const name of stubbed) delete (globalThis as Record<string, unknown>)[name]
84
+ })
85
+
86
+ test('physical WASD keys drive a screen-space direction; letters on other layouts do not', () => {
87
+ const state = idle()
88
+ expect(isKeyboardPanKey('KeyZ')).toBe(false)
89
+ expect(setKeyboardPanKey(state, 'KeyW', true)).toBe(true)
90
+ expect(setKeyboardPanKey(state, 'KeyW', true)).toBe(false)
91
+ setKeyboardPanKey(state, 'KeyD', true)
92
+ const motion = createKeyboardPanMotion()
93
+ syncKeyboardPanMotion(motion, state, 0)
94
+ advanceKeyboardPanMotion(motion, state, 100)
95
+ expect(motion.stepX).toBeGreaterThan(0)
96
+ expect(motion.stepY).toBeCloseTo(motion.stepX)
97
+ setKeyboardPanKey(state, 'KeyA', true)
98
+ advanceKeyboardPanMotion(motion, state, 200)
99
+ expect(motion.stepX).toBe(0)
100
+ expect(motion.stepY).toBeGreaterThan(0)
101
+ clearKeyboardPanKeys(state)
102
+ expect(hasKeyboardPanInput(state)).toBe(false)
103
+ })
104
+
105
+ test('modifier chords stay shortcuts, and typing in a field never pans', () => {
106
+ expect(acceptsKeyboardPan(key({}))).toBe(true)
107
+ for (const modifier of ['metaKey', 'ctrlKey', 'altKey'] as const)
108
+ expect(acceptsKeyboardPan(key({ [modifier]: true }))).toBe(false)
109
+ const Input = (globalThis as unknown as { HTMLInputElement: new () => EventTarget })
110
+ .HTMLInputElement
111
+ expect(acceptsKeyboardPan(key({ target: new Input() }))).toBe(false)
112
+ })
113
+
114
+ test('cruise speed scales with the visible width within fixed bounds', () => {
115
+ expect(keyboardPanSpeed(0)).toBe(0.5)
116
+ expect(keyboardPanSpeed(20)).toBeCloseTo(6)
117
+ expect(keyboardPanSpeed(1000)).toBe(30)
118
+ })
119
+
120
+ test('a press starts just under cruise and reaches it within the ramp', () => {
121
+ const at = (holdSeconds: number) => {
122
+ const motion = createKeyboardPanMotion()
123
+ syncKeyboardPanMotion(motion, idle(), 0)
124
+ advanceKeyboardPanMotion(motion, { ...idle(), right: true }, holdSeconds * 1000)
125
+ return motion.velocityX
126
+ }
127
+ expect(at(0.001)).toBeCloseTo(KEYBOARD_PAN_START_FRACTION, 2)
128
+ expect(at(KEYBOARD_PAN_RAMP_SECONDS / 2)).toBeGreaterThan(KEYBOARD_PAN_START_FRACTION)
129
+ expect(at(KEYBOARD_PAN_RAMP_SECONDS / 2)).toBeLessThan(1)
130
+ expect(at(KEYBOARD_PAN_RAMP_SECONDS)).toBe(1)
131
+ })
132
+
133
+ test('a tap moves a small, predictable amount and a hold travels at cruise', () => {
134
+ // 100 ms ramping up from the start fraction, plus a short glide.
135
+ const tap = simulate({ holdMs: 100 }).x
136
+ expect(tap).toBeGreaterThan(0.1 * KEYBOARD_PAN_START_FRACTION)
137
+ expect(tap).toBeLessThan(0.1)
138
+ const hold = simulate({ holdMs: 2000 }).x
139
+ expect(hold).toBeGreaterThan(1.98)
140
+ expect(hold).toBeLessThan(2.02)
141
+ })
142
+
143
+ test('a tap counts from the key events, not from the frames around them', () => {
144
+ const taps = [0, 5, 11, 16].map((phaseMs) => simulate({ holdMs: 100, phaseMs }).x)
145
+ const slowFrames = [0, 40, 80].map((phaseMs) => simulate({ holdMs: 100, hz: 10, phaseMs }).x)
146
+ for (const tap of [...taps, ...slowFrames]) expect(tap).toBeCloseTo(taps[0] ?? 0, 2)
147
+ // Shorter than a frame still moves, by its own length.
148
+ expect(simulate({ holdMs: 8, phaseMs: 1 }).x).toBeGreaterThan(0.006)
149
+ })
150
+
151
+ test('release comes to rest within the stop time, with a barely visible glide', () => {
152
+ const hold = simulate({ codes: ['KeyW'], holdMs: 1000 })
153
+ expect(hold.glideMs).toBeLessThanOrEqual(KEYBOARD_PAN_STOP_SECONDS * 1000 + 1000 / 60)
154
+ // Glide under 3% of a cruise-second: about 1% of the view width.
155
+ const held1s = 1 - ((1 - KEYBOARD_PAN_START_FRACTION) * KEYBOARD_PAN_RAMP_SECONDS) / 3
156
+ expect(hold.y - held1s).toBeGreaterThan(0)
157
+ expect(hold.y - held1s).toBeLessThan(0.03)
158
+ expect(hold.motion.velocityY).toBe(0)
159
+ })
160
+
161
+ test('travel does not depend on the frame rate', () => {
162
+ const at60 = simulate({ holdMs: 1000 }).x
163
+ expect(simulate({ holdMs: 1000, hz: 144 }).x).toBeCloseTo(at60, 3)
164
+ expect(simulate({ holdMs: 1000, hz: 20 }).x).toBeCloseTo(at60, 2)
165
+ })
166
+
167
+ test('diagonals move no faster than a straight press', () => {
168
+ const straight = simulate({ holdMs: 1000 })
169
+ const diagonal = simulate({ codes: ['KeyD', 'KeyW'], holdMs: 1000 })
170
+ expect(Math.hypot(diagonal.x, diagonal.y)).toBeCloseTo(straight.x)
171
+ expect(diagonal.x).toBeCloseTo(diagonal.y)
172
+ })
173
+
174
+ test('a press during the glide resumes from the current speed', () => {
175
+ const motion = createKeyboardPanMotion()
176
+ const right = { ...idle(), right: true }
177
+ syncKeyboardPanMotion(motion, idle(), 0)
178
+ syncKeyboardPanMotion(motion, right, 1000)
179
+ syncKeyboardPanMotion(motion, idle(), 1002)
180
+ const glidingSpeed = motion.velocityX
181
+ expect(glidingSpeed).toBeGreaterThan(KEYBOARD_PAN_START_FRACTION)
182
+ advanceKeyboardPanMotion(motion, right, 1003)
183
+ expect(motion.velocityX).toBeGreaterThanOrEqual(glidingSpeed)
184
+ })
@@ -0,0 +1,238 @@
1
+ // WASD navigation shared by the 3D camera and the 2D floor plan, so both views
2
+ // move the same way. Keys match by physical position (`event.code`): the
3
+ // cluster stays under the left hand on any layout (Z/Q/S/D on AZERTY).
4
+
5
+ export type KeyboardPanState = {
6
+ forward: boolean
7
+ backward: boolean
8
+ left: boolean
9
+ right: boolean
10
+ }
11
+
12
+ // Tuning. Speeds are in visible view widths per second, so a pan feels the
13
+ // same at any zoom; the world-unit clamps only bite on tiny or huge views.
14
+ /** Cruise speed once a key is held, in view widths per second. */
15
+ export const KEYBOARD_PAN_CRUISE_VIEW_WIDTHS_PER_SECOND = 0.3
16
+ /** Share of cruise speed a fresh press starts at. */
17
+ export const KEYBOARD_PAN_START_FRACTION = 0.55
18
+ /** Seconds of holding to reach cruise speed. */
19
+ export const KEYBOARD_PAN_RAMP_SECONDS = 0.22
20
+ /** Seconds to come to rest after the last key is released. */
21
+ export const KEYBOARD_PAN_STOP_SECONDS = 0.09
22
+ const KEYBOARD_PAN_MIN_SPEED = 0.5
23
+ const KEYBOARD_PAN_MAX_SPEED = 30
24
+ // A stalled frame (tab switch, long task) must not turn into one big jump.
25
+ const KEYBOARD_PAN_MAX_STEP_SECONDS = 0.25
26
+ // Release decays exponentially; three time constants leave 5%, then it snaps to rest.
27
+ const KEYBOARD_PAN_STOP_TIME_CONSTANT = KEYBOARD_PAN_STOP_SECONDS / 3
28
+ const KEYBOARD_PAN_REST_FRACTION = 0.05
29
+
30
+ export function isEditableKeyboardTarget(target: EventTarget | null) {
31
+ return (
32
+ target instanceof HTMLInputElement ||
33
+ target instanceof HTMLTextAreaElement ||
34
+ target instanceof HTMLSelectElement ||
35
+ (target instanceof HTMLElement && target.isContentEditable)
36
+ )
37
+ }
38
+
39
+ export function setKeyboardPanKey(
40
+ state: KeyboardPanState,
41
+ code: string,
42
+ pressed: boolean,
43
+ ): boolean {
44
+ if (code === 'KeyW') {
45
+ const changed = state.forward !== pressed
46
+ state.forward = pressed
47
+ return changed
48
+ }
49
+ if (code === 'KeyS') {
50
+ const changed = state.backward !== pressed
51
+ state.backward = pressed
52
+ return changed
53
+ }
54
+ if (code === 'KeyA') {
55
+ const changed = state.left !== pressed
56
+ state.left = pressed
57
+ return changed
58
+ }
59
+ if (code === 'KeyD') {
60
+ const changed = state.right !== pressed
61
+ state.right = pressed
62
+ return changed
63
+ }
64
+ return false
65
+ }
66
+
67
+ export function isKeyboardPanKey(code: string): boolean {
68
+ return code === 'KeyW' || code === 'KeyA' || code === 'KeyS' || code === 'KeyD'
69
+ }
70
+
71
+ export function hasKeyboardPanInput(state: KeyboardPanState): boolean {
72
+ return state.forward || state.backward || state.left || state.right
73
+ }
74
+
75
+ export function clearKeyboardPanKeys(state: KeyboardPanState) {
76
+ state.forward = false
77
+ state.backward = false
78
+ state.left = false
79
+ state.right = false
80
+ }
81
+
82
+ /** Pan keys are ignored with a modifier held (shortcuts) or while typing. */
83
+ export function acceptsKeyboardPan(event: KeyboardEvent) {
84
+ return (
85
+ !(event.metaKey || event.ctrlKey || event.altKey) && !isEditableKeyboardTarget(event.target)
86
+ )
87
+ }
88
+
89
+ /** Cruise speed, in world units per second, for a view `viewWidth` wide. */
90
+ export function keyboardPanSpeed(viewWidth: number) {
91
+ return Math.min(
92
+ Math.max(viewWidth * KEYBOARD_PAN_CRUISE_VIEW_WIDTHS_PER_SECOND, KEYBOARD_PAN_MIN_SPEED),
93
+ KEYBOARD_PAN_MAX_SPEED,
94
+ )
95
+ }
96
+
97
+ /**
98
+ * Per-view motion carried between frames. Velocity and step are screen space
99
+ * (+x right, +y forward) in shares of cruise speed; multiply the step by
100
+ * `keyboardPanSpeed` for world units.
101
+ */
102
+ export type KeyboardPanMotion = {
103
+ holding: boolean
104
+ /** Time on the ramp's clock, so a press during the glide resumes its speed. */
105
+ heldSeconds: number
106
+ velocityX: number
107
+ velocityY: number
108
+ /** This frame's travel, in cruise-seconds. */
109
+ stepX: number
110
+ stepY: number
111
+ // Travel integrated at key edges since the last frame.
112
+ pendingX: number
113
+ pendingY: number
114
+ /** `performance.now()` the motion was last advanced to; null before the first press. */
115
+ time: number | null
116
+ }
117
+
118
+ export function createKeyboardPanMotion(): KeyboardPanMotion {
119
+ return {
120
+ holding: false,
121
+ heldSeconds: 0,
122
+ velocityX: 0,
123
+ velocityY: 0,
124
+ stepX: 0,
125
+ stepY: 0,
126
+ pendingX: 0,
127
+ pendingY: 0,
128
+ time: null,
129
+ }
130
+ }
131
+
132
+ export function resetKeyboardPanMotion(motion: KeyboardPanMotion) {
133
+ motion.holding = false
134
+ motion.heldSeconds = 0
135
+ motion.velocityX = 0
136
+ motion.velocityY = 0
137
+ motion.stepX = 0
138
+ motion.stepY = 0
139
+ motion.pendingX = 0
140
+ motion.pendingY = 0
141
+ motion.time = null
142
+ }
143
+
144
+ // Speed eases out of the start fraction into cruise: s(u) = S + (1 - S)(2u - u²), u = t / ramp.
145
+ const rampSpeed = (seconds: number) => {
146
+ const u = Math.min(seconds / KEYBOARD_PAN_RAMP_SECONDS, 1)
147
+ return KEYBOARD_PAN_START_FRACTION + (1 - KEYBOARD_PAN_START_FRACTION) * (2 * u - u * u)
148
+ }
149
+
150
+ // ∫₀ᵗ s, exact, so the distance does not depend on the frame rate.
151
+ const rampDistance = (seconds: number) => {
152
+ const u = seconds / KEYBOARD_PAN_RAMP_SECONDS
153
+ const eased = u < 1 ? u * u - (u * u * u) / 3 : u - 1 / 3
154
+ return (
155
+ KEYBOARD_PAN_START_FRACTION * seconds +
156
+ (1 - KEYBOARD_PAN_START_FRACTION) * KEYBOARD_PAN_RAMP_SECONDS * eased
157
+ )
158
+ }
159
+
160
+ const rampSecondsForSpeed = (speed: number) => {
161
+ const eased = (speed - KEYBOARD_PAN_START_FRACTION) / (1 - KEYBOARD_PAN_START_FRACTION)
162
+ if (eased <= 0) return 0
163
+ if (eased >= 1) return KEYBOARD_PAN_RAMP_SECONDS
164
+ return KEYBOARD_PAN_RAMP_SECONDS * (1 - Math.sqrt(1 - eased))
165
+ }
166
+
167
+ /**
168
+ * Integrates `motion` up to `now` (`performance.now()`) under `keys`. Call it
169
+ * with the keys as they were just before one changes, so a press or release
170
+ * counts from the moment it happened rather than from the next frame.
171
+ */
172
+ export function syncKeyboardPanMotion(
173
+ motion: KeyboardPanMotion,
174
+ keys: KeyboardPanState,
175
+ now: number,
176
+ ) {
177
+ const elapsed = motion.time === null ? 0 : (now - motion.time) / 1000
178
+ motion.time = motion.time === null ? now : Math.max(motion.time, now)
179
+ const dt = Math.min(Math.max(elapsed, 0), KEYBOARD_PAN_MAX_STEP_SECONDS)
180
+ const horizontal = (keys.right ? 1 : 0) - (keys.left ? 1 : 0)
181
+ const vertical = (keys.forward ? 1 : 0) - (keys.backward ? 1 : 0)
182
+
183
+ if (horizontal !== 0 || vertical !== 0) {
184
+ if (!motion.holding) {
185
+ motion.holding = true
186
+ motion.heldSeconds = rampSecondsForSpeed(Math.hypot(motion.velocityX, motion.velocityY))
187
+ }
188
+ const from = motion.heldSeconds
189
+ motion.heldSeconds = from + dt
190
+ // Diagonals travel at the same speed as straight moves.
191
+ const length = Math.hypot(horizontal, vertical)
192
+ const distance = rampDistance(motion.heldSeconds) - rampDistance(from)
193
+ const speed = rampSpeed(motion.heldSeconds)
194
+ motion.pendingX += (horizontal / length) * distance
195
+ motion.pendingY += (vertical / length) * distance
196
+ motion.velocityX = (horizontal / length) * speed
197
+ motion.velocityY = (vertical / length) * speed
198
+ return
199
+ }
200
+
201
+ motion.holding = false
202
+ motion.heldSeconds = 0
203
+ if (motion.velocityX === 0 && motion.velocityY === 0) return
204
+ const decay = Math.exp(-dt / KEYBOARD_PAN_STOP_TIME_CONSTANT)
205
+ const glide = KEYBOARD_PAN_STOP_TIME_CONSTANT * (1 - decay)
206
+ motion.pendingX += motion.velocityX * glide
207
+ motion.pendingY += motion.velocityY * glide
208
+ motion.velocityX *= decay
209
+ motion.velocityY *= decay
210
+ if (Math.hypot(motion.velocityX, motion.velocityY) < KEYBOARD_PAN_REST_FRACTION) {
211
+ motion.velocityX = 0
212
+ motion.velocityY = 0
213
+ }
214
+ }
215
+
216
+ /**
217
+ * Once per frame: syncs `motion` to `now` and leaves the travel since the
218
+ * last frame in `stepX`/`stepY`. Returns false once the
219
+ * view is at rest with nothing left to apply.
220
+ */
221
+ export function advanceKeyboardPanMotion(
222
+ motion: KeyboardPanMotion,
223
+ keys: KeyboardPanState,
224
+ now: number,
225
+ ): boolean {
226
+ syncKeyboardPanMotion(motion, keys, now)
227
+ motion.stepX = motion.pendingX
228
+ motion.stepY = motion.pendingY
229
+ motion.pendingX = 0
230
+ motion.pendingY = 0
231
+ return (
232
+ motion.holding ||
233
+ motion.velocityX !== 0 ||
234
+ motion.velocityY !== 0 ||
235
+ motion.stepX !== 0 ||
236
+ motion.stepY !== 0
237
+ )
238
+ }