138 lines
3.7 KiB
TypeScript
138 lines
3.7 KiB
TypeScript
import type { DegreeRingConfig, TickMark, DegreeLabel, DegreeRingData } from './types';
|
|
import { generateTextPath, polarToXY } from './utils';
|
|
|
|
// 根据刻度级别计算长度,最小值由 `clampTickLength` 兜底
|
|
const resolveTickLength = (config: DegreeRingConfig, type: TickMark['type']): number => {
|
|
const step = config.tickLengthStep ?? 0;
|
|
if (type === 'major') return config.tickLength;
|
|
if (type === 'minor') return config.tickLength - step;
|
|
return config.tickLength - 2 * step;
|
|
};
|
|
|
|
const clampTickLength = (value: number): number => (value < 1 ? 1 : value);
|
|
|
|
export function buildDegreeRing(config: DegreeRingConfig): DegreeRingData {
|
|
const ticks: TickMark[] = [];
|
|
const { rInner, rOuter, mode } = config;
|
|
const labelFontSize = 8;
|
|
|
|
const majorTick = Math.max(1, config.majorTick);
|
|
const minorTick = Math.max(1, config.minorTick);
|
|
const microTick = Math.max(1, config.microTick);
|
|
|
|
for (let angle = 0; angle < 360; angle++) {
|
|
let type: TickMark['type'] | null = null;
|
|
|
|
if (angle % majorTick === 0) {
|
|
type = 'major';
|
|
} else if (angle % minorTick === 0) {
|
|
type = 'minor';
|
|
} else if (angle % microTick === 0) {
|
|
type = 'micro';
|
|
}
|
|
|
|
if (!type) continue;
|
|
|
|
const length = clampTickLength(resolveTickLength(config, type));
|
|
|
|
// 预计算坐标,避免渲染时重复三角计算
|
|
if (mode === 'inner') {
|
|
const start = polarToXY(angle, rInner);
|
|
const end = polarToXY(angle, rInner + length);
|
|
ticks.push({
|
|
angle,
|
|
type,
|
|
length,
|
|
startR: rInner,
|
|
endR: rInner + length,
|
|
x1: start.x,
|
|
y1: start.y,
|
|
x2: end.x,
|
|
y2: end.y,
|
|
});
|
|
continue;
|
|
}
|
|
|
|
if (mode === 'outer') {
|
|
const start = polarToXY(angle, rOuter - length);
|
|
const end = polarToXY(angle, rOuter);
|
|
ticks.push({
|
|
angle,
|
|
type,
|
|
length,
|
|
startR: rOuter - length,
|
|
endR: rOuter,
|
|
x1: start.x,
|
|
y1: start.y,
|
|
x2: end.x,
|
|
y2: end.y,
|
|
});
|
|
continue;
|
|
}
|
|
|
|
// 模式为 `both` 时,同角度生成内外两条刻度线
|
|
const innerStart = polarToXY(angle, rInner);
|
|
const innerEnd = polarToXY(angle, rInner + length);
|
|
ticks.push({
|
|
angle,
|
|
type,
|
|
length,
|
|
startR: rInner,
|
|
endR: rInner + length,
|
|
x1: innerStart.x,
|
|
y1: innerStart.y,
|
|
x2: innerEnd.x,
|
|
y2: innerEnd.y,
|
|
});
|
|
const outerStart = polarToXY(angle, rOuter - length);
|
|
const outerEnd = polarToXY(angle, rOuter);
|
|
ticks.push({
|
|
angle,
|
|
type,
|
|
length,
|
|
startR: rOuter - length,
|
|
endR: rOuter,
|
|
x1: outerStart.x,
|
|
y1: outerStart.y,
|
|
x2: outerEnd.x,
|
|
y2: outerEnd.y,
|
|
});
|
|
}
|
|
|
|
const labels: DegreeLabel[] = [];
|
|
if (config.showDegree === 1) {
|
|
for (let angle = 0; angle < 360; angle += majorTick) {
|
|
const r = (rInner + rOuter) / 2;
|
|
const text = angle.toString();
|
|
const estimatedArcLength = text.length * labelFontSize * 1.1;
|
|
const estimatedSpan = r > 0 ? (estimatedArcLength / r) * (180 / Math.PI) : 0;
|
|
const maxSpan = Math.max(majorTick * 0.9, 4);
|
|
const span = Math.min(Math.max(estimatedSpan, 4), maxSpan);
|
|
const aStart = angle - span / 2;
|
|
const aEnd = angle + span / 2;
|
|
|
|
// 使用 `textPath` 保持度数方向与扇区文字一致
|
|
labels.push({
|
|
angle,
|
|
text,
|
|
r,
|
|
fontSize: labelFontSize,
|
|
textPathId: `degree-label-${angle}`,
|
|
textPath: generateTextPath(r, r, aStart, aEnd, 'middle'),
|
|
});
|
|
}
|
|
}
|
|
|
|
return {
|
|
ticks,
|
|
tickColor: config.tickColor,
|
|
ring: {
|
|
rInner,
|
|
rOuter,
|
|
color: config.ringColor,
|
|
opacity: config.opacity,
|
|
},
|
|
labels: labels.length > 0 ? labels : undefined,
|
|
};
|
|
}
|