feat: add fixed-point math and network sync, fix docs links (#440)
- feat(math): add Fixed32, FixedMath, FixedVector2 for deterministic calculations - feat(network): add FixedSnapshotBuffer and FixedClientPrediction for lockstep sync - docs: fix relative links in behavior-tree, blueprint, guide docs - docs: add missing sidebar items (cocos-editor, distributed) - docs: add scene-manager and persistent-entity Chinese translations
This commit is contained in:
450
packages/framework/math/src/Fixed32.ts
Normal file
450
packages/framework/math/src/Fixed32.ts
Normal file
@@ -0,0 +1,450 @@
|
||||
/**
|
||||
* @zh Q16.16 定点数,用于确定性计算(帧同步)
|
||||
* @en Q16.16 fixed-point number for deterministic calculations (lockstep)
|
||||
*
|
||||
* @zh 使用 16 位整数部分 + 16 位小数部分,范围 ±32767.99998
|
||||
* @en Uses 16-bit integer + 16-bit fraction, range ±32767.99998
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* const a = Fixed32.from(3.14);
|
||||
* const b = Fixed32.from(2);
|
||||
* const c = a.mul(b); // 6.28
|
||||
* console.log(c.toNumber());
|
||||
* ```
|
||||
*/
|
||||
export class Fixed32 {
|
||||
/**
|
||||
* @zh 内部原始值(32位整数)
|
||||
* @en Internal raw value (32-bit integer)
|
||||
*/
|
||||
readonly raw: number;
|
||||
|
||||
/**
|
||||
* @zh 小数位数
|
||||
* @en Fraction bits
|
||||
*/
|
||||
static readonly FRACTION_BITS = 16;
|
||||
|
||||
/**
|
||||
* @zh 缩放因子 (2^16 = 65536)
|
||||
* @en Scale factor (2^16 = 65536)
|
||||
*/
|
||||
static readonly SCALE = 65536;
|
||||
|
||||
/**
|
||||
* @zh 最大值 (约 32767.99998)
|
||||
* @en Maximum value (approximately 32767.99998)
|
||||
*/
|
||||
static readonly MAX_VALUE = 0x7FFFFFFF;
|
||||
|
||||
/**
|
||||
* @zh 最小值 (约 -32768)
|
||||
* @en Minimum value (approximately -32768)
|
||||
*/
|
||||
static readonly MIN_VALUE = -0x80000000;
|
||||
|
||||
/**
|
||||
* @zh 精度 (1/65536 ≈ 0.0000153)
|
||||
* @en Precision (1/65536 ≈ 0.0000153)
|
||||
*/
|
||||
static readonly EPSILON = 1;
|
||||
|
||||
// ==================== 常量 ====================
|
||||
|
||||
/** @zh 零 @en Zero */
|
||||
static readonly ZERO = new Fixed32(0);
|
||||
|
||||
/** @zh 一 @en One */
|
||||
static readonly ONE = new Fixed32(Fixed32.SCALE);
|
||||
|
||||
/** @zh 负一 @en Negative one */
|
||||
static readonly NEG_ONE = new Fixed32(-Fixed32.SCALE);
|
||||
|
||||
/** @zh 二分之一 @en One half */
|
||||
static readonly HALF = new Fixed32(Fixed32.SCALE >> 1);
|
||||
|
||||
/** @zh 圆周率 π @en Pi */
|
||||
static readonly PI = new Fixed32(205887); // π * 65536
|
||||
|
||||
/** @zh 2π @en Two Pi */
|
||||
static readonly TWO_PI = new Fixed32(411775); // 2π * 65536
|
||||
|
||||
/** @zh π/2 @en Pi divided by 2 */
|
||||
static readonly HALF_PI = new Fixed32(102944); // π/2 * 65536
|
||||
|
||||
/** @zh 弧度转角度系数 (180/π) @en Radians to degrees factor */
|
||||
static readonly RAD_TO_DEG = new Fixed32(3754936); // (180/π) * 65536
|
||||
|
||||
/** @zh 角度转弧度系数 (π/180) @en Degrees to radians factor */
|
||||
static readonly DEG_TO_RAD = new Fixed32(1144); // (π/180) * 65536
|
||||
|
||||
// ==================== 构造 ====================
|
||||
|
||||
/**
|
||||
* @zh 私有构造函数,使用静态方法创建实例
|
||||
* @en Private constructor, use static methods to create instances
|
||||
*/
|
||||
private constructor(raw: number) {
|
||||
// 确保是 32 位有符号整数
|
||||
this.raw = raw | 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* @zh 从浮点数创建定点数
|
||||
* @en Create fixed-point from floating-point number
|
||||
* @param n - @zh 浮点数值 @en Floating-point value
|
||||
*/
|
||||
static from(n: number): Fixed32 {
|
||||
return new Fixed32(Math.round(n * Fixed32.SCALE));
|
||||
}
|
||||
|
||||
/**
|
||||
* @zh 从原始整数值创建定点数
|
||||
* @en Create fixed-point from raw integer value
|
||||
* @param raw - @zh 原始值 @en Raw value
|
||||
*/
|
||||
static fromRaw(raw: number): Fixed32 {
|
||||
return new Fixed32(raw);
|
||||
}
|
||||
|
||||
/**
|
||||
* @zh 从整数创建定点数(无精度损失)
|
||||
* @en Create fixed-point from integer (no precision loss)
|
||||
* @param n - @zh 整数值 @en Integer value
|
||||
*/
|
||||
static fromInt(n: number): Fixed32 {
|
||||
return new Fixed32((n | 0) << Fixed32.FRACTION_BITS);
|
||||
}
|
||||
|
||||
// ==================== 转换 ====================
|
||||
|
||||
/**
|
||||
* @zh 转换为浮点数
|
||||
* @en Convert to floating-point number
|
||||
*/
|
||||
toNumber(): number {
|
||||
return this.raw / Fixed32.SCALE;
|
||||
}
|
||||
|
||||
/**
|
||||
* @zh 获取原始整数值
|
||||
* @en Get raw integer value
|
||||
*/
|
||||
toRaw(): number {
|
||||
return this.raw;
|
||||
}
|
||||
|
||||
/**
|
||||
* @zh 转换为整数(向下取整)
|
||||
* @en Convert to integer (floor)
|
||||
*/
|
||||
toInt(): number {
|
||||
return this.raw >> Fixed32.FRACTION_BITS;
|
||||
}
|
||||
|
||||
/**
|
||||
* @zh 转换为字符串
|
||||
* @en Convert to string
|
||||
*/
|
||||
toString(): string {
|
||||
return `Fixed32(${this.toNumber().toFixed(5)})`;
|
||||
}
|
||||
|
||||
// ==================== 基础运算 ====================
|
||||
|
||||
/**
|
||||
* @zh 加法
|
||||
* @en Addition
|
||||
*/
|
||||
add(other: Fixed32): Fixed32 {
|
||||
return new Fixed32(this.raw + other.raw);
|
||||
}
|
||||
|
||||
/**
|
||||
* @zh 减法
|
||||
* @en Subtraction
|
||||
*/
|
||||
sub(other: Fixed32): Fixed32 {
|
||||
return new Fixed32(this.raw - other.raw);
|
||||
}
|
||||
|
||||
/**
|
||||
* @zh 乘法(使用 64 位中间结果防止溢出)
|
||||
* @en Multiplication (uses 64-bit intermediate to prevent overflow)
|
||||
*/
|
||||
mul(other: Fixed32): Fixed32 {
|
||||
// 拆分为高低 16 位进行乘法,避免溢出
|
||||
const a = this.raw;
|
||||
const b = other.raw;
|
||||
|
||||
// 使用 BigInt 确保精度(JS 数字在大数时会丢失精度)
|
||||
// 或者使用拆分法
|
||||
const aLow = a & 0xFFFF;
|
||||
const aHigh = a >> 16;
|
||||
const bLow = b & 0xFFFF;
|
||||
const bHigh = b >> 16;
|
||||
|
||||
// (aHigh * 2^16 + aLow) * (bHigh * 2^16 + bLow) / 2^16
|
||||
// = aHigh * bHigh * 2^16 + aHigh * bLow + aLow * bHigh + aLow * bLow / 2^16
|
||||
const lowLow = (aLow * bLow) >>> 16;
|
||||
const lowHigh = aLow * bHigh;
|
||||
const highLow = aHigh * bLow;
|
||||
const highHigh = aHigh * bHigh;
|
||||
|
||||
const result = highHigh * Fixed32.SCALE + lowHigh + highLow + lowLow;
|
||||
return new Fixed32(result | 0);
|
||||
}
|
||||
|
||||
/**
|
||||
* @zh 除法
|
||||
* @en Division
|
||||
* @throws @zh 除数为零时抛出错误 @en Throws when dividing by zero
|
||||
*/
|
||||
div(other: Fixed32): Fixed32 {
|
||||
if (other.raw === 0) {
|
||||
throw new Error('Fixed32: Division by zero');
|
||||
}
|
||||
// 先左移再除,保持精度
|
||||
const result = ((this.raw * Fixed32.SCALE) / other.raw) | 0;
|
||||
return new Fixed32(result);
|
||||
}
|
||||
|
||||
/**
|
||||
* @zh 取模运算
|
||||
* @en Modulo operation
|
||||
*/
|
||||
mod(other: Fixed32): Fixed32 {
|
||||
return new Fixed32(this.raw % other.raw);
|
||||
}
|
||||
|
||||
/**
|
||||
* @zh 取反
|
||||
* @en Negation
|
||||
*/
|
||||
neg(): Fixed32 {
|
||||
return new Fixed32(-this.raw);
|
||||
}
|
||||
|
||||
/**
|
||||
* @zh 绝对值
|
||||
* @en Absolute value
|
||||
*/
|
||||
abs(): Fixed32 {
|
||||
return this.raw >= 0 ? this : new Fixed32(-this.raw);
|
||||
}
|
||||
|
||||
// ==================== 比较运算 ====================
|
||||
|
||||
/**
|
||||
* @zh 等于
|
||||
* @en Equal to
|
||||
*/
|
||||
eq(other: Fixed32): boolean {
|
||||
return this.raw === other.raw;
|
||||
}
|
||||
|
||||
/**
|
||||
* @zh 不等于
|
||||
* @en Not equal to
|
||||
*/
|
||||
ne(other: Fixed32): boolean {
|
||||
return this.raw !== other.raw;
|
||||
}
|
||||
|
||||
/**
|
||||
* @zh 小于
|
||||
* @en Less than
|
||||
*/
|
||||
lt(other: Fixed32): boolean {
|
||||
return this.raw < other.raw;
|
||||
}
|
||||
|
||||
/**
|
||||
* @zh 小于等于
|
||||
* @en Less than or equal to
|
||||
*/
|
||||
le(other: Fixed32): boolean {
|
||||
return this.raw <= other.raw;
|
||||
}
|
||||
|
||||
/**
|
||||
* @zh 大于
|
||||
* @en Greater than
|
||||
*/
|
||||
gt(other: Fixed32): boolean {
|
||||
return this.raw > other.raw;
|
||||
}
|
||||
|
||||
/**
|
||||
* @zh 大于等于
|
||||
* @en Greater than or equal to
|
||||
*/
|
||||
ge(other: Fixed32): boolean {
|
||||
return this.raw >= other.raw;
|
||||
}
|
||||
|
||||
/**
|
||||
* @zh 检查是否为零
|
||||
* @en Check if zero
|
||||
*/
|
||||
isZero(): boolean {
|
||||
return this.raw === 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* @zh 检查是否为正数
|
||||
* @en Check if positive
|
||||
*/
|
||||
isPositive(): boolean {
|
||||
return this.raw > 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* @zh 检查是否为负数
|
||||
* @en Check if negative
|
||||
*/
|
||||
isNegative(): boolean {
|
||||
return this.raw < 0;
|
||||
}
|
||||
|
||||
// ==================== 数学函数 ====================
|
||||
|
||||
/**
|
||||
* @zh 平方根(牛顿迭代法,确定性)
|
||||
* @en Square root (Newton's method, deterministic)
|
||||
*/
|
||||
static sqrt(x: Fixed32): Fixed32 {
|
||||
if (x.raw <= 0) return Fixed32.ZERO;
|
||||
|
||||
// 牛顿迭代法
|
||||
let guess = x.raw;
|
||||
let prev = 0;
|
||||
|
||||
// 固定迭代次数确保确定性
|
||||
for (let i = 0; i < 16; i++) {
|
||||
prev = guess;
|
||||
guess = ((guess + ((x.raw * Fixed32.SCALE) / guess) | 0) >> 1) | 0;
|
||||
if (guess === prev) break;
|
||||
}
|
||||
|
||||
return new Fixed32(guess);
|
||||
}
|
||||
|
||||
/**
|
||||
* @zh 向下取整
|
||||
* @en Floor
|
||||
*/
|
||||
static floor(x: Fixed32): Fixed32 {
|
||||
return new Fixed32(x.raw & ~(Fixed32.SCALE - 1));
|
||||
}
|
||||
|
||||
/**
|
||||
* @zh 向上取整
|
||||
* @en Ceiling
|
||||
*/
|
||||
static ceil(x: Fixed32): Fixed32 {
|
||||
const frac = x.raw & (Fixed32.SCALE - 1);
|
||||
if (frac === 0) return x;
|
||||
return new Fixed32((x.raw & ~(Fixed32.SCALE - 1)) + Fixed32.SCALE);
|
||||
}
|
||||
|
||||
/**
|
||||
* @zh 四舍五入
|
||||
* @en Round
|
||||
*/
|
||||
static round(x: Fixed32): Fixed32 {
|
||||
return new Fixed32((x.raw + (Fixed32.SCALE >> 1)) & ~(Fixed32.SCALE - 1));
|
||||
}
|
||||
|
||||
/**
|
||||
* @zh 最小值
|
||||
* @en Minimum
|
||||
*/
|
||||
static min(a: Fixed32, b: Fixed32): Fixed32 {
|
||||
return a.raw < b.raw ? a : b;
|
||||
}
|
||||
|
||||
/**
|
||||
* @zh 最大值
|
||||
* @en Maximum
|
||||
*/
|
||||
static max(a: Fixed32, b: Fixed32): Fixed32 {
|
||||
return a.raw > b.raw ? a : b;
|
||||
}
|
||||
|
||||
/**
|
||||
* @zh 限制范围
|
||||
* @en Clamp to range
|
||||
*/
|
||||
static clamp(x: Fixed32, min: Fixed32, max: Fixed32): Fixed32 {
|
||||
if (x.raw < min.raw) return min;
|
||||
if (x.raw > max.raw) return max;
|
||||
return x;
|
||||
}
|
||||
|
||||
/**
|
||||
* @zh 线性插值
|
||||
* @en Linear interpolation
|
||||
* @param a - @zh 起始值 @en Start value
|
||||
* @param b - @zh 结束值 @en End value
|
||||
* @param t - @zh 插值参数 (0-1) @en Interpolation parameter (0-1)
|
||||
*/
|
||||
static lerp(a: Fixed32, b: Fixed32, t: Fixed32): Fixed32 {
|
||||
// a + (b - a) * t
|
||||
return a.add(b.sub(a).mul(t));
|
||||
}
|
||||
|
||||
/**
|
||||
* @zh 符号函数
|
||||
* @en Sign function
|
||||
* @returns @zh -1, 0, 或 1 @en -1, 0, or 1
|
||||
*/
|
||||
static sign(x: Fixed32): Fixed32 {
|
||||
if (x.raw > 0) return Fixed32.ONE;
|
||||
if (x.raw < 0) return Fixed32.NEG_ONE;
|
||||
return Fixed32.ZERO;
|
||||
}
|
||||
|
||||
// ==================== 静态运算(便捷方法) ====================
|
||||
|
||||
/**
|
||||
* @zh 加法(静态)
|
||||
* @en Addition (static)
|
||||
*/
|
||||
static add(a: Fixed32, b: Fixed32): Fixed32 {
|
||||
return a.add(b);
|
||||
}
|
||||
|
||||
/**
|
||||
* @zh 减法(静态)
|
||||
* @en Subtraction (static)
|
||||
*/
|
||||
static sub(a: Fixed32, b: Fixed32): Fixed32 {
|
||||
return a.sub(b);
|
||||
}
|
||||
|
||||
/**
|
||||
* @zh 乘法(静态)
|
||||
* @en Multiplication (static)
|
||||
*/
|
||||
static mul(a: Fixed32, b: Fixed32): Fixed32 {
|
||||
return a.mul(b);
|
||||
}
|
||||
|
||||
/**
|
||||
* @zh 除法(静态)
|
||||
* @en Division (static)
|
||||
*/
|
||||
static div(a: Fixed32, b: Fixed32): Fixed32 {
|
||||
return a.div(b);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @zh Fixed32 数据接口,用于序列化
|
||||
* @en Fixed32 data interface for serialization
|
||||
*/
|
||||
export interface IFixed32 {
|
||||
raw: number;
|
||||
}
|
||||
298
packages/framework/math/src/FixedMath.ts
Normal file
298
packages/framework/math/src/FixedMath.ts
Normal file
@@ -0,0 +1,298 @@
|
||||
import { Fixed32 } from './Fixed32';
|
||||
|
||||
/**
|
||||
* @zh 定点数数学函数库,使用查表法确保确定性
|
||||
* @en Fixed-point math functions using lookup tables for determinism
|
||||
*
|
||||
* @zh 所有三角函数使用预计算的查找表,确保在所有平台上结果一致
|
||||
* @en All trigonometric functions use precomputed lookup tables to ensure consistent results across all platforms
|
||||
*/
|
||||
export class FixedMath {
|
||||
/**
|
||||
* @zh 正弦表大小(每 90 度的采样点数)
|
||||
* @en Sine table size (samples per 90 degrees)
|
||||
*/
|
||||
private static readonly SIN_TABLE_SIZE = 1024;
|
||||
|
||||
/**
|
||||
* @zh 正弦查找表(0 到 90 度)
|
||||
* @en Sine lookup table (0 to 90 degrees)
|
||||
*/
|
||||
private static readonly SIN_TABLE: Int32Array = FixedMath.generateSinTable();
|
||||
|
||||
/**
|
||||
* @zh 生成正弦查找表
|
||||
* @en Generate sine lookup table
|
||||
*/
|
||||
private static generateSinTable(): Int32Array {
|
||||
const table = new Int32Array(FixedMath.SIN_TABLE_SIZE + 1);
|
||||
for (let i = 0; i <= FixedMath.SIN_TABLE_SIZE; i++) {
|
||||
const angle = (i * Math.PI) / (2 * FixedMath.SIN_TABLE_SIZE);
|
||||
table[i] = Math.round(Math.sin(angle) * Fixed32.SCALE);
|
||||
}
|
||||
return table;
|
||||
}
|
||||
|
||||
/**
|
||||
* @zh 正弦函数(确定性)
|
||||
* @en Sine function (deterministic)
|
||||
* @param angle - @zh 角度(弧度,定点数) @en Angle in radians (fixed-point)
|
||||
*/
|
||||
static sin(angle: Fixed32): Fixed32 {
|
||||
// 将角度规范化到 [0, 2π)
|
||||
let raw = angle.raw % Fixed32.TWO_PI.raw;
|
||||
if (raw < 0) raw += Fixed32.TWO_PI.raw;
|
||||
|
||||
const halfPi = Fixed32.HALF_PI.raw;
|
||||
const pi = Fixed32.PI.raw;
|
||||
const threeHalfPi = halfPi * 3;
|
||||
|
||||
let tableAngle: number;
|
||||
let negative = false;
|
||||
|
||||
if (raw <= halfPi) {
|
||||
// 第一象限: [0, π/2]
|
||||
tableAngle = raw;
|
||||
} else if (raw <= pi) {
|
||||
// 第二象限: (π/2, π]
|
||||
tableAngle = pi - raw;
|
||||
} else if (raw <= threeHalfPi) {
|
||||
// 第三象限: (π, 3π/2]
|
||||
tableAngle = raw - pi;
|
||||
negative = true;
|
||||
} else {
|
||||
// 第四象限: (3π/2, 2π)
|
||||
tableAngle = Fixed32.TWO_PI.raw - raw;
|
||||
negative = true;
|
||||
}
|
||||
|
||||
// 计算表索引 (tableAngle 范围是 [0, π/2])
|
||||
const tableIndex = Math.min(
|
||||
((tableAngle * FixedMath.SIN_TABLE_SIZE) / halfPi) | 0,
|
||||
FixedMath.SIN_TABLE_SIZE
|
||||
);
|
||||
|
||||
const result = FixedMath.SIN_TABLE[tableIndex];
|
||||
return Fixed32.fromRaw(negative ? -result : result);
|
||||
}
|
||||
|
||||
/**
|
||||
* @zh 余弦函数(确定性)
|
||||
* @en Cosine function (deterministic)
|
||||
* @param angle - @zh 角度(弧度,定点数) @en Angle in radians (fixed-point)
|
||||
*/
|
||||
static cos(angle: Fixed32): Fixed32 {
|
||||
// cos(x) = sin(x + π/2)
|
||||
return FixedMath.sin(angle.add(Fixed32.HALF_PI));
|
||||
}
|
||||
|
||||
/**
|
||||
* @zh 正切函数(确定性)
|
||||
* @en Tangent function (deterministic)
|
||||
* @param angle - @zh 角度(弧度,定点数) @en Angle in radians (fixed-point)
|
||||
*/
|
||||
static tan(angle: Fixed32): Fixed32 {
|
||||
const cosVal = FixedMath.cos(angle);
|
||||
if (cosVal.isZero()) {
|
||||
// 返回最大值表示无穷大
|
||||
return Fixed32.fromRaw(Fixed32.MAX_VALUE);
|
||||
}
|
||||
return FixedMath.sin(angle).div(cosVal);
|
||||
}
|
||||
|
||||
/**
|
||||
* @zh 反正切函数 atan2(确定性)
|
||||
* @en Arc tangent of y/x (deterministic)
|
||||
* @param y - @zh Y 坐标 @en Y coordinate
|
||||
* @param x - @zh X 坐标 @en X coordinate
|
||||
* @returns @zh 角度(弧度,范围 -π 到 π)@en Angle in radians (range -π to π)
|
||||
*/
|
||||
static atan2(y: Fixed32, x: Fixed32): Fixed32 {
|
||||
const yRaw = y.raw;
|
||||
const xRaw = x.raw;
|
||||
|
||||
if (xRaw === 0 && yRaw === 0) {
|
||||
return Fixed32.ZERO;
|
||||
}
|
||||
|
||||
// 使用 CORDIC 算法的简化版本
|
||||
const absY = Math.abs(yRaw);
|
||||
const absX = Math.abs(xRaw);
|
||||
|
||||
let angle: number;
|
||||
|
||||
if (absX >= absY) {
|
||||
// |y/x| <= 1,使用泰勒展开近似
|
||||
angle = FixedMath.atanApprox(absY, absX);
|
||||
} else {
|
||||
// |y/x| > 1,使用恒等式 atan(y/x) = π/2 - atan(x/y)
|
||||
angle = Fixed32.HALF_PI.raw - FixedMath.atanApprox(absX, absY);
|
||||
}
|
||||
|
||||
// 根据象限调整
|
||||
if (xRaw < 0) {
|
||||
angle = Fixed32.PI.raw - angle;
|
||||
}
|
||||
if (yRaw < 0) {
|
||||
angle = -angle;
|
||||
}
|
||||
|
||||
return Fixed32.fromRaw(angle);
|
||||
}
|
||||
|
||||
/**
|
||||
* @zh atan 近似计算(内部使用)
|
||||
* @en Approximate atan calculation (internal use)
|
||||
*/
|
||||
private static atanApprox(num: number, den: number): number {
|
||||
if (den === 0) return Fixed32.HALF_PI.raw;
|
||||
|
||||
// 使用多项式近似: atan(x) ≈ x - x³/3 + x⁵/5
|
||||
// 对于 |x| <= 1 精度足够
|
||||
const ratio = ((num * Fixed32.SCALE) / den) | 0;
|
||||
|
||||
// 简化的多项式: atan(x) ≈ x * (1 - x²/3)
|
||||
// 更精确的版本: atan(x) ≈ x / (1 + 0.28125 * x²)
|
||||
const x2 = ((ratio * ratio) / Fixed32.SCALE) | 0;
|
||||
const factor = Fixed32.SCALE + ((x2 * 18432) / Fixed32.SCALE | 0); // 0.28125 * 65536 ≈ 18432
|
||||
const result = ((ratio * Fixed32.SCALE) / factor) | 0;
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* @zh 反正弦函数(确定性)
|
||||
* @en Arc sine function (deterministic)
|
||||
* @param x - @zh 值(范围 -1 到 1)@en Value (range -1 to 1)
|
||||
*/
|
||||
static asin(x: Fixed32): Fixed32 {
|
||||
// asin(x) = atan2(x, sqrt(1 - x²))
|
||||
const one = Fixed32.ONE;
|
||||
const x2 = x.mul(x);
|
||||
const sqrt = Fixed32.sqrt(one.sub(x2));
|
||||
return FixedMath.atan2(x, sqrt);
|
||||
}
|
||||
|
||||
/**
|
||||
* @zh 反余弦函数(确定性)
|
||||
* @en Arc cosine function (deterministic)
|
||||
* @param x - @zh 值(范围 -1 到 1)@en Value (range -1 to 1)
|
||||
*/
|
||||
static acos(x: Fixed32): Fixed32 {
|
||||
// acos(x) = π/2 - asin(x)
|
||||
return Fixed32.HALF_PI.sub(FixedMath.asin(x));
|
||||
}
|
||||
|
||||
/**
|
||||
* @zh 角度规范化到 [-π, π]
|
||||
* @en Normalize angle to [-π, π]
|
||||
*/
|
||||
static normalizeAngle(angle: Fixed32): Fixed32 {
|
||||
let raw = angle.raw % Fixed32.TWO_PI.raw;
|
||||
|
||||
if (raw > Fixed32.PI.raw) {
|
||||
raw -= Fixed32.TWO_PI.raw;
|
||||
} else if (raw < -Fixed32.PI.raw) {
|
||||
raw += Fixed32.TWO_PI.raw;
|
||||
}
|
||||
|
||||
return Fixed32.fromRaw(raw);
|
||||
}
|
||||
|
||||
/**
|
||||
* @zh 角度差值(最短路径)
|
||||
* @en Angle difference (shortest path)
|
||||
*/
|
||||
static angleDelta(from: Fixed32, to: Fixed32): Fixed32 {
|
||||
return FixedMath.normalizeAngle(to.sub(from));
|
||||
}
|
||||
|
||||
/**
|
||||
* @zh 角度线性插值(最短路径)
|
||||
* @en Angle linear interpolation (shortest path)
|
||||
*/
|
||||
static lerpAngle(from: Fixed32, to: Fixed32, t: Fixed32): Fixed32 {
|
||||
const delta = FixedMath.angleDelta(from, to);
|
||||
return from.add(delta.mul(t));
|
||||
}
|
||||
|
||||
/**
|
||||
* @zh 弧度转角度
|
||||
* @en Radians to degrees
|
||||
*/
|
||||
static radToDeg(rad: Fixed32): Fixed32 {
|
||||
return rad.mul(Fixed32.RAD_TO_DEG);
|
||||
}
|
||||
|
||||
/**
|
||||
* @zh 角度转弧度
|
||||
* @en Degrees to radians
|
||||
*/
|
||||
static degToRad(deg: Fixed32): Fixed32 {
|
||||
return deg.mul(Fixed32.DEG_TO_RAD);
|
||||
}
|
||||
|
||||
/**
|
||||
* @zh 幂函数(整数次幂)
|
||||
* @en Power function (integer exponent)
|
||||
*/
|
||||
static pow(base: Fixed32, exp: number): Fixed32 {
|
||||
if (exp === 0) return Fixed32.ONE;
|
||||
if (exp < 0) {
|
||||
base = Fixed32.ONE.div(base);
|
||||
exp = -exp;
|
||||
}
|
||||
|
||||
let result = Fixed32.ONE;
|
||||
while (exp > 0) {
|
||||
if (exp & 1) {
|
||||
result = result.mul(base);
|
||||
}
|
||||
base = base.mul(base);
|
||||
exp >>= 1;
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* @zh 指数函数近似(e^x)
|
||||
* @en Exponential function approximation (e^x)
|
||||
*/
|
||||
static exp(x: Fixed32): Fixed32 {
|
||||
// 使用泰勒展开: e^x ≈ 1 + x + x²/2 + x³/6 + x⁴/24
|
||||
const one = Fixed32.ONE;
|
||||
const x2 = x.mul(x);
|
||||
const x3 = x2.mul(x);
|
||||
const x4 = x3.mul(x);
|
||||
|
||||
return one
|
||||
.add(x)
|
||||
.add(x2.div(Fixed32.from(2)))
|
||||
.add(x3.div(Fixed32.from(6)))
|
||||
.add(x4.div(Fixed32.from(24)));
|
||||
}
|
||||
|
||||
/**
|
||||
* @zh 自然对数近似
|
||||
* @en Natural logarithm approximation
|
||||
*/
|
||||
static ln(x: Fixed32): Fixed32 {
|
||||
if (x.raw <= 0) {
|
||||
throw new Error('FixedMath.ln: argument must be positive');
|
||||
}
|
||||
|
||||
// 使用牛顿迭代法: y_{n+1} = y_n + 2 * (x - exp(y_n)) / (x + exp(y_n))
|
||||
let y = Fixed32.ZERO;
|
||||
const two = Fixed32.from(2);
|
||||
|
||||
for (let i = 0; i < 10; i++) {
|
||||
const expY = FixedMath.exp(y);
|
||||
const diff = x.sub(expY);
|
||||
const sum = x.add(expY);
|
||||
y = y.add(two.mul(diff).div(sum));
|
||||
}
|
||||
|
||||
return y;
|
||||
}
|
||||
}
|
||||
504
packages/framework/math/src/FixedVector2.ts
Normal file
504
packages/framework/math/src/FixedVector2.ts
Normal file
@@ -0,0 +1,504 @@
|
||||
import { Fixed32, type IFixed32 } from './Fixed32';
|
||||
import { FixedMath } from './FixedMath';
|
||||
|
||||
/**
|
||||
* @zh 定点数 2D 向量数据接口
|
||||
* @en Fixed-point 2D vector data interface
|
||||
*/
|
||||
export interface IFixedVector2 {
|
||||
x: IFixed32;
|
||||
y: IFixed32;
|
||||
}
|
||||
|
||||
/**
|
||||
* @zh 定点数 2D 向量类,用于确定性计算(帧同步)
|
||||
* @en Fixed-point 2D vector class for deterministic calculations (lockstep)
|
||||
*
|
||||
* @zh 所有运算返回新实例,保证不可变性
|
||||
* @en All operations return new instances, ensuring immutability
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* const a = FixedVector2.from(3, 4);
|
||||
* const b = FixedVector2.from(1, 2);
|
||||
* const c = a.add(b); // (4, 6)
|
||||
* const len = a.length(); // 5
|
||||
* ```
|
||||
*/
|
||||
export class FixedVector2 {
|
||||
/** @zh X 分量 @en X component */
|
||||
readonly x: Fixed32;
|
||||
|
||||
/** @zh Y 分量 @en Y component */
|
||||
readonly y: Fixed32;
|
||||
|
||||
// ==================== 常量 ====================
|
||||
|
||||
/** @zh 零向量 (0, 0) @en Zero vector */
|
||||
static readonly ZERO = new FixedVector2(Fixed32.ZERO, Fixed32.ZERO);
|
||||
|
||||
/** @zh 单位向量 (1, 1) @en One vector */
|
||||
static readonly ONE = new FixedVector2(Fixed32.ONE, Fixed32.ONE);
|
||||
|
||||
/** @zh 右方向 (1, 0) @en Right direction */
|
||||
static readonly RIGHT = new FixedVector2(Fixed32.ONE, Fixed32.ZERO);
|
||||
|
||||
/** @zh 左方向 (-1, 0) @en Left direction */
|
||||
static readonly LEFT = new FixedVector2(Fixed32.NEG_ONE, Fixed32.ZERO);
|
||||
|
||||
/** @zh 上方向 (0, 1) @en Up direction */
|
||||
static readonly UP = new FixedVector2(Fixed32.ZERO, Fixed32.ONE);
|
||||
|
||||
/** @zh 下方向 (0, -1) @en Down direction */
|
||||
static readonly DOWN = new FixedVector2(Fixed32.ZERO, Fixed32.NEG_ONE);
|
||||
|
||||
// ==================== 构造 ====================
|
||||
|
||||
/**
|
||||
* @zh 创建定点数向量
|
||||
* @en Create fixed-point vector
|
||||
*/
|
||||
constructor(x: Fixed32, y: Fixed32) {
|
||||
this.x = x;
|
||||
this.y = y;
|
||||
}
|
||||
|
||||
/**
|
||||
* @zh 从浮点数创建向量
|
||||
* @en Create vector from floating-point numbers
|
||||
*/
|
||||
static from(x: number, y: number): FixedVector2 {
|
||||
return new FixedVector2(Fixed32.from(x), Fixed32.from(y));
|
||||
}
|
||||
|
||||
/**
|
||||
* @zh 从原始整数值创建向量
|
||||
* @en Create vector from raw integer values
|
||||
*/
|
||||
static fromRaw(rawX: number, rawY: number): FixedVector2 {
|
||||
return new FixedVector2(Fixed32.fromRaw(rawX), Fixed32.fromRaw(rawY));
|
||||
}
|
||||
|
||||
/**
|
||||
* @zh 从整数创建向量
|
||||
* @en Create vector from integers
|
||||
*/
|
||||
static fromInt(x: number, y: number): FixedVector2 {
|
||||
return new FixedVector2(Fixed32.fromInt(x), Fixed32.fromInt(y));
|
||||
}
|
||||
|
||||
/**
|
||||
* @zh 从普通向量接口创建
|
||||
* @en Create from plain vector interface
|
||||
*/
|
||||
static fromObject(obj: { x: number; y: number }): FixedVector2 {
|
||||
return FixedVector2.from(obj.x, obj.y);
|
||||
}
|
||||
|
||||
// ==================== 转换 ====================
|
||||
|
||||
/**
|
||||
* @zh 转换为浮点数对象(用于渲染)
|
||||
* @en Convert to floating-point object (for rendering)
|
||||
*/
|
||||
toObject(): { x: number; y: number } {
|
||||
return {
|
||||
x: this.x.toNumber(),
|
||||
y: this.y.toNumber()
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* @zh 转换为数组
|
||||
* @en Convert to array
|
||||
*/
|
||||
toArray(): [number, number] {
|
||||
return [this.x.toNumber(), this.y.toNumber()];
|
||||
}
|
||||
|
||||
/**
|
||||
* @zh 获取原始值对象(用于网络传输)
|
||||
* @en Get raw values object (for network transmission)
|
||||
*/
|
||||
toRawObject(): { x: number; y: number } {
|
||||
return {
|
||||
x: this.x.toRaw(),
|
||||
y: this.y.toRaw()
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* @zh 转换为字符串
|
||||
* @en Convert to string
|
||||
*/
|
||||
toString(): string {
|
||||
return `FixedVector2(${this.x.toNumber().toFixed(3)}, ${this.y.toNumber().toFixed(3)})`;
|
||||
}
|
||||
|
||||
/**
|
||||
* @zh 克隆向量
|
||||
* @en Clone vector
|
||||
*/
|
||||
clone(): FixedVector2 {
|
||||
return new FixedVector2(this.x, this.y);
|
||||
}
|
||||
|
||||
// ==================== 基础运算 ====================
|
||||
|
||||
/**
|
||||
* @zh 向量加法
|
||||
* @en Vector addition
|
||||
*/
|
||||
add(other: FixedVector2): FixedVector2 {
|
||||
return new FixedVector2(this.x.add(other.x), this.y.add(other.y));
|
||||
}
|
||||
|
||||
/**
|
||||
* @zh 向量减法
|
||||
* @en Vector subtraction
|
||||
*/
|
||||
sub(other: FixedVector2): FixedVector2 {
|
||||
return new FixedVector2(this.x.sub(other.x), this.y.sub(other.y));
|
||||
}
|
||||
|
||||
/**
|
||||
* @zh 标量乘法
|
||||
* @en Scalar multiplication
|
||||
*/
|
||||
mul(scalar: Fixed32): FixedVector2 {
|
||||
return new FixedVector2(this.x.mul(scalar), this.y.mul(scalar));
|
||||
}
|
||||
|
||||
/**
|
||||
* @zh 标量除法
|
||||
* @en Scalar division
|
||||
*/
|
||||
div(scalar: Fixed32): FixedVector2 {
|
||||
return new FixedVector2(this.x.div(scalar), this.y.div(scalar));
|
||||
}
|
||||
|
||||
/**
|
||||
* @zh 分量乘法
|
||||
* @en Component-wise multiplication
|
||||
*/
|
||||
mulComponents(other: FixedVector2): FixedVector2 {
|
||||
return new FixedVector2(this.x.mul(other.x), this.y.mul(other.y));
|
||||
}
|
||||
|
||||
/**
|
||||
* @zh 分量除法
|
||||
* @en Component-wise division
|
||||
*/
|
||||
divComponents(other: FixedVector2): FixedVector2 {
|
||||
return new FixedVector2(this.x.div(other.x), this.y.div(other.y));
|
||||
}
|
||||
|
||||
/**
|
||||
* @zh 取反
|
||||
* @en Negate
|
||||
*/
|
||||
neg(): FixedVector2 {
|
||||
return new FixedVector2(this.x.neg(), this.y.neg());
|
||||
}
|
||||
|
||||
// ==================== 向量运算 ====================
|
||||
|
||||
/**
|
||||
* @zh 点积
|
||||
* @en Dot product
|
||||
*/
|
||||
dot(other: FixedVector2): Fixed32 {
|
||||
return this.x.mul(other.x).add(this.y.mul(other.y));
|
||||
}
|
||||
|
||||
/**
|
||||
* @zh 叉积(2D 返回标量)
|
||||
* @en Cross product (returns scalar in 2D)
|
||||
*/
|
||||
cross(other: FixedVector2): Fixed32 {
|
||||
return this.x.mul(other.y).sub(this.y.mul(other.x));
|
||||
}
|
||||
|
||||
/**
|
||||
* @zh 长度的平方
|
||||
* @en Length squared
|
||||
*/
|
||||
lengthSquared(): Fixed32 {
|
||||
return this.dot(this);
|
||||
}
|
||||
|
||||
/**
|
||||
* @zh 长度(模)
|
||||
* @en Length (magnitude)
|
||||
*/
|
||||
length(): Fixed32 {
|
||||
return Fixed32.sqrt(this.lengthSquared());
|
||||
}
|
||||
|
||||
/**
|
||||
* @zh 归一化(转换为单位向量)
|
||||
* @en Normalize (convert to unit vector)
|
||||
*/
|
||||
normalize(): FixedVector2 {
|
||||
const len = this.length();
|
||||
if (len.isZero()) {
|
||||
return FixedVector2.ZERO;
|
||||
}
|
||||
return this.div(len);
|
||||
}
|
||||
|
||||
/**
|
||||
* @zh 到另一个向量的距离平方
|
||||
* @en Distance squared to another vector
|
||||
*/
|
||||
distanceSquaredTo(other: FixedVector2): Fixed32 {
|
||||
const dx = this.x.sub(other.x);
|
||||
const dy = this.y.sub(other.y);
|
||||
return dx.mul(dx).add(dy.mul(dy));
|
||||
}
|
||||
|
||||
/**
|
||||
* @zh 到另一个向量的距离
|
||||
* @en Distance to another vector
|
||||
*/
|
||||
distanceTo(other: FixedVector2): Fixed32 {
|
||||
return Fixed32.sqrt(this.distanceSquaredTo(other));
|
||||
}
|
||||
|
||||
/**
|
||||
* @zh 获取垂直向量(顺时针旋转90度)
|
||||
* @en Get perpendicular vector (clockwise 90 degrees)
|
||||
*/
|
||||
perpendicular(): FixedVector2 {
|
||||
return new FixedVector2(this.y, this.x.neg());
|
||||
}
|
||||
|
||||
/**
|
||||
* @zh 获取垂直向量(逆时针旋转90度)
|
||||
* @en Get perpendicular vector (counter-clockwise 90 degrees)
|
||||
*/
|
||||
perpendicularCCW(): FixedVector2 {
|
||||
return new FixedVector2(this.y.neg(), this.x);
|
||||
}
|
||||
|
||||
/**
|
||||
* @zh 投影到另一个向量上
|
||||
* @en Project onto another vector
|
||||
*/
|
||||
projectOnto(onto: FixedVector2): FixedVector2 {
|
||||
const dot = this.dot(onto);
|
||||
const lenSq = onto.lengthSquared();
|
||||
if (lenSq.isZero()) {
|
||||
return FixedVector2.ZERO;
|
||||
}
|
||||
return onto.mul(dot.div(lenSq));
|
||||
}
|
||||
|
||||
/**
|
||||
* @zh 反射向量(关于法线)
|
||||
* @en Reflect vector (about normal)
|
||||
*/
|
||||
reflect(normal: FixedVector2): FixedVector2 {
|
||||
const dot = this.dot(normal);
|
||||
const two = Fixed32.from(2);
|
||||
return this.sub(normal.mul(two.mul(dot)));
|
||||
}
|
||||
|
||||
// ==================== 旋转和角度 ====================
|
||||
|
||||
/**
|
||||
* @zh 旋转向量(顺时针为正,左手坐标系)
|
||||
* @en Rotate vector (clockwise positive, left-hand coordinate system)
|
||||
* @param angle - @zh 旋转角度(弧度)@en Rotation angle in radians
|
||||
*/
|
||||
rotate(angle: Fixed32): FixedVector2 {
|
||||
const cos = FixedMath.cos(angle);
|
||||
const sin = FixedMath.sin(angle);
|
||||
// 顺时针旋转: x' = x*cos + y*sin, y' = -x*sin + y*cos
|
||||
return new FixedVector2(
|
||||
this.x.mul(cos).add(this.y.mul(sin)),
|
||||
this.x.neg().mul(sin).add(this.y.mul(cos))
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @zh 围绕一个点旋转
|
||||
* @en Rotate around a point
|
||||
*/
|
||||
rotateAround(center: FixedVector2, angle: Fixed32): FixedVector2 {
|
||||
return this.sub(center).rotate(angle).add(center);
|
||||
}
|
||||
|
||||
/**
|
||||
* @zh 获取向量角度(弧度)
|
||||
* @en Get vector angle in radians
|
||||
*/
|
||||
angle(): Fixed32 {
|
||||
return FixedMath.atan2(this.y, this.x);
|
||||
}
|
||||
|
||||
/**
|
||||
* @zh 获取与另一个向量的夹角
|
||||
* @en Get angle between this and another vector
|
||||
*/
|
||||
angleTo(other: FixedVector2): Fixed32 {
|
||||
const cross = this.cross(other);
|
||||
const dot = this.dot(other);
|
||||
return FixedMath.atan2(cross, dot);
|
||||
}
|
||||
|
||||
/**
|
||||
* @zh 从极坐标创建向量
|
||||
* @en Create vector from polar coordinates
|
||||
*/
|
||||
static fromPolar(length: Fixed32, angle: Fixed32): FixedVector2 {
|
||||
return new FixedVector2(
|
||||
length.mul(FixedMath.cos(angle)),
|
||||
length.mul(FixedMath.sin(angle))
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @zh 从角度创建单位向量
|
||||
* @en Create unit vector from angle
|
||||
*/
|
||||
static fromAngle(angle: Fixed32): FixedVector2 {
|
||||
return new FixedVector2(FixedMath.cos(angle), FixedMath.sin(angle));
|
||||
}
|
||||
|
||||
// ==================== 比较运算 ====================
|
||||
|
||||
/**
|
||||
* @zh 检查是否相等
|
||||
* @en Check equality
|
||||
*/
|
||||
equals(other: FixedVector2): boolean {
|
||||
return this.x.eq(other.x) && this.y.eq(other.y);
|
||||
}
|
||||
|
||||
/**
|
||||
* @zh 检查是否为零向量
|
||||
* @en Check if zero vector
|
||||
*/
|
||||
isZero(): boolean {
|
||||
return this.x.isZero() && this.y.isZero();
|
||||
}
|
||||
|
||||
// ==================== 限制和插值 ====================
|
||||
|
||||
/**
|
||||
* @zh 限制长度
|
||||
* @en Clamp length
|
||||
*/
|
||||
clampLength(maxLength: Fixed32): FixedVector2 {
|
||||
const lenSq = this.lengthSquared();
|
||||
const maxLenSq = maxLength.mul(maxLength);
|
||||
if (lenSq.gt(maxLenSq)) {
|
||||
return this.normalize().mul(maxLength);
|
||||
}
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @zh 限制分量范围
|
||||
* @en Clamp components
|
||||
*/
|
||||
clamp(min: FixedVector2, max: FixedVector2): FixedVector2 {
|
||||
return new FixedVector2(
|
||||
Fixed32.clamp(this.x, min.x, max.x),
|
||||
Fixed32.clamp(this.y, min.y, max.y)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @zh 线性插值
|
||||
* @en Linear interpolation
|
||||
*/
|
||||
lerp(target: FixedVector2, t: Fixed32): FixedVector2 {
|
||||
return new FixedVector2(
|
||||
Fixed32.lerp(this.x, target.x, t),
|
||||
Fixed32.lerp(this.y, target.y, t)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @zh 向目标移动固定距离
|
||||
* @en Move towards target by fixed distance
|
||||
*/
|
||||
moveTowards(target: FixedVector2, maxDistance: Fixed32): FixedVector2 {
|
||||
const diff = target.sub(this);
|
||||
const dist = diff.length();
|
||||
|
||||
if (dist.isZero() || dist.le(maxDistance)) {
|
||||
return target;
|
||||
}
|
||||
|
||||
return this.add(diff.div(dist).mul(maxDistance));
|
||||
}
|
||||
|
||||
// ==================== 静态方法 ====================
|
||||
|
||||
/**
|
||||
* @zh 向量加法(静态)
|
||||
* @en Vector addition (static)
|
||||
*/
|
||||
static add(a: FixedVector2, b: FixedVector2): FixedVector2 {
|
||||
return a.add(b);
|
||||
}
|
||||
|
||||
/**
|
||||
* @zh 向量减法(静态)
|
||||
* @en Vector subtraction (static)
|
||||
*/
|
||||
static sub(a: FixedVector2, b: FixedVector2): FixedVector2 {
|
||||
return a.sub(b);
|
||||
}
|
||||
|
||||
/**
|
||||
* @zh 点积(静态)
|
||||
* @en Dot product (static)
|
||||
*/
|
||||
static dot(a: FixedVector2, b: FixedVector2): Fixed32 {
|
||||
return a.dot(b);
|
||||
}
|
||||
|
||||
/**
|
||||
* @zh 叉积(静态)
|
||||
* @en Cross product (static)
|
||||
*/
|
||||
static cross(a: FixedVector2, b: FixedVector2): Fixed32 {
|
||||
return a.cross(b);
|
||||
}
|
||||
|
||||
/**
|
||||
* @zh 距离(静态)
|
||||
* @en Distance (static)
|
||||
*/
|
||||
static distance(a: FixedVector2, b: FixedVector2): Fixed32 {
|
||||
return a.distanceTo(b);
|
||||
}
|
||||
|
||||
/**
|
||||
* @zh 线性插值(静态)
|
||||
* @en Linear interpolation (static)
|
||||
*/
|
||||
static lerp(a: FixedVector2, b: FixedVector2, t: Fixed32): FixedVector2 {
|
||||
return a.lerp(b, t);
|
||||
}
|
||||
|
||||
/**
|
||||
* @zh 获取两个向量的最小分量
|
||||
* @en Get minimum components of two vectors
|
||||
*/
|
||||
static min(a: FixedVector2, b: FixedVector2): FixedVector2 {
|
||||
return new FixedVector2(Fixed32.min(a.x, b.x), Fixed32.min(a.y, b.y));
|
||||
}
|
||||
|
||||
/**
|
||||
* @zh 获取两个向量的最大分量
|
||||
* @en Get maximum components of two vectors
|
||||
*/
|
||||
static max(a: FixedVector2, b: FixedVector2): FixedVector2 {
|
||||
return new FixedVector2(Fixed32.max(a.x, b.x), Fixed32.max(a.y, b.y));
|
||||
}
|
||||
}
|
||||
@@ -3,6 +3,7 @@
|
||||
*
|
||||
* 2D数学库,为游戏开发提供完整的数学工具
|
||||
* - 基础数学类(向量、矩阵、几何形状)
|
||||
* - 定点数数学(用于帧同步确定性计算)
|
||||
* - 碰撞检测算法
|
||||
* - 动画插值和缓动函数
|
||||
* - 数学工具函数
|
||||
@@ -16,6 +17,11 @@ export { Matrix3 } from './Matrix3';
|
||||
export { Rectangle } from './Rectangle';
|
||||
export { Circle } from './Circle';
|
||||
|
||||
// 定点数数学(帧同步确定性计算)
|
||||
export { Fixed32, type IFixed32 } from './Fixed32';
|
||||
export { FixedVector2, type IFixedVector2 } from './FixedVector2';
|
||||
export { FixedMath } from './FixedMath';
|
||||
|
||||
// 数学工具
|
||||
export { MathUtils } from './MathUtils';
|
||||
|
||||
|
||||
225
packages/framework/math/tests/Fixed32.test.ts
Normal file
225
packages/framework/math/tests/Fixed32.test.ts
Normal file
@@ -0,0 +1,225 @@
|
||||
import { Fixed32 } from '../src/Fixed32';
|
||||
import { FixedMath } from '../src/FixedMath';
|
||||
|
||||
describe('Fixed32', () => {
|
||||
describe('创建和转换', () => {
|
||||
test('from 应正确从浮点数创建', () => {
|
||||
const a = Fixed32.from(3.5);
|
||||
expect(a.toNumber()).toBeCloseTo(3.5, 4);
|
||||
});
|
||||
|
||||
test('fromInt 应正确从整数创建', () => {
|
||||
const a = Fixed32.fromInt(42);
|
||||
expect(a.toInt()).toBe(42);
|
||||
expect(a.toNumber()).toBe(42);
|
||||
});
|
||||
|
||||
test('fromRaw 应正确从原始值创建', () => {
|
||||
const raw = 65536 * 2; // 2.0
|
||||
const a = Fixed32.fromRaw(raw);
|
||||
expect(a.toNumber()).toBe(2);
|
||||
});
|
||||
|
||||
test('常量应正确', () => {
|
||||
expect(Fixed32.ZERO.toNumber()).toBe(0);
|
||||
expect(Fixed32.ONE.toNumber()).toBe(1);
|
||||
expect(Fixed32.HALF.toNumber()).toBe(0.5);
|
||||
expect(Fixed32.PI.toNumber()).toBeCloseTo(Math.PI, 3);
|
||||
});
|
||||
});
|
||||
|
||||
describe('基础运算', () => {
|
||||
test('add 应正确计算', () => {
|
||||
const a = Fixed32.from(2.5);
|
||||
const b = Fixed32.from(1.5);
|
||||
expect(a.add(b).toNumber()).toBeCloseTo(4, 4);
|
||||
});
|
||||
|
||||
test('sub 应正确计算', () => {
|
||||
const a = Fixed32.from(5);
|
||||
const b = Fixed32.from(3);
|
||||
expect(a.sub(b).toNumber()).toBeCloseTo(2, 4);
|
||||
});
|
||||
|
||||
test('mul 应正确计算', () => {
|
||||
const a = Fixed32.from(3);
|
||||
const b = Fixed32.from(4);
|
||||
expect(a.mul(b).toNumber()).toBeCloseTo(12, 4);
|
||||
});
|
||||
|
||||
test('mul 应正确处理小数', () => {
|
||||
const a = Fixed32.from(2.5);
|
||||
const b = Fixed32.from(1.5);
|
||||
expect(a.mul(b).toNumber()).toBeCloseTo(3.75, 4);
|
||||
});
|
||||
|
||||
test('div 应正确计算', () => {
|
||||
const a = Fixed32.from(10);
|
||||
const b = Fixed32.from(4);
|
||||
expect(a.div(b).toNumber()).toBeCloseTo(2.5, 4);
|
||||
});
|
||||
|
||||
test('div 应抛出除零错误', () => {
|
||||
const a = Fixed32.from(10);
|
||||
expect(() => a.div(Fixed32.ZERO)).toThrow('Division by zero');
|
||||
});
|
||||
|
||||
test('neg 应正确取反', () => {
|
||||
const a = Fixed32.from(5);
|
||||
expect(a.neg().toNumber()).toBeCloseTo(-5, 4);
|
||||
});
|
||||
|
||||
test('abs 应正确取绝对值', () => {
|
||||
const a = Fixed32.from(-5);
|
||||
expect(a.abs().toNumber()).toBeCloseTo(5, 4);
|
||||
});
|
||||
});
|
||||
|
||||
describe('比较运算', () => {
|
||||
test('eq 应正确比较', () => {
|
||||
const a = Fixed32.from(5);
|
||||
const b = Fixed32.from(5);
|
||||
const c = Fixed32.from(6);
|
||||
expect(a.eq(b)).toBe(true);
|
||||
expect(a.eq(c)).toBe(false);
|
||||
});
|
||||
|
||||
test('lt/le/gt/ge 应正确比较', () => {
|
||||
const a = Fixed32.from(3);
|
||||
const b = Fixed32.from(5);
|
||||
expect(a.lt(b)).toBe(true);
|
||||
expect(a.le(b)).toBe(true);
|
||||
expect(b.gt(a)).toBe(true);
|
||||
expect(b.ge(a)).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('数学函数', () => {
|
||||
test('sqrt 应正确计算', () => {
|
||||
const a = Fixed32.from(16);
|
||||
expect(Fixed32.sqrt(a).toNumber()).toBeCloseTo(4, 3);
|
||||
|
||||
const b = Fixed32.from(2);
|
||||
expect(Fixed32.sqrt(b).toNumber()).toBeCloseTo(Math.sqrt(2), 3);
|
||||
});
|
||||
|
||||
test('floor/ceil/round 应正确计算', () => {
|
||||
const a = Fixed32.from(3.7);
|
||||
expect(Fixed32.floor(a).toNumber()).toBeCloseTo(3, 4);
|
||||
expect(Fixed32.ceil(a).toNumber()).toBeCloseTo(4, 4);
|
||||
expect(Fixed32.round(a).toNumber()).toBeCloseTo(4, 4);
|
||||
|
||||
const b = Fixed32.from(3.2);
|
||||
expect(Fixed32.round(b).toNumber()).toBeCloseTo(3, 4);
|
||||
});
|
||||
|
||||
test('min/max/clamp 应正确计算', () => {
|
||||
const a = Fixed32.from(3);
|
||||
const b = Fixed32.from(5);
|
||||
expect(Fixed32.min(a, b).toNumber()).toBe(3);
|
||||
expect(Fixed32.max(a, b).toNumber()).toBe(5);
|
||||
|
||||
const x = Fixed32.from(7);
|
||||
expect(Fixed32.clamp(x, a, b).toNumber()).toBe(5);
|
||||
});
|
||||
|
||||
test('lerp 应正确插值', () => {
|
||||
const a = Fixed32.from(0);
|
||||
const b = Fixed32.from(10);
|
||||
const t = Fixed32.from(0.5);
|
||||
expect(Fixed32.lerp(a, b, t).toNumber()).toBeCloseTo(5, 4);
|
||||
});
|
||||
});
|
||||
|
||||
describe('确定性', () => {
|
||||
test('相同输入应产生相同输出', () => {
|
||||
const results: number[] = [];
|
||||
for (let i = 0; i < 100; i++) {
|
||||
const a = Fixed32.from(3.14159);
|
||||
const b = Fixed32.from(2.71828);
|
||||
const result = a.mul(b).add(Fixed32.sqrt(a)).toRaw();
|
||||
results.push(result);
|
||||
}
|
||||
// 所有结果应该完全相同
|
||||
expect(new Set(results).size).toBe(1);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('FixedMath', () => {
|
||||
describe('三角函数', () => {
|
||||
test('sin 应正确计算', () => {
|
||||
expect(FixedMath.sin(Fixed32.ZERO).toNumber()).toBeCloseTo(0, 3);
|
||||
expect(FixedMath.sin(Fixed32.HALF_PI).toNumber()).toBeCloseTo(1, 3);
|
||||
expect(FixedMath.sin(Fixed32.PI).toNumber()).toBeCloseTo(0, 2);
|
||||
});
|
||||
|
||||
test('cos 应正确计算', () => {
|
||||
expect(FixedMath.cos(Fixed32.ZERO).toNumber()).toBeCloseTo(1, 3);
|
||||
expect(FixedMath.cos(Fixed32.HALF_PI).toNumber()).toBeCloseTo(0, 2);
|
||||
expect(FixedMath.cos(Fixed32.PI).toNumber()).toBeCloseTo(-1, 3);
|
||||
});
|
||||
|
||||
test('sin²x + cos²x = 1', () => {
|
||||
const angles = [0, 0.5, 1, 1.5, 2, 2.5, 3];
|
||||
for (const a of angles) {
|
||||
const angle = Fixed32.from(a);
|
||||
const sin = FixedMath.sin(angle);
|
||||
const cos = FixedMath.cos(angle);
|
||||
const sum = sin.mul(sin).add(cos.mul(cos));
|
||||
expect(sum.toNumber()).toBeCloseTo(1, 2);
|
||||
}
|
||||
});
|
||||
|
||||
test('atan2 应正确计算', () => {
|
||||
// atan2(0, 1) = 0
|
||||
expect(FixedMath.atan2(Fixed32.ZERO, Fixed32.ONE).toNumber()).toBeCloseTo(0, 3);
|
||||
|
||||
// atan2(1, 0) = π/2
|
||||
expect(FixedMath.atan2(Fixed32.ONE, Fixed32.ZERO).toNumber()).toBeCloseTo(Math.PI / 2, 2);
|
||||
|
||||
// atan2(1, 1) = π/4
|
||||
expect(FixedMath.atan2(Fixed32.ONE, Fixed32.ONE).toNumber()).toBeCloseTo(Math.PI / 4, 2);
|
||||
});
|
||||
});
|
||||
|
||||
describe('角度函数', () => {
|
||||
test('radToDeg/degToRad 应正确转换', () => {
|
||||
const rad = Fixed32.PI;
|
||||
const deg = FixedMath.radToDeg(rad);
|
||||
expect(deg.toNumber()).toBeCloseTo(180, 1);
|
||||
|
||||
const deg90 = Fixed32.from(90);
|
||||
const rad90 = FixedMath.degToRad(deg90);
|
||||
expect(rad90.toNumber()).toBeCloseTo(Math.PI / 2, 2);
|
||||
});
|
||||
|
||||
test('normalizeAngle 应正确规范化', () => {
|
||||
const angle1 = Fixed32.from(Math.PI * 3); // 3π -> π
|
||||
expect(Math.abs(FixedMath.normalizeAngle(angle1).toNumber())).toBeLessThanOrEqual(Math.PI + 0.1);
|
||||
|
||||
const angle2 = Fixed32.from(-Math.PI * 3); // -3π -> -π
|
||||
expect(Math.abs(FixedMath.normalizeAngle(angle2).toNumber())).toBeLessThanOrEqual(Math.PI + 0.1);
|
||||
});
|
||||
|
||||
test('lerpAngle 应走最短路径', () => {
|
||||
const from = Fixed32.from(0.1);
|
||||
const to = Fixed32.from(-0.1);
|
||||
const t = Fixed32.HALF;
|
||||
const result = FixedMath.lerpAngle(from, to, t);
|
||||
expect(result.toNumber()).toBeCloseTo(0, 2);
|
||||
});
|
||||
});
|
||||
|
||||
describe('确定性', () => {
|
||||
test('三角函数应产生确定性结果', () => {
|
||||
const results: number[] = [];
|
||||
for (let i = 0; i < 100; i++) {
|
||||
const angle = Fixed32.from(1.234);
|
||||
const result = FixedMath.sin(angle).toRaw();
|
||||
results.push(result);
|
||||
}
|
||||
expect(new Set(results).size).toBe(1);
|
||||
});
|
||||
});
|
||||
});
|
||||
242
packages/framework/math/tests/FixedVector2.test.ts
Normal file
242
packages/framework/math/tests/FixedVector2.test.ts
Normal file
@@ -0,0 +1,242 @@
|
||||
import { Fixed32 } from '../src/Fixed32';
|
||||
import { FixedVector2 } from '../src/FixedVector2';
|
||||
|
||||
describe('FixedVector2', () => {
|
||||
describe('创建和转换', () => {
|
||||
test('from 应正确从浮点数创建', () => {
|
||||
const v = FixedVector2.from(3, 4);
|
||||
const obj = v.toObject();
|
||||
expect(obj.x).toBeCloseTo(3, 4);
|
||||
expect(obj.y).toBeCloseTo(4, 4);
|
||||
});
|
||||
|
||||
test('fromInt 应正确从整数创建', () => {
|
||||
const v = FixedVector2.fromInt(5, 6);
|
||||
expect(v.x.toInt()).toBe(5);
|
||||
expect(v.y.toInt()).toBe(6);
|
||||
});
|
||||
|
||||
test('常量应正确', () => {
|
||||
expect(FixedVector2.ZERO.isZero()).toBe(true);
|
||||
expect(FixedVector2.ONE.x.toNumber()).toBe(1);
|
||||
expect(FixedVector2.ONE.y.toNumber()).toBe(1);
|
||||
expect(FixedVector2.RIGHT.x.toNumber()).toBe(1);
|
||||
expect(FixedVector2.RIGHT.y.toNumber()).toBe(0);
|
||||
});
|
||||
|
||||
test('toRawObject 应返回原始值', () => {
|
||||
const v = FixedVector2.from(1, 2);
|
||||
const raw = v.toRawObject();
|
||||
expect(raw.x).toBe(Fixed32.from(1).toRaw());
|
||||
expect(raw.y).toBe(Fixed32.from(2).toRaw());
|
||||
});
|
||||
});
|
||||
|
||||
describe('基础运算', () => {
|
||||
test('add 应正确计算', () => {
|
||||
const a = FixedVector2.from(1, 2);
|
||||
const b = FixedVector2.from(3, 4);
|
||||
const result = a.add(b).toObject();
|
||||
expect(result.x).toBeCloseTo(4, 4);
|
||||
expect(result.y).toBeCloseTo(6, 4);
|
||||
});
|
||||
|
||||
test('sub 应正确计算', () => {
|
||||
const a = FixedVector2.from(5, 7);
|
||||
const b = FixedVector2.from(2, 3);
|
||||
const result = a.sub(b).toObject();
|
||||
expect(result.x).toBeCloseTo(3, 4);
|
||||
expect(result.y).toBeCloseTo(4, 4);
|
||||
});
|
||||
|
||||
test('mul 应正确计算标量乘法', () => {
|
||||
const v = FixedVector2.from(3, 4);
|
||||
const result = v.mul(Fixed32.from(2)).toObject();
|
||||
expect(result.x).toBeCloseTo(6, 4);
|
||||
expect(result.y).toBeCloseTo(8, 4);
|
||||
});
|
||||
|
||||
test('div 应正确计算标量除法', () => {
|
||||
const v = FixedVector2.from(6, 8);
|
||||
const result = v.div(Fixed32.from(2)).toObject();
|
||||
expect(result.x).toBeCloseTo(3, 4);
|
||||
expect(result.y).toBeCloseTo(4, 4);
|
||||
});
|
||||
|
||||
test('neg 应正确取反', () => {
|
||||
const v = FixedVector2.from(3, -4);
|
||||
const result = v.neg().toObject();
|
||||
expect(result.x).toBeCloseTo(-3, 4);
|
||||
expect(result.y).toBeCloseTo(4, 4);
|
||||
});
|
||||
});
|
||||
|
||||
describe('向量运算', () => {
|
||||
test('dot 应正确计算点积', () => {
|
||||
const a = FixedVector2.from(1, 2);
|
||||
const b = FixedVector2.from(3, 4);
|
||||
// 1*3 + 2*4 = 11
|
||||
expect(a.dot(b).toNumber()).toBeCloseTo(11, 4);
|
||||
});
|
||||
|
||||
test('cross 应正确计算叉积', () => {
|
||||
const a = FixedVector2.from(1, 0);
|
||||
const b = FixedVector2.from(0, 1);
|
||||
// 1*1 - 0*0 = 1
|
||||
expect(a.cross(b).toNumber()).toBeCloseTo(1, 4);
|
||||
});
|
||||
|
||||
test('length 应正确计算', () => {
|
||||
const v = FixedVector2.from(3, 4);
|
||||
expect(v.length().toNumber()).toBeCloseTo(5, 3);
|
||||
});
|
||||
|
||||
test('lengthSquared 应正确计算', () => {
|
||||
const v = FixedVector2.from(3, 4);
|
||||
expect(v.lengthSquared().toNumber()).toBeCloseTo(25, 4);
|
||||
});
|
||||
|
||||
test('normalize 应正确归一化', () => {
|
||||
const v = FixedVector2.from(3, 4);
|
||||
const n = v.normalize();
|
||||
expect(n.length().toNumber()).toBeCloseTo(1, 2);
|
||||
expect(n.x.toNumber()).toBeCloseTo(0.6, 2);
|
||||
expect(n.y.toNumber()).toBeCloseTo(0.8, 2);
|
||||
});
|
||||
|
||||
test('normalize 零向量应返回零向量', () => {
|
||||
const v = FixedVector2.ZERO;
|
||||
const n = v.normalize();
|
||||
expect(n.isZero()).toBe(true);
|
||||
});
|
||||
|
||||
test('distanceTo 应正确计算', () => {
|
||||
const a = FixedVector2.from(0, 0);
|
||||
const b = FixedVector2.from(3, 4);
|
||||
expect(a.distanceTo(b).toNumber()).toBeCloseTo(5, 3);
|
||||
});
|
||||
|
||||
test('perpendicular 应正确计算', () => {
|
||||
const v = FixedVector2.from(1, 0);
|
||||
const perp = v.perpendicular();
|
||||
// 顺时针 90 度: (1, 0) -> (0, -1)
|
||||
expect(perp.x.toNumber()).toBeCloseTo(0, 4);
|
||||
expect(perp.y.toNumber()).toBeCloseTo(-1, 4);
|
||||
});
|
||||
});
|
||||
|
||||
describe('旋转和角度', () => {
|
||||
test('rotate 应正确旋转', () => {
|
||||
const v = FixedVector2.from(1, 0);
|
||||
const angle = Fixed32.HALF_PI; // 90 度
|
||||
const rotated = v.rotate(angle);
|
||||
// 顺时针旋转 90 度: (1, 0) -> (0, -1)
|
||||
expect(rotated.x.toNumber()).toBeCloseTo(0, 2);
|
||||
expect(rotated.y.toNumber()).toBeCloseTo(-1, 2);
|
||||
});
|
||||
|
||||
test('angle 应正确计算', () => {
|
||||
const v = FixedVector2.from(1, 0);
|
||||
expect(v.angle().toNumber()).toBeCloseTo(0, 3);
|
||||
|
||||
const v2 = FixedVector2.from(0, 1);
|
||||
expect(v2.angle().toNumber()).toBeCloseTo(Math.PI / 2, 2);
|
||||
});
|
||||
|
||||
test('fromAngle 应正确创建', () => {
|
||||
const v = FixedVector2.fromAngle(Fixed32.ZERO);
|
||||
expect(v.x.toNumber()).toBeCloseTo(1, 3);
|
||||
expect(v.y.toNumber()).toBeCloseTo(0, 3);
|
||||
});
|
||||
|
||||
test('fromPolar 应正确创建', () => {
|
||||
const v = FixedVector2.fromPolar(Fixed32.from(5), Fixed32.ZERO);
|
||||
expect(v.x.toNumber()).toBeCloseTo(5, 3);
|
||||
expect(v.y.toNumber()).toBeCloseTo(0, 3);
|
||||
});
|
||||
});
|
||||
|
||||
describe('插值和限制', () => {
|
||||
test('lerp 应正确插值', () => {
|
||||
const a = FixedVector2.from(0, 0);
|
||||
const b = FixedVector2.from(10, 20);
|
||||
const result = a.lerp(b, Fixed32.HALF).toObject();
|
||||
expect(result.x).toBeCloseTo(5, 4);
|
||||
expect(result.y).toBeCloseTo(10, 4);
|
||||
});
|
||||
|
||||
test('clampLength 应正确限制长度', () => {
|
||||
const v = FixedVector2.from(6, 8); // 长度 10
|
||||
const clamped = v.clampLength(Fixed32.from(5));
|
||||
expect(clamped.length().toNumber()).toBeCloseTo(5, 2);
|
||||
});
|
||||
|
||||
test('moveTowards 应正确移动', () => {
|
||||
const a = FixedVector2.from(0, 0);
|
||||
const b = FixedVector2.from(10, 0);
|
||||
const result = a.moveTowards(b, Fixed32.from(3));
|
||||
expect(result.x.toNumber()).toBeCloseTo(3, 3);
|
||||
expect(result.y.toNumber()).toBeCloseTo(0, 3);
|
||||
});
|
||||
});
|
||||
|
||||
describe('比较运算', () => {
|
||||
test('equals 应正确比较', () => {
|
||||
const a = FixedVector2.from(3, 4);
|
||||
const b = FixedVector2.from(3, 4);
|
||||
const c = FixedVector2.from(3, 5);
|
||||
expect(a.equals(b)).toBe(true);
|
||||
expect(a.equals(c)).toBe(false);
|
||||
});
|
||||
|
||||
test('isZero 应正确判断', () => {
|
||||
expect(FixedVector2.ZERO.isZero()).toBe(true);
|
||||
expect(FixedVector2.ONE.isZero()).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('确定性', () => {
|
||||
test('向量运算应产生确定性结果', () => {
|
||||
const results: string[] = [];
|
||||
for (let i = 0; i < 100; i++) {
|
||||
const a = FixedVector2.from(3.14159, 2.71828);
|
||||
const b = FixedVector2.from(1.41421, 1.73205);
|
||||
const result = a.add(b).mul(Fixed32.from(0.5)).normalize();
|
||||
results.push(`${result.x.toRaw()},${result.y.toRaw()}`);
|
||||
}
|
||||
expect(new Set(results).size).toBe(1);
|
||||
});
|
||||
|
||||
test('旋转应产生确定性结果', () => {
|
||||
const results: string[] = [];
|
||||
for (let i = 0; i < 100; i++) {
|
||||
const v = FixedVector2.from(1, 0);
|
||||
const angle = Fixed32.from(0.7853981634); // π/4
|
||||
const rotated = v.rotate(angle);
|
||||
results.push(`${rotated.x.toRaw()},${rotated.y.toRaw()}`);
|
||||
}
|
||||
expect(new Set(results).size).toBe(1);
|
||||
});
|
||||
});
|
||||
|
||||
describe('静态方法', () => {
|
||||
test('distance 应正确计算', () => {
|
||||
const a = FixedVector2.from(0, 0);
|
||||
const b = FixedVector2.from(3, 4);
|
||||
expect(FixedVector2.distance(a, b).toNumber()).toBeCloseTo(5, 3);
|
||||
});
|
||||
|
||||
test('min/max 应正确计算', () => {
|
||||
const a = FixedVector2.from(1, 5);
|
||||
const b = FixedVector2.from(3, 2);
|
||||
|
||||
const min = FixedVector2.min(a, b);
|
||||
expect(min.x.toNumber()).toBeCloseTo(1, 4);
|
||||
expect(min.y.toNumber()).toBeCloseTo(2, 4);
|
||||
|
||||
const max = FixedVector2.max(a, b);
|
||||
expect(max.x.toNumber()).toBeCloseTo(3, 4);
|
||||
expect(max.y.toNumber()).toBeCloseTo(5, 4);
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user