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; } }