mirror of
https://github.com/IfcOpenShell/IfcOpenShell.git
synced 2026-09-23 21:58:46 +00:00
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:
@@ -238,8 +238,17 @@ void MainWindow::setupUi() {
|
|||||||
viewport_->setHudText("Length tool: click first point");
|
viewport_->setHudText("Length tool: click first point");
|
||||||
status_label_->setText("Length tool: LMB add point, Backspace remove last, Esc exits");
|
status_label_->setText("Length tool: LMB add point, Backspace remove last, Esc exits");
|
||||||
break;
|
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:
|
case ViewportWindow::ToolMode::None:
|
||||||
viewport_->setHudText(QString());
|
viewport_->setHudText(QString());
|
||||||
|
viewport_->setOverlayLabels({});
|
||||||
status_label_->setText("Ready");
|
status_label_->setText("Ready");
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
@@ -332,6 +341,9 @@ void MainWindow::setupMenus() {
|
|||||||
view_menu->addAction("Measure &Length", this, [this]() {
|
view_menu->addAction("Measure &Length", this, [this]() {
|
||||||
viewport_->toggleLengthTool();
|
viewport_->toggleLengthTool();
|
||||||
}, QKeySequence("Ctrl+Shift+L"));
|
}, QKeySequence("Ctrl+Shift+L"));
|
||||||
|
view_menu->addAction("Measure &Volume", this, [this]() {
|
||||||
|
viewport_->toggleVolumeTool();
|
||||||
|
}, QKeySequence("Ctrl+Shift+V"));
|
||||||
view_menu->addSeparator();
|
view_menu->addSeparator();
|
||||||
view_menu->addAction("Set &Home View", this, &MainWindow::onSetHomeView);
|
view_menu->addAction("Set &Home View", this, &MainWindow::onSetHomeView);
|
||||||
view_menu->addAction("&Go to Home View", this, &MainWindow::onGoHomeView);
|
view_menu->addAction("&Go to Home View", this, &MainWindow::onGoHomeView);
|
||||||
@@ -929,15 +941,47 @@ void MainWindow::onObjectPicked(uint32_t object_id) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
populateProperties(object_id);
|
populateProperties(object_id);
|
||||||
|
updateVolumeReadout();
|
||||||
|
}
|
||||||
|
|
||||||
// Volume readout: report for the full selection so multi-select
|
void MainWindow::updateVolumeReadout() {
|
||||||
// matches the highlighted set.
|
// Volume readout only renders while the volume tool is active —
|
||||||
const auto& selection = viewport_->selection().selectionIds();
|
// matches Area/Length, which are also gated behind their own tool
|
||||||
if (!selection.empty()) {
|
// mode. Other modes own the HUD + overlay labels for their
|
||||||
std::vector<uint32_t> ids(selection.begin(), selection.end());
|
// lifetime, so we stay quiet here.
|
||||||
const double v = volumeOfObjects(*viewport_, ids);
|
if (viewport_->toolMode() != ViewportWindow::ToolMode::Volume) return;
|
||||||
qInfo("Volume of %zu selected object(s): %.6f m^3", ids.size(), v);
|
|
||||||
|
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) + " m³";
|
||||||
|
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() {
|
void MainWindow::onTreeSelectionChanged() {
|
||||||
|
|||||||
@@ -93,6 +93,10 @@ private:
|
|||||||
bool confirmDiscardIfDirty();
|
bool confirmDiscardIfDirty();
|
||||||
void updateWindowTitle();
|
void updateWindowTitle();
|
||||||
void populateProperties(uint32_t object_id);
|
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,
|
void appendElementToTree(uint32_t model_id,
|
||||||
uint32_t object_id,
|
uint32_t object_id,
|
||||||
int ifc_id,
|
int ifc_id,
|
||||||
|
|||||||
@@ -94,6 +94,41 @@ double volumeOfObjects(ViewportWindow& vp,
|
|||||||
return total;
|
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 {
|
namespace {
|
||||||
|
|
||||||
// edge_key: undirected edge between two mesh-local vertex indices.
|
// edge_key: undirected edge between two mesh-local vertex indices.
|
||||||
|
|||||||
@@ -39,6 +39,15 @@
|
|||||||
double volumeOfObjects(ViewportWindow& vp,
|
double volumeOfObjects(ViewportWindow& vp,
|
||||||
const std::vector<uint32_t>& object_ids);
|
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-accumulate area measurement. Each pick resolves the screen
|
||||||
// click to a (instance, triangle) using ViewportWindow's primitives,
|
// click to a (instance, triangle) using ViewportWindow's primitives,
|
||||||
// expands it into the connected coplanar patch (BFS over shared edges,
|
// expands it into the connected coplanar patch (BFS over shared edges,
|
||||||
|
|||||||
@@ -3596,8 +3596,11 @@ void ViewportWindow::handleMousePress(QMouseEvent* e) {
|
|||||||
// drag that happens to start on an object still box-selects, which
|
// drag that happens to start on an object still box-selects, which
|
||||||
// matches user intuition: the start point shouldn't disqualify the
|
// matches user intuition: the start point shouldn't disqualify the
|
||||||
// gesture. Tool-mode LMB defers pick handling to surfacePickedInTool
|
// gesture. Tool-mode LMB defers pick handling to surfacePickedInTool
|
||||||
// on release.
|
// on release for tools that consume clicks (Area / Length); Volume is
|
||||||
if (e->button() == Qt::LeftButton && tool_mode_ == ToolMode::None) {
|
// passive and routes input through normal selection.
|
||||||
|
const bool tool_consumes_clicks =
|
||||||
|
(tool_mode_ == ToolMode::Area || tool_mode_ == ToolMode::Length);
|
||||||
|
if (e->button() == Qt::LeftButton && !tool_consumes_clicks) {
|
||||||
press_pick_id_ = pickObjectAt(e->pos().x(), e->pos().y());
|
press_pick_id_ = pickObjectAt(e->pos().x(), e->pos().y());
|
||||||
box_select_start_pos_ = e->pos();
|
box_select_start_pos_ = e->pos();
|
||||||
box_select_current_pos_ = e->pos();
|
box_select_current_pos_ = e->pos();
|
||||||
@@ -3637,8 +3640,11 @@ void ViewportWindow::handleMouseRelease(QMouseEvent* e) {
|
|||||||
selection_.setSelection(picks, selection_.activeObjectId());
|
selection_.setSelection(picks, selection_.activeObjectId());
|
||||||
}
|
}
|
||||||
} else if (!was_drag) {
|
} else if (!was_drag) {
|
||||||
// Click — apply press-time pick + modifiers.
|
// Click — apply press-time pick + modifiers. Area / Length
|
||||||
if (tool_mode_ != ToolMode::None) {
|
// intercept clicks; None and Volume drive normal selection.
|
||||||
|
const bool tool_consumes_clicks =
|
||||||
|
(tool_mode_ == ToolMode::Area || tool_mode_ == ToolMode::Length);
|
||||||
|
if (tool_consumes_clicks) {
|
||||||
emit surfacePickedInTool(e->pos().x(), e->pos().y(),
|
emit surfacePickedInTool(e->pos().x(), e->pos().y(),
|
||||||
int(e->modifiers()));
|
int(e->modifiers()));
|
||||||
} else {
|
} else {
|
||||||
@@ -4225,6 +4231,10 @@ void ViewportWindow::toggleLengthTool() {
|
|||||||
setToolMode(tool_mode_ == ToolMode::Length ? ToolMode::None : ToolMode::Length);
|
setToolMode(tool_mode_ == ToolMode::Length ? ToolMode::None : ToolMode::Length);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void ViewportWindow::toggleVolumeTool() {
|
||||||
|
setToolMode(tool_mode_ == ToolMode::Volume ? ToolMode::None : ToolMode::Volume);
|
||||||
|
}
|
||||||
|
|
||||||
void ViewportWindow::setHighlightTriangles(const std::vector<float>& world_xyz,
|
void ViewportWindow::setHighlightTriangles(const std::vector<float>& world_xyz,
|
||||||
float r, float g, float b, float a) {
|
float r, float g, float b, float a) {
|
||||||
if (!gl_initialized_) return;
|
if (!gl_initialized_) return;
|
||||||
|
|||||||
@@ -255,11 +255,15 @@ public:
|
|||||||
// object selection); the app interprets them per-tool. Esc exits the
|
// object selection); the app interprets them per-tool. Esc exits the
|
||||||
// active tool. Backspace/Delete in length mode emits
|
// active tool. Backspace/Delete in length mode emits
|
||||||
// toolBackspacePressed for "remove last point" semantics.
|
// toolBackspacePressed for "remove last point" semantics.
|
||||||
enum class ToolMode { None, Area, Length };
|
// Area / Length consume LMB clicks via surfacePickedInTool; Volume
|
||||||
|
// is passive — selection behaves as in None and the host just gates
|
||||||
|
// its volume HUD/labels on this mode.
|
||||||
|
enum class ToolMode { None, Area, Length, Volume };
|
||||||
Q_ENUM(ToolMode)
|
Q_ENUM(ToolMode)
|
||||||
|
|
||||||
void toggleAreaTool();
|
void toggleAreaTool();
|
||||||
void toggleLengthTool();
|
void toggleLengthTool();
|
||||||
|
void toggleVolumeTool();
|
||||||
void setToolMode(ToolMode mode);
|
void setToolMode(ToolMode mode);
|
||||||
ToolMode toolMode() const { return tool_mode_; }
|
ToolMode toolMode() const { return tool_mode_; }
|
||||||
|
|
||||||
@@ -379,6 +383,10 @@ public:
|
|||||||
// Frame the union of all finalized models. No-op if the scene is empty.
|
// Frame the union of all finalized models. No-op if the scene is empty.
|
||||||
void viewAll();
|
void viewAll();
|
||||||
|
|
||||||
|
// World-space AABB query. Returns false when the object has no live
|
||||||
|
// instance or no mesh AABB yet (caller should treat as "unknown").
|
||||||
|
bool computeObjectAabb(uint32_t object_id, QVector3D& mn, QVector3D& mx) const;
|
||||||
|
|
||||||
struct CameraState {
|
struct CameraState {
|
||||||
QVector3D target;
|
QVector3D target;
|
||||||
float distance;
|
float distance;
|
||||||
@@ -466,10 +474,8 @@ private:
|
|||||||
void updateSectionDrag(int x, int y);
|
void updateSectionDrag(int x, int y);
|
||||||
void updateCamera();
|
void updateCamera();
|
||||||
|
|
||||||
// Geometry queries used by focusOnSelectedObject() / viewAll(). Both
|
// Scene-wide AABB used by viewAll(). Returns false when the scene
|
||||||
// return false when nothing matched (caller should leave the camera
|
// has no finalized geometry (caller should leave the camera alone).
|
||||||
// alone). Bounds are world-space AABBs.
|
|
||||||
bool computeObjectAabb(uint32_t object_id, QVector3D& mn, QVector3D& mx) const;
|
|
||||||
bool computeSceneAabb(QVector3D& mn, QVector3D& mx) const;
|
bool computeSceneAabb(QVector3D& mn, QVector3D& mx) const;
|
||||||
// Re-aim the orbit camera so the bounding sphere of [mn, mx] just fits
|
// Re-aim the orbit camera so the bounding sphere of [mn, mx] just fits
|
||||||
// vertically and horizontally within the current FOV, with `padding`
|
// vertically and horizontally within the current FOV, with `padding`
|
||||||
|
|||||||
Reference in New Issue
Block a user