diff --git a/src/blenderbim/blenderbim/bim/module/model/__init__.py b/src/blenderbim/blenderbim/bim/module/model/__init__.py index d7a37ed37e..246b7c3c31 100644 --- a/src/blenderbim/blenderbim/bim/module/model/__init__.py +++ b/src/blenderbim/blenderbim/bim/module/model/__init__.py @@ -31,6 +31,7 @@ from . import ( stair, window, opening, + mep, pie, workspace, profile, @@ -105,6 +106,7 @@ classes = ( space.GenerateSpace, space.GenerateSpacesFromWalls, space.ToggleSpaceVisibility, + mep.FitFlowSegments, prop.BIMModelProperties, prop.BIMArrayProperties, prop.BIMStairProperties, diff --git a/src/blenderbim/blenderbim/bim/module/model/mep.py b/src/blenderbim/blenderbim/bim/module/model/mep.py index ff480b4f19..1a0119afb5 100644 --- a/src/blenderbim/blenderbim/bim/module/model/mep.py +++ b/src/blenderbim/blenderbim/bim/module/model/mep.py @@ -39,7 +39,92 @@ V = lambda *x: Vector([float(i) for i in x]) float_is_zero = lambda f: 0.0001 >= f >= -0.0001 -class MEPGenerator: +class FitFlowSegments(bpy.types.Operator, tool.Ifc.Operator): + bl_idname = "bim.fit_flow_segments" + bl_label = "Fit Flow Segments" + bl_options = {"REGISTER", "UNDO"} + + def _execute(self, context): + selected_objs = [] + selected_profiles = [] + + selected_class = None + for obj in context.selected_objects: + element = tool.Ifc.get_entity(obj) + if element and element.is_a("IfcFlowSegment"): + if selected_class and not element.is_a(selected_class): + return # The user is mixing up ducts and pipes. + profile = tool.Model.get_flow_segment_profile(element) + if profile: + selected_profiles.append(profile) + selected_objs.append(obj) + selected_class = element.is_a() + + total_selected_objs = len(selected_objs) + total_profiles = len(set(selected_profiles)) + fitting_type = None + + if total_selected_objs == 1: + fitting_type = "OBSTRUCTION" + elif total_selected_objs == 2: + # Shorten the axis by the profile size to allow for fuzzy intersections + # e.g. if two ducts touch, we want a bend, not a cross. + + axis1 = tool.Model.get_flow_segment_axis(selected_objs[0]) + profile_size = max(selected_objs[0].dimensions.x, selected_objs[0].dimensions.y) + offset = (axis1[1] - axis1[0]).normalized() * profile_size + axis1 = (axis1[0] + offset, axis1[1] - offset) + + axis2 = tool.Model.get_flow_segment_axis(selected_objs[1]) + profile_size = max(selected_objs[1].dimensions.x, selected_objs[1].dimensions.y) + offset = (axis2[1] - axis2[0]).normalized() * profile_size + axis2 = (axis2[0] + offset, axis2[1] - offset) + + angle = tool.Cad.angle_edges(axis1, axis2, signed=False, degrees=True) + is_parallel = tool.Cad.is_x(angle, (0, 180), tolerance=0.001) + + if total_profiles == 1: + if is_parallel: + return + intersect1, intersect2 = tool.Cad.intersect_edges(axis1, axis2) + is_on_axis1 = tool.Cad.is_point_on_edge(intersect1, axis1) + is_on_axis2 = tool.Cad.is_point_on_edge(intersect2, axis2) + if not is_on_axis1 and not is_on_axis2: + fitting_type = "BEND" + elif is_on_axis1 and is_on_axis2: + fitting_type = "CROSS" + else: + fitting_type = "TEE" + elif total_profiles == 2: + if is_parallel: + fitting_type = "TRANSITION" + elif total_selected_objs == 3: + if total_profiles > 1: + return + + axis1 = tool.Model.get_flow_segment_axis(selected_objs[0]) + axis2 = tool.Model.get_flow_segment_axis(selected_objs[1]) + axis3 = tool.Model.get_flow_segment_axis(selected_objs[2]) + + angle12 = tool.Cad.angle_edges(axis1, axis2, signed=False, degrees=True) + angle13 = tool.Cad.angle_edges(axis1, axis3, signed=False, degrees=True) + angle21 = tool.Cad.angle_edges(axis2, axis1, signed=False, degrees=True) + angle23 = tool.Cad.angle_edges(axis2, axis3, signed=False, degrees=True) + is_parallel12 = tool.Cad.is_x(angle12, (0, 180), tolerance=0.001) + is_parallel13 = tool.Cad.is_x(angle13, (0, 180), tolerance=0.001) + is_parallel21 = tool.Cad.is_x(angle21, (0, 180), tolerance=0.001) + is_parallel23 = tool.Cad.is_x(angle23, (0, 180), tolerance=0.001) + + if not all(is_parallel12, is_parallel13, is_parallel21, is_parallel23): + fitting_type = "WYE" + + if not fitting_type: + return + + print(fitting_type) + + +class MepGenerator: def __init__(self, relating_type=None): self.relating_type = relating_type diff --git a/src/blenderbim/blenderbim/bim/module/model/workspace.py b/src/blenderbim/blenderbim/bim/module/model/workspace.py index 94d6137cd2..c379419579 100644 --- a/src/blenderbim/blenderbim/bim/module/model/workspace.py +++ b/src/blenderbim/blenderbim/bim/module/model/workspace.py @@ -318,10 +318,20 @@ class BimToolUI: op.depth = cls.props.extrusion_depth add_layout_hotkey_operator(cls.layout, "Extend", "S_E", "") - add_layout_hotkey_operator(cls.layout, "Edit Axis", "A_E", "") - add_layout_hotkey_operator(cls.layout, "Butt", "S_T", "") - add_layout_hotkey_operator(cls.layout, "Mitre", "S_Y", "") - add_layout_hotkey_operator(cls.layout, "Rotate 90", "S_R", bpy.ops.bim.rotate_90.__doc__) + + if AuthoringData.data["active_class"] in ( + "IfcCableCarrierSegment", + "IfcCableSegment", + "IfcDuctSegment", + "IfcPipeSegment", + ): + add_layout_hotkey_operator(cls.layout, "Extend", "S_E", "") + add_layout_hotkey_operator(cls.layout, "Add Fitting", "S_F", "") + else: + add_layout_hotkey_operator(cls.layout, "Edit Axis", "A_E", "") + add_layout_hotkey_operator(cls.layout, "Butt", "S_T", "") + add_layout_hotkey_operator(cls.layout, "Mitre", "S_Y", "") + add_layout_hotkey_operator(cls.layout, "Rotate 90", "S_R", bpy.ops.bim.rotate_90.__doc__) add_layout_hotkey_operator(cls.layout, "Regen", "S_G", bpy.ops.bim.recalculate_profile.__doc__) row.operator("bim.extend_profile", icon="X", text="").join_type = "" @@ -358,14 +368,6 @@ class BimToolUI: elif AuthoringData.data["active_class"] in ("IfcSpace",): add_layout_hotkey_operator(cls.layout, "Regen", "S_G", bpy.ops.bim.generate_space.__doc__) - elif AuthoringData.data["active_class"] in ( - "IfcCableCarrierSegmentType", - "IfcCableSegmentType", - "IfcDuctSegmentType", - "IfcPipeSegmentType", - ): - add_layout_hotkey_operator(cls.layout, "Extend", "S_E", "") - elif ( (RoofData.is_loaded or not RoofData.load()) and RoofData.data["pset_data"] @@ -631,6 +633,8 @@ class Hotkey(bpy.types.Operator, tool.Ifc.Operator): bpy.ops.bim.flip_wall() elif self.active_class in ("IfcWindow", "IfcWindowStandardCase", "IfcDoor", "IfcDoorStandardCase"): bpy.ops.bim.flip_fill() + elif self.active_class in ("IfcDuctSegment", "IfcPipeSegment", "IfcCableCarrierSegment", "IfcCableSegment"): + bpy.ops.bim.fit_flow_segments() def hotkey_S_G(self): if not bpy.context.selected_objects: diff --git a/src/blenderbim/blenderbim/tool/model.py b/src/blenderbim/blenderbim/tool/model.py index 65774fec9e..abfadc96e5 100644 --- a/src/blenderbim/blenderbim/tool/model.py +++ b/src/blenderbim/blenderbim/tool/model.py @@ -495,6 +495,17 @@ class Model(blenderbim.core.tool.Model): items.append(item.FirstOperand) return booleans + @classmethod + def get_flow_segment_axis(cls, obj): + z_values = [v[2] for v in obj.bound_box] + return (obj.matrix_world @ Vector((0, 0, min(z_values))), obj.matrix_world @ Vector((0, 0, max(z_values)))) + + @classmethod + def get_flow_segment_profile(cls, element): + material = ifcopenshell.util.element.get_material(element, should_skip_usage=True) + if material and material.is_a("IfcMaterialProfileSet") and len(material.MaterialProfiles) == 1: + return material.MaterialProfiles[0].Profile + @classmethod def get_usage_type(cls, element): material = ifcopenshell.util.element.get_material(element, should_inherit=False)