From 75b43909d340ee8f86b2551b21ab7b962ea5b4c2 Mon Sep 17 00:00:00 2001 From: fallenoak Date: Mon, 25 May 2020 13:40:37 -0500 Subject: [PATCH] feat(vector2): add initial Vector2 implementation (#57) * feat(vector2): add initial Vector2 implementation * feat(vector2): add accessors for x, y * chore: add test for setElements * chore: add additional tests --- src/lib/Vector2.mjs | 196 +++++++++++++++++++++++++++++++++++ src/lib/index.mjs | 2 + src/spec/Vector2.spec.js | 214 +++++++++++++++++++++++++++++++++++++++ 3 files changed, 412 insertions(+) create mode 100644 src/lib/Vector2.mjs create mode 100644 src/spec/Vector2.spec.js diff --git a/src/lib/Vector2.mjs b/src/lib/Vector2.mjs new file mode 100644 index 0000000..e6a5776 --- /dev/null +++ b/src/lib/Vector2.mjs @@ -0,0 +1,196 @@ +import { EPSILON } from './common.mjs'; + +/** + * Default values + * + * @readonly + * @memberof Vector2 + * @type {Array} + */ +const DEFAULT = [0.0, 0.0]; + +Object.freeze(DEFAULT); + +/** + * Element length + * + * @readonly + * @memberof Vector2 + * @type {Number} + */ +const LENGTH = 2; + +/** + * A 2-component vector. + * + * @extends Float32Array + */ +class Vector2 extends Float32Array { + /** + * Create a new vector. + * + * @param {...*} args Arguments for new vector + */ + constructor(...args) { + if (args.length === 0) { + super(DEFAULT); + } else { + super(...args); + } + + if (this.length !== LENGTH) { + throw new Error('Invalid length'); + } + } + + /** + * @type {Number} + */ + get x() { + return this[0]; + } + + set x(x) { + this[0] = x; + } + + /** + * @type {Number} + */ + get y() { + return this[1]; + } + + set y(y) { + this[1] = y; + } + + /** + * Check for approximate equality against the given vector using given + * epsilon. + * + * @param {Vector2} v Vector to compare + * @param {Number} e Epsilon + * @returns {Boolean} Approximate equality + */ + approximates(v, e = EPSILON) { + const t0 = this[0], t1 = this[1]; + const v0 = v[0], v1 = v[1]; + + return Math.abs(t0 - v0) <= e * Math.max(1.0, Math.abs(t0), Math.abs(v0)) && + Math.abs(t1 - v1) <= e * Math.max(1.0, Math.abs(t1), Math.abs(v1)); + } + + /** + * Check for exact equality against the given vector. + * + * @param {Vector2} v Vector to compare + * @returns {Boolean} Equality + */ + equals(v) { + return this[0] === v[0] && this[1] === v[1]; + } + + /** + * Set the elements of this vector to the given values. + * + * @param {Number} x X component + * @param {Number} y Y component + * @returns {Vector2} Self + */ + setElements(x, y) { + this[0] = x; + this[1] = y; + + return this; + } + + /** + * Execute the provided function once for each vector in the given array. + * + * @param {(Float32Array|Array)} arr Array to traverse + * @param {Function} cb Callback + * @returns {void} + */ + static forEach(arr, cb) { + if (arr.length % LENGTH !== 0) { + throw new Error('Invalid length'); + } + + const l = arr.length; + const v = new Vector2(); + + for (let i = 0; i < l; i += LENGTH) { + v[0] = arr[i]; + v[1] = arr[i + 1]; + + cb.call(this, v, i / LENGTH, arr); + + arr[i] = v[0]; + arr[i + 1] = v[1]; + } + } + + /** + * Generate an iterator capable of iterating over an array as vector indices. + * + * @param {(Float32Array|Array)} arr Array to iterate + * @returns {Iterator} New iterator + */ + static *keys(arr) { + if (arr.length % LENGTH !== 0) { + throw new Error('Invalid length'); + } + + const l = arr.length / LENGTH; + + for (let i = 0; i < l; i++) { + yield i; + } + } + + /** + * Create a new vector with a variable number of arguments. + * + * @param {...*} args Arguments for new vector + * @returns {Vector2} New vector + */ + static of(...args) { + if (args.length === 0) { + return new Vector2(); + } else { + return new Vector2(args); + } + } + + /** + * Generate an iterator capable of iterating over an array as vectors. + * + * @param {(Float32Array|Array)} arr Array to iterate + * @returns {Iterator} New iterator + */ + static *values(arr) { + if (arr.length % LENGTH !== 0) { + throw new Error('Invalid length'); + } + + const l = arr.length; + const v = new Vector2(); + + for (let i = 0; i < l; i += LENGTH) { + v[0] = arr[i]; + v[1] = arr[i + 1]; + + yield v; + + arr[i] = v[0]; + arr[i + 1] = v[1]; + } + } +} + +Vector2.DEFAULT = DEFAULT; + +Vector2.LENGTH = LENGTH; + +export default Vector2; diff --git a/src/lib/index.mjs b/src/lib/index.mjs index dc3decc..0e1e875 100644 --- a/src/lib/index.mjs +++ b/src/lib/index.mjs @@ -3,6 +3,7 @@ import Matrix3 from './Matrix3.mjs'; import Matrix4 from './Matrix4.mjs'; import Plane from './Plane.mjs'; import Quaternion from './Quaternion.mjs'; +import Vector2 from './Vector2.mjs'; import Vector3 from './Vector3.mjs'; export { @@ -12,5 +13,6 @@ export { Matrix4, Plane, Quaternion, + Vector2, Vector3, }; diff --git a/src/spec/Vector2.spec.js b/src/spec/Vector2.spec.js new file mode 100644 index 0000000..29cdee2 --- /dev/null +++ b/src/spec/Vector2.spec.js @@ -0,0 +1,214 @@ +import { Vector2 } from '../lib'; + +describe('Vector2', () => { + describe('prototype.constructor()', () => { + test('returns new vector with expected type', () => { + const vector = new Vector2(); + + expect(vector).toBeInstanceOf(Vector2); + expect(vector).toBeInstanceOf(Float32Array); + }); + + test('returns new vector with default values', () => { + const vector = new Vector2(); + + expect(vector).toEqual(new Vector2(Vector2.DEFAULT)); + }); + }); + + describe('prototype.x', () => { + test('gets x component', () => { + const vector = Vector2.of(1.0, 2.0); + + expect(vector.x).toEqual(1.0); + }); + + test('sets x component', () => { + const vector = Vector2.of(1.0, 2.0); + + vector.x = 4.0; + + expect(vector.x).toEqual(4.0); + }); + }); + + describe('prototype.y', () => { + test('gets y component', () => { + const vector = Vector2.of(1.0, 2.0); + + expect(vector.y).toEqual(2.0); + }); + + test('sets y component', () => { + const vector = Vector2.of(1.0, 2.0); + + vector.y = 4.0; + + expect(vector.y).toEqual(4.0); + }); + }); + + describe('prototype.approximates()', () => { + test('returns true when vectors are approximately equal', () => { + const vector1 = Vector2.of(0.0, 1.0); + const vector2 = Vector2.of(0.0, 1.000001); + + expect(vector1.approximates(vector2)).toBe(true); + }); + + test('returns true when vectors are exactly equal', () => { + const vector1 = Vector2.of(0.0, 1.0); + const vector2 = Vector2.of(0.0, 1.0); + + expect(vector1.approximates(vector2)).toBe(true); + }); + + test('returns false when vectors are not equal', () => { + const vector1 = Vector2.of(0.0, 1.0); + const vector2 = Vector2.of(0.0, 2.0); + + expect(vector1.approximates(vector2)).toBe(false); + }); + }); + + describe('prototype.equals()', () => { + test('returns true when vectors are exactly equal', () => { + const vector1 = Vector2.of(1.0, 2.0); + const vector2 = Vector2.of(1.0, 2.0); + + expect(vector1.equals(vector2)).toBe(true); + }); + + test('returns false when vectors are nearly equal', () => { + const vector1 = Vector2.of(1.000001, 2.0); + const vector2 = Vector2.of(1.000002, 2.0); + + expect(vector1.equals(vector2)).toBe(false); + }); + + test('returns false when vectors are not equal', () => { + const vector1 = Vector2.of(3.0, 4.0); + const vector2 = Vector2.of(1.0, 2.0); + + expect(vector1.equals(vector2)).toBe(false); + }); + }); + + describe('prototype.setElements()', () => { + test('sets elements in this vector', () => { + const vector = Vector2.of(7.0, 8.0); + + vector.setElements(1.0, 2.0); + + expect(vector).toEqual(Vector2.of(1.0, 2.0)); + }); + }); + + describe('forEach()', () => { + test('iterates over array', () => { + const arr = new Float32Array([1.0, 2.0, 3.0, 4.0]); + const indices = []; + + Vector2.forEach(arr, function (v, i) { + indices.push(i); + }); + + expect(indices).toEqual([0, 1]); + }); + + test('permits manipulation of array', () => { + const arr = new Float32Array([1.0, 2.0, 3.0, 4.0]); + + Vector2.forEach(arr, function (v) { + v[0] = 0.0; + }); + + expect(arr).toEqual(new Float32Array([0.0, 2.0, 0.0, 4.0])); + }); + + test('throws if array length is not multiple of Vector2.LENGTH', () => { + const arr = new Float32Array([1.0, 2.0, 3.0]); + const callback = () => {}; + + expect(() => Vector2.forEach(arr, callback)).toThrow(); + }); + }); + + describe('keys()', () => { + test('returns vector iterator', () => { + const arr = new Float32Array([1.0, 2.0, 3.0]); + const iterator = Vector2.keys(arr); + + expect(iterator.next).toBeInstanceOf(Function); + }); + + test('iterates over array as vector indices', () => { + const arr = new Float32Array([1.0, 2.0, 3.0, 4.0]); + const indices = []; + + for (const k of Vector2.keys(arr)) { + indices.push(k); + } + + expect(indices).toEqual([0, 1]); + }); + + test('throws if array length is not multiple of Vector2.LENGTH', () => { + const arr = new Float32Array([1.0, 2.0, 3.0]); + const iterator = Vector2.keys(arr); + + expect(() => iterator.next()).toThrow(); + }); + }); + + describe('of()', () => { + test('returns new vector matching given values', () => { + const vector = Vector2.of(1.0, 2.0); + + expect(vector).toEqual(new Vector2([1.0, 2.0])); + }); + + test('returns new default vector when not given values', () => { + const vector = Vector2.of(); + + expect(vector).toEqual(new Vector2(Vector2.DEFAULT)); + }); + }); + + describe('values()', () => { + test('returns vector iterator', () => { + const arr = new Float32Array([1.0, 2.0]); + const iterator = Vector2.values(arr); + + expect(iterator.next).toBeInstanceOf(Function); + }); + + test('iterates over array as vectors', () => { + const arr = new Float32Array([1.0, 2.0, 3.0, 4.0]); + const vectors = []; + + for (const v of Vector2.values(arr)) { + vectors.push(new Vector2(v)); + } + + expect(vectors).toEqual([new Vector2([1.0, 2.0]), new Vector2([3.0, 4.0])]); + }); + + test('permits manipulation of array', () => { + const arr = new Float32Array([1.0, 2.0, 3.0, 4.0]); + + for (const v of Vector2.values(arr)) { + v[0] = 9.0; + } + + expect(arr).toEqual(new Float32Array([9.0, 2.0, 9.0, 4.0])); + }); + + test('throws if array length is not multiple of Vector2.LENGTH', () => { + const arr = new Float32Array([1.0, 2.0, 3.0]); + const iterator = Vector2.values(arr); + + expect(() => iterator.next()).toThrow(); + }); + }); +});