Files
lupin-demo/src/sectorBuilder.ts
2026-01-22 20:42:57 +08:00

227 lines
6.7 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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;
insetDistance?: number;
}
export class SectorBuilder {
private colorResolver: ColorResolver;
private textRadialPosition: TextRadialPosition;
private svgIconPath: string;
private insetDistance: number;
constructor(colorResolver: ColorResolver, options: SectorBuilderOptions = {}) {
this.colorResolver = colorResolver;
this.textRadialPosition = options.textRadialPosition ?? 'middle';
this.svgIconPath = ensureTrailingSlash(options.svgIconPath ?? 'src/assets/icons/');
this.insetDistance =
typeof options.insetDistance === 'number'
? Math.max(0, options.insetDistance)
: SECTOR_INSET_DISTANCE;
}
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,
this.insetDistance
)
: undefined;
const hasInnerFillPath = Boolean(innerFillPath);
const baseFillColor = hasInnerFillPath ? '#ffffff' : fillColor;
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: baseFillColor,
textColor,
label: isMultiText ? '' : rawContent,
path: annularSectorPath(layer.rInner, layer.rOuter, aStart, aEnd),
innerFillPath,
innerFillColor: hasInnerFillPath ? 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;
}
}