import bpy from ..utils import find_closest_obj, show_message_box, find_3d_viewport from bpy.utils import register_class, unregister_class from bpy.types import Operator, PropertyGroup, UIList from bpy.props import EnumProperty, StringProperty from ..utils import invalidate_udim_cache def refresh_bake_objects_list(context): sbp = context.scene.SimpleBake_Props if sbp.global_mode == "PBR" and sbp.selected_s2a: if sbp.s2a_opmode != "automatch": if sbp.targetobj != None: if sbp.targetobj.name not in bpy.context.scene.objects: sbp.targetobj = None if sbp.global_mode == "CyclesBake" and sbp.cycles_s2a: if sbp.s2a_opmode != "automatch": if sbp.targetobj_cycles != None: if sbp.targetobj_cycles.name not in bpy.context.scene.objects: sbp.targetobj_cycles = None objects_list = sbp.objects_list gone = [] for li in objects_list: #Is it empty? if li.obj_point == None: gone.append(li.name) #Is it no longer in the scene? elif li.obj_point.name not in context.scene.objects: gone.append(li.name) #Is it not in use anywhere else? elif len(li.obj_point.users_scene) < 1: gone.append(li.name) elif sbp.selected_s2a or sbp.cycles_s2a: if sbp.s2a_opmode=="automatch" and sbp.auto_match_mode == "name": for o in sbp.objects_list: if o.obj_point != None and not o.obj_point.name.lower().endswith("_high"): gone.append(o.name) for g in gone: objects_list.remove(objects_list.find(g)) for i in objects_list: i.name = i.obj_point.name #Throw in a refresh of the UV list. WHy not? bpy.ops.simplebake.sync_uv_list() def _strip_sb_prefixes(name): """Strip SimpleBake backup prefixes (SBW_, SBM_) to recover the original material name.""" if name.startswith("SBW_"): name = name[4:] if name.startswith("SBM_"): name = name[4:] return name def refresh_mat_bake_list(context): """Keep mat_bake_list in sync with objects_list and their materials.""" sbp = context.scene.SimpleBake_Props # Build the set of (obj_name, mat_name) pairs that should exist. # Strip SimpleBake backup prefixes so the list always stores original names. wanted = set() for item in sbp.objects_list: if item.obj_point is None: continue obj = item.obj_point for slot in obj.material_slots: if slot.material is not None: wanted.add((obj.name, _strip_sb_prefixes(slot.material.name))) # Remove stale entries to_remove = [] for i, entry in enumerate(sbp.mat_bake_list): if (entry.obj_name, entry.mat_name) not in wanted: to_remove.append(i) for i in reversed(to_remove): sbp.mat_bake_list.remove(i) # Add missing entries (default enabled=True) existing = {(e.obj_name, e.mat_name) for e in sbp.mat_bake_list} for obj_name, mat_name in sorted(wanted): if (obj_name, mat_name) not in existing: entry = sbp.mat_bake_list.add() entry.obj_name = obj_name entry.mat_name = mat_name entry.enabled = True class SIMPLEBAKE_UL_Objects_List(UIList): """Bake objects list""" viable_high_low_bakes = [] def draw_item(self, context, layout, data, item, icon, active_data, active_propname, index): sbp = context.scene.SimpleBake_Props custom_icon = 'OBJECT_DATAMODE' name = item.obj_point.name if item.obj_point != None else "_?_" target_obj = "???" cage_obj = "" #Set the icon if in name mode if (sbp.selected_s2a or sbp.cycles_s2a) and item.obj_point != None: if sbp.s2a_opmode=="automatch": if sbp.auto_match_mode == "name": custom_icon = "SEQUENCE_COLOR_01" if bpy.app.version < (4,4,0) else "STRIP_COLOR_01" target_obj = "???" for o in context.scene.objects: if name.lower().replace("_high", "_low") == o.name.lower(): target_obj = o.name custom_icon = "SEQUENCE_COLOR_04" if bpy.app.version < (4,4,0) else "STRIP_COLOR_04" if name not in __class__.viable_high_low_bakes: __class__.viable_high_low_bakes.append(name) #We didn't find a match if target_obj == "???": if name in __class__.viable_high_low_bakes: __class__.viable_high_low_bakes.remove(name) # Look up cage object by naming convention cage_obj = "" if target_obj != "???": parts = target_obj.split("_") base_name = '_'.join(parts[:-1]).lower() for o in context.scene.objects: if o.name.lower() == f"{base_name}_cage": cage_obj = o.name break #Set the icon if in position mode if (sbp.selected_s2a or sbp.cycles_s2a) and item.obj_point != None: if sbp.s2a_opmode=="automatch": if sbp.auto_match_mode == "position": co = find_closest_obj(context, name) if co != None: custom_icon = "SEQUENCE_COLOR_04" if bpy.app.version < (4,4,0) else "STRIP_COLOR_04" target_obj = co.name if name not in __class__.viable_high_low_bakes: __class__.viable_high_low_bakes.append(name) else: custom_icon = "SEQUENCE_COLOR_01" if bpy.app.version < (4,4,0) else "STRIP_COLOR_01" target_obj = "???" if name in __class__.viable_high_low_bakes: __class__.viable_high_low_bakes.remove(name) # Draw if self.layout_type in {'DEFAULT', 'COMPACT'}: col = layout.column() col.label(text=name, icon=custom_icon) if (sbp.selected_s2a or sbp.cycles_s2a) and sbp.s2a_opmode=="automatch": col = layout.column() col.alignment = 'CENTER' col.label(text=" --> ") col = layout.column() icon = "QUESTION" if target_obj == "???" else "CHECKMARK" col.label(text=target_obj, icon=icon) if sbp.auto_match_mode == "name": col = layout.column() if cage_obj: col.label(text=cage_obj, icon="MOD_MESHDEFORM") else: col.label(text="No cage") elif self.layout_type in {'GRID'}: layout.alignment = 'CENTER' layout.label(text="", icon = custom_icon) class SimpleBake_OT_Add_Bake_Object(Operator): """Add selected object(s) to the bake list""" bl_idname = "simplebake.add_bake_object" bl_label = "Adds a bake object to the list" @classmethod def poll(cls, context): return len(context.selected_objects) def execute(self, context): sbp = context.scene.SimpleBake_Props #Lets get rid of the non-mesh objects from the selections [obj.select_set(False) for obj in context.scene.objects if obj.type != "MESH"] #Do we still have an active object? if context.active_object == None: #If not, pick one context.view_layer.objects.active = context.selected_objects[0] #Add to list if not already in the list objs = context.selected_objects.copy() for obj in objs: #If we are in matching high to low mode, only allow adding of _high objects if (sbp.selected_s2a or sbp.cycles_s2a): if sbp.s2a_opmode=="automatch" and sbp.auto_match_mode == "name": if not obj.name.lower().endswith("_high"): messages = ([ f"ERROR: Can't add object {obj.name}", "You have auto match high and low poly selected and", "you are using name mode.", "Only object names ending in \"_high\" can be added" ]) show_message_box(context, messages, "Errors occured", icon = 'ERROR') continue #Only add if not already on the list r = [o.name for o in sbp.objects_list if o.name == obj.name] if len(r) == 0: n = sbp.objects_list.add() n.obj_point = obj n.name = obj.name #Throw in a refresh refresh_bake_objects_list(context) refresh_mat_bake_list(context) if (sbp.selected_s2a or sbp.cycles_s2a) and sbp.s2a_opmode=="automatch": if len(SIMPLEBAKE_UL_Objects_List.viable_high_low_bakes)<2: sbp.merged_bake = False #Invalidate UDIM cache invalidate_udim_cache() return{'FINISHED'} class SimpleBake_OT_Add_Bake_Object_By_Name(Operator): """Add specified object(s) to the bake list""" bl_idname = "simplebake.add_bake_object_by_name" bl_label = "Adds a bake object to the list" override_target_obj_name: StringProperty(default="") def execute(self, context): sbp = context.scene.SimpleBake_Props n = sbp.objects_list.add() obj = context.scene.objects[self.override_target_obj_name] n.obj_point = obj n.name = obj.name #Invalidate UDIM cache invalidate_udim_cache() return{'FINISHED'} class SimpleBake_OT_Remove_Bake_Object(Operator): """Remove the selected object from the bake list.""" bl_idname = "simplebake.remove_bake_object" bl_label = "Removes a bake object from the list" @classmethod def poll(cls, context): #TODO------- return context.scene.SimpleBake_Props.objects_list def execute(self, context): sbp = context.scene.SimpleBake_Props objects_list = sbp.objects_list index = sbp.objects_list_index #Record it's name name = sbp.objects_list[index].name objects_list.remove(index) sbp.objects_list_index = min(max(0, index - 1), len(objects_list) - 1) #Throw in a refresh refresh_bake_objects_list(context) refresh_mat_bake_list(context) #Also remove from high low list if name in SIMPLEBAKE_UL_Objects_List.viable_high_low_bakes: SIMPLEBAKE_UL_Objects_List.viable_high_low_bakes.remove(name) if (sbp.selected_s2a or sbp.cycles_s2a) and sbp.s2a_opmode=="automatch": if len(SIMPLEBAKE_UL_Objects_List.viable_high_low_bakes)<2: sbp.merged_bake = False #Invalidate UDIM cache invalidate_udim_cache() return{'FINISHED'} class SimpleBake_OT_Clear_Bake_Objects_List(Operator): """Clear the object list""" bl_idname = "simplebake.clear_bake_objects_list" bl_label = "Clears the bake object list" @classmethod def poll(cls, context): return True def execute(self, context): sbp = context.scene.SimpleBake_Props sbp.objects_list.clear() #Throw in a refresh refresh_bake_objects_list(context) refresh_mat_bake_list(context) #Clear the high low match list SIMPLEBAKE_UL_Objects_List.viable_high_low_bakes = [] # if len(SIMPLEBAKE_UL_Objects_List.viable_high_low_bakes)<2: # sbp.merged_bake = False #Invalidate UDIM cache invalidate_udim_cache() return{'FINISHED'} class SimpleBake_OT_Move_Bake_Object_List(Operator): """Move an object in the list.""" bl_idname = "simplebake.move_bake_object_list" bl_label = "Moves an item in the bake objects list" direction: bpy.props.EnumProperty(items=(('UP', 'Up', ""), ('DOWN', 'Down', ""),)) @classmethod def poll(cls, context): return context.scene.SimpleBake_Props.objects_list def move_index(self, context): """ Move index of an item render queue while clamping it. """ sbp = context.scene.SimpleBake_Props index = sbp.objects_list_index list_length = len(sbp.objects_list) - 1 # (index starts at 0) new_index = index + (-1 if self.direction == 'UP' else 1) sbp.objects_list_index = max(0, min(new_index, list_length)) def execute(self, context): sbp = context.scene.SimpleBake_Props objects_list = sbp.objects_list index = sbp.objects_list_index neighbor = index + (-1 if self.direction == 'UP' else 1) objects_list.move(neighbor, index) self.move_index(context) #Throw in a refresh refresh_bake_objects_list(context) return{'FINISHED'} class SimpleBake_OT_Refresh_Bake_Object_List(Operator): """Refresh the list to remove objects""" bl_idname = "simplebake.refresh_bake_object_list" bl_label = "Refresh the bake objects list" @classmethod def poll(cls, context): return True def execute(self, context): refresh_bake_objects_list(context) refresh_mat_bake_list(context) #Invalidate UDIM cache invalidate_udim_cache() return{'FINISHED'} #highlight_on = False class SimpleBake_OT_Set_Highlight_Cols(Operator): """Actually modify the object colours""" bl_idname = "simplebake.set_highlight_cols" bl_label = "Set highlight cols to objects" @classmethod def update_object_list_from_selectd(cls): sbp = bpy.context.scene.SimpleBake_Props if (o:=bpy.context.active_object): if (i:=sbp.objects_list.find(o.name))!=-1: if sbp.objects_list_index !=i: sbp.objects_list_index =i def execute(self, context): sbp = bpy.context.scene.SimpleBake_Props #Do this anyway (EEVEE updates even if object selection changed) __class__.update_object_list_from_selectd() if not sbp.highlight_on: return{'CANCELLED'} #Highlight turned off - end timer #Do nothing more if we aren't in shading mode any_shading = False for space in find_3d_viewport(): if space.shading.type == "SOLID": any_shading = True if not any_shading: return{'CANCELLED'} #return 0.2 #Do nothing. Check back later def set_col(obj_names, col): for o_name in obj_names: if (o:=bpy.data.objects.get(o_name)): if o.color != col: if "SB_orig_col" not in o: o["SB_orig_col"] = o.color o.color = col bake_objs = [o.name for o in sbp.objects_list] all_objs = ([o.name for o in bpy.context.scene.objects if o.name not in bake_objs and o.type == "MESH" and "SB_auto_cage" not in o]) set_col(all_objs, (1,1,1,1)) c = sbp.highlight_col set_col(bake_objs, (c.r, c.g, c.b, 1.0)) return{'FINISHED'} class SimpleBake_OT_Highlight_Bake_Objects(Operator): """Highlight selected objects in the viewport""" bl_idname = "simplebake.highlight_bake_objects" bl_label = "Highlights bake objects in the viewport on this screen" @classmethod def poll(cls, context): sbp = context.scene.SimpleBake_Props return not sbp.highlight_on @classmethod def set_timer(cls): bpy.app.timers.register(cls.col_bake_objects, first_interval=1.0) @classmethod def col_bake_objects(cls): sbp = bpy.context.scene.SimpleBake_Props bpy.ops.simplebake.set_highlight_cols() if not sbp.highlight_on: return None else: return 0.2 @classmethod def set_display(cls, context): for space in find_3d_viewport(): space.shading.color_type = "OBJECT" def execute(self, context): sbp = context.scene.SimpleBake_Props sbp.highlight_on = True __class__.set_timer() __class__.col_bake_objects() __class__.set_display(context) return{'FINISHED'} class SimpleBake_OT_Remove_Highlight(Operator): """Remove highlight of selected objects in the viewport""" bl_idname = "simplebake.remove_highlight" bl_label = "Removes the highlights for bake objects in the viewport on this screen" @classmethod def poll(cls, context): sbp = context.scene.SimpleBake_Props return sbp.highlight_on def uncol_bake_objects(self, context): sbp = context.scene.SimpleBake_Props bpy.ops.simplebake.refresh_bake_object_list() obj_names = [o.name for o in sbp.objects_list] for o_name in obj_names: if (o:=bpy.data.objects.get(o_name)): if "SB_orig_col" in o: o.color = o["SB_orig_col"] del o["SB_orig_col"] def unset_display(self, context): #for screen in bpy.data.screens: screen = bpy.context.window.screen # Iterate through the areas in the current screen for area in screen.areas: # Check if the area is a 3D viewport if area.type == 'VIEW_3D': # Optionally, you can access the space data of the area for space in area.spaces: if space.type == 'VIEW_3D': space.shading.color_type = "MATERIAL" def execute(self, context): sbp = context.scene.SimpleBake_Props sbp.highlight_on = False self.uncol_bake_objects(context) self.unset_display(context) return{'FINISHED'} class SimpleBake_OT_Materials_Expand_All(Operator): """Expand all objects in the Materials section""" bl_idname = "simplebake.materials_expand_all" bl_label = "Expand All" def execute(self, context): for item in context.scene.SimpleBake_Props.objects_list: item.expanded = True return {'FINISHED'} class SimpleBake_OT_Materials_Collapse_All(Operator): """Collapse all objects in the Materials section""" bl_idname = "simplebake.materials_collapse_all" bl_label = "Collapse All" def execute(self, context): for item in context.scene.SimpleBake_Props.objects_list: item.expanded = False return {'FINISHED'} classes = ([ SIMPLEBAKE_UL_Objects_List, SimpleBake_OT_Add_Bake_Object, SimpleBake_OT_Add_Bake_Object_By_Name, SimpleBake_OT_Remove_Bake_Object, SimpleBake_OT_Clear_Bake_Objects_List, SimpleBake_OT_Move_Bake_Object_List, SimpleBake_OT_Refresh_Bake_Object_List, SimpleBake_OT_Highlight_Bake_Objects, SimpleBake_OT_Remove_Highlight, SimpleBake_OT_Set_Highlight_Cols, SimpleBake_OT_Materials_Expand_All, SimpleBake_OT_Materials_Collapse_All, ]) def register(): global classes for cls in classes: register_class(cls) def unregister(): global classes for cls in classes: unregister_class(cls)