mirror of
https://github.com/IfcOpenShell/IfcOpenShell.git
synced 2026-09-18 06:21:40 +00:00
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:
@@ -20,6 +20,7 @@ import bpy
|
|||||||
from blenderbim.bim.module.model.data import AuthoringData
|
from blenderbim.bim.module.model.data import AuthoringData
|
||||||
from blenderbim.bim.module.model.root import ConstrTypeEntityNotFound
|
from blenderbim.bim.module.model.root import ConstrTypeEntityNotFound
|
||||||
from bpy.types import PropertyGroup
|
from bpy.types import PropertyGroup
|
||||||
|
from math import pi
|
||||||
|
|
||||||
|
|
||||||
class BIMCadProperties(PropertyGroup):
|
class BIMCadProperties(PropertyGroup):
|
||||||
@@ -28,3 +29,6 @@ class BIMCadProperties(PropertyGroup):
|
|||||||
distance: bpy.props.FloatProperty(name="Distance", default=0.1)
|
distance: bpy.props.FloatProperty(name="Distance", default=0.1)
|
||||||
x: bpy.props.FloatProperty(name="X", default=0.2)
|
x: bpy.props.FloatProperty(name="X", default=0.2)
|
||||||
y: bpy.props.FloatProperty(name="Y", default=0.1)
|
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.cad_hotkey", text="Apply Roof Path").hotkey = "S_Q"
|
||||||
row.operator("bim.cancel_editing_roof_path", icon="CANCEL", text="")
|
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 = layout.row(align=True)
|
||||||
row.label(text="", icon="EVENT_SHIFT")
|
row.label(text="", icon="EVENT_SHIFT")
|
||||||
row.label(text="", icon="EVENT_E")
|
row.label(text="", icon="EVENT_E")
|
||||||
@@ -200,20 +205,31 @@ class CadHotkey(bpy.types.Operator):
|
|||||||
if self.is_profile():
|
if self.is_profile():
|
||||||
row = self.layout.row()
|
row = self.layout.row()
|
||||||
row.prop(props, "radius")
|
row.prop(props, "radius")
|
||||||
|
|
||||||
elif self.hotkey == "S_F":
|
elif self.hotkey == "S_F":
|
||||||
if not self.is_profile():
|
if not self.is_profile():
|
||||||
row = self.layout.row()
|
row = self.layout.row()
|
||||||
row.prop(props, "resolution")
|
row.prop(props, "resolution")
|
||||||
row = self.layout.row()
|
row = self.layout.row()
|
||||||
row.prop(props, "radius")
|
row.prop(props, "radius")
|
||||||
|
|
||||||
elif self.hotkey == "S_O":
|
elif self.hotkey == "S_O":
|
||||||
row = self.layout.row()
|
row = self.layout.row()
|
||||||
row.prop(props, "distance")
|
row.prop(props, "distance")
|
||||||
|
|
||||||
elif self.hotkey == "S_R":
|
elif self.hotkey == "S_R":
|
||||||
row = self.layout.row()
|
if self.is_profile():
|
||||||
row.prop(props, "x")
|
row = self.layout.row()
|
||||||
row = self.layout.row()
|
row.prop(props, "x")
|
||||||
row.prop(props, "y")
|
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":
|
elif self.hotkey == "S_V":
|
||||||
if not self.is_profile():
|
if not self.is_profile():
|
||||||
row = self.layout.row()
|
row = self.layout.row()
|
||||||
@@ -264,6 +280,12 @@ class CadHotkey(bpy.types.Operator):
|
|||||||
def hotkey_S_R(self):
|
def hotkey_S_R(self):
|
||||||
if self.is_profile():
|
if self.is_profile():
|
||||||
bpy.ops.bim.add_rectangle(x=self.props.x, y=self.props.y)
|
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):
|
def hotkey_S_T(self):
|
||||||
bpy.ops.bim.cad_mitre()
|
bpy.ops.bim.cad_mitre()
|
||||||
|
|||||||
@@ -163,6 +163,7 @@ classes = (
|
|||||||
roof.FinishEditingRoofPath,
|
roof.FinishEditingRoofPath,
|
||||||
roof.EnableEditingRoofPath,
|
roof.EnableEditingRoofPath,
|
||||||
roof.RemoveRoof,
|
roof.RemoveRoof,
|
||||||
|
roof.SetGableRoofEdgeAngle,
|
||||||
)
|
)
|
||||||
|
|
||||||
addon_keymaps = []
|
addon_keymaps = []
|
||||||
|
|||||||
@@ -27,15 +27,28 @@ from gpu.types import GPUShader, GPUBatch, GPUIndexBuf, GPUVertBuf, GPUVertForma
|
|||||||
from gpu_extras.batch import batch_for_shader
|
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:
|
class ProfileDecorator:
|
||||||
installed = None
|
installed = None
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def install(cls, context):
|
def install(cls, context, get_custom_bmesh=None, draw_faces=False):
|
||||||
if cls.installed:
|
if cls.installed:
|
||||||
cls.uninstall()
|
cls.uninstall()
|
||||||
handler = cls()
|
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
|
@classmethod
|
||||||
def uninstall(cls):
|
def uninstall(cls):
|
||||||
@@ -45,25 +58,63 @@ class ProfileDecorator:
|
|||||||
pass
|
pass
|
||||||
cls.installed = None
|
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
|
obj = context.active_object
|
||||||
|
|
||||||
if obj.mode != "EDIT":
|
if obj.mode != "EDIT":
|
||||||
return
|
return
|
||||||
|
|
||||||
bgl.glLineWidth(2)
|
if get_custom_bmesh:
|
||||||
bgl.glPointSize(6)
|
bm = get_custom_bmesh()
|
||||||
bgl.glEnable(bgl.GL_BLEND)
|
else:
|
||||||
bgl.glEnable(bgl.GL_LINE_SMOOTH)
|
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 = []
|
all_vertices = []
|
||||||
error_vertices = []
|
error_vertices = []
|
||||||
selected_vertices = []
|
selected_vertices = []
|
||||||
unselected_vertices = []
|
unselected_vertices = []
|
||||||
|
# special = associated with arcs/circles
|
||||||
special_vertices = []
|
special_vertices = []
|
||||||
special_vertex_indices = {}
|
special_vertex_indices = {}
|
||||||
selected_edges = []
|
selected_edges = []
|
||||||
unselected_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 = []
|
arc_groups = []
|
||||||
circle_groups = []
|
circle_groups = []
|
||||||
@@ -76,11 +127,11 @@ class ProfileDecorator:
|
|||||||
arcs = {}
|
arcs = {}
|
||||||
circles = {}
|
circles = {}
|
||||||
|
|
||||||
bm = bmesh.from_edit_mesh(obj.data)
|
|
||||||
|
|
||||||
# https://docs.blender.org/api/blender_python_api_2_63_8/bmesh.html#CustomDataAccess
|
# 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
|
# This is how we access vertex groups via bmesh, apparently, it's not very intuitive
|
||||||
deform_layer = bm.verts.layers.deform.active
|
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:
|
for vertex in bm.verts:
|
||||||
co = tuple(obj.matrix_world @ vertex.co)
|
co = tuple(obj.matrix_world @ vertex.co)
|
||||||
@@ -88,6 +139,8 @@ class ProfileDecorator:
|
|||||||
if vertex.hide:
|
if vertex.hide:
|
||||||
continue
|
continue
|
||||||
|
|
||||||
|
# TODO: iterate over deform layers instead of all vertex groups?
|
||||||
|
# move to separate function `bm_check_vertex_in_groups`
|
||||||
is_arc = False
|
is_arc = False
|
||||||
for group_index in arc_groups:
|
for group_index in arc_groups:
|
||||||
if group_index in vertex[deform_layer]:
|
if group_index in vertex[deform_layer]:
|
||||||
@@ -128,51 +181,35 @@ class ProfileDecorator:
|
|||||||
selected_edges.append(edge_indices)
|
selected_edges.append(edge_indices)
|
||||||
else:
|
else:
|
||||||
i1, i2 = edge.verts[0].index, edge.verts[1].index
|
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):
|
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:
|
else:
|
||||||
unselected_edges.append(edge_indices)
|
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")
|
self.shader = gpu.shader.from_builtin("3D_UNIFORM_COLOR")
|
||||||
|
|
||||||
batch = batch_for_shader(self.shader, "LINES", {"pos": all_vertices}, indices=unselected_edges)
|
# Draw faces
|
||||||
self.shader.bind()
|
if draw_faces:
|
||||||
self.shader.uniform_float("color", white)
|
self.draw_faces(bm, all_vertices)
|
||||||
batch.draw(self.shader)
|
|
||||||
|
|
||||||
batch = batch_for_shader(self.shader, "LINES", {"pos": all_vertices}, indices=selected_edges)
|
self.create_batch("LINES", all_vertices, lightgrey, unselected_edges)
|
||||||
self.shader.uniform_float("color", green)
|
self.create_batch("LINES", all_vertices, green, selected_edges)
|
||||||
batch.draw(self.shader)
|
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.create_batch("POINTS", unselected_vertices, lightgrey)
|
||||||
self.shader.uniform_float("color", grey)
|
self.create_batch("POINTS", error_vertices, red)
|
||||||
batch.draw(self.shader)
|
self.create_batch("POINTS", special_vertices, blue)
|
||||||
|
self.create_batch("POINTS", selected_vertices, green)
|
||||||
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)
|
|
||||||
|
|
||||||
# Draw arcs
|
# Draw arcs
|
||||||
|
|
||||||
arc_centroids = []
|
arc_centroids = []
|
||||||
arc_segments = []
|
arc_segments = []
|
||||||
for arc in arcs.values():
|
for arc in arcs.values():
|
||||||
@@ -195,17 +232,11 @@ class ProfileDecorator:
|
|||||||
arc_centroids.append(tuple(centroid))
|
arc_centroids.append(tuple(centroid))
|
||||||
arc_segments.append(tool.Cad.create_arc_segments(pts=points, num_verts=17, make_edges=True))
|
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.create_batch("POINTS", arc_centroids, grey, bind=False)
|
||||||
self.shader.uniform_float("color", (0.2, 0.2, 0.2, 1))
|
|
||||||
batch.draw(self.shader)
|
|
||||||
|
|
||||||
for verts, edges in arc_segments:
|
for verts, edges in arc_segments:
|
||||||
batch = batch_for_shader(self.shader, "LINES", {"pos": verts}, indices=edges)
|
self.create_batch("LINES", verts, blue, edges, bind=False)
|
||||||
self.shader.uniform_float("color", (0.157, 0.565, 1, 1))
|
|
||||||
batch.draw(self.shader)
|
|
||||||
|
|
||||||
# Draw circles
|
# Draw circles
|
||||||
|
|
||||||
circle_centroids = []
|
circle_centroids = []
|
||||||
circle_segments = []
|
circle_segments = []
|
||||||
for circle in circles.values():
|
for circle in circles.values():
|
||||||
@@ -222,14 +253,9 @@ class ProfileDecorator:
|
|||||||
segments = [[list(matrix @ Vector(v)) for v in segments[0]], segments[1]]
|
segments = [[list(matrix @ Vector(v)) for v in segments[0]], segments[1]]
|
||||||
circle_segments.append(segments)
|
circle_segments.append(segments)
|
||||||
|
|
||||||
batch = batch_for_shader(self.shader, "POINTS", {"pos": circle_centroids})
|
self.create_batch("POINTS", circle_centroids, grey, bind=False)
|
||||||
self.shader.uniform_float("color", (0.2, 0.2, 0.2, 1))
|
|
||||||
batch.draw(self.shader)
|
|
||||||
|
|
||||||
for verts, edges in circle_segments:
|
for verts, edges in circle_segments:
|
||||||
batch = batch_for_shader(self.shader, "LINES", {"pos": verts}, indices=edges)
|
self.create_batch("LINES", verts, blue, edges, bind=False)
|
||||||
self.shader.uniform_float("color", (0.157, 0.565, 1, 1))
|
|
||||||
batch.draw(self.shader)
|
|
||||||
|
|
||||||
def create_matrix(self, p, x, y, z):
|
def create_matrix(self, p, x, y, z):
|
||||||
return Matrix([x, y, z, p]).to_4x4().transposed()
|
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)
|
return self.create_matrix(p, x_axis, y_axis, z_axis)
|
||||||
|
|
||||||
def create_matrix(self, p, x, y, z):
|
def create_matrix(self, p, x, y, z):
|
||||||
return Matrix(
|
return Matrix([x, y, z, p]).to_4x4().transposed()
|
||||||
(
|
|
||||||
(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),
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
def get_profile_axis(self, obj):
|
def get_profile_axis(self, obj):
|
||||||
z_values = [v[2] for v in obj.bound_box]
|
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.data import AuthoringData
|
||||||
from blenderbim.bim.module.model.root import ConstrTypeEntityNotFound
|
from blenderbim.bim.module.model.root import ConstrTypeEntityNotFound
|
||||||
from bpy.types import PropertyGroup, NodeTree
|
from bpy.types import PropertyGroup, NodeTree
|
||||||
|
from math import pi
|
||||||
|
|
||||||
|
|
||||||
def get_ifc_class(self, context):
|
def get_ifc_class(self, context):
|
||||||
@@ -569,7 +570,7 @@ class BIMRailingProperties(PropertyGroup):
|
|||||||
|
|
||||||
|
|
||||||
class BIMRoofProperties(PropertyGroup):
|
class BIMRoofProperties(PropertyGroup):
|
||||||
roof_types = (("HIP_ROOF", "HIP_ROOF", ""),)
|
roof_types = (("HIP/GABLE ROOF", "HIP/GABLE ROOF", ""),)
|
||||||
roof_generation_methods = (
|
roof_generation_methods = (
|
||||||
("HEIGHT", "HEIGHT", ""),
|
("HEIGHT", "HEIGHT", ""),
|
||||||
("ANGLE", "ANGLE", ""),
|
("ANGLE", "ANGLE", ""),
|
||||||
@@ -579,12 +580,14 @@ class BIMRoofProperties(PropertyGroup):
|
|||||||
is_editing: bpy.props.IntProperty(default=-1)
|
is_editing: bpy.props.IntProperty(default=-1)
|
||||||
is_editing_path: bpy.props.BoolProperty(default=False)
|
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(
|
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)
|
height: bpy.props.FloatProperty(
|
||||||
angle: bpy.props.FloatProperty(name="Slope Angle", default=10, description="In degrees")
|
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):
|
def get_general_kwargs(self):
|
||||||
kwargs = {
|
kwargs = {
|
||||||
|
|||||||
@@ -29,23 +29,59 @@ from blenderbim.bim.module.model.data import RoofData, refresh
|
|||||||
from blenderbim.bim.module.model.decorator import ProfileDecorator
|
from blenderbim.bim.module.model.decorator import ProfileDecorator
|
||||||
|
|
||||||
import json
|
import json
|
||||||
from math import tan, radians
|
from math import tan, radians, degrees, atan
|
||||||
from mathutils import Vector, Matrix
|
from mathutils import Vector, Matrix
|
||||||
from bpypolyskel import bpypolyskel
|
from bpypolyskel import bpypolyskel
|
||||||
import shapely
|
import shapely
|
||||||
|
from pprint import pprint
|
||||||
|
from itertools import chain
|
||||||
|
from math import pi
|
||||||
|
|
||||||
# reference:
|
# 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/IfcRoof.htm
|
||||||
# https://ifc43-docs.standards.buildingsmart.org/IFC/RELEASE/IFC4x3/HTML/lexical/IfcRoofType.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):
|
class GenerateHippedRoof(bpy.types.Operator, tool.Ifc.Operator):
|
||||||
bl_idname = "bim.generate_hipped_roof"
|
bl_idname = "bim.generate_hipped_roof"
|
||||||
bl_label = "Generate Hipped Roof"
|
bl_label = "Generate Hipped Roof"
|
||||||
bl_options = {"REGISTER"}
|
bl_options = {"REGISTER", "UNDO"}
|
||||||
mode: bpy.props.StringProperty(default="ANGLE")
|
|
||||||
height: bpy.props.FloatProperty(default=1)
|
roof_generation_methods = (
|
||||||
angle: bpy.props.FloatProperty(default=10)
|
("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):
|
def _execute(self, context):
|
||||||
obj = bpy.context.active_object
|
obj = bpy.context.active_object
|
||||||
@@ -54,90 +90,190 @@ class GenerateHippedRoof(bpy.types.Operator, tool.Ifc.Operator):
|
|||||||
return {"CANCELLED"}
|
return {"CANCELLED"}
|
||||||
|
|
||||||
bm = tool.Blender.get_bmesh_for_mesh(obj.data)
|
bm = tool.Blender.get_bmesh_for_mesh(obj.data)
|
||||||
# argument values are the defaults for `bpy.ops.mesh.dissolve_limited`
|
generate_hiped_roof_bmesh(bm, self.mode, self.height, self.angle)
|
||||||
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)
|
tool.Blender.apply_bmesh(obj.data, bm)
|
||||||
|
|
||||||
generate_hipped_roof(obj, self.mode, self.height, self.angle)
|
|
||||||
return {"FINISHED"}
|
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 = []
|
boundary_lines = []
|
||||||
|
|
||||||
for edge in obj.data.edges:
|
original_geometry_data = dict()
|
||||||
boundary_lines.append(
|
angle_layer = bm.edges.layers.float.get("BBIM_gable_roof_angles")
|
||||||
shapely.LineString([obj.data.vertices[edge.vertices[0]].co, obj.data.vertices[edge.vertices[1]].co])
|
if angle_layer:
|
||||||
)
|
original_geometry_data["edges"] = [(set(bm_get_indices(e.verts)), e[angle_layer]) for e in bm.edges]
|
||||||
|
|
||||||
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
|
|
||||||
else:
|
else:
|
||||||
angle = tan(radians(round(angle, 4)))
|
original_geometry_data["edges"] = [(set(bm_get_indices(e.verts)), None) for e in bm.edges]
|
||||||
height = 0.0
|
|
||||||
|
|
||||||
faces = bpypolyskel.polygonize(
|
original_geometry_data["verts"] = {v.index: v.co.copy() for v in bm.verts}
|
||||||
verts, start_exterior_index, total_exterior_verts, inner_loops, height, angle, faces, unit_vectors
|
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_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_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]
|
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"]
|
extrusion_geom = bmesh.ops.extrude_face_region(bm, geom=bm.faces)["geom"]
|
||||||
extruded_verts = bm_sort_out_geom(extrusion_geom)["verts"]
|
extruded_verts = bm_sort_out_geom(extrusion_geom)["verts"]
|
||||||
bmesh.ops.translate(bm, vec=[0.0, 0.0, 0.1], verts=extruded_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):
|
def bm_get_indices(sequence):
|
||||||
return [i.index for i in 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):
|
def update_roof_modifier_ifc_data(context):
|
||||||
"""should be called after new geometry settled
|
"""should be called after new geometry settled
|
||||||
since it's going to update ifc representation
|
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)
|
element = tool.Ifc.get_entity(obj)
|
||||||
|
|
||||||
# type attributes
|
# 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 attributes
|
||||||
# occurences = tool.Ifc.get_all_element_occurences(element)
|
# occurences = tool.Ifc.get_all_element_occurences(element)
|
||||||
|
|
||||||
@@ -172,34 +310,34 @@ def update_roof_modifier_bmesh(context):
|
|||||||
if not RoofData.is_loaded:
|
if not RoofData.is_loaded:
|
||||||
RoofData.load()
|
RoofData.load()
|
||||||
path_data = RoofData.data["parameters"]["data_dict"]["path_data"]
|
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())
|
si_conversion = ifcopenshell.util.unit.calculate_unit_scale(tool.Ifc.get())
|
||||||
# need to make sure we support edit mode
|
# need to make sure we support edit mode
|
||||||
# since users will probably be in edit mode when they'll be changing roof path
|
# 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)
|
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
|
# generating roof path
|
||||||
new_verts = [bm.verts.new(Vector(v) * si_conversion) for v in path_data["verts"]]
|
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:
|
if props.is_editing_path:
|
||||||
tool.Blender.apply_bmesh(obj.data, bm)
|
tool.Blender.apply_bmesh(obj.data, bm)
|
||||||
return
|
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
|
height = props.height * si_conversion
|
||||||
angle = props.angle * si_conversion
|
generate_hiped_roof_bmesh(bm, props.generation_method, height, props.angle, mutate_current_bmesh=True)
|
||||||
generation_method = props.generation_method
|
tool.Blender.apply_bmesh(obj.data, bm)
|
||||||
generate_hipped_roof(obj, generation_method, height, angle)
|
|
||||||
|
|
||||||
|
|
||||||
def get_path_data(obj):
|
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())
|
si_conversion = ifcopenshell.util.unit.calculate_unit_scale(tool.Ifc.get())
|
||||||
|
|
||||||
if obj.mode == "EDIT":
|
if obj.mode == "EDIT":
|
||||||
@@ -208,18 +346,15 @@ def get_path_data(obj):
|
|||||||
obj.update_from_editmode()
|
obj.update_from_editmode()
|
||||||
|
|
||||||
bm = tool.Blender.get_bmesh_for_mesh(obj.data)
|
bm = tool.Blender.get_bmesh_for_mesh(obj.data)
|
||||||
|
bm_mesh_clean_up(bm)
|
||||||
|
|
||||||
# remove internal edges and faces
|
angle_layer = bm.edges.layers.float.get("BBIM_gable_roof_angles")
|
||||||
# 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")
|
|
||||||
|
|
||||||
path_data = dict()
|
path_data = dict()
|
||||||
path_data["edges"] = [bm_get_indices(e.verts) for e in bm.edges]
|
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]
|
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"]:
|
if not path_data["edges"] or not path_data["verts"]:
|
||||||
return None
|
return None
|
||||||
@@ -399,7 +534,30 @@ class EnableEditingRoofPath(bpy.types.Operator, tool.Ifc.Operator):
|
|||||||
if bpy.context.object.mode != "EDIT":
|
if bpy.context.object.mode != "EDIT":
|
||||||
bpy.ops.object.mode_set(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")
|
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"}
|
return {"FINISHED"}
|
||||||
|
|
||||||
|
|
||||||
@@ -440,6 +598,7 @@ class FinishEditingRoofPath(bpy.types.Operator, tool.Ifc.Operator):
|
|||||||
update_bbim_roof_pset(element, roof_data)
|
update_bbim_roof_pset(element, roof_data)
|
||||||
refresh() # RoofData has to be updated before run update_roof_modifier_bmesh
|
refresh() # RoofData has to be updated before run update_roof_modifier_bmesh
|
||||||
update_roof_modifier_bmesh(context)
|
update_roof_modifier_bmesh(context)
|
||||||
|
update_roof_modifier_ifc_data(context)
|
||||||
if bpy.context.object.mode == "EDIT":
|
if bpy.context.object.mode == "EDIT":
|
||||||
bpy.ops.object.mode_set(mode="OBJECT")
|
bpy.ops.object.mode_set(mode="OBJECT")
|
||||||
update_roof_modifier_ifc_data(context)
|
update_roof_modifier_ifc_data(context)
|
||||||
@@ -463,5 +622,43 @@ class RemoveRoof(bpy.types.Operator, tool.Ifc.Operator):
|
|||||||
return {"FINISHED"}
|
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):
|
def add_object_button(self, context):
|
||||||
self.layout.operator(BIM_OT_add_roof.bl_idname, icon="PLUGIN")
|
self.layout.operator(BIM_OT_add_roof.bl_idname, icon="PLUGIN")
|
||||||
|
|||||||
@@ -1234,14 +1234,7 @@ class DumbWallJoiner:
|
|||||||
tool.Geometry.record_object_materials(obj)
|
tool.Geometry.record_object_materials(obj)
|
||||||
|
|
||||||
def create_matrix(self, p, x, y, z):
|
def create_matrix(self, p, x, y, z):
|
||||||
return Matrix(
|
return Matrix([x, y, z, p]).to_4x4().transposed()
|
||||||
(
|
|
||||||
(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),
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
def get_extrusion_data(self, representation):
|
def get_extrusion_data(self, representation):
|
||||||
results = {"item": None, "height": 3.0, "x_angle": 0, "is_sloped": False, "direction": Vector((0, 0, 1))}
|
results = {"item": None, "height": 3.0, "x_angle": 0, "is_sloped": False, "direction": Vector((0, 0, 1))}
|
||||||
|
|||||||
@@ -93,6 +93,28 @@ class Blender:
|
|||||||
return False
|
return False
|
||||||
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
|
@classmethod
|
||||||
def apply_bmesh(cls, mesh, bm):
|
def apply_bmesh(cls, mesh, bm):
|
||||||
import bmesh
|
import bmesh
|
||||||
@@ -133,22 +155,16 @@ class Blender:
|
|||||||
return bm
|
return bm
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def get_viewport_context(cls):
|
def bmesh_join(cls, bm_a, bm_b, callback=None):
|
||||||
"""Get viewport area context for context overriding.
|
"""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
|
if callback:
|
||||||
when in real life you can have a couple of those but should work for the most cases.
|
callback(bm_a, new_verts, new_edges, new_faces)
|
||||||
"""
|
|
||||||
area = next(area for area in bpy.context.screen.areas if area.type == "VIEW_3D")
|
|
||||||
context_override = {"area": area}
|
|
||||||
return context_override
|
|
||||||
|
|
||||||
@classmethod
|
return bm_a
|
||||||
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()
|
|
||||||
Reference in New Issue
Block a user