update at 2026-01-23 12:07:22

This commit is contained in:
douboer@gmail.com
2026-01-23 12:07:22 +08:00
parent 405576c7c3
commit f0234d1d8a
9 changed files with 45 additions and 25 deletions

View File

@@ -321,7 +321,7 @@ const props = withDefaults(defineProps<Props>(), {
/**
* 状态
*/
const showGuides = ref(true);
const showGuides = ref(false);
const textRadialPosition = ref<TextRadialPosition>(DEFAULT_TEXT_RADIAL_POSITION);
// 缩放和平移状态
@@ -362,7 +362,7 @@ const {
error,
} = useLuopan(configInput, textRadialPosition);
// viewBox 以实际外半径为准,确保完整显示配置中的大半径罗盘
// 以实际外半径作为 `viewBox`,确保完整显示配置中的大半径罗盘
const viewBoxSize = computed(() => {
const radius = outerMost.value > 0 ? outerMost.value : props.size / 2;
return radius * 2;
@@ -388,7 +388,7 @@ const boundaryRings = computed(() => {
return Array.from(set);
});
// 使用 rAF 合并缩放/拖拽更新,减少渲染频率
// 使用 `rAF` 合并缩放/拖拽更新,减少渲染频率
const scheduleTransform = () => {
if (rafId !== null) return;
const requestFrame =
@@ -416,7 +416,7 @@ const setPan = (x: number, y: number) => {
};
const getUnitSvgBox = (sector: Sector, unit: TextUnit) => {
// SVG 图标与文字共享布局规则,使用单元角度范围计算形心位置
// `SVG` 图标与文字共享布局规则,使用单元角度范围计算形心位置
const centroid = annularSectorCentroid({
rInner: sector.rInner,
rOuter: sector.rOuter,

View File

@@ -19,6 +19,7 @@ export const applyPatternColoring = (
return colorMap;
}
// 规律填色:连续着色 `num` 个扇区,然后跳过 `interval` 个扇区。
let currentIndex = 0;
while (currentIndex < divisions) {
for (let i = 0; i < num && currentIndex < divisions; i++) {
@@ -70,6 +71,7 @@ export class ColorResolver {
sector: SectorConfig | undefined,
sectorIndex: number
): string {
// 优先级:扇区 `colorRef` > 层级规律色 > 背景。
if (sector?.colorRef) {
return this.resolveColor(sector.colorRef);
}

View File

@@ -19,6 +19,7 @@ import { SectorBuilder } from '../sectorBuilder';
import { buildDegreeRing } from '../degreeRing';
import { loadCenterIcon } from '../centerIcon';
// 只有扇区层会生成扇区几何,其它层仍参与层级顺序。
const isSectorLayer = (layer: LayerConfig): layer is SectorLayerConfig =>
layer.type !== 'centerIcon' && layer.type !== 'degreeRing';
@@ -67,8 +68,10 @@ export function useLuopan(
textRadialPosition: textRadialPosition.value,
insetDistance: configObj.insetDistance,
});
const sectorLayers = configObj.layers.filter(isSectorLayer);
return sectorLayers.flatMap((layer, index) => builder.buildLayer(layer, index));
// 层索引与配置顺序一致(`centerIcon`/`degreeRing` 仍占位)。
return configObj.layers.flatMap((layer, index) =>
isSectorLayer(layer) ? builder.buildLayer(layer, index) : []
);
};
const loadConfig = async () => {
@@ -156,6 +159,7 @@ export function useLuopan(
const outerMost = computed(() => {
if (!config.value) return 0;
// 取扇区层与刻度环中的最大半径。
const radii = sectorLayers.value.map((layer) => layer.rOuter);
const degreeRingLayer = findDegreeRingLayer(config.value.layers);
if (degreeRingLayer) {

View File

@@ -1,7 +1,7 @@
import type { DegreeRingConfig, TickMark, DegreeLabel, DegreeRingData } from './types';
import { generateTextPath, polarToXY } from './utils';
// 根据刻度级别计算长度,后续由 clamp 处理最小值
// 根据刻度级别计算长度,最小值`clampTickLength` 兜底
const resolveTickLength = (config: DegreeRingConfig, type: TickMark['type']): number => {
const step = config.tickLengthStep ?? 0;
if (type === 'major') return config.tickLength;
@@ -70,7 +70,7 @@ export function buildDegreeRing(config: DegreeRingConfig): DegreeRingData {
continue;
}
// both: 同角度生成内外两条刻度线
// 模式为 `both` 时,同角度生成内外两条刻度线
const innerStart = polarToXY(angle, rInner);
const innerEnd = polarToXY(angle, rInner + length);
ticks.push({
@@ -111,7 +111,7 @@ export function buildDegreeRing(config: DegreeRingConfig): DegreeRingData {
const aStart = angle - span / 2;
const aEnd = angle + span / 2;
// 使用 textPath 保持度数方向与扇区文字一致
// 使用 `textPath` 保持度数方向与扇区文字一致
labels.push({
angle,
text,

View File

@@ -37,7 +37,7 @@ export {
getTextColorForBackground,
} from './utils';
// Composables 导出
// 组合式函数导出
export { useLuopan } from './composables/useLuopan';
export type { UseLuopanReturn } from './composables/useLuopan';

View File

@@ -53,12 +53,13 @@ export class SectorBuilder {
const rawContent = typeof sectorConfig?.content === 'string' ? sectorConfig.content.trim() : '';
const isMultiText = rawContent.includes('|');
// 颜色优先级:sector > layer pattern > background
// 颜色优先级:扇区 > 规律填色 > 背景
const fillColor = this.colorResolver.resolveSectorColor(layerColorMap, sectorConfig, i);
const layerColor = layer.colorRef ? this.colorResolver.resolveColor(layer.colorRef) : undefined;
const sectorColor = sectorConfig?.colorRef
? this.colorResolver.resolveColor(sectorConfig.colorRef)
: undefined;
// 扇区的 `innerFill` 优先级高于层级的 `innerFill`。
const innerFill = (sectorConfig?.innerFill ?? layer.innerFill ?? 0) === 1;
const innerFillPath = innerFill
? annularSectorInsetPath(
@@ -72,6 +73,7 @@ export class SectorBuilder {
const normalizedInnerFillPath =
innerFillPath && innerFillPath.length > 0 ? innerFillPath : undefined;
const hasInnerFillPath = Boolean(normalizedInnerFillPath);
// `innerFill` 开启时:外圈保持白色,仅填充内缩块。
const baseFillColor = hasInnerFillPath ? '#ffffff' : fillColor;
const innerFillColor = hasInnerFillPath ? sectorColor ?? layerColor ?? fillColor : undefined;
const textBaseColor = hasInnerFillPath ? innerFillColor ?? fillColor : fillColor;
@@ -79,6 +81,7 @@ export class SectorBuilder {
const sectorKey = `L${layerIndex}-P${i}`;
const textPathId = `text-path-${sectorKey}`;
// 最内层使用形心位置,保证文字可读性。
const effectiveTextRadialPosition =
layerIndex === 0 ? 'centroid' : this.textRadialPosition;
@@ -222,13 +225,14 @@ export class SectorBuilder {
}
private shouldShowGroupSplit(layer: SectorLayerConfig, sectorIndex: number): boolean {
// groupSplit 关闭时,仅保留分组边界线
// `groupSplit` 关闭时,仅保留分组边界线
if (layer.groupSplit !== false) return true;
if (!layer.num) return true;
const cycleLength = layer.num + (layer.interval ?? 0);
const posInCycle = sectorIndex % cycleLength;
// 仅保留每个着色分组的末尾分割线。
return posInCycle >= layer.num - 1;
}
}

View File

@@ -92,7 +92,7 @@ export function annularSectorCentroid(params: AnnularSectorParams): CentroidResu
return { cx: 0, cy: 0, rho: 0, aMidDeg, aMidRad, deltaDeg };
}
// rho = (2/3) * (r2^3 - r1^3)/(r2^2 - r1^2) * sinc(delta/2)
// 形心径向距离公式:`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));
@@ -124,7 +124,7 @@ export function annularSectorPath(
let delta = a2 - a1;
if (delta < 0) delta += 360;
// SVG arc flags
// `SVG` 圆弧标记位
const largeArc = delta > 180 ? 1 : 0;
const sweepOuter = 1;
const sweepInner = 0;
@@ -134,7 +134,7 @@ export function annularSectorPath(
const p3 = polarToXY(a2, rInner);
const p4 = polarToXY(a1, rInner);
// 如果 rInner=0内弧退化为点
// 如果 `rInner`=0内弧退化为点
if (rInner <= 0.000001) {
return [
`M ${p1.x} ${p1.y}`,