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, }; }