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