Commit af0b8944 authored by jizhou's avatar jizhou

feat: UI增强 - 重命名为SimuFlow, 可调节侧边栏, 端口位置优化, 内置符号编辑功能

- 应用重命名: EPLAN Visualizer → SimuFlow (Toolbar + index.html)
- 侧边栏: 新增拖拽调节宽度功能, 匹配ComponentEditor样式
- 导入tab文字改为 EPLAN导入
- 所有内置符号端口从顶部迁移至左/右/底部, 避免与header重叠
- Switch节点端口: False→FalseVal, True→TrueVal
- 内置符号编辑: 新增编辑开关, 开启后显示编辑/删除按钮
- 编辑内置符号时加载完整数据到符号编辑器
- 修复内置符号保存写回DEVICE_CATEGORIES而非自定义库
- 修复getDeviceType读取实时数据避免缓存不一致
- 新增runtime-setup skill (nvm/端口转发)
parent 10cbf803
...@@ -72,6 +72,7 @@ npm run build ...@@ -72,6 +72,7 @@ npm run build
| `modelica-export` | .mo 导出、模型映射(非因果建模) | | `modelica-export` | .mo 导出、模型映射(非因果建模) |
| `data-import` | xlsx 导入、JSON 导入导出、自动布局 | | `data-import` | xlsx 导入、JSON 导入导出、自动布局 |
| `ui-styling` | 暗色主题、CSS Modules、工具栏/侧栏/属性面板 | | `ui-styling` | 暗色主题、CSS Modules、工具栏/侧栏/属性面板 |
| `runtime-setup` | nvm 环境搭建、项目启动、端口转发 |
## 建模理念 ## 建模理念
......
---
name: runtime-setup
description: 运行环境搭建与项目启动 - 使用 nvm 管理 Node.js/npm,Linux 下端口转发供 Windows 访问
---
# 运行环境搭建与项目启动
> 当用户提到「运行」「启动」「配置运行环境」「搭建环境」等关键字时触发本 Skill。
## 1. Node.js 环境(通过 nvm 管理)
### 1.1 检查 nvm 是否已安装
```bash
bash -c 'export NVM_DIR="$HOME/.nvm"; [ -s "$NVM_DIR/nvm.sh" ] && . "$NVM_DIR/nvm.sh"; nvm --version 2>&1 || echo "NVM_NOT_FOUND"'
```
- 若输出 `NVM_NOT_FOUND`,执行步骤 1.2 安装 nvm。
- 若输出版本号,跳到步骤 1.3。
### 1.2 安装 nvm
```bash
curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.40.1/install.sh | bash
```
安装完成后**不需要**重启终端,后续命令统一使用以下前缀加载 nvm:
```bash
export NVM_DIR="$HOME/.nvm"; [ -s "$NVM_DIR/nvm.sh" ] && . "$NVM_DIR/nvm.sh";
```
### 1.3 安装 Node.js LTS
```bash
bash -c 'export NVM_DIR="$HOME/.nvm"; [ -s "$NVM_DIR/nvm.sh" ] && . "$NVM_DIR/nvm.sh"; nvm install --lts'
```
### 1.4 验证安装
```bash
bash -c 'export NVM_DIR="$HOME/.nvm"; [ -s "$NVM_DIR/nvm.sh" ] && . "$NVM_DIR/nvm.sh"; node -v && npm -v'
```
## 2. 安装项目依赖
```bash
bash -c 'export NVM_DIR="$HOME/.nvm"; [ -s "$NVM_DIR/nvm.sh" ] && . "$NVM_DIR/nvm.sh"; npm install'
```
- 工作目录:项目根目录(含 `package.json` 处)
- 忽略 `npm fund` / `npm audit` 提示,不影响运行
## 3. 启动开发服务器
### 3.1 启动命令(监听所有网络接口)
```bash
bash -c 'export NVM_DIR="$HOME/.nvm"; [ -s "$NVM_DIR/nvm.sh" ] && . "$NVM_DIR/nvm.sh"; npx vite --host 0.0.0.0'
```
> **关键**:必须使用 `--host 0.0.0.0`,否则 Vite 默认只监听 `localhost`,从 Windows 无法访问。
### 3.2 确认启动成功
输出中应包含类似:
```
VITE v7.x.x ready in xxx ms
➜ Local: http://localhost:5173/
➜ Network: http://<linux-ip>:5173/
```
记录 `Network` 地址,告知用户在 Windows 浏览器中打开该地址。
## 4. Linux → Windows 端口转发(跨机器访问)
### 4.1 直接访问(同一局域网)
如果 Windows 和 Linux 在同一局域网,用户可直接在 Windows 浏览器打开:
```
http://<linux-ip>:5173/
```
### 4.2 防火墙放行(如果无法访问)
```bash
sudo ufw allow 5173
```
### 4.3 SSH 端口转发(备选方案)
如果用户通过 SSH 连接 Linux,可在 Windows 端执行:
```powershell
ssh -L 5173:localhost:5173 <username>@<linux-ip>
```
然后在 Windows 浏览器打开 `http://localhost:5173/`
## 5. 注意事项
- **禁止使用 `sudo apt install npm`**:本项目统一使用 nvm 管理 Node.js 和 npm,避免系统级安装导致版本冲突。
- **nvm 命令前缀**:由于 nvm 是 shell 函数而非二进制文件,每条命令都需要先 source nvm.sh,统一使用 `bash -c 'export NVM_DIR="$HOME/.nvm"; [ -s "$NVM_DIR/nvm.sh" ] && . "$NVM_DIR/nvm.sh"; <命令>'` 格式。
- **默认端口**:Vite 默认使用 5173 端口,如被占用会自动递增(5174、5175…),以实际输出为准。
- **热更新**:开发服务器启动后,修改代码会自动热更新,无需重启。
...@@ -40,7 +40,7 @@ export const DEVICE_CATEGORIES = [ ...@@ -40,7 +40,7 @@ export const DEVICE_CATEGORIES = [
### 新增内置符号必须遵守 ### 新增内置符号必须遵守
1. **code 必须全局唯一**(按分类区间分配:1000+ 基本电子, 100~999 电气控制, 2000+ PLC, 3000+ 水利, 4000+ 气动) 1. **code 必须全局唯一**(按分类区间分配:5000+ 流程, 1000+ 基本电子, 100~999 电气控制, 2000+ PLC, 3000+ 水利, 4000+ 气动)
2. **type 必须为英文小写下划线**,与 `modelMapping.js` 的 MODEL_MAP key 一致 2. **type 必须为英文小写下划线**,与 `modelMapping.js` 的 MODEL_MAP key 一致
3. **每个端口必须有 connector 字段** 3. **每个端口必须有 connector 字段**
...@@ -67,6 +67,7 @@ export const DEVICE_CATEGORIES = [ ...@@ -67,6 +67,7 @@ export const DEVICE_CATEGORIES = [
| PLC 通道 | di1, do1, ai1, ao1 | PLC 各模块 | | PLC 通道 | di1, do1, ai1, ao1 | PLC 各模块 |
| 流体端口 | inlet, outlet, waterIn | 阀门、水泵 | | 流体端口 | inlet, outlet, waterIn | 阀门、水泵 |
| 电源引脚 | vcc, gnd, pe | 传感器、电机 | | 电源引脚 | vcc, gnd, pe | 传感器、电机 |
| 流程节点信号 | a, b, out, inVal, cond | 逻辑/数值节点 |
| 自定义符号默认 | p1, p2, p3... | addPort 自动生成 | | 自定义符号默认 | p1, p2, p3... | addPort 自动生成 |
**禁止使用中文作为 connector 值。** **禁止使用中文作为 connector 值。**
...@@ -75,9 +76,10 @@ export const DEVICE_CATEGORIES = [ ...@@ -75,9 +76,10 @@ export const DEVICE_CATEGORIES = [
```javascript ```javascript
export const PORT_TYPES = { export const PORT_TYPES = {
power: { label: '电力', color: '#f44336' }, power: { label: '电力', color: '#FF9800' },
digital: { label: '数字量', color: '#2196F3' }, digital: { label: '数字量', color: '#2196F3' },
analog: { label: '模拟量', color: '#4CAF50' }, analog: { label: '模拟量', color: '#4CAF50' },
signal: { label: '信号', color: '#AB47BC' },
generic: { label: '通用', color: '#9E9E9E' }, generic: { label: '通用', color: '#9E9E9E' },
water: { label: '水流', color: '#00BCD4' }, water: { label: '水流', color: '#00BCD4' },
air: { label: '气流', color: '#8BC34A' }, air: { label: '气流', color: '#8BC34A' },
...@@ -130,6 +132,7 @@ export const PORT_TYPES = { ...@@ -130,6 +132,7 @@ export const PORT_TYPES = {
| 分类 | 数量 | 符号 | | 分类 | 数量 | 符号 |
|------|------|------| |------|------|------|
| 流程 | 11 | greater_than, less_than, equal, logic_and, logic_or, logic_not, logic_switch, val_integer, val_real, val_string, val_boolean |
| 基本电子元件 | 6 | resistor, capacitor, inductor, diode, voltage_source, ground | | 基本电子元件 | 6 | resistor, capacitor, inductor, diode, voltage_source, ground |
| 电气控制 | 9 | terminal, contactor, relay, breaker, switch, motor, transformer, sensor, cable | | 电气控制 | 9 | terminal, contactor, relay, breaker, switch, motor, transformer, sensor, cable |
| PLC | 5 | plc_cpu, plc_di, plc_do, plc_ai, plc_ao | | PLC | 5 | plc_cpu, plc_di, plc_do, plc_ai, plc_ao |
......
...@@ -4,7 +4,7 @@ ...@@ -4,7 +4,7 @@
<meta charset="UTF-8" /> <meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/vite.svg" /> <link rel="icon" type="image/svg+xml" href="/vite.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>EPLAN Visualizer</title> <title>SimuFlow</title>
</head> </head>
<body> <body>
<div id="root"></div> <div id="root"></div>
......
...@@ -128,8 +128,8 @@ function CustomDeviceNode({ data, selected }) { ...@@ -128,8 +128,8 @@ function CustomDeviceNode({ data, selected }) {
handlePos = { top: `${((HEADER_H + tH * port.position) / totalH) * 100}%` }; handlePos = { top: `${((HEADER_H + tH * port.position) / totalH) * 100}%` };
break; break;
case 'top': case 'top':
// CSS 管 top:0 + transform 居中,我们只设 left // 偏移到 header 底部边缘,避免与 header 文字/图标重叠
handlePos = { left: `${port.position * 100}%` }; handlePos = { left: `${port.position * 100}%`, top: `${(HEADER_H / totalH) * 100}%` };
break; break;
case 'bottom': case 'bottom':
// CSS 管 bottom:0 + transform 居中,我们只设 left // CSS 管 bottom:0 + transform 居中,我们只设 left
......
...@@ -27,6 +27,9 @@ export default function Sidebar() { ...@@ -27,6 +27,9 @@ export default function Sidebar() {
return init; return init;
}); });
const [sidebarWidth, setSidebarWidth] = useState(280);
const isDragging = useRef(false);
const [builtinEditMode, setBuiltinEditMode] = useState(false);
const connInputRef = useRef(null); const connInputRef = useRef(null);
const { const {
...@@ -37,7 +40,7 @@ export default function Sidebar() { ...@@ -37,7 +40,7 @@ export default function Sidebar() {
edges, edges,
} = useFlowStore(); } = useFlowStore();
const { templates, customCategories, startEditing, startNew, deleteTemplate, addCategory, deleteCategory, switchView } = useComponentLibrary(); const { templates, deleteTemplate, startEditing, startEditingBuiltin, switchView, startNew, customCategories, addCategory, deleteCategory } = useComponentLibrary();
// ==================== 文件导入 ==================== // ==================== 文件导入 ====================
const handleImport = useCallback(async () => { const handleImport = useCallback(async () => {
...@@ -71,6 +74,12 @@ export default function Sidebar() { ...@@ -71,6 +74,12 @@ export default function Sidebar() {
switchView('editor'); switchView('editor');
}, [startEditing, switchView]); }, [startEditing, switchView]);
/** 编辑内置符号 */
const handleEditBuiltin = useCallback((item) => {
startEditingBuiltin(item);
switchView('editor');
}, [startEditingBuiltin, switchView]);
/** 新建自定义符号 */ /** 新建自定义符号 */
const handleNewTemplate = useCallback(() => { const handleNewTemplate = useCallback(() => {
startNew(); startNew();
...@@ -82,8 +91,31 @@ export default function Sidebar() { ...@@ -82,8 +91,31 @@ export default function Sidebar() {
setCollapsedCats(prev => ({ ...prev, [catName]: !prev[catName] })); setCollapsedCats(prev => ({ ...prev, [catName]: !prev[catName] }));
}; };
/** 拖拽调整宽度 */
const startResize = useCallback((e) => {
e.preventDefault();
isDragging.current = true;
const startX = e.clientX;
const startWidth = sidebarWidth;
function onMove(ev) {
const delta = ev.clientX - startX;
const newWidth = Math.max(200, Math.min(500, startWidth + delta));
setSidebarWidth(newWidth);
}
function onUp() {
isDragging.current = false;
window.removeEventListener('mousemove', onMove);
window.removeEventListener('mouseup', onUp);
}
window.addEventListener('mousemove', onMove);
window.addEventListener('mouseup', onUp);
}, [sidebarWidth]);
return ( return (
<div className={styles.sidebar}> <div className={styles.sidebar} style={{ width: sidebarWidth, position: 'relative' }}>
{/* 右侧拖拽手柄 */}
<div className={styles.resizeHandle} onMouseDown={startResize} />
{/* Tab 切换 */} {/* Tab 切换 */}
<div className={styles.tabs}> <div className={styles.tabs}>
<button <button
...@@ -102,7 +134,7 @@ export default function Sidebar() { ...@@ -102,7 +134,7 @@ export default function Sidebar() {
className={`${styles.tab} ${activeTab === 'import' ? styles.activeTab : ''}`} className={`${styles.tab} ${activeTab === 'import' ? styles.activeTab : ''}`}
onClick={() => setActiveTab('import')} onClick={() => setActiveTab('import')}
> >
导入 EPLAN导入
</button> </button>
</div> </div>
...@@ -151,7 +183,10 @@ export default function Sidebar() { ...@@ -151,7 +183,10 @@ export default function Sidebar() {
{/* ==================== 模型映射面板 ==================== */} {/* ==================== 模型映射面板 ==================== */}
{activeTab === 'mapping' && (() => { {activeTab === 'mapping' && (() => {
const allMappings = getAllMappings(); const allMappings = getAllMappings();
const categories = DEVICE_CATEGORIES; // 过滤掉流程节点分类(流程节点无需模型映射,导出时直接生成 Modelica 内部语句)
const categories = DEVICE_CATEGORIES.filter(cat =>
!cat.items.every(item => allMappings[item.type]?.isFlowNode)
);
const saveMappingOverride = (type, field, value) => { const saveMappingOverride = (type, field, value) => {
setMappingOverrides(prev => { setMappingOverrides(prev => {
const next = { ...prev, [type]: { ...(prev[type] || {}), [field]: value } }; const next = { ...prev, [type]: { ...(prev[type] || {}), [field]: value } };
...@@ -163,7 +198,7 @@ export default function Sidebar() { ...@@ -163,7 +198,7 @@ export default function Sidebar() {
<div className={styles.panel}> <div className={styles.panel}>
<div className={styles.sectionTitle}>模型映射配置</div> <div className={styles.sectionTitle}>模型映射配置</div>
<div style={{ fontSize: 10, color: '#666', padding: '0 0 8px 0' }}> <div style={{ fontSize: 10, color: '#666', padding: '0 0 8px 0' }}>
符号类型 → Modelica 模型名称。修改后导出 .mo 时自动生效。 符号类型 → Modelica 模型名称。修改后导出 .mo 时自动生效。流程节点无需映射。
</div> </div>
{categories.map(cat => ( {categories.map(cat => (
<div key={cat.category} style={{ marginBottom: 12 }}> <div key={cat.category} style={{ marginBottom: 12 }}>
...@@ -263,6 +298,14 @@ export default function Sidebar() { ...@@ -263,6 +298,14 @@ export default function Sidebar() {
{collapsedCats['__builtin__'] ? '\u25B6' : '\u25BC'} {collapsedCats['__builtin__'] ? '\u25B6' : '\u25BC'}
</span> </span>
<span className={styles.categoryName}>内置符号</span> <span className={styles.categoryName}>内置符号</span>
<button
className={styles.newTemplateBtn}
onClick={(e) => { e.stopPropagation(); setBuiltinEditMode(prev => !prev); }}
style={builtinEditMode ? { background: '#6366f1', color: '#fff', borderColor: '#6366f1' } : {}}
title={builtinEditMode ? '关闭编辑模式' : '开启编辑模式'}
>
&#9998;
</button>
</div> </div>
{!collapsedCats['__builtin__'] && ( {!collapsedCats['__builtin__'] && (
<div className={styles.subTree}> <div className={styles.subTree}>
...@@ -293,7 +336,41 @@ export default function Sidebar() { ...@@ -293,7 +336,41 @@ export default function Sidebar() {
> >
<span className={styles.libIcon}>{item.icon}</span> <span className={styles.libIcon}>{item.icon}</span>
<span className={styles.libLabel}>{item.label}</span> <span className={styles.libLabel}>{item.label}</span>
<span className={styles.libPortCount}>{item.ports.length}P</span> {builtinEditMode ? (
<span className={styles.customActions}>
<button
className={styles.editBtn}
onClick={(e) => {
e.stopPropagation();
// 使用符号编辑器打开内置符号进行编辑
handleEditBuiltin(item);
}}
title="编辑"
>
&#9998;
</button>
<button
className={styles.deleteBtn}
onClick={(e) => {
e.stopPropagation();
if (window.confirm(`确认删除内置符号"${item.label}"?`)) {
// 从 DEVICE_CATEGORIES 中删除此项
const catObj = DEVICE_CATEGORIES.find(c => c.category === cat.category);
if (catObj) {
catObj.items = catObj.items.filter(i => i.code !== item.code);
// 触发重新渲染
setCollapsedCats(prev => ({ ...prev }));
}
}
}}
title="删除"
>
&#10005;
</button>
</span>
) : (
<span className={styles.libPortCount}>{item.ports.length}P</span>
)}
</div> </div>
))} ))}
</div> </div>
......
.sidebar { .sidebar {
width: 280px;
background: #16161e; background: #16161e;
border-right: 1px solid #2a2a3a; border-right: 1px solid #2a2a3a;
display: flex; display: flex;
...@@ -8,6 +7,41 @@ ...@@ -8,6 +7,41 @@
overflow: hidden; overflow: hidden;
} }
/* 右侧拖拽调整宽度手柄 */
.resizeHandle {
position: absolute;
right: 0;
top: 0;
bottom: 0;
width: 5px;
cursor: col-resize;
background: #2a2a3a;
flex-shrink: 0;
transition: background 0.15s;
z-index: 10;
}
.resizeHandle:hover,
.resizeHandle:active {
background: #6366f1;
}
.resizeHandle::after {
content: '';
position: absolute;
top: 50%;
left: 1px;
width: 3px;
height: 30px;
transform: translateY(-50%);
border-radius: 2px;
background: rgba(255,255,255,0.15);
}
.resizeHandle:hover::after {
background: rgba(255,255,255,0.4);
}
/* ===== Tabs ===== */ /* ===== Tabs ===== */
.tabs { .tabs {
display: flex; display: flex;
......
...@@ -91,7 +91,7 @@ export default function Toolbar() { ...@@ -91,7 +91,7 @@ export default function Toolbar() {
<div className={styles.toolbar}> <div className={styles.toolbar}>
<div className={styles.brand}> <div className={styles.brand}>
<span className={styles.logo}>&#9883;</span> <span className={styles.logo}>&#9883;</span>
<span className={styles.title}>EPLAN Visualizer</span> <span className={styles.title}>SimuFlow</span>
</div> </div>
<div className={styles.divider} /> <div className={styles.divider} />
......
...@@ -8,6 +8,7 @@ ...@@ -8,6 +8,7 @@
* ports: [{ id, name, type, side, position }] * ports: [{ id, name, type, side, position }]
*/ */
import { create } from 'zustand'; import { create } from 'zustand';
import { DEVICE_CATEGORIES } from '../utils/constants';
const STORAGE_KEY = 'eplan-component-library'; const STORAGE_KEY = 'eplan-component-library';
const CATEGORIES_KEY = 'eplan-custom-categories'; const CATEGORIES_KEY = 'eplan-custom-categories';
...@@ -110,6 +111,34 @@ const useComponentLibrary = create((set, get) => ({ ...@@ -110,6 +111,34 @@ const useComponentLibrary = create((set, get) => ({
} }
}, },
/** 开始编辑内置符号(将 DEVICE_CATEGORIES item 转为编辑器模板格式) */
startEditingBuiltin: (builtinItem) => {
const template = {
id: `builtin-${builtinItem.type}`,
name: builtinItem.label,
category: '内置',
color: builtinItem.color,
icon: builtinItem.icon,
width: builtinItem.width,
height: builtinItem.height,
shapes: [],
params: builtinItem.params ? builtinItem.params.map(p => ({ ...p })) : [],
ports: builtinItem.ports.map((p, i) => ({
id: p.id,
portId: i + 1,
name: p.name,
description: '',
type: p.type,
side: p.side,
position: p.position,
connector: p.connector,
})),
_builtinType: builtinItem.type,
_builtinCode: builtinItem.code,
};
set({ editingTemplate: JSON.parse(JSON.stringify(template)), editingHistory: [] });
},
/** 开始创建新模板 */ /** 开始创建新模板 */
startNew: () => { startNew: () => {
set({ editingTemplate: createBlankTemplate(), editingHistory: [] }); set({ editingTemplate: createBlankTemplate(), editingHistory: [] });
...@@ -213,6 +242,34 @@ const useComponentLibrary = create((set, get) => ({ ...@@ -213,6 +242,34 @@ const useComponentLibrary = create((set, get) => ({
const current = get().editingTemplate; const current = get().editingTemplate;
if (!current) return; if (!current) return;
// 内置符号:写回 DEVICE_CATEGORIES(运行时内存)
if (current._builtinCode != null) {
for (const cat of DEVICE_CATEGORIES) {
const idx = cat.items.findIndex(i => i.code === current._builtinCode);
if (idx >= 0) {
const item = cat.items[idx];
item.label = current.name;
item.color = current.color;
item.icon = current.icon;
item.width = current.width;
item.height = current.height;
item.ports = current.ports.map(p => ({
id: p.id,
name: p.name,
side: p.side,
position: p.position,
type: p.type,
connector: p.connector,
}));
item.params = current.params ? current.params.map(p => ({ ...p })) : [];
break;
}
}
set({ editingTemplate: null });
return;
}
// 自定义符号:保存到 localStorage
const templates = [...get().templates]; const templates = [...get().templates];
const existingIdx = templates.findIndex(t => t.id === current.id); const existingIdx = templates.findIndex(t => t.id === current.id);
if (existingIdx >= 0) { if (existingIdx >= 0) {
......
...@@ -10,6 +10,114 @@ ...@@ -10,6 +10,114 @@
/** 分类符号定义 */ /** 分类符号定义 */
export const DEVICE_CATEGORIES = [ export const DEVICE_CATEGORIES = [
{
category: '流程',
items: [
{
code: 5001, type: 'greater_than', label: '大于', color: '#7E57C2', icon: '>',
width: 120, height: 60,
ports: [
{ id: 'p-a', name: 'A', side: 'left', position: 0.3, type: 'signal', connector: 'a' },
{ id: 'p-b', name: 'B', side: 'left', position: 0.7, type: 'signal', connector: 'b' },
{ id: 'p-out', name: 'out', side: 'right', position: 0.5, type: 'signal', connector: 'out' },
],
},
{
code: 5002, type: 'less_than', label: '小于', color: '#7E57C2', icon: '<',
width: 120, height: 60,
ports: [
{ id: 'p-a', name: 'A', side: 'left', position: 0.3, type: 'signal', connector: 'a' },
{ id: 'p-b', name: 'B', side: 'left', position: 0.7, type: 'signal', connector: 'b' },
{ id: 'p-out', name: 'out', side: 'right', position: 0.5, type: 'signal', connector: 'out' },
],
},
{
code: 5003, type: 'equal', label: '等于', color: '#7E57C2', icon: '=',
width: 120, height: 60,
ports: [
{ id: 'p-a', name: 'A', side: 'left', position: 0.3, type: 'signal', connector: 'a' },
{ id: 'p-b', name: 'B', side: 'left', position: 0.7, type: 'signal', connector: 'b' },
{ id: 'p-out', name: 'out', side: 'right', position: 0.5, type: 'signal', connector: 'out' },
],
},
{
code: 5004, type: 'logic_and', label: '与', color: '#7E57C2', icon: '&',
width: 120, height: 60,
ports: [
{ id: 'p-a', name: 'A', side: 'left', position: 0.3, type: 'signal', connector: 'a' },
{ id: 'p-b', name: 'B', side: 'left', position: 0.7, type: 'signal', connector: 'b' },
{ id: 'p-out', name: 'out', side: 'right', position: 0.5, type: 'signal', connector: 'out' },
],
},
{
code: 5005, type: 'logic_or', label: '或', color: '#7E57C2', icon: '|',
width: 120, height: 60,
ports: [
{ id: 'p-a', name: 'A', side: 'left', position: 0.3, type: 'signal', connector: 'a' },
{ id: 'p-b', name: 'B', side: 'left', position: 0.7, type: 'signal', connector: 'b' },
{ id: 'p-out', name: 'out', side: 'right', position: 0.5, type: 'signal', connector: 'out' },
],
},
{
code: 5006, type: 'logic_not', label: '非', color: '#7E57C2', icon: '!',
width: 120, height: 60,
ports: [
{ id: 'p-in', name: 'in', side: 'left', position: 0.5, type: 'signal', connector: 'inVal' },
{ id: 'p-out', name: 'out', side: 'right', position: 0.5, type: 'signal', connector: 'out' },
],
},
{
code: 5007, type: 'logic_switch', label: 'Switch', color: '#7E57C2', icon: '⇋',
width: 140, height: 100,
ports: [
{ id: 'p-switch', name: 'Switch', side: 'left', position: 0.2, type: 'signal', connector: 'switchCtrl' },
{ id: 'p-false', name: 'FalseVal', side: 'left', position: 0.5, type: 'signal', connector: 'falseVal' },
{ id: 'p-true', name: 'TrueVal', side: 'left', position: 0.8, type: 'signal', connector: 'trueVal' },
{ id: 'p-out', name: 'Output', side: 'right', position: 0.5, type: 'signal', connector: 'out' },
],
},
{
code: 5008, type: 'val_integer', label: '整型', color: '#26A69A', icon: 'Z',
width: 120, height: 60,
ports: [
{ id: 'p-out', name: 'out', side: 'right', position: 0.5, type: 'signal', connector: 'out' },
],
params: [
{ key: 'value', label: '值', unit: '', defaultValue: '0' },
],
},
{
code: 5009, type: 'val_real', label: '浮点型', color: '#26A69A', icon: 'R',
width: 120, height: 60,
ports: [
{ id: 'p-out', name: 'out', side: 'right', position: 0.5, type: 'signal', connector: 'out' },
],
params: [
{ key: 'value', label: '值', unit: '', defaultValue: '0.0' },
],
},
{
code: 5010, type: 'val_string', label: '字符串型', color: '#26A69A', icon: 'S',
width: 120, height: 60,
ports: [
{ id: 'p-out', name: 'out', side: 'right', position: 0.5, type: 'signal', connector: 'out' },
],
params: [
{ key: 'value', label: '值', unit: '', defaultValue: '' },
],
},
{
code: 5011, type: 'val_boolean', label: '布尔型', color: '#26A69A', icon: 'B',
width: 120, height: 60,
ports: [
{ id: 'p-out', name: 'out', side: 'right', position: 0.5, type: 'signal', connector: 'out' },
],
params: [
{ key: 'value', label: '值', unit: '', defaultValue: 'true' },
],
},
],
},
{ {
category: '基本电子元件', category: '基本电子元件',
items: [ items: [
...@@ -71,7 +179,7 @@ export const DEVICE_CATEGORIES = [ ...@@ -71,7 +179,7 @@ export const DEVICE_CATEGORIES = [
code: 1006, type: 'ground', label: '接地', color: '#607D8B', icon: 'GND', code: 1006, type: 'ground', label: '接地', color: '#607D8B', icon: 'GND',
width: 80, height: 60, width: 80, height: 60,
ports: [ ports: [
{ id: 'p-gnd', name: 'GND', side: 'top', position: 0.5, type: 'power', connector: 'p' }, { id: 'p-gnd', name: 'GND', side: 'left', position: 0.5, type: 'power', connector: 'p' },
], ],
}, },
], ],
...@@ -89,16 +197,16 @@ export const DEVICE_CATEGORIES = [ ...@@ -89,16 +197,16 @@ export const DEVICE_CATEGORIES = [
}, },
{ {
code: 200, type: 'contactor', label: '接触器', color: '#2196F3', icon: '◈', code: 200, type: 'contactor', label: '接触器', color: '#2196F3', icon: '◈',
width: 180, height: 100, width: 180, height: 120,
ports: [ ports: [
{ id: 'p-l1', name: 'L1', side: 'top', position: 0.25, type: 'power', connector: 'l1' }, { id: 'p-l1', name: 'L1', side: 'left', position: 0.2, type: 'power', connector: 'l1' },
{ id: 'p-l2', name: 'L2', side: 'top', position: 0.5, type: 'power', connector: 'l2' }, { id: 'p-l2', name: 'L2', side: 'left', position: 0.5, type: 'power', connector: 'l2' },
{ id: 'p-l3', name: 'L3', side: 'top', position: 0.75, type: 'power', connector: 'l3' }, { id: 'p-l3', name: 'L3', side: 'left', position: 0.8, type: 'power', connector: 'l3' },
{ id: 'p-t1', name: 'T1', side: 'bottom', position: 0.25, type: 'power', connector: 't1' }, { id: 'p-t1', name: 'T1', side: 'right', position: 0.2, type: 'power', connector: 't1' },
{ id: 'p-t2', name: 'T2', side: 'bottom', position: 0.5, type: 'power', connector: 't2' }, { id: 'p-t2', name: 'T2', side: 'right', position: 0.5, type: 'power', connector: 't2' },
{ id: 'p-t3', name: 'T3', side: 'bottom', position: 0.75, type: 'power', connector: 't3' }, { id: 'p-t3', name: 'T3', side: 'right', position: 0.8, type: 'power', connector: 't3' },
{ id: 'p-a1', name: 'A1', side: 'left', position: 0.5, type: 'digital', connector: 'a1' }, { id: 'p-a1', name: 'A1', side: 'bottom', position: 0.3, type: 'digital', connector: 'a1' },
{ id: 'p-a2', name: 'A2', side: 'right', position: 0.5, type: 'digital', connector: 'a2' }, { id: 'p-a2', name: 'A2', side: 'bottom', position: 0.7, type: 'digital', connector: 'a2' },
], ],
}, },
{ {
...@@ -115,8 +223,8 @@ export const DEVICE_CATEGORIES = [ ...@@ -115,8 +223,8 @@ export const DEVICE_CATEGORIES = [
code: 501, type: 'breaker', label: '断路器', color: '#F44336', icon: '⊗', code: 501, type: 'breaker', label: '断路器', color: '#F44336', icon: '⊗',
width: 140, height: 80, width: 140, height: 80,
ports: [ ports: [
{ id: 'p-in', name: '进线', side: 'top', position: 0.5, type: 'power', connector: 'pIn' }, { id: 'p-in', name: '进线', side: 'left', position: 0.5, type: 'power', connector: 'pIn' },
{ id: 'p-out', name: '出线', side: 'bottom', position: 0.5, type: 'power', connector: 'pOut' }, { id: 'p-out', name: '出线', side: 'right', position: 0.5, type: 'power', connector: 'pOut' },
], ],
params: [ params: [
{ key: 'In', label: '额定电流', unit: 'A', defaultValue: '16' }, { key: 'In', label: '额定电流', unit: 'A', defaultValue: '16' },
...@@ -135,10 +243,10 @@ export const DEVICE_CATEGORIES = [ ...@@ -135,10 +243,10 @@ export const DEVICE_CATEGORIES = [
code: 400, type: 'motor', label: '电动机', color: '#FF9800', icon: '◎', code: 400, type: 'motor', label: '电动机', color: '#FF9800', icon: '◎',
width: 160, height: 100, width: 160, height: 100,
ports: [ ports: [
{ id: 'p-u', name: 'U', side: 'top', position: 0.25, type: 'power', connector: 'u' }, { id: 'p-u', name: 'U', side: 'left', position: 0.2, type: 'power', connector: 'u' },
{ id: 'p-v', name: 'V', side: 'top', position: 0.5, type: 'power', connector: 'v' }, { id: 'p-v', name: 'V', side: 'left', position: 0.5, type: 'power', connector: 'v' },
{ id: 'p-w', name: 'W', side: 'top', position: 0.75, type: 'power', connector: 'w' }, { id: 'p-w', name: 'W', side: 'left', position: 0.8, type: 'power', connector: 'w' },
{ id: 'p-pe',name: 'PE', side: 'bottom', position: 0.5, type: 'power', connector: 'pe' }, { id: 'p-pe',name: 'PE', side: 'bottom', position: 0.5, type: 'power', connector: 'pe' },
], ],
params: [ params: [
{ key: 'Pw', label: '功率', unit: 'kW', defaultValue: '2.2' }, { key: 'Pw', label: '功率', unit: 'kW', defaultValue: '2.2' },
...@@ -159,8 +267,8 @@ export const DEVICE_CATEGORIES = [ ...@@ -159,8 +267,8 @@ export const DEVICE_CATEGORIES = [
code: 700, type: 'sensor', label: '传感器', color: '#795548', icon: '◉', code: 700, type: 'sensor', label: '传感器', color: '#795548', icon: '◉',
width: 140, height: 80, width: 140, height: 80,
ports: [ ports: [
{ id: 'p-vcc', name: 'VCC', side: 'top', position: 0.5, type: 'power', connector: 'vcc' }, { id: 'p-vcc', name: 'VCC', side: 'left', position: 0.3, type: 'power', connector: 'vcc' },
{ id: 'p-gnd', name: 'GND', side: 'bottom',position: 0.5, type: 'power', connector: 'gnd' }, { id: 'p-gnd', name: 'GND', side: 'left', position: 0.7, type: 'power', connector: 'gnd' },
{ id: 'p-sig', name: 'SIG', side: 'right', position: 0.5, type: 'analog', connector: 'sig' }, { id: 'p-sig', name: 'SIG', side: 'right', position: 0.5, type: 'analog', connector: 'sig' },
], ],
}, },
...@@ -179,16 +287,16 @@ export const DEVICE_CATEGORIES = [ ...@@ -179,16 +287,16 @@ export const DEVICE_CATEGORIES = [
items: [ items: [
{ {
code: 2001, type: 'plc_cpu', label: 'PLC CPU', color: '#3F51B5', icon: 'CPU', code: 2001, type: 'plc_cpu', label: 'PLC CPU', color: '#3F51B5', icon: 'CPU',
width: 200, height: 120, width: 200, height: 140,
ports: [ ports: [
{ id: 'p-24v', name: '24V', side: 'top', position: 0.25, type: 'power', connector: 'v24' }, { id: 'p-24v', name: '24V', side: 'bottom', position: 0.3, type: 'power', connector: 'v24' },
{ id: 'p-0v', name: '0V', side: 'top', position: 0.75, type: 'power', connector: 'v0' }, { id: 'p-0v', name: '0V', side: 'bottom', position: 0.7, type: 'power', connector: 'v0' },
{ id: 'p-di1', name: 'DI1', side: 'left', position: 0.25, type: 'digital', connector: 'di1' }, { id: 'p-di1', name: 'DI1', side: 'left', position: 0.2, type: 'digital', connector: 'di1' },
{ id: 'p-di2', name: 'DI2', side: 'left', position: 0.5, type: 'digital', connector: 'di2' }, { id: 'p-di2', name: 'DI2', side: 'left', position: 0.5, type: 'digital', connector: 'di2' },
{ id: 'p-di3', name: 'DI3', side: 'left', position: 0.75, type: 'digital', connector: 'di3' }, { id: 'p-di3', name: 'DI3', side: 'left', position: 0.8, type: 'digital', connector: 'di3' },
{ id: 'p-do1', name: 'DO1', side: 'right', position: 0.25, type: 'digital', connector: 'do1' }, { id: 'p-do1', name: 'DO1', side: 'right', position: 0.2, type: 'digital', connector: 'do1' },
{ id: 'p-do2', name: 'DO2', side: 'right', position: 0.5, type: 'digital', connector: 'do2' }, { id: 'p-do2', name: 'DO2', side: 'right', position: 0.5, type: 'digital', connector: 'do2' },
{ id: 'p-do3', name: 'DO3', side: 'right', position: 0.75, type: 'digital', connector: 'do3' }, { id: 'p-do3', name: 'DO3', side: 'right', position: 0.8, type: 'digital', connector: 'do3' },
], ],
}, },
{ {
...@@ -217,20 +325,20 @@ export const DEVICE_CATEGORIES = [ ...@@ -217,20 +325,20 @@ export const DEVICE_CATEGORIES = [
code: 2004, type: 'plc_ai', label: 'PLC 模拟输入', color: '#E91E63', icon: 'AI', code: 2004, type: 'plc_ai', label: 'PLC 模拟输入', color: '#E91E63', icon: 'AI',
width: 180, height: 100, width: 180, height: 100,
ports: [ ports: [
{ id: 'p-ai1', name: 'AI1', side: 'left', position: 0.3, type: 'analog', connector: 'ai1' }, { id: 'p-ai1', name: 'AI1', side: 'left', position: 0.3, type: 'analog', connector: 'ai1' },
{ id: 'p-ai2', name: 'AI2', side: 'left', position: 0.7, type: 'analog', connector: 'ai2' }, { id: 'p-ai2', name: 'AI2', side: 'left', position: 0.7, type: 'analog', connector: 'ai2' },
{ id: 'p-vcc', name: 'VCC', side: 'top', position: 0.5, type: 'power', connector: 'vcc' }, { id: 'p-vcc', name: 'VCC', side: 'right', position: 0.3, type: 'power', connector: 'vcc' },
{ id: 'p-gnd', name: 'GND', side: 'bottom',position: 0.5, type: 'power', connector: 'gnd' }, { id: 'p-gnd', name: 'GND', side: 'right', position: 0.7, type: 'power', connector: 'gnd' },
], ],
}, },
{ {
code: 2005, type: 'plc_ao', label: 'PLC 模拟输出', color: '#FF5722', icon: 'AO', code: 2005, type: 'plc_ao', label: 'PLC 模拟输出', color: '#FF5722', icon: 'AO',
width: 180, height: 100, width: 180, height: 100,
ports: [ ports: [
{ id: 'p-ao1', name: 'AO1', side: 'right', position: 0.3, type: 'analog', connector: 'ao1' }, { id: 'p-ao1', name: 'AO1', side: 'right', position: 0.3, type: 'analog', connector: 'ao1' },
{ id: 'p-ao2', name: 'AO2', side: 'right', position: 0.7, type: 'analog', connector: 'ao2' }, { id: 'p-ao2', name: 'AO2', side: 'right', position: 0.7, type: 'analog', connector: 'ao2' },
{ id: 'p-vcc', name: 'VCC', side: 'top', position: 0.5, type: 'power', connector: 'vcc' }, { id: 'p-vcc', name: 'VCC', side: 'left', position: 0.3, type: 'power', connector: 'vcc' },
{ id: 'p-gnd', name: 'GND', side: 'bottom',position: 0.5, type: 'power', connector: 'gnd' }, { id: 'p-gnd', name: 'GND', side: 'left', position: 0.7, type: 'power', connector: 'gnd' },
], ],
}, },
], ],
...@@ -244,17 +352,17 @@ export const DEVICE_CATEGORIES = [ ...@@ -244,17 +352,17 @@ export const DEVICE_CATEGORIES = [
ports: [ ports: [
{ id: 'p-in', name: '进水', side: 'left', position: 0.5, type: 'water', connector: 'waterIn' }, { id: 'p-in', name: '进水', side: 'left', position: 0.5, type: 'water', connector: 'waterIn' },
{ id: 'p-out', name: '出水', side: 'right', position: 0.5, type: 'water', connector: 'waterOut' }, { id: 'p-out', name: '出水', side: 'right', position: 0.5, type: 'water', connector: 'waterOut' },
{ id: 'p-pwr1', name: 'L', side: 'top', position: 0.3, type: 'power', connector: 'l' }, { id: 'p-pwr1', name: 'L', side: 'bottom', position: 0.3, type: 'power', connector: 'l' },
{ id: 'p-pwr2', name: 'N', side: 'top', position: 0.7, type: 'power', connector: 'n' }, { id: 'p-pwr2', name: 'N', side: 'bottom', position: 0.7, type: 'power', connector: 'n' },
], ],
}, },
{ {
code: 3002, type: 'valve', label: '阀门', color: '#009688', icon: 'V', code: 3002, type: 'valve', label: '阀门', color: '#009688', icon: 'V',
width: 120, height: 80, width: 120, height: 80,
ports: [ ports: [
{ id: 'p-in', name: '进口', side: 'left', position: 0.5, type: 'water', connector: 'inlet' }, { id: 'p-in', name: '进口', side: 'left', position: 0.5, type: 'water', connector: 'inlet' },
{ id: 'p-out', name: '出口', side: 'right', position: 0.5, type: 'water', connector: 'outlet' }, { id: 'p-out', name: '出口', side: 'right', position: 0.5, type: 'water', connector: 'outlet' },
{ id: 'p-ctl', name: '控制', side: 'top', position: 0.5, type: 'digital', connector: 'ctrl' }, { id: 'p-ctl', name: '控制', side: 'bottom', position: 0.5, type: 'digital', connector: 'ctrl' },
], ],
}, },
{ {
...@@ -270,7 +378,7 @@ export const DEVICE_CATEGORIES = [ ...@@ -270,7 +378,7 @@ export const DEVICE_CATEGORIES = [
code: 3004, type: 'tank', label: '水箱', color: '#0288D1', icon: 'TK', code: 3004, type: 'tank', label: '水箱', color: '#0288D1', icon: 'TK',
width: 180, height: 120, width: 180, height: 120,
ports: [ ports: [
{ id: 'p-in', name: '进水', side: 'top', position: 0.5, type: 'water', connector: 'waterIn' }, { id: 'p-in', name: '进水', side: 'left', position: 0.5, type: 'water', connector: 'waterIn' },
{ id: 'p-out', name: '出水', side: 'bottom', position: 0.5, type: 'water', connector: 'waterOut' }, { id: 'p-out', name: '出水', side: 'bottom', position: 0.5, type: 'water', connector: 'waterOut' },
{ id: 'p-level', name: '液位信号', side: 'right', position: 0.5, type: 'analog', connector: 'levelSig' }, { id: 'p-level', name: '液位信号', side: 'right', position: 0.5, type: 'analog', connector: 'levelSig' },
], ],
...@@ -293,30 +401,30 @@ export const DEVICE_CATEGORIES = [ ...@@ -293,30 +401,30 @@ export const DEVICE_CATEGORIES = [
code: 4002, type: 'solenoid_valve', label: '电磁阀', color: '#689F38', icon: 'SV', code: 4002, type: 'solenoid_valve', label: '电磁阀', color: '#689F38', icon: 'SV',
width: 140, height: 80, width: 140, height: 80,
ports: [ ports: [
{ id: 'p-in', name: 'P', side: 'left', position: 0.5, type: 'air', connector: 'supply' }, { id: 'p-in', name: 'P', side: 'left', position: 0.5, type: 'air', connector: 'supply' },
{ id: 'p-a', name: 'A', side: 'right', position: 0.3, type: 'air', connector: 'portA' }, { id: 'p-a', name: 'A', side: 'right', position: 0.3, type: 'air', connector: 'portA' },
{ id: 'p-b', name: 'B', side: 'right', position: 0.7, type: 'air', connector: 'portB' }, { id: 'p-b', name: 'B', side: 'right', position: 0.7, type: 'air', connector: 'portB' },
{ id: 'p-ctl', name: '线圈', side: 'top', position: 0.5, type: 'digital', connector: 'coil' }, { id: 'p-ctl', name: '线圈', side: 'bottom', position: 0.5, type: 'digital', connector: 'coil' },
], ],
}, },
{ {
code: 4003, type: 'servo', label: '伺服电机', color: '#FF9800', icon: 'SV', code: 4003, type: 'servo', label: '伺服电机', color: '#FF9800', icon: 'SV',
width: 180, height: 100, width: 180, height: 120,
ports: [ ports: [
{ id: 'p-u', name: 'U', side: 'top', position: 0.2, type: 'power', connector: 'u' }, { id: 'p-u', name: 'U', side: 'left', position: 0.2, type: 'power', connector: 'u' },
{ id: 'p-v', name: 'V', side: 'top', position: 0.5, type: 'power', connector: 'v' }, { id: 'p-v', name: 'V', side: 'left', position: 0.5, type: 'power', connector: 'v' },
{ id: 'p-w', name: 'W', side: 'top', position: 0.8, type: 'power', connector: 'w' }, { id: 'p-w', name: 'W', side: 'left', position: 0.8, type: 'power', connector: 'w' },
{ id: 'p-enc',name: '编码器', side: 'right',position: 0.5, type: 'analog', connector: 'encoder' }, { id: 'p-enc',name: '编码器', side: 'right', position: 0.5, type: 'analog', connector: 'encoder' },
{ id: 'p-pe', name: 'PE', side: 'bottom',position: 0.5, type: 'power', connector: 'pe' }, { id: 'p-pe', name: 'PE', side: 'bottom', position: 0.5, type: 'power', connector: 'pe' },
], ],
}, },
{ {
code: 4004, type: 'vfd', label: '变频器', color: '#673AB7', icon: 'VFD', code: 4004, type: 'vfd', label: '变频器', color: '#673AB7', icon: 'VFD',
width: 180, height: 120, width: 180, height: 120,
ports: [ ports: [
{ id: 'p-r', name: 'R', side: 'top', position: 0.2, type: 'power', connector: 'r' }, { id: 'p-r', name: 'R', side: 'left', position: 0.2, type: 'power', connector: 'r' },
{ id: 'p-s', name: 'S', side: 'top', position: 0.5, type: 'power', connector: 's' }, { id: 'p-s', name: 'S', side: 'left', position: 0.5, type: 'power', connector: 's' },
{ id: 'p-t', name: 'T', side: 'top', position: 0.8, type: 'power', connector: 't' }, { id: 'p-t', name: 'T', side: 'left', position: 0.8, type: 'power', connector: 't' },
{ id: 'p-u', name: 'U', side: 'bottom', position: 0.2, type: 'power', connector: 'u' }, { id: 'p-u', name: 'U', side: 'bottom', position: 0.2, type: 'power', connector: 'u' },
{ id: 'p-v', name: 'V', side: 'bottom', position: 0.5, type: 'power', connector: 'v' }, { id: 'p-v', name: 'V', side: 'bottom', position: 0.5, type: 'power', connector: 'v' },
{ id: 'p-w', name: 'W', side: 'bottom', position: 0.8, type: 'power', connector: 'w' }, { id: 'p-w', name: 'W', side: 'bottom', position: 0.8, type: 'power', connector: 'w' },
...@@ -353,7 +461,11 @@ export function getDeviceDefinition(code) { ...@@ -353,7 +461,11 @@ export function getDeviceDefinition(code) {
/** 获取设备类型信息,未知功能代码返回默认值 */ /** 获取设备类型信息,未知功能代码返回默认值 */
export function getDeviceType(functionCode) { export function getDeviceType(functionCode) {
return FUNCTION_CODE_MAP[functionCode] || { const dev = _allDevices[functionCode];
if (dev) {
return { type: dev.type, label: dev.label, color: dev.color, icon: dev.icon };
}
return {
type: 'unknown', type: 'unknown',
label: `未知(${functionCode})`, label: `未知(${functionCode})`,
color: '#9E9E9E', color: '#9E9E9E',
...@@ -402,6 +514,7 @@ export const NODE_DIMENSIONS = { ...@@ -402,6 +514,7 @@ export const NODE_DIMENSIONS = {
export const PORT_TYPES = { export const PORT_TYPES = {
analog: { label: '模拟量', color: '#E91E63' }, analog: { label: '模拟量', color: '#E91E63' },
digital: { label: '数字量', color: '#2196F3' }, digital: { label: '数字量', color: '#2196F3' },
signal: { label: '信号', color: '#AB47BC' },
water: { label: '水流', color: '#00BCD4' }, water: { label: '水流', color: '#00BCD4' },
air: { label: '气流', color: '#8BC34A' }, air: { label: '气流', color: '#8BC34A' },
power: { label: '电力', color: '#FF9800' }, power: { label: '电力', color: '#FF9800' },
...@@ -418,6 +531,8 @@ export const PORT_TYPES = { ...@@ -418,6 +531,8 @@ export const PORT_TYPES = {
export function isPortCompatible(typeA, typeB) { export function isPortCompatible(typeA, typeB) {
if (!typeA || !typeB) return true; if (!typeA || !typeB) return true;
if (typeA === 'generic' || typeB === 'generic') return true; if (typeA === 'generic' || typeB === 'generic') return true;
// signal 端口只能与 signal 或 generic 连接
if (typeA === 'signal' || typeB === 'signal') return typeA === typeB;
return typeA === typeB; return typeA === typeB;
} }
......
...@@ -118,6 +118,63 @@ const MODEL_MAP = { ...@@ -118,6 +118,63 @@ const MODEL_MAP = {
portMap: { 'p-ao1': 'AO1', 'p-ao2': 'AO2', 'p-com': 'COM' }, portMap: { 'p-ao1': 'AO1', 'p-ao2': 'AO2', 'p-com': 'COM' },
paramMap: {}, paramMap: {},
}, },
// ===== 流程节点 =====
greater_than: {
modelName: 'GreaterThan', isFlowNode: true, operator: '>',
portMap: { 'p-a': 'a', 'p-b': 'b', 'p-out': 'out' },
paramMap: {},
},
less_than: {
modelName: 'LessThan', isFlowNode: true, operator: '<',
portMap: { 'p-a': 'a', 'p-b': 'b', 'p-out': 'out' },
paramMap: {},
},
equal: {
modelName: 'Equal', isFlowNode: true, operator: '==',
portMap: { 'p-a': 'a', 'p-b': 'b', 'p-out': 'out' },
paramMap: {},
},
logic_and: {
modelName: 'And', isFlowNode: true, operator: 'and',
portMap: { 'p-a': 'a', 'p-b': 'b', 'p-out': 'out' },
paramMap: {},
},
logic_or: {
modelName: 'Or', isFlowNode: true, operator: 'or',
portMap: { 'p-a': 'a', 'p-b': 'b', 'p-out': 'out' },
paramMap: {},
},
logic_not: {
modelName: 'Not', isFlowNode: true, operator: 'not',
portMap: { 'p-in': 'inVal', 'p-out': 'out' },
paramMap: {},
},
logic_switch: {
modelName: 'Switch', isFlowNode: true, operator: 'if',
portMap: { 'p-switch': 'switchCtrl', 'p-false': 'falseVal', 'p-true': 'trueVal', 'p-out': 'out' },
paramMap: {},
},
val_integer: {
modelName: 'IntegerValue', isFlowNode: true, valueType: 'Integer',
portMap: { 'p-out': 'out' },
paramMap: { value: 'value' },
},
val_real: {
modelName: 'RealValue', isFlowNode: true, valueType: 'Real',
portMap: { 'p-out': 'out' },
paramMap: { value: 'value' },
},
val_string: {
modelName: 'StringValue', isFlowNode: true, valueType: 'String',
portMap: { 'p-out': 'out' },
paramMap: { value: 'value' },
},
val_boolean: {
modelName: 'BooleanValue', isFlowNode: true, valueType: 'Boolean',
portMap: { 'p-out': 'out' },
paramMap: { value: 'value' },
},
}; };
/** /**
......
...@@ -46,8 +46,6 @@ function toMoId(str) { ...@@ -46,8 +46,6 @@ function toMoId(str) {
return id; return id;
} }
// ===== 核心导出函数 =====
/** /**
* 将 { nodes, edges } 转为 OpenModelica .mo 模型字符串 * 将 { nodes, edges } 转为 OpenModelica .mo 模型字符串
* @param {Object} data - { nodes: [], edges: [] } * @param {Object} data - { nodes: [], edges: [] }
...@@ -59,7 +57,7 @@ export function exportToModelica(data, modelName = 'Circuit') { ...@@ -59,7 +57,7 @@ export function exportToModelica(data, modelName = 'Circuit') {
const warnings = []; const warnings = [];
const errors = []; const errors = [];
const lines = []; const lines = [];
const instanceMap = {}; // nodeId → { instanceName, type, ports } const instanceMap = {}; // nodeId → { instanceName, type, ports, isFlowNode }
// 读取用户自定义的映射覆盖 // 读取用户自定义的映射覆盖
let mappingOverrides = {}; let mappingOverrides = {};
...@@ -70,20 +68,24 @@ export function exportToModelica(data, modelName = 'Circuit') { ...@@ -70,20 +68,24 @@ export function exportToModelica(data, modelName = 'Circuit') {
lines.push(`model ${toMoId(modelName)}`); lines.push(`model ${toMoId(modelName)}`);
lines.push(''); lines.push('');
// ===== 1. 组件声明 ===== // ===== 1. 组件声明(跳过流程节点) =====
nodes.forEach((node, idx) => { nodes.forEach((node, idx) => {
const td = node.data?.templateData; const td = node.data?.templateData;
const type = td?.type; const type = td?.type;
const label = node.data?.label || `comp_${idx}`; const label = node.data?.label || `comp_${idx}`;
const instanceName = toMoId(label) + '_' + (idx + 1); const instanceName = toMoId(label) + '_' + (idx + 1);
const mapping = getModelMapping(type);
instanceMap[node.id] = { instanceMap[node.id] = {
instanceName, instanceName,
type, type,
ports: td?.ports || [], ports: td?.ports || [],
isFlowNode: mapping?.isFlowNode || false,
}; };
const mapping = getModelMapping(type); // 流程节点不生成组件声明
if (mapping?.isFlowNode) return;
if (!mapping) { if (!mapping) {
// 检查用户自定义映射(自定义符号用 custom_${id} 作为 key) // 检查用户自定义映射(自定义符号用 custom_${id} 作为 key)
...@@ -103,7 +105,7 @@ export function exportToModelica(data, modelName = 'Circuit') { ...@@ -103,7 +105,7 @@ export function exportToModelica(data, modelName = 'Circuit') {
lines.push(` ${customOverride.modelName} ${instanceName}${paramStr};`); lines.push(` ${customOverride.modelName} ${instanceName}${paramStr};`);
return; return;
} }
errors.push(`"${label}" (type: ${type || 'custom'}) 未建立模型映射,请先在“模型映射”中配置`); errors.push(`"${label}" (type: ${type || 'custom'}) 未建立模型映射,请先在"模型映射"中配置`);
return; return;
} }
...@@ -131,9 +133,70 @@ export function exportToModelica(data, modelName = 'Circuit') { ...@@ -131,9 +133,70 @@ export function exportToModelica(data, modelName = 'Circuit') {
lines.push(''); lines.push('');
// ===== 2. connect 方程 ===== // ===== 2. 构建流程节点端口到连接源的映射 =====
// flowInputMap: "nodeId:portId" → "对端变量表达式"
const flowInputMap = {};
edges.forEach(edge => {
const srcInfo = instanceMap[edge.source];
const tgtInfo = instanceMap[edge.target];
if (!srcInfo || !tgtInfo) return;
// 如果目标是流程节点,记录其输入端口的信号来源
if (tgtInfo.isFlowNode) {
const tgtPort = resolvePortName(edge.targetHandle, tgtInfo.type, tgtInfo.ports);
const srcPort = resolvePortName(edge.sourceHandle, srcInfo.type, srcInfo.ports);
flowInputMap[`${edge.target}:${tgtPort}`] = `${srcInfo.instanceName}.${srcPort}`;
}
});
// ===== 3. equation 段 =====
lines.push('equation'); lines.push('equation');
// 3a. 流程节点表达式
nodes.forEach((node, idx) => {
const td = node.data?.templateData;
const type = td?.type;
const mapping = getModelMapping(type);
if (!mapping?.isFlowNode) return;
const info = instanceMap[node.id];
const inst = info.instanceName;
const pv = node.data?.paramValues || {};
// 辅助:获取输入端口的信号来源
const getInput = (portName) => flowInputMap[`${node.id}:${portName}`] || '0';
if (mapping.valueType) {
// 数值节点:直接输出常量
const rawVal = pv.value ?? td?.params?.[0]?.defaultValue ?? '0';
let valStr;
if (mapping.valueType === 'String') {
valStr = `"${rawVal}"`;
} else if (mapping.valueType === 'Boolean') {
valStr = rawVal === 'true' || rawVal === true ? 'true' : 'false';
} else {
const numVal = parseEngValue(rawVal);
valStr = numVal != null ? formatMoValue(numVal) : rawVal;
}
lines.push(` ${inst}.out = ${valStr};`);
} else if (mapping.operator === 'not') {
// 非:一元运算
lines.push(` ${inst}.out = not ${getInput('inVal')};`);
} else if (mapping.operator === 'if') {
// Switch 条件选择(Blender 风格:Switch=true → True input, Switch=false → False input)
const switchCtrl = getInput('switchCtrl');
const falseVal = getInput('falseVal');
const trueVal = getInput('trueVal');
lines.push(` ${inst}.out = if ${switchCtrl} then ${trueVal} else ${falseVal};`);
} else if (mapping.operator) {
// 二元运算
const a = getInput('a');
const b = getInput('b');
lines.push(` ${inst}.out = ${a} ${mapping.operator} ${b};`);
}
});
// 3b. 物理节点的 connect 语句(跳过涉及流程节点的连接)
edges.forEach(edge => { edges.forEach(edge => {
const srcInfo = instanceMap[edge.source]; const srcInfo = instanceMap[edge.source];
const tgtInfo = instanceMap[edge.target]; const tgtInfo = instanceMap[edge.target];
...@@ -142,6 +205,9 @@ export function exportToModelica(data, modelName = 'Circuit') { ...@@ -142,6 +205,9 @@ export function exportToModelica(data, modelName = 'Circuit') {
return; return;
} }
// 流程节点的连接已在表达式中处理,跳过
if (srcInfo.isFlowNode || tgtInfo.isFlowNode) return;
// 通过映射模块解析端口名 // 通过映射模块解析端口名
const srcPort = resolvePortName(edge.sourceHandle, srcInfo.type, srcInfo.ports); const srcPort = resolvePortName(edge.sourceHandle, srcInfo.type, srcInfo.ports);
const tgtPort = resolvePortName(edge.targetHandle, tgtInfo.type, tgtInfo.ports); const tgtPort = resolvePortName(edge.targetHandle, tgtInfo.type, tgtInfo.ports);
...@@ -151,7 +217,7 @@ export function exportToModelica(data, modelName = 'Circuit') { ...@@ -151,7 +217,7 @@ export function exportToModelica(data, modelName = 'Circuit') {
lines.push(''); lines.push('');
// ===== 3. annotation ===== // ===== 4. annotation =====
if (nodes.length > 0) { if (nodes.length > 0) {
lines.push(' annotation(Diagram(coordinateSystem(preserveAspectRatio=false,'); lines.push(' annotation(Diagram(coordinateSystem(preserveAspectRatio=false,');
lines.push(' extent={{-200,-200},{800,800}})));'); lines.push(' extent={{-200,-200},{800,800}})));');
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment