diff --git a/src/bonsai/bonsai/bim/module/model/__init__.py b/src/bonsai/bonsai/bim/module/model/__init__.py index deec325c9e..0756930afa 100644 --- a/src/bonsai/bonsai/bim/module/model/__init__.py +++ b/src/bonsai/bonsai/bim/module/model/__init__.py @@ -100,6 +100,7 @@ classes = ( roof.GenerateHippedRoof, slab.DisableEditingExtrusionProfile, slab.DisableEditingSketchExtrusionProfile, + slab.AddSlabFromWall, slab.DrawPolylineSlab, slab.EditExtrusionProfile, slab.EditSketchExtrusionProfile, diff --git a/src/bonsai/bonsai/bim/module/model/slab.py b/src/bonsai/bonsai/bim/module/model/slab.py index a8d8ef7efb..0c61d89cb5 100644 --- a/src/bonsai/bonsai/bim/module/model/slab.py +++ b/src/bonsai/bonsai/bim/module/model/slab.py @@ -42,9 +42,9 @@ class DumbSlabGenerator: def __init__(self, relating_type: ifcopenshell.entity_instance): self.relating_type = relating_type - def generate(self, draw_from_polyline=False): + def generate(self, insertion_type="CURSOR"): self.file = tool.Ifc.get() - unit_scale = ifcopenshell.util.unit.calculate_unit_scale(tool.Ifc.get()) + self.unit_scale = ifcopenshell.util.unit.calculate_unit_scale(tool.Ifc.get()) thicknesses = [] for rel in self.relating_type.HasAssociations: if rel.is_a("IfcRelAssociatesMaterial"): @@ -69,16 +69,18 @@ class DumbSlabGenerator: self.container = container self.container_obj = tool.Ifc.get_object(container) - self.depth = sum(thicknesses) * unit_scale + self.depth = sum(thicknesses) * self.unit_scale self.width = 3 self.length = 3 self.rotation = 0 self.location = Vector((0, 0, 0)) self.x_angle = 0 if tool.Cad.is_x(props.x_angle, 0, tolerance=0.001) else props.x_angle - if draw_from_polyline: + if insertion_type == "POLYLINE": return self.derive_from_polyline() - else: + elif insertion_type == "WALLS": + return self.derive_from_walls() + elif insertion_type == "CURSOR": return self.derive_from_cursor() def derive_from_polyline(self): @@ -100,6 +102,24 @@ class DumbSlabGenerator: self.location = bpy.context.scene.cursor.location return self.create_slab() + def derive_from_walls(self): + walls, is_closed_loop = tool.Model.get_connected_walls(bpy.context.selected_objects) + polyline_points = [] + poly = tool.Model.get_polygons_from_wall_axis(walls) + polyline_points = [tuple([v for v in c]) for c in poly.exterior.coords] + + self.location = Vector((polyline_points[0][0], polyline_points[0][1], self.container_obj.location.z)) + self.polyline = [tuple(Vector((p[0], p[1], 0.0)) - self.location) for p in polyline_points] + + if len(self.polyline) <= 2: + return + + # Always assume a closed polyline + if self.polyline[0] != self.polyline[-1]: + self.polyline.append(self.polyline[0]) + + return self.create_slab() + def create_slab(self): ifc_classes = ifcopenshell.util.type.get_applicable_entities(self.relating_type.is_a(), self.file.schema) # Standard cases are deprecated, so let's cull them @@ -729,6 +749,33 @@ class SetArcIndex(bpy.types.Operator): return {"FINISHED"} +class AddSlabFromWall(bpy.types.Operator, tool.Ifc.Operator): + bl_idname = "bim.draw_slab_from_wall" + bl_label = "Draw Slab From Wall" + bl_options = {"REGISTER", "UNDO"} + + @classmethod + def poll(cls, context): + return context.space_data.type == "VIEW_3D" + + def __init__(self): + self.relating_type = None + props = bpy.context.scene.BIMModelProperties + relating_type_id = props.relating_type_id + if relating_type_id: + self.relating_type = tool.Ifc.get().by_id(int(relating_type_id)) + + def _execute(self, context): + if not self.relating_type: + return {"FINISHED"} + walls, is_closed_loop = tool.Model.get_connected_walls(bpy.context.selected_objects) + if not is_closed_loop: + self.report({"WARNING"}, "Please select a closed loop of walls, or deselect the walls to add a slab using the polyline tool.") + return {"FINISHED"} + + DumbSlabGenerator(self.relating_type).generate("WALLS") + return {"FINISHED"} + class DrawPolylineSlab(bpy.types.Operator, PolylineOperator): bl_idname = "bim.draw_polyline_slab" bl_label = "Draw Polyline Slab" @@ -750,7 +797,7 @@ class DrawPolylineSlab(bpy.types.Operator, PolylineOperator): if not self.relating_type: return {"FINISHED"} - DumbSlabGenerator(self.relating_type).generate(True) + DumbSlabGenerator(self.relating_type).generate("POLYLINE") def modal(self, context, event): if not self.relating_type: diff --git a/src/bonsai/bonsai/bim/module/model/workspace.py b/src/bonsai/bonsai/bim/module/model/workspace.py index a7366bf0c3..ec787be62d 100644 --- a/src/bonsai/bonsai/bim/module/model/workspace.py +++ b/src/bonsai/bonsai/bim/module/model/workspace.py @@ -963,6 +963,17 @@ class Hotkey(bpy.types.Operator, tool.Ifc.Operator): if bpy.context.scene.BIMGeometryProperties.mode == "ITEM": bpy.ops.wm.call_menu(name="BIM_MT_add_representation_item") else: + walls = False + for obj in bpy.context.selected_objects: + walls = tool.Ifc.get_entity(obj).is_a("IfcWall") + if ( + walls + and relating_type_id + and tool.Model.get_usage_type(tool.Ifc.get().by_id(int(relating_type_id))) == "LAYER3" + ): + bpy.ops.bim.draw_slab_from_wall("INVOKE_DEFAULT") + return {"FINISHED"} + for obj in tool.Blender.get_selected_objects(): obj.select_set(False) if relating_type_id and tool.Model.get_usage_type(tool.Ifc.get().by_id(int(relating_type_id))) == "LAYER2": diff --git a/src/bonsai/bonsai/tool/model.py b/src/bonsai/bonsai/tool/model.py index ab05aa8d79..6a79b8bf23 100644 --- a/src/bonsai/bonsai/tool/model.py +++ b/src/bonsai/bonsai/tool/model.py @@ -672,6 +672,71 @@ class Model(bonsai.core.tool.Model): ] return axes + @classmethod + def get_connected_walls( + cls, walls: list[bpy.types.Object], opposite_direction: bool = False + ) -> list[bpy.types.Object]: + """ + Loop through walls by retrieving the next connected wall using the ConnectedTo attribute. + If the function encounters the first wall again, it will return the list of connected walls and indicate that a closed loop has been formed. + If it reaches the end of the connections, the function will call itself in the opposite direction using the ConnectedFrom method + to obtain a list of connected walls in an open loop. + """ + + is_closed_loop = False + wall1 = tool.Ifc.get_entity(walls[0]) + wall = wall1 + if not opposite_direction: + connection = "ConnectedTo" + relation = "RelatedElement" + else: + connection = "ConnectedFrom" + relation = "RelatingElement" + + ordered_walls = [] + ordered_walls.append(wall1) + for i in range(len(walls)): + if not getattr(wall, connection) or ( + next_wall := tool.Ifc.get_object(getattr(getattr(wall, connection)[0], relation)) not in walls + ): + if opposite_direction: + return ordered_walls, is_closed_loop + ordered_walls, is_closed_loop = cls.get_connected_walls(walls, True) + break + next_wall = getattr(getattr(wall, connection)[0], relation) + if next_wall == wall1: + is_closed_loop = True + break + else: + ordered_walls.append(next_wall) + wall = next_wall + + return [tool.Ifc.get_object(wall) for wall in ordered_walls], is_closed_loop + + @classmethod + def get_polygons_from_wall_axis(cls, walls: list[bpy.types.Object]) -> list[shapely.Polygon]: + """ + Get the polygons formed by the intersection of the wall axis reference and side. + The polygon with the larger area will be considered the external polygon. + This function only works with closed loops. + """ + points1 = [] + points2 = [] + for w1, w2 in zip(walls, walls[1:] + [walls[0]]): + layers1 = tool.Model.get_material_layer_parameters(tool.Ifc.get_entity(w1)) + layers2 = tool.Model.get_material_layer_parameters(tool.Ifc.get_entity(w2)) + axis1 = tool.Model.get_wall_axis(w1, layers1) + axis2 = tool.Model.get_wall_axis(w2, layers2) + intersection1 = tool.Cad.intersect_edges(axis1["reference"], axis2["reference"]) + intersection2 = tool.Cad.intersect_edges(axis1["side"], axis2["side"]) + points1.append(intersection1[0]) + points2.append(intersection2[0]) + + poly1 = shapely.Polygon(points1) + poly2 = shapely.Polygon(points2) + + return poly1 if poly1.area > poly2.area else poly2 + @classmethod def handle_array_on_copied_element( cls, element: ifcopenshell.entity_instance, array_data: Optional[dict[str, Any]] = None