/**
* @file plc_config.js
* @brief PLC 逻辑配置页 — 信号列表 + 逻辑图查看器 + 编辑器
* @details Canvas 可视化 + XML 解析/生成, 11 种门类型
*/
/* ========================================================================
* PLC 逻辑配置页面 — 信号管理 + 逻辑图查看 + 新建配置编辑器
* ======================================================================== */
var PlcConfigPage = (function() {
"use strict";
/* ---- Canvas roundRect polyfill (兼容旧浏览器) ---- */
if (!CanvasRenderingContext2D.prototype.roundRect) {
CanvasRenderingContext2D.prototype.roundRect = function(x, y, w, h, r) {
if (typeof r === 'number') r = { tl: r, tr: r, br: r, bl: r };
else if (Array.isArray(r)) r = { tl: r[0]||0, tr: r[1]||r[0]||0, br: r[2]||r[0]||0, bl: r[3]||r[0]||0 };
this.beginPath();
this.moveTo(x + r.tl, y);
this.lineTo(x + w - r.tr, y);
this.quadraticCurveTo(x + w, y, x + w, y + r.tr);
this.lineTo(x + w, y + h - r.br);
this.quadraticCurveTo(x + w, y + h, x + w - r.br, y + h);
this.lineTo(x + r.bl, y + h);
this.quadraticCurveTo(x, y + h, x, y + h - r.bl);
this.lineTo(x, y + r.tl);
this.quadraticCurveTo(x, y, x + r.tl, y);
this.closePath();
};
}
/* ========== 全局状态 ========== */
var activeTab = 'signals'; // signals | viewer | editor
var dcOutSignals = []; // [{no, saddr, desc}] 从 datacenter_out.xml
var logicGraphs = []; // 已解析的逻辑图数组
var onlineCfgId = '';
var loadedFiles = {}; // {path: content} 记录已加载的文件
/* ======== 编辑器状态 ======== */
var editNodes = []; // [{type, id, x, y, saddr, desc, no, inPins, _lx, _ly}]
var editLinks = []; // [{srcIdx, dstIdx, dstPin}] — dstPin 目标节点输入引脚序号
var editKey = ''; // 逻辑图 key
var editName = ''; // 逻辑图名称
var editOnlineCfgId = ''; // 当前 online_cfgId
var editSelectedNode = -1; // 当前选中节点索引,-1=无选中
var editSelectedLink = -1; // 当前选中连线索引,-1=无选中
var editWiring = null; // {fromNodeIdx, fromSide, fromPin} 当前连线起点
var editDragNode = null; // 拖拽中的节点索引
var editDragOffX = 0, editDragOffY = 0;
var editDragCanvas = null; // 画布拖拽状态
var editPanX = 0, editPanY = 0; // 画布偏移
var editScale = 1.0; // 缩放
var editNextGateIdx = {}; // {type: nextIdx} 每种门类型独立序号从0开始
var editLoadedFromFile = false; // 是否从文件加载
var editLastClickTime = 0; // 上次 click 时间戳(双击检测)
var editLastClickNode = -1; // 上次 click 的节点索引
/* ========== 查看器状态 ========== */
var viewPanX = 0, viewPanY = 0;
var viewScale = 1.0;
var viewDrag = null; // {startX, startY, panX, panY}
var viewAutoFit = true; // 首次 / resize 后自动适配缩放
/* ========== 节点类型常量 ========== */
var NT_OUTPUT = 0, NT_INPUT = 1, NT_OR = 2, NT_AND = 3, NT_NOT = 4;
var NT_62P = 5, NT_62D = 6, NT_RISING = 7, NT_SR = 8, NT_RS = 9, NT_FALLING = 10;
var NODE_INFO = {
0: { name: '输出点', cls: 'node-out', icon: '📤', color: '#4CAF50', inPins: 1, outPins: 0, inMax: 1, outMax: 0 },
1: { name: '输入点', cls: 'node-in', icon: '📥', color: '#2196F3', inPins: 0, outPins: 1, inMax: 0, outMax: 99 },
2: { name: '或门 OR', cls: 'node-or', icon: '🔶', color: '#FF9800', inPins: 2, outPins: 1, inMax: 10, outMax: 10 },
3: { name: '与门 AND', cls: 'node-and', icon: '🔷', color: '#9C27B0', inPins: 2, outPins: 1, inMax: 10, outMax: 10 },
4: { name: '非门 NOT', cls: 'node-not', icon: '🔺', color: '#f44336', inPins: 1, outPins: 1, inMax: 1, outMax: 10 },
5: { name: '62p延时', cls: 'node-62p', icon: '⏱️', color: '#00BCD4', inPins: 1, outPins: 1, inMax: 1, outMax: 10 },
6: { name: '62d延时', cls: 'node-62d', icon: '⏱️', color: '#009688', inPins: 1, outPins: 1, inMax: 1, outMax: 10 },
7: { name: '上升沿', cls: 'node-re', icon: '⬆️', color: '#E91E63', inPins: 1, outPins: 1, inMax: 1, outMax: 10 },
8: { name: 'SR触发器', cls: 'node-sr', icon: '🔒', color: '#795548', inPins: 2, outPins: 1, inMax: 2, outMax: 10 },
9: { name: 'RS触发器', cls: 'node-rs', icon: '🔒', color: '#607D8B', inPins: 2, outPins: 1, inMax: 2, outMax: 10 },
10: { name: '下降沿', cls: 'node-fe', icon: '⬇️', color: '#FF5722', inPins: 1, outPins: 1, inMax: 1, outMax: 10 }
};
/* ========== 工具函数 ========== */
function fmtVal(v) { return (v === null || v === undefined) ? '' : String(v); }
function escHtml(s) { return String(s).replace(/&/g,'&').replace(//g,'>').replace(/"/g,'"'); }
/* ---- 通过节点 ID 查找信号信息 (datacenter out 中 no 即 ID) ---- */
function findSignalByNo(signalNo) {
for (var i = 0; i < dcOutSignals.length; i++) {
if (dcOutSignals[i].no === signalNo) return dcOutSignals[i];
}
return null;
}
/* ---- 生成节点显示标签 (十进制ID, 含saddr/desc) ---- */
function getNodeDisplayLabel(node) {
var info = NODE_INFO[node.type] || {};
if (node.type === NT_INPUT || node.type === NT_OUTPUT) {
var prefix = (node.type === NT_INPUT) ? '输入' : '输出';
var decId = node.id; // id 在 parse 时已转为十进制
var sig = findSignalByNo(decId);
if (sig) {
return prefix + ' #' + decId + ' ("' + sig.saddr + '", "' + sig.desc + '")';
}
return prefix + ' #' + decId;
}
// 逻辑门:显示组件序号
var gateIdx = node.id & 0xFF;
return info.icon + ' ' + info.name + ' #' + gateIdx;
}
/* ---- 生成编辑器小标签 (简洁版,用于编辑器节点) ---- */
function getEditorNodeLabel(node) {
var info = NODE_INFO[node.type] || {};
if (node.type === NT_INPUT || node.type === NT_OUTPUT) {
var prefix = (node.type === NT_INPUT) ? '输入点' : '输出点';
var decId = node.id;
var sig = node.saddr ? node : findSignalByNo(decId);
if (sig && sig.saddr) {
return info.icon + ' ' + prefix + ' #' + decId + '\n' + sig.saddr + '\n' + (sig.desc || '');
}
return info.icon + ' ' + prefix + ' #' + decId;
}
var gateIdx = node.id & 0xFF;
return info.icon + ' ' + info.name + ' #' + gateIdx;
}
function showCfgToast(msg, type) { // wraps global toast
type = type || 'info';
var t = document.getElementById('plc-cfg-toast');
if (!t) {
t = document.createElement('div');
t.id = 'plc-cfg-toast';
t.style.cssText = 'position:fixed;top:80px;left:50%;transform:translateX(-50%);padding:10px 24px;border-radius:6px;z-index:99999;font-size:14px;box-shadow:0 4px 12px rgba(0,0,0,0.3);transition:opacity 0.3s;pointer-events:none;color:#fff';
document.body.appendChild(t);
}
t.style.background = (type === 'err') ? '#f44336' : (type === 'ok') ? '#4CAF50' : '#333';
t.textContent = msg; t.style.opacity = '1';
clearTimeout(t._timer);
t._timer = setTimeout(function() { t.style.opacity = '0'; }, 2000);
}
/* ========== WebSocket 文件请求 ========== */
function requestFile(path, callback) {
if (loadedFiles[path]) {
callback(loadedFiles[path]);
return;
}
if (!PlcConfigPage._pending) PlcConfigPage._pending = {};
PlcConfigPage._pending[path] = callback;
var sent = WsClient.send({ type: 'get_file', path: path });
if (!sent) {
if (!PlcConfigPage._retryList) PlcConfigPage._retryList = [];
PlcConfigPage._retryList.push(path);
}
}
/* ========== 主渲染 ========== */
function renderTabs() {
var tabs = [
{ id: 'signals', label: '📋 信号列表', title: 'datacenter_out.xml — 已注册 Out 信号' },
{ id: 'viewer', label: '👁️ 逻辑图查看', title: 'Reclose_logic.xml 可视化' },
{ id: 'editor', label: '✏️ 新建配置', title: '新建/编辑 PLC 逻辑配置' }
];
var html = '
';
for (var i = 0; i < tabs.length; i++) {
var cls = (tabs[i].id === activeTab) ? 'plc-cfg-tab active' : 'plc-cfg-tab';
html += '';
}
html += '
';
return html;
}
function render(container) {
container.innerHTML = ''
+ ''
+ renderTabs()
+ '';
// Tab 切换事件
var tabEls = container.querySelectorAll('.plc-cfg-tab');
for (var i = 0; i < tabEls.length; i++) {
tabEls[i].onclick = function() {
activeTab = this.getAttribute('data-tab');
render(container);
};
}
// 渲染当前 Tab
var content = document.getElementById('plc-cfg-content');
if (!content) return;
switch (activeTab) {
case 'signals': renderSignalsTab(content); break;
case 'viewer': renderViewerTab(content); break;
case 'editor': renderEditorTab(content); break;
}
}
/* ================================================================
* Tab 1: 信号列表 — 解析 datacenter_out.xml
* ================================================================ */
function renderSignalsTab(container) {
container.innerHTML = ''
+ ''
+ ''
+ '| 序号 (no) | 短地址 (saddr) | 描述 (desc) |
'
+ ''
+ '
';
document.getElementById('plc-sig-search').oninput = function() { refreshSigTable(); };
document.getElementById('plc-sig-reload').onclick = function() {
loadedFiles['config/SYSTEM/datacenter_out.xml'] = null;
dcOutSignals = [];
loadDcOutSignals(function() { refreshSigTable(); });
};
if (dcOutSignals.length === 0) {
loadDcOutSignals(function() { refreshSigTable(); });
} else {
refreshSigTable();
}
}
function loadDcOutSignals(cb) {
requestFile('config/SYSTEM/datacenter_out.xml', function(content) {
if (!content) { showToast('加载 datacenter_out.xml 失败,请先在终端执行 datacenter out', 'err'); return; }
parseDcOutXml(content);
if (cb) cb();
});
}
function parseDcOutXml(xmlStr) {
dcOutSignals = [];
try {
var parser = new DOMParser();
var doc = parser.parseFromString(xmlStr, 'text/xml');
var outNodes = doc.documentElement.getElementsByTagName('Out');
if (outNodes.length > 0) {
var signals = outNodes[0].getElementsByTagName('Signal');
for (var i = 0; i < signals.length; i++) {
dcOutSignals.push({
no: parseInt(signals[i].getAttribute('no')) || i,
saddr: signals[i].getAttribute('saddr') || '',
desc: signals[i].getAttribute('desc') || ''
});
}
}
} catch(e) {
showToast('解析 datacenter_out.xml 失败: ' + e.message, 'err');
}
}
function refreshSigTable() {
var search = (document.getElementById('plc-sig-search').value || '').toLowerCase();
var data = dcOutSignals;
// 过滤
var filtered = data;
if (search) {
filtered = data.filter(function(s) {
return s.saddr.toLowerCase().indexOf(search) !== -1
|| s.desc.toLowerCase().indexOf(search) !== -1
|| String(s.no).indexOf(search) !== -1;
});
}
document.getElementById('plc-sig-count').textContent = '共 ' + filtered.length + ' / ' + data.length + ' 条';
var html = '';
for (var i = 0; i < filtered.length; i++) {
var s = filtered[i];
html += ''
+ '| ' + s.no + ' | '
+ '' + escHtml(s.saddr) + ' | '
+ '' + escHtml(s.desc) + ' | '
+ '
';
}
document.getElementById('plc-sig-tbody').innerHTML = html;
}
/* ================================================================
* Tab 2: 逻辑图查看器 — Canvas 可视化 Reclose_logic.txt
* ================================================================ */
function renderViewerTab(container) {
container.innerHTML = ''
+ ''
+ ''
+ ''
+ ''
+ ''
+ ''
+ ''
+ '
'
+ ''
+ ''
+ '
'
+ '';
// 图例
var legendHtml = '图例: ';
var types = [1,0,3,2,4,5,6,7,8,9,10];
for (var i = 0; i < types.length; i++) {
var info = NODE_INFO[types[i]];
legendHtml += '' + info.icon + ' ' + info.name + '';
}
document.getElementById('plc-view-legend').innerHTML = legendHtml;
// 绑定事件
document.getElementById('plc-view-zoomin').onclick = function() { viewScale = Math.min(3, viewScale * 1.2); drawViewer(); };
document.getElementById('plc-view-zoomout').onclick = function() { viewScale = Math.max(0.2, viewScale / 1.2); drawViewer(); };
document.getElementById('plc-view-reset').onclick = function() { viewAutoFit = true; drawViewer(); };
document.getElementById('plc-view-reload').onclick = function() {
loadedFiles['config/PLC/Reclose_logic.xml'] = null;
loadedFiles['config/PLC/Reclose_logic.xml.bak'] = null;
logicGraphs = [];
loadLogicFileAll(function() { refreshGraphSelect(); });
};
document.getElementById('plc-view-graph').onchange = function() { viewAutoFit = true; drawViewer(); };
// 画布交互
setupViewerCanvas();
if (logicGraphs.length === 0) {
loadLogicFileAll(function() { refreshGraphSelect(); });
} else {
refreshGraphSelect();
}
}
function loadLogicFileAll(cb) {
var loadCount = 0;
var loadedFiles2 = []; // 记录成功加载的文件名
var failedFiles = []; // 记录失败的文件名
function tryCb() {
loadCount++;
if (loadCount >= 2 && cb) {
if (failedFiles.length > 0) {
var msg = loadedFiles2.length > 0
? '已加载: ' + loadedFiles2.join(', ') + ' | 缺失: ' + failedFiles.join(', ')
: '配置文件均缺失: ' + failedFiles.join(', ') + ',请先在编辑器中生成配置';
showToast(msg, loadedFiles2.length > 0 ? 'info' : 'err');
}
cb();
}
}
requestFile('config/PLC/Reclose_logic.xml', function(content) {
if (!content) { failedFiles.push('Reclose_logic.xml'); tryCb(); return; }
parseLogicFile(content, 'Reclose_logic.xml');
loadedFiles2.push('Reclose_logic.xml');
tryCb();
});
requestFile('config/PLC/Reclose_logic.xml.bak', function(content) {
if (!content) { tryCb(); return; }
parseLogicFile(content, 'Reclose_logic.xml.bak');
loadedFiles2.push('Reclose_logic.xml.bak');
tryCb();
});
}
function loadLogicFile(cb) {
requestFile('config/PLC/Reclose_logic.xml', function(content) {
if (!content) { showToast('加载 Reclose_logic.xml 失败', 'err'); return; }
parseLogicFile(content, 'Reclose_logic.xml');
if (cb) cb();
});
}
function parseLogicFile(content, sourceName) {
logicGraphs = logicGraphs.filter(function(g) { return g._source !== sourceName; });
onlineCfgId = onlineCfgId || '';
parseLogicXml(content, sourceName);
}
// XML → 扁平 graph,与 C++ parse_xml_comb 完全一致
function parseLogicXml(xmlStr, sourceName) {
var parser = new DOMParser();
var doc = parser.parseFromString(xmlStr, 'text/xml');
var root = doc.documentElement;
if (!root || root.nodeName !== 'PLCConfig') {
showToast('XML 根元素不是 PLCConfig', 'err'); return;
}
if (sourceName.indexOf('.bak') < 0) {
onlineCfgId = root.getAttribute('checksum') || '';
}
var thisKey = root.getAttribute('key') || sourceName;
var thisName = root.getAttribute('name') || '';
var g = { key: thisKey, name: thisName, nodes: [], links: [] };
var GTM = {'INPUT':1,'OUTPUT':0,'OR':2,'AND':3,'NOT':4,'T62P':5,'T62D':6,'RISING':7,'SR':8,'RS':9,'FALLING':10};
var sigNodeMap = {};
function ensureSig(ioType, no, x, y) {
var k = ioType + ':' + no;
if (sigNodeMap[k] !== undefined) {
if (x !== undefined) { g.nodes[sigNodeMap[k]].x = x; g.nodes[sigNodeMap[k]].y = y; }
return sigNodeMap[k];
}
g.nodes.push({ type:ioType, id:no, x:x||0, y:y||0 });
sigNodeMap[k] = g.nodes.length - 1;
return g.nodes.length - 1;
}
function ensureGate(type, id, x, y, inPins, delayMs) {
for (var i = 0; i < g.nodes.length; i++)
if (g.nodes[i].type === type && (g.nodes[i].id & 0xFF) === (id & 0xFF)) return i;
var nd = { type:type, id:id, x:x||0, y:y||0 };
if (inPins) nd.inPins = inPins;
if (delayMs) nd.delayMs = delayMs;
g.nodes.push(nd);
return g.nodes.length - 1;
}
function parseComb(combEl) {
var ge = combEl.firstElementChild;
while (ge && ge.nodeName !== 'Gate') ge = ge.nextElementSibling;
if (!ge) return -1;
var t = GTM[ge.getAttribute('type')] || 0;
var gid = parseInt(ge.getAttribute('id'));
var gi = ensureGate(t, gid,
parseInt(ge.getAttribute('x')), parseInt(ge.getAttribute('y')),
ge.getAttribute('inPins') ? parseInt(ge.getAttribute('inPins')) : null,
ge.getAttribute('delayMs') ? parseInt(ge.getAttribute('delayMs')) : null);
// 读取 Comb id
var combId = combEl.getAttribute('id');
if (combId) g.nodes[gi].combId = parseInt(combId);
// 读取 SR/RS 的 R/S 属性
var rAttr = ge.getAttribute('R');
var sAttr = ge.getAttribute('S');
if (rAttr) g.nodes[gi].rsR = rAttr;
if (sAttr) g.nodes[gi].rsS = sAttr;
var c = combEl.firstElementChild;
while (c) {
if (c.nodeName === 'Comb') {
var upi = parseComb(c);
if (upi >= 0) g.links.push({ src_type:g.nodes[upi].type, src_id:g.nodes[upi].id, dst_type:t, dst_id:gid });
}
c = c.nextElementSibling;
}
var ie = combEl.firstElementChild;
while (ie && ie.nodeName !== 'Inputs') ie = ie.nextElementSibling;
if (ie) {
var s = ie.firstElementChild;
while (s) {
if (s.nodeName === 'Signal') {
var no = parseInt(s.getAttribute('no'));
ensureSig(NT_INPUT, no, parseInt(s.getAttribute('x')), parseInt(s.getAttribute('y')));
g.links.push({ src_type:NT_INPUT, src_id:no, dst_type:t, dst_id:gid });
}
s = s.nextElementSibling;
}
}
var oe = combEl.firstElementChild;
while (oe && oe.nodeName !== 'Outputs') oe = oe.nextElementSibling;
if (oe) {
var s = oe.firstElementChild;
while (s) {
if (s.nodeName === 'Signal') {
var no = parseInt(s.getAttribute('no'));
ensureSig(NT_OUTPUT, no, parseInt(s.getAttribute('x')), parseInt(s.getAttribute('y')));
g.links.push({ src_type:t, src_id:gid, dst_type:NT_OUTPUT, dst_id:no });
}
s = s.nextElementSibling;
}
}
return gi;
}
var chains = root.getElementsByTagName('Chain');
for (var ci = 0; ci < chains.length; ci++) {
var chain = chains[ci];
var c = chain.firstElementChild;
while (c) {
if (c.nodeName === 'Inputs') {
var s = c.firstElementChild;
while (s) {
if (s.nodeName === 'Signal')
ensureSig(NT_INPUT, parseInt(s.getAttribute('no')),
parseInt(s.getAttribute('x')), parseInt(s.getAttribute('y')));
s = s.nextElementSibling;
}
}
if (c.nodeName === 'Outputs') {
var s = c.firstElementChild;
while (s) {
if (s.nodeName === 'Signal')
ensureSig(NT_OUTPUT, parseInt(s.getAttribute('no')),
parseInt(s.getAttribute('x')), parseInt(s.getAttribute('y')));
s = s.nextElementSibling;
}
}
if (c.nodeName === 'Comb') parseComb(c);
if (c.nodeName === 'Gate') {
var gt = GTM[c.getAttribute('type')] || 0;
var giChain = ensureGate(gt, parseInt(c.getAttribute('id')),
parseInt(c.getAttribute('x')), parseInt(c.getAttribute('y')));
var rAttrC = c.getAttribute('R');
var sAttrC = c.getAttribute('S');
var dmsC = c.getAttribute('delayMs');
if (rAttrC) g.nodes[giChain].rsR = rAttrC;
if (sAttrC) g.nodes[giChain].rsS = sAttrC;
if (dmsC) g.nodes[giChain].delayMs = parseInt(dmsC);
}
c = c.nextElementSibling;
}
}
// Comb链: Comb Gate → 链级 Gate (一个Comb一根线) + 链级 Gate → Outputs
// 简单链: Inputs → Gate → Outputs
for (var ci2 = 0; ci2 < chains.length; ci2++) {
var chain2 = chains[ci2];
var hasComb = false;
var c3 = chain2.firstElementChild;
while (c3) { if (c3.nodeName === 'Comb') { hasComb = true; break; } c3 = c3.nextElementSibling; }
var ge2 = null, ie2 = null, oe2 = null;
c3 = chain2.firstElementChild;
while (c3) {
if (c3.nodeName === 'Gate') ge2 = c3;
if (c3.nodeName === 'Inputs') ie2 = c3;
if (c3.nodeName === 'Outputs') oe2 = c3;
c3 = c3.nextElementSibling;
}
if (hasComb && ge2) {
// 链级 Comb → 取其 Gate → 链级 Gate
c3 = chain2.firstElementChild;
while (c3) {
if (c3.nodeName === 'Comb') {
var cg = c3.firstElementChild;
while (cg && cg.nodeName !== 'Gate') cg = cg.nextElementSibling;
if (cg) {
var ct = GTM[cg.getAttribute('type')] || 0;
var ci = parseInt(cg.getAttribute('id'));
g.links.push({ src_type:ct, src_id:ci, dst_type:GTM[ge2.getAttribute('type')] || 0, dst_id:parseInt(ge2.getAttribute('id')) });
}
}
c3 = c3.nextElementSibling;
}
// 链级 Inputs(如 RS/SR 直接连 Input 信号)→ 链级 Gate
if (ie2) {
var s = ie2.firstElementChild;
while (s) { if (s.nodeName === 'Signal') g.links.push({ src_type:NT_INPUT, src_id:parseInt(s.getAttribute('no')), dst_type:GTM[ge2.getAttribute('type')] || 0, dst_id:parseInt(ge2.getAttribute('id')) }); s = s.nextElementSibling; }
}
// RS/SR 的 R/S 属性引用的 Input 信号(无 块时也需创建链接)
if (ge2) {
var gtCS = GTM[ge2.getAttribute('type')] || 0;
if (gtCS === NT_SR || gtCS === NT_RS) {
var sAttrCS = ge2.getAttribute('S');
var rAttrCS = ge2.getAttribute('R');
if (sAttrCS && sAttrCS.indexOf('Input#') === 0) {
var sNo = parseInt(sAttrCS.substring(6));
ensureSig(NT_INPUT, sNo, 0, 0);
g.links.push({ src_type:NT_INPUT, src_id:sNo, dst_type:gtCS, dst_id:parseInt(ge2.getAttribute('id')) });
}
if (rAttrCS && rAttrCS.indexOf('Input#') === 0) {
var rNo = parseInt(rAttrCS.substring(6));
ensureSig(NT_INPUT, rNo, 0, 0);
g.links.push({ src_type:NT_INPUT, src_id:rNo, dst_type:gtCS, dst_id:parseInt(ge2.getAttribute('id')) });
}
}
}
if (oe2) {
var s = oe2.firstElementChild;
while (s) { if (s.nodeName === 'Signal') g.links.push({ src_type:GTM[ge2.getAttribute('type')] || 0, src_id:parseInt(ge2.getAttribute('id')), dst_type:NT_OUTPUT, dst_id:parseInt(s.getAttribute('no')) }); s = s.nextElementSibling; }
}
} else if (!hasComb && ge2) {
var gt = GTM[ge2.getAttribute('type')] || 0;
var gid = parseInt(ge2.getAttribute('id'));
if (ie2) {
var s = ie2.firstElementChild;
while (s) { if (s.nodeName === 'Signal') g.links.push({ src_type:NT_INPUT, src_id:parseInt(s.getAttribute('no')), dst_type:gt, dst_id:gid }); s = s.nextElementSibling; }
}
if (oe2) {
var s = oe2.firstElementChild;
while (s) { if (s.nodeName === 'Signal') g.links.push({ src_type:gt, src_id:gid, dst_type:NT_OUTPUT, dst_id:parseInt(s.getAttribute('no')) }); s = s.nextElementSibling; }
}
}
}
// 最终遍:为 SR/RS 节点修复 dstPin
for (var ni = 0; ni < g.nodes.length; ni++) {
var nd = g.nodes[ni];
if (nd.type !== NT_SR && nd.type !== NT_RS) continue;
var sSrc = nd.rsS || '';
var rSrc = nd.rsR || '';
if (!sSrc && !rSrc) continue;
// 遍历所有到该节点的链路
for (var lj = 0; lj < g.links.length; lj++) {
var lk = g.links[lj];
if (lk.dst_type !== nd.type || (lk.dst_id & 0xFF) !== (nd.id & 0xFF)) continue;
var srcDesc = '';
var sN = null;
for (var sn = 0; sn < g.nodes.length; sn++) {
if (g.nodes[sn].type === lk.src_type &&
(g.nodes[sn].type <= 1 ? g.nodes[sn].id === lk.src_id : (g.nodes[sn].id & 0xFF) === (lk.src_id & 0xFF))) {
sN = g.nodes[sn]; break;
}
}
if (!sN) continue;
if (sN.type === NT_INPUT) srcDesc = 'Input#' + sN.id;
else srcDesc = 'Comb#' + (sN.combId !== undefined ? sN.combId : (sN.id & 0xFF));
if (nd.type === NT_SR) {
if (sSrc && srcDesc === sSrc) lk.dstPin = 0;
else if (rSrc && srcDesc === rSrc) lk.dstPin = 1;
} else {
if (sSrc && srcDesc === sSrc) lk.dstPin = 1;
else if (rSrc && srcDesc === rSrc) lk.dstPin = 0;
}
}
}
g._source = sourceName;
logicGraphs.push(g);
}
function refreshGraphSelect() {
var sel = document.getElementById('plc-view-graph');
if (!sel) return;
sel.innerHTML = '';
for (var i = 0; i < logicGraphs.length; i++) {
var g = logicGraphs[i];
var bakTag = (g._source && g._source.indexOf(".bak") >= 0) ? " [备份]" : "";
var label = g.key + " — " + (g.name || "(未命名)") + bakTag + " (" + g.nodes.length + "节点 " + g.links.length + "连线)";
sel.innerHTML += '';
}
if (logicGraphs.length > 0) {
sel.value = '0';
setTimeout(function() { drawViewer(); }, 100);
}
}
/* ---- 查看器 Canvas ---- */
function setupViewerCanvas() {
var wrap = document.getElementById('plc-view-canvas-wrap');
var canvas = document.getElementById('plc-view-canvas');
if (!wrap || !canvas) return;
function resize() {
canvas.width = wrap.clientWidth;
canvas.height = wrap.clientHeight - 4;
viewAutoFit = true;
drawViewer();
}
resize();
window.addEventListener('resize', function() { resize(); });
// 鼠标事件
canvas.onmousedown = function(e) {
viewDrag = { startX: e.clientX, startY: e.clientY, panX: viewPanX, panY: viewPanY };
};
canvas.onmousemove = function(e) {
if (!viewDrag) return;
viewPanX = viewDrag.panX + (e.clientX - viewDrag.startX);
viewPanY = viewDrag.panY + (e.clientY - viewDrag.startY);
drawViewer();
};
canvas.onmouseup = function() { viewDrag = null; };
canvas.onmouseleave = function() { viewDrag = null; };
canvas.onwheel = function(e) {
e.preventDefault();
var delta = e.deltaY > 0 ? 0.9 : 1.1;
var newScale = Math.max(0.2, Math.min(3, viewScale * delta));
// 以鼠标位置为中心缩放
var rect = canvas.getBoundingClientRect();
var mx = e.clientX - rect.left;
var my = e.clientY - rect.top;
viewPanX = mx - (mx - viewPanX) * (newScale / viewScale);
viewPanY = my - (my - viewPanY) * (newScale / viewScale);
viewScale = newScale;
drawViewer();
};
}
function drawViewer() {
var canvas = document.getElementById('plc-view-canvas');
if (!canvas) return;
var ctx = canvas.getContext('2d');
var W = canvas.width, H = canvas.height;
ctx.clearRect(0, 0, W, H);
ctx.fillStyle = '#1a1a2e';
ctx.fillRect(0, 0, W, H);
var sel = document.getElementById('plc-view-graph');
if (!sel || sel.value === '') {
ctx.fillStyle = '#888';
ctx.font = '16px sans-serif';
ctx.textAlign = 'center';
ctx.fillText('请选择一个逻辑图', W/2, H/2);
return;
}
var g = logicGraphs[parseInt(sel.value)];
if (!g) return;
// 使用文件中保存的 x,y 坐标(与编辑器布局一致)
var viewNodes = [];
var bMinX = Infinity, bMaxX = -Infinity, bMinY = Infinity, bMaxY = -Infinity;
for (var vi = 0; vi < g.nodes.length; vi++) {
var vn = g.nodes[vi];
var vx = vn.x, vy = vn.y;
if (vx < bMinX) bMinX = vx; if (vx > bMaxX) bMaxX = vx;
if (vy < bMinY) bMinY = vy; if (vy > bMaxY) bMaxY = vy;
var vsig = findSignalByNo(vn.id);
viewNodes.push({
type: vn.type, id: vn.id, _lx: vx, _ly: vy,
saddr: (vsig && vsig.saddr) || '', desc: (vsig && vsig.desc) || '', no: vn.id
});
}
// 首次绘制 / resize 后自动适配缩放
if (viewAutoFit) {
viewAutoFit = false;
var cw2 = bMaxX - bMinX, ch2 = bMaxY - bMinY;
if (cw2 <= 0) cw2 = 800; if (ch2 <= 0) ch2 = 600;
var pad = 80;
viewScale = Math.min((W - pad * 2) / cw2, (H - pad * 2) / ch2, 1.2);
viewPanX = W / 2 - (bMinX + cw2 / 2) * viewScale;
viewPanY = H / 2 - (bMinY + ch2 / 2) * viewScale;
}
// 链路 —— 修复:首个命中而非最后命中(避免同名门 ID 歧义)
var viewLinks = [];
for (var vlj = 0; vlj < g.links.length; vlj++) {
var vl = g.links[vlj];
var vls = -1, vld = -1;
for (var vlk = 0; vlk < g.nodes.length; vlk++) {
var vln = g.nodes[vlk];
if (vls < 0 && vln.type === vl.src_type && (vln.type <= 1 ? vln.id === vl.src_id : (vln.id & 0xFF) === (vl.src_id & 0xFF))) vls = vlk;
if (vld < 0 && vln.type === vl.dst_type && (vln.type <= 1 ? vln.id === vl.dst_id : (vln.id & 0xFF) === (vl.dst_id & 0xFF))) vld = vlk;
}
if (vls >= 0 && vld >= 0) viewLinks.push({ srcIdx: vls, dstIdx: vld, dstPin: vl.dstPin || 0 });
}
// 网格
ctx.strokeStyle = 'rgba(255,255,255,0.04)';
ctx.lineWidth = 1;
var gridSize = 50;
var startGx = Math.floor(-viewPanX / viewScale / gridSize) * gridSize;
var startGy = Math.floor(-viewPanY / viewScale / gridSize) * gridSize;
ctx.save();
ctx.translate(viewPanX, viewPanY);
ctx.scale(viewScale, viewScale);
for (var gx = startGx - gridSize; gx < startGx + W/viewScale + gridSize; gx += gridSize) {
ctx.beginPath(); ctx.moveTo(gx, startGy - gridSize); ctx.lineTo(gx, startGy + H/viewScale + gridSize); ctx.stroke();
}
for (var gy = startGy - gridSize; gy < startGy + H/viewScale + gridSize; gy += gridSize) {
ctx.beginPath(); ctx.moveTo(startGx - gridSize, gy); ctx.lineTo(startGx + W/viewScale + gridSize, gy); ctx.stroke();
}
drawGraphContent(ctx, viewNodes, viewLinks, -1, -1, null);
ctx.restore();
}
function calcLayout(g) {
// 自动布局:每条链路 (输入→运算→输出) 构成一条横向数据流链
// 节点类型常量复用的全局 NODE_INFO
var layout = {};
// 1. 构建邻接关系,按连线确定连通分量(每条链)
var adjOut = {}; for (var i = 0; i < g.nodes.length; i++) adjOut[i] = [];
var adjIn = {}; for (var i = 0; i < g.nodes.length; i++) adjIn[i] = [];
for (var j = 0; j < g.links.length; j++) {
var l = g.links[j];
var si = -1, di = -1;
for (var k = 0; k < g.nodes.length; k++) {
var n = g.nodes[k];
if (si < 0 && n.type === l.src_type && (n.type <= 1 ? n.id === l.src_id : (n.id & 0xFF) === (l.src_id & 0xFF))) si = k;
if (di < 0 && n.type === l.dst_type && (n.type <= 1 ? n.id === l.dst_id : (n.id & 0xFF) === (l.dst_id & 0xFF))) di = k;
}
if (si >= 0 && di >= 0) { adjOut[si].push(di); adjIn[di].push(si); }
}
// BFS 找连通分量
var visited = {};
var chains = [];
for (var idx = 0; idx < g.nodes.length; idx++) {
if (visited[idx]) continue;
var chain = [];
var queue = [idx];
visited[idx] = true;
while (queue.length > 0) {
var cur = queue.shift();
chain.push(cur);
var nbrs = adjOut[cur].concat(adjIn[cur]);
for (var nb = 0; nb < nbrs.length; nb++) {
if (!visited[nbrs[nb]]) { visited[nbrs[nb]] = true; queue.push(nbrs[nb]); }
}
}
chains.push(chain);
}
// 2. 计算每条链的实际竖向高度(含节点+间距)
var NODE_IO_H = 80; // 输入/输出节点高度
var NODE_GATE_H = 66; // 逻辑门节点高度
var ITEM_GAP = 28; // 同列节点纵向间距
function calcChainPixels(chain) {
var cin = 0, cg = 0, cout = 0;
for (var i = 0; i < chain.length; i++) {
var nd = g.nodes[chain[i]];
if (nd.type === NT_INPUT) cin++;
else if (nd.type === NT_OUTPUT) cout++;
else cg++;
}
// 每列独立高度
var hIn = cin > 0 ? cin * NODE_IO_H + (cin - 1) * ITEM_GAP : 0;
var hGate = cg > 0 ? cg * NODE_GATE_H + (cg - 1) * ITEM_GAP : 0;
var hOut = cout > 0 ? cout * NODE_IO_H + (cout - 1) * ITEM_GAP : 0;
return Math.max(hIn, hGate, hOut, 60); // 至少 60px 避免空链过扁
}
var CHAIN_GAP = 80; // 链间纵向间距
// 累计 Y 偏移
var chainHeights = [];
var totalH = 0;
for (var ci = 0; ci < chains.length; ci++) {
var h = calcChainPixels(chains[ci]);
chainHeights.push(h);
totalH += h;
}
totalH += (chains.length - 1) * CHAIN_GAP;
// 使链条在 Y 轴居中:startY = -(totalH / 2) + (firstHalfHeight/2)
var startY = -totalH / 2;
var curY = startY;
// 3. 列布局 (输入 | 运算 | 输出)
var COL_INPUT = -420;
var COL_GATE = 0;
var COL_OUTPUT = 420;
for (var ci2 = 0; ci2 < chains.length; ci2++) {
var chain = chains[ci2];
var ch = chainHeights[ci2];
var cy = curY + ch / 2; // 链中心 Y
// 分类节点
var cInputs = [], cGates = [], cOutputs = [];
for (var n = 0; n < chain.length; n++) {
var nd = g.nodes[chain[n]];
if (nd.type === NT_INPUT) cInputs.push(chain[n]);
else if (nd.type === NT_OUTPUT) cOutputs.push(chain[n]);
else cGates.push(chain[n]);
}
// 计算每列起始 Y(使该列节点在链高度内居中)
function colTop(count, itemH) {
if (count === 0) return cy;
var colH = count * itemH + (count - 1) * ITEM_GAP;
return cy - colH / 2 + itemH / 2;
}
var topIn = colTop(cInputs.length, NODE_IO_H);
var topGate = colTop(cGates.length, NODE_GATE_H);
var topOut = colTop(cOutputs.length, NODE_IO_H);
for (var a = 0; a < cInputs.length; a++)
layout[cInputs[a]] = { x: COL_INPUT, y: topIn + a * (NODE_IO_H + ITEM_GAP) };
for (var b = 0; b < cGates.length; b++)
layout[cGates[b]] = { x: COL_GATE, y: topGate + b * (NODE_GATE_H + ITEM_GAP) };
for (var c = 0; c < cOutputs.length; c++)
layout[cOutputs[c]] = { x: COL_OUTPUT, y: topOut + c * (NODE_IO_H + ITEM_GAP) };
curY += ch + CHAIN_GAP;
}
// 暴露边界信息供 canvas 自适应
layout._bounds = {
minX: COL_INPUT - 130, maxX: COL_OUTPUT + 130,
minY: startY - 40, maxY: startY + totalH + 40
};
return layout;
}
/* ---- 通用绘制函数(编辑器 + 查看器共用) ---- */
function drawGraphContent(ctx, nodes, links, selectedIdx, selectedLinkIdx, wiring) {
// 节点渲染尺寸常量
var IO_W = 250, IO_H = 80; // 输入/输出
var GATE_W = 170, GATE_H = 66; // 逻辑门
var CORNER_R = 12;
var SHADOW_OFF = 3;
var LEFT_BAR_W = 8;
// ---- 连线层 ----
for (var i = 0; i < links.length; i++) {
var l = links[i];
if (l.srcIdx >= nodes.length || l.dstIdx >= nodes.length) continue;
var sn = nodes[l.srcIdx], dn = nodes[l.dstIdx];
var srcIsIO = (sn.type === NT_OUTPUT || sn.type === NT_INPUT);
var dstIsIO = (dn.type === NT_OUTPUT || dn.type === NT_INPUT);
var dstIsSRRS = (dn.type === NT_SR || dn.type === NT_RS);
var sW = srcIsIO ? IO_W : GATE_W, dW = dstIsIO ? IO_W : GATE_W;
var dH = dstIsIO ? IO_H : (dstIsSRRS ? 86 : GATE_H);
var x1 = sn._lx + sW/2;
var y1 = sn._ly;
var dstInP = getNodeInPins(dn);
var effPinIdx = (l.dstPin !== undefined) ? l.dstPin : 0;
var y2 = dn._ly - dH/2 + dH * (effPinIdx + 1) / (dstIsSRRS ? 3 : (dstInP + 1));
var x2 = dn._lx - dW/2;
var isActive = wiring && wiring.fromIdx === l.srcIdx;
var isLinkSelected = (selectedLinkIdx === i);
var midX = (x1 + x2) / 2;
var lineAlpha = (isActive || isLinkSelected) ? 1 : 0.6;
var lineW = (isActive || isLinkSelected) ? 3 : 2;
var lineColor = isLinkSelected ? '#FF5722' : (isActive ? '#FFC107' : 'rgba(180,200,220,' + lineAlpha + ')');
// 发光效果 (阴影线)
ctx.beginPath();
ctx.strokeStyle = isLinkSelected ? 'rgba(255,87,34,0.3)' : 'rgba(255,200,0,' + (lineAlpha * 0.2) + ')';
ctx.lineWidth = lineW + 4;
ctx.moveTo(x1, y1);
ctx.bezierCurveTo(midX, y1, midX, y2, x2, y2);
ctx.stroke();
// 主线
ctx.beginPath();
ctx.strokeStyle = lineColor;
ctx.lineWidth = lineW;
ctx.moveTo(x1, y1);
ctx.bezierCurveTo(midX, y1, midX, y2, x2, y2);
ctx.stroke();
// 箭头 (指向目标)
ctx.fillStyle = lineColor;
ctx.beginPath();
ctx.moveTo(x2, y2);
ctx.lineTo(x2 - 7, y2 - 3.5);
ctx.lineTo(x2 - 7, y2 + 3.5);
ctx.closePath();
ctx.fill();
// 选中连线删除按钮标记(×)
if (isLinkSelected) {
var labelX = (x1 + x2) / 2, labelY = (y1 + y2) / 2 - 18;
ctx.fillStyle = '#FF5722';
ctx.font = 'bold 14px sans-serif';
ctx.textAlign = 'center';
ctx.textBaseline = 'middle';
ctx.fillText('✕ 右键删除', labelX, labelY);
}
}
// 临时连线(仅编辑器)
if (wiring && wiring.fromIdx < nodes.length) {
var fn = nodes[wiring.fromIdx];
var fnIsIO = (fn.type === NT_OUTPUT || fn.type === NT_INPUT);
var fW = fnIsIO ? IO_W : GATE_W;
var fx = (wiring.fromSide === 'out') ? fn._lx + fW/2 : fn._lx - fW/2;
var fy = fn._ly;
ctx.beginPath();
ctx.strokeStyle = '#FFD700';
ctx.lineWidth = 2;
ctx.setLineDash([6, 4]);
ctx.moveTo(fx, fy);
ctx.lineTo(fx + (wiring.fromSide === 'out' ? 100 : -100), fy);
ctx.stroke();
ctx.setLineDash([]);
}
// ---- 节点层 ----
for (var j = 0; j < nodes.length; j++) {
var n = nodes[j];
var info = NODE_INFO[n.type] || { name: '?', color: '#555', icon: '?' };
var isIO = (n.type === NT_OUTPUT || n.type === NT_INPUT);
var inP = getNodeInPins(n);
var nw = isIO ? IO_W : GATE_W;
var nh = isIO ? IO_H : (GATE_H + Math.max(0, inP - 2) * 14);
var x = n._lx, y = n._ly;
var halfW = nw/2, halfH = nh/2;
var isWiringSrc = wiring && wiring.fromIdx === j;
var isSelected = (selectedIdx === j);
// 发光描边宽度
var glowW = (isSelected || isWiringSrc) ? 3 : 1;
// --- 阴影 ---
ctx.fillStyle = 'rgba(0,0,0,0.5)';
ctx.beginPath();
ctx.roundRect(x - halfW + SHADOW_OFF, y - halfH + SHADOW_OFF, nw, nh, CORNER_R);
ctx.fill();
// --- 主体渐变 ---
var grad = ctx.createLinearGradient(x - halfW, y - halfH, x - halfW, y + halfH);
if (isSelected) {
grad.addColorStop(0, '#3b3b6b');
grad.addColorStop(1, '#282848');
} else if (isWiringSrc) {
grad.addColorStop(0, '#3c3c50');
grad.addColorStop(1, '#252538');
} else {
grad.addColorStop(0, '#2e2e4a');
grad.addColorStop(1, '#1e1e36');
}
ctx.fillStyle = grad;
ctx.beginPath();
ctx.roundRect(x - halfW, y - halfH, nw, nh, CORNER_R);
ctx.fill();
// --- 描边 ---
ctx.strokeStyle = isSelected ? '#FFD700' : (isWiringSrc ? '#FFC107' : info.color);
ctx.lineWidth = glowW;
ctx.beginPath();
ctx.roundRect(x - halfW, y - halfH, nw, nh, CORNER_R);
ctx.stroke();
// --- 左侧色条 ---
ctx.fillStyle = info.color;
ctx.beginPath();
ctx.roundRect(x - halfW, y - halfH, LEFT_BAR_W, nh, [CORNER_R, 0, 0, CORNER_R]);
ctx.fill();
// --- 顶部类型标签 ---
if (info.name) {
var labelBgX = x - halfW + LEFT_BAR_W;
var labelBgW = info.name.length * 7.5 + 18;
var labelBgY = y - halfH + 2;
var labelBgH = 16;
ctx.fillStyle = info.color;
ctx.globalAlpha = 0.25;
ctx.fillRect(labelBgX, labelBgY, Math.min(labelBgW, nw - LEFT_BAR_W - 4), labelBgH);
ctx.globalAlpha = 1;
ctx.fillStyle = isIO ? '#fff' : info.color;
ctx.font = '9px -apple-system, sans-serif';
ctx.textAlign = 'left';
ctx.textBaseline = 'top';
ctx.fillText(info.name, labelBgX + 4, labelBgY + 2.5);
}
// --- 文本内容 ---
ctx.textAlign = 'center';
ctx.textBaseline = 'middle';
if (isIO) {
var decId = n.id;
var sig = n.saddr ? n : findSignalByNo(decId);
var prefix = (n.type === NT_INPUT) ? '📥' : '📤';
var roleLabel = (n.type === NT_INPUT) ? '输入点' : '输出点';
// ID 行
ctx.fillStyle = '#e0e0e0';
ctx.font = 'bold 12px -apple-system, sans-serif';
ctx.fillText(prefix + ' ' + roleLabel + ' #' + decId, x, y - 18);
// saddr 行
ctx.fillStyle = '#aaa';
ctx.font = '10px monospace';
var saddrText = (sig && sig.saddr) ? sig.saddr : '—';
ctx.fillText(saddrText, x, y);
// desc 行
var descText = (sig && sig.desc) ? sig.desc : '';
if (descText) {
ctx.fillStyle = '#888';
ctx.font = '9px -apple-system, sans-serif';
var truncated = descText.length > 18 ? descText.substring(0, 16) + '…' : descText;
ctx.fillText(truncated, x, y + 14);
}
} else {
// 逻辑门
var gateIdx = n.id & 0xFF;
var realIn = 0, realOut = 0;
for (var li = 0; li < links.length; li++) {
if (links[li].dstIdx === j) realIn++;
if (links[li].srcIdx === j) realOut++;
}
// 中部:名称 + 序号
ctx.fillStyle = '#e8e8e8';
ctx.font = 'bold 11px -apple-system, sans-serif';
var titleText = info.icon + ' #' + gateIdx;
if (n.type === NT_62P || n.type === NT_62D) {
titleText += ' (' + (n.delayMs || 1000) + 'ms)';
}
ctx.fillText(titleText, x, y - 6);
// 连接数 / 延时信息
ctx.fillStyle = '#999';
ctx.font = '9px monospace';
if (n.type === NT_SR || n.type === NT_RS) {
ctx.fillText(realIn + '入 ' + realOut + '出', x, y + 14);
} else {
ctx.fillText(realIn + '入 ' + realOut + '出', x, y + 14);
}
}
// --- 输入引脚 ---
if (inP > 0) {
var isSRRS = (n.type === NT_SR || n.type === NT_RS);
var renderPins = isSRRS ? 2 : (isIO ? inP : 1);
for (var p = 0; p < renderPins; p++) {
var py = y - nh/2 + nh * (p + 1) / (isSRRS ? 3 : (inP + 1));
// 外圈
ctx.fillStyle = '#2e2e4a';
ctx.beginPath();
ctx.arc(x - halfW, py, 7, 0, Math.PI * 2);
ctx.fill();
ctx.strokeStyle = isSRRS ? (p === 0 ? '#FFC107' : '#FF5722') : '#4CAF50';
ctx.lineWidth = 2;
ctx.stroke();
// 内芯
ctx.fillStyle = isSRRS ? (p === 0 ? '#FFC107' : '#FF5722') : '#4CAF50';
ctx.beginPath();
ctx.arc(x - halfW, py, 3.5, 0, Math.PI * 2);
ctx.fill();
// SR/RS 引脚标签
if (isSRRS) {
ctx.fillStyle = '#fff';
ctx.font = 'bold 10px monospace';
ctx.textAlign = 'right';
ctx.textBaseline = 'middle';
var srl = (n.type === NT_SR) ? ['S', 'R'] : ['R', 'S'];
ctx.fillText(srl[p], x - halfW - 12, py);
}
}
}
// --- 输出引脚 ---
if (info.outPins > 0) {
// 外圈
ctx.fillStyle = '#2e2e4a';
ctx.beginPath();
ctx.arc(x + halfW, y, 7, 0, Math.PI * 2);
ctx.fill();
ctx.strokeStyle = '#FF9800';
ctx.lineWidth = 2;
ctx.stroke();
// 内芯
ctx.fillStyle = '#FF9800';
ctx.beginPath();
ctx.arc(x + halfW, y, 3.5, 0, Math.PI * 2);
ctx.fill();
}
}
}
function renderEditorTab(container) {
editNodes = [];
editLinks = [];
editKey = '';
editName = '';
editOnlineCfgId = '';
editSelectedNode = -1;
editSelectedLink = -1;
editWiring = null;
editDragNode = null;
editPanX = 0; editPanY = 0;
editScale = 1.0;
editLoadedFromFile = false;
editNextGateIdx = {};
container.innerHTML = ''
+ ''
+ ''
+ ''
+ ''
+ ''
+ ''
+ ''
+ ''
+ ''
+ ''
+ ''
+ ''
+ '
'
+ ''
+ ''
+ '💡 双击模块可修改配置 | 单击选中/引脚连线 | 右键删除连线 | 滚轮缩放 | 按住空白拖动画布'
+ '
';
// 工具栏事件
document.getElementById('plc-edit-key').oninput = function() { editKey = this.value; };
document.getElementById('plc-edit-name').oninput = function() { editName = this.value; };
document.getElementById('plc-edit-add-in').onclick = function() { showAddInputModal(); };
document.getElementById('plc-edit-add-out').onclick = function() { showAddOutputModal(); };
document.getElementById('plc-edit-add-gate').onclick = function() { showAddGateModal(); };
document.getElementById('plc-edit-zoomin').onclick = function() { editScale = Math.min(3, editScale * 1.2); drawEditor(); };
document.getElementById('plc-edit-zoomout').onclick = function() { editScale = Math.max(0.2, editScale / 1.2); drawEditor(); };
document.getElementById('plc-edit-del').onclick = function() { deleteSelectedNode(); };
document.getElementById('plc-edit-modify').onclick = function() { modifySelectedNode(); };
document.getElementById('plc-edit-generate').onclick = function() { generateAndSaveConfig(); };
setupEditorCanvas();
drawEditor();
// 加载当前 Reclose_logic.xml 为编辑起点
function initEditor() {
loadLogicFile(function() {
if (logicGraphs.length > 0) {
// 优先使用非 bak 文件的最新配置
var g = null;
for (var gi = 0; gi < logicGraphs.length; gi++) {
if (!logicGraphs[gi]._source || logicGraphs[gi]._source.indexOf('.bak') < 0) {
g = logicGraphs[gi]; break;
}
}
if (!g && logicGraphs.length > 0) g = logicGraphs[0];
if (g) {
editKey = g.key;
editName = g.name || '';
editOnlineCfgId = onlineCfgId;
// 复制节点到编辑器
for (var i = 0; i < g.nodes.length; i++) {
var n = g.nodes[i];
var newNode = {
type: n.type, id: n.id, x: n.x, y: n.y,
_lx: n.x, _ly: n.y,
saddr: '', desc: '', no: n.id
};
// 查找信号信息
var sig = findSignalByNo(n.id);
if (sig) { newNode.saddr = sig.saddr; newNode.desc = sig.desc; }
// 逻辑门 inPins — 优先使用文件中保存的配置
if (n.type !== NT_INPUT && n.type !== NT_OUTPUT) {
newNode.inPins = (n.inPins !== undefined) ? n.inPins : ((NODE_INFO[n.type] || {}).inPins || 2);
}
// 延时字段
if (n.delayMs !== undefined) newNode.delayMs = n.delayMs;
// Comb id 和 RS 信息
if (n.combId !== undefined) newNode.combId = n.combId;
if (n.rsR) newNode.rsR = n.rsR;
if (n.rsS) newNode.rsS = n.rsS;
editNodes.push(newNode);
}
// 复制链路(按节点在新数组中的索引)
for (var j = 0; j < g.links.length; j++) {
var l = g.links[j];
var si = findEditNodeIndex(l.src_type, l.src_id);
var di = findEditNodeIndex(l.dst_type, l.dst_id);
if (si >= 0 && di >= 0) {
var el = { srcIdx: si, dstIdx: di };
if (l.dstPin !== undefined) el.dstPin = l.dstPin;
editLinks.push(el);
}
}
editLoadedFromFile = true;
// 按类型统计最大序号,后续新增时每种类型独立递增
var maxByType = {};
for (var di = 0; di < editNodes.length; di++) {
var dn = editNodes[di];
if (dn.type === NT_INPUT || dn.type === NT_OUTPUT) continue;
var idx = dn.id & 0xFF;
if (idx >= (maxByType[dn.type] || -1)) maxByType[dn.type] = idx;
}
for (var t in maxByType) {
editNextGateIdx[t] = maxByType[t] + 1;
}
// 消除已有的重复 type+id 并基于每类型独立序号分配新 ID
var seenIds = {};
for (var di = 0; di < editNodes.length; di++) {
var dn = editNodes[di];
if (dn.type === NT_INPUT || dn.type === NT_OUTPUT) continue;
var key2 = dn.type + ':' + (dn.id & 0xFF);
if (seenIds[key2]) {
var next = editNextGateIdx[dn.type] || 0;
editNodes[di].id = (dn.id & ~0xFF) | next;
editNextGateIdx[dn.type] = next + 1;
} else {
seenIds[key2] = true;
}
}
// 更新输入框
var keyEl = document.getElementById('plc-edit-key');
var nameEl = document.getElementById('plc-edit-name');
if (keyEl) keyEl.value = editKey;
if (nameEl) nameEl.value = editName;
} else {
editKey = String(Date.now());
editName = '';
var keyEl = document.getElementById('plc-edit-key');
if (keyEl) keyEl.value = editKey;
}
drawEditor();
}
});
}
if (dcOutSignals.length === 0) {
loadDcOutSignals(function() { initEditor(); });
} else {
initEditor();
}
}
function findEditNodeIndex(type, id) {
for (var i = 0; i < editNodes.length; i++) {
var n = editNodes[i];
if (n.type === type) {
if (type <= 1 && n.id === id) return i;
if (type > 1 && (n.id & 0xFF) === (id & 0xFF)) return i;
}
}
return -1;
}
function deleteSelectedNode() {
if (editSelectedNode < 0 || editSelectedNode >= editNodes.length) {
showToast('请先在画布上双击或单击选中一个模块', 'err');
return;
}
var node = editNodes[editSelectedNode];
var idx = editSelectedNode;
var label = getNodeDisplayLabel(node);
var html = ''
+ '
🗑 确认删除
'
+ '
确定删除 ' + escHtml(label) + ' 及其所有连线?
'
+ '
'
+ ''
+ ''
+ '
';
var div = document.createElement('div');
div.innerHTML = html;
document.body.appendChild(div.firstElementChild);
document.getElementById('plc-modal-cancel').onclick = closeModal;
document.getElementById('plc-modal-ok').onclick = function() {
closeModal();
var delType = node.type;
editLinks = editLinks.filter(function(l) { return l.srcIdx !== idx && l.dstIdx !== idx; });
for (var i = 0; i < editLinks.length; i++) {
if (editLinks[i].srcIdx > idx) editLinks[i].srcIdx--;
if (editLinks[i].dstIdx > idx) editLinks[i].dstIdx--;
}
editNodes.splice(idx, 1);
editSelectedNode = -1;
// 压缩同类型门序号:删除 #1 后 #2→#1, #3→#2 …
if (delType !== NT_INPUT && delType !== NT_OUTPUT) {
var sameType = [];
for (var s = 0; s < editNodes.length; s++) {
if (editNodes[s].type === delType) sameType.push(s);
}
sameType.sort(function(a, b) { return (editNodes[a].id & 0xFF) - (editNodes[b].id & 0xFF); });
for (var a = 0; a < sameType.length; a++) {
editNodes[sameType[a]].id = (editNodes[sameType[a]].id & ~0xFF) | a;
}
editNextGateIdx[delType] = sameType.length;
}
drawEditor();
showToast('模块已删除', 'ok');
};
}
function modifySelectedNode() {
if (editSelectedNode < 0 || editSelectedNode >= editNodes.length) {
showToast('请先在画布上双击或单击选中一个模块', 'err');
return;
}
var idx = editSelectedNode;
var node = editNodes[idx];
if (node.type === NT_INPUT || node.type === NT_OUTPUT) {
showRebindSignalModal(idx);
} else if (node.type === NT_AND || node.type === NT_OR || node.type === NT_NOT) {
showGateConfigModal(idx);
} else if (node.type === NT_62P || node.type === NT_62D) {
showDelayEditModal(idx);
} else if (node.type === NT_SR || node.type === NT_RS) {
showRSInfoModal(idx);
} else {
showGateConfigModal(idx);
}
}
// 节点双击 — 根据类型弹出编辑框
function onNodeDoubleClicked(idx) {
var node = editNodes[idx];
editSelectedNode = idx;
drawEditor();
if (node.type === NT_INPUT || node.type === NT_OUTPUT) {
showRebindSignalModal(idx);
} else if (node.type === NT_62P || node.type === NT_62D) {
showDelayEditModal(idx);
} else if (node.type === NT_SR || node.type === NT_RS) {
showRSInfoModal(idx);
} else {
showGateConfigModal(idx);
}
}
/** 延时模块双击:弹出延时编辑框 */
function showDelayEditModal(nodeIdx) {
var node = editNodes[nodeIdx];
var info = NODE_INFO[node.type] || {};
var curMs = node.delayMs || 1000;
var html = ''
+ '
⏱️ ' + info.name + ' — 修改延时
'
+ '
当前延时: ' + curMs + ' ms
'
+ '
'
+ '
'
+ ''
+ ''
+ '
';
var div = document.createElement('div');
div.innerHTML = html;
document.body.appendChild(div.firstElementChild);
document.getElementById('plc-modal-cancel').onclick = closeModal;
document.getElementById('plc-modal-ok').onclick = function() {
var val = parseInt(document.getElementById('plc-delay-input').value);
if (isNaN(val) || val < 0) { showToast('请输入有效的延时毫秒数', 'err'); return; }
editNodes[nodeIdx].delayMs = val;
closeModal();
drawEditor();
showToast('延时已更新: ' + val + ' ms', 'ok');
};
document.getElementById('plc-delay-input').onkeydown = function(e) {
if (e.key === 'Enter') document.getElementById('plc-modal-ok').click();
};
setTimeout(function() {
var inp = document.getElementById('plc-delay-input');
if (inp) { inp.focus(); inp.select(); }
}, 100);
}
/** SR/RS 双击:显示 R/S 连接信息 */
function showRSInfoModal(nodeIdx) {
var node = editNodes[nodeIdx];
var info = NODE_INFO[node.type] || {};
var sSrc = '', rSrc = '';
for (var li = 0; li < editLinks.length; li++) {
var l = editLinks[li];
if (l.dstIdx === nodeIdx) {
var srcNode = editNodes[l.srcIdx];
var desc = getEditorNodeLabel(srcNode).replace(/\n/g, ' ');
if (node.type === NT_SR) {
if (l.dstPin === 0) sSrc = desc; else rSrc = desc;
} else {
if (l.dstPin === 1) sSrc = desc; else rSrc = desc;
}
}
}
var html = ''
+ '
🔒 ' + info.name + ' — 连接信息
'
+ '
S (置位): ' + (sSrc || '未连接') + '
'
+ '
R (复位): ' + (rSrc || '未连接') + '
'
+ '
💡 拖动输出引脚到目标节点的 S/R 引脚来连线
'
+ '
'
+ '
';
var div = document.createElement('div');
div.innerHTML = html;
document.body.appendChild(div.firstElementChild);
document.getElementById('plc-modal-cancel').onclick = closeModal;
}
// 重新绑定 I/O 节点信号
function showRebindSignalModal(nodeIdx) {
var node = editNodes[nodeIdx];
var signals = dcOutSignals;
if (signals.length === 0) { showToast('信号列表为空,请先加载', 'err'); return; }
var title = (node.type === NT_INPUT) ? '📥 重新绑定输入信号' : '📤 重新绑定输出信号';
var html = ''
+ '
' + title + '
'
+ '
当前: ' + escHtml(getNodeDisplayLabel(node)) + '
'
+ '
'
+ '
';
for (var j = 0; j < signals.length; j++) {
var s = signals[j];
var isCurrent = (node.no === s.no);
html += '
'
+ '#' + s.no + ' ' + escHtml(s.saddr) + ''
+ ' ' + escHtml(s.desc) + ''
+ (isCurrent ? '✓ 当前' : '')
+ '
';
}
html += '
';
showModalCustom(html, function(no, saddr, desc) {
var sig = findSignalByNo(no);
editNodes[nodeIdx].no = no;
editNodes[nodeIdx].id = no;
editNodes[nodeIdx].saddr = saddr;
editNodes[nodeIdx].desc = desc;
drawEditor();
showToast('已绑定: ' + saddr, 'ok');
return true;
});
}
// 配置门电路引脚数
function showGateConfigModal(nodeIdx) {
var node = editNodes[nodeIdx];
var curPins = getNodeInPins(node);
var info = NODE_INFO[node.type] || {};
var maxPins = info.inMax || 99;
var minPins = 1;
if (node.type === NT_AND || node.type === NT_OR) minPins = 1;
else if (node.type === NT_NOT) minPins = 1;
else minPins = 1;
var html = ''
+ '
' + info.icon + ' ' + info.name + ' 配置
'
+ '
当前输入节点数: ' + curPins + '
'
+ '
'
+ '
'
+ '
'
+ ''
+ ''
+ '
';
var div = document.createElement('div');
div.innerHTML = html;
document.body.appendChild(div.firstElementChild);
document.getElementById('plc-modal-cancel').onclick = closeModal;
document.getElementById('plc-modal-save').onclick = function() {
var v = parseInt(document.getElementById('plc-gate-pins').value);
if (isNaN(v) || v < minPins) { showToast('最少 ' + minPins + ' 个输入引脚', 'err'); return; }
if (v > maxPins) { showToast('最多 ' + maxPins + ' 个输入引脚', 'err'); return; }
editNodes[nodeIdx].inPins = v;
closeModal();
drawEditor();
showToast(info.name + ' 输入引脚已设为 ' + v, 'ok');
};
}
/* ---- 编辑器 Canvas ---- */
function setupEditorCanvas() {
var wrap = document.getElementById('plc-edit-canvas-wrap');
var canvas = document.getElementById('plc-edit-canvas');
if (!wrap || !canvas) return;
function resize() {
canvas.width = wrap.clientWidth;
canvas.height = Math.max(500, wrap.clientHeight - 4);
// 居中画布
editScale = 0.7;
editPanX = canvas.width / 2;
editPanY = canvas.height / 2;
drawEditor();
}
setTimeout(resize, 50);
window.addEventListener('resize', function() { resize(); });
canvas.onmousedown = function(e) {
var rect = canvas.getBoundingClientRect();
var mx = (e.clientX - rect.left - editPanX) / editScale;
var my = (e.clientY - rect.top - editPanY) / editScale;
// 检查是否点击了引脚 → 连线操作
var pinHit = hitTestPin(mx, my);
if (pinHit) {
if (editWiring) {
// 完成连线
if (editWiring.fromIdx !== pinHit.nodeIdx) {
if (editWiring.fromSide === 'out' && pinHit.side === 'in') {
var srcInfo = NODE_INFO[editNodes[editWiring.fromIdx].type] || {};
var outMax = srcInfo.outMax || 99;
var outCount = 0;
for (var lc = 0; lc < editLinks.length; lc++) {
if (editLinks[lc].srcIdx === editWiring.fromIdx) outCount++;
}
if (outCount >= outMax) {
showToast('该模块输出已达上限 (' + outMax + ')', 'err');
editWiring = null;
editSelectedNode = -1;
drawEditor();
return;
}
var link = { srcIdx: editWiring.fromIdx, dstIdx: pinHit.nodeIdx, dstPin: pinHit.pinIdx };
// SR/RS 每个引脚只能接一根线
var dstNode = editNodes[pinHit.nodeIdx];
if (dstNode.type === NT_SR || dstNode.type === NT_RS) {
for (var lc2 = 0; lc2 < editLinks.length; lc2++) {
if (editLinks[lc2].dstIdx === pinHit.nodeIdx && editLinks[lc2].dstPin === pinHit.pinIdx) {
var pinLabel = (dstNode.type === NT_SR) ? ['S','R'] : ['R','S'];
showToast('引脚 ' + pinLabel[pinHit.pinIdx] + ' 已有连线,每个引脚只能接一根线', 'err');
editWiring = null;
editSelectedNode = -1;
drawEditor();
return;
}
}
}
editLinks.push(link);
showToast('连线已添加', 'ok');
} else if (editWiring.fromSide === 'in' && pinHit.side === 'out') {
editLinks.push({ srcIdx: pinHit.nodeIdx, dstIdx: editWiring.fromIdx, dstPin: 0 });
showToast('连线已添加', 'ok');
} else {
showToast('不能这样连线', 'err');
}
}
editWiring = null;
editSelectedNode = -1;
editSelectedLink = -1;
drawEditor();
return;
} else {
editWiring = { fromIdx: pinHit.nodeIdx, fromSide: pinHit.side, fromPin: pinHit.pinIdx };
editSelectedNode = -1;
editSelectedLink = -1;
drawEditor();
return;
}
}
// 检查是否点击了连线 → 选中高亮(左键选择,右键删除)
var linkHit = hitTestLink(mx, my);
if (linkHit >= 0) {
editSelectedLink = linkHit;
editSelectedNode = -1;
editDragNode = null;
if (editWiring) { editWiring = null; }
drawEditor();
return;
}
// 检查是否点击了节点 → 拖拽或选中
var nodeHit = hitTestNode(mx, my);
if (nodeHit >= 0) {
editSelectedNode = nodeHit;
editSelectedLink = -1;
editDragNode = nodeHit;
editDragOffX = mx - editNodes[nodeHit]._lx;
editDragOffY = my - editNodes[nodeHit]._ly;
drawEditor();
return;
}
// 空白处取消选中和连线,并开始画布拖拽
editSelectedNode = -1;
editSelectedLink = -1;
if (editWiring) { editWiring = null; }
editDragCanvas = { startX: e.clientX, startY: e.clientY, panX: editPanX, panY: editPanY };
canvas.style.cursor = 'grabbing';
drawEditor();
};
canvas.onmousemove = function(e) {
var rect = canvas.getBoundingClientRect();
var mx = (e.clientX - rect.left - editPanX) / editScale;
var my = (e.clientY - rect.top - editPanY) / editScale;
if (editDragCanvas) {
editPanX = editDragCanvas.panX + (e.clientX - editDragCanvas.startX);
editPanY = editDragCanvas.panY + (e.clientY - editDragCanvas.startY);
drawEditor();
return;
}
if (editDragNode !== null) {
editNodes[editDragNode]._lx = mx - editDragOffX;
editNodes[editDragNode]._ly = my - editDragOffY;
drawEditor();
return;
}
if (editWiring && editWiring.fromIdx < editNodes.length) {
drawEditor();
}
var pin = hitTestPin(mx, my);
canvas.style.cursor = pin ? 'crosshair' : (hitTestNode(mx, my) >= 0 ? 'grab' : 'default');
};
canvas.onmouseup = function(e) {
if (editDragCanvas) {
editDragCanvas = null;
canvas.style.cursor = 'default';
return;
}
if (editDragNode !== null) {
var node = editNodes[editDragNode];
var nodeIdx = editDragNode;
var dx = Math.abs((e.clientX - canvas.getBoundingClientRect().left - editPanX) / editScale - node._lx - editDragOffX);
var dy = Math.abs((e.clientY - canvas.getBoundingClientRect().top - editPanY) / editScale - node._ly - editDragOffY);
// 小于 3px 视为点击(非拖拽)
if (dx < 3 && dy < 3) {
// 双击检测:同节点 400ms 内连续点击
var now = Date.now();
if (nodeIdx === editLastClickNode && (now - editLastClickTime) < 400) {
onNodeDoubleClicked(nodeIdx);
editLastClickTime = 0;
editLastClickNode = -1;
} else {
editLastClickTime = now;
editLastClickNode = nodeIdx;
}
}
}
editDragNode = null;
};
canvas.onmouseleave = function() { editDragNode = null; editDragCanvas = null; canvas.style.cursor = 'default'; };
canvas.onwheel = function(e) {
e.preventDefault();
var delta = e.deltaY > 0 ? 0.9 : 1.1;
var rect = canvas.getBoundingClientRect();
var mx = e.clientX - rect.left;
var my = e.clientY - rect.top;
var ns = Math.max(0.2, Math.min(3, editScale * delta));
editPanX = mx - (mx - editPanX) * (ns / editScale);
editPanY = my - (my - editPanY) * (ns / editScale);
editScale = ns;
drawEditor();
};
// 右键 —— 删除选中的连线 / 节点
canvas.oncontextmenu = function(e) {
e.preventDefault();
if (editSelectedLink >= 0) {
editLinks.splice(editSelectedLink, 1);
editSelectedLink = -1;
editSelectedNode = -1;
showToast('连线已删除', 'ok');
drawEditor();
return;
}
// 如果选中了节点也可以用右键删除
if (editSelectedNode >= 0) {
deleteSelectedNode();
return;
}
};
canvas.tabIndex = 0;
canvas.onkeydown = function(e) {
if (e.key === 'Escape') { editWiring = null; editSelectedNode = -1; editSelectedLink = -1; drawEditor(); }
if (e.key === 'Delete' && editSelectedNode >= 0) { deleteSelectedNode(); }
if (e.key === 'Delete' && editSelectedLink >= 0) { editLinks.splice(editSelectedLink, 1); editSelectedLink = -1; showToast('连线已删除', 'ok'); drawEditor(); }
};
}
/* ---- 获取节点的实际 inPins(优先使用节点自身的配置) ---- */
function getNodeInPins(node) {
if (node.inPins !== undefined && node.inPins > 0) return node.inPins;
var info = NODE_INFO[node.type] || {};
return info.inPins || 0;
}
function getNodeSize(node) {
var isIO = (node.type === NT_OUTPUT || node.type === NT_INPUT);
var isSRRS = (node.type === NT_SR || node.type === NT_RS);
var inP = getNodeInPins(node);
var h = isIO ? 80 : (isSRRS ? 86 : (66 + Math.max(0, inP - 2) * 14));
var w = isIO ? 250 : 170;
return { w: w, h: h };
}
function hitTestNode(mx, my) {
for (var i = editNodes.length - 1; i >= 0; i--) {
var n = editNodes[i];
var sz = getNodeSize(n);
var x = n._lx, y = n._ly;
if (mx > x - sz.w/2 && mx < x + sz.w/2 && my > y - sz.h/2 && my < y + sz.h/2) return i;
}
return -1;
}
function hitTestPin(mx, my) {
var pinR = 8;
for (var i = editNodes.length - 1; i >= 0; i--) {
var n = editNodes[i];
var sz = getNodeSize(n);
var info = NODE_INFO[n.type] || NODE_INFO[0];
var x = n._lx, y = n._ly;
var isIO = (n.type === NT_INPUT || n.type === NT_OUTPUT);
var isSRRS = (n.type === NT_SR || n.type === NT_RS);
var inP = getNodeInPins(n);
// 输入引脚(左侧)
if (info.inPins > 0) {
var testPins = isSRRS ? 2 : (isIO ? inP : 1);
for (var p = 0; p < testPins; p++) {
var py = y - sz.h/2 + sz.h * (p + 1) / (isSRRS ? 3 : (inP + 1));
var dx = mx - (x - sz.w/2), dy = my - py;
if (Math.sqrt(dx*dx + dy*dy) <= pinR) return { nodeIdx: i, side: 'in', pinIdx: p };
}
}
// 输出引脚(右侧)
if (info.outPins > 0) {
var dx = mx - (x + sz.w/2), dy = my - y;
if (Math.sqrt(dx*dx + dy*dy) <= pinR) return { nodeIdx: i, side: 'out', pinIdx: 0 };
}
}
return null;
}
function hitTestLink(mx, my) {
for (var i = 0; i < editLinks.length; i++) {
var l = editLinks[i];
if (l.srcIdx >= editNodes.length || l.dstIdx >= editNodes.length) continue;
var sn = editNodes[l.srcIdx], dn = editNodes[l.dstIdx];
var szSrc = getNodeSize(sn), szDst = getNodeSize(dn);
var x1 = sn._lx + szSrc.w/2, y1 = sn._ly;
var dstIsSRRS = (dn.type === NT_SR || dn.type === NT_RS);
var gateInPins = getNodeInPins(dn);
var dstPinIdx2 = (l.dstPin !== undefined) ? l.dstPin : 0;
var y2 = dn._ly - szDst.h/2 + szDst.h * (dstPinIdx2 + 1) / (dstIsSRRS ? 3 : (gateInPins + 1));
var x2 = dn._lx - szDst.w/2;
var t = ((mx - x1)*(x2 - x1) + (my - y1)*(y2 - y1)) / ((x2 - x1)*(x2 - x1) + (y2 - y1)*(y2 - y1));
t = Math.max(0, Math.min(1, t));
var px = x1 + t * (x2 - x1), py = y1 + t * (y2 - y1);
if (Math.sqrt((mx - px)*(mx - px) + (my - py)*(my - py)) < 10) return i;
}
return -1;
}
function getNodeLabel(n) {
var info = NODE_INFO[n.type] || {};
var label = (info.name || '?');
if (n.saddr) label += ' ' + n.saddr;
else if (n.type <= 1) label += ' #' + n.id;
else label += ' #' + (n.id & 0xFF);
return label;
}
/* ---- 编辑器绘制 ---- */
function drawEditor() {
var canvas = document.getElementById('plc-edit-canvas');
if (!canvas) return;
var ctx = canvas.getContext('2d');
var W = canvas.width, H = canvas.height;
ctx.clearRect(0, 0, W, H);
ctx.fillStyle = '#1a1a2e';
ctx.fillRect(0, 0, W, H);
ctx.save();
ctx.translate(editPanX, editPanY);
ctx.scale(editScale, editScale);
// 网格
ctx.strokeStyle = 'rgba(255,255,255,0.05)';
ctx.lineWidth = 1;
var gridSize = 40;
var startX = Math.floor(-editPanX / editScale / gridSize) * gridSize;
var startY = Math.floor(-editPanY / editScale / gridSize) * gridSize;
for (var gx = startX - gridSize; gx < startX + W/editScale + gridSize; gx += gridSize) {
ctx.beginPath(); ctx.moveTo(gx, startY - gridSize); ctx.lineTo(gx, startY + H/editScale + gridSize); ctx.stroke();
}
for (var gy = startY - gridSize; gy < startY + H/editScale + gridSize; gy += gridSize) {
ctx.beginPath(); ctx.moveTo(startX - gridSize, gy); ctx.lineTo(startX + W/editScale + gridSize, gy); ctx.stroke();
}
// 区域标签
ctx.fillStyle = 'rgba(255,255,255,0.08)';
ctx.font = '28px sans-serif';
ctx.textAlign = 'center';
ctx.fillText('📥 输入信号区', -250, -200);
ctx.fillText('🧮 逻辑运算区', 0, -200);
ctx.fillText('📤 输出信号区', 250, -200);
// 共用绘制
drawGraphContent(ctx, editNodes, editLinks, editSelectedNode, editSelectedLink, editWiring);
ctx.restore();
// 更新信息栏
var infoEl = document.getElementById('plc-edit-info');
if (infoEl) {
var selInfo = '';
if (editSelectedNode >= 0 && editSelectedNode < editNodes.length) {
selInfo = ' | 🔍 选中: ' + getNodeDisplayLabel(editNodes[editSelectedNode]);
}
infoEl.innerHTML = '节点: ' + editNodes.length + ' | 连线: ' + editLinks.length
+ ' | Key: ' + escHtml(editKey) + ' | 名称: ' + escHtml(editName || '(未设置)')
+ (editWiring ? ' | 🔗 连线中... (点击目标引脚完成,ESC取消)' : '')
+ selInfo
+ ' | 右键删除连线 | 滚轮缩放 | Delete键删除选中';
}
}
/* ---- 添加输入/输出/运算弹窗 ---- */
function showAddInputModal() {
if (dcOutSignals.length === 0) { showToast('请先加载信号', 'err'); return; }
var signals = dcOutSignals;
var html = ''
+ '
📥 绑定输入信号
'
+ '
'
+ '
';
for (var j = 0; j < signals.length; j++) {
var s = signals[j];
html += '
'
+ '#' + s.no + ' ' + escHtml(s.saddr) + ''
+ ' ' + escHtml(s.desc) + '
';
}
html += '
';
showModalCustom(html, function(no, saddr, desc) {
editNodes.push({
type: NT_INPUT, id: no, x: 0, y: 0,
_lx: -260, _ly: editNodes.filter(function(n){return n.type===NT_INPUT;}).length * 110,
saddr: saddr, desc: desc, no: no
});
drawEditor();
showToast('已添加输入: ' + saddr, 'ok');
return true;
});
}
function showAddOutputModal() {
if (dcOutSignals.length === 0) { showToast('请先加载信号', 'err'); return; }
var signals = dcOutSignals;
var html = ''
+ '
📤 绑定输出信号
'
+ '
'
+ '
';
for (var j = 0; j < signals.length; j++) {
var s = signals[j];
html += '
'
+ '#' + s.no + ' ' + escHtml(s.saddr) + ''
+ ' ' + escHtml(s.desc) + '
';
}
html += '
';
showModalCustom(html, function(no, saddr, desc) {
editNodes.push({
type: NT_OUTPUT, id: no, x: 0, y: 0,
_lx: 260, _ly: editNodes.filter(function(n){return n.type===NT_OUTPUT;}).length * 110,
saddr: saddr, desc: desc, no: no
});
drawEditor();
showToast('已添加输出: ' + saddr, 'ok');
return true;
});
}
function showAddGateModal() {
var gates = [
{ type: NT_AND, name: '与门 (AND)', desc: '2输入1输出,所有输入为1时输出1', icon: '🔷' },
{ type: NT_OR, name: '或门 (OR)', desc: '2输入1输出,任一输入为1时输出1', icon: '🔶' },
{ type: NT_NOT, name: '非门 (NOT)', desc: '1输入1输出,输入取反', icon: '🔺' },
{ type: NT_62P, name: '62p 延时', desc: '输入1延时输出1,输入0立即输出0', icon: '⏱️' },
{ type: NT_62D, name: '62d 延时', desc: '输入1立即输出1,输入0延时输出0', icon: '⏱️' },
{ type: NT_RISING, name: '上升沿', desc: '0→1跳变时输出脉冲', icon: '⬆️' },
{ type: NT_FALLING, name: '下降沿', desc: '1→0跳变时输出脉冲', icon: '⬇️' },
{ type: NT_SR, name: 'SR触发器', desc: 'S置位优先,S=1置位 R=1复位', icon: '🔒' },
{ type: NT_RS, name: 'RS触发器', desc: 'R复位优先,R=1复位 S=1置位', icon: '🔒' }
];
var html = ''
+ '
🧮 选择运算模块
'
+ '
';
for (var i = 0; i < gates.length; i++) {
var g = gates[i];
html += '
'
+ '' + g.icon + ' ' + g.name + ''
+ '
' + g.desc + ''
+ '
';
}
html += '
';
// 自定义 modal 处理
var div = document.createElement('div');
div.innerHTML = html;
document.body.appendChild(div.firstElementChild);
document.getElementById('plc-modal-cancel').onclick = closeModal;
var rows = document.querySelectorAll('#plc-modal .plc-modal-row');
for (var j = 0; j < rows.length; j++) {
rows[j].onclick = function(e) {
e.stopPropagation();
var gateType = parseInt(this.getAttribute('data-gate-type'));
var info = NODE_INFO[gateType];
var nextIdx = editNextGateIdx[gateType] || 0;
// 延时模块:弹出自定义延时输入框
if (gateType === NT_62P || gateType === NT_62D) {
closeModal();
showDelayInputModal(gateType, nextIdx, info);
return;
}
// 非延时模块:直接添加
addGateNode(gateType, nextIdx, 0, info);
editNextGateIdx[gateType] = nextIdx + 1;
closeModal();
drawEditor();
};
}
}
/** 弹出延时毫秒输入框 */
function showDelayInputModal(gateType, nextIdx, info) {
var html = ''
+ '
⏱️ ' + info.name + ' — 延时配置
'
+ '
请输入延时时间(毫秒)
'
+ '
'
+ '
'
+ ''
+ ''
+ '
';
var div = document.createElement('div');
div.innerHTML = html;
document.body.appendChild(div.firstElementChild);
document.getElementById('plc-modal-cancel').onclick = closeModal;
document.getElementById('plc-modal-ok').onclick = function() {
var val = parseInt(document.getElementById('plc-delay-input').value);
if (isNaN(val) || val < 0) { showToast('请输入有效的延时毫秒数', 'err'); return; }
closeModal();
addGateNode(gateType, nextIdx, val, info);
editNextGateIdx[gateType] = nextIdx + 1;
drawEditor();
};
// 回车确认
document.getElementById('plc-delay-input').onkeydown = function(e) {
if (e.key === 'Enter') document.getElementById('plc-modal-ok').click();
};
setTimeout(function() {
var inp = document.getElementById('plc-delay-input');
if (inp) { inp.focus(); inp.select(); }
}, 100);
}
/** 添加门节点到编辑区,自动找空白位置 */
function addGateNode(gateType, nextIdx, delayMs, info) {
var pos = findEmptyPosition();
var node = {
type: gateType, id: nextIdx,
x: 0, y: 0,
_lx: pos.x, _ly: pos.y,
saddr: '', desc: '', no: 0
};
if (delayMs > 0) node.delayMs = delayMs;
editNodes.push(node);
showToast('已添加: ' + (info ? info.name : NODE_INFO[gateType].name) + ' #' + nextIdx, 'ok');
}
/** 寻找画布上最空白的区域 */
function findEmptyPosition() {
if (editNodes.length === 0) return { x: 0, y: 0 };
// 先计算已有节点的包围盒,在右侧偏下寻找空白
var minX = Infinity, maxX = -Infinity, minY = Infinity, maxY = -Infinity;
for (var i = 0; i < editNodes.length; i++) {
var n = editNodes[i];
if (n._lx < minX) minX = n._lx;
if (n._lx > maxX) maxX = n._lx;
if (n._ly < minY) minY = n._ly;
if (n._ly > maxY) maxY = n._ly;
}
// 在所有节点下方居中放置
var candidates = [
{ x: 0, y: maxY + 140 },
{ x: 0, y: minY - 140 },
{ x: maxX + 300, y: (minY + maxY) / 2 },
{ x: minX - 300, y: (minY + maxY) / 2 }
];
for (var c = 0; c < candidates.length; c++) {
if (!isPositionOccupied(candidates[c].x, candidates[c].y)) {
return candidates[c];
}
}
return { x: 0, y: maxY + 140 };
}
function isPositionOccupied(x, y) {
for (var i = 0; i < editNodes.length; i++) {
var n = editNodes[i];
if (Math.abs(n._lx - x) < 120 && Math.abs(n._ly - y) < 80) return true;
}
return false;
}
function showModal(html, onSelect) {
var div = document.createElement('div');
div.innerHTML = html;
document.body.appendChild(div.firstElementChild);
document.getElementById('plc-modal-cancel').onclick = closeModal;
// 搜索
var searchInput = document.getElementById('plc-modal-search');
if (searchInput) {
searchInput.oninput = function() {
var q = this.value.toLowerCase();
var rows = document.querySelectorAll('#plc-modal .plc-modal-row');
for (var r = 0; r < rows.length; r++) {
var text = (rows[r].textContent || '').toLowerCase();
rows[r].style.display = (text.indexOf(q) !== -1) ? '' : 'none';
}
};
}
var rows = document.querySelectorAll('#plc-modal .plc-modal-row');
for (var j = 0; j < rows.length; j++) {
rows[j].onclick = function() {
if (this.classList.contains('plc-modal-used')) {
showToast('该信号已被使用', 'err');
return;
}
var noAttr = this.getAttribute('data-no');
var saddr = this.getAttribute('data-saddr');
var desc = this.getAttribute('data-desc') || '';
var no = parseInt(noAttr) || 0;
var result = onSelect(no, saddr, desc);
if (result !== false) closeModal();
};
}
}
function showModalCustom(html, onSelect) {
var div = document.createElement('div');
div.innerHTML = html;
document.body.appendChild(div.firstElementChild);
document.getElementById('plc-modal-cancel').onclick = closeModal;
var searchInput = document.getElementById('plc-modal-search');
if (searchInput) {
searchInput.oninput = function() {
var q = this.value.toLowerCase();
var rows = document.querySelectorAll('#plc-modal .plc-modal-row');
for (var r = 0; r < rows.length; r++) {
var text = (rows[r].textContent || '').toLowerCase();
rows[r].style.display = (text.indexOf(q) !== -1) ? '' : 'none';
}
};
}
var rows = document.querySelectorAll('#plc-modal .plc-modal-row');
for (var j = 0; j < rows.length; j++) {
rows[j].onclick = function() {
if (this.classList.contains('plc-modal-used')) {
showToast('该信号已被使用', 'err');
return;
}
var noAttr = this.getAttribute('data-no');
var saddr = this.getAttribute('data-saddr');
var desc = this.getAttribute('data-desc') || '';
var no = parseInt(noAttr) || 0;
var result = onSelect(no, saddr, desc);
if (result !== false) closeModal();
};
}
}
function closeModal() {
var m = document.getElementById('plc-modal');
if (m) m.remove();
}
/* ---- 生成并保存配置 ---- */
// 生成 XML 配置文件
function generateXmlConfig() {
var typeNames = {2:'OR',3:'AND',4:'NOT',5:'T62P',6:'T62D',7:'RISING',8:'SR',9:'RS',10:'FALLING'};
var adjIn = {}, adjOut = {};
for (var i = 0; i < editNodes.length; i++) { adjIn[i] = []; adjOut[i] = []; }
for (var j = 0; j < editLinks.length; j++) {
var l = editLinks[j];
adjOut[l.srcIdx].push(l.dstIdx);
adjIn[l.dstIdx].push(l.srcIdx);
}
var visited = {};
var chains = [];
for (var s = 0; s < editNodes.length; s++) {
if (visited[s]) continue;
var c = [], q = [s]; visited[s] = true;
while (q.length > 0) {
var cur = q.shift(); c.push(cur);
var nbrs = adjOut[cur].concat(adjIn[cur]);
for (var nb = 0; nb < nbrs.length; nb++)
if (!visited[nbrs[nb]]) { visited[nbrs[nb]] = true; q.push(nbrs[nb]); }
}
chains.push(c);
}
function sigXml(n) { return ''; }
function gateTag(n, nodeIdx) {
var tn = typeNames[n.type] || '?';
var a = ' type="' + tn + '" id="' + (n.id & 0xFF) + '" x="' + Math.round(n._lx) + '" y="' + Math.round(n._ly) + '"';
if (n.inPins && n.inPins > 2) a += ' inPins="' + n.inPins + '"';
if (n.type === NT_62P || n.type === NT_62D) a += ' delayMs="' + (n.delayMs || 1000) + '"';
// SR/RS: 输出 R/S 属性
if ((n.type === NT_SR || n.type === NT_RS) && nodeIdx !== undefined) {
var srLinks = [];
for (var li = 0; li < editLinks.length; li++) {
if (editLinks[li].dstIdx === nodeIdx) srLinks.push(editLinks[li]);
}
// 按 dstPin 排序,未设置的用出现顺序兜底
var hasPin = false;
for (var sl = 0; sl < srLinks.length; sl++) {
if (srLinks[sl].dstPin !== undefined) { hasPin = true; break; }
}
if (!hasPin) {
// 兜底:SR 按出现顺序 S→R,RS 按出现顺序 R→S
for (var sl2 = 0; sl2 < srLinks.length; sl2++) {
if (n.type === NT_SR) srLinks[sl2].dstPin = sl2; // 0=S, 1=R
else srLinks[sl2].dstPin = 1 - sl2; // 0=R, 1=S
}
}
for (var sl3 = 0; sl3 < srLinks.length; sl3++) {
var l3 = srLinks[sl3];
var srcNode = editNodes[l3.srcIdx];
var srcDesc = '';
if (srcNode.type === NT_INPUT) {
srcDesc = 'Input#' + srcNode.id;
} else {
srcDesc = 'Comb#' + (srcNode.combId !== undefined ? srcNode.combId : (srcNode.id & 0xFF));
}
if (n.type === NT_SR) {
if (l3.dstPin === 0) a += ' S="' + srcDesc + '"';
else a += ' R="' + srcDesc + '"';
} else {
if (l3.dstPin === 1) a += ' S="' + srcDesc + '"';
else a += ' R="' + srcDesc + '"';
}
}
}
return '';
}
/** 后序遍历分配 combId(自底向上,从 0 起) */
function assignCombIds(idx, ctrObj, cGatesArr) {
editNodes[idx].combId = undefined;
for (var ui = 0; ui < adjIn[idx].length; ui++) {
var up = adjIn[idx][ui];
if (editNodes[up].type > 1 && cGatesArr.indexOf(up) >= 0) {
assignCombIds(up, ctrObj, cGatesArr);
}
}
editNodes[idx].combId = ctrObj.count++;
}
function combXml(idx, indent) {
var s = '';
var cid = editNodes[idx].combId;
s += indent + '\n';
var innerIndent = indent + ' ';
var di = [];
for (var fi = 0; fi < adjIn[idx].length; fi++)
if (editNodes[adjIn[idx][fi]].type === NT_INPUT) di.push(adjIn[idx][fi]);
if (di.length > 0) {
s += innerIndent + '\n';
for (var d = 0; d < di.length; d++) s += innerIndent + ' ' + sigXml(editNodes[di[d]]) + '\n';
s += innerIndent + '\n';
}
for (var ui = 0; ui < adjIn[idx].length; ui++)
if (editNodes[adjIn[idx][ui]].type > 1 && cGates.indexOf(adjIn[idx][ui]) >= 0) {
s += combXml(adjIn[idx][ui], innerIndent);
}
s += innerIndent + gateTag(editNodes[idx], idx) + '\n';
var dos = [];
for (var oi = 0; oi < adjOut[idx].length; oi++)
if (editNodes[adjOut[idx][oi]].type === NT_OUTPUT) dos.push(adjOut[idx][oi]);
if (dos.length > 0) {
s += innerIndent + '\n';
for (var o = 0; o < dos.length; o++) s += innerIndent + ' ' + sigXml(editNodes[dos[o]]) + '\n';
s += innerIndent + '\n';
}
s += indent + '\n';
return s;
}
var xml = '\n';
xml += '\n';
for (var ci = 0; ci < chains.length; ci++) {
var chain = chains[ci];
var cIns = [], cGates = [], cOuts = [];
for (var ni = 0; ni < chain.length; ni++) {
var nd = editNodes[chain[ni]];
if (nd.type === NT_INPUT) cIns.push(chain[ni]);
else if (nd.type === NT_OUTPUT) cOuts.push(chain[ni]);
else cGates.push(chain[ni]);
}
if (cGates.length === 0) continue;
xml += ' \n';
if (cGates.length === 1) {
// 简单链: Inputs → Gate → Outputs
xml += ' \n';
for (var ii = 0; ii < cIns.length; ii++) xml += ' ' + sigXml(editNodes[cIns[ii]]) + '\n';
xml += ' \n';
xml += ' ' + gateTag(editNodes[cGates[0]], cGates[0]) + '\n';
xml += ' \n';
for (var oo = 0; oo < cOuts.length; oo++) xml += ' ' + sigXml(editNodes[cOuts[oo]]) + '\n';
xml += ' \n';
} else {
// 组合链: 链级门 + Comb + Outputs
// 链级门 = 下游没有 cGates 内其他门的门(只连输出或链外)
var chainGates = [];
for (var gg = 0; gg < cGates.length; gg++) {
var allToOut = true;
for (var so = 0; so < adjOut[cGates[gg]].length; so++)
if (cGates.indexOf(adjOut[cGates[gg]][so]) >= 0) { allToOut = false; break; }
if (allToOut) chainGates.push(cGates[gg]);
}
// combRoots: 直接馈入 chainGate 的门(combXml 会递归处理上游嵌套)
var combRootSet = {};
for (var cg = 0; cg < chainGates.length; cg++) {
for (var ui = 0; ui < adjIn[chainGates[cg]].length; ui++) {
var up = adjIn[chainGates[cg]][ui];
if (cGates.indexOf(up) >= 0) combRootSet[up] = true;
}
}
// 去重:若 A 是 B 的下游,只保留 B(combXml 从 B 开始会递归包含 A)
var combRootArr = Object.keys(combRootSet).map(Number);
var combRoots = [];
for (var rr = 0; rr < combRootArr.length; rr++) {
var isDownstream = false;
for (var rr2 = 0; rr2 < combRootArr.length; rr2++) {
if (rr === rr2) continue;
var visited2 = {};
var queue2 = [combRootArr[rr2]];
visited2[combRootArr[rr2]] = true;
while (queue2.length > 0) {
var cur2 = queue2.shift();
if (cur2 === combRootArr[rr]) { isDownstream = true; break; }
for (var si = 0; si < adjOut[cur2].length; si++) {
var nx = adjOut[cur2][si];
if (!visited2[nx] && cGates.indexOf(nx) >= 0) { visited2[nx] = true; queue2.push(nx); }
}
}
if (isDownstream) break;
}
if (!isDownstream) combRoots.push(combRootArr[rr]);
}
// 输出 Comb(每个 combRoot 对应一个顶层 Comb,内部递归嵌套)
// 先分配 combId
var combIdCtr = { count: 0 };
for (var tt = 0; tt < combRoots.length; tt++) {
assignCombIds(combRoots[tt], combIdCtr, cGates);
}
for (var tt = 0; tt < combRoots.length; tt++) {
xml += combXml(combRoots[tt], ' ');
}
// 输出链级门 — 若存在直接 Input 连接(未进入 Comb 的),先行输出
for (var cg = 0; cg < chainGates.length; cg++) {
var cgNode = editNodes[chainGates[cg]];
var directInputs = [];
for (var cui = 0; cui < adjIn[chainGates[cg]].length; cui++) {
var upIdx = adjIn[chainGates[cg]][cui];
if (editNodes[upIdx].type === NT_INPUT) {
directInputs.push(upIdx);
}
}
if (directInputs.length > 0) {
xml += ' \n';
for (var di2 = 0; di2 < directInputs.length; di2++) {
xml += ' ' + sigXml(editNodes[directInputs[di2]]) + '\n';
}
xml += ' \n';
}
xml += ' ' + gateTag(cgNode, chainGates[cg]) + '\n';
}
// 链级 Outputs — 仅包含直接连到 chainGate 的输出信号
// (Comb 内部的输出信号已在 combXml 中处理)
var chainOutSet = {};
for (var cg2 = 0; cg2 < chainGates.length; cg2++) {
for (var oi = 0; oi < adjOut[chainGates[cg2]].length; oi++) {
var outIdx = adjOut[chainGates[cg2]][oi];
if (editNodes[outIdx].type === NT_OUTPUT) chainOutSet[outIdx] = true;
}
}
var chainOuts = Object.keys(chainOutSet).map(Number);
// 同时保留直接连到输入信号(无门)的输出
for (var oo2 = 0; oo2 < cOuts.length; oo2++) {
var hasGateSrc = false;
for (var ai = 0; ai < adjIn[cOuts[oo2]].length; ai++) {
if (cGates.indexOf(adjIn[cOuts[oo2]][ai]) >= 0) hasGateSrc = true;
}
if (!hasGateSrc) chainOutSet[cOuts[oo2]] = true;
}
chainOuts = Object.keys(chainOutSet).map(Number);
if (chainOuts.length > 0) {
xml += ' \n';
for (var oo = 0; oo < chainOuts.length; oo++) xml += ' ' + sigXml(editNodes[chainOuts[oo]]) + '\n';
xml += ' \n';
}
}
xml += ' \n';
}
xml += '\n';
return xml;
}
function escXml(s) { return String(s).replace(/&/g,'&').replace(//g,'>').replace(/"/g,'"'); }
function generateAndSaveConfig() {
if (editNodes.length === 0) { showToast('请先添加节点', 'err'); return; }
var hasInput = editNodes.some(function(n) { return n.type === NT_INPUT; });
var hasOutput = editNodes.some(function(n) { return n.type === NT_OUTPUT; });
if (!hasInput || !hasOutput) { showToast('至少需要一个输入和一个输出', 'err'); return; }
if (editLinks.length === 0) { showToast('请先连线', 'err'); return; }
var seenIds = {};
for (var di = 0; di < editNodes.length; di++) {
var dn = editNodes[di];
if (dn.type === NT_INPUT || dn.type === NT_OUTPUT) continue;
var key2 = dn.type + ':' + (dn.id & 0xFF);
if (seenIds[key2]) {
var next = editNextGateIdx[dn.type] || 0;
editNodes[di].id = (dn.id & ~0xFF) | next;
editNextGateIdx[dn.type] = next + 1;
} else { seenIds[key2] = true; }
}
var fileContent = generateXmlConfig();
var preview = fileContent.length > 2000 ? fileContent.substring(0,2000)+'…' : fileContent;
var previewHtml = ''
+ '
✅ 配置预览 (XML)
'
+ '
Key: ' + escHtml(editKey) + ' | 名称: ' + escHtml(editName || 'PLC Logic') + '
'
+ '
节点: ' + editNodes.length + ' | 连线: ' + editLinks.length + '
'
+ '
' + escHtml(preview) + '
'
+ '
'
+ ''
+ ''
+ '
';
var div = document.createElement('div');
div.innerHTML = previewHtml;
document.body.appendChild(div.firstElementChild);
document.getElementById('plc-modal-cancel').onclick = closeModal;
document.getElementById('plc-modal-save').onclick = function() {
closeModal();
var oldContent = loadedFiles['config/PLC/Reclose_logic.xml'] || '';
if (oldContent && oldContent.length > 10) {
WsClient.send({ type: 'set_file', path: 'config/PLC/Reclose_logic.xml.bak', content: oldContent });
}
var sent = WsClient.send({
type: 'set_file',
path: 'config/PLC/Reclose_logic.xml',
content: fileContent
});
if (sent) {
showToast('XML 配置已保存!请重启 PLC 模块生效', 'ok');
loadedFiles['config/PLC/Reclose_logic.xml'] = null;
loadedFiles['config/PLC/Reclose_logic.xml.bak'] = null;
logicGraphs = [];
} else {
showToast('保存失败:WebSocket 未连接', 'err');
}
};
}
/* ========== 页面生命周期 ========== */
function onEnter() {
// 重试之前因WS未连接而失败的文件请求
if (PlcConfigPage._retryList && PlcConfigPage._retryList.length > 0) {
var retry = PlcConfigPage._retryList.slice();
PlcConfigPage._retryList = [];
for (var ri = 0; ri < retry.length; ri++) {
requestFile(retry[ri], PlcConfigPage._pending[retry[ri]] || function(){});
}
}
// 自动加载 datacenter out 信号
if (dcOutSignals.length === 0) {
loadDcOutSignals();
}
}
function onLeave() {
editWiring = null;
editDragNode = null;
editSelectedNode = -1;
editSelectedLink = -1;
}
function onData(data) {
if (!data || typeof data !== 'object') return;
// 处理 file_content 响应
if (data.type === 'file_content') {
var path = data.path;
var content = data.content || '';
var error = data.error || '';
if (error) {
// 文件不存在等情况:仍然触发回调(传null),确保 loadLogicFileAll 的 tryCb 能正常计数
loadedFiles[path] = null;
if (PlcConfigPage._pending && PlcConfigPage._pending[path]) {
PlcConfigPage._pending[path](null);
delete PlcConfigPage._pending[path];
}
} else {
loadedFiles[path] = content;
if (PlcConfigPage._pending && PlcConfigPage._pending[path]) {
PlcConfigPage._pending[path](content);
delete PlcConfigPage._pending[path];
}
}
if (PlcConfigPage._retryList) {
var rIdx = PlcConfigPage._retryList.indexOf(path);
if (rIdx >= 0) PlcConfigPage._retryList.splice(rIdx, 1);
}
return;
}
// 处理 file_saved 响应
if (data.type === 'file_saved') {
if (data.success) {
showToast('文件已保存: ' + data.path, 'ok');
} else {
showToast('保存失败: ' + (data.error || '未知错误'), 'err');
}
return;
}
}
return {
render: render,
onEnter: onEnter,
onLeave: onLeave,
onData: onData,
_pending: {}
};
})();
Router.register('#plc_config', function(c) { PlcConfigPage.render(c); });