1""" This module wraps the calls to the Blender Python API. This is intended
2for all the cases we need to run MORSE code outside Blender (mostly for
3documentation generation purposes).
4"""
5from morse.core.exceptions import MorseBuilderNoComponentError
6import logging
7
8logger = logging.getLogger('morse')
9
10bpy = None
11
12try:
13    import bpy
14except ImportError:
15    print("WARNING: MORSE is running outside Blender! (no bpy)")
16
17def empty_method(*args, **kwargs):
18    print(args, kwargs)
19
20select_all = empty_method
21add_mesh_monkey = empty_method
22add_mesh_plane = empty_method
23add_mesh_cube = empty_method
24add_mesh_uv_sphere = empty_method
25add_mesh_ico_sphere = empty_method
26add_mesh_cylinder = empty_method
27add_mesh_cone = empty_method
28add_mesh_torus = empty_method
29add_lamp = empty_method
30add_camera = empty_method
31new_material = empty_method
32new_text = empty_method
33new_game_property = empty_method
34add_sensor = empty_method
35add_controller = empty_method
36add_actuator = empty_method
37link_append = empty_method
38link = empty_method # 2.71.6
39append = empty_method # 2.71.6
40collada_import = empty_method
41add_object = empty_method
42add_empty = empty_method
43new_mesh = empty_method
44new_object = empty_method
45apply_transform = empty_method
46open_sound = empty_method
47new_scene = empty_method
48del_scene = empty_method
49armatures = empty_method
50make_links_scene = empty_method
51
52if bpy:
53    select_all = bpy.ops.object.select_all
54    add_mesh_monkey = bpy.ops.mesh.primitive_monkey_add
55    add_mesh_plane = bpy.ops.mesh.primitive_plane_add
56    add_mesh_cube = bpy.ops.mesh.primitive_cube_add
57    add_mesh_uv_sphere = bpy.ops.mesh.primitive_uv_sphere_add
58    add_mesh_ico_sphere = bpy.ops.mesh.primitive_ico_sphere_add
59    add_mesh_cylinder = bpy.ops.mesh.primitive_cylinder_add
60    add_mesh_cone = bpy.ops.mesh.primitive_cone_add
61    add_mesh_torus = bpy.ops.mesh.primitive_torus_add
62    add_lamp = bpy.ops.object.lamp_add
63    add_camera = bpy.ops.object.camera_add
64    new_material = bpy.ops.material.new
65    new_game_property = bpy.ops.object.game_property_new
66    add_sensor = bpy.ops.logic.sensor_add
67    add_controller = bpy.ops.logic.controller_add
68    add_actuator = bpy.ops.logic.actuator_add
69    if bpy.app.version >= (2, 71, 6):
70        link = bpy.ops.wm.link
71        append = bpy.ops.wm.append
72    else: # link_append dropped in 2.71.6
73        link_append = bpy.ops.wm.link_append
74    collada_import = bpy.ops.wm.collada_import
75    add_object = bpy.ops.object.add
76    add_empty = bpy.ops.object.empty_add
77    new_mesh = bpy.data.meshes.new
78    new_object = bpy.data.objects.new
79    apply_transform = bpy.ops.object.transform_apply
80    open_sound = bpy.ops.sound.open
81    new_scene = bpy.ops.scene.new
82    del_scene = bpy.ops.scene.delete
83    armatures = bpy.data.armatures
84    make_links_scene = bpy.ops.object.make_links_scene
85
86def version():
87    if bpy:
88        return bpy.app.version
89    else:
90        return 0,0,0
91
92
93def create_new_material():
94    all_materials = get_materials().keys()
95    new_material()
96    material_name = [name for name in get_materials().keys() \
97                     if name not in all_materials].pop()
98    return get_material(material_name)
99
100def add_morse_empty(shape = 'ARROWS'):
101    """Add MORSE Component Empty object which hlods MORSE logic"""
102    add_empty(type = shape)
103
104def deselect_all():
105    select_all(action='DESELECT')
106
107def get_first_selected_object():
108    if bpy and bpy.context.selected_objects:
109        return bpy.context.selected_objects[0]
110    else:
111        return None
112
113def get_selected_objects():
114    if bpy:
115        return bpy.context.selected_objects
116    else:
117        return []
118
119def get_lamps():
120    if bpy:
121        return bpy.data.lamps
122    else:
123        return []
124
125def get_lamp(name_or_id):
126    if bpy and bpy.data.lamps:
127        return bpy.data.lamps[name_or_id]
128    else:
129        return None
130
131def get_last_lamp():
132    return get_lamp(-1)
133
134def get_materials():
135    if bpy:
136        return bpy.data.materials
137    else:
138        return []
139
140def get_material(name_or_id):
141    if bpy and bpy.data.materials:
142        return bpy.data.materials[name_or_id]
143    else:
144        return None
145
146def get_last_material():
147    return get_material(-1)
148
149def new_text():
150    if bpy:
151        texts_before = set(get_texts())
152        bpy.ops.text.new()
153        texts_after = set(get_texts())
154        return (texts_before ^ texts_after).pop()
155    else:
156        return None
157
158def get_texts():
159    if bpy:
160        return bpy.data.texts
161    else:
162        return []
163
164def get_text(name_or_id):
165    if bpy and bpy.data.texts:
166        return bpy.data.texts[name_or_id]
167    else:
168        return None
169
170def get_last_text():
171    return get_text(-1)
172
173def get_sounds():
174    if bpy:
175        return bpy.data.sounds
176    else:
177        return []
178
179def get_sound(name_or_id):
180    if bpy and bpy.data.sounds:
181        return bpy.data.sounds[name_or_id]
182    else:
183        return None
184
185def get_last_sound():
186    return get_sound(-1)
187
188def get_scenes():
189    if bpy:
190        return bpy.data.scenes
191    else:
192        return []
193
194def get_scene(name_or_id):
195    if bpy and bpy.data.scenes:
196        return bpy.data.scenes[name_or_id]
197    else:
198        return None
199
200def set_active_scene(name_or_id):
201    if bpy:
202        scene = get_scene(name_or_id)
203        if scene:
204            bpy.data.screens['Default'].scene = scene
205            bpy.context.screen.scene = scene
206            return scene
207        else:
208            return None
209    else:
210        return None
211
212def get_last_scene():
213    return get_scene(-1)
214
215def select_only(obj):
216    if bpy:
217        deselect_all()
218        obj.select = True
219        bpy.context.scene.objects.active = obj
220
221def delete(objects):
222    if not bpy:
223        return
224    if not isinstance(objects, list):
225        objects = [objects]
226    for obj in objects:
227        if isinstance(obj, str):
228            obj = bpy.data.objects[obj]
229        select_only(obj)
230        bpy.ops.object.delete()
231
232def get_objects():
233    if bpy:
234        return bpy.data.objects
235    else:
236        return []
237
238def get_object(name_or_id):
239    if bpy and bpy.data.objects:
240        return bpy.data.objects[name_or_id]
241    else:
242        return None
243
244def get_fps():
245    if bpy:
246        return bpy.context.scene.game_settings.fps
247    else:
248        return -1
249
250def get_context_object():
251    if bpy:
252        return bpy.context.object
253    else:
254        return None
255
256def get_context_scene():
257    if bpy:
258        return bpy.context.scene
259    else:
260        return None
261
262def get_context_window():
263    if bpy:
264        return bpy.context.window
265    else:
266        return None
267
268def set_debug(debug=True):
269    bpy.app.debug = debug
270
271
272def _get_xxx_in_blend(filepath, kind):
273    if not bpy:
274        return []
275    objects = []
276    try:
277        with bpy.data.libraries.load(filepath) as (src, _):
278            try:
279                objects = [obj for obj in getattr(src, kind)]
280            except UnicodeDecodeError as detail:
281                logger.error("Unable to open file '%s'. Exception: %s" % \
282                             (filepath, detail))
283    except IOError as detail:
284        logger.error(detail)
285        raise MorseBuilderNoComponentError("Component not found")
286    return objects
287
288
289def get_objects_in_blend(filepath):
290    return _get_xxx_in_blend(filepath, 'objects')
291
292def get_scenes_in_blend(filepath):
293    return _get_xxx_in_blend(filepath, 'scenes')
294
295def save(filepath=None, check_existing=False, compress=True):
296    """ Save .blend file
297
298    :param filepath: File Path
299    :type  filepath: string, (optional, default: current file)
300    :param check_existing: Check and warn on overwriting existing files
301    :type  check_existing: boolean, (optional, default: False)
302    :param compress: Compress, Write compressed .blend file
303    :type  compress: boolean, (optional, default: True)
304    """
305    if not bpy:
306        return
307    if not filepath:
308        filepath = bpy.data.filepath
309    bpy.ops.wm.save_mainfile(filepath=filepath, check_existing=check_existing,
310            compress=compress)
311
312def set_speed(fps=60, logic_step_max=20, physics_step_max=20):
313    """ Tune the speed of the simulation
314
315    :param fps: Nominal number of game frames per second
316        (physics fixed timestep = 1/fps, independently of actual frame rate)
317    :type fps: default 60
318    :param logic_step_max: Maximum number of logic frame per game frame if
319        graphics slows down the game, higher value allows better
320        synchronization with physics
321    :type logic_step_max: default value : 20
322    :param physics_step_max: Maximum number of physics step per game frame
323        if graphics slows down the game, higher value allows physics to keep
324        up with realtime
325    :type physics_step_max: default value : 20
326
327    usage::
328
329        bpymorse.set_speed(120, 5, 5)
330
331    .. note:: It is recommended to use the same value for logic_step_max
332      | physics_step_max
333
334    .. warning:: This method must be called at the top of your Builder script,
335      before creating any component.
336    """
337    logger.warning("`bpymorse.set_speed` is deprecated, "
338                     "use `Environment.simulator_frequency` instead")
339
340    get_context_scene().game_settings.fps = fps
341    get_context_scene().game_settings.logic_step_max = logic_step_max
342    get_context_scene().game_settings.physics_step_max = physics_step_max
343
344def get_properties(obj):
345    return {n: p.value for n,p in obj.game.properties.items()}
346
347def properties(obj, **kwargs):
348    """ Add/modify the game properties of the Blender object
349
350    Usage example:
351
352    .. code-block:: python
353
354        properties(obj, capturing = True, classpath='module.Class', speed = 5.0)
355
356    will create and/or set the 3 game properties Component_Tag, classpath, and
357    speed at the value True (boolean), 'module.Class' (string), 5.0 (float).
358    In Python the type of numeric value is 'int', if you want to force it to
359    float, use the following: float(5) or 5.0
360    Same if you want to force to integer, use: int(a/b)
361    For the TIMER type, see the class timer(float) defined in this module:
362
363    .. code-block:: python
364
365        properties(obj, my_clock = timer(5.0), my_speed = int(5/2))
366
367    """
368    for key in kwargs.keys():
369        if key in obj.game.properties.keys():
370            _property_set(obj, key, kwargs[key])
371        else:
372            _property_new(obj, key, kwargs[key])
373
374def _property_new(obj, name, value, ptype=None):
375    """ Add a new game property for the Blender object
376
377    :param name: property name (string)
378    :param value: property value
379    :param ptype: property type (enum in ['BOOL', 'INT', 'FLOAT', 'STRING', 'TIMER'],
380                  optional, auto-detect, default=None)
381    """
382    select_only(obj)
383    new_game_property()
384    # select the last property in the list (which is the one we just added)
385    obj.game.properties[-1].name = name
386    return _property_set(obj, -1, value, ptype)
387
388def _property_set(obj, name_or_id, value, ptype=None):
389    """ Really set the property for the property referenced by name_or_id
390
391    :param name_or_id: the index or name of property (OrderedDict)
392    :param value: the property value
393    :param ptype: property type (enum in ['BOOL', 'INT', 'FLOAT', 'STRING', 'TIMER'],
394                  optional, auto-detect, default=None)
395    """
396    if ptype is None:
397        # Detect the type (class name upper case)
398        ptype = value.__class__.__name__.upper()
399    if ptype == 'STR':
400        # Blender property string are called 'STRING' (and not 'str' as in Python)
401        ptype = 'STRING'
402    obj.game.properties[name_or_id].type = ptype
403    obj.game.properties[name_or_id].value = value
404    return obj.game.properties[name_or_id]
405
406def set_viewport(viewport_shade='WIREFRAME', clip_end=1000):
407    """ Set the default view mode
408
409    :param viewport_shade: enum in ['BOUNDBOX', 'WIREFRAME', 'SOLID', 'TEXTURED'], default 'WIREFRAME'
410    """
411    for area in bpy.context.window.screen.areas:
412        if area.type == 'VIEW_3D':
413            for space in area.spaces:
414                if space.type == 'VIEW_3D':
415                    space.viewport_shade = viewport_shade
416                    space.clip_end = clip_end
417
418def set_viewport_perspective(perspective='CAMERA'):
419    """ Set the default view view_perspective
420
421    Equivalent to ``bpy.ops.view3d.viewnumpad`` with good context.
422
423    :param perspective: View, Preset viewpoint to use
424    :type  perspective: enum in ['FRONT', 'BACK', 'LEFT', 'RIGHT', 'TOP',
425                                 'BOTTOM', 'CAMERA'], default 'CAMERA'
426    """
427    for area in bpy.context.window.screen.areas:
428        if area.type == 'VIEW_3D':
429            for space in area.spaces:
430                if space.type == 'VIEW_3D':
431                    space.region_3d.view_perspective = perspective
432
433def fullscreen(fullscreen=True, desktop=True):
434    """ Run the simulation fullscreen
435
436    :param fullscreen: Start player in a new fullscreen display
437    :type  fullscreen: Boolean, default: True
438    :param desktop: Use the current desktop resolution in fullscreen mode
439    :type  desktop: Boolean, default: True
440    """
441    if not bpy:
442        return
443    bpy.context.scene.game_settings.show_fullscreen = fullscreen
444    bpy.context.scene.game_settings.use_desktop = desktop
445