This repository has been archived on 2025-10-29. You can view files and clone it. You cannot open issues or pull requests or push a commit.
Files
FPGA_WebLab/src/components/equipments/Wire.vue

451 lines
12 KiB
Vue
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.
<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>