spatial hierarchy: storey elevation + Long Name column + resizable layout

- helpers/placement: port get_storey_elevation (placement Z, falling back
  to the Elevation attribute), matching ifcopenshell.util.placement.
- Add a secondary column: the storey elevation for IfcBuildingStorey,
  otherwise the LongName when filled. Elevations are right-aligned.
- Columns: Name is drag-resizable (interactive) and defaults to 20% of the
  width, Long Name stretches to fill the rest, and the eye is pinned to the
  right at a fixed width. Header shown so the divider can be grabbed.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Dion Moult
2026-07-08 20:33:29 +10:00
parent 90196dd51d
commit 4afb3892a2
6 changed files with 95 additions and 11 deletions
@@ -25,6 +25,7 @@
#include <QHeaderView>
#include <QMenu>
#include <QShowEvent>
#include <QSizePolicy>
#include <QTreeWidget>
#include <QTreeWidgetItem>
@@ -50,21 +51,26 @@ SpatialHierarchyPanel::SpatialHierarchyPanel(QWidget* parent)
tree_ = new QTreeWidget(section);
tree_->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Expanding);
tree_->setColumnCount(2);
tree_->setHeaderLabels({"Spatial Item", ""});
tree_->setColumnCount(3);
tree_->setHeaderLabels({"Name", "Long Name", ""});
tree_->setIconSize(QSize(16, 16));
tree_->setSelectionMode(QAbstractItemView::ExtendedSelection);
tree_->setUniformRowHeights(true);
// Name is drag-resizable, Long Name fills the rest, the eye is pinned to
// the right at a fixed width. Header stays visible so the Name/Long-Name
// divider can be dragged. Initial 20/80 split applied in showEvent once the
// real width is known.
tree_->header()->setStretchLastSection(false);
tree_->header()->setSectionResizeMode(0, QHeaderView::Stretch);
tree_->header()->setSectionResizeMode(1, QHeaderView::Fixed);
tree_->header()->resizeSection(1, 28);
tree_->header()->hide();
tree_->header()->setSectionsMovable(false);
tree_->header()->setSectionResizeMode(0, QHeaderView::Interactive); // name
tree_->header()->setSectionResizeMode(1, QHeaderView::Stretch); // LongName / elevation
tree_->header()->setSectionResizeMode(2, QHeaderView::Fixed); // visibility
tree_->header()->resizeSection(2, 28);
section->addBodyWidget(tree_);
addBodyWidget(section);
connect(tree_, &QTreeWidget::itemClicked, this, [this](QTreeWidgetItem* item, int column) {
if (!item || column != 1) return;
if (!item || column != 2) return;
emit visibilityToggleRequested(itemPath(item));
});
@@ -83,6 +89,20 @@ SpatialHierarchyPanel::SpatialHierarchyPanel(QWidget* parent)
});
}
void SpatialHierarchyPanel::showEvent(QShowEvent* event) {
components::Panel::showEvent(event);
// Default the Name column to 20% of the width once the panel has a real
// layout size; Long Name (stretch) takes the rest. Left interactive after,
// so the user's own drag persists.
if (!column_widths_initialized_) {
const int available = tree_->viewport()->width();
if (available > 100) {
tree_->header()->resizeSection(0, available / 5);
column_widths_initialized_ = true;
}
}
}
void SpatialHierarchyPanel::setNodes(const QList<TreeNode>& nodes) {
tree_->clear();
for (const auto& node : nodes) {
@@ -92,11 +112,15 @@ void SpatialHierarchyPanel::setNodes(const QList<TreeNode>& nodes) {
}
void SpatialHierarchyPanel::addNode(QTreeWidgetItem* parent, const TreeNode& node) {
auto* item = new QTreeWidgetItem(parent, {node.name, ""});
item->setData(1, Qt::UserRole, node.visible);
auto* item = new QTreeWidgetItem(parent, {node.name, node.detail, ""});
item->setData(2, Qt::UserRole, node.visible);
item->setSizeHint(0, QSize(0, 24));
item->setIcon(0, components::icons::makeSvgIcon(iconPath(node.kind)));
item->setIcon(1, components::icons::makeSvgIcon(node.visible ? ":/icons/eye.svg" : ":/icons/eye-closed.svg"));
// Storey elevations read as right-aligned numbers; LongNames stay left.
if (node.kind == ItemKind::Storey) {
item->setTextAlignment(1, Qt::AlignRight | Qt::AlignVCenter);
}
item->setIcon(2, components::icons::makeSvgIcon(node.visible ? ":/icons/eye.svg" : ":/icons/eye-closed.svg"));
for (const auto& child : node.children) {
addNode(item, child);
}
@@ -27,6 +27,7 @@
class QTreeWidget;
class QTreeWidgetItem;
class QShowEvent;
namespace bonsaiviewer::modules::spatial_hierarchy {
@@ -40,12 +41,16 @@ public:
signals:
void visibilityToggleRequested(const NodePath& path);
protected:
void showEvent(QShowEvent* event) override;
private:
void addNode(QTreeWidgetItem* parent, const TreeNode& node);
NodePath itemPath(QTreeWidgetItem* item) const;
QString iconPath(ItemKind kind) const;
QTreeWidget* tree_ = nullptr;
bool column_widths_initialized_ = false;
};
} // namespace bonsaiviewer::modules::spatial_hierarchy
@@ -36,6 +36,7 @@ enum class ItemKind {
struct TreeNode {
QString name;
QString detail; // secondary column: LongName, or the elevation for storeys
ItemKind kind = ItemKind::Space;
bool visible = true;
QList<TreeNode> children;
@@ -27,7 +27,8 @@
#include "../../../ifcparse/file.h"
#include "../../../ifcparse/schema.h"
#include "element.h" // helpers: get_spatial_children, get_string_attribute
#include "element.h" // helpers: get_spatial_children, get_string_attribute
#include "placement.h" // helpers: get_storey_elevation
#include <QCollator>
@@ -79,6 +80,13 @@ TreeNode buildNode(const express::Base& element) {
node.name = displayName(element);
node.kind = kindOf(element);
node.visible = true;
// Secondary column: the storey elevation, else the LongName when filled.
if (node.kind == ItemKind::Storey) {
node.detail = QString::number(get_storey_elevation(element));
} else if (auto long_name = get_string_attribute(element, "LongName");
long_name && !long_name->empty()) {
node.detail = QString::fromStdString(*long_name);
}
for (const auto& child : get_spatial_children(element)) {
node.children.append(buildNode(child));
}
+41
View File
@@ -20,8 +20,11 @@
#include "placement.h"
#include "../ifcparse/exception.h"
#include "../ifcparse/instance_data.h"
#include "../ifcparse/schema.h"
#include "schema_dispatch.i"
#include <cstddef>
#include <type_traits>
#include <vector>
@@ -125,6 +128,31 @@ Eigen::Matrix4d get_local_placement_s(const express::Base& placement) {
return get_axis2_placement_s<Schema>(placement);
}
// ifcopenshell.util.placement.get_storey_elevation: the Z of the storey's
// placement in project units, falling back to the Elevation attribute.
template <typename Schema>
double get_storey_elevation_s(const express::Base& storey) {
const auto typed = storey.template as<typename Schema::IfcBuildingStorey>();
if (!typed) {
return 0.0;
}
if (const auto placement = typed.ObjectPlacement()) {
return get_local_placement_s<Schema>(placement)(2, 3);
}
// Fallback: the optional Elevation attribute (read by name).
const ifcopenshell::entity* declaration = storey.declaration().as_entity();
if (declaration != nullptr) {
const std::ptrdiff_t index = declaration->attribute_index("Elevation");
if (index >= 0) {
const attribute_value value = storey.get_attribute_value(static_cast<std::size_t>(index));
if (!value.isNull() && value.type() == ifcopenshell::Argument_DOUBLE) {
return static_cast<double>(value);
}
}
}
return 0.0;
}
} // namespace
Eigen::Matrix4d axes_to_placement(const Eigen::Vector3d& origin,
@@ -167,3 +195,16 @@ Eigen::Matrix4d get_local_placement(const express::Base& placement) {
#undef IFCOPENSHELL_DISPATCH
unsupported_schema(name);
}
double get_storey_elevation(const express::Base& storey) {
if (!storey) {
return 0.0;
}
const auto name = storey.declaration().schema()->name();
#define IFCOPENSHELL_DISPATCH(Schema, Identifier) \
if (name == Identifier) \
return get_storey_elevation_s<Schema>(storey);
IFCOPENSHELL_HELPER_FOR_EACH_SCHEMA(IFCOPENSHELL_DISPATCH)
#undef IFCOPENSHELL_DISPATCH
unsupported_schema(name);
}
+5
View File
@@ -47,4 +47,9 @@ Eigen::Matrix4d get_axis2_placement(const express::Base& placement);
// identity for a null input.
Eigen::Matrix4d get_local_placement(const express::Base& placement);
// ifcopenshell.util.placement.get_storey_elevation: the Z elevation of an
// IfcBuildingStorey in the project's length unit — the Z of its placement, or
// the Elevation attribute as a fallback. 0 for a non-storey or null input.
double get_storey_elevation(const express::Base& storey);
#endif // PLACEMENT_H