1# ##### BEGIN GPL LICENSE BLOCK ##### 2# 3# This program is free software; you can redistribute it and/or 4# modify it under the terms of the GNU General Public License 5# as published by the Free Software Foundation; either version 2 6# of the License, or (at your option) any later version. 7# 8# This program is distributed in the hope that it will be useful, 9# but WITHOUT ANY WARRANTY; without even the implied warranty of 10# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 11# GNU General Public License for more details. 12# 13# You should have received a copy of the GNU General Public License 14# along with this program; if not, write to the Free Software Foundation, 15# Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. 16# 17# ##### END GPL LICENSE BLOCK ##### 18 19# <pep8-80 compliant> 20 21import bpy 22from bpy.types import Operator 23from bpy.props import ( 24 BoolProperty, 25 EnumProperty, 26 IntProperty, 27 StringProperty, 28) 29 30 31class SelectPattern(Operator): 32 """Select objects matching a naming pattern""" 33 bl_idname = "object.select_pattern" 34 bl_label = "Select Pattern" 35 bl_options = {'REGISTER', 'UNDO'} 36 37 pattern: StringProperty( 38 name="Pattern", 39 description="Name filter using '*', '?' and " 40 "'[abc]' unix style wildcards", 41 maxlen=64, 42 default="*", 43 ) 44 case_sensitive: BoolProperty( 45 name="Case Sensitive", 46 description="Do a case sensitive compare", 47 default=False, 48 ) 49 extend: BoolProperty( 50 name="Extend", 51 description="Extend the existing selection", 52 default=True, 53 ) 54 55 def execute(self, context): 56 57 import fnmatch 58 59 if self.case_sensitive: 60 pattern_match = fnmatch.fnmatchcase 61 else: 62 pattern_match = (lambda a, b: 63 fnmatch.fnmatchcase(a.upper(), b.upper())) 64 is_ebone = False 65 is_pbone = False 66 obj = context.object 67 if obj and obj.mode == 'POSE': 68 items = obj.data.bones 69 if not self.extend: 70 bpy.ops.pose.select_all(action='DESELECT') 71 is_pbone = True 72 elif obj and obj.type == 'ARMATURE' and obj.mode == 'EDIT': 73 items = obj.data.edit_bones 74 if not self.extend: 75 bpy.ops.armature.select_all(action='DESELECT') 76 is_ebone = True 77 else: 78 items = context.visible_objects 79 if not self.extend: 80 bpy.ops.object.select_all(action='DESELECT') 81 82 # Can be pose bones, edit bones or objects 83 for item in items: 84 if pattern_match(item.name, self.pattern): 85 86 # hrmf, perhaps there should be a utility function for this. 87 if is_ebone: 88 item.select = True 89 item.select_head = True 90 item.select_tail = True 91 if item.use_connect: 92 item_parent = item.parent 93 if item_parent is not None: 94 item_parent.select_tail = True 95 elif is_pbone: 96 item.select = True 97 else: 98 item.select_set(True) 99 100 return {'FINISHED'} 101 102 def invoke(self, context, event): 103 wm = context.window_manager 104 return wm.invoke_props_popup(self, event) 105 106 def draw(self, _context): 107 layout = self.layout 108 109 layout.prop(self, "pattern") 110 row = layout.row() 111 row.prop(self, "case_sensitive") 112 row.prop(self, "extend") 113 114 @classmethod 115 def poll(cls, context): 116 obj = context.object 117 return (not obj) or (obj.mode == 'OBJECT') or (obj.type == 'ARMATURE') 118 119 120class SelectCamera(Operator): 121 """Select the active camera""" 122 bl_idname = "object.select_camera" 123 bl_label = "Select Camera" 124 bl_options = {'REGISTER', 'UNDO'} 125 126 extend: BoolProperty( 127 name="Extend", 128 description="Extend the selection", 129 default=False, 130 ) 131 132 def execute(self, context): 133 scene = context.scene 134 view_layer = context.view_layer 135 view = context.space_data 136 if view.type == 'VIEW_3D' and view.use_local_camera: 137 camera = view.camera 138 else: 139 camera = scene.camera 140 141 if camera is None: 142 self.report({'WARNING'}, "No camera found") 143 elif camera.name not in scene.objects: 144 self.report({'WARNING'}, "Active camera is not in this scene") 145 else: 146 if not self.extend: 147 bpy.ops.object.select_all(action='DESELECT') 148 view_layer.objects.active = camera 149 # camera.hide = False # XXX TODO where is this now? 150 camera.select_set(True) 151 return {'FINISHED'} 152 153 return {'CANCELLED'} 154 155 156class SelectHierarchy(Operator): 157 """Select object relative to the active object's position """ \ 158 """in the hierarchy""" 159 bl_idname = "object.select_hierarchy" 160 bl_label = "Select Hierarchy" 161 bl_options = {'REGISTER', 'UNDO'} 162 163 direction: EnumProperty( 164 items=( 165 ('PARENT', "Parent", ""), 166 ('CHILD', "Child", ""), 167 ), 168 name="Direction", 169 description="Direction to select in the hierarchy", 170 default='PARENT', 171 ) 172 extend: BoolProperty( 173 name="Extend", 174 description="Extend the existing selection", 175 default=False, 176 ) 177 178 @classmethod 179 def poll(cls, context): 180 return context.object 181 182 def execute(self, context): 183 view_layer = context.view_layer 184 select_new = [] 185 act_new = None 186 187 selected_objects = context.selected_objects 188 obj_act = context.object 189 190 if context.object not in selected_objects: 191 selected_objects.append(context.object) 192 193 if self.direction == 'PARENT': 194 for obj in selected_objects: 195 parent = obj.parent 196 197 if parent and parent.visible_get(): 198 if obj_act == obj: 199 act_new = parent 200 201 select_new.append(parent) 202 203 else: 204 for obj in selected_objects: 205 select_new.extend([child for child in obj.children if child.visible_get()]) 206 207 if select_new: 208 select_new.sort(key=lambda obj_iter: obj_iter.name) 209 act_new = select_new[0] 210 211 # don't edit any object settings above this 212 if select_new: 213 if not self.extend: 214 bpy.ops.object.select_all(action='DESELECT') 215 216 for obj in select_new: 217 obj.select_set(True) 218 219 view_layer.objects.active = act_new 220 return {'FINISHED'} 221 222 return {'CANCELLED'} 223 224 225class SubdivisionSet(Operator): 226 """Sets a Subdivision Surface Level (1-5)""" 227 228 bl_idname = "object.subdivision_set" 229 bl_label = "Subdivision Set" 230 bl_options = {'REGISTER', 'UNDO'} 231 232 level: IntProperty( 233 name="Level", 234 min=-100, max=100, 235 soft_min=-6, soft_max=6, 236 default=1, 237 ) 238 relative: BoolProperty( 239 name="Relative", 240 description=("Apply the subdivision surface level as an offset " 241 "relative to the current level"), 242 default=False, 243 ) 244 245 @classmethod 246 def poll(cls, context): 247 obs = context.selected_editable_objects 248 return (obs is not None) 249 250 def execute(self, context): 251 level = self.level 252 relative = self.relative 253 254 if relative and level == 0: 255 return {'CANCELLED'} # nothing to do 256 257 if not relative and level < 0: 258 self.level = level = 0 259 260 def set_object_subd(obj): 261 for mod in obj.modifiers: 262 if mod.type == 'MULTIRES': 263 if not relative: 264 if level > mod.total_levels: 265 sub = level - mod.total_levels 266 for _ in range(sub): 267 bpy.ops.object.multires_subdivide(modifier="Multires") 268 269 if obj.mode == 'SCULPT': 270 if mod.sculpt_levels != level: 271 mod.sculpt_levels = level 272 elif obj.mode == 'OBJECT': 273 if mod.levels != level: 274 mod.levels = level 275 return 276 else: 277 if obj.mode == 'SCULPT': 278 if mod.sculpt_levels + level <= mod.total_levels: 279 mod.sculpt_levels += level 280 elif obj.mode == 'OBJECT': 281 if mod.levels + level <= mod.total_levels: 282 mod.levels += level 283 return 284 285 elif mod.type == 'SUBSURF': 286 if relative: 287 mod.levels += level 288 else: 289 if mod.levels != level: 290 mod.levels = level 291 292 return 293 294 # add a new modifier 295 try: 296 if obj.mode == 'SCULPT': 297 mod = obj.modifiers.new("Multires", 'MULTIRES') 298 if level > 0: 299 for _ in range(level): 300 bpy.ops.object.multires_subdivide(modifier="Multires") 301 else: 302 mod = obj.modifiers.new("Subdivision", 'SUBSURF') 303 mod.levels = level 304 except: 305 self.report({'WARNING'}, 306 "Modifiers cannot be added to object: " + obj.name) 307 308 for obj in context.selected_editable_objects: 309 set_object_subd(obj) 310 311 return {'FINISHED'} 312 313 314class ShapeTransfer(Operator): 315 """Copy the active shape key of another selected object to this one""" 316 317 bl_idname = "object.shape_key_transfer" 318 bl_label = "Transfer Shape Key" 319 bl_options = {'REGISTER', 'UNDO'} 320 321 mode: EnumProperty( 322 items=( 323 ('OFFSET', 324 "Offset", 325 "Apply the relative positional offset", 326 ), 327 ('RELATIVE_FACE', 328 "Relative Face", 329 "Calculate relative position (using faces)", 330 ), 331 ('RELATIVE_EDGE', 332 "Relative Edge", 333 "Calculate relative position (using edges)", 334 ), 335 ), 336 name="Transformation Mode", 337 description="Relative shape positions to the new shape method", 338 default='OFFSET', 339 ) 340 use_clamp: BoolProperty( 341 name="Clamp Offset", 342 description=("Clamp the transformation to the distance each " 343 "vertex moves in the original shape"), 344 default=False, 345 ) 346 347 def _main(self, ob_act, objects, mode='OFFSET', use_clamp=False): 348 349 def me_nos(verts): 350 return [v.normal.copy() for v in verts] 351 352 def me_cos(verts): 353 return [v.co.copy() for v in verts] 354 355 def ob_add_shape(ob, name): 356 me = ob.data 357 key = ob.shape_key_add(from_mix=False) 358 if len(me.shape_keys.key_blocks) == 1: 359 key.name = "Basis" 360 key = ob.shape_key_add(from_mix=False) # we need a rest 361 key.name = name 362 ob.active_shape_key_index = len(me.shape_keys.key_blocks) - 1 363 ob.show_only_shape_key = True 364 365 from mathutils.geometry import barycentric_transform 366 from mathutils import Vector 367 368 if use_clamp and mode == 'OFFSET': 369 use_clamp = False 370 371 me = ob_act.data 372 orig_key_name = ob_act.active_shape_key.name 373 374 orig_shape_coords = me_cos(ob_act.active_shape_key.data) 375 376 orig_normals = me_nos(me.vertices) 377 # actual mesh vertex location isn't as reliable as the base shape :S 378 # orig_coords = me_cos(me.vertices) 379 orig_coords = me_cos(me.shape_keys.key_blocks[0].data) 380 381 for ob_other in objects: 382 if ob_other.type != 'MESH': 383 self.report({'WARNING'}, 384 ("Skipping '%s', " 385 "not a mesh") % ob_other.name) 386 continue 387 me_other = ob_other.data 388 if len(me_other.vertices) != len(me.vertices): 389 self.report({'WARNING'}, 390 ("Skipping '%s', " 391 "vertex count differs") % ob_other.name) 392 continue 393 394 target_normals = me_nos(me_other.vertices) 395 if me_other.shape_keys: 396 target_coords = me_cos(me_other.shape_keys.key_blocks[0].data) 397 else: 398 target_coords = me_cos(me_other.vertices) 399 400 ob_add_shape(ob_other, orig_key_name) 401 402 # editing the final coords, only list that stores wrapped coords 403 target_shape_coords = [v.co for v in 404 ob_other.active_shape_key.data] 405 406 median_coords = [[] for i in range(len(me.vertices))] 407 408 # Method 1, edge 409 if mode == 'OFFSET': 410 for i, vert_cos in enumerate(median_coords): 411 vert_cos.append(target_coords[i] + 412 (orig_shape_coords[i] - orig_coords[i])) 413 414 elif mode == 'RELATIVE_FACE': 415 for poly in me.polygons: 416 idxs = poly.vertices[:] 417 v_before = idxs[-2] 418 v = idxs[-1] 419 for v_after in idxs: 420 pt = barycentric_transform(orig_shape_coords[v], 421 orig_coords[v_before], 422 orig_coords[v], 423 orig_coords[v_after], 424 target_coords[v_before], 425 target_coords[v], 426 target_coords[v_after], 427 ) 428 median_coords[v].append(pt) 429 v_before = v 430 v = v_after 431 432 elif mode == 'RELATIVE_EDGE': 433 for ed in me.edges: 434 i1, i2 = ed.vertices 435 v1, v2 = orig_coords[i1], orig_coords[i2] 436 edge_length = (v1 - v2).length 437 n1loc = v1 + orig_normals[i1] * edge_length 438 n2loc = v2 + orig_normals[i2] * edge_length 439 440 # now get the target nloc's 441 v1_to, v2_to = target_coords[i1], target_coords[i2] 442 edlen_to = (v1_to - v2_to).length 443 n1loc_to = v1_to + target_normals[i1] * edlen_to 444 n2loc_to = v2_to + target_normals[i2] * edlen_to 445 446 pt = barycentric_transform(orig_shape_coords[i1], 447 v2, v1, n1loc, 448 v2_to, v1_to, n1loc_to) 449 median_coords[i1].append(pt) 450 451 pt = barycentric_transform(orig_shape_coords[i2], 452 v1, v2, n2loc, 453 v1_to, v2_to, n2loc_to) 454 median_coords[i2].append(pt) 455 456 # apply the offsets to the new shape 457 from functools import reduce 458 VectorAdd = Vector.__add__ 459 460 for i, vert_cos in enumerate(median_coords): 461 if vert_cos: 462 co = reduce(VectorAdd, vert_cos) / len(vert_cos) 463 464 if use_clamp: 465 # clamp to the same movement as the original 466 # breaks copy between different scaled meshes. 467 len_from = (orig_shape_coords[i] - 468 orig_coords[i]).length 469 ofs = co - target_coords[i] 470 ofs.length = len_from 471 co = target_coords[i] + ofs 472 473 target_shape_coords[i][:] = co 474 475 return {'FINISHED'} 476 477 @classmethod 478 def poll(cls, context): 479 obj = context.active_object 480 return (obj and obj.mode != 'EDIT') 481 482 def execute(self, context): 483 ob_act = context.active_object 484 objects = [ob for ob in context.selected_editable_objects 485 if ob != ob_act] 486 487 if 1: # swap from/to, means we can't copy to many at once. 488 if len(objects) != 1: 489 self.report({'ERROR'}, 490 ("Expected one other selected " 491 "mesh object to copy from")) 492 493 return {'CANCELLED'} 494 ob_act, objects = objects[0], [ob_act] 495 496 if ob_act.type != 'MESH': 497 self.report({'ERROR'}, "Other object is not a mesh") 498 return {'CANCELLED'} 499 500 if ob_act.active_shape_key is None: 501 self.report({'ERROR'}, "Other object has no shape key") 502 return {'CANCELLED'} 503 return self._main(ob_act, objects, self.mode, self.use_clamp) 504 505 506class JoinUVs(Operator): 507 """Transfer UV Maps from active to selected objects """ \ 508 """(needs matching geometry)""" 509 bl_idname = "object.join_uvs" 510 bl_label = "Transfer UV Maps" 511 bl_options = {'REGISTER', 'UNDO'} 512 513 @classmethod 514 def poll(cls, context): 515 obj = context.active_object 516 return (obj and obj.type == 'MESH') 517 518 def _main(self, context): 519 import array 520 obj = context.active_object 521 mesh = obj.data 522 523 is_editmode = (obj.mode == 'EDIT') 524 if is_editmode: 525 bpy.ops.object.mode_set(mode='OBJECT', toggle=False) 526 527 if not mesh.uv_layers: 528 self.report({'WARNING'}, 529 "Object: %s, Mesh: '%s' has no UVs" 530 % (obj.name, mesh.name)) 531 else: 532 nbr_loops = len(mesh.loops) 533 534 # seems to be the fastest way to create an array 535 uv_array = array.array('f', [0.0] * 2) * nbr_loops 536 mesh.uv_layers.active.data.foreach_get("uv", uv_array) 537 538 objects = context.selected_editable_objects[:] 539 540 for obj_other in objects: 541 if obj_other.type == 'MESH': 542 obj_other.data.tag = False 543 544 for obj_other in objects: 545 if obj_other != obj and obj_other.type == 'MESH': 546 mesh_other = obj_other.data 547 if mesh_other != mesh: 548 if mesh_other.tag is False: 549 mesh_other.tag = True 550 551 if len(mesh_other.loops) != nbr_loops: 552 self.report({'WARNING'}, "Object: %s, Mesh: " 553 "'%s' has %d loops (for %d faces)," 554 " expected %d\n" 555 % (obj_other.name, 556 mesh_other.name, 557 len(mesh_other.loops), 558 len(mesh_other.polygons), 559 nbr_loops, 560 ), 561 ) 562 else: 563 uv_other = mesh_other.uv_layers.active 564 if not uv_other: 565 mesh_other.uv_layers.new() 566 uv_other = mesh_other.uv_layers.active 567 if not uv_other: 568 self.report({'ERROR'}, "Could not add " 569 "a new UV map tp object " 570 "'%s' (Mesh '%s')\n" 571 % (obj_other.name, 572 mesh_other.name, 573 ), 574 ) 575 576 # finally do the copy 577 uv_other.data.foreach_set("uv", uv_array) 578 mesh_other.update() 579 580 if is_editmode: 581 bpy.ops.object.mode_set(mode='EDIT', toggle=False) 582 583 def execute(self, context): 584 self._main(context) 585 return {'FINISHED'} 586 587 588class MakeDupliFace(Operator): 589 """Convert objects into instanced faces""" 590 bl_idname = "object.make_dupli_face" 591 bl_label = "Make Instance Face" 592 bl_options = {'REGISTER', 'UNDO'} 593 594 @staticmethod 595 def _main(context): 596 from mathutils import Vector 597 from collections import defaultdict 598 599 SCALE_FAC = 0.01 600 offset = 0.5 * SCALE_FAC 601 base_tri = (Vector((-offset, -offset, 0.0)), 602 Vector((+offset, -offset, 0.0)), 603 Vector((+offset, +offset, 0.0)), 604 Vector((-offset, +offset, 0.0)), 605 ) 606 607 def matrix_to_quad(matrix): 608 # scale = matrix.median_scale 609 trans = matrix.to_translation() 610 rot = matrix.to_3x3() # also contains scale 611 612 return [(rot @ b) + trans for b in base_tri] 613 linked = defaultdict(list) 614 for obj in context.selected_objects: 615 if obj.type == 'MESH': 616 linked[obj.data].append(obj) 617 618 for data, objects in linked.items(): 619 face_verts = [axis for obj in objects 620 for v in matrix_to_quad(obj.matrix_world) 621 for axis in v] 622 nbr_verts = len(face_verts) // 3 623 nbr_faces = nbr_verts // 4 624 625 faces = list(range(nbr_verts)) 626 627 mesh = bpy.data.meshes.new(data.name + "_dupli") 628 629 mesh.vertices.add(nbr_verts) 630 mesh.loops.add(nbr_faces * 4) # Safer than nbr_verts. 631 mesh.polygons.add(nbr_faces) 632 633 mesh.vertices.foreach_set("co", face_verts) 634 mesh.loops.foreach_set("vertex_index", faces) 635 mesh.polygons.foreach_set("loop_start", range(0, nbr_faces * 4, 4)) 636 mesh.polygons.foreach_set("loop_total", (4,) * nbr_faces) 637 mesh.update() # generates edge data 638 639 ob_new = bpy.data.objects.new(mesh.name, mesh) 640 context.collection.objects.link(ob_new) 641 642 ob_inst = bpy.data.objects.new(data.name, data) 643 context.collection.objects.link(ob_inst) 644 645 ob_new.instance_type = 'FACES' 646 ob_inst.parent = ob_new 647 ob_new.use_instance_faces_scale = True 648 ob_new.instance_faces_scale = 1.0 / SCALE_FAC 649 650 ob_inst.select_set(True) 651 ob_new.select_set(True) 652 653 for obj in objects: 654 for collection in obj.users_collection: 655 collection.objects.unlink(obj) 656 657 def execute(self, context): 658 self._main(context) 659 return {'FINISHED'} 660 661 662class IsolateTypeRender(Operator): 663 """Hide unselected render objects of same type as active """ \ 664 """by setting the hide render flag""" 665 bl_idname = "object.isolate_type_render" 666 bl_label = "Restrict Render Unselected" 667 bl_options = {'REGISTER', 'UNDO'} 668 669 def execute(self, context): 670 act_type = context.object.type 671 672 for obj in context.visible_objects: 673 674 if obj.select_get(): 675 obj.hide_render = False 676 else: 677 if obj.type == act_type: 678 obj.hide_render = True 679 680 return {'FINISHED'} 681 682 683class ClearAllRestrictRender(Operator): 684 """Reveal all render objects by setting the hide render flag""" 685 bl_idname = "object.hide_render_clear_all" 686 bl_label = "Clear All Restrict Render" 687 bl_options = {'REGISTER', 'UNDO'} 688 689 def execute(self, context): 690 for obj in context.scene.objects: 691 obj.hide_render = False 692 return {'FINISHED'} 693 694 695class TransformsToDeltas(Operator): 696 """Convert normal object transforms to delta transforms, """ \ 697 """any existing delta transforms will be included as well""" 698 bl_idname = "object.transforms_to_deltas" 699 bl_label = "Transforms to Deltas" 700 bl_options = {'REGISTER', 'UNDO'} 701 702 mode: EnumProperty( 703 items=( 704 ('ALL', "All Transforms", "Transfer location, rotation, and scale transforms"), 705 ('LOC', "Location", "Transfer location transforms only"), 706 ('ROT', "Rotation", "Transfer rotation transforms only"), 707 ('SCALE', "Scale", "Transfer scale transforms only"), 708 ), 709 name="Mode", 710 description="Which transforms to transfer", 711 default='ALL', 712 ) 713 reset_values: BoolProperty( 714 name="Reset Values", 715 description=("Clear transform values after transferring to deltas"), 716 default=True, 717 ) 718 719 @classmethod 720 def poll(cls, context): 721 obs = context.selected_editable_objects 722 return (obs is not None) 723 724 def execute(self, context): 725 for obj in context.selected_editable_objects: 726 if self.mode in {'ALL', 'LOC'}: 727 self.transfer_location(obj) 728 729 if self.mode in {'ALL', 'ROT'}: 730 self.transfer_rotation(obj) 731 732 if self.mode in {'ALL', 'SCALE'}: 733 self.transfer_scale(obj) 734 735 return {'FINISHED'} 736 737 def transfer_location(self, obj): 738 obj.delta_location += obj.location 739 740 if self.reset_values: 741 obj.location.zero() 742 743 def transfer_rotation(self, obj): 744 # TODO: add transforms together... 745 if obj.rotation_mode == 'QUATERNION': 746 delta = obj.delta_rotation_quaternion.copy() 747 obj.delta_rotation_quaternion = obj.rotation_quaternion 748 obj.delta_rotation_quaternion.rotate(delta) 749 750 if self.reset_values: 751 obj.rotation_quaternion.identity() 752 elif obj.rotation_mode == 'AXIS_ANGLE': 753 pass # Unsupported 754 else: 755 delta = obj.delta_rotation_euler.copy() 756 obj.delta_rotation_euler = obj.rotation_euler 757 obj.delta_rotation_euler.rotate(delta) 758 759 if self.reset_values: 760 obj.rotation_euler.zero() 761 762 def transfer_scale(self, obj): 763 obj.delta_scale[0] *= obj.scale[0] 764 obj.delta_scale[1] *= obj.scale[1] 765 obj.delta_scale[2] *= obj.scale[2] 766 767 if self.reset_values: 768 obj.scale[:] = (1, 1, 1) 769 770 771class TransformsToDeltasAnim(Operator): 772 """Convert object animation for normal transforms to delta transforms""" 773 bl_idname = "object.anim_transforms_to_deltas" 774 bl_label = "Animated Transforms to Deltas" 775 bl_options = {'REGISTER', 'UNDO'} 776 777 @classmethod 778 def poll(cls, context): 779 obs = context.selected_editable_objects 780 return (obs is not None) 781 782 def execute(self, context): 783 # map from standard transform paths to "new" transform paths 784 STANDARD_TO_DELTA_PATHS = { 785 "location": "delta_location", 786 "rotation_euler": "delta_rotation_euler", 787 "rotation_quaternion": "delta_rotation_quaternion", 788 # "rotation_axis_angle" : "delta_rotation_axis_angle", 789 "scale": "delta_scale" 790 } 791 DELTA_PATHS = STANDARD_TO_DELTA_PATHS.values() 792 793 # try to apply on each selected object 794 for obj in context.selected_editable_objects: 795 adt = obj.animation_data 796 if (adt is None) or (adt.action is None): 797 self.report({'WARNING'}, 798 "No animation data to convert on object: %r" % 799 obj.name) 800 continue 801 802 # first pass over F-Curves: ensure that we don't have conflicting 803 # transforms already (e.g. if this was applied already) T29110. 804 existingFCurves = {} 805 for fcu in adt.action.fcurves: 806 # get "delta" path - i.e. the final paths which may clash 807 path = fcu.data_path 808 if path in STANDARD_TO_DELTA_PATHS: 809 # to be converted - conflicts may exist... 810 dpath = STANDARD_TO_DELTA_PATHS[path] 811 elif path in DELTA_PATHS: 812 # already delta - check for conflicts... 813 dpath = path 814 else: 815 # non-transform - ignore 816 continue 817 818 # a delta path like this for the same index shouldn't 819 # exist already, otherwise we've got a conflict 820 if dpath in existingFCurves: 821 # ensure that this index hasn't occurred before 822 if fcu.array_index in existingFCurves[dpath]: 823 # conflict 824 self.report({'ERROR'}, 825 "Object '%r' already has '%r' F-Curve(s). " 826 "Remove these before trying again" % 827 (obj.name, dpath)) 828 return {'CANCELLED'} 829 else: 830 # no conflict here 831 existingFCurves[dpath] += [fcu.array_index] 832 else: 833 # no conflict yet 834 existingFCurves[dpath] = [fcu.array_index] 835 836 # if F-Curve uses standard transform path 837 # just append "delta_" to this path 838 for fcu in adt.action.fcurves: 839 if fcu.data_path == "location": 840 fcu.data_path = "delta_location" 841 obj.location.zero() 842 elif fcu.data_path == "rotation_euler": 843 fcu.data_path = "delta_rotation_euler" 844 obj.rotation_euler.zero() 845 elif fcu.data_path == "rotation_quaternion": 846 fcu.data_path = "delta_rotation_quaternion" 847 obj.rotation_quaternion.identity() 848 # XXX: currently not implemented 849 # ~ elif fcu.data_path == "rotation_axis_angle": 850 # ~ fcu.data_path = "delta_rotation_axis_angle" 851 elif fcu.data_path == "scale": 852 fcu.data_path = "delta_scale" 853 obj.scale = 1.0, 1.0, 1.0 854 855 # hack: force animsys flush by changing frame, so that deltas get run 856 context.scene.frame_set(context.scene.frame_current) 857 858 return {'FINISHED'} 859 860 861class DupliOffsetFromCursor(Operator): 862 """Set offset used for collection instances based on cursor position""" 863 bl_idname = "object.instance_offset_from_cursor" 864 bl_label = "Set Offset from Cursor" 865 bl_options = {'INTERNAL', 'UNDO'} 866 867 @classmethod 868 def poll(cls, context): 869 return (context.active_object is not None) 870 871 def execute(self, context): 872 scene = context.scene 873 collection = context.collection 874 875 collection.instance_offset = scene.cursor.location 876 877 return {'FINISHED'} 878 879 880class LoadImageAsEmpty: 881 bl_options = {'REGISTER', 'UNDO'} 882 883 filepath: StringProperty( 884 subtype='FILE_PATH' 885 ) 886 887 filter_image: BoolProperty(default=True, options={'HIDDEN', 'SKIP_SAVE'}) 888 filter_folder: BoolProperty(default=True, options={'HIDDEN', 'SKIP_SAVE'}) 889 890 view_align: BoolProperty( 891 name="Align to view", 892 default=True, 893 ) 894 895 @classmethod 896 def poll(cls, context): 897 return context.mode == 'OBJECT' 898 899 def invoke(self, context, _event): 900 context.window_manager.fileselect_add(self) 901 return {'RUNNING_MODAL'} 902 903 def execute(self, context): 904 scene = context.scene 905 cursor = scene.cursor.location 906 907 try: 908 image = bpy.data.images.load(self.filepath, check_existing=True) 909 except RuntimeError as ex: 910 self.report({'ERROR'}, str(ex)) 911 return {'CANCELLED'} 912 913 bpy.ops.object.empty_add( 914 'INVOKE_REGION_WIN', 915 type='IMAGE', 916 location=cursor, 917 align=('VIEW' if self.view_align else 'WORLD'), 918 ) 919 920 view_layer = context.view_layer 921 obj = view_layer.objects.active 922 obj.data = image 923 obj.empty_display_size = 5.0 924 self.set_settings(context, obj) 925 return {'FINISHED'} 926 927 def set_settings(self, context, obj): 928 pass 929 930 931class LoadBackgroundImage(LoadImageAsEmpty, Operator): 932 """Add a reference image into the background behind objects""" 933 bl_idname = "object.load_background_image" 934 bl_label = "Load Background Image" 935 936 def set_settings(self, context, obj): 937 obj.empty_image_depth = 'BACK' 938 obj.empty_image_side = 'FRONT' 939 940 if context.space_data.type == 'VIEW_3D': 941 if not context.space_data.region_3d.is_perspective: 942 obj.show_empty_image_perspective = False 943 944 945class LoadReferenceImage(LoadImageAsEmpty, Operator): 946 """Add a reference image into the scene between objects""" 947 bl_idname = "object.load_reference_image" 948 bl_label = "Load Reference Image" 949 950 def set_settings(self, context, obj): 951 pass 952 953 954class OBJECT_OT_assign_property_defaults(Operator): 955 """Assign the current values of custom properties as their defaults, """ \ 956 """for use as part of the rest pose state in NLA track mixing""" 957 bl_idname = "object.assign_property_defaults" 958 bl_label = "Assign Custom Property Values as Default" 959 bl_options = {'UNDO', 'REGISTER'} 960 961 process_data: BoolProperty(name="Process data properties", default=True) 962 process_bones: BoolProperty(name="Process bone properties", default=True) 963 964 @classmethod 965 def poll(cls, context): 966 obj = context.active_object 967 return obj is not None and obj.library is None and obj.mode in {'POSE', 'OBJECT'} 968 969 @staticmethod 970 def assign_defaults(obj): 971 from rna_prop_ui import rna_idprop_ui_prop_default_set 972 973 rna_properties = {'_RNA_UI'} | {prop.identifier for prop in obj.bl_rna.properties if prop.is_runtime} 974 975 for prop, value in obj.items(): 976 if prop not in rna_properties: 977 rna_idprop_ui_prop_default_set(obj, prop, value) 978 979 def execute(self, context): 980 obj = context.active_object 981 982 self.assign_defaults(obj) 983 984 if self.process_bones and obj.pose: 985 for pbone in obj.pose.bones: 986 self.assign_defaults(pbone) 987 988 if self.process_data and obj.data and obj.data.library is None: 989 self.assign_defaults(obj.data) 990 991 if self.process_bones and isinstance(obj.data, bpy.types.Armature): 992 for bone in obj.data.bones: 993 self.assign_defaults(bone) 994 995 return {'FINISHED'} 996 997 998classes = ( 999 ClearAllRestrictRender, 1000 DupliOffsetFromCursor, 1001 IsolateTypeRender, 1002 JoinUVs, 1003 LoadBackgroundImage, 1004 LoadReferenceImage, 1005 MakeDupliFace, 1006 SelectCamera, 1007 SelectHierarchy, 1008 SelectPattern, 1009 ShapeTransfer, 1010 SubdivisionSet, 1011 TransformsToDeltas, 1012 TransformsToDeltasAnim, 1013 OBJECT_OT_assign_property_defaults, 1014) 1015