diff --git a/src/ifcviewer-full/Federation.cpp b/src/ifcviewer-full/Federation.cpp
new file mode 100644
index 0000000000..a72f783103
--- /dev/null
+++ b/src/ifcviewer-full/Federation.cpp
@@ -0,0 +1,308 @@
+/********************************************************************************
+ * *
+ * 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 "Federation.h"
+
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+
+namespace {
+constexpr const char* kSchema = "ifcfed/1";
+
+QString resolvePath(const QString& fed_dir, const QString& stored) {
+ if (stored.isEmpty()) return stored;
+ QFileInfo fi(stored);
+ if (fi.isAbsolute()) return QDir::cleanPath(stored);
+ return QDir::cleanPath(QDir(fed_dir).absoluteFilePath(stored));
+}
+
+// Returns abs_path relative to fed_dir if abs_path lives under fed_dir,
+// otherwise returns abs_path unchanged.
+QString relativizePath(const QString& fed_dir, const QString& abs_path) {
+ QString fed_canon = QDir::cleanPath(fed_dir);
+ QString abs_canon = QDir::cleanPath(abs_path);
+ if (!fed_canon.endsWith('/')) fed_canon += '/';
+ if (abs_canon.startsWith(fed_canon)) {
+ return QDir(fed_canon).relativeFilePath(abs_canon);
+ }
+ return abs_canon;
+}
+} // namespace
+
+Federation::Federation(QObject* parent) : QObject(parent) {}
+
+QString Federation::generateId() {
+ return QUuid::createUuid().toString(QUuid::WithoutBraces);
+}
+
+bool Federation::isFederationPath(const QString& path) {
+ return path.endsWith(".ifcfed", Qt::CaseInsensitive);
+}
+
+void Federation::clear() {
+ file_path_.clear();
+ name_.clear();
+ created_ = QDateTime();
+ modified_ = QDateTime();
+ models_.clear();
+ has_home_view_ = false;
+ home_view_ = HomeView{};
+ setDirty(false);
+}
+
+void Federation::markClean() {
+ setDirty(false);
+}
+
+void Federation::setDirty(bool d) {
+ if (dirty_ == d) return;
+ dirty_ = d;
+ emit dirtyChanged(d);
+}
+
+const Federation::Model* Federation::findById(const QString& fed_id) const {
+ for (const auto& m : models_) {
+ if (m.id == fed_id) return &m;
+ }
+ return nullptr;
+}
+
+QString Federation::addModel(const QString& source_path,
+ const QString& display_name) {
+ if (source_path.isEmpty()) return {};
+ if (isFederationPath(source_path)) return {}; // no nested federations
+
+ Model m;
+ m.id = generateId();
+ m.display_name = display_name.isEmpty()
+ ? QFileInfo(source_path).fileName()
+ : display_name;
+ m.source_kind = "local";
+ m.source_path = QDir::cleanPath(QFileInfo(source_path).absoluteFilePath());
+ models_.push_back(std::move(m));
+ setDirty(true);
+ return models_.back().id;
+}
+
+void Federation::removeModel(const QString& fed_id) {
+ for (auto it = models_.begin(); it != models_.end(); ++it) {
+ if (it->id == fed_id) {
+ models_.erase(it);
+ setDirty(true);
+ return;
+ }
+ }
+}
+
+void Federation::setHomeView(const HomeView& hv) {
+ home_view_ = hv;
+ has_home_view_ = true;
+ setDirty(true);
+}
+
+void Federation::clearHomeView() {
+ if (!has_home_view_) return;
+ has_home_view_ = false;
+ home_view_ = HomeView{};
+ setDirty(true);
+}
+
+bool Federation::load(const QString& path,
+ QStringList* warnings,
+ QString* err) {
+ QFile f(path);
+ if (!f.open(QIODevice::ReadOnly)) {
+ if (err) *err = QString("Cannot open %1: %2").arg(path, f.errorString());
+ return false;
+ }
+ QByteArray bytes = f.readAll();
+ f.close();
+
+ QJsonParseError pe;
+ QJsonDocument doc = QJsonDocument::fromJson(bytes, &pe);
+ if (doc.isNull() || !doc.isObject()) {
+ if (err) *err = QString("Parse error in %1: %2").arg(path, pe.errorString());
+ return false;
+ }
+ QJsonObject root = doc.object();
+
+ clear();
+ file_path_ = QDir::cleanPath(QFileInfo(path).absoluteFilePath());
+ QString fed_dir = QFileInfo(file_path_).absolutePath();
+
+ QString schema = root.value("schema").toString();
+ if (schema != kSchema && warnings) {
+ *warnings << QString("Unknown schema '%1' (expected '%2'); attempting to load anyway.")
+ .arg(schema, kSchema);
+ }
+
+ name_ = root.value("name").toString();
+ created_ = QDateTime::fromString(root.value("created").toString(), Qt::ISODate);
+ modified_ = QDateTime::fromString(root.value("modified").toString(), Qt::ISODate);
+
+ QJsonArray arr = root.value("models").toArray();
+ for (int i = 0; i < arr.size(); ++i) {
+ if (!arr[i].isObject()) {
+ if (warnings) *warnings << QString("models[%1] is not an object; skipping.").arg(i);
+ continue;
+ }
+ QJsonObject mo = arr[i].toObject();
+
+ Model m;
+ m.id = mo.value("id").toString();
+ if (m.id.isEmpty()) m.id = generateId();
+ m.display_name = mo.value("display_name").toString();
+
+ QJsonObject so = mo.value("source").toObject();
+ m.source_kind = so.value("kind").toString("local");
+ if (m.source_kind != "local") {
+ if (warnings)
+ *warnings << QString("models[%1]: unsupported source kind '%2'; entry kept but not loaded.")
+ .arg(i).arg(m.source_kind);
+ // Keep raw stored path so save() round-trips correctly.
+ m.source_path = so.value("path").toString();
+ models_.push_back(std::move(m));
+ continue;
+ }
+
+ QString stored = so.value("path").toString();
+ if (stored.isEmpty()) {
+ if (warnings) *warnings << QString("models[%1]: missing source.path; skipping.").arg(i);
+ continue;
+ }
+ m.source_path = resolvePath(fed_dir, stored);
+
+ if (m.display_name.isEmpty())
+ m.display_name = QFileInfo(m.source_path).fileName();
+
+ QJsonValue tv = mo.value("transform");
+ if (tv.isArray()) {
+ QJsonArray ta = tv.toArray();
+ if (ta.size() == 16) {
+ for (int k = 0; k < 16; ++k) m.transform[k] = float(ta[k].toDouble());
+ m.has_transform = true;
+ } else if (warnings) {
+ *warnings << QString("models[%1]: transform must be 16 floats; ignored.").arg(i);
+ }
+ }
+
+ QJsonValue vv = mo.value("visible");
+ if (vv.isBool()) m.visible = vv.toBool();
+
+ models_.push_back(std::move(m));
+ }
+
+ QJsonValue hv = root.value("home_view");
+ if (hv.isObject()) {
+ QJsonObject ho = hv.toObject();
+ QJsonArray ta = ho.value("target").toArray();
+ HomeView v;
+ if (ta.size() == 3) {
+ v.target = QVector3D(float(ta[0].toDouble()),
+ float(ta[1].toDouble()),
+ float(ta[2].toDouble()));
+ }
+ v.distance = float(ho.value("distance").toDouble(50.0));
+ v.yaw = float(ho.value("yaw").toDouble(45.0));
+ v.pitch = float(ho.value("pitch").toDouble(30.0));
+ home_view_ = v;
+ has_home_view_ = true;
+ }
+
+ setDirty(false);
+ return true;
+}
+
+bool Federation::save(const QString& path, QString* err) {
+ QString abs_path = QDir::cleanPath(QFileInfo(path).absoluteFilePath());
+ QString fed_dir = QFileInfo(abs_path).absolutePath();
+
+ QJsonObject root;
+ root["schema"] = kSchema;
+ if (!name_.isEmpty()) root["name"] = name_;
+
+ if (!created_.isValid()) created_ = QDateTime::currentDateTimeUtc();
+ modified_ = QDateTime::currentDateTimeUtc();
+ root["created"] = created_.toUTC().toString(Qt::ISODate);
+ root["modified"] = modified_.toUTC().toString(Qt::ISODate);
+
+ QJsonArray arr;
+ for (const auto& m : models_) {
+ QJsonObject mo;
+ mo["id"] = m.id;
+ mo["display_name"] = m.display_name;
+
+ QJsonObject so;
+ so["kind"] = m.source_kind;
+ if (m.source_kind == "local") {
+ so["path"] = relativizePath(fed_dir, m.source_path);
+ } else {
+ // Round-trip raw value for unsupported kinds.
+ so["path"] = m.source_path;
+ }
+ mo["source"] = so;
+
+ if (m.has_transform) {
+ QJsonArray ta;
+ for (float v : m.transform) ta.append(double(v));
+ mo["transform"] = ta;
+ }
+ if (!m.visible) mo["visible"] = false;
+
+ arr.append(mo);
+ }
+ root["models"] = arr;
+
+ if (has_home_view_) {
+ QJsonObject ho;
+ QJsonArray ta;
+ ta.append(double(home_view_.target.x()));
+ ta.append(double(home_view_.target.y()));
+ ta.append(double(home_view_.target.z()));
+ ho["target"] = ta;
+ ho["distance"] = double(home_view_.distance);
+ ho["yaw"] = double(home_view_.yaw);
+ ho["pitch"] = double(home_view_.pitch);
+ root["home_view"] = ho;
+ } else {
+ root["home_view"] = QJsonValue(); // null
+ }
+
+ QSaveFile f(abs_path);
+ if (!f.open(QIODevice::WriteOnly | QIODevice::Truncate)) {
+ if (err) *err = QString("Cannot write %1: %2").arg(abs_path, f.errorString());
+ return false;
+ }
+ f.write(QJsonDocument(root).toJson(QJsonDocument::Indented));
+ if (!f.commit()) {
+ if (err) *err = QString("Failed to commit %1: %2").arg(abs_path, f.errorString());
+ return false;
+ }
+
+ file_path_ = abs_path;
+ setDirty(false);
+ return true;
+}
diff --git a/src/ifcviewer-full/Federation.h b/src/ifcviewer-full/Federation.h
new file mode 100644
index 0000000000..21bb6ba3bd
--- /dev/null
+++ b/src/ifcviewer-full/Federation.h
@@ -0,0 +1,105 @@
+/********************************************************************************
+ * *
+ * 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 . *
+ * *
+ ********************************************************************************/
+
+#ifndef FEDERATION_H
+#define FEDERATION_H
+
+#include
+#include
+#include
+#include
+#include
+
+#include
+#include
+
+// In-memory representation of an .ifcfed file (IFC federation).
+//
+// A federation is a named, ordered list of model sources plus an optional
+// "home view" camera state. Source paths can be relative (resolved against
+// the .ifcfed's directory) or absolute. Save() reserialises paths relative
+// when they live under the federation file's directory tree, absolute
+// otherwise — Save As recomputes against the new location.
+//
+// Round-trip-only fields today (no UI to edit, but preserved across load/
+// save): per-model `transform` (4x4, column-major), per-model `visible`,
+// future cloud `source.kind`s.
+class Federation : public QObject {
+ Q_OBJECT
+public:
+ struct HomeView {
+ QVector3D target;
+ float distance = 50.0f;
+ float yaw = 45.0f; // degrees
+ float pitch = 30.0f; // degrees
+ };
+
+ struct Model {
+ QString id; // stable, persisted
+ QString display_name;
+ QString source_kind = "local"; // future: "http", "speckle", ...
+ QString source_path; // resolved absolute when kind == "local"
+ bool has_transform = false;
+ std::array transform{}; // column-major; identity when !has_transform
+ bool visible = true;
+ };
+
+ explicit Federation(QObject* parent = nullptr);
+
+ // Round-trip
+ bool load(const QString& path, QStringList* warnings, QString* err);
+ bool save(const QString& path, QString* err);
+
+ // Mutations
+ void clear();
+ QString addModel(const QString& source_path,
+ const QString& display_name = QString());
+ void removeModel(const QString& fed_id);
+ void setHomeView(const HomeView& hv);
+ void clearHomeView();
+
+ // Accessors
+ const std::vector& models() const { return models_; }
+ const Model* findById(const QString& fed_id) const;
+ bool isDirty() const { return dirty_; }
+ void markClean();
+ QString filePath() const { return file_path_; }
+ QString name() const { return name_; }
+ bool hasHomeView() const { return has_home_view_; }
+ const HomeView& homeView() const { return home_view_; }
+
+signals:
+ void dirtyChanged(bool dirty);
+
+private:
+ void setDirty(bool d);
+ static QString generateId();
+ static bool isFederationPath(const QString& path);
+
+ QString file_path_;
+ QString name_;
+ QDateTime created_;
+ QDateTime modified_;
+ std::vector models_;
+ bool has_home_view_ = false;
+ HomeView home_view_;
+ bool dirty_ = false;
+};
+
+#endif // FEDERATION_H
diff --git a/src/ifcviewer-full/MainWindow.cpp b/src/ifcviewer-full/MainWindow.cpp
index e36af95670..3105af7cf5 100644
--- a/src/ifcviewer-full/MainWindow.cpp
+++ b/src/ifcviewer-full/MainWindow.cpp
@@ -19,11 +19,13 @@
#include "MainWindow.h"
#include "AppSettings.h"
+#include "Federation.h"
#include "SettingsWindow.h"
#include "LodBuilder.h"
#include "SidecarCache.h"
#include
+#include
#include
#include
#include
@@ -40,6 +42,11 @@ MainWindow::MainWindow(QWidget* parent)
setupUi();
setupMenus();
+ federation_ = new Federation(this);
+ connect(federation_, &Federation::dirtyChanged, this, [this](bool dirty) {
+ setWindowModified(dirty);
+ });
+
loader_ = new SceneLoader(viewport_, this);
connect(loader_, &SceneLoader::loadStarted,
this, &MainWindow::onLoadStarted);
@@ -82,7 +89,7 @@ MainWindow::MainWindow(QWidget* parent)
if (!show) stats_label_->clear();
});
- setWindowTitle("IfcViewer");
+ updateWindowTitle();
resize(1400, 900);
}
@@ -132,12 +139,32 @@ void MainWindow::setupUi() {
void MainWindow::setupMenus() {
auto* file_menu = menuBar()->addMenu("&File");
- auto* open_action = file_menu->addAction("&Add Files...", this, &MainWindow::onFileOpen);
- open_action->setShortcut(QKeySequence::Open);
+ file_menu->addAction("&New Federation",
+ this, &MainWindow::onFederationNew,
+ QKeySequence::New);
+ file_menu->addAction("&Open Federation...",
+ this, &MainWindow::onFederationOpen,
+ QKeySequence::Open);
+ file_menu->addSeparator();
+ file_menu->addAction("&Add Files...",
+ this, &MainWindow::onFileOpen,
+ QKeySequence("Ctrl+Shift+O"));
file_menu->addAction("Add &Database...", this, &MainWindow::onDatabaseOpen);
+ file_menu->addSeparator();
+ file_menu->addAction("&Save Federation",
+ this, &MainWindow::onFederationSave,
+ QKeySequence::Save);
+ file_menu->addAction("Save Federation &As...",
+ this, &MainWindow::onFederationSaveAs,
+ QKeySequence::SaveAs);
+ file_menu->addSeparator();
file_menu->addAction("&Settings...", this, &MainWindow::onFileSettings);
file_menu->addSeparator();
file_menu->addAction("&Quit", QKeySequence::Quit, qApp, &QApplication::quit);
+
+ auto* view_menu = menuBar()->addMenu("&View");
+ view_menu->addAction("Set &Home View", this, &MainWindow::onSetHomeView);
+ view_menu->addAction("&Go to Home View", this, &MainWindow::onGoHomeView);
}
void MainWindow::onFileOpen() {
@@ -170,18 +197,189 @@ void MainWindow::onFileSettings() {
}
void MainWindow::addFiles(const QStringList& paths) {
+ QStringList accepted_paths;
+ QStringList accepted_fed_ids;
+ for (const auto& p : paths) {
+ QString fed_id = federation_->addModel(p);
+ if (fed_id.isEmpty()) continue; // .ifcfed or empty path — silently skipped
+ accepted_paths << p;
+ accepted_fed_ids << fed_id;
+ }
+ loadModelsFromPaths(accepted_paths, accepted_fed_ids);
+ updateWindowTitle();
+}
+
+void MainWindow::loadModelsFromPaths(const QStringList& paths,
+ const QStringList& fed_ids) {
+ if (paths.isEmpty()) return;
auto ids = loader_->addFiles(paths);
for (int i = 0; i < paths.size() && i < static_cast(ids.size()); ++i) {
- uint32_t id = ids[i];
+ uint32_t mid = ids[i];
+ const QString& fed_id = fed_ids[i];
+ fed_id_to_model_id_[fed_id] = mid;
+ model_id_to_fed_id_[mid] = fed_id;
+
QString display = QFileInfo(paths[i]).fileName();
auto* root = new QTreeWidgetItem(element_tree_);
root->setText(0, display);
root->setText(1, "IFC Model");
root->setData(0, Qt::UserRole, static_cast(0));
- tree_roots_[id] = root;
+ tree_roots_[mid] = root;
}
}
+bool MainWindow::openFederation(const QString& path) {
+ if (!confirmDiscardIfDirty()) return false;
+
+ QStringList warnings;
+ QString err;
+ if (!federation_->load(path, &warnings, &err)) {
+ QMessageBox::warning(this, "Open Federation",
+ QString("Could not open federation:\n%1").arg(err));
+ return false;
+ }
+
+ clearScene();
+
+ QStringList paths;
+ QStringList fed_ids;
+ QStringList missing;
+ for (const auto& m : federation_->models()) {
+ if (m.source_kind != "local") continue; // already warned by load()
+ if (!QFileInfo::exists(m.source_path)) {
+ missing << m.source_path;
+ continue;
+ }
+ paths << m.source_path;
+ fed_ids << m.id;
+ }
+ loadModelsFromPaths(paths, fed_ids);
+
+ for (const auto& msg : missing) {
+ warnings << QString("Source not found, kept in federation: %1").arg(msg);
+ }
+ if (!warnings.isEmpty()) {
+ QMessageBox::warning(this, "Open Federation",
+ "Federation opened with warnings:\n\n" + warnings.join("\n"));
+ }
+
+ federation_->markClean();
+ updateWindowTitle();
+
+ if (federation_->hasHomeView()) {
+ const auto& hv = federation_->homeView();
+ viewport_->setCamera(hv.target.x(), hv.target.y(), hv.target.z(),
+ hv.distance, hv.yaw, hv.pitch);
+ }
+ return true;
+}
+
+void MainWindow::onFederationNew() {
+ if (!confirmDiscardIfDirty()) return;
+ clearScene();
+ federation_->clear();
+ updateWindowTitle();
+}
+
+void MainWindow::onFederationOpen() {
+ QString path = QFileDialog::getOpenFileName(
+ this, "Open Federation", QString(),
+ "IFC Federation (*.ifcfed);;All Files (*)");
+ if (path.isEmpty()) return;
+ openFederation(path);
+}
+
+bool MainWindow::onFederationSave() {
+ if (federation_->filePath().isEmpty()) return onFederationSaveAs();
+ QString err;
+ if (!federation_->save(federation_->filePath(), &err)) {
+ QMessageBox::warning(this, "Save Federation",
+ QString("Could not save federation:\n%1").arg(err));
+ return false;
+ }
+ updateWindowTitle();
+ return true;
+}
+
+bool MainWindow::onFederationSaveAs() {
+ QString suggested = federation_->filePath();
+ if (suggested.isEmpty()) suggested = "federation.ifcfed";
+ QString path = QFileDialog::getSaveFileName(
+ this, "Save Federation As", suggested,
+ "IFC Federation (*.ifcfed);;All Files (*)");
+ if (path.isEmpty()) return false;
+ if (!path.endsWith(".ifcfed", Qt::CaseInsensitive)) path += ".ifcfed";
+
+ QString err;
+ if (!federation_->save(path, &err)) {
+ QMessageBox::warning(this, "Save Federation",
+ QString("Could not save federation:\n%1").arg(err));
+ return false;
+ }
+ updateWindowTitle();
+ return true;
+}
+
+void MainWindow::onSetHomeView() {
+ auto cs = viewport_->cameraState();
+ Federation::HomeView hv;
+ hv.target = cs.target;
+ hv.distance = cs.distance;
+ hv.yaw = cs.yaw;
+ hv.pitch = cs.pitch;
+ federation_->setHomeView(hv);
+ updateWindowTitle();
+}
+
+void MainWindow::onGoHomeView() {
+ if (!federation_->hasHomeView()) {
+ status_label_->setText("No home view set for this federation.");
+ return;
+ }
+ const auto& hv = federation_->homeView();
+ viewport_->setCamera(hv.target.x(), hv.target.y(), hv.target.z(),
+ hv.distance, hv.yaw, hv.pitch);
+}
+
+void MainWindow::clearScene() {
+ while (!tree_roots_.empty()) {
+ uint32_t mid = tree_roots_.begin()->first;
+ viewport_->removeModel(mid);
+ removeModelUi(mid);
+ }
+ fed_id_to_model_id_.clear();
+ model_id_to_fed_id_.clear();
+}
+
+bool MainWindow::confirmDiscardIfDirty() {
+ if (!federation_->isDirty()) return true;
+ auto ret = QMessageBox::question(
+ this, "Unsaved Federation",
+ "The current federation has unsaved changes. Save before continuing?",
+ QMessageBox::Save | QMessageBox::Discard | QMessageBox::Cancel,
+ QMessageBox::Save);
+ if (ret == QMessageBox::Cancel) return false;
+ if (ret == QMessageBox::Save) return onFederationSave();
+ return true; // Discard
+}
+
+void MainWindow::updateWindowTitle() {
+ QString fed_path = federation_->filePath();
+ if (fed_path.isEmpty() && federation_->models().empty()) {
+ setWindowTitle("IfcViewer");
+ } else if (fed_path.isEmpty()) {
+ setWindowTitle("untitled[*] — IfcViewer");
+ } else {
+ setWindowTitle(QFileInfo(fed_path).fileName() + "[*] — IfcViewer");
+ }
+ setWindowModified(federation_->isDirty());
+}
+
+void MainWindow::closeEvent(QCloseEvent* event) {
+ if (confirmDiscardIfDirty()) event->accept();
+ else event->ignore();
+}
+
void MainWindow::onLoadStarted(uint32_t /*mid*/, QString display_name) {
progress_bar_->setValue(0);
progress_bar_->setVisible(true);
@@ -326,6 +524,12 @@ void MainWindow::writeSidecarForModel(uint32_t mid) {
}
void MainWindow::removeModelUi(uint32_t mid) {
+ auto fed_it = model_id_to_fed_id_.find(mid);
+ if (fed_it != model_id_to_fed_id_.end()) {
+ fed_id_to_model_id_.erase(fed_it->second);
+ model_id_to_fed_id_.erase(fed_it);
+ }
+
auto root_it = tree_roots_.find(mid);
if (root_it != tree_roots_.end()) {
delete root_it->second;
diff --git a/src/ifcviewer-full/MainWindow.h b/src/ifcviewer-full/MainWindow.h
index b616293d59..13dfecf061 100644
--- a/src/ifcviewer-full/MainWindow.h
+++ b/src/ifcviewer-full/MainWindow.h
@@ -34,6 +34,7 @@
#include "ViewportWindow.h"
#include "SceneLoader.h"
+class Federation;
class SettingsWindow;
class MainWindow : public QMainWindow {
@@ -43,12 +44,22 @@ public:
~MainWindow();
void addFiles(const QStringList& paths);
+ bool openFederation(const QString& path);
void setPendingCamera(const QString& params);
void setPendingBenchmark(int frames);
+protected:
+ void closeEvent(QCloseEvent* event) override;
+
private slots:
void onFileOpen();
void onDatabaseOpen();
+ void onFederationNew();
+ void onFederationOpen();
+ bool onFederationSave();
+ bool onFederationSaveAs();
+ void onSetHomeView();
+ void onGoHomeView();
void onFileSettings();
void onObjectPicked(uint32_t object_id);
void onTreeSelectionChanged();
@@ -69,6 +80,11 @@ private slots:
private:
void setupUi();
void setupMenus();
+ void loadModelsFromPaths(const QStringList& paths,
+ const QStringList& fed_ids);
+ void clearScene();
+ bool confirmDiscardIfDirty();
+ void updateWindowTitle();
void populateProperties(uint32_t object_id);
void appendElementToTree(uint32_t model_id,
uint32_t object_id,
@@ -84,6 +100,7 @@ private:
ViewportWindow* viewport_ = nullptr;
SceneLoader* loader_ = nullptr;
+ Federation* federation_ = nullptr;
SettingsWindow* settings_ = nullptr;
QWidget* viewport_container_ = nullptr;
QTreeWidget* element_tree_ = nullptr;
@@ -95,6 +112,11 @@ private:
// Per-model tree roots, keyed by model_id.
std::map tree_roots_;
+ // Bidirectional federation_id <-> model_id map. Federation owns the
+ // persistent ids; SceneLoader owns the runtime model_ids.
+ std::unordered_map fed_id_to_model_id_;
+ std::unordered_map model_id_to_fed_id_;
+
// Display-side element registry for tree + property lookup.
std::unordered_map element_map_;
std::unordered_map tree_items_;
diff --git a/src/ifcviewer-full/main.cpp b/src/ifcviewer-full/main.cpp
index d15a525c53..8f861e3f31 100644
--- a/src/ifcviewer-full/main.cpp
+++ b/src/ifcviewer-full/main.cpp
@@ -40,7 +40,9 @@ int main(int argc, char* argv[]) {
QCommandLineParser parser;
parser.setApplicationDescription("IfcOpenShell IFC Viewer");
parser.addHelpOption();
- parser.addPositionalArgument("files", "IFC file(s) to open", "[files...]");
+ parser.addPositionalArgument("files",
+ "IFC file(s) and/or one .ifcfed federation to open",
+ "[files...]");
parser.addOption({{"c", "camera"},
"Set camera: tx,ty,tz,dist,yaw,pitch", "params"});
parser.addOption({{"b", "benchmark"},
@@ -51,9 +53,17 @@ int main(int argc, char* argv[]) {
window.show();
auto args = parser.positionalArguments();
- if (!args.isEmpty()) {
- window.addFiles(args);
+ QStringList file_args;
+ QString fed_arg;
+ for (const auto& a : args) {
+ if (fed_arg.isEmpty() && a.endsWith(".ifcfed", Qt::CaseInsensitive)) {
+ fed_arg = a;
+ } else {
+ file_args << a;
+ }
}
+ if (!fed_arg.isEmpty()) window.openFederation(fed_arg);
+ if (!file_args.isEmpty()) window.addFiles(file_args);
if (parser.isSet("camera")) {
window.setPendingCamera(parser.value("camera"));
diff --git a/src/ifcviewer/ViewportWindow.cpp b/src/ifcviewer/ViewportWindow.cpp
index 81d75aebd7..e928525f03 100644
--- a/src/ifcviewer/ViewportWindow.cpp
+++ b/src/ifcviewer/ViewportWindow.cpp
@@ -1186,6 +1186,10 @@ QString ViewportWindow::cameraString() const {
.arg(camera_pitch_, 0, 'f', 2);
}
+ViewportWindow::CameraState ViewportWindow::cameraState() const {
+ return { camera_target_, camera_distance_, camera_yaw_, camera_pitch_ };
+}
+
void ViewportWindow::keyPressEvent(QKeyEvent* event) {
const int key = event->key();
diff --git a/src/ifcviewer/ViewportWindow.h b/src/ifcviewer/ViewportWindow.h
index 68e9893020..ec223c559f 100644
--- a/src/ifcviewer/ViewportWindow.h
+++ b/src/ifcviewer/ViewportWindow.h
@@ -172,6 +172,14 @@ public:
void setBenchmarkFrames(int n);
QString cameraString() const;
+ struct CameraState {
+ QVector3D target;
+ float distance;
+ float yaw; // degrees
+ float pitch; // degrees
+ };
+ CameraState cameraState() const;
+
struct FrameStats {
float fps;
float frame_time_ms;