/** * 编辑连接线组件 * 实现从编辑点到输入框的弧线连接 */ import React, { useMemo } from 'react'; import { motion } from 'framer-motion'; import { ConnectionPathParams, InputBoxPosition } from './types'; interface EditConnectionProps { /** 起始点坐标(编辑点位置) */ startPoint: { x: number; y: number }; /** 结束点坐标(输入框位置) */ endPoint: { x: number; y: number }; /** 容器尺寸 */ containerSize: { width: number; height: number }; /** 连接线样式 */ style?: { color?: string; strokeWidth?: number; dashArray?: string; }; /** 弧线弯曲程度 */ curvature?: number; /** 是否显示动画 */ animated?: boolean; } /** * 计算弧线路径 */ function calculateCurvePath({ start, end, containerSize, curvature = 0.3 }: ConnectionPathParams): string { const dx = end.x - start.x; const dy = end.y - start.y; const distance = Math.sqrt(dx * dx + dy * dy); // 计算控制点,创建优雅的弧线 const midX = (start.x + end.x) / 2; const midY = (start.y + end.y) / 2; // 根据方向调整控制点 let controlX = midX; let controlY = midY; // 如果是水平方向较长,控制点偏向垂直方向 if (Math.abs(dx) > Math.abs(dy)) { controlY = midY + (dy > 0 ? -1 : 1) * distance * curvature; } else { // 如果是垂直方向较长,控制点偏向水平方向 controlX = midX + (dx > 0 ? -1 : 1) * distance * curvature; } // 确保控制点在容器范围内 controlX = Math.max(10, Math.min(containerSize.width - 10, controlX)); controlY = Math.max(10, Math.min(containerSize.height - 10, controlY)); // 创建二次贝塞尔曲线路径 return `M ${start.x} ${start.y} Q ${controlX} ${controlY} ${end.x} ${end.y}`; } /** * 计算最佳输入框位置 */ export function calculateInputPosition( editPointPosition: { x: number; y: number }, containerSize: { width: number; height: number }, inputBoxSize: { width: number; height: number } = { width: 200, height: 80 } ): InputBoxPosition { const { x: pointX, y: pointY } = editPointPosition; const { width: containerWidth, height: containerHeight } = containerSize; const { width: inputWidth, height: inputHeight } = inputBoxSize; const margin = 20; // 与边界的最小距离 const connectionLength = 80; // 连接线长度 // 计算各个方向的可用空间 const spaceTop = pointY; const spaceBottom = containerHeight - pointY; const spaceLeft = pointX; const spaceRight = containerWidth - pointX; let direction: 'top' | 'bottom' | 'left' | 'right' = 'right'; let inputX = pointX + connectionLength; let inputY = pointY - inputHeight / 2; let connectionEndX = pointX + connectionLength; let connectionEndY = pointY; // 优先选择右侧 if (spaceRight >= inputWidth + connectionLength + margin) { direction = 'right'; inputX = pointX + connectionLength; inputY = Math.max(margin, Math.min(containerHeight - inputHeight - margin, pointY - inputHeight / 2)); connectionEndX = inputX; connectionEndY = inputY + inputHeight / 2; } // 其次选择左侧 else if (spaceLeft >= inputWidth + connectionLength + margin) { direction = 'left'; inputX = pointX - connectionLength - inputWidth; inputY = Math.max(margin, Math.min(containerHeight - inputHeight - margin, pointY - inputHeight / 2)); connectionEndX = inputX + inputWidth; connectionEndY = inputY + inputHeight / 2; } // 然后选择下方 else if (spaceBottom >= inputHeight + connectionLength + margin) { direction = 'bottom'; inputX = Math.max(margin, Math.min(containerWidth - inputWidth - margin, pointX - inputWidth / 2)); inputY = pointY + connectionLength; connectionEndX = inputX + inputWidth / 2; connectionEndY = inputY; } // 最后选择上方 else if (spaceTop >= inputHeight + connectionLength + margin) { direction = 'top'; inputX = Math.max(margin, Math.min(containerWidth - inputWidth - margin, pointX - inputWidth / 2)); inputY = pointY - connectionLength - inputHeight; connectionEndX = inputX + inputWidth / 2; connectionEndY = inputY + inputHeight; } // 如果空间不足,强制放在右侧并调整位置 else { direction = 'right'; inputX = Math.min(containerWidth - inputWidth - margin, pointX + 40); inputY = Math.max(margin, Math.min(containerHeight - inputHeight - margin, pointY - inputHeight / 2)); connectionEndX = inputX; connectionEndY = inputY + inputHeight / 2; } return { x: inputX, y: inputY, connectionEnd: { x: connectionEndX, y: connectionEndY }, direction }; } /** * 编辑连接线组件 */ export const EditConnection: React.FC = ({ startPoint, endPoint, containerSize, style = {}, curvature = 0.3, animated = true }) => { const { color = 'rgba(255, 255, 255, 0.8)', strokeWidth = 2, dashArray = '5,5' } = style; // 计算路径 const path = useMemo(() => calculateCurvePath({ start: startPoint, end: endPoint, containerSize, curvature }), [startPoint, endPoint, containerSize, curvature]); // 计算路径长度用于动画 const pathLength = useMemo(() => { const dx = endPoint.x - startPoint.x; const dy = endPoint.y - startPoint.y; return Math.sqrt(dx * dx + dy * dy) * 1.2; // 弧线比直线长约20% }, [startPoint, endPoint]); return ( {/* 连接线路径 */} {/* 连接线末端的小圆点 */} {/* 动画流动效果(可选) */} {animated && ( )} ); };