Compare commits

..

No commits in common. "1c75aa621a969e8474f22487dcfa8c1034d1e386" and "b3a5342d6bda1627d475c4ed84513bbfa98277a3" have entirely different histories.

13 changed files with 846 additions and 897 deletions

View File

@ -8,9 +8,6 @@
/* eslint-disable */ /* eslint-disable */
// ReSharper disable InconsistentNaming // ReSharper disable InconsistentNaming
import { batchSetConstraintStates, notifyConstraintChange } from './stores/constraints';
import type { ConstraintLevel } from './stores/constraints';
export class Client { export class Client {
private http: { fetch(url: RequestInfo, init?: RequestInit): Promise<Response> }; private http: { fetch(url: RequestInfo, init?: RequestInit): Promise<Response> };
private baseUrl: string; private baseUrl: string;
@ -870,56 +867,3 @@ function throwException(message: string, status: number, response: string, heade
else else
throw new ApiException(message, status, response, headers, null); throw new ApiException(message, status, response, headers, null);
} }
// 约束通信相关方法
export function receiveConstraintUpdates(constraints: Record<string, ConstraintLevel>) {
// 批量更新约束状态
batchSetConstraintStates(constraints);
}
export function sendConstraintUpdate(constraint: string, level: ConstraintLevel) {
// 向后端发送约束状态变化
console.log(`发送约束 ${constraint} 状态变化为 ${level}`);
// TODO: 实际的WebSocket或HTTP请求发送约束变化
// 例如:
// socket.emit('constraintUpdate', { constraint, level });
// 或
// fetch('/api/constraints', {
// method: 'POST',
// body: JSON.stringify({ constraint, level }),
// headers: { 'Content-Type': 'application/json' }
// });
}
// 初始化约束通信
export function initConstraintCommunication() {
// 监听服务器发来的约束状态变化
// 示例:
// socket.on('constraintUpdates', (data) => {
// receiveConstraintUpdates(data);
// });
// 模拟接收一些初始约束状态
setTimeout(() => {
receiveConstraintUpdates({
'A1': 'high',
'A2': 'low',
'A3': 'undefined'
});
}, 1000);
}
// 覆盖全局notifyConstraintChange加入发送逻辑
const originalNotifyConstraintChange = notifyConstraintChange;
const wrappedNotifyConstraintChange = (constraint: string, level: ConstraintLevel) => {
// 调用原始方法更新本地状态
originalNotifyConstraintChange(constraint, level);
// 向后端发送更新
sendConstraintUpdate(constraint, level);
};
// 替换全局方法
(window as any).__notifyConstraintChange = notifyConstraintChange;
(window as any).notifyConstraintChange = wrappedNotifyConstraintChange;

View File

@ -0,0 +1,80 @@
.diagram-canvas {
position: relative;
width: 4000px;
height: 4000px;
transform-origin: 0 0;
}
/* 将网格线应用到容器而不是画布上,确保覆盖整个可视区域 */
.flex-1.min-w-\[60\%\].bg-base-200.relative.overflow-auto {
background-image:
linear-gradient(to right, rgba(100, 100, 100, 0.1) 1px, transparent 1px),
linear-gradient(to bottom, rgba(100, 100, 100, 0.1) 1px, transparent 1px),
linear-gradient(to right, rgba(80, 80, 80, 0.2) 100px, transparent 100px),
linear-gradient(to bottom, rgba(80, 80, 80, 0.2) 100px, transparent 100px);
background-size: 20px 20px, 20px 20px, 100px 100px, 100px 100px;
background-position: 0 0;
}
/* 为黑暗模式设置不同的网格线颜色 */
:root[data-theme="dark"] .flex-1.min-w-\[60\%\].bg-base-200.relative.overflow-auto {
background-image:
linear-gradient(to right, rgba(200, 200, 200, 0.1) 1px, transparent 1px),
linear-gradient(to bottom, rgba(200, 200, 200, 0.1) 1px, transparent 1px),
linear-gradient(to right, rgba(180, 180, 180, 0.15) 100px, transparent 100px),
linear-gradient(to bottom, rgba(180, 180, 180, 0.15) 100px, transparent 100px);
background-size: 20px 20px, 20px 20px, 100px 100px, 100px 100px;
background-position: 0 0;
}
/* 禁用滚动条 */
.flex-1.min-w-\[60\%\].bg-base-200.relative.overflow-auto::-webkit-scrollbar {
display: none; /* Chrome, Safari, Opera */
}
.flex-1.min-w-\[60\%\].bg-base-200.relative.overflow-auto {
-ms-overflow-style: none; /* IE and Edge */
scrollbar-width: none; /* Firefox */
}
/* 元器件容器样式 */
.component-wrapper {
position: relative;
padding: 5px;
box-sizing: border-box;
display: inline-block; /* 确保元素宽度基于内容 */
max-width: fit-content; /* 强制宽度适应内容 */
max-height: fit-content; /* 强制高度适应内容 */
overflow: visible; /* 允许内容溢出(用于显示边框) */
}
/* 悬停状态 */
.component-hover::before {
content: '';
position: absolute;
top: -4px;
left: -4px;
right: -4px;
bottom: -4px;
border: 3px dashed #3498db;
pointer-events: none;
z-index: 1;
border-radius: 4px;
box-sizing: content-box;
}
/* 选中状态 */
.component-selected::before {
content: '';
position: absolute;
top: -4px;
left: -4px;
right: -4px;
bottom: -4px;
border: 4px dashed #e74c3c;
border-color: #e74c3c #f39c12 #3498db #2ecc71;
pointer-events: none;
z-index: 1;
border-radius: 4px;
box-sizing: content-box;
}

View File

@ -9,7 +9,7 @@
:style="{ transform: `translate(${position.x}px, ${position.y}px) scale(${scale})` }"> <!-- 渲染连线 --> :style="{ transform: `translate(${position.x}px, ${position.y}px) scale(${scale})` }"> <!-- 渲染连线 -->
<svg class="wires-layer" width="4000" height="4000"> <svg class="wires-layer" width="4000" height="4000">
<!-- 已完成的连线 --> <!-- 已完成的连线 -->
<WireComponent <Wire
v-for="wire in wires" v-for="wire in wires"
:key="wire.id" :key="wire.id"
:id="wire.id" :id="wire.id"
@ -19,14 +19,16 @@
:end-y="wire.endY" :end-y="wire.endY"
:stroke-color="wire.color || '#4a5568'" :stroke-color="wire.color || '#4a5568'"
:stroke-width="2" :stroke-width="2"
:is-active="false" :is-active="wire.isActive"
:start-component-id="wire.startComponentId" :start-component-id="wire.startComponentId"
:start-pin-label="wire.startPinLabel"
:end-component-id="wire.endComponentId" :end-component-id="wire.endComponentId"
:constraint="wire.constraint" :end-pin-label="wire.endPinLabel"
@click="handleWireClick(wire)"
/> />
<!-- 正在创建的连线 --> <!-- 正在创建的连线 -->
<WireComponent <Wire
v-if="isCreatingWire" v-if="isCreatingWire"
id="temp-wire" id="temp-wire"
:start-x="creatingWireStart.x" :start-x="creatingWireStart.x"
@ -56,7 +58,7 @@
@mouseleave="hoveredComponent = null"><!-- 动态渲染组件 --> <component @mouseleave="hoveredComponent = null"><!-- 动态渲染组件 --> <component
:is="getComponentDefinition(component.type)" :is="getComponentDefinition(component.type)"
v-if="props.componentModules[component.type]" v-if="props.componentModules[component.type]"
v-bind="prepareComponentProps(component.props || {}, component.id)" v-bind="prepareComponentProps(component.props || {})"
@update:bindKey="(value: string) => updateComponentProp(component.id, 'bindKey', value)" @update:bindKey="(value: string) => updateComponentProp(component.id, 'bindKey', value)"
@pin-click="(pinInfo: any) => handlePinClick(component.id, pinInfo, pinInfo.originalEvent)" @pin-click="(pinInfo: any) => handlePinClick(component.id, pinInfo, pinInfo.originalEvent)"
:ref="(el: any) => { if (el) componentRefs[component.id] = el; }" :ref="(el: any) => { if (el) componentRefs[component.id] = el; }"
@ -78,16 +80,7 @@
<script setup lang="ts"> <script setup lang="ts">
import { ref, reactive, onMounted, onUnmounted } from 'vue'; import { ref, reactive, onMounted, onUnmounted } from 'vue';
import WireComponent from '@/components/equipments/Wire.vue'; import Wire from '@/components/Wire.vue';
import { wires, addWire, deleteWire, updateWiresConstraintByPin, findWiresByPin, isCreatingWire, creatingWireStart, creatingWireStartInfo, mousePosition, resetWireCreation, setWireCreationStart, setMousePosition } from './wireManager';
import type { WireItem } from './wireManager';
//
function handleContextMenu(e: MouseEvent) {
//
//
e.preventDefault();
}
// //
interface ComponentItem { interface ComponentItem {
@ -177,10 +170,19 @@ const getComponentDefinition = (type: string) => {
}; };
// //
function prepareComponentProps(props: Record<string, any>, componentId?: string): Record<string, any> { function prepareComponentProps(props: Record<string, any>): Record<string, any> {
const result: Record<string, any> = { ...props }; const result: Record<string, any> = {};
if (componentId) { for (const key in props) {
result.componentId = componentId; let value = props[key];
// null/undefined string
if (
(key === 'style' || key === 'direction' || key === 'type') &&
value != null &&
typeof value !== 'string'
) {
value = String(value);
}
result[key] = value;
} }
return result; return result;
} }
@ -349,19 +351,6 @@ function stopComponentDrag() {
// //
function updateComponentProp(componentId: string, propName: string, value: any) { function updateComponentProp(componentId: string, propName: string, value: any) {
// updatePinConstraint
if (propName === 'constraint') {
// Pin labellabelPin
const componentRef = componentRefs.value[componentId];
if (componentRef && componentRef.getInfo) {
const pinInfo = componentRef.getInfo();
if (pinInfo) {
updatePinConstraint(componentId, value);
return; // emit
}
}
}
// emit
emit('update-component-prop', { id: componentId, propName, value }); emit('update-component-prop', { id: componentId, propName, value });
} }
@ -374,17 +363,6 @@ function getComponentRef(componentId: string) {
return componentRefs.value[component.id] || null; return componentRefs.value[component.id] || null;
} }
// Wire
function getWireRef(wireId: string) {
// SVGWire
const wire = document.querySelector(`[data-wire-id="${wireId}"]`);
if (wire && '__vueParentInstance' in wire) {
// @ts-ignore - 访Vue
return wire.__vueParentInstance?.component?.exposed;
}
return null;
}
// //
defineExpose({ defineExpose({
getComponentRef, getComponentRef,
@ -393,18 +371,170 @@ defineExpose({
}); });
// --- 线 --- // --- 线 ---
// 线 interface WireItem {
function handleWireCreated(wireData: any) { id: string;
addWire(wireData); startX: number;
emit('wire-created', wireData); startY: number;
endX: number;
endY: number;
startComponentId: string;
startPinLabel: string;
endComponentId?: string;
endPinLabel?: string;
color?: string;
isActive?: boolean;
constraint?: string;
} }
// 线
function handleWireDeleted(wireId: string) { const wires = ref<WireItem[]>([]);
deleteWire(wireId); const isCreatingWire = ref(false);
emit('wire-deleted', wireId); const creatingWireStart = reactive({ x: 0, y: 0 });
const creatingWireStartInfo = reactive({
componentId: '',
pinLabel: '',
constraint: ''
});
const mousePosition = reactive({ x: 0, y: 0 });
//
function updateMousePosition(e: MouseEvent) {
if (!canvasContainer.value) return;
const containerRect = canvasContainer.value.getBoundingClientRect();
mousePosition.x = (e.clientX - containerRect.left - position.x) / scale.value;
mousePosition.y = (e.clientY - containerRect.top - position.y) / scale.value;
} }
// PinWire
function updatePinConstraint(componentId: string, constraint: string) { // Pin
function handlePinClick(componentId: string, pinInfo: any, event: MouseEvent) {
if (!canvasContainer.value) return;
//
const containerRect = canvasContainer.value.getBoundingClientRect();
// (线)
updateMousePosition(event);
// pinInfo
if (!pinInfo || !pinInfo.label) {
console.error('无效的针脚信息:', pinInfo);
return;
}
//
if (!pinInfo.position) {
console.error('针脚信息中缺少位置数据:', pinInfo);
return;
}
const pinPagePosition = pinInfo.position;
console.log(`针脚 ${pinInfo.label} 的页面坐标:`, pinPagePosition);
//
const pinCanvasX = (pinPagePosition.x - containerRect.left - position.x) / scale.value;
const pinCanvasY = (pinPagePosition.y - containerRect.top - position.y) / scale.value;
console.log(`针脚 ${pinInfo.label} 的画布坐标:`, { x: pinCanvasX, y: pinCanvasY });
if (!isCreatingWire.value) {
// 线
isCreatingWire.value = true;
// 使线
creatingWireStart.x = pinCanvasX;
creatingWireStart.y = pinCanvasY;
creatingWireStartInfo.componentId = componentId;
creatingWireStartInfo.pinLabel = pinInfo.label;
creatingWireStartInfo.constraint = pinInfo.constraint;
console.log(`开始创建连线,起点针脚: ${componentId}/${pinInfo.label}, 位置: (${pinCanvasX}, ${pinCanvasY})`);
//
document.addEventListener('mousemove', onCreatingWireMouseMove);
} else {
// 线
if (componentId === creatingWireStartInfo.componentId && pinInfo.label === creatingWireStartInfo.pinLabel) {
// Pin线
cancelWireCreation();
return;
}
//
const startConstraint = creatingWireStartInfo.constraint;
const endConstraint = pinInfo.constraint;
if (startConstraint && endConstraint && startConstraint !== endConstraint) {
// Pin
promptForConstraintSelection(
componentId,
pinInfo,
startConstraint,
endConstraint
);
} else {
//
let finalConstraint = '';
if (startConstraint) {
finalConstraint = startConstraint;
} else if (endConstraint) {
finalConstraint = endConstraint;
} else {
// Pin
finalConstraint = generateRandomConstraint();
}
// 线
completeWireCreation(
componentId,
pinInfo.label,
finalConstraint,
pinInfo
);
// Pin
updatePinConstraint(creatingWireStartInfo.componentId, creatingWireStartInfo.pinLabel, finalConstraint);
updatePinConstraint(componentId, pinInfo.label, finalConstraint);
}
}
}
//
function generateRandomConstraint() {
const randomId = Math.floor(Math.random() * 1000000);
return `$auto_constraint_${randomId}`;
}
//
function promptForConstraintSelection(
endComponentId: string,
endPinInfo: any,
startConstraint: string,
endConstraint: string
) {
// 使ModalUI
// 使confirm
const useStartConstraint = confirm(
`连接两个不同约束的Pin:\n` +
`- 起点约束: ${startConstraint}\n` +
`- 终点约束: ${endConstraint}\n\n` +
`点击"确定"使用起点约束,点击"取消"使用终点约束。`
);
const finalConstraint = useStartConstraint ? startConstraint : endConstraint;
// 线
completeWireCreation(
endComponentId,
endPinInfo.label,
finalConstraint,
endPinInfo
);
// Pin
updatePinConstraint(creatingWireStartInfo.componentId, creatingWireStartInfo.pinLabel, finalConstraint);
updatePinConstraint(endComponentId, endPinInfo.label, finalConstraint);
}
// Pin
function updatePinConstraint(componentId: string, pinLabel: string, constraint: string) {
// ID // ID
const component = props.components.find(c => c.id === componentId); const component = props.components.find(c => c.id === componentId);
if (!component) return; if (!component) return;
@ -416,7 +546,7 @@ function updatePinConstraint(componentId: string, constraint: string) {
// //
if (component.props && componentRef.getInfo) { if (component.props && componentRef.getInfo) {
const pinInfo = componentRef.getInfo(); const pinInfo = componentRef.getInfo();
if (pinInfo) { if (pinInfo && pinInfo.label === pinLabel) {
emit('update-component-prop', { emit('update-component-prop', {
id: componentId, id: componentId,
propName: 'constraint', propName: 'constraint',
@ -424,114 +554,10 @@ function updatePinConstraint(componentId: string, constraint: string) {
}); });
} }
} }
// Wireconstraint
updateWiresConstraintByPin(componentId, constraint);
}
//
function updateMousePosition(e: MouseEvent) {
if (!canvasContainer.value) return;
const containerRect = canvasContainer.value.getBoundingClientRect();
setMousePosition(
(e.clientX - containerRect.left - position.x) / scale.value,
(e.clientY - containerRect.top - position.y) / scale.value
);
}
// Pin
function handlePinClick(componentId: string, pinInfo: any, event: MouseEvent) {
if (!canvasContainer.value) return;
const containerRect = canvasContainer.value.getBoundingClientRect();
updateMousePosition(event);
if (!pinInfo || !pinInfo.label) {
console.error('无效的针脚信息:', pinInfo);
return;
}
if (!pinInfo.position) {
console.error('针脚信息中缺少位置数据:', pinInfo);
return;
}
const pinPagePosition = pinInfo.position;
const pinCanvasX = (pinPagePosition.x - containerRect.left - position.x) / scale.value;
const pinCanvasY = (pinPagePosition.y - containerRect.top - position.y) / scale.value;
if (!isCreatingWire.value) {
setWireCreationStart(pinCanvasX, pinCanvasY, componentId, pinInfo.label, pinInfo.constraint);
document.addEventListener('mousemove', onCreatingWireMouseMove);
} else {
if (componentId === creatingWireStartInfo.componentId && pinInfo.label === creatingWireStartInfo.pinLabel) {
cancelWireCreation();
return;
}
//
const startConstraint = creatingWireStartInfo.constraint || '';
const endConstraint = pinInfo.constraint || '';
let finalConstraint = '';
//
if (startConstraint && endConstraint && startConstraint === endConstraint) {
finalConstraint = startConstraint;
} else {
// 使
let replacedConstraint: string | null = null;
let newConstraint: string | null = null;
if (startConstraint && endConstraint) {
const isStartSystemConstraint = startConstraint.startsWith('$');
const isEndSystemConstraint = endConstraint.startsWith('$');
if (!isStartSystemConstraint && isEndSystemConstraint) {
finalConstraint = startConstraint;
replacedConstraint = endConstraint;
newConstraint = startConstraint;
} else if (isStartSystemConstraint && !isEndSystemConstraint) {
finalConstraint = endConstraint;
replacedConstraint = startConstraint;
newConstraint = endConstraint;
} else if (isStartSystemConstraint && isEndSystemConstraint) {
finalConstraint = Math.random() < 0.5 ? startConstraint : endConstraint;
replacedConstraint = (finalConstraint === startConstraint) ? endConstraint : startConstraint;
newConstraint = finalConstraint;
} else {
const userChoice = confirm(`针脚约束冲突:${startConstraint}${endConstraint}。点击"确定"保留${startConstraint},点击"取消"保留${endConstraint}`);
finalConstraint = userChoice ? startConstraint : endConstraint;
replacedConstraint = userChoice ? endConstraint : startConstraint;
newConstraint = finalConstraint;
}
} else if (startConstraint) {
finalConstraint = startConstraint;
} else if (endConstraint) {
finalConstraint = endConstraint;
} else {
finalConstraint = generateRandomConstraint();
}
// Pin
if (replacedConstraint && newConstraint && replacedConstraint !== newConstraint) {
props.components.forEach(comp => {
if (comp.props && comp.props.constraint === replacedConstraint) {
emit('update-component-prop', { id: comp.id, propName: 'constraint', value: newConstraint });
}
});
}
}
// 线
completeWireCreation(
componentId,
finalConstraint,
pinInfo
);
// Pin
updatePinConstraint(creatingWireStartInfo.componentId, finalConstraint);
updatePinConstraint(componentId, finalConstraint);
}
}
//
function generateRandomConstraint() {
const randomId = Math.floor(Math.random() * 1000000);
return `$auto_constraint_${randomId}`;
} }
// 线 // 线
function completeWireCreation(endComponentId: string, constraint: string, endPinInfo?: any) { function completeWireCreation(endComponentId: string, endPinLabel: string, constraint: string, endPinInfo?: any) {
if (!canvasContainer.value) return; if (!canvasContainer.value) return;
const containerRect = canvasContainer.value.getBoundingClientRect(); const containerRect = canvasContainer.value.getBoundingClientRect();
@ -544,20 +570,31 @@ function completeWireCreation(endComponentId: string, constraint: string, endPin
// 使 getPinPosition // 使 getPinPosition
const endComponentRef = componentRefs.value[endComponentId]; const endComponentRef = componentRefs.value[endComponentId];
if (endComponentRef && endComponentRef.getPinPosition) { if (endComponentRef && endComponentRef.getPinPosition) {
const pinPosition = endComponentRef.getPinPosition(endComponentId); const pinPosition = endComponentRef.getPinPosition(endPinLabel);
if (pinPosition) { if (pinPosition) {
endX = (pinPosition.x - containerRect.left - position.x) / scale.value; endX = (pinPosition.x - containerRect.left - position.x) / scale.value;
endY = (pinPosition.y - containerRect.top - position.y) / scale.value; endY = (pinPosition.y - containerRect.top - position.y) / scale.value;
console.log(`通过 getPinPosition 获取终点针脚 ${endComponentId} 的画布坐标: (${endX}, ${endY})`); console.log(`通过 getPinPosition 获取终点针脚 ${endPinLabel} 的画布坐标: (${endX}, ${endY})`);
} else { console.warn(`getPinPosition 返回 null将使用备选方法`); } else {
console.warn(`getPinPosition 返回 null将使用备选方法`);
} }
} else if (endPinInfo && endPinInfo.position) { } else if (endPinInfo && endPinInfo.position) {
// getPinPosition 使 // getPinPosition 使
const pinPagePosition = endPinInfo.position; const pinPagePosition = endPinInfo.position;
endX = (pinPagePosition.x - containerRect.left - position.x) / scale.value; endX = (pinPagePosition.x - containerRect.left - position.x) / scale.value;
endY = (pinPagePosition.y - containerRect.top - position.y) / scale.value; endY = (pinPagePosition.y - containerRect.top - position.y) / scale.value;
console.log(`通过 pinInfo.position 获取终点针脚位置: (${endX}, ${endY})`);
} else {
console.warn(`无法获取针脚 ${endPinLabel} 的精确位置,使用鼠标位置代替`);
}
//
const distanceSquared = Math.pow(endX - creatingWireStart.x, 2) + Math.pow(endY - creatingWireStart.y, 2);
if (distanceSquared < 1) { // 1
console.warn(`起点和终点太接近 (${distanceSquared}像素²),调整终点位置`);
//
endX += 10 + Math.random() * 5;
endY += 10 + Math.random() * 5;
} }
// 线 // 线
const newWire: WireItem = { const newWire: WireItem = {
id: `wire-${Date.now()}`, id: `wire-${Date.now()}`,
@ -566,14 +603,22 @@ function completeWireCreation(endComponentId: string, constraint: string, endPin
endX: endX, endX: endX,
endY: endY, endY: endY,
startComponentId: creatingWireStartInfo.componentId, startComponentId: creatingWireStartInfo.componentId,
startPinLabel: creatingWireStartInfo.pinLabel,
endComponentId: endComponentId, endComponentId: endComponentId,
endPinLabel: endPinLabel,
color: '#4a5568', color: '#4a5568',
constraint: constraint, constraint: constraint
routingMode: 'orthogonal',
strokeWidth: 2,
showLabel: false
}; };
console.log(`新连线创建完成:`, newWire);
//
if (Math.abs(newWire.startX - newWire.endX) < 1 && Math.abs(newWire.startY - newWire.endY) < 1) {
console.warn(`连线的起点和终点重合,调整终点位置`);
newWire.endX += 20;
newWire.endY += 20;
}
wires.value.push(newWire); wires.value.push(newWire);
// 线 // 线
@ -586,7 +631,7 @@ function completeWireCreation(endComponentId: string, constraint: string, endPin
// 线 // 线
function cancelWireCreation() { function cancelWireCreation() {
resetWireCreation(); isCreatingWire.value = false;
document.removeEventListener('mousemove', onCreatingWireMouseMove); document.removeEventListener('mousemove', onCreatingWireMouseMove);
} }
@ -595,53 +640,97 @@ function onCreatingWireMouseMove(e: MouseEvent) {
updateMousePosition(e); updateMousePosition(e);
} }
// 线
function handleWireClick(wire: WireItem) {
// 线
const deleteWire = confirm('是否删除此连线?');
if (deleteWire) {
// 线
const index = wires.value.findIndex(w => w.id === wire.id);
if (index !== -1) {
const deletedWire = wires.value.splice(index, 1)[0];
// 线
emit('wire-deleted', deletedWire.id);
}
}
}
// 线
function updateAllWires() {
if (!canvasContainer.value) return;
// 线
wires.value.forEach(wire => {
updateWireWithPinPositions(wire);
});
}
// 线 // 线
function updateWireWithPinPositions(wire: WireItem) { function updateWireWithPinPositions(wire: WireItem) {
if (!canvasContainer.value) return; if (!canvasContainer.value) return;
const containerRect = canvasContainer.value.getBoundingClientRect(); const containerRect = canvasContainer.value.getBoundingClientRect();
console.log(`更新连线 ${wire.id},当前位置: 起点(${wire.startX}, ${wire.startY}), 终点(${wire.endX}, ${wire.endY})`);
//
const originalStartX = wire.startX;
const originalStartY = wire.startY;
const originalEndX = wire.endX;
const originalEndY = wire.endY;
// //
if (wire.startComponentId) { if (wire.startComponentId && wire.startPinLabel) {
const startComponentRef = componentRefs.value[wire.startComponentId]; const startComponentRef = componentRefs.value[wire.startComponentId];
if (startComponentRef && startComponentRef.getPinPosition) { if (startComponentRef && startComponentRef.getPinPosition) {
const pinPosition = startComponentRef.getPinPosition(wire.startComponentId); const pinPosition = startComponentRef.getPinPosition(wire.startPinLabel);
if (pinPosition) { if (pinPosition) {
wire.startX = (pinPosition.x - containerRect.left - position.x) / scale.value; const newStartX = (pinPosition.x - containerRect.left - position.x) / scale.value;
wire.startY = (pinPosition.y - containerRect.top - position.y) / scale.value; const newStartY = (pinPosition.y - containerRect.top - position.y) / scale.value;
console.log(`更新连线起点 ${wire.startComponentId}/${wire.startPinLabel}: (${wire.startX}, ${wire.startY}) => (${newStartX}, ${newStartY})`);
wire.startX = newStartX;
wire.startY = newStartY;
} else { } else {
console.warn(`无法获取组件${wire.startComponentId}的针脚${wire.startComponentId}位置`); console.warn(`无法获取针脚 ${wire.startComponentId}/${wire.startPinLabel} 的位置`);
//
setTimeout(() => {
const retryPosition = startComponentRef.getPinPosition(wire.startComponentId);
if (retryPosition) {
wire.startX = (retryPosition.x - containerRect.left - position.x) / scale.value;
wire.startY = (retryPosition.y - containerRect.top - position.y) / scale.value;
}
}, 100);
} }
} else {
console.warn(`组件 ${wire.startComponentId} 没有实现 getPinPosition 方法`);
} }
} }
// //
if (wire.endComponentId) { if (wire.endComponentId && wire.endPinLabel) {
const endComponentRef = componentRefs.value[wire.endComponentId]; const endComponentRef = componentRefs.value[wire.endComponentId];
if (endComponentRef && endComponentRef.getPinPosition) { if (endComponentRef && endComponentRef.getPinPosition) {
const pinPosition = endComponentRef.getPinPosition(wire.endComponentId); const pinPosition = endComponentRef.getPinPosition(wire.endPinLabel);
if (pinPosition) { if (pinPosition) {
wire.endX = (pinPosition.x - containerRect.left - position.x) / scale.value; const newEndX = (pinPosition.x - containerRect.left - position.x) / scale.value;
wire.endY = (pinPosition.y - containerRect.top - position.y) / scale.value; const newEndY = (pinPosition.y - containerRect.top - position.y) / scale.value;
console.log(`更新连线终点 ${wire.endComponentId}/${wire.endPinLabel}: (${wire.endX}, ${wire.endY}) => (${newEndX}, ${newEndY})`);
wire.endX = newEndX;
wire.endY = newEndY;
} else { } else {
console.warn(`无法获取组件${wire.endComponentId}的针脚${wire.endComponentId}位置`); console.warn(`无法获取针脚 ${wire.endComponentId}/${wire.endPinLabel} 的位置`);
//
setTimeout(() => {
const retryPosition = endComponentRef.getPinPosition(wire.endComponentId);
if (retryPosition) {
wire.endX = (retryPosition.x - containerRect.left - position.x) / scale.value;
wire.endY = (retryPosition.y - containerRect.top - position.y) / scale.value;
} }
}, 100); } else {
console.warn(`组件 ${wire.endComponentId} 没有实现 getPinPosition 方法`);
} }
} }
//
const positionChanged =
originalStartX !== wire.startX ||
originalStartY !== wire.startY ||
originalEndX !== wire.endX ||
originalEndY !== wire.endY;
if (positionChanged) {
//
ensureUniquePinPositions(wire);
} }
} }
@ -649,9 +738,14 @@ function updateWireWithPinPositions(wire: WireItem) {
function updateWiresForComponent(componentId: string) { function updateWiresForComponent(componentId: string) {
if (!canvasContainer.value || !componentId) return; if (!canvasContainer.value || !componentId) return;
console.log(`更新组件 ${componentId} 相关的连线位置`);
// //
const component = props.components.find(c => c.id === componentId); const component = props.components.find(c => c.id === componentId);
if (!component) return; if (!component) {
console.warn(`找不到组件 ${componentId}`);
return;
}
// 线 // 线
const relatedWires = wires.value.filter(wire => const relatedWires = wires.value.filter(wire =>
@ -659,14 +753,49 @@ function updateWiresForComponent(componentId: string) {
wire.endComponentId === componentId wire.endComponentId === componentId
); );
if (relatedWires.length === 0) return; console.log(`找到 ${relatedWires.length} 条相关连线`);
if (relatedWires.length === 0) {
// 线线
console.log('没有找到直接关联的连线,检查所有连线');
// 线
wires.value.forEach((wire, index) => {
console.log(`连线 ${index}: startComponentId=${wire.startComponentId}, endComponentId=${wire.endComponentId}`);
});
return;
}
// 线 // 线
relatedWires.forEach(wire => { relatedWires.forEach(wire => {
console.log(`更新连线 ${wire.id} (${wire.startComponentId}/${wire.startPinLabel} -> ${wire.endComponentId}/${wire.endPinLabel})`);
updateWireWithPinPositions(wire); updateWireWithPinPositions(wire);
}); });
} }
//
function ensureUniquePinPositions(wire: WireItem) {
if (Math.abs(wire.startX - wire.endX) < 5 && Math.abs(wire.startY - wire.endY) < 5) {
console.warn('检测到连线起点和终点非常接近,添加随机偏移');
// ID
const idSum = (wire.startComponentId?.charCodeAt(0) || 0) +
(wire.endComponentId?.charCodeAt(0) || 0) +
(wire.startPinLabel?.charCodeAt(0) || 0) +
(wire.endPinLabel?.charCodeAt(0) || 0);
// 使ID
const offsetX = 20 * Math.cos(idSum * 0.1);
const offsetY = 20 * Math.sin(idSum * 0.1);
wire.endX += offsetX;
wire.endY += offsetY;
console.log(`应用偏移 (${offsetX.toFixed(2)}, ${offsetY.toFixed(2)}) 到连线终点`);
}
}
// --- --- // --- ---
onMounted(() => { onMounted(() => {
// //
@ -680,6 +809,18 @@ onMounted(() => {
// //
window.addEventListener('keydown', handleKeyDown); window.addEventListener('keydown', handleKeyDown);
// 线
const wireUpdateInterval = setInterval(() => {
if (wires.value.length > 0) {
updateAllWires();
}
}, 1000); // 线
//
onUnmounted(() => {
clearInterval(wireUpdateInterval);
});
}); });
// //
@ -719,11 +860,11 @@ onUnmounted(() => {
width: 100%; width: 100%;
height: 100%; height: 100%;
overflow: hidden; overflow: hidden;
/* background-image: background-image:
linear-gradient(to right, rgba(100, 100, 100, 0.1) 1px, transparent 1px), linear-gradient(to right, rgba(100, 100, 100, 0.1) 1px, transparent 1px),
linear-gradient(to bottom, rgba(100, 100, 100, 0.1) 1px, transparent 1px), linear-gradient(to bottom, rgba(100, 100, 100, 0.1) 1px, transparent 1px),
linear-gradient(to right, rgba(80, 80, 80, 0.2) 100px, transparent 100px), linear-gradient(to right, rgba(80, 80, 80, 0.2) 100px, transparent 100px),
linear-gradient(to bottom, rgba(80, 80, 80, 0.2) 100px, transparent 100px); */ linear-gradient(to bottom, rgba(80, 80, 80, 0.2) 100px, transparent 100px);
background-size: 20px 20px, 20px 20px, 100px 100px, 100px 100px; background-size: 20px 20px, 20px 20px, 100px 100px, 100px 100px;
background-position: 0 0; background-position: 0 0;
user-select: none; user-select: none;
@ -750,7 +891,7 @@ onUnmounted(() => {
left: 0; left: 0;
width: 100%; width: 100%;
height: 100%; height: 100%;
pointer-events: auto; /* 修复:允许线被点击 */ pointer-events: none;
z-index: 50; z-index: 50;
} }
@ -789,13 +930,13 @@ onUnmounted(() => {
} }
/* 为黑暗模式设置不同的网格线颜色 */ /* 为黑暗模式设置不同的网格线颜色 */
/* :root[data-theme="dark"] .diagram-container { :root[data-theme="dark"] .diagram-container {
background-image: background-image:
linear-gradient(to right, rgba(200, 200, 200, 0.1) 1px, transparent 1px), linear-gradient(to right, rgba(200, 200, 200, 0.1) 1px, transparent 1px),
linear-gradient(to bottom, rgba(200, 200, 200, 0.1) 1px, transparent 1px), linear-gradient(to bottom, rgba(200, 200, 200, 0.1) 1px, transparent 1px),
linear-gradient(to right, rgba(180, 180, 180, 0.15) 100px, transparent 100px), linear-gradient(to right, rgba(180, 180, 180, 0.15) 100px, transparent 100px),
linear-gradient(to bottom, rgba(180, 180, 180, 0.15) 100px, transparent 100px); linear-gradient(to bottom, rgba(180, 180, 180, 0.15) 100px, transparent 100px);
} */ }
/* 深度选择器 - 默认阻止SVG内部元素的鼠标事件但允许SVG本身和特定交互元素 */ /* 深度选择器 - 默认阻止SVG内部元素的鼠标事件但允许SVG本身和特定交互元素 */
.component-wrapper :deep(svg) { .component-wrapper :deep(svg) {
@ -820,9 +961,4 @@ onUnmounted(() => {
.component-wrapper :deep(input) { .component-wrapper :deep(input) {
pointer-events: auto !important; pointer-events: auto !important;
} }
.wire-active {
stroke: #0099ff !important;
filter: drop-shadow(0 0 4px #0099ffcc);
}
</style> </style>

141
src/components/Wire.vue Normal file
View File

@ -0,0 +1,141 @@
<template>
<path
:d="pathData"
fill="none"
:stroke="strokeColor"
:stroke-width="strokeWidth"
stroke-linecap="round"
stroke-linejoin="round"
:class="{ 'wire-active': isActive }"
@click="handleClick"
/>
</template>
<script setup lang="ts">
import { computed, defineEmits } from 'vue';
interface Props {
id: string;
startX: number;
startY: number;
endX: number;
endY: number;
strokeColor?: string;
strokeWidth?: number;
isActive?: boolean;
routingMode?: 'auto' | 'orthogonal' | 'direct';
//
startComponentId?: string;
startPinLabel?: string;
endComponentId?: string;
endPinLabel?: string;
}
const props = withDefaults(defineProps<Props>(), {
strokeColor: '#4a5568',
strokeWidth: 2,
isActive: false,
routingMode: 'orthogonal'
});
const emit = defineEmits(['click']);
function handleClick(event: MouseEvent) {
emit('click', { id: props.id, event });
}
const pathData = computed(() => {
//
const dx = Math.abs(props.endX - props.startX);
const dy = Math.abs(props.endY - props.startY);
//
if (dx < 0.5 && dy < 0.5) {
console.warn('连线的起点和终点几乎重合,强制绘制可见路径');
//
const r = 5; // 5
return `M ${props.startX} ${props.startY}
m -${r}, 0
a ${r},${r} 0 1,0 ${r*2},0
a ${r},${r} 0 1,0 -${r*2},0`;
}
if (props.routingMode === 'direct') {
return `M ${props.startX} ${props.startY} L ${props.endX} ${props.endY}`;
} else if (props.routingMode === 'orthogonal') {
// 线
return calculateOrthogonalPath(props.startX, props.startY, props.endX, props.endY);
} else {
// 线
if (dx < 10 || dy < 10) {
// 使线
return `M ${props.startX} ${props.startY} L ${props.endX} ${props.endY}`;
} else {
// 使线
return calculateOrthogonalPath(props.startX, props.startY, props.endX, props.endY);
}
}
});
function calculateOrthogonalPath(startX: number, startY: number, endX: number, endY: number) {
//
const dx = endX - startX;
const dy = endY - startY;
//
if (Math.abs(dx) < 1 && Math.abs(dy) < 1) {
console.warn('连线的起点和终点几乎重合,调整路径显示');
//
const offset = 5; // 5
return `M ${startX} ${startY}
L ${startX + offset} ${startY}
L ${startX + offset} ${startY + offset}
L ${startX} ${startY + offset}
L ${startX} ${startY}`;
}
// 线线
if (dx === 0 || dy === 0) {
return `M ${startX} ${startY} L ${endX} ${endY}`;
}
// 45线
const absDx = Math.abs(dx);
const absDy = Math.abs(dy);
if (absDx === absDy) {
// 45线
return `M ${startX} ${startY} L ${endX} ${endY}`;
}
// - 使L
//
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}`;
}
}
</script>
<style scoped>
.wire-active {
stroke-dasharray: 5;
animation: dash 0.5s linear infinite;
}
@keyframes dash {
to {
stroke-dashoffset: 10;
}
}
</style>

View File

@ -76,15 +76,13 @@
pointerEvents: 'auto' pointerEvents: 'auto'
}"> }">
<Pin <Pin
ref="pinRef"
direction="output" direction="output"
type="digital" type="digital"
appearance="None"
:label="props.label" :label="props.label"
:constraint="props.constraint" :constraint="props.constraint"
:size="0.8" :size="0.8"
:componentId="props.componentId"
@value-change="handlePinValueChange" @value-change="handlePinValueChange"
@pin-click="handlePinClick"
/> />
</div> </div>
</div> </div>
@ -93,18 +91,15 @@
<script setup lang="ts"> <script setup lang="ts">
import { ref, onMounted, onUnmounted, computed } from 'vue'; import { ref, onMounted, onUnmounted, computed } from 'vue';
import Pin from './Pin.vue'; import Pin from './Pin.vue';
import { notifyConstraintChange } from '../../stores/constraints';
const pinRef = ref<any>(null);
// Pin // Pin
interface PinProps { interface PinProps {
label?: string; label?: string;
constraint?: string; constraint?: string;
componentId?: string; // componentId
// 便 // 便
direction?: 'input' | 'output' | 'inout'; direction?: 'input' | 'output' | 'inout';
type?: 'digital' | 'analog'; type?: 'digital' | 'analog';
appearance?: 'None' | 'Dip' | 'SMT';
} }
// //
@ -123,10 +118,10 @@ const props = withDefaults(defineProps<Props>(), {
buttonText: '', buttonText: '',
label: 'BTN', label: 'BTN',
constraint: '', constraint: '',
componentId: 'button-default', // componentId
// //
direction: 'output', direction: 'output',
type: 'digital' type: 'digital',
appearance: 'Dip'
}); });
// //
@ -147,8 +142,7 @@ const emit = defineEmits([
'press', 'press',
'release', 'release',
'click', 'click',
'value-change', 'value-change'
'pin-click'
]); ]);
// //
@ -161,11 +155,6 @@ function handlePinValueChange(value: any) {
emit('value-change', value); emit('value-change', value);
} }
// Pin
function handlePinClick(info: any) {
emit('pin-click', info);
}
// --- --- // --- ---
function toggleButtonState(isPressed: boolean) { function toggleButtonState(isPressed: boolean) {
isKeyPressed.value = isPressed; isKeyPressed.value = isPressed;
@ -174,17 +163,9 @@ function toggleButtonState(isPressed: boolean) {
// //
if (isPressed) { if (isPressed) {
emit('press'); emit('press');
//
if (props.constraint) {
notifyConstraintChange(props.constraint, 'high');
}
} else { } else {
emit('release'); emit('release');
emit('click'); emit('click');
//
if (props.constraint) {
notifyConstraintChange(props.constraint, 'low');
}
} }
} }
@ -215,18 +196,11 @@ defineExpose({
// Pin // Pin
label: props.label, label: props.label,
constraint: props.constraint, constraint: props.constraint,
componentId: props.componentId, // componentId
// Pin // Pin
direction: 'output', direction: 'output',
type: 'digital' type: 'digital',
}), appearance: 'None'
// getPinPosition Pin })
getPinPosition: (pinLabel: string) => {
if (pinRef.value && pinRef.value.getPinPosition) {
return pinRef.value.getPinPosition(pinLabel);
}
return null;
}
}); });
</script> </script>

View File

@ -1,35 +1,79 @@
<template> <template>
<svg <svg
xmlns="http://www.w3.org/2000/svg"
:width="width" :width="width"
:height="height" :height="height"
:viewBox="'0 0 ' + viewBoxWidth + ' ' + viewBoxHeight" :viewBox="'0 0 ' + viewBoxWidth + ' ' + viewBoxHeight"
class="pin-component" class="pin-component"
:data-component-id="props.componentId" :data-pin-id="props.label"
:data-pin-label="props.label"
> >
<g :transform="`translate(${viewBoxWidth/2}, ${viewBoxHeight/2})`"> <g :transform="`translate(${viewBoxWidth/2}, ${viewBoxHeight/2})`"> <g v-if="props.appearance === 'None'">
<g> <g transform="translate(-12.5, -12.5)" class="interactive"> <circle
<g transform="translate(-12.5, -12.5)"> style="fill:#909090"
<circle
:style="{ fill: pinColor }"
cx="12.5" cx="12.5"
cy="12.5" cy="12.5"
r="3.75" r="3.75"
class="interactive" @mouseenter="showPinTooltip"
@mouseleave="hidePinTooltip"
@click.stop="handlePinClick" @click.stop="handlePinClick"
:data-pin-element="`${props.componentId}`" /> :data-pin-id="`${props.label}-${props.constraint}`" />
</g> </g>
</g> </g>
<g v-else-if="props.appearance === 'Dip'">
<!-- 使用inkscape创建的SVG替代原有Dip样式 -->
<g transform="translate(-12.5, -12.5)" class="interactive">
<rect
:style="`fill:${props.type === 'analog' ? '#2a6099' : '#000000'};fill-opacity:0.772973`"
width="25"
height="25"
x="0"
y="0"
rx="2.5" /> <circle
style="fill:#ecececc5;fill-opacity:0.772973"
cx="12.5"
cy="12.5"
r="3.75"
@mouseenter="showPinTooltip"
@mouseleave="hidePinTooltip"
@click.stop="handlePinClick"
:data-pin-id="`${props.label}-${props.constraint}`" />
<text
style="font-size:6.85px;text-align:start;fill:#ffffff;fill-opacity:0.772973"
x="7.3"
y="7"
xml:space="preserve">{{ props.label }}</text>
</g> </g>
</g> <g v-else-if="props.appearance === 'SMT'">
<rect x="-20" y="-10" width="40" height="20" fill="#aaa" rx="2" ry="2" />
<rect x="-18" y="-8" width="36" height="16" :fill="getColorByType" rx="1" ry="1" />
<rect x="-16" y="-6" width="26" height="12" :fill="getColorByType" rx="1" ry="1" />
<text text-anchor="middle" dominant-baseline="middle" font-size="8" fill="white" x="-3">{{ props.label }}</text>
<!-- SMT样式的针脚 --> <circle
fill="#ecececc5"
cx="10"
cy="0"
r="3.75"
class="interactive"
@mouseenter="showPinTooltip"
@mouseleave="hidePinTooltip"
@click.stop="handlePinClick"
:data-pin-id="`${props.label}-${props.constraint}`" />
</g> </g>
</svg> </svg>
<!-- 提示框 - 在SVG外部 -->
<div v-if="showTooltip" class="pin-tooltip" :style="{
position: 'absolute',
top: tooltipPosition.top + 'px',
left: tooltipPosition.left + 'px',
transform: 'translate(-50%, -100%)',
marginTop: '-5px'
}">
{{ tooltipText }}
</div>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { ref, computed, reactive, watch, onMounted, onUnmounted } from 'vue'; import { ref, computed, reactive } from 'vue';
import { getConstraintColor, getConstraintState, onConstraintStateChange, notifyConstraintChange } from '../../stores/constraints';
// ID
const uniqueId = `pin-${Date.now()}-${Math.random().toString(36).substring(2, 9)}`;
interface Props { interface Props {
size?: number; size?: number;
@ -37,7 +81,7 @@ interface Props {
constraint?: string; constraint?: string;
direction?: 'input' | 'output' | 'inout'; direction?: 'input' | 'output' | 'inout';
type?: 'digital' | 'analog'; type?: 'digital' | 'analog';
componentId?: string; // ID appearance?: 'None' | 'Dip' | 'SMT';
} }
const props = withDefaults(defineProps<Props>(), { const props = withDefaults(defineProps<Props>(), {
@ -46,7 +90,7 @@ const props = withDefaults(defineProps<Props>(), {
constraint: '', constraint: '',
direction: 'input', direction: 'input',
type: 'digital', type: 'digital',
componentId: 'pin-default' // ID appearance: 'Dip'
}); });
const emit = defineEmits([ const emit = defineEmits([
@ -56,6 +100,58 @@ const emit = defineEmits([
// //
const analogValue = ref(0); const analogValue = ref(0);
const showTooltip = ref(false);
const tooltipText = ref('');
const tooltipPosition = reactive({
top: 0,
left: 0
});
//
function showPinTooltip(event: MouseEvent) {
showTooltip.value = true;
const target = event.target as SVGElement;
const rect = target.getBoundingClientRect();
//
tooltipPosition.top = rect.top;
tooltipPosition.left = rect.left + rect.width / 2;
//
tooltipText.value = generateTooltipText();
}
//
function hidePinTooltip() {
showTooltip.value = false;
}
//
function generateTooltipText() {
const parts = [];
parts.push(`标签: ${props.label}`);
if (props.constraint) {
parts.push(`约束: ${props.constraint}`);
} else {
parts.push('约束: 未定义');
}
parts.push(`方向: ${getDirectionText()}`);
parts.push(`类型: ${props.type === 'digital' ? '数字' : '模拟'}`);
return parts.join(' | ');
}
//
function getDirectionText() {
switch (props.direction) {
case 'input': return '输入';
case 'output': return '输出';
case 'inout': return '双向';
default: return '未知';
}
}
// //
function handlePinClick(event: MouseEvent) { function handlePinClick(event: MouseEvent) {
@ -66,64 +162,42 @@ function handlePinClick(event: MouseEvent) {
x: rect.left + rect.width / 2, x: rect.left + rect.width / 2,
y: rect.top + rect.height / 2 y: rect.top + rect.height / 2
}; };
console.log(`针脚 ${props.label} 被点击,位置:`, pinCenter);
//
emit('pin-click', { emit('pin-click', {
label: props.label, label: props.label,
constraint: props.constraint, constraint: props.constraint,
type: props.type, type: props.type,
direction: props.direction, direction: props.direction,
position: pinCenter, //
position: {
x: pinCenter.x,
y: pinCenter.y
},
//
originalEvent: event originalEvent: event
}); });
} }
const width = computed(() => 40 * props.size); const width = computed(() => props.appearance === 'None' ? 40 * props.size : 30 * props.size);
const height = computed(() => 20 * props.size); const height = computed(() => {
const viewBoxWidth = computed(() => 40); if (props.appearance === 'None') return 20 * props.size;
const viewBoxHeight = computed(() => 20); if (props.appearance === 'Dip') return 30 * props.size; // Dip
return 60 * props.size;
});
const viewBoxWidth = computed(() => props.appearance === 'None' ? 40 : 30);
const viewBoxHeight = computed(() => {
if (props.appearance === 'None') return 20;
if (props.appearance === 'Dip') return 30; // Dip
return 60;
});
const getColorByType = computed(() => { const getColorByType = computed(() => {
return props.type === 'analog' ? '#2a6099' : '#444'; return props.type === 'analog' ? '#2a6099' : '#444';
}); });
//
const pinColor = computed(() => {
return getConstraintColor(props.constraint) || getColorByType.value;
});
//
let unsubscribe: (() => void) | null = null;
onMounted(() => {
//
if (props.constraint) {
unsubscribe = onConstraintStateChange((constraint, level) => {
if (constraint === props.constraint) {
emit('value-change', { constraint, level });
}
});
}
});
onUnmounted(() => {
if (unsubscribe) {
unsubscribe();
}
});
watch(() => props.constraint, (newConstraint, oldConstraint) => {
if (unsubscribe) {
unsubscribe();
unsubscribe = null;
}
if (newConstraint) {
unsubscribe = onConstraintStateChange((constraint, level) => {
if (constraint === newConstraint) {
emit('value-change', { constraint, level });
}
});
}
});
function updateAnalogValue(value: number) { function updateAnalogValue(value: number) {
if (props.type !== 'analog') return; if (props.type !== 'analog') return;
analogValue.value = Math.max(0, Math.min(1, value)); analogValue.value = Math.max(0, Math.min(1, value));
@ -142,39 +216,46 @@ defineExpose({
constraint: props.constraint, constraint: props.constraint,
direction: props.direction, direction: props.direction,
type: props.type, type: props.type,
componentId: props.componentId appearance: props.appearance
}), }), //
getPinPosition: (componentId: string) => { getPinPosition: (pinLabel: string) => {
if (componentId !== props.componentId) return null; // Pin
console.log('getPinPosition', componentId, props.componentId); if (pinLabel !== props.label) return null;
const uniqueSelector = `[data-pin-element="${props.componentId}"]`;
console.log('uniqueSelector', uniqueSelector); // 使DOM
const pinElements = document.querySelectorAll(uniqueSelector); // document.querySelector
console.log('pinElements', pinElements); const pinElement = document.querySelector(`[data-pin-id="${props.label}-${props.constraint}"]`) as SVGElement;
if (pinElements.length === 0) return null;
if (pinElements.length === 1) { if (pinElement) {
const rect = pinElements[0].getBoundingClientRect(); //
return {
x: rect.left + rect.width / 2,
y: rect.top + rect.height / 2
};
}
for (const pinElement of pinElements) {
let parentSvg = pinElement.closest('svg.pin-component');
if (!parentSvg) continue;
if (parentSvg.getAttribute('data-component-id') === props.componentId) {
const rect = pinElement.getBoundingClientRect(); const rect = pinElement.getBoundingClientRect();
return { return {
x: rect.left + rect.width / 2, x: rect.left + rect.width / 2,
y: rect.top + rect.height / 2 y: rect.top + rect.height / 2
}; };
} }
// 使
// SVG
const svgElement = document.querySelector(`.pin-component[data-pin-id="${props.label}"]`) as SVGElement;
if (!svgElement) {
console.error(`找不到针脚 ${props.label} 的SVG元素`);
return null;
} }
const rect = pinElements[0].getBoundingClientRect();
return { const svgRect = svgElement.getBoundingClientRect();
x: rect.left + rect.width / 2,
y: rect.top + rect.height / 2 //
}; let pinX = svgRect.left + svgRect.width / 2;
let pinY = svgRect.top + svgRect.height / 2;
//
// 使
const randomOffset = 0.1;
pinX += Math.random() * randomOffset;
pinY += Math.random() * randomOffset;
return { x: pinX, y: pinY };
} }
}); });
</script> </script>
@ -192,4 +273,15 @@ defineExpose({
.interactive:hover { .interactive:hover {
filter: brightness(1.2); filter: brightness(1.2);
} }
.pin-tooltip {
background-color: rgba(0, 0, 0, 0.8);
color: white;
padding: 4px 8px;
border-radius: 4px;
font-size: 12px;
z-index: 1000;
pointer-events: none;
white-space: nowrap;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.2);
}
</style> </style>

View File

@ -20,9 +20,10 @@
x="15" x="15"
y="15" y="15"
:fill="ledColor" :fill="ledColor"
:style="{ opacity: isOn ? 1 : 0.2 }" :style="{ opacity: isOn ? brightness/100 : 0.2 }"
rx="15" rx="15"
ry="15" ry="15"
@click="toggleLed"
class="interactive" class="interactive"
/> />
@ -34,64 +35,46 @@
x="12" x="12"
y="12" y="12"
:fill="ledColor" :fill="ledColor"
:style="{ opacity: 0.3 }" :style="{ opacity: brightness/100 * 0.3 }"
rx="18" rx="18"
ry="18" ry="18"
filter="blur(5px)" filter="blur(5px)"
class="glow" class="glow"
/> />
</svg> </svg>
<!-- 新增数字输入引脚Pin放在LED左侧居中 -->
<div style="position:absolute;left:-18px;top:50%;transform:translateY(-50%);">
<Pin
ref="pinRef"
v-bind="props"
@pin-click="$emit('pin-click', $event)"
/>
</div>
</div> </div>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { ref, computed, watch, onMounted, onUnmounted } from 'vue'; import { ref, computed, watch } from 'vue';
import { getConstraintState, onConstraintStateChange } from '../../stores/constraints';
import Pin from './Pin.vue';
// --- getPinPositionPin ---
const pinRef = ref<any>(null);
// Pin
interface PinProps {
label?: string;
constraint?: string;
componentId?: string; // componentId
// 便
direction?: 'input' | 'output' | 'inout';
type?: 'digital' | 'analog';
}
// LED // LED
interface LEDProps { interface Props {
size?: number; size?: number;
color?: string; color?: string;
initialOn?: boolean;
brightness?: number;
constraint?: string;
} }
// //
interface Props extends PinProps, LEDProps {}
const props = withDefaults(defineProps<Props>(), { const props = withDefaults(defineProps<Props>(), {
size: 1, size: 1,
color: 'red', color: 'red',
constraint: '', initialOn: false,
label: 'LED', brightness: 80, // 80%
componentId: '' constraint: ''
}); });
//
const width = computed(() => 100 * props.size); const width = computed(() => 100 * props.size);
const height = computed(() => 60 * props.size); const height = computed(() => 60 * props.size);
const isOn = ref(false); //
const isOn = ref(props.initialOn);
const brightness = ref(props.brightness);
// LED
const colorMap: Record<string, string> = { const colorMap: Record<string, string> = {
'red': '#ff3333', 'red': '#ff3333',
'green': '#33ff33', 'green': '#33ff33',
@ -102,64 +85,70 @@ const colorMap: Record<string, string> = {
'purple': '#9933ff' 'purple': '#9933ff'
}; };
// LED
const ledColor = computed(() => { const ledColor = computed(() => {
return colorMap[props.color.toLowerCase()] || props.color; return colorMap[props.color.toLowerCase()] || props.color;
}); });
// //
let unsubscribe: (() => void) | null = null; const emit = defineEmits([
'toggle',
'brightness-change',
'value-change'
]);
onMounted(() => { // LED
if (props.constraint) { function toggleLed() {
unsubscribe = onConstraintStateChange((constraint, level) => { isOn.value = !isOn.value;
if (constraint === props.constraint) { emit('toggle', isOn.value);
isOn.value = (level === 'high'); emit('value-change', {
} isOn: isOn.value,
brightness: brightness.value
}); });
// LED }
const currentState = getConstraintState(props.constraint);
isOn.value = (currentState === 'high');
}
});
onUnmounted(() => { //
if (unsubscribe) { function setBrightness(value: number) {
unsubscribe(); // 0-100
} brightness.value = Math.max(0, Math.min(100, value));
}); emit('brightness-change', brightness.value);
emit('value-change', {
watch(() => props.constraint, (newConstraint) => { isOn: isOn.value,
if (unsubscribe) { brightness: brightness.value
unsubscribe();
unsubscribe = null;
}
if (newConstraint) {
unsubscribe = onConstraintStateChange((constraint, level) => {
if (constraint === newConstraint) {
isOn.value = (level === 'high');
}
}); });
// LED }
const currentState = getConstraintState(newConstraint);
isOn.value = (currentState === 'high'); // LED
} function setLedState(on: boolean) {
isOn.value = on;
emit('toggle', isOn.value);
emit('value-change', {
isOn: isOn.value,
brightness: brightness.value
});
}
// props
watch(() => props.brightness, (newVal) => {
brightness.value = newVal;
}); });
watch(() => props.initialOn, (newVal) => {
isOn.value = newVal;
});
//
defineExpose({ defineExpose({
toggleLed,
setBrightness,
setLedState,
getInfo: () => ({ getInfo: () => ({
// LED
color: props.color, color: props.color,
isOn: isOn.value, isOn: isOn.value,
constraint: props.constraint, brightness: brightness.value,
componentId: props.componentId, constraint: props.constraint
direction: 'input', })
type: 'digital'
}),
getPinPosition: (componentId: string) => {
if (pinRef.value && pinRef.value.getPinPosition) {
return pinRef.value.getPinPosition(componentId);
}
return null;
}
}); });
</script> </script>

View File

@ -1,198 +0,0 @@
<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 { computed, defineEmits, reactive, watch, onMounted, onUnmounted } from 'vue';
import { getConstraintColor, getConstraintState, onConstraintStateChange } from '../../stores/constraints';
interface Props {
id: string;
startX: number;
startY: number;
endX: number;
endY: number;
strokeColor?: string;
strokeWidth?: number;
isActive?: boolean;
routingMode?: 'auto' | 'orthogonal' | 'direct';
//
startComponentId?: string;
startPinLabel?: string;
endComponentId?: string;
endPinLabel?: string;
//
constraint?: string;
//
showLabel?: boolean;
}
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);
});
// 使isActiveconstraint
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(() => {
return calculateOrthogonalPath(props.startX, props.startY, props.endX, props.endY);
});
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}`;
}
}
//
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,
startPinLabel: props.startPinLabel,
endComponentId: props.endComponentId,
endPinLabel: props.endPinLabel,
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>

View File

@ -154,6 +154,18 @@ const componentConfigs: Record<string, ComponentConfig> = {
{ value: 'analog', label: 'analog' } { value: 'analog', label: 'analog' }
], ],
description: '引脚的模数特性,数字或模拟' description: '引脚的模数特性,数字或模拟'
},
{
name: 'appearance',
type: 'select',
label: '引脚样式',
default: 'Dip',
options: [
{ value: 'None', label: 'None' },
{ value: 'Dip', label: 'Dip' },
{ value: 'SMT', label: 'SMT' }
],
description: '引脚的外观样式,不影响功能'
} }
] ]
}, },
@ -301,60 +313,12 @@ const componentConfigs: Record<string, ComponentConfig> = {
{ {
name: 'constraint', name: 'constraint',
type: 'string', type: 'string',
label: '引脚约束', label: '连接约束',
default: '', default: '',
description: '相同约束字符串的引脚将被视为有电气连接' description: '相同约束字符串的组件将被视为有电气连接'
} }
] ]
},
// 线缆配置
Wire: {
props: [
{
name: 'routingMode',
type: 'select',
label: '路由方式',
default: 'orthogonal',
options: [
{ value: 'orthogonal', label: '直角' },
{ value: 'direct', label: '直线' },
{ value: 'auto', label: '自动' }
],
description: '线路连接方式'
},
{
name: 'strokeColor',
type: 'string',
label: '线条颜色',
default: '#4a5568',
description: '线条颜色使用CSS颜色值'
},
{
name: 'strokeWidth',
type: 'number',
label: '线条宽度',
default: 2,
min: 1,
max: 10,
step: 0.5,
description: '线条宽度'
},
{
name: 'constraint',
type: 'string',
label: '约束名称',
default: '',
description: '线路约束名称,用于标识连接关系'
},
{
name: 'showLabel',
type: 'boolean',
label: '显示标签',
default: false,
description: '是否显示连线上的约束标签'
} }
]
},
}; };
// 获取组件配置的函数 // 获取组件配置的函数

View File

@ -1,86 +0,0 @@
import { ref, reactive } from 'vue';
export interface WireItem {
id: string;
startX: number;
startY: number;
endX: number;
endY: number;
startComponentId: string;
endComponentId?: string;
color?: string;
isActive?: boolean;
constraint?: string;
strokeWidth?: number;
routingMode?: 'auto' | 'orthogonal' | 'direct';
showLabel?: boolean;
}
// 全局wires状态
export const wires = ref<WireItem[]>([]);
// 添加新连线
export function addWire(wire: WireItem) {
wires.value.push(wire);
}
// 删除连线
export function deleteWire(wireId: string) {
const idx = wires.value.findIndex(w => w.id === wireId);
if (idx !== -1) wires.value.splice(idx, 1);
}
// 批量更新与某个引脚相关的所有Wire的约束
export function updateWiresConstraintByPin(componentId: string, newConstraint: string) {
wires.value.forEach(wire => {
if (
(wire.startComponentId === componentId) ||
(wire.endComponentId === componentId)
) {
wire.constraint = newConstraint;
}
});
}
// 查找与某个引脚相关的所有Wire
export function findWiresByPin(componentId: string) {
return wires.value.filter(wire =>
(wire.startComponentId === componentId) ||
(wire.endComponentId === componentId)
);
}
// 临时连线相关状态
export const isCreatingWire = ref(false);
export const creatingWireStart = reactive({ x: 0, y: 0 });
export const creatingWireStartInfo = reactive({
componentId: '',
pinLabel: '',
constraint: ''
});
export const mousePosition = reactive({ x: 0, y: 0 });
export function resetWireCreation() {
isCreatingWire.value = false;
creatingWireStart.x = 0;
creatingWireStart.y = 0;
creatingWireStartInfo.componentId = '';
creatingWireStartInfo.pinLabel = '';
creatingWireStartInfo.constraint = '';
}
export function setWireCreationStart(x: number, y: number, componentId: string, pinLabel: string, constraint: string) {
isCreatingWire.value = true;
creatingWireStart.x = x;
creatingWireStart.y = y;
creatingWireStartInfo.componentId = componentId;
creatingWireStartInfo.pinLabel = pinLabel;
creatingWireStartInfo.constraint = constraint;
}
export function setMousePosition(x: number, y: number) {
mousePosition.x = x;
mousePosition.y = y;
}
// 其它Wire相关操作可继续扩展...

View File

@ -5,10 +5,6 @@ import { createPinia } from 'pinia'
import App from '@/App.vue' import App from '@/App.vue'
import router from './router' import router from './router'
import { initConstraintCommunication } from './APIClient'
const app = createApp(App).use(router).use(createPinia()).mount('#app') const app = createApp(App).use(router).use(createPinia()).mount('#app')
// 初始化约束通信
initConstraintCommunication();

View File

@ -1,82 +0,0 @@
import { ref, reactive } from 'vue';
// 约束电平状态类型
export type ConstraintLevel = 'high' | 'low' | 'undefined';
// 约束状态存储
const constraintStates = reactive<Record<string, ConstraintLevel>>({});
// 约束颜色映射
export const constraintColors = {
high: '#ff3333', // 高电平为红色
low: '#3333ff', // 低电平为蓝色
undefined: '#999999' // 未定义为灰色
};
// 获取约束状态
export function getConstraintState(constraint: string): ConstraintLevel {
if (!constraint) return 'undefined';
return constraintStates[constraint] || 'undefined';
}
// 设置约束状态
export function setConstraintState(constraint: string, level: ConstraintLevel) {
if (!constraint) return;
constraintStates[constraint] = level;
}
// 批量设置约束状态
export function batchSetConstraintStates(states: Record<string, ConstraintLevel>) {
// 收集发生变化的约束
const changedConstraints: [string, ConstraintLevel][] = [];
// 更新状态并收集变化
Object.entries(states).forEach(([constraint, level]) => {
if (constraintStates[constraint] !== level) {
constraintStates[constraint] = level;
changedConstraints.push([constraint, level]);
}
});
// 通知所有变化
changedConstraints.forEach(([constraint, level]) => {
stateChangeCallbacks.forEach(callback => callback(constraint, level));
});
}
// 获取约束对应的颜色
export function getConstraintColor(constraint: string): string {
const state = getConstraintState(constraint);
return constraintColors[state];
}
// 清除所有约束状态
export function clearAllConstraintStates() {
Object.keys(constraintStates).forEach(key => {
delete constraintStates[key];
});
}
// 获取所有约束状态
export function getAllConstraintStates(): Record<string, ConstraintLevel> {
return { ...constraintStates };
}
// 注册约束状态变化回调
const stateChangeCallbacks: ((constraint: string, level: ConstraintLevel) => void)[] = [];
export function onConstraintStateChange(callback: (constraint: string, level: ConstraintLevel) => void) {
stateChangeCallbacks.push(callback);
return () => {
const index = stateChangeCallbacks.indexOf(callback);
if (index > -1) {
stateChangeCallbacks.splice(index, 1);
}
};
}
// 触发约束变化
export function notifyConstraintChange(constraint: string, level: ConstraintLevel) {
setConstraintState(constraint, level);
stateChangeCallbacks.forEach(callback => callback(constraint, level));
}

View File

@ -50,7 +50,7 @@
type="text" type="text"
:placeholder="prop.label || prop.name" :placeholder="prop.label || prop.name"
class="input input-bordered input-sm w-full" class="input input-bordered input-sm w-full"
:value="selectedComponentData.props?.[prop.name]" :value="selectedComponentData.props[prop.name]"
@input="updateComponentProp(selectedComponentData.id, prop.name, ($event.target as HTMLInputElement).value)" @input="updateComponentProp(selectedComponentData.id, prop.name, ($event.target as HTMLInputElement).value)"
/> />
<input <input
@ -58,14 +58,14 @@
type="number" type="number"
:placeholder="prop.label || prop.name" :placeholder="prop.label || prop.name"
class="input input-bordered input-sm w-full" class="input input-bordered input-sm w-full"
:value="selectedComponentData.props?.[prop.name]" :value="selectedComponentData.props[prop.name]"
@input="updateComponentProp(selectedComponentData.id, prop.name, parseFloat(($event.target as HTMLInputElement).value) || prop.default)" @input="updateComponentProp(selectedComponentData.id, prop.name, parseFloat(($event.target as HTMLInputElement).value) || prop.default)"
/> <!-- boolean checkbox color color picker --> /> <!-- boolean checkbox color color picker -->
<div v-else-if="prop.type === 'boolean'" class="flex items-center"> <div v-else-if="prop.type === 'boolean'" class="flex items-center">
<input <input
type="checkbox" type="checkbox"
class="checkbox checkbox-sm mr-2" class="checkbox checkbox-sm mr-2"
:checked="selectedComponentData.props?.[prop.name]" :checked="selectedComponentData.props[prop.name]"
@change="updateComponentProp(selectedComponentData.id, prop.name, ($event.target as HTMLInputElement).checked)" @change="updateComponentProp(selectedComponentData.id, prop.name, ($event.target as HTMLInputElement).checked)"
/> />
<span>{{ prop.label || prop.name }}</span> <span>{{ prop.label || prop.name }}</span>
@ -73,12 +73,12 @@
<select <select
v-else-if="prop.type === 'select' && prop.options" v-else-if="prop.type === 'select' && prop.options"
class="select select-bordered select-sm w-full" class="select select-bordered select-sm w-full"
:value="selectedComponentData.props?.[prop.name]" :value="selectedComponentData.props[prop.name]"
@change="(event) => { @change="(event) => {
const selectElement = event.target as HTMLSelectElement; const selectElement = event.target as HTMLSelectElement;
const value = selectElement.value; const value = selectElement.value;
console.log('选择的值:', value, '类型:', typeof value); console.log('选择的值:', value, '类型:', typeof value);
if (selectedComponentData) {updateComponentProp(selectedComponentData.id, prop.name, value);} updateComponentProp(selectedComponentData.id, prop.name, value);
}" }"
> >
<option v-for="option in prop.options" :key="option.value" :value="option.value"> <option v-for="option in prop.options" :key="option.value" :value="option.value">
@ -143,7 +143,6 @@ interface ComponentModule {
type: string; type: string;
label?: string; label?: string;
default: any; default: any;
options?: Array<{ value: any; label: string }>; // options select
}>; }>;
}; };
} }
@ -345,7 +344,7 @@ function updateComponentProp(componentId: string | { id: string; propName: strin
// 线 // 线
function handleWireCreated(wireData: any) { function handleWireCreated(wireData: any) {
console.log('Wire created:', wireData); console.log('Wire created:', wireData);
// 线DiagramCanvas.vue // 线
} }
// 线 // 线