# `BB.Math.Transform2D`
[🔗](https://github.com/beam-bots/bb/blob/main/lib/bb/math/transform2d.ex#L5)

A rigid transform within a plane: translation and rotation about the plane's normal.

This is the configuration of a `:planar` joint — two translations in the plane
and one rotation about its normal, which is the joint's `axis`.

## Not tensor-backed

Unlike `BB.Math.Transform`, this holds three plain floats rather than an Nx
tensor. `BB.Math.Transform` needs a matrix because it composes through
kinematic chains by matrix multiply; a planar configuration does not, because
it is lifted into a `BB.Math.Transform` by `to_transform/2` before it reaches
forward kinematics.

Storing `theta` as an angle rather than as the `sin`/`cos` entries of a
rotation matrix means there is no orthonormality to lose and no
renormalisation on read.

## Conventions

- `x` and `y` are in metres, within the plane
- `theta` is in radians, right-handed about the plane's normal

## Examples

    iex> t = BB.Math.Transform2D.new(1.0, 2.0, :math.pi() / 2)
    iex> {t.x, t.y}
    {1.0, 2.0}

# `t`

```elixir
@type t() :: %BB.Math.Transform2D{theta: float(), x: float(), y: float()}
```

# `compose`

```elixir
@spec compose(t(), t()) :: t()
```

Compose two planar transforms.

`compose(a, b)` returns the transform that applies `a` first, then `b`,
matching `BB.Math.Transform.compose/2`.

## Examples

    iex> a = BB.Math.Transform2D.new(1.0, 0.0, :math.pi() / 2)
    iex> b = BB.Math.Transform2D.new(1.0, 0.0, 0.0)
    iex> c = BB.Math.Transform2D.compose(a, b)
    iex> {Float.round(c.x, 6), Float.round(c.y, 6)}
    {1.0, 1.0}

# `identity`

```elixir
@spec identity() :: t()
```

The identity planar transform.

## Examples

    iex> BB.Math.Transform2D.identity()
    %BB.Math.Transform2D{x: 0.0, y: 0.0, theta: 0.0}

# `inverse`

```elixir
@spec inverse(t()) :: t()
```

Invert a planar transform.

## Examples

    iex> t = BB.Math.Transform2D.new(1.0, 2.0, 0.5)
    iex> c = BB.Math.Transform2D.compose(t, BB.Math.Transform2D.inverse(t))
    iex> {abs(c.x) < 1.0e-12, abs(c.y) < 1.0e-12, abs(c.theta) < 1.0e-12}
    {true, true, true}

# `new`

```elixir
@spec new(number(), number(), number()) :: t()
```

Create a planar transform from in-plane translation and rotation.

## Examples

    iex> BB.Math.Transform2D.new(1, 2, 0)
    %BB.Math.Transform2D{x: 1.0, y: 2.0, theta: 0.0}

# `plane_basis`

```elixir
@spec plane_basis(BB.Math.Vec3.t()) :: {BB.Math.Vec3.t(), BB.Math.Vec3.t()}
```

The two in-plane axes for a given plane normal.

`{u, v, normal}` is right-handed with `u × v == normal`, `u` is the direction
`x` measures along and `v` the direction `y` measures along. The canonical
`{0, 0, 1}` normal reduces to the XY plane with `u == x̂` and `v == ŷ`.

Exposed because a planar joint's Jacobian columns must be expressed in the same
basis `to_transform/2` lifts its configuration through, and the two disagreeing
would be a silently wrong derivative.

## Examples

    iex> {u, v} = BB.Math.Transform2D.plane_basis(BB.Math.Vec3.unit_z())
    iex> {BB.Math.Vec3.to_list(u), BB.Math.Vec3.to_list(v)}
    {[1.0, 0.0, 0.0], [0.0, 1.0, 0.0]}

# `to_transform`

```elixir
@spec to_transform(t(), BB.Math.Vec3.t()) :: BB.Math.Transform.t()
```

Lift into 3D, given the normal of the plane the transform is in.

The normal is the `:planar` joint's `axis`. It is taken as a parameter rather
than carried on the struct so that the joint remains the single source of
truth for its own plane — a configuration carrying a copy could disagree with
the joint that owns it.

The plane is spanned by two axes perpendicular to `normal`, with `x` along the
first and `y` along the second, and `theta` is a right-handed rotation about
`normal`. For the canonical `{0, 0, 1}` normal this reduces to the XY plane
with `theta` as yaw.

## Examples

    iex> t2d = BB.Math.Transform2D.new(1.0, 2.0, 0.0)
    iex> t = BB.Math.Transform2D.to_transform(t2d, BB.Math.Vec3.unit_z())
    iex> BB.Math.Transform.get_translation(t) |> BB.Math.Vec3.to_list()
    [1.0, 2.0, 0.0]

---

*Consult [api-reference.md](api-reference.md) for complete listing*
