Cleanup: style
[blender.git] / release / scripts / startup / bl_operators / object.py
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
21 import bpy
22 from bpy.types import Operator
23 from bpy.props import (
24     BoolProperty,
25     EnumProperty,
26     IntProperty,
27     StringProperty,
28 )
29
30
31 class 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
120 class 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
156 class 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:
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(obj.children)
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
225 class 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 subsurf 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
314 class ShapeTransfer(Operator):
315     """Copy another selected objects active shape to this one by """ \
316         """applying the relative offsets"""
317
318     bl_idname = "object.shape_key_transfer"
319     bl_label = "Transfer Shape Key"
320     bl_options = {'REGISTER', 'UNDO'}
321
322     mode: EnumProperty(
323         items=(
324             ('OFFSET',
325              "Offset",
326              "Apply the relative positional offset",
327              ),
328             ('RELATIVE_FACE',
329              "Relative Face",
330              "Calculate relative position (using faces)",
331              ),
332             ('RELATIVE_EDGE',
333              "Relative Edge",
334              "Calculate relative position (using edges)",
335              ),
336         ),
337         name="Transformation Mode",
338         description="Relative shape positions to the new shape method",
339         default='OFFSET',
340     )
341     use_clamp: BoolProperty(
342         name="Clamp Offset",
343         description=("Clamp the transformation to the distance each "
344                      "vertex moves in the original shape"),
345         default=False,
346     )
347
348     def _main(self, ob_act, objects, mode='OFFSET', use_clamp=False):
349
350         def me_nos(verts):
351             return [v.normal.copy() for v in verts]
352
353         def me_cos(verts):
354             return [v.co.copy() for v in verts]
355
356         def ob_add_shape(ob, name):
357             me = ob.data
358             key = ob.shape_key_add(from_mix=False)
359             if len(me.shape_keys.key_blocks) == 1:
360                 key.name = "Basis"
361                 key = ob.shape_key_add(from_mix=False)  # we need a rest
362             key.name = name
363             ob.active_shape_key_index = len(me.shape_keys.key_blocks) - 1
364             ob.show_only_shape_key = True
365
366         from mathutils.geometry import barycentric_transform
367         from mathutils import Vector
368
369         if use_clamp and mode == 'OFFSET':
370             use_clamp = False
371
372         me = ob_act.data
373         orig_key_name = ob_act.active_shape_key.name
374
375         orig_shape_coords = me_cos(ob_act.active_shape_key.data)
376
377         orig_normals = me_nos(me.vertices)
378         # actual mesh vertex location isn't as reliable as the base shape :S
379         # orig_coords = me_cos(me.vertices)
380         orig_coords = me_cos(me.shape_keys.key_blocks[0].data)
381
382         for ob_other in objects:
383             if ob_other.type != 'MESH':
384                 self.report({'WARNING'},
385                             ("Skipping '%s', "
386                              "not a mesh") % ob_other.name)
387                 continue
388             me_other = ob_other.data
389             if len(me_other.vertices) != len(me.vertices):
390                 self.report({'WARNING'},
391                             ("Skipping '%s', "
392                              "vertex count differs") % ob_other.name)
393                 continue
394
395             target_normals = me_nos(me_other.vertices)
396             if me_other.shape_keys:
397                 target_coords = me_cos(me_other.shape_keys.key_blocks[0].data)
398             else:
399                 target_coords = me_cos(me_other.vertices)
400
401             ob_add_shape(ob_other, orig_key_name)
402
403             # editing the final coords, only list that stores wrapped coords
404             target_shape_coords = [v.co for v in
405                                    ob_other.active_shape_key.data]
406
407             median_coords = [[] for i in range(len(me.vertices))]
408
409             # Method 1, edge
410             if mode == 'OFFSET':
411                 for i, vert_cos in enumerate(median_coords):
412                     vert_cos.append(target_coords[i] +
413                                     (orig_shape_coords[i] - orig_coords[i]))
414
415             elif mode == 'RELATIVE_FACE':
416                 for poly in me.polygons:
417                     idxs = poly.vertices[:]
418                     v_before = idxs[-2]
419                     v = idxs[-1]
420                     for v_after in idxs:
421                         pt = barycentric_transform(orig_shape_coords[v],
422                                                    orig_coords[v_before],
423                                                    orig_coords[v],
424                                                    orig_coords[v_after],
425                                                    target_coords[v_before],
426                                                    target_coords[v],
427                                                    target_coords[v_after],
428                                                    )
429                         median_coords[v].append(pt)
430                         v_before = v
431                         v = v_after
432
433             elif mode == 'RELATIVE_EDGE':
434                 for ed in me.edges:
435                     i1, i2 = ed.vertices
436                     v1, v2 = orig_coords[i1], orig_coords[i2]
437                     edge_length = (v1 - v2).length
438                     n1loc = v1 + orig_normals[i1] * edge_length
439                     n2loc = v2 + orig_normals[i2] * edge_length
440
441                     # now get the target nloc's
442                     v1_to, v2_to = target_coords[i1], target_coords[i2]
443                     edlen_to = (v1_to - v2_to).length
444                     n1loc_to = v1_to + target_normals[i1] * edlen_to
445                     n2loc_to = v2_to + target_normals[i2] * edlen_to
446
447                     pt = barycentric_transform(orig_shape_coords[i1],
448                                                v2, v1, n1loc,
449                                                v2_to, v1_to, n1loc_to)
450                     median_coords[i1].append(pt)
451
452                     pt = barycentric_transform(orig_shape_coords[i2],
453                                                v1, v2, n2loc,
454                                                v1_to, v2_to, n2loc_to)
455                     median_coords[i2].append(pt)
456
457             # apply the offsets to the new shape
458             from functools import reduce
459             VectorAdd = Vector.__add__
460
461             for i, vert_cos in enumerate(median_coords):
462                 if vert_cos:
463                     co = reduce(VectorAdd, vert_cos) / len(vert_cos)
464
465                     if use_clamp:
466                         # clamp to the same movement as the original
467                         # breaks copy between different scaled meshes.
468                         len_from = (orig_shape_coords[i] -
469                                     orig_coords[i]).length
470                         ofs = co - target_coords[i]
471                         ofs.length = len_from
472                         co = target_coords[i] + ofs
473
474                     target_shape_coords[i][:] = co
475
476         return {'FINISHED'}
477
478     @classmethod
479     def poll(cls, context):
480         obj = context.active_object
481         return (obj and obj.mode != 'EDIT')
482
483     def execute(self, context):
484         ob_act = context.active_object
485         objects = [ob for ob in context.selected_editable_objects
486                    if ob != ob_act]
487
488         if 1:  # swap from/to, means we can't copy to many at once.
489             if len(objects) != 1:
490                 self.report({'ERROR'},
491                             ("Expected one other selected "
492                              "mesh object to copy from"))
493
494                 return {'CANCELLED'}
495             ob_act, objects = objects[0], [ob_act]
496
497         if ob_act.type != 'MESH':
498             self.report({'ERROR'}, "Other object is not a mesh")
499             return {'CANCELLED'}
500
501         if ob_act.active_shape_key is None:
502             self.report({'ERROR'}, "Other object has no shape key")
503             return {'CANCELLED'}
504         return self._main(ob_act, objects, self.mode, self.use_clamp)
505
506
507 class JoinUVs(Operator):
508     """Transfer UV Maps from active to selected objects """ \
509         """(needs matching geometry)"""
510     bl_idname = "object.join_uvs"
511     bl_label = "Transfer UV Maps"
512     bl_options = {'REGISTER', 'UNDO'}
513
514     @classmethod
515     def poll(cls, context):
516         obj = context.active_object
517         return (obj and obj.type == 'MESH')
518
519     def _main(self, context):
520         import array
521         obj = context.active_object
522         mesh = obj.data
523
524         is_editmode = (obj.mode == 'EDIT')
525         if is_editmode:
526             bpy.ops.object.mode_set(mode='OBJECT', toggle=False)
527
528         if not mesh.uv_layers:
529             self.report({'WARNING'},
530                         "Object: %s, Mesh: '%s' has no UVs"
531                         % (obj.name, mesh.name))
532         else:
533             nbr_loops = len(mesh.loops)
534
535             # seems to be the fastest way to create an array
536             uv_array = array.array('f', [0.0] * 2) * nbr_loops
537             mesh.uv_layers.active.data.foreach_get("uv", uv_array)
538
539             objects = context.selected_editable_objects[:]
540
541             for obj_other in objects:
542                 if obj_other.type == 'MESH':
543                     obj_other.data.tag = False
544
545             for obj_other in objects:
546                 if obj_other != obj and obj_other.type == 'MESH':
547                     mesh_other = obj_other.data
548                     if mesh_other != mesh:
549                         if mesh_other.tag is False:
550                             mesh_other.tag = True
551
552                             if len(mesh_other.loops) != nbr_loops:
553                                 self.report({'WARNING'}, "Object: %s, Mesh: "
554                                             "'%s' has %d loops (for %d faces),"
555                                             " expected %d\n"
556                                             % (obj_other.name,
557                                                mesh_other.name,
558                                                len(mesh_other.loops),
559                                                len(mesh_other.polygons),
560                                                nbr_loops,
561                                                ),
562                                             )
563                             else:
564                                 uv_other = mesh_other.uv_layers.active
565                                 if not uv_other:
566                                     mesh_other.uv_layers.new()
567                                     uv_other = mesh_other.uv_layers.active
568                                     if not uv_other:
569                                         self.report({'ERROR'}, "Could not add "
570                                                     "a new UV map tp object "
571                                                     "'%s' (Mesh '%s')\n"
572                                                     % (obj_other.name,
573                                                        mesh_other.name,
574                                                        ),
575                                                     )
576
577                                 # finally do the copy
578                                 uv_other.data.foreach_set("uv", uv_array)
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
588 class MakeDupliFace(Operator):
589     """Convert objects into dupli-face instanced"""
590     bl_idname = "object.make_dupli_face"
591     bl_label = "Make Dupli-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         scene = context.scene
614         linked = defaultdict(list)
615         for obj in context.selected_objects:
616             if obj.type == 'MESH':
617                 linked[obj.data].append(obj)
618
619         for data, objects in linked.items():
620             face_verts = [axis for obj in objects
621                           for v in matrix_to_quad(obj.matrix_world)
622                           for axis in v]
623             nbr_verts = len(face_verts) // 3
624             nbr_faces = nbr_verts // 4
625
626             faces = list(range(nbr_verts))
627
628             mesh = bpy.data.meshes.new(data.name + "_dupli")
629
630             mesh.vertices.add(nbr_verts)
631             mesh.loops.add(nbr_faces * 4)  # Safer than nbr_verts.
632             mesh.polygons.add(nbr_faces)
633
634             mesh.vertices.foreach_set("co", face_verts)
635             mesh.loops.foreach_set("vertex_index", faces)
636             mesh.polygons.foreach_set("loop_start", range(0, nbr_faces * 4, 4))
637             mesh.polygons.foreach_set("loop_total", (4,) * nbr_faces)
638             mesh.update()  # generates edge data
639
640             ob_new = bpy.data.objects.new(mesh.name, mesh)
641             context.collection.objects.link(ob_new)
642
643             ob_inst = bpy.data.objects.new(data.name, data)
644             context.collection.objects.link(ob_inst)
645
646             ob_new.instance_type = 'FACES'
647             ob_inst.parent = ob_new
648             ob_new.use_instance_faces_scale = True
649             ob_new.instance_faces_scale = 1.0 / SCALE_FAC
650
651             ob_inst.select_set(True)
652             ob_new.select_set(True)
653
654             for obj in objects:
655                 for collection in obj.users_collection:
656                     collection.objects.unlink(obj)
657
658     def execute(self, context):
659         self._main(context)
660         return {'FINISHED'}
661
662
663 class IsolateTypeRender(Operator):
664     """Hide unselected render objects of same type as active """ \
665         """by setting the hide render flag"""
666     bl_idname = "object.isolate_type_render"
667     bl_label = "Restrict Render Unselected"
668     bl_options = {'REGISTER', 'UNDO'}
669
670     def execute(self, context):
671         act_type = context.object.type
672
673         for obj in context.visible_objects:
674
675             if obj.select_get():
676                 obj.hide_render = False
677             else:
678                 if obj.type == act_type:
679                     obj.hide_render = True
680
681         return {'FINISHED'}
682
683
684 class ClearAllRestrictRender(Operator):
685     """Reveal all render objects by setting the hide render flag"""
686     bl_idname = "object.hide_render_clear_all"
687     bl_label = "Clear All Restrict Render"
688     bl_options = {'REGISTER', 'UNDO'}
689
690     def execute(self, context):
691         for obj in context.scene.objects:
692             obj.hide_render = False
693         return {'FINISHED'}
694
695
696 class TransformsToDeltas(Operator):
697     """Convert normal object transforms to delta transforms, """ \
698         """any existing delta transforms will be included as well"""
699     bl_idname = "object.transforms_to_deltas"
700     bl_label = "Transforms to Deltas"
701     bl_options = {'REGISTER', 'UNDO'}
702
703     mode: EnumProperty(
704         items=(
705             ('ALL', "All Transforms", "Transfer location, rotation, and scale transforms"),
706             ('LOC', "Location", "Transfer location transforms only"),
707             ('ROT', "Rotation", "Transfer rotation transforms only"),
708             ('SCALE', "Scale", "Transfer scale transforms only"),
709         ),
710         name="Mode",
711         description="Which transforms to transfer",
712         default='ALL',
713     )
714     reset_values: BoolProperty(
715         name="Reset Values",
716         description=("Clear transform values after transferring to deltas"),
717         default=True,
718     )
719
720     @classmethod
721     def poll(cls, context):
722         obs = context.selected_editable_objects
723         return (obs is not None)
724
725     def execute(self, context):
726         for obj in context.selected_editable_objects:
727             if self.mode in {'ALL', 'LOC'}:
728                 self.transfer_location(obj)
729
730             if self.mode in {'ALL', 'ROT'}:
731                 self.transfer_rotation(obj)
732
733             if self.mode in {'ALL', 'SCALE'}:
734                 self.transfer_scale(obj)
735
736         return {'FINISHED'}
737
738     def transfer_location(self, obj):
739         obj.delta_location += obj.location
740
741         if self.reset_values:
742             obj.location.zero()
743
744     def transfer_rotation(self, obj):
745         # TODO: add transforms together...
746         if obj.rotation_mode == 'QUATERNION':
747             obj.delta_rotation_quaternion += obj.rotation_quaternion
748
749             if self.reset_values:
750                 obj.rotation_quaternion.identity()
751         elif obj.rotation_mode == 'AXIS_ANGLE':
752             pass  # Unsupported
753         else:
754             delta = obj.delta_rotation_euler.copy()
755             obj.delta_rotation_euler = obj.rotation_euler
756             obj.delta_rotation_euler.rotate(delta)
757
758             if self.reset_values:
759                 obj.rotation_euler.zero()
760
761     def transfer_scale(self, obj):
762         obj.delta_scale[0] *= obj.scale[0]
763         obj.delta_scale[1] *= obj.scale[1]
764         obj.delta_scale[2] *= obj.scale[2]
765
766         if self.reset_values:
767             obj.scale[:] = (1, 1, 1)
768
769
770 class TransformsToDeltasAnim(Operator):
771     """Convert object animation for normal transforms to delta transforms"""
772     bl_idname = "object.anim_transforms_to_deltas"
773     bl_label = "Animated Transforms to Deltas"
774     bl_options = {'REGISTER', 'UNDO'}
775
776     @classmethod
777     def poll(cls, context):
778         obs = context.selected_editable_objects
779         return (obs is not None)
780
781     def execute(self, context):
782         # map from standard transform paths to "new" transform paths
783         STANDARD_TO_DELTA_PATHS = {
784             "location": "delta_location",
785             "rotation_euler": "delta_rotation_euler",
786             "rotation_quaternion": "delta_rotation_quaternion",
787             # "rotation_axis_angle" : "delta_rotation_axis_angle",
788             "scale": "delta_scale"
789         }
790         DELTA_PATHS = STANDARD_TO_DELTA_PATHS.values()
791
792         # try to apply on each selected object
793         for obj in context.selected_editable_objects:
794             adt = obj.animation_data
795             if (adt is None) or (adt.action is None):
796                 self.report({'WARNING'},
797                             "No animation data to convert on object: %r" %
798                             obj.name)
799                 continue
800
801             # first pass over F-Curves: ensure that we don't have conflicting
802             # transforms already (e.g. if this was applied already) [#29110]
803             existingFCurves = {}
804             for fcu in adt.action.fcurves:
805                 # get "delta" path - i.e. the final paths which may clash
806                 path = fcu.data_path
807                 if path in STANDARD_TO_DELTA_PATHS:
808                     # to be converted - conflicts may exist...
809                     dpath = STANDARD_TO_DELTA_PATHS[path]
810                 elif path in DELTA_PATHS:
811                     # already delta - check for conflicts...
812                     dpath = path
813                 else:
814                     # non-transform - ignore
815                     continue
816
817                 # a delta path like this for the same index shouldn't
818                 # exist already, otherwise we've got a conflict
819                 if dpath in existingFCurves:
820                     # ensure that this index hasn't occurred before
821                     if fcu.array_index in existingFCurves[dpath]:
822                         # conflict
823                         self.report({'ERROR'},
824                                     "Object '%r' already has '%r' F-Curve(s). "
825                                     "Remove these before trying again" %
826                                     (obj.name, dpath))
827                         return {'CANCELLED'}
828                     else:
829                         # no conflict here
830                         existingFCurves[dpath] += [fcu.array_index]
831                 else:
832                     # no conflict yet
833                     existingFCurves[dpath] = [fcu.array_index]
834
835             # if F-Curve uses standard transform path
836             # just append "delta_" to this path
837             for fcu in adt.action.fcurves:
838                 if fcu.data_path == "location":
839                     fcu.data_path = "delta_location"
840                     obj.location.zero()
841                 elif fcu.data_path == "rotation_euler":
842                     fcu.data_path = "delta_rotation_euler"
843                     obj.rotation_euler.zero()
844                 elif fcu.data_path == "rotation_quaternion":
845                     fcu.data_path = "delta_rotation_quaternion"
846                     obj.rotation_quaternion.identity()
847                 # XXX: currently not implemented
848                 # ~ elif fcu.data_path == "rotation_axis_angle":
849                 # ~    fcu.data_path = "delta_rotation_axis_angle"
850                 elif fcu.data_path == "scale":
851                     fcu.data_path = "delta_scale"
852                     obj.scale = 1.0, 1.0, 1.0
853
854         # hack: force animsys flush by changing frame, so that deltas get run
855         context.scene.frame_set(context.scene.frame_current)
856
857         return {'FINISHED'}
858
859
860 class DupliOffsetFromCursor(Operator):
861     """Set offset used for collection instances based on cursor position"""
862     bl_idname = "object.instance_offset_from_cursor"
863     bl_label = "Set Offset From Cursor"
864     bl_options = {'INTERNAL', 'UNDO'}
865
866     @classmethod
867     def poll(cls, context):
868         return (context.active_object is not None)
869
870     def execute(self, context):
871         scene = context.scene
872         collection = context.collection
873
874         collection.instance_offset = scene.cursor.location
875
876         return {'FINISHED'}
877
878
879 class LoadImageAsEmpty:
880     bl_options = {'REGISTER', 'UNDO'}
881
882     filepath: StringProperty(
883         subtype='FILE_PATH'
884     )
885
886     filter_image: BoolProperty(default=True, options={'HIDDEN', 'SKIP_SAVE'})
887     filter_folder: BoolProperty(default=True, options={'HIDDEN', 'SKIP_SAVE'})
888
889     view_align: BoolProperty(
890         name="Align to view",
891         default=True
892     )
893
894     @classmethod
895     def poll(cls, context):
896         return context.mode == 'OBJECT'
897
898     def invoke(self, context, event):
899         context.window_manager.fileselect_add(self)
900         return {'RUNNING_MODAL'}
901
902     def execute(self, context):
903         scene = context.scene
904         cursor = scene.cursor.location
905
906         try:
907             image = bpy.data.images.load(self.filepath, check_existing=True)
908         except RuntimeError as ex:
909             self.report({'ERROR'}, str(ex))
910             return {'CANCELLED'}
911
912         bpy.ops.object.empty_add(
913             'INVOKE_REGION_WIN',
914             type='IMAGE',
915             location=cursor,
916             view_align=self.view_align,
917         )
918
919         obj = context.active_object
920         obj.data = image
921         obj.empty_display_size = 5.0
922         self.set_settings(context, obj)
923         return {'FINISHED'}
924
925     def set_settings(self, context, obj):
926         pass
927
928
929 class LoadBackgroundImage(LoadImageAsEmpty, Operator):
930     """Add a reference image into the background behind objects"""
931     bl_idname = "object.load_background_image"
932     bl_label = "Load Background Image"
933
934     def set_settings(self, context, obj):
935         obj.empty_image_depth = 'BACK'
936         obj.empty_image_side = 'FRONT'
937
938         if context.space_data.type == 'VIEW_3D':
939             if not context.space_data.region_3d.is_perspective:
940                 obj.show_empty_image_perspective = False
941
942
943 class LoadReferenceImage(LoadImageAsEmpty, Operator):
944     """Add a reference image into the scene between objects"""
945     bl_idname = "object.load_reference_image"
946     bl_label = "Load Reference Image"
947
948     def set_settings(self, context, obj):
949         pass
950
951
952 class OBJECT_OT_assign_property_defaults(Operator):
953     """Assign the current values of custom properties as their defaults, """ \
954     """for use as part of the rest pose state in NLA track mixing"""
955     bl_idname = "object.assign_property_defaults"
956     bl_label = "Assign Custom Property Values as Default"
957     bl_options = {'UNDO', 'REGISTER'}
958
959     process_data: BoolProperty(name="Process data properties", default=True)
960     process_bones: BoolProperty(name="Process bone properties", default=True)
961
962     @classmethod
963     def poll(cls, context):
964         obj = context.active_object
965         return obj is not None and obj.library is None and obj.mode in {'POSE', 'OBJECT'}
966
967     @staticmethod
968     def assign_defaults(obj):
969         from rna_prop_ui import rna_idprop_ui_prop_default_set
970
971         rna_properties = {'_RNA_UI'} | {prop.identifier for prop in obj.bl_rna.properties if prop.is_runtime}
972
973         for prop, value in obj.items():
974             if prop not in rna_properties:
975                 rna_idprop_ui_prop_default_set(obj, prop, value)
976
977     def execute(self, context):
978         obj = context.active_object
979
980         self.assign_defaults(obj)
981
982         if self.process_bones and obj.pose:
983             for pbone in obj.pose.bones:
984                 self.assign_defaults(pbone)
985
986         if self.process_data and obj.data and obj.data.library is None:
987             self.assign_defaults(obj.data)
988
989             if self.process_bones and isinstance(obj.data, bpy.types.Armature):
990                 for bone in obj.data.bones:
991                     self.assign_defaults(bone)
992
993         return {'FINISHED'}
994
995
996 classes = (
997     ClearAllRestrictRender,
998     DupliOffsetFromCursor,
999     IsolateTypeRender,
1000     JoinUVs,
1001     LoadBackgroundImage,
1002     LoadReferenceImage,
1003     MakeDupliFace,
1004     SelectCamera,
1005     SelectHierarchy,
1006     SelectPattern,
1007     ShapeTransfer,
1008     SubdivisionSet,
1009     TransformsToDeltas,
1010     TransformsToDeltasAnim,
1011     OBJECT_OT_assign_property_defaults,
1012 )