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 edu.wpi.first.math.geometry;
006
007import static edu.wpi.first.units.Units.Meters;
008
009import edu.wpi.first.math.geometry.proto.Rectangle2dProto;
010import edu.wpi.first.math.geometry.struct.Rectangle2dStruct;
011import edu.wpi.first.units.measure.Distance;
012import edu.wpi.first.util.protobuf.ProtobufSerializable;
013import edu.wpi.first.util.struct.StructSerializable;
014import java.util.Objects;
015
016/**
017 * Represents a 2d rectangular space containing translational, rotational, and scaling components.
018 */
019public class Rectangle2d implements ProtobufSerializable, StructSerializable {
020  private final Pose2d m_center;
021  private final double m_xWidth;
022  private final double m_yWidth;
023
024  /**
025   * Constructs a rectangle at the specified position with the specified width and height.
026   *
027   * @param center The position (translation and rotation) of the rectangle.
028   * @param xWidth The x size component of the rectangle, in unrotated coordinate frame.
029   * @param yWidth The y size component of the rectangle, in unrotated coordinate frame.
030   */
031  public Rectangle2d(Pose2d center, double xWidth, double yWidth) {
032    if (xWidth < 0 || yWidth < 0) {
033      throw new IllegalArgumentException("Rectangle2d dimensions cannot be less than 0!");
034    }
035
036    m_center = center;
037    m_xWidth = xWidth;
038    m_yWidth = yWidth;
039  }
040
041  /**
042   * Constructs a rectangle at the specified position with the specified width and height. The X and
043   * Y widths will be converted to and tracked as meters.
044   *
045   * @param center The position (translation and rotation) of the rectangle.
046   * @param xWidth The x size component of the rectangle, in unrotated coordinate frame.
047   * @param yWidth The y size component of the rectangle, in unrotated coordinate frame.
048   */
049  public Rectangle2d(Pose2d center, Distance xWidth, Distance yWidth) {
050    this(center, xWidth.in(Meters), yWidth.in(Meters));
051  }
052
053  /**
054   * Creates an unrotated rectangle from the given corners. The corners should be diagonally
055   * opposite of each other.
056   *
057   * @param cornerA The first corner of the rectangle.
058   * @param cornerB The second corner of the rectangle.
059   */
060  public Rectangle2d(Translation2d cornerA, Translation2d cornerB) {
061    this(
062        new Pose2d(cornerA.plus(cornerB).div(2.0), Rotation2d.kZero),
063        Math.abs(cornerA.getX() - cornerB.getX()),
064        Math.abs(cornerA.getY() - cornerB.getY()));
065  }
066
067  /**
068   * Returns the center of the rectangle.
069   *
070   * @return The center of the rectangle.
071   */
072  public Pose2d getCenter() {
073    return m_center;
074  }
075
076  /**
077   * Returns the rotational component of the rectangle.
078   *
079   * @return The rotational component of the rectangle.
080   */
081  public Rotation2d getRotation() {
082    return m_center.getRotation();
083  }
084
085  /**
086   * Returns the x size component of the rectangle.
087   *
088   * @return The x size component of the rectangle.
089   */
090  public double getXWidth() {
091    return m_xWidth;
092  }
093
094  /**
095   * Returns the y size component of the rectangle.
096   *
097   * @return The y size component of the rectangle.
098   */
099  public double getYWidth() {
100    return m_yWidth;
101  }
102
103  /**
104   * Returns the X size component of the rectangle in a measure.
105   *
106   * @return The x size component of the rectangle in a measure.
107   */
108  public Distance getMeasureXWidth() {
109    return Meters.of(m_xWidth);
110  }
111
112  /**
113   * Returns the Y size component of the rectangle in a measure.
114   *
115   * @return The y size component of the rectangle in a measure.
116   */
117  public Distance getMeasureYWidth() {
118    return Meters.of(m_yWidth);
119  }
120
121  /**
122   * Transforms the center of the rectangle and returns the new rectangle.
123   *
124   * @param other The transform to transform by.
125   * @return The transformed rectangle
126   */
127  public Rectangle2d transformBy(Transform2d other) {
128    return new Rectangle2d(m_center.transformBy(other), m_xWidth, m_yWidth);
129  }
130
131  /**
132   * Rotates the center of the rectangle and returns the new rectangle.
133   *
134   * @param other The rotation to transform by.
135   * @return The rotated rectangle.
136   */
137  public Rectangle2d rotateBy(Rotation2d other) {
138    return new Rectangle2d(m_center.rotateBy(other), m_xWidth, m_yWidth);
139  }
140
141  /**
142   * Checks if a point is intersected by the rectangle's perimeter.
143   *
144   * @param point The point to check.
145   * @return True, if the rectangle's perimeter intersects the point.
146   */
147  public boolean intersects(Translation2d point) {
148    // Move the point into the rectangle's coordinate frame
149    point = point.minus(m_center.getTranslation());
150    point = point.rotateBy(m_center.getRotation().unaryMinus());
151
152    if (Math.abs(Math.abs(point.getX()) - m_xWidth / 2.0) <= 1E-9) {
153      // Point rests on left/right perimeter
154      return Math.abs(point.getY()) <= m_yWidth / 2.0;
155    } else if (Math.abs(Math.abs(point.getY()) - m_yWidth / 2.0) <= 1E-9) {
156      // Point rests on top/bottom perimeter
157      return Math.abs(point.getX()) <= m_xWidth / 2.0;
158    }
159
160    return false;
161  }
162
163  /**
164   * Checks if a point is contained within the rectangle. This is inclusive, if the point lies on
165   * the perimeter it will return true.
166   *
167   * @param point The point to check.
168   * @return True, if the rectangle contains the point or the perimeter intersects the point.
169   */
170  public boolean contains(Translation2d point) {
171    // Rotate the point into the rectangle's coordinate frame
172    point = point.rotateAround(m_center.getTranslation(), m_center.getRotation().unaryMinus());
173
174    // Check if within bounding box
175    return point.getX() >= (m_center.getX() - m_xWidth / 2.0)
176        && point.getX() <= (m_center.getX() + m_xWidth / 2.0)
177        && point.getY() >= (m_center.getY() - m_yWidth / 2.0)
178        && point.getY() <= (m_center.getY() + m_yWidth / 2.0);
179  }
180
181  /**
182   * Returns the distance between the perimeter of the rectangle and the point.
183   *
184   * @param point The point to check.
185   * @return The distance (0, if the point is contained by the rectangle)
186   */
187  public double getDistance(Translation2d point) {
188    return nearest(point).getDistance(point);
189  }
190
191  /**
192   * Returns the distance between the perimeter of the rectangle and the point in a measure.
193   *
194   * @param point The point to check.
195   * @return The distance (0, if the point is contained by the rectangle) in a measure.
196   */
197  public Distance getMeasureDistance(Translation2d point) {
198    return Meters.of(getDistance(point));
199  }
200
201  /**
202   * Returns the nearest point that is contained within the rectangle.
203   *
204   * @param point The point that this will find the nearest point to.
205   * @return A new point that is nearest to {@code point} and contained in the rectangle.
206   */
207  public Translation2d nearest(Translation2d point) {
208    // Check if already in rectangle
209    if (contains(point)) {
210      return point;
211    }
212
213    // Rotate the point by the inverse of the rectangle's rotation
214    point = point.rotateAround(m_center.getTranslation(), m_center.getRotation().unaryMinus());
215
216    // Find nearest point
217    point =
218        new Translation2d(
219            Math.clamp(
220                point.getX(), m_center.getX() - m_xWidth / 2.0, m_center.getX() + m_xWidth / 2.0),
221            Math.clamp(
222                point.getY(), m_center.getY() - m_yWidth / 2.0, m_center.getY() + m_yWidth / 2.0));
223
224    // Undo rotation
225    return point.rotateAround(m_center.getTranslation(), m_center.getRotation());
226  }
227
228  @Override
229  public String toString() {
230    return String.format("Rectangle2d(center: %s, x: %.2f, y: %.2f)", m_center, m_xWidth, m_yWidth);
231  }
232
233  /**
234   * Checks equality between this Rectangle2d and another object.
235   *
236   * @param obj The other object.
237   * @return Whether the two objects are equal or not.
238   */
239  @Override
240  public boolean equals(Object obj) {
241    if (obj instanceof Rectangle2d) {
242      return ((Rectangle2d) obj).getCenter().equals(m_center)
243          && ((Rectangle2d) obj).getXWidth() == m_xWidth
244          && ((Rectangle2d) obj).getYWidth() == m_yWidth;
245    }
246    return false;
247  }
248
249  @Override
250  public int hashCode() {
251    return Objects.hash(m_center, m_xWidth, m_yWidth);
252  }
253
254  /** Rectangle2d protobuf for serialization. */
255  public static final Rectangle2dProto proto = new Rectangle2dProto();
256
257  /** Rectangle2d struct for serialization. */
258  public static final Rectangle2dStruct struct = new Rectangle2dStruct();
259}