Refactor: Node Wrangler: PreviewNode operator
[blender-addons.git] / amaranth / scene / debug.py
blobaf447beeb4fed52fea18e0b0e8c62aa2f467ba05
1 # SPDX-FileCopyrightText: 2019-2023 Blender Foundation
3 # SPDX-License-Identifier: GPL-2.0-or-later
5 """
6 Scene Debug Panel
8 This is something I've been wanting to have for a while, a way to know
9 certain info about your scene. A way to "debug" it, especially when
10 working in production with other teams, this came in very handy.
12 Being mostly a lighting guy myself, I needed two main features to start with:
14 * List Cycles Material using X shader
15 Where X is any shader type you want. It will display (and print on console)
16 a list of all the materials containing the shader you specified above.
17 Good for finding out if there's any Meshlight (Emission) material hidden,
18 or if there are many glossy shaders making things noisy.
19 A current limitation is that it doesn't look inside node groups (yet,
20 working on it!). It works since 0.8.8!
22 Under the "Scene Debug" panel in Scene properties.
24 * Lighter's Corner
25 This is an UI List of Lights in the scene(s).
26 It allows you to quickly see how many lights you have, select them by
27 clicking on their name, see their type (icon), samples number (if using
28 Branched Path Tracing), size, and change their visibility.
30 """
32 # TODO: module cleanup! maybe break it up in a package
33 # dicts instead of if, elif, else all over the place.
34 # helper functions instead of everything on the execute method.
35 # str.format() + dicts instead of inline % op all over the place.
36 # remove/manage debug print calls.
37 # avoid duplicate code/patterns through helper functions.
39 import os
40 import bpy
41 from amaranth import utils
42 from bpy.types import (
43 Operator,
44 Panel,
45 UIList,
46 PropertyGroup,
48 from bpy.props import (
49 BoolProperty,
50 CollectionProperty,
51 EnumProperty,
52 IntProperty,
53 PointerProperty,
54 StringProperty,
57 # default string used in the List Users for Datablock section menus
58 USER_X_NAME_EMPTY = "Data Block not selected/existing"
61 class AMTH_store_data():
62 # used by: AMTH_SCENE_OT_list_users_for_x operator
63 users = {
64 'OBJECT_DATA': [], # Store Objects with Material
65 'MATERIAL': [], # Materials (Node tree)
66 'LIGHT': [], # Lights
67 'WORLD': [], # World
68 'TEXTURE': [], # Textures (Psys, Brushes)
69 'MODIFIER': [], # Modifiers
70 'MESH_DATA': [], # Vertex Colors
71 'OUTLINER_OB_CAMERA': [], # Background Images in Cameras
72 'OUTLINER_OB_EMPTY': [], # Empty type Image
73 'NODETREE': [], # Compositor
75 libraries = [] # Libraries x type
77 # used by: AMTH_SCENE_OT_list_missing_material_slots operator
78 obj_mat_slots = [] # Missing material slots
79 obj_mat_slots_lib = [] # Libraries with missing material slots
81 # used by: AMTH_SCENE_OT_cycles_shader_list_nodes operator
82 mat_shaders = [] # Materials that use a specific shader
84 # used by : AMTH_SCENE_OT_list_missing_node_links operator
85 count_groups = 0 # Missing node groups count
86 count_images = 0 # Missing node images
87 count_image_node_unlinked = 0 # Unlinked Image nodes
90 def call_update_datablock_type(self, context):
91 try:
92 # Note: this is pretty weak, but updates the operator enum selection
93 bpy.ops.scene.amth_list_users_for_x_type(list_type_select='0')
94 except:
95 pass
98 def init():
99 scene = bpy.types.Scene
101 scene.amaranth_lighterscorner_list_meshlights = BoolProperty(
102 default=False,
103 name="List Meshlights",
104 description="Include light emitting meshes on the list"
106 amth_datablock_types = (
107 ("IMAGE_DATA", "Image", "Image Datablocks", 0),
108 ("MATERIAL", "Material", "Material Datablocks", 1),
109 ("GROUP_VCOL", "Vertex Colors", "Vertex Color Layers", 2),
111 scene.amth_datablock_types = EnumProperty(
112 items=amth_datablock_types,
113 name="Type",
114 description="Datablock Type",
115 default="MATERIAL",
116 update=call_update_datablock_type,
117 options={"SKIP_SAVE"}
119 if utils.cycles_exists():
120 cycles_shader_node_types = (
121 ("BSDF_DIFFUSE", "Diffuse BSDF", "", 0),
122 ("BSDF_GLOSSY", "Glossy BSDF", "", 1),
123 ("BSDF_TRANSPARENT", "Transparent BSDF", "", 2),
124 ("BSDF_REFRACTION", "Refraction BSDF", "", 3),
125 ("BSDF_GLASS", "Glass BSDF", "", 4),
126 ("BSDF_TRANSLUCENT", "Translucent BSDF", "", 5),
127 ("BSDF_ANISOTROPIC", "Anisotropic BSDF", "", 6),
128 ("BSDF_VELVET", "Velvet BSDF", "", 7),
129 ("BSDF_TOON", "Toon BSDF", "", 8),
130 ("SUBSURFACE_SCATTERING", "Subsurface Scattering", "", 9),
131 ("EMISSION", "Emission", "", 10),
132 ("BSDF_HAIR", "Hair BSDF", "", 11),
133 ("BACKGROUND", "Background", "", 12),
134 ("AMBIENT_OCCLUSION", "Ambient Occlusion", "", 13),
135 ("HOLDOUT", "Holdout", "", 14),
136 ("VOLUME_ABSORPTION", "Volume Absorption", "", 15),
137 ("VOLUME_SCATTER", "Volume Scatter", "", 16),
138 ("MIX_SHADER", "Mix Shader", "", 17),
139 ("ADD_SHADER", "Add Shader", "", 18),
140 ('BSDF_PRINCIPLED', 'Principled BSDF', "", 19),
142 scene.amaranth_cycles_node_types = EnumProperty(
143 items=cycles_shader_node_types,
144 name="Shader"
148 def clear():
149 props = (
150 "amaranth_cycles_node_types",
151 "amaranth_lighterscorner_list_meshlights",
153 wm = bpy.context.window_manager
154 for p in props:
155 if wm.get(p):
156 del wm[p]
159 def print_with_count_list(text="", send_list=[]):
160 if text:
161 print("\n* {}\n".format(text))
162 if not send_list:
163 print("List is empty, no items to display")
164 return
166 for i, entry in enumerate(send_list):
167 print('{:02d}. {}'.format(i + 1, send_list[i]))
168 print("\n")
171 def print_grammar(line="", single="", multi="", cond=[]):
172 phrase = single if len(cond) == 1 else multi
173 print("\n* {} {}:\n".format(line, phrase))
176 def reset_global_storage(what="NONE"):
177 if what == "NONE":
178 return
180 if what == "XTYPE":
181 for user in AMTH_store_data.users:
182 AMTH_store_data.users[user] = []
183 AMTH_store_data.libraries = []
185 elif what == "MAT_SLOTS":
186 AMTH_store_data.obj_mat_slots[:] = []
187 AMTH_store_data.obj_mat_slots_lib[:] = []
189 elif what == "NODE_LINK":
190 AMTH_store_data.obj_mat_slots[:] = []
191 AMTH_store_data.count_groups = 0
192 AMTH_store_data.count_images = 0
193 AMTH_store_data.count_image_node_unlinked = 0
195 elif what == "SHADER":
196 AMTH_store_data.mat_shaders[:] = []
199 class AMTH_SCENE_OT_cycles_shader_list_nodes(Operator):
200 """List Cycles materials containing a specific shader"""
201 bl_idname = "scene.cycles_list_nodes"
202 bl_label = "List Materials"
204 @classmethod
205 def poll(cls, context):
206 return utils.cycles_exists() and utils.cycles_active(context)
208 def execute(self, context):
209 node_type = context.scene.amaranth_cycles_node_types
210 roughness = False
211 shaders_roughness = ("BSDF_GLOSSY", "BSDF_DIFFUSE", "BSDF_GLASS")
213 reset_global_storage("SHADER")
215 print("\n=== Cycles Shader Type: {} === \n".format(node_type))
217 for ma in bpy.data.materials:
218 if not ma.node_tree:
219 continue
221 nodes = ma.node_tree.nodes
222 print_unconnected = (
223 "Note: \nOutput from \"{}\" node in material \"{}\" "
224 "not connected\n".format(node_type, ma.name)
227 for no in nodes:
228 if no.type == node_type:
229 for ou in no.outputs:
230 if ou.links:
231 connected = True
232 if no.type in shaders_roughness:
233 roughness = "R: {:.4f}".format(
234 no.inputs["Roughness"].default_value
236 else:
237 roughness = False
238 else:
239 connected = False
240 print(print_unconnected)
242 if ma.name not in AMTH_store_data.mat_shaders:
243 AMTH_store_data.mat_shaders.append(
244 "%s%s [%s] %s%s%s" %
245 ("[L] " if ma.library else "",
246 ma.name,
247 ma.users,
248 "[F]" if ma.use_fake_user else "",
249 " - [%s]" %
250 roughness if roughness else "",
251 " * Output not connected" if not connected else "")
253 elif no.type == "GROUP":
254 if no.node_tree:
255 for nog in no.node_tree.nodes:
256 if nog.type == node_type:
257 for ou in nog.outputs:
258 if ou.links:
259 connected = True
260 if nog.type in shaders_roughness:
261 roughness = "R: {:.4f}".format(
262 nog.inputs["Roughness"].default_value
264 else:
265 roughness = False
266 else:
267 connected = False
268 print(print_unconnected)
270 if ma.name not in AMTH_store_data.mat_shaders:
271 AMTH_store_data.mat_shaders.append(
272 '%s%s%s [%s] %s%s%s' %
273 ("[L] " if ma.library else "",
274 "Node Group: %s%s -> " %
275 ("[L] " if no.node_tree.library else "",
276 no.node_tree.name),
277 ma.name,
278 ma.users,
279 "[F]" if ma.use_fake_user else "",
280 " - [%s]" %
281 roughness if roughness else "",
282 " * Output not connected" if not connected else "")
284 AMTH_store_data.mat_shaders = sorted(list(set(AMTH_store_data.mat_shaders)))
286 message = "No materials with nodes type {} found".format(node_type)
287 if len(AMTH_store_data.mat_shaders) > 0:
288 message = "A total of {} {} using {} found".format(
289 len(AMTH_store_data.mat_shaders),
290 "material" if len(AMTH_store_data.mat_shaders) == 1 else "materials",
291 node_type)
292 print_with_count_list(send_list=AMTH_store_data.mat_shaders)
294 self.report({'INFO'}, message)
295 AMTH_store_data.mat_shaders = sorted(list(set(AMTH_store_data.mat_shaders)))
297 return {"FINISHED"}
300 class AMTH_SCENE_OT_amaranth_object_select(Operator):
301 """Select object"""
302 bl_idname = "scene.amaranth_object_select"
303 bl_label = "Select Object"
305 object_name: StringProperty()
307 def execute(self, context):
308 if not (self.object_name and self.object_name in bpy.data.objects):
309 self.report({'WARNING'},
310 "Object with the given name could not be found. Operation Cancelled")
311 return {"CANCELLED"}
313 obj = bpy.data.objects[self.object_name]
315 bpy.ops.object.select_all(action="DESELECT")
316 obj.select_set(True)
317 context.view_layer.objects.active = obj
319 return {"FINISHED"}
322 class AMTH_SCENE_OT_list_missing_node_links(Operator):
323 """Print a list of missing node links"""
324 bl_idname = "scene.list_missing_node_links"
325 bl_label = "List Missing Node Links"
327 def execute(self, context):
328 missing_groups = []
329 missing_images = []
330 image_nodes_unlinked = []
331 libraries = []
333 reset_global_storage(what="NODE_LINK")
335 for ma in bpy.data.materials:
336 if not ma.node_tree:
337 continue
339 for no in ma.node_tree.nodes:
340 if no.type == "GROUP":
341 if not no.node_tree:
342 AMTH_store_data.count_groups += 1
344 users_ngroup = []
346 for ob in bpy.data.objects:
347 if ob.material_slots and ma.name in ob.material_slots:
348 users_ngroup.append("%s%s%s" % (
349 "[L] " if ob.library else "",
350 "[F] " if ob.use_fake_user else "",
351 ob.name))
353 missing_groups.append(
354 "MA: %s%s%s [%s]%s%s%s\n" %
355 ("[L] " if ma.library else "",
356 "[F] " if ma.use_fake_user else "",
357 ma.name,
358 ma.users,
359 " *** No users *** " if ma.users == 0 else "",
360 "\nLI: %s" %
361 ma.library.filepath if ma.library else "",
362 "\nOB: %s" %
363 ", ".join(users_ngroup) if users_ngroup else "")
365 if ma.library:
366 libraries.append(ma.library.filepath)
368 if no.type == "TEX_IMAGE":
370 outputs_empty = not no.outputs["Color"].is_linked and \
371 not no.outputs["Alpha"].is_linked
373 if no.image:
374 image_path_exists = os.path.exists(
375 bpy.path.abspath(
376 no.image.filepath,
377 library=no.image.library)
380 if outputs_empty or not no.image or not image_path_exists:
382 users_images = []
384 for ob in bpy.data.objects:
385 if ob.material_slots and ma.name in ob.material_slots:
386 users_images.append("%s%s%s" % (
387 "[L] " if ob.library else "",
388 "[F] " if ob.use_fake_user else "",
389 ob.name))
391 if outputs_empty:
392 AMTH_store_data.count_image_node_unlinked += 1
394 image_nodes_unlinked.append(
395 "%s%s%s%s%s [%s]%s%s%s%s%s\n" %
396 ("NO: %s" %
397 no.name,
398 "\nMA: ",
399 "[L] " if ma.library else "",
400 "[F] " if ma.use_fake_user else "",
401 ma.name,
402 ma.users,
403 " *** No users *** " if ma.users == 0 else "",
404 "\nLI: %s" %
405 ma.library.filepath if ma.library else "",
406 "\nIM: %s" %
407 no.image.name if no.image else "",
408 "\nLI: %s" %
409 no.image.filepath if no.image and no.image.filepath else "",
410 "\nOB: %s" %
411 ', '.join(users_images) if users_images else ""))
413 if not no.image or not image_path_exists:
414 AMTH_store_data.count_images += 1
416 missing_images.append(
417 "MA: %s%s%s [%s]%s%s%s%s%s\n" %
418 ("[L] " if ma.library else "",
419 "[F] " if ma.use_fake_user else "",
420 ma.name,
421 ma.users,
422 " *** No users *** " if ma.users == 0 else "",
423 "\nLI: %s" %
424 ma.library.filepath if ma.library else "",
425 "\nIM: %s" %
426 no.image.name if no.image else "",
427 "\nLI: %s" %
428 no.image.filepath if no.image and no.image.filepath else "",
429 "\nOB: %s" %
430 ', '.join(users_images) if users_images else ""))
432 if ma.library:
433 libraries.append(ma.library.filepath)
435 # Remove duplicates and sort
436 missing_groups = sorted(list(set(missing_groups)))
437 missing_images = sorted(list(set(missing_images)))
438 image_nodes_unlinked = sorted(list(set(image_nodes_unlinked)))
439 libraries = sorted(list(set(libraries)))
441 print(
442 "\n\n== %s missing image %s, %s missing node %s and %s image %s unlinked ==" %
443 ("No" if AMTH_store_data.count_images == 0 else str(
444 AMTH_store_data.count_images),
445 "node" if AMTH_store_data.count_images == 1 else "nodes",
446 "no" if AMTH_store_data.count_groups == 0 else str(
447 AMTH_store_data.count_groups),
448 "group" if AMTH_store_data.count_groups == 1 else "groups",
449 "no" if AMTH_store_data.count_image_node_unlinked == 0 else str(
450 AMTH_store_data.count_image_node_unlinked),
451 "node" if AMTH_store_data.count_groups == 1 else "nodes")
453 # List Missing Node Groups
454 if missing_groups:
455 print_with_count_list("Missing Node Group Links", missing_groups)
457 # List Missing Image Nodes
458 if missing_images:
459 print_with_count_list("Missing Image Nodes Link", missing_images)
461 # List Image Nodes with its outputs unlinked
462 if image_nodes_unlinked:
463 print_with_count_list("Image Nodes Unlinked", image_nodes_unlinked)
465 if missing_groups or missing_images or image_nodes_unlinked:
466 if libraries:
467 print_grammar("That's bad, run check", "this library", "these libraries", libraries)
468 print_with_count_list(send_list=libraries)
469 else:
470 self.report({"INFO"}, "Yay! No missing node links")
472 if missing_groups and missing_images:
473 self.report(
474 {"WARNING"},
475 "%d missing image %s and %d missing node %s found" %
476 (AMTH_store_data.count_images,
477 "node" if AMTH_store_data.count_images == 1 else "nodes",
478 AMTH_store_data.count_groups,
479 "group" if AMTH_store_data.count_groups == 1 else "groups")
482 return {"FINISHED"}
485 class AMTH_SCENE_OT_list_missing_material_slots(Operator):
486 """List objects with empty material slots"""
487 bl_idname = "scene.list_missing_material_slots"
488 bl_label = "List Empty Material Slots"
490 def execute(self, context):
491 reset_global_storage("MAT_SLOTS")
493 for ob in bpy.data.objects:
494 for ma in ob.material_slots:
495 if not ma.material:
496 AMTH_store_data.obj_mat_slots.append('{}{}'.format(
497 '[L] ' if ob.library else '', ob.name))
498 if ob.library:
499 AMTH_store_data.obj_mat_slots_lib.append(ob.library.filepath)
501 AMTH_store_data.obj_mat_slots = sorted(list(set(AMTH_store_data.obj_mat_slots)))
502 AMTH_store_data.obj_mat_slots_lib = sorted(list(set(AMTH_store_data.obj_mat_slots_lib)))
504 if len(AMTH_store_data.obj_mat_slots) == 0:
505 self.report({"INFO"},
506 "No objects with empty material slots found")
507 return {"FINISHED"}
509 print(
510 "\n* A total of {} {} with empty material slots was found \n".format(
511 len(AMTH_store_data.obj_mat_slots),
512 "object" if len(AMTH_store_data.obj_mat_slots) == 1 else "objects")
514 print_with_count_list(send_list=AMTH_store_data.obj_mat_slots)
516 if AMTH_store_data.obj_mat_slots_lib:
517 print_grammar("Check", "this library", "these libraries",
518 AMTH_store_data.obj_mat_slots_lib
520 print_with_count_list(send_list=AMTH_store_data.obj_mat_slots_lib)
522 return {"FINISHED"}
525 class AMTH_SCENE_OT_list_users_for_x_type(Operator):
526 bl_idname = "scene.amth_list_users_for_x_type"
527 bl_label = "Select"
528 bl_description = "Select Datablock Name"
530 @staticmethod
531 def fill_where():
532 where = []
533 data_block = bpy.context.scene.amth_datablock_types
535 if data_block == 'IMAGE_DATA':
536 for im in bpy.data.images:
537 if im.name not in {'Render Result', 'Viewer Node'}:
538 where.append(im)
540 elif data_block == 'MATERIAL':
541 where = bpy.data.materials
543 elif data_block == 'GROUP_VCOL':
544 for ob in bpy.data.objects:
545 if ob.type == 'MESH':
546 for v in ob.data.vertex_colors:
547 if v and v not in where:
548 where.append(v)
549 where = list(set(where))
551 return where
553 def avail(self, context):
554 datablock_type = bpy.context.scene.amth_datablock_types
555 where = AMTH_SCENE_OT_list_users_for_x_type.fill_where()
556 items = [(str(i), x.name, x.name, datablock_type, i) for i, x in enumerate(where)]
557 items = sorted(list(set(items)))
558 if not items:
559 items = [('0', USER_X_NAME_EMPTY, USER_X_NAME_EMPTY, "INFO", 0)]
560 return items
562 list_type_select: EnumProperty(
563 items=avail,
564 name="Available",
565 options={"SKIP_SAVE"}
568 @classmethod
569 def poll(cls, context):
570 return bpy.context.scene.amth_datablock_types
572 def execute(self, context):
573 where = self.fill_where()
574 bpy.context.scene.amth_list_users_for_x_name = \
575 where[int(self.list_type_select)].name if where else USER_X_NAME_EMPTY
577 return {'FINISHED'}
580 class AMTH_SCENE_OT_list_users_for_x(Operator):
581 """List users for a particular datablock"""
582 bl_idname = "scene.amth_list_users_for_x"
583 bl_label = "List Users for Datablock"
585 name: StringProperty()
587 def execute(self, context):
588 d = bpy.data
589 x = self.name if self.name else context.scene.amth_list_users_for_x_name
591 if USER_X_NAME_EMPTY in x:
592 self.report({'INFO'},
593 "Please select a DataBlock name first. Operation Cancelled")
594 return {"CANCELLED"}
596 dtype = context.scene.amth_datablock_types
598 reset_global_storage("XTYPE")
600 # IMAGE TYPE
601 if dtype == 'IMAGE_DATA':
602 # Check Materials
603 for ma in d.materials:
604 # Cycles
605 if utils.cycles_exists():
606 if ma and ma.node_tree and ma.node_tree.nodes:
607 materials = []
609 for nd in ma.node_tree.nodes:
610 if nd and nd.type in {'TEX_IMAGE', 'TEX_ENVIRONMENT'}:
611 materials.append(nd)
613 if nd and nd.type == 'GROUP':
614 if nd.node_tree and nd.node_tree.nodes:
615 for ng in nd.node_tree.nodes:
616 if ng.type in {'TEX_IMAGE', 'TEX_ENVIRONMENT'}:
617 materials.append(ng)
619 for no in materials:
620 if no.image and no.image.name == x:
621 objects = []
623 for ob in d.objects:
624 if ma.name in ob.material_slots:
625 objects.append(ob.name)
626 links = False
628 for o in no.outputs:
629 if o.links:
630 links = True
632 name = '"{0}" {1}{2}'.format(
633 ma.name,
634 'in object: {0}'.format(objects) if objects else ' (unassigned)',
635 '' if links else ' (unconnected)')
637 if name not in AMTH_store_data.users['MATERIAL']:
638 AMTH_store_data.users['MATERIAL'].append(name)
640 # Check Lights
641 for la in d.lights:
642 # Cycles
643 if utils.cycles_exists():
644 if la and la.node_tree and la.node_tree.nodes:
645 for no in la.node_tree.nodes:
646 if no and \
647 no.type in {'TEX_IMAGE', 'TEX_ENVIRONMENT'} and \
648 no.image and no.image.name == x:
649 if la.name not in AMTH_store_data.users['LIGHT']:
650 AMTH_store_data.users['LIGHT'].append(la.name)
652 # Check World
653 for wo in d.worlds:
654 # Cycles
655 if utils.cycles_exists():
656 if wo and wo.node_tree and wo.node_tree.nodes:
657 for no in wo.node_tree.nodes:
658 if no and \
659 no.type in {'TEX_IMAGE', 'TEX_ENVIRONMENT'} and \
660 no.image and no.image.name == x:
661 if wo.name not in AMTH_store_data.users['WORLD']:
662 AMTH_store_data.users['WORLD'].append(wo.name)
664 # Check Textures
665 for te in d.textures:
666 if te and te.type == 'IMAGE' and te.image:
667 name = te.image.name
669 if name == x and \
670 name not in AMTH_store_data.users['TEXTURE']:
671 AMTH_store_data.users['TEXTURE'].append(te.name)
673 # Check Modifiers in Objects
674 for ob in d.objects:
675 for mo in ob.modifiers:
676 if mo.type in {'UV_PROJECT'}:
677 image = mo.image
679 if mo and image and image.name == x:
680 name = '"{0}" modifier in {1}'.format(mo.name, ob.name)
681 if name not in AMTH_store_data.users['MODIFIER']:
682 AMTH_store_data.users['MODIFIER'].append(name)
684 # Check Background Images in Cameras
685 for ob in d.objects:
686 if ob and ob.type == 'CAMERA' and ob.data.background_images:
687 for bg in ob.data.background_images:
688 image = bg.image
690 if bg and image and image.name == x:
691 name = 'Used as background for Camera "{0}"'\
692 .format(ob.name)
693 if name not in AMTH_store_data.users['OUTLINER_OB_CAMERA']:
694 AMTH_store_data.users['OUTLINER_OB_CAMERA'].append(name)
696 # Check Empties type Image
697 for ob in d.objects:
698 if ob and ob.type == 'EMPTY' and ob.image_user:
699 if ob.image_user.id_data.data:
700 image = ob.image_user.id_data.data
702 if image and image.name == x:
703 name = 'Used in Empty "{0}"'\
704 .format(ob.name)
705 if name not in AMTH_store_data.users['OUTLINER_OB_EMPTY']:
706 AMTH_store_data.users['OUTLINER_OB_EMPTY'].append(name)
708 # Check the Compositor
709 for sce in d.scenes:
710 if sce.node_tree and sce.node_tree.nodes:
711 nodes = []
712 for nd in sce.node_tree.nodes:
713 if nd.type == 'IMAGE':
714 nodes.append(nd)
715 elif nd.type == 'GROUP':
716 if nd.node_tree and nd.node_tree.nodes:
717 for ng in nd.node_tree.nodes:
718 if ng.type == 'IMAGE':
719 nodes.append(ng)
721 for no in nodes:
722 if no.image and no.image.name == x:
723 links = False
725 for o in no.outputs:
726 if o.links:
727 links = True
729 name = 'Node {0} in Compositor (Scene "{1}"){2}'.format(
730 no.name,
731 sce.name,
732 '' if links else ' (unconnected)')
734 if name not in AMTH_store_data.users['NODETREE']:
735 AMTH_store_data.users['NODETREE'].append(name)
736 # MATERIAL TYPE
737 if dtype == 'MATERIAL':
738 # Check Materials - Note: build an object_check list as only strings are stored
739 object_check = [d.objects[names] for names in AMTH_store_data.users['OBJECT_DATA'] if
740 names in d.objects]
741 for ob in d.objects:
742 for ma in ob.material_slots:
743 if ma.name == x:
744 if ma not in object_check:
745 AMTH_store_data.users['OBJECT_DATA'].append(ob.name)
747 if ob.library:
748 AMTH_store_data.libraries.append(ob.library.filepath)
749 # VERTEX COLOR TYPE
750 elif dtype == 'GROUP_VCOL':
751 # Check VCOL in Meshes
752 for ob in bpy.data.objects:
753 if ob.type == 'MESH':
754 for v in ob.data.vertex_colors:
755 if v.name == x:
756 name = '{0}'.format(ob.name)
758 if name not in AMTH_store_data.users['MESH_DATA']:
759 AMTH_store_data.users['MESH_DATA'].append(name)
760 # Check VCOL in Materials
761 for ma in d.materials:
762 # Cycles
763 if utils.cycles_exists():
764 if ma and ma.node_tree and ma.node_tree.nodes:
765 for no in ma.node_tree.nodes:
766 if no and no.type in {'ATTRIBUTE'}:
767 if no.attribute_name == x:
768 objects = []
770 for ob in d.objects:
771 if ma.name in ob.material_slots:
772 objects.append(ob.name)
774 if objects:
775 name = '{0} in object: {1}'.format(ma.name, objects)
776 else:
777 name = '{0} (unassigned)'.format(ma.name)
779 if name not in AMTH_store_data.users['MATERIAL']:
780 AMTH_store_data.users['MATERIAL'].append(name)
782 AMTH_store_data.libraries = sorted(list(set(AMTH_store_data.libraries)))
784 # Print on console
785 empty = True
786 for t in AMTH_store_data.users:
787 if AMTH_store_data.users[t]:
788 empty = False
789 print('\n== {0} {1} use {2} "{3}" ==\n'.format(
790 len(AMTH_store_data.users[t]),
792 dtype,
794 for p in AMTH_store_data.users[t]:
795 print(' {0}'.format(p))
797 if AMTH_store_data.libraries:
798 print_grammar("Check", "this library", "these libraries",
799 AMTH_store_data.libraries
801 print_with_count_list(send_list=AMTH_store_data.libraries)
803 if empty:
804 self.report({'INFO'}, "No users for {}".format(x))
806 return {"FINISHED"}
809 class AMTH_SCENE_OT_list_users_debug_clear(Operator):
810 """Clear the list below"""
811 bl_idname = "scene.amth_list_users_debug_clear"
812 bl_label = "Clear Debug Panel lists"
814 what: StringProperty(
815 name="",
816 default="NONE",
817 options={'HIDDEN'}
820 def execute(self, context):
821 reset_global_storage(self.what)
823 return {"FINISHED"}
826 class AMTH_SCENE_OT_blender_instance_open(Operator):
827 """Open in a new Blender instance"""
828 bl_idname = "scene.blender_instance_open"
829 bl_label = "Open Blender Instance"
831 filepath: StringProperty()
833 def execute(self, context):
834 if self.filepath:
835 filepath = os.path.normpath(bpy.path.abspath(self.filepath))
837 import subprocess
838 try:
839 subprocess.Popen([bpy.app.binary_path, filepath])
840 except:
841 print("Error opening a new Blender instance")
842 import traceback
843 traceback.print_exc()
845 return {"FINISHED"}
848 class AMTH_SCENE_OT_Collection_List_Refresh(Operator):
849 bl_idname = "scene.amaranth_lighters_corner_refresh"
850 bl_label = "Refresh"
851 bl_description = ("Generate/Refresh the Lists\n"
852 "Use to generate/refresh the list or after changes to Data")
853 bl_options = {"REGISTER", "INTERNAL"}
855 what: StringProperty(default="NONE")
857 def execute(self, context):
858 message = "No changes applied"
860 if self.what == "LIGHTS":
861 fill_ligters_corner_props(context, refresh=True)
863 found_lights = len(context.window_manager.amth_lighters_state.keys())
864 message = "No Lights in the Data" if found_lights == 0 else \
865 "Generated list for {} found light(s)".format(found_lights)
867 elif self.what == "IMAGES":
868 fill_missing_images_props(context, refresh=True)
870 found_images = len(context.window_manager.amth_missing_images_state.keys())
871 message = "Great! No missing Images" if found_images == 0 else \
872 "Missing {} image(s) in the Data".format(found_images)
874 self.report({'INFO'}, message)
876 return {"FINISHED"}
879 class AMTH_SCENE_PT_scene_debug(Panel):
880 """Scene Debug"""
881 bl_label = "Scene Debug"
882 bl_space_type = "PROPERTIES"
883 bl_region_type = "WINDOW"
884 bl_context = "scene"
885 bl_options = {"DEFAULT_CLOSED"}
887 def draw_header(self, context):
888 layout = self.layout
889 layout.label(text="", icon="RADIOBUT_ON")
891 def draw_label(self, layout, body_text, single, multi, lists, ico="BLANK1"):
892 layout.label(
893 text="{} {} {}".format(
894 str(len(lists)), body_text,
895 single if len(lists) == 1 else multi),
896 icon=ico
899 def draw_miss_link(self, layout, text1, single, multi, text2, count, ico="BLANK1"):
900 layout.label(
901 text="{} {} {} {}".format(
902 count, text1,
903 single if count == 1 else multi, text2),
904 icon=ico
907 def draw(self, context):
908 layout = self.layout
909 scene = context.scene
911 has_images = len(bpy.data.images)
912 engine = scene.render.engine
914 # List Missing Images
915 box = layout.box()
916 split = box.split(factor=0.8, align=True)
917 row = split.row()
919 if has_images:
920 subrow = split.row(align=True)
921 subrow.alignment = "RIGHT"
922 subrow.operator(AMTH_SCENE_OT_Collection_List_Refresh.bl_idname,
923 text="", icon="FILE_REFRESH").what = "IMAGES"
924 image_state = context.window_manager.amth_missing_images_state
926 row.label(
927 text="{} Image Blocks present in the Data".format(has_images),
928 icon="IMAGE_DATA"
930 if len(image_state.keys()) > 0:
931 box.template_list(
932 'AMTH_UL_MissingImages_UI',
933 'amth_collection_index_prop',
934 context.window_manager,
935 'amth_missing_images_state',
936 context.window_manager.amth_collection_index_prop,
937 'index_image',
938 rows=3
940 else:
941 row.label(text="No images loaded yet", icon="RIGHTARROW_THIN")
943 # List Cycles Materials by Shader
944 if utils.cycles_exists() and engine == "CYCLES":
945 box = layout.box()
946 split = box.split()
947 col = split.column(align=True)
948 col.prop(scene, "amaranth_cycles_node_types",
949 icon="MATERIAL")
951 row = split.row(align=True)
952 row.operator(AMTH_SCENE_OT_cycles_shader_list_nodes.bl_idname,
953 icon="SORTSIZE",
954 text="List Materials Using Shader")
955 if len(AMTH_store_data.mat_shaders) != 0:
956 row.operator(
957 AMTH_SCENE_OT_list_users_debug_clear.bl_idname,
958 icon="X", text="").what = "SHADER"
959 col.separator()
961 if len(AMTH_store_data.mat_shaders) != 0:
962 col = box.column(align=True)
963 self.draw_label(col, "found", "material", "materials",
964 AMTH_store_data.mat_shaders, "INFO"
966 for i, mat in enumerate(AMTH_store_data.mat_shaders):
967 col.label(
968 text="{}".format(AMTH_store_data.mat_shaders[i]), icon="MATERIAL"
971 # List Missing Node Trees
972 box = layout.box()
973 row = box.row(align=True)
974 split = row.split()
975 col = split.column(align=True)
977 split = col.split(align=True)
978 split.label(text="Node Links")
979 row = split.row(align=True)
980 row.operator(AMTH_SCENE_OT_list_missing_node_links.bl_idname,
981 icon="NODETREE")
983 if AMTH_store_data.count_groups != 0 or \
984 AMTH_store_data.count_images != 0 or \
985 AMTH_store_data.count_image_node_unlinked != 0:
987 row.operator(
988 AMTH_SCENE_OT_list_users_debug_clear.bl_idname,
989 icon="X", text="").what = "NODE_LINK"
990 col.label(text="Warning! Check Console", icon="ERROR")
992 if AMTH_store_data.count_groups != 0:
993 self.draw_miss_link(col, "node", "group", "groups", "missing link",
994 AMTH_store_data.count_groups, "NODE_TREE"
996 if AMTH_store_data.count_images != 0:
997 self.draw_miss_link(col, "image", "node", "nodes", "missing link",
998 AMTH_store_data.count_images, "IMAGE_DATA"
1000 if AMTH_store_data.count_image_node_unlinked != 0:
1001 self.draw_miss_link(col, "image", "node", "nodes", "with no output connected",
1002 AMTH_store_data.count_image_node_unlinked, "NODE"
1005 # List Empty Materials Slots
1006 box = layout.box()
1007 split = box.split()
1008 col = split.column(align=True)
1009 col.label(text="Material Slots")
1011 row = split.row(align=True)
1012 row.operator(AMTH_SCENE_OT_list_missing_material_slots.bl_idname,
1013 icon="MATERIAL",
1014 text="List Empty Materials Slots"
1016 if len(AMTH_store_data.obj_mat_slots) != 0:
1017 row.operator(
1018 AMTH_SCENE_OT_list_users_debug_clear.bl_idname,
1019 icon="X", text="").what = "MAT_SLOTS"
1021 col.separator()
1022 col = box.column(align=True)
1023 self.draw_label(col, "found empty material slot", "object", "objects",
1024 AMTH_store_data.obj_mat_slots, "INFO"
1026 for entry, obs in enumerate(AMTH_store_data.obj_mat_slots):
1027 row = col.row()
1028 row.alignment = "LEFT"
1029 row.label(
1030 text="{}".format(AMTH_store_data.obj_mat_slots[entry]),
1031 icon="OBJECT_DATA")
1033 if AMTH_store_data.obj_mat_slots_lib:
1034 col.separator()
1035 col.label("Check {}:".format(
1036 "this library" if
1037 len(AMTH_store_data.obj_mat_slots_lib) == 1 else
1038 "these libraries")
1040 for ilib, libs in enumerate(AMTH_store_data.obj_mat_slots_lib):
1041 row = col.row(align=True)
1042 row.alignment = "LEFT"
1043 row.operator(
1044 AMTH_SCENE_OT_blender_instance_open.bl_idname,
1045 text=AMTH_store_data.obj_mat_slots_lib[ilib],
1046 icon="LINK_BLEND",
1047 emboss=False).filepath = AMTH_store_data.obj_mat_slots_lib[ilib]
1049 box = layout.box()
1050 row = box.row(align=True)
1051 row.label(text="List Users for Datablock")
1053 col = box.column(align=True)
1054 split = col.split()
1055 row = split.row(align=True)
1056 row.prop(
1057 scene, "amth_datablock_types",
1058 icon=scene.amth_datablock_types,
1059 text=""
1061 row.operator_menu_enum(
1062 "scene.amth_list_users_for_x_type",
1063 "list_type_select",
1064 text=scene.amth_list_users_for_x_name
1067 row = split.row(align=True)
1068 row.enabled = True if USER_X_NAME_EMPTY not in scene.amth_list_users_for_x_name else False
1069 row.operator(
1070 AMTH_SCENE_OT_list_users_for_x.bl_idname,
1071 icon="COLLAPSEMENU").name = scene.amth_list_users_for_x_name
1073 if any(val for val in AMTH_store_data.users.values()):
1074 col = box.column(align=True)
1076 for t in AMTH_store_data.users:
1078 for ma in AMTH_store_data.users[t]:
1079 subrow = col.row(align=True)
1080 subrow.alignment = "LEFT"
1082 if t == 'OBJECT_DATA':
1083 text_lib = " [L] " if \
1084 ma in bpy.data.objects and bpy.data.objects[ma].library else ""
1085 subrow.operator(
1086 AMTH_SCENE_OT_amaranth_object_select.bl_idname,
1087 text="{} {}{}".format(text_lib, ma,
1088 "" if ma in context.scene.objects else " [Not in Scene]"),
1089 icon=t,
1090 emboss=False).object_name = ma
1091 else:
1092 subrow.label(text=ma, icon=t)
1093 row.operator(
1094 AMTH_SCENE_OT_list_users_debug_clear.bl_idname,
1095 icon="X", text="").what = "XTYPE"
1097 if AMTH_store_data.libraries:
1098 count_lib = 0
1100 col.separator()
1101 col.label("Check {}:".format(
1102 "this library" if
1103 len(AMTH_store_data.libraries) == 1 else
1104 "these libraries")
1106 for libs in AMTH_store_data.libraries:
1107 count_lib += 1
1108 row = col.row(align=True)
1109 row.alignment = "LEFT"
1110 row.operator(
1111 AMTH_SCENE_OT_blender_instance_open.bl_idname,
1112 text=AMTH_store_data.libraries[count_lib - 1],
1113 icon="LINK_BLEND",
1114 emboss=False).filepath = AMTH_store_data.libraries[count_lib - 1]
1117 class AMTH_PT_LightersCorner(Panel):
1118 """The Lighters Panel"""
1119 bl_label = "Lighter's Corner"
1120 bl_idname = "AMTH_SCENE_PT_lighters_corner"
1121 bl_space_type = 'PROPERTIES'
1122 bl_region_type = 'WINDOW'
1123 bl_context = "scene"
1124 bl_options = {"DEFAULT_CLOSED"}
1126 def draw_header(self, context):
1127 layout = self.layout
1128 layout.label(text="", icon="LIGHT_SUN")
1130 def draw(self, context):
1131 layout = self.layout
1132 state_props = len(context.window_manager.amth_lighters_state)
1133 engine = context.scene.render.engine
1134 box = layout.box()
1135 row = box.row(align=True)
1137 if utils.cycles_exists():
1138 row.prop(context.scene, "amaranth_lighterscorner_list_meshlights")
1140 subrow = row.row(align=True)
1141 subrow.alignment = "RIGHT"
1142 subrow.operator(AMTH_SCENE_OT_Collection_List_Refresh.bl_idname,
1143 text="", icon="FILE_REFRESH").what = "LIGHTS"
1145 if not state_props:
1146 row = box.row()
1147 message = "Please Refresh" if len(bpy.data.lights) > 0 else "No Lights in Data"
1148 row.label(text=message, icon="INFO")
1149 else:
1150 row = box.row(align=True)
1151 split = row.split(factor=0.5, align=True)
1152 col = split.column(align=True)
1154 col.label(text="Name/Library link")
1156 if engine in ["CYCLES"]:
1157 splits = 0.4
1158 splita = split.split(factor=splits, align=True)
1160 if utils.cycles_exists() and engine == "CYCLES":
1161 col = splita.column(align=True)
1162 col.label(text="Size")
1164 cols = row.row(align=True)
1165 cols.alignment = "RIGHT"
1166 cols.label(text="{}Render Visibility/Selection".format(
1167 "Rays /" if utils.cycles_exists() else "")
1169 box.template_list(
1170 'AMTH_UL_LightersCorner_UI',
1171 'amth_collection_index_prop',
1172 context.window_manager,
1173 'amth_lighters_state',
1174 context.window_manager.amth_collection_index_prop,
1175 'index',
1176 rows=5
1180 class AMTH_UL_MissingImages_UI(UIList):
1182 def draw_item(self, context, layout, data, item, icon, active_data, active_propname):
1183 text_lib = item.text_lib
1184 has_filepath = item.has_filepath
1185 is_library = item.is_library
1187 split = layout.split(factor=0.4)
1188 row = split.row(align=True)
1189 row.alignment = "LEFT"
1190 row.label(text=text_lib, icon="IMAGE_DATA")
1191 image = bpy.data.images.get(item.name, None)
1193 subrow = split.row(align=True)
1194 splitp = subrow.split(factor=0.8, align=True).row(align=True)
1195 splitp.alignment = "LEFT"
1196 row_lib = subrow.row(align=True)
1197 row_lib.alignment = "RIGHT"
1198 if not image:
1199 splitp.label(text="Image is not available", icon="ERROR")
1200 else:
1201 splitp.label(text=has_filepath, icon="LIBRARY_DATA_DIRECT")
1202 if is_library:
1203 row_lib.operator(
1204 AMTH_SCENE_OT_blender_instance_open.bl_idname,
1205 text="",
1206 emboss=False, icon="LINK_BLEND").filepath = is_library
1209 class AMTH_UL_LightersCorner_UI(UIList):
1211 def draw_item(self, context, layout, data, item, icon, active_data, active_propname):
1212 icon_type = item.icon_type
1213 engine = context.scene.render.engine
1214 text_lib = item.text_lib
1215 is_library = item.is_library
1217 split = layout.split(factor=0.35)
1218 row = split.row(align=True)
1219 row.alignment = "LEFT"
1220 row.label(text=text_lib, icon=icon_type)
1221 ob = bpy.data.objects.get(item.name, None)
1222 if not ob:
1223 row.label(text="Object is not available", icon="ERROR")
1224 else:
1225 if is_library:
1226 row.operator(
1227 AMTH_SCENE_OT_blender_instance_open.bl_idname,
1228 text="",
1229 emboss=False, icon="LINK_BLEND").filepath = is_library
1231 rows = split.row(align=True)
1232 splits = 0.4
1233 splitlamp = rows.split(factor=splits, align=True)
1234 splitlampc = splitlamp.row(align=True)
1235 splitlampd = rows.row(align=True)
1236 splitlampd.alignment = "RIGHT"
1238 if utils.cycles_exists() and engine == "CYCLES":
1239 if "LIGHT" in icon_type:
1240 clamp = ob.data.cycles
1241 lamp = ob.data
1242 if lamp.type in ["POINT", "SUN", "SPOT"]:
1243 splitlampc.label(text="{:.2f}".format(lamp.shadow_soft_size))
1244 elif lamp.type == "HEMI":
1245 splitlampc.label(text="N/A")
1246 elif lamp.type == "AREA" and lamp.shape == "RECTANGLE":
1247 splitlampc.label(
1248 text="{:.2f} x {:.2f}".format(lamp.size, lamp.size_y)
1250 else:
1251 splitlampc.label(text="{:.2f}".format(lamp.size))
1252 if utils.cycles_exists():
1253 splitlampd.prop(ob, "visible_camera", text="")
1254 splitlampd.prop(ob, "visible_diffuse", text="")
1255 splitlampd.prop(ob, "visible_glossy", text="")
1256 splitlampd.prop(ob, "visible_shadow", text="")
1257 splitlampd.separator()
1258 splitlampd.prop(ob, "hide_viewport", text="", emboss=False)
1259 splitlampd.prop(ob, "hide_render", text="", emboss=False)
1260 splitlampd.operator(
1261 AMTH_SCENE_OT_amaranth_object_select.bl_idname,
1262 text="",
1263 emboss=False, icon="RESTRICT_SELECT_OFF").object_name = item.name
1266 def fill_missing_images_props(context, refresh=False):
1267 image_state = context.window_manager.amth_missing_images_state
1268 if refresh:
1269 for key in image_state.keys():
1270 index = image_state.find(key)
1271 if index != -1:
1272 image_state.remove(index)
1274 for im in bpy.data.images:
1275 if im.type not in ("UV_TEST", "RENDER_RESULT", "COMPOSITING"):
1276 if not im.packed_file and \
1277 not os.path.exists(bpy.path.abspath(im.filepath, library=im.library)):
1278 text_l = "{}{} [{}]{}".format("[L] " if im.library else "", im.name,
1279 im.users, " [F]" if im.use_fake_user else "")
1280 prop = image_state.add()
1281 prop.name = im.name
1282 prop.text_lib = text_l
1283 prop.has_filepath = im.filepath if im.filepath else "No Filepath"
1284 prop.is_library = im.library.filepath if im.library else ""
1287 def fill_ligters_corner_props(context, refresh=False):
1288 light_state = context.window_manager.amth_lighters_state
1289 list_meshlights = context.scene.amaranth_lighterscorner_list_meshlights
1290 if refresh:
1291 for key in light_state.keys():
1292 index = light_state.find(key)
1293 if index != -1:
1294 light_state.remove(index)
1296 for ob in bpy.data.objects:
1297 if ob.name not in light_state.keys() or refresh:
1298 is_light = ob.type == "LIGHT"
1299 is_emission = True if utils.cycles_is_emission(
1300 context, ob) and list_meshlights else False
1302 if is_light or is_emission:
1303 icons = "LIGHT_%s" % ob.data.type if is_light else "MESH_GRID"
1304 text_l = "{} {}{}".format(" [L] " if ob.library else "", ob.name,
1305 "" if ob.name in context.scene.objects else " [Not in Scene]")
1306 prop = light_state.add()
1307 prop.name = ob.name
1308 prop.icon_type = icons
1309 prop.text_lib = text_l
1310 prop.is_library = ob.library.filepath if ob.library else ""
1313 class AMTH_LightersCornerStateProp(PropertyGroup):
1314 icon_type: StringProperty()
1315 text_lib: StringProperty()
1316 is_library: StringProperty()
1319 class AMTH_MissingImagesStateProp(PropertyGroup):
1320 text_lib: StringProperty()
1321 has_filepath: StringProperty()
1322 is_library: StringProperty()
1325 class AMTH_LightersCollectionIndexProp(PropertyGroup):
1326 index: IntProperty(
1327 name="index"
1329 index_image: IntProperty(
1330 name="index"
1334 classes = (
1335 AMTH_SCENE_PT_scene_debug,
1336 AMTH_SCENE_OT_list_users_debug_clear,
1337 AMTH_SCENE_OT_blender_instance_open,
1338 AMTH_SCENE_OT_amaranth_object_select,
1339 AMTH_SCENE_OT_list_missing_node_links,
1340 AMTH_SCENE_OT_list_missing_material_slots,
1341 AMTH_SCENE_OT_cycles_shader_list_nodes,
1342 AMTH_SCENE_OT_list_users_for_x,
1343 AMTH_SCENE_OT_list_users_for_x_type,
1344 AMTH_SCENE_OT_Collection_List_Refresh,
1345 AMTH_LightersCornerStateProp,
1346 AMTH_LightersCollectionIndexProp,
1347 AMTH_MissingImagesStateProp,
1348 AMTH_PT_LightersCorner,
1349 AMTH_UL_LightersCorner_UI,
1350 AMTH_UL_MissingImages_UI,
1354 def register():
1355 init()
1357 for cls in classes:
1358 bpy.utils.register_class(cls)
1360 bpy.types.Scene.amth_list_users_for_x_name = StringProperty(
1361 default="Select DataBlock Name",
1362 name="Name",
1363 description=USER_X_NAME_EMPTY,
1364 options={"SKIP_SAVE"}
1366 bpy.types.WindowManager.amth_collection_index_prop = PointerProperty(
1367 type=AMTH_LightersCollectionIndexProp
1369 bpy.types.WindowManager.amth_lighters_state = CollectionProperty(
1370 type=AMTH_LightersCornerStateProp
1372 bpy.types.WindowManager.amth_missing_images_state = CollectionProperty(
1373 type=AMTH_MissingImagesStateProp
1377 def unregister():
1378 clear()
1380 for cls in classes:
1381 bpy.utils.unregister_class(cls)
1383 del bpy.types.Scene.amth_list_users_for_x_name
1384 del bpy.types.WindowManager.amth_collection_index_prop
1385 del bpy.types.WindowManager.amth_lighters_state
1386 del bpy.types.WindowManager.amth_missing_images_state