diff --git a/src/helpers/CMakeLists.txt b/src/helpers/CMakeLists.txt index c21c56113f..81e8fc83ee 100644 --- a/src/helpers/CMakeLists.txt +++ b/src/helpers/CMakeLists.txt @@ -36,8 +36,34 @@ message("Running CMakeLists.txt in /src/helpers") find_package(Eigen3 REQUIRED) +# helpers_math: the schema-free subset — unit conversion tables and Helmert +# matrix math over plain numbers. No IfcParse, no Qt, no schema dispatch, so it +# builds under Emscripten where the rest of this directory cannot. +# +# It exists as its own target rather than being compiled into both `helpers` and +# IfcViewerCore so the objects are built exactly once and both libraries agree +# on them. IfcViewerCore links it directly (see src/ifcviewer/CMakeLists.txt); +# `helpers` re-exports it PUBLIC below so existing consumers keep working +# through the unchanged unit.h / geolocation.h includes. +set(HELPER_MATH_CPP_FILES + ${CMAKE_CURRENT_SOURCE_DIR}/unit_convert.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/geolocation_transform.cpp +) +add_library(helpers_math STATIC ${HELPER_MATH_CPP_FILES}) +target_include_directories(helpers_math PUBLIC ${CMAKE_CURRENT_SOURCE_DIR}) +target_link_libraries(helpers_math PUBLIC Eigen3::Eigen) +set_target_properties(helpers_math PROPERTIES POSITION_INDEPENDENT_CODE ON) +install(TARGETS helpers_math EXPORT ${IFCOPENSHELL_EXPORT_TARGETS}) + +# The rest of this directory needs IfcParse and the generated schema bindings, +# so it is desktop-only. +if(EMSCRIPTEN) + return() +endif() + file(GLOB HELPER_CPP_FILES ${CMAKE_CURRENT_SOURCE_DIR}/*.cpp) file(GLOB HELPER_H_FILES ${CMAKE_CURRENT_SOURCE_DIR}/*.h) +list(REMOVE_ITEM HELPER_CPP_FILES ${HELPER_MATH_CPP_FILES}) add_library(helpers STATIC ${HELPER_CPP_FILES} ${HELPER_H_FILES}) @@ -64,6 +90,7 @@ target_include_directories(helpers PUBLIC ${CMAKE_CURRENT_SOURCE_DIR}) target_link_libraries(helpers PUBLIC IfcParse Eigen3::Eigen + helpers_math PRIVATE ${HELPER_SCHEMA_LIBRARIES} ) diff --git a/src/helpers/geolocation.cpp b/src/helpers/geolocation.cpp index 8e2670f0fe..414cdb136d 100644 --- a/src/helpers/geolocation.cpp +++ b/src/helpers/geolocation.cpp @@ -18,6 +18,7 @@ ********************************************************************************/ #include "geolocation.h" +#include "geolocation_transform.h" #include "../ifcparse/exception.h" #include "../ifcparse/file.h" @@ -247,40 +248,6 @@ std::optional get_wcs(ifcopenshell::file* ifc_file) { unsupported_schema(name); } -Eigen::Matrix4d local_to_global(const Eigen::Matrix4d& matrix, - const HelmertTransformation& p) { - const double theta = std::atan2(p.xao, p.xaa); - const double c = std::cos(theta); - const double s = std::sin(theta); - - Eigen::Matrix4d S = Eigen::Matrix4d::Identity(); - S(0, 0) = p.scale * p.factor_x; - S(1, 1) = p.scale * p.factor_y; - S(2, 2) = p.scale * p.factor_z; - - Eigen::Matrix4d R = Eigen::Matrix4d::Identity(); - R(0, 0) = c; - R(0, 1) = -s; - R(1, 0) = s; - R(1, 1) = c; - - Eigen::Matrix4d result = R * S * matrix; - // The scale was baked into the rotation+scale matrix so each axis column - // ended up scaled. Renormalise so the rotation part is pure orientation - // and the translation alone carries the scaled offsets. - for (int col = 0; col < 3; ++col) { - Eigen::Vector3d v = result.block<3, 1>(0, col); - const double n = v.norm(); - if (n > 0.0) { - result.block<3, 1>(0, col) = v / n; - } - } - result(0, 3) += p.e; - result(1, 3) += p.n; - result(2, 3) += p.h; - return result; -} - Eigen::Matrix4d auto_local_to_global(ifcopenshell::file* ifc_file, const Eigen::Matrix4d& matrix, bool should_return_in_map_units) { @@ -302,32 +269,6 @@ Eigen::Matrix4d auto_local_to_global(ifcopenshell::file* ifc_file, return result; } -Eigen::Matrix4d helmert_meters_from_parameters(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 get_map_unit(ifcopenshell::file* ifc_file) { const auto name = ifc_file->schema()->name(); #define IFCOPENSHELL_DISPATCH(Schema, Identifier) \ @@ -337,8 +278,3 @@ std::optional get_map_unit(ifcopenshell::file* ifc_file) { #undef IFCOPENSHELL_DISPATCH unsupported_schema(name); } - -double x_axis_to_angle_deg(double xaa, double xao) { - constexpr double PI = 3.14159265358979323846; - return -std::atan2(xao, xaa) * (180.0 / PI); -} diff --git a/src/helpers/geolocation.h b/src/helpers/geolocation.h index 94194d2acc..72ff44fa75 100644 --- a/src/helpers/geolocation.h +++ b/src/helpers/geolocation.h @@ -24,6 +24,7 @@ #define GEOLOCATION_H #include "../ifcparse/express.h" +#include "geolocation_transform.h" #include #include @@ -32,18 +33,6 @@ namespace ifcopenshell { class file; } -struct HelmertTransformation { - double e = 0.0; // eastings offset - double n = 0.0; // northings offset - double h = 0.0; // orthogonal-height offset - double xaa = 1.0; // X-axis abscissa (cos of grid-rotation angle) - double xao = 0.0; // X-axis ordinate (sin of grid-rotation angle) - double scale = 1.0; // unit scale (project unit -> map unit) - double factor_x = 1.0; // combined scale factor along X - double factor_y = 1.0; // combined scale factor along Y - double factor_z = 1.0; // combined scale factor along Z -}; - // Detect a Helmert transformation in the IFC model. Reads IfcMapConversion / // IfcMapConversionScaled / IfcRigidOperation in IFC4+, or the // IfcProject.ePSet_MapConversion property set in IFC2X3. Returns nullopt @@ -56,10 +45,6 @@ get_helmert_transformation_parameters(ifcopenshell::file* ifc_file); // no parseable WCS. std::optional get_wcs(ifcopenshell::file* ifc_file); -// Apply a Helmert transformation to a 4x4 local matrix. -Eigen::Matrix4d local_to_global(const Eigen::Matrix4d& matrix, - const HelmertTransformation& params); - // Lift a 4x4 local matrix into global (map) coordinates using the IFC model's // georeferencing data. When no map conversion is present the matrix is // returned unchanged. When should_return_in_map_units is false, the @@ -69,30 +54,6 @@ Eigen::Matrix4d auto_local_to_global(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 auto_local_to_global'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 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 = helmert_meters_from_parameters(...) · inv(wcs_meters) -// (where wcs_meters has its translation column converted from project units -// to meters via calculate_unit_scale). -// -// Unlike auto_local_to_global, this preserves IfcMapConversionScaled.FactorX/Y/Z -// in the rotation block, so they apply correctly to placement translations -// when composing per-model. -Eigen::Matrix4d helmert_meters_from_parameters(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. This is retained for UI / @@ -100,9 +61,4 @@ Eigen::Matrix4d helmert_meters_from_parameters(const HelmertTransformation& para // IfcMapConversion.Scale instead. std::optional get_map_unit(ifcopenshell::file* ifc_file); -// "How do I rotate project east to get to grid east?" — i.e. -atan2(xao, xaa) -// converted to degrees, anticlockwise positive. Mirrors -// ifcopenshell.util.geolocation.xaxis2angle. -double x_axis_to_angle_deg(double xaa, double xao); - #endif // GEOLOCATION_H diff --git a/src/helpers/geolocation_transform.cpp b/src/helpers/geolocation_transform.cpp new file mode 100644 index 0000000000..d885e5333a --- /dev/null +++ b/src/helpers/geolocation_transform.cpp @@ -0,0 +1,87 @@ +/******************************************************************************** + * * + * 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 "geolocation_transform.h" + +#include + +Eigen::Matrix4d local_to_global(const Eigen::Matrix4d& matrix, + const HelmertTransformation& p) { + const double theta = std::atan2(p.xao, p.xaa); + const double c = std::cos(theta); + const double s = std::sin(theta); + + Eigen::Matrix4d S = Eigen::Matrix4d::Identity(); + S(0, 0) = p.scale * p.factor_x; + S(1, 1) = p.scale * p.factor_y; + S(2, 2) = p.scale * p.factor_z; + + Eigen::Matrix4d R = Eigen::Matrix4d::Identity(); + R(0, 0) = c; + R(0, 1) = -s; + R(1, 0) = s; + R(1, 1) = c; + + Eigen::Matrix4d result = R * S * matrix; + // The scale was baked into the rotation+scale matrix so each axis column + // ended up scaled. Renormalise so the rotation part is pure orientation + // and the translation alone carries the scaled offsets. + for (int col = 0; col < 3; ++col) { + Eigen::Vector3d v = result.block<3, 1>(0, col); + const double n = v.norm(); + if (n > 0.0) { + result.block<3, 1>(0, col) = v / n; + } + } + result(0, 3) += p.e; + result(1, 3) += p.n; + result(2, 3) += p.h; + return result; +} + +Eigen::Matrix4d helmert_meters_from_parameters(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; +} + +double x_axis_to_angle_deg(double xaa, double xao) { + constexpr double PI = 3.14159265358979323846; + return -std::atan2(xao, xaa) * (180.0 / PI); +} diff --git a/src/helpers/geolocation_transform.h b/src/helpers/geolocation_transform.h new file mode 100644 index 0000000000..31b48c65b3 --- /dev/null +++ b/src/helpers/geolocation_transform.h @@ -0,0 +1,80 @@ +/******************************************************************************** + * * + * 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 . * + * * + ********************************************************************************/ + +// Georeferencing matrix math over plain numbers: no IFC file, no schema +// dispatch, no Qt. +// +// Split out of geolocation.h/geolocation.cpp for the same reason as +// unit_convert: those pull in ../ifcparse/express.h to read an +// IfcMapConversion out of a model, but IfcViewerCore — and through it the +// Emscripten build — needs to compose the resulting matrices without +// linking IfcParse. Reading the parameters out of an IFC stays in +// geolocation.h; turning them into matrices lives here. + +#ifndef GEOLOCATION_TRANSFORM_H +#define GEOLOCATION_TRANSFORM_H + +#include + +struct HelmertTransformation { + double e = 0.0; // eastings offset + double n = 0.0; // northings offset + double h = 0.0; // orthogonal-height offset + double xaa = 1.0; // X-axis abscissa (cos of grid-rotation angle) + double xao = 0.0; // X-axis ordinate (sin of grid-rotation angle) + double scale = 1.0; // unit scale (project unit -> map unit) + double factor_x = 1.0; // combined scale factor along X + double factor_y = 1.0; // combined scale factor along Y + double factor_z = 1.0; // combined scale factor along Z +}; + +// Apply a Helmert transformation to a 4x4 local matrix. +Eigen::Matrix4d local_to_global(const Eigen::Matrix4d& matrix, + const HelmertTransformation& params); + +// Build the Helmert transformation as a meter-input / meter-output 4x4 matrix +// directly from parsed parameters, bypassing auto_local_to_global'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 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 = helmert_meters_from_parameters(...) · inv(wcs_meters) +// (where wcs_meters has its translation column converted from project units +// to meters via calculate_unit_scale). +// +// Unlike auto_local_to_global, this preserves IfcMapConversionScaled.FactorX/Y/Z +// in the rotation block, so they apply correctly to placement translations +// when composing per-model. +Eigen::Matrix4d helmert_meters_from_parameters(const HelmertTransformation& params, + double map_unit_to_meters); + +// "How do I rotate project east to get to grid east?" — i.e. -atan2(xao, xaa) +// converted to degrees, anticlockwise positive. Mirrors +// ifcopenshell.util.geolocation.xaxis2angle. +double x_axis_to_angle_deg(double xaa, double xao); + +#endif // GEOLOCATION_TRANSFORM_H diff --git a/src/helpers/unit.cpp b/src/helpers/unit.cpp index 3066923215..8764c98358 100644 --- a/src/helpers/unit.cpp +++ b/src/helpers/unit.cpp @@ -18,6 +18,7 @@ ********************************************************************************/ #include "unit.h" +#include "unit_convert.h" #include "../ifcparse/exception.h" #include "../ifcparse/file.h" @@ -27,187 +28,8 @@ #include #include -const std::unordered_map SI_PREFIXES = { - {"EXA", 1e18}, - {"PETA", 1e15}, - {"TERA", 1e12}, - {"GIGA", 1e9}, - {"MEGA", 1e6}, - {"KILO", 1e3}, - {"HECTO", 1e2}, - {"DECA", 1e1}, - {"DECI", 1e-1}, - {"CENTI", 1e-2}, - {"MILLI", 1e-3}, - {"MICRO", 1e-6}, - {"NANO", 1e-9}, - {"PICO", 1e-12}, - {"FEMTO", 1e-15}, - {"ATTO", 1e-18}, -}; - -const std::unordered_map SI_PREFIX_SYMBOLS = { - {"EXA", "E"}, - {"PETA", "P"}, - {"TERA", "T"}, - {"GIGA", "G"}, - {"MEGA", "M"}, - {"KILO", "k"}, - {"HECTO", "h"}, - {"DECA", "da"}, - {"DECI", "d"}, - {"CENTI", "c"}, - {"MILLI", "m"}, - {"MICRO", "\xCE\xBC"}, // μ (UTF-8) - {"NANO", "n"}, - {"PICO", "p"}, - {"FEMTO", "f"}, - {"ATTO", "a"}, -}; - -const std::unordered_map SI_CONVERSIONS = { - {"thou", 0.0000254}, - {"inch", 0.0254}, - {"foot", 0.3048}, - {"yard", 0.914}, - {"mile", 1609.0}, - {"square thou", 6.4516e-10}, - {"square inch", 0.0006452}, - {"square foot", 0.09290304}, - {"square yard", 0.83612736}, - {"acre", 4046.86}, - {"square mile", 2588881.0}, - {"cubic thou", 1.6387064e-14}, - {"cubic inch", 0.00001639}, - {"cubic foot", 0.02831684671168849}, - {"cubic yard", 0.7636}, - {"cubic mile", 4165509529.0}, - {"litre", 0.001}, - {"fluid ounce uk", 0.0000284130625}, - {"fluid ounce us", 0.00002957353}, - {"pint uk", 0.000568}, - {"pint us", 0.000473}, - {"gallon uk", 0.004546}, - {"gallon us", 0.003785}, - {"degree", 0.0174532925199433}, // pi / 180 - {"ounce", 0.02835}, - {"pound", 0.454}, - {"ton uk", 1016.0469088}, - {"ton us", 907.18474}, - {"tonne", 1000.0}, - {"lbf", 4.4482216153}, - {"kip", 4448.2216153}, - {"psi", 6894.7572932}, - {"ksi", 6894757.2932}, - {"minute", 60.0}, - {"hour", 3600.0}, - {"day", 86400.0}, - {"btu", 1055.056}, - {"fahrenheit", 1.8}, -}; - -const std::unordered_map IMPERIAL_TYPES = { - {"thou", "LENGTHUNIT"}, - {"inch", "LENGTHUNIT"}, - {"foot", "LENGTHUNIT"}, - {"yard", "LENGTHUNIT"}, - {"mile", "LENGTHUNIT"}, - {"square thou", "AREAUNIT"}, - {"square inch", "AREAUNIT"}, - {"square foot", "AREAUNIT"}, - {"square yard", "AREAUNIT"}, - {"acre", "AREAUNIT"}, - {"square mile", "AREAUNIT"}, - {"cubic thou", "VOLUMEUNIT"}, - {"cubic inch", "VOLUMEUNIT"}, - {"cubic foot", "VOLUMEUNIT"}, - {"cubic yard", "VOLUMEUNIT"}, - {"cubic mile", "VOLUMEUNIT"}, - {"litre", "VOLUMEUNIT"}, - {"fluid ounce uk", "VOLUMEUNIT"}, - {"fluid ounce us", "VOLUMEUNIT"}, - {"pint uk", "VOLUMEUNIT"}, - {"pint us", "VOLUMEUNIT"}, - {"gallon uk", "VOLUMEUNIT"}, - {"gallon us", "VOLUMEUNIT"}, - {"degree", "PLANEANGLEUNIT"}, - {"ounce", "MASSUNIT"}, - {"pound", "MASSUNIT"}, - {"ton uk", "MASSUNIT"}, - {"ton us", "MASSUNIT"}, - {"tonne", "MASSUNIT"}, - {"lbf", "FORCEUNIT"}, - {"kip", "FORCEUNIT"}, - {"psi", "PRESSUREUNIT"}, - {"ksi", "PRESSUREUNIT"}, - {"minute", "TIMEUNIT"}, - {"hour", "TIMEUNIT"}, - {"day", "TIMEUNIT"}, - {"btu", "ENERGYUNIT"}, - {"fahrenheit", "THERMODYNAMICTEMPERATUREUNIT"}, -}; - -const std::unordered_map UNIT_SYMBOLS = { - // SI base / derived - {"CUBIC_METRE", "m3"}, - {"GRAM", "g"}, - {"SECOND", "s"}, - {"SQUARE_METRE", "m2"}, - {"METRE", "m"}, - {"NEWTON", "N"}, - {"PASCAL", "Pa"}, - // Conversion-based - {"pound-force", "lbf"}, - {"pound-force per square inch", "psi"}, - {"thou", "th"}, - {"inch", "in"}, - {"foot", "ft"}, - {"yard", "yd"}, - {"mile", "mi"}, - {"square thou", "th2"}, - {"square inch", "in2"}, - {"square foot", "ft2"}, - {"square yard", "yd2"}, - {"acre", "ac"}, - {"square mile", "mi2"}, - {"cubic thou", "th3"}, - {"cubic inch", "in3"}, - {"cubic foot", "ft3"}, - {"cubic yard", "yd3"}, - {"cubic mile", "mi3"}, - {"litre", "L"}, - {"fluid ounce uk", "fl oz"}, - {"fluid ounce us", "fl oz"}, - {"pint uk", "pt"}, - {"pint us", "pt"}, - {"gallon uk", "gal"}, - {"gallon us", "gal"}, - {"degree", "\xC2\xB0"}, // ° - {"ounce", "oz"}, - {"pound", "lb"}, - {"ton uk", "ton"}, - {"ton us", "ton"}, - {"tonne", "t"}, - {"lbf", "lbf"}, - {"kip", "kip"}, - {"psi", "psi"}, - {"ksi", "ksi"}, - {"minute", "min"}, - {"hour", "hr"}, - {"day", "day"}, - {"btu", "btu"}, - {"fahrenheit", "\xC2\xB0\x46"}, // °F -}; - namespace { -std::string to_lower(const std::string& s) { - std::string r; - r.resize(s.size()); - std::transform(s.begin(), s.end(), r.begin(), [](unsigned char c) { return std::tolower(c); }); - return r; -} - [[noreturn]] void unsupported_schema(const std::string& name) { throw ifcopenshell::exception("No helper implementation was built for schema " + name); } @@ -344,14 +166,6 @@ double convert_unit_s(double value, const express::base& from_unit, const expres } // namespace -double get_prefix_multiplier(const std::string& prefix) { - if (prefix.empty()) { - return 1.0; - } - auto it = SI_PREFIXES.find(prefix); - return (it == SI_PREFIXES.end()) ? 1.0 : it->second; -} - std::optional si_scale_from_named_unit(express::base unit) { if (!unit) { return std::nullopt; @@ -397,42 +211,6 @@ double calculate_unit_scale(ifcopenshell::file* ifc_file, unsupported_schema(name); } -double convert(double value, - const std::string& from_prefix, - const std::string& from_unit, - const std::string& to_prefix, - const std::string& to_unit) { - const std::string fl = to_lower(from_unit); - const std::string tl = to_lower(to_unit); - - if (auto it = SI_CONVERSIONS.find(fl); it != SI_CONVERSIONS.end()) { - value *= it->second; - } else if (!from_prefix.empty()) { - value *= get_prefix_multiplier(from_prefix); - if (from_unit.find("SQUARE") != std::string::npos) { - value *= get_prefix_multiplier(from_prefix); - } else if (from_unit.find("CUBIC") != std::string::npos) { - value *= get_prefix_multiplier(from_prefix); - value *= get_prefix_multiplier(from_prefix); - } - } - - if (auto it = SI_CONVERSIONS.find(tl); it != SI_CONVERSIONS.end()) { - return value * (1.0 / it->second); - } else if (!to_prefix.empty()) { - value *= 1.0 / get_prefix_multiplier(to_prefix); - // NB: python ifcopenshell.util.unit.convert checks `from_unit` (not - // `to_unit`) here. Mirrored for parity — from_unit and to_unit are - // always the same dimension in valid calls, so behaviour is the same. - if (from_unit.find("SQUARE") != std::string::npos) { - value *= 1.0 / get_prefix_multiplier(to_prefix); - } else if (from_unit.find("CUBIC") != std::string::npos) { - value *= 1.0 / get_prefix_multiplier(to_prefix); - value *= 1.0 / get_prefix_multiplier(to_prefix); - } - } - return value; -} double convert_unit(double value, express::base from_unit, express::base to_unit) { if (!from_unit || !to_unit) { diff --git a/src/helpers/unit.h b/src/helpers/unit.h index c8ca040088..280e4f07cd 100644 --- a/src/helpers/unit.h +++ b/src/helpers/unit.h @@ -24,6 +24,7 @@ #define UNIT_H #include "../ifcparse/express.h" +#include "unit_convert.h" #include #include @@ -33,27 +34,6 @@ namespace ifcopenshell { class file; } -// SI prefix multipliers, e.g. "MILLI" -> 1e-3. Empty key not present; -// callers should pass an empty prefix string for "no prefix". -extern const std::unordered_map SI_PREFIXES; - -// SI prefix display symbols, e.g. "MILLI" -> "m". -extern const std::unordered_map SI_PREFIX_SYMBOLS; - -// Conversion-based unit name (lowercase, IFC convention) -> SI base scale. -// e.g. "foot" -> 0.3048, "square foot" -> 0.09290304. -extern const std::unordered_map SI_CONVERSIONS; - -// Conversion-based unit name -> IFC unit type, e.g. "foot" -> "LENGTHUNIT". -extern const std::unordered_map IMPERIAL_TYPES; - -// Display symbol per unit name. Covers IfcSIUnit names ("METRE" -> "m") and -// IfcConversionBasedUnit names ("foot" -> "ft"). -extern const std::unordered_map UNIT_SYMBOLS; - -// Returns the multiplier for an SI prefix. Empty string returns 1.0. -double get_prefix_multiplier(const std::string& prefix); - // Returns the SI scale for an IfcNamedUnit such that // value_in_unit * scale == value_in_si_base // Walks IfcConversionBasedUnit chains down to IfcSIUnit. Returns nullopt @@ -74,15 +54,6 @@ std::optional get_project_unit(ifcopenshell::file* ifc_file, double calculate_unit_scale(ifcopenshell::file* ifc_file, const std::string& unit_type = "LENGTHUNIT"); -// Convert between two units identified by name + optional SI prefix. -// SQUARE_/CUBIC_ prefixed SI names get the prefix multiplier squared/cubed -// (matches python ifcopenshell.util.unit.convert). -double convert(double value, - const std::string& from_prefix, - const std::string& from_unit, - const std::string& to_prefix, - const std::string& to_unit); - // Convert between two IfcNamedUnit entities. Pulls Name and Prefix off each // and delegates to convert(). IfcConversionBasedUnit names that don't appear // in SI_CONVERSIONS return the value unchanged. diff --git a/src/helpers/unit_convert.cpp b/src/helpers/unit_convert.cpp new file mode 100644 index 0000000000..d347a3e2c6 --- /dev/null +++ b/src/helpers/unit_convert.cpp @@ -0,0 +1,247 @@ +/******************************************************************************** + * * + * 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 "unit_convert.h" + +#include +#include + +const std::unordered_map SI_PREFIXES = { + {"EXA", 1e18}, + {"PETA", 1e15}, + {"TERA", 1e12}, + {"GIGA", 1e9}, + {"MEGA", 1e6}, + {"KILO", 1e3}, + {"HECTO", 1e2}, + {"DECA", 1e1}, + {"DECI", 1e-1}, + {"CENTI", 1e-2}, + {"MILLI", 1e-3}, + {"MICRO", 1e-6}, + {"NANO", 1e-9}, + {"PICO", 1e-12}, + {"FEMTO", 1e-15}, + {"ATTO", 1e-18}, +}; + +const std::unordered_map SI_PREFIX_SYMBOLS = { + {"EXA", "E"}, + {"PETA", "P"}, + {"TERA", "T"}, + {"GIGA", "G"}, + {"MEGA", "M"}, + {"KILO", "k"}, + {"HECTO", "h"}, + {"DECA", "da"}, + {"DECI", "d"}, + {"CENTI", "c"}, + {"MILLI", "m"}, + {"MICRO", "\xCE\xBC"}, // μ (UTF-8) + {"NANO", "n"}, + {"PICO", "p"}, + {"FEMTO", "f"}, + {"ATTO", "a"}, +}; + +const std::unordered_map SI_CONVERSIONS = { + {"thou", 0.0000254}, + {"inch", 0.0254}, + {"foot", 0.3048}, + {"yard", 0.914}, + {"mile", 1609.0}, + {"square thou", 6.4516e-10}, + {"square inch", 0.0006452}, + {"square foot", 0.09290304}, + {"square yard", 0.83612736}, + {"acre", 4046.86}, + {"square mile", 2588881.0}, + {"cubic thou", 1.6387064e-14}, + {"cubic inch", 0.00001639}, + {"cubic foot", 0.02831684671168849}, + {"cubic yard", 0.7636}, + {"cubic mile", 4165509529.0}, + {"litre", 0.001}, + {"fluid ounce uk", 0.0000284130625}, + {"fluid ounce us", 0.00002957353}, + {"pint uk", 0.000568}, + {"pint us", 0.000473}, + {"gallon uk", 0.004546}, + {"gallon us", 0.003785}, + {"degree", 0.0174532925199433}, // pi / 180 + {"ounce", 0.02835}, + {"pound", 0.454}, + {"ton uk", 1016.0469088}, + {"ton us", 907.18474}, + {"tonne", 1000.0}, + {"lbf", 4.4482216153}, + {"kip", 4448.2216153}, + {"psi", 6894.7572932}, + {"ksi", 6894757.2932}, + {"minute", 60.0}, + {"hour", 3600.0}, + {"day", 86400.0}, + {"btu", 1055.056}, + {"fahrenheit", 1.8}, +}; + +const std::unordered_map IMPERIAL_TYPES = { + {"thou", "LENGTHUNIT"}, + {"inch", "LENGTHUNIT"}, + {"foot", "LENGTHUNIT"}, + {"yard", "LENGTHUNIT"}, + {"mile", "LENGTHUNIT"}, + {"square thou", "AREAUNIT"}, + {"square inch", "AREAUNIT"}, + {"square foot", "AREAUNIT"}, + {"square yard", "AREAUNIT"}, + {"acre", "AREAUNIT"}, + {"square mile", "AREAUNIT"}, + {"cubic thou", "VOLUMEUNIT"}, + {"cubic inch", "VOLUMEUNIT"}, + {"cubic foot", "VOLUMEUNIT"}, + {"cubic yard", "VOLUMEUNIT"}, + {"cubic mile", "VOLUMEUNIT"}, + {"litre", "VOLUMEUNIT"}, + {"fluid ounce uk", "VOLUMEUNIT"}, + {"fluid ounce us", "VOLUMEUNIT"}, + {"pint uk", "VOLUMEUNIT"}, + {"pint us", "VOLUMEUNIT"}, + {"gallon uk", "VOLUMEUNIT"}, + {"gallon us", "VOLUMEUNIT"}, + {"degree", "PLANEANGLEUNIT"}, + {"ounce", "MASSUNIT"}, + {"pound", "MASSUNIT"}, + {"ton uk", "MASSUNIT"}, + {"ton us", "MASSUNIT"}, + {"tonne", "MASSUNIT"}, + {"lbf", "FORCEUNIT"}, + {"kip", "FORCEUNIT"}, + {"psi", "PRESSUREUNIT"}, + {"ksi", "PRESSUREUNIT"}, + {"minute", "TIMEUNIT"}, + {"hour", "TIMEUNIT"}, + {"day", "TIMEUNIT"}, + {"btu", "ENERGYUNIT"}, + {"fahrenheit", "THERMODYNAMICTEMPERATUREUNIT"}, +}; + +const std::unordered_map UNIT_SYMBOLS = { + // SI base / derived + {"CUBIC_METRE", "m3"}, + {"GRAM", "g"}, + {"SECOND", "s"}, + {"SQUARE_METRE", "m2"}, + {"METRE", "m"}, + {"NEWTON", "N"}, + {"PASCAL", "Pa"}, + // Conversion-based + {"pound-force", "lbf"}, + {"pound-force per square inch", "psi"}, + {"thou", "th"}, + {"inch", "in"}, + {"foot", "ft"}, + {"yard", "yd"}, + {"mile", "mi"}, + {"square thou", "th2"}, + {"square inch", "in2"}, + {"square foot", "ft2"}, + {"square yard", "yd2"}, + {"acre", "ac"}, + {"square mile", "mi2"}, + {"cubic thou", "th3"}, + {"cubic inch", "in3"}, + {"cubic foot", "ft3"}, + {"cubic yard", "yd3"}, + {"cubic mile", "mi3"}, + {"litre", "L"}, + {"fluid ounce uk", "fl oz"}, + {"fluid ounce us", "fl oz"}, + {"pint uk", "pt"}, + {"pint us", "pt"}, + {"gallon uk", "gal"}, + {"gallon us", "gal"}, + {"degree", "\xC2\xB0"}, // ° + {"ounce", "oz"}, + {"pound", "lb"}, + {"ton uk", "ton"}, + {"ton us", "ton"}, + {"tonne", "t"}, + {"lbf", "lbf"}, + {"kip", "kip"}, + {"psi", "psi"}, + {"ksi", "ksi"}, + {"minute", "min"}, + {"hour", "hr"}, + {"day", "day"}, + {"btu", "btu"}, + {"fahrenheit", "\xC2\xB0\x46"}, // °F +}; + +std::string to_lower(const std::string& s) { + std::string r; + r.resize(s.size()); + std::transform(s.begin(), s.end(), r.begin(), [](unsigned char c) { return std::tolower(c); }); + return r; +} + +double get_prefix_multiplier(const std::string& prefix) { + if (prefix.empty()) { + return 1.0; + } + auto it = SI_PREFIXES.find(prefix); + return (it == SI_PREFIXES.end()) ? 1.0 : it->second; +} + +double convert(double value, + const std::string& from_prefix, + const std::string& from_unit, + const std::string& to_prefix, + const std::string& to_unit) { + const std::string fl = to_lower(from_unit); + const std::string tl = to_lower(to_unit); + + if (auto it = SI_CONVERSIONS.find(fl); it != SI_CONVERSIONS.end()) { + value *= it->second; + } else if (!from_prefix.empty()) { + value *= get_prefix_multiplier(from_prefix); + if (from_unit.find("SQUARE") != std::string::npos) { + value *= get_prefix_multiplier(from_prefix); + } else if (from_unit.find("CUBIC") != std::string::npos) { + value *= get_prefix_multiplier(from_prefix); + value *= get_prefix_multiplier(from_prefix); + } + } + + if (auto it = SI_CONVERSIONS.find(tl); it != SI_CONVERSIONS.end()) { + return value * (1.0 / it->second); + } else if (!to_prefix.empty()) { + value *= 1.0 / get_prefix_multiplier(to_prefix); + // NB: python ifcopenshell.util.unit.convert checks `from_unit` (not + // `to_unit`) here. Mirrored for parity — from_unit and to_unit are + // always the same dimension in valid calls, so behaviour is the same. + if (from_unit.find("SQUARE") != std::string::npos) { + value *= 1.0 / get_prefix_multiplier(to_prefix); + } else if (from_unit.find("CUBIC") != std::string::npos) { + value *= 1.0 / get_prefix_multiplier(to_prefix); + value *= 1.0 / get_prefix_multiplier(to_prefix); + } + } + return value; +} diff --git a/src/helpers/unit_convert.h b/src/helpers/unit_convert.h new file mode 100644 index 0000000000..4fe3adb747 --- /dev/null +++ b/src/helpers/unit_convert.h @@ -0,0 +1,71 @@ +/******************************************************************************** + * * + * 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 . * + * * + ********************************************************************************/ + +// Unit conversion that needs no IFC file: the ported lookup tables from +// src/ifcopenshell-python/ifcopenshell/util/unit.py plus the two pure +// functions over them. +// +// Split out of unit.h/unit.cpp because those pull in ../ifcparse/express.h for +// the entity-walking helpers (calculate_unit_scale, si_scale_from_named_unit, +// convert_unit). IfcViewerCore — and through it the Emscripten build — needs +// convert() to resolve a federation unit name to metres, but links neither +// IfcParse nor Qt. Keeping one copy of the tables here rather than a second +// one in the web layer is the whole point of the split. + +#ifndef UNIT_CONVERT_H +#define UNIT_CONVERT_H + +#include +#include + +// SI prefix multipliers, e.g. "MILLI" -> 1e-3. Empty key not present; +// callers should pass an empty prefix string for "no prefix". +extern const std::unordered_map SI_PREFIXES; + +// SI prefix display symbols, e.g. "MILLI" -> "m". +extern const std::unordered_map SI_PREFIX_SYMBOLS; + +// Conversion-based unit name (lowercase, IFC convention) -> SI base scale. +// e.g. "foot" -> 0.3048, "square foot" -> 0.09290304. +extern const std::unordered_map SI_CONVERSIONS; + +// Conversion-based unit name -> IFC unit type, e.g. "foot" -> "LENGTHUNIT". +extern const std::unordered_map IMPERIAL_TYPES; + +// Display symbol per unit name. Covers IfcSIUnit names ("METRE" -> "m") and +// IfcConversionBasedUnit names ("foot" -> "ft"). +extern const std::unordered_map UNIT_SYMBOLS; + +// Lowercase an IFC unit name so it can be looked up in the tables above, +// which are keyed by the lowercase IFC convention ("foot", "square foot"). +std::string to_lower(const std::string& s); + +// Returns the multiplier for an SI prefix. Empty string returns 1.0. +double get_prefix_multiplier(const std::string& prefix); + +// Convert between two units identified by name + optional SI prefix. +// SQUARE_/CUBIC_ prefixed SI names get the prefix multiplier squared/cubed +// (matches python ifcopenshell.util.unit.convert). +double convert(double value, + const std::string& from_prefix, + const std::string& from_unit, + const std::string& to_prefix, + const std::string& to_unit); + +#endif // UNIT_CONVERT_H