diff --git a/src/bonsaiviewer/modules/properties/View.cpp b/src/bonsaiviewer/modules/properties/View.cpp index 0b992e0f1a..5fe66566c6 100644 --- a/src/bonsaiviewer/modules/properties/View.cpp +++ b/src/bonsaiviewer/modules/properties/View.cpp @@ -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); diff --git a/src/helpers/element.cpp b/src/helpers/element.cpp index b9968a8aba..d6d854a283 100644 --- a/src/helpers/element.cpp +++ b/src/helpers/element.cpp @@ -27,7 +27,10 @@ #include "../ifcparse/schema.h" #include "schema_dispatch.i" +#include + #include +#include #include 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 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(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 format_scalar(const attribute_value& value) { if (value.isNull()) { return std::nullopt; } switch (value.type()) { + case ifcopenshell::Argument_STRING: + return static_cast(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(value); + case ifcopenshell::Argument_INT: + return std::to_string(static_cast(value)); + case ifcopenshell::Argument_DOUBLE: { + std::ostringstream stream; + stream << static_cast(value); + return stream.str(); + } + case ifcopenshell::Argument_BOOL: + return static_cast(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(logical) ? std::string("True") : std::string("False"); + } default: return std::nullopt; } @@ -129,6 +137,45 @@ std::optional 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 +express::Base get_aggregate_s(const express::Base& element) { + const auto object = element.template as(); + if (!object) { + return {}; + } + const auto decomposes = object.Decomposes(); + if (decomposes.empty()) { + return {}; + } + const auto relationship = decomposes.front(); + if constexpr (!is_ifc4_or_higher::value) { + // IFC2X3 reuses Decomposes for both aggregates and nests. + if (!relationship.template as()) { + 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 +express::Base get_container_s(const express::Base& element) { + if (const auto product = element.template as()) { + const auto relationships = product.ContainedInStructure(); + if (!relationships.empty()) { + return relationships.front().RelatingStructure(); + } + } + if (const express::Base aggregate = get_aggregate_s(element)) { + return get_container_s(aggregate); + } + return {}; +} + } // namespace std::optional get_predefined_type(const express::Base& element) { @@ -144,3 +191,82 @@ std::optional get_predefined_type(const express::Base& element) { #undef IFCOPENSHELL_DISPATCH unsupported_schema(name); } + +std::vector> get_scalar_attributes(const express::Base& element) { + std::vector> 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(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(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 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(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(value); + default: + return std::nullopt; + } +} diff --git a/src/helpers/element.h b/src/helpers/element.h index c340c262f4..2b15c3424b 100644 --- a/src/helpers/element.h +++ b/src/helpers/element.h @@ -26,6 +26,8 @@ #include #include +#include +#include // 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 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 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> get_scalar_attributes(const express::Base& element); + #endif // ELEMENT_H