Route bonsai through wgpu; delete the GL backend

Bonsai now drives the wgpu viewport for both sidecar and direct-IFC
loads. The GL viewer and its supporting state classes are gone.

SceneLoader rewire:
- Takes WgpuViewportWindow* instead of ViewportWindow*.
- Sidecar path reads metadata only (readSidecarMetadataOnly) and hands
  the StreamingSidecar off to the new applyCachedModel. Field accesses
  inside applySidecarData go through .meta.
- Direct-IFC path uses the wgpu A-path (upload{Mesh,Instance}Chunk +
  finalizeModel). The applyLodExtension call is dropped — wgpu has no
  live LOD1 splice; LOD1 still lands in the on-disk sidecar for the
  next open.

Bonsai migration:
- ViewportWindow → WgpuViewportWindow across MainWindow, Measurement,
  SessionState, and every modules/*/{Commands,Panel,View}.{h,cpp} —
  116 sites total. Same s/OverlayRenderer::/WgpuOverlayRenderer::/
  rename, 12 sites.
- Includes flipped from ../ifcviewer/ViewportWindow.h to
  ../ifcviewer-wgpu/WgpuViewportWindow.h. OverlayRenderer.h include
  dropped (transitively reached via the viewport header).
- BonsaiViewer links IfcViewerWgpu in addition to IfcViewer for the
  duration of the migration; the GL-side IfcViewer also publicly links
  IfcViewerWgpu so SceneLoader can resolve WgpuViewportWindow.

GL backend deletion:
- src/ifcviewer/ViewportWindow.{cpp,h}, BvhAccel.*, OverlayRenderer.*,
  Selection.*, Visibility.* all gone.
- src/ifcviewer-minimal/ removed entirely (MinimalWindow drove the GL
  viewport).
- src/ifcviewer/tests: test_bvh_accel, test_selection, test_visibility
  removed. The first has no replacement (wgpu doesn't use a per-instance
  BVH); the latter two are ported separately. test_lod_builder,
  test_sidecar_cache, test_instanced_geometry, test_federation remain
  (backend-agnostic).
- IfcViewer's CMakeLists drops OpenGL, Qt::OpenGL, Qt::Widgets — none
  of the surviving translation units reach for them.

Build flag plumbing:
- BUILD_BONSAIVIEWER now auto-enables BUILD_BONSAIVIEWER_WGPU since
  SceneLoader requires the wgpu lib for its WgpuViewportWindow* arg.
- The wgpu subprojects add_subdirectory ahead of the GL one so
  IfcViewerWgpu exists when IfcViewer's link evaluates.
- src/ifcviewer-minimal subdir reference removed from cmake/CMakeLists.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
Dion Moult
2026-06-01 17:38:33 +10:00
parent 9c067d1d0e
commit 2981500b3b
40 changed files with 208 additions and 8008 deletions
+4
View File
@@ -121,7 +121,11 @@ set_target_properties(BonsaiViewer PROPERTIES
)
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
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/ViewportWindow.h"
#include "../ifcviewer-wgpu/WgpuViewportWindow.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(), &ViewportWindow::frameStatsUpdated, this,
[this](const ViewportWindow::FrameStats& s) {
connect(viewport_widget_->viewport(), &WgpuViewportWindow::frameStatsUpdated, this,
[this](const WgpuViewportWindow::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(), &ViewportWindow::objectPicked,
connect(viewport_widget_->viewport(), &WgpuViewportWindow::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 "ViewportWindow.h"
#include "WgpuViewportWindow.h"
#include <QtGlobal>
#include <Eigen/Dense>
@@ -35,7 +35,7 @@
namespace {
double meshLocalVolume(const ViewportWindow::MeshTriangles& tris) {
double meshLocalVolume(const WgpuViewportWindow::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(ViewportWindow& vp,
double volumeOfObjects(WgpuViewportWindow& vp,
const std::vector<uint32_t>& object_ids) {
if (object_ids.empty()) return 0.0;
@@ -77,14 +77,14 @@ double volumeOfObjects(ViewportWindow& vp,
std::unordered_map<uint64_t, std::vector<double>> by_mesh;
by_mesh.reserve(object_ids.size());
for (uint32_t oid : object_ids) {
ViewportWindow::InstanceLookup lk;
WgpuViewportWindow::InstanceLookup lk;
if (!vp.findInstance(oid, lk)) continue;
const uint64_t key = (uint64_t(lk.model_id) << 32) | lk.mesh_id;
by_mesh[key].push_back(std::abs(det3(lk.placement_transformation)));
}
double total = 0.0;
ViewportWindow::MeshTriangles tris;
WgpuViewportWindow::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(ViewportWindow& vp,
}
std::vector<std::pair<uint32_t, double>>
volumesPerObject(ViewportWindow& vp,
volumesPerObject(WgpuViewportWindow& 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(ViewportWindow& vp,
std::unordered_map<uint64_t, double> mesh_vol_local;
mesh_vol_local.reserve(object_ids.size());
ViewportWindow::MeshTriangles tris;
WgpuViewportWindow::MeshTriangles tris;
for (uint32_t oid : object_ids) {
ViewportWindow::InstanceLookup lk;
WgpuViewportWindow::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(ViewportWindow& vp) {
void AreaMeasurement::clear(WgpuViewportWindow& vp) {
mesh_cache_.clear();
selected_.clear();
total_area_m2_ = 0.0;
@@ -246,7 +246,7 @@ void AreaMeasurement::clear(ViewportWindow& vp) {
vp.setOverlayLabels({});
}
void AreaMeasurement::rebuildHighlight(ViewportWindow& vp) {
void AreaMeasurement::rebuildHighlight(WgpuViewportWindow& 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(ViewportWindow& vp) {
by_object[object_id].push_back(&sel);
}
std::vector<OverlayRenderer::Label> labels;
std::vector<WgpuOverlayRenderer::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(ViewportWindow& vp) {
// Centroid → world via the instance's composed transform.
const float* M = any.composed_transform;
OverlayRenderer::Label lbl;
WgpuOverlayRenderer::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(ViewportWindow& vp) {
vp.setOverlayLabels(labels);
}
AreaMeasurement::MeshCache* AreaMeasurement::meshCache(ViewportWindow& vp,
AreaMeasurement::MeshCache* AreaMeasurement::meshCache(WgpuViewportWindow& 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;
ViewportWindow::MeshTriangles tris;
WgpuViewportWindow::MeshTriangles tris;
if (!vp.readbackMeshTriangles(model_id, mesh_id, tris)) return nullptr;
MeshCache c;
@@ -397,8 +397,8 @@ AreaMeasurement::MeshCache* AreaMeasurement::meshCache(ViewportWindow& vp,
return &mesh_cache_.emplace(key, std::move(c)).first->second;
}
void AreaMeasurement::onPick(ViewportWindow& vp, int x, int y, bool alt) {
ViewportWindow::MeshLocalPick pick;
void AreaMeasurement::onPick(WgpuViewportWindow& vp, int x, int y, bool alt) {
WgpuViewportWindow::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
OverlayRenderer::LineGroup makeGroup(std::vector<float> xyz,
WgpuOverlayRenderer::LineGroup makeGroup(std::vector<float> xyz,
float r, float g, float b,
bool dashed = false) {
OverlayRenderer::LineGroup gp;
WgpuOverlayRenderer::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());
}
OverlayRenderer::Label makeLabel(const std::array<float, 3>& a,
WgpuOverlayRenderer::Label makeLabel(const std::array<float, 3>& a,
const std::array<float, 3>& b,
const QString& text) {
OverlayRenderer::Label lbl;
WgpuOverlayRenderer::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 @@ OverlayRenderer::Label makeLabel(const std::array<float, 3>& a,
return lbl;
}
void pushDots(ViewportWindow& vp, const std::vector<float>& xyz) {
void pushDots(WgpuViewportWindow& 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(ViewportWindow& vp, const std::vector<float>& xyz) {
} // namespace
void LengthMeasurement::clear(ViewportWindow& vp) {
void LengthMeasurement::clear(WgpuViewportWindow& vp) {
points_.clear();
normals_.clear();
vp.setOverlayPoints({}, 0,0,0,0, 0, 0,0,0,0, 0);
@@ -665,8 +665,8 @@ void LengthMeasurement::clear(ViewportWindow& vp) {
vp.setHudText(QString());
}
void LengthMeasurement::onPick(ViewportWindow& vp, int x, int y, bool /*alt*/) {
ViewportWindow::MeshLocalPick pick;
void LengthMeasurement::onPick(WgpuViewportWindow& vp, int x, int y, bool /*alt*/) {
WgpuViewportWindow::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(ViewportWindow& vp, int x, int y, bool /*alt*/) {
rebuildOverlay(vp);
}
void LengthMeasurement::removeLastPoint(ViewportWindow& vp) {
void LengthMeasurement::removeLastPoint(WgpuViewportWindow& vp) {
if (points_.empty()) return;
points_.pop_back();
if (!normals_.empty()) normals_.pop_back();
rebuildOverlay(vp);
}
void LengthMeasurement::rebuildOverlay(ViewportWindow& vp) {
void LengthMeasurement::rebuildOverlay(WgpuViewportWindow& vp) {
if (points_.size() == 1 && normals_.size() == 1) {
rebuildLaserOverlay(vp);
return;
@@ -694,8 +694,8 @@ void LengthMeasurement::rebuildOverlay(ViewportWindow& vp) {
for (const auto& p : points_) pushDot(pts_xyz, p);
pushDots(vp, pts_xyz);
std::vector<OverlayRenderer::LineGroup> groups;
std::vector<OverlayRenderer::Label> labels;
std::vector<WgpuOverlayRenderer::LineGroup> groups;
std::vector<WgpuOverlayRenderer::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(ViewportWindow& vp) {
void LengthMeasurement::rebuildLaserOverlay(WgpuViewportWindow& 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(ViewportWindow& vp) {
n[0]*t1[1] - n[1]*t1[0],
};
std::vector<OverlayRenderer::LineGroup> groups;
std::vector<OverlayRenderer::Label> labels;
std::vector<WgpuOverlayRenderer::LineGroup> groups;
std::vector<WgpuOverlayRenderer::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(ViewportWindow& 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).
ViewportWindow::MeshTriangles tris;
WgpuViewportWindow::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(ViewportWindow& vp) {
wp[1] + NUDGE * n[1],
wp[2] + NUDGE * n[2],
};
ViewportWindow::RaycastHit hit;
WgpuViewportWindow::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 "ViewportWindow.h"
#include "WgpuViewportWindow.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(ViewportWindow& vp,
double volumeOfObjects(WgpuViewportWindow& vp,
const std::vector<uint32_t>& object_ids);
// Per-object volumes (m³). Same algorithm as volumeOfObjects but
@@ -45,11 +45,11 @@ double volumeOfObjects(ViewportWindow& 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(ViewportWindow& vp,
volumesPerObject(WgpuViewportWindow& vp,
const std::vector<uint32_t>& object_ids);
// Click-to-accumulate area measurement. Each pick resolves the screen
// click to a (instance, triangle) using ViewportWindow's primitives,
// click to a (instance, triangle) using WgpuViewportWindow'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(ViewportWindow& vp,
// separate patches and their areas are summed.
//
// On every pick the world-space triangles of the running set are pushed
// to ViewportWindow::setHighlightTriangles for in-viewport shading.
// to WgpuViewportWindow::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(ViewportWindow& vp, int x, int y, bool alt);
void onPick(WgpuViewportWindow& vp, int x, int y, bool alt);
// Wipe all accumulated triangles, per-mesh adjacency caches, and the
// viewport overlay.
void clear(ViewportWindow& vp);
void clear(WgpuViewportWindow& 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(ViewportWindow& vp, uint32_t model_id, uint32_t mesh_id);
MeshCache* meshCache(WgpuViewportWindow& 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(ViewportWindow& vp);
void rebuildHighlight(WgpuViewportWindow& 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(ViewportWindow& vp, int x, int y, bool alt);
void removeLastPoint(ViewportWindow& vp);
void clear(ViewportWindow& vp);
void onPick(WgpuViewportWindow& vp, int x, int y, bool alt);
void removeLastPoint(WgpuViewportWindow& vp);
void clear(WgpuViewportWindow& vp);
size_t pointCount() const { return points_.size(); }
private:
void rebuildOverlay(ViewportWindow& vp);
void rebuildLaserOverlay(ViewportWindow& vp);
void rebuildOverlay(WgpuViewportWindow& vp);
void rebuildLaserOverlay(WgpuViewportWindow& 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).
ViewportWindow::MeshLocalPick first_pick_{};
WgpuViewportWindow::MeshLocalPick first_pick_{};
};
#endif // IFCINTERFACE_MEASUREMENT_H
+1 -1
View File
@@ -35,7 +35,7 @@ SessionState::SessionState(QObject* parent)
{
}
void SessionState::createLoader(ViewportWindow* viewport) {
void SessionState::createLoader(WgpuViewportWindow* 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 ViewportWindow;
class WgpuViewportWindow;
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(ViewportWindow* viewport);
void createLoader(WgpuViewportWindow* 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/ViewportWindow.h"
#include "../../../ifcviewer-wgpu/WgpuViewportWindow.h"
#include "../../../ifcgeom/Serializer.h"
#include "../../../serializers/document_serializer_plugin.h"
@@ -148,7 +148,7 @@ void removeGroup(SessionState& s, QWidget& host, const QString& group_id) {
s.setStatusMessage("Models", "Group removed");
}
void removeModel(SessionState& s, ViewportWindow& vp, QWidget& host, const QString& fed_id) {
void removeModel(SessionState& s, WgpuViewportWindow& 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 ViewportWindow;
class WgpuViewportWindow;
namespace bonsaiviewer { class SessionState; }
namespace bonsaiviewer::modules::models::commands {
@@ -41,7 +41,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, ViewportWindow& vp, QWidget& host, const QString& fed_id);
void removeModel(SessionState& s, WgpuViewportWindow& 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,
ViewportWindow* viewport,
WgpuViewportWindow* 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 ViewportWindow;
class WgpuViewportWindow;
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,
ViewportWindow* viewport,
WgpuViewportWindow* 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;
ViewportWindow* viewport_ = nullptr;
WgpuViewportWindow* 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/ViewportWindow.h"
#include "../../../ifcviewer-wgpu/WgpuViewportWindow.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, ViewportWindow& vp) {
void clearScene(SessionState& s, WgpuViewportWindow& 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, ViewportWindow& vp) {
void resolveCloudModels(SessionState& s, WgpuViewportWindow& 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, ViewportWindow& vp) {
auto* registry = s.connectorRegistry();
QPointer<SessionState> sguard(&s);
QPointer<ViewportWindow> vguard(&vp);
QPointer<WgpuViewportWindow> 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, ViewportWindow& vp, const QString& path) {
bool openProjectAt(SessionState& s, QWidget& host, WgpuViewportWindow& 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, ViewportWindow& vp) {
bool newProject(SessionState& s, QWidget& host, WgpuViewportWindow& vp) {
SceneLoader* loader = s.loader();
if (loader && loader->isLoading()) {
QMessageBox::information(
@@ -285,7 +285,7 @@ bool newProject(SessionState& s, QWidget& host, ViewportWindow& vp) {
return true;
}
bool openProject(SessionState& s, QWidget& host, ViewportWindow& vp) {
bool openProject(SessionState& s, QWidget& host, WgpuViewportWindow& 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, ViewportWindow& vp) {
return openProjectAt(s, host, vp, path);
}
bool openProjectPath(SessionState& s, QWidget& host, ViewportWindow& vp, const QString& path) {
bool openProjectPath(SessionState& s, QWidget& host, WgpuViewportWindow& vp, const QString& path) {
if (path.isEmpty()) return false;
return openProjectAt(s, host, vp, path);
}
bool openCloudProject(SessionState& s, QWidget& host, ViewportWindow& vp) {
bool openCloudProject(SessionState& s, QWidget& host, WgpuViewportWindow& vp) {
SceneLoader* loader = s.loader();
if (loader && loader->isLoading()) {
QMessageBox::information(
@@ -341,7 +341,7 @@ bool openCloudProject(SessionState& s, QWidget& host, ViewportWindow& vp) {
QPointer<SessionState> sguard(&s);
QPointer<QWidget> hguard(&host);
QPointer<ViewportWindow> vguard(&vp);
QPointer<WgpuViewportWindow> 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, ViewportWindow& vp) {
return true;
}
bool syncCloudProject(SessionState& s, QWidget& host, ViewportWindow& vp) {
bool syncCloudProject(SessionState& s, QWidget& host, WgpuViewportWindow& 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, ViewportWindow& vp) {
QPointer<SessionState> sguard(&s);
QPointer<QWidget> hguard(&host);
QPointer<ViewportWindow> vguard(&vp);
QPointer<WgpuViewportWindow> 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 ViewportWindow;
class WgpuViewportWindow;
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, ViewportWindow& vp);
bool openProject(SessionState& s, QWidget& host, ViewportWindow& vp);
bool newProject(SessionState& s, QWidget& host, WgpuViewportWindow& vp);
bool openProject(SessionState& s, QWidget& host, WgpuViewportWindow& 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, ViewportWindow& vp, const QString& path);
bool openProjectPath(SessionState& s, QWidget& host, WgpuViewportWindow& 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, ViewportWindow& vp);
bool openCloudProject(SessionState& s, QWidget& host, WgpuViewportWindow& 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, ViewportWindow& vp);
bool syncCloudProject(SessionState& s, QWidget& host, WgpuViewportWindow& 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/ViewportWindow.h"
#include "../../../ifcviewer-wgpu/WgpuViewportWindow.h"
namespace bonsaiviewer::modules::viewport::commands {
void setHome(SessionState& session, ViewportWindow& vp) {
void setHome(SessionState& session, WgpuViewportWindow& vp) {
auto camera = vp.cameraState();
Federation::HomeView home_view;
home_view.target = camera.target;
@@ -37,7 +37,7 @@ void setHome(SessionState& session, ViewportWindow& vp) {
session.setStatusMessage("Camera", "Home view updated");
}
void goHome(SessionState& session, ViewportWindow& vp) {
void goHome(SessionState& session, WgpuViewportWindow& 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, ViewportWindow& vp) {
session.setStatusMessage("Camera", "Home view restored");
}
void viewSelected(ViewportWindow& vp) {
void viewSelected(WgpuViewportWindow& vp) {
vp.focusOnSelectedObject();
}
void fly(SessionState& session, ViewportWindow& vp) {
void fly(SessionState& session, WgpuViewportWindow& vp) {
vp.requestActivate();
vp.enterFpsMode();
session.setStatusMessage("Mode", "Fly mode active");
}
void toggleSection(SessionState& session, ViewportWindow& vp) {
void toggleSection(SessionState& session, WgpuViewportWindow& vp) {
vp.toggleSectionTool();
session.setStatusMessage("Section",
vp.sectionToolActive() ? "Section tool active" : "Section tool off");
}
void clearSection(SessionState& session, ViewportWindow& vp) {
void clearSection(SessionState& session, WgpuViewportWindow& vp) {
vp.clearSectionPlanes();
session.setStatusMessage("Section", "Section planes cleared");
}
void toggleDistance(ViewportWindow& vp) {
void toggleDistance(WgpuViewportWindow& vp) {
vp.toggleLengthTool();
}
void toggleArea(ViewportWindow& vp) {
void toggleArea(WgpuViewportWindow& vp) {
vp.toggleAreaTool();
}
void toggleVolume(ViewportWindow& vp) {
void toggleVolume(WgpuViewportWindow& vp) {
vp.toggleVolumeTool();
}
void hideSelected(ViewportWindow& vp) {
void hideSelected(WgpuViewportWindow& vp) {
vp.hideSelectedElements();
}
void isolateSelected(ViewportWindow& vp) {
void isolateSelected(WgpuViewportWindow& vp) {
vp.isolateSelectedElements();
}
void showAll(ViewportWindow& vp) {
void showAll(WgpuViewportWindow& vp) {
vp.showAllElements();
}
void invertVisibility(ViewportWindow& vp) {
void invertVisibility(WgpuViewportWindow& vp) {
vp.invertElementVisibility();
}
+14 -14
View File
@@ -21,27 +21,27 @@
#ifndef IFCINTERFACE_MODULES_VIEWPORT_COMMANDS_H
#define IFCINTERFACE_MODULES_VIEWPORT_COMMANDS_H
class ViewportWindow;
class WgpuViewportWindow;
namespace bonsaiviewer { class SessionState; }
namespace bonsaiviewer::modules::viewport::commands {
void setHome(SessionState& session, ViewportWindow& vp);
void goHome(SessionState& session, ViewportWindow& vp);
void viewSelected(ViewportWindow& vp);
void setHome(SessionState& session, WgpuViewportWindow& vp);
void goHome(SessionState& session, WgpuViewportWindow& vp);
void viewSelected(WgpuViewportWindow& vp);
void fly(SessionState& session, ViewportWindow& vp);
void toggleSection(SessionState& session, ViewportWindow& vp);
void clearSection(SessionState& session, ViewportWindow& vp);
void fly(SessionState& session, WgpuViewportWindow& vp);
void toggleSection(SessionState& session, WgpuViewportWindow& vp);
void clearSection(SessionState& session, WgpuViewportWindow& vp);
void toggleDistance(ViewportWindow& vp);
void toggleArea(ViewportWindow& vp);
void toggleVolume(ViewportWindow& vp);
void toggleDistance(WgpuViewportWindow& vp);
void toggleArea(WgpuViewportWindow& vp);
void toggleVolume(WgpuViewportWindow& vp);
void hideSelected(ViewportWindow& vp);
void isolateSelected(ViewportWindow& vp);
void showAll(ViewportWindow& vp);
void invertVisibility(ViewportWindow& vp);
void hideSelected(WgpuViewportWindow& vp);
void isolateSelected(WgpuViewportWindow& vp);
void showAll(WgpuViewportWindow& vp);
void invertVisibility(WgpuViewportWindow& vp);
} // namespace bonsaiviewer::modules::viewport::commands
+2 -2
View File
@@ -20,7 +20,7 @@
#include "Panel.h"
#include "../../../ifcviewer/ViewportWindow.h"
#include "../../../ifcviewer-wgpu/WgpuViewportWindow.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 ViewportWindow();
viewport_ = new WgpuViewportWindow();
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 ViewportWindow;
class WgpuViewportWindow;
namespace bonsaiviewer::modules::viewport {
@@ -33,10 +33,10 @@ class ViewportPanel : public QWidget {
public:
explicit ViewportPanel(QWidget* parent = nullptr);
ViewportWindow* viewport() const { return viewport_; }
WgpuViewportWindow* viewport() const { return viewport_; }
private:
ViewportWindow* viewport_ = nullptr;
WgpuViewportWindow* viewport_ = nullptr;
QWidget* viewport_container_ = nullptr;
};
+20 -20
View File
@@ -24,8 +24,8 @@
#include "../../SessionState.h"
#include "../../../ifcviewer/Federation.h"
#include "../../../ifcviewer/SceneLoader.h"
#include "../../../ifcviewer/ViewportWindow.h"
#include "../../../ifcviewer/OverlayRenderer.h"
#include "../../../ifcviewer-wgpu/WgpuViewportWindow.h"
#include "../../../ifcviewer-wgpu/WgpuOverlayRenderer.h"
#include "../../Measurement.h"
#include <Eigen/Dense>
@@ -36,7 +36,7 @@
namespace bonsaiviewer::modules::viewport {
ViewportView::ViewportView(bonsaiviewer::SessionState* session_state,
ViewportWindow* viewport,
WgpuViewportWindow* viewport,
QObject* parent)
: QObject(parent)
, session_state_(session_state)
@@ -59,54 +59,54 @@ ViewportView::ViewportView(bonsaiviewer::SessionState* session_state,
connect(session_state_, &SessionState::modelGeometryReady, this, [this](uint32_t) { refresh(); });
// Measurement tools — input-driven, share the View's lifetime.
connect(viewport_, &ViewportWindow::surfacePickedInTool, this,
connect(viewport_, &WgpuViewportWindow::surfacePickedInTool, this,
[this](int x, int y, int modifiers) {
const bool alt = (modifiers & Qt::AltModifier) != 0;
switch (viewport_->toolMode()) {
case ViewportWindow::ToolMode::Area:
case WgpuViewportWindow::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 ViewportWindow::ToolMode::Length:
case WgpuViewportWindow::ToolMode::Length:
length_measurement_->onPick(*viewport_, x, y, alt);
break;
case ViewportWindow::ToolMode::None:
case ViewportWindow::ToolMode::Volume:
case WgpuViewportWindow::ToolMode::NoTool:
case WgpuViewportWindow::ToolMode::Volume:
break;
}
});
connect(viewport_, &ViewportWindow::toolModeChanged, this,
[this](ViewportWindow::ToolMode mode) {
connect(viewport_, &WgpuViewportWindow::toolModeChanged, this,
[this](WgpuViewportWindow::ToolMode mode) {
area_measurement_->clear(*viewport_);
length_measurement_->clear(*viewport_);
switch (mode) {
case ViewportWindow::ToolMode::None:
case WgpuViewportWindow::ToolMode::NoTool:
viewport_->setHudText(QString());
viewport_->setOverlayLabels({});
session_state_->setStatusMessage("Measure", "Measurement tool off");
break;
case ViewportWindow::ToolMode::Length:
case WgpuViewportWindow::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 ViewportWindow::ToolMode::Area:
case WgpuViewportWindow::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 ViewportWindow::ToolMode::Volume:
case WgpuViewportWindow::ToolMode::Volume:
session_state_->setStatusMessage("Measure", "Volume tool: click / box-select objects, Esc exits");
updateVolumeReadout();
break;
}
});
connect(viewport_, &ViewportWindow::toolBackspacePressed, this, [this]() {
if (viewport_->toolMode() == ViewportWindow::ToolMode::Length) {
connect(viewport_, &WgpuViewportWindow::toolBackspacePressed, this, [this]() {
if (viewport_->toolMode() == WgpuViewportWindow::ToolMode::Length) {
length_measurement_->removeLastPoint(*viewport_);
}
});
connect(viewport_, &ViewportWindow::objectPicked, this, [this](uint32_t) {
connect(viewport_, &WgpuViewportWindow::objectPicked, this, [this](uint32_t) {
updateVolumeReadout();
});
@@ -194,7 +194,7 @@ void ViewportView::maybeGuessFederatedFalseOrigin(uint32_t mid) {
}
void ViewportView::updateVolumeReadout() {
if (viewport_->toolMode() != ViewportWindow::ToolMode::Volume) return;
if (viewport_->toolMode() != WgpuViewportWindow::ToolMode::Volume) return;
const auto& sel = viewport_->selection().selectionIds();
if (sel.empty()) {
@@ -207,13 +207,13 @@ void ViewportView::updateVolumeReadout() {
const auto per_obj = volumesPerObject(*viewport_, ids);
double total = 0.0;
std::vector<OverlayRenderer::Label> labels;
std::vector<WgpuOverlayRenderer::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;
OverlayRenderer::Label lbl;
WgpuOverlayRenderer::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 ViewportWindow;
class WgpuViewportWindow;
class AreaMeasurement;
class LengthMeasurement;
@@ -44,7 +44,7 @@ class ViewportView : public QObject {
public:
explicit ViewportView(bonsaiviewer::SessionState* session_state,
ViewportWindow* viewport,
WgpuViewportWindow* viewport,
QObject* parent = nullptr);
~ViewportView() override;
@@ -57,7 +57,7 @@ private:
void updateVolumeReadout();
bonsaiviewer::SessionState* session_state_ = nullptr;
ViewportWindow* viewport_ = nullptr;
WgpuViewportWindow* viewport_ = nullptr;
std::unique_ptr<AreaMeasurement> area_measurement_;
std::unique_ptr<LengthMeasurement> length_measurement_;
};