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