diff --git a/src/bonsai/bonsai/bim/module/structural/operator.py b/src/bonsai/bonsai/bim/module/structural/operator.py index c156ef1f32..bab7cffc1c 100644 --- a/src/bonsai/bonsai/bim/module/structural/operator.py +++ b/src/bonsai/bonsai/bim/module/structural/operator.py @@ -23,14 +23,13 @@ import ifcopenshell.api import ifcopenshell.api.aggregate import ifcopenshell.api.group import ifcopenshell.api.structural -import ifcopenshell.util.attribute import bonsai.bim.helper import bonsai.core.structural as core import bonsai.tool as tool from math import degrees from mathutils import Vector, Matrix from bonsai.bim.module.structural.decorator import LoadsDecorator -from typing import Literal +from typing import Literal, Any, TYPE_CHECKING class ShowLoads(bpy.types.Operator): @@ -175,39 +174,17 @@ class EnableEditingStructuralBoundaryCondition(bpy.types.Operator): bl_idname = "bim.enable_editing_structural_boundary_condition" bl_label = "Enable Editing Structural Boundary Condition" bl_options = {"REGISTER", "UNDO"} - boundary_condition: bpy.props.IntProperty() + boundary_condition: bpy.props.IntProperty() # pyright: ignore[reportRedeclaration] + + if TYPE_CHECKING: + boundary_condition: int def execute(self, context): obj = context.active_object assert obj props = tool.Structural.get_object_structural_props(obj) - props.boundary_condition_attributes.clear() - condition = tool.Ifc.get().by_id(self.boundary_condition) - schema = tool.Ifc.schema() - - for attribute in schema.declaration_by_name(condition.is_a()).all_attributes(): - value = getattr(condition, attribute.name(), None) - data_type = ifcopenshell.util.attribute.get_primitive_type(attribute) - new = props.boundary_condition_attributes.add() - new.name = attribute.name() - new.is_null = value is None - new.is_optional = attribute.optional() - if isinstance(data_type, tuple) and data_type[0] == "select": - enum_items = [s.name() for s in ifcopenshell.util.attribute.get_select_items(attribute)] - new.enum_items = json.dumps(enum_items) - if isinstance(value, bool): - new.bool_value = False if new.is_null else value - new.data_type = "bool" - new.enum_value = "IfcBoolean" - elif isinstance(value, float): - new.float_value = 0.0 if new.is_null else value - new.data_type = "float" - new.enum_value = next(i for i in enum_items if i != "IfcBoolean") - elif data_type == "string": - new.string_value = "" if new.is_null else value - new.data_type = "string" - + tool.Structural.import_boundary_condition_attributes(condition, props) props.active_boundary_condition = self.boundary_condition return {"FINISHED"} @@ -216,7 +193,10 @@ class EditStructuralBoundaryCondition(bpy.types.Operator, tool.Ifc.Operator): bl_idname = "bim.edit_structural_boundary_condition" bl_label = "Edit Structural Boundary Condition" bl_options = {"REGISTER", "UNDO"} - connection: bpy.props.IntProperty() + connection: bpy.props.IntProperty() # pyright: ignore[reportRedeclaration] + + if TYPE_CHECKING: + connection: int def _execute(self, context): obj = context.active_object @@ -227,18 +207,7 @@ class EditStructuralBoundaryCondition(bpy.types.Operator, tool.Ifc.Operator): connection = file.by_id(self.connection) condition = connection.AppliedCondition - attributes = {} - for attribute in props.boundary_condition_attributes: - if attribute.is_null: - attributes[attribute.name] = {"value": None, "type": "null"} - elif attribute.data_type == "string": - attributes[attribute.name] = {"value": attribute.string_value, "type": "string"} - elif attribute.enum_value == "IfcBoolean": - attributes[attribute.name] = {"value": attribute.bool_value, "type": attribute.enum_value} - else: - attributes[attribute.name] = {"value": attribute.float_value, "type": attribute.enum_value} - - ifcopenshell.api.structural.edit_structural_boundary_condition(file, condition=condition, attributes=attributes) + tool.Structural.export_and_apply_boundary_condition_attributes(condition, props) bpy.ops.bim.disable_editing_structural_boundary_condition() return {"FINISHED"} @@ -955,36 +924,15 @@ class EnableEditingBoundaryCondition(bpy.types.Operator): bl_idname = "bim.enable_editing_boundary_condition" bl_label = "Enable Editing Boundary Condition" bl_options = {"REGISTER", "UNDO"} - boundary_condition: bpy.props.IntProperty() + boundary_condition: bpy.props.IntProperty() # pyright: ignore[reportRedeclaration] + + if TYPE_CHECKING: + boundary_condition: int def execute(self, context): - props = tool.Structural.get_structural_props() - props.boundary_condition_attributes.clear() - boundary_condition = tool.Ifc.get().by_id(self.boundary_condition) - # bonsai.bim.helper.import_attributes(data["type"], props.boundary_condition_attributes, data) - schema = tool.Ifc.schema() - for attribute in schema.declaration_by_name(boundary_condition.is_a()).all_attributes(): - value = getattr(boundary_condition, attribute.name(), None) - data_type = ifcopenshell.util.attribute.get_primitive_type(attribute) - new = props.boundary_condition_attributes.add() - new.name = attribute.name() - new.is_null = value is None - new.is_optional = attribute.optional() - if isinstance(data_type, tuple) and data_type[0] == "select": - enum_items = [s.name() for s in ifcopenshell.util.attribute.get_select_items(attribute)] - new.enum_items = json.dumps(enum_items) - if isinstance(value, bool): - new.bool_value = False if new.is_null else value - new.data_type = "bool" - new.enum_value = "IfcBoolean" - elif isinstance(value, float): - new.float_value = 0.0 if new.is_null else value - new.data_type = "float" - new.enum_value = next(i for i in enum_items if i != "IfcBoolean") - elif data_type == "string": - new.string_value = "" if new.is_null else value - new.data_type = "string" + props = tool.Structural.get_structural_props() + tool.Structural.import_boundary_condition_attributes(boundary_condition, props) props.active_boundary_condition_id = self.boundary_condition return {"FINISHED"} @@ -1023,22 +971,8 @@ class EditBoundaryCondition(bpy.types.Operator, tool.Ifc.Operator): def _execute(self, context): props = tool.Structural.get_structural_props() - self.file = tool.Ifc.get() - # attributes = bonsai.bim.helper.export_attributes(props.boundary_condition_attributes) - attributes = {} - for attribute in props.boundary_condition_attributes: - if attribute.is_null: - attributes[attribute.name] = {"value": None, "type": "null"} - elif attribute.data_type == "string": - attributes[attribute.name] = {"value": attribute.string_value, "type": "string"} - elif attribute.enum_value == "IfcBoolean": - attributes[attribute.name] = {"value": attribute.bool_value, "type": attribute.enum_value} - else: - attributes[attribute.name] = {"value": attribute.float_value, "type": attribute.enum_value} - ifcopenshell.api.structural.edit_structural_boundary_condition( - self.file, - condition=self.file.by_id(props.active_boundary_condition_id), - attributes=attributes, - ) + ifc_file = tool.Ifc.get() + condition = ifc_file.by_id(props.active_boundary_condition_id) + tool.Structural.export_and_apply_boundary_condition_attributes(condition, props) bpy.ops.bim.load_boundary_conditions() return {"FINISHED"} diff --git a/src/bonsai/bonsai/bim/module/structural/ui.py b/src/bonsai/bonsai/bim/module/structural/ui.py index ea359e4ca2..44b56633a1 100644 --- a/src/bonsai/bonsai/bim/module/structural/ui.py +++ b/src/bonsai/bonsai/bim/module/structural/ui.py @@ -79,7 +79,20 @@ def draw_boundary_condition_ui( def draw_boundary_condition_editable_ui( layout: bpy.types.UILayout, props: Union[BIMStructuralProperties, BIMObjectStructuralProperties] ) -> None: - draw_attributes(props.boundary_condition_attributes, layout) + # Reimplement `draw_attributes` as we need to support multiple types for the same attribute. + for attribute in props.boundary_condition_attributes: + if attribute.data_type == "string": + row = layout.row(align=True) + bonsai.bim.helper.draw_attribute(attribute, row, enable_search=True) + else: + row = layout.row(align=True) + row.prop(attribute, "enum_value", text=attribute["name"]) + if attribute.enum_value == "IfcBoolean": + row.prop(attribute, "bool_value", text="") + else: + row.prop(attribute, "float_value", text="") + if attribute.is_optional: + row.prop(attribute, "is_null", icon="RADIOBUT_OFF" if attribute.is_null else "RADIOBUT_ON", text="") def draw_boundary_condition_read_only_ui(layout: bpy.types.UILayout, boundary_condition: dict[str, Any]) -> None: @@ -605,6 +618,7 @@ class BIM_PT_boundary_conditions(Panel): BoundaryConditionsData.load() self.props = tool.Structural.get_structural_props() + assert self.layout row = self.layout.row(align=True) row.label( @@ -636,9 +650,8 @@ class BIM_PT_boundary_conditions(Panel): "active_boundary_condition_index", ) - if self.props.active_boundary_condition_id: - draw_boundary_condition_editable_ui(self.layout, self.props) - # bonsai.bim.helper.draw_attributes(self.props.boundary_condition_attributes, self.layout) + if self.props.active_boundary_condition_id: + draw_boundary_condition_editable_ui(self.layout, self.props) class BIM_UL_boundary_conditions(UIList): diff --git a/src/bonsai/bonsai/tool/structural.py b/src/bonsai/bonsai/tool/structural.py index ce923cf4e4..cf984bf176 100644 --- a/src/bonsai/bonsai/tool/structural.py +++ b/src/bonsai/bonsai/tool/structural.py @@ -20,6 +20,8 @@ from __future__ import annotations import bpy import ifcopenshell import ifcopenshell.api.context +import ifcopenshell.api.structural +import ifcopenshell.util.attribute import ifcopenshell.util.representation import json import bonsai.bim.helper @@ -29,6 +31,7 @@ from typing import Union, Any, TYPE_CHECKING if TYPE_CHECKING: from bonsai.bim.module.structural.prop import BIMStructuralProperties, BIMObjectStructuralProperties + from ifcopenshell.api.structural.edit_structural_boundary_condition import AttributeDict class Structural(bonsai.core.tool.Structural): @@ -214,3 +217,68 @@ class Structural(bonsai.core.tool.Structural): return None return undefined_representation + + @classmethod + def import_boundary_condition_attributes( + cls, + boundary_condition: ifcopenshell.entity_instance, + props: Union[BIMStructuralProperties, BIMObjectStructuralProperties], + ) -> None: + props_attrs = props.boundary_condition_attributes + props_attrs.clear() + schema = tool.Ifc.schema() + # Don't use `import_attributes`, + # because we need to support 2 types of values for the same props. + ifc_class = boundary_condition.is_a() + entity = schema.declaration_by_name(ifc_class).as_entity() + assert entity + for attribute in entity.all_attributes(): + attribute_name = attribute.name() + value = getattr(boundary_condition, attribute_name) + new = props_attrs.add() + new.name = attribute_name + new.ifc_class = ifc_class + new.is_null = value is None + new.is_optional = attribute.optional() + + # Select attribute values are typically wrapped. + if isinstance(value, ifcopenshell.entity_instance): + value = value.wrappedValue + + if attribute_name == "Name": + new.string_value = "" if new.is_null else value + new.data_type = "string" + else: + enum_items = [s.name() for s in ifcopenshell.util.attribute.get_select_items(attribute)] + new.enum_items = json.dumps(enum_items) + if isinstance(value, bool): + new.bool_value = False if new.is_null else value + new.data_type = "boolean" + new.enum_value = "IfcBoolean" + else: + print(value) + new.float_value = 0 if new.is_null else value + new.data_type = "float" + new.enum_value = next(i for i in enum_items if i != "IfcBoolean") + + @classmethod + def export_and_apply_boundary_condition_attributes( + cls, + boundary_condition: ifcopenshell.entity_instance, + props: Union[BIMStructuralProperties, BIMObjectStructuralProperties], + ) -> None: + attributes: dict[str, AttributeDict] = {} + for attribute in props.boundary_condition_attributes: + if attribute.is_null: + attributes[attribute.name] = {"value": None, "type": "null"} + elif attribute.data_type == "string": + attributes[attribute.name] = {"value": attribute.string_value, "type": "string"} + elif attribute.enum_value == "IfcBoolean": + attributes[attribute.name] = {"value": attribute.bool_value, "type": attribute.enum_value} + else: + attributes[attribute.name] = {"value": attribute.float_value, "type": attribute.enum_value} + + ifc_file = tool.Ifc.get() + ifcopenshell.api.structural.edit_structural_boundary_condition( + ifc_file, condition=boundary_condition, attributes=attributes + ) diff --git a/src/ifcopenshell-python/ifcopenshell/api/structural/edit_structural_boundary_condition.py b/src/ifcopenshell-python/ifcopenshell/api/structural/edit_structural_boundary_condition.py index 3d33e4e8ad..ca9cfa7a66 100644 --- a/src/ifcopenshell-python/ifcopenshell/api/structural/edit_structural_boundary_condition.py +++ b/src/ifcopenshell-python/ifcopenshell/api/structural/edit_structural_boundary_condition.py @@ -16,11 +16,28 @@ # You should have received a copy of the GNU Lesser General Public License # along with IfcOpenShell. If not, see . import ifcopenshell -from typing import Any +from typing import Any, TypedDict, Literal, Union + + +AttributeDict = TypedDict( + "AttributeDict", + { + "type": Union[ + Literal[ + "string", + "null", + ], + str, # IFC Class. + ], + "value": Any, + }, +) def edit_structural_boundary_condition( - file: ifcopenshell.file, condition: ifcopenshell.entity_instance, attributes: dict[str, Any] + file: ifcopenshell.file, + condition: ifcopenshell.entity_instance, + attributes: dict[str, AttributeDict], ) -> None: """Edits the attributes of an IfcBoundaryCondition @@ -29,6 +46,7 @@ def edit_structural_boundary_condition( :param condition: The IfcBoundaryCondition entity you want to edit :param attributes: a dictionary of attribute names and values. + Each value is represented by a dictionary. :return: None """ for name, data in attributes.items():