Add AddSlabFromWalls operator.

This operator allows users to create a slab from a selected closed loop of walls.
When the slab tool is active, pressing `Shift + A` will generate a slab based on the exterior polygon of the selected walls.
This commit is contained in:
Bruno Perdigão
2024-12-20 17:38:01 -03:00
parent 44ef07c43e
commit 40f1055ac3
4 changed files with 130 additions and 6 deletions
@@ -100,6 +100,7 @@ classes = (
roof.GenerateHippedRoof, roof.GenerateHippedRoof,
slab.DisableEditingExtrusionProfile, slab.DisableEditingExtrusionProfile,
slab.DisableEditingSketchExtrusionProfile, slab.DisableEditingSketchExtrusionProfile,
slab.AddSlabFromWall,
slab.DrawPolylineSlab, slab.DrawPolylineSlab,
slab.EditExtrusionProfile, slab.EditExtrusionProfile,
slab.EditSketchExtrusionProfile, slab.EditSketchExtrusionProfile,
+53 -6
View File
@@ -42,9 +42,9 @@ class DumbSlabGenerator:
def __init__(self, relating_type: ifcopenshell.entity_instance): def __init__(self, relating_type: ifcopenshell.entity_instance):
self.relating_type = relating_type self.relating_type = relating_type
def generate(self, draw_from_polyline=False): def generate(self, insertion_type="CURSOR"):
self.file = tool.Ifc.get() 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 = [] thicknesses = []
for rel in self.relating_type.HasAssociations: for rel in self.relating_type.HasAssociations:
if rel.is_a("IfcRelAssociatesMaterial"): if rel.is_a("IfcRelAssociatesMaterial"):
@@ -69,16 +69,18 @@ class DumbSlabGenerator:
self.container = container self.container = container
self.container_obj = tool.Ifc.get_object(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.width = 3
self.length = 3 self.length = 3
self.rotation = 0 self.rotation = 0
self.location = Vector((0, 0, 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 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() return self.derive_from_polyline()
else: elif insertion_type == "WALLS":
return self.derive_from_walls()
elif insertion_type == "CURSOR":
return self.derive_from_cursor() return self.derive_from_cursor()
def derive_from_polyline(self): def derive_from_polyline(self):
@@ -100,6 +102,24 @@ class DumbSlabGenerator:
self.location = bpy.context.scene.cursor.location self.location = bpy.context.scene.cursor.location
return self.create_slab() 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): def create_slab(self):
ifc_classes = ifcopenshell.util.type.get_applicable_entities(self.relating_type.is_a(), self.file.schema) 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 # Standard cases are deprecated, so let's cull them
@@ -729,6 +749,33 @@ class SetArcIndex(bpy.types.Operator):
return {"FINISHED"} 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): class DrawPolylineSlab(bpy.types.Operator, PolylineOperator):
bl_idname = "bim.draw_polyline_slab" bl_idname = "bim.draw_polyline_slab"
bl_label = "Draw Polyline Slab" bl_label = "Draw Polyline Slab"
@@ -750,7 +797,7 @@ class DrawPolylineSlab(bpy.types.Operator, PolylineOperator):
if not self.relating_type: if not self.relating_type:
return {"FINISHED"} return {"FINISHED"}
DumbSlabGenerator(self.relating_type).generate(True) DumbSlabGenerator(self.relating_type).generate("POLYLINE")
def modal(self, context, event): def modal(self, context, event):
if not self.relating_type: if not self.relating_type:
@@ -963,6 +963,17 @@ class Hotkey(bpy.types.Operator, tool.Ifc.Operator):
if bpy.context.scene.BIMGeometryProperties.mode == "ITEM": if bpy.context.scene.BIMGeometryProperties.mode == "ITEM":
bpy.ops.wm.call_menu(name="BIM_MT_add_representation_item") bpy.ops.wm.call_menu(name="BIM_MT_add_representation_item")
else: 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(): for obj in tool.Blender.get_selected_objects():
obj.select_set(False) obj.select_set(False)
if relating_type_id and tool.Model.get_usage_type(tool.Ifc.get().by_id(int(relating_type_id))) == "LAYER2": if relating_type_id and tool.Model.get_usage_type(tool.Ifc.get().by_id(int(relating_type_id))) == "LAYER2":
+65
View File
@@ -672,6 +672,71 @@ class Model(bonsai.core.tool.Model):
] ]
return axes 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 @classmethod
def handle_array_on_copied_element( def handle_array_on_copied_element(
cls, element: ifcopenshell.entity_instance, array_data: Optional[dict[str, Any]] = None cls, element: ifcopenshell.entity_instance, array_data: Optional[dict[str, Any]] = None