You can now generate type-based spaces and regenerate spaces when boundaries change.

This commit is contained in:
Dion Moult
2023-02-27 20:49:38 +11:00
parent 4fe321275a
commit 20d44378d9
7 changed files with 177 additions and 263 deletions
@@ -26,6 +26,7 @@ from . import (
product, product,
wall, wall,
slab, slab,
space,
stair, stair,
window, window,
opening, opening,
@@ -47,11 +48,8 @@ classes = (
product.AddEmptyType, product.AddEmptyType,
product.AlignProduct, product.AlignProduct,
product.ChangeTypePage, product.ChangeTypePage,
product.DisplayConstrTypes,
product.GenerateHippedRoof, product.GenerateHippedRoof,
product.GenerateSpace,
product.LoadTypeThumbnails, product.LoadTypeThumbnails,
product.ReinvokeOperator,
workspace.Hotkey, workspace.Hotkey,
wall.AlignWall, wall.AlignWall,
wall.ChangeExtrusionDepth, wall.ChangeExtrusionDepth,
@@ -87,6 +85,7 @@ classes = (
slab.EnableEditingSketchExtrusionProfile, slab.EnableEditingSketchExtrusionProfile,
slab.ResetVertex, slab.ResetVertex,
slab.SetArcIndex, slab.SetArcIndex,
space.GenerateSpace,
prop.ConstrTypeInfo, prop.ConstrTypeInfo,
prop.ConstrClassInfo, prop.ConstrClassInfo,
prop.ConstrBrowserState, prop.ConstrBrowserState,
@@ -102,7 +101,6 @@ classes = (
ui.BIM_PT_sverchok, ui.BIM_PT_sverchok,
ui.BIM_PT_window, ui.BIM_PT_window,
ui.BIM_PT_door, ui.BIM_PT_door,
ui.DisplayConstrTypesUI,
ui.LaunchTypeManager, ui.LaunchTypeManager,
ui.HelpConstrTypes, ui.HelpConstrTypes,
ui.BIM_MT_model, ui.BIM_MT_model,
@@ -31,7 +31,6 @@ import blenderbim.core.geometry
from . import wall, slab, profile, mep from . import wall, slab, profile, mep
from blenderbim.bim.ifc import IfcStore from blenderbim.bim.ifc import IfcStore
from blenderbim.bim.module.model.data import AuthoringData from blenderbim.bim.module.model.data import AuthoringData
from blenderbim.bim.module.model.prop import store_cursor_position
from mathutils import Vector, Matrix from mathutils import Vector, Matrix
from bpy_extras.object_utils import AddObjectHelper from bpy_extras.object_utils import AddObjectHelper
from . import prop from . import prop
@@ -223,66 +222,6 @@ class ChangeTypePage(bpy.types.Operator, tool.Ifc.Operator):
return {"FINISHED"} return {"FINISHED"}
class DisplayConstrTypes(bpy.types.Operator):
bl_idname = "bim.display_constr_types"
bl_label = "Browse Construction Types"
bl_options = {"REGISTER"}
bl_description = "Display all available Construction Types to add new instances"
def invoke(self, context, event):
if not AuthoringData.is_loaded:
AuthoringData.load()
props = context.scene.BIMModelProperties
ifc_class = props.ifc_class
constr_class_info = AuthoringData.constr_class_info(ifc_class)
if constr_class_info is None or not constr_class_info.fully_loaded:
AuthoringData.assetize_constr_class(ifc_class)
bpy.ops.bim.display_constr_types_ui("INVOKE_DEFAULT")
return {"FINISHED"}
class ReinvokeOperator(bpy.types.Operator):
bl_idname = "bim.reinvoke_operator"
bl_label = "Reinvoke Popup Operator"
bl_options = {"REGISTER"}
bl_description = "Reinvoke a popup operator"
operator: bpy.props.StringProperty()
def execute(self, context):
return {"FINISHED"}
def invoke(self, context, event):
browser_state = context.scene.BIMModelProperties.constr_browser_state
store_cursor_position(context, event, window=False)
cursor_x, cursor_y = browser_state.cursor_x, browser_state.cursor_y
window_x, window_y = browser_state.window_x, browser_state.window_y
window = context.window
operator = self.operator
self.move_cursor_away(context, window)
def move_cursor_to_window():
window.cursor_warp(window_x, window_y)
def run_operator(operator, *args, **kwargs):
reduce(lambda x, arg: getattr(x, arg), operator.split("."), bpy.ops)(*args, **kwargs)
def reinvoke():
run_operator(operator, "INVOKE_DEFAULT", reinvoked=True)
window.cursor_warp(cursor_x, cursor_y)
bpy.app.timers.register(move_cursor_to_window, first_interval=browser_state.update_delay)
bpy.app.timers.register(reinvoke, first_interval=3 * browser_state.update_delay)
return {"FINISHED"}
def move_cursor_away(self, context, window): # closes current popup
browser_state = context.scene.BIMModelProperties.constr_browser_state
window.cursor_warp(browser_state.far_away_x, browser_state.far_away_y)
@staticmethod
def run_operator(operator, *args, **kwargs):
reduce(lambda x, arg: getattr(x, arg), operator.split("."), bpy.ops)(*args, **kwargs)
class AlignProduct(bpy.types.Operator): class AlignProduct(bpy.types.Operator):
bl_idname = "bim.align_product" bl_idname = "bim.align_product"
bl_label = "Align Product" bl_label = "Align Product"
@@ -579,110 +518,6 @@ def ensure_material_unassigned(usecase_path, ifc_file, settings):
total_removed += 1 total_removed += 1
class GenerateSpace(bpy.types.Operator, tool.Ifc.Operator):
bl_idname = "bim.generate_space"
bl_label = "Generate Space"
bl_options = {"REGISTER"}
def _execute(self, context):
# Note: this experimental operator relies on Shapely v2 (not v1.8 which
# is currently bundled). It only works based on a 2D plan only
# considering the standard layered walls (i.e. prismatic) in the
# currently active storey (though we can easily extend it to include
# other "bounding" elements). For now we generate rooms that exclude
# walls (i.e. not to wall midpoint or exterior / interior edge).
import bmesh
import shapely
from math import pi
collection = context.view_layer.active_layer_collection.collection
collection_obj = bpy.data.objects.get(collection.name)
if not collection_obj:
return
spatial_element = tool.Ifc.get_entity(collection_obj)
if not spatial_element:
return
boundary_elements = []
for subelement in ifcopenshell.util.element.get_decomposition(spatial_element):
if subelement.is_a("IfcWall") and tool.Model.get_usage_type(subelement) == "LAYER2":
boundary_elements.append(subelement)
boundary_lines = []
for element in boundary_elements:
obj = tool.Ifc.get_object(element)
if not obj:
continue
axis = self.get_wall_axis(obj)
for ypos in ["miny", "maxy"]:
start, end = axis[ypos]
offset = (end - start).normalized() * 0.3 # Offset wall lines by 300mm
boundary_lines.append(shapely.LineString([start - offset, end + offset]))
unioned_boundaries = shapely.union_all(shapely.GeometryCollection(boundary_lines))
closed_polygons = shapely.polygonize(unioned_boundaries.geoms)
x, y = context.scene.cursor.location.xy
space_polygon = None
for polygon in closed_polygons.geoms:
if shapely.contains_xy(polygon, x, y):
space_polygon = shapely.force_3d(polygon)
if not space_polygon:
return
bm = bmesh.new()
bm.verts.index_update()
bm.edges.index_update()
new_verts = [bm.verts.new(v) for v in space_polygon.exterior.coords[0:-1]]
[bm.edges.new((new_verts[i], new_verts[i + 1])) for i in range(len(new_verts) - 1)]
bm.edges.new((new_verts[len(new_verts) - 1], new_verts[0]))
for interior in space_polygon.interiors:
new_verts = [bm.verts.new(v) for v in interior.coords[0:-1]]
[bm.edges.new((new_verts[i], new_verts[i + 1])) for i in range(len(new_verts) - 1)]
bm.edges.new((new_verts[len(new_verts) - 1], new_verts[0]))
bm.verts.index_update()
bm.edges.index_update()
bmesh.ops.remove_doubles(bm, verts=bm.verts, dist=1e-5)
bmesh.ops.triangle_fill(bm, edges=bm.edges)
bmesh.ops.dissolve_limit(bm, angle_limit=pi / 180 * 5, verts=bm.verts, edges=bm.edges)
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, 3.0], verts=extruded_verts)
mesh = bpy.data.meshes.new(name="Space")
bm.to_mesh(mesh)
bm.free()
obj = bpy.data.objects.new("Space", mesh)
bpy.context.scene.collection.objects.link(obj)
def get_wall_axis(self, obj):
x_values = [v[0] for v in obj.bound_box]
y_values = [v[1] for v in obj.bound_box]
min_x = min(x_values)
max_x = max(x_values)
min_y = min(y_values)
max_y = max(y_values)
return {
"miny": [
(obj.matrix_world @ Vector((min_x, min_y, 0.0))).to_2d(),
(obj.matrix_world @ Vector((max_x, min_y, 0.0))).to_2d(),
],
"maxy": [
(obj.matrix_world @ Vector((min_x, max_y, 0.0))).to_2d(),
(obj.matrix_world @ Vector((max_x, max_y, 0.0))).to_2d(),
],
}
class GenerateHippedRoof(bpy.types.Operator, tool.Ifc.Operator): class GenerateHippedRoof(bpy.types.Operator, tool.Ifc.Operator):
bl_idname = "bim.generate_hipped_roof" bl_idname = "bim.generate_hipped_roof"
bl_label = "Generate Hipped Roof" bl_label = "Generate Hipped Roof"
@@ -55,14 +55,6 @@ def get_type_predefined_type(self, context):
return AuthoringData.data["type_predefined_type"] return AuthoringData.data["type_predefined_type"]
def store_cursor_position(context, event, cursor=True, window=True):
browser_state = context.scene.BIMModelProperties.constr_browser_state
if cursor:
browser_state.cursor_x, browser_state.cursor_y = event.mouse_x, event.mouse_y
if window:
browser_state.window_x, browser_state.window_y = event.mouse_x, event.mouse_y
def update_icon_id(self, context, browser=False): def update_icon_id(self, context, browser=False):
if context == "lost_context" or (context.region is not None and context.region.type != "TOOL_HEADER"): if context == "lost_context" or (context.region is not None and context.region.type != "TOOL_HEADER"):
ifc_class = self.ifc_class_browser if browser else self.ifc_class ifc_class = self.ifc_class_browser if browser else self.ifc_class
@@ -0,0 +1,156 @@
# BlenderBIM Add-on - OpenBIM Blender Add-on
# Copyright (C) 2023 Dion Moult <dion@thinkmoult.com>
#
# 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 <http://www.gnu.org/licenses/>.
import bpy
import bmesh
import shapely
import ifcopenshell
import ifcopenshell.util.element
import blenderbim.tool as tool
import blenderbim.core.type
from math import pi
from mathutils import Vector, Matrix
class GenerateSpace(bpy.types.Operator, tool.Ifc.Operator):
bl_idname = "bim.generate_space"
bl_label = "Generate Space"
bl_options = {"REGISTER"}
def _execute(self, context):
# This only works based on a 2D plan only considering the standard
# layered walls (i.e. prismatic) in the currently active storey (though
# we can easily extend it to include other "bounding" elements). For
# now we generate rooms that exclude walls (i.e. not to wall midpoint
# or exterior / interior edge).
props = context.scene.BIMModelProperties
relating_type_id = props.relating_type_id
relating_type = None
if relating_type_id:
relating_type = tool.Ifc.get().by_id(int(relating_type_id))
if not relating_type.is_a("IfcSpaceType"):
relating_type = None
collection = context.view_layer.active_layer_collection.collection
collection_obj = bpy.data.objects.get(collection.name)
if not collection_obj:
return
spatial_element = tool.Ifc.get_entity(collection_obj)
if not spatial_element:
return
boundary_elements = []
for subelement in ifcopenshell.util.element.get_decomposition(spatial_element):
if subelement.is_a("IfcWall") and tool.Model.get_usage_type(subelement) == "LAYER2":
boundary_elements.append(subelement)
boundary_lines = []
for element in boundary_elements:
obj = tool.Ifc.get_object(element)
if not obj:
continue
axis = self.get_wall_axis(obj)
for ypos in ["miny", "maxy"]:
start, end = axis[ypos]
offset = (end - start).normalized() * 0.3 # Offset wall lines by 300mm
boundary_lines.append(shapely.LineString([start - offset, end + offset]))
unioned_boundaries = shapely.union_all(shapely.GeometryCollection(boundary_lines))
closed_polygons = shapely.polygonize(unioned_boundaries.geoms)
x, y = context.scene.cursor.location.xy
space_polygon = None
for polygon in closed_polygons.geoms:
if shapely.contains_xy(polygon, x, y):
space_polygon = shapely.force_3d(polygon)
if not space_polygon:
return
bm = bmesh.new()
bm.verts.index_update()
bm.edges.index_update()
new_verts = [bm.verts.new(v) for v in space_polygon.exterior.coords[0:-1]]
[bm.edges.new((new_verts[i], new_verts[i + 1])) for i in range(len(new_verts) - 1)]
bm.edges.new((new_verts[len(new_verts) - 1], new_verts[0]))
for interior in space_polygon.interiors:
new_verts = [bm.verts.new(v) for v in interior.coords[0:-1]]
[bm.edges.new((new_verts[i], new_verts[i + 1])) for i in range(len(new_verts) - 1)]
bm.edges.new((new_verts[len(new_verts) - 1], new_verts[0]))
bm.verts.index_update()
bm.edges.index_update()
bmesh.ops.remove_doubles(bm, verts=bm.verts, dist=1e-5)
bmesh.ops.triangle_fill(bm, edges=bm.edges)
bmesh.ops.dissolve_limit(bm, angle_limit=pi / 180 * 5, verts=bm.verts, edges=bm.edges)
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, 3.0], verts=extruded_verts)
mesh = bpy.data.meshes.new(name="Space")
bm.to_mesh(mesh)
bm.free()
active_obj = bpy.context.active_object
element = None
if bpy.context.selected_objects and active_obj:
element = tool.Ifc.get_entity(active_obj)
if element and element.is_a("IfcSpace"):
mesh.name = active_obj.data.name
mesh.BIMMeshProperties.ifc_definition_id = active_obj.data.BIMMeshProperties.ifc_definition_id
tool.Geometry.change_object_data(active_obj, mesh, is_global=True)
tool.Ifc.edit(active_obj)
else:
if relating_type:
name = tool.Model.generate_occurrence_name(relating_type, "IfcSpace")
else:
name = "Space"
obj = bpy.data.objects.new("Space", mesh)
collection.objects.link(obj)
bpy.ops.bim.assign_class(obj=obj.name, ifc_class="IfcSpace")
element = tool.Ifc.get_entity(obj)
if relating_type:
print(element)
print(relating_type)
blenderbim.core.type.assign_type(tool.Ifc, tool.Type, element=element, type=relating_type)
def get_wall_axis(self, obj):
x_values = [v[0] for v in obj.bound_box]
y_values = [v[1] for v in obj.bound_box]
min_x = min(x_values)
max_x = max(x_values)
min_y = min(y_values)
max_y = max(y_values)
return {
"miny": [
(obj.matrix_world @ Vector((min_x, min_y, 0.0))).to_2d(),
(obj.matrix_world @ Vector((max_x, min_y, 0.0))).to_2d(),
],
"maxy": [
(obj.matrix_world @ Vector((min_x, max_y, 0.0))).to_2d(),
(obj.matrix_world @ Vector((max_x, max_y, 0.0))).to_2d(),
],
}
@@ -20,7 +20,7 @@ import bpy
import blenderbim.tool as tool import blenderbim.tool as tool
from bpy.types import Panel, Operator, Menu 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
from blenderbim.bim.module.model.prop import store_cursor_position, get_ifc_class 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.stair import update_stair_modifier
from blenderbim.bim.module.model.window import update_window_modifier_bmesh 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.door import update_door_modifier_bmesh
@@ -125,86 +125,6 @@ class BIM_PT_authoring(Panel):
row.operator("bim.align_wall", icon="ANCHOR_BOTTOM", text="Int.").align_type = "INTERIOR" row.operator("bim.align_wall", icon="ANCHOR_BOTTOM", text="Int.").align_type = "INTERIOR"
class DisplayConstrTypesUI(Operator):
bl_idname = "bim.display_constr_types_ui"
bl_label = "Browse Construction Types"
bl_options = {"REGISTER"}
bl_description = "Display all available Construction Types to add new instances"
reinvoked: bpy.props.BoolProperty(default=False)
def execute(self, context):
return {"FINISHED"}
def invoke(self, context, event):
store_cursor_position(context, event, window=not self.reinvoked)
return context.window_manager.invoke_popup(self, width=550)
def reinvoke(self, context):
browser_state = context.scene.BIMModelProperties.constr_browser_state
def set_updating_transaction():
browser_state.updating = True
def run_operator():
bpy.ops.bim.reinvoke_operator("INVOKE_DEFAULT", operator="bim.display_constr_types_ui")
browser_state.updating = False
if not browser_state.updating:
bpy.app.timers.register(set_updating_transaction)
bpy.app.timers.register(run_operator, first_interval=browser_state.update_delay)
def draw(self, context):
props = context.scene.BIMModelProperties
if AuthoringData.data["ifc_classes"]:
row = self.layout.row()
row.label(text="", icon="FILE_VOLUME")
prop_with_search(row, props, "ifc_class_browser", text="")
ifc_class = props.ifc_class_browser
num_cols = 3
self.layout.row().separator(factor=0.25)
flow = self.layout.grid_flow(row_major=True, columns=num_cols, even_columns=True, even_rows=True, align=True)
relating_types = AuthoringData.relating_types_browser()
num_types = len(relating_types)
for idx, (relating_type_id, name, desc) in enumerate(relating_types):
outer_col = flow.column()
box = outer_col.box()
row = box.row()
row.label(text=name, icon="FILE_3D")
row.alignment = "CENTER"
row = box.row()
if ifc_class in props.constr_classes:
constr_class_info = props.constr_classes[ifc_class]
constr_types_info = constr_class_info.constr_types
if relating_type_id in constr_types_info:
icon_id = constr_types_info[relating_type_id].icon_id
row.template_icon(icon_value=icon_id, scale=6.0)
else:
self.reinvoke(context)
return
row = box.row()
op = row.operator("bim.add_constr_type_instance", icon="ADD")
op.from_invoke = True
op.ifc_class = ifc_class
if relating_type_id.isnumeric():
op.relating_type_id = int(relating_type_id)
last_row_cols = num_types % num_cols
if last_row_cols != 0:
for _ in range(num_cols - last_row_cols):
flow.column()
row = self.layout.row()
row.alignment = "RIGHT"
row.operator("bim.help_relating_types", text="", icon="QUESTION")
class HelpConstrTypes(Operator): class HelpConstrTypes(Operator):
bl_idname = "bim.help_relating_types" bl_idname = "bim.help_relating_types"
bl_label = "Construction Types Help" bl_label = "Construction Types Help"
@@ -122,11 +122,6 @@ class BimToolUI:
if not AuthoringData.is_loaded: if not AuthoringData.is_loaded:
AuthoringData.load() AuthoringData.load()
# if ifc_classes and relating_types_ids and not cls.props.icon_id and not is_tool_header:
# # hack Dion won't like to show a preview also on the first time the sidebar is shown
# bpy.app.timers.register(lambda: prop.update_ifc_class(cls.props, "lost_context"))
# bpy.app.timers.register(lambda: prop.update_relating_type(cls.props, "lost_context"))
if context.region.type == "TOOL_HEADER": if context.region.type == "TOOL_HEADER":
cls.draw_header_interface() cls.draw_header_interface()
elif context.region.type in ("UI", "WINDOW"): elif context.region.type in ("UI", "WINDOW"):
@@ -170,6 +165,11 @@ class BimToolUI:
elif cls.props.ifc_class in ("IfcWindowType", "IfcWindowStyle", "IfcDoorType", "IfcDoorStyle"): elif cls.props.ifc_class in ("IfcWindowType", "IfcWindowStyle", "IfcDoorType", "IfcDoorStyle"):
row = cls.layout.row(align=True) row = cls.layout.row(align=True)
row.prop(data=cls.props, property="rl2", text="RL") row.prop(data=cls.props, property="rl2", text="RL")
elif cls.props.ifc_class in ("IfcSpaceType"):
row = cls.layout.row(align=True)
row.label(text="", icon="EVENT_SHIFT")
row.label(text="", icon="EVENT_G")
add_layout_hotkey_operator(row, "Generate", "S_G", bpy.ops.bim.generate_space.__doc__)
@classmethod @classmethod
def draw_edit_object_interface(cls, context): def draw_edit_object_interface(cls, context):
@@ -290,6 +290,11 @@ class BimToolUI:
row.label(text="", icon="EVENT_SHIFT") row.label(text="", icon="EVENT_SHIFT")
row.label(text="", icon="EVENT_F") row.label(text="", icon="EVENT_F")
row.operator("bim.hotkey", text="Flip").hotkey = "S_F" row.operator("bim.hotkey", text="Flip").hotkey = "S_F"
elif AuthoringData.data["active_class"] in ("IfcSpace",):
row = cls.layout.row(align=True)
row.label(text="", icon="EVENT_SHIFT")
row.label(text="", icon="EVENT_G")
add_layout_hotkey_operator(row, "Regen", "S_G", bpy.ops.bim.generate_space.__doc__)
elif AuthoringData.data["active_class"] in ( elif AuthoringData.data["active_class"] in (
"IfcCableCarrierSegmentType", "IfcCableCarrierSegmentType",
"IfcCableSegmentType", "IfcCableSegmentType",
@@ -514,6 +519,8 @@ class Hotkey(bpy.types.Operator, tool.Ifc.Operator):
def hotkey_S_G(self): def hotkey_S_G(self):
if not bpy.context.selected_objects: if not bpy.context.selected_objects:
if self.props.ifc_class == "IfcSpaceType":
bpy.ops.bim.generate_space()
return return
if self.active_material_usage == "LAYER2": if self.active_material_usage == "LAYER2":
bpy.ops.bim.recalculate_wall() bpy.ops.bim.recalculate_wall()
@@ -521,6 +528,8 @@ class Hotkey(bpy.types.Operator, tool.Ifc.Operator):
bpy.ops.bim.recalculate_profile() bpy.ops.bim.recalculate_profile()
elif self.active_class in ("IfcWindow", "IfcWindowStandardCase", "IfcDoor", "IfcDoorStandardCase"): elif self.active_class in ("IfcWindow", "IfcWindowStandardCase", "IfcDoor", "IfcDoorStandardCase"):
bpy.ops.bim.recalculate_fill() bpy.ops.bim.recalculate_fill()
elif self.active_class in ("IfcSpace"):
bpy.ops.bim.generate_space()
def hotkey_S_M(self): def hotkey_S_M(self):
if not bpy.context.selected_objects: if not bpy.context.selected_objects:
+4
View File
@@ -88,6 +88,10 @@ class Ifc(blenderbim.core.tool.Ifc):
def link(cls, element, obj): def link(cls, element, obj):
IfcStore.link_element(element, obj) IfcStore.link_element(element, obj)
@classmethod
def edit(cls, obj):
IfcStore.edited_objs.add(obj)
@classmethod @classmethod
def delete(cls, element): def delete(cls, element):
IfcStore.delete_element(element) IfcStore.delete_element(element)