1import logging; logger = logging.getLogger("morsebuilder." + __name__)
2import sys
3import math
4from morse.builder.blenderobjects import Cube
5from morse.builder import bpymorse
6from morse.builder.abstractcomponent import AbstractComponent
7from morse.core.exceptions import *
8
9"""
10Morse Builder API
11
12To test this module you can c/p the following code in Blender Python console::
13
14.. code-block:: python
15
16    import sys
17    sys.path.append("/usr/local/lib/python3/dist-packages")
18    from morse.builder import *
19    atrv=ATRV()
20
21The string passed to the differents Components Classes must be an existing
22.blend file-name, ie. for ``ATRV()`` the file ``atrv.blend`` must exists
23in the folder ``MORSE_COMPONENTS/robots/``.
24"""
25
26# Override the default Python exception handler
27def morse_excepthook(*args, **kwargs):
28    logger.error("[ERROR][MORSE] Uncaught exception, quit Blender.", exc_info = tuple(args))
29    # call default python exception hook
30    # on Ubuntu/Python3.4 sys.excepthook is overriden by `apport_excepthook`
31    sys.__excepthook__(*args, **kwargs)
32    import os
33    os._exit(-1)
34
35# Uncaught exception quit Blender
36sys.excepthook = morse_excepthook
37# workaround avoid numpy.core.multiarray missmatch ( see #630 )
38sys.path.insert(0, '%s/lib/python%i.%i/site-packages'%(sys.prefix,
39    sys.version_info.major, sys.version_info.minor))
40
41class PassiveObject(AbstractComponent):
42    """ Allows to import any Blender object to the scene.
43    """
44
45    def __init__(self, filename="props/objects", prefix=None, keep_pose=False):
46        """ Initialize a PassiveObject
47
48        :param filename: The Blender file to load. Path can be absolute
49                         or if no extension relative to MORSE assets'
50                         installation path (typically, $PREFIX/share/morse/data)
51        :param prefix: (optional) the prefix of the objects to load in the
52                       Blender file. If not set, all objects present in the file
53                       are loaded. If set, all objects **prefixed** by this
54                       name are imported.
55        :param keep_pose: If set, the object pose (translation and rotation)
56                        in the Blender file is kept. Else, the object
57                        own center is placed at origin and all rotation are
58                        reset.
59        :return: a new AbstractComponent instance.
60        """
61        AbstractComponent.__init__(self, filename=filename)
62
63        logger.info("Importing the following passive object(s): %s" % prefix)
64
65        imported_objects = self.append_meshes(prefix=prefix)
66        # Here we use the fact that after appending, Blender select the objects
67        # and the root (parent) object first ( [0] )
68        self.set_blender_object(imported_objects[0])
69
70        if not keep_pose:
71            self.location = (0.0, 0.0, 0.0)
72            self.rotation_euler = (0.0, 0.0, 0.0)
73
74    def setgraspable(self):
75        """
76        Makes an object graspable to the human avatar by adding a NEAR collision
77        sensor to the object.
78
79        This function also set the object to be an active game object (property
80        'Object' set to true), and set the object label to the Blender object
81        name (if not already set).
82        """
83        obj = self._bpy_object
84
85        if not "Label" in obj.game.properties:
86            self.properties(Object = True, Graspable = True, Label = obj.name)
87        else:
88            self.properties(Object = True, Graspable = True)
89
90        # Add collision sensor for object placement
91        if not 'Collision' in obj.game.sensors:
92            bpymorse.add_sensor(type = 'NEAR')
93            sens = obj.game.sensors[-1]
94            sens.name = 'Collision'
95            sens.distance = 0.05
96            sens.reset_distance = 0.075
97            bpymorse.add_controller()
98            contr = obj.game.controllers[-1]
99            contr.link(sensor = sens)
100
101class Zone(Cube):
102    def __init__(self, type):
103        Cube.__init__(self, 'xxx')
104        # Need to create a new material before calling make_transparent
105        self._bpy_object.active_material = bpymorse.create_new_material()
106        self._make_transparent(self._bpy_object, 1e-6)
107        self.properties(Zone_Tag = True, Type = type)
108
109    @property
110    def size(self):
111        return self._bpy_object.scale
112    @size.setter
113    def size(self, value):
114        self._bpy_object.scale = value
115
116    def rotate(self, x=0.0, y=0.0, z=0.0):
117        logger.warning("rotate is not supported for Zone")
118
119class Component(AbstractComponent):
120    """ Append a morse-component to the scene
121
122    cf. `bpy.ops.wm.link_append` and `bpy.data.libraries.load`
123    """
124    def __init__(self, category='', filename='',blender_object_name=None, make_morseable=True):
125        """ Initialize a MORSE component
126
127        :param category: The category of the component (folder in
128            MORSE_COMPONENTS)
129        :param filename: The name of the component (file in
130            MORSE_COMPONENTS/category/name.blend) If ends with '.blend',
131            append the objects from the Blender file.
132        :param blender_object_name: If set, use the given Blender object
133            as 'root' for this component. Otherwise, select the first
134            available Blender object (the top parent in case of a hierarchy
135            of objects).
136        :param make_morseable: If the component has no property for the
137            simulation, append default Morse ones. See self.morseable()
138        """
139        AbstractComponent.__init__(self, filename=filename, category=category)
140
141
142        if blender_object_name is None:
143            imported_objects = self.append_meshes()
144        else:
145            imported_objects = self.append_meshes(objects=[blender_object_name])
146            if not imported_objects:
147                raise MorseBuilderNoComponentError("No object named <%s> in %s" % (blender_object_name, filename))
148
149
150        # Here we use the fact that after appending, Blender select the objects
151        # and the root (parent) object first ( [0] )
152        self.set_blender_object(imported_objects[0])
153        # If the object has no MORSE logic, add default one
154        if make_morseable and category in ['sensors', 'actuators', 'robots'] \
155                and not self.is_morseable():
156            self.morseable()
157
158
159class Robot(Component):
160    def __init__(self, filename='', name=None, blender_object_name=None):
161        """ Initialize a MORSE robot
162
163        :param filename: The name of the component (file in
164            MORSE_COMPONENTS/category/name.blend) If ends with '.blend',
165            append the objects from the Blender file.
166        :param name: Name of the resulting robot in the simulation, default to 'robot'.
167        :param blender_object_name: If set, use the given Blender object
168            in 'filename' as 'root' for this robot. Otherwise, select the first
169            available Blender object (the top parent in case of a hierarchy
170            of objects).
171        """
172        Component.__init__(self, 'robots', filename, blender_object_name=blender_object_name)
173        self.properties(Robot_Tag = True)
174        self.default_interface = None
175        if name:
176            self.name = name
177
178    def set_friction(self, friction=0.0):
179        """ Set  Coulomb friction coefficient
180
181        :param friction: [0, 100], default 0.0
182        :type  friction: float
183        """
184        for slot in self._bpy_object.material_slots: # ['TireMat']
185            slot.material.physics.friction = friction
186
187    def set_mass(self, mass):
188        """ Set component's mass
189
190        :param mass: The component's mass
191        :type  mass: float
192
193        .. note::
194            The object must have a physics controller for the mass to be
195            applied, otherwise the mass value will be returned as 0.0.
196        """
197        self._bpy_object.game.mass = mass
198
199    def add_default_interface(self, stream):
200        """ Add a service and stream interface to all components of the robot
201
202        .. note::
203            If add_stream or add_service is used explicitly for some
204            components and the specified interface is the same it will be
205            added twice.
206        """
207        self.default_interface = stream
208
209    def make_external(self):
210        self._bpy_object.game.properties['Robot_Tag'].name = 'External_Robot_Tag'
211
212    def make_ghost(self, alpha=0.3):
213        """ Make this robot a ghost
214
215        The robot is made transparent, with no collision.
216
217        .. note::
218             A ghost robot has no influence on other simulated robots
219             (no collision, invisible to laser sensors) except for video sensors.
220
221        :param alpha: Transparency alpha coefficient (0 for invisible, 1 for opaque, default is 0.3)
222        """
223        self._make_transparent(self._bpy_object, alpha)
224
225    def set_rigid_body(self):
226        """ Configure this robot to use rigid_body physics """
227        self._bpy_object.game.use_actor = True
228        self._bpy_object.game.physics_type = 'RIGID_BODY'
229        self._bpy_object.game.use_sleep = True
230
231    def set_no_collision(self):
232        """ Configure this robot to not use physics at all """
233        self._bpy_object.game.physics_type = 'NO_COLLISION'
234
235    def set_physics_type(self, physics_type='STATIC'):
236        """ Configure this robot physics type """
237        self._bpy_object.game.physics_type = physics_type
238
239    def set_use_record_animation(self, use_record_animation=True):
240        """ Record animation objects without physics """
241        self._bpy_object.game.use_record_animation = use_record_animation
242
243    def set_dynamic(self):
244        self._bpy_object.game.physics_type = 'DYNAMIC'
245        self._bpy_object.game.use_actor = True
246        self._bpy_object.game.use_sleep = True
247
248    def set_collision_bounds(self):
249        self._bpy_object.game.use_collision_bounds = True
250        self._bpy_object.game.collision_bounds_type = 'CONVEX_HULL'
251        self._bpy_object.game.use_collision_compound = True
252
253    def make_grasper(self, obj_name):
254        obj = bpymorse.get_object(obj_name)
255        bpymorse.select_only(obj)
256        bpymorse.add_sensor(type = 'NEAR')
257        sens = obj.game.sensors[-1]
258        sens.name = 'Near'
259        sens.distance = 5.0
260        sens.reset_distance = 0.075
261        sens.property = "Graspable"
262        bpymorse.add_controller()
263        contr = obj.game.controllers[-1]
264        contr.link(sensor = sens)
265
266
267class GroundRobot(Robot):
268    def __init__(self, filename, name, blender_object_name=None):
269        Robot.__init__(self, filename, name, blender_object_name=blender_object_name)
270        self.properties(GroundRobot = True)
271
272class WheeledRobot(GroundRobot):
273    def __init__(self, filename, name, blender_object_name=None):
274        Robot.__init__(self, filename, name, blender_object_name=blender_object_name)
275
276    def unparent_wheels(self):
277        """ Make the wheels orphans, but keep the transformation applied to
278            the parent robot """
279        # Force Blender to update the transformation matrices of objects
280        bpymorse.get_context_scene().update()
281
282        keys = ['WheelFLName', 'WheelFRName', 'WheelRLName',
283                'WheelRRName', 'CasterWheelName']
284        properties = bpymorse.get_properties(self._bpy_object)
285        for key in keys:
286            expected_wheel = properties.get(key, None)
287            if expected_wheel:
288                wheel = self.get_child(expected_wheel)
289                if wheel:
290                    # Make a copy of the current transformation matrix
291                    transformation = wheel.matrix_world.copy()
292                    wheel.parent = None
293                    wheel.matrix_world = transformation
294                else:
295                    logger.error('Wheel %s is required but not found' % expected_wheel)
296
297    def after_renaming(self):
298        self.unparent_wheels()
299