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,
 | 
						|
)
 |