Skip to content
This repository was archived by the owner on Feb 14, 2023. It is now read-only.
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.

Commit df14c7b

Browse files
varun7654Aum-PjwbonnerAyushk2023
committedSep 3, 2022
Add circle fitting for doing vision tracking
Most code stolen from FRC 6328 Mechanical Advantage Co-Authored-By: Aum-P <[email protected]> Co-Authored-By: Jonah <[email protected]> Co-Authored-By: Ayushk2023 <[email protected]>
1 parent bb82e07 commit df14c7b

File tree

4 files changed

+479
-11
lines changed

4 files changed

+479
-11
lines changed
 

‎src/main/java/frc/subsystem/VisionManager.java

+287-6
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
import edu.wpi.first.math.Nat;
88
import edu.wpi.first.math.geometry.Pose2d;
99
import edu.wpi.first.math.geometry.Rotation2d;
10+
import edu.wpi.first.math.geometry.Transform2d;
1011
import edu.wpi.first.math.geometry.Translation2d;
1112
import edu.wpi.first.math.numbers.N1;
1213
import edu.wpi.first.math.numbers.N3;
@@ -18,9 +19,11 @@
1819
import frc.robot.Constants;
1920
import frc.subsystem.BlinkinLED.BlinkinLedMode;
2021
import frc.subsystem.BlinkinLED.LedStatus;
22+
import frc.utility.CircleFitter;
2123
import frc.utility.Limelight;
2224
import frc.utility.Limelight.LedMode;
2325
import frc.utility.Limelight.LimelightResolution;
26+
import frc.utility.geometry.GeometryUtils;
2427
import frc.utility.net.editing.LiveEditableValue;
2528
import org.apache.commons.math3.geometry.euclidean.threed.Rotation;
2629
import org.apache.commons.math3.geometry.euclidean.threed.RotationConvention;
@@ -30,10 +33,7 @@
3033
import org.jetbrains.annotations.NotNull;
3134
import org.jetbrains.annotations.Nullable;
3235

33-
import java.util.HashSet;
34-
import java.util.Objects;
35-
import java.util.Optional;
36-
import java.util.Set;
36+
import java.util.*;
3737
import java.util.concurrent.atomic.AtomicInteger;
3838
import java.util.concurrent.locks.ReentrantReadWriteLock;
3939

@@ -122,10 +122,11 @@ public void logData() {
122122
private static final MatBuilder<N3, N1> THREE_BY_ONE_MAT_BUILDER = new MatBuilder<>(Nat.N3(), Nat.N1());
123123

124124
private final LiveEditableValue<Rotation> cameraRotation;
125-
125+
private final LiveEditableValue<Rotation2d> cameraRotation2d;
126126
private final LiveEditableValue<Double> hOffset;
127127
private final LiveEditableValue<Double> depthOffset;
128128
private final LiveEditableValue<Vector3D> centerOffset;
129+
private final LiveEditableValue<Transform2d> centerOffsetTranslation2d;
129130

130131
{
131132
NetworkTable guiTable = limelight.limelightGuiTable;
@@ -135,6 +136,13 @@ public void logData() {
135136
guiTable.getEntry("centerOffset"),
136137
(value) -> new Vector3D(0, 0, (Double) value),
137138
Vector3D::getZ);
139+
140+
centerOffsetTranslation2d = new LiveEditableValue<>(
141+
new Transform2d(new Translation2d(IS_PRACTICE ? 6.9 : 18, 0), new Rotation2d()),
142+
guiTable.getEntry("centerOffsetNew"),
143+
(value) -> new Transform2d(new Translation2d((Double) value, 0), new Rotation2d()),
144+
Transform2d::getX);
145+
138146
cameraRotation = new LiveEditableValue<>(
139147
new Rotation(RotationOrder.XYZ, RotationConvention.VECTOR_OPERATOR,
140148
Math.toRadians(IS_PRACTICE ? -37.5 : -34.5), 0, 0),
@@ -146,6 +154,13 @@ public void logData() {
146154
(value) ->
147155
Math.toDegrees(value.getAngles(RotationOrder.XYZ, RotationConvention.VECTOR_OPERATOR)[0])
148156
);
157+
158+
cameraRotation2d = new LiveEditableValue<>(
159+
new Rotation2d(Math.toRadians(IS_PRACTICE ? -37.5 : -34.5)),
160+
guiTable.getEntry("angle"),
161+
(value) -> Rotation2d.fromDegrees((Double) value),
162+
Rotation2d::getDegrees
163+
);
149164
}
150165

151166
private Vector3D getPositionOfTargetRelativeToRobot() {
@@ -277,6 +292,28 @@ public void update() {
277292
logData("Angle To Target", angleToTarget);
278293

279294

295+
Optional<Pose2d> circleFitVisionPoseOptional = processFrame();
296+
297+
if (circleFitVisionPoseOptional.isPresent()) {
298+
Pose2d circleFitVisionPose = circleFitVisionPoseOptional.get();
299+
300+
if (DriverStation.isTeleopEnabled()) {
301+
robotTracker.addVisionMeasurement(circleFitVisionPose.getTranslation(), getLimelightTime(), false);
302+
}
303+
RobotPositionSender.addRobotPosition(new RobotState(
304+
circleFitVisionPose.getX(),
305+
circleFitVisionPose.getY(),
306+
circleFitVisionPose.getRotation().getRadians(),
307+
getLimelightTime(),
308+
"Circle Fit Vision Pose"
309+
));
310+
311+
logData("Circle Fit Vision Pose X", circleFitVisionPose.getX());
312+
logData("Circle Fit Vision Pose Y", circleFitVisionPose.getY());
313+
logData("Circle Fit Vision Pose Angle", circleFitVisionPose.getRotation().getRadians());
314+
logData("Circle Fit Vision Pose Time", getLimelightTime());
315+
}
316+
280317
Optional<Translation2d> robotTranslationOptional = getVisionTranslation();
281318
if (robotTranslationOptional.isPresent()) {
282319
Translation2d robotTranslation = robotTranslationOptional.get();
@@ -306,7 +343,7 @@ public void update() {
306343
if (dist2(robotTracker.getLatencyCompedPoseMeters().getTranslation(),
307344
robotTranslation) < Constants.VISION_MANAGER_DISTANCE_THRESHOLD_SQUARED) {
308345
if (DriverStation.isTeleopEnabled()) {
309-
robotTracker.addVisionMeasurement(robotTranslation, getLimelightTime(), false);
346+
// robotTracker.addVisionMeasurement(robotTranslation, getLimelightTime(), false);
310347
}
311348

312349
logData("Using Vision Info", "Using Vision Info");
@@ -351,4 +388,248 @@ public void shootBallsUntilBeamBreakHasBroken(double shootTime) throws Interrupt
351388
public void close() throws Exception {
352389

353390
}
391+
392+
double lastCaptureTimestamp = 0;
393+
private static final int MIN_TARGET_COUNT = 3; // For calculating odometry
394+
private double lastTranslationsTimestamp;
395+
private List<Translation2d> lastTranslations;
396+
397+
public static final double VISION_TARGET_HEIGHT_LOWER =
398+
Units.inchesToMeters(8.0 * 12.0 + 5.625); // Bottom of tape
399+
public static final double VISION_TARGET_HEIGHT_UPPER = VISION_TARGET_HEIGHT_LOWER + Units.inchesToMeters(2.0); // Top of tape
400+
public static final double VISION_TARGET_DIAMETER = Units.inchesToMeters(4.0 * 12.0 + 5.375);
401+
private static final double CIRCLE_FIT_PRECISION = 0.01;
402+
403+
public static final double FIELD_LENGTH = Units.inchesToMeters(54.0 * 12.0);
404+
public static final double FIELD_WIDTH = Units.inchesToMeters(27.0 * 12.0);
405+
public static final Translation2d HUB_CENTER = new Translation2d(FIELD_LENGTH / 2.0, FIELD_WIDTH / 2.0);
406+
407+
/**
408+
* Process the current vision data
409+
*
410+
* @return The robot's position relative to the field
411+
* @author jonahb55
412+
* @author Aum-P
413+
* @author V-RA
414+
* @author Ayushk2023
415+
* @author Mechanical Advantage 6328
416+
*/
417+
private Optional<Pose2d> processFrame() {
418+
// noinspection FloatingPointEquality
419+
if (Limelight.getInstance().getTimestamp() == lastCaptureTimestamp) {
420+
// Exit if no new frame
421+
return Optional.empty();
422+
}
423+
424+
Limelight.getInstance().getTimestamp();
425+
lastCaptureTimestamp = Limelight.getInstance().getTimestamp();
426+
427+
CameraPosition cameraPosition = new CameraPosition(hOffset.get(), cameraRotation2d.get(),
428+
centerOffsetTranslation2d.get());
429+
Vector2d[] cornersRaw = Limelight.getInstance().getCorners();
430+
431+
int targetCount = cornersRaw.length / 4;
432+
// Calculate camera to target translation
433+
if (targetCount >= MIN_TARGET_COUNT && cornersRaw.length % 4 == 0) {
434+
// Calculate individual corner translations
435+
List<Translation2d> cameraToTargetTranslations = new ArrayList<>();
436+
for (int targetIndex = 0; targetIndex < targetCount; targetIndex++) {
437+
List<VisionPoint> corners = new ArrayList<>();
438+
double totalX = 0.0, totalY = 0.0;
439+
for (int i = targetIndex * 4; i < (targetIndex * 4) + 4; i++) {
440+
if (i < cornersRaw.length) {
441+
corners.add(new VisionPoint(cornersRaw[i].x, cornersRaw[i].y));
442+
totalX += cornersRaw[i].x;
443+
totalY += cornersRaw[i].y;
444+
}
445+
}
446+
447+
VisionPoint targetAvg = new VisionPoint(totalX / 4, totalY / 4);
448+
corners = sortCorners(corners, targetAvg);
449+
450+
for (int i = 0; i < corners.size(); i++) {
451+
Translation2d translation = solveCameraToTargetTranslation(
452+
corners.get(i), i < 2 ? VISION_TARGET_HEIGHT_LOWER : VISION_TARGET_HEIGHT_UPPER, cameraPosition);
453+
if (translation != null) {
454+
cameraToTargetTranslations.add(translation);
455+
}
456+
}
457+
}
458+
459+
// Save individual translations
460+
lastTranslationsTimestamp = Timer.getFPGATimestamp();
461+
lastTranslations = cameraToTargetTranslations;
462+
463+
// Combine corner translations to full target translation
464+
if (cameraToTargetTranslations.size() >= MIN_TARGET_COUNT * 4) {
465+
Translation2d cameraToTargetTranslation =
466+
CircleFitter.fit(VISION_TARGET_DIAMETER / 2.0,
467+
cameraToTargetTranslations, CIRCLE_FIT_PRECISION);
468+
469+
// Calculate field to robot translation
470+
Rotation2d robotRotation = RobotTracker.getInstance().getGyroRotation(lastCaptureTimestamp);
471+
Rotation2d cameraRotation = robotRotation
472+
.rotateBy(cameraPosition.vehicleToCamera.getRotation());
473+
Transform2d fieldToTargetRotated =
474+
new Transform2d(HUB_CENTER, cameraRotation);
475+
Transform2d fieldToCamera = fieldToTargetRotated.plus(
476+
GeometryUtils.transformFromTranslation(cameraToTargetTranslation.unaryMinus()));
477+
Pose2d fieldToVehicle =
478+
GeometryUtils.transformToPose(fieldToCamera.plus(cameraPosition.vehicleToCamera.inverse()));
479+
480+
if (fieldToVehicle.getX() > FIELD_LENGTH
481+
|| fieldToVehicle.getX() < 0.0
482+
|| fieldToVehicle.getY() > FIELD_WIDTH
483+
|| fieldToVehicle.getY() < 0.0) {
484+
return Optional.empty();
485+
}
486+
return Optional.of(fieldToVehicle);
487+
}
488+
}
489+
return Optional.empty();
490+
}
491+
492+
/**
493+
* @author jonahb55
494+
* @author Aum-P
495+
* @author V-RA
496+
* @author Ayushk2023
497+
* @author Mechanical Advantage 6328
498+
*/
499+
private List<VisionPoint> sortCorners(List<VisionPoint> corners,
500+
VisionPoint average) {
501+
502+
// Find top corners
503+
Integer topLeftIndex = null;
504+
Integer topRightIndex = null;
505+
double minPosRads = Math.PI;
506+
double minNegRads = Math.PI;
507+
for (int i = 0; i < corners.size(); i++) {
508+
VisionPoint corner = corners.get(i);
509+
double angleRad =
510+
new Rotation2d(corner.x - average.x, average.y - corner.y)
511+
.minus(Rotation2d.fromDegrees(90)).getRadians();
512+
if (angleRad > 0) {
513+
if (angleRad < minPosRads) {
514+
minPosRads = angleRad;
515+
topLeftIndex = i;
516+
}
517+
} else {
518+
if (Math.abs(angleRad) < minNegRads) {
519+
minNegRads = Math.abs(angleRad);
520+
topRightIndex = i;
521+
}
522+
}
523+
}
524+
525+
// Find lower corners
526+
Integer lowerIndex1 = null;
527+
Integer lowerIndex2 = null;
528+
for (int i = 0; i < corners.size(); i++) {
529+
boolean alreadySaved = false;
530+
if (topLeftIndex != null) {
531+
if (topLeftIndex.equals(i)) {
532+
alreadySaved = true;
533+
}
534+
}
535+
if (topRightIndex != null) {
536+
if (topRightIndex.equals(i)) {
537+
alreadySaved = true;
538+
}
539+
}
540+
if (!alreadySaved) {
541+
if (lowerIndex1 == null) {
542+
lowerIndex1 = i;
543+
} else {
544+
lowerIndex2 = i;
545+
}
546+
}
547+
}
548+
549+
// Combine final list
550+
List<VisionPoint> newCorners = new ArrayList<>();
551+
if (topLeftIndex != null) {
552+
newCorners.add(corners.get(topLeftIndex));
553+
}
554+
if (topRightIndex != null) {
555+
newCorners.add(corners.get(topRightIndex));
556+
}
557+
if (lowerIndex1 != null) {
558+
newCorners.add(corners.get(lowerIndex1));
559+
}
560+
if (lowerIndex2 != null) {
561+
newCorners.add(corners.get(lowerIndex2));
562+
}
563+
return newCorners;
564+
}
565+
566+
public static final Rotation2d FOV_HORIZONTAL = Rotation2d.fromDegrees(59.6);
567+
public static final Rotation2d FOV_VERTICAL = Rotation2d.fromDegrees(49.7);
568+
569+
private static final double vpw = 2.0 * Math.tan(FOV_HORIZONTAL.getRadians() / 2.0);
570+
private static final double vph = 2.0 * Math.tan(FOV_VERTICAL.getRadians() / 2.0);
571+
572+
/**
573+
* Camera offsets in pixels?
574+
*/
575+
public static final double CROSSHAIR_X = 0;
576+
public static final double CROSSHAIR_Y = 0;
577+
578+
/**
579+
* @author jonahb55
580+
* @author Aum-P
581+
* @author V-RA
582+
* @author Ayushk2023
583+
* @author Mechanical Advantage 6328
584+
*/
585+
private Translation2d solveCameraToTargetTranslation(VisionPoint corner, double goalHeight, CameraPosition cameraPosition) {
586+
587+
double halfWidthPixels = limelight.getCameraResolution().width / 2.0;
588+
double halfHeightPixels = limelight.getCameraResolution().height / 2.0;
589+
double nY = -((corner.x - halfWidthPixels - CROSSHAIR_X) / halfWidthPixels);
590+
double nZ = -((corner.y - halfHeightPixels - CROSSHAIR_Y) / halfHeightPixels);
591+
592+
Translation2d xzPlaneTranslation = new Translation2d(1.0, vph / 2.0 * nZ).rotateBy(cameraPosition.verticalRotation);
593+
double x = xzPlaneTranslation.getX();
594+
double y = vpw / 2.0 * nY;
595+
double z = xzPlaneTranslation.getY();
596+
597+
double differentialHeight = cameraPosition.cameraHeight - goalHeight;
598+
if ((z < 0.0) == (differentialHeight > 0.0)) {
599+
double scaling = differentialHeight / -z;
600+
double distance = Math.hypot(x, y) * scaling;
601+
Rotation2d angle = new Rotation2d(x, y);
602+
return new Translation2d(distance * angle.getCos(), distance * angle.getSin());
603+
}
604+
return null;
605+
}
606+
607+
/**
608+
* @author jonahb55
609+
*/
610+
public static class VisionPoint {
611+
public final double x;
612+
public final double y;
613+
614+
public VisionPoint(double x, double y) {
615+
this.x = x;
616+
this.y = y;
617+
}
618+
}
619+
620+
/**
621+
* @author jonahb55
622+
*/
623+
public static final class CameraPosition {
624+
public final double cameraHeight;
625+
public final Rotation2d verticalRotation;
626+
public final Transform2d vehicleToCamera;
627+
628+
public CameraPosition(double cameraHeight, Rotation2d verticalRotation,
629+
Transform2d vehicleToCamera) {
630+
this.cameraHeight = cameraHeight;
631+
this.verticalRotation = verticalRotation;
632+
this.vehicleToCamera = vehicleToCamera;
633+
}
634+
}
354635
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
package frc.utility;
2+
3+
import edu.wpi.first.math.geometry.Translation2d;
4+
5+
import java.util.List;
6+
7+
/**
8+
* @author jonahb55
9+
* @author Mechanical Advantage 6328
10+
*/
11+
public final class CircleFitter {
12+
private CircleFitter() {}
13+
14+
public static Translation2d fit(double radius, List<Translation2d> points,
15+
double precision) {
16+
17+
// Find starting point
18+
double xSum = 0.0;
19+
double ySum = 0.0;
20+
for (Translation2d point : points) {
21+
xSum += point.getX();
22+
ySum += point.getY();
23+
}
24+
Translation2d center =
25+
new Translation2d(xSum / points.size() + radius, ySum / points.size());
26+
27+
// Iterate to find optimal center
28+
double shiftDist = radius / 2.0;
29+
double minResidual = calcResidual(radius, points, center);
30+
while (true) {
31+
List<Translation2d> translations = List.of(
32+
new Translation2d(shiftDist, 0.0), new Translation2d(-shiftDist, 0.0),
33+
new Translation2d(0.0, shiftDist),
34+
new Translation2d(0.0, -shiftDist));
35+
Translation2d bestPoint = center;
36+
boolean centerIsBest = true;
37+
38+
// Check all adjacent positions
39+
for (Translation2d translation : translations) {
40+
double residual =
41+
calcResidual(radius, points, center.plus(translation));
42+
if (residual < minResidual) {
43+
bestPoint = center.plus(translation);
44+
minResidual = residual;
45+
centerIsBest = false;
46+
break;
47+
}
48+
}
49+
50+
// Decrease shift, exit, or continue
51+
if (centerIsBest) {
52+
shiftDist /= 2.0;
53+
if (shiftDist < precision) {
54+
return center;
55+
}
56+
} else {
57+
center = bestPoint;
58+
}
59+
}
60+
}
61+
62+
private static double calcResidual(double radius, List<Translation2d> points,
63+
Translation2d center) {
64+
double residual = 0.0;
65+
for (Translation2d point : points) {
66+
double diff = point.getDistance(center) - radius;
67+
residual += diff * diff;
68+
}
69+
return residual;
70+
}
71+
}

‎src/main/java/frc/utility/Limelight.java

+6-3
Original file line numberDiff line numberDiff line change
@@ -37,16 +37,19 @@ public enum LimelightResolution {
3737
this.height = height;
3838
}
3939

40-
final int width;
41-
final int height;
40+
public final int width;
41+
public final int height;
4242
}
4343

44-
public LimelightResolution cameraResolution = LimelightResolution.k320x240;
44+
private LimelightResolution cameraResolution = LimelightResolution.k320x240;
4545

4646
public void setCameraResolution(LimelightResolution resolution) {
4747
cameraResolution = resolution;
4848
}
4949

50+
public LimelightResolution getCameraResolution() {
51+
return cameraResolution;
52+
}
5053

5154
public static @NotNull Limelight getInstance(String name) {
5255
LIMELIGHT_MAP_LOCK.readLock().lock();

‎src/main/java/frc/utility/geometry/GeometryUtils.java

+115-2
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
package frc.utility.geometry;
22

3-
import edu.wpi.first.math.geometry.Rotation2d;
4-
import edu.wpi.first.math.geometry.Translation2d;
3+
import edu.wpi.first.math.geometry.*;
54
import org.jetbrains.annotations.Contract;
65
import org.jetbrains.annotations.NotNull;
76

@@ -14,9 +13,123 @@ private GeometryUtils() {}
1413
*
1514
* @param translation The translation2d to compute the angle of.
1615
* @return The angle of the translation. (in radians)
16+
* @author Varun
1717
*/
1818
@Contract(value = "_ -> new", pure = true)
1919
public static @NotNull Rotation2d angleOf(@NotNull Translation2d translation) {
2020
return new Rotation2d(Math.atan2(translation.getY(), translation.getX()));
2121
}
22+
23+
/**
24+
* Creates a pure translating transform
25+
*
26+
* @param translation The translation to create the transform with
27+
* @return The resulting transform
28+
* @author jonahb55
29+
*/
30+
public static Transform2d transformFromTranslation(
31+
Translation2d translation) {
32+
return new Transform2d(translation, new Rotation2d());
33+
}
34+
35+
/**
36+
* Creates a pure translating transform
37+
*
38+
* @param x The x componenet of the translation
39+
* @param y The y componenet of the translation
40+
* @return The resulting transform
41+
* @author jonahb55
42+
*/
43+
public static Transform2d transformFromTranslation(double x, double y) {
44+
return new Transform2d(new Translation2d(x, y), new Rotation2d());
45+
}
46+
47+
/**
48+
* Creates a pure rotating transform
49+
*
50+
* @param rotation The rotation to create the transform with
51+
* @return The resulting transform
52+
* @author jonahb55
53+
*/
54+
public static Transform2d transformFromRotation(Rotation2d rotation) {
55+
return new Transform2d(new Translation2d(), rotation);
56+
}
57+
58+
/**
59+
* Creates a pure translated pose
60+
*
61+
* @param translation The translation to create the pose with
62+
* @return The resulting pose
63+
* @author jonahb55
64+
*/
65+
public static Pose2d poseFromTranslation(Translation2d translation) {
66+
return new Pose2d(translation, new Rotation2d());
67+
}
68+
69+
/**
70+
* Creates a pure rotated pose
71+
*
72+
* @param rotation The rotation to create the pose with
73+
* @return The resulting pose
74+
* @author jonahb55
75+
*/
76+
public static Pose2d poseFromRotation(Rotation2d rotation) {
77+
return new Pose2d(new Translation2d(), rotation);
78+
}
79+
80+
/**
81+
* Converts a Pose2d to a Transform2d to be used in a kinematic chain
82+
*
83+
* @param pose The pose that will represent the transform
84+
* @return The resulting transform
85+
* @author jonahb55
86+
*/
87+
public static Transform2d poseToTransform(Pose2d pose) {
88+
return new Transform2d(pose.getTranslation(), pose.getRotation());
89+
}
90+
91+
/**
92+
* Converts a Transform2d to a Pose2d to be used as a position or as the start of a kinematic chain
93+
*
94+
* @param transform The transform that will represent the pose
95+
* @return The resulting pose
96+
* @author jonahb55
97+
*/
98+
public static Pose2d transformToPose(Transform2d transform) {
99+
return new Pose2d(transform.getTranslation(), transform.getRotation());
100+
}
101+
102+
/**
103+
* Interpolates between two poses based on the scale factor t. For example, t=0 would result in the first pose, t=1 would
104+
* result in the last pose, and t=0.5 would result in a pose which is exactly halfway between the two poses. Values of t less
105+
* than zero return the first pose, and values of t greater than 1 return the last pose.
106+
*
107+
* @param lhs The left hand side, or first pose to use for interpolation
108+
* @param rhs The right hand side, or last pose to use for interpolation
109+
* @param t The scale factor, 0 <= t <= 1
110+
* @return The pose which represents the interpolation. For t <= 0, the "lhs" parameter is returned directly. For t >= 1, the
111+
* "rhs" parameter is returned directly.
112+
* @author jonahb55
113+
*/
114+
public static Pose2d interpolate(Pose2d lhs, Pose2d rhs, double t) {
115+
if (t <= 0) {
116+
return lhs;
117+
} else if (t >= 1) {
118+
return rhs;
119+
}
120+
Twist2d twist = lhs.log(rhs);
121+
Twist2d scaled = new Twist2d(twist.dx * t, twist.dy * t, twist.dtheta * t);
122+
return lhs.exp(scaled);
123+
}
124+
125+
/**
126+
* Returns the direction that this translation makes with the origin as a Rotation2d
127+
*
128+
* @param translation The translation
129+
* @return The direction of the translation
130+
* @author jonahb55
131+
*/
132+
public static Rotation2d direction(Translation2d translation) {
133+
return new Rotation2d(translation.getX(), translation.getY());
134+
}
22135
}

0 commit comments

Comments
 (0)
This repository has been archived.