import type { JSXElement } from '../../jsx'; import { Path, Polygon } from '../../jsx'; export const getMidPoint = (points: [number, number][]) => { if (points.length === 0) return null; if (points.length === 1) return points[0]; let total = 0; const segments: { length: number; start: [number, number]; end: [number, number]; }[] = []; for (let i = 0; i < points.length - 1; i += 1) { const start = points[i]; const end = points[i + 1]; const length = Math.hypot(end[0] - start[0], end[1] - start[1]); segments.push({ length, start, end }); total += length; } if (total === 0) return points[0]; let target = total / 2; for (let i = 0; i < segments.length; i += 1) { const segment = segments[i]; if (target <= segment.length || i === segments.length - 1) { const ratio = segment.length === 0 ? 0 : Math.max(0, Math.min(1, target / segment.length)); return [ segment.start[0] + (segment.end[0] - segment.start[0]) * ratio, segment.start[1] + (segment.end[1] - segment.start[1]) * ratio, ] as [number, number]; } target -= segment.length; } return points[Math.floor(points.length / 2)]; }; export const createStraightPath = ( points: [number, number][], dx: number, dy: number, ) => points .map(([x, y], index) => { const prefix = index === 0 ? 'M' : 'L'; return `${prefix} ${x + dx} ${y + dy}`; }) .join(' '); export const createRoundedPath = ( points: [number, number][], radius: number, dx: number, dy: number, ) => { if (points.length < 2) return ''; const clamp = (value: number, min: number, max: number) => Math.min(max, Math.max(min, value)); const toPoint = ([x, y]: [number, number]) => ({ x: x + dx, y: y + dy, }); const output: string[] = []; const first = toPoint(points[0]); output.push(`M ${first.x} ${first.y}`); if (points.length === 2) { const last = toPoint(points[1]); output.push(`L ${last.x} ${last.y}`); return output.join(' '); } for (let i = 1; i < points.length - 1; i += 1) { const prev = points[i - 1]; const curr = points[i]; const next = points[i + 1]; const v0x = curr[0] - prev[0]; const v0y = curr[1] - prev[1]; const v1x = next[0] - curr[0]; const v1y = next[1] - curr[1]; const d0 = Math.hypot(v0x, v0y); const d1 = Math.hypot(v1x, v1y); if (d0 === 0 || d1 === 0) { const currPoint = toPoint(curr); output.push(`L ${currPoint.x} ${currPoint.y}`); continue; } const r = clamp(radius, 0, Math.min(d0, d1) / 2); if (r === 0) { const currPoint = toPoint(curr); output.push(`L ${currPoint.x} ${currPoint.y}`); continue; } const u0x = v0x / d0; const u0y = v0y / d0; const u1x = v1x / d1; const u1y = v1y / d1; const start = toPoint([curr[0] - u0x * r, curr[1] - u0y * r]); const end = toPoint([curr[0] + u1x * r, curr[1] + u1y * r]); output.push(`L ${start.x} ${start.y}`); const currPoint = toPoint(curr); output.push(`Q ${currPoint.x} ${currPoint.y} ${end.x} ${end.y}`); } const last = toPoint(points[points.length - 1]); output.push(`L ${last.x} ${last.y}`); return output.join(' '); }; export const createArrowElements = ( x: number, y: number, angle: number, type: 'arrow' | 'triangle' | 'diamond', fillColor: string, edgeWidth: number, arrowSize: number, ): JSXElement[] => { const ux = Math.cos(angle); const uy = Math.sin(angle); const px = -uy; const py = ux; const length = arrowSize; const halfWidth = arrowSize * 0.55; if (type === 'arrow') { const leftX = x - ux * length + px * halfWidth; const leftY = y - uy * length + py * halfWidth; const rightX = x - ux * length - px * halfWidth; const rightY = y - uy * length - py * halfWidth; return [ , ]; } if (type === 'diamond') { const diamondLength = length * 1.25; const diamondWidth = halfWidth * 0.75; const midX = x - ux * diamondLength * 0.5; const midY = y - uy * diamondLength * 0.5; const diamondPoints = [ { x, y }, { x: midX + px * diamondWidth, y: midY + py * diamondWidth }, { x: x - ux * diamondLength, y: y - uy * diamondLength }, { x: midX - px * diamondWidth, y: midY - py * diamondWidth }, ]; return [ , ]; } const trianglePoints = [ { x, y }, { x: x - ux * length + px * halfWidth, y: y - uy * length + py * halfWidth, }, { x: x - ux * length - px * halfWidth, y: y - uy * length - py * halfWidth, }, ]; return [ , ]; }; // LT: Left Top (radio), LC: Left Center (1/2), LB: Left Bottom (1 - radio) // RT: Right Top (radio), RC: Right Center (1/2), RB: Right Bottom (1 - radio) export const getNodesAnchors = (node: { x: number; y: number; width: number; height: number; radio?: number; }) => { const { x, y, width, height, radio = 0.25 } = node; const q1H = height * radio; const halfH = height * 0.5; const q3H = height * (1 - radio); return { LT: { x, y: y + q1H }, LC: { x, y: y + halfH }, LB: { x, y: y + q3H }, RT: { x: x + width, y: y + q1H }, RC: { x: x + width, y: y + halfH }, RB: { x: x + width, y: y + q3H }, }; }; export const getTangentAngle = (points: [number, number][], t: 0 | 1) => { const len = points.length; // Cubic Bezier (Self loop) if (len === 4) { const p0 = points[0], p1 = points[1], p2 = points[2], p3 = points[3]; if (t === 0) { return Math.atan2(p1[1] - p0[1], p1[0] - p0[0]); } else { return Math.atan2(p3[1] - p2[1], p3[0] - p2[0]); } } // Quad Bezier (Curved) if (len === 3) { const p0 = points[0], p1 = points[1], p2 = points[2]; if (t === 0) { return Math.atan2(p1[1] - p0[1], p1[0] - p0[0]); } else { return Math.atan2(p2[1] - p1[1], p2[0] - p1[0]); } } // Line if (len === 2) { const p0 = points[0], p1 = points[1]; const angle = Math.atan2(p1[1] - p0[1], p1[0] - p0[0]); return angle; } return 0; }; /** * 计算贝塞尔曲线上任意 t (0-1) 位置的点 */ export const getPointAtT = ( points: [number, number][], t = 0.5, ): [number, number] => { const len = points.length; if (len === 4) { const [p0, p1, p2, p3] = points; const mt = 1 - t; // B(t) = (1-t)^3*P0 + 3(1-t)^2*t*P1 + 3(1-t)*t^2*P2 + t^3*P3 return [ mt ** 3 * p0[0] + 3 * mt ** 2 * t * p1[0] + 3 * mt * t ** 2 * p2[0] + t ** 3 * p3[0], mt ** 3 * p0[1] + 3 * mt ** 2 * t * p1[1] + 3 * mt * t ** 2 * p2[1] + t ** 3 * p3[1], ]; } if (len === 3) { const [p0, p1, p2] = points; const mt = 1 - t; // B(t) = (1-t)^2*P0 + 2(1-t)*t*P1 + t^2*P2 return [ mt ** 2 * p0[0] + 2 * mt * t * p1[0] + t ** 2 * p2[0], mt ** 2 * p0[1] + 2 * mt * t * p1[1] + t ** 2 * p2[1], ]; } if (len === 2) { const [p0, p1] = points; return [p0[0] + (p1[0] - p0[0]) * t, p0[1] + (p1[1] - p0[1]) * t]; } return points[0] || [0, 0]; }; export const getLabelPosition = ( points: [number, number][], selfLoopOffset = 10, ) => { const len = points.length; // 默认取中点 const labelPoint = getPointAtT(points); if (len === 4) { // 针对自连接(len=4)的特殊偏移处理 labelPoint[0] += selfLoopOffset; } return labelPoint as [number, number]; }; export const getEdgePathD = (points: [number, number][]) => { const len = points.length; if (len === 4) { const [p0, p1, p2, p3] = points; return `M ${p0[0]} ${p0[1]} C ${p1[0]} ${p1[1]} ${p2[0]} ${p2[1]} ${p3[0]} ${p3[1]}`; } if (len === 3) { const [p0, p1, p2] = points; return `M ${p0[0]} ${p0[1]} Q ${p1[0]} ${p1[1]} ${p2[0]} ${p2[1]}`; } if (len === 2) { const [p0, p1] = points; return `M ${p0[0]} ${p0[1]} L ${p1[0]} ${p1[1]}`; } return ''; };