Files
lupin-demo/src/utils.ts
2026-01-23 23:20:39 +08:00

707 lines
22 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/**
* 罗盘工具函数
* 所有函数都是纯函数,便于测试
*/
import type { PolarPoint, AnnularSectorParams, CentroidResult } from './types';
import { TEXT_LAYOUT_CONFIG } from './constants';
const DEFAULT_SECTOR_FILL = '#e5e7eb';
const TEXT_ON_LIGHT = '#111827';
const TEXT_ON_DARK = '#ffffff';
const HEX_COLOR_RE = /^#([0-9a-fA-F]{3}|[0-9a-fA-F]{6})$/;
const toLinearChannel = (channel: number): number => {
const c = channel / 255;
return c <= 0.03928 ? c / 12.92 : Math.pow((c + 0.055) / 1.055, 2.4);
};
const relativeLuminance = (r: number, g: number, b: number): number => {
const rl = toLinearChannel(r);
const gl = toLinearChannel(g);
const bl = toLinearChannel(b);
return 0.2126 * rl + 0.7152 * gl + 0.0722 * bl;
};
const parseHexColor = (input: string): { r: number; g: number; b: number } | null => {
const match = input.trim().match(HEX_COLOR_RE);
if (!match) return null;
const hex = match[1];
if (hex.length === 3) {
return {
r: parseInt(hex[0] + hex[0], 16),
g: parseInt(hex[1] + hex[1], 16),
b: parseInt(hex[2] + hex[2], 16),
};
}
return {
r: parseInt(hex.slice(0, 2), 16),
g: parseInt(hex.slice(2, 4), 16),
b: parseInt(hex.slice(4, 6), 16),
};
};
/**
* 极坐标转 SVG 坐标
* 约定:角度 aDeg0°在北上方顺时针为正
* SVG坐标x右正y下正
* @param aDeg 角度(度)
* @param r 半径
* @returns SVG 坐标点
*/
export function polarToXY(aDeg: number, r: number): PolarPoint {
const a = (aDeg * Math.PI) / 180;
return {
x: r * Math.sin(a),
y: -r * Math.cos(a)
};
}
/**
* 角度归一化到 [0, 360) 范围
* @param deg 角度
* @returns 归一化后的角度
*/
export function normalizeDeg(deg: number): number {
const d = deg % 360;
return d < 0 ? d + 360 : d;
}
/**
* 圆环扇形形心(用于放文字/图标)
* 输入rInner, rOuter, aStartDeg, aEndDeg
* 输出:形心坐标 (cx, cy) + 中心方向角 aMiddeg/rad
* @param params 扇形参数
* @returns 形心结果
*/
export function annularSectorCentroid(params: AnnularSectorParams): CentroidResult {
const { rInner, rOuter, aStartDeg, aEndDeg } = params;
const a1 = normalizeDeg(aStartDeg);
const a2 = normalizeDeg(aEndDeg);
let deltaDeg = a2 - a1;
if (deltaDeg < 0) deltaDeg += 360;
const aMidDeg = normalizeDeg(a1 + deltaDeg / 2);
const aMidRad = (aMidDeg * Math.PI) / 180;
if (rOuter <= rInner || deltaDeg === 0) {
return { cx: 0, cy: 0, rho: 0, aMidDeg, aMidRad, deltaDeg };
}
// 形心径向距离公式:`rho = (2/3) * (r2^3 - r1^3)/(r2^2 - r1^2) * sinc(delta/2)`
const delta = (deltaDeg * Math.PI) / 180;
const radialFactor =
(2 / 3) * ((rOuter ** 3 - rInner ** 3) / (rOuter ** 2 - rInner ** 2));
const half = delta / 2;
const sinc = half === 0 ? 1 : Math.sin(half) / half;
const rho = radialFactor * sinc;
const p = polarToXY(aMidDeg, rho);
return { cx: p.x, cy: p.y, rho, aMidDeg, aMidRad, deltaDeg };
}
/**
* 生成圆环扇形路径SVG path
* @param rInner 内半径
* @param rOuter 外半径
* @param aStartDeg 起始角度(度)
* @param aEndDeg 结束角度(度)
* @returns SVG path 字符串
*/
export function annularSectorPath(
rInner: number,
rOuter: number,
aStartDeg: number,
aEndDeg: number
): string {
const a1 = normalizeDeg(aStartDeg);
const a2 = normalizeDeg(aEndDeg);
let delta = a2 - a1;
if (delta < 0) delta += 360;
// `SVG` 圆弧标记位
const largeArc = delta > 180 ? 1 : 0;
const sweepOuter = 1;
const sweepInner = 0;
const p1 = polarToXY(a1, rOuter);
const p2 = polarToXY(a2, rOuter);
const p3 = polarToXY(a2, rInner);
const p4 = polarToXY(a1, rInner);
// 如果 `rInner`=0内弧退化为点
if (rInner <= 0.000001) {
return [
`M ${p1.x} ${p1.y}`,
`A ${rOuter} ${rOuter} 0 ${largeArc} ${sweepOuter} ${p2.x} ${p2.y}`,
`L 0 0`,
"Z",
].join(" ");
}
return [
`M ${p1.x} ${p1.y}`,
`A ${rOuter} ${rOuter} 0 ${largeArc} ${sweepOuter} ${p2.x} ${p2.y}`,
`L ${p3.x} ${p3.y}`,
`A ${rInner} ${rInner} 0 ${largeArc} ${sweepInner} ${p4.x} ${p4.y}`,
"Z",
].join(" ");
}
/**
* 生成内缩的圆环扇形路径(用于填色区域)
* 实现所有边界的等距离内缩
* @param rInner 内半径
* @param rOuter 外半径
* @param aStartDeg 起始角度(度)
* @param aEndDeg 结束角度(度)
* @param inset 内缩距离(像素)
* @returns SVG path 字符串
*/
export function annularSectorInsetPath(
rInner: number,
rOuter: number,
aStartDeg: number,
aEndDeg: number,
inset: number = 2
): string {
const a1 = normalizeDeg(aStartDeg);
const a2 = normalizeDeg(aEndDeg);
let delta = a2 - a1;
if (delta < 0) delta += 360;
// 半径方向内缩
const rInnerInset = rInner + inset;
const rOuterInset = rOuter - inset;
// 如果内缩后内外半径重叠,返回空路径
if (rInnerInset >= rOuterInset) return '';
// 角度方向内缩:在外圆上内缩 inset 距离
// 弧长 = 半径 × 角度(弧度),所以角度偏移 = 弧长 / 半径
const angleInsetRadOuter = inset / rOuterInset;
const angleInsetDegOuter = (angleInsetRadOuter * 180) / Math.PI;
// 角度方向内缩:在内圆上内缩 inset 距离
let angleInsetDegInner = 0;
if (rInnerInset > 0.1) {
// 正常的圆环或从圆心开始但内缩后有半径的扇区
const angleInsetRadInner = inset / rInnerInset;
angleInsetDegInner = (angleInsetRadInner * 180) / Math.PI;
} else {
// 内缩后半径接近0使用外圆的角度偏移
angleInsetDegInner = angleInsetDegOuter;
}
// 计算新的角度范围(使用外圆的角度偏移作为主要参考)
const aStartInset = a1 + angleInsetDegOuter;
const aEndInset = a2 - angleInsetDegOuter;
// 如果角度内缩后重叠,返回空路径
let deltaInset = aEndInset - aStartInset;
if (deltaInset < 0) deltaInset += 360;
if (deltaInset <= 0) return '';
// 生成内缩路径
const largeArc = deltaInset > 180 ? 1 : 0;
// 外圆:使用外圆的角度偏移
const p1Outer = polarToXY(aStartInset, rOuterInset);
const p2Outer = polarToXY(aEndInset, rOuterInset);
// 内圆:计算内圆的实际角度
const aStartInner = a1 + angleInsetDegInner;
const aEndInner = a2 - angleInsetDegInner;
const p1Inner = polarToXY(aStartInner, rInnerInset);
const p2Inner = polarToXY(aEndInner, rInnerInset);
// 统一使用相同的路径结构,无论是否从圆心开始
return [
`M ${p1Outer.x} ${p1Outer.y}`,
`A ${rOuterInset} ${rOuterInset} 0 ${largeArc} 1 ${p2Outer.x} ${p2Outer.y}`,
`L ${p2Inner.x} ${p2Inner.y}`,
`A ${rInnerInset} ${rInnerInset} 0 ${largeArc} 0 ${p1Inner.x} ${p1Inner.y}`,
"Z",
].join(" ");
}
/**
* 生成文字路径的圆弧(用于 textPath
* @param rInner 内半径
* @param rOuter 外半径
* @param aStartDeg 起始角度(度)
* @param aEndDeg 结束角度(度)
* @param textRadialPosition 文字径向位置:'centroid'(形心)或 'middle'(中点,默认)
* @returns SVG path 字符串
*/
export function generateTextPath(
rInner: number,
rOuter: number,
aStartDeg: number,
aEndDeg: number,
textRadialPosition: 'centroid' | 'middle' = 'middle'
): string {
// 根据配置选择径向位置
let rMid: number;
if (textRadialPosition === 'centroid') {
// 计算形心半径
const centroid = annularSectorCentroid({ rInner, rOuter, aStartDeg, aEndDeg });
rMid = centroid.rho;
} else {
// 使用几何中点
rMid = (rInner + rOuter) / 2;
}
// 不调整半径,保持在中线位置
// 使用 `dominant-baseline` 属性控制文字垂直对齐
const adjustedRMid = rMid;
const a1 = normalizeDeg(aStartDeg);
const a2 = normalizeDeg(aEndDeg);
let delta = a2 - a1;
if (delta < 0) delta += 360;
// 计算中间角度,自动判断是否需要反向
const aMidDeg = normalizeDeg(a1 + delta / 2);
const needReverse = aMidDeg > 90 && aMidDeg < 270;
const largeArc = delta > 180 ? 1 : 0;
// 保持路径完整,不在这里应用内边距(`padding`
// 内边距(`padding`)通过字体大小计算以及 `textPath` 的 `startOffset`/`text-anchor` 实现
if (needReverse) {
// 反向路径(从结束点到起始点),保持文字头朝外
const p1 = polarToXY(a2, adjustedRMid);
const p2 = polarToXY(a1, adjustedRMid);
return `M ${p1.x} ${p1.y} A ${adjustedRMid} ${adjustedRMid} 0 ${largeArc} 0 ${p2.x} ${p2.y}`;
} else {
// 正向路径(从起始点到结束点)
const p1 = polarToXY(a1, adjustedRMid);
const p2 = polarToXY(a2, adjustedRMid);
return `M ${p1.x} ${p1.y} A ${adjustedRMid} ${adjustedRMid} 0 ${largeArc} 1 ${p2.x} ${p2.y}`;
}
}
/**
* 生成竖排文字路径(径向方向)
* 用于宽度小于高度的扇区
* @param rInner 内半径
* @param rOuter 外半径
* @param aStartDeg 起始角度(度)
* @param aEndDeg 结束角度(度)
* @param textRadialPosition 文字径向位置:'centroid'(形心)或 'middle'(中点,默认)
* @param textLength 文字字符数(可选,用于路径长度估算)
* @param fontSize 字体大小(可选,用于路径长度估算)
* @returns SVG 路径字符串(直线)
*/
export function generateVerticalTextPath(
rInner: number,
rOuter: number,
aStartDeg: number,
aEndDeg: number,
textRadialPosition: 'centroid' | 'middle' = 'middle',
textLength?: number,
fontSize?: number
): string {
// 计算中间角度
const a1 = normalizeDeg(aStartDeg);
const a2 = normalizeDeg(aEndDeg);
let delta = a2 - a1;
if (delta < 0) delta += 360;
const aMidDeg = normalizeDeg(a1 + delta / 2);
// 根据配置选择径向位置
let rMid: number;
if (textRadialPosition === 'centroid') {
// 计算形心半径
const centroid = annularSectorCentroid({ rInner, rOuter, aStartDeg, aEndDeg });
rMid = centroid.rho;
} else {
// 使用几何中点
rMid = (rInner + rOuter) / 2;
}
// 计算径向高度
const radialHeight = rOuter - rInner;
let resolvedTextLength = textLength;
let resolvedFontSize = fontSize;
const hasTextLength = typeof resolvedTextLength === 'number';
const hasFontSize = typeof resolvedFontSize === 'number';
if (!hasTextLength && !hasFontSize) {
const tempLength = 2;
const tempFontSize = calculateSectorFontSize(
rInner,
rOuter,
aStartDeg,
aEndDeg,
tempLength,
3,
20,
true
);
resolvedTextLength = calculateVerticalTextLength(rInner, rOuter, tempFontSize);
resolvedFontSize = calculateSectorFontSize(
rInner,
rOuter,
aStartDeg,
aEndDeg,
resolvedTextLength,
3,
20,
true
);
} else if (hasTextLength && !hasFontSize) {
resolvedFontSize = calculateSectorFontSize(
rInner,
rOuter,
aStartDeg,
aEndDeg,
resolvedTextLength,
3,
20,
true
);
} else if (!hasTextLength && hasFontSize) {
resolvedTextLength = calculateVerticalTextLength(rInner, rOuter, resolvedFontSize);
}
const effectiveTextLength = resolvedTextLength ?? 0;
const effectiveFontSize = resolvedFontSize ?? TEXT_LAYOUT_CONFIG.FONT_SIZE.MIN;
// 竖排文字路径:根据扇区特点选择合适的起始位置
// 计算实际需要的路径长度:字符数 × 字符间距系数 × 字体大小
const requiredPathLength =
effectiveTextLength * TEXT_LAYOUT_CONFIG.CHAR_SPACING_RATIO * effectiveFontSize;
// 确保路径不超出扇区边界(考虑径向内边距 `padding`
const maxPathLength = radialHeight * TEXT_LAYOUT_CONFIG.RADIAL_PADDING_RATIO;
const actualPathLength = Math.min(requiredPathLength, maxPathLength);
let finalStartR: number;
let finalEndR: number;
// 对于从圆心开始的扇区(`rInner`=0形心会偏向外侧
// 需要特殊处理以防止溢出
if (rInner === 0) {
// 计算路径应该在哪里结束(从外圆向内)
// 使用形心位置作为参考,但确保路径从外圆附近开始
const halfPath = actualPathLength / 2;
// 如果使用形心居中,检查是否会溢出
if (rMid + halfPath > rOuter) {
// 会溢出:改为从外圆向内延伸
finalStartR = rOuter;
finalEndR = rOuter - actualPathLength;
} else {
// 不会溢出:使用形心居中
finalStartR = rMid + halfPath;
finalEndR = rMid - halfPath;
}
} else {
// 普通扇区:以 `rMid` 为中心
const halfPathLength = actualPathLength / 2;
// 确保不超出边界
const safeHalfPath = Math.min(
halfPathLength,
rMid - rInner,
rOuter - rMid
);
finalStartR = rMid + safeHalfPath;
finalEndR = rMid - safeHalfPath;
}
const p1 = polarToXY(aMidDeg, finalStartR);
const p2 = polarToXY(aMidDeg, finalEndR);
return `M ${p1.x} ${p1.y} L ${p2.x} ${p2.y}`;
}
/**
* 根据背景颜色亮度计算文字颜色
* 确保文字与背景有足够的对比度
* @param backgroundColor 颜色字符串,如 '#ffffff' 或 'hsl(180 70% 48%)'
* @returns 文字颜色(深色或浅色)
*/
export function getTextColorForBackground(backgroundColor: string): string {
const hex = parseHexColor(backgroundColor);
if (hex) {
const lum = relativeLuminance(hex.r, hex.g, hex.b);
return lum < 0.5 ? TEXT_ON_DARK : TEXT_ON_LIGHT;
}
const match = backgroundColor.match(/hsl\([^)]+\s+(\d+)%\)/i);
if (!match) return TEXT_ON_LIGHT;
const lightness = parseInt(match[1], 10);
return lightness < 50 ? TEXT_ON_DARK : TEXT_ON_LIGHT;
}
/**
* 计算扇区的合适字体大小
* 精确根据扇区尺寸和文字长度计算,确保文字不溢出
* @param rInner 内半径
* @param rOuter 外半径
* @param aStartDeg 起始角度(度)
* @param aEndDeg 结束角度(度)
* @param textLength 文字长度(字符数)
* @param minFontSize 最小字体大小(可选,默认 6
* @param maxFontSize 最大字体大小(可选,默认 28
* @param isVertical 是否竖排文字(可选,默认 false
* @returns 计算后的字体大小
*/
export function calculateSectorFontSize(
rInner: number,
rOuter: number,
aStartDeg: number,
aEndDeg: number,
textLength: number = 1,
minFontSize: number = TEXT_LAYOUT_CONFIG.FONT_SIZE.MIN,
maxFontSize: number = TEXT_LAYOUT_CONFIG.FONT_SIZE.MAX,
isVertical: boolean = false
): number {
if (textLength === 0) return minFontSize;
// 计算径向宽度(圆环宽度)
const radialWidth = rOuter - rInner;
// 计算角度跨度
const a1 = normalizeDeg(aStartDeg);
const a2 = normalizeDeg(aEndDeg);
let deltaDeg = a2 - a1;
if (deltaDeg < 0) deltaDeg += 360;
let calculatedSize: number;
if (isVertical) {
// 竖排文字:沿径向排列,从外圆向内圆
// 约束1径向高度所有字符的总高度
const availableHeight = radialWidth * TEXT_LAYOUT_CONFIG.RADIAL_PADDING_RATIO;
const maxByHeight = availableHeight / (textLength * TEXT_LAYOUT_CONFIG.CHAR_SPACING_RATIO);
// 约束2最内侧字符的弧长宽度这是最严格的宽度限制
// 最内侧字符的中心位置大约在 `rInner` + `fontSize`/2 处
// 保守估计:假设字体大小约为径向宽度的一半
const estimatedFontSize = radialWidth * 0.5;
const innerMostRadius = rInner + estimatedFontSize / 2;
const innerArcLength = (innerMostRadius * deltaDeg * Math.PI) / 180;
// 字符宽度约为 `fontSize` × 1.0(方块字)
const availableArcLength = innerArcLength * TEXT_LAYOUT_CONFIG.TANGENT_PADDING_RATIO;
const maxByWidth = availableArcLength / 1.0; // 单个字符宽度
// 取两个约束中的较小值
calculatedSize = Math.min(maxByHeight, maxByWidth);
} else {
// 横排文字:同时受径向宽度和弧长限制
// 计算形心半径处的弧长
const centroid = annularSectorCentroid({ rInner, rOuter, aStartDeg, aEndDeg });
const rMid = centroid.rho;
const arcLength = (rMid * deltaDeg * Math.PI) / 180;
// 1. 高度约束:字体高度不能超过径向宽度
const maxByHeight = radialWidth * TEXT_LAYOUT_CONFIG.RADIAL_PADDING_RATIO;
// 2. 宽度约束:根据文字总宽度计算
// 中文字符宽度 = `fontSize`(方块字)
// 字符间距:约 0.1 * `fontSize`,总占用约 1.1 * `fontSize`
const availableArcLength = arcLength * TEXT_LAYOUT_CONFIG.TANGENT_PADDING_RATIO;
// 反推字体大小:`fontSize` = 可用弧长 / (字符数 × 1.1)
const maxByWidth = availableArcLength / (textLength * 1.1);
// 3. 取宽度和高度约束中较小的那个(更严格的限制)
calculatedSize = Math.min(maxByHeight, maxByWidth);
}
// 对于横排小角度的扇区,需要额外限制(竖排不需要此限制)
if (!isVertical) {
// 当角度很小时,弧线弯曲明显,文字更容易溢出
const { TINY_THRESHOLD, TINY_SCALE, SMALL_THRESHOLD, SMALL_SCALE } = TEXT_LAYOUT_CONFIG.SMALL_ANGLE_SCALE;
if (deltaDeg < TINY_THRESHOLD) {
calculatedSize *= TINY_SCALE;
} else if (deltaDeg < SMALL_THRESHOLD) {
calculatedSize *= SMALL_SCALE;
}
}
// 限制在合理范围内
const finalSize = Math.max(minFontSize, Math.min(maxFontSize, calculatedSize));
return Math.round(finalSize * 10) / 10; // 保留一位小数
}
/**
* 根据扇区径向高度决定竖排文字应显示的字符数
* @param rInner 内半径
* @param rOuter 外半径
* @param fontSize 字体大小
* @returns 应显示的字符数
*/
function calculateVerticalTextLength(
rInner: number,
rOuter: number,
fontSize: number
): number {
// 从配置中读取字符数范围
const { MIN_CHARS, MAX_CHARS } = TEXT_LAYOUT_CONFIG.VERTICAL_TEXT;
// 计算径向可用高度
const radialHeight = rOuter - rInner;
// 考虑上下内边距(`padding`),可用高度约为总高度的配置比例
const availableHeight = radialHeight * TEXT_LAYOUT_CONFIG.RADIAL_PADDING_RATIO;
// 计算可以容纳的字符数
const maxFittableChars = Math.floor(availableHeight / (fontSize * TEXT_LAYOUT_CONFIG.CHAR_SPACING_RATIO));
// 限制在 [`MIN_CHARS`, `MAX_CHARS`] 范围内
const charCount = Math.max(MIN_CHARS, Math.min(MAX_CHARS, maxFittableChars));
return charCount;
}
/**
* 生成单个扇区的完整数据
* @param params 扇区参数
* @returns 扇区数据
*/
export function generateSectorData(params: {
layerIndex: number;
pieIndex: number;
rInner: number;
rOuter: number;
aStart: number;
aEnd: number;
textRadialPosition: 'centroid' | 'middle';
fill?: string;
textColor?: string;
label?: string;
}): any {
const {
layerIndex,
pieIndex,
rInner,
rOuter,
aStart,
aEnd,
textRadialPosition,
fill,
textColor,
label,
} = params;
const deltaDeg = aEnd - aStart;
const c = annularSectorCentroid({ rInner, rOuter, aStartDeg: aStart, aEndDeg: aEnd });
// 计算扇区尺寸
const radialHeight = rOuter - rInner;
const arcWidth = (c.rho * deltaDeg * Math.PI) / 180;
// 判断是否需要竖排
const isVertical = arcWidth < radialHeight;
const providedLabel = typeof label === 'string' && label.length > 0 ? label : undefined;
const providedLength = providedLabel ? providedLabel.length : undefined;
// 计算文字长度和字体大小
let textLength: number;
let sectorFontSize: number;
if (isVertical) {
if (typeof providedLength === 'number') {
textLength = providedLength;
sectorFontSize = calculateSectorFontSize(rInner, rOuter, aStart, aEnd, textLength, 3, 20, true);
} else {
const tempLength = 2;
const tempFontSize = calculateSectorFontSize(rInner, rOuter, aStart, aEnd, tempLength, 3, 20, true);
textLength = calculateVerticalTextLength(rInner, rOuter, tempFontSize);
sectorFontSize = calculateSectorFontSize(rInner, rOuter, aStart, aEnd, textLength, 3, 20, true);
}
} else {
if (typeof providedLength === 'number') {
textLength = providedLength;
} else {
const { RADIAL_PADDING_RATIO, CHAR_SPACING_RATIO, TANGENT_PADDING_RATIO } = TEXT_LAYOUT_CONFIG;
const { MIN_CHARS, MAX_CHARS } = TEXT_LAYOUT_CONFIG.HORIZONTAL_TEXT;
const estimatedFontSize = radialHeight * RADIAL_PADDING_RATIO;
const charWidth = estimatedFontSize * CHAR_SPACING_RATIO;
const availableWidth = arcWidth * TANGENT_PADDING_RATIO;
const maxChars = Math.floor(availableWidth / charWidth);
textLength = Math.max(MIN_CHARS, Math.min(MAX_CHARS, maxChars));
}
sectorFontSize = calculateSectorFontSize(rInner, rOuter, aStart, aEnd, textLength);
}
const finalLabel = providedLabel ?? '测'.repeat(textLength);
const textPathId = `text-path-L${layerIndex}-P${pieIndex}`;
// 最内层使用形心位置
const isInnermostLayer = layerIndex === 0;
const effectiveTextRadialPosition = isInnermostLayer ? 'centroid' : textRadialPosition;
// 生成文字路径(路径方向已经在函数内部自动处理)
const textPath = isVertical
? generateVerticalTextPath(
rInner,
rOuter,
aStart,
aEnd,
effectiveTextRadialPosition,
textLength,
sectorFontSize
)
: generateTextPath(rInner, rOuter, aStart, aEnd, effectiveTextRadialPosition);
// 生成颜色
const fillColor = fill ?? DEFAULT_SECTOR_FILL;
const baseTextColor = textColor ?? getTextColorForBackground(fillColor);
// 内部填色逻辑
const shouldFill = (pieIndex + layerIndex) % 3 === 0;
const innerFillPath = shouldFill ? annularSectorInsetPath(rInner, rOuter, aStart, aEnd, 1) : undefined;
const innerFillColor = shouldFill ? fillColor : undefined;
const baseFillColor = shouldFill ? '#ffffff' : fillColor;
const finalTextColor = shouldFill ? TEXT_ON_LIGHT : baseTextColor;
return {
key: `L${layerIndex}-P${pieIndex}`,
layerIndex,
pieIndex,
rInner,
rOuter,
aStart,
aEnd,
aMidDeg: c.aMidDeg,
aMidRad: c.aMidRad,
cx: c.cx,
cy: c.cy,
fill: baseFillColor,
textColor: finalTextColor,
label: finalLabel,
path: annularSectorPath(rInner, rOuter, aStart, aEnd),
innerFillPath,
innerFillColor,
textPath,
textPathId,
isVertical,
fontSize: sectorFontSize,
};
}