ifcviewer: overhaul model/object ID tracking

Rename the two overloaded model identifiers and make object_id
assignment single-authority, fixing a pick -> properties mismatch.

Identifiers:
- Per-model UUID fed_id -> model_id; the uint32 runtime handle
  model_id -> session_model_id (SessionState accessors + mirror hashes
  renamed to match). "fed_id" was a misnomer -- the federation is the
  whole collection, not one model.

object_id assignment (fixes wrong class on click):
- Producers (GeometryStreamer, .ifcview sidecar) now stamp model-LOCAL
  object_ids; ViewportCore::applyCachedModel is the sole authority that
  assigns the session-global id (base + local). Removed
  SceneLoader::next_object_id_, GeometryStreamer::lastObjectId(), and the
  streamer's start_object_id parameter.
- The element table is stamped by the same base on both load paths
  (applySidecarData and onStreamerFinished), so registry ids match the
  ids pick returns. Previously the sidecar path double-rebased instances
  vs the registry (click IfcSite -> showed IfcDoor); the live-stream path
  had the same latent mismatch. Both closed.

Naming / cleanup:
- SceneLoader::addFiles -> queueModels; startStreamLoadFor ->
  loadFromGeometryStreamer; readSidecarMetadataOnly -> readSidecarMetadata.
- Federation::addModel takes an explicit display_name (no QFileInfo
  fallback); callers pass QFileInfo(path).fileName().
- Disambiguate cryptic short locals (d->sidecar, m->model, c->chunk, ...)
  in SceneLoader, Federation, ViewportWindow, AreaMeasurement,
  SectionGizmoRenderer, and the SidecarData/SidecarReadPlan spots in
  ViewportCore.

Tests: 125/125 pass.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Dion Moult
2026-07-08 14:10:05 +10:00
parent b441fada90
commit 75c9da5098
49 changed files with 914 additions and 885 deletions
+50 -49
View File
@@ -166,31 +166,31 @@ void removeGroup(SessionState& session, QWidget& host, const QString& group_id)
session.setStatusMessage("Models", "Group removed");
}
void removeModel(SessionState& session, ViewportWindow& viewport, QWidget& host, const QString& fed_id) {
const Federation::Model* model = session.federation()->findById(fed_id);
const QString label = model ? model->display_name : fed_id;
void removeModel(SessionState& session, ViewportWindow& viewport, QWidget& host, const QString& model_id) {
const Federation::Model* model = session.federation()->findById(model_id);
const QString label = model ? model->display_name : model_id;
const auto choice = QMessageBox::question(
&host, "Remove Model",
QString("Remove model '%1' from the federation?").arg(label),
QMessageBox::Yes | QMessageBox::No, QMessageBox::No);
if (choice != QMessageBox::Yes) return;
const uint32_t model_id = session.modelIdForFedId(fed_id);
if (model_id == 0) {
session.federation()->removeModel(fed_id);
const uint32_t session_model_id = session.sessionModelIdForModelId(model_id);
if (session_model_id == 0) {
session.federation()->removeModel(model_id);
session.notifyFederationChanged();
session.setStatusMessage("Models", "Model removed");
return;
}
if (session.loader()->isLoadingModel(model_id)) return;
if (session.loader()->isLoadingModel(session_model_id)) return;
viewport.setSelectedObjectId(0);
session.setSelectedObjectId(0);
session.federation()->removeModel(fed_id);
viewport.removeModel(model_id);
session.loader()->removeModel(model_id);
session.elementRegistry()->removeModel(model_id);
session.removeModelMappingByFedId(fed_id);
session.federation()->removeModel(model_id);
viewport.removeModel(session_model_id);
session.loader()->removeModel(session_model_id);
session.elementRegistry()->removeModel(session_model_id);
session.removeModelMappingByModelId(model_id);
session.notifySelectionChanged();
session.notifyModelsChanged();
session.setStatusMessage("Models", "Model removed");
@@ -198,12 +198,12 @@ void removeModel(SessionState& session, ViewportWindow& viewport, QWidget& host,
namespace detail {
void loadModels(SessionState& session, const QStringList& paths, const QStringList& fed_ids) {
void loadModels(SessionState& session, const QStringList& paths, const QStringList& model_ids) {
if (paths.isEmpty()) return;
const auto model_ids = session.loader()->addFiles(paths);
for (int i = 0; i < paths.size() && i < static_cast<int>(model_ids.size()) && i < fed_ids.size(); ++i) {
session.setModelMapping(fed_ids[i], model_ids[i]);
const auto session_model_ids = session.loader()->queueModels(paths);
for (int i = 0; i < paths.size() && i < static_cast<int>(session_model_ids.size()) && i < model_ids.size(); ++i) {
session.setModelMapping(model_ids[i], session_model_ids[i]);
}
}
@@ -269,20 +269,21 @@ void addModel(SessionState& session, QWidget& host) {
// models yet — the first model that finishes loading will set the
// origin via ViewportView. Checked here (before federation->addModel)
// because federation->addModel doesn't yet populate SessionState's
// model mapping; modelIds() reflects pre-add state at this point.
if (session.modelIds().isEmpty()) {
// model mapping; sessionModelIds() reflects pre-add state at this point.
if (session.sessionModelIds().isEmpty()) {
armFederatedFalseOriginGuess();
}
QStringList accepted_paths;
QStringList accepted_fed_ids;
QStringList accepted_model_ids;
for (const auto& path : paths) {
const QString fed_id = session.federation()->addModel(path);
if (fed_id.isEmpty()) continue;
const QString model_id =
session.federation()->addModel(path, QFileInfo(path).fileName());
if (model_id.isEmpty()) continue;
accepted_paths << path;
accepted_fed_ids << fed_id;
accepted_model_ids << model_id;
}
detail::loadModels(session, accepted_paths, accepted_fed_ids);
detail::loadModels(session, accepted_paths, accepted_model_ids);
session.notifyModelsChanged();
}
@@ -317,15 +318,15 @@ void addModelFromCloud(SessionState& session, QWidget& host) {
proc->call("pull_models_interactive", QJsonValue(),
[sguard, connector_id](const QJsonValue& result) {
if (!sguard) return;
// Arm before the first addCloudModel — modelIds() reflects the
// Arm before the first addCloudModel — sessionModelIds() reflects the
// session state at the moment the connector returns, which is
// when the user's "add into empty session" intent applies.
if (sguard->modelIds().isEmpty()) {
if (sguard->sessionModelIds().isEmpty()) {
armFederatedFalseOriginGuess();
}
const QJsonArray arr = result.toArray();
QStringList paths;
QStringList fed_ids;
QStringList model_ids;
int added = 0;
for (const QJsonValue& value : arr) {
if (value.isNull() || !value.isObject()) continue;
@@ -337,19 +338,19 @@ void addModelFromCloud(SessionState& session, QWidget& host) {
QString src_connector = source.value("connector").toString();
if (src_connector.isEmpty()) src_connector = connector_id;
const QString fed_id = sguard->federation()->addCloudModel(
const QString model_id = sguard->federation()->addCloudModel(
display_name, src_connector, source);
if (fed_id.isEmpty()) continue;
if (model_id.isEmpty()) continue;
const QJsonObject meta = entry.value("metadata").toObject();
sguard->setCloudMetadata(fed_id, meta.toVariantMap());
sguard->setCloudMetadata(model_id, meta.toVariantMap());
paths << path;
fed_ids << fed_id;
model_ids << model_id;
++added;
}
if (!paths.isEmpty()) {
detail::loadModels(*sguard, paths, fed_ids);
detail::loadModels(*sguard, paths, model_ids);
sguard->notifyModelsChanged();
}
sguard->setStatusMessage("Cloud",
@@ -368,28 +369,28 @@ void addModelFromCloud(SessionState& session, QWidget& host) {
namespace {
// Shared "local path on disk" lookup for the right-click cloud commands:
// the loader keeps the path keyed by model_id (set when a file or pull_models
// the loader keeps the path keyed by session_model_id (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& session, const QString& fed_id) {
const uint32_t model_id = session.modelIdForFedId(fed_id);
if (model_id == 0 || !session.loader()) return {};
return session.loader()->filePath(model_id);
QString localPathForModel(SessionState& session, const QString& model_id) {
const uint32_t session_model_id = session.sessionModelIdForModelId(model_id);
if (session_model_id == 0 || !session.loader()) return {};
return session.loader()->filePath(session_model_id);
}
} // namespace
void saveModelToCloud(SessionState& session, QWidget& host, const QString& fed_id) {
void saveModelToCloud(SessionState& session, QWidget& host, const QString& model_id) {
auto* federation = session.federation();
const Federation::Model* model = federation->findById(fed_id);
const Federation::Model* model = federation->findById(model_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(session, fed_id);
const QString local_path = localPathForModel(session, model_id);
if (local_path.isEmpty()) {
QMessageBox::warning(&host, "Save Model To Cloud",
"Cannot find a local copy of this model to push.");
@@ -416,15 +417,15 @@ void saveModelToCloud(SessionState& session, QWidget& host, const QString& fed_i
QPointer<SessionState> sguard(&session);
proc->call("push_model", params,
[sguard, fed_id, connector_id](const QJsonValue& result) {
[sguard, model_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);
sguard->federation()->setModelSource(model_id, new_connector, new_source);
const QJsonObject meta = obj.value("metadata").toObject();
sguard->setCloudMetadata(fed_id, meta.toVariantMap());
sguard->setCloudMetadata(model_id, meta.toVariantMap());
sguard->setStatusMessage("Cloud",
QString("Saved to %1").arg(new_connector));
},
@@ -438,10 +439,10 @@ void saveModelToCloud(SessionState& session, QWidget& host, const QString& fed_i
});
}
void saveModelAsToCloud(SessionState& session, QWidget& host, const QString& fed_id) {
const Federation::Model* model = session.federation()->findById(fed_id);
void saveModelAsToCloud(SessionState& session, QWidget& host, const QString& model_id) {
const Federation::Model* model = session.federation()->findById(model_id);
if (!model) return;
const QString local_path = localPathForModel(session, fed_id);
const QString local_path = localPathForModel(session, model_id);
if (local_path.isEmpty()) {
QMessageBox::warning(&host, "Save Model As To Cloud",
"Cannot find a local copy of this model to push.");
@@ -480,20 +481,20 @@ void saveModelAsToCloud(SessionState& session, QWidget& host, const QString& fed
QPointer<SessionState> sguard(&session);
proc->call("push_model_interactive", params,
[sguard, fed_id, connector_id](const QJsonValue& result) {
[sguard, model_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);
sguard->federation()->setModelSource(model_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);
sguard->federation()->setModelDisplayName(model_id, new_name);
}
const QJsonObject meta = obj.value("metadata").toObject();
sguard->setCloudMetadata(fed_id, meta.toVariantMap());
sguard->setCloudMetadata(model_id, meta.toVariantMap());
sguard->setStatusMessage("Cloud",
QString("Pushed to %1").arg(new_connector));
},