mirror of
https://github.com/IfcOpenShell/IfcOpenShell.git
synced 2026-09-12 06:32:09 +00:00
refactor: merge ifcviewer-wgpu into ifcviewer, drop Wgpu prefix
The GL backend is gone (task #53). The wgpu/non-wgpu folder split and the Wgpu* class prefix were both disambiguation artefacts from the overlap period — now pure dead weight. ## Folder + library merge * `src/ifcviewer-wgpu/` → folded into `src/ifcviewer/` (git mv tracks every file as a rename so blame/log history survives). * `src/ifcviewer-wgpu-minimal/` → `src/ifcviewer-minimal/` (the exe was already named `IfcViewerMinimal`; this just brings the folder + CMake target name into line). * `src/ifcviewer-wgpu/tests/test_wgpu_{selection,visibility}.cpp` → `src/ifcviewer/tests/test_{selection,visibility}.cpp`, folded into the existing `add_ifcviewer_unit_test(...)` helper. * The `IfcViewerWgpu` static library is dissolved — its sources become part of the unified `IfcViewer` static library, which now bundles scene/loader + renderer in one target. The pre-merge circular dependency (IfcViewer linking IfcViewerWgpu just to get the ViewportWindow.h include path that SceneLoader.h needs) goes away. * The wgpu-native FetchContent block, the Cocoa/QuartzCore link on Apple, the OBJCXX-enabled `.mm` source, and the wgpu-native runtime install all move into `src/ifcviewer/CMakeLists.txt` unchanged. ## Type renames (Wgpu prefix dropped from every Wgpu* identifier) WgpuAreaMeasurement → AreaMeasurement WgpuBufferPool → BufferPool WgpuLengthMeasurement → LengthMeasurement WgpuMetalSurface → MetalSurface WgpuModelGpuData → ModelGpuData WgpuOverlayFrame → OverlayFrame WgpuOverlayRenderer → OverlayRenderer WgpuSectionPlane → SectionPlane WgpuSelectionState → SelectionState WgpuStreamingLoader → StreamingLoader WgpuStreamingThread → StreamingThread WgpuViewportWindow → ViewportWindow WgpuVisibilityState → VisibilityState CMake target IfcViewerWgpuMinimal → IfcViewerMinimal (exe name was already this since wgpu shipped as default). Deliberately kept: `onWgpuLog` (wgpu-native log callback — names a binding to an external API, not one of *our* types), and the WGPU* enum/struct prefixes from wgpu-native's own headers. `WgpuMemProbe` lives in the separate `src/wgpu-mem-probe/` standalone diagnostic project and isn't touched. ## Include-path updates Every `#include "../ifcviewer-wgpu/Wgpu<X>.h"` → `"../ifcviewer/<X>.h"`, every in-directory `#include "Wgpu<X>.h"` → `"<X>.h"`. Includes from sibling subdirectories (modules/, etc.) are updated to point at `../../../ifcviewer/` instead of `../../../ifcviewer-wgpu/`. ## cmake/CMakeLists.txt simplification The redundant `add_subdirectory(ifcviewer-wgpu)` blocks (one inside the BUILD_BONSAIVIEWER fan-in, one in the BONSAIVIEWER-less standalone block) collapse into a single unconditional `add_subdirectory(../src/ifcviewer ifcviewer)`. The standalone block keeps only `wgpu-mem-probe` (the diagnostic tool, unrelated to the viewer lib). ## Verification * Full build green: `IfcViewer` static lib, `IfcViewerMinimal` exe, `BonsaiViewer` exe, all four pre-existing ifcviewer unit tests, and the two new-location tests (`test_selection`, `test_visibility`). * No stray `Wgpu<X>` identifier remains across `src/ifcviewer/`, `src/bonsaiviewer/`, `src/ifcviewer-minimal/` (verified by grep). * Renames tracked by git as `R` entries — `git log --follow` on ViewportWindow.cpp etc. continues to show history through the move. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,405 @@
|
||||
/********************************************************************************
|
||||
* *
|
||||
* 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 <QDebug>
|
||||
#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 model_id, uint32_t mesh_id) {
|
||||
const uint64_t key = (uint64_t(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(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.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.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.model_id = pick.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);
|
||||
|
||||
qInfo("[wgpu area] %s%.6f m^2 (total: %.6f m^2, %zu tris)",
|
||||
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 model_id, uint32_t mesh_id)
|
||||
-> ViewportWindow::MeshTriangles* {
|
||||
const uint64_t k = (uint64_t(model_id) << 32) | uint64_t(mesh_id);
|
||||
auto it = tris_cache.find(k);
|
||||
if (it != tris_cache.end()) return &it->second;
|
||||
ViewportWindow::MeshTriangles t;
|
||||
if (!vp.readbackMeshTriangles(model_id, mesh_id, t)) return nullptr;
|
||||
return &tris_cache.emplace(k, std::move(t)).first->second;
|
||||
};
|
||||
|
||||
for (const auto& [key, sel] : selected_) {
|
||||
ViewportWindow::MeshTriangles* t = get_tris(sel.model_id, sel.mesh_id);
|
||||
if (!t) continue;
|
||||
if (size_t(sel.tri) * 3 + 2 >= t->indices.size()) continue;
|
||||
const float* M = sel.composed_transform; // column-major
|
||||
for (int e = 0; e < 3; ++e) {
|
||||
const uint32_t vi = t->indices[3 * sel.tri + e];
|
||||
if (3 * vi + 2 >= t->positions.size()) continue;
|
||||
const float* p = &t->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* t = get_tris(any.model_id, any.mesh_id);
|
||||
if (!t) continue;
|
||||
MeshAdj* adj = meshAdj(vp, any.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 >= t->indices.size()) continue;
|
||||
for (int e = 0; e < 3; ++e) {
|
||||
const uint32_t ia = t->indices[3 * tri + e];
|
||||
const uint32_t ib = t->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 = t->indices[3 * tri + 0];
|
||||
const uint32_t ib = t->indices[3 * tri + 1];
|
||||
const uint32_t ic = t->indices[3 * tri + 2];
|
||||
const float* va = &t->positions[3 * ia];
|
||||
const float* vb = &t->positions[3 * ib];
|
||||
const float* vc = &t->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(" m²");
|
||||
labels.push_back(std::move(lbl));
|
||||
}
|
||||
}
|
||||
vp.setOverlayLabels(labels);
|
||||
}
|
||||
@@ -0,0 +1,96 @@
|
||||
/********************************************************************************
|
||||
* *
|
||||
* 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 WGPUAREAMEASUREMENT_H
|
||||
#define WGPUAREAMEASUREMENT_H
|
||||
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
#include <unordered_map>
|
||||
#include <vector>
|
||||
|
||||
class ViewportWindow;
|
||||
|
||||
// Click-to-accumulate area measurement for the wgpu viewport. Mirrors
|
||||
// src/bonsaiviewer/Measurement.h's AreaMeasurement: each pick resolves
|
||||
// to (instance, triangle) via ViewportWindow::pickMeshLocalAt, then
|
||||
// either adds or removes the connected coplanar patch (BFS over shared
|
||||
// edges, dot(normal, seed_normal) > 0.9999) depending on whether the
|
||||
// seed triangle was already in the running set. Alt-click skips the BFS.
|
||||
// Picks on different instances (even of the same mesh) are kept as
|
||||
// separate patches.
|
||||
//
|
||||
// On every mutation the world-space triangles of the running set are
|
||||
// pushed to ViewportWindow::setHighlightTriangles for the
|
||||
// translucent cyan patch shading, and per-component "X.XXXX m²" labels
|
||||
// are pushed to setOverlayLabels at each connected component's
|
||||
// area-weighted centroid.
|
||||
class AreaMeasurement {
|
||||
public:
|
||||
AreaMeasurement();
|
||||
|
||||
// Pixel coords are physical (post-DPR), to match
|
||||
// ViewportWindow::pickMeshLocalAt's convention.
|
||||
void onPick(ViewportWindow& vp, int x_phys, int y_phys, bool alt);
|
||||
void clear(ViewportWindow& vp);
|
||||
|
||||
double totalArea() const { return total_area_m2_; }
|
||||
size_t triangleCount() const { return selected_.size(); }
|
||||
|
||||
private:
|
||||
// Cached per-mesh derived data: triangle normals + areas + edge
|
||||
// adjacency. Computed once per (model, mesh) on first pick; the
|
||||
// raw positions + indices live in
|
||||
// ViewportWindow::readbackMeshTriangles' CPU shadow.
|
||||
struct MeshAdj {
|
||||
std::vector<float> tri_normals; // 3 floats per tri (unit, mesh-local)
|
||||
std::vector<float> tri_areas; // mesh-local area per tri
|
||||
// edge_key (min<<32 | max) → list of triangle indices touching it.
|
||||
std::unordered_map<uint64_t, std::vector<uint32_t>> edges;
|
||||
};
|
||||
// Keyed by (model_id << 32) | mesh_id.
|
||||
MeshAdj* meshAdj(ViewportWindow& vp,
|
||||
uint32_t model_id, uint32_t mesh_id);
|
||||
|
||||
// Per-selected-triangle record. The composed transform is captured
|
||||
// at pick time so highlight rebuilds don't have to re-query the
|
||||
// viewport for it (and so the overlay keeps working if the picked
|
||||
// instance later goes hidden).
|
||||
struct SelectedTri {
|
||||
uint32_t model_id;
|
||||
uint32_t mesh_id;
|
||||
uint32_t tri;
|
||||
float composed_transform[16];
|
||||
};
|
||||
|
||||
// Selection key: object_id (high 32) | tri index (low 32). Packing
|
||||
// by object_id rather than mesh_id keeps two distinct instances of
|
||||
// the same mesh contributing independently — matches the GL impl.
|
||||
static uint64_t triKey(uint32_t object_id, uint32_t tri) {
|
||||
return (uint64_t(object_id) << 32) | uint64_t(tri);
|
||||
}
|
||||
|
||||
void rebuildHighlightAndLabels(ViewportWindow& vp);
|
||||
|
||||
std::unordered_map<uint64_t, MeshAdj> mesh_cache_;
|
||||
std::unordered_map<uint64_t, SelectedTri> selected_;
|
||||
double total_area_m2_ = 0.0;
|
||||
};
|
||||
|
||||
#endif // WGPUAREAMEASUREMENT_H
|
||||
@@ -0,0 +1,225 @@
|
||||
/********************************************************************************
|
||||
* *
|
||||
* 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 "BufferPool.h"
|
||||
|
||||
#include <QtDebug>
|
||||
|
||||
#include <cassert>
|
||||
#include <cstring>
|
||||
|
||||
BufferPool::~BufferPool() {
|
||||
destroy();
|
||||
}
|
||||
|
||||
void BufferPool::configure(WGPUInstance instance, WGPUDevice device,
|
||||
WGPUBufferUsage usage,
|
||||
uint64_t per_sub_buffer_capacity,
|
||||
const char* label_prefix) {
|
||||
destroy();
|
||||
instance_ = instance;
|
||||
device_ = device;
|
||||
usage_ = usage;
|
||||
per_sub_buffer_capacity_ = per_sub_buffer_capacity;
|
||||
last_growth_size_ = per_sub_buffer_capacity;
|
||||
label_prefix_ = label_prefix ? label_prefix : "";
|
||||
}
|
||||
|
||||
void BufferPool::destroy() {
|
||||
for (auto& sp : sub_pools_) {
|
||||
if (sp.buffer) wgpuBufferRelease(sp.buffer);
|
||||
}
|
||||
sub_pools_.clear();
|
||||
device_ = nullptr;
|
||||
instance_ = nullptr;
|
||||
usage_ = 0;
|
||||
per_sub_buffer_capacity_ = 0;
|
||||
last_growth_size_ = 0;
|
||||
growth_disabled_ = false;
|
||||
label_prefix_.clear();
|
||||
}
|
||||
|
||||
bool BufferPool::addSubBuffer() {
|
||||
if (!device_ || per_sub_buffer_capacity_ == 0) return false;
|
||||
if (growth_disabled_) return false;
|
||||
|
||||
// 64 MB floor: smaller sub-buffers aren't worth the per-allocation
|
||||
// bookkeeping cost (one bind group per chunk, free-list overhead).
|
||||
// If the driver won't grant even 64 MB the pool is genuinely at
|
||||
// its ceiling; growth_disabled_ latches and future grow attempts
|
||||
// skip the doomed retry.
|
||||
constexpr uint64_t MIN_SUB_BUFFER_BYTES = 64ull * 1024 * 1024;
|
||||
uint64_t try_size = last_growth_size_ > 0
|
||||
? last_growth_size_
|
||||
: per_sub_buffer_capacity_;
|
||||
if (try_size < MIN_SUB_BUFFER_BYTES) try_size = MIN_SUB_BUFFER_BYTES;
|
||||
|
||||
while (try_size >= MIN_SUB_BUFFER_BYTES) {
|
||||
// wgpu-native classifies "Not enough memory left" as Validation,
|
||||
// not OutOfMemory. Nested scopes: OOM inner, Validation outer.
|
||||
wgpuDevicePushErrorScope(device_, WGPUErrorFilter_Validation);
|
||||
wgpuDevicePushErrorScope(device_, WGPUErrorFilter_OutOfMemory);
|
||||
|
||||
char label[128];
|
||||
std::snprintf(label, sizeof(label), "%s.sub%zu",
|
||||
label_prefix_.c_str(), sub_pools_.size());
|
||||
|
||||
WGPUBufferDescriptor desc = {};
|
||||
desc.usage = usage_;
|
||||
desc.size = try_size;
|
||||
desc.label.data = label;
|
||||
desc.label.length = std::strlen(label);
|
||||
WGPUBuffer buf = wgpuDeviceCreateBuffer(device_, &desc);
|
||||
|
||||
struct PopResult { bool done = false; bool error = false; };
|
||||
auto pop = [&](PopResult& pr) {
|
||||
WGPUPopErrorScopeCallbackInfo pcb = {};
|
||||
pcb.mode = WGPUCallbackMode_AllowProcessEvents;
|
||||
pcb.callback = [](WGPUPopErrorScopeStatus, WGPUErrorType type,
|
||||
WGPUStringView, void* ud1, void* /*ud2*/) {
|
||||
auto* p = static_cast<PopResult*>(ud1);
|
||||
p->done = true;
|
||||
p->error = (type != WGPUErrorType_NoError);
|
||||
};
|
||||
pcb.userdata1 = ≺
|
||||
wgpuDevicePopErrorScope(device_, pcb);
|
||||
while (!pr.done) wgpuInstanceProcessEvents(instance_);
|
||||
};
|
||||
PopResult oom_pop, validation_pop;
|
||||
pop(oom_pop);
|
||||
pop(validation_pop);
|
||||
|
||||
if (buf && !oom_pop.error && !validation_pop.error) {
|
||||
SubPool sp;
|
||||
sp.buffer = buf;
|
||||
sp.capacity = try_size;
|
||||
sp.used = 0;
|
||||
sp.free_ranges.push_back({0, try_size});
|
||||
sub_pools_.push_back(std::move(sp));
|
||||
last_growth_size_ = try_size;
|
||||
qInfo().noquote().nospace()
|
||||
<< "[wgpu pool] added sub-buffer " << (sub_pools_.size() - 1)
|
||||
<< " (" << (try_size / (1024 * 1024)) << " MB); pool total now "
|
||||
<< (total_capacity_bytes() / (1024 * 1024)) << " MB";
|
||||
return true;
|
||||
}
|
||||
if (buf) wgpuBufferRelease(buf);
|
||||
try_size /= 2;
|
||||
}
|
||||
|
||||
qInfo().noquote().nospace()
|
||||
<< "[wgpu pool] driver refused growth even at "
|
||||
<< (MIN_SUB_BUFFER_BYTES / (1024 * 1024)) << " MB; pool capped at "
|
||||
<< (total_capacity_bytes() / (1024 * 1024))
|
||||
<< " MB across " << sub_pools_.size() << " sub-buffer(s) — growth disabled";
|
||||
growth_disabled_ = true;
|
||||
return false;
|
||||
}
|
||||
|
||||
BufferPool::Slice BufferPool::alloc(uint64_t size, uint64_t align) {
|
||||
Slice out;
|
||||
if (size == 0 || align == 0) return out;
|
||||
|
||||
// First-fit across all sub-buffers. When none fits, try to grow by
|
||||
// adding another sub-buffer and retry once.
|
||||
for (int attempt = 0; attempt < 2; ++attempt) {
|
||||
for (size_t sp_idx = 0; sp_idx < sub_pools_.size(); ++sp_idx) {
|
||||
SubPool& sp = sub_pools_[sp_idx];
|
||||
for (size_t i = 0; i < sp.free_ranges.size(); ++i) {
|
||||
const FreeRange& r = sp.free_ranges[i];
|
||||
const uint64_t aligned = (r.offset + (align - 1)) & ~(align - 1);
|
||||
const uint64_t pad = aligned - r.offset;
|
||||
if (pad >= r.size) continue;
|
||||
if (size > r.size - pad) continue;
|
||||
|
||||
const uint64_t post_off = aligned + size;
|
||||
const uint64_t post_size = (r.offset + r.size) - post_off;
|
||||
|
||||
if (pad == 0 && post_size == 0) {
|
||||
sp.free_ranges.erase(sp.free_ranges.begin() + i);
|
||||
} else if (pad == 0) {
|
||||
sp.free_ranges[i] = {post_off, post_size};
|
||||
} else if (post_size == 0) {
|
||||
sp.free_ranges[i] = {r.offset, pad};
|
||||
} else {
|
||||
sp.free_ranges[i] = {r.offset, pad};
|
||||
sp.free_ranges.insert(sp.free_ranges.begin() + i + 1,
|
||||
{post_off, post_size});
|
||||
}
|
||||
|
||||
sp.used += size;
|
||||
out.buffer = sp.buffer;
|
||||
out.offset = aligned;
|
||||
out.size = size;
|
||||
out.sub_idx = int(sp_idx);
|
||||
return out;
|
||||
}
|
||||
}
|
||||
// Existing sub-buffers can't fit. Grow once before giving up.
|
||||
if (attempt == 0) {
|
||||
if (!addSubBuffer()) break;
|
||||
}
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
void BufferPool::free(const Slice& s) {
|
||||
if (!s.valid()) return;
|
||||
if (s.sub_idx < 0 || size_t(s.sub_idx) >= sub_pools_.size()) return;
|
||||
SubPool& sp = sub_pools_[size_t(s.sub_idx)];
|
||||
assert(s.offset + s.size <= sp.capacity);
|
||||
|
||||
size_t i = 0;
|
||||
while (i < sp.free_ranges.size() && sp.free_ranges[i].offset < s.offset) ++i;
|
||||
sp.free_ranges.insert(sp.free_ranges.begin() + i, {s.offset, s.size});
|
||||
sp.used -= s.size;
|
||||
|
||||
if (i + 1 < sp.free_ranges.size()
|
||||
&& sp.free_ranges[i].offset + sp.free_ranges[i].size == sp.free_ranges[i + 1].offset) {
|
||||
sp.free_ranges[i].size += sp.free_ranges[i + 1].size;
|
||||
sp.free_ranges.erase(sp.free_ranges.begin() + i + 1);
|
||||
}
|
||||
if (i > 0
|
||||
&& sp.free_ranges[i - 1].offset + sp.free_ranges[i - 1].size == sp.free_ranges[i].offset) {
|
||||
sp.free_ranges[i - 1].size += sp.free_ranges[i].size;
|
||||
sp.free_ranges.erase(sp.free_ranges.begin() + i);
|
||||
}
|
||||
}
|
||||
|
||||
uint64_t BufferPool::total_capacity_bytes() const {
|
||||
uint64_t s = 0;
|
||||
for (const auto& sp : sub_pools_) s += sp.capacity;
|
||||
return s;
|
||||
}
|
||||
|
||||
uint64_t BufferPool::total_used_bytes() const {
|
||||
uint64_t s = 0;
|
||||
for (const auto& sp : sub_pools_) s += sp.used;
|
||||
return s;
|
||||
}
|
||||
|
||||
uint64_t BufferPool::largest_free_run_bytes() const {
|
||||
uint64_t m = 0;
|
||||
for (const auto& sp : sub_pools_) {
|
||||
for (const auto& r : sp.free_ranges) {
|
||||
if (r.size > m) m = r.size;
|
||||
}
|
||||
}
|
||||
return m;
|
||||
}
|
||||
@@ -0,0 +1,154 @@
|
||||
/********************************************************************************
|
||||
* *
|
||||
* 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 WGPUBUFFERPOOL_H
|
||||
#define WGPUBUFFERPOOL_H
|
||||
|
||||
#include <webgpu/webgpu.h>
|
||||
|
||||
#include <cstdint>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
// Multi-sub-buffer sub-allocator. Owns one or more fixed-size WGPUBuffers
|
||||
// and hands out byte ranges within them.
|
||||
//
|
||||
// Why multiple sub-buffers: WebGPU caps any single buffer at
|
||||
// `limits.maxBufferSize`, which on wgpu-native + Vulkan tops out
|
||||
// around 2 GB regardless of how much GPU memory exists. The GL backend
|
||||
// reaches 4+ GB by letting the driver sub-allocate across many
|
||||
// VkDeviceMemory blocks behind one logical GL buffer; here we do the
|
||||
// same explicitly — `per_sub_buffer_capacity` (set from a probe) is the
|
||||
// largest single buffer that allocates cleanly, and the pool grows
|
||||
// lazily by adding more sub-buffers of that size when alloc demand
|
||||
// exceeds what existing sub-buffers can fit.
|
||||
//
|
||||
// Lifetime model: alloc/free are immediate. WebGPU guarantees that
|
||||
// queue.writeBuffer to a just-freed range is correctly serialised against
|
||||
// any prior submitted GPU reads — we never need to fence frees ourselves.
|
||||
//
|
||||
// Allocator: per-sub-buffer sorted free list with adjacent-range
|
||||
// coalescing, first-fit across sub-buffers. Adequate for the chunk
|
||||
// workload (a few hundred allocations of broadly similar size).
|
||||
class BufferPool {
|
||||
public:
|
||||
// A handle to a previously-allocated range. Includes the underlying
|
||||
// sub-buffer so callers (bind-group builders, queueWriteBuffer) can
|
||||
// address the correct buffer; includes sub_idx so free() knows which
|
||||
// sub-pool's bookkeeping to update.
|
||||
struct Slice {
|
||||
WGPUBuffer buffer = nullptr;
|
||||
uint64_t offset = 0;
|
||||
uint64_t size = 0;
|
||||
int sub_idx = -1;
|
||||
bool valid() const { return size > 0 && buffer != nullptr; }
|
||||
};
|
||||
|
||||
BufferPool() = default;
|
||||
~BufferPool();
|
||||
|
||||
BufferPool(const BufferPool&) = delete;
|
||||
BufferPool& operator=(const BufferPool&) = delete;
|
||||
|
||||
// Record the device + usage + sub-buffer size. Does NOT allocate any
|
||||
// sub-buffer here — that happens lazily on first alloc(). `instance`
|
||||
// is needed so the pool can drain async PopErrorScope events when
|
||||
// probing whether a new sub-buffer can be created.
|
||||
void configure(WGPUInstance instance, WGPUDevice device,
|
||||
WGPUBufferUsage usage,
|
||||
uint64_t per_sub_buffer_capacity,
|
||||
const char* label_prefix);
|
||||
void destroy();
|
||||
|
||||
// Sub-allocate a range of `size` bytes, aligned to `align` (must be
|
||||
// a power of two; typical: 256 for storage-buffer binding offsets).
|
||||
// Tries every existing sub-buffer; if none can fit, attempts to add
|
||||
// a new sub-buffer at per_sub_buffer_capacity. Returns an invalid
|
||||
// Slice (size == 0) if no sub-buffer fits and growth fails.
|
||||
Slice alloc(uint64_t size, uint64_t align);
|
||||
// Return a slice to the free list. Coalesces with adjacent free
|
||||
// ranges in the same sub-buffer.
|
||||
void free(const Slice& s);
|
||||
|
||||
// Tally summed across every sub-buffer.
|
||||
uint64_t total_capacity_bytes() const;
|
||||
uint64_t total_used_bytes() const;
|
||||
uint64_t total_free_bytes() const { return total_capacity_bytes() - total_used_bytes(); }
|
||||
// Largest contiguous free run across all sub-buffers. Useful for
|
||||
// evictor heuristics ("can this allocation even fit, ever, without
|
||||
// eviction or growth?").
|
||||
uint64_t largest_free_run_bytes() const;
|
||||
// Per-sub-buffer count, for diagnostics / logging.
|
||||
size_t sub_buffer_count() const { return sub_pools_.size(); }
|
||||
uint64_t per_sub_buffer_capacity_bytes() const { return per_sub_buffer_capacity_; }
|
||||
// Best estimate of the size a *future* sub-buffer would land at:
|
||||
// last_growth_size_ if we've ever grown (or just been configured),
|
||||
// else the configured per_sub_buffer_capacity. After the driver
|
||||
// refuses a size, halve-on-failure in addSubBuffer pushes this down
|
||||
// so callers' "can this chunk fit via growth?" check stays honest.
|
||||
uint64_t next_growth_size_bytes() const {
|
||||
return last_growth_size_ > 0 ? last_growth_size_ : per_sub_buffer_capacity_;
|
||||
}
|
||||
// Whether the pool can still attempt to add a sub-buffer. Flips to
|
||||
// false the first time addSubBuffer is refused even at the floor
|
||||
// size — eviction callers need this to know whether a future alloc
|
||||
// could rescue them, or whether eviction is the only path.
|
||||
bool can_grow() const { return !growth_disabled_ && per_sub_buffer_capacity_ > 0; }
|
||||
|
||||
private:
|
||||
struct FreeRange { uint64_t offset; uint64_t size; };
|
||||
struct SubPool {
|
||||
WGPUBuffer buffer = nullptr;
|
||||
uint64_t capacity = 0;
|
||||
uint64_t used = 0;
|
||||
std::vector<FreeRange> free_ranges;
|
||||
};
|
||||
|
||||
// Append a new sub-buffer to the pool. Starts at last_growth_size_
|
||||
// (initially per_sub_buffer_capacity_) and halves on driver refusal
|
||||
// before giving up — many Vulkan drivers cap single VkDeviceMemory
|
||||
// allocations at a couple GB (e.g. NVIDIA: maxStorageBufferBindingSize
|
||||
// is exactly 2 GB on consumer GeForce cards) or refuse big contiguous
|
||||
// allocations once heap is fragmented, but happily grant smaller ones.
|
||||
// Halving turns "stop at first refused 2 GB" into "2 GB + 1 GB + …",
|
||||
// which on a 4 GB card lets us reach 3 GB total instead of 2 GB.
|
||||
// Wrapped in OOM/Validation error scopes so failed attempts don't
|
||||
// take the device down. Returns true on success at some size
|
||||
// ≥ MIN_SUB_BUFFER_BYTES; false only when even the minimum size is
|
||||
// refused, at which point growth_disabled_ latches.
|
||||
bool addSubBuffer();
|
||||
|
||||
std::vector<SubPool> sub_pools_;
|
||||
|
||||
WGPUInstance instance_ = nullptr;
|
||||
WGPUDevice device_ = nullptr;
|
||||
WGPUBufferUsage usage_ = 0;
|
||||
uint64_t per_sub_buffer_capacity_ = 0;
|
||||
// The largest size addSubBuffer last *succeeded* at, in bytes.
|
||||
// Starts at per_sub_buffer_capacity_ (the probe's discovered max)
|
||||
// and decays as the driver refuses larger allocations. Future grow
|
||||
// attempts start from here rather than re-trying the max every
|
||||
// time — once the driver has refused 2 GB, retrying 2 GB on every
|
||||
// subsequent grow is wasted work.
|
||||
uint64_t last_growth_size_ = 0;
|
||||
bool growth_disabled_ = false;
|
||||
std::string label_prefix_;
|
||||
};
|
||||
|
||||
#endif // WGPUBUFFERPOOL_H
|
||||
+129
-10
@@ -20,15 +20,105 @@
|
||||
message("Running CMakeLists.txt in /src/ifcviewer")
|
||||
|
||||
set(QT_VERSION 6 CACHE STRING "Qt version")
|
||||
# IfcViewerLib always needs OpenGL in addition to Core/Gui/Widgets. We don't
|
||||
# use the CACHE'd QT_COMPONENTS here because it may have been set by another
|
||||
# target (e.g. qtviewer) without the OpenGL component.
|
||||
find_package(Qt${QT_VERSION} COMPONENTS Core Gui REQUIRED PATHS ${QT_DIR})
|
||||
|
||||
# Eigen3 — used by Federation matrices, ViewportWindow's instance compose,
|
||||
# the per-model coordinate-operation matrices stored on ModelGpuData, and
|
||||
# everywhere a 4x4 transform shows up.
|
||||
find_package(Eigen3 REQUIRED)
|
||||
|
||||
# wgpu-native — fetched as a pre-built binary release from upstream.
|
||||
# Pin the version with WGPU_NATIVE_VERSION; bump to pull a newer release.
|
||||
set(WGPU_NATIVE_VERSION "v29.0.0.0" CACHE STRING "wgpu-native release tag")
|
||||
|
||||
# Pick the right release archive for the host platform.
|
||||
if(CMAKE_SYSTEM_NAME STREQUAL "Windows")
|
||||
if(CMAKE_SYSTEM_PROCESSOR MATCHES "ARM64|aarch64")
|
||||
set(_wgpu_archive "wgpu-windows-aarch64-msvc-release.zip")
|
||||
else()
|
||||
set(_wgpu_archive "wgpu-windows-x86_64-msvc-release.zip")
|
||||
endif()
|
||||
set(_wgpu_lib "wgpu_native.dll.lib")
|
||||
set(_wgpu_runtime "wgpu_native.dll")
|
||||
elseif(CMAKE_SYSTEM_NAME STREQUAL "Darwin")
|
||||
if(CMAKE_SYSTEM_PROCESSOR MATCHES "arm64|aarch64")
|
||||
set(_wgpu_archive "wgpu-macos-aarch64-release.zip")
|
||||
else()
|
||||
set(_wgpu_archive "wgpu-macos-x86_64-release.zip")
|
||||
endif()
|
||||
set(_wgpu_lib "libwgpu_native.dylib")
|
||||
else() # Linux + BSDs
|
||||
if(CMAKE_SYSTEM_PROCESSOR MATCHES "aarch64|arm64")
|
||||
set(_wgpu_archive "wgpu-linux-aarch64-release.zip")
|
||||
else()
|
||||
set(_wgpu_archive "wgpu-linux-x86_64-release.zip")
|
||||
endif()
|
||||
set(_wgpu_lib "libwgpu_native.so")
|
||||
endif()
|
||||
|
||||
include(FetchContent)
|
||||
FetchContent_Declare(
|
||||
wgpu_native
|
||||
URL https://github.com/gfx-rs/wgpu-native/releases/download/${WGPU_NATIVE_VERSION}/${_wgpu_archive}
|
||||
DOWNLOAD_NO_PROGRESS FALSE
|
||||
)
|
||||
FetchContent_MakeAvailable(wgpu_native)
|
||||
|
||||
# Release archive layout: include/webgpu/*.h and lib/<libname>.
|
||||
#
|
||||
# The Linux .so shipped in the v29 release has no DT_SONAME, which causes
|
||||
# CMake to bake the relative IMPORTED_LOCATION path into DT_NEEDED. We patch
|
||||
# the SONAME in once at configure time so dependents get a clean
|
||||
# libwgpu_native.so reference, and pin the executable's rpath to the lib dir.
|
||||
if(UNIX AND NOT APPLE)
|
||||
find_program(PATCHELF_EXECUTABLE patchelf)
|
||||
if(PATCHELF_EXECUTABLE)
|
||||
execute_process(
|
||||
COMMAND ${PATCHELF_EXECUTABLE} --set-soname "${_wgpu_lib}"
|
||||
"${wgpu_native_SOURCE_DIR}/lib/${_wgpu_lib}"
|
||||
RESULT_VARIABLE _patchelf_rc
|
||||
)
|
||||
if(NOT _patchelf_rc EQUAL 0)
|
||||
message(WARNING "patchelf --set-soname failed on libwgpu_native.so")
|
||||
endif()
|
||||
else()
|
||||
message(WARNING
|
||||
"patchelf not found; libwgpu_native.so will be linked with a "
|
||||
"relative DT_NEEDED. Install patchelf to fix.")
|
||||
endif()
|
||||
endif()
|
||||
|
||||
add_library(wgpu_native SHARED IMPORTED GLOBAL)
|
||||
set_target_properties(wgpu_native PROPERTIES
|
||||
IMPORTED_LOCATION "${wgpu_native_SOURCE_DIR}/lib/${_wgpu_lib}"
|
||||
INTERFACE_INCLUDE_DIRECTORIES "${wgpu_native_SOURCE_DIR}/include"
|
||||
)
|
||||
if(WIN32)
|
||||
# On Windows the .lib is the import library; the .dll is the runtime.
|
||||
set_target_properties(wgpu_native PROPERTIES
|
||||
IMPORTED_IMPLIB "${wgpu_native_SOURCE_DIR}/lib/${_wgpu_lib}"
|
||||
IMPORTED_LOCATION "${wgpu_native_SOURCE_DIR}/lib/${_wgpu_runtime}"
|
||||
)
|
||||
endif()
|
||||
|
||||
# Expose the lib dir so dependents can put it on their rpath.
|
||||
set(WGPU_NATIVE_LIB_DIR "${wgpu_native_SOURCE_DIR}/lib" CACHE INTERNAL
|
||||
"Directory containing the wgpu-native shared library")
|
||||
|
||||
file(GLOB IFCVIEWER_CPP_FILES ${CMAKE_CURRENT_SOURCE_DIR}/*.cpp)
|
||||
file(GLOB IFCVIEWER_H_FILES ${CMAKE_CURRENT_SOURCE_DIR}/*.h)
|
||||
file(GLOB IFCVIEWER_H_FILES ${CMAKE_CURRENT_SOURCE_DIR}/*.h)
|
||||
set(IFCVIEWER_FILES ${IFCVIEWER_CPP_FILES} ${IFCVIEWER_H_FILES})
|
||||
|
||||
# Cocoa bridge for the CAMetalLayer surface attach — Objective-C++.
|
||||
# Only compiled into the target on Apple platforms; CMake handles `.mm`
|
||||
# natively once OBJCXX is enabled.
|
||||
if(APPLE)
|
||||
enable_language(OBJCXX)
|
||||
list(APPEND IFCVIEWER_FILES
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/MetalSurface_mac.mm
|
||||
)
|
||||
endif()
|
||||
|
||||
add_library(IfcViewer STATIC ${IFCVIEWER_FILES})
|
||||
|
||||
set_target_properties(IfcViewer PROPERTIES
|
||||
@@ -38,9 +128,9 @@ set_target_properties(IfcViewer PROPERTIES
|
||||
)
|
||||
|
||||
if (WITH_MESH_OPTIMIZER)
|
||||
find_package(meshoptimizer REQUIRED)
|
||||
set(MESH_OPTIMIZER_LIB meshoptimizer::meshoptimizer)
|
||||
target_compile_definitions(IfcViewer PUBLIC -DWITH_MESH_OPTIMIZER)
|
||||
find_package(meshoptimizer REQUIRED)
|
||||
set(MESH_OPTIMIZER_LIB meshoptimizer::meshoptimizer)
|
||||
target_compile_definitions(IfcViewer PUBLIC -DWITH_MESH_OPTIMIZER)
|
||||
endif()
|
||||
|
||||
# Consumers include headers as `#include "ViewportWindow.h"`, so expose this
|
||||
@@ -57,23 +147,52 @@ target_link_libraries(IfcViewer PUBLIC
|
||||
${CGAL_LIBRARIES}
|
||||
Qt${QT_VERSION}::Core
|
||||
Qt${QT_VERSION}::Gui
|
||||
Eigen3::Eigen
|
||||
wgpu_native
|
||||
${MESH_OPTIMIZER_LIB}
|
||||
# SceneLoader drives WgpuViewportWindow; the wgpu lib also provides
|
||||
# the include path for WgpuViewportWindow.h that SceneLoader.h pulls in.
|
||||
IfcViewerWgpu
|
||||
)
|
||||
|
||||
# Qt platform-handle access (QNativeInterface::QX11Application etc.) is in
|
||||
# the public Gui headers in Qt 6.2+, no PRIVATE_INCLUDE_DIRS needed.
|
||||
|
||||
if(UNIX AND NOT APPLE)
|
||||
find_package(Threads REQUIRED)
|
||||
target_link_libraries(IfcViewer PUBLIC Threads::Threads)
|
||||
endif()
|
||||
|
||||
# Cocoa + QuartzCore for MetalSurface_mac.mm (NSView, CAMetalLayer).
|
||||
if(APPLE)
|
||||
target_link_libraries(IfcViewer PUBLIC
|
||||
"-framework Cocoa"
|
||||
"-framework QuartzCore"
|
||||
)
|
||||
endif()
|
||||
|
||||
install(TARGETS IfcViewer EXPORT ${IFCOPENSHELL_EXPORT_TARGETS})
|
||||
|
||||
install(FILES ${IFCVIEWER_H_FILES}
|
||||
DESTINATION ${INCLUDEDIR}/ifcviewer
|
||||
)
|
||||
|
||||
# Install the wgpu_native shared library so the deployed runtime can find it.
|
||||
# At build/run-from-build-tree time CMake adds wgpu_native_SOURCE_DIR to the
|
||||
# binary's rpath automatically (IMPORTED_LOCATION dirname).
|
||||
#
|
||||
# macOS: drop libwgpu_native.dylib straight into BonsaiViewer.app's
|
||||
# Frameworks/. The exe has `LC_LOAD_DYLIB @rpath/libwgpu_native.dylib`
|
||||
# (baked from the dylib's install_name), and BonsaiViewer's
|
||||
# INSTALL_RPATH is set to @executable_path/../Frameworks — together
|
||||
# they resolve at launch without depending on macdeployqt to follow
|
||||
# non-Qt @rpath references.
|
||||
if(WIN32)
|
||||
install(FILES "${wgpu_native_SOURCE_DIR}/lib/${_wgpu_runtime}" DESTINATION bin)
|
||||
elseif(APPLE AND BUILD_BONSAIVIEWER)
|
||||
install(FILES "${wgpu_native_SOURCE_DIR}/lib/${_wgpu_lib}"
|
||||
DESTINATION "BonsaiViewer.app/Contents/Frameworks")
|
||||
else()
|
||||
install(FILES "${wgpu_native_SOURCE_DIR}/lib/${_wgpu_lib}" DESTINATION lib)
|
||||
endif()
|
||||
|
||||
if(BUILD_BONSAIVIEWER_TESTS)
|
||||
add_subdirectory(tests)
|
||||
endif()
|
||||
|
||||
@@ -0,0 +1,738 @@
|
||||
/********************************************************************************
|
||||
* *
|
||||
* 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 "LengthMeasurement.h"
|
||||
|
||||
#include "OverlayRenderer.h"
|
||||
|
||||
#include <QDebug>
|
||||
#include <QStringList>
|
||||
#include <QtGlobal>
|
||||
#include <QtMath>
|
||||
|
||||
#include <algorithm>
|
||||
#include <cmath>
|
||||
#include <cstring>
|
||||
#include <limits>
|
||||
#include <queue>
|
||||
#include <unordered_map>
|
||||
#include <unordered_set>
|
||||
|
||||
namespace {
|
||||
|
||||
double dist3(const std::array<float, 3>& a, const std::array<float, 3>& b) {
|
||||
const double dx = double(b[0]) - a[0];
|
||||
const double dy = double(b[1]) - a[1];
|
||||
const double dz = double(b[2]) - a[2];
|
||||
return std::sqrt(dx*dx + dy*dy + dz*dz);
|
||||
}
|
||||
|
||||
double triArea3(const std::array<float, 3>& a,
|
||||
const std::array<float, 3>& b,
|
||||
const std::array<float, 3>& c) {
|
||||
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;
|
||||
return 0.5 * std::sqrt(nx*nx + ny*ny + nz*nz);
|
||||
}
|
||||
|
||||
// Symmetric 3×3 eigendecomposition via Jacobi rotations. Tiny inline
|
||||
// alternative to pulling Eigen into the wgpu module for the single
|
||||
// polygon-planarity check. Converges in <10 sweeps for 3×3.
|
||||
void jacobiEigen3(double m00, double m01, double m02,
|
||||
double m11, double m12, double m22,
|
||||
double eigvals[3], double eigvecs[3][3]) {
|
||||
double a[3][3] = {{m00, m01, m02},
|
||||
{m01, m11, m12},
|
||||
{m02, m12, m22}};
|
||||
double v[3][3] = {{1, 0, 0}, {0, 1, 0}, {0, 0, 1}};
|
||||
for (int iter = 0; iter < 50; ++iter) {
|
||||
// Pick largest off-diagonal magnitude.
|
||||
int p = 0, q = 1;
|
||||
double off = std::abs(a[0][1]);
|
||||
if (std::abs(a[0][2]) > off) { p = 0; q = 2; off = std::abs(a[0][2]); }
|
||||
if (std::abs(a[1][2]) > off) { p = 1; q = 2; off = std::abs(a[1][2]); }
|
||||
if (off < 1e-12) break;
|
||||
const double app = a[p][p], aqq = a[q][q], apq = a[p][q];
|
||||
const double theta = (aqq - app) / (2.0 * apq);
|
||||
double t = (theta >= 0.0) ? 1.0 / (theta + std::sqrt(1.0 + theta*theta))
|
||||
: 1.0 / (theta - std::sqrt(1.0 + theta*theta));
|
||||
const double c = 1.0 / std::sqrt(1.0 + t*t);
|
||||
const double s = t * c;
|
||||
a[p][p] = app - t * apq;
|
||||
a[q][q] = aqq + t * apq;
|
||||
a[p][q] = a[q][p] = 0.0;
|
||||
for (int k = 0; k < 3; ++k) {
|
||||
if (k == p || k == q) continue;
|
||||
const double akp = a[k][p], akq = a[k][q];
|
||||
a[k][p] = a[p][k] = c * akp - s * akq;
|
||||
a[k][q] = a[q][k] = s * akp + c * akq;
|
||||
}
|
||||
for (int k = 0; k < 3; ++k) {
|
||||
const double vkp = v[k][p], vkq = v[k][q];
|
||||
v[k][p] = c * vkp - s * vkq;
|
||||
v[k][q] = s * vkp + c * vkq;
|
||||
}
|
||||
}
|
||||
eigvals[0] = a[0][0];
|
||||
eigvals[1] = a[1][1];
|
||||
eigvals[2] = a[2][2];
|
||||
std::memcpy(eigvecs, v, sizeof(v));
|
||||
}
|
||||
|
||||
struct PolygonAreaResult {
|
||||
double area_m2;
|
||||
const char* method;
|
||||
};
|
||||
|
||||
PolygonAreaResult polygonArea(const std::vector<std::array<float, 3>>& pts) {
|
||||
const size_t n = pts.size();
|
||||
|
||||
// Centroid + bounding box (for the planarity threshold).
|
||||
double centroid[3] = {0, 0, 0};
|
||||
double bbox_min[3] = { std::numeric_limits<double>::infinity(),
|
||||
std::numeric_limits<double>::infinity(),
|
||||
std::numeric_limits<double>::infinity() };
|
||||
double bbox_max[3] = {-std::numeric_limits<double>::infinity(),
|
||||
-std::numeric_limits<double>::infinity(),
|
||||
-std::numeric_limits<double>::infinity() };
|
||||
for (const auto& p : pts) {
|
||||
centroid[0] += p[0]; centroid[1] += p[1]; centroid[2] += p[2];
|
||||
bbox_min[0] = std::min(bbox_min[0], double(p[0]));
|
||||
bbox_min[1] = std::min(bbox_min[1], double(p[1]));
|
||||
bbox_min[2] = std::min(bbox_min[2], double(p[2]));
|
||||
bbox_max[0] = std::max(bbox_max[0], double(p[0]));
|
||||
bbox_max[1] = std::max(bbox_max[1], double(p[1]));
|
||||
bbox_max[2] = std::max(bbox_max[2], double(p[2]));
|
||||
}
|
||||
centroid[0] /= double(n);
|
||||
centroid[1] /= double(n);
|
||||
centroid[2] /= double(n);
|
||||
const double bbx = bbox_max[0] - bbox_min[0];
|
||||
const double bby = bbox_max[1] - bbox_min[1];
|
||||
const double bbz = bbox_max[2] - bbox_min[2];
|
||||
const double bbox_diag = std::sqrt(bbx*bbx + bby*bby + bbz*bbz);
|
||||
|
||||
// 3×3 symmetric covariance. Smallest eigenvector → plane normal.
|
||||
double c00 = 0, c01 = 0, c02 = 0, c11 = 0, c12 = 0, c22 = 0;
|
||||
for (const auto& p : pts) {
|
||||
const double dx = double(p[0]) - centroid[0];
|
||||
const double dy = double(p[1]) - centroid[1];
|
||||
const double dz = double(p[2]) - centroid[2];
|
||||
c00 += dx*dx; c01 += dx*dy; c02 += dx*dz;
|
||||
c11 += dy*dy; c12 += dy*dz; c22 += dz*dz;
|
||||
}
|
||||
double eigvals[3];
|
||||
double eigvecs[3][3];
|
||||
jacobiEigen3(c00, c01, c02, c11, c12, c22, eigvals, eigvecs);
|
||||
int min_i = 0;
|
||||
if (eigvals[1] < eigvals[min_i]) min_i = 1;
|
||||
if (eigvals[2] < eigvals[min_i]) min_i = 2;
|
||||
const double normal[3] = { eigvecs[0][min_i],
|
||||
eigvecs[1][min_i],
|
||||
eigvecs[2][min_i] };
|
||||
|
||||
double sq_sum = 0.0;
|
||||
for (const auto& p : pts) {
|
||||
const double d = (double(p[0]) - centroid[0]) * normal[0]
|
||||
+ (double(p[1]) - centroid[1]) * normal[1]
|
||||
+ (double(p[2]) - centroid[2]) * normal[2];
|
||||
sq_sum += d * d;
|
||||
}
|
||||
const double rms = std::sqrt(sq_sum / double(n));
|
||||
const bool planar = bbox_diag > 0.0 && (rms / bbox_diag) < 1e-3;
|
||||
|
||||
if (planar) {
|
||||
// In-plane orthonormal basis. Cross with whichever world axis is
|
||||
// least parallel to the normal so u doesn't collapse.
|
||||
double u[3];
|
||||
u[0] = normal[1] * 0.0 - normal[2] * 0.0; // normal × X
|
||||
u[1] = normal[2] * 1.0 - normal[0] * 0.0;
|
||||
u[2] = normal[0] * 0.0 - normal[1] * 1.0;
|
||||
double ul2 = u[0]*u[0] + u[1]*u[1] + u[2]*u[2];
|
||||
if (ul2 < 1e-6) {
|
||||
u[0] = normal[1] * 0.0 - normal[2] * 1.0; // normal × Y
|
||||
u[1] = normal[2] * 0.0 - normal[0] * 0.0;
|
||||
u[2] = normal[0] * 1.0 - normal[1] * 0.0;
|
||||
ul2 = u[0]*u[0] + u[1]*u[1] + u[2]*u[2];
|
||||
}
|
||||
const double ul = std::sqrt(ul2);
|
||||
u[0] /= ul; u[1] /= ul; u[2] /= ul;
|
||||
const double v[3] = {
|
||||
normal[1]*u[2] - normal[2]*u[1],
|
||||
normal[2]*u[0] - normal[0]*u[2],
|
||||
normal[0]*u[1] - normal[1]*u[0]
|
||||
};
|
||||
// Project + shoelace.
|
||||
std::vector<std::array<double, 2>> uv(n);
|
||||
for (size_t i = 0; i < n; ++i) {
|
||||
const double dx = double(pts[i][0]) - centroid[0];
|
||||
const double dy = double(pts[i][1]) - centroid[1];
|
||||
const double dz = double(pts[i][2]) - centroid[2];
|
||||
uv[i][0] = dx*u[0] + dy*u[1] + dz*u[2];
|
||||
uv[i][1] = dx*v[0] + dy*v[1] + dz*v[2];
|
||||
}
|
||||
double s = 0.0;
|
||||
for (size_t i = 0; i < n; ++i) {
|
||||
const auto& a = uv[i];
|
||||
const auto& b = uv[(i + 1) % n];
|
||||
s += a[0] * b[1] - b[0] * a[1];
|
||||
}
|
||||
return { 0.5 * std::abs(s), "planar" };
|
||||
}
|
||||
|
||||
// Fan from p0 — heuristic for non-planar / star-shaped 3D loops.
|
||||
double area = 0.0;
|
||||
for (size_t i = 1; i + 1 < n; ++i) {
|
||||
area += triArea3(pts[0], pts[i], pts[i + 1]);
|
||||
}
|
||||
return { area, "fan-triangulated (non-planar)" };
|
||||
}
|
||||
|
||||
uint64_t edgeKey(uint32_t a, uint32_t b) {
|
||||
if (a > b) std::swap(a, b);
|
||||
return (uint64_t(a) << 32) | uint64_t(b);
|
||||
}
|
||||
|
||||
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
|
||||
|
||||
// Visual style — reused across all length-tool overlay paths.
|
||||
constexpr float LINE_WIDTH = 1.5f;
|
||||
constexpr float LINE_HALO = 0.5f;
|
||||
constexpr float DOT_SIZE = 6.0f;
|
||||
constexpr float DOT_HALO = 1.0f;
|
||||
constexpr float DASH_PERIOD = 9.0f; // px
|
||||
constexpr float DASH_ON_RATIO = 0.55f; // 5 on, 4 off
|
||||
|
||||
OverlayRenderer::LineGroup makeGroup(std::vector<float> xyz,
|
||||
float r, float g, float b,
|
||||
bool dashed = false) {
|
||||
OverlayRenderer::LineGroup gp;
|
||||
gp.world_xyz = std::move(xyz);
|
||||
gp.color[0] = r; gp.color[1] = g; gp.color[2] = b; gp.color[3] = 1.0f;
|
||||
gp.stroke_color[0] = 0.0f; gp.stroke_color[1] = 0.0f;
|
||||
gp.stroke_color[2] = 0.0f; gp.stroke_color[3] = 1.0f;
|
||||
gp.line_width = LINE_WIDTH;
|
||||
gp.stroke_extra = LINE_HALO;
|
||||
gp.dash_period_px = dashed ? DASH_PERIOD : 0.0f;
|
||||
gp.dash_on_ratio = DASH_ON_RATIO;
|
||||
return gp;
|
||||
}
|
||||
|
||||
void pushDot(std::vector<float>& xyz, const std::array<float, 3>& p) {
|
||||
xyz.push_back(p[0]); xyz.push_back(p[1]); xyz.push_back(p[2]);
|
||||
}
|
||||
|
||||
void pushSeg(std::vector<float>& xyz,
|
||||
const std::array<float, 3>& a,
|
||||
const std::array<float, 3>& b) {
|
||||
xyz.insert(xyz.end(), a.begin(), a.end());
|
||||
xyz.insert(xyz.end(), b.begin(), b.end());
|
||||
}
|
||||
|
||||
OverlayRenderer::Label makeLabel(const std::array<float, 3>& a,
|
||||
const std::array<float, 3>& b,
|
||||
const QString& text) {
|
||||
OverlayRenderer::Label lbl;
|
||||
lbl.world_pos[0] = 0.5f * (a[0] + b[0]);
|
||||
lbl.world_pos[1] = 0.5f * (a[1] + b[1]);
|
||||
lbl.world_pos[2] = 0.5f * (a[2] + b[2]);
|
||||
lbl.text = text;
|
||||
return lbl;
|
||||
}
|
||||
|
||||
void pushDots(ViewportWindow& vp, const std::vector<float>& xyz) {
|
||||
vp.setOverlayPoints(xyz,
|
||||
/*inner*/ 1.0f, 1.0f, 1.0f, 1.0f,
|
||||
/*size*/ DOT_SIZE,
|
||||
/*stroke*/ 0.0f, 0.0f, 0.0f, 1.0f,
|
||||
/*extra*/ DOT_HALO);
|
||||
}
|
||||
|
||||
const char* dominantAxisLabel(const float v[3]) {
|
||||
const float ax = std::abs(v[0]);
|
||||
const float ay = std::abs(v[1]);
|
||||
const float az = std::abs(v[2]);
|
||||
if (az >= ax && az >= ay) return "Z";
|
||||
if (ax >= ay) return "X";
|
||||
return "Y";
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
LengthMeasurement::LengthMeasurement() = default;
|
||||
|
||||
void LengthMeasurement::clear(ViewportWindow& vp) {
|
||||
points_.clear();
|
||||
normals_.clear();
|
||||
vp.setOverlayPoints({}, 0,0,0,0, 0, 0,0,0,0, 0);
|
||||
vp.setOverlayLines({});
|
||||
vp.setOverlayLabels({});
|
||||
vp.setHudText(QString());
|
||||
}
|
||||
|
||||
void LengthMeasurement::onPick(ViewportWindow& vp,
|
||||
int x_phys, int y_phys, bool /*alt*/) {
|
||||
ViewportWindow::MeshLocalPick pick;
|
||||
if (!vp.pickMeshLocalAt(x_phys, y_phys, pick)) return;
|
||||
points_.push_back({pick.world_pos[0], pick.world_pos[1], pick.world_pos[2]});
|
||||
normals_.push_back({pick.world_normal[0], pick.world_normal[1], pick.world_normal[2]});
|
||||
if (points_.size() == 1) {
|
||||
first_pick_ = pick; // record info the laser BFS needs
|
||||
}
|
||||
rebuildOverlay(vp);
|
||||
}
|
||||
|
||||
void LengthMeasurement::removeLastPoint(ViewportWindow& vp) {
|
||||
if (points_.empty()) return;
|
||||
points_.pop_back();
|
||||
if (!normals_.empty()) normals_.pop_back();
|
||||
rebuildOverlay(vp);
|
||||
}
|
||||
|
||||
void LengthMeasurement::rebuildOverlay(ViewportWindow& vp) {
|
||||
if (points_.size() == 1 && normals_.size() == 1) {
|
||||
rebuildLaserOverlay(vp);
|
||||
return;
|
||||
}
|
||||
|
||||
std::vector<float> pts_xyz;
|
||||
pts_xyz.reserve(points_.size() * 3);
|
||||
for (const auto& p : points_) pushDot(pts_xyz, p);
|
||||
pushDots(vp, pts_xyz);
|
||||
|
||||
std::vector<OverlayRenderer::LineGroup> groups;
|
||||
std::vector<OverlayRenderer::Label> labels;
|
||||
const size_t n = points_.size();
|
||||
|
||||
if (n == 2) {
|
||||
const auto& a = points_[0];
|
||||
const auto& b = points_[1];
|
||||
groups.push_back(makeGroup({a[0], a[1], a[2], b[0], b[1], b[2]},
|
||||
1.0f, 1.0f, 1.0f));
|
||||
labels.push_back(makeLabel(a, b,
|
||||
QString::number(dist3(a, b), 'f', 3) + QStringLiteral(" m")));
|
||||
|
||||
const std::array<float, 3> kx = {b[0], a[1], a[2]};
|
||||
const std::array<float, 3> ky = {b[0], b[1], a[2]};
|
||||
const double dx = std::abs(double(b[0]) - a[0]);
|
||||
const double dy = std::abs(double(b[1]) - a[1]);
|
||||
const double dz = std::abs(double(b[2]) - a[2]);
|
||||
if (dx > 1e-6) {
|
||||
groups.push_back(makeGroup({a[0],a[1],a[2], kx[0],kx[1],kx[2]},
|
||||
1.00f, 0.30f, 0.30f));
|
||||
labels.push_back(makeLabel(a, kx,
|
||||
QStringLiteral("ΔX: ") + QString::number(dx, 'f', 3) + QStringLiteral(" m")));
|
||||
}
|
||||
if (dy > 1e-6) {
|
||||
groups.push_back(makeGroup({kx[0],kx[1],kx[2], ky[0],ky[1],ky[2]},
|
||||
0.30f, 0.90f, 0.30f));
|
||||
labels.push_back(makeLabel(kx, ky,
|
||||
QStringLiteral("ΔY: ") + QString::number(dy, 'f', 3) + QStringLiteral(" m")));
|
||||
}
|
||||
if (dz > 1e-6) {
|
||||
groups.push_back(makeGroup({ky[0],ky[1],ky[2], b[0],b[1],b[2]},
|
||||
0.30f, 0.55f, 1.00f));
|
||||
labels.push_back(makeLabel(ky, b,
|
||||
QStringLiteral("ΔZ: ") + QString::number(dz, 'f', 3) + QStringLiteral(" m")));
|
||||
}
|
||||
|
||||
if (normals_.size() == 2) {
|
||||
const auto& na = normals_[0];
|
||||
const auto& nb = normals_[1];
|
||||
const double dot_nn = double(na[0])*nb[0]
|
||||
+ double(na[1])*nb[1]
|
||||
+ double(na[2])*nb[2];
|
||||
if (std::abs(dot_nn) > 0.95) {
|
||||
const float sign = dot_nn >= 0.0 ? 1.0f : -1.0f;
|
||||
float n_avg[3] = {
|
||||
0.5f * (na[0] + sign * nb[0]),
|
||||
0.5f * (na[1] + sign * nb[1]),
|
||||
0.5f * (na[2] + sign * nb[2]),
|
||||
};
|
||||
const float len = std::sqrt(n_avg[0]*n_avg[0]
|
||||
+ n_avg[1]*n_avg[1]
|
||||
+ n_avg[2]*n_avg[2]);
|
||||
if (len > 1e-6f) {
|
||||
n_avg[0] /= len; n_avg[1] /= len; n_avg[2] /= len;
|
||||
}
|
||||
const double abx = double(b[0]) - a[0];
|
||||
const double aby = double(b[1]) - a[1];
|
||||
const double abz = double(b[2]) - a[2];
|
||||
const double perp = abx*n_avg[0] + aby*n_avg[1] + abz*n_avg[2];
|
||||
const double abs_perp = std::abs(perp);
|
||||
constexpr double kAxisCollapseTol = 1e-3;
|
||||
const bool redundant =
|
||||
std::abs(abs_perp - dx) < kAxisCollapseTol
|
||||
|| std::abs(abs_perp - dy) < kAxisCollapseTol
|
||||
|| std::abs(abs_perp - dz) < kAxisCollapseTol;
|
||||
if (abs_perp > 1e-6 && !redundant) {
|
||||
const std::array<float, 3> tip = {
|
||||
float(a[0] + perp * n_avg[0]),
|
||||
float(a[1] + perp * n_avg[1]),
|
||||
float(a[2] + perp * n_avg[2]),
|
||||
};
|
||||
auto perp_grp = makeGroup(
|
||||
{a[0],a[1],a[2], tip[0],tip[1],tip[2]},
|
||||
1.0f, 1.0f, 1.0f, /*dashed*/ true);
|
||||
groups.push_back(perp_grp);
|
||||
labels.push_back(makeLabel(a, tip,
|
||||
QStringLiteral("perp: ") + QString::number(abs_perp, 'f', 3) + QStringLiteral(" m")));
|
||||
}
|
||||
}
|
||||
}
|
||||
} else if (n >= 3) {
|
||||
std::vector<float> seg_xyz;
|
||||
seg_xyz.reserve(n * 6);
|
||||
labels.reserve(n);
|
||||
auto addSeg = [&](const std::array<float, 3>& a,
|
||||
const std::array<float, 3>& b) {
|
||||
pushSeg(seg_xyz, a, b);
|
||||
labels.push_back(makeLabel(a, b,
|
||||
QString::number(dist3(a, b), 'f', 3) + QStringLiteral(" m")));
|
||||
};
|
||||
for (size_t i = 0; i + 1 < n; ++i) addSeg(points_[i], points_[i + 1]);
|
||||
if (n >= 4) addSeg(points_[n - 1], points_[0]);
|
||||
groups.push_back(makeGroup(std::move(seg_xyz), 1.0f, 1.0f, 1.0f));
|
||||
}
|
||||
|
||||
vp.setOverlayLines(groups);
|
||||
vp.setOverlayLabels(labels);
|
||||
vp.setHudText(formatReadout());
|
||||
}
|
||||
|
||||
void LengthMeasurement::rebuildLaserOverlay(ViewportWindow& vp) {
|
||||
const auto& wp = first_pick_.world_pos;
|
||||
const auto& n = first_pick_.world_normal;
|
||||
|
||||
// Tangent basis in world space.
|
||||
constexpr float WORLD_UP[3] = {0.0f, 0.0f, 1.0f};
|
||||
const float dot_un = WORLD_UP[0]*n[0] + WORLD_UP[1]*n[1] + WORLD_UP[2]*n[2];
|
||||
float t1[3] = {
|
||||
WORLD_UP[0] - dot_un * n[0],
|
||||
WORLD_UP[1] - dot_un * n[1],
|
||||
WORLD_UP[2] - dot_un * n[2],
|
||||
};
|
||||
float t1_len = std::sqrt(t1[0]*t1[0] + t1[1]*t1[1] + t1[2]*t1[2]);
|
||||
if (t1_len < 0.1f) {
|
||||
constexpr float WORLD_X[3] = {1.0f, 0.0f, 0.0f};
|
||||
const float dot_xn = WORLD_X[0]*n[0] + WORLD_X[1]*n[1] + WORLD_X[2]*n[2];
|
||||
t1[0] = WORLD_X[0] - dot_xn * n[0];
|
||||
t1[1] = WORLD_X[1] - dot_xn * n[1];
|
||||
t1[2] = WORLD_X[2] - dot_xn * n[2];
|
||||
t1_len = std::sqrt(t1[0]*t1[0] + t1[1]*t1[1] + t1[2]*t1[2]);
|
||||
}
|
||||
if (t1_len > 1e-6f) {
|
||||
t1[0] /= t1_len; t1[1] /= t1_len; t1[2] /= t1_len;
|
||||
}
|
||||
const float t2[3] = {
|
||||
n[1]*t1[2] - n[2]*t1[1],
|
||||
n[2]*t1[0] - n[0]*t1[2],
|
||||
n[0]*t1[1] - n[1]*t1[0],
|
||||
};
|
||||
|
||||
std::vector<OverlayRenderer::LineGroup> groups;
|
||||
std::vector<OverlayRenderer::Label> labels;
|
||||
QStringList hud_lines;
|
||||
hud_lines << QStringLiteral("Laser measure (click another point for distance)");
|
||||
double enh[3] = {0.0, 0.0, 0.0};
|
||||
if (vp.meshLocalToGlobal(first_pick_.object_id, first_pick_.mesh_local, enh)) {
|
||||
hud_lines << QStringLiteral("ENH: %1, %2, %3")
|
||||
.arg(enh[0], 0, 'f', 3)
|
||||
.arg(enh[1], 0, 'f', 3)
|
||||
.arg(enh[2], 0, 'f', 3);
|
||||
}
|
||||
|
||||
// Coplanar-patch BFS for face extent.
|
||||
ViewportWindow::MeshTriangles tris;
|
||||
bool have_extent = false;
|
||||
double min_t1 = 0.0, max_t1 = 0.0, min_t2 = 0.0, max_t2 = 0.0;
|
||||
if (vp.readbackMeshTriangles(first_pick_.model_id, first_pick_.mesh_id, tris)) {
|
||||
const size_t n_verts = tris.positions.size() / 3;
|
||||
const size_t n_tris = tris.indices.size() / 3;
|
||||
if (n_tris > 0) {
|
||||
std::vector<float> wv(n_verts * 3);
|
||||
const float* M = first_pick_.composed_transform;
|
||||
for (size_t i = 0; i < n_verts; ++i) {
|
||||
const float* p = &tris.positions[i * 3];
|
||||
wv[i*3 + 0] = M[0]*p[0] + M[4]*p[1] + M[8]*p[2] + M[12];
|
||||
wv[i*3 + 1] = M[1]*p[0] + M[5]*p[1] + M[9]*p[2] + M[13];
|
||||
wv[i*3 + 2] = M[2]*p[0] + M[6]*p[1] + M[10]*p[2] + M[14];
|
||||
}
|
||||
std::vector<std::array<float, 3>> tri_n(n_tris);
|
||||
std::unordered_map<uint64_t, std::vector<uint32_t>> edges;
|
||||
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];
|
||||
const float* a = &wv[3*ia];
|
||||
const float* b = &wv[3*ib];
|
||||
const float* c = &wv[3*ic];
|
||||
const float bax = b[0]-a[0], bay = b[1]-a[1], baz = b[2]-a[2];
|
||||
const float cax = c[0]-a[0], cay = c[1]-a[1], caz = c[2]-a[2];
|
||||
float nx = bay*caz - baz*cay;
|
||||
float ny = baz*cax - bax*caz;
|
||||
float nz = bax*cay - bay*cax;
|
||||
const float nl = std::sqrt(nx*nx + ny*ny + nz*nz);
|
||||
if (nl > 0.0f) { nx /= nl; ny /= nl; nz /= nl; }
|
||||
tri_n[t] = {nx, ny, nz};
|
||||
edges[edgeKey(ia, ib)].push_back(uint32_t(t));
|
||||
edges[edgeKey(ib, ic)].push_back(uint32_t(t));
|
||||
edges[edgeKey(ic, ia)].push_back(uint32_t(t));
|
||||
}
|
||||
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(
|
||||
wp, &wv[3*ia], &wv[3*ib], &wv[3*ic]);
|
||||
if (d < best) { best = d; seed = uint32_t(t); }
|
||||
}
|
||||
const auto& sn = tri_n[seed];
|
||||
std::unordered_set<uint32_t> in_patch;
|
||||
in_patch.insert(seed);
|
||||
std::queue<uint32_t> frontier;
|
||||
frontier.push(seed);
|
||||
while (!frontier.empty()) {
|
||||
const uint32_t t = frontier.front(); frontier.pop();
|
||||
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 it = edges.find(edgeKey(ia, ib));
|
||||
if (it == edges.end()) continue;
|
||||
for (uint32_t nt : it->second) {
|
||||
if (nt == t || in_patch.count(nt)) continue;
|
||||
const auto& nn = tri_n[nt];
|
||||
const double dot = double(sn[0])*nn[0]
|
||||
+ double(sn[1])*nn[1]
|
||||
+ double(sn[2])*nn[2];
|
||||
if (dot < kCoplanarDot) continue;
|
||||
in_patch.insert(nt);
|
||||
frontier.push(nt);
|
||||
}
|
||||
}
|
||||
}
|
||||
std::unordered_set<uint32_t> patch_verts;
|
||||
for (uint32_t t : in_patch) {
|
||||
patch_verts.insert(tris.indices[3*t + 0]);
|
||||
patch_verts.insert(tris.indices[3*t + 1]);
|
||||
patch_verts.insert(tris.indices[3*t + 2]);
|
||||
}
|
||||
for (uint32_t vi : patch_verts) {
|
||||
const float* v = &wv[3 * vi];
|
||||
const double dx = double(v[0]) - wp[0];
|
||||
const double dy = double(v[1]) - wp[1];
|
||||
const double dz = double(v[2]) - wp[2];
|
||||
const double a1 = dx*t1[0] + dy*t1[1] + dz*t1[2];
|
||||
const double a2 = dx*t2[0] + dy*t2[1] + dz*t2[2];
|
||||
if (!have_extent) {
|
||||
min_t1 = max_t1 = a1;
|
||||
min_t2 = max_t2 = a2;
|
||||
have_extent = true;
|
||||
} else {
|
||||
min_t1 = std::min(min_t1, a1); max_t1 = std::max(max_t1, a1);
|
||||
min_t2 = std::min(min_t2, a2); max_t2 = std::max(max_t2, a2);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
auto pushBar = [&](const float t[3], double mn, double mx) {
|
||||
const std::array<float, 3> a = {
|
||||
float(wp[0] + mn * t[0]),
|
||||
float(wp[1] + mn * t[1]),
|
||||
float(wp[2] + mn * t[2]),
|
||||
};
|
||||
const std::array<float, 3> b = {
|
||||
float(wp[0] + mx * t[0]),
|
||||
float(wp[1] + mx * t[1]),
|
||||
float(wp[2] + mx * t[2]),
|
||||
};
|
||||
const double extent = mx - mn;
|
||||
const QString axis = QString::fromLatin1(dominantAxisLabel(t));
|
||||
groups.push_back(makeGroup({a[0],a[1],a[2], b[0],b[1],b[2]},
|
||||
1.0f, 1.0f, 1.0f, /*dashed*/ true));
|
||||
labels.push_back(makeLabel(a, b,
|
||||
QStringLiteral("%1 extent: %2 m").arg(axis).arg(extent, 0, 'f', 3)));
|
||||
hud_lines << QStringLiteral("%1 extent: %2 m").arg(axis).arg(extent, 0, 'f', 3);
|
||||
};
|
||||
if (have_extent && (max_t1 - min_t1) > 1e-6) pushBar(t1, min_t1, max_t1);
|
||||
if (have_extent && (max_t2 - min_t2) > 1e-6) pushBar(t2, min_t2, max_t2);
|
||||
|
||||
// Hybrid vertical raycast for floors / ceilings.
|
||||
if (std::abs(n[2]) > 0.85f) {
|
||||
constexpr float NUDGE = 1e-3f;
|
||||
const float ro[3] = {
|
||||
wp[0] + NUDGE * n[0],
|
||||
wp[1] + NUDGE * n[1],
|
||||
wp[2] + NUDGE * n[2],
|
||||
};
|
||||
ViewportWindow::RaycastHit hit;
|
||||
if (vp.raycast(ro, n, hit)) {
|
||||
const double dist = double(hit.distance) + double(NUDGE);
|
||||
const std::array<float, 3> a = {wp[0], wp[1], wp[2]};
|
||||
const std::array<float, 3> b = {hit.world_pos[0],
|
||||
hit.world_pos[1],
|
||||
hit.world_pos[2]};
|
||||
const QString tag = (n[2] > 0.0f)
|
||||
? QStringLiteral("ceiling height")
|
||||
: QStringLiteral("floor distance");
|
||||
groups.push_back(makeGroup({a[0],a[1],a[2], b[0],b[1],b[2]},
|
||||
1.0f, 1.0f, 1.0f, /*dashed*/ true));
|
||||
labels.push_back(makeLabel(a, b,
|
||||
QStringLiteral("%1: %2 m").arg(tag).arg(dist, 0, 'f', 3)));
|
||||
hud_lines << QStringLiteral("%1: %2 m").arg(tag).arg(dist, 0, 'f', 3);
|
||||
}
|
||||
}
|
||||
|
||||
pushDots(vp, std::vector<float>(wp, wp + 3));
|
||||
vp.setOverlayLines(groups);
|
||||
vp.setOverlayLabels(labels);
|
||||
vp.setHudText(hud_lines.join('\n'));
|
||||
}
|
||||
|
||||
QString LengthMeasurement::formatReadout() const {
|
||||
const size_t n = points_.size();
|
||||
if (n == 0) return QStringLiteral("Length tool: click first point");
|
||||
if (n == 1) return QStringLiteral("1 point (click another)");
|
||||
|
||||
if (n == 2) {
|
||||
const auto& a = points_[0];
|
||||
const auto& b = points_[1];
|
||||
const double d = dist3(a, b);
|
||||
const double dx = std::abs(double(b[0]) - a[0]);
|
||||
const double dy = std::abs(double(b[1]) - a[1]);
|
||||
const double dz = std::abs(double(b[2]) - a[2]);
|
||||
return QStringLiteral("Length: %1 m\nΔX: %2 ΔY: %3 ΔZ: %4 m")
|
||||
.arg(d, 0, 'f', 4)
|
||||
.arg(dx, 0, 'f', 4)
|
||||
.arg(dy, 0, 'f', 4)
|
||||
.arg(dz, 0, 'f', 4);
|
||||
}
|
||||
|
||||
if (n == 3) {
|
||||
const auto& a = points_[0];
|
||||
const auto& b = points_[1];
|
||||
const auto& c = points_[2];
|
||||
const double bax = double(a[0]) - b[0];
|
||||
const double bay = double(a[1]) - b[1];
|
||||
const double baz = double(a[2]) - b[2];
|
||||
const double bcx = double(c[0]) - b[0];
|
||||
const double bcy = double(c[1]) - b[1];
|
||||
const double bcz = double(c[2]) - b[2];
|
||||
const double la = std::sqrt(bax*bax + bay*bay + baz*baz);
|
||||
const double lc = std::sqrt(bcx*bcx + bcy*bcy + bcz*bcz);
|
||||
double angle_deg = 0.0;
|
||||
if (la > 0.0 && lc > 0.0) {
|
||||
const double cosang = std::clamp(
|
||||
(bax*bcx + bay*bcy + baz*bcz) / (la * lc), -1.0, 1.0);
|
||||
angle_deg = std::acos(cosang) * 180.0 / M_PI;
|
||||
}
|
||||
return QStringLiteral("Angle at pt 2: %1°\nTriangle area: %2 m²\nPerimeter: %3 m")
|
||||
.arg(angle_deg, 0, 'f', 2)
|
||||
.arg(triArea3(a, b, c), 0, 'f', 4)
|
||||
.arg(dist3(a, b) + dist3(b, c) + dist3(c, a), 0, 'f', 4);
|
||||
}
|
||||
|
||||
const PolygonAreaResult r = polygonArea(points_);
|
||||
double perimeter = 0.0;
|
||||
for (size_t i = 0; i < n; ++i) {
|
||||
perimeter += dist3(points_[i], points_[(i + 1) % n]);
|
||||
}
|
||||
return QStringLiteral("Polygon (%1 pts, %2)\nArea: %3 m²\nPerimeter: %4 m")
|
||||
.arg(n)
|
||||
.arg(r.method)
|
||||
.arg(r.area_m2, 0, 'f', 4)
|
||||
.arg(perimeter, 0, 'f', 4);
|
||||
}
|
||||
@@ -0,0 +1,75 @@
|
||||
/********************************************************************************
|
||||
* *
|
||||
* 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 WGPULENGTHMEASUREMENT_H
|
||||
#define WGPULENGTHMEASUREMENT_H
|
||||
|
||||
#include "ViewportWindow.h"
|
||||
|
||||
#include <QString>
|
||||
|
||||
#include <array>
|
||||
#include <vector>
|
||||
|
||||
// Click-to-place length / angle / area measurement for the wgpu viewport.
|
||||
// Mirrors src/bonsaiviewer/Measurement.h's LengthMeasurement — the
|
||||
// readout adapts to the running point count:
|
||||
//
|
||||
// 1 point → laser-measure: BFS the coplanar surface patch the click
|
||||
// landed on, project its vertices into the surface's own
|
||||
// tangent basis to get the face extent, plus a vertical
|
||||
// raycast for floor/ceiling distance on horizontal surfaces.
|
||||
// 2 points → straight-line distance + ΔX/ΔY/ΔZ stair-step + optional
|
||||
// perpendicular projection when both picks landed on
|
||||
// near-parallel surfaces.
|
||||
// 3 points → angle at the middle vertex + triangle area + perimeter.
|
||||
// 4+ pts → polygon area via best-fit-plane shoelace if near-planar,
|
||||
// fan-triangulated otherwise; perimeter on the closed loop.
|
||||
//
|
||||
// Pushes the running set as overlay points + the connecting polyline +
|
||||
// per-segment labels to the viewport; the multi-line HUD carries the
|
||||
// adaptive readout.
|
||||
class LengthMeasurement {
|
||||
public:
|
||||
LengthMeasurement();
|
||||
|
||||
// Pixel coords are physical (post-DPR). `alt` is currently unused
|
||||
// (kept for API symmetry with the Area tool).
|
||||
void onPick(ViewportWindow& vp, int x_phys, int y_phys, bool alt);
|
||||
void removeLastPoint(ViewportWindow& vp);
|
||||
void clear(ViewportWindow& vp);
|
||||
|
||||
size_t pointCount() const { return points_.size(); }
|
||||
|
||||
private:
|
||||
void rebuildOverlay(ViewportWindow& vp);
|
||||
void rebuildLaserOverlay(ViewportWindow& vp);
|
||||
QString formatReadout() const;
|
||||
|
||||
std::vector<std::array<float, 3>> points_;
|
||||
std::vector<std::array<float, 3>> normals_; // surface normal at each pick
|
||||
|
||||
// Captured at the very first pick of a fresh sequence and never
|
||||
// updated afterwards. Used by the 1-pt laser BFS to locate the
|
||||
// mesh-local position of points_[0] without re-picking. Stays valid
|
||||
// while points_[0] does (pop_back never touches the first element).
|
||||
ViewportWindow::MeshLocalPick first_pick_{};
|
||||
};
|
||||
|
||||
#endif // WGPULENGTHMEASUREMENT_H
|
||||
@@ -0,0 +1,32 @@
|
||||
/**
|
||||
* Objective-C++ bridge between ViewportWindow (pure C++) and Cocoa /
|
||||
* QuartzCore (Objective-C). Compiled only on macOS — see CMakeLists.txt.
|
||||
*
|
||||
* Qt's QWindow::winId() returns the backing NSView* (as a WId) on macOS;
|
||||
* we need a CAMetalLayer attached to that view to hand to wgpu-native
|
||||
* via WGPUSurfaceSourceMetalLayer. Doing that requires Objective-C, so
|
||||
* the actual layer attach lives in MetalSurface_mac.mm.
|
||||
*/
|
||||
|
||||
#ifndef WGPU_METAL_SURFACE_MAC_H
|
||||
#define WGPU_METAL_SURFACE_MAC_H
|
||||
|
||||
#if defined(__APPLE__)
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
/// Ensures the given NSView has a CAMetalLayer as its backing layer.
|
||||
/// Returns the CAMetalLayer pointer (`void*` so callers don't need to
|
||||
/// pull QuartzCore into their TU); the layer is owned by the NSView.
|
||||
/// Returns nullptr if `nsview_ptr` is null.
|
||||
void* wgpu_macos_attach_metal_layer(void* nsview_ptr);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
|
||||
#endif // __APPLE__
|
||||
|
||||
#endif // WGPU_METAL_SURFACE_MAC_H
|
||||
@@ -0,0 +1,45 @@
|
||||
/**
|
||||
* Objective-C++ implementation of the Cocoa bridge declared in
|
||||
* MetalSurface_mac.h. Compiled only on macOS.
|
||||
*/
|
||||
|
||||
#include "MetalSurface_mac.h"
|
||||
|
||||
#if defined(__APPLE__)
|
||||
|
||||
// The AppKit umbrella header pulls in NSWindow so `view.window`'s
|
||||
// `backingScaleFactor` resolves — `<AppKit/NSView.h>` alone only
|
||||
// forward-declares NSWindow.
|
||||
#import <AppKit/AppKit.h>
|
||||
#import <QuartzCore/CAMetalLayer.h>
|
||||
|
||||
void* wgpu_macos_attach_metal_layer(void* nsview_ptr) {
|
||||
if (!nsview_ptr) {
|
||||
return nullptr;
|
||||
}
|
||||
NSView* view = (__bridge NSView*)nsview_ptr;
|
||||
|
||||
// When QWindow::surfaceType is QSurface::MetalSurface, Qt already
|
||||
// backs the NSView with a CAMetalLayer — just hand it back. Otherwise
|
||||
// attach one ourselves (defensive: Qt's behaviour can change between
|
||||
// major versions and 6.x has occasionally regressed this).
|
||||
CAMetalLayer* layer = nil;
|
||||
if ([view.layer isKindOfClass:[CAMetalLayer class]]) {
|
||||
layer = (CAMetalLayer*)view.layer;
|
||||
} else {
|
||||
layer = [CAMetalLayer layer];
|
||||
view.wantsLayer = YES;
|
||||
view.layer = layer;
|
||||
}
|
||||
|
||||
// Track the screen's backing scale so we get retina-resolution
|
||||
// drawables. wgpu's surface configure picks the drawable size up
|
||||
// from layer.drawableSize at present-time.
|
||||
if (view.window) {
|
||||
layer.contentsScale = view.window.backingScaleFactor;
|
||||
}
|
||||
|
||||
return (__bridge void*)layer;
|
||||
}
|
||||
|
||||
#endif // __APPLE__
|
||||
@@ -0,0 +1,362 @@
|
||||
/********************************************************************************
|
||||
* *
|
||||
* 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 WGPUMODELGPUDATA_H
|
||||
#define WGPUMODELGPUDATA_H
|
||||
|
||||
#include <webgpu/webgpu.h>
|
||||
|
||||
#include <Eigen/Dense>
|
||||
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
#include <limits>
|
||||
#include <string>
|
||||
#include <unordered_map>
|
||||
#include <vector>
|
||||
|
||||
#include "InstancedGeometry.h"
|
||||
#include "BufferPool.h"
|
||||
|
||||
// Per-model wgpu state. Mirrors the GL backend's ModelGpuData but with
|
||||
// wgpu handles. Stage 2 only allocates and uploads the four core buffers;
|
||||
// bind groups, pipelines, BVH and cull scratch land in later stages.
|
||||
//
|
||||
// All vertex/index/mesh/instance bytes are uploaded once at load time via
|
||||
// wgpuQueueWriteBuffer. The vertex storage buffer is read by the vertex
|
||||
// shader (vertex pulling), not used as a classic vertex buffer — there is
|
||||
// no input-assembler vertex layout to match.
|
||||
// Web (WebGPU) mandates `maxStorageBufferBindingSize` ≥ 128 MB; some browsers
|
||||
// grant more, but we plan for the floor. Applied identically on desktop —
|
||||
// the cost is a few extra draws per frame (1 per chunk; typical models =
|
||||
// 1–3 chunks), which is invisible compared to per-frame GPU work.
|
||||
//
|
||||
// At INSTANCED_VERTEX_STRIDE_BYTES = 12 B/vertex this caps a chunk at
|
||||
// ~1.4 M vertices. 16 MB is the sweet spot once background-thread I/O
|
||||
// (StreamingThread) is in place: scatter-gather per-mesh seeks
|
||||
// happen on the worker, not the render thread, so smaller chunks
|
||||
// (and thus more per-frame loads as orbit shifts) no longer stall
|
||||
// rendering. The win is much finer pool-allocation granularity —
|
||||
// a 3 GB pool fits ~190 chunks vs ~21 at 128 MB — so visible
|
||||
// geometry is far less likely to get "trapped" behind invisible
|
||||
// chunkmates. Pre-async this size gave 7 fps (the sync loads blocked
|
||||
// the render thread); now it's bounded by cull cost not stream cost.
|
||||
//
|
||||
// Sidecar v14 (on-disk spatial reorder) would let us go smaller still
|
||||
// (~4 MB) with single-fread chunk loads, but the difference between
|
||||
// 16 MB and 4 MB is much smaller than the difference between 128 MB
|
||||
// and 16 MB.
|
||||
static constexpr uint64_t WGPU_CHUNK_VERTEX_BYTES_LIMIT = 16ull * 1024 * 1024;
|
||||
|
||||
struct ModelGpuData {
|
||||
// std430 layout: 16 bytes per entry, naturally aligned. base_vertex is
|
||||
// CHUNK-LOCAL — the bound vertex_storage on that chunk's bind group
|
||||
// gives the right slice when the shader indexes vertices[].
|
||||
struct alignas(16) VisibleDrawGpu {
|
||||
uint32_t mesh_id; // -> meshes[] for quantisation basis
|
||||
uint32_t instance_idx; // -> instances[] for transform + ids
|
||||
uint32_t ebo_first_u32; // start of this entry's slice in indices[] (global)
|
||||
uint32_t base_vertex; // chunk-local start of this mesh's slice in vertex_storage
|
||||
};
|
||||
static_assert(sizeof(VisibleDrawGpu) == 16, "VisibleDrawGpu must be 16 bytes");
|
||||
|
||||
// Per-chunk state. Each chunk references a vertex range and an
|
||||
// index range inside ViewportWindow::pool_, plus a small set of
|
||||
// per-frame buffers (visible_draws, prefix_sums, uniform) and a bind
|
||||
// group that binds the pool ranges alongside the model-shared
|
||||
// mesh/instance storage. Rendering issues one drawcall per non-empty
|
||||
// chunk.
|
||||
//
|
||||
// Streaming (task #16): a chunk may be marked is_resident=false; its
|
||||
// pool ranges (pool_*_size == 0) and bind_group are then unclaimed
|
||||
// until the streaming loader brings it in. Other per-chunk buffers
|
||||
// (visible_draws etc.) stay allocated regardless because cull still
|
||||
// needs them. Non-streaming path always sets is_resident=true and
|
||||
// populates pool ranges at applyCachedModel time.
|
||||
struct Chunk {
|
||||
// Pool-allocated vertex + index bytes. Both slices land in the
|
||||
// shared ViewportWindow::pool_; the slice tells us which
|
||||
// sub-buffer they live in (the pool may span multiple sub-buffers
|
||||
// when scenes exceed wgpu's single-buffer cap). When non-resident,
|
||||
// both .size are 0.
|
||||
BufferPool::Slice vertex_slice;
|
||||
BufferPool::Slice index_slice;
|
||||
|
||||
WGPUBuffer visible_draws_buffer = nullptr;
|
||||
WGPUBuffer prefix_sums_buffer = nullptr;
|
||||
WGPUBuffer per_chunk_uniform = nullptr;
|
||||
WGPUBindGroup bind_group = nullptr;
|
||||
|
||||
uint32_t vertex_count = 0; // chunk capacity (vertices)
|
||||
size_t visible_draws_capacity = 0;
|
||||
size_t prefix_sums_capacity = 0;
|
||||
|
||||
// Per-frame, populated by cullModelCpuCompute and consumed by render().
|
||||
// total_visible_* are post-frustum + contribution + HiZ — used to size
|
||||
// the actual draw call. frustum_visible_count is bumped immediately
|
||||
// after the frustum check (before contribution / HiZ), and is what
|
||||
// driveStreamingLoads keys on for residency decisions. Streaming
|
||||
// must NOT use the HiZ-post counters: HiZ visibility flips
|
||||
// frame-to-frame as occluders shift, which would otherwise thrash
|
||||
// the loader (evict-then-reload every frame even with the camera
|
||||
// stationary, killing FPS and producing visible flicker).
|
||||
uint32_t total_visible_vertices = 0;
|
||||
uint32_t total_visible_draws = 0;
|
||||
uint32_t frustum_visible_count = 0;
|
||||
|
||||
std::vector<VisibleDrawGpu> visible_draws_scratch;
|
||||
std::vector<uint32_t> prefix_sums_scratch;
|
||||
|
||||
// Residency. Streaming sets is_resident=false at applyCachedModel
|
||||
// and flips true once the chunk's vertex bytes are uploaded.
|
||||
// Render and pick skip chunks where !is_resident.
|
||||
bool is_resident = true;
|
||||
// Set true while a worker-thread read is in flight for this
|
||||
// chunk. Prevents driveStreamingLoads from re-enqueueing it
|
||||
// every frame until its result is drained. Cleared when the
|
||||
// result is applied (or dropped on failure / stale model).
|
||||
// Eviction is not gated on this (eviction only acts on resident
|
||||
// chunks; a loading chunk has no slice to free yet).
|
||||
bool is_loading = false;
|
||||
|
||||
// Aggregate vertex / index sizes across all meshes in this chunk
|
||||
// (sum of mesh.vertex_count * stride / mesh.index_count for each
|
||||
// mesh in mesh_ids). Used to size the pool allocation and to
|
||||
// compute the cull's per-chunk free-room check. Per-mesh layout
|
||||
// is recovered by walking mesh_ids and the model's MeshInfo[].
|
||||
uint64_t vertex_byte_size = 0;
|
||||
uint64_t index_count = 0;
|
||||
// Of `index_count`, how many are LOD1 indices. LOD0 indices occupy
|
||||
// chunk-local u32 offsets [0, index_count - lod1_index_count); LOD1
|
||||
// indices occupy [index_count - lod1_index_count, index_count). 0
|
||||
// when no mesh in this chunk had a baked LOD1 slice.
|
||||
uint32_t lod1_index_count = 0;
|
||||
|
||||
// World-space AABB covering every instance whose mesh lives in
|
||||
// this chunk. With spatial chunk planning this AABB is tight
|
||||
// (chunks group meshes by world centroid, not mesh-id), so the
|
||||
// distance-based evictor can meaningfully tell chunks apart.
|
||||
// Used by cull to reject whole chunks against the frustum before
|
||||
// iterating instances — and by the streaming loader to
|
||||
// prioritise which non-resident chunks to fetch first.
|
||||
float aabb_min[3] = { std::numeric_limits<float>::infinity(),
|
||||
std::numeric_limits<float>::infinity(),
|
||||
std::numeric_limits<float>::infinity() };
|
||||
float aabb_max[3] = { -std::numeric_limits<float>::infinity(),
|
||||
-std::numeric_limits<float>::infinity(),
|
||||
-std::numeric_limits<float>::infinity() };
|
||||
|
||||
// Mesh IDs assigned to this chunk, in chunk-local layout order.
|
||||
// Spatial chunk planning sorts meshes by world centroid first,
|
||||
// so this list is not in mesh-id order in general — each mesh's
|
||||
// bytes live at scattered offsets in the sidecar file. The
|
||||
// loader walks this list to scatter-gather the chunk's vertex
|
||||
// + index bytes; mesh_chunk_local_base_vertex /
|
||||
// mesh_chunk_local_ebo_first_u32 are computed in this same
|
||||
// order at planning time so the cull's VisibleDrawGpu entries
|
||||
// point at the correct chunk-local offsets.
|
||||
std::vector<uint32_t> mesh_ids;
|
||||
|
||||
// Instance indices belonging to this chunk (i.e. whose mesh lives
|
||||
// in this chunk). Built at chunk-planning time. Lets cull iterate
|
||||
// chunks as the outer loop, frustum-test the chunk AABB once,
|
||||
// and skip every instance inside in one shot when the chunk is
|
||||
// off-screen — far cheaper than the per-instance frustum check
|
||||
// on flat-scan culls of 1M+ instance scenes.
|
||||
std::vector<uint32_t> instance_ids;
|
||||
|
||||
// LRU marker for streaming eviction. Updated to the window's
|
||||
// streaming_frame_idx_ every frame the chunk is rendered (i.e.
|
||||
// total_visible_draws > 0). The evictor picks the smallest value
|
||||
// among non-visible resident chunks when it needs to free VRAM.
|
||||
uint64_t last_visible_frame_idx = 0;
|
||||
// EMA-smoothed visibility score, in [0, 1]. Bumped each frame
|
||||
// toward 1 when total_visible_draws > 0 (the chunk's instances
|
||||
// passed frustum + contribution + HiZ), toward 0 otherwise.
|
||||
// Time constant ~30 frames. Used by the streaming evictor to
|
||||
// de-prioritise chunks that are technically in the frustum but
|
||||
// consistently HiZ-occluded — e.g. interior pipes behind a
|
||||
// building's exterior walls. The smoothing prevents thrash from
|
||||
// momentary HiZ flicker (a wall briefly visible behind a panning
|
||||
// window doesn't displace the window from the pool).
|
||||
float visibility_history = 0.0f;
|
||||
// streaming_frame_idx_ when this chunk was last loaded. The
|
||||
// evictor grants newly-loaded chunks ~30 frames of grace at
|
||||
// full priority (max history factor = 1.0) so they have time
|
||||
// for visibility_history to develop. Without this, a just-
|
||||
// loaded chunk's effective priority drops to contribution ×
|
||||
// 0.05 next frame, and the chunk it displaced — back as a
|
||||
// candidate at full priority — re-displaces it: infinite
|
||||
// cycle between equal-priority chunks. The cycle prevents any
|
||||
// lower-priority candidate (e.g. a structural-brace chunk
|
||||
// ranked position 20 in the missing list) from ever getting
|
||||
// attempted.
|
||||
uint64_t loaded_frame_idx = 0;
|
||||
// How many times this chunk has been (re-)loaded over the
|
||||
// session. Bumped each successful applyStreamedChunk. A chunk
|
||||
// with load_count >> 1 has been cycling — used by the stream
|
||||
// debug log (WGPU_STREAM_DEBUG=1) to surface thrash.
|
||||
uint32_t load_count = 0;
|
||||
// Eviction attribution — who pushed this chunk out the last
|
||||
// time? Filled by evict_lowest_priority_than when the chunk is
|
||||
// unloaded. Read by the cycle-detection logger when this chunk
|
||||
// re-enters as a candidate so we can spot A→B→A 2-cycles. Zero
|
||||
// for chunks that were never evicted or were LRU-evicted (the
|
||||
// latter doesn't have an obvious "evictor" — just a slot
|
||||
// pressure event).
|
||||
uint32_t last_evicted_by_model_id = 0;
|
||||
uint32_t last_evicted_by_chunk_idx = UINT32_MAX;
|
||||
float last_evicted_by_priority = 0.0f;
|
||||
// Frame at which this chunk was most recently evicted, so the
|
||||
// cycle log only fires when re-entry is "soon" (cache thrash)
|
||||
// rather than "minutes later" (legitimate camera move).
|
||||
uint64_t last_evicted_frame_idx = 0;
|
||||
// Cooldown frame: if streaming_frame_idx_ < this, skip the
|
||||
// chunk in the candidate gather. Set when a candidate is
|
||||
// blocked OOM (eviction exhausted, still doesn't fit) OR when
|
||||
// applyStreamedChunk fails on the drained worker result. Caps
|
||||
// web bandwidth waste at one fetch per cooldown for chunks
|
||||
// that genuinely can't fit in the current pool state; the
|
||||
// cooldown expires naturally so the chunk re-enters when
|
||||
// pool layout has had a chance to change.
|
||||
uint64_t blocked_cooldown_until_frame_idx = 0;
|
||||
// Per-frame instance-aware priority. Sum of px² projected
|
||||
// contributions of every instance owned by this chunk —
|
||||
// captures the chunk's actual on-screen footprint, not the
|
||||
// (often loose) AABB union projection. Computed once per
|
||||
// frame at the top of driveStreamingLoads from the camera
|
||||
// state; the candidate/resident priority lambdas just read
|
||||
// this. See task #57 for the rationale.
|
||||
float current_priority = 0.0f;
|
||||
};
|
||||
std::vector<Chunk> chunks;
|
||||
|
||||
// Streaming source. Non-empty path means this model was loaded via the
|
||||
// streaming path: chunks may be non-resident and need byte-range reads
|
||||
// from this file. Empty path = legacy non-streaming load.
|
||||
std::string streaming_file_path;
|
||||
uint64_t streaming_vertex_section_offset = 0;
|
||||
uint64_t streaming_index_section_offset = 0;
|
||||
|
||||
// For each mesh in meshes[], the chunk it lives in plus the chunk-local
|
||||
// offsets into that chunk's vertex_storage and index_buffer. Populated
|
||||
// at applyCachedModel time; consumed by cullModelCpuCompute when it
|
||||
// populates VisibleDrawGpu entries.
|
||||
std::vector<uint32_t> mesh_chunk_idx;
|
||||
std::vector<uint32_t> mesh_chunk_local_base_vertex;
|
||||
std::vector<uint32_t> mesh_chunk_local_ebo_first_u32;
|
||||
// Where in the chunk's index slice this mesh's LOD1 indices start
|
||||
// (in u32 units). Only meaningful when m.meshes[mi].lod1_index_count > 0;
|
||||
// entries for meshes without LOD1 are 0 and unused.
|
||||
std::vector<uint32_t> mesh_chunk_local_lod1_first_u32;
|
||||
|
||||
// Per-INSTANCE chunk lookup tables. Mirror the per-mesh arrays above,
|
||||
// but resolved at planning time so cull can read them directly without
|
||||
// routing through mesh_id. The split exists because the spatial-
|
||||
// bucketing planner (#55) can place the same mesh in multiple chunks
|
||||
// (mesh data duplicated when its instances live in different buckets)
|
||||
// — under that scheme `mesh_chunk_idx[mesh_id]` is ambiguous, but
|
||||
// `instance_chunk_idx[instance_id]` is always exactly one chunk.
|
||||
// The mesh-keyed planner populates these by translation
|
||||
// (instance_chunk_idx[i] = mesh_chunk_idx[instances[i].mesh_id]);
|
||||
// the spatial-bucket planner populates them directly.
|
||||
std::vector<uint32_t> instance_chunk_idx;
|
||||
std::vector<uint32_t> instance_base_vertex;
|
||||
std::vector<uint32_t> instance_ebo_first_u32;
|
||||
std::vector<uint32_t> instance_lod1_first_u32;
|
||||
|
||||
// Model-shared buffers. Mesh + instance storage are small (<10 MB on
|
||||
// any real scene we've seen); the chunked index buffer lives in Chunk
|
||||
// alongside vertex_storage so streaming can defer both together.
|
||||
WGPUBuffer mesh_storage = nullptr; // MeshGpu[]: aabb_min/max
|
||||
WGPUBuffer instance_storage = nullptr; // InstanceGpu[]: transform + ids
|
||||
|
||||
// Cumulative VRAM accounting (bytes), populated at applyCachedModel
|
||||
// time. Sum of vertex_storage across chunks + index_buffer + mesh_storage
|
||||
// + instance_storage + per-chunk visible_draws + prefix_sums + uniforms.
|
||||
// Used by the per-frame stats log to attribute total VRAM.
|
||||
uint64_t vram_bytes_vbo = 0; // vertex storage total
|
||||
uint64_t vram_bytes_ebo = 0; // index buffer
|
||||
uint64_t vram_bytes_ssbo = 0; // mesh + instance + per-chunk small buffers
|
||||
|
||||
// Size mirrors for stats / range checks. vertex_bytes is the sum across
|
||||
// all chunks; index_count / mesh_count / instance_count are unchanged.
|
||||
size_t vertex_bytes = 0;
|
||||
uint32_t index_count = 0;
|
||||
uint32_t mesh_count = 0;
|
||||
uint32_t instance_count = 0;
|
||||
|
||||
// CPU side, kept for cull / picking / federation recompose.
|
||||
std::vector<MeshInfo> meshes;
|
||||
std::vector<InstanceCpu> instances;
|
||||
|
||||
// Local-frame volume (m³) of every mesh, indexed by mesh_id. Computed
|
||||
// once at applyCachedModel via signed-tetrahedra-from-origin on the
|
||||
// raw vertex+index data; reused by the Volume measurement tool to
|
||||
// avoid re-reading the GPU buffers per click. Empty in streaming mode
|
||||
// until the chunk holding the mesh has been delivered.
|
||||
std::vector<double> mesh_local_volumes;
|
||||
|
||||
// CPU shadow of each mesh's mesh-local positions + LOD0 indices.
|
||||
// Populated at applyCachedModel (or per-chunk in streaming) from
|
||||
// the same raw vertex bytes the volume calc dequantises. The Area
|
||||
// measurement tool reads this directly — no GPU readback, matching
|
||||
// the Volume tool's policy.
|
||||
//
|
||||
// Doubles per-vertex memory (12 B/vert GPU + 12 B/vert CPU). The
|
||||
// alternative is a wgpu mapAsync readback per first-touched mesh,
|
||||
// which adds async plumbing and a per-click stall; pay the memory
|
||||
// upfront instead. Trim by sizing each entry down at population
|
||||
// (reserve exact). For huge federations this can be a real
|
||||
// working-set cost — revisit if it shows up in profiles.
|
||||
struct MeshTriangles {
|
||||
std::vector<float> positions; // 3 * vertex_count, mesh-local
|
||||
std::vector<uint32_t> indices; // 3 * triangle_count, LOD0
|
||||
};
|
||||
std::vector<MeshTriangles> mesh_triangles_cache;
|
||||
|
||||
// object_id (globally rebased) → instance index in `instances`.
|
||||
// Populated alongside the instance vector so the Volume tool can do
|
||||
// O(1) instance lookup instead of linear-scanning every model.
|
||||
std::unordered_map<uint32_t, uint32_t> object_id_to_instance;
|
||||
|
||||
// Spatial chunk-cull replaced the per-model BVH walk — chunks are
|
||||
// already a one-level spatial partition of the instances, so a
|
||||
// single frustum test per chunk gives the same wholesale-reject
|
||||
// win without the BVH's per-node traversal overhead. The BVH field
|
||||
// is gone; cull iterates m.chunks instead.
|
||||
|
||||
bool hidden = false;
|
||||
|
||||
// Per-model federation matrices in metres. Default identity → no
|
||||
// per-model contribution to the composed transform. See bonsai's
|
||||
// Federation.h for the full pipeline composition order. Stored
|
||||
// here so setModelCoordinateOperation / setModelTransformation
|
||||
// have somewhere to land; the recompose-and-reupload pass that
|
||||
// would actually apply them is deferred.
|
||||
Eigen::Matrix4d coordinate_operation_meters = Eigen::Matrix4d::Identity();
|
||||
Eigen::Matrix4d model_transformation_meters = Eigen::Matrix4d::Identity();
|
||||
};
|
||||
|
||||
// Release every wgpu handle in `m` (including per-chunk and per-model pool
|
||||
// ranges via `pool.free()`) and clear its size mirrors. Safe to call
|
||||
// repeatedly; idempotent on already-released entries.
|
||||
void releaseWgpuModelGpuData(ModelGpuData& m, BufferPool& pool);
|
||||
|
||||
#endif // WGPUMODELGPUDATA_H
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,339 @@
|
||||
/********************************************************************************
|
||||
* *
|
||||
* 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 WGPUOVERLAYRENDERER_H
|
||||
#define WGPUOVERLAYRENDERER_H
|
||||
|
||||
#include <QHash>
|
||||
#include <QMatrix4x4>
|
||||
#include <QPoint>
|
||||
#include <QString>
|
||||
#include <QVector3D>
|
||||
|
||||
#include <webgpu/webgpu.h>
|
||||
|
||||
#include <cstdint>
|
||||
#include <vector>
|
||||
|
||||
// Per-frame snapshot of viewport state that every overlay needs. Built once
|
||||
// at the top of render() and passed by const-ref to each encodeX() call so
|
||||
// the overlay renderer never reaches back into the viewport.
|
||||
struct OverlayFrame {
|
||||
QMatrix4x4 view_proj;
|
||||
QVector3D camera_target;
|
||||
float camera_distance = 5.0f;
|
||||
float camera_yaw_deg = 0.0f;
|
||||
float camera_pitch_deg = 0.0f;
|
||||
float camera_fov_y_deg = 45.0f;
|
||||
int viewport_w_px = 0;
|
||||
int viewport_h_px = 0;
|
||||
int device_pixel_ratio = 1;
|
||||
};
|
||||
|
||||
// One section plane as the visualizer consumes it. The viewport owns the
|
||||
// authoritative state vector (the section tool mutates it); the overlay
|
||||
// reads from a non-owning span every frame. Held by value because the
|
||||
// struct is small and copies happen at most six times per frame.
|
||||
struct SectionPlane {
|
||||
QVector3D n; // unit normal (camera-facing after auto-flip)
|
||||
float d; // -dot(n, origin)
|
||||
QVector3D origin; // surface point at the moment the plane was added
|
||||
float visual_radius; // unused by the visualizer (kept here so the
|
||||
// section tool's state struct round-trips
|
||||
// through this overlay-facing definition).
|
||||
};
|
||||
|
||||
// All viewport overlays in one place: axis indicator (corner + pivot),
|
||||
// section plane gizmos, and the marquee drag rect. Mirrors GL's
|
||||
// OverlayRenderer split so ViewportWindow.cpp doesn't have to
|
||||
// carry ~1.5k lines of pipeline plumbing.
|
||||
//
|
||||
// Lifecycle: init() once after the device is up, destroy() before the
|
||||
// device dies. Pipelines are immutable after init; only per-frame
|
||||
// uniforms get re-written.
|
||||
//
|
||||
// Threading: all calls are main-thread only — they touch the wgpu queue.
|
||||
class OverlayRenderer {
|
||||
public:
|
||||
OverlayRenderer() = default;
|
||||
~OverlayRenderer();
|
||||
|
||||
OverlayRenderer(const OverlayRenderer&) = delete;
|
||||
OverlayRenderer& operator=(const OverlayRenderer&) = delete;
|
||||
|
||||
bool init(WGPUInstance instance, WGPUDevice device, WGPUQueue queue,
|
||||
WGPUTextureFormat surface_format, int sample_count);
|
||||
void destroy();
|
||||
|
||||
// ---- Inside the main MSAA pass, after geometry ----
|
||||
// Both share depth with the scene so they're correctly occluded.
|
||||
|
||||
// Orbit pivot indicator. `visible` is the viewport's UI gate (orbit
|
||||
// drag / wheel-zoom afterglow). When false this is a cheap no-op.
|
||||
void encodePivot(WGPURenderPassEncoder pass,
|
||||
const OverlayFrame& f,
|
||||
bool visible);
|
||||
|
||||
// Per-plane wireframe gizmo (2 × 2 m quad outline + arrow shaft +
|
||||
// arrow head). Drawn at each plane's origin in its local basis;
|
||||
// colour comes from Bonsai's decorator_color_error.
|
||||
void encodeSectionGizmos(WGPURenderPassEncoder pass,
|
||||
const OverlayFrame& f,
|
||||
const std::vector<SectionPlane>& planes);
|
||||
|
||||
// Replace the highlight-triangle list. `world_xyz` is 3 floats per
|
||||
// vertex, 3 vertices per triangle, in world space (post-composed-
|
||||
// transform). Empty disables the overlay. Color is RGBA in [0, 1] —
|
||||
// alpha < 1 gives the translucent patch shading the Area tool uses.
|
||||
// Drawn inside the main MSAA pass so depth-test hides patches
|
||||
// behind closer geometry; depth-write stays off so later overlays
|
||||
// can still draw over the highlight.
|
||||
void setHighlightTriangles(const std::vector<float>& world_xyz,
|
||||
float r, float g, float b, float a);
|
||||
void encodeHighlightTriangles(WGPURenderPassEncoder pass,
|
||||
const OverlayFrame& f);
|
||||
|
||||
// One stylistic group of world-space line segments. Mirrors GL
|
||||
// OverlayRenderer::LineGroup so callers can target either backend
|
||||
// with one struct.
|
||||
struct LineGroup {
|
||||
std::vector<float> world_xyz; // 6 floats per segment (a, b)
|
||||
float color[4] = {1, 1, 1, 1}; // inner color
|
||||
float stroke_color[4] = {0, 0, 0, 1}; // halo (alpha 0 = no stroke)
|
||||
float line_width = 1.5f; // inner full-width (px)
|
||||
float stroke_extra = 0.5f; // halo per side (px)
|
||||
float dash_period_px = 0.0f; // 0 = solid
|
||||
float dash_on_ratio = 0.6f; // [0..1], only when period > 0
|
||||
};
|
||||
|
||||
// Replace the overlay-line set. Each call CPU-expands every segment
|
||||
// into six vertices (two triangles), uploads the concatenated
|
||||
// expanded buffer once, and writes one uniform slot per group; the
|
||||
// next encodeOverlayLines() draws them in order. Empty `groups`
|
||||
// clears the set so subsequent encodes are no-ops.
|
||||
void setOverlayLines(const std::vector<LineGroup>& groups);
|
||||
|
||||
// Encode the most-recently set line groups. One draw per group with
|
||||
// a dynamic uniform offset; the shader handles stroke + dash from
|
||||
// per-group uniforms. Drawn inside the main MSAA pass so the lines
|
||||
// are depth-tested against geometry.
|
||||
void encodeOverlayLines(WGPURenderPassEncoder pass,
|
||||
const OverlayFrame& f);
|
||||
|
||||
// Replace the overlay-point set. World-space positions are CPU-
|
||||
// expanded into screen-space quads at encode time. `pixel_size` is
|
||||
// the inner-disc diameter (px); when `stroke_a > 0` each quad picks
|
||||
// up a `stroke_extra`-pixel halo per side. Empty `world_xyz` clears
|
||||
// the set. Mirrors GL OverlayRenderer::setOverlayPoints.
|
||||
void setOverlayPoints(const std::vector<float>& world_xyz,
|
||||
float r, float g, float b, float a,
|
||||
float pixel_size,
|
||||
float stroke_r, float stroke_g,
|
||||
float stroke_b, float stroke_a,
|
||||
float stroke_extra);
|
||||
|
||||
// Encode the most-recently set point list. Single draw covering all
|
||||
// points; the shader does the sprite-distance pick + AA. Drawn
|
||||
// inside the main MSAA pass so depth-test correctly hides points
|
||||
// behind closer geometry.
|
||||
void encodeOverlayPoints(WGPURenderPassEncoder pass,
|
||||
const OverlayFrame& f);
|
||||
|
||||
// World-anchored text label. Mirrors GL OverlayRenderer::Label so
|
||||
// measure-tool readouts can target either backend.
|
||||
struct Label {
|
||||
float world_pos[3];
|
||||
QString text;
|
||||
};
|
||||
void setOverlayLabels(const std::vector<Label>& labels);
|
||||
|
||||
// Top-left HUD text (tool prompts, length / area readouts). Empty
|
||||
// string hides it. Each newline starts a new line in the same rect.
|
||||
void setHudText(const QString& text);
|
||||
|
||||
// Encode all currently-set labels + the HUD. Per-string textures are
|
||||
// rasterised via QPainter into a small QImage on first sight and
|
||||
// cached by content; per-frame work is just projection, vertex
|
||||
// assembly, and one draw per visible label. Drawn on the resolved
|
||||
// surface (no depth test, no MSAA), so labels stack on top of every
|
||||
// overlay above.
|
||||
void encodeLabels(WGPUCommandEncoder enc,
|
||||
WGPUTextureView surface_view,
|
||||
const OverlayFrame& f);
|
||||
|
||||
// ---- After the edge silhouette pass, on the resolved surface ----
|
||||
|
||||
// Corner axis gizmo (bottom-left, 110×110 px). Independent ortho
|
||||
// projection — only the camera direction matters.
|
||||
void encodeCornerAxis(WGPUCommandEncoder enc,
|
||||
WGPUTextureView surface_view,
|
||||
const OverlayFrame& f);
|
||||
|
||||
// Marquee box-select drag rect (translucent fill + thick outline).
|
||||
// No-op when `active` is false.
|
||||
void encodeMarquee(WGPUCommandEncoder enc,
|
||||
WGPUTextureView surface_view,
|
||||
const OverlayFrame& f,
|
||||
QPoint start_logical_px,
|
||||
QPoint current_logical_px,
|
||||
bool active);
|
||||
|
||||
// Shared with the viewport's main FrameUniforms: same cap so the
|
||||
// viewport's clip-plane array and the gizmo visualiser agree on
|
||||
// how many planes can ever be active.
|
||||
static constexpr int kMaxSectionPlanes = 6;
|
||||
|
||||
private:
|
||||
bool buildAxisIndicator();
|
||||
bool buildSectionVisualizer();
|
||||
bool buildMarquee();
|
||||
bool buildOverlayLines();
|
||||
bool buildOverlayPoints();
|
||||
bool buildHighlightTriangles();
|
||||
bool buildLabels();
|
||||
|
||||
// Rasterise a single string at `font_pt` with dark-grey padded
|
||||
// background + white text, upload as an RGBA8 texture, build the
|
||||
// matching bind group. Width/height are the texture's physical-pixel
|
||||
// dimensions and are also what encodeLabels uses to size the quad.
|
||||
struct LabelTexture {
|
||||
WGPUTexture texture = nullptr;
|
||||
WGPUTextureView view = nullptr;
|
||||
WGPUBindGroup bind_group = nullptr;
|
||||
int width_px = 0;
|
||||
int height_px = 0;
|
||||
};
|
||||
LabelTexture* getOrCreateLabelTexture(const QString& cache_key,
|
||||
const QString& text,
|
||||
int font_pt,
|
||||
int dpr);
|
||||
void releaseLabelTextures();
|
||||
|
||||
WGPUInstance instance_ = nullptr;
|
||||
WGPUDevice device_ = nullptr;
|
||||
WGPUQueue queue_ = nullptr;
|
||||
WGPUTextureFormat surface_format_ = WGPUTextureFormat_Undefined;
|
||||
int sample_count_ = 1;
|
||||
|
||||
// ---- Axis indicator (shared shape, three pipelines) ----
|
||||
// Slot 0 = corner gizmo. Slots 1/2 = pivot visible/x-ray.
|
||||
WGPUShaderModule axis_shader_module_ = nullptr;
|
||||
WGPUBindGroupLayout axis_bgl_ = nullptr;
|
||||
WGPUPipelineLayout axis_pipeline_layout_ = nullptr;
|
||||
WGPURenderPipeline axis_pivot_pipeline_ = nullptr;
|
||||
WGPURenderPipeline axis_pivot_xray_pipeline_ = nullptr;
|
||||
WGPURenderPipeline axis_corner_pipeline_ = nullptr;
|
||||
WGPUBuffer axis_vertex_buffer_ = nullptr;
|
||||
WGPUBuffer axis_uniform_buffer_ = nullptr;
|
||||
WGPUBindGroup axis_bind_group_ = nullptr;
|
||||
static constexpr uint32_t kAxisUniformSlotSize = 256;
|
||||
|
||||
// ---- Section plane gizmos (1 pipeline, dynamic offset per plane) ----
|
||||
WGPUShaderModule section_shader_module_ = nullptr;
|
||||
WGPUBindGroupLayout section_bgl_ = nullptr;
|
||||
WGPUPipelineLayout section_pipeline_layout_ = nullptr;
|
||||
WGPURenderPipeline section_pipeline_ = nullptr;
|
||||
WGPUBuffer section_vertex_buffer_ = nullptr;
|
||||
WGPUBuffer section_uniform_buffer_ = nullptr;
|
||||
WGPUBindGroup section_bind_group_ = nullptr;
|
||||
static constexpr uint32_t kSectionUniformSlotSize = 256;
|
||||
|
||||
// ---- Marquee (fill + outline pipelines, one uniform buffer) ----
|
||||
WGPUShaderModule marquee_shader_module_ = nullptr;
|
||||
WGPUBindGroupLayout marquee_bgl_ = nullptr;
|
||||
WGPUPipelineLayout marquee_pipeline_layout_ = nullptr;
|
||||
WGPURenderPipeline marquee_pipeline_ = nullptr;
|
||||
WGPURenderPipeline marquee_fill_pipeline_ = nullptr;
|
||||
WGPUBuffer marquee_vertex_buffer_ = nullptr;
|
||||
WGPUBuffer marquee_fill_vertex_buffer_ = nullptr;
|
||||
WGPUBuffer marquee_uniform_buffer_ = nullptr;
|
||||
WGPUBindGroup marquee_bind_group_ = nullptr;
|
||||
|
||||
// ---- Overlay lines (per-group dynamic offset, resizable buffers) ----
|
||||
// Vertex buffer holds the concatenated expansion of every group's
|
||||
// segments (8 floats × 6 verts per segment). Uniform buffer holds
|
||||
// one 256-byte slot per group; the bind group binds a single 128-byte
|
||||
// window that the encoder rebinds via dynamic offset.
|
||||
WGPUShaderModule overlay_line_shader_module_ = nullptr;
|
||||
WGPUBindGroupLayout overlay_line_bgl_ = nullptr;
|
||||
WGPUPipelineLayout overlay_line_pipeline_layout_ = nullptr;
|
||||
WGPURenderPipeline overlay_line_pipeline_ = nullptr;
|
||||
WGPUBuffer overlay_line_vertex_buffer_ = nullptr;
|
||||
uint64_t overlay_line_vertex_capacity_ = 0;
|
||||
WGPUBuffer overlay_line_uniform_buffer_ = nullptr;
|
||||
uint32_t overlay_line_uniform_slots_ = 0;
|
||||
WGPUBindGroup overlay_line_bind_group_ = nullptr;
|
||||
static constexpr uint32_t kOverlayLineUniformSlotSize = 256;
|
||||
|
||||
// Per-group draw record. setOverlayLines() populates one per
|
||||
// LineGroup; encodeOverlayLines() iterates and issues one draw each.
|
||||
struct OverlayLineDraw {
|
||||
uint32_t first_vertex = 0;
|
||||
uint32_t vertex_count = 0;
|
||||
};
|
||||
std::vector<OverlayLineDraw> overlay_line_draws_;
|
||||
|
||||
// ---- Overlay points (sprite-style, quad-expanded per point) ----
|
||||
// Single draw per encode covering every point in the set. Vertex
|
||||
// buffer holds 6 verts × 8 bytes per point (vec3 world + vec2 corner).
|
||||
// Uniforms are global to the set (one inner + one stroke color).
|
||||
WGPUShaderModule overlay_point_shader_module_ = nullptr;
|
||||
WGPUBindGroupLayout overlay_point_bgl_ = nullptr;
|
||||
WGPUPipelineLayout overlay_point_pipeline_layout_ = nullptr;
|
||||
WGPURenderPipeline overlay_point_pipeline_ = nullptr;
|
||||
WGPUBuffer overlay_point_vertex_buffer_ = nullptr;
|
||||
uint64_t overlay_point_vertex_capacity_ = 0;
|
||||
WGPUBuffer overlay_point_uniform_buffer_ = nullptr;
|
||||
WGPUBindGroup overlay_point_bind_group_ = nullptr;
|
||||
uint32_t overlay_point_vertex_count_ = 0;
|
||||
|
||||
// ---- Highlight triangles (translucent world-space triangle list) ----
|
||||
// One pipeline + one uniform buffer (view_proj + RGBA tint). Vertex
|
||||
// buffer grows on demand to fit the current set; empty set ⇒ encode
|
||||
// is a no-op.
|
||||
WGPUShaderModule highlight_shader_module_ = nullptr;
|
||||
WGPUBindGroupLayout highlight_bgl_ = nullptr;
|
||||
WGPUPipelineLayout highlight_pipeline_layout_ = nullptr;
|
||||
WGPURenderPipeline highlight_pipeline_ = nullptr;
|
||||
WGPUBuffer highlight_vertex_buffer_ = nullptr;
|
||||
uint64_t highlight_vertex_capacity_ = 0;
|
||||
WGPUBuffer highlight_uniform_buffer_ = nullptr;
|
||||
WGPUBindGroup highlight_bind_group_ = nullptr;
|
||||
uint32_t highlight_vertex_count_ = 0;
|
||||
float highlight_color_[4] = {0, 0, 0, 0};
|
||||
|
||||
// ---- Labels + HUD text (textured quads, cached by content) ----
|
||||
// One QPainter-rasterised QImage per unique text string, uploaded as
|
||||
// an RGBA8 texture and re-used across frames. Per-frame work is
|
||||
// projection + vertex assembly + draws; no allocation in the steady
|
||||
// state. Bind groups are layout-shared across all label textures so
|
||||
// every cache entry holds its own bind_group ready to bind.
|
||||
WGPUShaderModule label_shader_module_ = nullptr;
|
||||
WGPUBindGroupLayout label_bgl_ = nullptr;
|
||||
WGPUPipelineLayout label_pipeline_layout_ = nullptr;
|
||||
WGPURenderPipeline label_pipeline_ = nullptr;
|
||||
WGPUSampler label_sampler_ = nullptr;
|
||||
WGPUBuffer label_vertex_buffer_ = nullptr;
|
||||
uint64_t label_vertex_capacity_ = 0;
|
||||
QHash<QString, LabelTexture> label_tex_cache_;
|
||||
std::vector<Label> labels_;
|
||||
QString hud_text_;
|
||||
};
|
||||
|
||||
#endif // WGPUOVERLAYRENDERER_H
|
||||
@@ -37,7 +37,7 @@ void SceneLoader::setShouldWriteSidecar(bool enabled) {
|
||||
should_write_sidecar_ = enabled;
|
||||
}
|
||||
|
||||
SceneLoader::SceneLoader(WgpuViewportWindow* viewport, QObject* parent)
|
||||
SceneLoader::SceneLoader(ViewportWindow* viewport, QObject* parent)
|
||||
: QObject(parent), viewport_(viewport)
|
||||
{
|
||||
connect(&element_poll_timer_, &QTimer::timeout,
|
||||
|
||||
@@ -35,13 +35,13 @@
|
||||
#include <vector>
|
||||
|
||||
#include "Federation.h"
|
||||
#include "../ifcviewer-wgpu/WgpuViewportWindow.h"
|
||||
#include "../ifcviewer-wgpu/WgpuStreamingLoader.h"
|
||||
#include "../ifcviewer/ViewportWindow.h"
|
||||
#include "../ifcviewer/StreamingLoader.h"
|
||||
#include "GeometryStreamer.h"
|
||||
#include "SidecarBuilder.h"
|
||||
#include "SidecarCache.h"
|
||||
|
||||
// Drives IFC file loading into a WgpuViewportWindow. Owns the per-model
|
||||
// Drives IFC file loading into a ViewportWindow. Owns the per-model
|
||||
// GeometryStreamer, the load queue, the sidecar read thread, and the
|
||||
// next-free object_id counter used to rebase cached models onto the
|
||||
// current session's ID space.
|
||||
@@ -55,7 +55,7 @@
|
||||
class SceneLoader : public QObject {
|
||||
Q_OBJECT
|
||||
public:
|
||||
explicit SceneLoader(WgpuViewportWindow* viewport, QObject* parent = nullptr);
|
||||
explicit SceneLoader(ViewportWindow* viewport, QObject* parent = nullptr);
|
||||
~SceneLoader();
|
||||
|
||||
// Sidecar cache use is opt-in per direction. Embedders that don't care
|
||||
@@ -165,7 +165,7 @@ private:
|
||||
void applySidecarData(uint32_t mid, StreamingSidecar metadata);
|
||||
void startDataSourceLoad(uint32_t mid);
|
||||
|
||||
WgpuViewportWindow* viewport_ = nullptr;
|
||||
ViewportWindow* viewport_ = nullptr;
|
||||
bool should_read_sidecar_ = false;
|
||||
bool should_write_sidecar_ = false;
|
||||
std::map<uint32_t, Model> models_;
|
||||
|
||||
@@ -0,0 +1,105 @@
|
||||
/********************************************************************************
|
||||
* *
|
||||
* 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 WGPUSELECTIONSTATE_H
|
||||
#define WGPUSELECTIONSTATE_H
|
||||
|
||||
#include <cstdint>
|
||||
#include <unordered_set>
|
||||
#include <vector>
|
||||
|
||||
// CPU-side selection tracking. Mirrors src/ifcviewer/Selection.h shape but
|
||||
// without Qt deps (kept pure stdlib so it can move into ifcviewer-core
|
||||
// later without dragging Qt along).
|
||||
//
|
||||
// Two flavours of "selected":
|
||||
// - the multi-set (ids()): every object the user has Shift-added.
|
||||
// - active (activeId()): the *last* single-clicked object. UIs typically
|
||||
// use this to drive the properties panel; the renderer tints it
|
||||
// slightly more strongly than the rest of the multi-set.
|
||||
//
|
||||
// The GPU consumes a flat u32 array indexed by object_id: bit 0 = selected,
|
||||
// bit 1 = active. Sized to (max_object_id + 1) by the caller.
|
||||
class SelectionState {
|
||||
public:
|
||||
void clear() {
|
||||
if (ids_.empty() && active_ == 0) return;
|
||||
ids_.clear();
|
||||
active_ = 0;
|
||||
dirty_ = true;
|
||||
}
|
||||
|
||||
// Replace the selection with a single object. id == 0 clears.
|
||||
void replace(uint32_t id) {
|
||||
ids_.clear();
|
||||
if (id != 0) ids_.insert(id);
|
||||
active_ = id;
|
||||
dirty_ = true;
|
||||
}
|
||||
|
||||
void add(uint32_t id) {
|
||||
if (id == 0) return;
|
||||
ids_.insert(id);
|
||||
active_ = id;
|
||||
dirty_ = true;
|
||||
}
|
||||
|
||||
void remove(uint32_t id) {
|
||||
if (id == 0) return;
|
||||
if (ids_.erase(id) == 0) return;
|
||||
if (active_ == id) {
|
||||
active_ = ids_.empty() ? 0 : *ids_.begin();
|
||||
}
|
||||
dirty_ = true;
|
||||
}
|
||||
|
||||
void toggle(uint32_t id) {
|
||||
if (id == 0) return;
|
||||
if (ids_.count(id)) remove(id);
|
||||
else add(id);
|
||||
}
|
||||
|
||||
bool contains(uint32_t id) const { return ids_.count(id) > 0; }
|
||||
uint32_t activeId() const { return active_; }
|
||||
// Named selectionIds() rather than ids() so bonsai's
|
||||
// `viewport_->selection().selectionIds()` compiles unchanged.
|
||||
const std::unordered_set<uint32_t>& selectionIds() const { return ids_; }
|
||||
size_t count() const { return ids_.size(); }
|
||||
|
||||
bool dirty() const { return dirty_; }
|
||||
void markClean() { dirty_ = false; }
|
||||
|
||||
// Fill `out` (sized to entries u32s) with bit-packed flags:
|
||||
// bit 0 = selected (in ids_), bit 1 = active. out[0] is always 0
|
||||
// because object_id 0 is the "miss" sentinel.
|
||||
void fillFlagsArray(std::vector<uint32_t>& out, uint32_t entries) const {
|
||||
out.assign(entries, 0);
|
||||
for (uint32_t id : ids_) {
|
||||
if (id < entries) out[id] |= 1u;
|
||||
}
|
||||
if (active_ != 0 && active_ < entries) out[active_] |= 2u;
|
||||
}
|
||||
|
||||
private:
|
||||
std::unordered_set<uint32_t> ids_;
|
||||
uint32_t active_ = 0;
|
||||
bool dirty_ = false;
|
||||
};
|
||||
|
||||
#endif // WGPUSELECTIONSTATE_H
|
||||
@@ -0,0 +1,320 @@
|
||||
/********************************************************************************
|
||||
* *
|
||||
* 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/>. *
|
||||
* *
|
||||
********************************************************************************/
|
||||
|
||||
// v13 sidecar layout (matched against SidecarCache.cpp):
|
||||
//
|
||||
// SidecarHeader (12 bytes)
|
||||
// uint32 num_vertex_bytes
|
||||
// uint8[num_vertex_bytes] vertex data <-- streaming skips
|
||||
// uint32 num_indices
|
||||
// uint32[num_indices] index data <-- streaming skips
|
||||
// uint32 num_meshes + MeshInfo[] <-- streaming reads
|
||||
// uint32 num_instances + InstanceCpu[] <-- streaming reads
|
||||
// uint32 has_coord_op + double[16] + 2× double <-- streaming reads
|
||||
// uint32 num_elements + PackedElementInfo[] <-- streaming reads
|
||||
// uint32 string_table_bytes + char[] <-- streaming reads
|
||||
//
|
||||
// Streaming reader returns offsets to the two skipped sections so chunks
|
||||
// can be range-read on demand. File handle is closed before return.
|
||||
|
||||
#include "StreamingLoader.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <cstdio>
|
||||
#include <cstring>
|
||||
|
||||
namespace {
|
||||
|
||||
struct SidecarHeaderRaw {
|
||||
uint32_t magic;
|
||||
uint32_t version;
|
||||
uint32_t endian;
|
||||
};
|
||||
|
||||
template<typename T>
|
||||
bool readVec(FILE* f, std::vector<T>& v) {
|
||||
uint32_t n;
|
||||
if (std::fread(&n, 4, 1, f) != 1) return false;
|
||||
v.resize(n);
|
||||
if (n > 0 && std::fread(v.data(), sizeof(T), n, f) != n) return false;
|
||||
return true;
|
||||
}
|
||||
|
||||
std::string sidecarPath(const std::string& ifc_path) {
|
||||
std::string p = ifc_path;
|
||||
while (!p.empty() && (p.back() == '/' || p.back() == '\\')) p.pop_back();
|
||||
auto slash = p.find_last_of("/\\");
|
||||
auto dot = p.find_last_of('.');
|
||||
std::string stem = (dot != std::string::npos &&
|
||||
(slash == std::string::npos || dot > slash))
|
||||
? p.substr(0, dot)
|
||||
: p;
|
||||
return stem + ".ifcview";
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
std::optional<StreamingSidecar> readSidecarMetadataOnly(const std::string& ifc_path) {
|
||||
const std::string path = sidecarPath(ifc_path);
|
||||
FILE* f = std::fopen(path.c_str(), "rb");
|
||||
if (!f) return std::nullopt;
|
||||
|
||||
auto fail = [&]() -> std::optional<StreamingSidecar> {
|
||||
std::fclose(f);
|
||||
return std::nullopt;
|
||||
};
|
||||
|
||||
SidecarHeaderRaw hdr;
|
||||
if (std::fread(&hdr, sizeof(hdr), 1, f) != 1) return fail();
|
||||
if (hdr.magic != SIDECAR_MAGIC) return fail();
|
||||
if (hdr.version != SIDECAR_VERSION) return fail();
|
||||
if (hdr.endian != SIDECAR_ENDIAN) return fail();
|
||||
|
||||
StreamingSidecar out;
|
||||
out.file_path = path;
|
||||
|
||||
// Vertex section: read count, record offset of data, seek past.
|
||||
uint32_t num_vertex_bytes = 0;
|
||||
if (std::fread(&num_vertex_bytes, 4, 1, f) != 1) return fail();
|
||||
out.vertex_section_offset = uint64_t(std::ftell(f));
|
||||
out.vertex_total_bytes = num_vertex_bytes;
|
||||
if (std::fseek(f, long(num_vertex_bytes), SEEK_CUR) != 0) return fail();
|
||||
|
||||
// Index section: same dance, in u32 units.
|
||||
uint32_t num_indices = 0;
|
||||
if (std::fread(&num_indices, 4, 1, f) != 1) return fail();
|
||||
out.index_section_offset = uint64_t(std::ftell(f));
|
||||
out.index_total_count = num_indices;
|
||||
if (std::fseek(f, long(num_indices) * 4, SEEK_CUR) != 0) return fail();
|
||||
|
||||
// Mesh dict + instance dict — small, load into meta.
|
||||
if (!readVec(f, out.meta.meshes)) return fail();
|
||||
if (!readVec(f, out.meta.instances)) return fail();
|
||||
|
||||
// v11 georef block (148 bytes total).
|
||||
if (std::fread(&out.meta.has_coordinate_operation, 4, 1, f) != 1) return fail();
|
||||
if (std::fread(out.meta.coordinate_operation_meters,
|
||||
sizeof(double), 16, f) != 16) return fail();
|
||||
if (std::fread(&out.meta.project_length_to_meters,
|
||||
sizeof(double), 1, f) != 1) return fail();
|
||||
if (std::fread(&out.meta.map_unit_to_meters,
|
||||
sizeof(double), 1, f) != 1) return fail();
|
||||
|
||||
// Element table + string table.
|
||||
if (!readVec(f, out.meta.elements)) return fail();
|
||||
uint32_t stbl_len = 0;
|
||||
if (std::fread(&stbl_len, 4, 1, f) != 1) return fail();
|
||||
out.meta.string_table.resize(stbl_len);
|
||||
if (stbl_len > 0 &&
|
||||
std::fread(out.meta.string_table.data(), 1, stbl_len, f) != stbl_len)
|
||||
return fail();
|
||||
|
||||
std::fclose(f);
|
||||
return out;
|
||||
}
|
||||
|
||||
bool readSidecarVertexChunk(const std::string& ifc_path,
|
||||
uint64_t vertex_section_offset,
|
||||
uint64_t chunk_byte_offset,
|
||||
uint64_t chunk_byte_size,
|
||||
std::vector<uint8_t>& out_bytes) {
|
||||
if (chunk_byte_size == 0) { out_bytes.clear(); return true; }
|
||||
|
||||
const std::string path = sidecarPath(ifc_path);
|
||||
FILE* f = std::fopen(path.c_str(), "rb");
|
||||
if (!f) return false;
|
||||
if (std::fseek(f, long(vertex_section_offset + chunk_byte_offset), SEEK_SET) != 0) {
|
||||
std::fclose(f);
|
||||
return false;
|
||||
}
|
||||
out_bytes.resize(size_t(chunk_byte_size));
|
||||
const size_t got = std::fread(out_bytes.data(), 1, size_t(chunk_byte_size), f);
|
||||
std::fclose(f);
|
||||
return got == size_t(chunk_byte_size);
|
||||
}
|
||||
|
||||
bool readSidecarIndexChunk(const std::string& ifc_path,
|
||||
uint64_t index_section_offset,
|
||||
uint64_t chunk_first_index,
|
||||
uint64_t chunk_index_count,
|
||||
std::vector<uint32_t>& out_indices) {
|
||||
if (chunk_index_count == 0) { out_indices.clear(); return true; }
|
||||
|
||||
const std::string path = sidecarPath(ifc_path);
|
||||
FILE* f = std::fopen(path.c_str(), "rb");
|
||||
if (!f) return false;
|
||||
const uint64_t byte_offset = index_section_offset + chunk_first_index * 4u;
|
||||
if (std::fseek(f, long(byte_offset), SEEK_SET) != 0) {
|
||||
std::fclose(f);
|
||||
return false;
|
||||
}
|
||||
out_indices.resize(size_t(chunk_index_count));
|
||||
const size_t got = std::fread(out_indices.data(), sizeof(uint32_t),
|
||||
size_t(chunk_index_count), f);
|
||||
std::fclose(f);
|
||||
return got == size_t(chunk_index_count);
|
||||
}
|
||||
|
||||
// Coalesce ranges that are close in file order into single reads. The
|
||||
// input order is preserved in the destination buffer; we just merge
|
||||
// reads on the file side. A `max_gap_bytes` tolerance lets us swallow
|
||||
// small file gaps when reading would be cheaper than seeking.
|
||||
//
|
||||
// SIDE EFFECT: callers must give the dst buffer in INPUT order; the
|
||||
// reader scatters bytes via per-input-range dst offsets after a single
|
||||
// coalesced fread. Returns false on any I/O failure.
|
||||
namespace {
|
||||
|
||||
struct ReadPlan {
|
||||
uint64_t file_offset; // absolute file offset
|
||||
uint64_t read_size; // total bytes to read
|
||||
// Per input range: where its bytes land in this read, and where to
|
||||
// copy them into the destination buffer.
|
||||
struct Slice {
|
||||
uint64_t src_offset; // offset within the read buffer
|
||||
uint64_t dst_offset; // offset within the destination buffer
|
||||
uint64_t bytes;
|
||||
};
|
||||
std::vector<Slice> slices;
|
||||
};
|
||||
|
||||
// Build a plan that merges adjacent file ranges into single reads.
|
||||
// `ranges` are (section-relative offset, size). `max_gap_bytes` is the
|
||||
// largest "wasted bytes" we'll read to bridge two ranges into one read.
|
||||
std::vector<ReadPlan> buildReadPlan(
|
||||
uint64_t section_offset,
|
||||
const std::vector<std::pair<uint64_t, uint64_t>>& ranges,
|
||||
uint64_t max_gap_bytes) {
|
||||
// Sort by file offset, remembering original order so we can scatter
|
||||
// to the destination correctly.
|
||||
struct Indexed { uint64_t off, size, dst; };
|
||||
std::vector<Indexed> sorted;
|
||||
sorted.reserve(ranges.size());
|
||||
uint64_t dst_cursor = 0;
|
||||
for (const auto& [off, sz] : ranges) {
|
||||
sorted.push_back({off, sz, dst_cursor});
|
||||
dst_cursor += sz;
|
||||
}
|
||||
std::sort(sorted.begin(), sorted.end(),
|
||||
[](const Indexed& a, const Indexed& b) { return a.off < b.off; });
|
||||
|
||||
std::vector<ReadPlan> plans;
|
||||
for (const auto& r : sorted) {
|
||||
if (r.size == 0) continue;
|
||||
if (!plans.empty()) {
|
||||
ReadPlan& back = plans.back();
|
||||
const uint64_t end_of_back = back.file_offset + back.read_size;
|
||||
const uint64_t r_file = section_offset + r.off;
|
||||
if (r_file >= end_of_back && r_file - end_of_back <= max_gap_bytes) {
|
||||
// Merge: extend the read to include r (plus any gap).
|
||||
const uint64_t new_size = (r_file + r.size) - back.file_offset;
|
||||
back.slices.push_back({
|
||||
r_file - back.file_offset, // src within read
|
||||
r.dst,
|
||||
r.size,
|
||||
});
|
||||
back.read_size = new_size;
|
||||
continue;
|
||||
}
|
||||
}
|
||||
ReadPlan np;
|
||||
np.file_offset = section_offset + r.off;
|
||||
np.read_size = r.size;
|
||||
np.slices.push_back({0, r.dst, r.size});
|
||||
plans.push_back(std::move(np));
|
||||
}
|
||||
return plans;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
bool readSidecarVertexRanges(const std::string& ifc_path,
|
||||
uint64_t vertex_section_offset,
|
||||
const std::vector<std::pair<uint64_t, uint64_t>>& ranges,
|
||||
std::vector<uint8_t>& out_bytes) {
|
||||
uint64_t total = 0;
|
||||
for (const auto& r : ranges) total += r.second;
|
||||
out_bytes.resize(size_t(total));
|
||||
if (total == 0) return true;
|
||||
|
||||
// 64 KB max gap: on SSDs a small contiguous read is much cheaper
|
||||
// than a seek + fresh read, even if some bytes are discarded.
|
||||
auto plans = buildReadPlan(vertex_section_offset, ranges, 64 * 1024);
|
||||
|
||||
const std::string path = sidecarPath(ifc_path);
|
||||
FILE* f = std::fopen(path.c_str(), "rb");
|
||||
if (!f) return false;
|
||||
|
||||
std::vector<uint8_t> scratch;
|
||||
for (const auto& p : plans) {
|
||||
scratch.resize(size_t(p.read_size));
|
||||
if (std::fseek(f, long(p.file_offset), SEEK_SET) != 0) { std::fclose(f); return false; }
|
||||
if (std::fread(scratch.data(), 1, scratch.size(), f) != scratch.size()) {
|
||||
std::fclose(f); return false;
|
||||
}
|
||||
for (const auto& s : p.slices) {
|
||||
std::memcpy(out_bytes.data() + s.dst_offset,
|
||||
scratch.data() + s.src_offset, size_t(s.bytes));
|
||||
}
|
||||
}
|
||||
std::fclose(f);
|
||||
return true;
|
||||
}
|
||||
|
||||
bool readSidecarIndexRanges(const std::string& ifc_path,
|
||||
uint64_t index_section_offset,
|
||||
const std::vector<std::pair<uint64_t, uint64_t>>& ranges,
|
||||
std::vector<uint32_t>& out_indices) {
|
||||
uint64_t total = 0;
|
||||
for (const auto& r : ranges) total += r.second;
|
||||
out_indices.resize(size_t(total));
|
||||
if (total == 0) return true;
|
||||
|
||||
// Convert u32-range (first_u32, count_u32) to byte-range
|
||||
// (file_offset, byte_size). Then coalesce + read.
|
||||
std::vector<std::pair<uint64_t, uint64_t>> byte_ranges;
|
||||
byte_ranges.reserve(ranges.size());
|
||||
uint64_t out_byte_cursor = 0;
|
||||
for (const auto& [first_u32, count] : ranges) {
|
||||
// Store byte offsets relative to the index section.
|
||||
byte_ranges.emplace_back(first_u32 * 4u, count * 4u);
|
||||
out_byte_cursor += count * 4u;
|
||||
}
|
||||
auto plans = buildReadPlan(index_section_offset, byte_ranges, 64 * 1024);
|
||||
|
||||
const std::string path = sidecarPath(ifc_path);
|
||||
FILE* f = std::fopen(path.c_str(), "rb");
|
||||
if (!f) return false;
|
||||
|
||||
std::vector<uint8_t> scratch;
|
||||
uint8_t* out_bytes = reinterpret_cast<uint8_t*>(out_indices.data());
|
||||
for (const auto& p : plans) {
|
||||
scratch.resize(size_t(p.read_size));
|
||||
if (std::fseek(f, long(p.file_offset), SEEK_SET) != 0) { std::fclose(f); return false; }
|
||||
if (std::fread(scratch.data(), 1, scratch.size(), f) != scratch.size()) {
|
||||
std::fclose(f); return false;
|
||||
}
|
||||
for (const auto& s : p.slices) {
|
||||
std::memcpy(out_bytes + s.dst_offset,
|
||||
scratch.data() + s.src_offset, size_t(s.bytes));
|
||||
}
|
||||
}
|
||||
std::fclose(f);
|
||||
return true;
|
||||
}
|
||||
@@ -0,0 +1,107 @@
|
||||
/********************************************************************************
|
||||
* *
|
||||
* 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 WGPUSTREAMINGLOADER_H
|
||||
#define WGPUSTREAMINGLOADER_H
|
||||
|
||||
#include "SidecarCache.h"
|
||||
|
||||
#include <cstdint>
|
||||
#include <optional>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
// Metadata-only sidecar load — the foundation for streaming. Reads the v13
|
||||
// header + mesh dict + instance dict + georef + element table from disk but
|
||||
// *skips* the bulky vertex and index byte sections, leaving file offsets +
|
||||
// sizes for later random-access reads.
|
||||
//
|
||||
// On a typical real-scene sidecar this returns in milliseconds even when the
|
||||
// full readSidecar would block on hundreds of MB of vertex bytes. Lets the
|
||||
// renderer set up cull / instance state immediately and load vertex chunks
|
||||
// on demand as they become frustum-visible.
|
||||
//
|
||||
// Backwards-compatible with v13 sidecars on disk (the format isn't changing
|
||||
// in this step — we're just reading less of it). v14 with an explicit
|
||||
// per-chunk TOC arrives in a follow-up; this layer abstracts the chunk
|
||||
// boundaries so the upgrade is internal.
|
||||
struct StreamingSidecar {
|
||||
// Everything except vertices + indices — same shape as SidecarData but
|
||||
// with empty vertices / indices vectors. The renderer uses meshes /
|
||||
// instances / georef / elements immediately.
|
||||
SidecarData meta;
|
||||
|
||||
// Byte offsets in the on-disk file where the vertex and index sections
|
||||
// start (after their 4-byte count headers). Pair with vertex_total_bytes
|
||||
// / index_total_bytes for the section length; per-chunk reads slice
|
||||
// arbitrary ranges within these.
|
||||
uint64_t vertex_section_offset = 0;
|
||||
uint64_t vertex_total_bytes = 0;
|
||||
uint64_t index_section_offset = 0;
|
||||
uint64_t index_total_count = 0; // u32 indices, NOT bytes
|
||||
|
||||
// Resolved on-disk path so subsequent chunk reads can re-open / seek.
|
||||
std::string file_path;
|
||||
};
|
||||
|
||||
// Read just the metadata + section offsets. Returns nullopt on any I/O or
|
||||
// version error (same failure modes as readSidecar). The file is closed
|
||||
// before return — callers re-open for per-chunk reads.
|
||||
std::optional<StreamingSidecar> readSidecarMetadataOnly(const std::string& ifc_path);
|
||||
|
||||
// Read a byte range from a sidecar's vertex section. `chunk_byte_offset` is
|
||||
// RELATIVE to vertex_section_offset (i.e. 0 = first vertex byte). Returns
|
||||
// false on I/O error or out-of-range request.
|
||||
//
|
||||
// Synchronous; intended to be called from a worker thread for async
|
||||
// streaming or from the main thread for stage-1 on-demand load.
|
||||
bool readSidecarVertexChunk(const std::string& ifc_path,
|
||||
uint64_t vertex_section_offset,
|
||||
uint64_t chunk_byte_offset,
|
||||
uint64_t chunk_byte_size,
|
||||
std::vector<uint8_t>& out_bytes);
|
||||
|
||||
// Read a u32-index range. `chunk_first_index` is RELATIVE to the start of
|
||||
// the index section (i.e. 0 = first u32 index). `chunk_index_count` is in
|
||||
// indices (multiply by 4 internally).
|
||||
bool readSidecarIndexChunk(const std::string& ifc_path,
|
||||
uint64_t index_section_offset,
|
||||
uint64_t chunk_first_index,
|
||||
uint64_t chunk_index_count,
|
||||
std::vector<uint32_t>& out_indices);
|
||||
|
||||
// Multi-range vertex read. `ranges` is a list of (section-relative
|
||||
// byte_offset, byte_size) tuples; their contents are concatenated into
|
||||
// out_bytes in input order. Single fopen across all ranges, so it's
|
||||
// far cheaper than calling readSidecarVertexChunk N times when a
|
||||
// spatially-grouped chunk needs to scatter-gather meshes that aren't
|
||||
// adjacent in the sidecar. out_bytes is resized to the total size.
|
||||
bool readSidecarVertexRanges(const std::string& ifc_path,
|
||||
uint64_t vertex_section_offset,
|
||||
const std::vector<std::pair<uint64_t, uint64_t>>& ranges,
|
||||
std::vector<uint8_t>& out_bytes);
|
||||
|
||||
// Same for the index section. Ranges are (first_u32, count_u32);
|
||||
// concatenated into out_indices in input order.
|
||||
bool readSidecarIndexRanges(const std::string& ifc_path,
|
||||
uint64_t index_section_offset,
|
||||
const std::vector<std::pair<uint64_t, uint64_t>>& ranges,
|
||||
std::vector<uint32_t>& out_indices);
|
||||
|
||||
#endif // WGPUSTREAMINGLOADER_H
|
||||
@@ -0,0 +1,122 @@
|
||||
/********************************************************************************
|
||||
* *
|
||||
* 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 "StreamingThread.h"
|
||||
|
||||
#include "StreamingLoader.h"
|
||||
|
||||
StreamingThread::~StreamingThread() {
|
||||
stop();
|
||||
}
|
||||
|
||||
void StreamingThread::start() {
|
||||
std::unique_lock lk(mu_);
|
||||
if (running_) return;
|
||||
shutdown_ = false;
|
||||
running_ = true;
|
||||
lk.unlock();
|
||||
worker_ = std::thread(&StreamingThread::workerLoop, this);
|
||||
}
|
||||
|
||||
void StreamingThread::stop() {
|
||||
{
|
||||
std::unique_lock lk(mu_);
|
||||
if (!running_) return;
|
||||
shutdown_ = true;
|
||||
}
|
||||
cv_.notify_all();
|
||||
if (worker_.joinable()) worker_.join();
|
||||
std::unique_lock lk(mu_);
|
||||
running_ = false;
|
||||
requests_.clear();
|
||||
results_.clear();
|
||||
}
|
||||
|
||||
bool StreamingThread::enqueue(Request req) {
|
||||
{
|
||||
std::unique_lock lk(mu_);
|
||||
if (!running_ || shutdown_) return false;
|
||||
requests_.push_back(std::move(req));
|
||||
}
|
||||
cv_.notify_one();
|
||||
return true;
|
||||
}
|
||||
|
||||
std::vector<StreamingThread::Result> StreamingThread::drainResults() {
|
||||
std::vector<Result> out;
|
||||
{
|
||||
std::unique_lock lk(mu_);
|
||||
out.reserve(results_.size());
|
||||
while (!results_.empty()) {
|
||||
out.push_back(std::move(results_.front()));
|
||||
results_.pop_front();
|
||||
}
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
std::size_t StreamingThread::inFlightApprox() const {
|
||||
std::unique_lock lk(mu_);
|
||||
return requests_.size() + (in_progress_ ? 1u : 0u);
|
||||
}
|
||||
|
||||
void StreamingThread::workerLoop() {
|
||||
for (;;) {
|
||||
Request req;
|
||||
{
|
||||
std::unique_lock lk(mu_);
|
||||
cv_.wait(lk, [this]() { return shutdown_ || !requests_.empty(); });
|
||||
if (shutdown_ && requests_.empty()) return;
|
||||
req = std::move(requests_.front());
|
||||
requests_.pop_front();
|
||||
in_progress_ = true;
|
||||
}
|
||||
|
||||
// Disk reads happen off-thread. Each Request carries everything
|
||||
// the reader needs; the viewport keeps the corresponding chunk
|
||||
// marked is_loading so eviction won't yank the slot underneath
|
||||
// us. The vbytes / idx buffers are allocated here on the worker
|
||||
// thread — they cross back to the main thread when the result
|
||||
// is drained and applied (pool.alloc + queueWriteBuffer).
|
||||
Result res;
|
||||
res.model_id = req.model_id;
|
||||
res.chunk_idx = req.chunk_idx;
|
||||
res.success = true;
|
||||
if (!req.v_ranges.empty()) {
|
||||
if (!readSidecarVertexRanges(req.file_path,
|
||||
req.vertex_section_offset,
|
||||
req.v_ranges, res.vbytes)) {
|
||||
res.success = false;
|
||||
}
|
||||
}
|
||||
if (res.success && !req.i_ranges.empty()) {
|
||||
if (!readSidecarIndexRanges(req.file_path,
|
||||
req.index_section_offset,
|
||||
req.i_ranges, res.idx)) {
|
||||
res.success = false;
|
||||
}
|
||||
}
|
||||
|
||||
{
|
||||
std::unique_lock lk(mu_);
|
||||
results_.push_back(std::move(res));
|
||||
in_progress_ = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,100 @@
|
||||
/********************************************************************************
|
||||
* *
|
||||
* 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 WGPUSTREAMINGTHREAD_H
|
||||
#define WGPUSTREAMINGTHREAD_H
|
||||
|
||||
#include <condition_variable>
|
||||
#include <cstdint>
|
||||
#include <deque>
|
||||
#include <mutex>
|
||||
#include <string>
|
||||
#include <thread>
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
|
||||
// Worker thread for scatter-gather chunk reads. Decouples disk I/O
|
||||
// (~tens of ms per chunk on SSD, hundreds on slower media) from the
|
||||
// render thread. The viewport's per-frame driveStreamingLoads enqueues
|
||||
// requests for non-resident-frustum-visible chunks, drains any
|
||||
// completed Results on subsequent frames, and only performs the
|
||||
// GPU-side (pool.alloc + queueWriteBuffer + bind-group build) work
|
||||
// on the main thread — wgpu queue ops aren't thread-safe.
|
||||
//
|
||||
// Lifetime: start() spawns the worker; stop() signals shutdown and
|
||||
// joins. The Result destructor releases its byte vectors back to the
|
||||
// heap, so dropping unclaimed Results (e.g. when their model was
|
||||
// unloaded mid-flight) is a free operation.
|
||||
class StreamingThread {
|
||||
public:
|
||||
struct Request {
|
||||
uint32_t model_id;
|
||||
std::size_t chunk_idx;
|
||||
std::string file_path;
|
||||
uint64_t vertex_section_offset;
|
||||
uint64_t index_section_offset;
|
||||
// (section-relative byte_offset, byte_size)
|
||||
std::vector<std::pair<uint64_t, uint64_t>> v_ranges;
|
||||
// (first_u32, count_u32)
|
||||
std::vector<std::pair<uint64_t, uint64_t>> i_ranges;
|
||||
};
|
||||
|
||||
struct Result {
|
||||
uint32_t model_id;
|
||||
std::size_t chunk_idx;
|
||||
bool success;
|
||||
std::vector<uint8_t> vbytes;
|
||||
std::vector<uint32_t> idx;
|
||||
};
|
||||
|
||||
~StreamingThread();
|
||||
|
||||
// Spawn the worker thread. Safe to call once; subsequent calls are
|
||||
// no-ops while the worker is alive.
|
||||
void start();
|
||||
// Signal shutdown, wake the worker, join. Idempotent. Must be
|
||||
// called before the BufferPool the results would upload into
|
||||
// is destroyed.
|
||||
void stop();
|
||||
|
||||
// Enqueue a request. Returns false if the worker has stopped.
|
||||
bool enqueue(Request req);
|
||||
// Move all completed results out of the result queue. Always
|
||||
// non-blocking; if nothing is ready, returns an empty vector.
|
||||
std::vector<Result> drainResults();
|
||||
|
||||
// Approximate count of requests still in flight (in queue or
|
||||
// currently being processed). Useful for the bench warm gate to
|
||||
// know when streaming has truly settled.
|
||||
std::size_t inFlightApprox() const;
|
||||
|
||||
private:
|
||||
void workerLoop();
|
||||
|
||||
std::thread worker_;
|
||||
mutable std::mutex mu_;
|
||||
std::condition_variable cv_;
|
||||
std::deque<Request> requests_;
|
||||
std::deque<Result> results_;
|
||||
bool in_progress_ = false;
|
||||
bool shutdown_ = false;
|
||||
bool running_ = false;
|
||||
};
|
||||
|
||||
#endif // WGPUSTREAMINGTHREAD_H
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,59 @@
|
||||
/********************************************************************************
|
||||
* *
|
||||
* 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 WGPUVISIBILITYSTATE_H
|
||||
#define WGPUVISIBILITYSTATE_H
|
||||
|
||||
#include <cstdint>
|
||||
#include <unordered_set>
|
||||
|
||||
// CPU-side per-element visibility. Mirrors src/ifcviewer/Visibility.h shape
|
||||
// but pure stdlib so it can move into ifcviewer-core later.
|
||||
//
|
||||
// Consulted in cullModelCpuCompute: instances whose object_id is in
|
||||
// hidden_ids_ are dropped from the visible-draws list entirely (no GPU
|
||||
// work, no triangle in the depth buffer, no pick hit). Concurrent reads
|
||||
// from multiple cull worker threads are safe as long as no mutations
|
||||
// happen during render — which is the case here (input handlers
|
||||
// requestUpdate after mutating, render then reads).
|
||||
class VisibilityState {
|
||||
public:
|
||||
bool isHidden(uint32_t object_id) const {
|
||||
return hidden_ids_.count(object_id) > 0;
|
||||
}
|
||||
|
||||
void hide(uint32_t object_id) {
|
||||
if (object_id == 0) return;
|
||||
hidden_ids_.insert(object_id);
|
||||
}
|
||||
|
||||
void show(uint32_t object_id) {
|
||||
hidden_ids_.erase(object_id);
|
||||
}
|
||||
|
||||
void clear() { hidden_ids_.clear(); }
|
||||
|
||||
size_t hiddenCount() const { return hidden_ids_.size(); }
|
||||
const std::unordered_set<uint32_t>& hiddenIds() const { return hidden_ids_; }
|
||||
|
||||
private:
|
||||
std::unordered_set<uint32_t> hidden_ids_;
|
||||
};
|
||||
|
||||
#endif // WGPUVISIBILITYSTATE_H
|
||||
@@ -52,6 +52,11 @@ add_ifcviewer_unit_test(test_sidecar_cache
|
||||
|
||||
add_ifcviewer_unit_test(test_instanced_geometry)
|
||||
|
||||
# Header-only state-machine tests for the renderer subsystems (selection,
|
||||
# visibility). Subjects are inline in their .h files, so no SOURCES needed.
|
||||
add_ifcviewer_unit_test(test_selection)
|
||||
add_ifcviewer_unit_test(test_visibility)
|
||||
|
||||
# Federation is Qt-derived (QObject + signals). It has to pull Qt6 in
|
||||
# directly and enable AUTOMOC for the Q_OBJECT moc-generation.
|
||||
find_package(Qt${QT_VERSION} COMPONENTS Core Gui Test REQUIRED PATHS ${QT_DIR})
|
||||
|
||||
@@ -0,0 +1,256 @@
|
||||
/********************************************************************************
|
||||
* *
|
||||
* 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/>. *
|
||||
* *
|
||||
********************************************************************************/
|
||||
|
||||
// Tier-1 coverage of SelectionState — the CPU-side selection set + active
|
||||
// id used by the wgpu viewport. The class is pure stdlib (no Qt, no QObject),
|
||||
// so the test exercises the state machine directly. The GPU-side flags SSBO
|
||||
// is filled via fillFlagsArray; that pure-function path is also covered.
|
||||
|
||||
#include "SelectionState.h"
|
||||
|
||||
#include <catch2/catch_test_macros.hpp>
|
||||
|
||||
#include <vector>
|
||||
|
||||
TEST_CASE("SelectionState starts empty with no active id", "[wgpu-selection]") {
|
||||
SelectionState sel;
|
||||
REQUIRE(sel.count() == 0);
|
||||
REQUIRE(sel.activeId() == 0);
|
||||
REQUIRE_FALSE(sel.contains(1));
|
||||
REQUIRE_FALSE(sel.dirty());
|
||||
}
|
||||
|
||||
TEST_CASE("replace(id) selects a single id and makes it active",
|
||||
"[wgpu-selection]") {
|
||||
SelectionState sel;
|
||||
|
||||
sel.replace(5);
|
||||
REQUIRE(sel.count() == 1);
|
||||
REQUIRE(sel.contains(5));
|
||||
REQUIRE(sel.activeId() == 5);
|
||||
REQUIRE(sel.dirty());
|
||||
}
|
||||
|
||||
TEST_CASE("replace(0) clears the selection", "[wgpu-selection]") {
|
||||
SelectionState sel;
|
||||
sel.replace(5);
|
||||
sel.markClean();
|
||||
|
||||
sel.replace(0);
|
||||
REQUIRE(sel.count() == 0);
|
||||
REQUIRE(sel.activeId() == 0);
|
||||
REQUIRE(sel.dirty());
|
||||
}
|
||||
|
||||
TEST_CASE("add(id) appends to the set and steals active", "[wgpu-selection]") {
|
||||
SelectionState sel;
|
||||
sel.replace(1);
|
||||
sel.markClean();
|
||||
|
||||
sel.add(2);
|
||||
REQUIRE(sel.count() == 2);
|
||||
REQUIRE(sel.contains(1));
|
||||
REQUIRE(sel.contains(2));
|
||||
// Each click should drive the properties panel to the most recently
|
||||
// touched object, so active follows the last add — distinct from GL's
|
||||
// addToSelection (which kept the prior active).
|
||||
REQUIRE(sel.activeId() == 2);
|
||||
REQUIRE(sel.dirty());
|
||||
}
|
||||
|
||||
TEST_CASE("add(0) is ignored", "[wgpu-selection]") {
|
||||
SelectionState sel;
|
||||
sel.replace(1);
|
||||
sel.markClean();
|
||||
|
||||
sel.add(0);
|
||||
REQUIRE(sel.count() == 1);
|
||||
REQUIRE(sel.activeId() == 1);
|
||||
REQUIRE_FALSE(sel.dirty()); // no-op didn't flip the flag
|
||||
}
|
||||
|
||||
TEST_CASE("remove(non-active) keeps active", "[wgpu-selection]") {
|
||||
SelectionState sel;
|
||||
sel.replace(1);
|
||||
sel.add(2);
|
||||
sel.add(3);
|
||||
REQUIRE(sel.activeId() == 3);
|
||||
sel.markClean();
|
||||
|
||||
sel.remove(1);
|
||||
REQUIRE(sel.count() == 2);
|
||||
REQUIRE_FALSE(sel.contains(1));
|
||||
REQUIRE(sel.activeId() == 3); // still the most recently touched
|
||||
REQUIRE(sel.dirty());
|
||||
}
|
||||
|
||||
TEST_CASE("remove(active) falls back to some remaining id", "[wgpu-selection]") {
|
||||
SelectionState sel;
|
||||
sel.replace(1);
|
||||
sel.add(2);
|
||||
sel.add(3);
|
||||
REQUIRE(sel.activeId() == 3);
|
||||
|
||||
sel.remove(3);
|
||||
REQUIRE(sel.count() == 2);
|
||||
REQUIRE_FALSE(sel.contains(3));
|
||||
// Active falls back to *some* remaining id (implementation picks the
|
||||
// unordered_set's first element; documenting non-determinism rather
|
||||
// than the specific choice).
|
||||
const uint32_t a = sel.activeId();
|
||||
REQUIRE((a == 1 || a == 2));
|
||||
REQUIRE(sel.contains(a));
|
||||
}
|
||||
|
||||
TEST_CASE("remove(last id) clears active", "[wgpu-selection]") {
|
||||
SelectionState sel;
|
||||
sel.replace(7);
|
||||
REQUIRE(sel.activeId() == 7);
|
||||
|
||||
sel.remove(7);
|
||||
REQUIRE(sel.count() == 0);
|
||||
REQUIRE(sel.activeId() == 0);
|
||||
}
|
||||
|
||||
TEST_CASE("remove(non-existent) is a no-op for state, no dirty flag",
|
||||
"[wgpu-selection]") {
|
||||
SelectionState sel;
|
||||
sel.replace(1);
|
||||
sel.markClean();
|
||||
|
||||
sel.remove(99);
|
||||
REQUIRE(sel.count() == 1);
|
||||
REQUIRE(sel.activeId() == 1);
|
||||
REQUIRE_FALSE(sel.dirty());
|
||||
}
|
||||
|
||||
TEST_CASE("remove(0) is ignored", "[wgpu-selection]") {
|
||||
SelectionState sel;
|
||||
sel.replace(1);
|
||||
sel.markClean();
|
||||
|
||||
sel.remove(0);
|
||||
REQUIRE(sel.count() == 1);
|
||||
REQUIRE_FALSE(sel.dirty());
|
||||
}
|
||||
|
||||
TEST_CASE("toggle adds when absent, removes when present", "[wgpu-selection]") {
|
||||
SelectionState sel;
|
||||
|
||||
sel.toggle(5);
|
||||
REQUIRE(sel.contains(5));
|
||||
REQUIRE(sel.activeId() == 5);
|
||||
|
||||
sel.toggle(6);
|
||||
REQUIRE(sel.contains(6));
|
||||
REQUIRE(sel.activeId() == 6); // last add steals active
|
||||
|
||||
sel.toggle(5); // remove non-active — active unchanged
|
||||
REQUIRE_FALSE(sel.contains(5));
|
||||
REQUIRE(sel.activeId() == 6);
|
||||
|
||||
sel.toggle(6); // remove active — fallback (set empty → 0)
|
||||
REQUIRE(sel.count() == 0);
|
||||
REQUIRE(sel.activeId() == 0);
|
||||
}
|
||||
|
||||
TEST_CASE("toggle(0) is ignored", "[wgpu-selection]") {
|
||||
SelectionState sel;
|
||||
sel.markClean();
|
||||
|
||||
sel.toggle(0);
|
||||
REQUIRE(sel.count() == 0);
|
||||
REQUIRE_FALSE(sel.dirty());
|
||||
}
|
||||
|
||||
TEST_CASE("clear empties; no-op when already empty", "[wgpu-selection]") {
|
||||
SelectionState sel;
|
||||
|
||||
sel.clear(); // already empty
|
||||
REQUIRE_FALSE(sel.dirty());
|
||||
|
||||
sel.replace(1);
|
||||
sel.markClean();
|
||||
sel.clear();
|
||||
REQUIRE(sel.count() == 0);
|
||||
REQUIRE(sel.activeId() == 0);
|
||||
REQUIRE(sel.dirty());
|
||||
}
|
||||
|
||||
TEST_CASE("markClean clears the dirty flag", "[wgpu-selection]") {
|
||||
SelectionState sel;
|
||||
sel.replace(5);
|
||||
REQUIRE(sel.dirty());
|
||||
|
||||
sel.markClean();
|
||||
REQUIRE_FALSE(sel.dirty());
|
||||
|
||||
// Subsequent mutation re-arms the flag.
|
||||
sel.add(6);
|
||||
REQUIRE(sel.dirty());
|
||||
}
|
||||
|
||||
TEST_CASE("selectionIds returns the live set", "[wgpu-selection]") {
|
||||
SelectionState sel;
|
||||
sel.add(1);
|
||||
sel.add(2);
|
||||
sel.add(3);
|
||||
|
||||
const auto& ids = sel.selectionIds();
|
||||
REQUIRE(ids.size() == 3);
|
||||
REQUIRE(ids.count(1) == 1);
|
||||
REQUIRE(ids.count(2) == 1);
|
||||
REQUIRE(ids.count(3) == 1);
|
||||
}
|
||||
|
||||
TEST_CASE("fillFlagsArray packs selected/active bits per object_id",
|
||||
"[wgpu-selection]") {
|
||||
SelectionState sel;
|
||||
sel.add(1);
|
||||
sel.add(3); // active is now 3
|
||||
|
||||
std::vector<uint32_t> flags;
|
||||
sel.fillFlagsArray(flags, 8);
|
||||
|
||||
REQUIRE(flags.size() == 8);
|
||||
REQUIRE(flags[0] == 0u); // sentinel
|
||||
REQUIRE(flags[1] == 1u); // selected, not active
|
||||
REQUIRE(flags[2] == 0u);
|
||||
REQUIRE(flags[3] == (1u | 2u)); // selected + active
|
||||
REQUIRE(flags[4] == 0u);
|
||||
REQUIRE(flags[5] == 0u);
|
||||
REQUIRE(flags[6] == 0u);
|
||||
REQUIRE(flags[7] == 0u);
|
||||
}
|
||||
|
||||
TEST_CASE("fillFlagsArray drops ids past the entries cap",
|
||||
"[wgpu-selection]") {
|
||||
SelectionState sel;
|
||||
sel.add(1);
|
||||
sel.add(100); // active is 100
|
||||
|
||||
std::vector<uint32_t> flags;
|
||||
sel.fillFlagsArray(flags, 4);
|
||||
|
||||
REQUIRE(flags.size() == 4);
|
||||
REQUIRE(flags[1] == 1u);
|
||||
// id 100 is out of range — should not write to flags[2]/[3]/etc.
|
||||
REQUIRE(flags[2] == 0u);
|
||||
REQUIRE(flags[3] == 0u);
|
||||
}
|
||||
@@ -0,0 +1,118 @@
|
||||
/********************************************************************************
|
||||
* *
|
||||
* 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/>. *
|
||||
* *
|
||||
********************************************************************************/
|
||||
|
||||
// Tier-1 coverage of VisibilityState — the per-element hidden-id set
|
||||
// consulted in cull. The class is pure stdlib; this test exercises its
|
||||
// primitives directly. Bulk hide/isolate/show-all semantics live in
|
||||
// ViewportWindow (which composes VisibilityState + the model
|
||||
// instance lists) and would need an integration test, not a Tier-1 unit.
|
||||
|
||||
#include "VisibilityState.h"
|
||||
|
||||
#include <catch2/catch_test_macros.hpp>
|
||||
|
||||
TEST_CASE("VisibilityState starts empty", "[wgpu-visibility]") {
|
||||
VisibilityState vis;
|
||||
REQUIRE(vis.hiddenCount() == 0);
|
||||
REQUIRE_FALSE(vis.isHidden(0)); // 0 is the "no object" sentinel
|
||||
REQUIRE_FALSE(vis.isHidden(1));
|
||||
REQUIRE_FALSE(vis.isHidden(1u << 20));
|
||||
}
|
||||
|
||||
TEST_CASE("hide(id) records the id; isHidden reflects it",
|
||||
"[wgpu-visibility]") {
|
||||
VisibilityState vis;
|
||||
|
||||
vis.hide(1);
|
||||
vis.hide(2);
|
||||
vis.hide(3);
|
||||
|
||||
REQUIRE(vis.hiddenCount() == 3);
|
||||
REQUIRE(vis.isHidden(1));
|
||||
REQUIRE(vis.isHidden(2));
|
||||
REQUIRE(vis.isHidden(3));
|
||||
REQUIRE_FALSE(vis.isHidden(4));
|
||||
}
|
||||
|
||||
TEST_CASE("hide is idempotent", "[wgpu-visibility]") {
|
||||
VisibilityState vis;
|
||||
|
||||
vis.hide(5);
|
||||
vis.hide(5);
|
||||
vis.hide(5);
|
||||
|
||||
REQUIRE(vis.hiddenCount() == 1);
|
||||
REQUIRE(vis.isHidden(5));
|
||||
}
|
||||
|
||||
TEST_CASE("hide(0) is ignored", "[wgpu-visibility]") {
|
||||
VisibilityState vis;
|
||||
|
||||
vis.hide(0);
|
||||
REQUIRE(vis.hiddenCount() == 0);
|
||||
REQUIRE_FALSE(vis.isHidden(0));
|
||||
}
|
||||
|
||||
TEST_CASE("show(id) removes a previously hidden id",
|
||||
"[wgpu-visibility]") {
|
||||
VisibilityState vis;
|
||||
vis.hide(1);
|
||||
vis.hide(2);
|
||||
|
||||
vis.show(1);
|
||||
REQUIRE(vis.hiddenCount() == 1);
|
||||
REQUIRE_FALSE(vis.isHidden(1));
|
||||
REQUIRE(vis.isHidden(2));
|
||||
}
|
||||
|
||||
TEST_CASE("show(non-hidden) is a no-op", "[wgpu-visibility]") {
|
||||
VisibilityState vis;
|
||||
vis.hide(1);
|
||||
|
||||
vis.show(99); // never hidden
|
||||
REQUIRE(vis.hiddenCount() == 1);
|
||||
REQUIRE(vis.isHidden(1));
|
||||
}
|
||||
|
||||
TEST_CASE("clear() drops every hidden id", "[wgpu-visibility]") {
|
||||
VisibilityState vis;
|
||||
vis.hide(1);
|
||||
vis.hide(2);
|
||||
vis.hide(3);
|
||||
|
||||
vis.clear();
|
||||
REQUIRE(vis.hiddenCount() == 0);
|
||||
REQUIRE_FALSE(vis.isHidden(1));
|
||||
REQUIRE_FALSE(vis.isHidden(2));
|
||||
REQUIRE_FALSE(vis.isHidden(3));
|
||||
}
|
||||
|
||||
TEST_CASE("hiddenIds returns the live set", "[wgpu-visibility]") {
|
||||
VisibilityState vis;
|
||||
vis.hide(10);
|
||||
vis.hide(20);
|
||||
vis.hide(30);
|
||||
|
||||
const auto& ids = vis.hiddenIds();
|
||||
REQUIRE(ids.size() == 3);
|
||||
REQUIRE(ids.count(10) == 1);
|
||||
REQUIRE(ids.count(20) == 1);
|
||||
REQUIRE(ids.count(30) == 1);
|
||||
REQUIRE(ids.count(40) == 0);
|
||||
}
|
||||
Reference in New Issue
Block a user