feat: 集成Rust WASM渲染引擎与TypeScript ECS框架 (#228)
* feat: 集成Rust WASM渲染引擎与TypeScript ECS框架 * feat: 增强编辑器UI功能与跨平台支持 * fix: 修复CI测试和类型检查问题 * fix: 修复CI问题并提高测试覆盖率 * fix: 修复CI问题并提高测试覆盖率
This commit is contained in:
183
packages/core/tests/ECS/Core/SoASerializer.test.ts
Normal file
183
packages/core/tests/ECS/Core/SoASerializer.test.ts
Normal 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);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
233
packages/core/tests/ECS/Core/SoATypeRegistry.test.ts
Normal file
233
packages/core/tests/ECS/Core/SoATypeRegistry.test.ts
Normal 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');
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user