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:
Dion Moult
2026-06-04 11:11:14 +10:00
parent 7f87408b78
commit 8ab5c31e75
48 changed files with 705 additions and 815 deletions
+3 -4
View File
@@ -134,11 +134,10 @@ if(APPLE)
endif()
target_link_libraries(BonsaiViewer PRIVATE
# IfcViewer still ships SceneLoader / Federation / GeometryStreamer /
# SidecarBuilder until the GL ViewportWindow is deleted. IfcViewerWgpu
# ships the live render path. Both link cleanly side-by-side.
# IfcViewer is the unified scene + render lib (SceneLoader, Federation,
# GeometryStreamer, SidecarBuilder, ViewportWindow, OverlayRenderer,
# BufferPool, …) since the wgpu/ifcviewer merge.
IfcViewer
IfcViewerWgpu
Qt${QT_VERSION}::Core
Qt${QT_VERSION}::CorePrivate
Qt${QT_VERSION}::Gui
+4 -4
View File
@@ -22,7 +22,7 @@
#include "../ifcviewer/AppSettings.h"
#include "../ifcviewer/Federation.h"
#include "../ifcviewer-wgpu/WgpuViewportWindow.h"
#include "../ifcviewer/ViewportWindow.h"
#include "SessionState.h"
#include "components/Buttons.h"
#include "components/Panel.h"
@@ -529,8 +529,8 @@ void MainWindow::setupLoader() {
QMessageBox::warning(this, "Bonsai Viewer", message);
});
connect(viewport_widget_->viewport(), &WgpuViewportWindow::frameStatsUpdated, this,
[this](const WgpuViewportWindow::FrameStats& s) {
connect(viewport_widget_->viewport(), &ViewportWindow::frameStatsUpdated, this,
[this](const ViewportWindow::FrameStats& s) {
if (!status_perf_label_->isVisible()) return;
status_perf_label_->setText(
QString("%1 fps | %2 ms | %3/%4 obj | %5/%6 tri | %7 draws")
@@ -542,7 +542,7 @@ void MainWindow::setupLoader() {
.arg(s.total_triangles)
.arg(s.gl_draw_calls));
});
connect(viewport_widget_->viewport(), &WgpuViewportWindow::objectPicked,
connect(viewport_widget_->viewport(), &ViewportWindow::objectPicked,
this, [this](uint32_t object_id) {
session_state_->setSelectedObjectId(object_id);
session_state_->notifySelectionChanged();
+33 -33
View File
@@ -20,7 +20,7 @@
#include "Measurement.h"
#include "WgpuViewportWindow.h"
#include "ViewportWindow.h"
#include <QtGlobal>
#include <Eigen/Dense>
@@ -35,7 +35,7 @@
namespace {
double meshLocalVolume(const WgpuViewportWindow::MeshTriangles& tris) {
double meshLocalVolume(const ViewportWindow::MeshTriangles& tris) {
// Signed tetrahedra from the origin: V = sum( a · (b × c) ) / 6.
// Absolute value at the end so winding convention doesn't matter.
double sum = 0.0;
@@ -67,7 +67,7 @@ double det3(const double M[16]) {
} // namespace
double volumeOfObjects(WgpuViewportWindow& vp,
double volumeOfObjects(ViewportWindow& vp,
const std::vector<uint32_t>& object_ids) {
if (object_ids.empty()) return 0.0;
@@ -77,14 +77,14 @@ double volumeOfObjects(WgpuViewportWindow& vp,
std::unordered_map<uint64_t, std::vector<double>> by_mesh;
by_mesh.reserve(object_ids.size());
for (uint32_t oid : object_ids) {
WgpuViewportWindow::InstanceLookup lk;
ViewportWindow::InstanceLookup lk;
if (!vp.findInstance(oid, lk)) continue;
const uint64_t key = (uint64_t(lk.model_id) << 32) | lk.mesh_id;
by_mesh[key].push_back(std::abs(det3(lk.placement_transformation)));
}
double total = 0.0;
WgpuViewportWindow::MeshTriangles tris;
ViewportWindow::MeshTriangles tris;
for (const auto& [key, dets] : by_mesh) {
const uint32_t model_id = uint32_t(key >> 32);
const uint32_t mesh_id = uint32_t(key & 0xffffffffu);
@@ -96,7 +96,7 @@ double volumeOfObjects(WgpuViewportWindow& vp,
}
std::vector<std::pair<uint32_t, double>>
volumesPerObject(WgpuViewportWindow& vp,
volumesPerObject(ViewportWindow& vp,
const std::vector<uint32_t>& object_ids) {
std::vector<std::pair<uint32_t, double>> out;
if (object_ids.empty()) return out;
@@ -108,9 +108,9 @@ volumesPerObject(WgpuViewportWindow& vp,
std::unordered_map<uint64_t, double> mesh_vol_local;
mesh_vol_local.reserve(object_ids.size());
WgpuViewportWindow::MeshTriangles tris;
ViewportWindow::MeshTriangles tris;
for (uint32_t oid : object_ids) {
WgpuViewportWindow::InstanceLookup lk;
ViewportWindow::InstanceLookup lk;
if (!vp.findInstance(oid, lk)) continue;
const uint64_t key = (uint64_t(lk.model_id) << 32) | lk.mesh_id;
@@ -238,7 +238,7 @@ constexpr double kCoplanarDot = 0.9999; // ~0.81° tolerance
AreaMeasurement::AreaMeasurement() = default;
void AreaMeasurement::clear(WgpuViewportWindow& vp) {
void AreaMeasurement::clear(ViewportWindow& vp) {
mesh_cache_.clear();
selected_.clear();
total_area_m2_ = 0.0;
@@ -246,7 +246,7 @@ void AreaMeasurement::clear(WgpuViewportWindow& vp) {
vp.setOverlayLabels({});
}
void AreaMeasurement::rebuildHighlight(WgpuViewportWindow& vp) {
void AreaMeasurement::rebuildHighlight(ViewportWindow& vp) {
// Push every selected triangle's three world-space vertices to the
// overlay. Mesh-local positions × per-instance composed transform.
std::vector<float> world_xyz;
@@ -287,7 +287,7 @@ void AreaMeasurement::rebuildHighlight(WgpuViewportWindow& vp) {
by_object[object_id].push_back(&sel);
}
std::vector<WgpuOverlayRenderer::Label> labels;
std::vector<OverlayRenderer::Label> labels;
for (const auto& [obj_id, sels] : by_object) {
if (sels.empty()) continue;
// All tris belonging to one object share its mesh + transform.
@@ -350,7 +350,7 @@ void AreaMeasurement::rebuildHighlight(WgpuViewportWindow& vp) {
// Centroid → world via the instance's composed transform.
const float* M = any.composed_transform;
WgpuOverlayRenderer::Label lbl;
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]);
@@ -361,14 +361,14 @@ void AreaMeasurement::rebuildHighlight(WgpuViewportWindow& vp) {
vp.setOverlayLabels(labels);
}
AreaMeasurement::MeshCache* AreaMeasurement::meshCache(WgpuViewportWindow& vp,
AreaMeasurement::MeshCache* AreaMeasurement::meshCache(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;
WgpuViewportWindow::MeshTriangles tris;
ViewportWindow::MeshTriangles tris;
if (!vp.readbackMeshTriangles(model_id, mesh_id, tris)) return nullptr;
MeshCache c;
@@ -397,8 +397,8 @@ AreaMeasurement::MeshCache* AreaMeasurement::meshCache(WgpuViewportWindow& vp,
return &mesh_cache_.emplace(key, std::move(c)).first->second;
}
void AreaMeasurement::onPick(WgpuViewportWindow& vp, int x, int y, bool alt) {
WgpuViewportWindow::MeshLocalPick pick;
void AreaMeasurement::onPick(ViewportWindow& vp, int x, int y, bool alt) {
ViewportWindow::MeshLocalPick pick;
if (!vp.pickMeshLocalAt(x, y, pick)) return;
MeshCache* cache = meshCache(vp, pick.model_id, pick.mesh_id);
@@ -607,10 +607,10 @@ constexpr float DOT_HALO = 1.0f;
constexpr float DASH_PERIOD = 9.0f; // px
constexpr float DASH_ON_RATIO = 0.55f; // 5 on, 4 off
WgpuOverlayRenderer::LineGroup makeGroup(std::vector<float> xyz,
OverlayRenderer::LineGroup makeGroup(std::vector<float> xyz,
float r, float g, float b,
bool dashed = false) {
WgpuOverlayRenderer::LineGroup gp;
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;
@@ -635,10 +635,10 @@ void pushSeg(std::vector<float>& xyz,
xyz.insert(xyz.end(), b.begin(), b.end());
}
WgpuOverlayRenderer::Label makeLabel(const std::array<float, 3>& a,
OverlayRenderer::Label makeLabel(const std::array<float, 3>& a,
const std::array<float, 3>& b,
const QString& text) {
WgpuOverlayRenderer::Label lbl;
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]);
@@ -646,7 +646,7 @@ WgpuOverlayRenderer::Label makeLabel(const std::array<float, 3>& a,
return lbl;
}
void pushDots(WgpuViewportWindow& vp, const std::vector<float>& xyz) {
void pushDots(ViewportWindow& vp, const std::vector<float>& xyz) {
vp.setOverlayPoints(xyz,
/*inner*/ 1.0f, 1.0f, 1.0f, 1.0f,
/*size*/ DOT_SIZE,
@@ -656,7 +656,7 @@ void pushDots(WgpuViewportWindow& vp, const std::vector<float>& xyz) {
} // namespace
void LengthMeasurement::clear(WgpuViewportWindow& vp) {
void LengthMeasurement::clear(ViewportWindow& vp) {
points_.clear();
normals_.clear();
vp.setOverlayPoints({}, 0,0,0,0, 0, 0,0,0,0, 0);
@@ -665,8 +665,8 @@ void LengthMeasurement::clear(WgpuViewportWindow& vp) {
vp.setHudText(QString());
}
void LengthMeasurement::onPick(WgpuViewportWindow& vp, int x, int y, bool /*alt*/) {
WgpuViewportWindow::MeshLocalPick pick;
void LengthMeasurement::onPick(ViewportWindow& vp, int x, int y, bool /*alt*/) {
ViewportWindow::MeshLocalPick pick;
if (!vp.pickMeshLocalAt(x, y, 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]});
@@ -676,14 +676,14 @@ void LengthMeasurement::onPick(WgpuViewportWindow& vp, int x, int y, bool /*alt*
rebuildOverlay(vp);
}
void LengthMeasurement::removeLastPoint(WgpuViewportWindow& vp) {
void LengthMeasurement::removeLastPoint(ViewportWindow& vp) {
if (points_.empty()) return;
points_.pop_back();
if (!normals_.empty()) normals_.pop_back();
rebuildOverlay(vp);
}
void LengthMeasurement::rebuildOverlay(WgpuViewportWindow& vp) {
void LengthMeasurement::rebuildOverlay(ViewportWindow& vp) {
if (points_.size() == 1 && normals_.size() == 1) {
rebuildLaserOverlay(vp);
return;
@@ -694,8 +694,8 @@ void LengthMeasurement::rebuildOverlay(WgpuViewportWindow& vp) {
for (const auto& p : points_) pushDot(pts_xyz, p);
pushDots(vp, pts_xyz);
std::vector<WgpuOverlayRenderer::LineGroup> groups;
std::vector<WgpuOverlayRenderer::Label> labels;
std::vector<OverlayRenderer::LineGroup> groups;
std::vector<OverlayRenderer::Label> labels;
const size_t n = points_.size();
if (n == 2) {
@@ -823,7 +823,7 @@ const char* dominantAxisLabel(const float v[3]) {
} // namespace
void LengthMeasurement::rebuildLaserOverlay(WgpuViewportWindow& vp) {
void LengthMeasurement::rebuildLaserOverlay(ViewportWindow& vp) {
const auto& wp = first_pick_.world_pos; // float[3] world click
const auto& n = first_pick_.world_normal; // float[3] world normal
@@ -855,8 +855,8 @@ void LengthMeasurement::rebuildLaserOverlay(WgpuViewportWindow& vp) {
n[0]*t1[1] - n[1]*t1[0],
};
std::vector<WgpuOverlayRenderer::LineGroup> groups;
std::vector<WgpuOverlayRenderer::Label> labels;
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};
@@ -873,7 +873,7 @@ void LengthMeasurement::rebuildLaserOverlay(WgpuViewportWindow& vp) {
// co-normal neighbours, then project each patch vertex into the
// (t1, t2) basis to get the bounding extent of the face. Stops
// exactly at the face edge (no overshoot into adjacent geometry).
WgpuViewportWindow::MeshTriangles tris;
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)) {
@@ -1008,7 +1008,7 @@ void LengthMeasurement::rebuildLaserOverlay(WgpuViewportWindow& vp) {
wp[1] + NUDGE * n[1],
wp[2] + NUDGE * n[2],
};
WgpuViewportWindow::RaycastHit hit;
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]};
+15 -15
View File
@@ -22,7 +22,7 @@
#define IFCINTERFACE_MEASUREMENT_H
#include <cstddef>
#include "WgpuViewportWindow.h"
#include "ViewportWindow.h"
#include <QString>
#include <array>
#include <cstdint>
@@ -36,7 +36,7 @@
// taken as the absolute value of the signed-tetrahedra sum, so winding
// convention does not matter. Returns 0.0 for empty input or when nothing
// resolves. Recomputes from scratch on every call — no cache.
double volumeOfObjects(WgpuViewportWindow& vp,
double volumeOfObjects(ViewportWindow& vp,
const std::vector<uint32_t>& object_ids);
// Per-object volumes (m³). Same algorithm as volumeOfObjects but
@@ -45,11 +45,11 @@ double volumeOfObjects(WgpuViewportWindow& vp,
// Used by MainWindow's volume readout to drive both the total HUD and
// the per-object overlay labels.
std::vector<std::pair<uint32_t, double>>
volumesPerObject(WgpuViewportWindow& vp,
volumesPerObject(ViewportWindow& vp,
const std::vector<uint32_t>& object_ids);
// Click-to-accumulate area measurement. Each pick resolves the screen
// click to a (instance, triangle) using WgpuViewportWindow's primitives,
// click to a (instance, triangle) using ViewportWindow's primitives,
// expands it into the connected coplanar patch (BFS over shared edges,
// dot(normal, seed_normal) > 0.9999), then either adds or removes that
// patch from the running set depending on whether the seed triangle was
@@ -58,7 +58,7 @@ volumesPerObject(WgpuViewportWindow& vp,
// separate patches and their areas are summed.
//
// On every pick the world-space triangles of the running set are pushed
// to WgpuViewportWindow::setHighlightTriangles for in-viewport shading.
// to ViewportWindow::setHighlightTriangles for in-viewport shading.
// State is cleared on construction, on clear(), and is expected to be
// reset by the host (e.g. when the viewport's area tool toggles off).
class AreaMeasurement {
@@ -68,11 +68,11 @@ public:
// Main entry point: handle one click in area-tool mode. alt = true
// suppresses BFS expansion. Logs the per-click delta and running total
// via qInfo. Misses are silent.
void onPick(WgpuViewportWindow& vp, int x, int y, bool alt);
void onPick(ViewportWindow& vp, int x, int y, bool alt);
// Wipe all accumulated triangles, per-mesh adjacency caches, and the
// viewport overlay.
void clear(WgpuViewportWindow& vp);
void clear(ViewportWindow& vp);
double totalArea() const { return total_area_m2_; }
size_t triangleCount() const { return selected_.size(); }
@@ -89,7 +89,7 @@ private:
// edge_key (min<<32 | max) → list of triangle indices touching it.
std::unordered_map<uint64_t, std::vector<uint32_t>> edges;
};
MeshCache* meshCache(WgpuViewportWindow& vp, uint32_t model_id, uint32_t mesh_id);
MeshCache* meshCache(ViewportWindow& vp, uint32_t model_id, uint32_t mesh_id);
// Per-selected-triangle record. The composed transform is captured at
// pick time so the overlay rebuild doesn't have to re-query the
@@ -109,7 +109,7 @@ private:
return (uint64_t(object_id) << 32) | uint64_t(tri);
}
void rebuildHighlight(WgpuViewportWindow& vp);
void rebuildHighlight(ViewportWindow& vp);
std::unordered_map<uint64_t, MeshCache> mesh_cache_;
std::unordered_map<uint64_t, SelectedTri> selected_;
@@ -138,15 +138,15 @@ class LengthMeasurement {
public:
LengthMeasurement();
void onPick(WgpuViewportWindow& vp, int x, int y, bool alt);
void removeLastPoint(WgpuViewportWindow& vp);
void clear(WgpuViewportWindow& vp);
void onPick(ViewportWindow& vp, int x, int y, bool alt);
void removeLastPoint(ViewportWindow& vp);
void clear(ViewportWindow& vp);
size_t pointCount() const { return points_.size(); }
private:
void rebuildOverlay(WgpuViewportWindow& vp);
void rebuildLaserOverlay(WgpuViewportWindow& vp);
void rebuildOverlay(ViewportWindow& vp);
void rebuildLaserOverlay(ViewportWindow& vp);
QString formatReadout() const;
std::vector<std::array<float, 3>> points_;
@@ -156,7 +156,7 @@ private:
// updated afterwards. Used by the 1-pt laser BFS to re-locate the
// mesh-local position of points_[0] without re-picking. Stays valid
// while points_[0] does (pop_back never touches the first element).
WgpuViewportWindow::MeshLocalPick first_pick_{};
ViewportWindow::MeshLocalPick first_pick_{};
};
#endif // IFCINTERFACE_MEASUREMENT_H
+1 -1
View File
@@ -42,7 +42,7 @@ SessionState::SessionState(QObject* parent)
this, &SessionState::notifyFederationChanged);
}
void SessionState::createLoader(WgpuViewportWindow* viewport) {
void SessionState::createLoader(ViewportWindow* viewport) {
Q_ASSERT(!loader_);
loader_ = new SceneLoader(viewport, this);
loader_->setShouldReadSidecar(true);
+2 -2
View File
@@ -28,7 +28,7 @@
class Federation;
class SceneLoader;
class WgpuViewportWindow;
class ViewportWindow;
namespace bonsaiviewer {
@@ -44,7 +44,7 @@ public:
// Owned by SessionState once it can be tied to a viewport. Wires
// loader → element registry signals internally. Call exactly once.
void createLoader(WgpuViewportWindow* viewport);
void createLoader(ViewportWindow* viewport);
Federation* federation() const { return federation_; }
SceneLoader* loader() const { return loader_; }
+2 -2
View File
@@ -31,7 +31,7 @@
#include "../../../ifcviewer/Federation.h"
#include "../../../ifcviewer/SceneLoader.h"
#include "../../../ifcviewer/SidecarBuilder.h"
#include "../../../ifcviewer-wgpu/WgpuViewportWindow.h"
#include "../../../ifcviewer/ViewportWindow.h"
#include "../../../ifcgeom/Serializer.h"
#include "../../../serializers/document_serializer_plugin.h"
@@ -166,7 +166,7 @@ void removeGroup(SessionState& s, QWidget& host, const QString& group_id) {
s.setStatusMessage("Models", "Group removed");
}
void removeModel(SessionState& s, WgpuViewportWindow& vp, QWidget& host, const QString& fed_id) {
void removeModel(SessionState& s, ViewportWindow& vp, QWidget& host, const QString& fed_id) {
const Federation::Model* model = s.federation()->findById(fed_id);
const QString label = model ? model->display_name : fed_id;
const auto choice = QMessageBox::question(
+2 -2
View File
@@ -28,7 +28,7 @@
#include <cstdint>
class QWidget;
class WgpuViewportWindow;
class ViewportWindow;
namespace bonsaiviewer { class SessionState; }
namespace bonsaiviewer::modules::models {
@@ -58,7 +58,7 @@ void renameGroup(SessionState& s, QWidget& host, const QString& group_id);
void moveGroup(SessionState& s, const QString& id, const QString& parent_group_id);
void moveModels(SessionState& s, const QStringList& ids, const QString& parent_group_id);
void removeGroup(SessionState& s, QWidget& host, const QString& group_id);
void removeModel(SessionState& s, WgpuViewportWindow& vp, QWidget& host, const QString& fed_id);
void removeModel(SessionState& s, ViewportWindow& vp, QWidget& host, const QString& fed_id);
void addModel(SessionState& s, QWidget& host);
// Connector picker → pull_models_interactive → addCloudModel + load.
// Reachable from AddModelDialog's CloudModel button; the underlying call
+1 -1
View File
@@ -220,7 +220,7 @@ private:
} // namespace
ModelsPanel::ModelsPanel(bonsaiviewer::SessionState* session_state,
WgpuViewportWindow* viewport,
ViewportWindow* viewport,
QWidget* parent)
: components::Panel("Models", nullptr, parent, true)
, session_state_(session_state)
+3 -3
View File
@@ -26,7 +26,7 @@
#include "../../components/Panel.h"
class QTreeView;
class WgpuViewportWindow;
class ViewportWindow;
namespace bonsaiviewer { class SessionState; }
namespace bonsaiviewer::modules::models {
@@ -41,7 +41,7 @@ class ModelsPanel : public components::Panel {
Q_OBJECT
public:
explicit ModelsPanel(bonsaiviewer::SessionState* session_state,
WgpuViewportWindow* viewport,
ViewportWindow* viewport,
QWidget* parent = nullptr);
// Owned externally (the View constructs and owns the model). The panel
@@ -52,7 +52,7 @@ private:
void applyColumnLayout();
bonsaiviewer::SessionState* session_state_ = nullptr;
WgpuViewportWindow* viewport_ = nullptr;
ViewportWindow* viewport_ = nullptr;
QTreeView* tree_ = nullptr;
FederationItemModel* model_ = nullptr;
};
+12 -12
View File
@@ -30,7 +30,7 @@
#include "../models/Commands.h"
#include "../../../ifcviewer/Federation.h"
#include "../../../ifcviewer/SceneLoader.h"
#include "../../../ifcviewer-wgpu/WgpuViewportWindow.h"
#include "../../../ifcviewer/ViewportWindow.h"
#include <QDebug>
#include <QDir>
@@ -53,7 +53,7 @@ namespace {
// Pure helper — clears the loaded scene without emitting any signals. The
// caller (newProject / openProject) emits projectReset / projectOpened once
// the whole flow finishes.
void clearScene(SessionState& s, WgpuViewportWindow& vp) {
void clearScene(SessionState& s, ViewportWindow& vp) {
vp.setSelectedObjectId(0);
s.setSelectedObjectId(0);
for (uint32_t mid : s.modelIds()) {
@@ -88,7 +88,7 @@ bool confirmDiscardIfDirty(SessionState& s, QWidget& host) {
// - if no scene entry exists yet (initial open), queue a load.
// Per spec, connector errors are not surfaced to the user; the connector
// has already shown its own UI.
void resolveCloudModels(SessionState& s, WgpuViewportWindow& vp) {
void resolveCloudModels(SessionState& s, ViewportWindow& vp) {
auto* fed = s.federation();
QHash<QString, QStringList> connector_to_fed_ids;
for (const auto& m : fed->models()) {
@@ -99,7 +99,7 @@ void resolveCloudModels(SessionState& s, WgpuViewportWindow& vp) {
auto* registry = s.connectorRegistry();
QPointer<SessionState> sguard(&s);
QPointer<WgpuViewportWindow> vguard(&vp);
QPointer<ViewportWindow> vguard(&vp);
for (auto it = connector_to_fed_ids.constBegin();
it != connector_to_fed_ids.constEnd(); ++it) {
@@ -203,7 +203,7 @@ bool isIfcfedUnchanged(const QString& current_path, const QString& candidate_pat
return a.readAll() == b.readAll();
}
bool openProjectAt(SessionState& s, QWidget& host, WgpuViewportWindow& vp, const QString& path) {
bool openProjectAt(SessionState& s, QWidget& host, ViewportWindow& vp, const QString& path) {
SceneLoader* loader = s.loader();
if (loader && loader->isLoading()) {
QMessageBox::information(
@@ -268,7 +268,7 @@ bool saveProjectTo(SessionState& s, QWidget& host, const QString& path) {
} // namespace
bool newProject(SessionState& s, QWidget& host, WgpuViewportWindow& vp) {
bool newProject(SessionState& s, QWidget& host, ViewportWindow& vp) {
SceneLoader* loader = s.loader();
if (loader && loader->isLoading()) {
QMessageBox::information(
@@ -285,7 +285,7 @@ bool newProject(SessionState& s, QWidget& host, WgpuViewportWindow& vp) {
return true;
}
bool openProject(SessionState& s, QWidget& host, WgpuViewportWindow& vp) {
bool openProject(SessionState& s, QWidget& host, ViewportWindow& vp) {
QFileDialog file_dialog(&host, "Open Project");
file_dialog.setFileMode(QFileDialog::ExistingFile);
file_dialog.setNameFilter("IFC Federation (*.ifcfed);;All Files (*)");
@@ -297,12 +297,12 @@ bool openProject(SessionState& s, QWidget& host, WgpuViewportWindow& vp) {
return openProjectAt(s, host, vp, path);
}
bool openProjectPath(SessionState& s, QWidget& host, WgpuViewportWindow& vp, const QString& path) {
bool openProjectPath(SessionState& s, QWidget& host, ViewportWindow& vp, const QString& path) {
if (path.isEmpty()) return false;
return openProjectAt(s, host, vp, path);
}
bool openCloudProject(SessionState& s, QWidget& host, WgpuViewportWindow& vp) {
bool openCloudProject(SessionState& s, QWidget& host, ViewportWindow& vp) {
SceneLoader* loader = s.loader();
if (loader && loader->isLoading()) {
QMessageBox::information(
@@ -341,7 +341,7 @@ bool openCloudProject(SessionState& s, QWidget& host, WgpuViewportWindow& vp) {
QPointer<SessionState> sguard(&s);
QPointer<QWidget> hguard(&host);
QPointer<WgpuViewportWindow> vguard(&vp);
QPointer<ViewportWindow> vguard(&vp);
proc->call("pull_ifcfed_interactive", QJsonValue(),
[sguard, hguard, vguard, connector_id](const QJsonValue& result) {
@@ -371,7 +371,7 @@ bool openCloudProject(SessionState& s, QWidget& host, WgpuViewportWindow& vp) {
return true;
}
bool syncCloudProject(SessionState& s, QWidget& host, WgpuViewportWindow& vp) {
bool syncCloudProject(SessionState& s, QWidget& host, ViewportWindow& vp) {
auto* fed = s.federation();
// Per spec, sync has two independent phases — refreshing the .ifcfed
@@ -429,7 +429,7 @@ bool syncCloudProject(SessionState& s, QWidget& host, WgpuViewportWindow& vp) {
QPointer<SessionState> sguard(&s);
QPointer<QWidget> hguard(&host);
QPointer<WgpuViewportWindow> vguard(&vp);
QPointer<ViewportWindow> vguard(&vp);
const QString current_path = fed->filePath();
proc->call("pull_ifcfed", fed->manifest(),
+6 -6
View File
@@ -24,7 +24,7 @@
#include <QString>
class QWidget;
class WgpuViewportWindow;
class ViewportWindow;
namespace bonsaiviewer { class SessionState; }
namespace bonsaiviewer::modules::project::commands {
@@ -32,21 +32,21 @@ namespace bonsaiviewer::modules::project::commands {
// User-facing commands. Each owns its own dialogs and confirmations; each
// emits exactly one notify() at the end (projectReset / projectOpened /
// projectSaved) so views refresh once per command.
bool newProject(SessionState& s, QWidget& host, WgpuViewportWindow& vp);
bool openProject(SessionState& s, QWidget& host, WgpuViewportWindow& vp);
bool newProject(SessionState& s, QWidget& host, ViewportWindow& vp);
bool openProject(SessionState& s, QWidget& host, ViewportWindow& vp);
// Open a specific .ifcfed by path, bypassing the file dialog. Used by the
// "Open Recent" menu. Same dirty-check / load / cloud-resolve flow as
// openProject; returns false if the load failed or was cancelled.
bool openProjectPath(SessionState& s, QWidget& host, WgpuViewportWindow& vp, const QString& path);
bool openProjectPath(SessionState& s, QWidget& host, ViewportWindow& vp, const QString& path);
// Pick a connector, then call pull_ifcfed_interactive and open the resulting
// .ifcfed as a fresh project. Non-local models in the loaded federation are
// resolved asynchronously via pull_models.
bool openCloudProject(SessionState& s, QWidget& host, WgpuViewportWindow& vp);
bool openCloudProject(SessionState& s, QWidget& host, ViewportWindow& vp);
// pull_ifcfed using the current project's .ifcfed.manifest. Re-downloads
// the .ifcfed from the same cloud target it came from (typically without
// user interaction), then opens it like a fresh project — discarding any
// local edits after the usual dirty-check prompt.
bool syncCloudProject(SessionState& s, QWidget& host, WgpuViewportWindow& vp);
bool syncCloudProject(SessionState& s, QWidget& host, ViewportWindow& vp);
bool saveProject(SessionState& s, QWidget& host);
bool saveProjectAs(SessionState& s, QWidget& host);
// Push the current federation to the cloud target named in its manifest
+14 -14
View File
@@ -22,11 +22,11 @@
#include "../../SessionState.h"
#include "../../../ifcviewer/Federation.h"
#include "../../../ifcviewer-wgpu/WgpuViewportWindow.h"
#include "../../../ifcviewer/ViewportWindow.h"
namespace bonsaiviewer::modules::viewport::commands {
void setHome(SessionState& session, WgpuViewportWindow& vp) {
void setHome(SessionState& session, ViewportWindow& vp) {
auto camera = vp.cameraState();
Federation::HomeView home_view;
home_view.target = camera.target;
@@ -37,7 +37,7 @@ void setHome(SessionState& session, WgpuViewportWindow& vp) {
session.setStatusMessage("Camera", "Home view updated");
}
void goHome(SessionState& session, WgpuViewportWindow& vp) {
void goHome(SessionState& session, ViewportWindow& vp) {
Federation* federation = session.federation();
if (!federation->hasHomeView()) {
session.setStatusMessage("Camera", "No home view set for this project");
@@ -50,52 +50,52 @@ void goHome(SessionState& session, WgpuViewportWindow& vp) {
session.setStatusMessage("Camera", "Home view restored");
}
void viewSelected(WgpuViewportWindow& vp) {
void viewSelected(ViewportWindow& vp) {
vp.focusOnSelectedObject();
}
void fly(SessionState& session, WgpuViewportWindow& vp) {
void fly(SessionState& session, ViewportWindow& vp) {
vp.requestActivate();
vp.enterFpsMode();
session.setStatusMessage("Mode", "Fly mode active");
}
void toggleSection(SessionState& session, WgpuViewportWindow& vp) {
void toggleSection(SessionState& session, ViewportWindow& vp) {
vp.toggleSectionTool();
session.setStatusMessage("Section",
vp.sectionToolActive() ? "Section tool active" : "Section tool off");
}
void clearSection(SessionState& session, WgpuViewportWindow& vp) {
void clearSection(SessionState& session, ViewportWindow& vp) {
vp.clearSectionPlanes();
session.setStatusMessage("Section", "Section planes cleared");
}
void toggleDistance(WgpuViewportWindow& vp) {
void toggleDistance(ViewportWindow& vp) {
vp.toggleLengthTool();
}
void toggleArea(WgpuViewportWindow& vp) {
void toggleArea(ViewportWindow& vp) {
vp.toggleAreaTool();
}
void toggleVolume(WgpuViewportWindow& vp) {
void toggleVolume(ViewportWindow& vp) {
vp.toggleVolumeTool();
}
void hideSelected(WgpuViewportWindow& vp) {
void hideSelected(ViewportWindow& vp) {
vp.hideSelectedElements();
}
void isolateSelected(WgpuViewportWindow& vp) {
void isolateSelected(ViewportWindow& vp) {
vp.isolateSelectedElements();
}
void showAll(WgpuViewportWindow& vp) {
void showAll(ViewportWindow& vp) {
vp.showAllElements();
}
void invertVisibility(WgpuViewportWindow& vp) {
void invertVisibility(ViewportWindow& vp) {
vp.invertElementVisibility();
}
+14 -14
View File
@@ -21,27 +21,27 @@
#ifndef IFCINTERFACE_MODULES_VIEWPORT_COMMANDS_H
#define IFCINTERFACE_MODULES_VIEWPORT_COMMANDS_H
class WgpuViewportWindow;
class ViewportWindow;
namespace bonsaiviewer { class SessionState; }
namespace bonsaiviewer::modules::viewport::commands {
void setHome(SessionState& session, WgpuViewportWindow& vp);
void goHome(SessionState& session, WgpuViewportWindow& vp);
void viewSelected(WgpuViewportWindow& vp);
void setHome(SessionState& session, ViewportWindow& vp);
void goHome(SessionState& session, ViewportWindow& vp);
void viewSelected(ViewportWindow& vp);
void fly(SessionState& session, WgpuViewportWindow& vp);
void toggleSection(SessionState& session, WgpuViewportWindow& vp);
void clearSection(SessionState& session, WgpuViewportWindow& vp);
void fly(SessionState& session, ViewportWindow& vp);
void toggleSection(SessionState& session, ViewportWindow& vp);
void clearSection(SessionState& session, ViewportWindow& vp);
void toggleDistance(WgpuViewportWindow& vp);
void toggleArea(WgpuViewportWindow& vp);
void toggleVolume(WgpuViewportWindow& vp);
void toggleDistance(ViewportWindow& vp);
void toggleArea(ViewportWindow& vp);
void toggleVolume(ViewportWindow& vp);
void hideSelected(WgpuViewportWindow& vp);
void isolateSelected(WgpuViewportWindow& vp);
void showAll(WgpuViewportWindow& vp);
void invertVisibility(WgpuViewportWindow& vp);
void hideSelected(ViewportWindow& vp);
void isolateSelected(ViewportWindow& vp);
void showAll(ViewportWindow& vp);
void invertVisibility(ViewportWindow& vp);
} // namespace bonsaiviewer::modules::viewport::commands
+2 -2
View File
@@ -20,7 +20,7 @@
#include "Panel.h"
#include "../../../ifcviewer-wgpu/WgpuViewportWindow.h"
#include "../../../ifcviewer/ViewportWindow.h"
#include <QFrame>
#include <QVBoxLayout>
@@ -47,7 +47,7 @@ ViewportPanel::ViewportPanel(QWidget* parent)
frame_layout->setContentsMargins(0, 0, 0, 0);
frame_layout->setSpacing(0);
viewport_ = new WgpuViewportWindow();
viewport_ = new ViewportWindow();
viewport_container_ = QWidget::createWindowContainer(viewport_, frame);
viewport_container_->setMinimumSize(400, 300);
viewport_container_->setFocusPolicy(Qt::StrongFocus);
+3 -3
View File
@@ -23,7 +23,7 @@
#include <QWidget>
class WgpuViewportWindow;
class ViewportWindow;
namespace bonsaiviewer::modules::viewport {
@@ -33,10 +33,10 @@ class ViewportPanel : public QWidget {
public:
explicit ViewportPanel(QWidget* parent = nullptr);
WgpuViewportWindow* viewport() const { return viewport_; }
ViewportWindow* viewport() const { return viewport_; }
private:
WgpuViewportWindow* viewport_ = nullptr;
ViewportWindow* viewport_ = nullptr;
QWidget* viewport_container_ = nullptr;
};
+20 -20
View File
@@ -25,8 +25,8 @@
#include "../models/Commands.h"
#include "../../../ifcviewer/Federation.h"
#include "../../../ifcviewer/SceneLoader.h"
#include "../../../ifcviewer-wgpu/WgpuViewportWindow.h"
#include "../../../ifcviewer-wgpu/WgpuOverlayRenderer.h"
#include "../../../ifcviewer/ViewportWindow.h"
#include "../../../ifcviewer/OverlayRenderer.h"
#include "../../Measurement.h"
#include <Eigen/Dense>
@@ -37,7 +37,7 @@
namespace bonsaiviewer::modules::viewport {
ViewportView::ViewportView(bonsaiviewer::SessionState* session_state,
WgpuViewportWindow* viewport,
ViewportWindow* viewport,
QObject* parent)
: QObject(parent)
, session_state_(session_state)
@@ -73,54 +73,54 @@ ViewportView::ViewportView(bonsaiviewer::SessionState* session_state,
});
// Measurement tools — input-driven, share the View's lifetime.
connect(viewport_, &WgpuViewportWindow::surfacePickedInTool, this,
connect(viewport_, &ViewportWindow::surfacePickedInTool, this,
[this](int x, int y, int modifiers) {
const bool alt = (modifiers & Qt::AltModifier) != 0;
switch (viewport_->toolMode()) {
case WgpuViewportWindow::ToolMode::Area:
case ViewportWindow::ToolMode::Area:
area_measurement_->onPick(*viewport_, x, y, alt);
viewport_->setHudText(QString("Area: %1 m² (%2 tris)")
.arg(area_measurement_->totalArea(), 0, 'f', 4)
.arg(area_measurement_->triangleCount()));
break;
case WgpuViewportWindow::ToolMode::Length:
case ViewportWindow::ToolMode::Length:
length_measurement_->onPick(*viewport_, x, y, alt);
break;
case WgpuViewportWindow::ToolMode::NoTool:
case WgpuViewportWindow::ToolMode::Volume:
case ViewportWindow::ToolMode::NoTool:
case ViewportWindow::ToolMode::Volume:
break;
}
});
connect(viewport_, &WgpuViewportWindow::toolModeChanged, this,
[this](WgpuViewportWindow::ToolMode mode) {
connect(viewport_, &ViewportWindow::toolModeChanged, this,
[this](ViewportWindow::ToolMode mode) {
area_measurement_->clear(*viewport_);
length_measurement_->clear(*viewport_);
switch (mode) {
case WgpuViewportWindow::ToolMode::NoTool:
case ViewportWindow::ToolMode::NoTool:
viewport_->setHudText(QString());
viewport_->setOverlayLabels({});
session_state_->setStatusMessage("Measure", "Measurement tool off");
break;
case WgpuViewportWindow::ToolMode::Length:
case ViewportWindow::ToolMode::Length:
viewport_->setHudText("Length tool: click first point");
session_state_->setStatusMessage("Measure", "Length tool: LMB add point, Backspace remove last, Esc exits");
break;
case WgpuViewportWindow::ToolMode::Area:
case ViewportWindow::ToolMode::Area:
viewport_->setHudText("Area: 0.0000 m² (0 tris)");
session_state_->setStatusMessage("Measure", "Area tool: LMB add, Alt+LMB single tri, click again to remove, Esc exits");
break;
case WgpuViewportWindow::ToolMode::Volume:
case ViewportWindow::ToolMode::Volume:
session_state_->setStatusMessage("Measure", "Volume tool: click / box-select objects, Esc exits");
updateVolumeReadout();
break;
}
});
connect(viewport_, &WgpuViewportWindow::toolBackspacePressed, this, [this]() {
if (viewport_->toolMode() == WgpuViewportWindow::ToolMode::Length) {
connect(viewport_, &ViewportWindow::toolBackspacePressed, this, [this]() {
if (viewport_->toolMode() == ViewportWindow::ToolMode::Length) {
length_measurement_->removeLastPoint(*viewport_);
}
});
connect(viewport_, &WgpuViewportWindow::objectPicked, this, [this](uint32_t) {
connect(viewport_, &ViewportWindow::objectPicked, this, [this](uint32_t) {
updateVolumeReadout();
});
@@ -238,7 +238,7 @@ void ViewportView::guessFederatedFalseOriginFromFirstModel(uint32_t mid) {
}
void ViewportView::updateVolumeReadout() {
if (viewport_->toolMode() != WgpuViewportWindow::ToolMode::Volume) return;
if (viewport_->toolMode() != ViewportWindow::ToolMode::Volume) return;
const auto& sel = viewport_->selection().selectionIds();
if (sel.empty()) {
@@ -251,13 +251,13 @@ void ViewportView::updateVolumeReadout() {
const auto per_obj = volumesPerObject(*viewport_, ids);
double total = 0.0;
std::vector<WgpuOverlayRenderer::Label> labels;
std::vector<OverlayRenderer::Label> labels;
labels.reserve(per_obj.size());
for (const auto& [oid, v] : per_obj) {
total += v;
QVector3D mn, mx;
if (!viewport_->computeObjectAabb(oid, mn, mx)) continue;
WgpuOverlayRenderer::Label lbl;
OverlayRenderer::Label lbl;
const QVector3D c = (mn + mx) * 0.5f;
lbl.world_pos[0] = c.x();
lbl.world_pos[1] = c.y();
+3 -3
View File
@@ -25,7 +25,7 @@
#include <memory>
namespace bonsaiviewer { class SessionState; }
class WgpuViewportWindow;
class ViewportWindow;
class AreaMeasurement;
class LengthMeasurement;
@@ -44,7 +44,7 @@ class ViewportView : public QObject {
public:
explicit ViewportView(bonsaiviewer::SessionState* session_state,
WgpuViewportWindow* viewport,
ViewportWindow* viewport,
QObject* parent = nullptr);
~ViewportView() override;
@@ -57,7 +57,7 @@ private:
void updateVolumeReadout();
bonsaiviewer::SessionState* session_state_ = nullptr;
WgpuViewportWindow* viewport_ = nullptr;
ViewportWindow* viewport_ = nullptr;
std::unique_ptr<AreaMeasurement> area_measurement_;
std::unique_ptr<LengthMeasurement> length_measurement_;
};
@@ -17,22 +17,22 @@
# #
################################################################################
message("Running CMakeLists.txt in /src/ifcviewer-wgpu-minimal")
message("Running CMakeLists.txt in /src/ifcviewer-minimal")
find_package(Qt${QT_VERSION} COMPONENTS Widgets REQUIRED PATHS ${QT_DIR})
file(GLOB IFCVIEWER_WGPU_MIN_CPP_FILES ${CMAKE_CURRENT_SOURCE_DIR}/*.cpp)
file(GLOB IFCVIEWER_MIN_CPP_FILES ${CMAKE_CURRENT_SOURCE_DIR}/*.cpp)
add_executable(IfcViewerWgpuMinimal ${IFCVIEWER_WGPU_MIN_CPP_FILES})
add_executable(IfcViewerMinimal ${IFCVIEWER_MIN_CPP_FILES})
set_target_properties(IfcViewerWgpuMinimal PROPERTIES
set_target_properties(IfcViewerMinimal PROPERTIES
AUTOMOC ON
WIN32_EXECUTABLE ON
MACOSX_BUNDLE ON
)
target_link_libraries(IfcViewerWgpuMinimal PRIVATE
IfcViewerWgpu
target_link_libraries(IfcViewerMinimal PRIVATE
IfcViewer
Qt${QT_VERSION}::Widgets
)
@@ -40,19 +40,19 @@ target_link_libraries(IfcViewerWgpuMinimal PRIVATE
# run directly out of build-viewer-wgpu/. The patched SONAME means DT_NEEDED
# is just the basename, so a single rpath entry suffices.
if(UNIX AND NOT APPLE AND WGPU_NATIVE_LIB_DIR)
set_target_properties(IfcViewerWgpuMinimal PROPERTIES
set_target_properties(IfcViewerMinimal PROPERTIES
BUILD_RPATH "${WGPU_NATIVE_LIB_DIR}"
)
endif()
# BUNDLE DESTINATION must be "." (install-prefix root) on macOS Qt's
# deploy generator emits `macdeployqt IfcViewerWgpuMinimal.app` (no
# deploy generator emits `macdeployqt IfcViewerMinimal.app` (no
# path prefix), which resolves only when the .app sits at the install
# root. With `bin/` it can't find the bundle and the install/strip
# step fails.
install(TARGETS IfcViewerWgpuMinimal
install(TARGETS IfcViewerMinimal
EXPORT ${IFCOPENSHELL_EXPORT_TARGETS}
RUNTIME DESTINATION bin
BUNDLE DESTINATION .
)
ifcopenshell_deploy_qt_runtime(IfcViewerWgpuMinimal)
ifcopenshell_deploy_qt_runtime(IfcViewerMinimal)
@@ -23,7 +23,7 @@
#include <QWidget>
#include <QVBoxLayout>
#include "WgpuViewportWindow.h"
#include "ViewportWindow.h"
// Stage-1 driver: opens a single window with the wgpu viewport embedded,
// clears to background colour, and exits on close. The shape mirrors
@@ -31,7 +31,7 @@
// benchmark-comparable binary.
int main(int argc, char* argv[]) {
QApplication app(argc, argv);
app.setApplicationName("IfcViewerWgpuMinimal");
app.setApplicationName("IfcViewerMinimal");
app.setOrganizationName("IfcOpenShell");
QCommandLineParser parser;
@@ -56,7 +56,7 @@ int main(int argc, char* argv[]) {
"browser constraints."});
parser.process(app);
auto* viewport = new WgpuViewportWindow;
auto* viewport = new ViewportWindow;
viewport->resize(1280, 800);
if (parser.isSet("no-hiz")) viewport->hiz_enabled_ = false;
if (parser.isSet("web-limits")) viewport->web_limits_ = true;
-194
View File
@@ -1,194 +0,0 @@
################################################################################
# #
# 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/>. #
# #
################################################################################
message("Running CMakeLists.txt in /src/ifcviewer-wgpu")
set(QT_VERSION 6 CACHE STRING "Qt version")
find_package(Qt${QT_VERSION} COMPONENTS Core Gui REQUIRED PATHS ${QT_DIR})
# Eigen3 for the federation hooks (setModelTransformation etc.) and the
# per-model coordinate-operation matrices stored on WgpuModelGpuData.
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_WGPU_CPP_FILES ${CMAKE_CURRENT_SOURCE_DIR}/*.cpp)
file(GLOB IFCVIEWER_WGPU_H_FILES ${CMAKE_CURRENT_SOURCE_DIR}/*.h)
set(IFCVIEWER_WGPU_FILES ${IFCVIEWER_WGPU_CPP_FILES} ${IFCVIEWER_WGPU_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_WGPU_FILES
${CMAKE_CURRENT_SOURCE_DIR}/WgpuMetalSurface_mac.mm
)
endif()
# Intentional source-level borrowing from the GL backend until ifcviewer-core
# is extracted (task #12). SidecarCache + InstancedGeometry have zero Qt /
# OCCT / IFC-parse deps, so compiling them directly into IfcViewerWgpu is
# cheaper than dragging in the IfcViewer static lib (which would pull all
# of IfcGeom + IfcParse + OCCT + Qt OpenGL).
set(IFCVIEWER_SHARED_DIR ${CMAKE_CURRENT_SOURCE_DIR}/../ifcviewer)
list(APPEND IFCVIEWER_WGPU_FILES
${IFCVIEWER_SHARED_DIR}/SidecarCache.cpp
)
add_library(IfcViewerWgpu STATIC ${IFCVIEWER_WGPU_FILES})
set_target_properties(IfcViewerWgpu PROPERTIES
AUTOMOC ON
VERSION "${PROJECT_VERSION}"
SOVERSION "${PROJECT_VERSION_MAJOR}.${PROJECT_VERSION_MINOR}"
)
target_include_directories(IfcViewerWgpu
PUBLIC ${CMAKE_CURRENT_SOURCE_DIR}
PUBLIC ${IFCVIEWER_SHARED_DIR} # SidecarCache.h is reachable via WgpuViewportWindow.h
)
target_link_libraries(IfcViewerWgpu PUBLIC
Qt${QT_VERSION}::Core
Qt${QT_VERSION}::Gui
wgpu_native
Eigen3::Eigen
)
# 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(IfcViewerWgpu PUBLIC Threads::Threads)
endif()
# Cocoa + QuartzCore for WgpuMetalSurface_mac.mm (NSView, CAMetalLayer).
if(APPLE)
target_link_libraries(IfcViewerWgpu PUBLIC
"-framework Cocoa"
"-framework QuartzCore"
)
endif()
install(TARGETS IfcViewerWgpu EXPORT ${IFCOPENSHELL_EXPORT_TARGETS})
install(FILES ${IFCVIEWER_WGPU_H_FILES}
DESTINATION ${INCLUDEDIR}/ifcviewer-wgpu
)
# 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()
-35
View File
@@ -1,35 +0,0 @@
################################################################################
# #
# 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 unit tests for the wgpu backend's pure-CPU state machines (selection,
# visibility). Header-only subjects — the test binaries compile against the
# class definitions directly, no link to IfcViewerWgpu needed and no
# wgpu-native runtime involvement.
set(IFCVIEWER_WGPU_SRC ${CMAKE_CURRENT_SOURCE_DIR}/..)
function(add_ifcviewer_wgpu_unit_test name)
add_executable(${name} ${name}.cpp)
target_include_directories(${name} PRIVATE ${IFCVIEWER_WGPU_SRC})
target_link_libraries(${name} PRIVATE Catch2::Catch2WithMain)
catch_discover_tests(${name})
endfunction()
add_ifcviewer_wgpu_unit_test(test_wgpu_selection)
add_ifcviewer_wgpu_unit_test(test_wgpu_visibility)
@@ -17,10 +17,10 @@
* *
********************************************************************************/
#include "WgpuAreaMeasurement.h"
#include "AreaMeasurement.h"
#include "WgpuOverlayRenderer.h"
#include "WgpuViewportWindow.h"
#include "OverlayRenderer.h"
#include "ViewportWindow.h"
#include <QDebug>
#include <QString>
@@ -138,9 +138,9 @@ constexpr double kCoplanarDot = 0.9999; // ~0.81° tolerance, matches GL
} // namespace
WgpuAreaMeasurement::WgpuAreaMeasurement() = default;
AreaMeasurement::AreaMeasurement() = default;
void WgpuAreaMeasurement::clear(WgpuViewportWindow& vp) {
void AreaMeasurement::clear(ViewportWindow& vp) {
mesh_cache_.clear();
selected_.clear();
total_area_m2_ = 0.0;
@@ -148,8 +148,8 @@ void WgpuAreaMeasurement::clear(WgpuViewportWindow& vp) {
vp.setOverlayLabels({});
}
WgpuAreaMeasurement::MeshAdj*
WgpuAreaMeasurement::meshAdj(WgpuViewportWindow& vp,
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);
@@ -159,7 +159,7 @@ WgpuAreaMeasurement::meshAdj(WgpuViewportWindow& vp,
// 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.
WgpuViewportWindow::MeshTriangles tris;
ViewportWindow::MeshTriangles tris;
if (!vp.readbackMeshTriangles(model_id, mesh_id, tris)) return nullptr;
if (tris.indices.size() < 3) return nullptr;
@@ -190,12 +190,12 @@ WgpuAreaMeasurement::meshAdj(WgpuViewportWindow& vp,
return &mesh_cache_.emplace(key, std::move(a)).first->second;
}
void WgpuAreaMeasurement::onPick(WgpuViewportWindow& vp,
void AreaMeasurement::onPick(ViewportWindow& vp,
int x_phys, int y_phys, bool alt) {
WgpuViewportWindow::MeshLocalPick pick;
ViewportWindow::MeshLocalPick pick;
if (!vp.pickMeshLocalAt(x_phys, y_phys, pick)) return;
WgpuViewportWindow::MeshTriangles tris;
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;
@@ -285,7 +285,7 @@ void WgpuAreaMeasurement::onPick(WgpuViewportWindow& vp,
total_area_m2_, selected_.size());
}
void WgpuAreaMeasurement::rebuildHighlightAndLabels(WgpuViewportWindow& vp) {
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
@@ -295,20 +295,20 @@ void WgpuAreaMeasurement::rebuildHighlightAndLabels(WgpuViewportWindow& vp) {
// 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, WgpuViewportWindow::MeshTriangles> tris_cache;
std::unordered_map<uint64_t, ViewportWindow::MeshTriangles> tris_cache;
auto get_tris = [&](uint32_t model_id, uint32_t mesh_id)
-> WgpuViewportWindow::MeshTriangles* {
-> 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;
WgpuViewportWindow::MeshTriangles t;
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_) {
WgpuViewportWindow::MeshTriangles* t = get_tris(sel.model_id, sel.mesh_id);
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
@@ -337,11 +337,11 @@ void WgpuAreaMeasurement::rebuildHighlightAndLabels(WgpuViewportWindow& vp) {
by_object[object_id].push_back(&sel);
}
std::vector<WgpuOverlayRenderer::Label> labels;
std::vector<OverlayRenderer::Label> labels;
for (const auto& [obj_id, sels] : by_object) {
if (sels.empty()) continue;
const SelectedTri& any = *sels[0];
WgpuViewportWindow::MeshTriangles* t = get_tris(any.model_id, any.mesh_id);
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;
@@ -393,7 +393,7 @@ void WgpuAreaMeasurement::rebuildHighlightAndLabels(WgpuViewportWindow& vp) {
cx /= area; cy /= area; cz /= area;
const float* M = any.composed_transform;
WgpuOverlayRenderer::Label lbl;
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]);
@@ -25,11 +25,11 @@
#include <unordered_map>
#include <vector>
class WgpuViewportWindow;
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 WgpuViewportWindow::pickMeshLocalAt, then
// 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.
@@ -37,18 +37,18 @@ class WgpuViewportWindow;
// separate patches.
//
// On every mutation the world-space triangles of the running set are
// pushed to WgpuViewportWindow::setHighlightTriangles for the
// 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 WgpuAreaMeasurement {
class AreaMeasurement {
public:
WgpuAreaMeasurement();
AreaMeasurement();
// Pixel coords are physical (post-DPR), to match
// WgpuViewportWindow::pickMeshLocalAt's convention.
void onPick(WgpuViewportWindow& vp, int x_phys, int y_phys, bool alt);
void clear(WgpuViewportWindow& vp);
// 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(); }
@@ -57,7 +57,7 @@ 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
// WgpuViewportWindow::readbackMeshTriangles' CPU shadow.
// 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
@@ -65,7 +65,7 @@ private:
std::unordered_map<uint64_t, std::vector<uint32_t>> edges;
};
// Keyed by (model_id << 32) | mesh_id.
MeshAdj* meshAdj(WgpuViewportWindow& vp,
MeshAdj* meshAdj(ViewportWindow& vp,
uint32_t model_id, uint32_t mesh_id);
// Per-selected-triangle record. The composed transform is captured
@@ -86,7 +86,7 @@ private:
return (uint64_t(object_id) << 32) | uint64_t(tri);
}
void rebuildHighlightAndLabels(WgpuViewportWindow& vp);
void rebuildHighlightAndLabels(ViewportWindow& vp);
std::unordered_map<uint64_t, MeshAdj> mesh_cache_;
std::unordered_map<uint64_t, SelectedTri> selected_;
@@ -17,18 +17,18 @@
* *
********************************************************************************/
#include "WgpuBufferPool.h"
#include "BufferPool.h"
#include <QtDebug>
#include <cassert>
#include <cstring>
WgpuBufferPool::~WgpuBufferPool() {
BufferPool::~BufferPool() {
destroy();
}
void WgpuBufferPool::configure(WGPUInstance instance, WGPUDevice device,
void BufferPool::configure(WGPUInstance instance, WGPUDevice device,
WGPUBufferUsage usage,
uint64_t per_sub_buffer_capacity,
const char* label_prefix) {
@@ -41,7 +41,7 @@ void WgpuBufferPool::configure(WGPUInstance instance, WGPUDevice device,
label_prefix_ = label_prefix ? label_prefix : "";
}
void WgpuBufferPool::destroy() {
void BufferPool::destroy() {
for (auto& sp : sub_pools_) {
if (sp.buffer) wgpuBufferRelease(sp.buffer);
}
@@ -55,7 +55,7 @@ void WgpuBufferPool::destroy() {
label_prefix_.clear();
}
bool WgpuBufferPool::addSubBuffer() {
bool BufferPool::addSubBuffer() {
if (!device_ || per_sub_buffer_capacity_ == 0) return false;
if (growth_disabled_) return false;
@@ -132,7 +132,7 @@ bool WgpuBufferPool::addSubBuffer() {
return false;
}
WgpuBufferPool::Slice WgpuBufferPool::alloc(uint64_t size, uint64_t align) {
BufferPool::Slice BufferPool::alloc(uint64_t size, uint64_t align) {
Slice out;
if (size == 0 || align == 0) return out;
@@ -179,7 +179,7 @@ WgpuBufferPool::Slice WgpuBufferPool::alloc(uint64_t size, uint64_t align) {
return out;
}
void WgpuBufferPool::free(const Slice& s) {
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)];
@@ -202,19 +202,19 @@ void WgpuBufferPool::free(const Slice& s) {
}
}
uint64_t WgpuBufferPool::total_capacity_bytes() const {
uint64_t BufferPool::total_capacity_bytes() const {
uint64_t s = 0;
for (const auto& sp : sub_pools_) s += sp.capacity;
return s;
}
uint64_t WgpuBufferPool::total_used_bytes() const {
uint64_t BufferPool::total_used_bytes() const {
uint64_t s = 0;
for (const auto& sp : sub_pools_) s += sp.used;
return s;
}
uint64_t WgpuBufferPool::largest_free_run_bytes() const {
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) {
@@ -46,7 +46,7 @@
// 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 WgpuBufferPool {
class BufferPool {
public:
// A handle to a previously-allocated range. Includes the underlying
// sub-buffer so callers (bind-group builders, queueWriteBuffer) can
@@ -60,11 +60,11 @@ public:
bool valid() const { return size > 0 && buffer != nullptr; }
};
WgpuBufferPool() = default;
~WgpuBufferPool();
BufferPool() = default;
~BufferPool();
WgpuBufferPool(const WgpuBufferPool&) = delete;
WgpuBufferPool& operator=(const WgpuBufferPool&) = delete;
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`
+129 -10
View File
@@ -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()
@@ -17,9 +17,9 @@
* *
********************************************************************************/
#include "WgpuLengthMeasurement.h"
#include "LengthMeasurement.h"
#include "WgpuOverlayRenderer.h"
#include "OverlayRenderer.h"
#include <QDebug>
#include <QStringList>
@@ -292,10 +292,10 @@ constexpr float DOT_HALO = 1.0f;
constexpr float DASH_PERIOD = 9.0f; // px
constexpr float DASH_ON_RATIO = 0.55f; // 5 on, 4 off
WgpuOverlayRenderer::LineGroup makeGroup(std::vector<float> xyz,
OverlayRenderer::LineGroup makeGroup(std::vector<float> xyz,
float r, float g, float b,
bool dashed = false) {
WgpuOverlayRenderer::LineGroup gp;
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;
@@ -318,10 +318,10 @@ void pushSeg(std::vector<float>& xyz,
xyz.insert(xyz.end(), b.begin(), b.end());
}
WgpuOverlayRenderer::Label makeLabel(const std::array<float, 3>& a,
OverlayRenderer::Label makeLabel(const std::array<float, 3>& a,
const std::array<float, 3>& b,
const QString& text) {
WgpuOverlayRenderer::Label lbl;
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]);
@@ -329,7 +329,7 @@ WgpuOverlayRenderer::Label makeLabel(const std::array<float, 3>& a,
return lbl;
}
void pushDots(WgpuViewportWindow& vp, const std::vector<float>& xyz) {
void pushDots(ViewportWindow& vp, const std::vector<float>& xyz) {
vp.setOverlayPoints(xyz,
/*inner*/ 1.0f, 1.0f, 1.0f, 1.0f,
/*size*/ DOT_SIZE,
@@ -348,9 +348,9 @@ const char* dominantAxisLabel(const float v[3]) {
} // namespace
WgpuLengthMeasurement::WgpuLengthMeasurement() = default;
LengthMeasurement::LengthMeasurement() = default;
void WgpuLengthMeasurement::clear(WgpuViewportWindow& vp) {
void LengthMeasurement::clear(ViewportWindow& vp) {
points_.clear();
normals_.clear();
vp.setOverlayPoints({}, 0,0,0,0, 0, 0,0,0,0, 0);
@@ -359,9 +359,9 @@ void WgpuLengthMeasurement::clear(WgpuViewportWindow& vp) {
vp.setHudText(QString());
}
void WgpuLengthMeasurement::onPick(WgpuViewportWindow& vp,
void LengthMeasurement::onPick(ViewportWindow& vp,
int x_phys, int y_phys, bool /*alt*/) {
WgpuViewportWindow::MeshLocalPick pick;
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]});
@@ -371,14 +371,14 @@ void WgpuLengthMeasurement::onPick(WgpuViewportWindow& vp,
rebuildOverlay(vp);
}
void WgpuLengthMeasurement::removeLastPoint(WgpuViewportWindow& vp) {
void LengthMeasurement::removeLastPoint(ViewportWindow& vp) {
if (points_.empty()) return;
points_.pop_back();
if (!normals_.empty()) normals_.pop_back();
rebuildOverlay(vp);
}
void WgpuLengthMeasurement::rebuildOverlay(WgpuViewportWindow& vp) {
void LengthMeasurement::rebuildOverlay(ViewportWindow& vp) {
if (points_.size() == 1 && normals_.size() == 1) {
rebuildLaserOverlay(vp);
return;
@@ -389,8 +389,8 @@ void WgpuLengthMeasurement::rebuildOverlay(WgpuViewportWindow& vp) {
for (const auto& p : points_) pushDot(pts_xyz, p);
pushDots(vp, pts_xyz);
std::vector<WgpuOverlayRenderer::LineGroup> groups;
std::vector<WgpuOverlayRenderer::Label> labels;
std::vector<OverlayRenderer::LineGroup> groups;
std::vector<OverlayRenderer::Label> labels;
const size_t n = points_.size();
if (n == 2) {
@@ -489,7 +489,7 @@ void WgpuLengthMeasurement::rebuildOverlay(WgpuViewportWindow& vp) {
vp.setHudText(formatReadout());
}
void WgpuLengthMeasurement::rebuildLaserOverlay(WgpuViewportWindow& vp) {
void LengthMeasurement::rebuildLaserOverlay(ViewportWindow& vp) {
const auto& wp = first_pick_.world_pos;
const auto& n = first_pick_.world_normal;
@@ -519,8 +519,8 @@ void WgpuLengthMeasurement::rebuildLaserOverlay(WgpuViewportWindow& vp) {
n[0]*t1[1] - n[1]*t1[0],
};
std::vector<WgpuOverlayRenderer::LineGroup> groups;
std::vector<WgpuOverlayRenderer::Label> labels;
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};
@@ -532,7 +532,7 @@ void WgpuLengthMeasurement::rebuildLaserOverlay(WgpuViewportWindow& vp) {
}
// Coplanar-patch BFS for face extent.
WgpuViewportWindow::MeshTriangles tris;
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)) {
@@ -658,7 +658,7 @@ void WgpuLengthMeasurement::rebuildLaserOverlay(WgpuViewportWindow& vp) {
wp[1] + NUDGE * n[1],
wp[2] + NUDGE * n[2],
};
WgpuViewportWindow::RaycastHit hit;
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]};
@@ -682,7 +682,7 @@ void WgpuLengthMeasurement::rebuildLaserOverlay(WgpuViewportWindow& vp) {
vp.setHudText(hud_lines.join('\n'));
}
QString WgpuLengthMeasurement::formatReadout() const {
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)");
@@ -20,7 +20,7 @@
#ifndef WGPULENGTHMEASUREMENT_H
#define WGPULENGTHMEASUREMENT_H
#include "WgpuViewportWindow.h"
#include "ViewportWindow.h"
#include <QString>
@@ -45,21 +45,21 @@
// 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 WgpuLengthMeasurement {
class LengthMeasurement {
public:
WgpuLengthMeasurement();
LengthMeasurement();
// Pixel coords are physical (post-DPR). `alt` is currently unused
// (kept for API symmetry with the Area tool).
void onPick(WgpuViewportWindow& vp, int x_phys, int y_phys, bool alt);
void removeLastPoint(WgpuViewportWindow& vp);
void clear(WgpuViewportWindow& vp);
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(WgpuViewportWindow& vp);
void rebuildLaserOverlay(WgpuViewportWindow& vp);
void rebuildOverlay(ViewportWindow& vp);
void rebuildLaserOverlay(ViewportWindow& vp);
QString formatReadout() const;
std::vector<std::array<float, 3>> points_;
@@ -69,7 +69,7 @@ private:
// 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).
WgpuViewportWindow::MeshLocalPick first_pick_{};
ViewportWindow::MeshLocalPick first_pick_{};
};
#endif // WGPULENGTHMEASUREMENT_H
@@ -1,11 +1,11 @@
/**
* Objective-C++ bridge between WgpuViewportWindow (pure C++) and Cocoa /
* 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 WgpuMetalSurface_mac.mm.
* the actual layer attach lives in MetalSurface_mac.mm.
*/
#ifndef WGPU_METAL_SURFACE_MAC_H
@@ -1,9 +1,9 @@
/**
* Objective-C++ implementation of the Cocoa bridge declared in
* WgpuMetalSurface_mac.h. Compiled only on macOS.
* MetalSurface_mac.h. Compiled only on macOS.
*/
#include "WgpuMetalSurface_mac.h"
#include "MetalSurface_mac.h"
#if defined(__APPLE__)
@@ -32,7 +32,7 @@
#include <vector>
#include "InstancedGeometry.h"
#include "WgpuBufferPool.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;
@@ -49,7 +49,7 @@
//
// 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
// (WgpuStreamingThread) is in place: scatter-gather per-mesh seeks
// (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 —
@@ -64,7 +64,7 @@
// and 16 MB.
static constexpr uint64_t WGPU_CHUNK_VERTEX_BYTES_LIMIT = 16ull * 1024 * 1024;
struct WgpuModelGpuData {
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[].
@@ -77,7 +77,7 @@ struct WgpuModelGpuData {
static_assert(sizeof(VisibleDrawGpu) == 16, "VisibleDrawGpu must be 16 bytes");
// Per-chunk state. Each chunk references a vertex range and an
// index range inside WgpuViewportWindow::pool_, plus a small set of
// 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
@@ -91,12 +91,12 @@ struct WgpuModelGpuData {
// populates pool ranges at applyCachedModel time.
struct Chunk {
// Pool-allocated vertex + index bytes. Both slices land in the
// shared WgpuViewportWindow::pool_; the slice tells us which
// 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.
WgpuBufferPool::Slice vertex_slice;
WgpuBufferPool::Slice index_slice;
BufferPool::Slice vertex_slice;
BufferPool::Slice index_slice;
WGPUBuffer visible_draws_buffer = nullptr;
WGPUBuffer prefix_sums_buffer = nullptr;
@@ -357,6 +357,6 @@ struct WgpuModelGpuData {
// 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(WgpuModelGpuData& m, WgpuBufferPool& pool);
void releaseWgpuModelGpuData(ModelGpuData& m, BufferPool& pool);
#endif // WGPUMODELGPUDATA_H
@@ -17,7 +17,7 @@
* *
********************************************************************************/
#include "WgpuOverlayRenderer.h"
#include "OverlayRenderer.h"
#include <QFont>
#include <QFontMetrics>
@@ -439,11 +439,11 @@ fn fs_main(in: VsOut) -> @location(0) vec4<f32> {
// Construction / destruction
// -----------------------------------------------------------------------------
WgpuOverlayRenderer::~WgpuOverlayRenderer() {
OverlayRenderer::~OverlayRenderer() {
destroy();
}
bool WgpuOverlayRenderer::init(WGPUInstance instance, WGPUDevice device,
bool OverlayRenderer::init(WGPUInstance instance, WGPUDevice device,
WGPUQueue queue, WGPUTextureFormat surface_format,
int sample_count) {
instance_ = instance;
@@ -461,7 +461,7 @@ bool WgpuOverlayRenderer::init(WGPUInstance instance, WGPUDevice device,
return true;
}
void WgpuOverlayRenderer::destroy() {
void OverlayRenderer::destroy() {
// Axis indicator
if (axis_bind_group_) { wgpuBindGroupRelease(axis_bind_group_); axis_bind_group_ = nullptr; }
if (axis_pivot_pipeline_) { wgpuRenderPipelineRelease(axis_pivot_pipeline_); axis_pivot_pipeline_ = nullptr; }
@@ -545,7 +545,7 @@ void WgpuOverlayRenderer::destroy() {
// Axis indicator
// -----------------------------------------------------------------------------
bool WgpuOverlayRenderer::buildAxisIndicator() {
bool OverlayRenderer::buildAxisIndicator() {
// Bonsai decorator palette (src/bonsai/bonsai/bim/ui.py:593+):
// decorator_color_error = (1.000, 0.200, 0.322) — red → +X
// decorator_color_selected = (0.545, 0.863, 0.000) — green → +Y
@@ -719,8 +719,8 @@ bool WgpuOverlayRenderer::buildAxisIndicator() {
&& axis_corner_pipeline_;
}
void WgpuOverlayRenderer::encodePivot(WGPURenderPassEncoder pass,
const WgpuOverlayFrame& f,
void OverlayRenderer::encodePivot(WGPURenderPassEncoder pass,
const OverlayFrame& f,
bool visible) {
if (!visible || !axis_pivot_pipeline_ || !axis_pivot_xray_pipeline_) return;
if (f.viewport_h_px <= 0) return;
@@ -760,9 +760,9 @@ void WgpuOverlayRenderer::encodePivot(WGPURenderPassEncoder pass,
wgpuRenderPassEncoderDraw(pass, 18, 1, 0, 0);
}
void WgpuOverlayRenderer::encodeCornerAxis(WGPUCommandEncoder enc,
void OverlayRenderer::encodeCornerAxis(WGPUCommandEncoder enc,
WGPUTextureView surface_view,
const WgpuOverlayFrame& f) {
const OverlayFrame& f) {
if (!axis_corner_pipeline_ || !surface_view) return;
const int dpr = std::max(1, f.device_pixel_ratio);
const uint32_t gizmo_size = uint32_t(110 * dpr);
@@ -829,7 +829,7 @@ void WgpuOverlayRenderer::encodeCornerAxis(WGPUCommandEncoder enc,
// Section plane visualizer
// -----------------------------------------------------------------------------
bool WgpuOverlayRenderer::buildSectionVisualizer() {
bool OverlayRenderer::buildSectionVisualizer() {
struct Seg {
std::array<float, 3> s, e;
std::array<float, 3> c;
@@ -967,9 +967,9 @@ bool WgpuOverlayRenderer::buildSectionVisualizer() {
return section_pipeline_ != nullptr;
}
void WgpuOverlayRenderer::encodeSectionGizmos(WGPURenderPassEncoder pass,
const WgpuOverlayFrame& f,
const std::vector<WgpuSectionPlane>& planes) {
void OverlayRenderer::encodeSectionGizmos(WGPURenderPassEncoder pass,
const OverlayFrame& f,
const std::vector<SectionPlane>& planes) {
if (!section_pipeline_ || planes.empty()) return;
wgpuRenderPassEncoderSetPipeline(pass, section_pipeline_);
@@ -978,7 +978,7 @@ void WgpuOverlayRenderer::encodeSectionGizmos(WGPURenderPassEncoder pass,
const int n = std::min<int>(int(planes.size()), kMaxSectionPlanes);
for (int i = 0; i < n; ++i) {
const WgpuSectionPlane& p = planes[i];
const SectionPlane& p = planes[i];
// Stable in-plane basis: pick the world axis least parallel to n
// so the cross-product stays well-conditioned at any orientation.
@@ -1021,7 +1021,7 @@ void WgpuOverlayRenderer::encodeSectionGizmos(WGPURenderPassEncoder pass,
// Marquee
// -----------------------------------------------------------------------------
bool WgpuOverlayRenderer::buildMarquee() {
bool OverlayRenderer::buildMarquee() {
struct Seg { std::array<float, 2> s, e; };
static const Seg segs[] = {
{ {0, 0}, {1, 0} },
@@ -1183,9 +1183,9 @@ bool WgpuOverlayRenderer::buildMarquee() {
return marquee_pipeline_ != nullptr && marquee_fill_pipeline_ != nullptr;
}
void WgpuOverlayRenderer::encodeMarquee(WGPUCommandEncoder enc,
void OverlayRenderer::encodeMarquee(WGPUCommandEncoder enc,
WGPUTextureView surface_view,
const WgpuOverlayFrame& f,
const OverlayFrame& f,
QPoint start_logical_px,
QPoint current_logical_px,
bool active) {
@@ -1258,7 +1258,7 @@ void WgpuOverlayRenderer::encodeMarquee(WGPUCommandEncoder enc,
// Overlay lines
// -----------------------------------------------------------------------------
bool WgpuOverlayRenderer::buildOverlayLines() {
bool OverlayRenderer::buildOverlayLines() {
// Empty initial buffers — both grow on demand inside setOverlayLines.
// Use a tiny starter capacity so the very first set call doesn't have
// to special-case "buffer is null."
@@ -1379,7 +1379,7 @@ bool WgpuOverlayRenderer::buildOverlayLines() {
return overlay_line_pipeline_ != nullptr;
}
void WgpuOverlayRenderer::setOverlayLines(const std::vector<LineGroup>& groups) {
void OverlayRenderer::setOverlayLines(const std::vector<LineGroup>& groups) {
overlay_line_draws_.clear();
if (groups.empty()) return;
@@ -1493,8 +1493,8 @@ void WgpuOverlayRenderer::setOverlayLines(const std::vector<LineGroup>& groups)
}
}
void WgpuOverlayRenderer::encodeOverlayLines(WGPURenderPassEncoder pass,
const WgpuOverlayFrame& f) {
void OverlayRenderer::encodeOverlayLines(WGPURenderPassEncoder pass,
const OverlayFrame& f) {
if (!overlay_line_pipeline_ || overlay_line_draws_.empty()) return;
if (f.viewport_w_px <= 0 || f.viewport_h_px <= 0) return;
@@ -1529,7 +1529,7 @@ void WgpuOverlayRenderer::encodeOverlayLines(WGPURenderPassEncoder pass,
// Overlay points
// -----------------------------------------------------------------------------
bool WgpuOverlayRenderer::buildOverlayPoints() {
bool OverlayRenderer::buildOverlayPoints() {
{
WGPUBufferDescriptor bdesc = {};
bdesc.usage = WGPUBufferUsage_Vertex | WGPUBufferUsage_CopyDst;
@@ -1642,7 +1642,7 @@ bool WgpuOverlayRenderer::buildOverlayPoints() {
return overlay_point_pipeline_ != nullptr;
}
void WgpuOverlayRenderer::setOverlayPoints(const std::vector<float>& world_xyz,
void OverlayRenderer::setOverlayPoints(const std::vector<float>& world_xyz,
float r, float g, float b, float a,
float pixel_size,
float stroke_r, float stroke_g,
@@ -1715,8 +1715,8 @@ void WgpuOverlayRenderer::setOverlayPoints(const std::vector<float>& world_xyz,
&inner_norm, sizeof(inner_norm));
}
void WgpuOverlayRenderer::encodeOverlayPoints(WGPURenderPassEncoder pass,
const WgpuOverlayFrame& f) {
void OverlayRenderer::encodeOverlayPoints(WGPURenderPassEncoder pass,
const OverlayFrame& f) {
if (!overlay_point_pipeline_ || overlay_point_vertex_count_ == 0) return;
if (f.viewport_w_px <= 0 || f.viewport_h_px <= 0) return;
@@ -1738,7 +1738,7 @@ void WgpuOverlayRenderer::encodeOverlayPoints(WGPURenderPassEncoder pass,
// Highlight triangles (translucent world-space triangle list)
// -----------------------------------------------------------------------------
bool WgpuOverlayRenderer::buildHighlightTriangles() {
bool OverlayRenderer::buildHighlightTriangles() {
{
WGPUBufferDescriptor bdesc = {};
bdesc.usage = WGPUBufferUsage_Vertex | WGPUBufferUsage_CopyDst;
@@ -1853,7 +1853,7 @@ bool WgpuOverlayRenderer::buildHighlightTriangles() {
return highlight_pipeline_ != nullptr;
}
void WgpuOverlayRenderer::setHighlightTriangles(
void OverlayRenderer::setHighlightTriangles(
const std::vector<float>& world_xyz,
float r, float g, float b, float a) {
highlight_color_[0] = r;
@@ -1881,8 +1881,8 @@ void WgpuOverlayRenderer::setHighlightTriangles(
highlight_vertex_count_ = uint32_t(n_floats / 3);
}
void WgpuOverlayRenderer::encodeHighlightTriangles(WGPURenderPassEncoder pass,
const WgpuOverlayFrame& f) {
void OverlayRenderer::encodeHighlightTriangles(WGPURenderPassEncoder pass,
const OverlayFrame& f) {
if (!highlight_pipeline_ || highlight_vertex_count_ == 0) return;
// Pack mat4 + vec4 into the slot. mat4 is column-major 16 floats.
uint8_t slot[80] = {};
@@ -1901,7 +1901,7 @@ void WgpuOverlayRenderer::encodeHighlightTriangles(WGPURenderPassEncoder pass,
// Labels + HUD text (textured quads, content-cached)
// -----------------------------------------------------------------------------
bool WgpuOverlayRenderer::buildLabels() {
bool OverlayRenderer::buildLabels() {
{
WGPUSamplerDescriptor sd = {};
sd.minFilter = WGPUFilterMode_Linear;
@@ -2001,7 +2001,7 @@ bool WgpuOverlayRenderer::buildLabels() {
return label_pipeline_ != nullptr;
}
void WgpuOverlayRenderer::releaseLabelTextures() {
void OverlayRenderer::releaseLabelTextures() {
for (auto it = label_tex_cache_.begin(); it != label_tex_cache_.end(); ++it) {
if (it.value().bind_group) wgpuBindGroupRelease(it.value().bind_group);
if (it.value().view) wgpuTextureViewRelease(it.value().view);
@@ -2010,16 +2010,16 @@ void WgpuOverlayRenderer::releaseLabelTextures() {
label_tex_cache_.clear();
}
void WgpuOverlayRenderer::setOverlayLabels(const std::vector<Label>& labels) {
void OverlayRenderer::setOverlayLabels(const std::vector<Label>& labels) {
labels_ = labels;
}
void WgpuOverlayRenderer::setHudText(const QString& text) {
void OverlayRenderer::setHudText(const QString& text) {
hud_text_ = text;
}
WgpuOverlayRenderer::LabelTexture*
WgpuOverlayRenderer::getOrCreateLabelTexture(const QString& cache_key,
OverlayRenderer::LabelTexture*
OverlayRenderer::getOrCreateLabelTexture(const QString& cache_key,
const QString& text,
int font_pt,
int dpr) {
@@ -2129,9 +2129,9 @@ WgpuOverlayRenderer::getOrCreateLabelTexture(const QString& cache_key,
return &inserted.value();
}
void WgpuOverlayRenderer::encodeLabels(WGPUCommandEncoder enc,
void OverlayRenderer::encodeLabels(WGPUCommandEncoder enc,
WGPUTextureView surface_view,
const WgpuOverlayFrame& f) {
const OverlayFrame& f) {
if (!label_pipeline_ || !surface_view) return;
if (labels_.empty() && hud_text_.isEmpty()) {
// Selection cleared / tool exited — drop the cache so we don't
@@ -34,7 +34,7 @@
// 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 WgpuOverlayFrame {
struct OverlayFrame {
QMatrix4x4 view_proj;
QVector3D camera_target;
float camera_distance = 5.0f;
@@ -50,7 +50,7 @@ struct WgpuOverlayFrame {
// 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 WgpuSectionPlane {
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
@@ -61,7 +61,7 @@ struct WgpuSectionPlane {
// All viewport overlays in one place: axis indicator (corner + pivot),
// section plane gizmos, and the marquee drag rect. Mirrors GL's
// OverlayRenderer split so WgpuViewportWindow.cpp doesn't have to
// 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
@@ -69,13 +69,13 @@ struct WgpuSectionPlane {
// uniforms get re-written.
//
// Threading: all calls are main-thread only — they touch the wgpu queue.
class WgpuOverlayRenderer {
class OverlayRenderer {
public:
WgpuOverlayRenderer() = default;
~WgpuOverlayRenderer();
OverlayRenderer() = default;
~OverlayRenderer();
WgpuOverlayRenderer(const WgpuOverlayRenderer&) = delete;
WgpuOverlayRenderer& operator=(const WgpuOverlayRenderer&) = delete;
OverlayRenderer(const OverlayRenderer&) = delete;
OverlayRenderer& operator=(const OverlayRenderer&) = delete;
bool init(WGPUInstance instance, WGPUDevice device, WGPUQueue queue,
WGPUTextureFormat surface_format, int sample_count);
@@ -87,15 +87,15 @@ public:
// 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 WgpuOverlayFrame& f,
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 WgpuOverlayFrame& f,
const std::vector<WgpuSectionPlane>& planes);
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-
@@ -107,7 +107,7 @@ public:
void setHighlightTriangles(const std::vector<float>& world_xyz,
float r, float g, float b, float a);
void encodeHighlightTriangles(WGPURenderPassEncoder pass,
const WgpuOverlayFrame& f);
const OverlayFrame& f);
// One stylistic group of world-space line segments. Mirrors GL
// OverlayRenderer::LineGroup so callers can target either backend
@@ -134,7 +134,7 @@ public:
// per-group uniforms. Drawn inside the main MSAA pass so the lines
// are depth-tested against geometry.
void encodeOverlayLines(WGPURenderPassEncoder pass,
const WgpuOverlayFrame& f);
const OverlayFrame& f);
// Replace the overlay-point set. World-space positions are CPU-
// expanded into screen-space quads at encode time. `pixel_size` is
@@ -153,7 +153,7 @@ public:
// inside the main MSAA pass so depth-test correctly hides points
// behind closer geometry.
void encodeOverlayPoints(WGPURenderPassEncoder pass,
const WgpuOverlayFrame& f);
const OverlayFrame& f);
// World-anchored text label. Mirrors GL OverlayRenderer::Label so
// measure-tool readouts can target either backend.
@@ -175,7 +175,7 @@ public:
// overlay above.
void encodeLabels(WGPUCommandEncoder enc,
WGPUTextureView surface_view,
const WgpuOverlayFrame& f);
const OverlayFrame& f);
// ---- After the edge silhouette pass, on the resolved surface ----
@@ -183,13 +183,13 @@ public:
// projection — only the camera direction matters.
void encodeCornerAxis(WGPUCommandEncoder enc,
WGPUTextureView surface_view,
const WgpuOverlayFrame& f);
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 WgpuOverlayFrame& f,
const OverlayFrame& f,
QPoint start_logical_px,
QPoint current_logical_px,
bool active);
+1 -1
View File
@@ -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,
+5 -5
View File
@@ -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_;
@@ -36,7 +36,7 @@
//
// 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 WgpuSelectionState {
class SelectionState {
public:
void clear() {
if (ids_.empty() && active_ == 0) return;
@@ -33,7 +33,7 @@
// Streaming reader returns offsets to the two skipped sections so chunks
// can be range-read on demand. File handle is closed before return.
#include "WgpuStreamingLoader.h"
#include "StreamingLoader.h"
#include <algorithm>
#include <cstdio>
@@ -17,24 +17,24 @@
* *
********************************************************************************/
#include "WgpuStreamingThread.h"
#include "StreamingThread.h"
#include "WgpuStreamingLoader.h"
#include "StreamingLoader.h"
WgpuStreamingThread::~WgpuStreamingThread() {
StreamingThread::~StreamingThread() {
stop();
}
void WgpuStreamingThread::start() {
void StreamingThread::start() {
std::unique_lock lk(mu_);
if (running_) return;
shutdown_ = false;
running_ = true;
lk.unlock();
worker_ = std::thread(&WgpuStreamingThread::workerLoop, this);
worker_ = std::thread(&StreamingThread::workerLoop, this);
}
void WgpuStreamingThread::stop() {
void StreamingThread::stop() {
{
std::unique_lock lk(mu_);
if (!running_) return;
@@ -48,7 +48,7 @@ void WgpuStreamingThread::stop() {
results_.clear();
}
bool WgpuStreamingThread::enqueue(Request req) {
bool StreamingThread::enqueue(Request req) {
{
std::unique_lock lk(mu_);
if (!running_ || shutdown_) return false;
@@ -58,7 +58,7 @@ bool WgpuStreamingThread::enqueue(Request req) {
return true;
}
std::vector<WgpuStreamingThread::Result> WgpuStreamingThread::drainResults() {
std::vector<StreamingThread::Result> StreamingThread::drainResults() {
std::vector<Result> out;
{
std::unique_lock lk(mu_);
@@ -71,12 +71,12 @@ std::vector<WgpuStreamingThread::Result> WgpuStreamingThread::drainResults() {
return out;
}
std::size_t WgpuStreamingThread::inFlightApprox() const {
std::size_t StreamingThread::inFlightApprox() const {
std::unique_lock lk(mu_);
return requests_.size() + (in_progress_ ? 1u : 0u);
}
void WgpuStreamingThread::workerLoop() {
void StreamingThread::workerLoop() {
for (;;) {
Request req;
{
@@ -41,7 +41,7 @@
// 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 WgpuStreamingThread {
class StreamingThread {
public:
struct Request {
uint32_t model_id;
@@ -63,13 +63,13 @@ public:
std::vector<uint32_t> idx;
};
~WgpuStreamingThread();
~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 WgpuBufferPool the results would upload into
// called before the BufferPool the results would upload into
// is destroyed.
void stop();
File diff suppressed because it is too large Load Diff
@@ -41,12 +41,12 @@
#include <unordered_set>
#include "SidecarCache.h"
#include "WgpuBufferPool.h"
#include "WgpuModelGpuData.h"
#include "WgpuOverlayRenderer.h"
#include "WgpuSelectionState.h"
#include "WgpuStreamingThread.h"
#include "WgpuVisibilityState.h"
#include "BufferPool.h"
#include "ModelGpuData.h"
#include "OverlayRenderer.h"
#include "SelectionState.h"
#include "StreamingThread.h"
#include "VisibilityState.h"
// Stage-2 wgpu viewport: opens a native QWindow, brings up a wgpu instance/
// adapter/device, configures a surface against the platform-native window
@@ -56,11 +56,11 @@
//
// Mirrors the lifecycle shape of the GL ViewportWindow so subsequent stages
// can grow this into a full IFC renderer without restructuring the host.
class WgpuViewportWindow : public QWindow {
class ViewportWindow : public QWindow {
Q_OBJECT
public:
explicit WgpuViewportWindow(QWindow* parent = nullptr);
~WgpuViewportWindow();
explicit ViewportWindow(QWindow* parent = nullptr);
~ViewportWindow();
void setBackgroundColor(const QColor& color);
@@ -103,7 +103,7 @@ public:
void resetScene();
// Model-level visibility. Mirrors the GL ViewportWindow API — flips
// WgpuModelGpuData::hidden, which every render/pick/cull pass already
// ModelGpuData::hidden, which every render/pick/cull pass already
// consults. requestUpdate() so the change is visible immediately.
void hideModel(uint32_t model_id);
void showModel(uint32_t model_id);
@@ -213,7 +213,7 @@ private:
// 2D pixel area covered on screen. This is the streaming loader's
// chunk-priority metric — extracted from driveStreamingLoads as a
// member so the click-and-track diagnostic can compare scores.
float chunkScreenAreaPx(const WgpuModelGpuData::Chunk& c,
float chunkScreenAreaPx(const ModelGpuData::Chunk& c,
const QMatrix4x4& vp_mat) const;
public:
@@ -250,27 +250,27 @@ private:
void shutdown();
bool buildPipelines();
void buildModelBindGroup(WgpuModelGpuData& m);
void buildChunkBindGroup(WgpuModelGpuData& m, size_t chunk_idx);
void buildModelBindGroup(ModelGpuData& m);
void buildChunkBindGroup(ModelGpuData& m, size_t chunk_idx);
// Streaming: read the chunk's vertex + index bytes from disk,
// sub-allocate ranges in pool_, queueWriteBuffer them in, build the
// chunk's bind group, flip is_resident=true. Returns true on success;
// false if either the disk read or a pool alloc fails (caller is
// expected to have already evicted enough). No-op (returns true)
// when already resident.
bool loadChunkBytesAndUploadGpu(WgpuModelGpuData& m, size_t chunk_idx);
bool loadChunkBytesAndUploadGpu(ModelGpuData& m, size_t chunk_idx);
// Pool-allocate + queueWriteBuffer + build bind group for a chunk
// whose vbytes/idx have already been read (by either the worker
// thread's drained result or the sync fallback). Returns false on
// pool OOM. Toggles is_resident=true / is_loading=false on success.
bool applyStreamedChunk(WgpuModelGpuData& m, size_t chunk_idx,
bool applyStreamedChunk(ModelGpuData& m, size_t chunk_idx,
const std::vector<uint8_t>& vbytes,
const std::vector<uint32_t>& idx);
// Release a resident chunk's pool ranges + bind group; flip
// is_resident=false. The chunk's CPU metadata (offsets, AABB,
// visible-draw scratch) is retained so a subsequent
// loadChunkBytesAndUploadGpu can bring it back without re-planning.
void unloadChunk(WgpuModelGpuData& m, size_t chunk_idx);
void unloadChunk(ModelGpuData& m, size_t chunk_idx);
// Called from render() after cull: find non-resident chunks with
// current visible draw counts > 0 and bring them resident. When the
// pool is full, evicts LRU non-visible chunks first, then falls back
@@ -294,7 +294,7 @@ private:
// Show/hide the pivot indicator. hide_after_ms > 0 starts the
// single-shot auto-hide timer used by the wheel-zoom afterglow;
// drag callers pass 0 and toggle manually on press/release. The
// actual gizmo rendering lives in WgpuOverlayRenderer — this just
// actual gizmo rendering lives in OverlayRenderer — this just
// manages the UI-side visibility timer.
void setPivotIndicatorVisible(bool visible, int hide_after_ms = 0);
void releaseEdgeResources();
@@ -355,14 +355,14 @@ public:
// Overlay primitives. Mirror GL ViewportWindow so the Measurement +
// dimension tools can target either backend through one API.
// Empty inputs clears the corresponding set.
void setOverlayLines(const std::vector<WgpuOverlayRenderer::LineGroup>& groups);
void setOverlayLines(const std::vector<OverlayRenderer::LineGroup>& groups);
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);
void setOverlayLabels(const std::vector<WgpuOverlayRenderer::Label>& labels);
void setOverlayLabels(const std::vector<OverlayRenderer::Label>& labels);
void setHudText(const QString& text);
// Translucent world-space triangle overlay (Area-tool patch shading).
// Empty list disables; color is RGBA in [0, 1].
@@ -375,7 +375,7 @@ public:
// (model_id, mesh_id) pair doesn't resolve. Matches the GL
// ViewportWindow::MeshTriangles + readbackMeshTriangles shape so
// the measure tools port verbatim.
using MeshTriangles = WgpuModelGpuData::MeshTriangles;
using MeshTriangles = ModelGpuData::MeshTriangles;
bool readbackMeshTriangles(uint32_t model_id, uint32_t mesh_id,
MeshTriangles& out) const;
@@ -423,8 +423,8 @@ public:
// Selection accessor. Exposed for callers (bonsai's volume readout)
// that need to read selectionIds() / activeObjectId(). Mutation goes
// through the existing setSelection / pick paths.
WgpuSelectionState& selection() { return selection_; }
const WgpuSelectionState& selection() const { return selection_; }
SelectionState& selection() { return selection_; }
const SelectionState& selection() const { return selection_; }
// Pick + resolve to mesh-local space. Runs pickSurfaceAt to get the
// world-space hit, then inverts the instance's composed transform
@@ -499,14 +499,14 @@ signals:
// Selection moved by a pick / marquee. Emitted with the active id
// (0 = miss). Bonsai mirrors this into SessionState.
void objectPicked(uint32_t object_id);
void frameStatsUpdated(const WgpuViewportWindow::FrameStats& stats);
void frameStatsUpdated(const ViewportWindow::FrameStats& stats);
// Emitted instead of objectPicked when an Area / Length tool is
// active. The host branches on toolMode() and calls
// pickMeshLocalAt(x, y, ...) for hit details. Coordinates are in
// physical pixels (post-DPR).
void surfacePickedInTool(int x, int y, int modifiers);
// Emitted whenever the active tool changes (incl. on→off).
void toolModeChanged(WgpuViewportWindow::ToolMode mode);
void toolModeChanged(ViewportWindow::ToolMode mode);
// Backspace/Delete pressed while a tool is active. Length tool's
// remove-last-point; other tools may ignore.
void toolBackspacePressed();
@@ -578,7 +578,7 @@ private:
// is overwhelmingly thin-in-one-axis (pipes, columns, slabs,
// windows). Sphere projection is kept for contribution / LOD picks
// because conservative-over is the right failure mode there.
uint32_t cullModelCpuCompute(WgpuModelGpuData& m,
uint32_t cullModelCpuCompute(ModelGpuData& m,
const float planes[6][4],
const float eye[3],
const float forward[3],
@@ -591,7 +591,7 @@ private:
// Upload phase: wgpuQueueWriteBuffer for visible_draws / prefix_sums /
// per-model uniform. Main-thread only (wgpu queue ops are not all
// thread-safe).
void cullModelCpuUpload(WgpuModelGpuData& m);
void cullModelCpuUpload(ModelGpuData& m);
// Compose one instance's `transform` (float[16] column-major) from
// FederatedFalseOrigin · ModelTransformation · CoordinateOperation
@@ -601,7 +601,7 @@ private:
// get cancelled by the federation false origin before precision is
// narrowed. Mirrors GL ViewportWindow::composeInstanceFromPlacement.
void composeInstanceFromPlacement(InstanceCpu& inst,
const WgpuModelGpuData& m) const;
const ModelGpuData& m) const;
// Walk every instance of `model_id`, recompose its transform from the
// current federation matrices, refresh per-chunk world AABBs, and
@@ -638,7 +638,7 @@ private:
// frame bind group because object_ids are globally unique across models.
WGPUBuffer selection_flags_buffer_ = nullptr;
uint32_t selection_flags_capacity_ = 0; // number of u32 entries
WgpuSelectionState selection_;
SelectionState selection_;
std::vector<uint32_t> selection_flags_scratch_;
// Per-element visibility. Consulted in cullModelCpuCompute to drop
@@ -646,7 +646,7 @@ private:
// hidden geometry out of cost on every axis (no draw, no depth, no
// pick). Mutated on the main thread between renders; cull workers
// read concurrently which is safe as long as no concurrent writes.
WgpuVisibilityState visibility_;
VisibilityState visibility_;
// Depth attachment (4× MSAA), recreated on surface resize.
WGPUTexture depth_texture_ = nullptr;
@@ -711,9 +711,9 @@ private:
// All viewport overlays (axis indicator, section gizmos, marquee
// rect) — pipelines + shaders + buffers + encoders. The viewport
// builds a WgpuOverlayFrame each frame and asks the renderer to
// encode each overlay; see WgpuOverlayRenderer.h.
WgpuOverlayRenderer overlays_;
// builds a OverlayFrame each frame and asks the renderer to
// encode each overlay; see OverlayRenderer.h.
OverlayRenderer overlays_;
// Active measurement tool. setToolMode() / setSelection mutations
// both funnel into updateVolumeReadout() which pushes the HUD +
@@ -724,18 +724,18 @@ private:
// after entering Volume mode this primes the overlay.
void updateVolumeReadout();
// Area tool state lives in WgpuAreaMeasurement (header below). The
// Area tool state lives in AreaMeasurement (header below). The
// viewport owns it for the session and routes LMB picks in Area
// mode through onAreaPick.
std::unique_ptr<class WgpuAreaMeasurement> area_tool_;
std::unique_ptr<class AreaMeasurement> area_tool_;
void onAreaPick(int x_phys, int y_phys, bool alt);
void updateAreaHud();
// Length tool state lives in WgpuLengthMeasurement. Same lifecycle
// Length tool state lives in LengthMeasurement. Same lifecycle
// pattern: lazily constructed on first L press, cleared on tool
// exit, click handler routes LMB through onLengthPick + Backspace
// through onLengthBackspace.
std::unique_ptr<class WgpuLengthMeasurement> length_tool_;
std::unique_ptr<class LengthMeasurement> length_tool_;
void onLengthPick(int x_phys, int y_phys, bool alt);
void onLengthBackspace();
@@ -759,10 +759,10 @@ private:
int pick_w_ = 0;
int pick_h_ = 0;
// Section-cutting state. WgpuSectionPlane lives in WgpuOverlayRenderer.h
// Section-cutting state. SectionPlane lives in OverlayRenderer.h
// because the visualiser reads it; the viewport owns the authoritative
// vector that the section tool mutates.
std::vector<WgpuSectionPlane> section_planes_;
std::vector<SectionPlane> section_planes_;
bool section_tool_active_ = false;
// Marquee box-select. Armed on LMB press (when no other tool consumes
@@ -934,14 +934,14 @@ public:
// ceiling that one-WGPUBuffer-per-chunk would otherwise hit. All
// chunk allocations land here; nothing else uses the pool. Replaces
// the old hand-picked streaming_vram_budget_bytes_ knob entirely.
WgpuBufferPool pool_;
BufferPool pool_;
// Background worker that does scatter-gather chunk reads off the
// render thread. driveStreamingLoads enqueues requests for visible
// non-resident chunks and drains completed results into the pool
// on subsequent frames. Kills the 100-300 ms per-frame stutters
// that synchronous disk reads caused during orbit.
WgpuStreamingThread streaming_thread_;
StreamingThread streaming_thread_;
// Per-frame streaming activity, written by driveStreamingLoads,
// consumed by the benchmark harness to delay the orbit sweep until
@@ -976,7 +976,7 @@ private:
float lod1_pixel_threshold_ = 30.0f;
// Per-model state, keyed by viewport-assigned model_id.
std::unordered_map<uint32_t, WgpuModelGpuData> models_gpu_;
std::unordered_map<uint32_t, ModelGpuData> models_gpu_;
uint32_t next_model_id_ = 1;
// Globally-unique object_id allocator. Each applyCachedModel rebases
// the sidecar's local object_ids by base_object_id_so_far so picks
@@ -32,7 +32,7 @@
// 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 WgpuVisibilityState {
class VisibilityState {
public:
bool isHidden(uint32_t object_id) const {
return hidden_ids_.count(object_id) > 0;
+5
View File
@@ -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})
@@ -17,19 +17,19 @@
* *
********************************************************************************/
// Tier-1 coverage of WgpuSelectionState — the CPU-side selection set + active
// 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 "WgpuSelectionState.h"
#include "SelectionState.h"
#include <catch2/catch_test_macros.hpp>
#include <vector>
TEST_CASE("WgpuSelectionState starts empty with no active id", "[wgpu-selection]") {
WgpuSelectionState sel;
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));
@@ -38,7 +38,7 @@ TEST_CASE("WgpuSelectionState starts empty with no active id", "[wgpu-selection]
TEST_CASE("replace(id) selects a single id and makes it active",
"[wgpu-selection]") {
WgpuSelectionState sel;
SelectionState sel;
sel.replace(5);
REQUIRE(sel.count() == 1);
@@ -48,7 +48,7 @@ TEST_CASE("replace(id) selects a single id and makes it active",
}
TEST_CASE("replace(0) clears the selection", "[wgpu-selection]") {
WgpuSelectionState sel;
SelectionState sel;
sel.replace(5);
sel.markClean();
@@ -59,7 +59,7 @@ TEST_CASE("replace(0) clears the selection", "[wgpu-selection]") {
}
TEST_CASE("add(id) appends to the set and steals active", "[wgpu-selection]") {
WgpuSelectionState sel;
SelectionState sel;
sel.replace(1);
sel.markClean();
@@ -75,7 +75,7 @@ TEST_CASE("add(id) appends to the set and steals active", "[wgpu-selection]") {
}
TEST_CASE("add(0) is ignored", "[wgpu-selection]") {
WgpuSelectionState sel;
SelectionState sel;
sel.replace(1);
sel.markClean();
@@ -86,7 +86,7 @@ TEST_CASE("add(0) is ignored", "[wgpu-selection]") {
}
TEST_CASE("remove(non-active) keeps active", "[wgpu-selection]") {
WgpuSelectionState sel;
SelectionState sel;
sel.replace(1);
sel.add(2);
sel.add(3);
@@ -101,7 +101,7 @@ TEST_CASE("remove(non-active) keeps active", "[wgpu-selection]") {
}
TEST_CASE("remove(active) falls back to some remaining id", "[wgpu-selection]") {
WgpuSelectionState sel;
SelectionState sel;
sel.replace(1);
sel.add(2);
sel.add(3);
@@ -119,7 +119,7 @@ TEST_CASE("remove(active) falls back to some remaining id", "[wgpu-selection]")
}
TEST_CASE("remove(last id) clears active", "[wgpu-selection]") {
WgpuSelectionState sel;
SelectionState sel;
sel.replace(7);
REQUIRE(sel.activeId() == 7);
@@ -130,7 +130,7 @@ TEST_CASE("remove(last id) clears active", "[wgpu-selection]") {
TEST_CASE("remove(non-existent) is a no-op for state, no dirty flag",
"[wgpu-selection]") {
WgpuSelectionState sel;
SelectionState sel;
sel.replace(1);
sel.markClean();
@@ -141,7 +141,7 @@ TEST_CASE("remove(non-existent) is a no-op for state, no dirty flag",
}
TEST_CASE("remove(0) is ignored", "[wgpu-selection]") {
WgpuSelectionState sel;
SelectionState sel;
sel.replace(1);
sel.markClean();
@@ -151,7 +151,7 @@ TEST_CASE("remove(0) is ignored", "[wgpu-selection]") {
}
TEST_CASE("toggle adds when absent, removes when present", "[wgpu-selection]") {
WgpuSelectionState sel;
SelectionState sel;
sel.toggle(5);
REQUIRE(sel.contains(5));
@@ -171,7 +171,7 @@ TEST_CASE("toggle adds when absent, removes when present", "[wgpu-selection]") {
}
TEST_CASE("toggle(0) is ignored", "[wgpu-selection]") {
WgpuSelectionState sel;
SelectionState sel;
sel.markClean();
sel.toggle(0);
@@ -180,7 +180,7 @@ TEST_CASE("toggle(0) is ignored", "[wgpu-selection]") {
}
TEST_CASE("clear empties; no-op when already empty", "[wgpu-selection]") {
WgpuSelectionState sel;
SelectionState sel;
sel.clear(); // already empty
REQUIRE_FALSE(sel.dirty());
@@ -194,7 +194,7 @@ TEST_CASE("clear empties; no-op when already empty", "[wgpu-selection]") {
}
TEST_CASE("markClean clears the dirty flag", "[wgpu-selection]") {
WgpuSelectionState sel;
SelectionState sel;
sel.replace(5);
REQUIRE(sel.dirty());
@@ -207,7 +207,7 @@ TEST_CASE("markClean clears the dirty flag", "[wgpu-selection]") {
}
TEST_CASE("selectionIds returns the live set", "[wgpu-selection]") {
WgpuSelectionState sel;
SelectionState sel;
sel.add(1);
sel.add(2);
sel.add(3);
@@ -221,7 +221,7 @@ TEST_CASE("selectionIds returns the live set", "[wgpu-selection]") {
TEST_CASE("fillFlagsArray packs selected/active bits per object_id",
"[wgpu-selection]") {
WgpuSelectionState sel;
SelectionState sel;
sel.add(1);
sel.add(3); // active is now 3
@@ -241,7 +241,7 @@ TEST_CASE("fillFlagsArray packs selected/active bits per object_id",
TEST_CASE("fillFlagsArray drops ids past the entries cap",
"[wgpu-selection]") {
WgpuSelectionState sel;
SelectionState sel;
sel.add(1);
sel.add(100); // active is 100
@@ -17,18 +17,18 @@
* *
********************************************************************************/
// Tier-1 coverage of WgpuVisibilityState — the per-element hidden-id set
// 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
// WgpuViewportWindow (which composes WgpuVisibilityState + the model
// ViewportWindow (which composes VisibilityState + the model
// instance lists) and would need an integration test, not a Tier-1 unit.
#include "WgpuVisibilityState.h"
#include "VisibilityState.h"
#include <catch2/catch_test_macros.hpp>
TEST_CASE("WgpuVisibilityState starts empty", "[wgpu-visibility]") {
WgpuVisibilityState vis;
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));
@@ -37,7 +37,7 @@ TEST_CASE("WgpuVisibilityState starts empty", "[wgpu-visibility]") {
TEST_CASE("hide(id) records the id; isHidden reflects it",
"[wgpu-visibility]") {
WgpuVisibilityState vis;
VisibilityState vis;
vis.hide(1);
vis.hide(2);
@@ -51,7 +51,7 @@ TEST_CASE("hide(id) records the id; isHidden reflects it",
}
TEST_CASE("hide is idempotent", "[wgpu-visibility]") {
WgpuVisibilityState vis;
VisibilityState vis;
vis.hide(5);
vis.hide(5);
@@ -62,7 +62,7 @@ TEST_CASE("hide is idempotent", "[wgpu-visibility]") {
}
TEST_CASE("hide(0) is ignored", "[wgpu-visibility]") {
WgpuVisibilityState vis;
VisibilityState vis;
vis.hide(0);
REQUIRE(vis.hiddenCount() == 0);
@@ -71,7 +71,7 @@ TEST_CASE("hide(0) is ignored", "[wgpu-visibility]") {
TEST_CASE("show(id) removes a previously hidden id",
"[wgpu-visibility]") {
WgpuVisibilityState vis;
VisibilityState vis;
vis.hide(1);
vis.hide(2);
@@ -82,7 +82,7 @@ TEST_CASE("show(id) removes a previously hidden id",
}
TEST_CASE("show(non-hidden) is a no-op", "[wgpu-visibility]") {
WgpuVisibilityState vis;
VisibilityState vis;
vis.hide(1);
vis.show(99); // never hidden
@@ -91,7 +91,7 @@ TEST_CASE("show(non-hidden) is a no-op", "[wgpu-visibility]") {
}
TEST_CASE("clear() drops every hidden id", "[wgpu-visibility]") {
WgpuVisibilityState vis;
VisibilityState vis;
vis.hide(1);
vis.hide(2);
vis.hide(3);
@@ -104,7 +104,7 @@ TEST_CASE("clear() drops every hidden id", "[wgpu-visibility]") {
}
TEST_CASE("hiddenIds returns the live set", "[wgpu-visibility]") {
WgpuVisibilityState vis;
VisibilityState vis;
vis.hide(10);
vis.hide(20);
vis.hide(30);