First Commit

master
“christopheseux” 2022-10-28 23:23:06 +02:00
parent 804bec1364
commit 9d339585f5
56 changed files with 2028 additions and 91 deletions

2
.gitignore vendored Normal file
View File

@ -0,0 +1,2 @@
__pycache__
*.py[cod]

View File

@ -1,92 +1,2 @@
# bone_widget
vvezvzev
## Getting started
To make it easy for you to get started with GitLab, here's a list of recommended next steps.
Already a pro? Just edit this README.md and make it your own. Want to make it easy? [Use the template at the bottom](#editing-this-readme)!
## Add your files
- [ ] [Create](https://docs.gitlab.com/ee/user/project/repository/web_editor.html#create-a-file) or [upload](https://docs.gitlab.com/ee/user/project/repository/web_editor.html#upload-a-file) files
- [ ] [Add files using the command line](https://docs.gitlab.com/ee/gitlab-basics/add-file.html#add-a-file-using-the-command-line) or push an existing Git repository with the following command:
```
cd existing_repo
git remote add origin https://gitlab.com/autour-de-minuit/blender/bone_widget.git
git branch -M main
git push -uf origin main
```
## Integrate with your tools
- [ ] [Set up project integrations](https://gitlab.com/autour-de-minuit/blender/bone_widget/-/settings/integrations)
## Collaborate with your team
- [ ] [Invite team members and collaborators](https://docs.gitlab.com/ee/user/project/members/)
- [ ] [Create a new merge request](https://docs.gitlab.com/ee/user/project/merge_requests/creating_merge_requests.html)
- [ ] [Automatically close issues from merge requests](https://docs.gitlab.com/ee/user/project/issues/managing_issues.html#closing-issues-automatically)
- [ ] [Enable merge request approvals](https://docs.gitlab.com/ee/user/project/merge_requests/approvals/)
- [ ] [Automatically merge when pipeline succeeds](https://docs.gitlab.com/ee/user/project/merge_requests/merge_when_pipeline_succeeds.html)
## Test and Deploy
Use the built-in continuous integration in GitLab.
- [ ] [Get started with GitLab CI/CD](https://docs.gitlab.com/ee/ci/quick_start/index.html)
- [ ] [Analyze your code for known vulnerabilities with Static Application Security Testing(SAST)](https://docs.gitlab.com/ee/user/application_security/sast/)
- [ ] [Deploy to Kubernetes, Amazon EC2, or Amazon ECS using Auto Deploy](https://docs.gitlab.com/ee/topics/autodevops/requirements.html)
- [ ] [Use pull-based deployments for improved Kubernetes management](https://docs.gitlab.com/ee/user/clusters/agent/)
- [ ] [Set up protected environments](https://docs.gitlab.com/ee/ci/environments/protected_environments.html)
***
# Editing this README
When you're ready to make this README your own, just edit this file and use the handy template below (or feel free to structure it however you want - this is just a starting point!). Thank you to [makeareadme.com](https://www.makeareadme.com/) for this template.
## Suggestions for a good README
Every project is different, so consider which of these sections apply to yours. The sections used in the template are suggestions for most open source projects. Also keep in mind that while a README can be too long and detailed, too long is better than too short. If you think your README is too long, consider utilizing another form of documentation rather than cutting out information.
## Name
Choose a self-explaining name for your project.
## Description
Let people know what your project can do specifically. Provide context and add a link to any reference visitors might be unfamiliar with. A list of Features or a Background subsection can also be added here. If there are alternatives to your project, this is a good place to list differentiating factors.
## Badges
On some READMEs, you may see small images that convey metadata, such as whether or not all the tests are passing for the project. You can use Shields to add some to your README. Many services also have instructions for adding a badge.
## Visuals
Depending on what you are making, it can be a good idea to include screenshots or even a video (you'll frequently see GIFs rather than actual videos). Tools like ttygif can help, but check out Asciinema for a more sophisticated method.
## Installation
Within a particular ecosystem, there may be a common way of installing things, such as using Yarn, NuGet, or Homebrew. However, consider the possibility that whoever is reading your README is a novice and would like more guidance. Listing specific steps helps remove ambiguity and gets people to using your project as quickly as possible. If it only runs in a specific context like a particular programming language version or operating system or has dependencies that have to be installed manually, also add a Requirements subsection.
## Usage
Use examples liberally, and show the expected output if you can. It's helpful to have inline the smallest example of usage that you can demonstrate, while providing links to more sophisticated examples if they are too long to reasonably include in the README.
## Support
Tell people where they can go to for help. It can be any combination of an issue tracker, a chat room, an email address, etc.
## Roadmap
If you have ideas for releases in the future, it is a good idea to list them in the README.
## Contributing
State if you are open to contributions and what your requirements are for accepting them.
For people who want to make changes to your project, it's helpful to have some documentation on how to get started. Perhaps there is a script that they should run or some environment variables that they need to set. Make these steps explicit. These instructions could also be useful to your future self.
You can also document commands to lint the code or run tests. These steps help to ensure high code quality and reduce the likelihood that the changes inadvertently break something. Having instructions for running tests is especially helpful if it requires external setup, such as starting a Selenium server for testing in a browser.
## Authors and acknowledgment
Show your appreciation to those who have contributed to the project.
## License
For open source projects, say how it is licensed.
## Project status
If you have run out of energy or time for your project, put a note at the top of the README saying that development has slowed down or stopped completely. Someone may choose to fork your project or volunteer to step in as a maintainer or owner, allowing your project to keep going. You can also make an explicit request for maintainers.
explanation : https://vimeo.com/184159913

55
__init__.py Normal file
View File

@ -0,0 +1,55 @@
bl_info = {
"name": "Bone Widget",
"author": "Christophe SEUX",
"version": (2, 0, 0),
"blender": (2, 92, 0),
"description": "Create custom shapes for bone controller",
"warning": "",
"wiki_url": "",
"category": "Rigging",
}
import sys
if "bpy" in locals():
import importlib as imp
imp.reload(context)
imp.reload(properties)
imp.reload(operators)
imp.reload(ui)
else:
from . import context
from . import operators
from . import ui
from . import properties
import bpy
#sys.modules.update({"bone_widget.ctx": context.BW_ctx()})
from bone_widget import ctx
def register():
properties.register()
operators.register()
ui.register()
#bpy.types.Scene.bone_widget = bpy.props.PointerProperty(type=BoneWidgetSettings)
#get_widgets(DefaultFolder, DefaultShapes)
#get_widgets(CustomFolder, CustomShapes)
for f in ctx.folders:
f.load_widgets()
def unregister():
#print('UnRegister BoneWidget')
properties.unregister()
operators.unregister()
ui.unregister()
del sys.modules['bone_widget.ctx']
#remove_icons(bpy.types.Scene.bone_widget)

283
context.py Normal file
View File

@ -0,0 +1,283 @@
import sys
import bpy
from pathlib import Path
import os
from bone_widget.shape_utils import get_bone
class BW_ctx:
def __init__(self):
self.module_dir = Path(__file__).parent
self.module_name = self.module_dir.name
self.id_name = 'bonewidget'
self._rig = None
self._folder_items = []
self.shapes = {} # used to store vert co for the transform properties
os.chdir(self.module_dir/'widgets')
@property
def addon(self):
from bpy import context
return context.preferences.addons[__package__]
@property
def prefs(self):
return self.addon.preferences
#def settings(self):
# return bpy.context.window_manager.
@property
def show_transforms(self):
ops = bpy.context.window_manager.operators
id_names = [self.get_op_id(o) for o in ('transform_widget','create_widget', 'match_transform')]
return (ops and ops[-1].bl_idname in id_names)
@property
def clean_widget_op(self):
ops = bpy.context.window_manager.operators
op_idname = self.get_op_id('clean_widget')
#transforms = self.prefs.transforms
if ops and ops[-1].bl_idname == op_idname:
return ops[-1]
@property
def show_prefs_op(self):
ops = bpy.context.window_manager.operators
op_idname = self.get_op_id('show_preferences')
#transforms = self.prefs.transforms
if ops and ops[-1].bl_idname == op_idname:
return ops[-1]
@property
def category(self):
if not self.prefs:
return 'Rig'
return self.prefs.category
@property
def rig(self):
bone = self.bone
if bone:
return bone.id_data
@property
def widget_col(self):
col_name = self.prefs.collection
col = bpy.data.collections.get(col_name)
if not col:
col = bpy.data.collections.new(col_name)
col.hide_viewport = True
if col not in self.get_scene_cols():
bpy.context.scene.collection.children.link(col)
return col
@property
def widget_layer_col(self):
return next((c for c in self.get_layer_cols() if c.collection == self.widget_col), None)
@property
def default_folder_path(self):
return self.prefs.default_folder.abspath
@property
def custom_folder_paths(self):
return [f.abspath for f in self.prefs.folders if f.path]
@property
def folder_paths(self):
return [self.default_folder_path] + self.custom_folder_paths
@property
def folders(self):
return list(self.prefs.folders) + [self.prefs.default_folder]
@property
def active_folder(self):
folder = self.abspath(self.prefs.folder_enum)
if folder == self.default_folder_path:
return self.prefs.default_folder
else:
return next((f for f in self.prefs.folders if f.abspath==folder), None)
@property
def active_widget(self):
folder = self.active_folder
if not folder:
return
index = folder.widget_index
return folder.active_widget
@property
def bone(self):
if not self.poll():
return
ob = bpy.context.object
if ob.type == 'ARMATURE':
return bpy.context.active_pose_bone
else:
return get_bone(ob)
@property
def bones(self):
ob = bpy.context.object
if ob.type == 'ARMATURE':
return list(ob.pose.bones)
else:
widgets = self.widgets
return [b for b in self.rig.pose.bones if b.custom_shape in widgets]
@property
def selected_bones(self):
if not self.poll():
return []
ob = bpy.context.object
if ob.type == 'ARMATURE':
bones = ob.pose.bones
return [b for b in bones if b.bone.select]
else:
widgets = self.selected_widgets
return [b for b in self.rig.pose.bones if b.custom_shape in widgets]
@property
def widget(self):
if not self.poll():
return
ob = bpy.context.object
if ob.type == 'ARMATURE':
if bpy.context.active_pose_bone:
return bpy.context.active_pose_bone.custom_shape
else:
return ob
@property
def widgets(self):
ob = bpy.context.object
if ob.type == 'ARMATURE':
return [b.custom_shape for b in self.bones if b.custom_shape]
@property
def selected_widgets(self):
if not self.poll():
return []
ob = bpy.context.object
if ob.type == 'ARMATURE':
bones = self.selected_bones
return [b.custom_shape for b in bones if b.custom_shape]
else:
objects = bpy.context.selected_objects
return [o for o in objects if o.select_get() and o.type in ('MESH', 'CURVE')]
@property
def folder_items(self):
for folder in self.folder_paths:
if not any(self.abspath(f[0])==folder for f in self._folder_items):
id = 0
if self._folder_items:
id = max([i[-1] for i in self._folder_items])+1
self._folder_items.append((str(folder), folder.name, '', '', id))
for f in reversed(self._folder_items):
if self.abspath(f[0]) not in self.folder_paths:
self._folder_items.remove(f)
return self._folder_items
def get_flipped_bone(self, bone):
pass
#def get_bone(self, shape):
# armatures = [o for o in bpy.context.scene.objects if o.type == 'ARMATURE']
# return next((b for a in armatures for b in a.pose.bones if b.custom_shape == shape), None)
def get_scene_cols(self, col=None, children=None):
if children is None:
children = set()
if col is None:
col = bpy.context.scene.collection
for c in col.children:
children.add(c)
self.get_scene_cols(c, children)
return list(children)
def get_layer_cols(self, col=None, children=None):
if children is None:
children = set()
if col is None:
col = bpy.context.view_layer.layer_collection
for c in col.children:
children.add(c)
self.get_layer_cols(c, children)
return list(children)
def abspath(self, path):
if not path:
return
if path.startswith('//'):
path = './' + path[2:]
return Path(path).absolute().resolve()
def poll(self):
ob = bpy.context.object
if not ob:
return False
if ob.type == 'ARMATURE' and ob.mode == 'POSE':
return True
if ob.type in ('MESH','CURVE'):
return True
return False
def store_shapes(self):
self.shapes.clear()
for widget in self.selected_widgets:
coords = [0]*len(widget.data.vertices)*3
widget.data.vertices.foreach_get('co', coords)
self.shapes[widget] = coords
def reset_transforms(self):
transforms = self.prefs.transforms
transforms['size'] = 1
transforms['xz_scale'] = 1
transforms['slide'] = 0
transforms['loc'] = (0, 0, 0)
transforms['rot'] = (0, 0, 0)
transforms['scale'] = (1, 1, 1)
def init_transforms(self):
self.store_shapes()
self.reset_transforms()
def get_op_id(self, name):
return f'{self.id_name.upper()}_OT_{name}'
#def call(self, op):
# ops_data = getattr(bpy.ops, ctx.id_name)
# getattr(ops_data, op)()
sys.modules.update({"bone_widget.ctx": BW_ctx()})

110
icon_utils.py Normal file
View File

@ -0,0 +1,110 @@
import bpy
import bmesh
import numpy as np
import bgl
from mathutils import Vector, Matrix
import gpu
from gpu_extras.batch import batch_for_shader
def bounds_2d_co(coords, width, height):
from bpy_extras.view3d_utils import location_3d_to_region_2d as space_2d
region = bpy.context.region
rv3d = bpy.context.space_data.region_3d
co_2d = [space_2d(region, rv3d, co) for co in coords]
x_value = sorted(co_2d, key=lambda x:x[0])
y_value = sorted(co_2d, key=lambda x:x[1])
min_x = x_value[0][0]
min_y = y_value[0][1]
box_width = x_value[-1][0] - min_x
box_height = y_value[-1][1] - min_y
x_margin = 1 / width # one pixel margin
y_margin = 1 / height
scale_fac = 2 / max(box_width, box_height)
if box_width < box_height :
center_offset = (box_height - box_width) /2
min_x -= center_offset
else :
center_offset = (box_width - box_height) /2
min_y -= center_offset
co_2d_bounds = []
for co in co_2d:
x = (co[0] - min_x) *scale_fac * (1 - 2*x_margin) -1 + x_margin#+ x_offset
y = (co[1] - min_y) *scale_fac * (1 - 2*y_margin) -1 + y_margin#-height/2 #* (1-y_offset) #+ y_offset
co_2d_bounds.append(Vector((x, y)))
return co_2d_bounds
def render_widget(shape, filepath, width=32, height=32) :
dg = bpy.context.evaluated_depsgraph_get()
bm = bmesh.new()
bm.from_object(shape, dg)
coords = [shape.matrix_world @ v.co for v in bm.verts]
coords = bounds_2d_co(coords, width, height)
indices = [(e.verts[0].index, e.verts[1].index) for e in bm.edges]
bm.free()
offscreen = gpu.types.GPUOffScreen(width, height)
shader = gpu.shader.from_builtin('2D_UNIFORM_COLOR')
batch = batch_for_shader(shader, 'LINES', {"pos": coords}, indices=indices)
with offscreen.bind():
bgl.glClearColor(0.0, 0.0, 0.0, 0.0)
bgl.glClear(bgl.GL_COLOR_BUFFER_BIT)
with gpu.matrix.push_pop():
# reset matrices -> use normalized device coordinates [-1, 1]
gpu.matrix.load_matrix(Matrix.Identity(4))
gpu.matrix.load_projection_matrix(Matrix.Identity(4))
bgl.glLineWidth(4)
bgl.glEnable( bgl.GL_LINE_SMOOTH )
bgl.glEnable(bgl.GL_BLEND)
shader.bind()
shader.uniform_float("color", (0, 0, 0, 0.1))
batch.draw(shader)
bgl.glLineWidth(2)
shader.uniform_float("color", (0.85, 0.85, 0.85, 1))
batch.draw(shader)
buffer = bgl.Buffer(bgl.GL_BYTE, width * height * 4)
bgl.glReadBuffer(bgl.GL_BACK)
bgl.glReadPixels(0, 0, width, height, bgl.GL_RGBA, bgl.GL_UNSIGNED_BYTE, buffer)
offscreen.free()
#icon_name = '.' + shape.name + '_icon.png'
icon_name = shape.name + '_icon.png'
image = bpy.data.images.get(icon_name)
if image:
bpy.data.images.remove(image)
image = bpy.data.images.new(icon_name, width, height)
#image_data = np.asarray(buffer, dtype=np.uint8)
#image.pixels.foreach_set(image_data.flatten()/255)
#image_data = np.asarray(buffer, dtype=np.uint8)
image.pixels = [v / 255 for v in buffer]
image.save_render(str(filepath))

761
operators.py Normal file
View File

@ -0,0 +1,761 @@
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
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 = {
'root': [0, 0, 0],
'spine chest hips': [1, 1, 0],
'.R': [0, 0.035, 0.95],
'.L': [1, 0, 0],
'ik.R': [1, 0.1, 0.85],
'ik.L': [0.67, 0, 0.87],
'tweak.L': [0.75, 0.65, 0],
'tweak.R': [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):
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):
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):
bl_idname = 'bonewidget.create_widget'
bl_label = "Create Widget"
bl_options = {'REGISTER', 'UNDO'}
symmetrize : BoolProperty(name='Symmetrize', default=True)
def invoke(self, context, event):
self.symmetrize = ctx.prefs.symmetrize
return self.execute(context)
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_copy, col=ctx.widget_col, prefix=ctx.prefs.prefix, separate=False)
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'}
@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
rig = ctx.rig
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 w in widgets:
link_to_col(w, ctx.widget_col)
w.select_set(True)
context.view_layer.objects.active = w
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'}
symmetrize : BoolProperty(name='Symmetrize', default=True)
def invoke(self, context, event):
self.symmetrize = ctx.prefs.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.symmetrize:
for b in bones:
symmetrize_bone_shape(b, prefix=ctx.prefs.prefix)
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'}
@classmethod
def poll(cls, context):
return ctx.widget
def execute(self, context):
shape = ctx.widget
bone = ctx.bone
if bone:
name = bone.name
else:
name = shape.name
#name = name.split('-', 1)[-1]
#name = re.sub('.\d{3}', '', name)
folder = ctx.active_folder
name = folder.get_widget_clean_name(name)
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})
render_widget(shape, icon_path)
widget = folder.add_widget(name)
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.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=True,
rename=prefs.rename, col=ctx.widget_col, prefix=prefs.prefix)
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)

245
properties.py Normal file
View File

@ -0,0 +1,245 @@
import bpy
import shutil
from pathlib import Path
import os
import bpy.utils.previews
from bpy.types import PropertyGroup, AddonPreferences
from bpy.props import StringProperty, BoolProperty, CollectionProperty, \
FloatProperty, IntProperty, PointerProperty, EnumProperty, FloatVectorProperty
from bone_widget import ctx
from .ui import update_tab, draw_prefs
from .transform_utils import transform_mesh
from .shape_utils import symmetrize_bone_shape, get_flipped_bone
def undo_redo(self, context):
wm = context.window_manager
op_names = ('transform_widget','create_widget', 'match_transform', 'clean_widget')
op_ids = [ctx.get_op_id(o) for o in op_ids]
if (ops and ops[-1].bl_idname in op_ids):
bpy.ops.wm.undo()
bpy.ops.wm.redo()
def transform_widgets(self, context):
#bones = []
shapes = ctx.shapes
for shape, coords in shapes.items():
#bones.append(ctx.get_bone(shape))
#print(shape)
transform_mesh(shape.data, self.loc, self.rot, self.scale,
self.xz_scale, self.size, self.slide, coords=coords)
if ctx.prefs.symmetrize:
for bone in ctx.selected_bones:
flipped_bone = get_flipped_bone(bone)
if flipped_bone and flipped_bone.custom_shape in shapes:
continue
symmetrize_bone_shape(bone, prefix=ctx.prefs.prefix)
class BW_PG_transforms(PropertyGroup):
size : FloatProperty(name='Size', default=1.0, min=0, update=transform_widgets)
xz_scale : FloatProperty(name='XZ Scale', default=1.0, min=0, update=transform_widgets)
slide : FloatProperty(name='Slide', default=0.0, update=transform_widgets)
loc : FloatVectorProperty(name='Location', default=(0.0, 0.0, 0.0),
subtype='TRANSLATION', update=transform_widgets)
rot : FloatVectorProperty(name = 'Rotation', default=(0.0, 0.0, 0.0),
subtype='EULER', step=100, precision=0, update=transform_widgets)
scale : FloatVectorProperty(name='Scale', default=(1.0, 1.0, 1.0),
subtype='XYZ', update=transform_widgets)
def rename_widget(self, context):
ctx.active_folder.rename_widget(self, self.name)
class BW_PG_widget(PropertyGroup):
name : StringProperty(update=rename_widget)
#path : StringProperty(subtype='FILE_PATH')
class BW_PG_folder(PropertyGroup):
icons = bpy.utils.previews.new()
path : StringProperty(subtype='FILE_PATH', default='Default')
expand : BoolProperty()
widgets : CollectionProperty(type=BW_PG_widget)
widget_index : IntProperty()
@property
def abspath(self):
return ctx.abspath(self.path)
@property
def name(self):
return Path(self.path).name
def load_icon(self, widget):
icon = self.get_widget_icon(widget)
if icon:
icon.reload()
else :
icon_path = self.get_icon_path(widget)
self.icons.load(widget.name, str(icon_path), 'IMAGE', True)
def get_widget_icon(self, widget):
return self.icons.get(widget.name)
def add_widget(self, name):
name = self.get_widget_display_name(name)
w = self.widgets.get(name)
if w:
self.load_icon(w)
return
w = self.widgets.add()
w.name = name
self.active_widget = w
self.load_icon(w)
return w
def remove_widget(self, widget):
index = self.widgets[:].index(widget)
widget_path = self.get_widget_path(widget)
icon_path = self.get_icon_path(widget)
if widget_path.exists():
os.remove(widget_path)
if icon_path.exists():
os.remove(icon_path)
self.widgets.remove(index)
self.widget_index = min(len(self.widgets)-1, index)
@property
def active_widget(self):
if self.widgets and self.widget_index < len(self.widgets):
return self.widgets[self.widget_index]
@active_widget.setter
def active_widget(self, widget):
self.widget_index = list(self.widgets).index(widget)
def get_widget_clean_name(self, widget):
name = widget if isinstance(widget, str) else widget.name
name = name.split('-', 1)[-1]
return bpy.path.clean_name(name).lower()
def get_widget_display_name(self, widget):
name = widget if isinstance(widget, str) else widget.name
return name.replace('_', ' ').title()
def get_widget_path(self, widget):
name = self.get_widget_clean_name(widget)
return (self.abspath/name).with_suffix('.blend')
def get_icon_path(self, widget):
name = self.get_widget_clean_name(widget)
return (self.abspath/name).with_suffix('.png')
def rename_widget(self, widget, name):
if not widget.get('_name'):
widget['_name'] = name
return
src_blend = self.get_widget_path(widget['_name'])
dst_blend = self.get_widget_path(name)
src_icon = self.get_icon_path(widget['_name'])
dst_icon = self.get_icon_path(name)
widget['_name'] = name
if not src_blend:
return
if not src_blend.exists():
self.remove_widget(widget)
if src_blend != dst_blend:
os.rename(str(src_blend), str(dst_blend))
if src_icon != dst_icon:
os.rename(str(src_icon), str(dst_icon))
self.load_icon(widget)
def load_widgets(self):
self.widgets.clear()
for widget_blend in self.abspath.glob('*.blend'):
self.add_widget(widget_blend.stem)
#class BW_PG_settings(PropertyGroup):
# pass
class BW_PG_bone_color(PropertyGroup):
value: FloatVectorProperty(name='Color', subtype='COLOR', default=[0.0, 0.0, 0.0])
name: StringProperty(name='Name')
class BW_prefs(AddonPreferences):
bl_idname = __package__
default_folder : PointerProperty(type=BW_PG_folder)
folders : CollectionProperty(type=BW_PG_folder)
folder_index : IntProperty()
folder_enum : EnumProperty(name='Folders', items=lambda s,c : ctx.folder_items)
collection : StringProperty(name='Collection', default='Widget')
prefix : StringProperty(name='Prefix', default='WGT-')
category : StringProperty(name='Tab', default='Rigging', update=lambda s,c : update_tab())
symmetrize : BoolProperty(name='Symmetrize', default=True, update=undo_redo)
separate : BoolProperty(default=True, name="Separate", update=undo_redo)
match_transform : BoolProperty(default=True, name="Match Transform", update=undo_redo)
rename : BoolProperty(default=True, name="Rename", update=undo_redo)
show_transforms : BoolProperty(default=False)
transforms : PointerProperty(type=BW_PG_transforms)
grid_view : BoolProperty(name='Grid View', default=True)
#use_custom_colors : BoolProperty(name='Custom Colors', default=False, update=set_default_colors)
def draw(self, context):
draw_prefs(self.layout)
classes = (
BW_PG_bone_color,
BW_PG_widget,
BW_PG_transforms,
BW_PG_folder,
BW_prefs
)
def register():
for cls in classes:
bpy.utils.register_class(cls)
#update_folder_items()
def unregister():
#bpy.utils.previews.remove(ctx.prefs.default_folder.icons)
#for f in ctx.folders:
# bpy.utils.previews.remove(f.icons)
for cls in classes:
bpy.utils.unregister_class(cls)

181
shape_utils.py Normal file
View File

@ -0,0 +1,181 @@
import bpy
from .transform_utils import get_bone_matrix, transform_matrix
#from bone_widget import ctx
def rename_shape(shape, bone, prefix=''):
shape.name = shape.data.name = prefix + bone.name
#print('Rename Shape', bone, shape.name)
def get_bone_from_shape(shape):
armatures = [o for o in bpy.context.scene.objects if o.type == 'ARMATURE']
return next((b for a in armatures for b in a.pose.bones if b.custom_shape is shape), None)
'''
def set_shape(bone, shape):
shape.matrix_world = get_bone_matrix(bone)
bone.custom_shape = shape
ctx.rename_shape(shape, bone)
ctx.link_to_widget_col(shape)
'''
def get_side(name, fallback=None):
name = name.lower()
if name.endswith(('.l', '_l')) or name.startswith(('l.', 'l_')):
return 'LEFT'
elif name.endswith(('.r', '_r')) or name.startswith(('r.', 'r_')):
return 'RIGHT'
return fallback
def get_flipped_name(name):
import re
def flip(match, start=False):
if not match.group(1) or not match.group(2):
return
sides = {
'R' : 'L',
'r' : 'l',
'L' : 'R',
'l' : 'r',
}
if start:
side, sep = match.groups()
return sides[side] + sep
else:
sep, side, num = match.groups()
return sep + sides[side] + (num or '')
start_reg = re.compile(r'(l|r)([._-])', flags=re.I)
if start_reg.match(name):
flipped_name = start_reg.sub(lambda x: flip(x, True), name)
else:
flipped_name = re.sub(r'([._-])(l|r)(\.\d+)?$', flip, name, flags=re.I)
return flipped_name
def get_flipped_bone(bone):
flipped_name = get_flipped_name(bone.name)
if flipped_name == bone.name:
return
return bone.id_data.pose.bones.get(flipped_name)
def link_to_col(shape, col):
for c in shape.users_collection:
c.objects.unlink(shape)
col.objects.link(shape)
def get_clean_shape(bone, shape, separate=True, rename=True,
col=None, match=True, prefix='', apply_transforms=True):
old_shape = shape
old_bone = get_bone(old_shape)
bone.custom_shape = None
if separate:
if old_bone:
rename_shape(old_shape, old_bone, prefix=prefix)
else:
bpy.data.objects.remove(old_shape)
shape = shape.copy()
shape.data = shape.data.copy()
bone.custom_shape = shape
if match:
shape.matrix_world = get_bone_matrix(bone)
if apply_transforms:
if bpy.app.version_string < '3.0.0':
scale = bone.custom_shape_scale
if not bone.use_custom_shape_bone_size:
scale /= bone.bone.length
mat = transform_matrix(scale=(1/scale,)*3)
shape.data.transform(mat)
bone.custom_shape_scale = 1
#mirror_bone.custom_shape_scale =
else:
loc = bone.custom_shape_translation
rot = bone.custom_shape_rotation_euler
scale = bone.custom_shape_scale_xyz
if not bone.use_custom_shape_bone_size:
scale /= bone.bone.length
mat = transform_matrix(loc=loc, rot=rot, scale=scale)
shape.data.transform(mat)
bone.custom_shape_translation = 0, 0, 0
bone.custom_shape_rotation_euler = 0, 0, 0
bone.custom_shape_scale_xyz = 1, 1, 1
bone.use_custom_shape_bone_size = True
if rename:
rename_shape(shape, bone, prefix=prefix)
if col:
link_to_col(shape, col)
return shape
def get_bone(shape):
armatures = [o for o in bpy.context.scene.objects if o.type == 'ARMATURE']
return next((b for a in armatures for b in a.pose.bones if b.custom_shape == shape), None)
def symmetrize_bone_shape(bone, prefix=None):
active_side = get_side(bone.name, 'LEFT')
shape = bone.custom_shape
flipped_bone = get_flipped_bone(bone)
if not flipped_bone or not shape:
return
if bpy.app.version_string < '3.0.0':
flipped_bone.custom_shape_scale = b.custom_shape_scale
else:
flipped_bone.custom_shape_translation = bone.custom_shape_translation
flipped_bone.custom_shape_rotation_euler = bone.custom_shape_rotation_euler
flipped_bone.custom_shape_scale_xyz = bone.custom_shape_scale_xyz
flipped_shape = flipped_bone.custom_shape
if flipped_shape:
flipped_bone.custom_shape = None
old_bone = get_bone(flipped_shape)
if old_bone:
rename_shape(flipped_shape, old_bone, prefix=prefix)
else:
bpy.data.objects.remove(flipped_shape)
flipped_shape = shape.copy()
flipped_shape.data = flipped_shape.data.copy()
flipped_shape.data.transform(transform_matrix(scale=(-1, 1, 1)))
flipped_bone.custom_shape = get_clean_shape(flipped_bone, flipped_shape, rename=True, separate=False, prefix=prefix, apply_transforms=False)
flipped_bone.use_custom_shape_bone_size = bone.use_custom_shape_bone_size
for c in shape.users_collection:
c.objects.link(flipped_bone.custom_shape)
return flipped_bone.custom_shape

81
transform_utils.py Normal file
View File

@ -0,0 +1,81 @@
import bpy
from mathutils import Matrix, Vector
def get_bone_size_factor(bone, ob, relative=True):
world_box = [ob.matrix_world @ Vector(co) for co in ob.bound_box]
height = (world_box[1]-world_box[0]).length
depth = (world_box[3]-world_box[0]).length
width = (world_box[4]-world_box[0]).length
'''
length = height
if height < 0.001:
if depth > 0.001:
length = depth
elif width > 0.001:
length = width
'''
length = max((height, depth, width))
size_factor = 1 / length
if relative:
size_factor /= bone.bone.length
return size_factor
def transform_matrix(loc=(0, 0, 0), rot=(0, 0, 0), scale=(1, 1, 1)):
l_mat = Matrix.Translation(Vector(loc))
rx_mat = Matrix.Rotation(rot[0], 4, 'X')
ry_mat = Matrix.Rotation(rot[1], 4, 'Y')
rz_mat =Matrix.Rotation(rot[2], 4, 'Z')
r_mat = rz_mat @ ry_mat @ rx_mat
sx_mat = Matrix.Scale(scale[0], 4, Vector((1,0,0)))
sy_mat = Matrix.Scale(scale[1], 4, Vector((0,1,0)))
sz_mat = Matrix.Scale(scale[2], 4, Vector((0,0,1)))
s_mat = sx_mat @ sy_mat @ sz_mat
return l_mat @ r_mat @ s_mat
def get_bone_matrix(bone):
bone_mat = bone.matrix
if bone.custom_shape_transform:
bone_mat = bone.custom_shape_transform.matrix
bone_mat = bone_mat @ transform_matrix(scale=(bone.bone.length,)*3)
bone_mat = bone.id_data.matrix_world @ bone_mat
return bone_mat
def apply_mat_to_verts(data, mat, coords=None):
if coords is None:
coords = [v.co for v in data.vertices]
coords = [v for co in coords for v in mat @ co]
data.vertices.foreach_set('co', coords)
data.update()
def transform_mesh(data, loc, rot, scale, xz_scale, size, slide, coords=None):
if coords is None:
coords = [0]*len(data.vertices)*3
data.vertices.foreach_get('co', coords)
sx = scale[0] * size * xz_scale
sy = scale[1] * size
sz = scale[2] * size * xz_scale
l_vec = loc + Vector((0, slide, 0))
mat = transform_matrix(l_vec, rot, (sx, sy, sz))
data.vertices.foreach_set('co', coords)
data.transform(mat)
#apply_mat_to_verts(data, mat, coords)

309
ui.py Normal file
View File

@ -0,0 +1,309 @@
import bpy
from bpy.types import Menu, Panel, UIList
from bpy.props import EnumProperty, StringProperty
from bone_widget import ctx
from pathlib import Path
def add_row(layout, prop, data=None, name='', icon=None, operator=None, properties={}):
data = data or ctx.prefs
split = layout.split(factor=0.33, align=True)
split.alignment= 'RIGHT'
split.label(text=name+':' if name else '')
row = split.row(align=True)
row.prop(data, prop, text='')
if icon and not operator:
row.label(icon=icon)
row.separator()
if operator:
op = row.operator(operator, icon=icon, text='', emboss=False)
for k, v in properties.items():
setattr(op, k, v)
else:
row.label(icon='BLANK1')
def add_bool_row(layout, prop, data=None, name='', icon=None):
data = data or ctx.prefs
row = layout.row(align=True)
row.label(icon=icon)
row.prop(data, prop, text=name)
'''
def add_color_row(layout, data=None, name='', index=0):
data = data or ctx.prefs.colors
#split = layout.split(factor=0.25, align=True)
#split.alignment= 'RIGHT'
#split.label(text=name+':' if name else '')
split = layout.split(factor=0.66, align=True)
row = split.row(align=True)
row.prop(data, 'name', text='')
row = split.row(align=True)
row.prop(data, 'value', text='')
row.separator()
#row = split.row(align=True)
row.operator('bonewidget.remove_color', icon='REMOVE', text='', emboss=False).index = index
'''
def draw_prefs(layout):
layout = layout.column(align=False)
add_row(layout, 'category', name='Tab', icon='COPY_ID')
add_row(layout, 'collection', name='Collection', icon='OUTLINER_COLLECTION')
add_row(layout, 'prefix', name='Prefix', icon='SYNTAX_OFF')
layout.separator()
add_row(layout, 'path', data=ctx.prefs.default_folder, name='Folders',
icon='ADD', operator='bonewidget.add_folder')
for i, f in enumerate(ctx.prefs.folders):
add_row(layout, 'path', icon='REMOVE',
operator='bonewidget.remove_folder', data=f, properties={'index': i})
layout.separator()
split = layout.split(factor=0.33, align=True)
split.alignment= 'RIGHT'
split.label(text='Auto:')
col = split.column(align=True)
add_bool_row(col, 'symmetrize', name='Symmetrize', icon='MOD_MIRROR')
add_bool_row(col, 'separate', name='Separate', icon='UNLINKED')
#col = row.column(align=True)
add_bool_row(col, 'match_transform', name='Match Transform', icon='TRANSFORM_ORIGINS')
add_bool_row(col, 'rename', name='Rename', icon='SYNTAX_OFF')
#col.prop(ctx.prefs, 'symmetrize', text='Symetrize', icon='MOD_MIRROR')
#col.prop(ctx.prefs, 'separate', text='Separate', icon='UNLINKED')
#col.prop(ctx.prefs, 'match_transform', text='Match Transform', icon='TRANSFORM_ORIGINS')
#col.prop(ctx.prefs, 'rename', text='Rename', icon='SYNTAX_OFF')
'''
layout.separator()
split = layout.split(factor=0.33, align=True)
split.alignment= 'RIGHT'
split.label(text='Bone Colors:')
#split.prop(ctx.prefs, 'use_custom_colors')
col = split.column(align=True)
row = col.row(align=True)
row.prop(ctx.prefs, 'select_color', text='')
row.prop(ctx.prefs, 'active_color', text='')
row.separator()
row.operator("bonewidget.add_color", icon='ADD', text='', emboss=False)
#if ctx.prefs.use_custom_colors:
col.separator()
for i, c in enumerate(ctx.prefs.colors):
add_color_row(col, data=c, index=i)
'''
class BW_UL_widget(UIList):
def draw_item(self, context, layout, data, item, icon, active_data, active_propname, index, flt_flag):
icon = data.icons.get(item.name)
icon_id = icon.icon_id if icon else -1
#layout.scale_x = 1.5
if self.layout_type in {'DEFAULT', 'COMPACT'}:
layout.prop(item, 'name', text="", emboss=False, icon_value=icon_id)
elif self.layout_type in {'GRID'}:
layout.alignment = 'CENTER'
layout.label(text="", icon_value=icon_id)
class BW_MT_folder(Menu):
bl_label = "Folder Special Menu"
def draw(self, context):
layout = self.layout
#layout.operator("bonewidget.add_folder", icon='ADD')
#layout.operator("bonewidget.remove_folder", icon='REMOVE')
#layout.operator("bonewidget.rename_folder", icon='SYNTAX_OFF')
layout.operator('bonewidget.refresh_folders', icon='FILE_REFRESH')
layout.operator('bonewidget.remove_unused_shape', icon='TRASH')
layout.operator('bonewidget.show_preferences', icon='PREFERENCES')
layout.operator('bonewidget.copy_widgets', icon='COPYDOWN')
layout.operator('bonewidget.paste_widgets', icon='PASTEDOWN')
layout.prop(ctx.prefs, 'grid_view')
class BW_PT_transforms(Panel):
bl_label = "Transforms"
bl_category = ctx.category
bl_space_type = 'VIEW_3D'
bl_region_type = 'UI'
bl_parent_id = 'BW_PT_main'
bl_options = {'DEFAULT_CLOSED'}
@classmethod
def poll(cls, context):
return ctx.show_transforms
def draw_header(self, context):
layout = self.layout
layout.label(icon='EMPTY_ARROWS')
def draw(self, context):
layout = self.layout
col = layout.column()
transforms = ctx.prefs.transforms
col.prop(transforms, "loc")
col.prop(transforms, "rot")
col.prop(transforms, "scale")
class BW_PT_main(Panel):
bl_label = "Bone Widget"
bl_category = ctx.category
bl_space_type = 'VIEW_3D'
bl_region_type = 'UI'
def get_nb_col(self, context) :
icon_size = 35
if context.region.width < 100:
return 1
_, y0 = context.region.view2d.region_to_view(0, 0)
_, y1 = context.region.view2d.region_to_view(0, 10)
region_scale = 10 / abs(y1 - y0)
ui_scale = context.preferences.view.ui_scale
cols = int((context.region.width - 48) / (ui_scale*region_scale*icon_size))
return max(1, cols)
def draw(self, context):
layout = self.layout
row = layout.row()
tool_col = row.column(align=True)
tool_col.scale_x = 1.05
tool_col.scale_y = 1.05
tool_col.menu('BW_MT_folder', icon='TRIA_DOWN', text='')
#tool_col.separator()
tool_col.operator('bonewidget.add_widget', icon='ADD', text='')
tool_col.operator('bonewidget.remove_widget', icon='REMOVE', text='')
tool_col.separator()
tool_col.separator()
#if ctx.prefs.auto_symmetrize:
# tool_col.operator('bonewidget.symmetrize_widget', icon='MOD_MIRROR', text='')
op_col = tool_col.column(align=True)
op_col.scale_y = 1.5
op_col.operator('bonewidget.transform_widget', icon='EMPTY_ARROWS', text='') #
op_col.operator('bonewidget.clean_widget', icon='SHADERFX', text='')
folder = ctx.active_folder
widget_col = row.column(align=True)
folder_row = widget_col.row(align=True)
folder_row.scale_y = 1.05
#folder_row.scale_y = 1.15
#folder_row.scale_x = 1.25
if len(ctx.folder_items) <= 3:
folder_row.prop(ctx.prefs, 'folder_enum', expand=True)
else:
folder_row.prop(ctx.prefs, 'folder_enum', text='', icon='FILE_FOLDER')
if folder:
widget_col.template_list('BW_UL_widget', 'BW_widgets', folder, 'widgets', folder, 'widget_index',
rows=5, columns=self.get_nb_col(context), type='GRID' if ctx.prefs.grid_view else 'DEFAULT')
#widget_col.template_list('UI_UL_list', 'BW_widgets', folder, 'widgets', folder, 'widget_index', rows=4)
#layout.prop(self, 'folder_enum')
edit_row = widget_col.row(align=True)
if context.mode in ('EDIT_MESH', 'OBJECT') and ctx.bone:
edit_row.operator('bonewidget.return_to_rig', text='Return', icon='LOOP_BACK')
else:
edit_row.operator('bonewidget.create_widget', text='Create')
edit_row.operator('bonewidget.edit_widget', text='Edit')
if ctx.show_transforms:
data = context.window_manager.operators[-1].properties
layout.separator()
#opt_box = layout.box()
opt_col = layout.column(align=True)
#opt_col.operator("bonewidget.match_transform",icon = 'GROUP_BONE')
row = opt_col.row(align =True)
row.label(text='Size :')
row.operator("bonewidget.match_transform", text='Relative').relative = True
row.operator("bonewidget.match_transform", text='Absolute').relative = False
opt_col.separator()
transforms = ctx.prefs.transforms
opt_col.prop(transforms, "size")
opt_col.prop(transforms, "xz_scale")
opt_col.prop(transforms, "slide")
elif ctx.clean_widget_op:
data = ctx.clean_widget_op.properties
#layout.separator()
#opt_col = layout.column()
elif ctx.show_prefs_op:
layout.separator()
draw_prefs(layout)
#bpy.ops.wm.save_userpref()
prefs_unsaved = context.preferences.is_dirty
layout.operator('wm.save_userpref', text="Save Preferences" + (" *" if prefs_unsaved else ""))
classes = (
BW_UL_widget,
BW_MT_folder,
BW_PT_main,
BW_PT_transforms,
)
def update_tab():
panels = (bpy.types.BW_PT_main, bpy.types.BW_PT_transforms)
for p in panels:
p.bl_category = ctx.prefs.category
bpy.utils.unregister_class(p)
for p in panels:
bpy.utils.register_class(p)
def register():
for cls in classes:
bpy.utils.register_class(cls)
update_tab()
def unregister():
for cls in reversed(classes):
bpy.utils.unregister_class(cls)

BIN
widgets/Default/all.blend Normal file

Binary file not shown.

BIN
widgets/Default/all.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

BIN
widgets/Default/circle.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

BIN
widgets/Default/cube.blend Normal file

Binary file not shown.

BIN
widgets/Default/cube.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

BIN
widgets/Default/fk.blend Normal file

Binary file not shown.

BIN
widgets/Default/fk.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 809 B

Binary file not shown.

BIN
widgets/Default/foot_fk.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

BIN
widgets/Default/foot_ik.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

BIN
widgets/Default/gum.blend Normal file

Binary file not shown.

BIN
widgets/Default/gum.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

BIN
widgets/Default/hand_fk.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 797 B

Binary file not shown.

BIN
widgets/Default/hand_ik.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 783 B

BIN
widgets/Default/head.blend Normal file

Binary file not shown.

BIN
widgets/Default/head.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

BIN
widgets/Default/ik_l.blend Normal file

Binary file not shown.

BIN
widgets/Default/ik_l.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 809 B

BIN
widgets/Default/jaw.blend Normal file

Binary file not shown.

BIN
widgets/Default/jaw.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 584 B

Binary file not shown.

BIN
widgets/Default/jaw_up.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 300 B

BIN
widgets/Default/line.blend Normal file

Binary file not shown.

BIN
widgets/Default/line.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 415 B

Binary file not shown.

BIN
widgets/Default/master.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

BIN
widgets/Default/mouth.blend Normal file

Binary file not shown.

BIN
widgets/Default/mouth.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 692 B

BIN
widgets/Default/root.blend Normal file

Binary file not shown.

BIN
widgets/Default/root.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

BIN
widgets/Default/sphere.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 KiB

BIN
widgets/Default/teeth.blend Normal file

Binary file not shown.

BIN
widgets/Default/teeth.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 830 B

BIN
widgets/Default/tools.blend Normal file

Binary file not shown.

BIN
widgets/Default/tools.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 977 B

BIN
widgets/Default/up.blend Normal file

Binary file not shown.

BIN
widgets/Default/up.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 930 B