@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
@@ -3,9 +3,11 @@ import {
3
3
  type AnyNodeId,
4
4
  type DoorNode,
5
5
  getGarageVisibleOpeningRatio,
6
+ getLevelElevations,
6
7
  isOperationDoorType,
7
8
  nodeRegistry,
8
9
  sceneRegistry,
10
+ terrainFieldOf,
9
11
  useInteractive,
10
12
  useScene,
11
13
  } from '@pascal-app/core'
@@ -13,6 +15,7 @@ import * as THREE from 'three'
13
15
  import { mergeGeometries } from 'three/examples/jsm/utils/BufferGeometryUtils.js'
14
16
  import { acceleratedRaycast, computeBoundsTree, disposeBoundsTree } from 'three-mesh-bvh'
15
17
  import { computeSceneBoundsXZ } from '../../../lib/scene-bounds'
18
+ import { createTerrainColliderGeometry } from './terrain-collider'
16
19
 
17
20
  const SKIPPED_MESH_NAMES = new Set(['cutout', 'collision-mesh'])
18
21
  const COLLIDER_NODE_CATEGORIES = new Set(['structure', 'furnish'])
@@ -27,6 +30,7 @@ const OPERATION_DOOR_COLLIDER_OPEN_THRESHOLD = 0.85
27
30
  const LEVEL_FALLBACK_FLOOR_THICKNESS = 0.08
28
31
  const LEVEL_FALLBACK_FLOOR_PADDING = 2
29
32
  const LEVEL_FALLBACK_FLOOR_MIN_SIZE = 30
33
+ const SITE_GROUND_COLLIDER_MIN_SIZE = 2000
30
34
 
31
35
  export const FIRST_PERSON_SPAWN_EYE_HEIGHT = SPAWN_EYE_HEIGHT
32
36
 
@@ -49,6 +53,22 @@ function isMesh(object: THREE.Object3D): object is THREE.Mesh {
49
53
  return 'isMesh' in object && (object as THREE.Mesh).isMesh
50
54
  }
51
55
 
56
+ // Renderer-effective visibility: an invisible ancestor hides the whole
57
+ // subtree at render time even when the object's own flag is true. The
58
+ // collider world must match what's rendered — the roof keeps stale,
59
+ // UNCUT per-segment CSG inside its hidden `segments-wrapper` (full-edit
60
+ // exit hides the wrapper without stripping geometry), and cloning those
61
+ // meshes would block the walkthrough player at openings the visible
62
+ // merged shell has cut through.
63
+ function isEffectivelyVisible(object: THREE.Object3D) {
64
+ let current: THREE.Object3D | null = object
65
+ while (current) {
66
+ if (!current.visible) return false
67
+ current = current.parent
68
+ }
69
+ return true
70
+ }
71
+
52
72
  function isColliderMaterialVisible(material: THREE.Material | THREE.Material[]) {
53
73
  return Array.isArray(material) ? material.some((entry) => entry.visible) : material.visible
54
74
  }
@@ -112,10 +132,12 @@ function createLevelFallbackFloorGeometry(level: LevelNode, nodes: SceneNodes) {
112
132
 
113
133
  function collectLevelFallbackFloorGeometries(nodes: SceneNodes) {
114
134
  const geometries: THREE.BufferGeometry[] = []
135
+ const levelElevations = getLevelElevations(nodes)
115
136
 
116
137
  for (const levelId of sceneRegistry.byType.level!) {
117
138
  const node = nodes[levelId as AnyNodeId]
118
139
  if (node?.type !== 'level') continue
140
+ if (levelElevations.get(node.id)?.baseY !== 0) continue
119
141
 
120
142
  const geometry = createLevelFallbackFloorGeometry(node, nodes)
121
143
  if (geometry) geometries.push(geometry)
@@ -129,24 +151,41 @@ function collectLevelFallbackFloorGeometries(nodes: SceneNodes) {
129
151
  // a dedicated collider, a spawn on the bare ground (no slab, or not parented to
130
152
  // a level that triggers the per-level fallback) has no floor to stand on and the
131
153
  // walkthrough player falls through. Derive a thin ground slab from node data (not
132
- // the rendered mesh) so it exists regardless of geometry-mount timing, sized to
133
- // cover the whole scene footprint at the site's ground plane.
154
+ // the rendered mesh) so it exists regardless of geometry-mount timing. The slab
155
+ // is effectively unbounded (not sized to the site polygon): the ground plane must
156
+ // keep holding the player up even after they step past the site boundary,
157
+ // otherwise they fall below the ground plane into the void.
158
+ //
159
+ // A sculpted site takes the terrain path instead: the flat slab would hold the
160
+ // player at the datum inside an excavation and bury them inside a hill. Derived
161
+ // from the field rather than the rendered terrain mesh for the same
162
+ // mount-timing reason as the flat slab.
134
163
  function createSiteGroundColliderGeometry(site: SiteNode, nodes: SceneNodes) {
135
164
  if (site.visible === false) return null
136
165
 
137
166
  const siteObject = sceneRegistry.nodes.get(site.id)
138
167
  if (!siteObject?.visible) return null
139
168
 
169
+ const field = terrainFieldOf(site)
170
+ if (field) {
171
+ const terrainGeometry = createTerrainColliderGeometry(field)
172
+ if (terrainGeometry) {
173
+ siteObject.updateWorldMatrix(true, false)
174
+ terrainGeometry.applyMatrix4(siteObject.matrixWorld)
175
+ return terrainGeometry
176
+ }
177
+ }
178
+
140
179
  const bounds = computeSceneBoundsXZ(nodes)
141
180
  const [centerX, centerZ] = bounds?.center ?? [0, 0]
142
181
  const [boundsWidth, boundsDepth] = bounds?.size ?? [0, 0]
143
182
  const width = Math.max(
144
183
  boundsWidth + LEVEL_FALLBACK_FLOOR_PADDING * 2,
145
- LEVEL_FALLBACK_FLOOR_MIN_SIZE,
184
+ SITE_GROUND_COLLIDER_MIN_SIZE,
146
185
  )
147
186
  const depth = Math.max(
148
187
  boundsDepth + LEVEL_FALLBACK_FLOOR_PADDING * 2,
149
- LEVEL_FALLBACK_FLOOR_MIN_SIZE,
188
+ SITE_GROUND_COLLIDER_MIN_SIZE,
150
189
  )
151
190
 
152
191
  const geometry = createBoxColliderGeometry(width, LEVEL_FALLBACK_FLOOR_THICKNESS, depth)
@@ -316,9 +355,12 @@ function collectColliderGeometriesFromNode(
316
355
  if (visitedMeshes.has(object)) return
317
356
  visitedMeshes.add(object)
318
357
 
358
+ // Prune hidden subtrees — children of an invisible group never render,
359
+ // so they must not collide either (see isEffectivelyVisible).
360
+ if (!object.visible) return
361
+
319
362
  if (
320
363
  isMesh(object) &&
321
- object.visible &&
322
364
  isColliderMaterialVisible(object.material) &&
323
365
  !SKIPPED_MESH_NAMES.has(object.name)
324
366
  ) {
@@ -361,6 +403,11 @@ export function buildFirstPersonColliderWorldFromRegistry(): FirstPersonCollider
361
403
  const root = sceneRegistry.nodes.get(nodeId)
362
404
  if (!root) continue
363
405
 
406
+ // Registered objects can sit inside a hidden wrapper (roof segments
407
+ // under `segments-wrapper`) — the per-node traversal starts AT the
408
+ // object, so the ancestor chain must be checked here.
409
+ if (!isEffectivelyVisible(root)) continue
410
+
364
411
  if (node.type === 'door') {
365
412
  const doorGeometry = createDoorLeafColliderGeometry(root, node)
366
413
  if (doorGeometry) {
@@ -0,0 +1,132 @@
1
+ import { describe, expect, test } from 'bun:test'
2
+ import {
3
+ applyHeightPatch,
4
+ createTerrainField,
5
+ flattenPatch,
6
+ heightAt,
7
+ type TerrainField,
8
+ } from '@pascal-app/core'
9
+ import { Mesh, MeshBasicMaterial, Raycaster, Vector3 } from 'three'
10
+ import { createTerrainColliderGeometry } from './terrain-collider'
11
+
12
+ const DOWN = new Vector3(0, -1, 0)
13
+ const UP = new Vector3(0, 1, 0)
14
+
15
+ /** A 2.5 m plateau over x,z ∈ [2,5] on an otherwise flat field. */
16
+ function plateauField(): TerrainField {
17
+ const base = createTerrainField({ cols: 17, rows: 17, spacing: 1, origin: [-8, -8] })
18
+ const patch = flattenPatch(base, { minX: 2, minZ: 2, maxX: 5, maxZ: 5 }, 2.5)
19
+ return applyHeightPatch(base, patch as never)
20
+ }
21
+
22
+ /** A field sloping 0.1 m per metre along +x, so every sample differs. */
23
+ function slopedField(): TerrainField {
24
+ const field = createTerrainField({ cols: 9, rows: 9, spacing: 1, origin: [-4, -4] })
25
+ const heights = new Int16Array(field.heights)
26
+ for (let r = 0; r < field.rows; r++) {
27
+ for (let c = 0; c < field.cols; c++) {
28
+ heights[r * field.cols + c] = Math.round((c * 0.1) /* m */ / field.step)
29
+ }
30
+ }
31
+ return { ...field, heights }
32
+ }
33
+
34
+ function colliderMesh(field: TerrainField) {
35
+ const geometry = createTerrainColliderGeometry(field)
36
+ if (!geometry) throw new Error('no collider geometry')
37
+ const mesh = new Mesh(geometry, new MeshBasicMaterial())
38
+ mesh.updateMatrixWorld(true)
39
+ return mesh
40
+ }
41
+
42
+ /** The height a downward ray finds — what the walkthrough player stands on. */
43
+ function groundUnder(mesh: Mesh, x: number, z: number): number | null {
44
+ const raycaster = new Raycaster()
45
+ raycaster.set(new Vector3(x, 500, z), DOWN)
46
+ const hits = raycaster.intersectObject(mesh, false)
47
+ const floor = hits.find((hit) => {
48
+ if (!hit.face) return true
49
+ return hit.face.normal.clone().transformDirection(mesh.matrixWorld).dot(UP) > 0.2
50
+ })
51
+ return floor ? floor.point.y : null
52
+ }
53
+
54
+ describe('createTerrainColliderGeometry', () => {
55
+ test('the walkable surface is the field, not a plane', () => {
56
+ const field = plateauField()
57
+ const mesh = colliderMesh(field)
58
+
59
+ // On the plateau, off it, and on the slope between: the collider has to agree
60
+ // with `heightAt` at each, or what you see is not what you stand on.
61
+ for (const [x, z] of [
62
+ [3, 3],
63
+ [-3, -3],
64
+ [1.5, 3],
65
+ [4.5, 4.5],
66
+ ] as Array<[number, number]>) {
67
+ expect(groundUnder(mesh, x, z)).toBeCloseTo(heightAt(field, x, z), 4)
68
+ }
69
+ })
70
+
71
+ test('a sloped field is sampled per vertex, not averaged', () => {
72
+ const field = slopedField()
73
+ const mesh = colliderMesh(field)
74
+
75
+ // Sample midpoints, where a flat or decimated collider would disagree most.
76
+ for (const x of [-3.5, -0.5, 1.5, 3.5]) {
77
+ expect(groundUnder(mesh, x, 0)).toBeCloseTo(heightAt(field, x, 0), 4)
78
+ }
79
+ })
80
+
81
+ test('the skirt keeps holding the player far past the field', () => {
82
+ const field = slopedField()
83
+ const mesh = colliderMesh(field)
84
+
85
+ // Past every edge and past a corner: `heightAt` clamps to the border there, so
86
+ // the collider must too. Without the skirt these are null and the player falls.
87
+ for (const [x, z] of [
88
+ [400, 0],
89
+ [-400, 0],
90
+ [0, 400],
91
+ [0, -400],
92
+ [400, 400],
93
+ [-400, -400],
94
+ ] as Array<[number, number]>) {
95
+ expect(groundUnder(mesh, x, z)).toBeCloseTo(heightAt(field, x, z), 4)
96
+ }
97
+ })
98
+
99
+ test('every triangle carries a normal, and the surface faces up', () => {
100
+ const geometry = createTerrainColliderGeometry(plateauField())
101
+ if (!geometry) throw new Error('no collider geometry')
102
+
103
+ const position = geometry.getAttribute('position')
104
+ const normal = geometry.getAttribute('normal')
105
+ expect(normal).toBeDefined()
106
+ expect(normal.count).toBe(position.count)
107
+ // Non-indexed triangles — `mergeGeometries` needs every contributor to match.
108
+ expect(geometry.index).toBeNull()
109
+ expect(position.count % 3).toBe(0)
110
+
111
+ for (let i = 0; i < normal.count; i++) {
112
+ expect(normal.getY(i)).toBeGreaterThan(0)
113
+ }
114
+ })
115
+
116
+ test('decimates rather than exploding on an absurd field', () => {
117
+ // 1025² is inside the schema's decode-bomb guard but 1M cells — an exact
118
+ // collider would be 2M triangles for a surface nobody can walk that far across.
119
+ const field = createTerrainField({ cols: 1025, rows: 1025, spacing: 0.5 })
120
+ const geometry = createTerrainColliderGeometry(field)
121
+ if (!geometry) throw new Error('no collider geometry')
122
+
123
+ const triangles = geometry.getAttribute('position').count / 3
124
+ expect(triangles).toBeLessThan(200_000)
125
+ // Still a real surface, not an empty one.
126
+ expect(triangles).toBeGreaterThan(1000)
127
+ })
128
+
129
+ test('returns null for a degenerate field', () => {
130
+ expect(createTerrainColliderGeometry(createTerrainField({ cols: 1, rows: 1 }))).toBeNull()
131
+ })
132
+ })
@@ -0,0 +1,212 @@
1
+ /**
2
+ * The sculpted ground as walkthrough collision geometry.
3
+ *
4
+ * The first-person collider world is a single merged BVH mesh, and before terrain
5
+ * the ground contributed one flat 2 km box to it. That box is wrong in both
6
+ * directions on a sculpted site: it holds the player up at the datum inside an
7
+ * excavation, and buries them inside a hill. This module replaces it.
8
+ *
9
+ * Two properties it has to hold, because the whole point is that walking agrees
10
+ * with every other consumer of the field:
11
+ *
12
+ * - **The surface is the field**, sampled at every vertex — the same
13
+ * `heightAtSample`/`normalAt` the rendered mesh uses (`terrain-geometry.ts`),
14
+ * so what you see is what you stand on. It is deliberately not a decimated
15
+ * proxy at realistic field sizes.
16
+ * - **It extends past the field**, because `heightAt` clamps to the border
17
+ * outside it. A collider that stopped at the field edge would drop the player
18
+ * into the void one step past the site, which is exactly the bug the flat box
19
+ * existed to prevent. The skirt extrudes each border sample outward at its own
20
+ * height, so the collider and `heightAt` agree everywhere, not just inside.
21
+ *
22
+ * Non-indexed triangles with position + normal, matching what the other collider
23
+ * contributors emit — `mergeGeometries` requires every input to agree on both.
24
+ */
25
+
26
+ import { heightAtSample, normalAt, type TerrainField } from '@pascal-app/core'
27
+ import * as THREE from 'three'
28
+
29
+ /**
30
+ * How far past the field the ground keeps holding the player, in metres. Matches
31
+ * the flat box's half-extent so a site's collider bounds do not change size when
32
+ * it gains terrain.
33
+ */
34
+ const SKIRT_REACH = 1000
35
+
36
+ /**
37
+ * Cell budget before the collider decimates.
38
+ *
39
+ * 66k cells is a 257² field — the largest anything realistic produces, and ~131k
40
+ * triangles, which the BVH builds in well under a frame's worth of budget. The
41
+ * schema allows up to 4097² (a decode-bomb guard, not a real terrain); at that
42
+ * size an exact collider would be 100M triangles, so past the budget this steps
43
+ * the sample grid and the collider becomes a faithful-but-coarser version of the
44
+ * same surface rather than an out-of-memory crash.
45
+ */
46
+ const MAX_COLLIDER_CELLS = 66_000
47
+
48
+ function colliderStride(field: TerrainField): number {
49
+ const cells = Math.max(1, (field.cols - 1) * (field.rows - 1))
50
+ if (cells <= MAX_COLLIDER_CELLS) return 1
51
+ return Math.ceil(Math.sqrt(cells / MAX_COLLIDER_CELLS))
52
+ }
53
+
54
+ /** The sample indices the collider walks: every `stride`-th, always including the border. */
55
+ function sampleLine(count: number, stride: number): number[] {
56
+ const line: number[] = []
57
+ for (let i = 0; i < count - 1; i += stride) line.push(i)
58
+ line.push(count - 1)
59
+ return line
60
+ }
61
+
62
+ type Vertex = { x: number; y: number; z: number }
63
+
64
+ class TriangleSink {
65
+ private readonly positions: number[] = []
66
+ private readonly normals: number[] = []
67
+
68
+ constructor(private readonly field: TerrainField) {}
69
+
70
+ private push(v: Vertex): void {
71
+ this.positions.push(v.x, v.y, v.z)
72
+ const [nx, ny, nz] = normalAt(this.field, v.x, v.z)
73
+ this.normals.push(nx, ny, nz)
74
+ }
75
+
76
+ /**
77
+ * One cell, given its four corners named by axis order. Winding matches
78
+ * `buildTerrainMesh`: `(p00, p01, p10)` and `(p10, p01, p11)` faces +Y, which is
79
+ * what the spawn resolver's `normal.dot(UP) > 0.2` floor test needs.
80
+ */
81
+ quad(p00: Vertex, p10: Vertex, p01: Vertex, p11: Vertex): void {
82
+ this.push(p00)
83
+ this.push(p01)
84
+ this.push(p10)
85
+ this.push(p10)
86
+ this.push(p01)
87
+ this.push(p11)
88
+ }
89
+
90
+ build(): THREE.BufferGeometry | null {
91
+ if (this.positions.length === 0) return null
92
+ const geometry = new THREE.BufferGeometry()
93
+ geometry.setAttribute(
94
+ 'position',
95
+ new THREE.BufferAttribute(new Float32Array(this.positions), 3),
96
+ )
97
+ geometry.setAttribute('normal', new THREE.BufferAttribute(new Float32Array(this.normals), 3))
98
+ return geometry
99
+ }
100
+ }
101
+
102
+ /**
103
+ * Terrain collision geometry in the site's own frame — the caller applies the
104
+ * site's world matrix, exactly as it did for the flat box it replaces.
105
+ */
106
+ export function createTerrainColliderGeometry(field: TerrainField): THREE.BufferGeometry | null {
107
+ if (field.cols < 2 || field.rows < 2) return null
108
+
109
+ const stride = colliderStride(field)
110
+ const cols = sampleLine(field.cols, stride)
111
+ const rows = sampleLine(field.rows, stride)
112
+ const sink = new TriangleSink(field)
113
+
114
+ const xAt = (col: number) => field.origin[0] + col * field.spacing
115
+ const zAt = (row: number) => field.origin[1] + row * field.spacing
116
+ const at = (col: number, row: number): Vertex => ({
117
+ x: xAt(col),
118
+ y: heightAtSample(field, col, row),
119
+ z: zAt(row),
120
+ })
121
+
122
+ for (let ri = 0; ri < rows.length - 1; ri++) {
123
+ const r0 = rows[ri] as number
124
+ const r1 = rows[ri + 1] as number
125
+ for (let ci = 0; ci < cols.length - 1; ci++) {
126
+ const c0 = cols[ci] as number
127
+ const c1 = cols[ci + 1] as number
128
+ sink.quad(at(c0, r0), at(c1, r0), at(c0, r1), at(c1, r1))
129
+ }
130
+ }
131
+
132
+ // The skirt: each border sample extruded outward at its own height, so stepping
133
+ // off the field lands on the border height rather than on nothing. This is the
134
+ // collider spelling of `heightAt`'s edge clamp.
135
+ const firstCol = 0
136
+ const lastCol = field.cols - 1
137
+ const firstRow = 0
138
+ const lastRow = field.rows - 1
139
+ const westX = xAt(firstCol) - SKIRT_REACH
140
+ const eastX = xAt(lastCol) + SKIRT_REACH
141
+ const northZ = zAt(firstRow) - SKIRT_REACH
142
+ const southZ = zAt(lastRow) + SKIRT_REACH
143
+
144
+ for (let ci = 0; ci < cols.length - 1; ci++) {
145
+ const c0 = cols[ci] as number
146
+ const c1 = cols[ci + 1] as number
147
+ const north0 = at(c0, firstRow)
148
+ const north1 = at(c1, firstRow)
149
+ sink.quad(
150
+ { x: north0.x, y: north0.y, z: northZ },
151
+ { x: north1.x, y: north1.y, z: northZ },
152
+ north0,
153
+ north1,
154
+ )
155
+ const south0 = at(c0, lastRow)
156
+ const south1 = at(c1, lastRow)
157
+ sink.quad(
158
+ south0,
159
+ south1,
160
+ { x: south0.x, y: south0.y, z: southZ },
161
+ {
162
+ x: south1.x,
163
+ y: south1.y,
164
+ z: southZ,
165
+ },
166
+ )
167
+ }
168
+
169
+ for (let ri = 0; ri < rows.length - 1; ri++) {
170
+ const r0 = rows[ri] as number
171
+ const r1 = rows[ri + 1] as number
172
+ const west0 = at(firstCol, r0)
173
+ const west1 = at(firstCol, r1)
174
+ sink.quad(
175
+ { x: westX, y: west0.y, z: west0.z },
176
+ west0,
177
+ { x: westX, y: west1.y, z: west1.z },
178
+ west1,
179
+ )
180
+ const east0 = at(lastCol, r0)
181
+ const east1 = at(lastCol, r1)
182
+ sink.quad(east0, { x: eastX, y: east0.y, z: east0.z }, east1, {
183
+ x: eastX,
184
+ y: east1.y,
185
+ z: east1.z,
186
+ })
187
+ }
188
+
189
+ // Corner quads, flat at the corner sample's height — the diagonal region no edge
190
+ // strip covers, and where `heightAt` also reports the corner value.
191
+ const corners: Array<[Vertex, number, number]> = [
192
+ [at(firstCol, firstRow), westX, northZ],
193
+ [at(lastCol, firstRow), eastX, northZ],
194
+ [at(firstCol, lastRow), westX, southZ],
195
+ [at(lastCol, lastRow), eastX, southZ],
196
+ ]
197
+ for (const [corner, farX, farZ] of corners) {
198
+ const minX = Math.min(corner.x, farX)
199
+ const maxX = Math.max(corner.x, farX)
200
+ const minZ = Math.min(corner.z, farZ)
201
+ const maxZ = Math.max(corner.z, farZ)
202
+ const y = corner.y
203
+ sink.quad(
204
+ { x: minX, y, z: minZ },
205
+ { x: maxX, y, z: minZ },
206
+ { x: minX, y, z: maxZ },
207
+ { x: maxX, y, z: maxZ },
208
+ )
209
+ }
210
+
211
+ return sink.build()
212
+ }