diff --git a/src/core/part.ts b/src/core/part.ts index fd7d31b..f0b64db 100644 --- a/src/core/part.ts +++ b/src/core/part.ts @@ -16,7 +16,7 @@ export type Part = Entity & { // current region region: EntityAccessor; - // current position in region + // current position in region, expect to be the same length as region's axes position: number[]; } @@ -30,4 +30,4 @@ export function flipTo(part: Part, side: number) { export function roll(part: Part, rng: RNG) { part.side = rng.nextInt(part.sides); -} \ No newline at end of file +} diff --git a/src/core/region.ts b/src/core/region.ts index cbe6c9e..ed3bf76 100644 --- a/src/core/region.ts +++ b/src/core/region.ts @@ -3,7 +3,7 @@ import {Part} from "./part"; import {RNG} from "../utils/rng"; export type Region = Entity & { - // aligning axes of the region + // aligning axes of the region, expect a part's position to have a matching number of elements axes: RegionAxis[]; // current children; expect no overlapped positions @@ -26,33 +26,30 @@ export type RegionAxis = { * @param region */ export function applyAlign(region: Region){ - for (const axis of region.axes) { - if (region.children.length === 0) continue; + if (region.children.length === 0) return; + + // 对每个 axis 分别处理 + for (let axisIndex = 0; axisIndex < region.axes.length; axisIndex++) { + const axis = region.axes[axisIndex]; + if (!axis.align) continue; - // 获取当前轴向上的所有位置 - const positions = region.children.map(accessor => accessor.value.position); - // 根据当前轴的位置排序 children region.children.sort((a, b) => { - const posA = a.value.position[0] ?? 0; - const posB = b.value.position[0] ?? 0; + const posA = a.value.position[axisIndex] ?? 0; + const posB = b.value.position[axisIndex] ?? 0; return posA - posB; }); if (axis.align === 'start' && axis.min !== undefined) { // 从 min 开始紧凑排列 region.children.forEach((accessor, index) => { - const currentPos = accessor.value.position.slice(); - currentPos[0] = axis.min! + index; - accessor.value.position = currentPos; + accessor.value.position[axisIndex] = axis.min! + index; }); } else if (axis.align === 'end' && axis.max !== undefined) { // 从 max 开始向前紧凑排列 const count = region.children.length; region.children.forEach((accessor, index) => { - const currentPos = accessor.value.position.slice(); - currentPos[0] = axis.max! - (count - 1 - index); - accessor.value.position = currentPos; + accessor.value.position[axisIndex] = axis.max! - (count - 1 - index); }); } else if (axis.align === 'center') { // 居中排列 @@ -61,13 +58,11 @@ export function applyAlign(region: Region){ const max = axis.max ?? count - 1; const range = max - min; const center = min + range / 2; - + region.children.forEach((accessor, index) => { - const currentPos = accessor.value.position.slice(); // 计算相对于中心的偏移 const offset = index - (count - 1) / 2; - currentPos[0] = center + offset; - accessor.value.position = currentPos; + accessor.value.position[axisIndex] = center + offset; }); } } @@ -85,11 +80,9 @@ export function shuffle(region: Region, rng: RNG){ const children = [...region.children]; for (let i = children.length - 1; i > 0; i--) { const j = rng.nextInt(i + 1); - // 交换位置 - const posI = children[i].value.position.slice(); - const posJ = children[j].value.position.slice(); - - children[i].value.position = posJ; - children[j].value.position = posI; + // 交换两个 part 的整个 position 数组 + const temp = children[i].value.position; + children[i].value.position = children[j].value.position; + children[j].value.position = temp; } } diff --git a/tests/core/region.test.ts b/tests/core/region.test.ts index 5fec8bf..4a19336 100644 --- a/tests/core/region.test.ts +++ b/tests/core/region.test.ts @@ -1,4 +1,4 @@ -import { describe, it, expect, beforeEach } from 'vitest'; +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'; @@ -34,30 +34,34 @@ describe('Region', () => { expect(region.children).toHaveLength(0); }); - it('should align parts to start', () => { - const part1 = createPart('p1', [5, 0]); - const part2 = createPart('p2', [7, 0]); - const part3 = createPart('p3', [2, 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: 'x', min: 0, align: 'start' }, { name: 'y' }], [part1, part2, part3] ); applyAlign(region); - // 排序后应该是 part3(2), part1(5), part2(7) -> 对齐到 0, 1, 2 + // 排序后应该是 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, 0]); - const part2 = createPart('p2', [7, 0]); + const part1 = createPart('p1', [5, 100]); + const part2 = createPart('p2', [7, 200]); const region = createRegion( - [{ name: 'x', min: 10, align: 'start' }], + [{ name: 'x', min: 10, align: 'start' }, { name: 'y' }], [part1, part2] ); @@ -65,15 +69,18 @@ describe('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', () => { - const part1 = createPart('p1', [2, 0]); - const part2 = createPart('p2', [4, 0]); - const part3 = createPart('p3', [1, 0]); + 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: 'x', max: 10, align: 'end' }, { name: 'y' }], [part1, part2, part3] ); @@ -85,13 +92,13 @@ describe('Region', () => { expect(region.children[2].value.position[0]).toBe(10); }); - it('should align parts to center', () => { - const part1 = createPart('p1', [0, 0]); - const part2 = createPart('p2', [1, 0]); - const part3 = createPart('p3', [2, 0]); + 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: 'x', min: 0, max: 10, align: 'center' }, { name: 'y' }], [part1, part2, part3] ); @@ -104,11 +111,11 @@ describe('Region', () => { }); it('should handle even count center alignment', () => { - const part1 = createPart('p1', [0, 0]); - const part2 = createPart('p2', [1, 0]); + const part1 = createPart('p1', [0, 10]); + const part2 = createPart('p2', [1, 20]); const region = createRegion( - [{ name: 'x', min: 0, max: 10, align: 'center' }], + [{ name: 'x', min: 0, max: 10, align: 'center' }, { name: 'y' }], [part1, part2] ); @@ -119,23 +126,64 @@ describe('Region', () => { expect(region.children[1].value.position[0]).toBe(5.5); }); - it('should sort children by position', () => { - const part1 = createPart('p1', [5, 0]); - const part2 = createPart('p2', [1, 0]); - const part3 = createPart('p3', [3, 0]); + 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: 'x', min: 0, align: 'start' }, { name: 'y' }], [part1, part2, part3] ); applyAlign(region); - // children 应该按位置排序 + // 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, axisIndex=0) 对齐: + // 排序:part3(x=2), part1(x=5), part2(x=7) → children=[part3, part1, part2] + // 对齐:part3.position[0]=0, part1.position[0]=1, part2.position[0]=2 + // 结果:part3=[0,30], part1=[1,10], part2=[2,20] + // + // 第二轴 (y, axisIndex=1) 对齐: + // 排序:part1(y=10), part2(y=20), part3(y=30) → children=[part1, part2, part3] + // 对齐:part1.position[1]=0, part2.position[1]=1, part3.position[1]=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 按 y 轴排序后的顺序 + expect(positions[0].id).toBe('p1'); + expect(positions[0].position).toEqual([1, 0]); + + expect(positions[1].id).toBe('p2'); + expect(positions[1].position).toEqual([2, 1]); + + expect(positions[2].id).toBe('p3'); + expect(positions[2].position).toEqual([0, 2]); + }); }); describe('shuffle', () => { @@ -147,17 +195,17 @@ describe('Region', () => { }); it('should do nothing with single part', () => { - const part = createPart('p1', [0, 0]); + 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]); + expect(region.children[0].value.position).toEqual([0, 0, 0]); }); it('should shuffle positions of multiple parts', () => { - const part1 = createPart('p1', [0, 0]); - const part2 = createPart('p2', [1, 0]); - const part3 = createPart('p3', [2, 0]); + 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); @@ -179,9 +227,9 @@ describe('Region', () => { it('should be deterministic with same seed', () => { const createRegionForTest = () => { - const part1 = createPart('p1', [0, 0]); - const part2 = createPart('p2', [1, 0]); - const part3 = createPart('p3', [2, 0]); + const part1 = createPart('p1', [0, 10]); + const part2 = createPart('p2', [1, 20]); + const part3 = createPart('p3', [2, 30]); return createRegion([], [part1, part2, part3]); }; @@ -201,17 +249,20 @@ describe('Region', () => { }); it('should produce different results with different seeds', () => { - const part1 = createPart('p1', [0, 0]); - const part2 = createPart('p2', [1, 0]); - const part3 = createPart('p3', [2, 0]); - const part4 = createPart('p4', [3, 0]); - const part5 = createPart('p5', [4, 0]); - + 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 = createRegion([], [part1, part2, part3, part4, part5]); + const region = createRegionForTest(); const rng = createRNG(seed); shuffle(region, rng);