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,
slab.DisableEditingExtrusionProfile,
slab.DisableEditingSketchExtrusionProfile,
slab.AddSlabFromWall,
slab.DrawPolylineSlab,
slab.EditExtrusionProfile,
slab.EditSketchExtrusionProfile,
+53 -6
View File
@@ -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:
@@ -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":
+65
View File
@@ -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