251 lines
7.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 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<EditConnectionProps> = ({
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 (
<svg
className="absolute inset-0 pointer-events-none"
width={containerSize.width}
height={containerSize.height}
style={{ zIndex: 10 }}
>
{/* 统一的虚线连接线 - 精确连接到箭头中心 */}
<motion.path
d={path}
fill="none"
stroke={color}
strokeWidth={strokeWidth}
strokeDasharray={dashArray}
strokeLinecap="round"
strokeLinejoin="round"
initial={animationConfig.line.initial}
animate={animationConfig.line.animate}
transition={animationConfig.line.transition}
style={{
filter: CONNECTION_STYLE.dropShadow
}}
/>
{/* 几何精确的箭头 - 与连接线完美对齐 */}
<motion.polygon
points={arrowGeometry.points.map(p => `${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 */}
<circle
cx={arrowGeometry.base.x}
cy={arrowGeometry.base.y}
r={1}
fill="red"
opacity={0.5}
/>
{/* Arrow tip point */}
<circle
cx={arrowGeometry.tip.x}
cy={arrowGeometry.tip.y}
r={1}
fill="blue"
opacity={0.5}
/>
</>
)}
</svg>
);
};