ifcviewer: add helmertMetersFromParameters and getMapUnit

helmertMetersFromParameters builds the helmert transformation as a
meter-input/meter-output 4x4 directly from parsed parameters, bypassing
autoLocal2Global's normalisation step.  This preserves
IfcMapConversionScaled.FactorX/Y/Z in the rotation block so the factor
applies to placement translations when the matrix is precomputed
per-model and composed with placements at upload time.  For ordinary
IfcMapConversion (factor = 1) this is bit-identical to
autoLocal2Global; only diverges on rare surveyed models with non-unit
factors, where it is the only correct behaviour.

getMapUnit returns IfcCoordinateOperation.TargetCRS.MapUnit so callers
can resolve the unit-to-metres scale via Unit.h's siScaleFromNamedUnit.

autoLocal2Global is unchanged — kept as a clean port of the python
ifcopenshell.util.geolocation reference impl for one-shot
project-units-in / map-units-out callers.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
Dion Moult
2026-05-01 12:43:27 +10:00
parent 0d3849737c
commit b3d29c4081
2 changed files with 67 additions and 0 deletions
+37
View File
@@ -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<express::Base> getMapUnit(ifcopenshell::file* ifc_file) {
std::vector<express::Base> 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<express::Entity>().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<express::Entity>().get("MapUnit");
if (mu_attr.isNull()) return std::nullopt;
return (express::Base) mu_attr;
}
+30
View File
@@ -27,6 +27,8 @@
#ifndef GEOLOCATION_H
#define GEOLOCATION_H
#include "../ifcparse/express.h"
#include <Eigen/Dense>
#include <optional>
@@ -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<express::Base> getMapUnit(ifcopenshell::file* ifc_file);
#endif // GEOLOCATION_H