import bpy from bpy.types import Operator from bpy.props import BoolProperty, IntProperty, FloatProperty, FloatVectorProperty, \ StringProperty from bone_widget import ctx from .transform_utils import transform_matrix, apply_mat_to_verts, get_bone_size_factor, \ get_bone_matrix from .icon_utils import render_widget from .shape_utils import get_clean_shape, symmetrize_bone_shape, link_to_col, get_bone, custom_shape_matrix import re from pathlib import Path import os from tempfile import gettempdir from mathutils import Matrix import json class BW_OT_copy_widgets(Operator): bl_idname = 'bonewidget.copy_widgets' bl_label = "Copy Widgets" @classmethod def poll(cls, context): return context.object and context.object.type == 'ARMATURE' def execute(self, context): from tempfile import gettempdir blend_file = Path(gettempdir())/'bone_widgets.blend' shapes = [] ob = context.object if ob.mode == 'POSE': bones = ctx.selected_bones else: bones = ctx.bones for b in bones: if b.custom_shape: s = b.custom_shape.copy() s.data = s.data.copy() if not b.use_custom_shape_bone_size: mat = transform_matrix(scale=(1/b.bone.length,)*3) s.data.transform(mat) #s.data.transform(s.matrix_world.inverted()) #s.matrix_world = Matrix() s['.bw_bone'] = b.name if bpy.app.version_string < '3.0.0': s['.bw_custom_shape_scale'] = b.custom_shape_scale else: s['.bw_custom_shape_translation'] = b.custom_shape_translation s['.bw_custom_shape_rotation_euler'] = b.custom_shape_rotation_euler s['.bw_custom_shape_scale_xyz'] = b.custom_shape_scale_xyz s['.bw_custom_shape_transform'] = b.custom_shape_transform s['.bw_use_custom_shape_bone_size'] = b.use_custom_shape_bone_size #s['.bw_size_factor'] = get_bone_size_factor(b, s, b.use_custom_shape_bone_size) shapes.append(s) bpy.data.libraries.write(str(blend_file), set(shapes)) for s in shapes: data = s.data data_type = s.type bpy.data.objects.remove(s) if data_type == 'MESH': bpy.data.meshes.remove(data) elif data_type == 'CURVE': bpy.data.curves.remove(data) # for b in ctx.bones: # if b.custom_shape: # for k in b.custom_shape.keys(): # if k.startswith('.bw'): # del b.custom_shape[k] return {'FINISHED'} class BW_OT_paste_widgets(Operator): bl_idname = 'bonewidget.paste_widgets' bl_label = "Paste Widgets" path: StringProperty(subtype='FILE_PATH') @classmethod def poll(cls, context): return context.object and context.object.type == 'ARMATURE' def execute(self, context): from tempfile import gettempdir rig = context.object #for b in rig.pose.bones: # if b.custom_shape: # bpy.data.objects.remove(b.custom_shape) #b.custom_shape = get_clean_shape(b, b.custom_shape, separate=b.custom_shape.users>2) if self.path: blend_file = Path(self.path) else: blend_file = Path(gettempdir())/'bone_widgets.blend' with bpy.data.libraries.load(str(blend_file), link=False) as (data_src, data_dst): data_dst.objects = data_src.objects shapes = data_src.objects #old_shapes = set() for s in shapes: bone = rig.pose.bones.get(s['.bw_bone']) if not bone: print('No bone found for', s) continue if bpy.app.version_string < '3.0.0': bone.custom_shape_scale = s['.bw_custom_shape_scale'] else: bone.custom_shape_scale_xyz = s['.bw_custom_shape_scale_xyz'] link_to_col(s, ctx.widget_col) #if bone.custom_shape: #size_factor = max(s.dimensions)#get_bone_size_factor(bone, s, relative=False) #size_factor /= s['.bw_size_factor'] #size_factor = s['.bw_size_factor'] #mat = transform_matrix(scale=(size_factor,)*3) #s.data.transform(Matrix.Scale(1/size_factor, 4)) #s.scale = 1,1,1 bone.custom_shape = get_clean_shape(bone, s, col=ctx.widget_col, prefix=ctx.prefs.prefix, separate=False, apply_transforms=False) if bpy.app.version_string < '3.0.0': #bone.custom_shape_scale = s['.bw_custom_shape_scale'] bone.custom_shape_scale = 1 else: bone.custom_shape_translation = s['.bw_custom_shape_translation'] bone.custom_shape_rotation_euler = s['.bw_custom_shape_rotation_euler'] #bone.custom_shape_scale_xyz = s['.bw_custom_shape_scale_xyz'] bone.custom_shape_scale_xyz = 1,1,1 #use_custom_shape_bone_size bone.custom_shape_transform = s['.bw_custom_shape_transform'] bone.use_custom_shape_bone_size = True#s['.bw_use_custom_shape_bone_size'] bone.bone.show_wire = not bool(s.data.polygons) #mat = transform_matrix(scale=(s['.bw_size_factor'],)*3) #s.data.transform(mat) for b in rig.pose.bones: if b.custom_shape: for k in list(b.custom_shape.keys()): if k.startswith('.bw'): del b.custom_shape[k] return {'FINISHED'} class BW_OT_remove_unused_shape(Operator): bl_idname = 'bonewidget.remove_unused_shape' bl_label = "Remove Unused Shape" def execute(self, context): objects = list(ctx.widget_col.all_objects) for ob in objects: if not get_bone(ob) and ob in bpy.data.objects[:]: bpy.data.objects.remove(ob) return {'FINISHED'} class BW_OT_auto_color(Operator): bl_idname = 'bonewidget.auto_color' bl_label = "Auto Color" bl_options = {'REGISTER', 'UNDO'} def is_bone_protected(self, bone): rig = bone.id_data return any([i==j==True for i, j in zip(rig.data.layers_protected, bone.bone.layers)]) def execute(self, context): ob = context.object bones = context.selected_pose_bones or ob.pose.bones for b in bones: if self.is_bone_protected(b): continue for group in ob.pose.bone_groups: if any(i in b.name.lower() for i in group.name.lower().split(' ')): b.bone_group = group return {'FINISHED'} class BW_OT_load_default_color(Operator): bl_idname = 'bonewidget.load_default_color' bl_label = "Load Default Color" bl_options = {'REGISTER', 'UNDO'} select_color: FloatVectorProperty(name='Color', subtype='COLOR', default=[0.0, 1.0, 1.0]) active_color: FloatVectorProperty(name='Color', subtype='COLOR', default=[1.0, 1.0, 1.0]) def execute(self, context): ob = context.object colors = { 'Black': [0, 0, 0], 'Yellow': [1, 1, 0], 'Red': [0, 0.035, 0.95], 'Blue': [1, 0, 0], 'Green': [0.733333, 0.937255, 0.356863], 'Pink': [1, 0.1, 0.85], 'Purple': [0.67, 0, 0.87], 'Brown': [0.75, 0.65, 0], 'Orange': [1, 0.6, 0], } for k, v in colors.items(): bone_group = ob.pose.bone_groups.get(k) if not bone_group: bone_group = ob.pose.bone_groups.new(name=k) bone_group.color_set = 'CUSTOM' bone_group.colors.normal = v bone_group.colors.select = self.select_color bone_group.colors.active = self.active_color return {'FINISHED'} class BW_OT_copy_bone_groups(Operator): bl_idname = 'bonewidget.copy_bone_groups' bl_label = "Copy Bone Groups" bl_options = {'REGISTER', 'UNDO'} @classmethod def poll(self, context): return context.object and context.object.type == 'ARMATURE' def execute(self, context): print('Copy Bone Group') ob = context.object bone_groups = {} for bg in ob.pose.bone_groups: bone_groups[bg.name] = { 'colors': [bg.colors.normal[:], bg.colors.select[:], bg.colors.active[:]], 'bones': [b.name for b in ob.pose.bones if b.bone_group == bg] } blend_file = Path(gettempdir()) / 'bw_copy_bone_groups.json' blend_file.write_text(json.dumps(bone_groups)) return {'FINISHED'} class BW_OT_paste_bone_groups(Operator): bl_idname = 'bonewidget.paste_bone_groups' bl_label = "Paste Bone Groups" bl_options = {'REGISTER', 'UNDO'} clear: BoolProperty(default=False) @classmethod def poll(self, context): return context.object and context.object.type == 'ARMATURE' def execute(self, context): print('Paste Bone Group') ob = context.object blend_file = Path(gettempdir()) / 'bw_copy_bone_groups.json' bone_groups = json.loads(blend_file.read_text()) if self.clear: for bg in reversed(ob.pose.bone_groups): ob.pose.bone_groups.remove(bg) for bg_name, bg_data in bone_groups.items(): bg = ob.pose.bone_groups.get(bg_name) if not bg: bg = ob.pose.bone_groups.new(name=bg_name) bg.color_set = 'CUSTOM' bg.colors.normal,bg.colors.select, bg.colors.active = bg_data['colors'] for b in bg_data['bones']: bone = ob.pose.bones.get(b) if bone: bone.bone_group = bg return {'FINISHED'} class BW_OT_add_folder(Operator): bl_idname = 'bonewidget.add_folder' bl_label = "Add Folder" def execute(self, context): folder = ctx.prefs.folders.add() folder.path = '' return {'FINISHED'} class BW_OT_remove_folder(Operator): bl_idname = 'bonewidget.remove_folder' bl_label = "Remove Folder" bl_options = {'REGISTER', 'UNDO'} index: IntProperty() def execute(self, context): ctx.prefs.folders.remove(self.index) if self.index == 0: ctx.prefs.folder_enum = str(ctx.default_folder_path) #update_folder_items() bpy.context.area.tag_redraw() return {'FINISHED'} class BW_OT_refresh_folders(Operator): bl_idname = 'bonewidget.refresh_folders' bl_label = "Refresh" def execute(self, context): for f in ctx.folders: f.widgets.clear() f.load_widgets() return {'FINISHED'} class BW_OT_rename_folder(Operator): bl_idname = 'bonewidget.rename_folder' bl_label = "Rename Folder" bl_options = {'REGISTER', 'UNDO'} name: StringProperty(name='Name') @classmethod def poll(cls, context): return ctx.active_folder def execute(self, context): folder = ctx.active_folder folder.rename(self.name) return {'FINISHED'} def invoke(self, context, event): wm = context.window_manager self.name = Path(ctx.prefs.folder_enum).name return wm.invoke_props_dialog(self) # class BW_OT_show_preferences(Operator): # """Display the preferences to the tab panel""" # bl_idname = 'bonewidget.show_preferences' # bl_label = "Show Preferences" # bl_options = {'REGISTER'} # def execute(self, context): # #ctx.init_transforms() # return {'FINISHED'} class BW_OT_transform_widget(Operator): """Transform the Rotation Location or Scale of the selected shapes""" bl_idname = 'bonewidget.transform_widget' bl_label = "Transfom" bl_options = {'REGISTER', 'UNDO'} symmetrize: BoolProperty(name='Symmetrize', default=True) @classmethod def poll(cls, context): return context.object and context.object.mode == 'POSE' def execute(self, context): ctx.init_transforms() return {'FINISHED'} class BW_OT_create_widget(Operator): """Create the widget shape and assign it to the bone""" bl_idname = 'bonewidget.create_widget' bl_label = "Create Widget" bl_options = {'REGISTER', 'UNDO'} symmetrize: BoolProperty(name='Symmetrize', default=True) @classmethod def poll(cls, context): return ctx.selected_bones and ctx.active_widget def execute(self, context): folder = ctx.active_folder blend = folder.get_widget_path(folder.active_widget) with bpy.data.libraries.load(str(blend), link=False) as (data_src, data_dst): data_dst.objects = data_src.objects shape = data_src.objects[0] for bone in ctx.selected_bones: #shape_copy = shape.copy() if bpy.app.version_string < '3.0.0': bone.custom_shape_scale = 1.0 else: bone.custom_shape_scale_xyz = [1.0]*3 # for blender 3.0 bone.bone.show_wire = not bool(shape.data.polygons) #if bone.custom_shape and bone.custom_shape.users == 2: # bpy.data.objects.remove(bone.custom_shape) #copy_shape = shape.copy() #copy_shape.data = copy_shape.data.copy() bone.custom_shape = get_clean_shape(bone, shape, col=ctx.widget_col, prefix=ctx.prefs.prefix) if self.symmetrize: symmetrize_bone_shape(bone, prefix=ctx.prefs.prefix) bpy.data.objects.remove(shape) ctx.init_transforms() return {'FINISHED'} class BW_OT_match_transform(Operator): bl_idname = 'bonewidget.match_transform' bl_label = "Match Transforms" bl_options = {'REGISTER', 'UNDO'} relative: BoolProperty(default=False) def execute(self, context): for bone in ctx.selected_bones: shape = bone.custom_shape if not shape: continue size_factor = get_bone_size_factor(bone, shape, self.relative) mat = transform_matrix(scale=(size_factor,)*3) shape.data.transform(mat) #apply_mat_to_verts(shape.data, mat) ctx.init_transforms() return {'FINISHED'} class BW_OT_edit_widget(Operator): bl_idname = 'bonewidget.edit_widget' bl_label = "Edit Widget" bl_options = {'REGISTER', 'UNDO'} clean: BoolProperty(name='Clean', default=True) @classmethod def poll(cls, context): return ctx.bone and ctx.bone.custom_shape def execute(self, context): #ctx._rig = ctx.rig widgets = ctx.selected_widgets bones = ctx.selected_bones rig = ctx.rig prefs = ctx.prefs if self.clean: bpy.ops.bonewidget.clean_widget() bpy.ops.object.mode_set(mode='OBJECT') bpy.ops.object.select_all(action='DESELECT') rig.hide_set(True) ctx.widget_col.hide_viewport = False ctx.widget_layer_col.exclude = False ctx.widget_layer_col.hide_viewport = False for bone in bones: if not bone.custom_shape: continue #link_to_col(w, ctx.widget_col) bone.custom_shape.select_set(True) context.view_layer.objects.active = bone.custom_shape bpy.ops.object.mode_set(mode='EDIT') return {'FINISHED'} class BW_OT_return_to_rig(Operator): bl_idname = 'bonewidget.return_to_rig' bl_label = "Return to rig" bl_options = {'REGISTER', 'UNDO'} clean: BoolProperty(name='Clean', default=True) def invoke(self, context, event): self.symmetrize = ctx.prefs.auto_symmetrize return self.execute(context) def execute(self, context): rig = ctx.rig #print('Rig', rig) bone = ctx.bone bones = ctx.selected_bones ctx.widget_col.hide_viewport = False bpy.ops.object.mode_set(mode='OBJECT') bpy.ops.object.select_all(action='DESELECT') rig.hide_set(False) ctx.widget_col.hide_viewport = True context.view_layer.objects.active = rig bpy.ops.object.mode_set(mode='POSE') rig.data.bones.active = bone.bone for b in rig.pose.bones: b.bone.select = b in bones+[bone] if self.clean: bpy.ops.bonewidget.clean_widget() return {'FINISHED'} ''' class BW_OT_symmetrize_widget(Operator): bl_idname = 'bonewidget.symmetrize_widget' bl_label = "Symmetrize" bl_options = {'REGISTER', 'UNDO'} match_transform: BoolProperty(True) def get_name_side(self, name, fallback=None): if name.lower().endswith('.l'): return 'LEFT' elif name.lower().endswith('.r'): return 'RIGHT' return fallback def mirror_name(self, name): mirror = None match = { 'R': 'L', 'r': 'l', 'L': 'R', 'l': 'r', } separator = ['.', '_'] if name.startswith(tuple(match.keys())): if name[1] in separator: mirror = match[name[0]] + name[1:] if name.endswith(tuple(match.keys())): if name[-2] in separator: mirror = name[:-1] + match[name[-1]] return mirror def execute(self, context): bones = ctx.selected_bones if ctx.bone: active_side = self.get_name_side(ctx.bone.name, 'LEFT') if active_side: bones = [b for b in ctx.selected_bones if self.get_name_side(b) == active_side] for bone in bones: flip_name = self.mirror_name(bone.name) flip_bone = ctx.rig.pose.bones.get(flip_name) if flip_bone: shape = flip_bone.custom_shape if shape.users <= 2: bpy.data.objects.remove(shape) shape = bone.custom_shape.copy() if shape flip_bone.custom_shape = shape shape.matrix_world = get_bone_matrix(bone) shape.data.transform(transform_matrix(scale=(-1, 1, 1))) ctx.rename_shape(shape, flip_bone) return {'FINISHED'} ''' class BW_OT_add_widget(Operator): bl_idname = 'bonewidget.add_widget' bl_label = "Add Widget" bl_options = {'REGISTER', 'UNDO'} replace: BoolProperty(default=False, name='Replace') @classmethod def poll(cls, context): return ctx.widget def invoke(self, context, event): if event.ctrl: self.replace = True return self.execute(context) def execute(self, context): folder = ctx.active_folder shape = ctx.widget bone = ctx.bone if self.replace and ctx.active_widget: name = ctx.active_widget.name else: #Find a unique name name = folder.get_widget_display_name(bone.name if bone else shape.name) i = 0 org_name = name while name in folder.widgets: name = f'{org_name} {i:02d}' i += 1 widget_path = folder.get_widget_path(name) icon_path = folder.get_icon_path(name) widget_path.parent.mkdir(exist_ok=True, parents=True) bpy.data.libraries.write(str(widget_path), {shape}) # Copy the shape to apply the bone matrix to the mesh shape_copy = shape.copy() shape_copy.data = shape_copy.data.copy() if bone: shape_copy.matrix_world = get_bone_matrix(bone) mat = custom_shape_matrix(bone) shape_copy.data.transform(mat) render_widget(shape_copy, icon_path) folder.add_widget(name) bpy.data.objects.remove(shape_copy) bpy.context.area.tag_redraw() return {'FINISHED'} class BW_OT_remove_widget(Operator): bl_idname = 'bonewidget.remove_widget' bl_label = "Remove Widget" bl_options = {'REGISTER', 'UNDO'} @classmethod def poll(cls, context): return ctx.active_widget def execute(self, context): folder = ctx.active_folder widget = ctx.active_widget folder.remove_widget(widget) return {'FINISHED'} class BW_OT_clean_widget(Operator): bl_idname = 'bonewidget.clean_widget' bl_label = "Clean" bl_options = {'REGISTER', 'UNDO'} all: BoolProperty(name='All', default=False) symmetrize: BoolProperty(name='Symmetrize', default=True) @classmethod def poll(cls, context): return context.object and context.object.mode == 'POSE' def invoke(self, context, event): self.symmetrize = ctx.prefs.auto_symmetrize return self.execute(context) def execute(self, context): scene = context.scene prefs = ctx.prefs if self.all: bones = ctx.bones else: bones = ctx.selected_bones for bone in bones: shape = bone.custom_shape if not shape: continue bone.custom_shape = get_clean_shape(bone, shape, separate=prefs.auto_separate, rename=prefs.auto_rename, col=ctx.widget_col, prefix=prefs.prefix, match=prefs.auto_match_transform) if self.symmetrize: symmetrize_bone_shape(bone, prefix=ctx.prefs.prefix) if shape in bpy.data.objects[:] and shape.users <= 1: bpy.data.objects.remove(shape) return {'FINISHED'} classes = ( BW_OT_remove_unused_shape, BW_OT_auto_color, BW_OT_load_default_color, BW_OT_add_folder, BW_OT_remove_folder, BW_OT_refresh_folders, BW_OT_rename_folder, BW_OT_transform_widget, BW_OT_match_transform, BW_OT_create_widget, BW_OT_edit_widget, BW_OT_return_to_rig, #BW_OT_symmetrize_widget, BW_OT_add_widget, BW_OT_remove_widget, BW_OT_clean_widget, # BW_OT_show_preferences, BW_OT_copy_widgets, BW_OT_paste_widgets, BW_OT_copy_bone_groups, BW_OT_paste_bone_groups ) def bw_bone_group_menu(self, context): layout = self.layout layout.operator("bonewidget.load_default_color", icon='IMPORT') layout.operator("bonewidget.auto_color", icon='COLOR') layout.operator("bonewidget.copy_bone_groups", icon='COPYDOWN') layout.operator("bonewidget.paste_bone_groups", icon='PASTEDOWN') def register(): for cls in classes: bpy.utils.register_class(cls) bpy.types.DATA_MT_bone_group_context_menu.prepend(bw_bone_group_menu) def unregister(): bpy.types.DATA_MT_bone_group_context_menu.remove(bw_bone_group_menu) for cls in reversed(classes): bpy.utils.unregister_class(cls)