Gable Roof support for roof modifier

How it works:
1) You add Roof Modifier to some object that represents roof footprint.
2) Start editing path (Shift-E in BIM Tool).
3) Then you can select any edge and specify roof angles manually for each edge (Shift-R in BIM Tool)
to make side gabled you just need to set angle to 90 degrees. Preview will update automatically.  You can set it back to 0 for angle to be defined automatically.

Some other changes:
- refactored decorator.py, in decorator now it's possible to supply some custom bmesh function that will be called on redraw, decorator now also can draw faces;
- changed colors for selected verts / edges to less bright, now it in more contrast to selected verts / edges color and seems much more readable;
This commit is contained in:
Andrej730
2023-03-28 12:20:13 +05:00
parent 5943e7e62e
commit ab618be49f
9 changed files with 442 additions and 187 deletions
@@ -20,6 +20,7 @@ import bpy
from blenderbim.bim.module.model.data import AuthoringData
from blenderbim.bim.module.model.root import ConstrTypeEntityNotFound
from bpy.types import PropertyGroup
from math import pi
class BIMCadProperties(PropertyGroup):
@@ -28,3 +29,6 @@ class BIMCadProperties(PropertyGroup):
distance: bpy.props.FloatProperty(name="Distance", default=0.1)
x: bpy.props.FloatProperty(name="X", default=0.2)
y: bpy.props.FloatProperty(name="Y", default=0.1)
gable_roof_edge_angle: bpy.props.FloatProperty(
name="Gable Roof Edge Angle", default=pi / 2, soft_min=0, soft_max=pi / 2, subtype="ANGLE"
)
@@ -153,6 +153,11 @@ class CadTool(WorkSpaceTool):
row.operator("bim.cad_hotkey", text="Apply Roof Path").hotkey = "S_Q"
row.operator("bim.cancel_editing_roof_path", icon="CANCEL", text="")
row = layout.row(align=True)
row.label(text="", icon="EVENT_SHIFT")
row.label(text="", icon="EVENT_R")
row.operator("bim.hotkey", text="Set gable roof angle").hotkey = "S_R"
row = layout.row(align=True)
row.label(text="", icon="EVENT_SHIFT")
row.label(text="", icon="EVENT_E")
@@ -200,20 +205,31 @@ class CadHotkey(bpy.types.Operator):
if self.is_profile():
row = self.layout.row()
row.prop(props, "radius")
elif self.hotkey == "S_F":
if not self.is_profile():
row = self.layout.row()
row.prop(props, "resolution")
row = self.layout.row()
row.prop(props, "radius")
elif self.hotkey == "S_O":
row = self.layout.row()
row.prop(props, "distance")
elif self.hotkey == "S_R":
row = self.layout.row()
row.prop(props, "x")
row = self.layout.row()
row.prop(props, "y")
if self.is_profile():
row = self.layout.row()
row.prop(props, "x")
row = self.layout.row()
row.prop(props, "y")
elif (
(RoofData.is_loaded or not RoofData.load())
and RoofData.data["parameters"]
and bpy.context.active_object.BIMRoofProperties.is_editing_path
):
self.layout.row().prop(props, "gable_roof_edge_angle")
elif self.hotkey == "S_V":
if not self.is_profile():
row = self.layout.row()
@@ -264,6 +280,12 @@ class CadHotkey(bpy.types.Operator):
def hotkey_S_R(self):
if self.is_profile():
bpy.ops.bim.add_rectangle(x=self.props.x, y=self.props.y)
elif (
(RoofData.is_loaded or not RoofData.load())
and RoofData.data["parameters"]
and bpy.context.active_object.BIMRoofProperties.is_editing_path
):
bpy.ops.bim.set_gable_roof_edge_angle(angle=self.props.gable_roof_edge_angle)
def hotkey_S_T(self):
bpy.ops.bim.cad_mitre()
@@ -163,6 +163,7 @@ classes = (
roof.FinishEditingRoofPath,
roof.EnableEditingRoofPath,
roof.RemoveRoof,
roof.SetGableRoofEdgeAngle,
)
addon_keymaps = []
@@ -27,15 +27,28 @@ from gpu.types import GPUShader, GPUBatch, GPUIndexBuf, GPUVertBuf, GPUVertForma
from gpu_extras.batch import batch_for_shader
white = (1, 1, 1, 1)
lightgrey = (0.7, 0.7, 0.7, 1)
green = (0.545, 0.863, 0, 1)
red = (1, 0.2, 0.322, 1)
blue = (0.157, 0.565, 1, 1)
grey = (0.2, 0.2, 0.2, 1)
faces_color = (0.494, 0.540, 0.593, 1)
preview_edges_color = (0.130, 0.141, 0.371, 1)
class ProfileDecorator:
installed = None
@classmethod
def install(cls, context):
def install(cls, context, get_custom_bmesh=None, draw_faces=False):
if cls.installed:
cls.uninstall()
handler = cls()
cls.installed = SpaceView3D.draw_handler_add(handler, (context,), "WINDOW", "POST_VIEW")
cls.installed = SpaceView3D.draw_handler_add(
handler, (context, get_custom_bmesh, draw_faces), "WINDOW", "POST_VIEW"
)
@classmethod
def uninstall(cls):
@@ -45,25 +58,63 @@ class ProfileDecorator:
pass
cls.installed = None
def __call__(self, context):
def create_batch(self, shader_type, content_pos, color, indices=None, bind=True):
batch = batch_for_shader(self.shader, shader_type, {"pos": content_pos}, indices=indices)
# TODO: what's bind is for?
if bind:
self.shader.bind()
self.shader.uniform_float("color", color)
batch.draw(self.shader)
def draw_faces(self, bm, vertices_coords):
"""mutates original bm (triangulates it)
so the triangulation edges will be shown too
"""
traingulated_bm = bm
bmesh.ops.triangulate(traingulated_bm, faces=traingulated_bm.faces)
face_indices = [[v.index for v in f.verts] for f in traingulated_bm.faces]
self.create_batch("TRIS", vertices_coords, faces_color, face_indices)
def __call__(self, context, get_custom_bmesh=None, draw_faces=False):
obj = context.active_object
if obj.mode != "EDIT":
return
bgl.glLineWidth(2)
bgl.glPointSize(6)
bgl.glEnable(bgl.GL_BLEND)
bgl.glEnable(bgl.GL_LINE_SMOOTH)
if get_custom_bmesh:
bm = get_custom_bmesh()
else:
bm = bmesh.from_edit_mesh(obj.data)
def gl_init(use_bgl=False):
# TODO: remove as deprecated?
if use_bgl:
bgl.glLineWidth(2)
bgl.glPointSize(6)
bgl.glEnable(bgl.GL_BLEND)
bgl.glEnable(bgl.GL_LINE_SMOOTH)
else:
gpu.state.line_width_set(2)
gpu.state.point_size_set(6)
gpu.state.blend_set("ALPHA")
bgl.glEnable(bgl.GL_LINE_SMOOTH)
gl_init(True)
### Actually drawing
all_vertices = []
error_vertices = []
selected_vertices = []
unselected_vertices = []
# special = associated with arcs/circles
special_vertices = []
special_vertex_indices = {}
selected_edges = []
unselected_edges = []
special_edges = [] # edges that have a circle or an arc associated with them
arc_edges = []
roof_angle_edges = []
preview_edges = []
arc_groups = []
circle_groups = []
@@ -76,11 +127,11 @@ class ProfileDecorator:
arcs = {}
circles = {}
bm = bmesh.from_edit_mesh(obj.data)
# https://docs.blender.org/api/blender_python_api_2_63_8/bmesh.html#CustomDataAccess
# This is how we access vertex groups via bmesh, apparently, it's not very intuitive
deform_layer = bm.verts.layers.deform.active
angle_layer = bm.edges.layers.float.get("BBIM_gable_roof_angles")
preview_layer = bm.edges.layers.int.get("BBIM_preview")
for vertex in bm.verts:
co = tuple(obj.matrix_world @ vertex.co)
@@ -88,6 +139,8 @@ class ProfileDecorator:
if vertex.hide:
continue
# TODO: iterate over deform layers instead of all vertex groups?
# move to separate function `bm_check_vertex_in_groups`
is_arc = False
for group_index in arc_groups:
if group_index in vertex[deform_layer]:
@@ -128,51 +181,35 @@ class ProfileDecorator:
selected_edges.append(edge_indices)
else:
i1, i2 = edge.verts[0].index, edge.verts[1].index
# making sure that both vertices are in the same group
if i1 in special_vertex_indices and special_vertex_indices[i1] == special_vertex_indices.get(i2, None):
special_edges.append(edge_indices)
arc_edges.append(edge_indices)
elif angle_layer and edge[angle_layer] > 0:
roof_angle_edges.append(edge_indices)
elif preview_layer and edge[preview_layer] == 1:
preview_edges.append(edge_indices)
else:
unselected_edges.append(edge_indices)
indices = [[v.index for v in e.verts] for e in bm.edges]
white = (1, 1, 1, 1)
green = (0.545, 0.863, 0, 1)
red = (1, 0.2, 0.322, 1)
blue = (0.157, 0.565, 1, 1)
grey = (0.2, 0.2, 0.2, 1)
### Actually drawing
self.shader = gpu.shader.from_builtin("3D_UNIFORM_COLOR")
batch = batch_for_shader(self.shader, "LINES", {"pos": all_vertices}, indices=unselected_edges)
self.shader.bind()
self.shader.uniform_float("color", white)
batch.draw(self.shader)
# Draw faces
if draw_faces:
self.draw_faces(bm, all_vertices)
batch = batch_for_shader(self.shader, "LINES", {"pos": all_vertices}, indices=selected_edges)
self.shader.uniform_float("color", green)
batch.draw(self.shader)
self.create_batch("LINES", all_vertices, lightgrey, unselected_edges)
self.create_batch("LINES", all_vertices, green, selected_edges)
self.create_batch("LINES", all_vertices, grey, arc_edges)
self.create_batch("LINES", all_vertices, preview_edges_color, preview_edges)
self.create_batch("LINES", all_vertices, blue, roof_angle_edges)
batch = batch_for_shader(self.shader, "LINES", {"pos": all_vertices}, indices=special_edges)
self.shader.uniform_float("color", grey)
batch.draw(self.shader)
batch = batch_for_shader(self.shader, "POINTS", {"pos": unselected_vertices})
self.shader.uniform_float("color", white)
batch.draw(self.shader)
batch = batch_for_shader(self.shader, "POINTS", {"pos": error_vertices})
self.shader.uniform_float("color", red)
batch.draw(self.shader)
batch = batch_for_shader(self.shader, "POINTS", {"pos": special_vertices})
self.shader.uniform_float("color", blue)
batch.draw(self.shader)
batch = batch_for_shader(self.shader, "POINTS", {"pos": selected_vertices})
self.shader.uniform_float("color", green)
batch.draw(self.shader)
self.create_batch("POINTS", unselected_vertices, lightgrey)
self.create_batch("POINTS", error_vertices, red)
self.create_batch("POINTS", special_vertices, blue)
self.create_batch("POINTS", selected_vertices, green)
# Draw arcs
arc_centroids = []
arc_segments = []
for arc in arcs.values():
@@ -195,17 +232,11 @@ class ProfileDecorator:
arc_centroids.append(tuple(centroid))
arc_segments.append(tool.Cad.create_arc_segments(pts=points, num_verts=17, make_edges=True))
batch = batch_for_shader(self.shader, "POINTS", {"pos": arc_centroids})
self.shader.uniform_float("color", (0.2, 0.2, 0.2, 1))
batch.draw(self.shader)
self.create_batch("POINTS", arc_centroids, grey, bind=False)
for verts, edges in arc_segments:
batch = batch_for_shader(self.shader, "LINES", {"pos": verts}, indices=edges)
self.shader.uniform_float("color", (0.157, 0.565, 1, 1))
batch.draw(self.shader)
self.create_batch("LINES", verts, blue, edges, bind=False)
# Draw circles
circle_centroids = []
circle_segments = []
for circle in circles.values():
@@ -222,14 +253,9 @@ class ProfileDecorator:
segments = [[list(matrix @ Vector(v)) for v in segments[0]], segments[1]]
circle_segments.append(segments)
batch = batch_for_shader(self.shader, "POINTS", {"pos": circle_centroids})
self.shader.uniform_float("color", (0.2, 0.2, 0.2, 1))
batch.draw(self.shader)
self.create_batch("POINTS", circle_centroids, grey, bind=False)
for verts, edges in circle_segments:
batch = batch_for_shader(self.shader, "LINES", {"pos": verts}, indices=edges)
self.shader.uniform_float("color", (0.157, 0.565, 1, 1))
batch.draw(self.shader)
self.create_batch("LINES", verts, blue, edges, bind=False)
def create_matrix(self, p, x, y, z):
return Matrix([x, y, z, p]).to_4x4().transposed()
@@ -800,14 +800,7 @@ class DumbProfileJoiner:
return self.create_matrix(p, x_axis, y_axis, z_axis)
def create_matrix(self, p, x, y, z):
return Matrix(
(
(x[0], y[0], z[0], p[0]),
(x[1], y[1], z[1], p[1]),
(x[2], y[2], z[2], p[2]),
(0.0, 0.0, 0.0, 1.0),
)
)
return Matrix([x, y, z, p]).to_4x4().transposed()
def get_profile_axis(self, obj):
z_values = [v[2] for v in obj.bound_box]
@@ -23,6 +23,7 @@ from blenderbim.bim.prop import ObjProperty
from blenderbim.bim.module.model.data import AuthoringData
from blenderbim.bim.module.model.root import ConstrTypeEntityNotFound
from bpy.types import PropertyGroup, NodeTree
from math import pi
def get_ifc_class(self, context):
@@ -569,7 +570,7 @@ class BIMRailingProperties(PropertyGroup):
class BIMRoofProperties(PropertyGroup):
roof_types = (("HIP_ROOF", "HIP_ROOF", ""),)
roof_types = (("HIP/GABLE ROOF", "HIP/GABLE ROOF", ""),)
roof_generation_methods = (
("HEIGHT", "HEIGHT", ""),
("ANGLE", "ANGLE", ""),
@@ -579,12 +580,14 @@ class BIMRoofProperties(PropertyGroup):
is_editing: bpy.props.IntProperty(default=-1)
is_editing_path: bpy.props.BoolProperty(default=False)
roof_type: bpy.props.EnumProperty(name="Roof Type", items=roof_types, default="HIP_ROOF")
roof_type: bpy.props.EnumProperty(name="Roof Type", items=roof_types, default="HIP/GABLE ROOF")
generation_method: bpy.props.EnumProperty(
name="Roof Generation Method", items=roof_generation_methods, default="HEIGHT"
name="Roof Generation Method", items=roof_generation_methods, default="ANGLE"
)
height: bpy.props.FloatProperty(name="Height", default=1.0)
angle: bpy.props.FloatProperty(name="Slope Angle", default=10, description="In degrees")
height: bpy.props.FloatProperty(
name="Height", default=1.0, description="Maximum height of the roof to be generated.", subtype="DISTANCE"
)
angle: bpy.props.FloatProperty(name="Slope Angle", default=pi / 18, subtype="ANGLE")
def get_general_kwargs(self):
kwargs = {
@@ -29,23 +29,59 @@ from blenderbim.bim.module.model.data import RoofData, refresh
from blenderbim.bim.module.model.decorator import ProfileDecorator
import json
from math import tan, radians
from math import tan, radians, degrees, atan
from mathutils import Vector, Matrix
from bpypolyskel import bpypolyskel
import shapely
from pprint import pprint
from itertools import chain
from math import pi
# reference:
# https://ifc43-docs.standards.buildingsmart.org/IFC/RELEASE/IFC4x3/HTML/lexical/IfcRoof.htm
# https://ifc43-docs.standards.buildingsmart.org/IFC/RELEASE/IFC4x3/HTML/lexical/IfcRoofType.htm
# create read only property in blender operator
def float_is_zero(f):
return 0.0001 >= f >= -0.0001
def bm_mesh_clean_up(bm):
# remove internal edges and faces
# adding missing faces so we could rely on `e.is_boundary` later
bmesh.ops.contextual_create(bm, geom=bm.edges[:])
edges_to_dissolve = [e for e in bm.edges if not e.is_boundary]
bmesh.ops.dissolve_edges(bm, edges=edges_to_dissolve)
bmesh.ops.delete(bm, geom=bm.faces[:], context="FACES_ONLY")
bmesh.ops.dissolve_limit(
bm,
angle_limit=0.0872665,
use_dissolve_boundaries=False,
delimit={"NORMAL"},
edges=bm.edges[:],
verts=bm.verts[:],
)
bmesh.ops.remove_doubles(bm, verts=bm.verts, dist=0.0001)
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)
bl_options = {"REGISTER", "UNDO"}
roof_generation_methods = (
("HEIGHT", "HEIGHT", ""),
("ANGLE", "ANGLE", ""),
)
mode: bpy.props.EnumProperty(name="Roof Generation Method", items=roof_generation_methods, default="ANGLE")
height: bpy.props.FloatProperty(
name="Height", default=1.0, description="Maximum height of the roof to be generated.", subtype="DISTANCE"
)
angle: bpy.props.FloatProperty(name="Slope Angle", default=pi / 18, subtype="ANGLE")
def _execute(self, context):
obj = bpy.context.active_object
@@ -54,90 +90,190 @@ class GenerateHippedRoof(bpy.types.Operator, tool.Ifc.Operator):
return {"CANCELLED"}
bm = tool.Blender.get_bmesh_for_mesh(obj.data)
# argument values are the defaults for `bpy.ops.mesh.dissolve_limited`
bmesh.ops.dissolve_limit(
bm, angle_limit=0.0872665, use_dissolve_boundaries=False, delimit={"NORMAL"}, edges=bm.edges[:], verts=bm.verts[:]
)
generate_hiped_roof_bmesh(bm, self.mode, self.height, self.angle)
tool.Blender.apply_bmesh(obj.data, bm)
generate_hipped_roof(obj, self.mode, self.height, self.angle)
return {"FINISHED"}
def generate_hipped_roof(obj, mode="ANGLE", height=1.0, angle=10):
def generate_hiped_roof_bmesh(bm, mode="ANGLE", height=1.0, angle=pi / 18, mutate_current_bmesh=True):
"""return bmesh with gable roof geometry
`mutate_current_bmesh` is a flag to indicate whether the input bmesh
should be mutated or a new bmesh should be created and returned.
If the object is in EDIT mode then it will be the only way to change it.
If roof bmesh needed only to supply into decorator then there is no reason to mutate it.
"""
if not mutate_current_bmesh:
bm = bm.copy()
# CLEAN UP
bm_mesh_clean_up(bm)
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 mode == "HEIGHT":
height = height
angle = 0.0
original_geometry_data = dict()
angle_layer = bm.edges.layers.float.get("BBIM_gable_roof_angles")
if angle_layer:
original_geometry_data["edges"] = [(set(bm_get_indices(e.verts)), e[angle_layer]) for e in bm.edges]
else:
angle = tan(radians(round(angle, 4)))
height = 0.0
original_geometry_data["edges"] = [(set(bm_get_indices(e.verts)), None) for e in bm.edges]
faces = bpypolyskel.polygonize(
verts, start_exterior_index, total_exterior_verts, inner_loops, height, angle, faces, unit_vectors
)
original_geometry_data["verts"] = {v.index: v.co.copy() for v in bm.verts}
footprint_z = bm.verts[:][0].co.z
edges = []
def calculate_hiped_roof():
for edge in bm.edges:
boundary_lines.append(shapely.LineString([v.co for v in edge.verts]))
unioned_boundaries = shapely.union_all(shapely.GeometryCollection(boundary_lines))
closed_polygons = shapely.polygonize(unioned_boundaries.geoms)
# find the polygon with the biggest area
roof_polygon = max(closed_polygons.geoms, key=lambda polygon: polygon.area)
# add z coordinate if not present
roof_polygon = shapely.force_3d(roof_polygon)
# make sure the polygon is counter-clockwise
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 # in case when there is no .interiors
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 = []
nonlocal height, angle
if mode == "HEIGHT":
height = height
angle = 0.0
else:
angle = tan(angle)
height = 0.0
faces = bpypolyskel.polygonize(
verts, start_exterior_index, total_exterior_verts, inner_loops, height, angle, faces, unit_vectors
)
edges = []
return verts, edges, faces
verts, edges, faces = calculate_hiped_roof()
bm.clear()
bm = tool.Blender.get_bmesh_for_mesh(obj.data, clean=True)
new_verts = [bm.verts.new(v) for v in verts]
new_edges = [bm.edges.new([new_verts[vi] for vi in edge]) for edge in edges]
new_faces = [bm.faces.new([new_verts[vi] for vi in face]) for face in faces]
def find_identical_new_vert(co):
for v in bm.verts:
if float_is_zero((co - v.co).length):
return v
def find_other_polygon_verts(edge):
polygon = edge.link_faces[0]
return [v for v in polygon.verts if v not in edge.verts]
def change_angle(projected_vert_co, edge_verts, new_angle):
A, B = [v.co for v in edge_verts]
P = projected_vert_co
AP = P - A
AB = B - A
AB_dir = AB.normalized()
proj_length = AP.dot(AB_dir)
C = A + AB_dir * proj_length
Pp = P * Vector([1, 1, 0]) + Vector([0, 0, C.z])
Pp = P * Vector([1, 1, 0]) + Vector([0, 0, C.z])
angle_tan = tan(new_angle)
dist = (P.z - Pp.z) / angle_tan
PPnew = C + (Pp - C).normalized() * dist
Pnew = PPnew * Vector([1, 1, 0]) + Vector([0, 0, P.z])
return Pnew
footprint_edges = []
footprint_verts = set()
verts_to_change = {}
# find footprint edges
for edge in bm.edges:
if all(float_is_zero(v.co.z-footprint_z) for v in edge.verts):
footprint_edges.append(edge)
footprint_verts.update(edge.verts)
old_verts_remap = {}
for old_vert in original_geometry_data["verts"]:
old_vert_co = original_geometry_data["verts"][old_vert]
old_verts_remap[old_vert] = find_identical_new_vert(old_vert_co)
# iterate over edges from original geometry
# if their angle was redefined by user - apply the changes to the related vertices
# to match the requested angle
for old_edge_verts, defined_angle in original_geometry_data["edges"]:
if not defined_angle:
continue
edge_verts_remaped = set(old_verts_remap[old_vert] for old_vert in old_edge_verts)
for edge in footprint_edges:
if set(edge.verts) == edge_verts_remaped:
identical_edge = edge
break
verts_to_move = find_other_polygon_verts(identical_edge)
for v in verts_to_move:
vert_co = verts_to_change.get(v, v.co)
new_vert_co = change_angle(vert_co, edge_verts_remaped, defined_angle)
verts_to_change[v] = new_vert_co
# apply all changes once at the end
for v in verts_to_change:
v.co = verts_to_change[v]
extrusion_geom = bmesh.ops.extrude_face_region(bm, geom=bm.faces)["geom"]
extruded_verts = bm_sort_out_geom(extrusion_geom)["verts"]
bmesh.ops.translate(bm, vec=[0.0, 0.0, 0.1], verts=extruded_verts)
tool.Blender.apply_bmesh(obj.data, bm)
bmesh.ops.recalc_face_normals(bm, faces=bm.faces[:])
return bm
def bm_get_indices(sequence):
return [i.index for i in sequence]
def roof_is_gabled():
if not RoofData.is_loaded:
RoofData.load()
path_data = RoofData.parameters()["data_dict"]["path_data"]
angle_layer = path_data.get("gable_roof_angles", None)
if not angle_layer:
return False
for edge_angle in angle_layer:
if float_is_zero(edge_angle - pi / 2):
return True
return False
def update_roof_modifier_ifc_data(context):
"""should be called after new geometry settled
since it's going to update ifc representation
@@ -147,7 +283,9 @@ def update_roof_modifier_ifc_data(context):
element = tool.Ifc.get_entity(obj)
# type attributes
element.PredefinedType = props.roof_type
if props.roof_type == "HIP/GABLE ROOF":
element.PredefinedType = "GABLE_ROOF" if roof_is_gabled() else "HIP_ROOF"
# occurences attributes
# occurences = tool.Ifc.get_all_element_occurences(element)
@@ -172,34 +310,34 @@ def update_roof_modifier_bmesh(context):
if not RoofData.is_loaded:
RoofData.load()
path_data = RoofData.data["parameters"]["data_dict"]["path_data"]
angle_layer_data = path_data.get("gable_roof_angles", None)
si_conversion = ifcopenshell.util.unit.calculate_unit_scale(tool.Ifc.get())
# need to make sure we support edit mode
# since users will probably be in edit mode when they'll be changing roof path
bm = tool.Blender.get_bmesh_for_mesh(obj.data, clean=True)
angle_layer = bm.edges.layers.float.new("BBIM_gable_roof_angles")
# generating roof path
new_verts = [bm.verts.new(Vector(v) * si_conversion) for v in path_data["verts"]]
new_edges = [bm.edges.new((new_verts[e[0]], new_verts[e[1]])) for e in path_data["edges"]]
new_edges = []
for i in range(len(path_data["edges"])):
e = path_data["edges"][i]
edge = bm.edges.new((new_verts[e[0]], new_verts[e[1]]))
edge[angle_layer] = angle_layer_data[i] if angle_layer_data else 0
new_edges.append(edge)
if props.is_editing_path:
tool.Blender.apply_bmesh(obj.data, bm)
return
# apply dissolve limit seems to get more correct results with `generate_hipped_roof`
# argument values are the defaults for `bpy.ops.mesh.dissolve_limited`
bmesh.ops.dissolve_limit(
bm, angle_limit=0.0872665, use_dissolve_boundaries=False, delimit={"NORMAL"}, edges=bm.edges[:], verts=bm.verts[:]
)
tool.Blender.apply_bmesh(obj.data, bm)
height = props.height * si_conversion
angle = props.angle * si_conversion
generation_method = props.generation_method
generate_hipped_roof(obj, generation_method, height, angle)
generate_hiped_roof_bmesh(bm, props.generation_method, height, props.angle, mutate_current_bmesh=True)
tool.Blender.apply_bmesh(obj.data, bm)
def get_path_data(obj):
"""get path data for current mesh, path data is cleaned up"""
si_conversion = ifcopenshell.util.unit.calculate_unit_scale(tool.Ifc.get())
if obj.mode == "EDIT":
@@ -208,18 +346,15 @@ def get_path_data(obj):
obj.update_from_editmode()
bm = tool.Blender.get_bmesh_for_mesh(obj.data)
bm_mesh_clean_up(bm)
# remove internal edges and faces
# adding missing faces so we could rely on `e.is_boundary` later
bmesh.ops.contextual_create(bm, geom=bm.edges[:])
edges_to_dissolve = [e for e in bm.edges if not e.is_boundary]
bmesh.ops.dissolve_edges(bm, edges=edges_to_dissolve)
bmesh.ops.delete(bm, geom=bm.faces[:], context="FACES_ONLY")
angle_layer = bm.edges.layers.float.get("BBIM_gable_roof_angles")
path_data = dict()
path_data["edges"] = [bm_get_indices(e.verts) for e in bm.edges]
path_data["verts"] = [v.co / si_conversion for v in bm.verts]
if angle_layer:
path_data["gable_roof_angles"] = [e[angle_layer] for e in bm.edges]
if not path_data["edges"] or not path_data["verts"]:
return None
@@ -399,7 +534,30 @@ class EnableEditingRoofPath(bpy.types.Operator, tool.Ifc.Operator):
if bpy.context.object.mode != "EDIT":
bpy.ops.object.mode_set(mode="EDIT")
bpy.ops.wm.tool_set_by_id(tool.Blender.get_viewport_context(), name="bim.cad_tool")
ProfileDecorator.install(context)
def mark_preview_edges(bm, bew_verts, new_edges, new_faces):
preview_layer = bm.edges.layers.int["BBIM_preview"]
# can't create layer in callback because it kill all the bm edge references
for edge in new_edges:
edge[preview_layer] = 1
def get_custom_bmesh():
# copying to make sure not to mutate the edit mode bmesh
bm = tool.Blender.get_bmesh_for_mesh(obj.data)
main_bm = bm.copy()
main_bm.edges.layers.int.new("BBIM_preview")
si_conversion = ifcopenshell.util.unit.calculate_unit_scale(tool.Ifc.get())
height = props.height * si_conversion
second_bm = generate_hiped_roof_bmesh(
bm, props.generation_method, height, props.angle, mutate_current_bmesh=False
)
bmesh.ops.translate(second_bm, verts=second_bm.verts, vec=Vector((0, 0, 1)))
tool.Blender.bmesh_join(main_bm, second_bm, callback=mark_preview_edges)
return main_bm
ProfileDecorator.install(context, get_custom_bmesh, draw_faces=True)
return {"FINISHED"}
@@ -440,6 +598,7 @@ class FinishEditingRoofPath(bpy.types.Operator, tool.Ifc.Operator):
update_bbim_roof_pset(element, roof_data)
refresh() # RoofData has to be updated before run update_roof_modifier_bmesh
update_roof_modifier_bmesh(context)
update_roof_modifier_ifc_data(context)
if bpy.context.object.mode == "EDIT":
bpy.ops.object.mode_set(mode="OBJECT")
update_roof_modifier_ifc_data(context)
@@ -463,5 +622,43 @@ class RemoveRoof(bpy.types.Operator, tool.Ifc.Operator):
return {"FINISHED"}
class SetGableRoofEdgeAngle(bpy.types.Operator):
bl_idname = "bim.set_gable_roof_edge_angle"
bl_label = "Set gable roof edge angle"
bl_options = {"REGISTER", "UNDO"}
angle: bpy.props.FloatProperty(name="Angle", default=90)
@classmethod
def poll(cls, context):
obj = context.active_object
return obj and obj.type == "MESH" and context.mode == "EDIT_MESH"
def draw(self, context):
layout = self.layout
for prop in self.__class__.__annotations__.keys():
layout.prop(self, prop)
def execute(self, context):
# tried to avoid bmesh with foreach_get and foreach_set
# but in EDIT mode it's only possible to change attributes by working with bmesh
me = context.object.data
bm = tool.Blender.get_bmesh_for_mesh(me)
# check if attribute exists or create one
if "BBIM_gable_roof_angles" not in me.attributes:
me.attributes.new("BBIM_gable_roof_angles", type="FLOAT", domain="EDGE")
angles_layer = bm.edges.layers.float["BBIM_gable_roof_angles"]
for e in bm.edges:
if not e.select:
continue
e[angles_layer] = self.angle
tool.Blender.apply_bmesh(me, bm)
return {"FINISHED"}
def add_object_button(self, context):
self.layout.operator(BIM_OT_add_roof.bl_idname, icon="PLUGIN")
@@ -1234,14 +1234,7 @@ class DumbWallJoiner:
tool.Geometry.record_object_materials(obj)
def create_matrix(self, p, x, y, z):
return Matrix(
(
(x[0], y[0], z[0], p[0]),
(x[1], y[1], z[1], p[1]),
(x[2], y[2], z[2], p[2]),
(0.0, 0.0, 0.0, 1.0),
)
)
return Matrix([x, y, z, p]).to_4x4().transposed()
def get_extrusion_data(self, representation):
results = {"item": None, "height": 3.0, "x_angle": 0, "is_sloped": False, "direction": Vector((0, 0, 1))}
+33 -17
View File
@@ -93,6 +93,28 @@ class Blender:
return False
return False
@classmethod
def get_viewport_context(cls):
"""Get viewport area context for context overriding.
Useful for calling operators outside viewport context.
It's a bit naive since it's just taking the first available `VIEW_3D` area
when in real life you can have a couple of those but should work for the most cases.
"""
area = next(area for area in bpy.context.screen.areas if area.type == "VIEW_3D")
context_override = {"area": area}
return context_override
@classmethod
def update_viewport(cls):
# if it stops working in future Blender versions
# there is an alternative:
# bpy.ops.wm.redraw_timer(type='DRAW_WIN_SWAP', iterations=1)
tool.Blender.get_viewport_context()['area'].tag_redraw()
## BMESH UTILS ##
@classmethod
def apply_bmesh(cls, mesh, bm):
import bmesh
@@ -131,24 +153,18 @@ class Blender:
if not clean:
bm.from_mesh(mesh)
return bm
@classmethod
def get_viewport_context(cls):
"""Get viewport area context for context overriding.
def bmesh_join(cls, bm_a, bm_b, callback=None):
"""Join two meshes into single one, store it in `bm_a`"""
import bmesh
Useful for calling operators outside viewport context.
new_verts = [bm_a.verts.new(v.co) for v in bm_b.verts]
new_edges = [bm_a.edges.new([new_verts[v.index] for v in edge.verts]) for edge in bm_b.edges]
new_faces = [bm_a.faces.new([new_verts[v.index] for v in face.verts]) for face in bm_b.faces]
bmesh.ops.recalc_face_normals(bm_a, faces=bm_a.faces[:])
It's a bit naive since it's just taking the first available `VIEW_3D` area
when in real life you can have a couple of those but should work for the most cases.
"""
area = next(area for area in bpy.context.screen.areas if area.type == "VIEW_3D")
context_override = {"area": area}
return context_override
@classmethod
def update_viewport(cls):
# if it stops working in future Blender versions
# there is an alternative:
# bpy.ops.wm.redraw_timer(type='DRAW_WIN_SWAP', iterations=1)
if callback:
callback(bm_a, new_verts, new_edges, new_faces)
tool.Blender.get_viewport_context()['area'].tag_redraw()
return bm_a