mirror of
https://github.com/IfcOpenShell/IfcOpenShell.git
synced 2026-08-11 18:16:40 +00:00
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:
@@ -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));
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user