refactor: extract src/ifcutil/ from src/ifcviewer/ (Unit, Geolocation, Placement)

Unit / Geolocation / Placement are schema-agnostic IFC helpers ported
from ifcopenshell.util.{unit,geolocation,placement}. Nothing about
them is viewer-specific: pure IfcParse + Eigen, no Qt, no IfcGeom, no
renderer. Living under src/ifcviewer/ implies an unwanted dependency
direction every time a non-viewer caller (test_federation, the bonsai
SettingsView georef readout, a future standalone IFC tool) wants to
use them.

Move them to a new `src/ifcutil/` static lib (IfcUtil). The lib has
PUBLIC `target_include_directories(${CMAKE_CURRENT_SOURCE_DIR})` so
callers that link IfcUtil can keep `#include "Unit.h"` etc. without
relative-path adjustments — the include dir propagates transitively
via IfcViewer's PUBLIC link.

## Changes

* `git mv src/ifcviewer/{Geolocation,Placement,Unit}.{h,cpp}
   → src/ifcutil/` (history follows the rename).
* `src/ifcutil/CMakeLists.txt`: IfcUtil static lib, PUBLIC links
  IfcParse + Eigen3::Eigen, PUBLIC include dir.
* `cmake/CMakeLists.txt`: `add_subdirectory(../src/ifcutil ifcutil)`
  before ifcviewer/ so the link target exists when IfcViewer's
  CMakeLists runs.
* `src/ifcviewer/CMakeLists.txt`: IfcUtil added to IfcViewer's PUBLIC
  link_libraries.
* `src/ifcviewer/tests/CMakeLists.txt`: test_federation drops the
  explicit `${IFCVIEWER_SRC}/{Unit,Geolocation,Placement}.cpp`
  source list and links `IfcUtil` instead (matches how production
  code resolves the symbols).
* `src/bonsaiviewer/modules/models/SettingsView.cpp`: the two
  explicit `#include "../../../ifcviewer/{Geolocation,Unit}.h"`
  paths swap to `../../../ifcutil/…`. All other callers use bare
  `#include "Unit.h"` style and continue to work via the propagated
  include dir.

## Verification

* `ninja -C build-viewer` builds clean: IfcUtil + IfcViewer +
  IfcViewerMinimal + BonsaiViewer + all four pre-existing
  ifcviewer tests + the two from-wgpu tests.
* `test_federation` runs green: 226 assertions in 22 test cases
  pass with IfcUtil linked instead of the explicit-source compile.
* `git log --follow` traces e.g. `Geolocation.cpp` back through the
  rename to its prior location in src/ifcviewer/.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
Dion Moult
2026-06-04 13:25:28 +10:00
parent 32d9fd6c1c
commit b3fbcd6a66
11 changed files with 74 additions and 12 deletions
+1
View File
@@ -140,6 +140,7 @@ target_include_directories(IfcViewer PUBLIC ${CMAKE_CURRENT_SOURCE_DIR})
add_dependencies(IfcViewer ${kernel_libraries} ${mapping_libraries})
target_link_libraries(IfcViewer PUBLIC
IfcUtil
IfcGeom
IfcParse
${OpenCASCADE_LIBRARIES}
-270
View File
@@ -1,270 +0,0 @@
/********************************************************************************
* *
* 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 <http://www.gnu.org/licenses/>. *
* *
********************************************************************************/
#include "Geolocation.h"
#include "Placement.h"
#include "../ifcparse/express.h"
#include "../ifcparse/file.h"
#include "../ifcparse/instance_data.h"
#include "../ifcparse/schema.h"
#include <cmath>
#include <string>
#include <vector>
namespace {
// Read a numeric NominalValue out of an IfcPropertySingleValue. IFC2X3
// ePSet_MapConversion stores eastings/northings/scale as IfcLengthMeasure or
// IfcReal wrapped inside IfcValue (a SELECT) — get_attribute_value(0) peels
// the wrapper. Returns nullopt if the value is missing or non-numeric.
std::optional<double> readPropertyValueDouble(const express::Base& property) {
if (!property.declaration().is("IfcPropertySingleValue")) return std::nullopt;
auto pe = property.as<express::Entity>();
auto nv = pe.get("NominalValue");
if (nv.isNull()) return std::nullopt;
express::Base wrapper = nv;
auto inner = wrapper.get_attribute_value(0);
if (inner.isNull()) return std::nullopt;
switch (inner.type()) {
case ifcopenshell::Argument_DOUBLE: return (double) inner;
case ifcopenshell::Argument_INT: return (double)(int) inner;
default: return std::nullopt;
}
}
} // namespace
std::optional<HelmertTransformation>
getHelmertTransformationParameters(ifcopenshell::file* ifc_file) {
HelmertTransformation p;
const std::string schema_name = ifc_file->schema()->name();
if (schema_name == "IFC2X3") {
auto projects = ifc_file->instances_by_type("IfcProject");
if (projects.empty()) return std::nullopt;
const auto& project = projects[0];
bool found = false;
auto rels = project.as<express::Entity>().get_inverse("IsDefinedBy");
for (const auto& rel : rels) {
if (!rel.declaration().is("IfcRelDefinesByProperties")) continue;
express::Base pset_base = rel.get("RelatingPropertyDefinition");
if (!pset_base.declaration().is("IfcPropertySet")) continue;
auto pset = pset_base.as<express::Entity>();
auto name_attr = pset.get("Name");
if (name_attr.isNull()) continue;
std::string pset_name = name_attr;
if (pset_name != "ePSet_MapConversion") continue;
std::vector<express::Base> props = pset.get("HasProperties");
for (const auto& prop : props) {
if (!prop.declaration().is("IfcPropertySingleValue")) continue;
auto pe = prop.as<express::Entity>();
auto pname_attr = pe.get("Name");
if (pname_attr.isNull()) continue;
std::string pname = pname_attr;
auto value = readPropertyValueDouble(prop);
if (!value) continue;
if (pname == "Eastings") p.e = *value;
else if (pname == "Northings") p.n = *value;
else if (pname == "OrthogonalHeight") p.h = *value;
else if (pname == "XAxisAbscissa") p.xaa = *value;
else if (pname == "XAxisOrdinate") p.xao = *value;
else if (pname == "Scale") p.scale = *value;
}
found = true;
break;
}
if (!found) return std::nullopt;
// Python: `conversion.get("Scale", None) or 1` — 0 falls back to 1.
if (p.scale == 0.0) p.scale = 1.0;
p.factor_x = p.factor_y = p.factor_z = 1.0;
} else {
std::vector<express::Base> conversions;
try {
conversions = ifc_file->instances_by_type("IfcCoordinateOperation");
} catch (...) {
// Schema doesn't know IfcCoordinateOperation.
return std::nullopt;
}
if (conversions.empty()) return std::nullopt;
const auto& conversion = conversions[0];
auto entity = conversion.as<express::Entity>();
const std::string type_name = conversion.declaration().name();
auto get_or = [&](const std::string& name, double fallback) {
auto a = entity.get(name);
return a.isNull() ? fallback : (double) a;
};
if (conversion.declaration().is("IfcMapConversion")) {
p.e = get_or("Eastings", 0.0);
p.n = get_or("Northings", 0.0);
p.h = get_or("OrthogonalHeight", 0.0);
p.xaa = get_or("XAxisAbscissa", 0.0);
p.xao = get_or("XAxisOrdinate", 0.0);
p.scale = get_or("Scale", 1.0);
if (p.scale == 0.0) p.scale = 1.0;
if (type_name == "IfcMapConversionScaled") {
p.factor_x = entity.get("FactorX");
p.factor_y = entity.get("FactorY");
p.factor_z = entity.get("FactorZ");
} else {
p.factor_x = p.factor_y = p.factor_z = 1.0;
}
} else if (type_name == "IfcRigidOperation") {
// FirstCoordinate / SecondCoordinate are IfcLengthMeasure-typed
// values; the C++ binding auto-unwraps defined types of REAL.
p.e = get_or("FirstCoordinate", 0.0);
p.n = get_or("SecondCoordinate", 0.0);
p.h = get_or("Height", 0.0);
p.xaa = 1.0;
p.xao = 0.0;
p.scale = p.factor_x = p.factor_y = p.factor_z = 1.0;
} else {
return std::nullopt;
}
}
if (p.xaa == 0.0 && p.xao == 0.0) {
p.xaa = 1.0;
p.xao = 0.0;
}
return p;
}
std::optional<Eigen::Matrix4d> getWcs(ifcopenshell::file* ifc_file) {
auto contexts = ifc_file->instances_by_type_excl_subtypes(
"IfcGeometricRepresentationContext");
express::Base wcs;
bool found = false;
for (const auto& ctx : contexts) {
auto entity = ctx.as<express::Entity>();
auto wcs_attr = entity.get("WorldCoordinateSystem");
if (wcs_attr.isNull()) continue;
wcs = (express::Base) wcs_attr;
found = true;
auto ctype_attr = entity.get("ContextType");
if (!ctype_attr.isNull()) {
std::string ctype = ctype_attr;
if (ctype == "Model") break;
}
}
if (!found) return std::nullopt;
const auto& decl = wcs.declaration();
if (!(decl.is("IfcAxis2Placement3D") || decl.is("IfcAxis2PlacementLinear"))) {
return std::nullopt;
}
return getAxis2Placement(wcs);
}
Eigen::Matrix4d local2global(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 autoLocal2Global(ifcopenshell::file* ifc_file,
const Eigen::Matrix4d& matrix,
bool should_return_in_map_units) {
auto params = getHelmertTransformationParameters(ifc_file);
if (!params) return matrix;
Eigen::Matrix4d m = matrix;
if (auto wcs = getWcs(ifc_file)) {
m = wcs->inverse() * m;
}
Eigen::Matrix4d result = local2global(m, *params);
if (!should_return_in_map_units) {
result(0, 3) /= params->scale;
result(1, 3) /= params->scale;
result(2, 3) /= params->scale;
}
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;
}
double xaxis2angleDeg(double xaa, double xao) {
constexpr double kPi = 3.14159265358979323846;
return -std::atan2(xao, xaa) * (180.0 / kPi);
}
-111
View File
@@ -1,111 +0,0 @@
/********************************************************************************
* *
* 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 <http://www.gnu.org/licenses/>. *
* *
********************************************************************************/
// Port of selected helpers from
// src/ifcopenshell-python/ifcopenshell/util/geolocation.py — primarily
// auto_local2global, which builds a 4x4 matrix that lifts an element's local
// transform into the model's global (georeferenced) frame. The python utils
// are expected to be ported to C++ in their own module later; this file is
// the temporary home until that lands.
#ifndef GEOLOCATION_H
#define GEOLOCATION_H
#include "../ifcparse/express.h"
#include <Eigen/Dense>
#include <optional>
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
// when the model has no map conversion.
std::optional<HelmertTransformation>
getHelmertTransformationParameters(ifcopenshell::file* ifc_file);
// Read the IfcGeometricRepresentationContext.WorldCoordinateSystem (preferring
// the "Model" context) as a 4x4 matrix. Returns nullopt when the model has
// no parseable WCS.
std::optional<Eigen::Matrix4d> getWcs(ifcopenshell::file* ifc_file);
// Apply a Helmert transformation to a 4x4 local matrix.
Eigen::Matrix4d local2global(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
// translation column is divided by the map scale so the result is expressed
// in project length units.
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 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).
//
// 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. This is retained for UI /
// metadata inspection; transform composition derives map unit scale from
// IfcMapConversion.Scale instead.
std::optional<express::Base> getMapUnit(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 xaxis2angleDeg(double xaa, double xao);
#endif // GEOLOCATION_H
-144
View File
@@ -1,144 +0,0 @@
/********************************************************************************
* *
* 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 <http://www.gnu.org/licenses/>. *
* *
********************************************************************************/
#include "Placement.h"
#include "../ifcparse/instance_data.h"
#include "../ifcparse/schema.h"
#include <vector>
namespace {
Eigen::Vector3d safeNormalize(const Eigen::Vector3d& v,
const Eigen::Vector3d& fallback) {
const double n = v.norm();
return (n > 0.0) ? Eigen::Vector3d(v / n) : fallback;
}
std::vector<double> readDirectionRatios(const express::Base& dir) {
if (!dir) return {};
auto attr = dir.as<express::Entity>().get("DirectionRatios");
if (attr.isNull()) return {};
return attr;
}
} // namespace
Eigen::Matrix4d a2p(const Eigen::Vector3d& origin,
const Eigen::Vector3d& z,
const Eigen::Vector3d& x) {
const Eigen::Vector3d xn = safeNormalize(x, Eigen::Vector3d::UnitX());
const Eigen::Vector3d zn = safeNormalize(z, Eigen::Vector3d::UnitZ());
const Eigen::Vector3d yn = safeNormalize(zn.cross(xn), Eigen::Vector3d::UnitY());
Eigen::Matrix4d m = Eigen::Matrix4d::Identity();
m.block<3, 1>(0, 0) = xn;
m.block<3, 1>(0, 1) = yn;
m.block<3, 1>(0, 2) = zn;
m.block<3, 1>(0, 3) = origin;
return m;
}
Eigen::Matrix4d getAxis2Placement(const express::Base& placement) {
if (!placement) return Eigen::Matrix4d::Identity();
const auto& decl = placement.declaration();
auto entity = placement.as<express::Entity>();
Eigen::Vector3d z(0.0, 0.0, 1.0);
Eigen::Vector3d x(1.0, 0.0, 0.0);
Eigen::Vector3d o(0.0, 0.0, 0.0);
if (decl.is("IfcAxis2Placement3D") || decl.is("IfcAxis2PlacementLinear")) {
auto axis_attr = entity.get("Axis");
if (!axis_attr.isNull()) {
auto dr = readDirectionRatios((express::Base) axis_attr);
if (dr.size() >= 3) z = Eigen::Vector3d(dr[0], dr[1], dr[2]);
}
auto refdir_attr = entity.get("RefDirection");
if (!refdir_attr.isNull()) {
auto dr = readDirectionRatios((express::Base) refdir_attr);
if (dr.size() >= 3) x = Eigen::Vector3d(dr[0], dr[1], dr[2]);
}
auto loc_attr = entity.get("Location");
if (loc_attr.isNull()) return Eigen::Matrix4d::Identity();
express::Base location = loc_attr;
auto coords_attr = location.as<express::Entity>().get("Coordinates");
if (coords_attr.isNull()) return Eigen::Matrix4d::Identity();
std::vector<double> coords = coords_attr;
if (coords.size() >= 3) o = Eigen::Vector3d(coords[0], coords[1], coords[2]);
} else if (decl.is("IfcAxis2Placement2D")) {
auto refdir_attr = entity.get("RefDirection");
if (!refdir_attr.isNull()) {
auto dr = readDirectionRatios((express::Base) refdir_attr);
if (dr.size() >= 1) {
x = Eigen::Vector3d(dr.size() > 0 ? dr[0] : 1.0,
dr.size() > 1 ? dr[1] : 0.0,
0.0);
}
}
auto loc_attr = entity.get("Location");
if (loc_attr.isNull()) return Eigen::Matrix4d::Identity();
express::Base location = loc_attr;
auto coords_attr = location.as<express::Entity>().get("Coordinates");
if (coords_attr.isNull()) return Eigen::Matrix4d::Identity();
std::vector<double> coords = coords_attr;
if (coords.size() >= 2) {
o = Eigen::Vector3d(coords[0], coords[1],
coords.size() >= 3 ? coords[2] : 0.0);
}
} else if (decl.is("IfcAxis1Placement")) {
auto axis_attr = entity.get("Axis");
if (!axis_attr.isNull()) {
auto dr = readDirectionRatios((express::Base) axis_attr);
if (dr.size() >= 3) z = Eigen::Vector3d(dr[0], dr[1], dr[2]);
}
auto loc_attr = entity.get("Location");
if (loc_attr.isNull()) return Eigen::Matrix4d::Identity();
express::Base location = loc_attr;
auto coords_attr = location.as<express::Entity>().get("Coordinates");
if (coords_attr.isNull()) return Eigen::Matrix4d::Identity();
std::vector<double> coords = coords_attr;
if (coords.size() >= 3) o = Eigen::Vector3d(coords[0], coords[1], coords[2]);
} else {
return Eigen::Matrix4d::Identity();
}
return a2p(o, z, x);
}
Eigen::Matrix4d getLocalPlacement(const express::Base& placement) {
if (!placement) return Eigen::Matrix4d::Identity();
const auto& decl = placement.declaration();
if (decl.is("IfcLocalPlacement")) {
auto entity = placement.as<express::Entity>();
Eigen::Matrix4d parent = Eigen::Matrix4d::Identity();
auto rel_attr = entity.get("PlacementRelTo");
if (!rel_attr.isNull()) {
parent = getLocalPlacement((express::Base) rel_attr);
}
auto rp_attr = entity.get("RelativePlacement");
if (rp_attr.isNull()) return parent;
return parent * getAxis2Placement((express::Base) rp_attr);
}
// IfcAxis2Placement* / IfcAxis1Placement passed in directly.
return getAxis2Placement(placement);
}
-50
View File
@@ -1,50 +0,0 @@
/********************************************************************************
* *
* 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 <http://www.gnu.org/licenses/>. *
* *
********************************************************************************/
// Port of selected helpers from
// src/ifcopenshell-python/ifcopenshell/util/placement.py — entity-instance
// placement chains -> 4x4 matrices, used by callers that need to read
// IfcLocalPlacement / IfcAxis2Placement* outside of the geometry kernel.
#ifndef PLACEMENT_H
#define PLACEMENT_H
#include "../ifcparse/express.h"
#include <Eigen/Dense>
// Build a 4x4 placement matrix from an origin + Z + X axis triple, mirroring
// ifcopenshell.util.placement.a2p. The Y axis is derived as Z × X. Inputs
// don't need to be unit; vectors are renormalised internally.
Eigen::Matrix4d a2p(const Eigen::Vector3d& origin,
const Eigen::Vector3d& z,
const Eigen::Vector3d& x);
// IfcAxis2Placement{2D,3D,Linear} / IfcAxis1Placement -> 4x4 matrix. Mirrors
// ifcopenshell.util.placement.get_axis2placement. Returns identity for null
// or unparseable inputs. Translation is in the IFC's project length unit.
Eigen::Matrix4d getAxis2Placement(const express::Base& placement);
// Resolve an IfcLocalPlacement (or an IfcAxis2Placement* directly) into a
// 4x4 matrix in the IFC project's length unit, walking the PlacementRelTo
// chain. Mirrors ifcopenshell.util.placement.get_local_placement. Returns
// identity for a null input.
Eigen::Matrix4d getLocalPlacement(const express::Base& placement);
#endif // PLACEMENT_H
-329
View File
@@ -1,329 +0,0 @@
/********************************************************************************
* *
* 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 <http://www.gnu.org/licenses/>. *
* *
********************************************************************************/
#include "Unit.h"
#include "../ifcparse/file.h"
#include "../ifcparse/instance_data.h"
#include "../ifcparse/schema.h"
#include <algorithm>
#include <cctype>
const std::unordered_map<std::string, double> kSiPrefixes = {
{ "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> kSiPrefixSymbols = {
{ "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> kSiConversions = {
{ "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> kImperialTypes = {
{ "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> kUnitSymbols = {
// 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 toLower(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;
}
// Pull an enumeration string off an attribute_value, or "" if null/invalid.
std::string enumString(const attribute_value& av) {
if (av.isNull()) return {};
if (av.type() != ifcopenshell::Argument_ENUMERATION) return {};
enumeration_reference er = av;
return std::string(er.value() ? er.value() : "");
}
} // namespace
double getPrefixMultiplier(const std::string& prefix) {
if (prefix.empty()) return 1.0;
auto it = kSiPrefixes.find(prefix);
return (it == kSiPrefixes.end()) ? 1.0 : it->second;
}
std::optional<double> siScaleFromNamedUnit(express::Base unit) {
double scale = 1.0;
while (unit && unit.declaration().is("IfcConversionBasedUnit")) {
auto e = unit.as<express::Entity>();
// Fast path: name in si_conversions table — matches python.
std::string name;
auto name_attr = e.get("Name");
if (!name_attr.isNull()) name = (std::string) name_attr;
if (auto it = kSiConversions.find(toLower(name));
it != kSiConversions.end()) {
return scale * it->second;
}
// Otherwise walk the ConversionFactor chain.
auto cf_attr = e.get("ConversionFactor");
if (cf_attr.isNull()) return std::nullopt;
express::Base cf = cf_attr;
auto cf_e = cf.as<express::Entity>();
auto vc_attr = cf_e.get("ValueComponent");
if (vc_attr.isNull()) return std::nullopt;
express::Base vc = vc_attr;
// ValueComponent is an IfcValue SELECT wrapping a measure.
scale *= (double) vc.get_attribute_value(0);
auto uc_attr = cf_e.get("UnitComponent");
if (uc_attr.isNull()) return std::nullopt;
unit = (express::Base) uc_attr;
}
if (unit && unit.declaration().is("IfcSIUnit")) {
auto e = unit.as<express::Entity>();
const std::string prefix = enumString(e.get("Prefix"));
const std::string name = enumString(e.get("Name"));
double m = getPrefixMultiplier(prefix);
// SQUARE_/CUBIC_-prefixed SI names: prefix multiplier squared/cubed.
if (name.find("SQUARE") != std::string::npos) {
m *= getPrefixMultiplier(prefix);
} else if (name.find("CUBIC") != std::string::npos) {
m *= getPrefixMultiplier(prefix);
m *= getPrefixMultiplier(prefix);
}
return scale * m;
}
if (unit && unit.declaration().is("IfcContextDependentUnit")) {
// No conversion to SI is possible for a context-dependent unit.
return std::nullopt;
}
return scale;
}
std::optional<express::Base> getUnitAssignment(ifcopenshell::file* ifc_file) {
auto projects = ifc_file->instances_by_type("IfcProject");
if (projects.empty()) return std::nullopt;
auto ua_attr = projects[0].as<express::Entity>().get("UnitsInContext");
if (ua_attr.isNull()) return std::nullopt;
return (express::Base) ua_attr;
}
std::optional<express::Base> getProjectUnit(ifcopenshell::file* ifc_file,
const std::string& unit_type) {
auto ua = getUnitAssignment(ifc_file);
if (!ua) return std::nullopt;
auto units_attr = ua->as<express::Entity>().get("Units");
if (units_attr.isNull()) return std::nullopt;
std::vector<express::Base> units = units_attr;
for (const auto& unit : units) {
// IfcMonetaryUnit has no UnitType — guard via declaration check.
if (!unit.declaration().is("IfcNamedUnit") &&
!unit.declaration().is("IfcDerivedUnit")) {
continue;
}
auto ut = unit.as<express::Entity>().get("UnitType");
if (enumString(ut) == unit_type) return unit;
}
return std::nullopt;
}
double calculateUnitScale(ifcopenshell::file* ifc_file,
const std::string& unit_type) {
auto unit = getProjectUnit(ifc_file, unit_type);
if (!unit) return 1.0;
auto scale = siScaleFromNamedUnit(*unit);
return scale.value_or(1.0);
}
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 = toLower(from_unit);
const std::string tl = toLower(to_unit);
if (auto it = kSiConversions.find(fl); it != kSiConversions.end()) {
value *= it->second;
} else if (!from_prefix.empty()) {
value *= getPrefixMultiplier(from_prefix);
if (from_unit.find("SQUARE") != std::string::npos) {
value *= getPrefixMultiplier(from_prefix);
} else if (from_unit.find("CUBIC") != std::string::npos) {
value *= getPrefixMultiplier(from_prefix);
value *= getPrefixMultiplier(from_prefix);
}
}
if (auto it = kSiConversions.find(tl); it != kSiConversions.end()) {
return value * (1.0 / it->second);
} else if (!to_prefix.empty()) {
value *= 1.0 / getPrefixMultiplier(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 / getPrefixMultiplier(to_prefix);
} else if (from_unit.find("CUBIC") != std::string::npos) {
value *= 1.0 / getPrefixMultiplier(to_prefix);
value *= 1.0 / getPrefixMultiplier(to_prefix);
}
}
return value;
}
double convertUnit(double value, express::Base from_unit, express::Base to_unit) {
auto pull = [](express::Base u, std::string& prefix, std::string& name) {
auto e = u.as<express::Entity>();
if (u.declaration().is("IfcSIUnit")) {
prefix = enumString(e.get("Prefix"));
name = enumString(e.get("Name"));
} else {
// IfcConversionBasedUnit / IfcContextDependentUnit: no Prefix,
// Name is a string attribute.
prefix.clear();
auto name_attr = e.get("Name");
name = name_attr.isNull() ? "" : (std::string) name_attr;
}
};
std::string fp, fn, tp, tn;
pull(from_unit, fp, fn);
pull(to_unit, tp, tn);
return convert(value, fp, fn, tp, tn);
}
-89
View File
@@ -1,89 +0,0 @@
/********************************************************************************
* *
* 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 <http://www.gnu.org/licenses/>. *
* *
********************************************************************************/
// Port of selected helpers from
// src/ifcopenshell-python/ifcopenshell/util/unit.py. Lives in src/ifcviewer/
// for now alongside Geolocation; will move out once ifcopenshell.util is
// ported to C++.
#ifndef UNIT_H
#define UNIT_H
#include "../ifcparse/express.h"
#include <optional>
#include <string>
#include <unordered_map>
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<std::string, double> kSiPrefixes;
// SI prefix display symbols, e.g. "MILLI" -> "m".
extern const std::unordered_map<std::string, std::string> kSiPrefixSymbols;
// Conversion-based unit name (lowercase, IFC convention) -> SI base scale.
// e.g. "foot" -> 0.3048, "square foot" -> 0.09290304.
extern const std::unordered_map<std::string, double> kSiConversions;
// Conversion-based unit name -> IFC unit type, e.g. "foot" -> "LENGTHUNIT".
extern const std::unordered_map<std::string, std::string> kImperialTypes;
// Display symbol per unit name. Covers IfcSIUnit names ("METRE" -> "m") and
// IfcConversionBasedUnit names ("foot" -> "ft").
extern const std::unordered_map<std::string, std::string> kUnitSymbols;
// Returns the multiplier for an SI prefix. Empty string returns 1.0.
double getPrefixMultiplier(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
// when the chain bottoms out in IfcContextDependentUnit (cannot convert).
std::optional<double> siScaleFromNamedUnit(express::Base named_unit);
// IfcProject.UnitsInContext (the IfcUnitAssignment). Returns nullopt if
// the file has no project or no assignment.
std::optional<express::Base> getUnitAssignment(ifcopenshell::file* ifc_file);
// First unit in the project's IfcUnitAssignment matching `unit_type`
// (e.g. "LENGTHUNIT"). Returns nullopt if not found.
std::optional<express::Base> getProjectUnit(ifcopenshell::file* ifc_file,
const std::string& unit_type);
// Project unit -> SI base scale (e.g. project in mm => 0.001). Defaults
// to 1.0 when no project unit of the requested type is set.
double calculateUnitScale(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 kSiConversions return the value unchanged.
double convertUnit(double value, express::Base from_unit, express::Base to_unit);
#endif // UNIT_H
+6 -10
View File
@@ -66,15 +66,6 @@ find_package(Eigen3 REQUIRED)
add_executable(test_federation
test_federation.cpp
${IFCVIEWER_SRC}/Federation.cpp
# Federation pulls in Unit::convert for federationUnitToMeters and
# Geolocation helpers (helmertMetersFromParameters, getWcs, getMapUnit)
# for computeModelGeoref; compile both directly so the test doesn't
# have to link the whole IfcViewer library (which would drag in
# Qt6::OpenGL, OpenCASCADE, etc.). Placement.cpp provides
# getAxis2Placement, called from Geolocation::getWcs.
${IFCVIEWER_SRC}/Unit.cpp
${IFCVIEWER_SRC}/Geolocation.cpp
${IFCVIEWER_SRC}/Placement.cpp
)
set_target_properties(test_federation PROPERTIES AUTOMOC ON)
target_include_directories(test_federation PRIVATE ${IFCVIEWER_SRC})
@@ -84,6 +75,11 @@ target_link_libraries(test_federation PRIVATE
Qt${QT_VERSION}::Gui # Federation::HomeView uses QVector3D from QtGui
Qt${QT_VERSION}::Test # QSignalSpy
Eigen3::Eigen # Federation.h: composed matrices use Eigen
IfcParse # Unit.cpp uses express::Base / file APIs
# IfcUtil provides Unit::convert + Geolocation helpers
# (helmertMetersFromParameters, getWcs, getMapUnit) + Placement
# (getAxis2Placement, called from Geolocation::getWcs). Linking the
# static lib avoids re-compiling those .cpp files here and pulls
# the IfcUtil include dir + IfcParse transitively.
IfcUtil
)
catch_discover_tests(test_federation)