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

@@ -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');
});
});
});