1016 lines
		
	
	
		
			35 KiB
		
	
	
	
		
			Python
		
	
	
	
	
	
			
		
		
	
	
			1016 lines
		
	
	
		
			35 KiB
		
	
	
	
		
			Python
		
	
	
	
	
	
| # ##### BEGIN GPL LICENSE BLOCK #####
 | |
| #
 | |
| #  This program is free software; you can redistribute it and/or
 | |
| #  modify it under the terms of the GNU General Public License
 | |
| #  as published by the Free Software Foundation; either version 2
 | |
| #  of the License, or (at your option) any later version.
 | |
| #
 | |
| #  This program is distributed in the hope that it will be useful,
 | |
| #  but WITHOUT ANY WARRANTY; without even the implied warranty of
 | |
| #  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 | |
| #  GNU General Public License for more details.
 | |
| #
 | |
| #  You should have received a copy of the GNU General Public License
 | |
| #  along with this program; if not, write to the Free Software Foundation,
 | |
| #  Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
 | |
| #
 | |
| # ##### END GPL LICENSE BLOCK #####
 | |
| 
 | |
| # <pep8-80 compliant>
 | |
| 
 | |
| import bpy
 | |
| from bpy.types import Operator
 | |
| from bpy.props import (
 | |
|     BoolProperty,
 | |
|     EnumProperty,
 | |
|     IntProperty,
 | |
|     StringProperty,
 | |
| )
 | |
| 
 | |
| 
 | |
| class SelectPattern(Operator):
 | |
|     """Select objects matching a naming pattern"""
 | |
|     bl_idname = "object.select_pattern"
 | |
|     bl_label = "Select Pattern"
 | |
|     bl_options = {'REGISTER', 'UNDO'}
 | |
| 
 | |
|     pattern: StringProperty(
 | |
|         name="Pattern",
 | |
|         description="Name filter using '*', '?' and "
 | |
|         "'[abc]' unix style wildcards",
 | |
|         maxlen=64,
 | |
|         default="*",
 | |
|     )
 | |
|     case_sensitive: BoolProperty(
 | |
|         name="Case Sensitive",
 | |
|         description="Do a case sensitive compare",
 | |
|         default=False,
 | |
|     )
 | |
|     extend: BoolProperty(
 | |
|         name="Extend",
 | |
|         description="Extend the existing selection",
 | |
|         default=True,
 | |
|     )
 | |
| 
 | |
|     def execute(self, context):
 | |
| 
 | |
|         import fnmatch
 | |
| 
 | |
|         if self.case_sensitive:
 | |
|             pattern_match = fnmatch.fnmatchcase
 | |
|         else:
 | |
|             pattern_match = (lambda a, b:
 | |
|                              fnmatch.fnmatchcase(a.upper(), b.upper()))
 | |
|         is_ebone = False
 | |
|         is_pbone = False
 | |
|         obj = context.object
 | |
|         if obj and obj.mode == 'POSE':
 | |
|             items = obj.data.bones
 | |
|             if not self.extend:
 | |
|                 bpy.ops.pose.select_all(action='DESELECT')
 | |
|             is_pbone = True
 | |
|         elif obj and obj.type == 'ARMATURE' and obj.mode == 'EDIT':
 | |
|             items = obj.data.edit_bones
 | |
|             if not self.extend:
 | |
|                 bpy.ops.armature.select_all(action='DESELECT')
 | |
|             is_ebone = True
 | |
|         else:
 | |
|             items = context.visible_objects
 | |
|             if not self.extend:
 | |
|                 bpy.ops.object.select_all(action='DESELECT')
 | |
| 
 | |
|         # Can be pose bones, edit bones or objects
 | |
|         for item in items:
 | |
|             if pattern_match(item.name, self.pattern):
 | |
| 
 | |
|                 # hrmf, perhaps there should be a utility function for this.
 | |
|                 if is_ebone:
 | |
|                     item.select = True
 | |
|                     item.select_head = True
 | |
|                     item.select_tail = True
 | |
|                     if item.use_connect:
 | |
|                         item_parent = item.parent
 | |
|                         if item_parent is not None:
 | |
|                             item_parent.select_tail = True
 | |
|                 elif is_pbone:
 | |
|                     item.select = True
 | |
|                 else:
 | |
|                     item.select_set(True)
 | |
| 
 | |
|         return {'FINISHED'}
 | |
| 
 | |
|     def invoke(self, context, event):
 | |
|         wm = context.window_manager
 | |
|         return wm.invoke_props_popup(self, event)
 | |
| 
 | |
|     def draw(self, _context):
 | |
|         layout = self.layout
 | |
| 
 | |
|         layout.prop(self, "pattern")
 | |
|         row = layout.row()
 | |
|         row.prop(self, "case_sensitive")
 | |
|         row.prop(self, "extend")
 | |
| 
 | |
|     @classmethod
 | |
|     def poll(cls, context):
 | |
|         obj = context.object
 | |
|         return (not obj) or (obj.mode == 'OBJECT') or (obj.type == 'ARMATURE')
 | |
| 
 | |
| 
 | |
| class SelectCamera(Operator):
 | |
|     """Select the active camera"""
 | |
|     bl_idname = "object.select_camera"
 | |
|     bl_label = "Select Camera"
 | |
|     bl_options = {'REGISTER', 'UNDO'}
 | |
| 
 | |
|     extend: BoolProperty(
 | |
|         name="Extend",
 | |
|         description="Extend the selection",
 | |
|         default=False,
 | |
|     )
 | |
| 
 | |
|     def execute(self, context):
 | |
|         scene = context.scene
 | |
|         view_layer = context.view_layer
 | |
|         view = context.space_data
 | |
|         if view.type == 'VIEW_3D' and view.use_local_camera:
 | |
|             camera = view.camera
 | |
|         else:
 | |
|             camera = scene.camera
 | |
| 
 | |
|         if camera is None:
 | |
|             self.report({'WARNING'}, "No camera found")
 | |
|         elif camera.name not in scene.objects:
 | |
|             self.report({'WARNING'}, "Active camera is not in this scene")
 | |
|         else:
 | |
|             if not self.extend:
 | |
|                 bpy.ops.object.select_all(action='DESELECT')
 | |
|             view_layer.objects.active = camera
 | |
|             # camera.hide = False  # XXX TODO where is this now?
 | |
|             camera.select_set(True)
 | |
|             return {'FINISHED'}
 | |
| 
 | |
|         return {'CANCELLED'}
 | |
| 
 | |
| 
 | |
| class SelectHierarchy(Operator):
 | |
|     """Select object relative to the active object's position """ \
 | |
|         """in the hierarchy"""
 | |
|     bl_idname = "object.select_hierarchy"
 | |
|     bl_label = "Select Hierarchy"
 | |
|     bl_options = {'REGISTER', 'UNDO'}
 | |
| 
 | |
|     direction: EnumProperty(
 | |
|         items=(
 | |
|             ('PARENT', "Parent", ""),
 | |
|             ('CHILD', "Child", ""),
 | |
|         ),
 | |
|         name="Direction",
 | |
|         description="Direction to select in the hierarchy",
 | |
|         default='PARENT',
 | |
|     )
 | |
|     extend: BoolProperty(
 | |
|         name="Extend",
 | |
|         description="Extend the existing selection",
 | |
|         default=False,
 | |
|     )
 | |
| 
 | |
|     @classmethod
 | |
|     def poll(cls, context):
 | |
|         return context.object
 | |
| 
 | |
|     def execute(self, context):
 | |
|         view_layer = context.view_layer
 | |
|         select_new = []
 | |
|         act_new = None
 | |
| 
 | |
|         selected_objects = context.selected_objects
 | |
|         obj_act = context.object
 | |
| 
 | |
|         if context.object not in selected_objects:
 | |
|             selected_objects.append(context.object)
 | |
| 
 | |
|         if self.direction == 'PARENT':
 | |
|             for obj in selected_objects:
 | |
|                 parent = obj.parent
 | |
| 
 | |
|                 if parent and parent.visible_get():
 | |
|                     if obj_act == obj:
 | |
|                         act_new = parent
 | |
| 
 | |
|                     select_new.append(parent)
 | |
| 
 | |
|         else:
 | |
|             for obj in selected_objects:
 | |
|                 select_new.extend([child for child in obj.children if child.visible_get()])
 | |
| 
 | |
|             if select_new:
 | |
|                 select_new.sort(key=lambda obj_iter: obj_iter.name)
 | |
|                 act_new = select_new[0]
 | |
| 
 | |
| 
 | |
|         # don't edit any object settings above this
 | |
|         if select_new:
 | |
|             if not self.extend:
 | |
|                 bpy.ops.object.select_all(action='DESELECT')
 | |
| 
 | |
|             for obj in select_new:
 | |
|                 obj.select_set(True)
 | |
| 
 | |
|             view_layer.objects.active = act_new
 | |
|             return {'FINISHED'}
 | |
| 
 | |
|         return {'CANCELLED'}
 | |
| 
 | |
| 
 | |
| class SubdivisionSet(Operator):
 | |
|     """Sets a Subdivision Surface Level (1-5)"""
 | |
| 
 | |
|     bl_idname = "object.subdivision_set"
 | |
|     bl_label = "Subdivision Set"
 | |
|     bl_options = {'REGISTER', 'UNDO'}
 | |
| 
 | |
|     level: IntProperty(
 | |
|         name="Level",
 | |
|         min=-100, max=100,
 | |
|         soft_min=-6, soft_max=6,
 | |
|         default=1,
 | |
|     )
 | |
|     relative: BoolProperty(
 | |
|         name="Relative",
 | |
|         description=("Apply the subdivision surface level as an offset "
 | |
|                      "relative to the current level"),
 | |
|         default=False,
 | |
|     )
 | |
| 
 | |
|     @classmethod
 | |
|     def poll(cls, context):
 | |
|         obs = context.selected_editable_objects
 | |
|         return (obs is not None)
 | |
| 
 | |
|     def execute(self, context):
 | |
|         level = self.level
 | |
|         relative = self.relative
 | |
| 
 | |
|         if relative and level == 0:
 | |
|             return {'CANCELLED'}  # nothing to do
 | |
| 
 | |
|         if not relative and level < 0:
 | |
|             self.level = level = 0
 | |
| 
 | |
|         def set_object_subd(obj):
 | |
|             for mod in obj.modifiers:
 | |
|                 if mod.type == 'MULTIRES':
 | |
|                     if not relative:
 | |
|                         if level > mod.total_levels:
 | |
|                             sub = level - mod.total_levels
 | |
|                             for _ in range(sub):
 | |
|                                 bpy.ops.object.multires_subdivide(modifier="Multires")
 | |
| 
 | |
|                         if obj.mode == 'SCULPT':
 | |
|                             if mod.sculpt_levels != level:
 | |
|                                 mod.sculpt_levels = level
 | |
|                         elif obj.mode == 'OBJECT':
 | |
|                             if mod.levels != level:
 | |
|                                 mod.levels = level
 | |
|                         return
 | |
|                     else:
 | |
|                         if obj.mode == 'SCULPT':
 | |
|                             if mod.sculpt_levels + level <= mod.total_levels:
 | |
|                                 mod.sculpt_levels += level
 | |
|                         elif obj.mode == 'OBJECT':
 | |
|                             if mod.levels + level <= mod.total_levels:
 | |
|                                 mod.levels += level
 | |
|                         return
 | |
| 
 | |
|                 elif mod.type == 'SUBSURF':
 | |
|                     if relative:
 | |
|                         mod.levels += level
 | |
|                     else:
 | |
|                         if mod.levels != level:
 | |
|                             mod.levels = level
 | |
| 
 | |
|                     return
 | |
| 
 | |
|             # add a new modifier
 | |
|             try:
 | |
|                 if obj.mode == 'SCULPT':
 | |
|                     mod = obj.modifiers.new("Multires", 'MULTIRES')
 | |
|                     if level > 0:
 | |
|                         for _ in range(level):
 | |
|                             bpy.ops.object.multires_subdivide(modifier="Multires")
 | |
|                 else:
 | |
|                     mod = obj.modifiers.new("Subdivision", 'SUBSURF')
 | |
|                     mod.levels = level
 | |
|             except:
 | |
|                 self.report({'WARNING'},
 | |
|                             "Modifiers cannot be added to object: " + obj.name)
 | |
| 
 | |
|         for obj in context.selected_editable_objects:
 | |
|             set_object_subd(obj)
 | |
| 
 | |
|         return {'FINISHED'}
 | |
| 
 | |
| 
 | |
| class ShapeTransfer(Operator):
 | |
|     """Copy the active shape key of another selected object to this one"""
 | |
| 
 | |
|     bl_idname = "object.shape_key_transfer"
 | |
|     bl_label = "Transfer Shape Key"
 | |
|     bl_options = {'REGISTER', 'UNDO'}
 | |
| 
 | |
|     mode: EnumProperty(
 | |
|         items=(
 | |
|             ('OFFSET',
 | |
|              "Offset",
 | |
|              "Apply the relative positional offset",
 | |
|              ),
 | |
|             ('RELATIVE_FACE',
 | |
|              "Relative Face",
 | |
|              "Calculate relative position (using faces)",
 | |
|              ),
 | |
|             ('RELATIVE_EDGE',
 | |
|              "Relative Edge",
 | |
|              "Calculate relative position (using edges)",
 | |
|              ),
 | |
|         ),
 | |
|         name="Transformation Mode",
 | |
|         description="Relative shape positions to the new shape method",
 | |
|         default='OFFSET',
 | |
|     )
 | |
|     use_clamp: BoolProperty(
 | |
|         name="Clamp Offset",
 | |
|         description=("Clamp the transformation to the distance each "
 | |
|                      "vertex moves in the original shape"),
 | |
|         default=False,
 | |
|     )
 | |
| 
 | |
|     def _main(self, ob_act, objects, mode='OFFSET', use_clamp=False):
 | |
| 
 | |
|         def me_nos(verts):
 | |
|             return [v.normal.copy() for v in verts]
 | |
| 
 | |
|         def me_cos(verts):
 | |
|             return [v.co.copy() for v in verts]
 | |
| 
 | |
|         def ob_add_shape(ob, name):
 | |
|             me = ob.data
 | |
|             key = ob.shape_key_add(from_mix=False)
 | |
|             if len(me.shape_keys.key_blocks) == 1:
 | |
|                 key.name = "Basis"
 | |
|                 key = ob.shape_key_add(from_mix=False)  # we need a rest
 | |
|             key.name = name
 | |
|             ob.active_shape_key_index = len(me.shape_keys.key_blocks) - 1
 | |
|             ob.show_only_shape_key = True
 | |
| 
 | |
|         from mathutils.geometry import barycentric_transform
 | |
|         from mathutils import Vector
 | |
| 
 | |
|         if use_clamp and mode == 'OFFSET':
 | |
|             use_clamp = False
 | |
| 
 | |
|         me = ob_act.data
 | |
|         orig_key_name = ob_act.active_shape_key.name
 | |
| 
 | |
|         orig_shape_coords = me_cos(ob_act.active_shape_key.data)
 | |
| 
 | |
|         orig_normals = me_nos(me.vertices)
 | |
|         # actual mesh vertex location isn't as reliable as the base shape :S
 | |
|         # orig_coords = me_cos(me.vertices)
 | |
|         orig_coords = me_cos(me.shape_keys.key_blocks[0].data)
 | |
| 
 | |
|         for ob_other in objects:
 | |
|             if ob_other.type != 'MESH':
 | |
|                 self.report({'WARNING'},
 | |
|                             ("Skipping '%s', "
 | |
|                              "not a mesh") % ob_other.name)
 | |
|                 continue
 | |
|             me_other = ob_other.data
 | |
|             if len(me_other.vertices) != len(me.vertices):
 | |
|                 self.report({'WARNING'},
 | |
|                             ("Skipping '%s', "
 | |
|                              "vertex count differs") % ob_other.name)
 | |
|                 continue
 | |
| 
 | |
|             target_normals = me_nos(me_other.vertices)
 | |
|             if me_other.shape_keys:
 | |
|                 target_coords = me_cos(me_other.shape_keys.key_blocks[0].data)
 | |
|             else:
 | |
|                 target_coords = me_cos(me_other.vertices)
 | |
| 
 | |
|             ob_add_shape(ob_other, orig_key_name)
 | |
| 
 | |
|             # editing the final coords, only list that stores wrapped coords
 | |
|             target_shape_coords = [v.co for v in
 | |
|                                    ob_other.active_shape_key.data]
 | |
| 
 | |
|             median_coords = [[] for i in range(len(me.vertices))]
 | |
| 
 | |
|             # Method 1, edge
 | |
|             if mode == 'OFFSET':
 | |
|                 for i, vert_cos in enumerate(median_coords):
 | |
|                     vert_cos.append(target_coords[i] +
 | |
|                                     (orig_shape_coords[i] - orig_coords[i]))
 | |
| 
 | |
|             elif mode == 'RELATIVE_FACE':
 | |
|                 for poly in me.polygons:
 | |
|                     idxs = poly.vertices[:]
 | |
|                     v_before = idxs[-2]
 | |
|                     v = idxs[-1]
 | |
|                     for v_after in idxs:
 | |
|                         pt = barycentric_transform(orig_shape_coords[v],
 | |
|                                                    orig_coords[v_before],
 | |
|                                                    orig_coords[v],
 | |
|                                                    orig_coords[v_after],
 | |
|                                                    target_coords[v_before],
 | |
|                                                    target_coords[v],
 | |
|                                                    target_coords[v_after],
 | |
|                                                    )
 | |
|                         median_coords[v].append(pt)
 | |
|                         v_before = v
 | |
|                         v = v_after
 | |
| 
 | |
|             elif mode == 'RELATIVE_EDGE':
 | |
|                 for ed in me.edges:
 | |
|                     i1, i2 = ed.vertices
 | |
|                     v1, v2 = orig_coords[i1], orig_coords[i2]
 | |
|                     edge_length = (v1 - v2).length
 | |
|                     n1loc = v1 + orig_normals[i1] * edge_length
 | |
|                     n2loc = v2 + orig_normals[i2] * edge_length
 | |
| 
 | |
|                     # now get the target nloc's
 | |
|                     v1_to, v2_to = target_coords[i1], target_coords[i2]
 | |
|                     edlen_to = (v1_to - v2_to).length
 | |
|                     n1loc_to = v1_to + target_normals[i1] * edlen_to
 | |
|                     n2loc_to = v2_to + target_normals[i2] * edlen_to
 | |
| 
 | |
|                     pt = barycentric_transform(orig_shape_coords[i1],
 | |
|                                                v2, v1, n1loc,
 | |
|                                                v2_to, v1_to, n1loc_to)
 | |
|                     median_coords[i1].append(pt)
 | |
| 
 | |
|                     pt = barycentric_transform(orig_shape_coords[i2],
 | |
|                                                v1, v2, n2loc,
 | |
|                                                v1_to, v2_to, n2loc_to)
 | |
|                     median_coords[i2].append(pt)
 | |
| 
 | |
|             # apply the offsets to the new shape
 | |
|             from functools import reduce
 | |
|             VectorAdd = Vector.__add__
 | |
| 
 | |
|             for i, vert_cos in enumerate(median_coords):
 | |
|                 if vert_cos:
 | |
|                     co = reduce(VectorAdd, vert_cos) / len(vert_cos)
 | |
| 
 | |
|                     if use_clamp:
 | |
|                         # clamp to the same movement as the original
 | |
|                         # breaks copy between different scaled meshes.
 | |
|                         len_from = (orig_shape_coords[i] -
 | |
|                                     orig_coords[i]).length
 | |
|                         ofs = co - target_coords[i]
 | |
|                         ofs.length = len_from
 | |
|                         co = target_coords[i] + ofs
 | |
| 
 | |
|                     target_shape_coords[i][:] = co
 | |
| 
 | |
|         return {'FINISHED'}
 | |
| 
 | |
|     @classmethod
 | |
|     def poll(cls, context):
 | |
|         obj = context.active_object
 | |
|         return (obj and obj.mode != 'EDIT')
 | |
| 
 | |
|     def execute(self, context):
 | |
|         ob_act = context.active_object
 | |
|         objects = [ob for ob in context.selected_editable_objects
 | |
|                    if ob != ob_act]
 | |
| 
 | |
|         if 1:  # swap from/to, means we can't copy to many at once.
 | |
|             if len(objects) != 1:
 | |
|                 self.report({'ERROR'},
 | |
|                             ("Expected one other selected "
 | |
|                              "mesh object to copy from"))
 | |
| 
 | |
|                 return {'CANCELLED'}
 | |
|             ob_act, objects = objects[0], [ob_act]
 | |
| 
 | |
|         if ob_act.type != 'MESH':
 | |
|             self.report({'ERROR'}, "Other object is not a mesh")
 | |
|             return {'CANCELLED'}
 | |
| 
 | |
|         if ob_act.active_shape_key is None:
 | |
|             self.report({'ERROR'}, "Other object has no shape key")
 | |
|             return {'CANCELLED'}
 | |
|         return self._main(ob_act, objects, self.mode, self.use_clamp)
 | |
| 
 | |
| 
 | |
| class JoinUVs(Operator):
 | |
|     """Transfer UV Maps from active to selected objects """ \
 | |
|         """(needs matching geometry)"""
 | |
|     bl_idname = "object.join_uvs"
 | |
|     bl_label = "Transfer UV Maps"
 | |
|     bl_options = {'REGISTER', 'UNDO'}
 | |
| 
 | |
|     @classmethod
 | |
|     def poll(cls, context):
 | |
|         obj = context.active_object
 | |
|         return (obj and obj.type == 'MESH')
 | |
| 
 | |
|     def _main(self, context):
 | |
|         import array
 | |
|         obj = context.active_object
 | |
|         mesh = obj.data
 | |
| 
 | |
|         is_editmode = (obj.mode == 'EDIT')
 | |
|         if is_editmode:
 | |
|             bpy.ops.object.mode_set(mode='OBJECT', toggle=False)
 | |
| 
 | |
|         if not mesh.uv_layers:
 | |
|             self.report({'WARNING'},
 | |
|                         "Object: %s, Mesh: '%s' has no UVs"
 | |
|                         % (obj.name, mesh.name))
 | |
|         else:
 | |
|             nbr_loops = len(mesh.loops)
 | |
| 
 | |
|             # seems to be the fastest way to create an array
 | |
|             uv_array = array.array('f', [0.0] * 2) * nbr_loops
 | |
|             mesh.uv_layers.active.data.foreach_get("uv", uv_array)
 | |
| 
 | |
|             objects = context.selected_editable_objects[:]
 | |
| 
 | |
|             for obj_other in objects:
 | |
|                 if obj_other.type == 'MESH':
 | |
|                     obj_other.data.tag = False
 | |
| 
 | |
|             for obj_other in objects:
 | |
|                 if obj_other != obj and obj_other.type == 'MESH':
 | |
|                     mesh_other = obj_other.data
 | |
|                     if mesh_other != mesh:
 | |
|                         if mesh_other.tag is False:
 | |
|                             mesh_other.tag = True
 | |
| 
 | |
|                             if len(mesh_other.loops) != nbr_loops:
 | |
|                                 self.report({'WARNING'}, "Object: %s, Mesh: "
 | |
|                                             "'%s' has %d loops (for %d faces),"
 | |
|                                             " expected %d\n"
 | |
|                                             % (obj_other.name,
 | |
|                                                mesh_other.name,
 | |
|                                                len(mesh_other.loops),
 | |
|                                                len(mesh_other.polygons),
 | |
|                                                nbr_loops,
 | |
|                                                ),
 | |
|                                             )
 | |
|                             else:
 | |
|                                 uv_other = mesh_other.uv_layers.active
 | |
|                                 if not uv_other:
 | |
|                                     mesh_other.uv_layers.new()
 | |
|                                     uv_other = mesh_other.uv_layers.active
 | |
|                                     if not uv_other:
 | |
|                                         self.report({'ERROR'}, "Could not add "
 | |
|                                                     "a new UV map tp object "
 | |
|                                                     "'%s' (Mesh '%s')\n"
 | |
|                                                     % (obj_other.name,
 | |
|                                                        mesh_other.name,
 | |
|                                                        ),
 | |
|                                                     )
 | |
| 
 | |
|                                 # finally do the copy
 | |
|                                 uv_other.data.foreach_set("uv", uv_array)
 | |
|                                 mesh_other.update()
 | |
| 
 | |
|         if is_editmode:
 | |
|             bpy.ops.object.mode_set(mode='EDIT', toggle=False)
 | |
| 
 | |
|     def execute(self, context):
 | |
|         self._main(context)
 | |
|         return {'FINISHED'}
 | |
| 
 | |
| 
 | |
| class MakeDupliFace(Operator):
 | |
|     """Convert objects into instanced faces"""
 | |
|     bl_idname = "object.make_dupli_face"
 | |
|     bl_label = "Make Instance Face"
 | |
|     bl_options = {'REGISTER', 'UNDO'}
 | |
| 
 | |
|     @staticmethod
 | |
|     def _main(context):
 | |
|         from mathutils import Vector
 | |
|         from collections import defaultdict
 | |
| 
 | |
|         SCALE_FAC = 0.01
 | |
|         offset = 0.5 * SCALE_FAC
 | |
|         base_tri = (Vector((-offset, -offset, 0.0)),
 | |
|                     Vector((+offset, -offset, 0.0)),
 | |
|                     Vector((+offset, +offset, 0.0)),
 | |
|                     Vector((-offset, +offset, 0.0)),
 | |
|                     )
 | |
| 
 | |
|         def matrix_to_quad(matrix):
 | |
|             # scale = matrix.median_scale
 | |
|             trans = matrix.to_translation()
 | |
|             rot = matrix.to_3x3()  # also contains scale
 | |
| 
 | |
|             return [(rot @ b) + trans for b in base_tri]
 | |
|         linked = defaultdict(list)
 | |
|         for obj in context.selected_objects:
 | |
|             if obj.type == 'MESH':
 | |
|                 linked[obj.data].append(obj)
 | |
| 
 | |
|         for data, objects in linked.items():
 | |
|             face_verts = [axis for obj in objects
 | |
|                           for v in matrix_to_quad(obj.matrix_world)
 | |
|                           for axis in v]
 | |
|             nbr_verts = len(face_verts) // 3
 | |
|             nbr_faces = nbr_verts // 4
 | |
| 
 | |
|             faces = list(range(nbr_verts))
 | |
| 
 | |
|             mesh = bpy.data.meshes.new(data.name + "_dupli")
 | |
| 
 | |
|             mesh.vertices.add(nbr_verts)
 | |
|             mesh.loops.add(nbr_faces * 4)  # Safer than nbr_verts.
 | |
|             mesh.polygons.add(nbr_faces)
 | |
| 
 | |
|             mesh.vertices.foreach_set("co", face_verts)
 | |
|             mesh.loops.foreach_set("vertex_index", faces)
 | |
|             mesh.polygons.foreach_set("loop_start", range(0, nbr_faces * 4, 4))
 | |
|             mesh.polygons.foreach_set("loop_total", (4,) * nbr_faces)
 | |
|             mesh.update()  # generates edge data
 | |
| 
 | |
|             ob_new = bpy.data.objects.new(mesh.name, mesh)
 | |
|             context.collection.objects.link(ob_new)
 | |
| 
 | |
|             ob_inst = bpy.data.objects.new(data.name, data)
 | |
|             context.collection.objects.link(ob_inst)
 | |
| 
 | |
|             ob_new.instance_type = 'FACES'
 | |
|             ob_inst.parent = ob_new
 | |
|             ob_new.use_instance_faces_scale = True
 | |
|             ob_new.instance_faces_scale = 1.0 / SCALE_FAC
 | |
| 
 | |
|             ob_inst.select_set(True)
 | |
|             ob_new.select_set(True)
 | |
| 
 | |
|             for obj in objects:
 | |
|                 for collection in obj.users_collection:
 | |
|                     collection.objects.unlink(obj)
 | |
| 
 | |
|     def execute(self, context):
 | |
|         self._main(context)
 | |
|         return {'FINISHED'}
 | |
| 
 | |
| 
 | |
| class IsolateTypeRender(Operator):
 | |
|     """Hide unselected render objects of same type as active """ \
 | |
|         """by setting the hide render flag"""
 | |
|     bl_idname = "object.isolate_type_render"
 | |
|     bl_label = "Restrict Render Unselected"
 | |
|     bl_options = {'REGISTER', 'UNDO'}
 | |
| 
 | |
|     def execute(self, context):
 | |
|         act_type = context.object.type
 | |
| 
 | |
|         for obj in context.visible_objects:
 | |
| 
 | |
|             if obj.select_get():
 | |
|                 obj.hide_render = False
 | |
|             else:
 | |
|                 if obj.type == act_type:
 | |
|                     obj.hide_render = True
 | |
| 
 | |
|         return {'FINISHED'}
 | |
| 
 | |
| 
 | |
| class ClearAllRestrictRender(Operator):
 | |
|     """Reveal all render objects by setting the hide render flag"""
 | |
|     bl_idname = "object.hide_render_clear_all"
 | |
|     bl_label = "Clear All Restrict Render"
 | |
|     bl_options = {'REGISTER', 'UNDO'}
 | |
| 
 | |
|     def execute(self, context):
 | |
|         for obj in context.scene.objects:
 | |
|             obj.hide_render = False
 | |
|         return {'FINISHED'}
 | |
| 
 | |
| 
 | |
| class TransformsToDeltas(Operator):
 | |
|     """Convert normal object transforms to delta transforms, """ \
 | |
|         """any existing delta transforms will be included as well"""
 | |
|     bl_idname = "object.transforms_to_deltas"
 | |
|     bl_label = "Transforms to Deltas"
 | |
|     bl_options = {'REGISTER', 'UNDO'}
 | |
| 
 | |
|     mode: EnumProperty(
 | |
|         items=(
 | |
|             ('ALL', "All Transforms", "Transfer location, rotation, and scale transforms"),
 | |
|             ('LOC', "Location", "Transfer location transforms only"),
 | |
|             ('ROT', "Rotation", "Transfer rotation transforms only"),
 | |
|             ('SCALE', "Scale", "Transfer scale transforms only"),
 | |
|         ),
 | |
|         name="Mode",
 | |
|         description="Which transforms to transfer",
 | |
|         default='ALL',
 | |
|     )
 | |
|     reset_values: BoolProperty(
 | |
|         name="Reset Values",
 | |
|         description=("Clear transform values after transferring to deltas"),
 | |
|         default=True,
 | |
|     )
 | |
| 
 | |
|     @classmethod
 | |
|     def poll(cls, context):
 | |
|         obs = context.selected_editable_objects
 | |
|         return (obs is not None)
 | |
| 
 | |
|     def execute(self, context):
 | |
|         for obj in context.selected_editable_objects:
 | |
|             if self.mode in {'ALL', 'LOC'}:
 | |
|                 self.transfer_location(obj)
 | |
| 
 | |
|             if self.mode in {'ALL', 'ROT'}:
 | |
|                 self.transfer_rotation(obj)
 | |
| 
 | |
|             if self.mode in {'ALL', 'SCALE'}:
 | |
|                 self.transfer_scale(obj)
 | |
| 
 | |
|         return {'FINISHED'}
 | |
| 
 | |
|     def transfer_location(self, obj):
 | |
|         obj.delta_location += obj.location
 | |
| 
 | |
|         if self.reset_values:
 | |
|             obj.location.zero()
 | |
| 
 | |
|     def transfer_rotation(self, obj):
 | |
|         # TODO: add transforms together...
 | |
|         if obj.rotation_mode == 'QUATERNION':
 | |
|             delta = obj.delta_rotation_quaternion.copy()
 | |
|             obj.delta_rotation_quaternion = obj.rotation_quaternion
 | |
|             obj.delta_rotation_quaternion.rotate(delta)
 | |
| 
 | |
|             if self.reset_values:
 | |
|                 obj.rotation_quaternion.identity()
 | |
|         elif obj.rotation_mode == 'AXIS_ANGLE':
 | |
|             pass  # Unsupported
 | |
|         else:
 | |
|             delta = obj.delta_rotation_euler.copy()
 | |
|             obj.delta_rotation_euler = obj.rotation_euler
 | |
|             obj.delta_rotation_euler.rotate(delta)
 | |
| 
 | |
|             if self.reset_values:
 | |
|                 obj.rotation_euler.zero()
 | |
| 
 | |
|     def transfer_scale(self, obj):
 | |
|         obj.delta_scale[0] *= obj.scale[0]
 | |
|         obj.delta_scale[1] *= obj.scale[1]
 | |
|         obj.delta_scale[2] *= obj.scale[2]
 | |
| 
 | |
|         if self.reset_values:
 | |
|             obj.scale[:] = (1, 1, 1)
 | |
| 
 | |
| 
 | |
| class TransformsToDeltasAnim(Operator):
 | |
|     """Convert object animation for normal transforms to delta transforms"""
 | |
|     bl_idname = "object.anim_transforms_to_deltas"
 | |
|     bl_label = "Animated Transforms to Deltas"
 | |
|     bl_options = {'REGISTER', 'UNDO'}
 | |
| 
 | |
|     @classmethod
 | |
|     def poll(cls, context):
 | |
|         obs = context.selected_editable_objects
 | |
|         return (obs is not None)
 | |
| 
 | |
|     def execute(self, context):
 | |
|         # map from standard transform paths to "new" transform paths
 | |
|         STANDARD_TO_DELTA_PATHS = {
 | |
|             "location": "delta_location",
 | |
|             "rotation_euler": "delta_rotation_euler",
 | |
|             "rotation_quaternion": "delta_rotation_quaternion",
 | |
|             # "rotation_axis_angle" : "delta_rotation_axis_angle",
 | |
|             "scale": "delta_scale"
 | |
|         }
 | |
|         DELTA_PATHS = STANDARD_TO_DELTA_PATHS.values()
 | |
| 
 | |
|         # try to apply on each selected object
 | |
|         for obj in context.selected_editable_objects:
 | |
|             adt = obj.animation_data
 | |
|             if (adt is None) or (adt.action is None):
 | |
|                 self.report({'WARNING'},
 | |
|                             "No animation data to convert on object: %r" %
 | |
|                             obj.name)
 | |
|                 continue
 | |
| 
 | |
|             # first pass over F-Curves: ensure that we don't have conflicting
 | |
|             # transforms already (e.g. if this was applied already) [#29110]
 | |
|             existingFCurves = {}
 | |
|             for fcu in adt.action.fcurves:
 | |
|                 # get "delta" path - i.e. the final paths which may clash
 | |
|                 path = fcu.data_path
 | |
|                 if path in STANDARD_TO_DELTA_PATHS:
 | |
|                     # to be converted - conflicts may exist...
 | |
|                     dpath = STANDARD_TO_DELTA_PATHS[path]
 | |
|                 elif path in DELTA_PATHS:
 | |
|                     # already delta - check for conflicts...
 | |
|                     dpath = path
 | |
|                 else:
 | |
|                     # non-transform - ignore
 | |
|                     continue
 | |
| 
 | |
|                 # a delta path like this for the same index shouldn't
 | |
|                 # exist already, otherwise we've got a conflict
 | |
|                 if dpath in existingFCurves:
 | |
|                     # ensure that this index hasn't occurred before
 | |
|                     if fcu.array_index in existingFCurves[dpath]:
 | |
|                         # conflict
 | |
|                         self.report({'ERROR'},
 | |
|                                     "Object '%r' already has '%r' F-Curve(s). "
 | |
|                                     "Remove these before trying again" %
 | |
|                                     (obj.name, dpath))
 | |
|                         return {'CANCELLED'}
 | |
|                     else:
 | |
|                         # no conflict here
 | |
|                         existingFCurves[dpath] += [fcu.array_index]
 | |
|                 else:
 | |
|                     # no conflict yet
 | |
|                     existingFCurves[dpath] = [fcu.array_index]
 | |
| 
 | |
|             # if F-Curve uses standard transform path
 | |
|             # just append "delta_" to this path
 | |
|             for fcu in adt.action.fcurves:
 | |
|                 if fcu.data_path == "location":
 | |
|                     fcu.data_path = "delta_location"
 | |
|                     obj.location.zero()
 | |
|                 elif fcu.data_path == "rotation_euler":
 | |
|                     fcu.data_path = "delta_rotation_euler"
 | |
|                     obj.rotation_euler.zero()
 | |
|                 elif fcu.data_path == "rotation_quaternion":
 | |
|                     fcu.data_path = "delta_rotation_quaternion"
 | |
|                     obj.rotation_quaternion.identity()
 | |
|                 # XXX: currently not implemented
 | |
|                 # ~ elif fcu.data_path == "rotation_axis_angle":
 | |
|                 # ~    fcu.data_path = "delta_rotation_axis_angle"
 | |
|                 elif fcu.data_path == "scale":
 | |
|                     fcu.data_path = "delta_scale"
 | |
|                     obj.scale = 1.0, 1.0, 1.0
 | |
| 
 | |
|         # hack: force animsys flush by changing frame, so that deltas get run
 | |
|         context.scene.frame_set(context.scene.frame_current)
 | |
| 
 | |
|         return {'FINISHED'}
 | |
| 
 | |
| 
 | |
| class DupliOffsetFromCursor(Operator):
 | |
|     """Set offset used for collection instances based on cursor position"""
 | |
|     bl_idname = "object.instance_offset_from_cursor"
 | |
|     bl_label = "Set Offset From Cursor"
 | |
|     bl_options = {'INTERNAL', 'UNDO'}
 | |
| 
 | |
|     @classmethod
 | |
|     def poll(cls, context):
 | |
|         return (context.active_object is not None)
 | |
| 
 | |
|     def execute(self, context):
 | |
|         scene = context.scene
 | |
|         collection = context.collection
 | |
| 
 | |
|         collection.instance_offset = scene.cursor.location
 | |
| 
 | |
|         return {'FINISHED'}
 | |
| 
 | |
| 
 | |
| class LoadImageAsEmpty:
 | |
|     bl_options = {'REGISTER', 'UNDO'}
 | |
| 
 | |
|     filepath: StringProperty(
 | |
|         subtype='FILE_PATH'
 | |
|     )
 | |
| 
 | |
|     filter_image: BoolProperty(default=True, options={'HIDDEN', 'SKIP_SAVE'})
 | |
|     filter_folder: BoolProperty(default=True, options={'HIDDEN', 'SKIP_SAVE'})
 | |
| 
 | |
|     view_align: BoolProperty(
 | |
|         name="Align to view",
 | |
|         default=True,
 | |
|     )
 | |
| 
 | |
|     @classmethod
 | |
|     def poll(cls, context):
 | |
|         return context.mode == 'OBJECT'
 | |
| 
 | |
|     def invoke(self, context, _event):
 | |
|         context.window_manager.fileselect_add(self)
 | |
|         return {'RUNNING_MODAL'}
 | |
| 
 | |
|     def execute(self, context):
 | |
|         scene = context.scene
 | |
|         cursor = scene.cursor.location
 | |
| 
 | |
|         try:
 | |
|             image = bpy.data.images.load(self.filepath, check_existing=True)
 | |
|         except RuntimeError as ex:
 | |
|             self.report({'ERROR'}, str(ex))
 | |
|             return {'CANCELLED'}
 | |
| 
 | |
|         bpy.ops.object.empty_add(
 | |
|             'INVOKE_REGION_WIN',
 | |
|             type='IMAGE',
 | |
|             location=cursor,
 | |
|             align=('VIEW' if self.view_align else 'WORLD'),
 | |
|         )
 | |
| 
 | |
|         view_layer = context.view_layer
 | |
|         obj = view_layer.objects.active
 | |
|         obj.data = image
 | |
|         obj.empty_display_size = 5.0
 | |
|         self.set_settings(context, obj)
 | |
|         return {'FINISHED'}
 | |
| 
 | |
|     def set_settings(self, context, obj):
 | |
|         pass
 | |
| 
 | |
| 
 | |
| class LoadBackgroundImage(LoadImageAsEmpty, Operator):
 | |
|     """Add a reference image into the background behind objects"""
 | |
|     bl_idname = "object.load_background_image"
 | |
|     bl_label = "Load Background Image"
 | |
| 
 | |
|     def set_settings(self, context, obj):
 | |
|         obj.empty_image_depth = 'BACK'
 | |
|         obj.empty_image_side = 'FRONT'
 | |
| 
 | |
|         if context.space_data.type == 'VIEW_3D':
 | |
|             if not context.space_data.region_3d.is_perspective:
 | |
|                 obj.show_empty_image_perspective = False
 | |
| 
 | |
| 
 | |
| class LoadReferenceImage(LoadImageAsEmpty, Operator):
 | |
|     """Add a reference image into the scene between objects"""
 | |
|     bl_idname = "object.load_reference_image"
 | |
|     bl_label = "Load Reference Image"
 | |
| 
 | |
|     def set_settings(self, context, obj):
 | |
|         pass
 | |
| 
 | |
| 
 | |
| class OBJECT_OT_assign_property_defaults(Operator):
 | |
|     """Assign the current values of custom properties as their defaults, """ \
 | |
|     """for use as part of the rest pose state in NLA track mixing"""
 | |
|     bl_idname = "object.assign_property_defaults"
 | |
|     bl_label = "Assign Custom Property Values as Default"
 | |
|     bl_options = {'UNDO', 'REGISTER'}
 | |
| 
 | |
|     process_data: BoolProperty(name="Process data properties", default=True)
 | |
|     process_bones: BoolProperty(name="Process bone properties", default=True)
 | |
| 
 | |
|     @classmethod
 | |
|     def poll(cls, context):
 | |
|         obj = context.active_object
 | |
|         return obj is not None and obj.library is None and obj.mode in {'POSE', 'OBJECT'}
 | |
| 
 | |
|     @staticmethod
 | |
|     def assign_defaults(obj):
 | |
|         from rna_prop_ui import rna_idprop_ui_prop_default_set
 | |
| 
 | |
|         rna_properties = {'_RNA_UI'} | {prop.identifier for prop in obj.bl_rna.properties if prop.is_runtime}
 | |
| 
 | |
|         for prop, value in obj.items():
 | |
|             if prop not in rna_properties:
 | |
|                 rna_idprop_ui_prop_default_set(obj, prop, value)
 | |
| 
 | |
|     def execute(self, context):
 | |
|         obj = context.active_object
 | |
| 
 | |
|         self.assign_defaults(obj)
 | |
| 
 | |
|         if self.process_bones and obj.pose:
 | |
|             for pbone in obj.pose.bones:
 | |
|                 self.assign_defaults(pbone)
 | |
| 
 | |
|         if self.process_data and obj.data and obj.data.library is None:
 | |
|             self.assign_defaults(obj.data)
 | |
| 
 | |
|             if self.process_bones and isinstance(obj.data, bpy.types.Armature):
 | |
|                 for bone in obj.data.bones:
 | |
|                     self.assign_defaults(bone)
 | |
| 
 | |
|         return {'FINISHED'}
 | |
| 
 | |
| 
 | |
| classes = (
 | |
|     ClearAllRestrictRender,
 | |
|     DupliOffsetFromCursor,
 | |
|     IsolateTypeRender,
 | |
|     JoinUVs,
 | |
|     LoadBackgroundImage,
 | |
|     LoadReferenceImage,
 | |
|     MakeDupliFace,
 | |
|     SelectCamera,
 | |
|     SelectHierarchy,
 | |
|     SelectPattern,
 | |
|     ShapeTransfer,
 | |
|     SubdivisionSet,
 | |
|     TransformsToDeltas,
 | |
|     TransformsToDeltasAnim,
 | |
|     OBJECT_OT_assign_property_defaults,
 | |
| )
 |