@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
@@ -0,0 +1,47 @@
1
+ import { expect, test } from 'bun:test'
2
+ import { useScene, ZoneNode } from '@pascal-app/core'
3
+ import { useViewer } from '@pascal-app/viewer'
4
+ import { act, create } from '@react-three/test-renderer'
5
+ import { createElement } from 'react'
6
+ import useEditor from '../../../store/use-editor'
7
+ import { ZoneSystem } from './zone-system'
8
+
9
+ test('zone volumes mount only while a unit is painted, never for a selected zone', async () => {
10
+ const previousScene = useScene.getState()
11
+ const previousViewer = useViewer.getState()
12
+ const previousEditor = useEditor.getState()
13
+ const zone = ZoneNode.parse({
14
+ name: 'Courtyard',
15
+ polygon: [
16
+ [0, 0],
17
+ [4, 0],
18
+ [4, 4],
19
+ [0, 4],
20
+ ],
21
+ })
22
+ useScene.setState({ nodes: { [zone.id]: zone } })
23
+ useViewer.setState({
24
+ focusedUnitId: null,
25
+ showZones: false,
26
+ selection: { buildingId: null, levelId: null, zoneId: null, selectedIds: [zone.id] },
27
+ })
28
+ useEditor.setState({ isCaptureMode: false })
29
+ const renderer = await create(createElement(ZoneSystem))
30
+ try {
31
+ // A selected drawn zone looks like a room: outline, no tinted volume.
32
+ expect(useViewer.getState().showZones).toBe(false)
33
+ await act(async () => useViewer.setState({ focusedUnitId: 'unit_paint' as never }))
34
+ expect(useViewer.getState().showZones).toBe(true)
35
+ await act(async () => useEditor.getState().setCaptureMode(true))
36
+ expect(useViewer.getState().showZones).toBe(false)
37
+ await act(async () => useEditor.getState().setCaptureMode(false))
38
+ expect(useViewer.getState().showZones).toBe(true)
39
+ await act(async () => useViewer.setState({ focusedUnitId: null }))
40
+ expect(useViewer.getState().showZones).toBe(false)
41
+ } finally {
42
+ await renderer.unmount()
43
+ useScene.setState(previousScene)
44
+ useViewer.setState(previousViewer)
45
+ useEditor.setState(previousEditor)
46
+ }
47
+ })
@@ -9,40 +9,32 @@ import useEditor from '../../../store/use-editor'
9
9
  import useInteractionScope from '../../../store/use-interaction-scope'
10
10
 
11
11
  // Disable raycasting on zone geometry so clicks pass through to items underneath.
12
- // Zone selection in the editor is handled exclusively via the HTML label overlay.
13
12
  const noopRaycast = () => {}
14
13
 
14
+ /**
15
+ * Zone volumes are a working view for one job only: painting a unit's
16
+ * membership, where every zone on the level has to be in view. Otherwise every
17
+ * zone, drawn or detected, shows as a room: a light label pill (`RoomLabels3D`)
18
+ * and the room outline when selected. Mounting is the viewer's `showZones`
19
+ * flag: unmounting skips the meshes and drei labels entirely, and the cleanup
20
+ * restores the default so preview / first-person surfaces keep their labels.
21
+ */
15
22
  export const ZoneSystem = () => {
16
- // Outside the zones layer (or during snapshot capture) zones unmount
17
- // entirely — meshes AND drei <Html> labels, which cost per-frame matrix work
18
- // + live DOM even at opacity 0. The renderer reads this viewer flag; the
19
- // unmount cleanup restores the default so preview / first-person surfaces
20
- // (which swap this system for ViewerZoneSystem) keep their labels.
21
- const structureLayerState = useEditor((s) => s.structureLayer)
22
- const isCaptureModeState = useEditor((s) => s.isCaptureMode)
23
+ const focusedUnitId = useViewer((s) => s.focusedUnitId)
24
+ const isCaptureMode = useEditor((s) => s.isCaptureMode)
25
+ const active = !!focusedUnitId && !isCaptureMode
23
26
  useEffect(() => {
24
- useViewer.getState().setShowZones(structureLayerState === 'zones' && !isCaptureModeState)
27
+ useViewer.getState().setShowZones(active)
25
28
  return () => useViewer.getState().setShowZones(true)
26
- }, [structureLayerState, isCaptureModeState])
29
+ }, [active])
27
30
 
28
31
  useFrame((_, delta) => {
29
32
  if (!useViewer.getState().showZones) return
30
33
 
31
- const structureLayer = useEditor.getState().structureLayer
32
- const editorMode = useEditor.getState().mode
33
- const selectedLevelId = useViewer.getState().selection.levelId
34
- const selectedZoneId = useViewer.getState().selection.zoneId
35
- const hoveredId = useViewer.getState().hoveredId
36
- // Snapshot capture is a clean, camera-only surface — never show zone
37
- // geometry or the HTML zone tags in the framed shot.
38
- const isCaptureMode = useEditor.getState().isCaptureMode
39
-
40
- // During any active interaction zone labels step back entirely — they are
41
- // not a primary editing concern and would distract / invite misclicks.
34
+ const { levelId: selectedLevelId } = useViewer.getState().selection
35
+ // During any active interaction zone labels step back entirely.
42
36
  const zoneLabelsHidden =
43
37
  resolveOverlayPolicy(useInteractionScope.getState().scope).zoneLabels === 'hidden'
44
-
45
- const zoneGeometryVisible = structureLayer === 'zones'
46
38
  const zones = sceneRegistry.byType.zone || new Set()
47
39
  const nodes = useScene.getState().nodes
48
40
  const lerpSpeed = 10 * delta
@@ -52,63 +44,32 @@ export const ZoneSystem = () => {
52
44
  if (!obj) return
53
45
 
54
46
  const zone = nodes[zoneId as ZoneNode['id']] as ZoneNode | undefined
55
-
56
47
  const isOnSelectedLevel = zone?.parentId === selectedLevelId
57
- const isSelected = zoneId === selectedZoneId
58
- const isDeleteHovered = editorMode === 'delete' && hoveredId === zoneId
59
-
60
- // Keep group visible (so <Html> labels stay active), hide/show meshes only.
61
- // Show meshes when: in zone mode, selected, or delete-hovered. A zone the
62
- // author hid (sidebar eye) is the one case where the group itself goes:
63
- // otherwise this per-frame write would undo the renderer's `visible` prop.
48
+ // A zone the author hid (sidebar eye) takes its group with it; otherwise
49
+ // this per-frame write would undo the renderer's `visible` prop.
64
50
  const nodeVisible = zone?.visible !== false
65
51
  if (obj.visible !== nodeVisible) obj.visible = nodeVisible
66
- const meshVisible =
67
- nodeVisible && !isCaptureMode && (zoneGeometryVisible || isSelected || isDeleteHovered)
68
- const targetOpacity = isCaptureMode
69
- ? 0
70
- : isSelected || isDeleteHovered
71
- ? 1
72
- : zoneGeometryVisible
73
- ? 1
74
- : 0
75
-
76
- // Raycast is re-disabled per frame (not once per group): the meshes
77
- // remount whenever the zones layer toggles, so a one-shot flag on the
78
- // persistent group would leave fresh meshes clickable.
79
- const walls = (obj as Group).getObjectByName('walls') as Mesh | undefined
80
- if (walls) {
81
- walls.visible = meshVisible
82
- walls.raycast = noopRaycast
83
- const material = walls.material as MeshBasicNodeMaterial
84
- if (material?.userData?.uOpacity) {
85
- material.userData.uOpacity.value = MathUtils.lerp(
86
- material.userData.uOpacity.value,
87
- targetOpacity,
88
- lerpSpeed,
89
- )
90
- }
91
- }
52
+ const shown = nodeVisible && isOnSelectedLevel
92
53
 
93
- const floor = (obj as Group).getObjectByName('floor') as Mesh | undefined
94
- if (floor) {
95
- floor.visible = meshVisible
96
- floor.raycast = noopRaycast
97
- const material = floor.material as MeshBasicNodeMaterial
54
+ // Raycast is re-disabled per frame: the meshes remount whenever zones
55
+ // mount again, so a one-shot flag on the persistent group would leave
56
+ // fresh meshes clickable.
57
+ for (const name of ['walls', 'floor']) {
58
+ const mesh = (obj as Group).getObjectByName(name) as Mesh | undefined
59
+ if (!mesh) continue
60
+ mesh.visible = shown
61
+ mesh.raycast = noopRaycast
62
+ const material = mesh.material as MeshBasicNodeMaterial
98
63
  if (material?.userData?.uOpacity) {
99
64
  material.userData.uOpacity.value = MathUtils.lerp(
100
65
  material.userData.uOpacity.value,
101
- targetOpacity,
66
+ shown ? 1 : 0,
102
67
  lerpSpeed,
103
68
  )
104
69
  }
105
70
  }
106
71
 
107
- // Labels: visible on the current level (regardless of mode), but never
108
- // during snapshot capture.
109
- const showLabel =
110
- nodeVisible && !isCaptureMode && !zoneLabelsHidden && !!selectedLevelId && isOnSelectedLevel
111
- const labelOpacity = showLabel ? '1' : '0'
72
+ const labelOpacity = shown && !zoneLabelsHidden ? '1' : '0'
112
73
  const labelEl = document.getElementById(`${zoneId}-label`)
113
74
  if (labelEl && labelEl.style.opacity !== labelOpacity) {
114
75
  labelEl.style.opacity = labelOpacity
@@ -0,0 +1,34 @@
1
+ import { expect, test } from 'bun:test'
2
+ import { connectionCompatibility } from './connection-compatibility'
3
+
4
+ const supply = { system: 'supply', diameter: 6 }
5
+ test('matching profiles report a match', () => {
6
+ expect(connectionCompatibility(supply, { ...supply }).status).toBe('match')
7
+ })
8
+ test('system mismatch takes precedence over size', () => {
9
+ expect(connectionCompatibility(supply, { system: 'return', diameter: 8 }).status).toBe(
10
+ 'incompatible',
11
+ )
12
+ expect(
13
+ connectionCompatibility({ system: 'waste', diameter: 4 }, { system: 'vent', diameter: 4 })
14
+ .status,
15
+ ).toBe('incompatible')
16
+ })
17
+ test('size and shape mismatches describe the required adapter', () => {
18
+ expect(connectionCompatibility(supply, { ...supply, diameter: 8 }).label).toContain('Reducer')
19
+ expect(
20
+ connectionCompatibility(supply, { ...supply, shape: 'rect', width: 8, height: 6 }).label,
21
+ ).toContain('Transition')
22
+ expect(
23
+ connectionCompatibility(
24
+ { ...supply, shape: 'rect', width: 8, height: 6 },
25
+ { ...supply, shape: 'rect', width: 8, height: 8 },
26
+ ).status,
27
+ ).toBe('adapter')
28
+ })
29
+ test('missing system or section data cannot claim compatibility', () => {
30
+ expect(connectionCompatibility(supply, { diameter: 6 }).status).toBe('unknown')
31
+ expect(
32
+ connectionCompatibility({ ...supply, shape: 'rect' }, { ...supply, shape: 'rect' }).status,
33
+ ).toBe('unknown')
34
+ })
@@ -0,0 +1,43 @@
1
+ import type { NodePort } from '@pascal-app/core'
2
+
3
+ export type ConnectionProfile = Pick<NodePort, 'system' | 'diameter' | 'shape' | 'width' | 'height'>
4
+ export type ConnectionCompatibility = {
5
+ status: 'match' | 'adapter' | 'incompatible' | 'unknown'
6
+ label: string
7
+ }
8
+
9
+ export function connectionCompatibility(
10
+ source: ConnectionProfile,
11
+ target: ConnectionProfile,
12
+ ): ConnectionCompatibility {
13
+ if (source.system && target.system && source.system !== target.system) {
14
+ return {
15
+ status: 'incompatible',
16
+ label: `Different systems: ${source.system} / ${target.system}`,
17
+ }
18
+ }
19
+ const sourceShape = source.shape ?? 'round'
20
+ const targetShape = target.shape ?? 'round'
21
+ if (sourceShape !== targetShape) {
22
+ return { status: 'adapter', label: `Transition needed: ${sourceShape} / ${targetShape}` }
23
+ }
24
+ const sourceSize = sourceShape === 'round' ? [source.diameter] : [source.width, source.height]
25
+ const targetSize = targetShape === 'round' ? [target.diameter] : [target.width, target.height]
26
+ if (
27
+ [...sourceSize, ...targetSize].some(
28
+ (value) => value === undefined || !Number.isFinite(value) || value <= 0,
29
+ )
30
+ ) {
31
+ return { status: 'unknown', label: 'Connection size unavailable' }
32
+ }
33
+ if (sourceSize.some((value, index) => Math.abs(value! - targetSize[index]!) > 0.001)) {
34
+ return {
35
+ status: 'adapter',
36
+ label: `Reducer needed: ${sourceSize.join(' × ')}″ / ${targetSize.join(' × ')}″`,
37
+ }
38
+ }
39
+ if (!source.system || !target.system) {
40
+ return { status: 'unknown', label: 'Size matches; system unspecified' }
41
+ }
42
+ return { status: 'match', label: `Matching system and size: ${target.system}` }
43
+ }
@@ -0,0 +1,9 @@
1
+ export * from './connection-compatibility'
2
+ export * from './distribution-run-contract'
3
+ export * from './distribution-run-tool'
4
+ export * from './draw-alignment'
5
+ export * from './ports'
6
+ export * from './run-cursor'
7
+ export * from './run-direction-feedback'
8
+ export * from './run-port-snap'
9
+ export * from './run-screen-direction'
@@ -0,0 +1,26 @@
1
+ import { describe, expect, test } from 'bun:test'
2
+ import { offsetRunPointFromSurface, type RunSurfaceTarget } from './distribution-run-contract'
3
+
4
+ const wall = (
5
+ hostId: `wall_${string}` = 'wall_1',
6
+ side: 'front' | 'back' = 'front',
7
+ ): RunSurfaceTarget => ({
8
+ kind: 'wall',
9
+ levelId: 'level_1',
10
+ hostId,
11
+ side,
12
+ frame: {
13
+ origin: [2, 1, 0],
14
+ normal: [0, 0, 1],
15
+ tangent: [1, 0, 0],
16
+ bitangent: [0, 1, 0],
17
+ },
18
+ bounds: { minU: 0, maxU: 4, minV: 0, maxV: 2.5 },
19
+ })
20
+
21
+ describe('distribution run interaction contract', () => {
22
+ test('offsets wall geometry along the face normal', () => {
23
+ const target = wall()
24
+ expect(offsetRunPointFromSurface([3, 1, 0], target, 0.1)).toEqual([3, 1, 0.1])
25
+ })
26
+ })
@@ -0,0 +1,94 @@
1
+ import type { AnyNodeId } from '@pascal-app/core'
2
+
3
+ export type RunPoint = [number, number, number]
4
+
5
+ export type RunWallSide = 'front' | 'back'
6
+
7
+ export type RunSurfaceFrame = {
8
+ origin: RunPoint
9
+ normal: RunPoint
10
+ tangent: RunPoint
11
+ bitangent: RunPoint
12
+ }
13
+
14
+ export type RunSurfaceBounds = {
15
+ minU: number
16
+ maxU: number
17
+ minV: number
18
+ maxV: number
19
+ }
20
+
21
+ export type RunWallAttachment = {
22
+ wallId: Extract<AnyNodeId, `wall_${string}`>
23
+ side: RunWallSide
24
+ startUV: [number, number]
25
+ endUV: [number, number]
26
+ offset: number
27
+ }
28
+
29
+ /**
30
+ * The surface selected for the current run. A wall target is semantic: the
31
+ * host id and side are required so later snapping cannot fall back to any
32
+ * other object that happens to be close in the viewport.
33
+ */
34
+ export type RunSurfaceTarget =
35
+ | {
36
+ kind: 'floor' | 'ceiling' | 'surface'
37
+ hostId?: AnyNodeId
38
+ levelId: AnyNodeId
39
+ frame: RunSurfaceFrame
40
+ }
41
+ | {
42
+ kind: 'wall'
43
+ levelId: AnyNodeId
44
+ hostId: AnyNodeId
45
+ side: RunWallSide
46
+ frame: RunSurfaceFrame
47
+ bounds: RunSurfaceBounds
48
+ }
49
+
50
+ /** Move a run centerline clear of a wall face along the captured normal. */
51
+ export function offsetRunPointFromSurface(
52
+ point: RunPoint,
53
+ target: RunSurfaceTarget | null,
54
+ offset: number,
55
+ ): RunPoint {
56
+ if (!target || offset === 0) return [...point]
57
+ return [
58
+ point[0] + target.frame.normal[0] * offset,
59
+ point[1] + target.frame.normal[1] * offset,
60
+ point[2] + target.frame.normal[2] * offset,
61
+ ]
62
+ }
63
+
64
+ export function runPointToSurfaceUV(
65
+ point: RunPoint,
66
+ target: Extract<RunSurfaceTarget, { kind: 'wall' }>,
67
+ ): [number, number] {
68
+ const dx = point[0] - target.frame.origin[0]
69
+ const dy = point[1] - target.frame.origin[1]
70
+ const dz = point[2] - target.frame.origin[2]
71
+ return [
72
+ dx * target.frame.tangent[0] + dy * target.frame.tangent[1] + dz * target.frame.tangent[2],
73
+ dx * target.frame.bitangent[0] +
74
+ dy * target.frame.bitangent[1] +
75
+ dz * target.frame.bitangent[2],
76
+ ]
77
+ }
78
+
79
+ export function createRunWallAttachment(
80
+ wallId: Extract<AnyNodeId, `wall_${string}`>,
81
+ side: RunWallSide,
82
+ start: RunPoint,
83
+ end: RunPoint,
84
+ target: Extract<RunSurfaceTarget, { kind: 'wall' }>,
85
+ offset: number,
86
+ ): RunWallAttachment {
87
+ return {
88
+ wallId,
89
+ side,
90
+ startUV: runPointToSurfaceUV(start, target),
91
+ endUV: runPointToSurfaceUV(end, target),
92
+ offset,
93
+ }
94
+ }
@@ -0,0 +1,241 @@
1
+ import { describe, expect, test } from 'bun:test'
2
+ import { OrthographicCamera, PerspectiveCamera, Raycaster, Vector2, Vector3 } from 'three'
3
+ import {
4
+ createRunSurfaceFrame,
5
+ projectRunPointToSurface,
6
+ projectRunToAngleLock,
7
+ projectRunToCameraDirection,
8
+ projectRunToDirection,
9
+ projectRunToSurfaceAngleLock,
10
+ type RunCursorRay,
11
+ type RunPoint,
12
+ runDistanceSquared,
13
+ snapRunLength,
14
+ snapRunPointToSurface,
15
+ snapRunValue,
16
+ stepNominalRunSize,
17
+ } from './distribution-run-tool'
18
+
19
+ describe('distribution run drafting helpers', () => {
20
+ test('grid length preserves a diagonal from an off-grid socket', () => {
21
+ const start: RunPoint = [0.13, 0.27, 0.19]
22
+ const angled = projectRunToAngleLock(start, [1.4, 0.27, 1.3])
23
+ const point = snapRunLength(start, angled, 0.25)
24
+ expect(point[0] - start[0]).toBeCloseTo(point[2] - start[2])
25
+ expect(Math.sqrt(runDistanceSquared(start, point))).toBeCloseTo(1.75)
26
+ expect(point[1]).toBe(start[1])
27
+ expect(snapRunLength(start, angled, 0)).toEqual(angled)
28
+ expect(snapRunLength(start, start, 0.25)).toEqual(start)
29
+ })
30
+
31
+ test('grid length preserves wall-plane diagonals', () => {
32
+ const start: RunPoint = [0.13, 1.17, 4]
33
+ const wall = createRunSurfaceFrame(start, [0, 0, 1])
34
+ const angled = projectRunToSurfaceAngleLock(start, [1.4, 2.3, 4], wall)
35
+ const point = snapRunLength(start, angled, 0.25)
36
+ expect(point[0] - start[0]).toBeCloseTo(point[1] - start[1])
37
+ expect(point[2]).toBe(4)
38
+ expect(Math.sqrt(runDistanceSquared(start, point))).toBeCloseTo(1.75)
39
+ })
40
+
41
+ test('camera grid snapping steps along a vertical guide from an off-grid origin', () => {
42
+ const result = projectRunToCameraDirection(
43
+ [0.13, 0.17, 0.19],
44
+ { origin: [3.13, 2.3, 3.19], direction: [-Math.SQRT1_2, 0, -Math.SQRT1_2] },
45
+ [1, 0, 0],
46
+ 0.05,
47
+ 0.25,
48
+ [[0, 1, 0]],
49
+ )
50
+ expect(result?.point[0]).toBe(0.13)
51
+ expect(result?.point[1]).toBeCloseTo(2.42)
52
+ expect(result?.point[2]).toBe(0.19)
53
+ })
54
+ for (const camera of [
55
+ new OrthographicCamera(-8, 8, 6, -6, 0.1, 100),
56
+ new PerspectiveCamera(50, 4 / 3, 0.1, 100),
57
+ ]) {
58
+ test(`screen-space diagonal picking follows the ${camera.type} view`, () => {
59
+ camera.position.set(5, 8, 10)
60
+ camera.lookAt(0, 0, 0)
61
+ camera.updateMatrixWorld(true)
62
+ const target = new Vector3(2, 0, 2)
63
+ const ndc = target.clone().project(camera)
64
+ const ray = new Raycaster()
65
+ ray.setFromCamera(new Vector2(ndc.x, ndc.y), camera)
66
+ const project = (point: RunPoint): [number, number] => {
67
+ const p = new Vector3(...point).project(camera)
68
+ return [(p.x + 1) * 400, (1 - p.y) * 300]
69
+ }
70
+ const result = projectRunToCameraDirection(
71
+ [0, 0, 0],
72
+ {
73
+ origin: ray.ray.origin.toArray(),
74
+ direction: ray.ray.direction.toArray(),
75
+ },
76
+ [1, 0, 0],
77
+ 0.05,
78
+ 0,
79
+ [
80
+ [1, 0, 0],
81
+ [0, 0, 1],
82
+ [Math.SQRT1_2, 0, Math.SQRT1_2],
83
+ ],
84
+ undefined,
85
+ {
86
+ project,
87
+ pointer: project(target.toArray()),
88
+ previous: [1, 0, 0],
89
+ },
90
+ )
91
+ expect(result?.direction).toEqual([Math.SQRT1_2, 0, Math.SQRT1_2])
92
+ expect(result?.point[0]).toBeCloseTo(2)
93
+ expect(result?.point[2]).toBeCloseTo(2)
94
+ })
95
+ }
96
+ test('releases an airborne direction guide when the cursor returns to a wall', () => {
97
+ const start: RunPoint = [0, 1, 1]
98
+ const ray: RunCursorRay = {
99
+ origin: [3, 3, 5],
100
+ direction: [-1, -1, -2],
101
+ }
102
+ const directions: RunPoint[] = [[1, 0, 0]]
103
+ expect(
104
+ projectRunToCameraDirection(start, ray, [1, 0, 0], 0.05, 0, directions)?.point[0],
105
+ ).toBeCloseTo(1)
106
+
107
+ for (const clearance of [0.0254, 0.1016]) {
108
+ const wall = createRunSurfaceFrame([0, 0, 2 + clearance], [0, 0, 1])
109
+ expect(
110
+ projectRunToCameraDirection(start, ray, [1, 0, 0], 0.05, 0, directions, wall),
111
+ ).toBeNull()
112
+ }
113
+ })
114
+
115
+ test('keeps direction guides that lie on the active wall face', () => {
116
+ const wall = createRunSurfaceFrame([0, 0, 1], [0, 0, 1])
117
+ const result = projectRunToCameraDirection(
118
+ [0, 1, 1],
119
+ { origin: [3, 3, 5], direction: [-1, -1, -2] },
120
+ [1, 0, 0],
121
+ 0.05,
122
+ 0,
123
+ [[1, 0, 0]],
124
+ wall,
125
+ )
126
+ expect(result?.point[0]).toBeCloseTo(1)
127
+ expect(result?.point[2]).toBe(1)
128
+ })
129
+
130
+ test('camera hover resolves downward without a ground-plane height', () => {
131
+ const projected = projectRunToCameraDirection(
132
+ [0, 5, 0],
133
+ { origin: [3, 2, 3], direction: [-Math.SQRT1_2, 0, -Math.SQRT1_2] },
134
+ [1, 0, 0],
135
+ 0.05,
136
+ 0,
137
+ )
138
+ expect(projected?.direction).toEqual([0, -1, 0])
139
+ expect(projected?.point[1]).toBeCloseTo(2)
140
+ })
141
+
142
+ test('projects the cursor onto the direction selected by an arrow handle', () => {
143
+ const point = projectRunToDirection([1, 2, 3], [4, 9, 1], [Math.SQRT1_2, 0, Math.SQRT1_2])
144
+
145
+ expect(point[1]).toBe(2)
146
+ expect(point[0] - 1).toBeCloseTo(point[2] - 3)
147
+ expect(point[0]).toBeGreaterThan(1)
148
+ })
149
+
150
+ test('snaps values only when the step is active', () => {
151
+ expect(snapRunValue(1.13, 0.25)).toBe(1.25)
152
+ expect(snapRunValue(1.13, 0)).toBe(1.13)
153
+ })
154
+
155
+ test('projects a cursor onto the nearest 45 degree ray', () => {
156
+ const point = projectRunToAngleLock([2, 3, 4], [4, 9, 5.8])
157
+
158
+ expect(point[1]).toBe(3)
159
+ expect(point[0] - 2).toBeCloseTo(point[2] - 4)
160
+ })
161
+
162
+ test('projects a connected continuation onto directions relative to its source run', () => {
163
+ const source = [Math.SQRT1_2, 0, Math.SQRT1_2] as const
164
+ const projected = projectRunToAngleLock([0, 0, 0], [1, 0, -2], source)
165
+
166
+ expect(projected[0]).toBeCloseTo(1.5)
167
+ expect(projected[1]).toBe(0)
168
+ expect(projected[2]).toBeCloseTo(-1.5)
169
+ })
170
+
171
+ test('selects a true vertical direction from the camera cursor ray', () => {
172
+ const projected = projectRunToCameraDirection(
173
+ [0, 0, 0],
174
+ {
175
+ origin: [3, 3, 3],
176
+ direction: [-Math.SQRT1_2, 0, -Math.SQRT1_2],
177
+ },
178
+ [1, 0, 0],
179
+ 0.05,
180
+ 0,
181
+ )
182
+
183
+ expect(projected?.direction[0]).toBeCloseTo(0)
184
+ expect(projected?.direction[1]).toBeCloseTo(1)
185
+ expect(projected?.direction[2]).toBeCloseTo(0)
186
+ expect(projected?.point[1]).toBeCloseTo(3)
187
+ })
188
+
189
+ test('selects a rising 45 degree direction in the source plane', () => {
190
+ const target = [Math.SQRT1_2 * 4, Math.SQRT1_2 * 4, 0] as const
191
+ const projected = projectRunToCameraDirection(
192
+ [0, 0, 0],
193
+ {
194
+ origin: [target[0] + 3, target[1], 3],
195
+ direction: [-Math.SQRT1_2, 0, -Math.SQRT1_2],
196
+ },
197
+ [1, 0, 0],
198
+ 0.05,
199
+ 0,
200
+ )
201
+
202
+ expect(projected?.direction[0]).toBeCloseTo(Math.SQRT1_2)
203
+ expect(projected?.direction[1]).toBeCloseTo(Math.SQRT1_2)
204
+ expect(projected?.direction[2]).toBeCloseTo(0)
205
+ })
206
+
207
+ test('steps from off-catalogue sizes using the nearest nominal size', () => {
208
+ const sizes = [2, 3, 4, 6]
209
+
210
+ expect(stepNominalRunSize(sizes, 3.2, 1)).toBe(4)
211
+ expect(stepNominalRunSize(sizes, 3.2, -1)).toBe(2)
212
+ expect(stepNominalRunSize(sizes, 6, 1)).toBe(6)
213
+ })
214
+
215
+ test('computes squared 3D distance for fitting degeneracy checks', () => {
216
+ expect(runDistanceSquared([1, 2, 3], [4, 6, 3])).toBe(25)
217
+ })
218
+
219
+ test('projects and snaps a run in a vertical wall plane', () => {
220
+ const wall = createRunSurfaceFrame([4, 2, 6], [0, 0, 1])
221
+
222
+ expect(projectRunPointToSurface([7, 3, 9], wall)).toEqual([7, 3, 6])
223
+ expect(snapRunPointToSurface([7.12, 2.88, 6], wall, 0.25)).toEqual([7, 3, 6])
224
+ })
225
+
226
+ test('keeps angle snapping inside the active surface plane', () => {
227
+ const wall = createRunSurfaceFrame([0, 1, 4], [0, 0, 1])
228
+ const point = projectRunToSurfaceAngleLock([0, 1, 4], [1.2, 2.1, 4], wall)
229
+
230
+ expect(point[2]).toBeCloseTo(4)
231
+ expect(point[1] - 1).toBeCloseTo(point[0])
232
+ })
233
+
234
+ test('keeps a run on a rotated wall plane', () => {
235
+ const diagonalWall = createRunSurfaceFrame([2, 1, 2], [Math.SQRT1_2, 0, Math.SQRT1_2])
236
+ const projected = projectRunPointToSurface([4, 3, 0], diagonalWall)
237
+
238
+ expect(projected[0] + projected[2]).toBeCloseTo(4)
239
+ expect(projected[1]).toBeCloseTo(3)
240
+ })
241
+ })