update at 2026-01-23 12:07:22
This commit is contained in:
@@ -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,
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -37,7 +37,7 @@ export {
|
||||
getTextColorForBackground,
|
||||
} from './utils';
|
||||
|
||||
// Composables 导出
|
||||
// 组合式函数导出
|
||||
export { useLuopan } from './composables/useLuopan';
|
||||
export type { UseLuopanReturn } from './composables/useLuopan';
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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}`,
|
||||
|
||||
Reference in New Issue
Block a user