spatial hierarchy from IFC + active-model concept

Build the spatial hierarchy panel from the loaded model's real IFC
spatial structure instead of mock data:
- helpers/element: add get_spatial_children (IsDecomposedBy -> RelatedObjects,
  filtered to spatial elements) to walk IfcProject -> IfcSite -> IfcBuilding
  -> IfcBuildingStorey -> IfcSpace.
- SessionState: relay dataSourceReady as modelDataSourceReady (the .ifc for a
  sidecar hit loads asynchronously, so the tree can only build once it arrives).
- spatial_hierarchy/View: walk the active model's IFC file into a TreeNode
  tree, naming nodes by Name (fallback to class), mapping site/building/storey
  kinds; siblings sorted with natural (numeric) collation.
- spatial_hierarchy/Panel: tree now fills the panel height (setBodyExpanding +
  Expanding size policy); right-click menu for recursive Expand/Collapse
  Subtree and Expand/Collapse All.

Add the concept of an active model:
- SessionState: activeModelId / setActiveModelId / activeModelChanged; the
  first loaded model is active by default; reassigns/clears on removal.
- Models panel: clicking a model makes it active; its cube icon is drawn with
  the accent colour (makeAccentSvgIcon) via FederationItemModel::setActiveModelId.
- The spatial hierarchy reflects only the active model.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Dion Moult
2026-07-08 16:38:42 +10:00
parent ec285fc32c
commit 60cab3e7c4
11 changed files with 221 additions and 11 deletions
+21
View File
@@ -94,6 +94,15 @@ void SessionState::createLoader(ViewportWindow* viewport) {
connect(loader_, &SceneLoader::allLoadsFinished, this, [this]() {
setStatusMessage("Loaded", QString("%1 model(s)").arg(loader_->modelCount()));
});
connect(loader_, &SceneLoader::dataSourceReady, this, [this](uint32_t session_model_id) {
emit modelDataSourceReady(session_model_id);
});
// First model to load becomes the active model by default.
connect(this, &SessionState::modelGeometryReady, this, [this](uint32_t session_model_id) {
if (active_model_id_.isEmpty()) {
setActiveModelId(modelIdForSessionModelId(session_model_id));
}
});
}
void SessionState::setSelectedObjectId(uint32_t object_id) {
@@ -129,12 +138,24 @@ void SessionState::removeModelMappingByModelId(const QString& model_id) {
if (it == model_id_to_session_model_id_.end()) return;
session_model_id_to_model_id_.remove(it.value());
model_id_to_session_model_id_.erase(it);
if (model_id == active_model_id_) {
setActiveModelId(model_id_to_session_model_id_.isEmpty()
? QString()
: model_id_to_session_model_id_.keys().first());
}
}
void SessionState::clearModelMappings() {
model_id_to_session_model_id_.clear();
session_model_id_to_model_id_.clear();
cloud_metadata_.clear();
setActiveModelId(QString());
}
void SessionState::setActiveModelId(const QString& model_id) {
if (model_id == active_model_id_) return;
active_model_id_ = model_id;
emit activeModelChanged(active_model_id_);
}
void SessionState::setCloudMetadata(const QString& model_id, const QVariantMap& metadata) {