ifcviewer-full: add .ifcfed federation save/load

Federation (JSON) tracks an ordered list of model sources plus an
optional home-view camera state. Sources are stored relative when
under the federation file's directory, absolute otherwise.

File menu now exposes New / Open / Save / Save As; Add Files moves
to Ctrl+Shift+O. View menu gains Set/Go to Home View. Window title
binds to dirty state via setWindowModified, and the close-window
prompt offers Save/Discard/Cancel.

Per-model transform (4x4 column-major) and visible round-trip
through load/save but are not yet applied at the viewport — the
georeferencing work uses them.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
Dion Moult
2026-04-27 18:31:59 +10:00
parent 81a7c5b50e
commit 7bc64bef1a
7 changed files with 669 additions and 8 deletions
+308
View File
@@ -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 <http://www.gnu.org/licenses/>. *
* *
********************************************************************************/
#include "Federation.h"
#include <QDir>
#include <QFile>
#include <QFileInfo>
#include <QSaveFile>
#include <QJsonArray>
#include <QJsonDocument>
#include <QJsonObject>
#include <QJsonValue>
#include <QUuid>
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;
}
+105
View File
@@ -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 <http://www.gnu.org/licenses/>. *
* *
********************************************************************************/
#ifndef FEDERATION_H
#define FEDERATION_H
#include <QObject>
#include <QString>
#include <QStringList>
#include <QDateTime>
#include <QVector3D>
#include <array>
#include <vector>
// 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<float, 16> 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<Model>& 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<Model> models_;
bool has_home_view_ = false;
HomeView home_view_;
bool dirty_ = false;
};
#endif // FEDERATION_H
+209 -5
View File
@@ -19,11 +19,13 @@
#include "MainWindow.h"
#include "AppSettings.h"
#include "Federation.h"
#include "SettingsWindow.h"
#include "LodBuilder.h"
#include "SidecarCache.h"
#include <QApplication>
#include <QCloseEvent>
#include <QMenuBar>
#include <QFileDialog>
#include <QFileInfo>
@@ -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<int>(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<uint32_t>(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;
+22
View File
@@ -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<uint32_t, QTreeWidgetItem*> tree_roots_;
// Bidirectional federation_id <-> model_id map. Federation owns the
// persistent ids; SceneLoader owns the runtime model_ids.
std::unordered_map<QString, uint32_t> fed_id_to_model_id_;
std::unordered_map<uint32_t, QString> model_id_to_fed_id_;
// Display-side element registry for tree + property lookup.
std::unordered_map<uint32_t, ElementInfo> element_map_;
std::unordered_map<uint32_t, QTreeWidgetItem*> tree_items_;
+13 -3
View File
@@ -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"));
+4
View File
@@ -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();
+8
View File
@@ -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;