Files
IfcOpenShell/src/ifcviewer/AreaMeasurement.cpp
T
Dion Moult 75c9da5098 ifcviewer: overhaul model/object ID tracking
Rename the two overloaded model identifiers and make object_id
assignment single-authority, fixing a pick -> properties mismatch.

Identifiers:
- Per-model UUID fed_id -> model_id; the uint32 runtime handle
  model_id -> session_model_id (SessionState accessors + mirror hashes
  renamed to match). "fed_id" was a misnomer -- the federation is the
  whole collection, not one model.

object_id assignment (fixes wrong class on click):
- Producers (GeometryStreamer, .ifcview sidecar) now stamp model-LOCAL
  object_ids; ViewportCore::applyCachedModel is the sole authority that
  assigns the session-global id (base + local). Removed
  SceneLoader::next_object_id_, GeometryStreamer::lastObjectId(), and the
  streamer's start_object_id parameter.
- The element table is stamped by the same base on both load paths
  (applySidecarData and onStreamerFinished), so registry ids match the
  ids pick returns. Previously the sidecar path double-rebased instances
  vs the registry (click IfcSite -> showed IfcDoor); the live-stream path
  had the same latent mismatch. Both closed.

Naming / cleanup:
- SceneLoader::addFiles -> queueModels; startStreamLoadFor ->
  loadFromGeometryStreamer; readSidecarMetadataOnly -> readSidecarMetadata.
- Federation::addModel takes an explicit display_name (no QFileInfo
  fallback); callers pass QFileInfo(path).fileName().
- Disambiguate cryptic short locals (d->sidecar, m->model, c->chunk, ...)
  in SceneLoader, Federation, ViewportWindow, AreaMeasurement,
  SectionGizmoRenderer, and the SidecarData/SidecarReadPlan spots in
  ViewportCore.

Tests: 125/125 pass.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-08 14:10:05 +10:00

407 lines
17 KiB
C++
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/********************************************************************************
* *
* 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 "AreaMeasurement.h"
#include "OverlayRenderer.h"
#include "ViewportWindow.h"
#include <cstdio>
#include <QString>
#include <algorithm>
#include <cmath>
#include <cstring>
#include <limits>
#include <queue>
#include <unordered_set>
namespace {
// Undirected edge key between two mesh-local vertex indices.
uint64_t edgeKey(uint32_t a, uint32_t b) {
if (a > b) std::swap(a, b);
return (uint64_t(a) << 32) | uint64_t(b);
}
// Triangle area = 0.5 * |(b - a) × (c - a)|. Also returns the unit
// normal (zeroed for degenerate tris).
double triAreaAndNormal(const float* a, const float* b, const float* c,
float n_out[3]) {
const double bax = double(b[0]) - a[0];
const double bay = double(b[1]) - a[1];
const double baz = double(b[2]) - a[2];
const double cax = double(c[0]) - a[0];
const double cay = double(c[1]) - a[1];
const double caz = double(c[2]) - a[2];
const double nx = bay * caz - baz * cay;
const double ny = baz * cax - bax * caz;
const double nz = bax * cay - bay * cax;
const double len = std::sqrt(nx * nx + ny * ny + nz * nz);
if (len > 0.0) {
n_out[0] = float(nx / len);
n_out[1] = float(ny / len);
n_out[2] = float(nz / len);
} else {
n_out[0] = n_out[1] = n_out[2] = 0.0f;
}
return 0.5 * len;
}
// Squared distance from point `p` to triangle (a, b, c) — clipped to
// the triangle's interior or boundary, whichever is closest. Standard
// Ericson "Real-Time Collision Detection" implementation; identical to
// the GL AreaMeasurement helper.
double pointTriangleDistSq(const float p[3],
const float a[3], const float b[3], const float c[3]) {
auto sub = [](const float u[3], const float v[3], double r[3]) {
r[0] = double(u[0]) - v[0];
r[1] = double(u[1]) - v[1];
r[2] = double(u[2]) - v[2];
};
auto dot = [](const double u[3], const double v[3]) {
return u[0] * v[0] + u[1] * v[1] + u[2] * v[2];
};
double ab[3], ac[3], ap[3];
sub(b, a, ab);
sub(c, a, ac);
sub(p, a, ap);
const double d1 = dot(ab, ap);
const double d2 = dot(ac, ap);
if (d1 <= 0.0 && d2 <= 0.0) {
return ap[0]*ap[0] + ap[1]*ap[1] + ap[2]*ap[2];
}
double bp[3];
sub(p, b, bp);
const double d3 = dot(ab, bp);
const double d4 = dot(ac, bp);
if (d3 >= 0.0 && d4 <= d3) {
return bp[0]*bp[0] + bp[1]*bp[1] + bp[2]*bp[2];
}
const double vc = d1 * d4 - d3 * d2;
if (vc <= 0.0 && d1 >= 0.0 && d3 <= 0.0) {
const double v = d1 / (d1 - d3);
const double qx = ap[0] - v * ab[0];
const double qy = ap[1] - v * ab[1];
const double qz = ap[2] - v * ab[2];
return qx*qx + qy*qy + qz*qz;
}
double cp[3];
sub(p, c, cp);
const double d5 = dot(ab, cp);
const double d6 = dot(ac, cp);
if (d6 >= 0.0 && d5 <= d6) {
return cp[0]*cp[0] + cp[1]*cp[1] + cp[2]*cp[2];
}
const double vb = d5 * d2 - d1 * d6;
if (vb <= 0.0 && d2 >= 0.0 && d6 <= 0.0) {
const double w = d2 / (d2 - d6);
const double qx = ap[0] - w * ac[0];
const double qy = ap[1] - w * ac[1];
const double qz = ap[2] - w * ac[2];
return qx*qx + qy*qy + qz*qz;
}
const double va = d3 * d6 - d5 * d4;
if (va <= 0.0 && (d4 - d3) >= 0.0 && (d5 - d6) >= 0.0) {
const double w = (d4 - d3) / ((d4 - d3) + (d5 - d6));
const double qx = double(b[0]) + w * (double(c[0]) - b[0]) - p[0];
const double qy = double(b[1]) + w * (double(c[1]) - b[1]) - p[1];
const double qz = double(b[2]) + w * (double(c[2]) - b[2]) - p[2];
return qx*qx + qy*qy + qz*qz;
}
const double denom = 1.0 / (va + vb + vc);
const double v = vb * denom;
const double w = vc * denom;
const double qx = double(a[0]) + v * ab[0] + w * ac[0] - p[0];
const double qy = double(a[1]) + v * ab[1] + w * ac[1] - p[1];
const double qz = double(a[2]) + v * ab[2] + w * ac[2] - p[2];
return qx*qx + qy*qy + qz*qz;
}
constexpr double kCoplanarDot = 0.9999; // ~0.81° tolerance, matches GL
} // namespace
AreaMeasurement::AreaMeasurement() = default;
void AreaMeasurement::clear(ViewportWindow& vp) {
mesh_cache_.clear();
selected_.clear();
total_area_m2_ = 0.0;
vp.setHighlightTriangles({}, 0, 0, 0, 0);
vp.setOverlayLabels({});
}
AreaMeasurement::MeshAdj*
AreaMeasurement::meshAdj(ViewportWindow& vp,
uint32_t session_model_id, uint32_t mesh_id) {
const uint64_t key = (uint64_t(session_model_id) << 32) | uint64_t(mesh_id);
auto it = mesh_cache_.find(key);
if (it != mesh_cache_.end()) return &it->second;
// Need the raw positions + indices for adjacency. We never store
// them in the per-mesh cache (positions can be hundreds of KB each
// and live in the viewport already), so just look them up freshly
// each time the user picks a brand-new mesh.
ViewportWindow::MeshTriangles tris;
if (!vp.readbackMeshTriangles(session_model_id, mesh_id, tris)) return nullptr;
if (tris.indices.size() < 3) return nullptr;
MeshAdj a;
const size_t n_tris = tris.indices.size() / 3;
a.tri_normals.resize(n_tris * 3);
a.tri_areas.resize(n_tris);
a.edges.reserve(n_tris * 3);
for (size_t t = 0; t < n_tris; ++t) {
const uint32_t ia = tris.indices[3 * t + 0];
const uint32_t ib = tris.indices[3 * t + 1];
const uint32_t ic = tris.indices[3 * t + 2];
if (3 * ia + 2 >= tris.positions.size()
|| 3 * ib + 2 >= tris.positions.size()
|| 3 * ic + 2 >= tris.positions.size()) continue;
const float* pa = &tris.positions[3 * ia];
const float* pb = &tris.positions[3 * ib];
const float* pc = &tris.positions[3 * ic];
float n[3];
a.tri_areas[t] = triAreaAndNormal(pa, pb, pc, n);
a.tri_normals[3 * t + 0] = n[0];
a.tri_normals[3 * t + 1] = n[1];
a.tri_normals[3 * t + 2] = n[2];
a.edges[edgeKey(ia, ib)].push_back(uint32_t(t));
a.edges[edgeKey(ib, ic)].push_back(uint32_t(t));
a.edges[edgeKey(ic, ia)].push_back(uint32_t(t));
}
return &mesh_cache_.emplace(key, std::move(a)).first->second;
}
void AreaMeasurement::onPick(ViewportWindow& vp,
int x_phys, int y_phys, bool alt) {
ViewportWindow::MeshLocalPick pick;
if (!vp.pickMeshLocalAt(x_phys, y_phys, pick)) return;
ViewportWindow::MeshTriangles tris;
if (!vp.readbackMeshTriangles(pick.session_model_id, pick.mesh_id, tris)) return;
const size_t n_tris = tris.indices.size() / 3;
if (n_tris == 0) return;
MeshAdj* adj = meshAdj(vp, pick.session_model_id, pick.mesh_id);
if (!adj) return;
// Seed: the triangle whose interior (or boundary) is closest to the
// mesh-local pick point.
uint32_t seed = 0;
double best = std::numeric_limits<double>::infinity();
for (size_t t = 0; t < n_tris; ++t) {
const uint32_t ia = tris.indices[3 * t + 0];
const uint32_t ib = tris.indices[3 * t + 1];
const uint32_t ic = tris.indices[3 * t + 2];
const double d = pointTriangleDistSq(pick.mesh_local,
&tris.positions[3 * ia],
&tris.positions[3 * ib],
&tris.positions[3 * ic]);
if (d < best) { best = d; seed = uint32_t(t); }
}
// Coplanar patch via BFS over shared edges. Alt skips the expand
// (single-triangle accumulate).
std::vector<uint32_t> patch;
if (alt) {
patch.push_back(seed);
} else {
const float* sn = &adj->tri_normals[3 * seed];
std::unordered_set<uint32_t> visited;
visited.insert(seed);
std::queue<uint32_t> frontier;
frontier.push(seed);
while (!frontier.empty()) {
const uint32_t t = frontier.front(); frontier.pop();
patch.push_back(t);
for (int e = 0; e < 3; ++e) {
const uint32_t ia = tris.indices[3 * t + e];
const uint32_t ib = tris.indices[3 * t + (e + 1) % 3];
auto eit = adj->edges.find(edgeKey(ia, ib));
if (eit == adj->edges.end()) continue;
for (uint32_t nt : eit->second) {
if (nt == t || visited.count(nt)) continue;
const float* nn = &adj->tri_normals[3 * nt];
const double dot = double(sn[0]) * nn[0]
+ double(sn[1]) * nn[1]
+ double(sn[2]) * nn[2];
if (dot < kCoplanarDot) continue;
visited.insert(nt);
frontier.push(nt);
}
}
}
}
// Toggle: if the seed was already in the set, remove the patch;
// otherwise add it.
const uint64_t seed_key = triKey(pick.object_id, seed);
const bool removing = selected_.count(seed_key) > 0;
double delta = 0.0;
for (uint32_t t : patch) {
const uint64_t k = triKey(pick.object_id, t);
if (removing) {
auto it = selected_.find(k);
if (it != selected_.end()) {
if (t < adj->tri_areas.size()) delta -= adj->tri_areas[t];
selected_.erase(it);
}
} else {
SelectedTri sel;
sel.session_model_id = pick.session_model_id;
sel.mesh_id = pick.mesh_id;
sel.tri = t;
std::memcpy(sel.composed_transform, pick.composed_transform,
sizeof(sel.composed_transform));
if (selected_.emplace(k, sel).second) {
if (t < adj->tri_areas.size()) delta += adj->tri_areas[t];
}
}
}
total_area_m2_ += delta;
rebuildHighlightAndLabels(vp);
std::fprintf(stderr,
"[info] [wgpu area] %s%.6f m^2 (total: %.6f m^2, %zu tris)\n",
delta >= 0.0 ? "+" : "", delta,
total_area_m2_, selected_.size());
}
void AreaMeasurement::rebuildHighlightAndLabels(ViewportWindow& vp) {
// 1) Highlight triangle list — each selected tri's three vertices
// transformed by its captured composed_transform. Push as a
// flat world-space tri list; the overlay tints them translucent
// cyan to match GL.
std::vector<float> world_xyz;
world_xyz.reserve(selected_.size() * 9);
// Cache the latest MeshTriangles per (model,mesh) for this rebuild
// to avoid repeated viewport lookups when many tris share a mesh.
std::unordered_map<uint64_t, ViewportWindow::MeshTriangles> tris_cache;
auto get_tris = [&](uint32_t session_model_id, uint32_t mesh_id)
-> ViewportWindow::MeshTriangles* {
const uint64_t k = (uint64_t(session_model_id) << 32) | uint64_t(mesh_id);
auto it = tris_cache.find(k);
if (it != tris_cache.end()) return &it->second;
ViewportWindow::MeshTriangles tris;
if (!vp.readbackMeshTriangles(session_model_id, mesh_id, tris)) return nullptr;
return &tris_cache.emplace(k, std::move(tris)).first->second;
};
for (const auto& [key, sel] : selected_) {
ViewportWindow::MeshTriangles* tris = get_tris(sel.session_model_id, sel.mesh_id);
if (!tris) continue;
if (size_t(sel.tri) * 3 + 2 >= tris->indices.size()) continue;
const float* M = sel.composed_transform; // column-major
for (int e = 0; e < 3; ++e) {
const uint32_t vi = tris->indices[3 * sel.tri + e];
if (3 * vi + 2 >= tris->positions.size()) continue;
const float* p = &tris->positions[3 * vi];
// World = M * (p, 1). Column-major: M[col*4 + row].
const float wx = M[0]*p[0] + M[4]*p[1] + M[8]*p[2] + M[12];
const float wy = M[1]*p[0] + M[5]*p[1] + M[9]*p[2] + M[13];
const float wz = M[2]*p[0] + M[6]*p[1] + M[10]*p[2] + M[14];
world_xyz.push_back(wx);
world_xyz.push_back(wy);
world_xyz.push_back(wz);
}
}
// Bonsai's area-tool cyan tint: 0.20, 0.85, 1.00 @ 0.45 alpha.
vp.setHighlightTriangles(world_xyz, 0.20f, 0.85f, 1.00f, 0.45f);
// 2) Per-patch labels via connected-components sweep restricted to
// selected tris, one label per component at its area-weighted
// centroid (mesh-local → world via the captured transform).
std::unordered_map<uint32_t, std::vector<const SelectedTri*>> by_object;
for (const auto& [key, sel] : selected_) {
const uint32_t object_id = uint32_t(key >> 32);
by_object[object_id].push_back(&sel);
}
std::vector<OverlayRenderer::Label> labels;
for (const auto& [obj_id, sels] : by_object) {
if (sels.empty()) continue;
const SelectedTri& any = *sels[0];
ViewportWindow::MeshTriangles* tris = get_tris(any.session_model_id, any.mesh_id);
if (!tris) continue;
MeshAdj* adj = meshAdj(vp, any.session_model_id, any.mesh_id);
if (!adj) continue;
std::unordered_set<uint32_t> remaining;
remaining.reserve(sels.size());
for (const SelectedTri* s : sels) remaining.insert(s->tri);
while (!remaining.empty()) {
const uint32_t start = *remaining.begin();
std::unordered_set<uint32_t> in_comp{start};
std::queue<uint32_t> frontier;
frontier.push(start);
std::vector<uint32_t> component;
while (!frontier.empty()) {
const uint32_t tri = frontier.front(); frontier.pop();
component.push_back(tri);
if (size_t(tri) * 3 + 2 >= tris->indices.size()) continue;
for (int e = 0; e < 3; ++e) {
const uint32_t ia = tris->indices[3 * tri + e];
const uint32_t ib = tris->indices[3 * tri + (e + 1) % 3];
auto eit = adj->edges.find(edgeKey(ia, ib));
if (eit == adj->edges.end()) continue;
for (uint32_t nt : eit->second) {
if (in_comp.count(nt) || remaining.count(nt) == 0) continue;
in_comp.insert(nt);
frontier.push(nt);
}
}
}
for (uint32_t tri : component) remaining.erase(tri);
double area = 0.0, cx = 0.0, cy = 0.0, cz = 0.0;
for (uint32_t tri : component) {
if (size_t(tri) >= adj->tri_areas.size()) continue;
const double a = adj->tri_areas[tri];
area += a;
const uint32_t ia = tris->indices[3 * tri + 0];
const uint32_t ib = tris->indices[3 * tri + 1];
const uint32_t ic = tris->indices[3 * tri + 2];
const float* va = &tris->positions[3 * ia];
const float* vb = &tris->positions[3 * ib];
const float* vc = &tris->positions[3 * ic];
cx += a * (double(va[0]) + vb[0] + vc[0]) / 3.0;
cy += a * (double(va[1]) + vb[1] + vc[1]) / 3.0;
cz += a * (double(va[2]) + vb[2] + vc[2]) / 3.0;
}
if (area <= 0.0) continue;
cx /= area; cy /= area; cz /= area;
const float* M = any.composed_transform;
OverlayRenderer::Label lbl;
lbl.world_pos[0] = float(M[0]*cx + M[4]*cy + M[8]*cz + M[12]);
lbl.world_pos[1] = float(M[1]*cx + M[5]*cy + M[9]*cz + M[13]);
lbl.world_pos[2] = float(M[2]*cx + M[6]*cy + M[10]*cz + M[14]);
lbl.text = QString::number(area, 'f', 4) + QStringLiteral("");
labels.push_back(std::move(lbl));
}
}
vp.setOverlayLabels(labels);
}