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