@pascal-app/editor 0.9.1 → 1.0.0-beta.1

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 (366) hide show
  1. package/package.json +14 -9
  2. package/src/components/editor/alignment-3d-guide-layer.tsx +10 -2
  3. package/src/components/editor/bake-exporter.tsx +49 -0
  4. package/src/components/editor/bake-thumbnail.tsx +95 -0
  5. package/src/components/editor/camera-dragging-lifecycle.test.ts +49 -0
  6. package/src/components/editor/camera-dragging-lifecycle.ts +41 -0
  7. package/src/components/editor/custom-camera-controls.tsx +332 -199
  8. package/src/components/editor/delete-confirmation-dialog.tsx +54 -0
  9. package/src/components/editor/editor-layout-mobile.tsx +5 -0
  10. package/src/components/editor/editor-layout-v2.tsx +18 -0
  11. package/src/components/editor/elevation-3d-guide-layer.tsx +120 -0
  12. package/src/components/editor/export-manager.tsx +71 -63
  13. package/src/components/editor/fence-tangent-lines-3d.tsx +87 -0
  14. package/src/components/editor/first-person/build-collider-world.test.ts +102 -5
  15. package/src/components/editor/first-person/build-collider-world.ts +52 -5
  16. package/src/components/editor/first-person/terrain-collider.test.ts +132 -0
  17. package/src/components/editor/first-person/terrain-collider.ts +212 -0
  18. package/src/components/editor/first-person-controls.tsx +410 -136
  19. package/src/components/editor/floating-action-menu.tsx +435 -64
  20. package/src/components/editor/floating-building-action-menu.tsx +1 -1
  21. package/src/components/editor/floorplan-background-selection.test.ts +72 -0
  22. package/src/components/editor/floorplan-background-selection.ts +7 -20
  23. package/src/components/editor/floorplan-camera-sync.test.ts +249 -0
  24. package/src/components/editor/floorplan-camera-sync.ts +213 -0
  25. package/src/components/editor/floorplan-mode-coordinator.tsx +82 -0
  26. package/src/components/editor/floorplan-navigation-presentation.test.ts +142 -0
  27. package/src/components/editor/floorplan-navigation-presentation.ts +156 -0
  28. package/src/components/editor/floorplan-panel.tsx +2517 -1492
  29. package/src/components/editor/grid.tsx +166 -39
  30. package/src/components/editor/group-actions.ts +597 -0
  31. package/src/components/editor/group-floating-action-menu.tsx +130 -0
  32. package/src/components/editor/group-move-3d.ts +397 -0
  33. package/src/components/editor/group-rotate-handle.tsx +102 -64
  34. package/src/components/editor/group-selection-box-3d.tsx +173 -0
  35. package/src/components/editor/group-transform-shared.test.ts +192 -1
  36. package/src/components/editor/group-transform-shared.ts +240 -7
  37. package/src/components/editor/handles/handle-arrow.tsx +81 -23
  38. package/src/components/editor/handles/handle-drag-history.test.ts +96 -0
  39. package/src/components/editor/handles/handle-drag-history.ts +14 -0
  40. package/src/components/editor/handles/preview-overrides.test.ts +26 -0
  41. package/src/components/editor/handles/preview-overrides.ts +13 -0
  42. package/src/components/editor/handles/resize-snap.test.ts +52 -0
  43. package/src/components/editor/handles/resize-snap.ts +23 -0
  44. package/src/components/editor/handles/use-handle-drag.ts +49 -6
  45. package/src/components/editor/index.tsx +289 -35
  46. package/src/components/editor/measurement-pill.tsx +68 -23
  47. package/src/components/editor/node-action-menu.tsx +14 -1
  48. package/src/components/editor/node-arrow-handles.tsx +409 -180
  49. package/src/components/editor/opening-guides-3d-layer.tsx +144 -0
  50. package/src/components/editor/quick-measurement-card.tsx +76 -0
  51. package/src/components/editor/quick-measurement-hud.tsx +34 -0
  52. package/src/components/editor/riser-diagram-panel.tsx +137 -0
  53. package/src/components/editor/selection-manager.tsx +814 -466
  54. package/src/components/editor/site-edge-labels.tsx +28 -6
  55. package/src/components/editor/slab-hole-highlights.tsx +13 -5
  56. package/src/components/editor/snapshot-capture-overlay.tsx +258 -115
  57. package/src/components/editor/three-context-bridge.ts +22 -0
  58. package/src/components/editor/thumbnail-generator.tsx +118 -253
  59. package/src/components/editor/use-floorplan-background-placement.ts +113 -45
  60. package/src/components/editor/use-floorplan-hit-testing.test.ts +82 -0
  61. package/src/components/editor/use-floorplan-hit-testing.ts +144 -161
  62. package/src/components/editor/use-floorplan-scene-data.ts +5 -5
  63. package/src/components/editor/use-mesh-settle-epoch.ts +27 -0
  64. package/src/components/editor/wall-measurement-label.tsx +10 -28
  65. package/src/components/editor/wall-move-side-handles.tsx +229 -39
  66. package/src/components/editor/wall-opening-highlights.tsx +63 -32
  67. package/src/components/editor/wall-snap-beacon-layer.tsx +162 -6
  68. package/src/components/editor-2d/floorplan-action-menu-layer.tsx +9 -5
  69. package/src/components/editor-2d/floorplan-alignment-guide-layer.tsx +2 -1
  70. package/src/components/editor-2d/floorplan-cursor-indicator-overlay.tsx +40 -9
  71. package/src/components/editor-2d/floorplan-cursor-indicator-position.test.ts +31 -0
  72. package/src/components/editor-2d/floorplan-cursor-indicator-position.ts +39 -0
  73. package/src/components/editor-2d/floorplan-group-action-menu.tsx +87 -0
  74. package/src/components/editor-2d/floorplan-group-move.tsx +744 -0
  75. package/src/components/editor-2d/floorplan-measurement-tool-layer.test.ts +113 -0
  76. package/src/components/editor-2d/floorplan-measurement-tool-layer.tsx +1672 -0
  77. package/src/components/editor-2d/floorplan-quick-measure-layer.tsx +212 -0
  78. package/src/components/editor-2d/floorplan-registered-tool-layer.tsx +72 -0
  79. package/src/components/editor-2d/floorplan-registry-action-menu.tsx +219 -20
  80. package/src/components/editor-2d/floorplan-registry-move-overlay.tsx +327 -59
  81. package/src/components/editor-2d/floorplan-render-context.test.ts +27 -0
  82. package/src/components/editor-2d/floorplan-render-context.tsx +113 -8
  83. package/src/components/editor-2d/floorplan-snap-beacon-layer.tsx +5 -3
  84. package/src/components/editor-2d/renderers/floorplan-annotation-layout.test.ts +285 -0
  85. package/src/components/editor-2d/renderers/floorplan-annotation-layout.ts +516 -0
  86. package/src/components/editor-2d/renderers/floorplan-dimension-renderer.test.tsx +236 -0
  87. package/src/components/editor-2d/renderers/floorplan-dimension-renderer.tsx +618 -0
  88. package/src/components/editor-2d/renderers/floorplan-geometry-renderer.test.tsx +369 -0
  89. package/src/components/editor-2d/renderers/floorplan-geometry-renderer.tsx +431 -32
  90. package/src/components/editor-2d/renderers/floorplan-label-angle.test.ts +89 -0
  91. package/src/components/editor-2d/renderers/floorplan-label-angle.ts +106 -0
  92. package/src/components/editor-2d/renderers/floorplan-placement-preview-layer.tsx +94 -26
  93. package/src/components/editor-2d/renderers/floorplan-registry-layer.test.ts +587 -0
  94. package/src/components/editor-2d/renderers/floorplan-registry-layer.tsx +2280 -578
  95. package/src/components/editor-2d/renderers/floorplan-stair-layer.tsx +20 -10
  96. package/src/components/editor-2d/renderers/floorplan-voronoi-layer.tsx +128 -0
  97. package/src/components/systems/ceiling/ceiling-selection-affordance-system.tsx +425 -61
  98. package/src/components/systems/ceiling/ceiling-system.tsx +97 -4
  99. package/src/components/systems/roof/roof-edit-system.tsx +1768 -13
  100. package/src/components/systems/selection-affordance-manager.tsx +38 -0
  101. package/src/components/systems/zone/zone-label-editor-system.tsx +14 -1
  102. package/src/components/systems/zone/zone-system.tsx +42 -13
  103. package/src/components/tools/elevator/elevator-tool.tsx +34 -8
  104. package/src/components/tools/elevator/move-elevator-tool.tsx +13 -2
  105. package/src/components/tools/fence/fence-drafting.ts +107 -8
  106. package/src/components/tools/item/move-tool.tsx +17 -13
  107. package/src/components/tools/item/placement-math.test.ts +13 -1
  108. package/src/components/tools/item/placement-math.ts +28 -2
  109. package/src/components/tools/item/placement-strategies.ts +246 -5
  110. package/src/components/tools/item/placement-types.ts +13 -1
  111. package/src/components/tools/item/use-draft-node.ts +146 -57
  112. package/src/components/tools/item/use-placement-coordinator.tsx +813 -125
  113. package/src/components/tools/registry/move-registry-node-tool.tsx +636 -103
  114. package/src/components/tools/roof/roof-tool.tsx +336 -94
  115. package/src/components/tools/select/box-select-state.ts +18 -7
  116. package/src/components/tools/select/box-select-tool.tsx +176 -40
  117. package/src/components/tools/select/marquee-geometry.test.ts +161 -0
  118. package/src/components/tools/select/marquee-geometry.ts +155 -0
  119. package/src/components/tools/select/plane-box-select-tool.tsx +1 -8
  120. package/src/components/tools/select/select-candidates.ts +1 -1
  121. package/src/components/tools/shared/cursor-sphere.tsx +62 -26
  122. package/src/components/tools/shared/drag-bounding-box.tsx +28 -4
  123. package/src/components/tools/shared/facing-indicator.tsx +87 -0
  124. package/src/components/tools/shared/facing-pose-indicator.tsx +54 -0
  125. package/src/components/tools/shared/floor-stack-preview.ts +4 -0
  126. package/src/components/tools/shared/horizontal-construction-plane.ts +123 -0
  127. package/src/components/tools/shared/placement-box.tsx +183 -1
  128. package/src/components/tools/shared/pointer-support-cap.test.ts +98 -0
  129. package/src/components/tools/shared/pointer-support-cap.ts +273 -0
  130. package/src/components/tools/shared/polygon-editor.tsx +276 -35
  131. package/src/components/tools/site/site-boundary-editor.tsx +101 -42
  132. package/src/components/tools/site/stroke-pointer-ownership.test.ts +91 -0
  133. package/src/components/tools/site/stroke-pointer-ownership.ts +57 -0
  134. package/src/components/tools/site/terrain-brush-cursor.tsx +247 -0
  135. package/src/components/tools/site/terrain-sculpt-grid.tsx +192 -0
  136. package/src/components/tools/site/terrain-sculpt-tool.tsx +356 -0
  137. package/src/components/tools/stair/stair-click-guard.test.ts +58 -0
  138. package/src/components/tools/stair/stair-click-guard.ts +72 -0
  139. package/src/components/tools/stair/stair-defaults.ts +1 -0
  140. package/src/components/tools/stair/stair-tool.tsx +164 -42
  141. package/src/components/tools/tool-manager.tsx +149 -30
  142. package/src/components/tools/wall/wall-drafting.test.ts +447 -0
  143. package/src/components/tools/wall/wall-drafting.ts +304 -108
  144. package/src/components/tools/wall/wall-snap-geometry.test.ts +52 -0
  145. package/src/components/tools/wall/wall-snap-geometry.ts +137 -9
  146. package/src/components/tools/zone/zone-boundary-editor.tsx +5 -1
  147. package/src/components/tools/zone/zone-tool.tsx +98 -88
  148. package/src/components/ui/action-menu/camera-actions.tsx +24 -17
  149. package/src/components/ui/action-menu/control-modes.tsx +44 -94
  150. package/src/components/ui/action-menu/furnish-tools.tsx +5 -5
  151. package/src/components/ui/action-menu/index.tsx +2 -4
  152. package/src/components/ui/action-menu/measurement-control.tsx +267 -0
  153. package/src/components/ui/action-menu/structure-tools.tsx +22 -13
  154. package/src/components/ui/action-menu/view-toggles.tsx +42 -90
  155. package/src/components/ui/command-palette/editor-commands.tsx +16 -3
  156. package/src/components/ui/command-palette/index.tsx +4 -3
  157. package/src/components/ui/controls/material-paint-panel.tsx +93 -18
  158. package/src/components/ui/controls/material-picker.tsx +162 -152
  159. package/src/components/ui/controls/material-properties-editor.tsx +108 -0
  160. package/src/components/ui/controls/metric-control.tsx +118 -44
  161. package/src/components/ui/controls/scene-material-list.tsx +247 -0
  162. package/src/components/ui/controls/slider-control.tsx +88 -38
  163. package/src/components/ui/controls/terrain-sculpt-panel.tsx +193 -0
  164. package/src/components/ui/floating-level-selector.tsx +74 -20
  165. package/src/components/ui/helpers/building-helper.tsx +10 -23
  166. package/src/components/ui/helpers/contextual-helper-panel.tsx +428 -0
  167. package/src/components/ui/helpers/helper-manager.tsx +273 -14
  168. package/src/components/ui/helpers/item-helper.tsx +28 -32
  169. package/src/components/ui/helpers/registered-tool-helper.tsx +45 -11
  170. package/src/components/ui/helpers/roof-helper.tsx +10 -12
  171. package/src/components/ui/icon-ref.tsx +47 -0
  172. package/src/components/ui/item-catalog/catalog-items.tsx +39 -1
  173. package/src/components/ui/item-catalog/item-catalog.tsx +13 -36
  174. package/src/components/ui/level-duplicate-dialog.tsx +1 -1
  175. package/src/components/ui/panels/multi-selection-panel.tsx +57 -0
  176. package/src/components/ui/panels/node-display.ts +17 -17
  177. package/src/components/ui/panels/panel-manager.tsx +36 -14
  178. package/src/components/ui/panels/panel-wrapper.tsx +24 -3
  179. package/src/components/ui/panels/parametric-inspector.tsx +19 -4
  180. package/src/components/ui/panels/reference-panel.tsx +54 -19
  181. package/src/components/ui/panels/selection-breakdown.test.ts +24 -0
  182. package/src/components/ui/panels/selection-breakdown.ts +20 -0
  183. package/src/components/ui/primitives/shortcut-token.tsx +39 -3
  184. package/src/components/ui/primitives/sidebar.tsx +1 -0
  185. package/src/components/ui/sidebar/app-sidebar.tsx +5 -1
  186. package/src/components/ui/sidebar/icon-rail.tsx +50 -31
  187. package/src/components/ui/sidebar/panels/plugins-panel.tsx +218 -0
  188. package/src/components/ui/sidebar/panels/settings-panel/index.tsx +82 -41
  189. package/src/components/ui/sidebar/panels/settings-panel/keyboard-shortcuts-dialog.tsx +76 -6
  190. package/src/components/ui/sidebar/panels/settings-panel/load-build-dialog.tsx +12 -4
  191. package/src/components/ui/sidebar/panels/site-panel/building-tree-node.tsx +2 -1
  192. package/src/components/ui/sidebar/panels/site-panel/ceiling-tree-node.tsx +4 -3
  193. package/src/components/ui/sidebar/panels/site-panel/chimney-tree-node.tsx +10 -7
  194. package/src/components/ui/sidebar/panels/site-panel/column-tree-node.tsx +1 -1
  195. package/src/components/ui/sidebar/panels/site-panel/door-tree-node.tsx +7 -2
  196. package/src/components/ui/sidebar/panels/site-panel/dormer-tree-node.tsx +10 -7
  197. package/src/components/ui/sidebar/panels/site-panel/elevator-tree-node.tsx +1 -1
  198. package/src/components/ui/sidebar/panels/site-panel/fence-tree-node.tsx +1 -1
  199. package/src/components/ui/sidebar/panels/site-panel/gutter-tree-node.tsx +10 -7
  200. package/src/components/ui/sidebar/panels/site-panel/index.tsx +42 -17
  201. package/src/components/ui/sidebar/panels/site-panel/item-tree-node.tsx +31 -17
  202. package/src/components/ui/sidebar/panels/site-panel/level-tree-node.tsx +1 -1
  203. package/src/components/ui/sidebar/panels/site-panel/registry-tree-node.tsx +72 -25
  204. package/src/components/ui/sidebar/panels/site-panel/roof-tree-node.tsx +7 -3
  205. package/src/components/ui/sidebar/panels/site-panel/shelf-tree-node.tsx +12 -8
  206. package/src/components/ui/sidebar/panels/site-panel/slab-tree-node.tsx +4 -3
  207. package/src/components/ui/sidebar/panels/site-panel/solar-panel-tree-node.tsx +10 -7
  208. package/src/components/ui/sidebar/panels/site-panel/spawn-tree-node.tsx +1 -1
  209. package/src/components/ui/sidebar/panels/site-panel/stair-tree-node.tsx +2 -2
  210. package/src/components/ui/sidebar/panels/site-panel/tree-node.tsx +44 -3
  211. package/src/components/ui/sidebar/panels/site-panel/tree-structure.ts +36 -0
  212. package/src/components/ui/sidebar/panels/site-panel/wall-tree-node.tsx +1 -1
  213. package/src/components/ui/sidebar/panels/site-panel/window-tree-node.tsx +7 -2
  214. package/src/components/ui/sidebar/panels/site-panel/zone-tree-node.tsx +3 -3
  215. package/src/components/ui/sidebar/tab-bar.tsx +42 -28
  216. package/src/components/ui/sidebar/use-plugin-panels.tsx +132 -0
  217. package/src/components/ui/snap-target-badge.test.tsx +40 -0
  218. package/src/components/ui/snap-target-badge.tsx +88 -0
  219. package/src/components/viewer/viewer-controls-bar.tsx +466 -0
  220. package/src/components/viewer/viewer-scene-header.tsx +235 -0
  221. package/src/components/viewer-overlay.tsx +36 -596
  222. package/src/components/viewer-zone-system.tsx +6 -1
  223. package/src/components/walkthrough-hud.tsx +111 -0
  224. package/src/hooks/use-auto-save.test.ts +14 -0
  225. package/src/hooks/use-auto-save.ts +54 -7
  226. package/src/hooks/use-ceiling-events.ts +3 -2
  227. package/src/hooks/use-drag-action.ts +4 -1
  228. package/src/hooks/use-grid-events.ts +24 -0
  229. package/src/hooks/use-keyboard.ts +438 -70
  230. package/src/index.tsx +355 -9
  231. package/src/lib/active-placement-surface.test.ts +24 -0
  232. package/src/lib/active-placement-surface.ts +42 -0
  233. package/src/lib/camera-pose.test.ts +237 -0
  234. package/src/lib/camera-pose.ts +172 -0
  235. package/src/lib/ceiling-plan-snap.ts +24 -0
  236. package/src/lib/contextual-help.test.ts +112 -0
  237. package/src/lib/contextual-help.ts +144 -0
  238. package/src/lib/continuation.ts +64 -0
  239. package/src/lib/direct-manipulation.test.ts +275 -0
  240. package/src/lib/direct-manipulation.ts +118 -0
  241. package/src/lib/door-interaction.ts +1 -1
  242. package/src/lib/editor-api.ts +23 -35
  243. package/src/lib/elevation-guides.test.ts +107 -0
  244. package/src/lib/elevation-guides.ts +236 -0
  245. package/src/lib/floorplan/annotation-visibility.test.ts +327 -0
  246. package/src/lib/floorplan/annotation-visibility.ts +133 -0
  247. package/src/lib/floorplan/apply-alignment.test.ts +25 -0
  248. package/src/lib/floorplan/apply-alignment.ts +18 -12
  249. package/src/lib/floorplan/drawing-coordination.test.ts +46 -0
  250. package/src/lib/floorplan/drawing-coordination.ts +13 -0
  251. package/src/lib/floorplan/floorplan-export.test.ts +319 -0
  252. package/src/lib/floorplan/floorplan-export.tsx +978 -0
  253. package/src/lib/floorplan/floorplan-extension.test.ts +43 -0
  254. package/src/lib/floorplan/floorplan-extension.ts +181 -0
  255. package/src/lib/floorplan/floorplan-mode.test.ts +120 -0
  256. package/src/lib/floorplan/floorplan-mode.ts +73 -0
  257. package/src/lib/floorplan/floorplan-pdfkit-document.ts +194 -0
  258. package/src/lib/floorplan/floorplan-pdfkit-renderer.test.ts +107 -0
  259. package/src/lib/floorplan/floorplan-pdfkit-renderer.ts +680 -0
  260. package/src/lib/floorplan/geometry.ts +52 -0
  261. package/src/lib/floorplan/index.ts +3 -0
  262. package/src/lib/floorplan/items.ts +5 -2
  263. package/src/lib/floorplan/plan-coords.ts +21 -0
  264. package/src/lib/fresh-planar-placement.test.ts +283 -3
  265. package/src/lib/fresh-planar-placement.ts +85 -1
  266. package/src/lib/glb-export.test.ts +556 -0
  267. package/src/lib/glb-export.ts +1071 -0
  268. package/src/lib/ground-surface.ts +95 -0
  269. package/src/lib/history.test.ts +96 -0
  270. package/src/lib/history.ts +31 -0
  271. package/src/lib/interaction/hot-set.test.ts +133 -0
  272. package/src/lib/interaction/hot-set.ts +67 -0
  273. package/src/lib/interaction/overlay-policy.test.ts +52 -0
  274. package/src/lib/interaction/overlay-policy.ts +59 -0
  275. package/src/lib/interaction/scope.ts +243 -0
  276. package/src/lib/level-duplication.test.ts +2 -1
  277. package/src/lib/level-selection.ts +2 -2
  278. package/src/lib/material-paint.ts +36 -49
  279. package/src/lib/measurement-kind.test.ts +30 -0
  280. package/src/lib/measurement-kind.ts +19 -0
  281. package/src/lib/measurement-label.test.ts +47 -0
  282. package/src/lib/measurement-label.ts +68 -0
  283. package/src/lib/measurement-parser.ts +116 -0
  284. package/src/lib/measurements.test.ts +188 -0
  285. package/src/lib/measurements.ts +210 -0
  286. package/src/lib/paint-scope.test.ts +454 -0
  287. package/src/lib/paint-scope.ts +461 -0
  288. package/src/lib/placement-drag-release.ts +23 -0
  289. package/src/lib/planar-cursor-placement.test.ts +57 -0
  290. package/src/lib/plugin-panels.test.ts +19 -0
  291. package/src/lib/plugin-panels.ts +91 -0
  292. package/src/lib/quick-action-feedback.ts +33 -0
  293. package/src/lib/quick-action-nodes.test.ts +94 -0
  294. package/src/lib/quick-action-nodes.ts +47 -0
  295. package/src/lib/quick-measurement.test.ts +77 -0
  296. package/src/lib/quick-measurement.ts +109 -0
  297. package/src/lib/roof-duplication.ts +6 -6
  298. package/src/lib/roof-hover-outline-proxy.ts +22 -0
  299. package/src/lib/roof-wall-hit.ts +164 -0
  300. package/src/lib/scene-clipboard.test.ts +325 -0
  301. package/src/lib/scene-clipboard.ts +310 -21
  302. package/src/lib/scene.ts +28 -5
  303. package/src/lib/selection-routing.test.ts +313 -0
  304. package/src/lib/selection-routing.ts +180 -0
  305. package/src/lib/sfx/index.ts +1 -0
  306. package/src/lib/sfx/movement-tick.test.ts +65 -0
  307. package/src/lib/sfx/movement-tick.ts +18 -0
  308. package/src/lib/sfx-bus.ts +82 -14
  309. package/src/lib/sfx-player.test.ts +266 -0
  310. package/src/lib/sfx-player.ts +262 -69
  311. package/src/lib/site-boundary.test.ts +53 -0
  312. package/src/lib/site-boundary.ts +27 -0
  313. package/src/lib/slab-plan-snap.test.ts +93 -0
  314. package/src/lib/slab-plan-snap.ts +108 -0
  315. package/src/lib/snapping-mode.test.ts +165 -0
  316. package/src/lib/snapping-mode.ts +191 -0
  317. package/src/lib/stair-duplication.test.ts +103 -0
  318. package/src/lib/stair-duplication.ts +58 -86
  319. package/src/lib/stair-levels.ts +2 -0
  320. package/src/lib/surface-plan-snap.test.ts +71 -0
  321. package/src/lib/surface-plan-snap.ts +256 -0
  322. package/src/lib/terrain-sculpt.test.ts +429 -0
  323. package/src/lib/terrain-sculpt.ts +301 -0
  324. package/src/lib/terrain-verb-color.test.ts +51 -0
  325. package/src/lib/terrain-verb-color.ts +58 -0
  326. package/src/lib/touch-gesture-priority.test.ts +45 -0
  327. package/src/lib/touch-gesture-priority.ts +30 -0
  328. package/src/lib/use-linear-display.ts +40 -0
  329. package/src/lib/window-interaction.ts +1 -1
  330. package/src/lib/world-grid-snap.ts +37 -5
  331. package/src/store/camera-pose-store.test.ts +29 -0
  332. package/src/store/camera-pose-store.ts +27 -0
  333. package/src/store/live-draft-preview-stores.test.ts +136 -0
  334. package/src/store/navigation-sync-pose-store.test.ts +34 -0
  335. package/src/store/navigation-sync-pose-store.ts +27 -0
  336. package/src/store/terrain-sculpt-mode.test.ts +223 -0
  337. package/src/store/use-delete-confirmation.ts +27 -0
  338. package/src/store/use-direct-manipulation-feedback.ts +20 -0
  339. package/src/store/use-drawing-view.test.ts +23 -0
  340. package/src/store/use-drawing-view.ts +84 -0
  341. package/src/store/use-editor.tsx +665 -126
  342. package/src/store/use-elevation-guides.ts +40 -0
  343. package/src/store/use-facing-pose.ts +44 -0
  344. package/src/store/use-fence-curve-draft.ts +51 -0
  345. package/src/store/use-first-person-hud.ts +40 -0
  346. package/src/store/use-floorplan-annotation-visibility.ts +60 -0
  347. package/src/store/use-floorplan-draft-preview.ts +151 -0
  348. package/src/store/use-floorplan-marquee.ts +50 -0
  349. package/src/store/use-floorplan-mode.ts +110 -0
  350. package/src/store/use-interaction-scope.test.ts +206 -0
  351. package/src/store/use-interaction-scope.ts +140 -0
  352. package/src/store/use-measurement-draft.test.ts +559 -0
  353. package/src/store/use-measurement-draft.ts +570 -0
  354. package/src/store/use-opening-guides.ts +41 -0
  355. package/src/store/use-path-draft-preview.ts +90 -0
  356. package/src/store/use-placement-preview.ts +11 -3
  357. package/src/store/use-quick-measurement-hud.test.ts +53 -0
  358. package/src/store/use-quick-measurement-hud.ts +77 -0
  359. package/src/store/use-segment-draft-chain.ts +28 -0
  360. package/src/store/use-stair-build-preview.ts +53 -0
  361. package/src/store/use-wall-snap-indicator.ts +2 -0
  362. package/src/components/editor/first-person/bvh-ecctrl.tsx +0 -860
  363. package/src/components/editor/group-move-handle.tsx +0 -316
  364. package/src/components/ui/panels/paint-panel.tsx +0 -163
  365. package/src/lib/floorplan/selection-tool.ts +0 -271
  366. package/src/lib/level-name.ts +0 -11
@@ -0,0 +1,429 @@
1
+ import { beforeEach, describe, expect, test } from 'bun:test'
2
+ import {
3
+ createTerrainField,
4
+ DEFAULT_TERRAIN_SPACING,
5
+ heightAt,
6
+ minBrushRadius,
7
+ type SiteNode,
8
+ terrainFieldOf,
9
+ useLiveTerrain,
10
+ useScene,
11
+ } from '@pascal-app/core'
12
+ import {
13
+ brushRadiusRange,
14
+ clampBrushRadius,
15
+ clipTerrainPatchToSite,
16
+ fieldExtentForSite,
17
+ flattenSite,
18
+ resetSiteTerrain,
19
+ resolveFlattenTarget,
20
+ sculptFieldForSite,
21
+ terrainPointInsideSite,
22
+ } from './terrain-sculpt'
23
+
24
+ // `updateNode` batches its dirty-node flush through rAF, which bun's runtime does
25
+ // not provide. Running the callback synchronously is what the other scene-writing
26
+ // unit tests do (`handle-drag-history.test.ts`).
27
+ type RafFn = (callback: (time: number) => void) => number
28
+ ;(globalThis as { requestAnimationFrame?: RafFn }).requestAnimationFrame ??= (callback) => {
29
+ callback(0)
30
+ return 0
31
+ }
32
+ ;(globalThis as { cancelAnimationFrame?: (id: number) => void }).cancelAnimationFrame ??= () => {}
33
+
34
+ function site(points: Array<[number, number]>): SiteNode {
35
+ return {
36
+ id: 'site_1',
37
+ type: 'site',
38
+ children: [],
39
+ polygon: { type: 'polygon', points },
40
+ } as unknown as SiteNode
41
+ }
42
+
43
+ describe('fieldExtentForSite', () => {
44
+ test('covers the whole polygon with padding to spare', () => {
45
+ const extent = fieldExtentForSite(
46
+ site([
47
+ [-10, -10],
48
+ [10, -10],
49
+ [10, 10],
50
+ [-10, 10],
51
+ ]),
52
+ )
53
+ const maxX = extent.origin[0] + (extent.cols - 1) * extent.spacing
54
+ const maxZ = extent.origin[1] + (extent.rows - 1) * extent.spacing
55
+
56
+ // Strictly outside the lot on all four sides: the field must not end at the
57
+ // property line, or the terrain mesh stops exactly where the boundary is
58
+ // drawn and leaves a visible cliff.
59
+ expect(extent.origin[0]).toBeLessThan(-10)
60
+ expect(extent.origin[1]).toBeLessThan(-10)
61
+ expect(maxX).toBeGreaterThan(10)
62
+ expect(maxZ).toBeGreaterThan(10)
63
+ })
64
+
65
+ test('is square and 2ⁿ+1 so a future LOD halving lands on real samples', () => {
66
+ for (const half of [4, 12, 30, 80]) {
67
+ const extent = fieldExtentForSite(
68
+ site([
69
+ [-half, -half],
70
+ [half, -half],
71
+ [half, half],
72
+ [-half, half],
73
+ ]),
74
+ )
75
+ expect(extent.cols).toBe(extent.rows)
76
+ expect([33, 65, 129, 257]).toContain(extent.cols)
77
+ }
78
+ })
79
+
80
+ test('centres on the lot even when the lot is off-origin', () => {
81
+ const extent = fieldExtentForSite(
82
+ site([
83
+ [100, 40],
84
+ [120, 40],
85
+ [120, 60],
86
+ [100, 60],
87
+ ]),
88
+ )
89
+ const centerX = extent.origin[0] + ((extent.cols - 1) / 2) * extent.spacing
90
+ const centerZ = extent.origin[1] + ((extent.rows - 1) / 2) * extent.spacing
91
+ expect(centerX).toBeCloseTo(110, 6)
92
+ expect(centerZ).toBeCloseTo(50, 6)
93
+ })
94
+
95
+ test('holds the default spacing while the lot fits in 257 samples', () => {
96
+ const extent = fieldExtentForSite(
97
+ site([
98
+ [-20, -20],
99
+ [20, -20],
100
+ [20, 20],
101
+ [-20, 20],
102
+ ]),
103
+ )
104
+ expect(extent.spacing).toBeCloseTo(DEFAULT_TERRAIN_SPACING, 6)
105
+ })
106
+
107
+ test('stretches spacing rather than stopping short on a huge lot', () => {
108
+ // A 400 m lot needs 801 samples at 0.5 m. Capping the sample count and
109
+ // coarsening is the right trade: coarse ground everywhere beats fine ground
110
+ // that ends mid-lot.
111
+ const extent = fieldExtentForSite(
112
+ site([
113
+ [-200, -200],
114
+ [200, -200],
115
+ [200, 200],
116
+ [-200, 200],
117
+ ]),
118
+ )
119
+ expect(extent.cols).toBe(257)
120
+ expect(extent.spacing).toBeGreaterThan(DEFAULT_TERRAIN_SPACING)
121
+ const maxX = extent.origin[0] + (extent.cols - 1) * extent.spacing
122
+ expect(maxX).toBeGreaterThan(200)
123
+ })
124
+
125
+ test('a degenerate polygon still yields a usable field', () => {
126
+ for (const points of [[], [[0, 0]] as Array<[number, number]>]) {
127
+ const extent = fieldExtentForSite(site(points as Array<[number, number]>))
128
+ expect(extent.cols).toBe(65)
129
+ expect(extent.spacing).toBeCloseTo(DEFAULT_TERRAIN_SPACING, 6)
130
+ }
131
+ })
132
+
133
+ test('a null site does not throw', () => {
134
+ expect(fieldExtentForSite(null).cols).toBe(65)
135
+ expect(fieldExtentForSite(undefined).cols).toBe(65)
136
+ })
137
+ })
138
+
139
+ describe('sculptFieldForSite', () => {
140
+ test('a virgin site gets a fresh flat field sized to its lot', () => {
141
+ const target = site([
142
+ [-10, -10],
143
+ [10, -10],
144
+ [10, 10],
145
+ [-10, 10],
146
+ ])
147
+ const field = sculptFieldForSite(target)
148
+ expect(field.cols).toBe(fieldExtentForSite(target).cols)
149
+ expect(heightAt(field, 0, 0)).toBeCloseTo(0, 6)
150
+ })
151
+ })
152
+
153
+ describe('site footprint', () => {
154
+ const concaveSite = site([
155
+ [0, 0],
156
+ [4, 0],
157
+ [4, 2],
158
+ [2, 2],
159
+ [2, 4],
160
+ [0, 4],
161
+ ])
162
+
163
+ test('includes the boundary but excludes the padded field and concave notch', () => {
164
+ expect(terrainPointInsideSite(concaveSite, 0, 2)).toBe(true)
165
+ expect(terrainPointInsideSite(concaveSite, 1, 3)).toBe(true)
166
+ expect(terrainPointInsideSite(concaveSite, 3, 3)).toBe(false)
167
+ expect(terrainPointInsideSite(concaveSite, -1, 2)).toBe(false)
168
+ })
169
+
170
+ test('a brush patch preserves samples outside the property polygon', () => {
171
+ const field = createTerrainField({ cols: 5, rows: 5, spacing: 1, origin: [0, 0] })
172
+ const heights = new Int16Array(25)
173
+ heights.fill(100)
174
+ const clipped = clipTerrainPatchToSite(
175
+ field,
176
+ { col0: 0, row0: 0, cols: 5, rows: 5, heights },
177
+ concaveSite,
178
+ )
179
+
180
+ expect(clipped.heights[1 * 5 + 1]).toBe(100)
181
+ expect(clipped.heights[3 * 5 + 3]).toBe(0)
182
+ expect(clipped.heights[2 * 5 + 0]).toBe(100)
183
+ expect(clipped.heights[2 * 5 + 4]).toBe(100)
184
+ })
185
+ })
186
+
187
+ describe('brushRadiusRange', () => {
188
+ const hugeLot = site([
189
+ [-200, -200],
190
+ [200, -200],
191
+ [200, 200],
192
+ [-200, 200],
193
+ ])
194
+ const smallLot = site([
195
+ [-10, -10],
196
+ [10, -10],
197
+ [10, 10],
198
+ [-10, 10],
199
+ ])
200
+
201
+ test('the low end is the field floor, not a constant', () => {
202
+ // The bug: both the slider and the `[`/`]` keys hardcoded 0.5 m. On a lot big
203
+ // enough that `fieldExtentForSite` stretches spacing, 0.5 m is under the
204
+ // sample gap and the brush silently paints nothing. The floor has to move
205
+ // with the lot.
206
+ const [smallMin] = brushRadiusRange(smallLot)
207
+ const [hugeMin] = brushRadiusRange(hugeLot)
208
+ expect(smallMin).toBeCloseTo(minBrushRadius({ spacing: DEFAULT_TERRAIN_SPACING }), 6)
209
+ expect(hugeMin).toBeGreaterThan(smallMin)
210
+ expect(hugeMin).toBeCloseTo(minBrushRadius(sculptFieldForSite(hugeLot)), 6)
211
+ })
212
+
213
+ test('reads the spacing of terrain that already exists', () => {
214
+ // A lot whose field was created earlier (or imported from a DEM at its own
215
+ // resolution) must be measured by *that* field, not by what a fresh one
216
+ // would get — otherwise the floor is wrong for exactly the sites that have
217
+ // been sculpted.
218
+ const existing = { ...smallLot, terrain: { spacing: 4 } } as unknown as SiteNode
219
+ const [min] = brushRadiusRange(existing)
220
+ expect(min).toBeCloseTo(minBrushRadius({ spacing: 4 }), 6)
221
+ })
222
+
223
+ test('a site with no lot yet still yields a usable range', () => {
224
+ for (const target of [null, undefined]) {
225
+ const [min, max] = brushRadiusRange(target)
226
+ expect(min).toBeGreaterThan(0)
227
+ expect(max).toBeGreaterThan(min)
228
+ }
229
+ })
230
+
231
+ test('the range never inverts', () => {
232
+ // Reachable: a lot coarse enough that the floor passes MAX_BRUSH_RADIUS would
233
+ // otherwise give min > max, and a slider whose min exceeds its max clamps
234
+ // every value to a different number depending on which bound is applied last.
235
+ for (const spacing of [0.5, 4, 20, 100]) {
236
+ const target = { ...smallLot, terrain: { spacing } } as unknown as SiteNode
237
+ const [min, max] = brushRadiusRange(target)
238
+ expect(max).toBeGreaterThanOrEqual(min)
239
+ expect(clampBrushRadius(target, 1)).toBeGreaterThanOrEqual(min)
240
+ expect(clampBrushRadius(target, 1e6)).toBeLessThanOrEqual(max)
241
+ }
242
+ })
243
+ })
244
+
245
+ describe('clampBrushRadius', () => {
246
+ const lot = site([
247
+ [-10, -10],
248
+ [10, -10],
249
+ [10, 10],
250
+ [-10, 10],
251
+ ])
252
+
253
+ test('holds a radius already in range untouched', () => {
254
+ expect(clampBrushRadius(lot, 5)).toBe(5)
255
+ })
256
+
257
+ test('the `[`/`]` ladder stays inside the range and reverses', () => {
258
+ // The keys step multiplicatively (×1.25) and quantize to decimetres, which is
259
+ // where a ratchet would hide: if `]` then `[` did not return, the pair would
260
+ // walk the radius away from where the user left it. Only the clamp at the top
261
+ // is allowed to be one-way.
262
+ const step = (radius: number, up: boolean) =>
263
+ clampBrushRadius(lot, Math.round(radius * (up ? 1.25 : 1 / 1.25) * 10) / 10)
264
+ const [min, max] = brushRadiusRange(lot)
265
+
266
+ let radius = min
267
+ const visited: number[] = []
268
+ for (let i = 0; i < 40 && radius < max; i++) {
269
+ const next = step(radius, true)
270
+ if (next === radius) break
271
+ visited.push(next)
272
+ radius = next
273
+ }
274
+ // The ladder has to actually traverse the range, or `]` is a dead key
275
+ // somewhere in the middle.
276
+ expect(visited.at(-1)).toBe(max)
277
+ expect(visited.length).toBeGreaterThan(8)
278
+
279
+ for (const value of visited) {
280
+ expect(value).toBeGreaterThanOrEqual(min)
281
+ expect(value).toBeLessThanOrEqual(max)
282
+ // Below the top, up-then-down returns. At the ceiling it cannot: one step
283
+ // down from the max is a genuine 20% reduction, which is correct.
284
+ if (step(value, true) !== max) expect(step(step(value, true), false)).toBe(value)
285
+ }
286
+ })
287
+ })
288
+
289
+ describe('resolveFlattenTarget', () => {
290
+ test('an explicit target wins over the ground under the cursor', () => {
291
+ const field = createTerrainField({ cols: 33, rows: 33, spacing: 0.5, origin: [-8, -8] })
292
+ expect(resolveFlattenTarget(field, 3.25, 0, 0)).toBeCloseTo(3.25, 6)
293
+ })
294
+
295
+ test('with no explicit target the first click samples the ground', () => {
296
+ // This is what makes flatten usable without ever opening a number field.
297
+ const field = createTerrainField({ cols: 33, rows: 33, spacing: 0.5, origin: [-8, -8] })
298
+ const raised = { ...field, heights: field.heights.slice() }
299
+ // Sample [16,16] is world (0,0) — put it at 1.5 m.
300
+ raised.heights[16 * 33 + 16] = Math.round(1.5 / raised.step)
301
+ expect(resolveFlattenTarget(raised, null, 0, 0)).toBeCloseTo(1.5, 6)
302
+ })
303
+
304
+ test('an explicit target of 0 is honoured, not treated as absent', () => {
305
+ // `?? ` rather than `||` — flatten-to-datum is the single most common
306
+ // explicit target, and `||` would silently resample instead.
307
+ const field = createTerrainField({ cols: 33, rows: 33, spacing: 0.5, origin: [-8, -8] })
308
+ const raised = { ...field, heights: field.heights.slice() }
309
+ raised.heights[16 * 33 + 16] = Math.round(2 / raised.step)
310
+ expect(resolveFlattenTarget(raised, 0, 0, 0)).toBe(0)
311
+ })
312
+ })
313
+
314
+ /**
315
+ * What Escape mid-stroke actually promises.
316
+ *
317
+ * The tool's `tool:cancel` handler ends the live stroke *without* calling
318
+ * `commitStroke`, which is the whole mechanism: the live stroke is what every reader
319
+ * prefers over the persisted terrain, so dropping it un-committed reverts the scene
320
+ * by construction. The half worth asserting is that it reverts to the *right* place —
321
+ * a site sculpted before the abandoned stroke must keep that earlier work — and that
322
+ * the abandon leaves no live stroke behind for the next reader to pick up.
323
+ */
324
+ describe('abandoning a stroke — the Escape contract', () => {
325
+ const siteId = 'site_cancel' as SiteNode['id']
326
+
327
+ function fieldAt(metres: number): ReturnType<typeof createTerrainField> {
328
+ const field = createTerrainField({ cols: 33, rows: 33, spacing: 0.5, origin: [-8, -8] })
329
+ const heights = field.heights.slice()
330
+ heights.fill(Math.round(metres / field.step))
331
+ return { ...field, heights }
332
+ }
333
+
334
+ test('ends the live stroke and commits nothing', () => {
335
+ const committed = fieldAt(1.5)
336
+ useLiveTerrain.getState().begin(siteId, committed)
337
+ useLiveTerrain.getState().advance(siteId, fieldAt(9), null as never)
338
+ expect(useLiveTerrain.getState().fieldOf(siteId)).toBeDefined()
339
+
340
+ // Exactly what the tool's `onCancel` does: end, no `commitStroke`.
341
+ useLiveTerrain.getState().end(siteId)
342
+
343
+ // No stroke left armed — a leaked one shows ground the scene graph never had.
344
+ expect(useLiveTerrain.getState().strokes.has(siteId)).toBe(false)
345
+ expect(useLiveTerrain.getState().fieldOf(siteId)).toBeUndefined()
346
+ })
347
+
348
+ test('the snapshot taken at begin is never mutated by the dabs it survives', () => {
349
+ // The anti-compounding invariant, from the cancel side: if a dab could write
350
+ // through to the snapshot, Escape would revert to a *partially* sculpted field
351
+ // rather than the baseline, and the user's earlier work would drift every stroke.
352
+ const committed = fieldAt(1.5)
353
+ const baseline = Array.from(committed.heights)
354
+ useLiveTerrain.getState().begin(siteId, committed)
355
+ useLiveTerrain.getState().advance(siteId, fieldAt(9), null as never)
356
+ useLiveTerrain.getState().end(siteId)
357
+ expect(Array.from(committed.heights)).toEqual(baseline)
358
+ })
359
+ })
360
+
361
+ /**
362
+ * The lot-wide writes have to win over a stroke in flight.
363
+ *
364
+ * Both replace the entire field, and every reader prefers the live stroke over the
365
+ * persisted terrain — so landing one *under* a stroke is invisible twice over: the
366
+ * ground keeps showing the stroke, and the stroke's commit (computed from a snapshot
367
+ * predating the write) then overwrites it. A button that silently does nothing.
368
+ *
369
+ * Not a hypothetical race. On touch one finger holds a stroke while the other taps
370
+ * the panel; with a mouse a stroke survives a wheel zoom's 500 ms `cameraDragging`
371
+ * window and a held pointer that has left the canvas.
372
+ */
373
+ describe('lot-wide terrain writes vs a stroke in flight', () => {
374
+ const lotSiteId = 'site_lotwide' as SiteNode['id']
375
+
376
+ function siteNodeWithTerrain(terrain: SiteNode['terrain']): SiteNode {
377
+ return {
378
+ id: lotSiteId,
379
+ type: 'site',
380
+ children: [],
381
+ polygon: {
382
+ type: 'polygon',
383
+ points: [
384
+ [-10, -10],
385
+ [10, -10],
386
+ [10, 10],
387
+ [-10, 10],
388
+ ],
389
+ },
390
+ terrain,
391
+ } as unknown as SiteNode
392
+ }
393
+
394
+ beforeEach(() => {
395
+ useLiveTerrain.getState().endAll()
396
+ useScene.setState({ nodes: {}, rootNodeIds: [], dirtyNodes: new Set() } as never)
397
+ useScene.temporal.getState().clear()
398
+ useScene.temporal.getState().resume()
399
+ })
400
+
401
+ test('levelling the lot drops the stroke instead of hiding behind it', () => {
402
+ const site = siteNodeWithTerrain(undefined)
403
+ useScene.setState({ nodes: { [lotSiteId]: site }, rootNodeIds: [lotSiteId] } as never)
404
+ useLiveTerrain.getState().begin(lotSiteId, sculptFieldForSite(site))
405
+
406
+ flattenSite(site, 3)
407
+
408
+ // The assertion that fails without the abandon: with a stroke still live,
409
+ // `terrainFieldOf` — and so the mesh, the raycast and the next stroke's
410
+ // snapshot — would keep reading the pre-flatten ground.
411
+ expect(useLiveTerrain.getState().fieldOf(lotSiteId)).toBeUndefined()
412
+ const written = useScene.getState().nodes[lotSiteId] as SiteNode
413
+ expect(heightAt(terrainFieldOf(written) as never, 0, 0)).toBeCloseTo(3, 6)
414
+ })
415
+
416
+ test('clearing terrain drops the stroke even when there is nothing to clear', () => {
417
+ // The early-return path: a first-ever stroke on a virgin site leaves
418
+ // `site.terrain` undefined, so `resetSiteTerrain` returns before writing —
419
+ // and a live stroke surviving that shows ground the scene graph has none of.
420
+ const site = siteNodeWithTerrain(undefined)
421
+ useScene.setState({ nodes: { [lotSiteId]: site }, rootNodeIds: [lotSiteId] } as never)
422
+ useLiveTerrain.getState().begin(lotSiteId, sculptFieldForSite(site))
423
+
424
+ resetSiteTerrain(site)
425
+
426
+ expect(useLiveTerrain.getState().fieldOf(lotSiteId)).toBeUndefined()
427
+ expect(terrainFieldOf(useScene.getState().nodes[lotSiteId] as SiteNode)).toBeNull()
428
+ })
429
+ })