feat: frontend add boundary scan
This commit is contained in:
@@ -1,28 +1,18 @@
|
||||
<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"
|
||||
/>
|
||||
<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>
|
||||
<text v-if="showLabel" :x="labelPosition.x" :y="labelPosition.y" class="wire-label">{{ props.constraint || ""
|
||||
}}</text>
|
||||
</g>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed, defineEmits, reactive, watch, onMounted, onUnmounted } from 'vue';
|
||||
import { getConstraintColor, getConstraintState, onConstraintStateChange } from '../../stores/constraints';
|
||||
import { useConstraintsStore } from "@/stores/constraints";
|
||||
import { computed, defineEmits, watch, onMounted, onUnmounted } from "vue";
|
||||
|
||||
const { getConstraintColor, onConstraintStateChange } = useConstraintsStore();
|
||||
|
||||
interface Props {
|
||||
id: string;
|
||||
@@ -33,7 +23,7 @@ interface Props {
|
||||
strokeColor?: string;
|
||||
strokeWidth?: number;
|
||||
isActive?: boolean;
|
||||
routingMode?: 'auto' | 'orthogonal' | 'direct' | 'path';
|
||||
routingMode?: "auto" | "orthogonal" | "direct" | "path";
|
||||
// 针脚引用属性
|
||||
startComponentId?: string;
|
||||
startPinId?: string;
|
||||
@@ -48,12 +38,12 @@ interface Props {
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
strokeColor: '#4a5568',
|
||||
strokeColor: "#4a5568",
|
||||
strokeWidth: 2,
|
||||
isActive: false,
|
||||
routingMode: 'orthogonal',
|
||||
routingMode: "orthogonal",
|
||||
showLabel: false,
|
||||
constraint: ''
|
||||
constraint: "",
|
||||
});
|
||||
|
||||
// 响应约束状态变化的颜色
|
||||
@@ -64,53 +54,62 @@ const constraintColor = computed(() => {
|
||||
|
||||
// 计算实际使用的颜色:isActive优先,其次是constraint电平颜色,最后是默认色
|
||||
const computedStroke = computed(() => {
|
||||
if (props.isActive) return '#ff9800';
|
||||
if (props.isActive) return "#ff9800";
|
||||
return constraintColor.value || props.strokeColor;
|
||||
});
|
||||
|
||||
const emit = defineEmits(['click', 'update:active', 'update:position']);
|
||||
const emit = defineEmits(["click", "update:active", "update:position"]);
|
||||
|
||||
function handleClick(event: MouseEvent) {
|
||||
emit('click', { id: props.id, event });
|
||||
emit("click", { id: props.id, event });
|
||||
}
|
||||
|
||||
// 计算标签位置 - 放在连线中间
|
||||
const labelPosition = computed(() => {
|
||||
return {
|
||||
x: (props.startX + props.endX) / 2,
|
||||
y: (props.startY + props.endY) / 2 - 5
|
||||
y: (props.startY + props.endY) / 2 - 5,
|
||||
};
|
||||
});
|
||||
|
||||
const pathData = computed(() => {
|
||||
// 如果有路径命令,使用路径布线模式
|
||||
if (props.routingMode === 'path' && props.pathCommands && props.pathCommands.length > 0) {
|
||||
if (
|
||||
props.routingMode === "path" &&
|
||||
props.pathCommands &&
|
||||
props.pathCommands.length > 0
|
||||
) {
|
||||
return calculatePathFromCommands(
|
||||
props.startX,
|
||||
props.startY,
|
||||
props.endX,
|
||||
props.endY,
|
||||
props.pathCommands
|
||||
props.startX,
|
||||
props.startY,
|
||||
props.endX,
|
||||
props.endY,
|
||||
props.pathCommands,
|
||||
);
|
||||
}
|
||||
// 否则使用正交路径
|
||||
return calculateOrthogonalPath(props.startX, props.startY, props.endX, props.endY);
|
||||
return calculateOrthogonalPath(
|
||||
props.startX,
|
||||
props.startY,
|
||||
props.endX,
|
||||
props.endY,
|
||||
);
|
||||
});
|
||||
|
||||
function calculatePathFromCommands(
|
||||
startX: number,
|
||||
startY: number,
|
||||
endX: number,
|
||||
endY: number,
|
||||
commands: string[]
|
||||
startX: number,
|
||||
startY: number,
|
||||
endX: number,
|
||||
endY: number,
|
||||
commands: string[],
|
||||
) {
|
||||
// 找到分隔符索引,通常是 "*"
|
||||
const splitterIndex = commands.indexOf('*');
|
||||
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);
|
||||
@@ -118,10 +117,10 @@ function calculatePathFromCommands(
|
||||
// 从起点开始生成路径
|
||||
let currentX = startX;
|
||||
let currentY = startY;
|
||||
|
||||
|
||||
// 处理起点路径命令
|
||||
const pathPoints: [number, number][] = [[currentX, currentY]];
|
||||
|
||||
|
||||
// 解析并执行起点命令
|
||||
for (const cmd of startCommands) {
|
||||
const { newX, newY } = executePathCommand(currentX, currentY, cmd);
|
||||
@@ -129,13 +128,13 @@ function calculatePathFromCommands(
|
||||
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]);
|
||||
@@ -143,76 +142,91 @@ function calculatePathFromCommands(
|
||||
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]) {
|
||||
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;
|
||||
|
||||
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])) {
|
||||
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]
|
||||
startPathEndPoint[0],
|
||||
startPathEndPoint[1],
|
||||
endPathStartPoint[0],
|
||||
endPathStartPoint[1],
|
||||
);
|
||||
|
||||
|
||||
// 在起点路径和终点路径之间插入中间点
|
||||
allPoints.splice(pathPoints.length, 0, ...middlePoints);
|
||||
}
|
||||
} // 生成SVG路径
|
||||
} // 生成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 } {
|
||||
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')) {
|
||||
|
||||
if (command.startsWith("right")) {
|
||||
const distance = parseInt(command.substring(5), 10) || 10;
|
||||
newX = x + distance;
|
||||
newY = y;
|
||||
} else if (command.startsWith('left')) {
|
||||
} 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')) {
|
||||
} 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')) {
|
||||
} else if (command.startsWith("downleft")) {
|
||||
const distance = parseInt(command.substring(8), 10) || 10;
|
||||
newX = x - distance;
|
||||
newY = y + distance;
|
||||
@@ -221,12 +235,12 @@ function executePathCommand(x: number, y: number, command: string): { newX: numb
|
||||
newX = x;
|
||||
newY = y + distance;
|
||||
}
|
||||
} else if (command.startsWith('up')) {
|
||||
if (command.startsWith('upright')) {
|
||||
} 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')) {
|
||||
} else if (command.startsWith("upleft")) {
|
||||
const distance = parseInt(command.substring(6), 10) || 10;
|
||||
newX = x - distance;
|
||||
newY = y - distance;
|
||||
@@ -236,23 +250,28 @@ function executePathCommand(x: number, y: number, command: string): { newX: numb
|
||||
newY = y - distance;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
return { newX, newY };
|
||||
}
|
||||
|
||||
// 生成两点之间的正交连接点
|
||||
function generateOrthogonalConnection(x1: number, y1: number, x2: number, y2: number): [number, number][] {
|
||||
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]);
|
||||
@@ -262,21 +281,26 @@ function generateOrthogonalConnection(x1: number, y1: number, x2: number, y2: nu
|
||||
middlePoints.push([x1, y1 + dy / 2]);
|
||||
middlePoints.push([x2, y1 + dy / 2]);
|
||||
}
|
||||
|
||||
|
||||
return middlePoints;
|
||||
}
|
||||
|
||||
// 计算正交路径
|
||||
function calculateOrthogonalPath(startX: number, startY: number, endX: number, endY: number) {
|
||||
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) {
|
||||
@@ -295,39 +319,33 @@ function reversePathCommand(command: string): string {
|
||||
// 提取距离部分
|
||||
const distanceMatch = command.match(/\d+$/);
|
||||
const distance = distanceMatch ? distanceMatch[0] : "10"; // 默认距离是10
|
||||
|
||||
|
||||
// 根据命令类型返回反向命令
|
||||
// 水平方向反转
|
||||
if (command.startsWith('right')) {
|
||||
if (command.startsWith("right")) {
|
||||
return `left${distance}`;
|
||||
}
|
||||
else if (command.startsWith('left')) {
|
||||
} else if (command.startsWith("left")) {
|
||||
return `right${distance}`;
|
||||
}
|
||||
}
|
||||
// 垂直和斜向反转
|
||||
else if (command.startsWith('down')) {
|
||||
if (command.startsWith('downright')) {
|
||||
else if (command.startsWith("down")) {
|
||||
if (command.startsWith("downright")) {
|
||||
return `upleft${distance}`;
|
||||
}
|
||||
else if (command.startsWith('downleft')) {
|
||||
} else if (command.startsWith("downleft")) {
|
||||
return `upright${distance}`;
|
||||
}
|
||||
else {
|
||||
} else {
|
||||
return `up${distance}`;
|
||||
}
|
||||
}
|
||||
else if (command.startsWith('up')) {
|
||||
if (command.startsWith('upright')) {
|
||||
} else if (command.startsWith("up")) {
|
||||
if (command.startsWith("upright")) {
|
||||
return `downleft${distance}`;
|
||||
}
|
||||
else if (command.startsWith('upleft')) {
|
||||
} else if (command.startsWith("upleft")) {
|
||||
return `downright${distance}`;
|
||||
}
|
||||
else {
|
||||
} else {
|
||||
return `down${distance}`;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// 默认情况下,无法反转就返回原命令
|
||||
return command;
|
||||
}
|
||||
@@ -354,39 +372,49 @@ onUnmounted(() => {
|
||||
});
|
||||
|
||||
// 监听约束属性变化
|
||||
watch(() => props.constraint, (newConstraint, oldConstraint) => {
|
||||
if (unsubscribe) {
|
||||
unsubscribe();
|
||||
unsubscribe = null;
|
||||
}
|
||||
|
||||
if (newConstraint) {
|
||||
unsubscribe = onConstraintStateChange((constraint, level) => {
|
||||
if (constraint === newConstraint) {
|
||||
// 约束状态变化,触发重新渲染
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
watch(
|
||||
() => props.constraint,
|
||||
(newConstraint, oldConstraint) => {
|
||||
if (unsubscribe) {
|
||||
unsubscribe();
|
||||
unsubscribe = null;
|
||||
}
|
||||
|
||||
if (newConstraint) {
|
||||
unsubscribe = onConstraintStateChange((constraint, level) => {
|
||||
if (constraint === newConstraint) {
|
||||
// 约束状态变化,触发重新渲染
|
||||
}
|
||||
});
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
// 暴露方法,用于获取这条连线的信息
|
||||
defineExpose({ id: props.id, getInfo: () => ({
|
||||
defineExpose({
|
||||
id: props.id,
|
||||
getInfo: () => ({
|
||||
id: props.id,
|
||||
startComponentId: props.startComponentId,
|
||||
startPinId: props.startPinId,
|
||||
endComponentId: props.endComponentId,
|
||||
endPinId: props.endPinId,
|
||||
constraint: props.constraint
|
||||
constraint: props.constraint,
|
||||
}),
|
||||
// 更新连线位置
|
||||
updatePosition: (newStartX: number, newStartY: number, newEndX: number, newEndY: number) => {
|
||||
updatePosition: (
|
||||
newStartX: number,
|
||||
newStartY: number,
|
||||
newEndX: number,
|
||||
newEndY: number,
|
||||
) => {
|
||||
// 由于 props 是只读的,我们只能通过事件通知父组件更新
|
||||
emit('update:position', {
|
||||
emit("update:position", {
|
||||
id: props.id,
|
||||
startX: newStartX,
|
||||
startY: newStartY,
|
||||
endX: newEndX,
|
||||
endY: newEndY
|
||||
endY: newEndY,
|
||||
});
|
||||
},
|
||||
// 获取连线的针脚情况
|
||||
@@ -395,8 +423,8 @@ defineExpose({ id: props.id, getInfo: () => ({
|
||||
getRoutingMode: () => props.routingMode,
|
||||
// 设置连线状态(如高亮等)
|
||||
setActive: (active: boolean) => {
|
||||
emit('update:active', active);
|
||||
}
|
||||
emit("update:active", active);
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user