wgpu: area measurement tool (A hotkey, BFS coplanar patch + cyan highlight)

Ports Bonsai's AreaMeasurement onto WgpuViewportWindow as a new
WgpuAreaMeasurement class. Each LMB pick resolves to (instance,
triangle), BFS-expands the coplanar patch (dot(normal, seed_normal)
> 0.9999, ~0.81° tolerance), and toggles it in/out of the running set.
Alt+LMB skips BFS for single-triangle accumulate. Connected-components
sweep over the selected set produces one "X.XXXX m²" label per patch
at its area-weighted centroid in world space; HUD shows the running
total + triangle count.

Dependencies layered in:

- WgpuOverlayRenderer.setHighlightTriangles / encodeHighlightTriangles:
  translucent world-space triangle list (cyan @ 0.45 alpha), depth-
  tested but depth-write off so the corner gizmo + labels still sit
  on top.
- WgpuViewportWindow.pickMeshLocalAt: reuses pickSurfaceAt for the
  world hit, then inverts the instance's composed transform to express
  it in mesh-local space — what the BFS needs. Uses the live map key
  (`mid`) rather than InstanceCpu.model_id, which is whatever the GL
  streamer wrote at sidecar-write time and goes stale across sessions.
- WgpuViewportWindow.readbackMeshTriangles: CPU mesh shadow lookup.
  The shadow itself is populated during the same dequant pass that
  computes mesh-local volume — applyCachedModel for full loads and
  applyStreamedChunk for streaming, so the BFS has data the moment
  the user can pick it.

WgpuModelGpuData gains a MeshTriangles vector indexed by mesh_id;
doubles per-vertex CPU memory (12 B/vert) but skips wgpu mapAsync
plumbing for now. Bounds-check at pick time gracefully no-ops when a
stale sidecar field is out of range.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
Dion Moult
2026-05-31 22:12:26 +10:00
parent 0774398d4e
commit 8761f9ac46
7 changed files with 977 additions and 39 deletions
+18
View File
@@ -288,6 +288,24 @@ struct WgpuModelGpuData {
// until the chunk holding the mesh has been delivered.
std::vector<double> mesh_local_volumes;
// CPU shadow of each mesh's mesh-local positions + LOD0 indices.
// Populated at applyCachedModel (or per-chunk in streaming) from
// the same raw vertex bytes the volume calc dequantises. The Area
// measurement tool reads this directly — no GPU readback, matching
// the Volume tool's policy.
//
// Doubles per-vertex memory (12 B/vert GPU + 12 B/vert CPU). The
// alternative is a wgpu mapAsync readback per first-touched mesh,
// which adds async plumbing and a per-click stall; pay the memory
// upfront instead. Trim by sizing each entry down at population
// (reserve exact). For huge federations this can be a real
// working-set cost — revisit if it shows up in profiles.
struct MeshTriangles {
std::vector<float> positions; // 3 * vertex_count, mesh-local
std::vector<uint32_t> indices; // 3 * triangle_count, LOD0
};
std::vector<MeshTriangles> mesh_triangles_cache;
// object_id (globally rebased) → instance index in `instances`.
// Populated alongside the instance vector so the Volume tool can do
// O(1) instance lookup instead of linear-scanning every model.