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