123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011 |
- # ##### 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:
- if obj_act == obj:
- act_new = parent
- select_new.append(parent)
- else:
- for obj in selected_objects:
- select_new.extend(obj.children)
- 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 subsurf 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)
- 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':
- obj.delta_rotation_quaternion += obj.rotation_quaternion
- 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'),
- )
- obj = context.active_object
- 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,
- )
|