ifcviewer: gate CoordinateOperation on a settings toggle

AppSettings.applyCoordinateOperation (default false, persisted via
QSettings) controls whether each loaded model's IfcCoordinateOperation
is applied at upload time.  Off keeps models in their local engineering
frame (current behaviour).  On lifts each model into map coordinates
via the stage-2 georef matrix cached on SceneLoader.

MainWindow:
  - applyCoordinateOperationToViewport(mid) reads the toggle, fetches
    the model's ModelGeoref, and pushes either the
    coordinate_operation_meters matrix or identity to the viewport.
  - Called from onLoadedFromStream (streamer path) and onDataSourceReady
    (sidecar-hit path, where the IFC arrives asynchronously).
  - Subscribed to AppSettings::applyCoordinateOperationChanged: a
    runtime toggle walks every loaded model and re-applies, so users
    can flip georef on/off without reloading.

SettingsWindow gains a "Apply Coordinate Operation" checkbox alongside
the existing per-load toggles.

Default-off so the change is opt-in — users with georeferenced models
(UTM coords etc.) can flip the toggle to see them in their map frame
once they're ready.  Visual verification on a real georeferenced
model still pending.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
Dion Moult
2026-05-02 07:45:40 +10:00
parent 7f29850022
commit e9d577890e
6 changed files with 78 additions and 0 deletions
+33
View File
@@ -89,6 +89,16 @@ 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);
}
@@ -457,6 +467,13 @@ void MainWindow::onSidecarElementsReady(uint32_t mid,
}
void MainWindow::onDataSourceReady(uint32_t mid) {
// The IFC file is now available — push the model's CoordinateOperation
// (or identity) to the viewport. Sidecar-hit models get here for the
// first time; stream-loaded models also pass through here when a
// separate data source opens, but applyCoordinateOperationToViewport
// is idempotent so double-applying is harmless.
applyCoordinateOperationToViewport(mid);
// Re-populate if the current selection belongs to this model, since
// populateProperties() now has an ifcFile() to query.
auto items = element_tree_->selectedItems();
@@ -468,6 +485,18 @@ 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;
}
}
}
viewport_->setModelCoordinateOperation(mid, M);
}
void MainWindow::onLoadedFromSidecar(uint32_t /*mid*/, qint64 elapsed_ms) {
progress_bar_->setVisible(false);
status_label_->setText(QString("%1 elements across %2 model(s) — loaded from cache in %3")
@@ -572,6 +601,10 @@ void MainWindow::onLoadedFromStream(uint32_t mid, qint64 elapsed_ms) {
.arg(loader_->modelCount())
.arg(formatElapsed(elapsed_ms)));
// Stream path: the IFC is owned by the streamer, so georef is
// computable now. (Sidecar-hit models defer to onDataSourceReady.)
applyCoordinateOperationToViewport(mid);
writeSidecarForModel(mid);
}
+7
View File
@@ -96,6 +96,13 @@ private:
void writeSidecarForModel(uint32_t mid);
void removeModelUi(uint32_t mid);
void applyPendingBenchmark();
// Push a model's CoordinateOperation matrix to the viewport (or
// identity, when the AppSettings toggle is off or the model has no
// map conversion). No-op if the model isn't yet known to the loader
// or its IFC file isn't available (sidecar-hit before data-source
// load); the call retries on onDataSourceReady.
void applyCoordinateOperationToViewport(uint32_t mid);
QString formatElapsed(qint64 ms) const;
ViewportWindow* viewport_ = nullptr;
+12
View File
@@ -59,6 +59,14 @@ void SettingsWindow::setupUi() {
"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(
@@ -109,6 +117,8 @@ void SettingsWindow::syncFromSettings() {
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());
@@ -119,6 +129,8 @@ void SettingsWindow::onAccepted() {
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());
+1
View File
@@ -47,6 +47,7 @@ private:
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;
+15
View File
@@ -27,6 +27,7 @@ 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";
@@ -88,6 +89,17 @@ void AppSettings::setLoadDataSource(bool value) {
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_;
}
@@ -130,6 +142,8 @@ void AppSettings::load() {
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();
@@ -144,6 +158,7 @@ void AppSettings::persist() {
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_);
+10
View File
@@ -47,6 +47,14 @@ public:
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.
@@ -69,6 +77,7 @@ signals:
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);
@@ -82,6 +91,7 @@ private:
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;