A turtle object that keeps track of its position and rotation in 3D space. All angles given in degrees.
| 7 | |
| 8 | |
| 9 | class Turtle: |
| 10 | """A turtle object that keeps track of its position and rotation in 3D space. |
| 11 | |
| 12 | All angles given in degrees. |
| 13 | """ |
| 14 | |
| 15 | def __init__(self, position: np.ndarray = None, rotation: Rotation = None): |
| 16 | """Initialize the turtle with the given position and rotation. |
| 17 | |
| 18 | Use a traditional RHS coordinate system with Z pointing up. |
| 19 | Transformations are performed using intrinsic Euler angles, so rotations about an axis are |
| 20 | relative to the turtle's local reference frame. |
| 21 | |
| 22 | :param position: The starting position of the turtle. Defaults to (0, 0, 0). |
| 23 | :param rotation: The starting rotation of the turtle, applied to the vector (0, 0, 1). |
| 24 | """ |
| 25 | if rotation is not None and not isinstance(rotation, Rotation): |
| 26 | raise TypeError("Rotation must be a scipy.spatial.transform.Rotation") |
| 27 | |
| 28 | self._position = np.array(position) if position is not None else np.array([[0, 0, 0]]) |
| 29 | self.rotation = rotation if rotation is not None else Rotation.from_matrix(np.eye(3)) |
| 30 | |
| 31 | @property |
| 32 | def position(self): |
| 33 | """Ensure the position is externally always treated as (3,), not (1, 3).""" |
| 34 | return self._position.reshape((3,)) |
| 35 | |
| 36 | @position.setter |
| 37 | def position(self, value): |
| 38 | self._position = np.array(value) |
| 39 | |
| 40 | def forward(self, stepsize=1): |
| 41 | """Move the turtle forward by the given stepsize.""" |
| 42 | orientation = (0, 0, 1) |
| 43 | orientation = self.rotation.apply(orientation) |
| 44 | self.position = self.position + stepsize * orientation |
| 45 | logger.debug(f"stepping forward to {self.position}") |
| 46 | |
| 47 | def yaw(self, angle): |
| 48 | """Yaw the turtle around its local Z axis.""" |
| 49 | # NOTE: Capital axes indicate intrinsic Euler angles. |
| 50 | # Apparently, it's normal to indicate the normal and longitudinal axes with X and Z respectively |
| 51 | # I still want to keep the mental model of "Z is up, duh." |
| 52 | self.rotation = self.rotation * Rotation.from_euler("X", [angle], degrees=True) |
| 53 | logger.debug(f"yaw {angle}deg") |
| 54 | |
| 55 | def pitch(self, angle): |
| 56 | """Pitch the turtle around its local Y axis.""" |
| 57 | self.rotation = self.rotation * Rotation.from_euler("Y", [angle], degrees=True) |
| 58 | logger.debug(f"pitch {angle}deg") |
| 59 | |
| 60 | def roll(self, angle): |
| 61 | """Roll the turtle around its local X axis. |
| 62 | |
| 63 | Just a roll is enough to affect direction, since it's a rotation around the longitudinal |
| 64 | axis. That is, a rotation around the axis you're facing. |
| 65 | """ |
| 66 | self.rotation = self.rotation * Rotation.from_euler("Z", [angle], degrees=True) |
no outgoing calls