diff --git a/src/ifcviewer/Federation.cpp b/src/ifcviewer/Federation.cpp index 478c636de0..8022c92b99 100644 --- a/src/ifcviewer/Federation.cpp +++ b/src/ifcviewer/Federation.cpp @@ -105,18 +105,9 @@ ModelGeoref computeModelGeoref(ifcopenshell::file* ifc_file) { 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); + const double scale = (params && params->scale != 0.0) ? params->scale : 1.0; + out.units.map_unit_to_meters = out.units.project_length_to_meters / scale; if (!params) return out; Eigen::Matrix4d helmert = diff --git a/src/ifcviewer/Federation.h b/src/ifcviewer/Federation.h index 6102b7cb60..6e671b58e1 100644 --- a/src/ifcviewer/Federation.h +++ b/src/ifcviewer/Federation.h @@ -100,10 +100,11 @@ struct ModelTransformation { Eigen::Vector3d pivot = Eigen::Vector3d::Zero(); // federation unit }; -// Per-model unit scales captured at load time. project_length_to_meters -// comes from calculateUnitScale(file, "LENGTHUNIT"); map_unit_to_meters from -// siScaleFromNamedUnit(getMapUnit(file)) and falls back to the project length -// scale when the model has no MapUnit. +// Per-model unit scales captured at load time. project_length_to_meters comes +// from calculateUnitScale(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; diff --git a/src/ifcviewer/Geolocation.h b/src/ifcviewer/Geolocation.h index 13eaecd8b1..e4b456259c 100644 --- a/src/ifcviewer/Geolocation.h +++ b/src/ifcviewer/Geolocation.h @@ -81,9 +81,11 @@ Eigen::Matrix4d autoLocal2Global(ifcopenshell::file* ifc_file, // [ 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: +// `map_unit_to_meters` is derived by the caller from the IFC project length +// unit and the authoritative IfcMapConversion.Scale. Since this matrix takes +// meter inputs from the geometry iterator, Scale is represented by that unit +// conversion and is not applied again in the linear block. 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). @@ -96,8 +98,9 @@ Eigen::Matrix4d helmertMetersFromParameters(const HelmertTransformation& params, // 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. +// when MapUnit is absent on the IfcProjectedCRS. This is retained for UI / +// metadata inspection; transform composition derives map unit scale from +// IfcMapConversion.Scale instead. std::optional getMapUnit(ifcopenshell::file* ifc_file); // "How do I rotate project east to get to grid east?" — i.e. -atan2(xao, xaa) diff --git a/src/ifcviewer/SidecarCache.cpp b/src/ifcviewer/SidecarCache.cpp index 0b5a346e6a..d0ca2ccac2 100644 --- a/src/ifcviewer/SidecarCache.cpp +++ b/src/ifcviewer/SidecarCache.cpp @@ -17,7 +17,7 @@ * * ********************************************************************************/ -// v12 layout (all multi-byte fields native-endian; endianness marker in header). +// v13 layout (all multi-byte fields native-endian; endianness marker in header). // // SidecarHeader (12 bytes) // @@ -30,7 +30,7 @@ // MeshInfo[num_meshes] // // uint32_t num_instances -// InstanceCpu[num_instances] (already sorted by mesh_id; v12 layout) +// InstanceCpu[num_instances] (already sorted by mesh_id; v13 layout) // // uint32_t has_coordinate_operation (v11+) // double[16] coordinate_operation_meters (v11+; column-major) diff --git a/src/ifcviewer/SidecarCache.h b/src/ifcviewer/SidecarCache.h index 78b5ac2e0a..76dd52e4b2 100644 --- a/src/ifcviewer/SidecarCache.h +++ b/src/ifcviewer/SidecarCache.h @@ -63,7 +63,9 @@ static constexpr uint32_t SIDECAR_MAGIC = 0x49465657; // "IFVW" // InstanceChunk carries the streamer placement as double[16]. This keeps // large IFC placements exact until CoordinateOperation / FederatedFalseOrigin // composition has reduced them to viewport-local float-sized values. -static constexpr uint32_t SIDECAR_VERSION = 12; +// v13 = Map unit scale in cached ModelGeoref is derived from +// IfcMapConversion.Scale, not IfcProjectedCRS.MapUnit. +static constexpr uint32_t SIDECAR_VERSION = 13; static constexpr uint32_t SIDECAR_ENDIAN = 0x01020304; // Fixed-size element record. Strings are stored as (offset, length) pairs diff --git a/src/ifcviewer/tests/test_federation.cpp b/src/ifcviewer/tests/test_federation.cpp index a93ddec56b..b95f866dde 100644 --- a/src/ifcviewer/tests/test_federation.cpp +++ b/src/ifcviewer/tests/test_federation.cpp @@ -18,6 +18,7 @@ ********************************************************************************/ #include "Federation.h" +#include "Geolocation.h" #include @@ -457,6 +458,25 @@ TEST_CASE("composeFederatedFalseOrigin scales by federation unit", REQUIRE(std::abs(M(0, 3) - (-0.3048)) < 1e-9); } +TEST_CASE("helmert metres transform consumes Scale through map unit scale", + "[federation][georef]") { + HelmertTransformation params; + params.e = 500000.0; + params.n = 7000000.0; + params.scale = 0.001; // project mm -> map numeric metres + + const double project_length_to_meters = 0.001; + const double map_unit_to_meters = project_length_to_meters / params.scale; + Eigen::Matrix4d M = helmertMetersFromParameters(params, map_unit_to_meters); + + Eigen::Vector4d local_m(10.0, 20.0, 0.0, 1.0); + Eigen::Vector4d global_m = M * local_m; + + REQUIRE(std::abs(map_unit_to_meters - 1.0) < 1e-12); + REQUIRE(std::abs(global_m.x() - 500010.0) < 1e-9); + REQUIRE(std::abs(global_m.y() - 7000020.0) < 1e-9); +} + TEST_CASE("addGroup creates a top-level group; addGroup with parent nests it", "[federation][groups]") { ensureQApp(); diff --git a/src/ifcviewer/tests/test_sidecar_cache.cpp b/src/ifcviewer/tests/test_sidecar_cache.cpp index b4a96ff9a0..a9747462ce 100644 --- a/src/ifcviewer/tests/test_sidecar_cache.cpp +++ b/src/ifcviewer/tests/test_sidecar_cache.cpp @@ -155,7 +155,7 @@ bool sidecarDataEqual(const SidecarData& a, const SidecarData& b) { TEST_CASE("MeshInfo and InstanceCpu have stable layouts (sidecar wire format)", "[sidecar]") { REQUIRE(sizeof(MeshInfo) == 56); REQUIRE(sizeof(InstanceGpu) == 80); - REQUIRE(SIDECAR_VERSION == 12); + REQUIRE(SIDECAR_VERSION == 13); REQUIRE(SIDECAR_MAGIC == 0x49465657u); }