update at 2026-01-21 21:48:08
This commit is contained in:
5
package-lock.json
generated
5
package-lock.json
generated
@@ -1027,6 +1027,7 @@
|
|||||||
"integrity": "sha512-xa57bCPGuzEFqGjPs3vVLyqareG8DX0uMkr5U/v5vLv5/ZUrBrPL7gzxzTJedEyZxFMfsozwTIbbYfEQVo3kgg==",
|
"integrity": "sha512-xa57bCPGuzEFqGjPs3vVLyqareG8DX0uMkr5U/v5vLv5/ZUrBrPL7gzxzTJedEyZxFMfsozwTIbbYfEQVo3kgg==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@vitest/utils": "1.6.1",
|
"@vitest/utils": "1.6.1",
|
||||||
"fast-glob": "^3.3.2",
|
"fast-glob": "^3.3.2",
|
||||||
@@ -2756,6 +2757,7 @@
|
|||||||
"integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
|
"integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
|
||||||
"devOptional": true,
|
"devOptional": true,
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
|
"peer": true,
|
||||||
"bin": {
|
"bin": {
|
||||||
"tsc": "bin/tsc",
|
"tsc": "bin/tsc",
|
||||||
"tsserver": "bin/tsserver"
|
"tsserver": "bin/tsserver"
|
||||||
@@ -2784,6 +2786,7 @@
|
|||||||
"integrity": "sha512-o5a9xKjbtuhY6Bi5S3+HvbRERmouabWbyUcpXXUA1u+GNUKoROi9byOJ8M0nHbHYHkYICiMlqxkg1KkYmm25Sw==",
|
"integrity": "sha512-o5a9xKjbtuhY6Bi5S3+HvbRERmouabWbyUcpXXUA1u+GNUKoROi9byOJ8M0nHbHYHkYICiMlqxkg1KkYmm25Sw==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"esbuild": "^0.21.3",
|
"esbuild": "^0.21.3",
|
||||||
"postcss": "^8.4.43",
|
"postcss": "^8.4.43",
|
||||||
@@ -2867,6 +2870,7 @@
|
|||||||
"integrity": "sha512-Ljb1cnSJSivGN0LqXd/zmDbWEM0RNNg2t1QW/XUhYl/qPqyu7CsqeWtqQXHVaJsecLPuDoak2oJcZN2QoRIOag==",
|
"integrity": "sha512-Ljb1cnSJSivGN0LqXd/zmDbWEM0RNNg2t1QW/XUhYl/qPqyu7CsqeWtqQXHVaJsecLPuDoak2oJcZN2QoRIOag==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@vitest/expect": "1.6.1",
|
"@vitest/expect": "1.6.1",
|
||||||
"@vitest/runner": "1.6.1",
|
"@vitest/runner": "1.6.1",
|
||||||
@@ -2932,6 +2936,7 @@
|
|||||||
"resolved": "https://registry.npmjs.org/vue/-/vue-3.5.26.tgz",
|
"resolved": "https://registry.npmjs.org/vue/-/vue-3.5.26.tgz",
|
||||||
"integrity": "sha512-SJ/NTccVyAoNUJmkM9KUqPcYlY+u8OVL1X5EW9RIs3ch5H2uERxyyIUI4MRxVCSOiEcupX9xNGde1tL9ZKpimA==",
|
"integrity": "sha512-SJ/NTccVyAoNUJmkM9KUqPcYlY+u8OVL1X5EW9RIs3ch5H2uERxyyIUI4MRxVCSOiEcupX9xNGde1tL9ZKpimA==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@vue/compiler-dom": "3.5.26",
|
"@vue/compiler-dom": "3.5.26",
|
||||||
"@vue/compiler-sfc": "3.5.26",
|
"@vue/compiler-sfc": "3.5.26",
|
||||||
|
|||||||
@@ -206,7 +206,7 @@ const currentExample = computed(() => examples[exampleIndex.value]);
|
|||||||
/**
|
/**
|
||||||
* 使用罗盘逻辑
|
* 使用罗盘逻辑
|
||||||
*/
|
*/
|
||||||
const { anglesDeg, rings, outerMost, sectors, getLabelTransform, toXY } =
|
const { anglesDeg, rings, outerMost, sectors, toXY } =
|
||||||
useLuopan(currentExample, textRadialPosition);
|
useLuopan(currentExample, textRadialPosition);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -6,7 +6,6 @@ import { computed, type Ref, type ComputedRef } from 'vue';
|
|||||||
import type { Example, Sector, TextRadialPosition } from '../types';
|
import type { Example, Sector, TextRadialPosition } from '../types';
|
||||||
import {
|
import {
|
||||||
polarToXY,
|
polarToXY,
|
||||||
calculateLabelRotation,
|
|
||||||
generateSectorData,
|
generateSectorData,
|
||||||
} from '../utils';
|
} from '../utils';
|
||||||
|
|
||||||
@@ -64,8 +63,6 @@ export function useLuopan(
|
|||||||
rOuter,
|
rOuter,
|
||||||
aStart,
|
aStart,
|
||||||
aEnd,
|
aEnd,
|
||||||
layerCount,
|
|
||||||
pieCount,
|
|
||||||
textRadialPosition: textRadialPositionRef.value,
|
textRadialPosition: textRadialPositionRef.value,
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -75,16 +72,6 @@ export function useLuopan(
|
|||||||
return res;
|
return res;
|
||||||
});
|
});
|
||||||
|
|
||||||
/**
|
|
||||||
* 计算标签的变换属性
|
|
||||||
* @param s 扇区数据
|
|
||||||
* @returns SVG transform 字符串
|
|
||||||
*/
|
|
||||||
const getLabelTransform = (s: Sector): string => {
|
|
||||||
const rotDeg = calculateLabelRotation(s.aMidDeg);
|
|
||||||
return `translate(${s.cx} ${s.cy}) rotate(${rotDeg})`;
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 极坐标转 XY(暴露给模板使用)
|
* 极坐标转 XY(暴露给模板使用)
|
||||||
*/
|
*/
|
||||||
@@ -95,7 +82,6 @@ export function useLuopan(
|
|||||||
rings,
|
rings,
|
||||||
outerMost,
|
outerMost,
|
||||||
sectors,
|
sectors,
|
||||||
getLabelTransform,
|
|
||||||
toXY,
|
toXY,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -21,8 +21,6 @@ export {
|
|||||||
annularSectorCentroid,
|
annularSectorCentroid,
|
||||||
annularSectorPath,
|
annularSectorPath,
|
||||||
annularSectorInsetPath,
|
annularSectorInsetPath,
|
||||||
calculateLabelRotation,
|
|
||||||
generateSectorColor,
|
|
||||||
generateTextPath,
|
generateTextPath,
|
||||||
generateVerticalTextPath,
|
generateVerticalTextPath,
|
||||||
getTextColorForBackground,
|
getTextColorForBackground,
|
||||||
|
|||||||
264
src/utils.ts
264
src/utils.ts
@@ -6,6 +6,43 @@
|
|||||||
import type { PolarPoint, AnnularSectorParams, CentroidResult } from './types';
|
import type { PolarPoint, AnnularSectorParams, CentroidResult } from './types';
|
||||||
import { TEXT_LAYOUT_CONFIG } from './constants';
|
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 坐标
|
* 极坐标转 SVG 坐标
|
||||||
* 约定:角度 aDeg:0°在北(上方),顺时针为正
|
* 约定:角度 aDeg:0°在北(上方),顺时针为正
|
||||||
@@ -195,21 +232,6 @@ export function annularSectorInsetPath(
|
|||||||
].join(" ");
|
].join(" ");
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* 计算文字旋转角度
|
|
||||||
* - 文字沿径向方向:rot = aMid(头朝外、脚朝圆心)
|
|
||||||
* - 为避免倒着读:当角度在 (180°, 360°) 之间时翻转 180°
|
|
||||||
* @param aMidDeg 中间角度(度)
|
|
||||||
* @returns 旋转角度
|
|
||||||
*/
|
|
||||||
export function calculateLabelRotation(aMidDeg: number): number {
|
|
||||||
let rotDeg = aMidDeg;
|
|
||||||
if (aMidDeg > 180 && aMidDeg < 360) {
|
|
||||||
rotDeg += 180;
|
|
||||||
}
|
|
||||||
return rotDeg;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 生成文字路径的圆弧(用于 textPath)
|
* 生成文字路径的圆弧(用于 textPath)
|
||||||
* @param rInner 内半径
|
* @param rInner 内半径
|
||||||
@@ -269,19 +291,6 @@ export function generateTextPath(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* 生成竖排文字路径(径向方向)
|
|
||||||
* 用于宽度小于高度的扇区
|
|
||||||
* @param rInner 内半径
|
|
||||||
* @param rOuter 外半径
|
|
||||||
* @param aStartDeg 起始角度(度)
|
|
||||||
* @param aEndDeg 结束角度(度)
|
|
||||||
* @param aMidDeg 中心角度
|
|
||||||
* @param textRadialPosition 文字径向位置:'centroid'(形心)或 'middle'(中点,默认)
|
|
||||||
* @param fontSize 字体大小
|
|
||||||
* @param textLength 文字字符数(用于计算路径长度,默认4)
|
|
||||||
* @returns SVG path 字符串(径向直线)
|
|
||||||
*/
|
|
||||||
/**
|
/**
|
||||||
* 生成竖排文字路径(径向方向)
|
* 生成竖排文字路径(径向方向)
|
||||||
* 用于宽度小于高度的扇区
|
* 用于宽度小于高度的扇区
|
||||||
@@ -290,6 +299,8 @@ export function generateTextPath(
|
|||||||
* @param aStartDeg 起始角度(度)
|
* @param aStartDeg 起始角度(度)
|
||||||
* @param aEndDeg 结束角度(度)
|
* @param aEndDeg 结束角度(度)
|
||||||
* @param textRadialPosition 文字径向位置:'centroid'(形心)或 'middle'(中点,默认)
|
* @param textRadialPosition 文字径向位置:'centroid'(形心)或 'middle'(中点,默认)
|
||||||
|
* @param textLength 文字字符数(可选,用于路径长度估算)
|
||||||
|
* @param fontSize 字体大小(可选,用于路径长度估算)
|
||||||
* @returns SVG 路径字符串(直线)
|
* @returns SVG 路径字符串(直线)
|
||||||
*/
|
*/
|
||||||
export function generateVerticalTextPath(
|
export function generateVerticalTextPath(
|
||||||
@@ -297,7 +308,9 @@ export function generateVerticalTextPath(
|
|||||||
rOuter: number,
|
rOuter: number,
|
||||||
aStartDeg: number,
|
aStartDeg: number,
|
||||||
aEndDeg: number,
|
aEndDeg: number,
|
||||||
textRadialPosition: 'centroid' | 'middle' = 'middle'
|
textRadialPosition: 'centroid' | 'middle' = 'middle',
|
||||||
|
textLength?: number,
|
||||||
|
fontSize?: number
|
||||||
): string {
|
): string {
|
||||||
// 计算中间角度
|
// 计算中间角度
|
||||||
const a1 = normalizeDeg(aStartDeg);
|
const a1 = normalizeDeg(aStartDeg);
|
||||||
@@ -317,22 +330,59 @@ export function generateVerticalTextPath(
|
|||||||
rMid = (rInner + rOuter) / 2;
|
rMid = (rInner + rOuter) / 2;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 计算径向高度和字体大小
|
// 计算径向高度
|
||||||
const radialHeight = rOuter - rInner;
|
const radialHeight = rOuter - rInner;
|
||||||
|
|
||||||
// 计算字体大小(竖排)
|
let resolvedTextLength = textLength;
|
||||||
const tempLength = 2; // 先假设2个字
|
let resolvedFontSize = fontSize;
|
||||||
const tempFontSize = calculateSectorFontSize(rInner, rOuter, aStartDeg, aEndDeg, tempLength, 3, 20, true);
|
const hasTextLength = typeof resolvedTextLength === 'number';
|
||||||
|
const hasFontSize = typeof resolvedFontSize === 'number';
|
||||||
// 根据字体大小决定实际字符数
|
|
||||||
const textLength = calculateVerticalTextLength(rInner, rOuter, tempFontSize);
|
if (!hasTextLength && !hasFontSize) {
|
||||||
|
const tempLength = 2;
|
||||||
// 用实际字符数重新计算字体大小
|
const tempFontSize = calculateSectorFontSize(
|
||||||
const fontSize = calculateSectorFontSize(rInner, rOuter, aStartDeg, aEndDeg, textLength, 3, 20, true);
|
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 = textLength * TEXT_LAYOUT_CONFIG.CHAR_SPACING_RATIO * fontSize;
|
const requiredPathLength =
|
||||||
|
effectiveTextLength * TEXT_LAYOUT_CONFIG.CHAR_SPACING_RATIO * effectiveFontSize;
|
||||||
|
|
||||||
// 确保路径不超出扇区边界(考虑径向 padding)
|
// 确保路径不超出扇区边界(考虑径向 padding)
|
||||||
const maxPathLength = radialHeight * TEXT_LAYOUT_CONFIG.RADIAL_PADDING_RATIO;
|
const maxPathLength = radialHeight * TEXT_LAYOUT_CONFIG.RADIAL_PADDING_RATIO;
|
||||||
@@ -379,54 +429,24 @@ export function generateVerticalTextPath(
|
|||||||
return `M ${p1.x} ${p1.y} L ${p2.x} ${p2.y}`;
|
return `M ${p1.x} ${p1.y} L ${p2.x} ${p2.y}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* 生成扇区颜色
|
|
||||||
* @param layerIndex 层索引
|
|
||||||
* @param pieIndex 扇区索引
|
|
||||||
* @param totalLayers 总层数
|
|
||||||
* @param totalPies 总扇区数(可选,默认24)
|
|
||||||
* @returns HSL 颜色字符串
|
|
||||||
*/
|
|
||||||
export function generateSectorColor(
|
|
||||||
layerIndex: number,
|
|
||||||
pieIndex: number,
|
|
||||||
totalLayers: number = 10,
|
|
||||||
totalPies: number = 24
|
|
||||||
): string {
|
|
||||||
const hue = (pieIndex * 360) / totalPies;
|
|
||||||
|
|
||||||
// 根据总层数动态调整亮度范围
|
|
||||||
// 最浅:85%,最深:25%
|
|
||||||
// 使用线性插值,让颜色分布更均匀
|
|
||||||
const maxLight = 85;
|
|
||||||
const minLight = 25;
|
|
||||||
const lightRange = maxLight - minLight;
|
|
||||||
|
|
||||||
// 计算当前层的亮度比例(0到1)
|
|
||||||
const ratio = totalLayers > 1 ? layerIndex / (totalLayers - 1) : 0;
|
|
||||||
|
|
||||||
// 从浅到深:最内层最浅,最外层最深
|
|
||||||
const light = maxLight - (lightRange * ratio);
|
|
||||||
|
|
||||||
return `hsl(${hue} 70% ${Math.round(light)}%)`;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 根据背景颜色亮度计算文字颜色
|
* 根据背景颜色亮度计算文字颜色
|
||||||
* 确保文字与背景有足够的对比度
|
* 确保文字与背景有足够的对比度
|
||||||
* @param backgroundColor HSL 颜色字符串,如 'hsl(180 70% 48%)'
|
* @param backgroundColor 颜色字符串,如 '#ffffff' 或 'hsl(180 70% 48%)'
|
||||||
* @returns 文字颜色(深色或浅色)
|
* @returns 文字颜色(深色或浅色)
|
||||||
*/
|
*/
|
||||||
export function getTextColorForBackground(backgroundColor: string): string {
|
export function getTextColorForBackground(backgroundColor: string): string {
|
||||||
// 从 HSL 字符串中提取亮度值
|
const hex = parseHexColor(backgroundColor);
|
||||||
const match = backgroundColor.match(/hsl\([^)]+\s+(\d+)%\)/);
|
if (hex) {
|
||||||
if (!match) return '#111827'; // 默认深色
|
const lum = relativeLuminance(hex.r, hex.g, hex.b);
|
||||||
|
return lum < 0.5 ? TEXT_ON_DARK : TEXT_ON_LIGHT;
|
||||||
const lightness = parseInt(match[1]);
|
}
|
||||||
|
|
||||||
// 亮度阈值:50%
|
const match = backgroundColor.match(/hsl\([^)]+\s+(\d+)%\)/i);
|
||||||
// 亮度低于50%使用白色文字,否则使用深色文字
|
if (!match) return TEXT_ON_LIGHT;
|
||||||
return lightness < 50 ? '#ffffff' : '#111827';
|
|
||||||
|
const lightness = parseInt(match[1], 10);
|
||||||
|
return lightness < 50 ? TEXT_ON_DARK : TEXT_ON_LIGHT;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -530,7 +550,7 @@ export function calculateSectorFontSize(
|
|||||||
* @param fontSize 字体大小
|
* @param fontSize 字体大小
|
||||||
* @returns 应显示的字符数
|
* @returns 应显示的字符数
|
||||||
*/
|
*/
|
||||||
export function calculateVerticalTextLength(
|
function calculateVerticalTextLength(
|
||||||
rInner: number,
|
rInner: number,
|
||||||
rOuter: number,
|
rOuter: number,
|
||||||
fontSize: number
|
fontSize: number
|
||||||
@@ -565,11 +585,23 @@ export function generateSectorData(params: {
|
|||||||
rOuter: number;
|
rOuter: number;
|
||||||
aStart: number;
|
aStart: number;
|
||||||
aEnd: number;
|
aEnd: number;
|
||||||
layerCount: number;
|
|
||||||
pieCount: number;
|
|
||||||
textRadialPosition: 'centroid' | 'middle';
|
textRadialPosition: 'centroid' | 'middle';
|
||||||
|
fill?: string;
|
||||||
|
textColor?: string;
|
||||||
|
label?: string;
|
||||||
}): any {
|
}): any {
|
||||||
const { layerIndex, pieIndex, rInner, rOuter, aStart, aEnd, layerCount, pieCount, textRadialPosition } = params;
|
const {
|
||||||
|
layerIndex,
|
||||||
|
pieIndex,
|
||||||
|
rInner,
|
||||||
|
rOuter,
|
||||||
|
aStart,
|
||||||
|
aEnd,
|
||||||
|
textRadialPosition,
|
||||||
|
fill,
|
||||||
|
textColor,
|
||||||
|
label,
|
||||||
|
} = params;
|
||||||
|
|
||||||
const deltaDeg = aEnd - aStart;
|
const deltaDeg = aEnd - aStart;
|
||||||
const c = annularSectorCentroid({ rInner, rOuter, aStartDeg: aStart, aEndDeg: aEnd });
|
const c = annularSectorCentroid({ rInner, rOuter, aStartDeg: aStart, aEndDeg: aEnd });
|
||||||
@@ -580,32 +612,42 @@ export function generateSectorData(params: {
|
|||||||
|
|
||||||
// 判断是否需要竖排
|
// 判断是否需要竖排
|
||||||
const isVertical = arcWidth < radialHeight;
|
const isVertical = arcWidth < radialHeight;
|
||||||
|
|
||||||
|
const providedLabel = typeof label === 'string' && label.length > 0 ? label : undefined;
|
||||||
|
const providedLength = providedLabel ? providedLabel.length : undefined;
|
||||||
|
|
||||||
// 计算文字长度和字体大小
|
// 计算文字长度和字体大小
|
||||||
let textLength: number;
|
let textLength: number;
|
||||||
let sectorFontSize: number;
|
let sectorFontSize: number;
|
||||||
|
|
||||||
if (isVertical) {
|
if (isVertical) {
|
||||||
// 竖排逻辑
|
if (typeof providedLength === 'number') {
|
||||||
const tempLength = 2;
|
textLength = providedLength;
|
||||||
const tempFontSize = calculateSectorFontSize(rInner, rOuter, aStart, aEnd, tempLength, 3, 20, true);
|
sectorFontSize = calculateSectorFontSize(rInner, rOuter, aStart, aEnd, textLength, 3, 20, true);
|
||||||
textLength = calculateVerticalTextLength(rInner, rOuter, tempFontSize);
|
} else {
|
||||||
sectorFontSize = calculateSectorFontSize(rInner, rOuter, aStart, aEnd, textLength, 3, 20, true);
|
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 {
|
} else {
|
||||||
// 横排逻辑
|
if (typeof providedLength === 'number') {
|
||||||
const { RADIAL_PADDING_RATIO, CHAR_SPACING_RATIO, TANGENT_PADDING_RATIO } = TEXT_LAYOUT_CONFIG;
|
textLength = providedLength;
|
||||||
const { MIN_CHARS, MAX_CHARS } = TEXT_LAYOUT_CONFIG.HORIZONTAL_TEXT;
|
} else {
|
||||||
|
const { RADIAL_PADDING_RATIO, CHAR_SPACING_RATIO, TANGENT_PADDING_RATIO } = TEXT_LAYOUT_CONFIG;
|
||||||
const estimatedFontSize = radialHeight * RADIAL_PADDING_RATIO;
|
const { MIN_CHARS, MAX_CHARS } = TEXT_LAYOUT_CONFIG.HORIZONTAL_TEXT;
|
||||||
const charWidth = estimatedFontSize * CHAR_SPACING_RATIO;
|
|
||||||
const availableWidth = arcWidth * TANGENT_PADDING_RATIO;
|
const estimatedFontSize = radialHeight * RADIAL_PADDING_RATIO;
|
||||||
const maxChars = Math.floor(availableWidth / charWidth);
|
const charWidth = estimatedFontSize * CHAR_SPACING_RATIO;
|
||||||
|
const availableWidth = arcWidth * TANGENT_PADDING_RATIO;
|
||||||
textLength = Math.max(MIN_CHARS, Math.min(MAX_CHARS, maxChars));
|
const maxChars = Math.floor(availableWidth / charWidth);
|
||||||
|
|
||||||
|
textLength = Math.max(MIN_CHARS, Math.min(MAX_CHARS, maxChars));
|
||||||
|
}
|
||||||
sectorFontSize = calculateSectorFontSize(rInner, rOuter, aStart, aEnd, textLength);
|
sectorFontSize = calculateSectorFontSize(rInner, rOuter, aStart, aEnd, textLength);
|
||||||
}
|
}
|
||||||
|
|
||||||
const label = '测'.repeat(textLength);
|
const finalLabel = providedLabel ?? '测'.repeat(textLength);
|
||||||
|
|
||||||
const textPathId = `text-path-L${layerIndex}-P${pieIndex}`;
|
const textPathId = `text-path-L${layerIndex}-P${pieIndex}`;
|
||||||
|
|
||||||
@@ -615,12 +657,20 @@ export function generateSectorData(params: {
|
|||||||
|
|
||||||
// 生成文字路径(路径方向已经在函数内部自动处理)
|
// 生成文字路径(路径方向已经在函数内部自动处理)
|
||||||
const textPath = isVertical
|
const textPath = isVertical
|
||||||
? generateVerticalTextPath(rInner, rOuter, aStart, aEnd, effectiveTextRadialPosition)
|
? generateVerticalTextPath(
|
||||||
|
rInner,
|
||||||
|
rOuter,
|
||||||
|
aStart,
|
||||||
|
aEnd,
|
||||||
|
effectiveTextRadialPosition,
|
||||||
|
textLength,
|
||||||
|
sectorFontSize
|
||||||
|
)
|
||||||
: generateTextPath(rInner, rOuter, aStart, aEnd, effectiveTextRadialPosition);
|
: generateTextPath(rInner, rOuter, aStart, aEnd, effectiveTextRadialPosition);
|
||||||
|
|
||||||
// 生成颜色
|
// 生成颜色
|
||||||
const fillColor = generateSectorColor(layerIndex, pieIndex, layerCount, pieCount);
|
const fillColor = fill ?? DEFAULT_SECTOR_FILL;
|
||||||
const textColor = getTextColorForBackground(fillColor);
|
const baseTextColor = textColor ?? getTextColorForBackground(fillColor);
|
||||||
|
|
||||||
// 内部填色逻辑
|
// 内部填色逻辑
|
||||||
const shouldFill = (pieIndex + layerIndex) % 3 === 0;
|
const shouldFill = (pieIndex + layerIndex) % 3 === 0;
|
||||||
@@ -628,7 +678,7 @@ export function generateSectorData(params: {
|
|||||||
const innerFillColor = shouldFill ? fillColor : undefined;
|
const innerFillColor = shouldFill ? fillColor : undefined;
|
||||||
|
|
||||||
const baseFillColor = shouldFill ? '#ffffff' : fillColor;
|
const baseFillColor = shouldFill ? '#ffffff' : fillColor;
|
||||||
const finalTextColor = shouldFill ? '#111827' : textColor;
|
const finalTextColor = shouldFill ? TEXT_ON_LIGHT : baseTextColor;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
key: `L${layerIndex}-P${pieIndex}`,
|
key: `L${layerIndex}-P${pieIndex}`,
|
||||||
@@ -644,7 +694,7 @@ export function generateSectorData(params: {
|
|||||||
cy: c.cy,
|
cy: c.cy,
|
||||||
fill: baseFillColor,
|
fill: baseFillColor,
|
||||||
textColor: finalTextColor,
|
textColor: finalTextColor,
|
||||||
label,
|
label: finalLabel,
|
||||||
path: annularSectorPath(rInner, rOuter, aStart, aEnd),
|
path: annularSectorPath(rInner, rOuter, aStart, aEnd),
|
||||||
innerFillPath,
|
innerFillPath,
|
||||||
innerFillColor,
|
innerFillColor,
|
||||||
|
|||||||
@@ -22,26 +22,23 @@ tests/
|
|||||||
- 角度数组和半径数组的有效性验证
|
- 角度数组和半径数组的有效性验证
|
||||||
- 不同类型示例的存在性检查
|
- 不同类型示例的存在性检查
|
||||||
|
|
||||||
### 2. utils.test.ts (48 个测试)
|
### 2. utils.test.ts (39 个测试)
|
||||||
测试所有工具函数的核心逻辑:
|
测试所有工具函数的核心逻辑:
|
||||||
- **polarToXY**: 极坐标转换
|
- **polarToXY**: 极坐标转换
|
||||||
- **normalizeDeg**: 角度归一化
|
- **normalizeDeg**: 角度归一化
|
||||||
- **annularSectorCentroid**: 扇形形心计算
|
- **annularSectorCentroid**: 扇形形心计算
|
||||||
- **annularSectorPath**: SVG 路径生成
|
- **annularSectorPath**: SVG 路径生成
|
||||||
- **annularSectorInsetPath**: 内缩路径生成
|
- **annularSectorInsetPath**: 内缩路径生成
|
||||||
- **calculateLabelRotation**: 文字旋转角度
|
|
||||||
- **generateSectorColor**: 扇区颜色生成
|
|
||||||
- **generateTextPath**: 文字路径生成
|
- **generateTextPath**: 文字路径生成
|
||||||
- **generateVerticalTextPath**: 竖排文字路径
|
- **generateVerticalTextPath**: 竖排文字路径
|
||||||
- **getTextColorForBackground**: 文字颜色适配
|
- **getTextColorForBackground**: 文字颜色适配
|
||||||
- **calculateSectorFontSize**: 字体大小计算
|
- **calculateSectorFontSize**: 字体大小计算
|
||||||
|
|
||||||
### 3. useLuopan.test.ts (32 个测试)
|
### 3. useLuopan.test.ts (29 个测试)
|
||||||
测试罗盘业务逻辑组合函数:
|
测试罗盘业务逻辑组合函数:
|
||||||
- 基本功能和返回值验证
|
- 基本功能和返回值验证
|
||||||
- 扇区生成逻辑
|
- 扇区生成逻辑
|
||||||
- 文字位置模式(middle/centroid)
|
- 文字位置模式(middle/centroid)
|
||||||
- 文字方向判断
|
|
||||||
- 竖排文字判断
|
- 竖排文字判断
|
||||||
- 内部填色逻辑
|
- 内部填色逻辑
|
||||||
- 响应式更新
|
- 响应式更新
|
||||||
@@ -90,7 +87,7 @@ npm test -- --coverage
|
|||||||
## 测试统计
|
## 测试统计
|
||||||
|
|
||||||
- **总测试文件**: 4 个
|
- **总测试文件**: 4 个
|
||||||
- **总测试用例**: 159 个
|
- **总测试用例**: 147 个
|
||||||
- **测试通过率**: 100%
|
- **测试通过率**: 100%
|
||||||
- **测试运行时间**: ~1.8 秒
|
- **测试运行时间**: ~1.8 秒
|
||||||
|
|
||||||
|
|||||||
@@ -25,7 +25,6 @@ describe('useLuopan', () => {
|
|||||||
expect(result).toHaveProperty('rings');
|
expect(result).toHaveProperty('rings');
|
||||||
expect(result).toHaveProperty('outerMost');
|
expect(result).toHaveProperty('outerMost');
|
||||||
expect(result).toHaveProperty('sectors');
|
expect(result).toHaveProperty('sectors');
|
||||||
expect(result).toHaveProperty('getLabelTransform');
|
|
||||||
expect(result).toHaveProperty('toXY');
|
expect(result).toHaveProperty('toXY');
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -92,7 +91,6 @@ describe('useLuopan', () => {
|
|||||||
expect(sector).toHaveProperty('path');
|
expect(sector).toHaveProperty('path');
|
||||||
expect(sector).toHaveProperty('textPath');
|
expect(sector).toHaveProperty('textPath');
|
||||||
expect(sector).toHaveProperty('textPathId');
|
expect(sector).toHaveProperty('textPathId');
|
||||||
expect(sector).toHaveProperty('needReverse');
|
|
||||||
expect(sector).toHaveProperty('isVertical');
|
expect(sector).toHaveProperty('isVertical');
|
||||||
expect(sector).toHaveProperty('fontSize');
|
expect(sector).toHaveProperty('fontSize');
|
||||||
});
|
});
|
||||||
@@ -161,8 +159,8 @@ describe('useLuopan', () => {
|
|||||||
const { sectors } = useLuopan(example, textRadialPosition);
|
const { sectors } = useLuopan(example, textRadialPosition);
|
||||||
|
|
||||||
sectors.value.forEach((sector) => {
|
sectors.value.forEach((sector) => {
|
||||||
expect(sector.fill).toMatch(/^(hsl\(.*\)|#[0-9a-fA-F]{6}|#ffffff)$/);
|
expect(sector.fill).toMatch(/^#[0-9a-fA-F]{6}$/);
|
||||||
expect(sector.textColor).toMatch(/^(#[0-9a-fA-F]{6}|#ffffff)$/);
|
expect(sector.textColor).toMatch(/^#[0-9a-fA-F]{6}$/);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -248,38 +246,6 @@ describe('useLuopan', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('文字方向', () => {
|
|
||||||
it('应该为左半圆的扇区设置 needReverse', () => {
|
|
||||||
const example = ref(createMockExample());
|
|
||||||
const textRadialPosition = ref<TextRadialPosition>('middle');
|
|
||||||
|
|
||||||
const { sectors } = useLuopan(example, textRadialPosition);
|
|
||||||
|
|
||||||
// 中间角度在 (90, 270) 范围内的扇区应该需要反向
|
|
||||||
const leftSectors = sectors.value.filter(
|
|
||||||
(s) => s.aMidDeg > 90 && s.aMidDeg < 270
|
|
||||||
);
|
|
||||||
leftSectors.forEach((sector) => {
|
|
||||||
expect(sector.needReverse).toBe(true);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
it('应该为右半圆的扇区不设置 needReverse', () => {
|
|
||||||
const example = ref(createMockExample());
|
|
||||||
const textRadialPosition = ref<TextRadialPosition>('middle');
|
|
||||||
|
|
||||||
const { sectors } = useLuopan(example, textRadialPosition);
|
|
||||||
|
|
||||||
// 中间角度在 [0, 90] 或 [270, 360] 范围内的扇区不需要反向
|
|
||||||
const rightSectors = sectors.value.filter(
|
|
||||||
(s) => s.aMidDeg <= 90 || s.aMidDeg >= 270
|
|
||||||
);
|
|
||||||
rightSectors.forEach((sector) => {
|
|
||||||
expect(sector.needReverse).toBe(false);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('竖排文字判断', () => {
|
describe('竖排文字判断', () => {
|
||||||
it('应该为窄扇区设置 isVertical', () => {
|
it('应该为窄扇区设置 isVertical', () => {
|
||||||
const example = ref({
|
const example = ref({
|
||||||
@@ -384,21 +350,6 @@ describe('useLuopan', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('getLabelTransform', () => {
|
|
||||||
it('应该返回有效的 SVG transform 字符串', () => {
|
|
||||||
const example = ref(createMockExample());
|
|
||||||
const textRadialPosition = ref<TextRadialPosition>('middle');
|
|
||||||
|
|
||||||
const { sectors, getLabelTransform } = useLuopan(example, textRadialPosition);
|
|
||||||
|
|
||||||
const sector = sectors.value[0];
|
|
||||||
const transform = getLabelTransform(sector);
|
|
||||||
|
|
||||||
expect(transform).toContain('translate');
|
|
||||||
expect(transform).toContain('rotate');
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('toXY', () => {
|
describe('toXY', () => {
|
||||||
it('应该正确转换极坐标', () => {
|
it('应该正确转换极坐标', () => {
|
||||||
const example = ref(createMockExample());
|
const example = ref(createMockExample());
|
||||||
|
|||||||
@@ -10,8 +10,6 @@ import {
|
|||||||
annularSectorCentroid,
|
annularSectorCentroid,
|
||||||
annularSectorPath,
|
annularSectorPath,
|
||||||
annularSectorInsetPath,
|
annularSectorInsetPath,
|
||||||
calculateLabelRotation,
|
|
||||||
generateSectorColor,
|
|
||||||
generateTextPath,
|
generateTextPath,
|
||||||
generateVerticalTextPath,
|
generateVerticalTextPath,
|
||||||
getTextColorForBackground,
|
getTextColorForBackground,
|
||||||
@@ -142,73 +140,6 @@ describe('annularSectorPath', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('calculateLabelRotation', () => {
|
|
||||||
it('应该在上半圆不进行翻转', () => {
|
|
||||||
expect(calculateLabelRotation(0)).toBe(0);
|
|
||||||
expect(calculateLabelRotation(45)).toBe(45);
|
|
||||||
expect(calculateLabelRotation(90)).toBe(90);
|
|
||||||
expect(calculateLabelRotation(180)).toBe(180);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('应该在下半圆翻转180°避免倒字', () => {
|
|
||||||
expect(calculateLabelRotation(181)).toBe(361); // 181 + 180
|
|
||||||
expect(calculateLabelRotation(270)).toBe(450); // 270 + 180
|
|
||||||
expect(calculateLabelRotation(359)).toBe(539); // 359 + 180
|
|
||||||
});
|
|
||||||
|
|
||||||
it('应该在边界值正确处理', () => {
|
|
||||||
expect(calculateLabelRotation(180)).toBe(180); // 等于180不翻转
|
|
||||||
expect(calculateLabelRotation(360)).toBe(360); // 等于360不翻转
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('generateSectorColor', () => {
|
|
||||||
it('应该生成有效的 HSL 颜色', () => {
|
|
||||||
const color = generateSectorColor(0, 0);
|
|
||||||
expect(color).toMatch(/^hsl\(\d+(\.\d+)? \d+% \d+%\)$/);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('应该根据层索引改变亮度', () => {
|
|
||||||
const color1 = generateSectorColor(0, 0);
|
|
||||||
const color2 = generateSectorColor(1, 0);
|
|
||||||
const color3 = generateSectorColor(2, 0);
|
|
||||||
|
|
||||||
// 提取亮度值
|
|
||||||
const light1 = parseInt(color1.match(/(\d+)%\)$/)?.[1] || '0');
|
|
||||||
const light2 = parseInt(color2.match(/(\d+)%\)$/)?.[1] || '0');
|
|
||||||
const light3 = parseInt(color3.match(/(\d+)%\)$/)?.[1] || '0');
|
|
||||||
|
|
||||||
expect(light1).toBeGreaterThan(light2);
|
|
||||||
expect(light2).toBeGreaterThan(light3);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('应该根据扇区索引改变色相', () => {
|
|
||||||
const color1 = generateSectorColor(0, 0);
|
|
||||||
const color2 = generateSectorColor(0, 6);
|
|
||||||
|
|
||||||
// 提取色相值
|
|
||||||
const hue1 = parseInt(color1.match(/^hsl\((\d+(\.\d+)?)/)?.[1] || '0');
|
|
||||||
const hue2 = parseInt(color2.match(/^hsl\((\d+(\.\d+)?)/)?.[1] || '0');
|
|
||||||
|
|
||||||
expect(hue1).not.toBe(hue2);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('应该为单层生成颜色', () => {
|
|
||||||
const color = generateSectorColor(0, 0, 1, 24);
|
|
||||||
expect(color).toMatch(/^hsl\(\d+(\.\d+)? \d+% \d+%\)$/);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('应该处理大量层数', () => {
|
|
||||||
const color1 = generateSectorColor(0, 0, 31, 24);
|
|
||||||
const color2 = generateSectorColor(30, 0, 31, 24);
|
|
||||||
|
|
||||||
const light1 = parseInt(color1.match(/(\d+)%\)$/)?.[1] || '0');
|
|
||||||
const light2 = parseInt(color2.match(/(\d+)%\)$/)?.[1] || '0');
|
|
||||||
|
|
||||||
expect(light1).toBeGreaterThan(light2);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('annularSectorInsetPath', () => {
|
describe('annularSectorInsetPath', () => {
|
||||||
it('应该生成内缩路径', () => {
|
it('应该生成内缩路径', () => {
|
||||||
const path = annularSectorInsetPath(50, 100, 0, 90, 2);
|
const path = annularSectorInsetPath(50, 100, 0, 90, 2);
|
||||||
@@ -236,33 +167,33 @@ describe('annularSectorInsetPath', () => {
|
|||||||
|
|
||||||
describe('generateTextPath', () => {
|
describe('generateTextPath', () => {
|
||||||
it('应该生成正向文字路径', () => {
|
it('应该生成正向文字路径', () => {
|
||||||
const path = generateTextPath(50, 100, 0, 90, false, 'middle', 12);
|
const path = generateTextPath(50, 100, 0, 90, 'middle');
|
||||||
expect(path).toContain('M ');
|
expect(path).toContain('M ');
|
||||||
expect(path).toContain('A ');
|
expect(path).toContain('A ');
|
||||||
expect(path).toContain('0 1'); // 正向扫描
|
expect(path).toContain('0 1'); // 正向扫描
|
||||||
});
|
});
|
||||||
|
|
||||||
it('应该生成反向文字路径', () => {
|
it('应该生成反向文字路径', () => {
|
||||||
const path = generateTextPath(50, 100, 0, 90, true, 'middle', 12);
|
const path = generateTextPath(50, 100, 120, 240, 'middle');
|
||||||
expect(path).toContain('M ');
|
expect(path).toContain('M ');
|
||||||
expect(path).toContain('A ');
|
expect(path).toContain('A ');
|
||||||
expect(path).toContain('0 0'); // 反向扫描
|
expect(path).toContain('0 0'); // 反向扫描
|
||||||
});
|
});
|
||||||
|
|
||||||
it('应该在 centroid 模式下使用形心半径', () => {
|
it('应该在 centroid 模式下使用形心半径', () => {
|
||||||
const pathCentroid = generateTextPath(50, 100, 0, 90, false, 'centroid', 12);
|
const pathCentroid = generateTextPath(50, 100, 0, 90, 'centroid');
|
||||||
const pathMiddle = generateTextPath(50, 100, 0, 90, false, 'middle', 12);
|
const pathMiddle = generateTextPath(50, 100, 0, 90, 'middle');
|
||||||
expect(pathCentroid).not.toBe(pathMiddle);
|
expect(pathCentroid).not.toBe(pathMiddle);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('应该处理跨越360°的扇区', () => {
|
it('应该处理跨越360°的扇区', () => {
|
||||||
const path = generateTextPath(50, 100, 315, 45, false, 'middle', 12);
|
const path = generateTextPath(50, 100, 315, 45, 'middle');
|
||||||
expect(path).toContain('M ');
|
expect(path).toContain('M ');
|
||||||
expect(path).toContain('A ');
|
expect(path).toContain('A ');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('应该处理大角度扇区', () => {
|
it('应该处理大角度扇区', () => {
|
||||||
const path = generateTextPath(50, 100, 0, 270, false, 'middle', 12);
|
const path = generateTextPath(50, 100, 0, 270, 'middle');
|
||||||
expect(path).toContain('M ');
|
expect(path).toContain('M ');
|
||||||
expect(path).toContain('A ');
|
expect(path).toContain('A ');
|
||||||
});
|
});
|
||||||
@@ -270,25 +201,25 @@ describe('generateTextPath', () => {
|
|||||||
|
|
||||||
describe('generateVerticalTextPath', () => {
|
describe('generateVerticalTextPath', () => {
|
||||||
it('应该生成竖排文字路径', () => {
|
it('应该生成竖排文字路径', () => {
|
||||||
const path = generateVerticalTextPath(50, 100, 0, 30, 15, 'middle', 12);
|
const path = generateVerticalTextPath(50, 100, 0, 30, 'middle');
|
||||||
expect(path).toContain('M ');
|
expect(path).toContain('M ');
|
||||||
expect(path).toContain('L ');
|
expect(path).toContain('L ');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('应该在 centroid 模式下使用形心', () => {
|
it('应该在 centroid 模式下使用形心', () => {
|
||||||
const pathCentroid = generateVerticalTextPath(50, 100, 0, 30, 15, 'centroid', 12);
|
const pathCentroid = generateVerticalTextPath(50, 100, 0, 30, 'centroid');
|
||||||
const pathMiddle = generateVerticalTextPath(50, 100, 0, 30, 15, 'middle', 12);
|
const pathMiddle = generateVerticalTextPath(50, 100, 0, 30, 'middle');
|
||||||
expect(pathCentroid).not.toBe(pathMiddle);
|
expect(pathCentroid).not.toBe(pathMiddle);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('应该处理不同的角度', () => {
|
it('应该处理不同的角度', () => {
|
||||||
const path1 = generateVerticalTextPath(50, 100, 0, 30, 15, 'middle', 12);
|
const path1 = generateVerticalTextPath(50, 100, 0, 30, 'middle');
|
||||||
const path2 = generateVerticalTextPath(50, 100, 0, 30, 180, 'middle', 12);
|
const path2 = generateVerticalTextPath(50, 100, 150, 180, 'middle');
|
||||||
expect(path1).not.toBe(path2);
|
expect(path1).not.toBe(path2);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('应该处理窄扇区', () => {
|
it('应该处理窄扇区', () => {
|
||||||
const path = generateVerticalTextPath(80, 100, 0, 10, 5, 'middle', 12);
|
const path = generateVerticalTextPath(80, 100, 0, 10, 'middle');
|
||||||
expect(path).toContain('M ');
|
expect(path).toContain('M ');
|
||||||
expect(path).toContain('L ');
|
expect(path).toContain('L ');
|
||||||
});
|
});
|
||||||
@@ -296,22 +227,17 @@ describe('generateVerticalTextPath', () => {
|
|||||||
|
|
||||||
describe('getTextColorForBackground', () => {
|
describe('getTextColorForBackground', () => {
|
||||||
it('应该为深色背景返回白色', () => {
|
it('应该为深色背景返回白色', () => {
|
||||||
const color = getTextColorForBackground('hsl(180 70% 25%)');
|
const color = getTextColorForBackground('#111111');
|
||||||
expect(color).toBe('#ffffff');
|
expect(color).toBe('#ffffff');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('应该为浅色背景返回深色', () => {
|
it('应该为浅色背景返回深色', () => {
|
||||||
const color = getTextColorForBackground('hsl(180 70% 85%)');
|
const color = getTextColorForBackground('#f9fafb');
|
||||||
expect(color).toBe('#111827');
|
expect(color).toBe('#111827');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('应该处理边界值(50%)', () => {
|
it('应该支持 HSL 输入', () => {
|
||||||
const color = getTextColorForBackground('hsl(180 70% 50%)');
|
const color = getTextColorForBackground('hsl(180 70% 25%)');
|
||||||
expect(color).toBe('#111827');
|
|
||||||
});
|
|
||||||
|
|
||||||
it('应该处理边界值(49%)', () => {
|
|
||||||
const color = getTextColorForBackground('hsl(180 70% 49%)');
|
|
||||||
expect(color).toBe('#ffffff');
|
expect(color).toBe('#ffffff');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user