Code cleanup: unused python vars & imports
[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                             if obj.mode == 'SCULPT':
253                                 if mod.sculpt_levels != level:
254                                     mod.sculpt_levels = level
255                             elif obj.mode == 'OBJECT':
256                                 if mod.levels != level:
257                                     mod.levels = level
258                         return
259                     else:
260                         if obj.mode == 'SCULPT':
261                             if mod.sculpt_levels + level <= mod.total_levels:
262                                 mod.sculpt_levels += level
263                         elif obj.mode == 'OBJECT':
264                             if mod.levels + level <= mod.total_levels:
265                                 mod.levels += level
266                         return
267
268                 elif mod.type == 'SUBSURF':
269                     if relative:
270                         mod.levels += level
271                     else:
272                         if mod.levels != level:
273                             mod.levels = level
274
275                     return
276
277             # add a new modifier
278             try:
279                 mod = obj.modifiers.new("Subsurf", 'SUBSURF')
280                 mod.levels = level
281             except:
282                 self.report({'WARNING'},
283                             "Modifiers cannot be added to object: " + obj.name)
284
285         for obj in context.selected_editable_objects:
286             set_object_subd(obj)
287
288         return {'FINISHED'}
289
290
291 class ShapeTransfer(Operator):
292     """Copy another selected objects active shape to this one by """ \
293     """applying the relative offsets"""
294
295     bl_idname = "object.shape_key_transfer"
296     bl_label = "Transfer Shape Key"
297     bl_options = {'REGISTER', 'UNDO'}
298
299     mode = EnumProperty(
300             items=(('OFFSET',
301                     "Offset",
302                     "Apply the relative positional offset",
303                     ),
304                    ('RELATIVE_FACE',
305                     "Relative Face",
306                     "Calculate relative position (using faces)",
307                     ),
308                    ('RELATIVE_EDGE',
309                    "Relative Edge",
310                    "Calculate relative position (using edges)",
311                     ),
312                    ),
313             name="Transformation Mode",
314             description="Relative shape positions to the new shape method",
315             default='OFFSET',
316             )
317     use_clamp = BoolProperty(
318             name="Clamp Offset",
319             description=("Clamp the transformation to the distance each "
320                          "vertex moves in the original shape"),
321             default=False,
322             )
323
324     def _main(self, ob_act, objects, mode='OFFSET', use_clamp=False):
325
326         def me_nos(verts):
327             return [v.normal.copy() for v in verts]
328
329         def me_cos(verts):
330             return [v.co.copy() for v in verts]
331
332         def ob_add_shape(ob, name):
333             me = ob.data
334             key = ob.shape_key_add(from_mix=False)
335             if len(me.shape_keys.key_blocks) == 1:
336                 key.name = "Basis"
337                 key = ob.shape_key_add(from_mix=False)  # we need a rest
338             key.name = name
339             ob.active_shape_key_index = len(me.shape_keys.key_blocks) - 1
340             ob.show_only_shape_key = True
341
342         from mathutils.geometry import barycentric_transform
343         from mathutils import Vector
344
345         if use_clamp and mode == 'OFFSET':
346             use_clamp = False
347
348         me = ob_act.data
349         orig_key_name = ob_act.active_shape_key.name
350
351         orig_shape_coords = me_cos(ob_act.active_shape_key.data)
352
353         orig_normals = me_nos(me.vertices)
354         # actual mesh vertex location isn't as reliable as the base shape :S
355         #~ orig_coords = me_cos(me.vertices)
356         orig_coords = me_cos(me.shape_keys.key_blocks[0].data)
357
358         for ob_other in objects:
359             me_other = ob_other.data
360             if len(me_other.vertices) != len(me.vertices):
361                 self.report({'WARNING'},
362                             ("Skipping '%s', "
363                              "vertex count differs") % ob_other.name)
364                 continue
365
366             target_normals = me_nos(me_other.vertices)
367             if me_other.shape_keys:
368                 target_coords = me_cos(me_other.shape_keys.key_blocks[0].data)
369             else:
370                 target_coords = me_cos(me_other.vertices)
371
372             ob_add_shape(ob_other, orig_key_name)
373
374             # editing the final coords, only list that stores wrapped coords
375             target_shape_coords = [v.co for v in
376                                    ob_other.active_shape_key.data]
377
378             median_coords = [[] for i in range(len(me.vertices))]
379
380             # Method 1, edge
381             if mode == 'OFFSET':
382                 for i, vert_cos in enumerate(median_coords):
383                     vert_cos.append(target_coords[i] +
384                                     (orig_shape_coords[i] - orig_coords[i]))
385
386             elif mode == 'RELATIVE_FACE':
387                 for poly in me.polygons:
388                     idxs = poly.vertices[:]
389                     v_before = idxs[-2]
390                     v = idxs[-1]
391                     for v_after in idxs:
392                         pt = barycentric_transform(orig_shape_coords[v],
393                                                    orig_coords[v_before],
394                                                    orig_coords[v],
395                                                    orig_coords[v_after],
396                                                    target_coords[v_before],
397                                                    target_coords[v],
398                                                    target_coords[v_after],
399                                                    )
400                         median_coords[v].append(pt)
401                         v_before = v
402                         v = v_after
403
404             elif mode == 'RELATIVE_EDGE':
405                 for ed in me.edges:
406                     i1, i2 = ed.vertices
407                     v1, v2 = orig_coords[i1], orig_coords[i2]
408                     edge_length = (v1 - v2).length
409                     n1loc = v1 + orig_normals[i1] * edge_length
410                     n2loc = v2 + orig_normals[i2] * edge_length
411
412                     # now get the target nloc's
413                     v1_to, v2_to = target_coords[i1], target_coords[i2]
414                     edlen_to = (v1_to - v2_to).length
415                     n1loc_to = v1_to + target_normals[i1] * edlen_to
416                     n2loc_to = v2_to + target_normals[i2] * edlen_to
417
418                     pt = barycentric_transform(orig_shape_coords[i1],
419                                                v2, v1, n1loc,
420                                                v2_to, v1_to, n1loc_to)
421                     median_coords[i1].append(pt)
422
423                     pt = barycentric_transform(orig_shape_coords[i2],
424                                                v1, v2, n2loc,
425                                                v1_to, v2_to, n2loc_to)
426                     median_coords[i2].append(pt)
427
428             # apply the offsets to the new shape
429             from functools import reduce
430             VectorAdd = Vector.__add__
431
432             for i, vert_cos in enumerate(median_coords):
433                 if vert_cos:
434                     co = reduce(VectorAdd, vert_cos) / len(vert_cos)
435
436                     if use_clamp:
437                         # clamp to the same movement as the original
438                         # breaks copy between different scaled meshes.
439                         len_from = (orig_shape_coords[i] -
440                                     orig_coords[i]).length
441                         ofs = co - target_coords[i]
442                         ofs.length = len_from
443                         co = target_coords[i] + ofs
444
445                     target_shape_coords[i][:] = co
446
447         return {'FINISHED'}
448
449     @classmethod
450     def poll(cls, context):
451         obj = context.active_object
452         return (obj and obj.mode != 'EDIT')
453
454     def execute(self, context):
455         ob_act = context.active_object
456         objects = [ob for ob in context.selected_editable_objects
457                    if ob != ob_act]
458
459         if 1:  # swap from/to, means we cant copy to many at once.
460             if len(objects) != 1:
461                 self.report({'ERROR'},
462                             ("Expected one other selected "
463                              "mesh object to copy from"))
464
465                 return {'CANCELLED'}
466             ob_act, objects = objects[0], [ob_act]
467
468         if ob_act.type != 'MESH':
469             self.report({'ERROR'}, "Other object is not a mesh")
470             return {'CANCELLED'}
471
472         if ob_act.active_shape_key is None:
473             self.report({'ERROR'}, "Other object has no shape key")
474             return {'CANCELLED'}
475         return self._main(ob_act, objects, self.mode, self.use_clamp)
476
477
478 class JoinUVs(Operator):
479     """Transfer UV Maps from active to selected objects """ \
480     """(needs matching geometry)"""
481     bl_idname = "object.join_uvs"
482     bl_label = "Transfer UV Maps"
483     bl_options = {'REGISTER', 'UNDO'}
484
485     @classmethod
486     def poll(cls, context):
487         obj = context.active_object
488         return (obj and obj.type == 'MESH')
489
490     def _main(self, context):
491         import array
492         obj = context.active_object
493         mesh = obj.data
494
495         is_editmode = (obj.mode == 'EDIT')
496         if is_editmode:
497             bpy.ops.object.mode_set(mode='OBJECT', toggle=False)
498
499         if not mesh.uv_textures:
500             self.report({'WARNING'},
501                         "Object: %s, Mesh: '%s' has no UVs"
502                         % (obj.name, mesh.name))
503         else:
504             nbr_loops = len(mesh.loops)
505
506             # seems to be the fastest way to create an array
507             uv_array = array.array('f', [0.0] * 2) * nbr_loops
508             mesh.uv_layers.active.data.foreach_get("uv", uv_array)
509
510             objects = context.selected_editable_objects[:]
511
512             for obj_other in objects:
513                 if obj_other.type == 'MESH':
514                     obj_other.data.tag = False
515
516             for obj_other in objects:
517                 if obj_other != obj and obj_other.type == 'MESH':
518                     mesh_other = obj_other.data
519                     if mesh_other != mesh:
520                         if mesh_other.tag is False:
521                             mesh_other.tag = True
522
523                             if len(mesh_other.loops) != nbr_loops:
524                                 self.report({'WARNING'}, "Object: %s, Mesh: "
525                                             "'%s' has %d loops (for %d faces),"
526                                             " expected %d\n"
527                                             % (obj_other.name,
528                                                mesh_other.name,
529                                                len(mesh_other.loops),
530                                                len(mesh_other.polygons),
531                                                nbr_loops,
532                                                ),
533                                             )
534                             else:
535                                 uv_other = mesh_other.uv_layers.active
536                                 if not uv_other:
537                                     mesh_other.uv_textures.new()
538                                     uv_other = mesh_other.uv_layers.active
539                                     if not uv_other:
540                                         self.report({'ERROR'}, "Could not add "
541                                                     "a new UV map tp object "
542                                                     "'%s' (Mesh '%s')\n"
543                                                     % (obj_other.name,
544                                                        mesh_other.name,
545                                                        ),
546                                                     )
547
548                                 # finally do the copy
549                                 uv_other.data.foreach_set("uv", uv_array)
550
551         if is_editmode:
552             bpy.ops.object.mode_set(mode='EDIT', toggle=False)
553
554     def execute(self, context):
555         self._main(context)
556         return {'FINISHED'}
557
558
559 class MakeDupliFace(Operator):
560     """Convert objects into dupli-face instanced"""
561     bl_idname = "object.make_dupli_face"
562     bl_label = "Make Dupli-Face"
563     bl_options = {'REGISTER', 'UNDO'}
564
565     def _main(self, context):
566         from mathutils import Vector
567
568         SCALE_FAC = 0.01
569         offset = 0.5 * SCALE_FAC
570         base_tri = (Vector((-offset, -offset, 0.0)),
571                     Vector((+offset, -offset, 0.0)),
572                     Vector((+offset, +offset, 0.0)),
573                     Vector((-offset, +offset, 0.0)),
574                     )
575
576         def matrix_to_quad(matrix):
577             # scale = matrix.median_scale
578             trans = matrix.to_translation()
579             rot = matrix.to_3x3()  # also contains scale
580
581             return [(rot * b) + trans for b in base_tri]
582         scene = context.scene
583         linked = {}
584         for obj in context.selected_objects:
585             data = obj.data
586             if data:
587                 linked.setdefault(data, []).append(obj)
588
589         for data, objects in linked.items():
590             face_verts = [axis for obj in objects
591                           for v in matrix_to_quad(obj.matrix_world)
592                           for axis in v]
593             nbr_verts = len(face_verts) // 3
594             nbr_faces = nbr_verts // 4
595
596             faces = list(range(nbr_verts))
597
598             mesh = bpy.data.meshes.new(data.name + "_dupli")
599
600             mesh.vertices.add(nbr_verts)
601             mesh.loops.add(nbr_faces * 4)  # Safer than nbr_verts.
602             mesh.polygons.add(nbr_faces)
603
604             mesh.vertices.foreach_set("co", face_verts)
605             mesh.loops.foreach_set("vertex_index", faces)
606             mesh.polygons.foreach_set("loop_start", range(0, nbr_faces * 4, 4))
607             mesh.polygons.foreach_set("loop_total", (4,) * nbr_faces)
608             mesh.update()  # generates edge data
609
610             # pick an object to use
611             obj = objects[0]
612
613             ob_new = bpy.data.objects.new(mesh.name, mesh)
614             base = scene.objects.link(ob_new)
615             base.layers[:] = obj.layers
616
617             ob_inst = bpy.data.objects.new(data.name, data)
618             base = scene.objects.link(ob_inst)
619             base.layers[:] = obj.layers
620
621             for obj in objects:
622                 scene.objects.unlink(obj)
623
624             ob_new.dupli_type = 'FACES'
625             ob_inst.parent = ob_new
626             ob_new.use_dupli_faces_scale = True
627             ob_new.dupli_faces_scale = 1.0 / SCALE_FAC
628
629     def execute(self, context):
630         self._main(context)
631         return {'FINISHED'}
632
633
634 class IsolateTypeRender(Operator):
635     """Hide unselected render objects of same type as active """ \
636     """by setting the hide render flag"""
637     bl_idname = "object.isolate_type_render"
638     bl_label = "Restrict Render Unselected"
639     bl_options = {'REGISTER', 'UNDO'}
640
641     def execute(self, context):
642         act_type = context.object.type
643
644         for obj in context.visible_objects:
645
646             if obj.select:
647                 obj.hide_render = False
648             else:
649                 if obj.type == act_type:
650                     obj.hide_render = True
651
652         return {'FINISHED'}
653
654
655 class ClearAllRestrictRender(Operator):
656     """Reveal all render objects by setting the hide render flag"""
657     bl_idname = "object.hide_render_clear_all"
658     bl_label = "Clear All Restrict Render"
659     bl_options = {'REGISTER', 'UNDO'}
660
661     def execute(self, context):
662         for obj in context.scene.objects:
663             obj.hide_render = False
664         return {'FINISHED'}
665
666
667 class TransformsToDeltasAnim(Operator):
668     """Convert object animation for normal transforms to delta transforms"""
669     bl_idname = "object.anim_transforms_to_deltas"
670     bl_label = "Animated Transforms to Deltas"
671     bl_options = {'REGISTER', 'UNDO'}
672
673     @classmethod
674     def poll(cls, context):
675         obs = context.selected_editable_objects
676         return (obs is not None)
677
678     def execute(self, context):
679         # map from standard transform paths to "new" transform paths
680         STANDARD_TO_DELTA_PATHS = {
681             "location"             : "delta_location",
682             "rotation_euler"       : "delta_rotation_euler",
683             "rotation_quaternion"  : "delta_rotation_quaternion",
684             #"rotation_axis_angle" : "delta_rotation_axis_angle",
685             "scale"                : "delta_scale"
686         }
687         DELTA_PATHS = STANDARD_TO_DELTA_PATHS.values()
688
689         # try to apply on each selected object
690         for obj in context.selected_editable_objects:
691             adt = obj.animation_data
692             if (adt is None) or (adt.action is None):
693                 self.report({'WARNING'},
694                             "No animation data to convert on object: %r" %
695                             obj.name)
696                 continue
697
698             # first pass over F-Curves: ensure that we don't have conflicting
699             # transforms already (e.g. if this was applied already) [#29110]
700             existingFCurves = {}
701             for fcu in adt.action.fcurves:
702                 # get "delta" path - i.e. the final paths which may clash
703                 path = fcu.data_path
704                 if path in STANDARD_TO_DELTA_PATHS:
705                     # to be converted - conflicts may exist...
706                     dpath = STANDARD_TO_DELTA_PATHS[path]
707                 elif path in DELTA_PATHS:
708                     # already delta - check for conflicts...
709                     dpath = path
710                 else:
711                     # non-transform - ignore
712                     continue
713
714                 # a delta path like this for the same index shouldn't
715                 # exist already, otherwise we've got a conflict
716                 if dpath in existingFCurves:
717                     # ensure that this index hasn't occurred before
718                     if fcu.array_index in existingFCurves[dpath]:
719                         # conflict
720                         self.report({'ERROR'},
721                                     "Object '%r' already has '%r' F-Curve(s). "
722                                     "Remove these before trying again" %
723                                     (obj.name, dpath))
724                         return {'CANCELLED'}
725                     else:
726                         # no conflict here
727                         existingFCurves[dpath] += [fcu.array_index]
728                 else:
729                     # no conflict yet
730                     existingFCurves[dpath] = [fcu.array_index]
731
732             # if F-Curve uses standard transform path
733             # just append "delta_" to this path
734             for fcu in adt.action.fcurves:
735                 if fcu.data_path == "location":
736                     fcu.data_path = "delta_location"
737                     obj.location.zero()
738                 elif fcu.data_path == "rotation_euler":
739                     fcu.data_path = "delta_rotation_euler"
740                     obj.rotation_euler.zero()
741                 elif fcu.data_path == "rotation_quaternion":
742                     fcu.data_path = "delta_rotation_quaternion"
743                     obj.rotation_quaternion.identity()
744                 # XXX: currently not implemented
745                 #~ elif fcu.data_path == "rotation_axis_angle":
746                 #~    fcu.data_path = "delta_rotation_axis_angle"
747                 elif fcu.data_path == "scale":
748                     fcu.data_path = "delta_scale"
749                     obj.scale = 1.0, 1.0, 1.0
750
751         # hack: force animsys flush by changing frame, so that deltas get run
752         context.scene.frame_set(context.scene.frame_current)
753
754         return {'FINISHED'}
755
756
757 class DupliOffsetFromCursor(Operator):
758     """Set offset used for DupliGroup based on cursor position"""
759     bl_idname = "object.dupli_offset_from_cursor"
760     bl_label = "Set Offset From Cursor"
761     bl_options = {'REGISTER', 'UNDO'}
762
763     group = IntProperty(
764             name="Group",
765             description="Group index to set offset for",
766             default=0,
767             )
768
769     @classmethod
770     def poll(cls, context):
771         return (context.active_object is not None)
772
773     def execute(self, context):
774         scene = context.scene
775         ob = context.active_object
776         group = self.group
777
778         ob.users_group[group].dupli_offset = scene.cursor_location
779
780         return {'FINISHED'}
781
782
783 class LodByName(Operator):
784     """Add levels of detail to this object based on object names"""
785     bl_idname = "object.lod_by_name"
786     bl_label = "Setup Levels of Detail By Name"
787     bl_options = {'REGISTER', 'UNDO'}
788
789     @classmethod
790     def poll(cls, context):
791         return (context.active_object is not None)
792
793     def execute(self, context):
794         ob = context.active_object
795
796         prefix = ""
797         suffix = ""
798         name = ""
799         if ob.name.lower().startswith("lod0"):
800             prefix = ob.name[:4]
801             name = ob.name[4:]
802         elif ob.name.lower().endswith("lod0"):
803             name = ob.name[:-4]
804             suffix = ob.name[-4:]
805         else:
806             return {'CANCELLED'}
807
808         level = 0
809         while True:
810             level += 1
811
812             if prefix:
813                 prefix = prefix[:3] + str(level)
814             if suffix:
815                 suffix = suffix[:3] + str(level)
816
817             lod = None
818             try:
819                 lod = bpy.data.objects[prefix + name + suffix]
820             except KeyError:
821                 break
822
823             try:
824                 ob.lod_levels[level]
825             except IndexError:
826                 bpy.ops.object.lod_add()
827
828             ob.lod_levels[level].object = lod
829
830         return {'FINISHED'}
831
832
833 class LodClearAll(Operator):
834     """Remove all levels of detail from this object"""
835     bl_idname = "object.lod_clear_all"
836     bl_label = "Clear All Levels of Detail"
837     bl_options = {'REGISTER', 'UNDO'}
838
839     @classmethod
840     def poll(cls, context):
841         return (context.active_object is not None)
842
843     def execute(self, context):
844         ob = context.active_object
845
846         if ob.lod_levels:
847             while 'CANCELLED' not in bpy.ops.object.lod_remove():
848                 pass
849
850         return {'FINISHED'}
851
852
853 class LodGenerate(Operator):
854     """Generate levels of detail using the decimate modifier"""
855     bl_idname = "object.lod_generate"
856     bl_label = "Generate Levels of Detail"
857     bl_options = {'REGISTER', 'UNDO'}
858
859     count = IntProperty(
860             name="Count",
861             default=3,
862             )
863     target = FloatProperty(
864             name="Target Size",
865             min=0.0, max=1.0,
866             default=0.1,
867             )
868     package = BoolProperty(
869             name="Package into Group",
870             default=False,
871             )
872
873     @classmethod
874     def poll(cls, context):
875         return (context.active_object is not None)
876
877     def execute(self, context):
878         scene = context.scene
879         ob = scene.objects.active
880
881         lod_name = ob.name
882         lod_suffix = "lod"
883         lod_prefix = ""
884         if lod_name.lower().endswith("lod0"):
885             lod_suffix = lod_name[-3:-1]
886             lod_name = lod_name[:-3]
887         elif lod_name.lower().startswith("lod0"):
888             lod_suffix = ""
889             lod_prefix = lod_name[:3]
890             lod_name = lod_name[4:]
891
892         group_name = lod_name.strip(' ._')
893         if self.package:
894             try:
895                 bpy.ops.object.group_link(group=group_name)
896             except TypeError:
897                 bpy.ops.group.create(name=group_name)
898
899         step = (1.0 - self.target) / (self.count - 1)
900         for i in range(1, self.count):
901             scene.objects.active = ob
902             bpy.ops.object.duplicate()
903             lod = context.selected_objects[0]
904
905             scene.objects.active = ob
906             bpy.ops.object.lod_add()
907             scene.objects.active = lod
908
909             if lod_prefix:
910                 lod.name = lod_prefix + str(i) + lod_name
911             else:
912                 lod.name = lod_name + lod_suffix + str(i)
913
914             lod.location.y = ob.location.y + 3.0 * i
915
916             if i == 1:
917                 modifier = lod.modifiers.new("lod_decimate", "DECIMATE")
918             else:
919                 modifier = lod.modifiers[-1]
920
921             modifier.ratio = 1.0 - step * i
922
923             ob.lod_levels[i].object = lod
924
925             if self.package:
926                 bpy.ops.object.group_link(group=group_name)
927                 lod.parent = ob
928
929         if self.package:
930             for level in ob.lod_levels[1:]:
931                 level.object.hide = level.object.hide_render = True
932
933         lod.select = False
934         ob.select = True
935         scene.objects.active = ob
936
937         return {'FINISHED'}