mirror of
https://github.com/IfcOpenShell/IfcOpenShell.git
synced 2026-08-09 09:21:46 +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.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))}
|
||||
|
||||
@@ -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
|
||||
Reference in New Issue
Block a user