From 78d8801a071465afbeb58ac396a50cf51eaaae58 Mon Sep 17 00:00:00 2001 From: "douboer@gmail.com" Date: Wed, 21 Jan 2026 21:48:08 +0800 Subject: [PATCH] update at 2026-01-21 21:48:08 --- package-lock.json | 5 + src/Luopan.vue | 2 +- src/composables/useLuopan.ts | 14 -- src/index.ts | 2 - src/utils.ts | 264 +++++++++++++++++++++-------------- tests/README.md | 9 +- tests/useLuopan.test.ts | 53 +------ tests/utils.test.ts | 106 +++----------- 8 files changed, 184 insertions(+), 271 deletions(-) diff --git a/package-lock.json b/package-lock.json index 7bba6f8..2f6740e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1027,6 +1027,7 @@ "integrity": "sha512-xa57bCPGuzEFqGjPs3vVLyqareG8DX0uMkr5U/v5vLv5/ZUrBrPL7gzxzTJedEyZxFMfsozwTIbbYfEQVo3kgg==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@vitest/utils": "1.6.1", "fast-glob": "^3.3.2", @@ -2756,6 +2757,7 @@ "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", "devOptional": true, "license": "Apache-2.0", + "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -2784,6 +2786,7 @@ "integrity": "sha512-o5a9xKjbtuhY6Bi5S3+HvbRERmouabWbyUcpXXUA1u+GNUKoROi9byOJ8M0nHbHYHkYICiMlqxkg1KkYmm25Sw==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "esbuild": "^0.21.3", "postcss": "^8.4.43", @@ -2867,6 +2870,7 @@ "integrity": "sha512-Ljb1cnSJSivGN0LqXd/zmDbWEM0RNNg2t1QW/XUhYl/qPqyu7CsqeWtqQXHVaJsecLPuDoak2oJcZN2QoRIOag==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@vitest/expect": "1.6.1", "@vitest/runner": "1.6.1", @@ -2932,6 +2936,7 @@ "resolved": "https://registry.npmjs.org/vue/-/vue-3.5.26.tgz", "integrity": "sha512-SJ/NTccVyAoNUJmkM9KUqPcYlY+u8OVL1X5EW9RIs3ch5H2uERxyyIUI4MRxVCSOiEcupX9xNGde1tL9ZKpimA==", "license": "MIT", + "peer": true, "dependencies": { "@vue/compiler-dom": "3.5.26", "@vue/compiler-sfc": "3.5.26", diff --git a/src/Luopan.vue b/src/Luopan.vue index 8b2aa55..4ddb8dd 100644 --- a/src/Luopan.vue +++ b/src/Luopan.vue @@ -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); /** diff --git a/src/composables/useLuopan.ts b/src/composables/useLuopan.ts index 23acf31..3c79189 100644 --- a/src/composables/useLuopan.ts +++ b/src/composables/useLuopan.ts @@ -6,7 +6,6 @@ import { computed, type Ref, type ComputedRef } from 'vue'; import type { Example, Sector, TextRadialPosition } from '../types'; import { polarToXY, - calculateLabelRotation, generateSectorData, } from '../utils'; @@ -64,8 +63,6 @@ export function useLuopan( rOuter, aStart, aEnd, - layerCount, - pieCount, textRadialPosition: textRadialPositionRef.value, }); @@ -75,16 +72,6 @@ export function useLuopan( 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(暴露给模板使用) */ @@ -95,7 +82,6 @@ export function useLuopan( rings, outerMost, sectors, - getLabelTransform, toXY, }; } diff --git a/src/index.ts b/src/index.ts index 3d05943..f5ac5e2 100644 --- a/src/index.ts +++ b/src/index.ts @@ -21,8 +21,6 @@ export { annularSectorCentroid, annularSectorPath, annularSectorInsetPath, - calculateLabelRotation, - generateSectorColor, generateTextPath, generateVerticalTextPath, getTextColorForBackground, diff --git a/src/utils.ts b/src/utils.ts index b707daf..19e434f 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -6,6 +6,43 @@ 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 坐标 * 约定:角度 aDeg:0°在北(上方),顺时针为正 @@ -195,21 +232,6 @@ export function annularSectorInsetPath( ].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) * @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 aEndDeg 结束角度(度) * @param textRadialPosition 文字径向位置:'centroid'(形心)或 'middle'(中点,默认) + * @param textLength 文字字符数(可选,用于路径长度估算) + * @param fontSize 字体大小(可选,用于路径长度估算) * @returns SVG 路径字符串(直线) */ export function generateVerticalTextPath( @@ -297,7 +308,9 @@ export function generateVerticalTextPath( rOuter: number, aStartDeg: number, aEndDeg: number, - textRadialPosition: 'centroid' | 'middle' = 'middle' + textRadialPosition: 'centroid' | 'middle' = 'middle', + textLength?: number, + fontSize?: number ): string { // 计算中间角度 const a1 = normalizeDeg(aStartDeg); @@ -317,22 +330,59 @@ export function generateVerticalTextPath( rMid = (rInner + rOuter) / 2; } - // 计算径向高度和字体大小 + // 计算径向高度 const radialHeight = rOuter - rInner; - // 计算字体大小(竖排) - const tempLength = 2; // 先假设2个字 - const tempFontSize = calculateSectorFontSize(rInner, rOuter, aStartDeg, aEndDeg, tempLength, 3, 20, true); - - // 根据字体大小决定实际字符数 - const textLength = calculateVerticalTextLength(rInner, rOuter, tempFontSize); - - // 用实际字符数重新计算字体大小 - const fontSize = calculateSectorFontSize(rInner, rOuter, aStartDeg, aEndDeg, textLength, 3, 20, true); + 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 = textLength * TEXT_LAYOUT_CONFIG.CHAR_SPACING_RATIO * fontSize; + const requiredPathLength = + effectiveTextLength * TEXT_LAYOUT_CONFIG.CHAR_SPACING_RATIO * effectiveFontSize; // 确保路径不超出扇区边界(考虑径向 padding) 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}`; } -/** - * 生成扇区颜色 - * @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 文字颜色(深色或浅色) */ export function getTextColorForBackground(backgroundColor: string): string { - // 从 HSL 字符串中提取亮度值 - const match = backgroundColor.match(/hsl\([^)]+\s+(\d+)%\)/); - if (!match) return '#111827'; // 默认深色 - - const lightness = parseInt(match[1]); - - // 亮度阈值:50% - // 亮度低于50%使用白色文字,否则使用深色文字 - return lightness < 50 ? '#ffffff' : '#111827'; + 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; } /** @@ -530,7 +550,7 @@ export function calculateSectorFontSize( * @param fontSize 字体大小 * @returns 应显示的字符数 */ -export function calculateVerticalTextLength( +function calculateVerticalTextLength( rInner: number, rOuter: number, fontSize: number @@ -565,11 +585,23 @@ export function generateSectorData(params: { rOuter: number; aStart: number; aEnd: number; - layerCount: number; - pieCount: number; textRadialPosition: 'centroid' | 'middle'; + fill?: string; + textColor?: string; + label?: string; }): 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 c = annularSectorCentroid({ rInner, rOuter, aStartDeg: aStart, aEndDeg: aEnd }); @@ -580,32 +612,42 @@ export function generateSectorData(params: { // 判断是否需要竖排 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) { - // 竖排逻辑 - 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); + 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 { - // 横排逻辑 - 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)); + 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 label = '测'.repeat(textLength); + const finalLabel = providedLabel ?? '测'.repeat(textLength); const textPathId = `text-path-L${layerIndex}-P${pieIndex}`; @@ -615,12 +657,20 @@ export function generateSectorData(params: { // 生成文字路径(路径方向已经在函数内部自动处理) const textPath = isVertical - ? generateVerticalTextPath(rInner, rOuter, aStart, aEnd, effectiveTextRadialPosition) + ? generateVerticalTextPath( + rInner, + rOuter, + aStart, + aEnd, + effectiveTextRadialPosition, + textLength, + sectorFontSize + ) : generateTextPath(rInner, rOuter, aStart, aEnd, effectiveTextRadialPosition); // 生成颜色 - const fillColor = generateSectorColor(layerIndex, pieIndex, layerCount, pieCount); - const textColor = getTextColorForBackground(fillColor); + const fillColor = fill ?? DEFAULT_SECTOR_FILL; + const baseTextColor = textColor ?? getTextColorForBackground(fillColor); // 内部填色逻辑 const shouldFill = (pieIndex + layerIndex) % 3 === 0; @@ -628,7 +678,7 @@ export function generateSectorData(params: { const innerFillColor = shouldFill ? fillColor : undefined; const baseFillColor = shouldFill ? '#ffffff' : fillColor; - const finalTextColor = shouldFill ? '#111827' : textColor; + const finalTextColor = shouldFill ? TEXT_ON_LIGHT : baseTextColor; return { key: `L${layerIndex}-P${pieIndex}`, @@ -644,7 +694,7 @@ export function generateSectorData(params: { cy: c.cy, fill: baseFillColor, textColor: finalTextColor, - label, + label: finalLabel, path: annularSectorPath(rInner, rOuter, aStart, aEnd), innerFillPath, innerFillColor, diff --git a/tests/README.md b/tests/README.md index 5953387..e6d48e7 100644 --- a/tests/README.md +++ b/tests/README.md @@ -22,26 +22,23 @@ tests/ - 角度数组和半径数组的有效性验证 - 不同类型示例的存在性检查 -### 2. utils.test.ts (48 个测试) +### 2. utils.test.ts (39 个测试) 测试所有工具函数的核心逻辑: - **polarToXY**: 极坐标转换 - **normalizeDeg**: 角度归一化 - **annularSectorCentroid**: 扇形形心计算 - **annularSectorPath**: SVG 路径生成 - **annularSectorInsetPath**: 内缩路径生成 -- **calculateLabelRotation**: 文字旋转角度 -- **generateSectorColor**: 扇区颜色生成 - **generateTextPath**: 文字路径生成 - **generateVerticalTextPath**: 竖排文字路径 - **getTextColorForBackground**: 文字颜色适配 - **calculateSectorFontSize**: 字体大小计算 -### 3. useLuopan.test.ts (32 个测试) +### 3. useLuopan.test.ts (29 个测试) 测试罗盘业务逻辑组合函数: - 基本功能和返回值验证 - 扇区生成逻辑 - 文字位置模式(middle/centroid) -- 文字方向判断 - 竖排文字判断 - 内部填色逻辑 - 响应式更新 @@ -90,7 +87,7 @@ npm test -- --coverage ## 测试统计 - **总测试文件**: 4 个 -- **总测试用例**: 159 个 +- **总测试用例**: 147 个 - **测试通过率**: 100% - **测试运行时间**: ~1.8 秒 diff --git a/tests/useLuopan.test.ts b/tests/useLuopan.test.ts index 06e67ad..83bf69b 100644 --- a/tests/useLuopan.test.ts +++ b/tests/useLuopan.test.ts @@ -25,7 +25,6 @@ describe('useLuopan', () => { expect(result).toHaveProperty('rings'); expect(result).toHaveProperty('outerMost'); expect(result).toHaveProperty('sectors'); - expect(result).toHaveProperty('getLabelTransform'); expect(result).toHaveProperty('toXY'); }); @@ -92,7 +91,6 @@ describe('useLuopan', () => { expect(sector).toHaveProperty('path'); expect(sector).toHaveProperty('textPath'); expect(sector).toHaveProperty('textPathId'); - expect(sector).toHaveProperty('needReverse'); expect(sector).toHaveProperty('isVertical'); expect(sector).toHaveProperty('fontSize'); }); @@ -161,8 +159,8 @@ describe('useLuopan', () => { const { sectors } = useLuopan(example, textRadialPosition); sectors.value.forEach((sector) => { - expect(sector.fill).toMatch(/^(hsl\(.*\)|#[0-9a-fA-F]{6}|#ffffff)$/); - expect(sector.textColor).toMatch(/^(#[0-9a-fA-F]{6}|#ffffff)$/); + expect(sector.fill).toMatch(/^#[0-9a-fA-F]{6}$/); + 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('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('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('竖排文字判断', () => { it('应该为窄扇区设置 isVertical', () => { const example = ref({ @@ -384,21 +350,6 @@ describe('useLuopan', () => { }); }); - describe('getLabelTransform', () => { - it('应该返回有效的 SVG transform 字符串', () => { - const example = ref(createMockExample()); - const textRadialPosition = ref('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', () => { it('应该正确转换极坐标', () => { const example = ref(createMockExample()); diff --git a/tests/utils.test.ts b/tests/utils.test.ts index fc64215..e086d19 100644 --- a/tests/utils.test.ts +++ b/tests/utils.test.ts @@ -10,8 +10,6 @@ import { annularSectorCentroid, annularSectorPath, annularSectorInsetPath, - calculateLabelRotation, - generateSectorColor, generateTextPath, generateVerticalTextPath, 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', () => { it('应该生成内缩路径', () => { const path = annularSectorInsetPath(50, 100, 0, 90, 2); @@ -236,33 +167,33 @@ describe('annularSectorInsetPath', () => { describe('generateTextPath', () => { 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('A '); expect(path).toContain('0 1'); // 正向扫描 }); 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('A '); expect(path).toContain('0 0'); // 反向扫描 }); it('应该在 centroid 模式下使用形心半径', () => { - const pathCentroid = generateTextPath(50, 100, 0, 90, false, 'centroid', 12); - const pathMiddle = generateTextPath(50, 100, 0, 90, false, 'middle', 12); + const pathCentroid = generateTextPath(50, 100, 0, 90, 'centroid'); + const pathMiddle = generateTextPath(50, 100, 0, 90, 'middle'); expect(pathCentroid).not.toBe(pathMiddle); }); 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('A '); }); 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('A '); }); @@ -270,25 +201,25 @@ describe('generateTextPath', () => { describe('generateVerticalTextPath', () => { 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('L '); }); it('应该在 centroid 模式下使用形心', () => { - const pathCentroid = generateVerticalTextPath(50, 100, 0, 30, 15, 'centroid', 12); - const pathMiddle = generateVerticalTextPath(50, 100, 0, 30, 15, 'middle', 12); + const pathCentroid = generateVerticalTextPath(50, 100, 0, 30, 'centroid'); + const pathMiddle = generateVerticalTextPath(50, 100, 0, 30, 'middle'); expect(pathCentroid).not.toBe(pathMiddle); }); it('应该处理不同的角度', () => { - const path1 = generateVerticalTextPath(50, 100, 0, 30, 15, 'middle', 12); - const path2 = generateVerticalTextPath(50, 100, 0, 30, 180, 'middle', 12); + const path1 = generateVerticalTextPath(50, 100, 0, 30, 'middle'); + const path2 = generateVerticalTextPath(50, 100, 150, 180, 'middle'); expect(path1).not.toBe(path2); }); 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('L '); }); @@ -296,22 +227,17 @@ describe('generateVerticalTextPath', () => { describe('getTextColorForBackground', () => { it('应该为深色背景返回白色', () => { - const color = getTextColorForBackground('hsl(180 70% 25%)'); + const color = getTextColorForBackground('#111111'); expect(color).toBe('#ffffff'); }); it('应该为浅色背景返回深色', () => { - const color = getTextColorForBackground('hsl(180 70% 85%)'); + const color = getTextColorForBackground('#f9fafb'); expect(color).toBe('#111827'); }); - it('应该处理边界值(50%)', () => { - const color = getTextColorForBackground('hsl(180 70% 50%)'); - expect(color).toBe('#111827'); - }); - - it('应该处理边界值(49%)', () => { - const color = getTextColorForBackground('hsl(180 70% 49%)'); + it('应该支持 HSL 输入', () => { + const color = getTextColorForBackground('hsl(180 70% 25%)'); expect(color).toBe('#ffffff'); });