wgpu: volume measurement tool (V hotkey, selection-driven HUD + per-object labels)

Ports Bonsai's volumeOfObjects + volumesPerObject onto WgpuViewportWindow.
Mesh-local volumes are precomputed at applyCachedModel via signed-
tetrahedra-from-origin (dequantising positions from the 12 B/vertex GPU
layout); per-instance volume is just the cached local × |det(placement)|.
No GPU readback — measurement is O(K) in the selection size.

Streaming path computes volumes per-chunk as they arrive — fills any
mesh whose chunk just delivered, then re-runs updateVolumeReadout if
the user is staring at a Volume readout while the geometry pages in.

UX matches GL: V toggles, Esc exits, selection-driven (LMB pick / marquee
/ Shift/Ctrl set ops all funnel into updateVolumeReadout). HUD shows
total + count; one overlay label per object at its AABB centre, capped
at 200 to keep the label-texture cache bounded on large marquees.

Side fixes layered on the label overlay:
- O(1) AABB lookup via object_id_to_instance instead of linear-scanning
  every model's instance list per selected object.
- Label texture cache evicts entries not touched this frame, so churning
  through "X.XXXX m³" strings doesn't pin GPU memory.
- DrawRec stores the WGPUBindGroup handle by value rather than a
  LabelTexture* pointer into the QHash — getOrCreateLabelTexture can
  rehash the table and invalidate every captured pointer, which crashed
  large marquee selections with BindGroup-no-longer-alive.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
Dion Moult
2026-05-31 20:57:44 +10:00
parent 3ac7a79b7a
commit 0774398d4e
4 changed files with 359 additions and 9 deletions
+30
View File
@@ -295,6 +295,27 @@ private:
void setOverlayLabels(const std::vector<WgpuOverlayRenderer::Label>& labels);
void setHudText(const QString& text);
// Measurement tools. Mirrors GL ViewportWindow::ToolMode. Volume is
// the first ported tool — selection-driven (LMB pick / marquee /
// Shift/Ctrl set ops drive the readout), no clicks-to-place. V
// toggles, Esc exits.
// NoTool (not None) because X11/X.h #define's None as 0L; including
// it transitively via Qt's xcb back-end breaks any enum named None.
enum class ToolMode { NoTool, Volume };
ToolMode toolMode() const { return tool_mode_; }
void setToolMode(ToolMode m);
// Sum of mesh-local volumes (m³) of every instance whose object_id
// is in `object_ids`. Each instance is scaled by |det(placement_3x3)|
// to pick up mapped-item scale/mirror; signed-tetrahedra absolute
// value means winding is ignored. Volumes are precomputed at
// applyCachedModel — this call is just lookups + multiplies.
double volumeOfObjects(const std::vector<uint32_t>& object_ids) const;
// Per-object variant. Used by the Volume tool to drive both the
// total HUD and the per-object overlay labels at AABB centres.
std::vector<std::pair<uint32_t, double>>
volumesPerObject(const std::vector<uint32_t>& object_ids) const;
void ensureHizTextures(int viewport_w, int viewport_h);
void releaseHizResources();
// Resolves the just-rendered MSAA depth into the small single-sample
@@ -469,6 +490,15 @@ private:
// encode each overlay; see WgpuOverlayRenderer.h.
WgpuOverlayRenderer overlays_;
// Active measurement tool. setToolMode() / setSelection mutations
// both funnel into updateVolumeReadout() which pushes the HUD +
// per-object labels into overlays_.
ToolMode tool_mode_ = ToolMode::NoTool;
// Recompute the volume HUD + per-object labels from the current
// selection. No-op unless tool_mode_ == Volume; on the first call
// after entering Volume mode this primes the overlay.
void updateVolumeReadout();
// Pick pass (stage 4). Single-sample R32UInt target + depth, vertex-
// pulled from the same visible_draws / instances buffers as the main
// pass — pick fragment outputs the instance's object_id. The pick