diff --git a/src/ifcviewer-full/MainWindow.cpp b/src/ifcviewer-full/MainWindow.cpp index 7e261eaf3d..616ca0bb68 100644 --- a/src/ifcviewer-full/MainWindow.cpp +++ b/src/ifcviewer-full/MainWindow.cpp @@ -594,6 +594,30 @@ void MainWindow::applyFederatedFalseOriginToViewport() { viewport_->setFederatedFalseOrigin(M); } +void MainWindow::maybeGuessFederatedFalseOrigin(uint32_t mid) { + // Only auto-guess for untitled federations. A saved .ifcfed carries its + // authoritative origin (even if that happens to be the default), so we + // never silently overwrite it when the user re-adds a model. + if (!federation_->filePath().isEmpty()) return; + + // Skip once the origin is non-default — either the user edited it, a + // previous batch already guessed, or a load-from-file populated it. + // For a multi-file batch this means whichever model finishes first + // anchors the federation; the rest see a non-default origin and skip. + const FederatedFalseOrigin& cur = federation_->federatedFalseOrigin(); + const FederatedFalseOrigin def; + if (cur.xyz != def.xyz || cur.rz_deg != def.rz_deg) return; + + const Eigen::Matrix4d* placement = loader_->firstPlacement(mid); + const ModelGeoref* gr = loader_->modelGeoref(mid); + if (placement == nullptr || gr == nullptr) return; + + const FederatedFalseOrigin guess = guessFederatedFalseOrigin( + *placement, *gr, federation_->config(), + AppSettings::instance().applyCoordinateOperation()); + federation_->setFederatedFalseOrigin(guess); +} + void MainWindow::onLoadedFromSidecar(uint32_t mid, qint64 elapsed_ms) { progress_bar_->setVisible(false); status_label_->setText(QString("%1 elements across %2 model(s) — loaded from cache in %3") @@ -606,6 +630,7 @@ void MainWindow::onLoadedFromSidecar(uint32_t mid, qint64 elapsed_ms) { // ModelTransformation immediately rather than waiting for the // (possibly never-arriving) data-source load. applyCoordinateOperationToViewport(mid); + maybeGuessFederatedFalseOrigin(mid); } void MainWindow::onStreamedElementsReady(uint32_t /*mid*/, std::vector elements) { @@ -714,9 +739,8 @@ void MainWindow::onLoadedFromStream(uint32_t mid, qint64 elapsed_ms) { .arg(loader_->modelCount()) .arg(formatElapsed(elapsed_ms))); - // Stream path: the IFC is owned by the streamer, so georef is - // computable now. (Sidecar-hit models defer to onDataSourceReady.) applyCoordinateOperationToViewport(mid); + maybeGuessFederatedFalseOrigin(mid); writeSidecarForModel(mid); } diff --git a/src/ifcviewer-full/MainWindow.h b/src/ifcviewer-full/MainWindow.h index 85bd6b2473..3910f4fd62 100644 --- a/src/ifcviewer-full/MainWindow.h +++ b/src/ifcviewer-full/MainWindow.h @@ -117,6 +117,15 @@ private: // Push the federation-wide FederatedFalseOrigin (stage 3) matrix to // the viewport. Affects every loaded model. void applyFederatedFalseOriginToViewport(); + + // For an untitled federation whose FederatedFalseOrigin is still at + // its default, derive a sensible origin from `mid`'s first instance + // placement + georef and push it via Federation. Idempotent: a + // non-default origin (user-edited, already guessed by a sibling load + // in the same batch, or loaded from a saved .ifcfed) is left + // untouched, so multi-file batches naturally anchor on whichever + // model finishes first. + void maybeGuessFederatedFalseOrigin(uint32_t mid); QString formatElapsed(qint64 ms) const; ViewportWindow* viewport_ = nullptr; diff --git a/src/ifcviewer/Federation.cpp b/src/ifcviewer/Federation.cpp index ba4b33dddf..892266e3a7 100644 --- a/src/ifcviewer/Federation.cpp +++ b/src/ifcviewer/Federation.cpp @@ -135,6 +135,36 @@ ModelGeoref computeModelGeoref(ifcopenshell::file* ifc_file) { return out; } +FederatedFalseOrigin +guessFederatedFalseOrigin(const Eigen::Matrix4d& first_placement_meters, + const ModelGeoref& georef, + const FederationConfig& fed_cfg, + bool apply_coordinate_operation) { + Eigen::Vector3d t_m = first_placement_meters.block<3, 1>(0, 3); + + const bool use_coord_op = + apply_coordinate_operation && 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. + // helmertMetersFromParameters built that block as R_z(theta)·diag(fx,fy,fz) + // with theta = atan2(xao, xaa); xaxis2angle is `-theta` in degrees. + if (use_coord_op) { + const Eigen::Matrix4d& M = georef.coordinate_operation_meters; + out.rz_deg = xaxis2angleDeg(M(0, 0), M(1, 0)); + } + return out; +} + Eigen::Matrix4d composeModelTransformation(const ModelTransformation& xf, const FederationConfig& fed_cfg, const ModelUnits& model_units, diff --git a/src/ifcviewer/Federation.h b/src/ifcviewer/Federation.h index 87562168c1..ce0e715df0 100644 --- a/src/ifcviewer/Federation.h +++ b/src/ifcviewer/Federation.h @@ -28,6 +28,7 @@ #include #include +#include #include #include @@ -125,6 +126,27 @@ struct ModelGeoref { // 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 InstanceCpu cache + ModelGeoref). +// +// Position: `first_placement_meters` is the model's "anchor" placement — +// typically the first instance's `placement_transformation`, which the +// iterator already produces in metres (its `convert-back-units` default +// is false). The translation is optionally lifted through +// `georef.coordinate_operation_meters` (controlled by +// `apply_coordinate_operation`), then expressed in the federation unit. +// +// Rotation: read directly from `georef.coordinate_operation_meters` when +// `apply_coordinate_operation && has_coordinate_operation` (this is the +// helmert grid-north angle). Otherwise zero. Anticlockwise positive. +FederatedFalseOrigin +guessFederatedFalseOrigin(const Eigen::Matrix4d& first_placement_meters, + const ModelGeoref& georef, + const FederationConfig& fed_cfg, + bool apply_coordinate_operation); + // 1 federation_unit -> N metres. double federationUnitToMeters(const FederationConfig&); diff --git a/src/ifcviewer/Geolocation.cpp b/src/ifcviewer/Geolocation.cpp index 80833ec062..e9b6889003 100644 --- a/src/ifcviewer/Geolocation.cpp +++ b/src/ifcviewer/Geolocation.cpp @@ -18,6 +18,7 @@ ********************************************************************************/ #include "Geolocation.h" +#include "Placement.h" #include "../ifcparse/express.h" #include "../ifcparse/file.h" @@ -30,59 +31,6 @@ namespace { -// IfcAxis2Placement3D / IfcAxis2PlacementLinear -> column-major 4x4 matrix. -// Mirrors ifcopenshell.util.placement.a2p + get_axis2placement, but only the -// branches needed for IfcGeometricRepresentationContext.WorldCoordinateSystem. -std::optional getAxis2Placement(express::Base placement) { - if (!placement) return std::nullopt; - const auto& decl = placement.declaration(); - if (!(decl.is("IfcAxis2Placement3D") || decl.is("IfcAxis2PlacementLinear"))) { - return std::nullopt; - } - - auto entity = placement.as(); - - Eigen::Vector3d z(0.0, 0.0, 1.0); - Eigen::Vector3d x(1.0, 0.0, 0.0); - - auto axis_attr = entity.get("Axis"); - if (!axis_attr.isNull()) { - express::Base axis = axis_attr; - std::vector dr = - axis.as().get("DirectionRatios"); - if (dr.size() >= 3) z = Eigen::Vector3d(dr[0], dr[1], dr[2]); - } - - auto refdir_attr = entity.get("RefDirection"); - if (!refdir_attr.isNull()) { - express::Base refdir = refdir_attr; - std::vector dr = - refdir.as().get("DirectionRatios"); - if (dr.size() >= 3) x = Eigen::Vector3d(dr[0], dr[1], dr[2]); - } - - auto loc_attr = entity.get("Location"); - if (loc_attr.isNull()) return std::nullopt; - express::Base location = loc_attr; - auto coords_attr = location.as().get("Coordinates"); - if (coords_attr.isNull()) return std::nullopt; - std::vector coords = coords_attr; - if (coords.size() < 3) return std::nullopt; - - Eigen::Vector3d xn = x.normalized(); - Eigen::Vector3d zn = z.normalized(); - Eigen::Vector3d yn = zn.cross(xn).normalized(); - - 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(0, 3) = coords[0]; - m(1, 3) = coords[1]; - m(2, 3) = coords[2]; - return m; -} - // 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 @@ -223,6 +171,10 @@ std::optional getWcs(ifcopenshell::file* ifc_file) { } } if (!found) return std::nullopt; + const auto& decl = wcs.declaration(); + if (!(decl.is("IfcAxis2Placement3D") || decl.is("IfcAxis2PlacementLinear"))) { + return std::nullopt; + } return getAxis2Placement(wcs); } @@ -311,3 +263,8 @@ std::optional getMapUnit(ifcopenshell::file* ifc_file) { 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); +} diff --git a/src/ifcviewer/Geolocation.h b/src/ifcviewer/Geolocation.h index d5f14c196e..13eaecd8b1 100644 --- a/src/ifcviewer/Geolocation.h +++ b/src/ifcviewer/Geolocation.h @@ -100,4 +100,9 @@ Eigen::Matrix4d helmertMetersFromParameters(const HelmertTransformation& params, // calculateUnitScale(file, "LENGTHUNIT") in that case. std::optional 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 diff --git a/src/ifcviewer/Placement.cpp b/src/ifcviewer/Placement.cpp new file mode 100644 index 0000000000..6d0bd7ecb0 --- /dev/null +++ b/src/ifcviewer/Placement.cpp @@ -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 . * + * * + ********************************************************************************/ + +#include "Placement.h" + +#include "../ifcparse/instance_data.h" +#include "../ifcparse/schema.h" + +#include + +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 readDirectionRatios(const express::Base& dir) { + if (!dir) return {}; + auto attr = dir.as().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(); + + 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().get("Coordinates"); + if (coords_attr.isNull()) return Eigen::Matrix4d::Identity(); + std::vector 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().get("Coordinates"); + if (coords_attr.isNull()) return Eigen::Matrix4d::Identity(); + std::vector 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().get("Coordinates"); + if (coords_attr.isNull()) return Eigen::Matrix4d::Identity(); + std::vector 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(); + 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); +} diff --git a/src/ifcviewer/Placement.h b/src/ifcviewer/Placement.h new file mode 100644 index 0000000000..346e20651c --- /dev/null +++ b/src/ifcviewer/Placement.h @@ -0,0 +1,50 @@ +/******************************************************************************** + * * + * 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 . * + * * + ********************************************************************************/ + +// 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 + +// 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 diff --git a/src/ifcviewer/SceneLoader.cpp b/src/ifcviewer/SceneLoader.cpp index e8184ff7fd..b37c4cb6f1 100644 --- a/src/ifcviewer/SceneLoader.cpp +++ b/src/ifcviewer/SceneLoader.cpp @@ -81,6 +81,12 @@ const ModelGeoref* SceneLoader::modelGeoref(uint32_t mid) { return &m.georef; } +const Eigen::Matrix4d* SceneLoader::firstPlacement(uint32_t mid) const { + auto it = models_.find(mid); + if (it == models_.end() || !it->second.has_first_placement) return nullptr; + return &it->second.first_placement; +} + std::vector SceneLoader::addFiles(const QStringList& paths) { std::vector assigned; assigned.reserve(paths.size()); @@ -214,6 +220,14 @@ void SceneLoader::applySidecarData(uint32_t mid, SidecarData data) { model.has_georef = true; } + if (!data.instances.empty() && !model.has_first_placement) { + using Mat4fCol = Eigen::Matrix; + model.first_placement = + Eigen::Map(data.instances[0].placement_transformation) + .cast(); + model.has_first_placement = true; + } + std::vector elements = std::move(data.elements); std::string stbl = std::move(data.string_table); @@ -302,6 +316,15 @@ void SceneLoader::onStreamerMeshReady(MeshChunk chunk) { } void SceneLoader::onStreamerInstanceReady(InstanceChunk chunk) { + if (loading_model_id_ != 0) { + auto it = models_.find(loading_model_id_); + if (it != models_.end() && !it->second.has_first_placement) { + using Mat4fCol = Eigen::Matrix; + it->second.first_placement = + Eigen::Map(chunk.transform).cast(); + it->second.has_first_placement = true; + } + } viewport_->uploadInstanceChunk(chunk); } diff --git a/src/ifcviewer/SceneLoader.h b/src/ifcviewer/SceneLoader.h index 6efca5a555..71459f8d6a 100644 --- a/src/ifcviewer/SceneLoader.h +++ b/src/ifcviewer/SceneLoader.h @@ -72,6 +72,14 @@ public: // sidecar-hit path before the data-source thread populates the streamer). const ModelGeoref* modelGeoref(uint32_t mid); + // The placement_transformation (in metres, column-major 4x4) of the + // first instance the loader saw for `mid` — captured from the streamer's + // first InstanceChunk during a stream load, or from the cached + // InstanceCpu[0] on a sidecar hit. Returns nullptr until at least one + // instance has been observed. Used by the federation false-origin + // auto-guess to anchor the model without re-parsing the IFC. + const Eigen::Matrix4d* firstPlacement(uint32_t mid) const; + signals: void progressChanged(int percent); void loadStarted(uint32_t mid, QString display_name); @@ -125,6 +133,12 @@ private: // streamer has its IFC file loaded. ModelGeoref georef; bool has_georef = false; + + // The first instance's placement_transformation (in metres) — set + // once per model from either the sidecar's InstanceCpu[0] or the + // streamer's first InstanceChunk. + Eigen::Matrix4d first_placement = Eigen::Matrix4d::Identity(); + bool has_first_placement = false; }; void startNextLoad();