Units and geometry
Distances, angles and speeds that carry their units, poses on the field, and the season's field.
Units
A bare number like 30 could be centimeters, inches or degrees. BearPlatform’s numbers carry their units:
val reach = 30.cm + 4.inch // a Distance
val turn = 90.degrees // an Angle
val speed = 2.tiles / 1.5.seconds // a LinearVelocity
val spin = 435.rpm // an AngularVelocity
They are read back in the unit wanted: reach.inCm, turn.inRadians, spin.inDegreesPerSecond. Mixing them up
doesn’t compile: a distance can’t be added to an angle. They cost no more than plain Doubles.
| Type | Made with | Read with |
|---|---|---|
Distance |
10.cm, 5.mm, 1.5.meters, 4.inch, 2.tiles |
inCm, inMm, inMeters, inInches, inTiles |
Angle |
90.degrees, 1.5.radians, 0.25.rotations |
inDegrees, inRadians, inRotations |
Heading |
135.degrees.toHeading() |
inDegrees, inRadians |
LinearVelocity |
50.cm.perSecond, distance / duration |
inCmPerSecond, inInchesPerSecond |
AngularVelocity |
90.degrees.perSecond, 435.rpm, angle / duration |
inDegreesPerSecond, inRadiansPerSecond, inRpm |
Duration |
300.milliseconds, 1.5.seconds (Kotlin’s own) |
inWholeMilliseconds, toDouble(DurationUnit.SECONDS) |
A tile is a standard FTC field tile, 24 inches. Arithmetic works as expected: speed * 2.seconds is a distance,
reach / 2 is half of it, reach / 1.cm is a plain number, and abs, sin, cos, hypot and atan2 take units
too.
Angles and headings
An Angle is an amount of turning, and never wraps around: 270.degrees + 180.degrees is 450°, which is what a
turret, an arm or counting rotations needs. Positive is counterclockwise.
A Heading is a direction on the field, like the way the robot faces, and always stays within (-180°, 180°]:
val facing = robot.localizer.pose.heading
val turned = facing + 90.degrees // wraps around by itself
val turn = target.heading - facing // the shortest turn there: an Angle within (-180°, 180°]
So turning the robot never needs angle wrapping by hand. Most places that take a heading, like
Pose(x, y, 135.degrees), take an angle too and convert it.
Points, poses and offsets
| Type | Is |
|---|---|
Point |
a place on the field: Point(30.cm, -10.cm) |
Pose |
a place and the way the robot faces there: Pose(30.cm, -10.cm, 90.degrees) |
RobotOffset |
a move or a place in the robot’s own frame: forward, right, and turn counterclockwise |
PoseVelocity |
how fast the robot moves along field x and y, and turns |
val next = pose + RobotOffset(forward = 30.cm, turn = 90.degrees) // 30 cm ahead, then a quarter turn left
val camera = (pose + RobotOffset(forward = 20.cm, right = -5.cm)).point // where a camera mounted there is
val toGo = target.relativeTo(pose) // forward, right and turn the robot still has to go
val face = pose.headingTo(goal) // the heading that faces the goal from here
val far = pose.distanceTo(goal)
A point is also a vector on the field: b - a is the move from a to b, with its length and direction. Points
and poses can be shiftBy field distances, poses turnBy an angle, and lerp goes part of the way to another one.
Field coordinates
Poses on the field use FTC’s field coordinate system:
- the origin is the center of the field;
- heading 0 faces +x, and +y is 90° counterclockwise from it;
- angles and turns are positive counterclockwise.
Which wall +x points to changes from season to season, so each season’s code describes its field:
// DECODE's field: +x points toward the audience, the red drivers stand at the audience's left, and
// blue's half of the field is a mirror image of red's across the x axis
val DecodeField = Field(
xAxis = FieldDirection.TOWARD_AUDIENCE,
symmetry = FieldSymmetry.FLIP_Y,
redStation = FieldDirection.AUDIENCE_LEFT,
)
Field property |
Is |
|---|---|
xAxis |
where +x points, as the audience sees the field |
symmetry |
how one alliance’s side maps onto the other’s: FLIP_Y, FLIP_X or ROTATE_180 |
referenceAlliance |
the alliance poses in code are written for, red by default |
redStation, blueStation |
the walls the drivers stand at; field-centric driving needs it |
tileSize |
24 inches |
Then:
field.forAlliance(pose, alliance)converts a pose written for the reference alliance to the other’s, so autonomous paths are written once;field.driversFace(alliance)is the way the alliance’s drivers look, for field-centric driving;field.contains(point)andfield.clamp(point)keep points within the walls.
Tiles
Tiles are named the way the audience sees the field: columns A to F from left to right, rows 1 to 6 from the
audience’s side to the far side. field.tile("D4") is the center of D4, and a suffix picks a spot on the tile, as the
audience sees it: Top, Bottom, Left, Right, Center, one or two of them in any order. tile("F6TR") is the top
right corner of F6, and tile("C3L") the middle of its left edge.
val start = DecodeField.tile("F6BL").withHeading(135.degrees)
Settings can be tiles too, like AutoPositions.START = F6BL:135, once the season’s field is set with
Config.field = DecodeField (see Settings).
Robot coordinates
Moves relative to the robot use RobotOffset: forward along its heading, right to its right side, and turn
counterclockwise. The drivetrains take powers the same way: forward, right, and turn counterclockwise.