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.Radians; 008 009import com.fasterxml.jackson.annotation.JsonAutoDetect; 010import com.fasterxml.jackson.annotation.JsonCreator; 011import com.fasterxml.jackson.annotation.JsonIgnoreProperties; 012import com.fasterxml.jackson.annotation.JsonProperty; 013import edu.wpi.first.math.MatBuilder; 014import edu.wpi.first.math.MathSharedStore; 015import edu.wpi.first.math.MathUtil; 016import edu.wpi.first.math.Matrix; 017import edu.wpi.first.math.Nat; 018import edu.wpi.first.math.geometry.proto.Rotation2dProto; 019import edu.wpi.first.math.geometry.struct.Rotation2dStruct; 020import edu.wpi.first.math.interpolation.Interpolatable; 021import edu.wpi.first.math.numbers.N2; 022import edu.wpi.first.math.util.Units; 023import edu.wpi.first.units.measure.Angle; 024import edu.wpi.first.util.protobuf.ProtobufSerializable; 025import edu.wpi.first.util.struct.StructSerializable; 026import java.util.Objects; 027 028/** 029 * A rotation in a 2D coordinate frame represented by a point on the unit circle (cosine and sine). 030 * 031 * <p>The angle is continuous, that is if a Rotation2d is constructed with 361 degrees, it will 032 * return 361 degrees. This allows algorithms that wouldn't want to see a discontinuity in the 033 * rotations as it sweeps past from 360 to 0 on the second time around. 034 */ 035@JsonIgnoreProperties(ignoreUnknown = true) 036@JsonAutoDetect(getterVisibility = JsonAutoDetect.Visibility.NONE) 037public class Rotation2d 038 implements Interpolatable<Rotation2d>, ProtobufSerializable, StructSerializable { 039 /** 040 * A preallocated Rotation2d representing no rotation. 041 * 042 * <p>This exists to avoid allocations for common rotations. 043 */ 044 public static final Rotation2d kZero = new Rotation2d(); 045 046 /** 047 * A preallocated Rotation2d representing a clockwise rotation by π/2 rad (90°). 048 * 049 * <p>This exists to avoid allocations for common rotations. 050 */ 051 public static final Rotation2d kCW_Pi_2 = new Rotation2d(-Math.PI / 2); 052 053 /** 054 * A preallocated Rotation2d representing a clockwise rotation by 90° (π/2 rad). 055 * 056 * <p>This exists to avoid allocations for common rotations. 057 */ 058 public static final Rotation2d kCW_90deg = kCW_Pi_2; 059 060 /** 061 * A preallocated Rotation2d representing a counterclockwise rotation by π/2 rad (90°). 062 * 063 * <p>This exists to avoid allocations for common rotations. 064 */ 065 public static final Rotation2d kCCW_Pi_2 = new Rotation2d(Math.PI / 2); 066 067 /** 068 * A preallocated Rotation2d representing a counterclockwise rotation by 90° (π/2 rad). 069 * 070 * <p>This exists to avoid allocations for common rotations. 071 */ 072 public static final Rotation2d kCCW_90deg = kCCW_Pi_2; 073 074 /** 075 * A preallocated Rotation2d representing a counterclockwise rotation by π rad (180°). 076 * 077 * <p>This exists to avoid allocations for common rotations. 078 */ 079 public static final Rotation2d kPi = new Rotation2d(Math.PI); 080 081 /** 082 * A preallocated Rotation2d representing a counterclockwise rotation by 180° (π rad). 083 * 084 * <p>This exists to avoid allocations for common rotations. 085 */ 086 public static final Rotation2d k180deg = kPi; 087 088 private final double m_value; 089 private final double m_cos; 090 private final double m_sin; 091 092 /** Constructs a Rotation2d with a default angle of 0 degrees. */ 093 public Rotation2d() { 094 m_value = 0.0; 095 m_cos = 1.0; 096 m_sin = 0.0; 097 } 098 099 /** 100 * Constructs a Rotation2d with the given radian value. 101 * 102 * @param value The value of the angle in radians. 103 */ 104 @JsonCreator 105 public Rotation2d(@JsonProperty(required = true, value = "radians") double value) { 106 m_value = value; 107 m_cos = Math.cos(value); 108 m_sin = Math.sin(value); 109 } 110 111 /** 112 * Constructs a Rotation2d with the given x and y (cosine and sine) components. 113 * 114 * @param x The x component or cosine of the rotation. 115 * @param y The y component or sine of the rotation. 116 */ 117 public Rotation2d(double x, double y) { 118 double magnitude = Math.hypot(x, y); 119 if (magnitude > 1e-6) { 120 m_cos = x / magnitude; 121 m_sin = y / magnitude; 122 } else { 123 m_cos = 1.0; 124 m_sin = 0.0; 125 MathSharedStore.reportError( 126 "x and y components of Rotation2d are zero\n", Thread.currentThread().getStackTrace()); 127 } 128 m_value = Math.atan2(m_sin, m_cos); 129 } 130 131 /** 132 * Constructs a Rotation2d with the given angle. 133 * 134 * @param angle The angle of the rotation. 135 */ 136 public Rotation2d(Angle angle) { 137 this(angle.in(Radians)); 138 } 139 140 /** 141 * Constructs a Rotation2d from a rotation matrix. 142 * 143 * @param rotationMatrix The rotation matrix. 144 * @throws IllegalArgumentException if the rotation matrix isn't special orthogonal. 145 */ 146 public Rotation2d(Matrix<N2, N2> rotationMatrix) { 147 final var R = rotationMatrix; 148 149 // Require that the rotation matrix is special orthogonal. This is true if 150 // the matrix is orthogonal (RRᵀ = I) and normalized (determinant is 1). 151 if (R.times(R.transpose()).minus(Matrix.eye(Nat.N2())).normF() > 1e-9) { 152 var msg = "Rotation matrix isn't orthogonal\n\nR =\n" + R.getStorage().toString() + '\n'; 153 MathSharedStore.reportError(msg, Thread.currentThread().getStackTrace()); 154 throw new IllegalArgumentException(msg); 155 } 156 if (Math.abs(R.det() - 1.0) > 1e-9) { 157 var msg = 158 "Rotation matrix is orthogonal but not special orthogonal\n\nR =\n" 159 + R.getStorage().toString() 160 + '\n'; 161 MathSharedStore.reportError(msg, Thread.currentThread().getStackTrace()); 162 throw new IllegalArgumentException(msg); 163 } 164 165 // R = [cosθ −sinθ] 166 // [sinθ cosθ] 167 m_cos = R.get(0, 0); 168 m_sin = R.get(1, 0); 169 170 m_value = Math.atan2(m_sin, m_cos); 171 } 172 173 /** 174 * Constructs and returns a Rotation2d with the given radian value. 175 * 176 * @param radians The value of the angle in radians. 177 * @return The rotation object with the desired angle value. 178 */ 179 public static Rotation2d fromRadians(double radians) { 180 return new Rotation2d(radians); 181 } 182 183 /** 184 * Constructs and returns a Rotation2d with the given degree value. 185 * 186 * @param degrees The value of the angle in degrees. 187 * @return The rotation object with the desired angle value. 188 */ 189 public static Rotation2d fromDegrees(double degrees) { 190 return new Rotation2d(Math.toRadians(degrees)); 191 } 192 193 /** 194 * Constructs and returns a Rotation2d with the given number of rotations. 195 * 196 * @param rotations The value of the angle in rotations. 197 * @return The rotation object with the desired angle value. 198 */ 199 public static Rotation2d fromRotations(double rotations) { 200 return new Rotation2d(Units.rotationsToRadians(rotations)); 201 } 202 203 /** 204 * Adds two rotations together, with the result being bounded between -π and π. 205 * 206 * <p>For example, <code>Rotation2d.fromDegrees(30).plus(Rotation2d.fromDegrees(60))</code> equals 207 * <code>Rotation2d(Math.PI/2.0)</code> 208 * 209 * @param other The rotation to add. 210 * @return The sum of the two rotations. 211 */ 212 public Rotation2d plus(Rotation2d other) { 213 return rotateBy(other); 214 } 215 216 /** 217 * Subtracts the new rotation from the current rotation and returns the new rotation. 218 * 219 * <p>For example, <code>Rotation2d.fromDegrees(10).minus(Rotation2d.fromDegrees(100))</code> 220 * equals <code>Rotation2d(-Math.PI/2.0)</code> 221 * 222 * @param other The rotation to subtract. 223 * @return The difference between the two rotations. 224 */ 225 public Rotation2d minus(Rotation2d other) { 226 return rotateBy(other.unaryMinus()); 227 } 228 229 /** 230 * Takes the inverse of the current rotation. This is simply the negative of the current angular 231 * value. 232 * 233 * @return The inverse of the current rotation. 234 */ 235 public Rotation2d unaryMinus() { 236 return new Rotation2d(-m_value); 237 } 238 239 /** 240 * Multiplies the current rotation by a scalar. 241 * 242 * @param scalar The scalar. 243 * @return The new scaled Rotation2d. 244 */ 245 public Rotation2d times(double scalar) { 246 return new Rotation2d(m_value * scalar); 247 } 248 249 /** 250 * Divides the current rotation by a scalar. 251 * 252 * @param scalar The scalar. 253 * @return The new scaled Rotation2d. 254 */ 255 public Rotation2d div(double scalar) { 256 return times(1.0 / scalar); 257 } 258 259 /** 260 * Adds the new rotation to the current rotation using a rotation matrix. 261 * 262 * <p>The matrix multiplication is as follows: 263 * 264 * <pre> 265 * [cos_new] [other.cos, -other.sin][cos] 266 * [sin_new] = [other.sin, other.cos][sin] 267 * value_new = atan2(sin_new, cos_new) 268 * </pre> 269 * 270 * @param other The rotation to rotate by. 271 * @return The new rotated Rotation2d. 272 */ 273 public Rotation2d rotateBy(Rotation2d other) { 274 return new Rotation2d( 275 m_cos * other.m_cos - m_sin * other.m_sin, m_cos * other.m_sin + m_sin * other.m_cos); 276 } 277 278 /** 279 * Returns matrix representation of this rotation. 280 * 281 * @return Matrix representation of this rotation. 282 */ 283 public Matrix<N2, N2> toMatrix() { 284 // R = [cosθ −sinθ] 285 // [sinθ cosθ] 286 return MatBuilder.fill(Nat.N2(), Nat.N2(), m_cos, -m_sin, m_sin, m_cos); 287 } 288 289 /** 290 * Returns the measure of the Rotation2d. 291 * 292 * @return The measure of the Rotation2d. 293 */ 294 public Angle getMeasure() { 295 return Radians.of(getRadians()); 296 } 297 298 /** 299 * Returns the radian value of the Rotation2d. 300 * 301 * @return The radian value of the Rotation2d. 302 * @see edu.wpi.first.math.MathUtil#angleModulus(double) to constrain the angle within (-π, π] 303 */ 304 @JsonProperty 305 public double getRadians() { 306 return m_value; 307 } 308 309 /** 310 * Returns the degree value of the Rotation2d. 311 * 312 * @return The degree value of the Rotation2d. 313 * @see edu.wpi.first.math.MathUtil#inputModulus(double, double, double) to constrain the angle 314 * within (-180, 180] 315 */ 316 public double getDegrees() { 317 return Math.toDegrees(m_value); 318 } 319 320 /** 321 * Returns the number of rotations of the Rotation2d. 322 * 323 * @return The number of rotations of the Rotation2d. 324 */ 325 public double getRotations() { 326 return Units.radiansToRotations(m_value); 327 } 328 329 /** 330 * Returns the cosine of the Rotation2d. 331 * 332 * @return The cosine of the Rotation2d. 333 */ 334 public double getCos() { 335 return m_cos; 336 } 337 338 /** 339 * Returns the sine of the Rotation2d. 340 * 341 * @return The sine of the Rotation2d. 342 */ 343 public double getSin() { 344 return m_sin; 345 } 346 347 /** 348 * Returns the tangent of the Rotation2d. 349 * 350 * @return The tangent of the Rotation2d. 351 */ 352 public double getTan() { 353 return m_sin / m_cos; 354 } 355 356 @Override 357 public String toString() { 358 return String.format("Rotation2d(Rads: %.2f, Deg: %.2f)", m_value, Math.toDegrees(m_value)); 359 } 360 361 /** 362 * Checks equality between this Rotation2d and another object. 363 * 364 * @param obj The other object. 365 * @return Whether the two objects are equal or not. 366 */ 367 @Override 368 public boolean equals(Object obj) { 369 return obj instanceof Rotation2d other 370 && Math.hypot(m_cos - other.m_cos, m_sin - other.m_sin) < 1E-9; 371 } 372 373 @Override 374 public int hashCode() { 375 return Objects.hash(m_value); 376 } 377 378 @Override 379 public Rotation2d interpolate(Rotation2d endValue, double t) { 380 return plus(endValue.minus(this).times(MathUtil.clamp(t, 0, 1))); 381 } 382 383 /** Rotation2d protobuf for serialization. */ 384 public static final Rotation2dProto proto = new Rotation2dProto(); 385 386 /** Rotation2d struct for serialization. */ 387 public static final Rotation2dStruct struct = new Rotation2dStruct(); 388}