From 4a9af1362676366930f411ee9b77f997e2f75564 Mon Sep 17 00:00:00 2001 From: Dion Moult Date: Sat, 2 May 2026 07:58:26 +1000 Subject: [PATCH] ifcviewer: wire FederatedFalseOrigin / ModelTransformation to viewport MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Federation grows three granular signals so consumers can recompose only what's affected: - configChanged() — federation unit changed - federatedFalseOriginChanged() — stage 3 changed - modelTransformationChanged(fed_id) — stage 4 changed for one model Emitted from setConfig / setFederatedFalseOrigin / setModelTransformation in addition to the existing dirtyChanged. MainWindow gains applyFederatedFalseOriginToViewport and applyModelTransformationToViewport helpers. Each composes the matrix from the current federation state (using composeFederatedFalseOrigin / composeModelTransformation, which already exist on Federation.h) and pushes to the viewport's setFederatedFalseOrigin / setModelTransformation. ModelTransformation reads ModelUnits and the active CoordinateOperation matrix from SceneLoader::modelGeoref so ModelLocal-frame `a` lifts correctly through stage 2 when authored. Wiring: - federation.federatedFalseOriginChanged -> applyFederatedFalseOriginToViewport - federation.configChanged -> stage 3 + walk all models for stage 4 - federation.modelTransformationChanged -> stage 4 for that one model - applyCoordinateOperationToViewport now also re-pushes stage 4 (the compose result depends on the active stage 2 when a_frame is ModelLocal) - openFederation() pushes the loaded FederatedFalseOrigin once load completes; per-model stage 4 falls out of the existing onLoadedFromStream / onDataSourceReady path. End-to-end pipeline is now active under the AppSettings toggle: edit the federation in memory and the viewport recomposes immediately. UI for editing (form-based dialog) still pending. Co-Authored-By: Claude Opus 4.7 --- src/ifcviewer-full/MainWindow.cpp | 62 +++++++++++++++++++++++++++++++ src/ifcviewer-full/MainWindow.h | 10 +++++ src/ifcviewer/Federation.cpp | 3 ++ src/ifcviewer/Federation.h | 7 ++++ 4 files changed, 82 insertions(+) diff --git a/src/ifcviewer-full/MainWindow.cpp b/src/ifcviewer-full/MainWindow.cpp index 98bcc9224c..a07ad82db8 100644 --- a/src/ifcviewer-full/MainWindow.cpp +++ b/src/ifcviewer-full/MainWindow.cpp @@ -43,6 +43,28 @@ MainWindow::MainWindow(QWidget* parent) setupMenus(); federation_ = new Federation(this); + + // Federation -> viewport: granular signals so we recompose only what's + // affected. Each handler reads the current federation state, composes + // the matrix, and pushes to the viewport. + connect(federation_, &Federation::federatedFalseOriginChanged, + this, &MainWindow::applyFederatedFalseOriginToViewport); + connect(federation_, &Federation::configChanged, this, [this]() { + // Federation unit changed — both stage 3 (uses fed unit) and every + // model's stage 4 (b/pivot are in fed units) need recomposing. + applyFederatedFalseOriginToViewport(); + for (const auto& kv : fed_id_to_model_id_) { + applyModelTransformationToViewport(kv.second); + } + }); + connect(federation_, &Federation::modelTransformationChanged, + this, [this](const QString& fed_id) { + auto it = fed_id_to_model_id_.find(fed_id); + if (it != fed_id_to_model_id_.end()) { + applyModelTransformationToViewport(it->second); + } + }); + connect(federation_, &Federation::dirtyChanged, this, [this](bool dirty) { setWindowModified(dirty); }); @@ -276,6 +298,11 @@ bool MainWindow::openFederation(const QString& path) { federation_->markClean(); updateWindowTitle(); + // Push the federation's loaded FederatedFalseOrigin to the viewport. + // Per-model ModelTransformations get pushed as each model finishes + // loading, via applyCoordinateOperationToViewport. + applyFederatedFalseOriginToViewport(); + if (federation_->hasHomeView()) { const auto& hv = federation_->homeView(); viewport_->setCamera(hv.target.x(), hv.target.y(), hv.target.z(), @@ -495,6 +522,41 @@ void MainWindow::applyCoordinateOperationToViewport(uint32_t mid) { } } viewport_->setModelCoordinateOperation(mid, M); + // ModelTransformation's compose can depend on the active + // CoordinateOperation (when ModelTransformation.a_frame == ModelLocal, + // a is lifted through stage 2), so re-push it whenever stage 2 changes. + applyModelTransformationToViewport(mid); +} + +void MainWindow::applyModelTransformationToViewport(uint32_t mid) { + Eigen::Matrix4d M = Eigen::Matrix4d::Identity(); + auto fed_it = model_id_to_fed_id_.find(mid); + if (fed_it != model_id_to_fed_id_.end()) { + if (const Federation::Model* m = federation_->findById(fed_it->second)) { + // ModelUnits + the active CoordinateOperation come from the + // ModelGeoref cache; defaults are safe (1.0, 1.0, identity) + // when the IFC file isn't yet available. + ModelUnits units; + 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) { + coord_op = gr->coordinate_operation_meters; + } + } + M = composeModelTransformation( + m->model_transformation, federation_->config(), + units, coord_op); + } + } + viewport_->setModelTransformation(mid, M); +} + +void MainWindow::applyFederatedFalseOriginToViewport() { + const Eigen::Matrix4d M = composeFederatedFalseOrigin( + federation_->federatedFalseOrigin(), federation_->config()); + viewport_->setFederatedFalseOrigin(M); } void MainWindow::onLoadedFromSidecar(uint32_t /*mid*/, qint64 elapsed_ms) { diff --git a/src/ifcviewer-full/MainWindow.h b/src/ifcviewer-full/MainWindow.h index 03dac8f4f2..86bfc3c00c 100644 --- a/src/ifcviewer-full/MainWindow.h +++ b/src/ifcviewer-full/MainWindow.h @@ -103,6 +103,16 @@ private: // or its IFC file isn't available (sidecar-hit before data-source // load); the call retries on onDataSourceReady. void applyCoordinateOperationToViewport(uint32_t mid); + + // Push a model's ModelTransformation (stage 4) matrix to the viewport, + // composed from the federation's authoring intent + the model's units + // + the active CoordinateOperation matrix. Identity when the model is + // not in the federation. + void applyModelTransformationToViewport(uint32_t mid); + + // Push the federation-wide FederatedFalseOrigin (stage 3) matrix to + // the viewport. Affects every loaded model. + void applyFederatedFalseOriginToViewport(); QString formatElapsed(qint64 ms) const; ViewportWindow* viewport_ = nullptr; diff --git a/src/ifcviewer/Federation.cpp b/src/ifcviewer/Federation.cpp index 9dc500cbd1..ba4b33dddf 100644 --- a/src/ifcviewer/Federation.cpp +++ b/src/ifcviewer/Federation.cpp @@ -202,6 +202,7 @@ void Federation::setConfig(const FederationConfig& c) { return; config_ = c; setDirty(true); + emit configChanged(); } void Federation::setFederatedFalseOrigin(const FederatedFalseOrigin& o) { @@ -209,6 +210,7 @@ void Federation::setFederatedFalseOrigin(const FederatedFalseOrigin& o) { federated_false_origin_.rz_deg == o.rz_deg) return; federated_false_origin_ = o; setDirty(true); + emit federatedFalseOriginChanged(); } void Federation::setModelTransformation(const QString& fed_id, @@ -217,6 +219,7 @@ void Federation::setModelTransformation(const QString& fed_id, if (m.id != fed_id) continue; m.model_transformation = xf; setDirty(true); + emit modelTransformationChanged(fed_id); return; } } diff --git a/src/ifcviewer/Federation.h b/src/ifcviewer/Federation.h index 97fe072cff..87562168c1 100644 --- a/src/ifcviewer/Federation.h +++ b/src/ifcviewer/Federation.h @@ -204,6 +204,13 @@ public: signals: void dirtyChanged(bool dirty); + // Granular signals so consumers (notably the viewport-pushing layer in + // the host app) can recompose only what's needed. Emitted in addition + // to dirtyChanged from the corresponding setters. + void configChanged(); + void federatedFalseOriginChanged(); + void modelTransformationChanged(const QString& fed_id); + private: void setDirty(bool d); static QString generateId();