Amaranth 0.8.4:
authorPablo Vazquez <venomgfx@gmail.com>
Sat, 15 Mar 2014 22:38:47 +0000 (19:38 -0300)
committerPablo Vazquez <venomgfx@gmail.com>
Sat, 15 Mar 2014 22:45:04 +0000 (19:45 -0300)
- Cycles Sampling, display sampling values for each renderlayers as well.
- Rename Missing Node Groups to Node Links, because it can now look
  for empty node images or images that doesnt exist as well.
- Scene Debug: Empty Material Slots, print a list of all the objects
  that have a empty material slots (common when linked materials got lost)
- List Missing Images now show a path if linked from a .blend, clicking
  on the path will open a new blender instance with that file (same for
  dupligroups path)

Full changelog at: http://pablovazquez.org/amaranth

scene_amaranth_toolset.py

index 6e7a679..6c34a54 100755 (executable)
@@ -19,7 +19,7 @@
 bl_info = {
     "name": "Amaranth Toolset",
     "author": "Pablo Vazquez, Bassam Kurdali, Sergey Sharybin",
-    "version": (0, 8, 2),
+    "version": (0, 8, 4),
     "blender": (2, 70),
     "location": "Everywhere!",
     "description": "A collection of tools and settings to improve productivity",
@@ -1009,7 +1009,7 @@ class MESH_OT_make_symmetric(Operator):
         return {'FINISHED'}
 # // FEATURE: Mesh Symmetry Tools by Sergey Sharybin
 
-# FEATURE: Cycles Render Samples per Scene
+# FEATURE: Cycles Render Sampling Extra
 def render_cycles_scene_samples(self, context):
 
     layout = self.layout
@@ -1018,46 +1018,54 @@ def render_cycles_scene_samples(self, context):
     scene = context.scene
     cscene = scene.cycles
 
+    col = layout.column(align=True)
+
+    if len(scene.render.layers) == 1 and \
+        scene.render.layers[0].samples == 0:
+        pass
+    else:
+        col.separator()
+        col.label(text="Samples Per RenderLayer:")
+
+        for rl in scene.render.layers:
+            row = col.row(align=True)
+            row.label(rl.name)
+            row.prop(rl, "samples", text="%s" %
+                "Samples" if rl.samples > 0 else "Samples [Auto]")
+
     if (len(bpy.data.scenes) > 1):
-        layout.separator()
+        col.separator()
 
-        layout.label(text="Samples Per Scene:")
+        col.label(text="Samples Per Scene:")
+        row = col.row(align=True)
 
         if cscene.progressive == 'PATH':
             for s in bpy.data.scenes:
                 if s != scene:
+                    row = col.row(align=True)
                     if s.render.engine == 'CYCLES':
                         cscene = s.cycles
-        
-                        split = layout.split()
-                        col = split.column()
-                        sub = col.column(align=True)
-        
-                        sub.label(text="%s" % s.name)
-        
-                        col = split.column()
-                        sub = col.column(align=True)
-                        sub.prop(cscene, "samples", text="Samples")
+
+                        row.label(s.name)
+                        row.prop(cscene, "samples")
                     else:
-                        layout.label(text="Scene: '%s' is not using Cycles" % s.name)
+                        row.label(text="Scene: '%s' is not using Cycles" % s.name)
         else:
             for s in bpy.data.scenes:
                 if s != scene:
+                    row = col.row(align=True)
                     if s.render.engine == 'CYCLES':
                         cscene = s.cycles
-        
-                        split = layout.split()
-                        col = split.column()
-                        sub = col.column(align=True)
-        
-                        sub.label(text="%s" % s.name)
-        
-                        col = split.column()
-                        sub = col.column(align=True)
-                        sub.prop(cscene, "aa_samples", text="AA Samples")
+
+                        row.label(s.name)
+                        row.prop(cscene, "aa_samples",
+                            text="AA Samples")
                     else:
-                        layout.label(text="Scene: '%s' is not using Cycles" % s.name)
-# // FEATURE: Cycles Render Samples per Scene
+                        row.label(text="Scene: '%s' is not using Cycles" % s.name)
+
+# // FEATURE: Dupli  Group Path
+
+# // FEATURE: Cycles Render Sampling Extra
 
 # FEATURE: Motion Paths Extras
 class POSE_OT_paths_clear_all(Operator):
@@ -1195,7 +1203,6 @@ class SCENE_OT_cycles_shader_list_nodes(Operator):
     """List Cycles materials containing a specific shader"""
     bl_idname = "scene.cycles_list_nodes"
     bl_label = "List Materials"
-    count_ma = 0
     materials = []
 
     @classmethod
@@ -1205,10 +1212,7 @@ class SCENE_OT_cycles_shader_list_nodes(Operator):
     def execute(self, context):
         node_type = context.scene.amaranth_cycles_node_types
         roughness = False
-
-        # Reset the list and counter
         self.__class__.materials = []
-        self.__class__.count_ma = 0
 
         print("\n=== Cycles Shader Type: %s === \n" % node_type)
 
@@ -1229,8 +1233,7 @@ class SCENE_OT_cycles_shader_list_nodes(Operator):
                                       'not connected',
                                       '\n')
 
-                            self.__class__.materials = list(set(self.__class__.materials))
-                            self.__class__.count_ma += 1
+                            self.__class__.materials = sorted(list(set(self.__class__.materials)))
 
                             if ma.name not in self.__class__.materials:
                                 self.__class__.materials.append('%s%s [%s] %s%s' % (
@@ -1239,23 +1242,22 @@ class SCENE_OT_cycles_shader_list_nodes(Operator):
                                     '[F]' if ma.use_fake_user else '',
                                     ' - [%s]' % roughness if roughness else ''))
 
-        if self.__class__.count_ma == 0:
+        if len(self.__class__.materials) == 0:
             self.report({"INFO"}, "No materials with nodes type %s found" % node_type)
-
         else:
-            print("* A total of %s %s using %s was found \n" % (
-                    self.__class__.count_ma,
-                    "material" if self.__class__.count_ma == 1 else "materials",
+            print("* A total of %d %s using %s were found \n" % (
+                    len(self.__class__.materials),
+                    "material" if len(self.__class__.materials) == 1 else "materials",
                     node_type))
 
             count = 0
 
             for mat in self.__class__.materials:
-                print('%02d. %s' % (count + 1, self.__class__.materials[count]))
+                print('%02d. %s' % (count+1, self.__class__.materials[count]))
                 count += 1
             print("\n")
 
-        self.__class__.materials = list(set(self.__class__.materials))
+        self.__class__.materials = sorted(list(set(self.__class__.materials)))
 
         return {'FINISHED'}
 
@@ -1266,7 +1268,6 @@ class SCENE_OT_cycles_shader_list_nodes_clear(Operator):
     
     def execute(self, context):
         SCENE_OT_cycles_shader_list_nodes.materials[:] = []
-        SCENE_OT_cycles_shader_list_nodes.count_ma = 0
         print("* Cleared Cycles Materials List")
         return {'FINISHED'}
 
@@ -1286,56 +1287,183 @@ class SCENE_OT_amaranth_debug_lamp_select(Operator):
 
         return{'FINISHED'}
 
-class SCENE_OT_list_missing_node_tree(Operator):
-    '''Print a list of missing (link lost) node groups'''
-    bl_idname = "scene.list_missing_node_tree"
-    bl_label = "List Missing Node Groups"
+class SCENE_OT_list_missing_node_links(Operator):
+    '''Print a list of missing node links'''
+    bl_idname = "scene.list_missing_node_links"
+    bl_label = "List Missing Node Links"
 
-    count = 0
+    count_groups = 0
+    count_images = 0
 
     def execute(self, context):
-        missing = []
+        missing_groups = []
+        missing_images = []
         libraries = []
-        self.__class__.count = 0
+        self.__class__.count_groups = 0
+        self.__class__.count_images = 0
 
-        print("\n* Missing Node Groups\n")
         for ma in bpy.data.materials:
             if ma.node_tree:
                 for no in ma.node_tree.nodes:
                     if no.type == 'GROUP':
                         if not no.node_tree:
-                            self.__class__.count += 1
+                            self.__class__.count_groups += 1
+
+                            users_ngroup = []
 
-                            missing.append("%02d. %s%s%s [%s]%s" % (
-                                self.__class__.count,
+                            for ob in bpy.data.objects:
+                                if ob.material_slots and ma.name in ob.material_slots:
+                                    users_ngroup.append("%s%s%s" % (
+                                        "[L] " if ob.library else "",
+                                        "[F] " if ob.use_fake_user else "",
+                                        ob.name))
+
+                            missing_groups.append("NG: %s%s%s [%s]%s%s\n" % (
                                 "[L] " if ma.library else "",
                                 "[F] " if ma.use_fake_user else "",
                                 ma.name, ma.users,
-                                "\n    %s" % 
-                                ma.library.filepath if ma.library else ""))
+                                "\nLI: %s" % 
+                                ma.library.filepath if ma.library else "",
+                                "\nOB: %s" % ',  '.join(users_ngroup) if users_ngroup else ""))
 
                             if ma.library:
                                 libraries.append(ma.library.filepath)
+                    if no.type == 'TEX_IMAGE':
+                        if no.image:
+                            import os.path
+                            image_path_exists = os.path.exists(
+                                                    bpy.path.abspath(
+                                                        no.image.filepath, library=no.image.library))
+
+                        if not no.image or not image_path_exists:
+                            self.__class__.count_images += 1
+
+                            users_images = []
+
+                            for ob in bpy.data.objects:
+                                if ob.material_slots and ma.name in ob.material_slots:
+                                    users_images.append("%s%s%s" % (
+                                        "[L] " if ob.library else "",
+                                        "[F] " if ob.use_fake_user else "",
+                                        ob.name))
+
+                            missing_images.append("MA: %s%s%s [%s]%s%s%s%s\n" % (
+                                "[L] " if ma.library else "",
+                                "[F] " if ma.use_fake_user else "",
+                                ma.name, ma.users,
+                                "\nLI: %s" % 
+                                ma.library.filepath if ma.library else "",
+                                "\nIM: %s" % no.image.name if no.image else "",
+                                "\nLI: %s" % no.image.filepath if no.image and no.image.filepath else "",
+                                "\nOB: %s" % ',  '.join(users_images) if users_images else ""))
 
-        # Remove duplicates and sort
-        missing = list(set(missing))
-        missing = sorted(missing)
-        libraries = list(set(libraries))
-
-        for mi in missing:
-            print(mi)
+                            if ma.library:
+                                libraries.append(ma.library.filepath)
 
-        if missing:
-            self.report({"INFO"}, "%d missing node %s found! Check console" %
-                    (self.__class__.count, "group" if self.__class__.count == 1 else "groups"))
+        # Remove duplicates and sort
+        missing_groups = sorted(list(set(missing_groups)))
+        missing_images = sorted(list(set(missing_images)))
+        libraries = sorted(list(set(libraries)))
+
+        print("\n\n== %s missing image %s and %s missing node %s ==" %
+            ("No" if self.__class__.count_images == 0 else str(self.__class__.count_images),
+            "node" if self.__class__.count_images == 1 else "nodes",
+            "no" if self.__class__.count_groups == 0 else str(self.__class__.count_groups),
+            "group" if self.__class__.count_groups == 1 else "groups"))
+
+        # List Missing Node Groups
+        if missing_groups:
+            print("\n* Missing Node Group Links [NG]\n")
+            for mig in missing_groups:
+                print(mig)
+
+        # List Missing Image Nodes
+        if missing_images:
+            print("\n* Missing Image Nodes Link [IM]\n")
+
+            for mii in missing_images:
+                print(mii)
+
+        if missing_groups or missing_images:
             if libraries:
-                print("\nThat's bad, run this check on %s:" % (
+                print("\nThat's bad, run check on %s:" % (
                     "this library" if len(libraries) == 1 else "these libraries"))
                 for li in libraries:
                     print(li)
-            print("\n")
         else:
-            self.report({"INFO"}, "Yay! No missing node groups")
+            self.report({"INFO"}, "Yay! No missing node links")            
+
+        print("\n")
+
+        if missing_groups and missing_images:
+            self.report({"WARNING"}, "%d missing image %s and %d missing node %s found" %
+                (self.__class__.count_images, "node" if self.__class__.count_images == 1 else "nodes",
+                self.__class__.count_groups, "group" if self.__class__.count_groups == 1 else "groups"))
+
+        return{'FINISHED'}
+
+class SCENE_OT_list_missing_material_slots(Operator):
+    '''List objects with empty material slots'''
+    bl_idname = "scene.list_missing_material_slots"
+    bl_label = "List Empty Material Slots"
+    objects = []
+    def execute(self, context):
+        self.__class__.objects = []
+
+        for ob in bpy.data.objects:
+            for ma in ob.material_slots:
+                if not ma.material:
+                    self.__class__.objects.append('%s%s' % (
+                        '[L] ' if ob.library else '',
+                        ob.name))
+        self.__class__.objects = sorted(list(set(self.__class__.objects)))
+
+        if len(self.__class__.objects) == 0:
+            self.report({"INFO"}, "No objects with empty material slots found")
+        else:
+            print("\n* A total of %d %s with empty material slots was found \n" % (
+                    len(self.__class__.objects),
+                    "object" if len(self.__class__.objects) == 1 else "objects"))
+
+            count = 0
+
+            for obs in self.__class__.objects:
+                print('%02d. %s' % (count+1, self.__class__.objects[count]))
+                count += 1
+            print("\n")
+
+        return{'FINISHED'}
+
+class SCENE_OT_list_missing_material_slots_clear(Operator):
+    """Clear the list below"""
+    bl_idname = "scene.list_missing_material_slots_clear"
+    bl_label = "Clear Empty Material Slots List"
+    
+    def execute(self, context):
+        SCENE_OT_list_missing_material_slots.objects[:] = []
+        print("* Cleared Empty Material Slots List")
+        return {'FINISHED'}
+
+class SCENE_OT_blender_instance_open(Operator):
+    '''Open in a new Blender instance'''
+    bl_idname = "scene.blender_instance_open"
+    bl_label = "Open Blender Instance"
+    filepath = bpy.props.StringProperty()
+    def execute(self, context):
+        if self.filepath:
+            filepath = bpy.path.abspath(self.filepath)
+
+            import subprocess
+            try:
+                subprocess.Popen([bpy.app.binary_path, filepath])
+            except:
+                print("Error on the new Blender instance")
+                import traceback
+                traceback.print_exc()
+
         return{'FINISHED'}
 
 class SCENE_PT_scene_debug(Panel):
@@ -1356,7 +1484,9 @@ class SCENE_PT_scene_debug(Panel):
         list_lamps = scene.amaranth_debug_scene_list_lamps
         list_missing_images = scene.amaranth_debug_scene_list_missing_images
         materials = SCENE_OT_cycles_shader_list_nodes.materials
-        materials_count = SCENE_OT_cycles_shader_list_nodes.count_ma
+        materials_count = len(SCENE_OT_cycles_shader_list_nodes.materials)
+        missing_material_slots_obs = SCENE_OT_list_missing_material_slots.objects
+        missing_material_slots_count = len(SCENE_OT_list_missing_material_slots.objects)
         engine = scene.render.engine
 
         # List Lamps
@@ -1488,7 +1618,8 @@ class SCENE_PT_scene_debug(Panel):
                             '[L] ' if im.library else '',
                             im.name, im.users,
                             ' [F]' if im.use_fake_user else ''),
-                            im.filepath if im.filepath else 'No Filepath'])
+                            im.filepath if im.filepath else 'No Filepath',
+                            im.library.filepath if im.library else ''])
 
             if images_missing:
                 row = col.row(align=True)
@@ -1510,7 +1641,14 @@ class SCENE_PT_scene_debug(Panel):
                     for mis in images_missing:
                         col.label(text=mis[0],
                          icon="IMAGE_DATA")
-                        col.label(text=mis[1], icon="BLANK1")
+                        col.label(text=mis[1], icon="LIBRARY_DATA_DIRECT")
+                        if mis[2]:
+                            row = col.row(align=True)
+                            row.alignment = "LEFT"
+                            row.operator(SCENE_OT_blender_instance_open.bl_idname,
+                                         text=mis[2],
+                                         icon="LINK_BLEND",
+                                         emboss=False).filepath=mis[2]
                         col.separator()
             else:
                 row = col.row(align=True)
@@ -1552,6 +1690,7 @@ class SCENE_PT_scene_debug(Panel):
                 pass
             else:
                 if materials_count != 0: 
+                    col = box.column(align=True)
                     count = 0
                     col.label(text="%s %s found" % (materials_count,
                         'material' if materials_count == 1 else 'materials'), icon="INFO")
@@ -1560,25 +1699,83 @@ class SCENE_PT_scene_debug(Panel):
                         col.label(text='%s' % (materials[count-1]), icon="MATERIAL")
 
         # List Missing Node Trees
-        split = layout.split()
-        split.label(text="Missing Node Groups")
-        split.operator(SCENE_OT_list_missing_node_tree.bl_idname,
+        box = layout.box()
+        row = box.row(align=True)
+        split = row.split()
+        col = split.column(align=True)
+
+        split = col.split()
+        split.label(text="Node Links")
+        split.operator(SCENE_OT_list_missing_node_links.bl_idname,
                         icon="NODETREE")
 
-        if SCENE_OT_list_missing_node_tree.count != 0:
-            layout.label(text="%s" % ("%s missing %s found, check the console!" % (
-                     str(SCENE_OT_list_missing_node_tree.count),
-                     "group" if SCENE_OT_list_missing_node_tree.count == 1 else "groups")),
-                     icon="ERROR")
+        if SCENE_OT_list_missing_node_links.count_groups != 0 or \
+            SCENE_OT_list_missing_node_links.count_images != 0:
+            col.label(text="Warning! Check Console", icon="ERROR")
+
+        if SCENE_OT_list_missing_node_links.count_groups != 0:
+            col.label(text="%s" % ("%s node %s missing link" % (
+                     str(SCENE_OT_list_missing_node_links.count_groups),
+                     "group" if SCENE_OT_list_missing_node_links.count_groups == 1 else "groups")),
+                     icon="NODETREE")
+        if SCENE_OT_list_missing_node_links.count_images != 0:
+            col.label(text="%s" % ("%s image %s missing link" % (
+                     str(SCENE_OT_list_missing_node_links.count_images),
+                     "node" if SCENE_OT_list_missing_node_links.count_images == 1 else "nodes")),
+                     icon="IMAGE_DATA")
+
+        col.separator()
+
+        # List Empty Materials Slots
+        box = layout.box()
+        split = box.split()
+        col = split.column(align=True)
+        col.label(text="Material Slots")
+
+        row = split.row(align=True)
+        row.operator(SCENE_OT_list_missing_material_slots.bl_idname,
+                        icon="MATERIAL",
+                        text="List Empty Materials Slots")
+        if missing_material_slots_count != 0: 
+            row.operator(SCENE_OT_list_missing_material_slots_clear.bl_idname,
+                            icon="X", text="")
+        col.separator()
+
+        try:
+            missing_material_slots_obs
+        except NameError:
+            pass
+        else:
+            if missing_material_slots_count != 0: 
+                col = box.column(align=True)
+                count = 0
+                col.label(text="%s %s with empty material slots found" % (
+                    missing_material_slots_count,
+                    'object' if missing_material_slots_count == 1 else 'objects'),
+                    icon="INFO")
+
+                for obs in missing_material_slots_obs:
+                    count += 1
+                    col.label(text='%s' % (
+                        missing_material_slots_obs[count-1]),
+                        icon="OBJECT_DATA")
 
 # // FEATURE: Scene Debug
 # FEATURE: Dupli  Group Path
 def ui_dupli_group_library_path(self, context):
 
     ob = context.object
+    lib = ob.dupli_group.library.filepath
+
+    row = self.layout.row()
+    row.alignment = 'LEFT'
 
     if ob and ob.dupli_group and ob.dupli_group.library:
-        self.layout.label(text="Library: %s" % ob.dupli_group.library.filepath)
+        row.operator(SCENE_OT_blender_instance_open.bl_idname,
+            text="Library: %s" % lib,
+            emboss=False,
+            icon="LINK_BLEND").filepath=lib
+
 # // FEATURE: Dupli  Group Path
 # FEATURE: Color Management Presets
 class SCENE_MT_color_management_presets(Menu):
@@ -1652,7 +1849,10 @@ classes = (SCENE_MT_color_management_presets,
            SCENE_OT_cycles_shader_list_nodes,
            SCENE_OT_cycles_shader_list_nodes_clear,
            SCENE_OT_amaranth_debug_lamp_select,
-           SCENE_OT_list_missing_node_tree,
+           SCENE_OT_list_missing_node_links,
+           SCENE_OT_list_missing_material_slots,
+           SCENE_OT_list_missing_material_slots_clear,
+           SCENE_OT_blender_instance_open,
            WM_OT_save_reload,
            MESH_OT_find_asymmetric,
            MESH_OT_make_symmetric,