diff --git a/src/bonsai/bonsai/bim/module/model/data.py b/src/bonsai/bonsai/bim/module/model/data.py index bfe7c729e2..df2e66dc7c 100644 --- a/src/bonsai/bonsai/bim/module/model/data.py +++ b/src/bonsai/bonsai/bim/module/model/data.py @@ -57,20 +57,23 @@ class AuthoringData: cls.data["ifc_element_type"] = cls.ifc_element_type cls.data["ifc_classes"] = cls.ifc_classes() cls.data["ifc_class_current"] = cls.ifc_class_current() - cls.data["relating_type_id"] = cls.relating_type_id() # only after .ifc_classes() - cls.data["relating_type_id_current"] = cls.relating_type_id_current() # only after .ifc_classes() - cls.data["relating_type_name"] = cls.relating_type_name() # only after .relating_type_id() - cls.data["relating_type_description"] = cls.relating_type_description() # only after .relating_type_id() - cls.data["predefined_type"] = cls.predefined_type() # only after .relating_type_id() - - # Only after .ifc_classes() + # Make sure .ifc_classes() was run before next lines + cls.data["type_elements"] = cls.type_elements() + cls.data["type_elements_filtered"] = cls.type_elements_filtered() + cls.data["relating_type_id"] = cls.relating_type_id() + # Make sure .relating_type_id() was run before next lines + cls.data["relating_type_id_current"] = cls.relating_type_id_current() + cls.data["relating_type_name"] = cls.relating_type_name() + cls.data["relating_type_description"] = cls.relating_type_description() + cls.data["predefined_type"] = cls.predefined_type() + # Make sure .type_elements_filtered() was run before next lines cls.data["total_types"] = cls.total_types() - cls.data["total_pages"] = cls.total_pages() + cls.data["total_pages"] = cls.total_pages() # Only after .total_types() cls.data["next_page"] = cls.next_page() cls.data["prev_page"] = cls.prev_page() cls.data["paginated_relating_types"] = cls.paginated_relating_types() - cls.data["type_thumbnail"] = cls.type_thumbnail() # Only after .relating_type_id() + cls.data["type_thumbnail"] = cls.type_thumbnail() # Only after .relating_type_id_current() cls.data["is_voidable_element"] = cls.is_voidable_element() cls.data["has_visible_openings"] = cls.has_visible_openings() cls.data["has_visible_boundaries"] = cls.has_visible_boundaries() @@ -104,23 +107,15 @@ class AuthoringData: @classmethod def type_thumbnail(cls): - if not cls.data["relating_type_id"]: - return 0 - relating_type_id = tool.Blender.get_enum_safe(cls.props, "relating_type_id") - if relating_type_id is None: - return 0 - element = tool.Ifc.get().by_id(int(relating_type_id)) - return cls.type_thumbnails.get(element.id(), None) or 0 + return cls.type_thumbnails.get(int(cls.data["relating_type_id_current"] or 0), 0) @classmethod def total_types(cls): - ifc_class = cls.data["ifc_class_current"] - return len(tool.Ifc.get().by_type(ifc_class)) if ifc_class else 0 + return len(cls.data["type_elements_filtered"]) @classmethod def total_pages(cls): - ifc_class = cls.data["ifc_class_current"] - total_types = len(tool.Ifc.get().by_type(ifc_class)) if ifc_class else 0 + total_types = cls.data["total_types"] return math.ceil(total_types / cls.types_per_page) @classmethod @@ -134,12 +129,35 @@ class AuthoringData: return cls.props.type_page - 1 @classmethod - def paginated_relating_types(cls): + def type_elements(cls): ifc_class = cls.data["ifc_class_current"] if not ifc_class: return [] + elements = list(tool.Ifc.get().by_type(ifc_class)) + return natsorted(elements, key=lambda s: (s.Name or "Unnamed").lower()) + + @classmethod + def type_elements_filtered(cls): + search_query = cls.props.search_name.lower() + + def filter_element(element): + if search_query in (element.Name or "Unnamed").lower(): + return True + if search_query in (element.Description or "").lower(): + return True + if search_query in (ifcopenshell.util.element.get_predefined_type(element) or "").lower(): + return True + return False + + elements = cls.data["type_elements"] + if cls.props.search_name: + return [e for e in elements if filter_element(e)] + return elements + + @classmethod + def paginated_relating_types(cls): results = [] - elements = natsorted(tool.Ifc.get().by_type(ifc_class), key=lambda e: e.Name or "Unnamed") + elements = cls.data["type_elements_filtered"] elements = elements[(cls.props.type_page - 1) * cls.types_per_page : cls.props.type_page * cls.types_per_page] for element in elements: predefined_type = ifcopenshell.util.element.get_predefined_type(element) @@ -266,13 +284,8 @@ class AuthoringData: @classmethod def relating_type_id(cls): - results = [] - ifc_class = cls.data["ifc_class_current"] - if ifc_class: - elements = natsorted(tool.Ifc.get().by_type(ifc_class), key=lambda s: (s.Name or "Unnamed").lower()) - results.extend(elements) - return [(str(e.id()), e.Name or "Unnamed", e.Description or "") for e in results] - return [] + elements = cls.data["type_elements"] + return [(str(e.id()), e.Name or "Unnamed", e.Description or "") for e in elements] @classmethod def relating_type_id_current(cls): diff --git a/src/bonsai/bonsai/bim/module/model/product.py b/src/bonsai/bonsai/bim/module/model/product.py index b4bec77d48..b572376532 100644 --- a/src/bonsai/bonsai/bim/module/model/product.py +++ b/src/bonsai/bonsai/bim/module/model/product.py @@ -491,7 +491,6 @@ class SetActiveType(bpy.types.Operator, tool.Ifc.Operator): def _execute(self, context): props = context.scene.BIMModelProperties props.relating_type_id = str(self.relating_type) - context.window.screen = context.window.screen # Closes the type manager popup class AlignProduct(bpy.types.Operator): @@ -566,10 +565,11 @@ class LoadTypeThumbnails(bpy.types.Operator, tool.Ifc.Operator): return props = bpy.context.scene.BIMModelProperties - processing = set() # Only process at most one paginated class at a time. # Large projects have hundreds of types which can lead to unnecessary lag. - queue = sorted(tool.Ifc.get().by_type(self.ifc_class), key=lambda e: e.Name or "Unnamed") + if not AuthoringData.is_loaded: + AuthoringData.load() + queue = AuthoringData.data["type_elements_filtered"] if self.limit: queue = queue[self.offset : self.offset + self.limit] else: @@ -578,6 +578,12 @@ class LoadTypeThumbnails(bpy.types.Operator, tool.Ifc.Operator): offset = 0 queue = queue[offset : offset + 9] + # The active type may be in another page than the active one : + if relating_type_id_current := AuthoringData.data["relating_type_id_current"]: + active_element = tool.Ifc.get_entity_by_id(int(relating_type_id_current)) + if active_element and active_element not in queue: + queue.append(active_element) + while queue: # if bpy.app.is_job_running("RENDER_PREVIEW") does not seem to reflect asset preview generation element = queue.pop() diff --git a/src/bonsai/bonsai/bim/module/model/prop.py b/src/bonsai/bonsai/bim/module/model/prop.py index afc93db5d3..78ebe62751 100644 --- a/src/bonsai/bonsai/bim/module/model/prop.py +++ b/src/bonsai/bonsai/bim/module/model/prop.py @@ -43,6 +43,9 @@ def get_boundary_class(self, context): def get_relating_type_id(self, context): if not AuthoringData.is_loaded: AuthoringData.load() + else: + AuthoringData.data["type_elements"] = AuthoringData.type_elements() + AuthoringData.data["relating_type_id"] = AuthoringData.relating_type_id() return AuthoringData.data["relating_type_id"] @@ -65,10 +68,14 @@ def update_relating_type_id(self, context): AuthoringData.data["relating_type_name"] = AuthoringData.relating_type_name() AuthoringData.data["type_thumbnail"] = AuthoringData.type_thumbnail() AuthoringData.data["predefined_type"] = AuthoringData.predefined_type() + self.type_page = [e[0] for e in AuthoringData.data["relating_type_id"]].index(self.relating_type_id) // 9 + 1 def update_type_page(self, context): AuthoringData.data["paginated_relating_types"] = AuthoringData.paginated_relating_types() + bpy.ops.bim.load_type_thumbnails(ifc_class=self.ifc_class, offset=9 * (self.type_page - 1), limit=9) + self["type_page"] = min(self["type_page"], AuthoringData.data["total_pages"]) + self["type_page"] = max(self["type_page"], 1) def update_relating_array_from_object(self, context): @@ -97,6 +104,14 @@ def update_slab_direction_decorator(self, context): SlabDirectionDecorator.uninstall() +def update_search_name(self, context): + AuthoringData.load() + # Total number of pages may decrease when using the search bar : + if self.type_page > AuthoringData.data["total_pages"]: + self.type_page = max(1, AuthoringData.data["total_pages"]) + bpy.ops.bim.load_type_thumbnails(ifc_class=self.ifc_class) + + def update_x_angle(self, context): angle_deg = math.degrees(self.x_angle) if tool.Cad.is_x(angle_deg, -90, 0.5) or tool.Cad.is_x(angle_deg, 90, 0.5): @@ -108,6 +123,13 @@ class BIMModelProperties(PropertyGroup): relating_type_id: bpy.props.EnumProperty( items=get_relating_type_id, name="Relating Type", update=update_relating_type_id ) + search_name: bpy.props.StringProperty( + name="Search Name", + default="", + description="Use this property to filter the list of available types", + update=update_search_name, + options={"SKIP_SAVE", "TEXTEDIT_UPDATE"}, + ) menu_relating_type_id: bpy.props.IntProperty() icon_id: bpy.props.IntProperty() updating: bpy.props.BoolProperty(default=False) @@ -167,10 +189,10 @@ class BIMModelProperties(PropertyGroup): rl2: bpy.props.FloatProperty(name="RL", default=1, subtype="DISTANCE", description="Z offset for windows") # Used for plan calculation points such as in room generation rl3: bpy.props.FloatProperty(name="RL", default=1, subtype="DISTANCE", description="Z offset for space calculation") + type_page: bpy.props.IntProperty(name="Type Page", default=1, min=1, update=update_type_page) x_angle: bpy.props.FloatProperty( name="X Angle", default=0, subtype="ANGLE", min=math.radians(-180), max=math.radians(180), update=update_x_angle ) - type_page: bpy.props.IntProperty(name="Type Page", default=1, update=update_type_page) type_name: bpy.props.StringProperty(name="Name", default="TYPEX") boundary_class: bpy.props.EnumProperty(items=get_boundary_class, name="Boundary Class") direction_sense: bpy.props.EnumProperty( diff --git a/src/bonsai/bonsai/bim/module/model/ui.py b/src/bonsai/bonsai/bim/module/model/ui.py index 0563c03a5b..487711c30a 100644 --- a/src/bonsai/bonsai/bim/module/model/ui.py +++ b/src/bonsai/bonsai/bim/module/model/ui.py @@ -84,10 +84,9 @@ class LaunchTypeManager(bpy.types.Operator): bl_idname = "bim.launch_type_manager" bl_label = "Launch Type Manager" bl_options = {"REGISTER", "UNDO"} - bl_description = "Display all available Construction Types to add new instances" + bl_description = "Browse, Edit and Manage Types" def execute(self, context): - bpy.ops.bim.hotkey("INVOKE_DEFAULT", hotkey="S_A") return {"FINISHED"} def invoke(self, context, event): @@ -101,40 +100,42 @@ class LaunchTypeManager(bpy.types.Operator): # will be None if project has no types if ifc_class is not None: bpy.ops.bim.load_type_thumbnails(ifc_class=ifc_class, offset=0, limit=9) - return context.window_manager.invoke_props_dialog( - self, width=550, title="Type Manager", confirm_text="Add Type" - ) + return context.window_manager.invoke_props_dialog(self, width=550, title="Type Manager", confirm_text="Close") def draw(self, context): props = context.scene.BIMModelProperties row = self.layout.row(align=True) + text = f"{AuthoringData.data['total_types']} {AuthoringData.data['ifc_element_type']}" if AuthoringData.data["total_types"] > 1: - text = f"{AuthoringData.data['total_types']} {AuthoringData.data['ifc_element_type']}s" - else: - text = f"{AuthoringData.data['total_types']} {AuthoringData.data['ifc_element_type']}" + text += "s" row.label(text=text, icon="FILE_VOLUME") - # prop_with_search(row, props, "ifc_class", text="", should_click_ok_to_validate=True) row.menu("BIM_MT_type_manager_menu", text="", icon="PREFERENCES") + row = self.layout.row(align=True) row.operator("bim.launch_add_element", text=f"Create New {AuthoringData.data['ifc_element_type']}", icon="ADD") + row = self.layout.row(align=True) + # row.prop(props, "relating_type_id", text="") + row.prop(props, "search_name", icon="FILTER", text="") + columns = self.layout.column_flow(columns=3) - row = columns.row(align=True) - row.alignment = "CENTER" - ### In case you want something here in the future + if AuthoringData.data["total_pages"] > 0: + row = columns.row(align=True) + row.alignment = "RIGHT" + row2 = row.row(align=True) + row2.label(text="Page") + row2.prop(props, "type_page", text="", emboss=False) + row2.label(text=f"/{AuthoringData.data['total_pages']} ") - ### + prev_page_op = row.row(align=True) + op = prev_page_op.operator("bim.change_type_page", icon="TRIA_LEFT", text="") + op.page = AuthoringData.data["prev_page"] or 0 + prev_page_op.enabled = op.page > 0 - row = columns.row(align=True) - row.alignment = "RIGHT" - if AuthoringData.data["total_pages"] > 1: - row.label(text=f"Page {props.type_page}/{AuthoringData.data['total_pages']} ") - if AuthoringData.data["prev_page"]: - op = row.operator("bim.change_type_page", icon="TRIA_LEFT", text="") - op.page = AuthoringData.data["prev_page"] - if AuthoringData.data["next_page"]: - op = row.operator("bim.change_type_page", icon="TRIA_RIGHT", text="") - op.page = AuthoringData.data["next_page"] + next_page_op = row.row(align=True) + op = next_page_op.operator("bim.change_type_page", icon="TRIA_RIGHT", text="") + op.page = AuthoringData.data["next_page"] or 0 + next_page_op.enabled = op.page > 0 flow = self.layout.grid_flow(row_major=True, columns=3, even_columns=True, even_rows=True, align=True) @@ -142,11 +143,11 @@ class LaunchTypeManager(bpy.types.Operator): outer_col = flow.column() box = outer_col.box() - row = box.row(align=True) + row = box.row() op = row.operator("bim.set_active_type", text=relating_type["name"], icon="BLANK1", emboss=False) op.relating_type = relating_type["id"] - op = row.operator("bim.launch_type_menu", icon="DOWNARROW_HLT", text="", emboss=False) + op = row.operator("bim.launch_type_menu", icon="PREFERENCES", text="", emboss=True) op.relating_type_id = relating_type["id"] row = box.row() @@ -163,7 +164,15 @@ class LaunchTypeManager(bpy.types.Operator): row2.operator("bim.set_active_type", text="", emboss=False).relating_type = relating_type["id"] row2.operator("bim.set_active_type", text="", emboss=False).relating_type = relating_type["id"] row2.operator("bim.set_active_type", text="", emboss=False).relating_type = relating_type["id"] - row2.operator("bim.set_active_type", text="", emboss=False).relating_type = relating_type["id"] + is_current_relating_type = str(relating_type["id"]) == str( + AuthoringData.data["relating_type_id_current"] + ) + if is_current_relating_type: + active_row = row2.row() + active_row.alignment = "CENTER" + active_row.label(text="Active", icon="CHECKMARK") + else: + row2.operator("bim.set_active_type", text="", emboss=False).relating_type = relating_type["id"] else: row = box.row() op = box.operator("bim.load_type_thumbnails", text="", icon="FILE_REFRESH", emboss=False) diff --git a/src/bonsai/bonsai/bim/module/model/workspace.py b/src/bonsai/bonsai/bim/module/model/workspace.py index dbfd571803..90500a41d2 100644 --- a/src/bonsai/bonsai/bim/module/model/workspace.py +++ b/src/bonsai/bonsai/bim/module/model/workspace.py @@ -436,8 +436,9 @@ class CreateObjectUI: row = cls.layout.row(align=True) if AuthoringData.data["relating_type_id"]: row = cls.layout.row(align=True) if ui_context != "TOOL_HEADER" else row - op = row.operator("bim.hotkey", text="Add", icon_value=custom_icon_previews["ADD"].icon_id) - op.hotkey = "S_A" + if context.space_data.type == "VIEW_3D": # Wall polyline tool works only in 3D Space + op = row.operator("bim.hotkey", text="Add", icon_value=custom_icon_previews["ADD"].icon_id) + op.hotkey = "S_A" else: row.label(text="No Construction Type", icon="FILE_3D") @@ -500,62 +501,58 @@ class CreateObjectUI: row = cls.layout.row(align=True) if not AuthoringData.data["ifc_element_type"]: prop_with_search(row, cls.props, "ifc_class", text="Type Class" if ui_context != "TOOL_HEADER" else "") - if AuthoringData.data["ifc_classes"]: - ifc_class = AuthoringData.data["ifc_class_current"] - if ifc_class: - box = cls.layout.box() - row = box.row(align=True) - if AuthoringData.data["type_thumbnail"] and ui_context == "TOOL_HEADER": - row.template_icon(icon_value=AuthoringData.data["type_thumbnail"]) - row.operator("bim.launch_type_manager", text=AuthoringData.data["relating_type_name"], emboss=False) - else: - row.operator( - "bim.launch_type_manager", - icon="BLANK1", - text=AuthoringData.data["relating_type_name"], - emboss=False, - ) + if not AuthoringData.data["ifc_classes"]: + return + if not (ifc_class := AuthoringData.data["ifc_class_current"]): + return - row.operator( - "bim.launch_type_manager", - icon=tool.Blender.TYPE_MANAGER_ICON, - text="", - emboss=False, - ) + box = cls.layout.box() - if ui_context != "TOOL_HEADER": - row = box.row(align=True) - row.alignment = "CENTER" - row.operator( - "bim.launch_type_manager", - text=AuthoringData.data["relating_type_description"], - emboss=False, - ) + row = box.row(align=True) + thumbnail: int = AuthoringData.data["type_thumbnail"] + row.template_icon(icon_value=thumbnail) + row.operator("bim.launch_type_manager", text=AuthoringData.data["relating_type_name"], emboss=False) + row.operator( + "bim.launch_type_manager", + icon=tool.Blender.TYPE_MANAGER_ICON, + text="", + emboss=False, + ) - if AuthoringData.data["type_thumbnail"]: - row1 = box.row() - row1.ui_units_y = 0.01 - row1.template_icon(icon_value=AuthoringData.data["type_thumbnail"], scale=4) - row2 = box.column(align=True) - row2.ui_units_y = 4 - for _ in range(4): - row2.operator("bim.launch_type_manager", text="", emboss=False) - else: - op = box.operator( - "bim.load_type_thumbnails", - text="", - icon="FILE_REFRESH", - emboss=False, - ) - op.ifc_class = ifc_class + if ui_context == "TOOL_HEADER": + return + row = box.row(align=True) + row.alignment = "CENTER" + row.operator( + "bim.launch_type_manager", + text=AuthoringData.data["relating_type_description"], + emboss=False, + ) - row = box.row(align=True) - row.alignment = "CENTER" - row.operator( - "bim.launch_type_manager", - text=AuthoringData.data["predefined_type"], - emboss=False, - ) + if thumbnail != 0: + row1 = box.row() + row1.ui_units_y = 0.01 + row1.template_icon(icon_value=thumbnail, scale=4) + row2 = box.column(align=True) + row2.ui_units_y = 4 + for _ in range(4): + row2.operator("bim.launch_type_manager", text="", emboss=False) + else: + op = box.operator( + "bim.load_type_thumbnails", + text="", + icon="FILE_REFRESH", + emboss=False, + ) + op.ifc_class = ifc_class + + row = box.row(align=True) + row.alignment = "CENTER" + row.operator( + "bim.launch_type_manager", + text=AuthoringData.data["predefined_type"], + emboss=False, + ) class EditObjectUI: @@ -963,16 +960,18 @@ class Hotkey(bpy.types.Operator, tool.Ifc.Operator): bpy.ops.wm.call_menu(name="BIM_MT_add_representation_item") else: # Slab from walls - walls = False + # Make sure only Ifc Walls are selected for obj in bpy.context.selected_objects: - walls = tool.Ifc.get_entity(obj).is_a("IfcWall") - if ( - walls - and relating_type_id - and tool.Model.get_usage_type(tool.Ifc.get().by_id(int(relating_type_id))) == "LAYER3" - ): - bpy.ops.bim.draw_slab_from_wall("INVOKE_DEFAULT") - return {"FINISHED"} + element = tool.Ifc.get_entity(obj) + if not element or not element.is_a("IfcWall"): + break + else: + if ( + relating_type_id + and tool.Model.get_usage_type(tool.Ifc.get().by_id(int(relating_type_id))) == "LAYER3" + ): + bpy.ops.bim.draw_slab_from_wall("INVOKE_DEFAULT") + return {"FINISHED"} # Walls from slab slab = tool.Ifc.get_entity(bpy.context.active_object) if (