diff --git a/src/ifcviewer/Geolocation.cpp b/src/ifcviewer/Geolocation.cpp index 9649dc1255..80833ec062 100644 --- a/src/ifcviewer/Geolocation.cpp +++ b/src/ifcviewer/Geolocation.cpp @@ -274,3 +274,40 @@ Eigen::Matrix4d autoLocal2Global(ifcopenshell::file* ifc_file, } return result; } + +Eigen::Matrix4d helmertMetersFromParameters(const HelmertTransformation& p, + double map_unit_to_meters) { + const double theta = std::atan2(p.xao, p.xaa); + const double c = std::cos(theta); + const double s = std::sin(theta); + + Eigen::Matrix4d M = Eigen::Matrix4d::Identity(); + // R_z(theta) · diag(fx, fy, fz). Factors stay in the rotation block so + // they apply to placement translations on compose; this is the behaviour + // IfcMapConversionScaled actually wants ("grid distance ≠ ground + // distance" — buildings on the grid should appear scaled by f). + M(0, 0) = c * p.factor_x; M(0, 1) = -s * p.factor_y; M(0, 2) = 0.0; + M(1, 0) = s * p.factor_x; M(1, 1) = c * p.factor_y; M(1, 2) = 0.0; + M(2, 0) = 0.0; M(2, 1) = 0.0; M(2, 2) = p.factor_z; + M(0, 3) = p.e * map_unit_to_meters; + M(1, 3) = p.n * map_unit_to_meters; + M(2, 3) = p.h * map_unit_to_meters; + return M; +} + +std::optional getMapUnit(ifcopenshell::file* ifc_file) { + std::vector coordops; + try { + coordops = ifc_file->instances_by_type("IfcCoordinateOperation"); + } catch (...) { + return std::nullopt; + } + if (coordops.empty()) return std::nullopt; + auto target_attr = coordops[0].as().get("TargetCRS"); + if (target_attr.isNull()) return std::nullopt; + express::Base target = target_attr; + if (!target.declaration().is("IfcProjectedCRS")) return std::nullopt; + auto mu_attr = target.as().get("MapUnit"); + if (mu_attr.isNull()) return std::nullopt; + return (express::Base) mu_attr; +} diff --git a/src/ifcviewer/Geolocation.h b/src/ifcviewer/Geolocation.h index e83640a718..d5f14c196e 100644 --- a/src/ifcviewer/Geolocation.h +++ b/src/ifcviewer/Geolocation.h @@ -27,6 +27,8 @@ #ifndef GEOLOCATION_H #define GEOLOCATION_H +#include "../ifcparse/express.h" + #include #include @@ -70,4 +72,32 @@ Eigen::Matrix4d autoLocal2Global(ifcopenshell::file* ifc_file, const Eigen::Matrix4d& matrix, bool should_return_in_map_units = true); +// Build the Helmert transformation as a meter-input / meter-output 4x4 matrix +// directly from parsed parameters, bypassing autoLocal2Global's normalisation +// step. Used by callers that want a single per-model georef matrix to compose +// with placement matrices at upload time. +// +// Result has shape: +// [ R_z(theta) · diag(fx, fy, fz) | (e, n, h) · u_m ] +// [ 0 | 1 ] +// +// `map_unit_to_meters` is the SI scale of IfcProjectedCRS.MapUnit (or the +// project's LENGTHUNIT scale if MapUnit is absent). The caller composes any +// IfcGeometricRepresentationContext WCS on the right: +// G = helmertMetersFromParameters(...) · inv(wcs_meters) +// (where wcs_meters has its translation column converted from project units +// to meters via calculateUnitScale). +// +// Unlike autoLocal2Global, this preserves IfcMapConversionScaled.FactorX/Y/Z +// in the rotation block, so they apply correctly to placement translations +// when composing per-model. +Eigen::Matrix4d helmertMetersFromParameters(const HelmertTransformation& params, + double map_unit_to_meters); + +// IfcCoordinateOperation.TargetCRS.MapUnit (the IfcNamedUnit), if present. +// Returns nullopt for IFC2X3, models without an IfcCoordinateOperation, or +// when MapUnit is absent on the IfcProjectedCRS. Callers fall back to +// calculateUnitScale(file, "LENGTHUNIT") in that case. +std::optional getMapUnit(ifcopenshell::file* ifc_file); + #endif // GEOLOCATION_H