RTU_ALL_AI/config/web/js/pages/plc_config.js

553 lines
72 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/**
* @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,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;').replace(/"/g,'&quot;'); }
function escXml(s) { return String(s).replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;').replace(/"/g,'&quot;'); }
function findSignalByNo(no) { for (var i=0;i<dcOutSignals.length;i++) if(dcOutSignals[i].no===no) return dcOutSignals[i]; return null; }
function getNodeDisplayLabel(node) {
var info = NODE_INFO[node.type] || {};
if (node.type===NT_INPUT||node.type===NT_OUTPUT) {
var pfx = node.type===NT_INPUT?'输入':'输出', sig=findSignalByNo(node.id);
return pfx+' #'+node.id+(sig?' ("'+sig.saddr+'", "'+sig.desc+'")':'');
}
return info.icon+' '+info.name+' #'+(node.id&0xFF);
}
function getNodeInPins(node) {
if (node.inPins!==undefined&&node.inPins>0) 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 = ''
+ '<div class="card" style="margin-bottom:12px"><div class="card-header"><span>🔌 PLC 逻辑配置</span>'
+ '<div class="btn-group" style="margin-left:auto">'
+ '<button class="btn btn-sm '+(mode==='view'?'btn-primary':'btn-outline')+'" id="plc-btn-view">👁️ 查看</button>'
+ '<button class="btn btn-sm '+(mode==='edit'?'btn-primary':'btn-outline')+'" id="plc-btn-edit">✏️ 编辑</button>'
+ '</div></div></div>'
+ '<div id="plc-main-area" style="display:flex;gap:12px;height:calc(100vh - 200px)"></div>';
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 '<div class="card" style="width:290px;flex-shrink:0;display:flex;flex-direction:column;overflow:hidden">'
+ '<div class="card-header"><span>📋 信号 ('+dcOutSignals.length+')</span>'
+ '<button class="btn btn-sm btn-outline" id="plc-sig-reload" style="margin-left:auto;font-size:11px">🔄</button></div>'
+ '<input type="text" id="plc-sig-search" placeholder="搜索 saddr/desc..." style="margin:8px;padding:6px 10px;border:1px solid var(--border);border-radius:4px;background:var(--bg-input);color:var(--text-primary);font-size:12px">'
+ '<div style="flex:1;overflow-y:auto;font-size:12px;padding:0 8px" id="plc-sig-list"></div>'
+ '</div>';
}
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='<div class="card" style="width:160px;flex-shrink:0;display:flex;flex-direction:column;overflow:hidden"><div class="card-header"><span>🧮 运算符</span></div><div style="flex:1;overflow-y:auto;font-size:11px;padding:4px" id="plc-op-list">';
for(var oi=0;oi<ops.length;oi++){var op=ops[oi];h+='<div class="plc-op-row" data-gt="'+op.t+'" style="padding:6px 8px;cursor:pointer;border-bottom:1px solid rgba(255,255,255,0.04);display:flex;align-items:center;gap:6px;transition:background 0.15s" onmouseover="this.style.background=\'rgba(255,255,255,0.06)\'" onmouseout="this.style.background=\'transparent\'"><span>'+op.i+'</span><span style="color:'+op.c+';font-weight:600;font-size:11px">'+op.n+'</span></div>';}
h+='</div></div>';
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<sigs.length;i++)dcOutSignals.push({no:parseInt(sigs[i].getAttribute('no'))||i,saddr:sigs[i].getAttribute('saddr')||'',desc:sigs[i].getAttribute('desc')||''});}
}catch(e){showToast('解析失败: '+e.message,'err');}
if(cb)cb();
});
}
function refreshSigPanel() {
var list=document.getElementById('plc-sig-list'),search=(document.getElementById('plc-sig-search')||{}).value||'';
if(!list)return;
var data=dcOutSignals;
if(search){var q=search.toLowerCase();data=data.filter(function(s){return s.saddr.toLowerCase().indexOf(q)!==-1||s.desc.toLowerCase().indexOf(q)!==-1||String(s.no).indexOf(q)!==-1;});}
var h='';
for(var i=0;i<data.length;i++){var s=data[i];h+='<div class="plc-sig-row" data-no="'+s.no+'" data-saddr="'+escHtml(s.saddr)+'" data-desc="'+escHtml(s.desc)+'"><span style="color:var(--accent);min-width:24px;font-size:11px">#'+s.no+'</span><code style="flex:1;font-size:11px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap">'+escHtml(s.saddr)+'</code><span style="color:var(--text-muted);font-size:10px;max-width:55px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap">'+escHtml(s.desc)+'</span>';if(mode==='edit'){h+='<button class="btn btn-xs" style="font-size:10px;padding:1px 4px;background:#2196F3;color:#fff" onclick="event.stopPropagation();PlcConfigPage._addSigIO('+s.no+',\''+escHtml(s.saddr)+'\',\''+escHtml(s.desc)+'\',1)">入</button><button class="btn btn-xs" style="font-size:10px;padding:1px 4px;background:#4CAF50;color:#fff;margin-left:2px" onclick="event.stopPropagation();PlcConfigPage._addSigIO('+s.no+',\''+escHtml(s.saddr)+'\',\''+escHtml(s.desc)+'\',0)">出</button>';}h+='</div>';}
list.innerHTML=h||'<div style="color:var(--text-muted);text-align:center;padding:20px">无匹配信号</div>';
[].forEach.call(list.querySelectorAll('.plc-sig-row'),function(row){});
}
/* ================================================================
* 查看模式
* ================================================================ */
function renderViewMode(main) {
main.innerHTML = renderSigPanel()
+ '<div class="card" style="flex:1;display:flex;flex-direction:column;overflow:hidden">'
+ '<div class="card-header"><span>👁️ 逻辑图查看</span>'
+ '<select id="plc-view-graph" style="margin:0 8px;padding:4px 8px;border:1px solid var(--border);border-radius:4px;background:var(--bg-input);color:var(--text-primary);font-size:12px;min-width:240px"><option value="">-- 选择逻辑图 --</option></select>'
+ '<span style="flex:1"></span>'
+ '<button class="btn btn-sm btn-outline" id="plc-view-zoomin">🔍+</button>'
+ '<button class="btn btn-sm btn-outline" id="plc-view-zoomout">🔍−</button>'
+ '<button class="btn btn-sm btn-outline" id="plc-view-reset">↺</button>'
+ '<button class="btn btn-sm btn-primary" id="plc-view-reload">🔄 刷新</button>'
+ '</div>'
+ '<div style="flex:1;position:relative;min-height:400px" id="plc-view-canvas-wrap"><canvas id="plc-view-canvas"></canvas></div>'
+ '<div style="padding:6px 12px;font-size:11px;color:var(--text-muted);border-top:1px solid var(--border)" id="plc-view-legend"></div>'
+ '</div>';
var lh='<span style="font-weight:700;color:var(--text-secondary)">图例:</span> ';
for(var t in NODE_INFO){var inf=NODE_INFO[t];lh+='<span style="display:inline-block;margin:2px 4px;padding:1px 6px;border-radius:3px;background:'+inf.color+';color:#fff;font-size:11px">'+inf.icon+' '+inf.name+'</span>';}
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='<option value="">-- 选择逻辑图 --</option>';for(var i=0;i<logicGraphs.length;i++){var g=logicGraphs[i],bak=g._source&&g._source.indexOf('.bak')>=0?' [备份]':'';sel.innerHTML+='<option value="'+i+'">'+(g.key||'?')+' — '+(g.name||'(未命名)')+bak+' ('+g.nodes.length+'节点 '+g.links.length+'连线)</option>';}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;vi<g.nodes.length;vi++){var vv=g.nodes[vi];if(vv.x<bMinX)bMinX=vv.x;if(vv.x>bMaxX)bMaxX=vv.x;if(vv.y<bMinY)bMinY=vv.y;if(vv.y>bMaxY)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<g.links.length;vj++){var vlk=g.links[vj],vls=-1,vld=-1;for(var vk=0;vk<g.nodes.length;vk++){var vln=g.nodes[vk];if(vls<0&&vln.type===vlk.src_type&&(vln.type<=1?vln.id===vlk.src_id:(vln.id&0xFF)===(vlk.src_id&0xFF)))vls=vk;if(vld<0&&vln.type===vlk.dst_type&&(vln.type<=1?vln.id===vlk.dst_id:(vln.id&0xFF)===(vlk.dst_id&0xFF)))vld=vk;}if(vls>=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<sx+W/viewScale+gs;gx+=gs){ctx.beginPath();ctx.moveTo(gx,sy-gs);ctx.lineTo(gx,sy+H/viewScale+gs);ctx.stroke();}for(var gy=sy-gs;gy<sy+H/viewScale+gs;gy+=gs){ctx.beginPath();ctx.moveTo(sx-gs,gy);ctx.lineTo(sx+W/viewScale+gs,gy);ctx.stroke();}drawGraphContent(ctx,vn,vl,-1,-1,null);ctx.restore();}
/* ================================================================
* 编辑模式
* ================================================================ */
function renderEditMode(main) {
editNodes=[];editLinks=[];editKey='';editName='';editSelectedNode=-1;editSelectedLink=-1;
editWiring=null;editDragNode=null;editPanX=0;editPanY=0;editScale=1.0;editNextGateIdx={};
main.innerHTML = renderSigPanel()
+ renderOpPanel()
+ '<div style="flex:1;display:flex;flex-direction:column;gap:8px">'
+ '<div class="card"><div class="card-header"><span>✏️ 编辑配置</span>'
+ '<span style="flex:1"></span>'
+ '<span style="font-size:12px;color:var(--text-secondary);margin-right:8px">基础:</span>'
+ '<select id="plc-edit-base" style="margin-right:8px;padding:4px 8px;border:1px solid var(--border);border-radius:4px;background:var(--bg-input);color:var(--text-primary);font-size:12px">'
+ '<option value="current">当前配置</option><option value="backup">备份配置</option><option value="empty">空白新建</option>'
+ '</select>'
+ '<button class="btn btn-sm btn-primary" id="plc-edit-load">📥 加载</button>'
+ '</div></div>'
+ '<div class="card" style="flex:1;display:flex;flex-direction:column;overflow:hidden">'
+ '<div style="padding:6px 8px;display:flex;gap:4px;align-items:center;flex-wrap:wrap;border-bottom:1px solid var(--border)">'
+ '<input type="text" id="plc-edit-key" placeholder="Key" style="width:100px;padding:4px 8px;border:1px solid var(--border);border-radius:4px;background:var(--bg-input);color:var(--text-primary);font-size:12px">'
+ '<input type="text" id="plc-edit-name" placeholder="名称" style="width:130px;padding:4px 8px;border:1px solid var(--border);border-radius:4px;background:var(--bg-input);color:var(--text-primary);font-size:12px">'
+ '<span style="flex:1"></span>'
+ '<button class="btn btn-sm" style="background:#2196F3;color:#fff" id="plc-edit-add-in">📥 输入</button>'
+ '<button class="btn btn-sm btn-outline" id="plc-edit-add-gate">🧮 运算符</button>'
+ '<button class="btn btn-sm" style="background:#4CAF50;color:#fff" id="plc-edit-add-out">📤 输出</button>'
+ '<button class="btn btn-sm btn-outline" id="plc-edit-modify">✏️ 修改</button>'
+ '<button class="btn btn-sm btn-danger" id="plc-edit-del">🗑 删除</button>'
+ '<button class="btn btn-sm btn-outline" id="plc-edit-zoomin">🔍+</button>'
+ '<button class="btn btn-sm btn-outline" id="plc-edit-zoomout">🔍−</button>'
+ '<button class="btn btn-sm btn-primary" id="plc-edit-generate">✅ 生成</button>'
+ '</div>'
+ '<div style="flex:1;position:relative;min-height:400px" id="plc-edit-canvas-wrap"><canvas id="plc-edit-canvas" tabindex="0"></canvas></div>'
+ '<div style="padding:4px 12px;font-size:11px;color:var(--text-muted);border-top:1px solid var(--border)" id="plc-edit-info">💡 双击修改 | 单击选中/连线 | 右键删除连线 | 滚轮缩放 | Delete删除</div>'
+ '</div></div>';
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;gi<logicGraphs.length;gi++){if(logicGraphs[gi]._source&&(logicGraphs[gi]._source===srcName||logicGraphs[gi]._source.endsWith('/'+srcName))){g=logicGraphs[gi];break;}}if(!g&&logicGraphs.length>0)g=logicGraphs[0];if(!g){resetEditor();return;}editNodes=[];editLinks=[];editKey=g.key;editName=g.name||'';for(var i=0;i<g.nodes.length;i++){var n=g.nodes[i];var sig=findSignalByNo(n.id);var nd={type:n.type,id:n.id,x:n.x,y:n.y,_lx:n.x,_ly:n.y,saddr:(sig&&sig.saddr)||'',desc:(sig&&sig.desc)||'',no:n.id};if(n.type!==NT_INPUT&&n.type!==NT_OUTPUT)nd.inPins=(n.inPins!==undefined)?n.inPins:((NODE_INFO[n.type]||{}).inPins||2);if(n.delayMs!==undefined)nd.delayMs=n.delayMs;if(n.combId!==undefined)nd.combId=n.combId;if(n.rsR)nd.rsR=n.rsR;if(n.rsS)nd.rsS=n.rsS;editNodes.push(nd);}for(var j=0;j<g.links.length;j++){var l=g.links[j],si=findEditIdx(l.src_type,l.src_id),di=findEditIdx(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);}}var mx={};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>=(mx[dn.type]||-1))mx[dn.type]=idx;}for(var t in mx)editNextGateIdx[t]=mx[t]+1;var seen={};for(var di=0;di<editNodes.length;di++){var dn=editNodes[di];if(dn.type===NT_INPUT||dn.type===NT_OUTPUT)continue;var k=dn.type+':'+(dn.id&0xFF);if(seen[k]){var nxt=editNextGateIdx[dn.type]||0;editNodes[di].id=(dn.id&~0xFF)|nxt;editNextGateIdx[dn.type]=nxt+1;}else seen[k]=true;}var kel=document.getElementById('plc-edit-key'),nel=document.getElementById('plc-edit-name');if(kel)kel.value=editKey;if(nel)nel.value=editName;drawEditor();}
function findEditIdx(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;}
/* ---- 编辑器 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<editLinks.length;lc++)if(editLinks[lc].srcIdx===editWiring.fromIdx)oc++;if(oc>=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<editLinks.length;lc2++){if(editLinks[lc2].dstIdx===ph.nodeIdx&&editLinks[lc2].dstPin===ph.pinIdx){showToast('引脚已连线','err');editWiring=null;editSelectedNode=-1;drawEditor();return;}}}editLinks.push(lk);showToast('连线已添加','ok');}else if(editWiring.fromSide==='in'&&ph.side==='out'){editLinks.push({srcIdx:ph.nodeIdx,dstIdx:editWiring.fromIdx,dstPin:0});showToast('连线已添加','ok');}else{showToast('不能这样连线','err');}}editWiring=null;editSelectedNode=-1;editSelectedLink=-1;drawEditor();return;}else{editWiring={fromIdx:ph.nodeIdx,fromSide:ph.side,fromPin:ph.pinIdx};editSelectedNode=-1;editSelectedLink=-1;drawEditor();return;}}var lh=hitTestLink(mx,my);if(lh>=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<editNodes.length)drawEditor();var pin=hitTestPin(mx,my);cnv.style.cursor=pin?'crosshair':(hitTestNode(mx,my)>=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&&mx<n._lx+sz.w/2&&my>n._ly-sz.h/2&&my<n._ly+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],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;p<tp;p++){var py=n._ly-sz.h/2+sz.h*(p+1)/(isSR?3:(inP+1));if(Math.sqrt((mx-(n._lx-sz.w/2))*(mx-(n._lx-sz.w/2))+(my-py)*(my-py))<=pinR)return{nodeIdx:i,side:'in',pinIdx:p};}}if(info.outPins>0){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<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],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<sx+W/editScale+gs;gx+=gs){ctx.beginPath();ctx.moveTo(gx,sy-gs);ctx.lineTo(gx,sy+H/editScale+gs);ctx.stroke();}for(var gy=sy-gs;gy<sy+H/editScale+gs;gy+=gs){ctx.beginPath();ctx.moveTo(sx-gs,gy);ctx.lineTo(sx+W/editScale+gs,gy);ctx.stroke();}ctx.fillStyle='rgba(255,255,255,0.08)';ctx.font='24px sans-serif';ctx.textAlign='center';ctx.fillText('📥 输入区',-250,-180);ctx.fillText('🧮 逻辑区',0,-180);ctx.fillText('📤 输出区',250,-180);drawGraphContent(ctx,editNodes,editLinks,editSelectedNode,editSelectedLink,editWiring);ctx.restore();var ie=document.getElementById('plc-edit-info');if(ie){var si='';if(editSelectedNode>=0&&editSelectedNode<editNodes.length)si=' | 🔍 '+getNodeDisplayLabel(editNodes[editSelectedNode]);ie.innerHTML='节点:'+editNodes.length+' | 连线:'+editLinks.length+' | Key:'+escHtml(editKey)+' | '+escHtml(editName||'(未命名)')+si+(editWiring?' | 🔗 连线...':'')+' | 右键删除连线';}}
/* ---- 节点操作 ---- */
function onNodeDblClick(idx){var nd=editNodes[idx];editSelectedNode=idx;drawEditor();if(nd.type===NT_INPUT||nd.type===NT_OUTPUT)showRebindModal(idx);else if(nd.type===NT_62P||nd.type===NT_62D)showDelayEditModal(idx);else if(nd.type===NT_SR||nd.type===NT_RS)showRSInfoModal(idx);else if(nd.type>=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('🗑 确认删除','确定删除 <strong style="color:#f44336">'+escHtml(lb)+'</strong> 及其连线?',function(){var dt=nd.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;if(dt!==NT_INPUT&&dt!==NT_OUTPUT){var st=[];for(var s=0;s<editNodes.length;s++)if(editNodes[s].type===dt)st.push(s);st.sort(function(a,b){return(editNodes[a].id&0xFF)-(editNodes[b].id&0xFF);});for(var a=0;a<st.length;a++)editNodes[st[a]].id=(editNodes[st[a]].id&~0xFF)|a;editNextGateIdx[dt]=st.length;}drawEditor();showToast('已删除','ok');});}
/* ---- Modals ---- */
function showConfirm(title,msg,cb){var h='<div class="modal-overlay" id="plc-modal"><div class="modal-box" style="max-width:420px"><h3>'+title+'</h3><p style="margin:12px 0;font-size:14px">'+msg+'</p><div style="margin-top:16px;display:flex;gap:8px"><button class="btn btn-danger" id="plc-modal-ok">确认</button><button class="btn btn-outline" id="plc-modal-cancel">取消</button></div></div></div>';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='<div class="modal-overlay" id="plc-modal"><div class="modal-box" style="max-width:600px"><h3>'+title+'</h3><input type="text" id="plc-modal-search" placeholder="搜索..." style="width:100%;margin:8px 0;padding:8px;border:1px solid var(--border);border-radius:4px;background:var(--bg-input);color:var(--text-primary)"><div style="max-height:350px;overflow-y:auto" id="plc-modal-list">';for(var j=0;j<dcOutSignals.length;j++){var s=dcOutSignals[j];h+='<div class="plc-modal-row" data-no="'+s.no+'" data-saddr="'+escHtml(s.saddr)+'" data-desc="'+escHtml(s.desc)+'"><strong>#'+s.no+' '+escHtml(s.saddr)+'</strong> <span style="color:var(--text-muted)">'+escHtml(s.desc)+'</span></div>';}h+='</div><button class="btn btn-outline" id="plc-modal-cancel">取消</button></div></div>';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='<div class="modal-overlay" id="plc-modal"><div class="modal-box" style="max-width:480px"><h3>🧮 选择运算模块</h3><div style="max-height:400px;overflow-y:auto">';for(var i=0;i<gates.length;i++){var g=gates[i];h+='<div class="plc-modal-row" data-gt="'+g.t+'"><strong>'+g.i+' '+g.n+'</strong><br><span style="color:var(--text-muted);font-size:12px">'+g.d+'</span></div>';}h+='</div><button class="btn btn-outline" id="plc-modal-cancel">取消</button></div></div>';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='<div class="modal-overlay" id="plc-modal"><div class="modal-box" style="max-width:360px"><h3>⏱️ '+info.name+' — 延时配置</h3><p style="color:var(--text-muted);margin:8px 0">延时(毫秒)</p><input type="number" id="plc-delay-input" value="1000" min="0" max="600000" step="100" style="width:100%;padding:10px;border:1px solid var(--border);border-radius:6px;background:var(--bg-input);color:var(--text-primary);font-size:16px;margin:8px 0"><div style="margin-top:12px;display:flex;gap:8px"><button class="btn btn-primary" id="plc-modal-ok">✅ 确认</button><button class="btn btn-outline" id="plc-modal-cancel">取消</button></div></div></div>';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='<div class="modal-overlay" id="plc-modal"><div class="modal-box" style="max-width:360px"><h3>⏱️ '+info.name+' — 修改延时</h3><p style="color:var(--text-muted)">当前: <strong>'+cur+' ms</strong></p><input type="number" id="plc-delay-input" value="'+cur+'" min="0" max="600000" step="100" style="width:100%;padding:10px;border:1px solid var(--border);border-radius:6px;background:var(--bg-input);color:var(--text-primary);font-size:16px;margin:8px 0"><div style="margin-top:12px;display:flex;gap:8px"><button class="btn btn-primary" id="plc-modal-ok">💾 保存</button><button class="btn btn-outline" id="plc-modal-cancel">取消</button></div></div></div>';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;li<editLinks.length;li++){var l=editLinks[li];if(l.dstIdx===idx){var snl=editNodes[l.srcIdx],desc=info.icon+' '+(snl.type<=1?'输入点':NODE_INFO[snl.type].name)+' #'+(snl.type<=1?snl.id:(snl.id&0xFF));if(nd.type===NT_SR){if(l.dstPin===0)sSrc=desc;else rSrc=desc;}else{if(l.dstPin===1)sSrc=desc;else rSrc=desc;}}}var h='<div class="modal-overlay" id="plc-modal"><div class="modal-box" style="max-width:400px"><h3>🔒 '+info.name+' — 连接</h3><p style="margin:8px 0"><strong>S(置位)</strong>: '+(sSrc||'<span style="color:#f44336">未连接</span>')+'</p><p style="margin:8px 0"><strong>R(复位)</strong>: '+(rSrc||'<span style="color:#f44336">未连接</span>')+'</p><p style="color:var(--text-muted);font-size:12px;margin-top:12px">💡 拖输出到S/R引脚连线</p><button class="btn btn-outline" id="plc-modal-cancel">关闭</button></div></div>';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<editLinks.length;li++){var l=editLinks[li];if(l.dstIdx===idx){var snl=editNodes[l.srcIdx],desc=(snl.type<=1?'输入点':(NODE_INFO[snl.type]||{}).icon+' '+(NODE_INFO[snl.type]||{}).name)+' #'+(snl.type<=1?snl.id:(snl.id&0xFF));if(l.dstPin===0)aSrc=desc;else if(l.dstPin===1)bSrc=desc;}}var orderNote='';if(nd.type===NT_MX_SUB)orderNote='<p style="color:#FF9800;font-size:12px;margin-top:8px">⚠️ 顺序敏感: A - B</p>';else if(nd.type===NT_MX_DIV)orderNote='<p style="color:#FF9800;font-size:12px;margin-top:8px">⚠️ 顺序敏感: A ÷ B (B=0→结果为0)</p>';else if(nd.type===NT_MX_CMP)orderNote='<p style="color:#FF9800;font-size:12px;margin-top:8px">⚠️ 顺序敏感: A > B → 1否则 → 0</p>';else orderNote='<p style="color:var(--text-muted);font-size:12px;margin-top:8px">💡 输入可交换,顺序不影响结果</p>';var h='<div class="modal-overlay" id="plc-modal"><div class="modal-box" style="max-width:400px"><h3>'+info.icon+' '+info.name+' — 连接</h3><p style="margin:8px 0"><strong>A</strong>: '+(aSrc||'<span style="color:#f44336">未连接</span>')+'</p><p style="margin:8px 0"><strong>B</strong>: '+(bSrc||'<span style="color:#f44336">未连接</span>')+'</p>'+orderNote+'<button class="btn btn-outline" id="plc-modal-cancel">关闭</button></div></div>';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='<div class="modal-overlay" id="plc-modal"><div class="modal-box" style="max-width:380px"><h3>'+info.icon+' '+info.name+' 配置</h3><p style="color:var(--text-muted)">当前输入: <strong>'+cur+'</strong></p><label style="display:block;margin:12px 0 4px">输入引脚数(1~'+mx+'):</label><input type="number" id="plc-gate-pins" value="'+cur+'" min="1" max="'+mx+'" style="width:100%;padding:8px;border:1px solid var(--border);border-radius:4px;background:var(--bg-input);color:var(--text-primary);font-size:16px"><div style="margin-top:16px;display:flex;gap:8px"><button class="btn btn-primary" id="plc-modal-save">💾 保存</button><button class="btn btn-outline" id="plc-modal-cancel">取消</button></div></div></div>';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;i<editNodes.length;i++){var n=editNodes[i];if(n._lx>mxX)mxX=n._lx;if(n._lx<mnX)mnX=n._lx;if(n._ly>mxY)mxY=n._ly;if(n._ly<mnY)mnY=n._ly;}var cs=[{x:0,y:mxY+140},{x:0,y:mnY-140},{x:mxX+300,y:(mnY+mxY)/2},{x:mnX-300,y:(mnY+mxY)/2}];for(var c=0;c<cs.length;c++){var occ=false;for(var i2=0;i2<editNodes.length;i2++){if(Math.abs(editNodes[i2]._lx-cs[c].x)<120&&Math.abs(editNodes[i2]._ly-cs[c].y)<80){occ=true;break;}}if(!occ)return cs[c];}return{x:0,y:mxY+140};}
/* ================================================================
* 共用 Canvas 绘制
* ================================================================ */
function drawGraphContent(ctx,nodes,links,selectedIdx,selectedLinkIdx,wiring){var IW=260,IH=90,GW=180,GH=72,CR=12,SO=3,LBW=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 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.fromIdx<nodes.length){var fn=nodes[wiring.fromIdx],fIO=(fn.type===NT_OUTPUT||fn.type===NT_INPUT),fW=fIO?IW:GW;var fx=wiring.fromSide==='out'?fn._lx+fW/2:fn._lx-fW/2,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],info=NODE_INFO[n.type]||{name:'?',color:'#555',icon:'?'};var isIO=(n.type===NT_OUTPUT||n.type===NT_INPUT),inP=getNodeInPins(n);var nw=isIO?IW:GW,nh=isIO?IH:(GH+Math.max(0,inP-2)*14);var x=n._lx,y=n._ly,hw=nw/2,hh=nh/2;var isWS=wiring&&wiring.fromIdx===j,isS=(selectedIdx===j);var gw=(isS||isWS)?3:1;ctx.fillStyle='rgba(0,0,0,0.5)';ctx.beginPath();ctx.roundRect(x-hw+SO,y-hh+SO,nw,nh,CR);ctx.fill();var grad=ctx.createLinearGradient(x-hw,y-hh,x-hw,y+hh);if(isS){grad.addColorStop(0,'#3b3b6b');grad.addColorStop(1,'#282848');}else if(isWS){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-hw,y-hh,nw,nh,CR);ctx.fill();ctx.strokeStyle=isS?'#FFD700':(isWS?'#FFC107':info.color);ctx.lineWidth=gw;ctx.beginPath();ctx.roundRect(x-hw,y-hh,nw,nh,CR);ctx.stroke();ctx.fillStyle=info.color;ctx.beginPath();ctx.roundRect(x-hw,y-hh,LBW,nh,[CR,0,0,CR]);ctx.fill();if(info.name){var lbx=x-hw+LBW,lbw=info.name.length*9+20,lby=y-hh+2,lbh=16;ctx.fillStyle=info.color;ctx.globalAlpha=0.25;ctx.fillRect(lbx,lby,Math.min(lbw,nw-LBW-4),lbh);ctx.globalAlpha=1;ctx.fillStyle=isIO?'#fff':info.color;ctx.font='11px sans-serif';ctx.textAlign='left';ctx.textBaseline='top';ctx.fillText(info.name,lbx+4,lby+2.5);}ctx.textAlign='center';ctx.textBaseline='middle';if(isIO){var sig=n.saddr?n:findSignalByNo(n.id),pfx=n.type===NT_INPUT?'📥':'📤',rl=n.type===NT_INPUT?'输入点':'输出点';ctx.fillStyle='#e0e0e0';ctx.font='bold 14px sans-serif';ctx.fillText(pfx+' '+rl+' #'+n.id,x,y-18);ctx.fillStyle='#aaa';ctx.font='12px monospace';ctx.fillText((sig&&sig.saddr)?sig.saddr:'—',x,y);var dt2=(sig&&sig.desc)?sig.desc:'';if(dt2){ctx.fillStyle='#888';ctx.font='11px sans-serif';ctx.fillText(dt2.length>18?dt2.substring(0,16)+'…':dt2,x,y+14);}}else{var gi=n.id&0xFF,ri=0,ro=0;for(var li=0;li<links.length;li++){if(links[li].dstIdx===j)ri++;if(links[li].srcIdx===j)ro++;}ctx.fillStyle='#e8e8e8';ctx.font='bold 13px sans-serif';var tt=info.icon+' #'+gi;if(n.type===NT_62P||n.type===NT_62D)tt+=' ('+(n.delayMs||1000)+'ms)';ctx.fillText(tt,x,y-6);ctx.fillStyle='#999';ctx.font='11px monospace';ctx.fillText(ri+'入 '+ro+'出',x,y+14);}if(inP>0){var isSR=(n.type===NT_SR||n.type===NT_RS),rp=isSR?2:(isIO?inP:inP);for(var p=0;p<rp;p++){var py=y-nh/2+nh*(p+1)/(isSR?3:(inP+1));ctx.fillStyle='#2e2e4a';ctx.beginPath();ctx.arc(x-hw,py,7,0,Math.PI*2);ctx.fill();ctx.strokeStyle=isSR?(p===0?'#FFC107':'#FF5722'):'#4CAF50';ctx.lineWidth=2;ctx.stroke();ctx.fillStyle=isSR?(p===0?'#FFC107':'#FF5722'):'#4CAF50';ctx.beginPath();ctx.arc(x-hw,py,3.5,0,Math.PI*2);ctx.fill();if(isSR){ctx.fillStyle='#fff';ctx.font='bold 12px monospace';ctx.textAlign='right';ctx.textBaseline='middle';var sl=(n.type===NT_SR)?['S','R']:['R','S'];ctx.fillText(sl[p],x-hw-12,py);}var isMX=(n.type>=50&&n.type<=57);if(isMX){ctx.fillStyle='#fff';ctx.font='bold 12px 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<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(ce){var ge=ce.firstElementChild;while(ge&&ge.nodeName!=='Gate')ge=ge.nextElementSibling;if(!ge)return -1;var t=GTM[ge.getAttribute('type')]||0,gid=parseInt(ge.getAttribute('id'));var gi=eng(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);var cid=ce.getAttribute('id');if(cid)g.nodes[gi].combId=parseInt(cid);var ra=ge.getAttribute('R'),sa=ge.getAttribute('S');if(ra)g.nodes[gi].rsR=ra;if(sa)g.nodes[gi].rsS=sa;var c=ce.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=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<chains.length;ci++){var chain=chains[ci],c=chain.firstElementChild;while(c){if(c.nodeName==='Inputs'){var s=c.firstElementChild;while(s){if(s.nodeName==='Signal')ens(NT_INPUT,parseInt(s.getAttribute('no')),parseInt(s.getAttribute('x')),parseInt(s.getAttribute('y')));s=s.nextElementSibling;}}if(c.nodeName==='Outputs'){var s2=c.firstElementChild;while(s2){if(s2.nodeName==='Signal')ens(NT_OUTPUT,parseInt(s2.getAttribute('no')),parseInt(s2.getAttribute('x')),parseInt(s2.getAttribute('y')));s2=s2.nextElementSibling;}}if(c.nodeName==='Comb')parseComb(c);if(c.nodeName==='Gate'){var gt=GTM[c.getAttribute('type')]||0,gi2=eng(gt,parseInt(c.getAttribute('id')),parseInt(c.getAttribute('x')),parseInt(c.getAttribute('y')));var ra=c.getAttribute('R'),sa=c.getAttribute('S'),dms=c.getAttribute('delayMs');if(ra)g.nodes[gi2].rsR=ra;if(sa)g.nodes[gi2].rsS=sa;if(dms)g.nodes[gi2].delayMs=parseInt(dms);}c=c.nextElementSibling;}}for(var ci2=0;ci2<chains.length;ci2++){var c2=chains[ci2];var hc=false,c3=c2.firstElementChild;while(c3){if(c3.nodeName==='Comb'){hc=true;break;}c3=c3.nextElementSibling;}var ge2=null,ie2=null,oe2=null;c3=c2.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(hc&&ge2){c3=c2.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,cid2=parseInt(cg.getAttribute('id'));g.links.push({src_type:ct,src_id:cid2,dst_type:GTM[ge2.getAttribute('type')]||0,dst_id:parseInt(ge2.getAttribute('id'))});}}c3=c3.nextElementSibling;}var alrIn={};if(ie2){var s3=ie2.firstElementChild;while(s3){if(s3.nodeName==='Signal'){var n2=parseInt(s3.getAttribute('no'));alrIn[n2]=true;g.links.push({src_type:NT_INPUT,src_id:n2,dst_type:GTM[ge2.getAttribute('type')]||0,dst_id:parseInt(ge2.getAttribute('id'))});}s3=s3.nextElementSibling;}}if(ge2){var gtCS=GTM[ge2.getAttribute('type')]||0;if(gtCS===NT_SR||gtCS===NT_RS){var sa2=ge2.getAttribute('S'),ra2=ge2.getAttribute('R');if(sa2&&sa2.indexOf('Input#')===0){var sn2=parseInt(sa2.substring(6));if(!alrIn[sn2]){ens(NT_INPUT,sn2,0,0);g.links.push({src_type:NT_INPUT,src_id:sn2,dst_type:gtCS,dst_id:parseInt(ge2.getAttribute('id'))});}}if(ra2&&ra2.indexOf('Input#')===0){var rn2=parseInt(ra2.substring(6));if(!alrIn[rn2]){ens(NT_INPUT,rn2,0,0);g.links.push({src_type:NT_INPUT,src_id:rn2,dst_type:gtCS,dst_id:parseInt(ge2.getAttribute('id'))});}}}}if(oe2){var s4=oe2.firstElementChild;while(s4){if(s4.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(s4.getAttribute('no'))});s4=s4.nextElementSibling;}}}else if(!hc&&ge2){var gt2=GTM[ge2.getAttribute('type')]||0,gid2=parseInt(ge2.getAttribute('id'));if(ie2){var s5=ie2.firstElementChild;while(s5){if(s5.nodeName==='Signal')g.links.push({src_type:NT_INPUT,src_id:parseInt(s5.getAttribute('no')),dst_type:gt2,dst_id:gid2});s5=s5.nextElementSibling;}}if(oe2){var s6=oe2.firstElementChild;while(s6){if(s6.nodeName==='Signal')g.links.push({src_type:gt2,src_id:gid2,dst_type:NT_OUTPUT,dst_id:parseInt(s6.getAttribute('no'))});s6=s6.nextElementSibling;}}}}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 sS=nd.rsS||'',rS=nd.rsR||'';if(!sS&&!rS)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 sd='',sN=null;for(var sk=0;sk<g.nodes.length;sk++)if(g.nodes[sk].type===lk.src_type&&(g.nodes[sk].type<=1?g.nodes[sk].id===lk.src_id:(g.nodes[sk].id&0xFF)===(lk.src_id&0xFF))){sN=g.nodes[sk];break;}if(!sN)continue;if(sN.type===NT_INPUT)sd='Input#'+sN.id;else sd='Comb#'+(sN.combId!==undefined?sN.combId:(sN.id&0xFF));if(nd.type===NT_SR){if(sS&&sd===sS)lk.dstPin=0;else if(rS&&sd===rS)lk.dstPin=1;}else{if(sS&&sd===sS)lk.dstPin=1;else if(rS&&sd===rS)lk.dstPin=0;}}}g._source=sourceName;logicGraphs=logicGraphs.filter(function(x){return x._source!==sourceName;});logicGraphs.push(g);}
/* ================================================================
* XML 生成 + 保存
* ================================================================ */
function findLinkPin(srcIdx, dstIdx) { for (var lk = 0; lk < editLinks.length; lk++) { if (editLinks[lk].srcIdx === srcIdx && editLinks[lk].dstIdx === dstIdx) return editLinks[lk].dstPin; } return -1; }
function generateXmlConfig() {
var tN = {2:'OR',3:'AND',4:'NOT',5:'T62P',6:'T62D',7:'RISING',8:'SR',9:'RS',10:'FALLING',11:'TOGGLE',50:'MX_ADD',51:'MX_SUB',52:'MX_MAX',53:'MX_MIN',54:'MX_MUL',55:'MX_DIV',56:'MX_ABS',57:'MX_CMP'};
var aI = {}, aO = {};
for (var i = 0; i < editNodes.length; i++) { aI[i] = []; aO[i] = []; }
for (var j = 0; j < editLinks.length; j++) {
var l = editLinks[j];
aO[l.srcIdx].push(l.dstIdx);
aI[l.dstIdx].push(l.srcIdx);
}
var vis = {}, chs = [];
for (var s = 0; s < editNodes.length; s++) {
if (vis[s]) continue;
var c = [], q = [s]; vis[s] = true;
while (q.length > 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 = '<Signal no="' + n.id + '" x="' + Math.round(n._lx) + '" y="' + Math.round(n._ly) + '"'; if (pin !== undefined && pin >= 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 '<Gate' + a + ' />';
}
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 + '<Comb id="' + (cid !== undefined ? cid : 0) + '">\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 + '<Inputs>\n';
for (var d = 0; d < di2.length; d++) s2 += i2 + ' ' + sx(editNodes[di2[d]]) + '\n';
s2 += i2 + '</Inputs>\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 + '<Outputs>\n';
for (var o = 0; o < dos.length; o++) s2 += i2 + ' ' + sx(editNodes[dos[o]]) + '\n';
s2 += i2 + '</Outputs>\n';
}
s2 += ind + '</Comb>\n';
return s2;
}
var cGs = [];
var xml = '<?xml version="1.0" encoding="UTF-8"?>\n';
xml += '<PLCConfig name="' + escXml(editName || 'PLC Logic') + '" key="' + escXml(editKey) + '" checksum="00000000">\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 += ' <Chain gates="' + cGs.length + '" inputs="' + cis.length + '" outputs="' + cos.length + '">\n';
if (cGs.length === 1) {
xml += ' <Inputs>\n';
for (var ii = 0; ii < cis.length; ii++) xml += ' ' + sx(editNodes[cis[ii]]) + '\n';
xml += ' </Inputs>\n';
xml += ' ' + gt(editNodes[cGs[0]], cGs[0]) + '\n';
xml += ' <Outputs>\n';
for (var oo = 0; oo < cos.length; oo++) xml += ' ' + sx(editNodes[cos[oo]]) + '\n';
xml += ' </Outputs>\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 += ' <Inputs>\n';
for (var di3 = 0; di3 < dis.length; di3++) xml += ' ' + sx(editNodes[dis[di3]]) + '\n';
xml += ' </Inputs>\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 += ' <Outputs>\n';
for (var oo3 = 0; oo3 < coL.length; oo3++) xml += ' ' + sx(editNodes[coL[oo3]]) + '\n';
xml += ' </Outputs>\n';
}
}
xml += ' </Chain>\n';
}
xml += '</PLCConfig>\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;di<editNodes.length;di++){var dn=editNodes[di];if(dn.type===NT_INPUT||dn.type===NT_OUTPUT)continue;var k=dn.type+':'+(dn.id&0xFF);if(seen[k]){var nxt=editNextGateIdx[dn.type]||0;editNodes[di].id=(dn.id&~0xFF)|nxt;editNextGateIdx[dn.type]=nxt+1;}else seen[k]=true;}var fc=generateXmlConfig(),pv=fc.length>2000?fc.substring(0,2000)+'…':fc;var h='<div class="modal-overlay" id="plc-modal"><div class="modal-box" style="max-width:700px"><h3>✅ 配置预览</h3><p>Key: <strong>'+escHtml(editKey)+'</strong> | '+escHtml(editName||'PLC Logic')+'</p><p>节点:'+editNodes.length+' | 连线:'+editLinks.length+'</p><div style="max-height:250px;overflow:auto;background:var(--bg-deep);color:#0f0;padding:12px;border-radius:6px;font-family:monospace;font-size:12px;white-space:pre-wrap">'+escHtml(pv)+'</div><div style="margin-top:12px;display:flex;gap:8px"><button class="btn btn-primary" id="plc-modal-save">💾 保存配置</button><button class="btn btn-outline" id="plc-modal-cancel">取消</button></div></div></div>';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<rty.length;ri++)requestFile(rty[ri],PlcConfigPage._pending[rty[ri]]||function(){});}if(dcOutSignals.length===0)loadDcOutSignals();}
function onLeave(){editWiring=null;editDragNode=null;editSelectedNode=-1;editSelectedLink=-1;}
function onData(data){if(!data||typeof data!=='object')return;if(data.type==='file_content'){var p=data.path,c=data.content||'',e=data.error||'';loadedFiles[p]=e?null:c;if(PlcConfigPage._pending&&PlcConfigPage._pending[p]){PlcConfigPage._pending[p](e?null:c);delete PlcConfigPage._pending[p];}if(PlcConfigPage._retryList){var rIdx=PlcConfigPage._retryList.indexOf(p);if(rIdx>=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); });