diff --git a/src/blenderbim/blenderbim/bim/helper.py b/src/blenderbim/blenderbim/bim/helper.py index 06508b50fa..45661d64f1 100644 --- a/src/blenderbim/blenderbim/bim/helper.py +++ b/src/blenderbim/blenderbim/bim/helper.py @@ -118,6 +118,17 @@ def prop_with_search(layout, data, prop_name, **kwargs): op.prop_name = prop_name +def layout_with_margins(layout, margin_left=0.025, margin_right=None): + margin_right = margin_left if margin_right is None else margin_right + split = layout.split(factor=margin_left, align=True) + cols = [split.column() for _ in range(2)] + cols[0].label(text="") + subsplit = cols[-1].split(factor=(1. - margin_right), align=True) + subcol = subsplit.column() + subsplit.column().label(text="") + return subcol + + def get_enum_items(data, prop_name, context): # Retrieve items from a dynamic EnumProperty, which is otherwise not supported # Or throws an error in the console when the items callback returns an empty list diff --git a/src/blenderbim/blenderbim/bim/module/model/__init__.py b/src/blenderbim/blenderbim/bim/module/model/__init__.py index c226210dae..30c5ff8c12 100644 --- a/src/blenderbim/blenderbim/bim/module/model/__init__.py +++ b/src/blenderbim/blenderbim/bim/module/model/__init__.py @@ -17,11 +17,14 @@ # along with BlenderBIM Add-on. If not, see . import bpy -from . import handler, prop, ui, grid, product, wall, slab, stair, opening, pie, workspace +from . import handler, prop, ui, grid, product, wall, slab, stair, opening, pie, workspace, profile classes = ( product.AddEmptyType, - product.AddTypeInstance, + product.AddConstrTypeInstance, + product.DisplayConstrTypes, + product.SelectConstructionType, + product.HelpConstrTypes, product.AlignProduct, product.DynamicallyVoidProduct, workspace.Hotkey, @@ -32,8 +35,8 @@ classes = ( opening.AddElementOpening, profile.ExtendProfile, prop.BIMModelProperties, + prop.ConstrTypeInfo, ui.BIM_PT_authoring, - ui.BIM_PT_authoring_architectural, grid.BIM_OT_add_object, stair.BIM_OT_add_object, opening.BIM_OT_add_object, @@ -51,6 +54,7 @@ def register(): if not bpy.app.background: bpy.utils.register_tool(workspace.BimTool, after={"builtin.scale_cage"}, separator=True, group=True) bpy.types.Scene.BIMModelProperties = bpy.props.PointerProperty(type=prop.BIMModelProperties) + bpy.types.Scene.ConstrTypeInfo = bpy.props.CollectionProperty(type=prop.ConstrTypeInfo) 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(opening.add_object_button) @@ -68,6 +72,7 @@ def unregister(): if not bpy.app.background: bpy.utils.unregister_tool(workspace.BimTool) del bpy.types.Scene.BIMModelProperties + del bpy.types.Scene.ConstrTypeInfo 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) diff --git a/src/blenderbim/blenderbim/bim/module/model/data.py b/src/blenderbim/blenderbim/bim/module/model/data.py index 393fe73273..c5ba73182c 100644 --- a/src/blenderbim/blenderbim/bim/module/model/data.py +++ b/src/blenderbim/blenderbim/bim/module/model/data.py @@ -16,8 +16,14 @@ # You should have received a copy of the GNU General Public License # along with BlenderBIM Add-on. If not, see . +import functools import bpy import blenderbim.tool as tool +from blenderbim.bim.ifc import IfcStore + + +preview_icon_ids = {} +attempts = 0 def refresh(): @@ -31,10 +37,29 @@ class AuthoringData: @classmethod def load(cls): cls.is_loaded = True - cls.data = { - "ifc_classes": cls.ifc_classes(), - "relating_types": cls.relating_types(), - } + if not hasattr(cls, "data"): + cls.data = {} + cls.props = bpy.context.scene.BIMModelProperties + cls.load_ifc_classes() + cls.load_relating_types() + cls.load_relating_types_browser() + cls.load_preview_constr_types() + + @classmethod + def load_ifc_classes(cls): + cls.data["ifc_classes"] = cls.ifc_classes() + + @classmethod + def load_relating_types(cls): + cls.data["relating_types_ids"] = cls.relating_types() + + @classmethod + def load_relating_types_browser(cls): + cls.data["relating_types_ids_browser"] = cls.relating_types_browser() + + @classmethod + def load_preview_constr_types(cls): + cls.data["preview_constr_types"] = preview_icon_ids @classmethod def ifc_classes(cls): @@ -49,16 +74,131 @@ class AuthoringData: return results @classmethod - def relating_types(cls): - ifc_classes = cls.ifc_classes() + def constr_class_entities(cls, ifc_class=None): + ifc_classes = cls.data["ifc_classes"] if not ifc_classes: return [] results = [] - ifc_class = bpy.context.scene.BIMModelProperties.ifc_class + if ifc_class is None: + ifc_class = cls.props.ifc_class if not ifc_class and ifc_classes: ifc_class = ifc_classes[0][0] if ifc_class: - elements = [(str(e.id()), e.Name, e.Description or "") for e in tool.Ifc.get().by_type(ifc_class)] - results.extend(sorted(elements, key=lambda s: s[1])) + elements = sorted(tool.Ifc.get().by_type(ifc_class), key=lambda s: s.Name) + results.extend(elements) return results return [] + + @classmethod + def relating_types(cls, ifc_class=None): + return [ + (str(e.id()), e.Name, e.Description or "") for e in cls.constr_class_entities(ifc_class=ifc_class) + ] + + @classmethod + def relating_types_browser(cls): + return cls.relating_types(ifc_class=cls.props.ifc_class_browser) + + @staticmethod + def new_relating_type_info(ifc_class): + relating_type_info = bpy.context.scene.ConstrTypeInfo.add() + relating_type_info.name = ifc_class + return relating_type_info + + @classmethod + def assetize_constr_class(cls, ifc_class=None): + if ifc_class is None: + ifc_class = cls.props.ifc_class + relating_type_info = cls.relating_type_info(ifc_class) + _ = cls.new_relating_type_info(ifc_class) if relating_type_info is None else relating_type_info + constr_class_occurrences = cls.constr_class_entities(ifc_class) + preview_constr_types = cls.data["preview_constr_types"] + for constr_class_entity in constr_class_occurrences: + + ### handle asset regeneration when library entity is updated ¿? + if (ifc_class not in preview_constr_types + or str(constr_class_entity.id()) not in preview_constr_types[ifc_class]): + obj = tool.Ifc.get_object(constr_class_entity) + cls.assetize_object(obj, ifc_class, constr_class_entity) + relating_type_info = cls.relating_type_info(ifc_class) + relating_type_info.fully_loaded = True + + @classmethod + def assetize_object(cls, obj, ifc_class, ifc_class_entity, from_selection=False): + relating_type_id = ifc_class_entity.id() + to_be_deleted = False + if obj.type == 'EMPTY': + kwargs = {} + if not from_selection: + kwargs.update({'ifc_class': ifc_class, 'relating_type_id': relating_type_id}) + new_obj = cls.new_relating_type(**kwargs) + if new_obj is not None: + to_be_deleted = True + obj = new_obj + obj.asset_mark() + obj.asset_generate_preview() + icon_id = obj.preview.icon_id + if ifc_class not in cls.data["preview_constr_types"]: + cls.data["preview_constr_types"][ifc_class] = {} + cls.data["preview_constr_types"][ifc_class][str(relating_type_id)] = {"icon_id": icon_id, "object": obj} + if to_be_deleted: + for col in obj.users_collection: + col.objects.unlink(obj) + + @classmethod + def assetize_relating_type_from_selection(cls): + ifc_class_browser = cls.props.ifc_class_browser + relating_type_id_browser = cls.props.relating_type_id_browser + constr_class_occurrences = cls.constr_class_entities(ifc_class=ifc_class_browser) + constr_class_occurrences = [ + entity for entity in constr_class_occurrences if entity.id() == int(relating_type_id_browser) + ] + if len(constr_class_occurrences) == 0: + return False + constr_class_entity = constr_class_occurrences[0] + obj = tool.Ifc.get_object(constr_class_entity) + if obj is None: + return False + cls.assetize_object(obj, ifc_class_browser, constr_class_entity, from_selection=True) + return True + + @staticmethod + def relating_type_info(ifc_class): + relating_type_infos = [element for element in bpy.context.scene.ConstrTypeInfo if element.name == ifc_class] + return None if len(relating_type_infos) == 0 else relating_type_infos[0] + + @classmethod + def new_relating_type(cls, ifc_class=None, relating_type_id=None): + if ifc_class is None: + bpy.ops.bim.add_constr_type_instance( + ifc_class=cls.props.ifc_class_browser, relating_type_id=int(cls.props.relating_type_id_browser) + ) + else: + cls.props.ifc_class = ifc_class + cls.props.relating_type_id = str(relating_type_id) + bpy.ops.bim.add_constr_type_instance() + return bpy.context.selected_objects[-1] + + @staticmethod + def relating_type_name_by_id(ifc_class, relating_type_id): + file = IfcStore.get_file() + try: + constr_class_entity = file.by_id(int(relating_type_id)) + except (RuntimeError, ValueError): + return None + return constr_class_entity.Name if constr_class_entity.is_a() == ifc_class else None + + @classmethod + def relating_type_id_by_name(cls, ifc_class, relating_type): + relating_types = [ct[0] for ct in cls.relating_types(ifc_class=ifc_class) if ct[1] == relating_type] + return None if len(relating_types) == 0 else relating_types[0] + + @classmethod + def consolidate_relating_type(cls): + cls.props.ifc_class = cls.props.ifc_class_browser + cls.props.relating_type_id = cls.props.relating_type_id_browser + + @classmethod + def setup_relating_type_browser(cls): + cls.props.ifc_class_browser = cls.props.ifc_class + cls.props.relating_type_id_browser = cls.props.relating_type_id diff --git a/src/blenderbim/blenderbim/bim/module/model/mep.py b/src/blenderbim/blenderbim/bim/module/model/mep.py index e8b02b97e6..eeeaff9aee 100644 --- a/src/blenderbim/blenderbim/bim/module/model/mep.py +++ b/src/blenderbim/blenderbim/bim/module/model/mep.py @@ -114,5 +114,13 @@ class MepGenerator: tool.Ifc.run("system.assign_port", element=element, port=port) tool.Ifc.run("geometry.edit_object_placement", product=port, matrix=obj.matrix_world @ mat, is_si=True) - obj.select_set(True) + try: + obj.select_set(True) + except RuntimeError: + def msg(self, context): + txt = "The created object could not be assigned to a collection. " + txt += "Has any IfcSpatialElement been deleted?" + self.layout.label(text=txt) + + bpy.context.window_manager.popup_menu(msg, title="Error", icon="ERROR") return obj diff --git a/src/blenderbim/blenderbim/bim/module/model/product.py b/src/blenderbim/blenderbim/bim/module/model/product.py index 20c948d4e3..312caa0dcf 100644 --- a/src/blenderbim/blenderbim/bim/module/model/product.py +++ b/src/blenderbim/blenderbim/bim/module/model/product.py @@ -17,6 +17,7 @@ # along with BlenderBIM Add-on. If not, see . import bpy +import math import mathutils import ifcopenshell import ifcopenshell.api @@ -29,9 +30,12 @@ import blenderbim.core.type import blenderbim.core.geometry from . import wall, slab, profile, mep from blenderbim.bim.ifc import IfcStore +from blenderbim.bim.module.model.data import AuthoringData +from blenderbim.bim.helper import prop_with_search, layout_with_margins, close_operator_panel from ifcopenshell.api.pset.data import Data as PsetData from mathutils import Vector, Matrix from bpy_extras.object_utils import AddObjectHelper +from . import prop class AddEmptyType(bpy.types.Operator, AddObjectHelper): @@ -53,13 +57,19 @@ def add_empty_type_button(self, context): self.layout.operator(AddEmptyType.bl_idname, icon="FILE_3D") -class AddTypeInstance(bpy.types.Operator): - bl_idname = "bim.add_type_instance" - bl_label = "Add Type Instance" +class AddConstrTypeInstance(bpy.types.Operator): + bl_idname = "bim.add_constr_type_instance" + bl_label = "Add" bl_options = {"REGISTER", "UNDO"} - bl_description = "Add the selected Type Instance to the model" + bl_description = "Add Type Instance to the model" ifc_class: bpy.props.StringProperty() - relating_type: bpy.props.IntProperty() + relating_type_id: bpy.props.IntProperty() + from_invoke: bpy.props.BoolProperty(default=False) + + def invoke(self, context, event): + if self.from_invoke: + close_operator_panel(event) + return self.execute(context) def execute(self, context): return IfcStore.execute_ifc_operator(self, context) @@ -67,11 +77,15 @@ class AddTypeInstance(bpy.types.Operator): def _execute(self, context): props = context.scene.BIMModelProperties ifc_class = self.ifc_class or props.ifc_class - relating_type_id = self.relating_type or props.relating_type + relating_type_id = self.relating_type_id or props.relating_type_id if not ifc_class or not relating_type_id: return {"FINISHED"} + if self.from_invoke: + props.ifc_class = self.ifc_class + props.relating_type_id = str(self.relating_type_id) + self.file = IfcStore.get_file() instance_class = ifcopenshell.util.type.get_applicable_entities(ifc_class, self.file.schema)[0] relating_type = self.file.by_id(int(relating_type_id)) @@ -150,7 +164,8 @@ class AddTypeInstance(bpy.types.Operator): context.view_layer.objects.active = obj return {"FINISHED"} - def generate_layered_element(self, ifc_class, relating_type): + @staticmethod + def generate_layered_element(ifc_class, relating_type): layer_set_direction = None parametric = ifcopenshell.util.element.get_psets(relating_type).get("EPset_Parametric") @@ -172,6 +187,217 @@ class AddTypeInstance(bpy.types.Operator): pass # Dumb block generator? Eh? :) +class DisplayConstrTypes(bpy.types.Operator): + bl_idname = "bim.display_relating_types" + bl_label = "Browse Construction Types" + bl_options = {"REGISTER", "UNDO"} + bl_description = "Display all available Construction Types to add new instances" + + def execute(self, context): + return {"FINISHED"} + + def invoke(self, context, event): + if not AuthoringData.is_loaded: + AuthoringData.load() + AuthoringData.setup_relating_type_browser() + props = context.scene.BIMModelProperties + if props.unfold_relating_types: + ifc_class = props.ifc_class_browser + relating_type_info = AuthoringData.relating_type_info(ifc_class) + if relating_type_info is None or not relating_type_info.fully_loaded: + AuthoringData.assetize_constr_class(ifc_class) + else: + prop.update_relating_type_browser(props, context) + min_width = 250 + width_scaling = 5 ** -1 + width = max([min_width, int(width_scaling * context.region.width)]) + return context.window_manager.invoke_popup(self, width=width) + + def draw(self, context): + props = context.scene.BIMModelProperties + header_data = self.draw_header(props) + if props.unfold_relating_types: + self.draw_by_ifc_class(props, header_data) + else: + self.draw_by_ifc_class_and_type(props, header_data) + + def draw_header(self, props): + layout = self.layout + inner_layout = layout_with_margins(layout, margin_left=0.004) + inner_layout.row().separator(factor=0.75) + split = inner_layout.split(align=True, factor=2./3) + col1 = split.column(align=True) + row = col1.row() + row.prop(data=props, property="unfold_relating_types", text="Preview All Construction Types") + col1.row().separator(factor=1) + row = col1.row() + row.label(text="Select Construction Type:") + col1.row().separator(factor=1.5) + enabled = True + if AuthoringData.data["ifc_classes"]: + subsplit = col1.split(factor=1./3) + subsplit.column().row().label(text="Construction Class:", icon="FILE_VOLUME") + prop_with_search(subsplit.column(), props, "ifc_class_browser", text="") + col1.row().separator() + else: + enabled = False + col2 = split.column(align=True) + subsplit = col2.split(factor=0.9) + subcol = [subsplit.column() for _ in range(2)][-1] + subcol.operator("bim.help_relating_types", text="", icon="QUESTION") + col2.row().separator(factor=1) + return {"enabled": enabled, "layout": inner_layout, "col1": col1, "col2": col2} + + def draw_by_ifc_class(self, props, header_data): + enabled, layout = [header_data[key] for key in ["enabled", "layout"]] + ifc_class_browser = props.ifc_class_browser + num_cols = 3 + layout.row().separator(factor=0.25) + layout.row().label(text="Construction Types:", icon="FILE_3D") + layout.row().separator(factor=0.25) + flow = layout.grid_flow(row_major=True, columns=num_cols, even_columns=True, even_rows=True, align=True) + relating_types_browser = AuthoringData.relating_types_browser() + num_types = len(relating_types_browser) + for idx, (relating_type_id_browser, name, desc) in enumerate(relating_types_browser): + 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 enabled: + preview_constr_types = AuthoringData.data["preview_constr_types"] + if ifc_class_browser in preview_constr_types: + preview_ifc_class = preview_constr_types[ifc_class_browser] + if relating_type_id_browser in preview_ifc_class: + icon_id = preview_ifc_class[relating_type_id_browser]["icon_id"] + row.template_icon(icon_value=icon_id, scale=6.) + box.row().separator(factor=0.2) + row = box.row() + split = row.split(factor=0.5) + col = split.column() + op = col.operator("bim.select_construction_type", icon="RIGHTARROW_THIN") + op.ifc_class = ifc_class_browser + op.relating_type_id = relating_type_id_browser + col = split.column() + op = col.operator("bim.add_constr_type_instance", icon="ADD") + op.from_invoke = True + op.ifc_class = ifc_class_browser + if relating_type_id_browser.isnumeric(): + op.relating_type_id = int(relating_type_id_browser) + factor = 2 if idx + 1 < math.ceil(num_types / num_cols) else 1.5 + outer_col.row().separator(factor=factor) + last_row_cols = num_types % num_cols + if last_row_cols != 0: + for _ in range(num_cols - last_row_cols): + flow.column() + + def draw_by_ifc_class_and_type(self, props, header_data): + enabled, col1, col2 = [header_data[key] for key in ["enabled", "col1", "col2"]] + ifc_class_browser = props.ifc_class_browser + relating_type_id_browser = props.relating_type_id_browser + if AuthoringData.data["relating_types_ids_browser"]: + subsplit = col1.split(factor=1. / 3) + subsplit.column().row().label(text="Construction Type:", icon="FILE_3D") + prop_with_search(subsplit.column(), props, "relating_type_id_browser", text="") + col1.row().separator() + else: + enabled = False + col1.row().separator(factor=4.75) + row = col1.row() + row.enabled = enabled + op = row.operator("bim.select_construction_type", icon="RIGHTARROW_THIN") + op.ifc_class = ifc_class_browser + op.relating_type_id = relating_type_id_browser + op = row.operator("bim.add_constr_type_instance", icon="ADD") + op.from_invoke = True + op.ifc_class = ifc_class_browser + if relating_type_id_browser.isnumeric(): + op.relating_type_id = int(relating_type_id_browser) + col2.row().separator(factor=1.25) + split = col2.split(factor=0.025) + col = [split.column() for _ in range(2)][-1] + box = col.box() + if enabled: + box.template_icon(icon_value=props.icon_id, scale=5.6) + col1.row().separator(factor=1) + + +class SelectConstructionType(bpy.types.Operator): + bl_idname = "bim.select_construction_type" + bl_label = "Select" + bl_options = {"REGISTER", "UNDO"} + bl_description = "Pick Type Instance as selection for subsequent operations" + ifc_class: bpy.props.StringProperty() + relating_type_id: bpy.props.StringProperty() + + def invoke(self, context, event): + close_operator_panel(event) + return self.execute(context) + + def execute(self, context): + props = context.scene.BIMModelProperties + if self.ifc_class != "": + props.ifc_class = self.ifc_class + AuthoringData.load_relating_types() + if self.relating_type_id != "": + props.relating_type_id = self.relating_type_id + return {"FINISHED"} + + +class HelpConstrTypes(bpy.types.Operator): + bl_idname = "bim.help_relating_types" + bl_label = "Construction Types Help" + bl_options = {"REGISTER", "UNDO"} + bl_description = "Click to read some contextual help" + + def execute(self, context): + return {"FINISHED"} + + def invoke(self, context, event): + return context.window_manager.invoke_popup(self, width=510) + + def draw(self, context): + layout = self.layout + layout.row().separator(factor=0.5) + row = layout.row() + row.alignment = "CENTER" + row.label(text="BlenderBIM Help", icon="BLENDER") + layout.row().separator(factor=0.5) + + row = layout_with_margins(layout.row()).row() + row.label(text="Overview:", icon="KEYTYPE_MOVING_HOLD_VEC") + self.draw_lines(layout, self.message_summary) + layout.row().separator() + + row = layout_with_margins(layout.row()).row() + row.label(text="Further support:", icon="KEYTYPE_MOVING_HOLD_VEC") + layout.row().separator(factor=0.5) + row = layout_with_margins(layout).row() + op = row.operator("bim.open_upstream", text="Homepage", icon="HOME") + op.page = "home" + op = row.operator("bim.open_upstream", text="Docs", icon="DOCUMENTS") + op.page = "docs" + op = row.operator("bim.open_upstream", text="Wiki", icon="CURRENT_FILE") + op.page = "wiki" + op = row.operator("bim.open_upstream", text="Community", icon="COMMUNITY") + op.page = "community" + layout.row().separator() + + def draw_lines(self, layout, lines): + box = layout_with_margins(layout).box() + for line in lines: + row = box.row() + row.label(text=f" {line}") + + @property + def message_summary(self): + return [ + 'The Construction Type Browser allows to preview and add new instances to the model.', + 'For further support, please click on the Documentation link below.' + ] + + class AlignProduct(bpy.types.Operator): bl_idname = "bim.align_product" bl_label = "Align Product" diff --git a/src/blenderbim/blenderbim/bim/module/model/profile.py b/src/blenderbim/blenderbim/bim/module/model/profile.py index 1c3e6bbe26..b074ef6dac 100644 --- a/src/blenderbim/blenderbim/bim/module/model/profile.py +++ b/src/blenderbim/blenderbim/bim/module/model/profile.py @@ -162,7 +162,15 @@ class DumbProfileGenerator: pset = ifcopenshell.api.run("pset.add_pset", self.file, product=element, name="EPset_Parametric") ifcopenshell.api.run("pset.edit_pset", self.file, pset=pset, properties={"Engine": "BlenderBIM.DumbProfile"}) MaterialData.load(self.file) - obj.select_set(True) + try: + obj.select_set(True) + except RuntimeError: + def msg(self, context): + txt = "The created object could not be assigned to a collection. " + txt += "Has any IfcSpatialElement been deleted?" + self.layout.label(text=txt) + + bpy.context.window_manager.popup_menu(msg, title="Error", icon="ERROR") return obj diff --git a/src/blenderbim/blenderbim/bim/module/model/prop.py b/src/blenderbim/blenderbim/bim/module/model/prop.py index 1cbe8eb3df..9d6b7b3794 100644 --- a/src/blenderbim/blenderbim/bim/module/model/prop.py +++ b/src/blenderbim/blenderbim/bim/module/model/prop.py @@ -17,21 +17,8 @@ # along with BlenderBIM Add-on. If not, see . import bpy -import ifcopenshell.util.type from blenderbim.bim.module.model.data import AuthoringData -from blenderbim.bim.prop import StrProperty, Attribute -from blenderbim.bim.ifc import IfcStore from bpy.types import PropertyGroup -from bpy.props import ( - PointerProperty, - StringProperty, - EnumProperty, - BoolProperty, - IntProperty, - FloatProperty, - FloatVectorProperty, - CollectionProperty, -) def get_ifc_class(self, context): @@ -43,18 +30,110 @@ def get_ifc_class(self, context): def get_relating_type(self, context): if not AuthoringData.is_loaded: AuthoringData.load() - return AuthoringData.data["relating_types"] + return AuthoringData.data["relating_types_ids"] + + +def get_relating_type_browser(self, context): + if not AuthoringData.is_loaded: + AuthoringData.load() + return AuthoringData.data["relating_types_ids_browser"] + + +def update_icon_id(self, context): + ifc_class_browser = self.ifc_class_browser + relating_type_id_browser = self.relating_type_id_browser + relating_type_browser = AuthoringData.relating_type_name_by_id(ifc_class_browser, relating_type_id_browser) + if ((ifc_class_browser not in AuthoringData.data["preview_constr_types"] + or relating_type_id_browser not in AuthoringData.data["preview_constr_types"][ifc_class_browser]) + and relating_type_browser is not None): + if not AuthoringData.assetize_relating_type_from_selection(): + return + self.icon_id = AuthoringData.data["preview_constr_types"][ifc_class_browser][relating_type_id_browser]["icon_id"] def update_ifc_class(self, context): - AuthoringData.is_loaded = False + AuthoringData.load_ifc_classes() + AuthoringData.load_relating_types() + self.relating_type_id = AuthoringData.data["relating_types_ids"][0][0] + + +def update_ifc_class_browser(self, context): + AuthoringData.load_ifc_classes() + AuthoringData.load_relating_types_browser() + props = context.scene.BIMModelProperties + if props.unfold_relating_types: + ifc_class_browser = props.ifc_class_browser + relating_type_info = AuthoringData.relating_type_info(ifc_class_browser) + if relating_type_info is None or not relating_type_info.fully_loaded: + curr_selection = props.ifc_class, props.relating_type_id + AuthoringData.assetize_constr_class(ifc_class_browser) + props.ifc_class, props.relating_type_id = curr_selection + else: + self.relating_type_id_browser = AuthoringData.data["relating_types_ids_browser"][0][0] + + +def update_relating_type(self, context): + AuthoringData.load_relating_types() + + +def update_relating_type_by_name(self, context): + AuthoringData.load_relating_types() + relating_type_id = AuthoringData.relating_type_id_by_name(self.ifc_class, self.relating_type) + if relating_type_id is not None: + self.relating_type_id = relating_type_id + + +def update_relating_type_browser_by_name(self, context): + AuthoringData.load_relating_types_browser() + relating_type_id_browser = AuthoringData.relating_type_id_by_name(self.ifc_class_browser, self.relating_type_browser) + if relating_type_id_browser is not None: + self.relating_type_id_browser = relating_type_id_browser + + +def update_relating_type_browser(self, context): + AuthoringData.load_relating_types_browser() + update_icon_id(self, context) + + +def update_unfold_relating_type(self, context): + update_ifc_class_browser(self, context) class BIMModelProperties(PropertyGroup): - ifc_class: bpy.props.EnumProperty(items=get_ifc_class, name="IFC Class", update=update_ifc_class) - relating_type: bpy.props.EnumProperty(items=get_relating_type, name="Relating Type") + ifc_class: bpy.props.EnumProperty(items=get_ifc_class, name="Construction Class", update=update_ifc_class) + ifc_class_browser: bpy.props.EnumProperty( + items=get_ifc_class, name="Construction Class", update=update_ifc_class_browser + ) + relating_type: bpy.props.StringProperty(update=update_relating_type_by_name) + relating_type_id: bpy.props.EnumProperty( + items=get_relating_type, name="Construction Type", update=update_relating_type + ) + relating_type_browser: bpy.props.StringProperty(update=update_relating_type_browser_by_name) + relating_type_id_browser: bpy.props.EnumProperty( + items=get_relating_type_browser, name="Construction Type", update=update_relating_type_browser + ) + icon_id: bpy.props.IntProperty() + unfold_relating_types: bpy.props.BoolProperty(update=update_unfold_relating_type) occurrence_name_style: bpy.props.EnumProperty( items=[("CLASS", "By Class", ""), ("TYPE", "By Type", ""), ("CUSTOM", "Custom", "")], name="Occurrence Name Style", ) occurrence_name_function: bpy.props.StringProperty(name="Occurrence Name Function") + getter_enum = { + "ifc_class_browser": get_ifc_class, + "relating_type_browser": get_relating_type_browser + } + + +def get_relating_type_info(self, context): + return AuthoringData.relating_types(ifc_class=self.name) + + +class ConstrTypeInfo(PropertyGroup): + name: bpy.props.StringProperty(name="Construction class") + relating_type: bpy.props.EnumProperty( + name="Construction type", items=get_relating_type_info + ) + fully_loaded: bpy.props.BoolProperty(default=False) + + diff --git a/src/blenderbim/blenderbim/bim/module/model/slab.py b/src/blenderbim/blenderbim/bim/module/model/slab.py index 622f447e83..6a96490d18 100644 --- a/src/blenderbim/blenderbim/bim/module/model/slab.py +++ b/src/blenderbim/blenderbim/bim/module/model/slab.py @@ -166,7 +166,7 @@ def calculate_quantities(usecase_path, ifc_file, settings): obj = settings["blender_object"] product = ifc_file.by_id(obj.BIMObjectProperties.ifc_definition_id) parametric = ifcopenshell.util.element.get_psets(product).get("EPset_Parametric") - if not parametric or parametric["Engine"] != "BlenderBIM.DumbLayer3": + if not parametric or "Engine" not in parametric or parametric["Engine"] != "BlenderBIM.DumbLayer3": return qto = ifcopenshell.api.run( "pset.add_qto", ifc_file, should_run_listeners=False, product=product, name="Qto_SlabBaseQuantities" @@ -306,7 +306,15 @@ class DumbSlabGenerator: pset = ifcopenshell.api.run("pset.add_pset", self.file, product=element, name="EPset_Parametric") ifcopenshell.api.run("pset.edit_pset", self.file, pset=pset, properties={"Engine": "BlenderBIM.DumbLayer3"}) MaterialData.load(self.file) - obj.select_set(True) + try: + obj.select_set(True) + except RuntimeError: + def msg(self, context): + txt = "The created object could not be assigned to a collection. " + txt += "Has any IfcSpatialElement been deleted?" + self.layout.label(text=txt) + + bpy.context.window_manager.popup_menu(msg, title="Error", icon="ERROR") return obj @@ -342,6 +350,23 @@ class DumbSlabPlaner: return new_thickness = sum([l.LayerThickness for l in new_material.MaterialLayers]) material = ifcopenshell.util.element.get_material(settings["related_object"]) + + relating_type = settings["relating_type"] + if hasattr(relating_type, "HasPropertySets"): + psets = relating_type.HasPropertySets + if psets is not None: + for pset in psets: + if hasattr(pset, "HasProperties"): + pset_props = pset.HasProperties + if pset_props is not None: + for prop in pset_props: + if prop.Name == "LayerSetDirection": + if hasattr(prop, "NominalValue"): + nominal_value = prop.NominalValue + if hasattr(nominal_value, "wrappedValue"): + if nominal_value.wrappedValue == "AXIS2": + return + if material and material.is_a("IfcMaterialLayerSetUsage") and material.LayerSetDirection == "AXIS3": self.change_thickness(settings["related_object"], new_thickness) diff --git a/src/blenderbim/blenderbim/bim/module/model/ui.py b/src/blenderbim/blenderbim/bim/module/model/ui.py index 8a198ce7a1..dcdcf6655d 100644 --- a/src/blenderbim/blenderbim/bim/module/model/ui.py +++ b/src/blenderbim/blenderbim/bim/module/model/ui.py @@ -16,54 +16,15 @@ # You should have received a copy of the GNU General Public License # along with BlenderBIM Add-on. If not, see . -import blenderbim.bim.module.type.prop as type_prop from bpy.types import Panel -from blenderbim.bim.ifc import IfcStore -from blenderbim.bim.module.model.data import AuthoringData class BIM_PT_authoring(Panel): - bl_idname = "BIM_PT_authoring" - bl_label = "Authoring" - bl_space_type = "VIEW_3D" - bl_region_type = "UI" - bl_category = "BlenderBIM" - - @classmethod - def poll(cls, context): - return IfcStore.get_file() - - def draw(self, context): - if not AuthoringData.is_loaded: - AuthoringData.load() - - props = context.scene.BIMModelProperties - col = self.layout.column(align=True) - enabled = True - - if AuthoringData.data["ifc_classes"]: - col.prop(props, "ifc_class", text="", icon="FILE_VOLUME") - else: - col.label(text="No IFC Class", icon="FILE_VOLUME") - enabled = False - if AuthoringData.data["relating_types"]: - col.prop(props, "relating_type", text="", icon="FILE_3D") - else: - col.label(text="No Relating Type", icon="FILE_3D") - enabled = False - row = col.row() - row.operator("bim.add_type_instance", icon="ADD") - row.enabled = enabled - - -class BIM_PT_authoring_architectural(Panel): bl_label = "Architectural" - bl_idname = "BIM_PT_authoring_architectural" - bl_options = {"DEFAULT_CLOSED"} + bl_idname = "BIM_PT_authoring" bl_space_type = "VIEW_3D" bl_region_type = "UI" bl_category = "BlenderBIM" - bl_parent_id = "BIM_PT_authoring" def draw(self, context): row = self.layout.row(align=True) diff --git a/src/blenderbim/blenderbim/bim/module/model/wall.py b/src/blenderbim/blenderbim/bim/module/model/wall.py index 368c907350..b3662208c1 100644 --- a/src/blenderbim/blenderbim/bim/module/model/wall.py +++ b/src/blenderbim/blenderbim/bim/module/model/wall.py @@ -817,7 +817,15 @@ class DumbWallGenerator: pset = ifcopenshell.api.run("pset.add_pset", self.file, product=element, name="EPset_Parametric") ifcopenshell.api.run("pset.edit_pset", self.file, pset=pset, properties={"Engine": "BlenderBIM.DumbLayer2"}) MaterialData.load(self.file) - obj.select_set(True) + try: + obj.select_set(True) + except RuntimeError: + def msg(self, context): + txt = "The created object could not be assigned to a collection. " + txt += "Has any IfcSpatialElement been deleted?" + self.layout.label(text=txt) + + bpy.context.window_manager.popup_menu(msg, title="Error", icon="ERROR") return obj @@ -861,7 +869,7 @@ def calculate_quantities(usecase_path, ifc_file, settings): obj = settings["blender_object"] product = ifc_file.by_id(obj.BIMObjectProperties.ifc_definition_id) parametric = ifcopenshell.util.element.get_psets(product).get("EPset_Parametric") - if not parametric or parametric["Engine"] != "BlenderBIM.DumbLayer2": + if not parametric or "Engine" not in parametric or parametric["Engine"] != "BlenderBIM.DumbLayer2": return qto = ifcopenshell.api.run( "pset.add_qto", ifc_file, should_run_listeners=False, product=product, name="Qto_WallBaseQuantities" @@ -941,6 +949,23 @@ class DumbWallPlaner: return new_thickness = sum([l.LayerThickness for l in new_material.MaterialLayers]) material = ifcopenshell.util.element.get_material(settings["related_object"]) + + relating_type = settings["relating_type"] + if hasattr(relating_type, "HasPropertySets"): + psets = relating_type.HasPropertySets + if psets is not None: + for pset in psets: + if hasattr(pset, "HasProperties"): + pset_props = pset.HasProperties + if pset_props is not None: + for prop in pset_props: + if prop.Name == "LayerSetDirection": + if hasattr(prop, "NominalValue"): + nominal_value = prop.NominalValue + if hasattr(nominal_value, "wrappedValue"): + if nominal_value.wrappedValue == "AXIS3": + return + if material and material.is_a("IfcMaterialLayerSetUsage") and material.LayerSetDirection == "AXIS2": self.change_thickness(settings["related_object"], new_thickness) diff --git a/src/blenderbim/blenderbim/bim/module/model/workspace.py b/src/blenderbim/blenderbim/bim/module/model/workspace.py index 20239c2855..d7be1ebe61 100644 --- a/src/blenderbim/blenderbim/bim/module/model/workspace.py +++ b/src/blenderbim/blenderbim/bim/module/model/workspace.py @@ -55,28 +55,56 @@ class BimTool(WorkSpaceTool): if not AuthoringData.is_loaded and IfcStore.get_file(): AuthoringData.load() + props = context.scene.BIMModelProperties + is_tool_header = context.region.type == "TOOL_HEADER" row = layout.row(align=True) if not IfcStore.get_file(): row.label(text="No IFC Project", icon="ERROR") return - props = context.scene.BIMModelProperties - if AuthoringData.data["ifc_classes"]: - row.prop(props, "ifc_class", text="") - else: - row.label(text="No IFC Class") - if AuthoringData.data["relating_types"]: - row.prop(props, "relating_type", text="") - else: - row.label(text="No Relating Type") - row.label(text="", icon="BLANK1") + ifc_classes = AuthoringData.data["ifc_classes"] + relating_types_ids = AuthoringData.data["relating_types_ids"] - row = layout.row(align=True) - row.label(text="", icon="EVENT_SHIFT") - row.label(text="Add Type Instance", icon="EVENT_A") + if is_tool_header: + row.operator("bim.help_relating_types", text="", icon="QUESTION") + + if ifc_classes and is_tool_header: + row.label(text="", icon="BLANK1") + row.operator("bim.display_relating_types", icon="COLLAPSEMENU") + + ifc_class = props.ifc_class + relating_type_id = props.relating_type_id + relating_type = AuthoringData.relating_type_name_by_id(ifc_class, relating_type_id) + + if is_tool_header: + row.label(text="", icon="BLANK1") + row = layout.row(align=True) + row.label(text="", icon="EVENT_SHIFT") + row.label(text="", icon="EVENT_A") + if ifc_classes: + row.label(text=f" Add") + row.label(text="", icon="FILE_VOLUME") + row.label(text=ifc_class) + row.label(text="", icon="FILE_3D") + row.label(text=f"{relating_type} ") + else: + row.label(text=f" Add instance") + else: + txt_ifc_class = ifc_class if ifc_classes else "No Construction Class" + txt_relating_type = relating_type if relating_types_ids else "No Construction Type" + row = layout.row(align=True) + row.label(text="Selected Construction Type:") + row = layout.row(align=True) + row.label(text=txt_ifc_class, icon="FILE_VOLUME") + row = layout.row(align=True) + row.label(text=txt_relating_type, icon="FILE_3D") + row = layout.row(align=True) + row.label(text="", icon="EVENT_SHIFT") + row.label(text="", icon="EVENT_A") + row.label(text=f" Add Type Instance") if AuthoringData.data["ifc_classes"]: - if props.ifc_class == "IfcWallType": + if ifc_class == "IfcWallType": row = layout.row() row.label(text="Join") row = layout.row(align=True) @@ -98,7 +126,7 @@ class BimTool(WorkSpaceTool): row.label(text="", icon="EVENT_SHIFT") row.label(text="Split", icon="EVENT_S") - if props.ifc_class in ("IfcColumnType", "IfcBeamType", "IfcMemberType"): + if ifc_class in ("IfcColumnType", "IfcBeamType", "IfcMemberType"): row = layout.row() row.label(text="Join") row = layout.row(align=True) @@ -156,7 +184,7 @@ class Hotkey(bpy.types.Operator): return {"FINISHED"} def hotkey_S_A(self): - bpy.ops.bim.add_type_instance() + bpy.ops.bim.add_constr_type_instance() def hotkey_S_C(self): if self.has_ifc_class and self.props.ifc_class == "IfcWallType": diff --git a/src/blenderbim/blenderbim/bim/module/type/ui.py b/src/blenderbim/blenderbim/bim/module/type/ui.py index 95256e99c7..58cebe2c14 100644 --- a/src/blenderbim/blenderbim/bim/module/type/ui.py +++ b/src/blenderbim/blenderbim/bim/module/type/ui.py @@ -88,4 +88,4 @@ class BIM_PT_type(Panel): def add_object_button(self, context): - self.layout.operator("bim.add_type_instance", icon="PLUGIN") + self.layout.operator("bim.add_constr_type_instance", icon="PLUGIN") diff --git a/src/blenderbim/blenderbim/tool/collector.py b/src/blenderbim/blenderbim/tool/collector.py index e202ab50cb..8b08e14f0b 100644 --- a/src/blenderbim/blenderbim/tool/collector.py +++ b/src/blenderbim/blenderbim/tool/collector.py @@ -82,7 +82,8 @@ class Collector(blenderbim.core.tool.Collector): if obj.users_collection != (object_collection,): for collection in obj.users_collection: collection.objects.unlink(obj) - object_collection.objects.link(obj) + if object_collection is not None: + object_collection.objects.link(obj) if collection_collection and collection_collection.children.find(object_collection.name) == -1: if bpy.context.scene.collection.children.find(object_collection.name) != -1: diff --git a/src/blenderbim/test/bim/feature/geometry.feature b/src/blenderbim/test/bim/feature/geometry.feature index dae44cff62..ee0cdedc39 100644 --- a/src/blenderbim/test/bim/feature/geometry.feature +++ b/src/blenderbim/test/bim/feature/geometry.feature @@ -31,8 +31,8 @@ Scenario: Add representation - add a new representation to a typed instance And I set "scene.BIMRootProperties.ifc_product" to "IfcElementType" And I set "scene.BIMRootProperties.ifc_class" to "IfcWallType" And I press "bim.assign_class" - And I press "bim.add_type_instance" - And I press "bim.add_type_instance" + And I press "bim.add_constr_type_instance" + And I press "bim.add_constr_type_instance" Then the object "IfcWall/Wall" data is a "Tessellation" representation of "Model/Body/MODEL_VIEW" And the object "IfcWall/Wall.001" data is a "Tessellation" representation of "Model/Body/MODEL_VIEW" When the object "IfcWall/Wall" is selected @@ -146,9 +146,9 @@ Scenario: Remove representation - remove an instanced representation from an act And I press "bim.assign_class" And I set "scene.BIMModelProperties.ifc_class" to "IfcWallType" And the variable "cube" is "{ifc}.by_type('IfcWallType')[0].id()" - And I set "scene.BIMModelProperties.relating_type" to "{cube}" - And I press "bim.add_type_instance" - And I press "bim.add_type_instance" + And I set "scene.BIMModelProperties.relating_type_id" to "{cube}" + And I press "bim.add_constr_type_instance" + And I press "bim.add_constr_type_instance" And the object "IfcWallType/Cube" is selected When the variable "representation" is "{ifc}.by_type('IfcWallType')[0].RepresentationMaps[1].MappedRepresentation.id()" And I press "bim.remove_representation(representation_id={representation})" @@ -165,9 +165,9 @@ Scenario: Remove representation - remove an instanced representation from an act And I press "bim.assign_class" And I set "scene.BIMModelProperties.ifc_class" to "IfcWallType" And the variable "cube" is "{ifc}.by_type('IfcWallType')[0].id()" - And I set "scene.BIMModelProperties.relating_type" to "{cube}" - And I press "bim.add_type_instance" - And I press "bim.add_type_instance" + And I set "scene.BIMModelProperties.relating_type_id" to "{cube}" + And I press "bim.add_constr_type_instance" + And I press "bim.add_constr_type_instance" And the object "IfcWall/Wall" is selected When the variable "representation" is "{ifc}.by_type('IfcWall')[0].Representation.Representations[1].id()" And I press "bim.remove_representation(representation_id={representation})" @@ -339,8 +339,8 @@ Scenario: Override duplicate move - copying a type instance with a representatio And I press "bim.assign_class" And I set "scene.BIMModelProperties.ifc_class" to "IfcWallType" And the variable "cube" is "{ifc}.by_type('IfcWallType')[0].id()" - And I set "scene.BIMModelProperties.relating_type" to "{cube}" - And I press "bim.add_type_instance" + And I set "scene.BIMModelProperties.relating_type_id" to "{cube}" + And I press "bim.add_constr_type_instance" And the object "IfcWall/Wall" is selected When I press "object.duplicate_move" Then the object "IfcWall/Wall.001" exists diff --git a/src/blenderbim/test/bim/feature/model.feature b/src/blenderbim/test/bim/feature/model.feature index 49b85e9641..cd950f888d 100644 --- a/src/blenderbim/test/bim/feature/model.feature +++ b/src/blenderbim/test/bim/feature/model.feature @@ -10,8 +10,8 @@ Scenario: Add type instance - add from a mesh And I press "bim.assign_class" And I set "scene.BIMModelProperties.ifc_class" to "IfcWallType" And the variable "cube" is "{ifc}.by_type('IfcWallType')[0].id()" - And I set "scene.BIMModelProperties.relating_type" to "{cube}" - When I press "bim.add_type_instance" + And I set "scene.BIMModelProperties.relating_type_id" to "{cube}" + When I press "bim.add_constr_type_instance" Then the object "IfcWall/Wall" exists Scenario: Add type instance - add from an empty @@ -23,8 +23,8 @@ Scenario: Add type instance - add from an empty And I press "bim.assign_class" And I set "scene.BIMModelProperties.ifc_class" to "IfcWallType" And the variable "empty" is "{ifc}.by_type('IfcWallType')[0].id()" - And I set "scene.BIMModelProperties.relating_type" to "{empty}" - When I press "bim.add_type_instance" + And I set "scene.BIMModelProperties.relating_type_id" to "{empty}" + When I press "bim.add_constr_type_instance" Then the object "IfcWall/Wall" exists Scenario: Add type instance - add a mesh where existing instances have changed context @@ -36,18 +36,51 @@ Scenario: Add type instance - add a mesh where existing instances have changed c And I press "bim.assign_class" And I set "scene.BIMModelProperties.ifc_class" to "IfcWallType" And the variable "cube" is "{ifc}.by_type('IfcWallType')[0].id()" - And I set "scene.BIMModelProperties.relating_type" to "{cube}" - And I press "bim.add_type_instance" + And I set "scene.BIMModelProperties.relating_type_id" to "{cube}" + And I press "bim.add_constr_type_instance" And the object "IfcWall/Wall" data is a "Tessellation" representation of "Model/Body/MODEL_VIEW" And the object "IfcWall/Wall" is selected And the variable "context" is "[c for c in {ifc}.by_type('IfcGeometricRepresentationSubContext') if c.TargetView == 'PLAN_VIEW'][0].id()" And I set "scene.BIMRootProperties.contexts" to "{context}" And I press "bim.add_representation" And the object "IfcWall/Wall" data is a "Annotation2D" representation of "Plan/Annotation/PLAN_VIEW" - When I press "bim.add_type_instance" + When I press "bim.add_constr_type_instance" Then the object "IfcWall/Wall" data is a "Annotation2D" representation of "Plan/Annotation/PLAN_VIEW" And the object "IfcWall/Wall.001" data is a "Annotation2D" representation of "Plan/Annotation/PLAN_VIEW" +Scenario: Preview one type on the Construction Type Browser + Given an empty IFC project + And I load the demo construction library + When I display the construction type browser + And I preview only one asset on the construction type browser + And I set "scene.BIMModelProperties.ifc_class_browser" to "IfcColumnType" + And I set "scene.BIMModelProperties.relating_type_browser" to "DEMO2" + And I select the browser construction type + Then "scene.BIMModelProperties.ifc_class" is "IfcColumnType" + And construction type is DEMO2 + And objects starting with "IfcColumn/" do not exist + And the construction type "IfcColumnType"/"DEMO2" has a preview + +Scenario: Preview one class on the construction type browser + Given an empty IFC project + And I load the demo construction library + When I display the construction type browser + And I preview all available assets on the construction type browser + And I set "scene.BIMModelProperties.ifc_class_browser" to "IfcWallType" + Then "scene.BIMModelProperties.ifc_class_browser" is "IfcWallType" + And objects starting with "IfcWall/" do not exist + And all construction types for "IfcWallType" have a preview + +Scenario: Add one type from the Construction Type Browser + Given an empty IFC project + And I load the demo construction library + When I display the construction type browser + And I preview only one asset on the construction type browser + And I set "scene.BIMModelProperties.ifc_class_browser" to "IfcColumnType" + And I set "scene.BIMModelProperties.relating_type_browser" to "DEMO2" + And I add the browser construction type + Then the object "IfcColumn/Column" exists + Scenario: Add grid Given an empty IFC project When I press "mesh.add_grid" diff --git a/src/blenderbim/test/bim/test_feature.py b/src/blenderbim/test/bim/test_feature.py index 579930012f..d964e883b9 100644 --- a/src/blenderbim/test/bim/test_feature.py +++ b/src/blenderbim/test/bim/test_feature.py @@ -23,6 +23,7 @@ import ifcopenshell import blenderbim.tool as tool import blenderbim.bim from blenderbim.bim.ifc import IfcStore +from blenderbim.bim.module.model.data import AuthoringData from pytest_bdd import scenarios, given, when, then, parsers from mathutils import Vector @@ -108,6 +109,7 @@ def i_add_a_new_collection_item(collection): assert False, "Collection does not exist" + @given(parsers.parse('the material "{name}" colour is set to "{colour}"')) @when(parsers.parse('the material "{name}" colour is set to "{colour}"')) def the_material_name_colour_is_set_to_colour(name, colour): @@ -507,7 +509,16 @@ def the_collection_name1_is_in_the_collection_name2(name1, name2): @then(parsers.parse('the object "{name}" does not exist')) def the_object_name_does_not_exist(name): - assert bpy.data.objects.get(name) is None, "Object exists" + obj = bpy.data.objects.get(name) + assert obj is None or len(obj.users_collection) == 0, "Object exists" + + +@then(parsers.parse('objects starting with "{name}" do not exist')) +def objects_not_exist_starting_with(name): + objs = [obj for obj in bpy.data.objects if obj.name.startswith(name) and len(obj.users_collection) > 0] + if len(objs) > 0: + assert False, f'{len(objs)} objects starting with "{name}" exist' + assert True @then(parsers.parse('the object "{name}" is at "{location}"')) @@ -562,3 +573,108 @@ def the_file_name_should_contain_value(name, value): @then(parsers.parse('the object "{name}" has no modifiers')) def the_object_name_has_no_modifiers(name): assert len(the_object_name_exists(name).modifiers) == 0 + + +@then(parsers.parse('the construction type "{ifc_class}"/"{relating_type}" has a preview')) +def the_construction_type_has_a_preview(ifc_class, relating_type): + if "preview_constr_types" not in AuthoringData.data: + assert False, 'There are no previews loaded' + preview_constr_types = AuthoringData.data["preview_constr_types"] + if ifc_class not in preview_constr_types: + assert False, f'Construction class {ifc_class} has no available previews' + relating_type_id = AuthoringData.relating_type_id_by_name(ifc_class, relating_type) + if relating_type_id is None: + assert False, f'No construction type {ifc_class}/{relating_type} was found' + if relating_type_id not in preview_constr_types[ifc_class]: + assert False, f'Construction type {ifc_class}/{relating_type} has no available previews' + preview_data = preview_constr_types[ifc_class][relating_type_id] + if 'icon_id' not in preview_data: + assert False, f'Construction type {ifc_class}/{relating_type} has a preview, but no assigned icon_id' + icon_id = preview_data["icon_id"] + if not isinstance(icon_id, int): + assert False, f'Construction type {ifc_class}/{relating_type} has an invalid icon_id {icon_id}' + # Note: icon_id must be > 0 in UI mode, but asset_generate_preview() doesn't work headlessly -> skipping for now + # if icon_id == 0: + # assert False, f'Construction type {ifc_class}/{relating_type} has the default null value for icon_id' + assert True + + +@then("there is a Construction Type preview") +def there_is_a_construction_type_preview(): + props = bpy.context.scene.BIMModelProperties + assert props.icon_id > 0, f"There isn't a Construction Type preview" + + +@then(parsers.parse('all construction types for "{ifc_class}" have a preview')) +def all_construction_types_have_a_preview(ifc_class): + if "preview_constr_types" not in AuthoringData.data: + assert False, 'There are no previews loaded' + preview_constr_types = AuthoringData.data["preview_constr_types"] + if ifc_class not in preview_constr_types: + assert False, f'Construction class {ifc_class} has no available previews' + constr_class_occurrences = AuthoringData.constr_class_entities(ifc_class) + for constr_class_entity in constr_class_occurrences: + the_construction_type_has_a_preview(ifc_class, constr_class_entity.Name) + + +@given("I load the demo construction library") +@when("I load the demo construction library") +def i_add_a_construction_library(): + lib_path = './blenderbim/bim/data/libraries/IFC4 Demo Library.ifc' + bpy.ops.bim.select_library_file(filepath=lib_path, append_all=True) + + +@given("I display the construction type browser") +@when("I display the construction type browser") +def i_display_the_construction_type_browser(): + bpy.ops.bim.display_relating_types('INVOKE_DEFAULT') + + +@given("I preview only one asset on the construction type browser") +@when("I preview only one asset on the construction type browser") +def i_preview_one_construction_type(): + bpy.context.scene.BIMModelProperties.unfold_relating_types = False + + +@given("I preview all available assets on the construction type browser") +@when("I preview all available assets on the construction type browser") +def i_preview_all_construction_types(): + bpy.context.scene.BIMModelProperties.unfold_relating_types = True + + +@given("I select the browser construction type") +@when("I select the browser construction type") +def i_select_the_active_construction_type(): + props = bpy.context.scene.BIMModelProperties + bpy.ops.bim.select_construction_type( + ifc_class=props.ifc_class_browser, relating_type_id=props.relating_type_id_browser + ) + + +@given("I add the browser construction type") +@when("I add the browser construction type") +def i_add_the_active_construction_type(): + props = bpy.context.scene.BIMModelProperties + bpy.ops.bim.add_constr_type_instance( + ifc_class=props.ifc_class_browser, relating_type_id=int(props.relating_type_id_browser) + ) + + +@then(parsers.parse("browser construction type is {relating_type_name}")) +def browser_construction_type(relating_type_name): + props = bpy.context.scene.BIMModelProperties + relating_type_browser = AuthoringData.relating_type_name_by_id(props.ifc_class_browser, props.relating_type_id_browser) + assert relating_type_browser == relating_type_name, (f"Construction Type is a {relating_type_browser}, not " + + f"a {relating_type_name}") + + +@then(parsers.parse("construction type is {relating_type_name}")) +def construction_type(relating_type_name): + props = bpy.context.scene.BIMModelProperties + relating_type = AuthoringData.relating_type_name_by_id(props.ifc_class, props.relating_type_id) + assert relating_type == relating_type_name, f"Construction Type is a {relating_type}, not a {relating_type_name}" + + +@when("I move the cursor to the bottom left corner") +def move_cursor_bottom_left(): + bpy.context.window.cursor_warp(10, 10) diff --git a/src/ifcopenshell-python/test/Sample-BIM-Files b/src/ifcopenshell-python/test/Sample-BIM-Files index adadc7740b..2daf6c46ed 160000 --- a/src/ifcopenshell-python/test/Sample-BIM-Files +++ b/src/ifcopenshell-python/test/Sample-BIM-Files @@ -1 +1 @@ -Subproject commit adadc7740b7337f4cb0935ec8e84ba30ed4ca10d +Subproject commit 2daf6c46ed9fc7a17259ef6d4c17c3553ef642cd