/**
* @file plc_config.js
* @brief PLC 逻辑配置页 — 信号面板 + 逻辑图查看(当前/备份) + 在线编辑
* @details Canvas 可视化 + XML 解析/生成, 11 种门类型.
* 从原工程迁移, 按现工程 UI 风格重构.
*
* 布局: 左侧信号面板(datacenter_out.xml) | 右侧 Canvas
* 功能: 查看(当前/备份切换) | 编辑(可选基础: 当前/备份/空白)
*/
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 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, NT_TOGGLE=11, NT_MX_ADD=50, NT_MX_SUB=51, NT_MX_MAX=52, NT_MX_MIN=53, NT_MX_MUL=54, NT_MX_DIV=55, NT_MX_ABS=56, NT_MX_CMP=57;
var NODE_INFO = {
0: { name:'输出点', icon:'📤', color:'#4CAF50', inPins:1, outPins:0, inMax:1, outMax:0 },
1: { name:'输入点', icon:'📥', color:'#2196F3', inPins:0, outPins:1, inMax:0, outMax:99 },
2: { name:'或门 OR', icon:'🔶', color:'#FF9800', inPins:2, outPins:1, inMax:10, outMax:10 },
3: { name:'与门 AND', icon:'🔷', color:'#9C27B0', inPins:2, outPins:1, inMax:10, outMax:10 },
4: { name:'非门 NOT', icon:'🔺', color:'#f44336', inPins:1, outPins:1, inMax:1, outMax:10 },
5: { name:'62p延时', icon:'⏱️', color:'#00BCD4', inPins:1, outPins:1, inMax:1, outMax:10 },
6: { name:'62d延时', icon:'⏱️', color:'#009688', inPins:1, outPins:1, inMax:1, outMax:10 },
7: { name:'上升沿', icon:'⬆️', color:'#E91E63', inPins:1, outPins:1, inMax:1, outMax:10 },
8: { name:'SR触发器', icon:'🔒', color:'#795548', inPins:2, outPins:1, inMax:2, outMax:10 },
9: { name:'RS触发器', icon:'🔒', color:'#607D8B', inPins:2, outPins:1, inMax:2, outMax:10 },
10: { name:'下降沿', icon:'⬇️', color:'#FF5722', inPins:1, outPins:1, inMax:1, outMax:10 },
11: { name:'翻转元件', icon:'🔄', color:'#CDDC39', inPins:1, outPins:1, inMax:1, outMax:10 },
50: { name:'加法元件', icon:'➕', color:'#E040FB', inPins:2, outPins:1, inMax:2, outMax:10 },
51: { name:'减法元件', icon:'➖', color:'#00E5FF', inPins:2, outPins:1, inMax:2, outMax:10 },
52: { name:'最大值元件', icon:'⬆️', color:'#FFD600', inPins:2, outPins:1, inMax:2, outMax:10 },
53: { name:'最小值元件', icon:'⬇️', color:'#76FF03', inPins:2, outPins:1, inMax:2, outMax:10 },
54: { name:'乘法元件', icon:'✖️', color:'#FF6D00', inPins:2, outPins:1, inMax:2, outMax:10 },
55: { name:'除法元件', icon:'➗', color:'#D50000', inPins:2, outPins:1, inMax:2, outMax:10 },
56: { name:'绝对值元件', icon:'📏', color:'#00C853', inPins:1, outPins:1, inMax:1, outMax:10 },
57: { name:'比较元件', icon:'⚖️', color:'#FF1744', inPins:2, outPins:1, inMax:2, outMax:10 }
};
/* ========== 全局状态 ========== */
var mode = 'view'; // 'view' | 'edit'
var dcOutSignals = []; // [{no, saddr, desc}]
var logicGraphs = []; // parsed graphs
var loadedFiles = {}; // {path: content}
/* ======== 编辑器状态 ======== */
var editNodes = [];
var editLinks = [];
var editKey = '', editName = '';
var editSelectedNode = -1, editSelectedLink = -1;
var editWiring = null;
var editDragNode = null, editDragOffX = 0, editDragOffY = 0;
var editDragCanvas = null;
var editPanX = 0, editPanY = 0, editScale = 1.0;
var editNextGateIdx = {};
var editLastClickTime = 0, editLastClickNode = -1;
/* ======== 查看器状态 ======== */
var viewPanX = 0, viewPanY = 0, viewScale = 1.0;
var viewDrag = null, viewAutoFit = true;
/* ========== 工具函数 ========== */
function escHtml(s) { return String(s).replace(/&/g,'&').replace(//g,'>').replace(/"/g,'"'); }
function escXml(s) { return String(s).replace(/&/g,'&').replace(//g,'>').replace(/"/g,'"'); }
function findSignalByNo(no) { for (var i=0;i0) return node.inPins;
return (NODE_INFO[node.type]||{}).inPins||0;
}
function getNodeSize(node) {
var isIO=(node.type===NT_OUTPUT||node.type===NT_INPUT),isSR=(node.type===NT_SR||node.type===NT_RS);
var inP=getNodeInPins(node);
return {w:isIO?250:170, h:isIO?80:(isSR?86:(66+Math.max(0,inP-2)*14))};
}
function showToast(msg, type) {
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,cb){if(loadedFiles[path]){cb(loadedFiles[path]);return;}if(!PlcConfigPage._pending)PlcConfigPage._pending={};PlcConfigPage._pending[path]=cb;if(!WsClient.send({type:'get_file',path:path})){if(!PlcConfigPage._retryList)PlcConfigPage._retryList=[];PlcConfigPage._retryList.push(path);}}
/* ========== 主渲染 ========== */
function render(container) {
container.innerHTML = ''
+ '
'
+ '
';
document.getElementById('plc-btn-view').onclick = function(){mode='view';editWiring=null;render(container);};
document.getElementById('plc-btn-edit').onclick = function(){mode='edit';editWiring=null;render(container);};
var main=document.getElementById('plc-main-area');if(!main)return;
if(mode==='view')renderViewMode(main);else renderEditMode(main);
if(dcOutSignals.length===0)loadDcOutSignals(function(){refreshSigPanel();});else refreshSigPanel();
}
/* ================================================================
* 信号面板
* ================================================================ */
function renderSigPanel() {
return '';
}
function renderOpPanel() {
var ops=[{t:NT_AND,n:'与门',i:'🔷',c:'#9C27B0'},{t:NT_OR,n:'或门',i:'🔶',c:'#FF9800'},{t:NT_NOT,n:'非门',i:'🔺',c:'#f44336'},{t:NT_62P,n:'62P',i:'⏱️',c:'#00BCD4'},{t:NT_62D,n:'62D',i:'⏱️',c:'#009688'},{t:NT_RISING,n:'上升沿',i:'⬆️',c:'#E91E63'},{t:NT_FALLING,n:'下降沿',i:'⬇️',c:'#FF5722'},{t:NT_TOGGLE,n:'翻转',i:'🔄',c:'#CDDC39'},{t:NT_SR,n:'SR',i:'🔒',c:'#795548'},{t:NT_RS,n:'RS',i:'🔒',c:'#607D8B'},{t:NT_MX_ADD,n:'加法',i:'➕',c:'#E040FB'},{t:NT_MX_SUB,n:'减法',i:'➖',c:'#00E5FF'},{t:NT_MX_MAX,n:'最大值',i:'⬆️',c:'#FFD600'},{t:NT_MX_MIN,n:'最小值',i:'⬇️',c:'#76FF03'},{t:NT_MX_MUL,n:'乘法',i:'✖️',c:'#FF6D00'},{t:NT_MX_DIV,n:'除法',i:'➗',c:'#D50000'},{t:NT_MX_ABS,n:'绝对值',i:'📏',c:'#00C853'},{t:NT_MX_CMP,n:'比较',i:'⚖️',c:'#FF1744'}];
var h='';
for(var oi=0;oi'+op.i+' '+op.n+'
';}
h+='
';
return h;
}
function loadDcOutSignals(cb) {
requestFile('config/SYSTEM/datacenter_out.xml', function(content) {
if(!content){showToast('加载 datacenter_out.xml 失败','err');return;}
dcOutSignals=[];
try{
var doc=new DOMParser().parseFromString(content,'text/xml');
var outNodes=doc.documentElement.getElementsByTagName('Out');
if(outNodes.length>0){var sigs=outNodes[0].getElementsByTagName('Signal');for(var i=0;i#'+s.no+' '+escHtml(s.saddr)+''+escHtml(s.desc)+' ';if(mode==='edit'){h+='入 出 ';}h+='';}
list.innerHTML=h||'无匹配信号
';
[].forEach.call(list.querySelectorAll('.plc-sig-row'),function(row){});
}
/* ================================================================
* 查看模式
* ================================================================ */
function renderViewMode(main) {
main.innerHTML = renderSigPanel()
+ '';
var lh='图例: ';
for(var t in NODE_INFO){var inf=NODE_INFO[t];lh+=''+inf.icon+' '+inf.name+' ';}
document.getElementById('plc-view-legend').innerHTML=lh;
document.getElementById('plc-sig-search').oninput=refreshSigPanel;
document.getElementById('plc-sig-reload').onclick=function(){loadedFiles['config/SYSTEM/datacenter_out.xml']=null;dcOutSignals=[];loadDcOutSignals(function(){refreshSigPanel();});};
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=[];loadAllConfigs(function(){refreshViewSelect();});};
document.getElementById('plc-view-graph').onchange=function(){viewAutoFit=true;drawViewer();};
setupViewerCanvas();
if(logicGraphs.length===0)loadAllConfigs(function(){refreshViewSelect();});else refreshViewSelect();
}
function loadAllConfigs(cb){var pending=2;function done(){pending--;if(pending<=0&&cb)cb();}requestFile('config/PLC/Reclose_logic.xml',function(c){if(!c){done();return;}parseLogicXml(c,'Reclose_logic.xml');done();});requestFile('config/PLC/Reclose_logic.xml.bak',function(c){if(!c){done();return;}parseLogicXml(c,'Reclose_logic.xml.bak');done();});}
function loadCurrentLogic(cb){requestFile('config/PLC/Reclose_logic.xml',function(c){if(!c){showToast('加载失败','err');return;}parseLogicXml(c,'Reclose_logic.xml');if(cb)cb();});}
function refreshViewSelect(){var sel=document.getElementById('plc-view-graph');if(!sel)return;sel.innerHTML='-- 选择逻辑图 -- ';for(var i=0;i=0?' [备份]':'';sel.innerHTML+=''+(g.key||'?')+' — '+(g.name||'(未命名)')+bak+' ('+g.nodes.length+'节点 '+g.links.length+'连线) ';}if(logicGraphs.length>0){sel.value='0';setTimeout(function(){drawViewer();},100);}}
function setupViewerCanvas(){var wrap=document.getElementById('plc-view-canvas-wrap'),cnv=document.getElementById('plc-view-canvas');if(!wrap||!cnv)return;function resize(){cnv.width=wrap.clientWidth;cnv.height=wrap.clientHeight-4;viewAutoFit=true;drawViewer();}resize();window.addEventListener('resize',function(){resize();});cnv.onmousedown=function(e){viewDrag={sx:e.clientX,sy:e.clientY,px:viewPanX,py:viewPanY};};cnv.onmousemove=function(e){if(!viewDrag)return;viewPanX=viewDrag.px+(e.clientX-viewDrag.sx);viewPanY=viewDrag.py+(e.clientY-viewDrag.sy);drawViewer();};cnv.onmouseup=function(){viewDrag=null;};cnv.onmouseleave=function(){viewDrag=null;};cnv.onwheel=function(e){e.preventDefault();var d=e.deltaY>0?0.9:1.1,ns=Math.max(0.2,Math.min(3,viewScale*d)),r=cnv.getBoundingClientRect();viewPanX=(e.clientX-r.left)-((e.clientX-r.left)-viewPanX)*(ns/viewScale);viewPanY=(e.clientY-r.top)-((e.clientY-r.top)-viewPanY)*(ns/viewScale);viewScale=ns;drawViewer();};}
function drawViewer(){var cnv=document.getElementById('plc-view-canvas');if(!cnv)return;var ctx=cnv.getContext('2d'),W=cnv.width,H=cnv.height;ctx.clearRect(0,0,W,H);ctx.fillStyle='#0a0f16';ctx.fillRect(0,0,W,H);var sel=document.getElementById('plc-view-graph');if(!sel||sel.value===''){ctx.fillStyle='#556677';ctx.font='14px sans-serif';ctx.textAlign='center';ctx.fillText('请选择一个逻辑图',W/2,H/2);return;}var g=logicGraphs[parseInt(sel.value)];if(!g)return;var vn=[],bMinX=Infinity,bMaxX=-Infinity,bMinY=Infinity,bMaxY=-Infinity;for(var vi=0;vibMaxX)bMaxX=vv.x;if(vv.ybMaxY)bMaxY=vv.y;var vsig=findSignalByNo(vv.id);vn.push({type:vv.type,id:vv.id,_lx:vv.x,_ly:vv.y,saddr:(vsig&&vsig.saddr)||'',desc:(vsig&&vsig.desc)||'',no:vv.id});}if(viewAutoFit){viewAutoFit=false;var cw=bMaxX-bMinX,ch=bMaxY-bMinY;if(cw<=0)cw=800;if(ch<=0)ch=600;viewScale=Math.min((W-160)/cw,(H-160)/ch,1.2);viewPanX=W/2-(bMinX+cw/2)*viewScale;viewPanY=H/2-(bMinY+ch/2)*viewScale;}var vl=[];for(var vj=0;vj=0&&vld>=0)vl.push({srcIdx:vls,dstIdx:vld,dstPin:vlk.dstPin||0});}ctx.strokeStyle='rgba(255,255,255,0.04)';ctx.lineWidth=1;var gs=50,sx=Math.floor(-viewPanX/viewScale/gs)*gs,sy=Math.floor(-viewPanY/viewScale/gs)*gs;ctx.save();ctx.translate(viewPanX,viewPanY);ctx.scale(viewScale,viewScale);for(var gx=sx-gs;gx'
+ '
'
+ ''
+ '
'
+ ' '
+ ' '
+ ' '
+ '📥 输入 '
+ '🧮 运算符 '
+ '📤 输出 '
+ '✏️ 修改 '
+ '🗑 删除 '
+ '🔍+ '
+ '🔍− '
+ '✅ 生成 '
+ '
'
+ '
'
+ '
💡 双击修改 | 单击选中/连线 | 右键删除连线 | 滚轮缩放 | Delete删除
'
+ '
';
document.getElementById('plc-sig-search').oninput=refreshSigPanel;
document.getElementById('plc-sig-reload').onclick=function(){loadedFiles['config/SYSTEM/datacenter_out.xml']=null;dcOutSignals=[];loadDcOutSignals(function(){refreshSigPanel();});};
document.getElementById('plc-edit-load').onclick=function(){var base=document.getElementById('plc-edit-base').value;if(base==='empty'){resetEditor();return;}var path=base==='backup'?'config/PLC/Reclose_logic.xml.bak':'config/PLC/Reclose_logic.xml';logicGraphs=[];loadedFiles[path]=null;requestFile(path,function(c){if(!c){showToast('加载失败: '+path,'err');resetEditor();return;}parseLogicXml(c,path);loadGraphIntoEditor(path);});};
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(){showAddIOModal(NT_INPUT);};
document.getElementById('plc-edit-add-out').onclick=function(){showAddIOModal(NT_OUTPUT);};
document.getElementById('plc-edit-add-gate').onclick=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=deleteSelectedNode;
document.getElementById('plc-edit-modify').onclick=modifySelectedNode;
document.getElementById('plc-edit-generate').onclick=generateAndSaveConfig;
setupEditorCanvas();drawEditor();
/* 运算符面板点击事件 */
var opList=document.getElementById('plc-op-list');
if(opList){[].forEach.call(opList.querySelectorAll('.plc-op-row'),function(row){row.onclick=function(e){e.stopPropagation();var gt=parseInt(this.dataset.gt);PlcConfigPage._addOp(gt);};});}
/* 运算符面板点击事件 */
var opList=document.getElementById('plc-op-list');
if(opList){[].forEach.call(opList.querySelectorAll('.plc-op-row'),function(row){row.onclick=function(e){e.stopPropagation();var gt=parseInt(this.dataset.gt);PlcConfigPage._addOp(gt);};});}
/* 重试之前未成功的文件请求(同页切换时 onEnter 不触发) */
if (PlcConfigPage._retryList && PlcConfigPage._retryList.length > 0) {
var _rty = PlcConfigPage._retryList.slice();
PlcConfigPage._retryList = [];
for (var _ri = 0; _ri < _rty.length; _ri++)
requestFile(_rty[_ri], (PlcConfigPage._pending || {})[_rty[_ri]] || function(){});
}
if(dcOutSignals.length===0){loadDcOutSignals(function(){refreshSigPanel();autoLoadEditor();});}else{refreshSigPanel();autoLoadEditor();}
}
function resetEditor(){editNodes=[];editLinks=[];editKey=String(Date.now());editName='';editNextGateIdx={};var kel=document.getElementById('plc-edit-key'),nel=document.getElementById('plc-edit-name');if(kel)kel.value=editKey;if(nel)nel.value=editName;drawEditor();showToast('已重置为空白','ok');}
function autoLoadEditor(){if(editNodes.length>0)return;loadCurrentLogic(function(){if(logicGraphs.length>0)loadGraphIntoEditor('Reclose_logic.xml');else resetEditor();});}
function loadGraphIntoEditor(srcName){var g=null;for(var gi=0;gi0)g=logicGraphs[0];if(!g){resetEditor();return;}editNodes=[];editLinks=[];editKey=g.key;editName=g.name||'';for(var i=0;i=0&&di>=0){var el={srcIdx:si,dstIdx:di};if(l.dstPin!==undefined)el.dstPin=l.dstPin;editLinks.push(el);}}var mx={};for(var di=0;di=(mx[dn.type]||-1))mx[dn.type]=idx;}for(var t in mx)editNextGateIdx[t]=mx[t]+1;var seen={};for(var di=0;di1&&(n.id&0xFF)===(id&0xFF))return i;}}return -1;}
/* ---- 编辑器 Canvas ---- */
function setupEditorCanvas(){var wrap=document.getElementById('plc-edit-canvas-wrap'),cnv=document.getElementById('plc-edit-canvas');if(!wrap||!cnv)return;function resize(){cnv.width=wrap.clientWidth;cnv.height=Math.max(500,wrap.clientHeight-4);editScale=0.7;editPanX=cnv.width/2;editPanY=cnv.height/2;drawEditor();}setTimeout(resize,50);window.addEventListener('resize',function(){resize();});cnv.onmousedown=function(e){var r=cnv.getBoundingClientRect(),mx=(e.clientX-r.left-editPanX)/editScale,my=(e.clientY-r.top-editPanY)/editScale;var ph=hitTestPin(mx,my);if(ph){if(editWiring){if(editWiring.fromIdx!==ph.nodeIdx){if(editWiring.fromSide==='out'&&ph.side==='in'){var si=NODE_INFO[editNodes[editWiring.fromIdx].type]||{},om=si.outMax||99,oc=0;for(var lc=0;lc=om){showToast('输出上限('+om+')','err');editWiring=null;editSelectedNode=-1;drawEditor();return;}var lk={srcIdx:editWiring.fromIdx,dstIdx:ph.nodeIdx,dstPin:ph.pinIdx};var dn=editNodes[ph.nodeIdx];var dInfo=NODE_INFO[dn.type]||{};if(dInfo.inPins>1){for(var lc2=0;lc2=0){editSelectedLink=lh;editSelectedNode=-1;editDragNode=null;if(editWiring)editWiring=null;drawEditor();return;}var nh=hitTestNode(mx,my);if(nh>=0){editSelectedNode=nh;editSelectedLink=-1;editDragNode=nh;editDragOffX=mx-editNodes[nh]._lx;editDragOffY=my-editNodes[nh]._ly;drawEditor();return;}editSelectedNode=-1;editSelectedLink=-1;if(editWiring)editWiring=null;editDragCanvas={sx:e.clientX,sy:e.clientY,px:editPanX,py:editPanY};cnv.style.cursor='grabbing';drawEditor();};cnv.onmousemove=function(e){var r=cnv.getBoundingClientRect(),mx=(e.clientX-r.left-editPanX)/editScale,my=(e.clientY-r.top-editPanY)/editScale;if(editDragCanvas){editPanX=editDragCanvas.px+(e.clientX-editDragCanvas.sx);editPanY=editDragCanvas.py+(e.clientY-editDragCanvas.sy);drawEditor();return;}if(editDragNode!==null){editNodes[editDragNode]._lx=mx-editDragOffX;editNodes[editDragNode]._ly=my-editDragOffY;drawEditor();return;}if(editWiring&&editWiring.fromIdx=0?'grab':'default');};cnv.onmouseup=function(e){if(editDragCanvas){editDragCanvas=null;cnv.style.cursor='default';return;}if(editDragNode!==null){var nd=editNodes[editDragNode],ni=editDragNode;var dx=Math.abs((e.clientX-cnv.getBoundingClientRect().left-editPanX)/editScale-nd._lx-editDragOffX);var dy=Math.abs((e.clientY-cnv.getBoundingClientRect().top-editPanY)/editScale-nd._ly-editDragOffY);if(dx<3&&dy<3){var n=Date.now();if(ni===editLastClickNode&&(n-editLastClickTime)<400){onNodeDblClick(ni);editLastClickTime=0;editLastClickNode=-1;}else{editLastClickTime=n;editLastClickNode=ni;}}}editDragNode=null;};cnv.onmouseleave=function(){editDragNode=null;editDragCanvas=null;cnv.style.cursor='default';};cnv.onwheel=function(e){e.preventDefault();var d=e.deltaY>0?0.9:1.1,r=cnv.getBoundingClientRect();var ns=Math.max(0.2,Math.min(3,editScale*d));editPanX=(e.clientX-r.left)-((e.clientX-r.left)-editPanX)*(ns/editScale);editPanY=(e.clientY-r.top)-((e.clientY-r.top)-editPanY)*(ns/editScale);editScale=ns;drawEditor();};cnv.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;}};cnv.tabIndex=0;cnv.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();}};}
function hitTestNode(mx,my){for(var i=editNodes.length-1;i>=0;i--){var n=editNodes[i],sz=getNodeSize(n);if(mx>n._lx-sz.w/2&&mxn._ly-sz.h/2&&my=0;i--){var n=editNodes[i],sz=getNodeSize(n),info=NODE_INFO[n.type]||NODE_INFO[0];var isIO=(n.type===NT_INPUT||n.type===NT_OUTPUT),isSR=(n.type===NT_SR||n.type===NT_RS);var inP=getNodeInPins(n);if(info.inPins>0){var tp=isSR?2:(isIO?inP:inP);for(var p=0;p0){if(Math.sqrt((mx-(n._lx+sz.w/2))*(mx-(n._lx+sz.w/2))+(my-n._ly)*(my-n._ly))<=pinR)return{nodeIdx:i,side:'out',pinIdx:0};}}return null;}
function hitTestLink(mx,my){for(var i=0;i=editNodes.length||l.dstIdx>=editNodes.length)continue;var sn=editNodes[l.srcIdx],dn=editNodes[l.dstIdx],szS=getNodeSize(sn),szD=getNodeSize(dn);var x1=sn._lx+szS.w/2,y1=sn._ly,isSR=(dn.type===NT_SR||dn.type===NT_RS);var inP=getNodeInPins(dn),dpi=(l.dstPin!==undefined)?l.dstPin:0;var y2=dn._ly-szD.h/2+szD.h*(dpi+1)/(isSR?3:(inP+1)),x2=dn._lx-szD.w/2;var t=Math.max(0,Math.min(1,((mx-x1)*(x2-x1)+(my-y1)*(y2-y1))/((x2-x1)*(x2-x1)+(y2-y1)*(y2-y1))));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 drawEditor(){var cnv=document.getElementById('plc-edit-canvas');if(!cnv)return;var ctx=cnv.getContext('2d'),W=cnv.width,H=cnv.height;ctx.clearRect(0,0,W,H);ctx.fillStyle='#0a0f16';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 gs=40,sx=Math.floor(-editPanX/editScale/gs)*gs,sy=Math.floor(-editPanY/editScale/gs)*gs;for(var gx=sx-gs;gx=0&&editSelectedNode=50&&nd.type<=56)showMXInfoModal(idx);else showGateCfgModal(idx);}
function modifySelectedNode(){if(editSelectedNode<0||editSelectedNode>=editNodes.length){showToast('请先选中模块','err');return;}var nd=editNodes[editSelectedNode];if(nd.type===NT_INPUT||nd.type===NT_OUTPUT)showRebindModal(editSelectedNode);else if(nd.type===NT_62P||nd.type===NT_62D)showDelayEditModal(editSelectedNode);else if(nd.type===NT_SR||nd.type===NT_RS)showRSInfoModal(editSelectedNode);else if(nd.type>=50&&nd.type<=56)showMXInfoModal(editSelectedNode);else showGateCfgModal(editSelectedNode);}
function deleteSelectedNode(){if(editSelectedNode<0||editSelectedNode>=editNodes.length){showToast('请先选中模块','err');return;}var nd=editNodes[editSelectedNode],idx=editSelectedNode,lb=getNodeDisplayLabel(nd);showConfirm('🗑 确认删除','确定删除 '+escHtml(lb)+' 及其连线?',function(){var dt=nd.type;editLinks=editLinks.filter(function(l){return l.srcIdx!==idx&&l.dstIdx!==idx;});for(var i=0;iidx)editLinks[i].srcIdx--;if(editLinks[i].dstIdx>idx)editLinks[i].dstIdx--;}editNodes.splice(idx,1);editSelectedNode=-1;if(dt!==NT_INPUT&&dt!==NT_OUTPUT){var st=[];for(var s=0;s'+msg+'
确认 取消
';var d=document.createElement('div');d.innerHTML=h;document.body.appendChild(d.firstElementChild);document.getElementById('plc-modal-cancel').onclick=closeModal;document.getElementById('plc-modal-ok').onclick=function(){closeModal();cb();};}
function closeModal(){var m=document.getElementById('plc-modal');if(m)m.remove();}
function showIOModal(title,cb){if(dcOutSignals.length===0){showToast('信号列表为空','err');return;}var h='';var d=document.createElement('div');d.innerHTML=h;document.body.appendChild(d.firstElementChild);document.getElementById('plc-modal-cancel').onclick=closeModal;var si2=document.getElementById('plc-modal-search');if(si2)si2.oninput=function(){var q=this.value.toLowerCase();document.querySelectorAll('#plc-modal .plc-modal-row').forEach(function(r){r.style.display=(r.textContent||'').toLowerCase().indexOf(q)!==-1?'':'none';});};document.querySelectorAll('#plc-modal .plc-modal-row').forEach(function(r){r.onclick=function(){var no=parseInt(this.dataset.no)||0,sa=this.dataset.saddr,de=this.dataset.desc||'';var rs=cb(no,sa,de);if(rs!==false)closeModal();};});}
function showAddIOModal(ioType){var title=ioType===NT_INPUT?'📥 绑定输入信号':'📤 绑定输出信号';showIOModal(title,function(no,saddr,desc){editNodes.push({type:ioType,id:no,x:0,y:0,_lx:ioType===NT_INPUT?-260:260,_ly:editNodes.filter(function(n){return n.type===ioType;}).length*110,saddr:saddr,desc:desc,no:no});drawEditor();showToast('已添加: '+saddr,'ok');return true;});}
function showRebindModal(idx){var nd=editNodes[idx],title=nd.type===NT_INPUT?'📥 重新绑定输入':'📤 重新绑定输出';showIOModal(title,function(no,saddr,desc){editNodes[idx].no=no;editNodes[idx].id=no;editNodes[idx].saddr=saddr;editNodes[idx].desc=desc;drawEditor();showToast('已绑定: '+saddr,'ok');return true;});}
function showAddGateModal(){var gates=[{t:NT_AND,n:'与门AND',d:'全1出1',i:'🔷'},{t:NT_OR,n:'或门OR',d:'有1出1',i:'🔶'},{t:NT_NOT,n:'非门NOT',d:'取反',i:'🔺'},{t:NT_62P,n:'62p延时',d:'开延时',i:'⏱️'},{t:NT_62D,n:'62d延时',d:'关延时',i:'⏱️'},{t:NT_RISING,n:'上升沿',d:'0→1脉冲',i:'⬆️'},{t:NT_FALLING,n:'下降沿',d:'1→0脉冲',i:'⬇️'},{t:NT_TOGGLE,n:'翻转元件',d:'输入变化翻转',i:'🔄'},{t:NT_SR,n:'SR触发器',d:'S优先',i:'🔒'},{t:NT_RS,n:'RS触发器',d:'R优先',i:'🔒'},{t:NT_MX_ADD,n:'加法元件',d:'两输入求和',i:'➕'},{t:NT_MX_SUB,n:'减法元件',d:'两输入差绝对值',i:'➖'},{t:NT_MX_MAX,n:'最大值元件',d:'两输入取大',i:'⬆️'},{t:NT_MX_MIN,n:'最小值元件',d:'两输入取小',i:'⬇️'},{t:NT_MX_MUL,n:'乘法元件',d:'两输入乘积',i:'✖️'},{t:NT_MX_DIV,n:'除法元件',d:'两输入之商(除0出0)',i:'➗'},{t:NT_MX_ABS,n:'绝对值元件',d:'单输入取绝对值',i:'📏'},{t:NT_MX_CMP,n:'比较元件',d:'A>B出1,否则出0',i:'⚖️'}];var h='🧮 选择运算模块 ';for(var i=0;i'+g.i+' '+g.n+' '+g.d+'
';}h+='
取消 ';var d=document.createElement('div');d.innerHTML=h;document.body.appendChild(d.firstElementChild);document.getElementById('plc-modal-cancel').onclick=closeModal;document.querySelectorAll('#plc-modal .plc-modal-row').forEach(function(r){r.onclick=function(e){e.stopPropagation();var gt=parseInt(this.dataset.gt),info=NODE_INFO[gt],ni=editNextGateIdx[gt]||0;if(gt===NT_62P||gt===NT_62D){closeModal();showDelayInputModal(gt,ni,info);return;}addGateNode(gt,ni,0,info);editNextGateIdx[gt]=ni+1;closeModal();drawEditor();};});}
function showDelayInputModal(gateType,nextIdx,info){var h='';var d=document.createElement('div');d.innerHTML=h;document.body.appendChild(d.firstElementChild);document.getElementById('plc-modal-cancel').onclick=closeModal;document.getElementById('plc-modal-ok').onclick=function(){var v=parseInt(document.getElementById('plc-delay-input').value);if(isNaN(v)||v<0){showToast('无效延时','err');return;}closeModal();addGateNode(gateType,nextIdx,v,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 showDelayEditModal(idx){var nd=editNodes[idx],info=NODE_INFO[nd.type]||{},cur=nd.delayMs||1000;var h='';var d=document.createElement('div');d.innerHTML=h;document.body.appendChild(d.firstElementChild);document.getElementById('plc-modal-cancel').onclick=closeModal;document.getElementById('plc-modal-ok').onclick=function(){var v=parseInt(document.getElementById('plc-delay-input').value);if(isNaN(v)||v<0){showToast('无效延时','err');return;}editNodes[idx].delayMs=v;closeModal();drawEditor();showToast('延时更新: '+v+' ms','ok');};document.getElementById('plc-delay-input').onkeydown=function(e){if(e.key==='Enter')document.getElementById('plc-modal-ok').click();};setTimeout(function(){var inp2=document.getElementById('plc-delay-input');if(inp2){inp2.focus();inp2.select();}},100);}
function showRSInfoModal(idx){var nd=editNodes[idx],info=NODE_INFO[nd.type]||{},sSrc='',rSrc='';for(var li=0;liS(置位) : '+(sSrc||'未连接 ')+'
R(复位) : '+(rSrc||'未连接 ')+'
💡 拖输出到S/R引脚连线
关闭 ';var d=document.createElement('div');d.innerHTML=h;document.body.appendChild(d.firstElementChild);document.getElementById('plc-modal-cancel').onclick=closeModal;}
function showMXInfoModal(idx){var nd=editNodes[idx],info=NODE_INFO[nd.type]||{},inPins=getNodeInPins(nd);var aSrc='',bSrc='';for(var li=0;li⚠️ 顺序敏感: A - B
';else if(nd.type===NT_MX_DIV)orderNote='⚠️ 顺序敏感: A ÷ B (B=0→结果为0)
';else if(nd.type===NT_MX_CMP)orderNote='⚠️ 顺序敏感: A > B → 1,否则 → 0
';else orderNote='💡 输入可交换,顺序不影响结果
';var h=''+info.icon+' '+info.name+' — 连接 A : '+(aSrc||'未连接 ')+'
B : '+(bSrc||'未连接 ')+'
'+orderNote+'
关闭 ';var d=document.createElement('div');d.innerHTML=h;document.body.appendChild(d.firstElementChild);document.getElementById('plc-modal-cancel').onclick=closeModal;}
function showGateCfgModal(idx){var nd=editNodes[idx],cur=getNodeInPins(nd),info=NODE_INFO[nd.type]||{},mx=info.inMax||99;var h='';var d=document.createElement('div');d.innerHTML=h;document.body.appendChild(d.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<1){showToast('最少1个','err');return;}if(v>mx){showToast('最多'+mx+'个','err');return;}editNodes[idx].inPins=v;closeModal();drawEditor();showToast(info.name+' 输入='+v,'ok');};}
function addGateNode(gateType,nextIdx,delayMs,info){var pos=findEmptyPos();var nd={type:gateType,id:nextIdx,x:0,y:0,_lx:pos.x,_ly:pos.y,saddr:'',desc:'',no:0};if(delayMs>0)nd.delayMs=delayMs;editNodes.push(nd);showToast('已添加: '+(info?info.name:NODE_INFO[gateType].name)+' #'+nextIdx,'ok');}
function findEmptyPos(){if(editNodes.length===0)return{x:0,y:0};var mxX=-Infinity,mnX=Infinity,mxY=-Infinity,mnY=Infinity;for(var i=0;imxX)mxX=n._lx;if(n._lxmxY)mxY=n._ly;if(n._ly=nodes.length||l.dstIdx>=nodes.length)continue;var sn=nodes[l.srcIdx],dn=nodes[l.dstIdx];var sIO=(sn.type===NT_OUTPUT||sn.type===NT_INPUT),dIO=(dn.type===NT_OUTPUT||dn.type===NT_INPUT);var dSR=(dn.type===NT_SR||dn.type===NT_RS);var sW=sIO?IW:GW,dW=dIO?IW:GW,dH=dIO?IH:(dSR?86:GH);var x1=sn._lx+sW/2,y1=sn._ly;var dIP=getNodeInPins(dn),epi=(l.dstPin!==undefined)?l.dstPin:0;var y2=dn._ly-dH/2+dH*(epi+1)/(dSR?3:(dIP+1)),x2=dn._lx-dW/2;var isAct=wiring&&wiring.fromIdx===l.srcIdx,isSel=(selectedLinkIdx===i);var mid=(x1+x2)/2,la=isAct||isSel?1:0.6,lw=isAct||isSel?3:2;var lc=isSel?'#FF5722':(isAct?'#FFC107':'rgba(180,200,220,'+la+')');ctx.beginPath();ctx.strokeStyle=isSel?'rgba(255,87,34,0.3)':'rgba(255,200,0,'+(la*0.2)+')';ctx.lineWidth=lw+4;ctx.moveTo(x1,y1);ctx.bezierCurveTo(mid,y1,mid,y2,x2,y2);ctx.stroke();ctx.beginPath();ctx.strokeStyle=lc;ctx.lineWidth=lw;ctx.moveTo(x1,y1);ctx.bezierCurveTo(mid,y1,mid,y2,x2,y2);ctx.stroke();ctx.fillStyle=lc;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(isSel){var lx2=(x1+x2)/2,ly2=(y1+y2)/2-18;ctx.fillStyle='#FF5722';ctx.font='bold 14px sans-serif';ctx.textAlign='center';ctx.textBaseline='middle';ctx.fillText('✕ 右键删除',lx2,ly2);}}if(wiring&&wiring.fromIdx18?dt2.substring(0,16)+'…':dt2,x,y+14);}}else{var gi=n.id&0xFF,ri=0,ro=0;for(var li=0;li0){var isSR=(n.type===NT_SR||n.type===NT_RS),rp=isSR?2:(isIO?inP:inP);for(var p=0;p=50&&n.type<=57);if(isMX){ctx.fillStyle='#fff';ctx.font='bold 10px monospace';ctx.textAlign='right';ctx.textBaseline='middle';ctx.fillText(p===0?'A':'B',x-hw-12,py);}}}if(info.outPins>0){ctx.fillStyle='#2e2e4a';ctx.beginPath();ctx.arc(x+hw,y,7,0,Math.PI*2);ctx.fill();ctx.strokeStyle='#FF9800';ctx.lineWidth=2;ctx.stroke();ctx.fillStyle='#FF9800';ctx.beginPath();ctx.arc(x+hw,y,3.5,0,Math.PI*2);ctx.fill();}}}
/* ================================================================
* XML 解析 (与原工程完全一致)
* ================================================================ */
function parseLogicXml(xmlStr,sourceName){var parser=new DOMParser(),doc=parser.parseFromString(xmlStr,'text/xml'),root=doc.documentElement;if(!root||root.nodeName!=='PLCConfig'){showToast('根元素不是 PLCConfig','err');return;}var thisKey=root.getAttribute('key')||sourceName,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,TOGGLE:11,MX_ADD:50,MX_SUB:51,MX_MAX:52,MX_MIN:53,MX_MUL:54,MX_DIV:55,MX_ABS:56,MX_CMP:57};var sigMap={};function ens(ioType,no,x,y){var k=ioType+':'+no;if(sigMap[k]!==undefined){if(x!==undefined){g.nodes[sigMap[k]].x=x;g.nodes[sigMap[k]].y=y;}return sigMap[k];}g.nodes.push({type:ioType,id:no,x:x||0,y:y||0});sigMap[k]=g.nodes.length-1;return g.nodes.length-1;}function eng(type,id,x,y,inPins,delayMs){for(var i=0;i=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=ce.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'));ens(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=ce.firstElementChild;while(oe&&oe.nodeName!=='Outputs')oe=oe.nextElementSibling;if(oe){var s2=oe.firstElementChild;while(s2){if(s2.nodeName==='Signal'){var no2=parseInt(s2.getAttribute('no'));ens(NT_OUTPUT,no2,parseInt(s2.getAttribute('x')),parseInt(s2.getAttribute('y')));g.links.push({src_type:t,src_id:gid,dst_type:NT_OUTPUT,dst_id:no2});}s2=s2.nextElementSibling;}}return gi;}var chains=root.getElementsByTagName('Chain');for(var ci=0;ci 0) {
var cur = q.shift(); c.push(cur);
var nb = aO[cur].concat(aI[cur]);
for (var nb2 = 0; nb2 < nb.length; nb2++)
if (!vis[nb[nb2]]) { vis[nb[nb2]] = true; q.push(nb[nb2]); }
}
chs.push(c);
}
function sx(n, pin) { var s = '= 0) s += ' pin="' + pin + '"'; s += ' />'; return s; }
function gt(n, ni) {
var tn = tN[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) + '"';
if ((n.type === NT_SR || n.type === NT_RS) && ni !== undefined) {
var sl = [];
for (var li = 0; li < editLinks.length; li++)
if (editLinks[li].dstIdx === ni) sl.push(editLinks[li]);
var hp = false;
for (var si2 = 0; si2 < sl.length; si2++)
if (sl[si2].dstPin !== undefined) { hp = true; break; }
if (!hp) for (var si3 = 0; si3 < sl.length; si3++) {
if (n.type === NT_SR) sl[si3].dstPin = si3;
else sl[si3].dstPin = 1 - si3;
}
var dedupSR = {};
for (var si4 = 0; si4 < sl.length; si4++) {
var l3 = sl[si4], snl = editNodes[l3.srcIdx];
var sd2 = '';
if (snl.type === NT_INPUT) sd2 = 'Input#' + snl.id;
else sd2 = 'Comb#' + (snl.combId !== undefined ? snl.combId : (snl.id & 0xFF));
var dk = snl.type + '_' + snl.id + '_' + (l3.dstPin || 0);
if (!dedupSR[dk]) {
dedupSR[dk] = true;
if (n.type === NT_SR) {
if (l3.dstPin === 0) a += ' S="' + sd2 + '"';
else a += ' R="' + sd2 + '"';
} else {
if (l3.dstPin === 1) a += ' S="' + sd2 + '"';
else a += ' R="' + sd2 + '"';
}
}
}
}
return ' ';
}
function ac(idx, ctr, cga) {
editNodes[idx].combId = undefined;
for (var ui = 0; ui < aI[idx].length; ui++) {
var up = aI[idx][ui];
if (editNodes[up].type > 1 && cga.indexOf(up) >= 0) ac(up, ctr, cga);
}
editNodes[idx].combId = ctr.count++;
}
function cx(idx, ind) {
var s2 = '';
var cid = editNodes[idx].combId;
s2 += ind + '\n';
var i2 = ind + ' ';
var di2 = [];
for (var fi = 0; fi < aI[idx].length; fi++)
if (editNodes[aI[idx][fi]].type === NT_INPUT) di2.push(aI[idx][fi]);
if (di2.length > 0) {
s2 += i2 + '\n';
for (var d = 0; d < di2.length; d++) s2 += i2 + ' ' + sx(editNodes[di2[d]]) + '\n';
s2 += i2 + ' \n';
}
for (var ui2 = 0; ui2 < aI[idx].length; ui2++)
if (editNodes[aI[idx][ui2]].type > 1 && cGs.indexOf(aI[idx][ui2]) >= 0)
s2 += cx(aI[idx][ui2], i2);
s2 += i2 + gt(editNodes[idx], idx) + '\n';
var dos = [];
for (var oi = 0; oi < aO[idx].length; oi++)
if (editNodes[aO[idx][oi]].type === NT_OUTPUT) dos.push(aO[idx][oi]);
if (dos.length > 0) {
s2 += i2 + '\n';
for (var o = 0; o < dos.length; o++) s2 += i2 + ' ' + sx(editNodes[dos[o]]) + '\n';
s2 += i2 + ' \n';
}
s2 += ind + ' \n';
return s2;
}
var cGs = [];
var xml = '\n';
xml += '\n';
for (var ci = 0; ci < chs.length; ci++) {
var ch = chs[ci], cis = [], cos = [];
cGs = [];
for (var ni = 0; ni < ch.length; ni++) {
var nd = editNodes[ch[ni]];
if (nd.type === NT_INPUT) cis.push(ch[ni]);
else if (nd.type === NT_OUTPUT) cos.push(ch[ni]);
else cGs.push(ch[ni]);
}
if (cGs.length === 0) continue;
xml += ' \n';
if (cGs.length === 1) {
xml += ' \n';
for (var ii = 0; ii < cis.length; ii++) xml += ' ' + sx(editNodes[cis[ii]]) + '\n';
xml += ' \n';
xml += ' ' + gt(editNodes[cGs[0]], cGs[0]) + '\n';
xml += ' \n';
for (var oo = 0; oo < cos.length; oo++) xml += ' ' + sx(editNodes[cos[oo]]) + '\n';
xml += ' \n';
} else {
var chG = [];
for (var gg = 0; gg < cGs.length; gg++) {
var ato = true;
for (var so = 0; so < aO[cGs[gg]].length; so++)
if (cGs.indexOf(aO[cGs[gg]][so]) >= 0) { ato = false; break; }
if (ato) chG.push(cGs[gg]);
}
var crS = {};
for (var cg = 0; cg < chG.length; cg++)
for (var ui3 = 0; ui3 < aI[chG[cg]].length; ui3++) {
var up = aI[chG[cg]][ui3];
if (cGs.indexOf(up) >= 0) crS[up] = true;
}
var crA = Object.keys(crS).map(Number), crs = [];
for (var rr = 0; rr < crA.length; rr++) {
var isD = false;
for (var rr2 = 0; rr2 < crA.length; rr2++) {
if (rr === rr2) continue;
var v2 = {}, q2 = [crA[rr2]]; v2[crA[rr2]] = true;
while (q2.length > 0) {
var c2 = q2.shift();
if (c2 === crA[rr]) { isD = true; break; }
for (var si5 = 0; si5 < aO[c2].length; si5++) {
var nx = aO[c2][si5];
if (!v2[nx] && cGs.indexOf(nx) >= 0) { v2[nx] = true; q2.push(nx); }
}
}
if (isD) break;
}
if (!isD) crs.push(crA[rr]);
}
var cidC = {count: 0};
for (var tt = 0; tt < crs.length; tt++) ac(crs[tt], cidC, cGs);
for (var tt = 0; tt < crs.length; tt++) xml += cx(crs[tt], ' ');
for (var cg2 = 0; cg2 < chG.length; cg2++) {
var cgN = editNodes[chG[cg2]], dis = [], seenDis = {};
for (var cui = 0; cui < aI[chG[cg2]].length; cui++) {
var ui4 = aI[chG[cg2]][cui];
if (editNodes[ui4].type === NT_INPUT && !seenDis[ui4]) { seenDis[ui4] = true; dis.push(ui4); }
}
if (dis.length > 0) {
xml += ' \n';
for (var di3 = 0; di3 < dis.length; di3++) xml += ' ' + sx(editNodes[dis[di3]]) + '\n';
xml += ' \n';
}
xml += ' ' + gt(cgN, chG[cg2]) + '\n';
}
var coS = {};
for (var cg3 = 0; cg3 < chG.length; cg3++)
for (var oi2 = 0; oi2 < aO[chG[cg3]].length; oi2++) {
var oi3 = aO[chG[cg3]][oi2];
if (editNodes[oi3].type === NT_OUTPUT) coS[oi3] = true;
}
for (var oo2 = 0; oo2 < cos.length; oo2++) {
var hgs = false;
for (var ai = 0; ai < aI[cos[oo2]].length; ai++)
if (cGs.indexOf(aI[cos[oo2]][ai]) >= 0) hgs = true;
if (!hgs) coS[cos[oo2]] = true;
}
var coL = Object.keys(coS).map(Number);
if (coL.length > 0) {
xml += ' \n';
for (var oo3 = 0; oo3 < coL.length; oo3++) xml += ' ' + sx(editNodes[coL[oo3]]) + '\n';
xml += ' \n';
}
}
xml += ' \n';
}
xml += ' \n';
return xml;
}
function generateAndSaveConfig(){if(editNodes.length===0){showToast('请先添加节点','err');return;}var hi=editNodes.some(function(n){return n.type===NT_INPUT;}),ho=editNodes.some(function(n){return n.type===NT_OUTPUT;});if(!hi||!ho){showToast('至少需要一个输入和输出','err');return;}if(editLinks.length===0){showToast('请先连线','err');return;}var seen={};for(var di=0;di2000?fc.substring(0,2000)+'…':fc;var h='✅ 配置预览 Key: '+escHtml(editKey)+' | '+escHtml(editName||'PLC Logic')+'
节点:'+editNodes.length+' | 连线:'+editLinks.length+'
'+escHtml(pv)+'
💾 保存配置 取消
';var d=document.createElement('div');d.innerHTML=h;document.body.appendChild(d.firstElementChild);document.getElementById('plc-modal-cancel').onclick=closeModal;document.getElementById('plc-modal-save').onclick=function(){closeModal();var old=loadedFiles['config/PLC/Reclose_logic.xml']||'';if(old&&old.length>10){WsClient.send({type:'set_file',path:'config/PLC/Reclose_logic.xml.bak',content:old});}var snt=WsClient.send({type:'set_file',path:'config/PLC/Reclose_logic.xml',content:fc});if(snt){showToast('已保存!重启PLC生效','ok');loadedFiles['config/PLC/Reclose_logic.xml']=null;loadedFiles['config/PLC/Reclose_logic.xml.bak']=null;logicGraphs=[];}else{showToast('保存失败','err');}};}
/* ================================================================
* 页面生命周期
* ================================================================ */
function onEnter(){if(PlcConfigPage._retryList&&PlcConfigPage._retryList.length>0){var rty=PlcConfigPage._retryList.slice();PlcConfigPage._retryList=[];for(var ri=0;ri=0)PlcConfigPage._retryList.splice(rIdx,1);}return;}if(data.type==='file_saved'){if(data.success)showToast('已保存: '+data.path,'ok');else showToast('保存失败: '+(data.error||''),'err');}}
return {render:render,onEnter:onEnter,onLeave:onLeave,onData:onData,_pending:{},
_addSigIO:function(no,saddr,desc,ioType){
if(mode!=='edit')return;
var type=ioType===1?NT_INPUT:NT_OUTPUT;
editNodes.push({type:type,id:no,x:0,y:0,_lx:type===NT_INPUT?-260:260,_ly:editNodes.filter(function(n){return n.type===type;}).length*110,saddr:saddr,desc:desc||'',no:no});
drawEditor();showToast('已添加'+(type===NT_INPUT?'输入':'输出')+': '+saddr,'ok');
},
_addOp:function(gt){
if(mode!=='edit')return;
var info=NODE_INFO[gt],ni=editNextGateIdx[gt]||0;
if(gt===NT_62P||gt===NT_62D){addGateNode(gt,ni,0,info);editNextGateIdx[gt]=ni+1;drawEditor();return;}
addGateNode(gt,ni,0,info);editNextGateIdx[gt]=ni+1;drawEditor();
}
};
})();
Router.register('#plc_config', function(c) { PlcConfigPage.render(c); });