import bpy from .node_tree import _prefs, _print, BW_TREE_VERSION, BakeWrangler_Operator # Panel displaying info about recipe version and containing update button class BakeWrangler_RecipeInfo(bpy.types.Panel): '''Panel in node editor to show recipe information''' bl_label = "Recipe Info" bl_idname = "OBJECT_PT_BW_RecipeInfo" bl_space_type = 'NODE_EDITOR' bl_region_type = 'UI' bl_context = "area" bl_category = "Bake Wrangler" @classmethod def poll(cls, context): # Only display if the edited tree is of the correct type return (context.area and context.area.ui_type == 'BakeWrangler_Tree') def draw(self, context): tree = context.space_data.node_tree layout = self.layout if tree is None: layout.label(text="No recipe loaded") return tree_ver = getattr(tree, "tree_version", 0) curr_ver = BW_TREE_VERSION nodes = len(tree.nodes) col = layout.column() op = col.operator("bake_wrangler.show_log", icon='TEXT') op.tree = tree.name col.label(text="Recipe version: " + str(tree_ver)) col.label(text="Add-on version: " + str(curr_ver)) col.label(text="Nodes: " + str(nodes)) if tree_ver != curr_ver: row = col.row() if tree_ver > curr_ver: row.label(text="Status: Add-on requires update") else: row.label(text="Status: Recipe requires update") op_row = col.row() if tree_ver >= 5: op = op_row.operator("bake_wrangler_op.update_recipe", icon='FILE_REFRESH', text="Update Recipe") op.tree = tree.name else: op_row.operator("bake_wrangler_op.update_recipe", icon='CANCEL', text="Update Unavailable") op_row.enabled = False # Panel for automatic cage management tasks class BakeWrangler_AutoCages(bpy.types.Panel): '''Panel in node editor to manage automatic cages''' bl_label = "Auto Cages" bl_idname = "OBJECT_PT_BW_AutoCages" bl_space_type = 'NODE_EDITOR' bl_region_type = 'UI' bl_context = "area" bl_category = "Bake Wrangler" @classmethod def poll(cls, context): # Only display if the edited tree is of the correct type return (context.area and context.area.ui_type == 'BakeWrangler_Tree') def draw(self, context): tree = context.space_data.node_tree layout = self.layout if tree is None: layout.label(text="No recipe loaded") return col = layout.column() op = col.operator("bake_wrangler.auto_cage_create") op.tree = tree.name op = col.operator("bake_wrangler.auto_cage_update") op.tree = tree.name op = col.operator("bake_wrangler.auto_cage_remove") op.tree = tree.name # Show log file class BakeWrangler_Operator_ShowLog(BakeWrangler_Operator, bpy.types.Operator): '''Show last log created by this recipe''' bl_idname = "bake_wrangler.show_log" bl_label = "Show Log" bl_options = {"REGISTER"} # Called either after invoke from UI or directly from script def execute(self, context): return {'FINISHED'} # Called from button press, set modifier key states def invoke(self, context, event): tree = bpy.data.node_groups[self.tree] if tree.last_log: bpy.ops.screen.area_dupli('INVOKE_DEFAULT') open_ed = bpy.context.window_manager.windows[len(bpy.context.window_manager.windows) - 1].screen.areas[0] open_ed.type = 'TEXT_EDITOR' log = bpy.data.texts.load(tree.last_log) open_ed.spaces[0].text = log open_ed.spaces[0].show_line_numbers = False open_ed.spaces[0].show_syntax_highlight = False return {'FINISHED'} else: self.report({'WARNING'}, "No log file set") return {'CANCELLED'} # Generate auto cages class BakeWrangler_Operator_AutoCageCreate(BakeWrangler_Operator, bpy.types.Operator): '''Create cages in current scene for objects in recipe that don't have a cage set.\nShift-Click to exclude hidden objects''' bl_idname = "bake_wrangler.auto_cage_create" bl_label = "Generate Cages" bl_options = {"REGISTER", "UNDO"} # Called either after invoke from UI or directly from script def execute(self, context): return {'FINISHED'} # Called from button press, set modifier key states def invoke(self, context, event): mod_shift = event.shift objs = get_auto_caged(bpy.data.node_groups[self.tree], mod_shift, context) if len(objs): # Check if cage collection exists and create it if needed if 'BW Cages' not in bpy.data.collections.keys(): bpy.data.collections.new('BW Cages') # Check if cage collection is in current scene and link if needed if 'BW Cages' not in context.scene.collection.children.keys(): context.scene.collection.children.link(bpy.data.collections['BW Cages']) bw_cages = bpy.data.collections['BW Cages'].objects # Create and link cages to the collection for all objects for obj in objs: if not obj[0].bw_auto_cage: generate_auto_cage(obj[0], obj[1], obj[2], context) if obj[0].bw_auto_cage not in bw_cages.values(): bw_cages.link(obj[0].bw_auto_cage) return {'FINISHED'} else: self.report({'WARNING'}, "No objects with auto cages found") return {'CANCELLED'} # Update auto cages class BakeWrangler_Operator_AutoCageUpdate(BakeWrangler_Operator, bpy.types.Operator): '''Update cages in current scene for objects in recipe. Overwrites user changes if 'bw_cage' modifier has been removed.\nShift-Click to exclude hidden objects''' bl_idname = "bake_wrangler.auto_cage_update" bl_label = "Update Cages" bl_options = {"REGISTER", "UNDO"} # Called either after invoke from UI or directly from script def execute(self, context): return {'FINISHED'} # Called from button press, set modifier key states def invoke(self, context, event): mod_shift = event.shift objs = get_auto_caged(bpy.data.node_groups[self.tree], mod_shift, context) if len(objs): for obj in objs: if obj[0].bw_auto_cage: cage = obj[0].bw_auto_cage # If the modifier is still on the object just change it instead of making a new object if "bw_cage" in cage.modifiers: cage.modifiers["bw_cage"].strength = obj[1] cage.data.auto_smooth_angle = obj[2] elif 'BW Cages' in bpy.data.collections.keys(): bpy.data.collections['BW Cages'].objects.unlink(cage) generate_auto_cage(obj[0], obj[1], obj[2], context) return {'FINISHED'} else: self.report({'WARNING'}, "No objects with auto cages found") return {'CANCELLED'} # Remove auto cages class BakeWrangler_Operator_AutoCageRemove(BakeWrangler_Operator, bpy.types.Operator): '''Remove cages in current scene for objects in recipe.\nShift-Click to exclude hidden objects''' bl_idname = "bake_wrangler.auto_cage_remove" bl_label = "Remove Cages" bl_options = {"REGISTER", "UNDO"} # Called either after invoke from UI or directly from script def execute(self, context): return {'FINISHED'} # Called from button press, set modifier key states def invoke(self, context, event): mod_shift = event.shift if 'BW Cages' in bpy.data.collections.keys(): bw_cages = bpy.data.collections['BW Cages'].objects objs = context.scene.collection.all_objects for obj in objs: if obj.bw_auto_cage and (not mod_shift or obj.visible_get()): bw_cages.unlink(obj.bw_auto_cage) obj.bw_auto_cage = None if 'BW Cages' in context.scene.collection.children: context.scene.collection.children.unlink(bw_cages.id_data) return {'FINISHED'} else: self.report({'WARNING'}, "No objects with auto cages found") return {'CANCELLED'} # Return a list of objects that would get a cage auto generated def get_auto_caged(tree, vis, context): nodes = tree.nodes objs = [] for node in nodes: if node.bl_idname == 'BakeWrangler_Output_Image_Path': objs += node.get_unique_objects('TARGET', for_auto_cage=True) # Get a list of all objects in the scene and cull it down to only visible ones vl_objs = context.scene.collection.all_objects.values() if vis: vl_vis = [] for obj in vl_objs: if obj.visible_get() and obj not in vl_vis: vl_vis.append(obj) vl_objs = vl_vis # Return a list of unique objects that are in the scene and visible and would have a cage objs_prune = [] for obj in objs: if obj not in objs_prune and obj[0] in vl_objs: objs_prune.append(obj) return objs_prune # Create an auto cage for the given mesh def generate_auto_cage(mesh, cage_exp, smooth, context): # Create a copy of the base mesh with modifiers applied to use a the base cage cage = mesh.copy() cage.data = mesh.data.copy() cage.name = mesh.name + '.cage' cage.name = mesh.name + '.cage' cage.data.materials.clear() cage.data.polygons.foreach_set('material_index', [0] * len(cage.data.polygons)) cage.display_type = 'WIRE' if cage not in bpy.data.collections['BW Cages'].objects.values(): bpy.data.collections['BW Cages'].objects.link(cage) if len(cage.modifiers): prev_active = bpy.context.view_layer.objects.active bpy.context.view_layer.objects.active = cage for mod in cage.modifiers: if mod.show_render: try: bpy.ops.object.modifier_apply(modifier=mod.name) except: _print("Error applying modifier '%s' to object '%s'" % (mod.name, mesh.name)) bpy.ops.object.modifier_remove(modifier=mod.name) else: bpy.ops.object.modifier_remove(modifier=mod.name) bpy.context.view_layer.objects.active = prev_active # Expand cage on normals cage_disp = cage.modifiers.new("bw_cage", 'DISPLACE') cage_disp.strength = cage_exp cage_disp.direction = 'NORMAL' cage_disp.mid_level = 0.0 cage_disp.show_in_editmode = True cage_disp.show_on_cage = True cage_disp.show_expanded = False # Smooth normals and clear sharps cage.data.use_auto_smooth = True cage.data.auto_smooth_angle = smooth for poly in cage.data.polygons: poly.use_smooth = True for edge in cage.data.edges: edge.use_edge_sharp = False # Link cage via property on mesh mesh.bw_auto_cage = cage # Classes to register classes = ( BakeWrangler_RecipeInfo, BakeWrangler_AutoCages, BakeWrangler_Operator_ShowLog, BakeWrangler_Operator_AutoCageCreate, BakeWrangler_Operator_AutoCageUpdate, BakeWrangler_Operator_AutoCageRemove, ) def register(): # Add pointer to generated cage bpy.types.Object.bw_auto_cage = bpy.props.PointerProperty(name="Cage", description="Bake Wrangler auto generated cage", type=bpy.types.Object) from bpy.utils import register_class for cls in classes: register_class(cls) def unregister(): from bpy.utils import unregister_class for cls in reversed(classes): unregister_class(cls) if __name__ == "__main__": register()