diff --git a/src/blenderbim/blenderbim/bim/module/structural/__init__.py b/src/blenderbim/blenderbim/bim/module/structural/__init__.py index aa72eed6a1..7c49636828 100644 --- a/src/blenderbim/blenderbim/bim/module/structural/__init__.py +++ b/src/blenderbim/blenderbim/bim/module/structural/__init__.py @@ -44,9 +44,18 @@ classes = ( operator.RemoveStructuralLoad, operator.EditStructuralLoad, operator.ToggleFilterStructuralLoads, + operator.LoadBoundaryConditions, + operator.DisableBoundaryConditionEditingUI, + operator.AddBoundaryCondition, + operator.EnableEditingBoundaryCondition, + operator.DisableEditingBoundaryCondition, + operator.RemoveBoundaryCondition, + operator.EditBoundaryCondition, + operator.ToggleFilterBoundaryConditions, prop.StructuralAnalysisModel, prop.StructuralActivity, prop.StructuralLoad, + prop.BoundaryCondition, prop.BIMStructuralProperties, prop.BIMObjectStructuralProperties, ui.BIM_PT_structural_analysis_models, @@ -59,6 +68,8 @@ classes = ( ui.BIM_PT_structural_load_cases, ui.BIM_UL_structural_loads, ui.BIM_PT_structural_loads, + ui.BIM_UL_boundary_conditions, + ui.BIM_PT_boundary_conditions, ) diff --git a/src/blenderbim/blenderbim/bim/module/structural/operator.py b/src/blenderbim/blenderbim/bim/module/structural/operator.py index c84955ef46..e0502cd19c 100644 --- a/src/blenderbim/blenderbim/bim/module/structural/operator.py +++ b/src/blenderbim/blenderbim/bim/module/structural/operator.py @@ -888,3 +888,166 @@ class ToggleFilterStructuralLoads(bpy.types.Operator): return {"FINISHED"} +class LoadBoundaryConditions(bpy.types.Operator): + bl_idname = "bim.load_boundary_conditions" + bl_label = "Load Boundary Conditions" + + def execute(self, context): + self.file = IfcStore.get_file() + props = context.scene.BIMStructuralProperties + while len(props.boundary_conditions) > 0: + props.boundary_conditions.remove(0) + if props.filtered_boundary_conditions: + names = [ + boundary_condition["Name"] or "Unnamed" for _, boundary_condition in Data.boundary_conditions.items() + ] + for ifc_definition_id, boundary_condition in Data.boundary_conditions.items(): + if ( + names.count(boundary_condition["Name"] or "Unnamed") > 1 + and len(self.file.get_inverse(self.file.by_id(ifc_definition_id))) < 2 + ): + continue + new = props.boundary_conditions.add() + new.ifc_definition_id = ifc_definition_id + new.name = boundary_condition["Name"] or "Unnamed" + new.number_of_inverse_references = len(self.file.get_inverse(self.file.by_id(ifc_definition_id))) + + else: + for ifc_definition_id, boundary_condition in Data.boundary_conditions.items(): + new = props.boundary_conditions.add() + new.ifc_definition_id = ifc_definition_id + new.name = boundary_condition["Name"] or "Unnamed" + new.number_of_inverse_references = len(self.file.get_inverse(self.file.by_id(ifc_definition_id))) + props.is_editing_boundary_conditions = True + bpy.ops.bim.disable_editing_boundary_condition() + return {"FINISHED"} + + +class ToggleFilterBoundaryConditions(bpy.types.Operator): + bl_idname = "bim.toggle_filter_boundary_conditions" + bl_label = "Toggle Filter Boundary Conditions" + + def execute(self, context): + props = context.scene.BIMStructuralProperties + props.filtered_boundary_conditions = not props.filtered_boundary_conditions + bpy.ops.bim.load_boundary_conditions() + return {"FINISHED"} + + +class DisableBoundaryConditionEditingUI(bpy.types.Operator): + bl_idname = "bim.disable_boundary_condition_editing_ui" + bl_label = "Disable Boundary Condition Editing UI" + + def execute(self, context): + context.scene.BIMStructuralProperties.is_editing_boundary_conditions = False + return {"FINISHED"} + + +class AddBoundaryCondition(bpy.types.Operator): + bl_idname = "bim.add_boundary_condition" + bl_label = "Add Boundary Condition" + ifc_class: bpy.props.StringProperty() + + def execute(self, context): + result = ifcopenshell.api.run( + "structural.add_structural_boundary_condition", + IfcStore.get_file(), + name="New Load", + ifc_class=self.ifc_class, + ) + Data.load(IfcStore.get_file()) + bpy.ops.bim.load_boundary_conditions() + bpy.ops.bim.enable_editing_boundary_condition(boundary_condition=result.id()) + return {"FINISHED"} + + +class EnableEditingBoundaryCondition(bpy.types.Operator): + bl_idname = "bim.enable_editing_boundary_condition" + bl_label = "Enable Editing Boundary Condition" + boundary_condition: bpy.props.IntProperty() + + def execute(self, context): + props = context.scene.BIMStructuralProperties + while len(props.boundary_condition_attributes) > 0: + props.boundary_condition_attributes.remove(0) + + data = Data.boundary_conditions[self.boundary_condition] + # blenderbim.bim.helper.import_attributes(data["type"], props.boundary_condition_attributes, data) + for attribute in IfcStore.get_schema().declaration_by_name(data["type"]).all_attributes(): + value = data[attribute.name()] + 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 data_type == "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 data[attribute.name()] + new.data_type = "bool" + new.enum_value = "IfcBoolean" + elif isinstance(value, float): + new.float_value = 0.0 if new.is_null else data[attribute.name()] + new.data_type = "float" + new.enum_value = [i for i in enum_items if i != "IfcBoolean"][0] + elif data_type == "string": + new.string_value = "" if new.is_null else data[attribute.name()] + new.data_type = "string" + props.active_boundary_condition_id = self.boundary_condition + return {"FINISHED"} + + +class DisableEditingBoundaryCondition(bpy.types.Operator): + bl_idname = "bim.disable_editing_boundary_condition" + bl_label = "Disable Editing Boundary Condition" + + def execute(self, context): + context.scene.BIMStructuralProperties.active_boundary_condition_id = 0 + return {"FINISHED"} + + +class RemoveBoundaryCondition(bpy.types.Operator): + bl_idname = "bim.remove_boundary_condition" + bl_label = "Remove Boundary Condition" + boundary_condition: bpy.props.IntProperty() + + def execute(self, context): + props = context.scene.BIMStructuralProperties + self.file = IfcStore.get_file() + ifcopenshell.api.run( + "structural.remove_structural_boundary_condition", + self.file, + **{"boundary_condition": self.file.by_id(self.boundary_condition)}, + ) + Data.load(IfcStore.get_file()) + bpy.ops.bim.load_boundary_conditions() + return {"FINISHED"} + + +class EditBoundaryCondition(bpy.types.Operator): + bl_idname = "bim.edit_boundary_condition" + bl_label = "Edit Boundary Condition" + + def execute(self, context): + props = context.scene.BIMStructuralProperties + self.file = IfcStore.get_file() + # attributes = blenderbim.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.run( + "structural.edit_structural_boundary_condition", + self.file, + **{"condition": self.file.by_id(props.active_boundary_condition_id), "attributes": attributes}, + ) + Data.load(IfcStore.get_file()) + bpy.ops.bim.load_boundary_conditions() + return {"FINISHED"} diff --git a/src/blenderbim/blenderbim/bim/module/structural/prop.py b/src/blenderbim/blenderbim/bim/module/structural/prop.py index 588dd79edd..7ae769c55d 100644 --- a/src/blenderbim/blenderbim/bim/module/structural/prop.py +++ b/src/blenderbim/blenderbim/bim/module/structural/prop.py @@ -73,6 +73,15 @@ def getStructuralLoadTypes(self, context): return structuralloadtypes_enum +def getBoundaryConditionTypes(self, context): + file = IfcStore.get_file() + if file: + declaration = IfcStore.get_schema().declaration_by_name("IfcBoundaryCondition") + boundaryconditiontypes_enum = [(d.name(), d.name(), "") for d in declaration.subtypes()] + return boundaryconditiontypes_enum + return [] + + def updateAxisAngle(self, context): if not self.axis_empty: return @@ -115,6 +124,12 @@ class StructuralLoad(PropertyGroup): number_of_inverse_references: IntProperty(name="Number of Inverse References") +class BoundaryCondition(PropertyGroup): + name: StringProperty(name="Name") + ifc_definition_id: IntProperty(name="IFC Definition ID") + number_of_inverse_references: IntProperty(name="Number of Inverse References") + + class BIMStructuralProperties(PropertyGroup): structural_analysis_model_attributes: CollectionProperty( name="Structural Analysis Model Attributes", type=Attribute @@ -147,6 +162,13 @@ class BIMStructuralProperties(PropertyGroup): structural_load_attributes: CollectionProperty(name="Structural Load Attributes", type=Attribute) filtered_structural_loads: BoolProperty(name="Filtered Structural Loads", default=False) + boundary_conditions: CollectionProperty(name="Boundary Conditions", type=BoundaryCondition) + active_boundary_condition_index: IntProperty(name="Active Boundary Condition Index") + active_boundary_condition_id: IntProperty(name="Active Boundary Condition Id") + is_editing_boundary_conditions: BoolProperty(name="Is Editing Boundary Conditions", default=False) + boundary_condition_types: EnumProperty(items=getBoundaryConditionTypes, name="Boundary Condition Types") + boundary_condition_attributes: CollectionProperty(name="Boundary Condition Attributes", type=Attribute) + filtered_boundary_conditions: BoolProperty(name="Filtered Boundary Conditions", default=False) class BIMObjectStructuralProperties(PropertyGroup): diff --git a/src/blenderbim/blenderbim/bim/module/structural/ui.py b/src/blenderbim/blenderbim/bim/module/structural/ui.py index 2ad4b0fbea..295cd0f1c5 100644 --- a/src/blenderbim/blenderbim/bim/module/structural/ui.py +++ b/src/blenderbim/blenderbim/bim/module/structural/ui.py @@ -6,7 +6,11 @@ from ifcopenshell.api.structural.data import Data def draw_boundary_condition_ui(layout, boundary_condition_id, connection_id, props): - data = Data.boundary_conditions[boundary_condition_id] if boundary_condition_id else {} + data = ( + Data.boundary_conditions[boundary_condition_id] + if boundary_condition_id and boundary_condition_id in Data.boundary_conditions.keys() + else {} + ) row = layout.row(align=True) if not data: row.label(text="No Boundary Condition Found", icon="CON_TRACKTO") @@ -486,7 +490,7 @@ class BIM_UL_structural_loads(UIList): def draw_item(self, context, layout, data, item, icon, active_data, active_propname): if item: row = layout.row(align=True) - row.label(text=item.name) + row.label(text=f"{item.name} ({item.number_of_inverse_references})") row.label(text=Data.structural_loads[item.ifc_definition_id]["type"]) if context.scene.BIMStructuralProperties.active_structural_load_id == item.ifc_definition_id: @@ -500,3 +504,73 @@ class BIM_UL_structural_loads(UIList): op.structural_load = item.ifc_definition_id op = row.operator("bim.remove_structural_load", text="", icon="X") op.structural_load = item.ifc_definition_id + + +class BIM_PT_boundary_conditions(Panel): + bl_label = "IFC Boundary Conditions" + bl_idname = "BIM_PT_boundary_conditions" + bl_options = {"DEFAULT_CLOSED"} + bl_space_type = "PROPERTIES" + bl_region_type = "WINDOW" + bl_context = "scene" + + @classmethod + def poll(cls, context): + return IfcStore.get_file() + + def draw(self, context): + if not Data.is_loaded: + Data.load(IfcStore.get_file()) + self.props = context.scene.BIMStructuralProperties + + row = self.layout.row(align=True) + row.label(text="{} Boundary Conditions Found".format(len(Data.boundary_conditions)), icon="CON_TRACKTO") + if self.props.is_editing_boundary_conditions: + row.operator( + "bim.toggle_filter_boundary_conditions", + text="FILTER - OFF" if not self.props.filtered_boundary_conditions else "FILTER - ON", + icon="FILTER", + ) + row.operator("bim.disable_boundary_condition_editing_ui", text="", icon="SCREEN_BACK") + + row = self.layout.row(align=True) + row.prop(self.props, "boundary_condition_types", text="") + row.operator( + "bim.add_boundary_condition", text="", icon="ADD" + ).ifc_class = self.props.boundary_condition_types + else: + row.operator("bim.load_boundary_conditions", text="", icon="GREASEPENCIL") + + if self.props.is_editing_boundary_conditions: + self.layout.template_list( + "BIM_UL_boundary_conditions", + "", + self.props, + "boundary_conditions", + self.props, + "active_boundary_condition_index", + ) + + if self.props.active_boundary_condition_id: + draw_boundary_condition_editable_ui(self.layout, self.props) + # blenderbim.bim.helper.draw_attributes(self.props.boundary_condition_attributes, self.layout) + + +class BIM_UL_boundary_conditions(UIList): + def draw_item(self, context, layout, data, item, icon, active_data, active_propname): + if item: + row = layout.row(align=True) + row.label(text=f"{item.name} ({item.number_of_inverse_references})") + row.label(text=Data.boundary_conditions[item.ifc_definition_id]["type"]) + + if context.scene.BIMStructuralProperties.active_boundary_condition_id == item.ifc_definition_id: + row.operator("bim.edit_boundary_condition", text="", icon="CHECKMARK") + row.operator("bim.disable_editing_boundary_condition", text="", icon="CANCEL") + elif context.scene.BIMStructuralProperties.active_boundary_condition_id: + op = row.operator("bim.remove_boundary_condition", text="", icon="X") + op.boundary_condition = item.ifc_definition_id + else: + op = row.operator("bim.enable_editing_boundary_condition", text="", icon="GREASEPENCIL") + op.boundary_condition = item.ifc_definition_id + op = row.operator("bim.remove_boundary_condition", text="", icon="X") + op.boundary_condition = item.ifc_definition_id diff --git a/src/ifcopenshell-python/ifcopenshell/api/structural/add_structural_boundary_condition.py b/src/ifcopenshell-python/ifcopenshell/api/structural/add_structural_boundary_condition.py index 9cb49ec47a..184221dce5 100644 --- a/src/ifcopenshell-python/ifcopenshell/api/structural/add_structural_boundary_condition.py +++ b/src/ifcopenshell-python/ifcopenshell/api/structural/add_structural_boundary_condition.py @@ -1,21 +1,28 @@ class Usecase: def __init__(self, file, **settings): self.file = file - self.settings = {"connection": None} + self.settings = {"name": None, "connection": None, "ifc_class": "IfcBoundaryNodeCondition"} for key, value in settings.items(): self.settings[key] = value def execute(self): - if self.settings["connection"].is_a("IfcRelConnectsStructuralMember"): - related_connection = self.settings["connection"].RelatedStructuralConnection + if self.settings["connection"]: + # assign boundary condition to a connection + if self.settings["connection"].is_a("IfcRelConnectsStructuralMember"): + related_connection = self.settings["connection"].RelatedStructuralConnection + else: + related_connection = self.settings["connection"] + + if related_connection.is_a("IfcStructuralPointConnection"): + boundary_class = "IfcBoundaryNodeCondition" + elif related_connection.is_a("IfcStructuralCurveConnection"): + boundary_class = "IfcBoundaryEdgeCondition" + elif related_connection.is_a("IfcStructuralSurfaceConnection"): + boundary_class = "IfcBoundaryFaceCondition" + + self.settings["connection"].AppliedCondition = self.file.create_entity( + boundary_class, Name=self.settings["name"] + ) else: - related_connection = self.settings["connection"] - - if related_connection.is_a("IfcStructuralPointConnection"): - boundary_class = "IfcBoundaryNodeCondition" - elif related_connection.is_a("IfcStructuralCurveConnection"): - boundary_class = "IfcBoundaryEdgeCondition" - elif related_connection.is_a("IfcStructuralSurfaceConnection"): - boundary_class = "IfcBoundaryFaceCondition" - - self.settings["connection"].AppliedCondition = self.file.create_entity(boundary_class) + # add an orphan boundary condition + return self.file.create_entity(self.settings["ifc_class"], Name=self.settings["name"]) diff --git a/src/ifcopenshell-python/ifcopenshell/api/structural/data.py b/src/ifcopenshell-python/ifcopenshell/api/structural/data.py index 0104932c10..290f2b956c 100644 --- a/src/ifcopenshell-python/ifcopenshell/api/structural/data.py +++ b/src/ifcopenshell-python/ifcopenshell/api/structural/data.py @@ -50,6 +50,7 @@ class Data: cls.load_structural_load_groups() cls.load_structural_activities() cls.load_structural_loads() + cls.load_boundary_conditions() cls.is_loaded = True @classmethod @@ -143,7 +144,6 @@ class Data: @classmethod def load_structural_connection(cls, product_id): - cls.boundary_conditions = {} cls.connects_structural_members = {} connection = cls._file.by_id(product_id) @@ -205,6 +205,12 @@ class Data: def load_structural_load(cls, load): cls.structural_loads[load.id()] = load.get_info() + @classmethod + def load_boundary_conditions(cls): + cls.boundary_conditions = {} + for bc in cls._file.by_type("IfcBoundaryCondition"): + cls.load_boundary_condition(bc) + @classmethod def load_connects_structural_activity(cls, rel): rel_data = rel.get_info() diff --git a/src/ifcopenshell-python/ifcopenshell/api/structural/remove_structural_boundary_condition.py b/src/ifcopenshell-python/ifcopenshell/api/structural/remove_structural_boundary_condition.py index 48ca3354df..69a8244ea1 100644 --- a/src/ifcopenshell-python/ifcopenshell/api/structural/remove_structural_boundary_condition.py +++ b/src/ifcopenshell-python/ifcopenshell/api/structural/remove_structural_boundary_condition.py @@ -1,13 +1,20 @@ class Usecase: def __init__(self, file, **settings): self.file = file - self.settings = {"connection": None} + self.settings = {"connection": None, "boundary_condition": None} for key, value in settings.items(): self.settings[key] = value def execute(self): - if not self.settings["connection"].AppliedCondition: - return - if len(self.file.get_inverse(self.settings["connection"].AppliedCondition)) == 1: - self.file.remove(self.settings["connection"].AppliedCondition) - self.settings["connection"].AppliedCondition = None + if self.settings["connection"]: + # remove boundary condition from a connection + if not self.settings["connection"].AppliedCondition: + return + if len(self.file.get_inverse(self.settings["connection"].AppliedCondition)) == 1: + self.file.remove(self.settings["connection"].AppliedCondition) + self.settings["connection"].AppliedCondition = None + else: + # remove the boundary condition + for conn in self.file.get_inverse(self.settings["boundary_condition"]): + conn.AppliedCondition = None + self.file.remove(self.settings["boundary_condition"])