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;
}