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