Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Make RotateTo+RotationType functionality more available #3291

Merged
merged 19 commits into from
Dec 9, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -230,6 +230,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 @@ -265,6 +272,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
Loading