mirror of
https://github.com/IfcOpenShell/IfcOpenShell.git
synced 2026-08-10 17:58:20 +00:00
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 <noreply@anthropic.com>
This commit is contained in:
@@ -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() {
|
||||
|
||||
@@ -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 <http://www.gnu.org/licenses/>. *
|
||||
* *
|
||||
********************************************************************************/
|
||||
|
||||
#include "Measurement.h"
|
||||
|
||||
#include "ViewportWindow.h"
|
||||
|
||||
#include <cmath>
|
||||
#include <cstring>
|
||||
#include <unordered_map>
|
||||
#include <vector>
|
||||
|
||||
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<uint32_t>& 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<uint64_t, std::vector<double>> 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;
|
||||
}
|
||||
@@ -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 <http://www.gnu.org/licenses/>. *
|
||||
* *
|
||||
********************************************************************************/
|
||||
|
||||
#ifndef IFCVIEWER_FULL_MEASUREMENT_H
|
||||
#define IFCVIEWER_FULL_MEASUREMENT_H
|
||||
|
||||
#include <cstdint>
|
||||
#include <vector>
|
||||
|
||||
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<uint32_t>& object_ids);
|
||||
|
||||
#endif // IFCVIEWER_FULL_MEASUREMENT_H
|
||||
@@ -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<uint8_t> 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<const uint16_t*>(
|
||||
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;
|
||||
}
|
||||
|
||||
@@ -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<float> positions; // 3 * vertex_count
|
||||
std::vector<uint32_t> 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
|
||||
|
||||
Reference in New Issue
Block a user