mirror of
https://github.com/IfcOpenShell/IfcOpenShell.git
synced 2026-08-21 20:45:59 +00:00
Compare commits
17 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 9f2f1954f3 | |||
| b4323ba67a | |||
| b79070456f | |||
| 5cd4dbf603 | |||
| 2c18aebdcc | |||
| cb0a70ac7e | |||
| cf3f3c2c00 | |||
| 7029feca0a | |||
| f75526e6c3 | |||
| 6d4fe17975 | |||
| c1b4b5c1d0 | |||
| 994fa4bc03 | |||
| 3e066443be | |||
| d475553cd0 | |||
| 65e40b9923 | |||
| 29fdf629f9 | |||
| d9eb67b7a8 |
@@ -182,16 +182,6 @@ MainWindow::MainWindow(QWidget* parent)
|
||||
if (!show) stats_label_->clear();
|
||||
});
|
||||
|
||||
// Toggling the CoordinateOperation setting walks every loaded model
|
||||
// and pushes either its georef matrix or identity to the viewport.
|
||||
connect(&AppSettings::instance(),
|
||||
&AppSettings::applyCoordinateOperationChanged,
|
||||
this, [this](bool /*enabled*/) {
|
||||
for (const auto& kv : fed_id_to_model_id_) {
|
||||
applyCoordinateOperationToViewport(kv.second);
|
||||
}
|
||||
});
|
||||
|
||||
updateWindowTitle();
|
||||
resize(1400, 900);
|
||||
}
|
||||
@@ -209,20 +199,53 @@ void MainWindow::setupUi() {
|
||||
connect(viewport_, &ViewportWindow::surfacePickedInTool, this,
|
||||
[this](int x, int y, int modifiers) {
|
||||
const bool alt = (modifiers & Qt::AltModifier) != 0;
|
||||
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()));
|
||||
switch (viewport_->toolMode()) {
|
||||
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 ViewportWindow::ToolMode::Length:
|
||||
length_measurement_.onPick(*viewport_, x, y, alt);
|
||||
break;
|
||||
case ViewportWindow::ToolMode::None:
|
||||
break;
|
||||
}
|
||||
});
|
||||
connect(viewport_, &ViewportWindow::areaToolToggled, this,
|
||||
[this](bool active) {
|
||||
connect(viewport_, &ViewportWindow::toolModeChanged, this,
|
||||
[this](ViewportWindow::ToolMode mode) {
|
||||
// Always clear both — the previous tool's accumulator/overlay
|
||||
// shouldn't bleed into the next one.
|
||||
area_measurement_.clear(*viewport_);
|
||||
if (active) {
|
||||
length_measurement_.clear(*viewport_);
|
||||
switch (mode) {
|
||||
case ViewportWindow::ToolMode::Area:
|
||||
viewport_->setHudText("Area: 0.0000 m² (0 tris)");
|
||||
status_label_->setText("Area tool: LMB add, Alt+LMB single tri, click again to remove, Esc exits");
|
||||
} else {
|
||||
break;
|
||||
case ViewportWindow::ToolMode::Length:
|
||||
viewport_->setHudText("Length tool: click first point");
|
||||
status_label_->setText("Length tool: LMB add point, Backspace remove last, Esc exits");
|
||||
break;
|
||||
case ViewportWindow::ToolMode::Volume:
|
||||
// Volume tool is passive — selection works as in None. The
|
||||
// readout helper writes both HUD and per-object labels for
|
||||
// whatever's currently selected, then keeps them in sync as
|
||||
// the selection changes.
|
||||
status_label_->setText("Volume tool: click / box-select objects, Esc exits");
|
||||
updateVolumeReadout();
|
||||
break;
|
||||
case ViewportWindow::ToolMode::None:
|
||||
viewport_->setHudText(QString());
|
||||
viewport_->setOverlayLabels({});
|
||||
status_label_->setText("Ready");
|
||||
break;
|
||||
}
|
||||
});
|
||||
connect(viewport_, &ViewportWindow::toolBackspacePressed, this, [this]() {
|
||||
if (viewport_->toolMode() == ViewportWindow::ToolMode::Length) {
|
||||
length_measurement_.removeLastPoint(*viewport_);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -305,6 +328,26 @@ void MainWindow::setupMenus() {
|
||||
view_menu->addAction("&Measure Area", this, [this]() {
|
||||
viewport_->toggleAreaTool();
|
||||
}, QKeySequence("Ctrl+Shift+A"));
|
||||
view_menu->addAction("Measure &Length", this, [this]() {
|
||||
viewport_->toggleLengthTool();
|
||||
}, QKeySequence("Ctrl+Shift+L"));
|
||||
view_menu->addAction("Measure &Volume", this, [this]() {
|
||||
viewport_->toggleVolumeTool();
|
||||
}, QKeySequence("Ctrl+Shift+V"));
|
||||
view_menu->addSeparator();
|
||||
// Element visibility — H hides the current selection, Shift+H
|
||||
// isolates it (hides everything else within visible models), Alt+H
|
||||
// restores every element. Model-level hiding is independent and
|
||||
// stays put.
|
||||
view_menu->addAction("&Hide Selected", this, [this]() {
|
||||
viewport_->hideSelectedElements();
|
||||
}, QKeySequence(Qt::Key_H));
|
||||
view_menu->addAction("&Isolate Selected", this, [this]() {
|
||||
viewport_->isolateSelectedElements();
|
||||
}, QKeySequence(Qt::SHIFT | Qt::Key_H));
|
||||
view_menu->addAction("&Show All Elements", this, [this]() {
|
||||
viewport_->showAllElements();
|
||||
}, QKeySequence(Qt::ALT | Qt::Key_H));
|
||||
view_menu->addSeparator();
|
||||
view_menu->addAction("Set &Home View", this, &MainWindow::onSetHomeView);
|
||||
view_menu->addAction("&Go to Home View", this, &MainWindow::onGoHomeView);
|
||||
@@ -668,11 +711,9 @@ void MainWindow::onDataSourceReady(uint32_t mid) {
|
||||
|
||||
void MainWindow::applyCoordinateOperationToViewport(uint32_t mid) {
|
||||
Eigen::Matrix4d M = Eigen::Matrix4d::Identity();
|
||||
if (AppSettings::instance().applyCoordinateOperation()) {
|
||||
if (const ModelGeoref* gr = loader_->modelGeoref(mid)) {
|
||||
if (gr->has_coordinate_operation) {
|
||||
M = gr->coordinate_operation_meters;
|
||||
}
|
||||
if (const ModelGeoref* gr = loader_->modelGeoref(mid)) {
|
||||
if (gr->has_coordinate_operation) {
|
||||
M = gr->coordinate_operation_meters;
|
||||
}
|
||||
}
|
||||
viewport_->setModelCoordinateOperation(mid, M);
|
||||
@@ -694,8 +735,7 @@ void MainWindow::applyModelTransformationToViewport(uint32_t mid) {
|
||||
Eigen::Matrix4d coord_op = Eigen::Matrix4d::Identity();
|
||||
if (const ModelGeoref* gr = loader_->modelGeoref(mid)) {
|
||||
units = gr->units;
|
||||
if (AppSettings::instance().applyCoordinateOperation() &&
|
||||
gr->has_coordinate_operation) {
|
||||
if (gr->has_coordinate_operation) {
|
||||
coord_op = gr->coordinate_operation_meters;
|
||||
}
|
||||
}
|
||||
@@ -732,8 +772,7 @@ void MainWindow::maybeGuessFederatedFalseOrigin(uint32_t mid) {
|
||||
if (placement == nullptr || gr == nullptr) return;
|
||||
|
||||
const FederatedFalseOrigin guess = guessFederatedFalseOrigin(
|
||||
*placement, *gr, federation_->config(),
|
||||
AppSettings::instance().applyCoordinateOperation());
|
||||
*placement, *gr, federation_->config());
|
||||
federation_->setFederatedFalseOrigin(guess);
|
||||
}
|
||||
|
||||
@@ -884,21 +923,65 @@ void MainWindow::onAllLoadsFinished() {
|
||||
}
|
||||
|
||||
void MainWindow::onObjectPicked(uint32_t object_id) {
|
||||
viewport_->setSelectedObjectId(object_id);
|
||||
|
||||
// Signal originates from the viewport's SelectionState — viewport
|
||||
// selection is already authoritative. Pushing setSelectedObjectId
|
||||
// back here would clobber a multi-select set with {object_id}.
|
||||
auto it = tree_items_.find(object_id);
|
||||
if (it != tree_items_.end()) {
|
||||
element_tree_->blockSignals(true);
|
||||
element_tree_->setCurrentItem(it->second);
|
||||
element_tree_->blockSignals(false);
|
||||
} else {
|
||||
// No active (e.g. selection cleared, or active id is in a model
|
||||
// whose tree node hasn't materialised) — drop the tree highlight
|
||||
// so the panel doesn't lie about what's active.
|
||||
element_tree_->blockSignals(true);
|
||||
element_tree_->setCurrentItem(nullptr);
|
||||
element_tree_->blockSignals(false);
|
||||
}
|
||||
|
||||
populateProperties(object_id);
|
||||
updateVolumeReadout();
|
||||
}
|
||||
|
||||
if (object_id != 0) {
|
||||
const double v = volumeOfObjects(*viewport_, {object_id});
|
||||
qInfo("Volume of object %u: %.6f m^3", object_id, v);
|
||||
void MainWindow::updateVolumeReadout() {
|
||||
// Volume readout only renders while the volume tool is active —
|
||||
// matches Area/Length, which are also gated behind their own tool
|
||||
// mode. Other modes own the HUD + overlay labels for their
|
||||
// lifetime, so we stay quiet here.
|
||||
if (viewport_->toolMode() != ViewportWindow::ToolMode::Volume) return;
|
||||
|
||||
const auto& sel = viewport_->selection().selectionIds();
|
||||
if (sel.empty()) {
|
||||
viewport_->setHudText(QString());
|
||||
viewport_->setOverlayLabels({});
|
||||
return;
|
||||
}
|
||||
|
||||
std::vector<uint32_t> ids(sel.begin(), sel.end());
|
||||
const auto per_obj = volumesPerObject(*viewport_, ids);
|
||||
|
||||
double total = 0.0;
|
||||
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;
|
||||
OverlayRenderer::Label lbl;
|
||||
const QVector3D c = (mn + mx) * 0.5f;
|
||||
lbl.world_pos[0] = c.x();
|
||||
lbl.world_pos[1] = c.y();
|
||||
lbl.world_pos[2] = c.z();
|
||||
lbl.text = QString::number(v, 'f', 4) + " m³";
|
||||
labels.push_back(std::move(lbl));
|
||||
}
|
||||
|
||||
viewport_->setHudText(QString("Volume: %1 m³ (%2 object%3)")
|
||||
.arg(total, 0, 'f', 4)
|
||||
.arg(per_obj.size())
|
||||
.arg(per_obj.size() == 1 ? "" : "s"));
|
||||
viewport_->setOverlayLabels(labels);
|
||||
}
|
||||
|
||||
void MainWindow::onTreeSelectionChanged() {
|
||||
@@ -906,6 +989,8 @@ void MainWindow::onTreeSelectionChanged() {
|
||||
if (items.isEmpty()) return;
|
||||
|
||||
uint32_t object_id = items.first()->data(0, Qt::UserRole).toUInt();
|
||||
// Tree stays single-select per UX choice — clicking a tree item
|
||||
// replaces the viewport's multi-set with just that item.
|
||||
viewport_->setSelectedObjectId(object_id);
|
||||
populateProperties(object_id);
|
||||
}
|
||||
|
||||
@@ -93,6 +93,10 @@ private:
|
||||
bool confirmDiscardIfDirty();
|
||||
void updateWindowTitle();
|
||||
void populateProperties(uint32_t object_id);
|
||||
// Push the volume HUD + per-object volume labels for the current
|
||||
// selection. No-op when a measurement tool is active — that tool
|
||||
// owns the overlay state until it's exited.
|
||||
void updateVolumeReadout();
|
||||
void appendElementToTree(uint32_t model_id,
|
||||
uint32_t object_id,
|
||||
int ifc_id,
|
||||
@@ -200,7 +204,8 @@ private:
|
||||
QString pending_camera_;
|
||||
int pending_benchmark_ = 0;
|
||||
|
||||
AreaMeasurement area_measurement_;
|
||||
AreaMeasurement area_measurement_;
|
||||
LengthMeasurement length_measurement_;
|
||||
};
|
||||
|
||||
#endif // MAINWINDOW_H
|
||||
|
||||
@@ -22,6 +22,7 @@
|
||||
#include "ViewportWindow.h"
|
||||
|
||||
#include <QtGlobal>
|
||||
#include <Eigen/Dense>
|
||||
|
||||
#include <algorithm>
|
||||
#include <cmath>
|
||||
@@ -93,6 +94,41 @@ double volumeOfObjects(ViewportWindow& vp,
|
||||
return total;
|
||||
}
|
||||
|
||||
std::vector<std::pair<uint32_t, double>>
|
||||
volumesPerObject(ViewportWindow& vp,
|
||||
const std::vector<uint32_t>& object_ids) {
|
||||
std::vector<std::pair<uint32_t, double>> out;
|
||||
if (object_ids.empty()) return out;
|
||||
out.reserve(object_ids.size());
|
||||
|
||||
// Cache the local-frame volume per unique (model_id, mesh_id) so each
|
||||
// mesh is read back at most once even when many instances share it
|
||||
// (common for repeated families like windows / columns).
|
||||
std::unordered_map<uint64_t, double> mesh_vol_local;
|
||||
mesh_vol_local.reserve(object_ids.size());
|
||||
|
||||
ViewportWindow::MeshTriangles tris;
|
||||
for (uint32_t oid : object_ids) {
|
||||
ViewportWindow::InstanceLookup lk;
|
||||
if (!vp.findInstance(oid, lk)) continue;
|
||||
|
||||
const uint64_t key = (uint64_t(lk.model_id) << 32) | lk.mesh_id;
|
||||
auto it = mesh_vol_local.find(key);
|
||||
double v_local = 0.0;
|
||||
if (it == mesh_vol_local.end()) {
|
||||
if (vp.readbackMeshTriangles(lk.model_id, lk.mesh_id, tris)) {
|
||||
v_local = meshLocalVolume(tris);
|
||||
}
|
||||
mesh_vol_local.emplace(key, v_local);
|
||||
} else {
|
||||
v_local = it->second;
|
||||
}
|
||||
const double det = std::abs(det3(lk.placement_transformation));
|
||||
out.emplace_back(oid, v_local * det);
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
namespace {
|
||||
|
||||
// edge_key: undirected edge between two mesh-local vertex indices.
|
||||
@@ -206,6 +242,7 @@ void AreaMeasurement::clear(ViewportWindow& vp) {
|
||||
selected_.clear();
|
||||
total_area_m2_ = 0.0;
|
||||
vp.setHighlightTriangles({}, 0, 0, 0, 0);
|
||||
vp.setOverlayLabels({});
|
||||
}
|
||||
|
||||
void AreaMeasurement::rebuildHighlight(ViewportWindow& vp) {
|
||||
@@ -235,6 +272,92 @@ void AreaMeasurement::rebuildHighlight(ViewportWindow& vp) {
|
||||
}
|
||||
// Translucent cyan-ish tint — readable on both light and dark surfaces.
|
||||
vp.setHighlightTriangles(world_xyz, 0.20f, 0.85f, 1.00f, 0.45f);
|
||||
|
||||
// Per-patch labels: connected-components sweep over the selected
|
||||
// triangles (using the mesh's full edge adjacency, restricted to
|
||||
// edges where both incident tris are in the selection). Each
|
||||
// component → one label at its area-weighted centroid in world
|
||||
// space, with the patch area in m². Two clicks on different walls
|
||||
// → two distinct components → two labels; one click that BFS-grew
|
||||
// 200 tris of one wall face → one label.
|
||||
std::unordered_map<uint32_t, std::vector<const SelectedTri*>> by_object;
|
||||
for (const auto& [key, sel] : selected_) {
|
||||
const uint32_t object_id = uint32_t(key >> 32);
|
||||
by_object[object_id].push_back(&sel);
|
||||
}
|
||||
|
||||
std::vector<OverlayRenderer::Label> labels;
|
||||
for (const auto& [obj_id, sels] : by_object) {
|
||||
if (sels.empty()) continue;
|
||||
// All tris belonging to one object share its mesh + transform.
|
||||
const SelectedTri& any = *sels[0];
|
||||
const uint64_t cache_key = (uint64_t(any.model_id) << 32)
|
||||
| uint64_t(any.mesh_id);
|
||||
auto cit = mesh_cache_.find(cache_key);
|
||||
if (cit == mesh_cache_.end()) continue;
|
||||
const MeshCache& c = cit->second;
|
||||
|
||||
// Selected-tri set restricted to this object.
|
||||
std::unordered_set<uint32_t> remaining;
|
||||
remaining.reserve(sels.size());
|
||||
for (const SelectedTri* s : sels) remaining.insert(s->tri);
|
||||
|
||||
// Find each connected component via BFS over shared edges,
|
||||
// accepting only neighbours that are themselves selected.
|
||||
while (!remaining.empty()) {
|
||||
const uint32_t start = *remaining.begin();
|
||||
std::unordered_set<uint32_t> in_comp{start};
|
||||
std::queue<uint32_t> frontier;
|
||||
frontier.push(start);
|
||||
std::vector<uint32_t> component;
|
||||
while (!frontier.empty()) {
|
||||
const uint32_t t = frontier.front(); frontier.pop();
|
||||
component.push_back(t);
|
||||
if (size_t(t) * 3 + 2 >= c.indices.size()) continue;
|
||||
for (int e = 0; e < 3; ++e) {
|
||||
const uint32_t ia = c.indices[3 * t + e];
|
||||
const uint32_t ib = c.indices[3 * t + (e + 1) % 3];
|
||||
auto it = c.edges.find(edgeKey(ia, ib));
|
||||
if (it == c.edges.end()) continue;
|
||||
for (uint32_t nt : it->second) {
|
||||
if (in_comp.count(nt) || remaining.count(nt) == 0) continue;
|
||||
in_comp.insert(nt);
|
||||
frontier.push(nt);
|
||||
}
|
||||
}
|
||||
}
|
||||
for (uint32_t t : component) remaining.erase(t);
|
||||
|
||||
// Area + area-weighted centroid (mesh-local).
|
||||
double area = 0.0, cx = 0.0, cy = 0.0, cz = 0.0;
|
||||
for (uint32_t t : component) {
|
||||
if (size_t(t) >= c.tri_areas.size()) continue;
|
||||
const double a = c.tri_areas[t];
|
||||
area += a;
|
||||
const uint32_t ia = c.indices[3 * t + 0];
|
||||
const uint32_t ib = c.indices[3 * t + 1];
|
||||
const uint32_t ic = c.indices[3 * t + 2];
|
||||
const float* va = &c.positions[3 * ia];
|
||||
const float* vb = &c.positions[3 * ib];
|
||||
const float* vc = &c.positions[3 * ic];
|
||||
cx += a * (double(va[0]) + vb[0] + vc[0]) / 3.0;
|
||||
cy += a * (double(va[1]) + vb[1] + vc[1]) / 3.0;
|
||||
cz += a * (double(va[2]) + vb[2] + vc[2]) / 3.0;
|
||||
}
|
||||
if (area <= 0.0) continue;
|
||||
cx /= area; cy /= area; cz /= area;
|
||||
|
||||
// Centroid → world via the instance's composed transform.
|
||||
const float* M = any.composed_transform;
|
||||
OverlayRenderer::Label lbl;
|
||||
lbl.world_pos[0] = float(M[0]*cx + M[4]*cy + M[8]*cz + M[12]);
|
||||
lbl.world_pos[1] = float(M[1]*cx + M[5]*cy + M[9]*cz + M[13]);
|
||||
lbl.world_pos[2] = float(M[2]*cx + M[6]*cy + M[10]*cz + M[14]);
|
||||
lbl.text = QString::number(area, 'f', 4) + " m²";
|
||||
labels.push_back(std::move(lbl));
|
||||
}
|
||||
}
|
||||
vp.setOverlayLabels(labels);
|
||||
}
|
||||
|
||||
AreaMeasurement::MeshCache* AreaMeasurement::meshCache(ViewportWindow& vp,
|
||||
@@ -365,3 +488,595 @@ void AreaMeasurement::onPick(ViewportWindow& vp, int x, int y, bool alt) {
|
||||
delta >= 0.0 ? "+" : "", delta,
|
||||
total_area_m2_, selected_.size());
|
||||
}
|
||||
|
||||
// ----- LengthMeasurement -----------------------------------------------------
|
||||
|
||||
namespace {
|
||||
|
||||
double dist3(const std::array<float, 3>& a, const std::array<float, 3>& b) {
|
||||
const double dx = double(b[0]) - a[0];
|
||||
const double dy = double(b[1]) - a[1];
|
||||
const double dz = double(b[2]) - a[2];
|
||||
return std::sqrt(dx*dx + dy*dy + dz*dz);
|
||||
}
|
||||
|
||||
double triArea3(const std::array<float, 3>& a,
|
||||
const std::array<float, 3>& b,
|
||||
const std::array<float, 3>& c) {
|
||||
const double bax = double(b[0]) - a[0];
|
||||
const double bay = double(b[1]) - a[1];
|
||||
const double baz = double(b[2]) - a[2];
|
||||
const double cax = double(c[0]) - a[0];
|
||||
const double cay = double(c[1]) - a[1];
|
||||
const double caz = double(c[2]) - a[2];
|
||||
const double nx = bay * caz - baz * cay;
|
||||
const double ny = baz * cax - bax * caz;
|
||||
const double nz = bax * cay - bay * cax;
|
||||
return 0.5 * std::sqrt(nx*nx + ny*ny + nz*nz);
|
||||
}
|
||||
|
||||
// Polygon area via best-fit plane + shoelace, falling back to fan
|
||||
// triangulation when the points stray off the plane. Returns the
|
||||
// resulting area and a label naming which path was taken.
|
||||
struct PolygonAreaResult {
|
||||
double area_m2;
|
||||
const char* method;
|
||||
};
|
||||
|
||||
PolygonAreaResult polygonArea(const std::vector<std::array<float, 3>>& pts) {
|
||||
using Vec3d = Eigen::Vector3d;
|
||||
using Mat3d = Eigen::Matrix3d;
|
||||
const size_t n = pts.size();
|
||||
|
||||
// Centroid + bounding box (for the planarity threshold).
|
||||
Vec3d centroid = Vec3d::Zero();
|
||||
Vec3d bbox_min = Vec3d::Constant(std::numeric_limits<double>::infinity());
|
||||
Vec3d bbox_max = Vec3d::Constant(-std::numeric_limits<double>::infinity());
|
||||
for (const auto& p : pts) {
|
||||
const Vec3d v(p[0], p[1], p[2]);
|
||||
centroid += v;
|
||||
bbox_min = bbox_min.cwiseMin(v);
|
||||
bbox_max = bbox_max.cwiseMax(v);
|
||||
}
|
||||
centroid /= double(n);
|
||||
const double bbox_diag = (bbox_max - bbox_min).norm();
|
||||
|
||||
// 3x3 covariance. Smallest eigenvector of this is the plane normal.
|
||||
Mat3d cov = Mat3d::Zero();
|
||||
for (const auto& p : pts) {
|
||||
const Vec3d d = Vec3d(p[0], p[1], p[2]) - centroid;
|
||||
cov += d * d.transpose();
|
||||
}
|
||||
|
||||
Eigen::SelfAdjointEigenSolver<Mat3d> es(cov);
|
||||
const Vec3d normal = es.eigenvectors().col(0); // smallest eigenvalue
|
||||
|
||||
// RMS plane distance, normalised against the bounding-box diagonal.
|
||||
double sq_sum = 0.0;
|
||||
for (const auto& p : pts) {
|
||||
const double d = (Vec3d(p[0], p[1], p[2]) - centroid).dot(normal);
|
||||
sq_sum += d * d;
|
||||
}
|
||||
const double rms = std::sqrt(sq_sum / double(n));
|
||||
const bool planar = bbox_diag > 0.0 && (rms / bbox_diag) < 1e-3;
|
||||
|
||||
if (planar) {
|
||||
// Build an in-plane orthonormal basis.
|
||||
Vec3d u = normal.cross(Vec3d::UnitX());
|
||||
if (u.squaredNorm() < 1e-6) u = normal.cross(Vec3d::UnitY());
|
||||
u.normalize();
|
||||
const Vec3d v = normal.cross(u);
|
||||
|
||||
// Project + shoelace.
|
||||
std::vector<std::array<double, 2>> uv(n);
|
||||
for (size_t i = 0; i < n; ++i) {
|
||||
const Vec3d d = Vec3d(pts[i][0], pts[i][1], pts[i][2]) - centroid;
|
||||
uv[i][0] = d.dot(u);
|
||||
uv[i][1] = d.dot(v);
|
||||
}
|
||||
double s = 0.0;
|
||||
for (size_t i = 0; i < n; ++i) {
|
||||
const auto& a = uv[i];
|
||||
const auto& b = uv[(i + 1) % n];
|
||||
s += a[0] * b[1] - b[0] * a[1];
|
||||
}
|
||||
return { 0.5 * std::abs(s), "planar" };
|
||||
}
|
||||
|
||||
// Fan from p0. Works for star-shaped polygons; for genuinely twisted
|
||||
// 3D point sets it's a heuristic — flagged in the method label.
|
||||
double area = 0.0;
|
||||
for (size_t i = 1; i + 1 < n; ++i) {
|
||||
area += triArea3(pts[0], pts[i], pts[i + 1]);
|
||||
}
|
||||
return { area, "fan-triangulated (non-planar)" };
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
LengthMeasurement::LengthMeasurement() = default;
|
||||
|
||||
namespace {
|
||||
|
||||
// Visual style — reused across all length-tool overlay paths.
|
||||
constexpr float LINE_WIDTH = 1.5f;
|
||||
constexpr float LINE_HALO = 0.5f;
|
||||
constexpr float DOT_SIZE = 6.0f;
|
||||
constexpr float DOT_HALO = 1.0f;
|
||||
constexpr float DASH_PERIOD = 9.0f; // px
|
||||
constexpr float DASH_ON_RATIO = 0.55f; // 5 on, 4 off
|
||||
|
||||
OverlayRenderer::LineGroup makeGroup(std::vector<float> xyz,
|
||||
float r, float g, float b,
|
||||
bool dashed = false) {
|
||||
OverlayRenderer::LineGroup gp;
|
||||
gp.world_xyz = std::move(xyz);
|
||||
gp.color[0] = r; gp.color[1] = g; gp.color[2] = b; gp.color[3] = 1.0f;
|
||||
gp.stroke_color[0] = 0.0f; gp.stroke_color[1] = 0.0f;
|
||||
gp.stroke_color[2] = 0.0f; gp.stroke_color[3] = 1.0f;
|
||||
gp.line_width = LINE_WIDTH;
|
||||
gp.stroke_extra = LINE_HALO;
|
||||
gp.dash_period_px = dashed ? DASH_PERIOD : 0.0f;
|
||||
gp.dash_on_ratio = DASH_ON_RATIO;
|
||||
return gp;
|
||||
}
|
||||
|
||||
void pushDot(std::vector<float>& xyz, const std::array<float, 3>& p) {
|
||||
xyz.push_back(p[0]);
|
||||
xyz.push_back(p[1]);
|
||||
xyz.push_back(p[2]);
|
||||
}
|
||||
|
||||
void pushSeg(std::vector<float>& xyz,
|
||||
const std::array<float, 3>& a,
|
||||
const std::array<float, 3>& b) {
|
||||
xyz.insert(xyz.end(), a.begin(), a.end());
|
||||
xyz.insert(xyz.end(), b.begin(), b.end());
|
||||
}
|
||||
|
||||
OverlayRenderer::Label makeLabel(const std::array<float, 3>& a,
|
||||
const std::array<float, 3>& b,
|
||||
const QString& text) {
|
||||
OverlayRenderer::Label lbl;
|
||||
lbl.world_pos[0] = 0.5f * (a[0] + b[0]);
|
||||
lbl.world_pos[1] = 0.5f * (a[1] + b[1]);
|
||||
lbl.world_pos[2] = 0.5f * (a[2] + b[2]);
|
||||
lbl.text = text;
|
||||
return lbl;
|
||||
}
|
||||
|
||||
void pushDots(ViewportWindow& vp, const std::vector<float>& xyz) {
|
||||
vp.setOverlayPoints(xyz,
|
||||
/*inner*/ 1.0f, 1.0f, 1.0f, 1.0f,
|
||||
/*size*/ DOT_SIZE,
|
||||
/*stroke*/ 0.0f, 0.0f, 0.0f, 1.0f,
|
||||
/*extra*/ DOT_HALO);
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
void LengthMeasurement::clear(ViewportWindow& vp) {
|
||||
points_.clear();
|
||||
normals_.clear();
|
||||
vp.setOverlayPoints({}, 0,0,0,0, 0, 0,0,0,0, 0);
|
||||
vp.setOverlayLines({});
|
||||
vp.setOverlayLabels({});
|
||||
vp.setHudText(QString());
|
||||
}
|
||||
|
||||
void LengthMeasurement::onPick(ViewportWindow& vp, int x, 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]});
|
||||
if (points_.size() == 1) {
|
||||
first_pick_ = pick; // record info the laser BFS needs
|
||||
}
|
||||
rebuildOverlay(vp);
|
||||
}
|
||||
|
||||
void LengthMeasurement::removeLastPoint(ViewportWindow& vp) {
|
||||
if (points_.empty()) return;
|
||||
points_.pop_back();
|
||||
if (!normals_.empty()) normals_.pop_back();
|
||||
rebuildOverlay(vp);
|
||||
}
|
||||
|
||||
void LengthMeasurement::rebuildOverlay(ViewportWindow& vp) {
|
||||
if (points_.size() == 1 && normals_.size() == 1) {
|
||||
rebuildLaserOverlay(vp);
|
||||
return;
|
||||
}
|
||||
|
||||
std::vector<float> pts_xyz;
|
||||
pts_xyz.reserve(points_.size() * 3);
|
||||
for (const auto& p : points_) pushDot(pts_xyz, p);
|
||||
pushDots(vp, pts_xyz);
|
||||
|
||||
std::vector<OverlayRenderer::LineGroup> groups;
|
||||
std::vector<OverlayRenderer::Label> labels;
|
||||
const size_t n = points_.size();
|
||||
|
||||
if (n == 2) {
|
||||
// Direct line A→B (white) + total-length label.
|
||||
const auto& a = points_[0];
|
||||
const auto& b = points_[1];
|
||||
groups.push_back(makeGroup({a[0], a[1], a[2], b[0], b[1], b[2]},
|
||||
1.0f, 1.0f, 1.0f));
|
||||
labels.push_back(makeLabel(a, b,
|
||||
QString::number(dist3(a, b), 'f', 3) + " m"));
|
||||
|
||||
// Axis-coloured stair-step A → (Bx,Ay,Az) → (Bx,By,Az) → B.
|
||||
// Each leg gets its delta label (omit zero legs to keep the
|
||||
// overlay clean when the points are axis-aligned).
|
||||
const std::array<float, 3> kx = {b[0], a[1], a[2]};
|
||||
const std::array<float, 3> ky = {b[0], b[1], a[2]};
|
||||
const double dx = std::abs(double(b[0]) - a[0]);
|
||||
const double dy = std::abs(double(b[1]) - a[1]);
|
||||
const double dz = std::abs(double(b[2]) - a[2]);
|
||||
if (dx > 1e-6) {
|
||||
groups.push_back(makeGroup({a[0],a[1],a[2], kx[0],kx[1],kx[2]},
|
||||
1.00f, 0.30f, 0.30f));
|
||||
labels.push_back(makeLabel(a, kx,
|
||||
"ΔX: " + QString::number(dx, 'f', 3) + " m"));
|
||||
}
|
||||
if (dy > 1e-6) {
|
||||
groups.push_back(makeGroup({kx[0],kx[1],kx[2], ky[0],ky[1],ky[2]},
|
||||
0.30f, 0.90f, 0.30f));
|
||||
labels.push_back(makeLabel(kx, ky,
|
||||
"ΔY: " + QString::number(dy, 'f', 3) + " m"));
|
||||
}
|
||||
if (dz > 1e-6) {
|
||||
groups.push_back(makeGroup({ky[0],ky[1],ky[2], b[0],b[1],b[2]},
|
||||
0.30f, 0.55f, 1.00f));
|
||||
labels.push_back(makeLabel(ky, b,
|
||||
"ΔZ: " + QString::number(dz, 'f', 3) + " m"));
|
||||
}
|
||||
|
||||
// Perpendicular projection: only when both picks landed on
|
||||
// surfaces with near-parallel normals (|n_a · n_b| > 0.95). We
|
||||
// pick the average normal (flipped to agree with n_a if needed)
|
||||
// and project AB onto it. Drawn dashed from A to A + perp·n.
|
||||
if (normals_.size() == 2) {
|
||||
const auto& na = normals_[0];
|
||||
const auto& nb = normals_[1];
|
||||
const double dot_nn = double(na[0])*nb[0]
|
||||
+ double(na[1])*nb[1]
|
||||
+ double(na[2])*nb[2];
|
||||
if (std::abs(dot_nn) > 0.95) {
|
||||
const float sign = dot_nn >= 0.0 ? 1.0f : -1.0f;
|
||||
float n_avg[3] = {
|
||||
0.5f * (na[0] + sign * nb[0]),
|
||||
0.5f * (na[1] + sign * nb[1]),
|
||||
0.5f * (na[2] + sign * nb[2]),
|
||||
};
|
||||
const float len = std::sqrt(n_avg[0]*n_avg[0]
|
||||
+ n_avg[1]*n_avg[1]
|
||||
+ n_avg[2]*n_avg[2]);
|
||||
if (len > 1e-6f) {
|
||||
n_avg[0] /= len; n_avg[1] /= len; n_avg[2] /= len;
|
||||
}
|
||||
const double abx = double(b[0]) - a[0];
|
||||
const double aby = double(b[1]) - a[1];
|
||||
const double abz = double(b[2]) - a[2];
|
||||
const double perp = abx*n_avg[0] + aby*n_avg[1] + abz*n_avg[2];
|
||||
const double abs_perp = std::abs(perp);
|
||||
// Skip the perpendicular dimension when it collapses onto
|
||||
// an existing axis-aligned leg — happens when the surface
|
||||
// normal lines up with a world axis, in which case
|
||||
// ΔX / ΔY / ΔZ already shows the same number.
|
||||
constexpr double kAxisCollapseTol = 1e-3; // 1mm
|
||||
const bool redundant =
|
||||
std::abs(abs_perp - dx) < kAxisCollapseTol
|
||||
|| std::abs(abs_perp - dy) < kAxisCollapseTol
|
||||
|| std::abs(abs_perp - dz) < kAxisCollapseTol;
|
||||
if (abs_perp > 1e-6 && !redundant) {
|
||||
const std::array<float, 3> tip = {
|
||||
float(a[0] + perp * n_avg[0]),
|
||||
float(a[1] + perp * n_avg[1]),
|
||||
float(a[2] + perp * n_avg[2]),
|
||||
};
|
||||
auto perp_grp = makeGroup(
|
||||
{a[0],a[1],a[2], tip[0],tip[1],tip[2]},
|
||||
1.0f, 1.0f, 1.0f, /*dashed*/ true);
|
||||
groups.push_back(perp_grp);
|
||||
labels.push_back(makeLabel(a, tip,
|
||||
"perp: " + QString::number(abs_perp, 'f', 3) + " m"));
|
||||
}
|
||||
}
|
||||
}
|
||||
} else if (n >= 3) {
|
||||
// 3-pt and 4+pt: white connecting polyline (closed for 4+) with
|
||||
// per-segment length labels. HUD carries the angle/area readout.
|
||||
std::vector<float> seg_xyz;
|
||||
seg_xyz.reserve(n * 6);
|
||||
labels.reserve(n);
|
||||
auto addSeg = [&](const std::array<float, 3>& a,
|
||||
const std::array<float, 3>& b) {
|
||||
pushSeg(seg_xyz, a, b);
|
||||
labels.push_back(makeLabel(a, b,
|
||||
QString::number(dist3(a, b), 'f', 3) + " m"));
|
||||
};
|
||||
for (size_t i = 0; i + 1 < n; ++i) addSeg(points_[i], points_[i + 1]);
|
||||
if (n >= 4) addSeg(points_[n - 1], points_[0]);
|
||||
groups.push_back(makeGroup(std::move(seg_xyz), 1.0f, 1.0f, 1.0f));
|
||||
}
|
||||
|
||||
vp.setOverlayLines(groups);
|
||||
vp.setOverlayLabels(labels);
|
||||
vp.setHudText(formatReadout());
|
||||
}
|
||||
|
||||
namespace {
|
||||
|
||||
// Which world axis is `v` closest to? Used to label the BFS extent
|
||||
// bars (X/Y/Z) without hard-coding wall vs floor convention.
|
||||
const char* dominantAxisLabel(const float v[3]) {
|
||||
const float ax = std::abs(v[0]);
|
||||
const float ay = std::abs(v[1]);
|
||||
const float az = std::abs(v[2]);
|
||||
if (az >= ax && az >= ay) return "Z";
|
||||
if (ax >= ay) return "X";
|
||||
return "Y";
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
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
|
||||
|
||||
// ---------- Tangent basis in world ----------
|
||||
// t1 = world-up Gram-Schmidt'd against n; fall back to world-X for
|
||||
// near-horizontal surfaces so the basis never degenerates.
|
||||
constexpr float WORLD_UP[3] = {0.0f, 0.0f, 1.0f};
|
||||
const float dot_un = WORLD_UP[0]*n[0] + WORLD_UP[1]*n[1] + WORLD_UP[2]*n[2];
|
||||
float t1[3] = {
|
||||
WORLD_UP[0] - dot_un * n[0],
|
||||
WORLD_UP[1] - dot_un * n[1],
|
||||
WORLD_UP[2] - dot_un * n[2],
|
||||
};
|
||||
float t1_len = std::sqrt(t1[0]*t1[0] + t1[1]*t1[1] + t1[2]*t1[2]);
|
||||
if (t1_len < 0.1f) {
|
||||
constexpr float WORLD_X[3] = {1.0f, 0.0f, 0.0f};
|
||||
const float dot_xn = WORLD_X[0]*n[0] + WORLD_X[1]*n[1] + WORLD_X[2]*n[2];
|
||||
t1[0] = WORLD_X[0] - dot_xn * n[0];
|
||||
t1[1] = WORLD_X[1] - dot_xn * n[1];
|
||||
t1[2] = WORLD_X[2] - dot_xn * n[2];
|
||||
t1_len = std::sqrt(t1[0]*t1[0] + t1[1]*t1[1] + t1[2]*t1[2]);
|
||||
}
|
||||
if (t1_len > 1e-6f) {
|
||||
t1[0] /= t1_len; t1[1] /= t1_len; t1[2] /= t1_len;
|
||||
}
|
||||
const float t2[3] = {
|
||||
n[1]*t1[2] - n[2]*t1[1],
|
||||
n[2]*t1[0] - n[0]*t1[2],
|
||||
n[0]*t1[1] - n[1]*t1[0],
|
||||
};
|
||||
|
||||
std::vector<OverlayRenderer::LineGroup> groups;
|
||||
std::vector<OverlayRenderer::Label> labels;
|
||||
QStringList hud_lines;
|
||||
hud_lines << QStringLiteral("Laser measure (click another point for distance)");
|
||||
|
||||
// ---------- Coplanar-patch BFS for face extent ----------
|
||||
// Read back the seed mesh, transform every vertex into world space,
|
||||
// build edge adjacency, BFS from the seed triangle keeping only
|
||||
// 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;
|
||||
bool have_extent = false;
|
||||
double min_t1 = 0.0, max_t1 = 0.0, min_t2 = 0.0, max_t2 = 0.0;
|
||||
if (vp.readbackMeshTriangles(first_pick_.model_id, first_pick_.mesh_id, tris)) {
|
||||
const size_t n_verts = tris.positions.size() / 3;
|
||||
const size_t n_tris = tris.indices.size() / 3;
|
||||
if (n_tris > 0) {
|
||||
// Vertices → world.
|
||||
std::vector<float> wv(n_verts * 3);
|
||||
const float* M = first_pick_.composed_transform;
|
||||
for (size_t i = 0; i < n_verts; ++i) {
|
||||
const float* p = &tris.positions[i * 3];
|
||||
wv[i*3 + 0] = M[0]*p[0] + M[4]*p[1] + M[8]*p[2] + M[12];
|
||||
wv[i*3 + 1] = M[1]*p[0] + M[5]*p[1] + M[9]*p[2] + M[13];
|
||||
wv[i*3 + 2] = M[2]*p[0] + M[6]*p[1] + M[10]*p[2] + M[14];
|
||||
}
|
||||
// Per-tri world normals + edge adjacency.
|
||||
std::vector<std::array<float, 3>> tri_n(n_tris);
|
||||
std::unordered_map<uint64_t, std::vector<uint32_t>> edges;
|
||||
edges.reserve(n_tris * 3);
|
||||
for (size_t t = 0; t < n_tris; ++t) {
|
||||
const uint32_t ia = tris.indices[3*t + 0];
|
||||
const uint32_t ib = tris.indices[3*t + 1];
|
||||
const uint32_t ic = tris.indices[3*t + 2];
|
||||
const float* a = &wv[3*ia];
|
||||
const float* b = &wv[3*ib];
|
||||
const float* c = &wv[3*ic];
|
||||
const float bax = b[0]-a[0], bay = b[1]-a[1], baz = b[2]-a[2];
|
||||
const float cax = c[0]-a[0], cay = c[1]-a[1], caz = c[2]-a[2];
|
||||
float nx = bay*caz - baz*cay;
|
||||
float ny = baz*cax - bax*caz;
|
||||
float nz = bax*cay - bay*cax;
|
||||
const float nl = std::sqrt(nx*nx + ny*ny + nz*nz);
|
||||
if (nl > 0.0f) { nx /= nl; ny /= nl; nz /= nl; }
|
||||
tri_n[t] = {nx, ny, nz};
|
||||
edges[edgeKey(ia, ib)].push_back(uint32_t(t));
|
||||
edges[edgeKey(ib, ic)].push_back(uint32_t(t));
|
||||
edges[edgeKey(ic, ia)].push_back(uint32_t(t));
|
||||
}
|
||||
// Seed = nearest triangle to world click.
|
||||
uint32_t seed = 0;
|
||||
double best = std::numeric_limits<double>::infinity();
|
||||
for (size_t t = 0; t < n_tris; ++t) {
|
||||
const uint32_t ia = tris.indices[3*t + 0];
|
||||
const uint32_t ib = tris.indices[3*t + 1];
|
||||
const uint32_t ic = tris.indices[3*t + 2];
|
||||
const double d = pointTriangleDistSq(
|
||||
wp, &wv[3*ia], &wv[3*ib], &wv[3*ic]);
|
||||
if (d < best) { best = d; seed = uint32_t(t); }
|
||||
}
|
||||
// BFS coplanar.
|
||||
const auto& sn = tri_n[seed];
|
||||
std::unordered_set<uint32_t> in_patch;
|
||||
in_patch.insert(seed);
|
||||
std::queue<uint32_t> frontier;
|
||||
frontier.push(seed);
|
||||
while (!frontier.empty()) {
|
||||
const uint32_t t = frontier.front(); frontier.pop();
|
||||
for (int e = 0; e < 3; ++e) {
|
||||
const uint32_t ia = tris.indices[3*t + e];
|
||||
const uint32_t ib = tris.indices[3*t + (e + 1) % 3];
|
||||
auto it = edges.find(edgeKey(ia, ib));
|
||||
if (it == edges.end()) continue;
|
||||
for (uint32_t nt : it->second) {
|
||||
if (nt == t || in_patch.count(nt)) continue;
|
||||
const auto& nn = tri_n[nt];
|
||||
const double dot = double(sn[0])*nn[0]
|
||||
+ double(sn[1])*nn[1]
|
||||
+ double(sn[2])*nn[2];
|
||||
if (dot < kCoplanarDot) continue;
|
||||
in_patch.insert(nt);
|
||||
frontier.push(nt);
|
||||
}
|
||||
}
|
||||
}
|
||||
// Project unique patch vertices → tangent coords.
|
||||
std::unordered_set<uint32_t> patch_verts;
|
||||
for (uint32_t t : in_patch) {
|
||||
patch_verts.insert(tris.indices[3*t + 0]);
|
||||
patch_verts.insert(tris.indices[3*t + 1]);
|
||||
patch_verts.insert(tris.indices[3*t + 2]);
|
||||
}
|
||||
for (uint32_t vi : patch_verts) {
|
||||
const float* v = &wv[3 * vi];
|
||||
const double dx = double(v[0]) - wp[0];
|
||||
const double dy = double(v[1]) - wp[1];
|
||||
const double dz = double(v[2]) - wp[2];
|
||||
const double a1 = dx*t1[0] + dy*t1[1] + dz*t1[2];
|
||||
const double a2 = dx*t2[0] + dy*t2[1] + dz*t2[2];
|
||||
if (!have_extent) {
|
||||
min_t1 = max_t1 = a1;
|
||||
min_t2 = max_t2 = a2;
|
||||
have_extent = true;
|
||||
} else {
|
||||
min_t1 = std::min(min_t1, a1); max_t1 = std::max(max_t1, a1);
|
||||
min_t2 = std::min(min_t2, a2); max_t2 = std::max(max_t2, a2);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
auto pushBar = [&](const float t[3], double mn, double mx) {
|
||||
const std::array<float, 3> a = {
|
||||
float(wp[0] + mn * t[0]),
|
||||
float(wp[1] + mn * t[1]),
|
||||
float(wp[2] + mn * t[2]),
|
||||
};
|
||||
const std::array<float, 3> b = {
|
||||
float(wp[0] + mx * t[0]),
|
||||
float(wp[1] + mx * t[1]),
|
||||
float(wp[2] + mx * t[2]),
|
||||
};
|
||||
const double extent = mx - mn;
|
||||
const QString axis = QString::fromLatin1(dominantAxisLabel(t));
|
||||
groups.push_back(makeGroup({a[0],a[1],a[2], b[0],b[1],b[2]},
|
||||
1.0f, 1.0f, 1.0f, /*dashed*/ true));
|
||||
labels.push_back(makeLabel(a, b,
|
||||
QString("%1 extent: %2 m").arg(axis).arg(extent, 0, 'f', 3)));
|
||||
hud_lines << QString("%1 extent: %2 m").arg(axis).arg(extent, 0, 'f', 3);
|
||||
};
|
||||
if (have_extent && (max_t1 - min_t1) > 1e-6) pushBar(t1, min_t1, max_t1);
|
||||
if (have_extent && (max_t2 - min_t2) > 1e-6) pushBar(t2, min_t2, max_t2);
|
||||
|
||||
// ---------- Hybrid: vertical raycast for horizontal surfaces ----------
|
||||
// For floors / ceilings (|n.z| close to 1) the BFS extents give the
|
||||
// floor footprint; the *useful* extra dimension is the room height,
|
||||
// which a single raycast in +n finds. Skip on walls (|n.z| < 0.85)
|
||||
// — there the BFS already covers the user's intent.
|
||||
if (std::abs(n[2]) > 0.85f) {
|
||||
constexpr float NUDGE = 1e-3f;
|
||||
const float ro[3] = {
|
||||
wp[0] + NUDGE * n[0],
|
||||
wp[1] + NUDGE * n[1],
|
||||
wp[2] + NUDGE * n[2],
|
||||
};
|
||||
ViewportWindow::RaycastHit hit;
|
||||
if (vp.raycast(ro, n, hit)) {
|
||||
const double dist = double(hit.distance) + double(NUDGE);
|
||||
const std::array<float, 3> a = {wp[0], wp[1], wp[2]};
|
||||
const std::array<float, 3> b = {hit.world_pos[0],
|
||||
hit.world_pos[1],
|
||||
hit.world_pos[2]};
|
||||
const QString tag = (n[2] > 0.0f)
|
||||
? QStringLiteral("ceiling height")
|
||||
: QStringLiteral("floor distance");
|
||||
groups.push_back(makeGroup({a[0],a[1],a[2], b[0],b[1],b[2]},
|
||||
1.0f, 1.0f, 1.0f, /*dashed*/ true));
|
||||
labels.push_back(makeLabel(a, b,
|
||||
QString("%1: %2 m").arg(tag).arg(dist, 0, 'f', 3)));
|
||||
hud_lines << QString("%1: %2 m").arg(tag).arg(dist, 0, 'f', 3);
|
||||
}
|
||||
}
|
||||
|
||||
pushDots(vp, std::vector<float>(wp, wp + 3));
|
||||
vp.setOverlayLines(groups);
|
||||
vp.setOverlayLabels(labels);
|
||||
vp.setHudText(hud_lines.join('\n'));
|
||||
}
|
||||
|
||||
QString LengthMeasurement::formatReadout() const {
|
||||
const size_t n = points_.size();
|
||||
if (n == 0) return QStringLiteral("Length tool: click first point");
|
||||
if (n == 1) return QStringLiteral("1 point (click another)");
|
||||
|
||||
if (n == 2) {
|
||||
const auto& a = points_[0];
|
||||
const auto& b = points_[1];
|
||||
const double d = dist3(a, b);
|
||||
const double dx = std::abs(double(b[0]) - a[0]);
|
||||
const double dy = std::abs(double(b[1]) - a[1]);
|
||||
const double dz = std::abs(double(b[2]) - a[2]);
|
||||
return QString("Length: %1 m\nΔX: %2 ΔY: %3 ΔZ: %4 m")
|
||||
.arg(d, 0, 'f', 4)
|
||||
.arg(dx, 0, 'f', 4)
|
||||
.arg(dy, 0, 'f', 4)
|
||||
.arg(dz, 0, 'f', 4);
|
||||
}
|
||||
|
||||
if (n == 3) {
|
||||
const auto& a = points_[0];
|
||||
const auto& b = points_[1];
|
||||
const auto& c = points_[2];
|
||||
// Angle at b (the middle-clicked vertex).
|
||||
const double bax = double(a[0]) - b[0];
|
||||
const double bay = double(a[1]) - b[1];
|
||||
const double baz = double(a[2]) - b[2];
|
||||
const double bcx = double(c[0]) - b[0];
|
||||
const double bcy = double(c[1]) - b[1];
|
||||
const double bcz = double(c[2]) - b[2];
|
||||
const double la = std::sqrt(bax*bax + bay*bay + baz*baz);
|
||||
const double lc = std::sqrt(bcx*bcx + bcy*bcy + bcz*bcz);
|
||||
double angle_deg = 0.0;
|
||||
if (la > 0.0 && lc > 0.0) {
|
||||
const double cosang = std::clamp(
|
||||
(bax*bcx + bay*bcy + baz*bcz) / (la * lc), -1.0, 1.0);
|
||||
angle_deg = std::acos(cosang) * 180.0 / M_PI;
|
||||
}
|
||||
return QString("Angle at pt 2: %1°\nTriangle area: %2 m²\nPerimeter: %3 m")
|
||||
.arg(angle_deg, 0, 'f', 2)
|
||||
.arg(triArea3(a, b, c), 0, 'f', 4)
|
||||
.arg(dist3(a, b) + dist3(b, c) + dist3(c, a), 0, 'f', 4);
|
||||
}
|
||||
|
||||
// 4+ points: polygon area.
|
||||
const PolygonAreaResult r = polygonArea(points_);
|
||||
double perimeter = 0.0;
|
||||
for (size_t i = 0; i < n; ++i) {
|
||||
perimeter += dist3(points_[i], points_[(i + 1) % n]);
|
||||
}
|
||||
return QString("Polygon (%1 pts, %2)\nArea: %3 m²\nPerimeter: %4 m")
|
||||
.arg(n)
|
||||
.arg(r.method)
|
||||
.arg(r.area_m2, 0, 'f', 4)
|
||||
.arg(perimeter, 0, 'f', 4);
|
||||
}
|
||||
|
||||
@@ -20,12 +20,15 @@
|
||||
#ifndef IFCVIEWER_FULL_MEASUREMENT_H
|
||||
#define IFCVIEWER_FULL_MEASUREMENT_H
|
||||
|
||||
#include "ViewportWindow.h"
|
||||
|
||||
#include <QString>
|
||||
|
||||
#include <array>
|
||||
#include <cstdint>
|
||||
#include <unordered_map>
|
||||
#include <vector>
|
||||
|
||||
class ViewportWindow;
|
||||
|
||||
// Sum of mesh-local volumes (m³) of every instance whose object_id is in
|
||||
// `object_ids`. Groups by (model, mesh) so each unique mesh is read back
|
||||
// from the GPU at most once per call; instances of the same mesh are scaled
|
||||
@@ -36,6 +39,15 @@ class ViewportWindow;
|
||||
double volumeOfObjects(ViewportWindow& vp,
|
||||
const std::vector<uint32_t>& object_ids);
|
||||
|
||||
// Per-object volumes (m³). Same algorithm as volumeOfObjects but
|
||||
// attributed per id rather than summed. Skips ids that don't resolve
|
||||
// to a live instance, so the result may be shorter than the input.
|
||||
// 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,
|
||||
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,
|
||||
// expands it into the connected coplanar patch (BFS over shared edges,
|
||||
@@ -104,4 +116,47 @@ private:
|
||||
double total_area_m2_ = 0.0;
|
||||
};
|
||||
|
||||
// Click-to-place length / angle / area measurement. Each pick appends a
|
||||
// world-space point. The readout adapts to the point count:
|
||||
//
|
||||
// 1 point → "laser-measure" mode: 6 rays (±surface-normal, ±tangent₁,
|
||||
// ±tangent₂ in the surface's own basis) trace into the scene.
|
||||
// On a wall this gives thickness + floor-to-ceiling height +
|
||||
// length-along-wall in one click. Tangent₁ is world up
|
||||
// projected onto the surface plane (Gram-Schmidt against the
|
||||
// normal); tangent₂ = normal × tangent₁.
|
||||
// 2 points → straight-line distance plus axis-aligned ΔX/ΔY/ΔZ
|
||||
// 3 points → angle at the middle vertex plus the triangle's area
|
||||
// 4+ → polygon area: best-fit-plane shoelace if the points are
|
||||
// near-coplanar (RMS plane distance < 1e-3 of the bounding
|
||||
// box), else fan-triangulated from the first point
|
||||
//
|
||||
// Clicked points are pushed to the viewport overlay as small dots and
|
||||
// the connecting polyline (or the laser rays for 1-point); readouts
|
||||
// go to the multi-line HUD.
|
||||
class LengthMeasurement {
|
||||
public:
|
||||
LengthMeasurement();
|
||||
|
||||
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(ViewportWindow& vp);
|
||||
void rebuildLaserOverlay(ViewportWindow& vp);
|
||||
QString formatReadout() const;
|
||||
|
||||
std::vector<std::array<float, 3>> points_;
|
||||
std::vector<std::array<float, 3>> normals_; // surface normal at each pick
|
||||
|
||||
// Captured at the very first pick of a fresh sequence and never
|
||||
// updated afterwards. Used by the 1-pt laser BFS to 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_{};
|
||||
};
|
||||
|
||||
#endif // IFCVIEWER_FULL_MEASUREMENT_H
|
||||
|
||||
@@ -52,21 +52,6 @@ void SettingsWindow::setupUi() {
|
||||
"closed solids; disable if you see holes in open geometry.");
|
||||
form->addRow("Backface Culling", backface_culling_check_);
|
||||
|
||||
load_data_source_check_ = new QCheckBox(this);
|
||||
load_data_source_check_->setToolTip(
|
||||
"Keep the .ifc/.rdb open after loading so element properties can "
|
||||
"be queried. Disable for geometry-only viewing — saves memory "
|
||||
"and, on sidecar hits, avoids a second file read.");
|
||||
form->addRow("Load Property Data Source", load_data_source_check_);
|
||||
|
||||
apply_coordinate_operation_check_ = new QCheckBox(this);
|
||||
apply_coordinate_operation_check_->setToolTip(
|
||||
"Apply each model's IfcCoordinateOperation (e.g. IfcMapConversion) "
|
||||
"after load so it lands in georeferenced map coordinates. "
|
||||
"Disable to keep models in their local engineering frame.");
|
||||
form->addRow("Apply Coordinate Operation",
|
||||
apply_coordinate_operation_check_);
|
||||
|
||||
void_limit_spin_ = new QSpinBox(this);
|
||||
void_limit_spin_->setRange(0, 100000);
|
||||
void_limit_spin_->setToolTip(
|
||||
@@ -94,6 +79,50 @@ void SettingsWindow::setupUi() {
|
||||
"curved surface. Smaller = smoother shading but more triangles.");
|
||||
form->addRow("Angular Tolerance", angular_tolerance_spin_);
|
||||
|
||||
min_pixel_radius_spin_ = new QDoubleSpinBox(this);
|
||||
min_pixel_radius_spin_->setRange(0.0, 100.0);
|
||||
min_pixel_radius_spin_->setDecimals(2);
|
||||
min_pixel_radius_spin_->setSingleStep(0.5);
|
||||
min_pixel_radius_spin_->setToolTip(
|
||||
"Minimum projected sphere radius (in pixels) for an instance to "
|
||||
"be drawn. Bigger = faster but more pop-in on small detail.");
|
||||
form->addRow("Min Pixel Radius", min_pixel_radius_spin_);
|
||||
|
||||
motion_min_pixel_radius_spin_ = new QDoubleSpinBox(this);
|
||||
motion_min_pixel_radius_spin_->setRange(0.0, 100.0);
|
||||
motion_min_pixel_radius_spin_->setDecimals(2);
|
||||
motion_min_pixel_radius_spin_->setSingleStep(1.0);
|
||||
motion_min_pixel_radius_spin_->setToolTip(
|
||||
"Aggressive cull threshold while the camera is moving. 0 = no "
|
||||
"motion boost (motion uses the same threshold as still frames). "
|
||||
"Big perceived FPS win on heavy scenes.");
|
||||
form->addRow("Motion Min Pixel Radius", motion_min_pixel_radius_spin_);
|
||||
|
||||
lod1_pixel_threshold_spin_ = new QDoubleSpinBox(this);
|
||||
lod1_pixel_threshold_spin_->setRange(0.0, 1000.0);
|
||||
lod1_pixel_threshold_spin_->setDecimals(1);
|
||||
lod1_pixel_threshold_spin_->setSingleStep(1.0);
|
||||
lod1_pixel_threshold_spin_->setToolTip(
|
||||
"Pixel radius below which an instance switches to its LOD1 "
|
||||
"representation. 0 disables LOD1 entirely (always draw LOD0).");
|
||||
form->addRow("LOD1 Pixel Threshold", lod1_pixel_threshold_spin_);
|
||||
|
||||
hiz_enabled_check_ = new QCheckBox(this);
|
||||
hiz_enabled_check_->setToolTip(
|
||||
"Enable HiZ (hierarchical Z) occlusion culling. Hides geometry "
|
||||
"behind opaque blockers based on a downsampled depth pyramid "
|
||||
"from the previous frame. Big perf win on dense interiors.");
|
||||
form->addRow("HiZ Occlusion", hiz_enabled_check_);
|
||||
|
||||
hiz_resolution_spin_ = new QSpinBox(this);
|
||||
hiz_resolution_spin_->setRange(64, 4096);
|
||||
hiz_resolution_spin_->setSingleStep(64);
|
||||
hiz_resolution_spin_->setToolTip(
|
||||
"Base HiZ pyramid width in texels (height tracks aspect). "
|
||||
"Bigger = tighter occlusion but more readback bandwidth. "
|
||||
"Changes take effect on next viewport reinitialization.");
|
||||
form->addRow("HiZ Resolution", hiz_resolution_spin_);
|
||||
|
||||
auto* button_box = new QDialogButtonBox(
|
||||
QDialogButtonBox::Ok | QDialogButtonBox::Cancel, this);
|
||||
|
||||
@@ -116,23 +145,27 @@ void SettingsWindow::syncFromSettings() {
|
||||
geometry_library_edit_->setText(AppSettings::instance().geometryLibrary());
|
||||
show_stats_check_->setChecked(AppSettings::instance().showStats());
|
||||
backface_culling_check_->setChecked(AppSettings::instance().backfaceCulling());
|
||||
load_data_source_check_->setChecked(AppSettings::instance().loadDataSource());
|
||||
apply_coordinate_operation_check_->setChecked(
|
||||
AppSettings::instance().applyCoordinateOperation());
|
||||
void_limit_spin_->setValue(AppSettings::instance().voidLimit());
|
||||
deflection_tolerance_spin_->setValue(AppSettings::instance().deflectionTolerance());
|
||||
angular_tolerance_spin_->setValue(AppSettings::instance().angularTolerance());
|
||||
min_pixel_radius_spin_->setValue(AppSettings::instance().minPixelRadius());
|
||||
motion_min_pixel_radius_spin_->setValue(AppSettings::instance().motionMinPixelRadius());
|
||||
lod1_pixel_threshold_spin_->setValue(AppSettings::instance().lod1PixelThreshold());
|
||||
hiz_enabled_check_->setChecked(AppSettings::instance().hizEnabled());
|
||||
hiz_resolution_spin_->setValue(AppSettings::instance().hizResolution());
|
||||
}
|
||||
|
||||
void SettingsWindow::onAccepted() {
|
||||
AppSettings::instance().setGeometryLibrary(geometry_library_edit_->text());
|
||||
AppSettings::instance().setShowStats(show_stats_check_->isChecked());
|
||||
AppSettings::instance().setBackfaceCulling(backface_culling_check_->isChecked());
|
||||
AppSettings::instance().setLoadDataSource(load_data_source_check_->isChecked());
|
||||
AppSettings::instance().setApplyCoordinateOperation(
|
||||
apply_coordinate_operation_check_->isChecked());
|
||||
AppSettings::instance().setVoidLimit(void_limit_spin_->value());
|
||||
AppSettings::instance().setDeflectionTolerance(deflection_tolerance_spin_->value());
|
||||
AppSettings::instance().setAngularTolerance(angular_tolerance_spin_->value());
|
||||
AppSettings::instance().setMinPixelRadius(min_pixel_radius_spin_->value());
|
||||
AppSettings::instance().setMotionMinPixelRadius(motion_min_pixel_radius_spin_->value());
|
||||
AppSettings::instance().setLod1PixelThreshold(lod1_pixel_threshold_spin_->value());
|
||||
AppSettings::instance().setHizEnabled(hiz_enabled_check_->isChecked());
|
||||
AppSettings::instance().setHizResolution(hiz_resolution_spin_->value());
|
||||
accept();
|
||||
}
|
||||
|
||||
@@ -46,11 +46,14 @@ private:
|
||||
QLineEdit* geometry_library_edit_ = nullptr;
|
||||
QCheckBox* show_stats_check_ = nullptr;
|
||||
QCheckBox* backface_culling_check_ = nullptr;
|
||||
QCheckBox* load_data_source_check_ = nullptr;
|
||||
QCheckBox* apply_coordinate_operation_check_ = nullptr;
|
||||
QSpinBox* void_limit_spin_ = nullptr;
|
||||
QDoubleSpinBox* deflection_tolerance_spin_ = nullptr;
|
||||
QDoubleSpinBox* angular_tolerance_spin_ = nullptr;
|
||||
QDoubleSpinBox* min_pixel_radius_spin_ = nullptr;
|
||||
QDoubleSpinBox* motion_min_pixel_radius_spin_ = nullptr;
|
||||
QDoubleSpinBox* lod1_pixel_threshold_spin_ = nullptr;
|
||||
QSpinBox* hiz_resolution_spin_ = nullptr;
|
||||
QCheckBox* hiz_enabled_check_ = nullptr;
|
||||
};
|
||||
|
||||
#endif
|
||||
|
||||
+109
-29
@@ -26,14 +26,23 @@ constexpr const char* kGeometryLibraryKey = "geometry/library";
|
||||
constexpr const char* kGeometryLibraryDefault = "hybrid-cgal-simple-opencascade";
|
||||
constexpr const char* kShowStatsKey = "viewport/show_stats";
|
||||
constexpr const char* kBackfaceCullingKey = "viewport/backface_culling";
|
||||
constexpr const char* kLoadDataSourceKey = "loading/load_data_source";
|
||||
constexpr const char* kApplyCoordinateOperationKey = "loading/apply_coordinate_operation";
|
||||
constexpr const char* kVoidLimitKey = "loading/void_limit";
|
||||
constexpr int kVoidLimitDefault = 30;
|
||||
constexpr const char* kDeflectionToleranceKey = "loading/deflection_tolerance";
|
||||
constexpr double kDeflectionToleranceDefault = 0.001;
|
||||
constexpr const char* kAngularToleranceKey = "loading/angular_tolerance";
|
||||
constexpr double kAngularToleranceDefault = 0.5;
|
||||
constexpr const char* kMinPixelRadiusKey = "viewport/min_pixel_radius";
|
||||
constexpr double kMinPixelRadiusDefault = 2.0;
|
||||
constexpr const char* kMotionMinPixelRadiusKey = "viewport/motion_min_pixel_radius";
|
||||
constexpr double kMotionMinPixelRadiusDefault = 10.0;
|
||||
constexpr const char* kLod1PixelThresholdKey = "viewport/lod1_pixel_threshold";
|
||||
constexpr double kLod1PixelThresholdDefault = 30.0;
|
||||
constexpr const char* kHizResolutionKey = "viewport/hiz_resolution";
|
||||
constexpr int kHizResolutionDefault = 256;
|
||||
constexpr int kHizResolutionFloor = 64;
|
||||
constexpr const char* kHizEnabledKey = "viewport/hiz_enabled";
|
||||
constexpr const char* kNavPresetKey = "viewport/nav_preset";
|
||||
}
|
||||
|
||||
AppSettings& AppSettings::instance() {
|
||||
@@ -78,28 +87,6 @@ void AppSettings::setBackfaceCulling(bool value) {
|
||||
emit backfaceCullingChanged(value);
|
||||
}
|
||||
|
||||
bool AppSettings::loadDataSource() const {
|
||||
return load_data_source_;
|
||||
}
|
||||
|
||||
void AppSettings::setLoadDataSource(bool value) {
|
||||
if (load_data_source_ == value) return;
|
||||
load_data_source_ = value;
|
||||
persist();
|
||||
emit loadDataSourceChanged(value);
|
||||
}
|
||||
|
||||
bool AppSettings::applyCoordinateOperation() const {
|
||||
return apply_coordinate_operation_;
|
||||
}
|
||||
|
||||
void AppSettings::setApplyCoordinateOperation(bool value) {
|
||||
if (apply_coordinate_operation_ == value) return;
|
||||
apply_coordinate_operation_ = value;
|
||||
persist();
|
||||
emit applyCoordinateOperationChanged(value);
|
||||
}
|
||||
|
||||
int AppSettings::voidLimit() const {
|
||||
return void_limit_;
|
||||
}
|
||||
@@ -136,20 +123,109 @@ void AppSettings::setAngularTolerance(double value) {
|
||||
emit angularToleranceChanged(value);
|
||||
}
|
||||
|
||||
double AppSettings::minPixelRadius() const {
|
||||
return min_pixel_radius_;
|
||||
}
|
||||
|
||||
void AppSettings::setMinPixelRadius(double value) {
|
||||
if (value < 0.0) value = 0.0;
|
||||
if (min_pixel_radius_ == value) return;
|
||||
min_pixel_radius_ = value;
|
||||
persist();
|
||||
emit minPixelRadiusChanged(value);
|
||||
}
|
||||
|
||||
double AppSettings::motionMinPixelRadius() const {
|
||||
return motion_min_pixel_radius_;
|
||||
}
|
||||
|
||||
void AppSettings::setMotionMinPixelRadius(double value) {
|
||||
if (value < 0.0) value = 0.0;
|
||||
if (motion_min_pixel_radius_ == value) return;
|
||||
motion_min_pixel_radius_ = value;
|
||||
persist();
|
||||
emit motionMinPixelRadiusChanged(value);
|
||||
}
|
||||
|
||||
double AppSettings::lod1PixelThreshold() const {
|
||||
return lod1_pixel_threshold_;
|
||||
}
|
||||
|
||||
void AppSettings::setLod1PixelThreshold(double value) {
|
||||
if (value < 0.0) value = 0.0;
|
||||
if (lod1_pixel_threshold_ == value) return;
|
||||
lod1_pixel_threshold_ = value;
|
||||
persist();
|
||||
emit lod1PixelThresholdChanged(value);
|
||||
}
|
||||
|
||||
int AppSettings::hizResolution() const {
|
||||
return hiz_resolution_;
|
||||
}
|
||||
|
||||
void AppSettings::setHizResolution(int value) {
|
||||
if (value < kHizResolutionFloor) value = kHizResolutionFloor;
|
||||
if (hiz_resolution_ == value) return;
|
||||
hiz_resolution_ = value;
|
||||
persist();
|
||||
emit hizResolutionChanged(value);
|
||||
}
|
||||
|
||||
bool AppSettings::hizEnabled() const {
|
||||
return hiz_enabled_;
|
||||
}
|
||||
|
||||
void AppSettings::setHizEnabled(bool value) {
|
||||
if (hiz_enabled_ == value) return;
|
||||
hiz_enabled_ = value;
|
||||
persist();
|
||||
emit hizEnabledChanged(value);
|
||||
}
|
||||
|
||||
AppSettings::NavPreset AppSettings::navPreset() const {
|
||||
return nav_preset_;
|
||||
}
|
||||
|
||||
void AppSettings::setNavPreset(NavPreset value) {
|
||||
if (nav_preset_ == value) return;
|
||||
nav_preset_ = value;
|
||||
persist();
|
||||
emit navPresetChanged(value);
|
||||
}
|
||||
|
||||
void AppSettings::load() {
|
||||
QSettings settings;
|
||||
geometry_library_ = settings.value(kGeometryLibraryKey, kGeometryLibraryDefault).toString();
|
||||
show_stats_ = settings.value(kShowStatsKey, false).toBool();
|
||||
backface_culling_ = settings.value(kBackfaceCullingKey, true).toBool();
|
||||
load_data_source_ = settings.value(kLoadDataSourceKey, true).toBool();
|
||||
apply_coordinate_operation_ =
|
||||
settings.value(kApplyCoordinateOperationKey, false).toBool();
|
||||
void_limit_ = settings.value(kVoidLimitKey, kVoidLimitDefault).toInt();
|
||||
if (void_limit_ < 0) void_limit_ = 0;
|
||||
deflection_tolerance_ = settings.value(kDeflectionToleranceKey, kDeflectionToleranceDefault).toDouble();
|
||||
if (deflection_tolerance_ <= 0.0) deflection_tolerance_ = kDeflectionToleranceDefault;
|
||||
angular_tolerance_ = settings.value(kAngularToleranceKey, kAngularToleranceDefault).toDouble();
|
||||
if (angular_tolerance_ <= 0.0) angular_tolerance_ = kAngularToleranceDefault;
|
||||
min_pixel_radius_ = settings.value(kMinPixelRadiusKey, kMinPixelRadiusDefault).toDouble();
|
||||
if (min_pixel_radius_ < 0.0) min_pixel_radius_ = 0.0;
|
||||
motion_min_pixel_radius_ =
|
||||
settings.value(kMotionMinPixelRadiusKey, kMotionMinPixelRadiusDefault).toDouble();
|
||||
if (motion_min_pixel_radius_ < 0.0) motion_min_pixel_radius_ = 0.0;
|
||||
lod1_pixel_threshold_ =
|
||||
settings.value(kLod1PixelThresholdKey, kLod1PixelThresholdDefault).toDouble();
|
||||
if (lod1_pixel_threshold_ < 0.0) lod1_pixel_threshold_ = 0.0;
|
||||
hiz_resolution_ = settings.value(kHizResolutionKey, kHizResolutionDefault).toInt();
|
||||
if (hiz_resolution_ < kHizResolutionFloor) hiz_resolution_ = kHizResolutionFloor;
|
||||
hiz_enabled_ = settings.value(kHizEnabledKey, true).toBool();
|
||||
{
|
||||
const int raw = settings.value(kNavPresetKey,
|
||||
static_cast<int>(NavPreset::Blender)).toInt();
|
||||
// Clamp to known values so a stale config doesn't drop us into
|
||||
// an undefined preset slot.
|
||||
if (raw < 0 || raw > static_cast<int>(NavPreset::Revit)) {
|
||||
nav_preset_ = NavPreset::Blender;
|
||||
} else {
|
||||
nav_preset_ = static_cast<NavPreset>(raw);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void AppSettings::persist() {
|
||||
@@ -157,9 +233,13 @@ void AppSettings::persist() {
|
||||
settings.setValue(kGeometryLibraryKey, geometry_library_);
|
||||
settings.setValue(kShowStatsKey, show_stats_);
|
||||
settings.setValue(kBackfaceCullingKey, backface_culling_);
|
||||
settings.setValue(kLoadDataSourceKey, load_data_source_);
|
||||
settings.setValue(kApplyCoordinateOperationKey, apply_coordinate_operation_);
|
||||
settings.setValue(kVoidLimitKey, void_limit_);
|
||||
settings.setValue(kDeflectionToleranceKey, deflection_tolerance_);
|
||||
settings.setValue(kAngularToleranceKey, angular_tolerance_);
|
||||
settings.setValue(kMinPixelRadiusKey, min_pixel_radius_);
|
||||
settings.setValue(kMotionMinPixelRadiusKey, motion_min_pixel_radius_);
|
||||
settings.setValue(kLod1PixelThresholdKey, lod1_pixel_threshold_);
|
||||
settings.setValue(kHizResolutionKey, hiz_resolution_);
|
||||
settings.setValue(kHizEnabledKey, hiz_enabled_);
|
||||
settings.setValue(kNavPresetKey, static_cast<int>(nav_preset_));
|
||||
}
|
||||
|
||||
+63
-19
@@ -29,6 +29,21 @@
|
||||
class AppSettings : public QObject {
|
||||
Q_OBJECT
|
||||
public:
|
||||
// Navigation preset. Selects which mouse button (+ optional Shift)
|
||||
// drives orbit and pan. Selection is unaffected and stays on LMB
|
||||
// for every preset — these three intentionally don't take LMB so
|
||||
// click + box-select remain available without modifier gymnastics.
|
||||
//
|
||||
// Blender — Orbit MMB, Pan Shift+MMB (current default)
|
||||
// Rhino — Orbit RMB, Pan Shift+RMB
|
||||
// Revit — Orbit Shift+MMB, Pan MMB
|
||||
enum class NavPreset {
|
||||
Blender = 0,
|
||||
Rhino = 1,
|
||||
Revit = 2,
|
||||
};
|
||||
Q_ENUM(NavPreset)
|
||||
|
||||
static AppSettings& instance();
|
||||
|
||||
QString geometryLibrary() const;
|
||||
@@ -40,21 +55,6 @@ public:
|
||||
bool backfaceCulling() const;
|
||||
void setBackfaceCulling(bool value);
|
||||
|
||||
// When true, the IFC/RocksDB file is kept open (and, on sidecar hits,
|
||||
// opened in the background) so element properties can be queried.
|
||||
// When false, only geometry is loaded — saves memory and avoids a
|
||||
// second file read on sidecar hits, at the cost of no property panel.
|
||||
bool loadDataSource() const;
|
||||
void setLoadDataSource(bool value);
|
||||
|
||||
// When true, each loaded model's IfcCoordinateOperation (e.g.
|
||||
// IfcMapConversion) is applied to the per-instance transform after
|
||||
// load, lifting the model into map (georeferenced) coordinates.
|
||||
// When false, models render in their local engineering frame —
|
||||
// useful for previewing geometry without translating to e.g. UTM.
|
||||
bool applyCoordinateOperation() const;
|
||||
void setApplyCoordinateOperation(bool value);
|
||||
|
||||
// Skip elements with more than this many voids (HasOpenings inverse).
|
||||
// Boolean subtraction of many openings is the dominant cost in some
|
||||
// pathological exports; dropping those elements keeps load times sane.
|
||||
@@ -72,15 +72,55 @@ public:
|
||||
double angularTolerance() const;
|
||||
void setAngularTolerance(double value);
|
||||
|
||||
// Minimum projected sphere radius (in pixels) for an instance to be
|
||||
// worth drawing — below this it's contribution-culled. Bigger value
|
||||
// = faster rendering but more pop-in on small detail; smaller =
|
||||
// more thorough but more draw cost.
|
||||
double minPixelRadius() const;
|
||||
void setMinPixelRadius(double value);
|
||||
|
||||
// Aggressive contribution-cull threshold while the camera is moving.
|
||||
// 0 disables the motion boost (motion uses minPixelRadius like still
|
||||
// frames). When >= minPixelRadius, motion frames raise the bar so
|
||||
// transient camera moves stay smooth on heavy scenes.
|
||||
double motionMinPixelRadius() const;
|
||||
void setMotionMinPixelRadius(double value);
|
||||
|
||||
// Projected sphere radius (in pixels) below which an instance
|
||||
// switches to its LOD1 representation. 0 disables LOD1 entirely
|
||||
// (always draw LOD0).
|
||||
double lod1PixelThreshold() const;
|
||||
void setLod1PixelThreshold(double value);
|
||||
|
||||
// Base HiZ pyramid width in texels (height tracks aspect). Bigger
|
||||
// = tighter occlusion but more readback bandwidth. Floored at 64.
|
||||
// Changes take effect on next viewport reinitialization.
|
||||
int hizResolution() const;
|
||||
void setHizResolution(int value);
|
||||
|
||||
// Master toggle for HiZ occlusion culling. When false, only the
|
||||
// frustum + contribution cull run; geometry hidden behind opaque
|
||||
// blockers still draws.
|
||||
bool hizEnabled() const;
|
||||
void setHizEnabled(bool value);
|
||||
|
||||
// Navigation preset (see NavPreset enum above).
|
||||
NavPreset navPreset() const;
|
||||
void setNavPreset(NavPreset value);
|
||||
|
||||
signals:
|
||||
void geometryLibraryChanged(const QString& value);
|
||||
void showStatsChanged(bool value);
|
||||
void backfaceCullingChanged(bool value);
|
||||
void loadDataSourceChanged(bool value);
|
||||
void applyCoordinateOperationChanged(bool value);
|
||||
void voidLimitChanged(int value);
|
||||
void deflectionToleranceChanged(double value);
|
||||
void angularToleranceChanged(double value);
|
||||
void minPixelRadiusChanged(double value);
|
||||
void motionMinPixelRadiusChanged(double value);
|
||||
void lod1PixelThresholdChanged(double value);
|
||||
void hizResolutionChanged(int value);
|
||||
void hizEnabledChanged(bool value);
|
||||
void navPresetChanged(NavPreset value);
|
||||
|
||||
private:
|
||||
AppSettings();
|
||||
@@ -90,11 +130,15 @@ private:
|
||||
QString geometry_library_;
|
||||
bool show_stats_ = false;
|
||||
bool backface_culling_ = true;
|
||||
bool load_data_source_ = true;
|
||||
bool apply_coordinate_operation_ = false;
|
||||
int void_limit_ = 30;
|
||||
double deflection_tolerance_ = 0.001;
|
||||
double angular_tolerance_ = 0.5;
|
||||
double min_pixel_radius_ = 2.0;
|
||||
double motion_min_pixel_radius_ = 10.0;
|
||||
double lod1_pixel_threshold_ = 30.0;
|
||||
int hiz_resolution_ = 256;
|
||||
bool hiz_enabled_ = true;
|
||||
NavPreset nav_preset_ = NavPreset::Blender;
|
||||
};
|
||||
|
||||
#endif // APPSETTINGS_H
|
||||
|
||||
@@ -140,12 +140,10 @@ ModelGeoref computeModelGeoref(ifcopenshell::file* ifc_file) {
|
||||
FederatedFalseOrigin
|
||||
guessFederatedFalseOrigin(const Eigen::Matrix4d& first_placement_meters,
|
||||
const ModelGeoref& georef,
|
||||
const FederationConfig& fed_cfg,
|
||||
bool apply_coordinate_operation) {
|
||||
const FederationConfig& fed_cfg) {
|
||||
Eigen::Vector3d t_m = first_placement_meters.block<3, 1>(0, 3);
|
||||
|
||||
const bool use_coord_op =
|
||||
apply_coordinate_operation && georef.has_coordinate_operation;
|
||||
const bool use_coord_op = georef.has_coordinate_operation;
|
||||
if (use_coord_op) {
|
||||
const Eigen::Vector4d th(t_m.x(), t_m.y(), t_m.z(), 1.0);
|
||||
t_m = (georef.coordinate_operation_meters * th).head<3>();
|
||||
|
||||
@@ -135,18 +135,17 @@ ModelGeoref computeModelGeoref(ifcopenshell::file* ifc_file);
|
||||
// Position: `first_placement_meters` is the model's "anchor" placement —
|
||||
// typically the first instance's `placement_transformation`, which the
|
||||
// iterator already produces in metres (its `convert-back-units` default
|
||||
// is false). The translation is optionally lifted through
|
||||
// `georef.coordinate_operation_meters` (controlled by
|
||||
// `apply_coordinate_operation`), then expressed in the federation unit.
|
||||
// is false). The translation is lifted through
|
||||
// `georef.coordinate_operation_meters` when one is present, then
|
||||
// expressed in the federation unit.
|
||||
//
|
||||
// Rotation: read directly from `georef.coordinate_operation_meters` when
|
||||
// `apply_coordinate_operation && has_coordinate_operation` (this is the
|
||||
// helmert grid-north angle). Otherwise zero. Anticlockwise positive.
|
||||
// Rotation: read directly from `georef.coordinate_operation_meters`
|
||||
// when `has_coordinate_operation` (this is the helmert grid-north
|
||||
// angle); otherwise zero. Anticlockwise positive.
|
||||
FederatedFalseOrigin
|
||||
guessFederatedFalseOrigin(const Eigen::Matrix4d& first_placement_meters,
|
||||
const ModelGeoref& georef,
|
||||
const FederationConfig& fed_cfg,
|
||||
bool apply_coordinate_operation);
|
||||
const FederationConfig& fed_cfg);
|
||||
|
||||
// 1 federation_unit -> N metres.
|
||||
double federationUnitToMeters(const FederationConfig&);
|
||||
|
||||
@@ -482,12 +482,15 @@ void GeometryStreamer::run(const std::string& path, int num_threads) {
|
||||
QElapsedTimer stream_timer;
|
||||
stream_timer.start();
|
||||
|
||||
// Split the 0–100 progress range proportionally to element counts so
|
||||
// the bar advances roughly with wall time across both passes.
|
||||
// Drive the bar from yields across all passes/contexts. Earlier the
|
||||
// [0,100] range was carved evenly across N prioritised contexts, but
|
||||
// in practice nearly every element yields from the first (Body)
|
||||
// context, so smooth progress only ever filled 1/n of the bar before
|
||||
// jumping to the next allocation — visually, a typical 5-context
|
||||
// file looked like it capped at ~20%.
|
||||
const size_t total_count = net_ids.size() + gross_ids.size();
|
||||
const int net_progress_end = total_count == 0
|
||||
? 100
|
||||
: static_cast<int>(100.0 * net_ids.size() / total_count + 0.5);
|
||||
size_t yielded_count = 0;
|
||||
int last_emitted_progress = 0;
|
||||
|
||||
// High-priority context first, so each element gets its preferred
|
||||
// representation; lower-priority contexts only pick up elements the
|
||||
@@ -497,9 +500,7 @@ void GeometryStreamer::run(const std::string& path, int num_threads) {
|
||||
prioritisedContextIds(ifc_file_.get());
|
||||
|
||||
auto run_pass = [&](const std::set<int>& include_ids,
|
||||
bool is_gross,
|
||||
int progress_lo,
|
||||
int progress_hi) -> bool {
|
||||
bool is_gross) -> bool {
|
||||
if (include_ids.empty()) return true;
|
||||
|
||||
ifcopenshell::geometry::Settings base_settings = settings;
|
||||
@@ -510,8 +511,7 @@ void GeometryStreamer::run(const std::string& path, int num_threads) {
|
||||
// Elements that haven't yet produced geometry from any context.
|
||||
std::set<int> remaining = include_ids;
|
||||
|
||||
auto run_iterator = [&](ifcopenshell::geometry::Settings& iter_settings,
|
||||
int sub_lo, int sub_hi) -> bool {
|
||||
auto run_iterator = [&](ifcopenshell::geometry::Settings& iter_settings) -> bool {
|
||||
if (remaining.empty()) return true;
|
||||
|
||||
std::vector<ifcopenshell::geometry::filter_t> filters;
|
||||
@@ -535,14 +535,10 @@ void GeometryStreamer::run(const std::string& path, int num_threads) {
|
||||
|
||||
if (!iterator->initialize()) {
|
||||
// No geometry survived this context for the remaining ids.
|
||||
// Still advance progress so the bar doesn't stall.
|
||||
progress_ = sub_hi;
|
||||
emit progressChanged(sub_hi);
|
||||
// Subsequent contexts will pick them up; nothing to emit.
|
||||
return true;
|
||||
}
|
||||
|
||||
int last_progress = sub_lo;
|
||||
|
||||
do {
|
||||
if (cancel_requested_.load()) break;
|
||||
|
||||
@@ -656,11 +652,13 @@ void GeometryStreamer::run(const std::string& path, int num_threads) {
|
||||
|
||||
emit instanceReady(std::move(inst));
|
||||
total_shapes++;
|
||||
yielded_count++;
|
||||
|
||||
const int p = sub_lo +
|
||||
(iterator->progress() * (sub_hi - sub_lo)) / 100;
|
||||
if (p != last_progress) {
|
||||
last_progress = p;
|
||||
const int p = total_count > 0
|
||||
? static_cast<int>((100 * yielded_count) / total_count)
|
||||
: 100;
|
||||
if (p != last_emitted_progress) {
|
||||
last_emitted_progress = p;
|
||||
progress_ = p;
|
||||
emit progressChanged(p);
|
||||
}
|
||||
@@ -672,12 +670,10 @@ void GeometryStreamer::run(const std::string& path, int num_threads) {
|
||||
if (prioritised_contexts.empty()) {
|
||||
// No IfcGeometricRepresentationContext entities — fall back to
|
||||
// a single iterator pass without context-id filtering.
|
||||
return run_iterator(base_settings, progress_lo, progress_hi);
|
||||
return run_iterator(base_settings);
|
||||
}
|
||||
|
||||
const int range = progress_hi - progress_lo;
|
||||
const int n = static_cast<int>(prioritised_contexts.size());
|
||||
for (int i = 0; i < n; ++i) {
|
||||
for (size_t i = 0; i < prioritised_contexts.size(); ++i) {
|
||||
if (cancel_requested_.load()) break;
|
||||
if (remaining.empty()) break;
|
||||
|
||||
@@ -685,22 +681,15 @@ void GeometryStreamer::run(const std::string& path, int num_threads) {
|
||||
iter_settings.set("context-ids",
|
||||
std::set<int>{ prioritised_contexts[i] });
|
||||
|
||||
const int sub_lo = progress_lo + (range * i) / n;
|
||||
const int sub_hi = (i + 1 == n)
|
||||
? progress_hi
|
||||
: progress_lo + (range * (i + 1)) / n;
|
||||
|
||||
if (!run_iterator(iter_settings, sub_lo, sub_hi)) return false;
|
||||
if (!run_iterator(iter_settings)) return false;
|
||||
}
|
||||
|
||||
progress_ = progress_hi;
|
||||
emit progressChanged(progress_hi);
|
||||
return true;
|
||||
};
|
||||
|
||||
if (!run_pass(net_ids, /*is_gross=*/false, 0, net_progress_end)) return;
|
||||
if (!run_pass(net_ids, /*is_gross=*/false)) return;
|
||||
if (!cancel_requested_.load()) {
|
||||
run_pass(gross_ids, /*is_gross=*/true, net_progress_end, 100);
|
||||
run_pass(gross_ids, /*is_gross=*/true);
|
||||
}
|
||||
|
||||
progress_ = 100;
|
||||
|
||||
@@ -58,7 +58,9 @@ GLuint link(QOpenGLFunctions_4_5_Core* gl, GLuint vs, GLuint fs) {
|
||||
return p;
|
||||
}
|
||||
|
||||
const char* VERT_SRC = R"(
|
||||
// ---- Triangle program (flat color) ----
|
||||
|
||||
const char* TRI_VS = R"(
|
||||
#version 450 core
|
||||
layout(location = 0) in vec3 in_pos;
|
||||
uniform mat4 u_view_proj;
|
||||
@@ -67,7 +69,7 @@ void main() {
|
||||
}
|
||||
)";
|
||||
|
||||
const char* FRAG_SRC = R"(
|
||||
const char* TRI_FS = R"(
|
||||
#version 450 core
|
||||
uniform vec4 u_color;
|
||||
out vec4 frag_color;
|
||||
@@ -76,34 +78,299 @@ void main() {
|
||||
}
|
||||
)";
|
||||
|
||||
// ---- Point sprite program (outlined disc via gl_PointCoord) ----
|
||||
|
||||
const char* POINT_VS = R"(
|
||||
#version 450 core
|
||||
layout(location = 0) in vec3 in_pos;
|
||||
uniform mat4 u_view_proj;
|
||||
uniform float u_point_size;
|
||||
void main() {
|
||||
gl_Position = u_view_proj * vec4(in_pos, 1.0);
|
||||
gl_PointSize = u_point_size;
|
||||
}
|
||||
)";
|
||||
|
||||
// inner_radius_norm is the inner-disc radius as a fraction of the
|
||||
// half-sprite (so 1.0 = no stroke, smaller = thicker stroke). The
|
||||
// fragment shader reads gl_PointCoord (range [0,1] across the sprite),
|
||||
// computes the distance from the centre normalised against the half-
|
||||
// sprite, picks inner vs stroke with a sharp `step()` (no soft band),
|
||||
// then anti-aliases the *outer* edge only.
|
||||
const char* POINT_FS = R"(
|
||||
#version 450 core
|
||||
uniform vec4 u_inner_color;
|
||||
uniform vec4 u_stroke_color;
|
||||
uniform float u_inner_radius_norm;
|
||||
out vec4 frag_color;
|
||||
void main() {
|
||||
vec2 c = gl_PointCoord - 0.5;
|
||||
float d = length(c) * 2.0; // 0 at centre, 1 at sprite edge
|
||||
if (d > 1.0) discard;
|
||||
float t_inner = step(u_inner_radius_norm, d);
|
||||
vec4 col = mix(u_inner_color, u_stroke_color, t_inner);
|
||||
float aa = fwidth(d);
|
||||
float outer_alpha = smoothstep(1.0, 1.0 - aa, d);
|
||||
frag_color = vec4(col.rgb, col.a * outer_alpha);
|
||||
}
|
||||
)";
|
||||
|
||||
// ---- Line program (screen-space-expanded quads with outline) ----
|
||||
//
|
||||
// Per-vertex layout: (in_a, in_b, in_side, in_along), 8 floats total.
|
||||
// The vertex shader projects both endpoints to screen pixels, computes
|
||||
// the screen-space perpendicular, and offsets *this* corner accordingly.
|
||||
// Output v_dist_px is the signed perpendicular distance from the line
|
||||
// axis at this corner; linear interpolation across the quad gives the
|
||||
// per-fragment distance the FS uses to discard / pick inner vs stroke.
|
||||
|
||||
const char* LINE_VS = R"(
|
||||
#version 450 core
|
||||
layout(location = 0) in vec3 in_a;
|
||||
layout(location = 1) in vec3 in_b;
|
||||
layout(location = 2) in float in_side; // -1 or +1
|
||||
layout(location = 3) in float in_along; // 0 (at a) or 1 (at b)
|
||||
uniform mat4 u_view_proj;
|
||||
uniform vec2 u_screen_size; // physical pixels
|
||||
uniform float u_half_width; // inner half-width (px)
|
||||
uniform float u_stroke_extra; // halo per side (px)
|
||||
out float v_dist_px;
|
||||
out float v_along_px; // distance from segment start (px)
|
||||
void main() {
|
||||
vec4 clip_a = u_view_proj * vec4(in_a, 1.0);
|
||||
vec4 clip_b = u_view_proj * vec4(in_b, 1.0);
|
||||
|
||||
// Project to screen pixels.
|
||||
vec2 screen_a = (clip_a.xy / clip_a.w) * 0.5 * u_screen_size;
|
||||
vec2 screen_b = (clip_b.xy / clip_b.w) * 0.5 * u_screen_size;
|
||||
|
||||
vec2 delta = screen_b - screen_a;
|
||||
float len = length(delta);
|
||||
vec2 dir = (len > 1e-6) ? (delta / len) : vec2(1.0, 0.0);
|
||||
vec2 perp = vec2(-dir.y, dir.x);
|
||||
|
||||
// Offset this corner perpendicular to the line.
|
||||
vec4 clip_self = mix(clip_a, clip_b, in_along);
|
||||
vec2 screen_self = (clip_self.xy / clip_self.w) * 0.5 * u_screen_size;
|
||||
float total_half = u_half_width + u_stroke_extra;
|
||||
screen_self += perp * in_side * total_half;
|
||||
|
||||
// Back to NDC, then to clip space (multiply by w to undo the w-divide
|
||||
// GL is about to apply). Depth is preserved from the picked endpoint.
|
||||
vec2 ndc_out = screen_self / (u_screen_size * 0.5);
|
||||
gl_Position = vec4(ndc_out * clip_self.w, clip_self.z, clip_self.w);
|
||||
|
||||
v_dist_px = in_side * total_half;
|
||||
v_along_px = in_along * len;
|
||||
}
|
||||
)";
|
||||
|
||||
// ---- Screen-space rect program (label + HUD backgrounds) ----
|
||||
//
|
||||
// Skip QPainter::fillRect entirely — on QOpenGLPaintDevice it's
|
||||
// unreliable across drivers. Backgrounds are drawn as raw GL quads
|
||||
// using NDC-space coordinates; QPainter only renders the text on top.
|
||||
|
||||
const char* RECT_VS = R"(
|
||||
#version 450 core
|
||||
layout(location = 0) in vec2 in_ndc;
|
||||
void main() {
|
||||
gl_Position = vec4(in_ndc, 0.0, 1.0);
|
||||
}
|
||||
)";
|
||||
|
||||
const char* RECT_FS = R"(
|
||||
#version 450 core
|
||||
uniform vec4 u_color;
|
||||
out vec4 frag_color;
|
||||
void main() {
|
||||
frag_color = u_color;
|
||||
}
|
||||
)";
|
||||
|
||||
const char* LINE_FS = R"(
|
||||
#version 450 core
|
||||
in float v_dist_px;
|
||||
in float v_along_px;
|
||||
uniform vec4 u_inner_color;
|
||||
uniform vec4 u_stroke_color;
|
||||
uniform float u_half_width;
|
||||
uniform float u_stroke_extra;
|
||||
uniform float u_dash_period; // 0 = solid
|
||||
uniform float u_dash_on_ratio;
|
||||
out vec4 frag_color;
|
||||
void main() {
|
||||
if (u_dash_period > 0.0) {
|
||||
float t = mod(v_along_px, u_dash_period);
|
||||
if (t > u_dash_period * u_dash_on_ratio) discard;
|
||||
}
|
||||
float ad = abs(v_dist_px);
|
||||
float total = u_half_width + u_stroke_extra;
|
||||
if (ad > total) discard;
|
||||
|
||||
// Sharp inner-to-stroke transition; AA only the outer halo edge so
|
||||
// the line reads crisp instead of mushy.
|
||||
float t_stroke = step(u_half_width, ad);
|
||||
vec4 col = mix(u_inner_color, u_stroke_color, t_stroke);
|
||||
float outer_a = smoothstep(total, total - 1.0, ad);
|
||||
frag_color = vec4(col.rgb, col.a * outer_a);
|
||||
}
|
||||
)";
|
||||
|
||||
void uploadFloats(QOpenGLFunctions_4_5_Core* gl,
|
||||
GLuint vbo, size_t& capacity_bytes,
|
||||
const std::vector<float>& data) {
|
||||
const size_t bytes = data.size() * sizeof(float);
|
||||
if (bytes == 0) return;
|
||||
if (bytes > capacity_bytes) {
|
||||
const size_t new_cap = bytes + bytes / 2;
|
||||
gl->glNamedBufferData(vbo, GLsizeiptr(new_cap),
|
||||
nullptr, GL_DYNAMIC_DRAW);
|
||||
capacity_bytes = new_cap;
|
||||
}
|
||||
gl->glNamedBufferSubData(vbo, 0, GLsizeiptr(bytes), data.data());
|
||||
}
|
||||
|
||||
// CPU expansion of N segments (3 floats * 2 verts per segment, packed) into
|
||||
// 6 vertices per segment, each carrying (a, b, side, along) = 8 floats.
|
||||
void expandLineSegments(const std::vector<float>& endpoints,
|
||||
std::vector<float>& out) {
|
||||
out.clear();
|
||||
if (endpoints.size() < 6) return;
|
||||
const size_t n_segs = endpoints.size() / 6;
|
||||
out.reserve(n_segs * 6 * 8);
|
||||
static const float CORNERS[6][2] = {
|
||||
{-1.0f, 0.0f}, {+1.0f, 0.0f}, {-1.0f, 1.0f},
|
||||
{-1.0f, 1.0f}, {+1.0f, 0.0f}, {+1.0f, 1.0f},
|
||||
};
|
||||
for (size_t s = 0; s < n_segs; ++s) {
|
||||
const float* a = &endpoints[s * 6 + 0];
|
||||
const float* b = &endpoints[s * 6 + 3];
|
||||
for (int c = 0; c < 6; ++c) {
|
||||
out.push_back(a[0]); out.push_back(a[1]); out.push_back(a[2]);
|
||||
out.push_back(b[0]); out.push_back(b[1]); out.push_back(b[2]);
|
||||
out.push_back(CORNERS[c][0]);
|
||||
out.push_back(CORNERS[c][1]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
void OverlayRenderer::initialize(QOpenGLFunctions_4_5_Core* gl) {
|
||||
if (gl_) return;
|
||||
gl_ = gl;
|
||||
|
||||
GLuint vs = compile(gl_, GL_VERTEX_SHADER, VERT_SRC);
|
||||
GLuint fs = compile(gl_, GL_FRAGMENT_SHADER, FRAG_SRC);
|
||||
program_ = link(gl_, vs, fs);
|
||||
u_view_proj_ = gl_->glGetUniformLocation(program_, "u_view_proj");
|
||||
u_color_ = gl_->glGetUniformLocation(program_, "u_color");
|
||||
// Triangle program.
|
||||
{
|
||||
GLuint vs = compile(gl_, GL_VERTEX_SHADER, TRI_VS);
|
||||
GLuint fs = compile(gl_, GL_FRAGMENT_SHADER, TRI_FS);
|
||||
program_tri_ = link(gl_, vs, fs);
|
||||
u_tri_view_proj_ = gl_->glGetUniformLocation(program_tri_, "u_view_proj");
|
||||
u_tri_color_ = gl_->glGetUniformLocation(program_tri_, "u_color");
|
||||
}
|
||||
// Point program.
|
||||
{
|
||||
GLuint vs = compile(gl_, GL_VERTEX_SHADER, POINT_VS);
|
||||
GLuint fs = compile(gl_, GL_FRAGMENT_SHADER, POINT_FS);
|
||||
program_pt_ = link(gl_, vs, fs);
|
||||
u_pt_view_proj_ = gl_->glGetUniformLocation(program_pt_, "u_view_proj");
|
||||
u_pt_point_size_ = gl_->glGetUniformLocation(program_pt_, "u_point_size");
|
||||
u_pt_inner_color_ = gl_->glGetUniformLocation(program_pt_, "u_inner_color");
|
||||
u_pt_stroke_color_ = gl_->glGetUniformLocation(program_pt_, "u_stroke_color");
|
||||
u_pt_inner_radius_ = gl_->glGetUniformLocation(program_pt_, "u_inner_radius_norm");
|
||||
}
|
||||
// Line program.
|
||||
{
|
||||
GLuint vs = compile(gl_, GL_VERTEX_SHADER, LINE_VS);
|
||||
GLuint fs = compile(gl_, GL_FRAGMENT_SHADER, LINE_FS);
|
||||
program_ln_ = link(gl_, vs, fs);
|
||||
u_ln_view_proj_ = gl_->glGetUniformLocation(program_ln_, "u_view_proj");
|
||||
u_ln_screen_size_ = gl_->glGetUniformLocation(program_ln_, "u_screen_size");
|
||||
u_ln_half_width_ = gl_->glGetUniformLocation(program_ln_, "u_half_width");
|
||||
u_ln_stroke_extra_ = gl_->glGetUniformLocation(program_ln_, "u_stroke_extra");
|
||||
u_ln_inner_color_ = gl_->glGetUniformLocation(program_ln_, "u_inner_color");
|
||||
u_ln_stroke_color_ = gl_->glGetUniformLocation(program_ln_, "u_stroke_color");
|
||||
u_ln_dash_period_ = gl_->glGetUniformLocation(program_ln_, "u_dash_period");
|
||||
u_ln_dash_on_ratio_ = gl_->glGetUniformLocation(program_ln_, "u_dash_on_ratio");
|
||||
}
|
||||
// Screen-space rect program.
|
||||
{
|
||||
GLuint vs = compile(gl_, GL_VERTEX_SHADER, RECT_VS);
|
||||
GLuint fs = compile(gl_, GL_FRAGMENT_SHADER, RECT_FS);
|
||||
program_rect_ = link(gl_, vs, fs);
|
||||
u_rect_color_ = gl_->glGetUniformLocation(program_rect_, "u_color");
|
||||
}
|
||||
|
||||
gl_->glCreateVertexArrays(1, &vao_);
|
||||
gl_->glCreateBuffers(1, &vbo_);
|
||||
gl_->glEnableVertexArrayAttrib(vao_, 0);
|
||||
gl_->glVertexArrayAttribFormat(vao_, 0, 3, GL_FLOAT, GL_FALSE, 0);
|
||||
gl_->glVertexArrayAttribBinding(vao_, 0, 0);
|
||||
gl_->glVertexArrayVertexBuffer(vao_, 0, vbo_, 0, 3 * sizeof(float));
|
||||
// Triangle VAO/VBO: one vec3 attribute.
|
||||
gl_->glCreateVertexArrays(1, &triangles_.vao);
|
||||
gl_->glCreateBuffers(1, &triangles_.vbo);
|
||||
gl_->glEnableVertexArrayAttrib(triangles_.vao, 0);
|
||||
gl_->glVertexArrayAttribFormat(triangles_.vao, 0, 3, GL_FLOAT, GL_FALSE, 0);
|
||||
gl_->glVertexArrayAttribBinding(triangles_.vao, 0, 0);
|
||||
gl_->glVertexArrayVertexBuffer(triangles_.vao, 0, triangles_.vbo,
|
||||
0, 3 * sizeof(float));
|
||||
|
||||
// Point VAO/VBO: one vec3 attribute.
|
||||
gl_->glCreateVertexArrays(1, &points_.vao);
|
||||
gl_->glCreateBuffers(1, &points_.vbo);
|
||||
gl_->glEnableVertexArrayAttrib(points_.vao, 0);
|
||||
gl_->glVertexArrayAttribFormat(points_.vao, 0, 3, GL_FLOAT, GL_FALSE, 0);
|
||||
gl_->glVertexArrayAttribBinding(points_.vao, 0, 0);
|
||||
gl_->glVertexArrayVertexBuffer(points_.vao, 0, points_.vbo,
|
||||
0, 3 * sizeof(float));
|
||||
|
||||
// Line VAO/VBO: 8 floats per vertex (a:vec3, b:vec3, side, along).
|
||||
// Shared across every group; line_draws_ records the (first, count)
|
||||
// slice for each.
|
||||
gl_->glCreateVertexArrays(1, &vao_lines_);
|
||||
gl_->glCreateBuffers(1, &vbo_lines_);
|
||||
const GLsizei stride = 8 * sizeof(float);
|
||||
gl_->glEnableVertexArrayAttrib(vao_lines_, 0);
|
||||
gl_->glVertexArrayAttribFormat(vao_lines_, 0, 3, GL_FLOAT, GL_FALSE, 0);
|
||||
gl_->glVertexArrayAttribBinding(vao_lines_, 0, 0);
|
||||
gl_->glEnableVertexArrayAttrib(vao_lines_, 1);
|
||||
gl_->glVertexArrayAttribFormat(vao_lines_, 1, 3, GL_FLOAT, GL_FALSE, 3 * sizeof(float));
|
||||
gl_->glVertexArrayAttribBinding(vao_lines_, 1, 0);
|
||||
gl_->glEnableVertexArrayAttrib(vao_lines_, 2);
|
||||
gl_->glVertexArrayAttribFormat(vao_lines_, 2, 1, GL_FLOAT, GL_FALSE, 6 * sizeof(float));
|
||||
gl_->glVertexArrayAttribBinding(vao_lines_, 2, 0);
|
||||
gl_->glEnableVertexArrayAttrib(vao_lines_, 3);
|
||||
gl_->glVertexArrayAttribFormat(vao_lines_, 3, 1, GL_FLOAT, GL_FALSE, 7 * sizeof(float));
|
||||
gl_->glVertexArrayAttribBinding(vao_lines_, 3, 0);
|
||||
gl_->glVertexArrayVertexBuffer(vao_lines_, 0, vbo_lines_, 0, stride);
|
||||
|
||||
// Screen-rect VAO/VBO: 2 floats per vertex (vec2 NDC).
|
||||
gl_->glCreateVertexArrays(1, &vao_rect_);
|
||||
gl_->glCreateBuffers(1, &vbo_rect_);
|
||||
gl_->glEnableVertexArrayAttrib(vao_rect_, 0);
|
||||
gl_->glVertexArrayAttribFormat(vao_rect_, 0, 2, GL_FLOAT, GL_FALSE, 0);
|
||||
gl_->glVertexArrayAttribBinding(vao_rect_, 0, 0);
|
||||
gl_->glVertexArrayVertexBuffer(vao_rect_, 0, vbo_rect_, 0, 2 * sizeof(float));
|
||||
}
|
||||
|
||||
void OverlayRenderer::release() {
|
||||
if (!gl_) return;
|
||||
if (vbo_) gl_->glDeleteBuffers(1, &vbo_);
|
||||
if (vao_) gl_->glDeleteVertexArrays(1, &vao_);
|
||||
if (program_) gl_->glDeleteProgram(program_);
|
||||
program_ = vao_ = vbo_ = 0;
|
||||
vbo_capacity_ = 0;
|
||||
vertex_count_ = 0;
|
||||
if (triangles_.vbo) gl_->glDeleteBuffers(1, &triangles_.vbo);
|
||||
if (triangles_.vao) gl_->glDeleteVertexArrays(1, &triangles_.vao);
|
||||
if (points_.vbo) gl_->glDeleteBuffers(1, &points_.vbo);
|
||||
if (points_.vao) gl_->glDeleteVertexArrays(1, &points_.vao);
|
||||
if (vbo_lines_) gl_->glDeleteBuffers(1, &vbo_lines_);
|
||||
if (vao_lines_) gl_->glDeleteVertexArrays(1, &vao_lines_);
|
||||
if (vbo_rect_) gl_->glDeleteBuffers(1, &vbo_rect_);
|
||||
if (vao_rect_) gl_->glDeleteVertexArrays(1, &vao_rect_);
|
||||
if (program_tri_) gl_->glDeleteProgram(program_tri_);
|
||||
if (program_pt_) gl_->glDeleteProgram(program_pt_);
|
||||
if (program_ln_) gl_->glDeleteProgram(program_ln_);
|
||||
if (program_rect_) gl_->glDeleteProgram(program_rect_);
|
||||
triangles_ = {};
|
||||
points_ = {};
|
||||
line_draws_.clear();
|
||||
vao_lines_ = vbo_lines_ = 0;
|
||||
vbo_lines_capacity_ = 0;
|
||||
vao_rect_ = vbo_rect_ = 0;
|
||||
vbo_rect_capacity_ = 0;
|
||||
program_tri_ = program_pt_ = program_ln_ = program_rect_ = 0;
|
||||
gl_ = nullptr;
|
||||
}
|
||||
|
||||
@@ -111,89 +378,338 @@ void OverlayRenderer::setHudText(const QString& text) {
|
||||
hud_text_ = text;
|
||||
}
|
||||
|
||||
void OverlayRenderer::setOverlayLabels(const std::vector<Label>& labels) {
|
||||
labels_ = labels;
|
||||
}
|
||||
|
||||
void OverlayRenderer::setSelectionRect(const QRect& rect_logical) {
|
||||
selection_rect_ = rect_logical;
|
||||
}
|
||||
|
||||
void OverlayRenderer::setHighlightTriangles(const std::vector<float>& world_xyz,
|
||||
float r, float g, float b, float a) {
|
||||
if (!gl_) return;
|
||||
color_[0] = r; color_[1] = g; color_[2] = b; color_[3] = a;
|
||||
vertex_count_ = GLsizei(world_xyz.size() / 3);
|
||||
if (vertex_count_ == 0) return;
|
||||
triangles_.color[0] = r; triangles_.color[1] = g;
|
||||
triangles_.color[2] = b; triangles_.color[3] = a;
|
||||
triangles_.vertex_count = GLsizei(world_xyz.size() / 3);
|
||||
uploadFloats(gl_, triangles_.vbo, triangles_.vbo_capacity, world_xyz);
|
||||
}
|
||||
|
||||
const size_t bytes = world_xyz.size() * sizeof(float);
|
||||
if (bytes > vbo_capacity_) {
|
||||
// Grow with a little headroom so frequent appends don't realloc.
|
||||
const size_t new_cap = bytes + bytes / 2;
|
||||
gl_->glNamedBufferData(vbo_, GLsizeiptr(new_cap),
|
||||
nullptr, GL_DYNAMIC_DRAW);
|
||||
vbo_capacity_ = new_cap;
|
||||
void OverlayRenderer::setOverlayPoints(const std::vector<float>& world_xyz,
|
||||
float r, float g, float b, float a,
|
||||
float pixel_size,
|
||||
float sr, float sg, float sb, float sa,
|
||||
float stroke_extra) {
|
||||
if (!gl_) return;
|
||||
points_.inner_color[0] = r; points_.inner_color[1] = g;
|
||||
points_.inner_color[2] = b; points_.inner_color[3] = a;
|
||||
points_.stroke_color[0] = sr; points_.stroke_color[1] = sg;
|
||||
points_.stroke_color[2] = sb; points_.stroke_color[3] = sa;
|
||||
points_.pixel_size = pixel_size;
|
||||
points_.stroke_extra = stroke_extra;
|
||||
points_.vertex_count = GLsizei(world_xyz.size() / 3);
|
||||
uploadFloats(gl_, points_.vbo, points_.vbo_capacity, world_xyz);
|
||||
}
|
||||
|
||||
void OverlayRenderer::setOverlayLines(const std::vector<LineGroup>& groups) {
|
||||
if (!gl_) return;
|
||||
line_draws_.clear();
|
||||
|
||||
// Concatenate every group's CPU-expanded vertices into one big buffer
|
||||
// and remember each group's (first, count) slice + style so render()
|
||||
// can iterate without re-expanding.
|
||||
std::vector<float> combined;
|
||||
for (const auto& g : groups) {
|
||||
std::vector<float> exp;
|
||||
expandLineSegments(g.world_xyz, exp);
|
||||
if (exp.empty()) continue;
|
||||
LineDrawCall dc;
|
||||
std::memcpy(dc.color, g.color, sizeof(dc.color));
|
||||
std::memcpy(dc.stroke_color, g.stroke_color, sizeof(dc.stroke_color));
|
||||
dc.line_width = g.line_width;
|
||||
dc.stroke_extra = g.stroke_extra;
|
||||
dc.dash_period_px = g.dash_period_px;
|
||||
dc.dash_on_ratio = g.dash_on_ratio;
|
||||
dc.first = GLint(combined.size() / 8);
|
||||
dc.count = GLsizei(exp.size() / 8);
|
||||
line_draws_.push_back(dc);
|
||||
combined.insert(combined.end(), exp.begin(), exp.end());
|
||||
}
|
||||
gl_->glNamedBufferSubData(vbo_, 0, GLsizeiptr(bytes), world_xyz.data());
|
||||
uploadFloats(gl_, vbo_lines_, vbo_lines_capacity_, combined);
|
||||
}
|
||||
|
||||
void OverlayRenderer::render(const float view_proj[16],
|
||||
int pixel_w, int pixel_h, qreal dpr) {
|
||||
if (!gl_) return;
|
||||
|
||||
// GL pass: tinted highlight triangles.
|
||||
if (program_ && vertex_count_ > 0 && color_[3] > 0.0f) {
|
||||
gl_->glUseProgram(program_);
|
||||
gl_->glUniformMatrix4fv(u_view_proj_, 1, GL_FALSE, view_proj);
|
||||
gl_->glUniform4fv(u_color_, 1, color_);
|
||||
// Save GL state we touch.
|
||||
GLboolean prev_blend = gl_->glIsEnabled(GL_BLEND);
|
||||
GLboolean prev_cull = gl_->glIsEnabled(GL_CULL_FACE);
|
||||
GLboolean prev_pt_size = gl_->glIsEnabled(GL_PROGRAM_POINT_SIZE);
|
||||
GLboolean prev_depth_msk = GL_TRUE;
|
||||
gl_->glGetBooleanv(GL_DEPTH_WRITEMASK, &prev_depth_msk);
|
||||
GLint prev_depth_func = GL_LESS;
|
||||
gl_->glGetIntegerv(GL_DEPTH_FUNC, &prev_depth_func);
|
||||
GLint prev_blend_src = GL_ONE, prev_blend_dst = GL_ZERO;
|
||||
gl_->glGetIntegerv(GL_BLEND_SRC_ALPHA, &prev_blend_src);
|
||||
gl_->glGetIntegerv(GL_BLEND_DST_ALPHA, &prev_blend_dst);
|
||||
|
||||
// Save the GL state we touch and restore at the end so the rest of
|
||||
// the render pass keeps seeing what it expects.
|
||||
GLboolean prev_blend = gl_->glIsEnabled(GL_BLEND);
|
||||
GLboolean prev_cull = gl_->glIsEnabled(GL_CULL_FACE);
|
||||
GLboolean prev_depth_msk = GL_TRUE;
|
||||
gl_->glGetBooleanv(GL_DEPTH_WRITEMASK, &prev_depth_msk);
|
||||
GLint prev_depth_func = GL_LESS;
|
||||
gl_->glGetIntegerv(GL_DEPTH_FUNC, &prev_depth_func);
|
||||
GLint prev_blend_src = GL_ONE, prev_blend_dst = GL_ZERO;
|
||||
gl_->glGetIntegerv(GL_BLEND_SRC_ALPHA, &prev_blend_src);
|
||||
gl_->glGetIntegerv(GL_BLEND_DST_ALPHA, &prev_blend_dst);
|
||||
gl_->glEnable(GL_BLEND);
|
||||
gl_->glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
|
||||
gl_->glDisable(GL_CULL_FACE);
|
||||
gl_->glDepthMask(GL_FALSE);
|
||||
gl_->glDepthFunc(GL_LEQUAL);
|
||||
gl_->glEnable(GL_PROGRAM_POINT_SIZE);
|
||||
|
||||
if (triangles_.vertex_count > 0 && triangles_.color[3] > 0.0f) {
|
||||
// Highlight triangles stay depth-aware (GL_LEQUAL) so they tint
|
||||
// the surface in place rather than poking through walls.
|
||||
gl_->glUseProgram(program_tri_);
|
||||
gl_->glUniformMatrix4fv(u_tri_view_proj_, 1, GL_FALSE, view_proj);
|
||||
gl_->glUniform4fv(u_tri_color_, 1, triangles_.color);
|
||||
gl_->glBindVertexArray(triangles_.vao);
|
||||
gl_->glDrawArrays(GL_TRIANGLES, 0, triangles_.vertex_count);
|
||||
}
|
||||
// Measurement annotations (lines + points) draw on top of every other
|
||||
// pass — the standard CAD convention. GL_ALWAYS wins every depth
|
||||
// compare; GL_LEQUAL is restored at the end of the function.
|
||||
gl_->glDepthFunc(GL_ALWAYS);
|
||||
if (!line_draws_.empty()) {
|
||||
gl_->glUseProgram(program_ln_);
|
||||
gl_->glUniformMatrix4fv(u_ln_view_proj_, 1, GL_FALSE, view_proj);
|
||||
gl_->glUniform2f(u_ln_screen_size_, float(pixel_w), float(pixel_h));
|
||||
gl_->glBindVertexArray(vao_lines_);
|
||||
for (const auto& dc : line_draws_) {
|
||||
if (dc.count == 0 || dc.color[3] <= 0.0f) continue;
|
||||
gl_->glUniform1f(u_ln_half_width_, dc.line_width * 0.5f);
|
||||
gl_->glUniform1f(u_ln_stroke_extra_, dc.stroke_extra);
|
||||
gl_->glUniform4fv(u_ln_inner_color_, 1, dc.color);
|
||||
gl_->glUniform4fv(u_ln_stroke_color_, 1, dc.stroke_color);
|
||||
gl_->glUniform1f(u_ln_dash_period_, dc.dash_period_px);
|
||||
gl_->glUniform1f(u_ln_dash_on_ratio_, dc.dash_on_ratio);
|
||||
gl_->glDrawArrays(GL_TRIANGLES, dc.first, dc.count);
|
||||
}
|
||||
}
|
||||
if (points_.vertex_count > 0 && points_.inner_color[3] > 0.0f) {
|
||||
// Inner-radius ratio in [0, 1]: how much of the sprite is the
|
||||
// inner colour vs the stroke band. pixel_size is the inner-disc
|
||||
// diameter; the sprite (and gl_PointSize) is enlarged by
|
||||
// 2*stroke_extra so the halo has somewhere to draw.
|
||||
const float total = points_.pixel_size + 2.0f * points_.stroke_extra;
|
||||
const float inner_ratio = (total > 0.0f)
|
||||
? (points_.pixel_size / total) : 1.0f;
|
||||
gl_->glUseProgram(program_pt_);
|
||||
gl_->glUniformMatrix4fv(u_pt_view_proj_, 1, GL_FALSE, view_proj);
|
||||
gl_->glUniform1f(u_pt_point_size_, total);
|
||||
gl_->glUniform1f(u_pt_inner_radius_, inner_ratio);
|
||||
gl_->glUniform4fv(u_pt_inner_color_, 1, points_.inner_color);
|
||||
gl_->glUniform4fv(u_pt_stroke_color_, 1, points_.stroke_color);
|
||||
gl_->glBindVertexArray(points_.vao);
|
||||
gl_->glDrawArrays(GL_POINTS, 0, points_.vertex_count);
|
||||
}
|
||||
gl_->glBindVertexArray(0);
|
||||
|
||||
if (!prev_blend) gl_->glDisable(GL_BLEND);
|
||||
gl_->glBlendFunc(prev_blend_src, prev_blend_dst);
|
||||
if (prev_cull) gl_->glEnable(GL_CULL_FACE);
|
||||
if (!prev_pt_size) gl_->glDisable(GL_PROGRAM_POINT_SIZE);
|
||||
gl_->glDepthMask(prev_depth_msk);
|
||||
gl_->glDepthFunc(prev_depth_func);
|
||||
|
||||
if (pixel_w <= 0 || pixel_h <= 0) return;
|
||||
|
||||
const float logical_w = float(pixel_w) / float(dpr ? dpr : 1.0);
|
||||
const float logical_h = float(pixel_h) / float(dpr ? dpr : 1.0);
|
||||
auto px_to_ndc_x = [logical_w](float px) {
|
||||
return (px / logical_w) * 2.0f - 1.0f;
|
||||
};
|
||||
auto px_to_ndc_y = [logical_h](float px) {
|
||||
return 1.0f - (px / logical_h) * 2.0f;
|
||||
};
|
||||
|
||||
// Box-select rectangle: translucent fill + 1-px outline drawn as
|
||||
// four thin rects. Comes before the HUD/label pass so the HUD
|
||||
// backgrounds still render on top of the rectangle if they overlap.
|
||||
if (selection_rect_.isValid()
|
||||
&& selection_rect_.width() > 0
|
||||
&& selection_rect_.height() > 0) {
|
||||
struct RectPx { float x0, y0, x1, y1; };
|
||||
const QRect& sr = selection_rect_;
|
||||
const float sx0 = float(sr.left());
|
||||
const float sy0 = float(sr.top());
|
||||
const float sx1 = float(sr.right() + 1);
|
||||
const float sy1 = float(sr.bottom() + 1);
|
||||
const RectPx pieces[5] = {
|
||||
// Fill
|
||||
{sx0, sy0, sx1, sy1},
|
||||
// Top edge
|
||||
{sx0, sy0, sx1, sy0 + 1.0f},
|
||||
// Bottom edge
|
||||
{sx0, sy1 - 1.0f, sx1, sy1},
|
||||
// Left edge
|
||||
{sx0, sy0, sx0 + 1.0f, sy1},
|
||||
// Right edge
|
||||
{sx1 - 1.0f, sy0, sx1, sy1},
|
||||
};
|
||||
const float colors[5][4] = {
|
||||
{0.30f, 0.65f, 1.0f, 0.18f}, // fill
|
||||
{0.30f, 0.65f, 1.0f, 0.9f}, // outline (each edge)
|
||||
{0.30f, 0.65f, 1.0f, 0.9f},
|
||||
{0.30f, 0.65f, 1.0f, 0.9f},
|
||||
{0.30f, 0.65f, 1.0f, 0.9f},
|
||||
};
|
||||
|
||||
GLboolean prev_dt2 = gl_->glIsEnabled(GL_DEPTH_TEST);
|
||||
GLboolean prev_cf2 = gl_->glIsEnabled(GL_CULL_FACE);
|
||||
GLboolean prev_bl2 = gl_->glIsEnabled(GL_BLEND);
|
||||
gl_->glDisable(GL_DEPTH_TEST);
|
||||
gl_->glDisable(GL_CULL_FACE);
|
||||
gl_->glEnable(GL_BLEND);
|
||||
gl_->glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
|
||||
gl_->glDisable(GL_CULL_FACE); // both sides tinted
|
||||
gl_->glDepthMask(GL_FALSE); // tint, don't occlude
|
||||
gl_->glDepthFunc(GL_LEQUAL); // win the coplanar fight
|
||||
gl_->glUseProgram(program_rect_);
|
||||
gl_->glBindVertexArray(vao_rect_);
|
||||
|
||||
gl_->glBindVertexArray(vao_);
|
||||
gl_->glDrawArrays(GL_TRIANGLES, 0, vertex_count_);
|
||||
// Each piece is its own draw so we can switch alpha between
|
||||
// fill and outline. All five share the same VBO slot — we
|
||||
// stream-overwrite per draw.
|
||||
const size_t bytes = 12 * sizeof(float);
|
||||
if (bytes > vbo_rect_capacity_) {
|
||||
const size_t new_cap = bytes + bytes / 2;
|
||||
gl_->glNamedBufferData(vbo_rect_, GLsizeiptr(new_cap),
|
||||
nullptr, GL_DYNAMIC_DRAW);
|
||||
vbo_rect_capacity_ = new_cap;
|
||||
}
|
||||
for (int i = 0; i < 5; ++i) {
|
||||
const float x0 = px_to_ndc_x(pieces[i].x0);
|
||||
const float x1 = px_to_ndc_x(pieces[i].x1);
|
||||
const float y0 = px_to_ndc_y(pieces[i].y0);
|
||||
const float y1 = px_to_ndc_y(pieces[i].y1);
|
||||
const float ndc[12] = {
|
||||
x0, y0, x1, y0, x0, y1,
|
||||
x0, y1, x1, y0, x1, y1
|
||||
};
|
||||
gl_->glNamedBufferSubData(vbo_rect_, 0, GLsizeiptr(bytes), ndc);
|
||||
gl_->glUniform4f(u_rect_color_,
|
||||
colors[i][0], colors[i][1],
|
||||
colors[i][2], colors[i][3]);
|
||||
gl_->glDrawArrays(GL_TRIANGLES, 0, 6);
|
||||
}
|
||||
gl_->glBindVertexArray(0);
|
||||
|
||||
if (!prev_blend) gl_->glDisable(GL_BLEND);
|
||||
gl_->glBlendFunc(prev_blend_src, prev_blend_dst);
|
||||
if (prev_cull) gl_->glEnable(GL_CULL_FACE);
|
||||
gl_->glDepthMask(prev_depth_msk);
|
||||
gl_->glDepthFunc(prev_depth_func);
|
||||
if (prev_dt2) gl_->glEnable(GL_DEPTH_TEST);
|
||||
if (prev_cf2) gl_->glEnable(GL_CULL_FACE);
|
||||
if (!prev_bl2) gl_->glDisable(GL_BLEND);
|
||||
}
|
||||
|
||||
// QPainter pass: HUD text. This rebinds programs/VAOs internally, so
|
||||
// it has to come after every other GL primitive in the overlay.
|
||||
if (!hud_text_.isEmpty() && pixel_w > 0 && pixel_h > 0) {
|
||||
QOpenGLPaintDevice device(QSize(pixel_w, pixel_h));
|
||||
device.setDevicePixelRatio(dpr);
|
||||
QPainter painter(&device);
|
||||
painter.setRenderHint(QPainter::Antialiasing);
|
||||
painter.setRenderHint(QPainter::TextAntialiasing);
|
||||
// Two-stage HUD/label pass: collect rect bounds (in logical pixels) +
|
||||
// text strings, draw all rect backgrounds via GL (screen-space NDC
|
||||
// quads, depth test off), then run a QPainter pass that *only* draws
|
||||
// text on top. Side-stepping QPainter::fillRect entirely avoids the
|
||||
// QOpenGLPaintDevice quirk where solid fills silently drop while
|
||||
// text continues to render.
|
||||
const bool any_painter = !hud_text_.isEmpty() || !labels_.empty();
|
||||
if (!any_painter) return;
|
||||
|
||||
QFont font("monospace", 11);
|
||||
font.setStyleHint(QFont::TypeWriter);
|
||||
painter.setFont(font);
|
||||
const QFontMetrics fm(font);
|
||||
const int pad_x = 10, pad_y = 6, margin = 12;
|
||||
const int text_w = fm.horizontalAdvance(hud_text_);
|
||||
const int text_h = fm.height();
|
||||
const QRect bg(margin, margin,
|
||||
text_w + 2 * pad_x,
|
||||
text_h + 2 * pad_y);
|
||||
QFont label_font("monospace", 9);
|
||||
label_font.setStyleHint(QFont::TypeWriter);
|
||||
QFont hud_font("monospace", 11);
|
||||
hud_font.setStyleHint(QFont::TypeWriter);
|
||||
const QFontMetrics lfm(label_font);
|
||||
const QFontMetrics hfm(hud_font);
|
||||
|
||||
painter.setPen(Qt::NoPen);
|
||||
painter.setBrush(QColor(0, 0, 0, 160));
|
||||
painter.drawRoundedRect(bg, 4, 4);
|
||||
painter.setPen(Qt::white);
|
||||
painter.drawText(bg.adjusted(pad_x, pad_y, -pad_x, -pad_y),
|
||||
Qt::AlignLeft | Qt::AlignVCenter,
|
||||
hud_text_);
|
||||
const int label_pad_x = 4, label_pad_y = 2;
|
||||
const int hud_pad_x = 10, hud_pad_y = 6;
|
||||
const int hud_margin = 12;
|
||||
|
||||
struct PaintItem { QRect bg; QString text; const QFont* font; int align; };
|
||||
std::vector<PaintItem> items;
|
||||
items.reserve(labels_.size() + 1);
|
||||
|
||||
// World-anchored label rects.
|
||||
for (const auto& lbl : labels_) {
|
||||
const float* p = lbl.world_pos;
|
||||
// Column-major: M[col*4 + row].
|
||||
const float wx = view_proj[0]*p[0] + view_proj[4]*p[1] + view_proj[8]*p[2] + view_proj[12];
|
||||
const float wy = view_proj[1]*p[0] + view_proj[5]*p[1] + view_proj[9]*p[2] + view_proj[13];
|
||||
const float ww = view_proj[3]*p[0] + view_proj[7]*p[1] + view_proj[11]*p[2] + view_proj[15];
|
||||
if (ww <= 0.0f) continue; // behind camera
|
||||
const float ndc_x = wx / ww;
|
||||
const float ndc_y = wy / ww;
|
||||
if (ndc_x < -1.0f || ndc_x > 1.0f
|
||||
|| ndc_y < -1.0f || ndc_y > 1.0f) continue;
|
||||
const float sx = (ndc_x * 0.5f + 0.5f) * logical_w;
|
||||
const float sy = (1.0f - (ndc_y * 0.5f + 0.5f)) * logical_h;
|
||||
const int tw = lfm.horizontalAdvance(lbl.text);
|
||||
const int th = lfm.height();
|
||||
QRect bg(int(sx) - tw / 2 - label_pad_x,
|
||||
int(sy) - th / 2 - label_pad_y,
|
||||
tw + 2 * label_pad_x,
|
||||
th + 2 * label_pad_y);
|
||||
items.push_back({bg, lbl.text, &label_font, Qt::AlignCenter});
|
||||
}
|
||||
// HUD rect (always top-left if any text).
|
||||
if (!hud_text_.isEmpty()) {
|
||||
const QStringList lines = hud_text_.split('\n');
|
||||
int tw = 0;
|
||||
for (const auto& ln : lines) tw = qMax(tw, hfm.horizontalAdvance(ln));
|
||||
const int th = hfm.height() * lines.size();
|
||||
QRect bg(hud_margin, hud_margin,
|
||||
tw + 2 * hud_pad_x,
|
||||
th + 2 * hud_pad_y);
|
||||
items.push_back({bg, hud_text_, &hud_font, int(Qt::AlignLeft | Qt::AlignTop)});
|
||||
}
|
||||
|
||||
// GL pass: draw all background rects as NDC-space triangles.
|
||||
if (!items.empty()) {
|
||||
std::vector<float> ndc;
|
||||
ndc.reserve(items.size() * 12); // 6 verts * 2 floats per rect
|
||||
for (const auto& it : items) {
|
||||
const float x0 = px_to_ndc_x(float(it.bg.left()));
|
||||
const float x1 = px_to_ndc_x(float(it.bg.right() + 1));
|
||||
const float y0 = px_to_ndc_y(float(it.bg.top()));
|
||||
const float y1 = px_to_ndc_y(float(it.bg.bottom() + 1));
|
||||
ndc.insert(ndc.end(), {
|
||||
x0, y0, x1, y0, x0, y1,
|
||||
x0, y1, x1, y0, x1, y1
|
||||
});
|
||||
}
|
||||
const size_t bytes = ndc.size() * sizeof(float);
|
||||
if (bytes > vbo_rect_capacity_) {
|
||||
const size_t new_cap = bytes + bytes / 2;
|
||||
gl_->glNamedBufferData(vbo_rect_, GLsizeiptr(new_cap),
|
||||
nullptr, GL_DYNAMIC_DRAW);
|
||||
vbo_rect_capacity_ = new_cap;
|
||||
}
|
||||
gl_->glNamedBufferSubData(vbo_rect_, 0, GLsizeiptr(bytes), ndc.data());
|
||||
|
||||
// GL_TRIANGLES respects GL_CULL_FACE; the NDC→window y-flip turns
|
||||
// our CCW NDC quads into window-CW which get back-culled if cull
|
||||
// is on (which it is by default in this app). Disable cull for
|
||||
// the rect pass — lines+points above were unaffected since
|
||||
// GL_LINES / GL_POINTS skip face culling entirely.
|
||||
GLboolean prev_depth_test = gl_->glIsEnabled(GL_DEPTH_TEST);
|
||||
GLboolean prev_cull_face = gl_->glIsEnabled(GL_CULL_FACE);
|
||||
gl_->glDisable(GL_DEPTH_TEST);
|
||||
gl_->glDisable(GL_CULL_FACE);
|
||||
gl_->glDisable(GL_BLEND);
|
||||
gl_->glUseProgram(program_rect_);
|
||||
gl_->glUniform4f(u_rect_color_, 0.08f, 0.08f, 0.08f, 1.0f);
|
||||
gl_->glBindVertexArray(vao_rect_);
|
||||
gl_->glDrawArrays(GL_TRIANGLES, 0, GLsizei(items.size() * 6));
|
||||
gl_->glBindVertexArray(0);
|
||||
if (prev_depth_test) gl_->glEnable(GL_DEPTH_TEST);
|
||||
if (prev_cull_face) gl_->glEnable(GL_CULL_FACE);
|
||||
}
|
||||
|
||||
// QPainter pass: text only, on top of the GL-drawn backgrounds.
|
||||
QOpenGLPaintDevice device(QSize(pixel_w, pixel_h));
|
||||
device.setDevicePixelRatio(dpr);
|
||||
QPainter painter(&device);
|
||||
painter.setRenderHint(QPainter::TextAntialiasing);
|
||||
painter.setPen(Qt::white);
|
||||
for (const auto& it : items) {
|
||||
painter.setFont(*it.font);
|
||||
const int px = it.font == &hud_font ? hud_pad_x : label_pad_x;
|
||||
const int py = it.font == &hud_font ? hud_pad_y : label_pad_y;
|
||||
painter.drawText(it.bg.adjusted(px, py, -px, -py), it.align, it.text);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -20,6 +20,7 @@
|
||||
#ifndef IFCVIEWER_OVERLAYRENDERER_H
|
||||
#define IFCVIEWER_OVERLAYRENDERER_H
|
||||
|
||||
#include <QRect>
|
||||
#include <QString>
|
||||
#include <QtOpenGL/QOpenGLFunctions_4_5_Core>
|
||||
|
||||
@@ -44,10 +45,54 @@ public:
|
||||
void setHighlightTriangles(const std::vector<float>& world_xyz,
|
||||
float r, float g, float b, float a);
|
||||
|
||||
// One stylistic group of line segments rendered through the
|
||||
// outlined / optionally-dashed line shader. Multiple groups in a
|
||||
// single setOverlayLines call let the caller mix solid + dashed +
|
||||
// axis-coloured legs in one frame (e.g. the length tool's white
|
||||
// total line + RGB XYZ stair-step + dashed perpendicular).
|
||||
struct LineGroup {
|
||||
std::vector<float> world_xyz; // 6 floats per segment (a, b)
|
||||
float color[4] = {1, 1, 1, 1}; // inner color
|
||||
float stroke_color[4] = {0, 0, 0, 1}; // outline (0 alpha = no outline)
|
||||
float line_width = 1.5f; // pixels (inner)
|
||||
float stroke_extra = 0.5f; // pixels per side outside inner
|
||||
float dash_period_px = 0.0f; // 0 = solid; else screen-space dash period
|
||||
float dash_on_ratio = 0.6f; // [0..1], used only when period > 0
|
||||
};
|
||||
void setOverlayLines(const std::vector<LineGroup>& groups);
|
||||
|
||||
// Replace the overlay-point list (3 floats per point, world space).
|
||||
// `pixel_size` is the inner-dot diameter in physical pixels.
|
||||
//
|
||||
// When stroke_a > 0, every point is rendered twice: a wider
|
||||
// (pixel_size + 2*stroke_extra) outer dot in stroke_color, then the
|
||||
// pixel_size inner dot in the main color — giving a crisp halo that
|
||||
// reads on any background.
|
||||
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);
|
||||
|
||||
// World-anchored text labels: each one is projected to screen space
|
||||
// and drawn via QPainter at that pixel. Used today for per-segment
|
||||
// length readouts in the length tool.
|
||||
struct Label {
|
||||
float world_pos[3];
|
||||
QString text;
|
||||
};
|
||||
void setOverlayLabels(const std::vector<Label>& labels);
|
||||
|
||||
// Top-left HUD text drawn via QPainter on the GL surface as part of
|
||||
// render(). Empty hides the HUD.
|
||||
void setHudText(const QString& text);
|
||||
|
||||
// Box-select rectangle (in logical pixel coords, top-left origin).
|
||||
// Drawn as a translucent fill + 1-px outline using the same
|
||||
// screen-space rect program that draws label/HUD backgrounds.
|
||||
// Empty rect hides it.
|
||||
void setSelectionRect(const QRect& rect_logical);
|
||||
|
||||
// Render every overlay primitive in order: GL highlight triangles
|
||||
// (using `view_proj`, column-major float[16]), then HUD text via
|
||||
// QPainter on a QOpenGLPaintDevice sized to (pixel_w × pixel_h)
|
||||
@@ -60,16 +105,93 @@ public:
|
||||
int pixel_w, int pixel_h, qreal device_pixel_ratio);
|
||||
|
||||
private:
|
||||
// Triangle bundle: position-only VBO, single-color shader.
|
||||
struct TriBundle {
|
||||
GLuint vao = 0;
|
||||
GLuint vbo = 0;
|
||||
size_t vbo_capacity = 0;
|
||||
GLsizei vertex_count = 0;
|
||||
float color[4] = {0, 0, 0, 0};
|
||||
};
|
||||
|
||||
// Point bundle: position-only VBO, sprite shader uses gl_PointCoord
|
||||
// to draw an antialiased disc with an outlined halo.
|
||||
struct PointBundle {
|
||||
GLuint vao = 0;
|
||||
GLuint vbo = 0;
|
||||
size_t vbo_capacity = 0;
|
||||
GLsizei vertex_count = 0;
|
||||
float inner_color[4] = {0, 0, 0, 0};
|
||||
float stroke_color[4] = {0, 0, 0, 0};
|
||||
float pixel_size = 8.0f;
|
||||
float stroke_extra = 0.0f;
|
||||
};
|
||||
|
||||
// Per-group draw-call record. setOverlayLines populates one of these
|
||||
// per LineGroup, with `first` indexing into a shared expanded-vertex
|
||||
// VBO. At render time we iterate them, set per-group uniforms, and
|
||||
// issue one glDrawArrays each.
|
||||
struct LineDrawCall {
|
||||
float color[4] = {1, 1, 1, 1};
|
||||
float stroke_color[4] = {0, 0, 0, 0};
|
||||
float line_width = 1.5f;
|
||||
float stroke_extra = 0.5f;
|
||||
float dash_period_px = 0.0f;
|
||||
float dash_on_ratio = 0.6f;
|
||||
GLint first = 0;
|
||||
GLsizei count = 0;
|
||||
};
|
||||
|
||||
QOpenGLFunctions_4_5_Core* gl_ = nullptr;
|
||||
GLuint program_ = 0;
|
||||
GLuint vao_ = 0;
|
||||
GLuint vbo_ = 0;
|
||||
size_t vbo_capacity_ = 0; // bytes
|
||||
GLsizei vertex_count_ = 0;
|
||||
float color_[4] = {0, 0, 0, 0};
|
||||
GLint u_view_proj_ = -1;
|
||||
GLint u_color_ = -1;
|
||||
QString hud_text_;
|
||||
|
||||
// Triangle program (highlight tris).
|
||||
GLuint program_tri_ = 0;
|
||||
GLint u_tri_view_proj_ = -1;
|
||||
GLint u_tri_color_ = -1;
|
||||
|
||||
// Point program (sprite).
|
||||
GLuint program_pt_ = 0;
|
||||
GLint u_pt_view_proj_ = -1;
|
||||
GLint u_pt_point_size_ = -1;
|
||||
GLint u_pt_inner_color_ = -1;
|
||||
GLint u_pt_stroke_color_ = -1;
|
||||
GLint u_pt_inner_radius_ = -1;
|
||||
|
||||
// Line program (screen-space expanded quads).
|
||||
GLuint program_ln_ = 0;
|
||||
GLint u_ln_view_proj_ = -1;
|
||||
GLint u_ln_screen_size_ = -1;
|
||||
GLint u_ln_half_width_ = -1;
|
||||
GLint u_ln_stroke_extra_ = -1;
|
||||
GLint u_ln_inner_color_ = -1;
|
||||
GLint u_ln_stroke_color_ = -1;
|
||||
GLint u_ln_dash_period_ = -1;
|
||||
GLint u_ln_dash_on_ratio_ = -1;
|
||||
|
||||
// Screen-space rect program (label + HUD backgrounds). Vertex
|
||||
// attribute is vec2 NDC; fragment outputs a uniform color. Drawn
|
||||
// with depth test off so rects stack on top of the entire scene.
|
||||
GLuint program_rect_ = 0;
|
||||
GLint u_rect_color_ = -1;
|
||||
GLuint vao_rect_ = 0;
|
||||
GLuint vbo_rect_ = 0;
|
||||
size_t vbo_rect_capacity_ = 0;
|
||||
|
||||
TriBundle triangles_;
|
||||
PointBundle points_;
|
||||
|
||||
// Lines: one shared VAO/VBO holding the concatenated expanded
|
||||
// vertices of every group; line_draws_ records each group's slice.
|
||||
GLuint vao_lines_ = 0;
|
||||
GLuint vbo_lines_ = 0;
|
||||
size_t vbo_lines_capacity_ = 0;
|
||||
std::vector<LineDrawCall> line_draws_;
|
||||
|
||||
std::vector<Label> labels_;
|
||||
QString hud_text_;
|
||||
|
||||
// Box-select rectangle in logical pixels. Null/empty = hidden.
|
||||
QRect selection_rect_;
|
||||
};
|
||||
|
||||
#endif // IFCVIEWER_OVERLAYRENDERER_H
|
||||
|
||||
@@ -166,26 +166,40 @@ void SceneLoader::startNextLoad() {
|
||||
|
||||
std::string ifc_path = model.file_path.toStdString();
|
||||
uint32_t mid = loading_model_id_;
|
||||
const bool is_sidecar_source =
|
||||
QFileInfo(model.file_path).suffix().compare("ifcview", Qt::CaseInsensitive) == 0;
|
||||
|
||||
// Sidecar read on a background thread so the UI stays responsive.
|
||||
joinSidecarThread();
|
||||
sidecar_read_thread_ = std::thread([this, ifc_path, mid]() {
|
||||
sidecar_read_thread_ = std::thread([this, ifc_path, mid, is_sidecar_source]() {
|
||||
QElapsedTimer rt; rt.start();
|
||||
auto cached = readSidecar(ifc_path);
|
||||
qDebug(" Sidecar read: %lld ms (%s)", rt.elapsed(), ifc_path.c_str());
|
||||
auto result = std::make_shared<std::optional<SidecarData>>(std::move(cached));
|
||||
QMetaObject::invokeMethod(this, [this, mid, result]() {
|
||||
QMetaObject::invokeMethod(this, [this, mid, result, is_sidecar_source]() {
|
||||
if (*result && !(*result)->instances.empty()) {
|
||||
applySidecarData(mid, std::move(**result));
|
||||
} else {
|
||||
auto it = models_.find(mid);
|
||||
if (it == models_.end()) return;
|
||||
auto& m = it->second;
|
||||
connectStreamer(m.streamer);
|
||||
element_poll_timer_.start();
|
||||
m.streamer->loadFile(
|
||||
m.file_path.toStdString(), next_object_id_, loading_model_id_);
|
||||
if (!is_sidecar_source) {
|
||||
startDataSourceLoad(mid);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
auto it = models_.find(mid);
|
||||
if (it == models_.end()) return;
|
||||
|
||||
if (is_sidecar_source) {
|
||||
loading_model_id_ = 0;
|
||||
emit loadError(mid, QString("Failed to read IFC Viewer cache:\n%1").arg(it->second.file_path));
|
||||
QTimer::singleShot(0, this, &SceneLoader::startNextLoad);
|
||||
return;
|
||||
}
|
||||
|
||||
auto& m = it->second;
|
||||
connectStreamer(m.streamer);
|
||||
element_poll_timer_.start();
|
||||
m.streamer->loadFile(
|
||||
m.file_path.toStdString(), next_object_id_, loading_model_id_);
|
||||
}, Qt::QueuedConnection);
|
||||
});
|
||||
}
|
||||
@@ -227,7 +241,7 @@ void SceneLoader::applySidecarData(uint32_t mid, SidecarData data) {
|
||||
// Restore the cached CoordinateOperation into the model so
|
||||
// modelGeoref(mid) returns it without needing the IFC source. Prevents
|
||||
// sidecar-loaded models from silently losing their georef when the
|
||||
// .ifc/.rdb sibling is absent or AppSettings.loadDataSource is off.
|
||||
// .ifc/.rdb sibling is absent.
|
||||
{
|
||||
ModelGeoref& gr = model.georef;
|
||||
gr.has_coordinate_operation = data.has_coordinate_operation != 0;
|
||||
@@ -257,47 +271,15 @@ void SceneLoader::applySidecarData(uint32_t mid, SidecarData data) {
|
||||
qint64 ms = model.load_timer.elapsed();
|
||||
emit loadedFromSidecar(mid, ms);
|
||||
|
||||
startDataSourceLoad(mid);
|
||||
|
||||
loading_model_id_ = 0;
|
||||
QTimer::singleShot(0, this, &SceneLoader::startNextLoad);
|
||||
}
|
||||
|
||||
// Match SidecarCache.cpp's sidecarPath() stem logic so we resolve the
|
||||
// data-source siblings against the same stem the sidecar was keyed on.
|
||||
static std::string pathStem(const std::string& path) {
|
||||
std::string p = path;
|
||||
while (!p.empty() && (p.back() == '/' || p.back() == '\\')) p.pop_back();
|
||||
auto slash = p.find_last_of("/\\");
|
||||
auto dot = p.find_last_of('.');
|
||||
return (dot != std::string::npos &&
|
||||
(slash == std::string::npos || dot > slash))
|
||||
? p.substr(0, dot)
|
||||
: p;
|
||||
}
|
||||
|
||||
void SceneLoader::startDataSourceLoad(uint32_t mid) {
|
||||
if (!AppSettings::instance().loadDataSource()) return;
|
||||
|
||||
auto it = models_.find(mid);
|
||||
if (it == models_.end()) return;
|
||||
|
||||
std::string original_path = it->second.file_path.toStdString();
|
||||
std::string stem = pathStem(original_path);
|
||||
|
||||
// Prefer RocksDB (foo.rdb) over SPF (foo.ifc) for fast random lookups.
|
||||
QString data_path;
|
||||
const QString rdb_candidate = QString::fromStdString(stem + ".rdb");
|
||||
const QString ifc_candidate = QString::fromStdString(stem + ".ifc");
|
||||
if (QFileInfo::exists(rdb_candidate)) {
|
||||
data_path = rdb_candidate;
|
||||
} else if (QFileInfo::exists(ifc_candidate)) {
|
||||
data_path = ifc_candidate;
|
||||
} else {
|
||||
return;
|
||||
}
|
||||
|
||||
std::string data_path_std = data_path.toStdString();
|
||||
std::string data_path_std = it->second.file_path.toStdString();
|
||||
data_source_threads_.emplace_back([this, mid, data_path_std]() {
|
||||
QElapsedTimer t; t.start();
|
||||
std::unique_ptr<ifcopenshell::file> file;
|
||||
@@ -371,13 +353,6 @@ void SceneLoader::onStreamerFinished() {
|
||||
|
||||
qint64 ms = it->second.load_timer.elapsed();
|
||||
emit loadedFromStream(mid, ms);
|
||||
|
||||
// Slot(s) above run synchronously (sidecar write uses element_map_,
|
||||
// not ifcFile()); drop the parsed file now to save memory if the
|
||||
// user has opted out of keeping a property data source.
|
||||
if (!AppSettings::instance().loadDataSource()) {
|
||||
it->second.streamer->setIfcFile(nullptr);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,184 @@
|
||||
/********************************************************************************
|
||||
* *
|
||||
* This file is part of IfcOpenShell. *
|
||||
* *
|
||||
* IfcOpenShell is free software: you can redistribute it and/or modify *
|
||||
* it under the terms of the Lesser GNU General Public License as published by *
|
||||
* the Free Software Foundation, either version 3.0 of the License, or *
|
||||
* (at your option) any later version. *
|
||||
* *
|
||||
* IfcOpenShell is distributed in the hope that it will be useful, *
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of *
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the *
|
||||
* Lesser GNU General Public License for more details. *
|
||||
* *
|
||||
* You should have received a copy of the Lesser GNU General Public License *
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>. *
|
||||
* *
|
||||
********************************************************************************/
|
||||
|
||||
#include "Selection.h"
|
||||
|
||||
#include <algorithm>
|
||||
|
||||
SelectionState::SelectionState(QObject* parent) : QObject(parent) {
|
||||
// Reserve slot 0 — object_id 0 is the "no object" sentinel and the
|
||||
// shader still indexes the buffer with v_object_id, so the slot must
|
||||
// exist (and be zero) to avoid OOB reads.
|
||||
cpu_flags_.assign(1, 0u);
|
||||
}
|
||||
|
||||
SelectionState::~SelectionState() = default;
|
||||
|
||||
void SelectionState::initializeGl(QOpenGLFunctions_4_5_Core* gl) {
|
||||
gl_ = gl;
|
||||
if (ssbo_ == 0) {
|
||||
gl_->glCreateBuffers(1, &ssbo_);
|
||||
}
|
||||
// Force a fresh upload against the new context.
|
||||
dirty_ = true;
|
||||
}
|
||||
|
||||
void SelectionState::releaseGl() {
|
||||
if (gl_ && ssbo_) {
|
||||
gl_->glDeleteBuffers(1, &ssbo_);
|
||||
}
|
||||
ssbo_ = 0;
|
||||
ssbo_capacity_slots_ = 0;
|
||||
gl_ = nullptr;
|
||||
dirty_ = true;
|
||||
}
|
||||
|
||||
void SelectionState::reset() {
|
||||
const bool had_state = !selected_ids_.empty() || active_id_ != 0;
|
||||
selected_ids_.clear();
|
||||
active_id_ = 0;
|
||||
std::fill(cpu_flags_.begin(), cpu_flags_.end(), 0u);
|
||||
markDirty();
|
||||
if (had_state) emit changed(active_id_);
|
||||
}
|
||||
|
||||
void SelectionState::noteObjectId(uint32_t id) {
|
||||
if (id == 0) return;
|
||||
if (uint32_t(cpu_flags_.size()) <= id) {
|
||||
// Grow CPU side; the SSBO is sized lazily inside uploadFlags so
|
||||
// we don't realloc GL on every streamed instance.
|
||||
cpu_flags_.resize(size_t(id) + 1, 0u);
|
||||
markDirty();
|
||||
}
|
||||
}
|
||||
|
||||
void SelectionState::setSelectedObjectId(uint32_t id) {
|
||||
if (id == 0) {
|
||||
clearSelection();
|
||||
return;
|
||||
}
|
||||
setSelection({id}, id);
|
||||
}
|
||||
|
||||
void SelectionState::setSelection(const std::unordered_set<uint32_t>& ids,
|
||||
uint32_t active) {
|
||||
// Avoid emitting churn when the call is a no-op.
|
||||
if (ids == selected_ids_ && active == active_id_) return;
|
||||
selected_ids_ = ids;
|
||||
selected_ids_.erase(0);
|
||||
active_id_ = (active != 0 && selected_ids_.count(active)) ? active : 0u;
|
||||
markDirty();
|
||||
emit changed(active_id_);
|
||||
}
|
||||
|
||||
void SelectionState::addToSelection(const std::unordered_set<uint32_t>& ids) {
|
||||
bool any_added = false;
|
||||
for (uint32_t id : ids) {
|
||||
if (id == 0) continue;
|
||||
if (selected_ids_.insert(id).second) any_added = true;
|
||||
}
|
||||
if (!any_added) return;
|
||||
markDirty();
|
||||
emit changed(active_id_);
|
||||
}
|
||||
|
||||
void SelectionState::removeFromSelection(const std::unordered_set<uint32_t>& ids) {
|
||||
bool any_removed = false;
|
||||
bool active_removed = false;
|
||||
for (uint32_t id : ids) {
|
||||
if (selected_ids_.erase(id) > 0) {
|
||||
any_removed = true;
|
||||
if (id == active_id_) active_removed = true;
|
||||
}
|
||||
}
|
||||
if (!any_removed) return;
|
||||
if (active_removed) active_id_ = 0;
|
||||
markDirty();
|
||||
emit changed(active_id_);
|
||||
}
|
||||
|
||||
void SelectionState::toggleInSelection(uint32_t id) {
|
||||
if (id == 0) return;
|
||||
if (selected_ids_.erase(id) > 0) {
|
||||
// Removed. If it was active, drop active.
|
||||
if (active_id_ == id) active_id_ = 0;
|
||||
} else {
|
||||
// Added. Last-toggled becomes active so the properties panel
|
||||
// tracks the most recent click — matches the user's "last single
|
||||
// clicked is active" expectation.
|
||||
selected_ids_.insert(id);
|
||||
active_id_ = id;
|
||||
}
|
||||
markDirty();
|
||||
emit changed(active_id_);
|
||||
}
|
||||
|
||||
void SelectionState::clearSelection() {
|
||||
if (selected_ids_.empty() && active_id_ == 0) return;
|
||||
selected_ids_.clear();
|
||||
active_id_ = 0;
|
||||
markDirty();
|
||||
emit changed(active_id_);
|
||||
}
|
||||
|
||||
void SelectionState::markDirty() {
|
||||
dirty_ = true;
|
||||
}
|
||||
|
||||
void SelectionState::growTo(uint32_t capacity_ids) {
|
||||
if (!gl_ || ssbo_ == 0) return;
|
||||
if (capacity_ids <= ssbo_capacity_slots_) return;
|
||||
// Round up to a power-of-two-ish step so streamed scenes don't realloc
|
||||
// every few instances. Floor at 1024 slots = 4 KB.
|
||||
size_t new_cap = std::max<size_t>(1024, ssbo_capacity_slots_ * 2);
|
||||
while (new_cap < capacity_ids) new_cap *= 2;
|
||||
gl_->glNamedBufferData(ssbo_,
|
||||
GLsizeiptr(new_cap * sizeof(uint32_t)),
|
||||
nullptr, GL_DYNAMIC_DRAW);
|
||||
ssbo_capacity_slots_ = new_cap;
|
||||
}
|
||||
|
||||
void SelectionState::uploadFlags() {
|
||||
if (!gl_ || ssbo_ == 0) return;
|
||||
|
||||
// Rebuild the CPU flag vector from the canonical selected_ids_. The
|
||||
// overhead is O(|cpu_flags_|), which scales with max object_id rather
|
||||
// than with selection size — acceptable: object_ids are dense so this
|
||||
// is just a memset + a handful of writes for the selected set.
|
||||
std::fill(cpu_flags_.begin(), cpu_flags_.end(), 0u);
|
||||
for (uint32_t id : selected_ids_) {
|
||||
if (id < cpu_flags_.size()) cpu_flags_[id] = 1u;
|
||||
}
|
||||
|
||||
growTo(static_cast<uint32_t>(cpu_flags_.size()));
|
||||
if (ssbo_capacity_slots_ == 0) return;
|
||||
|
||||
const GLsizeiptr bytes =
|
||||
GLsizeiptr(cpu_flags_.size() * sizeof(uint32_t));
|
||||
if (bytes > 0) {
|
||||
gl_->glNamedBufferSubData(ssbo_, 0, bytes, cpu_flags_.data());
|
||||
}
|
||||
dirty_ = false;
|
||||
}
|
||||
|
||||
void SelectionState::bindForRender(GLuint binding_index) {
|
||||
if (!gl_ || ssbo_ == 0) return;
|
||||
if (dirty_) uploadFlags();
|
||||
gl_->glBindBufferBase(GL_SHADER_STORAGE_BUFFER, binding_index, ssbo_);
|
||||
}
|
||||
@@ -0,0 +1,126 @@
|
||||
/********************************************************************************
|
||||
* *
|
||||
* This file is part of IfcOpenShell. *
|
||||
* *
|
||||
* IfcOpenShell is free software: you can redistribute it and/or modify *
|
||||
* it under the terms of the Lesser GNU General Public License as published by *
|
||||
* the Free Software Foundation, either version 3.0 of the License, or *
|
||||
* (at your option) any later version. *
|
||||
* *
|
||||
* IfcOpenShell is distributed in the hope that it will be useful, *
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of *
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the *
|
||||
* Lesser GNU General Public License for more details. *
|
||||
* *
|
||||
* You should have received a copy of the Lesser GNU General Public License *
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>. *
|
||||
* *
|
||||
********************************************************************************/
|
||||
|
||||
#ifndef IFCVIEWER_SELECTION_H
|
||||
#define IFCVIEWER_SELECTION_H
|
||||
|
||||
#include <QObject>
|
||||
#include <QtOpenGL/QOpenGLFunctions_4_5_Core>
|
||||
|
||||
#include <cstdint>
|
||||
#include <unordered_set>
|
||||
#include <vector>
|
||||
|
||||
// Multi-selection state for the viewport, owned by ViewportWindow.
|
||||
//
|
||||
// Tracks the *set* of currently selected object_ids plus a single "active"
|
||||
// id — the last single-clicked one. The active id is what the properties
|
||||
// panel and tree mirror; the full set is what the viewport highlights.
|
||||
//
|
||||
// Selection state is published to the main shader through a per-object_id
|
||||
// flags SSBO bound at a caller-chosen index (binding=3 today). The buffer
|
||||
// is sized to max(object_id) + 1 and grown on demand via noteObjectId,
|
||||
// which the viewport calls for every appended instance.
|
||||
//
|
||||
// On every mutation the selection emits `changed(active_id)`. Consumers
|
||||
// (MainWindow, the viewport itself for requestUpdate) connect to it.
|
||||
class SelectionState : public QObject {
|
||||
Q_OBJECT
|
||||
public:
|
||||
explicit SelectionState(QObject* parent = nullptr);
|
||||
~SelectionState() override;
|
||||
|
||||
// Wire up the GL context. Must be called once the viewport's GL
|
||||
// context is current. release() drops GL resources before context
|
||||
// teardown.
|
||||
void initializeGl(QOpenGLFunctions_4_5_Core* gl);
|
||||
void releaseGl();
|
||||
|
||||
// Tell the manager about a newly added object_id so the flag buffer
|
||||
// can grow ahead of the next render. Cheap when the id fits in the
|
||||
// already-allocated CPU vector; otherwise resizes (and marks dirty).
|
||||
void noteObjectId(uint32_t id);
|
||||
|
||||
// Clear everything — both the selection set and the per-object flags.
|
||||
// Called from clearScene.
|
||||
void reset();
|
||||
|
||||
// ---- Mutation API ----
|
||||
//
|
||||
// Plain LMB click → setSelectedObjectId(id) (or clearSelection() for 0).
|
||||
// Modifier+click → toggleInSelection(id).
|
||||
// Box-select on release → setSelection / addToSelection / removeFromSelection
|
||||
// depending on the modifier held when the drag started.
|
||||
//
|
||||
// setSelection's `active` should be in `ids` or 0; if it isn't, the
|
||||
// active is silently coerced to 0.
|
||||
|
||||
void setSelectedObjectId(uint32_t id);
|
||||
void setSelection(const std::unordered_set<uint32_t>& ids, uint32_t active);
|
||||
void addToSelection(const std::unordered_set<uint32_t>& ids);
|
||||
void removeFromSelection(const std::unordered_set<uint32_t>& ids);
|
||||
void toggleInSelection(uint32_t id);
|
||||
void clearSelection();
|
||||
|
||||
// ---- Accessors ----
|
||||
|
||||
bool isSelected(uint32_t id) const { return selected_ids_.count(id) > 0; }
|
||||
bool empty() const { return selected_ids_.empty(); }
|
||||
size_t size() const { return selected_ids_.size(); }
|
||||
const std::unordered_set<uint32_t>& selectionIds() const { return selected_ids_; }
|
||||
uint32_t activeObjectId() const { return active_id_; }
|
||||
|
||||
// ---- GL binding ----
|
||||
//
|
||||
// Bind the selection-flags SSBO at the given binding index for the
|
||||
// upcoming draw. Lazily uploads any pending flag changes. Caller
|
||||
// must have GL context current.
|
||||
void bindForRender(GLuint binding_index);
|
||||
|
||||
signals:
|
||||
// Emitted on any mutation that changes either the set or the active.
|
||||
// Carries the new active id for convenience (consumers usually only
|
||||
// care about the active for properties/tree sync).
|
||||
void changed(uint32_t active_id);
|
||||
|
||||
private:
|
||||
// Mark the SSBO dirty so the next bindForRender() re-uploads it.
|
||||
void markDirty();
|
||||
// Resize the CPU flag vector + GL buffer to hold up to capacity_ids
|
||||
// entries. Called when noteObjectId outgrows the current capacity.
|
||||
void growTo(uint32_t capacity_ids);
|
||||
// Fully overwrite cpu_flags_ from selected_ids_, then upload to GL.
|
||||
void uploadFlags();
|
||||
|
||||
QOpenGLFunctions_4_5_Core* gl_ = nullptr;
|
||||
|
||||
std::unordered_set<uint32_t> selected_ids_;
|
||||
uint32_t active_id_ = 0;
|
||||
|
||||
// Per-object_id flag, indexed by id directly (slot 0 unused — object_id 0
|
||||
// means "no object"). Bit 0 = selected. Stored as uint32 per slot for
|
||||
// std430 alignment simplicity; the byte cost (~4 MB at 1M objects) is
|
||||
// negligible compared to the instance SSBO.
|
||||
std::vector<uint32_t> cpu_flags_;
|
||||
GLuint ssbo_ = 0;
|
||||
size_t ssbo_capacity_slots_ = 0;
|
||||
bool dirty_ = true;
|
||||
};
|
||||
|
||||
#endif // IFCVIEWER_SELECTION_H
|
||||
+622
-115
@@ -88,15 +88,21 @@ struct MeshQuant { vec4 aabb_min; vec4 aabb_max; };
|
||||
layout(std430, binding = 2) readonly buffer Meshes {
|
||||
MeshQuant meshes[];
|
||||
};
|
||||
// Per-object_id selection flag (bit 0 = in selection). Sized so that
|
||||
// any object_id present in the scene is a valid index — see SelectionState.
|
||||
layout(std430, binding = 3) readonly buffer SelectionFlags {
|
||||
uint sel_flags[];
|
||||
};
|
||||
|
||||
uniform mat4 u_view_projection;
|
||||
uniform uint u_selected_id;
|
||||
uniform uint u_active_id; // last single-clicked id (0 = none)
|
||||
|
||||
out vec3 v_normal;
|
||||
out vec4 v_color;
|
||||
out vec3 v_world_pos;
|
||||
flat out uint v_object_id;
|
||||
flat out uint v_selected;
|
||||
flat out uint v_active;
|
||||
|
||||
// Meyer et al. octahedral normal decode. Input is in [-1,1]^2.
|
||||
vec3 octDecode(vec2 e) {
|
||||
@@ -143,7 +149,10 @@ void main() {
|
||||
v_color = baked;
|
||||
|
||||
v_object_id = inst.object_id;
|
||||
v_selected = (v_object_id == u_selected_id) ? 1u : 0u;
|
||||
// Index by object_id directly; SelectionState guarantees the buffer
|
||||
// is sized to cover every id allocated to the scene.
|
||||
v_selected = ((sel_flags[v_object_id] & 1u) != 0u) ? 1u : 0u;
|
||||
v_active = (v_object_id == u_active_id) ? 1u : 0u;
|
||||
}
|
||||
)";
|
||||
|
||||
@@ -154,6 +163,7 @@ in vec4 v_color;
|
||||
in vec3 v_world_pos;
|
||||
flat in uint v_object_id;
|
||||
flat in uint v_selected;
|
||||
flat in uint v_active;
|
||||
|
||||
uniform vec3 u_light_dir; // primary key direction (world-space)
|
||||
uniform vec3 u_fill_dir; // secondary fill direction
|
||||
@@ -202,7 +212,11 @@ void main() {
|
||||
float cavity = clamp(length(fwidth(n)) * 1.5, 0.0, 0.35);
|
||||
color *= (1.0 - cavity);
|
||||
|
||||
if (v_selected == 1u) color = mix(color, vec3(0.2, 0.6, 1.0), 0.5);
|
||||
// Selected (member of the multi-set) gets a blue tint; the active
|
||||
// (last single-clicked) gets a stronger mix so the user can always
|
||||
// tell which one drives the properties panel.
|
||||
if (v_selected == 1u) color = mix(color, vec3(0.2, 0.6, 1.0), 0.45);
|
||||
if (v_active == 1u) color = mix(color, vec3(0.4, 0.8, 1.0), 0.40);
|
||||
frag_color = vec4(color, v_color.a);
|
||||
}
|
||||
)";
|
||||
@@ -671,6 +685,7 @@ ViewportWindow::~ViewportWindow() {
|
||||
if (hiz_downsample_program_) gl_->glDeleteProgram(hiz_downsample_program_);
|
||||
if (hiz_downsample_vao_) gl_->glDeleteVertexArrays(1, &hiz_downsample_vao_);
|
||||
overlay_renderer_.release();
|
||||
selection_.releaseGl();
|
||||
}
|
||||
context_->doneCurrent();
|
||||
}
|
||||
@@ -692,10 +707,25 @@ void ViewportWindow::initGL() {
|
||||
buildPivotIndicator();
|
||||
buildSectionPlaneGizmo();
|
||||
overlay_renderer_.initialize(gl_);
|
||||
selection_.initializeGl(gl_);
|
||||
// Drive viewport repaints + tree/properties sync from selection state.
|
||||
connect(&selection_, &SelectionState::changed, this,
|
||||
[this](uint32_t active_id) {
|
||||
requestUpdate();
|
||||
emit objectPicked(active_id);
|
||||
});
|
||||
// Element-level visibility lives entirely on the CPU (the cull
|
||||
// consults the flag vector directly), but the cached cull result
|
||||
// must be invalidated whenever the hidden set changes — otherwise
|
||||
// a freshly-hidden object would stay drawn on still frames.
|
||||
connect(&visibility_, &VisibilityState::changed, this, [this]() {
|
||||
have_cached_cull_ = false;
|
||||
requestUpdate();
|
||||
});
|
||||
|
||||
gl_->glEnable(GL_DEPTH_TEST);
|
||||
gl_->glEnable(GL_MULTISAMPLE);
|
||||
gl_->glClearColor(0.18f, 0.20f, 0.22f, 1.0f);
|
||||
gl_->glClearColor(background_color_.redF(), background_color_.greenF(), background_color_.blueF(), 1.0f);
|
||||
gl_->glCullFace(GL_BACK);
|
||||
if (AppSettings::instance().backfaceCulling()) gl_->glEnable(GL_CULL_FACE);
|
||||
else gl_->glDisable(GL_CULL_FACE);
|
||||
@@ -711,6 +741,26 @@ void ViewportWindow::initGL() {
|
||||
requestUpdate();
|
||||
});
|
||||
|
||||
// Cull thresholds and HiZ live-toggle: cached cull results assume the
|
||||
// active values, so any change must invalidate them and request a
|
||||
// re-render so the new threshold actually takes effect.
|
||||
auto invalidate_cull = [this]() {
|
||||
if (!gl_initialized_) return;
|
||||
have_cached_cull_ = false;
|
||||
requestUpdate();
|
||||
};
|
||||
connect(&AppSettings::instance(), &AppSettings::minPixelRadiusChanged,
|
||||
this, [invalidate_cull](double){ invalidate_cull(); });
|
||||
connect(&AppSettings::instance(), &AppSettings::motionMinPixelRadiusChanged,
|
||||
this, [invalidate_cull](double){ invalidate_cull(); });
|
||||
connect(&AppSettings::instance(), &AppSettings::lod1PixelThresholdChanged,
|
||||
this, [invalidate_cull](double){ invalidate_cull(); });
|
||||
connect(&AppSettings::instance(), &AppSettings::hizEnabledChanged,
|
||||
this, [this, invalidate_cull](bool){
|
||||
hiz_vp_valid_ = false; // pyramid no longer trusted
|
||||
invalidate_cull();
|
||||
});
|
||||
|
||||
gl_initialized_ = true;
|
||||
flushPendingOperations();
|
||||
requestUpdate();
|
||||
@@ -718,6 +768,18 @@ void ViewportWindow::initGL() {
|
||||
emit initialized();
|
||||
}
|
||||
|
||||
void ViewportWindow::setBackgroundColor(const QColor& color) {
|
||||
if (!color.isValid()) return;
|
||||
const QColor normalized = color.toRgb();
|
||||
if (background_color_ == normalized) return;
|
||||
background_color_ = normalized;
|
||||
if (gl_initialized_ && gl_) {
|
||||
context_->makeCurrent(this);
|
||||
gl_->glClearColor(background_color_.redF(), background_color_.greenF(), background_color_.blueF(), 1.0f);
|
||||
requestUpdate();
|
||||
}
|
||||
}
|
||||
|
||||
void ViewportWindow::enqueuePendingOperation(PendingOperation op) {
|
||||
pending_ops_.push_back(std::move(op));
|
||||
}
|
||||
@@ -1074,6 +1136,9 @@ void ViewportWindow::uploadInstanceChunk(const InstanceChunk& chunk) {
|
||||
|
||||
ModelGpuData& m = getOrCreateModel(chunk.model_id);
|
||||
|
||||
selection_.noteObjectId(chunk.object_id);
|
||||
visibility_.noteObjectId(chunk.object_id);
|
||||
|
||||
InstanceCpu inst;
|
||||
inst.mesh_id = chunk.local_mesh_id;
|
||||
inst.object_id = chunk.object_id;
|
||||
@@ -1235,6 +1300,15 @@ void ViewportWindow::applyCachedModel(uint32_t model_id, SidecarData data) {
|
||||
m.meshes = std::move(data.meshes);
|
||||
m.instances = std::move(data.instances);
|
||||
|
||||
// Sidecar path bypasses uploadInstanceChunk, so register every
|
||||
// instance's object_id with both the selection and visibility
|
||||
// states up front — otherwise their per-object_id flag vectors
|
||||
// would be too small to cover these ids.
|
||||
for (const auto& inst : m.instances) {
|
||||
selection_.noteObjectId(inst.object_id);
|
||||
visibility_.noteObjectId(inst.object_id);
|
||||
}
|
||||
|
||||
uint32_t total_tri = 0;
|
||||
for (const auto& mesh : m.meshes) {
|
||||
total_tri += (mesh.index_count / 3) * mesh.instance_count;
|
||||
@@ -1374,7 +1448,8 @@ void ViewportWindow::resetScene() {
|
||||
if (m.indirect_buffer) gl_->glDeleteBuffers(1, &m.indirect_buffer);
|
||||
}
|
||||
models_gpu_.clear();
|
||||
selected_object_id_ = 0;
|
||||
selection_.reset();
|
||||
visibility_.reset();
|
||||
have_cached_cull_ = false;
|
||||
requestUpdate();
|
||||
}
|
||||
@@ -1436,8 +1511,54 @@ void ViewportWindow::removeModel(uint32_t model_id) {
|
||||
}
|
||||
|
||||
void ViewportWindow::setSelectedObjectId(uint32_t id) {
|
||||
selected_object_id_ = id;
|
||||
requestUpdate();
|
||||
// Convenience pass-through. SelectionState fires `changed` which
|
||||
// re-emits objectPicked + requestUpdate via the lambda hooked up in
|
||||
// initGL, so we don't need to do those manually here.
|
||||
selection_.setSelectedObjectId(id);
|
||||
}
|
||||
|
||||
void ViewportWindow::hideSelectedElements() {
|
||||
if (selection_.empty()) return;
|
||||
visibility_.hideObjects(selection_.selectionIds());
|
||||
}
|
||||
|
||||
void ViewportWindow::isolateSelectedElements() {
|
||||
if (selection_.empty()) return;
|
||||
// Build the new hidden set: every live object_id in a *visible*
|
||||
// model that isn't part of the selection. Skipping model-hidden
|
||||
// objects keeps element-level hidden_ids_ from accumulating ids
|
||||
// that are already model-hidden — model-hide always wins anyway.
|
||||
const auto& sel = selection_.selectionIds();
|
||||
std::unordered_set<uint32_t> new_hidden;
|
||||
new_hidden.reserve(sel.size() * 8);
|
||||
for (const auto& [mid, m] : models_gpu_) {
|
||||
if (m.hidden) continue;
|
||||
for (const InstanceCpu& inst : m.instances) {
|
||||
if (inst.object_id == 0) continue;
|
||||
if (sel.count(inst.object_id) == 0) {
|
||||
new_hidden.insert(inst.object_id);
|
||||
}
|
||||
}
|
||||
}
|
||||
visibility_.setHidden(new_hidden);
|
||||
}
|
||||
|
||||
void ViewportWindow::showAllElements() {
|
||||
visibility_.showAll();
|
||||
}
|
||||
|
||||
void ViewportWindow::invertElementVisibility() {
|
||||
std::unordered_set<uint32_t> inverted;
|
||||
for (const auto& [mid, m] : models_gpu_) {
|
||||
if (m.hidden) continue;
|
||||
for (const InstanceCpu& inst : m.instances) {
|
||||
if (inst.object_id == 0) continue;
|
||||
if (!visibility_.isHidden(inst.object_id)) {
|
||||
inverted.insert(inst.object_id);
|
||||
}
|
||||
}
|
||||
}
|
||||
visibility_.setHidden(inverted);
|
||||
}
|
||||
|
||||
void ViewportWindow::setCamera(float tx, float ty, float tz,
|
||||
@@ -1534,12 +1655,34 @@ void ViewportWindow::frameAabb(const QVector3D& mn, const QVector3D& mx,
|
||||
|
||||
void ViewportWindow::focusOnSelectedObject() {
|
||||
if (camera_mode_ == CameraMode::Fps) return;
|
||||
QVector3D mn, mx;
|
||||
if (!computeObjectAabb(selected_object_id_, mn, mx)) {
|
||||
const auto& sel = selection_.selectionIds();
|
||||
if (sel.empty()) {
|
||||
qDebug("Focus: no object selected or no AABB available");
|
||||
return;
|
||||
}
|
||||
frameAabb(mn, mx, 1.30f); // a bit of headroom around small objects
|
||||
|
||||
bool found = false;
|
||||
QVector3D lo(std::numeric_limits<float>::max(),
|
||||
std::numeric_limits<float>::max(),
|
||||
std::numeric_limits<float>::max());
|
||||
QVector3D hi(-std::numeric_limits<float>::max(),
|
||||
-std::numeric_limits<float>::max(),
|
||||
-std::numeric_limits<float>::max());
|
||||
for (uint32_t object_id : sel) {
|
||||
QVector3D mn, mx;
|
||||
if (!computeObjectAabb(object_id, mn, mx)) continue;
|
||||
for (int a = 0; a < 3; ++a) {
|
||||
if (mn[a] < lo[a]) lo[a] = mn[a];
|
||||
if (mx[a] > hi[a]) hi[a] = mx[a];
|
||||
}
|
||||
found = true;
|
||||
}
|
||||
|
||||
if (!found) {
|
||||
qDebug("Focus: no object selected or no AABB available");
|
||||
return;
|
||||
}
|
||||
frameAabb(lo, hi, 1.30f);
|
||||
}
|
||||
|
||||
void ViewportWindow::viewAll() {
|
||||
@@ -1579,16 +1722,6 @@ ViewportWindow::CameraState ViewportWindow::cameraState() const {
|
||||
void ViewportWindow::keyPressEvent(QKeyEvent* event) {
|
||||
const int key = event->key();
|
||||
|
||||
// Shift+F toggles FPS/fly mode. Checked before auto-repeat filtering so
|
||||
// a held Shift+F doesn't thrash between modes.
|
||||
if (key == Qt::Key_F
|
||||
&& (event->modifiers() & Qt::ShiftModifier)
|
||||
&& !event->isAutoRepeat()) {
|
||||
if (camera_mode_ == CameraMode::Fps) exitFpsMode();
|
||||
else enterFpsMode();
|
||||
return;
|
||||
}
|
||||
|
||||
if (camera_mode_ == CameraMode::Fps) {
|
||||
if (key == Qt::Key_Escape && !event->isAutoRepeat()) {
|
||||
exitFpsMode();
|
||||
@@ -1620,7 +1753,6 @@ void ViewportWindow::keyPressEvent(QKeyEvent* event) {
|
||||
}
|
||||
|
||||
// Plain F (no modifiers): focus camera on the currently selected object.
|
||||
// Shift+F is FPS-mode toggle and was handled above.
|
||||
if (key == Qt::Key_F
|
||||
&& event->modifiers() == Qt::NoModifier
|
||||
&& !event->isAutoRepeat()) {
|
||||
@@ -1658,23 +1790,6 @@ void ViewportWindow::keyPressEvent(QKeyEvent* event) {
|
||||
}
|
||||
return;
|
||||
}
|
||||
// K toggles the section tool.
|
||||
if (key == Qt::Key_K
|
||||
&& event->modifiers() == Qt::NoModifier
|
||||
&& !event->isAutoRepeat()) {
|
||||
toggleSectionTool();
|
||||
return;
|
||||
}
|
||||
// Shift+K clears every section plane (and the selection).
|
||||
if (key == Qt::Key_K
|
||||
&& event->modifiers() == Qt::ShiftModifier
|
||||
&& !event->isAutoRepeat()) {
|
||||
clearSectionPlanes();
|
||||
section_plane_selected_ = -1;
|
||||
section_drag_active_ = false;
|
||||
section_drag_index_ = -1;
|
||||
return;
|
||||
}
|
||||
// While the section tool is active: Esc exits the tool, Delete removes
|
||||
// the selected plane.
|
||||
if (section_tool_active_) {
|
||||
@@ -1690,12 +1805,19 @@ void ViewportWindow::keyPressEvent(QKeyEvent* event) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
// Esc also exits the area tool.
|
||||
if (area_tool_active_
|
||||
&& key == Qt::Key_Escape
|
||||
&& !event->isAutoRepeat()) {
|
||||
toggleAreaTool();
|
||||
return;
|
||||
// Esc exits any active measurement tool. Backspace/Delete in length
|
||||
// mode removes the last point — emitted as a signal because the
|
||||
// library doesn't track per-tool semantic state.
|
||||
if (tool_mode_ != ToolMode::None && !event->isAutoRepeat()) {
|
||||
if (key == Qt::Key_Escape) {
|
||||
setToolMode(ToolMode::None);
|
||||
return;
|
||||
}
|
||||
if (tool_mode_ == ToolMode::Length
|
||||
&& (key == Qt::Key_Backspace || key == Qt::Key_Delete)) {
|
||||
emit toolBackspacePressed();
|
||||
return;
|
||||
}
|
||||
}
|
||||
QWindow::keyPressEvent(event);
|
||||
}
|
||||
@@ -1771,21 +1893,17 @@ void ViewportWindow::fpsIntegrate() {
|
||||
// (walls, slabs) reliably; finer detail doesn't help much because we're
|
||||
// sampling the pyramid at the mip level where the AABB's rect is ~2
|
||||
// texels anyway. Readback cost is ~128 KB/frame ≈ negligible.
|
||||
// IFC_HIZ_SIZE=<N> overrides the width; height tracks aspect.
|
||||
// AppSettings::hizResolution() drives the width; height tracks aspect.
|
||||
// Captured once at first call — changing the setting requires a viewport
|
||||
// reinitialization to take effect (rebuilding the pyramid mid-session
|
||||
// would tangle with the framebuffer state machine for little gain).
|
||||
static int hizBaseWidth() {
|
||||
static const int w = []{
|
||||
const char* e = std::getenv("IFC_HIZ_SIZE");
|
||||
return (e && *e) ? std::max(64, std::atoi(e)) : 256;
|
||||
}();
|
||||
static const int w = std::max(64, AppSettings::instance().hizResolution());
|
||||
return w;
|
||||
}
|
||||
|
||||
static bool hizEnabled() {
|
||||
static const bool disabled = []{
|
||||
const char* e = std::getenv("IFC_NO_HIZ");
|
||||
return e && e[0] == '1';
|
||||
}();
|
||||
return !disabled;
|
||||
return AppSettings::instance().hizEnabled();
|
||||
}
|
||||
|
||||
void ViewportWindow::buildHizPyramid() {
|
||||
@@ -2072,6 +2190,59 @@ uint32_t ViewportWindow::pickObjectAt(int x, int y) {
|
||||
return pixel;
|
||||
}
|
||||
|
||||
std::unordered_set<uint32_t> ViewportWindow::picksInRect(const QRect& rect_logical) {
|
||||
std::unordered_set<uint32_t> out;
|
||||
if (!gl_initialized_) return out;
|
||||
if (rect_logical.isEmpty() || rect_logical.width() < 1 || rect_logical.height() < 1) {
|
||||
return out;
|
||||
}
|
||||
context_->makeCurrent(this);
|
||||
|
||||
const int w = width() * devicePixelRatio();
|
||||
const int h = height() * devicePixelRatio();
|
||||
if (w <= 0 || h <= 0) return out;
|
||||
|
||||
// Reuse the same allocation path as pickObjectAt — call it once at
|
||||
// (0,0) just to ensure the framebuffer matches the current size and
|
||||
// the pick pass has been rendered for this state.
|
||||
pickObjectAt(rect_logical.left(), rect_logical.top());
|
||||
|
||||
// Convert the logical-coords rect to physical pick-tex coords with
|
||||
// GL's bottom-left origin. Clamp to surface to avoid OOB reads.
|
||||
const qreal dpr = devicePixelRatio();
|
||||
QRect r = rect_logical
|
||||
.intersected(QRect(0, 0, width(), height()));
|
||||
if (r.isEmpty()) return out;
|
||||
const int rx = int(r.left() * dpr);
|
||||
const int ry_top = int((height() - (r.top() + r.height())) * dpr);
|
||||
const int rw = std::max(1, int(r.width() * dpr));
|
||||
const int rh = std::max(1, int(r.height() * dpr));
|
||||
if (rx < 0 || ry_top < 0 || rx + rw > w || ry_top + rh > h) {
|
||||
// Final defensive clamp — devicePixelRatio + integer rounding can
|
||||
// push the rect a pixel off the texture; readback would silently
|
||||
// return zeros for those rows.
|
||||
const int cx = std::max(0, std::min(rx, w - 1));
|
||||
const int cy = std::max(0, std::min(ry_top, h - 1));
|
||||
const int cw2 = std::max(1, std::min(rw, w - cx));
|
||||
const int ch2 = std::max(1, std::min(rh, h - cy));
|
||||
std::vector<uint32_t> pixels(size_t(cw2) * size_t(ch2), 0);
|
||||
gl_->glGetTextureSubImage(pick_color_tex_, 0, cx, cy, 0, cw2, ch2, 1,
|
||||
GL_RED_INTEGER, GL_UNSIGNED_INT,
|
||||
GLsizei(pixels.size() * sizeof(uint32_t)),
|
||||
pixels.data());
|
||||
for (uint32_t id : pixels) if (id != 0) out.insert(id);
|
||||
return out;
|
||||
}
|
||||
|
||||
std::vector<uint32_t> pixels(size_t(rw) * size_t(rh), 0);
|
||||
gl_->glGetTextureSubImage(pick_color_tex_, 0, rx, ry_top, 0, rw, rh, 1,
|
||||
GL_RED_INTEGER, GL_UNSIGNED_INT,
|
||||
GLsizei(pixels.size() * sizeof(uint32_t)),
|
||||
pixels.data());
|
||||
for (uint32_t id : pixels) if (id != 0) out.insert(id);
|
||||
return out;
|
||||
}
|
||||
|
||||
bool ViewportWindow::pickSurfaceAt(int x, int y,
|
||||
uint32_t& object_id_out,
|
||||
QVector3D& world_pos_out,
|
||||
@@ -2216,12 +2387,10 @@ void ViewportWindow::cullModelCpu(ModelGpuData& m, const float planes[6][4],
|
||||
phase_timer.restart();
|
||||
|
||||
// LOD1 switches in when projected sphere radius (in pixels) drops below
|
||||
// this threshold. Overridable for tuning. Set to 0 to disable LOD1
|
||||
// entirely (always draw LOD0).
|
||||
static const float lod1_px_threshold = []{
|
||||
const char* e = std::getenv("IFC_LOD1_PX");
|
||||
return (e && *e) ? static_cast<float>(std::atof(e)) : 30.0f;
|
||||
}();
|
||||
// this threshold. AppSettings drives it (default 30). Set to 0 in
|
||||
// settings to disable LOD1 entirely (always draw LOD0).
|
||||
const float lod1_px_threshold =
|
||||
static_cast<float>(AppSettings::instance().lod1PixelThreshold());
|
||||
|
||||
// Bounding-sphere contribution test: approximate an AABB by its enclosing
|
||||
// sphere (centre = midpoint, radius = half-diagonal). Project radius to
|
||||
@@ -2315,9 +2484,13 @@ void ViewportWindow::cullModelCpu(ModelGpuData& m, const float planes[6][4],
|
||||
// the buffer — a self-reinforcing feedback loop). On static views HiZ
|
||||
// kicks in after a single frame of lag.
|
||||
const QMatrix4x4 current_vp = proj_matrix_ * view_matrix_;
|
||||
// Default on: motion-frame HiZ stays trusted across small VP changes.
|
||||
// Env var =0 reverts to the strict gate (previous frame's pyramid is
|
||||
// discarded the moment the view-projection drifts) — useful when
|
||||
// chasing the missing-geometry class of HiZ correctness bug.
|
||||
static const bool hiz_force_motion = []{
|
||||
const char* e = std::getenv("IFC_HIZ_MOTION");
|
||||
return e && *e && std::atoi(e) != 0;
|
||||
return !(e && e[0] == '0');
|
||||
}();
|
||||
const bool hiz_vp_matches = hiz_vp_valid_
|
||||
&& (hiz_force_motion || hiz_vp_ == current_vp);
|
||||
@@ -2338,6 +2511,11 @@ void ViewportWindow::cullModelCpu(ModelGpuData& m, const float planes[6][4],
|
||||
}
|
||||
// Survivor — now pay the wide-struct fetch for mesh_id.
|
||||
const InstanceCpu& inst = m.instances[inst_idx];
|
||||
// Element-level hide check. Done after frustum/contribution/HiZ
|
||||
// so we don't pay the InstanceCpu fetch on instances that would
|
||||
// have been culled anyway; for the typical case (a small
|
||||
// fraction of objects hidden) the wasted cull work is trivial.
|
||||
if (visibility_.isHidden(inst.object_id)) return;
|
||||
if (inst.mesh_id >= m.meshes.size()) return;
|
||||
const MeshInfo& mesh = m.meshes[inst.mesh_id];
|
||||
const bool want_lod1 = mesh.lod1_index_count > 0 &&
|
||||
@@ -2542,15 +2720,11 @@ void ViewportWindow::render() {
|
||||
// culling below.
|
||||
const float focal_px = 0.5f * static_cast<float>(h) /
|
||||
std::tan(qDegreesToRadians(0.5f * camera_fov_y_deg_));
|
||||
static const float base_min_pixel_radius = []{
|
||||
const char* e = std::getenv("IFC_MIN_PX");
|
||||
return (e && *e) ? static_cast<float>(std::atof(e)) : 2.0f;
|
||||
}();
|
||||
static const float motion_min_pixel_radius = []{
|
||||
const char* e = std::getenv("IFC_MIN_PX_MOTION");
|
||||
return (e && *e) ? static_cast<float>(std::atof(e))
|
||||
: 0.0f; // 0 = disabled (no motion boost)
|
||||
}();
|
||||
const float base_min_pixel_radius =
|
||||
static_cast<float>(AppSettings::instance().minPixelRadius());
|
||||
// 0 = disabled (motion uses the same threshold as still frames).
|
||||
const float motion_min_pixel_radius =
|
||||
static_cast<float>(AppSettings::instance().motionMinPixelRadius());
|
||||
|
||||
gl_->glUseProgram(main_program_);
|
||||
GLint u_vp = gl_->glGetUniformLocation(main_program_, "u_view_projection");
|
||||
@@ -2558,7 +2732,7 @@ void ViewportWindow::render() {
|
||||
GLint u_fill = gl_->glGetUniformLocation(main_program_, "u_fill_dir");
|
||||
GLint u_sky = gl_->glGetUniformLocation(main_program_, "u_sky_color");
|
||||
GLint u_ground = gl_->glGetUniformLocation(main_program_, "u_ground_color");
|
||||
GLint u_sel = gl_->glGetUniformLocation(main_program_, "u_selected_id");
|
||||
GLint u_active = gl_->glGetUniformLocation(main_program_, "u_active_id");
|
||||
gl_->glUniformMatrix4fv(u_vp, 1, GL_FALSE, vp.constData());
|
||||
// Key light: high noon-ish from off-camera; fill ~120° away so back-of-
|
||||
// object surfaces still get some direct contribution. Sky/ground tints
|
||||
@@ -2570,7 +2744,8 @@ void ViewportWindow::render() {
|
||||
gl_->glUniform3f(u_fill, -0.3f, -0.5f, 0.8f);
|
||||
gl_->glUniform3f(u_sky, 0.55f, 0.60f, 0.70f);
|
||||
gl_->glUniform3f(u_ground, 0.35f, 0.32f, 0.28f);
|
||||
gl_->glUniform1ui(u_sel, selected_object_id_);
|
||||
gl_->glUniform1ui(u_active, selection_.activeObjectId());
|
||||
selection_.bindForRender(/*binding_index=*/3);
|
||||
uploadClipPlaneUniforms(main_program_);
|
||||
|
||||
visible_triangles_ = 0;
|
||||
@@ -2733,6 +2908,14 @@ void ViewportWindow::render() {
|
||||
// does not restore.
|
||||
{
|
||||
const qreal dpr = devicePixelRatio();
|
||||
// Push the live box-select rect (or empty when not dragging) so
|
||||
// the overlay draws/clears it as part of the per-frame pass.
|
||||
QRect sel_rect;
|
||||
if (box_select_active_) {
|
||||
sel_rect = QRect(box_select_start_pos_,
|
||||
box_select_current_pos_).normalized();
|
||||
}
|
||||
overlay_renderer_.setSelectionRect(sel_rect);
|
||||
overlay_renderer_.render(vp.constData(),
|
||||
int(width() * dpr),
|
||||
int(height() * dpr),
|
||||
@@ -3448,9 +3631,20 @@ void ViewportWindow::handleMousePress(QMouseEvent* e) {
|
||||
}
|
||||
active_button_ = e->button();
|
||||
last_mouse_pos_ = e->pos();
|
||||
if (e->button() == Qt::MiddleButton) {
|
||||
setPivotIndicatorVisible(true);
|
||||
requestUpdate();
|
||||
box_select_armed_ = false;
|
||||
box_select_active_ = false;
|
||||
press_pick_id_ = 0;
|
||||
// Show the pivot indicator while the user holds the navigation
|
||||
// button(s) for the active preset — Blender uses MMB for both
|
||||
// orbit + pan, Revit too (with Shift swapping the role), Rhino
|
||||
// uses RMB. Either way, the press of that button is what the
|
||||
// user means as "I'm about to navigate".
|
||||
{
|
||||
const NavBindings nb = currentNavBindings();
|
||||
if (e->button() == nb.orbit_button || e->button() == nb.pan_button) {
|
||||
setPivotIndicatorVisible(true);
|
||||
requestUpdate();
|
||||
}
|
||||
}
|
||||
if (section_tool_active_ && e->button() == Qt::LeftButton) {
|
||||
// First try to grab an existing plane's arrow gizmo.
|
||||
@@ -3476,6 +3670,28 @@ void ViewportWindow::handleMousePress(QMouseEvent* e) {
|
||||
section_plane_selected_ = -1;
|
||||
requestUpdate();
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// Plain LMB: pick now so release can apply click semantics without
|
||||
// a second pick pass, and arm a potential box-select drag — promoted
|
||||
// to box_select_active_ at the first move past the click threshold.
|
||||
// Arming on every LMB press (not just empty-space presses) means a
|
||||
// drag that happens to start on an object still box-selects, which
|
||||
// matches user intuition: the start point shouldn't disqualify the
|
||||
// gesture. Tool-mode LMB defers pick handling to surfacePickedInTool
|
||||
// on release for tools that consume clicks (Area / Length); Volume is
|
||||
// passive and routes input through normal selection.
|
||||
if (e->button() == Qt::LeftButton) {
|
||||
box_select_start_pos_ = e->pos();
|
||||
box_select_current_pos_ = e->pos();
|
||||
const bool tool_consumes_clicks =
|
||||
(tool_mode_ == ToolMode::Area || tool_mode_ == ToolMode::Length);
|
||||
if (!tool_consumes_clicks) {
|
||||
press_pick_id_ = pickObjectAt(e->pos().x(), e->pos().y());
|
||||
box_select_press_mods_ = e->modifiers();
|
||||
box_select_armed_ = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
void ViewportWindow::handleMouseRelease(QMouseEvent* e) {
|
||||
@@ -3486,23 +3702,63 @@ void ViewportWindow::handleMouseRelease(QMouseEvent* e) {
|
||||
active_button_ = Qt::NoButton;
|
||||
return;
|
||||
}
|
||||
// LMB pick is suppressed in section-tool mode — LMB there creates or
|
||||
// selects planes (handled in handleMousePress) and the release should
|
||||
// not also trigger object selection.
|
||||
if (active_button_ == Qt::LeftButton
|
||||
&& !section_tool_active_
|
||||
&& (e->pos() - last_mouse_pos_).manhattanLength() < 5) {
|
||||
if (area_tool_active_) {
|
||||
emit surfacePickedInTool(e->pos().x(), e->pos().y(),
|
||||
int(e->modifiers()));
|
||||
} else {
|
||||
uint32_t id = pickObjectAt(e->pos().x(), e->pos().y());
|
||||
selected_object_id_ = id;
|
||||
emit objectPicked(id);
|
||||
requestUpdate(); // selection highlight changed
|
||||
|
||||
if (active_button_ == Qt::LeftButton && !section_tool_active_) {
|
||||
const bool was_drag =
|
||||
(e->pos() - box_select_start_pos_).manhattanLength() >= 5;
|
||||
|
||||
if (box_select_active_) {
|
||||
// Finalize a box-select. Modifier captured at press-time
|
||||
// decides commit semantics: plain → replace, Shift → add,
|
||||
// Ctrl → remove. Box-select never sets the active id; the
|
||||
// last single-clicked object remains the active.
|
||||
const QRect rect = QRect(box_select_start_pos_,
|
||||
e->pos()).normalized();
|
||||
const auto picks = picksInRect(rect);
|
||||
const auto mods = box_select_press_mods_;
|
||||
if (mods & Qt::ShiftModifier) {
|
||||
selection_.addToSelection(picks);
|
||||
} else if (mods & Qt::ControlModifier) {
|
||||
selection_.removeFromSelection(picks);
|
||||
} else {
|
||||
// Replace. Active stays only if the new set still
|
||||
// contains it; SelectionState handles the coercion.
|
||||
selection_.setSelection(picks, selection_.activeObjectId());
|
||||
}
|
||||
} else if (!was_drag) {
|
||||
// Click — apply press-time pick + modifiers. Area / Length
|
||||
// intercept clicks; None and Volume drive normal selection.
|
||||
const bool tool_consumes_clicks =
|
||||
(tool_mode_ == ToolMode::Area || tool_mode_ == ToolMode::Length);
|
||||
if (tool_consumes_clicks) {
|
||||
emit surfacePickedInTool(e->pos().x(), e->pos().y(),
|
||||
int(e->modifiers()));
|
||||
} else {
|
||||
const auto mods = e->modifiers();
|
||||
if (mods & (Qt::ShiftModifier | Qt::ControlModifier)) {
|
||||
if (press_pick_id_ != 0) {
|
||||
// Toggle. No-op on empty space so a stray
|
||||
// modifier-click doesn't blow away the set.
|
||||
selection_.toggleInSelection(press_pick_id_);
|
||||
}
|
||||
} else {
|
||||
// Plain click — replace (or clear, for empty hit).
|
||||
selection_.setSelectedObjectId(press_pick_id_);
|
||||
}
|
||||
}
|
||||
}
|
||||
// Drag from an object (no box select armed, movement > threshold)
|
||||
// is intentionally a no-op — keeps stray drags from rewriting
|
||||
// the selection.
|
||||
|
||||
box_select_armed_ = false;
|
||||
box_select_active_ = false;
|
||||
press_pick_id_ = 0;
|
||||
}
|
||||
const bool was_navigating = (active_button_ == Qt::MiddleButton);
|
||||
|
||||
const NavBindings nb = currentNavBindings();
|
||||
const bool was_navigating =
|
||||
(active_button_ == nb.orbit_button || active_button_ == nb.pan_button);
|
||||
active_button_ = Qt::NoButton;
|
||||
if (was_navigating && pivot_indicator_visible_) {
|
||||
setPivotIndicatorVisible(false);
|
||||
@@ -3548,28 +3804,59 @@ void ViewportWindow::handleMouseMove(QMouseEvent* e) {
|
||||
last_mouse_pos_ = e->pos();
|
||||
return;
|
||||
}
|
||||
|
||||
// Promote an armed box-select to active once the cursor crosses the
|
||||
// 5-px click threshold, then keep updating the rect for the overlay.
|
||||
// last_mouse_pos_ stays at press position while armed so a fast tiny
|
||||
// drag isn't classified as a click on release.
|
||||
if (box_select_armed_ && active_button_ == Qt::LeftButton) {
|
||||
if (!box_select_active_
|
||||
&& (e->pos() - box_select_start_pos_).manhattanLength() >= 5) {
|
||||
box_select_active_ = true;
|
||||
}
|
||||
if (box_select_active_) {
|
||||
box_select_current_pos_ = e->pos();
|
||||
requestUpdate();
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
QPoint delta = e->pos() - last_mouse_pos_;
|
||||
last_mouse_pos_ = e->pos();
|
||||
if (active_button_ == Qt::MiddleButton) {
|
||||
if (e->modifiers() & Qt::ShiftModifier) {
|
||||
const float pan_speed = camera_distance_ * 0.002f;
|
||||
// Derive screen-right and screen-up from the actual camera basis
|
||||
// rather than yaw/pitch alone — the latter assumed up = world +Z,
|
||||
// which breaks at top/bottom views where updateCamera switches
|
||||
// the lookAt up vector to world +Y.
|
||||
const QVector3D forward = (camera_target_ - camera_eye_).normalized();
|
||||
const QVector3D up_ref = (std::abs(camera_pitch_) >= 89.0f)
|
||||
? QVector3D(0, 1, 0)
|
||||
: QVector3D(0, 0, 1);
|
||||
const QVector3D right = QVector3D::crossProduct(forward, up_ref).normalized();
|
||||
const QVector3D up = QVector3D::crossProduct(right, forward).normalized();
|
||||
camera_target_ -= right * delta.x() * pan_speed;
|
||||
camera_target_ += up * delta.y() * pan_speed;
|
||||
} else {
|
||||
camera_yaw_ -= delta.x() * 0.3f;
|
||||
camera_pitch_ += delta.y() * 0.3f;
|
||||
camera_pitch_ = qBound(-89.0f, camera_pitch_, 89.0f);
|
||||
}
|
||||
|
||||
// Dispatch nav drag against the active preset. Modifier matching
|
||||
// is exact on the Shift bit only — the only modifier any of our
|
||||
// presets uses — so toggling Shift mid-drag flips between orbit
|
||||
// and pan when both share a button (Blender / Revit).
|
||||
const NavBindings nb = currentNavBindings();
|
||||
const bool shift_now = (e->modifiers() & Qt::ShiftModifier) != 0;
|
||||
const bool shift_for_pan = (nb.pan_mods & Qt::ShiftModifier) != 0;
|
||||
const bool shift_for_orb = (nb.orbit_mods & Qt::ShiftModifier) != 0;
|
||||
const bool is_pan = (active_button_ == nb.pan_button)
|
||||
&& (shift_now == shift_for_pan);
|
||||
const bool is_orbit = !is_pan
|
||||
&& (active_button_ == nb.orbit_button)
|
||||
&& (shift_now == shift_for_orb);
|
||||
|
||||
if (is_pan) {
|
||||
const float pan_speed = camera_distance_ * 0.002f;
|
||||
// Derive screen-right and screen-up from the actual camera basis
|
||||
// rather than yaw/pitch alone — the latter assumed up = world +Z,
|
||||
// which breaks at top/bottom views where updateCamera switches
|
||||
// the lookAt up vector to world +Y.
|
||||
const QVector3D forward = (camera_target_ - camera_eye_).normalized();
|
||||
const QVector3D up_ref = (std::abs(camera_pitch_) >= 89.0f)
|
||||
? QVector3D(0, 1, 0)
|
||||
: QVector3D(0, 0, 1);
|
||||
const QVector3D right = QVector3D::crossProduct(forward, up_ref).normalized();
|
||||
const QVector3D up = QVector3D::crossProduct(right, forward).normalized();
|
||||
camera_target_ -= right * delta.x() * pan_speed;
|
||||
camera_target_ += up * delta.y() * pan_speed;
|
||||
requestUpdate();
|
||||
} else if (is_orbit) {
|
||||
camera_yaw_ -= delta.x() * 0.3f;
|
||||
camera_pitch_ += delta.y() * 0.3f;
|
||||
camera_pitch_ = qBound(-89.0f, camera_pitch_, 89.0f);
|
||||
requestUpdate();
|
||||
}
|
||||
}
|
||||
@@ -3690,7 +3977,8 @@ void ViewportWindow::setModelTransformation(uint32_t model_id,
|
||||
}
|
||||
|
||||
void ViewportWindow::printSelectedObjectCoords() {
|
||||
if (selected_object_id_ == 0) {
|
||||
const uint32_t target = selection_.activeObjectId();
|
||||
if (target == 0) {
|
||||
qInfo("printSelectedObjectCoords: no object selected");
|
||||
return;
|
||||
}
|
||||
@@ -3703,7 +3991,7 @@ void ViewportWindow::printSelectedObjectCoords() {
|
||||
const ModelGpuData& m = kv.second;
|
||||
for (size_t i = 0; i < m.instances.size(); ++i) {
|
||||
const InstanceCpu& inst = m.instances[i];
|
||||
if (inst.object_id != selected_object_id_) continue;
|
||||
if (inst.object_id != target) continue;
|
||||
|
||||
qInfo("Selected object %u (model %u, mesh %u, instance %zu):",
|
||||
inst.object_id, inst.model_id, inst.mesh_id, i);
|
||||
@@ -3769,7 +4057,7 @@ void ViewportWindow::printSelectedObjectCoords() {
|
||||
}
|
||||
}
|
||||
qInfo("printSelectedObjectCoords: object_id %u not found in any model",
|
||||
selected_object_id_);
|
||||
target);
|
||||
}
|
||||
|
||||
bool ViewportWindow::readbackMeshTriangles(uint32_t model_id, uint32_t mesh_id,
|
||||
@@ -3868,9 +4156,204 @@ bool ViewportWindow::pickMeshLocalAt(int x, int y, MeshLocalPick& out) {
|
||||
return false;
|
||||
}
|
||||
|
||||
void ViewportWindow::setToolMode(ToolMode mode) {
|
||||
if (tool_mode_ == mode) return;
|
||||
tool_mode_ = mode;
|
||||
emit toolModeChanged(mode);
|
||||
}
|
||||
|
||||
namespace {
|
||||
|
||||
// Slab method ray-AABB. inv_d is precomputed 1/dir per axis.
|
||||
bool rayAabb(const float ro[3], const float inv_d[3],
|
||||
const float bmin[3], const float bmax[3]) {
|
||||
float tmin = 0.0f, tmax = std::numeric_limits<float>::infinity();
|
||||
for (int i = 0; i < 3; ++i) {
|
||||
float t1 = (bmin[i] - ro[i]) * inv_d[i];
|
||||
float t2 = (bmax[i] - ro[i]) * inv_d[i];
|
||||
tmin = std::max(tmin, std::min(t1, t2));
|
||||
tmax = std::min(tmax, std::max(t1, t2));
|
||||
}
|
||||
return tmax >= tmin && tmax >= 0.0f;
|
||||
}
|
||||
|
||||
// Möller-Trumbore. Returns true on hit; t is in dir-units.
|
||||
bool rayTri(const float ro[3], const float rd[3],
|
||||
const float v0[3], const float v1[3], const float v2[3],
|
||||
float& t_out) {
|
||||
constexpr float EPS = 1e-7f;
|
||||
float e1[3] = {v1[0]-v0[0], v1[1]-v0[1], v1[2]-v0[2]};
|
||||
float e2[3] = {v2[0]-v0[0], v2[1]-v0[1], v2[2]-v0[2]};
|
||||
float h[3] = {
|
||||
rd[1]*e2[2] - rd[2]*e2[1],
|
||||
rd[2]*e2[0] - rd[0]*e2[2],
|
||||
rd[0]*e2[1] - rd[1]*e2[0]
|
||||
};
|
||||
float a = e1[0]*h[0] + e1[1]*h[1] + e1[2]*h[2];
|
||||
if (a > -EPS && a < EPS) return false;
|
||||
float f = 1.0f / a;
|
||||
float s[3] = {ro[0]-v0[0], ro[1]-v0[1], ro[2]-v0[2]};
|
||||
float u = f * (s[0]*h[0] + s[1]*h[1] + s[2]*h[2]);
|
||||
if (u < 0.0f || u > 1.0f) return false;
|
||||
float q[3] = {
|
||||
s[1]*e1[2] - s[2]*e1[1],
|
||||
s[2]*e1[0] - s[0]*e1[2],
|
||||
s[0]*e1[1] - s[1]*e1[0]
|
||||
};
|
||||
float v = f * (rd[0]*q[0] + rd[1]*q[1] + rd[2]*q[2]);
|
||||
if (v < 0.0f || u + v > 1.0f) return false;
|
||||
float t = f * (e2[0]*q[0] + e2[1]*q[1] + e2[2]*q[2]);
|
||||
if (t > EPS) { t_out = t; return true; }
|
||||
return false;
|
||||
}
|
||||
|
||||
// Stack-based BVH walk — collects the item indices of every leaf whose
|
||||
// AABB the ray intersects. Caller filters down to actual triangle hits.
|
||||
void bvhCollectRayCandidates(const ModelBvh& bvh,
|
||||
const float ro[3], const float inv_d[3],
|
||||
std::vector<uint32_t>& out) {
|
||||
if (bvh.nodes.empty()) return;
|
||||
std::vector<uint32_t> stack;
|
||||
stack.reserve(64);
|
||||
stack.push_back(0);
|
||||
while (!stack.empty()) {
|
||||
uint32_t idx = stack.back(); stack.pop_back();
|
||||
const BvhNode& node = bvh.nodes[idx];
|
||||
if (!rayAabb(ro, inv_d, node.aabb_min, node.aabb_max)) continue;
|
||||
if (node.count > 0) {
|
||||
for (uint32_t i = 0; i < node.count; ++i) {
|
||||
out.push_back(bvh.item_indices[node.right_or_first + i]);
|
||||
}
|
||||
} else {
|
||||
stack.push_back(idx + 1); // left child
|
||||
stack.push_back(node.right_or_first); // right child
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
bool ViewportWindow::raycast(const float origin[3], const float dir[3],
|
||||
RaycastHit& out) {
|
||||
if (!gl_initialized_) return false;
|
||||
|
||||
float inv_d[3];
|
||||
for (int i = 0; i < 3; ++i) {
|
||||
inv_d[i] = (std::abs(dir[i]) > 1e-12f)
|
||||
? 1.0f / dir[i] : std::numeric_limits<float>::infinity();
|
||||
}
|
||||
|
||||
// Local mesh-triangle cache shared across candidate instances of the
|
||||
// same mesh in this single call. Avoids re-stalling the GL pipeline
|
||||
// for repeated readbacks of the same VBO/EBO range.
|
||||
std::unordered_map<uint64_t, MeshTriangles> mesh_cache;
|
||||
|
||||
float closest_t = std::numeric_limits<float>::infinity();
|
||||
bool any_hit = false;
|
||||
using Mat4f = Eigen::Matrix<float, 4, 4, Eigen::ColMajor>;
|
||||
|
||||
for (auto& kv : models_gpu_) {
|
||||
ModelGpuData& m = kv.second;
|
||||
if (!m.finalized || m.bvh.nodes.empty()) continue;
|
||||
|
||||
std::vector<uint32_t> candidates;
|
||||
bvhCollectRayCandidates(m.bvh, origin, inv_d, candidates);
|
||||
if (candidates.empty()) continue;
|
||||
|
||||
for (uint32_t inst_idx : candidates) {
|
||||
if (inst_idx >= m.instances.size()) continue;
|
||||
const InstanceCpu& inst = m.instances[inst_idx];
|
||||
|
||||
// Transform ray into this instance's mesh-local space. The
|
||||
// returned t parameter is identical in world and mesh-local
|
||||
// (both are along the same parametric line) so we don't need
|
||||
// to convert it back — assumes caller passed a unit world dir.
|
||||
const Eigen::Matrix4f T = Eigen::Map<const Mat4f>(inst.transform);
|
||||
const Eigen::Matrix4f Ti = T.inverse();
|
||||
const Eigen::Vector4f wo(origin[0], origin[1], origin[2], 1.0f);
|
||||
const Eigen::Vector4f wd(dir[0], dir[1], dir[2], 0.0f);
|
||||
const Eigen::Vector4f lo = Ti * wo;
|
||||
const Eigen::Vector4f ld = Ti * wd;
|
||||
const float ro_l[3] = {lo.x(), lo.y(), lo.z()};
|
||||
const float rd_l[3] = {ld.x(), ld.y(), ld.z()};
|
||||
|
||||
const uint64_t key = (uint64_t(inst.model_id) << 32) | inst.mesh_id;
|
||||
auto it = mesh_cache.find(key);
|
||||
if (it == mesh_cache.end()) {
|
||||
MeshTriangles tris;
|
||||
if (!readbackMeshTriangles(inst.model_id, inst.mesh_id, tris)) continue;
|
||||
it = mesh_cache.emplace(key, std::move(tris)).first;
|
||||
}
|
||||
const MeshTriangles& tris = it->second;
|
||||
|
||||
// Möller-Trumbore against every triangle. No per-mesh BVH
|
||||
// here yet — buildings rarely have meshes with > a few
|
||||
// thousand tris; if this becomes a hotspot we can add one.
|
||||
for (size_t i = 0; i + 2 < tris.indices.size(); i += 3) {
|
||||
const uint32_t ia = tris.indices[i + 0];
|
||||
const uint32_t ib = tris.indices[i + 1];
|
||||
const uint32_t ic = tris.indices[i + 2];
|
||||
const float* a = &tris.positions[3 * ia];
|
||||
const float* b = &tris.positions[3 * ib];
|
||||
const float* c = &tris.positions[3 * ic];
|
||||
float t;
|
||||
if (!rayTri(ro_l, rd_l, a, b, c, t)) continue;
|
||||
if (t >= closest_t) continue;
|
||||
closest_t = t;
|
||||
any_hit = true;
|
||||
out.object_id = inst.object_id;
|
||||
out.distance = t;
|
||||
out.world_pos[0] = origin[0] + t * dir[0];
|
||||
out.world_pos[1] = origin[1] + t * dir[1];
|
||||
out.world_pos[2] = origin[2] + t * dir[2];
|
||||
// World normal: triangle normal in mesh-local, transformed
|
||||
// by inverse-transpose of the 3x3.
|
||||
const float e1x = b[0]-a[0], e1y = b[1]-a[1], e1z = b[2]-a[2];
|
||||
const float e2x = c[0]-a[0], e2y = c[1]-a[1], e2z = c[2]-a[2];
|
||||
Eigen::Vector3f n_local(
|
||||
e1y*e2z - e1z*e2y,
|
||||
e1z*e2x - e1x*e2z,
|
||||
e1x*e2y - e1y*e2x);
|
||||
Eigen::Matrix3f N = T.block<3, 3>(0, 0).inverse().transpose();
|
||||
Eigen::Vector3f world_n = (N * n_local).normalized();
|
||||
out.world_normal[0] = world_n.x();
|
||||
out.world_normal[1] = world_n.y();
|
||||
out.world_normal[2] = world_n.z();
|
||||
}
|
||||
}
|
||||
}
|
||||
return any_hit;
|
||||
}
|
||||
|
||||
void ViewportWindow::toggleAreaTool() {
|
||||
area_tool_active_ = !area_tool_active_;
|
||||
emit areaToolToggled(area_tool_active_);
|
||||
setToolMode(tool_mode_ == ToolMode::Area ? ToolMode::None : ToolMode::Area);
|
||||
}
|
||||
|
||||
void ViewportWindow::toggleLengthTool() {
|
||||
setToolMode(tool_mode_ == ToolMode::Length ? ToolMode::None : ToolMode::Length);
|
||||
}
|
||||
|
||||
void ViewportWindow::toggleVolumeTool() {
|
||||
setToolMode(tool_mode_ == ToolMode::Volume ? ToolMode::None : ToolMode::Volume);
|
||||
}
|
||||
|
||||
ViewportWindow::NavBindings ViewportWindow::currentNavBindings() const {
|
||||
using NP = AppSettings::NavPreset;
|
||||
switch (AppSettings::instance().navPreset()) {
|
||||
case NP::Rhino:
|
||||
// Orbit RMB, Pan Shift+RMB.
|
||||
return { Qt::RightButton, Qt::NoModifier,
|
||||
Qt::RightButton, Qt::ShiftModifier };
|
||||
case NP::Revit:
|
||||
// Orbit Shift+MMB, Pan MMB.
|
||||
return { Qt::MiddleButton, Qt::ShiftModifier,
|
||||
Qt::MiddleButton, Qt::NoModifier };
|
||||
case NP::Blender:
|
||||
default:
|
||||
// Orbit MMB, Pan Shift+MMB.
|
||||
return { Qt::MiddleButton, Qt::NoModifier,
|
||||
Qt::MiddleButton, Qt::ShiftModifier };
|
||||
}
|
||||
}
|
||||
|
||||
void ViewportWindow::setHighlightTriangles(const std::vector<float>& world_xyz,
|
||||
@@ -3881,6 +4364,30 @@ void ViewportWindow::setHighlightTriangles(const std::vector<float>& world_xyz,
|
||||
requestUpdate();
|
||||
}
|
||||
|
||||
void ViewportWindow::setOverlayLines(const std::vector<OverlayRenderer::LineGroup>& groups) {
|
||||
if (!gl_initialized_) return;
|
||||
context_->makeCurrent(this);
|
||||
overlay_renderer_.setOverlayLines(groups);
|
||||
requestUpdate();
|
||||
}
|
||||
|
||||
void ViewportWindow::setOverlayPoints(const std::vector<float>& world_xyz,
|
||||
float r, float g, float b, float a,
|
||||
float pixel_size,
|
||||
float sr, float sg, float sb, float sa,
|
||||
float stroke_extra) {
|
||||
if (!gl_initialized_) return;
|
||||
context_->makeCurrent(this);
|
||||
overlay_renderer_.setOverlayPoints(world_xyz, r, g, b, a, pixel_size,
|
||||
sr, sg, sb, sa, stroke_extra);
|
||||
requestUpdate();
|
||||
}
|
||||
|
||||
void ViewportWindow::setOverlayLabels(const std::vector<OverlayRenderer::Label>& labels) {
|
||||
overlay_renderer_.setOverlayLabels(labels);
|
||||
requestUpdate();
|
||||
}
|
||||
|
||||
void ViewportWindow::setHudText(const QString& text) {
|
||||
overlay_renderer_.setHudText(text);
|
||||
requestUpdate();
|
||||
|
||||
+152
-29
@@ -23,6 +23,7 @@
|
||||
#include <QWindow>
|
||||
#include <QOpenGLContext>
|
||||
#include <QtOpenGL/QOpenGLFunctions_4_5_Core>
|
||||
#include <QColor>
|
||||
#include <QElapsedTimer>
|
||||
#include <QMatrix4x4>
|
||||
#include <QVector3D>
|
||||
@@ -34,6 +35,7 @@ QT_END_NAMESPACE
|
||||
|
||||
#include <vector>
|
||||
#include <unordered_map>
|
||||
#include <unordered_set>
|
||||
#include <cstdint>
|
||||
#include <mutex>
|
||||
#include <memory>
|
||||
@@ -46,7 +48,9 @@ QT_END_NAMESPACE
|
||||
#include "BvhAccel.h"
|
||||
#include "InstancedGeometry.h"
|
||||
#include "OverlayRenderer.h"
|
||||
#include "Selection.h"
|
||||
#include "SidecarCache.h"
|
||||
#include "Visibility.h"
|
||||
|
||||
// Matches GL_DRAW_INDIRECT_BUFFER layout for glMultiDrawElementsIndirect.
|
||||
struct DrawElementsIndirectCommand {
|
||||
@@ -230,12 +234,40 @@ public:
|
||||
};
|
||||
bool pickMeshLocalAt(int x, int y, MeshLocalPick& out);
|
||||
|
||||
// Area tool: while active, LMB clicks emit surfacePickedInTool with
|
||||
// the click coordinates instead of swapping object selection — the
|
||||
// app interprets them (typically by calling pickMeshLocalAt and
|
||||
// accumulating triangle area). Esc exits.
|
||||
// CPU raycast against the per-model BVHs. Walks the BVH for each
|
||||
// finalised model, transforms the world ray into mesh-local space
|
||||
// for each candidate instance, reads back its triangles, and runs
|
||||
// Möller-Trumbore against them. Returns the closest hit overall.
|
||||
//
|
||||
// `dir` MUST be a unit vector — distance is reported as the t value
|
||||
// along the ray, which equals world distance only when |dir|=1.
|
||||
// Stalls the GL pipeline once per unique (model, mesh) candidate
|
||||
// because triangle data is read back lazily; budget ~1ms for
|
||||
// typical BIM scenes.
|
||||
struct RaycastHit {
|
||||
uint32_t object_id = 0;
|
||||
float distance = 0.0f;
|
||||
float world_pos[3] = {0, 0, 0};
|
||||
float world_normal[3]= {0, 0, 0};
|
||||
};
|
||||
bool raycast(const float origin[3], const float dir[3], RaycastHit& out);
|
||||
|
||||
// Measurement tool modes. While any tool is active, LMB clicks emit
|
||||
// surfacePickedInTool with the click coordinates (instead of swapping
|
||||
// object selection); the app interprets them per-tool. Esc exits the
|
||||
// active tool. Backspace/Delete in length mode emits
|
||||
// toolBackspacePressed for "remove last point" semantics.
|
||||
// Area / Length consume LMB clicks via surfacePickedInTool; Volume
|
||||
// is passive — selection behaves as in None and the host just gates
|
||||
// its volume HUD/labels on this mode.
|
||||
enum class ToolMode { None, Area, Length, Volume };
|
||||
Q_ENUM(ToolMode)
|
||||
|
||||
void toggleAreaTool();
|
||||
bool areaToolActive() const { return area_tool_active_; }
|
||||
void toggleLengthTool();
|
||||
void toggleVolumeTool();
|
||||
void setToolMode(ToolMode mode);
|
||||
ToolMode toolMode() const { return tool_mode_; }
|
||||
|
||||
// Replace the overlay highlight-triangle list rendered after the main
|
||||
// pass. `world_xyz` is 3 floats per vertex, 3 verts per triangle, in
|
||||
@@ -244,10 +276,30 @@ public:
|
||||
void setHighlightTriangles(const std::vector<float>& world_xyz,
|
||||
float r, float g, float b, float a);
|
||||
|
||||
// Top-left HUD text drawn via QPainter on top of the GL surface at
|
||||
// the end of each frame. Empty hides the HUD. Used today by the
|
||||
// area-measurement tool for the running total; later tools can pile
|
||||
// additional readouts in by extending this with a multi-line API.
|
||||
// Replace the overlay line groups. Each group has its own segments
|
||||
// + style (color/halo/width/dash) — see OverlayRenderer::LineGroup.
|
||||
// Empty disables every line.
|
||||
void setOverlayLines(const std::vector<OverlayRenderer::LineGroup>& groups);
|
||||
|
||||
// Replace the overlay-point list (3 floats per point, world space).
|
||||
// pixel_size is the inner-disc diameter in physical pixels; when
|
||||
// stroke_a > 0 the sprite is enlarged by 2*stroke_extra to draw a
|
||||
// halo around the inner disc — proper outlined points via the
|
||||
// sprite shader (no two-pass glPointSize trickery).
|
||||
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);
|
||||
|
||||
// Replace the world-anchored label list — projected to screen space
|
||||
// and drawn via QPainter inside the same overlay pass as the HUD.
|
||||
// Used today for per-segment length readouts.
|
||||
void setOverlayLabels(const std::vector<OverlayRenderer::Label>& labels);
|
||||
|
||||
// Multi-line HUD text drawn via QPainter on top of the GL surface at
|
||||
// the end of each frame. Empty hides the HUD. Newlines split the
|
||||
// string into separate rows in the same translucent box.
|
||||
void setHudText(const QString& text);
|
||||
|
||||
// Federation pipeline: composed instance transform =
|
||||
@@ -261,9 +313,47 @@ public:
|
||||
void setFederatedFalseOrigin(const Eigen::Matrix4d& matrix_meters);
|
||||
void setModelCoordinateOperation(uint32_t model_id, const Eigen::Matrix4d& matrix_meters);
|
||||
void setModelTransformation(uint32_t model_id, const Eigen::Matrix4d& matrix_meters);
|
||||
void setBackgroundColor(const QColor& color);
|
||||
void enterFpsMode();
|
||||
|
||||
// Selection. The viewport owns a SelectionState that tracks both
|
||||
// the multi-set and the "active" (last single-clicked) id. External
|
||||
// callers (MainWindow / tree sync) drive selection through it.
|
||||
SelectionState& selection() { return selection_; }
|
||||
const SelectionState& selection() const { return selection_; }
|
||||
// Convenience: replace the selection with {id} (or clear if id==0).
|
||||
// Used by callers that have not yet been ported off the single-id API.
|
||||
void setSelectedObjectId(uint32_t id);
|
||||
|
||||
uint32_t pickObjectAt(int x, int y);
|
||||
// Render the pick pass and collect every distinct non-zero object_id
|
||||
// covered by the pixels inside `rect` (logical coords, top-left origin).
|
||||
// Used by the box-select drag. Returns an empty set if rect is empty
|
||||
// or off-surface.
|
||||
std::unordered_set<uint32_t> picksInRect(const QRect& rect);
|
||||
|
||||
// Per-element visibility (independent of model-level hidden flag).
|
||||
// External callers go through the convenience verbs below; the
|
||||
// VisibilityState getter is exposed for read access (e.g. the host
|
||||
// mirroring its state into a tree's grey-out style).
|
||||
VisibilityState& visibility() { return visibility_; }
|
||||
const VisibilityState& visibility() const { return visibility_; }
|
||||
// Hide every currently-selected element. Selection itself is
|
||||
// preserved — toggling them back on with showAllElements() leaves
|
||||
// the same items selected.
|
||||
void hideSelectedElements();
|
||||
// Hide every element NOT in the current selection (limited to
|
||||
// visible models — model-hidden objects are left as-is so they
|
||||
// stay model-hidden rather than picking up a redundant
|
||||
// element-hidden flag too). No-op when nothing is selected.
|
||||
void isolateSelectedElements();
|
||||
// Clear element-level visibility overrides for every loaded model.
|
||||
// Model-level `hidden` flags are not touched — a hidden model stays
|
||||
// hidden, matching the user's expectation of "show all *elements*".
|
||||
void showAllElements();
|
||||
// Flip element-level visibility across every live element in visible
|
||||
// models. Model-hidden objects are skipped and remain model-hidden.
|
||||
void invertElementVisibility();
|
||||
|
||||
// Extended pick: returns the object id, world-space hit point, and
|
||||
// world-space surface normal at (x, y). Renders the same pick pass as
|
||||
@@ -312,14 +402,17 @@ public:
|
||||
void setBenchmarkFrames(int n);
|
||||
QString cameraString() const;
|
||||
|
||||
// Move camera_target_ to the selected object's world-AABB centroid and
|
||||
// dolly camera_distance_ so the object's bounding sphere fits the
|
||||
// current viewport. Yaw/pitch are preserved. No-op if no object is
|
||||
// selected or its AABB is unknown.
|
||||
// Move camera_target_ to the selected set's world-AABB centroid and
|
||||
// dolly camera_distance_ so the union fits the current viewport.
|
||||
// Yaw/pitch are preserved. No-op if no selected object has an AABB.
|
||||
void focusOnSelectedObject();
|
||||
// Frame the union of all finalized models. No-op if the scene is empty.
|
||||
void viewAll();
|
||||
|
||||
// World-space AABB query. Returns false when the object has no live
|
||||
// instance or no mesh AABB yet (caller should treat as "unknown").
|
||||
bool computeObjectAabb(uint32_t object_id, QVector3D& mn, QVector3D& mx) const;
|
||||
|
||||
struct CameraState {
|
||||
QVector3D target;
|
||||
float distance;
|
||||
@@ -344,14 +437,17 @@ signals:
|
||||
void objectPicked(uint32_t object_id);
|
||||
void initialized();
|
||||
void frameStatsUpdated(const ViewportWindow::FrameStats& stats);
|
||||
// Emitted instead of objectPicked when the area tool is active. The
|
||||
// app is expected to call pickMeshLocalAt(x, y, ...) and accumulate.
|
||||
// modifiers carries the Qt::KeyboardModifiers held at click time so
|
||||
// the app can branch on Alt etc.
|
||||
// Emitted instead of objectPicked when any measurement tool is active.
|
||||
// The app branches on toolMode() to decide what to do, calls
|
||||
// pickMeshLocalAt(x, y, ...) for hit details, and accumulates.
|
||||
// modifiers carries Qt::KeyboardModifiers at click time (Alt etc.).
|
||||
void surfacePickedInTool(int x, int y, int modifiers);
|
||||
// Emitted whenever toggleAreaTool flips the mode. The app uses this
|
||||
// to reset accumulator state on entry/exit.
|
||||
void areaToolToggled(bool active);
|
||||
// Emitted whenever the active tool changes (incl. on→off transitions).
|
||||
// The app uses this to reset accumulator state on entry/exit.
|
||||
void toolModeChanged(ViewportWindow::ToolMode mode);
|
||||
// Backspace/Delete pressed while a tool is active. Used by the length
|
||||
// tool to remove the last point; other tools may ignore it.
|
||||
void toolBackspacePressed();
|
||||
|
||||
protected:
|
||||
void exposeEvent(QExposeEvent* event) override;
|
||||
@@ -361,6 +457,17 @@ protected:
|
||||
bool event(QEvent* event) override;
|
||||
|
||||
private:
|
||||
// Resolved mouse-binding for the active AppSettings::NavPreset.
|
||||
// Selection always stays on LMB; only orbit and pan move around
|
||||
// (presets are picked so neither lands on plain LMB).
|
||||
struct NavBindings {
|
||||
Qt::MouseButton orbit_button;
|
||||
Qt::KeyboardModifiers orbit_mods;
|
||||
Qt::MouseButton pan_button;
|
||||
Qt::KeyboardModifiers pan_mods;
|
||||
};
|
||||
NavBindings currentNavBindings() const;
|
||||
|
||||
enum class PendingOpType {
|
||||
UploadMeshChunk,
|
||||
UploadInstanceChunk,
|
||||
@@ -404,10 +511,8 @@ private:
|
||||
void updateSectionDrag(int x, int y);
|
||||
void updateCamera();
|
||||
|
||||
// Geometry queries used by focusOnSelectedObject() / viewAll(). Both
|
||||
// return false when nothing matched (caller should leave the camera
|
||||
// alone). Bounds are world-space AABBs.
|
||||
bool computeObjectAabb(uint32_t object_id, QVector3D& mn, QVector3D& mx) const;
|
||||
// Scene-wide AABB used by viewAll(). Returns false when the scene
|
||||
// has no finalized geometry (caller should leave the camera alone).
|
||||
bool computeSceneAabb(QVector3D& mn, QVector3D& mx) const;
|
||||
// Re-aim the orbit camera so the bounding sphere of [mn, mx] just fits
|
||||
// vertically and horizontally within the current FOV, with `padding`
|
||||
@@ -499,7 +604,6 @@ private:
|
||||
// way one long frame only produces a single catch-up step instead of
|
||||
// also missing a QTimer tick.
|
||||
enum class CameraMode { Orbit, Fps };
|
||||
void enterFpsMode();
|
||||
void exitFpsMode();
|
||||
void fpsIntegrate(); // called from render()
|
||||
void recenterFpsCursor();
|
||||
@@ -507,6 +611,7 @@ private:
|
||||
QOpenGLContext* context_ = nullptr;
|
||||
QOpenGLFunctions_4_5_Core* gl_ = nullptr;
|
||||
bool gl_initialized_ = false;
|
||||
QColor background_color_ = QColor("#202329");
|
||||
std::deque<PendingOperation> pending_ops_;
|
||||
|
||||
// Shaders
|
||||
@@ -648,11 +753,29 @@ private:
|
||||
QElapsedTimer fps_last_tick_;
|
||||
bool fps_ignore_next_mouse_move_ = false;
|
||||
|
||||
// Selection
|
||||
uint32_t selected_object_id_ = 0;
|
||||
// Selection — set + active id + per-object_id flags SSBO (binding=3).
|
||||
SelectionState selection_;
|
||||
// Per-element visibility — CPU-only flag vector consulted by the
|
||||
// cull, plus a canonical hidden-id set for mutation/reporting.
|
||||
VisibilityState visibility_;
|
||||
|
||||
// Area-measurement tool: see toggleAreaTool / surfacePickedInTool.
|
||||
bool area_tool_active_ = false;
|
||||
// LMB-press state. press_pick_id_ caches the object hit at press
|
||||
// time so the release path can apply click semantics without a
|
||||
// second pick pass. box_select_armed_ is set only when the press
|
||||
// landed on empty space — a subsequent drag past the click
|
||||
// threshold then promotes to box_select_active_, so a small wobble
|
||||
// on a click doesn't accidentally box-select. Modifiers captured
|
||||
// at press-time decide commit semantics: plain replace, Shift add,
|
||||
// Ctrl remove.
|
||||
uint32_t press_pick_id_ = 0;
|
||||
bool box_select_armed_ = false;
|
||||
bool box_select_active_ = false;
|
||||
QPoint box_select_start_pos_;
|
||||
QPoint box_select_current_pos_;
|
||||
Qt::KeyboardModifiers box_select_press_mods_ = Qt::NoModifier;
|
||||
|
||||
// Active measurement tool: see ToolMode / surfacePickedInTool.
|
||||
ToolMode tool_mode_ = ToolMode::None;
|
||||
|
||||
// Renders any client-supplied overlay primitives (highlight triangles
|
||||
// today; lines/points/labels later) in their own pass after the main
|
||||
|
||||
@@ -0,0 +1,93 @@
|
||||
/********************************************************************************
|
||||
* *
|
||||
* This file is part of IfcOpenShell. *
|
||||
* *
|
||||
* IfcOpenShell is free software: you can redistribute it and/or modify *
|
||||
* it under the terms of the Lesser GNU General Public License as published by *
|
||||
* the Free Software Foundation, either version 3.0 of the License, or *
|
||||
* (at your option) any later version. *
|
||||
* *
|
||||
* IfcOpenShell is distributed in the hope that it will be useful, *
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of *
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the *
|
||||
* Lesser GNU General Public License for more details. *
|
||||
* *
|
||||
* You should have received a copy of the Lesser GNU General Public License *
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>. *
|
||||
* *
|
||||
********************************************************************************/
|
||||
|
||||
#include "Visibility.h"
|
||||
|
||||
#include <algorithm>
|
||||
|
||||
VisibilityState::VisibilityState(QObject* parent) : QObject(parent) {
|
||||
// Slot 0 is the "no object" sentinel; keep it present so isHidden(0)
|
||||
// is well-defined (returns false).
|
||||
cpu_flags_.assign(1, 0u);
|
||||
}
|
||||
|
||||
void VisibilityState::noteObjectId(uint32_t id) {
|
||||
if (id == 0) return;
|
||||
if (uint32_t(cpu_flags_.size()) <= id) {
|
||||
cpu_flags_.resize(size_t(id) + 1, 0u);
|
||||
}
|
||||
}
|
||||
|
||||
void VisibilityState::reset() {
|
||||
const bool had_state = !hidden_ids_.empty();
|
||||
hidden_ids_.clear();
|
||||
std::fill(cpu_flags_.begin(), cpu_flags_.end(), 0u);
|
||||
if (had_state) emit changed();
|
||||
}
|
||||
|
||||
void VisibilityState::hideObjects(const std::unordered_set<uint32_t>& ids) {
|
||||
bool any = false;
|
||||
for (uint32_t id : ids) {
|
||||
if (id == 0) continue;
|
||||
if (hidden_ids_.insert(id).second) {
|
||||
if (uint32_t(cpu_flags_.size()) <= id) {
|
||||
cpu_flags_.resize(size_t(id) + 1, 0u);
|
||||
}
|
||||
cpu_flags_[id] = 1u;
|
||||
any = true;
|
||||
}
|
||||
}
|
||||
if (any) emit changed();
|
||||
}
|
||||
|
||||
void VisibilityState::showObjects(const std::unordered_set<uint32_t>& ids) {
|
||||
bool any = false;
|
||||
for (uint32_t id : ids) {
|
||||
if (hidden_ids_.erase(id) > 0) {
|
||||
if (id < cpu_flags_.size()) cpu_flags_[id] = 0u;
|
||||
any = true;
|
||||
}
|
||||
}
|
||||
if (any) emit changed();
|
||||
}
|
||||
|
||||
void VisibilityState::setHidden(const std::unordered_set<uint32_t>& ids) {
|
||||
if (ids == hidden_ids_) return;
|
||||
hidden_ids_ = ids;
|
||||
hidden_ids_.erase(0);
|
||||
rebuildFlags();
|
||||
emit changed();
|
||||
}
|
||||
|
||||
void VisibilityState::showAll() {
|
||||
if (hidden_ids_.empty()) return;
|
||||
hidden_ids_.clear();
|
||||
std::fill(cpu_flags_.begin(), cpu_flags_.end(), 0u);
|
||||
emit changed();
|
||||
}
|
||||
|
||||
void VisibilityState::rebuildFlags() {
|
||||
std::fill(cpu_flags_.begin(), cpu_flags_.end(), 0u);
|
||||
for (uint32_t id : hidden_ids_) {
|
||||
if (uint32_t(cpu_flags_.size()) <= id) {
|
||||
cpu_flags_.resize(size_t(id) + 1, 0u);
|
||||
}
|
||||
cpu_flags_[id] = 1u;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,89 @@
|
||||
/********************************************************************************
|
||||
* *
|
||||
* This file is part of IfcOpenShell. *
|
||||
* *
|
||||
* IfcOpenShell is free software: you can redistribute it and/or modify *
|
||||
* it under the terms of the Lesser GNU General Public License as published by *
|
||||
* the Free Software Foundation, either version 3.0 of the License, or *
|
||||
* (at your option) any later version. *
|
||||
* *
|
||||
* IfcOpenShell is distributed in the hope that it will be useful, *
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of *
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the *
|
||||
* Lesser GNU General Public License for more details. *
|
||||
* *
|
||||
* You should have received a copy of the Lesser GNU General Public License *
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>. *
|
||||
* *
|
||||
********************************************************************************/
|
||||
|
||||
#ifndef IFCVIEWER_VISIBILITY_H
|
||||
#define IFCVIEWER_VISIBILITY_H
|
||||
|
||||
#include <QObject>
|
||||
|
||||
#include <cstdint>
|
||||
#include <unordered_set>
|
||||
#include <vector>
|
||||
|
||||
// Per-element visibility state, owned by ViewportWindow. Independent of
|
||||
// the per-model `hidden` flag — model-level hiding still wins (a hidden
|
||||
// model never draws regardless of its elements' visibility). This class
|
||||
// only tracks element-level overrides on top.
|
||||
//
|
||||
// Lookup is hot — the CPU cull queries `isHidden(object_id)` for every
|
||||
// surviving instance — so the canonical set is mirrored into a flat
|
||||
// per-id byte vector indexed directly by object_id. Ownership is purely
|
||||
// CPU: nothing on the GPU reads visibility, the cull just skips hidden
|
||||
// instances before they reach the visible[] SSBO.
|
||||
class VisibilityState : public QObject {
|
||||
Q_OBJECT
|
||||
public:
|
||||
explicit VisibilityState(QObject* parent = nullptr);
|
||||
|
||||
// Tell the manager about a newly added object_id so the flag vector
|
||||
// can grow ahead of the next cull. Cheap when the id fits already.
|
||||
void noteObjectId(uint32_t id);
|
||||
|
||||
// Drop everything — clears the hidden set. Called from clearScene.
|
||||
void reset();
|
||||
|
||||
// ---- Mutation ----
|
||||
//
|
||||
// hideObjects: union into the hidden set.
|
||||
// showObjects: subtract from the hidden set.
|
||||
// setHidden: replace the hidden set wholesale (used by isolate).
|
||||
// showAll: equivalent to setHidden({}).
|
||||
void hideObjects(const std::unordered_set<uint32_t>& ids);
|
||||
void showObjects(const std::unordered_set<uint32_t>& ids);
|
||||
void setHidden(const std::unordered_set<uint32_t>& ids);
|
||||
void showAll();
|
||||
|
||||
// ---- Hot-path query ----
|
||||
//
|
||||
// Inline so the cull's `if (visibility_.isHidden(...)) return;`
|
||||
// compiles to a bounds check + a byte load + a compare.
|
||||
bool isHidden(uint32_t id) const {
|
||||
return id < cpu_flags_.size() && cpu_flags_[id] != 0;
|
||||
}
|
||||
|
||||
// ---- Accessors ----
|
||||
bool empty() const { return hidden_ids_.empty(); }
|
||||
size_t size() const { return hidden_ids_.size(); }
|
||||
const std::unordered_set<uint32_t>& hiddenIds() const { return hidden_ids_; }
|
||||
|
||||
signals:
|
||||
// Emitted on any mutation that changes the hidden set. Consumers
|
||||
// (the viewport) connect to invalidate cached cull state and
|
||||
// requestUpdate.
|
||||
void changed();
|
||||
|
||||
private:
|
||||
// Recompute cpu_flags_ from hidden_ids_. Cheap: O(|cpu_flags_|).
|
||||
void rebuildFlags();
|
||||
|
||||
std::unordered_set<uint32_t> hidden_ids_;
|
||||
std::vector<uint8_t> cpu_flags_;
|
||||
};
|
||||
|
||||
#endif // IFCVIEWER_VISIBILITY_H
|
||||
@@ -0,0 +1,67 @@
|
||||
IfcViewer settings and environment variables
|
||||
=============================================
|
||||
|
||||
User-facing performance and quality settings (min pixel radius, LOD1
|
||||
threshold, HiZ resolution, etc.) live in the **Settings** dialog and are
|
||||
persisted via ``QSettings``. This page documents the remaining
|
||||
environment variables — instrumentation knobs, regression-hunting
|
||||
toggles, and LOD-build tuning — that are intentionally *not* surfaced
|
||||
in the GUI because they target developers and benchmark runs.
|
||||
|
||||
All variables are read once at first use (most are static-cached
|
||||
inside the function that consumes them), so set them in the shell
|
||||
before launching ``IfcViewerFull`` rather than expecting hot-toggle
|
||||
behaviour.
|
||||
|
||||
Diagnostic and benchmark instrumentation
|
||||
----------------------------------------
|
||||
|
||||
These variables expose hooks that are useful when triaging performance
|
||||
regressions or attributing frame cost. They have no effect on
|
||||
correctness and ship disabled.
|
||||
|
||||
.. csv-table::
|
||||
:header: "Variable", "Default", "Description"
|
||||
:widths: 22, 14, 64
|
||||
|
||||
"``IFC_HIZ_MOTION``", "on (1)", "Trust the previous frame's HiZ pyramid even when the view-projection has drifted (the default). Set to ``0`` to revert to the strict gate, which discards the pyramid the moment the camera moves — useful when chasing the *missing-geometry* class of HiZ correctness bug, since strict gating reproduces a known-good baseline."
|
||||
"``IFC_CULL_THREADS``", "on (1)", "Set to ``0`` to disable the multi-threaded culling path. Forces a single-threaded sweep through every model's BVH, which is useful when bisecting a regression suspected to live in the parallel cull."
|
||||
"``IFC_SKIP_MDI``", "off", "Set to ``1`` to skip ``glMultiDrawElementsIndirect`` calls without changing any other state. Cull, upload, and bind still run; only the actual draw is elided. A large FPS jump means the workload is draw-bound (GPU front-end) rather than upload- or cull-bound."
|
||||
"``IFC_MAX_SUBDRAWS``", "unlimited", "Truncate the drawcount passed to each MDI to ``N``, preserving the forward/reflected ratio. Lets you isolate per-subdraw command-processor overhead from raw triangle work — sweep ``N`` and watch where the FPS curve flattens."
|
||||
"``IFC_FPS_HITCH_MS``", "0 (off)", "When non-zero, log a ``[fps-hitch]`` line for any frame that costs more than ``N`` ms. Only active in FPS (first-person) navigation mode. Captures visible objects, sub-draws, and HiZ-rejection counts per hitch so the slowdown can be attributed."
|
||||
"``IFC_SUBDRAW_DIAG``", "off", "When set (any non-empty value), prints a one-shot histogram of sub-draw composition after the next ``finalizeModel`` — bucket counts of MDIs by sub-draw size, plus instances and triangles in each bucket. Useful for tuning the visible-list packing strategy."
|
||||
|
||||
LOD build tuning
|
||||
----------------
|
||||
|
||||
These affect how the LOD1 representation is generated when a sidecar
|
||||
is *baked*; loading an existing ``.ifcfed`` does not re-read them.
|
||||
Override only when you're regenerating sidecars and want to inspect or
|
||||
adjust the trade-off between LOD0 fidelity and LOD1 triangle savings.
|
||||
|
||||
.. csv-table::
|
||||
:header: "Variable", "Default", "Description"
|
||||
:widths: 22, 14, 64
|
||||
|
||||
"``IFC_LOD_ERROR``", "0.05 (clamped to ≥ 0.2)", "``meshopt_simplify`` ``target_error`` parameter — maximum positional error allowed when collapsing edges, normalised to the mesh AABB diagonal. BIM meshes are typically non-manifold and a 0.2 floor still looks fine at sub-4 pixel sizes; smaller values often produce zero collapses on these inputs."
|
||||
"``IFC_LOD_RATIO``", "meshopt default", "``meshopt_simplify`` ``target_ratio`` parameter — desired fraction of the original index count to retain. Combined with ``target_error`` it forms the simplification budget."
|
||||
"``IFC_LOD_MIN_SAVINGS``", "0.25", "Minimum fraction of triangles that must be eliminated for the LOD1 result to be accepted. Below this, the LOD1 slot is left empty and LOD0 is always drawn for that mesh — avoids paying upload cost for trivial reductions."
|
||||
"``IFC_LOD_DEBUG``", "off", "Set to ``1`` to print per-mesh LOD build diagnostics for the first few meshes of each ``buildLodsForSidecar`` call: input/output triangle counts, target error, and the accept/reject decision. Caps printing automatically so it can be left on for full builds without flooding the log."
|
||||
|
||||
GUI-promoted settings (no longer env-var driven)
|
||||
------------------------------------------------
|
||||
|
||||
For reference, the following knobs were previously read from
|
||||
environment variables and are now driven by ``AppSettings`` and the
|
||||
**Settings** dialog. Their old env-var spellings no longer have any
|
||||
effect.
|
||||
|
||||
.. csv-table::
|
||||
:header: "Setting", "QSettings key", "Old env var", "Default"
|
||||
:widths: 28, 32, 22, 18
|
||||
|
||||
"Min Pixel Radius", "``viewport/min_pixel_radius``", "``IFC_MIN_PX``", "2.0"
|
||||
"Motion Min Pixel Radius", "``viewport/motion_min_pixel_radius``", "``IFC_MIN_PX_MOTION``", "10.0"
|
||||
"LOD1 Pixel Threshold", "``viewport/lod1_pixel_threshold``", "``IFC_LOD1_PX``", "30.0"
|
||||
"HiZ Occlusion", "``viewport/hiz_enabled``", "``IFC_NO_HIZ`` (inverted)", "on"
|
||||
"HiZ Resolution", "``viewport/hiz_resolution``", "``IFC_HIZ_SIZE``", "256"
|
||||
@@ -27,6 +27,8 @@ set(INTERFACE_FILES
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/main.cpp
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/ElementRegistry.cpp
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/ElementRegistry.h
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/InterfaceSettings.cpp
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/InterfaceSettings.h
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/SessionState.cpp
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/SessionState.h
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/MainWindow.cpp
|
||||
@@ -47,33 +49,39 @@ set(INTERFACE_FILES
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/components/Section.h
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/components/Panel.cpp
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/components/Panel.h
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/panels/add_model/Dialog.cpp
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/panels/add_model/Dialog.h
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/panels/models/Types.h
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/panels/models/Controller.cpp
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/panels/models/Controller.h
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/panels/models/Widget.cpp
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/panels/models/Widget.h
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/panels/models/View.cpp
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/panels/models/View.h
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/panels/todo/Widget.cpp
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/panels/todo/Widget.h
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/panels/properties/Types.h
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/panels/properties/Widget.cpp
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/panels/properties/Widget.h
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/panels/properties/View.cpp
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/panels/properties/View.h
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/panels/settings/Dialog.cpp
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/panels/settings/Dialog.h
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/panels/spatial_hierarchy/Types.h
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/panels/spatial_hierarchy/Widget.cpp
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/panels/spatial_hierarchy/Widget.h
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/panels/spatial_hierarchy/View.cpp
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/panels/spatial_hierarchy/View.h
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/panels/viewport/Widget.cpp
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/panels/viewport/Widget.h
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/panels/viewport/Controller.cpp
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/panels/viewport/Controller.h
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/modules/models/AddModelDialog.cpp
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/modules/models/AddModelDialog.h
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/modules/models/SettingsDialog.cpp
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/modules/models/SettingsDialog.h
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/modules/models/Types.h
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/modules/models/Controller.cpp
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/modules/models/Controller.h
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/modules/models/Panel.cpp
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/modules/models/Panel.h
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/modules/models/View.cpp
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/modules/models/View.h
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/modules/todo/Panel.cpp
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/modules/todo/Panel.h
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/modules/properties/Types.h
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/modules/properties/Panel.cpp
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/modules/properties/Panel.h
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/modules/properties/View.cpp
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/modules/properties/View.h
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/modules/project/Controller.cpp
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/modules/project/Controller.h
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/modules/settings/Dialog.cpp
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/modules/settings/Dialog.h
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/modules/spatial_hierarchy/Types.h
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/modules/spatial_hierarchy/Panel.cpp
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/modules/spatial_hierarchy/Panel.h
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/modules/spatial_hierarchy/View.cpp
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/modules/spatial_hierarchy/View.h
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/modules/viewport/Panel.cpp
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/modules/viewport/Panel.h
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/modules/viewport/Controller.cpp
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/modules/viewport/Controller.h
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/../ifcviewer-full/Measurement.cpp
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/../ifcviewer-full/Measurement.h
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/interface_resources.qrc
|
||||
)
|
||||
|
||||
|
||||
@@ -0,0 +1,189 @@
|
||||
// This file was generated with the assistance of an AI coding tool.
|
||||
/********************************************************************************
|
||||
* *
|
||||
* This file is part of IfcOpenShell. *
|
||||
* *
|
||||
* IfcOpenShell is free software: you can redistribute it and/or modify *
|
||||
* it under the terms of the Lesser GNU General Public License as published by *
|
||||
* the Free Software Foundation, either version 3.0 of the License, or *
|
||||
* (at your option) any later version. *
|
||||
* *
|
||||
* IfcOpenShell is distributed in the hope that it will be useful, *
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of *
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the *
|
||||
* Lesser GNU General Public License for more details. *
|
||||
* *
|
||||
* You should have received a copy of the Lesser GNU General Public License *
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>. *
|
||||
* *
|
||||
********************************************************************************/
|
||||
|
||||
#include "InterfaceSettings.h"
|
||||
|
||||
#include "components/Style.h"
|
||||
|
||||
#include <QColor>
|
||||
#include <QSettings>
|
||||
|
||||
namespace {
|
||||
|
||||
constexpr const char* kThemeModeKey = "interface/theme/mode";
|
||||
constexpr const char* kThemeColorPrefix = "interface/theme/colors/";
|
||||
|
||||
using Spec = ifcinterface::InterfaceSettings::ThemeColorSpec;
|
||||
|
||||
const std::vector<Spec> kThemeColorSpecs = {
|
||||
{"app_background", "App Background", ifcinterface::components::style::palette::app_background, "#eef1f5"},
|
||||
{"border", "Border", ifcinterface::components::style::palette::border, "#c9d1dc"},
|
||||
{"selection_background", "Selection Background", ifcinterface::components::style::palette::selection_background,
|
||||
"#2f9e44"},
|
||||
{"tab_bar_background", "Tab Bar Background", ifcinterface::components::style::palette::tab_bar_background,
|
||||
"#eef1f5"},
|
||||
{"tab_background", "Tab Background", ifcinterface::components::style::palette::tab_background, "#e3e8ef"},
|
||||
{"ribbon_background", "Ribbon Background", ifcinterface::components::style::palette::ribbon_background,
|
||||
"#e3e8ef"},
|
||||
{"ribbon_button_hover", "Ribbon Button Hover", ifcinterface::components::style::palette::ribbon_button_hover,
|
||||
"#d6dde7"},
|
||||
{"ribbon_button_pressed", "Ribbon Button Pressed", ifcinterface::components::style::palette::ribbon_button_pressed,
|
||||
"#cad3df"},
|
||||
{"viewport_shell_background", "Viewport Shell Background",
|
||||
ifcinterface::components::style::palette::viewport_shell_background, "#dbe1e8"},
|
||||
{"viewport_background", "Viewport Background", ifcinterface::components::style::palette::viewport_background,
|
||||
"#eef2f6"},
|
||||
{"panel_background", "Panel Background", ifcinterface::components::style::palette::panel_background, "#ffffff"},
|
||||
{"control_background", "Control Background", ifcinterface::components::style::palette::control_background,
|
||||
"#f5f7fa"},
|
||||
{"control_border_focus", "Control Border Focus", ifcinterface::components::style::palette::control_border_focus,
|
||||
"#7c8ca3"},
|
||||
{"box_background", "Box Background", ifcinterface::components::style::palette::box_background, "#f5f7fa"},
|
||||
{"scroll_handle", "Scroll Handle", ifcinterface::components::style::palette::scroll_handle, "#b1bac8"},
|
||||
{"scroll_handle_hover", "Scroll Handle Hover", ifcinterface::components::style::palette::scroll_handle_hover,
|
||||
"#929daf"},
|
||||
{"status_background", "Status Background", ifcinterface::components::style::palette::status_background,
|
||||
"#edf1f5"},
|
||||
{"section_header_background", "Section Header Background",
|
||||
ifcinterface::components::style::palette::section_header_background, "#eef2f6"},
|
||||
{"primary_text", "Primary Text", ifcinterface::components::style::palette::primary_text, "#1f2937"},
|
||||
{"secondary_text", "Secondary Text", ifcinterface::components::style::palette::secondary_text, "#5b6676"},
|
||||
{"disabled_text", "Disabled Text", ifcinterface::components::style::palette::disabled_text, "#8b95a3"},
|
||||
{"warning_text", "Warning Text", ifcinterface::components::style::palette::warning_text, "#b26b00"},
|
||||
{"selection_text", "Selection Text", ifcinterface::components::style::palette::selection_text, "#ffffff"},
|
||||
{"hover_text", "Hover Text", ifcinterface::components::style::palette::hover_text, "#0f172a"},
|
||||
{"icon_color", "Icon Color", "#e7ebf2", "#445066"},
|
||||
{"icon_active_color", "Icon Active Color", "#ffffff", "#101828"},
|
||||
{"icon_disabled_color", "Icon Disabled Color", "#6f7988", "#98a2b3"},
|
||||
{"icon_accent_color", "Accent Icon Color", "#39b54a", "#2f9e44"},
|
||||
{"icon_accent_active_color", "Accent Icon Active Color", "#53c763", "#267e37"},
|
||||
};
|
||||
|
||||
int colorIndexForKey(const QString& key) {
|
||||
for (size_t i = 0; i < kThemeColorSpecs.size(); ++i) {
|
||||
if (QString::fromUtf8(kThemeColorSpecs[i].key) == key) {
|
||||
return static_cast<int>(i);
|
||||
}
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
QString normalizedColor(const QString& value, const QString& fallback) {
|
||||
const QString trimmed = value.trimmed();
|
||||
if (!QColor::isValidColorName(trimmed)) return fallback;
|
||||
return QColor(trimmed).name(QColor::HexRgb);
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
namespace ifcinterface {
|
||||
|
||||
InterfaceSettings& InterfaceSettings::instance() {
|
||||
static InterfaceSettings inst;
|
||||
return inst;
|
||||
}
|
||||
|
||||
const std::vector<InterfaceSettings::ThemeColorSpec>& InterfaceSettings::themeColorSpecs() {
|
||||
return kThemeColorSpecs;
|
||||
}
|
||||
|
||||
InterfaceSettings::InterfaceSettings() {
|
||||
custom_colors_.resize(kThemeColorSpecs.size());
|
||||
load();
|
||||
}
|
||||
|
||||
InterfaceSettings::ThemeMode InterfaceSettings::themeMode() const {
|
||||
return theme_mode_;
|
||||
}
|
||||
|
||||
void InterfaceSettings::setThemeMode(ThemeMode mode) {
|
||||
if (theme_mode_ == mode) return;
|
||||
theme_mode_ = mode;
|
||||
persist();
|
||||
emit themeModeChanged(mode);
|
||||
emit themeChanged();
|
||||
}
|
||||
|
||||
QString InterfaceSettings::color(const QString& key) const {
|
||||
const int index = colorIndexForKey(key);
|
||||
if (index < 0) return {};
|
||||
|
||||
const auto& spec = kThemeColorSpecs[static_cast<size_t>(index)];
|
||||
switch (theme_mode_) {
|
||||
case ThemeMode::Dark:
|
||||
return QString::fromUtf8(spec.dark_default);
|
||||
case ThemeMode::Light:
|
||||
return QString::fromUtf8(spec.light_default);
|
||||
case ThemeMode::Custom:
|
||||
return customColor(key);
|
||||
}
|
||||
return QString::fromUtf8(spec.dark_default);
|
||||
}
|
||||
|
||||
QString InterfaceSettings::customColor(const QString& key) const {
|
||||
const int index = colorIndexForKey(key);
|
||||
if (index < 0) return {};
|
||||
const auto& spec = kThemeColorSpecs[static_cast<size_t>(index)];
|
||||
const QString& stored = custom_colors_[static_cast<size_t>(index)];
|
||||
return stored.isEmpty() ? QString::fromUtf8(spec.dark_default) : stored;
|
||||
}
|
||||
|
||||
void InterfaceSettings::setCustomColor(const QString& key, const QString& value) {
|
||||
const int index = colorIndexForKey(key);
|
||||
if (index < 0) return;
|
||||
|
||||
const auto& spec = kThemeColorSpecs[static_cast<size_t>(index)];
|
||||
const QString normalized = normalizedColor(value, QString::fromUtf8(spec.dark_default));
|
||||
QString& stored = custom_colors_[static_cast<size_t>(index)];
|
||||
if (stored == normalized) return;
|
||||
stored = normalized;
|
||||
persist();
|
||||
if (theme_mode_ == ThemeMode::Custom) {
|
||||
emit themeChanged();
|
||||
}
|
||||
}
|
||||
|
||||
void InterfaceSettings::load() {
|
||||
QSettings settings;
|
||||
int raw_mode = settings.value(kThemeModeKey, static_cast<int>(ThemeMode::Dark)).toInt();
|
||||
if (raw_mode < static_cast<int>(ThemeMode::Dark) || raw_mode > static_cast<int>(ThemeMode::Custom)) {
|
||||
raw_mode = static_cast<int>(ThemeMode::Dark);
|
||||
}
|
||||
theme_mode_ = static_cast<ThemeMode>(raw_mode);
|
||||
|
||||
for (size_t i = 0; i < kThemeColorSpecs.size(); ++i) {
|
||||
const auto& spec = kThemeColorSpecs[i];
|
||||
const QString stored = settings.value(QString::fromUtf8(kThemeColorPrefix) + QString::fromUtf8(spec.key),
|
||||
QString::fromUtf8(spec.dark_default))
|
||||
.toString();
|
||||
custom_colors_[i] = normalizedColor(stored, QString::fromUtf8(spec.dark_default));
|
||||
}
|
||||
}
|
||||
|
||||
void InterfaceSettings::persist() const {
|
||||
QSettings settings;
|
||||
settings.setValue(kThemeModeKey, static_cast<int>(theme_mode_));
|
||||
for (size_t i = 0; i < kThemeColorSpecs.size(); ++i) {
|
||||
settings.setValue(QString::fromUtf8(kThemeColorPrefix) + QString::fromUtf8(kThemeColorSpecs[i].key),
|
||||
custom_colors_[i]);
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace ifcinterface
|
||||
@@ -0,0 +1,72 @@
|
||||
// This file was generated with the assistance of an AI coding tool.
|
||||
/********************************************************************************
|
||||
* *
|
||||
* This file is part of IfcOpenShell. *
|
||||
* *
|
||||
* IfcOpenShell is free software: you can redistribute it and/or modify *
|
||||
* it under the terms of the Lesser GNU General Public License as published by *
|
||||
* the Free Software Foundation, either version 3.0 of the License, or *
|
||||
* (at your option) any later version. *
|
||||
* *
|
||||
* IfcOpenShell is distributed in the hope that it will be useful, *
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of *
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the *
|
||||
* Lesser GNU General Public License for more details. *
|
||||
* *
|
||||
* You should have received a copy of the Lesser GNU General Public License *
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>. *
|
||||
* *
|
||||
********************************************************************************/
|
||||
|
||||
#ifndef IFCINTERFACE_INTERFACESETTINGS_H
|
||||
#define IFCINTERFACE_INTERFACESETTINGS_H
|
||||
|
||||
#include <QObject>
|
||||
#include <QString>
|
||||
#include <vector>
|
||||
|
||||
namespace ifcinterface {
|
||||
|
||||
class InterfaceSettings : public QObject {
|
||||
Q_OBJECT
|
||||
public:
|
||||
enum class ThemeMode {
|
||||
Dark = 0,
|
||||
Light = 1,
|
||||
Custom = 2,
|
||||
};
|
||||
Q_ENUM(ThemeMode)
|
||||
|
||||
struct ThemeColorSpec {
|
||||
const char* key;
|
||||
const char* label;
|
||||
const char* dark_default;
|
||||
const char* light_default;
|
||||
};
|
||||
|
||||
static InterfaceSettings& instance();
|
||||
static const std::vector<ThemeColorSpec>& themeColorSpecs();
|
||||
|
||||
ThemeMode themeMode() const;
|
||||
void setThemeMode(ThemeMode mode);
|
||||
|
||||
QString color(const QString& key) const;
|
||||
QString customColor(const QString& key) const;
|
||||
void setCustomColor(const QString& key, const QString& value);
|
||||
|
||||
signals:
|
||||
void themeModeChanged(ThemeMode mode);
|
||||
void themeChanged();
|
||||
|
||||
private:
|
||||
InterfaceSettings();
|
||||
void load();
|
||||
void persist() const;
|
||||
|
||||
ThemeMode theme_mode_ = ThemeMode::Dark;
|
||||
std::vector<QString> custom_colors_;
|
||||
};
|
||||
|
||||
} // namespace ifcinterface
|
||||
|
||||
#endif
|
||||
+151
-400
@@ -30,31 +30,29 @@
|
||||
#include "components/Panel.h"
|
||||
#include "components/Style.h"
|
||||
#include "components/Tabs.h"
|
||||
#include "panels/add_model/Dialog.h"
|
||||
#include "panels/models/Controller.h"
|
||||
#include "panels/todo/Widget.h"
|
||||
#include "panels/models/View.h"
|
||||
#include "panels/models/Widget.h"
|
||||
#include "panels/properties/View.h"
|
||||
#include "panels/properties/Widget.h"
|
||||
#include "panels/settings/Dialog.h"
|
||||
#include "panels/spatial_hierarchy/View.h"
|
||||
#include "panels/spatial_hierarchy/Widget.h"
|
||||
#include "panels/viewport/Controller.h"
|
||||
#include "panels/viewport/Widget.h"
|
||||
#include "modules/models/Controller.h"
|
||||
#include "modules/todo/Panel.h"
|
||||
#include "modules/models/View.h"
|
||||
#include "modules/models/Panel.h"
|
||||
#include "modules/project/Controller.h"
|
||||
#include "modules/properties/View.h"
|
||||
#include "modules/properties/Panel.h"
|
||||
#include "modules/settings/Dialog.h"
|
||||
#include "modules/spatial_hierarchy/View.h"
|
||||
#include "modules/spatial_hierarchy/Panel.h"
|
||||
#include "modules/viewport/Controller.h"
|
||||
#include "modules/viewport/Panel.h"
|
||||
|
||||
#include <QDockWidget>
|
||||
#include <QFileDialog>
|
||||
#include <QFileInfo>
|
||||
#include <QHBoxLayout>
|
||||
#include <QIcon>
|
||||
#include <QLabel>
|
||||
#include <QListView>
|
||||
#include <QMessageBox>
|
||||
#include <QShortcut>
|
||||
#include <QSignalBlocker>
|
||||
#include <QStackedWidget>
|
||||
#include <QStatusBar>
|
||||
#include <QTreeView>
|
||||
#include <QToolButton>
|
||||
#include <QVBoxLayout>
|
||||
|
||||
@@ -72,6 +70,14 @@ MainWindow::MainWindow(QWidget* parent)
|
||||
setWindowModified(dirty);
|
||||
updateWindowTitle();
|
||||
});
|
||||
connect(session_state_, &ifcinterface::SessionState::modelsChanged,
|
||||
this, &MainWindow::updateWindowTitle);
|
||||
connect(session_state_, &ifcinterface::SessionState::projectOpened,
|
||||
this, [this](const QString&) { updateWindowTitle(); });
|
||||
connect(session_state_, &ifcinterface::SessionState::projectSaved,
|
||||
this, [this](const QString&) { updateWindowTitle(); });
|
||||
connect(session_state_, &ifcinterface::SessionState::projectReset,
|
||||
this, &MainWindow::updateWindowTitle);
|
||||
setupChrome();
|
||||
setupViewport();
|
||||
setupPanels();
|
||||
@@ -86,19 +92,44 @@ void MainWindow::setupChrome() {
|
||||
setDockOptions(QMainWindow::AllowNestedDocks |
|
||||
QMainWindow::AllowTabbedDocks |
|
||||
QMainWindow::GroupedDragging);
|
||||
|
||||
auto bind_shortcut = [this](const QKeySequence& sequence, auto fn) {
|
||||
auto* shortcut = new QShortcut(sequence, this);
|
||||
shortcut->setContext(Qt::WindowShortcut);
|
||||
connect(shortcut, &QShortcut::activated, this, fn);
|
||||
};
|
||||
bind_shortcut(QKeySequence("Ctrl+Shift+L"), [this]() {
|
||||
viewport_controller_->toggleDistanceMode();
|
||||
});
|
||||
bind_shortcut(QKeySequence("Ctrl+Shift+A"), [this]() {
|
||||
viewport_controller_->toggleAreaMode();
|
||||
});
|
||||
bind_shortcut(QKeySequence("Ctrl+Shift+V"), [this]() {
|
||||
viewport_controller_->toggleVolumeMode();
|
||||
});
|
||||
bind_shortcut(QKeySequence(Qt::SHIFT | Qt::Key_F), [this]() {
|
||||
viewport_controller_->setFlyMode();
|
||||
});
|
||||
bind_shortcut(QKeySequence(Qt::Key_K), [this]() {
|
||||
viewport_controller_->toggleSectionMode();
|
||||
});
|
||||
bind_shortcut(QKeySequence(Qt::SHIFT | Qt::Key_K), [this]() {
|
||||
viewport_controller_->clearSectionPlanes();
|
||||
});
|
||||
bind_shortcut(QKeySequence(Qt::Key_H), [this]() {
|
||||
viewport_controller_->hideSelectedElements();
|
||||
});
|
||||
bind_shortcut(QKeySequence(Qt::SHIFT | Qt::Key_H), [this]() {
|
||||
viewport_controller_->isolateSelectedElements();
|
||||
});
|
||||
bind_shortcut(QKeySequence(Qt::ALT | Qt::Key_H), [this]() {
|
||||
viewport_controller_->showAllElements();
|
||||
});
|
||||
updateWindowTitle();
|
||||
}
|
||||
|
||||
QToolButton* MainWindow::makeRibbonAction(const QString& text, const QString& icon_path) {
|
||||
return components::buttons::makeButton(text, icon_path, this, QSize(90, 54));
|
||||
}
|
||||
|
||||
QWidget* MainWindow::makeRibbonGroup(const QString& title, const QList<QToolButton*>& buttons) {
|
||||
return components::buttons::makeButtonGroup(title, buttons, this);
|
||||
}
|
||||
|
||||
QToolButton* MainWindow::makePanelToggle(const QString& text, QDockWidget* dock) {
|
||||
auto* button = makeRibbonAction(text, ":/icons/sidebar-expand.svg");
|
||||
auto* button = components::buttons::makeButton(text, ":/icons/sidebar-expand.svg", this);
|
||||
button->setCheckable(true);
|
||||
button->setChecked(dock->isVisible());
|
||||
connect(button, &QToolButton::toggled, dock, [dock](bool checked) {
|
||||
@@ -119,39 +150,49 @@ QWidget* MainWindow::buildHomeRibbonPage() {
|
||||
row->setContentsMargins(6, 4, 6, 4);
|
||||
row->setSpacing(0);
|
||||
|
||||
auto* new_project = makeRibbonAction("New Project", ":/icons/plus-square.svg");
|
||||
connect(new_project, &QToolButton::clicked, this, &MainWindow::onNewProject);
|
||||
auto* open_project = makeRibbonAction("Open Project", ":/icons/download-square.svg");
|
||||
connect(open_project, &QToolButton::clicked, this, &MainWindow::onOpenProject);
|
||||
auto* open_cloud = makeRibbonAction("Open Cloud", ":/icons/cloud-square.svg");
|
||||
auto* new_project = components::buttons::makeButton("New Project", ":/icons/plus-square.svg", this);
|
||||
connect(new_project, &QToolButton::clicked, this, [this]() {
|
||||
project_controller_->newProject();
|
||||
});
|
||||
auto* open_project = components::buttons::makeButton("Open Project", ":/icons/download-square.svg", this);
|
||||
connect(open_project, &QToolButton::clicked, this, [this]() {
|
||||
project_controller_->openProject();
|
||||
});
|
||||
auto* open_cloud = components::buttons::makeButton("Open Cloud", ":/icons/cloud-square.svg", this);
|
||||
connect(open_cloud, &QToolButton::clicked, this, [this]() {
|
||||
session_state_->setStatusMessage("Project", "Open Cloud Project coming soon");
|
||||
});
|
||||
auto* open_recent = makeRibbonAction("Open Recent", ":/icons/clock-rotate-right.svg");
|
||||
auto* open_recent = components::buttons::makeButton("Open Recent", ":/icons/clock-rotate-right.svg", this);
|
||||
connect(open_recent, &QToolButton::clicked, this, [this]() {
|
||||
session_state_->setStatusMessage("Project", "Open Recent coming soon");
|
||||
});
|
||||
auto* save_project = makeRibbonAction("Save Project", ":/icons/floppy-disk.svg");
|
||||
connect(save_project, &QToolButton::clicked, this, &MainWindow::onSaveProject);
|
||||
auto* save_project_as = makeRibbonAction("Save As", ":/icons/floppy-disk-arrow-in.svg");
|
||||
connect(save_project_as, &QToolButton::clicked, this, &MainWindow::onSaveProjectAs);
|
||||
auto* save_project = components::buttons::makeButton("Save Project", ":/icons/floppy-disk.svg", this);
|
||||
connect(save_project, &QToolButton::clicked, this, [this]() {
|
||||
project_controller_->saveProject();
|
||||
});
|
||||
auto* save_project_as = components::buttons::makeButton("Save As", ":/icons/floppy-disk-arrow-in.svg", this);
|
||||
connect(save_project_as, &QToolButton::clicked, this, [this]() {
|
||||
project_controller_->saveProjectAs();
|
||||
});
|
||||
|
||||
auto* add_model = makeRibbonAction("Add Model", ":/icons/cube.svg");
|
||||
connect(add_model, &QToolButton::clicked, this, &MainWindow::onAddFiles);
|
||||
auto* sync_models = makeRibbonAction("Sync Models", ":/icons/refresh-double.svg");
|
||||
auto* add_model = components::buttons::makeButton("Add Model", ":/icons/cube.svg", this);
|
||||
connect(add_model, &QToolButton::clicked, this, [this]() {
|
||||
models_controller_->addFiles();
|
||||
});
|
||||
auto* sync_models = components::buttons::makeButton("Sync Models", ":/icons/refresh-double.svg", this);
|
||||
connect(sync_models, &QToolButton::clicked, this, [this]() {
|
||||
session_state_->setStatusMessage("Models", "Sync models coming soon");
|
||||
});
|
||||
|
||||
auto* settings_button = makeRibbonAction("Settings", ":/icons/settings.svg");
|
||||
auto* settings_button = components::buttons::makeButton("Settings", ":/icons/settings.svg", this);
|
||||
connect(settings_button, &QToolButton::clicked, this, [this]() {
|
||||
panels::settings::SettingsDialog dialog(this);
|
||||
modules::settings::SettingsDialog dialog(this);
|
||||
dialog.exec();
|
||||
});
|
||||
|
||||
row->addWidget(makeRibbonGroup("PROJECT", {new_project, open_project, open_cloud, open_recent, save_project, save_project_as}));
|
||||
row->addWidget(makeRibbonGroup("MODELS", {add_model, sync_models}));
|
||||
row->addWidget(makeRibbonGroup("SETTINGS", {settings_button}));
|
||||
row->addWidget(components::buttons::makeButtonGroup("PROJECT", {new_project, open_project, open_cloud, open_recent, save_project, save_project_as}, this));
|
||||
row->addWidget(components::buttons::makeButtonGroup("MODELS", {add_model, sync_models}, this));
|
||||
row->addWidget(components::buttons::makeButtonGroup("SETTINGS", {settings_button}, this));
|
||||
row->addStretch(1);
|
||||
return page;
|
||||
}
|
||||
@@ -163,36 +204,40 @@ QWidget* MainWindow::buildNavigateRibbonPage() {
|
||||
row->setContentsMargins(2, 4, 2, 4);
|
||||
row->setSpacing(0);
|
||||
|
||||
auto* set_home = makeRibbonAction("Set Home", ":/icons/home.svg");
|
||||
connect(set_home, &QToolButton::clicked, this, &MainWindow::onSetHomeView);
|
||||
auto* go_home = makeRibbonAction("Go Home", ":/icons/home-alt.svg");
|
||||
connect(go_home, &QToolButton::clicked, this, &MainWindow::onGoHomeView);
|
||||
auto* view_all = makeRibbonAction("View All", ":/icons/cube-scan.svg");
|
||||
auto* set_home = components::buttons::makeButton("Set Home", ":/icons/home.svg", this);
|
||||
connect(set_home, &QToolButton::clicked, this, [this]() {
|
||||
viewport_controller_->setHomeView();
|
||||
});
|
||||
auto* go_home = components::buttons::makeButton("Go Home", ":/icons/home-alt.svg", this);
|
||||
connect(go_home, &QToolButton::clicked, this, [this]() {
|
||||
viewport_controller_->goHomeView();
|
||||
});
|
||||
auto* view_all = components::buttons::makeButton("View All", ":/icons/cube-scan.svg", this);
|
||||
connect(view_all, &QToolButton::clicked, this, [this]() {
|
||||
if (viewport_widget_) viewport_widget_->viewport()->viewAll();
|
||||
});
|
||||
auto* view_selected = makeRibbonAction("View Selected", ":/icons/cube-scan-solid.svg");
|
||||
auto* view_selected = components::buttons::makeButton("View Selected", ":/icons/cube-scan-solid.svg", this);
|
||||
connect(view_selected, &QToolButton::clicked, this, [this]() {
|
||||
if (viewport_widget_) viewport_widget_->viewport()->focusOnSelectedObject();
|
||||
viewport_controller_->focusSelectedObject();
|
||||
});
|
||||
|
||||
auto* plan_view = makeRibbonAction("Plan", ":/icons/planimetry.svg");
|
||||
auto* plan_view = components::buttons::makeButton("Plan", ":/icons/planimetry.svg", this);
|
||||
connect(plan_view, &QToolButton::clicked, this, [this]() {
|
||||
if (viewport_widget_) viewport_widget_->viewport()->setStandardView(90.0f, 90.0f);
|
||||
});
|
||||
auto* front_view = makeRibbonAction("Front", ":/icons/city.svg");
|
||||
auto* front_view = components::buttons::makeButton("Front", ":/icons/city.svg", this);
|
||||
connect(front_view, &QToolButton::clicked, this, [this]() {
|
||||
if (viewport_widget_) viewport_widget_->viewport()->setStandardView(0.0f, 0.0f);
|
||||
});
|
||||
auto* side_view = makeRibbonAction("Side", ":/icons/building.svg");
|
||||
auto* side_view = components::buttons::makeButton("Side", ":/icons/building.svg", this);
|
||||
connect(side_view, &QToolButton::clicked, this, [this]() {
|
||||
if (viewport_widget_) viewport_widget_->viewport()->setStandardView(90.0f, 0.0f);
|
||||
});
|
||||
auto* align_object = makeRibbonAction("Align Object", ":/icons/cellar.svg");
|
||||
auto* align_object = components::buttons::makeButton("Align Object", ":/icons/cellar.svg", this);
|
||||
connect(align_object, &QToolButton::clicked, this, [this]() {
|
||||
session_state_->setStatusMessage("Orientation", "Align to object coming soon");
|
||||
});
|
||||
auto* projection_button = makeRibbonAction("Perspective", ":/icons/perspective-view.svg");
|
||||
auto* projection_button = components::buttons::makeButton("Perspective", ":/icons/perspective-view.svg", this);
|
||||
connect(projection_button, &QToolButton::clicked, this, [this, projection_button]() {
|
||||
if (!viewport_widget_) return;
|
||||
viewport_widget_->viewport()->toggleProjection();
|
||||
@@ -200,18 +245,18 @@ QWidget* MainWindow::buildNavigateRibbonPage() {
|
||||
viewport_widget_->viewport()->projectionOrtho() ? "Ortho" : "Perspective");
|
||||
});
|
||||
|
||||
auto* orbit_mode = makeRibbonAction("Orbit", ":/icons/rotate-camera-right.svg");
|
||||
connect(orbit_mode, &QToolButton::clicked, this, [this]() {
|
||||
session_state_->setStatusMessage("Mode", "Orbit mode active");
|
||||
});
|
||||
auto* fly_mode = makeRibbonAction("Fly", ":/icons/drone.svg");
|
||||
auto* fly_mode = components::buttons::makeButton("Fly", ":/icons/drone.svg", this);
|
||||
connect(fly_mode, &QToolButton::clicked, this, [this]() {
|
||||
session_state_->setStatusMessage("Mode", "Fly mode coming soon");
|
||||
viewport_controller_->setFlyMode();
|
||||
});
|
||||
auto* section_mode = components::buttons::makeButton("Section", ":/icons/cube-cut-with-curve.svg", this);
|
||||
connect(section_mode, &QToolButton::clicked, this, [this]() {
|
||||
viewport_controller_->toggleSectionMode();
|
||||
});
|
||||
|
||||
row->addWidget(makeRibbonGroup("CAMERA", {set_home, go_home, view_all, view_selected}));
|
||||
row->addWidget(makeRibbonGroup("ORIENTATION", {plan_view, front_view, side_view, align_object, projection_button}));
|
||||
row->addWidget(makeRibbonGroup("MODE", {orbit_mode, fly_mode}));
|
||||
row->addWidget(components::buttons::makeButtonGroup("CAMERA", {set_home, go_home, view_all, view_selected}, this));
|
||||
row->addWidget(components::buttons::makeButtonGroup("ORIENTATION", {plan_view, front_view, side_view, align_object, projection_button}, this));
|
||||
row->addWidget(components::buttons::makeButtonGroup("MODE", {fly_mode, section_mode}, this));
|
||||
row->addStretch(1);
|
||||
return page;
|
||||
}
|
||||
@@ -223,38 +268,38 @@ QWidget* MainWindow::buildInspectRibbonPage() {
|
||||
row->setContentsMargins(2, 4, 2, 4);
|
||||
row->setSpacing(0);
|
||||
|
||||
auto* hide_selected = makeRibbonAction("Hide", ":/icons/eye-closed.svg");
|
||||
auto* hide_selected = components::buttons::makeButton("Hide", ":/icons/eye-closed.svg", this);
|
||||
connect(hide_selected, &QToolButton::clicked, this, [this]() {
|
||||
session_state_->setStatusMessage("Selection", "Hide selected coming soon");
|
||||
viewport_controller_->hideSelectedElements();
|
||||
});
|
||||
auto* isolate_selected = makeRibbonAction("Isolate", ":/icons/eye-solid.svg");
|
||||
auto* isolate_selected = components::buttons::makeButton("Isolate", ":/icons/eye-solid.svg", this);
|
||||
connect(isolate_selected, &QToolButton::clicked, this, [this]() {
|
||||
session_state_->setStatusMessage("Selection", "Isolate selected coming soon");
|
||||
viewport_controller_->isolateSelectedElements();
|
||||
});
|
||||
auto* show_all = makeRibbonAction("Show All", ":/icons/eye.svg");
|
||||
auto* show_all = components::buttons::makeButton("Show All", ":/icons/eye.svg", this);
|
||||
connect(show_all, &QToolButton::clicked, this, [this]() {
|
||||
session_state_->setStatusMessage("Selection", "Show all coming soon");
|
||||
viewport_controller_->showAllElements();
|
||||
});
|
||||
auto* invert_selection = makeRibbonAction("Invert", ":/icons/intersect.svg");
|
||||
auto* invert_selection = components::buttons::makeButton("Invert", ":/icons/intersect.svg", this);
|
||||
connect(invert_selection, &QToolButton::clicked, this, [this]() {
|
||||
session_state_->setStatusMessage("Selection", "Invert selection coming soon");
|
||||
viewport_controller_->invertSelection();
|
||||
});
|
||||
|
||||
auto* distance = makeRibbonAction("Distance", ":/icons/select-edge3d.svg");
|
||||
auto* distance = components::buttons::makeButton("Distance", ":/icons/select-edge3d.svg", this);
|
||||
connect(distance, &QToolButton::clicked, this, [this]() {
|
||||
session_state_->setStatusMessage("Measure", "Distance coming soon");
|
||||
viewport_controller_->toggleDistanceMode();
|
||||
});
|
||||
auto* area = makeRibbonAction("Area", ":/icons/select-face3d.svg");
|
||||
auto* area = components::buttons::makeButton("Area", ":/icons/select-face3d.svg", this);
|
||||
connect(area, &QToolButton::clicked, this, [this]() {
|
||||
session_state_->setStatusMessage("Measure", "Area coming soon");
|
||||
viewport_controller_->toggleAreaMode();
|
||||
});
|
||||
auto* volume = makeRibbonAction("Volume", ":/icons/select-point3d.svg");
|
||||
auto* volume = components::buttons::makeButton("Volume", ":/icons/select-point3d.svg", this);
|
||||
connect(volume, &QToolButton::clicked, this, [this]() {
|
||||
session_state_->setStatusMessage("Measure", "Volume coming soon");
|
||||
viewport_controller_->toggleVolumeMode();
|
||||
});
|
||||
|
||||
row->addWidget(makeRibbonGroup("SELECTION", {hide_selected, isolate_selected, show_all, invert_selection}));
|
||||
row->addWidget(makeRibbonGroup("MEASURE", {distance, area, volume}));
|
||||
row->addWidget(components::buttons::makeButtonGroup("SELECTION", {hide_selected, isolate_selected, show_all, invert_selection}, this));
|
||||
row->addWidget(components::buttons::makeButtonGroup("MEASURE", {distance, area, volume}, this));
|
||||
row->addStretch(1);
|
||||
return page;
|
||||
}
|
||||
@@ -266,29 +311,28 @@ QWidget* MainWindow::buildPanelsRibbonPage() {
|
||||
row->setContentsMargins(2, 4, 2, 4);
|
||||
row->setSpacing(0);
|
||||
|
||||
row->addWidget(makeRibbonGroup("DATA", {
|
||||
row->addWidget(components::buttons::makeButtonGroup("DATA", {
|
||||
makePanelToggle("Models", models_panel_),
|
||||
makePanelToggle("Spatial", spatial_panel_),
|
||||
makePanelToggle("Layers", layers_panel_),
|
||||
makePanelToggle("Properties", properties_panel_)
|
||||
}));
|
||||
row->addWidget(makeRibbonGroup("QUERY", {
|
||||
}, this));
|
||||
row->addWidget(components::buttons::makeButtonGroup("QUERY", {
|
||||
makePanelToggle("Views", stored_views_panel_),
|
||||
makePanelToggle("Search", search_panel_),
|
||||
makePanelToggle("Sheets", spreadsheet_panel_),
|
||||
makePanelToggle("Audit", audit_panel_)
|
||||
}));
|
||||
row->addWidget(makeRibbonGroup("COLLABORATE", {
|
||||
}, this));
|
||||
row->addWidget(components::buttons::makeButtonGroup("COLLABORATE", {
|
||||
makePanelToggle("Clash", clash_panel_),
|
||||
makePanelToggle("Issues", issues_panel_)
|
||||
}));
|
||||
}, this));
|
||||
row->addStretch(1);
|
||||
return page;
|
||||
}
|
||||
|
||||
void MainWindow::setupRibbon() {
|
||||
auto* shell = new QFrame(this);
|
||||
shell->setObjectName("ribbonShell");
|
||||
|
||||
auto* shell_layout = new QVBoxLayout(shell);
|
||||
shell_layout->setContentsMargins(0, 0, 0, 0);
|
||||
@@ -324,31 +368,31 @@ void MainWindow::setupRibbon() {
|
||||
}
|
||||
|
||||
void MainWindow::setupViewport() {
|
||||
viewport_widget_ = new panels::viewport::ViewportWidget(this);
|
||||
viewport_widget_ = new modules::viewport::ViewportPanel(this);
|
||||
setCentralWidget(viewport_widget_);
|
||||
}
|
||||
|
||||
void MainWindow::setupPanels() {
|
||||
models_panel_ = new panels::models::ModelsPanelWidget(this);
|
||||
spatial_panel_ = new panels::spatial_hierarchy::SpatialHierarchyPanelWidget(this);
|
||||
properties_panel_ = new panels::properties::PropertiesPanelWidget(this);
|
||||
models_panel_ = new modules::models::ModelsPanel(this);
|
||||
spatial_panel_ = new modules::spatial_hierarchy::SpatialHierarchyPanel(this);
|
||||
properties_panel_ = new modules::properties::PropertiesPanel(this);
|
||||
|
||||
models_controller_ = new panels::models::ModelsPanelController(
|
||||
models_panel_, session_state_, viewport_widget_->viewport(), element_registry_, this);
|
||||
models_view_ = new panels::models::ModelsPanelView(models_panel_, session_state_, this);
|
||||
spatial_view_ = new panels::spatial_hierarchy::SpatialHierarchyPanelView(spatial_panel_, session_state_, this);
|
||||
properties_view_ = new panels::properties::PropertiesPanelView(properties_panel_, session_state_, this);
|
||||
models_controller_ = new modules::models::ModelsPanelController(
|
||||
this, models_panel_, session_state_, viewport_widget_->viewport(), this);
|
||||
models_view_ = new modules::models::ModelsPanelView(models_panel_, session_state_, this);
|
||||
spatial_view_ = new modules::spatial_hierarchy::SpatialHierarchyPanelView(spatial_panel_, session_state_, this);
|
||||
properties_view_ = new modules::properties::PropertiesPanelView(properties_panel_, session_state_, this);
|
||||
|
||||
layers_panel_ = new components::Panel("Layers", new panels::todo::TodoPanelWidget("Layers", this), this);
|
||||
layers_panel_ = new components::Panel("Layers", new modules::todo::TodoPanel("Layers", this), this);
|
||||
stored_views_panel_ = new components::Panel(
|
||||
"Stored Views", new panels::todo::TodoPanelWidget("Stored Views", this), this);
|
||||
"Stored Views", new modules::todo::TodoPanel("Stored Views", this), this);
|
||||
search_panel_ = new components::Panel(
|
||||
"Search and Query", new panels::todo::TodoPanelWidget("Search and Query", this), this);
|
||||
"Search and Query", new modules::todo::TodoPanel("Search and Query", this), this);
|
||||
spreadsheet_panel_ = new components::Panel(
|
||||
"Spreadsheet", new panels::todo::TodoPanelWidget("Spreadsheet", this), this);
|
||||
audit_panel_ = new components::Panel("Audit", new panels::todo::TodoPanelWidget("Audit", this), this);
|
||||
clash_panel_ = new components::Panel("Clash", new panels::todo::TodoPanelWidget("Clash", this), this);
|
||||
issues_panel_ = new components::Panel("Issues", new panels::todo::TodoPanelWidget("Issues", this), this);
|
||||
"Spreadsheet", new modules::todo::TodoPanel("Spreadsheet", this), this);
|
||||
audit_panel_ = new components::Panel("Audit", new modules::todo::TodoPanel("Audit", this), this);
|
||||
clash_panel_ = new components::Panel("Clash", new modules::todo::TodoPanel("Clash", this), this);
|
||||
issues_panel_ = new components::Panel("Issues", new modules::todo::TodoPanel("Issues", this), this);
|
||||
|
||||
addDockWidget(Qt::LeftDockWidgetArea, models_panel_);
|
||||
addDockWidget(Qt::LeftDockWidgetArea, spatial_panel_);
|
||||
@@ -411,14 +455,12 @@ void MainWindow::setupLoader() {
|
||||
loader_ = new SceneLoader(viewport_widget_->viewport(), this);
|
||||
element_registry_->bindLoader(loader_);
|
||||
session_state_->bindLoader(loader_);
|
||||
viewport_controller_ = new panels::viewport::ViewportController(
|
||||
models_controller_->bindLoader(loader_);
|
||||
viewport_controller_ = new modules::viewport::ViewportController(
|
||||
session_state_, viewport_widget_->viewport(), this);
|
||||
connect(loader_, &SceneLoader::loadStarted, this, &MainWindow::onLoadStarted);
|
||||
connect(loader_, &SceneLoader::loadedFromSidecar, this, &MainWindow::onLoadedFromSidecar);
|
||||
connect(loader_, &SceneLoader::loadedFromStream, this, &MainWindow::onLoadedFromStream);
|
||||
connect(loader_, &SceneLoader::loadCancelled, this, &MainWindow::onLoadCancelled);
|
||||
connect(loader_, &SceneLoader::loadError, this, &MainWindow::onLoadError);
|
||||
connect(loader_, &SceneLoader::allLoadsFinished, this, &MainWindow::onAllLoadsFinished);
|
||||
project_controller_ = new modules::project::ProjectController(
|
||||
this, federation_, session_state_, element_registry_,
|
||||
viewport_widget_->viewport(), models_controller_, viewport_controller_, this);
|
||||
|
||||
connect(viewport_widget_->viewport(), &ViewportWindow::frameStatsUpdated, this,
|
||||
[this](const ViewportWindow::FrameStats& s) {
|
||||
@@ -440,204 +482,6 @@ void MainWindow::setupLoader() {
|
||||
});
|
||||
}
|
||||
|
||||
void MainWindow::addFiles(const QStringList& paths) {
|
||||
QStringList accepted_paths;
|
||||
QStringList accepted_fed_ids;
|
||||
for (const auto& path : paths) {
|
||||
const QString fed_id = federation_->addModel(path);
|
||||
if (fed_id.isEmpty()) continue;
|
||||
accepted_paths << path;
|
||||
accepted_fed_ids << fed_id;
|
||||
}
|
||||
loadModelsFromPaths(accepted_paths, accepted_fed_ids);
|
||||
updateWindowTitle();
|
||||
}
|
||||
|
||||
QString MainWindow::formatElapsed(qint64 ms) const {
|
||||
return (ms >= 1000)
|
||||
? QString::number(ms / 1000.0, 'f', 2) + " s"
|
||||
: QString::number(ms) + " ms";
|
||||
}
|
||||
|
||||
void MainWindow::onAddFiles() {
|
||||
panels::add_model::AddModelDialog dialog(this);
|
||||
if (dialog.exec() != QDialog::Accepted) return;
|
||||
|
||||
QStringList paths;
|
||||
switch (dialog.selectedMode()) {
|
||||
case panels::add_model::SourceMode::IfcFile: {
|
||||
QFileDialog file_dialog(this, "Add IFC Files");
|
||||
file_dialog.setFileMode(QFileDialog::ExistingFiles);
|
||||
file_dialog.setNameFilter("IFC Files (*.ifc);;All Files (*)");
|
||||
file_dialog.setOption(QFileDialog::DontUseNativeDialog, true);
|
||||
if (file_dialog.exec() == QDialog::Accepted) {
|
||||
paths = file_dialog.selectedFiles();
|
||||
}
|
||||
break;
|
||||
}
|
||||
case panels::add_model::SourceMode::IfcDatabase: {
|
||||
QFileDialog database_dialog(this, "Add IFC Databases");
|
||||
database_dialog.setFileMode(QFileDialog::Directory);
|
||||
database_dialog.setOption(QFileDialog::ShowDirsOnly, true);
|
||||
database_dialog.setOption(QFileDialog::DontResolveSymlinks, true);
|
||||
database_dialog.setOption(QFileDialog::DontUseNativeDialog, true);
|
||||
if (auto* list = database_dialog.findChild<QListView*>("listView")) {
|
||||
list->setSelectionMode(QAbstractItemView::ExtendedSelection);
|
||||
}
|
||||
if (auto* tree = database_dialog.findChild<QTreeView*>()) {
|
||||
tree->setSelectionMode(QAbstractItemView::ExtendedSelection);
|
||||
}
|
||||
if (database_dialog.exec() == QDialog::Accepted) {
|
||||
paths = database_dialog.selectedFiles();
|
||||
}
|
||||
break;
|
||||
}
|
||||
case panels::add_model::SourceMode::GeometryOnly: {
|
||||
QFileDialog file_dialog(this, "Add Geometry Only");
|
||||
file_dialog.setFileMode(QFileDialog::ExistingFiles);
|
||||
file_dialog.setNameFilter("IFC Viewer Cache (*.ifcview);;All Files (*)");
|
||||
file_dialog.setOption(QFileDialog::DontUseNativeDialog, true);
|
||||
if (file_dialog.exec() == QDialog::Accepted) {
|
||||
paths = file_dialog.selectedFiles();
|
||||
}
|
||||
break;
|
||||
}
|
||||
case panels::add_model::SourceMode::None:
|
||||
return;
|
||||
}
|
||||
addFiles(paths);
|
||||
}
|
||||
|
||||
void MainWindow::clearScene() {
|
||||
if (viewport_widget_) viewport_widget_->viewport()->setSelectedObjectId(0);
|
||||
session_state_->setSelectedObjectId(0);
|
||||
session_state_->notifySelectionChanged();
|
||||
|
||||
const auto model_ids = session_state_->modelIds();
|
||||
for (uint32_t mid : model_ids) {
|
||||
viewport_widget_->viewport()->removeModel(mid);
|
||||
loader_->removeModel(mid);
|
||||
}
|
||||
|
||||
session_state_->clearModelMappings();
|
||||
element_registry_->clear();
|
||||
session_state_->notifyModelsChanged();
|
||||
}
|
||||
|
||||
bool MainWindow::confirmDiscardIfDirty() {
|
||||
if (!federation_->isDirty()) return true;
|
||||
const auto result = QMessageBox::question(
|
||||
this, "Unsaved Project",
|
||||
"The current project has unsaved changes. Save before continuing?",
|
||||
QMessageBox::Save | QMessageBox::Discard | QMessageBox::Cancel,
|
||||
QMessageBox::Save);
|
||||
if (result == QMessageBox::Cancel) return false;
|
||||
if (result == QMessageBox::Save) return saveProject();
|
||||
return true;
|
||||
}
|
||||
|
||||
void MainWindow::loadModelsFromPaths(const QStringList& paths, const QStringList& fed_ids) {
|
||||
if (paths.isEmpty()) return;
|
||||
const auto ids = loader_->addFiles(paths);
|
||||
for (int i = 0; i < paths.size() && i < static_cast<int>(ids.size()) && i < fed_ids.size(); ++i) {
|
||||
session_state_->setModelMapping(fed_ids[i], ids[i]);
|
||||
}
|
||||
session_state_->notifyModelsChanged();
|
||||
}
|
||||
|
||||
bool MainWindow::openProject(const QString& path) {
|
||||
if (loader_ && loader_->isLoading()) {
|
||||
QMessageBox::information(
|
||||
this, "Open Project",
|
||||
"Wait until the current model load finishes before opening another project.");
|
||||
return false;
|
||||
}
|
||||
if (!confirmDiscardIfDirty()) return false;
|
||||
|
||||
QStringList warnings;
|
||||
QString err;
|
||||
if (!federation_->load(path, &warnings, &err)) {
|
||||
QMessageBox::warning(this, "Open Project",
|
||||
QString("Could not open project:\n%1").arg(err));
|
||||
return false;
|
||||
}
|
||||
|
||||
clearScene();
|
||||
|
||||
QStringList paths;
|
||||
QStringList fed_ids;
|
||||
QStringList missing;
|
||||
for (const auto& model : federation_->models()) {
|
||||
if (model.source_kind != "local") continue;
|
||||
if (!QFileInfo::exists(model.source_path)) {
|
||||
missing << model.source_path;
|
||||
continue;
|
||||
}
|
||||
paths << model.source_path;
|
||||
fed_ids << model.id;
|
||||
}
|
||||
loadModelsFromPaths(paths, fed_ids);
|
||||
|
||||
for (const auto& msg : missing) {
|
||||
warnings << QString("Source not found, kept in project: %1").arg(msg);
|
||||
}
|
||||
if (!warnings.isEmpty()) {
|
||||
QMessageBox::warning(this, "Open Project",
|
||||
"Project opened with warnings:\n\n" + warnings.join("\n"));
|
||||
}
|
||||
|
||||
federation_->markClean();
|
||||
viewport_controller_->applyFederatedFalseOrigin();
|
||||
if (federation_->hasHomeView()) {
|
||||
const auto& hv = federation_->homeView();
|
||||
viewport_widget_->viewport()->setCamera(
|
||||
hv.target.x(), hv.target.y(), hv.target.z(), hv.distance, hv.yaw, hv.pitch);
|
||||
}
|
||||
updateWindowTitle();
|
||||
session_state_->setStatusMessage("Project", QFileInfo(path).fileName());
|
||||
session_state_->notifyProjectOpened(path);
|
||||
return true;
|
||||
}
|
||||
|
||||
bool MainWindow::saveProject() {
|
||||
if (federation_->filePath().isEmpty()) return saveProjectAs();
|
||||
|
||||
QString err;
|
||||
if (!federation_->save(federation_->filePath(), &err)) {
|
||||
QMessageBox::warning(this, "Save Project",
|
||||
QString("Could not save project:\n%1").arg(err));
|
||||
return false;
|
||||
}
|
||||
updateWindowTitle();
|
||||
session_state_->setStatusMessage("Project", QFileInfo(federation_->filePath()).fileName());
|
||||
return true;
|
||||
}
|
||||
|
||||
bool MainWindow::saveProjectAs() {
|
||||
QString suggested = federation_->filePath();
|
||||
if (suggested.isEmpty()) suggested = "project.ifcfed";
|
||||
|
||||
QFileDialog file_dialog(this, "Save Project As", suggested);
|
||||
file_dialog.setAcceptMode(QFileDialog::AcceptSave);
|
||||
file_dialog.setFileMode(QFileDialog::AnyFile);
|
||||
file_dialog.setNameFilter("IFC Federation (*.ifcfed);;All Files (*)");
|
||||
file_dialog.setOption(QFileDialog::DontUseNativeDialog, true);
|
||||
if (file_dialog.exec() != QDialog::Accepted) return false;
|
||||
QString path = file_dialog.selectedFiles().value(0);
|
||||
if (path.isEmpty()) return false;
|
||||
if (!path.endsWith(".ifcfed", Qt::CaseInsensitive)) path += ".ifcfed";
|
||||
|
||||
QString err;
|
||||
if (!federation_->save(path, &err)) {
|
||||
QMessageBox::warning(this, "Save Project",
|
||||
QString("Could not save project:\n%1").arg(err));
|
||||
return false;
|
||||
}
|
||||
updateWindowTitle();
|
||||
session_state_->setStatusMessage("Project", QFileInfo(path).fileName());
|
||||
return true;
|
||||
}
|
||||
|
||||
void MainWindow::updateWindowTitle() {
|
||||
const QString project_path = federation_ ? federation_->filePath() : QString();
|
||||
if (project_path.isEmpty() && (!federation_ || federation_->models().empty())) {
|
||||
@@ -649,97 +493,4 @@ void MainWindow::updateWindowTitle() {
|
||||
}
|
||||
}
|
||||
|
||||
void MainWindow::onNewProject() {
|
||||
if (loader_ && loader_->isLoading()) {
|
||||
QMessageBox::information(
|
||||
this, "New Project",
|
||||
"Wait until the current model load finishes before creating a new project.");
|
||||
return;
|
||||
}
|
||||
if (!confirmDiscardIfDirty()) return;
|
||||
clearScene();
|
||||
federation_->clear();
|
||||
viewport_controller_->applyFederatedFalseOrigin();
|
||||
updateWindowTitle();
|
||||
session_state_->setStatusMessage("Project", "Untitled");
|
||||
session_state_->notifyProjectReset();
|
||||
}
|
||||
|
||||
void MainWindow::onOpenProject() {
|
||||
QFileDialog file_dialog(this, "Open Project");
|
||||
file_dialog.setFileMode(QFileDialog::ExistingFile);
|
||||
file_dialog.setNameFilter("IFC Federation (*.ifcfed);;All Files (*)");
|
||||
file_dialog.setOption(QFileDialog::DontUseNativeDialog, true);
|
||||
if (file_dialog.exec() != QDialog::Accepted) return;
|
||||
const QString path = file_dialog.selectedFiles().value(0);
|
||||
if (path.isEmpty()) return;
|
||||
openProject(path);
|
||||
}
|
||||
|
||||
void MainWindow::onSaveProject() {
|
||||
saveProject();
|
||||
}
|
||||
|
||||
void MainWindow::onSaveProjectAs() {
|
||||
saveProjectAs();
|
||||
}
|
||||
|
||||
void MainWindow::onSetHomeView() {
|
||||
auto camera = viewport_widget_->viewport()->cameraState();
|
||||
Federation::HomeView home_view;
|
||||
home_view.target = camera.target;
|
||||
home_view.distance = camera.distance;
|
||||
home_view.yaw = camera.yaw;
|
||||
home_view.pitch = camera.pitch;
|
||||
federation_->setHomeView(home_view);
|
||||
updateWindowTitle();
|
||||
session_state_->setStatusMessage("Camera", "Home view updated");
|
||||
}
|
||||
|
||||
void MainWindow::onGoHomeView() {
|
||||
if (!federation_->hasHomeView()) {
|
||||
session_state_->setStatusMessage("Camera", "No home view set for this project");
|
||||
return;
|
||||
}
|
||||
|
||||
const auto& home_view = federation_->homeView();
|
||||
viewport_widget_->viewport()->setCamera(
|
||||
home_view.target.x(), home_view.target.y(), home_view.target.z(),
|
||||
home_view.distance, home_view.yaw, home_view.pitch);
|
||||
session_state_->setStatusMessage("Camera", "Home view restored");
|
||||
}
|
||||
|
||||
void MainWindow::onLoadStarted(uint32_t /*mid*/, QString display_name) {
|
||||
session_state_->setStatusMessage("Loading", display_name);
|
||||
}
|
||||
|
||||
void MainWindow::onLoadedFromSidecar(uint32_t mid, qint64 elapsed_ms) {
|
||||
session_state_->setStatusMessage(
|
||||
"Loaded",
|
||||
QString("%1 from cache in %2")
|
||||
.arg(loader_->displayName(mid))
|
||||
.arg(formatElapsed(elapsed_ms)));
|
||||
}
|
||||
|
||||
void MainWindow::onLoadedFromStream(uint32_t mid, qint64 elapsed_ms) {
|
||||
session_state_->setStatusMessage(
|
||||
"Loaded",
|
||||
QString("%1 streamed in %2")
|
||||
.arg(loader_->displayName(mid))
|
||||
.arg(formatElapsed(elapsed_ms)));
|
||||
}
|
||||
|
||||
void MainWindow::onLoadCancelled(uint32_t mid) {
|
||||
session_state_->setStatusMessage("Cancelled", loader_->displayName(mid));
|
||||
}
|
||||
|
||||
void MainWindow::onLoadError(uint32_t /*mid*/, QString message) {
|
||||
session_state_->setStatusMessage("Error", message);
|
||||
QMessageBox::warning(this, "IfcInterfaceMockup", message);
|
||||
}
|
||||
|
||||
void MainWindow::onAllLoadsFinished() {
|
||||
session_state_->setStatusMessage("Loaded", QString("%1 model(s)").arg(loader_->modelCount()));
|
||||
}
|
||||
|
||||
} // namespace ifcinterface::shell
|
||||
|
||||
+20
-43
@@ -34,15 +34,16 @@ class SceneLoader;
|
||||
namespace ifcinterface { class ElementRegistry; }
|
||||
namespace ifcinterface { class SessionState; }
|
||||
namespace ifcinterface::components { class TabBar; }
|
||||
namespace ifcinterface::panels::models { class ModelsPanelController; }
|
||||
namespace ifcinterface::panels::models { class ModelsPanelWidget; }
|
||||
namespace ifcinterface::panels::models { class ModelsPanelView; }
|
||||
namespace ifcinterface::panels::spatial_hierarchy { class SpatialHierarchyPanelWidget; }
|
||||
namespace ifcinterface::panels::spatial_hierarchy { class SpatialHierarchyPanelView; }
|
||||
namespace ifcinterface::panels::properties { class PropertiesPanelWidget; }
|
||||
namespace ifcinterface::panels::properties { class PropertiesPanelView; }
|
||||
namespace ifcinterface::panels::viewport { class ViewportController; }
|
||||
namespace ifcinterface::panels::viewport { class ViewportWidget; }
|
||||
namespace ifcinterface::modules::models { class ModelsPanelController; }
|
||||
namespace ifcinterface::modules::models { class ModelsPanel; }
|
||||
namespace ifcinterface::modules::models { class ModelsPanelView; }
|
||||
namespace ifcinterface::modules::project { class ProjectController; }
|
||||
namespace ifcinterface::modules::spatial_hierarchy { class SpatialHierarchyPanel; }
|
||||
namespace ifcinterface::modules::spatial_hierarchy { class SpatialHierarchyPanelView; }
|
||||
namespace ifcinterface::modules::properties { class PropertiesPanel; }
|
||||
namespace ifcinterface::modules::properties { class PropertiesPanelView; }
|
||||
namespace ifcinterface::modules::viewport { class ViewportController; }
|
||||
namespace ifcinterface::modules::viewport { class ViewportPanel; }
|
||||
|
||||
namespace ifcinterface::shell {
|
||||
|
||||
@@ -62,33 +63,8 @@ private:
|
||||
QWidget* buildNavigateRibbonPage();
|
||||
QWidget* buildInspectRibbonPage();
|
||||
QWidget* buildPanelsRibbonPage();
|
||||
void clearScene();
|
||||
bool confirmDiscardIfDirty();
|
||||
void loadModelsFromPaths(const QStringList& paths, const QStringList& fed_ids);
|
||||
bool openProject(const QString& path);
|
||||
bool saveProject();
|
||||
bool saveProjectAs();
|
||||
void updateWindowTitle();
|
||||
QToolButton* makeRibbonAction(const QString& text, const QString& icon_path);
|
||||
QWidget* makeRibbonGroup(const QString& title, const QList<QToolButton*>& buttons);
|
||||
QToolButton* makePanelToggle(const QString& text, QDockWidget* dock);
|
||||
void addFiles(const QStringList& paths);
|
||||
QString formatElapsed(qint64 ms) const;
|
||||
|
||||
private slots:
|
||||
void onAddFiles();
|
||||
void onLoadStarted(uint32_t mid, QString display_name);
|
||||
void onLoadedFromSidecar(uint32_t mid, qint64 elapsed_ms);
|
||||
void onLoadedFromStream(uint32_t mid, qint64 elapsed_ms);
|
||||
void onLoadCancelled(uint32_t mid);
|
||||
void onLoadError(uint32_t mid, QString message);
|
||||
void onAllLoadsFinished();
|
||||
void onSetHomeView();
|
||||
void onGoHomeView();
|
||||
void onNewProject();
|
||||
void onOpenProject();
|
||||
void onSaveProject();
|
||||
void onSaveProjectAs();
|
||||
|
||||
private:
|
||||
Federation* federation_ = nullptr;
|
||||
@@ -97,25 +73,26 @@ private:
|
||||
QLabel* status_perf_label_ = nullptr;
|
||||
ifcinterface::components::TabBar* ribbon_tabs_ = nullptr;
|
||||
QStackedWidget* ribbon_pages_ = nullptr;
|
||||
ifcinterface::panels::viewport::ViewportWidget* viewport_widget_ = nullptr;
|
||||
ifcinterface::panels::viewport::ViewportController* viewport_controller_ = nullptr;
|
||||
ifcinterface::modules::viewport::ViewportPanel* viewport_widget_ = nullptr;
|
||||
ifcinterface::modules::viewport::ViewportController* viewport_controller_ = nullptr;
|
||||
SceneLoader* loader_ = nullptr;
|
||||
ifcinterface::ElementRegistry* element_registry_ = nullptr;
|
||||
ifcinterface::SessionState* session_state_ = nullptr;
|
||||
ifcinterface::panels::models::ModelsPanelWidget* models_panel_ = nullptr;
|
||||
ifcinterface::panels::spatial_hierarchy::SpatialHierarchyPanelWidget* spatial_panel_ = nullptr;
|
||||
ifcinterface::modules::models::ModelsPanel* models_panel_ = nullptr;
|
||||
ifcinterface::modules::spatial_hierarchy::SpatialHierarchyPanel* spatial_panel_ = nullptr;
|
||||
QDockWidget* layers_panel_ = nullptr;
|
||||
ifcinterface::panels::properties::PropertiesPanelWidget* properties_panel_ = nullptr;
|
||||
ifcinterface::modules::properties::PropertiesPanel* properties_panel_ = nullptr;
|
||||
QDockWidget* stored_views_panel_ = nullptr;
|
||||
QDockWidget* search_panel_ = nullptr;
|
||||
QDockWidget* spreadsheet_panel_ = nullptr;
|
||||
QDockWidget* audit_panel_ = nullptr;
|
||||
QDockWidget* clash_panel_ = nullptr;
|
||||
QDockWidget* issues_panel_ = nullptr;
|
||||
ifcinterface::panels::models::ModelsPanelController* models_controller_ = nullptr;
|
||||
ifcinterface::panels::models::ModelsPanelView* models_view_ = nullptr;
|
||||
ifcinterface::panels::spatial_hierarchy::SpatialHierarchyPanelView* spatial_view_ = nullptr;
|
||||
ifcinterface::panels::properties::PropertiesPanelView* properties_view_ = nullptr;
|
||||
ifcinterface::modules::models::ModelsPanelController* models_controller_ = nullptr;
|
||||
ifcinterface::modules::models::ModelsPanelView* models_view_ = nullptr;
|
||||
ifcinterface::modules::project::ProjectController* project_controller_ = nullptr;
|
||||
ifcinterface::modules::spatial_hierarchy::SpatialHierarchyPanelView* spatial_view_ = nullptr;
|
||||
ifcinterface::modules::properties::PropertiesPanelView* properties_view_ = nullptr;
|
||||
};
|
||||
|
||||
} // namespace ifcinterface::shell
|
||||
|
||||
@@ -100,6 +100,10 @@ void SessionState::notifyProjectOpened(const QString& path) {
|
||||
emit projectOpened(path);
|
||||
}
|
||||
|
||||
void SessionState::notifyProjectSaved(const QString& path) {
|
||||
emit projectSaved(path);
|
||||
}
|
||||
|
||||
void SessionState::notifyProjectReset() {
|
||||
emit projectReset();
|
||||
}
|
||||
|
||||
@@ -64,10 +64,12 @@ public:
|
||||
void notifyFederationStructureChanged();
|
||||
void notifyVisibilityChanged();
|
||||
void notifyProjectOpened(const QString& path);
|
||||
void notifyProjectSaved(const QString& path);
|
||||
void notifyProjectReset();
|
||||
|
||||
signals:
|
||||
void projectOpened(const QString& path);
|
||||
void projectSaved(const QString& path);
|
||||
void projectReset();
|
||||
void modelsChanged();
|
||||
void federationStructureChanged();
|
||||
|
||||
@@ -32,15 +32,14 @@ namespace ifcinterface::components::buttons {
|
||||
|
||||
QToolButton* makeButton(const QString& text,
|
||||
const QString& icon_path,
|
||||
QWidget* parent,
|
||||
const QSize& minimum_size) {
|
||||
QWidget* parent) {
|
||||
auto* button = new QToolButton(parent);
|
||||
button->setObjectName("ribbonButton");
|
||||
button->setToolButtonStyle(Qt::ToolButtonTextUnderIcon);
|
||||
button->setIcon(components::icons::makeTintedSvgIcon(icon_path));
|
||||
button->setIcon(components::icons::makeAccentSvgIcon(icon_path));
|
||||
button->setIconSize(QSize(20, 20));
|
||||
button->setText(text);
|
||||
button->setMinimumSize(minimum_size);
|
||||
button->setMinimumSize(QSize(90, 68));
|
||||
button->setAutoRaise(false);
|
||||
return button;
|
||||
}
|
||||
|
||||
@@ -22,7 +22,6 @@
|
||||
#define IFCINTERFACE_COMPONENTS_BUTTONS_H
|
||||
|
||||
#include <QList>
|
||||
#include <QSize>
|
||||
|
||||
class QToolButton;
|
||||
class QWidget;
|
||||
@@ -31,8 +30,7 @@ namespace ifcinterface::components::buttons {
|
||||
|
||||
QToolButton* makeButton(const QString& text,
|
||||
const QString& icon_path,
|
||||
QWidget* parent,
|
||||
const QSize& minimum_size = QSize(90, 54));
|
||||
QWidget* parent);
|
||||
|
||||
QWidget* makeButtonGroup(const QString& title,
|
||||
const QList<QToolButton*>& buttons,
|
||||
|
||||
@@ -21,6 +21,7 @@
|
||||
#include "Dialog.h"
|
||||
|
||||
#include "Style.h"
|
||||
#include "Tabs.h"
|
||||
|
||||
#include <QDialog>
|
||||
#include <QFrame>
|
||||
@@ -48,26 +49,29 @@ Dialog::Dialog(QWidget* parent, bool scrollable)
|
||||
style::metrics::section_body_padding);
|
||||
frame_layout->setSpacing(0);
|
||||
|
||||
if (scrollable) {
|
||||
auto* scroll = new QScrollArea(frame);
|
||||
scroll->setWidgetResizable(true);
|
||||
scroll->setFrameShape(QFrame::NoFrame);
|
||||
auto* scroll = new QScrollArea(frame);
|
||||
scroll->setWidgetResizable(true);
|
||||
scroll->setFrameShape(QFrame::NoFrame);
|
||||
|
||||
auto* scroll_body = new QWidget(scroll);
|
||||
scroll_body->setObjectName("panelScrollBody");
|
||||
body_layout_ = new QVBoxLayout(scroll_body);
|
||||
body_layout_->setContentsMargins(0, 0, 0, 0);
|
||||
body_layout_->setSpacing(style::metrics::section_body_padding);
|
||||
auto* scroll_body = new QWidget(scroll);
|
||||
scroll_body->setObjectName("panelScrollBody");
|
||||
body_layout_ = new QVBoxLayout(scroll_body);
|
||||
body_layout_->setContentsMargins(0, 0, 0, 0);
|
||||
body_layout_->setSpacing(style::metrics::section_body_padding);
|
||||
body_layout_->setAlignment(Qt::AlignTop);
|
||||
|
||||
scroll->setWidget(scroll_body);
|
||||
frame_layout->addWidget(scroll);
|
||||
} else {
|
||||
auto* body = new QWidget(frame);
|
||||
body_layout_ = new QVBoxLayout(body);
|
||||
body_layout_->setContentsMargins(0, 0, 0, 0);
|
||||
body_layout_->setSpacing(style::metrics::section_body_padding);
|
||||
frame_layout->addWidget(body);
|
||||
}
|
||||
scroll->setWidget(scroll_body);
|
||||
frame_layout->addWidget(scroll, scrollable ? 1 : 0);
|
||||
|
||||
auto* footer = new QWidget(frame);
|
||||
footer_layout_ = new QVBoxLayout(footer);
|
||||
footer_layout_->setContentsMargins(style::metrics::section_body_padding,
|
||||
style::metrics::section_body_padding,
|
||||
style::metrics::section_body_padding,
|
||||
0);
|
||||
footer_layout_->setSpacing(style::metrics::section_body_padding);
|
||||
footer_layout_->setAlignment(Qt::AlignTop);
|
||||
frame_layout->addWidget(footer);
|
||||
|
||||
outer_layout->addWidget(frame);
|
||||
}
|
||||
@@ -76,4 +80,64 @@ void Dialog::addBodyWidget(QWidget* widget) {
|
||||
body_layout_->addWidget(widget);
|
||||
}
|
||||
|
||||
void Dialog::addFooterWidget(QWidget* widget) {
|
||||
footer_layout_->addWidget(widget);
|
||||
}
|
||||
|
||||
TabbedDialog::TabbedDialog(QWidget* parent)
|
||||
: QDialog(parent)
|
||||
{
|
||||
auto* outer_layout = new QVBoxLayout(this);
|
||||
outer_layout->setContentsMargins(style::metrics::padding,
|
||||
style::metrics::padding,
|
||||
style::metrics::padding,
|
||||
style::metrics::padding);
|
||||
outer_layout->setSpacing(0);
|
||||
|
||||
auto* frame = new QFrame(this);
|
||||
frame->setObjectName("panel");
|
||||
auto* frame_layout = new QVBoxLayout(frame);
|
||||
frame_layout->setContentsMargins(0,
|
||||
style::metrics::section_body_padding,
|
||||
0,
|
||||
style::metrics::section_body_padding);
|
||||
frame_layout->setSpacing(0);
|
||||
|
||||
tabs_ = new TabWidget(frame);
|
||||
frame_layout->addWidget(tabs_, 1);
|
||||
|
||||
auto* footer = new QWidget(frame);
|
||||
footer_layout_ = new QVBoxLayout(footer);
|
||||
footer_layout_->setContentsMargins(style::metrics::section_body_padding,
|
||||
style::metrics::section_body_padding,
|
||||
style::metrics::section_body_padding,
|
||||
0);
|
||||
footer_layout_->setSpacing(style::metrics::section_body_padding);
|
||||
footer_layout_->setAlignment(Qt::AlignTop);
|
||||
frame_layout->addWidget(footer);
|
||||
|
||||
outer_layout->addWidget(frame);
|
||||
}
|
||||
|
||||
void TabbedDialog::addTab(const QString& title, QWidget* widget) {
|
||||
auto* scroll = new QScrollArea(tabs_);
|
||||
scroll->setWidgetResizable(true);
|
||||
scroll->setFrameShape(QFrame::NoFrame);
|
||||
|
||||
auto* scroll_body = new QWidget(scroll);
|
||||
scroll_body->setObjectName("panelScrollBody");
|
||||
auto* layout = new QVBoxLayout(scroll_body);
|
||||
layout->setContentsMargins(0, 0, 0, 0);
|
||||
layout->setSpacing(style::metrics::section_body_padding);
|
||||
layout->setAlignment(Qt::AlignTop);
|
||||
layout->addWidget(widget);
|
||||
|
||||
scroll->setWidget(scroll_body);
|
||||
tabs_->addTab(scroll, title);
|
||||
}
|
||||
|
||||
void TabbedDialog::addFooterWidget(QWidget* widget) {
|
||||
footer_layout_->addWidget(widget);
|
||||
}
|
||||
|
||||
} // namespace ifcinterface::components
|
||||
|
||||
@@ -25,6 +25,7 @@
|
||||
|
||||
class QVBoxLayout;
|
||||
class QWidget;
|
||||
class QTabWidget;
|
||||
|
||||
namespace ifcinterface::components {
|
||||
|
||||
@@ -36,9 +37,25 @@ public:
|
||||
bool scrollable = false);
|
||||
|
||||
void addBodyWidget(QWidget* widget);
|
||||
void addFooterWidget(QWidget* widget);
|
||||
|
||||
private:
|
||||
QVBoxLayout* body_layout_ = nullptr;
|
||||
QVBoxLayout* footer_layout_ = nullptr;
|
||||
};
|
||||
|
||||
class TabbedDialog : public QDialog {
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
explicit TabbedDialog(QWidget* parent = nullptr);
|
||||
|
||||
void addTab(const QString& title, QWidget* widget);
|
||||
void addFooterWidget(QWidget* widget);
|
||||
|
||||
private:
|
||||
QTabWidget* tabs_ = nullptr;
|
||||
QVBoxLayout* footer_layout_ = nullptr;
|
||||
};
|
||||
|
||||
} // namespace ifcinterface::components
|
||||
|
||||
@@ -38,7 +38,10 @@ namespace {
|
||||
|
||||
class DockTitleBar : public QWidget {
|
||||
public:
|
||||
explicit DockTitleBar(const QString& title, bool has_settings = false, QWidget* parent = nullptr)
|
||||
explicit DockTitleBar(const QString& title,
|
||||
bool has_settings = false,
|
||||
std::function<void()> on_settings = {},
|
||||
QWidget* parent = nullptr)
|
||||
: QWidget(parent)
|
||||
{
|
||||
auto* layout = new QHBoxLayout(this);
|
||||
@@ -58,11 +61,8 @@ public:
|
||||
settings->setFixedSize(18, 18);
|
||||
settings->setObjectName("panelTitleButton");
|
||||
settings->setToolTip(QString("%1 settings").arg(title));
|
||||
connect(settings, &QToolButton::clicked, this, [this, title]() {
|
||||
auto* anchor = parentWidget();
|
||||
QMenu menu(anchor);
|
||||
menu.addAction(QString("%1 settings coming soon").arg(title));
|
||||
menu.exec(QCursor::pos());
|
||||
connect(settings, &QToolButton::clicked, this, [on_settings = std::move(on_settings)]() {
|
||||
if (on_settings) on_settings();
|
||||
});
|
||||
layout->addWidget(settings);
|
||||
}
|
||||
@@ -98,6 +98,7 @@ Panel::Panel(const QString& title, QWidget* content, QWidget* parent, bool has_s
|
||||
body_layout_ = new QVBoxLayout(scroll_body);
|
||||
body_layout_->setContentsMargins(0, 0, 0, 0);
|
||||
body_layout_->setSpacing(style::metrics::section_body_padding);
|
||||
body_layout_->setAlignment(Qt::AlignTop);
|
||||
|
||||
scroll->setWidget(scroll_body);
|
||||
frame_layout->addWidget(scroll);
|
||||
@@ -106,11 +107,12 @@ Panel::Panel(const QString& title, QWidget* content, QWidget* parent, bool has_s
|
||||
body_layout_ = new QVBoxLayout(body);
|
||||
body_layout_->setContentsMargins(0, 0, 0, 0);
|
||||
body_layout_->setSpacing(style::metrics::section_body_padding);
|
||||
body_layout_->setAlignment(Qt::AlignTop);
|
||||
frame_layout->addWidget(body);
|
||||
}
|
||||
|
||||
if (content) {
|
||||
body_layout_->addWidget(content);
|
||||
addBodyWidget(content);
|
||||
}
|
||||
outer_layout->addWidget(frame);
|
||||
|
||||
@@ -118,7 +120,9 @@ Panel::Panel(const QString& title, QWidget* content, QWidget* parent, bool has_s
|
||||
setFeatures(QDockWidget::DockWidgetMovable |
|
||||
QDockWidget::DockWidgetFloatable |
|
||||
QDockWidget::DockWidgetClosable);
|
||||
setTitleBarWidget(new DockTitleBar(title, has_settings, this));
|
||||
setTitleBarWidget(new DockTitleBar(title, has_settings, [this]() {
|
||||
emit settingsRequested();
|
||||
}, this));
|
||||
setWidget(outer);
|
||||
}
|
||||
|
||||
|
||||
@@ -41,6 +41,9 @@ public:
|
||||
void addBodyWidget(QWidget* widget);
|
||||
void clearBodyWidgets();
|
||||
|
||||
signals:
|
||||
void settingsRequested();
|
||||
|
||||
private:
|
||||
QVBoxLayout* body_layout_ = nullptr;
|
||||
};
|
||||
|
||||
@@ -24,6 +24,7 @@
|
||||
|
||||
#include <QFrame>
|
||||
#include <QHBoxLayout>
|
||||
#include <QSizePolicy>
|
||||
#include <QToolButton>
|
||||
#include <QVBoxLayout>
|
||||
|
||||
@@ -33,6 +34,7 @@ Section::Section(const QString& title, SectionHeaderMode header_mode, QWidget* p
|
||||
: QWidget(parent)
|
||||
{
|
||||
setObjectName("panelSection");
|
||||
setSizePolicy(QSizePolicy::Preferred, QSizePolicy::Maximum);
|
||||
auto* layout = new QVBoxLayout(this);
|
||||
layout->setContentsMargins(0, 0, 0, 0);
|
||||
layout->setSpacing(style::metrics::padding);
|
||||
@@ -59,6 +61,11 @@ Section::Section(const QString& title, SectionHeaderMode header_mode, QWidget* p
|
||||
connect(toggle_button_, &QToolButton::toggled, this, [this](bool expanded) {
|
||||
toggle_button_->setArrowType(expanded ? Qt::DownArrow : Qt::RightArrow);
|
||||
body_->setVisible(expanded);
|
||||
body_->setMaximumHeight(expanded ? QWIDGETSIZE_MAX : 0);
|
||||
body_->setMinimumHeight(0);
|
||||
setSizePolicy(QSizePolicy::Preferred,
|
||||
body_expanding_ && expanded ? QSizePolicy::Expanding : QSizePolicy::Maximum);
|
||||
updateGeometry();
|
||||
});
|
||||
}
|
||||
|
||||
@@ -70,6 +77,7 @@ Section::Section(const QString& title, SectionHeaderMode header_mode, QWidget* p
|
||||
style::metrics::section_body_padding,
|
||||
0);
|
||||
body_layout_->setSpacing(style::metrics::padding);
|
||||
body_->setMinimumHeight(0);
|
||||
layout->addWidget(body_);
|
||||
}
|
||||
|
||||
@@ -98,4 +106,13 @@ void Section::setExpanded(bool expanded) {
|
||||
toggle_button_->setChecked(expanded);
|
||||
}
|
||||
|
||||
void Section::setBodyExpanding(bool expanding) {
|
||||
body_expanding_ = expanding;
|
||||
body_->setSizePolicy(QSizePolicy::Preferred, expanding ? QSizePolicy::Expanding : QSizePolicy::Preferred);
|
||||
body_layout_->setStretch(0, expanding ? 1 : 0);
|
||||
setSizePolicy(QSizePolicy::Preferred,
|
||||
expanding && isExpanded() ? QSizePolicy::Expanding : QSizePolicy::Maximum);
|
||||
updateGeometry();
|
||||
}
|
||||
|
||||
} // namespace ifcinterface::components
|
||||
|
||||
@@ -46,12 +46,14 @@ public:
|
||||
void addHeaderWidget(QWidget* widget);
|
||||
bool isExpanded() const;
|
||||
void setExpanded(bool expanded);
|
||||
void setBodyExpanding(bool expanding);
|
||||
|
||||
private:
|
||||
QWidget* body_ = nullptr;
|
||||
QVBoxLayout* body_layout_ = nullptr;
|
||||
QHBoxLayout* header_layout_ = nullptr;
|
||||
QToolButton* toggle_button_ = nullptr;
|
||||
bool body_expanding_ = false;
|
||||
};
|
||||
|
||||
} // namespace ifcinterface::components
|
||||
|
||||
@@ -20,9 +20,12 @@
|
||||
|
||||
#include "Style.h"
|
||||
|
||||
#include "../InterfaceSettings.h"
|
||||
|
||||
namespace ifcinterface::components::style {
|
||||
|
||||
QString buildAppStyleSheet() {
|
||||
const auto& theme = ifcinterface::InterfaceSettings::instance();
|
||||
QString stylesheet = QStringLiteral(R"(
|
||||
QMainWindow#appWindow {
|
||||
background: ${app_background};
|
||||
@@ -47,15 +50,11 @@ QString buildAppStyleSheet() {
|
||||
background: ${app_background};
|
||||
color: ${primary_text};
|
||||
}
|
||||
QFrame#ribbonShell {
|
||||
background: ${ribbon_shell_background};
|
||||
border-bottom: 1px solid ${border};
|
||||
}
|
||||
QTabBar#appTabBar {
|
||||
background: ${ribbon_shell_background};
|
||||
background: ${tab_bar_background};
|
||||
}
|
||||
QTabBar#appTabBar::tab {
|
||||
background: transparent;
|
||||
background: ${tab_background};
|
||||
color: ${secondary_text};
|
||||
padding: 8px 14px;
|
||||
margin-right: 2px;
|
||||
@@ -66,10 +65,10 @@ QString buildAppStyleSheet() {
|
||||
border-bottom: 2px solid ${selection_background};
|
||||
}
|
||||
QTabBar#appTabBar::tab:hover {
|
||||
color: ${ribbon_tab_hover_text};
|
||||
color: ${hover_text};
|
||||
}
|
||||
QFrame#ribbonBand {
|
||||
background: ${ribbon_band_background};
|
||||
background: ${ribbon_background};
|
||||
border-top: 1px solid ${border};
|
||||
}
|
||||
QTabWidget#appTabWidget::pane {
|
||||
@@ -87,15 +86,13 @@ QString buildAppStyleSheet() {
|
||||
border-right: none;
|
||||
}
|
||||
QLabel#ribbonGroupLabel {
|
||||
font-size: 9px;
|
||||
font-size: ${font_small}px;
|
||||
font-weight: 600;
|
||||
letter-spacing: 0.08em;
|
||||
}
|
||||
QToolButton#ribbonButton {
|
||||
background: transparent;
|
||||
border: none;
|
||||
padding: 6px 4px 4px 4px;
|
||||
font-size: 11px;
|
||||
color: ${primary_text};
|
||||
}
|
||||
QToolButton#ribbonButton:hover {
|
||||
@@ -109,7 +106,6 @@ QString buildAppStyleSheet() {
|
||||
border-top: none;
|
||||
}
|
||||
QFrame#viewportFrame {
|
||||
background: ${viewport_background};
|
||||
border: 1px solid ${border};
|
||||
}
|
||||
QDockWidget {
|
||||
@@ -128,6 +124,7 @@ QString buildAppStyleSheet() {
|
||||
QSpinBox,
|
||||
QDoubleSpinBox,
|
||||
QCheckBox,
|
||||
QRadioButton,
|
||||
QPushButton,
|
||||
QToolButton {
|
||||
color: ${primary_text};
|
||||
@@ -143,18 +140,16 @@ QString buildAppStyleSheet() {
|
||||
}
|
||||
QLabel#panelTitleText {
|
||||
color: ${primary_text};
|
||||
font-size: 10px;
|
||||
font-weight: 700;
|
||||
letter-spacing: 0.08em;
|
||||
font-size: ${font_small}px;
|
||||
font-weight: 600;
|
||||
}
|
||||
QToolButton#panelTitleButton {
|
||||
color: ${panel_title_button};
|
||||
border: none;
|
||||
background: transparent;
|
||||
}
|
||||
QToolButton#panelTitleButton:hover {
|
||||
color: ${ribbon_tab_hover_text};
|
||||
background: ${panel_title_button_hover};
|
||||
color: ${hover_text};
|
||||
background: ${ribbon_button_hover};
|
||||
}
|
||||
QFrame#panel {
|
||||
background: ${panel_background};
|
||||
@@ -167,6 +162,12 @@ QString buildAppStyleSheet() {
|
||||
outline: none;
|
||||
gridline-color: ${border};
|
||||
}
|
||||
QTableWidget#modelCoordinatesTable {
|
||||
font-size: ${font_small}px;
|
||||
}
|
||||
QTableWidget#modelCoordinatesTable QHeaderView::section {
|
||||
font-size: ${font_small}px;
|
||||
}
|
||||
QTreeWidget::viewport, QListWidget::viewport, QTableWidget::viewport {
|
||||
background: ${panel_background};
|
||||
}
|
||||
@@ -203,11 +204,26 @@ QString buildAppStyleSheet() {
|
||||
background: ${control_background};
|
||||
border: 1px solid ${border};
|
||||
border-radius: ${panel_radius}px;
|
||||
padding: ${padding}px ${padding}px;
|
||||
padding: ${padding}px 24px ${padding}px ${padding}px;
|
||||
}
|
||||
QComboBox:focus {
|
||||
border: 1px solid ${control_border_focus};
|
||||
}
|
||||
QComboBox::drop-down {
|
||||
subcontrol-origin: padding;
|
||||
subcontrol-position: top right;
|
||||
width: 18px;
|
||||
border: none;
|
||||
background: transparent;
|
||||
}
|
||||
QComboBox::down-arrow {
|
||||
width: 0px;
|
||||
height: 0px;
|
||||
border-left: 5px solid transparent;
|
||||
border-right: 5px solid transparent;
|
||||
border-top: 6px solid ${secondary_text};
|
||||
margin-right: 4px;
|
||||
}
|
||||
QComboBox QAbstractItemView {
|
||||
background: ${panel_background};
|
||||
border: 1px solid ${border};
|
||||
@@ -218,11 +234,38 @@ QString buildAppStyleSheet() {
|
||||
background: ${control_background};
|
||||
border: 1px solid ${border};
|
||||
border-radius: ${panel_radius}px;
|
||||
padding: ${padding}px ${padding}px;
|
||||
padding: ${padding}px 24px ${padding}px ${padding}px;
|
||||
}
|
||||
QSpinBox:focus, QDoubleSpinBox:focus {
|
||||
border: 1px solid ${control_border_focus};
|
||||
}
|
||||
QSpinBox::up-button, QDoubleSpinBox::up-button,
|
||||
QSpinBox::down-button, QDoubleSpinBox::down-button {
|
||||
subcontrol-origin: padding;
|
||||
width: 18px;
|
||||
border: none;
|
||||
background: transparent;
|
||||
}
|
||||
QSpinBox::up-button, QDoubleSpinBox::up-button {
|
||||
subcontrol-position: top right;
|
||||
}
|
||||
QSpinBox::down-button, QDoubleSpinBox::down-button {
|
||||
subcontrol-position: bottom right;
|
||||
}
|
||||
QSpinBox::up-arrow, QDoubleSpinBox::up-arrow {
|
||||
width: 0px;
|
||||
height: 0px;
|
||||
border-left: 4px solid transparent;
|
||||
border-right: 4px solid transparent;
|
||||
border-bottom: 5px solid ${secondary_text};
|
||||
}
|
||||
QSpinBox::down-arrow, QDoubleSpinBox::down-arrow {
|
||||
width: 0px;
|
||||
height: 0px;
|
||||
border-left: 4px solid transparent;
|
||||
border-right: 4px solid transparent;
|
||||
border-top: 5px solid ${secondary_text};
|
||||
}
|
||||
QCheckBox {
|
||||
background: transparent;
|
||||
spacing: 8px;
|
||||
@@ -241,6 +284,29 @@ QString buildAppStyleSheet() {
|
||||
border: 1px solid ${selection_background};
|
||||
background: ${selection_background};
|
||||
}
|
||||
QRadioButton {
|
||||
background: transparent;
|
||||
color: ${primary_text};
|
||||
spacing: 8px;
|
||||
padding: 2px 0;
|
||||
}
|
||||
QRadioButton::indicator {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
border: 1px solid ${border};
|
||||
border-radius: 8px;
|
||||
background: ${control_background};
|
||||
}
|
||||
QRadioButton::indicator:hover {
|
||||
background: ${ribbon_button_hover};
|
||||
}
|
||||
QRadioButton::indicator:checked {
|
||||
border: 1px solid ${selection_background};
|
||||
background: ${selection_background};
|
||||
}
|
||||
QRadioButton::indicator:unchecked {
|
||||
background: ${control_background};
|
||||
}
|
||||
QPushButton {
|
||||
background: ${control_background};
|
||||
border: 1px solid ${border};
|
||||
@@ -371,7 +437,7 @@ QString buildAppStyleSheet() {
|
||||
margin: 0;
|
||||
}
|
||||
QToolButton#panelSectionHeaderButton:hover {
|
||||
color: ${ribbon_tab_hover_text};
|
||||
color: ${hover_text};
|
||||
}
|
||||
QToolButton#panelSectionHeaderButton::menu-indicator {
|
||||
image: none;
|
||||
@@ -383,13 +449,13 @@ QString buildAppStyleSheet() {
|
||||
padding: ${section_header_padding}px;
|
||||
}
|
||||
QToolButton#panelSectionFilterToggle:hover {
|
||||
background: ${panel_title_button_hover};
|
||||
background: ${ribbon_button_hover};
|
||||
}
|
||||
QWidget#panelSectionBody {
|
||||
background: transparent;
|
||||
}
|
||||
QLabel#keyValueValueLabel {
|
||||
color: ${key_value_value_text};
|
||||
color: ${primary_text};
|
||||
background: transparent;
|
||||
}
|
||||
)");
|
||||
@@ -397,34 +463,33 @@ QString buildAppStyleSheet() {
|
||||
stylesheet.replace("${panel_radius}", QString::number(metrics::panel_radius));
|
||||
stylesheet.replace("${padding}", QString::number(metrics::padding));
|
||||
stylesheet.replace("${section_header_padding}", QString::number(metrics::section_header_padding));
|
||||
stylesheet.replace("${font_small}", QString::number(typography::small));
|
||||
|
||||
stylesheet.replace("${app_background}", palette::app_background);
|
||||
stylesheet.replace("${border}", palette::border);
|
||||
stylesheet.replace("${selection_background}", palette::selection_background);
|
||||
stylesheet.replace("${selection_text}", palette::selection_text);
|
||||
stylesheet.replace("${ribbon_shell_background}", palette::ribbon_shell_background);
|
||||
stylesheet.replace("${ribbon_tab_hover_text}", palette::ribbon_tab_hover_text);
|
||||
stylesheet.replace("${ribbon_band_background}", palette::ribbon_band_background);
|
||||
stylesheet.replace("${ribbon_button_hover}", palette::ribbon_button_hover);
|
||||
stylesheet.replace("${ribbon_button_pressed}", palette::ribbon_button_pressed);
|
||||
stylesheet.replace("${viewport_shell_background}", palette::viewport_shell_background);
|
||||
stylesheet.replace("${viewport_background}", palette::viewport_background);
|
||||
stylesheet.replace("${panel_title_button}", palette::panel_title_button);
|
||||
stylesheet.replace("${panel_title_button_hover}", palette::panel_title_button_hover);
|
||||
stylesheet.replace("${panel_background}", palette::panel_background);
|
||||
stylesheet.replace("${control_background}", palette::control_background);
|
||||
stylesheet.replace("${control_border_focus}", palette::control_border_focus);
|
||||
stylesheet.replace("${box_background}", palette::box_background);
|
||||
stylesheet.replace("${scroll_handle}", palette::scroll_handle);
|
||||
stylesheet.replace("${scroll_handle_hover}", palette::scroll_handle_hover);
|
||||
stylesheet.replace("${status_background}", palette::status_background);
|
||||
stylesheet.replace("${section_header_background}", palette::section_header_background);
|
||||
stylesheet.replace("${key_value_value_text}", palette::key_value_value_text);
|
||||
stylesheet.replace("${app_background}", theme.color("app_background"));
|
||||
stylesheet.replace("${border}", theme.color("border"));
|
||||
stylesheet.replace("${selection_background}", theme.color("selection_background"));
|
||||
stylesheet.replace("${selection_text}", theme.color("selection_text"));
|
||||
stylesheet.replace("${tab_bar_background}", theme.color("tab_bar_background"));
|
||||
stylesheet.replace("${tab_background}", theme.color("tab_background"));
|
||||
stylesheet.replace("${hover_text}", theme.color("hover_text"));
|
||||
stylesheet.replace("${ribbon_background}", theme.color("ribbon_background"));
|
||||
stylesheet.replace("${ribbon_button_hover}", theme.color("ribbon_button_hover"));
|
||||
stylesheet.replace("${ribbon_button_pressed}", theme.color("ribbon_button_pressed"));
|
||||
stylesheet.replace("${viewport_shell_background}", theme.color("viewport_shell_background"));
|
||||
stylesheet.replace("${viewport_background}", theme.color("viewport_background"));
|
||||
stylesheet.replace("${panel_background}", theme.color("panel_background"));
|
||||
stylesheet.replace("${control_background}", theme.color("control_background"));
|
||||
stylesheet.replace("${control_border_focus}", theme.color("control_border_focus"));
|
||||
stylesheet.replace("${box_background}", theme.color("box_background"));
|
||||
stylesheet.replace("${scroll_handle}", theme.color("scroll_handle"));
|
||||
stylesheet.replace("${scroll_handle_hover}", theme.color("scroll_handle_hover"));
|
||||
stylesheet.replace("${status_background}", theme.color("status_background"));
|
||||
stylesheet.replace("${section_header_background}", theme.color("section_header_background"));
|
||||
|
||||
stylesheet.replace("${primary_text}", palette::primary_text);
|
||||
stylesheet.replace("${secondary_text}", palette::secondary_text);
|
||||
stylesheet.replace("${disabled_text}", palette::disabled_text);
|
||||
stylesheet.replace("${warning_text}", palette::warning_text);
|
||||
stylesheet.replace("${primary_text}", theme.color("primary_text"));
|
||||
stylesheet.replace("${secondary_text}", theme.color("secondary_text"));
|
||||
stylesheet.replace("${disabled_text}", theme.color("disabled_text"));
|
||||
stylesheet.replace("${warning_text}", theme.color("warning_text"));
|
||||
|
||||
return stylesheet;
|
||||
}
|
||||
|
||||
@@ -32,20 +32,24 @@ inline constexpr int panel_radius = 3;
|
||||
|
||||
} // namespace ifcinterface::components::style::metrics
|
||||
|
||||
namespace ifcinterface::components::style::typography {
|
||||
|
||||
inline constexpr int small = 10;
|
||||
|
||||
} // namespace ifcinterface::components::style::typography
|
||||
|
||||
namespace ifcinterface::components::style::palette {
|
||||
|
||||
inline constexpr auto app_background = "#26292f";
|
||||
inline constexpr auto border = "#3e444e";
|
||||
inline constexpr auto selection_background = "#39b54a";
|
||||
inline constexpr auto ribbon_shell_background = "#2d3138";
|
||||
inline constexpr auto ribbon_tab_hover_text = "#ffffff";
|
||||
inline constexpr auto ribbon_band_background = "#31353d";
|
||||
inline constexpr auto tab_bar_background = "#26292f";
|
||||
inline constexpr auto tab_background = "#31353d";
|
||||
inline constexpr auto ribbon_background = "#31353d";
|
||||
inline constexpr auto ribbon_button_hover = "#3a3f48";
|
||||
inline constexpr auto ribbon_button_pressed = "#24282f";
|
||||
inline constexpr auto viewport_shell_background = "#202329";
|
||||
inline constexpr auto viewport_background = "#1a1d22";
|
||||
inline constexpr auto panel_title_button = "#8e97a5";
|
||||
inline constexpr auto panel_title_button_hover = "#353a42";
|
||||
inline constexpr auto viewport_background = "#2e3338";
|
||||
inline constexpr auto panel_background = "#2b2f36";
|
||||
inline constexpr auto control_background = "#31353d";
|
||||
inline constexpr auto control_border_focus = "#5b6472";
|
||||
@@ -54,13 +58,13 @@ inline constexpr auto scroll_handle = "#525a67";
|
||||
inline constexpr auto scroll_handle_hover = "#697385";
|
||||
inline constexpr auto status_background = "#26292f";
|
||||
inline constexpr auto section_header_background = "#26292f";
|
||||
inline constexpr auto key_value_value_text = "#dce2eb";
|
||||
|
||||
inline constexpr auto primary_text = "#d0d5dd";
|
||||
inline constexpr auto secondary_text = "#9aa4b3";
|
||||
inline constexpr auto disabled_text = "#8f98a6";
|
||||
inline constexpr auto warning_text = "#e4b35a";
|
||||
inline constexpr auto selection_text = "#14161a";
|
||||
inline constexpr auto hover_text = "#ffffff";
|
||||
|
||||
} // namespace ifcinterface::components::style::palette
|
||||
|
||||
|
||||
@@ -20,6 +20,8 @@
|
||||
|
||||
#include "SvgIcon.h"
|
||||
|
||||
#include "../InterfaceSettings.h"
|
||||
|
||||
#include <QFile>
|
||||
#include <QPainter>
|
||||
#include <QRegularExpression>
|
||||
@@ -61,12 +63,24 @@ QIcon makeTintedSvgIcon(const QString& icon_path, const QString& normal,
|
||||
return icon;
|
||||
}
|
||||
|
||||
QIcon makeAccentSvgIcon(const QString& icon_path) {
|
||||
const auto& theme = ifcinterface::InterfaceSettings::instance();
|
||||
return makeTintedSvgIcon(icon_path,
|
||||
theme.color("icon_accent_color"),
|
||||
theme.color("icon_accent_active_color"),
|
||||
theme.color("icon_disabled_color"));
|
||||
}
|
||||
|
||||
QIcon makeSvgIcon(const QString& icon_path) {
|
||||
return makeTintedSvgIcon(icon_path, "#e7ebf2", "#ffffff", "#6f7988");
|
||||
const auto& theme = ifcinterface::InterfaceSettings::instance();
|
||||
return makeTintedSvgIcon(icon_path,
|
||||
theme.color("icon_color"),
|
||||
theme.color("icon_active_color"),
|
||||
theme.color("icon_disabled_color"));
|
||||
}
|
||||
|
||||
QPixmap makeSvgPixmap(const QString& icon_path, const QSize& size) {
|
||||
return renderTintedSvgPixmap(icon_path, "#e7ebf2", size);
|
||||
return renderTintedSvgPixmap(icon_path, ifcinterface::InterfaceSettings::instance().color("icon_color"), size);
|
||||
}
|
||||
|
||||
} // namespace ifcinterface::components::icons
|
||||
|
||||
@@ -33,6 +33,7 @@ QIcon makeTintedSvgIcon(const QString& icon_path,
|
||||
const QString& normal = "#39b54a",
|
||||
const QString& active = "#53c763",
|
||||
const QString& disabled = "#6f7988");
|
||||
QIcon makeAccentSvgIcon(const QString& icon_path);
|
||||
QIcon makeSvgIcon(const QString& icon_path);
|
||||
QPixmap makeSvgPixmap(const QString& icon_path, const QSize& size);
|
||||
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
<svg width="24" height="24" viewBox="0 0 24 24" stroke-width="1.5" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M3.52844 7.29346L9 10.3332" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M22 2L20 2M12 12L12 10C12 6.45478 14.3061 3.44817 17.5 2.39838" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M22 12L20 12M12 22L12 20C12 16.4548 14.3061 13.4482 17.5 12.3984" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M12 22L3.30861 17.1715C3.11813 17.0656 3 16.8649 3 16.647L2.99998 7.35304C2.99998 7.13514 3.11812 6.93437 3.3086 6.82855L11.7086 2.16188C11.8898 2.06121 12.1102 2.06121 12.2914 2.16188L15 3.66667" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M12 11.9999L3.52842 7.29346" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M12 21L12 12" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M15 13.5L15 4" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.1 KiB |
@@ -1,6 +0,0 @@
|
||||
<svg width="24" height="24" stroke-width="1.5" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M22.0028 3V7.49704C22.0028 7.77482 21.7776 8 21.4998 8V8C21.2999 8 21.12 7.88104 21.034 7.70059C19.4262 4.32948 15.9866 2 12.0028 2C6.81746 2 2.55391 5.94668 2.05219 11" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M6 16.4V9.39365C6 9.06228 6.26863 8.79365 6.6 8.79365H7.77283C7.97677 8.79365 8.16674 8.69006 8.2772 8.51863L9.7228 6.27502C9.83326 6.10359 10.0232 6 10.2272 6H13.7728C13.9768 6 14.1667 6.10359 14.2772 6.27502L15.7228 8.51863C15.8333 8.69006 16.0232 8.79365 16.2272 8.79365H17.4C17.7314 8.79365 18 9.06228 18 9.39365V16.4C18 16.7314 17.7314 17 17.4 17H6.6C6.26863 17 6 16.7314 6 16.4Z" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M12 14C13.1046 14 14 13.1046 14 12C14 10.8954 13.1046 10 12 10C10.8954 10 10 10.8954 10 12C10 13.1046 10.8954 14 12 14Z" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M2.05078 21V16.503C2.05078 16.2252 2.27596 16 2.55374 16V16C2.75366 16 2.93357 16.119 3.01963 16.2994C4.62737 19.6705 8.06703 22 12.0508 22C17.2361 22 21.4997 18.0533 22.0014 13" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 1.3 KiB |
@@ -22,8 +22,8 @@
|
||||
<file alias="building.svg">icons/building.svg</file>
|
||||
<file alias="cellar.svg">icons/cellar.svg</file>
|
||||
<file alias="perspective-view.svg">icons/perspective-view.svg</file>
|
||||
<file alias="rotate-camera-right.svg">icons/rotate-camera-right.svg</file>
|
||||
<file alias="drone.svg">icons/drone.svg</file>
|
||||
<file alias="cube-cut-with-curve.svg">icons/cube-cut-with-curve.svg</file>
|
||||
<file alias="folder-plus.svg">icons/folder-plus.svg</file>
|
||||
<file alias="folder-minus.svg">icons/folder-minus.svg</file>
|
||||
<file alias="folder.svg">icons/folder.svg</file>
|
||||
|
||||
+11
-9
@@ -19,6 +19,7 @@
|
||||
********************************************************************************/
|
||||
|
||||
#include "MainWindow.h"
|
||||
#include "InterfaceSettings.h"
|
||||
#include "components/Style.h"
|
||||
|
||||
#include <QApplication>
|
||||
@@ -30,14 +31,8 @@
|
||||
namespace {
|
||||
|
||||
void installUiFont() {
|
||||
const QString blender_font =
|
||||
"/home/dion/drive/blender/blender-5.1.0-linux-x64/5.1/datafiles/fonts/Inter.woff2";
|
||||
|
||||
int font_id = QFontDatabase::addApplicationFont(blender_font);
|
||||
if (font_id < 0) {
|
||||
font_id = QFontDatabase::addApplicationFont(
|
||||
":/fonts/DMSans-VariableFont_opsz,wght.ttf");
|
||||
}
|
||||
const int font_id = QFontDatabase::addApplicationFont(
|
||||
":/fonts/DMSans-VariableFont_opsz,wght.ttf");
|
||||
QString family;
|
||||
if (font_id >= 0) {
|
||||
const QStringList families = QFontDatabase::applicationFontFamilies(font_id);
|
||||
@@ -71,7 +66,14 @@ int main(int argc, char* argv[]) {
|
||||
parser.process(app);
|
||||
|
||||
installUiFont();
|
||||
app.setStyleSheet(ifcinterface::components::style::buildAppStyleSheet());
|
||||
const auto applyStyle = [&app]() {
|
||||
app.setStyleSheet(ifcinterface::components::style::buildAppStyleSheet());
|
||||
};
|
||||
applyStyle();
|
||||
QObject::connect(&ifcinterface::InterfaceSettings::instance(),
|
||||
&ifcinterface::InterfaceSettings::themeChanged,
|
||||
&app,
|
||||
applyStyle);
|
||||
|
||||
ifcinterface::shell::MainWindow window;
|
||||
window.show();
|
||||
|
||||
+7
-8
@@ -18,7 +18,7 @@
|
||||
* *
|
||||
********************************************************************************/
|
||||
|
||||
#include "Dialog.h"
|
||||
#include "AddModelDialog.h"
|
||||
|
||||
#include "../../components/Dialog.h"
|
||||
#include "../../components/Buttons.h"
|
||||
@@ -31,7 +31,7 @@
|
||||
#include <QToolButton>
|
||||
#include <QVBoxLayout>
|
||||
|
||||
namespace ifcinterface::panels::add_model {
|
||||
namespace ifcinterface::modules::models {
|
||||
|
||||
namespace {
|
||||
|
||||
@@ -65,7 +65,6 @@ AddModelDialog::AddModelDialog(QWidget* parent)
|
||||
setObjectName("appDialog");
|
||||
setWindowTitle("Add Model");
|
||||
setModal(true);
|
||||
setStyleSheet(components::style::buildAppStyleSheet());
|
||||
setupUi();
|
||||
}
|
||||
|
||||
@@ -90,7 +89,7 @@ void AddModelDialog::setupUi() {
|
||||
row->setContentsMargins(0, 0, 0, 0);
|
||||
row->setSpacing(components::style::metrics::padding);
|
||||
|
||||
auto* add_ifc = components::buttons::makeButton("Add IFC File", ":/icons/cube.svg", choices, QSize(90, 68));
|
||||
auto* add_ifc = components::buttons::makeButton("Add IFC File", ":/icons/cube.svg", choices);
|
||||
connect(add_ifc, &QToolButton::clicked, this, [this]() {
|
||||
selected_mode_ = SourceMode::IfcFile;
|
||||
accept();
|
||||
@@ -100,7 +99,7 @@ void AddModelDialog::setupUi() {
|
||||
"Add IFC files and load both geometry and data.",
|
||||
default_description));
|
||||
|
||||
auto* add_database = components::buttons::makeButton("Add IFC\nDatabase", ":/icons/database.svg", choices, QSize(90, 68));
|
||||
auto* add_database = components::buttons::makeButton("Add IFC\nDatabase", ":/icons/database.svg", choices);
|
||||
connect(add_database, &QToolButton::clicked, this, [this]() {
|
||||
selected_mode_ = SourceMode::IfcDatabase;
|
||||
accept();
|
||||
@@ -110,7 +109,7 @@ void AddModelDialog::setupUi() {
|
||||
"Add IFC RDB databases for optimised performance",
|
||||
default_description));
|
||||
|
||||
auto* add_geometry = components::buttons::makeButton("Add Geometry", ":/icons/cube-bandage.svg", choices, QSize(90, 68));
|
||||
auto* add_geometry = components::buttons::makeButton("Add Geometry", ":/icons/cube-bandage.svg", choices);
|
||||
connect(add_geometry, &QToolButton::clicked, this, [this]() {
|
||||
selected_mode_ = SourceMode::GeometryOnly;
|
||||
accept();
|
||||
@@ -120,7 +119,7 @@ void AddModelDialog::setupUi() {
|
||||
"Add pure geometry for fast visualisation",
|
||||
default_description));
|
||||
|
||||
auto* convert_database = components::buttons::makeButton("Convert IFC File\nto Database", ":/icons/database-restore.svg", choices, QSize(90, 68));
|
||||
auto* convert_database = components::buttons::makeButton("Convert IFC File\nto Database", ":/icons/database-restore.svg", choices);
|
||||
connect(convert_database, &QToolButton::clicked, this, [description]() {
|
||||
description->setText("IFC-to-database conversion is coming soon.");
|
||||
});
|
||||
@@ -137,4 +136,4 @@ void AddModelDialog::setupUi() {
|
||||
addBodyWidget(choices_section);
|
||||
}
|
||||
|
||||
} // namespace ifcinterface::panels::add_model
|
||||
} // namespace ifcinterface::modules::models
|
||||
+2
-2
@@ -23,7 +23,7 @@
|
||||
|
||||
#include "../../components/Dialog.h"
|
||||
|
||||
namespace ifcinterface::panels::add_model {
|
||||
namespace ifcinterface::modules::models {
|
||||
|
||||
enum class SourceMode {
|
||||
None,
|
||||
@@ -45,6 +45,6 @@ private:
|
||||
SourceMode selected_mode_ = SourceMode::None;
|
||||
};
|
||||
|
||||
} // namespace ifcinterface::panels::add_model
|
||||
} // namespace ifcinterface::modules::models
|
||||
|
||||
#endif
|
||||
@@ -0,0 +1,229 @@
|
||||
// This file was generated with the assistance of an AI coding tool.
|
||||
/********************************************************************************
|
||||
* *
|
||||
* This file is part of IfcOpenShell. *
|
||||
* *
|
||||
* IfcOpenShell is free software: you can redistribute it and/or modify *
|
||||
* it under the terms of the Lesser GNU General Public License as published by *
|
||||
* the Free Software Foundation, either version 3.0 of the License, or *
|
||||
* (at your option) any later version. *
|
||||
* *
|
||||
* IfcOpenShell is distributed in the hope that it will be useful, *
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of *
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the *
|
||||
* Lesser GNU General Public License for more details. *
|
||||
* *
|
||||
* You should have received a copy of the Lesser GNU General Public License *
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>. *
|
||||
* *
|
||||
********************************************************************************/
|
||||
|
||||
#include "Controller.h"
|
||||
|
||||
#include "SettingsDialog.h"
|
||||
#include "Panel.h"
|
||||
|
||||
#include "../../ElementRegistry.h"
|
||||
#include "../../SessionState.h"
|
||||
#include "AddModelDialog.h"
|
||||
#include "../../../ifcviewer/Federation.h"
|
||||
#include "../../../ifcviewer/SceneLoader.h"
|
||||
#include "../../../ifcviewer/ViewportWindow.h"
|
||||
|
||||
#include <QFileDialog>
|
||||
#include <QListView>
|
||||
#include <QMessageBox>
|
||||
#include <QTreeView>
|
||||
|
||||
namespace ifcinterface::modules::models {
|
||||
|
||||
ModelsPanelController::ModelsPanelController(QWidget* host,
|
||||
ModelsPanel* widget,
|
||||
ifcinterface::SessionState* session_state,
|
||||
ViewportWindow* viewport,
|
||||
QObject* parent)
|
||||
: QObject(parent)
|
||||
, host_(host)
|
||||
, widget_(widget)
|
||||
, session_state_(session_state)
|
||||
, viewport_(viewport)
|
||||
{
|
||||
connect(widget_, &ModelsPanel::visibilityToggleRequested, this,
|
||||
[this](ItemKind kind, const QString& id) {
|
||||
Federation* federation = session_state_->federation();
|
||||
if (kind == ItemKind::Group) {
|
||||
if (const Federation::Group* group = federation->findGroupById(id)) {
|
||||
federation->setGroupVisible(id, !group->visible);
|
||||
session_state_->notifyVisibilityChanged();
|
||||
session_state_->setStatusMessage("Models", group->visible ? "Group hidden" : "Group shown");
|
||||
}
|
||||
} else {
|
||||
if (const Federation::Model* model = federation->findById(id)) {
|
||||
federation->setModelVisible(id, !model->visible);
|
||||
session_state_->notifyVisibilityChanged();
|
||||
session_state_->setStatusMessage("Models", model->visible ? "Model hidden" : "Model shown");
|
||||
}
|
||||
}
|
||||
});
|
||||
connect(widget_, &ModelsPanel::addGroupRequested, this,
|
||||
[this](const QString& parent_group_id, const QString& name) {
|
||||
session_state_->federation()->addGroup(name, parent_group_id);
|
||||
session_state_->notifyFederationStructureChanged();
|
||||
session_state_->setStatusMessage("Models", "Group added");
|
||||
});
|
||||
connect(widget_, &ModelsPanel::removeGroupRequested, this,
|
||||
[this](const QString& id) {
|
||||
session_state_->federation()->removeGroup(id);
|
||||
session_state_->notifyFederationStructureChanged();
|
||||
session_state_->setStatusMessage("Models", "Group removed");
|
||||
});
|
||||
connect(widget_, &ModelsPanel::removeModelRequested, this,
|
||||
[this](const QString& id) {
|
||||
removeLoadedModel(id);
|
||||
session_state_->setStatusMessage("Models", "Model removed");
|
||||
});
|
||||
connect(widget_, &components::Panel::settingsRequested, this, [this]() {
|
||||
openSettings();
|
||||
});
|
||||
}
|
||||
|
||||
void ModelsPanelController::bindLoader(SceneLoader* loader) {
|
||||
connect(loader, &SceneLoader::loadStarted, this,
|
||||
[this](uint32_t /*mid*/, const QString& display_name) {
|
||||
session_state_->setStatusMessage("Loading", display_name);
|
||||
});
|
||||
connect(loader, &SceneLoader::loadedFromSidecar, this,
|
||||
[this, loader](uint32_t mid, qint64 elapsed_ms) {
|
||||
session_state_->setStatusMessage(
|
||||
"Loaded",
|
||||
QString("%1 from cache in %2")
|
||||
.arg(loader->displayName(mid))
|
||||
.arg(formatElapsed(elapsed_ms)));
|
||||
});
|
||||
connect(loader, &SceneLoader::loadedFromStream, this,
|
||||
[this, loader](uint32_t mid, qint64 elapsed_ms) {
|
||||
session_state_->setStatusMessage(
|
||||
"Loaded",
|
||||
QString("%1 streamed in %2")
|
||||
.arg(loader->displayName(mid))
|
||||
.arg(formatElapsed(elapsed_ms)));
|
||||
});
|
||||
connect(loader, &SceneLoader::loadCancelled, this,
|
||||
[this, loader](uint32_t mid) {
|
||||
session_state_->setStatusMessage("Cancelled", loader->displayName(mid));
|
||||
});
|
||||
connect(loader, &SceneLoader::loadError, this,
|
||||
[this, host = host_](uint32_t /*mid*/, const QString& message) {
|
||||
session_state_->setStatusMessage("Error", message);
|
||||
QMessageBox::warning(host, "IfcInterfaceMockup", message);
|
||||
});
|
||||
connect(loader, &SceneLoader::allLoadsFinished, this,
|
||||
[this, loader]() {
|
||||
session_state_->setStatusMessage("Loaded", QString("%1 model(s)").arg(loader->modelCount()));
|
||||
});
|
||||
}
|
||||
|
||||
void ModelsPanelController::addFiles() {
|
||||
modules::models::AddModelDialog dialog(host_);
|
||||
if (dialog.exec() != QDialog::Accepted) return;
|
||||
|
||||
QStringList paths;
|
||||
switch (dialog.selectedMode()) {
|
||||
case modules::models::SourceMode::IfcFile: {
|
||||
QFileDialog file_dialog(host_, "Add IFC Files");
|
||||
file_dialog.setFileMode(QFileDialog::ExistingFiles);
|
||||
file_dialog.setNameFilter("IFC Files (*.ifc);;All Files (*)");
|
||||
file_dialog.setOption(QFileDialog::DontUseNativeDialog, true);
|
||||
if (file_dialog.exec() == QDialog::Accepted) {
|
||||
paths = file_dialog.selectedFiles();
|
||||
}
|
||||
break;
|
||||
}
|
||||
case modules::models::SourceMode::IfcDatabase: {
|
||||
QFileDialog database_dialog(host_, "Add IFC Databases");
|
||||
database_dialog.setFileMode(QFileDialog::Directory);
|
||||
database_dialog.setOption(QFileDialog::ShowDirsOnly, true);
|
||||
database_dialog.setOption(QFileDialog::DontResolveSymlinks, true);
|
||||
database_dialog.setOption(QFileDialog::DontUseNativeDialog, true);
|
||||
if (auto* list = database_dialog.findChild<QListView*>("listView")) {
|
||||
list->setSelectionMode(QAbstractItemView::ExtendedSelection);
|
||||
}
|
||||
if (auto* tree = database_dialog.findChild<QTreeView*>()) {
|
||||
tree->setSelectionMode(QAbstractItemView::ExtendedSelection);
|
||||
}
|
||||
if (database_dialog.exec() == QDialog::Accepted) {
|
||||
paths = database_dialog.selectedFiles();
|
||||
}
|
||||
break;
|
||||
}
|
||||
case modules::models::SourceMode::GeometryOnly: {
|
||||
QFileDialog file_dialog(host_, "Add Geometry Only");
|
||||
file_dialog.setFileMode(QFileDialog::ExistingFiles);
|
||||
file_dialog.setNameFilter("IFC Viewer Cache (*.ifcview);;All Files (*)");
|
||||
file_dialog.setOption(QFileDialog::DontUseNativeDialog, true);
|
||||
if (file_dialog.exec() == QDialog::Accepted) {
|
||||
paths = file_dialog.selectedFiles();
|
||||
}
|
||||
break;
|
||||
}
|
||||
case modules::models::SourceMode::None:
|
||||
return;
|
||||
}
|
||||
|
||||
addFiles(paths);
|
||||
}
|
||||
|
||||
void ModelsPanelController::addFiles(const QStringList& paths) {
|
||||
QStringList accepted_paths;
|
||||
QStringList accepted_fed_ids;
|
||||
for (const auto& path : paths) {
|
||||
const QString fed_id = session_state_->federation()->addModel(path);
|
||||
if (fed_id.isEmpty()) continue;
|
||||
accepted_paths << path;
|
||||
accepted_fed_ids << fed_id;
|
||||
}
|
||||
|
||||
loadModels(accepted_paths, accepted_fed_ids);
|
||||
}
|
||||
|
||||
void ModelsPanelController::loadModels(const QStringList& paths, const QStringList& fed_ids) {
|
||||
if (paths.isEmpty()) return;
|
||||
|
||||
const auto ids = session_state_->loader()->addFiles(paths);
|
||||
for (int i = 0; i < paths.size() && i < static_cast<int>(ids.size()) && i < fed_ids.size(); ++i) {
|
||||
session_state_->setModelMapping(fed_ids[i], ids[i]);
|
||||
}
|
||||
session_state_->notifyModelsChanged();
|
||||
}
|
||||
|
||||
void ModelsPanelController::removeLoadedModel(const QString& fed_id) {
|
||||
const uint32_t mid = session_state_->modelIdForFedId(fed_id);
|
||||
if (mid == 0) {
|
||||
session_state_->federation()->removeModel(fed_id);
|
||||
return;
|
||||
}
|
||||
if (session_state_->loader()->isLoadingModel(mid)) return;
|
||||
|
||||
viewport_->setSelectedObjectId(0);
|
||||
session_state_->setSelectedObjectId(0);
|
||||
session_state_->federation()->removeModel(fed_id);
|
||||
viewport_->removeModel(mid);
|
||||
session_state_->loader()->removeModel(mid);
|
||||
session_state_->elementRegistry()->removeModel(mid);
|
||||
session_state_->removeModelMappingByFedId(fed_id);
|
||||
session_state_->notifySelectionChanged();
|
||||
session_state_->notifyModelsChanged();
|
||||
}
|
||||
|
||||
void ModelsPanelController::openSettings() {
|
||||
SettingsDialog dialog(session_state_, host_);
|
||||
dialog.exec();
|
||||
}
|
||||
|
||||
QString ModelsPanelController::formatElapsed(qint64 ms) const {
|
||||
return (ms >= 1000)
|
||||
? QString::number(ms / 1000.0, 'f', 2) + " s"
|
||||
: QString::number(ms) + " ms";
|
||||
}
|
||||
|
||||
} // namespace ifcinterface::modules::models
|
||||
@@ -0,0 +1,66 @@
|
||||
// This file was generated with the assistance of an AI coding tool.
|
||||
/********************************************************************************
|
||||
* *
|
||||
* This file is part of IfcOpenShell. *
|
||||
* *
|
||||
* IfcOpenShell is free software: you can redistribute it and/or modify *
|
||||
* it under the terms of the Lesser GNU General Public License as published by *
|
||||
* the Free Software Foundation, either version 3.0 of the License, or *
|
||||
* (at your option) any later version. *
|
||||
* *
|
||||
* IfcOpenShell is distributed in the hope that it will be useful, *
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of *
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the *
|
||||
* Lesser GNU General Public License for more details. *
|
||||
* *
|
||||
* You should have received a copy of the Lesser GNU General Public License *
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>. *
|
||||
* *
|
||||
********************************************************************************/
|
||||
|
||||
#ifndef IFCINTERFACE_PANELS_MODELSPANELCONTROLLER_H
|
||||
#define IFCINTERFACE_PANELS_MODELSPANELCONTROLLER_H
|
||||
|
||||
#include "Types.h"
|
||||
|
||||
#include <QObject>
|
||||
#include <QStringList>
|
||||
|
||||
class QWidget;
|
||||
namespace ifcinterface { class SessionState; }
|
||||
class ViewportWindow;
|
||||
class SceneLoader;
|
||||
|
||||
namespace ifcinterface::modules::models {
|
||||
|
||||
class ModelsPanel;
|
||||
|
||||
class ModelsPanelController : public QObject {
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
explicit ModelsPanelController(QWidget* host,
|
||||
ModelsPanel* widget,
|
||||
ifcinterface::SessionState* session_state,
|
||||
ViewportWindow* viewport,
|
||||
QObject* parent = nullptr);
|
||||
|
||||
void bindLoader(SceneLoader* loader);
|
||||
void addFiles();
|
||||
void addFiles(const QStringList& paths);
|
||||
void loadModels(const QStringList& paths, const QStringList& fed_ids);
|
||||
void removeLoadedModel(const QString& fed_id);
|
||||
void openSettings();
|
||||
|
||||
private:
|
||||
QString formatElapsed(qint64 ms) const;
|
||||
|
||||
QWidget* host_ = nullptr;
|
||||
ModelsPanel* widget_ = nullptr;
|
||||
ifcinterface::SessionState* session_state_ = nullptr;
|
||||
ViewportWindow* viewport_ = nullptr;
|
||||
};
|
||||
|
||||
} // namespace ifcinterface::modules::models
|
||||
|
||||
#endif
|
||||
@@ -0,0 +1,139 @@
|
||||
// This file was generated with the assistance of an AI coding tool.
|
||||
/********************************************************************************
|
||||
* *
|
||||
* This file is part of IfcOpenShell. *
|
||||
* *
|
||||
* IfcOpenShell is free software: you can redistribute it and/or modify *
|
||||
* it under the terms of the Lesser GNU General Public License as published by *
|
||||
* the Free Software Foundation, either version 3.0 of the License, or *
|
||||
* (at your option) any later version. *
|
||||
* *
|
||||
* IfcOpenShell is distributed in the hope that it will be useful, *
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of *
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the *
|
||||
* Lesser GNU General Public License for more details. *
|
||||
* *
|
||||
* You should have received a copy of the Lesser GNU General Public License *
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>. *
|
||||
* *
|
||||
********************************************************************************/
|
||||
|
||||
#include "Panel.h"
|
||||
|
||||
#include "../../components/Section.h"
|
||||
#include "../../components/SvgIcon.h"
|
||||
|
||||
#include <QHeaderView>
|
||||
#include <QInputDialog>
|
||||
#include <QLineEdit>
|
||||
#include <QMenu>
|
||||
#include <QSizePolicy>
|
||||
#include <QTreeWidget>
|
||||
#include <QTreeWidgetItem>
|
||||
|
||||
namespace ifcinterface::modules::models {
|
||||
|
||||
ModelsPanel::ModelsPanel(QWidget* parent)
|
||||
: components::Panel("Models", nullptr, parent, true)
|
||||
{
|
||||
auto* section = new components::Section("", components::SectionHeaderMode::Hidden, this);
|
||||
section->setBodyExpanding(true);
|
||||
tree_ = new QTreeWidget(section);
|
||||
tree_->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Expanding);
|
||||
tree_->setColumnCount(2);
|
||||
tree_->setHeaderLabels({"Model", ""});
|
||||
tree_->setIconSize(QSize(16, 16));
|
||||
tree_->setSelectionMode(QAbstractItemView::ExtendedSelection);
|
||||
tree_->setContextMenuPolicy(Qt::CustomContextMenu);
|
||||
tree_->setUniformRowHeights(true);
|
||||
tree_->header()->setStretchLastSection(false);
|
||||
tree_->header()->setSectionResizeMode(0, QHeaderView::Stretch);
|
||||
tree_->header()->setSectionResizeMode(1, QHeaderView::Fixed);
|
||||
tree_->header()->resizeSection(1, 28);
|
||||
tree_->header()->hide();
|
||||
section->addBodyWidget(tree_);
|
||||
addBodyWidget(section);
|
||||
|
||||
connect(tree_, &QTreeWidget::itemClicked, this, [this](QTreeWidgetItem* item, int column) {
|
||||
if (!item || column != 1) return;
|
||||
emit visibilityToggleRequested(
|
||||
static_cast<ItemKind>(item->data(0, Qt::UserRole).toInt()),
|
||||
item->data(0, Qt::UserRole + 1).toString());
|
||||
});
|
||||
|
||||
connect(tree_, &QTreeWidget::customContextMenuRequested, this, [this](const QPoint& pos) {
|
||||
auto* item = tree_->itemAt(pos);
|
||||
QMenu menu(tree_);
|
||||
if (!item) {
|
||||
menu.addAction(components::icons::makeSvgIcon(":/icons/folder-plus.svg"), "Add Group", [this]() {
|
||||
bool ok = false;
|
||||
const QString name = QInputDialog::getText(
|
||||
this, "New Group", "Group name:", QLineEdit::Normal, "Group", &ok);
|
||||
if (ok && !name.isEmpty()) {
|
||||
emit addGroupRequested(QString(), name);
|
||||
}
|
||||
});
|
||||
menu.exec(tree_->viewport()->mapToGlobal(pos));
|
||||
return;
|
||||
}
|
||||
|
||||
const auto kind = static_cast<ItemKind>(item->data(0, Qt::UserRole).toInt());
|
||||
const QString id = item->data(0, Qt::UserRole + 1).toString();
|
||||
|
||||
menu.addAction(components::icons::makeSvgIcon(":/icons/eye.svg"), "Toggle Visibility", [this, kind, id]() {
|
||||
emit visibilityToggleRequested(kind, id);
|
||||
});
|
||||
|
||||
if (kind == ItemKind::Group) {
|
||||
menu.addAction(components::icons::makeSvgIcon(":/icons/folder-plus.svg"), "Add Group", [this, id]() {
|
||||
bool ok = false;
|
||||
const QString name = QInputDialog::getText(
|
||||
this, "New Group", "Group name:", QLineEdit::Normal, "Group", &ok);
|
||||
if (ok && !name.isEmpty()) {
|
||||
emit addGroupRequested(id, name);
|
||||
}
|
||||
});
|
||||
menu.addAction(components::icons::makeSvgIcon(":/icons/folder-minus.svg"), "Remove Group", [this, id]() {
|
||||
emit removeGroupRequested(id);
|
||||
});
|
||||
} else {
|
||||
menu.addAction(components::icons::makeSvgIcon(":/icons/minus-square.svg"), "Remove Model", [this, id]() {
|
||||
emit removeModelRequested(id);
|
||||
});
|
||||
}
|
||||
menu.exec(tree_->viewport()->mapToGlobal(pos));
|
||||
});
|
||||
}
|
||||
|
||||
void ModelsPanel::setNodes(const QList<TreeNode>& nodes) {
|
||||
tree_->clear();
|
||||
const bool has_hierarchy = std::any_of(nodes.begin(), nodes.end(), [](const TreeNode& node) {
|
||||
return !node.children.isEmpty() || node.kind == ItemKind::Group;
|
||||
});
|
||||
tree_->setRootIsDecorated(has_hierarchy);
|
||||
for (const auto& node : nodes) {
|
||||
addNode(tree_->invisibleRootItem(), node);
|
||||
}
|
||||
tree_->expandAll();
|
||||
}
|
||||
|
||||
void ModelsPanel::addNode(QTreeWidgetItem* parent, const TreeNode& node) {
|
||||
auto* item = new QTreeWidgetItem(parent, {node.name, ""});
|
||||
item->setData(0, Qt::UserRole, static_cast<int>(node.kind));
|
||||
item->setData(0, Qt::UserRole + 1, node.id);
|
||||
item->setSizeHint(0, QSize(0, 24));
|
||||
|
||||
if (node.kind == ItemKind::Group) {
|
||||
item->setIcon(0, components::icons::makeSvgIcon(":/icons/folder.svg"));
|
||||
item->setIcon(1, components::icons::makeSvgIcon(node.visible ? ":/icons/eye.svg" : ":/icons/eye-closed.svg"));
|
||||
} else {
|
||||
item->setIcon(0, components::icons::makeSvgIcon(":/icons/cube.svg"));
|
||||
item->setIcon(1, components::icons::makeSvgIcon(node.visible ? ":/icons/eye-solid.svg" : ":/icons/eye-closed.svg"));
|
||||
}
|
||||
|
||||
for (const auto& child : node.children) {
|
||||
addNode(item, child);
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace ifcinterface::modules::models
|
||||
@@ -0,0 +1,54 @@
|
||||
// This file was generated with the assistance of an AI coding tool.
|
||||
/********************************************************************************
|
||||
* *
|
||||
* This file is part of IfcOpenShell. *
|
||||
* *
|
||||
* IfcOpenShell is free software: you can redistribute it and/or modify *
|
||||
* it under the terms of the Lesser GNU General Public License as published by *
|
||||
* the Free Software Foundation, either version 3.0 of the License, or *
|
||||
* (at your option) any later version. *
|
||||
* *
|
||||
* IfcOpenShell is distributed in the hope that it will be useful, *
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of *
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the *
|
||||
* Lesser GNU General Public License for more details. *
|
||||
* *
|
||||
* You should have received a copy of the Lesser GNU General Public License *
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>. *
|
||||
* *
|
||||
********************************************************************************/
|
||||
|
||||
#ifndef IFCINTERFACE_MODULES_MODELS_PANEL_H
|
||||
#define IFCINTERFACE_MODULES_MODELS_PANEL_H
|
||||
|
||||
#include "Types.h"
|
||||
|
||||
#include "../../components/Panel.h"
|
||||
|
||||
class QTreeWidget;
|
||||
class QTreeWidgetItem;
|
||||
|
||||
namespace ifcinterface::modules::models {
|
||||
|
||||
class ModelsPanel : public components::Panel {
|
||||
Q_OBJECT
|
||||
public:
|
||||
explicit ModelsPanel(QWidget* parent = nullptr);
|
||||
|
||||
void setNodes(const QList<TreeNode>& nodes);
|
||||
|
||||
signals:
|
||||
void visibilityToggleRequested(ItemKind kind, const QString& id);
|
||||
void addGroupRequested(const QString& parent_group_id, const QString& name);
|
||||
void removeGroupRequested(const QString& id);
|
||||
void removeModelRequested(const QString& id);
|
||||
|
||||
private:
|
||||
void addNode(QTreeWidgetItem* parent, const TreeNode& node);
|
||||
|
||||
QTreeWidget* tree_ = nullptr;
|
||||
};
|
||||
|
||||
} // namespace ifcinterface::modules::models
|
||||
|
||||
#endif
|
||||
@@ -0,0 +1,527 @@
|
||||
// This file was generated with the assistance of an AI coding tool.
|
||||
/********************************************************************************
|
||||
* *
|
||||
* This file is part of IfcOpenShell. *
|
||||
* *
|
||||
* IfcOpenShell is free software: you can redistribute it and/or modify *
|
||||
* it under the terms of the Lesser GNU General Public License as published by *
|
||||
* the Free Software Foundation, either version 3.0 of the License, or *
|
||||
* (at your option) any later version. *
|
||||
* *
|
||||
* IfcOpenShell is distributed in the hope that it will be useful, *
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of *
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the *
|
||||
* Lesser GNU General Public License for more details. *
|
||||
* *
|
||||
* You should have received a copy of the Lesser GNU General Public License *
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>. *
|
||||
* *
|
||||
********************************************************************************/
|
||||
|
||||
#include "SettingsDialog.h"
|
||||
|
||||
#include "../../SessionState.h"
|
||||
#include "../../../ifcviewer/Federation.h"
|
||||
#include "../../../ifcviewer/SceneLoader.h"
|
||||
#include "../../components/Section.h"
|
||||
#include "../../components/Style.h"
|
||||
#include "../../components/SvgIcon.h"
|
||||
#include "../../components/Tabs.h"
|
||||
|
||||
#include <QComboBox>
|
||||
#include <QDialogButtonBox>
|
||||
#include <QFormLayout>
|
||||
#include <QHeaderView>
|
||||
#include <QHBoxLayout>
|
||||
#include <QLabel>
|
||||
#include <QLineEdit>
|
||||
#include <QPushButton>
|
||||
#include <QRegularExpression>
|
||||
#include <QSizePolicy>
|
||||
#include <QShowEvent>
|
||||
#include <QSignalBlocker>
|
||||
#include <QScrollBar>
|
||||
#include <QTableWidget>
|
||||
#include <QTableWidgetItem>
|
||||
#include <QVBoxLayout>
|
||||
|
||||
#include <cmath>
|
||||
|
||||
namespace ifcinterface::modules::models {
|
||||
|
||||
namespace {
|
||||
|
||||
struct UnitChoice {
|
||||
const char* label;
|
||||
const char* prefix;
|
||||
const char* name;
|
||||
};
|
||||
|
||||
const UnitChoice kUnitChoices[] = {
|
||||
{"Metres (m)", "", "METRE"},
|
||||
{"Millimetres (mm)", "MILLI", "METRE"},
|
||||
{"Centimetres (cm)", "CENTI", "METRE"},
|
||||
{"Kilometres (km)", "KILO", "METRE"},
|
||||
{"Feet (ft)", "", "foot"},
|
||||
{"Inches (in)", "", "inch"},
|
||||
{"Yards (yd)", "", "yard"},
|
||||
{"Miles (mi)", "", "mile"},
|
||||
};
|
||||
|
||||
QLineEdit* makeNumericField(QWidget* parent, const QString& placeholder = {}) {
|
||||
auto* field = new QLineEdit(parent);
|
||||
field->setPlaceholderText(placeholder);
|
||||
field->setMaximumWidth(96);
|
||||
return field;
|
||||
}
|
||||
|
||||
QWidget* makeEqualThirdsRow(QWidget* parent, QLineEdit* a, QLineEdit* b, QLineEdit* c) {
|
||||
auto* row = new QWidget(parent);
|
||||
auto* layout = new QHBoxLayout(row);
|
||||
layout->setContentsMargins(0, 0, 0, 0);
|
||||
layout->setSpacing(components::style::metrics::padding);
|
||||
for (QLineEdit* field : {a, b, c}) {
|
||||
field->setMaximumWidth(QWIDGETSIZE_MAX);
|
||||
field->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Fixed);
|
||||
layout->addWidget(field, 1);
|
||||
}
|
||||
return row;
|
||||
}
|
||||
|
||||
QString formatNumber(double value) {
|
||||
return QString::number(value, 'f', 6);
|
||||
}
|
||||
|
||||
double parseNumber(QLineEdit* field) {
|
||||
bool ok = false;
|
||||
const double value = field->text().toDouble(&ok);
|
||||
return ok ? value : 0.0;
|
||||
}
|
||||
|
||||
QString formatVector3(const Eigen::Vector3d& value) {
|
||||
return QString("%1, %2, %3").arg(formatNumber(value.x()), formatNumber(value.y()), formatNumber(value.z()));
|
||||
}
|
||||
|
||||
Eigen::Vector3d parseVector3(const QString& text) {
|
||||
const QStringList parts = text.split(QRegularExpression("[,\\s]+"), Qt::SkipEmptyParts);
|
||||
if (parts.size() != 3) return Eigen::Vector3d::Zero();
|
||||
|
||||
bool ok_x = false;
|
||||
bool ok_y = false;
|
||||
bool ok_z = false;
|
||||
const double x = parts[0].toDouble(&ok_x);
|
||||
const double y = parts[1].toDouble(&ok_y);
|
||||
const double z = parts[2].toDouble(&ok_z);
|
||||
if (!ok_x || !ok_y || !ok_z) return Eigen::Vector3d::Zero();
|
||||
return Eigen::Vector3d(x, y, z);
|
||||
}
|
||||
|
||||
QString formatAngleDms(double degrees) {
|
||||
const double absolute = std::fabs(degrees);
|
||||
const int d = static_cast<int>(absolute);
|
||||
const double minutes_total = (absolute - static_cast<double>(d)) * 60.0;
|
||||
const int m = static_cast<int>(minutes_total);
|
||||
const double s = (minutes_total - static_cast<double>(m)) * 60.0;
|
||||
const QString sign = degrees < 0.0 ? "-" : "";
|
||||
return QString("%1%2° %3' %4\"").arg(sign).arg(d).arg(m, 2, 10, QChar('0')).arg(formatNumber(s));
|
||||
}
|
||||
|
||||
QLabel* makeReadOnlyValue(QWidget* parent) {
|
||||
auto* label = new QLabel(parent);
|
||||
label->setTextInteractionFlags(Qt::TextSelectableByMouse);
|
||||
label->setWordWrap(true);
|
||||
return label;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
SettingsDialog::SettingsDialog(ifcinterface::SessionState* session_state, QWidget* parent)
|
||||
: components::TabbedDialog(parent)
|
||||
, session_state_(session_state)
|
||||
, federation_(session_state ? session_state->federation() : nullptr)
|
||||
, loader_(session_state ? session_state->loader() : nullptr)
|
||||
{
|
||||
setObjectName("appDialog");
|
||||
setWindowTitle("Model Settings");
|
||||
setModal(true);
|
||||
resize(980, 560);
|
||||
setupUi();
|
||||
}
|
||||
|
||||
void SettingsDialog::showEvent(QShowEvent* event) {
|
||||
federation_ = session_state_ ? session_state_->federation() : federation_;
|
||||
loader_ = session_state_ ? session_state_->loader() : loader_;
|
||||
syncFromFederation();
|
||||
populateModelTable();
|
||||
QDialog::showEvent(event);
|
||||
}
|
||||
|
||||
void SettingsDialog::setupUi() {
|
||||
auto* federation_tab = new QWidget(this);
|
||||
auto* federation_layout = new QVBoxLayout(federation_tab);
|
||||
federation_layout->setContentsMargins(0, 0, 0, 0);
|
||||
federation_layout->setSpacing(components::style::metrics::padding);
|
||||
federation_layout->setAlignment(Qt::AlignTop);
|
||||
|
||||
auto* federation_unit_section =
|
||||
new components::Section("Federation Unit", components::SectionHeaderMode::Visible, federation_tab);
|
||||
auto* unit_hint = new QLabel(
|
||||
"All measurements, federated false origins, and destination model transforms will be interpreted in this unit.",
|
||||
federation_unit_section);
|
||||
unit_hint->setProperty("textRole", "secondary");
|
||||
unit_hint->setWordWrap(true);
|
||||
auto* federation_unit_body = new QWidget(federation_unit_section);
|
||||
auto* federation_unit_form = new QFormLayout(federation_unit_body);
|
||||
federation_unit_form->setContentsMargins(0, 0, 0, 0);
|
||||
federation_unit_form->setHorizontalSpacing(16);
|
||||
federation_unit_form->setVerticalSpacing(10);
|
||||
unit_combo_ = new QComboBox(federation_unit_body);
|
||||
for (const auto& uc : kUnitChoices) {
|
||||
QStringList data;
|
||||
data << QString::fromUtf8(uc.prefix) << QString::fromUtf8(uc.name);
|
||||
unit_combo_->addItem(uc.label, data);
|
||||
}
|
||||
federation_unit_form->addRow("Unit", unit_combo_);
|
||||
federation_unit_section->addBodyWidget(unit_hint);
|
||||
federation_unit_section->addBodyWidget(federation_unit_body);
|
||||
|
||||
auto* origin_section =
|
||||
new components::Section("Federated False Origin", components::SectionHeaderMode::Visible, federation_tab);
|
||||
auto* origin_hint = new QLabel(
|
||||
"Nominate a false origin and project north to use when viewing the federation of models.",
|
||||
origin_section);
|
||||
origin_hint->setProperty("textRole", "secondary");
|
||||
origin_hint->setWordWrap(true);
|
||||
auto* origin_body = new QWidget(origin_section);
|
||||
auto* origin_form = new QFormLayout(origin_body);
|
||||
origin_form->setContentsMargins(0, 0, 0, 0);
|
||||
origin_form->setHorizontalSpacing(16);
|
||||
origin_form->setVerticalSpacing(10);
|
||||
xyz_x_ = makeNumericField(origin_body, "X");
|
||||
xyz_y_ = makeNumericField(origin_body, "Y");
|
||||
xyz_z_ = makeNumericField(origin_body, "Z");
|
||||
rz_deg_ = makeNumericField(origin_body, "deg");
|
||||
origin_form->addRow("XYZ", makeEqualThirdsRow(origin_body, xyz_x_, xyz_y_, xyz_z_));
|
||||
origin_form->addRow("Z rotation (°)", rz_deg_);
|
||||
origin_section->addBodyWidget(origin_hint);
|
||||
origin_section->addBodyWidget(origin_body);
|
||||
|
||||
federation_layout->addWidget(federation_unit_section);
|
||||
federation_layout->addWidget(origin_section);
|
||||
federation_layout->addStretch(1);
|
||||
|
||||
auto* model_tab = new QWidget(this);
|
||||
auto* model_layout = new QVBoxLayout(model_tab);
|
||||
model_layout->setContentsMargins(0, 0, 0, 0);
|
||||
model_layout->setSpacing(components::style::metrics::padding);
|
||||
model_layout->setAlignment(Qt::AlignTop);
|
||||
|
||||
auto* georef_section =
|
||||
new components::Section("Selected Model Georeferencing", components::SectionHeaderMode::Visible, model_tab);
|
||||
auto* georef_body = new QWidget(georef_section);
|
||||
auto* georef_layout = new QHBoxLayout(georef_body);
|
||||
georef_layout->setContentsMargins(0, 0, 0, 0);
|
||||
georef_layout->setSpacing(16);
|
||||
georef_layout->setAlignment(Qt::AlignTop);
|
||||
georef_present_value_ = makeReadOnlyValue(georef_body);
|
||||
georef_type_value_ = makeReadOnlyValue(georef_body);
|
||||
georef_easting_value_ = makeReadOnlyValue(georef_body);
|
||||
georef_northing_value_ = makeReadOnlyValue(georef_body);
|
||||
georef_height_value_ = makeReadOnlyValue(georef_body);
|
||||
georef_x_axis_abscissa_value_ = makeReadOnlyValue(georef_body);
|
||||
georef_x_axis_ordinate_value_ = makeReadOnlyValue(georef_body);
|
||||
georef_rotation_dd_value_ = makeReadOnlyValue(georef_body);
|
||||
georef_rotation_dms_value_ = makeReadOnlyValue(georef_body);
|
||||
georef_scale_value_ = makeReadOnlyValue(georef_body);
|
||||
georef_factor_x_value_ = makeReadOnlyValue(georef_body);
|
||||
georef_factor_y_value_ = makeReadOnlyValue(georef_body);
|
||||
georef_factor_z_value_ = makeReadOnlyValue(georef_body);
|
||||
|
||||
auto* georef_col_1 = new QFormLayout();
|
||||
georef_col_1->setContentsMargins(0, 0, 0, 0);
|
||||
georef_col_1->setHorizontalSpacing(12);
|
||||
georef_col_1->setVerticalSpacing(8);
|
||||
georef_col_1->addRow("Georeferenced", georef_present_value_);
|
||||
georef_col_1->addRow("Coordinate Operation", georef_type_value_);
|
||||
georef_col_1->addRow("Easting", georef_easting_value_);
|
||||
georef_col_1->addRow("Northing", georef_northing_value_);
|
||||
georef_col_1->addRow("OrthogonalHeight", georef_height_value_);
|
||||
|
||||
auto* georef_col_2 = new QFormLayout();
|
||||
georef_col_2->setContentsMargins(0, 0, 0, 0);
|
||||
georef_col_2->setHorizontalSpacing(12);
|
||||
georef_col_2->setVerticalSpacing(8);
|
||||
georef_col_2->addRow("XAxisAbscissa", georef_x_axis_abscissa_value_);
|
||||
georef_col_2->addRow("XAxisOrdinate", georef_x_axis_ordinate_value_);
|
||||
georef_col_2->addRow("Rotation (DD)", georef_rotation_dd_value_);
|
||||
georef_col_2->addRow("Rotation (DMS)", georef_rotation_dms_value_);
|
||||
|
||||
auto* georef_col_3 = new QFormLayout();
|
||||
georef_col_3->setContentsMargins(0, 0, 0, 0);
|
||||
georef_col_3->setHorizontalSpacing(12);
|
||||
georef_col_3->setVerticalSpacing(8);
|
||||
georef_col_3->addRow("Scale", georef_scale_value_);
|
||||
georef_col_3->addRow("FactorX", georef_factor_x_value_);
|
||||
georef_col_3->addRow("FactorY", georef_factor_y_value_);
|
||||
georef_col_3->addRow("FactorZ", georef_factor_z_value_);
|
||||
|
||||
auto* georef_col_1_widget = new QWidget(georef_body);
|
||||
georef_col_1_widget->setLayout(georef_col_1);
|
||||
auto* georef_col_2_widget = new QWidget(georef_body);
|
||||
georef_col_2_widget->setLayout(georef_col_2);
|
||||
auto* georef_col_3_widget = new QWidget(georef_body);
|
||||
georef_col_3_widget->setLayout(georef_col_3);
|
||||
|
||||
georef_layout->addWidget(georef_col_1_widget, 1);
|
||||
georef_layout->addWidget(georef_col_2_widget, 1);
|
||||
georef_layout->addWidget(georef_col_3_widget, 1);
|
||||
georef_section->addBodyWidget(georef_body);
|
||||
|
||||
auto* table_section =
|
||||
new components::Section("Model Transformations", components::SectionHeaderMode::Visible, model_tab);
|
||||
auto* table_body = new QWidget(table_section);
|
||||
auto* table_layout = new QVBoxLayout(table_body);
|
||||
table_layout->setContentsMargins(0, 0, 0, 0);
|
||||
table_layout->setSpacing(components::style::metrics::padding);
|
||||
|
||||
model_table_ = new QTableWidget(table_body);
|
||||
model_table_->setObjectName("modelCoordinatesTable");
|
||||
model_table_->setColumnCount(6);
|
||||
model_table_->setHorizontalHeaderLabels(
|
||||
{"Model", "From", "From Point", "To Point", "Rotate", "Pivot Point"});
|
||||
model_table_->verticalHeader()->setVisible(false);
|
||||
model_table_->horizontalHeader()->setStretchLastSection(false);
|
||||
model_table_->horizontalHeader()->setSectionsMovable(false);
|
||||
model_table_->horizontalHeader()->setSectionsClickable(true);
|
||||
model_table_->horizontalHeader()->setSectionResizeMode(QHeaderView::Interactive);
|
||||
model_table_->horizontalHeader()->resizeSection(0, 180);
|
||||
model_table_->horizontalHeader()->resizeSection(1, 150);
|
||||
model_table_->horizontalHeader()->resizeSection(2, 180);
|
||||
model_table_->horizontalHeader()->resizeSection(3, 180);
|
||||
model_table_->horizontalHeader()->resizeSection(4, 180);
|
||||
model_table_->horizontalHeader()->resizeSection(5, 180);
|
||||
model_table_->setSelectionBehavior(QAbstractItemView::SelectRows);
|
||||
model_table_->setSelectionMode(QAbstractItemView::SingleSelection);
|
||||
model_table_->setShowGrid(true);
|
||||
model_table_->setWordWrap(false);
|
||||
model_table_->setEditTriggers(QAbstractItemView::DoubleClicked | QAbstractItemView::EditKeyPressed);
|
||||
model_table_->setAlternatingRowColors(false);
|
||||
model_table_->setVerticalScrollBarPolicy(Qt::ScrollBarAlwaysOff);
|
||||
|
||||
auto* table_hint = new QLabel(
|
||||
"This section lets you override model coordinates by specifying an optional translation from a point to "
|
||||
"another desired point, and an optional rotation.",
|
||||
table_section);
|
||||
table_hint->setProperty("textRole", "secondary");
|
||||
table_hint->setWordWrap(true);
|
||||
|
||||
table_layout->addWidget(model_table_, 1);
|
||||
table_section->addBodyWidget(table_hint);
|
||||
table_section->addBodyWidget(table_body);
|
||||
model_layout->addWidget(georef_section);
|
||||
model_layout->addWidget(table_section);
|
||||
|
||||
addTab("Federation", federation_tab);
|
||||
addTab("Model", model_tab);
|
||||
|
||||
connect(model_table_, &QTableWidget::currentCellChanged, this,
|
||||
[this](int /*current_row*/, int /*current_column*/, int /*previous_row*/, int /*previous_column*/) {
|
||||
updateSelectedModelGeoref();
|
||||
});
|
||||
|
||||
auto* buttons = new QDialogButtonBox(QDialogButtonBox::Ok | QDialogButtonBox::Cancel, this);
|
||||
if (auto* ok = buttons->button(QDialogButtonBox::Ok)) {
|
||||
ok->setText("OK");
|
||||
ok->setIcon(components::icons::makeSvgIcon(":/icons/check.svg"));
|
||||
}
|
||||
if (auto* cancel = buttons->button(QDialogButtonBox::Cancel)) {
|
||||
cancel->setText("Cancel");
|
||||
cancel->setIcon(components::icons::makeSvgIcon(":/icons/xmark-circle.svg"));
|
||||
}
|
||||
connect(buttons, &QDialogButtonBox::accepted, this, [this]() { onAccepted(); });
|
||||
connect(buttons, &QDialogButtonBox::rejected, this, &QDialog::reject);
|
||||
|
||||
addFooterWidget(buttons);
|
||||
}
|
||||
|
||||
void SettingsDialog::syncFromFederation() {
|
||||
if (!federation_) return;
|
||||
|
||||
const auto& cfg = federation_->config();
|
||||
int idx = -1;
|
||||
for (int i = 0; i < unit_combo_->count(); ++i) {
|
||||
const QStringList data = unit_combo_->itemData(i).toStringList();
|
||||
if (data.size() == 2 &&
|
||||
data[0].toStdString() == cfg.unit_prefix &&
|
||||
data[1].toStdString() == cfg.unit_name) {
|
||||
idx = i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
unit_combo_->setCurrentIndex(idx >= 0 ? idx : 0);
|
||||
|
||||
const auto& origin = federation_->federatedFalseOrigin();
|
||||
xyz_x_->setText(formatNumber(origin.xyz.x()));
|
||||
xyz_y_->setText(formatNumber(origin.xyz.y()));
|
||||
xyz_z_->setText(formatNumber(origin.xyz.z()));
|
||||
rz_deg_->setText(formatNumber(origin.rz_deg));
|
||||
}
|
||||
|
||||
void SettingsDialog::populateModelTable() {
|
||||
model_rows_.clear();
|
||||
const QSignalBlocker blocker(model_table_);
|
||||
model_table_->clearContents();
|
||||
model_table_->setRowCount(0);
|
||||
if (!federation_) return;
|
||||
|
||||
int row = 0;
|
||||
for (const auto& model : federation_->models()) {
|
||||
const auto& xf = model.model_transformation;
|
||||
model_table_->insertRow(row);
|
||||
|
||||
auto* model_item = new QTableWidgetItem(model.display_name.isEmpty() ? model.id : model.display_name);
|
||||
model_item->setData(Qt::UserRole, model.id);
|
||||
model_table_->setItem(row, 0, model_item);
|
||||
|
||||
ModelRowWidgets widgets;
|
||||
widgets.fed_id = model.id;
|
||||
|
||||
widgets.frame = new QComboBox(model_table_);
|
||||
widgets.frame->addItem("Local", static_cast<int>(AFrame::ModelLocal));
|
||||
widgets.frame->addItem("Global", static_cast<int>(AFrame::ModelGlobal));
|
||||
widgets.frame->setCurrentIndex(xf.a_frame == AFrame::ModelGlobal ? 1 : 0);
|
||||
model_table_->setCellWidget(row, 1, widgets.frame);
|
||||
|
||||
widgets.from_point = new QTableWidgetItem(formatVector3(xf.a));
|
||||
widgets.to_point = new QTableWidgetItem(formatVector3(xf.b));
|
||||
widgets.rotate = new QTableWidgetItem(formatVector3(xf.rxyz_deg));
|
||||
widgets.pivot = new QTableWidgetItem(formatVector3(xf.pivot));
|
||||
model_table_->setItem(row, 2, widgets.from_point);
|
||||
model_table_->setItem(row, 3, widgets.to_point);
|
||||
model_table_->setItem(row, 4, widgets.rotate);
|
||||
model_table_->setItem(row, 5, widgets.pivot);
|
||||
|
||||
model_rows_.push_back(widgets);
|
||||
model_table_->setRowHeight(row, 42);
|
||||
++row;
|
||||
}
|
||||
|
||||
int table_height = model_table_->frameWidth() * 2 + model_table_->horizontalHeader()->height();
|
||||
for (int i = 0; i < model_table_->rowCount(); ++i) {
|
||||
table_height += model_table_->rowHeight(i);
|
||||
}
|
||||
if (model_table_->horizontalScrollBar()->isVisible()) {
|
||||
table_height += model_table_->horizontalScrollBar()->sizeHint().height();
|
||||
}
|
||||
model_table_->setMinimumHeight(table_height);
|
||||
model_table_->setMaximumHeight(table_height);
|
||||
|
||||
if (model_table_->rowCount() > 0) {
|
||||
model_table_->setCurrentCell(0, 0);
|
||||
}
|
||||
updateSelectedModelGeoref();
|
||||
}
|
||||
|
||||
void SettingsDialog::updateSelectedModelGeoref() {
|
||||
auto set_unknown = [this](const QString& georef, const QString& type) {
|
||||
georef_present_value_->setText(georef);
|
||||
georef_type_value_->setText(type);
|
||||
georef_easting_value_->setText("—");
|
||||
georef_northing_value_->setText("—");
|
||||
georef_height_value_->setText("—");
|
||||
georef_x_axis_abscissa_value_->setText("—");
|
||||
georef_x_axis_ordinate_value_->setText("—");
|
||||
georef_rotation_dd_value_->setText("—");
|
||||
georef_rotation_dms_value_->setText("—");
|
||||
georef_scale_value_->setText("—");
|
||||
georef_factor_x_value_->setText("—");
|
||||
georef_factor_y_value_->setText("—");
|
||||
georef_factor_z_value_->setText("—");
|
||||
};
|
||||
|
||||
const int row = model_table_->currentRow();
|
||||
if (row < 0 || row >= static_cast<int>(model_rows_.size())) {
|
||||
set_unknown("No model selected", "—");
|
||||
return;
|
||||
}
|
||||
|
||||
const QString& fed_id = model_rows_[row].fed_id;
|
||||
if (!session_state_) {
|
||||
set_unknown("Unavailable", "No session state");
|
||||
return;
|
||||
}
|
||||
|
||||
const uint32_t mid = session_state_->modelIdForFedId(fed_id);
|
||||
if (mid == 0 || !loader_) {
|
||||
set_unknown("Not loaded", "No live model");
|
||||
return;
|
||||
}
|
||||
|
||||
const ModelGeoref* georef = loader_->modelGeoref(mid);
|
||||
if (!georef) {
|
||||
set_unknown("Not available yet", "No data source");
|
||||
return;
|
||||
}
|
||||
|
||||
if (!georef->has_coordinate_operation) {
|
||||
set_unknown("No", "None");
|
||||
return;
|
||||
}
|
||||
|
||||
const Eigen::Matrix4d& m = georef->coordinate_operation_meters;
|
||||
const Eigen::Vector3d translation = m.block<3, 1>(0, 3);
|
||||
const Eigen::Vector3d x_axis = m.block<3, 1>(0, 0);
|
||||
const Eigen::Vector3d y_axis = m.block<3, 1>(0, 1);
|
||||
const double factor_x = x_axis.norm();
|
||||
const double factor_y = y_axis.norm();
|
||||
const double factor_z = m.block<3, 1>(0, 2).norm();
|
||||
const double scale = (factor_x + factor_y) * 0.5;
|
||||
const double x_axis_abscissa = factor_x > 0.0 ? x_axis.x() / factor_x : 1.0;
|
||||
const double x_axis_ordinate = factor_x > 0.0 ? x_axis.y() / factor_x : 0.0;
|
||||
constexpr double kRadiansToDegrees = 57.29577951308232;
|
||||
const double rotation_dd = std::atan2(x_axis_ordinate, x_axis_abscissa) * kRadiansToDegrees;
|
||||
|
||||
georef_present_value_->setText("Yes");
|
||||
georef_type_value_->setText("IfcMapConversion");
|
||||
georef_easting_value_->setText(formatNumber(translation.x()));
|
||||
georef_northing_value_->setText(formatNumber(translation.y()));
|
||||
georef_height_value_->setText(formatNumber(translation.z()));
|
||||
georef_x_axis_abscissa_value_->setText(formatNumber(x_axis_abscissa));
|
||||
georef_x_axis_ordinate_value_->setText(formatNumber(x_axis_ordinate));
|
||||
georef_rotation_dd_value_->setText(formatNumber(rotation_dd));
|
||||
georef_rotation_dms_value_->setText(formatAngleDms(rotation_dd));
|
||||
georef_scale_value_->setText(formatNumber(scale));
|
||||
georef_factor_x_value_->setText(formatNumber(factor_x));
|
||||
georef_factor_y_value_->setText(formatNumber(factor_y));
|
||||
georef_factor_z_value_->setText(formatNumber(factor_z));
|
||||
}
|
||||
|
||||
void SettingsDialog::onAccepted() {
|
||||
if (federation_) {
|
||||
const QStringList data = unit_combo_->currentData().toStringList();
|
||||
FederationConfig cfg;
|
||||
if (data.size() == 2) {
|
||||
cfg.unit_prefix = data[0].toStdString();
|
||||
cfg.unit_name = data[1].toStdString();
|
||||
}
|
||||
federation_->setConfig(cfg);
|
||||
|
||||
FederatedFalseOrigin origin;
|
||||
origin.xyz = Eigen::Vector3d(parseNumber(xyz_x_), parseNumber(xyz_y_), parseNumber(xyz_z_));
|
||||
origin.rz_deg = parseNumber(rz_deg_);
|
||||
federation_->setFederatedFalseOrigin(origin);
|
||||
|
||||
for (const auto& row : model_rows_) {
|
||||
ModelTransformation xf;
|
||||
xf.a_frame = static_cast<AFrame>(row.frame->currentData().toInt());
|
||||
xf.a = parseVector3(row.from_point->text());
|
||||
xf.b = parseVector3(row.to_point->text());
|
||||
xf.rxyz_deg = parseVector3(row.rotate->text());
|
||||
xf.pivot = parseVector3(row.pivot->text());
|
||||
federation_->setModelTransformation(row.fed_id, xf);
|
||||
}
|
||||
}
|
||||
accept();
|
||||
}
|
||||
|
||||
} // namespace ifcinterface::modules::models
|
||||
@@ -0,0 +1,98 @@
|
||||
// This file was generated with the assistance of an AI coding tool.
|
||||
/********************************************************************************
|
||||
* *
|
||||
* This file is part of IfcOpenShell. *
|
||||
* *
|
||||
* IfcOpenShell is free software: you can redistribute it and/or modify *
|
||||
* it under the terms of the Lesser GNU General Public License as published by *
|
||||
* the Free Software Foundation, either version 3.0 of the License, or *
|
||||
* (at your option) any later version. *
|
||||
* *
|
||||
* IfcOpenShell is distributed in the hope that it will be useful, *
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of *
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the *
|
||||
* Lesser GNU General Public License for more details. *
|
||||
* *
|
||||
* You should have received a copy of the Lesser GNU General Public License *
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>. *
|
||||
* *
|
||||
********************************************************************************/
|
||||
|
||||
#ifndef IFCINTERFACE_MODULES_MODELS_SETTINGSDIALOG_H
|
||||
#define IFCINTERFACE_MODULES_MODELS_SETTINGSDIALOG_H
|
||||
|
||||
#include "../../components/Dialog.h"
|
||||
|
||||
#include <QString>
|
||||
#include <vector>
|
||||
|
||||
class Federation;
|
||||
class SceneLoader;
|
||||
class QComboBox;
|
||||
class QLabel;
|
||||
class QLineEdit;
|
||||
class QShowEvent;
|
||||
class QTableWidget;
|
||||
class QTableWidgetItem;
|
||||
|
||||
namespace ifcinterface {
|
||||
class SessionState;
|
||||
}
|
||||
|
||||
namespace ifcinterface::modules::models {
|
||||
|
||||
class SettingsDialog : public components::TabbedDialog {
|
||||
Q_OBJECT
|
||||
public:
|
||||
explicit SettingsDialog(ifcinterface::SessionState* session_state, QWidget* parent = nullptr);
|
||||
|
||||
protected:
|
||||
void showEvent(QShowEvent* event) override;
|
||||
|
||||
private:
|
||||
struct ModelRowWidgets {
|
||||
QString fed_id;
|
||||
QComboBox* frame = nullptr;
|
||||
QTableWidgetItem* from_point = nullptr;
|
||||
QTableWidgetItem* to_point = nullptr;
|
||||
QTableWidgetItem* rotate = nullptr;
|
||||
QTableWidgetItem* pivot = nullptr;
|
||||
};
|
||||
|
||||
void setupUi();
|
||||
void syncFromFederation();
|
||||
void populateModelTable();
|
||||
void updateSelectedModelGeoref();
|
||||
void onAccepted();
|
||||
|
||||
ifcinterface::SessionState* session_state_ = nullptr;
|
||||
Federation* federation_ = nullptr;
|
||||
SceneLoader* loader_ = nullptr;
|
||||
|
||||
QComboBox* unit_combo_ = nullptr;
|
||||
QLineEdit* xyz_x_ = nullptr;
|
||||
QLineEdit* xyz_y_ = nullptr;
|
||||
QLineEdit* xyz_z_ = nullptr;
|
||||
QLineEdit* rz_deg_ = nullptr;
|
||||
|
||||
QLabel* georef_present_value_ = nullptr;
|
||||
QLabel* georef_type_value_ = nullptr;
|
||||
QLabel* georef_easting_value_ = nullptr;
|
||||
QLabel* georef_northing_value_ = nullptr;
|
||||
QLabel* georef_height_value_ = nullptr;
|
||||
QLabel* georef_x_axis_abscissa_value_ = nullptr;
|
||||
QLabel* georef_x_axis_ordinate_value_ = nullptr;
|
||||
QLabel* georef_rotation_dd_value_ = nullptr;
|
||||
QLabel* georef_rotation_dms_value_ = nullptr;
|
||||
QLabel* georef_scale_value_ = nullptr;
|
||||
QLabel* georef_factor_x_value_ = nullptr;
|
||||
QLabel* georef_factor_y_value_ = nullptr;
|
||||
QLabel* georef_factor_z_value_ = nullptr;
|
||||
|
||||
QTableWidget* model_table_ = nullptr;
|
||||
std::vector<ModelRowWidgets> model_rows_;
|
||||
};
|
||||
|
||||
} // namespace ifcinterface::modules::models
|
||||
|
||||
#endif
|
||||
@@ -0,0 +1,44 @@
|
||||
// This file was generated with the assistance of an AI coding tool.
|
||||
/********************************************************************************
|
||||
* *
|
||||
* This file is part of IfcOpenShell. *
|
||||
* *
|
||||
* IfcOpenShell is free software: you can redistribute it and/or modify *
|
||||
* it under the terms of the Lesser GNU General Public License as published by *
|
||||
* the Free Software Foundation, either version 3.0 of the License, or *
|
||||
* (at your option) any later version. *
|
||||
* *
|
||||
* IfcOpenShell is distributed in the hope that it will be useful, *
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of *
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the *
|
||||
* Lesser GNU General Public License for more details. *
|
||||
* *
|
||||
* You should have received a copy of the Lesser GNU General Public License *
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>. *
|
||||
* *
|
||||
********************************************************************************/
|
||||
|
||||
#ifndef IFCINTERFACE_PANELS_MODELSPANELTYPES_H
|
||||
#define IFCINTERFACE_PANELS_MODELSPANELTYPES_H
|
||||
|
||||
#include <QList>
|
||||
#include <QString>
|
||||
|
||||
namespace ifcinterface::modules::models {
|
||||
|
||||
enum class ItemKind {
|
||||
Group,
|
||||
Model,
|
||||
};
|
||||
|
||||
struct TreeNode {
|
||||
QString id;
|
||||
QString name;
|
||||
ItemKind kind = ItemKind::Group;
|
||||
bool visible = true;
|
||||
QList<TreeNode> children;
|
||||
};
|
||||
|
||||
} // namespace ifcinterface::modules::models
|
||||
|
||||
#endif
|
||||
@@ -0,0 +1,96 @@
|
||||
// This file was generated with the assistance of an AI coding tool.
|
||||
/********************************************************************************
|
||||
* *
|
||||
* This file is part of IfcOpenShell. *
|
||||
* *
|
||||
* IfcOpenShell is free software: you can redistribute it and/or modify *
|
||||
* it under the terms of the Lesser GNU General Public License as published by *
|
||||
* the Free Software Foundation, either version 3.0 of the License, or *
|
||||
* (at your option) any later version. *
|
||||
* *
|
||||
* IfcOpenShell is distributed in the hope that it will be useful, *
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of *
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the *
|
||||
* Lesser GNU General Public License for more details. *
|
||||
* *
|
||||
* You should have received a copy of the Lesser GNU General Public License *
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>. *
|
||||
* *
|
||||
********************************************************************************/
|
||||
|
||||
#include "View.h"
|
||||
|
||||
#include "Panel.h"
|
||||
|
||||
#include "../../SessionState.h"
|
||||
#include "../../../ifcviewer/Federation.h"
|
||||
|
||||
namespace ifcinterface::modules::models {
|
||||
|
||||
namespace {
|
||||
|
||||
TreeNode makeGroupNode(const Federation* federation, const Federation::Group* group) {
|
||||
TreeNode node;
|
||||
node.id = group->id;
|
||||
node.name = group->display_name;
|
||||
node.kind = ItemKind::Group;
|
||||
node.visible = group->visible;
|
||||
|
||||
for (const auto& child_group : group->children) {
|
||||
node.children.append(makeGroupNode(federation, child_group.get()));
|
||||
}
|
||||
|
||||
for (const auto& model : federation->models()) {
|
||||
if (model.group_id != group->id) continue;
|
||||
node.children.append({
|
||||
model.id,
|
||||
model.display_name,
|
||||
ItemKind::Model,
|
||||
federation->isModelEffectivelyVisible(model.id),
|
||||
{}
|
||||
});
|
||||
}
|
||||
return node;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
ModelsPanelView::ModelsPanelView(ModelsPanel* widget,
|
||||
ifcinterface::SessionState* session_state,
|
||||
QObject* parent)
|
||||
: QObject(parent), widget_(widget), session_state_(session_state)
|
||||
{
|
||||
connect(session_state_, &ifcinterface::SessionState::modelsChanged,
|
||||
this, [this]() { reload(); });
|
||||
connect(session_state_, &ifcinterface::SessionState::federationStructureChanged,
|
||||
this, [this]() { reload(); });
|
||||
connect(session_state_, &ifcinterface::SessionState::visibilityChanged,
|
||||
this, [this]() { reload(); });
|
||||
connect(session_state_, &ifcinterface::SessionState::projectReset,
|
||||
this, [this]() { reload(); });
|
||||
connect(session_state_, &ifcinterface::SessionState::projectOpened,
|
||||
this, [this](const QString&) { reload(); });
|
||||
|
||||
reload();
|
||||
}
|
||||
|
||||
void ModelsPanelView::reload() {
|
||||
Federation* federation = session_state_->federation();
|
||||
QList<TreeNode> nodes;
|
||||
for (const auto& root_group : federation->rootGroups()) {
|
||||
nodes.append(makeGroupNode(federation, root_group.get()));
|
||||
}
|
||||
for (const auto& model : federation->models()) {
|
||||
if (!model.group_id.isEmpty()) continue;
|
||||
nodes.append({
|
||||
model.id,
|
||||
model.display_name,
|
||||
ItemKind::Model,
|
||||
federation->isModelEffectivelyVisible(model.id),
|
||||
{}
|
||||
});
|
||||
}
|
||||
widget_->setNodes(nodes);
|
||||
}
|
||||
|
||||
} // namespace ifcinterface::modules::models
|
||||
@@ -0,0 +1,50 @@
|
||||
// This file was generated with the assistance of an AI coding tool.
|
||||
/********************************************************************************
|
||||
* *
|
||||
* This file is part of IfcOpenShell. *
|
||||
* *
|
||||
* IfcOpenShell is free software: you can redistribute it and/or modify *
|
||||
* it under the terms of the Lesser GNU General Public License as published by *
|
||||
* the Free Software Foundation, either version 3.0 of the License, or *
|
||||
* (at your option) any later version. *
|
||||
* *
|
||||
* IfcOpenShell is distributed in the hope that it will be useful, *
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of *
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the *
|
||||
* Lesser GNU General Public License for more details. *
|
||||
* *
|
||||
* You should have received a copy of the Lesser GNU General Public License *
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>. *
|
||||
* *
|
||||
********************************************************************************/
|
||||
|
||||
#ifndef IFCINTERFACE_PANELS_MODELSPANELVIEW_H
|
||||
#define IFCINTERFACE_PANELS_MODELSPANELVIEW_H
|
||||
|
||||
#include "Types.h"
|
||||
|
||||
#include <QObject>
|
||||
|
||||
namespace ifcinterface { class SessionState; }
|
||||
|
||||
namespace ifcinterface::modules::models {
|
||||
|
||||
class ModelsPanel;
|
||||
|
||||
class ModelsPanelView : public QObject {
|
||||
Q_OBJECT
|
||||
public:
|
||||
explicit ModelsPanelView(ModelsPanel* widget,
|
||||
ifcinterface::SessionState* session_state,
|
||||
QObject* parent = nullptr);
|
||||
|
||||
private:
|
||||
void reload();
|
||||
|
||||
ModelsPanel* widget_ = nullptr;
|
||||
ifcinterface::SessionState* session_state_ = nullptr;
|
||||
};
|
||||
|
||||
} // namespace ifcinterface::modules::models
|
||||
|
||||
#endif
|
||||
@@ -0,0 +1,207 @@
|
||||
// This file was generated with the assistance of an AI coding tool.
|
||||
/********************************************************************************
|
||||
* *
|
||||
* This file is part of IfcOpenShell. *
|
||||
* *
|
||||
* IfcOpenShell is free software: you can redistribute it and/or modify *
|
||||
* it under the terms of the Lesser GNU General Public License as published by *
|
||||
* the Free Software Foundation, either version 3.0 of the License, or *
|
||||
* (at your option) any later version. *
|
||||
* *
|
||||
* IfcOpenShell is distributed in the hope that it will be useful, *
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of *
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the *
|
||||
* Lesser GNU General Public License for more details. *
|
||||
* *
|
||||
* You should have received a copy of the Lesser GNU General Public License *
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>. *
|
||||
* *
|
||||
********************************************************************************/
|
||||
|
||||
#include "Controller.h"
|
||||
|
||||
#include "../../ElementRegistry.h"
|
||||
#include "../../SessionState.h"
|
||||
#include "../models/Controller.h"
|
||||
#include "../viewport/Controller.h"
|
||||
#include "../../../ifcviewer/Federation.h"
|
||||
#include "../../../ifcviewer/SceneLoader.h"
|
||||
#include "../../../ifcviewer/ViewportWindow.h"
|
||||
|
||||
#include <QFileDialog>
|
||||
#include <QFileInfo>
|
||||
#include <QMessageBox>
|
||||
|
||||
namespace ifcinterface::modules::project {
|
||||
|
||||
ProjectController::ProjectController(QWidget* host,
|
||||
Federation* federation,
|
||||
ifcinterface::SessionState* session_state,
|
||||
ifcinterface::ElementRegistry* element_registry,
|
||||
ViewportWindow* viewport,
|
||||
ifcinterface::modules::models::ModelsPanelController* models_controller,
|
||||
ifcinterface::modules::viewport::ViewportController* viewport_controller,
|
||||
QObject* parent)
|
||||
: QObject(parent)
|
||||
, host_(host)
|
||||
, federation_(federation)
|
||||
, session_state_(session_state)
|
||||
, element_registry_(element_registry)
|
||||
, viewport_(viewport)
|
||||
, models_controller_(models_controller)
|
||||
, viewport_controller_(viewport_controller)
|
||||
{
|
||||
}
|
||||
|
||||
bool ProjectController::newProject() {
|
||||
SceneLoader* loader = session_state_->loader();
|
||||
if (loader && loader->isLoading()) {
|
||||
QMessageBox::information(
|
||||
host_, "New Project",
|
||||
"Wait until the current model load finishes before creating a new project.");
|
||||
return false;
|
||||
}
|
||||
if (!confirmDiscardIfDirty()) return false;
|
||||
|
||||
clearScene();
|
||||
federation_->clear();
|
||||
viewport_controller_->applyFederatedFalseOrigin();
|
||||
session_state_->setStatusMessage("Project", "Untitled");
|
||||
session_state_->notifyProjectReset();
|
||||
return true;
|
||||
}
|
||||
|
||||
bool ProjectController::openProject() {
|
||||
QFileDialog file_dialog(host_, "Open Project");
|
||||
file_dialog.setFileMode(QFileDialog::ExistingFile);
|
||||
file_dialog.setNameFilter("IFC Federation (*.ifcfed);;All Files (*)");
|
||||
file_dialog.setOption(QFileDialog::DontUseNativeDialog, true);
|
||||
if (file_dialog.exec() != QDialog::Accepted) return false;
|
||||
|
||||
const QString path = file_dialog.selectedFiles().value(0);
|
||||
if (path.isEmpty()) return false;
|
||||
return openProject(path);
|
||||
}
|
||||
|
||||
bool ProjectController::openProject(const QString& path) {
|
||||
SceneLoader* loader = session_state_->loader();
|
||||
if (loader && loader->isLoading()) {
|
||||
QMessageBox::information(
|
||||
host_, "Open Project",
|
||||
"Wait until the current model load finishes before opening another project.");
|
||||
return false;
|
||||
}
|
||||
if (!confirmDiscardIfDirty()) return false;
|
||||
|
||||
QStringList warnings;
|
||||
QString err;
|
||||
if (!federation_->load(path, &warnings, &err)) {
|
||||
QMessageBox::warning(host_, "Open Project",
|
||||
QString("Could not open project:\n%1").arg(err));
|
||||
return false;
|
||||
}
|
||||
|
||||
clearScene();
|
||||
|
||||
QStringList paths;
|
||||
QStringList fed_ids;
|
||||
for (const auto& model : federation_->models()) {
|
||||
if (model.source_kind != "local") continue;
|
||||
if (!QFileInfo::exists(model.source_path)) {
|
||||
warnings << QString("Source not found, kept in project: %1").arg(model.source_path);
|
||||
continue;
|
||||
}
|
||||
paths << model.source_path;
|
||||
fed_ids << model.id;
|
||||
}
|
||||
models_controller_->loadModels(paths, fed_ids);
|
||||
|
||||
if (!warnings.isEmpty()) {
|
||||
QMessageBox::warning(host_, "Open Project",
|
||||
"Project opened with warnings:\n\n" + warnings.join("\n"));
|
||||
}
|
||||
|
||||
federation_->markClean();
|
||||
viewport_controller_->applyFederatedFalseOrigin();
|
||||
if (federation_->hasHomeView()) {
|
||||
const auto& hv = federation_->homeView();
|
||||
viewport_->setCamera(
|
||||
hv.target.x(), hv.target.y(), hv.target.z(), hv.distance, hv.yaw, hv.pitch);
|
||||
}
|
||||
session_state_->setStatusMessage("Project", QFileInfo(path).fileName());
|
||||
session_state_->notifyProjectOpened(path);
|
||||
return true;
|
||||
}
|
||||
|
||||
bool ProjectController::saveProject() {
|
||||
if (federation_->filePath().isEmpty()) return saveProjectAs();
|
||||
|
||||
QString err;
|
||||
if (!federation_->save(federation_->filePath(), &err)) {
|
||||
QMessageBox::warning(host_, "Save Project",
|
||||
QString("Could not save project:\n%1").arg(err));
|
||||
return false;
|
||||
}
|
||||
session_state_->setStatusMessage("Project", QFileInfo(federation_->filePath()).fileName());
|
||||
session_state_->notifyProjectSaved(federation_->filePath());
|
||||
return true;
|
||||
}
|
||||
|
||||
bool ProjectController::saveProjectAs() {
|
||||
QString suggested = federation_->filePath();
|
||||
if (suggested.isEmpty()) suggested = "project.ifcfed";
|
||||
|
||||
QFileDialog file_dialog(host_, "Save Project As", suggested);
|
||||
file_dialog.setAcceptMode(QFileDialog::AcceptSave);
|
||||
file_dialog.setFileMode(QFileDialog::AnyFile);
|
||||
file_dialog.setNameFilter("IFC Federation (*.ifcfed);;All Files (*)");
|
||||
file_dialog.setOption(QFileDialog::DontUseNativeDialog, true);
|
||||
if (file_dialog.exec() != QDialog::Accepted) return false;
|
||||
|
||||
QString path = file_dialog.selectedFiles().value(0);
|
||||
if (path.isEmpty()) return false;
|
||||
if (!path.endsWith(".ifcfed", Qt::CaseInsensitive)) path += ".ifcfed";
|
||||
return saveProjectAs(path);
|
||||
}
|
||||
|
||||
bool ProjectController::saveProjectAs(const QString& path) {
|
||||
QString err;
|
||||
if (!federation_->save(path, &err)) {
|
||||
QMessageBox::warning(host_, "Save Project",
|
||||
QString("Could not save project:\n%1").arg(err));
|
||||
return false;
|
||||
}
|
||||
session_state_->setStatusMessage("Project", QFileInfo(path).fileName());
|
||||
session_state_->notifyProjectSaved(path);
|
||||
return true;
|
||||
}
|
||||
|
||||
void ProjectController::clearScene() {
|
||||
viewport_->setSelectedObjectId(0);
|
||||
session_state_->setSelectedObjectId(0);
|
||||
session_state_->notifySelectionChanged();
|
||||
|
||||
const auto model_ids = session_state_->modelIds();
|
||||
for (uint32_t mid : model_ids) {
|
||||
viewport_->removeModel(mid);
|
||||
session_state_->loader()->removeModel(mid);
|
||||
}
|
||||
|
||||
session_state_->clearModelMappings();
|
||||
element_registry_->clear();
|
||||
session_state_->notifyModelsChanged();
|
||||
}
|
||||
|
||||
bool ProjectController::confirmDiscardIfDirty() {
|
||||
if (!federation_->isDirty()) return true;
|
||||
const auto result = QMessageBox::question(
|
||||
host_, "Unsaved Project",
|
||||
"The current project has unsaved changes. Save before continuing?",
|
||||
QMessageBox::Save | QMessageBox::Discard | QMessageBox::Cancel,
|
||||
QMessageBox::Save);
|
||||
if (result == QMessageBox::Cancel) return false;
|
||||
if (result == QMessageBox::Save) return saveProject();
|
||||
return true;
|
||||
}
|
||||
|
||||
} // namespace ifcinterface::modules::project
|
||||
@@ -0,0 +1,71 @@
|
||||
// This file was generated with the assistance of an AI coding tool.
|
||||
/********************************************************************************
|
||||
* *
|
||||
* This file is part of IfcOpenShell. *
|
||||
* *
|
||||
* IfcOpenShell is free software: you can redistribute it and/or modify *
|
||||
* it under the terms of the Lesser GNU General Public License as published by *
|
||||
* the Free Software Foundation, either version 3.0 of the License, or *
|
||||
* (at your option) any later version. *
|
||||
* *
|
||||
* IfcOpenShell is distributed in the hope that it will be useful, *
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of *
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the *
|
||||
* Lesser GNU General Public License for more details. *
|
||||
* *
|
||||
* You should have received a copy of the Lesser GNU General Public License *
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>. *
|
||||
* *
|
||||
********************************************************************************/
|
||||
|
||||
#ifndef IFCINTERFACE_PANELS_PROJECT_CONTROLLER_H
|
||||
#define IFCINTERFACE_PANELS_PROJECT_CONTROLLER_H
|
||||
|
||||
#include <QObject>
|
||||
|
||||
class QWidget;
|
||||
class Federation;
|
||||
class ViewportWindow;
|
||||
namespace ifcinterface { class ElementRegistry; }
|
||||
namespace ifcinterface { class SessionState; }
|
||||
namespace ifcinterface::modules::models { class ModelsPanelController; }
|
||||
namespace ifcinterface::modules::viewport { class ViewportController; }
|
||||
|
||||
namespace ifcinterface::modules::project {
|
||||
|
||||
class ProjectController : public QObject {
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
explicit ProjectController(QWidget* host,
|
||||
Federation* federation,
|
||||
ifcinterface::SessionState* session_state,
|
||||
ifcinterface::ElementRegistry* element_registry,
|
||||
ViewportWindow* viewport,
|
||||
ifcinterface::modules::models::ModelsPanelController* models_controller,
|
||||
ifcinterface::modules::viewport::ViewportController* viewport_controller,
|
||||
QObject* parent = nullptr);
|
||||
|
||||
bool newProject();
|
||||
bool openProject();
|
||||
bool saveProject();
|
||||
bool saveProjectAs();
|
||||
|
||||
private:
|
||||
bool openProject(const QString& path);
|
||||
bool saveProjectAs(const QString& path);
|
||||
void clearScene();
|
||||
bool confirmDiscardIfDirty();
|
||||
|
||||
QWidget* host_ = nullptr;
|
||||
Federation* federation_ = nullptr;
|
||||
ifcinterface::SessionState* session_state_ = nullptr;
|
||||
ifcinterface::ElementRegistry* element_registry_ = nullptr;
|
||||
ViewportWindow* viewport_ = nullptr;
|
||||
ifcinterface::modules::models::ModelsPanelController* models_controller_ = nullptr;
|
||||
ifcinterface::modules::viewport::ViewportController* viewport_controller_ = nullptr;
|
||||
};
|
||||
|
||||
} // namespace ifcinterface::modules::project
|
||||
|
||||
#endif
|
||||
+9
-9
@@ -18,7 +18,7 @@
|
||||
* *
|
||||
********************************************************************************/
|
||||
|
||||
#include "Widget.h"
|
||||
#include "Panel.h"
|
||||
|
||||
#include "../../components/KeyValueTable.h"
|
||||
#include "../../components/Section.h"
|
||||
@@ -35,7 +35,7 @@
|
||||
|
||||
namespace {
|
||||
|
||||
QWidget* makePropertySetPanel(const ifcinterface::panels::properties::PropertySet& property_set, QWidget* parent = nullptr) {
|
||||
QWidget* makePropertySetPanel(const ifcinterface::modules::properties::PropertySet& property_set, QWidget* parent = nullptr) {
|
||||
auto* group = new QGroupBox(property_set.title, parent);
|
||||
group->setObjectName("propertySetBox");
|
||||
auto* layout = new QVBoxLayout(group);
|
||||
@@ -50,7 +50,7 @@ QWidget* makePropertySetPanel(const ifcinterface::panels::properties::PropertySe
|
||||
return group;
|
||||
}
|
||||
|
||||
QWidget* makeAttributeList(const QList<ifcinterface::panels::properties::KeyValueRow>& rows, QWidget* parent = nullptr) {
|
||||
QWidget* makeAttributeList(const QList<ifcinterface::modules::properties::KeyValueRow>& rows, QWidget* parent = nullptr) {
|
||||
QList<ifcinterface::components::KeyValueTableRow> table_rows;
|
||||
for (const auto& row : rows) {
|
||||
table_rows.append({row.key, row.value, "keyValueValueLabel", "", "", 0});
|
||||
@@ -58,7 +58,7 @@ QWidget* makeAttributeList(const QList<ifcinterface::panels::properties::KeyValu
|
||||
return new ifcinterface::components::KeyValueTable(table_rows, parent);
|
||||
}
|
||||
|
||||
QWidget* makeRelationshipList(const QList<ifcinterface::panels::properties::RelationshipRow>& rows, QWidget* parent = nullptr) {
|
||||
QWidget* makeRelationshipList(const QList<ifcinterface::modules::properties::RelationshipRow>& rows, QWidget* parent = nullptr) {
|
||||
QList<ifcinterface::components::KeyValueTableRow> table_rows;
|
||||
for (const auto& row_data : rows) {
|
||||
table_rows.append({row_data.key,
|
||||
@@ -91,7 +91,7 @@ QWidget* makeFilterWrapper(QLineEdit** field_out, QWidget* parent = nullptr) {
|
||||
return wrapper;
|
||||
}
|
||||
|
||||
QFrame* makeEntityBox(const ifcinterface::panels::properties::EntitySummary& entity, QWidget* parent = nullptr) {
|
||||
QFrame* makeEntityBox(const ifcinterface::modules::properties::EntitySummary& entity, QWidget* parent = nullptr) {
|
||||
auto* entity_box = new QFrame(parent);
|
||||
entity_box->setObjectName("entityClassBox");
|
||||
auto* entity_layout = new QHBoxLayout(entity_box);
|
||||
@@ -121,14 +121,14 @@ QFrame* makeEntityBox(const ifcinterface::panels::properties::EntitySummary& ent
|
||||
|
||||
} // namespace
|
||||
|
||||
namespace ifcinterface::panels::properties {
|
||||
namespace ifcinterface::modules::properties {
|
||||
|
||||
PropertiesPanelWidget::PropertiesPanelWidget(QWidget* parent)
|
||||
PropertiesPanel::PropertiesPanel(QWidget* parent)
|
||||
: components::Panel("Properties", nullptr, parent, false, true)
|
||||
{
|
||||
}
|
||||
|
||||
void PropertiesPanelWidget::render(const PropertiesPanelState& state) {
|
||||
void PropertiesPanel::render(const PropertiesPanelState& state) {
|
||||
clearBodyWidgets();
|
||||
|
||||
QList<QWidget*> property_set_widgets;
|
||||
@@ -234,4 +234,4 @@ void PropertiesPanelWidget::render(const PropertiesPanelState& state) {
|
||||
addBodyWidget(quantities_section);
|
||||
}
|
||||
|
||||
} // namespace ifcinterface::panels::properties
|
||||
} // namespace ifcinterface::modules::properties
|
||||
@@ -18,8 +18,8 @@
|
||||
* *
|
||||
********************************************************************************/
|
||||
|
||||
#ifndef IFCINTERFACE_PANELS_PROPERTIESPANELWIDGET_H
|
||||
#define IFCINTERFACE_PANELS_PROPERTIESPANELWIDGET_H
|
||||
#ifndef IFCINTERFACE_MODULES_PROPERTIES_PANEL_H
|
||||
#define IFCINTERFACE_MODULES_PROPERTIES_PANEL_H
|
||||
|
||||
#include "Types.h"
|
||||
|
||||
@@ -31,12 +31,12 @@ class QLabel;
|
||||
class QLineEdit;
|
||||
class QToolButton;
|
||||
|
||||
namespace ifcinterface::panels::properties {
|
||||
namespace ifcinterface::modules::properties {
|
||||
|
||||
class PropertiesPanelWidget : public components::Panel {
|
||||
class PropertiesPanel : public components::Panel {
|
||||
Q_OBJECT
|
||||
public:
|
||||
explicit PropertiesPanelWidget(QWidget* parent = nullptr);
|
||||
explicit PropertiesPanel(QWidget* parent = nullptr);
|
||||
|
||||
void render(const PropertiesPanelState& state);
|
||||
|
||||
@@ -51,6 +51,6 @@ private:
|
||||
QString quantities_filter_text_;
|
||||
};
|
||||
|
||||
} // namespace ifcinterface::panels::properties
|
||||
} // namespace ifcinterface::modules::properties
|
||||
|
||||
#endif
|
||||
@@ -25,7 +25,7 @@
|
||||
#include <QPair>
|
||||
#include <QString>
|
||||
|
||||
namespace ifcinterface::panels::properties {
|
||||
namespace ifcinterface::modules::properties {
|
||||
|
||||
struct KeyValueRow {
|
||||
QString key;
|
||||
@@ -55,6 +55,6 @@ struct PropertiesPanelState {
|
||||
QList<PropertySet> quantity_sets;
|
||||
};
|
||||
|
||||
} // namespace ifcinterface::panels::properties
|
||||
} // namespace ifcinterface::modules::properties
|
||||
|
||||
#endif
|
||||
+15
-17
@@ -20,15 +20,14 @@
|
||||
|
||||
#include "View.h"
|
||||
|
||||
#include "Widget.h"
|
||||
#include "Panel.h"
|
||||
|
||||
#include "../../ElementRegistry.h"
|
||||
#include "../../SessionState.h"
|
||||
#include "../../../ifcviewer/AppSettings.h"
|
||||
|
||||
namespace ifcinterface::panels::properties {
|
||||
namespace ifcinterface::modules::properties {
|
||||
|
||||
PropertiesPanelView::PropertiesPanelView(PropertiesPanelWidget* widget,
|
||||
PropertiesPanelView::PropertiesPanelView(PropertiesPanel* widget,
|
||||
ifcinterface::SessionState* session_state,
|
||||
QObject* parent)
|
||||
: QObject(parent), widget_(widget), session_state_(session_state)
|
||||
@@ -88,7 +87,17 @@ void PropertiesPanelView::refresh(uint32_t object_id) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!AppSettings::instance().loadDataSource()) {
|
||||
auto entity = registry->findEntity(object_id);
|
||||
if (entity) {
|
||||
state.entity.entity_class = QString::fromStdString(entity->declaration().name());
|
||||
if (!state.property_sets.isEmpty() && !state.property_sets[1].rows.isEmpty()) {
|
||||
state.property_sets[1].rows[0].value = state.entity.entity_class;
|
||||
}
|
||||
} else {
|
||||
// No live IFC source for this object — typical when a pure-geometry
|
||||
// .ifcview sidecar was loaded without its .ifc/.rdb sibling. Fall
|
||||
// back to the basic info cached in the element registry so the
|
||||
// panel still shows class / name / guid for visible elements.
|
||||
auto info = registry->findBasicElementInfo(object_id);
|
||||
if (info && !info->type.isEmpty()) {
|
||||
state.entity.entity_class = info->type;
|
||||
@@ -105,19 +114,8 @@ void PropertiesPanelView::refresh(uint32_t object_id) {
|
||||
if (info && !info->guid.isEmpty()) {
|
||||
state.attributes[0].value = info->guid;
|
||||
}
|
||||
|
||||
widget_->render(state);
|
||||
return;
|
||||
}
|
||||
|
||||
auto entity = registry->findEntity(object_id);
|
||||
if (entity) {
|
||||
state.entity.entity_class = QString::fromStdString(entity->declaration().name());
|
||||
if (!state.property_sets.isEmpty() && !state.property_sets[1].rows.isEmpty()) {
|
||||
state.property_sets[1].rows[0].value = state.entity.entity_class;
|
||||
}
|
||||
}
|
||||
widget_->render(state);
|
||||
}
|
||||
|
||||
} // namespace ifcinterface::panels::properties
|
||||
} // namespace ifcinterface::modules::properties
|
||||
@@ -26,24 +26,24 @@
|
||||
#include <QObject>
|
||||
|
||||
namespace ifcinterface { class SessionState; }
|
||||
namespace ifcinterface::panels::properties {
|
||||
namespace ifcinterface::modules::properties {
|
||||
|
||||
class PropertiesPanelWidget;
|
||||
class PropertiesPanel;
|
||||
|
||||
class PropertiesPanelView : public QObject {
|
||||
Q_OBJECT
|
||||
public:
|
||||
explicit PropertiesPanelView(PropertiesPanelWidget* widget,
|
||||
explicit PropertiesPanelView(PropertiesPanel* widget,
|
||||
ifcinterface::SessionState* session_state,
|
||||
QObject* parent = nullptr);
|
||||
|
||||
private:
|
||||
void refresh(uint32_t object_id);
|
||||
|
||||
PropertiesPanelWidget* widget_ = nullptr;
|
||||
PropertiesPanel* widget_ = nullptr;
|
||||
ifcinterface::SessionState* session_state_ = nullptr;
|
||||
};
|
||||
|
||||
} // namespace ifcinterface::panels::properties
|
||||
} // namespace ifcinterface::modules::properties
|
||||
|
||||
#endif
|
||||
@@ -0,0 +1,357 @@
|
||||
// This file was generated with the assistance of an AI coding tool.
|
||||
/********************************************************************************
|
||||
* *
|
||||
* This file is part of IfcOpenShell. *
|
||||
* *
|
||||
* IfcOpenShell is free software: you can redistribute it and/or modify *
|
||||
* it under the terms of the Lesser GNU General Public License as published by *
|
||||
* the Free Software Foundation, either version 3.0 of the License, or *
|
||||
* (at your option) any later version. *
|
||||
* *
|
||||
* IfcOpenShell is distributed in the hope that it will be useful, *
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of *
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the *
|
||||
* Lesser GNU General Public License for more details. *
|
||||
* *
|
||||
* You should have received a copy of the Lesser GNU General Public License *
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>. *
|
||||
* *
|
||||
********************************************************************************/
|
||||
|
||||
#include "Dialog.h"
|
||||
|
||||
#include "../../InterfaceSettings.h"
|
||||
#include "../../../ifcviewer/AppSettings.h"
|
||||
#include "../../components/Dialog.h"
|
||||
#include "../../components/Section.h"
|
||||
#include "../../components/SvgIcon.h"
|
||||
#include "../../components/Style.h"
|
||||
|
||||
#include <QCheckBox>
|
||||
#include <QColorDialog>
|
||||
#include <QComboBox>
|
||||
#include <QDialogButtonBox>
|
||||
#include <QDoubleSpinBox>
|
||||
#include <QFrame>
|
||||
#include <QFormLayout>
|
||||
#include <QGridLayout>
|
||||
#include <QLabel>
|
||||
#include <QLineEdit>
|
||||
#include <QPushButton>
|
||||
#include <QShowEvent>
|
||||
#include <QSpinBox>
|
||||
#include <QVBoxLayout>
|
||||
|
||||
namespace ifcinterface::modules::settings {
|
||||
|
||||
SettingsDialog::SettingsDialog(QWidget* parent)
|
||||
: components::TabbedDialog(parent)
|
||||
{
|
||||
setObjectName("appDialog");
|
||||
setWindowTitle("Settings");
|
||||
setModal(true);
|
||||
resize(520, 420);
|
||||
setupUi();
|
||||
}
|
||||
|
||||
void SettingsDialog::showEvent(QShowEvent* event) {
|
||||
syncFromSettings();
|
||||
QDialog::showEvent(event);
|
||||
}
|
||||
|
||||
void SettingsDialog::setupUi() {
|
||||
auto* graphics_tab = new QWidget(this);
|
||||
auto* graphics_layout = new QVBoxLayout(graphics_tab);
|
||||
graphics_layout->setContentsMargins(0, 0, 0, 0);
|
||||
graphics_layout->setSpacing(components::style::metrics::padding);
|
||||
graphics_layout->setAlignment(Qt::AlignTop);
|
||||
|
||||
auto* general_section = new components::Section("General", components::SectionHeaderMode::Visible, graphics_tab);
|
||||
auto* general_body = new QWidget(general_section);
|
||||
auto* general_form = new QFormLayout(general_body);
|
||||
general_form->setContentsMargins(0, 0, 0, 0);
|
||||
general_form->setHorizontalSpacing(16);
|
||||
general_form->setVerticalSpacing(10);
|
||||
|
||||
geometry_library_edit_ = new QLineEdit(general_body);
|
||||
geometry_library_edit_->setMinimumWidth(300);
|
||||
general_form->addRow("Geometry Library", geometry_library_edit_);
|
||||
|
||||
show_stats_check_ = new QCheckBox(general_body);
|
||||
general_form->addRow("Show Performance Stats", show_stats_check_);
|
||||
|
||||
backface_culling_check_ = new QCheckBox(general_body);
|
||||
backface_culling_check_->setToolTip(
|
||||
"Skip triangles facing away from the camera. Big FPS win on closed solids; "
|
||||
"disable if you see holes in open geometry.");
|
||||
general_form->addRow("Backface Culling", backface_culling_check_);
|
||||
|
||||
general_section->addBodyWidget(general_body);
|
||||
|
||||
auto* loading_section = new components::Section("Loading", components::SectionHeaderMode::Visible, graphics_tab);
|
||||
auto* loading_body = new QWidget(loading_section);
|
||||
auto* loading_form = new QFormLayout(loading_body);
|
||||
loading_form->setContentsMargins(0, 0, 0, 0);
|
||||
loading_form->setHorizontalSpacing(16);
|
||||
loading_form->setVerticalSpacing(10);
|
||||
|
||||
void_limit_spin_ = new QSpinBox(loading_body);
|
||||
void_limit_spin_->setRange(0, 100000);
|
||||
loading_form->addRow("Void Limit", void_limit_spin_);
|
||||
|
||||
deflection_tolerance_spin_ = new QDoubleSpinBox(loading_body);
|
||||
deflection_tolerance_spin_->setRange(0.000001, 1000.0);
|
||||
deflection_tolerance_spin_->setDecimals(6);
|
||||
deflection_tolerance_spin_->setSingleStep(0.001);
|
||||
loading_form->addRow("Deflection Tolerance", deflection_tolerance_spin_);
|
||||
|
||||
angular_tolerance_spin_ = new QDoubleSpinBox(loading_body);
|
||||
angular_tolerance_spin_->setRange(0.000001, 3.141592);
|
||||
angular_tolerance_spin_->setDecimals(6);
|
||||
angular_tolerance_spin_->setSingleStep(0.05);
|
||||
loading_form->addRow("Angular Tolerance", angular_tolerance_spin_);
|
||||
|
||||
min_pixel_radius_spin_ = new QDoubleSpinBox(loading_body);
|
||||
min_pixel_radius_spin_->setRange(0.0, 100.0);
|
||||
min_pixel_radius_spin_->setDecimals(2);
|
||||
min_pixel_radius_spin_->setSingleStep(0.5);
|
||||
min_pixel_radius_spin_->setToolTip(
|
||||
"Minimum projected sphere radius (in pixels) for an instance to "
|
||||
"be drawn. Bigger = faster but more pop-in on small detail.");
|
||||
loading_form->addRow("Min Pixel Radius", min_pixel_radius_spin_);
|
||||
|
||||
motion_min_pixel_radius_spin_ = new QDoubleSpinBox(loading_body);
|
||||
motion_min_pixel_radius_spin_->setRange(0.0, 100.0);
|
||||
motion_min_pixel_radius_spin_->setDecimals(2);
|
||||
motion_min_pixel_radius_spin_->setSingleStep(1.0);
|
||||
motion_min_pixel_radius_spin_->setToolTip(
|
||||
"Aggressive cull threshold while the camera is moving. 0 = no "
|
||||
"motion boost (motion uses the same threshold as still frames).");
|
||||
loading_form->addRow("Motion Min Pixel Radius", motion_min_pixel_radius_spin_);
|
||||
|
||||
lod1_pixel_threshold_spin_ = new QDoubleSpinBox(loading_body);
|
||||
lod1_pixel_threshold_spin_->setRange(0.0, 1000.0);
|
||||
lod1_pixel_threshold_spin_->setDecimals(1);
|
||||
lod1_pixel_threshold_spin_->setSingleStep(1.0);
|
||||
lod1_pixel_threshold_spin_->setToolTip(
|
||||
"Pixel radius below which an instance switches to its LOD1 "
|
||||
"representation. 0 disables LOD1 entirely.");
|
||||
loading_form->addRow("LOD1 Pixel Threshold", lod1_pixel_threshold_spin_);
|
||||
|
||||
hiz_enabled_check_ = new QCheckBox(loading_body);
|
||||
hiz_enabled_check_->setToolTip(
|
||||
"Enable HiZ (hierarchical Z) occlusion culling. Hides geometry "
|
||||
"behind opaque blockers based on a downsampled depth pyramid.");
|
||||
loading_form->addRow("HiZ Occlusion", hiz_enabled_check_);
|
||||
|
||||
hiz_resolution_spin_ = new QSpinBox(loading_body);
|
||||
hiz_resolution_spin_->setRange(64, 4096);
|
||||
hiz_resolution_spin_->setSingleStep(64);
|
||||
hiz_resolution_spin_->setToolTip(
|
||||
"Base HiZ pyramid width in texels (height tracks aspect). "
|
||||
"Changes take effect on next viewport reinitialization.");
|
||||
loading_form->addRow("HiZ Resolution", hiz_resolution_spin_);
|
||||
|
||||
loading_section->addBodyWidget(loading_body);
|
||||
graphics_layout->addWidget(general_section);
|
||||
graphics_layout->addWidget(loading_section);
|
||||
graphics_layout->addStretch(1);
|
||||
|
||||
// Navigation tab: orbit / pan presets. Selection always stays on
|
||||
// LMB so click + box-select keep working regardless of preset.
|
||||
auto* interface_tab = new QWidget(this);
|
||||
{
|
||||
auto* layout = new QVBoxLayout(interface_tab);
|
||||
layout->setContentsMargins(0, 0, 0, 0);
|
||||
layout->setSpacing(components::style::metrics::padding);
|
||||
layout->setAlignment(Qt::AlignTop);
|
||||
|
||||
auto* section = new components::Section(
|
||||
"Navigation", components::SectionHeaderMode::Visible, interface_tab);
|
||||
auto* body = new QWidget(section);
|
||||
auto* form = new QFormLayout(body);
|
||||
form->setContentsMargins(0, 0, 0, 0);
|
||||
form->setHorizontalSpacing(16);
|
||||
form->setVerticalSpacing(10);
|
||||
|
||||
nav_preset_combo_ = new QComboBox(body);
|
||||
// Order must match AppSettings::NavPreset enum ordering — index
|
||||
// is what we read back via currentIndex / setCurrentIndex.
|
||||
nav_preset_combo_->addItem("Blender (Orbit MMB, Pan Shift+MMB)");
|
||||
nav_preset_combo_->addItem("Rhino (Orbit RMB, Pan Shift+RMB)");
|
||||
nav_preset_combo_->addItem("Revit (Orbit Shift+MMB, Pan MMB)");
|
||||
nav_preset_combo_->setToolTip(
|
||||
"Mouse-button mapping for orbit and pan. Selection stays on "
|
||||
"left mouse button for every preset, so click + box-select "
|
||||
"always work.");
|
||||
form->addRow("Preset", nav_preset_combo_);
|
||||
|
||||
section->addBodyWidget(body);
|
||||
layout->addWidget(section);
|
||||
|
||||
auto* theme_section = new components::Section(
|
||||
"Theme", components::SectionHeaderMode::Visible, interface_tab);
|
||||
auto* theme_body = new QWidget(theme_section);
|
||||
auto* theme_layout = new QVBoxLayout(theme_body);
|
||||
theme_layout->setContentsMargins(0, 0, 0, 0);
|
||||
theme_layout->setSpacing(components::style::metrics::padding);
|
||||
|
||||
auto* theme_form = new QFormLayout();
|
||||
theme_form->setContentsMargins(0, 0, 0, 0);
|
||||
theme_form->setHorizontalSpacing(16);
|
||||
theme_form->setVerticalSpacing(10);
|
||||
|
||||
theme_mode_combo_ = new QComboBox(theme_body);
|
||||
theme_mode_combo_->addItem("Dark", static_cast<int>(ifcinterface::InterfaceSettings::ThemeMode::Dark));
|
||||
theme_mode_combo_->addItem("Light", static_cast<int>(ifcinterface::InterfaceSettings::ThemeMode::Light));
|
||||
theme_mode_combo_->addItem("Custom", static_cast<int>(ifcinterface::InterfaceSettings::ThemeMode::Custom));
|
||||
theme_form->addRow("Preset", theme_mode_combo_);
|
||||
|
||||
theme_custom_body_ = new QWidget(theme_body);
|
||||
auto* custom_grid = new QGridLayout(theme_custom_body_);
|
||||
custom_grid->setContentsMargins(0, 0, 0, 0);
|
||||
custom_grid->setHorizontalSpacing(12);
|
||||
custom_grid->setVerticalSpacing(8);
|
||||
|
||||
int row = 0;
|
||||
for (const auto& spec : ifcinterface::InterfaceSettings::themeColorSpecs()) {
|
||||
auto* label = new QLabel(QString::fromUtf8(spec.label), theme_custom_body_);
|
||||
auto* edit = new QLineEdit(theme_custom_body_);
|
||||
edit->setPlaceholderText("#000000");
|
||||
auto* pick = new QPushButton("Pick", theme_custom_body_);
|
||||
connect(pick, &QPushButton::clicked, this, [this, edit]() { pickThemeColor(edit); });
|
||||
custom_grid->addWidget(label, row, 0);
|
||||
custom_grid->addWidget(edit, row, 1);
|
||||
custom_grid->addWidget(pick, row, 2);
|
||||
theme_color_editors_.push_back({QString::fromUtf8(spec.key), edit});
|
||||
++row;
|
||||
}
|
||||
|
||||
auto* theme_form_widget = new QWidget(theme_body);
|
||||
theme_form_widget->setLayout(theme_form);
|
||||
theme_layout->addWidget(theme_form_widget);
|
||||
theme_layout->addWidget(theme_custom_body_);
|
||||
theme_section->addBodyWidget(theme_body);
|
||||
layout->addWidget(theme_section);
|
||||
|
||||
connect(theme_mode_combo_, &QComboBox::currentIndexChanged, this, [this](int) {
|
||||
updateThemeEditorEnabled();
|
||||
});
|
||||
}
|
||||
|
||||
auto make_placeholder_tab = [this](const QString& title, const QString& detail) {
|
||||
auto* tab = new QWidget(this);
|
||||
auto* layout = new QVBoxLayout(tab);
|
||||
layout->setContentsMargins(0, 0, 0, 0);
|
||||
layout->setSpacing(components::style::metrics::padding);
|
||||
layout->setAlignment(Qt::AlignTop);
|
||||
|
||||
auto* section = new components::Section(title, components::SectionHeaderMode::Visible, tab);
|
||||
auto* body = new QWidget(section);
|
||||
auto* body_layout = new QVBoxLayout(body);
|
||||
body_layout->setContentsMargins(0, 0, 0, 0);
|
||||
body_layout->setSpacing(8);
|
||||
|
||||
auto* heading = new QLabel(title, body);
|
||||
auto* content = new QLabel(detail, body);
|
||||
content->setProperty("textRole", "secondary");
|
||||
content->setWordWrap(true);
|
||||
|
||||
body_layout->addWidget(heading);
|
||||
body_layout->addWidget(content);
|
||||
section->addBodyWidget(body);
|
||||
layout->addWidget(section);
|
||||
layout->addStretch(1);
|
||||
return tab;
|
||||
};
|
||||
|
||||
addTab("Interface", interface_tab);
|
||||
addTab("Keybindings", make_placeholder_tab("Keybindings", "Shortcut presets and command bindings will live here."));
|
||||
addTab("Graphics", graphics_tab);
|
||||
addTab("About", make_placeholder_tab("About", "Version, credits, and environment information will live here."));
|
||||
|
||||
auto* buttons = new QDialogButtonBox(QDialogButtonBox::Ok | QDialogButtonBox::Cancel, this);
|
||||
if (auto* ok = buttons->button(QDialogButtonBox::Ok)) {
|
||||
ok->setText("OK");
|
||||
ok->setIcon(components::icons::makeSvgIcon(":/icons/check.svg"));
|
||||
}
|
||||
if (auto* cancel = buttons->button(QDialogButtonBox::Cancel)) {
|
||||
cancel->setText("Cancel");
|
||||
cancel->setIcon(components::icons::makeSvgIcon(":/icons/xmark-circle.svg"));
|
||||
}
|
||||
connect(buttons, &QDialogButtonBox::accepted, this, &SettingsDialog::onAccepted);
|
||||
connect(buttons, &QDialogButtonBox::rejected, this, &QDialog::reject);
|
||||
|
||||
addFooterWidget(buttons);
|
||||
}
|
||||
|
||||
void SettingsDialog::syncFromSettings() {
|
||||
geometry_library_edit_->setText(AppSettings::instance().geometryLibrary());
|
||||
show_stats_check_->setChecked(AppSettings::instance().showStats());
|
||||
backface_culling_check_->setChecked(AppSettings::instance().backfaceCulling());
|
||||
void_limit_spin_->setValue(AppSettings::instance().voidLimit());
|
||||
deflection_tolerance_spin_->setValue(AppSettings::instance().deflectionTolerance());
|
||||
angular_tolerance_spin_->setValue(AppSettings::instance().angularTolerance());
|
||||
min_pixel_radius_spin_->setValue(AppSettings::instance().minPixelRadius());
|
||||
motion_min_pixel_radius_spin_->setValue(AppSettings::instance().motionMinPixelRadius());
|
||||
lod1_pixel_threshold_spin_->setValue(AppSettings::instance().lod1PixelThreshold());
|
||||
hiz_enabled_check_->setChecked(AppSettings::instance().hizEnabled());
|
||||
hiz_resolution_spin_->setValue(AppSettings::instance().hizResolution());
|
||||
nav_preset_combo_->setCurrentIndex(static_cast<int>(AppSettings::instance().navPreset()));
|
||||
syncThemeSettings();
|
||||
}
|
||||
|
||||
void SettingsDialog::syncThemeSettings() {
|
||||
const auto& settings = ifcinterface::InterfaceSettings::instance();
|
||||
const int idx = theme_mode_combo_->findData(static_cast<int>(settings.themeMode()));
|
||||
theme_mode_combo_->setCurrentIndex(idx >= 0 ? idx : 0);
|
||||
for (auto& editor : theme_color_editors_) {
|
||||
editor.edit->setText(settings.customColor(editor.key));
|
||||
}
|
||||
updateThemeEditorEnabled();
|
||||
}
|
||||
|
||||
void SettingsDialog::updateThemeEditorEnabled() {
|
||||
if (!theme_mode_combo_ || !theme_custom_body_) return;
|
||||
const auto mode =
|
||||
static_cast<ifcinterface::InterfaceSettings::ThemeMode>(theme_mode_combo_->currentData().toInt());
|
||||
const bool is_custom = mode == ifcinterface::InterfaceSettings::ThemeMode::Custom;
|
||||
theme_custom_body_->setVisible(is_custom);
|
||||
theme_custom_body_->setEnabled(is_custom);
|
||||
}
|
||||
|
||||
void SettingsDialog::pickThemeColor(QLineEdit* edit) {
|
||||
QColorDialog dialog(QColor(edit->text()), this);
|
||||
dialog.setObjectName("appDialog");
|
||||
dialog.setWindowTitle("Choose Color");
|
||||
dialog.setOption(QColorDialog::DontUseNativeDialog, true);
|
||||
if (dialog.exec() != QDialog::Accepted) return;
|
||||
const QColor color = dialog.selectedColor();
|
||||
if (!color.isValid()) return;
|
||||
edit->setText(color.name(QColor::HexRgb));
|
||||
}
|
||||
|
||||
void SettingsDialog::onAccepted() {
|
||||
AppSettings::instance().setGeometryLibrary(geometry_library_edit_->text());
|
||||
AppSettings::instance().setShowStats(show_stats_check_->isChecked());
|
||||
AppSettings::instance().setBackfaceCulling(backface_culling_check_->isChecked());
|
||||
AppSettings::instance().setVoidLimit(void_limit_spin_->value());
|
||||
AppSettings::instance().setDeflectionTolerance(deflection_tolerance_spin_->value());
|
||||
AppSettings::instance().setAngularTolerance(angular_tolerance_spin_->value());
|
||||
AppSettings::instance().setMinPixelRadius(min_pixel_radius_spin_->value());
|
||||
AppSettings::instance().setMotionMinPixelRadius(motion_min_pixel_radius_spin_->value());
|
||||
AppSettings::instance().setLod1PixelThreshold(lod1_pixel_threshold_spin_->value());
|
||||
AppSettings::instance().setHizEnabled(hiz_enabled_check_->isChecked());
|
||||
AppSettings::instance().setHizResolution(hiz_resolution_spin_->value());
|
||||
AppSettings::instance().setNavPreset(
|
||||
static_cast<AppSettings::NavPreset>(nav_preset_combo_->currentIndex()));
|
||||
auto& interface_settings = ifcinterface::InterfaceSettings::instance();
|
||||
interface_settings.setThemeMode(
|
||||
static_cast<ifcinterface::InterfaceSettings::ThemeMode>(theme_mode_combo_->currentData().toInt()));
|
||||
for (const auto& editor : theme_color_editors_) {
|
||||
interface_settings.setCustomColor(editor.key, editor.edit->text());
|
||||
}
|
||||
accept();
|
||||
}
|
||||
|
||||
} // namespace ifcinterface::modules::settings
|
||||
@@ -23,15 +23,20 @@
|
||||
|
||||
#include "../../components/Dialog.h"
|
||||
|
||||
#include <QString>
|
||||
#include <vector>
|
||||
|
||||
class QCheckBox;
|
||||
class QComboBox;
|
||||
class QDoubleSpinBox;
|
||||
class QLineEdit;
|
||||
class QShowEvent;
|
||||
class QSpinBox;
|
||||
class QWidget;
|
||||
|
||||
namespace ifcinterface::panels::settings {
|
||||
namespace ifcinterface::modules::settings {
|
||||
|
||||
class SettingsDialog : public components::Dialog {
|
||||
class SettingsDialog : public components::TabbedDialog {
|
||||
Q_OBJECT
|
||||
public:
|
||||
explicit SettingsDialog(QWidget* parent = nullptr);
|
||||
@@ -42,18 +47,33 @@ protected:
|
||||
private:
|
||||
void setupUi();
|
||||
void syncFromSettings();
|
||||
void syncThemeSettings();
|
||||
void updateThemeEditorEnabled();
|
||||
void pickThemeColor(QLineEdit* edit);
|
||||
void onAccepted();
|
||||
|
||||
struct ThemeColorEditor {
|
||||
QString key;
|
||||
QLineEdit* edit = nullptr;
|
||||
};
|
||||
|
||||
QLineEdit* geometry_library_edit_ = nullptr;
|
||||
QCheckBox* show_stats_check_ = nullptr;
|
||||
QCheckBox* backface_culling_check_ = nullptr;
|
||||
QCheckBox* load_data_source_checkbox_ = nullptr;
|
||||
QCheckBox* apply_coordinate_operation_check_ = nullptr;
|
||||
QSpinBox* void_limit_spin_ = nullptr;
|
||||
QDoubleSpinBox* deflection_tolerance_spin_ = nullptr;
|
||||
QDoubleSpinBox* angular_tolerance_spin_ = nullptr;
|
||||
QDoubleSpinBox* min_pixel_radius_spin_ = nullptr;
|
||||
QDoubleSpinBox* motion_min_pixel_radius_spin_ = nullptr;
|
||||
QDoubleSpinBox* lod1_pixel_threshold_spin_ = nullptr;
|
||||
QCheckBox* hiz_enabled_check_ = nullptr;
|
||||
QSpinBox* hiz_resolution_spin_ = nullptr;
|
||||
QComboBox* nav_preset_combo_ = nullptr;
|
||||
QComboBox* theme_mode_combo_ = nullptr;
|
||||
QWidget* theme_custom_body_ = nullptr;
|
||||
std::vector<ThemeColorEditor> theme_color_editors_;
|
||||
};
|
||||
|
||||
} // namespace ifcinterface::panels::settings
|
||||
} // namespace ifcinterface::modules::settings
|
||||
|
||||
#endif
|
||||
+8
-8
@@ -18,7 +18,7 @@
|
||||
* *
|
||||
********************************************************************************/
|
||||
|
||||
#include "Widget.h"
|
||||
#include "Panel.h"
|
||||
|
||||
#include "../../components/Section.h"
|
||||
#include "../../components/SvgIcon.h"
|
||||
@@ -27,9 +27,9 @@
|
||||
#include <QTreeWidget>
|
||||
#include <QTreeWidgetItem>
|
||||
|
||||
namespace ifcinterface::panels::spatial_hierarchy {
|
||||
namespace ifcinterface::modules::spatial_hierarchy {
|
||||
|
||||
SpatialHierarchyPanelWidget::SpatialHierarchyPanelWidget(QWidget* parent)
|
||||
SpatialHierarchyPanel::SpatialHierarchyPanel(QWidget* parent)
|
||||
: components::Panel("Spatial Hierarchy", nullptr, parent)
|
||||
{
|
||||
auto* section = new components::Section("", components::SectionHeaderMode::Hidden, this);
|
||||
@@ -54,7 +54,7 @@ SpatialHierarchyPanelWidget::SpatialHierarchyPanelWidget(QWidget* parent)
|
||||
});
|
||||
}
|
||||
|
||||
void SpatialHierarchyPanelWidget::setNodes(const QList<TreeNode>& nodes) {
|
||||
void SpatialHierarchyPanel::setNodes(const QList<TreeNode>& nodes) {
|
||||
tree_->clear();
|
||||
for (const auto& node : nodes) {
|
||||
addNode(tree_->invisibleRootItem(), node);
|
||||
@@ -62,7 +62,7 @@ void SpatialHierarchyPanelWidget::setNodes(const QList<TreeNode>& nodes) {
|
||||
tree_->expandAll();
|
||||
}
|
||||
|
||||
void SpatialHierarchyPanelWidget::addNode(QTreeWidgetItem* parent, const TreeNode& node) {
|
||||
void SpatialHierarchyPanel::addNode(QTreeWidgetItem* parent, const TreeNode& node) {
|
||||
auto* item = new QTreeWidgetItem(parent, {node.name, ""});
|
||||
item->setData(1, Qt::UserRole, node.visible);
|
||||
item->setSizeHint(0, QSize(0, 24));
|
||||
@@ -73,7 +73,7 @@ void SpatialHierarchyPanelWidget::addNode(QTreeWidgetItem* parent, const TreeNod
|
||||
}
|
||||
}
|
||||
|
||||
NodePath SpatialHierarchyPanelWidget::itemPath(QTreeWidgetItem* item) const {
|
||||
NodePath SpatialHierarchyPanel::itemPath(QTreeWidgetItem* item) const {
|
||||
NodePath path;
|
||||
while (item) {
|
||||
path.prepend(item->text(0));
|
||||
@@ -82,7 +82,7 @@ NodePath SpatialHierarchyPanelWidget::itemPath(QTreeWidgetItem* item) const {
|
||||
return path;
|
||||
}
|
||||
|
||||
QString SpatialHierarchyPanelWidget::iconPath(ItemKind kind) const {
|
||||
QString SpatialHierarchyPanel::iconPath(ItemKind kind) const {
|
||||
switch (kind) {
|
||||
case ItemKind::Site: return ":/icons/frame-alt.svg";
|
||||
case ItemKind::Building: return ":/icons/city.svg";
|
||||
@@ -92,4 +92,4 @@ QString SpatialHierarchyPanelWidget::iconPath(ItemKind kind) const {
|
||||
return ":/icons/frame-alt.svg";
|
||||
}
|
||||
|
||||
} // namespace ifcinterface::panels::spatial_hierarchy
|
||||
} // namespace ifcinterface::modules::spatial_hierarchy
|
||||
+6
-6
@@ -18,8 +18,8 @@
|
||||
* *
|
||||
********************************************************************************/
|
||||
|
||||
#ifndef IFCINTERFACE_PANELS_SPATIALHIERARCHYPANELWIDGET_H
|
||||
#define IFCINTERFACE_PANELS_SPATIALHIERARCHYPANELWIDGET_H
|
||||
#ifndef IFCINTERFACE_MODULES_SPATIAL_HIERARCHY_PANEL_H
|
||||
#define IFCINTERFACE_MODULES_SPATIAL_HIERARCHY_PANEL_H
|
||||
|
||||
#include "Types.h"
|
||||
|
||||
@@ -28,12 +28,12 @@
|
||||
class QTreeWidget;
|
||||
class QTreeWidgetItem;
|
||||
|
||||
namespace ifcinterface::panels::spatial_hierarchy {
|
||||
namespace ifcinterface::modules::spatial_hierarchy {
|
||||
|
||||
class SpatialHierarchyPanelWidget : public components::Panel {
|
||||
class SpatialHierarchyPanel : public components::Panel {
|
||||
Q_OBJECT
|
||||
public:
|
||||
explicit SpatialHierarchyPanelWidget(QWidget* parent = nullptr);
|
||||
explicit SpatialHierarchyPanel(QWidget* parent = nullptr);
|
||||
|
||||
void setNodes(const QList<TreeNode>& nodes);
|
||||
|
||||
@@ -48,6 +48,6 @@ private:
|
||||
QTreeWidget* tree_ = nullptr;
|
||||
};
|
||||
|
||||
} // namespace ifcinterface::panels::spatial_hierarchy
|
||||
} // namespace ifcinterface::modules::spatial_hierarchy
|
||||
|
||||
#endif
|
||||
+2
-2
@@ -25,7 +25,7 @@
|
||||
#include <QString>
|
||||
#include <QStringList>
|
||||
|
||||
namespace ifcinterface::panels::spatial_hierarchy {
|
||||
namespace ifcinterface::modules::spatial_hierarchy {
|
||||
|
||||
enum class ItemKind {
|
||||
Site,
|
||||
@@ -43,6 +43,6 @@ struct TreeNode {
|
||||
|
||||
using NodePath = QStringList;
|
||||
|
||||
} // namespace ifcinterface::panels::spatial_hierarchy
|
||||
} // namespace ifcinterface::modules::spatial_hierarchy
|
||||
|
||||
#endif
|
||||
+5
-5
@@ -20,11 +20,11 @@
|
||||
|
||||
#include "View.h"
|
||||
|
||||
#include "Widget.h"
|
||||
#include "Panel.h"
|
||||
|
||||
#include "../../SessionState.h"
|
||||
|
||||
namespace ifcinterface::panels::spatial_hierarchy {
|
||||
namespace ifcinterface::modules::spatial_hierarchy {
|
||||
|
||||
namespace {
|
||||
|
||||
@@ -39,7 +39,7 @@ TreeNode* findNodeRecursive(QList<TreeNode>& nodes, const NodePath& path, int de
|
||||
|
||||
} // namespace
|
||||
|
||||
SpatialHierarchyPanelView::SpatialHierarchyPanelView(SpatialHierarchyPanelWidget* widget,
|
||||
SpatialHierarchyPanelView::SpatialHierarchyPanelView(SpatialHierarchyPanel* widget,
|
||||
ifcinterface::SessionState* session_state,
|
||||
QObject* parent)
|
||||
: QObject(parent), widget_(widget), session_state_(session_state)
|
||||
@@ -52,7 +52,7 @@ SpatialHierarchyPanelView::SpatialHierarchyPanelView(SpatialHierarchyPanelWidget
|
||||
{"Core", ItemKind::Space, true, {}}}}}}}},
|
||||
};
|
||||
|
||||
connect(widget_, &SpatialHierarchyPanelWidget::visibilityToggleRequested, this, [this](const NodePath& path) {
|
||||
connect(widget_, &SpatialHierarchyPanel::visibilityToggleRequested, this, [this](const NodePath& path) {
|
||||
if (auto* node = findNode(path)) {
|
||||
node->visible = !node->visible;
|
||||
reload();
|
||||
@@ -72,4 +72,4 @@ TreeNode* SpatialHierarchyPanelView::findNode(const NodePath& path) {
|
||||
return findNodeRecursive(nodes_, path, 0);
|
||||
}
|
||||
|
||||
} // namespace ifcinterface::panels::spatial_hierarchy
|
||||
} // namespace ifcinterface::modules::spatial_hierarchy
|
||||
+5
-5
@@ -26,14 +26,14 @@
|
||||
#include <QObject>
|
||||
|
||||
namespace ifcinterface { class SessionState; }
|
||||
namespace ifcinterface::panels::spatial_hierarchy {
|
||||
namespace ifcinterface::modules::spatial_hierarchy {
|
||||
|
||||
class SpatialHierarchyPanelWidget;
|
||||
class SpatialHierarchyPanel;
|
||||
|
||||
class SpatialHierarchyPanelView : public QObject {
|
||||
Q_OBJECT
|
||||
public:
|
||||
explicit SpatialHierarchyPanelView(SpatialHierarchyPanelWidget* widget,
|
||||
explicit SpatialHierarchyPanelView(SpatialHierarchyPanel* widget,
|
||||
ifcinterface::SessionState* session_state,
|
||||
QObject* parent = nullptr);
|
||||
|
||||
@@ -41,11 +41,11 @@ private:
|
||||
void reload();
|
||||
TreeNode* findNode(const NodePath& path);
|
||||
|
||||
SpatialHierarchyPanelWidget* widget_ = nullptr;
|
||||
SpatialHierarchyPanel* widget_ = nullptr;
|
||||
ifcinterface::SessionState* session_state_ = nullptr;
|
||||
QList<TreeNode> nodes_;
|
||||
};
|
||||
|
||||
} // namespace ifcinterface::panels::spatial_hierarchy
|
||||
} // namespace ifcinterface::modules::spatial_hierarchy
|
||||
|
||||
#endif
|
||||
@@ -18,16 +18,16 @@
|
||||
* *
|
||||
********************************************************************************/
|
||||
|
||||
#include "Widget.h"
|
||||
#include "Panel.h"
|
||||
|
||||
#include "../../components/Section.h"
|
||||
|
||||
#include <QLabel>
|
||||
#include <QVBoxLayout>
|
||||
|
||||
namespace ifcinterface::panels::todo {
|
||||
namespace ifcinterface::modules::todo {
|
||||
|
||||
TodoPanelWidget::TodoPanelWidget(const QString& title, QWidget* parent)
|
||||
TodoPanel::TodoPanel(const QString& title, QWidget* parent)
|
||||
: QWidget(parent)
|
||||
{
|
||||
auto* layout = new QVBoxLayout(this);
|
||||
@@ -56,4 +56,4 @@ TodoPanelWidget::TodoPanelWidget(const QString& title, QWidget* parent)
|
||||
layout->addWidget(section);
|
||||
}
|
||||
|
||||
} // namespace ifcinterface::panels::todo
|
||||
} // namespace ifcinterface::modules::todo
|
||||
@@ -18,19 +18,19 @@
|
||||
* *
|
||||
********************************************************************************/
|
||||
|
||||
#ifndef IFCINTERFACE_PANELS_TODO_TODOPANELWIDGET_H
|
||||
#define IFCINTERFACE_PANELS_TODO_TODOPANELWIDGET_H
|
||||
#ifndef IFCINTERFACE_MODULES_TODO_PANEL_H
|
||||
#define IFCINTERFACE_MODULES_TODO_PANEL_H
|
||||
|
||||
#include <QWidget>
|
||||
|
||||
namespace ifcinterface::panels::todo {
|
||||
namespace ifcinterface::modules::todo {
|
||||
|
||||
class TodoPanelWidget : public QWidget {
|
||||
class TodoPanel : public QWidget {
|
||||
Q_OBJECT
|
||||
public:
|
||||
explicit TodoPanelWidget(const QString& title, QWidget* parent = nullptr);
|
||||
explicit TodoPanel(const QString& title, QWidget* parent = nullptr);
|
||||
};
|
||||
|
||||
} // namespace ifcinterface::panels::todo
|
||||
} // namespace ifcinterface::modules::todo
|
||||
|
||||
#endif
|
||||
@@ -0,0 +1,337 @@
|
||||
// This file was generated with the assistance of an AI coding tool.
|
||||
/********************************************************************************
|
||||
* *
|
||||
* This file is part of IfcOpenShell. *
|
||||
* *
|
||||
* IfcOpenShell is free software: you can redistribute it and/or modify *
|
||||
* it under the terms of the Lesser GNU General Public License as published by *
|
||||
* the Free Software Foundation, either version 3.0 of the License, or *
|
||||
* (at your option) any later version. *
|
||||
* *
|
||||
* IfcOpenShell is distributed in the hope that it will be useful, *
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of *
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the *
|
||||
* Lesser GNU General Public License for more details. *
|
||||
* *
|
||||
* You should have received a copy of the Lesser GNU General Public License *
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>. *
|
||||
* *
|
||||
********************************************************************************/
|
||||
|
||||
#include "Controller.h"
|
||||
|
||||
#include "../../InterfaceSettings.h"
|
||||
#include "../../SessionState.h"
|
||||
#include "../../../ifcviewer/AppSettings.h"
|
||||
#include "../../../ifcviewer/Federation.h"
|
||||
#include "../../../ifcviewer/SceneLoader.h"
|
||||
#include "../../../ifcviewer/ViewportWindow.h"
|
||||
#include "../../../ifcviewer/OverlayRenderer.h"
|
||||
#include "../../../ifcviewer-full/Measurement.h"
|
||||
|
||||
#include <Eigen/Dense>
|
||||
#include <QVector3D>
|
||||
|
||||
#include <vector>
|
||||
|
||||
namespace ifcinterface::modules::viewport {
|
||||
|
||||
ViewportController::ViewportController(ifcinterface::SessionState* session_state,
|
||||
ViewportWindow* viewport,
|
||||
QObject* parent)
|
||||
: QObject(parent)
|
||||
, session_state_(session_state)
|
||||
, viewport_(viewport)
|
||||
, area_measurement_(std::make_unique<AreaMeasurement>())
|
||||
, length_measurement_(std::make_unique<LengthMeasurement>())
|
||||
{
|
||||
Federation* federation = session_state_->federation();
|
||||
SceneLoader* loader = session_state_->loader();
|
||||
connect(&ifcinterface::InterfaceSettings::instance(),
|
||||
&ifcinterface::InterfaceSettings::themeChanged,
|
||||
this, [this]() {
|
||||
viewport_->setBackgroundColor(QColor(ifcinterface::InterfaceSettings::instance().color("viewport_background")));
|
||||
});
|
||||
viewport_->setBackgroundColor(QColor(ifcinterface::InterfaceSettings::instance().color("viewport_background")));
|
||||
connect(federation, &Federation::federatedFalseOriginChanged,
|
||||
this, &ViewportController::applyFederatedFalseOrigin);
|
||||
connect(federation, &Federation::configChanged, this, [this]() {
|
||||
applyFederatedFalseOrigin();
|
||||
for (uint32_t mid : session_state_->modelIds()) {
|
||||
applyModelTransformation(mid);
|
||||
}
|
||||
});
|
||||
connect(federation, &Federation::modelTransformationChanged,
|
||||
this, [this](const QString& fed_id) {
|
||||
const uint32_t mid = session_state_->modelIdForFedId(fed_id);
|
||||
if (mid != 0) applyModelTransformation(mid);
|
||||
});
|
||||
connect(federation, &Federation::modelVisibilityChanged,
|
||||
this, [this](const QString& fed_id, bool /*visible*/) {
|
||||
const uint32_t mid = session_state_->modelIdForFedId(fed_id);
|
||||
if (mid != 0) applyModelVisibility(mid);
|
||||
});
|
||||
connect(federation, &Federation::modelGroupChanged,
|
||||
this, [this](const QString& fed_id, const QString& /*group_id*/) {
|
||||
const uint32_t mid = session_state_->modelIdForFedId(fed_id);
|
||||
if (mid != 0) applyModelVisibility(mid);
|
||||
});
|
||||
connect(federation, &Federation::groupVisibilityChanged,
|
||||
this, [this](const QString&, bool /*visible*/) {
|
||||
for (uint32_t mid : session_state_->modelIds()) {
|
||||
applyModelVisibility(mid);
|
||||
}
|
||||
});
|
||||
connect(loader, &SceneLoader::loadedFromSidecar, this,
|
||||
[this](uint32_t mid, qint64 /*elapsed_ms*/) {
|
||||
applyCoordinateOperation(mid);
|
||||
applyModelVisibility(mid);
|
||||
maybeGuessFederatedFalseOrigin(mid);
|
||||
});
|
||||
connect(loader, &SceneLoader::dataSourceReady, this,
|
||||
[this](uint32_t mid) {
|
||||
applyCoordinateOperation(mid);
|
||||
});
|
||||
connect(loader, &SceneLoader::loadedFromStream, this,
|
||||
[this](uint32_t mid, qint64 /*elapsed_ms*/) {
|
||||
applyCoordinateOperation(mid);
|
||||
applyModelVisibility(mid);
|
||||
maybeGuessFederatedFalseOrigin(mid);
|
||||
});
|
||||
connect(viewport_, &ViewportWindow::surfacePickedInTool, this,
|
||||
[this](int x, int y, int modifiers) {
|
||||
const bool alt = (modifiers & Qt::AltModifier) != 0;
|
||||
switch (viewport_->toolMode()) {
|
||||
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 ViewportWindow::ToolMode::Length:
|
||||
length_measurement_->onPick(*viewport_, x, y, alt);
|
||||
break;
|
||||
case ViewportWindow::ToolMode::None:
|
||||
case ViewportWindow::ToolMode::Volume:
|
||||
break;
|
||||
}
|
||||
});
|
||||
connect(viewport_, &ViewportWindow::toolModeChanged, this,
|
||||
[this](ViewportWindow::ToolMode mode) {
|
||||
area_measurement_->clear(*viewport_);
|
||||
length_measurement_->clear(*viewport_);
|
||||
switch (mode) {
|
||||
case ViewportWindow::ToolMode::None:
|
||||
viewport_->setHudText(QString());
|
||||
viewport_->setOverlayLabels({});
|
||||
session_state_->setStatusMessage("Measure", "Measurement tool off");
|
||||
break;
|
||||
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 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 ViewportWindow::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) {
|
||||
length_measurement_->removeLastPoint(*viewport_);
|
||||
}
|
||||
});
|
||||
connect(viewport_, &ViewportWindow::objectPicked, this, [this](uint32_t) {
|
||||
updateVolumeReadout();
|
||||
});
|
||||
}
|
||||
|
||||
ViewportController::~ViewportController() = default;
|
||||
|
||||
void ViewportController::applyCoordinateOperation(uint32_t mid) {
|
||||
SceneLoader* loader = session_state_->loader();
|
||||
Eigen::Matrix4d matrix = Eigen::Matrix4d::Identity();
|
||||
if (const ModelGeoref* georef = loader->modelGeoref(mid)) {
|
||||
if (georef->has_coordinate_operation) {
|
||||
matrix = georef->coordinate_operation_meters;
|
||||
}
|
||||
}
|
||||
viewport_->setModelCoordinateOperation(mid, matrix);
|
||||
applyModelTransformation(mid);
|
||||
}
|
||||
|
||||
void ViewportController::applyModelTransformation(uint32_t mid) {
|
||||
Federation* federation = session_state_->federation();
|
||||
SceneLoader* loader = session_state_->loader();
|
||||
Eigen::Matrix4d matrix = Eigen::Matrix4d::Identity();
|
||||
const QString fed_id = session_state_->fedIdForModelId(mid);
|
||||
if (!fed_id.isEmpty()) {
|
||||
if (const Federation::Model* model = federation->findById(fed_id)) {
|
||||
ModelUnits units;
|
||||
Eigen::Matrix4d coordinate_operation = Eigen::Matrix4d::Identity();
|
||||
if (const ModelGeoref* georef = loader->modelGeoref(mid)) {
|
||||
units = georef->units;
|
||||
if (georef->has_coordinate_operation) {
|
||||
coordinate_operation = georef->coordinate_operation_meters;
|
||||
}
|
||||
}
|
||||
matrix = composeModelTransformation(
|
||||
model->model_transformation, federation->config(), units, coordinate_operation);
|
||||
}
|
||||
}
|
||||
viewport_->setModelTransformation(mid, matrix);
|
||||
}
|
||||
|
||||
void ViewportController::applyModelVisibility(uint32_t mid) {
|
||||
Federation* federation = session_state_->federation();
|
||||
const QString fed_id = session_state_->fedIdForModelId(mid);
|
||||
if (fed_id.isEmpty()) return;
|
||||
|
||||
if (federation->isModelEffectivelyVisible(fed_id)) {
|
||||
viewport_->showModel(mid);
|
||||
} else {
|
||||
viewport_->hideModel(mid);
|
||||
}
|
||||
}
|
||||
|
||||
void ViewportController::applyFederatedFalseOrigin() {
|
||||
Federation* federation = session_state_->federation();
|
||||
viewport_->setFederatedFalseOrigin(
|
||||
composeFederatedFalseOrigin(federation->federatedFalseOrigin(), federation->config()));
|
||||
}
|
||||
|
||||
void ViewportController::setHomeView() {
|
||||
auto camera = viewport_->cameraState();
|
||||
Federation::HomeView home_view;
|
||||
home_view.target = camera.target;
|
||||
home_view.distance = camera.distance;
|
||||
home_view.yaw = camera.yaw;
|
||||
home_view.pitch = camera.pitch;
|
||||
session_state_->federation()->setHomeView(home_view);
|
||||
session_state_->setStatusMessage("Camera", "Home view updated");
|
||||
}
|
||||
|
||||
void ViewportController::goHomeView() {
|
||||
Federation* federation = session_state_->federation();
|
||||
if (!federation->hasHomeView()) {
|
||||
session_state_->setStatusMessage("Camera", "No home view set for this project");
|
||||
return;
|
||||
}
|
||||
|
||||
const auto& home_view = federation->homeView();
|
||||
viewport_->setCamera(
|
||||
home_view.target.x(), home_view.target.y(), home_view.target.z(),
|
||||
home_view.distance, home_view.yaw, home_view.pitch);
|
||||
session_state_->setStatusMessage("Camera", "Home view restored");
|
||||
}
|
||||
|
||||
void ViewportController::setFlyMode() {
|
||||
viewport_->requestActivate();
|
||||
viewport_->enterFpsMode();
|
||||
session_state_->setStatusMessage("Mode", "Fly mode active");
|
||||
}
|
||||
|
||||
void ViewportController::toggleSectionMode() {
|
||||
viewport_->toggleSectionTool();
|
||||
if (viewport_->sectionToolActive()) {
|
||||
session_state_->setStatusMessage("Section", "Section tool active");
|
||||
} else {
|
||||
session_state_->setStatusMessage("Section", "Section tool off");
|
||||
}
|
||||
}
|
||||
|
||||
void ViewportController::clearSectionPlanes() {
|
||||
viewport_->clearSectionPlanes();
|
||||
session_state_->setStatusMessage("Section", "Section planes cleared");
|
||||
}
|
||||
|
||||
void ViewportController::toggleDistanceMode() {
|
||||
viewport_->toggleLengthTool();
|
||||
}
|
||||
|
||||
void ViewportController::toggleAreaMode() {
|
||||
viewport_->toggleAreaTool();
|
||||
}
|
||||
|
||||
void ViewportController::toggleVolumeMode() {
|
||||
viewport_->toggleVolumeTool();
|
||||
}
|
||||
|
||||
void ViewportController::focusSelectedObject() {
|
||||
viewport_->focusOnSelectedObject();
|
||||
}
|
||||
|
||||
void ViewportController::hideSelectedElements() {
|
||||
viewport_->hideSelectedElements();
|
||||
}
|
||||
|
||||
void ViewportController::isolateSelectedElements() {
|
||||
viewport_->isolateSelectedElements();
|
||||
}
|
||||
|
||||
void ViewportController::showAllElements() {
|
||||
viewport_->showAllElements();
|
||||
}
|
||||
|
||||
void ViewportController::invertSelection() {
|
||||
viewport_->invertElementVisibility();
|
||||
}
|
||||
|
||||
void ViewportController::updateVolumeReadout() {
|
||||
if (viewport_->toolMode() != ViewportWindow::ToolMode::Volume) return;
|
||||
|
||||
const auto& sel = viewport_->selection().selectionIds();
|
||||
if (sel.empty()) {
|
||||
viewport_->setHudText(QString());
|
||||
viewport_->setOverlayLabels({});
|
||||
return;
|
||||
}
|
||||
|
||||
std::vector<uint32_t> ids(sel.begin(), sel.end());
|
||||
const auto per_obj = volumesPerObject(*viewport_, ids);
|
||||
|
||||
double total = 0.0;
|
||||
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;
|
||||
OverlayRenderer::Label lbl;
|
||||
const QVector3D c = (mn + mx) * 0.5f;
|
||||
lbl.world_pos[0] = c.x();
|
||||
lbl.world_pos[1] = c.y();
|
||||
lbl.world_pos[2] = c.z();
|
||||
lbl.text = QString::number(v, 'f', 4) + " m³";
|
||||
labels.push_back(std::move(lbl));
|
||||
}
|
||||
|
||||
viewport_->setHudText(QString("Volume: %1 m³ (%2 object%3)")
|
||||
.arg(total, 0, 'f', 4)
|
||||
.arg(per_obj.size())
|
||||
.arg(per_obj.size() == 1 ? "" : "s"));
|
||||
viewport_->setOverlayLabels(labels);
|
||||
}
|
||||
|
||||
void ViewportController::maybeGuessFederatedFalseOrigin(uint32_t mid) {
|
||||
Federation* federation = session_state_->federation();
|
||||
SceneLoader* loader = session_state_->loader();
|
||||
if (!federation->filePath().isEmpty()) return;
|
||||
|
||||
const FederatedFalseOrigin& current = federation->federatedFalseOrigin();
|
||||
const FederatedFalseOrigin defaults;
|
||||
if (current.xyz != defaults.xyz || current.rz_deg != defaults.rz_deg) return;
|
||||
|
||||
const Eigen::Matrix4d* placement = loader->firstPlacement(mid);
|
||||
const ModelGeoref* georef = loader->modelGeoref(mid);
|
||||
if (placement == nullptr || georef == nullptr) return;
|
||||
|
||||
federation->setFederatedFalseOrigin(guessFederatedFalseOrigin(
|
||||
*placement, *georef, federation->config()));
|
||||
}
|
||||
|
||||
} // namespace ifcinterface::modules::viewport
|
||||
+22
-2
@@ -22,11 +22,14 @@
|
||||
#define IFCINTERFACE_PANELS_VIEWPORT_CONTROLLER_H
|
||||
|
||||
#include <QObject>
|
||||
#include <memory>
|
||||
|
||||
namespace ifcinterface { class SessionState; }
|
||||
class ViewportWindow;
|
||||
class AreaMeasurement;
|
||||
class LengthMeasurement;
|
||||
|
||||
namespace ifcinterface::panels::viewport {
|
||||
namespace ifcinterface::modules::viewport {
|
||||
|
||||
class ViewportController : public QObject {
|
||||
Q_OBJECT
|
||||
@@ -35,19 +38,36 @@ public:
|
||||
explicit ViewportController(ifcinterface::SessionState* session_state,
|
||||
ViewportWindow* viewport,
|
||||
QObject* parent = nullptr);
|
||||
~ViewportController() override;
|
||||
|
||||
void applyFederatedFalseOrigin();
|
||||
void setHomeView();
|
||||
void goHomeView();
|
||||
void setFlyMode();
|
||||
void toggleSectionMode();
|
||||
void clearSectionPlanes();
|
||||
void toggleDistanceMode();
|
||||
void toggleAreaMode();
|
||||
void toggleVolumeMode();
|
||||
void focusSelectedObject();
|
||||
void hideSelectedElements();
|
||||
void isolateSelectedElements();
|
||||
void showAllElements();
|
||||
void invertSelection();
|
||||
|
||||
private:
|
||||
void applyCoordinateOperation(uint32_t mid);
|
||||
void applyModelTransformation(uint32_t mid);
|
||||
void applyModelVisibility(uint32_t mid);
|
||||
void maybeGuessFederatedFalseOrigin(uint32_t mid);
|
||||
void updateVolumeReadout();
|
||||
|
||||
ifcinterface::SessionState* session_state_ = nullptr;
|
||||
ViewportWindow* viewport_ = nullptr;
|
||||
std::unique_ptr<AreaMeasurement> area_measurement_;
|
||||
std::unique_ptr<LengthMeasurement> length_measurement_;
|
||||
};
|
||||
|
||||
} // namespace ifcinterface::panels::viewport
|
||||
} // namespace ifcinterface::modules::viewport
|
||||
|
||||
#endif
|
||||
@@ -18,7 +18,7 @@
|
||||
* *
|
||||
********************************************************************************/
|
||||
|
||||
#include "Widget.h"
|
||||
#include "Panel.h"
|
||||
|
||||
#include "../../../ifcviewer/ViewportWindow.h"
|
||||
|
||||
@@ -26,9 +26,9 @@
|
||||
#include <QVBoxLayout>
|
||||
#include <QWidget>
|
||||
|
||||
namespace ifcinterface::panels::viewport {
|
||||
namespace ifcinterface::modules::viewport {
|
||||
|
||||
ViewportWidget::ViewportWidget(QWidget* parent)
|
||||
ViewportPanel::ViewportPanel(QWidget* parent)
|
||||
: QWidget(parent)
|
||||
{
|
||||
auto* root = new QVBoxLayout(this);
|
||||
@@ -57,4 +57,4 @@ ViewportWidget::ViewportWidget(QWidget* parent)
|
||||
root->addWidget(shell);
|
||||
}
|
||||
|
||||
} // namespace ifcinterface::panels::viewport
|
||||
} // namespace ifcinterface::modules::viewport
|
||||
@@ -18,20 +18,20 @@
|
||||
* *
|
||||
********************************************************************************/
|
||||
|
||||
#ifndef IFCINTERFACE_PANELS_VIEWPORT_WIDGET_H
|
||||
#define IFCINTERFACE_PANELS_VIEWPORT_WIDGET_H
|
||||
#ifndef IFCINTERFACE_MODULES_VIEWPORT_PANEL_H
|
||||
#define IFCINTERFACE_MODULES_VIEWPORT_PANEL_H
|
||||
|
||||
#include <QWidget>
|
||||
|
||||
class ViewportWindow;
|
||||
|
||||
namespace ifcinterface::panels::viewport {
|
||||
namespace ifcinterface::modules::viewport {
|
||||
|
||||
class ViewportWidget : public QWidget {
|
||||
class ViewportPanel : public QWidget {
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
explicit ViewportWidget(QWidget* parent = nullptr);
|
||||
explicit ViewportPanel(QWidget* parent = nullptr);
|
||||
|
||||
ViewportWindow* viewport() const { return viewport_; }
|
||||
|
||||
@@ -40,6 +40,6 @@ private:
|
||||
QWidget* viewport_container_ = nullptr;
|
||||
};
|
||||
|
||||
} // namespace ifcinterface::panels::viewport
|
||||
} // namespace ifcinterface::modules::viewport
|
||||
|
||||
#endif
|
||||
@@ -1,214 +0,0 @@
|
||||
// This file was generated with the assistance of an AI coding tool.
|
||||
/********************************************************************************
|
||||
* *
|
||||
* This file is part of IfcOpenShell. *
|
||||
* *
|
||||
* IfcOpenShell is free software: you can redistribute it and/or modify *
|
||||
* it under the terms of the Lesser GNU General Public License as published by *
|
||||
* the Free Software Foundation, either version 3.0 of the License, or *
|
||||
* (at your option) any later version. *
|
||||
* *
|
||||
* IfcOpenShell is distributed in the hope that it will be useful, *
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of *
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the *
|
||||
* Lesser GNU General Public License for more details. *
|
||||
* *
|
||||
* You should have received a copy of the Lesser GNU General Public License *
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>. *
|
||||
* *
|
||||
********************************************************************************/
|
||||
|
||||
#include "Dialog.h"
|
||||
|
||||
#include "../../../ifcviewer/AppSettings.h"
|
||||
#include "../../components/Dialog.h"
|
||||
#include "../../components/Section.h"
|
||||
#include "../../components/Style.h"
|
||||
#include "../../components/SvgIcon.h"
|
||||
#include "../../components/Tabs.h"
|
||||
|
||||
#include <QCheckBox>
|
||||
#include <QDialogButtonBox>
|
||||
#include <QDoubleSpinBox>
|
||||
#include <QFrame>
|
||||
#include <QFormLayout>
|
||||
#include <QLabel>
|
||||
#include <QLineEdit>
|
||||
#include <QPushButton>
|
||||
#include <QShowEvent>
|
||||
#include <QSpinBox>
|
||||
#include <QVBoxLayout>
|
||||
|
||||
namespace ifcinterface::panels::settings {
|
||||
|
||||
SettingsDialog::SettingsDialog(QWidget* parent)
|
||||
: components::Dialog(parent)
|
||||
{
|
||||
setObjectName("appDialog");
|
||||
setWindowTitle("Settings");
|
||||
setModal(true);
|
||||
resize(520, 420);
|
||||
setStyleSheet(components::style::buildAppStyleSheet());
|
||||
setupUi();
|
||||
}
|
||||
|
||||
void SettingsDialog::showEvent(QShowEvent* event) {
|
||||
syncFromSettings();
|
||||
QDialog::showEvent(event);
|
||||
}
|
||||
|
||||
void SettingsDialog::setupUi() {
|
||||
auto* tabs = new components::TabWidget(this);
|
||||
|
||||
auto* graphics_tab = new QWidget(tabs);
|
||||
auto* graphics_layout = new QVBoxLayout(graphics_tab);
|
||||
graphics_layout->setContentsMargins(0, 0, 0, 0);
|
||||
graphics_layout->setSpacing(components::style::metrics::padding);
|
||||
|
||||
auto* general_section = new components::Section("General", components::SectionHeaderMode::Visible, graphics_tab);
|
||||
auto* general_body = new QWidget(general_section);
|
||||
auto* general_form = new QFormLayout(general_body);
|
||||
general_form->setContentsMargins(0, 0, 0, 0);
|
||||
general_form->setHorizontalSpacing(16);
|
||||
general_form->setVerticalSpacing(10);
|
||||
|
||||
geometry_library_edit_ = new QLineEdit(general_body);
|
||||
geometry_library_edit_->setMinimumWidth(300);
|
||||
general_form->addRow("Geometry Library", geometry_library_edit_);
|
||||
|
||||
show_stats_check_ = new QCheckBox(general_body);
|
||||
general_form->addRow("Show Performance Stats", show_stats_check_);
|
||||
|
||||
backface_culling_check_ = new QCheckBox(general_body);
|
||||
backface_culling_check_->setToolTip(
|
||||
"Skip triangles facing away from the camera. Big FPS win on closed solids; "
|
||||
"disable if you see holes in open geometry.");
|
||||
general_form->addRow("Backface Culling", backface_culling_check_);
|
||||
|
||||
general_section->addBodyWidget(general_body);
|
||||
|
||||
auto* loading_section = new components::Section("Loading", components::SectionHeaderMode::Visible, graphics_tab);
|
||||
auto* loading_body = new QWidget(loading_section);
|
||||
auto* loading_form = new QFormLayout(loading_body);
|
||||
loading_form->setContentsMargins(0, 0, 0, 0);
|
||||
loading_form->setHorizontalSpacing(16);
|
||||
loading_form->setVerticalSpacing(10);
|
||||
|
||||
load_data_source_checkbox_ = new QCheckBox(loading_body);
|
||||
load_data_source_checkbox_->setToolTip(
|
||||
"Keep the .ifc/.rdb open after loading so element properties can be queried. "
|
||||
"Disable for geometry-only viewing.");
|
||||
loading_form->addRow("Load Property Data Source", load_data_source_checkbox_);
|
||||
|
||||
apply_coordinate_operation_check_ = new QCheckBox(loading_body);
|
||||
apply_coordinate_operation_check_->setToolTip(
|
||||
"Apply each model's IfcCoordinateOperation after load so it lands in "
|
||||
"georeferenced map coordinates.");
|
||||
loading_form->addRow("Apply Coordinate Operation", apply_coordinate_operation_check_);
|
||||
|
||||
void_limit_spin_ = new QSpinBox(loading_body);
|
||||
void_limit_spin_->setRange(0, 100000);
|
||||
loading_form->addRow("Void Limit", void_limit_spin_);
|
||||
|
||||
deflection_tolerance_spin_ = new QDoubleSpinBox(loading_body);
|
||||
deflection_tolerance_spin_->setRange(0.000001, 1000.0);
|
||||
deflection_tolerance_spin_->setDecimals(6);
|
||||
deflection_tolerance_spin_->setSingleStep(0.001);
|
||||
loading_form->addRow("Deflection Tolerance", deflection_tolerance_spin_);
|
||||
|
||||
angular_tolerance_spin_ = new QDoubleSpinBox(loading_body);
|
||||
angular_tolerance_spin_->setRange(0.000001, 3.141592);
|
||||
angular_tolerance_spin_->setDecimals(6);
|
||||
angular_tolerance_spin_->setSingleStep(0.05);
|
||||
loading_form->addRow("Angular Tolerance", angular_tolerance_spin_);
|
||||
|
||||
auto* description = new QLabel(
|
||||
"These settings are shared with the viewer backend and persist via QSettings.",
|
||||
loading_body);
|
||||
description->setProperty("textRole", "secondary");
|
||||
description->setWordWrap(true);
|
||||
loading_form->addRow(QString(), description);
|
||||
|
||||
loading_section->addBodyWidget(loading_body);
|
||||
graphics_layout->addWidget(general_section);
|
||||
graphics_layout->addWidget(loading_section);
|
||||
graphics_layout->addStretch(1);
|
||||
|
||||
auto make_placeholder_tab = [tabs](const QString& title, const QString& detail) {
|
||||
auto* tab = new QWidget(tabs);
|
||||
auto* layout = new QVBoxLayout(tab);
|
||||
layout->setContentsMargins(0, 0, 0, 0);
|
||||
layout->setSpacing(components::style::metrics::padding);
|
||||
|
||||
auto* section = new components::Section(title, components::SectionHeaderMode::Visible, tab);
|
||||
auto* body = new QWidget(section);
|
||||
auto* body_layout = new QVBoxLayout(body);
|
||||
body_layout->setContentsMargins(0, 0, 0, 0);
|
||||
body_layout->setSpacing(8);
|
||||
|
||||
auto* heading = new QLabel(title, body);
|
||||
auto* content = new QLabel(detail, body);
|
||||
content->setProperty("textRole", "secondary");
|
||||
content->setWordWrap(true);
|
||||
|
||||
body_layout->addWidget(heading);
|
||||
body_layout->addWidget(content);
|
||||
section->addBodyWidget(body);
|
||||
layout->addWidget(section);
|
||||
layout->addStretch(1);
|
||||
return tab;
|
||||
};
|
||||
|
||||
tabs->addTab(make_placeholder_tab("Navigation", "Navigation preferences and interaction modes will live here."),
|
||||
"Navigation");
|
||||
tabs->addTab(make_placeholder_tab("Keybindings", "Shortcut presets and command bindings will live here."),
|
||||
"Keybindings");
|
||||
tabs->addTab(graphics_tab, "Graphics");
|
||||
tabs->addTab(make_placeholder_tab("Theme", "Theme, density, and UI appearance settings will live here."),
|
||||
"Theme");
|
||||
tabs->addTab(make_placeholder_tab("About", "Version, credits, and environment information will live here."),
|
||||
"About");
|
||||
|
||||
auto* buttons = new QDialogButtonBox(QDialogButtonBox::Ok | QDialogButtonBox::Cancel, this);
|
||||
if (auto* ok = buttons->button(QDialogButtonBox::Ok)) {
|
||||
ok->setText("OK");
|
||||
ok->setIcon(components::icons::makeSvgIcon(":/icons/check.svg"));
|
||||
}
|
||||
if (auto* cancel = buttons->button(QDialogButtonBox::Cancel)) {
|
||||
cancel->setText("Cancel");
|
||||
cancel->setIcon(components::icons::makeSvgIcon(":/icons/xmark-circle.svg"));
|
||||
}
|
||||
connect(buttons, &QDialogButtonBox::accepted, this, &SettingsDialog::onAccepted);
|
||||
connect(buttons, &QDialogButtonBox::rejected, this, &QDialog::reject);
|
||||
|
||||
auto* actions_section = new components::Section("", components::SectionHeaderMode::Hidden, this);
|
||||
actions_section->addBodyWidget(buttons);
|
||||
|
||||
addBodyWidget(tabs);
|
||||
addBodyWidget(actions_section);
|
||||
}
|
||||
|
||||
void SettingsDialog::syncFromSettings() {
|
||||
geometry_library_edit_->setText(AppSettings::instance().geometryLibrary());
|
||||
show_stats_check_->setChecked(AppSettings::instance().showStats());
|
||||
backface_culling_check_->setChecked(AppSettings::instance().backfaceCulling());
|
||||
load_data_source_checkbox_->setChecked(AppSettings::instance().loadDataSource());
|
||||
apply_coordinate_operation_check_->setChecked(AppSettings::instance().applyCoordinateOperation());
|
||||
void_limit_spin_->setValue(AppSettings::instance().voidLimit());
|
||||
deflection_tolerance_spin_->setValue(AppSettings::instance().deflectionTolerance());
|
||||
angular_tolerance_spin_->setValue(AppSettings::instance().angularTolerance());
|
||||
}
|
||||
|
||||
void SettingsDialog::onAccepted() {
|
||||
AppSettings::instance().setGeometryLibrary(geometry_library_edit_->text());
|
||||
AppSettings::instance().setShowStats(show_stats_check_->isChecked());
|
||||
AppSettings::instance().setBackfaceCulling(backface_culling_check_->isChecked());
|
||||
AppSettings::instance().setLoadDataSource(load_data_source_checkbox_->isChecked());
|
||||
AppSettings::instance().setApplyCoordinateOperation(apply_coordinate_operation_check_->isChecked());
|
||||
AppSettings::instance().setVoidLimit(void_limit_spin_->value());
|
||||
AppSettings::instance().setDeflectionTolerance(deflection_tolerance_spin_->value());
|
||||
AppSettings::instance().setAngularTolerance(angular_tolerance_spin_->value());
|
||||
accept();
|
||||
}
|
||||
|
||||
} // namespace ifcinterface::panels::settings
|
||||
@@ -1,169 +0,0 @@
|
||||
// This file was generated with the assistance of an AI coding tool.
|
||||
/********************************************************************************
|
||||
* *
|
||||
* This file is part of IfcOpenShell. *
|
||||
* *
|
||||
* IfcOpenShell is free software: you can redistribute it and/or modify *
|
||||
* it under the terms of the Lesser GNU General Public License as published by *
|
||||
* the Free Software Foundation, either version 3.0 of the License, or *
|
||||
* (at your option) any later version. *
|
||||
* *
|
||||
* IfcOpenShell is distributed in the hope that it will be useful, *
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of *
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the *
|
||||
* Lesser GNU General Public License for more details. *
|
||||
* *
|
||||
* You should have received a copy of the Lesser GNU General Public License *
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>. *
|
||||
* *
|
||||
********************************************************************************/
|
||||
|
||||
#include "Controller.h"
|
||||
|
||||
#include "../../SessionState.h"
|
||||
#include "../../../ifcviewer/AppSettings.h"
|
||||
#include "../../../ifcviewer/Federation.h"
|
||||
#include "../../../ifcviewer/SceneLoader.h"
|
||||
#include "../../../ifcviewer/ViewportWindow.h"
|
||||
|
||||
#include <Eigen/Dense>
|
||||
|
||||
namespace ifcinterface::panels::viewport {
|
||||
|
||||
ViewportController::ViewportController(ifcinterface::SessionState* session_state,
|
||||
ViewportWindow* viewport,
|
||||
QObject* parent)
|
||||
: QObject(parent)
|
||||
, session_state_(session_state)
|
||||
, viewport_(viewport)
|
||||
{
|
||||
Federation* federation = session_state_->federation();
|
||||
SceneLoader* loader = session_state_->loader();
|
||||
connect(federation, &Federation::federatedFalseOriginChanged,
|
||||
this, &ViewportController::applyFederatedFalseOrigin);
|
||||
connect(federation, &Federation::configChanged, this, [this]() {
|
||||
applyFederatedFalseOrigin();
|
||||
for (uint32_t mid : session_state_->modelIds()) {
|
||||
applyModelTransformation(mid);
|
||||
}
|
||||
});
|
||||
connect(federation, &Federation::modelTransformationChanged,
|
||||
this, [this](const QString& fed_id) {
|
||||
const uint32_t mid = session_state_->modelIdForFedId(fed_id);
|
||||
if (mid != 0) applyModelTransformation(mid);
|
||||
});
|
||||
connect(federation, &Federation::modelVisibilityChanged,
|
||||
this, [this](const QString& fed_id, bool /*visible*/) {
|
||||
const uint32_t mid = session_state_->modelIdForFedId(fed_id);
|
||||
if (mid != 0) applyModelVisibility(mid);
|
||||
});
|
||||
connect(federation, &Federation::modelGroupChanged,
|
||||
this, [this](const QString& fed_id, const QString& /*group_id*/) {
|
||||
const uint32_t mid = session_state_->modelIdForFedId(fed_id);
|
||||
if (mid != 0) applyModelVisibility(mid);
|
||||
});
|
||||
connect(federation, &Federation::groupVisibilityChanged,
|
||||
this, [this](const QString&, bool /*visible*/) {
|
||||
for (uint32_t mid : session_state_->modelIds()) {
|
||||
applyModelVisibility(mid);
|
||||
}
|
||||
});
|
||||
connect(loader, &SceneLoader::loadedFromSidecar, this,
|
||||
[this](uint32_t mid, qint64 /*elapsed_ms*/) {
|
||||
applyCoordinateOperation(mid);
|
||||
applyModelVisibility(mid);
|
||||
maybeGuessFederatedFalseOrigin(mid);
|
||||
});
|
||||
connect(loader, &SceneLoader::dataSourceReady, this,
|
||||
[this](uint32_t mid) {
|
||||
applyCoordinateOperation(mid);
|
||||
});
|
||||
connect(loader, &SceneLoader::loadedFromStream, this,
|
||||
[this](uint32_t mid, qint64 /*elapsed_ms*/) {
|
||||
applyCoordinateOperation(mid);
|
||||
applyModelVisibility(mid);
|
||||
maybeGuessFederatedFalseOrigin(mid);
|
||||
});
|
||||
connect(&AppSettings::instance(),
|
||||
&AppSettings::applyCoordinateOperationChanged,
|
||||
this, [this](bool /*enabled*/) {
|
||||
for (uint32_t mid : session_state_->modelIds()) {
|
||||
applyCoordinateOperation(mid);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
void ViewportController::applyCoordinateOperation(uint32_t mid) {
|
||||
SceneLoader* loader = session_state_->loader();
|
||||
Eigen::Matrix4d matrix = Eigen::Matrix4d::Identity();
|
||||
if (AppSettings::instance().applyCoordinateOperation()) {
|
||||
if (const ModelGeoref* georef = loader->modelGeoref(mid)) {
|
||||
if (georef->has_coordinate_operation) {
|
||||
matrix = georef->coordinate_operation_meters;
|
||||
}
|
||||
}
|
||||
}
|
||||
viewport_->setModelCoordinateOperation(mid, matrix);
|
||||
applyModelTransformation(mid);
|
||||
}
|
||||
|
||||
void ViewportController::applyModelTransformation(uint32_t mid) {
|
||||
Federation* federation = session_state_->federation();
|
||||
SceneLoader* loader = session_state_->loader();
|
||||
Eigen::Matrix4d matrix = Eigen::Matrix4d::Identity();
|
||||
const QString fed_id = session_state_->fedIdForModelId(mid);
|
||||
if (!fed_id.isEmpty()) {
|
||||
if (const Federation::Model* model = federation->findById(fed_id)) {
|
||||
ModelUnits units;
|
||||
Eigen::Matrix4d coordinate_operation = Eigen::Matrix4d::Identity();
|
||||
if (const ModelGeoref* georef = loader->modelGeoref(mid)) {
|
||||
units = georef->units;
|
||||
if (AppSettings::instance().applyCoordinateOperation() &&
|
||||
georef->has_coordinate_operation) {
|
||||
coordinate_operation = georef->coordinate_operation_meters;
|
||||
}
|
||||
}
|
||||
matrix = composeModelTransformation(
|
||||
model->model_transformation, federation->config(), units, coordinate_operation);
|
||||
}
|
||||
}
|
||||
viewport_->setModelTransformation(mid, matrix);
|
||||
}
|
||||
|
||||
void ViewportController::applyModelVisibility(uint32_t mid) {
|
||||
Federation* federation = session_state_->federation();
|
||||
const QString fed_id = session_state_->fedIdForModelId(mid);
|
||||
if (fed_id.isEmpty()) return;
|
||||
|
||||
if (federation->isModelEffectivelyVisible(fed_id)) {
|
||||
viewport_->showModel(mid);
|
||||
} else {
|
||||
viewport_->hideModel(mid);
|
||||
}
|
||||
}
|
||||
|
||||
void ViewportController::applyFederatedFalseOrigin() {
|
||||
Federation* federation = session_state_->federation();
|
||||
viewport_->setFederatedFalseOrigin(
|
||||
composeFederatedFalseOrigin(federation->federatedFalseOrigin(), federation->config()));
|
||||
}
|
||||
|
||||
void ViewportController::maybeGuessFederatedFalseOrigin(uint32_t mid) {
|
||||
Federation* federation = session_state_->federation();
|
||||
SceneLoader* loader = session_state_->loader();
|
||||
if (!federation->filePath().isEmpty()) return;
|
||||
|
||||
const FederatedFalseOrigin& current = federation->federatedFalseOrigin();
|
||||
const FederatedFalseOrigin defaults;
|
||||
if (current.xyz != defaults.xyz || current.rz_deg != defaults.rz_deg) return;
|
||||
|
||||
const Eigen::Matrix4d* placement = loader->firstPlacement(mid);
|
||||
const ModelGeoref* georef = loader->modelGeoref(mid);
|
||||
if (placement == nullptr || georef == nullptr) return;
|
||||
|
||||
federation->setFederatedFalseOrigin(guessFederatedFalseOrigin(
|
||||
*placement, *georef, federation->config(),
|
||||
AppSettings::instance().applyCoordinateOperation()));
|
||||
}
|
||||
|
||||
} // namespace ifcinterface::panels::viewport
|
||||
Reference in New Issue
Block a user