-
Notifications
You must be signed in to change notification settings - Fork 3
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
1 parent
b4f720c
commit 4ed862c
Showing
5 changed files
with
182 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,5 @@ | ||
/** | ||
* Module s1 implements types and functions for working with geometry in S¹ (circular geometry). | ||
* @module s1 | ||
*/ | ||
export * as angle from './angle' |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,87 @@ | ||
import { DEGREE } from './angle_constants' | ||
|
||
/** | ||
* Angle represents a 1D angle. The internal representation is a double precision value in radians, so conversion to and from radians is exact. | ||
* Conversions between E5, E6, E7, and Degrees are not always exact. | ||
* | ||
* For example, Degrees(3.1) is different from E6(3100000) or E7(31000000). | ||
* | ||
* The following conversions between degrees and radians are exact: | ||
* | ||
* Degree*180 == Radian*Math.PI | ||
* Degree*(180/n) == Radian*(Math.PI/n) for n == 0..8 | ||
* | ||
* These identities hold when the arguments are scaled up or down by any power of 2. Some similar identities are also true, for example, | ||
* | ||
* Degree*60 == Radian*(Math.PI/3) | ||
* | ||
* But be aware that this type of identity does not hold in general. | ||
* For example: | ||
* | ||
* Degree*3 != Radian*(Math.PI/60) | ||
* | ||
* Similarly, the conversion to radians means that (Angle(x)*Degree).Degrees() does not always equal x. | ||
* For example: | ||
* | ||
* (Angle(45*n)*Degree).Degrees() == 45*n for n == 0..8 | ||
* | ||
* but | ||
* | ||
* (60*Degree).Degrees() != 60 | ||
* | ||
* When testing for equality, you should allow for numerical errors (ApproxEqual) | ||
* or convert to discrete E5/E6/E7 values first. | ||
* | ||
* @module angle | ||
*/ | ||
export type angle = number | ||
|
||
/** | ||
* Returns the angle in radians. | ||
*/ | ||
export const radians = (a: angle): number => a | ||
|
||
/** | ||
* Returns the angle in degrees. | ||
*/ | ||
export const degrees = (a: angle): number => a / DEGREE | ||
|
||
/** | ||
* Returns the value rounded to nearest as an int32. | ||
* This does not match C++ exactly for the case of x.5. | ||
*/ | ||
export const round = (a: angle): number => Math.round(a) || 0 | ||
|
||
/** Returns an angle larger than any finite angle. */ | ||
export const infAngle = (): angle => Infinity | ||
|
||
/** Reports whether this Angle is infinite. */ | ||
export const isInf = (a: angle): boolean => a == Infinity | ||
|
||
/** Returns the angle in hundred thousandths of degrees. */ | ||
export const e5 = (a: angle): number => round(degrees(a) * 1e5) | ||
|
||
/** Returns the angle in millionths of degrees. */ | ||
export const e6 = (a: angle): number => round(degrees(a) * 1e6) | ||
|
||
/** Returns the angle in ten millionths of degrees. */ | ||
export const e7 = (a: angle): number => round(degrees(a) * 1e7) | ||
|
||
/** Returns the absolute value of the angle. */ | ||
export const abs = (a: angle): angle => Math.abs(a) | ||
|
||
/** Returns an equivalent angle in (-π, π]. */ | ||
export const normalized = (a: angle): angle => { | ||
const rad = a % (2 * Math.PI) | ||
if (rad == -Math.PI) return Math.PI | ||
if (rad < -Math.PI) return -rad - Math.PI | ||
return rad | ||
} | ||
|
||
/** | ||
* Generates a human readable string. | ||
*/ | ||
export const toString = (a: angle): string => degrees(a).toFixed(7) | ||
|
||
/** Reports whether the two angles are the same up to a small tolerance. */ | ||
export const approxEqual = (a: angle, oa: angle, epsilon = 1e-15): boolean => Math.abs(a - oa) <= epsilon |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,8 @@ | ||
import type { angle } from './angle' | ||
|
||
// angle units. | ||
export const RADIAN: angle = 1 | ||
export const DEGREE: angle = (Math.PI / 180) * RADIAN | ||
export const E5: angle = 1e-5 * DEGREE | ||
export const E6: angle = 1e-6 * DEGREE | ||
export const E7: angle = 1e-7 * DEGREE |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,81 @@ | ||
import test from 'node:test' | ||
import { equal, ok } from 'node:assert/strict' | ||
import * as angle from './angle' | ||
import { DEGREE, RADIAN, E5, E6, E7 } from './angle_constants' | ||
|
||
test('empty', t => { | ||
equal(0, angle.radians(0)) | ||
}) | ||
|
||
test('PI radians exactly 180 degrees', t => { | ||
equal(angle.radians(Math.PI * RADIAN), Math.PI, '(π * Radian).Radians() was %v, want π') | ||
equal(angle.degrees(Math.PI * RADIAN), 180, '(π * Radian).Degrees() was %v, want 180') | ||
equal(angle.radians(180 * DEGREE), Math.PI, '(180 * Degree).Radians() was %v, want π') | ||
equal(angle.degrees(180 * DEGREE), 180, '(180 * Degree).Degrees() was %v, want 180') | ||
|
||
equal(angle.degrees((Math.PI / 2) * RADIAN), 90, '(π/2 * Radian).Degrees() was %v, want 90') | ||
|
||
// Check negative angles. | ||
equal(angle.degrees((-Math.PI / 2) * RADIAN), -90, '(-π/2 * Radian).Degrees() was %v, want -90') | ||
equal(angle.radians(-45 * DEGREE), -Math.PI / 4, '(-45 * Degree).Radians() was %v, want -π/4') | ||
}) | ||
|
||
test('E5/E6/E7 representation', t => { | ||
ok(Math.abs(angle.radians(-45 * DEGREE) - angle.radians(-4500000 * E5)) <= 1e-15) | ||
equal(angle.radians(-60 * DEGREE), angle.radians(-60000000 * E6)) | ||
equal(angle.radians(-75 * DEGREE), angle.radians(-750000000 * E7)) | ||
|
||
equal(-17256123, angle.e5(-172.56123 * DEGREE)) | ||
equal(12345678, angle.e6(12.345678 * DEGREE)) | ||
equal(-123456789, angle.e7(-12.3456789 * DEGREE)) | ||
|
||
equal(angle.e5(0.500000001 * 1e-5 * DEGREE), 1) | ||
equal(angle.e6(0.500000001 * 1e-6 * DEGREE), 1) | ||
equal(angle.e7(0.500000001 * 1e-7 * DEGREE), 1) | ||
|
||
equal(angle.e5(-0.500000001 * 1e-5 * DEGREE), -1) | ||
equal(angle.e6(-0.500000001 * 1e-6 * DEGREE), -1) | ||
equal(angle.e7(-0.500000001 * 1e-7 * DEGREE), -1) | ||
|
||
equal(angle.e5(0.499999999 * 1e-5 * DEGREE), 0) | ||
equal(angle.e6(0.499999999 * 1e-6 * DEGREE), 0) | ||
equal(angle.e7(0.499999999 * 1e-7 * DEGREE), 0) | ||
|
||
equal(angle.e5(-0.499999999 * 1e-5 * DEGREE), 0) | ||
equal(angle.e6(-0.499999999 * 1e-6 * DEGREE), 0) | ||
equal(angle.e7(-0.499999999 * 1e-7 * DEGREE), 0) | ||
}) | ||
|
||
test('normalize correctly canonicalizes angles', t => { | ||
equal(angle.normalized(360 * DEGREE), 0 * DEGREE) | ||
equal(angle.normalized(-90 * DEGREE), -90 * DEGREE) | ||
equal(angle.normalized(-180 * DEGREE), 180 * DEGREE) | ||
equal(angle.normalized(180 * DEGREE), 180 * DEGREE) | ||
equal(angle.normalized(540 * DEGREE), 180 * DEGREE) | ||
equal(angle.normalized(-270 * DEGREE), 90 * DEGREE) | ||
}) | ||
|
||
test('toString', t => { | ||
equal(angle.toString(180 * DEGREE), '180.0000000') | ||
}) | ||
|
||
test('degrees vs. radians', t => { | ||
// This test tests the exactness of specific values between degrees and radians. | ||
for (let k = -8; k <= 8; k++) { | ||
equal(45 * k * DEGREE, ((k * Math.PI) / 4) * RADIAN) | ||
equal(angle.degrees(45 * k * DEGREE), 45 * k) | ||
} | ||
|
||
for (let k = 0; k < 30; k++) { | ||
const m = 1 << k | ||
equal((180 / m) * DEGREE, Math.PI / (1 * m)) | ||
equal((60 / m) * DEGREE, Math.PI / (3 * m)) | ||
equal((36 / m) * DEGREE, Math.PI / (5 * m)) | ||
equal((20 / m) * DEGREE, Math.PI / (9 * m)) | ||
equal((4 / m) * DEGREE, Math.PI / (45 * m)) | ||
} | ||
|
||
// We also spot check a non-identity. | ||
// @missinglink: this fails for epsilon=1e-15 | ||
ok(angle.approxEqual(angle.degrees(60 * DEGREE), 60, 1e-14)) | ||
}) |