mirror of
https://github.com/IfcOpenShell/IfcOpenShell.git
synced 2026-08-17 10:59:17 +00:00
ifcviewer: add stage 3+4 data model and compose helpers to Federation
Adds the structs that were briefly in src/ifcviewer/Federation.{h,cpp}
two commits ago, now folded into the merged Federation alongside the
file persistence layer:
- FederationConfig: federation-wide unit ({prefix, name}). Default
METRE; one-of an IfcSIUnit name with optional prefix or an
IfcConversionBasedUnit name.
- FederationOrigin: stage 3 — XYZ in federation unit + Z-rot.
Composes to R_z · T(-xyz_meters), nominating a point as origin.
- AFrame + ModelTransform: stage 4 intent — A (model project or
map unit, per a_frame), B and pivot (federation unit), full
intrinsic-XYZ Euler rotation in degrees.
- ModelUnits: per-model project_length_to_meters / map_unit_to_meters
cached at load time.
Free functions composeFederationOrigin and composeModelTransform
return Eigen::Matrix4d in metres. composeModelTransform takes the
model's stage-2 georef matrix so it can lift `a` into metres when
authored in ModelLocal.
Federation gains config_, origin_ members + setters that emit
dirtyChanged. Each Model carries a transform_intent. JSON I/O
emits config / origin always; transform_intent only when non-default.
Schema stays "ifcfed/1" — additive, optional, sane defaults.
Five new tests: round-trip of the new fields, default-omission
behaviour, two compose smoke tests for FederationOrigin, and one
verifying the "pivot at B preserves A→B" invariant of
composeModelTransform. All 36 ctest cases pass.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
@@ -304,3 +304,130 @@ TEST_CASE("load on malformed JSON fails with an error", "[federation]") {
|
||||
REQUIRE_FALSE(fed.load(bad, &warnings, &err));
|
||||
REQUIRE_FALSE(err.isEmpty());
|
||||
}
|
||||
|
||||
TEST_CASE("config / origin / transform_intent round-trip through save+load",
|
||||
"[federation]") {
|
||||
ensureQApp();
|
||||
QTemporaryDir tmp;
|
||||
REQUIRE(tmp.isValid());
|
||||
|
||||
QString src1 = writeStubFile(tmp.filePath("models/wall.ifc"));
|
||||
QString fed_path = tmp.filePath("project.ifcfed");
|
||||
|
||||
Federation src;
|
||||
QString id1 = src.addModel(src1, "Wall");
|
||||
|
||||
FederationConfig cfg;
|
||||
cfg.unit_name = "FOOT";
|
||||
cfg.unit_prefix = "";
|
||||
src.setConfig(cfg);
|
||||
|
||||
FederationOrigin org;
|
||||
org.xyz = Eigen::Vector3d(100.0, 200.0, 30.0);
|
||||
org.rz_deg = 45.0;
|
||||
src.setOrigin(org);
|
||||
|
||||
ModelTransform 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);
|
||||
|
||||
QString err;
|
||||
REQUIRE(src.save(fed_path, &err));
|
||||
REQUIRE(err.isEmpty());
|
||||
|
||||
Federation dst;
|
||||
QStringList warnings;
|
||||
REQUIRE(dst.load(fed_path, &warnings, &err));
|
||||
REQUIRE(err.isEmpty());
|
||||
REQUIRE(warnings.isEmpty());
|
||||
|
||||
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.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);
|
||||
}
|
||||
|
||||
TEST_CASE("default ModelTransform is omitted from saved JSON", "[federation]") {
|
||||
ensureQApp();
|
||||
QTemporaryDir tmp;
|
||||
REQUIRE(tmp.isValid());
|
||||
|
||||
QString src1 = writeStubFile(tmp.filePath("models/wall.ifc"));
|
||||
QString fed_path = tmp.filePath("project.ifcfed");
|
||||
|
||||
Federation src;
|
||||
src.addModel(src1, "Wall");
|
||||
QString err;
|
||||
REQUIRE(src.save(fed_path, &err));
|
||||
|
||||
QJsonObject root = readJsonFile(fed_path);
|
||||
QJsonArray models = root.value("models").toArray();
|
||||
REQUIRE(models.size() == 1);
|
||||
REQUIRE_FALSE(models[0].toObject().contains("transform_intent"));
|
||||
}
|
||||
|
||||
TEST_CASE("composeFederationOrigin moves the nominated point to the origin",
|
||||
"[federation][compose]") {
|
||||
FederationConfig cfg; // METRE, no prefix
|
||||
FederationOrigin org;
|
||||
org.xyz = Eigen::Vector3d(10.0, 20.0, 5.0);
|
||||
org.rz_deg = 0.0;
|
||||
|
||||
Eigen::Matrix4d M = composeFederationOrigin(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);
|
||||
Eigen::Vector4d r = M * p;
|
||||
REQUIRE(std::abs(r.x()) < 1e-9);
|
||||
REQUIRE(std::abs(r.y()) < 1e-9);
|
||||
REQUIRE(std::abs(r.z()) < 1e-9);
|
||||
}
|
||||
|
||||
TEST_CASE("composeFederationOrigin scales by federation unit",
|
||||
"[federation][compose]") {
|
||||
FederationConfig cfg;
|
||||
cfg.unit_name = "FOOT"; // 1 ft = 0.3048 m
|
||||
FederationOrigin org;
|
||||
org.xyz = Eigen::Vector3d(1.0, 0.0, 0.0); // 1 foot in fed coords
|
||||
|
||||
Eigen::Matrix4d M = composeFederationOrigin(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",
|
||||
"[federation][compose]") {
|
||||
// A in ModelGlobal frame, federation in metres, model has identity stage 2.
|
||||
FederationConfig fed_cfg; // METRE
|
||||
ModelUnits mu; // 1.0 / 1.0 (already in metres)
|
||||
Eigen::Matrix4d stage2 = Eigen::Matrix4d::Identity();
|
||||
|
||||
ModelTransform 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::Vector4d a(xf.a.x(), xf.a.y(), xf.a.z(), 1.0);
|
||||
Eigen::Vector4d r = M * a;
|
||||
REQUIRE(std::abs(r.x() - xf.b.x()) < 1e-9);
|
||||
REQUIRE(std::abs(r.y() - xf.b.y()) < 1e-9);
|
||||
REQUIRE(std::abs(r.z() - xf.b.z()) < 1e-9);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user