From f6e72ec06fbc1cab3ade3e817bfc30dc23c1c799 Mon Sep 17 00:00:00 2001 From: Andrej730 Date: Tue, 30 May 2023 18:00:37 +0500 Subject: [PATCH] rafter edge angle and roof thickness #3092 Added option to adjust rafter edge angle and roof thickness Short demonstration - https://imgur.com/a/TsYSs6i --- .../blenderbim/bim/module/model/prop.py | 17 +++- .../blenderbim/bim/module/model/roof.py | 77 +++++++++++-------- .../blenderbim/bim/module/model/ui.py | 2 +- src/blenderbim/blenderbim/tool/model.py | 1 - 4 files changed, 59 insertions(+), 38 deletions(-) diff --git a/src/blenderbim/blenderbim/bim/module/model/prop.py b/src/blenderbim/blenderbim/bim/module/model/prop.py index 66c45333cb..ae25f960da 100644 --- a/src/blenderbim/blenderbim/bim/module/model/prop.py +++ b/src/blenderbim/blenderbim/bim/module/model/prop.py @@ -564,6 +564,7 @@ class BIMRailingProperties(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_generation_methods = ( ("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" ) 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 = { "roof_type": self.roof_type, "generation_method": self.generation_method, + "roof_thickness": self.roof_thickness, + "rafter_edge_angle": self.rafter_edge_angle, } if self.generation_method == "HEIGHT": kwargs["height"] = self.height else: 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]) diff --git a/src/blenderbim/blenderbim/bim/module/model/roof.py b/src/blenderbim/blenderbim/bim/module/model/roof.py index 6b2f2dfdd4..3900fb1c4b 100644 --- a/src/blenderbim/blenderbim/bim/module/model/roof.py +++ b/src/blenderbim/blenderbim/bim/module/model/roof.py @@ -28,7 +28,7 @@ from blenderbim.bim.module.model.data import RoofData, refresh from blenderbim.bim.module.model.decorator import ProfileDecorator import json -from math import tan, pi +from math import tan, pi, radians from mathutils import Vector, Matrix from bpypolyskel import bpypolyskel import shapely @@ -39,9 +39,6 @@ from pprint import pprint # 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): return 0.0001 >= f >= -0.0001 @@ -110,7 +107,7 @@ def is_valid_roof_footprint(bm): 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 `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_rip = [] 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 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_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: verts_to_move = find_other_polygon_verts(identical_edge) 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: 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") - 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) + # add roof thickness + extrusion_geom = bm_sort_out_geom(bmesh.ops.extrude_face_region(bm, geom=bm.faces)["geom"]) + 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[:]) return bm @@ -395,8 +418,9 @@ def update_roof_modifier_bmesh(context): tool.Blender.apply_bmesh(obj.data, bm) return - height = props.height * si_conversion - generate_hiped_roof_bmesh(bm, props.generation_method, height, props.angle, mutate_current_bmesh=True) + generate_hiped_roof_bmesh( + 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) @@ -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.") 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 # taking into account only it's bounding box dimensions 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"] min_z = bbox["min_z"] - roof_data = props.get_general_kwargs() + roof_data = props.get_general_kwargs(convert_to_project_units=True) path_data = { "edges": [[0, 1], [1, 2], [2, 3], [3, 0]], "verts": [ @@ -529,17 +549,8 @@ class EnableEditingRoof(bpy.types.Operator, tool.Ifc.Operator): props = obj.BIMRoofProperties element = tool.Ifc.get_entity(obj) 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 - for prop_name in 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.set_props_kwargs_from_ifc_data(data) props.is_editing = 1 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")) props = obj.BIMRoofProperties # restore previous settings since editing was canceled - for prop_name in data: - setattr(props, prop_name, data[prop_name]) + props.set_props_kwargs_from_ifc_data(data) body = ifcopenshell.util.representation.get_representation(element, "Model", "Body", "MODEL_VIEW") blenderbim.core.geometry.switch_representation( @@ -587,7 +597,7 @@ class FinishEditingRoof(bpy.types.Operator, tool.Ifc.Operator): RoofData.load() 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 props.is_editing = -1 @@ -629,10 +639,9 @@ class EnableEditingRoofPath(bpy.types.Operator, tool.Ifc.Operator): 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 + 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))) @@ -686,7 +695,7 @@ class FinishEditingRoofPath(bpy.types.Operator, tool.Ifc.Operator): self.report(op_status, error_message) 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) roof_data["path_data"] = path_data ProfileDecorator.uninstall() diff --git a/src/blenderbim/blenderbim/bim/module/model/ui.py b/src/blenderbim/blenderbim/bim/module/model/ui.py index 5e3a0239d9..4b28fe7752 100644 --- a/src/blenderbim/blenderbim/bim/module/model/ui.py +++ b/src/blenderbim/blenderbim/bim/module/model/ui.py @@ -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 row = box.row(align=True) 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) row.label(text=str(prop_value)) else: diff --git a/src/blenderbim/blenderbim/tool/model.py b/src/blenderbim/blenderbim/tool/model.py index 7c27eafc74..c9d8249a4d 100644 --- a/src/blenderbim/blenderbim/tool/model.py +++ b/src/blenderbim/blenderbim/tool/model.py @@ -64,7 +64,6 @@ class Model(blenderbim.core.tool.Model): if prop_name in non_si_props: continue prop_value = data[prop_name] - print(prop_name, prop_value) if isinstance(prop_value, collections.abc.Iterable): data[prop_name] = [v * si_conversion for v in prop_value] else: