ifcviewer-full: volume tool with HUD + per-object labels

Mirrors the Area tool's display: HUD shows total volume + object count,
each selected object gets a label at its world-AABB centroid showing
its individual volume.  Gated behind ToolMode::Volume (Ctrl+Shift+V) so
it stays out of the way until invoked.

Volume is a passive tool — selection works as in None (multi-select,
modifier toggle, box-select all keep working).  Area / Length still
intercept clicks through surfacePickedInTool.

Adds volumesPerObject() reusing the same mesh-cached readback path as
volumeOfObjects, so the per-object split costs no extra GL readbacks.
computeObjectAabb is promoted to public for the centroid lookup.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
Dion Moult
2026-05-10 08:45:08 +10:00
parent bba2f30816
commit 0a297babea
6 changed files with 124 additions and 16 deletions
+51 -7
View File
@@ -238,8 +238,17 @@ void MainWindow::setupUi() {
viewport_->setHudText("Length tool: click first point");
status_label_->setText("Length tool: LMB add point, Backspace remove last, Esc exits");
break;
case ViewportWindow::ToolMode::Volume:
// Volume tool is passive — selection works as in None. The
// readout helper writes both HUD and per-object labels for
// whatever's currently selected, then keeps them in sync as
// the selection changes.
status_label_->setText("Volume tool: click / box-select objects, Esc exits");
updateVolumeReadout();
break;
case ViewportWindow::ToolMode::None:
viewport_->setHudText(QString());
viewport_->setOverlayLabels({});
status_label_->setText("Ready");
break;
}
@@ -332,6 +341,9 @@ void MainWindow::setupMenus() {
view_menu->addAction("Measure &Length", this, [this]() {
viewport_->toggleLengthTool();
}, QKeySequence("Ctrl+Shift+L"));
view_menu->addAction("Measure &Volume", this, [this]() {
viewport_->toggleVolumeTool();
}, QKeySequence("Ctrl+Shift+V"));
view_menu->addSeparator();
view_menu->addAction("Set &Home View", this, &MainWindow::onSetHomeView);
view_menu->addAction("&Go to Home View", this, &MainWindow::onGoHomeView);
@@ -929,15 +941,47 @@ void MainWindow::onObjectPicked(uint32_t object_id) {
}
populateProperties(object_id);
updateVolumeReadout();
}
// Volume readout: report for the full selection so multi-select
// matches the highlighted set.
const auto& selection = viewport_->selection().selectionIds();
if (!selection.empty()) {
std::vector<uint32_t> ids(selection.begin(), selection.end());
const double v = volumeOfObjects(*viewport_, ids);
qInfo("Volume of %zu selected object(s): %.6f m^3", ids.size(), v);
void MainWindow::updateVolumeReadout() {
// Volume readout only renders while the volume tool is active —
// matches Area/Length, which are also gated behind their own tool
// mode. Other modes own the HUD + overlay labels for their
// lifetime, so we stay quiet here.
if (viewport_->toolMode() != ViewportWindow::ToolMode::Volume) return;
const auto& sel = viewport_->selection().selectionIds();
if (sel.empty()) {
viewport_->setHudText(QString());
viewport_->setOverlayLabels({});
return;
}
std::vector<uint32_t> ids(sel.begin(), sel.end());
const auto per_obj = volumesPerObject(*viewport_, ids);
double total = 0.0;
std::vector<OverlayRenderer::Label> labels;
labels.reserve(per_obj.size());
for (const auto& [oid, v] : per_obj) {
total += v;
QVector3D mn, mx;
if (!viewport_->computeObjectAabb(oid, mn, mx)) continue;
OverlayRenderer::Label lbl;
const QVector3D c = (mn + mx) * 0.5f;
lbl.world_pos[0] = c.x();
lbl.world_pos[1] = c.y();
lbl.world_pos[2] = c.z();
lbl.text = QString::number(v, 'f', 4) + "";
labels.push_back(std::move(lbl));
}
viewport_->setHudText(QString("Volume: %1 m³ (%2 object%3)")
.arg(total, 0, 'f', 4)
.arg(per_obj.size())
.arg(per_obj.size() == 1 ? "" : "s"));
viewport_->setOverlayLabels(labels);
}
void MainWindow::onTreeSelectionChanged() {
+4
View File
@@ -93,6 +93,10 @@ private:
bool confirmDiscardIfDirty();
void updateWindowTitle();
void populateProperties(uint32_t object_id);
// Push the volume HUD + per-object volume labels for the current
// selection. No-op when a measurement tool is active — that tool
// owns the overlay state until it's exited.
void updateVolumeReadout();
void appendElementToTree(uint32_t model_id,
uint32_t object_id,
int ifc_id,
+35
View File
@@ -94,6 +94,41 @@ double volumeOfObjects(ViewportWindow& vp,
return total;
}
std::vector<std::pair<uint32_t, double>>
volumesPerObject(ViewportWindow& vp,
const std::vector<uint32_t>& object_ids) {
std::vector<std::pair<uint32_t, double>> out;
if (object_ids.empty()) return out;
out.reserve(object_ids.size());
// Cache the local-frame volume per unique (model_id, mesh_id) so each
// mesh is read back at most once even when many instances share it
// (common for repeated families like windows / columns).
std::unordered_map<uint64_t, double> mesh_vol_local;
mesh_vol_local.reserve(object_ids.size());
ViewportWindow::MeshTriangles tris;
for (uint32_t oid : object_ids) {
ViewportWindow::InstanceLookup lk;
if (!vp.findInstance(oid, lk)) continue;
const uint64_t key = (uint64_t(lk.model_id) << 32) | lk.mesh_id;
auto it = mesh_vol_local.find(key);
double v_local = 0.0;
if (it == mesh_vol_local.end()) {
if (vp.readbackMeshTriangles(lk.model_id, lk.mesh_id, tris)) {
v_local = meshLocalVolume(tris);
}
mesh_vol_local.emplace(key, v_local);
} else {
v_local = it->second;
}
const double det = std::abs(det3(lk.placement_transformation));
out.emplace_back(oid, v_local * det);
}
return out;
}
namespace {
// edge_key: undirected edge between two mesh-local vertex indices.
+9
View File
@@ -38,6 +38,15 @@
double volumeOfObjects(ViewportWindow& vp,
const std::vector<uint32_t>& object_ids);
// Per-object volumes (m³). Same algorithm as volumeOfObjects but
// attributed per id rather than summed. Skips ids that don't resolve
// to a live instance, so the result may be shorter than the input.
// Used by MainWindow's volume readout to drive both the total HUD and
// the per-object overlay labels.
std::vector<std::pair<uint32_t, double>>
volumesPerObject(ViewportWindow& vp,
const std::vector<uint32_t>& object_ids);
// Click-to-accumulate area measurement. Each pick resolves the screen
// click to a (instance, triangle) using ViewportWindow's primitives,
// expands it into the connected coplanar patch (BFS over shared edges,