First Commit
|
@ -0,0 +1,2 @@
|
|||
__pycache__
|
||||
*.py[cod]
|
92
README.md
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
|
|
@ -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()})
|
||||
|
||||
|
|
@ -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))
|
||||
|
|
@ -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)
|
|
@ -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)
|
|
@ -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
|
|
@ -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)
|
||||
|
|
@ -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)
|
||||
|
||||
|
||||
|
||||
|
||||
|
After Width: | Height: | Size: 1.1 KiB |
After Width: | Height: | Size: 1.2 KiB |
After Width: | Height: | Size: 1.1 KiB |
After Width: | Height: | Size: 1.1 KiB |
After Width: | Height: | Size: 809 B |
After Width: | Height: | Size: 1.5 KiB |
After Width: | Height: | Size: 1.7 KiB |
After Width: | Height: | Size: 1.2 KiB |
After Width: | Height: | Size: 1.3 KiB |
After Width: | Height: | Size: 797 B |
After Width: | Height: | Size: 783 B |
After Width: | Height: | Size: 1.3 KiB |
After Width: | Height: | Size: 809 B |
After Width: | Height: | Size: 584 B |
After Width: | Height: | Size: 300 B |
After Width: | Height: | Size: 415 B |
After Width: | Height: | Size: 1.1 KiB |
After Width: | Height: | Size: 692 B |
After Width: | Height: | Size: 1.5 KiB |
After Width: | Height: | Size: 2.1 KiB |
After Width: | Height: | Size: 830 B |
After Width: | Height: | Size: 977 B |
After Width: | Height: | Size: 930 B |