import {
getElementBounds,
Group,
Path,
Text,
type ComponentType,
type JSXElement,
} from '../../jsx';
import { ItemIconCircle } from '../components';
import { Triangle } from '../decorations';
import { FlexLayout } from '../layouts';
import { getPaletteColor } from '../utils';
import { registerStructure } from './registry';
import type { BaseStructureProps } from './types';
export interface SequenceRoadmapVerticalProps extends BaseStructureProps {
/** Item spacing */
spacing?: number;
flipped?: boolean;
}
const CONFIG = {
iconSize: 50,
roadWidth: 24,
outerRadius: 60,
rowWidth: 400,
spacing: 30,
colorDefault: '#666666',
};
const moveTo = (x: number, y: number) => `M ${x} ${y}`;
const lineTo = (x: number, y: number) => `L ${x} ${y}`;
const arcTo = (r: number, sweep: 0 | 1, x: number, y: number) =>
`A ${r} ${r} 0 0 ${sweep} ${x} ${y}`;
const getYPositions = (
i: number,
{
roadWidth,
innerRadius,
outerRadius,
}: { roadWidth: number; innerRadius: number; outerRadius: number },
) => {
const y1 = (roadWidth + innerRadius * 2) * i;
return {
y1,
y2: y1 + roadWidth,
y3: y1 + roadWidth + innerRadius,
y4: y1 + roadWidth + innerRadius * 2,
y5: y1 + outerRadius * 2,
};
};
function renderItemRow({
i,
direction,
x,
y,
color,
data,
itemBounds,
item,
Item,
flipped,
}: any) {
const { iconSize } = CONFIG;
const isLeft = direction === 'left';
const iconX = isLeft ? x.x4 - iconSize / 2 : x.x3 - iconSize / 2;
const iconY = y.y3 - iconSize / 2;
const itemX = isLeft
? x.x6 + CONFIG.spacing
: x.x1 - CONFIG.spacing - itemBounds.width;
const itemY = y.y3 - itemBounds.height / 2;
// 根据 flipped 参数决定 positionH
const positionH = isLeft
? flipped
? 'flipped'
: 'normal'
: flipped
? 'normal'
: 'flipped';
return {
icon: (
),
label: (
{String(i + 1).padStart(2, '0')}
),
item: (
),
};
}
function buildDecorations({ direction, x, y, color, elements }: any) {
const isLeft = direction === 'left';
elements.push(
,
);
}
export const SequenceRoadmapVertical: ComponentType<
SequenceRoadmapVerticalProps
> = (props) => {
const {
Title,
Item,
data,
spacing = CONFIG.spacing,
options,
flipped = false,
} = props;
const { title, desc, items = [] } = data;
const titleContent = Title ?
: null;
const itemBounds = getElementBounds(
,
);
const { roadWidth, outerRadius, rowWidth, colorDefault } = CONFIG;
const halfRoadWidth = roadWidth / 2;
const innerRadius = outerRadius - roadWidth;
const totalWidth = (itemBounds.width + spacing) * 2 + rowWidth;
const x1 = itemBounds.width + spacing;
const x3 = x1 + outerRadius;
const x4 = x1 + rowWidth - outerRadius;
const x5 = x4 + innerRadius;
const x6 = x5 + roadWidth;
const xMid = x1 + rowWidth / 2;
const midPath: string[] = [];
const positivePath: string[] = [];
const negativePath: string[] = [];
const itemIcons: JSXElement[] = [];
const seriesNumber: JSXElement[] = [];
const itemElements: JSXElement[] = [];
const decorationElements: JSXElement[] = [];
for (let i = 0; i < items.length; i++) {
const color = getPaletteColor(options, [i]) || colorDefault;
const direction = i % 2 === 0 ? 'right' : 'left';
const isFirst = i === 0;
const isLast = i === items.length - 1;
const y = getYPositions(i, { roadWidth, innerRadius, outerRadius });
if (direction === 'right') {
const p1: [number, number] = isFirst ? [x6, y.y1] : [x4, y.y1];
const p2: [number, number] = [x3, y.y1];
const p3: [number, number] = [x3, y.y5];
const p4: [number, number] = isFirst ? [x6, y.y2] : [x4, y.y2];
const p5: [number, number] = [x3, y.y2];
midPath.push(
isFirst
? moveTo(x6, y.y1 + halfRoadWidth)
: lineTo(x4, y.y1 + halfRoadWidth),
lineTo(x3, y.y1 + halfRoadWidth),
arcTo(outerRadius - halfRoadWidth, 0, x3, y.y4 + halfRoadWidth),
);
positivePath.push(
isFirst ? moveTo(...p1) : lineTo(...p1),
lineTo(...p2),
arcTo(outerRadius, 0, ...p3),
);
negativePath.push(
lineTo(...p4),
lineTo(...p5),
arcTo(innerRadius, 1, ...p5),
);
if (isLast) {
const s = roadWidth / 2;
positivePath.push(
lineTo(xMid, y.y5),
lineTo(xMid, y.y5 + s),
lineTo(xMid + roadWidth, y.y5 - s),
lineTo(xMid, y.y4 - s),
lineTo(xMid, y.y4),
lineTo(x3, y.y4),
);
midPath.push(lineTo(xMid, y.y4 + halfRoadWidth));
}
} else {
const p1: [number, number] = [x3, y.y2];
const p2: [number, number] = [x4, y.y2];
const p3: [number, number] = [x4, y.y4];
const p4: [number, number] = [x3, y.y1];
const p5: [number, number] = [x4, y.y1];
midPath.push(
lineTo(x4, y.y1 + halfRoadWidth),
arcTo(outerRadius - halfRoadWidth, 1, x4, y.y4 + halfRoadWidth),
);
positivePath.push(
lineTo(...p1),
lineTo(...p2),
arcTo(innerRadius, 1, ...p3),
);
negativePath.push(
lineTo(...p4),
lineTo(...p5),
arcTo(outerRadius, 0, ...p5),
);
if (isLast) {
const s = roadWidth / 2;
positivePath.push(
lineTo(xMid, y.y4),
lineTo(xMid, y.y4 - s),
lineTo(xMid - roadWidth, y.y4 + s),
lineTo(xMid, y.y5 + s),
lineTo(xMid, y.y5),
lineTo(x4, y.y5),
);
midPath.push(lineTo(xMid, y.y4 + halfRoadWidth));
}
}
// 装饰
buildDecorations({
direction,
x: { x1, x4, x6 },
y,
color,
elements: decorationElements,
});
// 元素
const { icon, label, item } = renderItemRow({
i,
direction,
x: { x1, x3, x4, x6 },
y,
color,
data,
itemBounds,
item: items[i],
Item,
flipped,
});
itemIcons.push(icon);
seriesNumber.push(label);
itemElements.push(item);
}
const pathArr = [...positivePath, ...negativePath.reverse(), 'Z'];
const roadmapHeight =
items.length * (roadWidth + innerRadius * 2) + roadWidth * 1.5;
return (
{titleContent}
<>
{itemIcons}
{seriesNumber}
{itemElements}
{decorationElements}
>
);
};
registerStructure('sequence-roadmap-vertical', {
component: SequenceRoadmapVertical,
composites: ['title', 'item'],
});