Add tests for Vec2, Rect, and AffineTransform (#52)
* Tests for clamp, Vec2 * Switch to using jsverify for testing math functions * Add tests for AffineTransform * Add tests for Rect * Add tests for rect transformations
This commit is contained in:
+368
@@ -0,0 +1,368 @@
|
||||
import {clamp, Vec2, AffineTransform, Rect} from './math'
|
||||
import * as jsc from 'jsverify'
|
||||
|
||||
test('clamp', () => {
|
||||
jsc.assertForall(jsc.number, jsc.number, jsc.number, (a, b, c) => {
|
||||
const result = clamp(a, b, c)
|
||||
if (a < b) return result == b
|
||||
if (a > c) return result == c
|
||||
return result == a
|
||||
})
|
||||
})
|
||||
|
||||
// Change this to jsc.integer to debug failures more easily
|
||||
let numericType = jsc.number
|
||||
|
||||
const arbitraryVec2 = jsc
|
||||
.record({x: jsc.integer, y: numericType})
|
||||
.smap(v => new Vec2(v.x, v.y), v => v)
|
||||
|
||||
const positiveVec2 = jsc.suchthat(arbitraryVec2, v => v.x > 0 && v.y > 0)
|
||||
|
||||
const arbitraryTransform = jsc
|
||||
.record({
|
||||
m00: numericType,
|
||||
m01: numericType,
|
||||
m02: numericType,
|
||||
m10: numericType,
|
||||
m11: numericType,
|
||||
m12: numericType,
|
||||
})
|
||||
.smap(t => new AffineTransform(t.m00, t.m01, t.m02, t.m10, t.m11, t.m12), t => t)
|
||||
|
||||
const invertibleTransform = jsc.suchthat(arbitraryTransform, t => t.det() != 0)
|
||||
|
||||
const simpleTransform = jsc.suchthat(
|
||||
jsc
|
||||
.record({scale: arbitraryVec2, translation: arbitraryVec2})
|
||||
.smap(
|
||||
t => AffineTransform.withScale(t.scale).withTranslation(t.translation),
|
||||
t => ({scale: t.getScale(), translation: t.getTranslation()}),
|
||||
),
|
||||
t => t.det() != 0,
|
||||
)
|
||||
|
||||
const arbitraryRect = jsc
|
||||
.record({origin: arbitraryVec2, size: positiveVec2})
|
||||
.smap(r => new Rect(r.origin, r.size), r => r)
|
||||
|
||||
describe('Vec2', () => {
|
||||
test('constructor', () => {
|
||||
jsc.assertForall(jsc.number, jsc.number, (a, b) => {
|
||||
const v = new Vec2(a, b)
|
||||
return v.x == a && v.y == b
|
||||
})
|
||||
})
|
||||
|
||||
test('withX', () => {
|
||||
jsc.assertForall(arbitraryVec2, jsc.number, (v, n) => {
|
||||
return v.withX(n).x === n
|
||||
})
|
||||
})
|
||||
|
||||
test('withY', () => {
|
||||
jsc.assertForall(arbitraryVec2, jsc.number, (v, n) => {
|
||||
return v.withY(n).y === n
|
||||
})
|
||||
})
|
||||
|
||||
test('plus', () => {
|
||||
jsc.assertForall(arbitraryVec2, arbitraryVec2, (v1, v2) => {
|
||||
const sum = v1.plus(v2)
|
||||
return sum.x === v1.x + v2.x && sum.y === v1.y + v2.y
|
||||
})
|
||||
})
|
||||
|
||||
test('minus', () => {
|
||||
jsc.assertForall(arbitraryVec2, arbitraryVec2, (v1, v2) => {
|
||||
const diff = v1.minus(v2)
|
||||
return diff.x === v1.x - v2.x && diff.y === v1.y - v2.y
|
||||
})
|
||||
})
|
||||
|
||||
test('times', () => {
|
||||
jsc.assertForall(arbitraryVec2, jsc.number, (v1, s) => {
|
||||
const prod = v1.times(s)
|
||||
return prod.x === v1.x * s && prod.y === v1.y * s
|
||||
})
|
||||
})
|
||||
|
||||
test('timesPointwise', () => {
|
||||
jsc.assertForall(arbitraryVec2, arbitraryVec2, (v1, v2) => {
|
||||
const prod = v1.timesPointwise(v2)
|
||||
return prod.x === v1.x * v2.x && prod.y === v1.y * v2.y
|
||||
})
|
||||
})
|
||||
|
||||
test('dividedByPointwise', () => {
|
||||
jsc.assertForall(
|
||||
arbitraryVec2,
|
||||
jsc.suchthat(arbitraryVec2, v => v.x !== 0 && v.y !== 0),
|
||||
(v1, v2) => {
|
||||
const div = v1.dividedByPointwise(v2)
|
||||
return div.x === v1.x / v2.x && div.y === v1.y / v2.y
|
||||
},
|
||||
)
|
||||
})
|
||||
|
||||
test('dot', () => {
|
||||
jsc.assertForall(arbitraryVec2, arbitraryVec2, (v1, v2) => {
|
||||
return v1.dot(v2) === v1.x * v2.x + v1.y * v2.y
|
||||
})
|
||||
})
|
||||
|
||||
test('equals', () => {
|
||||
jsc.assertForall(jsc.number, jsc.number, (a, b) => {
|
||||
return new Vec2(a, b).equals(new Vec2(a, b))
|
||||
})
|
||||
|
||||
jsc.assertForall(arbitraryVec2, arbitraryVec2, (a, b) => {
|
||||
return a.equals(b) === (a.x === b.x && a.y === b.y)
|
||||
})
|
||||
})
|
||||
|
||||
test('length2', () => {
|
||||
jsc.assertForall(arbitraryVec2, v => {
|
||||
return v.length2() === v.x * v.x + v.y * v.y
|
||||
})
|
||||
})
|
||||
|
||||
test('length', () => {
|
||||
jsc.assertForall(arbitraryVec2, v => {
|
||||
return v.length() === Math.sqrt(v.x * v.x + v.y * v.y)
|
||||
})
|
||||
})
|
||||
|
||||
test('abs', () => {
|
||||
jsc.assertForall(arbitraryVec2, v => {
|
||||
const q = v.abs()
|
||||
return q.x === Math.abs(v.x) && q.y === Math.abs(v.y)
|
||||
})
|
||||
})
|
||||
|
||||
test('min', () => {
|
||||
jsc.assertForall(arbitraryVec2, arbitraryVec2, (v1, v2) => {
|
||||
const min = Vec2.min(v1, v2)
|
||||
return min.x === Math.min(v1.x, v2.x) && min.y === Math.min(v1.y, v2.y)
|
||||
})
|
||||
})
|
||||
|
||||
test('max', () => {
|
||||
jsc.assertForall(arbitraryVec2, arbitraryVec2, (v1, v2) => {
|
||||
const max = Vec2.max(v1, v2)
|
||||
return max.x === Math.max(v1.x, v2.x) && max.y === Math.max(v1.y, v2.y)
|
||||
})
|
||||
})
|
||||
|
||||
test('flatten', () => {
|
||||
jsc.assertForall(arbitraryVec2, v1 => {
|
||||
const flat = v1.flatten()
|
||||
return flat[0] == v1.x && flat[1] == v1.y
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('Rect', () => {
|
||||
test('isEmpty', () => {
|
||||
jsc.assertForall(arbitraryVec2, jsc.number, (v, n) => {
|
||||
return new Rect(v, new Vec2(0, n)).isEmpty()
|
||||
})
|
||||
jsc.assertForall(arbitraryVec2, jsc.number, (v, n) => {
|
||||
return new Rect(v, new Vec2(n, 0)).isEmpty()
|
||||
})
|
||||
jsc.assertForall(arbitraryVec2, v => {
|
||||
return !new Rect(v, Vec2.unit).isEmpty()
|
||||
})
|
||||
})
|
||||
|
||||
test('width', () => {
|
||||
jsc.assertForall(arbitraryVec2, arbitraryVec2, (v1, v2) => {
|
||||
return new Rect(v1, v2).width() == v2.x
|
||||
})
|
||||
})
|
||||
test('height', () => {
|
||||
jsc.assertForall(arbitraryVec2, arbitraryVec2, (v1, v2) => {
|
||||
return new Rect(v1, v2).height() == v2.y
|
||||
})
|
||||
})
|
||||
test('left', () => {
|
||||
jsc.assertForall(arbitraryVec2, arbitraryVec2, (v1, v2) => {
|
||||
return new Rect(v1, v2).left() == v1.x
|
||||
})
|
||||
})
|
||||
test('top', () => {
|
||||
jsc.assertForall(arbitraryVec2, arbitraryVec2, (v1, v2) => {
|
||||
return new Rect(v1, v2).top() == v1.y
|
||||
})
|
||||
})
|
||||
test('right', () => {
|
||||
jsc.assertForall(arbitraryVec2, arbitraryVec2, (v1, v2) => {
|
||||
return new Rect(v1, v2).right() == v1.x + v2.x
|
||||
})
|
||||
})
|
||||
test('bottom', () => {
|
||||
jsc.assertForall(arbitraryVec2, arbitraryVec2, (v1, v2) => {
|
||||
return new Rect(v1, v2).bottom() == v1.y + v2.y
|
||||
})
|
||||
})
|
||||
test('topLeft', () => {
|
||||
jsc.assertForall(arbitraryVec2, arbitraryVec2, (v1, v2) => {
|
||||
return new Rect(v1, v2).topLeft().equals(v1)
|
||||
})
|
||||
})
|
||||
test('topRight', () => {
|
||||
jsc.assertForall(arbitraryVec2, arbitraryVec2, (v1, v2) => {
|
||||
return new Rect(v1, v2).topRight().equals(v1.plus(v2.withY(0)))
|
||||
})
|
||||
})
|
||||
test('bottomLeft', () => {
|
||||
jsc.assertForall(arbitraryVec2, arbitraryVec2, (v1, v2) => {
|
||||
return new Rect(v1, v2).bottomLeft().equals(v1.plus(v2.withX(0)))
|
||||
})
|
||||
})
|
||||
test('bottomRight', () => {
|
||||
jsc.assertForall(arbitraryVec2, arbitraryVec2, (v1, v2) => {
|
||||
return new Rect(v1, v2).bottomRight().equals(v1.plus(v2))
|
||||
})
|
||||
})
|
||||
|
||||
test('withOrigin', () => {
|
||||
jsc.assertForall(arbitraryRect, arbitraryVec2, (r, v) => {
|
||||
return r.withOrigin(v).origin.equals(v)
|
||||
})
|
||||
})
|
||||
test('withSize', () => {
|
||||
jsc.assertForall(arbitraryRect, arbitraryVec2, (r, v) => {
|
||||
return r.withSize(v).size.equals(v)
|
||||
})
|
||||
})
|
||||
|
||||
test('closestPointTo', () => {
|
||||
jsc.assertForall(arbitraryRect, arbitraryVec2, (r, v) => {
|
||||
const p = r.closestPointTo(v)
|
||||
return p.x >= r.left() && p.x <= r.right() && p.y >= r.top() && p.y <= r.bottom()
|
||||
})
|
||||
})
|
||||
|
||||
test('hasIntersectionWith', () => {
|
||||
jsc.assertForall(arbitraryRect, arbitraryRect, (r1, r2) => {
|
||||
return r1.hasIntersectionWith(r2) === !r1.intersectWith(r2).isEmpty()
|
||||
})
|
||||
})
|
||||
|
||||
test('intersectWith', () => {
|
||||
jsc.assertForall(arbitraryRect, arbitraryRect, (r1, r2) => {
|
||||
const inter = r1.intersectWith(r2)
|
||||
return inter.isEmpty() || (r1.hasIntersectionWith(inter) && r2.hasIntersectionWith(inter))
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('AffineTransform', () => {
|
||||
test('inverted', () => {
|
||||
expect(new AffineTransform(0, 0, 0, 0, 0, 0).inverted()).toBe(null)
|
||||
|
||||
jsc.assertForall(invertibleTransform, t => {
|
||||
return t
|
||||
.inverted()!
|
||||
.inverted()!
|
||||
.approxEquals(t)
|
||||
})
|
||||
})
|
||||
|
||||
test('translation', () => {
|
||||
jsc.assertForall(arbitraryTransform, arbitraryVec2, (t, v1) => {
|
||||
return t
|
||||
.withTranslation(v1)
|
||||
.getTranslation()
|
||||
.equals(v1)
|
||||
})
|
||||
|
||||
jsc.assertForall(arbitraryTransform, arbitraryVec2, (t, v1) => {
|
||||
const initialTranslation = t.getTranslation()
|
||||
return t
|
||||
.translatedBy(v1)
|
||||
.getTranslation()
|
||||
.approxEquals(initialTranslation.plus(v1))
|
||||
})
|
||||
})
|
||||
|
||||
test('scale', () => {
|
||||
jsc.assertForall(arbitraryTransform, arbitraryVec2, (t, v1) => {
|
||||
return t
|
||||
.withScale(v1)
|
||||
.getScale()
|
||||
.equals(v1)
|
||||
})
|
||||
})
|
||||
|
||||
test('transformVector', () => {
|
||||
// Vector transformation are translation-invariant
|
||||
jsc.assertForall(arbitraryVec2, arbitraryVec2, (v1, v2) => {
|
||||
return AffineTransform.withTranslation(v1)
|
||||
.transformVector(v2)
|
||||
.approxEquals(v2)
|
||||
})
|
||||
|
||||
jsc.assertForall(arbitraryVec2, arbitraryVec2, (v1, v2) => {
|
||||
return AffineTransform.withScale(v1)
|
||||
.transformVector(v2)
|
||||
.approxEquals(v2.timesPointwise(v1))
|
||||
})
|
||||
})
|
||||
|
||||
test('inverseTransformVector', () => {
|
||||
jsc.assertForall(invertibleTransform, arbitraryVec2, (t, v) => {
|
||||
return t.inverseTransformVector(t.transformVector(v))!.approxEquals(v)
|
||||
})
|
||||
})
|
||||
|
||||
test('transformPosition', () => {
|
||||
jsc.assertForall(arbitraryVec2, arbitraryVec2, (v1, v2) => {
|
||||
return AffineTransform.withTranslation(v1)
|
||||
.transformPosition(v2)
|
||||
.approxEquals(v2.plus(v1))
|
||||
})
|
||||
|
||||
jsc.assertForall(arbitraryVec2, arbitraryVec2, (v1, v2) => {
|
||||
return AffineTransform.withScale(v1)
|
||||
.transformPosition(v2)
|
||||
.approxEquals(v2.timesPointwise(v1))
|
||||
})
|
||||
})
|
||||
|
||||
test('inverseTransformPosition', () => {
|
||||
jsc.assertForall(invertibleTransform, arbitraryVec2, (t, v) => {
|
||||
return t.inverseTransformPosition(t.transformPosition(v))!.approxEquals(v)
|
||||
})
|
||||
})
|
||||
|
||||
test('transformRect', () => {
|
||||
jsc.assertForall(arbitraryVec2, arbitraryRect, (v, r) => {
|
||||
return AffineTransform.withTranslation(v)
|
||||
.transformRect(r)
|
||||
.equals(r.withOrigin(r.origin.plus(v)))
|
||||
})
|
||||
|
||||
jsc.assertForall(arbitraryVec2, arbitraryRect, (v, r) => {
|
||||
const t = AffineTransform.withScale(v)
|
||||
const rt = t.transformRect(r)
|
||||
return Math.abs(rt.area() - r.area() * Math.abs(t.det())) < 1e-6
|
||||
})
|
||||
})
|
||||
|
||||
test('inverseTransformRect', () => {
|
||||
jsc.assertForall(simpleTransform, arbitraryRect, (t, r) => {
|
||||
return t.inverseTransformRect(t.transformRect(r))!.approxEquals(r)
|
||||
})
|
||||
})
|
||||
|
||||
test('times', () => {
|
||||
jsc.assertForall(invertibleTransform, invertibleTransform, (t1, t2) => {
|
||||
return t1
|
||||
.times(t2)
|
||||
.times(t2.inverted()!)
|
||||
.approxEquals(t1)
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -34,6 +34,10 @@ export class Vec2 {
|
||||
equals(other: Vec2) {
|
||||
return this.x === other.x && this.y === other.y
|
||||
}
|
||||
approxEquals(other: Vec2, epsilon = 1e-9) {
|
||||
return Math.abs(this.x - other.x) < epsilon && Math.abs(this.y - other.y) < epsilon
|
||||
}
|
||||
|
||||
length2() {
|
||||
return this.dot(this)
|
||||
}
|
||||
@@ -130,6 +134,17 @@ export class AffineTransform {
|
||||
)
|
||||
}
|
||||
|
||||
approxEquals(other: AffineTransform, epsilon = 1e-9) {
|
||||
return (
|
||||
Math.abs(this.m00 - other.m00) < epsilon &&
|
||||
Math.abs(this.m01 - other.m01) < epsilon &&
|
||||
Math.abs(this.m02 - other.m02) < epsilon &&
|
||||
Math.abs(this.m10 - other.m10) < epsilon &&
|
||||
Math.abs(this.m11 - other.m11) < epsilon &&
|
||||
Math.abs(this.m12 - other.m12) < epsilon
|
||||
)
|
||||
}
|
||||
|
||||
timesScalar(s: number) {
|
||||
const {m00, m01, m02, m10, m11, m12} = this
|
||||
return new AffineTransform(s * m00, s * m01, s * m02, s * m10, s * m11, s * m12)
|
||||
@@ -313,6 +328,14 @@ export class Rect {
|
||||
return this.origin.equals(other.origin) && this.size.equals(other.size)
|
||||
}
|
||||
|
||||
approxEquals(other: Rect) {
|
||||
return this.origin.approxEquals(other.origin) && this.size.approxEquals(other.size)
|
||||
}
|
||||
|
||||
area() {
|
||||
return this.size.x * this.size.y
|
||||
}
|
||||
|
||||
static empty = new Rect(Vec2.zero, Vec2.zero)
|
||||
static unit = new Rect(Vec2.zero, Vec2.unit)
|
||||
static NDC = new Rect(new Vec2(-1, -1), new Vec2(2, 2))
|
||||
|
||||
Generated
+36
@@ -6395,6 +6395,18 @@
|
||||
"verror": "1.10.0"
|
||||
}
|
||||
},
|
||||
"jsverify": {
|
||||
"version": "0.8.3",
|
||||
"resolved": "https://registry.npmjs.org/jsverify/-/jsverify-0.8.3.tgz",
|
||||
"integrity": "sha1-AukXjhkktar5WcAjmvhO1cbQc4w=",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"lazy-seq": "1.0.0",
|
||||
"rc4": "0.1.5",
|
||||
"trampa": "1.0.0",
|
||||
"typify-parser": "1.1.0"
|
||||
}
|
||||
},
|
||||
"kind-of": {
|
||||
"version": "3.2.2",
|
||||
"resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz",
|
||||
@@ -6411,6 +6423,12 @@
|
||||
"dev": true,
|
||||
"optional": true
|
||||
},
|
||||
"lazy-seq": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/lazy-seq/-/lazy-seq-1.0.0.tgz",
|
||||
"integrity": "sha1-iAy4qrJWAmOC4C9T7AiWgqdMW2o=",
|
||||
"dev": true
|
||||
},
|
||||
"lcid": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/lcid/-/lcid-1.0.0.tgz",
|
||||
@@ -9994,6 +10012,12 @@
|
||||
"integrity": "sha1-9JvmtIeJTdxA3MlKMi9hEJLgDV4=",
|
||||
"dev": true
|
||||
},
|
||||
"rc4": {
|
||||
"version": "0.1.5",
|
||||
"resolved": "https://registry.npmjs.org/rc4/-/rc4-0.1.5.tgz",
|
||||
"integrity": "sha1-CMbgSgFo9utiHCKrbLEVG9n0pk0=",
|
||||
"dev": true
|
||||
},
|
||||
"read-pkg": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-1.1.0.tgz",
|
||||
@@ -11942,6 +11966,12 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"trampa": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/trampa/-/trampa-1.0.0.tgz",
|
||||
"integrity": "sha1-Ukc0esM0gH+mwAAERMuRtjmECtU=",
|
||||
"dev": true
|
||||
},
|
||||
"trim-right": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/trim-right/-/trim-right-1.0.1.tgz",
|
||||
@@ -12319,6 +12349,12 @@
|
||||
"semver": "5.5.0"
|
||||
}
|
||||
},
|
||||
"typify-parser": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/typify-parser/-/typify-parser-1.1.0.tgz",
|
||||
"integrity": "sha1-rHO/pfJTQ0aOLQ8zRsYRe8A9PJk=",
|
||||
"dev": true
|
||||
},
|
||||
"uglify-es": {
|
||||
"version": "3.2.2",
|
||||
"resolved": "https://registry.npmjs.org/uglify-es/-/uglify-es-3.2.2.tgz",
|
||||
|
||||
@@ -24,6 +24,7 @@
|
||||
"eslint": "^4.19.1",
|
||||
"eslint-plugin-prettier": "^2.6.0",
|
||||
"jest": "^23.0.1",
|
||||
"jsverify": "^0.8.3",
|
||||
"pako": "1.0.6",
|
||||
"parcel-bundler": "1.7.0",
|
||||
"preact": "8.2.7",
|
||||
|
||||
Reference in New Issue
Block a user