From f2b655fcf61d7e68733f47a4bff3caf66154d27f Mon Sep 17 00:00:00 2001 From: Dion Moult Date: Thu, 7 May 2026 11:25:30 +1000 Subject: [PATCH] ifcviewer-full: print object volume on click via lazy GPU readback Adds neutral primitives on ViewportWindow (readbackMeshTriangles, findInstance) so consumers can compute per-object geometry queries without the library retaining a CPU triangle copy. Measurement.cpp in ifcviewer-full uses them to sum signed-tetrahedra in mesh-local space, weighted by |det(placement_3x3)| per instance for mapped-item scaling. Co-Authored-By: Claude Opus 4.7 --- src/ifcviewer-full/MainWindow.cpp | 6 ++ src/ifcviewer-full/Measurement.cpp | 89 ++++++++++++++++++++++++++++++ src/ifcviewer-full/Measurement.h | 38 +++++++++++++ src/ifcviewer/ViewportWindow.cpp | 62 +++++++++++++++++++++ src/ifcviewer/ViewportWindow.h | 25 +++++++++ 5 files changed, 220 insertions(+) create mode 100644 src/ifcviewer-full/Measurement.cpp create mode 100644 src/ifcviewer-full/Measurement.h diff --git a/src/ifcviewer-full/MainWindow.cpp b/src/ifcviewer-full/MainWindow.cpp index c1d1220b15..57e60282cc 100644 --- a/src/ifcviewer-full/MainWindow.cpp +++ b/src/ifcviewer-full/MainWindow.cpp @@ -21,6 +21,7 @@ #include "AppSettings.h" #include "Federation.h" #include "FederationSettingsDialog.h" +#include "Measurement.h" #include "ModelTransformationDialog.h" #include "SettingsWindow.h" #include "LodBuilder.h" @@ -871,6 +872,11 @@ void MainWindow::onObjectPicked(uint32_t object_id) { } populateProperties(object_id); + + if (object_id != 0) { + const double v = volumeOfObjects(*viewport_, {object_id}); + qInfo("Volume of object %u: %.6f m^3", object_id, v); + } } void MainWindow::onTreeSelectionChanged() { diff --git a/src/ifcviewer-full/Measurement.cpp b/src/ifcviewer-full/Measurement.cpp new file mode 100644 index 0000000000..1283fcd9d4 --- /dev/null +++ b/src/ifcviewer-full/Measurement.cpp @@ -0,0 +1,89 @@ +/******************************************************************************** + * * + * This file is part of IfcOpenShell. * + * * + * IfcOpenShell is free software: you can redistribute it and/or modify * + * it under the terms of the Lesser GNU General Public License as published by * + * the Free Software Foundation, either version 3.0 of the License, or * + * (at your option) any later version. * + * * + * IfcOpenShell is distributed in the hope that it will be useful, * + * but WITHOUT ANY WARRANTY; without even the implied warranty of * + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * + * Lesser GNU General Public License for more details. * + * * + * You should have received a copy of the Lesser GNU General Public License * + * along with this program. If not, see . * + * * + ********************************************************************************/ + +#include "Measurement.h" + +#include "ViewportWindow.h" + +#include +#include +#include +#include + +namespace { + +double meshLocalVolume(const ViewportWindow::MeshTriangles& tris) { + // Signed tetrahedra from the origin: V = sum( a · (b × c) ) / 6. + // Absolute value at the end so winding convention doesn't matter. + double sum = 0.0; + const size_t n = tris.indices.size(); + for (size_t i = 0; i + 2 < n; i += 3) { + const uint32_t ia = tris.indices[i + 0]; + const uint32_t ib = tris.indices[i + 1]; + const uint32_t ic = tris.indices[i + 2]; + const float* a = &tris.positions[3 * ia]; + const float* b = &tris.positions[3 * ib]; + const float* c = &tris.positions[3 * ic]; + const double cx = double(b[1]) * c[2] - double(b[2]) * c[1]; + const double cy = double(b[2]) * c[0] - double(b[0]) * c[2]; + const double cz = double(b[0]) * c[1] - double(b[1]) * c[0]; + sum += double(a[0]) * cx + double(a[1]) * cy + double(a[2]) * cz; + } + return std::abs(sum) / 6.0; +} + +double det3(const float M[16]) { + // Upper-left 3x3 of a column-major 4x4: M[col * 4 + row]. + 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); +} + +} // namespace + +double volumeOfObjects(ViewportWindow& vp, + const std::vector& object_ids) { + if (object_ids.empty()) return 0.0; + + // Group selected instances by (model_id, mesh_id) so each unique mesh + // is read back at most once per call. Each entry stores the |det| of + // every instance of that mesh in the request. + std::unordered_map> by_mesh; + by_mesh.reserve(object_ids.size()); + 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; + by_mesh[key].push_back(std::abs(det3(lk.placement_transformation))); + } + + double total = 0.0; + ViewportWindow::MeshTriangles tris; + for (const auto& [key, dets] : by_mesh) { + const uint32_t model_id = uint32_t(key >> 32); + const uint32_t mesh_id = uint32_t(key & 0xffffffffu); + if (!vp.readbackMeshTriangles(model_id, mesh_id, tris)) continue; + const double v = meshLocalVolume(tris); + for (double d : dets) total += v * d; + } + return total; +} diff --git a/src/ifcviewer-full/Measurement.h b/src/ifcviewer-full/Measurement.h new file mode 100644 index 0000000000..847fcd7d69 --- /dev/null +++ b/src/ifcviewer-full/Measurement.h @@ -0,0 +1,38 @@ +/******************************************************************************** + * * + * This file is part of IfcOpenShell. * + * * + * IfcOpenShell is free software: you can redistribute it and/or modify * + * it under the terms of the Lesser GNU General Public License as published by * + * the Free Software Foundation, either version 3.0 of the License, or * + * (at your option) any later version. * + * * + * IfcOpenShell is distributed in the hope that it will be useful, * + * but WITHOUT ANY WARRANTY; without even the implied warranty of * + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * + * Lesser GNU General Public License for more details. * + * * + * You should have received a copy of the Lesser GNU General Public License * + * along with this program. If not, see . * + * * + ********************************************************************************/ + +#ifndef IFCVIEWER_FULL_MEASUREMENT_H +#define IFCVIEWER_FULL_MEASUREMENT_H + +#include +#include + +class ViewportWindow; + +// Sum of mesh-local volumes (m³) of every instance whose object_id is in +// `object_ids`. Groups by (model, mesh) so each unique mesh is read back +// from the GPU at most once per call; instances of the same mesh are scaled +// by |det(placement_3x3)| to pick up mapped-item scale/mirror. Volume is +// taken as the absolute value of the signed-tetrahedra sum, so winding +// convention does not matter. Returns 0.0 for empty input or when nothing +// resolves. Recomputes from scratch on every call — no cache. +double volumeOfObjects(ViewportWindow& vp, + const std::vector& object_ids); + +#endif // IFCVIEWER_FULL_MEASUREMENT_H diff --git a/src/ifcviewer/ViewportWindow.cpp b/src/ifcviewer/ViewportWindow.cpp index 8330ce16a7..cd64ad2510 100644 --- a/src/ifcviewer/ViewportWindow.cpp +++ b/src/ifcviewer/ViewportWindow.cpp @@ -3747,3 +3747,65 @@ void ViewportWindow::printSelectedObjectCoords() { qInfo("printSelectedObjectCoords: object_id %u not found in any model", selected_object_id_); } + +bool ViewportWindow::readbackMeshTriangles(uint32_t model_id, uint32_t mesh_id, + MeshTriangles& out) { + out.positions.clear(); + out.indices.clear(); + if (!gl_initialized_) return false; + auto it = models_gpu_.find(model_id); + if (it == models_gpu_.end()) return false; + const ModelGpuData& m = it->second; + if (!m.finalized) return false; + if (mesh_id >= m.meshes.size()) return false; + const MeshInfo& mi = m.meshes[mesh_id]; + if (mi.vertex_count == 0 || mi.index_count == 0) return false; + + context_->makeCurrent(this); + + // Vertices: stride is INSTANCED_VERTEX_STRIDE_BYTES (12), positions are + // the first 6 bytes (3 x uint16) of each vertex. Read the full range + // and stride through it — simpler than chasing a position-only buffer. + std::vector raw(size_t(mi.vertex_count) * INSTANCED_VERTEX_STRIDE_BYTES); + gl_->glGetNamedBufferSubData(m.vbo, mi.vbo_byte_offset, + GLsizeiptr(raw.size()), raw.data()); + + out.positions.resize(size_t(mi.vertex_count) * 3); + const float ox = mi.local_aabb_min[0]; + const float oy = mi.local_aabb_min[1]; + const float oz = mi.local_aabb_min[2]; + const float sx = (mi.local_aabb_max[0] - mi.local_aabb_min[0]) / 65535.0f; + const float sy = (mi.local_aabb_max[1] - mi.local_aabb_min[1]) / 65535.0f; + const float sz = (mi.local_aabb_max[2] - mi.local_aabb_min[2]) / 65535.0f; + for (uint32_t i = 0; i < mi.vertex_count; ++i) { + const uint16_t* p = reinterpret_cast( + raw.data() + size_t(i) * INSTANCED_VERTEX_STRIDE_BYTES); + out.positions[3 * i + 0] = ox + sx * float(p[0]); + out.positions[3 * i + 1] = oy + sy * float(p[1]); + out.positions[3 * i + 2] = oz + sz * float(p[2]); + } + + // Indices: LOD0 only — measurements should use the full-resolution mesh. + out.indices.resize(mi.index_count); + gl_->glGetNamedBufferSubData(m.ebo, mi.ebo_byte_offset, + GLsizeiptr(mi.index_count * sizeof(uint32_t)), + out.indices.data()); + return true; +} + +bool ViewportWindow::findInstance(uint32_t object_id, InstanceLookup& out) const { + if (object_id == 0) return false; + for (const auto& kv : models_gpu_) { + const ModelGpuData& m = kv.second; + for (const InstanceCpu& inst : m.instances) { + if (inst.object_id != object_id) continue; + out.model_id = inst.model_id; + out.mesh_id = inst.mesh_id; + std::memcpy(out.placement_transformation, + inst.placement_transformation, + sizeof(out.placement_transformation)); + return true; + } + } + return false; +} diff --git a/src/ifcviewer/ViewportWindow.h b/src/ifcviewer/ViewportWindow.h index 74cfdb36ad..09f89a03b8 100644 --- a/src/ifcviewer/ViewportWindow.h +++ b/src/ifcviewer/ViewportWindow.h @@ -183,6 +183,31 @@ public: // (CoordinateOperation · placement_transformation), in metres. void printSelectedObjectCoords(); + // Mesh-local CPU triangles read back from the VBO/EBO. Positions are + // dequantised against the mesh's local AABB; units match the streamer + // (metres). Indices are mesh-local (0..vertex_count-1). + struct MeshTriangles { + std::vector positions; // 3 * vertex_count + std::vector indices; // 3 * triangle_count + }; + + // Lazy GPU readback of one mesh's triangles. Fails if model_id / + // mesh_id aren't live, the model isn't finalised, or GL isn't ready. + // Stalls the GL pipeline for the readback — call from the main thread + // and not from inside render(). + bool readbackMeshTriangles(uint32_t model_id, uint32_t mesh_id, + MeshTriangles& out); + + // Pure CPU lookup: object_id → owning model + mesh + raw streamer + // placement matrix (column-major, pre-CoordinateOperation / + // FederatedFalseOrigin / ModelTransformation). + struct InstanceLookup { + uint32_t model_id = 0; + uint32_t mesh_id = 0; + float placement_transformation[16]{}; + }; + bool findInstance(uint32_t object_id, InstanceLookup& out) const; + // Federation pipeline: composed instance transform = // FederatedFalseOrigin · ModelTransformation · CoordinateOperation // · placement_transformation