@turbox3d/math
Version:
Large-scale graphics application math library
347 lines (317 loc) • 11.5 kB
text/typescript
/* eslint-disable camelcase */
import { Quaternion } from './base/Quaternion';
import { Tolerance } from './base/Tolerance';
import { Vector2 } from './base/Vector2';
import { Vector3 } from './base/Vector3';
const _lut: string[] = [];
for (let i = 0; i < 256; i++) {
_lut[i] = (i < 16 ? '0' : '') + i.toString(16);
}
let _seed = 1234567;
const MathUtils = {
DEG2RAD: Math.PI / 180,
RAD2DEG: 180 / Math.PI,
generateUUID() {
// http://stackoverflow.com/questions/105034/how-to-create-a-guid-uuid-in-javascript/21963136#21963136
const d0 = (Math.random() * 0xffffffff) | 0;
const d1 = (Math.random() * 0xffffffff) | 0;
const d2 = (Math.random() * 0xffffffff) | 0;
const d3 = (Math.random() * 0xffffffff) | 0;
const uuid = `${_lut[d0 & 0xff] + _lut[(d0 >> 8) & 0xff] + _lut[(d0 >> 16) & 0xff] + _lut[(d0 >> 24) & 0xff]}-${
_lut[d1 & 0xff]
}${_lut[(d1 >> 8) & 0xff]}-${_lut[((d1 >> 16) & 0x0f) | 0x40]}${_lut[(d1 >> 24) & 0xff]}-${
_lut[(d2 & 0x3f) | 0x80]
}${_lut[(d2 >> 8) & 0xff]}-${_lut[(d2 >> 16) & 0xff]}${_lut[(d2 >> 24) & 0xff]}${_lut[d3 & 0xff]}${
_lut[(d3 >> 8) & 0xff]
}${_lut[(d3 >> 16) & 0xff]}${_lut[(d3 >> 24) & 0xff]}`;
// .toUpperCase() here flattens concatenated strings to save heap memory space.
return uuid.toUpperCase();
},
clamp(value: number, min: number, max: number) {
return Math.max(min, Math.min(max, value));
},
// compute euclidian modulo of m % n
// https://en.wikipedia.org/wiki/Modulo_operation
euclideanModulo(n: number, m: number) {
return ((n % m) + m) % m;
},
// Linear mapping from range <a1, a2> to range <b1, b2>
mapLinear(x: number, a1: number, a2: number, b1: number, b2: number) {
return b1 + ((x - a1) * (b2 - b1)) / (a2 - a1);
},
// https://en.wikipedia.org/wiki/Linear_interpolation
lerp(x: number, y: number, t: number) {
return (1 - t) * x + t * y;
},
// http://www.rorydriscoll.com/2016/03/07/frame-rate-independent-damping-using-lerp/
damp(x: number, y: number, lambda: number, dt: number) {
return MathUtils.lerp(x, y, 1 - Math.exp(-lambda * dt));
},
// https://www.desmos.com/calculator/vcsjnyz7x4
pingpong(x: number, length = 1) {
return length - Math.abs(MathUtils.euclideanModulo(x, length * 2) - length);
},
// http://en.wikipedia.org/wiki/Smoothstep
smoothstep(x: number, min: number, max: number) {
if (x <= min) return 0;
if (x >= max) return 1;
x = (x - min) / (max - min);
return x * x * (3 - 2 * x);
},
smootherstep(x: number, min: number, max: number) {
if (x <= min) return 0;
if (x >= max) return 1;
x = (x - min) / (max - min);
return x * x * x * (x * (x * 6 - 15) + 10);
},
// Random integer from <low, high> interval
randInt(low: number, high: number) {
return low + Math.floor(Math.random() * (high - low + 1));
},
// Random float from <low, high> interval
randFloat(low: number, high: number) {
return low + Math.random() * (high - low);
},
// Random float from <-range/2, range/2> interval
randFloatSpread(range: number) {
return range * (0.5 - Math.random());
},
// Deterministic pseudo-random float in the interval [ 0, 1 ]
seededRandom(s: number) {
if (s !== undefined) _seed = s % 2147483647;
// Park-Miller algorithm
_seed = (_seed * 16807) % 2147483647;
return (_seed - 1) / 2147483646;
},
degToRad(degrees: number) {
return degrees * MathUtils.DEG2RAD;
},
radToDeg(radians: number) {
return radians * MathUtils.RAD2DEG;
},
isPowerOfTwo(value: number) {
return (value & (value - 1)) === 0 && value !== 0;
},
ceilPowerOfTwo(value: number) {
return 2 ** Math.ceil(Math.log(value) / Math.LN2);
},
floorPowerOfTwo(value: number) {
return 2 ** Math.floor(Math.log(value) / Math.LN2);
},
setQuaternionFromProperEuler(q: Quaternion, a: number, b: number, c: number, order: string) {
// Intrinsic Proper Euler Angles - see https://en.wikipedia.org/wiki/Euler_angles
// rotations are applied to the axes in the order specified by 'order'
// rotation by angle 'a' is applied first, then by angle 'b', then by angle 'c'
// angles are in radians
const cos = Math.cos;
const sin = Math.sin;
const c2 = cos(b / 2);
const s2 = sin(b / 2);
const c13 = cos((a + c) / 2);
const s13 = sin((a + c) / 2);
const c1_3 = cos((a - c) / 2);
const s1_3 = sin((a - c) / 2);
const c3_1 = cos((c - a) / 2);
const s3_1 = sin((c - a) / 2);
switch (order) {
case 'XYX':
q.set(c2 * s13, s2 * c1_3, s2 * s1_3, c2 * c13);
break;
case 'YZY':
q.set(s2 * s1_3, c2 * s13, s2 * c1_3, c2 * c13);
break;
case 'ZXZ':
q.set(s2 * c1_3, s2 * s1_3, c2 * s13, c2 * c13);
break;
case 'XZX':
q.set(c2 * s13, s2 * s3_1, s2 * c3_1, c2 * c13);
break;
case 'YXY':
q.set(s2 * c3_1, c2 * s13, s2 * s3_1, c2 * c13);
break;
case 'ZYZ':
q.set(s2 * s3_1, s2 * c3_1, c2 * s13, c2 * c13);
break;
default:
console.warn(`THREE.MathUtils: .setQuaternionFromProperEuler() encountered an unknown order: ${order}`);
}
},
isEqual(num1: number, num2: number, tol?: number) {
const tolerance = tol || Tolerance.global.numTol;
if (num1 === num2) {
return true;
}
return Math.abs(num1 - num2) <= tolerance;
},
isZero(num: number, tol?: number) {
return MathUtils.isEqual(num, 0, tol);
},
/**
* Check wether num1 is smaller than num2 with specified tolerance.
* Global (default) tolerance will be used if tol is not provided.
* @param num1
* @param num2
* @param tol
*/
isSmaller(num1: number, num2: number, tol?: number) {
const tolerance = tol || Tolerance.global.numTol;
const diff = num1 - num2;
return diff < -tolerance;
},
/**
* Check whether num1 is bigger than num2 with specified tolerance.
* Global (default) tolerance will be used if tol is not provided.
* @param num1
* @param num2
* @param tol
*/
isBigger(num1: number, num2: number, tol?: number) {
const tolerance = tol || Tolerance.global.numTol;
const diff = num1 - num2;
return diff > tolerance;
},
/**
* Check whether num1 is smaller than or equal to num2 with specified tolerance.
* Global (default) tolerance will be used if tol is not provided.
* @param num1
* @param num2
* @param tol
*/
isSmallerOrEqual(num1: number, num2: number, tol?: number) {
const tolerance = tol || Tolerance.global.numTol;
const diff = num1 - num2;
return diff <= tolerance;
},
/**
* Check whether num1 is bigger than or equal to num2 with specified tolerance.
* Global (default) tolerance will be used if tol is not provided.
* @param num1
* @param num2
* @param tol
*/
isBiggerOrEqual(num1: number, num2: number, tol?: number) {
const tolerance = tol || Tolerance.global.numTol;
const diff = num1 - num2;
return diff >= -tolerance;
},
/**
* Check whether a number is in a range with specified tolerance.
* Global (default) tolerance will be used if tol is not provided.
* @param value the value to be checked.
* @param min the range's lower limit.
* @param max the range's upper limit.
* @param minOpen whether the range's lower limit is open, default is false.
* @param maxOpen whether the range's upper limit is open, default is false.
* @param tol
*/
isInRange(value: number, min: number, max: number, minOpen?: boolean, maxOpen?: boolean, tol?: number) {
const tolerance = tol || Tolerance.global.numTol;
const diffMin = value - min;
const diffMax = value - max;
const biggerMin = minOpen ? diffMin > tolerance : diffMin >= -tolerance;
const smallerMax = maxOpen ? diffMax < -tolerance : diffMax <= tolerance;
return biggerMin && smallerMax;
},
/**
* Compare two numbers:
* If num1 is fuzzy equal to num2, return 0.
* If num1 is fuzzy bigger than num2, return 1.
* If num1 is fuzzy smaller than num2, return -1.
* If num1 or num2 is a NaN, throw exception.
* @param num1
* @param num2
* @param tol
*/
compare(num1: number, num2: number, tol?: number): -1 | 0 | 1 {
if (Number.isNaN(num1) || Number.isNaN(num2)) {
throw Error('Invalid NaN number');
}
if (MathUtils.isEqual(num1, num2, tol)) {
return 0;
}
if (num1 < num2) {
return -1;
}
return 1;
},
/**
* 单向线性插值
*/
interpolation(start: Vector3, end: Vector3, segments = 10) {
const v = end.subtracted(start);
const step = v.length / segments;
const points: Vector3[] = [];
const n = v.normalized();
for (let index = 0; index < segments - 1; index++) {
points.push(start.added(n.clone().multiplyScalar(step * (index + 1))));
}
points.unshift(start.clone());
points.push(end.clone());
return points;
},
/**
* 根据四方点生成网格
* @param quadPositions 四方点,左上角为起点,按照顺时针顺序
* @param widthSegments 宽度分割段数
* @param heightSegments 高度分割段数
*/
generateMeshByQuad(quadPositions: Vector3[], widthSegments = 10, heightSegments = 10) {
const indices: number[] = [];
const vertices: number[] = [];
const normals: number[] = [];
const uvs: number[] = [];
const a1 = MathUtils.interpolation(quadPositions[0], quadPositions[1], widthSegments);
const a2 = MathUtils.interpolation(quadPositions[3], quadPositions[2], widthSegments);
const a3 = a1.map((v, i) => MathUtils.interpolation(v.clone(), a2[i].clone(), heightSegments));
for (let i = 0; i <= heightSegments; i++) {
if (i === 0) {
vertices.push(...a1.flatMap(a => a.toArray()));
} else if (i === heightSegments) {
vertices.push(...a2.flatMap(a => a.toArray()));
} else {
vertices.push(...a3.flatMap(a => a[i].toArray()));
}
for (let j = 0; j <= widthSegments; j++) {
normals.push(0, 0, 1);
uvs.push(j * (1 / widthSegments), i * (1 / heightSegments));
}
}
for (let i = 0; i < heightSegments; i++) {
for (let j = 0; j < widthSegments; j++) {
const a = i * (widthSegments + 1) + (j + 1);
const b = i * (widthSegments + 1) + j;
const c = (i + 1) * (widthSegments + 1) + j;
const d = (i + 1) * (widthSegments + 1) + (j + 1);
indices.push(a, b, d);
indices.push(b, c, d);
}
}
return {
indices,
vertices,
normals,
uvs,
};
},
/** 把无序的点排序成顺逆时针的邻接点,只支持凸多边形 */
clockwisePoints(points: Vector2[], clockwise = false) {
const newPoints: Vector2[] = [points[0]];
for (let i = 0; i < points.length - 1; i++) {
const p1 = newPoints[newPoints.length - 1];
const waitComparePoints = points.filter(wp => wp !== p1);
for (let index = 0; index < waitComparePoints.length; index++) {
const p2 = waitComparePoints[index];
const v = new Vector2(p2.x - p1.x, p2.y - p1.y);
const otherPoints = points.filter(p => p !== p1 && p !== p2);
const otherVs = otherPoints.map(p => new Vector2(p.x - p1.x, p.y - p1.y));
const isAllMatched = otherVs.every(ov => (clockwise ? v.cross(ov) < 0 : v.cross(ov) > 0));
if (isAllMatched) {
newPoints.push(p2);
break;
}
}
}
return newPoints;
},
};
export { MathUtils };