From bea4e38e6580277bab90e924726c068256a19c6d Mon Sep 17 00:00:00 2001 From: Dion Moult Date: Fri, 1 May 2026 17:31:08 +1000 Subject: [PATCH] ifcviewer: cache per-model georef in SceneLoader MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds ModelGeoref { ModelUnits units; Eigen::Matrix4d stage2_meters; bool has_stage2; } and computeModelGeoref(file*) in Federation.{h,cpp}. The helper reads the project length unit, IfcProjectedCRS.MapUnit, helmert parameters and WCS, and reduces them to a metres-in/metres-out stage 2 matrix using the existing Geolocation + Unit primitives. When the model has no IfcMapConversion it returns an identity stage_2 with has_stage2 == false, so the upload pipeline can branch cheaply. SceneLoader::Model gains a cached ModelGeoref; SceneLoader::modelGeoref (uint32_t mid) computes lazily on first call (returns nullptr when the IFC file isn't available yet — happens on the sidecar-hit path before the data-source thread populates the streamer) and serves from cache afterwards. Not yet consumed by the upload pipeline; that's the next commit. Co-Authored-By: Claude Opus 4.7 --- src/ifcviewer/Federation.cpp | 40 ++++++++++++++++++++++++++++++ src/ifcviewer/Federation.h | 17 +++++++++++++ src/ifcviewer/SceneLoader.cpp | 12 +++++++++ src/ifcviewer/SceneLoader.h | 12 +++++++++ src/ifcviewer/tests/CMakeLists.txt | 9 ++++--- 5 files changed, 87 insertions(+), 3 deletions(-) diff --git a/src/ifcviewer/Federation.cpp b/src/ifcviewer/Federation.cpp index b7237dcde5..ac1fb3a894 100644 --- a/src/ifcviewer/Federation.cpp +++ b/src/ifcviewer/Federation.cpp @@ -18,6 +18,7 @@ ********************************************************************************/ #include "Federation.h" +#include "Geolocation.h" #include "Unit.h" #include @@ -95,6 +96,45 @@ Eigen::Matrix4d composeFederationOrigin(const FederationOrigin& origin, return Rz4 * translation4(-xyz_m); } +ModelGeoref computeModelGeoref(ifcopenshell::file* ifc_file) { + ModelGeoref out; + if (!ifc_file) return out; + + out.units.project_length_to_meters = + calculateUnitScale(ifc_file, "LENGTHUNIT"); + + if (auto map_unit = getMapUnit(ifc_file)) { + if (auto s = siScaleFromNamedUnit(*map_unit)) { + out.units.map_unit_to_meters = *s; + } else { + out.units.map_unit_to_meters = out.units.project_length_to_meters; + } + } else { + // No MapUnit on the IfcProjectedCRS — fall back to project length unit. + out.units.map_unit_to_meters = out.units.project_length_to_meters; + } + + auto params = getHelmertTransformationParameters(ifc_file); + if (!params) return out; + + Eigen::Matrix4d helmert = + helmertMetersFromParameters(*params, out.units.map_unit_to_meters); + + if (auto wcs = getWcs(ifc_file)) { + // getWcs returns the WCS in project units (translation in project + // length units). Convert translation to metres before inverting. + Eigen::Matrix4d wcs_m = *wcs; + wcs_m(0, 3) *= out.units.project_length_to_meters; + wcs_m(1, 3) *= out.units.project_length_to_meters; + wcs_m(2, 3) *= out.units.project_length_to_meters; + out.stage2_meters = helmert * wcs_m.inverse(); + } else { + out.stage2_meters = helmert; + } + out.has_stage2 = true; + return out; +} + Eigen::Matrix4d composeModelTransform(const ModelTransform& xf, const FederationConfig& fed_cfg, const ModelUnits& model_units, diff --git a/src/ifcviewer/Federation.h b/src/ifcviewer/Federation.h index 721ecfba34..d93603e45e 100644 --- a/src/ifcviewer/Federation.h +++ b/src/ifcviewer/Federation.h @@ -31,6 +31,8 @@ #include #include +namespace ifcopenshell { class file; } + // === Stage-3/4 data model === // // A federation places one or more IFC models in a shared scene. Each model's @@ -96,6 +98,21 @@ struct ModelUnits { double map_unit_to_meters = 1.0; }; +// Per-model georeferencing data derived from the IFC. `stage2_meters` is +// the helmert · inv(wcs) georef matrix in metres; consumers compose it +// before stage 3 / stage 4 at upload time. When the model has no map +// conversion, `has_stage2 == false` and `stage2_meters` is identity. +struct ModelGeoref { + ModelUnits units; + Eigen::Matrix4d stage2_meters = Eigen::Matrix4d::Identity(); + bool has_stage2 = false; +}; + +// Read a model's project length unit, map unit, helmert parameters, and WCS +// from `ifc_file` and reduce them to a metres-in / metres-out georef matrix. +// Pure compute; safe to call repeatedly if the caller doesn't want to cache. +ModelGeoref computeModelGeoref(ifcopenshell::file* ifc_file); + // 1 federation_unit -> N metres. double federationUnitToMeters(const FederationConfig&); diff --git a/src/ifcviewer/SceneLoader.cpp b/src/ifcviewer/SceneLoader.cpp index df6e66c71c..d455615376 100644 --- a/src/ifcviewer/SceneLoader.cpp +++ b/src/ifcviewer/SceneLoader.cpp @@ -69,6 +69,18 @@ ifcopenshell::file* SceneLoader::ifcFile(uint32_t mid) const { return it == models_.end() ? nullptr : it->second.streamer->ifcFile(); } +const ModelGeoref* SceneLoader::modelGeoref(uint32_t mid) { + auto it = models_.find(mid); + if (it == models_.end()) return nullptr; + auto& m = it->second; + if (m.has_georef) return &m.georef; + auto* file = m.streamer ? m.streamer->ifcFile() : nullptr; + if (!file) return nullptr; + m.georef = computeModelGeoref(file); + m.has_georef = true; + return &m.georef; +} + std::vector SceneLoader::addFiles(const QStringList& paths) { std::vector assigned; assigned.reserve(paths.size()); diff --git a/src/ifcviewer/SceneLoader.h b/src/ifcviewer/SceneLoader.h index 344c4b985d..6efca5a555 100644 --- a/src/ifcviewer/SceneLoader.h +++ b/src/ifcviewer/SceneLoader.h @@ -33,6 +33,7 @@ #include #include +#include "Federation.h" #include "ViewportWindow.h" #include "GeometryStreamer.h" #include "SidecarCache.h" @@ -65,6 +66,12 @@ public: QString displayName(uint32_t mid) const; ifcopenshell::file* ifcFile(uint32_t mid) const; + // Lazily computes the model's georef matrix + unit scales the first + // time it's asked for, caches the result, and returns a pointer into the + // cache. Returns nullptr when the IFC file isn't available yet (e.g. + // sidecar-hit path before the data-source thread populates the streamer). + const ModelGeoref* modelGeoref(uint32_t mid); + signals: void progressChanged(int percent); void loadStarted(uint32_t mid, QString display_name); @@ -113,6 +120,11 @@ private: QString display_name; GeometryStreamer* streamer = nullptr; QElapsedTimer load_timer; + + // Cached on first SceneLoader::modelGeoref(mid) call once the + // streamer has its IFC file loaded. + ModelGeoref georef; + bool has_georef = false; }; void startNextLoad(); diff --git a/src/ifcviewer/tests/CMakeLists.txt b/src/ifcviewer/tests/CMakeLists.txt index bcf72ac87e..c07512d66d 100644 --- a/src/ifcviewer/tests/CMakeLists.txt +++ b/src/ifcviewer/tests/CMakeLists.txt @@ -58,10 +58,13 @@ find_package(Eigen3 REQUIRED) add_executable(test_federation test_federation.cpp ${IFCVIEWER_SRC}/Federation.cpp - # Federation pulls in Unit::convert for federation_unit_to_meters; compile - # Unit.cpp directly so the test doesn't have to link the whole IfcViewer - # library (which would drag in Qt6::OpenGL, OpenCASCADE, etc.). + # Federation pulls in Unit::convert for federationUnitToMeters and + # Geolocation helpers (helmertMetersFromParameters, getWcs, getMapUnit) + # for computeModelGeoref; compile both directly so the test doesn't + # have to link the whole IfcViewer library (which would drag in + # Qt6::OpenGL, OpenCASCADE, etc.). ${IFCVIEWER_SRC}/Unit.cpp + ${IFCVIEWER_SRC}/Geolocation.cpp ) set_target_properties(test_federation PROPERTIES AUTOMOC ON) target_include_directories(test_federation PRIVATE ${IFCVIEWER_SRC})