mirror of
https://github.com/IfcOpenShell/IfcOpenShell.git
synced 2026-09-19 06:39:13 +00:00
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:
@@ -94,6 +94,15 @@ void SessionState::createLoader(ViewportWindow* viewport) {
|
|||||||
connect(loader_, &SceneLoader::allLoadsFinished, this, [this]() {
|
connect(loader_, &SceneLoader::allLoadsFinished, this, [this]() {
|
||||||
setStatusMessage("Loaded", QString("%1 model(s)").arg(loader_->modelCount()));
|
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) {
|
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;
|
if (it == model_id_to_session_model_id_.end()) return;
|
||||||
session_model_id_to_model_id_.remove(it.value());
|
session_model_id_to_model_id_.remove(it.value());
|
||||||
model_id_to_session_model_id_.erase(it);
|
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() {
|
void SessionState::clearModelMappings() {
|
||||||
model_id_to_session_model_id_.clear();
|
model_id_to_session_model_id_.clear();
|
||||||
session_model_id_to_model_id_.clear();
|
session_model_id_to_model_id_.clear();
|
||||||
cloud_metadata_.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) {
|
void SessionState::setCloudMetadata(const QString& model_id, const QVariantMap& metadata) {
|
||||||
|
|||||||
@@ -78,6 +78,12 @@ public:
|
|||||||
QString modelIdForSessionModelId(uint32_t session_model_id) const;
|
QString modelIdForSessionModelId(uint32_t session_model_id) const;
|
||||||
QList<uint32_t> sessionModelIds() const;
|
QList<uint32_t> sessionModelIds() const;
|
||||||
|
|
||||||
|
// The active model — the single model the spatial hierarchy (and other
|
||||||
|
// model-scoped views) operate on. Set by clicking a model in the models
|
||||||
|
// panel; defaults to the first loaded model. Empty when no model is loaded.
|
||||||
|
QString activeModelId() const { return active_model_id_; }
|
||||||
|
void setActiveModelId(const QString& model_id);
|
||||||
|
|
||||||
void notifySelectionChanged();
|
void notifySelectionChanged();
|
||||||
void notifyModelsChanged();
|
void notifyModelsChanged();
|
||||||
void notifyFederationChanged();
|
void notifyFederationChanged();
|
||||||
@@ -101,6 +107,12 @@ signals:
|
|||||||
// for both sidecar-cache and stream loads; subscribers that just need to
|
// for both sidecar-cache and stream loads; subscribers that just need to
|
||||||
// re-derive view state (e.g. ViewportView::refresh) listen to this.
|
// re-derive view state (e.g. ViewportView::refresh) listen to this.
|
||||||
void modelGeometryReady(uint32_t session_model_id);
|
void modelGeometryReady(uint32_t session_model_id);
|
||||||
|
// Fires when a model's live IFC data source (the .ifc/.rdb, opened in the
|
||||||
|
// background after a sidecar-cache hit) becomes available for queries —
|
||||||
|
// e.g. so the spatial hierarchy can be built once the file is loaded.
|
||||||
|
void modelDataSourceReady(uint32_t session_model_id);
|
||||||
|
// Fires when the active model changes (empty model_id when cleared).
|
||||||
|
void activeModelChanged(const QString& model_id);
|
||||||
// Fires when SceneLoader reports a load failure. SessionState turns the
|
// Fires when SceneLoader reports a load failure. SessionState turns the
|
||||||
// raw loader signal into a session-level one so views (e.g. the MessageBox)
|
// raw loader signal into a session-level one so views (e.g. the MessageBox)
|
||||||
// can subscribe without touching the loader directly.
|
// can subscribe without touching the loader directly.
|
||||||
@@ -121,6 +133,7 @@ private:
|
|||||||
QString status_detail_;
|
QString status_detail_;
|
||||||
QHash<QString, uint32_t> model_id_to_session_model_id_;
|
QHash<QString, uint32_t> model_id_to_session_model_id_;
|
||||||
QHash<uint32_t, QString> session_model_id_to_model_id_;
|
QHash<uint32_t, QString> session_model_id_to_model_id_;
|
||||||
|
QString active_model_id_;
|
||||||
QHash<QString, QVariantMap> cloud_metadata_;
|
QHash<QString, QVariantMap> cloud_metadata_;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -87,14 +87,28 @@ QStandardItem* FederationItemModel::makeGroupNameItem(const QString& group_id, c
|
|||||||
return item;
|
return item;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
QIcon FederationItemModel::modelIcon(const QString& model_id) const {
|
||||||
|
return model_id == active_model_id_
|
||||||
|
? components::icons::makeAccentSvgIcon(":/icons/cube.svg")
|
||||||
|
: components::icons::makeSvgIcon(":/icons/cube.svg");
|
||||||
|
}
|
||||||
|
|
||||||
QStandardItem* FederationItemModel::makeModelNameItem(const QString& model_id, const QString& display_name) const {
|
QStandardItem* FederationItemModel::makeModelNameItem(const QString& model_id, const QString& display_name) const {
|
||||||
auto* item = new QStandardItem(components::icons::makeSvgIcon(":/icons/cube.svg"), display_name);
|
auto* item = new QStandardItem(modelIcon(model_id), display_name);
|
||||||
item->setData(model_id, IdRole);
|
item->setData(model_id, IdRole);
|
||||||
item->setData(int(ItemKind::Model), KindRole);
|
item->setData(int(ItemKind::Model), KindRole);
|
||||||
item->setEditable(false);
|
item->setEditable(false);
|
||||||
return item;
|
return item;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void FederationItemModel::setActiveModelId(const QString& model_id) {
|
||||||
|
if (model_id == active_model_id_) return;
|
||||||
|
const QString previous = active_model_id_;
|
||||||
|
active_model_id_ = model_id;
|
||||||
|
if (auto* item = id_to_name_item_.value(previous)) item->setIcon(modelIcon(previous));
|
||||||
|
if (auto* item = id_to_name_item_.value(active_model_id_)) item->setIcon(modelIcon(active_model_id_));
|
||||||
|
}
|
||||||
|
|
||||||
QStandardItem* FederationItemModel::makeVisibilityItem(ItemKind kind, bool visible) const {
|
QStandardItem* FederationItemModel::makeVisibilityItem(ItemKind kind, bool visible) const {
|
||||||
QString icon_path;
|
QString icon_path;
|
||||||
if (kind == ItemKind::Group) {
|
if (kind == ItemKind::Group) {
|
||||||
|
|||||||
@@ -53,6 +53,10 @@ public:
|
|||||||
// preserving anyway).
|
// preserving anyway).
|
||||||
void rebuildAll();
|
void rebuildAll();
|
||||||
|
|
||||||
|
// The active model is drawn with an accent-coloured cube icon. Restyles the
|
||||||
|
// previously- and newly-active model rows.
|
||||||
|
void setActiveModelId(const QString& model_id);
|
||||||
|
|
||||||
private slots:
|
private slots:
|
||||||
void onGroupAdded(const QString& group_id);
|
void onGroupAdded(const QString& group_id);
|
||||||
void onGroupRemoved(const QString& group_id);
|
void onGroupRemoved(const QString& group_id);
|
||||||
@@ -77,8 +81,11 @@ private:
|
|||||||
void appendGroupSubtreeTo(QStandardItem* parent_item, const QString& group_id);
|
void appendGroupSubtreeTo(QStandardItem* parent_item, const QString& group_id);
|
||||||
void refreshSubtreeVisibility(QStandardItem* root);
|
void refreshSubtreeVisibility(QStandardItem* root);
|
||||||
|
|
||||||
|
QIcon modelIcon(const QString& model_id) const; // accent cube when active, else plain
|
||||||
|
|
||||||
Federation* federation_ = nullptr;
|
Federation* federation_ = nullptr;
|
||||||
QHash<QString, QStandardItem*> id_to_name_item_; // both group_ids and model_ids
|
QHash<QString, QStandardItem*> id_to_name_item_; // both group_ids and model_ids
|
||||||
|
QString active_model_id_;
|
||||||
};
|
};
|
||||||
|
|
||||||
} // namespace bonsaiviewer::modules::models
|
} // namespace bonsaiviewer::modules::models
|
||||||
|
|||||||
@@ -249,8 +249,15 @@ ModelsPanel::ModelsPanel(bonsaiviewer::SessionState* session_state,
|
|||||||
addBodyWidget(section);
|
addBodyWidget(section);
|
||||||
|
|
||||||
connect(tree_, &QTreeView::clicked, this, [this](const QModelIndex& index) {
|
connect(tree_, &QTreeView::clicked, this, [this](const QModelIndex& index) {
|
||||||
if (!index.isValid() || index.column() != 1) return;
|
if (!index.isValid()) return;
|
||||||
commands::toggleVisibility(*session_state_, kindOf(index), idOf(index));
|
if (index.column() == 1) {
|
||||||
|
commands::toggleVisibility(*session_state_, kindOf(index), idOf(index));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// Clicking a model (its cube icon / row) makes it the active model.
|
||||||
|
if (kindOf(index) == ItemKind::Model) {
|
||||||
|
session_state_->setActiveModelId(idOf(index));
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
connect(tree_, &QTreeView::customContextMenuRequested, this, [this](const QPoint& pos) {
|
connect(tree_, &QTreeView::customContextMenuRequested, this, [this](const QPoint& pos) {
|
||||||
|
|||||||
@@ -70,6 +70,9 @@ ModelsPanelView::ModelsPanelView(ModelsPanel* widget,
|
|||||||
connect(&bonsaiviewer::ViewerSettings::instance(),
|
connect(&bonsaiviewer::ViewerSettings::instance(),
|
||||||
&bonsaiviewer::ViewerSettings::themeChanged,
|
&bonsaiviewer::ViewerSettings::themeChanged,
|
||||||
this, rebuild);
|
this, rebuild);
|
||||||
|
connect(session_state_, &SessionState::activeModelChanged, this, [this](const QString& model_id) {
|
||||||
|
model_->setActiveModelId(model_id);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
} // namespace bonsaiviewer::modules::models
|
} // namespace bonsaiviewer::modules::models
|
||||||
|
|||||||
@@ -24,17 +24,32 @@
|
|||||||
#include "../../components/SvgIcon.h"
|
#include "../../components/SvgIcon.h"
|
||||||
|
|
||||||
#include <QHeaderView>
|
#include <QHeaderView>
|
||||||
|
#include <QMenu>
|
||||||
|
#include <QSizePolicy>
|
||||||
#include <QTreeWidget>
|
#include <QTreeWidget>
|
||||||
#include <QTreeWidgetItem>
|
#include <QTreeWidgetItem>
|
||||||
|
|
||||||
namespace bonsaiviewer::modules::spatial_hierarchy {
|
namespace bonsaiviewer::modules::spatial_hierarchy {
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
|
||||||
|
void setSubtreeExpanded(QTreeWidgetItem* item, bool expanded) {
|
||||||
|
item->setExpanded(expanded);
|
||||||
|
for (int i = 0; i < item->childCount(); ++i) {
|
||||||
|
setSubtreeExpanded(item->child(i), expanded);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace
|
||||||
|
|
||||||
SpatialHierarchyPanel::SpatialHierarchyPanel(QWidget* parent)
|
SpatialHierarchyPanel::SpatialHierarchyPanel(QWidget* parent)
|
||||||
: components::Panel("Spatial Hierarchy", nullptr, parent)
|
: components::Panel("Spatial Hierarchy", nullptr, parent)
|
||||||
{
|
{
|
||||||
auto* section = new components::Section("", components::SectionHeaderMode::Hidden, this);
|
auto* section = new components::Section("", components::SectionHeaderMode::Hidden, this);
|
||||||
|
section->setBodyExpanding(true); // let the tree fill the panel's height
|
||||||
|
|
||||||
tree_ = new QTreeWidget(section);
|
tree_ = new QTreeWidget(section);
|
||||||
|
tree_->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Expanding);
|
||||||
tree_->setColumnCount(2);
|
tree_->setColumnCount(2);
|
||||||
tree_->setHeaderLabels({"Spatial Item", ""});
|
tree_->setHeaderLabels({"Spatial Item", ""});
|
||||||
tree_->setIconSize(QSize(16, 16));
|
tree_->setIconSize(QSize(16, 16));
|
||||||
@@ -52,6 +67,20 @@ SpatialHierarchyPanel::SpatialHierarchyPanel(QWidget* parent)
|
|||||||
if (!item || column != 1) return;
|
if (!item || column != 1) return;
|
||||||
emit visibilityToggleRequested(itemPath(item));
|
emit visibilityToggleRequested(itemPath(item));
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Right-click: recursive expand/collapse of a subtree or the whole tree.
|
||||||
|
tree_->setContextMenuPolicy(Qt::CustomContextMenu);
|
||||||
|
connect(tree_, &QTreeWidget::customContextMenuRequested, this, [this](const QPoint& pos) {
|
||||||
|
QMenu menu(tree_);
|
||||||
|
if (QTreeWidgetItem* item = tree_->itemAt(pos); item && item->childCount() > 0) {
|
||||||
|
menu.addAction("Expand Subtree", tree_, [item]() { setSubtreeExpanded(item, true); });
|
||||||
|
menu.addAction("Collapse Subtree", tree_, [item]() { setSubtreeExpanded(item, false); });
|
||||||
|
menu.addSeparator();
|
||||||
|
}
|
||||||
|
menu.addAction("Expand All", tree_, [this]() { tree_->expandAll(); });
|
||||||
|
menu.addAction("Collapse All", tree_, [this]() { tree_->collapseAll(); });
|
||||||
|
menu.exec(tree_->viewport()->mapToGlobal(pos));
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
void SpatialHierarchyPanel::setNodes(const QList<TreeNode>& nodes) {
|
void SpatialHierarchyPanel::setNodes(const QList<TreeNode>& nodes) {
|
||||||
|
|||||||
@@ -23,11 +23,33 @@
|
|||||||
#include "Panel.h"
|
#include "Panel.h"
|
||||||
|
|
||||||
#include "../../SessionState.h"
|
#include "../../SessionState.h"
|
||||||
|
#include "../../../ifcviewer/SceneLoader.h"
|
||||||
|
#include "../../../ifcparse/file.h"
|
||||||
|
#include "../../../ifcparse/schema.h"
|
||||||
|
|
||||||
|
#include "element.h" // helpers: get_spatial_children, get_string_attribute
|
||||||
|
|
||||||
|
#include <QCollator>
|
||||||
|
|
||||||
|
#include <algorithm>
|
||||||
|
|
||||||
namespace bonsaiviewer::modules::spatial_hierarchy {
|
namespace bonsaiviewer::modules::spatial_hierarchy {
|
||||||
|
|
||||||
namespace {
|
namespace {
|
||||||
|
|
||||||
|
// Sort siblings by name with natural ordering (so "Level 2" precedes "Level 10").
|
||||||
|
void sortByName(QList<TreeNode>& nodes) {
|
||||||
|
static const QCollator collator = [] {
|
||||||
|
QCollator c;
|
||||||
|
c.setNumericMode(true);
|
||||||
|
c.setCaseSensitivity(Qt::CaseInsensitive);
|
||||||
|
return c;
|
||||||
|
}();
|
||||||
|
std::sort(nodes.begin(), nodes.end(), [](const TreeNode& a, const TreeNode& b) {
|
||||||
|
return collator.compare(a.name, b.name) < 0;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
TreeNode* findNodeRecursive(QList<TreeNode>& nodes, const NodePath& path, int depth) {
|
TreeNode* findNodeRecursive(QList<TreeNode>& nodes, const NodePath& path, int depth) {
|
||||||
for (auto& node : nodes) {
|
for (auto& node : nodes) {
|
||||||
if (node.name != path.at(depth)) continue;
|
if (node.name != path.at(depth)) continue;
|
||||||
@@ -37,6 +59,33 @@ TreeNode* findNodeRecursive(QList<TreeNode>& nodes, const NodePath& path, int de
|
|||||||
return nullptr;
|
return nullptr;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
ItemKind kindOf(const express::Base& element) {
|
||||||
|
const auto& declaration = element.declaration();
|
||||||
|
if (declaration.is("IfcSite")) return ItemKind::Site;
|
||||||
|
if (declaration.is("IfcBuilding")) return ItemKind::Building;
|
||||||
|
if (declaration.is("IfcBuildingStorey")) return ItemKind::Storey;
|
||||||
|
return ItemKind::Space; // IfcSpace, IfcSpatialZone, …
|
||||||
|
}
|
||||||
|
|
||||||
|
QString displayName(const express::Base& element) {
|
||||||
|
if (auto name = get_string_attribute(element, "Name"); name && !name->empty()) {
|
||||||
|
return QString::fromStdString(*name);
|
||||||
|
}
|
||||||
|
return QString::fromStdString(element.declaration().name());
|
||||||
|
}
|
||||||
|
|
||||||
|
TreeNode buildNode(const express::Base& element) {
|
||||||
|
TreeNode node;
|
||||||
|
node.name = displayName(element);
|
||||||
|
node.kind = kindOf(element);
|
||||||
|
node.visible = true;
|
||||||
|
for (const auto& child : get_spatial_children(element)) {
|
||||||
|
node.children.append(buildNode(child));
|
||||||
|
}
|
||||||
|
sortByName(node.children);
|
||||||
|
return node;
|
||||||
|
}
|
||||||
|
|
||||||
} // namespace
|
} // namespace
|
||||||
|
|
||||||
SpatialHierarchyPanelView::SpatialHierarchyPanelView(SpatialHierarchyPanel* widget,
|
SpatialHierarchyPanelView::SpatialHierarchyPanelView(SpatialHierarchyPanel* widget,
|
||||||
@@ -44,14 +93,6 @@ SpatialHierarchyPanelView::SpatialHierarchyPanelView(SpatialHierarchyPanel* widg
|
|||||||
QObject* parent)
|
QObject* parent)
|
||||||
: QObject(parent), widget_(widget), session_state_(session_state)
|
: QObject(parent), widget_(widget), session_state_(session_state)
|
||||||
{
|
{
|
||||||
nodes_ = {
|
|
||||||
{"Site A", ItemKind::Site, true,
|
|
||||||
{{"Building 01", ItemKind::Building, true,
|
|
||||||
{{"Level 02", ItemKind::Storey, true,
|
|
||||||
{{"Lobby", ItemKind::Space, true, {}},
|
|
||||||
{"Core", ItemKind::Space, true, {}}}}}}}},
|
|
||||||
};
|
|
||||||
|
|
||||||
connect(widget_, &SpatialHierarchyPanel::visibilityToggleRequested, this, [this](const NodePath& path) {
|
connect(widget_, &SpatialHierarchyPanel::visibilityToggleRequested, this, [this](const NodePath& path) {
|
||||||
if (auto* node = findNode(path)) {
|
if (auto* node = findNode(path)) {
|
||||||
node->visible = !node->visible;
|
node->visible = !node->visible;
|
||||||
@@ -60,6 +101,42 @@ SpatialHierarchyPanelView::SpatialHierarchyPanelView(SpatialHierarchyPanel* widg
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// The tree reflects the active model only. Rebuild when it changes, when its
|
||||||
|
// geometry or its live IFC data source arrives (the .ifc for a sidecar hit
|
||||||
|
// loads asynchronously), and on project open/reset.
|
||||||
|
connect(session_state_, &bonsaiviewer::SessionState::activeModelChanged, this, [this](const QString&) { rebuild(); });
|
||||||
|
connect(session_state_, &bonsaiviewer::SessionState::modelDataSourceReady, this, [this](uint32_t) { rebuild(); });
|
||||||
|
connect(session_state_, &bonsaiviewer::SessionState::modelGeometryReady, this, [this](uint32_t) { rebuild(); });
|
||||||
|
connect(session_state_, &bonsaiviewer::SessionState::projectOpened, this, [this](const QString&) { rebuild(); });
|
||||||
|
connect(session_state_, &bonsaiviewer::SessionState::projectReset, this, [this]() { rebuild(); });
|
||||||
|
|
||||||
|
rebuild();
|
||||||
|
}
|
||||||
|
|
||||||
|
void SpatialHierarchyPanelView::rebuild() {
|
||||||
|
nodes_.clear();
|
||||||
|
|
||||||
|
auto* loader = session_state_->loader();
|
||||||
|
const QString active_model_id = session_state_->activeModelId();
|
||||||
|
if (loader != nullptr && !active_model_id.isEmpty()) {
|
||||||
|
const uint32_t session_model_id = session_state_->sessionModelIdForModelId(active_model_id);
|
||||||
|
ifcopenshell::file* file = session_model_id != 0 ? loader->ifcFile(session_model_id) : nullptr;
|
||||||
|
if (file != nullptr) { // null for a geometry-only model with no live IFC
|
||||||
|
try {
|
||||||
|
// IfcProject → IfcSite → … ; start the tree at the project's
|
||||||
|
// spatial children (the project itself has no ItemKind).
|
||||||
|
for (const auto& project : file->instances_by_type("IfcProject")) {
|
||||||
|
for (const auto& child : get_spatial_children(project)) {
|
||||||
|
nodes_.append(buildNode(child));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (const std::exception&) {
|
||||||
|
// Unsupported schema or malformed decomposition — show nothing.
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
sortByName(nodes_);
|
||||||
reload();
|
reload();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -38,6 +38,7 @@ public:
|
|||||||
QObject* parent = nullptr);
|
QObject* parent = nullptr);
|
||||||
|
|
||||||
private:
|
private:
|
||||||
|
void rebuild(); // re-derive nodes_ from the loaded models' IFC spatial structure
|
||||||
void reload();
|
void reload();
|
||||||
TreeNode* findNode(const NodePath& path);
|
TreeNode* findNode(const NodePath& path);
|
||||||
|
|
||||||
|
|||||||
@@ -159,6 +159,25 @@ express::Base get_aggregate_s(const express::Base& element) {
|
|||||||
return relationship.RelatingObject();
|
return relationship.RelatingObject();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// The spatial-structure children aggregated under this element (IsDecomposedBy →
|
||||||
|
// RelatedObjects, filtered to spatial elements).
|
||||||
|
template <typename Schema>
|
||||||
|
std::vector<express::Base> get_spatial_children_s(const express::Base& element) {
|
||||||
|
std::vector<express::Base> children;
|
||||||
|
const auto object = element.template as<typename Schema::IfcObjectDefinition>();
|
||||||
|
if (!object) {
|
||||||
|
return children;
|
||||||
|
}
|
||||||
|
for (const auto& relationship : object.IsDecomposedBy()) {
|
||||||
|
for (const auto& related : relationship.RelatedObjects()) {
|
||||||
|
if (related.template as<typename Schema::IfcSpatialStructureElement>()) {
|
||||||
|
children.push_back(related);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return children;
|
||||||
|
}
|
||||||
|
|
||||||
// ifcopenshell.util.element.get_container (should_get_direct=false, no
|
// ifcopenshell.util.element.get_container (should_get_direct=false, no
|
||||||
// ifc_class): the directly containing spatial element, or the container of the
|
// ifc_class): the directly containing spatial element, or the container of the
|
||||||
// aggregate parent for an aggregated part.
|
// aggregate parent for an aggregated part.
|
||||||
@@ -239,6 +258,20 @@ express::Base get_container(const express::Base& element) {
|
|||||||
unsupported_schema(name);
|
unsupported_schema(name);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
std::vector<express::Base> get_spatial_children(const express::Base& element) {
|
||||||
|
if (!element) {
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
const std::string name = schema_name(element);
|
||||||
|
#define IFCOPENSHELL_DISPATCH(Schema, Identifier) \
|
||||||
|
if (name == Identifier) { \
|
||||||
|
return get_spatial_children_s<Schema>(element); \
|
||||||
|
}
|
||||||
|
IFCOPENSHELL_HELPER_FOR_EACH_SCHEMA(IFCOPENSHELL_DISPATCH)
|
||||||
|
#undef IFCOPENSHELL_DISPATCH
|
||||||
|
unsupported_schema(name);
|
||||||
|
}
|
||||||
|
|
||||||
// getattr(element, name) for a string- or enum-valued attribute. Reads by name,
|
// getattr(element, name) for a string- or enum-valued attribute. Reads by name,
|
||||||
// so it works uniformly across the various subtypes that carry a given
|
// so it works uniformly across the various subtypes that carry a given
|
||||||
// attribute (PredefinedType, Name, ...).
|
// attribute (PredefinedType, Name, ...).
|
||||||
|
|||||||
@@ -54,6 +54,11 @@ express::Base get_container(const express::Base& element);
|
|||||||
std::optional<std::string> get_string_attribute(const express::Base& element,
|
std::optional<std::string> get_string_attribute(const express::Base& element,
|
||||||
const std::string& name);
|
const std::string& name);
|
||||||
|
|
||||||
|
// The spatial-structure elements aggregated directly under `element` (its
|
||||||
|
// IsDecomposedBy → RelatedObjects, filtered to spatial elements). Used to walk
|
||||||
|
// the IfcProject → IfcSite → IfcBuilding → IfcBuildingStorey → IfcSpace tree.
|
||||||
|
std::vector<express::Base> get_spatial_children(const express::Base& element);
|
||||||
|
|
||||||
// The element's direct EXPRESS attributes that have a primitive scalar value
|
// The element's direct EXPRESS attributes that have a primitive scalar value
|
||||||
// (string / enum / integer / real / boolean / logical), as (name, formatted
|
// (string / enum / integer / real / boolean / logical), as (name, formatted
|
||||||
// value) pairs in declaration order. Attributes that are entity references,
|
// value) pairs in declaration order. Attributes that are entity references,
|
||||||
|
|||||||
Reference in New Issue
Block a user