feat: Enhance equipment components with pin functionality and constraint management

- Added pin support to MechanicalButton, enabling pin-click events and componentId handling.
- Updated Pin component to manage constraint states and colors dynamically.
- Integrated SMT_LED with pin functionality, allowing LED state to respond to constraints.
- Enhanced Wire component to reflect constraint colors and manage wire states based on pin connections.
- Introduced wireManager for managing wire states and constraints.
- Implemented a constraints store for managing and notifying constraint state changes across components.
- Updated component configuration to remove appearance options and clarify constraint descriptions.
- Improved ProjectView to handle optional chaining for props and ensure robust data handling.
- Initialized constraint communication in main application entry point.
This commit is contained in:
alivender
2025-04-29 11:05:30 +08:00
parent 10db7c67bf
commit 1c75aa621a
12 changed files with 590 additions and 420 deletions

View File

@@ -76,13 +76,15 @@
pointerEvents: 'auto'
}">
<Pin
ref="pinRef"
direction="output"
type="digital"
appearance="None"
:label="props.label"
:constraint="props.constraint"
:size="0.8"
:componentId="props.componentId"
@value-change="handlePinValueChange"
@pin-click="handlePinClick"
/>
</div>
</div>
@@ -91,15 +93,18 @@
<script setup lang="ts">
import { ref, onMounted, onUnmounted, computed } from 'vue';
import Pin from './Pin.vue';
import { notifyConstraintChange } from '../../stores/constraints';
const pinRef = ref<any>(null);
// 从Pin组件继承属性
interface PinProps {
label?: string;
constraint?: string;
componentId?: string; // 添加componentId属性
// 这些属性被预设为固定值,但仍然包含在类型中以便完整继承
direction?: 'input' | 'output' | 'inout';
type?: 'digital' | 'analog';
appearance?: 'None' | 'Dip' | 'SMT';
}
// 按钮特有属性
@@ -118,10 +123,10 @@ const props = withDefaults(defineProps<Props>(), {
buttonText: '',
label: 'BTN',
constraint: '',
componentId: 'button-default', // 添加默认componentId
// 这些值会被覆盖,但需要默认值以满足类型要求
direction: 'output',
type: 'digital',
appearance: 'Dip'
type: 'digital'
});
// 计算实际宽高
@@ -142,7 +147,8 @@ const emit = defineEmits([
'press',
'release',
'click',
'value-change'
'value-change',
'pin-click'
]);
// 内部状态
@@ -155,6 +161,11 @@ function handlePinValueChange(value: any) {
emit('value-change', value);
}
// 处理Pin点击事件
function handlePinClick(info: any) {
emit('pin-click', info);
}
// --- 按键状态逻辑 ---
function toggleButtonState(isPressed: boolean) {
isKeyPressed.value = isPressed;
@@ -163,9 +174,17 @@ function toggleButtonState(isPressed: boolean) {
// 发出事件通知父组件
if (isPressed) {
emit('press');
// 如果有约束,通知约束状态变化为高电平
if (props.constraint) {
notifyConstraintChange(props.constraint, 'high');
}
} else {
emit('release');
emit('click');
// 如果有约束,通知约束状态变化为低电平
if (props.constraint) {
notifyConstraintChange(props.constraint, 'low');
}
}
}
@@ -196,11 +215,18 @@ defineExpose({
// 继承自Pin的属性
label: props.label,
constraint: props.constraint,
componentId: props.componentId, // 添加componentId
// 固定的Pin属性
direction: 'output',
type: 'digital',
appearance: 'None'
})
type: 'digital'
}),
// 代理 getPinPosition 到内部 Pin
getPinPosition: (pinLabel: string) => {
if (pinRef.value && pinRef.value.getPinPosition) {
return pinRef.value.getPinPosition(pinLabel);
}
return null;
}
});
</script>

View File

@@ -1,71 +1,35 @@
<template>
<svg
xmlns="http://www.w3.org/2000/svg"
:width="width"
:height="height"
:width="width"
:height="height"
:viewBox="'0 0 ' + viewBoxWidth + ' ' + viewBoxHeight"
class="pin-component"
:data-component-id="componentId"
:data-component-id="props.componentId"
:data-pin-label="props.label"
> <g :transform="`translate(${viewBoxWidth/2}, ${viewBoxHeight/2})`">
<g v-if="props.appearance === 'None'">
>
<g :transform="`translate(${viewBoxWidth/2}, ${viewBoxHeight/2})`">
<g>
<g transform="translate(-12.5, -12.5)">
<circle
style="fill:#909090"
:style="{ fill: pinColor }"
cx="12.5"
cy="12.5"
r="3.75"
class="interactive"
@click.stop="handlePinClick"
:data-pin-element="`${props.componentId}-${props.label}`" />
</g>
</g>
<g v-else-if="props.appearance === 'Dip'">
<!-- 使用inkscape创建的SVG替代原有Dip样式 -->
<g transform="translate(-12.5, -12.5)">
<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"
class="interactive"
@click.stop="handlePinClick"
:data-pin-element="`${props.componentId}-${props.label}`" />
<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 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>
<circle
fill="#ecececc5"
cx="10"
cy="0"
r="3.75"
class="interactive"
@click.stop="handlePinClick"
:data-pin-element="`${props.componentId}-${props.label}`"
/>
:data-pin-element="`${props.componentId}`" />
</g>
</g>
</g>
</svg>
</template>
<script setup lang="ts">
import { ref, computed, reactive } from 'vue';
import { ref, computed, reactive, watch, onMounted, onUnmounted } 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 {
size?: number;
@@ -73,7 +37,6 @@ interface Props {
constraint?: string;
direction?: 'input' | 'output' | 'inout';
type?: 'digital' | 'analog';
appearance?: 'None' | 'Dip' | 'SMT';
componentId?: string; // 添加组件ID属性用于唯一标识
}
@@ -83,7 +46,6 @@ const props = withDefaults(defineProps<Props>(), {
constraint: '',
direction: 'input',
type: 'digital',
appearance: 'Dip',
componentId: 'pin-default' // 默认ID
});
@@ -104,42 +66,64 @@ function handlePinClick(event: MouseEvent) {
x: rect.left + rect.width / 2,
y: rect.top + rect.height / 2
};
console.log(`针脚 ${props.label} 被点击,位置:`, pinCenter);
// 发送针脚点击事件给父组件
emit('pin-click', {
label: props.label,
constraint: props.constraint,
type: props.type,
direction: props.direction,
// 获取针脚在页面上的位置
position: {
x: pinCenter.x,
y: pinCenter.y
},
// 获取原始事件
position: pinCenter,
originalEvent: event
});
}
const width = computed(() => props.appearance === 'None' ? 40 * props.size : 30 * props.size);
const height = computed(() => {
if (props.appearance === 'None') return 20 * props.size;
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 width = computed(() => 40 * props.size);
const height = computed(() => 20 * props.size);
const viewBoxWidth = computed(() => 40);
const viewBoxHeight = computed(() => 20);
const getColorByType = computed(() => {
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) {
if (props.type !== 'analog') return;
analogValue.value = Math.max(0, Math.min(1, value));
@@ -158,38 +142,39 @@ defineExpose({
constraint: props.constraint,
direction: props.direction,
type: props.type,
appearance: props.appearance
}), // 添加获取针脚位置的方法
getPinPosition: (pinLabel: string) => {
// 在 Pin 组件中,只有一个针脚,所以直接检查标签是否匹配
if (pinLabel !== props.label) return null;
// 使用组件ID和针脚标签的组合作为唯一标识符
const selector = `[data-pin-element="${props.componentId}-${props.label}"]`;
const pinElement = document.querySelector(selector) as SVGElement;
if (pinElement) {
// 获取针脚元素的位置
const rect = pinElement.getBoundingClientRect();
componentId: props.componentId
}),
getPinPosition: (componentId: string) => {
if (componentId !== props.componentId) return null;
console.log('getPinPosition', componentId, props.componentId);
const uniqueSelector = `[data-pin-element="${props.componentId}"]`;
console.log('uniqueSelector', uniqueSelector);
const pinElements = document.querySelectorAll(uniqueSelector);
console.log('pinElements', pinElements);
if (pinElements.length === 0) return null;
if (pinElements.length === 1) {
const rect = pinElements[0].getBoundingClientRect();
return {
x: rect.left + rect.width / 2,
y: rect.top + rect.height / 2
};
}
// 如果找不到特定元素使用SVG元素位置
const svgSelector = `svg.pin-component[data-component-id="${props.componentId}"][data-pin-label="${props.label}"]`;
const svgElement = document.querySelector(svgSelector) as SVGElement;
if (!svgElement) return null;
const svgRect = svgElement.getBoundingClientRect();
// 根据针脚类型和方向计算相对位置
let pinX = svgRect.left + svgRect.width / 2;
let pinY = svgRect.top + svgRect.height / 2;
return { x: pinX, y: pinY };
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();
return {
x: rect.left + rect.width / 2,
y: rect.top + rect.height / 2
};
}
}
const rect = pinElements[0].getBoundingClientRect();
return {
x: rect.left + rect.width / 2,
y: rect.top + rect.height / 2
};
}
});
</script>

View File

@@ -14,16 +14,15 @@
<rect width="90" height="50" x="5" y="5" fill="#222" rx="3" ry="3" />
<!-- LED 发光部分 -->
<rect
<rect
width="70"
height="30"
x="15"
y="15"
:fill="ledColor"
:style="{ opacity: isOn ? brightness/100 : 0.2 }"
:style="{ opacity: isOn ? 1 : 0.2 }"
rx="15"
ry="15"
@click="toggleLed"
class="interactive"
/>
@@ -35,46 +34,64 @@
x="12"
y="12"
:fill="ledColor"
:style="{ opacity: brightness/100 * 0.3 }"
:style="{ opacity: 0.3 }"
rx="18"
ry="18"
filter="blur(5px)"
class="glow"
/>
</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>
</template>
<script setup lang="ts">
import { ref, computed, watch } from 'vue';
import { ref, computed, watch, onMounted, onUnmounted } from 'vue';
import { getConstraintState, onConstraintStateChange } from '../../stores/constraints';
import Pin from './Pin.vue';
// LED特有属性
interface Props {
size?: number;
color?: string;
initialOn?: boolean;
brightness?: number;
// --- 关键暴露getPinPosition代理到内部Pin ---
const pinRef = ref<any>(null);
// 从Pin组件继承属性
interface PinProps {
label?: string;
constraint?: string;
componentId?: string; // 添加componentId属性
// 这些属性被预设为固定值,但仍然包含在类型中以便完整继承
direction?: 'input' | 'output' | 'inout';
type?: 'digital' | 'analog';
}
// 组件属性定义
// LED特有属性
interface LEDProps {
size?: number;
color?: string;
}
// 组合两个接口
interface Props extends PinProps, LEDProps {}
const props = withDefaults(defineProps<Props>(), {
size: 1,
color: 'red',
initialOn: false,
brightness: 80, // 亮度默认为80%
constraint: ''
constraint: '',
label: 'LED',
componentId: ''
});
// 计算实际宽高
const width = computed(() => 100 * props.size);
const height = computed(() => 60 * props.size);
// 内部状态
const isOn = ref(props.initialOn);
const brightness = ref(props.brightness);
const isOn = ref(false);
// LED 颜色映射表
const colorMap: Record<string, string> = {
'red': '#ff3333',
'green': '#33ff33',
@@ -85,70 +102,64 @@ const colorMap: Record<string, string> = {
'purple': '#9933ff'
};
// 计算实际LED颜色
const ledColor = computed(() => {
return colorMap[props.color.toLowerCase()] || props.color;
});
// 定义组件发出的事件
const emit = defineEmits([
'toggle',
'brightness-change',
'value-change'
]);
// 监听约束状态变化
let unsubscribe: (() => void) | null = null;
// 手动切换LED状态
function toggleLed() {
isOn.value = !isOn.value;
emit('toggle', isOn.value);
emit('value-change', {
isOn: isOn.value,
brightness: brightness.value
});
}
// 设置亮度
function setBrightness(value: number) {
// 限制亮度值在0-100范围内
brightness.value = Math.max(0, Math.min(100, value));
emit('brightness-change', brightness.value);
emit('value-change', {
isOn: isOn.value,
brightness: brightness.value
});
}
// 手动设置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;
onMounted(() => {
if (props.constraint) {
unsubscribe = onConstraintStateChange((constraint, level) => {
if (constraint === props.constraint) {
isOn.value = (level === 'high');
}
});
// 初始化LED状态
const currentState = getConstraintState(props.constraint);
isOn.value = (currentState === 'high');
}
});
watch(() => props.initialOn, (newVal) => {
isOn.value = newVal;
onUnmounted(() => {
if (unsubscribe) {
unsubscribe();
}
});
watch(() => props.constraint, (newConstraint) => {
if (unsubscribe) {
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');
}
});
// 向外暴露方法
defineExpose({
toggleLed,
setBrightness,
setLedState,
getInfo: () => ({
// LED特有属性
color: props.color,
isOn: isOn.value,
brightness: brightness.value,
constraint: props.constraint
})
constraint: props.constraint,
componentId: props.componentId,
direction: 'input',
type: 'digital'
}),
getPinPosition: (componentId: string) => {
if (pinRef.value && pinRef.value.getPinPosition) {
return pinRef.value.getPinPosition(componentId);
}
return null;
}
});
</script>

View File

@@ -21,7 +21,8 @@
</template>
<script setup lang="ts">
import { computed, defineEmits, reactive } from 'vue';
import { computed, defineEmits, reactive, watch, onMounted, onUnmounted } from 'vue';
import { getConstraintColor, getConstraintState, onConstraintStateChange } from '../../stores/constraints';
interface Props {
id: string;
@@ -53,7 +54,17 @@ const props = withDefaults(defineProps<Props>(), {
constraint: ''
});
const computedStroke = computed(() => props.isActive ? '#ff9800' : props.strokeColor);
// 响应约束状态变化的颜色
const constraintColor = computed(() => {
if (!props.constraint) return props.strokeColor;
return getConstraintColor(props.constraint);
});
// 计算实际使用的颜色isActive优先其次是constraint电平颜色最后是默认色
const computedStroke = computed(() => {
if (props.isActive) return '#ff9800';
return constraintColor.value || props.strokeColor;
});
const emit = defineEmits(['click', 'update:active', 'update:position']);
@@ -95,6 +106,44 @@ function calculateOrthogonalPath(startX: number, startY: number, endX: number, e
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: () => ({

View File

@@ -154,18 +154,6 @@ const componentConfigs: Record<string, ComponentConfig> = {
{ value: 'analog', label: 'analog' }
],
description: '引脚的模数特性,数字或模拟'
},
{
name: 'appearance',
type: 'select',
label: '引脚样式',
default: 'Dip',
options: [
{ value: 'None', label: 'None' },
{ value: 'Dip', label: 'Dip' },
{ value: 'SMT', label: 'SMT' }
],
description: '引脚的外观样式,不影响功能'
}
]
},
@@ -313,9 +301,9 @@ const componentConfigs: Record<string, ComponentConfig> = {
{
name: 'constraint',
type: 'string',
label: '连接约束',
label: '引脚约束',
default: '',
description: '相同约束字符串的组件将被视为有电气连接'
description: '相同约束字符串的引脚将被视为有电气连接'
}
]
},
@@ -341,7 +329,7 @@ const componentConfigs: Record<string, ComponentConfig> = {
default: '#4a5568',
description: '线条颜色使用CSS颜色值'
},
{
{
name: 'strokeWidth',
type: 'number',
label: '线条宽度',