001// Copyright (c) FIRST and other WPILib contributors.
002// Open Source Software; you can modify and/or share it under the terms of
003// the WPILib BSD license file in the root directory of this project.
004
005package org.wpilib.math.geometry;
006
007import static org.wpilib.units.Units.Meters;
008
009import java.util.Objects;
010import org.wpilib.math.geometry.proto.Transform2dProto;
011import org.wpilib.math.geometry.struct.Transform2dStruct;
012import org.wpilib.math.linalg.MatBuilder;
013import org.wpilib.math.linalg.Matrix;
014import org.wpilib.math.numbers.N3;
015import org.wpilib.math.util.Nat;
016import org.wpilib.units.measure.Distance;
017import org.wpilib.util.protobuf.ProtobufSerializable;
018import org.wpilib.util.struct.StructSerializable;
019
020/** Represents a transformation for a Pose2d in the pose's frame. */
021public class Transform2d implements ProtobufSerializable, StructSerializable {
022  /**
023   * A preallocated Transform2d representing no transformation.
024   *
025   * <p>This exists to avoid allocations for common transformations.
026   */
027  public static final Transform2d kZero = new Transform2d();
028
029  private final Translation2d m_translation;
030  private final Rotation2d m_rotation;
031
032  /**
033   * Constructs the transform that maps the initial pose to the final pose.
034   *
035   * @param initial The initial pose for the transformation.
036   * @param last The final pose for the transformation.
037   */
038  public Transform2d(Pose2d initial, Pose2d last) {
039    // To transform the global translation delta to be relative to the initial
040    // pose, rotate by the inverse of the initial pose's orientation.
041    m_translation =
042        last.getTranslation()
043            .minus(initial.getTranslation())
044            .rotateBy(initial.getRotation().unaryMinus());
045
046    m_rotation = last.getRotation().relativeTo(initial.getRotation());
047  }
048
049  /**
050   * Constructs a transform with the given translation and rotation components.
051   *
052   * @param translation Translational component of the transform.
053   * @param rotation Rotational component of the transform.
054   */
055  public Transform2d(Translation2d translation, Rotation2d rotation) {
056    m_translation = translation;
057    m_rotation = rotation;
058  }
059
060  /**
061   * Constructs a transform with x and y translations instead of a separate Translation2d.
062   *
063   * @param x The x component of the translational component of the transform.
064   * @param y The y component of the translational component of the transform.
065   * @param rotation The rotational component of the transform.
066   */
067  public Transform2d(double x, double y, Rotation2d rotation) {
068    m_translation = new Translation2d(x, y);
069    m_rotation = rotation;
070  }
071
072  /**
073   * Constructs a transform with x and y translations instead of a separate Translation2d. The X and
074   * Y translations will be converted to and tracked as meters.
075   *
076   * @param x The x component of the translational component of the transform.
077   * @param y The y component of the translational component of the transform.
078   * @param rotation The rotational component of the transform.
079   */
080  public Transform2d(Distance x, Distance y, Rotation2d rotation) {
081    this(x.in(Meters), y.in(Meters), rotation);
082  }
083
084  /**
085   * Constructs a transform with the specified affine transformation matrix.
086   *
087   * @param matrix The affine transformation matrix.
088   * @throws IllegalArgumentException if the affine transformation matrix is invalid.
089   */
090  public Transform2d(Matrix<N3, N3> matrix) {
091    m_translation = new Translation2d(matrix.get(0, 2), matrix.get(1, 2));
092    m_rotation = new Rotation2d(matrix.block(2, 2, 0, 0));
093    if (matrix.get(2, 0) != 0.0 || matrix.get(2, 1) != 0.0 || matrix.get(2, 2) != 1.0) {
094      throw new IllegalArgumentException("Affine transformation matrix is invalid");
095    }
096  }
097
098  /** Constructs the identity transform -- maps an initial pose to itself. */
099  public Transform2d() {
100    m_translation = Translation2d.kZero;
101    m_rotation = Rotation2d.kZero;
102  }
103
104  /**
105   * Multiplies the transform by the scalar.
106   *
107   * @param scalar The scalar.
108   * @return The scaled Transform2d.
109   */
110  public Transform2d times(double scalar) {
111    return new Transform2d(m_translation.times(scalar), m_rotation.times(scalar));
112  }
113
114  /**
115   * Divides the transform by the scalar.
116   *
117   * @param scalar The scalar.
118   * @return The scaled Transform2d.
119   */
120  public Transform2d div(double scalar) {
121    return times(1.0 / scalar);
122  }
123
124  /**
125   * Composes two transformations. The second transform is applied relative to the orientation of
126   * the first.
127   *
128   * @param other The transform to compose with this one.
129   * @return The composition of the two transformations.
130   */
131  public Transform2d plus(Transform2d other) {
132    return new Transform2d(Pose2d.kZero, Pose2d.kZero.transformBy(this).transformBy(other));
133  }
134
135  /**
136   * Returns the translation component of the transformation.
137   *
138   * @return The translational component of the transform.
139   */
140  public Translation2d getTranslation() {
141    return m_translation;
142  }
143
144  /**
145   * Returns the X component of the transformation's translation.
146   *
147   * @return The x component of the transformation's translation.
148   */
149  public double getX() {
150    return m_translation.getX();
151  }
152
153  /**
154   * Returns the Y component of the transformation's translation.
155   *
156   * @return The y component of the transformation's translation.
157   */
158  public double getY() {
159    return m_translation.getY();
160  }
161
162  /**
163   * Returns the X component of the transformation's translation in a measure.
164   *
165   * @return The x component of the transformation's translation in a measure.
166   */
167  public Distance getMeasureX() {
168    return m_translation.getMeasureX();
169  }
170
171  /**
172   * Returns the Y component of the transformation's translation in a measure.
173   *
174   * @return The y component of the transformation's translation in a measure.
175   */
176  public Distance getMeasureY() {
177    return m_translation.getMeasureY();
178  }
179
180  /**
181   * Returns an affine transformation matrix representation of this transformation.
182   *
183   * @return An affine transformation matrix representation of this transformation.
184   */
185  public Matrix<N3, N3> toMatrix() {
186    var vec = m_translation.toVector();
187    var mat = m_rotation.toMatrix();
188    return MatBuilder.fill(
189        Nat.N3(),
190        Nat.N3(),
191        mat.get(0, 0),
192        mat.get(0, 1),
193        vec.get(0),
194        mat.get(1, 0),
195        mat.get(1, 1),
196        vec.get(1),
197        0.0,
198        0.0,
199        1.0);
200  }
201
202  /**
203   * Returns the rotational component of the transformation.
204   *
205   * @return Reference to the rotational component of the transform.
206   */
207  public Rotation2d getRotation() {
208    return m_rotation;
209  }
210
211  /**
212   * Returns a Twist2d of the current transform (pose delta). If b is the output of {@code a.log()},
213   * then {@code b.exp()} would yield a.
214   *
215   * @return The twist that maps the current transform.
216   */
217  public Twist2d log() {
218    final double dtheta = m_rotation.getRadians();
219    final double halfDtheta = dtheta / 2.0;
220
221    final double cosMinusOne = m_rotation.getCos() - 1;
222
223    double halfThetaByTanOfHalfDtheta;
224    if (Math.abs(cosMinusOne) < 1E-9) {
225      halfThetaByTanOfHalfDtheta = 1.0 - 1.0 / 12.0 * dtheta * dtheta;
226    } else {
227      halfThetaByTanOfHalfDtheta = -(halfDtheta * m_rotation.getSin()) / cosMinusOne;
228    }
229
230    Translation2d translationPart =
231        m_translation
232            .rotateBy(new Rotation2d(halfThetaByTanOfHalfDtheta, -halfDtheta))
233            .times(Math.hypot(halfThetaByTanOfHalfDtheta, halfDtheta));
234
235    return new Twist2d(translationPart.getX(), translationPart.getY(), dtheta);
236  }
237
238  /**
239   * Invert the transformation. This is useful for undoing a transformation.
240   *
241   * @return The inverted transformation.
242   */
243  public Transform2d inverse() {
244    // We are rotating the difference between the translations
245    // using a clockwise rotation matrix. This transforms the global
246    // delta into a local delta (relative to the initial pose).
247    return new Transform2d(
248        getTranslation().unaryMinus().rotateBy(getRotation().unaryMinus()),
249        getRotation().unaryMinus());
250  }
251
252  @Override
253  public String toString() {
254    return String.format("Transform2d(%s, %s)", m_translation, m_rotation);
255  }
256
257  /**
258   * Checks equality between this Transform2d and another object.
259   *
260   * @param obj The other object.
261   * @return Whether the two objects are equal or not.
262   */
263  @Override
264  public boolean equals(Object obj) {
265    return obj instanceof Transform2d other
266        && other.m_translation.equals(m_translation)
267        && other.m_rotation.equals(m_rotation);
268  }
269
270  @Override
271  public int hashCode() {
272    return Objects.hash(m_translation, m_rotation);
273  }
274
275  /** Transform2d protobuf for serialization. */
276  public static final Transform2dProto proto = new Transform2dProto();
277
278  /** Transform2d struct for serialization. */
279  public static final Transform2dStruct struct = new Transform2dStruct();
280}