ttrpg-tools/src/components/stores/pinsStore.ts

160 lines
4.1 KiB
TypeScript

import { createStore } from "solid-js/store";
export interface Pin {
x: number;
y: number;
label: string;
}
interface PinsState {
pins: Pin[];
rawSrc: string;
isFixed: boolean;
labelStart: string;
}
// 判断 labelStart 是否为数字
function isNumericLabelStart(start: string): boolean {
return /^\d+$/.test(start);
}
// 生成字母标签 A-Z, AA-ZZ, AAA-ZZZ ... 从指定起始值开始
export function generateAlphaLabel(index: number, startLabel: string = 'A'): string {
const startOffset = alphaLabelToIndex(startLabel);
return indexToAlphaLabel(index + startOffset);
}
// 将字母标签转换为索引 (A=0, B=1, ..., Z=25, AA=26, ...)
export function alphaLabelToIndex(label: string): number {
let index = 0;
for (let i = 0; i < label.length; i++) {
index = index * 26 + (label.charCodeAt(i) - 64);
}
return index - 1;
}
// 将索引转换为字母标签 (0=A, 1=B, ..., 25=Z, 26=AA, ...)
export function indexToAlphaLabel(index: number): string {
const labels: string[] = [];
let num = index;
do {
const remainder = num % 26;
labels.unshift(String.fromCharCode(65 + remainder));
num = Math.floor(num / 26) - 1;
} while (num >= 0);
return labels.join('');
}
// 生成数字标签,从指定起始值开始
export function generateNumberLabel(index: number, startNum: number): string {
return String(startNum + index);
}
// 解析 pins 字符串 "A:30,40 B:10,30" 或 "1:30,40 2:10,30" -> Pin[]
export function parsePins(pinsStr: string): Pin[] {
if (!pinsStr) return [];
const pins: Pin[] = [];
// 支持任意非空白字符作为标签(不仅限于大写字母)
const regex = /([^:\s]+):(\d+),(\d+)/g;
let match;
while ((match = regex.exec(pinsStr)) !== null) {
pins.push({
label: match[1],
x: parseInt(match[2]),
y: parseInt(match[3])
});
}
return pins;
}
// 格式化 pins 为字符串 "A:30,40 B:10,30"
export function formatPins(pins: Pin[]): string {
return pins.map(pin => `${pin.label}:${pin.x},${pin.y}`).join(' ');
}
// 找到最早未使用的标签
export function findNextUnusedLabel(pins: Pin[], labelStart: string): string {
const usedLabels = new Set(pins.map(p => p.label));
if (isNumericLabelStart(labelStart)) {
// 数字标签模式
const startNum = parseInt(labelStart);
let index = 0;
while (true) {
const label = generateNumberLabel(index, startNum);
if (!usedLabels.has(label)) {
return label;
}
index++;
if (index > 10000) break; // 安全限制
}
} else {
// 字母标签模式
let index = 0;
while (true) {
const label = generateAlphaLabel(index, labelStart);
if (!usedLabels.has(label)) {
return label;
}
index++;
if (index > 10000) break; // 安全限制
}
}
// 回退值
if (isNumericLabelStart(labelStart)) {
return generateNumberLabel(pins.length, parseInt(labelStart));
}
return generateAlphaLabel(pins.length, labelStart);
}
// 创建 store 实例
export function createPinsStore(
initialPinsStr: string = "",
initialFixed: boolean = false,
initialLabelStart: string = "A"
) {
const [state, setState] = createStore<PinsState>({
pins: parsePins(initialPinsStr),
rawSrc: "",
isFixed: initialFixed,
labelStart: initialLabelStart
});
const setRawSrc = (src: string) => {
setState("rawSrc", src);
};
const addPin = (x: number, y: number) => {
const label = findNextUnusedLabel(state.pins, state.labelStart);
setState("pins", [...state.pins, { x, y, label }]);
};
const removePin = (index: number) => {
setState("pins", state.pins.filter((_, i) => i !== index));
};
const formatCurrentPins = () => formatPins(state.pins);
const getCopyText = () => {
const pinsStr = formatCurrentPins();
const labelStartAttr = state.labelStart !== 'A' ? ` labelStart="${state.labelStart}"` : '';
return `:md-pins[${state.rawSrc}]{pins="${pinsStr}" fixed${labelStartAttr}`;
};
return {
state,
setState,
setRawSrc,
addPin,
removePin,
formatCurrentPins,
getCopyText
};
}