-
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
4a2e135
commit c8775bb
Showing
4 changed files
with
303 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
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,2 +1,3 @@ | ||
export * as r1 from './r1/_index' | ||
export * as r2 from './r2/_index' | ||
export * as s2 from './s2/_index' |
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,172 @@ | ||
/** | ||
* Interval represents a closed interval on ℝ. | ||
* Zero-length intervals (where lo == hi) represent single points. | ||
* If lo > hi then the interval is empty. | ||
*/ | ||
export class Interval { | ||
lo: number = 0.0 | ||
hi: number = 0.0 | ||
|
||
/** | ||
* Returns a new Interval. | ||
* @category Constructors | ||
*/ | ||
constructor(lo: number, hi: number) { | ||
this.lo = lo | ||
this.hi = hi | ||
} | ||
|
||
/** | ||
* Reports whether the interval is empty. | ||
*/ | ||
isEmpty(): boolean { | ||
return this.lo > this.hi | ||
} | ||
|
||
/** | ||
* Returns true iff the interval contains the same points as oi. | ||
*/ | ||
equal(oi: Interval): boolean { | ||
return (this.lo == oi.lo && this.hi == oi.hi) || (this.isEmpty() && oi.isEmpty()) | ||
} | ||
|
||
/** | ||
* Returns the midpoint of the interval. | ||
* Behaviour is undefined for empty intervals. | ||
*/ | ||
center(): number { | ||
return 0.5 * (this.lo + this.hi) | ||
} | ||
|
||
/** | ||
* Returns the length of the interval. | ||
* The length of an empty interval is negative. | ||
*/ | ||
length(): number { | ||
return this.hi - this.lo | ||
} | ||
|
||
/** | ||
* Returns true iff the interval contains p. | ||
*/ | ||
contains(p: number): boolean { | ||
return this.lo <= p && p <= this.hi | ||
} | ||
|
||
/** | ||
* Returns true iff the interval contains oi. | ||
*/ | ||
containsInterval(oi: Interval): boolean { | ||
if (oi.isEmpty()) return true | ||
return this.lo <= oi.lo && oi.hi <= this.hi | ||
} | ||
|
||
/** | ||
* Returns true iff the interval strictly contains p. | ||
*/ | ||
interiorContains(p: number): boolean { | ||
return this.lo < p && p < this.hi | ||
} | ||
|
||
/** | ||
* Returns true iff the interval strictly contains oi. | ||
*/ | ||
interiorContainsInterval(oi: Interval): boolean { | ||
if (oi.isEmpty()) return true | ||
return this.lo < oi.lo && oi.hi < this.hi | ||
} | ||
|
||
/** | ||
* Returns true iff the interval contains any points in common with oi. | ||
*/ | ||
intersects(oi: Interval): boolean { | ||
if (this.lo <= oi.lo) return oi.lo <= this.hi && oi.lo <= oi.hi // oi.lo ∈ i and oi is not empty | ||
return this.lo <= oi.hi && this.lo <= this.hi // i.lo ∈ oi and i is not empty | ||
} | ||
|
||
/** | ||
* Returns true iff the interior of the interval contains any points in common with oi, including the latter's boundary. | ||
*/ | ||
interiorIntersects(oi: Interval): boolean { | ||
return oi.lo < this.hi && this.lo < oi.hi && this.lo < this.hi && oi.lo <= oi.hi | ||
} | ||
|
||
/** | ||
* Returns the interval containing all points common to i and j. | ||
* @note Empty intervals do not need to be special-cased. | ||
*/ | ||
intersection(j: Interval): Interval { | ||
return new Interval(Math.max(this.lo, j.lo), Math.min(this.hi, j.hi)) | ||
} | ||
|
||
/** | ||
* Returns the smallest interval that contains this interval and the given interval. | ||
*/ | ||
union(other: Interval): Interval { | ||
if (this.isEmpty()) return other | ||
if (other.isEmpty()) return this | ||
return new Interval(Math.min(this.lo, other.lo), Math.max(this.hi, other.hi)) | ||
} | ||
|
||
/** | ||
* Returns the interval expanded so that it contains the given point. | ||
*/ | ||
addPoint(p: number): Interval { | ||
if (this.isEmpty()) return new Interval(p, p) | ||
if (p < this.lo) return new Interval(p, this.hi) | ||
if (p > this.hi) return new Interval(this.lo, p) | ||
return this | ||
} | ||
|
||
/** | ||
* Returns the closest point in the interval to the given point p. | ||
* The interval must be non-empty. | ||
*/ | ||
clampPoint(p: number): number { | ||
return Math.max(this.lo, Math.min(this.hi, p)) | ||
} | ||
|
||
/** | ||
* Returns an interval that has been expanded on each side by margin. | ||
* If margin is negative, then the function shrinks the interval on each side by margin instead. | ||
* The resulting interval may be empty. | ||
* Any expansion of an empty interval remains empty. | ||
*/ | ||
expanded(margin: number): Interval { | ||
if (this.isEmpty()) return this | ||
return new Interval(this.lo - margin, this.hi + margin) | ||
} | ||
|
||
/** | ||
* Truncates {lo, hi} floats to n digits of precision. | ||
*/ | ||
trunc(n: number = 15): Interval { | ||
const p = Number(`1e${n}`) | ||
const trunc = (dim: number) => Math.round(dim * p) / p | ||
return new Interval(trunc(this.lo), trunc(this.hi)) | ||
} | ||
|
||
/** | ||
* Generates a human readable string. | ||
*/ | ||
toString(): string { | ||
const t = this.trunc(7) | ||
return `[${t.lo.toFixed(7)}, ${t.hi.toFixed(7)}]` | ||
} | ||
|
||
/** | ||
* Returns an empty interval. | ||
* @category Constructors | ||
*/ | ||
static empty(): Interval { | ||
return new Interval(1, 0) | ||
} | ||
|
||
/** | ||
* Returns an interval representing a single point. | ||
* @category Constructors | ||
*/ | ||
static fromPoint(p: number): Interval { | ||
return new Interval(p, p) | ||
} | ||
} |
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,125 @@ | ||
import test from 'node:test' | ||
import { ok, equal } from 'node:assert/strict' | ||
import { Interval } from './Interval' | ||
|
||
// Some standard intervals for use throughout the tests. | ||
const UNIT = new Interval(0, 1) | ||
const NEG_UNIT = new Interval(-1, 0) | ||
const HALF = new Interval(0.5, 0.5) | ||
const EMPTY = Interval.empty() | ||
|
||
test('isEmpty', t => { | ||
ok(!UNIT.isEmpty(), 'should not be empty') | ||
ok(!NEG_UNIT.isEmpty(), 'should not be empty') | ||
ok(!HALF.isEmpty(), 'should not be empty') | ||
ok(EMPTY.isEmpty(), 'should not empty') | ||
}) | ||
|
||
test('center', t => { | ||
equal(UNIT.center(), 0.5) | ||
equal(NEG_UNIT.center(), -0.5) | ||
equal(HALF.center(), 0.5) | ||
}) | ||
|
||
test('length', t => { | ||
equal(UNIT.length(), 1) | ||
equal(NEG_UNIT.length(), 1) | ||
equal(HALF.length(), 0) | ||
}) | ||
|
||
test('contains', t => { | ||
ok(UNIT.contains(0.5)) | ||
ok(UNIT.interiorContains(0.5)) | ||
|
||
ok(UNIT.contains(0)) | ||
ok(!UNIT.interiorContains(0)) | ||
|
||
ok(UNIT.contains(1)) | ||
ok(!UNIT.interiorContains(1)) | ||
}) | ||
|
||
test('operations', t => { | ||
ok(EMPTY.containsInterval(EMPTY)) | ||
ok(EMPTY.interiorContainsInterval(EMPTY)) | ||
ok(!EMPTY.intersects(EMPTY)) | ||
ok(!EMPTY.interiorIntersects(EMPTY)) | ||
|
||
ok(!EMPTY.containsInterval(UNIT)) | ||
ok(!EMPTY.interiorContainsInterval(UNIT)) | ||
ok(!EMPTY.intersects(UNIT)) | ||
ok(!EMPTY.interiorIntersects(UNIT)) | ||
|
||
ok(UNIT.containsInterval(HALF)) | ||
ok(UNIT.interiorContainsInterval(HALF)) | ||
ok(UNIT.intersects(HALF)) | ||
ok(UNIT.interiorIntersects(HALF)) | ||
|
||
ok(UNIT.containsInterval(UNIT)) | ||
ok(!UNIT.interiorContainsInterval(UNIT)) | ||
ok(UNIT.intersects(UNIT)) | ||
ok(UNIT.interiorIntersects(UNIT)) | ||
|
||
ok(UNIT.containsInterval(EMPTY)) | ||
ok(UNIT.interiorContainsInterval(EMPTY)) | ||
ok(!UNIT.intersects(EMPTY)) | ||
ok(!UNIT.interiorIntersects(EMPTY)) | ||
|
||
ok(!UNIT.containsInterval(NEG_UNIT)) | ||
ok(!UNIT.interiorContainsInterval(NEG_UNIT)) | ||
ok(UNIT.intersects(NEG_UNIT)) | ||
ok(!UNIT.interiorIntersects(NEG_UNIT)) | ||
|
||
const i = new Interval(0, 0.5) | ||
ok(UNIT.containsInterval(i)) | ||
ok(!UNIT.interiorContainsInterval(i)) | ||
ok(UNIT.intersects(i)) | ||
ok(UNIT.interiorIntersects(i)) | ||
|
||
ok(!HALF.containsInterval(i)) | ||
ok(!HALF.interiorContainsInterval(i)) | ||
ok(HALF.intersects(i)) | ||
ok(!HALF.interiorIntersects(i)) | ||
}) | ||
|
||
test('intersection', t => { | ||
ok(UNIT.intersection(HALF).equal(HALF)) | ||
ok(UNIT.intersection(NEG_UNIT).equal(new Interval(0, 0))) | ||
ok(NEG_UNIT.intersection(HALF).equal(EMPTY)) | ||
ok(UNIT.intersection(EMPTY).equal(EMPTY)) | ||
ok(EMPTY.intersection(UNIT).equal(EMPTY)) | ||
}) | ||
|
||
test('union', t => { | ||
ok(new Interval(99, 100).union(EMPTY).equal(new Interval(99, 100))) | ||
ok(EMPTY.union(new Interval(99, 100)).equal(new Interval(99, 100))) | ||
ok(new Interval(5, 3).union(new Interval(0, -2)).equal(EMPTY)) | ||
ok(new Interval(0, -2).union(new Interval(5, 3)).equal(EMPTY)) | ||
ok(UNIT.union(UNIT).equal(UNIT)) | ||
ok(UNIT.union(NEG_UNIT).equal(new Interval(-1, 1))) | ||
ok(NEG_UNIT.union(UNIT).equal(new Interval(-1, 1))) | ||
ok(HALF.union(UNIT).equal(UNIT)) | ||
}) | ||
|
||
test('addPoint', t => { | ||
ok(EMPTY.addPoint(5).equal(new Interval(5, 5))) | ||
ok(new Interval(5, 5).addPoint(-1).equal(new Interval(-1, 5))) | ||
ok(new Interval(-1, 5).addPoint(0).equal(new Interval(-1, 5))) | ||
ok(new Interval(-1, 5).addPoint(6).equal(new Interval(-1, 6))) | ||
}) | ||
|
||
test('clampPoint', t => { | ||
equal(new Interval(0.1, 0.4).clampPoint(0.3), 0.3) | ||
equal(new Interval(0.1, 0.4).clampPoint(-7.0), 0.1) | ||
equal(new Interval(0.1, 0.4).clampPoint(0.6), 0.4) | ||
}) | ||
|
||
test('expanded', t => { | ||
ok(EMPTY.expanded(0.45).equal(EMPTY)) | ||
ok(UNIT.expanded(0.5).equal(new Interval(-0.5, 1.5))) | ||
ok(UNIT.expanded(-0.5).equal(new Interval(0.5, 0.5))) | ||
ok(UNIT.expanded(-0.51).equal(EMPTY)) | ||
}) | ||
|
||
test('toString', t => { | ||
equal(new Interval(2, 4.5).toString(), '[2.0000000, 4.5000000]') | ||
}) |
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 r1 implements types and functions for working with geometry in ℝ¹. | ||
* @module r1 | ||
*/ | ||
export { Interval } from './Interval' |