From fc5f4f77766ee89fa0d40afb96614cf972172e03 Mon Sep 17 00:00:00 2001 From: Andrej730 Date: Thu, 20 Jul 2023 17:30:28 +0500 Subject: [PATCH] Added some MEP related type templates 3 more new types for distribution segments. Distribution segments work the same way as beams - they come without representation but with IfcMaterialProfileSet defined with some profile. Occurences will also have ports at the start and the end of the segment (ports can be displayed in "Ports" section in "Services and Systems"). If you extend the segment or move it around - ports will move accordingly. Small demo - https://imgur.com/a/Aq0utXa --- .../blenderbim/bim/module/model/mep.py | 102 ++++-------------- .../blenderbim/bim/module/model/product.py | 9 +- .../blenderbim/bim/module/model/profile.py | 24 ++++- .../blenderbim/bim/module/model/prop.py | 4 +- .../blenderbim/bim/module/type/operator.py | 46 ++++++-- src/blenderbim/blenderbim/tool/model.py | 10 ++ 6 files changed, 96 insertions(+), 99 deletions(-) diff --git a/src/blenderbim/blenderbim/bim/module/model/mep.py b/src/blenderbim/blenderbim/bim/module/model/mep.py index a8381f6e43..724a0c487b 100644 --- a/src/blenderbim/blenderbim/bim/module/model/mep.py +++ b/src/blenderbim/blenderbim/bim/module/model/mep.py @@ -35,93 +35,31 @@ from mathutils import Vector, Matrix class MepGenerator: - def __init__(self, relating_type): + def __init__(self, relating_type=None): self.relating_type = relating_type - def generate(self): + def setup_ports(self, obj): self.file = tool.Ifc.get() self.collection = bpy.context.view_layer.active_layer_collection.collection - if self.relating_type.is_a("IfcCableCarrierSegmentType"): - pass - elif self.relating_type.is_a("IfcCableSegmentType"): - pass - elif self.relating_type.is_a("IfcDuctSegmentType"): - dimensions = ifcopenshell.util.element.get_psets(self.relating_type).get("Pset_DuctSegmentTypeCommon") or {} - shape = dimensions.get("Shape") or dimensions.get("CrossSectionShape") - - if shape == "RECTANGULAR": - self.width = dimensions.get("NominalDiameterOrWidth") - self.height = dimensions.get("NominalHeight") - self.length = 1 - - return self.derive_from_cursor() - elif self.relating_type.is_a("IfcPipeSegmentType"): - pass - - def derive_from_cursor(self): - self.location = bpy.context.scene.cursor.location - return self.create_rectangle_segment() - - def create_rectangle_segment(self): - verts = [ - Vector((-self.width / 2, self.height / 2, 0)), - Vector((-self.width / 2, -self.height / 2, 0)), - Vector((-self.width / 2, self.height / 2, self.length)), - Vector((-self.width / 2, -self.height / 2, self.length)), - Vector((self.width / 2, self.height / 2, 0)), - Vector((self.width / 2, -self.height / 2, 0)), - Vector((self.width / 2, self.height / 2, self.length)), - Vector((self.width / 2, -self.height / 2, self.length)), - ] - faces = [ - [1, 3, 2, 0], - [4, 6, 7, 5], - [1, 0, 4, 5], - [3, 7, 6, 2], - [0, 2, 6, 4], - [1, 5, 7, 3], - ] - mesh = bpy.data.meshes.new(name="Segment") - mesh.from_pydata(verts, [], faces) - - ifc_classes = ifcopenshell.util.type.get_applicable_entities(self.relating_type.is_a(), self.file.schema) - ifc_class = ifc_classes[0] - - obj = bpy.data.objects.new(tool.Model.generate_occurrence_name(self.relating_type, ifc_class), mesh) - - obj.location = self.location - obj.rotation_euler[0] = math.pi / 2 - obj.rotation_euler[2] = math.pi / 2 - self.collection.objects.link(obj) - - bpy.ops.bim.assign_class( - obj=obj.name, - ifc_class=ifc_class, - ifc_representation_class="IfcExtrudedAreaSolid/IfcRectangleProfileDef", - ) - element = tool.Ifc.get_entity(obj) + representation = ifcopenshell.util.representation.get_representation(element, "Model", "Body", "MODEL_VIEW") + extrusion = tool.Model.get_extrusion(representation) + si_conversion = ifcopenshell.util.unit.calculate_unit_scale(tool.Ifc.get()) + length = extrusion.Depth * si_conversion + end_port_matrix = Matrix.Translation((0, 0, length)) - blenderbim.core.type.assign_type(tool.Ifc, tool.Type, element=element, type=self.relating_type) - pset = ifcopenshell.api.run("pset.add_pset", self.file, product=element, name="EPset_Parametric") - ifcopenshell.api.run("pset.edit_pset", self.file, pset=pset, properties={"Engine": "BlenderBIM.Mep"}) + ports = tool.System.get_ports(element) + if not ports: + start_port_matrix = Matrix() + for mat, flow_direction in zip([start_port_matrix, end_port_matrix], ("SINK", "SOURCE")): + port = tool.Ifc.run("system.add_port", element=element) + port.FlowDirection = flow_direction + tool.Ifc.run("geometry.edit_object_placement", product=port, matrix=obj.matrix_world @ mat, is_si=True) + return - start_port_matrix = Matrix() - end_port_matrix = Matrix.Translation((0, 0, self.length)) - - for mat in [start_port_matrix, end_port_matrix]: - port = tool.Ifc.run("root.create_entity", ifc_class="IfcDistributionPort") - tool.Ifc.run("system.assign_port", element=element, port=port) - tool.Ifc.run("geometry.edit_object_placement", product=port, matrix=obj.matrix_world @ mat, is_si=True) - - try: - obj.select_set(True) - except RuntimeError: - def msg(self, context): - txt = "The created object could not be assigned to a collection. " - txt += "Has any IfcSpatialElement been deleted?" - self.layout.label(text=txt) - - bpy.context.window_manager.popup_menu(msg, title="Error", icon="ERROR") - return obj + # TODO: better way to find the port to be moved + end_port = next((p for p in ports if p.FlowDirection == "SOURCE"), None) + if not end_port: + return + tool.Model.edit_element_placement(end_port, obj.matrix_world @ end_port_matrix) diff --git a/src/blenderbim/blenderbim/bim/module/model/product.py b/src/blenderbim/blenderbim/bim/module/model/product.py index ceff24ca7b..b89a9ac32d 100644 --- a/src/blenderbim/blenderbim/bim/module/model/product.py +++ b/src/blenderbim/blenderbim/bim/module/model/product.py @@ -105,15 +105,16 @@ class AddConstrTypeInstance(bpy.types.Operator): material = ifcopenshell.util.element.get_material(relating_type) if material and material.is_a("IfcMaterialProfileSet"): - if profile.DumbProfileGenerator(relating_type).generate(): + if obj := profile.DumbProfileGenerator(relating_type).generate(): + if not relating_type.is_a("IfcFlowSegmentType"): + return {"FINISHED"} + mep.MepGenerator(relating_type).setup_ports(obj) return {"FINISHED"} + elif material and material.is_a("IfcMaterialLayerSet"): if self.generate_layered_element(ifc_class, relating_type): tool.Blender.select_and_activate_single_object(context, context.selected_objects[-1]) return {"FINISHED"} - if relating_type.is_a("IfcFlowSegmentType") and not relating_type.RepresentationMaps: - if mep.MepGenerator(relating_type).generate(): - return {"FINISHED"} building_obj = None if len(context.selected_objects) == 1 and context.active_object: diff --git a/src/blenderbim/blenderbim/bim/module/model/profile.py b/src/blenderbim/blenderbim/bim/module/model/profile.py index 6e4b4ec16c..8e28277ae5 100644 --- a/src/blenderbim/blenderbim/bim/module/model/profile.py +++ b/src/blenderbim/blenderbim/bim/module/model/profile.py @@ -34,6 +34,7 @@ from mathutils import Vector, Matrix, Quaternion from blenderbim.bim.module.geometry.helper import Helper from blenderbim.bim.module.model.wall import DumbWallRecalculator from blenderbim.bim.module.model.decorator import ProfileDecorator +from blenderbim.bim.module.model.mep import MepGenerator class DumbProfileGenerator: @@ -74,7 +75,9 @@ class DumbProfileGenerator: obj = bpy.data.objects.new(tool.Model.generate_occurrence_name(self.relating_type, ifc_class), mesh) matrix_world = Matrix() - if self.relating_type.is_a() in ["IfcBeamType", "IfcMemberType"]: + if self.relating_type.is_a() in ["IfcBeamType", "IfcMemberType"] or self.relating_type.is_a( + "IfcFlowSegmentType" + ): matrix_world = Matrix.Rotation(pi / 2, 4, "Z") @ Matrix.Rotation(pi / 2, 4, "X") @ matrix_world matrix_world.col[3] = self.location.to_4d() if self.collection_obj and self.collection_obj.BIMObjectProperties.ifc_definition_id: @@ -230,9 +233,11 @@ class ExtendProfile(bpy.types.Operator, tool.Ifc.Operator): return {"FINISHED"} for obj in selected_objs: tool.Geometry.clear_scale(obj) + if len(selected_objs) == 1: joiner.join_E(context.active_object, context.scene.cursor.location) return {"FINISHED"} + if len(selected_objs) == 2: if self.join_type == "L": joiner.join_L([o for o in selected_objs if o != context.active_object][0], context.active_object) @@ -245,6 +250,7 @@ class ExtendProfile(bpy.types.Operator, tool.Ifc.Operator): if obj == context.active_object: continue joiner.join_T(obj, context.active_object) + return {"FINISHED"} @@ -486,6 +492,8 @@ class DumbProfileJoiner: should_sync_changes_first=False, ) tool.Geometry.record_object_materials(obj) + if element.is_a("IfcFlowSegment"): + MepGenerator().setup_ports(obj) def join(self, profile1, profile2, connection1, connection2, is_relating=True, description="BUTT"): element1 = tool.Ifc.get_entity(profile1) @@ -869,9 +877,15 @@ class Rotate90(bpy.types.Operator, tool.Ifc.Operator): elif usage == "LAYER2": layer2_objs.append(obj) if element.ConnectedTo or element.ConnectedFrom: - ifcopenshell.api.run("geometry.disconnect_path", tool.Ifc.get(), element=element, connection_type="ATSTART") - ifcopenshell.api.run("geometry.disconnect_path", tool.Ifc.get(), element=element, connection_type="ATEND") - ifcopenshell.api.run("geometry.disconnect_path", tool.Ifc.get(), element=element, connection_type="ATPATH") + ifcopenshell.api.run( + "geometry.disconnect_path", tool.Ifc.get(), element=element, connection_type="ATSTART" + ) + ifcopenshell.api.run( + "geometry.disconnect_path", tool.Ifc.get(), element=element, connection_type="ATEND" + ) + ifcopenshell.api.run( + "geometry.disconnect_path", tool.Ifc.get(), element=element, connection_type="ATPATH" + ) rotate_matrix = Matrix.Rotation(pi / 2, 4, self.axis) obj.matrix_world @= rotate_matrix bpy.context.view_layer.update() @@ -992,7 +1006,7 @@ class EditExtrusionAxis(bpy.types.Operator, tool.Ifc.Operator): y_axis = Vector((0, 0, 1)) # making sure z_axis != y_axis if z_axis == y_axis: - y_axis = Vector((0,1,0)) + y_axis = Vector((0, 1, 0)) x_axis = y_axis.cross(z_axis).normalized() y_axis = z_axis.cross(x_axis).normalized() diff --git a/src/blenderbim/blenderbim/bim/module/model/prop.py b/src/blenderbim/blenderbim/bim/module/model/prop.py index 81b57ea7e6..dcbdd45366 100644 --- a/src/blenderbim/blenderbim/bim/module/model/prop.py +++ b/src/blenderbim/blenderbim/bim/module/model/prop.py @@ -147,7 +147,9 @@ class BIMModelProperties(PropertyGroup): ("STAIR", "Stair", "Parametric stair"), ("RAILING", "Railing", "Parametric railing"), ("ROOF", "Roof", "Parametric roof"), - + ("DISTRIBUTION_SEGMENT_RECTANGULAR", "Rectangular Distribution Segment", "Works similarly to Profile, has distribution ports"), + ("DISTRIBUTION_SEGMENT_CIRCULAR", "Circular Distribution Segment", "Works similarly to Profile, has distribution ports"), + ("DISTRIBUTION_SEGMENT_CIRCULAR_HOLLOW", "Circular Hollow Distribution Segment", "Works similarly to Profile, has distribution ports"), ), name="Type Template", default="MESH", diff --git a/src/blenderbim/blenderbim/bim/module/type/operator.py b/src/blenderbim/blenderbim/bim/module/type/operator.py index cb74a7aae6..b029ffd36a 100644 --- a/src/blenderbim/blenderbim/bim/module/type/operator.py +++ b/src/blenderbim/blenderbim/bim/module/type/operator.py @@ -278,7 +278,7 @@ class AddType(bpy.types.Operator, tool.Ifc.Operator): axis = "AXIS3" ifcopenshell.api.run("pset.edit_pset", ifc_file, pset=pset, properties={"LayerSetDirection": axis}) - elif template == "PROFILESET": + elif template == "PROFILESET" or template.startswith("DISTRIBUTION_SEGMENT_"): unit_scale = ifcopenshell.util.unit.calculate_unit_scale(ifc_file) obj = bpy.data.objects.new(name, None) element = blenderbim.core.root.assign_class( @@ -298,13 +298,45 @@ class AddType(bpy.types.Operator, tool.Ifc.Operator): else: material = self.add_default_material() named_profiles = [p for p in ifc_file.by_type("IfcProfileDef") if p.ProfileName] - if named_profiles: - profile = named_profiles[0] + if template == "PROFILESET": + if named_profiles: + profile = named_profiles[0] + else: + size = 0.5 / unit_scale + profile = ifc_file.create_entity( + "IfcRectangleProfileDef", ProfileName="New Profile", ProfileType="AREA", XDim=size, YDim=size + ) else: - size = 0.5 / unit_scale - profile = ifc_file.create_entity( - "IfcRectangleProfileDef", ProfileName="New Profile", ProfileType="AREA", XDim=size, YDim=size - ) + # NOTE: defaults dims are in meters / mm + if template == "DISTRIBUTION_SEGMENT_RECTANGULAR": + default_x_dim = 0.4 / unit_scale + default_y_dim = 0.2 / unit_scale + profile_name = f"{ifc_class}-{default_x_dim*1000}x{default_x_dim*1000}" + profile = ifc_file.create_entity( + "IfcRectangleProfileDef", + ProfileName=profile_name, + ProfileType="AREA", + XDim=default_x_dim, + YDim=default_y_dim, + ) + elif template == "DISTRIBUTION_SEGMENT_CIRCULAR": + default_diameter = 0.1 / unit_scale + profile_name = f"{ifc_class}-{default_diameter*1000}" + profile = ifc_file.create_entity( + "IfcCircleProfileDef", ProfileName=profile_name, ProfileType="AREA", Radius=default_diameter / 2 + ) + elif template == "DISTRIBUTION_SEGMENT_CIRCULAR_HOLLOW": + default_diameter = 0.15 / unit_scale + default_thickness = 0.005 / unit_scale # TODO: double check + profile_name = f"{ifc_class}-{default_diameter*1000}x{default_thickness*1000}" + profile = ifc_file.create_entity( + "IfcCircleHollowProfileDef", + ProfileName=profile_name, + ProfileType="AREA", + Radius=default_diameter / 2, + WallThickness=default_thickness, + ) + rel = ifcopenshell.api.run( "material.assign_material", ifc_file, product=element, type="IfcMaterialProfileSet" ) diff --git a/src/blenderbim/blenderbim/tool/model.py b/src/blenderbim/blenderbim/tool/model.py index 2bd74e8762..08e5ef21e9 100644 --- a/src/blenderbim/blenderbim/tool/model.py +++ b/src/blenderbim/blenderbim/tool/model.py @@ -748,3 +748,13 @@ class Model(blenderbim.core.tool.Model): return pset_data["data_dict"] = json.loads(pset_data.get("Data", "[]") or "[]") return pset_data + + @classmethod + def edit_element_placement(cls, element, matrix): + """Useful for moving objects like ports or openings - + the method will ensure it will be moved in blender scene too if it exists""" + obj = tool.Ifc.get_object(element) + if obj: + obj.matrix_world = matrix + return + tool.Ifc.run("geometry.edit_object_placement", product=element, matrix=matrix, is_si=True)