Files
blender-portable-repo/extensions/blender_org/bone_widget/operators.py
T
2026-03-17 15:34:28 -06:00

1621 lines
56 KiB
Python

import bpy
import os
from .functions.main_functions import (
find_match_bones,
from_widget_find_bone,
symmetrize_widget_helper,
match_bone_matrix,
create_widget,
edit_widget,
return_to_armature,
get_collection,
get_view_layer_collection,
recursive_layer_collection,
delete_unused_widgets,
clear_bone_widgets,
resync_widget_names,
add_object_as_widget,
advanced_options_toggled,
set_bone_color,
copy_bone_color,
get_preferences,
)
from .functions.json_functions import (
add_remove_widgets,
get_widget_data,
import_widget_library,
export_widget_library,
update_custom_image,
reset_default_images,
update_widget_library,
save_color_sets,
add_color_set,
scan_armature_color_presets,
import_color_presets,
export_color_presets,
update_color_presets,
)
from .functions.preview_functions import (
remove_custom_image,
copy_custom_image,
create_wireframe_copy,
setup_viewport,
restore_viewport_position,
render_widget_thumbnail,
add_camera_from_view
)
from .props import ImportColorSet, ImportItemData, get_import_options
from .classes import ColorSet
from bpy.props import FloatProperty, BoolProperty, FloatVectorProperty, IntVectorProperty, StringProperty, EnumProperty
class BONEWIDGET_OT_shared_property_group(bpy.types.PropertyGroup):
"""Storage class for Shared Attribute Properties"""
custom_image_data = ("", "")
import_library_filepath = ""
color_sets: bpy.props.CollectionProperty(type=ImportColorSet)
import_item_data: bpy.props.CollectionProperty(type=ImportItemData)
image_collection = bpy.utils.previews.new()
class BONEWIDGET_OT_create_widget(bpy.types.Operator):
"""Creates a widget for selected bone"""
bl_idname = "bonewidget.create_widget"
bl_label = "Create Widget"
bl_options = {'REGISTER', 'UNDO'}
@classmethod
def poll(cls, context):
return (context.object and context.object.mode == 'POSE' and context.selected_pose_bones)
relative_size: BoolProperty(
name="Scale to Bone length",
default=True,
description="Scale Widget to bone length"
)
use_face_data: BoolProperty(
name="Use Face Data",
default=False,
description="When enabled this option will include the widget's face data (if available)"
)
advanced_options: BoolProperty(
name="Advanced options",
default=False,
description="Show advanced options",
update=advanced_options_toggled
)
global_size_simple: FloatProperty(
name="Global Size",
default=1.0,
description="Global Size"
)
global_size_advanced: FloatVectorProperty(
name="Global Size",
default=(1.0, 1.0, 1.0),
subtype='XYZ',
description="Global Size"
)
slide_simple: FloatProperty(
name="Slide",
default=0.0,
subtype='NONE',
unit='NONE',
description="Slide widget along bone y axis"
)
slide_advanced: FloatVectorProperty(
name="Slide",
default=(0.0, 0.0, 0.0),
subtype='XYZ',
unit='NONE',
description="Slide widget along bone xyz axes"
)
rotation: FloatVectorProperty(
name="Rotation",
description="Rotate the widget",
default=(0.0, 0.0, 0.0),
subtype='EULER',
unit='ROTATION',
precision=1,
)
wireframe_width: FloatProperty(
name="Wire Width",
default=2.0,
min=1.0,
max=16,
soft_max=10,
description="Set the thickness of a wireframe widget"
)
def draw(self, context):
layout = self.layout
layout.use_property_split = True
col = layout.column()
row = col.row(align=True)
row.prop(self, "relative_size")
row = col.row(align=True)
if self.advanced_options:
row.prop(self, "use_face_data")
row = col.row(align=True)
row.prop(
self, "global_size_advanced" if self.advanced_options else "global_size_simple", expand=False)
row = col.row(align=True)
row.prop(
self, "slide_advanced" if self.advanced_options else "slide_simple", text="Slide")
row = col.row(align=True)
row.prop(self, "rotation", text="Rotation")
row = col.row(align=True)
if bpy.app.version >= (4, 2, 0):
row.prop(self, "wireframe_width", text="Wire Width")
row = col.row(align=True)
row.prop(self, "advanced_options")
def execute(self, context):
widget_data = get_widget_data(context.window_manager.widget_list)
slide = self.slide_advanced if self.advanced_options else (
0.0, self.slide_simple, 0.0)
global_size = self.global_size_advanced if self.advanced_options else (
self.global_size_simple,) * 3
use_face_data = self.use_face_data if self.advanced_options else False
for bone in bpy.context.selected_pose_bones:
create_widget(bone, widget_data, self.relative_size, global_size, slide, self.rotation,
get_collection(context), use_face_data, self.wireframe_width)
return {'FINISHED'}
class BONEWIDGET_OT_edit_widget(bpy.types.Operator):
"""Edit the widget for selected bone"""
bl_idname = "bonewidget.edit_widget"
bl_label = "Edit Widget"
bl_options = {'REGISTER'}
@classmethod
def poll(cls, context):
return (context.object and context.object.type == 'ARMATURE' and context.object.mode == 'POSE'
and context.active_pose_bone is not None and context.active_pose_bone.custom_shape is not None)
def execute(self, context):
active_bone = context.active_pose_bone
try:
edit_widget(active_bone)
except KeyError:
self.report({'INFO'}, 'This widget is the Widget Collection')
return {'FINISHED'}
class BONEWIDGET_OT_return_to_armature(bpy.types.Operator):
"""Switch back to the armature"""
bl_idname = "bonewidget.return_to_armature"
bl_label = "Return to armature"
bl_options = {'REGISTER'}
@classmethod
def poll(cls, context):
return (context.object and context.object.type == 'MESH'
and context.object.mode in ['EDIT', 'OBJECT'])
def execute(self, context):
b = bpy.context.object
if from_widget_find_bone(bpy.context.object):
return_to_armature(bpy.context.object)
else:
self.report({'INFO'}, 'Object is not a bone widget')
return {'FINISHED'}
class BONEWIDGET_OT_match_bone_transforms(bpy.types.Operator):
"""Match the widget to the bone transforms"""
bl_idname = "bonewidget.match_bone_transforms"
bl_label = "Match bone transforms"
bl_options = {'REGISTER', 'UNDO'}
def execute(self, context):
if bpy.context.mode == "POSE":
for bone in bpy.context.selected_pose_bones:
match_bone_matrix(bone.custom_shape, bone)
else:
for ob in bpy.context.selected_objects:
if ob.type == 'MESH':
match_bone = from_widget_find_bone(ob)
if match_bone:
match_bone_matrix(ob, match_bone)
return {'FINISHED'}
class BONEWIDGET_OT_match_symmetrize_shape(bpy.types.Operator):
"""Symmetrize to the opposite side ONLY if it is named with a .L or .R (default settings)"""
bl_idname = "bonewidget.symmetrize_shape"
bl_label = "Symmetrize"
bl_options = {'REGISTER', 'UNDO'}
@classmethod
def poll(cls, context):
return (context.object and context.object.type == 'ARMATURE'
and context.object.mode in ['POSE'])
def execute(self, context):
widget = bpy.context.active_pose_bone.custom_shape
if widget is None:
self.report({"INFO"}, "There is no widget on this bone.")
return {'FINISHED'}
collection = get_view_layer_collection(context, widget)
widgets_and_bones = find_match_bones()[0]
active_object = find_match_bones()[1]
widgets_and_bones = find_match_bones()[0]
if not active_object:
self.report({"INFO"}, "No active bone or object")
return {'FINISHED'}
for bone in widgets_and_bones:
symmetrize_widget_helper(
bone, collection, active_object, widgets_and_bones)
return {'FINISHED'}
class BONEWIDGET_OT_image_select(bpy.types.Operator):
"""Open a Fileselect browser and get the image location"""
bl_idname = "bonewidget.image_select"
bl_label = "Select Image"
bl_options = {'INTERNAL'}
filter_glob: StringProperty(
default='*.jpg;*.jpeg;*.png;*.tif;',
options={'HIDDEN'}
)
filename: StringProperty(
name='Filename',
subtype='FILE_NAME',
description='Name of custom image',
)
filepath: StringProperty(
subtype="FILE_PATH"
)
def invoke(self, context, event):
self.filename = ""
context.window_manager.fileselect_add(self)
if context.area:
context.area.tag_redraw()
return {'RUNNING_MODAL'}
def execute(self, context):
bpy.context.window_manager.prop_grp.custom_image_name = self.filename
setattr(BONEWIDGET_OT_shared_property_group,
"custom_image_data", (self.filepath, self.filename))
context.area.tag_redraw()
return {'FINISHED'}
class BONEWIDGET_OT_add_custom_image(bpy.types.Operator):
"""Add a custom image to selected preview panel widget"""
bl_idname = "bonewidget.add_custom_image"
bl_label = "Select Image"
bl_options = {'REGISTER', 'UNDO'}
filter_glob: StringProperty(
default='*.jpg;*.jpeg;*.png;*.tif;',
options={'HIDDEN'}
)
filename: StringProperty(
name='Filename',
subtype='FILE_NAME',
description='Name of custom image',
)
filepath: StringProperty(
subtype="FILE_PATH"
)
def invoke(self, context, event):
self.filename = ""
context.window_manager.fileselect_add(self)
return {'RUNNING_MODAL'}
def execute(self, context):
if self.filepath:
# first remove previous custom image if present
current_widget = context.window_manager.widget_list
remove_custom_image(get_widget_data(current_widget).get("image"))
# copy over the image to custom folder
copy_custom_image(self.filepath, self.filename)
# update the json files with new image data
update_custom_image(self.filename)
self.report({'INFO'}, "Custom image has been added!")
return {'FINISHED'}
class BONEWIDGET_OT_add_widgets(bpy.types.Operator):
"""Add selected mesh object to Bone Widget Library and optionally Render Thumbnail"""
bl_idname = "bonewidget.add_widgets"
bl_label = "Add New Widget to Library"
bl_options = {'UNDO'}
widget_name: StringProperty(
name="Widget Name",
default="",
description="The name of the new widget",
options={"TEXTEDIT_UPDATE"},
)
image_mode: EnumProperty(
name="Thumbnail",
description="Choose how the widget image is handled",
items=[
('AUTO_RENDER', "Auto Render", "Render the widget automatically"),
('CUSTOM_IMAGE', "Custom Image", "Use a custom image"),
('PLACEHOLDER_IMAGE', "Placeholder Image", "Use the placeholder image"),
],
default='AUTO_RENDER'
)
@classmethod
def poll(cls, context):
return (context.object and context.object.type == 'MESH' and context.object.mode == 'OBJECT'
and context.active_object is not None)
def draw(self, context):
layout = self.layout
row = layout.row()
row.label(text="Widget Name:")
row.prop(self, "widget_name", text="")
row = layout.row()
# adding custom image this way doesn't work in blender 3.6
if bpy.app.version > (3, 7, 0):
row.prop(self, "image_mode")
if self.image_mode == 'CUSTOM_IMAGE':
row = layout.row()
if bpy.app.version >= (4, 1, 0):
row.prop(bpy.context.window_manager.prop_grp, "custom_image_name",
text="", placeholder="Choose an image...", icon="FILE_IMAGE")
else:
row.prop(bpy.context.window_manager.prop_grp,
"custom_image_name", text="", icon="FILE_IMAGE")
row.operator('bonewidget.image_select',
icon='FILEBROWSER', text="")
def invoke(self, context, event):
if bpy.context.selected_objects:
self.widget_name = context.active_object.name
setattr(BONEWIDGET_OT_shared_property_group,
"custom_image_name", StringProperty(name="Image Name"))
return context.window_manager.invoke_props_dialog(self)
self.report({'WARNING'}, 'Please select an object first!')
return {'CANCELLED'}
def execute(self, context):
objects = []
if bpy.context.mode == "POSE":
for bone in bpy.context.selected_pose_bones:
objects.append(bone.custom_shape)
else:
for ob in bpy.context.selected_objects:
if ob.type == 'MESH':
objects.append(ob)
if not objects:
self.report({'WARNING'}, 'Select Meshes or Pose bones')
return {'CANCELLED'}
# make sure widget name isn't empty
if not self.widget_name:
self.report({'WARNING'}, "Widget name can't be empty!")
return {'CANCELLED'}
# get filepath to custom image if specified and transfer to custom image folder
custom_image_name = ""
custom_image_path = ""
message_extra = ""
if self.image_mode == 'CUSTOM_IMAGE':
# context.window_manager.custom_image
custom_image_path, custom_image_name = bpy.context.window_manager.prop_grp.custom_image_data
# no image path found
if not custom_image_path:
# check if user pasted an image path into text field
text_field = bpy.context.window_manager.prop_grp.custom_image_name
if os.path.isfile(text_field) and text_field.endswith((".jpg", ".jpeg" ".png", ".tif")):
custom_image_name = os.path.basename(text_field)
custom_image_path = text_field
else:
message_extra = " - WARNING - No custom image specified!"
if custom_image_name and custom_image_path:
copy_custom_image(custom_image_path, custom_image_name)
# make sure the field is empty for next time
bpy.context.window_manager.prop_grp.custom_image_name = ""
elif self.image_mode == 'PLACEHOLDER_IMAGE':
# Use the user_defined image
directory = os.path.abspath(os.path.join(
os.path.dirname(__file__), '..', 'thumbnails'))
custom_image_path = os.path.join(directory, "user_defined.png")
elif self.image_mode == 'AUTO_RENDER':
# Render the widget
custom_image_name = self.widget_name + '.png'
bpy.ops.bonewidget.render_widget_thumbnail(
image_name=custom_image_name, use_blend_path=False)
custom_image_path = os.path.abspath(os.path.join(
os.path.dirname(__file__), '..', 'custom_thumbnails'))
message_type, return_message = add_remove_widgets(context, "add", bpy.types.WindowManager.widget_list.keywords['items'],
objects, self.widget_name, custom_image_name)
if return_message:
self.report({message_type}, return_message + message_extra)
return {'FINISHED'}
class BONEWIDGET_OT_remove_widgets(bpy.types.Operator):
"""Remove selected widget object from the Bone Widget Library"""
bl_idname = "bonewidget.remove_widgets"
bl_label = "Remove Widgets"
bl_options = {'INTERNAL'}
def execute(self, context):
objects = bpy.context.window_manager.widget_list
# try and remove the image - will abort if no custom image assigned or if missing
remove_custom_image(get_widget_data(objects).get("image"))
message_type, return_message = add_remove_widgets(
context, "remove", bpy.types.WindowManager.widget_list.keywords['items'], objects)
if return_message:
self.report({message_type}, return_message)
return {'FINISHED'}
class BONEWIDGET_OT_import_items_summary_popup(bpy.types.Operator):
"""Display summary of imported Items"""
bl_idname = "bonewidget.import_summary_popup"
bl_label = "Imported Item Summary"
bl_options = {'INTERNAL'}
def draw(self, context):
layout = self.layout
layout.scale_x = 1.2
layout.separator()
row = layout.row()
if context.window_manager.custom_data.json_import_error:
row.alert = True
row.label(text=f"Error: Unsupported or damaged import file!")
row.alert = False
layout.separator()
else:
row.label(
text=f"Imported Items: {context.window_manager.custom_data.imported()}")
row = layout.row()
row.label(
text=f"Skipped Items: {context.window_manager.custom_data.skipped()}")
row = layout.row()
row.label(
text=f"Failed Items: {context.window_manager.custom_data.failed()}")
def invoke(self, context, event):
return context.window_manager.invoke_props_dialog(self)
def execute(self, context):
return {'FINISHED'}
def update_selected_options(self, context):
wm = context.window_manager
selected_values = []
items = wm.prop_grp.import_item_data
for i, item in enumerate(items):
if self.select_all_items:
selected_values.append(item.import_option)
if item.import_option != "RENAME":
item.import_option = "OVERWRITE"
else:
if i < len(BONEWIDGET_OT_import_items_ask_popup.selected_options_values):
value = BONEWIDGET_OT_import_items_ask_popup.selected_options_values[i]
if value != "RENAME":
item.import_option = value
if self.select_all_items:
# reset and store only once
BONEWIDGET_OT_import_items_ask_popup.selected_options_values = selected_values
class BONEWIDGET_OT_import_items_ask_popup(bpy.types.Operator):
"""Ask user how to handle name collisions from the imported items"""
bl_idname = "bonewidget.import_items_ask_popup"
bl_label = "Imported Items"
bl_options = {'INTERNAL'}
import_options = get_import_options()
select_all_items: BoolProperty(name="Select All", description="Will select all items to be added",
default=False, update=update_selected_options)
selected_options_values = []
def draw(self, context):
layout = self.layout
layout.scale_x = 1.2
# layout.separator()
row = layout.row()
row.label(text="Choose an action:")
imported_items = context.window_manager.prop_grp.import_item_data
for i, _ in enumerate(self.custom_import_data.skipped_imports):
imported_item = imported_items[i]
if self.custom_import_data.import_type == "widget":
row = layout.row(align=True)
row.scale_x = 2.0
# Rename
if imported_item.import_option == self.import_options[2][0]:
row.prop(imported_item, "name", text="")
else:
row.label(text=str(imported_item.name))
widget_name = self.custom_import_data.skipped_imports[i].name
icon_id = context.window_manager.prop_grp.image_collection[widget_name].icon_id
icon_row = row.row(align=True)
icon_row.scale_x = 6
icon_row.template_icon(icon_id, scale=1.4)
row.separator(factor=0.4)
row.prop(imported_item, "import_option", text="")
elif self.custom_import_data.import_type == "colorset":
row = layout.row(align=True)
row.scale_x = 3.0
# Rename
if imported_item.import_option == self.import_options[2][0]:
row.prop(imported_item, "name", text="")
else:
row.label(text=str(imported_item.name))
row.separator(factor=0.4)
# color sets
color_set = context.window_manager.prop_grp.color_sets[i]
split = row.split(factor=0.9)
color_row = split.row(align=True)
color_row.prop(color_set, "normal", text="")
color_row.prop(color_set, "select", text="")
color_row.prop(color_set, "active", text="")
# options dropdown
row.separator(factor=0.4)
row.prop(imported_item, "import_option", text="")
row = layout.row()
row = layout.row()
row = layout.row()
row.prop(self, "select_all_items")
layout.separator()
def invoke(self, context, event):
self.custom_import_data = bpy.context.window_manager.custom_data
import_type = self.custom_import_data.import_type
# make sure class values are empty
BONEWIDGET_OT_import_items_ask_popup.selected_options_values = []
# make sure the shared property group has a clean slate
context.window_manager.prop_grp.color_sets.clear()
context.window_manager.prop_grp.import_item_data.clear()
context.window_manager.prop_grp.image_collection.clear()
# generate the x number of drop down lists and widget names needed
for n, widget in enumerate(self.custom_import_data.skipped_imports):
# add new imported item
import_item = context.window_manager.prop_grp.import_item_data.add()
import_item.name = widget.name
# add the color fields if the import is a color set
if import_type == "colorset":
color_instance = context.window_manager.prop_grp.color_sets.add()
color_instance.name = widget.name
color_instance.normal = widget.normal
color_instance.select = widget.select
color_instance.active = widget.active
# widget preview images
if import_type == "widget":
image_path = os.path.join(
bpy.app.tempdir, "custom_thumbnails", widget.image)
context.window_manager.prop_grp.image_collection.load(
widget.name, image_path, 'IMAGE')
return context.window_manager.invoke_props_dialog(self, width=350)
def execute(self, context):
widget_results = {}
widget_images = set()
import_type = self.custom_import_data.import_type
total_imports = len(self.custom_import_data.skipped_imports)
for i, widget in enumerate(self.custom_import_data.skipped_imports[:]):
imported_item = context.window_manager.prop_grp.import_item_data[i]
action = imported_item.import_option
if action == self.import_options[1][0]: # skip
continue
new_widget_name = imported_item.name
# error check before proceeding - widget renamed to empty string
if widget.name != new_widget_name and new_widget_name.strip() == "":
self.custom_import_data.failed_imports.update(widget)
continue
if import_type == "widget":
widget_data = widget
widget_image = widget.image
# only append custom images
widget_image = widget_image if widget_image != "user_defined.png" else ""
elif import_type == "colorset":
widget_data = context.window_manager.prop_grp.color_sets[i]
widget_data = ColorSet.from_pg(
widget_data) # convert to ColorSet class
if action == self.import_options[0][0]: # overwrite
if import_type == "widget":
widget_results.update(widget_data.to_dict())
if widget_image:
widget_images.add(widget_image)
elif import_type == "colorset":
# check if the import item name exists already and if it does, overwrite
color_set_list = context.window_manager.custom_color_presets
for index, item in enumerate(color_set_list):
if item.name == new_widget_name:
# Update the existing entry
item.normal = widget_data.normal
item.select = widget_data.select
item.active = widget_data.active
break
else:
widget_data.name = new_widget_name
add_color_set(context, widget_data)
elif action == self.import_options[2][0]: # Rename
widget_data.name = new_widget_name
if import_type == "widget":
# we need the dict version
widget_results.update(widget_data.to_dict())
if widget_image:
widget_images.add(widget_image)
elif import_type == "colorset":
add_color_set(context, widget_data)
# update the stats
self.custom_import_data.new_imported_items += 1
self.custom_import_data.skipped_imports.remove(widget)
if import_type == "widget":
update_widget_library(widget_results, widget_images,
bpy.context.window_manager.prop_grp.import_library_filepath)
# clear image collection if widgets were imported
context.window_manager.prop_grp.image_collection.clear()
# clear out all import item data
context.window_manager.prop_grp.import_item_data.clear()
# clear out all color sets
context.window_manager.prop_grp.color_sets.clear()
# del bpy.types.WindowManager.custom_data
self.custom_import_data = None
# reset previous selected options
BONEWIDGET_OT_import_items_ask_popup.selected_options_values = []
# display summary of imported widgets
bpy.ops.bonewidget.import_summary_popup('INVOKE_DEFAULT')
return {'FINISHED'}
class BONEWIDGET_OT_import_widget_library(bpy.types.Operator):
"""Import User Defined Widgets"""
bl_idname = "bonewidget.import_widget_library"
bl_label = "Import Library"
bl_options = {'REGISTER'}
filter_glob: StringProperty(
default='*.zip',
options={'HIDDEN'}
)
filename: StringProperty(
name='Filename',
subtype='FILE_NAME',
description='Name of file to be imported',
)
filepath: StringProperty(
subtype="FILE_PATH"
)
import_option: EnumProperty(
name="Import Option",
items=[
("OVERWRITE", "Overwrite", "Overwrite existing widget"),
("SKIP", "Skip", "Skip widget"),
("ASK", "Ask", "Ask user what to do")],
default="ASK",
)
def draw(self, context):
layout = self.layout
row = layout.row(align=True)
row.label(text="If duplicates are found:")
row = layout.row(align=True)
row.prop(self, "import_option", expand=True)
def execute(self, context):
if self.filepath and self.import_option:
import_library_data = import_widget_library(
self.filepath, self.import_option)
setattr(BONEWIDGET_OT_shared_property_group,
"import_library_filepath", self.filepath)
bpy.types.WindowManager.custom_data = import_library_data
# if the number of failed widgets are equal to total imported widgets - call summary popup
if import_library_data.failed() == import_library_data.total() or import_library_data.failed() == -1:
import_library_data.reset_imports()
bpy.ops.bonewidget.import_summary_popup('INVOKE_DEFAULT')
elif self.import_option == "ASK":
bpy.ops.bonewidget.import_items_ask_popup('INVOKE_DEFAULT')
elif self.import_option in ["OVERWRITE", "SKIP"]:
widget_images = set()
widgets = {}
# convert Widget objects to dict items and extract image names if any
for widget in import_library_data.imported_items:
widgets.update(widget.to_dict())
widget_images.add(widget.image)
update_widget_library(widgets, widget_images, self.filepath)
bpy.ops.bonewidget.import_summary_popup('INVOKE_DEFAULT')
else:
bpy.ops.bonewidget.import_summary_popup('INVOKE_DEFAULT')
return {'FINISHED'}
def invoke(self, context, event):
self.filename = ""
context.window_manager.fileselect_add(self)
return {'RUNNING_MODAL'}
class BONEWIDGET_OT_export_widget_library(bpy.types.Operator):
"""Export User Defined Widgets"""
bl_idname = "bonewidget.export_widget_library"
bl_label = "Export Library"
bl_options = {'REGISTER'}
filter_glob: StringProperty(
default='*.zip',
options={'HIDDEN'}
)
filename: StringProperty(
name='Filename',
subtype='FILE_NAME',
description='Name of file to be exported',
)
filepath: StringProperty(
subtype="FILE_PATH"
)
def execute(self, context):
if self.filepath and self.filename:
num_widgets = export_widget_library(self.filepath)
if num_widgets:
self.report(
{'INFO'}, f"{num_widgets} user defined widgets exported successfully!")
else:
self.report({'INFO'}, "0 user defined widgets exported!")
return {'FINISHED'}
def invoke(self, context, event):
self.filename = "widget_library.zip"
context.window_manager.fileselect_add(self)
return {'RUNNING_MODAL'}
class BONEWIDGET_OT_toggle_collection_visibility(bpy.types.Operator):
"""Show/hide the bone widget collection"""
bl_idname = "bonewidget.toggle_collection_visibilty"
bl_label = "Collection Visibilty"
bl_options = {'INTERNAL'}
@classmethod
def poll(cls, context):
return (context.object and context.object.type == 'ARMATURE' and context.object.mode == 'POSE')
def execute(self, context):
if not get_preferences(context).use_rigify_defaults:
bw_collection_name = get_preferences(
context).bonewidget_collection_name
else:
bw_collection_name = 'WGTS_' + context.active_object.name
bw_collection = recursive_layer_collection(
bpy.context.view_layer.layer_collection, bw_collection_name)
bw_collection.hide_viewport = not bw_collection.hide_viewport
# need to recursively search for the view_layer
bw_collection.exclude = False
return {'FINISHED'}
class BONEWIDGET_OT_delete_unused_widgets(bpy.types.Operator):
"""Delete unused objects in the WGT collection"""
bl_idname = "bonewidget.delete_unused_widgets"
bl_label = "Delete Unused Widgets"
bl_options = {'REGISTER', 'UNDO'}
@classmethod
def poll(cls, context):
return (context.object and context.object.type == 'ARMATURE' and context.object.mode == 'POSE')
def execute(self, context):
try:
delete_unused_widgets()
except:
self.report(
{'INFO'}, "Can't find the Widget Collection. Does it exist?")
return {'FINISHED'}
class BONEWIDGET_OT_clear_bone_widgets(bpy.types.Operator):
"""Clears widgets from selected pose bones but doesn't remove them from the scene"""
bl_idname = "bonewidget.clear_widgets"
bl_label = "Clear Widgets"
bl_options = {'REGISTER', 'UNDO'}
@classmethod
def poll(cls, context):
return (context.object and context.object.type == 'ARMATURE' and context.object.mode == 'POSE')
def execute(self, context):
clear_bone_widgets()
return {'FINISHED'}
class BONEWIDGET_OT_resync_widget_names(bpy.types.Operator):
"""Clear widgets from selected pose bones"""
bl_idname = "bonewidget.resync_widget_names"
bl_label = "Resync Widget Names"
bl_options = {'REGISTER', 'UNDO'}
@classmethod
def poll(cls, context):
return (context.object and context.object.type == 'ARMATURE' and context.object.mode == 'POSE')
def execute(self, context):
resync_widget_names()
return {'FINISHED'}
class BONEWIDGET_OT_add_object_as_widget(bpy.types.Operator):
"""Add selected object as widget for active bone"""
bl_idname = "bonewidget.add_as_widget"
bl_label = "Confirm selected Object as widget shape"
bl_options = {'REGISTER', 'UNDO'}
@classmethod
def poll(cls, context):
return (len(context.selected_objects) == 2 and context.object.mode == 'POSE')
def execute(self, context):
add_object_as_widget(context, get_collection(context))
return {'FINISHED'}
class BONEWIDGET_OT_reset_default_images(bpy.types.Operator):
"""Resets the thumbnails for all default widgets"""
bl_idname = "bonewidget.reset_default_images"
bl_label = "Reset"
bl_options = {'INTERNAL'}
def execute(self, context):
reset_default_images()
return {'FINISHED'}
class BONEWIDGET_OT_user_data_filebrowser(bpy.types.Operator):
"""Select Location for Custom User Data"""
bl_idname = "bonewidget.user_data_filebrowser"
bl_label = "Select Location"
bl_options = {'INTERNAL'}
directory: StringProperty(
name="User Data Directory",
description="Choose a directory to store user data",
subtype='DIR_PATH'
)
def execute(self, context):
get_preferences(context).user_data_location = self.directory
# self.report({'INFO'}, f"User data path set to: {self.directory}")
return {'FINISHED'}
def invoke(self, context, event):
context.window_manager.fileselect_add(self)
return {'RUNNING_MODAL'}
class BONEWIDGET_OT_set_bone_color(bpy.types.Operator):
"""Add bone color to selected widgets"""
bl_idname = "bonewidget.set_bone_color"
bl_label = "Set Bone Color to Widget"
bl_options = {'REGISTER', 'UNDO'}
@classmethod
def poll(cls, context):
return (context.object and context.object.type == 'ARMATURE' and context.object.mode in ['POSE', 'EDIT'] and
(context.selected_bones or context.selected_pose_bones))
def execute(self, context):
set_bone_color(context, context.scene.bw_settings.bone_widget_colors)
return {'FINISHED'}
class BONEWIDGET_OT_clear_bone_color(bpy.types.Operator):
"""Add bone color to selected widgets"""
bl_idname = "bonewidget.clear_bone_color"
bl_label = "Clear Bone Color"
bl_description = (
"Clear Bone Color from selected bones.\n"
"(Note: Blender will show Edit Bone color in Pose Mode if Pose Bone color is default)"
)
bl_options = {'REGISTER', 'UNDO'}
@classmethod
def poll(cls, context):
return (context.object and context.object.type == 'ARMATURE' and context.object.mode in ['POSE', 'EDIT'] and
(context.selected_bones or context.selected_pose_bones))
def execute(self, context):
set_bone_color(context, "DEFAULT", get_preferences(
context).clear_both_modes)
return {'FINISHED'}
class BONEWIDGET_OT_copy_bone_color(bpy.types.Operator):
"""Copy the colors of the active bone to the custom colors above (ignores default colors)"""
bl_idname = "bonewidget.copy_bone_color"
bl_label = "Copy Bone Color"
bl_options = {'REGISTER', 'UNDO'}
@classmethod
def poll(cls, context):
if not context.object:
return False
bones = context.selected_pose_bones if context.object.mode == 'POSE' else context.selected_bones
return (context.object and context.object.type == 'ARMATURE'
and context.object.mode in ['POSE', 'EDIT'] and len(bones) == 1)
def execute(self, context):
if context.object.mode == 'POSE':
selected_bone = context.selected_pose_bones[0]
if not selected_bone.color.is_custom and not 'THEME' in selected_bone.color.palette:
selected_bone = context.active_bone
copy_bone_color(context, selected_bone)
elif context.object.mode == 'EDIT':
copy_bone_color(context, context.selected_bones[0])
return {'FINISHED'}
class BONEWIDGET_OT_add_color_set_from(bpy.types.Operator):
"""Adds a color set to presets from selected Theme or from custom palette"""
bl_idname = "bonewidget.add_color_set_from"
bl_label = "Add color set to presets"
bl_options = {'REGISTER', 'UNDO'}
@classmethod
def poll(cls, context):
return (
context.object and
context.object.type == 'ARMATURE' and
(
(context.object.mode == 'POSE' and context.selected_pose_bones) or
(context.object.mode == 'EDIT' and context.selected_editable_bones and get_preferences(
context).edit_bone_colors != 'DEFAULT')
)
)
def execute(self, context):
base_name = "Color Set"
new_name = base_name
count = 1
while any(item.name == new_name for item in context.window_manager.custom_color_presets):
new_name = f"{base_name}.{count:03d}"
count += 1
new_item = context.window_manager.custom_color_presets.add()
if context.scene.bw_settings.bone_widget_colors == "CUSTOM":
# add item from custom color palette
new_item.name = new_name
if context.object.mode == 'POSE':
new_item.normal = context.scene.bw_settings.custom_pose_color_set.normal
new_item.select = context.scene.bw_settings.custom_pose_color_set.select
new_item.active = context.scene.bw_settings.custom_pose_color_set.active
elif context.object.mode == "EDIT" and \
get_preferences(context).edit_bone_colors != 'DEFAULT': # edit mode colors if turned on in preferences
new_item.normal = context.scene.bw_settings.custom_edit_color_set.normal
new_item.select = context.scene.bw_settings.custom_edit_color_set.select
new_item.active = context.scene.bw_settings.custom_edit_color_set.active
elif "THEME" in context.scene.bw_settings.bone_widget_colors:
# add item from selected theme
theme = context.scene.bw_settings.bone_widget_colors
theme_id = int(theme[-2:]) - 1
theme_color_set = bpy.context.preferences.themes[0].bone_color_sets[theme_id]
new_item.name = theme
new_item.normal = theme_color_set.normal
new_item.select = theme_color_set.select
new_item.active = theme_color_set.active
# save_color_sets(context)
return {'FINISHED'}
class BONEWIDGET_OT_add_default_colorset(bpy.types.Operator):
"""Adds a default color set to presets"""
bl_idname = "bonewidget.add_default_custom_colorset"
bl_label = "Add a default color set"
bl_options = {'INTERNAL'}
def execute(self, context):
add_color_set(context)
return {'FINISHED'}
class BONEWIDGET_OT_add_colorset_to_bone(bpy.types.Operator):
"""Adds a bone color set to selected bones"""
bl_idname = "bonewidget.add_colorset_to_bone"
bl_label = "Apply selected color set to selected bones - mode sensitive"
bl_options = {'REGISTER', 'UNDO'}
@classmethod
def poll(cls, context):
if context.object:
bones = context.selected_pose_bones if context.object.mode == 'POSE' else context.selected_bones
return (context.object.type == 'ARMATURE'
and context.object.mode in ['POSE', 'EDIT'] and len(bones) >= 1) \
and not (context.object.mode == "EDIT"
and get_preferences(context).edit_bone_colors == 'DEFAULT')
def execute(self, context):
if context.object.mode == "EDIT" and \
get_preferences(context).edit_bone_colors != 'DEFAULT':
selected_bones = context.selected_bones
elif context.object.mode == "POSE":
selected_bones = context.selected_pose_bones
else:
return {'CANCELLED'}
if selected_bones:
for bone in selected_bones:
bone.color.palette = "CUSTOM"
index = context.window_manager.colorset_list_index
item = context.window_manager.custom_color_presets[index]
bone.color.custom.normal = item.normal
bone.color.custom.select = item.select
bone.color.custom.active = item.active
return {'FINISHED'}
class BONEWIDGET_OT_remove_item(bpy.types.Operator):
"""Removes selected color set from the preset list"""
bl_idname = "bonewidget.remove_custom_item"
bl_label = "Remove Selected Color Set"
bl_options = {'INTERNAL'}
@classmethod
def poll(cls, context):
return context.window_manager.colorset_list_index >= 0 and not context.scene.bw_settings.lock_colorset_color_changes
def execute(self, context):
my_list = context.window_manager.custom_color_presets
index = context.window_manager.colorset_list_index
my_list.remove(index)
context.window_manager.colorset_list_index = min(
max(0, index - 1), len(my_list) - 1)
save_color_sets(context)
return {'FINISHED'}
class BONEWIDGET_OT_lock_custom_colorset_changes(bpy.types.Operator):
"""Locks/Unlocks the ability to save changes to color set items"""
bl_idname = "bonewidget.lock_custom_colorset_changes"
bl_label = "Lock/Unlock changes to color set presets"
bl_options = {'INTERNAL'}
def execute(self, context):
context.scene.bw_settings.lock_colorset_color_changes = not context.scene.bw_settings.lock_colorset_color_changes
return {'FINISHED'}
class BONEWIDGET_OT_move_custom_item_up(bpy.types.Operator):
"""Moves the selected color set up in the list"""
bl_idname = "bonewidget.move_custom_item_up"
bl_label = "Move Custom Item Up"
bl_options = {'INTERNAL'}
def execute(self, context):
wm = context.window_manager
idx = wm.colorset_list_index
if idx > 0:
wm.custom_color_presets.move(idx, idx - 1)
wm.colorset_list_index -= 1
save_color_sets(context)
return {'FINISHED'}
class BONEWIDGET_OT_move_custom_item_down(bpy.types.Operator):
"""Moves the selected color set down in the list"""
bl_idname = "bonewidget.move_custom_item_down"
bl_label = "Move Custom Item Down"
bl_options = {'INTERNAL'}
def execute(self, context):
wm = context.window_manager
idx = wm.colorset_list_index
if idx < len(wm.custom_color_presets) - 1:
wm.custom_color_presets.move(idx, idx + 1)
wm.colorset_list_index += 1
save_color_sets(context)
return {'FINISHED'}
class BONEWIDGET_OT_add_preset_from_bone(bpy.types.Operator):
"""Adds new preset from the active bone's color palette"""
bl_idname = "bonewidget.add_preset_from_bone"
bl_label = "Add Preset from active Bone"
bl_options = {'REGISTER'}
@classmethod
def poll(cls, context):
return (
context.object and
context.object.type == 'ARMATURE' and
(
(context.object.mode == 'POSE' and context.selected_pose_bones) or
(context.object.mode == 'EDIT' and context.selected_editable_bones and get_preferences(
context).edit_bone_colors != 'DEFAULT')
)
)
def execute(self, context):
base_name = "Color Set"
new_name = base_name
count = 1
bone = context.active_pose_bone if context.object.mode == 'POSE' else context.active_bone
# do some validation checking
if bone.color.palette == 'DEFAULT':
mode = "pose mode" if context.object.mode == 'POSE' else "edit mode"
self.report(
{'WARNING'}, f"No available color set found in {mode}!")
return {'CANCELLED'}
existing_names = {
item.name for item in context.window_manager.custom_color_presets}
while new_name in existing_names:
new_name = f"{base_name}.{count:03d}"
count += 1
new_item = context.window_manager.custom_color_presets.add()
if bone.color.is_custom:
# add item from custom color palette of active bone
new_item.name = new_name
new_item.normal = bone.color.custom.normal
new_item.select = bone.color.custom.select
new_item.active = bone.color.custom.active
elif "THEME" in bone.color.palette:
# add item from selected theme of active bone
theme = bone.color.palette
theme_id = int(theme[-2:]) - 1
theme_color_set = bpy.context.preferences.themes[0].bone_color_sets[theme_id]
new_item.name = theme
new_item.normal = theme_color_set.normal
new_item.select = theme_color_set.select
new_item.active = theme_color_set.active
# save_color_sets(context)
return {'FINISHED'}
class BONEWIDGET_OT_add_presets_from_armature(bpy.types.Operator):
"""Adds new presets from the active bone's color palette"""
bl_idname = "bonewidget.add_presets_from_armature"
bl_label = "Add Preset from selected Armature"
bl_options = {'REGISTER'}
@classmethod
def poll(cls, context):
return (
context.object and
context.object.type == 'ARMATURE' and
(
(context.object.mode == 'POSE' and context.selected_pose_bones) or
(context.object.mode == 'EDIT' and context.selected_editable_bones and get_preferences(
context).edit_bone_colors != 'DEFAULT')
)
)
def execute(self, context):
armature = context.object.data
colorset_imports = scan_armature_color_presets(context, armature)
if colorset_imports.skipped_imports:
bpy.types.WindowManager.custom_data = colorset_imports
bpy.ops.bonewidget.import_items_ask_popup('INVOKE_DEFAULT')
else:
self.report({'INFO'}, f"No new custom color sets found!")
return {'FINISHED'}
class BONEWIDGET_OT_import_color_presets(bpy.types.Operator):
"""Import User Defined Color Presets"""
bl_idname = "bonewidget.import_color_presets"
bl_label = "Import Color Presets"
bl_options = {'REGISTER'}
filter_glob: StringProperty(
default='*.zip',
options={'HIDDEN'}
)
filename: StringProperty(
name='Filename',
subtype='FILE_NAME',
description='Name of file to be imported',
)
filepath: StringProperty(
subtype="FILE_PATH"
)
import_option: EnumProperty(
name="Import Option",
items=[
("OVERWRITE", "Overwrite", "Overwrite existing preset"),
("SKIP", "Skip", "Skip preset"),
("ASK", "Ask", "Ask user what to do")],
default="ASK",
)
def draw(self, context):
layout = self.layout
row = layout.row(align=True)
row.label(text="If duplicates are found:")
row = layout.row(align=True)
row.prop(self, "import_option", expand=True)
def execute(self, context):
if self.filepath and self.import_option:
import_preset_data = import_color_presets(
self.filepath, self.import_option)
bpy.context.window_manager.prop_grp.import_library_filepath = self.filepath
bpy.types.WindowManager.custom_data = import_preset_data
# if the number of failed presets are equal to total imported presets - call summary popup
if import_preset_data.failed() == import_preset_data.total():
import_preset_data.reset_imports()
bpy.ops.bonewidget.import_summary_popup('INVOKE_DEFAULT')
elif self.import_option == "ASK":
bpy.ops.bonewidget.import_items_ask_popup('INVOKE_DEFAULT')
elif self.import_option in ["OVERWRITE", "SKIP"]:
update_color_presets(
import_preset_data.imported_items, self.filepath)
bpy.ops.bonewidget.import_summary_popup('INVOKE_DEFAULT')
else:
bpy.ops.bonewidget.import_summary_popup('INVOKE_DEFAULT')
return {'FINISHED'}
def invoke(self, context, event):
self.filename = ""
context.window_manager.fileselect_add(self)
return {'RUNNING_MODAL'}
class BONEWIDGET_OT_export_color_presets(bpy.types.Operator):
"""Export User Defined Color Presets"""
bl_idname = "bonewidget.export_color_presets"
bl_label = "Export Color Presets"
bl_options = {'REGISTER'}
filter_glob: StringProperty(
default='*.zip',
options={'HIDDEN'}
)
filename: StringProperty(
name='Filename',
subtype='FILE_NAME',
description='Name of file to be exported',
)
filepath: StringProperty(
subtype="FILE_PATH"
)
def execute(self, context):
if self.filepath and self.filename:
num_presets = export_color_presets(self.filepath, context)
if num_presets:
self.report(
{'INFO'}, f"{num_presets} color presets exported successfully!")
else:
self.report({'INFO'}, "0 color presets exported!")
return {'FINISHED'}
def invoke(self, context, event):
self.filename = "color_presets.zip"
context.window_manager.fileselect_add(self)
return {'RUNNING_MODAL'}
class BONEWIDGET_OT_render_widget_thumbnail(bpy.types.Operator):
"""Render a wireframe thumbnail of the active object"""
bl_idname = "bonewidget.render_widget_thumbnail"
bl_label = "Render Widget Thumbnail"
bl_options = {'REGISTER', 'UNDO'}
image_name: StringProperty(
name="Image Name",
default=""
)
wire_frame_color: FloatVectorProperty(
name="Wireframe Color",
subtype='COLOR',
size=4,
default=(1, 1, 1, 1),
min=0.0,
max=1.0
)
wire_frame_thickness: FloatProperty(
name="Wireframe Thickness",
default=0.5,
min=0.01,
max=2.0
)
use_object_color: BoolProperty(
name="Use Object Color",
default=False
)
use_blend_path: BoolProperty(
name="Save to Current Directory",
default=True
)
@classmethod
def poll(cls, context):
obj = context.active_object
return obj is not None and obj.type == 'MESH'
def invoke(self, context, event):
# Set the image name to the active object
if context.active_object:
self.image_name = context.active_object.name + "_thumbnail"
else:
self.image_name = "widget_thumbnail"
return context.window_manager.invoke_props_dialog(self)
def draw(self, context):
layout = self.layout
row = layout.row()
row.prop(self, "use_object_color", text="Use Object Color")
if not self.use_object_color:
row = layout.row()
split = row.split(factor=0.6)
split.label(text="Wireframe Color:")
split.prop(self, "wire_frame_color", text="")
row = layout.row()
split = row.split(factor=0.6)
split.label(text="Wireframe Thickness:")
split.prop(self, "wire_frame_thickness", text="")
def execute(self, context):
active_obj = context.view_layer.objects.active
if not active_obj:
self.report({'WARNING'}, "No active object found.")
return {'CANCELLED'}
widget_obj = create_wireframe_copy(
active_obj,
self.use_object_color,
self.wire_frame_color,
self.wire_frame_thickness
)
# store the current view perspective
original_view_perspective = context.space_data.region_3d.view_perspective
original_scene = context.scene
new_scene = bpy.data.scenes.new("BoneWidget_Thumbnail")
new_scene.collection.objects.link(widget_obj)
context.window.scene = new_scene
viewport_area = next(
(a for a in context.window.screen.areas if a.type == 'VIEW_3D'), None)
if not viewport_area:
self.report({'WARNING'}, "No 3D Viewport found.")
return {'CANCELLED'}
original_view_matrix = setup_viewport(context)
new_camera = add_camera_from_view(context)
destination_path = render_widget_thumbnail(
self.image_name, widget_obj, image_directory=self.use_blend_path)
restore_viewport_position(
context, original_view_matrix, original_view_perspective)
context.window.scene = original_scene
# Clean up (widget and camera objs and data)
widget_data = widget_obj.data
camera_data = new_camera.data
bpy.data.objects.remove(widget_obj, do_unlink=True)
bpy.data.meshes.remove(widget_data)
bpy.data.objects.remove(new_camera, do_unlink=True)
bpy.data.cameras.remove(camera_data)
# Remove Scene
bpy.data.scenes.remove(new_scene)
if self.use_blend_path:
self.report({'INFO'}, "Thumbnail saved at: " + destination_path)
return {'FINISHED'}
classes = (
BONEWIDGET_OT_remove_widgets,
BONEWIDGET_OT_add_widgets,
BONEWIDGET_OT_import_widget_library,
BONEWIDGET_OT_export_widget_library,
BONEWIDGET_OT_match_symmetrize_shape,
BONEWIDGET_OT_match_bone_transforms,
BONEWIDGET_OT_return_to_armature,
BONEWIDGET_OT_edit_widget,
BONEWIDGET_OT_create_widget,
BONEWIDGET_OT_toggle_collection_visibility,
BONEWIDGET_OT_delete_unused_widgets,
BONEWIDGET_OT_clear_bone_widgets,
BONEWIDGET_OT_resync_widget_names,
BONEWIDGET_OT_add_object_as_widget,
BONEWIDGET_OT_import_items_summary_popup,
BONEWIDGET_OT_import_items_ask_popup,
BONEWIDGET_OT_shared_property_group,
BONEWIDGET_OT_image_select,
BONEWIDGET_OT_add_custom_image,
BONEWIDGET_OT_reset_default_images,
BONEWIDGET_OT_user_data_filebrowser,
BONEWIDGET_OT_set_bone_color,
BONEWIDGET_OT_clear_bone_color,
BONEWIDGET_OT_copy_bone_color,
BONEWIDGET_OT_add_color_set_from,
BONEWIDGET_OT_add_default_colorset,
BONEWIDGET_OT_add_colorset_to_bone,
BONEWIDGET_OT_remove_item,
BONEWIDGET_OT_lock_custom_colorset_changes,
BONEWIDGET_OT_move_custom_item_up,
BONEWIDGET_OT_move_custom_item_down,
BONEWIDGET_OT_add_preset_from_bone,
BONEWIDGET_OT_add_presets_from_armature,
BONEWIDGET_OT_import_color_presets,
BONEWIDGET_OT_export_color_presets,
BONEWIDGET_OT_render_widget_thumbnail,
)
def register():
bpy.utils.register_class(ImportColorSet)
bpy.utils.register_class(ImportItemData)
from bpy.utils import register_class
for cls in classes:
register_class(cls)
bpy.types.WindowManager.prop_grp = bpy.props.PointerProperty(
type=BONEWIDGET_OT_shared_property_group)
def unregister():
del bpy.types.WindowManager.prop_grp
from bpy.utils import unregister_class
for cls in classes:
unregister_class(cls)
unregister_class(ImportColorSet)
unregister_class(ImportItemData)