2026-05-09 07:11:58 +10:00
|
|
|
// This file was generated with the assistance of an AI coding tool.
|
|
|
|
|
/********************************************************************************
|
|
|
|
|
* *
|
|
|
|
|
* 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/>. *
|
|
|
|
|
* *
|
|
|
|
|
********************************************************************************/
|
|
|
|
|
|
2026-05-16 17:04:23 +10:00
|
|
|
#include "Commands.h"
|
2026-05-09 07:11:58 +10:00
|
|
|
|
|
|
|
|
#include "../../ElementRegistry.h"
|
|
|
|
|
#include "../../SessionState.h"
|
2026-05-15 17:08:08 +10:00
|
|
|
#include "../models/Commands.h"
|
2026-05-09 07:11:58 +10:00
|
|
|
#include "../../../ifcviewer/Federation.h"
|
|
|
|
|
#include "../../../ifcviewer/SceneLoader.h"
|
|
|
|
|
#include "../../../ifcviewer/ViewportWindow.h"
|
|
|
|
|
|
|
|
|
|
#include <QFileDialog>
|
|
|
|
|
#include <QFileInfo>
|
|
|
|
|
#include <QMessageBox>
|
|
|
|
|
|
2026-05-16 17:04:23 +10:00
|
|
|
namespace ifcviewerfull::modules::project::commands {
|
2026-05-09 07:11:58 +10:00
|
|
|
|
2026-05-16 17:04:23 +10:00
|
|
|
namespace {
|
2026-05-09 07:11:58 +10:00
|
|
|
|
2026-05-16 17:04:23 +10:00
|
|
|
// Pure helper — clears the loaded scene without emitting any signals. The
|
|
|
|
|
// caller (newProject / openProject) emits projectReset / projectOpened once
|
|
|
|
|
// the whole flow finishes.
|
|
|
|
|
void clearScene(SessionState& s, ViewportWindow& vp) {
|
|
|
|
|
vp.setSelectedObjectId(0);
|
|
|
|
|
s.setSelectedObjectId(0);
|
|
|
|
|
for (uint32_t mid : s.modelIds()) {
|
|
|
|
|
vp.removeModel(mid);
|
|
|
|
|
s.loader()->removeModel(mid);
|
|
|
|
|
}
|
|
|
|
|
s.clearModelMappings();
|
|
|
|
|
s.elementRegistry()->clear();
|
2026-05-09 07:11:58 +10:00
|
|
|
}
|
|
|
|
|
|
2026-05-16 17:04:23 +10:00
|
|
|
// Returns false if the user cancelled (i.e. don't proceed with the destructive
|
|
|
|
|
// op). Handles the Save → Discard → Cancel branch including a follow-on save.
|
|
|
|
|
bool confirmDiscardIfDirty(SessionState& s, QWidget& host) {
|
|
|
|
|
if (!s.federation()->isDirty()) return true;
|
|
|
|
|
const auto result = QMessageBox::question(
|
|
|
|
|
&host, "Unsaved Project",
|
|
|
|
|
"The current project has unsaved changes. Save before continuing?",
|
|
|
|
|
QMessageBox::Save | QMessageBox::Discard | QMessageBox::Cancel,
|
|
|
|
|
QMessageBox::Save);
|
|
|
|
|
if (result == QMessageBox::Cancel) return false;
|
|
|
|
|
if (result == QMessageBox::Save) return saveProject(s, host);
|
|
|
|
|
return true;
|
2026-05-09 07:11:58 +10:00
|
|
|
}
|
|
|
|
|
|
2026-05-16 17:04:23 +10:00
|
|
|
bool openProjectAt(SessionState& s, QWidget& host, ViewportWindow& vp, const QString& path) {
|
|
|
|
|
SceneLoader* loader = s.loader();
|
2026-05-09 07:11:58 +10:00
|
|
|
if (loader && loader->isLoading()) {
|
|
|
|
|
QMessageBox::information(
|
2026-05-16 17:04:23 +10:00
|
|
|
&host, "Open Project",
|
2026-05-09 07:11:58 +10:00
|
|
|
"Wait until the current model load finishes before opening another project.");
|
|
|
|
|
return false;
|
|
|
|
|
}
|
2026-05-16 17:04:23 +10:00
|
|
|
if (!confirmDiscardIfDirty(s, host)) return false;
|
2026-05-09 07:11:58 +10:00
|
|
|
|
|
|
|
|
QStringList warnings;
|
|
|
|
|
QString err;
|
2026-05-16 17:04:23 +10:00
|
|
|
if (!s.federation()->load(path, &warnings, &err)) {
|
|
|
|
|
QMessageBox::warning(&host, "Open Project",
|
2026-05-09 07:11:58 +10:00
|
|
|
QString("Could not open project:\n%1").arg(err));
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-16 17:04:23 +10:00
|
|
|
clearScene(s, vp);
|
2026-05-09 07:11:58 +10:00
|
|
|
|
|
|
|
|
QStringList paths;
|
|
|
|
|
QStringList fed_ids;
|
2026-05-16 17:04:23 +10:00
|
|
|
for (const auto& model : s.federation()->models()) {
|
2026-05-09 07:11:58 +10:00
|
|
|
if (model.source_kind != "local") continue;
|
|
|
|
|
if (!QFileInfo::exists(model.source_path)) {
|
|
|
|
|
warnings << QString("Source not found, kept in project: %1").arg(model.source_path);
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
paths << model.source_path;
|
|
|
|
|
fed_ids << model.id;
|
|
|
|
|
}
|
2026-05-16 17:04:23 +10:00
|
|
|
modules::models::commands::detail::loadModels(s, paths, fed_ids);
|
2026-05-09 07:11:58 +10:00
|
|
|
|
|
|
|
|
if (!warnings.isEmpty()) {
|
2026-05-16 17:04:23 +10:00
|
|
|
QMessageBox::warning(&host, "Open Project",
|
2026-05-09 07:11:58 +10:00
|
|
|
"Project opened with warnings:\n\n" + warnings.join("\n"));
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-16 17:04:23 +10:00
|
|
|
s.federation()->markClean();
|
|
|
|
|
if (s.federation()->hasHomeView()) {
|
|
|
|
|
const auto& hv = s.federation()->homeView();
|
|
|
|
|
vp.setCamera(hv.target.x(), hv.target.y(), hv.target.z(),
|
|
|
|
|
hv.distance, hv.yaw, hv.pitch);
|
2026-05-09 07:11:58 +10:00
|
|
|
}
|
2026-05-16 17:04:23 +10:00
|
|
|
s.setStatusMessage("Project", QFileInfo(path).fileName());
|
|
|
|
|
s.notifyProjectOpened(path);
|
2026-05-09 07:11:58 +10:00
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-16 17:04:23 +10:00
|
|
|
bool saveProjectTo(SessionState& s, QWidget& host, const QString& path) {
|
2026-05-09 07:11:58 +10:00
|
|
|
QString err;
|
2026-05-16 17:04:23 +10:00
|
|
|
if (!s.federation()->save(path, &err)) {
|
|
|
|
|
QMessageBox::warning(&host, "Save Project",
|
2026-05-09 07:11:58 +10:00
|
|
|
QString("Could not save project:\n%1").arg(err));
|
|
|
|
|
return false;
|
|
|
|
|
}
|
2026-05-16 17:04:23 +10:00
|
|
|
s.setStatusMessage("Project", QFileInfo(path).fileName());
|
|
|
|
|
s.notifyProjectSaved(path);
|
2026-05-09 07:11:58 +10:00
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-16 17:04:23 +10:00
|
|
|
} // namespace
|
2026-05-09 07:11:58 +10:00
|
|
|
|
2026-05-16 17:04:23 +10:00
|
|
|
bool newProject(SessionState& s, QWidget& host, ViewportWindow& vp) {
|
|
|
|
|
SceneLoader* loader = s.loader();
|
|
|
|
|
if (loader && loader->isLoading()) {
|
|
|
|
|
QMessageBox::information(
|
|
|
|
|
&host, "New Project",
|
|
|
|
|
"Wait until the current model load finishes before creating a new project.");
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
if (!confirmDiscardIfDirty(s, host)) return false;
|
|
|
|
|
|
|
|
|
|
clearScene(s, vp);
|
|
|
|
|
s.federation()->clear();
|
|
|
|
|
s.setStatusMessage("Project", "Untitled");
|
|
|
|
|
s.notifyProjectReset();
|
|
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
bool openProject(SessionState& s, QWidget& host, ViewportWindow& vp) {
|
|
|
|
|
QFileDialog file_dialog(&host, "Open Project");
|
|
|
|
|
file_dialog.setFileMode(QFileDialog::ExistingFile);
|
2026-05-09 07:11:58 +10:00
|
|
|
file_dialog.setNameFilter("IFC Federation (*.ifcfed);;All Files (*)");
|
|
|
|
|
file_dialog.setOption(QFileDialog::DontUseNativeDialog, true);
|
|
|
|
|
if (file_dialog.exec() != QDialog::Accepted) return false;
|
|
|
|
|
|
2026-05-16 17:04:23 +10:00
|
|
|
const QString path = file_dialog.selectedFiles().value(0);
|
2026-05-09 07:11:58 +10:00
|
|
|
if (path.isEmpty()) return false;
|
2026-05-16 17:04:23 +10:00
|
|
|
return openProjectAt(s, host, vp, path);
|
2026-05-09 07:11:58 +10:00
|
|
|
}
|
|
|
|
|
|
2026-05-16 17:04:23 +10:00
|
|
|
bool saveProject(SessionState& s, QWidget& host) {
|
|
|
|
|
if (s.federation()->filePath().isEmpty()) return saveProjectAs(s, host);
|
|
|
|
|
return saveProjectTo(s, host, s.federation()->filePath());
|
2026-05-09 07:11:58 +10:00
|
|
|
}
|
|
|
|
|
|
2026-05-16 17:04:23 +10:00
|
|
|
bool saveProjectAs(SessionState& s, QWidget& host) {
|
|
|
|
|
QString suggested = s.federation()->filePath();
|
|
|
|
|
if (suggested.isEmpty()) suggested = "project.ifcfed";
|
2026-05-09 07:11:58 +10:00
|
|
|
|
2026-05-16 17:04:23 +10:00
|
|
|
QFileDialog file_dialog(&host, "Save Project As", suggested);
|
|
|
|
|
file_dialog.setAcceptMode(QFileDialog::AcceptSave);
|
|
|
|
|
file_dialog.setFileMode(QFileDialog::AnyFile);
|
|
|
|
|
file_dialog.setNameFilter("IFC Federation (*.ifcfed);;All Files (*)");
|
|
|
|
|
file_dialog.setOption(QFileDialog::DontUseNativeDialog, true);
|
|
|
|
|
if (file_dialog.exec() != QDialog::Accepted) return false;
|
|
|
|
|
|
|
|
|
|
QString path = file_dialog.selectedFiles().value(0);
|
|
|
|
|
if (path.isEmpty()) return false;
|
|
|
|
|
if (!path.endsWith(".ifcfed", Qt::CaseInsensitive)) path += ".ifcfed";
|
|
|
|
|
return saveProjectTo(s, host, path);
|
2026-05-09 07:11:58 +10:00
|
|
|
}
|
|
|
|
|
|
2026-05-16 17:04:23 +10:00
|
|
|
} // namespace ifcviewerfull::modules::project::commands
|