Wire IfcViewer to cloud sync connectors

Implements the viewer side of CLOUD_SYNC_PROTOCOL.md: connector
discovery, JSON-RPC stdio host, and Open/Save/Sync/Add cloud workflows
wired through the ribbon, Models panel right-click, and Settings tab.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
Dion Moult
2026-05-20 09:15:03 +10:00
parent 95c62cc70e
commit 486b858d56
28 changed files with 2153 additions and 67 deletions
@@ -25,6 +25,9 @@
#include "../../ElementRegistry.h"
#include "../../SessionState.h"
#include "../connectors/PickerDialog.h"
#include "../connectors/Process.h"
#include "../connectors/Registry.h"
#include "../../../ifcviewer/Federation.h"
#include "../../../ifcviewer/SceneLoader.h"
#include "../../../ifcviewer/SidecarBuilder.h"
@@ -40,9 +43,13 @@
#include <QFileDialog>
#include <QFileInfo>
#include <QInputDialog>
#include <QJsonArray>
#include <QJsonObject>
#include <QJsonValue>
#include <QLineEdit>
#include <QListView>
#include <QMessageBox>
#include <QPointer>
#include <QStandardPaths>
#include <QThread>
#include <QTreeView>
@@ -227,6 +234,9 @@ void addModel(SessionState& s, QWidget& host) {
}
break;
}
case SourceMode::CloudModel:
addModelFromCloud(s, host);
return;
case SourceMode::ConvertToDatabase:
convertIfcToDatabase(s, host);
return;
@@ -249,6 +259,221 @@ void addModel(SessionState& s, QWidget& host) {
s.notifyModelsChanged();
}
void addModelFromCloud(SessionState& s, QWidget& host) {
auto* registry = s.connectorRegistry();
const auto& manifests = registry->available();
if (manifests.empty()) {
QMessageBox::information(&host, "Add From Cloud",
"No connectors are installed.");
return;
}
modules::connectors::ConnectorPickerDialog picker(
manifests, "Add From Cloud",
"Pick a connector to browse models on.", &host);
if (picker.exec() != QDialog::Accepted) return;
const QString connector_id = picker.selectedId();
if (connector_id.isEmpty()) return;
auto* proc = registry->get(connector_id);
if (!proc) {
QMessageBox::warning(&host, "Add From Cloud",
QString("Could not launch connector '%1':\n%2")
.arg(connector_id, registry->lastError()));
return;
}
s.setStatusMessage("Cloud", QString("Browsing %1...").arg(connector_id));
QPointer<SessionState> sguard(&s);
proc->call("pull_models_interactive", QJsonValue(),
[sguard, connector_id](const QJsonValue& result) {
if (!sguard) return;
const QJsonArray arr = result.toArray();
QStringList paths;
QStringList fed_ids;
int added = 0;
for (const QJsonValue& v : arr) {
if (v.isNull() || !v.isObject()) continue;
const QJsonObject entry = v.toObject();
const QString display_name = entry.value("display_name").toString();
const QString path = entry.value("path").toString();
if (path.isEmpty()) continue;
const QJsonObject source = entry.value("source").toObject();
QString src_connector = source.value("connector").toString();
if (src_connector.isEmpty()) src_connector = connector_id;
const QString fed_id = sguard->federation()->addCloudModel(
display_name, src_connector, source);
if (fed_id.isEmpty()) continue;
const QJsonObject meta = entry.value("metadata").toObject();
sguard->setCloudMetadata(fed_id, meta.toVariantMap());
paths << path;
fed_ids << fed_id;
++added;
}
if (!paths.isEmpty()) {
detail::loadModels(*sguard, paths, fed_ids);
sguard->notifyModelsChanged();
}
sguard->setStatusMessage("Cloud",
QString("Added %1 model(s) from %2").arg(added).arg(connector_id));
},
[sguard, connector_id](int code, const QString& message) {
qWarning() << "pull_models_interactive from" << connector_id
<< "failed:" << code << message;
if (sguard) {
sguard->setStatusMessage("Cloud",
QString("%1 reported an error (see connector UI)").arg(connector_id));
}
});
}
namespace {
// Shared "local path on disk" lookup for the right-click cloud commands:
// the loader keeps the path keyed by mid (set when a file or pull_models
// path was queued). Both local-sourced and resolved cloud-sourced models
// have one; only un-resolved cloud models (where pull_models hasn't
// returned yet) won't.
QString localPathForModel(SessionState& s, const QString& fed_id) {
const uint32_t mid = s.modelIdForFedId(fed_id);
if (mid == 0 || !s.loader()) return {};
return s.loader()->filePath(mid);
}
} // namespace
void saveModelToCloud(SessionState& s, QWidget& host, const QString& fed_id) {
auto* fed = s.federation();
const Federation::Model* model = fed->findById(fed_id);
if (!model) return;
if (model->source_connector == "local") {
QMessageBox::information(&host, "Save Model To Cloud",
"This model has no cloud target. Use \"Save As To Cloud\" first.");
return;
}
const QString local_path = localPathForModel(s, fed_id);
if (local_path.isEmpty()) {
QMessageBox::warning(&host, "Save Model To Cloud",
"Cannot find a local copy of this model to push.");
return;
}
const QString connector_id = model->source_connector;
auto* registry = s.connectorRegistry();
auto* proc = registry->get(connector_id);
if (!proc) {
QMessageBox::warning(&host, "Save Model To Cloud",
QString("Could not launch connector '%1':\n%2")
.arg(connector_id, registry->lastError()));
return;
}
QJsonObject source = model->source_data;
source["connector"] = connector_id;
QJsonObject params;
params["path"] = local_path;
params["source"] = source;
s.setStatusMessage("Cloud",
QString("Saving %1 to %2...").arg(model->display_name, connector_id));
QPointer<SessionState> sguard(&s);
proc->call("push_model", params,
[sguard, fed_id, connector_id](const QJsonValue& result) {
if (!sguard) return;
const QJsonObject obj = result.toObject();
const QJsonObject new_source = obj.value("source").toObject();
QString new_connector = new_source.value("connector").toString();
if (new_connector.isEmpty()) new_connector = connector_id;
sguard->federation()->setModelSource(fed_id, new_connector, new_source);
const QJsonObject meta = obj.value("metadata").toObject();
sguard->setCloudMetadata(fed_id, meta.toVariantMap());
sguard->setStatusMessage("Cloud",
QString("Saved to %1").arg(new_connector));
},
[sguard, connector_id](int code, const QString& message) {
qWarning() << "push_model to" << connector_id
<< "failed:" << code << message;
if (sguard) {
sguard->setStatusMessage("Cloud",
QString("%1 reported an error (see connector UI)").arg(connector_id));
}
});
}
void saveModelAsToCloud(SessionState& s, QWidget& host, const QString& fed_id) {
const Federation::Model* model = s.federation()->findById(fed_id);
if (!model) return;
const QString local_path = localPathForModel(s, fed_id);
if (local_path.isEmpty()) {
QMessageBox::warning(&host, "Save Model As To Cloud",
"Cannot find a local copy of this model to push.");
return;
}
auto* registry = s.connectorRegistry();
const auto& manifests = registry->available();
if (manifests.empty()) {
QMessageBox::information(&host, "Save Model As To Cloud",
"No connectors are installed.");
return;
}
modules::connectors::ConnectorPickerDialog picker(
manifests, "Save Model As To Cloud",
QString("Pick a connector to push '%1' to.").arg(model->display_name),
&host);
if (picker.exec() != QDialog::Accepted) return;
const QString connector_id = picker.selectedId();
if (connector_id.isEmpty()) return;
auto* proc = registry->get(connector_id);
if (!proc) {
QMessageBox::warning(&host, "Save Model As To Cloud",
QString("Could not launch connector '%1':\n%2")
.arg(connector_id, registry->lastError()));
return;
}
QJsonObject params;
params["path"] = local_path;
s.setStatusMessage("Cloud",
QString("Pushing %1 to %2...").arg(model->display_name, connector_id));
QPointer<SessionState> sguard(&s);
proc->call("push_model_interactive", params,
[sguard, fed_id, connector_id](const QJsonValue& result) {
if (!sguard) return;
const QJsonObject obj = result.toObject();
const QJsonObject new_source = obj.value("source").toObject();
QString new_connector = new_source.value("connector").toString();
if (new_connector.isEmpty()) new_connector = connector_id;
sguard->federation()->setModelSource(fed_id, new_connector, new_source);
const QString new_name = obj.value("display_name").toString();
if (!new_name.isEmpty()) {
sguard->federation()->setModelDisplayName(fed_id, new_name);
}
const QJsonObject meta = obj.value("metadata").toObject();
sguard->setCloudMetadata(fed_id, meta.toVariantMap());
sguard->setStatusMessage("Cloud",
QString("Pushed to %1").arg(new_connector));
},
[sguard, connector_id](int code, const QString& message) {
qWarning() << "push_model_interactive to" << connector_id
<< "failed:" << code << message;
if (sguard) {
sguard->setStatusMessage("Cloud",
QString("%1 reported an error (see connector UI)").arg(connector_id));
}
});
}
void convertIfcToDatabase(SessionState& s, QWidget& host) {
QFileDialog input_dialog(&host, "Select IFC File to Convert");
input_dialog.setFileMode(QFileDialog::ExistingFile);