ifcviewer: rename stage1/2/3/4 to their proper IFC-mapped names

Replace the placeholder "stage1/2/3/4" terminology with names that
mirror the IFC concepts each step represents:

  stage 1 -> PlacementTransformation
            (per-instance, derived from IfcObjectPlacement)
  stage 2 -> CoordinateOperation
            (per-model, IfcCoordinateOperation / IfcMapConversion)
  stage 3 -> FederatedFalseOrigin
            (federation-wide, user-nominated)
  stage 4 -> ModelTransformation
            (per-model, user-authored within the federation)

API renames:
  FederationOrigin            -> FederatedFalseOrigin
  ModelTransform              -> ModelTransformation
  composeFederationOrigin     -> composeFederatedFalseOrigin
  composeModelTransform       -> composeModelTransformation
  Federation::setOrigin       -> Federation::setFederatedFalseOrigin
  Federation::setModelTransform -> Federation::setModelTransformation
  Federation::origin()        -> Federation::federatedFalseOrigin()
  Federation::Model::transform_intent -> ::model_transformation
  ModelGeoref::stage2_meters  -> ::coordinate_operation_meters
  ModelGeoref::has_stage2     -> ::has_coordinate_operation

JSON keys in .ifcfed renamed in lockstep:
  origin                -> federated_false_origin
  transform_intent      -> model_transformation

The streamer's per-mesh "stage 1 vertex rebasing" comment is reframed:
the rebase isn't its own stage — it's a precision optimisation applied
inside the PlacementTransformation step.

All 36 ctest cases pass under the new names.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
Dion Moult
2026-05-01 20:06:06 +10:00
parent 600d3a3460
commit 92c3b4c308
4 changed files with 143 additions and 126 deletions
+41 -38
View File
@@ -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;