2026-04-01 13:36:16 +08:00
|
|
|
import {Entity, EntityAccessor} from "../utils/entity";
|
|
|
|
|
import {Part} from "./part";
|
|
|
|
|
import {RNG} from "../utils/rng";
|
|
|
|
|
|
|
|
|
|
export type Region = Entity & {
|
|
|
|
|
// aligning axes of the region
|
|
|
|
|
axes: RegionAxis[];
|
2026-04-01 17:34:21 +08:00
|
|
|
|
2026-04-01 13:36:16 +08:00
|
|
|
// current children; expect no overlapped positions
|
|
|
|
|
children: EntityAccessor<Part>[];
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export type RegionAxis = {
|
|
|
|
|
name: string;
|
|
|
|
|
min?: number;
|
|
|
|
|
max?: number;
|
|
|
|
|
align?: 'start' | 'end' | 'center';
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* for each axis, try to remove gaps in positions.
|
|
|
|
|
* - if min exists and align is start, and there are parts at (for example) min+2 and min+4, then move them to min and min+1
|
|
|
|
|
* - if max exists and align is end, and there are parts at (for example) max-2 and max-4, then move them to max-1 and max-3
|
|
|
|
|
* - for center, move parts to the center, possibly creating parts placed at 0.5 positions
|
|
|
|
|
* - sort children so that they're in ascending order on each axes.
|
|
|
|
|
* @param region
|
|
|
|
|
*/
|
|
|
|
|
export function applyAlign(region: Region){
|
|
|
|
|
for (const axis of region.axes) {
|
2026-04-01 17:34:21 +08:00
|
|
|
if (region.children.length === 0) 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;
|
|
|
|
|
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;
|
|
|
|
|
});
|
|
|
|
|
} 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;
|
|
|
|
|
});
|
|
|
|
|
} else if (axis.align === 'center') {
|
|
|
|
|
// 居中排列
|
|
|
|
|
const count = region.children.length;
|
|
|
|
|
const min = axis.min ?? 0;
|
|
|
|
|
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;
|
|
|
|
|
});
|
|
|
|
|
}
|
2026-04-01 13:36:16 +08:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* shuffle on each axis. for each axis, try to swap position.
|
|
|
|
|
* @param region
|
|
|
|
|
* @param rng
|
|
|
|
|
*/
|
|
|
|
|
export function shuffle(region: Region, rng: RNG){
|
2026-04-01 17:34:21 +08:00
|
|
|
if (region.children.length <= 1) return;
|
|
|
|
|
|
|
|
|
|
// Fisher-Yates 洗牌算法
|
|
|
|
|
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;
|
|
|
|
|
}
|
|
|
|
|
}
|