framework/cg/blender/scripts/addons/BakeWrangler/nodes/node_panel.py
2023-10-25 10:54:36 +00:00

299 lines
No EOL
12 KiB
Python

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()