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)