update at 2026-01-22 18:43:01

This commit is contained in:
douboer
2026-01-22 18:43:01 +08:00
parent c23c71eabf
commit a930a99a50
23 changed files with 2082 additions and 1186 deletions

218
src/sectorBuilder.ts Normal file
View File

@@ -0,0 +1,218 @@
import type { SectorLayerConfig, Sector, TextUnit, TextRadialPosition } from './types';
import { ColorResolver } from './colorResolver';
import { splitMultiTextUnits } from './multiTextParser';
import { SECTOR_INSET_DISTANCE, TEXT_LAYOUT_CONFIG } from './constants';
import {
annularSectorCentroid,
annularSectorInsetPath,
annularSectorPath,
calculateSectorFontSize,
generateTextPath,
generateVerticalTextPath,
getTextColorForBackground,
} from './utils';
const ensureTrailingSlash = (input: string): string =>
input.endsWith('/') ? input : `${input}/`;
interface SectorBuilderOptions {
textRadialPosition?: TextRadialPosition;
svgIconPath?: string;
}
export class SectorBuilder {
private colorResolver: ColorResolver;
private textRadialPosition: TextRadialPosition;
private svgIconPath: string;
constructor(colorResolver: ColorResolver, options: SectorBuilderOptions = {}) {
this.colorResolver = colorResolver;
this.textRadialPosition = options.textRadialPosition ?? 'middle';
this.svgIconPath = ensureTrailingSlash(options.svgIconPath ?? 'src/assets/icons/');
}
buildLayer(layer: SectorLayerConfig, layerIndex: number): Sector[] {
if (!layer.divisions || layer.divisions <= 0) return [];
const sectors: Sector[] = [];
const angleStep = 360 / layer.divisions;
const startAngle = layer.startAngle ?? 0;
// 预计算层级规律填色映射,供扇区颜色合并使用
const layerColorMap = this.colorResolver.resolveLayerColors(layer);
for (let i = 0; i < layer.divisions; i++) {
const aStart = startAngle + i * angleStep;
const aEnd = aStart + angleStep;
const sectorConfig = layer.sectors?.[i];
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 textColor = getTextColorForBackground(fillColor);
const innerFill = (sectorConfig?.innerFill ?? layer.innerFill ?? 0) === 1;
const innerFillPath = innerFill
? annularSectorInsetPath(
layer.rInner,
layer.rOuter,
aStart,
aEnd,
SECTOR_INSET_DISTANCE
)
: undefined;
const sectorKey = `L${layerIndex}-P${i}`;
const textPathId = `text-path-${sectorKey}`;
const effectiveTextRadialPosition =
layerIndex === 0 ? 'centroid' : this.textRadialPosition;
const { isVertical, fontSize, textPath } = this.computeTextLayout(
layer.rInner,
layer.rOuter,
aStart,
aEnd,
rawContent.replace(/\|/g, ''),
effectiveTextRadialPosition
);
const textUnits = isMultiText
? this.buildTextUnits(
rawContent,
aStart,
aEnd,
layer.rInner,
layer.rOuter,
effectiveTextRadialPosition,
sectorKey
)
: undefined;
const isSvgContent = !isMultiText && rawContent.toLowerCase().endsWith('.svg');
const centroid = annularSectorCentroid({
rInner: layer.rInner,
rOuter: layer.rOuter,
aStartDeg: aStart,
aEndDeg: aEnd,
});
sectors.push({
key: sectorKey,
layerIndex,
pieIndex: i,
rInner: layer.rInner,
rOuter: layer.rOuter,
aStart,
aEnd,
aMidDeg: centroid.aMidDeg,
aMidRad: centroid.aMidRad,
cx: centroid.cx,
cy: centroid.cy,
fill: fillColor,
textColor,
label: isMultiText ? '' : rawContent,
path: annularSectorPath(layer.rInner, layer.rOuter, aStart, aEnd),
innerFillPath,
innerFillColor: innerFill ? fillColor : undefined,
textPath,
textPathId,
isVertical,
fontSize,
textUnits,
groupSplitVisible: this.shouldShowGroupSplit(layer, i),
isSvgContent,
svgPath: isSvgContent ? `${this.svgIconPath}${rawContent}` : undefined,
});
}
return sectors;
}
private buildTextUnits(
content: string,
aStart: number,
aEnd: number,
rInner: number,
rOuter: number,
textRadialPosition: TextRadialPosition,
sectorKey: string
): TextUnit[] {
const units = splitMultiTextUnits(content, aStart, aEnd, this.svgIconPath);
return units.map((unit, index) => {
const layout = this.computeTextLayout(
rInner,
rOuter,
unit.aStart,
unit.aEnd,
unit.content,
textRadialPosition
);
return {
...unit,
textPathId: `${sectorKey}-unit-${index}`,
textPath: layout.textPath,
fontSize: layout.fontSize,
isVertical: layout.isVertical,
};
});
}
private computeTextLayout(
rInner: number,
rOuter: number,
aStart: number,
aEnd: number,
content: string,
textRadialPosition: TextRadialPosition
): { isVertical: boolean; fontSize: number; textPath: string } {
const radialHeight = rOuter - rInner;
const deltaDeg = aEnd - aStart;
const centroid = annularSectorCentroid({
rInner,
rOuter,
aStartDeg: aStart,
aEndDeg: aEnd,
});
const arcWidth = (centroid.rho * Math.abs(deltaDeg) * Math.PI) / 180;
// 角度过窄时更适合竖排
const isVertical = arcWidth < radialHeight;
const textLength = Math.max(1, content.length);
const fontSize = calculateSectorFontSize(
rInner,
rOuter,
aStart,
aEnd,
textLength,
TEXT_LAYOUT_CONFIG.FONT_SIZE.MIN,
TEXT_LAYOUT_CONFIG.FONT_SIZE.MAX,
isVertical
);
const textPath = isVertical
? generateVerticalTextPath(
rInner,
rOuter,
aStart,
aEnd,
textRadialPosition,
textLength,
fontSize
)
: generateTextPath(rInner, rOuter, aStart, aEnd, textRadialPosition);
return { isVertical, fontSize, textPath };
}
private shouldShowGroupSplit(layer: SectorLayerConfig, sectorIndex: number): boolean {
// 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;
}
}