first commit
This commit is contained in:
679
detail-design.md
Normal file
679
detail-design.md
Normal file
@@ -0,0 +1,679 @@
|
||||
# 罗盘组件详细设计文档
|
||||
|
||||
## 1. 概述
|
||||
|
||||
本组件实现了一个可配置的圆盘罗盘可视化系统,支持将圆盘分为多个同心圆环和扇区,并在每个区域精确放置文字或图形。
|
||||
|
||||
## 2. 数学模型
|
||||
|
||||
### 2.1 坐标系统
|
||||
|
||||
- **角度约定**:0° 位于正北(上方),顺时针方向为正
|
||||
- **SVG 坐标系**:
|
||||
- x 轴:向右为正
|
||||
- y 轴:向下为正
|
||||
- 原点:画布中心
|
||||
|
||||
### 2.2 参数定义
|
||||
|
||||
#### 角度参数
|
||||
- `n`:总扇区数(pie 数量)
|
||||
- `angle[i]`:第 i 个分度线的角度,i ∈ [0...n]
|
||||
- `wide(i) = angle[i] - angle[i-1]`:第 i 块扇区的角度宽度
|
||||
|
||||
#### 半径参数
|
||||
- `m`:总圆环数
|
||||
- `radius(j)`:第 j 个圆环的半径,j ∈ [0...m]
|
||||
- `height(j) = radius(j) - radius(j-1)`:第 j 层圆环的高度
|
||||
|
||||
#### 区域定义
|
||||
- `layer(j)`:第 j 层,由圆环 j 和圆环 j-1 之间形成
|
||||
- `pie(i)`:第 i 个扇区,由分度线 i-1 和分度线 i 之间形成
|
||||
- `sector(j,i)`:第 j 层第 i 个扇区的区域
|
||||
|
||||
### 2.3 形心计算公式
|
||||
|
||||
对于圆环扇形区域 `sector(j,i)`,其形心位置计算如下:
|
||||
|
||||
#### 输入参数
|
||||
- `rInner = radius(j-1)`:内半径
|
||||
- `rOuter = radius(j)`:外半径
|
||||
- `aStart = angle[i-1]`:起始角度(度)
|
||||
- `aEnd = angle[i]`:结束角度(度)
|
||||
|
||||
#### 计算步骤
|
||||
|
||||
1. **角度归一化**
|
||||
```
|
||||
a₁ = normalize(aStart) // 转换到 [0, 360)
|
||||
a₂ = normalize(aEnd)
|
||||
Δ = a₂ - a₁ // 若 < 0,则加 360
|
||||
```
|
||||
|
||||
2. **中心角度**
|
||||
```
|
||||
aMid = a₁ + Δ/2 // 扇区中心角度
|
||||
```
|
||||
|
||||
3. **形心半径**(面积加权质心)
|
||||
```
|
||||
radialFactor = (2/3) × (r₂³ - r₁³) / (r₂² - r₁²)
|
||||
sinc(x) = sin(x) / x // x ≠ 0 时
|
||||
ρ = radialFactor × sinc(Δ/2)
|
||||
```
|
||||
|
||||
其中:
|
||||
- r₁ = rInner
|
||||
- r₂ = rOuter
|
||||
- Δ 为弧度制
|
||||
|
||||
4. **极坐标转笛卡尔坐标**
|
||||
```
|
||||
cx = ρ × sin(aMid)
|
||||
cy = -ρ × cos(aMid) // 注意:y 坐标取负以适应 SVG 坐标系
|
||||
```
|
||||
|
||||
#### 输出结果
|
||||
- `(cx, cy)`:形心的笛卡尔坐标
|
||||
- `ρ`:形心的极坐标半径
|
||||
- `aMid`:形心的极坐标角度
|
||||
|
||||
### 2.4 特殊情况处理
|
||||
|
||||
1. **纯扇形**(rInner = 0)
|
||||
- 公式仍然适用,内圆退化为点
|
||||
|
||||
2. **跨越 0° 的扇区**
|
||||
- 例如:aStart = 315°, aEnd = 45°
|
||||
- 通过归一化自动处理:Δ = 45 - 315 + 360 = 90°
|
||||
|
||||
3. **退化情况**
|
||||
- rInner ≥ rOuter:返回 (0, 0)
|
||||
- Δ = 0:返回 (0, 0)
|
||||
|
||||
## 3. 核心算法实现
|
||||
|
||||
### 3.1 极坐标转换
|
||||
|
||||
```typescript
|
||||
function polarToXY(aDeg: number, r: number): {x: number, y: number} {
|
||||
const a = (aDeg * Math.PI) / 180;
|
||||
return {
|
||||
x: r * Math.sin(a),
|
||||
y: -r * Math.cos(a) // y 坐标取负
|
||||
};
|
||||
}
|
||||
```
|
||||
|
||||
### 3.2 角度归一化
|
||||
|
||||
```typescript
|
||||
function normalizeDeg(deg: number): number {
|
||||
const d = deg % 360;
|
||||
return d < 0 ? d + 360 : d;
|
||||
}
|
||||
```
|
||||
|
||||
### 3.3 形心计算
|
||||
|
||||
```typescript
|
||||
function annularSectorCentroid(params: {
|
||||
rInner: number,
|
||||
rOuter: number,
|
||||
aStartDeg: number,
|
||||
aEndDeg: number
|
||||
}): CentroidResult {
|
||||
const { rInner, rOuter, aStartDeg, aEndDeg } = params;
|
||||
|
||||
// 1. 角度归一化
|
||||
const a1 = normalizeDeg(aStartDeg);
|
||||
const a2 = normalizeDeg(aEndDeg);
|
||||
let deltaDeg = a2 - a1;
|
||||
if (deltaDeg < 0) deltaDeg += 360;
|
||||
|
||||
// 2. 中心角度
|
||||
const aMidDeg = normalizeDeg(a1 + deltaDeg / 2);
|
||||
const aMidRad = (aMidDeg * Math.PI) / 180;
|
||||
|
||||
// 3. 边界检查
|
||||
if (rOuter <= rInner || deltaDeg === 0) {
|
||||
return { cx: 0, cy: 0, rho: 0, aMidDeg, aMidRad, deltaDeg };
|
||||
}
|
||||
|
||||
// 4. 形心半径计算
|
||||
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;
|
||||
|
||||
// 5. 坐标转换
|
||||
const p = polarToXY(aMidDeg, rho);
|
||||
return { cx: p.x, cy: p.y, rho, aMidDeg, aMidRad, deltaDeg };
|
||||
}
|
||||
```
|
||||
|
||||
## 4. 扇区路径生成
|
||||
|
||||
### 4.1 SVG 路径绘制
|
||||
|
||||
圆环扇形使用 SVG 的 `<path>` 元素绘制,包含以下步骤:
|
||||
|
||||
1. 移动到外弧起点
|
||||
2. 绘制外弧(arc to)
|
||||
3. 连线到内弧终点
|
||||
4. 绘制内弧(arc to)
|
||||
5. 闭合路径
|
||||
|
||||
```typescript
|
||||
function annularSectorPath(
|
||||
rInner: number,
|
||||
rOuter: number,
|
||||
aStartDeg: number,
|
||||
aEndDeg: number
|
||||
): string {
|
||||
// 计算关键点
|
||||
const p1 = polarToXY(aStart, rOuter); // 外弧起点
|
||||
const p2 = polarToXY(aEnd, rOuter); // 外弧终点
|
||||
const p3 = polarToXY(aEnd, rInner); // 内弧终点
|
||||
const p4 = polarToXY(aStart, rInner); // 内弧起点
|
||||
|
||||
// 确定 large-arc-flag
|
||||
const largeArc = delta > 180 ? 1 : 0;
|
||||
|
||||
// 生成路径
|
||||
return [
|
||||
`M ${p1.x} ${p1.y}`, // 起点
|
||||
`A ${rOuter} ${rOuter} 0 ${largeArc} 1 ${p2.x} ${p2.y}`, // 外弧
|
||||
`L ${p3.x} ${p3.y}`, // 连线
|
||||
`A ${rInner} ${rInner} 0 ${largeArc} 0 ${p4.x} ${p4.y}`, // 内弧
|
||||
"Z" // 闭合
|
||||
].join(" ");
|
||||
}
|
||||
```
|
||||
|
||||
### 4.2 特殊情况:纯扇形
|
||||
|
||||
当 `rInner ≈ 0` 时,内弧退化为圆心点:
|
||||
|
||||
```typescript
|
||||
if (rInner <= 0.000001) {
|
||||
return [
|
||||
`M ${p1.x} ${p1.y}`,
|
||||
`A ${rOuter} ${rOuter} 0 ${largeArc} 1 ${p2.x} ${p2.y}`,
|
||||
`L 0 0`, // 连线到圆心
|
||||
"Z"
|
||||
].join(" ");
|
||||
}
|
||||
```
|
||||
|
||||
## 5. 文字排列算法
|
||||
|
||||
### 5.1 文字路径生成
|
||||
|
||||
文字沿圆弧排列,使用 SVG 的 `<textPath>` 功能。路径使用形心半径:
|
||||
|
||||
```typescript
|
||||
function generateTextPath(
|
||||
rInner: number,
|
||||
rOuter: number,
|
||||
aStartDeg: number,
|
||||
aEndDeg: number,
|
||||
reverse: boolean
|
||||
): string {
|
||||
// 使用形心半径作为文字路径
|
||||
const centroid = annularSectorCentroid({
|
||||
rInner, rOuter, aStartDeg, aEndDeg
|
||||
});
|
||||
const rMid = centroid.rho; // 形心半径
|
||||
|
||||
const p1 = polarToXY(aStart, rMid);
|
||||
const p2 = polarToXY(aEnd, rMid);
|
||||
const largeArc = delta > 180 ? 1 : 0;
|
||||
|
||||
if (reverse) {
|
||||
// 反向路径(左半圆)
|
||||
return `M ${p2.x} ${p2.y} A ${rMid} ${rMid} 0 ${largeArc} 0 ${p1.x} ${p1.y}`;
|
||||
} else {
|
||||
// 正向路径(右半圆)
|
||||
return `M ${p1.x} ${p1.y} A ${rMid} ${rMid} 0 ${largeArc} 1 ${p2.x} ${p2.y}`;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 5.2 文字方向判断
|
||||
|
||||
为保持文字"头朝外",需要判断是否使用反向路径:
|
||||
|
||||
```typescript
|
||||
// 左半圆(90° ~ 270°)需要反向路径
|
||||
const needReverse = aMidDeg > 90 && aMidDeg < 270;
|
||||
```
|
||||
|
||||
**原理**:
|
||||
- 右半圆(0°~90°, 270°~360°):正向路径,文字自然朝外
|
||||
- 左半圆(90°~270°):反向路径,避免文字倒置
|
||||
|
||||
### 5.3 文字长度适配
|
||||
|
||||
根据层数自动调整文字长度:
|
||||
|
||||
```typescript
|
||||
// 第 j 层显示 (j+1) × 2 个字
|
||||
const textLength = (layerIndex + 1) * 2;
|
||||
const label = '测'.repeat(textLength);
|
||||
```
|
||||
|
||||
示例:
|
||||
- 第1层:2个字
|
||||
- 第2层:4个字
|
||||
- 第3层:6个字
|
||||
|
||||
## 6. 颜色生成策略
|
||||
|
||||
使用 HSL 颜色空间实现层次分明的配色:
|
||||
|
||||
```typescript
|
||||
function generateSectorColor(
|
||||
layerIndex: number,
|
||||
pieIndex: number,
|
||||
totalPies: number = 24
|
||||
): string {
|
||||
const hue = (pieIndex * 360) / totalPies; // 色相均匀分布
|
||||
const light = 78 - layerIndex * 10; // 亮度由内向外递减
|
||||
return `hsl(${hue} 70% ${light}%)`;
|
||||
}
|
||||
```
|
||||
|
||||
**效果**:
|
||||
- 相邻扇区色相不同,易于区分
|
||||
- 内层较亮,外层较暗,突出层次
|
||||
|
||||
## 7. 组件架构
|
||||
|
||||
### 7.1 模块划分
|
||||
|
||||
```
|
||||
src/
|
||||
├── types.ts # TypeScript 类型定义
|
||||
├── utils.ts # 纯函数工具集
|
||||
├── constants.ts # 配置常量
|
||||
├── composables/
|
||||
│ └── useLuopan.ts # 组合函数(业务逻辑)
|
||||
├── Luopan.vue # 主组件
|
||||
├── utils.test.ts # 单元测试
|
||||
└── index.ts # 入口文件
|
||||
```
|
||||
|
||||
### 7.2 数据流
|
||||
|
||||
```
|
||||
配置数据 (Example)
|
||||
↓
|
||||
useLuopan (计算扇区数据)
|
||||
↓
|
||||
Sector[] (包含形心、路径、颜色等)
|
||||
↓
|
||||
Luopan.vue (渲染 SVG)
|
||||
```
|
||||
|
||||
### 7.3 关键类型定义
|
||||
|
||||
```typescript
|
||||
// 配置
|
||||
interface Example {
|
||||
name: string;
|
||||
angles: number[]; // 角度分割点 [0, 30, 60, ..., 360]
|
||||
radii: number[]; // 圆环半径 [60, 120, 180]
|
||||
}
|
||||
|
||||
// 扇区数据
|
||||
interface Sector {
|
||||
key: string;
|
||||
layerIndex: number;
|
||||
pieIndex: number;
|
||||
rInner: number;
|
||||
rOuter: number;
|
||||
aStart: number;
|
||||
aEnd: number;
|
||||
aMidDeg: number;
|
||||
aMidRad: number;
|
||||
cx: number; // 形心 x
|
||||
cy: number; // 形心 y
|
||||
fill: string;
|
||||
label: string;
|
||||
path: string; // 扇区路径
|
||||
textPath: string; // 文字路径
|
||||
textPathId: string;
|
||||
needReverse: boolean;
|
||||
}
|
||||
```
|
||||
|
||||
## 8. 使用示例
|
||||
|
||||
### 8.1 基本使用
|
||||
|
||||
```vue
|
||||
<template>
|
||||
<Luopan :size="520" />
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { Luopan } from './src';
|
||||
</script>
|
||||
```
|
||||
|
||||
### 8.2 自定义配置
|
||||
|
||||
```typescript
|
||||
import { useLuopan } from './src';
|
||||
import { ref } from 'vue';
|
||||
|
||||
const customExample = ref({
|
||||
name: '自定义罗盘',
|
||||
angles: [0, 45, 90, 135, 180, 225, 270, 315, 360],
|
||||
radii: [80, 160, 240]
|
||||
});
|
||||
|
||||
const { sectors } = useLuopan(customExample);
|
||||
```
|
||||
|
||||
### 8.3 访问工具函数
|
||||
|
||||
```typescript
|
||||
import {
|
||||
polarToXY,
|
||||
annularSectorCentroid,
|
||||
generateSectorColor
|
||||
} from './src';
|
||||
|
||||
// 计算特定区域的形心
|
||||
const centroid = annularSectorCentroid({
|
||||
rInner: 50,
|
||||
rOuter: 100,
|
||||
aStartDeg: 0,
|
||||
aEndDeg: 30
|
||||
});
|
||||
|
||||
console.log(`形心位置: (${centroid.cx}, ${centroid.cy})`);
|
||||
```
|
||||
|
||||
## 9. 测试
|
||||
|
||||
### 9.1 单元测试覆盖
|
||||
|
||||
- ✅ 极坐标转换(4个方向)
|
||||
- ✅ 角度归一化(负数、大于360)
|
||||
- ✅ 形心计算(纯扇形、圆环、跨0°)
|
||||
- ✅ 路径生成(正常、退化情况)
|
||||
- ✅ 颜色生成(色相、亮度)
|
||||
|
||||
### 9.2 运行测试
|
||||
|
||||
```bash
|
||||
npm run test # 运行测试
|
||||
npm run test:ui # 测试 UI
|
||||
npm run test:coverage # 覆盖率报告
|
||||
```
|
||||
|
||||
## 10. 性能优化
|
||||
|
||||
### 10.1 计算优化
|
||||
|
||||
- 使用 Vue 的 `computed` 缓存计算结果
|
||||
- 避免重复计算:形心、路径等一次生成
|
||||
- 三角函数结果复用
|
||||
|
||||
### 10.2 渲染优化
|
||||
|
||||
- 使用 `key` 属性优化列表渲染
|
||||
- SVG 路径字符串预生成,避免模板内计算
|
||||
- 辅助线可选显示,减少 DOM 节点
|
||||
|
||||
## 11. 扩展性
|
||||
|
||||
### 11.1 支持的扩展
|
||||
|
||||
- ✅ 自定义角度分割
|
||||
- ✅ 自定义圆环半径
|
||||
- ✅ 自定义颜色方案
|
||||
- ✅ 自定义文字内容
|
||||
- 🔲 图标/图片放置(预留 `<foreignObject>`)
|
||||
- 🔲 交互事件(点击、悬停)
|
||||
- 🔲 动画效果
|
||||
|
||||
### 11.2 API 设计原则
|
||||
|
||||
- 纯函数优先,便于测试
|
||||
- 类型安全,完整的 TypeScript 支持
|
||||
- 组合式 API,灵活复用
|
||||
- 渐进式增强,保持简单使用
|
||||
|
||||
## 12. 文字布局与自适应
|
||||
|
||||
### 12.1 文字布局配置常量
|
||||
|
||||
所有文字布局相关的魔法数字已集中在 `TEXT_LAYOUT_CONFIG` 中管理:
|
||||
|
||||
```typescript
|
||||
export const TEXT_LAYOUT_CONFIG = {
|
||||
/** 字符间距系数:1.2 表示字符实际占用高度 = fontSize × 1.2 */
|
||||
CHAR_SPACING_RATIO: 1.2,
|
||||
|
||||
/** 径向留白比例:0.8 表示文字占用 80%,上下各留 10% */
|
||||
RADIAL_PADDING_RATIO: 0.8,
|
||||
|
||||
/** 切向留白比例:0.85 表示文字占用 85%,左右各留 7.5% */
|
||||
TANGENT_PADDING_RATIO: 0.85,
|
||||
|
||||
/** 小角度扇区字体缩放:防止弧线弯曲导致视觉溢出 */
|
||||
SMALL_ANGLE_SCALE: {
|
||||
TINY_THRESHOLD: 15, // < 15° 应用 70% 缩放
|
||||
TINY_SCALE: 0.7,
|
||||
SMALL_THRESHOLD: 30, // 15°-30° 应用 85% 缩放
|
||||
SMALL_SCALE: 0.85,
|
||||
},
|
||||
|
||||
/** 字体大小限制 */
|
||||
FONT_SIZE: {
|
||||
MIN: 3, // 最小 3px
|
||||
MAX: 24, // 最大 24px
|
||||
},
|
||||
|
||||
/** 竖排文字字符数范围 */
|
||||
VERTICAL_TEXT: {
|
||||
MIN_CHARS: 1,
|
||||
MAX_CHARS: 4,
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
### 12.2 字体大小自适应算法
|
||||
|
||||
#### 横排文字(沿弧线)
|
||||
|
||||
字体大小同时受径向高度和弧长双重约束:
|
||||
|
||||
```typescript
|
||||
// 1. 径向高度约束
|
||||
maxByHeight = radialHeight × RADIAL_PADDING_RATIO
|
||||
|
||||
// 2. 弧长宽度约束
|
||||
arcLength = rMid × deltaDeg × π/180
|
||||
availableArcLength = arcLength × TANGENT_PADDING_RATIO
|
||||
maxByWidth = availableArcLength / (textLength × 1.1)
|
||||
|
||||
// 3. 取较小值
|
||||
calculatedSize = min(maxByHeight, maxByWidth)
|
||||
|
||||
// 4. 小角度额外缩放
|
||||
if (deltaDeg < 15°) calculatedSize × 0.7
|
||||
else if (deltaDeg < 30°) calculatedSize × 0.85
|
||||
```
|
||||
|
||||
**关键点**:
|
||||
- `1.1` 系数包含字符宽度(1.0) + 字符间距(0.1)
|
||||
- 小角度缩放是因为弧线弯曲使文字在视觉上"更胖"
|
||||
|
||||
#### 竖排文字(沿径向)
|
||||
|
||||
竖排文字沿径向排列,受不同约束:
|
||||
|
||||
```typescript
|
||||
// 1. 径向高度约束(主要)
|
||||
availableHeight = radialHeight × RADIAL_PADDING_RATIO
|
||||
maxByHeight = availableHeight / (textLength × CHAR_SPACING_RATIO)
|
||||
|
||||
// 2. 最内侧字符的弧长约束(防止最里面的字溢出)
|
||||
estimatedFontSize = radialHeight × 0.5
|
||||
innerMostRadius = rInner + estimatedFontSize / 2
|
||||
innerArcLength = innerMostRadius × deltaDeg × π/180
|
||||
availableArcLength = innerArcLength × TANGENT_PADDING_RATIO
|
||||
maxByWidth = availableArcLength // 单个字符宽度
|
||||
|
||||
// 3. 取较小值
|
||||
calculatedSize = min(maxByHeight, maxByWidth)
|
||||
```
|
||||
|
||||
**关键点**:
|
||||
- 最内侧字符位置的半径最小,弧长最短,是最严格的宽度限制
|
||||
- 不需要小角度缩放(竖排不受弧线弯曲影响)
|
||||
|
||||
### 12.3 竖排文字路径生成
|
||||
|
||||
竖排文字路径是沿径向的直线,需要特殊处理不同场景:
|
||||
|
||||
#### 普通圆环扇区(rInner > 0)
|
||||
|
||||
```typescript
|
||||
// 以形心或中点为中心,向两边延伸
|
||||
rMid = centroid.rho // 或 (rInner + rOuter) / 2
|
||||
requiredPathLength = textLength × CHAR_SPACING_RATIO × fontSize
|
||||
halfPath = requiredPathLength / 2
|
||||
|
||||
// 确保不超出边界
|
||||
safeHalfPath = min(
|
||||
halfPath,
|
||||
radialHeight × RADIAL_PADDING_RATIO / 2,
|
||||
rMid - rInner,
|
||||
rOuter - rMid
|
||||
)
|
||||
|
||||
finalStartR = rMid + safeHalfPath // 外端
|
||||
finalEndR = rMid - safeHalfPath // 内端
|
||||
```
|
||||
|
||||
#### 从圆心开始的扇区(rInner = 0)
|
||||
|
||||
对于最内层扇区,形心会偏向外侧,需要特殊处理:
|
||||
|
||||
```typescript
|
||||
if (rInner === 0) {
|
||||
// 检查以形心居中是否会溢出外圆
|
||||
if (rMid + halfPath > rOuter) {
|
||||
// 会溢出:改为从外圆向内延伸
|
||||
finalStartR = rOuter
|
||||
finalEndR = rOuter - actualPathLength
|
||||
} else {
|
||||
// 不会溢出:正常使用形心居中
|
||||
finalStartR = rMid + halfPath
|
||||
finalEndR = rMid - halfPath
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**原理**:
|
||||
- 最内层的形心通常在 r = 2/3 × rOuter 附近(对于纯扇形)
|
||||
- 如果以形心为中心,向外空间只有 1/3 × rOuter
|
||||
- 当文字较多时,会向外溢出
|
||||
- 因此优先尝试形心居中,溢出时回退到从外圆向内
|
||||
|
||||
### 12.4 文字字符数计算
|
||||
|
||||
#### 横排文字
|
||||
|
||||
基于扇区尺寸和层数综合决定:
|
||||
|
||||
```typescript
|
||||
estimatedFontSize = radialHeight × 0.6
|
||||
charWidth = estimatedFontSize × 1.15
|
||||
availableWidth = arcWidth × 0.8
|
||||
maxChars = floor(availableWidth / charWidth)
|
||||
|
||||
layerBasedLength = max(2, layerIndex + 1) // 外层字多一些
|
||||
textLength = max(1, min(6, min(maxChars, layerBasedLength)))
|
||||
```
|
||||
|
||||
#### 竖排文字
|
||||
|
||||
两步计算法,确保字符数与字体大小匹配:
|
||||
|
||||
```typescript
|
||||
// 步骤1:用中等字符数估算字体大小
|
||||
tempFontSize = calculateSectorFontSize(rInner, rOuter, aStart, aEnd, 2, isVertical=true)
|
||||
|
||||
// 步骤2:根据字体大小计算实际能容纳的字符数(1-4个)
|
||||
textLength = calculateVerticalTextLength(rInner, rOuter, tempFontSize, 1, 4)
|
||||
|
||||
// 步骤3:用实际字符数重新精确计算字体大小
|
||||
finalFontSize = calculateSectorFontSize(rInner, rOuter, aStart, aEnd, textLength, isVertical=true)
|
||||
```
|
||||
|
||||
### 12.5 横排文字路径生成
|
||||
|
||||
横排文字沿弧线排列,路径保持完整(不缩短),padding 通过字体大小计算实现:
|
||||
|
||||
```typescript
|
||||
// 路径从 aStart 到 aEnd 的完整弧线
|
||||
rMid = centroid.rho // 或 (rInner + rOuter) / 2
|
||||
p1 = polarToXY(aStart, rMid)
|
||||
p2 = polarToXY(aEnd, rMid)
|
||||
|
||||
// 弧线路径
|
||||
if (reverse) {
|
||||
path = `M ${p2.x} ${p2.y} A ${rMid} ${rMid} 0 ${largeArc} 0 ${p1.x} ${p1.y}`
|
||||
} else {
|
||||
path = `M ${p1.x} ${p1.y} A ${rMid} ${rMid} 0 ${largeArc} 1 ${p2.x} ${p2.y}`
|
||||
}
|
||||
```
|
||||
|
||||
文字通过 `<textPath>` 的 `startOffset="50%"` 和 `text-anchor="middle"` 在路径上居中。
|
||||
|
||||
### 12.6 调试与验证
|
||||
|
||||
如需调试特定扇区的计算结果,可临时添加日志:
|
||||
|
||||
```typescript
|
||||
// 在 useLuopan.ts 中
|
||||
if (j === 0 && aStart >= 225 && aStart <= 260) {
|
||||
console.log(`扇区 [${aStart}-${aEnd}°, 层${j}]:`, {
|
||||
radialHeight, arcWidth, textLength, fontSize
|
||||
});
|
||||
}
|
||||
```
|
||||
|
||||
## 13. 参考资料
|
||||
|
||||
### 12.1 数学原理
|
||||
|
||||
- 圆环扇形形心公式:基于面积加权的质心计算
|
||||
- SVG 坐标系统:W3C SVG 规范
|
||||
- 极坐标系统:标准数学定义
|
||||
|
||||
### 12.2 技术栈
|
||||
|
||||
- Vue 3.4+
|
||||
- TypeScript 5.3+
|
||||
- Vite 5.0+
|
||||
- Vitest 1.2+
|
||||
|
||||
---
|
||||
|
||||
**版本**: 1.1.0
|
||||
**最后更新**: 2026年1月20日
|
||||
**主要变更**:
|
||||
- 新增文字布局配置常量集中管理
|
||||
- 优化字体大小自适应算法
|
||||
- 完善竖排文字特殊场景处理
|
||||
- 修复最内层扇区文字溢出问题
|
||||
Reference in New Issue
Block a user