├── .gitattributes ├── .gitignore ├── README.md ├── __init__.py ├── func_bgl.py ├── func_picker.py ├── func_shape.py ├── op_material.py ├── op_picker.py ├── op_shape.py ├── panels.py ├── properties.py ├── snapping_utils.py └── utils.py /.gitattributes: -------------------------------------------------------------------------------- 1 | # Auto detect text files and perform LF normalization 2 | * text=auto -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .pyc 2 | *.pyc 3 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # rigUI 2 | 3 | This addon need the rigutils module installed in the blender python modules directory. The rigutils module provide snap IK FK functions among other. 4 | 5 | Test file with a working picker 6 | https://drive.google.com/open?id=1Ld_0ON1zoFqdCqvZH_EelMMSQJqhfajB 7 | 8 | 9 | How to use the addon : 10 | https://vimeo.com/241970235 11 | -------------------------------------------------------------------------------- /__init__.py: -------------------------------------------------------------------------------- 1 | bl_info = { 2 | "name": "Rig UI", 3 | "author": "Christophe Seux", 4 | "version": (0, 1), 5 | "blender": (2, 77, 0), 6 | "location": "", 7 | "description": "", 8 | "warning": "", 9 | "wiki_url": "", 10 | "tracker_url": "", 11 | "category": "Rigging"} 12 | 13 | if "bpy" in locals(): 14 | import imp 15 | imp.reload(op_material) 16 | imp.reload(op_picker) 17 | imp.reload(op_shape) 18 | imp.reload(properties) 19 | imp.reload(panels) 20 | 21 | from .op_material import * 22 | from .op_picker import * 23 | from .op_shape import * 24 | from .properties import * 25 | from .panels import * 26 | 27 | import bpy 28 | 29 | def picker_icon(self, context): 30 | self.layout.operator("rigui.ui_draw",icon ='MOD_ARMATURE',text ="") 31 | 32 | def register(): 33 | 34 | bpy.types.Armature.UI = bpy.props.PointerProperty(type= bpy.types.PropertyGroup) 35 | bpy.utils.register_module(__name__) 36 | bpy.types.Object.UI = bpy.props.PointerProperty(type= ObjectUISettings) 37 | bpy.types.Scene.UI = bpy.props.PointerProperty(type= SceneUISettings) 38 | bpy.types.IMAGE_HT_header.prepend(picker_icon) 39 | 40 | 41 | 42 | def unregister(): 43 | del bpy.types.Armature.UI 44 | del bpy.types.Object.UI 45 | bpy.types.IMAGE_HT_header.remove(picker_icon) 46 | bpy.utils.unregister_module(__name__) 47 | -------------------------------------------------------------------------------- /func_bgl.py: -------------------------------------------------------------------------------- 1 | import bpy 2 | import bgl 3 | import blf 4 | 5 | from mathutils import Vector 6 | from .func_picker import * 7 | from .utils import intersect_rectangles,point_inside_rectangle,\ 8 | point_over_shape,border_over_shape,canevas_space 9 | 10 | 11 | def draw_polyline_2d_loop(verts,faces,loops,color,contour,width): 12 | dpi = int(bpy.context.user_preferences.system.pixel_size) 13 | 14 | bgl.glColor4f(*color) 15 | bgl.glEnable(bgl.GL_BLEND) 16 | #bgl.glEnable(bgl.GL_LINE_SMOOTH) 17 | 18 | bgl.glColor4f(*color) 19 | for face in faces : 20 | bgl.glBegin(bgl.GL_POLYGON) 21 | for v_index in face : 22 | coord = verts[v_index] 23 | bgl.glVertex2f(coord[0],coord[1]) 24 | bgl.glEnd() 25 | 26 | ''' 27 | #antialiasing contour 28 | bgl.glColor4f(*color) 29 | bgl.glLineWidth(1) 30 | for loop in loops : 31 | if faces : 32 | bgl.glBegin(bgl.GL_LINE_LOOP) 33 | else : 34 | bgl.glBegin(bgl.GL_LINE_STRIP) 35 | for v_index in loop : 36 | coord = verts[v_index] 37 | bgl.glVertex2f(coord[0],coord[1]) 38 | bgl.glEnd() 39 | ''' 40 | 41 | 42 | if width : 43 | bgl.glLineWidth(width*dpi) 44 | bgl.glColor4f(*contour) 45 | 46 | for loop in loops : 47 | if faces : 48 | bgl.glBegin(bgl.GL_LINE_LOOP) 49 | else : 50 | bgl.glBegin(bgl.GL_LINE_STRIP) 51 | for v_index in loop : 52 | coord = verts[v_index] 53 | bgl.glVertex2f(coord[0],coord[1]) 54 | bgl.glEnd() 55 | 56 | bgl.glDisable(bgl.GL_BLEND) 57 | #bgl.glDisable(bgl.GL_LINE_SMOOTH) 58 | bgl.glEnd() 59 | 60 | return 61 | 62 | def draw_border(border) : 63 | bgl.glEnable(bgl.GL_BLEND) 64 | bgl.glColor4f(1,1,1,0.2) 65 | bgl.glBegin(bgl.GL_POLYGON) 66 | for v in border : 67 | bgl.glVertex2f(v[0],v[1]) 68 | 69 | bgl.glEnd() 70 | 71 | bgl.glColor4f(0.0, 0.0, 0.0, 0.5) 72 | bgl.glLineWidth(2) 73 | bgl.glLineStipple(3, 0xAAAA) 74 | bgl.glEnable(bgl.GL_LINE_STIPPLE) 75 | 76 | bgl.glBegin(bgl.GL_LINE_LOOP) 77 | 78 | for v in border : 79 | bgl.glVertex2f(v[0],v[1]) 80 | 81 | bgl.glEnd() 82 | 83 | bgl.glColor4f(1.0, 1.0, 1.0, 1) 84 | bgl.glLineWidth(1) 85 | bgl.glBegin(bgl.GL_LINE_LOOP) 86 | for v in border : 87 | bgl.glVertex2f(v[0],v[1]) 88 | 89 | bgl.glEnd() 90 | bgl.glDisable(bgl.GL_LINE_STIPPLE) 91 | bgl.glDisable(bgl.GL_BLEND) 92 | 93 | def draw_text(mouse,text,color) : 94 | dpi = int(bpy.context.user_preferences.system.pixel_size) 95 | 96 | bgl.glEnable(bgl.GL_BLEND) 97 | font_id =0 # XXX, need to find out how best to get this. 98 | # draw some text 99 | bgl.glColor4f(0,0,0,0.75) 100 | blf.blur(font_id,5) 101 | blf.position(font_id, mouse[0]+10*dpi, mouse[1]-20*dpi, 0) 102 | blf.size(font_id, 9*dpi, 96) 103 | blf.draw(font_id, text) 104 | 105 | bgl.glEnd() 106 | bgl.glColor4f(*color) 107 | blf.blur(font_id,0) 108 | blf.draw(font_id, text) 109 | bgl.glDisable(bgl.GL_BLEND) 110 | 111 | 112 | def select_bone(self,context,event) : 113 | ob = context.object 114 | if ob and ob.type =='ARMATURE' and ob.data.UI : 115 | shape_data = ob.data.UI 116 | selected_bones = [b for b in ob.pose.bones if b.bone.select] 117 | 118 | if not event.shift and not event.alt : 119 | for b in ob.pose.bones : 120 | b.bone.select= False 121 | 122 | for shape in [s for s in shape_data['shapes'] if not s['shape_type']==["DISPLAY"]]: 123 | points = [canevas_space(p,self.scale,self.offset) for p in shape['points']] 124 | bound = [canevas_space(p,self.scale,self.offset) for p in shape['bound']] 125 | loops = shape['loops'] 126 | 127 | ## Colision check 128 | over = False 129 | if self.is_border : 130 | if intersect_rectangles(self.border,bound) : #start check on over bound_box 131 | over = border_over_shape(self.border,points,loops) 132 | else : 133 | if point_inside_rectangle(self.end,bound) : 134 | over = point_over_shape(self.end,points,loops) 135 | 136 | if over : 137 | if shape['shape_type'] == 'BONE' : 138 | bone = context.object.pose.bones.get(shape['bone']) 139 | if bone: 140 | if event.alt : 141 | bone.bone.select = False 142 | else : 143 | bone.bone.select = True 144 | context.object.data.bones.active = bone.bone 145 | 146 | if shape['shape_type'] == 'FUNCTION' and event.value== 'RELEASE' and not self.is_border: 147 | # restore selection 148 | for b in selected_bones : 149 | b.bone.select = True 150 | 151 | function = shape['function'] 152 | if shape.get('variables') : 153 | variables=shape['variables'].to_dict() 154 | 155 | else : 156 | variables={} 157 | 158 | variables['event']=event 159 | 160 | print(variables) 161 | 162 | globals()[function](variables) 163 | 164 | 165 | 166 | def draw_callback_px(self, context): 167 | ob = context.object 168 | if ob and ob.type =='ARMATURE' and ob.data.UI and context.area.as_pointer() == self.adress: 169 | shape_data = ob.data.UI 170 | rig_layers = [i for i,l in enumerate(ob.data.layers) if l] 171 | 172 | region = context.region 173 | self.scale = region.height 174 | self.offset = (-self.scale/2 + region.width/2,0) 175 | self.outside_point = (region.x-region.width,region.y-region.height) 176 | 177 | #draw BG 178 | bg_color = shape_data['shapes'][0]['color'] 179 | bg_point = [(0,region.height),(region.width,region.height),(region.width,0),(0,0)] 180 | bg_color = [c-0.05 for c in bg_color]+[1] 181 | draw_polyline_2d_loop(bg_point,[[0,1,2,3]],[[0,1,2,3]],bg_color,(0,0,0,1),0) 182 | 183 | show_tooltip = False 184 | for shape in shape_data['shapes']: 185 | 186 | color = shape['color'] 187 | points = [canevas_space(p,self.scale,self.offset) for p in shape['points']] 188 | bound = [canevas_space(p,self.scale,self.offset) for p in shape['bound']] 189 | loops = shape['loops'] 190 | faces = shape['faces'] 191 | 192 | select=None 193 | contour_color = [0,0,0] 194 | contour_alpha = 1 195 | width = 0 196 | shape_color = [c for c in color] 197 | shape_alpha = 1 198 | 199 | 200 | if shape['shape_type'] == 'DISPLAY' and not faces: 201 | width = 1 202 | 203 | if shape['shape_type'] != 'DISPLAY' : 204 | if shape['shape_type'] == 'BONE' : 205 | bone = ob.pose.bones.get(shape['bone']) 206 | if bone: 207 | b_layers = [i for i,l in enumerate(bone.bone.layers) if l] 208 | 209 | if bone.bone_group : 210 | group_color = list(bone.bone_group.colors.normal) 211 | contour_color = [c*0.85 for c in group_color] 212 | width = 1 213 | 214 | if bone.bone.select : 215 | shape_color = [c*1.2+0.1 for c in color] 216 | if bone.bone_group : 217 | contour_color = [0.05,0.95,0.95] 218 | 219 | if ob.data.bones.active and shape['bone'] == ob.data.bones.active.name : 220 | if bone.bone_group : 221 | if bone.bone.select : 222 | shape_color = [c*1.2+0.2 for c in color] 223 | contour_color = [1,1,1] 224 | width = 1.5 225 | else : 226 | shape_color = [c*1.2+0.15 for c in color] 227 | contour_color = [0.9,0.9,0.9] 228 | width = 1 229 | 230 | 231 | if bone.bone.hide or not len(set(b_layers).intersection(rig_layers)) : 232 | shape_alpha = 0.33 233 | contour_alpha = 0.33 234 | 235 | elif shape['shape_type'] == 'FUNCTION' : 236 | if shape['function'] == 'boolean' : 237 | path = shape['variables']['data_path'] 238 | 239 | if ob.path_resolve(path) : 240 | shape_color = [c*1.4+0.08 for c in color] 241 | else : 242 | shape_color = [color[0],color[1],color[2]] 243 | 244 | 245 | ## On mouse over checking 246 | over = False 247 | if self.is_border : 248 | if intersect_rectangles(self.border,bound) : #start check on over bound_box 249 | over = border_over_shape(self.border,points,loops) 250 | else : 251 | if point_inside_rectangle(self.end,bound) : 252 | over = point_over_shape(self.end,points,loops) 253 | 254 | if over : 255 | show_tooltip = True 256 | tooltip = shape['tooltip'] 257 | if not self.press: 258 | shape_color = [c*1.02+0.05 for c in shape_color] 259 | contour_color = [c*1.03+0.05 for c in contour_color] 260 | 261 | shape_color.append(shape_alpha) 262 | contour_color.append(contour_alpha) 263 | draw_polyline_2d_loop(points,faces,loops,shape_color,contour_color,width) 264 | 265 | if show_tooltip : 266 | draw_text(self.end,tooltip,(1,1,1,1)) 267 | 268 | if self.is_border : 269 | draw_border(self.border) 270 | -------------------------------------------------------------------------------- /func_picker.py: -------------------------------------------------------------------------------- 1 | import bpy 2 | 3 | #from. insert_keyframe import insert_keyframe 4 | from. snapping_utils import * 5 | from .utils import get_IK_bones 6 | 7 | try : 8 | from rigutils.driver_utils import split_path 9 | from rigutils.insert_keyframe import insert_keyframe 10 | from rigutils.snap_ik_fk import snap_ik_fk 11 | from rigutils.utils import find_mirror 12 | except : 13 | print('You need to install the rigutils module in your blender modules path') 14 | 15 | def hide_layers(args) : 16 | """ """ 17 | ob = bpy.context.object 18 | 19 | layers=[] 20 | for bone in [b for b in ob.pose.bones if b.bone.select] : 21 | for i,l in enumerate(bone.bone.layers) : 22 | if l and i not in layers : 23 | layers.append(i) 24 | 25 | for i in layers : 26 | ob.data.layers[i] = not ob.data.layers[i] 27 | 28 | 29 | def boolean(args) : 30 | """ data_path, keyable """ 31 | ob = bpy.context.object 32 | data_path = args['data_path'] 33 | keyable = args['keyable'] 34 | 35 | #bone,prop = split_path(data_path) 36 | 37 | try : 38 | value = ob.path_resolve(data_path) 39 | #setattr(ob.pose.bones.get(bone),'["%s"]'%prop,not value) 40 | try : 41 | exec("ob.%s = %s"%(data_path,not value)) 42 | except : 43 | exec("ob%s= %s"%(data_path,not value)) 44 | 45 | if keyable and bpy.context.scene.tool_settings.use_keyframe_insert_auto : 46 | 47 | if not ob.animation_data: 48 | ob.animation_data_create() 49 | 50 | ob.keyframe_insert(data_path = data_path ,group = bone) 51 | 52 | except ValueError : 53 | print("Property don't exist") 54 | 55 | 56 | def select_layer(args) : 57 | ob = bpy.context.object 58 | 59 | layers =[] 60 | for bone in [b for b in ob.pose.bones if b.bone.select] : 61 | bone_layers = [i for i,l in enumerate(bone.bone.layers) if l] 62 | 63 | for l in bone_layers : 64 | if l not in layers : 65 | layers.append(l) 66 | 67 | for bone in ob.pose.bones : 68 | bone_layers = [i for i,l in enumerate(bone.bone.layers) if l] 69 | 70 | if len(set(bone_layers).intersection(layers)) : 71 | bone.bone.select = True 72 | 73 | def hide_bones(args) : 74 | ob = bpy.context.object 75 | selected_bone = [b for b in ob.pose.bones if b.bone.select] 76 | 77 | hide = [b.bone.hide for b in selected_bone if not b.bone.hide] 78 | 79 | visibility = True if len(hide) else False 80 | 81 | for bone in selected_bone : 82 | bone.bone.hide = visibility 83 | 84 | 85 | def select_all(args) : 86 | ob = bpy.context.object 87 | shapes = ob.data.UI['shapes'] 88 | bones = [s['bone'] for s in shapes if s['shape_type']=='BONE'] 89 | 90 | for bone_name in bones : 91 | bone = ob.pose.bones.get(bone_name) 92 | 93 | if bone : 94 | bone.bone.select = True 95 | 96 | def select_bones(args) : 97 | """bones (name list)""" 98 | ob = bpy.context.object 99 | pBones = ob.pose.bones 100 | bones_name =args['bones'] 101 | event = args['event'] 102 | if not event.shift : 103 | for bone in bpy.context.object.pose.bones: 104 | bone.bone.select = False 105 | 106 | bones = [pBones.get(b) for b in bones_name] 107 | 108 | select = False 109 | for bone in bones : 110 | if bone.bone.select == False : 111 | select =True 112 | break 113 | 114 | for bone in bones : 115 | bone.bone.select = select 116 | ob.data.bones.active = bones[-1].bone 117 | 118 | def keyframe_bones(args) : 119 | print(args) 120 | event=args['event'] 121 | bones=[] 122 | 123 | for bone in bpy.context.object.pose.bones : 124 | if not bone.name.startswith(('DEF','ORG','MCH')) and not bone.get('_unkeyable_') ==1 : 125 | if event.shift : 126 | bones.append(bone) 127 | elif not event.shift and bone.bone.select : 128 | bones.append(bone) 129 | 130 | 131 | for bone in bones : 132 | insert_keyframe(bone) 133 | 134 | def reset_bones(args) : 135 | event=args['event'] 136 | avoid_value =args['avoid_value'] 137 | 138 | ob = bpy.context.object 139 | 140 | bones=[] 141 | for bone in bpy.context.object.pose.bones : 142 | if not bone.name.startswith(('DEF','ORG','MCH')) and not bone.get('_unkeyable_') ==1 : 143 | if event.shift : 144 | bones.append(bone) 145 | elif not event.shift and bone.bone.select : 146 | bones.append(bone) 147 | 148 | 149 | for bone in bones : 150 | if bone.rotation_mode =='QUATERNION': 151 | bone.rotation_quaternion = 1,0,0,0 152 | 153 | if bone.rotation_mode == 'AXIS_ANGLE': 154 | bone.rotation_axis_angle = 0,0,1,0 155 | 156 | else : 157 | bone.rotation_euler = 0,0,0 158 | 159 | bone.location = 0,0,0 160 | bone.scale = 1,1,1 161 | 162 | for key,value in bone.items() : 163 | if key not in avoid_value and type(value) in (int,float): 164 | if ob.data.get("DefaultValues") and ob.data.DefaultValues['bones'].get(bone.name) : 165 | 166 | if key in ob.data.DefaultValues['bones'][bone.name] : 167 | bone[key] = ob.data.DefaultValues['bones'][bone.name][key] 168 | 169 | else : 170 | if type(value)== int : 171 | bone[key]=0 172 | else : 173 | bone[key]=0.0 174 | else : 175 | if type(value)== int : 176 | bone[key]=0 177 | else : 178 | bone[key]=0.0 179 | 180 | if bpy.context.scene.tool_settings.use_keyframe_insert_auto : 181 | insert_keyframe(bone) 182 | 183 | 184 | def flip_bones(args) : 185 | event=args['event'] 186 | 187 | ob = bpy.context.object 188 | arm = bpy.context.object.pose.bones 189 | 190 | selected_bones = [bone for bone in ob.pose.bones if bone.bone.select==True ] 191 | mirrorActive = None 192 | 193 | for bone in selected_bones : 194 | boneName = bone.name 195 | mirrorBoneName= find_mirror(boneName) 196 | 197 | mirrorBone = ob.pose.bones.get(mirrorBoneName) if mirrorBoneName else None 198 | 199 | if bpy.context.active_pose_bone == bone : 200 | mirrorActive = mirrorBone 201 | 202 | #print(mirrorBone) 203 | if not event.shift and mirrorBone: 204 | bone.bone.select = False 205 | 206 | if mirrorBone : 207 | mirrorBone.bone.select = True 208 | if mirrorActive : 209 | ob.data.bones.active = mirrorActive.bone 210 | 211 | def snap_ikfk(args): 212 | """ way, chain_index """ 213 | 214 | way =args['way'] 215 | chain_index = args['chain_index'] 216 | #auto_switch = self.auto_switch 217 | 218 | ob = bpy.context.object 219 | armature = ob.data 220 | 221 | SnappingChain = armature.get('SnappingChain') 222 | 223 | poseBone = ob.pose.bones 224 | dataBone = ob.data.bones 225 | 226 | IKFK_chain = SnappingChain['IKFK_bones'][chain_index] 227 | switch_prop = IKFK_chain['switch_prop'] 228 | 229 | FK_root = poseBone.get(IKFK_chain['FK_root']) 230 | FK_mid = [poseBone.get(b['name']) for b in IKFK_chain['FK_mid']] 231 | FK_tip = poseBone.get(IKFK_chain['FK_tip']) 232 | 233 | IK_last = poseBone.get(IKFK_chain['IK_last']) 234 | IK_tip = poseBone.get(IKFK_chain['IK_tip']) 235 | IK_pole = poseBone.get(IKFK_chain['IK_pole']) 236 | 237 | invert = IKFK_chain['invert_switch'] 238 | 239 | ik_fk_layer = (IKFK_chain['FK_layer'],IKFK_chain['IK_layer']) 240 | 241 | 242 | for lock in ('lock_ik_x','lock_ik_y','lock_ik_z') : 243 | if getattr(IK_last,lock) : 244 | full_snapping = False 245 | break 246 | 247 | 248 | snap_ik_fk(ob,way,switch_prop,FK_root,FK_tip,IK_last,IK_tip,IK_pole,FK_mid,full_snapping,invert,ik_fk_layer,auto_switch=True) 249 | -------------------------------------------------------------------------------- /func_shape.py: -------------------------------------------------------------------------------- 1 | import bpy 2 | import bmesh 3 | from mathutils import Vector 4 | from bpy_extras import mesh_utils 5 | from .utils import bound_box_center,border_loops 6 | 7 | def store_ui_data(objects,canevas,rig): 8 | shapes_list = [] 9 | gamma = 1/2.2 10 | 11 | if rig.data.get('UI') : 12 | del rig.data['UI'] 13 | 14 | if canevas.type =='CURVE' : 15 | canevas_points = canevas.data.splines[0].points 16 | else : 17 | canevas_points = canevas.data.vertices 18 | 19 | world_canevas_points = [canevas.matrix_world * Vector((p.co[0],p.co[1],0)) for p in canevas_points] 20 | 21 | canevasX = [p[0] for p in world_canevas_points] 22 | canevasY = [p[1] for p in world_canevas_points] 23 | 24 | objects.append(canevas) 25 | 26 | #sorted by their z axes 27 | for ob in sorted(objects,key = lambda x : bound_box_center(x)[2]) : 28 | print(ob) 29 | data = ob.to_mesh(bpy.context.scene,True,'PREVIEW') 30 | 31 | bm = bmesh.new() 32 | bm.from_mesh(data) 33 | #bmesh.ops.beautify_fill(bm,faces = bm.faces,edges=bm.edges) 34 | bmesh.ops.remove_doubles(bm,verts=bm.verts,dist=0.002) 35 | bmesh.ops.dissolve_limit(bm, angle_limit=0.001745, verts=bm.verts, edges=bm.edges) 36 | 37 | vertex_count = [v.index for v in bm.verts] 38 | bm_loops = border_loops(bm,0,[],vertex_count) 39 | loops = [[l.index for l in L] for L in bm_loops] 40 | 41 | bmesh.ops.connect_verts_concave(bm, faces=bm.faces) 42 | bm.to_mesh(data) 43 | data.update() 44 | bm.clear() 45 | 46 | points = [] 47 | 48 | faces = [] 49 | 50 | for p in data.vertices : 51 | point = ob.matrix_world * Vector((p.co[0],p.co[1],0)) 52 | 53 | x = (point[0]-min(canevasX)) / (max(canevasX)-min(canevasX)) 54 | 55 | y = (point[1]-min(canevasY)) / (max(canevasY)-min(canevasY)) 56 | 57 | points.append([round(x,5),round(y,5)]) 58 | 59 | for f in data.polygons : 60 | faces.append([v for v in f.vertices]) 61 | 62 | try : 63 | colors = ob.data.materials[0].node_tree.nodes['Emission'].inputs['Color'].default_value 64 | color = [round(pow(colors[0],gamma),4),round(pow(colors[1],gamma),4),round(pow(colors[2],gamma),4)] 65 | except : 66 | color = [0.5,0.5,0.5] 67 | 68 | verts_x = [v[0] for v in points] 69 | verts_y = [v[1] for v in points] 70 | bound = [(min(verts_x),max(verts_y)),(max(verts_x),max(verts_y)),(max(verts_x),min(verts_y)),(min(verts_x),min(verts_y))] 71 | 72 | 73 | shape = {'tooltip':ob.UI.name,'points':points, 'faces':faces, 74 | 'loops' : loops,'bound' : bound,'color':color, 75 | 'shape_type': ob.UI.shape_type} 76 | 77 | if ob.UI.shape_type =='FUNCTION': 78 | shape['function'] = ob.UI.function 79 | if ob.UI.arguments : 80 | shape['variables'] = eval(ob.UI.arguments) 81 | if ob.UI.shortcut : 82 | shape['shortcut'] = ob.UI.shortcut 83 | 84 | elif ob.UI.shape_type =='BONE': 85 | shape['bone'] = ob.UI.name 86 | 87 | shapes_list.append(shape) 88 | 89 | print(shapes_list) 90 | 91 | rig.data.UI['shapes'] = shapes_list 92 | -------------------------------------------------------------------------------- /op_material.py: -------------------------------------------------------------------------------- 1 | import bpy 2 | from bpy_extras import view3d_utils 3 | 4 | class AddMat(bpy.types.Operator): 5 | bl_label = "Add Ui Material" 6 | bl_idname = "rigui.add_mat" 7 | #bl_options = {'REGISTER', 'UNDO'} 8 | 9 | def execute(self,context): 10 | scene = context.scene 11 | 12 | mat = bpy.data.materials.new('UI') 13 | mat.use_nodes = True 14 | 15 | for node in mat.node_tree.nodes : 16 | if node.type == 'OUTPUT_MATERIAL' : 17 | mat_output = node 18 | else : 19 | mat.node_tree.nodes.remove(node) 20 | 21 | emission = mat.node_tree.nodes.new('ShaderNodeEmission') 22 | mat.node_tree.links.new(emission.outputs[0],mat_output.inputs[0]) 23 | 24 | 25 | if not context.object.data.materials : 26 | context.object.data.materials.append(mat) 27 | else : 28 | context.object.material_slots[0].material = mat 29 | 30 | return {'FINISHED'} 31 | 32 | class RemoveMat(bpy.types.Operator): 33 | bl_label = "Remove Ui Material" 34 | bl_idname = "rigui.remove_mat" 35 | #bl_options = {'REGISTER', 'UNDO'} 36 | 37 | def execute(self,context): 38 | scene = context.scene 39 | #print(self.shape_type) 40 | for mat in context.object.data.materials : 41 | bpy.data.materials.remove(mat,True) 42 | context.area.tag_redraw() 43 | 44 | return {'FINISHED'} 45 | 46 | class EyeDropperMat(bpy.types.Operator): 47 | """Tooltip""" 48 | bl_idname = "rigui.eye_dropper_mat" 49 | bl_label = "Eye Dropper mat" 50 | #bl_options = {'REGISTER', 'UNDO'} 51 | 52 | #first_mouse_x = IntProperty() 53 | #first_value = FloatProperty() 54 | 55 | 56 | def modal(self, context, event): 57 | context.area.tag_redraw() 58 | 59 | context.window.cursor_modal_set("EYEDROPPER") 60 | 61 | scene = context.scene 62 | region = context.region 63 | rv3d = context.region_data 64 | 65 | 66 | if event.type == 'LEFTMOUSE' and event.value == 'RELEASE': 67 | self.mouse = event.mouse_region_x, event.mouse_region_y 68 | 69 | view_vector = view3d_utils.region_2d_to_vector_3d(region, rv3d, self.mouse) 70 | ray_origin = view3d_utils.region_2d_to_origin_3d(region, rv3d, self.mouse) 71 | 72 | raycast = scene.ray_cast(ray_origin,view_vector) 73 | 74 | if raycast[0]==True : 75 | ob = raycast[4] 76 | 77 | if ob.data.materials : 78 | mat = ob.data.materials[0] 79 | for shape in [o for o in context.selected_objects if o.type in ('MESH','CURVE','FONT')] : 80 | if not shape.data.materials : 81 | shape.data.materials.append(mat) 82 | else : 83 | shape.material_slots[0].material = mat 84 | 85 | 86 | #context.space_data.draw_handler_remove(self._handle, 'WINDOW') 87 | 88 | context.window.cursor_modal_restore() 89 | 90 | for ob in self.temp_ob : 91 | bpy.data.objects.remove(ob,True) 92 | 93 | return {'FINISHED'} 94 | 95 | 96 | #return {'FINISHED'} 97 | 98 | elif event.type in {'RIGHTMOUSE', 'ESC'}: 99 | #context.object.location.x = self.first_value 100 | context.window.cursor_modal_restore() 101 | 102 | for ob in self.temp_ob : 103 | bpy.data.objects.remove(ob,True) 104 | return {'CANCELLED'} 105 | 106 | return {'RUNNING_MODAL'} 107 | 108 | 109 | def invoke(self, context, event): 110 | scene = context.scene 111 | self.local_cursor = tuple(context.space_data.cursor_location) 112 | self.cursor = tuple(context.scene.cursor_location) 113 | 114 | curves =[o for o in context.visible_objects if o.type in ('CURVE','FONT')] 115 | 116 | self.temp_ob = [] 117 | 118 | for c in curves : 119 | mesh = c.to_mesh(bpy.context.scene,False,'PREVIEW') 120 | copy = bpy.data.objects.new(c.name+'_tmp',mesh) 121 | copy.matrix_world = c.matrix_world 122 | for mat in c.data.materials : 123 | copy.data.materials.append(mat) 124 | scene.objects.link(copy) 125 | self.temp_ob.append(copy) 126 | #args = (self,context) 127 | #self._handle = context.space_data.draw_handler_add(draw_callback_px, args, 'WINDOW', 'POST_PIXEL') 128 | context.window_manager.modal_handler_add(self) 129 | 130 | return {'RUNNING_MODAL'} 131 | -------------------------------------------------------------------------------- /op_picker.py: -------------------------------------------------------------------------------- 1 | import bpy 2 | 3 | from .func_bgl import draw_callback_px 4 | from .func_bgl import select_bone 5 | from .func_picker import * 6 | from .utils import is_over_region 7 | 8 | 9 | class FunctionExecute(bpy.types.Operator) : 10 | bl_idname = "rigui.function_execute" 11 | bl_label = 'Function Execute' 12 | 13 | shape_index = bpy.props.IntProperty() 14 | 15 | def execute(self,context) : 16 | event = self.event 17 | ob = context.object 18 | shape = ob.data.UI['shapes'][self.shape_index] 19 | 20 | function = shape['function'] 21 | if shape.get('variables') : 22 | variables=shape['variables'].to_dict() 23 | 24 | 25 | else : 26 | variables={} 27 | 28 | variables['event']=event 29 | globals()[function](variables) 30 | 31 | return {'FINISHED'} 32 | 33 | def invoke(self,context,event) : 34 | self.event = event 35 | return self.execute(context) 36 | 37 | 38 | class UIDraw(bpy.types.Operator): 39 | bl_idname = "rigui.ui_draw" 40 | bl_label = "Rig UI Draw" 41 | 42 | _handle = None 43 | tmp_ob = None 44 | tmp_bones = [] 45 | start = (0,0) 46 | end = (0,0) 47 | border = ((0,0),(0,0),(0,0),(0,0)) 48 | is_border = False 49 | press = False 50 | scale = 1 51 | offset = 0 52 | outside_point = (-1,-1) 53 | addon_keymaps = [] 54 | 55 | def set_shorcut(self,context) : 56 | ob = context.object 57 | addon = bpy.context.window_manager.keyconfigs.addon 58 | 59 | if ob and ob.type =='ARMATURE' and ob.data.UI and addon: 60 | for i,shape in [(i,s) for i,s in enumerate(ob.data.UI['shapes']) if s.get('function') and s.get('shortcut')] : 61 | km = addon.keymaps.new(name = 'Image Generic', space_type = 'IMAGE_EDITOR',region_type = 'WINDOW') 62 | 63 | split = shape["shortcut"].split(' ') 64 | if len(split)==1 : 65 | shortcut = shape["shortcut"].upper() 66 | modifier = None 67 | else : 68 | shortcut = split[1].upper() 69 | modifier = split[0].lower() 70 | 71 | kmi = km.keymap_items.new("rigui.function_execute", type = shortcut, value = "CLICK") 72 | kmi.properties.shape_index = i 73 | 74 | if modifier : 75 | setattr(kmi,modifier,True) 76 | 77 | self.addon_keymaps.append(km) 78 | 79 | def remove_shorcut(self,context) : 80 | # Remove Shortcut 81 | wm = bpy.context.window_manager 82 | for km in self.addon_keymaps: 83 | for kmi in km.keymap_items: 84 | km.keymap_items.remove(kmi) 85 | 86 | self.addon_keymaps.clear() 87 | 88 | def modal(self, context, event): 89 | inside = is_over_region(self,context,event) 90 | 91 | if context.object and context.object.type == 'ARMATURE' and context.area : 92 | if not context.screen.is_animation_playing : 93 | if self.tmp_ob != context.object : 94 | context.area.tag_redraw() 95 | self.remove_shorcut(context) 96 | self.set_shorcut(context) 97 | self.tmp_ob = context.object 98 | 99 | if self.tmp_bones != context.selected_pose_bones : 100 | context.area.tag_redraw() 101 | self.tmp_bones = context.selected_pose_bones 102 | 103 | if inside : 104 | context.area.tag_redraw() 105 | 106 | if event.type == 'LEFTMOUSE' : 107 | if event.value == 'PRESS': # start selection 108 | if inside: 109 | self.start = (event.mouse_region_x,event.mouse_region_y) 110 | self.press = True 111 | 112 | elif event.value == 'RELEASE' and self.press: 113 | self.end = (event.mouse_region_x,event.mouse_region_y) 114 | 115 | select_bone(self,context,event) 116 | bpy.ops.ed.undo_push() 117 | 118 | self.is_border= False 119 | self.press = False 120 | 121 | if event.type == 'MOUSEMOVE' : 122 | self.end = (event.mouse_region_x,event.mouse_region_y) 123 | 124 | if self.press : 125 | b_x = (min(self.start[0],self.end[0]),max(self.start[0],self.end[0])) 126 | b_y = (min(self.start[1],self.end[1]),max(self.start[1],self.end[1])) 127 | self.border = ((b_x[0],b_y[1]),(b_x[1],b_y[1]),(b_x[1],b_y[0]),(b_x[0],b_y[0])) 128 | self.is_border= True if (b_x[1]-b_x[0])+(b_y[1]-b_y[0]) > 4 else False 129 | 130 | if self.is_border : 131 | select_bone(self,context,event) 132 | 133 | elif event.type in {'ESC',} and inside: 134 | bpy.types.SpaceImageEditor.draw_handler_remove(self._handle, 'WINDOW') 135 | self.remove_shorcut(context) 136 | 137 | 138 | return {'CANCELLED'} 139 | 140 | return {'PASS_THROUGH'} 141 | 142 | def invoke(self, context, event): 143 | #shortcut Creation 144 | 145 | 146 | 147 | context.space_data.image = None 148 | self.adress = context.area.as_pointer() 149 | args = (self, context) 150 | 151 | self._handle = bpy.types.SpaceImageEditor.draw_handler_add(draw_callback_px, args, 'WINDOW', 'POST_PIXEL') 152 | 153 | 154 | context.window_manager.modal_handler_add(self) 155 | 156 | return {'RUNNING_MODAL'} 157 | -------------------------------------------------------------------------------- /op_shape.py: -------------------------------------------------------------------------------- 1 | import bpy 2 | from .func_shape import store_ui_data 3 | from .utils import is_shape,find_mirror 4 | 5 | class CreateShape(bpy.types.Operator): 6 | bl_label = "Create UI shape" 7 | bl_idname = "rigui.create_shape" 8 | #bl_options = {'REGISTER', 'UNDO'} 9 | 10 | def execute(self,context): 11 | scene = context.scene 12 | 13 | offset = 0 14 | for bone in context.selected_pose_bones : 15 | name = bone.name 16 | 17 | mesh = bpy.data.meshes.new(name) 18 | ob = bpy.data.objects.new(name,mesh) 19 | 20 | ob.UI.name = name 21 | 22 | verts=[(0,0,0)] 23 | edges = [] 24 | faces =[] 25 | 26 | mesh.from_pydata(verts, edges, faces) 27 | 28 | scene.objects.link(ob) 29 | 30 | if scene.UI.canevas : 31 | ob.layers = scene.UI.canevas.layers 32 | else : 33 | layers = [False]*20 34 | layers[8] = True 35 | ob.layers = layers 36 | 37 | scene.cursor_location = (0,0,0.05) 38 | ob.location[2]=0.05 39 | ob.location[0]= offset 40 | 41 | for obj in scene.objects : 42 | obj.select = False 43 | 44 | scene.objects.active = ob 45 | ob.select = True 46 | 47 | offset += 0.05 48 | 49 | return {'FINISHED'} 50 | 51 | 52 | class NameFromBone(bpy.types.Operator): 53 | bl_label = "Name Shape from selected bones" 54 | bl_idname = "rigui.name_from_bone" 55 | #bl_options = {'REGISTER', 'UNDO'} 56 | 57 | def execute(self,context): 58 | scene = context.scene 59 | rig = scene.UI.rig 60 | bone = rig.data.bones.active 61 | 62 | if bone : 63 | context.object.name = bone.name 64 | context.object.UI.name = bone.name 65 | 66 | return {'FINISHED'} 67 | 68 | 69 | class MirrorShape(bpy.types.Operator): 70 | bl_label = "Mirror UI shape" 71 | bl_idname = "rigui.mirror_shape" 72 | #bl_options = {'REGISTER', 'UNDO'} 73 | 74 | def execute(self,context): 75 | scene = context.scene 76 | 77 | for ob in bpy.context.selected_objects : 78 | 79 | name = find_mirror(ob.name) 80 | 81 | if not name : 82 | name = ob.name+'_flip' 83 | 84 | old_shape = bpy.data.objects.get(name) 85 | if old_shape: 86 | bpy.data.objects.remove(old_shape,True) 87 | 88 | mirrorShape = ob.copy() 89 | mirrorShape.data = ob.data.copy() 90 | mirrorShape.name = name 91 | #mirrorShape = bpy.data.objects.new(name,ob.data.copy()) 92 | 93 | scene.objects.link(mirrorShape) 94 | mirrorShape.layers = ob.layers 95 | 96 | if scene.UI.symmetry : 97 | symmetry_loc = scene.UI.symmetry.matrix_world.to_translation()[0] 98 | else : 99 | symmetry_loc = 0 100 | 101 | #print(symmetry_loc) 102 | mirrorShape.matrix_world = ob.matrix_world 103 | 104 | #if mirrorShape.location[0] < symmetry_loc : 105 | mirrorShape.location[0]= symmetry_loc+ (symmetry_loc- ob.location[0]) 106 | #else : 107 | # mirrorShape.location[0]= symmetry_loc+ (symmetry_loc- ob.location[0]) 108 | 109 | mirrorShape.rotation_euler[1] = -ob.rotation_euler[1] 110 | mirrorShape.rotation_euler[2] = -ob.rotation_euler[2] 111 | 112 | mirrorShape.scale[0] = -ob.scale[0] 113 | 114 | 115 | for key,value in ob.items() : 116 | if key not in ["_RNA_UI",'cycles']: 117 | mirrorShape[key] = value 118 | 119 | if ob.UI.shape_type == 'BONE' : 120 | mirrorShape.UI.name = find_mirror(ob.UI.name) 121 | 122 | elif ob.UI.shape_type == 'FUNCTION' : 123 | args = {} 124 | for key,value in eval(ob.UI.arguments).items() : 125 | if type(value) == list : 126 | mirrored_value = [] 127 | for item in value : 128 | mirrored_value.append(find_mirror(item)) 129 | 130 | elif type(value) == str : 131 | mirrored_value = find_mirror(value) 132 | args[key] = mirrored_value 133 | 134 | mirrorShape.UI.arguments = str(args) 135 | 136 | return {'FINISHED'} 137 | 138 | class SelectShapeType(bpy.types.Operator): 139 | bl_label = "Select Shape by Type" 140 | bl_idname = "rigui.select_shape_type" 141 | #bl_options = {'REGISTER', 'UNDO'} 142 | 143 | shape_type = bpy.props.EnumProperty(items =[(i.upper(),i,"") for i in ('Bone','Display','Function')]) 144 | 145 | def draw(self,context) : 146 | layout = self.layout 147 | 148 | col = layout.column() 149 | col.prop(self,'shape_type',expand = True) 150 | 151 | def execute(self,context): 152 | scene = context.scene 153 | #print(self.shape_type) 154 | canevas = context.scene.UI.canevas 155 | if canevas : 156 | for ob in [o for o in bpy.data.objects if o.layers==canevas.layers] : 157 | if ob.type in ['MESH','CURVE','FONT'] and ob.UI.shape_type == self.shape_type : 158 | ob.select = True 159 | 160 | return {'FINISHED'} 161 | 162 | def invoke(self, context, event): 163 | wm = context.window_manager 164 | return wm.invoke_props_dialog(self,width=150) 165 | 166 | class StoreUIData(bpy.types.Operator): 167 | bl_label = "Store UI Data" 168 | bl_idname = "rigui.store_ui_data" 169 | 170 | def execute(self,context): 171 | scene = context.scene 172 | canevas= scene.UI.canevas 173 | rig = scene.UI.rig 174 | shapes = [o for o in scene.objects if o != canevas and is_shape(o)] 175 | 176 | if rig.type == 'ARMATURE' and canevas: 177 | store_ui_data(shapes,canevas,rig) 178 | 179 | else : 180 | self.report({'INFO'},'active object not rig or canevas not found') 181 | 182 | return {'FINISHED'} 183 | -------------------------------------------------------------------------------- /panels.py: -------------------------------------------------------------------------------- 1 | import bpy 2 | #import collections 3 | import inspect 4 | from . import func_picker as mod 5 | 6 | class PickerMakerPanel(bpy.types.Panel): 7 | bl_label = "Rig UI" 8 | bl_category = "RIG Tools" 9 | bl_space_type = 'VIEW_3D' 10 | bl_region_type = 'TOOLS' 11 | 12 | def draw(self, context): 13 | ob = context.object 14 | scene = context.scene 15 | 16 | layout = self.layout 17 | col = layout.column(align=False) 18 | col.prop_search(context.scene.UI,'rig',context.scene,'objects',text = 'Rig ') 19 | col.prop_search(context.scene.UI,'canevas',context.scene,'objects',text = 'Canevas ') 20 | col.prop_search(context.scene.UI,'symmetry',context.scene,'objects',text = 'Symmetry ') 21 | 22 | 23 | col = layout.column(align=True) 24 | row = col.row(align=True) 25 | row.operator("rigui.create_shape",icon = 'MESH_DATA',text = 'Create shape') 26 | row.operator("rigui.mirror_shape",icon = 'ARROW_LEFTRIGHT',text = 'Mirror shape') 27 | col.operator("rigui.name_from_bone",icon = 'SORTALPHA' ,text = 'Name from bones') 28 | 29 | row = layout.row(align=True) 30 | 31 | row.operator("rigui.store_ui_data", icon = 'PASTEDOWN',text = 'Store shape') 32 | 33 | row = layout.row(align=True) 34 | 35 | if ob : 36 | col = layout.column(align=False) 37 | 38 | material_row = col.row(align = True) 39 | material_row.operator("rigui.remove_mat",icon='ZOOMOUT',text='') 40 | material_row.operator("rigui.add_mat",icon='ZOOMIN',text='') 41 | mat = False 42 | if ob.type in ('MESH','CURVE','FONT') and ob.data.materials : 43 | mat = ob.data.materials[0] 44 | if mat and mat.node_tree: 45 | emission_nodes = [n for n in mat.node_tree.nodes if n.type =='EMISSION'] 46 | if emission_nodes : 47 | material_row.prop(emission_nodes[0].inputs[0],'default_value',text='') 48 | mat = True 49 | if not mat : 50 | material_row.label('No Material') 51 | material_row.operator("rigui.eye_dropper_mat",icon='EYEDROPPER',text='') 52 | 53 | shape_type_row = col.row(align = True) 54 | shape_type_row.prop(ob.UI,'shape_type',expand = True) 55 | shape_type_row.operator('rigui.select_shape_type',text='',icon = 'RESTRICT_SELECT_OFF') 56 | 57 | 58 | if ob.UI.shape_type == 'FUNCTION' : 59 | col.prop(ob.UI,'name',text='Tooltip') 60 | function_row = col.row(align = True) 61 | function_row.prop(ob.UI,'function',text='Function') 62 | function_row.operator("rigui.function_selector",text='',icon='COLLAPSEMENU') 63 | if ob.UI.function : 64 | col.label("Arguments : (%s)"%inspect.getdoc(getattr(mod,ob.UI.function))) 65 | col.prop(ob.UI,'arguments',text='') 66 | col.prop(ob.UI,'shortcut',text='Shortcut') 67 | if ob.UI.shape_type == 'BONE' : 68 | if scene.UI.rig : 69 | col.prop_search(ob.UI,'name',scene.UI.rig.pose,'bones',text='Bone') 70 | -------------------------------------------------------------------------------- /properties.py: -------------------------------------------------------------------------------- 1 | import bpy 2 | import inspect 3 | 4 | def functions_item(self,context): 5 | items = [] 6 | from . import func_picker as mod 7 | 8 | for name,func in inspect.getmembers(mod, inspect.isfunction) : 9 | if inspect.getmodule(func) == mod : 10 | items.append((name,name,"")) 11 | 12 | return items 13 | 14 | def bones_item(self,context) : 15 | items = [] 16 | 17 | if context.scene.UI.rig : 18 | for bone in context.scene.UI.rig.pose.bones : 19 | items.append((bone.name,bone.name,'')) 20 | return items 21 | 22 | class ObjectUISettings(bpy.types.PropertyGroup) : 23 | shape_type = bpy.props.EnumProperty(items = [(i.upper(),i,"") for i in ('Bone','Display','Function')]) 24 | function = bpy.props.StringProperty() 25 | arguments = bpy.props.StringProperty() 26 | shortcut = bpy.props.StringProperty() 27 | name = bpy.props.StringProperty() 28 | 29 | 30 | class SceneUISettings(bpy.types.PropertyGroup) : 31 | rig = bpy.props.PointerProperty(type=bpy.types.Object) 32 | canevas = bpy.props.PointerProperty(type=bpy.types.Object) 33 | symmetry = bpy.props.PointerProperty(type=bpy.types.Object) 34 | functions = bpy.props.EnumProperty(items = functions_item) 35 | #bone_list = bpy.props.EnumProperty(items = bones_item) 36 | 37 | class FunctionSelector(bpy.types.Operator): 38 | bl_label = "Select function" 39 | bl_idname = "rigui.function_selector" 40 | bl_property = "functions" 41 | #bl_options = {'REGISTER', 'UNDO'} 42 | 43 | functions = SceneUISettings.functions 44 | 45 | def execute(self, context): 46 | ob = context.object 47 | ob['UI']['function'] = self.functions 48 | context.area.tag_redraw() 49 | return {'FINISHED'} 50 | 51 | def invoke(self, context, event): 52 | wm = context.window_manager 53 | wm.invoke_search_popup(self) 54 | return {'FINISHED'} 55 | -------------------------------------------------------------------------------- /snapping_utils.py: -------------------------------------------------------------------------------- 1 | import bpy 2 | from mathutils import Vector,Matrix 3 | from math import acos, pi 4 | #from .insert_keyframe import insert_keyframe 5 | 6 | ############################ 7 | ## Math utility functions ## 8 | ############################ 9 | 10 | def perpendicular_vector(v): 11 | """ Returns a vector that is perpendicular to the one given. 12 | The returned vector is _not_ guaranteed to be normalized. 13 | """ 14 | # Create a vector that is not aligned with v. 15 | # It doesn't matter what vector. Just any vector 16 | # that's guaranteed to not be pointing in the same 17 | # direction. 18 | if abs(v[0]) < abs(v[1]): 19 | tv = Vector((1,0,0)) 20 | else: 21 | tv = Vector((0,1,0)) 22 | 23 | # Use cross prouct to generate a vector perpendicular to 24 | # both tv and (more importantly) v. 25 | return v.cross(tv) 26 | 27 | 28 | def rotation_difference(mat1, mat2): 29 | """ Returns the shortest-path rotational difference between two 30 | matrices. 31 | """ 32 | q1 = mat1.to_quaternion() 33 | q2 = mat2.to_quaternion() 34 | angle = acos(min(1,max(-1,q1.dot(q2)))) * 2 35 | if angle > pi: 36 | angle = -angle + (2*pi) 37 | return angle 38 | 39 | 40 | ######################################### 41 | ## "Visual Transform" helper functions ## 42 | ######################################### 43 | 44 | def get_pose_matrix_in_other_space(mat, pose_bone): 45 | """ Returns the transform matrix relative to pose_bone's current 46 | transform space. In other words, presuming that mat is in 47 | armature space, slapping the returned matrix onto pose_bone 48 | should give it the armature-space transforms of mat. 49 | TODO: try to handle cases with axis-scaled parents better. 50 | """ 51 | rest = pose_bone.bone.matrix_local.copy() 52 | rest_inv = rest.inverted() 53 | if pose_bone.parent: 54 | par_mat = pose_bone.parent.matrix.copy() 55 | par_inv = par_mat.inverted() 56 | par_rest = pose_bone.parent.bone.matrix_local.copy() 57 | else: 58 | par_mat = Matrix() 59 | par_inv = Matrix() 60 | par_rest = Matrix() 61 | 62 | # Get matrix in bone's current transform space 63 | smat = rest_inv * (par_rest * (par_inv * mat)) 64 | 65 | # Compensate for non-local location 66 | #if not pose_bone.bone.use_local_location: 67 | # loc = smat.to_translation() * (par_rest.inverted() * rest).to_quaternion() 68 | # smat.translation = loc 69 | 70 | return smat 71 | 72 | 73 | def get_local_pose_matrix(pose_bone): 74 | """ Returns the local transform matrix of the given pose bone. 75 | """ 76 | return get_pose_matrix_in_other_space(pose_bone.matrix, pose_bone) 77 | 78 | 79 | def set_pose_translation(pose_bone, mat): 80 | """ Sets the pose bone's translation to the same translation as the given matrix. 81 | Matrix should be given in bone's local space. 82 | """ 83 | if pose_bone.bone.use_local_location is True: 84 | pose_bone.location = mat.to_translation() 85 | else: 86 | loc = mat.to_translation() 87 | 88 | rest = pose_bone.bone.matrix_local.copy() 89 | if pose_bone.bone.parent: 90 | par_rest = pose_bone.bone.parent.matrix_local.copy() 91 | else: 92 | par_rest = Matrix() 93 | 94 | q = (par_rest.inverted() * rest).to_quaternion() 95 | pose_bone.location = q * loc 96 | 97 | 98 | def set_pose_rotation(pose_bone, mat): 99 | """ Sets the pose bone's rotation to the same rotation as the given matrix. 100 | Matrix should be given in bone's local space. 101 | """ 102 | q = mat.to_quaternion() 103 | 104 | if pose_bone.rotation_mode == 'QUATERNION': 105 | pose_bone.rotation_quaternion = q 106 | elif pose_bone.rotation_mode == 'AXIS_ANGLE': 107 | pose_bone.rotation_axis_angle[0] = q.angle 108 | pose_bone.rotation_axis_angle[1] = q.axis[0] 109 | pose_bone.rotation_axis_angle[2] = q.axis[1] 110 | pose_bone.rotation_axis_angle[3] = q.axis[2] 111 | else: 112 | pose_bone.rotation_euler = q.to_euler(pose_bone.rotation_mode) 113 | 114 | 115 | def set_pose_scale(pose_bone, mat): 116 | """ Sets the pose bone's scale to the same scale as the given matrix. 117 | Matrix should be given in bone's local space. 118 | """ 119 | pose_bone.scale = mat.to_scale() 120 | 121 | 122 | def match_pose_translation(pose_bone, target_bone): 123 | """ Matches pose_bone's visual translation to target_bone's visual 124 | translation. 125 | This function assumes you are in pose mode on the relevant armature. 126 | """ 127 | mat = get_pose_matrix_in_other_space(target_bone.matrix, pose_bone) 128 | set_pose_translation(pose_bone, mat) 129 | bpy.ops.object.mode_set(mode='OBJECT') 130 | bpy.ops.object.mode_set(mode='POSE') 131 | 132 | 133 | def match_pose_rotation(pose_bone, target_bone): 134 | """ Matches pose_bone's visual rotation to target_bone's visual 135 | rotation. 136 | This function assumes you are in pose mode on the relevant armature. 137 | """ 138 | mat = get_pose_matrix_in_other_space(target_bone.matrix, pose_bone) 139 | set_pose_rotation(pose_bone, mat) 140 | bpy.ops.object.mode_set(mode='OBJECT') 141 | bpy.ops.object.mode_set(mode='POSE') 142 | 143 | 144 | def match_pose_scale(pose_bone, target_bone): 145 | """ Matches pose_bone's visual scale to target_bone's visual 146 | scale. 147 | This function assumes you are in pose mode on the relevant armature. 148 | """ 149 | mat = get_pose_matrix_in_other_space(target_bone.matrix, pose_bone) 150 | set_pose_scale(pose_bone, mat) 151 | bpy.ops.object.mode_set(mode='OBJECT') 152 | bpy.ops.object.mode_set(mode='POSE') 153 | 154 | 155 | ############################## 156 | ## IK/FK snapping functions ## 157 | ############################## 158 | 159 | def match_pole_target(ik_first, ik_last, pole, match_bone, length): 160 | """ Places an IK chain's pole target to match ik_first's 161 | transforms to match_bone. All bones should be given as pose bones. 162 | You need to be in pose mode on the relevant armature object. 163 | ik_first: first bone in the IK chain 164 | ik_last: last bone in the IK chain 165 | pole: pole target bone for the IK chain 166 | match_bone: bone to match ik_first to (probably first bone in a matching FK chain) 167 | length: distance pole target should be placed from the chain center 168 | """ 169 | a = ik_first.matrix.to_translation() 170 | b = ik_last.matrix.to_translation() + ik_last.vector 171 | 172 | # Vector from the head of ik_first to the 173 | # tip of ik_last 174 | ikv = b - a 175 | 176 | # Get a vector perpendicular to ikv 177 | pv = perpendicular_vector(ikv).normalized() * length 178 | 179 | def set_pole(pvi): 180 | """ Set pole target's position based on a vector 181 | from the arm center line. 182 | """ 183 | # Translate pvi into armature space 184 | ploc = a + (ikv/2) + pvi 185 | 186 | # Set pole target to location 187 | mat = get_pose_matrix_in_other_space(Matrix.Translation(ploc), pole) 188 | set_pose_translation(pole, mat) 189 | 190 | bpy.ops.object.mode_set(mode='OBJECT') 191 | bpy.ops.object.mode_set(mode='POSE') 192 | 193 | set_pole(pv) 194 | 195 | # Get the rotation difference between ik_first and match_bone 196 | angle = rotation_difference(ik_first.matrix, match_bone.matrix) 197 | 198 | # Try compensating for the rotation difference in both directions 199 | pv1 = Matrix.Rotation(angle, 4, ikv) * pv 200 | set_pole(pv1) 201 | ang1 = rotation_difference(ik_first.matrix, match_bone.matrix) 202 | 203 | pv2 = Matrix.Rotation(-angle, 4, ikv) * pv 204 | set_pole(pv2) 205 | ang2 = rotation_difference(ik_first.matrix, match_bone.matrix) 206 | 207 | # Do the one with the smaller angle 208 | if ang1 < ang2: 209 | set_pole(pv1) 210 | -------------------------------------------------------------------------------- /utils.py: -------------------------------------------------------------------------------- 1 | import bpy 2 | from mathutils import Vector 3 | from mathutils.geometry import intersect_line_line_2d 4 | 5 | def canevas_space(point,scale,offset) : 6 | return scale*Vector(point)+Vector(offset) 7 | 8 | 9 | def intersect_rectangles(bound, border): # returns None if rectangles don't intersect 10 | dx = min(border[1][0],bound[1][0]) - max(border[0][0],bound[0][0]) 11 | dy = min(border[0][1],bound[0][1]) - max(border[2][1],bound[2][1]) 12 | 13 | if (dx>=0) and (dy>=0): 14 | return dx*dy 15 | 16 | def point_inside_rectangle(point, rect): 17 | return rect[0][0]< point[0]< rect[1][0] and rect[2][1]< point[1]< rect[0][1] 18 | 19 | def point_over_shape(point,verts,loops,outside_point=(-1,-1)) : 20 | out = Vector(outside_point) 21 | pt = Vector(point) 22 | 23 | intersections = 0 24 | for loop in loops : 25 | for i,p in enumerate(loop) : 26 | a = Vector(verts[loop[i-1]]) 27 | b = Vector(verts[p]) 28 | 29 | if intersect_line_line_2d(pt,out,a,b): 30 | intersections += 1 31 | 32 | if intersections%2 == 1 : #chek if the nb of intersection is odd 33 | return True 34 | 35 | 36 | def border_over_shape(border,verts,loops) : 37 | for loop in loops : 38 | for i,p in enumerate(loop) : 39 | a = Vector(verts[loop[i-1]]) 40 | b = Vector(verts[p]) 41 | 42 | for j in range(0,4): 43 | c = border[j-1] 44 | d = border[j] 45 | if intersect_line_line_2d(a,b,c,d): 46 | return True 47 | 48 | for point in verts : 49 | if point_inside_rectangle(point,border) : 50 | return True 51 | 52 | for point in border : 53 | if point_over_shape(point,verts,loops) : 54 | return True 55 | 56 | 57 | 58 | def border_loop(vert,loop) : 59 | border_edge =[e for e in vert.link_edges if e.is_boundary or e.is_wire] 60 | 61 | if border_edge : 62 | for edge in border_edge : 63 | other_vert = edge.other_vert(vert) 64 | 65 | if not other_vert in loop : 66 | loop.append(other_vert) 67 | border_loop(other_vert,loop) 68 | 69 | return loop 70 | else : 71 | return [vert] 72 | 73 | 74 | def border_loops(bm,vert_index,loops,vertex_count): 75 | bm.verts.ensure_lookup_table() 76 | 77 | loop = border_loop(bm.verts[vert_index],[bm.verts[vert_index]]) 78 | if len(loop) >1 : 79 | loops.append(loop) 80 | 81 | for v in loop : 82 | vertex_count.remove(v.index) 83 | 84 | if len(vertex_count) : 85 | border_loops(bm,vertex_count[0],loops,vertex_count) 86 | return loops 87 | 88 | 89 | def get_IK_bones(IK_last): 90 | ik_chain = IK_last.parent_recursive 91 | ik_len = 0 92 | 93 | #Get IK len : 94 | for c in IK_last.constraints : 95 | if c.type == 'IK': 96 | ik_len = c.chain_count -2 97 | break 98 | 99 | IK_root = ik_chain[ik_len] 100 | 101 | IK_mid= ik_chain[:ik_len] 102 | 103 | IK_mid.reverse() 104 | IK_mid.append(IK_last) 105 | 106 | return IK_root,IK_mid 107 | 108 | def find_mirror(name) : 109 | mirror = None 110 | prop= False 111 | 112 | if name : 113 | 114 | if name.startswith('[')and name.endswith(']'): 115 | prop = True 116 | name= name[:-2][2:] 117 | 118 | match={ 119 | 'R' : 'L', 120 | 'r' : 'l', 121 | 'L' : 'R', 122 | 'l' : 'r', 123 | } 124 | 125 | separator=['.','_'] 126 | 127 | if name.startswith(tuple(match.keys())): 128 | if name[1] in separator : 129 | mirror = match[name[0]]+name[1:] 130 | 131 | if name.endswith(tuple(match.keys())): 132 | if name[-2] in separator : 133 | mirror = name[:-1]+match[name[-1]] 134 | 135 | if mirror and prop == True: 136 | mirror='["%s"]'%mirror 137 | 138 | return mirror 139 | 140 | else : 141 | return None 142 | 143 | 144 | def is_shape(ob) : 145 | shape = False 146 | if ob.type in('MESH','CURVE','FONT') : 147 | if ob.UI.shape_type == 'BONE' : 148 | if ob.UI.name : 149 | shape = True 150 | else : 151 | shape = True 152 | return shape 153 | 154 | def is_over_region(self,context,event) : 155 | inside = 2 < event.mouse_region_x < context.region.width -2 and \ 156 | 2 < event.mouse_region_y < context.region.height -2 and \ 157 | [a for a in context.screen.areas if a.as_pointer()==self.adress] and \ 158 | not context.screen.show_fullscreen 159 | 160 | return inside 161 | 162 | def bound_box_center(ob) : 163 | points = [ob.matrix_world*Vector(p) for p in ob.bound_box] 164 | 165 | x = [v[0] for v in points] 166 | y = [v[1] for v in points] 167 | z = [v[2] for v in points] 168 | 169 | return (sum(x) / len(points), sum(y) / len(points),sum(z) / len(points)) 170 | --------------------------------------------------------------------------------