@pascal-app/editor 1.0.3 → 1.0.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (624) hide show
  1. package/README.md +106 -0
  2. package/package.json +42 -38
  3. package/src/components/editor/__bench__/room-label-placement.bench.ts +108 -0
  4. package/src/components/editor/action-menu-button.tsx +83 -0
  5. package/src/components/editor/bake-thumbnail.tsx +307 -5
  6. package/src/components/editor/capture-level-picker.tsx +72 -0
  7. package/src/components/editor/construction-reveal-audio.test.tsx +150 -0
  8. package/src/components/editor/construction-reveal-audio.tsx +40 -0
  9. package/src/components/editor/custom-camera-controls.tsx +275 -57
  10. package/src/components/editor/delete-confirmation-dialog.tsx +297 -7
  11. package/src/components/editor/duplicate-node.ts +244 -0
  12. package/src/components/editor/editor-layout-mobile.tsx +37 -26
  13. package/src/components/editor/editor-layout-v2.tsx +132 -59
  14. package/src/components/editor/export-manager.tsx +47 -26
  15. package/src/components/editor/first-person/build-collider-world.test.ts +320 -0
  16. package/src/components/editor/first-person/build-collider-world.ts +28 -7
  17. package/src/components/editor/first-person/ground-holes.test.ts +128 -0
  18. package/src/components/editor/first-person/ground-holes.ts +130 -0
  19. package/src/components/editor/first-person/mechanism-targets.test.ts +162 -0
  20. package/src/components/editor/first-person/mechanism-targets.ts +88 -0
  21. package/src/components/editor/first-person-controls.tsx +220 -9
  22. package/src/components/editor/floating-action-menu.tsx +63 -207
  23. package/src/components/editor/floor-aware-camera-controls.test.ts +466 -0
  24. package/src/components/editor/floor-aware-camera-controls.ts +324 -0
  25. package/src/components/editor/floor-openings-overlay.tsx +243 -0
  26. package/src/components/editor/floor-region-controls.tsx +846 -0
  27. package/src/components/editor/floor-region-webgpu-materials.test.ts +12 -0
  28. package/src/components/editor/floor-surface.test.ts +165 -0
  29. package/src/components/editor/floor-surface.ts +182 -0
  30. package/src/components/editor/floorplan-background-selection.test.ts +11 -2
  31. package/src/components/editor/floorplan-background-selection.ts +1 -3
  32. package/src/components/editor/floorplan-mode-coordinator.tsx +2 -2
  33. package/src/components/editor/floorplan-panel.tsx +414 -363
  34. package/src/components/editor/follow-pascal.test.tsx +912 -0
  35. package/src/components/editor/follow-pascal.tsx +354 -0
  36. package/src/components/editor/footprint-height-handle.tsx +204 -0
  37. package/src/components/editor/group-actions.ts +56 -2
  38. package/src/components/editor/group-move-3d.ts +2 -0
  39. package/src/components/editor/group-rotate-handle.tsx +2 -0
  40. package/src/components/editor/group-transform-shared.test.ts +460 -0
  41. package/src/components/editor/handles/handle-hit-priority.test.tsx +63 -0
  42. package/src/components/editor/handles/handle-hit-priority.tsx +37 -0
  43. package/src/components/editor/handles/swallow-next-click.test.ts +28 -0
  44. package/src/components/editor/handles/use-handle-drag.ts +269 -197
  45. package/src/components/editor/index.tsx +409 -197
  46. package/src/components/editor/join-walls-pill.tsx +44 -0
  47. package/src/components/editor/node-action-menu.tsx +68 -70
  48. package/src/components/editor/node-arrow-handles.tsx +25 -6
  49. package/src/components/editor/open-wall-ends-3d-layer.tsx +273 -0
  50. package/src/components/editor/open-wall-ends-hint.tsx +89 -0
  51. package/src/components/editor/quick-measurement-hud.tsx +3 -2
  52. package/src/components/editor/registry-action-contributions.test.tsx +63 -0
  53. package/src/components/editor/registry-action-contributions.tsx +80 -1
  54. package/src/components/editor/riser-diagram-panel.tsx +14 -7
  55. package/src/components/editor/room-controls.tsx +847 -0
  56. package/src/components/editor/room-floor-highlight.tsx +128 -0
  57. package/src/components/editor/room-handles-2d.tsx +354 -0
  58. package/src/components/editor/room-handles.tsx +675 -0
  59. package/src/components/editor/room-highlight.tsx +233 -0
  60. package/src/components/editor/room-labels.test.ts +850 -0
  61. package/src/components/editor/room-labels.tsx +1890 -0
  62. package/src/components/editor/room-transform-ghost.tsx +256 -0
  63. package/src/components/editor/room-webgpu-materials.test.ts +27 -0
  64. package/src/components/editor/selection-manager.tsx +474 -419
  65. package/src/components/editor/slab-hole-highlights.tsx +5 -2
  66. package/src/components/editor/snapshot-capture-overlay.tsx +7 -2
  67. package/src/components/editor/snapshot-capture.test.ts +119 -60
  68. package/src/components/editor/snapshot-capture.ts +36 -5
  69. package/src/components/editor/thumbnail-generator.test.ts +654 -0
  70. package/src/components/editor/thumbnail-generator.tsx +1103 -223
  71. package/src/components/editor/use-floorplan-background-placement.ts +34 -12
  72. package/src/components/editor/vector-edge-extractor.tsx +121 -0
  73. package/src/components/editor/views/host-view.tsx +61 -0
  74. package/src/components/editor/views/plugins-view.tsx +663 -0
  75. package/src/components/editor/views/scene-region.tsx +59 -0
  76. package/src/components/editor/views/use-editor-views.ts +57 -0
  77. package/src/components/editor/views/view-bar.test.tsx +54 -0
  78. package/src/components/editor/views/view-bar.tsx +343 -0
  79. package/src/components/editor/wall-justification.test.ts +140 -0
  80. package/src/components/editor/wall-move-side-handles.tsx +43 -142
  81. package/src/components/editor/wall-push-arrow.tsx +92 -0
  82. package/src/components/editor/wall-region-handles.tsx +509 -0
  83. package/src/components/editor/wall-snap-beacon-layer.tsx +8 -4
  84. package/src/components/editor-2d/floorplan-cursor-indicator-overlay.tsx +8 -4
  85. package/src/components/editor-2d/floorplan-group-move.tsx +3 -0
  86. package/src/components/editor-2d/floorplan-open-wall-ends-layer.test.tsx +86 -0
  87. package/src/components/editor-2d/floorplan-open-wall-ends-layer.tsx +290 -0
  88. package/src/components/editor-2d/floorplan-registered-tool-layer.tsx +2 -2
  89. package/src/components/editor-2d/floorplan-registry-action-menu.tsx +11 -1
  90. package/src/components/editor-2d/floorplan-registry-move-overlay.tsx +116 -106
  91. package/src/components/editor-2d/renderers/floorplan-dimension-edit-overlay.test.ts +156 -0
  92. package/src/components/editor-2d/renderers/floorplan-dimension-edit-overlay.tsx +278 -0
  93. package/src/components/editor-2d/renderers/floorplan-dimension-renderer.tsx +77 -0
  94. package/src/components/editor-2d/renderers/{floorplan-draft-wall-measurement.tsx → floorplan-draft-measurement.tsx} +8 -8
  95. package/src/components/editor-2d/renderers/floorplan-geometry-renderer.tsx +4 -1
  96. package/src/components/editor-2d/renderers/floorplan-registry-layer.test.ts +38 -8
  97. package/src/components/editor-2d/renderers/floorplan-registry-layer.tsx +319 -233
  98. package/src/components/editor-2d/renderers/floorplan-reveal.test.tsx +254 -0
  99. package/src/components/editor-2d/renderers/floorplan-reveal.ts +141 -0
  100. package/src/components/editor-2d/svg-paths.ts +12 -15
  101. package/src/components/point-ask/bubble.tsx +538 -0
  102. package/src/components/point-ask/hover-chip.tsx +91 -0
  103. package/src/components/point-ask/mode-pill.tsx +92 -0
  104. package/src/components/point-ask/optional-motion.ts +22 -0
  105. package/src/components/point-ask/outline-corners.tsx +93 -0
  106. package/src/components/point-ask/pins.tsx +711 -0
  107. package/src/components/point-ask/point-ask-layer.tsx +83 -0
  108. package/src/components/point-ask/point-ask-styles.ts +105 -0
  109. package/src/components/point-ask/region-marquee-styles.ts +15 -0
  110. package/src/components/point-ask/region-marquee.tsx +64 -0
  111. package/src/components/procedural-items/inspector.tsx +40 -15
  112. package/src/components/systems/ceiling/ceiling-bracket-batch.test.ts +12 -2
  113. package/src/components/systems/ceiling/ceiling-selection-affordance-system.test.ts +97 -14
  114. package/src/components/systems/ceiling/ceiling-selection-affordance-system.tsx +18 -18
  115. package/src/components/systems/roof/roof-edit-system.tsx +1 -0
  116. package/src/components/systems/zone/zone-system.test.ts +47 -0
  117. package/src/components/systems/zone/zone-system.tsx +29 -68
  118. package/src/components/tools/distribution/connection-compatibility.test.ts +34 -0
  119. package/src/components/tools/distribution/connection-compatibility.ts +43 -0
  120. package/src/components/tools/distribution/distribution-public.ts +9 -0
  121. package/src/components/tools/distribution/distribution-run-contract.test.ts +26 -0
  122. package/src/components/tools/distribution/distribution-run-contract.ts +94 -0
  123. package/src/components/tools/distribution/distribution-run-tool.test.ts +241 -0
  124. package/src/components/tools/distribution/distribution-run-tool.test.tsx +125 -0
  125. package/src/components/tools/distribution/distribution-run-tool.tsx +1323 -0
  126. package/src/components/tools/distribution/draw-alignment.ts +35 -0
  127. package/src/components/tools/distribution/ports.test.ts +56 -0
  128. package/src/components/tools/distribution/ports.ts +378 -0
  129. package/src/components/tools/distribution/run-cursor.test.ts +135 -0
  130. package/src/components/tools/distribution/run-cursor.ts +36 -0
  131. package/src/components/tools/distribution/run-direction-feedback.test.ts +60 -0
  132. package/src/components/tools/distribution/run-direction-feedback.tsx +251 -0
  133. package/src/components/tools/distribution/run-port-snap.test.ts +79 -0
  134. package/src/components/tools/distribution/run-port-snap.ts +25 -0
  135. package/src/components/tools/distribution/run-screen-direction.test.ts +23 -0
  136. package/src/components/tools/distribution/run-screen-direction.ts +30 -0
  137. package/src/components/tools/elevator/elevator-tool.tsx +3 -6
  138. package/src/components/tools/elevator/move-elevator-tool.tsx +2 -3
  139. package/src/components/tools/item/item-surface-strategy-resolver.test.ts +67 -68
  140. package/src/components/tools/item/placement-history.test.tsx +512 -0
  141. package/src/components/tools/item/placement-strategies.test.ts +46 -1
  142. package/src/components/tools/item/placement-strategies.ts +11 -10
  143. package/src/components/tools/item/shelf-surface-strategy-resolver.test.ts +56 -56
  144. package/src/components/tools/item/use-draft-node.test.tsx +432 -1
  145. package/src/components/tools/item/use-draft-node.ts +166 -95
  146. package/src/components/tools/item/use-placement-coordinator.tsx +148 -95
  147. package/src/components/tools/paint-region/use-paint-tint.ts +37 -0
  148. package/src/components/tools/paint-region/wall-paint-region-tool.tsx +466 -0
  149. package/src/components/tools/registry/item-surface-move.test.ts +62 -63
  150. package/src/components/tools/registry/item-surface-move.ts +2 -0
  151. package/src/components/tools/registry/item-surface-resolver.test.ts +26 -26
  152. package/src/components/tools/registry/move-registry-node-tool.test.ts +268 -1
  153. package/src/components/tools/registry/move-registry-node-tool.tsx +66 -30
  154. package/src/components/tools/select/box-select-tool.tsx +2 -12
  155. package/src/components/tools/select/plane-box-select-tool.test.tsx +79 -0
  156. package/src/components/tools/select/plane-box-select-tool.tsx +35 -22
  157. package/src/components/tools/select/select-candidates.test.ts +37 -4
  158. package/src/components/tools/select/select-candidates.ts +15 -11
  159. package/src/components/tools/shared/cursor-sphere.tsx +36 -12
  160. package/src/components/tools/site/site-boundary-editor.tsx +1 -2
  161. package/src/components/tools/tool-manager.tsx +59 -24
  162. package/src/components/tools/wall/wall-drafting.test.ts +175 -47
  163. package/src/components/tools/zone/zone-tool.tsx +4 -12
  164. package/src/components/ui/action-menu/action-button.tsx +9 -1
  165. package/src/components/ui/action-menu/camera-actions.tsx +9 -4
  166. package/src/components/ui/action-menu/control-modes.tsx +15 -42
  167. package/src/components/ui/action-menu/follow-pascal-pill.tsx +32 -0
  168. package/src/components/ui/action-menu/follow-pascal-toggle.tsx +36 -0
  169. package/src/components/ui/action-menu/index.tsx +56 -33
  170. package/src/components/ui/action-menu/measurement-control.tsx +14 -19
  171. package/src/components/ui/action-menu/placement.ts +20 -0
  172. package/src/components/ui/action-menu/point-ask-button.tsx +52 -0
  173. package/src/components/ui/action-menu/view-toggles.tsx +47 -42
  174. package/src/components/ui/build-panel/build-panel.tsx +242 -0
  175. package/src/components/ui/command-palette/editor-commands.tsx +7 -42
  176. package/src/components/ui/command-palette/index.tsx +50 -39
  177. package/src/components/ui/command-palette/ranked-groups.test.ts +31 -0
  178. package/src/components/ui/controls/action-button.tsx +5 -2
  179. package/src/components/ui/controls/finish-swatch.tsx +110 -0
  180. package/src/components/ui/controls/material-paint-panel.tsx +97 -26
  181. package/src/components/ui/controls/material-picker.tsx +4 -1
  182. package/src/components/ui/controls/metric-control.tsx +1 -1
  183. package/src/components/ui/controls/scene-material-list.tsx +4 -32
  184. package/src/components/ui/controls/segmented-control.tsx +21 -8
  185. package/src/components/ui/controls/select-control.tsx +65 -0
  186. package/src/components/ui/controls/slider-control.tsx +1 -1
  187. package/src/components/ui/controls/toggle-control.tsx +3 -3
  188. package/src/components/ui/controls/tool-options-panel.tsx +1 -1
  189. package/src/components/ui/floating-level-selector.tsx +159 -59
  190. package/src/components/ui/helpers/building-helper.tsx +4 -1
  191. package/src/components/ui/helpers/contextual-helper-panel.tsx +181 -72
  192. package/src/components/ui/helpers/helper-manager.test.tsx +121 -0
  193. package/src/components/ui/helpers/helper-manager.tsx +331 -14
  194. package/src/components/ui/helpers/item-helper.tsx +17 -1
  195. package/src/components/ui/helpers/registered-tool-helper.tsx +8 -1
  196. package/src/components/ui/icon-ref.tsx +45 -1
  197. package/src/components/ui/level-duplicate-dialog.tsx +1 -1
  198. package/src/components/ui/node-icon-image.tsx +16 -0
  199. package/src/components/ui/panels/collections/collections-popover.tsx +28 -22
  200. package/src/components/ui/panels/floor-foundation-panel.tsx +551 -0
  201. package/src/components/ui/panels/floor-opening-panel.tsx +143 -0
  202. package/src/components/ui/panels/mobile-panel-sheet.tsx +4 -4
  203. package/src/components/ui/panels/mobile-selection-bar.tsx +8 -5
  204. package/src/components/ui/panels/multi-field-value.test.ts +26 -3
  205. package/src/components/ui/panels/multi-field-value.ts +6 -3
  206. package/src/components/ui/panels/multi-parametric-inspector.tsx +52 -2
  207. package/src/components/ui/panels/multi-selection-panel.tsx +161 -5
  208. package/src/components/ui/panels/node-display.ts +8 -3
  209. package/src/components/ui/panels/panel-manager.tsx +103 -68
  210. package/src/components/ui/panels/panel-wrapper.tsx +130 -62
  211. package/src/components/ui/panels/parametric-field-control.tsx +3 -3
  212. package/src/components/ui/panels/parametric-inspector.tsx +7 -7
  213. package/src/components/ui/panels/reference-panel.tsx +2 -3
  214. package/src/components/ui/panels/room-construction-rows.tsx +774 -0
  215. package/src/components/ui/panels/room-name-options.test.ts +55 -0
  216. package/src/components/ui/panels/room-name-options.ts +47 -0
  217. package/src/components/ui/panels/room-openings-field.tsx +110 -0
  218. package/src/components/ui/panels/room-panel.tsx +790 -0
  219. package/src/components/ui/panels/shape-choice.tsx +58 -0
  220. package/src/components/ui/panels/wall-region-list.tsx +109 -0
  221. package/src/components/ui/primitives/badge.tsx +8 -0
  222. package/src/components/ui/primitives/color-dot.tsx +36 -24
  223. package/src/components/ui/primitives/sidebar.tsx +3 -1
  224. package/src/components/ui/right-stack.test.ts +29 -0
  225. package/src/components/ui/right-stack.tsx +164 -0
  226. package/src/components/ui/sidebar/app-sidebar.tsx +1 -1
  227. package/src/components/ui/sidebar/icon-rail.tsx +8 -3
  228. package/src/components/ui/sidebar/panels/items-panel/function-tree-panel.tsx +1 -1
  229. package/src/components/ui/sidebar/panels/items-panel/index.tsx +6 -6
  230. package/src/components/ui/sidebar/panels/settings-panel/animation-speed-setting.tsx +40 -0
  231. package/src/components/ui/sidebar/panels/settings-panel/audio-settings-dialog.tsx +7 -8
  232. package/src/components/ui/sidebar/panels/settings-panel/construction-animation-setting.test.tsx +170 -0
  233. package/src/components/ui/sidebar/panels/settings-panel/construction-animation-setting.tsx +92 -0
  234. package/src/components/ui/sidebar/panels/settings-panel/index.tsx +138 -27
  235. package/src/components/ui/sidebar/panels/settings-panel/interface-theme.test.tsx +20 -0
  236. package/src/components/ui/sidebar/panels/settings-panel/keyboard-shortcuts-dialog.tsx +5 -5
  237. package/src/components/ui/sidebar/panels/settings-panel/print-export-button.tsx +4 -10
  238. package/src/components/ui/sidebar/panels/site-panel/building-tree-node.tsx +3 -11
  239. package/src/components/ui/sidebar/panels/site-panel/ceiling-tree-node.tsx +4 -4
  240. package/src/components/ui/sidebar/panels/site-panel/chimney-tree-node.tsx +1 -5
  241. package/src/components/ui/sidebar/panels/site-panel/collection-tree-node.tsx +197 -0
  242. package/src/components/ui/sidebar/panels/site-panel/column-tree-node.tsx +1 -5
  243. package/src/components/ui/sidebar/panels/site-panel/door-tree-node.tsx +1 -5
  244. package/src/components/ui/sidebar/panels/site-panel/dormer-tree-node.tsx +1 -5
  245. package/src/components/ui/sidebar/panels/site-panel/elevator-tree-node.tsx +1 -5
  246. package/src/components/ui/sidebar/panels/site-panel/fence-tree-node.tsx +24 -11
  247. package/src/components/ui/sidebar/panels/site-panel/gutter-tree-node.tsx +1 -5
  248. package/src/components/ui/sidebar/panels/site-panel/index.tsx +181 -223
  249. package/src/components/ui/sidebar/panels/site-panel/inline-rename-input.tsx +30 -8
  250. package/src/components/ui/sidebar/panels/site-panel/item-tree-node.tsx +1 -3
  251. package/src/components/ui/sidebar/panels/site-panel/level-tree-node.tsx +4 -1
  252. package/src/components/ui/sidebar/panels/site-panel/registry-tree-node.tsx +1 -3
  253. package/src/components/ui/sidebar/panels/site-panel/roof-tree-node.tsx +1 -4
  254. package/src/components/ui/sidebar/panels/site-panel/shelf-tree-node.tsx +1 -3
  255. package/src/components/ui/sidebar/panels/site-panel/slab-tree-node.tsx +21 -13
  256. package/src/components/ui/sidebar/panels/site-panel/solar-panel-tree-node.tsx +1 -5
  257. package/src/components/ui/sidebar/panels/site-panel/spawn-tree-node.tsx +1 -5
  258. package/src/components/ui/sidebar/panels/site-panel/stair-tree-node.tsx +1 -5
  259. package/src/components/ui/sidebar/panels/site-panel/tree-node.tsx +13 -37
  260. package/src/components/ui/sidebar/panels/site-panel/unit-tree-node.tsx +2 -3
  261. package/src/components/ui/sidebar/panels/site-panel/wall-tree-node.tsx +1 -5
  262. package/src/components/ui/sidebar/panels/site-panel/window-tree-node.tsx +1 -5
  263. package/src/components/ui/sidebar/panels/site-panel/zone-tree-node.tsx +9 -4
  264. package/src/components/ui/sidebar/panels/zone-panel/index.tsx +2 -4
  265. package/src/components/ui/sidebar/tab-bar.tsx +14 -4
  266. package/src/components/ui/sidebar/use-plugin-panels.tsx +15 -5
  267. package/src/components/viewer/floorplan-compass-button.tsx +9 -1
  268. package/src/components/viewer/floorplan-preview.tsx +5 -6
  269. package/src/components/viewer/viewer-controls-bar.tsx +18 -6
  270. package/src/components/viewer/viewer-hover-label.tsx +34 -0
  271. package/src/components/viewer/viewer-rooms.tsx +191 -0
  272. package/src/components/viewer/viewer-scene-header.tsx +181 -145
  273. package/src/components/viewer/viewer-selection-manager.tsx +124 -0
  274. package/src/components/viewer/viewer-stage.tsx +2 -2
  275. package/src/components/viewer-zone-system.tsx +3 -8
  276. package/src/hooks/use-auto-save.integration.test.tsx +215 -43
  277. package/src/hooks/use-auto-save.ts +60 -9
  278. package/src/hooks/use-ceiling-events.test.ts +123 -118
  279. package/src/hooks/use-grid-events.test.ts +105 -105
  280. package/src/hooks/use-keyboard.test.ts +243 -19
  281. package/src/hooks/use-keyboard.ts +118 -83
  282. package/src/hooks/use-open-wall-ends.test.ts +99 -0
  283. package/src/hooks/use-open-wall-ends.ts +238 -0
  284. package/src/hooks/use-placement-notice.test.ts +71 -0
  285. package/src/hooks/use-placement-notice.ts +73 -0
  286. package/src/hooks/use-selected-room.ts +143 -0
  287. package/src/index.tsx +312 -58
  288. package/src/lib/__fixtures__/owner-victor-seattle-parking.json +1 -0
  289. package/src/lib/__fixtures__/owner-villa-aurelia-basement.json +1 -0
  290. package/src/lib/add-storey.test.ts +177 -0
  291. package/src/lib/add-storey.ts +59 -0
  292. package/src/lib/below-grade.test.tsx +171 -0
  293. package/src/lib/below-grade.ts +133 -0
  294. package/src/lib/camera-facing.test.ts +24 -0
  295. package/src/lib/camera-facing.ts +24 -0
  296. package/src/lib/ceiling-edit-session.test.ts +321 -0
  297. package/src/lib/ceiling-edit-session.ts +206 -0
  298. package/src/lib/ceiling-region-session.test.ts +179 -0
  299. package/src/lib/continuation.test.ts +19 -1
  300. package/src/lib/continuation.ts +25 -4
  301. package/src/lib/direct-manipulation.ts +2 -0
  302. package/src/lib/door-interaction.ts +14 -0
  303. package/src/lib/download-project-file.ts +17 -0
  304. package/src/lib/editor-level-display.test.ts +96 -0
  305. package/src/lib/editor-level-display.ts +55 -0
  306. package/src/lib/editor-views.ts +178 -0
  307. package/src/lib/export-geometry-wait.test.ts +90 -0
  308. package/src/lib/export-texture-utils.ts +2 -4
  309. package/src/lib/floor-edit-session.ts +153 -0
  310. package/src/lib/floor-footprints.test.ts +563 -0
  311. package/src/lib/floor-footprints.ts +395 -0
  312. package/src/lib/floor-foundation-support.test.ts +284 -0
  313. package/src/lib/floor-opening-draft.ts +218 -0
  314. package/src/lib/floor-openings.test.ts +216 -0
  315. package/src/lib/floor-region-geometry.ts +191 -0
  316. package/src/lib/floor-region-session.test.ts +485 -0
  317. package/src/lib/floor-region-session.ts +589 -0
  318. package/src/lib/floor-region-snap.ts +101 -0
  319. package/src/lib/floor-step-hover.test.ts +32 -0
  320. package/src/lib/floorplan/annotation-visibility.ts +17 -0
  321. package/src/lib/floorplan/dimension-drive.test.ts +360 -0
  322. package/src/lib/floorplan/dimension-drive.ts +479 -0
  323. package/src/lib/floorplan/floorplan-export.test.ts +12 -8
  324. package/src/lib/floorplan/floorplan-export.tsx +32 -8
  325. package/src/lib/floorplan/floorplan-extension.ts +32 -0
  326. package/src/lib/floorplan/floorplan-mode.test.ts +4 -0
  327. package/src/lib/floorplan/floorplan-mode.ts +2 -0
  328. package/src/lib/floorplan/floorplan-pdfkit-document.ts +363 -27
  329. package/src/lib/floorplan/floorplan-pdfkit-renderer.test.ts +177 -8
  330. package/src/lib/floorplan/floorplan-pdfkit-renderer.ts +60 -25
  331. package/src/lib/floorplan/floorplan-plugin-install.test.tsx +138 -0
  332. package/src/lib/floorplan/floorplan-readonly.ts +6 -0
  333. package/src/lib/floorplan/geometry.ts +13 -6
  334. package/src/lib/floorplan/index.ts +0 -9
  335. package/src/lib/floorplan/items.ts +5 -2
  336. package/src/lib/floorplan/marks.test.ts +213 -0
  337. package/src/lib/floorplan/marks.ts +238 -0
  338. package/src/lib/floorplan/open-wall-ends.test.ts +268 -0
  339. package/src/lib/floorplan/open-wall-ends.ts +162 -0
  340. package/src/lib/floorplan/pdf-fonts/GeistMono-Bold.ttf +0 -0
  341. package/src/lib/floorplan/pdf-fonts/GeistMono-Regular.ttf +0 -0
  342. package/src/lib/floorplan/pdf-fonts/LICENSE-GeistMono.txt +92 -0
  343. package/src/lib/floorplan/pdf-fonts/LICENSE-LiberationSans.txt +102 -0
  344. package/src/lib/floorplan/pdf-fonts/LiberationSans-Bold.ttf +0 -0
  345. package/src/lib/floorplan/pdf-fonts/LiberationSans-Regular.ttf +0 -0
  346. package/src/lib/floorplan/pdfkit-standard-fonts.d.ts +5 -0
  347. package/src/lib/floorplan/schedules.test.ts +189 -0
  348. package/src/lib/floorplan/schedules.ts +339 -0
  349. package/src/lib/floorplan/sheet-export.test.ts +124 -0
  350. package/src/lib/floorplan/sheet-export.ts +318 -0
  351. package/src/lib/floorplan/site-plan/build-site-plan-drawing.ts +768 -0
  352. package/src/lib/floorplan/site-plan/contours.test.ts +43 -0
  353. package/src/lib/floorplan/site-plan/contributors.ts +97 -0
  354. package/src/lib/floorplan/site-plan/coverage.ts +230 -0
  355. package/src/lib/floorplan/site-plan/drawing-type-switch.test.ts +15 -0
  356. package/src/lib/floorplan/site-plan/drawing-type-switch.tsx +86 -0
  357. package/src/lib/floorplan/site-plan/front-edge.test.ts +103 -0
  358. package/src/lib/floorplan/site-plan/front-edge.ts +157 -0
  359. package/src/lib/floorplan/site-plan/geometry.test.ts +269 -0
  360. package/src/lib/floorplan/site-plan/geometry.ts +142 -0
  361. package/src/lib/floorplan/site-plan/index.ts +55 -0
  362. package/src/lib/floorplan/site-plan/site-annotations.test.ts +483 -0
  363. package/src/lib/floorplan/site-plan/site-annotations.ts +1084 -0
  364. package/src/lib/floorplan/site-plan/site-parts.ts +287 -0
  365. package/src/lib/floorplan/site-plan/site-plan-layer.tsx +138 -0
  366. package/src/lib/floorplan/site-plan/street-name.test.ts +69 -0
  367. package/src/lib/floorplan/site-plan/street-name.ts +302 -0
  368. package/src/lib/floorplan/site-plan/yards.test.ts +70 -0
  369. package/src/lib/floorplan/types.ts +1 -27
  370. package/src/lib/follow-pascal-exterior.test-fixture.ts +17 -0
  371. package/src/lib/follow-pascal-exterior.test.ts +171 -0
  372. package/src/lib/follow-pascal-exterior.ts +39 -0
  373. package/src/lib/follow-pascal-presentation.test.ts +203 -0
  374. package/src/lib/follow-pascal-presentation.ts +129 -0
  375. package/src/lib/follow-pascal-sight.ts +84 -0
  376. package/src/lib/follow-pascal-targets.test.ts +406 -0
  377. package/src/lib/follow-pascal-targets.ts +198 -0
  378. package/src/lib/follow-pascal.test.ts +350 -0
  379. package/src/lib/follow-pascal.ts +284 -0
  380. package/src/lib/fresh-planar-placement.test.ts +148 -0
  381. package/src/lib/fresh-planar-placement.ts +114 -37
  382. package/src/lib/geometry-script/author.ts +157 -0
  383. package/src/lib/geometry-script/client.ts +159 -0
  384. package/src/lib/geometry-script/finish-scripts.ts +127 -0
  385. package/src/lib/geometry-script/protocol.ts +13 -0
  386. package/src/lib/gesture-lifecycle.test.ts +595 -0
  387. package/src/lib/gesture-lifecycle.ts +290 -0
  388. package/src/lib/glb-export-zone-holes.test.ts +41 -0
  389. package/src/lib/glb-export.test.ts +636 -126
  390. package/src/lib/glb-export.ts +241 -120
  391. package/src/lib/history.test.ts +18 -7
  392. package/src/lib/history.ts +7 -0
  393. package/src/lib/hud-title.ts +109 -0
  394. package/src/lib/hydration-gated-storage.ts +49 -0
  395. package/src/lib/ifc-export.test.ts +174 -0
  396. package/src/lib/ifc-export.ts +201 -0
  397. package/src/lib/ifc-parts.ts +15 -0
  398. package/src/lib/inspector-expanded.test.ts +44 -0
  399. package/src/lib/inspector-expanded.ts +36 -0
  400. package/src/lib/interaction/scope.ts +22 -0
  401. package/src/lib/keyboard-pan.test.ts +129 -7
  402. package/src/lib/keyboard-pan.ts +161 -12
  403. package/src/lib/level-selection.test.ts +57 -0
  404. package/src/lib/level-selection.ts +16 -1
  405. package/src/lib/live-gesture-round-trip.test.ts +360 -0
  406. package/src/lib/lot/__fixtures__/map-dossier-miami-shores.json +1 -0
  407. package/src/lib/lot/__fixtures__/map-dossier-stpete.json +1 -0
  408. package/src/lib/lot/clean-ring.test.ts +187 -0
  409. package/src/lib/lot/clean-ring.ts +217 -0
  410. package/src/lib/lot/dossier.test.ts +202 -0
  411. package/src/lib/lot/dossier.ts +498 -0
  412. package/src/lib/lot/drop-in.test.ts +326 -0
  413. package/src/lib/lot/drop-in.ts +392 -0
  414. package/src/lib/lot/index.ts +43 -0
  415. package/src/lib/lot/lot-patch.test.ts +340 -0
  416. package/src/lib/lot/lot-patch.ts +394 -0
  417. package/src/lib/lot/parcel-provider.ts +43 -0
  418. package/src/lib/lot/terrain.test.ts +161 -0
  419. package/src/lib/lot/terrain.ts +266 -0
  420. package/src/lib/material-catalog-model.ts +3 -1
  421. package/src/lib/material-paint-panel-model.ts +9 -5
  422. package/src/lib/material-paint.ts +4 -0
  423. package/src/lib/measurement-kind.test.ts +6 -8
  424. package/src/lib/measurements.test.ts +23 -0
  425. package/src/lib/mezzanine-actions.test.ts +342 -0
  426. package/src/lib/mezzanine-draft.test.ts +501 -0
  427. package/src/lib/mezzanine-draft.ts +278 -0
  428. package/src/lib/mezzanine-messages.ts +26 -0
  429. package/src/lib/mezzanine-selection.test.ts +207 -0
  430. package/src/lib/mezzanine-stairs.ts +46 -0
  431. package/src/lib/model-export.ts +3 -1
  432. package/src/lib/node-action-movement.ts +17 -6
  433. package/src/lib/node-display-label.ts +37 -0
  434. package/src/lib/orbit-floor-clearance.test.ts +201 -0
  435. package/src/lib/orbit-floor-clearance.ts +154 -0
  436. package/src/lib/paint-commit-route.test.ts +37 -0
  437. package/src/lib/paint-commit-route.ts +17 -0
  438. package/src/lib/paint-eyedropper.test.tsx +165 -0
  439. package/src/lib/paint-eyedropper.ts +99 -0
  440. package/src/lib/paint-pass-through.test.ts +122 -0
  441. package/src/lib/paint-pass-through.ts +78 -0
  442. package/src/lib/paint-region-hover.ts +25 -0
  443. package/src/lib/paint-region-mode.ts +154 -0
  444. package/src/lib/paint-regions.test.ts +166 -0
  445. package/src/lib/paint-regions.ts +192 -0
  446. package/src/lib/paint-scope.test.ts +317 -35
  447. package/src/lib/paint-scope.ts +267 -25
  448. package/src/lib/panel-rows.ts +21 -0
  449. package/src/lib/parametric-node-update.ts +16 -7
  450. package/src/lib/plate-paint-affected.test.ts +300 -0
  451. package/src/lib/plate-paint-affected.ts +177 -0
  452. package/src/lib/plugin-chat.ts +65 -0
  453. package/src/lib/plugin-panels.test.ts +25 -0
  454. package/src/lib/plugin-panels.ts +94 -0
  455. package/src/lib/point-ask/__fixtures__/consumer-browser.tsx +76 -0
  456. package/src/lib/point-ask/__fixtures__/house.ts +81 -0
  457. package/src/lib/point-ask/ask-created.test.ts +195 -0
  458. package/src/lib/point-ask/ask-flow.test.ts +380 -0
  459. package/src/lib/point-ask/build-context.test.ts +173 -0
  460. package/src/lib/point-ask/build-context.ts +94 -0
  461. package/src/lib/point-ask/capture-crop.ts +129 -0
  462. package/src/lib/point-ask/choreography.ts +69 -0
  463. package/src/lib/point-ask/compare.test.ts +145 -0
  464. package/src/lib/point-ask/compare.ts +73 -0
  465. package/src/lib/point-ask/describe-target.test.ts +165 -0
  466. package/src/lib/point-ask/describe-target.ts +234 -0
  467. package/src/lib/point-ask/dictation.ts +79 -0
  468. package/src/lib/point-ask/edge-marker.test.ts +61 -0
  469. package/src/lib/point-ask/edge-marker.ts +27 -0
  470. package/src/lib/point-ask/frame-loop.ts +25 -0
  471. package/src/lib/point-ask/history-marks.test.ts +94 -0
  472. package/src/lib/point-ask/history-marks.ts +39 -0
  473. package/src/lib/point-ask/host-api.ts +32 -0
  474. package/src/lib/point-ask/outline-box.test.ts +30 -0
  475. package/src/lib/point-ask/outline-box.ts +11 -0
  476. package/src/lib/point-ask/overlay-metrics.ts +6 -0
  477. package/src/lib/point-ask/pick.test.ts +220 -0
  478. package/src/lib/point-ask/pick.ts +121 -0
  479. package/src/lib/point-ask/pin-machine.test.ts +494 -0
  480. package/src/lib/point-ask/pin-machine.ts +217 -0
  481. package/src/lib/point-ask/placement.test.ts +203 -0
  482. package/src/lib/point-ask/placement.ts +141 -0
  483. package/src/lib/point-ask/pointer-tracker.ts +34 -0
  484. package/src/lib/point-ask/projector.ts +83 -0
  485. package/src/lib/point-ask/region.test.ts +157 -0
  486. package/src/lib/point-ask/region.ts +136 -0
  487. package/src/lib/point-ask/retry.test.ts +217 -0
  488. package/src/lib/point-ask/retry.ts +37 -0
  489. package/src/lib/point-ask/scene-commit.test.ts +312 -0
  490. package/src/lib/point-ask/scene-commit.ts +32 -0
  491. package/src/lib/point-ask/session.ts +1391 -0
  492. package/src/lib/point-ask/spring-keyframes.test.ts +45 -0
  493. package/src/lib/point-ask/spring-keyframes.ts +38 -0
  494. package/src/lib/point-ask/spring.test.ts +63 -0
  495. package/src/lib/point-ask/spring.ts +107 -0
  496. package/src/lib/point-ask/suggestions.test.ts +80 -0
  497. package/src/lib/point-ask/suggestions.ts +125 -0
  498. package/src/lib/point-ask/target-scope.test.ts +622 -0
  499. package/src/lib/point-ask/target-scope.ts +338 -0
  500. package/src/lib/point-ask/types.ts +48 -0
  501. package/src/lib/portable-export.test.ts +34 -1
  502. package/src/lib/portable-export.ts +119 -53
  503. package/src/lib/print-feature-thickness.ts +10 -1
  504. package/src/lib/print-roof-solids.test.ts +39 -1
  505. package/src/lib/print-roof-solids.ts +43 -10
  506. package/src/lib/print-shell-compiler-manifold-core.ts +1 -1
  507. package/src/lib/print-shell-compiler.ts +1 -0
  508. package/src/lib/registered-draft-snap.ts +12 -0
  509. package/src/lib/roof-duplication.ts +10 -2
  510. package/src/lib/room-assembly-overlay.test.ts +275 -0
  511. package/src/lib/room-assembly-overlay.ts +265 -0
  512. package/src/lib/room-built-on.test.ts +379 -0
  513. package/src/lib/room-built-on.ts +138 -0
  514. package/src/lib/room-construction-commands.ts +712 -0
  515. package/src/lib/room-construction.test.ts +746 -0
  516. package/src/lib/room-construction.ts +385 -0
  517. package/src/lib/room-controls-phase6.test.ts +630 -0
  518. package/src/lib/room-divide-session.ts +523 -0
  519. package/src/lib/room-handle-drag.ts +1068 -0
  520. package/src/lib/room-name-catalog.test.ts +90 -0
  521. package/src/lib/room-name-catalog.ts +168 -0
  522. package/src/lib/room-push-dimensions.test.ts +311 -0
  523. package/src/lib/room-push-dimensions.ts +251 -0
  524. package/src/lib/room-selection-commands.ts +62 -0
  525. package/src/lib/room-selection.test.ts +1017 -0
  526. package/src/lib/room-selection.ts +417 -0
  527. package/src/lib/room-structure-commands.ts +94 -0
  528. package/src/lib/room-structure.test.ts +921 -0
  529. package/src/lib/room-transform-session.ts +570 -0
  530. package/src/lib/room-zone-routing.test.ts +294 -0
  531. package/src/lib/room-zone-routing.ts +197 -0
  532. package/src/lib/scene-clipboard.test.ts +610 -1
  533. package/src/lib/scene-clipboard.ts +269 -58
  534. package/src/lib/scene-material-refs.test.ts +79 -0
  535. package/src/lib/scene-material-refs.ts +79 -0
  536. package/src/lib/scene.test.ts +24 -36
  537. package/src/lib/scene.ts +33 -57
  538. package/src/lib/selection-routing.test.ts +129 -45
  539. package/src/lib/selection-routing.ts +54 -75
  540. package/src/lib/session-writes.test.ts +120 -0
  541. package/src/lib/session-writes.ts +205 -0
  542. package/src/lib/sfx-bus.ts +13 -0
  543. package/src/lib/sfx-player.test.ts +70 -9
  544. package/src/lib/sfx-player.ts +6 -0
  545. package/src/lib/sidebar-panel.ts +62 -0
  546. package/src/lib/site-boundary.test.ts +4 -6
  547. package/src/lib/site-boundary.ts +1 -1
  548. package/src/lib/snapping-mode.test.ts +4 -0
  549. package/src/lib/snapping-mode.ts +8 -1
  550. package/src/lib/stair-levels.test.ts +25 -0
  551. package/src/lib/stair-levels.ts +22 -3
  552. package/src/lib/terrace-draft.test.ts +145 -0
  553. package/src/lib/terrace-draft.ts +214 -0
  554. package/src/lib/tool-cursor-icon.test.ts +16 -0
  555. package/src/lib/tool-cursor-icon.ts +11 -0
  556. package/src/lib/typing-target.ts +9 -0
  557. package/src/lib/units.ts +77 -39
  558. package/src/lib/vector-edges.test.ts +58 -0
  559. package/src/lib/vector-edges.ts +400 -0
  560. package/src/lib/viewer-selection.test.ts +577 -0
  561. package/src/lib/viewer-selection.ts +580 -0
  562. package/src/lib/wall-draw-variant.test.ts +71 -0
  563. package/src/lib/wall-draw-variant.ts +91 -0
  564. package/src/lib/wall-paint-region-session.ts +246 -0
  565. package/src/lib/wall-paint-region.test.ts +373 -0
  566. package/src/lib/wall-placement-pointer.test.ts +42 -0
  567. package/src/lib/wall-placement-pointer.ts +57 -0
  568. package/src/lib/wall-polygon-draft.test.ts +262 -0
  569. package/src/lib/wall-polygon-draft.ts +286 -0
  570. package/src/lib/wall-push.test.ts +453 -0
  571. package/src/lib/wall-region-face.ts +352 -0
  572. package/src/lib/wall-region-handles.test.ts +304 -0
  573. package/src/lib/wall-region-handles.ts +368 -0
  574. package/src/lib/wall-region-list.test.ts +119 -0
  575. package/src/lib/wall-region-list.ts +136 -0
  576. package/src/lib/wall-region-snap.ts +110 -0
  577. package/src/lib/window-changes.ts +35 -0
  578. package/src/lib/window-interaction.ts +14 -0
  579. package/src/lib/zone-content.ts +4 -1
  580. package/src/lib/zone-name.test.ts +17 -0
  581. package/src/lib/zone-name.ts +17 -0
  582. package/src/store/opens-in-select-mode.test.ts +223 -0
  583. package/src/store/paint-mode.test.ts +188 -0
  584. package/src/store/terrain-sculpt-mode.test.ts +19 -21
  585. package/src/store/tool-mode.test.ts +137 -6
  586. package/src/store/use-audio.test.ts +146 -0
  587. package/src/store/use-audio.tsx +127 -3
  588. package/src/store/use-construction-reveal.test.ts +173 -0
  589. package/src/store/use-construction-reveal.ts +145 -0
  590. package/src/store/use-corner-snap-hint.test.ts +24 -0
  591. package/src/store/use-corner-snap-hint.ts +47 -0
  592. package/src/store/use-delete-confirmation.ts +28 -1
  593. package/src/store/use-drawing-view.test.ts +24 -3
  594. package/src/store/use-drawing-view.ts +29 -1
  595. package/src/store/use-editor.tsx +432 -321
  596. package/src/store/use-floorplan-draft-preview.ts +13 -0
  597. package/src/store/use-floorplan-marquee.ts +12 -2
  598. package/src/store/use-follow-pascal.test.ts +62 -0
  599. package/src/store/use-follow-pascal.ts +53 -0
  600. package/src/store/use-point-ask.ts +247 -0
  601. package/src/store/use-quick-measurement-hud.ts +3 -3
  602. package/src/store/use-stair-build-preview.ts +33 -5
  603. package/src/store/use-wall-move-ghosts.ts +4 -2
  604. package/src/store/use-wall-region-handles-selection.test.ts +222 -0
  605. package/src/store/use-wall-region-handles-selection.ts +139 -0
  606. package/src/store/view-layout.test.ts +201 -0
  607. package/src/store/view-layout.ts +199 -0
  608. package/src/test-utils/immediate-animation-frames.ts +16 -0
  609. package/src/test-utils/live-gesture-round-trip.ts +224 -0
  610. package/src/test-utils/render-current-markup.tsx +16 -0
  611. package/src/test-utils/selection-harness.ts +82 -0
  612. package/src/components/editor-2d/renderers/floorplan-stair-layer.tsx +0 -499
  613. package/src/components/systems/zone/zone-label-editor-system.tsx +0 -333
  614. package/src/components/tools/fence/fence-drafting.test.ts +0 -100
  615. package/src/components/tools/fence/fence-drafting.ts +0 -266
  616. package/src/components/tools/stair/stair-click-guard.test.ts +0 -58
  617. package/src/components/tools/stair/stair-click-guard.ts +0 -72
  618. package/src/components/tools/stair/stair-defaults.ts +0 -18
  619. package/src/components/tools/stair/stair-tool.tsx +0 -634
  620. package/src/components/ui/sidebar/panels/plugins-panel.tsx +0 -218
  621. package/src/components/viewer/viewer-units-panel.tsx +0 -116
  622. package/src/lib/floorplan/stairs.ts +0 -489
  623. package/src/lib/level-duplication.test.ts +0 -144
  624. package/src/lib/level-duplication.ts +0 -195
@@ -1,13 +1,18 @@
1
1
  import { afterAll, beforeAll, expect, test } from 'bun:test'
2
2
  import {
3
3
  acceptsKeyboardPan,
4
+ advanceKeyboardPanMotion,
4
5
  clearKeyboardPanKeys,
6
+ createKeyboardPanMotion,
5
7
  hasKeyboardPanInput,
6
8
  isKeyboardPanKey,
9
+ KEYBOARD_PAN_RAMP_SECONDS,
10
+ KEYBOARD_PAN_START_FRACTION,
11
+ KEYBOARD_PAN_STOP_SECONDS,
7
12
  type KeyboardPanState,
8
- keyboardPanDirection,
9
13
  keyboardPanSpeed,
10
14
  setKeyboardPanKey,
15
+ syncKeyboardPanMotion,
11
16
  } from './keyboard-pan'
12
17
 
13
18
  const idle = (): KeyboardPanState => ({
@@ -16,6 +21,51 @@ const idle = (): KeyboardPanState => ({
16
21
  left: false,
17
22
  right: false,
18
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
+
19
69
  const key = (init: Partial<KeyboardEvent>) => ({ target: null, ...init }) as KeyboardEvent
20
70
 
21
71
  // No DOM in this runner: stand in for the element classes the editable check reads.
@@ -39,9 +89,15 @@ test('physical WASD keys drive a screen-space direction; letters on other layout
39
89
  expect(setKeyboardPanKey(state, 'KeyW', true)).toBe(true)
40
90
  expect(setKeyboardPanKey(state, 'KeyW', true)).toBe(false)
41
91
  setKeyboardPanKey(state, 'KeyD', true)
42
- expect(keyboardPanDirection(state)).toEqual({ horizontal: 1, vertical: 1 })
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)
43
97
  setKeyboardPanKey(state, 'KeyA', true)
44
- expect(keyboardPanDirection(state)).toEqual({ horizontal: 0, vertical: 1 })
98
+ advanceKeyboardPanMotion(motion, state, 200)
99
+ expect(motion.stepX).toBe(0)
100
+ expect(motion.stepY).toBeGreaterThan(0)
45
101
  clearKeyboardPanKeys(state)
46
102
  expect(hasKeyboardPanInput(state)).toBe(false)
47
103
  })
@@ -55,8 +111,74 @@ test('modifier chords stay shortcuts, and typing in a field never pans', () => {
55
111
  expect(acceptsKeyboardPan(key({ target: new Input() }))).toBe(false)
56
112
  })
57
113
 
58
- test('speed scales with the visible width within fixed bounds', () => {
59
- expect(keyboardPanSpeed(0)).toBe(2)
60
- expect(keyboardPanSpeed(20)).toBeCloseTo(13)
61
- expect(keyboardPanSpeed(1000)).toBe(55)
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)
62
184
  })
@@ -9,9 +9,23 @@ export type KeyboardPanState = {
9
9
  right: boolean
10
10
  }
11
11
 
12
- const KEYBOARD_PAN_VIEW_WIDTH_PER_SECOND = 0.65
13
- const KEYBOARD_PAN_MIN_SPEED = 2
14
- const KEYBOARD_PAN_MAX_SPEED = 55
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
15
29
 
16
30
  export function isEditableKeyboardTarget(target: EventTarget | null) {
17
31
  return (
@@ -72,18 +86,153 @@ export function acceptsKeyboardPan(event: KeyboardEvent) {
72
86
  )
73
87
  }
74
88
 
75
- /** Screen-space direction: `horizontal` +1 is right, `vertical` +1 is forward (up). */
76
- export function keyboardPanDirection(state: KeyboardPanState) {
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 {
77
119
  return {
78
- horizontal: (state.right ? 1 : 0) - (state.left ? 1 : 0),
79
- vertical: (state.forward ? 1 : 0) - (state.backward ? 1 : 0),
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,
80
129
  }
81
130
  }
82
131
 
83
- /** World units per second for a view `viewWidth` wide. */
84
- export function keyboardPanSpeed(viewWidth: number) {
85
- return Math.min(
86
- Math.max(viewWidth * KEYBOARD_PAN_VIEW_WIDTH_PER_SECOND, KEYBOARD_PAN_MIN_SPEED),
87
- KEYBOARD_PAN_MAX_SPEED,
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
88
237
  )
89
238
  }
@@ -0,0 +1,57 @@
1
+ import { beforeEach, describe, expect, test } from 'bun:test'
2
+ import {
3
+ type AnyNode,
4
+ type AnyNodeId,
5
+ BuildingNode,
6
+ LevelNode,
7
+ RoofNode,
8
+ useScene,
9
+ WallNode,
10
+ } from '@pascal-app/core'
11
+ import { useViewer } from '@pascal-app/viewer'
12
+ import { selectPlacedNode } from './level-selection'
13
+
14
+ // A roof drawn over the ground floor's walls lands on the Roof level, which the editor hides while
15
+ // the ground floor is selected: selecting the roof there alone left it out of sight.
16
+ const BUILDING = 'building_select' as BuildingNode['id']
17
+ const GROUND = 'level_ground' as LevelNode['id']
18
+
19
+ beforeEach(() => {
20
+ const ground = LevelNode.parse({ id: GROUND, parentId: BUILDING, level: 0, children: ['wall_a'] })
21
+ const wall = WallNode.parse({ id: 'wall_a', parentId: GROUND, start: [0, 0], end: [4, 0] })
22
+ const roofLevel = LevelNode.parse({
23
+ id: 'level_roof',
24
+ parentId: BUILDING,
25
+ level: 1,
26
+ children: ['roof_main'],
27
+ metadata: { role: 'roof' },
28
+ })
29
+ const roof = RoofNode.parse({ id: 'roof_main', parentId: roofLevel.id })
30
+ const building = BuildingNode.parse({ id: BUILDING, children: [GROUND, roofLevel.id] })
31
+ useScene.setState({
32
+ nodes: Object.fromEntries(
33
+ [building, ground, wall, roofLevel, roof].map((node) => [node.id, node as AnyNode]),
34
+ ),
35
+ rootNodeIds: [BUILDING],
36
+ } as never)
37
+ useViewer.getState().setSelection({ buildingId: BUILDING, levelId: GROUND })
38
+ })
39
+
40
+ describe('selectPlacedNode', () => {
41
+ test('a node placed on another level is selected with its level', () => {
42
+ selectPlacedNode('roof_main' as AnyNodeId)
43
+ expect(useViewer.getState().selection).toMatchObject({
44
+ buildingId: BUILDING,
45
+ levelId: 'level_roof',
46
+ selectedIds: ['roof_main'],
47
+ })
48
+ })
49
+
50
+ test('a node placed on the level in view leaves the level as it is', () => {
51
+ selectPlacedNode('wall_a' as AnyNodeId)
52
+ expect(useViewer.getState().selection).toMatchObject({
53
+ levelId: GROUND,
54
+ selectedIds: ['wall_a'],
55
+ })
56
+ })
57
+ })
@@ -1,5 +1,5 @@
1
1
  import type { AnyNodeId, BuildingNode, LevelNode } from '@pascal-app/core'
2
- import { useScene } from '@pascal-app/core'
2
+ import { findLevelAncestorId, useScene } from '@pascal-app/core'
3
3
  import { useViewer } from '@pascal-app/viewer'
4
4
 
5
5
  function getAdjacentLevelIdForDeletion(levelId: AnyNodeId): LevelNode['id'] | null {
@@ -29,3 +29,18 @@ export function deleteLevelWithFallbackSelection(levelId: AnyNodeId) {
29
29
  useViewer.getState().setSelection({ levelId: nextLevelId })
30
30
  }
31
31
  }
32
+
33
+ /**
34
+ * Selects a node a tool just made, on its own level when that is not the one in view: a roof drawn
35
+ * over a storey's walls lands on the level above, which the editor hides while the storey is
36
+ * selected, so selecting it there alone would leave it out of sight.
37
+ */
38
+ export function selectPlacedNode(nodeId: AnyNodeId) {
39
+ const levelId = findLevelAncestorId(nodeId, useScene.getState().nodes)
40
+ const { selection, setSelection } = useViewer.getState()
41
+ setSelection(
42
+ levelId && levelId !== selection.levelId
43
+ ? { levelId: levelId as LevelNode['id'], selectedIds: [nodeId] }
44
+ : { selectedIds: [nodeId] },
45
+ )
46
+ }