2026-04-01 13:36:16 +08:00
|
|
|
import {Part} from "./part";
|
2026-04-02 15:59:27 +08:00
|
|
|
import {RNG} from "@/utils/rng";
|
2026-04-01 13:36:16 +08:00
|
|
|
|
2026-04-02 13:52:15 +08:00
|
|
|
export type Region = {
|
|
|
|
|
id: string;
|
2026-04-01 13:36:16 +08:00
|
|
|
axes: RegionAxis[];
|
2026-04-03 12:46:02 +08:00
|
|
|
childIds: string[];
|
|
|
|
|
partMap: Record<string, string>;
|
2026-04-01 13:36:16 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export type RegionAxis = {
|
|
|
|
|
name: string;
|
|
|
|
|
min?: number;
|
|
|
|
|
max?: number;
|
|
|
|
|
align?: 'start' | 'end' | 'center';
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-03 12:46:02 +08:00
|
|
|
export function createRegion(id: string, axes: RegionAxis[]): Region {
|
|
|
|
|
return {
|
|
|
|
|
id,
|
|
|
|
|
axes,
|
|
|
|
|
childIds: [],
|
|
|
|
|
partMap: {},
|
|
|
|
|
};
|
2026-04-02 16:32:26 +08:00
|
|
|
}
|
|
|
|
|
|
2026-04-03 15:00:25 +08:00
|
|
|
function buildPartMap<TMeta>(region: Region, parts: Record<string, Part<TMeta>>) {
|
2026-04-03 12:46:02 +08:00
|
|
|
const map: Record<string, string> = {};
|
|
|
|
|
for (const childId of region.childIds) {
|
|
|
|
|
const part = parts[childId];
|
|
|
|
|
if (part) {
|
|
|
|
|
map[part.position.join(',')] = childId;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return map;
|
2026-04-02 13:52:15 +08:00
|
|
|
}
|
|
|
|
|
|
2026-04-03 15:00:25 +08:00
|
|
|
export function applyAlign<TMeta>(region: Region, parts: Record<string, Part<TMeta>>) {
|
2026-04-03 12:46:02 +08:00
|
|
|
if (region.childIds.length === 0) return;
|
2026-04-01 17:48:40 +08:00
|
|
|
|
|
|
|
|
for (let axisIndex = 0; axisIndex < region.axes.length; axisIndex++) {
|
|
|
|
|
const axis = region.axes[axisIndex];
|
|
|
|
|
if (!axis.align) continue;
|
2026-04-01 17:34:21 +08:00
|
|
|
|
2026-04-01 18:33:09 +08:00
|
|
|
const positionValues = new Set<number>();
|
2026-04-03 12:46:02 +08:00
|
|
|
for (const childId of region.childIds) {
|
|
|
|
|
const part = parts[childId];
|
|
|
|
|
if (part) {
|
|
|
|
|
positionValues.add(part.position[axisIndex] ?? 0);
|
|
|
|
|
}
|
2026-04-01 18:33:09 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const sortedPositions = Array.from(positionValues).sort((a, b) => a - b);
|
|
|
|
|
const positionMap = new Map<number, number>();
|
2026-04-01 17:34:21 +08:00
|
|
|
|
|
|
|
|
if (axis.align === 'start' && axis.min !== undefined) {
|
2026-04-01 18:33:09 +08:00
|
|
|
sortedPositions.forEach((pos, index) => {
|
|
|
|
|
positionMap.set(pos, axis.min! + index);
|
2026-04-01 17:34:21 +08:00
|
|
|
});
|
|
|
|
|
} else if (axis.align === 'end' && axis.max !== undefined) {
|
2026-04-01 18:33:09 +08:00
|
|
|
const count = sortedPositions.length;
|
|
|
|
|
sortedPositions.forEach((pos, index) => {
|
|
|
|
|
positionMap.set(pos, axis.max! - (count - 1 - index));
|
2026-04-01 17:34:21 +08:00
|
|
|
});
|
|
|
|
|
} else if (axis.align === 'center') {
|
2026-04-01 18:33:09 +08:00
|
|
|
const count = sortedPositions.length;
|
2026-04-01 17:34:21 +08:00
|
|
|
const min = axis.min ?? 0;
|
|
|
|
|
const max = axis.max ?? count - 1;
|
|
|
|
|
const range = max - min;
|
|
|
|
|
const center = min + range / 2;
|
2026-04-01 17:48:40 +08:00
|
|
|
|
2026-04-01 18:33:09 +08:00
|
|
|
sortedPositions.forEach((pos, index) => {
|
2026-04-01 17:34:21 +08:00
|
|
|
const offset = index - (count - 1) / 2;
|
2026-04-01 18:33:09 +08:00
|
|
|
positionMap.set(pos, center + offset);
|
2026-04-01 17:34:21 +08:00
|
|
|
});
|
|
|
|
|
}
|
2026-04-01 18:33:09 +08:00
|
|
|
|
2026-04-03 12:46:02 +08:00
|
|
|
for (const childId of region.childIds) {
|
|
|
|
|
const part = parts[childId];
|
|
|
|
|
if (part) {
|
|
|
|
|
const currentPos = part.position[axisIndex] ?? 0;
|
|
|
|
|
part.position[axisIndex] = positionMap.get(currentPos) ?? currentPos;
|
|
|
|
|
}
|
2026-04-01 18:33:09 +08:00
|
|
|
}
|
2026-04-01 13:36:16 +08:00
|
|
|
}
|
2026-04-01 18:33:09 +08:00
|
|
|
|
2026-04-03 12:46:02 +08:00
|
|
|
region.childIds.sort((aId, bId) => {
|
|
|
|
|
const a = parts[aId];
|
|
|
|
|
const b = parts[bId];
|
|
|
|
|
if (!a || !b) return 0;
|
2026-04-01 18:33:09 +08:00
|
|
|
for (let i = 0; i < region.axes.length; i++) {
|
2026-04-03 12:46:02 +08:00
|
|
|
const diff = (a.position[i] ?? 0) - (b.position[i] ?? 0);
|
2026-04-01 18:33:09 +08:00
|
|
|
if (diff !== 0) return diff;
|
|
|
|
|
}
|
|
|
|
|
return 0;
|
|
|
|
|
});
|
2026-04-01 13:36:16 +08:00
|
|
|
|
2026-04-03 12:46:02 +08:00
|
|
|
region.partMap = buildPartMap(region, parts);
|
2026-04-02 13:52:15 +08:00
|
|
|
}
|
|
|
|
|
|
2026-04-03 15:00:25 +08:00
|
|
|
export function shuffle<TMeta>(region: Region, parts: Record<string, Part<TMeta>>, rng: RNG){
|
2026-04-03 12:46:02 +08:00
|
|
|
if (region.childIds.length <= 1) return;
|
2026-04-01 17:34:21 +08:00
|
|
|
|
2026-04-03 12:46:02 +08:00
|
|
|
const childIds = [...region.childIds];
|
|
|
|
|
for (let i = childIds.length - 1; i > 0; i--) {
|
2026-04-01 17:34:21 +08:00
|
|
|
const j = rng.nextInt(i + 1);
|
2026-04-03 12:46:02 +08:00
|
|
|
const partI = parts[childIds[i]];
|
|
|
|
|
const partJ = parts[childIds[j]];
|
|
|
|
|
if (!partI || !partJ) continue;
|
|
|
|
|
|
|
|
|
|
const posI = [...partI.position];
|
|
|
|
|
const posJ = [...partJ.position];
|
|
|
|
|
partI.position = posJ;
|
|
|
|
|
partJ.position = posI;
|
2026-04-01 17:34:21 +08:00
|
|
|
}
|
2026-04-03 12:46:02 +08:00
|
|
|
|
|
|
|
|
region.partMap = buildPartMap(region, parts);
|
2026-04-02 21:58:11 +08:00
|
|
|
}
|
|
|
|
|
|
2026-04-03 15:00:25 +08:00
|
|
|
export function moveToRegion<TMeta>(part: Part<TMeta>, sourceRegion: Region | null, targetRegion: Region, position?: number[]) {
|
|
|
|
|
if (sourceRegion && part.regionId === sourceRegion.id) {
|
|
|
|
|
sourceRegion.childIds = sourceRegion.childIds.filter(id => id !== part.id);
|
|
|
|
|
delete sourceRegion.partMap[part.position.join(',')];
|
|
|
|
|
}
|
2026-04-03 12:46:02 +08:00
|
|
|
|
|
|
|
|
targetRegion.childIds.push(part.id);
|
|
|
|
|
if (position) {
|
|
|
|
|
part.position = position;
|
|
|
|
|
}
|
|
|
|
|
targetRegion.partMap[part.position.join(',')] = part.id;
|
|
|
|
|
|
|
|
|
|
part.regionId = targetRegion.id;
|
2026-04-02 21:58:11 +08:00
|
|
|
}
|
|
|
|
|
|
2026-04-03 15:00:25 +08:00
|
|
|
export function moveToRegionAll<TMeta>(parts: Part<TMeta>[], sourceRegion: Region | null, targetRegion: Region, positions?: number[][]) {
|
2026-04-03 12:46:02 +08:00
|
|
|
for (let i = 0; i < parts.length; i++) {
|
|
|
|
|
const part = parts[i];
|
2026-04-03 15:00:25 +08:00
|
|
|
if (sourceRegion && part.regionId === sourceRegion.id) {
|
|
|
|
|
sourceRegion.childIds = sourceRegion.childIds.filter(id => id !== part.id);
|
|
|
|
|
delete sourceRegion.partMap[part.position.join(',')];
|
|
|
|
|
}
|
2026-04-03 12:46:02 +08:00
|
|
|
|
|
|
|
|
targetRegion.childIds.push(part.id);
|
|
|
|
|
if (positions && positions[i]) {
|
|
|
|
|
part.position = positions[i];
|
2026-04-02 21:58:11 +08:00
|
|
|
}
|
2026-04-03 12:46:02 +08:00
|
|
|
targetRegion.partMap[part.position.join(',')] = part.id;
|
|
|
|
|
|
|
|
|
|
part.regionId = targetRegion.id;
|
|
|
|
|
}
|
2026-04-02 21:58:11 +08:00
|
|
|
}
|
|
|
|
|
|
2026-04-03 15:00:25 +08:00
|
|
|
export function removeFromRegion<TMeta>(part: Part<TMeta>, region: Region) {
|
2026-04-03 12:46:02 +08:00
|
|
|
region.childIds = region.childIds.filter(id => id !== part.id);
|
|
|
|
|
delete region.partMap[part.position.join(',')];
|
|
|
|
|
}
|