update at 2026-01-22 18:43:01
This commit is contained in:
218
src/sectorBuilder.ts
Normal file
218
src/sectorBuilder.ts
Normal 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;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user