Skip to content

Commit

Permalink
feat: Make RotateTo+RotationType functionality more available (#3291)
Browse files Browse the repository at this point in the history
Closes #3288 

## Changes:

- Introduced `angleBetween` metdhod to `Vector` class. 
- fixed `canonicalizeAngle` to never return 2PI
- `RotationType` moved to `Math` 
- small documentation improvements
- added/modified tests to `Vector.rotate()` to illustrate the conventions

---------

Co-authored-by: petar-jr <[email protected]>
  • Loading branch information
isokissa and petar-jr authored Dec 9, 2024
1 parent 9ab78ee commit 5ecb464
Show file tree
Hide file tree
Showing 13 changed files with 122 additions and 18 deletions.
9 changes: 9 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -231,6 +231,13 @@ This project adheres to [Semantic Versioning](http://semver.org/).
- New `ex.SparseHashGridCollisionProcessor` which is a simpler (and faster) implementation for broadphase pair generation. This works by bucketing colliders into uniform sized square buckets and using that to generate pairs.
- CollisionContact can be biased toward a collider by using `contact.bias(collider)`. This adjusts the contact so that the given collider is colliderA, and is helpful if you
are doing mtv adjustments during precollision.
- `angleBetween` medhod added to Vector class, to find the angle for which a vector needs to be rotated to match some given angle:
```typescript
const point = vec(100, 100)
const destinationDirection = Math.PI / 4
const angleToRotate = point.angleBetween(destinationDirection, RotationType.ShortestPath)
expect(point.rotate(angleToRotate).toAngle()).toEqual(destinationDirection)
```

### Fixed

Expand Down Expand Up @@ -266,6 +273,8 @@ are doing mtv adjustments during precollision.
- Fixed issue where removing and re-adding an actor would cause subsequent children added not to function properly with regards to their parent/child transforms
- Fixed issue where `ex.GraphicsSystem` would crash if a parent entity did not have a `ex.TransformComponent`
- Fixed a bug in the new physics config merging, and re-arranged to better match the existing pattern
- Fixed a bug in `canonicalizeAngle`, don't allow the result to be 2PI, now it will be in semi-open range [0..2PI)
- Removed circular dependency between `Actions` and `Math` packages by moving `RotationType` into `Math` package.

### Updates

Expand Down
6 changes: 3 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -49,9 +49,9 @@ Please read our [Contributing Guidelines](.github/CONTRIBUTING.md) and our [Code

Prerequisites
* Docker for Mac https://docs.docker.com/desktop/mac/install/
* In the root, run `docker-compose build` (setup build environment and installs dependencies, only needed once)
* To run tests in watch mode `docker-compose run --rm dev npm run test:watch`
* To run a build `docker-compose run --rm dev npm run all`
* In the root, run `docker compose build` (setup build environment and installs dependencies, only needed once)
* To run tests in watch mode `docker compose run --rm dev npm run test:watch`
* To run a build `docker compose run --rm dev npm run all`

# Writing Documentation

Expand Down
3 changes: 1 addition & 2 deletions src/engine/Actions/Action/RotateBy.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,9 @@
import { Action, nextActionId } from '../Action';
import { RotationType } from '../RotationType';
import { TransformComponent } from '../../EntityComponentSystem/Components/TransformComponent';
import { MotionComponent } from '../../EntityComponentSystem/Components/MotionComponent';
import { Entity } from '../../EntityComponentSystem/Entity';
import { canonicalizeAngle, clamp, TwoPI } from '../../Math/util';
import { lerpAngle, remap } from '../../Math';
import { lerpAngle, remap, RotationType } from '../../Math';

export interface RotateByOptions {
/**
Expand Down
2 changes: 1 addition & 1 deletion src/engine/Actions/Action/RotateTo.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { Action, nextActionId } from '../Action';
import { RotationType } from '../RotationType';
import { RotationType } from '../../Math';
import { TransformComponent } from '../../EntityComponentSystem/Components/TransformComponent';
import { MotionComponent } from '../../EntityComponentSystem/Components/MotionComponent';
import { Entity } from '../../EntityComponentSystem/Entity';
Expand Down
4 changes: 1 addition & 3 deletions src/engine/Actions/ActionContext.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,3 @@
import { RotationType } from './RotationType';

import { EasingFunction, EasingFunctions } from '../Util/EasingFunctions';
import { ActionQueue } from './ActionQueue';
import { Repeat } from './Action/Repeat';
Expand All @@ -19,7 +17,7 @@ import { Delay } from './Action/Delay';
import { Die } from './Action/Die';
import { Follow } from './Action/Follow';
import { Meet } from './Action/Meet';
import { Vector } from '../Math/vector';
import { Vector, RotationType } from '../Math';
import { Entity } from '../EntityComponentSystem/Entity';
import { Action } from './Action';
import { Color } from '../Color';
Expand Down
3 changes: 1 addition & 2 deletions src/engine/Actions/ActionsComponent.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,9 @@ import { Entity } from '../EntityComponentSystem/Entity';
import { Actor } from '../Actor';
import { MotionComponent } from '../EntityComponentSystem/Components/MotionComponent';
import { TransformComponent } from '../EntityComponentSystem/Components/TransformComponent';
import { Vector } from '../Math/vector';
import { Vector, RotationType } from '../Math';
import { EasingFunction } from '../Util/EasingFunctions';
import { ActionQueue } from './ActionQueue';
import { RotationType } from './RotationType';
import { Action } from './Action';
import { Color } from '../Color';
import { CurveToOptions } from './Action/CurveTo';
Expand Down
1 change: 0 additions & 1 deletion src/engine/Actions/index.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
export * from './ActionContext';
export * from './ActionQueue';
export * from './Actionable';
export * from './RotationType';

export * from './Action';
export * from './Action/ActionSequence';
Expand Down
1 change: 1 addition & 0 deletions src/engine/Math/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,3 +12,4 @@ export * from './ray';
export * from './lerp';
export * from './bezier-curve';
export * from './util';
export * from './rotation-type';
2 changes: 1 addition & 1 deletion src/engine/Math/lerp.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { RotationType } from '../Actions/RotationType';
import { RotationType } from './rotation-type';
import { TwoPI } from './util';
import { Vector } from './vector';

Expand Down
File renamed without changes.
6 changes: 3 additions & 3 deletions src/engine/Math/util.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,12 +43,12 @@ export function approximatelyEqual(val1: number, val2: number, tolerance: number
}

/**
* Convert an angle to be the equivalent in the range [0, 2PI]
* Convert an angle to be the equivalent in the range [0, 2PI)
*/
export function canonicalizeAngle(angle: number): number {
let tmpAngle = angle;
if (angle > TwoPI) {
while (tmpAngle > TwoPI) {
if (angle >= TwoPI) {
while (tmpAngle >= TwoPI) {
tmpAngle -= TwoPI;
}
}
Expand Down
44 changes: 42 additions & 2 deletions src/engine/Math/vector.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { Clonable } from '../Interfaces/Clonable';
import { canonicalizeAngle, clamp } from './util';
import { RotationType } from './rotation-type';
import { canonicalizeAngle, clamp, TwoPI } from './util';

/**
* A 2D vector on a plane.
Expand Down Expand Up @@ -372,14 +373,53 @@ export class Vector implements Clonable<Vector> {
}

/**
* Returns the angle of this vector.
* Returns the angle of this vector, in range [0, 2*PI)
*/
public toAngle(): number {
return canonicalizeAngle(Math.atan2(this.y, this.x));
}

/**
* Returns the difference in radians between the angle of this vector and given angle,
* using the given rotation type.
* @param angle in radians to which the vector has to be rotated, using {@apilink rotate}
* @param rotationType what {@apilink RotationType} to use for the rotation
* @returns the angle by which the vector needs to be rotated to match the given angle
*/
public angleBetween(angle: number, rotationType: RotationType): number {
const startAngleRadians = this.toAngle();
const endAngleRadians = canonicalizeAngle(angle);
let rotationClockwise = 0;
let rotationAntiClockwise = 0;
if (endAngleRadians > startAngleRadians) {
rotationClockwise = endAngleRadians - startAngleRadians;
} else {
rotationClockwise = (TwoPI - startAngleRadians + endAngleRadians) % TwoPI;
}
rotationAntiClockwise = (rotationClockwise - TwoPI) % TwoPI;
switch (rotationType) {
case RotationType.ShortestPath:
if (Math.abs(rotationClockwise) < Math.abs(rotationAntiClockwise)) {
return rotationClockwise;
} else {
return rotationAntiClockwise;
}
case RotationType.LongestPath:
if (Math.abs(rotationClockwise) > Math.abs(rotationAntiClockwise)) {
return rotationClockwise;
} else {
return rotationAntiClockwise;
}
case RotationType.Clockwise:
return rotationClockwise;
case RotationType.CounterClockwise:
return rotationAntiClockwise;
}
}

/**
* Rotates the current vector around a point by a certain angle in radians.
* Positive angle means rotation clockwise.
*/
public rotate(angle: number, anchor?: Vector, dest?: Vector): Vector {
const result = dest || new Vector(0, 0);
Expand Down
59 changes: 59 additions & 0 deletions src/spec/AlgebraSpec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -125,6 +125,59 @@ describe('Vectors', () => {
expect(ex.Vector.distance(v1, v2)).toBe(20);
});

describe('angleBetween', () => {
type TestCaseParameters = {
description: string;
vector: ex.Vector;
angle: number;
rotationType: ex.RotationType;
expected: number;
};
const tc = (
description: string,
vector: ex.Vector,
angle: number,
rotationType: ex.RotationType,
expected: number
): TestCaseParameters => ({
vector,
description,
angle,
rotationType,
expected
});
const description = (tc: TestCaseParameters) =>
`${tc.description}: ${tc.vector.toAngle()} -> ${ex.canonicalizeAngle(tc.angle)} ${tc.rotationType} expected: ${tc.expected}`;
const downRight = ex.vec(1, 1);
const downLeft = ex.vec(-1, 1);
const upLeft = ex.vec(-1, -1);
const upRight = ex.vec(1, -1);
const downRightAngle = downRight.toAngle();
const downLeftAngle = downLeft.toAngle();
const upLeftAngle = upLeft.toAngle();
const upRightAngle = upRight.toAngle();

const testCases: TestCaseParameters[] = [
tc('returns 0 when the new angle is the same as vectors angle', ex.Vector.Right, 0, ex.RotationType.Clockwise, 0),
tc('knows that 2*PI is same as 0', ex.Vector.Right, ex.TwoPI, ex.RotationType.Clockwise, 0),
tc('rotates from I to IV quadrant', upRight, downRightAngle, ex.RotationType.Clockwise, Math.PI / 2),
tc('rotates from I to IV quadrant the shortest way', upRight, downRightAngle, ex.RotationType.ShortestPath, Math.PI / 2),
tc('rotates from I to IV quadrant counterclockwise', upRight, downRightAngle, ex.RotationType.CounterClockwise, -(3 * Math.PI) / 2),
tc('rotates from I to IV quadrant the longest way', upRight, downRightAngle, ex.RotationType.LongestPath, -(3 * Math.PI) / 2),
tc('rotates from I to II quadrant', upRight, upLeftAngle, ex.RotationType.Clockwise, (3 * Math.PI) / 2),
tc('rotates from I to II quadrant the shortest way', upRight, upLeftAngle, ex.RotationType.ShortestPath, -Math.PI / 2),
tc('rotates from I to II quadrant counterclockwise', upRight, upLeftAngle, ex.RotationType.CounterClockwise, -Math.PI / 2),
tc('rotates from I to II quadrant the longest way', upRight, upLeftAngle, ex.RotationType.LongestPath, (3 * Math.PI) / 2)
];

testCases.forEach((testCase) => {
it(description(testCase), () => {
const result = testCase.vector.angleBetween(testCase.angle, testCase.rotationType);
expect(result).toBeCloseTo(testCase.expected);
});
});
});

it('can be normalized to a length of 1', () => {
const v = new ex.Vector(10, 0);

Expand Down Expand Up @@ -208,6 +261,12 @@ describe('Vectors', () => {
expect(rotated.equals(new ex.Vector(-1, 0))).toBeTruthy();
});

it('can be rotated by positive angle clockwise', () => {
const v = ex.Vector.Up;
const rotated = v.rotate(Math.PI / 2);
expect(rotated.equals(ex.Vector.Right)).toBeTruthy();
});

it('can be rotated by an angle about a point', () => {
const v = new ex.Vector(1, 0);
const rotate = v.rotate(Math.PI, new ex.Vector(2, 0));
Expand Down

0 comments on commit 5ecb464

Please sign in to comment.