mirror of
https://github.com/IfcOpenShell/IfcOpenShell.git
synced 2026-08-11 02:02:22 +00:00
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:
@@ -26,6 +26,7 @@
|
||||
#include <cstdint>
|
||||
#include <limits>
|
||||
#include <string>
|
||||
#include <unordered_map>
|
||||
#include <vector>
|
||||
|
||||
#include "InstancedGeometry.h"
|
||||
@@ -280,6 +281,18 @@ struct WgpuModelGpuData {
|
||||
std::vector<MeshInfo> meshes;
|
||||
std::vector<InstanceCpu> instances;
|
||||
|
||||
// Local-frame volume (m³) of every mesh, indexed by mesh_id. Computed
|
||||
// once at applyCachedModel via signed-tetrahedra-from-origin on the
|
||||
// raw vertex+index data; reused by the Volume measurement tool to
|
||||
// avoid re-reading the GPU buffers per click. Empty in streaming mode
|
||||
// until the chunk holding the mesh has been delivered.
|
||||
std::vector<double> mesh_local_volumes;
|
||||
|
||||
// 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.
|
||||
std::unordered_map<uint32_t, uint32_t> object_id_to_instance;
|
||||
|
||||
// Spatial chunk-cull replaced the per-model BVH walk — chunks are
|
||||
// already a one-level spatial partition of the instances, so a
|
||||
// single frustum test per chunk gives the same wholesale-reject
|
||||
|
||||
@@ -23,6 +23,7 @@
|
||||
#include <QFontMetrics>
|
||||
#include <QImage>
|
||||
#include <QPainter>
|
||||
#include <QSet>
|
||||
#include <QStringList>
|
||||
#include <QtMath>
|
||||
|
||||
@@ -1930,28 +1931,44 @@ void WgpuOverlayRenderer::encodeLabels(WGPUCommandEncoder enc,
|
||||
WGPUTextureView surface_view,
|
||||
const WgpuOverlayFrame& f) {
|
||||
if (!label_pipeline_ || !surface_view) return;
|
||||
if (labels_.empty() && hud_text_.isEmpty()) return;
|
||||
if (labels_.empty() && hud_text_.isEmpty()) {
|
||||
// Selection cleared / tool exited — drop the cache so we don't
|
||||
// pin GPU memory for unreferenced strings until destroy().
|
||||
releaseLabelTextures();
|
||||
return;
|
||||
}
|
||||
if (f.viewport_w_px <= 0 || f.viewport_h_px <= 0) return;
|
||||
|
||||
const int dpr = std::max(1, f.device_pixel_ratio);
|
||||
const float w_phys = float(f.viewport_w_px);
|
||||
const float h_phys = float(f.viewport_h_px);
|
||||
|
||||
// Resolve each label/HUD to (texture, NDC quad), expanding into the
|
||||
// per-frame vertex buffer.
|
||||
struct DrawRec { LabelTexture* tex; uint32_t first_vertex; };
|
||||
// Track which cache keys we touched this frame so we can evict
|
||||
// stale entries afterwards — measure tools push a new string per
|
||||
// object on every selection mutation, so without pruning the cache
|
||||
// grows by O(selections_seen) over the session.
|
||||
QSet<QString> used_keys;
|
||||
|
||||
// Resolve each label/HUD to (bind_group, NDC quad), expanding into
|
||||
// the per-frame vertex buffer. Store the WGPUBindGroup handle by
|
||||
// value — getOrCreateLabelTexture may insert into label_tex_cache_
|
||||
// and trigger a QHash rehash, which invalidates any LabelTexture*
|
||||
// captured from earlier iterations. The handle itself is a stable
|
||||
// wgpu-side pointer that stays valid as long as the cache holds it,
|
||||
// and we don't evict mid-encode.
|
||||
struct DrawRec { WGPUBindGroup bind_group; uint32_t first_vertex; };
|
||||
std::vector<DrawRec> draws;
|
||||
std::vector<float> verts;
|
||||
draws.reserve(labels_.size() + 1);
|
||||
verts.reserve((labels_.size() + 1) * 6 * 4);
|
||||
|
||||
auto push_quad = [&](LabelTexture* tex, float nx0, float ny0,
|
||||
auto push_quad = [&](WGPUBindGroup bg, float nx0, float ny0,
|
||||
float nx1, float ny1) {
|
||||
// Two triangles, top-left at (nx0, ny0) (NDC Y up).
|
||||
// UV layout: (0,0) at top-left of image → flip Y because NDC Y
|
||||
// increases upward but image V increases downward.
|
||||
const float u0 = 0.0f, u1 = 1.0f, v0 = 0.0f, v1 = 1.0f;
|
||||
draws.push_back({tex, uint32_t(verts.size() / 4)});
|
||||
draws.push_back({bg, uint32_t(verts.size() / 4)});
|
||||
const float quad[24] = {
|
||||
nx0, ny0, u0, v0, nx1, ny0, u1, v0, nx0, ny1, u0, v1,
|
||||
nx0, ny1, u0, v1, nx1, ny0, u1, v0, nx1, ny1, u1, v1,
|
||||
@@ -1975,8 +1992,12 @@ void WgpuOverlayRenderer::encodeLabels(WGPUCommandEncoder enc,
|
||||
const float sx_phys = (ndc_x * 0.5f + 0.5f) * w_phys;
|
||||
const float sy_phys = (1.0f - (ndc_y * 0.5f + 0.5f)) * h_phys;
|
||||
const QString key = QStringLiteral("L9:") + lbl.text;
|
||||
used_keys.insert(key);
|
||||
LabelTexture* tex = getOrCreateLabelTexture(key, lbl.text, 9, dpr);
|
||||
if (!tex) continue;
|
||||
// Snapshot the fields we need NOW — `tex` may be invalidated by
|
||||
// the next getOrCreateLabelTexture call's hash rehash.
|
||||
const WGPUBindGroup bg = tex->bind_group;
|
||||
const float wq = float(tex->width_px);
|
||||
const float hq = float(tex->height_px);
|
||||
const float lx_phys = sx_phys - wq * 0.5f;
|
||||
@@ -1985,14 +2006,16 @@ void WgpuOverlayRenderer::encodeLabels(WGPUCommandEncoder enc,
|
||||
const float nx1 = ((lx_phys + wq) / w_phys) * 2.0f - 1.0f;
|
||||
const float ny0 = 1.0f - 2.0f * ly_phys / h_phys; // top
|
||||
const float ny1 = 1.0f - 2.0f * (ly_phys + hq) / h_phys; // bottom
|
||||
push_quad(tex, nx0, ny0, nx1, ny1);
|
||||
push_quad(bg, nx0, ny0, nx1, ny1);
|
||||
}
|
||||
|
||||
// HUD: top-left, point size 11 (matches GL OverlayRenderer).
|
||||
if (!hud_text_.isEmpty()) {
|
||||
const QString key = QStringLiteral("H11:") + hud_text_;
|
||||
used_keys.insert(key);
|
||||
LabelTexture* tex = getOrCreateLabelTexture(key, hud_text_, 11, dpr);
|
||||
if (tex) {
|
||||
const WGPUBindGroup bg = tex->bind_group;
|
||||
const float margin_phys = 12.0f * float(dpr);
|
||||
const float lx_phys = margin_phys;
|
||||
const float ly_phys = margin_phys;
|
||||
@@ -2002,7 +2025,7 @@ void WgpuOverlayRenderer::encodeLabels(WGPUCommandEncoder enc,
|
||||
const float nx1 = ((lx_phys + wq) / w_phys) * 2.0f - 1.0f;
|
||||
const float ny0 = 1.0f - 2.0f * ly_phys / h_phys;
|
||||
const float ny1 = 1.0f - 2.0f * (ly_phys + hq) / h_phys;
|
||||
push_quad(tex, nx0, ny0, nx1, ny1);
|
||||
push_quad(bg, nx0, ny0, nx1, ny1);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2040,9 +2063,26 @@ void WgpuOverlayRenderer::encodeLabels(WGPUCommandEncoder enc,
|
||||
wgpuRenderPassEncoderSetVertexBuffer(pass, 0, label_vertex_buffer_,
|
||||
0, WGPU_WHOLE_SIZE);
|
||||
for (const auto& d : draws) {
|
||||
wgpuRenderPassEncoderSetBindGroup(pass, 0, d.tex->bind_group, 0, nullptr);
|
||||
wgpuRenderPassEncoderSetBindGroup(pass, 0, d.bind_group, 0, nullptr);
|
||||
wgpuRenderPassEncoderDraw(pass, 6, 1, d.first_vertex, 0);
|
||||
}
|
||||
wgpuRenderPassEncoderEnd(pass);
|
||||
wgpuRenderPassEncoderRelease(pass);
|
||||
|
||||
// Evict cache entries that weren't referenced this frame. Measure
|
||||
// tools push a fresh string per object on every selection mutation,
|
||||
// so without this the cache grows by O(unique strings seen) for
|
||||
// the session.
|
||||
if (label_tex_cache_.size() > used_keys.size()) {
|
||||
for (auto it = label_tex_cache_.begin(); it != label_tex_cache_.end(); ) {
|
||||
if (used_keys.contains(it.key())) {
|
||||
++it;
|
||||
continue;
|
||||
}
|
||||
if (it.value().bind_group) wgpuBindGroupRelease(it.value().bind_group);
|
||||
if (it.value().view) wgpuTextureViewRelease(it.value().view);
|
||||
if (it.value().texture) wgpuTextureRelease(it.value().texture);
|
||||
it = label_tex_cache_.erase(it);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -91,6 +91,13 @@ static constexpr uint64_t WGPU_BYTES_PER_ROW_ALIGN = 256;
|
||||
static QVector3D orbitEye(const float target[3], float dist,
|
||||
float yaw_deg, float pitch_deg);
|
||||
|
||||
// Forward declaration — defined alongside the Volume tool. Called from
|
||||
// both applyCachedModel (full load) and applyStreamedChunk (per-chunk
|
||||
// fill in streaming mode) so the same quantised-bytes path runs in both.
|
||||
static double computeMeshLocalVolumeQuantised(
|
||||
const MeshInfo& mesh,
|
||||
const uint8_t* vbase, const uint32_t* ibase, uint32_t n_indices);
|
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
// Small helpers
|
||||
// -----------------------------------------------------------------------------
|
||||
@@ -930,6 +937,20 @@ void WgpuViewportWindow::applyCachedModelStreaming(uint32_t model_id,
|
||||
m.meshes = std::move(metadata.meta.meshes);
|
||||
m.instances = std::move(metadata.meta.instances);
|
||||
|
||||
// Streaming defers per-mesh vertex data until the owning chunk is
|
||||
// loaded, so mesh-local volumes can't be precomputed here. Volume
|
||||
// tool returns 0 for unloaded meshes; once we add lazy per-chunk
|
||||
// volume computation this assign() becomes the seed.
|
||||
m.mesh_local_volumes.assign(m.meshes.size(), 0.0);
|
||||
|
||||
// object_id → instance index lookup. Volume tool reads it on every
|
||||
// selection mutation; per-pick latency stays O(K) instead of O(K*N).
|
||||
m.object_id_to_instance.clear();
|
||||
m.object_id_to_instance.reserve(m.instances.size());
|
||||
for (uint32_t i = 0; i < uint32_t(m.instances.size()); ++i) {
|
||||
m.object_id_to_instance.emplace(m.instances[i].object_id, i);
|
||||
}
|
||||
|
||||
// Compute per-chunk world AABBs + instance-id lists from the
|
||||
// instance_to_chunk mapping. Under spatial bucketing this captures
|
||||
// each bucket's actual instance extent; under mesh-keyed it's
|
||||
@@ -1283,6 +1304,29 @@ void WgpuViewportWindow::applyCachedModel(uint32_t model_id, SidecarData data) {
|
||||
m.meshes = std::move(data.meshes);
|
||||
m.instances = std::move(data.instances);
|
||||
|
||||
// Mesh-local volumes (m³). Computed once per mesh by signed-tetrahedra-
|
||||
// from-origin over the LOD0 triangles; the Volume measurement tool
|
||||
// later just multiplies by |det(placement_3x3)| per instance. Helper
|
||||
// works on raw quantised bytes so the streaming path can reuse it.
|
||||
m.mesh_local_volumes.assign(m.meshes.size(), 0.0);
|
||||
for (size_t mi = 0; mi < m.meshes.size(); ++mi) {
|
||||
const MeshInfo& mesh = m.meshes[mi];
|
||||
if (mesh.vertex_count == 0 || mesh.index_count < 3) continue;
|
||||
const uint8_t* vbase = data.vertices.data() + mesh.vbo_byte_offset;
|
||||
const uint32_t* ibase = data.indices.data()
|
||||
+ (mesh.ebo_byte_offset / sizeof(uint32_t));
|
||||
m.mesh_local_volumes[mi] = computeMeshLocalVolumeQuantised(
|
||||
mesh, vbase, ibase, mesh.index_count);
|
||||
}
|
||||
|
||||
// object_id → instance index lookup. Volume tool reads it on every
|
||||
// selection mutation; per-pick latency stays O(K) instead of O(K*N).
|
||||
m.object_id_to_instance.clear();
|
||||
m.object_id_to_instance.reserve(m.instances.size());
|
||||
for (uint32_t i = 0; i < uint32_t(m.instances.size()); ++i) {
|
||||
m.object_id_to_instance.emplace(m.instances[i].object_id, i);
|
||||
}
|
||||
|
||||
// Per-chunk world AABB + instance-id list. Same logic as the
|
||||
// streaming path. Lets cull frustum-test each chunk's AABB once
|
||||
// and skip every instance inside in one shot when the chunk is
|
||||
@@ -2860,6 +2904,181 @@ void WgpuViewportWindow::setHudText(const QString& text) {
|
||||
if (isExposed()) requestUpdate();
|
||||
}
|
||||
|
||||
// |det(upper-left 3×3)| of a column-major 4×4 placement. Picks up
|
||||
// mapped-item scale / mirror so a uniformly-scaled clone of a 1 m³ mesh
|
||||
// reports its actual volume.
|
||||
static double det3OfPlacement(const double M[16]) {
|
||||
const double m00 = M[0], m10 = M[1], m20 = M[2];
|
||||
const double m01 = M[4], m11 = M[5], m21 = M[6];
|
||||
const double m02 = M[8], m12 = M[9], m22 = M[10];
|
||||
return m00 * (m11 * m22 - m12 * m21)
|
||||
- m01 * (m10 * m22 - m12 * m20)
|
||||
+ m02 * (m10 * m21 - m11 * m20);
|
||||
}
|
||||
|
||||
// Local-frame volume of a mesh from its raw quantised vertex+index bytes.
|
||||
// `vbase` points at the first vertex (12 B/vertex, 3×uint16 pos quantised
|
||||
// against mesh.local_aabb), `ibase` at the first u32 index in mesh-local
|
||||
// numbering, `n_indices` is the LOD0 index count. Signed-tetrahedra-
|
||||
// from-origin → |sum|/6 so winding doesn't matter. Same algorithm as
|
||||
// Bonsai's meshLocalVolume; takes the dequant step from
|
||||
// INSTANCED_VERTEX_STRIDE_BYTES layout.
|
||||
static double computeMeshLocalVolumeQuantised(
|
||||
const MeshInfo& mesh,
|
||||
const uint8_t* vbase, const uint32_t* ibase, uint32_t n_indices) {
|
||||
if (n_indices < 3 || vbase == nullptr || ibase == nullptr) return 0.0;
|
||||
const float ax = mesh.local_aabb_min[0];
|
||||
const float ay = mesh.local_aabb_min[1];
|
||||
const float az = mesh.local_aabb_min[2];
|
||||
const float ex = mesh.local_aabb_max[0] - ax;
|
||||
const float ey = mesh.local_aabb_max[1] - ay;
|
||||
const float ez = mesh.local_aabb_max[2] - az;
|
||||
const float inv_q = 1.0f / 65535.0f;
|
||||
auto dequant = [&](uint32_t vi, double out[3]) {
|
||||
const uint8_t* v = vbase + size_t(vi) * INSTANCED_VERTEX_STRIDE_BYTES;
|
||||
uint16_t qx, qy, qz;
|
||||
std::memcpy(&qx, v + 0, 2);
|
||||
std::memcpy(&qy, v + 2, 2);
|
||||
std::memcpy(&qz, v + 4, 2);
|
||||
out[0] = double(ax + float(qx) * inv_q * ex);
|
||||
out[1] = double(ay + float(qy) * inv_q * ey);
|
||||
out[2] = double(az + float(qz) * inv_q * ez);
|
||||
};
|
||||
double sum = 0.0;
|
||||
for (uint32_t i = 0; i + 2 < n_indices; i += 3) {
|
||||
double p0[3], p1[3], p2[3];
|
||||
dequant(ibase[i + 0], p0);
|
||||
dequant(ibase[i + 1], p1);
|
||||
dequant(ibase[i + 2], p2);
|
||||
const double cx = p1[1] * p2[2] - p1[2] * p2[1];
|
||||
const double cy = p1[2] * p2[0] - p1[0] * p2[2];
|
||||
const double cz = p1[0] * p2[1] - p1[1] * p2[0];
|
||||
sum += p0[0] * cx + p0[1] * cy + p0[2] * cz;
|
||||
}
|
||||
return std::abs(sum) / 6.0;
|
||||
}
|
||||
|
||||
void WgpuViewportWindow::setToolMode(ToolMode m) {
|
||||
if (tool_mode_ == m) return;
|
||||
tool_mode_ = m;
|
||||
switch (tool_mode_) {
|
||||
case ToolMode::NoTool:
|
||||
// Drop any HUD/labels the previous tool left behind. We don't
|
||||
// own the GL backend's per-tool clear callbacks, so the tool's
|
||||
// own state lives in the overlay renderer.
|
||||
overlays_.setHudText(QString());
|
||||
overlays_.setOverlayLabels({});
|
||||
qInfo() << "[wgpu measure] tool off";
|
||||
break;
|
||||
case ToolMode::Volume:
|
||||
qInfo() << "[wgpu measure] volume tool — pick / marquee objects, Esc to exit";
|
||||
updateVolumeReadout();
|
||||
break;
|
||||
}
|
||||
if (isExposed()) requestUpdate();
|
||||
}
|
||||
|
||||
double WgpuViewportWindow::volumeOfObjects(
|
||||
const std::vector<uint32_t>& object_ids) const {
|
||||
if (object_ids.empty()) return 0.0;
|
||||
double total = 0.0;
|
||||
for (uint32_t oid : object_ids) {
|
||||
for (const auto& [mid, m] : models_gpu_) {
|
||||
auto it = m.object_id_to_instance.find(oid);
|
||||
if (it == m.object_id_to_instance.end()) continue;
|
||||
const InstanceCpu& inst = m.instances[it->second];
|
||||
if (inst.mesh_id >= m.mesh_local_volumes.size()) break;
|
||||
const double v_local = m.mesh_local_volumes[inst.mesh_id];
|
||||
const double det = std::abs(det3OfPlacement(inst.placement_transformation));
|
||||
total += v_local * det;
|
||||
break; // object_id is globally unique → at most one hit
|
||||
}
|
||||
}
|
||||
return total;
|
||||
}
|
||||
|
||||
std::vector<std::pair<uint32_t, double>>
|
||||
WgpuViewportWindow::volumesPerObject(
|
||||
const std::vector<uint32_t>& object_ids) const {
|
||||
std::vector<std::pair<uint32_t, double>> out;
|
||||
if (object_ids.empty()) return out;
|
||||
out.reserve(object_ids.size());
|
||||
for (uint32_t oid : object_ids) {
|
||||
for (const auto& [mid, m] : models_gpu_) {
|
||||
auto it = m.object_id_to_instance.find(oid);
|
||||
if (it == m.object_id_to_instance.end()) continue;
|
||||
const InstanceCpu& inst = m.instances[it->second];
|
||||
if (inst.mesh_id >= m.mesh_local_volumes.size()) break;
|
||||
const double v_local = m.mesh_local_volumes[inst.mesh_id];
|
||||
const double det = std::abs(det3OfPlacement(inst.placement_transformation));
|
||||
out.emplace_back(oid, v_local * det);
|
||||
break;
|
||||
}
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
void WgpuViewportWindow::updateVolumeReadout() {
|
||||
if (tool_mode_ != ToolMode::Volume) return;
|
||||
|
||||
const auto& sel = selection_.ids();
|
||||
if (sel.empty()) {
|
||||
overlays_.setHudText(QString());
|
||||
overlays_.setOverlayLabels({});
|
||||
return;
|
||||
}
|
||||
|
||||
const std::vector<uint32_t> ids(sel.begin(), sel.end());
|
||||
const auto per_obj = volumesPerObject(ids);
|
||||
|
||||
// Per-object label cap. Each label allocates one wgpu texture +
|
||||
// bind group on first sight; rendering thousands of unique
|
||||
// "X.XXXX m³" strings drives the label-texture cache off a cliff
|
||||
// and the QPainter rasterise per label dominates the click cost.
|
||||
// The HUD total stays correct above the cap — only the per-object
|
||||
// overlay labels are suppressed. 200 fits a normal multi-object
|
||||
// selection and keeps both memory and per-frame draw count bounded.
|
||||
static constexpr size_t kMaxPerObjectLabels = 200;
|
||||
const bool show_labels = per_obj.size() <= kMaxPerObjectLabels;
|
||||
|
||||
double total = 0.0;
|
||||
std::vector<WgpuOverlayRenderer::Label> labels;
|
||||
if (show_labels) labels.reserve(per_obj.size());
|
||||
for (const auto& [oid, v] : per_obj) {
|
||||
total += v;
|
||||
if (!show_labels) continue;
|
||||
// O(1) instance lookup via object_id_to_instance, then read the
|
||||
// world AABB from the cached InstanceCpu directly — same data
|
||||
// computeObjectAabb's linear scan would have produced for the
|
||||
// first matching instance. For label placement at the AABB
|
||||
// centre this is identical-looking; only the rare multi-
|
||||
// representation object_id sees a slightly smaller union.
|
||||
for (const auto& [mid, m] : models_gpu_) {
|
||||
auto it = m.object_id_to_instance.find(oid);
|
||||
if (it == m.object_id_to_instance.end()) continue;
|
||||
const InstanceCpu& inst = m.instances[it->second];
|
||||
WgpuOverlayRenderer::Label lbl;
|
||||
lbl.world_pos[0] = (inst.world_aabb_min[0] + inst.world_aabb_max[0]) * 0.5f;
|
||||
lbl.world_pos[1] = (inst.world_aabb_min[1] + inst.world_aabb_max[1]) * 0.5f;
|
||||
lbl.world_pos[2] = (inst.world_aabb_min[2] + inst.world_aabb_max[2]) * 0.5f;
|
||||
lbl.text = QString::number(v, 'f', 4) + QStringLiteral(" m³");
|
||||
labels.push_back(std::move(lbl));
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
QString hud = QStringLiteral("Volume: %1 m³ (%2 object%3)")
|
||||
.arg(total, 0, 'f', 4)
|
||||
.arg(per_obj.size())
|
||||
.arg(per_obj.size() == 1 ? "" : "s");
|
||||
if (!show_labels) {
|
||||
hud += QStringLiteral("\n(per-object labels hidden above %1)")
|
||||
.arg(kMaxPerObjectLabels);
|
||||
}
|
||||
overlays_.setHudText(hud);
|
||||
overlays_.setOverlayLabels(labels);
|
||||
}
|
||||
|
||||
// Project a world point to LOGICAL pixel coords (Qt's mouse-event units).
|
||||
// Returns false if behind the camera.
|
||||
static bool projectWorldToLogicalScreen(const QMatrix4x4& vp,
|
||||
@@ -4925,6 +5144,39 @@ bool WgpuViewportWindow::applyStreamedChunk(
|
||||
c.is_resident = true;
|
||||
c.is_loading = false;
|
||||
c.loaded_frame_idx = streaming_frame_idx_;
|
||||
|
||||
// Mesh-local volumes for the meshes in this chunk. applyCachedModelStreaming
|
||||
// left them zero because the bytes weren't in memory yet; the first
|
||||
// chunk to deliver each mesh fills it in. Spatial-bucket mode may
|
||||
// re-enter for the same mesh from a different chunk — the != 0 guard
|
||||
// skips the redundant work. Indices are mesh-local (numbered against
|
||||
// the mesh's own vertex range), so vbase + ibase are per-mesh slices
|
||||
// into the chunk's freshly-arrived bytes.
|
||||
bool filled_volume = false;
|
||||
if (!m.mesh_local_volumes.empty() && !idx.empty()) {
|
||||
for (uint32_t mi : c.mesh_ids) {
|
||||
if (mi >= m.meshes.size() || mi >= m.mesh_local_volumes.size()) continue;
|
||||
if (m.mesh_local_volumes[mi] != 0.0) continue;
|
||||
const MeshInfo& mesh = m.meshes[mi];
|
||||
if (mesh.vertex_count == 0 || mesh.index_count < 3) continue;
|
||||
const size_t v_off = size_t(m.mesh_chunk_local_base_vertex[mi])
|
||||
* INSTANCED_VERTEX_STRIDE_BYTES;
|
||||
const size_t i_off = m.mesh_chunk_local_ebo_first_u32[mi];
|
||||
const size_t v_end = v_off
|
||||
+ size_t(mesh.vertex_count) * INSTANCED_VERTEX_STRIDE_BYTES;
|
||||
if (v_end > vbytes.size()) continue;
|
||||
if (i_off + mesh.index_count > idx.size()) continue;
|
||||
m.mesh_local_volumes[mi] = computeMeshLocalVolumeQuantised(
|
||||
mesh, vbytes.data() + v_off, idx.data() + i_off, mesh.index_count);
|
||||
filled_volume = true;
|
||||
}
|
||||
}
|
||||
// If the user is staring at a Volume readout while chunks page in,
|
||||
// refresh as soon as a chunk delivers a mesh we just filled — they'd
|
||||
// otherwise see 0 m³ for the whole selection until they click again.
|
||||
if (filled_volume && tool_mode_ == ToolMode::Volume) {
|
||||
updateVolumeReadout();
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -6041,6 +6293,7 @@ void WgpuViewportWindow::mouseReleaseEvent(QMouseEvent* event) {
|
||||
}
|
||||
nav_active_button_ = Qt::NoButton;
|
||||
nav_drag_kind_ = NavDrag::Inactive;
|
||||
updateVolumeReadout();
|
||||
requestUpdate();
|
||||
return;
|
||||
}
|
||||
@@ -6149,6 +6402,7 @@ void WgpuViewportWindow::mouseReleaseEvent(QMouseEvent* event) {
|
||||
tracked_object_id_ = 0;
|
||||
tracked_chunk_idx_ = SIZE_MAX;
|
||||
}
|
||||
updateVolumeReadout();
|
||||
requestUpdate();
|
||||
}
|
||||
nav_active_button_ = Qt::NoButton;
|
||||
@@ -6389,6 +6643,19 @@ void WgpuViewportWindow::keyPressEvent(QKeyEvent* event) {
|
||||
}
|
||||
}
|
||||
|
||||
// Measurement tools. V toggles Volume; Esc exits whichever tool is
|
||||
// active. Mirrors GL ViewportWindow + Bonsai's bind_shortcut(V).
|
||||
if (key == Qt::Key_V && mods == Qt::NoModifier && !event->isAutoRepeat()) {
|
||||
setToolMode(tool_mode_ == ToolMode::Volume ? ToolMode::NoTool
|
||||
: ToolMode::Volume);
|
||||
return;
|
||||
}
|
||||
if (tool_mode_ != ToolMode::NoTool && key == Qt::Key_Escape
|
||||
&& !event->isAutoRepeat()) {
|
||||
setToolMode(ToolMode::NoTool);
|
||||
return;
|
||||
}
|
||||
|
||||
// GL-parity viewport hotkeys.
|
||||
if (key == Qt::Key_F && mods == Qt::NoModifier && !event->isAutoRepeat()) {
|
||||
focusOnSelectedObject();
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user