From ee451b6b29a66cae7e41cb3d3b1a59d0a237e2f4 Mon Sep 17 00:00:00 2001 From: Dion Moult Date: Sun, 26 Feb 2023 21:23:35 +1100 Subject: [PATCH] Experimental operator to generate hipped roofs from a footprint. --- .../blenderbim/bim/module/model/__init__.py | 1 + .../blenderbim/bim/module/model/product.py | 98 ++++++++++++++++++- 2 files changed, 94 insertions(+), 5 deletions(-) diff --git a/src/blenderbim/blenderbim/bim/module/model/__init__.py b/src/blenderbim/blenderbim/bim/module/model/__init__.py index 88815ab256..335ca65145 100644 --- a/src/blenderbim/blenderbim/bim/module/model/__init__.py +++ b/src/blenderbim/blenderbim/bim/module/model/__init__.py @@ -48,6 +48,7 @@ classes = ( product.AlignProduct, product.ChangeTypePage, product.DisplayConstrTypes, + product.GenerateHippedRoof, product.GenerateSpace, product.LoadTypeThumbnails, product.ReinvokeOperator, diff --git a/src/blenderbim/blenderbim/bim/module/model/product.py b/src/blenderbim/blenderbim/bim/module/model/product.py index 3821b89fd0..680ff386b4 100644 --- a/src/blenderbim/blenderbim/bim/module/model/product.py +++ b/src/blenderbim/blenderbim/bim/module/model/product.py @@ -583,7 +583,6 @@ class GenerateSpace(bpy.types.Operator, tool.Ifc.Operator): bl_idname = "bim.generate_space" bl_label = "Generate Space" bl_options = {"REGISTER"} - page: bpy.props.IntProperty() def _execute(self, context): # Note: this experimental operator relies on Shapely v2 (not v1.8 which @@ -629,7 +628,7 @@ class GenerateSpace(bpy.types.Operator, tool.Ifc.Operator): space_polygon = None for polygon in closed_polygons.geoms: if shapely.contains_xy(polygon, x, y): - space_polygon = polygon + space_polygon = shapely.force_3d(polygon) if not space_polygon: return @@ -638,16 +637,15 @@ class GenerateSpace(bpy.types.Operator, tool.Ifc.Operator): bm.verts.index_update() bm.edges.index_update() - new_verts = [bm.verts.new(v) for v in shapely.force_3d(space_polygon).exterior.coords[0:-1]] + new_verts = [bm.verts.new(v) for v in space_polygon.exterior.coords[0:-1]] [bm.edges.new((new_verts[i], new_verts[i + 1])) for i in range(len(new_verts) - 1)] bm.edges.new((new_verts[len(new_verts) - 1], new_verts[0])) for interior in space_polygon.interiors: - new_verts = [bm.verts.new(v) for v in shapely.force_3d(interior).coords[0:-1]] + new_verts = [bm.verts.new(v) for v in interior.coords[0:-1]] [bm.edges.new((new_verts[i], new_verts[i + 1])) for i in range(len(new_verts) - 1)] bm.edges.new((new_verts[len(new_verts) - 1], new_verts[0])) - bm.verts.index_update() bm.edges.index_update() @@ -683,3 +681,93 @@ class GenerateSpace(bpy.types.Operator, tool.Ifc.Operator): (obj.matrix_world @ Vector((max_x, max_y, 0.0))).to_2d(), ], } + + +class GenerateHippedRoof(bpy.types.Operator, tool.Ifc.Operator): + bl_idname = "bim.generate_hipped_roof" + bl_label = "Generate Hipped Roof" + bl_options = {"REGISTER"} + mode: bpy.props.StringProperty(default="ANGLE") + height: bpy.props.FloatProperty(default=1) + angle: bpy.props.FloatProperty(default=10) + + def _execute(self, context): + import bmesh + import shapely + from math import tan, radians + from bpypolyskel import bpypolyskel + + obj = bpy.context.active_object + if not obj: + return + + boundary_lines = [] + + for edge in obj.data.edges: + boundary_lines.append( + shapely.LineString([obj.data.vertices[edge.vertices[0]].co, obj.data.vertices[edge.vertices[1]].co]) + ) + + unioned_boundaries = shapely.union_all(shapely.GeometryCollection(boundary_lines)) + closed_polygons = shapely.polygonize(unioned_boundaries.geoms) + + roof_polygon = None + biggest_area = 0 + for polygon in closed_polygons.geoms: + area = polygon.area + if area > biggest_area: + roof_polygon = polygon + biggest_area = area + + roof_polygon = shapely.force_3d(roof_polygon) + + if not shapely.is_ccw(roof_polygon): + roof_polygon = roof_polygon.reverse() + + # Define vertices for the base footprint of the building at height 0.0 + # counterclockwise order + verts = [Vector(v) for v in roof_polygon.exterior.coords[0:-1]] + total_exterior_verts = len(verts) + next_index = total_exterior_verts + + inner_loops = None + for interior in roof_polygon.interiors: + if inner_loops is None: + inner_loops = [] + loop = interior.coords[0:-1] + total_verts = len(loop) + verts.extend([Vector(v) for v in loop]) + inner_loops.append((next_index, total_verts)) + next_index += total_verts + + unit_vectors = None # we have no unit vectors, let them computed by polygonize() + start_exterior_index = 0 + + faces = [] + + if self.mode == "HEIGHT": + height = self.height + angle = 0.0 + else: + angle = tan(radians(round(self.angle, 4))) + height = 0.0 + + faces = bpypolyskel.polygonize( + verts, start_exterior_index, total_exterior_verts, inner_loops, height, angle, faces, unit_vectors + ) + + edges = [] + mesh = bpy.data.meshes.new(name="Building_with_Roof") + mesh.from_pydata(verts, edges, faces) + + bm = bmesh.new() + bm.from_mesh(mesh) + + extrusion = bmesh.ops.extrude_face_region(bm, geom=bm.faces) + extruded_verts = [g for g in extrusion["geom"] if isinstance(g, bmesh.types.BMVert)] + bmesh.ops.translate(bm, vec=[0.0, 0.0, 0.1], verts=extruded_verts) + + bm.to_mesh(mesh) + bm.free() + + obj.data = mesh