feat: frontend add boundary scan

This commit is contained in:
2025-05-19 13:30:06 +08:00
parent 2a3ef1ea7d
commit 5042bf8ce5
12 changed files with 611 additions and 538 deletions

View File

@@ -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>