diff --git a/src/blenderbim/blenderbim/bim/module/model/__init__.py b/src/blenderbim/blenderbim/bim/module/model/__init__.py index 228e3cae03..d7a37ed37e 100644 --- a/src/blenderbim/blenderbim/bim/module/model/__init__.py +++ b/src/blenderbim/blenderbim/bim/module/model/__init__.py @@ -38,6 +38,7 @@ from . import ( door, railing, roof, + mep, ) classes = ( @@ -172,6 +173,7 @@ classes = ( roof.EnableEditingRoofPath, roof.RemoveRoof, roof.SetGableRoofEdgeAngle, + mep.MEPAddObstruction, ) addon_keymaps = [] diff --git a/src/blenderbim/blenderbim/bim/module/model/mep.py b/src/blenderbim/blenderbim/bim/module/model/mep.py index 724a0c487b..915ffb6fba 100644 --- a/src/blenderbim/blenderbim/bim/module/model/mep.py +++ b/src/blenderbim/blenderbim/bim/module/model/mep.py @@ -32,9 +32,14 @@ import blenderbim.core.geometry import blenderbim.tool as tool from math import pi, degrees from mathutils import Vector, Matrix +import re +from blenderbim.bim.module.model.profile import DumbProfileJoiner + +V = lambda *x: Vector([float(i) for i in x]) +float_is_zero = lambda f: 0.0001 >= f >= -0.0001 -class MepGenerator: +class MEPGenerator: def __init__(self, relating_type=None): self.relating_type = relating_type @@ -52,14 +57,186 @@ class MepGenerator: ports = tool.System.get_ports(element) if not ports: start_port_matrix = Matrix() + # TODO: what are the best default directions here? 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 - # 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 + end_port = self.get_segment_data(element)["end_port"] tool.Model.edit_element_placement(end_port, obj.matrix_world @ end_port_matrix) + + def get_segment_data(self, segment): + ports = tool.System.get_ports(segment) + segment_object = tool.Ifc.get_object(segment) + start_point = segment_object.location + extrusion_depth = segment_object.dimensions.z + end_point = segment_object.matrix_world @ V(0, 0, extrusion_depth) + segment_data = { + "start_point": start_point, + "end_point": end_point, + "ports": ports, + "extrusion_depth": extrusion_depth, + } + + for port in ports: + port_local_position = V(*port.ObjectPlacement.RelativePlacement.Location.Coordinates) + if float_is_zero(port_local_position.length): + segment_data["start_port"] = port + else: + segment_data["end_port"] = port + + return segment_data + + def get_mep_element_class_name(self, element, mep_class_type): + split_camel_case = lambda x: re.findall("[A-Z][^A-Z]*", x) + class_name = "".join(split_camel_case(element.is_a())[:2] + [mep_class_type]) + return class_name + + def get_compatible_fitting_type(self, segment, predefined_type): + """We find compatible fitting only by checking if they were + already used with that segment type before. + + There lies the problem that it won't be + able to identify the fittings that were not connected to any segments yet. + """ + segment_type = ifcopenshell.util.element.get_type(segment) + if not segment_type: + return None + + fitting_types = tool.Ifc.get().by_type(self.get_mep_element_class_name(segment, "Fitting")) + for fitting_type in fitting_types: + if fitting_type.PredefinedType != predefined_type: + continue + fittings = tool.Ifc.get_all_element_occurences(fitting_type) + if not fittings: + continue + fitting = fittings[0] + elements = set( + ifcopenshell.util.system.get_connected_to(fitting) + + ifcopenshell.util.system.get_connected_from(fitting) + ) + for element in elements: + if element.IsTypedBy and element.IsTypedBy[0].RelatingType == segment_type: + return fitting_type + + def create_obstruction_type(self, segment): + # code is very similar to "bim.add_type" + profile_set = ifcopenshell.util.element.get_material(segment, should_skip_usage=True) + material_profile = profile_set.MaterialProfiles[0] + profile = material_profile.Profile + material = material_profile.Material + ifc_class = self.get_mep_element_class_name(segment, "FittingType") + ifc_file = tool.Ifc.get() + body = ifcopenshell.util.representation.get_context(ifc_file, "Model", "Body", "MODEL_VIEW") + + obj = bpy.data.objects.new("Fitting", None) + element = blenderbim.core.root.assign_class( + tool.Ifc, + tool.Collector, + tool.Root, + obj=obj, + ifc_class=ifc_class, + predefined_type="OBSTRUCTION", + should_add_representation=True, + context=body, + ifc_representation_class=None, + ) + + rel = ifcopenshell.api.run("material.assign_material", ifc_file, product=element, type="IfcMaterialProfileSet") + profile_set = rel.RelatingMaterial + material_profile = ifcopenshell.api.run( + "material.add_profile", ifc_file, profile_set=profile_set, material=material + ) + ifcopenshell.api.run("material.assign_profile", ifc_file, material_profile=material_profile, profile=profile) + return element + + def add_obstruction(self, segment, length, at_segment_start=False): + """ + `segment` is a segment ifc element + + `length` is obstruction length provided in si units + + returns `(None, error_message)` if there was some error in the process + or returns `(obstruction_element, None)` if everything went fine. + """ + + related_port_name = "start" if at_segment_start else "end" + segment_data = self.get_segment_data(segment) + related_port = segment_data[f"{related_port_name}_port"] + + # communicate error cases + if related_port.ConnectedTo or related_port.ConnectedFrom: + return None, f"Failed to add obstruction - {related_port_name} port is already connected." + if length >= segment_data["extrusion_depth"]: + return None, "Failed to add obstruction - obstruction length is larger than the segment." + + segment_obj = tool.Ifc.get_object(segment) + segment_matrix = segment_obj.matrix_world + segment_rotation = segment_matrix.to_quaternion() + obstruction_type = self.get_compatible_fitting_type(segment, "OBSTRUCTION") + if not obstruction_type: + obstruction_type = self.create_obstruction_type(segment) + + profile_joiner = DumbProfileJoiner() + # create obstruction occurence and setup it's length and port + # NOTE: at this point we loose current blender objects selection + bpy.ops.bim.add_constr_type_instance(relating_type_id=obstruction_type.id()) + obstruction_obj = bpy.context.active_object + obstruction_obj.matrix_world = segment_matrix + + profile_joiner.set_depth(obstruction_obj, length) + obstruction = tool.Ifc.get_entity(obstruction_obj) + obstruction_port = tool.Ifc.run("system.add_port", element=obstruction) + port_local_position = Matrix.Translation((0, 0, length)) if at_segment_start else Matrix() + tool.Ifc.run( + "geometry.edit_object_placement", + product=obstruction_port, + matrix=segment_matrix @ port_local_position, + is_si=True, + ) + + # change segment length + new_segment_length = segment_data["extrusion_depth"] - length + profile_joiner.set_depth(segment_obj, new_segment_length) + + if at_segment_start: + segment_obj.location += segment_rotation @ V(0, 0, length) + else: + obstruction_obj.location += segment_rotation @ V(0, 0, new_segment_length) + + # TODO: what's best default direction here? + tool.Ifc.run("system.connect_port", port1=related_port, port2=obstruction_port, direction="SOURCE") + return obstruction, None + + +class MEPAddObstruction(bpy.types.Operator, tool.Ifc.Operator): + bl_idname = "bim.mep_add_obstruction" + bl_label = "Add Obstruction" + bl_description = "Adds obstruction to the MEP segment" + bl_options = {"REGISTER", "UNDO"} + length: bpy.props.FloatProperty( + name="Obstruction Length", description="Obstruction length in SI units", default=0.1, subtype="DISTANCE" + ) + segment_id: bpy.props.IntProperty(name="Segment Element ID", default=0) + at_segment_start: bpy.props.BoolProperty( + name="At Segment Start", + description="Whether to add obstruction at the segment start or at the end", + default=False, + ) + + def _execute(self, context): + if self.segment_id: + element = tool.Ifc.get().by_id(self.segment_id) + else: + element = tool.Ifc.get_entity(context.active_object) + if not element: + return {"CANCELLED"} + + obstruction, error_msg = MEPGenerator().add_obstruction(element, self.length, self.at_segment_start) + if error_msg: + self.report({"ERROR"}, error_msg) + return {"CANCELLED"} + + return {"FINISHED"} diff --git a/src/blenderbim/blenderbim/bim/module/model/product.py b/src/blenderbim/blenderbim/bim/module/model/product.py index f9c8d28a7d..ca2da17022 100644 --- a/src/blenderbim/blenderbim/bim/module/model/product.py +++ b/src/blenderbim/blenderbim/bim/module/model/product.py @@ -139,9 +139,10 @@ class AddConstrTypeInstance(bpy.types.Operator): if material and material.is_a("IfcMaterialProfileSet"): if obj := profile.DumbProfileGenerator(relating_type).generate(): + tool.Blender.select_and_activate_single_object(context, obj) if relating_type.is_a("IfcFlowSegmentType"): self.set_flow_segment_rl(obj) - mep.MepGenerator(relating_type).setup_ports(obj) + 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): diff --git a/src/blenderbim/blenderbim/bim/module/model/profile.py b/src/blenderbim/blenderbim/bim/module/model/profile.py index e8db755fc4..ed7a601b4a 100644 --- a/src/blenderbim/blenderbim/bim/module/model/profile.py +++ b/src/blenderbim/blenderbim/bim/module/model/profile.py @@ -34,7 +34,6 @@ 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: @@ -493,7 +492,10 @@ class DumbProfileJoiner: ) tool.Geometry.record_object_materials(obj) if element.is_a("IfcFlowSegment"): - MepGenerator().setup_ports(obj) + # lazy import to avoid circular import errors + from blenderbim.bim.module.model.mep import MEPGenerator + + MEPGenerator().setup_ports(obj) def join(self, profile1, profile2, connection1, connection2, is_relating=True, description="BUTT"): element1 = tool.Ifc.get_entity(profile1) @@ -819,7 +821,7 @@ class ChangeProfileDepth(bpy.types.Operator, tool.Ifc.Operator): bl_idname = "bim.change_profile_depth" bl_label = "Change Profile Length" bl_options = {"REGISTER", "UNDO"} - depth: bpy.props.FloatProperty() + depth: bpy.props.FloatProperty(subtype="DISTANCE") @classmethod def poll(cls, context): diff --git a/src/blenderbim/blenderbim/bim/module/type/operator.py b/src/blenderbim/blenderbim/bim/module/type/operator.py index 19f02c1ff6..151eaf0dd0 100644 --- a/src/blenderbim/blenderbim/bim/module/type/operator.py +++ b/src/blenderbim/blenderbim/bim/module/type/operator.py @@ -301,8 +301,8 @@ class AddType(bpy.types.Operator, tool.Ifc.Operator): material = materials[0] # Arbitrarily pick a material else: material = self.add_default_material() - named_profiles = [p for p in ifc_file.by_type("IfcProfileDef") if p.ProfileName] if template == "PROFILESET": + named_profiles = [p for p in ifc_file.by_type("IfcProfileDef") if p.ProfileName] if named_profiles: profile = named_profiles[0] else: