/** * 编辑连接线组件 * 实现从编辑点到输入框的弧线连接 */ import React, { useMemo } from 'react'; import { motion } from 'framer-motion'; import { ConnectionPathParams, InputBoxPosition } from './types'; import { CONNECTION_STYLE, ARROW_GEOMETRY, calculateArrowGeometry, calculateCurvePath as calculateUnifiedCurvePath, getConnectionAnimationConfig } from './connection-config'; 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: 300, height: 50 } ): 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 - 8; // 向内偏移8px,指向输入框内部 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 + 8; // 向内偏移8px,指向输入框内部 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 - 8; // 向内偏移8px,指向输入框内部 } // 最后选择上方 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 + 8; // 向内偏移8px,指向输入框内部 } // 如果空间不足,强制放在右侧并调整位置 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 - 8; // 向内偏移8px,指向输入框内部 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 = CONNECTION_STYLE.color, strokeWidth = CONNECTION_STYLE.strokeWidth, dashArray = CONNECTION_STYLE.dashArray } = style; // 使用统一的箭头几何计算 const arrowGeometry = useMemo(() => calculateArrowGeometry(startPoint, endPoint), [startPoint, endPoint] ); // 使用统一的路径计算 const path = useMemo(() => calculateUnifiedCurvePath(startPoint, arrowGeometry.center, containerSize), [startPoint, arrowGeometry.center, containerSize] ); // 获取统一的动画配置 const animationConfig = useMemo(() => getConnectionAnimationConfig(animated), [animated] ); return ( {/* 统一的虚线连接线 - 精确连接到箭头中心 */} {/* 几何精确的箭头 - 与连接线完美对齐 */} `${p.x},${p.y}`).join(' ')} fill={color} initial={animationConfig.arrow.initial} animate={animationConfig.arrow.animate} transition={animationConfig.arrow.transition} style={{ filter: CONNECTION_STYLE.dropShadow }} /> {/* Debug visualization (remove in production) */} {process.env.NODE_ENV === 'development' && ( <> {/* Arrow base center point */} {/* Arrow tip point */} )} ); };