feat: 集成Rust WASM渲染引擎与TypeScript ECS框架 (#228)

* feat: 集成Rust WASM渲染引擎与TypeScript ECS框架

* feat: 增强编辑器UI功能与跨平台支持

* fix: 修复CI测试和类型检查问题

* fix: 修复CI问题并提高测试覆盖率

* fix: 修复CI问题并提高测试覆盖率
This commit is contained in:
YHH
2025-11-21 10:03:18 +08:00
committed by GitHub
parent 8b9616837d
commit a768b890fd
107 changed files with 10221 additions and 477 deletions

View File

@@ -40,7 +40,7 @@
"test:coverage": "jest --coverage --config jest.config.cjs",
"test:ci": "jest --ci --coverage --config jest.config.cjs",
"test:clear": "jest --clearCache",
"type-check": "tsc --noEmit",
"type-check": "npx tsc --noEmit",
"lint": "eslint \"src/**/*.{ts,tsx}\"",
"lint:fix": "eslint \"src/**/*.{ts,tsx}\" --fix"
},

View File

@@ -1,4 +1,5 @@
import type { IComponent } from '../Types';
import { Int32 } from './Core/SoAStorage';
/**
* 游戏组件基类
@@ -50,6 +51,7 @@ export abstract class Component implements IComponent {
*
* 存储实体ID而非引用避免循环引用符合ECS数据导向设计。
*/
@Int32
public entityId: number | null = null;
/**

View File

@@ -0,0 +1,108 @@
import { createLogger } from '../../Utils/Logger';
/**
* SoA 序列化器
* 负责复杂类型的序列化/反序列化和深拷贝
*/
export class SoASerializer {
private static readonly _logger = createLogger('SoASerializer');
/**
* 序列化值为 JSON 字符串
*/
public static serialize(
value: unknown,
fieldName: string,
options: {
isMap?: boolean;
isSet?: boolean;
isArray?: boolean;
} = {}
): string {
try {
if (options.isMap && value instanceof Map) {
return JSON.stringify(Array.from(value.entries()));
}
if (options.isSet && value instanceof Set) {
return JSON.stringify(Array.from(value));
}
if (options.isArray && Array.isArray(value)) {
return JSON.stringify(value);
}
return JSON.stringify(value);
} catch (error) {
this._logger.warn(`SoA序列化字段 ${fieldName} 失败:`, error);
return '{}';
}
}
/**
* 反序列化 JSON 字符串为值
*/
public static deserialize(
serialized: string,
fieldName: string,
options: {
isMap?: boolean;
isSet?: boolean;
isArray?: boolean;
} = {}
): unknown {
try {
const parsed = JSON.parse(serialized);
if (options.isMap) {
return new Map(parsed);
}
if (options.isSet) {
return new Set(parsed);
}
return parsed;
} catch (error) {
this._logger.warn(`SoA反序列化字段 ${fieldName} 失败:`, error);
return null;
}
}
/**
* 深拷贝对象
*/
public static deepClone<T>(obj: T): T {
if (obj === null || typeof obj !== 'object') {
return obj;
}
if (obj instanceof Date) {
return new Date(obj.getTime()) as T;
}
if (Array.isArray(obj)) {
return obj.map((item) => this.deepClone(item)) as T;
}
if (obj instanceof Map) {
const cloned = new Map();
for (const [key, value] of obj.entries()) {
cloned.set(key, this.deepClone(value));
}
return cloned as T;
}
if (obj instanceof Set) {
const cloned = new Set();
for (const value of obj.values()) {
cloned.add(this.deepClone(value));
}
return cloned as T;
}
// 普通对象
const cloned = {} as Record<string, unknown>;
for (const key in obj) {
if (Object.prototype.hasOwnProperty.call(obj, key)) {
cloned[key] = this.deepClone((obj as Record<string, unknown>)[key]);
}
}
return cloned as T;
}
}

View File

@@ -1,6 +1,16 @@
import { Component } from '../Component';
import { ComponentType } from './ComponentStorage';
import { createLogger } from '../../Utils/Logger';
import {
SoATypeRegistry,
SupportedTypedArray,
TypedArrayTypeName
} from './SoATypeRegistry';
import { SoASerializer } from './SoASerializer';
// 重新导出类型,保持向后兼容
export { SupportedTypedArray, TypedArrayTypeName } from './SoATypeRegistry';
export { SoATypeRegistry } from './SoATypeRegistry';
export { SoASerializer } from './SoASerializer';
/**
* 启用SoA优化装饰器
@@ -12,18 +22,6 @@ export function EnableSoA<T extends ComponentType>(target: T): T {
}
/**
* 高精度数值装饰器
* 标记字段需要保持完整精度存储为复杂对象而非TypedArray
*/
export function HighPrecision(target: any, propertyKey: string | symbol): void {
const key = String(propertyKey);
if (!target.constructor.__highPrecisionFields) {
target.constructor.__highPrecisionFields = new Set();
}
target.constructor.__highPrecisionFields.add(key);
}
/**
* 64位浮点数装饰器
* 标记字段使用Float64Array存储更高精度但更多内存
@@ -181,164 +179,16 @@ export function DeepCopy(target: any, propertyKey: string | symbol): void {
target.constructor.__deepCopyFields.add(key);
}
/**
* 自动类型推断装饰器
* 根据字段的默认值和数值范围自动选择最优的TypedArray类型
*
* @param options 类型推断选项
* @param options.minValue 数值的最小值(用于范围优化)
* @param options.maxValue 数值的最大值(用于范围优化)
* @param options.precision 是否需要浮点精度true: 使用浮点数组, false: 使用整数数组)
* @param options.signed 是否需要符号位(仅在整数模式下有效)
*/
export function AutoTyped(options?: {
minValue?: number;
maxValue?: number;
precision?: boolean;
signed?: boolean;
}) {
return function (target: any, propertyKey: string | symbol): void {
const key = String(propertyKey);
if (!target.constructor.__autoTypedFields) {
target.constructor.__autoTypedFields = new Map();
}
target.constructor.__autoTypedFields.set(key, options || {});
};
}
/**
* 自动类型推断器
* 根据数值类型和范围自动选择最优的TypedArray类型
*/
export class TypeInference {
/**
* 根据数值范围推断最优的TypedArray类型
*/
public static inferOptimalType(value: any, options: {
minValue?: number;
maxValue?: number;
precision?: boolean;
signed?: boolean;
} = {}): string {
const type = typeof value;
if (type === 'boolean') {
return 'uint8'; // 布尔值使用最小的无符号整数
}
if (type !== 'number') {
return 'float32'; // 非数值类型默认使用Float32
}
const { minValue, maxValue, precision, signed } = options;
// 如果显式要求精度,使用浮点数
if (precision === true) {
// 检查是否需要双精度
if (Math.abs(value) > 3.4028235e+38 || (minValue !== undefined && Math.abs(minValue) > 3.4028235e+38) || (maxValue !== undefined && Math.abs(maxValue) > 3.4028235e+38)) {
return 'float64';
}
return 'float32';
}
// 如果显式禁用精度,或者是整数值,尝试使用整数数组
if (precision === false || Number.isInteger(value)) {
const actualMin = minValue !== undefined ? minValue : value;
const actualMax = maxValue !== undefined ? maxValue : value;
const needsSigned = signed !== false && (actualMin < 0 || value < 0);
// 根据范围选择最小的整数类型
if (needsSigned) {
// 有符号整数
if (actualMin >= -128 && actualMax <= 127) {
return 'int8';
} else if (actualMin >= -32768 && actualMax <= 32767) {
return 'int16';
} else if (actualMin >= -2147483648 && actualMax <= 2147483647) {
return 'int32';
} else {
return 'float64'; // 超出int32范围使用双精度浮点
}
} else {
// 无符号整数
if (actualMax <= 255) {
return 'uint8';
} else if (actualMax <= 65535) {
return 'uint16';
} else if (actualMax <= 4294967295) {
return 'uint32';
} else {
return 'float64'; // 超出uint32范围使用双精度浮点
}
}
}
// 默认情况:检查是否为小数
if (!Number.isInteger(value)) {
return 'float32';
}
// 整数值,但没有指定范围,根据值的大小选择
if (value >= 0 && value <= 255) {
return 'uint8';
} else if (value >= -128 && value <= 127) {
return 'int8';
} else if (value >= 0 && value <= 65535) {
return 'uint16';
} else if (value >= -32768 && value <= 32767) {
return 'int16';
} else if (value >= 0 && value <= 4294967295) {
return 'uint32';
} else if (value >= -2147483648 && value <= 2147483647) {
return 'int32';
} else {
return 'float64';
}
}
/**
* 根据推断的类型名创建对应的TypedArray构造函数
*/
public static getTypedArrayConstructor(typeName: string): typeof Float32Array | typeof Float64Array | typeof Int32Array | typeof Uint32Array | typeof Int16Array | typeof Uint16Array | typeof Int8Array | typeof Uint8Array | typeof Uint8ClampedArray {
switch (typeName) {
case 'float32': return Float32Array;
case 'float64': return Float64Array;
case 'int32': return Int32Array;
case 'uint32': return Uint32Array;
case 'int16': return Int16Array;
case 'uint16': return Uint16Array;
case 'int8': return Int8Array;
case 'uint8': return Uint8Array;
case 'uint8clamped': return Uint8ClampedArray;
default: return Float32Array;
}
}
}
/**
* SoA存储器支持的TypedArray类型
*/
export type SupportedTypedArray =
| Float32Array
| Float64Array
| Int32Array
| Uint32Array
| Int16Array
| Uint16Array
| Int8Array
| Uint8Array
| Uint8ClampedArray;
/**
* SoA存储器需要装饰器启用
* 使用Structure of Arrays存储模式在大规模批量操作时提供优异性能
*/
export class SoAStorage<T extends Component> {
private static readonly _logger = createLogger('SoAStorage');
private fields = new Map<string, SupportedTypedArray>();
private stringFields = new Map<string, string[]>(); // 专门存储字符串
private serializedFields = new Map<string, string[]>(); // 序列化存储Map/Set/Array
private complexFields = new Map<number, Map<string, any>>(); // 存储复杂对象
private stringFields = new Map<string, string[]>();
private serializedFields = new Map<string, string[]>();
private complexFields = new Map<number, Map<string, unknown>>();
private entityToIndex = new Map<number, number>();
private indexToEntity: number[] = [];
private freeIndices: number[] = [];
@@ -346,6 +196,13 @@ export class SoAStorage<T extends Component> {
private _capacity = 1000;
public readonly type: ComponentType<T>;
// 缓存字段类型信息,避免重复创建实例
private fieldTypes = new Map<string, string>();
// 缓存装饰器元数据
private serializeMapFields: Set<string> = new Set();
private serializeSetFields: Set<string> = new Set();
private serializeArrayFields: Set<string> = new Set();
constructor(componentType: ComponentType<T>) {
this.type = componentType;
this.initializeFields(componentType);
@@ -353,88 +210,85 @@ export class SoAStorage<T extends Component> {
private initializeFields(componentType: ComponentType<T>): void {
const instance = new componentType();
const highPrecisionFields = (componentType as any).__highPrecisionFields || new Set();
const float64Fields = (componentType as any).__float64Fields || new Set();
const float32Fields = (componentType as any).__float32Fields || new Set();
const int32Fields = (componentType as any).__int32Fields || new Set();
const uint32Fields = (componentType as any).__uint32Fields || new Set();
const int16Fields = (componentType as any).__int16Fields || new Set();
const uint16Fields = (componentType as any).__uint16Fields || new Set();
const int8Fields = (componentType as any).__int8Fields || new Set();
const uint8Fields = (componentType as any).__uint8Fields || new Set();
const uint8ClampedFields = (componentType as any).__uint8ClampedFields || new Set();
const autoTypedFields = (componentType as any).__autoTypedFields || new Map();
const serializeMapFields = (componentType as any).__serializeMapFields || new Set();
const serializeSetFields = (componentType as any).__serializeSetFields || new Set();
const serializeArrayFields = (componentType as any).__serializeArrayFields || new Set();
// const deepCopyFields = (componentType as any).__deepCopyFields || new Set(); // 未使用但保留供future使用
const typeWithMeta = componentType as ComponentType<T> & {
__float64Fields?: Set<string>;
__float32Fields?: Set<string>;
__int32Fields?: Set<string>;
__uint32Fields?: Set<string>;
__int16Fields?: Set<string>;
__uint16Fields?: Set<string>;
__int8Fields?: Set<string>;
__uint8Fields?: Set<string>;
__uint8ClampedFields?: Set<string>;
__serializeMapFields?: Set<string>;
__serializeSetFields?: Set<string>;
__serializeArrayFields?: Set<string>;
};
for (const key in instance) {
if (instance.hasOwnProperty(key) && key !== 'id') {
const value = (instance as any)[key];
const type = typeof value;
const float64Fields = typeWithMeta.__float64Fields || new Set<string>();
const float32Fields = typeWithMeta.__float32Fields || new Set<string>();
const int32Fields = typeWithMeta.__int32Fields || new Set<string>();
const uint32Fields = typeWithMeta.__uint32Fields || new Set<string>();
const int16Fields = typeWithMeta.__int16Fields || new Set<string>();
const uint16Fields = typeWithMeta.__uint16Fields || new Set<string>();
const int8Fields = typeWithMeta.__int8Fields || new Set<string>();
const uint8Fields = typeWithMeta.__uint8Fields || new Set<string>();
const uint8ClampedFields = typeWithMeta.__uint8ClampedFields || new Set<string>();
if (type === 'number') {
if (highPrecisionFields.has(key)) {
// 标记为高精度,作为复杂对象处理
// 不添加到fields会在updateComponentAtIndex中自动添加到complexFields
} else if (autoTypedFields.has(key)) {
// 使用自动类型推断
const options = autoTypedFields.get(key);
const inferredType = TypeInference.inferOptimalType(value, options);
const ArrayConstructor = TypeInference.getTypedArrayConstructor(inferredType);
this.fields.set(key, new ArrayConstructor(this._capacity));
SoAStorage._logger.info(`字段 ${key} 自动推断为 ${inferredType} 类型,值: ${value}, 选项:`, options);
} else if (float64Fields.has(key)) {
// 使用Float64Array存储高精度浮点数
this.fields.set(key, new Float64Array(this._capacity));
} else if (int32Fields.has(key)) {
// 使用Int32Array存储32位有符号整数
this.fields.set(key, new Int32Array(this._capacity));
} else if (uint32Fields.has(key)) {
// 使用Uint32Array存储32位无符号整数
this.fields.set(key, new Uint32Array(this._capacity));
} else if (int16Fields.has(key)) {
// 使用Int16Array存储16位有符号整数
this.fields.set(key, new Int16Array(this._capacity));
} else if (uint16Fields.has(key)) {
// 使用Uint16Array存储16位无符号整数
this.fields.set(key, new Uint16Array(this._capacity));
} else if (int8Fields.has(key)) {
// 使用Int8Array存储8位有符号整数
this.fields.set(key, new Int8Array(this._capacity));
} else if (uint8Fields.has(key)) {
// 使用Uint8Array存储8位无符号整数
this.fields.set(key, new Uint8Array(this._capacity));
} else if (uint8ClampedFields.has(key)) {
// 使用Uint8ClampedArray存储8位夹紧无符号整数
this.fields.set(key, new Uint8ClampedArray(this._capacity));
} else if (float32Fields.has(key)) {
// 使用Float32Array存储32位浮点数
this.fields.set(key, new Float32Array(this._capacity));
} else {
// 默认使用Float32Array
this.fields.set(key, new Float32Array(this._capacity));
}
// 缓存装饰器元数据
this.serializeMapFields = typeWithMeta.__serializeMapFields || new Set<string>();
this.serializeSetFields = typeWithMeta.__serializeSetFields || new Set<string>();
this.serializeArrayFields = typeWithMeta.__serializeArrayFields || new Set<string>();
// 先收集所有有装饰器的字段,避免重复遍历
const decoratedFields = new Map<string, string>(); // fieldName -> arrayType
// 处理各类型装饰器标记的字段
for (const key of float64Fields) decoratedFields.set(key, 'float64');
for (const key of float32Fields) decoratedFields.set(key, 'float32');
for (const key of int32Fields) decoratedFields.set(key, 'int32');
for (const key of uint32Fields) decoratedFields.set(key, 'uint32');
for (const key of int16Fields) decoratedFields.set(key, 'int16');
for (const key of uint16Fields) decoratedFields.set(key, 'uint16');
for (const key of int8Fields) decoratedFields.set(key, 'int8');
for (const key of uint8Fields) decoratedFields.set(key, 'uint8');
for (const key of uint8ClampedFields) decoratedFields.set(key, 'uint8clamped');
// 只遍历实例自身的属性(不包括原型链),跳过 id
const instanceKeys = Object.keys(instance).filter(key => key !== 'id');
for (const key of instanceKeys) {
const value = (instance as Record<string, unknown>)[key];
const type = typeof value;
// 跳过函数(通常不会出现在 Object.keys 中,但以防万一)
if (type === 'function') continue;
// 检查装饰器类型
const decoratorType = decoratedFields.get(key);
const effectiveType = decoratorType ? 'number' : type;
this.fieldTypes.set(key, effectiveType);
if (decoratorType) {
// 有装饰器标记的数字字段
const ArrayConstructor = SoATypeRegistry.getConstructor(decoratorType as TypedArrayTypeName);
this.fields.set(key, new ArrayConstructor(this._capacity));
} else if (type === 'number') {
// 无装饰器的数字字段,默认使用 Float32Array
this.fields.set(key, new Float32Array(this._capacity));
} else if (type === 'boolean') {
// 布尔值默认使用Uint8Array存储为0/1(更节省内存)
if (uint8Fields.has(key) || (!float32Fields.has(key) && !float64Fields.has(key))) {
this.fields.set(key, new Uint8Array(this._capacity));
} else {
// 兼容性:如果显式指定浮点类型则使用原有方式
this.fields.set(key, new Float32Array(this._capacity));
}
// 布尔值使用 Uint8Array 存储为 0/1
this.fields.set(key, new Uint8Array(this._capacity));
} else if (type === 'string') {
// 字符串专门处理
this.stringFields.set(key, new Array(this._capacity));
} else if (type === 'object' && value !== null) {
// 处理集合类型
if (serializeMapFields.has(key) || serializeSetFields.has(key) || serializeArrayFields.has(key)) {
if (this.serializeMapFields.has(key) || this.serializeSetFields.has(key) || this.serializeArrayFields.has(key)) {
// 序列化存储
this.serializedFields.set(key, new Array(this._capacity));
}
// 其他对象类型会在updateComponentAtIndex中作为复杂对象处理
}
}
}
}
@@ -497,12 +351,16 @@ export class SoAStorage<T extends Component> {
} else if (this.serializedFields.has(key)) {
// 序列化字段处理
const serializedArray = this.serializedFields.get(key)!;
serializedArray[index] = this.serializeValue(value, key, serializeMapFields, serializeSetFields, serializeArrayFields);
serializedArray[index] = SoASerializer.serialize(value, key, {
isMap: serializeMapFields.has(key),
isSet: serializeSetFields.has(key),
isArray: serializeArrayFields.has(key)
});
} else {
// 复杂字段单独存储
if (deepCopyFields.has(key)) {
// 深拷贝处理
complexFieldMap.set(key, this.deepClone(value));
complexFieldMap.set(key, SoASerializer.deepClone(value));
} else {
complexFieldMap.set(key, value);
}
@@ -516,96 +374,6 @@ export class SoAStorage<T extends Component> {
}
}
/**
* 序列化值为JSON字符串
*/
private serializeValue(value: any, key: string, mapFields: Set<string>, setFields: Set<string>, arrayFields: Set<string>): string {
try {
if (mapFields.has(key) && value instanceof Map) {
// Map序列化为数组形式
return JSON.stringify(Array.from(value.entries()));
} else if (setFields.has(key) && value instanceof Set) {
// Set序列化为数组形式
return JSON.stringify(Array.from(value));
} else if (arrayFields.has(key) && Array.isArray(value)) {
// Array直接序列化
return JSON.stringify(value);
} else {
// 其他对象直接序列化
return JSON.stringify(value);
}
} catch (error) {
SoAStorage._logger.warn(`SoA序列化字段 ${key} 失败:`, error);
return '{}';
}
}
/**
* 反序列化JSON字符串为值
*/
private deserializeValue(serialized: string, key: string, mapFields: Set<string>, setFields: Set<string>, arrayFields: Set<string>): any {
try {
const parsed = JSON.parse(serialized);
if (mapFields.has(key)) {
// 恢复Map
return new Map(parsed);
} else if (setFields.has(key)) {
// 恢复Set
return new Set(parsed);
} else if (arrayFields.has(key)) {
// 恢复Array
return parsed;
} else {
return parsed;
}
} catch (error) {
SoAStorage._logger.warn(`SoA反序列化字段 ${key} 失败:`, error);
return null;
}
}
/**
* 深拷贝对象
*/
private deepClone(obj: any): any {
if (obj === null || typeof obj !== 'object') {
return obj;
}
if (obj instanceof Date) {
return new Date(obj.getTime());
}
if (obj instanceof Array) {
return obj.map((item) => this.deepClone(item));
}
if (obj instanceof Map) {
const cloned = new Map();
for (const [key, value] of obj.entries()) {
cloned.set(key, this.deepClone(value));
}
return cloned;
}
if (obj instanceof Set) {
const cloned = new Set();
for (const value of obj.values()) {
cloned.add(this.deepClone(value));
}
return cloned;
}
// 普通对象
const cloned: any = {};
for (const key in obj) {
if (obj.hasOwnProperty(key)) {
cloned[key] = this.deepClone(obj[key]);
}
}
return cloned;
}
public getComponent(entityId: number): T | null {
const index = this.entityToIndex.get(entityId);
@@ -613,11 +381,162 @@ export class SoAStorage<T extends Component> {
return null;
}
// 创建真正的组件实例以保持兼容性
// 返回 Proxy直接操作底层 TypedArray
return this.createProxyView(entityId, index);
}
/**
* 创建组件的 Proxy 视图
* 读写操作直接映射到底层 TypedArray无数据复制
*/
private createProxyView(entityId: number, index: number): T {
// eslint-disable-next-line @typescript-eslint/no-this-alias
const self = this;
// Proxy handler 类型定义
const handler: ProxyHandler<Record<string, unknown>> = {
get(_, prop: string | symbol) {
const propStr = String(prop);
// TypedArray 字段
const array = self.fields.get(propStr);
if (array) {
const fieldType = self.getFieldType(propStr);
if (fieldType === 'boolean') {
return array[index] === 1;
}
return array[index];
}
// 字符串字段
const stringArray = self.stringFields.get(propStr);
if (stringArray) {
return stringArray[index];
}
// 序列化字段
const serializedArray = self.serializedFields.get(propStr);
if (serializedArray) {
const serialized = serializedArray[index];
if (serialized) {
return SoASerializer.deserialize(serialized, propStr, {
isMap: self.serializeMapFields.has(propStr),
isSet: self.serializeSetFields.has(propStr),
isArray: self.serializeArrayFields.has(propStr)
});
}
return undefined;
}
// 复杂字段
const complexFieldMap = self.complexFields.get(entityId);
if (complexFieldMap?.has(propStr)) {
return complexFieldMap.get(propStr);
}
return undefined;
},
set(_, prop: string | symbol, value) {
const propStr = String(prop);
// entityId 是只读的
if (propStr === 'entityId') {
return false;
}
// TypedArray 字段
const array = self.fields.get(propStr);
if (array) {
const fieldType = self.getFieldType(propStr);
if (fieldType === 'boolean') {
array[index] = value ? 1 : 0;
} else {
array[index] = value;
}
return true;
}
// 字符串字段
const stringArray = self.stringFields.get(propStr);
if (stringArray) {
stringArray[index] = String(value);
return true;
}
// 序列化字段
if (self.serializedFields.has(propStr)) {
const serializedArray = self.serializedFields.get(propStr)!;
serializedArray[index] = SoASerializer.serialize(value, propStr, {
isMap: self.serializeMapFields.has(propStr),
isSet: self.serializeSetFields.has(propStr),
isArray: self.serializeArrayFields.has(propStr)
});
return true;
}
// 复杂字段
let complexFieldMap = self.complexFields.get(entityId);
if (!complexFieldMap) {
complexFieldMap = new Map();
self.complexFields.set(entityId, complexFieldMap);
}
complexFieldMap.set(propStr, value);
return true;
},
has(_, prop) {
const propStr = String(prop);
return self.fields.has(propStr) ||
self.stringFields.has(propStr) ||
self.serializedFields.has(propStr) ||
self.complexFields.get(entityId)?.has(propStr) || false;
},
ownKeys() {
const keys: string[] = [];
for (const key of self.fields.keys()) keys.push(key);
for (const key of self.stringFields.keys()) keys.push(key);
for (const key of self.serializedFields.keys()) keys.push(key);
const complexFieldMap = self.complexFields.get(entityId);
if (complexFieldMap) {
for (const key of complexFieldMap.keys()) keys.push(key);
}
return keys;
},
getOwnPropertyDescriptor(_, prop) {
const propStr = String(prop);
if (self.fields.has(propStr) ||
self.stringFields.has(propStr) ||
self.serializedFields.has(propStr) ||
self.complexFields.get(entityId)?.has(propStr)) {
return {
enumerable: true,
configurable: true,
// entityId 是只读的
writable: propStr !== 'entityId',
};
}
return undefined;
}
};
return new Proxy({} as Record<string, unknown>, handler) as unknown as T;
}
/**
* 获取组件的快照副本(用于序列化等需要独立副本的场景)
*/
public getComponentSnapshot(entityId: number): T | null {
const index = this.entityToIndex.get(entityId);
if (index === undefined) {
return null;
}
// 需要 any 因为要动态写入泛型 T 的属性
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const component = new this.type() as any;
const serializeMapFields = (this.type as any).__serializeMapFields || new Set();
const serializeSetFields = (this.type as any).__serializeSetFields || new Set();
const serializeArrayFields = (this.type as any).__serializeArrayFields || new Set();
// 恢复数值字段
for (const [fieldName, array] of this.fields.entries()) {
@@ -640,7 +559,11 @@ export class SoAStorage<T extends Component> {
for (const [fieldName, serializedArray] of this.serializedFields.entries()) {
const serialized = serializedArray[index];
if (serialized) {
component[fieldName] = this.deserializeValue(serialized, fieldName, serializeMapFields, serializeSetFields, serializeArrayFields);
component[fieldName] = SoASerializer.deserialize(serialized, fieldName, {
isMap: this.serializeMapFields.has(fieldName),
isSet: this.serializeSetFields.has(fieldName),
isArray: this.serializeArrayFields.has(fieldName)
});
}
}
@@ -656,10 +579,8 @@ export class SoAStorage<T extends Component> {
}
private getFieldType(fieldName: string): string {
// 通过创建临时实例检查字段类型
const tempInstance = new this.type();
const value = (tempInstance as any)[fieldName];
return typeof value;
// 使用缓存的字段类型
return this.fieldTypes.get(fieldName) || 'unknown';
}
public hasComponent(entityId: number): boolean {
@@ -687,32 +608,7 @@ export class SoAStorage<T extends Component> {
private resize(newCapacity: number): void {
// 调整数值字段的TypedArray
for (const [fieldName, oldArray] of this.fields.entries()) {
let newArray: SupportedTypedArray;
if (oldArray instanceof Float32Array) {
newArray = new Float32Array(newCapacity);
} else if (oldArray instanceof Float64Array) {
newArray = new Float64Array(newCapacity);
} else if (oldArray instanceof Int32Array) {
newArray = new Int32Array(newCapacity);
} else if (oldArray instanceof Uint32Array) {
newArray = new Uint32Array(newCapacity);
} else if (oldArray instanceof Int16Array) {
newArray = new Int16Array(newCapacity);
} else if (oldArray instanceof Uint16Array) {
newArray = new Uint16Array(newCapacity);
} else if (oldArray instanceof Int8Array) {
newArray = new Int8Array(newCapacity);
} else if (oldArray instanceof Uint8Array) {
newArray = new Uint8Array(newCapacity);
} else if (oldArray instanceof Uint8ClampedArray) {
newArray = new Uint8ClampedArray(newCapacity);
} else {
// 默认回退到Float32Array
newArray = new Float32Array(newCapacity);
SoAStorage._logger.warn(`未知的TypedArray类型用于字段 ${fieldName}回退到Float32Array`);
}
const newArray = SoATypeRegistry.createSameType(oldArray, newCapacity);
newArray.set(oldArray);
this.fields.set(fieldName, newArray);
}
@@ -849,42 +745,8 @@ export class SoAStorage<T extends Component> {
const fieldStats = new Map<string, any>();
for (const [fieldName, array] of this.fields.entries()) {
let bytesPerElement: number;
let typeName: string;
if (array instanceof Float32Array) {
bytesPerElement = 4;
typeName = 'float32';
} else if (array instanceof Float64Array) {
bytesPerElement = 8;
typeName = 'float64';
} else if (array instanceof Int32Array) {
bytesPerElement = 4;
typeName = 'int32';
} else if (array instanceof Uint32Array) {
bytesPerElement = 4;
typeName = 'uint32';
} else if (array instanceof Int16Array) {
bytesPerElement = 2;
typeName = 'int16';
} else if (array instanceof Uint16Array) {
bytesPerElement = 2;
typeName = 'uint16';
} else if (array instanceof Int8Array) {
bytesPerElement = 1;
typeName = 'int8';
} else if (array instanceof Uint8Array) {
bytesPerElement = 1;
typeName = 'uint8';
} else if (array instanceof Uint8ClampedArray) {
bytesPerElement = 1;
typeName = 'uint8clamped';
} else {
// 默认回退
bytesPerElement = 4;
typeName = 'unknown';
}
const typeName = SoATypeRegistry.getTypeName(array);
const bytesPerElement = SoATypeRegistry.getBytesPerElement(typeName);
const memory = array.length * bytesPerElement;
totalMemory += memory;
@@ -914,4 +776,5 @@ export class SoAStorage<T extends Component> {
const activeIndices = this.getActiveIndices();
operation(this.fields, activeIndices);
}
}

View File

@@ -0,0 +1,208 @@
/**
* SoA存储器支持的TypedArray类型
*/
export type SupportedTypedArray =
| Float32Array
| Float64Array
| Int32Array
| Uint32Array
| Int16Array
| Uint16Array
| Int8Array
| Uint8Array
| Uint8ClampedArray;
export type TypedArrayConstructor =
| typeof Float32Array
| typeof Float64Array
| typeof Int32Array
| typeof Uint32Array
| typeof Int16Array
| typeof Uint16Array
| typeof Int8Array
| typeof Uint8Array
| typeof Uint8ClampedArray;
/**
* TypedArray 类型名称
*/
export type TypedArrayTypeName =
| 'float32'
| 'float64'
| 'int32'
| 'uint32'
| 'int16'
| 'uint16'
| 'int8'
| 'uint8'
| 'uint8clamped';
/**
* 字段元数据
*/
export interface FieldMetadata {
name: string;
type: 'number' | 'boolean' | 'string' | 'object';
arrayType?: TypedArrayTypeName;
isSerializedMap?: boolean;
isSerializedSet?: boolean;
isSerializedArray?: boolean;
isDeepCopy?: boolean;
}
/**
* SoA 类型注册器
* 负责类型推断、TypedArray 创建和元数据管理
*/
export class SoATypeRegistry {
private static readonly TYPE_CONSTRUCTORS: Record<TypedArrayTypeName, TypedArrayConstructor> = {
float32: Float32Array,
float64: Float64Array,
int32: Int32Array,
uint32: Uint32Array,
int16: Int16Array,
uint16: Uint16Array,
int8: Int8Array,
uint8: Uint8Array,
uint8clamped: Uint8ClampedArray
};
private static readonly TYPE_BYTES: Record<TypedArrayTypeName, number> = {
float32: 4,
float64: 8,
int32: 4,
uint32: 4,
int16: 2,
uint16: 2,
int8: 1,
uint8: 1,
uint8clamped: 1
};
/**
* 获取 TypedArray 构造函数
*/
public static getConstructor(typeName: TypedArrayTypeName): TypedArrayConstructor {
return this.TYPE_CONSTRUCTORS[typeName] || Float32Array;
}
/**
* 获取每个元素的字节数
*/
public static getBytesPerElement(typeName: TypedArrayTypeName): number {
return this.TYPE_BYTES[typeName] || 4;
}
/**
* 从 TypedArray 实例获取类型名称
*/
public static getTypeName(array: SupportedTypedArray): TypedArrayTypeName {
if (array instanceof Float32Array) return 'float32';
if (array instanceof Float64Array) return 'float64';
if (array instanceof Int32Array) return 'int32';
if (array instanceof Uint32Array) return 'uint32';
if (array instanceof Int16Array) return 'int16';
if (array instanceof Uint16Array) return 'uint16';
if (array instanceof Int8Array) return 'int8';
if (array instanceof Uint8Array) return 'uint8';
if (array instanceof Uint8ClampedArray) return 'uint8clamped';
return 'float32';
}
/**
* 创建新的 TypedArray与原数组同类型
*/
public static createSameType(source: SupportedTypedArray, capacity: number): SupportedTypedArray {
const typeName = this.getTypeName(source);
const Constructor = this.getConstructor(typeName);
return new Constructor(capacity);
}
/**
* 从组件类型提取字段元数据
*/
public static extractFieldMetadata<T>(
componentType: new () => T
): Map<string, FieldMetadata> {
const instance = new componentType();
const metadata = new Map<string, FieldMetadata>();
const typeWithMeta = componentType as typeof componentType & {
__float64Fields?: Set<string>;
__float32Fields?: Set<string>;
__int32Fields?: Set<string>;
__uint32Fields?: Set<string>;
__int16Fields?: Set<string>;
__uint16Fields?: Set<string>;
__int8Fields?: Set<string>;
__uint8Fields?: Set<string>;
__uint8ClampedFields?: Set<string>;
__serializeMapFields?: Set<string>;
__serializeSetFields?: Set<string>;
__serializeArrayFields?: Set<string>;
__deepCopyFields?: Set<string>;
};
// 收集装饰器标记
const decoratorMap = new Map<string, TypedArrayTypeName>();
const addDecorators = (fields: Set<string> | undefined, type: TypedArrayTypeName) => {
if (fields) {
for (const key of fields) decoratorMap.set(key, type);
}
};
addDecorators(typeWithMeta.__float64Fields, 'float64');
addDecorators(typeWithMeta.__float32Fields, 'float32');
addDecorators(typeWithMeta.__int32Fields, 'int32');
addDecorators(typeWithMeta.__uint32Fields, 'uint32');
addDecorators(typeWithMeta.__int16Fields, 'int16');
addDecorators(typeWithMeta.__uint16Fields, 'uint16');
addDecorators(typeWithMeta.__int8Fields, 'int8');
addDecorators(typeWithMeta.__uint8Fields, 'uint8');
addDecorators(typeWithMeta.__uint8ClampedFields, 'uint8clamped');
// 遍历实例属性
const instanceKeys = Object.keys(instance as object).filter((key) => key !== 'id');
for (const key of instanceKeys) {
const value = (instance as Record<string, unknown>)[key];
const type = typeof value;
if (type === 'function') continue;
const fieldMeta: FieldMetadata = {
name: key,
type: type as 'number' | 'boolean' | 'string' | 'object'
};
const decoratorType = decoratorMap.get(key);
if (decoratorType) {
fieldMeta.arrayType = decoratorType;
} else if (type === 'number') {
fieldMeta.arrayType = 'float32';
} else if (type === 'boolean') {
fieldMeta.arrayType = 'uint8';
}
// 序列化标记
if (typeWithMeta.__serializeMapFields?.has(key)) {
fieldMeta.isSerializedMap = true;
}
if (typeWithMeta.__serializeSetFields?.has(key)) {
fieldMeta.isSerializedSet = true;
}
if (typeWithMeta.__serializeArrayFields?.has(key)) {
fieldMeta.isSerializedArray = true;
}
if (typeWithMeta.__deepCopyFields?.has(key)) {
fieldMeta.isDeepCopy = true;
}
metadata.set(key, fieldMeta);
}
return metadata;
}
}

View File

@@ -1,3 +1,3 @@
export { ComponentPool, ComponentPoolManager } from '../ComponentPool';
export { ComponentStorage, ComponentRegistry } from '../ComponentStorage';
export { EnableSoA, HighPrecision, Float64, Float32, Int32, SerializeMap, SoAStorage } from '../SoAStorage';
export { EnableSoA, Float64, Float32, Int32, SerializeMap, SoAStorage } from '../SoAStorage';

View File

@@ -23,7 +23,6 @@ export {
EnableSoA,
// 数值类型装饰器
HighPrecision,
Float64,
Float32,
Int32,
@@ -34,10 +33,6 @@ export {
Uint8,
Uint8Clamped,
// 自动类型推断
AutoTyped,
TypeInference,
// 序列化装饰器
SerializeMap,
SerializeSet,

View File

@@ -0,0 +1,183 @@
import { SoASerializer } from '../../../src/ECS/Core/SoASerializer';
describe('SoASerializer', () => {
describe('serialize', () => {
test('should serialize Map to JSON string', () => {
const map = new Map([['key1', 'value1'], ['key2', 'value2']]);
const result = SoASerializer.serialize(map, 'testMap', { isMap: true });
expect(result).toBe('[["key1","value1"],["key2","value2"]]');
});
test('should serialize Set to JSON string', () => {
const set = new Set([1, 2, 3]);
const result = SoASerializer.serialize(set, 'testSet', { isSet: true });
expect(result).toBe('[1,2,3]');
});
test('should serialize Array to JSON string', () => {
const arr = [1, 2, 3];
const result = SoASerializer.serialize(arr, 'testArray', { isArray: true });
expect(result).toBe('[1,2,3]');
});
test('should serialize plain object to JSON string', () => {
const obj = { a: 1, b: 'test' };
const result = SoASerializer.serialize(obj, 'testObj');
expect(result).toBe('{"a":1,"b":"test"}');
});
test('should serialize primitive values', () => {
expect(SoASerializer.serialize(42, 'num')).toBe('42');
expect(SoASerializer.serialize('hello', 'str')).toBe('"hello"');
expect(SoASerializer.serialize(true, 'bool')).toBe('true');
expect(SoASerializer.serialize(null, 'null')).toBe('null');
});
test('should return empty object on serialization error', () => {
const circular: Record<string, unknown> = {};
circular.self = circular;
const result = SoASerializer.serialize(circular, 'circular');
expect(result).toBe('{}');
});
});
describe('deserialize', () => {
test('should deserialize JSON string to Map', () => {
const json = '[["key1","value1"],["key2","value2"]]';
const result = SoASerializer.deserialize(json, 'testMap', { isMap: true });
expect(result).toBeInstanceOf(Map);
expect((result as Map<string, string>).get('key1')).toBe('value1');
expect((result as Map<string, string>).get('key2')).toBe('value2');
});
test('should deserialize JSON string to Set', () => {
const json = '[1,2,3]';
const result = SoASerializer.deserialize(json, 'testSet', { isSet: true });
expect(result).toBeInstanceOf(Set);
expect((result as Set<number>).has(1)).toBe(true);
expect((result as Set<number>).has(2)).toBe(true);
expect((result as Set<number>).has(3)).toBe(true);
});
test('should deserialize JSON string to Array', () => {
const json = '[1,2,3]';
const result = SoASerializer.deserialize(json, 'testArray', { isArray: true });
expect(result).toEqual([1, 2, 3]);
});
test('should deserialize JSON string to object', () => {
const json = '{"a":1,"b":"test"}';
const result = SoASerializer.deserialize(json, 'testObj');
expect(result).toEqual({ a: 1, b: 'test' });
});
test('should deserialize primitive values', () => {
expect(SoASerializer.deserialize('42', 'num')).toBe(42);
expect(SoASerializer.deserialize('"hello"', 'str')).toBe('hello');
expect(SoASerializer.deserialize('true', 'bool')).toBe(true);
expect(SoASerializer.deserialize('null', 'null')).toBe(null);
});
test('should return null on deserialization error', () => {
const result = SoASerializer.deserialize('invalid json', 'field');
expect(result).toBe(null);
});
});
describe('deepClone', () => {
test('should return primitive values as-is', () => {
expect(SoASerializer.deepClone(42)).toBe(42);
expect(SoASerializer.deepClone('hello')).toBe('hello');
expect(SoASerializer.deepClone(true)).toBe(true);
expect(SoASerializer.deepClone(null)).toBe(null);
expect(SoASerializer.deepClone(undefined)).toBe(undefined);
});
test('should clone Date objects', () => {
const date = new Date('2023-01-01');
const cloned = SoASerializer.deepClone(date);
expect(cloned).toBeInstanceOf(Date);
expect(cloned.getTime()).toBe(date.getTime());
expect(cloned).not.toBe(date);
});
test('should clone arrays deeply', () => {
const arr = [1, [2, 3], { a: 4 }];
const cloned = SoASerializer.deepClone(arr);
expect(cloned).toEqual(arr);
expect(cloned).not.toBe(arr);
expect(cloned[1]).not.toBe(arr[1]);
expect(cloned[2]).not.toBe(arr[2]);
});
test('should clone Map objects deeply', () => {
const map = new Map([
['key1', { value: 1 }],
['key2', { value: 2 }]
]);
const cloned = SoASerializer.deepClone(map);
expect(cloned).toBeInstanceOf(Map);
expect(cloned.size).toBe(2);
expect(cloned.get('key1')).toEqual({ value: 1 });
expect(cloned.get('key1')).not.toBe(map.get('key1'));
});
test('should clone Set objects deeply', () => {
const obj1 = { a: 1 };
const obj2 = { b: 2 };
const set = new Set([obj1, obj2]);
const cloned = SoASerializer.deepClone(set);
expect(cloned).toBeInstanceOf(Set);
expect(cloned.size).toBe(2);
const clonedArray = Array.from(cloned);
expect(clonedArray[0]).toEqual(obj1);
expect(clonedArray[0]).not.toBe(obj1);
});
test('should clone nested objects deeply', () => {
const obj = {
a: 1,
b: {
c: 2,
d: {
e: 3
}
}
};
const cloned = SoASerializer.deepClone(obj);
expect(cloned).toEqual(obj);
expect(cloned).not.toBe(obj);
expect(cloned.b).not.toBe(obj.b);
expect(cloned.b.d).not.toBe(obj.b.d);
});
test('should clone complex nested structures', () => {
const complex = {
array: [1, 2, 3],
map: new Map([['a', 1]]),
set: new Set([1, 2]),
date: new Date('2023-01-01'),
nested: {
value: 'test'
}
};
const cloned = SoASerializer.deepClone(complex);
expect(cloned.array).toEqual(complex.array);
expect(cloned.array).not.toBe(complex.array);
expect(cloned.map).toBeInstanceOf(Map);
expect(cloned.map.get('a')).toBe(1);
expect(cloned.set).toBeInstanceOf(Set);
expect(cloned.set.has(1)).toBe(true);
expect(cloned.date).toBeInstanceOf(Date);
expect(cloned.date.getTime()).toBe(complex.date.getTime());
expect(cloned.nested).toEqual(complex.nested);
expect(cloned.nested).not.toBe(complex.nested);
});
});
});

View File

@@ -2,7 +2,6 @@ import { Component } from '../../../src/ECS/Component';
import { ComponentStorageManager } from '../../../src/ECS/Core/ComponentStorage';
import {
EnableSoA,
HighPrecision,
Float64,
Int32,
SerializeMap,
@@ -50,7 +49,7 @@ class BasicTypesComponent extends Component {
class DecoratedNumberComponent extends Component {
public normalFloat: number;
@HighPrecision
@Float64
public highPrecisionNumber: number;
@Float64
@@ -143,7 +142,7 @@ class ComplexObjectComponent extends Component {
@EnableSoA
class MixedComponent extends Component {
@HighPrecision
@Float64
public bigIntId: number;
@Float64
@@ -288,7 +287,7 @@ describe('SoAStorage - SoA存储测试', () => {
});
describe('数值类型装饰器', () => {
test('@HighPrecision应该保持高精度数值', () => {
test('@Float64应该保持高精度数值', () => {
const component = new DecoratedNumberComponent(
0,
Number.MAX_SAFE_INTEGER
@@ -336,7 +335,7 @@ describe('SoAStorage - SoA存储测试', () => {
expect(storage.getFieldArray('normalFloat')).toBeInstanceOf(Float32Array);
expect(storage.getFieldArray('preciseFloat')).toBeInstanceOf(Float64Array);
expect(storage.getFieldArray('integerValue')).toBeInstanceOf(Int32Array);
expect(storage.getFieldArray('highPrecisionNumber')).toBeNull();
expect(storage.getFieldArray('highPrecisionNumber')).toBeInstanceOf(Float64Array);
});
});

View File

@@ -0,0 +1,233 @@
import { Component } from '../../../src/ECS/Component';
import {
SoATypeRegistry,
TypedArrayTypeName
} from '../../../src/ECS/Core/SoATypeRegistry';
// Test components
class SimpleComponent extends Component {
public value: number = 0;
public flag: boolean = false;
public name: string = '';
}
describe('SoATypeRegistry', () => {
describe('getConstructor', () => {
test('should return Float32Array constructor for float32', () => {
expect(SoATypeRegistry.getConstructor('float32')).toBe(Float32Array);
});
test('should return Float64Array constructor for float64', () => {
expect(SoATypeRegistry.getConstructor('float64')).toBe(Float64Array);
});
test('should return Int32Array constructor for int32', () => {
expect(SoATypeRegistry.getConstructor('int32')).toBe(Int32Array);
});
test('should return Uint32Array constructor for uint32', () => {
expect(SoATypeRegistry.getConstructor('uint32')).toBe(Uint32Array);
});
test('should return Int16Array constructor for int16', () => {
expect(SoATypeRegistry.getConstructor('int16')).toBe(Int16Array);
});
test('should return Uint16Array constructor for uint16', () => {
expect(SoATypeRegistry.getConstructor('uint16')).toBe(Uint16Array);
});
test('should return Int8Array constructor for int8', () => {
expect(SoATypeRegistry.getConstructor('int8')).toBe(Int8Array);
});
test('should return Uint8Array constructor for uint8', () => {
expect(SoATypeRegistry.getConstructor('uint8')).toBe(Uint8Array);
});
test('should return Uint8ClampedArray constructor for uint8clamped', () => {
expect(SoATypeRegistry.getConstructor('uint8clamped')).toBe(Uint8ClampedArray);
});
test('should return Float32Array as default for unknown type', () => {
expect(SoATypeRegistry.getConstructor('unknown' as TypedArrayTypeName)).toBe(Float32Array);
});
});
describe('getBytesPerElement', () => {
test('should return 4 for float32', () => {
expect(SoATypeRegistry.getBytesPerElement('float32')).toBe(4);
});
test('should return 8 for float64', () => {
expect(SoATypeRegistry.getBytesPerElement('float64')).toBe(8);
});
test('should return 4 for int32', () => {
expect(SoATypeRegistry.getBytesPerElement('int32')).toBe(4);
});
test('should return 4 for uint32', () => {
expect(SoATypeRegistry.getBytesPerElement('uint32')).toBe(4);
});
test('should return 2 for int16', () => {
expect(SoATypeRegistry.getBytesPerElement('int16')).toBe(2);
});
test('should return 2 for uint16', () => {
expect(SoATypeRegistry.getBytesPerElement('uint16')).toBe(2);
});
test('should return 1 for int8', () => {
expect(SoATypeRegistry.getBytesPerElement('int8')).toBe(1);
});
test('should return 1 for uint8', () => {
expect(SoATypeRegistry.getBytesPerElement('uint8')).toBe(1);
});
test('should return 1 for uint8clamped', () => {
expect(SoATypeRegistry.getBytesPerElement('uint8clamped')).toBe(1);
});
test('should return 4 as default for unknown type', () => {
expect(SoATypeRegistry.getBytesPerElement('unknown' as TypedArrayTypeName)).toBe(4);
});
});
describe('getTypeName', () => {
test('should return float32 for Float32Array', () => {
expect(SoATypeRegistry.getTypeName(new Float32Array(1))).toBe('float32');
});
test('should return float64 for Float64Array', () => {
expect(SoATypeRegistry.getTypeName(new Float64Array(1))).toBe('float64');
});
test('should return int32 for Int32Array', () => {
expect(SoATypeRegistry.getTypeName(new Int32Array(1))).toBe('int32');
});
test('should return uint32 for Uint32Array', () => {
expect(SoATypeRegistry.getTypeName(new Uint32Array(1))).toBe('uint32');
});
test('should return int16 for Int16Array', () => {
expect(SoATypeRegistry.getTypeName(new Int16Array(1))).toBe('int16');
});
test('should return uint16 for Uint16Array', () => {
expect(SoATypeRegistry.getTypeName(new Uint16Array(1))).toBe('uint16');
});
test('should return int8 for Int8Array', () => {
expect(SoATypeRegistry.getTypeName(new Int8Array(1))).toBe('int8');
});
test('should return uint8 for Uint8Array', () => {
expect(SoATypeRegistry.getTypeName(new Uint8Array(1))).toBe('uint8');
});
test('should return uint8clamped for Uint8ClampedArray', () => {
expect(SoATypeRegistry.getTypeName(new Uint8ClampedArray(1))).toBe('uint8clamped');
});
});
describe('createSameType', () => {
test('should create Float32Array from Float32Array source', () => {
const source = new Float32Array(10);
const result = SoATypeRegistry.createSameType(source, 20);
expect(result).toBeInstanceOf(Float32Array);
expect(result.length).toBe(20);
});
test('should create Float64Array from Float64Array source', () => {
const source = new Float64Array(10);
const result = SoATypeRegistry.createSameType(source, 20);
expect(result).toBeInstanceOf(Float64Array);
expect(result.length).toBe(20);
});
test('should create Int32Array from Int32Array source', () => {
const source = new Int32Array(10);
const result = SoATypeRegistry.createSameType(source, 20);
expect(result).toBeInstanceOf(Int32Array);
expect(result.length).toBe(20);
});
test('should create Uint32Array from Uint32Array source', () => {
const source = new Uint32Array(10);
const result = SoATypeRegistry.createSameType(source, 20);
expect(result).toBeInstanceOf(Uint32Array);
expect(result.length).toBe(20);
});
test('should create Int16Array from Int16Array source', () => {
const source = new Int16Array(10);
const result = SoATypeRegistry.createSameType(source, 15);
expect(result).toBeInstanceOf(Int16Array);
expect(result.length).toBe(15);
});
test('should create Uint16Array from Uint16Array source', () => {
const source = new Uint16Array(10);
const result = SoATypeRegistry.createSameType(source, 15);
expect(result).toBeInstanceOf(Uint16Array);
expect(result.length).toBe(15);
});
test('should create Int8Array from Int8Array source', () => {
const source = new Int8Array(10);
const result = SoATypeRegistry.createSameType(source, 15);
expect(result).toBeInstanceOf(Int8Array);
expect(result.length).toBe(15);
});
test('should create Uint8Array from Uint8Array source', () => {
const source = new Uint8Array(10);
const result = SoATypeRegistry.createSameType(source, 15);
expect(result).toBeInstanceOf(Uint8Array);
expect(result.length).toBe(15);
});
test('should create Uint8ClampedArray from Uint8ClampedArray source', () => {
const source = new Uint8ClampedArray(10);
const result = SoATypeRegistry.createSameType(source, 15);
expect(result).toBeInstanceOf(Uint8ClampedArray);
expect(result.length).toBe(15);
});
});
describe('extractFieldMetadata', () => {
test('should extract metadata for simple component', () => {
const metadata = SoATypeRegistry.extractFieldMetadata(SimpleComponent);
expect(metadata.has('value')).toBe(true);
expect(metadata.get('value')?.type).toBe('number');
expect(metadata.get('value')?.arrayType).toBe('float32');
expect(metadata.has('flag')).toBe(true);
expect(metadata.get('flag')?.type).toBe('boolean');
expect(metadata.get('flag')?.arrayType).toBe('uint8');
expect(metadata.has('name')).toBe(true);
expect(metadata.get('name')?.type).toBe('string');
});
test('should not include id field in metadata', () => {
const metadata = SoATypeRegistry.extractFieldMetadata(SimpleComponent);
expect(metadata.has('id')).toBe(false);
});
test('should handle component with object fields', () => {
class ObjectComponent extends Component {
public data: object = {};
}
const metadata = SoATypeRegistry.extractFieldMetadata(ObjectComponent);
expect(metadata.has('data')).toBe(true);
expect(metadata.get('data')?.type).toBe('object');
});
});
});