diff --git a/src/blenderbim/blenderbim/bim/module/model/__init__.py b/src/blenderbim/blenderbim/bim/module/model/__init__.py index b7d9470cb2..5d4937cf72 100644 --- a/src/blenderbim/blenderbim/bim/module/model/__init__.py +++ b/src/blenderbim/blenderbim/bim/module/model/__init__.py @@ -37,6 +37,7 @@ from . import ( sverchok_modifier, door, railing, + roof, ) classes = ( @@ -101,6 +102,7 @@ classes = ( prop.BIMWindowProperties, prop.BIMDoorProperties, prop.BIMRailingProperties, + prop.BIMRoofProperties, ui.BIM_PT_authoring, ui.BIM_PT_array, ui.BIM_PT_stair, @@ -108,6 +110,7 @@ classes = ( ui.BIM_PT_window, ui.BIM_PT_door, ui.BIM_PT_railing, + ui.BIM_PT_roof, ui.LaunchTypeManager, ui.HelpConstrTypes, ui.BIM_MT_model, @@ -150,6 +153,15 @@ classes = ( railing.FinishEditingRailingPath, railing.EnableEditingRailingPath, railing.RemoveRailing, + roof.BIM_OT_add_roof, + roof.AddRoof, + roof.CancelEditingRoof, + roof.FinishEditingRoof, + roof.EnableEditingRoof, + roof.CancelEditingRoofPath, + roof.FinishEditingRoofPath, + roof.EnableEditingRoofPath, + roof.RemoveRoof, ) addon_keymaps = [] @@ -165,11 +177,13 @@ def register(): bpy.types.Object.BIMWindowProperties = bpy.props.PointerProperty(type=prop.BIMWindowProperties) bpy.types.Object.BIMDoorProperties = bpy.props.PointerProperty(type=prop.BIMDoorProperties) bpy.types.Object.BIMRailingProperties = bpy.props.PointerProperty(type=prop.BIMRailingProperties) + bpy.types.Object.BIMRoofProperties = bpy.props.PointerProperty(type=prop.BIMRoofProperties) bpy.types.VIEW3D_MT_mesh_add.append(grid.add_object_button) bpy.types.VIEW3D_MT_mesh_add.append(stair.add_object_button) bpy.types.VIEW3D_MT_mesh_add.append(window.add_object_button) bpy.types.VIEW3D_MT_mesh_add.append(door.add_object_button) bpy.types.VIEW3D_MT_mesh_add.append(railing.add_object_button) + bpy.types.VIEW3D_MT_mesh_add.append(roof.add_object_button) bpy.types.VIEW3D_MT_add.append(ui.add_menu) bpy.app.handlers.load_post.append(handler.load_post) wm = bpy.context.window_manager @@ -190,12 +204,14 @@ def unregister(): del bpy.types.Object.BIMWindowProperties del bpy.types.Object.BIMDoorProperties del bpy.types.Object.BIMRailingProperties + del bpy.types.Object.BIMRoofProperties bpy.app.handlers.load_post.remove(handler.load_post) bpy.types.VIEW3D_MT_mesh_add.remove(grid.add_object_button) bpy.types.VIEW3D_MT_mesh_add.remove(stair.add_object_button) bpy.types.VIEW3D_MT_mesh_add.remove(window.add_object_button) bpy.types.VIEW3D_MT_mesh_add.remove(door.add_object_button) bpy.types.VIEW3D_MT_mesh_add.remove(railing.add_object_button) + bpy.types.VIEW3D_MT_mesh_add.remove(roof.add_object_button) bpy.types.VIEW3D_MT_add.remove(ui.add_menu) wm = bpy.context.window_manager kc = wm.keyconfigs.addon diff --git a/src/blenderbim/blenderbim/bim/module/model/data.py b/src/blenderbim/blenderbim/bim/module/model/data.py index c0c9f4c9e5..e9e7747649 100644 --- a/src/blenderbim/blenderbim/bim/module/model/data.py +++ b/src/blenderbim/blenderbim/bim/module/model/data.py @@ -36,6 +36,7 @@ def refresh(): WindowData.is_loaded = False DoorData.is_loaded = False RailingData.is_loaded = False + RoofData.is_loaded = False class AuthoringData: @@ -483,3 +484,23 @@ class RailingData: if parameters: parameters["data_dict"] = json.loads(parameters.get("Data", "[]") or "[]") return parameters + + +class RoofData: + data = {} + is_loaded = False + + @classmethod + def load(cls): + cls.is_loaded = True + cls.data = {"parameters": cls.parameters()} + + @classmethod + def parameters(cls): + element = tool.Ifc.get_entity(bpy.context.active_object) + if element: + psets = ifcopenshell.util.element.get_psets(element) + parameters = psets.get("BBIM_Roof", None) + if parameters: + parameters["data_dict"] = json.loads(parameters.get("Data", "[]") or "[]") + return parameters diff --git a/src/blenderbim/blenderbim/bim/module/model/prop.py b/src/blenderbim/blenderbim/bim/module/model/prop.py index 28c4f66b68..43de76c9c6 100644 --- a/src/blenderbim/blenderbim/bim/module/model/prop.py +++ b/src/blenderbim/blenderbim/bim/module/model/prop.py @@ -237,6 +237,7 @@ class BIMModelProperties(PropertyGroup): ("DOOR", "Door", ""), ("STAIR", "Stair", ""), ("RAILING", "Railing", ""), + ("ROOF", "Roof", ""), ), name="Type Template", default="MESH", @@ -564,3 +565,33 @@ class BIMRailingProperties(PropertyGroup): "thickness": self.thickness, "spacing": self.spacing, } + + +class BIMRoofProperties(PropertyGroup): + roof_types = (("HIP_ROOF", "HIP_ROOF", ""),) + roof_generation_methods = ( + ("HEIGHT", "HEIGHT", ""), + ("ANGLE", "ANGLE", ""), + ) + + roof_added_previously: bpy.props.BoolProperty(default=False) + 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") + generation_method: bpy.props.EnumProperty( + name="Roof Generation Method", items=roof_generation_methods, default="HEIGHT" + ) + height: bpy.props.FloatProperty(name="Height", default=1.0) + angle: bpy.props.FloatProperty(name="Slope Angle", default=10, description="In degrees") + + def get_general_kwargs(self): + kwargs = { + "roof_type": self.roof_type, + "generation_method": self.generation_method, + } + if self.generation_method == "HEIGHT": + kwargs["height"] = self.height + else: + kwargs["angle"] = self.angle + return kwargs diff --git a/src/blenderbim/blenderbim/bim/module/model/railing.py b/src/blenderbim/blenderbim/bim/module/model/railing.py index 1e64f5ba5a..881a4b2828 100644 --- a/src/blenderbim/blenderbim/bim/module/model/railing.py +++ b/src/blenderbim/blenderbim/bim/module/model/railing.py @@ -243,7 +243,7 @@ class AddRailing(bpy.types.Operator, tool.Ifc.Operator): # need to make sure all default props will have correct units if not props.railing_added_previously: - skip_props = ("is_editing", "railing_type", "railing_added_previously", "path_data") + skip_props = ("is_editing", "railing_type", "railing_added_previously") convert_property_group_from_si(props, skip_props=skip_props) railing_data = props.get_general_kwargs() @@ -260,6 +260,7 @@ class AddRailing(bpy.types.Operator, tool.Ifc.Operator): railing_data["path_data"] = path_data update_bbim_railing_pset(element, railing_data) + refresh() update_railing_modifier_ifc_data(context) update_railing_modifier_bmesh(context) return {"FINISHED"} diff --git a/src/blenderbim/blenderbim/bim/module/model/roof.py b/src/blenderbim/blenderbim/bim/module/model/roof.py index d003c3f9c5..543bf3c41b 100644 --- a/src/blenderbim/blenderbim/bim/module/model/roof.py +++ b/src/blenderbim/blenderbim/bim/module/model/roof.py @@ -1,5 +1,5 @@ # BlenderBIM Add-on - OpenBIM Blender Add-on -# Copyright (C) 2023 Dion Moult +# Copyright (C) 2023 Dion Moult , @Andrej730 # # This file is part of BlenderBIM Add-on. # @@ -16,14 +16,26 @@ # You should have received a copy of the GNU General Public License # along with BlenderBIM Add-on. If not, see . - import bpy +from bpy.types import Operator import bmesh -import shapely + +import ifcopenshell +import blenderbim import blenderbim.tool as tool +from blenderbim.bim.helper import convert_property_group_from_si +from blenderbim.bim.module.model.door import bm_sort_out_geom +from blenderbim.bim.module.model.data import RoofData, refresh + +import json from math import tan, radians from mathutils import Vector, Matrix from bpypolyskel import bpypolyskel +import shapely + +# 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 class GenerateHippedRoof(bpy.types.Operator, tool.Ifc.Operator): @@ -37,75 +49,393 @@ class GenerateHippedRoof(bpy.types.Operator, tool.Ifc.Operator): def _execute(self, context): obj = bpy.context.active_object if not obj: - return + self.report({"ERROR"}, "Need to select some object first.") + return {"CANCELLED"} - boundary_lines = [] + bpy.ops.mesh.dissolve_limited() + generate_hipped_roof(obj, self.mode, self.height, self.angle) + return {"FINISHED"} - 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) +def generate_hipped_roof(obj, mode="ANGLE", height=1.0, angle=10): + boundary_lines = [] - 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 self.mode == "HEIGHT": - height = self.height - angle = 0.0 - else: - angle = tan(radians(round(self.angle, 4))) - height = 0.0 - - faces = bpypolyskel.polygonize( - verts, start_exterior_index, total_exterior_verts, inner_loops, height, angle, faces, unit_vectors + 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]) ) - edges = [] - mesh = bpy.data.meshes.new(name="Building_with_Roof") - mesh.from_pydata(verts, edges, faces) + unioned_boundaries = shapely.union_all(shapely.GeometryCollection(boundary_lines)) + closed_polygons = shapely.polygonize(unioned_boundaries.geoms) - bm = bmesh.new() - bm.from_mesh(mesh) + 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 - extrusion = bmesh.ops.extrude_face_region(bm, geom=bm.faces) - extruded_verts = [g for g in extrusion["geom"] if isinstance(g, bmesh.types.BMVert)] - bmesh.ops.translate(bm, vec=[0.0, 0.0, 0.1], verts=extruded_verts) + roof_polygon = shapely.force_3d(roof_polygon) - bm.to_mesh(mesh) - bm.free() + if not shapely.is_ccw(roof_polygon): + roof_polygon = roof_polygon.reverse() - obj.data = mesh + # 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: + angle = tan(radians(round(angle, 4))) + height = 0.0 + + faces = bpypolyskel.polygonize( + verts, start_exterior_index, total_exterior_verts, inner_loops, height, angle, faces, unit_vectors + ) + + edges = [] + + 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] + + 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) + + +def bm_get_indices(sequence): + return [i.index for i in sequence] + + +def update_roof_modifier_ifc_data(context): + obj = context.active_object + props = obj.BIMRoofProperties + element = tool.Ifc.get_entity(obj) + ifc_file = tool.Ifc.get() + + # type attributes + element.PredefinedType = props.roof_type + # occurences attributes + # occurences = tool.Ifc.get_all_element_occurences(element) + + +def update_bbim_roof_pset(element, roof_data): + pset = tool.Pset.get_element_pset(element, "BBIM_Roof") + if not pset: + pset = ifcopenshell.api.run("pset.add_pset", tool.Ifc.get(), product=element, name="BBIM_Roof") + roof_data = json.dumps(roof_data, default=list) + ifcopenshell.api.run("pset.edit_pset", tool.Ifc.get(), pset=pset, properties={"Data": roof_data}) + + +def update_roof_modifier_bmesh(context): + obj = context.object + props = obj.BIMRoofProperties + + if not RoofData.is_loaded: + RoofData.load() + path_data = RoofData.data["parameters"]["data_dict"]["path_data"] + + 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) + + # 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"]] + + 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"}, 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) + + +def get_path_data(obj): + si_conversion = ifcopenshell.util.unit.calculate_unit_scale(tool.Ifc.get()) + + if obj.mode == "EDIT": + # otherwise mesh may not contain all changes + # added in edit mode + obj.update_from_editmode() + + bm = tool.Blender.get_bmesh_for_mesh(obj.data) + + # 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") + + 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 not path_data["edges"] or not path_data["verts"]: + return None + return path_data + + +class BIM_OT_add_roof(bpy.types.Operator, tool.Ifc.Operator): + bl_idname = "mesh.add_roof" + bl_label = "Roof" + bl_options = {"REGISTER", "UNDO"} + + def _execute(self, context): + ifc_file = tool.Ifc.get() + if not ifc_file: + self.report({"ERROR"}, "You need to start IFC project first to create a roof.") + return {"CANCELLED"} + + if context.object is not None: + spawn_location = context.object.location.copy() + context.object.select_set(False) + else: + spawn_location = bpy.context.scene.cursor.location.copy() + + mesh = bpy.data.meshes.new("IfcRoof") + obj = bpy.data.objects.new("IfcRoof", mesh) + obj.location = spawn_location + body_context = ifcopenshell.util.representation.get_context(ifc_file, "Model", "Body", "MODEL_VIEW") + blenderbim.core.root.assign_class( + tool.Ifc, + tool.Collector, + tool.Root, + obj=obj, + ifc_class="IfcRoof", + should_add_representation=True, + context=body_context, + ) + bpy.ops.object.select_all(action="DESELECT") + bpy.context.view_layer.objects.active = None + bpy.context.view_layer.objects.active = obj + obj.select_set(True) + bpy.ops.bim.add_roof() + return {"FINISHED"} + + +# UI operators +class AddRoof(bpy.types.Operator, tool.Ifc.Operator): + bl_idname = "bim.add_roof" + bl_label = "Add Roof" + bl_options = {"REGISTER"} + + def _execute(self, context): + obj = context.active_object + element = tool.Ifc.get_entity(obj) + props = obj.BIMRoofProperties + si_conversion = ifcopenshell.util.unit.calculate_unit_scale(tool.Ifc.get()) + + if element.is_a() not in ("IfcRoof", "IfcRoofType"): + 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: + skip_props = ("is_editing", "roof_type", "roof_added_previously", "generation_method") + convert_property_group_from_si(props, skip_props=skip_props) + + roof_data = props.get_general_kwargs() + path_data = get_path_data(obj) + if not path_data: + path_data = { + "edges": [[0, 1], [1, 2], [2, 3], [3, 0]], + "verts": [ + Vector([-5.0, -5.0, 0.0]) / si_conversion, + Vector([-5.0, 5.0, 0.0]) / si_conversion, + Vector([5.0, 5.0, 0.0]) / si_conversion, + Vector([5.0, -5.0, 0.0]) / si_conversion, + ], + } + roof_data["path_data"] = path_data + + update_bbim_roof_pset(element, roof_data) + update_roof_modifier_ifc_data(context) + refresh() + update_roof_modifier_bmesh(context) + return {"FINISHED"} + + +class EnableEditingRoof(bpy.types.Operator, tool.Ifc.Operator): + bl_idname = "bim.enable_editing_roof" + bl_label = "Enable Editing Roof" + bl_options = {"REGISTER"} + + def _execute(self, context): + obj = context.active_object + 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 = ("is_editing", "roof_type", "roof_added_previously", "generation_method") + skip_props += tuple(data.keys()) + convert_property_group_from_si(props, skip_props=skip_props) + + props.is_editing = 1 + return {"FINISHED"} + + +class CancelEditingRoof(bpy.types.Operator, tool.Ifc.Operator): + bl_idname = "bim.cancel_editing_roof" + bl_label = "Cancel editing Roof" + bl_options = {"REGISTER"} + + def _execute(self, context): + obj = context.active_object + element = tool.Ifc.get_entity(obj) + 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]) + + body = ifcopenshell.util.representation.get_representation(element, "Model", "Body", "MODEL_VIEW") + blenderbim.core.geometry.switch_representation( + tool.Ifc, + tool.Geometry, + obj=obj, + representation=body, + should_reload=True, + is_global=True, + should_sync_changes_first=False, + ) + + props.is_editing = -1 + return {"FINISHED"} + + +class FinishEditingRoof(bpy.types.Operator, tool.Ifc.Operator): + bl_idname = "bim.finish_editing_roof" + bl_label = "Finish editing roof" + bl_options = {"REGISTER"} + + def _execute(self, context): + obj = context.active_object + element = tool.Ifc.get_entity(obj) + props = obj.BIMRoofProperties + + if not RoofData.is_loaded: + RoofData.load() + path_data = RoofData.data["parameters"]["data_dict"]["path_data"] + + roof_data = props.get_general_kwargs() + roof_data["path_data"] = path_data + props.is_editing = -1 + + update_bbim_roof_pset(element, roof_data) + update_roof_modifier_ifc_data(context) + return {"FINISHED"} + + +class EnableEditingRoofPath(bpy.types.Operator, tool.Ifc.Operator): + bl_idname = "bim.enable_editing_roof_path" + bl_label = "Enable Editing Roof Path" + bl_options = {"REGISTER"} + + def _execute(self, context): + obj = context.active_object + props = obj.BIMRoofProperties + element = tool.Ifc.get_entity(obj) + + props.is_editing_path = True + update_roof_modifier_bmesh(context) + return {"FINISHED"} + + +class CancelEditingRoofPath(bpy.types.Operator, tool.Ifc.Operator): + bl_idname = "bim.cancel_editing_roof_path" + bl_label = "Cancel Editing Roof Path" + bl_options = {"REGISTER"} + + def _execute(self, context): + obj = context.active_object + element = tool.Ifc.get_entity(obj) + props = obj.BIMRoofProperties + update_roof_modifier_bmesh(context) + props.is_editing_path = False + return {"FINISHED"} + + +class FinishEditingRoofPath(bpy.types.Operator, tool.Ifc.Operator): + bl_idname = "bim.finish_editing_roof_path" + bl_label = "Finish Editing Roof Path" + bl_options = {"REGISTER"} + + def _execute(self, context): + obj = context.active_object + element = tool.Ifc.get_entity(obj) + props = obj.BIMRoofProperties + + roof_data = props.get_general_kwargs() + path_data = get_path_data(obj) + roof_data["path_data"] = path_data + props.is_editing_path = False + + update_bbim_roof_pset(element, roof_data) + refresh() # need RoofData to be updated before update_roof_modifier_bmesh + update_roof_modifier_bmesh(context) + return {"FINISHED"} + + +class RemoveRoof(bpy.types.Operator, tool.Ifc.Operator): + bl_idname = "bim.remove_roof" + bl_label = "Remove Roof" + bl_options = {"REGISTER"} + + def _execute(self, context): + obj = context.active_object + props = obj.BIMRoofProperties + element = tool.Ifc.get_entity(obj) + obj.BIMRoofProperties.is_editing = -1 + + pset = tool.Pset.get_element_pset(element, "BBIM_Roof") + ifcopenshell.api.run("pset.remove_pset", tool.Ifc.get(), pset=pset) + props.roof_added_previously = True + return {"FINISHED"} + + +def add_object_button(self, context): + self.layout.operator(BIM_OT_add_roof.bl_idname, icon="PLUGIN") diff --git a/src/blenderbim/blenderbim/bim/module/model/ui.py b/src/blenderbim/blenderbim/bim/module/model/ui.py index 3e8d9e8ce5..110b88b26f 100644 --- a/src/blenderbim/blenderbim/bim/module/model/ui.py +++ b/src/blenderbim/blenderbim/bim/module/model/ui.py @@ -27,12 +27,14 @@ from blenderbim.bim.module.model.data import ( WindowData, DoorData, RailingData, + RoofData, ) from blenderbim.bim.module.model.prop import get_ifc_class from blenderbim.bim.module.model.stair import update_stair_modifier from blenderbim.bim.module.model.window import update_window_modifier_bmesh from blenderbim.bim.module.model.door import update_door_modifier_bmesh from blenderbim.bim.module.model.railing import update_railing_modifier_bmesh +from blenderbim.bim.module.model.roof import update_roof_modifier_bmesh from blenderbim.bim.helper import prop_with_search @@ -597,6 +599,65 @@ class BIM_PT_railing(bpy.types.Panel): row.operator("bim.add_railing", icon="ADD", text="") +class BIM_PT_roof(bpy.types.Panel): + bl_label = "IFC Roof" + bl_idname = "BIM_PT_roof" + bl_space_type = "PROPERTIES" + bl_region_type = "WINDOW" + bl_context = "modifier" + bl_options = {"DEFAULT_CLOSED"} + + @classmethod + def poll(cls, context): + # always display modifier if it's IFC object + return tool.Ifc.get() and tool.Ifc.get_entity(context.active_object) + + def draw(self, context): + if not RoofData.is_loaded: + RoofData.load() + + props = context.active_object.BIMRoofProperties + + if RoofData.data["parameters"]: + row = self.layout.row(align=True) + row.label(text="Roof parameters", icon="OUTLINER_OB_LATTICE") + + roof_data = RoofData.data["parameters"]["data_dict"] + + if props.is_editing != -1: + row = self.layout.row(align=True) + row.operator("bim.finish_editing_roof", icon="CHECKMARK", text="Finish editing") + row.operator("bim.cancel_editing_roof", icon="CANCEL", text="") + + general_props = props.get_general_kwargs() + for prop in general_props: + self.layout.prop(props, prop) + + update_roof_modifier_bmesh(context) + + elif props.is_editing_path: + row.operator("bim.finish_editing_roof_path", icon="CHECKMARK", text="Finish editing path") + row.operator("bim.cancel_editing_roof_path", icon="CANCEL", text="") + + else: + row.operator("bim.enable_editing_roof", icon="GREASEPENCIL", text="") + row.operator("bim.enable_editing_roof_path", icon="ANIM", text="") + row.operator("bim.remove_roof", icon="X", text="") + + box = self.layout.box() + general_props = props.get_general_kwargs() + for prop in general_props: + prop_value = roof_data[prop] + 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}") + row.label(text=str(prop_value)) + else: + row = self.layout.row() + row.label(text="No Roof Found") + row.operator("bim.add_roof", icon="ADD", text="") + + class BIM_MT_model(Menu): bl_idname = "BIM_MT_model" bl_label = "IFC Objects" diff --git a/src/blenderbim/blenderbim/bim/module/type/operator.py b/src/blenderbim/blenderbim/bim/module/type/operator.py index cc33831444..60fff7464c 100644 --- a/src/blenderbim/blenderbim/bim/module/type/operator.py +++ b/src/blenderbim/blenderbim/bim/module/type/operator.py @@ -279,6 +279,7 @@ class AddType(bpy.types.Operator, tool.Ifc.Operator): context=body, ifc_representation_class=None, ) + elif template == "WINDOW": mesh = bpy.data.meshes.new("IfcWindow") obj = bpy.data.objects.new("TYPEX", mesh) @@ -353,6 +354,25 @@ class AddType(bpy.types.Operator, tool.Ifc.Operator): obj.select_set(True) bpy.ops.bim.add_railing() + elif template == "ROOF": + mesh = bpy.data.meshes.new("IfcRoof") + obj = bpy.data.objects.new("TYPEX", mesh) + element = blenderbim.core.root.assign_class( + tool.Ifc, + tool.Collector, + tool.Root, + obj=obj, + predefined_type=predefined_type, + ifc_class="IfcRoofType", + should_add_representation=True, + context=body, + ) + bpy.ops.object.select_all(action="DESELECT") + bpy.context.view_layer.objects.active = None + bpy.context.view_layer.objects.active = obj + obj.select_set(True) + bpy.ops.bim.add_roof() + bpy.ops.bim.load_type_thumbnails(ifc_class=ifc_class) return {"FINISHED"}