From e9d577890e2a5cc8a1d8b8024b306281c60936f7 Mon Sep 17 00:00:00 2001 From: Dion Moult Date: Sat, 2 May 2026 07:45:40 +1000 Subject: [PATCH] ifcviewer: gate CoordinateOperation on a settings toggle MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- src/ifcviewer-full/MainWindow.cpp | 33 +++++++++++++++++++++++++++ src/ifcviewer-full/MainWindow.h | 7 ++++++ src/ifcviewer-full/SettingsWindow.cpp | 12 ++++++++++ src/ifcviewer-full/SettingsWindow.h | 1 + src/ifcviewer/AppSettings.cpp | 15 ++++++++++++ src/ifcviewer/AppSettings.h | 10 ++++++++ 6 files changed, 78 insertions(+) diff --git a/src/ifcviewer-full/MainWindow.cpp b/src/ifcviewer-full/MainWindow.cpp index 3105af7cf5..98bcc9224c 100644 --- a/src/ifcviewer-full/MainWindow.cpp +++ b/src/ifcviewer-full/MainWindow.cpp @@ -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); } diff --git a/src/ifcviewer-full/MainWindow.h b/src/ifcviewer-full/MainWindow.h index 13dfecf061..03dac8f4f2 100644 --- a/src/ifcviewer-full/MainWindow.h +++ b/src/ifcviewer-full/MainWindow.h @@ -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; diff --git a/src/ifcviewer-full/SettingsWindow.cpp b/src/ifcviewer-full/SettingsWindow.cpp index 8fd867b3ae..9a7032fd86 100644 --- a/src/ifcviewer-full/SettingsWindow.cpp +++ b/src/ifcviewer-full/SettingsWindow.cpp @@ -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()); diff --git a/src/ifcviewer-full/SettingsWindow.h b/src/ifcviewer-full/SettingsWindow.h index e9a996e94d..070e90867e 100644 --- a/src/ifcviewer-full/SettingsWindow.h +++ b/src/ifcviewer-full/SettingsWindow.h @@ -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; diff --git a/src/ifcviewer/AppSettings.cpp b/src/ifcviewer/AppSettings.cpp index 3f7dfea9e0..d6383ee4c5 100644 --- a/src/ifcviewer/AppSettings.cpp +++ b/src/ifcviewer/AppSettings.cpp @@ -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_); diff --git a/src/ifcviewer/AppSettings.h b/src/ifcviewer/AppSettings.h index d435158f70..ac96ab3392 100644 --- a/src/ifcviewer/AppSettings.h +++ b/src/ifcviewer/AppSettings.h @@ -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;