diff --git a/src/blenderbim/blenderbim/bim/module/model/__init__.py b/src/blenderbim/blenderbim/bim/module/model/__init__.py index 14b7494a35..2245e92761 100644 --- a/src/blenderbim/blenderbim/bim/module/model/__init__.py +++ b/src/blenderbim/blenderbim/bim/module/model/__init__.py @@ -36,6 +36,7 @@ from . import ( profile, sverchok_modifier, door, + railing ) classes = ( @@ -99,12 +100,14 @@ classes = ( prop.BIMSverchokProperties, prop.BIMWindowProperties, prop.BIMDoorProperties, + prop.BIMRailingProperties, ui.BIM_PT_authoring, ui.BIM_PT_array, ui.BIM_PT_stair, ui.BIM_PT_sverchok, ui.BIM_PT_window, ui.BIM_PT_door, + ui.BIM_PT_railing, ui.LaunchTypeManager, ui.HelpConstrTypes, ui.BIM_MT_model, @@ -138,6 +141,15 @@ classes = ( door.FinishEditingDoor, door.EnableEditingDoor, door.RemoveDoor, + railing.BIM_OT_add_railing, + railing.AddRailing, + railing.CancelEditingRailing, + railing.FinishEditingRailing, + railing.EnableEditingRailing, + railing.CancelEditingRailingPath, + railing.FinishEditingRailingPath, + railing.EnableEditingRailingPath, + railing.RemoveRailing, ) addon_keymaps = [] @@ -152,10 +164,12 @@ def register(): bpy.types.Object.BIMSverchokProperties = bpy.props.PointerProperty(type=prop.BIMSverchokProperties) 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.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_add.append(ui.add_menu) bpy.app.handlers.load_post.append(handler.load_post) wm = bpy.context.window_manager @@ -175,11 +189,13 @@ def unregister(): del bpy.types.Object.BIMSverchokProperties del bpy.types.Object.BIMWindowProperties del bpy.types.Object.BIMDoorProperties + del bpy.types.Object.BIMRailingProperties 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_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 461255dbd2..9725901df6 100644 --- a/src/blenderbim/blenderbim/bim/module/model/data.py +++ b/src/blenderbim/blenderbim/bim/module/model/data.py @@ -35,6 +35,7 @@ def refresh(): SverchokData.is_loaded = False WindowData.is_loaded = False DoorData.is_loaded = False + RailingData.is_loaded = False class AuthoringData: @@ -462,3 +463,23 @@ class DoorData: if parameters: parameters["data_dict"] = json.loads(parameters.get("Data", "[]") or "[]") return parameters + + +class RailingData: + 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_Railing", 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 fa47b68287..09eef289ca 100644 --- a/src/blenderbim/blenderbim/bim/module/model/prop.py +++ b/src/blenderbim/blenderbim/bim/module/model/prop.py @@ -542,3 +542,26 @@ class BIMDoorProperties(PropertyGroup): kwargs["frame_depth"] = self.frame_depth return kwargs + + +class BIMRailingProperties(PropertyGroup): + railing_types = ( + ("FRAMELESS_PANEL", "FRAMELESS_PANEL", ""), + ) + + railing_added_previously: bpy.props.BoolProperty(default=False) + is_editing: bpy.props.IntProperty(default=-1) + is_editing_path: bpy.props.BoolProperty(default=False) + + railing_type: bpy.props.EnumProperty(name="Railing Type", items=railing_types, default="FRAMELESS_PANEL") + height: bpy.props.FloatProperty(name="Height", default=1.0) + thickness: bpy.props.FloatProperty(name="Thickness", default=0.050) + spacing: bpy.props.FloatProperty(name="Spacing", default=0.050) + + def get_general_kwargs(self): + return { + "railing_type": self.railing_type, + "height": self.height, + "thickness": self.thickness, + "spacing": self.spacing, + } diff --git a/src/blenderbim/blenderbim/bim/module/model/railing.py b/src/blenderbim/blenderbim/bim/module/model/railing.py new file mode 100644 index 0000000000..0bb49ce140 --- /dev/null +++ b/src/blenderbim/blenderbim/bim/module/model/railing.py @@ -0,0 +1,405 @@ +# BlenderBIM Add-on - OpenBIM Blender Add-on +# Copyright (C) 2023 @Andrej730 +# +# This file is part of BlenderBIM Add-on. +# +# BlenderBIM Add-on is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# BlenderBIM Add-on is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# 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 ifcopenshell +from ifcopenshell.util.shape_builder import V +import blenderbim +import blenderbim.tool as tool +import blenderbim.core.geometry as core +from blenderbim.bim.helper import convert_property_group_from_si +from blenderbim.bim.ifc import IfcStore +from blenderbim.bim.module.model.door import bm_sort_out_geom +from blenderbim.bim.module.model.data import RailingData, refresh + +from mathutils import Vector, Matrix +from pprint import pprint +import json + +# reference: +# https://ifc43-docs.standards.buildingsmart.org/IFC/RELEASE/IFC4x3/HTML/lexical/IfcRailing.htm +# https://ifc43-docs.standards.buildingsmart.org/IFC/RELEASE/IFC4x3/HTML/lexical/IfcRailingType.htm + +def bm_split_edge_at_offset(edge, offset): + v0, v1 = edge.verts + + offset = offset / 2 + edge_len = (v0.co-v1.co).xy.length + + split_output_0 = bmesh.utils.edge_split(edge, v0, offset/edge_len) + split_output_1 = bmesh.utils.edge_split(edge, v1, offset/(edge_len-offset)) + new_geometry = bm_sort_out_geom(split_output_0 + split_output_1) + return new_geometry + +def update_railing_modifier_ifc_data(context): + obj = context.active_object + props = obj.BIMRailingProperties + element = tool.Ifc.get_entity(obj) + ifc_file = tool.Ifc.get() + + # type attributes + element.PredefinedType = 'USERDEFINED' + # occurences attributes + occurences = tool.Ifc.get_all_element_occurences(element) + for occurence in occurences: + occurence.ObjectType = props.railing_type + + # update pset + pset_common = tool.Pset.get_element_pset(element, "Pset_RailingCommon") + if not pset_common: + pset_common = ifcopenshell.api.run("pset.add_pset", ifc_file, product=element, name="Pset_RailingCommon") + + ifcopenshell.api.run( + "pset.edit_pset", + ifc_file, + pset=pset_common, + properties={ + "Height": props.height, + }, + ) + +def update_bbim_railing_pset(element, railing_data): + pset = tool.Pset.get_element_pset(element, "BBIM_Railing") + if not pset: + pset = ifcopenshell.api.run("pset.add_pset", tool.Ifc.get(), product=element, name="BBIM_Railing") + railing_data = json.dumps(railing_data, default=list) + ifcopenshell.api.run("pset.edit_pset", tool.Ifc.get(), pset=pset, properties={"Data": railing_data}) + +def update_railing_modifier_bmesh(context): + obj = context.object + props = obj.BIMRailingProperties + + if not RailingData.is_loaded: + RailingData.load() + path_data = RailingData.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 railing path + bm = tool.Blender.get_bmesh_for_mesh(obj.data, clean=True) + + # generating railing path + bm.verts.index_update() + bm.edges.index_update() + 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']] + bm.verts.index_update() + bm.edges.index_update() + + if props.is_editing_path: + tool.Blender.apply_bmesh(obj.data, bm) + return + + # generating the entire railing + height = props.height * si_conversion + thickness = props.thickness * si_conversion + spacing = props.spacing * si_conversion + + # spacing + # split each edge in 3 segments by 0.5 * spacing by x-y plane + main_edges = bm.edges[:] + for main_edge in main_edges: + bm_split_edge_at_offset(main_edge, spacing) + + # thickness + # keep track of translated verts so we won't translate the same + # vert twice + edge_dissolving_verts = [] + for main_edge in main_edges: + v0, v1 = main_edge.verts + edge_dissolving_verts.extend([v0, v1]) + + edge_dir = ( (v1.co-v0.co) * V(1,1,0) ).normalized() + ortho_vector = edge_dir.cross( V(0,0,1) ) + + extruded_geom = bmesh.ops.extrude_edge_only(bm, edges=[main_edge])['geom'] + extruded_verts = bm_sort_out_geom(extruded_geom)['verts'] + bmesh.ops.translate(bm, vec=ortho_vector * (-thickness/2), verts=extruded_verts) + + extruded_geom = bmesh.ops.extrude_edge_only(bm, edges=[main_edge])['geom'] + extruded_verts = bm_sort_out_geom(extruded_geom)['verts'] + bmesh.ops.translate(bm, vec=ortho_vector * (thickness/2), verts=extruded_verts) + + # dissolve middle edge + bmesh.ops.dissolve_edges(bm, edges=[main_edge]) + + # height + extruded_geom = bmesh.ops.extrude_face_region(bm, geom=bm.faces)['geom'] + extruded_verts = bm_sort_out_geom(extruded_geom)['verts'] + extruded_edges = bm_sort_out_geom(extruded_geom)['edges'] + extrusion_vector = Vector((0, 0, 1)) * height + bmesh.ops.translate(bm, vec=extrusion_vector, verts=extruded_verts) + + # dissolve middle edges + edges_to_dissolve = [] + verts_to_dissolve = [] + for v in edge_dissolving_verts: + for e in v.link_edges: + other_vert = e.other_vert(v) + if other_vert in extruded_verts: + edges_to_dissolve.append(e) + verts_to_dissolve.append(other_vert) + + bmesh.ops.dissolve_edges(bm, edges=edges_to_dissolve) + bmesh.ops.dissolve_verts(bm, verts=verts_to_dissolve) + + # to remove unnecessary verts in 0 spacing case + bmesh.ops.remove_doubles(bm, verts=bm.verts, dist=0.0001) + + tool.Blender.apply_bmesh(obj.data, bm) + +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() + + mesh = obj.data + path_data = dict() + path_data['edges'] = [e.vertices[:] for e in mesh.edges] + path_data['verts'] = [v.co / si_conversion for v in mesh.vertices] + + if not path_data['edges'] or not path_data['verts']: + return None + return path_data + +class BIM_OT_add_railing(Operator): + bl_idname = "mesh.add_railing" + bl_label = "Railing" + 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 railing.") + 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("IfcRailing") + obj = bpy.data.objects.new("IfcRailing", mesh) + obj.location = spawn_location + body_context = ifcopenshell.util.representation.get_context(ifc_file, "Model", "Body", "MODEL_VIEW") + element = blenderbim.core.root.assign_class( + tool.Ifc, tool.Collector, tool.Root, obj=obj, ifc_class="IfcRailing", should_add_representation=False + ) + element.PredefinedType = "USERDEFINED" + + bm = bmesh.new() + bm.verts.index_update() + bm.edges.index_update() + + new_verts = [bm.verts.new(v) for v in [V(-1, 0, 0), V(0, 0, 0), V(1, 0, 0)]] + new_edges = [bm.edges.new( (new_verts[e[0]], new_verts[e[1]]) ) for e in [(0,1), (1,2)]] + bm.verts.index_update() + bm.edges.index_update() + tool.Blender.apply_bmesh(mesh, bm) + + 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_railing() + return {"FINISHED"} + + +# UI operators +class AddRailing(bpy.types.Operator, tool.Ifc.Operator): + bl_idname = "bim.add_railing" + bl_label = "Add Railing" + bl_options = {"REGISTER"} + + def _execute(self, context): + obj = context.active_object + element = tool.Ifc.get_entity(obj) + props = obj.BIMRailingProperties + + if element.is_a() not in ("IfcRailing", "IfcRailingType"): + self.report({"ERROR"}, "Object has to be IfcRailing/IfcRailingType type to add a railing.") + return {"CANCELLED"} + + # 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") + convert_property_group_from_si(props, skip_props=skip_props) + + railing_data = props.get_general_kwargs() + railing_data['path_data'] = get_path_data(obj) + + update_bbim_railing_pset(element, railing_data) + update_railing_modifier_ifc_data(context) + update_railing_modifier_bmesh(context) + return {"FINISHED"} + + +class EnableEditingRailing(bpy.types.Operator, tool.Ifc.Operator): + bl_idname = "bim.enable_editing_railing" + bl_label = "Enable Editing Railing" + bl_options = {"REGISTER"} + + def _execute(self, context): + obj = context.active_object + props = obj.BIMRailingProperties + element = tool.Ifc.get_entity(obj) + data = json.loads(ifcopenshell.util.element.get_pset(element, "BBIM_Railing", "Data")) + data['path_data'] = json.dumps(data['path_data']) + + # required since we could load pset from .ifc and BIMRailingProperties 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", "railing_type", "railing_added_previously") + skip_props += tuple(data.keys()) + convert_property_group_from_si(props, skip_props=skip_props) + + props.is_editing = 1 + return {"FINISHED"} + + +class CancelEditingRailing(bpy.types.Operator, tool.Ifc.Operator): + bl_idname = "bim.cancel_editing_railing" + bl_label = "Cancel editing Railing" + 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_Railing", "Data")) + props = obj.BIMRailingProperties + # 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 FinishEditingRailing(bpy.types.Operator, tool.Ifc.Operator): + bl_idname = "bim.finish_editing_railing" + bl_label = "Finish editing railing" + bl_options = {"REGISTER"} + + def _execute(self, context): + obj = context.active_object + element = tool.Ifc.get_entity(obj) + props = obj.BIMRailingProperties + + if not RailingData.is_loaded: + RailingData.load() + path_data = RailingData.data["parameters"]['data_dict']['path_data'] + + railing_data = props.get_general_kwargs() + railing_data['path_data'] = path_data + props.is_editing = -1 + + update_bbim_railing_pset(element, railing_data) + update_railing_modifier_ifc_data(context) + return {"FINISHED"} + + +class EnableEditingRailingPath(bpy.types.Operator, tool.Ifc.Operator): + bl_idname = "bim.enable_editing_railing_path" + bl_label = "Enable Editing Railing Path" + bl_options = {"REGISTER"} + + def _execute(self, context): + obj = context.active_object + props = obj.BIMRailingProperties + element = tool.Ifc.get_entity(obj) + + props.is_editing_path = True + update_railing_modifier_bmesh(context) + return {"FINISHED"} + + +class CancelEditingRailingPath(bpy.types.Operator, tool.Ifc.Operator): + bl_idname = "bim.cancel_editing_railing_path" + bl_label = "Cancel Editing Railing Path" + bl_options = {"REGISTER"} + + def _execute(self, context): + obj = context.active_object + element = tool.Ifc.get_entity(obj) + props = obj.BIMRailingProperties + update_railing_modifier_bmesh(context) + props.is_editing_path = False + return {"FINISHED"} + + +class FinishEditingRailingPath(bpy.types.Operator, tool.Ifc.Operator): + bl_idname = "bim.finish_editing_railing_path" + bl_label = "Finish Editing Railing Path" + bl_options = {"REGISTER"} + + def _execute(self, context): + obj = context.active_object + element = tool.Ifc.get_entity(obj) + props = obj.BIMRailingProperties + + railing_data = props.get_general_kwargs() + path_data = get_path_data(obj) + railing_data['path_data'] = path_data + props.is_editing_path = False + + update_bbim_railing_pset(element, railing_data) + refresh() # need RailingData to be updated before update_railing_modifier_bmesh + update_railing_modifier_bmesh(context) + return {"FINISHED"} + + +class RemoveRailing(bpy.types.Operator, tool.Ifc.Operator): + bl_idname = "bim.remove_railing" + bl_label = "Remove Railing" + bl_options = {"REGISTER"} + + def _execute(self, context): + obj = context.active_object + props = obj.BIMRailingProperties + element = tool.Ifc.get_entity(obj) + obj.BIMRailingProperties.is_editing = -1 + + pset = tool.Pset.get_element_pset(element, "BBIM_Railing") + ifcopenshell.api.run("pset.remove_pset", tool.Ifc.get(), pset=pset) + props.railing_added_previously = True + return {"FINISHED"} + + +def add_object_button(self, context): + self.layout.operator(BIM_OT_add_railing.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 8964f9e52e..71a5d20d61 100644 --- a/src/blenderbim/blenderbim/bim/module/model/ui.py +++ b/src/blenderbim/blenderbim/bim/module/model/ui.py @@ -19,11 +19,12 @@ import bpy import blenderbim.tool as tool from bpy.types import Panel, Operator, Menu -from blenderbim.bim.module.model.data import AuthoringData, ArrayData, StairData, SverchokData, WindowData, DoorData +from blenderbim.bim.module.model.data import AuthoringData, ArrayData, StairData, SverchokData, WindowData, DoorData, RailingData 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.helper import prop_with_search @@ -529,6 +530,65 @@ class BIM_PT_door(bpy.types.Panel): row.operator("bim.add_door", icon="ADD", text="") +class BIM_PT_railing(bpy.types.Panel): + bl_label = "IFC Railing" + bl_idname = "BIM_PT_railing" + 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 RailingData.is_loaded: + RailingData.load() + + props = context.active_object.BIMRailingProperties + + if RailingData.data["parameters"]: + row = self.layout.row(align=True) + row.label(text="Railing parameters", icon="OUTLINER_OB_LATTICE") + + railing_data = RailingData.data["parameters"]["data_dict"] + + if props.is_editing != -1: + row = self.layout.row(align=True) + row.operator("bim.finish_editing_railing", icon="CHECKMARK", text="Finish editing") + row.operator("bim.cancel_editing_railing", icon="CANCEL", text="") + + general_props = props.get_general_kwargs() + for prop in general_props: + self.layout.prop(props, prop) + + update_railing_modifier_bmesh(context) + + elif props.is_editing_path: + row.operator("bim.finish_editing_railing_path", icon="CHECKMARK", text="Finish editing path") + row.operator("bim.cancel_editing_railing_path", icon="CANCEL", text="") + + else: + row.operator("bim.enable_editing_railing", icon="GREASEPENCIL", text="") + row.operator("bim.enable_editing_railing_path", icon="ANIM", text="") + row.operator("bim.remove_railing", icon="X", text="") + + box = self.layout.box() + general_props = props.get_general_kwargs() + for prop in general_props: + prop_value = railing_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 Railing Found") + row.operator("bim.add_railing", icon="ADD", text="") + + class BIM_MT_model(Menu): bl_idname = "BIM_MT_model" bl_label = "IFC Objects" diff --git a/src/blenderbim/blenderbim/core/tool.py b/src/blenderbim/blenderbim/core/tool.py index 88ce1e018a..f662439b9b 100644 --- a/src/blenderbim/blenderbim/core/tool.py +++ b/src/blenderbim/blenderbim/core/tool.py @@ -76,6 +76,9 @@ def get_name(cls, ifc_class, name): pass def get_obj_ifc_definition_id(cls, obj, obj_type): pass def get_selected_objects(cls): pass def set_active_object(cls, obj): pass +def apply_bmesh(cls, mesh, bm): pass +def get_bmesh_for_mesh(cls, mesh, clean=False): pass + @interface class Boundary: pass diff --git a/src/blenderbim/blenderbim/tool/blender.py b/src/blenderbim/blenderbim/tool/blender.py index 48a7aef2b8..abaef2cfd9 100644 --- a/src/blenderbim/blenderbim/tool/blender.py +++ b/src/blenderbim/blenderbim/tool/blender.py @@ -92,3 +92,34 @@ class Blender: return tool.Ifc.get_entity(bpy.context.active_object).is_a(ifc_class) return False return False + + @classmethod + def apply_bmesh(cls, mesh, bm): + import bmesh + + if bpy.context.object and bpy.context.object.mode == "EDIT": + # better to be safe because otherwise mesh won't be updated + # and you won't get any errors + if not bm.is_wrapped or hash(bmesh.from_edit_mesh(mesh)) != hash(bm): + raise Exception(f'{bm} is not edit mesh for {mesh}. ' + 'For applying bmesh in edit mode bmesh should be acquired with `bmesh.from_edit_mesh(me)`.') + bmesh.update_edit_mesh(mesh) + else: + bm.to_mesh(mesh) + + bm.free() + mesh.update() + + @classmethod + def get_bmesh_for_mesh(cls, mesh, clean=False): + import bmesh + + if bpy.context.object.mode == "EDIT": + bm = bmesh.from_edit_mesh(mesh) + if clean: + bm.clear() + else: + bm = bmesh.new() + if not clean: + bm.from_mesh(mesh) + return bm \ No newline at end of file