rafter edge angle and roof thickness #3092

Added option to adjust rafter edge angle and roof thickness
Short demonstration - https://imgur.com/a/TsYSs6i
This commit is contained in:
Andrej730
2023-05-30 18:00:37 +05:00
parent 9dd82036ba
commit f6e72ec06f
4 changed files with 59 additions and 38 deletions
@@ -564,6 +564,7 @@ class BIMRailingProperties(PropertyGroup):
class BIMRoofProperties(PropertyGroup): class BIMRoofProperties(PropertyGroup):
non_si_units_props = ("is_editing", "path_data", "roof_type", "roof_added_previously", "generation_method", "angle", "rafter_edge_angle")
roof_types = (("HIP/GABLE ROOF", "HIP/GABLE ROOF", ""),) roof_types = (("HIP/GABLE ROOF", "HIP/GABLE ROOF", ""),)
roof_generation_methods = ( roof_generation_methods = (
("HEIGHT", "HEIGHT", ""), ("HEIGHT", "HEIGHT", ""),
@@ -582,14 +583,26 @@ class BIMRoofProperties(PropertyGroup):
name="Height", default=1.0, description="Maximum height of the roof to be generated.", subtype="DISTANCE" 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") angle: bpy.props.FloatProperty(name="Slope Angle", default=pi / 18, subtype="ANGLE")
roof_thickness: bpy.props.FloatProperty(name="Roof Thickness", default=0.1, subtype="DISTANCE")
rafter_edge_angle: bpy.props.FloatProperty(name="Rafter Edge Angle", min=0, default=pi / 2, subtype="ANGLE")
def get_general_kwargs(self): def get_general_kwargs(self, convert_to_project_units=False):
kwargs = { kwargs = {
"roof_type": self.roof_type, "roof_type": self.roof_type,
"generation_method": self.generation_method, "generation_method": self.generation_method,
"roof_thickness": self.roof_thickness,
"rafter_edge_angle": self.rafter_edge_angle,
} }
if self.generation_method == "HEIGHT": if self.generation_method == "HEIGHT":
kwargs["height"] = self.height kwargs["height"] = self.height
else: else:
kwargs["angle"] = self.angle kwargs["angle"] = self.angle
return kwargs
if not convert_to_project_units:
return kwargs
return tool.Model.convert_data_to_project_units(kwargs, self.non_si_units_props)
def set_props_kwargs_from_ifc_data(self, kwargs):
kwargs = tool.Model.convert_data_to_si_units(kwargs, self.non_si_units_props)
for prop_name in kwargs:
setattr(self, prop_name, kwargs[prop_name])
@@ -28,7 +28,7 @@ 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, pi from math import tan, pi, radians
from mathutils import Vector, Matrix from mathutils import Vector, Matrix
from bpypolyskel import bpypolyskel from bpypolyskel import bpypolyskel
import shapely import shapely
@@ -39,9 +39,6 @@ from pprint import pprint
# 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
NON_SI_ROOF_PROPS = ("is_editing", "roof_type", "roof_added_previously", "generation_method", "angle")
def float_is_zero(f): def float_is_zero(f):
return 0.0001 >= f >= -0.0001 return 0.0001 >= f >= -0.0001
@@ -110,7 +107,7 @@ def is_valid_roof_footprint(bm):
return ({"FINISHED"}, "") return ({"FINISHED"}, "")
def generate_hiped_roof_bmesh(bm, mode="ANGLE", height=1.0, angle=pi / 18, mutate_current_bmesh=True): def generate_hiped_roof_bmesh(bm, mode="ANGLE", height=1.0, roof_thickness = 0.1, angle=pi / 18, rafter_edge_angle = pi / 2, mutate_current_bmesh=True):
"""return bmesh with gable roof geometry """return bmesh with gable roof geometry
`mutate_current_bmesh` is a flag to indicate whether the input bmesh `mutate_current_bmesh` is a flag to indicate whether the input bmesh
@@ -232,10 +229,13 @@ def generate_hiped_roof_bmesh(bm, mode="ANGLE", height=1.0, angle=pi / 18, mutat
verts_to_change = {} verts_to_change = {}
verts_to_rip = [] verts_to_rip = []
bottom_chords_to_remove = [] bottom_chords_to_remove = []
def is_footprint_edge(edge):
return all(float_is_zero(v.co.z - footprint_z) for v in edge.verts)
# find footprint edges # find footprint edges
for edge in bm.edges: for edge in bm.edges:
if all(float_is_zero(v.co.z - footprint_z) for v in edge.verts): if is_footprint_edge(edge):
footprint_edges.append(edge) footprint_edges.append(edge)
footprint_verts.update(edge.verts) footprint_verts.update(edge.verts)
@@ -299,7 +299,7 @@ def generate_hiped_roof_bmesh(bm, mode="ANGLE", height=1.0, angle=pi / 18, mutat
for identical_edge, edge_verts_remaped, defined_angle in process_later: for identical_edge, edge_verts_remaped, defined_angle in process_later:
verts_to_move = find_other_polygon_verts(identical_edge) verts_to_move = find_other_polygon_verts(identical_edge)
for v in verts_to_move[:]: for v in verts_to_move[:]:
verts_to_move.extend(related_verts[v]) verts_to_move.extend(related_verts.get(v, []))
for v in verts_to_move: for v in verts_to_move:
vert_co = verts_to_change.get(v, v.co) vert_co = verts_to_change.get(v, v.co)
@@ -312,10 +312,33 @@ def generate_hiped_roof_bmesh(bm, mode="ANGLE", height=1.0, angle=pi / 18, mutat
bmesh.ops.delete(bm, geom=bottom_chords_to_remove, context="EDGES") bmesh.ops.delete(bm, geom=bottom_chords_to_remove, context="EDGES")
extrusion_geom = bmesh.ops.extrude_face_region(bm, geom=bm.faces)["geom"] # add roof thickness
extruded_verts = bm_sort_out_geom(extrusion_geom)["verts"] extrusion_geom = bm_sort_out_geom(bmesh.ops.extrude_face_region(bm, geom=bm.faces)["geom"])
bmesh.ops.translate(bm, vec=[0.0, 0.0, 0.1], verts=extruded_verts) extruded_edges = extrusion_geom["edges"]
extruded_verts = extrusion_geom["verts"]
rafter_edge_angle = pi/2 - rafter_edge_angle
default_offset_dir = Vector([0, 0, 1]) * roof_thickness
footprint_verts = set()
footprint_edges = []
for edge in extruded_edges:
if is_footprint_edge(edge):
footprint_edges.append(edge)
# TODO: rafter_edge_angle might differ
# if footprint edges are not connected by 90 degrees
shifted_verts = set()
for edge in footprint_edges:
footprint_verts.update(edge.verts)
v0, v1 = edge.verts
edge_dir = (v0.co - v1.co).normalized()
offset_dir = Matrix.Rotation(rafter_edge_angle, 4, edge_dir) @ default_offset_dir
for v in edge.verts:
v.co += offset_dir * (Vector([1, 1, 0]) if v in shifted_verts else 1)
shifted_verts.update(footprint_verts)
verts = [v for v in extruded_verts if v not in footprint_verts]
bmesh.ops.translate(bm, vec=default_offset_dir, verts=verts)
bmesh.ops.recalc_face_normals(bm, faces=bm.faces[:]) bmesh.ops.recalc_face_normals(bm, faces=bm.faces[:])
return bm return bm
@@ -395,8 +418,9 @@ def update_roof_modifier_bmesh(context):
tool.Blender.apply_bmesh(obj.data, bm) tool.Blender.apply_bmesh(obj.data, bm)
return return
height = props.height * si_conversion generate_hiped_roof_bmesh(
generate_hiped_roof_bmesh(bm, props.generation_method, height, props.angle, mutate_current_bmesh=True) bm, props.generation_method, props.height, props.roof_thickness,
props.angle, props.rafter_edge_angle, mutate_current_bmesh=True)
tool.Blender.apply_bmesh(obj.data, bm) tool.Blender.apply_bmesh(obj.data, bm)
@@ -482,10 +506,6 @@ class AddRoof(bpy.types.Operator, tool.Ifc.Operator):
self.report({"ERROR"}, "Object has to be IfcRoof/IfcRoofType type to add a roof.") self.report({"ERROR"}, "Object has to be IfcRoof/IfcRoofType type to add a roof.")
return {"CANCELLED"} return {"CANCELLED"}
# need to make sure all default props will have correct units
if not props.roof_added_previously:
convert_property_group_from_si(props, skip_props=NON_SI_ROOF_PROPS)
# rejecting original roof shape to be safe # rejecting original roof shape to be safe
# taking into account only it's bounding box dimensions # taking into account only it's bounding box dimensions
if obj.dimensions.x == 0 or obj.dimensions.y == 0: if obj.dimensions.x == 0 or obj.dimensions.y == 0:
@@ -500,7 +520,7 @@ class AddRoof(bpy.types.Operator, tool.Ifc.Operator):
max_y = bbox["max_y"] max_y = bbox["max_y"]
min_z = bbox["min_z"] min_z = bbox["min_z"]
roof_data = props.get_general_kwargs() roof_data = props.get_general_kwargs(convert_to_project_units=True)
path_data = { path_data = {
"edges": [[0, 1], [1, 2], [2, 3], [3, 0]], "edges": [[0, 1], [1, 2], [2, 3], [3, 0]],
"verts": [ "verts": [
@@ -529,17 +549,8 @@ class EnableEditingRoof(bpy.types.Operator, tool.Ifc.Operator):
props = obj.BIMRoofProperties props = obj.BIMRoofProperties
element = tool.Ifc.get_entity(obj) element = tool.Ifc.get_entity(obj)
data = json.loads(ifcopenshell.util.element.get_pset(element, "BBIM_Roof", "Data")) data = json.loads(ifcopenshell.util.element.get_pset(element, "BBIM_Roof", "Data"))
data["path_data"] = json.dumps(data["path_data"])
# required since we could load pset from .ifc and BIMRoofProperties won't be set # required since we could load pset from .ifc and BIMRoofProperties won't be set
for prop_name in data: props.set_props_kwargs_from_ifc_data(data)
setattr(props, prop_name, data[prop_name])
# need to make sure all props that weren't used before
# will have correct units
skip_props = NON_SI_ROOF_PROPS + tuple(data.keys())
convert_property_group_from_si(props, skip_props=skip_props)
props.is_editing = 1 props.is_editing = 1
return {"FINISHED"} return {"FINISHED"}
@@ -555,8 +566,7 @@ class CancelEditingRoof(bpy.types.Operator, tool.Ifc.Operator):
data = json.loads(ifcopenshell.util.element.get_pset(element, "BBIM_Roof", "Data")) data = json.loads(ifcopenshell.util.element.get_pset(element, "BBIM_Roof", "Data"))
props = obj.BIMRoofProperties props = obj.BIMRoofProperties
# restore previous settings since editing was canceled # restore previous settings since editing was canceled
for prop_name in data: props.set_props_kwargs_from_ifc_data(data)
setattr(props, prop_name, data[prop_name])
body = ifcopenshell.util.representation.get_representation(element, "Model", "Body", "MODEL_VIEW") body = ifcopenshell.util.representation.get_representation(element, "Model", "Body", "MODEL_VIEW")
blenderbim.core.geometry.switch_representation( blenderbim.core.geometry.switch_representation(
@@ -587,7 +597,7 @@ class FinishEditingRoof(bpy.types.Operator, tool.Ifc.Operator):
RoofData.load() RoofData.load()
path_data = RoofData.data["parameters"]["data_dict"]["path_data"] path_data = RoofData.data["parameters"]["data_dict"]["path_data"]
roof_data = props.get_general_kwargs() roof_data = props.get_general_kwargs(convert_to_project_units=True)
roof_data["path_data"] = path_data roof_data["path_data"] = path_data
props.is_editing = -1 props.is_editing = -1
@@ -629,10 +639,9 @@ class EnableEditingRoofPath(bpy.types.Operator, tool.Ifc.Operator):
main_bm.edges.layers.int.new("BBIM_preview") 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( second_bm = generate_hiped_roof_bmesh(
bm, props.generation_method, height, props.angle, mutate_current_bmesh=False bm,props.generation_method, props.height, props.roof_thickness,
props.angle, props.rafter_edge_angle, mutate_current_bmesh=False
) )
bmesh.ops.translate(second_bm, verts=second_bm.verts, vec=Vector((0, 0, 1))) bmesh.ops.translate(second_bm, verts=second_bm.verts, vec=Vector((0, 0, 1)))
@@ -686,7 +695,7 @@ class FinishEditingRoofPath(bpy.types.Operator, tool.Ifc.Operator):
self.report(op_status, error_message) self.report(op_status, error_message)
return {"CANCELLED"} return {"CANCELLED"}
roof_data = props.get_general_kwargs() roof_data = props.get_general_kwargs(convert_to_project_units=True)
path_data = get_path_data(obj) path_data = get_path_data(obj)
roof_data["path_data"] = path_data roof_data["path_data"] = path_data
ProfileDecorator.uninstall() ProfileDecorator.uninstall()
@@ -614,7 +614,7 @@ class BIM_PT_roof(bpy.types.Panel):
prop_value = round(prop_value, 5) if type(prop_value) is float else prop_value prop_value = round(prop_value, 5) if type(prop_value) is float else prop_value
row = box.row(align=True) row = box.row(align=True)
row.label(text=f"{props.bl_rna.properties[prop].name}") row.label(text=f"{props.bl_rna.properties[prop].name}")
if prop == "angle": if prop in ("angle", "rafter_edge_angle"):
prop_value = round(degrees(prop_value), 2) prop_value = round(degrees(prop_value), 2)
row.label(text=str(prop_value)) row.label(text=str(prop_value))
else: else:
-1
View File
@@ -64,7 +64,6 @@ class Model(blenderbim.core.tool.Model):
if prop_name in non_si_props: if prop_name in non_si_props:
continue continue
prop_value = data[prop_name] prop_value = data[prop_name]
print(prop_name, prop_value)
if isinstance(prop_value, collections.abc.Iterable): if isinstance(prop_value, collections.abc.Iterable):
data[prop_name] = [v * si_conversion for v in prop_value] data[prop_name] = [v * si_conversion for v in prop_value]
else: else: