import { describe, it, expect } from 'vitest'; import { applyAlign, shuffle, type Region, type RegionAxis } from '../../src/core/region'; import { createRNG } from '../../src/utils/rng'; import { createEntityCollection } from '../../src/utils/entity'; import { type Part } from '../../src/core/part'; describe('Region', () => { function createPart(id: string, position: number[]): Part { const collection = createEntityCollection(); const part: Part = { id, sides: 1, side: 0, region: { id: 'region1', value: {} as Region }, position: [...position] }; collection.add(part); return part; } function createRegion(axes: RegionAxis[], parts: Part[]): Region { const region: Region = { id: 'region1', axes: [...axes], children: parts.map(p => ({ id: p.id, value: p })) }; return region; } describe('applyAlign', () => { it('should do nothing with empty region', () => { const region = createRegion([{ name: 'x', min: 0, align: 'start' }], []); applyAlign(region); expect(region.children).toHaveLength(0); }); it('should align parts to start on first axis', () => { const part1 = createPart('p1', [5, 10]); const part2 = createPart('p2', [7, 20]); const part3 = createPart('p3', [2, 30]); const region = createRegion( [{ name: 'x', min: 0, align: 'start' }, { name: 'y' }], [part1, part2, part3] ); applyAlign(region); // 排序后应该是 part3(2), part1(5), part2(7) -> 对齐到 0, 1, 2 (第一轴) expect(region.children[0].value.position[0]).toBe(0); expect(region.children[1].value.position[0]).toBe(1); expect(region.children[2].value.position[0]).toBe(2); // 第二轴保持不变 expect(region.children[0].value.position[1]).toBe(30); expect(region.children[1].value.position[1]).toBe(10); expect(region.children[2].value.position[1]).toBe(20); }); it('should align parts to start with custom min', () => { const part1 = createPart('p1', [5, 100]); const part2 = createPart('p2', [7, 200]); const region = createRegion( [{ name: 'x', min: 10, align: 'start' }, { name: 'y' }], [part1, part2] ); applyAlign(region); expect(region.children[0].value.position[0]).toBe(10); expect(region.children[1].value.position[0]).toBe(11); // 第二轴保持不变 expect(region.children[0].value.position[1]).toBe(100); expect(region.children[1].value.position[1]).toBe(200); }); it('should align parts to end on first axis', () => { const part1 = createPart('p1', [2, 50]); const part2 = createPart('p2', [4, 60]); const part3 = createPart('p3', [1, 70]); const region = createRegion( [{ name: 'x', max: 10, align: 'end' }, { name: 'y' }], [part1, part2, part3] ); applyAlign(region); // 3 个部分,对齐到 end(max=10),应该是 8, 9, 10 expect(region.children[0].value.position[0]).toBe(8); expect(region.children[1].value.position[0]).toBe(9); expect(region.children[2].value.position[0]).toBe(10); }); it('should align parts to center on first axis', () => { const part1 = createPart('p1', [0, 5]); const part2 = createPart('p2', [1, 6]); const part3 = createPart('p3', [2, 7]); const region = createRegion( [{ name: 'x', min: 0, max: 10, align: 'center' }, { name: 'y' }], [part1, part2, part3] ); applyAlign(region); // 中心是 5,3 个部分应该是 4, 5, 6 expect(region.children[0].value.position[0]).toBe(4); expect(region.children[1].value.position[0]).toBe(5); expect(region.children[2].value.position[0]).toBe(6); }); it('should handle even count center alignment', () => { const part1 = createPart('p1', [0, 10]); const part2 = createPart('p2', [1, 20]); const region = createRegion( [{ name: 'x', min: 0, max: 10, align: 'center' }, { name: 'y' }], [part1, part2] ); applyAlign(region); // 中心是 5,2 个部分应该是 4.5, 5.5 expect(region.children[0].value.position[0]).toBe(4.5); expect(region.children[1].value.position[0]).toBe(5.5); }); it('should sort children by position on current axis', () => { const part1 = createPart('p1', [5, 100]); const part2 = createPart('p2', [1, 200]); const part3 = createPart('p3', [3, 300]); const region = createRegion( [{ name: 'x', min: 0, align: 'start' }, { name: 'y' }], [part1, part2, part3] ); applyAlign(region); // children 应该按第一轴位置排序 expect(region.children[0].value.id).toBe('p2'); expect(region.children[1].value.id).toBe('p3'); expect(region.children[2].value.id).toBe('p1'); }); it('should align on multiple axes', () => { const part1 = createPart('p1', [5, 10]); const part2 = createPart('p2', [7, 20]); const part3 = createPart('p3', [2, 30]); const region = createRegion( [ { name: 'x', min: 0, align: 'start' }, { name: 'y', min: 0, align: 'start' } ], [part1, part2, part3] ); applyAlign(region); // X 轴对齐: // 唯一位置值:[2, 5, 7] -> 映射到 [0, 1, 2] // part3: 2->0, part1: 5->1, part2: 7->2 // 结果:part3=[0,30], part1=[1,10], part2=[2,20] // // Y 轴对齐: // 唯一位置值:[10, 20, 30] -> 映射到 [0, 1, 2] // part1: 10->0, part2: 20->1, part3: 30->2 // 最终:part1=[1,0], part2=[2,1], part3=[0,2] const positions = region.children.map(c => ({ id: c.value.id, position: c.value.position })); // children 按位置排序后的顺序 expect(positions[0].id).toBe('p3'); expect(positions[0].position).toEqual([0, 2]); expect(positions[1].id).toBe('p1'); expect(positions[1].position).toEqual([1, 0]); expect(positions[2].id).toBe('p2'); expect(positions[2].position).toEqual([2, 1]); }); it('should align 4 elements on rectangle corners', () => { // 4 个元素放在矩形的四个角:(0,0), (10,0), (10,1), (0,1) // 期望:保持矩形布局,只是紧凑到 (0,0), (1,0), (1,1), (0,1) const part1 = createPart('p1', [0, 0]); // 左下角 const part2 = createPart('p2', [10, 0]); // 右下角 const part3 = createPart('p3', [10, 1]); // 右上角 const part4 = createPart('p4', [0, 1]); // 左上角 const region = createRegion( [ { name: 'x', min: 0, max: 10, align: 'start' }, { name: 'y', min: 0, max: 10, align: 'start' } ], [part1, part2, part3, part4] ); applyAlign(region); // X 轴对齐: // 唯一位置值:[0, 10] -> 映射到 [0, 1] // part1: 0->0, part4: 0->0, part2: 10->1, part3: 10->1 // 结果:part1=[0,0], part4=[0,1], part2=[1,0], part3=[1,1] // // Y 轴对齐: // 唯一位置值:[0, 1] -> 映射到 [0, 1] (已经是紧凑的) // part1: 0->0, part2: 0->0, part4: 1->1, part3: 1->1 // 最终:part1=[0,0], part2=[1,0], part4=[0,1], part3=[1,1] const positions = region.children.map(c => ({ id: c.value.id, position: c.value.position })); // children 按位置排序后的顺序:(0,0), (0,1), (1,0), (1,1) expect(positions[0].id).toBe('p1'); expect(positions[0].position).toEqual([0, 0]); expect(positions[1].id).toBe('p4'); expect(positions[1].position).toEqual([0, 1]); expect(positions[2].id).toBe('p2'); expect(positions[2].position).toEqual([1, 0]); expect(positions[3].id).toBe('p3'); expect(positions[3].position).toEqual([1, 1]); }); }); describe('shuffle', () => { it('should do nothing with empty region', () => { const region = createRegion([], []); const rng = createRNG(42); shuffle(region, rng); expect(region.children).toHaveLength(0); }); it('should do nothing with single part', () => { const part = createPart('p1', [0, 0, 0]); const region = createRegion([], [part]); const rng = createRNG(42); shuffle(region, rng); expect(region.children[0].value.position).toEqual([0, 0, 0]); }); it('should shuffle positions of multiple parts', () => { const part1 = createPart('p1', [0, 100]); const part2 = createPart('p2', [1, 200]); const part3 = createPart('p3', [2, 300]); const region = createRegion([], [part1, part2, part3]); const rng = createRNG(42); const originalPositions = region.children.map(c => [...c.value.position]); shuffle(region, rng); // 位置应该被交换 const newPositions = region.children.map(c => c.value.position); // 验证所有原始位置仍然存在(只是被交换了) originalPositions.forEach(origPos => { const found = newPositions.some(newPos => newPos[0] === origPos[0] && newPos[1] === origPos[1] ); expect(found).toBe(true); }); }); it('should be deterministic with same seed', () => { const createRegionForTest = () => { const part1 = createPart('p1', [0, 10]); const part2 = createPart('p2', [1, 20]); const part3 = createPart('p3', [2, 30]); return createRegion([], [part1, part2, part3]); }; const region1 = createRegionForTest(); const region2 = createRegionForTest(); const rng1 = createRNG(42); const rng2 = createRNG(42); shuffle(region1, rng1); shuffle(region2, rng2); const positions1 = region1.children.map(c => c.value.position); const positions2 = region2.children.map(c => c.value.position); expect(positions1).toEqual(positions2); }); it('should produce different results with different seeds', () => { const createRegionForTest = () => { const part1 = createPart('p1', [0, 10]); const part2 = createPart('p2', [1, 20]); const part3 = createPart('p3', [2, 30]); const part4 = createPart('p4', [3, 40]); const part5 = createPart('p5', [4, 50]); return createRegion([], [part1, part2, part3, part4, part5]); }; const results = new Set(); // 尝试多个种子,确保大多数产生不同结果 for (let seed = 1; seed <= 10; seed++) { const region = createRegionForTest(); const rng = createRNG(seed); shuffle(region, rng); const positions = JSON.stringify(region.children.map(c => c.value.position)); results.add(positions); } // 10 个种子中至少应该有 5 个不同的结果 expect(results.size).toBeGreaterThan(5); }); }); });