diff --git a/src/ifcviewer/CMakeLists.txt b/src/ifcviewer/CMakeLists.txt index fbca735837..d898b8ae3f 100644 --- a/src/ifcviewer/CMakeLists.txt +++ b/src/ifcviewer/CMakeLists.txt @@ -143,6 +143,7 @@ set(IFCVIEWER_CORE_SOURCES ChunkPlanner.cpp InstanceCompose.cpp LodBuilder.cpp + FederationMath.cpp SidecarCache.cpp SidecarCompress.cpp StreamingLoader.cpp @@ -200,6 +201,7 @@ set(IFCVIEWER_CORE_HEADERS SectionGizmoRenderer.h SectionPlane.h SelectionState.h + FederationMath.h SidecarCache.h SidecarCompress.h StreamingLoader.h @@ -213,7 +215,19 @@ list(TRANSFORM IFCVIEWER_CORE_SOURCES PREPEND "${CMAKE_CURRENT_SOURCE_DIR}/") list(TRANSFORM IFCVIEWER_CORE_HEADERS PREPEND "${CMAKE_CURRENT_SOURCE_DIR}/") add_library(IfcViewerCore STATIC ${IFCVIEWER_CORE_SOURCES} ${IFCVIEWER_CORE_HEADERS}) + target_include_directories(IfcViewerCore PUBLIC ${CMAKE_CURRENT_SOURCE_DIR}) + +# FederationMath needs convert() to resolve a federation unit name to metres and +# x_axis_to_angle_deg() to read grid north off a CoordinateOperation. Both live +# in src/helpers, whose main library needs IfcParse — helpers_math is the +# schema-free subset that does not, so it builds under Emscripten too. The web +# build reaches this via add_subdirectory below; on desktop src/CMakeLists.txt +# has already defined the target. +if(NOT TARGET helpers_math) + add_subdirectory(${CMAKE_CURRENT_SOURCE_DIR}/../helpers helpers_math_build EXCLUDE_FROM_ALL) +endif() +target_link_libraries(IfcViewerCore PUBLIC helpers_math) target_link_libraries(IfcViewerCore PUBLIC Eigen3::Eigen wgpu_native diff --git a/src/ifcviewer/Federation.cpp b/src/ifcviewer/Federation.cpp index 25f2b868f9..1470ac3b50 100644 --- a/src/ifcviewer/Federation.cpp +++ b/src/ifcviewer/Federation.cpp @@ -38,28 +38,6 @@ namespace { constexpr const char* kSchema = "ifcfed/1"; -constexpr double kPi = 3.14159265358979323846; -constexpr double kDegToRad = kPi / 180.0; - -Eigen::Matrix4d translation4(const Eigen::Vector3d& t) { - Eigen::Matrix4d M = Eigen::Matrix4d::Identity(); - M(0, 3) = t.x(); - M(1, 3) = t.y(); - M(2, 3) = t.z(); - return M; -} - -// Intrinsic XYZ Euler: R = R_z · R_y · R_x. -Eigen::Matrix4d eulerXYZ(const Eigen::Vector3d& rxyz_rad) { - const Eigen::Matrix3d R3 = - (Eigen::AngleAxisd(rxyz_rad.z(), Eigen::Vector3d::UnitZ()) * - Eigen::AngleAxisd(rxyz_rad.y(), Eigen::Vector3d::UnitY()) * - Eigen::AngleAxisd(rxyz_rad.x(), Eigen::Vector3d::UnitX())).matrix(); - Eigen::Matrix4d R = Eigen::Matrix4d::Identity(); - R.block<3, 3>(0, 0) = R3; - return R; -} - QString resolvePath(const QString& fed_dir, const QString& stored) { if (stored.isEmpty()) return stored; QFileInfo fi(stored); @@ -82,22 +60,6 @@ QString relativizePath(const QString& fed_dir, const QString& abs_path) { // === Stage-3/4 compose helpers === -double federationUnitToMeters(const FederationConfig& cfg) { - return convert(1.0, cfg.unit_prefix, cfg.unit_name, "", "METRE"); -} - -Eigen::Matrix4d composeFederatedFalseOrigin(const FederatedFalseOrigin& origin, - const FederationConfig& cfg) { - const double u = federationUnitToMeters(cfg); - const Eigen::Vector3d xyz_m = origin.xyz * u; - const double rz_rad = origin.rz_deg * kDegToRad; - const Eigen::Matrix3d Rz = - Eigen::AngleAxisd(rz_rad, Eigen::Vector3d::UnitZ()).matrix(); - Eigen::Matrix4d Rz4 = Eigen::Matrix4d::Identity(); - Rz4.block<3, 3>(0, 0) = Rz; - return Rz4 * translation4(-xyz_m); -} - ModelGeoref computeModelGeoref(ifcopenshell::file* ifc_file) { ModelGeoref out; if (!ifc_file) return out; @@ -128,71 +90,6 @@ ModelGeoref computeModelGeoref(ifcopenshell::file* ifc_file) { return out; } -FederatedFalseOrigin -guessFederatedFalseOrigin(const Eigen::Vector3d& first_geometry_point_m, - const ModelGeoref& georef, - const FederationConfig& fed_cfg) { - Eigen::Vector3d t_m = first_geometry_point_m; - - 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>(); - } - - const double u_fed = federationUnitToMeters(fed_cfg); - const double u_fed_inv = (u_fed != 0.0) ? (1.0 / u_fed) : 1.0; - - FederatedFalseOrigin out; - out.xyz = t_m * u_fed_inv; - - // Rotation: helmert grid-north baked into coordinate_operation_meters. - // helmert_meters_from_parameters built that block as R_z(theta)·diag(fx,fy,fz) - // with theta = atan2(xao, xaa); x_axis_to_angle_deg is `-theta` in degrees. - if (use_coord_op) { - const Eigen::Matrix4d& M = georef.coordinate_operation_meters; - out.rz_deg = x_axis_to_angle_deg(M(0, 0), M(1, 0)); - } - return out; -} - -Eigen::Matrix4d composeModelTransformation(const ModelTransformation& xf, - const FederationConfig& fed_cfg, - const ModelUnits& model_units, - const Eigen::Matrix4d& coordinate_operation_meters) { - const double u_fed = federationUnitToMeters(fed_cfg); - - Eigen::Vector3d A_m; - if (xf.a_frame == AFrame::ModelLocal) { - // a is in the model's project length unit, expressed in the - // pre-CoordinateOperation frame. Convert to metres, then lift - // through the CoordinateOperation. - const Eigen::Vector4d a_h( - xf.a.x() * model_units.project_length_to_meters, - xf.a.y() * model_units.project_length_to_meters, - xf.a.z() * model_units.project_length_to_meters, - 1.0); - A_m = (coordinate_operation_meters * a_h).head<3>(); - } else { - // a is in the model's map unit, expressed in the - // post-CoordinateOperation frame. - A_m = xf.a * model_units.map_unit_to_meters; - } - - const Eigen::Vector3d B_m = xf.b * u_fed; - const Eigen::Vector3d pivot_m = xf.pivot * u_fed; - - const Eigen::Matrix4d R_local = eulerXYZ(xf.rxyz_deg * kDegToRad); - const Eigen::Matrix4d R_at_pivot = - translation4(pivot_m) * R_local * translation4(-pivot_m); - - const Eigen::Vector4d Ah(A_m.x(), A_m.y(), A_m.z(), 1.0); - const Eigen::Vector3d RA = (R_at_pivot * Ah).head<3>(); - const Eigen::Matrix4d T = translation4(B_m - RA); - - return T * R_at_pivot; -} - // === Federation class === Federation::Federation(QObject* parent) : QObject(parent) {} diff --git a/src/ifcviewer/Federation.h b/src/ifcviewer/Federation.h index 98a66df35e..15a4c19be7 100644 --- a/src/ifcviewer/Federation.h +++ b/src/ifcviewer/Federation.h @@ -20,6 +20,8 @@ #ifndef FEDERATION_H #define FEDERATION_H +#include "FederationMath.h" + #include #include @@ -60,115 +62,12 @@ namespace ifcopenshell { class file; } // to round-trip without precision loss; conversion happens in the compose // helpers. -// Federation-wide unit; the value space for FederatedFalseOrigin.xyz and -// ModelTransformation::{b, pivot}. -struct FederationConfig { - // IfcSIUnit name ("METRE") or IfcConversionBasedUnit name ("foot", "inch"). - std::string unit_name = "METRE"; - // SI prefix ("MILLI", "KILO", ...) — empty for unprefixed or for - // conversion-based units. - std::string unit_prefix = ""; -}; - -// FederatedFalseOrigin — the user-nominated federation origin. Authoring -// intent is "nominate this XYZ as the new origin, with optional Z-axis -// heading rotation". Composed as R_z(rz_deg) · T(-xyz_in_metres). -struct FederatedFalseOrigin { - Eigen::Vector3d xyz = Eigen::Vector3d::Zero(); // federation unit - double rz_deg = 0.0; -}; - -// Frame in which ModelTransformation.a is expressed. -// ModelLocal — pre-CoordinateOperation model coordinates, in the model's -// project length unit -// ModelGlobal — post-CoordinateOperation model coordinates, in the model's -// map unit -enum class AFrame { ModelLocal, ModelGlobal }; - -// ModelTransformation — the per-model placement within the federation. -// Authoring intent is "rotate the model around `pivot`, then translate so -// that point `a` lands at point `b`". Composed as -// -// R_local = R_z(rz) · R_y(ry) · R_x(rx) [intrinsic XYZ] -// R_at_pivot = T(pivot_m) · R_local · T(-pivot_m) -// result = T(b_m - R_at_pivot · a_m) · R_at_pivot -struct ModelTransformation { - AFrame a_frame = AFrame::ModelGlobal; - Eigen::Vector3d a = Eigen::Vector3d::Zero(); // model project / map unit - Eigen::Vector3d b = Eigen::Vector3d::Zero(); // federation unit - Eigen::Vector3d rxyz_deg = Eigen::Vector3d::Zero(); // degrees, intrinsic XYZ - Eigen::Vector3d pivot = Eigen::Vector3d::Zero(); // federation unit -}; - -// Per-model unit scales captured at load time. project_length_to_meters comes -// from calculate_unit_scale(file, "LENGTHUNIT"). map_unit_to_meters is derived -// from IfcMapConversion.Scale as project_length_to_meters / Scale; the -// IfcProjectedCRS.MapUnit named unit is metadata and does not affect the -// transform composition. -struct ModelUnits { - double project_length_to_meters = 1.0; - double map_unit_to_meters = 1.0; -}; - -// Per-model georeferencing data derived from the IFC. -// `coordinate_operation_meters` is the helmert · inv(wcs) matrix in metres -// representing the IfcCoordinateOperation; consumers compose it before -// FederatedFalseOrigin / ModelTransformation at upload time. When the -// model has no map conversion, `has_coordinate_operation == false` and the -// matrix is identity. -struct ModelGeoref { - ModelUnits units; - Eigen::Matrix4d coordinate_operation_meters = Eigen::Matrix4d::Identity(); - bool has_coordinate_operation = 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 // CoordinateOperation matrix. Pure compute; safe to call repeatedly if the // caller doesn't want to cache. ModelGeoref computeModelGeoref(ifcopenshell::file* ifc_file); -// Build a FederatedFalseOrigin guess so that a model lands near the -// federation origin instead of out at its surveyor coordinates. Designed -// to work without an open IFC file so it's usable from sidecar-only loads -// (the inputs are all derivable from the resident MeshInfo + InstanceInfo -// data + ModelGeoref). -// -// Position: `first_geometry_point_m` is a point that actually lies on the -// model's first instance's geometry, in metres, pre-CoordinateOperation — -// typically the world-space centre of the first instance's mesh AABB -// (instance0.placement_transformation * mesh.local_aabb_center). We use -// a real geometry point rather than the instance's placement translation -// because IFC placements often live far from the actual geometry (long -// ObjectPlacement chains, intermediate local coordinate systems). The -// point 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 `has_coordinate_operation` (this is the helmert grid-north -// angle); otherwise zero. Anticlockwise positive. -FederatedFalseOrigin -guessFederatedFalseOrigin(const Eigen::Vector3d& first_geometry_point_m, - const ModelGeoref& georef, - const FederationConfig& fed_cfg); - -// 1 federation_unit -> N metres. -double federationUnitToMeters(const FederationConfig&); - -// Compose FederatedFalseOrigin into a 4x4 matrix in metres. -Eigen::Matrix4d composeFederatedFalseOrigin(const FederatedFalseOrigin&, - const FederationConfig&); - -// Compose ModelTransformation into a 4x4 matrix in metres. -// `coordinate_operation_meters` is the model's CoordinateOperation matrix -// (e.g. helmert_meters_from_parameters · inv(wcs_meters)) — needed to lift -// `a` into metres when a_frame == ModelLocal. Pass identity when the -// CoordinateOperation is disabled or absent. -Eigen::Matrix4d composeModelTransformation(const ModelTransformation&, - const FederationConfig& fed_cfg, - const ModelUnits& model_units, - const Eigen::Matrix4d& coordinate_operation_meters); - // === Federation persistence (.ifcfed) === // // In-memory representation of an .ifcfed file (IFC federation). diff --git a/src/ifcviewer/FederationMath.cpp b/src/ifcviewer/FederationMath.cpp new file mode 100644 index 0000000000..4546d87f1b --- /dev/null +++ b/src/ifcviewer/FederationMath.cpp @@ -0,0 +1,132 @@ +/******************************************************************************** + * * + * 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 . * + * * + ********************************************************************************/ + +#include "FederationMath.h" + +#include "geolocation_transform.h" // x_axis_to_angle_deg +#include "unit_convert.h" // convert + +#include + +namespace { + +constexpr double kPi = 3.14159265358979323846; +constexpr double kDegToRad = kPi / 180.0; + +Eigen::Matrix4d translation4(const Eigen::Vector3d& t) { + Eigen::Matrix4d M = Eigen::Matrix4d::Identity(); + M(0, 3) = t.x(); + M(1, 3) = t.y(); + M(2, 3) = t.z(); + return M; +} + +// Intrinsic XYZ Euler: R = R_z · R_y · R_x. +Eigen::Matrix4d eulerXYZ(const Eigen::Vector3d& rxyz_rad) { + const Eigen::Matrix3d R3 = + (Eigen::AngleAxisd(rxyz_rad.z(), Eigen::Vector3d::UnitZ()) * + Eigen::AngleAxisd(rxyz_rad.y(), Eigen::Vector3d::UnitY()) * + Eigen::AngleAxisd(rxyz_rad.x(), Eigen::Vector3d::UnitX())).matrix(); + Eigen::Matrix4d R = Eigen::Matrix4d::Identity(); + R.block<3, 3>(0, 0) = R3; + return R; +} + +} // namespace + +double federationUnitToMeters(const FederationConfig& cfg) { + return convert(1.0, cfg.unit_prefix, cfg.unit_name, "", "METRE"); +} + +Eigen::Matrix4d composeFederatedFalseOrigin(const FederatedFalseOrigin& origin, + const FederationConfig& cfg) { + const double u = federationUnitToMeters(cfg); + const Eigen::Vector3d xyz_m = origin.xyz * u; + const double rz_rad = origin.rz_deg * kDegToRad; + const Eigen::Matrix3d Rz = + Eigen::AngleAxisd(rz_rad, Eigen::Vector3d::UnitZ()).matrix(); + Eigen::Matrix4d Rz4 = Eigen::Matrix4d::Identity(); + Rz4.block<3, 3>(0, 0) = Rz; + return Rz4 * translation4(-xyz_m); +} + +FederatedFalseOrigin +guessFederatedFalseOrigin(const Eigen::Vector3d& first_geometry_point_m, + const ModelGeoref& georef, + const FederationConfig& fed_cfg) { + Eigen::Vector3d t_m = first_geometry_point_m; + + 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>(); + } + + const double u_fed = federationUnitToMeters(fed_cfg); + const double u_fed_inv = (u_fed != 0.0) ? (1.0 / u_fed) : 1.0; + + FederatedFalseOrigin out; + out.xyz = t_m * u_fed_inv; + + // Rotation: helmert grid-north baked into coordinate_operation_meters. + // helmert_meters_from_parameters built that block as R_z(theta)·diag(fx,fy,fz) + // with theta = atan2(xao, xaa); x_axis_to_angle_deg is `-theta` in degrees. + if (use_coord_op) { + const Eigen::Matrix4d& M = georef.coordinate_operation_meters; + out.rz_deg = x_axis_to_angle_deg(M(0, 0), M(1, 0)); + } + return out; +} + +Eigen::Matrix4d composeModelTransformation(const ModelTransformation& xf, + const FederationConfig& fed_cfg, + const ModelUnits& model_units, + const Eigen::Matrix4d& coordinate_operation_meters) { + const double u_fed = federationUnitToMeters(fed_cfg); + + Eigen::Vector3d A_m; + if (xf.a_frame == AFrame::ModelLocal) { + // a is in the model's project length unit, expressed in the + // pre-CoordinateOperation frame. Convert to metres, then lift + // through the CoordinateOperation. + const Eigen::Vector4d a_h( + xf.a.x() * model_units.project_length_to_meters, + xf.a.y() * model_units.project_length_to_meters, + xf.a.z() * model_units.project_length_to_meters, + 1.0); + A_m = (coordinate_operation_meters * a_h).head<3>(); + } else { + // a is in the model's map unit, expressed in the + // post-CoordinateOperation frame. + A_m = xf.a * model_units.map_unit_to_meters; + } + + const Eigen::Vector3d B_m = xf.b * u_fed; + const Eigen::Vector3d pivot_m = xf.pivot * u_fed; + + const Eigen::Matrix4d R_local = eulerXYZ(xf.rxyz_deg * kDegToRad); + const Eigen::Matrix4d R_at_pivot = + translation4(pivot_m) * R_local * translation4(-pivot_m); + + const Eigen::Vector4d Ah(A_m.x(), A_m.y(), A_m.z(), 1.0); + const Eigen::Vector3d RA = (R_at_pivot * Ah).head<3>(); + const Eigen::Matrix4d T = translation4(B_m - RA); + + return T * R_at_pivot; +} diff --git a/src/ifcviewer/FederationMath.h b/src/ifcviewer/FederationMath.h new file mode 100644 index 0000000000..aa198e60ec --- /dev/null +++ b/src/ifcviewer/FederationMath.h @@ -0,0 +1,144 @@ +/******************************************************************************** + * * + * 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 . * + * * + ********************************************************************************/ + +// Qt-free half of the federation transformation pipeline: the value types and +// the pure compose math over them. +// +// Lives apart from Federation.h so IfcViewerCore — and through it the +// Emscripten build — can compose federated transforms without linking Qt or +// IfcParse. What stays in Federation.h is everything that genuinely needs +// them: computeModelGeoref (reads an ifcopenshell::file) and the Federation +// document model (a QObject that persists .ifcfed). +// +// The pipeline comment in Federation.h describes how these compose. + +#ifndef FEDERATIONMATH_H +#define FEDERATIONMATH_H + +#include + +#include + +// Federation-wide unit; the value space for FederatedFalseOrigin.xyz and +// ModelTransformation::{b, pivot}. +struct FederationConfig { + // IfcSIUnit name ("METRE") or IfcConversionBasedUnit name ("foot", "inch"). + std::string unit_name = "METRE"; + // SI prefix ("MILLI", "KILO", ...) — empty for unprefixed or for + // conversion-based units. + std::string unit_prefix = ""; +}; + +// FederatedFalseOrigin — the user-nominated federation origin. Authoring +// intent is "nominate this XYZ as the new origin, with optional Z-axis +// heading rotation". Composed as R_z(rz_deg) · T(-xyz_in_metres). +struct FederatedFalseOrigin { + Eigen::Vector3d xyz = Eigen::Vector3d::Zero(); // federation unit + double rz_deg = 0.0; +}; + +// Frame in which ModelTransformation.a is expressed. +// ModelLocal — pre-CoordinateOperation model coordinates, in the model's +// project length unit +// ModelGlobal — post-CoordinateOperation model coordinates, in the model's +// map unit +enum class AFrame { ModelLocal, ModelGlobal }; + +// ModelTransformation — the per-model placement within the federation. +// Authoring intent is "rotate the model around `pivot`, then translate so +// that point `a` lands at point `b`". Composed as +// +// R_local = R_z(rz) · R_y(ry) · R_x(rx) [intrinsic XYZ] +// R_at_pivot = T(pivot_m) · R_local · T(-pivot_m) +// result = T(b_m - R_at_pivot · a_m) · R_at_pivot +struct ModelTransformation { + AFrame a_frame = AFrame::ModelGlobal; + Eigen::Vector3d a = Eigen::Vector3d::Zero(); // model project / map unit + Eigen::Vector3d b = Eigen::Vector3d::Zero(); // federation unit + Eigen::Vector3d rxyz_deg = Eigen::Vector3d::Zero(); // degrees, intrinsic XYZ + Eigen::Vector3d pivot = Eigen::Vector3d::Zero(); // federation unit +}; + +// Per-model unit scales captured at load time. project_length_to_meters comes +// from calculate_unit_scale(file, "LENGTHUNIT"). map_unit_to_meters is derived +// from IfcMapConversion.Scale as project_length_to_meters / Scale; the +// IfcProjectedCRS.MapUnit named unit is metadata and does not affect the +// transform composition. +struct ModelUnits { + double project_length_to_meters = 1.0; + double map_unit_to_meters = 1.0; +}; + +// Per-model georeferencing data derived from the IFC. +// `coordinate_operation_meters` is the helmert · inv(wcs) matrix in metres +// representing the IfcCoordinateOperation; consumers compose it before +// FederatedFalseOrigin / ModelTransformation at upload time. When the +// model has no map conversion, `has_coordinate_operation == false` and the +// matrix is identity. +struct ModelGeoref { + ModelUnits units; + Eigen::Matrix4d coordinate_operation_meters = Eigen::Matrix4d::Identity(); + bool has_coordinate_operation = false; +}; + +// computeModelGeoref, which fills a ModelGeoref from an ifcopenshell::file, +// stays in Federation.h — it needs IfcParse and the geolocation helpers. + +// Build a FederatedFalseOrigin guess so that a model lands near the +// federation origin instead of out at its surveyor coordinates. Designed +// to work without an open IFC file so it's usable from sidecar-only loads +// (the inputs are all derivable from the resident MeshInfo + InstanceInfo +// data + ModelGeoref). +// +// Position: `first_geometry_point_m` is a point that actually lies on the +// model's first instance's geometry, in metres, pre-CoordinateOperation — +// typically the world-space centre of the first instance's mesh AABB +// (instance0.placement_transformation * mesh.local_aabb_center). We use +// a real geometry point rather than the instance's placement translation +// because IFC placements often live far from the actual geometry (long +// ObjectPlacement chains, intermediate local coordinate systems). The +// point 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 `has_coordinate_operation` (this is the helmert grid-north +// angle); otherwise zero. Anticlockwise positive. +FederatedFalseOrigin +guessFederatedFalseOrigin(const Eigen::Vector3d& first_geometry_point_m, + const ModelGeoref& georef, + const FederationConfig& fed_cfg); + +// 1 federation_unit -> N metres. +double federationUnitToMeters(const FederationConfig&); + +// Compose FederatedFalseOrigin into a 4x4 matrix in metres. +Eigen::Matrix4d composeFederatedFalseOrigin(const FederatedFalseOrigin&, + const FederationConfig&); + +// Compose ModelTransformation into a 4x4 matrix in metres. +// `coordinate_operation_meters` is the model's CoordinateOperation matrix +// (e.g. helmert_meters_from_parameters · inv(wcs_meters)) — needed to lift +// `a` into metres when a_frame == ModelLocal. Pass identity when the +// CoordinateOperation is disabled or absent. +Eigen::Matrix4d composeModelTransformation(const ModelTransformation&, + const FederationConfig& fed_cfg, + const ModelUnits& model_units, + const Eigen::Matrix4d& coordinate_operation_meters); + +#endif // FEDERATIONMATH_H diff --git a/src/ifcviewer/tests/CMakeLists.txt b/src/ifcviewer/tests/CMakeLists.txt index 9306a5cfeb..42a953a6f2 100644 --- a/src/ifcviewer/tests/CMakeLists.txt +++ b/src/ifcviewer/tests/CMakeLists.txt @@ -149,6 +149,7 @@ find_package(Eigen3 REQUIRED) add_executable(test_federation test_federation.cpp ${IFCVIEWER_SRC}/Federation.cpp + ${IFCVIEWER_SRC}/FederationMath.cpp # compose helpers, split out for IfcViewerCore ) set_target_properties(test_federation PROPERTIES AUTOMOC ON) target_include_directories(test_federation PRIVATE ${IFCVIEWER_SRC})