451 lines
12 KiB
Vue
451 lines
12 KiB
Vue
<template>
|
||
<g :data-wire-id="props.id">
|
||
<path :d="pathData" fill="none" :stroke="computedStroke" :stroke-width="strokeWidth" stroke-linecap="round"
|
||
stroke-linejoin="round" :class="{ 'wire-active': props.isActive }" @click="handleClick" />
|
||
<!-- 可选:添加连线标签或状态指示器 -->
|
||
<text v-if="showLabel" :x="labelPosition.x" :y="labelPosition.y" class="wire-label">{{ props.constraint || ""
|
||
}}</text>
|
||
</g>
|
||
</template>
|
||
|
||
<script setup lang="ts">
|
||
import { useConstraintsStore } from "@/stores/constraints";
|
||
import { computed, defineEmits, watch, onMounted, onUnmounted } from "vue";
|
||
|
||
const { getConstraintColor, onConstraintStateChange } = useConstraintsStore();
|
||
|
||
interface Props {
|
||
id: string;
|
||
startX: number;
|
||
startY: number;
|
||
endX: number;
|
||
endY: number;
|
||
strokeColor?: string;
|
||
strokeWidth?: number;
|
||
isActive?: boolean;
|
||
routingMode?: "auto" | "orthogonal" | "direct" | "path";
|
||
// 针脚引用属性
|
||
startComponentId?: string;
|
||
startPinId?: string;
|
||
endComponentId?: string;
|
||
endPinId?: string;
|
||
// 添加约束属性
|
||
constraint?: string;
|
||
// 显示标签
|
||
showLabel?: boolean;
|
||
// 路径命令 - 对应diagram.json中的线放置迷你语言
|
||
pathCommands?: string[];
|
||
}
|
||
|
||
const props = withDefaults(defineProps<Props>(), {
|
||
strokeColor: "#4a5568",
|
||
strokeWidth: 2,
|
||
isActive: false,
|
||
routingMode: "orthogonal",
|
||
showLabel: false,
|
||
constraint: "",
|
||
});
|
||
|
||
// 响应约束状态变化的颜色
|
||
const constraintColor = computed(() => {
|
||
if (!props.constraint) return props.strokeColor;
|
||
return getConstraintColor(props.constraint);
|
||
});
|
||
|
||
// 计算实际使用的颜色:isActive优先,其次是constraint电平颜色,最后是默认色
|
||
const computedStroke = computed(() => {
|
||
if (props.isActive) return "#ff9800";
|
||
return constraintColor.value || props.strokeColor;
|
||
});
|
||
|
||
const emit = defineEmits(["click", "update:active", "update:position"]);
|
||
|
||
function handleClick(event: MouseEvent) {
|
||
emit("click", { id: props.id, event });
|
||
}
|
||
|
||
// 计算标签位置 - 放在连线中间
|
||
const labelPosition = computed(() => {
|
||
return {
|
||
x: (props.startX + props.endX) / 2,
|
||
y: (props.startY + props.endY) / 2 - 5,
|
||
};
|
||
});
|
||
|
||
const pathData = computed(() => {
|
||
// 如果有路径命令,使用路径布线模式
|
||
if (
|
||
props.routingMode === "path" &&
|
||
props.pathCommands &&
|
||
props.pathCommands.length > 0
|
||
) {
|
||
return calculatePathFromCommands(
|
||
props.startX,
|
||
props.startY,
|
||
props.endX,
|
||
props.endY,
|
||
props.pathCommands,
|
||
);
|
||
}
|
||
// 否则使用正交路径
|
||
return calculateOrthogonalPath(
|
||
props.startX,
|
||
props.startY,
|
||
props.endX,
|
||
props.endY,
|
||
);
|
||
});
|
||
|
||
function calculatePathFromCommands(
|
||
startX: number,
|
||
startY: number,
|
||
endX: number,
|
||
endY: number,
|
||
commands: string[],
|
||
) {
|
||
// 找到分隔符索引,通常是 "*"
|
||
const splitterIndex = commands.indexOf("*");
|
||
if (splitterIndex === -1) {
|
||
// 如果没有分隔符,回退到正交路径
|
||
return calculateOrthogonalPath(startX, startY, endX, endY);
|
||
}
|
||
|
||
// 分割命令为起点和终点两部分
|
||
const startCommands = commands.slice(0, splitterIndex);
|
||
const endCommands = commands.slice(splitterIndex + 1);
|
||
|
||
// 从起点开始生成路径
|
||
let currentX = startX;
|
||
let currentY = startY;
|
||
|
||
// 处理起点路径命令
|
||
const pathPoints: [number, number][] = [[currentX, currentY]];
|
||
|
||
// 解析并执行起点命令
|
||
for (const cmd of startCommands) {
|
||
const { newX, newY } = executePathCommand(currentX, currentY, cmd);
|
||
currentX = newX;
|
||
currentY = newY;
|
||
pathPoints.push([currentX, currentY]);
|
||
}
|
||
// 从终点开始反向处理
|
||
let endCurrentX = endX;
|
||
let endCurrentY = endY;
|
||
|
||
// 保存终点路径点,最后会反转
|
||
const endPathPoints: [number, number][] = [[endCurrentX, endCurrentY]];
|
||
|
||
// 解析并执行终点命令(需要从后向前执行反向命令)
|
||
for (let i = endCommands.length - 1; i >= 0; i--) {
|
||
const cmd = reversePathCommand(endCommands[i]);
|
||
const { newX, newY } = executePathCommand(endCurrentX, endCurrentY, cmd);
|
||
endCurrentX = newX;
|
||
endCurrentY = newY;
|
||
endPathPoints.push([endCurrentX, endCurrentY]);
|
||
} // 反转终点路径点,保留所有点
|
||
const reversedEndPoints = [...endPathPoints].reverse();
|
||
|
||
// 将两部分路径连接起来
|
||
// 如果终点路径的第一个点与起点路径的最后一个点相同,则去掉重复点
|
||
let combinedPoints;
|
||
if (
|
||
pathPoints.length > 0 &&
|
||
reversedEndPoints.length > 0 &&
|
||
pathPoints[pathPoints.length - 1][0] === reversedEndPoints[0][0] &&
|
||
pathPoints[pathPoints.length - 1][1] === reversedEndPoints[0][1]
|
||
) {
|
||
combinedPoints = [...pathPoints, ...reversedEndPoints.slice(1)];
|
||
} else {
|
||
combinedPoints = [...pathPoints, ...reversedEndPoints];
|
||
}
|
||
|
||
const allPoints = combinedPoints;
|
||
// 检查是否需要添加中间连接点
|
||
if (allPoints.length >= 2) {
|
||
const startPathEndPoint =
|
||
pathPoints.length > 0 ? pathPoints[pathPoints.length - 1] : null;
|
||
const endPathStartPoint =
|
||
reversedEndPoints.length > 0 ? reversedEndPoints[0] : null;
|
||
|
||
// 只有当起点路径和终点路径不相连时才添加连接线
|
||
if (
|
||
startPathEndPoint &&
|
||
endPathStartPoint &&
|
||
(startPathEndPoint[0] !== endPathStartPoint[0] ||
|
||
startPathEndPoint[1] !== endPathStartPoint[1])
|
||
) {
|
||
// 使用正交连接或直接连接
|
||
let middlePoints: [number, number][] = [];
|
||
|
||
// 正交连接,添加额外点以确保路径是正交的
|
||
middlePoints = generateOrthogonalConnection(
|
||
startPathEndPoint[0],
|
||
startPathEndPoint[1],
|
||
endPathStartPoint[0],
|
||
endPathStartPoint[1],
|
||
);
|
||
|
||
// 在起点路径和终点路径之间插入中间点
|
||
allPoints.splice(pathPoints.length, 0, ...middlePoints);
|
||
}
|
||
} // 生成SVG路径
|
||
if (allPoints.length < 2) {
|
||
// 如果没有足够的点,直接从起点到终点画一条线
|
||
return `M ${startX} ${startY} L ${endX} ${endY}`;
|
||
}
|
||
|
||
// 使用所有点生成路径字符串
|
||
let pathStr = `M ${allPoints[0][0]} ${allPoints[0][1]}`;
|
||
for (let i = 1; i < allPoints.length; i++) {
|
||
pathStr += ` L ${allPoints[i][0]} ${allPoints[i][1]}`;
|
||
}
|
||
|
||
return pathStr;
|
||
}
|
||
|
||
// 执行单个路径命令
|
||
function executePathCommand(
|
||
x: number,
|
||
y: number,
|
||
command: string,
|
||
): { newX: number; newY: number } {
|
||
// 解析命令,例如 "down10", "right20", "downright5" 等
|
||
let newX = x;
|
||
let newY = y;
|
||
|
||
if (command.startsWith("right")) {
|
||
const distance = parseInt(command.substring(5), 10) || 10;
|
||
newX = x + distance;
|
||
newY = y;
|
||
} else if (command.startsWith("left")) {
|
||
const distance = parseInt(command.substring(4), 10) || 10;
|
||
newX = x - distance;
|
||
newY = y;
|
||
} else if (command.startsWith("down")) {
|
||
if (command.startsWith("downright")) {
|
||
const distance = parseInt(command.substring(9), 10) || 10;
|
||
newX = x + distance;
|
||
newY = y + distance;
|
||
} else if (command.startsWith("downleft")) {
|
||
const distance = parseInt(command.substring(8), 10) || 10;
|
||
newX = x - distance;
|
||
newY = y + distance;
|
||
} else {
|
||
const distance = parseInt(command.substring(4), 10) || 10;
|
||
newX = x;
|
||
newY = y + distance;
|
||
}
|
||
} else if (command.startsWith("up")) {
|
||
if (command.startsWith("upright")) {
|
||
const distance = parseInt(command.substring(7), 10) || 10;
|
||
newX = x + distance;
|
||
newY = y - distance;
|
||
} else if (command.startsWith("upleft")) {
|
||
const distance = parseInt(command.substring(6), 10) || 10;
|
||
newX = x - distance;
|
||
newY = y - distance;
|
||
} else {
|
||
const distance = parseInt(command.substring(2), 10) || 10;
|
||
newX = x;
|
||
newY = y - distance;
|
||
}
|
||
}
|
||
|
||
return { newX, newY };
|
||
}
|
||
|
||
// 生成两点之间的正交连接点
|
||
function generateOrthogonalConnection(
|
||
x1: number,
|
||
y1: number,
|
||
x2: number,
|
||
y2: number,
|
||
): [number, number][] {
|
||
const dx = x2 - x1;
|
||
const dy = y2 - y1;
|
||
|
||
if (dx === 0 || dy === 0) {
|
||
// 如果在同一水平或垂直线上,不需要额外点
|
||
return [];
|
||
}
|
||
|
||
// 选择先水平移动还是先垂直移动
|
||
const middlePoints: [number, number][] = [];
|
||
|
||
if (Math.abs(dx) > Math.abs(dy)) {
|
||
// 先水平后垂直
|
||
middlePoints.push([x1 + dx / 2, y1]);
|
||
middlePoints.push([x1 + dx / 2, y2]);
|
||
} else {
|
||
// 先垂直后水平
|
||
middlePoints.push([x1, y1 + dy / 2]);
|
||
middlePoints.push([x2, y1 + dy / 2]);
|
||
}
|
||
|
||
return middlePoints;
|
||
}
|
||
|
||
// 计算正交路径
|
||
function calculateOrthogonalPath(
|
||
startX: number,
|
||
startY: number,
|
||
endX: number,
|
||
endY: number,
|
||
) {
|
||
// 计算两点之间的水平和垂直距离
|
||
const dx = endX - startX;
|
||
const dy = endY - startY;
|
||
|
||
// 如果在同一水平或垂直线上,直接连线
|
||
if (dx === 0 || dy === 0) {
|
||
return `M ${startX} ${startY} L ${endX} ${endY}`;
|
||
}
|
||
|
||
const absDx = Math.abs(dx);
|
||
const absDy = Math.abs(dy);
|
||
if (absDx > absDy) {
|
||
// 先水平移动,然后垂直移动
|
||
const middleX = startX + dx * 0.5;
|
||
return `M ${startX} ${startY} L ${middleX} ${startY} L ${middleX} ${endY} L ${endX} ${endY}`;
|
||
} else {
|
||
// 先垂直移动,然后水平移动
|
||
const middleY = startY + dy * 0.5;
|
||
return `M ${startX} ${startY} L ${startX} ${middleY} L ${endX} ${middleY} L ${endX} ${endY}`;
|
||
}
|
||
}
|
||
|
||
// 添加反转命令函数 - 将方向命令反转
|
||
function reversePathCommand(command: string): string {
|
||
// 提取距离部分
|
||
const distanceMatch = command.match(/\d+$/);
|
||
const distance = distanceMatch ? distanceMatch[0] : "10"; // 默认距离是10
|
||
|
||
// 根据命令类型返回反向命令
|
||
// 水平方向反转
|
||
if (command.startsWith("right")) {
|
||
return `left${distance}`;
|
||
} else if (command.startsWith("left")) {
|
||
return `right${distance}`;
|
||
}
|
||
// 垂直和斜向反转
|
||
else if (command.startsWith("down")) {
|
||
if (command.startsWith("downright")) {
|
||
return `upleft${distance}`;
|
||
} else if (command.startsWith("downleft")) {
|
||
return `upright${distance}`;
|
||
} else {
|
||
return `up${distance}`;
|
||
}
|
||
} else if (command.startsWith("up")) {
|
||
if (command.startsWith("upright")) {
|
||
return `downleft${distance}`;
|
||
} else if (command.startsWith("upleft")) {
|
||
return `downright${distance}`;
|
||
} else {
|
||
return `down${distance}`;
|
||
}
|
||
}
|
||
|
||
// 默认情况下,无法反转就返回原命令
|
||
return command;
|
||
}
|
||
|
||
// 监听约束状态变化
|
||
let unsubscribe: (() => void) | null = null;
|
||
|
||
onMounted(() => {
|
||
// 监听约束状态变化
|
||
if (props.constraint) {
|
||
unsubscribe = onConstraintStateChange((constraint, level) => {
|
||
if (constraint === props.constraint) {
|
||
// 约束状态变化,触发重新渲染
|
||
}
|
||
});
|
||
}
|
||
});
|
||
|
||
onUnmounted(() => {
|
||
// 清理监听
|
||
if (unsubscribe) {
|
||
unsubscribe();
|
||
}
|
||
});
|
||
|
||
// 监听约束属性变化
|
||
watch(
|
||
() => props.constraint,
|
||
(newConstraint, oldConstraint) => {
|
||
if (unsubscribe) {
|
||
unsubscribe();
|
||
unsubscribe = null;
|
||
}
|
||
|
||
if (newConstraint) {
|
||
unsubscribe = onConstraintStateChange((constraint, level) => {
|
||
if (constraint === newConstraint) {
|
||
// 约束状态变化,触发重新渲染
|
||
}
|
||
});
|
||
}
|
||
},
|
||
);
|
||
|
||
// 暴露方法,用于获取这条连线的信息
|
||
defineExpose({
|
||
id: props.id,
|
||
getInfo: () => ({
|
||
id: props.id,
|
||
startComponentId: props.startComponentId,
|
||
startPinId: props.startPinId,
|
||
endComponentId: props.endComponentId,
|
||
endPinId: props.endPinId,
|
||
constraint: props.constraint,
|
||
}),
|
||
// 更新连线位置
|
||
updatePosition: (
|
||
newStartX: number,
|
||
newStartY: number,
|
||
newEndX: number,
|
||
newEndY: number,
|
||
) => {
|
||
// 由于 props 是只读的,我们只能通过事件通知父组件更新
|
||
emit("update:position", {
|
||
id: props.id,
|
||
startX: newStartX,
|
||
startY: newStartY,
|
||
endX: newEndX,
|
||
endY: newEndY,
|
||
});
|
||
},
|
||
// 获取连线的针脚情况
|
||
getPinPosition: () => null, // 为了与其他组件接口一致,但Wire不是针脚组件
|
||
// 获取连线的路由模式
|
||
getRoutingMode: () => props.routingMode,
|
||
// 设置连线状态(如高亮等)
|
||
setActive: (active: boolean) => {
|
||
emit("update:active", active);
|
||
},
|
||
});
|
||
</script>
|
||
|
||
<style scoped>
|
||
.wire-active {
|
||
stroke-dasharray: 5;
|
||
animation: dash 0.5s linear infinite;
|
||
}
|
||
|
||
@keyframes dash {
|
||
to {
|
||
stroke-dashoffset: 10;
|
||
}
|
||
}
|
||
|
||
.wire-label {
|
||
font-size: 10px;
|
||
fill: #666;
|
||
text-anchor: middle;
|
||
pointer-events: none;
|
||
user-select: none;
|
||
}
|
||
</style>
|