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