mirror of
https://github.com/IfcOpenShell/IfcOpenShell.git
synced 2026-08-18 19:30:25 +00:00
Move the federation transform math into IfcViewerCore
The value types and compose helpers in Federation.h were already Qt-free — Eigen and std::string — but sat in the Qt half of the viewer, so the web build could not reach them. Split them into FederationMath and add it to IfcViewerCore, which the Emscripten build links. What stays behind is what genuinely needs the dependencies: computeModelGeoref reads an ifcopenshell::file, and the Federation class is a QObject that persists .ifcfed. Federation.h includes the new header, so no caller changes. FederationMath needs convert() to resolve a federation unit name to metres and x_axis_to_angle_deg() to read grid north off a coordinate operation, hence the helpers_math dependency added in the previous commit. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -143,6 +143,7 @@ set(IFCVIEWER_CORE_SOURCES
|
||||
ChunkPlanner.cpp
|
||||
InstanceCompose.cpp
|
||||
LodBuilder.cpp
|
||||
FederationMath.cpp
|
||||
SidecarCache.cpp
|
||||
SidecarCompress.cpp
|
||||
StreamingLoader.cpp
|
||||
@@ -200,6 +201,7 @@ set(IFCVIEWER_CORE_HEADERS
|
||||
SectionGizmoRenderer.h
|
||||
SectionPlane.h
|
||||
SelectionState.h
|
||||
FederationMath.h
|
||||
SidecarCache.h
|
||||
SidecarCompress.h
|
||||
StreamingLoader.h
|
||||
@@ -213,7 +215,19 @@ list(TRANSFORM IFCVIEWER_CORE_SOURCES PREPEND "${CMAKE_CURRENT_SOURCE_DIR}/")
|
||||
list(TRANSFORM IFCVIEWER_CORE_HEADERS PREPEND "${CMAKE_CURRENT_SOURCE_DIR}/")
|
||||
|
||||
add_library(IfcViewerCore STATIC ${IFCVIEWER_CORE_SOURCES} ${IFCVIEWER_CORE_HEADERS})
|
||||
|
||||
target_include_directories(IfcViewerCore PUBLIC ${CMAKE_CURRENT_SOURCE_DIR})
|
||||
|
||||
# FederationMath needs convert() to resolve a federation unit name to metres and
|
||||
# x_axis_to_angle_deg() to read grid north off a CoordinateOperation. Both live
|
||||
# in src/helpers, whose main library needs IfcParse — helpers_math is the
|
||||
# schema-free subset that does not, so it builds under Emscripten too. The web
|
||||
# build reaches this via add_subdirectory below; on desktop src/CMakeLists.txt
|
||||
# has already defined the target.
|
||||
if(NOT TARGET helpers_math)
|
||||
add_subdirectory(${CMAKE_CURRENT_SOURCE_DIR}/../helpers helpers_math_build EXCLUDE_FROM_ALL)
|
||||
endif()
|
||||
target_link_libraries(IfcViewerCore PUBLIC helpers_math)
|
||||
target_link_libraries(IfcViewerCore PUBLIC
|
||||
Eigen3::Eigen
|
||||
wgpu_native
|
||||
|
||||
@@ -38,28 +38,6 @@
|
||||
namespace {
|
||||
constexpr const char* kSchema = "ifcfed/1";
|
||||
|
||||
constexpr double kPi = 3.14159265358979323846;
|
||||
constexpr double kDegToRad = kPi / 180.0;
|
||||
|
||||
Eigen::Matrix4d translation4(const Eigen::Vector3d& t) {
|
||||
Eigen::Matrix4d M = Eigen::Matrix4d::Identity();
|
||||
M(0, 3) = t.x();
|
||||
M(1, 3) = t.y();
|
||||
M(2, 3) = t.z();
|
||||
return M;
|
||||
}
|
||||
|
||||
// Intrinsic XYZ Euler: R = R_z · R_y · R_x.
|
||||
Eigen::Matrix4d eulerXYZ(const Eigen::Vector3d& rxyz_rad) {
|
||||
const Eigen::Matrix3d R3 =
|
||||
(Eigen::AngleAxisd(rxyz_rad.z(), Eigen::Vector3d::UnitZ()) *
|
||||
Eigen::AngleAxisd(rxyz_rad.y(), Eigen::Vector3d::UnitY()) *
|
||||
Eigen::AngleAxisd(rxyz_rad.x(), Eigen::Vector3d::UnitX())).matrix();
|
||||
Eigen::Matrix4d R = Eigen::Matrix4d::Identity();
|
||||
R.block<3, 3>(0, 0) = R3;
|
||||
return R;
|
||||
}
|
||||
|
||||
QString resolvePath(const QString& fed_dir, const QString& stored) {
|
||||
if (stored.isEmpty()) return stored;
|
||||
QFileInfo fi(stored);
|
||||
@@ -82,22 +60,6 @@ QString relativizePath(const QString& fed_dir, const QString& abs_path) {
|
||||
|
||||
// === Stage-3/4 compose helpers ===
|
||||
|
||||
double federationUnitToMeters(const FederationConfig& cfg) {
|
||||
return convert(1.0, cfg.unit_prefix, cfg.unit_name, "", "METRE");
|
||||
}
|
||||
|
||||
Eigen::Matrix4d composeFederatedFalseOrigin(const FederatedFalseOrigin& origin,
|
||||
const FederationConfig& cfg) {
|
||||
const double u = federationUnitToMeters(cfg);
|
||||
const Eigen::Vector3d xyz_m = origin.xyz * u;
|
||||
const double rz_rad = origin.rz_deg * kDegToRad;
|
||||
const Eigen::Matrix3d Rz =
|
||||
Eigen::AngleAxisd(rz_rad, Eigen::Vector3d::UnitZ()).matrix();
|
||||
Eigen::Matrix4d Rz4 = Eigen::Matrix4d::Identity();
|
||||
Rz4.block<3, 3>(0, 0) = Rz;
|
||||
return Rz4 * translation4(-xyz_m);
|
||||
}
|
||||
|
||||
ModelGeoref computeModelGeoref(ifcopenshell::file* ifc_file) {
|
||||
ModelGeoref out;
|
||||
if (!ifc_file) return out;
|
||||
@@ -128,71 +90,6 @@ ModelGeoref computeModelGeoref(ifcopenshell::file* ifc_file) {
|
||||
return out;
|
||||
}
|
||||
|
||||
FederatedFalseOrigin
|
||||
guessFederatedFalseOrigin(const Eigen::Vector3d& first_geometry_point_m,
|
||||
const ModelGeoref& georef,
|
||||
const FederationConfig& fed_cfg) {
|
||||
Eigen::Vector3d t_m = first_geometry_point_m;
|
||||
|
||||
const bool use_coord_op = georef.has_coordinate_operation;
|
||||
if (use_coord_op) {
|
||||
const Eigen::Vector4d th(t_m.x(), t_m.y(), t_m.z(), 1.0);
|
||||
t_m = (georef.coordinate_operation_meters * th).head<3>();
|
||||
}
|
||||
|
||||
const double u_fed = federationUnitToMeters(fed_cfg);
|
||||
const double u_fed_inv = (u_fed != 0.0) ? (1.0 / u_fed) : 1.0;
|
||||
|
||||
FederatedFalseOrigin out;
|
||||
out.xyz = t_m * u_fed_inv;
|
||||
|
||||
// Rotation: helmert grid-north baked into coordinate_operation_meters.
|
||||
// helmert_meters_from_parameters built that block as R_z(theta)·diag(fx,fy,fz)
|
||||
// with theta = atan2(xao, xaa); x_axis_to_angle_deg is `-theta` in degrees.
|
||||
if (use_coord_op) {
|
||||
const Eigen::Matrix4d& M = georef.coordinate_operation_meters;
|
||||
out.rz_deg = x_axis_to_angle_deg(M(0, 0), M(1, 0));
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
Eigen::Matrix4d composeModelTransformation(const ModelTransformation& xf,
|
||||
const FederationConfig& fed_cfg,
|
||||
const ModelUnits& model_units,
|
||||
const Eigen::Matrix4d& coordinate_operation_meters) {
|
||||
const double u_fed = federationUnitToMeters(fed_cfg);
|
||||
|
||||
Eigen::Vector3d A_m;
|
||||
if (xf.a_frame == AFrame::ModelLocal) {
|
||||
// a is in the model's project length unit, expressed in the
|
||||
// pre-CoordinateOperation frame. Convert to metres, then lift
|
||||
// through the CoordinateOperation.
|
||||
const Eigen::Vector4d a_h(
|
||||
xf.a.x() * model_units.project_length_to_meters,
|
||||
xf.a.y() * model_units.project_length_to_meters,
|
||||
xf.a.z() * model_units.project_length_to_meters,
|
||||
1.0);
|
||||
A_m = (coordinate_operation_meters * a_h).head<3>();
|
||||
} else {
|
||||
// a is in the model's map unit, expressed in the
|
||||
// post-CoordinateOperation frame.
|
||||
A_m = xf.a * model_units.map_unit_to_meters;
|
||||
}
|
||||
|
||||
const Eigen::Vector3d B_m = xf.b * u_fed;
|
||||
const Eigen::Vector3d pivot_m = xf.pivot * u_fed;
|
||||
|
||||
const Eigen::Matrix4d R_local = eulerXYZ(xf.rxyz_deg * kDegToRad);
|
||||
const Eigen::Matrix4d R_at_pivot =
|
||||
translation4(pivot_m) * R_local * translation4(-pivot_m);
|
||||
|
||||
const Eigen::Vector4d Ah(A_m.x(), A_m.y(), A_m.z(), 1.0);
|
||||
const Eigen::Vector3d RA = (R_at_pivot * Ah).head<3>();
|
||||
const Eigen::Matrix4d T = translation4(B_m - RA);
|
||||
|
||||
return T * R_at_pivot;
|
||||
}
|
||||
|
||||
// === Federation class ===
|
||||
|
||||
Federation::Federation(QObject* parent) : QObject(parent) {}
|
||||
|
||||
+2
-103
@@ -20,6 +20,8 @@
|
||||
#ifndef FEDERATION_H
|
||||
#define FEDERATION_H
|
||||
|
||||
#include "FederationMath.h"
|
||||
|
||||
#include <Eigen/Dense>
|
||||
|
||||
#include <QJsonObject>
|
||||
@@ -60,115 +62,12 @@ namespace ifcopenshell { class file; }
|
||||
// to round-trip without precision loss; conversion happens in the compose
|
||||
// helpers.
|
||||
|
||||
// Federation-wide unit; the value space for FederatedFalseOrigin.xyz and
|
||||
// ModelTransformation::{b, pivot}.
|
||||
struct FederationConfig {
|
||||
// IfcSIUnit name ("METRE") or IfcConversionBasedUnit name ("foot", "inch").
|
||||
std::string unit_name = "METRE";
|
||||
// SI prefix ("MILLI", "KILO", ...) — empty for unprefixed or for
|
||||
// conversion-based units.
|
||||
std::string unit_prefix = "";
|
||||
};
|
||||
|
||||
// FederatedFalseOrigin — the user-nominated federation origin. Authoring
|
||||
// intent is "nominate this XYZ as the new origin, with optional Z-axis
|
||||
// heading rotation". Composed as R_z(rz_deg) · T(-xyz_in_metres).
|
||||
struct FederatedFalseOrigin {
|
||||
Eigen::Vector3d xyz = Eigen::Vector3d::Zero(); // federation unit
|
||||
double rz_deg = 0.0;
|
||||
};
|
||||
|
||||
// Frame in which ModelTransformation.a is expressed.
|
||||
// ModelLocal — pre-CoordinateOperation model coordinates, in the model's
|
||||
// project length unit
|
||||
// ModelGlobal — post-CoordinateOperation model coordinates, in the model's
|
||||
// map unit
|
||||
enum class AFrame { ModelLocal, ModelGlobal };
|
||||
|
||||
// ModelTransformation — the per-model placement within the federation.
|
||||
// Authoring intent is "rotate the model around `pivot`, then translate so
|
||||
// that point `a` lands at point `b`". Composed as
|
||||
//
|
||||
// R_local = R_z(rz) · R_y(ry) · R_x(rx) [intrinsic XYZ]
|
||||
// R_at_pivot = T(pivot_m) · R_local · T(-pivot_m)
|
||||
// result = T(b_m - R_at_pivot · a_m) · R_at_pivot
|
||||
struct ModelTransformation {
|
||||
AFrame a_frame = AFrame::ModelGlobal;
|
||||
Eigen::Vector3d a = Eigen::Vector3d::Zero(); // model project / map unit
|
||||
Eigen::Vector3d b = Eigen::Vector3d::Zero(); // federation unit
|
||||
Eigen::Vector3d rxyz_deg = Eigen::Vector3d::Zero(); // degrees, intrinsic XYZ
|
||||
Eigen::Vector3d pivot = Eigen::Vector3d::Zero(); // federation unit
|
||||
};
|
||||
|
||||
// Per-model unit scales captured at load time. project_length_to_meters comes
|
||||
// from calculate_unit_scale(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;
|
||||
};
|
||||
|
||||
// Per-model georeferencing data derived from the IFC.
|
||||
// `coordinate_operation_meters` is the helmert · inv(wcs) matrix in metres
|
||||
// representing the IfcCoordinateOperation; consumers compose it before
|
||||
// FederatedFalseOrigin / ModelTransformation at upload time. When the
|
||||
// model has no map conversion, `has_coordinate_operation == false` and the
|
||||
// matrix is identity.
|
||||
struct ModelGeoref {
|
||||
ModelUnits units;
|
||||
Eigen::Matrix4d coordinate_operation_meters = Eigen::Matrix4d::Identity();
|
||||
bool has_coordinate_operation = false;
|
||||
};
|
||||
|
||||
// Read a model's project length unit, map unit, helmert parameters, and WCS
|
||||
// from `ifc_file` and reduce them to a metres-in / metres-out
|
||||
// CoordinateOperation matrix. Pure compute; safe to call repeatedly if the
|
||||
// caller doesn't want to cache.
|
||||
ModelGeoref computeModelGeoref(ifcopenshell::file* ifc_file);
|
||||
|
||||
// Build a FederatedFalseOrigin guess so that a model lands near the
|
||||
// federation origin instead of out at its surveyor coordinates. Designed
|
||||
// to work without an open IFC file so it's usable from sidecar-only loads
|
||||
// (the inputs are all derivable from the resident MeshInfo + InstanceInfo
|
||||
// data + ModelGeoref).
|
||||
//
|
||||
// Position: `first_geometry_point_m` is a point that actually lies on the
|
||||
// model's first instance's geometry, in metres, pre-CoordinateOperation —
|
||||
// typically the world-space centre of the first instance's mesh AABB
|
||||
// (instance0.placement_transformation * mesh.local_aabb_center). We use
|
||||
// a real geometry point rather than the instance's placement translation
|
||||
// because IFC placements often live far from the actual geometry (long
|
||||
// ObjectPlacement chains, intermediate local coordinate systems). The
|
||||
// point is lifted through `georef.coordinate_operation_meters` when one
|
||||
// is present, then expressed in the federation unit.
|
||||
//
|
||||
// Rotation: read directly from `georef.coordinate_operation_meters`
|
||||
// when `has_coordinate_operation` (this is the helmert grid-north
|
||||
// angle); otherwise zero. Anticlockwise positive.
|
||||
FederatedFalseOrigin
|
||||
guessFederatedFalseOrigin(const Eigen::Vector3d& first_geometry_point_m,
|
||||
const ModelGeoref& georef,
|
||||
const FederationConfig& fed_cfg);
|
||||
|
||||
// 1 federation_unit -> N metres.
|
||||
double federationUnitToMeters(const FederationConfig&);
|
||||
|
||||
// Compose FederatedFalseOrigin into a 4x4 matrix in metres.
|
||||
Eigen::Matrix4d composeFederatedFalseOrigin(const FederatedFalseOrigin&,
|
||||
const FederationConfig&);
|
||||
|
||||
// Compose ModelTransformation into a 4x4 matrix in metres.
|
||||
// `coordinate_operation_meters` is the model's CoordinateOperation matrix
|
||||
// (e.g. helmert_meters_from_parameters · inv(wcs_meters)) — needed to lift
|
||||
// `a` into metres when a_frame == ModelLocal. Pass identity when the
|
||||
// CoordinateOperation is disabled or absent.
|
||||
Eigen::Matrix4d composeModelTransformation(const ModelTransformation&,
|
||||
const FederationConfig& fed_cfg,
|
||||
const ModelUnits& model_units,
|
||||
const Eigen::Matrix4d& coordinate_operation_meters);
|
||||
|
||||
// === Federation persistence (.ifcfed) ===
|
||||
//
|
||||
// In-memory representation of an .ifcfed file (IFC federation).
|
||||
|
||||
@@ -0,0 +1,132 @@
|
||||
/********************************************************************************
|
||||
* *
|
||||
* 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 "FederationMath.h"
|
||||
|
||||
#include "geolocation_transform.h" // x_axis_to_angle_deg
|
||||
#include "unit_convert.h" // convert
|
||||
|
||||
#include <cmath>
|
||||
|
||||
namespace {
|
||||
|
||||
constexpr double kPi = 3.14159265358979323846;
|
||||
constexpr double kDegToRad = kPi / 180.0;
|
||||
|
||||
Eigen::Matrix4d translation4(const Eigen::Vector3d& t) {
|
||||
Eigen::Matrix4d M = Eigen::Matrix4d::Identity();
|
||||
M(0, 3) = t.x();
|
||||
M(1, 3) = t.y();
|
||||
M(2, 3) = t.z();
|
||||
return M;
|
||||
}
|
||||
|
||||
// Intrinsic XYZ Euler: R = R_z · R_y · R_x.
|
||||
Eigen::Matrix4d eulerXYZ(const Eigen::Vector3d& rxyz_rad) {
|
||||
const Eigen::Matrix3d R3 =
|
||||
(Eigen::AngleAxisd(rxyz_rad.z(), Eigen::Vector3d::UnitZ()) *
|
||||
Eigen::AngleAxisd(rxyz_rad.y(), Eigen::Vector3d::UnitY()) *
|
||||
Eigen::AngleAxisd(rxyz_rad.x(), Eigen::Vector3d::UnitX())).matrix();
|
||||
Eigen::Matrix4d R = Eigen::Matrix4d::Identity();
|
||||
R.block<3, 3>(0, 0) = R3;
|
||||
return R;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
double federationUnitToMeters(const FederationConfig& cfg) {
|
||||
return convert(1.0, cfg.unit_prefix, cfg.unit_name, "", "METRE");
|
||||
}
|
||||
|
||||
Eigen::Matrix4d composeFederatedFalseOrigin(const FederatedFalseOrigin& origin,
|
||||
const FederationConfig& cfg) {
|
||||
const double u = federationUnitToMeters(cfg);
|
||||
const Eigen::Vector3d xyz_m = origin.xyz * u;
|
||||
const double rz_rad = origin.rz_deg * kDegToRad;
|
||||
const Eigen::Matrix3d Rz =
|
||||
Eigen::AngleAxisd(rz_rad, Eigen::Vector3d::UnitZ()).matrix();
|
||||
Eigen::Matrix4d Rz4 = Eigen::Matrix4d::Identity();
|
||||
Rz4.block<3, 3>(0, 0) = Rz;
|
||||
return Rz4 * translation4(-xyz_m);
|
||||
}
|
||||
|
||||
FederatedFalseOrigin
|
||||
guessFederatedFalseOrigin(const Eigen::Vector3d& first_geometry_point_m,
|
||||
const ModelGeoref& georef,
|
||||
const FederationConfig& fed_cfg) {
|
||||
Eigen::Vector3d t_m = first_geometry_point_m;
|
||||
|
||||
const bool use_coord_op = georef.has_coordinate_operation;
|
||||
if (use_coord_op) {
|
||||
const Eigen::Vector4d th(t_m.x(), t_m.y(), t_m.z(), 1.0);
|
||||
t_m = (georef.coordinate_operation_meters * th).head<3>();
|
||||
}
|
||||
|
||||
const double u_fed = federationUnitToMeters(fed_cfg);
|
||||
const double u_fed_inv = (u_fed != 0.0) ? (1.0 / u_fed) : 1.0;
|
||||
|
||||
FederatedFalseOrigin out;
|
||||
out.xyz = t_m * u_fed_inv;
|
||||
|
||||
// Rotation: helmert grid-north baked into coordinate_operation_meters.
|
||||
// helmert_meters_from_parameters built that block as R_z(theta)·diag(fx,fy,fz)
|
||||
// with theta = atan2(xao, xaa); x_axis_to_angle_deg is `-theta` in degrees.
|
||||
if (use_coord_op) {
|
||||
const Eigen::Matrix4d& M = georef.coordinate_operation_meters;
|
||||
out.rz_deg = x_axis_to_angle_deg(M(0, 0), M(1, 0));
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
Eigen::Matrix4d composeModelTransformation(const ModelTransformation& xf,
|
||||
const FederationConfig& fed_cfg,
|
||||
const ModelUnits& model_units,
|
||||
const Eigen::Matrix4d& coordinate_operation_meters) {
|
||||
const double u_fed = federationUnitToMeters(fed_cfg);
|
||||
|
||||
Eigen::Vector3d A_m;
|
||||
if (xf.a_frame == AFrame::ModelLocal) {
|
||||
// a is in the model's project length unit, expressed in the
|
||||
// pre-CoordinateOperation frame. Convert to metres, then lift
|
||||
// through the CoordinateOperation.
|
||||
const Eigen::Vector4d a_h(
|
||||
xf.a.x() * model_units.project_length_to_meters,
|
||||
xf.a.y() * model_units.project_length_to_meters,
|
||||
xf.a.z() * model_units.project_length_to_meters,
|
||||
1.0);
|
||||
A_m = (coordinate_operation_meters * a_h).head<3>();
|
||||
} else {
|
||||
// a is in the model's map unit, expressed in the
|
||||
// post-CoordinateOperation frame.
|
||||
A_m = xf.a * model_units.map_unit_to_meters;
|
||||
}
|
||||
|
||||
const Eigen::Vector3d B_m = xf.b * u_fed;
|
||||
const Eigen::Vector3d pivot_m = xf.pivot * u_fed;
|
||||
|
||||
const Eigen::Matrix4d R_local = eulerXYZ(xf.rxyz_deg * kDegToRad);
|
||||
const Eigen::Matrix4d R_at_pivot =
|
||||
translation4(pivot_m) * R_local * translation4(-pivot_m);
|
||||
|
||||
const Eigen::Vector4d Ah(A_m.x(), A_m.y(), A_m.z(), 1.0);
|
||||
const Eigen::Vector3d RA = (R_at_pivot * Ah).head<3>();
|
||||
const Eigen::Matrix4d T = translation4(B_m - RA);
|
||||
|
||||
return T * R_at_pivot;
|
||||
}
|
||||
@@ -0,0 +1,144 @@
|
||||
/********************************************************************************
|
||||
* *
|
||||
* 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/>. *
|
||||
* *
|
||||
********************************************************************************/
|
||||
|
||||
// Qt-free half of the federation transformation pipeline: the value types and
|
||||
// the pure compose math over them.
|
||||
//
|
||||
// Lives apart from Federation.h so IfcViewerCore — and through it the
|
||||
// Emscripten build — can compose federated transforms without linking Qt or
|
||||
// IfcParse. What stays in Federation.h is everything that genuinely needs
|
||||
// them: computeModelGeoref (reads an ifcopenshell::file) and the Federation
|
||||
// document model (a QObject that persists .ifcfed).
|
||||
//
|
||||
// The pipeline comment in Federation.h describes how these compose.
|
||||
|
||||
#ifndef FEDERATIONMATH_H
|
||||
#define FEDERATIONMATH_H
|
||||
|
||||
#include <Eigen/Dense>
|
||||
|
||||
#include <string>
|
||||
|
||||
// Federation-wide unit; the value space for FederatedFalseOrigin.xyz and
|
||||
// ModelTransformation::{b, pivot}.
|
||||
struct FederationConfig {
|
||||
// IfcSIUnit name ("METRE") or IfcConversionBasedUnit name ("foot", "inch").
|
||||
std::string unit_name = "METRE";
|
||||
// SI prefix ("MILLI", "KILO", ...) — empty for unprefixed or for
|
||||
// conversion-based units.
|
||||
std::string unit_prefix = "";
|
||||
};
|
||||
|
||||
// FederatedFalseOrigin — the user-nominated federation origin. Authoring
|
||||
// intent is "nominate this XYZ as the new origin, with optional Z-axis
|
||||
// heading rotation". Composed as R_z(rz_deg) · T(-xyz_in_metres).
|
||||
struct FederatedFalseOrigin {
|
||||
Eigen::Vector3d xyz = Eigen::Vector3d::Zero(); // federation unit
|
||||
double rz_deg = 0.0;
|
||||
};
|
||||
|
||||
// Frame in which ModelTransformation.a is expressed.
|
||||
// ModelLocal — pre-CoordinateOperation model coordinates, in the model's
|
||||
// project length unit
|
||||
// ModelGlobal — post-CoordinateOperation model coordinates, in the model's
|
||||
// map unit
|
||||
enum class AFrame { ModelLocal, ModelGlobal };
|
||||
|
||||
// ModelTransformation — the per-model placement within the federation.
|
||||
// Authoring intent is "rotate the model around `pivot`, then translate so
|
||||
// that point `a` lands at point `b`". Composed as
|
||||
//
|
||||
// R_local = R_z(rz) · R_y(ry) · R_x(rx) [intrinsic XYZ]
|
||||
// R_at_pivot = T(pivot_m) · R_local · T(-pivot_m)
|
||||
// result = T(b_m - R_at_pivot · a_m) · R_at_pivot
|
||||
struct ModelTransformation {
|
||||
AFrame a_frame = AFrame::ModelGlobal;
|
||||
Eigen::Vector3d a = Eigen::Vector3d::Zero(); // model project / map unit
|
||||
Eigen::Vector3d b = Eigen::Vector3d::Zero(); // federation unit
|
||||
Eigen::Vector3d rxyz_deg = Eigen::Vector3d::Zero(); // degrees, intrinsic XYZ
|
||||
Eigen::Vector3d pivot = Eigen::Vector3d::Zero(); // federation unit
|
||||
};
|
||||
|
||||
// Per-model unit scales captured at load time. project_length_to_meters comes
|
||||
// from calculate_unit_scale(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;
|
||||
};
|
||||
|
||||
// Per-model georeferencing data derived from the IFC.
|
||||
// `coordinate_operation_meters` is the helmert · inv(wcs) matrix in metres
|
||||
// representing the IfcCoordinateOperation; consumers compose it before
|
||||
// FederatedFalseOrigin / ModelTransformation at upload time. When the
|
||||
// model has no map conversion, `has_coordinate_operation == false` and the
|
||||
// matrix is identity.
|
||||
struct ModelGeoref {
|
||||
ModelUnits units;
|
||||
Eigen::Matrix4d coordinate_operation_meters = Eigen::Matrix4d::Identity();
|
||||
bool has_coordinate_operation = false;
|
||||
};
|
||||
|
||||
// computeModelGeoref, which fills a ModelGeoref from an ifcopenshell::file,
|
||||
// stays in Federation.h — it needs IfcParse and the geolocation helpers.
|
||||
|
||||
// Build a FederatedFalseOrigin guess so that a model lands near the
|
||||
// federation origin instead of out at its surveyor coordinates. Designed
|
||||
// to work without an open IFC file so it's usable from sidecar-only loads
|
||||
// (the inputs are all derivable from the resident MeshInfo + InstanceInfo
|
||||
// data + ModelGeoref).
|
||||
//
|
||||
// Position: `first_geometry_point_m` is a point that actually lies on the
|
||||
// model's first instance's geometry, in metres, pre-CoordinateOperation —
|
||||
// typically the world-space centre of the first instance's mesh AABB
|
||||
// (instance0.placement_transformation * mesh.local_aabb_center). We use
|
||||
// a real geometry point rather than the instance's placement translation
|
||||
// because IFC placements often live far from the actual geometry (long
|
||||
// ObjectPlacement chains, intermediate local coordinate systems). The
|
||||
// point is lifted through `georef.coordinate_operation_meters` when one
|
||||
// is present, then expressed in the federation unit.
|
||||
//
|
||||
// Rotation: read directly from `georef.coordinate_operation_meters`
|
||||
// when `has_coordinate_operation` (this is the helmert grid-north
|
||||
// angle); otherwise zero. Anticlockwise positive.
|
||||
FederatedFalseOrigin
|
||||
guessFederatedFalseOrigin(const Eigen::Vector3d& first_geometry_point_m,
|
||||
const ModelGeoref& georef,
|
||||
const FederationConfig& fed_cfg);
|
||||
|
||||
// 1 federation_unit -> N metres.
|
||||
double federationUnitToMeters(const FederationConfig&);
|
||||
|
||||
// Compose FederatedFalseOrigin into a 4x4 matrix in metres.
|
||||
Eigen::Matrix4d composeFederatedFalseOrigin(const FederatedFalseOrigin&,
|
||||
const FederationConfig&);
|
||||
|
||||
// Compose ModelTransformation into a 4x4 matrix in metres.
|
||||
// `coordinate_operation_meters` is the model's CoordinateOperation matrix
|
||||
// (e.g. helmert_meters_from_parameters · inv(wcs_meters)) — needed to lift
|
||||
// `a` into metres when a_frame == ModelLocal. Pass identity when the
|
||||
// CoordinateOperation is disabled or absent.
|
||||
Eigen::Matrix4d composeModelTransformation(const ModelTransformation&,
|
||||
const FederationConfig& fed_cfg,
|
||||
const ModelUnits& model_units,
|
||||
const Eigen::Matrix4d& coordinate_operation_meters);
|
||||
|
||||
#endif // FEDERATIONMATH_H
|
||||
@@ -149,6 +149,7 @@ find_package(Eigen3 REQUIRED)
|
||||
add_executable(test_federation
|
||||
test_federation.cpp
|
||||
${IFCVIEWER_SRC}/Federation.cpp
|
||||
${IFCVIEWER_SRC}/FederationMath.cpp # compose helpers, split out for IfcViewerCore
|
||||
)
|
||||
set_target_properties(test_federation PROPERTIES AUTOMOC ON)
|
||||
target_include_directories(test_federation PRIVATE ${IFCVIEWER_SRC})
|
||||
|
||||
Reference in New Issue
Block a user