mirror of
https://github.com/IfcOpenShell/IfcOpenShell.git
synced 2026-08-14 19:34:34 +00:00
properties: real attributes + relationships; keep selection on deselect
Add helpers to src/helpers/element: get_scalar_attributes (primitive EXPRESS attributes only — entity refs / aggregates omitted), get_type and get_container (ports of ifcopenshell.util.element), and a public get_string_attribute for safe by-name reads. Wire them into the properties panel: - Attributes section shows the element's direct primitive attributes for live entities, or cached GlobalId / Name for geometry-only elements. - Relationships section shows the construction Type and spatial Container by name (falling back to the class when unnamed). - Placeholders are cleared once a project is loaded, so no mock data leaks. Also: a deselect (click on empty space -> object_id 0) no longer resets the panel; it keeps showing the last active object. Project reset/open still clear it. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -35,6 +35,10 @@ PropertiesPanelView::PropertiesPanelView(PropertiesPanel* widget,
|
||||
: QObject(parent), widget_(widget), session_state_(session_state)
|
||||
{
|
||||
connect(session_state_, &bonsaiviewer::SessionState::selectionChanged, this, [this](uint32_t object_id) {
|
||||
// A deselect (click on empty space → object_id 0) leaves the panel
|
||||
// showing the last active object rather than resetting to the empty
|
||||
// placeholder. Project reset/open below still clear it explicitly.
|
||||
if (object_id == 0) return;
|
||||
refresh(object_id);
|
||||
});
|
||||
connect(session_state_, &bonsaiviewer::SessionState::projectReset, this, [this]() {
|
||||
@@ -92,10 +96,12 @@ void PropertiesPanelView::refresh(uint32_t object_id) {
|
||||
return;
|
||||
}
|
||||
|
||||
// A real project is loaded — don't leak the placeholder predefined type.
|
||||
// It's populated below from live IFC data, or set to "N/A" for
|
||||
// geometry-only elements that have no data to read it from.
|
||||
// A real project is loaded — don't leak the placeholder attributes /
|
||||
// relationships / predefined type. They're populated below from live IFC
|
||||
// data, or from the cached basics for geometry-only elements.
|
||||
state.entity.predefined_type.clear();
|
||||
state.attributes.clear();
|
||||
state.relationships.clear();
|
||||
|
||||
auto entity = registry->findEntity(object_id);
|
||||
if (entity) {
|
||||
@@ -103,14 +109,32 @@ void PropertiesPanelView::refresh(uint32_t object_id) {
|
||||
if (auto predefined_type = get_predefined_type(*entity)) {
|
||||
state.entity.predefined_type = QString::fromStdString(*predefined_type);
|
||||
}
|
||||
// Direct EXPRESS attributes, primitives only — lists / entity refs omitted.
|
||||
for (const auto& [name, value] : get_scalar_attributes(*entity)) {
|
||||
state.attributes.append({QString::fromStdString(name), QString::fromStdString(value)});
|
||||
}
|
||||
// Relationships: the construction type and the spatial container, shown
|
||||
// by name (falling back to the entity class when unnamed).
|
||||
auto display_name = [](const express::Base& related) -> QString {
|
||||
if (auto name = get_string_attribute(related, "Name"); name && !name->empty()) {
|
||||
return QString::fromStdString(*name);
|
||||
}
|
||||
return QString::fromStdString(related.declaration().name());
|
||||
};
|
||||
if (express::Base type = get_type(*entity)) {
|
||||
state.relationships.append({"Type", display_name(type)});
|
||||
}
|
||||
if (express::Base container = get_container(*entity)) {
|
||||
state.relationships.append({"Container", display_name(container)});
|
||||
}
|
||||
if (!state.property_sets.isEmpty() && !state.property_sets[1].rows.isEmpty()) {
|
||||
state.property_sets[1].rows[0].value = state.entity.entity_class;
|
||||
}
|
||||
} else {
|
||||
// No live IFC source for this object — typical when a pure-geometry
|
||||
// .ifcview sidecar was loaded without its .ifc/.rdb sibling. Fall
|
||||
// back to the basic info cached in the element registry so the
|
||||
// panel still shows class / name / guid for visible elements.
|
||||
// .ifcview sidecar was loaded without its .ifc/.rdb sibling. Fall back
|
||||
// to the basic info cached in the element registry so the panel still
|
||||
// shows class / GlobalId / Name for visible elements.
|
||||
auto info = registry->findBasicElementInfo(object_id);
|
||||
if (info && !info->type.isEmpty()) {
|
||||
state.entity.entity_class = info->type;
|
||||
@@ -120,14 +144,11 @@ void PropertiesPanelView::refresh(uint32_t object_id) {
|
||||
state.property_sets[1].rows[0].value = info->type;
|
||||
}
|
||||
}
|
||||
if (info && !info->name.isEmpty()) {
|
||||
state.attributes[1].value = info->name;
|
||||
if (state.property_sets.size() > 1 && state.property_sets[1].rows.size() > 1) {
|
||||
state.property_sets[1].rows[1].value = info->name;
|
||||
}
|
||||
}
|
||||
if (info && !info->guid.isEmpty()) {
|
||||
state.attributes[0].value = info->guid;
|
||||
state.attributes.append({"GlobalId", info->guid});
|
||||
}
|
||||
if (info && !info->name.isEmpty()) {
|
||||
state.attributes.append({"Name", info->name});
|
||||
}
|
||||
}
|
||||
widget_->render(state);
|
||||
|
||||
+143
-17
@@ -27,7 +27,10 @@
|
||||
#include "../ifcparse/schema.h"
|
||||
#include "schema_dispatch.i"
|
||||
|
||||
#include <boost/logic/tribool.hpp>
|
||||
|
||||
#include <cstddef>
|
||||
#include <sstream>
|
||||
#include <type_traits>
|
||||
|
||||
namespace {
|
||||
@@ -46,31 +49,36 @@ std::string schema_name(const express::Base& instance) {
|
||||
throw ifcopenshell::exception("No helper implementation was built for schema " + name);
|
||||
}
|
||||
|
||||
// getattr(element, name) for a string- or enum-valued attribute. std::nullopt
|
||||
// when the attribute is not part of this entity's type (Python's absent
|
||||
// getattr) or when it is IFC null. Reads by name, so it works uniformly across
|
||||
// the various IfcElement / IfcType* subtypes that carry PredefinedType et al.
|
||||
std::optional<std::string> get_string_attribute(const express::Base& element,
|
||||
const std::string& name) {
|
||||
const ifcopenshell::entity* declaration = element.declaration().as_entity();
|
||||
if (declaration == nullptr) {
|
||||
return std::nullopt;
|
||||
}
|
||||
const std::ptrdiff_t index = declaration->attribute_index(name);
|
||||
if (index < 0) {
|
||||
return std::nullopt;
|
||||
}
|
||||
const attribute_value value = element.get_attribute_value(static_cast<std::size_t>(index));
|
||||
// A primitive scalar attribute value formatted for display, or std::nullopt for
|
||||
// IFC null and for non-primitive values (entity references, aggregates/lists,
|
||||
// binary) — which the properties UI omits.
|
||||
std::optional<std::string> format_scalar(const attribute_value& value) {
|
||||
if (value.isNull()) {
|
||||
return std::nullopt;
|
||||
}
|
||||
switch (value.type()) {
|
||||
case ifcopenshell::Argument_STRING:
|
||||
return static_cast<std::string>(value);
|
||||
case ifcopenshell::Argument_ENUMERATION: {
|
||||
const enumeration_reference enumeration = value;
|
||||
return enumeration.value() ? std::string(enumeration.value()) : std::string();
|
||||
}
|
||||
case ifcopenshell::Argument_STRING:
|
||||
return static_cast<std::string>(value);
|
||||
case ifcopenshell::Argument_INT:
|
||||
return std::to_string(static_cast<int>(value));
|
||||
case ifcopenshell::Argument_DOUBLE: {
|
||||
std::ostringstream stream;
|
||||
stream << static_cast<double>(value);
|
||||
return stream.str();
|
||||
}
|
||||
case ifcopenshell::Argument_BOOL:
|
||||
return static_cast<bool>(value) ? std::string("True") : std::string("False");
|
||||
case ifcopenshell::Argument_LOGICAL: {
|
||||
const boost::logic::tribool logical = value;
|
||||
if (boost::logic::indeterminate(logical)) {
|
||||
return std::string("UNKNOWN");
|
||||
}
|
||||
return static_cast<bool>(logical) ? std::string("True") : std::string("False");
|
||||
}
|
||||
default:
|
||||
return std::nullopt;
|
||||
}
|
||||
@@ -129,6 +137,45 @@ std::optional<std::string> get_predefined_type_s(const express::Base& element) {
|
||||
return predefined_type;
|
||||
}
|
||||
|
||||
// ifcopenshell.util.element.get_aggregate: the aggregate parent, via the
|
||||
// Decomposes inverse (IfcRelAggregates.RelatingObject).
|
||||
template <typename Schema>
|
||||
express::Base get_aggregate_s(const express::Base& element) {
|
||||
const auto object = element.template as<typename Schema::IfcObjectDefinition>();
|
||||
if (!object) {
|
||||
return {};
|
||||
}
|
||||
const auto decomposes = object.Decomposes();
|
||||
if (decomposes.empty()) {
|
||||
return {};
|
||||
}
|
||||
const auto relationship = decomposes.front();
|
||||
if constexpr (!is_ifc4_or_higher<Schema>::value) {
|
||||
// IFC2X3 reuses Decomposes for both aggregates and nests.
|
||||
if (!relationship.template as<typename Schema::IfcRelAggregates>()) {
|
||||
return {};
|
||||
}
|
||||
}
|
||||
return relationship.RelatingObject();
|
||||
}
|
||||
|
||||
// ifcopenshell.util.element.get_container (should_get_direct=false, no
|
||||
// ifc_class): the directly containing spatial element, or the container of the
|
||||
// aggregate parent for an aggregated part.
|
||||
template <typename Schema>
|
||||
express::Base get_container_s(const express::Base& element) {
|
||||
if (const auto product = element.template as<typename Schema::IfcElement>()) {
|
||||
const auto relationships = product.ContainedInStructure();
|
||||
if (!relationships.empty()) {
|
||||
return relationships.front().RelatingStructure();
|
||||
}
|
||||
}
|
||||
if (const express::Base aggregate = get_aggregate_s<Schema>(element)) {
|
||||
return get_container_s<Schema>(aggregate);
|
||||
}
|
||||
return {};
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
std::optional<std::string> get_predefined_type(const express::Base& element) {
|
||||
@@ -144,3 +191,82 @@ std::optional<std::string> get_predefined_type(const express::Base& element) {
|
||||
#undef IFCOPENSHELL_DISPATCH
|
||||
unsupported_schema(name);
|
||||
}
|
||||
|
||||
std::vector<std::pair<std::string, std::string>> get_scalar_attributes(const express::Base& element) {
|
||||
std::vector<std::pair<std::string, std::string>> result;
|
||||
if (!element) {
|
||||
return result;
|
||||
}
|
||||
const ifcopenshell::entity* declaration = element.declaration().as_entity();
|
||||
if (declaration == nullptr) {
|
||||
return result;
|
||||
}
|
||||
// all_attributes() is supertype-first, matching get_attribute_value(index).
|
||||
const auto& attributes = declaration->all_attributes();
|
||||
for (std::size_t index = 0; index < attributes.size(); ++index) {
|
||||
if (auto value = format_scalar(element.get_attribute_value(index))) {
|
||||
result.emplace_back(attributes[index]->name(), std::move(*value));
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
express::Base get_type(const express::Base& element) {
|
||||
if (!element) {
|
||||
return {};
|
||||
}
|
||||
const std::string name = schema_name(element);
|
||||
#define IFCOPENSHELL_DISPATCH(Schema, Identifier) \
|
||||
if (name == Identifier) { \
|
||||
return get_type_s<Schema>(element); \
|
||||
}
|
||||
IFCOPENSHELL_HELPER_FOR_EACH_SCHEMA(IFCOPENSHELL_DISPATCH)
|
||||
#undef IFCOPENSHELL_DISPATCH
|
||||
unsupported_schema(name);
|
||||
}
|
||||
|
||||
express::Base get_container(const express::Base& element) {
|
||||
if (!element) {
|
||||
return {};
|
||||
}
|
||||
const std::string name = schema_name(element);
|
||||
#define IFCOPENSHELL_DISPATCH(Schema, Identifier) \
|
||||
if (name == Identifier) { \
|
||||
return get_container_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,
|
||||
// so it works uniformly across the various subtypes that carry a given
|
||||
// attribute (PredefinedType, Name, ...).
|
||||
std::optional<std::string> get_string_attribute(const express::Base& element,
|
||||
const std::string& name) {
|
||||
if (!element) {
|
||||
return std::nullopt;
|
||||
}
|
||||
const ifcopenshell::entity* declaration = element.declaration().as_entity();
|
||||
if (declaration == nullptr) {
|
||||
return std::nullopt;
|
||||
}
|
||||
const std::ptrdiff_t index = declaration->attribute_index(name);
|
||||
if (index < 0) {
|
||||
return std::nullopt;
|
||||
}
|
||||
const attribute_value value = element.get_attribute_value(static_cast<std::size_t>(index));
|
||||
if (value.isNull()) {
|
||||
return std::nullopt;
|
||||
}
|
||||
switch (value.type()) {
|
||||
case ifcopenshell::Argument_ENUMERATION: {
|
||||
const enumeration_reference enumeration = value;
|
||||
return enumeration.value() ? std::string(enumeration.value()) : std::string();
|
||||
}
|
||||
case ifcopenshell::Argument_STRING:
|
||||
return static_cast<std::string>(value);
|
||||
default:
|
||||
return std::nullopt;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -26,6 +26,8 @@
|
||||
|
||||
#include <optional>
|
||||
#include <string>
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
|
||||
// Mirrors ifcopenshell.util.element.get_predefined_type. Returns the element's
|
||||
// PredefinedType, falling back to the user-defined ObjectType / ElementType /
|
||||
@@ -35,4 +37,28 @@
|
||||
// IfcObject, or a geometry-only proxy with no live IFC data).
|
||||
std::optional<std::string> get_predefined_type(const express::Base& element);
|
||||
|
||||
// Mirrors ifcopenshell.util.element.get_type: the construction type element of
|
||||
// an occurrence (via IsTypedBy on IFC4+, IsDefinedBy on IFC2X3). A type element
|
||||
// returns itself. Empty express::Base when the element is untyped.
|
||||
express::Base get_type(const express::Base& element);
|
||||
|
||||
// Mirrors ifcopenshell.util.element.get_container (indirect, no ifc_class
|
||||
// filter): the spatial element that contains this element — the directly
|
||||
// containing spatial structure, or, for an aggregated part, the container of its
|
||||
// aggregate parent. Empty when uncontained. (The nest / filled-void /
|
||||
// voided-element branches of the Python original are not ported.)
|
||||
express::Base get_container(const express::Base& element);
|
||||
|
||||
// Safely read a string- or enum-valued attribute by name (Python's getattr).
|
||||
// std::nullopt when the attribute is absent for this entity's type or IFC null.
|
||||
std::optional<std::string> get_string_attribute(const express::Base& element,
|
||||
const std::string& name);
|
||||
|
||||
// The element's direct EXPRESS attributes that have a primitive scalar value
|
||||
// (string / enum / integer / real / boolean / logical), as (name, formatted
|
||||
// value) pairs in declaration order. Attributes that are entity references,
|
||||
// aggregates / lists, or unset (IFC null) are omitted — so the caller gets a
|
||||
// flat, display-ready view with no nested objects.
|
||||
std::vector<std::pair<std::string, std::string>> get_scalar_attributes(const express::Base& element);
|
||||
|
||||
#endif // ELEMENT_H
|
||||
|
||||
Reference in New Issue
Block a user