diff --git a/src/ifcviewer/Federation.cpp b/src/ifcviewer/Federation.cpp index ac1fb3a894..9dc500cbd1 100644 --- a/src/ifcviewer/Federation.cpp +++ b/src/ifcviewer/Federation.cpp @@ -84,8 +84,8 @@ double federationUnitToMeters(const FederationConfig& cfg) { return convert(1.0, cfg.unit_prefix, cfg.unit_name, "", "METRE"); } -Eigen::Matrix4d composeFederationOrigin(const FederationOrigin& origin, - const FederationConfig& cfg) { +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; @@ -127,32 +127,34 @@ ModelGeoref computeModelGeoref(ifcopenshell::file* ifc_file) { wcs_m(0, 3) *= out.units.project_length_to_meters; wcs_m(1, 3) *= out.units.project_length_to_meters; wcs_m(2, 3) *= out.units.project_length_to_meters; - out.stage2_meters = helmert * wcs_m.inverse(); + out.coordinate_operation_meters = helmert * wcs_m.inverse(); } else { - out.stage2_meters = helmert; + out.coordinate_operation_meters = helmert; } - out.has_stage2 = true; + out.has_coordinate_operation = true; return out; } -Eigen::Matrix4d composeModelTransform(const ModelTransform& xf, - const FederationConfig& fed_cfg, - const ModelUnits& model_units, - const Eigen::Matrix4d& stage2_meters) { +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-stage2 frame. Convert to metres, then lift through stage 2. + // 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 = (stage2_meters * a_h).head<3>(); + A_m = (coordinate_operation_meters * a_h).head<3>(); } else { - // a is in the model's map unit, expressed in the post-stage2 frame. + // a is in the model's map unit, expressed in the + // post-CoordinateOperation frame. A_m = xf.a * model_units.map_unit_to_meters; } @@ -188,8 +190,8 @@ void Federation::clear() { created_ = QDateTime(); modified_ = QDateTime(); models_.clear(); - config_ = FederationConfig{}; - origin_ = FederationOrigin{}; + config_ = FederationConfig{}; + federated_false_origin_ = FederatedFalseOrigin{}; has_home_view_ = false; home_view_ = HomeView{}; setDirty(false); @@ -202,17 +204,18 @@ void Federation::setConfig(const FederationConfig& c) { setDirty(true); } -void Federation::setOrigin(const FederationOrigin& o) { - if (origin_.xyz == o.xyz && origin_.rz_deg == o.rz_deg) return; - origin_ = o; +void Federation::setFederatedFalseOrigin(const FederatedFalseOrigin& o) { + if (federated_false_origin_.xyz == o.xyz && + federated_false_origin_.rz_deg == o.rz_deg) return; + federated_false_origin_ = o; setDirty(true); } -void Federation::setModelTransform(const QString& fed_id, - const ModelTransform& xf) { +void Federation::setModelTransformation(const QString& fed_id, + const ModelTransformation& xf) { for (auto& m : models_) { if (m.id != fed_id) continue; - m.transform_intent = xf; + m.model_transformation = xf; setDirty(true); return; } @@ -315,14 +318,14 @@ bool Federation::load(const QString& path, config_.unit_prefix = uo.value("prefix").toString("").toStdString(); } - if (QJsonValue ov = root.value("origin"); ov.isObject()) { + if (QJsonValue ov = root.value("federated_false_origin"); ov.isObject()) { QJsonObject oo = ov.toObject(); QJsonArray xyz = oo.value("xyz").toArray(); if (xyz.size() == 3) { - origin_.xyz = Eigen::Vector3d( + federated_false_origin_.xyz = Eigen::Vector3d( xyz[0].toDouble(), xyz[1].toDouble(), xyz[2].toDouble()); } - origin_.rz_deg = oo.value("rz_deg").toDouble(0.0); + federated_false_origin_.rz_deg = oo.value("rz_deg").toDouble(0.0); } QJsonArray arr = root.value("models").toArray(); @@ -360,20 +363,20 @@ bool Federation::load(const QString& path, if (m.display_name.isEmpty()) m.display_name = QFileInfo(m.source_path).fileName(); - if (QJsonValue tv = mo.value("transform_intent"); tv.isObject()) { + if (QJsonValue tv = mo.value("model_transformation"); tv.isObject()) { QJsonObject to = tv.toObject(); const QString af = to.value("a_frame").toString("ModelGlobal"); - m.transform_intent.a_frame = + m.model_transformation.a_frame = (af == "ModelLocal") ? AFrame::ModelLocal : AFrame::ModelGlobal; auto readVec3 = [](QJsonArray ja) { if (ja.size() != 3) return Eigen::Vector3d::Zero().eval(); return Eigen::Vector3d( ja[0].toDouble(), ja[1].toDouble(), ja[2].toDouble()); }; - m.transform_intent.a = readVec3(to.value("a").toArray()); - m.transform_intent.b = readVec3(to.value("b").toArray()); - m.transform_intent.rxyz_deg = readVec3(to.value("rxyz_deg").toArray()); - m.transform_intent.pivot = readVec3(to.value("pivot").toArray()); + m.model_transformation.a = readVec3(to.value("a").toArray()); + m.model_transformation.b = readVec3(to.value("b").toArray()); + m.model_transformation.rxyz_deg = readVec3(to.value("rxyz_deg").toArray()); + m.model_transformation.pivot = readVec3(to.value("pivot").toArray()); } QJsonValue vv = mo.value("visible"); @@ -426,12 +429,12 @@ bool Federation::save(const QString& path, QString* err) { { QJsonObject oo; QJsonArray xyz; - xyz.append(origin_.xyz.x()); - xyz.append(origin_.xyz.y()); - xyz.append(origin_.xyz.z()); + xyz.append(federated_false_origin_.xyz.x()); + xyz.append(federated_false_origin_.xyz.y()); + xyz.append(federated_false_origin_.xyz.z()); oo["xyz"] = xyz; - oo["rz_deg"] = origin_.rz_deg; - root["origin"] = oo; + oo["rz_deg"] = federated_false_origin_.rz_deg; + root["federated_false_origin"] = oo; } QJsonArray arr; @@ -450,9 +453,9 @@ bool Federation::save(const QString& path, QString* err) { } mo["source"] = so; - // Skip transform_intent when it's at defaults (identity placement). - const ModelTransform def; - const ModelTransform& xf = m.transform_intent; + // Skip model_transformation when it's at defaults (identity placement). + const ModelTransformation def; + const ModelTransformation& xf = m.model_transformation; const bool xf_is_default = xf.a_frame == def.a_frame && xf.a == def.a && xf.b == def.b && xf.rxyz_deg == def.rxyz_deg && xf.pivot == def.pivot; @@ -469,7 +472,7 @@ bool Federation::save(const QString& path, QString* err) { to["b"] = writeVec3(xf.b); to["rxyz_deg"] = writeVec3(xf.rxyz_deg); to["pivot"] = writeVec3(xf.pivot); - mo["transform_intent"] = to; + mo["model_transformation"] = to; } if (!m.visible) mo["visible"] = false; diff --git a/src/ifcviewer/Federation.h b/src/ifcviewer/Federation.h index d93603e45e..97fe072cff 100644 --- a/src/ifcviewer/Federation.h +++ b/src/ifcviewer/Federation.h @@ -33,27 +33,33 @@ namespace ifcopenshell { class file; } -// === Stage-3/4 data model === +// === Federation transformation pipeline === // // A federation places one or more IFC models in a shared scene. Each model's -// final per-instance transform is composed as +// final per-instance transform is the composition of four named stages: // -// stage3 · stage4 · stage2 · placement_stage1 +// FederatedFalseOrigin · ModelTransformation · CoordinateOperation +// · PlacementTransformation // // where: -// - stage1 is per-mesh vertex rebasing (load-time, immutable) -// - stage2 is the per-model georef matrix from IfcMapConversion etc. -// (load-time, immutable; can be toggled off) -// - stage3 is the federation-wide false origin (mutable, federation-scope) -// - stage4 is the per-model placement within the federation (mutable, per-model) +// - PlacementTransformation: per-instance, derived from the IFC's +// IfcObjectPlacement chain (load-time, immutable). This is the +// iterator's per-shape transform. +// - CoordinateOperation: per-model, derived from the IFC's +// IfcCoordinateOperation (e.g. IfcMapConversion + IfcProjectedCRS). +// Load-time, immutable; can be toggled on/off. +// - FederatedFalseOrigin: federation-wide. Mutable, persisted in +// `.ifcfed`. Re-applied to every model. +// - ModelTransformation: per-model, user-authored within the federation. +// Mutable, persisted in `.ifcfed`. // // All composed matrices are in metres. User-authored numbers are stored in // their source units (model project unit / model map unit / federation unit) // to round-trip without precision loss; conversion happens in the compose // helpers. -// Federation-wide unit; the value space for FederationOrigin.xyz and -// ModelTransform::{b, pivot}. +// 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"; @@ -62,26 +68,29 @@ struct FederationConfig { std::string unit_prefix = ""; }; -// Stage 3 — the federation false origin. Authoring intent is "nominate this -// XYZ as the new origin, with optional Z-axis heading rotation". Composed -// as stage3 = R_z(rz_deg) · T(-xyz_in_metres). -struct FederationOrigin { +// 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 ModelTransform.a is expressed. -// ModelLocal — pre-stage2 model coordinates, in the model's project length unit -// ModelGlobal — post-stage2 model coordinates, in the model's map unit +// 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 }; -// Stage 4 — 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 +// 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) -// stage4 = T(b_m - R_at_pivot · a_m) · R_at_pivot -struct ModelTransform { +// 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 @@ -98,37 +107,40 @@ struct ModelUnits { double map_unit_to_meters = 1.0; }; -// Per-model georeferencing data derived from the IFC. `stage2_meters` is -// the helmert · inv(wcs) georef matrix in metres; consumers compose it -// before stage 3 / stage 4 at upload time. When the model has no map -// conversion, `has_stage2 == false` and `stage2_meters` is identity. +// 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 stage2_meters = Eigen::Matrix4d::Identity(); - bool has_stage2 = false; + 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 georef matrix. -// Pure compute; safe to call repeatedly if the caller doesn't want to cache. +// 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); // 1 federation_unit -> N metres. double federationUnitToMeters(const FederationConfig&); -// Compose stage 3 (federation false origin) into a 4x4 matrix in metres. -Eigen::Matrix4d composeFederationOrigin(const FederationOrigin&, - const FederationConfig&); +// Compose FederatedFalseOrigin into a 4x4 matrix in metres. +Eigen::Matrix4d composeFederatedFalseOrigin(const FederatedFalseOrigin&, + const FederationConfig&); -// Compose stage 4 (per-model placement within the federation) into a 4x4 -// matrix in metres. `stage2_meters` is the model's georef matrix (e.g. -// helmertMetersFromParameters · inv(wcs_meters)) — needed to lift `a` into -// metres when a_frame == ModelLocal. Pass identity when stage 2 is disabled -// or absent. -Eigen::Matrix4d composeModelTransform(const ModelTransform&, - const FederationConfig& fed_cfg, - const ModelUnits& model_units, - const Eigen::Matrix4d& stage2_meters); +// Compose ModelTransformation into a 4x4 matrix in metres. +// `coordinate_operation_meters` is the model's CoordinateOperation matrix +// (e.g. helmertMetersFromParameters · 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) === // @@ -151,11 +163,11 @@ public: }; struct Model { - QString id; // stable, persisted + QString id; // stable, persisted QString display_name; - QString source_kind = "local"; // future: "http", "speckle", ... - QString source_path; // resolved absolute when kind == "local" - ModelTransform transform_intent; // stage 4 + QString source_kind = "local"; // future: "http", "speckle", ... + QString source_path; // resolved absolute when kind == "local" + ModelTransformation model_transformation; bool visible = true; }; @@ -174,8 +186,8 @@ public: void clearHomeView(); void setConfig(const FederationConfig&); - void setOrigin(const FederationOrigin&); - void setModelTransform(const QString& fed_id, const ModelTransform&); + void setFederatedFalseOrigin(const FederatedFalseOrigin&); + void setModelTransformation(const QString& fed_id, const ModelTransformation&); // Accessors const std::vector& models() const { return models_; } @@ -187,7 +199,7 @@ public: bool hasHomeView() const { return has_home_view_; } const HomeView& homeView() const { return home_view_; } const FederationConfig& config() const { return config_; } - const FederationOrigin& origin() const { return origin_; } + const FederatedFalseOrigin& federatedFalseOrigin() const { return federated_false_origin_; } signals: void dirtyChanged(bool dirty); @@ -202,8 +214,8 @@ private: QDateTime created_; QDateTime modified_; std::vector models_; - FederationConfig config_; - FederationOrigin origin_; + FederationConfig config_; + FederatedFalseOrigin federated_false_origin_; bool has_home_view_ = false; HomeView home_view_; bool dirty_ = false; diff --git a/src/ifcviewer/GeometryStreamer.cpp b/src/ifcviewer/GeometryStreamer.cpp index 1b418fc2c9..da7f6fc624 100644 --- a/src/ifcviewer/GeometryStreamer.cpp +++ b/src/ifcviewer/GeometryStreamer.cpp @@ -147,12 +147,12 @@ std::vector GeometryStreamer::drainElements() { // Build a mesh chunk (local coords, 28-byte interleaved vertices) from a // TriangulationElement. Per-vertex color is baked from material_ids so that // triangulations with per-face materials still render correctly. -// Stage 1 — vertex rebasing. When `offset` is non-zero, every vertex -// position is subtracted by it so the emitted mesh-local coordinates stay -// near the origin (and float32 precision survives upload to the GPU). -// Caller compensates by post-multiplying each instance's placement by -// T(+offset), which is mathematically the identity overall but moves the -// "magnitude" off the float-precision-sensitive vertex column. +// Vertex rebasing: when `offset` is non-zero, every vertex position is +// subtracted by it so the emitted mesh-local coordinates stay near the +// origin (and float32 precision survives upload to the GPU). Caller +// compensates by post-multiplying each instance's PlacementTransformation +// by T(+offset), which is mathematically the identity overall but moves +// the magnitude off the float-precision-sensitive vertex column. static MeshChunk buildMeshChunk(uint32_t model_id, uint32_t local_mesh_id, const IfcGeom::TriangulationElement* elem, @@ -593,8 +593,8 @@ void GeometryStreamer::run(const std::string& path, int num_threads) { } if (first_sight) { - // Stage 1: pick a rebase offset when the mesh's first - // source vertex is far from origin (>1 km in metres, + // Vertex rebasing: pick a rebase offset when the mesh's + // first source vertex is far from origin (>1 km in metres, // matching bonsai's distance_limit default). Iterator // outputs metres, so the threshold is in metres directly. Eigen::Vector3d offset = Eigen::Vector3d::Zero(); @@ -627,9 +627,10 @@ void GeometryStreamer::run(const std::string& path, int num_threads) { } } - // Stage 1 cont.: post-multiply the per-instance placement by - // T(+offset) so world position is preserved. Matrix arithmetic - // is in double; narrow to float at the end. + // Vertex rebasing cont.: post-multiply the per-instance + // PlacementTransformation by T(+offset) so world position is + // preserved. Matrix arithmetic is in double; narrow to float + // at the end. Eigen::Matrix4d mat_d = tri_elem->transformation().data()->ccomponents(); if (mesh_aabbs[local_mesh_id].has_offset) { diff --git a/src/ifcviewer/tests/test_federation.cpp b/src/ifcviewer/tests/test_federation.cpp index 42b2bfb4e6..223075c66f 100644 --- a/src/ifcviewer/tests/test_federation.cpp +++ b/src/ifcviewer/tests/test_federation.cpp @@ -305,8 +305,8 @@ TEST_CASE("load on malformed JSON fails with an error", "[federation]") { REQUIRE_FALSE(err.isEmpty()); } -TEST_CASE("config / origin / transform_intent round-trip through save+load", - "[federation]") { +TEST_CASE("config / federated_false_origin / model_transformation round-trip " + "through save+load", "[federation]") { ensureQApp(); QTemporaryDir tmp; REQUIRE(tmp.isValid()); @@ -322,18 +322,18 @@ TEST_CASE("config / origin / transform_intent round-trip through save+load", cfg.unit_prefix = ""; src.setConfig(cfg); - FederationOrigin org; + FederatedFalseOrigin org; org.xyz = Eigen::Vector3d(100.0, 200.0, 30.0); org.rz_deg = 45.0; - src.setOrigin(org); + src.setFederatedFalseOrigin(org); - ModelTransform xf; + ModelTransformation xf; xf.a_frame = AFrame::ModelLocal; xf.a = Eigen::Vector3d(1.0, 2.0, 3.0); xf.b = Eigen::Vector3d(4.0, 5.0, 6.0); xf.rxyz_deg = Eigen::Vector3d(90.0, 0.0, 0.0); xf.pivot = Eigen::Vector3d(7.0, 8.0, 9.0); - src.setModelTransform(id1, xf); + src.setModelTransformation(id1, xf); QString err; REQUIRE(src.save(fed_path, &err)); @@ -348,20 +348,21 @@ TEST_CASE("config / origin / transform_intent round-trip through save+load", REQUIRE(dst.config().unit_name == "FOOT"); REQUIRE(dst.config().unit_prefix == ""); - REQUIRE(dst.origin().xyz == org.xyz); - REQUIRE(dst.origin().rz_deg == 45.0); + REQUIRE(dst.federatedFalseOrigin().xyz == org.xyz); + REQUIRE(dst.federatedFalseOrigin().rz_deg == 45.0); REQUIRE(dst.models().size() == 1); const auto& m = dst.models()[0]; REQUIRE(m.id == id1); - REQUIRE(m.transform_intent.a_frame == AFrame::ModelLocal); - REQUIRE(m.transform_intent.a == xf.a); - REQUIRE(m.transform_intent.b == xf.b); - REQUIRE(m.transform_intent.rxyz_deg == xf.rxyz_deg); - REQUIRE(m.transform_intent.pivot == xf.pivot); + REQUIRE(m.model_transformation.a_frame == AFrame::ModelLocal); + REQUIRE(m.model_transformation.a == xf.a); + REQUIRE(m.model_transformation.b == xf.b); + REQUIRE(m.model_transformation.rxyz_deg == xf.rxyz_deg); + REQUIRE(m.model_transformation.pivot == xf.pivot); } -TEST_CASE("default ModelTransform is omitted from saved JSON", "[federation]") { +TEST_CASE("default ModelTransformation is omitted from saved JSON", + "[federation]") { ensureQApp(); QTemporaryDir tmp; REQUIRE(tmp.isValid()); @@ -377,17 +378,17 @@ TEST_CASE("default ModelTransform is omitted from saved JSON", "[federation]") { QJsonObject root = readJsonFile(fed_path); QJsonArray models = root.value("models").toArray(); REQUIRE(models.size() == 1); - REQUIRE_FALSE(models[0].toObject().contains("transform_intent")); + REQUIRE_FALSE(models[0].toObject().contains("model_transformation")); } -TEST_CASE("composeFederationOrigin moves the nominated point to the origin", +TEST_CASE("composeFederatedFalseOrigin moves the nominated point to the origin", "[federation][compose]") { FederationConfig cfg; // METRE, no prefix - FederationOrigin org; + FederatedFalseOrigin org; org.xyz = Eigen::Vector3d(10.0, 20.0, 5.0); org.rz_deg = 0.0; - Eigen::Matrix4d M = composeFederationOrigin(org, cfg); + Eigen::Matrix4d M = composeFederatedFalseOrigin(org, cfg); // The nominated point (10, 20, 5) should map to (0, 0, 0). Eigen::Vector4d p(10.0, 20.0, 5.0, 1.0); @@ -397,33 +398,33 @@ TEST_CASE("composeFederationOrigin moves the nominated point to the origin", REQUIRE(std::abs(r.z()) < 1e-9); } -TEST_CASE("composeFederationOrigin scales by federation unit", +TEST_CASE("composeFederatedFalseOrigin scales by federation unit", "[federation][compose]") { FederationConfig cfg; cfg.unit_name = "FOOT"; // 1 ft = 0.3048 m - FederationOrigin org; + FederatedFalseOrigin org; org.xyz = Eigen::Vector3d(1.0, 0.0, 0.0); // 1 foot in fed coords - Eigen::Matrix4d M = composeFederationOrigin(org, cfg); + Eigen::Matrix4d M = composeFederatedFalseOrigin(org, cfg); // Translation column should be -1 ft = -0.3048 m. REQUIRE(std::abs(M(0, 3) - (-0.3048)) < 1e-9); } -TEST_CASE("composeModelTransform with pivot=B keeps A landing on B", +TEST_CASE("composeModelTransformation with pivot=B keeps A landing on B", "[federation][compose]") { - // A in ModelGlobal frame, federation in metres, model has identity stage 2. + // A in ModelGlobal frame, federation in metres, identity CoordinateOperation. FederationConfig fed_cfg; // METRE ModelUnits mu; // 1.0 / 1.0 (already in metres) - Eigen::Matrix4d stage2 = Eigen::Matrix4d::Identity(); + Eigen::Matrix4d coord_op = Eigen::Matrix4d::Identity(); - ModelTransform xf; + ModelTransformation xf; xf.a_frame = AFrame::ModelGlobal; xf.a = Eigen::Vector3d(5.0, 0.0, 0.0); xf.b = Eigen::Vector3d(100.0, 50.0, 10.0); xf.rxyz_deg = Eigen::Vector3d(0.0, 0.0, 30.0); xf.pivot = xf.b; // pivot at B preserves A->B regardless of rotation - Eigen::Matrix4d M = composeModelTransform(xf, fed_cfg, mu, stage2); + Eigen::Matrix4d M = composeModelTransformation(xf, fed_cfg, mu, coord_op); Eigen::Vector4d a(xf.a.x(), xf.a.y(), xf.a.z(), 1.0); Eigen::Vector4d r = M * a;