Split the schema-free half of the unit and geolocation helpers out

unit.h and geolocation.h both include ../ifcparse/express.h for the entity
walking they do, which puts the whole module out of reach of anything that
cannot link IfcParse. Most of what a viewer wants from them needs no IFC at
all: the unit conversion tables, and the Helmert parameters-to-matrix math.

Move those into unit_convert and geolocation_transform, and build them as a
new helpers_math target that `helpers` re-exports PUBLIC, so existing callers
keep working through the unchanged unit.h / geolocation.h includes. The new
target has no IfcParse or Qt dependency and so builds under Emscripten, where
the rest of this directory cannot.

One target rather than compiling the sources into each consumer: the glob in
this directory would otherwise put them in libhelpers.a as well, leaving two
copies of the same objects in any link that pulls both.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
Dion Moult
2026-08-11 16:00:29 +10:00
parent 79bd3563de
commit e0b226f4ca
9 changed files with 516 additions and 363 deletions
+1 -223
View File
@@ -18,6 +18,7 @@
********************************************************************************/
#include "unit.h"
#include "unit_convert.h"
#include "../ifcparse/exception.h"
#include "../ifcparse/file.h"
@@ -27,187 +28,8 @@
#include <algorithm>
#include <cctype>
const std::unordered_map<std::string, double> 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<std::string, std::string> 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<std::string, double> 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<std::string, std::string> 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<std::string, std::string> 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<double> 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) {