/**
 * @file    plc_config.js
 * @brief   PLC 逻辑配置页 — 信号列表 + 逻辑图查看器 + 编辑器
 * @details Canvas 可视化 + XML 解析/生成, 11 种门类型
 */
/* ========================================================================
 * PLC 逻辑配置页面 — 信号管理 + 逻辑图查看 + 新建配置编辑器
 * ======================================================================== */
var PlcConfigPage = (function() {
	"use strict";

	/* ---- Canvas roundRect polyfill (兼容旧浏览器) ---- */
	if (!CanvasRenderingContext2D.prototype.roundRect) {
		CanvasRenderingContext2D.prototype.roundRect = function(x, y, w, h, r) {
			if (typeof r === 'number') r = { tl: r, tr: r, br: r, bl: r };
			else if (Array.isArray(r)) r = { tl: r[0]||0, tr: r[1]||r[0]||0, br: r[2]||r[0]||0, bl: r[3]||r[0]||0 };
			this.beginPath();
			this.moveTo(x + r.tl, y);
			this.lineTo(x + w - r.tr, y);
			this.quadraticCurveTo(x + w, y, x + w, y + r.tr);
			this.lineTo(x + w, y + h - r.br);
			this.quadraticCurveTo(x + w, y + h, x + w - r.br, y + h);
			this.lineTo(x + r.bl, y + h);
			this.quadraticCurveTo(x, y + h, x, y + h - r.bl);
			this.lineTo(x, y + r.tl);
			this.quadraticCurveTo(x, y, x + r.tl, y);
			this.closePath();
		};
	}

	/* ========== 全局状态 ========== */
	var activeTab = 'signals';          // signals | viewer | editor
	var dcOutSignals = [];              // [{no, saddr, desc}] 从 datacenter_out.xml
	var logicGraphs = [];               // 已解析的逻辑图数组
	var onlineCfgId = '';
	var loadedFiles = {};               // {path: content} 记录已加载的文件

	/* ======== 编辑器状态 ======== */
	var editNodes = [];                 // [{type, id, x, y, saddr, desc, no, inPins, _lx, _ly}]
	var editLinks = [];                 // [{srcIdx, dstIdx, dstPin}]  — dstPin 目标节点输入引脚序号
	var editKey = '';                  // 逻辑图 key
	var editName = '';                 // 逻辑图名称
	var editOnlineCfgId = '';          // 当前 online_cfgId
	var editSelectedNode = -1;         // 当前选中节点索引，-1=无选中
	var editSelectedLink = -1;         // 当前选中连线索引，-1=无选中
	var editWiring = null;             // {fromNodeIdx, fromSide, fromPin} 当前连线起点
	var editDragNode = null;           // 拖拽中的节点索引
	var editDragOffX = 0, editDragOffY = 0;
	var editDragCanvas = null;         // 画布拖拽状态
	var editPanX = 0, editPanY = 0;    // 画布偏移
	var editScale = 1.0;               // 缩放
	var editNextGateIdx = {};           // {type: nextIdx} 每种门类型独立序号从0开始
	var editLoadedFromFile = false;    // 是否从文件加载
	var editLastClickTime = 0;         // 上次 click 时间戳（双击检测）
	var editLastClickNode = -1;        // 上次 click 的节点索引

	/* ========== 查看器状态 ========== */
	var viewPanX = 0, viewPanY = 0;
	var viewScale = 1.0;
	var viewDrag = null;               // {startX, startY, panX, panY}
	var viewAutoFit = true;            // 首次 / resize 后自动适配缩放

	/* ========== 节点类型常量 ========== */
	var NT_OUTPUT = 0, NT_INPUT = 1, NT_OR = 2, NT_AND = 3, NT_NOT = 4;
	var NT_62P = 5, NT_62D = 6, NT_RISING = 7, NT_SR = 8, NT_RS = 9, NT_FALLING = 10;

	var NODE_INFO = {
		0:  { name: '输出点',   cls: 'node-out',  icon: '📤', color: '#4CAF50', inPins: 1, outPins: 0, inMax: 1,  outMax: 0 },
		1:  { name: '输入点',   cls: 'node-in',   icon: '📥', color: '#2196F3', inPins: 0, outPins: 1, inMax: 0,  outMax: 99 },
		2:  { name: '或门 OR',  cls: 'node-or',   icon: '🔶', color: '#FF9800', inPins: 2, outPins: 1, inMax: 10, outMax: 10 },
		3:  { name: '与门 AND', cls: 'node-and',  icon: '🔷', color: '#9C27B0', inPins: 2, outPins: 1, inMax: 10, outMax: 10 },
		4:  { name: '非门 NOT', cls: 'node-not',  icon: '🔺', color: '#f44336', inPins: 1, outPins: 1, inMax: 1,  outMax: 10 },
		5:  { name: '62p延时',  cls: 'node-62p',  icon: '⏱️', color: '#00BCD4', inPins: 1, outPins: 1, inMax: 1,  outMax: 10 },
		6:  { name: '62d延时',  cls: 'node-62d',  icon: '⏱️', color: '#009688', inPins: 1, outPins: 1, inMax: 1,  outMax: 10 },
		7:  { name: '上升沿',   cls: 'node-re',   icon: '⬆️', color: '#E91E63', inPins: 1, outPins: 1, inMax: 1,  outMax: 10 },
		8:  { name: 'SR触发器', cls: 'node-sr',   icon: '🔒', color: '#795548', inPins: 2, outPins: 1, inMax: 2,  outMax: 10 },
		9:  { name: 'RS触发器', cls: 'node-rs',   icon: '🔒', color: '#607D8B', inPins: 2, outPins: 1, inMax: 2,  outMax: 10 },
		10: { name: '下降沿',   cls: 'node-fe',   icon: '⬇️', color: '#FF5722', inPins: 1, outPins: 1, inMax: 1,  outMax: 10 }
	};

	/* ========== 工具函数 ========== */
	function fmtVal(v) { return (v === null || v === undefined) ? '' : String(v); }
	function escHtml(s) { return String(s).replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;').replace(/"/g,'&quot;'); }

	/* ---- 通过节点 ID 查找信号信息 (datacenter out 中 no 即 ID) ---- */
	function findSignalByNo(signalNo) {
		for (var i = 0; i < dcOutSignals.length; i++) {
			if (dcOutSignals[i].no === signalNo) return dcOutSignals[i];
		}
		return null;
	}

	/* ---- 生成节点显示标签 (十进制ID, 含saddr/desc) ---- */
	function getNodeDisplayLabel(node) {
		var info = NODE_INFO[node.type] || {};
		if (node.type === NT_INPUT || node.type === NT_OUTPUT) {
			var prefix = (node.type === NT_INPUT) ? '输入' : '输出';
			var decId = node.id; // id 在 parse 时已转为十进制
			var sig = findSignalByNo(decId);
			if (sig) {
				return prefix + ' #' + decId + ' ("' + sig.saddr + '", "' + sig.desc + '")';
			}
			return prefix + ' #' + decId;
		}
		// 逻辑门：显示组件序号
		var gateIdx = node.id & 0xFF;
		return info.icon + ' ' + info.name + ' #' + gateIdx;
	}

	/* ---- 生成编辑器小标签 (简洁版，用于编辑器节点) ---- */
	function getEditorNodeLabel(node) {
		var info = NODE_INFO[node.type] || {};
		if (node.type === NT_INPUT || node.type === NT_OUTPUT) {
			var prefix = (node.type === NT_INPUT) ? '输入点' : '输出点';
			var decId = node.id;
			var sig = node.saddr ? node : findSignalByNo(decId);
			if (sig && sig.saddr) {
				return info.icon + ' ' + prefix + ' #' + decId + '\n' + sig.saddr + '\n' + (sig.desc || '');
			}
			return info.icon + ' ' + prefix + ' #' + decId;
		}
		var gateIdx = node.id & 0xFF;
		return info.icon + ' ' + info.name + ' #' + gateIdx;
	}

	function showCfgToast(msg, type) { // wraps global toast
		type = type || 'info';
		var t = document.getElementById('plc-cfg-toast');
		if (!t) {
			t = document.createElement('div');
			t.id = 'plc-cfg-toast';
			t.style.cssText = 'position:fixed;top:80px;left:50%;transform:translateX(-50%);padding:10px 24px;border-radius:6px;z-index:99999;font-size:14px;box-shadow:0 4px 12px rgba(0,0,0,0.3);transition:opacity 0.3s;pointer-events:none;color:#fff';
			document.body.appendChild(t);
		}
		t.style.background = (type === 'err') ? '#f44336' : (type === 'ok') ? '#4CAF50' : '#333';
		t.textContent = msg; t.style.opacity = '1';
		clearTimeout(t._timer);
		t._timer = setTimeout(function() { t.style.opacity = '0'; }, 2000);
	}

	/* ========== WebSocket 文件请求 ========== */
	function requestFile(path, callback) {
		if (loadedFiles[path]) {
			callback(loadedFiles[path]);
			return;
		}
		if (!PlcConfigPage._pending) PlcConfigPage._pending = {};
		PlcConfigPage._pending[path] = callback;
		var sent = WsClient.send({ type: 'get_file', path: path });
		if (!sent) {
			if (!PlcConfigPage._retryList) PlcConfigPage._retryList = [];
			PlcConfigPage._retryList.push(path);
		}
	}

	/* ========== 主渲染 ========== */
	function renderTabs() {
		var tabs = [
			{ id: 'signals', label: '📋 信号列表',   title: 'datacenter_out.xml — 已注册 Out 信号' },
			{ id: 'viewer',  label: '👁️ 逻辑图查看', title: 'Reclose_logic.xml 可视化' },
			{ id: 'editor',  label: '✏️ 新建配置',   title: '新建/编辑 PLC 逻辑配置' }
		];
		var html = '<div class="plc-cfg-tabs">';
		for (var i = 0; i < tabs.length; i++) {
			var cls = (tabs[i].id === activeTab) ? 'plc-cfg-tab active' : 'plc-cfg-tab';
			html += '<button class="' + cls + '" data-tab="' + tabs[i].id + '">' + tabs[i].label + '</button>';
		}
		html += '</div>';
		return html;
	}

	function render(container) {
		container.innerHTML = ''
			+ '<div class="page-header"><h2>🔌 PLC 逻辑配置</h2><p>管理 PLC 逻辑图 — 信号源 / 逻辑图可视化 / 在线编辑</p></div>'
			+ renderTabs()
			+ '<div id="plc-cfg-content"></div>';

		// Tab 切换事件
		var tabEls = container.querySelectorAll('.plc-cfg-tab');
		for (var i = 0; i < tabEls.length; i++) {
			tabEls[i].onclick = function() {
				activeTab = this.getAttribute('data-tab');
				render(container);
			};
		}

		// 渲染当前 Tab
		var content = document.getElementById('plc-cfg-content');
		if (!content) return;

		switch (activeTab) {
			case 'signals': renderSignalsTab(content); break;
			case 'viewer':  renderViewerTab(content); break;
			case 'editor':  renderEditorTab(content); break;
		}
	}

	/* ================================================================
	 * Tab 1: 信号列表 — 解析 datacenter_out.xml
	 * ================================================================ */
	function renderSignalsTab(container) {
		container.innerHTML = ''
			+ '<div class="plc-sig-header">'
			+ '<input type="text" id="plc-sig-search" placeholder="搜索 saddr / desc...">'
			+ '<button class="btn btn-primary" id="plc-sig-reload">🔄 重新加载</button>'
			+ '<span style="margin-left:auto;color:#999;font-size:13px" id="plc-sig-count"></span>'
			+ '</div>'
			+ '<div class="plc-sig-table-wrap"><table class="plc-sig-table" id="plc-sig-table">'
			+ '<thead><tr><th>序号 (no)</th><th>短地址 (saddr)</th><th>描述 (desc)</th></tr></thead>'
			+ '<tbody id="plc-sig-tbody"></tbody>'
			+ '</table></div>';

		document.getElementById('plc-sig-search').oninput = function() { refreshSigTable(); };
		document.getElementById('plc-sig-reload').onclick = function() {
			loadedFiles['config/SYSTEM/datacenter_out.xml'] = null;
			dcOutSignals = [];
			loadDcOutSignals(function() { refreshSigTable(); });
		};

		if (dcOutSignals.length === 0) {
			loadDcOutSignals(function() { refreshSigTable(); });
		} else {
			refreshSigTable();
		}
	}

	function loadDcOutSignals(cb) {
		requestFile('config/SYSTEM/datacenter_out.xml', function(content) {
			if (!content) { showToast('加载 datacenter_out.xml 失败，请先在终端执行 datacenter out', 'err'); return; }
			parseDcOutXml(content);
			if (cb) cb();
		});
	}

	function parseDcOutXml(xmlStr) {
		dcOutSignals = [];
		try {
			var parser = new DOMParser();
			var doc = parser.parseFromString(xmlStr, 'text/xml');
			var outNodes = doc.documentElement.getElementsByTagName('Out');
			if (outNodes.length > 0) {
				var signals = outNodes[0].getElementsByTagName('Signal');
				for (var i = 0; i < signals.length; i++) {
					dcOutSignals.push({
						no: parseInt(signals[i].getAttribute('no')) || i,
						saddr: signals[i].getAttribute('saddr') || '',
						desc: signals[i].getAttribute('desc') || ''
					});
				}
			}
		} catch(e) {
			showToast('解析 datacenter_out.xml 失败: ' + e.message, 'err');
		}
	}

	function refreshSigTable() {
		var search = (document.getElementById('plc-sig-search').value || '').toLowerCase();
		var data = dcOutSignals;

		// 过滤
		var filtered = data;
		if (search) {
			filtered = data.filter(function(s) {
				return s.saddr.toLowerCase().indexOf(search) !== -1
					|| s.desc.toLowerCase().indexOf(search) !== -1
					|| String(s.no).indexOf(search) !== -1;
			});
		}

		document.getElementById('plc-sig-count').textContent = '共 ' + filtered.length + ' / ' + data.length + ' 条';

		var html = '';
		for (var i = 0; i < filtered.length; i++) {
			var s = filtered[i];
			html += '<tr>'
				+ '<td>' + s.no + '</td>'
				+ '<td><code>' + escHtml(s.saddr) + '</code></td>'
				+ '<td>' + escHtml(s.desc) + '</td>'
				+ '</tr>';
		}
		document.getElementById('plc-sig-tbody').innerHTML = html;
	}

	/* ================================================================
	 * Tab 2: 逻辑图查看器 — Canvas 可视化 Reclose_logic.txt
	 * ================================================================ */
	function renderViewerTab(container) {
		container.innerHTML = ''
			+ '<div class="plc-viewer-toolbar">'
			+ '<select id="plc-view-graph" style="min-width:200px"><option value="">-- 选择逻辑图 --</option></select>'
			+ '<span style="flex:1"></span>'
			+ '<button class="btn btn-outline" id="plc-view-zoomin">🔍+</button>'
			+ '<button class="btn btn-outline" id="plc-view-zoomout">🔍-</button>'
			+ '<button class="btn btn-outline" id="plc-view-reset">↺ 重置</button>'
			+ '<button class="btn btn-primary" id="plc-view-reload">🔄 刷新</button>'
			+ '</div>'
			+ '<div class="plc-canvas-wrap" id="plc-view-canvas-wrap">'
			+ '<canvas id="plc-view-canvas"></canvas>'
			+ '</div>'
			+ '<div class="plc-view-legend" id="plc-view-legend"></div>';

		// 图例
		var legendHtml = '<span style="font-weight:700">图例：</span> ';
		var types = [1,0,3,2,4,5,6,7,8,9,10];
		for (var i = 0; i < types.length; i++) {
			var info = NODE_INFO[types[i]];
			legendHtml += '<span style="display:inline-block;margin:2px 8px;padding:2px 8px;border-radius:4px;background:' + info.color + ';color:#fff;font-size:12px">' + info.icon + ' ' + info.name + '</span>';
		}
		document.getElementById('plc-view-legend').innerHTML = legendHtml;

		// 绑定事件
		document.getElementById('plc-view-zoomin').onclick = function() { viewScale = Math.min(3, viewScale * 1.2); drawViewer(); };
		document.getElementById('plc-view-zoomout').onclick = function() { viewScale = Math.max(0.2, viewScale / 1.2); drawViewer(); };
		document.getElementById('plc-view-reset').onclick = function() { viewAutoFit = true; drawViewer(); };
		document.getElementById('plc-view-reload').onclick = function() {
			loadedFiles['config/PLC/Reclose_logic.xml'] = null;
			loadedFiles['config/PLC/Reclose_logic.xml.bak'] = null;
			logicGraphs = [];
			loadLogicFileAll(function() { refreshGraphSelect(); });
		};
		document.getElementById('plc-view-graph').onchange = function() { viewAutoFit = true; drawViewer(); };

		// 画布交互
		setupViewerCanvas();

		if (logicGraphs.length === 0) {
			loadLogicFileAll(function() { refreshGraphSelect(); });
		} else {
			refreshGraphSelect();
		}
	}

	function loadLogicFileAll(cb) {
		var loadCount = 0;
		var loadedFiles2 = []; // 记录成功加载的文件名
		var failedFiles = [];  // 记录失败的文件名
		function tryCb() {
			loadCount++;
			if (loadCount >= 2 && cb) {
				if (failedFiles.length > 0) {
					var msg = loadedFiles2.length > 0
						? '已加载: ' + loadedFiles2.join(', ') + ' | 缺失: ' + failedFiles.join(', ')
						: '配置文件均缺失: ' + failedFiles.join(', ') + '，请先在编辑器中生成配置';
					showToast(msg, loadedFiles2.length > 0 ? 'info' : 'err');
				}
				cb();
			}
		}
		requestFile('config/PLC/Reclose_logic.xml', function(content) {
			if (!content) { failedFiles.push('Reclose_logic.xml'); tryCb(); return; }
			parseLogicFile(content, 'Reclose_logic.xml');
			loadedFiles2.push('Reclose_logic.xml');
			tryCb();
		});
		requestFile('config/PLC/Reclose_logic.xml.bak', function(content) {
			if (!content) { tryCb(); return; }
			parseLogicFile(content, 'Reclose_logic.xml.bak');
			loadedFiles2.push('Reclose_logic.xml.bak');
			tryCb();
		});
	}


	function loadLogicFile(cb) {
		requestFile('config/PLC/Reclose_logic.xml', function(content) {
			if (!content) { showToast('加载 Reclose_logic.xml 失败', 'err'); return; }
			parseLogicFile(content, 'Reclose_logic.xml');
			if (cb) cb();
		});
	}
	function parseLogicFile(content, sourceName) {
		logicGraphs = logicGraphs.filter(function(g) { return g._source !== sourceName; });
		onlineCfgId = onlineCfgId || '';
		parseLogicXml(content, sourceName);
	}

	// XML → 扁平 graph，与 C++ parse_xml_comb 完全一致
	function parseLogicXml(xmlStr, sourceName) {
		var parser = new DOMParser();
		var doc = parser.parseFromString(xmlStr, 'text/xml');
		var root = doc.documentElement;
		if (!root || root.nodeName !== 'PLCConfig') {
			showToast('XML 根元素不是 PLCConfig', 'err'); return;
		}
		if (sourceName.indexOf('.bak') < 0) {
			onlineCfgId = root.getAttribute('checksum') || '';
		}
		var thisKey = root.getAttribute('key') || sourceName;
		var thisName = root.getAttribute('name') || '';
		var g = { key: thisKey, name: thisName, nodes: [], links: [] };
		var GTM = {'INPUT':1,'OUTPUT':0,'OR':2,'AND':3,'NOT':4,'T62P':5,'T62D':6,'RISING':7,'SR':8,'RS':9,'FALLING':10};

		var sigNodeMap = {};
		function ensureSig(ioType, no, x, y) {
			var k = ioType + ':' + no;
			if (sigNodeMap[k] !== undefined) {
				if (x !== undefined) { g.nodes[sigNodeMap[k]].x = x; g.nodes[sigNodeMap[k]].y = y; }
				return sigNodeMap[k];
			}
			g.nodes.push({ type:ioType, id:no, x:x||0, y:y||0 });
			sigNodeMap[k] = g.nodes.length - 1;
			return g.nodes.length - 1;
		}
		function ensureGate(type, id, x, y, inPins, delayMs) {
			for (var i = 0; i < g.nodes.length; i++)
				if (g.nodes[i].type === type && (g.nodes[i].id & 0xFF) === (id & 0xFF)) return i;
			var nd = { type:type, id:id, x:x||0, y:y||0 };
			if (inPins) nd.inPins = inPins;
			if (delayMs) nd.delayMs = delayMs;
			g.nodes.push(nd);
			return g.nodes.length - 1;
		}

		function parseComb(combEl) {
			var ge = combEl.firstElementChild;
			while (ge && ge.nodeName !== 'Gate') ge = ge.nextElementSibling;
			if (!ge) return -1;
			var t = GTM[ge.getAttribute('type')] || 0;
			var gid = parseInt(ge.getAttribute('id'));
			var gi = ensureGate(t, gid,
				parseInt(ge.getAttribute('x')), parseInt(ge.getAttribute('y')),
				ge.getAttribute('inPins') ? parseInt(ge.getAttribute('inPins')) : null,
				ge.getAttribute('delayMs') ? parseInt(ge.getAttribute('delayMs')) : null);

			// 读取 Comb id
			var combId = combEl.getAttribute('id');
			if (combId) g.nodes[gi].combId = parseInt(combId);

			// 读取 SR/RS 的 R/S 属性
			var rAttr = ge.getAttribute('R');
			var sAttr = ge.getAttribute('S');
			if (rAttr) g.nodes[gi].rsR = rAttr;
			if (sAttr) g.nodes[gi].rsS = sAttr;

			var c = combEl.firstElementChild;
			while (c) {
				if (c.nodeName === 'Comb') {
					var upi = parseComb(c);
					if (upi >= 0) g.links.push({ src_type:g.nodes[upi].type, src_id:g.nodes[upi].id, dst_type:t, dst_id:gid });
				}
				c = c.nextElementSibling;
			}

			var ie = combEl.firstElementChild;
			while (ie && ie.nodeName !== 'Inputs') ie = ie.nextElementSibling;
			if (ie) {
				var s = ie.firstElementChild;
				while (s) {
					if (s.nodeName === 'Signal') {
						var no = parseInt(s.getAttribute('no'));
						ensureSig(NT_INPUT, no, parseInt(s.getAttribute('x')), parseInt(s.getAttribute('y')));
						g.links.push({ src_type:NT_INPUT, src_id:no, dst_type:t, dst_id:gid });
					}
					s = s.nextElementSibling;
				}
			}

			var oe = combEl.firstElementChild;
			while (oe && oe.nodeName !== 'Outputs') oe = oe.nextElementSibling;
			if (oe) {
				var s = oe.firstElementChild;
				while (s) {
					if (s.nodeName === 'Signal') {
						var no = parseInt(s.getAttribute('no'));
						ensureSig(NT_OUTPUT, no, parseInt(s.getAttribute('x')), parseInt(s.getAttribute('y')));
						g.links.push({ src_type:t, src_id:gid, dst_type:NT_OUTPUT, dst_id:no });
					}
					s = s.nextElementSibling;
				}
			}
			return gi;
		}

		var chains = root.getElementsByTagName('Chain');
		for (var ci = 0; ci < chains.length; ci++) {
			var chain = chains[ci];
			var c = chain.firstElementChild;
			while (c) {
				if (c.nodeName === 'Inputs') {
					var s = c.firstElementChild;
					while (s) {
						if (s.nodeName === 'Signal')
							ensureSig(NT_INPUT, parseInt(s.getAttribute('no')),
								parseInt(s.getAttribute('x')), parseInt(s.getAttribute('y')));
						s = s.nextElementSibling;
					}
				}
				if (c.nodeName === 'Outputs') {
					var s = c.firstElementChild;
					while (s) {
						if (s.nodeName === 'Signal')
							ensureSig(NT_OUTPUT, parseInt(s.getAttribute('no')),
								parseInt(s.getAttribute('x')), parseInt(s.getAttribute('y')));
						s = s.nextElementSibling;
					}
				}
				if (c.nodeName === 'Comb') parseComb(c);
				if (c.nodeName === 'Gate') {
					var gt = GTM[c.getAttribute('type')] || 0;
					var giChain = ensureGate(gt, parseInt(c.getAttribute('id')),
						parseInt(c.getAttribute('x')), parseInt(c.getAttribute('y')));
					var rAttrC = c.getAttribute('R');
					var sAttrC = c.getAttribute('S');
					var dmsC = c.getAttribute('delayMs');
					if (rAttrC) g.nodes[giChain].rsR = rAttrC;
					if (sAttrC) g.nodes[giChain].rsS = sAttrC;
					if (dmsC) g.nodes[giChain].delayMs = parseInt(dmsC);
				}
				c = c.nextElementSibling;
			}
		}

		// Comb链: Comb Gate → 链级 Gate (一个Comb一根线) + 链级 Gate → Outputs
		// 简单链: Inputs → Gate → Outputs
		for (var ci2 = 0; ci2 < chains.length; ci2++) {
			var chain2 = chains[ci2];
			var hasComb = false;
			var c3 = chain2.firstElementChild;
			while (c3) { if (c3.nodeName === 'Comb') { hasComb = true; break; } c3 = c3.nextElementSibling; }
			var ge2 = null, ie2 = null, oe2 = null;
			c3 = chain2.firstElementChild;
			while (c3) {
				if (c3.nodeName === 'Gate') ge2 = c3;
				if (c3.nodeName === 'Inputs') ie2 = c3;
				if (c3.nodeName === 'Outputs') oe2 = c3;
				c3 = c3.nextElementSibling;
			}
			if (hasComb && ge2) {
				// 链级 Comb → 取其 Gate → 链级 Gate
				c3 = chain2.firstElementChild;
				while (c3) {
					if (c3.nodeName === 'Comb') {
						var cg = c3.firstElementChild;
						while (cg && cg.nodeName !== 'Gate') cg = cg.nextElementSibling;
						if (cg) {
							var ct = GTM[cg.getAttribute('type')] || 0;
							var ci = parseInt(cg.getAttribute('id'));
							g.links.push({ src_type:ct, src_id:ci, dst_type:GTM[ge2.getAttribute('type')] || 0, dst_id:parseInt(ge2.getAttribute('id')) });
						}
					}
					c3 = c3.nextElementSibling;
				}
				// 链级 Inputs（如 RS/SR 直接连 Input 信号）→ 链级 Gate
				if (ie2) {
					var s = ie2.firstElementChild;
					while (s) { if (s.nodeName === 'Signal') g.links.push({ src_type:NT_INPUT, src_id:parseInt(s.getAttribute('no')), dst_type:GTM[ge2.getAttribute('type')] || 0, dst_id:parseInt(ge2.getAttribute('id')) }); s = s.nextElementSibling; }
				}
				// RS/SR 的 R/S 属性引用的 Input 信号（无 <Inputs> 块时也需创建链接）
				if (ge2) {
					var gtCS = GTM[ge2.getAttribute('type')] || 0;
					if (gtCS === NT_SR || gtCS === NT_RS) {
						var sAttrCS = ge2.getAttribute('S');
						var rAttrCS = ge2.getAttribute('R');
						if (sAttrCS && sAttrCS.indexOf('Input#') === 0) {
							var sNo = parseInt(sAttrCS.substring(6));
							ensureSig(NT_INPUT, sNo, 0, 0);
							g.links.push({ src_type:NT_INPUT, src_id:sNo, dst_type:gtCS, dst_id:parseInt(ge2.getAttribute('id')) });
						}
						if (rAttrCS && rAttrCS.indexOf('Input#') === 0) {
							var rNo = parseInt(rAttrCS.substring(6));
							ensureSig(NT_INPUT, rNo, 0, 0);
							g.links.push({ src_type:NT_INPUT, src_id:rNo, dst_type:gtCS, dst_id:parseInt(ge2.getAttribute('id')) });
						}
					}
				}
				if (oe2) {
					var s = oe2.firstElementChild;
					while (s) { if (s.nodeName === 'Signal') g.links.push({ src_type:GTM[ge2.getAttribute('type')] || 0, src_id:parseInt(ge2.getAttribute('id')), dst_type:NT_OUTPUT, dst_id:parseInt(s.getAttribute('no')) }); s = s.nextElementSibling; }
				}
			} else if (!hasComb && ge2) {
				var gt = GTM[ge2.getAttribute('type')] || 0;
				var gid = parseInt(ge2.getAttribute('id'));
				if (ie2) {
					var s = ie2.firstElementChild;
					while (s) { if (s.nodeName === 'Signal') g.links.push({ src_type:NT_INPUT, src_id:parseInt(s.getAttribute('no')), dst_type:gt, dst_id:gid }); s = s.nextElementSibling; }
				}
				if (oe2) {
					var s = oe2.firstElementChild;
					while (s) { if (s.nodeName === 'Signal') g.links.push({ src_type:gt, src_id:gid, dst_type:NT_OUTPUT, dst_id:parseInt(s.getAttribute('no')) }); s = s.nextElementSibling; }
				}
			}
		}

		// 最终遍：为 SR/RS 节点修复 dstPin
		for (var ni = 0; ni < g.nodes.length; ni++) {
			var nd = g.nodes[ni];
			if (nd.type !== NT_SR && nd.type !== NT_RS) continue;
			var sSrc = nd.rsS || '';
			var rSrc = nd.rsR || '';
			if (!sSrc && !rSrc) continue;
			// 遍历所有到该节点的链路
			for (var lj = 0; lj < g.links.length; lj++) {
				var lk = g.links[lj];
				if (lk.dst_type !== nd.type || (lk.dst_id & 0xFF) !== (nd.id & 0xFF)) continue;
				var srcDesc = '';
				var sN = null;
				for (var sn = 0; sn < g.nodes.length; sn++) {
					if (g.nodes[sn].type === lk.src_type &&
					    (g.nodes[sn].type <= 1 ? g.nodes[sn].id === lk.src_id : (g.nodes[sn].id & 0xFF) === (lk.src_id & 0xFF))) {
						sN = g.nodes[sn]; break;
					}
				}
				if (!sN) continue;
				if (sN.type === NT_INPUT) srcDesc = 'Input#' + sN.id;
				else srcDesc = 'Comb#' + (sN.combId !== undefined ? sN.combId : (sN.id & 0xFF));
				if (nd.type === NT_SR) {
					if (sSrc && srcDesc === sSrc) lk.dstPin = 0;
					else if (rSrc && srcDesc === rSrc) lk.dstPin = 1;
				} else {
					if (sSrc && srcDesc === sSrc) lk.dstPin = 1;
					else if (rSrc && srcDesc === rSrc) lk.dstPin = 0;
				}
			}
		}

		g._source = sourceName;
		logicGraphs.push(g);
	}

	function refreshGraphSelect() {
		var sel = document.getElementById('plc-view-graph');
		if (!sel) return;
		sel.innerHTML = '<option value="">-- 选择逻辑图 --</option>';
		for (var i = 0; i < logicGraphs.length; i++) {
			var g = logicGraphs[i];
	var bakTag = (g._source && g._source.indexOf(".bak") >= 0) ? " [备份]" : "";
	var label = g.key + " — " + (g.name || "(未命名)") + bakTag + " (" + g.nodes.length + "节点 " + g.links.length + "连线)";
			sel.innerHTML += '<option value="' + i + '">' + escHtml(label) + '</option>';
		}
		if (logicGraphs.length > 0) {
			sel.value = '0';
			setTimeout(function() { drawViewer(); }, 100);
		}
	}

	/* ---- 查看器 Canvas ---- */
	function setupViewerCanvas() {
		var wrap = document.getElementById('plc-view-canvas-wrap');
		var canvas = document.getElementById('plc-view-canvas');
		if (!wrap || !canvas) return;

		function resize() {
			canvas.width = wrap.clientWidth;
			canvas.height = wrap.clientHeight - 4;
			viewAutoFit = true;
			drawViewer();
		}
		resize();
		window.addEventListener('resize', function() { resize(); });

		// 鼠标事件
		canvas.onmousedown = function(e) {
			viewDrag = { startX: e.clientX, startY: e.clientY, panX: viewPanX, panY: viewPanY };
		};
		canvas.onmousemove = function(e) {
			if (!viewDrag) return;
			viewPanX = viewDrag.panX + (e.clientX - viewDrag.startX);
			viewPanY = viewDrag.panY + (e.clientY - viewDrag.startY);
			drawViewer();
		};
		canvas.onmouseup = function() { viewDrag = null; };
		canvas.onmouseleave = function() { viewDrag = null; };
		canvas.onwheel = function(e) {
			e.preventDefault();
			var delta = e.deltaY > 0 ? 0.9 : 1.1;
			var newScale = Math.max(0.2, Math.min(3, viewScale * delta));
			// 以鼠标位置为中心缩放
			var rect = canvas.getBoundingClientRect();
			var mx = e.clientX - rect.left;
			var my = e.clientY - rect.top;
			viewPanX = mx - (mx - viewPanX) * (newScale / viewScale);
			viewPanY = my - (my - viewPanY) * (newScale / viewScale);
			viewScale = newScale;
			drawViewer();
		};
	}

	function drawViewer() {
		var canvas = document.getElementById('plc-view-canvas');
		if (!canvas) return;
		var ctx = canvas.getContext('2d');
		var W = canvas.width, H = canvas.height;

		ctx.clearRect(0, 0, W, H);
		ctx.fillStyle = '#1a1a2e';
		ctx.fillRect(0, 0, W, H);

		var sel = document.getElementById('plc-view-graph');
		if (!sel || sel.value === '') {
			ctx.fillStyle = '#888';
			ctx.font = '16px sans-serif';
			ctx.textAlign = 'center';
			ctx.fillText('请选择一个逻辑图', W/2, H/2);
			return;
		}

		var g = logicGraphs[parseInt(sel.value)];
		if (!g) return;

		// 使用文件中保存的 x,y 坐标（与编辑器布局一致）
		var viewNodes = [];
		var bMinX = Infinity, bMaxX = -Infinity, bMinY = Infinity, bMaxY = -Infinity;
		for (var vi = 0; vi < g.nodes.length; vi++) {
			var vn = g.nodes[vi];
			var vx = vn.x, vy = vn.y;
			if (vx < bMinX) bMinX = vx; if (vx > bMaxX) bMaxX = vx;
			if (vy < bMinY) bMinY = vy; if (vy > bMaxY) bMaxY = vy;
			var vsig = findSignalByNo(vn.id);
			viewNodes.push({
				type: vn.type, id: vn.id, _lx: vx, _ly: vy,
				saddr: (vsig && vsig.saddr) || '', desc: (vsig && vsig.desc) || '', no: vn.id
			});
		}

		// 首次绘制 / resize 后自动适配缩放
		if (viewAutoFit) {
			viewAutoFit = false;
			var cw2 = bMaxX - bMinX, ch2 = bMaxY - bMinY;
			if (cw2 <= 0) cw2 = 800; if (ch2 <= 0) ch2 = 600;
			var pad = 80;
			viewScale = Math.min((W - pad * 2) / cw2, (H - pad * 2) / ch2, 1.2);
			viewPanX = W / 2 - (bMinX + cw2 / 2) * viewScale;
			viewPanY = H / 2 - (bMinY + ch2 / 2) * viewScale;
		}

		// 链路 —— 修复：首个命中而非最后命中（避免同名门 ID 歧义）
		var viewLinks = [];
		for (var vlj = 0; vlj < g.links.length; vlj++) {
			var vl = g.links[vlj];
			var vls = -1, vld = -1;
			for (var vlk = 0; vlk < g.nodes.length; vlk++) {
				var vln = g.nodes[vlk];
				if (vls < 0 && vln.type === vl.src_type && (vln.type <= 1 ? vln.id === vl.src_id : (vln.id & 0xFF) === (vl.src_id & 0xFF))) vls = vlk;
				if (vld < 0 && vln.type === vl.dst_type && (vln.type <= 1 ? vln.id === vl.dst_id : (vln.id & 0xFF) === (vl.dst_id & 0xFF))) vld = vlk;
			}
			if (vls >= 0 && vld >= 0) viewLinks.push({ srcIdx: vls, dstIdx: vld, dstPin: vl.dstPin || 0 });
		}

		// 网格
		ctx.strokeStyle = 'rgba(255,255,255,0.04)';
		ctx.lineWidth = 1;
		var gridSize = 50;
		var startGx = Math.floor(-viewPanX / viewScale / gridSize) * gridSize;
		var startGy = Math.floor(-viewPanY / viewScale / gridSize) * gridSize;

		ctx.save();
		ctx.translate(viewPanX, viewPanY);
		ctx.scale(viewScale, viewScale);

		for (var gx = startGx - gridSize; gx < startGx + W/viewScale + gridSize; gx += gridSize) {
			ctx.beginPath(); ctx.moveTo(gx, startGy - gridSize); ctx.lineTo(gx, startGy + H/viewScale + gridSize); ctx.stroke();
		}
		for (var gy = startGy - gridSize; gy < startGy + H/viewScale + gridSize; gy += gridSize) {
			ctx.beginPath(); ctx.moveTo(startGx - gridSize, gy); ctx.lineTo(startGx + W/viewScale + gridSize, gy); ctx.stroke();
		}

		drawGraphContent(ctx, viewNodes, viewLinks, -1, -1, null);

		ctx.restore();
	}

	function calcLayout(g) {
		// 自动布局：每条链路 (输入→运算→输出) 构成一条横向数据流链
		// 节点类型常量复用的全局 NODE_INFO
		var layout = {};

		// 1. 构建邻接关系，按连线确定连通分量（每条链）
		var adjOut = {}; for (var i = 0; i < g.nodes.length; i++) adjOut[i] = [];
		var adjIn  = {}; for (var i = 0; i < g.nodes.length; i++) adjIn[i] = [];
		for (var j = 0; j < g.links.length; j++) {
			var l = g.links[j];
			var si = -1, di = -1;
			for (var k = 0; k < g.nodes.length; k++) {
				var n = g.nodes[k];
				if (si < 0 && n.type === l.src_type && (n.type <= 1 ? n.id === l.src_id : (n.id & 0xFF) === (l.src_id & 0xFF))) si = k;
				if (di < 0 && n.type === l.dst_type && (n.type <= 1 ? n.id === l.dst_id : (n.id & 0xFF) === (l.dst_id & 0xFF))) di = k;
			}
			if (si >= 0 && di >= 0) { adjOut[si].push(di); adjIn[di].push(si); }
		}

		// BFS 找连通分量
		var visited = {};
		var chains = [];
		for (var idx = 0; idx < g.nodes.length; idx++) {
			if (visited[idx]) continue;
			var chain = [];
			var queue = [idx];
			visited[idx] = true;
			while (queue.length > 0) {
				var cur = queue.shift();
				chain.push(cur);
				var nbrs = adjOut[cur].concat(adjIn[cur]);
				for (var nb = 0; nb < nbrs.length; nb++) {
					if (!visited[nbrs[nb]]) { visited[nbrs[nb]] = true; queue.push(nbrs[nb]); }
				}
			}
			chains.push(chain);
		}

		// 2. 计算每条链的实际竖向高度（含节点+间距）
		var NODE_IO_H = 80;    // 输入/输出节点高度
		var NODE_GATE_H = 66;  // 逻辑门节点高度
		var ITEM_GAP = 28;     // 同列节点纵向间距

		function calcChainPixels(chain) {
			var cin = 0, cg = 0, cout = 0;
			for (var i = 0; i < chain.length; i++) {
				var nd = g.nodes[chain[i]];
				if (nd.type === NT_INPUT) cin++;
				else if (nd.type === NT_OUTPUT) cout++;
				else cg++;
			}
			// 每列独立高度
			var hIn = cin > 0 ? cin * NODE_IO_H + (cin - 1) * ITEM_GAP : 0;
			var hGate = cg > 0 ? cg * NODE_GATE_H + (cg - 1) * ITEM_GAP : 0;
			var hOut = cout > 0 ? cout * NODE_IO_H + (cout - 1) * ITEM_GAP : 0;
			return Math.max(hIn, hGate, hOut, 60); // 至少 60px 避免空链过扁
		}

		var CHAIN_GAP = 80; // 链间纵向间距

		// 累计 Y 偏移
		var chainHeights = [];
		var totalH = 0;
		for (var ci = 0; ci < chains.length; ci++) {
			var h = calcChainPixels(chains[ci]);
			chainHeights.push(h);
			totalH += h;
		}
		totalH += (chains.length - 1) * CHAIN_GAP;

		// 使链条在 Y 轴居中：startY = -(totalH / 2) + (firstHalfHeight/2)
		var startY = -totalH / 2;
		var curY = startY;

		// 3. 列布局 (输入 | 运算 | 输出)
		var COL_INPUT  = -420;
		var COL_GATE   = 0;
		var COL_OUTPUT = 420;

		for (var ci2 = 0; ci2 < chains.length; ci2++) {
			var chain = chains[ci2];
			var ch = chainHeights[ci2];
			var cy = curY + ch / 2;  // 链中心 Y

			// 分类节点
			var cInputs = [], cGates = [], cOutputs = [];
			for (var n = 0; n < chain.length; n++) {
				var nd = g.nodes[chain[n]];
				if (nd.type === NT_INPUT)       cInputs.push(chain[n]);
				else if (nd.type === NT_OUTPUT) cOutputs.push(chain[n]);
				else                             cGates.push(chain[n]);
			}

			// 计算每列起始 Y（使该列节点在链高度内居中）
			function colTop(count, itemH) {
				if (count === 0) return cy;
				var colH = count * itemH + (count - 1) * ITEM_GAP;
				return cy - colH / 2 + itemH / 2;
			}

			var topIn  = colTop(cInputs.length, NODE_IO_H);
			var topGate = colTop(cGates.length, NODE_GATE_H);
			var topOut  = colTop(cOutputs.length, NODE_IO_H);

			for (var a = 0; a < cInputs.length; a++)
				layout[cInputs[a]] = { x: COL_INPUT, y: topIn + a * (NODE_IO_H + ITEM_GAP) };
			for (var b = 0; b < cGates.length; b++)
				layout[cGates[b]] = { x: COL_GATE, y: topGate + b * (NODE_GATE_H + ITEM_GAP) };
			for (var c = 0; c < cOutputs.length; c++)
				layout[cOutputs[c]] = { x: COL_OUTPUT, y: topOut + c * (NODE_IO_H + ITEM_GAP) };

			curY += ch + CHAIN_GAP;
		}

		// 暴露边界信息供 canvas 自适应
		layout._bounds = {
			minX: COL_INPUT - 130, maxX: COL_OUTPUT + 130,
			minY: startY - 40, maxY: startY + totalH + 40
		};
		return layout;
	}

	/* ---- 通用绘制函数（编辑器 + 查看器共用） ---- */
	function drawGraphContent(ctx, nodes, links, selectedIdx, selectedLinkIdx, wiring) {
		// 节点渲染尺寸常量
		var IO_W = 250, IO_H = 80;      // 输入/输出
		var GATE_W = 170, GATE_H = 66;  // 逻辑门
		var CORNER_R = 12;
		var SHADOW_OFF = 3;
		var LEFT_BAR_W = 8;

		// ---- 连线层 ----
		for (var i = 0; i < links.length; i++) {
			var l = links[i];
			if (l.srcIdx >= nodes.length || l.dstIdx >= nodes.length) continue;
			var sn = nodes[l.srcIdx], dn = nodes[l.dstIdx];
			var srcIsIO = (sn.type === NT_OUTPUT || sn.type === NT_INPUT);
			var dstIsIO = (dn.type === NT_OUTPUT || dn.type === NT_INPUT);
			var dstIsSRRS = (dn.type === NT_SR || dn.type === NT_RS);
			var sW = srcIsIO ? IO_W : GATE_W, dW = dstIsIO ? IO_W : GATE_W;
			var dH = dstIsIO ? IO_H : (dstIsSRRS ? 86 : GATE_H);
			var x1 = sn._lx + sW/2;
			var y1 = sn._ly;
			var dstInP = getNodeInPins(dn);
			var effPinIdx = (l.dstPin !== undefined) ? l.dstPin : 0;
			var y2 = dn._ly - dH/2 + dH * (effPinIdx + 1) / (dstIsSRRS ? 3 : (dstInP + 1));
			var x2 = dn._lx - dW/2;
			var isActive = wiring && wiring.fromIdx === l.srcIdx;
			var isLinkSelected = (selectedLinkIdx === i);

			var midX = (x1 + x2) / 2;
			var lineAlpha = (isActive || isLinkSelected) ? 1 : 0.6;
			var lineW = (isActive || isLinkSelected) ? 3 : 2;
			var lineColor = isLinkSelected ? '#FF5722' : (isActive ? '#FFC107' : 'rgba(180,200,220,' + lineAlpha + ')');

			// 发光效果 (阴影线)
			ctx.beginPath();
			ctx.strokeStyle = isLinkSelected ? 'rgba(255,87,34,0.3)' : 'rgba(255,200,0,' + (lineAlpha * 0.2) + ')';
			ctx.lineWidth = lineW + 4;
			ctx.moveTo(x1, y1);
			ctx.bezierCurveTo(midX, y1, midX, y2, x2, y2);
			ctx.stroke();

			// 主线
			ctx.beginPath();
			ctx.strokeStyle = lineColor;
			ctx.lineWidth = lineW;
			ctx.moveTo(x1, y1);
			ctx.bezierCurveTo(midX, y1, midX, y2, x2, y2);
			ctx.stroke();

			// 箭头 (指向目标)
			ctx.fillStyle = lineColor;
			ctx.beginPath();
			ctx.moveTo(x2, y2);
			ctx.lineTo(x2 - 7, y2 - 3.5);
			ctx.lineTo(x2 - 7, y2 + 3.5);
			ctx.closePath();
			ctx.fill();

			// 选中连线删除按钮标记（×）
			if (isLinkSelected) {
				var labelX = (x1 + x2) / 2, labelY = (y1 + y2) / 2 - 18;
				ctx.fillStyle = '#FF5722';
				ctx.font = 'bold 14px sans-serif';
				ctx.textAlign = 'center';
				ctx.textBaseline = 'middle';
				ctx.fillText('✕ 右键删除', labelX, labelY);
			}
		}

		// 临时连线（仅编辑器）
		if (wiring && wiring.fromIdx < nodes.length) {
			var fn = nodes[wiring.fromIdx];
			var fnIsIO = (fn.type === NT_OUTPUT || fn.type === NT_INPUT);
			var fW = fnIsIO ? IO_W : GATE_W;
			var fx = (wiring.fromSide === 'out') ? fn._lx + fW/2 : fn._lx - fW/2;
			var fy = fn._ly;
			ctx.beginPath();
			ctx.strokeStyle = '#FFD700';
			ctx.lineWidth = 2;
			ctx.setLineDash([6, 4]);
			ctx.moveTo(fx, fy);
			ctx.lineTo(fx + (wiring.fromSide === 'out' ? 100 : -100), fy);
			ctx.stroke();
			ctx.setLineDash([]);
		}

		// ---- 节点层 ----
		for (var j = 0; j < nodes.length; j++) {
			var n = nodes[j];
			var info = NODE_INFO[n.type] || { name: '?', color: '#555', icon: '?' };
			var isIO = (n.type === NT_OUTPUT || n.type === NT_INPUT);
			var inP = getNodeInPins(n);
			var nw = isIO ? IO_W : GATE_W;
			var nh = isIO ? IO_H : (GATE_H + Math.max(0, inP - 2) * 14);
			var x = n._lx, y = n._ly;
			var halfW = nw/2, halfH = nh/2;
			var isWiringSrc = wiring && wiring.fromIdx === j;
			var isSelected = (selectedIdx === j);

			// 发光描边宽度
			var glowW = (isSelected || isWiringSrc) ? 3 : 1;

			// --- 阴影 ---
			ctx.fillStyle = 'rgba(0,0,0,0.5)';
			ctx.beginPath();
			ctx.roundRect(x - halfW + SHADOW_OFF, y - halfH + SHADOW_OFF, nw, nh, CORNER_R);
			ctx.fill();

			// --- 主体渐变 ---
			var grad = ctx.createLinearGradient(x - halfW, y - halfH, x - halfW, y + halfH);
			if (isSelected) {
				grad.addColorStop(0, '#3b3b6b');
				grad.addColorStop(1, '#282848');
			} else if (isWiringSrc) {
				grad.addColorStop(0, '#3c3c50');
				grad.addColorStop(1, '#252538');
			} else {
				grad.addColorStop(0, '#2e2e4a');
				grad.addColorStop(1, '#1e1e36');
			}
			ctx.fillStyle = grad;
			ctx.beginPath();
			ctx.roundRect(x - halfW, y - halfH, nw, nh, CORNER_R);
			ctx.fill();

			// --- 描边 ---
			ctx.strokeStyle = isSelected ? '#FFD700' : (isWiringSrc ? '#FFC107' : info.color);
			ctx.lineWidth = glowW;
			ctx.beginPath();
			ctx.roundRect(x - halfW, y - halfH, nw, nh, CORNER_R);
			ctx.stroke();

			// --- 左侧色条 ---
			ctx.fillStyle = info.color;
			ctx.beginPath();
			ctx.roundRect(x - halfW, y - halfH, LEFT_BAR_W, nh, [CORNER_R, 0, 0, CORNER_R]);
			ctx.fill();

			// --- 顶部类型标签 ---
			if (info.name) {
				var labelBgX = x - halfW + LEFT_BAR_W;
				var labelBgW = info.name.length * 7.5 + 18;
				var labelBgY = y - halfH + 2;
				var labelBgH = 16;
				ctx.fillStyle = info.color;
				ctx.globalAlpha = 0.25;
				ctx.fillRect(labelBgX, labelBgY, Math.min(labelBgW, nw - LEFT_BAR_W - 4), labelBgH);
				ctx.globalAlpha = 1;

				ctx.fillStyle = isIO ? '#fff' : info.color;
				ctx.font = '9px -apple-system, sans-serif';
				ctx.textAlign = 'left';
				ctx.textBaseline = 'top';
				ctx.fillText(info.name, labelBgX + 4, labelBgY + 2.5);
			}

			// --- 文本内容 ---
			ctx.textAlign = 'center';
			ctx.textBaseline = 'middle';

			if (isIO) {
				var decId = n.id;
				var sig = n.saddr ? n : findSignalByNo(decId);
				var prefix = (n.type === NT_INPUT) ? '📥' : '📤';
				var roleLabel = (n.type === NT_INPUT) ? '输入点' : '输出点';

				// ID 行
				ctx.fillStyle = '#e0e0e0';
				ctx.font = 'bold 12px -apple-system, sans-serif';
				ctx.fillText(prefix + ' ' + roleLabel + ' #' + decId, x, y - 18);

				// saddr 行
				ctx.fillStyle = '#aaa';
				ctx.font = '10px monospace';
				var saddrText = (sig && sig.saddr) ? sig.saddr : '—';
				ctx.fillText(saddrText, x, y);

				// desc 行
				var descText = (sig && sig.desc) ? sig.desc : '';
				if (descText) {
					ctx.fillStyle = '#888';
					ctx.font = '9px -apple-system, sans-serif';
					var truncated = descText.length > 18 ? descText.substring(0, 16) + '…' : descText;
					ctx.fillText(truncated, x, y + 14);
				}
			} else {
				// 逻辑门
				var gateIdx = n.id & 0xFF;
				var realIn = 0, realOut = 0;
				for (var li = 0; li < links.length; li++) {
					if (links[li].dstIdx === j) realIn++;
					if (links[li].srcIdx === j) realOut++;
				}

				// 中部：名称 + 序号
				ctx.fillStyle = '#e8e8e8';
				ctx.font = 'bold 11px -apple-system, sans-serif';
				var titleText = info.icon + ' #' + gateIdx;
				if (n.type === NT_62P || n.type === NT_62D) {
					titleText += ' (' + (n.delayMs || 1000) + 'ms)';
				}
				ctx.fillText(titleText, x, y - 6);

				// 连接数 / 延时信息
				ctx.fillStyle = '#999';
				ctx.font = '9px monospace';
				if (n.type === NT_SR || n.type === NT_RS) {
					ctx.fillText(realIn + '入 ' + realOut + '出', x, y + 14);
				} else {
					ctx.fillText(realIn + '入 ' + realOut + '出', x, y + 14);
				}
			}

			// --- 输入引脚 ---
			if (inP > 0) {
				var isSRRS = (n.type === NT_SR || n.type === NT_RS);
				var renderPins = isSRRS ? 2 : (isIO ? inP : 1);
				for (var p = 0; p < renderPins; p++) {
					var py = y - nh/2 + nh * (p + 1) / (isSRRS ? 3 : (inP + 1));
					// 外圈
					ctx.fillStyle = '#2e2e4a';
					ctx.beginPath();
					ctx.arc(x - halfW, py, 7, 0, Math.PI * 2);
					ctx.fill();
					ctx.strokeStyle = isSRRS ? (p === 0 ? '#FFC107' : '#FF5722') : '#4CAF50';
					ctx.lineWidth = 2;
					ctx.stroke();
					// 内芯
					ctx.fillStyle = isSRRS ? (p === 0 ? '#FFC107' : '#FF5722') : '#4CAF50';
					ctx.beginPath();
					ctx.arc(x - halfW, py, 3.5, 0, Math.PI * 2);
					ctx.fill();
					// SR/RS 引脚标签
					if (isSRRS) {
						ctx.fillStyle = '#fff';
						ctx.font = 'bold 10px monospace';
						ctx.textAlign = 'right';
						ctx.textBaseline = 'middle';
						var srl = (n.type === NT_SR) ? ['S', 'R'] : ['R', 'S'];
						ctx.fillText(srl[p], x - halfW - 12, py);
					}
				}
			}

			// --- 输出引脚 ---
			if (info.outPins > 0) {
				// 外圈
				ctx.fillStyle = '#2e2e4a';
				ctx.beginPath();
				ctx.arc(x + halfW, y, 7, 0, Math.PI * 2);
				ctx.fill();
				ctx.strokeStyle = '#FF9800';
				ctx.lineWidth = 2;
				ctx.stroke();
				// 内芯
				ctx.fillStyle = '#FF9800';
				ctx.beginPath();
				ctx.arc(x + halfW, y, 3.5, 0, Math.PI * 2);
				ctx.fill();
			}
		}
	}

	function renderEditorTab(container) {
		editNodes = [];
		editLinks = [];
		editKey = '';
		editName = '';
		editOnlineCfgId = '';
		editSelectedNode = -1;
		editSelectedLink = -1;
		editWiring = null;
		editDragNode = null;
		editPanX = 0; editPanY = 0;
		editScale = 1.0;
		editLoadedFromFile = false;
		editNextGateIdx = {};

		container.innerHTML = ''
			+ '<div class="plc-editor-toolbar">'
			+ '<input type="text" id="plc-edit-key" placeholder="逻辑图 Key" value="' + escHtml(editKey) + '" style="width:140px">'
			+ '<input type="text" id="plc-edit-name" placeholder="逻辑图名称" value="' + escHtml(editName) + '" style="width:160px">'
			+ '<span style="flex:1"></span>'
			+ '<button class="btn btn-success" id="plc-edit-add-in">📥 新增输入</button>'
			+ '<button class="btn btn-outline" id="plc-edit-add-gate">🧮 新增运算</button>'
			+ '<button class="btn btn-info" id="plc-edit-add-out">📤 新增输出</button>'
			+ '<button class="btn btn-outline" id="plc-edit-zoomin">🔍+</button>'
			+ '<button class="btn btn-outline" id="plc-edit-zoomout">🔍-</button>'
			+ '<button class="btn btn-warn" id="plc-edit-modify">✏️ 修改模块</button>'
			+ '<button class="btn btn-danger" id="plc-edit-del">🗑 删除模块</button>'
			+ '<button class="btn btn-primary" id="plc-edit-generate">✅ 生成配置</button>'
			+ '</div>'
			+ '<div class="plc-editor-main">'
			+ '<div class="plc-canvas-wrap" id="plc-edit-canvas-wrap">'
			+ '<canvas id="plc-edit-canvas" tabindex="0"></canvas>'
			+ '</div>'
			+ '</div>'
			+ '<div class="plc-editor-info" id="plc-edit-info">'
			+ '💡 双击模块可修改配置 | 单击选中/引脚连线 | 右键删除连线 | 滚轮缩放 | 按住空白拖动画布'
			+ '</div>';

		// 工具栏事件
		document.getElementById('plc-edit-key').oninput = function() { editKey = this.value; };
		document.getElementById('plc-edit-name').oninput = function() { editName = this.value; };
		document.getElementById('plc-edit-add-in').onclick = function() { showAddInputModal(); };
		document.getElementById('plc-edit-add-out').onclick = function() { showAddOutputModal(); };
		document.getElementById('plc-edit-add-gate').onclick = function() { showAddGateModal(); };
		document.getElementById('plc-edit-zoomin').onclick = function() { editScale = Math.min(3, editScale * 1.2); drawEditor(); };
		document.getElementById('plc-edit-zoomout').onclick = function() { editScale = Math.max(0.2, editScale / 1.2); drawEditor(); };
		document.getElementById('plc-edit-del').onclick = function() { deleteSelectedNode(); };
		document.getElementById('plc-edit-modify').onclick = function() { modifySelectedNode(); };
		document.getElementById('plc-edit-generate').onclick = function() { generateAndSaveConfig(); };

		setupEditorCanvas();
		drawEditor();

		// 加载当前 Reclose_logic.xml 为编辑起点
		function initEditor() {
			loadLogicFile(function() {
				if (logicGraphs.length > 0) {
					// 优先使用非 bak 文件的最新配置
					var g = null;
					for (var gi = 0; gi < logicGraphs.length; gi++) {
						if (!logicGraphs[gi]._source || logicGraphs[gi]._source.indexOf('.bak') < 0) {
							g = logicGraphs[gi]; break;
						}
					}
					if (!g && logicGraphs.length > 0) g = logicGraphs[0];
					if (g) {
					editKey = g.key;
					editName = g.name || '';
					editOnlineCfgId = onlineCfgId;
				// 复制节点到编辑器
				for (var i = 0; i < g.nodes.length; i++) {
					var n = g.nodes[i];
					var newNode = {
						type: n.type, id: n.id, x: n.x, y: n.y,
						_lx: n.x, _ly: n.y,
						saddr: '', desc: '', no: n.id
					};
					// 查找信号信息
					var sig = findSignalByNo(n.id);
					if (sig) { newNode.saddr = sig.saddr; newNode.desc = sig.desc; }
					// 逻辑门 inPins — 优先使用文件中保存的配置
					if (n.type !== NT_INPUT && n.type !== NT_OUTPUT) {
						newNode.inPins = (n.inPins !== undefined) ? n.inPins : ((NODE_INFO[n.type] || {}).inPins || 2);
					}
					// 延时字段
					if (n.delayMs !== undefined) newNode.delayMs = n.delayMs;
					// Comb id 和 RS 信息
					if (n.combId !== undefined) newNode.combId = n.combId;
					if (n.rsR) newNode.rsR = n.rsR;
					if (n.rsS) newNode.rsS = n.rsS;
					editNodes.push(newNode);
				}
					// 复制链路（按节点在新数组中的索引）
					for (var j = 0; j < g.links.length; j++) {
						var l = g.links[j];
						var si = findEditNodeIndex(l.src_type, l.src_id);
						var di = findEditNodeIndex(l.dst_type, l.dst_id);
						if (si >= 0 && di >= 0) {
							var el = { srcIdx: si, dstIdx: di };
							if (l.dstPin !== undefined) el.dstPin = l.dstPin;
							editLinks.push(el);
						}
					}
					editLoadedFromFile = true;

				// 按类型统计最大序号，后续新增时每种类型独立递增
				var maxByType = {};
				for (var di = 0; di < editNodes.length; di++) {
					var dn = editNodes[di];
					if (dn.type === NT_INPUT || dn.type === NT_OUTPUT) continue;
					var idx = dn.id & 0xFF;
					if (idx >= (maxByType[dn.type] || -1)) maxByType[dn.type] = idx;
				}
				for (var t in maxByType) {
					editNextGateIdx[t] = maxByType[t] + 1;
				}

				// 消除已有的重复 type+id 并基于每类型独立序号分配新 ID
				var seenIds = {};
				for (var di = 0; di < editNodes.length; di++) {
					var dn = editNodes[di];
					if (dn.type === NT_INPUT || dn.type === NT_OUTPUT) continue;
					var key2 = dn.type + ':' + (dn.id & 0xFF);
					if (seenIds[key2]) {
						var next = editNextGateIdx[dn.type] || 0;
						editNodes[di].id = (dn.id & ~0xFF) | next;
						editNextGateIdx[dn.type] = next + 1;
					} else {
						seenIds[key2] = true;
					}
				}

					// 更新输入框
					var keyEl = document.getElementById('plc-edit-key');
					var nameEl = document.getElementById('plc-edit-name');
					if (keyEl) keyEl.value = editKey;
					if (nameEl) nameEl.value = editName;
				} else {
					editKey = String(Date.now());
					editName = '';
					var keyEl = document.getElementById('plc-edit-key');
					if (keyEl) keyEl.value = editKey;
				}
				drawEditor();
			}
			});
		}

		if (dcOutSignals.length === 0) {
			loadDcOutSignals(function() { initEditor(); });
		} else {
			initEditor();
		}
	}

	function findEditNodeIndex(type, id) {
		for (var i = 0; i < editNodes.length; i++) {
			var n = editNodes[i];
			if (n.type === type) {
				if (type <= 1 && n.id === id) return i;
				if (type > 1 && (n.id & 0xFF) === (id & 0xFF)) return i;
			}
		}
		return -1;
	}

	function deleteSelectedNode() {
		if (editSelectedNode < 0 || editSelectedNode >= editNodes.length) {
			showToast('请先在画布上双击或单击选中一个模块', 'err');
			return;
		}
		var node = editNodes[editSelectedNode];
		var idx = editSelectedNode;
		var label = getNodeDisplayLabel(node);
		var html = '<div class="modal-overlay" id="plc-modal"><div class="modal-box" style="max-width:450px">'
			+ '<h3>🗑 确认删除</h3>'
			+ '<p style="margin:12px 0;font-size:14px">确定删除 <strong style="color:#f44336">' + escHtml(label) + '</strong> 及其所有连线？</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 div = document.createElement('div');
		div.innerHTML = html;
		document.body.appendChild(div.firstElementChild);
		document.getElementById('plc-modal-cancel').onclick = closeModal;
		document.getElementById('plc-modal-ok').onclick = function() {
			closeModal();
			var delType = node.type;
			editLinks = editLinks.filter(function(l) { return l.srcIdx !== idx && l.dstIdx !== idx; });
			for (var i = 0; i < editLinks.length; i++) {
				if (editLinks[i].srcIdx > idx) editLinks[i].srcIdx--;
				if (editLinks[i].dstIdx > idx) editLinks[i].dstIdx--;
			}
			editNodes.splice(idx, 1);
			editSelectedNode = -1;

			// 压缩同类型门序号：删除 #1 后 #2→#1, #3→#2 …
			if (delType !== NT_INPUT && delType !== NT_OUTPUT) {
				var sameType = [];
				for (var s = 0; s < editNodes.length; s++) {
					if (editNodes[s].type === delType) sameType.push(s);
				}
				sameType.sort(function(a, b) { return (editNodes[a].id & 0xFF) - (editNodes[b].id & 0xFF); });
				for (var a = 0; a < sameType.length; a++) {
					editNodes[sameType[a]].id = (editNodes[sameType[a]].id & ~0xFF) | a;
				}
				editNextGateIdx[delType] = sameType.length;
			}

			drawEditor();
			showToast('模块已删除', 'ok');
		};
	}

	function modifySelectedNode() {
		if (editSelectedNode < 0 || editSelectedNode >= editNodes.length) {
			showToast('请先在画布上双击或单击选中一个模块', 'err');
			return;
		}
		var idx = editSelectedNode;
		var node = editNodes[idx];
		if (node.type === NT_INPUT || node.type === NT_OUTPUT) {
			showRebindSignalModal(idx);
		} else if (node.type === NT_AND || node.type === NT_OR || node.type === NT_NOT) {
			showGateConfigModal(idx);
		} else if (node.type === NT_62P || node.type === NT_62D) {
			showDelayEditModal(idx);
		} else if (node.type === NT_SR || node.type === NT_RS) {
			showRSInfoModal(idx);
		} else {
			showGateConfigModal(idx);
		}
	}

	// 节点双击 — 根据类型弹出编辑框
	function onNodeDoubleClicked(idx) {
		var node = editNodes[idx];
		editSelectedNode = idx;
		drawEditor();
		if (node.type === NT_INPUT || node.type === NT_OUTPUT) {
			showRebindSignalModal(idx);
		} else if (node.type === NT_62P || node.type === NT_62D) {
			showDelayEditModal(idx);
		} else if (node.type === NT_SR || node.type === NT_RS) {
			showRSInfoModal(idx);
		} else {
			showGateConfigModal(idx);
		}
	}

	/** 延时模块双击：弹出延时编辑框 */
	function showDelayEditModal(nodeIdx) {
		var node = editNodes[nodeIdx];
		var info = NODE_INFO[node.type] || {};
		var curMs = node.delayMs || 1000;
		var html = '<div class="modal-overlay" id="plc-modal"><div class="modal-box" style="max-width:380px">'
			+ '<h3>⏱️ ' + info.name + ' — 修改延时</h3>'
			+ '<p style="color:#999">当前延时: <strong>' + curMs + ' ms</strong></p>'
			+ '<input type="number" id="plc-delay-input" value="' + curMs + '" min="0" max="600000" step="100"'
			+ ' style="width:100%;padding:10px;border:1px solid #555;border-radius:6px;background:#2a2a3e;color:#fff;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 div = document.createElement('div');
		div.innerHTML = html;
		document.body.appendChild(div.firstElementChild);

		document.getElementById('plc-modal-cancel').onclick = closeModal;
		document.getElementById('plc-modal-ok').onclick = function() {
			var val = parseInt(document.getElementById('plc-delay-input').value);
			if (isNaN(val) || val < 0) { showToast('请输入有效的延时毫秒数', 'err'); return; }
			editNodes[nodeIdx].delayMs = val;
			closeModal();
			drawEditor();
			showToast('延时已更新: ' + val + ' ms', 'ok');
		};
		document.getElementById('plc-delay-input').onkeydown = function(e) {
			if (e.key === 'Enter') document.getElementById('plc-modal-ok').click();
		};
		setTimeout(function() {
			var inp = document.getElementById('plc-delay-input');
			if (inp) { inp.focus(); inp.select(); }
		}, 100);
	}

	/** SR/RS 双击：显示 R/S 连接信息 */
	function showRSInfoModal(nodeIdx) {
		var node = editNodes[nodeIdx];
		var info = NODE_INFO[node.type] || {};
		var sSrc = '', rSrc = '';
		for (var li = 0; li < editLinks.length; li++) {
			var l = editLinks[li];
			if (l.dstIdx === nodeIdx) {
				var srcNode = editNodes[l.srcIdx];
				var desc = getEditorNodeLabel(srcNode).replace(/\n/g, ' ');
				if (node.type === NT_SR) {
					if (l.dstPin === 0) sSrc = desc; else rSrc = desc;
				} else {
					if (l.dstPin === 1) sSrc = desc; else rSrc = desc;
				}
			}
		}
		var html = '<div class="modal-overlay" id="plc-modal"><div class="modal-box" style="max-width:420px">'
			+ '<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:#999;font-size:12px;margin-top:12px">💡 拖动输出引脚到目标节点的 S/R 引脚来连线</p>'
			+ '<button class="btn btn-outline" id="plc-modal-cancel">关闭</button>'
			+ '</div></div>';
		var div = document.createElement('div');
		div.innerHTML = html;
		document.body.appendChild(div.firstElementChild);
		document.getElementById('plc-modal-cancel').onclick = closeModal;
	}

	// 重新绑定 I/O 节点信号
	function showRebindSignalModal(nodeIdx) {
		var node = editNodes[nodeIdx];
		var signals = dcOutSignals;
		if (signals.length === 0) { showToast('信号列表为空，请先加载', 'err'); return; }

		var title = (node.type === NT_INPUT) ? '📥 重新绑定输入信号' : '📤 重新绑定输出信号';
		var html = '<div class="modal-overlay" id="plc-modal"><div class="modal-box" style="max-width:650px">'
			+ '<h3>' + title + '</h3>'
			+ '<p style="color:#999">当前: ' + escHtml(getNodeDisplayLabel(node)) + '</p>'
			+ '<input type="text" id="plc-modal-search" placeholder="搜索信号..." style="width:100%;margin:8px 0;padding:8px;border:1px solid #ddd;border-radius:4px">'
			+ '<div style="max-height:350px;overflow-y:auto" id="plc-modal-list">';
		for (var j = 0; j < signals.length; j++) {
			var s = signals[j];
			var isCurrent = (node.no === s.no);
			html += '<div class="plc-modal-row' + (isCurrent ? ' plc-modal-current' : '') + '" data-no="' + s.no + '" data-saddr="' + escHtml(s.saddr) + '" data-desc="' + escHtml(s.desc) + '">'
				+ '<strong>#' + s.no + ' ' + escHtml(s.saddr) + '</strong>'
				+ ' <span style="color:#999">' + escHtml(s.desc) + '</span>'
				+ (isCurrent ? '<span style="float:right;color:#2196F3">✓ 当前</span>' : '')
				+ '</div>';
		}
		html += '</div><button class="btn btn-outline" id="plc-modal-cancel">取消</button></div></div>';

		showModalCustom(html, function(no, saddr, desc) {
			var sig = findSignalByNo(no);
			editNodes[nodeIdx].no = no;
			editNodes[nodeIdx].id = no;
			editNodes[nodeIdx].saddr = saddr;
			editNodes[nodeIdx].desc = desc;
			drawEditor();
			showToast('已绑定: ' + saddr, 'ok');
			return true;
		});
	}

	// 配置门电路引脚数
	function showGateConfigModal(nodeIdx) {
		var node = editNodes[nodeIdx];
		var curPins = getNodeInPins(node);
		var info = NODE_INFO[node.type] || {};
		var maxPins = info.inMax || 99;
		var minPins = 1;
		if (node.type === NT_AND || node.type === NT_OR) minPins = 1;
		else if (node.type === NT_NOT) minPins = 1;
		else minPins = 1;

		var html = '<div class="modal-overlay" id="plc-modal"><div class="modal-box" style="max-width:400px">'
			+ '<h3>' + info.icon + ' ' + info.name + ' 配置</h3>'
			+ '<p style="color:#999">当前输入节点数: <strong>' + curPins + '</strong></p>'
			+ '<label style="display:block;margin:12px 0 4px">输入引脚数量 (' + minPins + '~' + maxPins + '):</label>'
			+ '<input type="number" id="plc-gate-pins" value="' + curPins + '" min="' + minPins + '" max="' + maxPins + '" style="width:100%;padding:8px;border:1px solid #ddd;border-radius:4px;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 div = document.createElement('div');
		div.innerHTML = html;
		document.body.appendChild(div.firstElementChild);

		document.getElementById('plc-modal-cancel').onclick = closeModal;
		document.getElementById('plc-modal-save').onclick = function() {
			var v = parseInt(document.getElementById('plc-gate-pins').value);
			if (isNaN(v) || v < minPins) { showToast('最少 ' + minPins + ' 个输入引脚', 'err'); return; }
			if (v > maxPins) { showToast('最多 ' + maxPins + ' 个输入引脚', 'err'); return; }
			editNodes[nodeIdx].inPins = v;
			closeModal();
			drawEditor();
			showToast(info.name + ' 输入引脚已设为 ' + v, 'ok');
		};
	}

	/* ---- 编辑器 Canvas ---- */
	function setupEditorCanvas() {
		var wrap = document.getElementById('plc-edit-canvas-wrap');
		var canvas = document.getElementById('plc-edit-canvas');
		if (!wrap || !canvas) return;

		function resize() {
			canvas.width = wrap.clientWidth;
			canvas.height = Math.max(500, wrap.clientHeight - 4);
			// 居中画布
			editScale = 0.7;
			editPanX = canvas.width / 2;
			editPanY = canvas.height / 2;
			drawEditor();
		}
		setTimeout(resize, 50);
		window.addEventListener('resize', function() { resize(); });

		canvas.onmousedown = function(e) {
			var rect = canvas.getBoundingClientRect();
			var mx = (e.clientX - rect.left - editPanX) / editScale;
			var my = (e.clientY - rect.top - editPanY) / editScale;

			// 检查是否点击了引脚 → 连线操作
			var pinHit = hitTestPin(mx, my);
			if (pinHit) {
				if (editWiring) {
					// 完成连线
					if (editWiring.fromIdx !== pinHit.nodeIdx) {
						if (editWiring.fromSide === 'out' && pinHit.side === 'in') {
							var srcInfo = NODE_INFO[editNodes[editWiring.fromIdx].type] || {};
							var outMax = srcInfo.outMax || 99;
							var outCount = 0;
							for (var lc = 0; lc < editLinks.length; lc++) {
								if (editLinks[lc].srcIdx === editWiring.fromIdx) outCount++;
							}
							if (outCount >= outMax) {
								showToast('该模块输出已达上限 (' + outMax + ')', 'err');
								editWiring = null;
								editSelectedNode = -1;
								drawEditor();
								return;
							}
							var link = { srcIdx: editWiring.fromIdx, dstIdx: pinHit.nodeIdx, dstPin: pinHit.pinIdx };
							// SR/RS 每个引脚只能接一根线
							var dstNode = editNodes[pinHit.nodeIdx];
							if (dstNode.type === NT_SR || dstNode.type === NT_RS) {
								for (var lc2 = 0; lc2 < editLinks.length; lc2++) {
									if (editLinks[lc2].dstIdx === pinHit.nodeIdx && editLinks[lc2].dstPin === pinHit.pinIdx) {
										var pinLabel = (dstNode.type === NT_SR) ? ['S','R'] : ['R','S'];
										showToast('引脚 ' + pinLabel[pinHit.pinIdx] + ' 已有连线，每个引脚只能接一根线', 'err');
										editWiring = null;
										editSelectedNode = -1;
										drawEditor();
										return;
									}
								}
							}
							editLinks.push(link);
							showToast('连线已添加', 'ok');
						} else if (editWiring.fromSide === 'in' && pinHit.side === 'out') {
							editLinks.push({ srcIdx: pinHit.nodeIdx, dstIdx: editWiring.fromIdx, dstPin: 0 });
							showToast('连线已添加', 'ok');
						} else {
							showToast('不能这样连线', 'err');
						}
					}
					editWiring = null;
					editSelectedNode = -1;
					editSelectedLink = -1;
					drawEditor();
					return;
				} else {
					editWiring = { fromIdx: pinHit.nodeIdx, fromSide: pinHit.side, fromPin: pinHit.pinIdx };
					editSelectedNode = -1;
					editSelectedLink = -1;
					drawEditor();
					return;
				}
			}

			// 检查是否点击了连线 → 选中高亮（左键选择，右键删除）
			var linkHit = hitTestLink(mx, my);
			if (linkHit >= 0) {
				editSelectedLink = linkHit;
				editSelectedNode = -1;
				editDragNode = null;
				if (editWiring) { editWiring = null; }
				drawEditor();
				return;
			}

			// 检查是否点击了节点 → 拖拽或选中
			var nodeHit = hitTestNode(mx, my);
			if (nodeHit >= 0) {
				editSelectedNode = nodeHit;
				editSelectedLink = -1;
				editDragNode = nodeHit;
				editDragOffX = mx - editNodes[nodeHit]._lx;
				editDragOffY = my - editNodes[nodeHit]._ly;
				drawEditor();
				return;
			}

			// 空白处取消选中和连线，并开始画布拖拽
			editSelectedNode = -1;
			editSelectedLink = -1;
			if (editWiring) { editWiring = null; }
			editDragCanvas = { startX: e.clientX, startY: e.clientY, panX: editPanX, panY: editPanY };
			canvas.style.cursor = 'grabbing';
			drawEditor();
		};

		canvas.onmousemove = function(e) {
			var rect = canvas.getBoundingClientRect();
			var mx = (e.clientX - rect.left - editPanX) / editScale;
			var my = (e.clientY - rect.top - editPanY) / editScale;

			if (editDragCanvas) {
				editPanX = editDragCanvas.panX + (e.clientX - editDragCanvas.startX);
				editPanY = editDragCanvas.panY + (e.clientY - editDragCanvas.startY);
				drawEditor();
				return;
			}

			if (editDragNode !== null) {
				editNodes[editDragNode]._lx = mx - editDragOffX;
				editNodes[editDragNode]._ly = my - editDragOffY;
				drawEditor();
				return;
			}

			if (editWiring && editWiring.fromIdx < editNodes.length) {
				drawEditor();
			}

			var pin = hitTestPin(mx, my);
			canvas.style.cursor = pin ? 'crosshair' : (hitTestNode(mx, my) >= 0 ? 'grab' : 'default');
		};

		canvas.onmouseup = function(e) {
			if (editDragCanvas) {
				editDragCanvas = null;
				canvas.style.cursor = 'default';
				return;
			}
			if (editDragNode !== null) {
				var node = editNodes[editDragNode];
				var nodeIdx = editDragNode;
				var dx = Math.abs((e.clientX - canvas.getBoundingClientRect().left - editPanX) / editScale - node._lx - editDragOffX);
				var dy = Math.abs((e.clientY - canvas.getBoundingClientRect().top - editPanY) / editScale - node._ly - editDragOffY);
				// 小于 3px 视为点击（非拖拽）
				if (dx < 3 && dy < 3) {
					// 双击检测：同节点 400ms 内连续点击
					var now = Date.now();
					if (nodeIdx === editLastClickNode && (now - editLastClickTime) < 400) {
						onNodeDoubleClicked(nodeIdx);
						editLastClickTime = 0;
						editLastClickNode = -1;
					} else {
						editLastClickTime = now;
						editLastClickNode = nodeIdx;
					}
				}
			}
			editDragNode = null;
		};
		canvas.onmouseleave = function() { editDragNode = null; editDragCanvas = null; canvas.style.cursor = 'default'; };

		canvas.onwheel = function(e) {
			e.preventDefault();
			var delta = e.deltaY > 0 ? 0.9 : 1.1;
			var rect = canvas.getBoundingClientRect();
			var mx = e.clientX - rect.left;
			var my = e.clientY - rect.top;
			var ns = Math.max(0.2, Math.min(3, editScale * delta));
			editPanX = mx - (mx - editPanX) * (ns / editScale);
			editPanY = my - (my - editPanY) * (ns / editScale);
			editScale = ns;
			drawEditor();
		};

		// 右键 —— 删除选中的连线 / 节点
		canvas.oncontextmenu = function(e) {
			e.preventDefault();
			if (editSelectedLink >= 0) {
				editLinks.splice(editSelectedLink, 1);
				editSelectedLink = -1;
				editSelectedNode = -1;
				showToast('连线已删除', 'ok');
				drawEditor();
				return;
			}
			// 如果选中了节点也可以用右键删除
			if (editSelectedNode >= 0) {
				deleteSelectedNode();
				return;
			}
		};

		canvas.tabIndex = 0;
		canvas.onkeydown = function(e) {
			if (e.key === 'Escape') { editWiring = null; editSelectedNode = -1; editSelectedLink = -1; drawEditor(); }
			if (e.key === 'Delete' && editSelectedNode >= 0) { deleteSelectedNode(); }
			if (e.key === 'Delete' && editSelectedLink >= 0) { editLinks.splice(editSelectedLink, 1); editSelectedLink = -1; showToast('连线已删除', 'ok'); drawEditor(); }
		};
	}

	/* ---- 获取节点的实际 inPins（优先使用节点自身的配置） ---- */
	function getNodeInPins(node) {
		if (node.inPins !== undefined && node.inPins > 0) return node.inPins;
		var info = NODE_INFO[node.type] || {};
		return info.inPins || 0;
	}

	function getNodeSize(node) {
		var isIO = (node.type === NT_OUTPUT || node.type === NT_INPUT);
		var isSRRS = (node.type === NT_SR || node.type === NT_RS);
		var inP = getNodeInPins(node);
		var h = isIO ? 80 : (isSRRS ? 86 : (66 + Math.max(0, inP - 2) * 14));
		var w = isIO ? 250 : 170;
		return { w: w, h: h };
	}

	function hitTestNode(mx, my) {
		for (var i = editNodes.length - 1; i >= 0; i--) {
			var n = editNodes[i];
			var sz = getNodeSize(n);
			var x = n._lx, y = n._ly;
			if (mx > x - sz.w/2 && mx < x + sz.w/2 && my > y - sz.h/2 && my < y + sz.h/2) return i;
		}
		return -1;
	}

	function hitTestPin(mx, my) {
		var pinR = 8;
		for (var i = editNodes.length - 1; i >= 0; i--) {
			var n = editNodes[i];
			var sz = getNodeSize(n);
			var info = NODE_INFO[n.type] || NODE_INFO[0];
			var x = n._lx, y = n._ly;
			var isIO = (n.type === NT_INPUT || n.type === NT_OUTPUT);
			var isSRRS = (n.type === NT_SR || n.type === NT_RS);
			var inP = getNodeInPins(n);
			// 输入引脚（左侧）
			if (info.inPins > 0) {
				var testPins = isSRRS ? 2 : (isIO ? inP : 1);
				for (var p = 0; p < testPins; p++) {
					var py = y - sz.h/2 + sz.h * (p + 1) / (isSRRS ? 3 : (inP + 1));
					var dx = mx - (x - sz.w/2), dy = my - py;
					if (Math.sqrt(dx*dx + dy*dy) <= pinR) return { nodeIdx: i, side: 'in', pinIdx: p };
				}
			}
			// 输出引脚（右侧）
			if (info.outPins > 0) {
				var dx = mx - (x + sz.w/2), dy = my - y;
				if (Math.sqrt(dx*dx + dy*dy) <= pinR) return { nodeIdx: i, side: 'out', pinIdx: 0 };
			}
		}
		return null;
	}

	function hitTestLink(mx, my) {
		for (var i = 0; i < editLinks.length; i++) {
			var l = editLinks[i];
			if (l.srcIdx >= editNodes.length || l.dstIdx >= editNodes.length) continue;
			var sn = editNodes[l.srcIdx], dn = editNodes[l.dstIdx];
			var szSrc = getNodeSize(sn), szDst = getNodeSize(dn);
			var x1 = sn._lx + szSrc.w/2, y1 = sn._ly;
			var dstIsSRRS = (dn.type === NT_SR || dn.type === NT_RS);
			var gateInPins = getNodeInPins(dn);
			var dstPinIdx2 = (l.dstPin !== undefined) ? l.dstPin : 0;
			var y2 = dn._ly - szDst.h/2 + szDst.h * (dstPinIdx2 + 1) / (dstIsSRRS ? 3 : (gateInPins + 1));
			var x2 = dn._lx - szDst.w/2;
			var t = ((mx - x1)*(x2 - x1) + (my - y1)*(y2 - y1)) / ((x2 - x1)*(x2 - x1) + (y2 - y1)*(y2 - y1));
			t = Math.max(0, Math.min(1, t));
			var px = x1 + t * (x2 - x1), py = y1 + t * (y2 - y1);
			if (Math.sqrt((mx - px)*(mx - px) + (my - py)*(my - py)) < 10) return i;
		}
		return -1;
	}

	function getNodeLabel(n) {
		var info = NODE_INFO[n.type] || {};
		var label = (info.name || '?');
		if (n.saddr) label += ' ' + n.saddr;
		else if (n.type <= 1) label += ' #' + n.id;
		else label += ' #' + (n.id & 0xFF);
		return label;
	}

	/* ---- 编辑器绘制 ---- */
	function drawEditor() {
		var canvas = document.getElementById('plc-edit-canvas');
		if (!canvas) return;
		var ctx = canvas.getContext('2d');
		var W = canvas.width, H = canvas.height;

		ctx.clearRect(0, 0, W, H);
		ctx.fillStyle = '#1a1a2e';
		ctx.fillRect(0, 0, W, H);

		ctx.save();
		ctx.translate(editPanX, editPanY);
		ctx.scale(editScale, editScale);

		// 网格
		ctx.strokeStyle = 'rgba(255,255,255,0.05)';
		ctx.lineWidth = 1;
		var gridSize = 40;
		var startX = Math.floor(-editPanX / editScale / gridSize) * gridSize;
		var startY = Math.floor(-editPanY / editScale / gridSize) * gridSize;
		for (var gx = startX - gridSize; gx < startX + W/editScale + gridSize; gx += gridSize) {
			ctx.beginPath(); ctx.moveTo(gx, startY - gridSize); ctx.lineTo(gx, startY + H/editScale + gridSize); ctx.stroke();
		}
		for (var gy = startY - gridSize; gy < startY + H/editScale + gridSize; gy += gridSize) {
			ctx.beginPath(); ctx.moveTo(startX - gridSize, gy); ctx.lineTo(startX + W/editScale + gridSize, gy); ctx.stroke();
		}

		// 区域标签
		ctx.fillStyle = 'rgba(255,255,255,0.08)';
		ctx.font = '28px sans-serif';
		ctx.textAlign = 'center';
		ctx.fillText('📥 输入信号区', -250, -200);
		ctx.fillText('🧮 逻辑运算区', 0, -200);
		ctx.fillText('📤 输出信号区', 250, -200);

		// 共用绘制
		drawGraphContent(ctx, editNodes, editLinks, editSelectedNode, editSelectedLink, editWiring);

		ctx.restore();

		// 更新信息栏
		var infoEl = document.getElementById('plc-edit-info');
		if (infoEl) {
			var selInfo = '';
			if (editSelectedNode >= 0 && editSelectedNode < editNodes.length) {
				selInfo = ' | 🔍 选中: ' + getNodeDisplayLabel(editNodes[editSelectedNode]);
			}
			infoEl.innerHTML = '节点: ' + editNodes.length + ' | 连线: ' + editLinks.length
				+ ' | Key: ' + escHtml(editKey) + ' | 名称: ' + escHtml(editName || '(未设置)')
				+ (editWiring ? ' | 🔗 连线中... (点击目标引脚完成，ESC取消)' : '')
				+ selInfo
				+ ' | 右键删除连线 | 滚轮缩放 | Delete键删除选中';
		}
	}

	/* ---- 添加输入/输出/运算弹窗 ---- */
	function showAddInputModal() {
		if (dcOutSignals.length === 0) { showToast('请先加载信号', 'err'); return; }

		var signals = dcOutSignals;
		var html = '<div class="modal-overlay" id="plc-modal"><div class="modal-box" style="max-width:650px">'
			+ '<h3>📥 绑定输入信号</h3>'
			+ '<input type="text" id="plc-modal-search" placeholder="搜索信号 (no/saddr/desc)..." style="width:100%;margin:8px 0;padding:8px;border:1px solid #ddd;border-radius:4px">'
			+ '<div style="max-height:350px;overflow-y:auto" id="plc-modal-list">';
		for (var j = 0; j < signals.length; j++) {
			var s = signals[j];
			html += '<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:#999">' + escHtml(s.desc) + '</span></div>';
		}
		html += '</div><button class="btn btn-outline" id="plc-modal-cancel">取消</button></div></div>';

		showModalCustom(html, function(no, saddr, desc) {
			editNodes.push({
				type: NT_INPUT, id: no, x: 0, y: 0,
				_lx: -260, _ly: editNodes.filter(function(n){return n.type===NT_INPUT;}).length * 110,
				saddr: saddr, desc: desc, no: no
			});
			drawEditor();
			showToast('已添加输入: ' + saddr, 'ok');
			return true;
		});
	}

	function showAddOutputModal() {
		if (dcOutSignals.length === 0) { showToast('请先加载信号', 'err'); return; }

		var signals = dcOutSignals;
		var html = '<div class="modal-overlay" id="plc-modal"><div class="modal-box" style="max-width:650px">'
			+ '<h3>📤 绑定输出信号</h3>'
			+ '<input type="text" id="plc-modal-search" placeholder="搜索信号 (no/saddr/desc)..." style="width:100%;margin:8px 0;padding:8px;border:1px solid #ddd;border-radius:4px">'
			+ '<div style="max-height:350px;overflow-y:auto" id="plc-modal-list">';
		for (var j = 0; j < signals.length; j++) {
			var s = signals[j];
			html += '<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:#999">' + escHtml(s.desc) + '</span></div>';
		}
		html += '</div><button class="btn btn-outline" id="plc-modal-cancel">取消</button></div></div>';

		showModalCustom(html, function(no, saddr, desc) {
			editNodes.push({
				type: NT_OUTPUT, id: no, x: 0, y: 0,
				_lx: 260, _ly: editNodes.filter(function(n){return n.type===NT_OUTPUT;}).length * 110,
				saddr: saddr, desc: desc, no: no
			});
			drawEditor();
			showToast('已添加输出: ' + saddr, 'ok');
			return true;
		});
	}

	function showAddGateModal() {
		var gates = [
			{ type: NT_AND, name: '与门 (AND)', desc: '2输入1输出，所有输入为1时输出1', icon: '🔷' },
			{ type: NT_OR,  name: '或门 (OR)',  desc: '2输入1输出，任一输入为1时输出1', icon: '🔶' },
			{ type: NT_NOT, name: '非门 (NOT)', desc: '1输入1输出，输入取反', icon: '🔺' },
			{ type: NT_62P, name: '62p 延时',   desc: '输入1延时输出1，输入0立即输出0', icon: '⏱️' },
			{ type: NT_62D, name: '62d 延时',   desc: '输入1立即输出1，输入0延时输出0', icon: '⏱️' },
			{ type: NT_RISING, name: '上升沿',  desc: '0→1跳变时输出脉冲', icon: '⬆️' },
			{ type: NT_FALLING, name: '下降沿', desc: '1→0跳变时输出脉冲', icon: '⬇️' },
			{ type: NT_SR, name: 'SR触发器',   desc: 'S置位优先，S=1置位 R=1复位', icon: '🔒' },
			{ type: NT_RS, name: 'RS触发器',   desc: 'R复位优先，R=1复位 S=1置位', icon: '🔒' }
		];

		var html = '<div class="modal-overlay" id="plc-modal"><div class="modal-box" style="max-width:500px">'
			+ '<h3>🧮 选择运算模块</h3>'
			+ '<div style="max-height:400px;overflow-y:auto">';
		for (var i = 0; i < gates.length; i++) {
			var g = gates[i];
			html += '<div class="plc-modal-row" data-gate-type="' + g.type + '">'
				+ '<strong>' + g.icon + ' ' + g.name + '</strong>'
				+ '<br><span style="color:#999;font-size:12px">' + g.desc + '</span>'
				+ '</div>';
		}
		html += '</div><button class="btn btn-outline" id="plc-modal-cancel">取消</button></div></div>';

		// 自定义 modal 处理
		var div = document.createElement('div');
		div.innerHTML = html;
		document.body.appendChild(div.firstElementChild);

		document.getElementById('plc-modal-cancel').onclick = closeModal;

		var rows = document.querySelectorAll('#plc-modal .plc-modal-row');
		for (var j = 0; j < rows.length; j++) {
			rows[j].onclick = function(e) {
				e.stopPropagation();
				var gateType = parseInt(this.getAttribute('data-gate-type'));
				var info = NODE_INFO[gateType];
				var nextIdx = editNextGateIdx[gateType] || 0;

				// 延时模块：弹出自定义延时输入框
				if (gateType === NT_62P || gateType === NT_62D) {
					closeModal();
					showDelayInputModal(gateType, nextIdx, info);
					return;
				}

				// 非延时模块：直接添加
				addGateNode(gateType, nextIdx, 0, info);
				editNextGateIdx[gateType] = nextIdx + 1;
				closeModal();
				drawEditor();
			};
		}
	}

	/** 弹出延时毫秒输入框 */
	function showDelayInputModal(gateType, nextIdx, info) {
		var html = '<div class="modal-overlay" id="plc-modal"><div class="modal-box" style="max-width:380px">'
			+ '<h3>⏱️ ' + info.name + ' — 延时配置</h3>'
			+ '<p style="color:#999;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 #555;border-radius:6px;background:#2a2a3e;color:#fff;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 div = document.createElement('div');
		div.innerHTML = html;
		document.body.appendChild(div.firstElementChild);

		document.getElementById('plc-modal-cancel').onclick = closeModal;
		document.getElementById('plc-modal-ok').onclick = function() {
			var val = parseInt(document.getElementById('plc-delay-input').value);
			if (isNaN(val) || val < 0) { showToast('请输入有效的延时毫秒数', 'err'); return; }
			closeModal();
			addGateNode(gateType, nextIdx, val, info);
			editNextGateIdx[gateType] = nextIdx + 1;
			drawEditor();
		};
		// 回车确认
		document.getElementById('plc-delay-input').onkeydown = function(e) {
			if (e.key === 'Enter') document.getElementById('plc-modal-ok').click();
		};
		setTimeout(function() {
			var inp = document.getElementById('plc-delay-input');
			if (inp) { inp.focus(); inp.select(); }
		}, 100);
	}

	/** 添加门节点到编辑区，自动找空白位置 */
	function addGateNode(gateType, nextIdx, delayMs, info) {
		var pos = findEmptyPosition();
		var node = {
			type: gateType, id: nextIdx,
			x: 0, y: 0,
			_lx: pos.x, _ly: pos.y,
			saddr: '', desc: '', no: 0
		};
		if (delayMs > 0) node.delayMs = delayMs;
		editNodes.push(node);
		showToast('已添加: ' + (info ? info.name : NODE_INFO[gateType].name) + ' #' + nextIdx, 'ok');
	}

	/** 寻找画布上最空白的区域 */
	function findEmptyPosition() {
		if (editNodes.length === 0) return { x: 0, y: 0 };
		// 先计算已有节点的包围盒，在右侧偏下寻找空白
		var minX = Infinity, maxX = -Infinity, minY = Infinity, maxY = -Infinity;
		for (var i = 0; i < editNodes.length; i++) {
			var n = editNodes[i];
			if (n._lx < minX) minX = n._lx;
			if (n._lx > maxX) maxX = n._lx;
			if (n._ly < minY) minY = n._ly;
			if (n._ly > maxY) maxY = n._ly;
		}
		// 在所有节点下方居中放置
		var candidates = [
			{ x: 0, y: maxY + 140 },
			{ x: 0, y: minY - 140 },
			{ x: maxX + 300, y: (minY + maxY) / 2 },
			{ x: minX - 300, y: (minY + maxY) / 2 }
		];
		for (var c = 0; c < candidates.length; c++) {
			if (!isPositionOccupied(candidates[c].x, candidates[c].y)) {
				return candidates[c];
			}
		}
		return { x: 0, y: maxY + 140 };
	}

	function isPositionOccupied(x, y) {
		for (var i = 0; i < editNodes.length; i++) {
			var n = editNodes[i];
			if (Math.abs(n._lx - x) < 120 && Math.abs(n._ly - y) < 80) return true;
		}
		return false;
	}

	function showModal(html, onSelect) {
		var div = document.createElement('div');
		div.innerHTML = html;
		document.body.appendChild(div.firstElementChild);

		document.getElementById('plc-modal-cancel').onclick = closeModal;

		// 搜索
		var searchInput = document.getElementById('plc-modal-search');
		if (searchInput) {
			searchInput.oninput = function() {
				var q = this.value.toLowerCase();
				var rows = document.querySelectorAll('#plc-modal .plc-modal-row');
				for (var r = 0; r < rows.length; r++) {
					var text = (rows[r].textContent || '').toLowerCase();
					rows[r].style.display = (text.indexOf(q) !== -1) ? '' : 'none';
				}
			};
		}

		var rows = document.querySelectorAll('#plc-modal .plc-modal-row');
		for (var j = 0; j < rows.length; j++) {
			rows[j].onclick = function() {
				if (this.classList.contains('plc-modal-used')) {
					showToast('该信号已被使用', 'err');
					return;
				}
				var noAttr = this.getAttribute('data-no');
				var saddr = this.getAttribute('data-saddr');
				var desc = this.getAttribute('data-desc') || '';
				var no = parseInt(noAttr) || 0;
				var result = onSelect(no, saddr, desc);
				if (result !== false) closeModal();
			};
		}
	}

	function showModalCustom(html, onSelect) {
		var div = document.createElement('div');
		div.innerHTML = html;
		document.body.appendChild(div.firstElementChild);

		document.getElementById('plc-modal-cancel').onclick = closeModal;

		var searchInput = document.getElementById('plc-modal-search');
		if (searchInput) {
			searchInput.oninput = function() {
				var q = this.value.toLowerCase();
				var rows = document.querySelectorAll('#plc-modal .plc-modal-row');
				for (var r = 0; r < rows.length; r++) {
					var text = (rows[r].textContent || '').toLowerCase();
					rows[r].style.display = (text.indexOf(q) !== -1) ? '' : 'none';
				}
			};
		}

		var rows = document.querySelectorAll('#plc-modal .plc-modal-row');
		for (var j = 0; j < rows.length; j++) {
			rows[j].onclick = function() {
				if (this.classList.contains('plc-modal-used')) {
					showToast('该信号已被使用', 'err');
					return;
				}
				var noAttr = this.getAttribute('data-no');
				var saddr = this.getAttribute('data-saddr');
				var desc = this.getAttribute('data-desc') || '';
				var no = parseInt(noAttr) || 0;
				var result = onSelect(no, saddr, desc);
				if (result !== false) closeModal();
			};
		}
	}

	function closeModal() {
		var m = document.getElementById('plc-modal');
		if (m) m.remove();
	}

	/* ---- 生成并保存配置 ---- */
	// 生成 XML 配置文件
	function generateXmlConfig() {
		var typeNames = {2:'OR',3:'AND',4:'NOT',5:'T62P',6:'T62D',7:'RISING',8:'SR',9:'RS',10:'FALLING'};
		var adjIn = {}, adjOut = {};
		for (var i = 0; i < editNodes.length; i++) { adjIn[i] = []; adjOut[i] = []; }
		for (var j = 0; j < editLinks.length; j++) {
			var l = editLinks[j];
			adjOut[l.srcIdx].push(l.dstIdx);
			adjIn[l.dstIdx].push(l.srcIdx);
		}

		var visited = {};
		var chains = [];
		for (var s = 0; s < editNodes.length; s++) {
			if (visited[s]) continue;
			var c = [], q = [s]; visited[s] = true;
			while (q.length > 0) {
				var cur = q.shift(); c.push(cur);
				var nbrs = adjOut[cur].concat(adjIn[cur]);
				for (var nb = 0; nb < nbrs.length; nb++)
					if (!visited[nbrs[nb]]) { visited[nbrs[nb]] = true; q.push(nbrs[nb]); }
			}
			chains.push(c);
		}

		function sigXml(n) { return '<Signal no="' + n.id + '" x="' + Math.round(n._lx) + '" y="' + Math.round(n._ly) + '" />'; }
		function gateTag(n, nodeIdx) {
			var tn = typeNames[n.type] || '?';
			var a = ' type="' + tn + '" id="' + (n.id & 0xFF) + '" x="' + Math.round(n._lx) + '" y="' + Math.round(n._ly) + '"';
			if (n.inPins && n.inPins > 2) a += ' inPins="' + n.inPins + '"';
			if (n.type === NT_62P || n.type === NT_62D) a += ' delayMs="' + (n.delayMs || 1000) + '"';
			// SR/RS: 输出 R/S 属性
			if ((n.type === NT_SR || n.type === NT_RS) && nodeIdx !== undefined) {
				var srLinks = [];
				for (var li = 0; li < editLinks.length; li++) {
					if (editLinks[li].dstIdx === nodeIdx) srLinks.push(editLinks[li]);
				}
				// 按 dstPin 排序，未设置的用出现顺序兜底
				var hasPin = false;
				for (var sl = 0; sl < srLinks.length; sl++) {
					if (srLinks[sl].dstPin !== undefined) { hasPin = true; break; }
				}
				if (!hasPin) {
					// 兜底：SR 按出现顺序 S→R，RS 按出现顺序 R→S
					for (var sl2 = 0; sl2 < srLinks.length; sl2++) {
						if (n.type === NT_SR) srLinks[sl2].dstPin = sl2;  // 0=S, 1=R
						else srLinks[sl2].dstPin = 1 - sl2;              // 0=R, 1=S
					}
				}
				for (var sl3 = 0; sl3 < srLinks.length; sl3++) {
					var l3 = srLinks[sl3];
					var srcNode = editNodes[l3.srcIdx];
					var srcDesc = '';
					if (srcNode.type === NT_INPUT) {
						srcDesc = 'Input#' + srcNode.id;
					} else {
						srcDesc = 'Comb#' + (srcNode.combId !== undefined ? srcNode.combId : (srcNode.id & 0xFF));
					}
					if (n.type === NT_SR) {
						if (l3.dstPin === 0) a += ' S="' + srcDesc + '"';
						else a += ' R="' + srcDesc + '"';
					} else {
						if (l3.dstPin === 1) a += ' S="' + srcDesc + '"';
						else a += ' R="' + srcDesc + '"';
					}
				}
			}
			return '<Gate' + a + ' />';
		}

		/** 后序遍历分配 combId（自底向上，从 0 起） */
		function assignCombIds(idx, ctrObj, cGatesArr) {
			editNodes[idx].combId = undefined;
			for (var ui = 0; ui < adjIn[idx].length; ui++) {
				var up = adjIn[idx][ui];
				if (editNodes[up].type > 1 && cGatesArr.indexOf(up) >= 0) {
					assignCombIds(up, ctrObj, cGatesArr);
				}
			}
			editNodes[idx].combId = ctrObj.count++;
		}

		function combXml(idx, indent) {
			var s = '';
			var cid = editNodes[idx].combId;
			s += indent + '<Comb id="' + (cid !== undefined ? cid : 0) + '">\n';
			var innerIndent = indent + '    ';
			var di = [];
			for (var fi = 0; fi < adjIn[idx].length; fi++)
				if (editNodes[adjIn[idx][fi]].type === NT_INPUT) di.push(adjIn[idx][fi]);
			if (di.length > 0) {
				s += innerIndent + '<Inputs>\n';
				for (var d = 0; d < di.length; d++) s += innerIndent + '    ' + sigXml(editNodes[di[d]]) + '\n';
				s += innerIndent + '</Inputs>\n';
			}
			for (var ui = 0; ui < adjIn[idx].length; ui++)
				if (editNodes[adjIn[idx][ui]].type > 1 && cGates.indexOf(adjIn[idx][ui]) >= 0) {
					s += combXml(adjIn[idx][ui], innerIndent);
				}
			s += innerIndent + gateTag(editNodes[idx], idx) + '\n';
			var dos = [];
			for (var oi = 0; oi < adjOut[idx].length; oi++)
				if (editNodes[adjOut[idx][oi]].type === NT_OUTPUT) dos.push(adjOut[idx][oi]);
			if (dos.length > 0) {
				s += innerIndent + '<Outputs>\n';
				for (var o = 0; o < dos.length; o++) s += innerIndent + '    ' + sigXml(editNodes[dos[o]]) + '\n';
				s += innerIndent + '</Outputs>\n';
			}
			s += indent + '</Comb>\n';
			return s;
		}

		var xml = '<?xml version="1.0" encoding="UTF-8"?>\n';
		xml += '<PLCConfig name="' + escXml(editName || 'PLC Logic') + '" key="' + escXml(editKey)
			+ '" checksum="' + (editOnlineCfgId || '00000000') + '">\n';

		for (var ci = 0; ci < chains.length; ci++) {
			var chain = chains[ci];
			var cIns = [], cGates = [], cOuts = [];
			for (var ni = 0; ni < chain.length; ni++) {
				var nd = editNodes[chain[ni]];
				if (nd.type === NT_INPUT) cIns.push(chain[ni]);
				else if (nd.type === NT_OUTPUT) cOuts.push(chain[ni]);
				else cGates.push(chain[ni]);
			}
			if (cGates.length === 0) continue;

			xml += '    <Chain gates="' + cGates.length + '" inputs="' + cIns.length + '" outputs="' + cOuts.length + '">\n';

			if (cGates.length === 1) {
				// 简单链: Inputs → Gate → Outputs
				xml += '        <Inputs>\n';
				for (var ii = 0; ii < cIns.length; ii++) xml += '            ' + sigXml(editNodes[cIns[ii]]) + '\n';
				xml += '        </Inputs>\n';
				xml += '        ' + gateTag(editNodes[cGates[0]], cGates[0]) + '\n';
				xml += '        <Outputs>\n';
				for (var oo = 0; oo < cOuts.length; oo++) xml += '            ' + sigXml(editNodes[cOuts[oo]]) + '\n';
				xml += '        </Outputs>\n';
			} else {
				// 组合链: 链级门 + Comb + Outputs
				// 链级门 = 下游没有 cGates 内其他门的门（只连输出或链外）
				var chainGates = [];
				for (var gg = 0; gg < cGates.length; gg++) {
					var allToOut = true;
					for (var so = 0; so < adjOut[cGates[gg]].length; so++)
						if (cGates.indexOf(adjOut[cGates[gg]][so]) >= 0) { allToOut = false; break; }
					if (allToOut) chainGates.push(cGates[gg]);
				}
				// combRoots: 直接馈入 chainGate 的门（combXml 会递归处理上游嵌套）
				var combRootSet = {};
				for (var cg = 0; cg < chainGates.length; cg++) {
					for (var ui = 0; ui < adjIn[chainGates[cg]].length; ui++) {
						var up = adjIn[chainGates[cg]][ui];
						if (cGates.indexOf(up) >= 0) combRootSet[up] = true;
					}
				}
				// 去重：若 A 是 B 的下游，只保留 B（combXml 从 B 开始会递归包含 A）
				var combRootArr = Object.keys(combRootSet).map(Number);
				var combRoots = [];
				for (var rr = 0; rr < combRootArr.length; rr++) {
					var isDownstream = false;
					for (var rr2 = 0; rr2 < combRootArr.length; rr2++) {
						if (rr === rr2) continue;
						var visited2 = {};
						var queue2 = [combRootArr[rr2]];
						visited2[combRootArr[rr2]] = true;
						while (queue2.length > 0) {
							var cur2 = queue2.shift();
							if (cur2 === combRootArr[rr]) { isDownstream = true; break; }
							for (var si = 0; si < adjOut[cur2].length; si++) {
								var nx = adjOut[cur2][si];
								if (!visited2[nx] && cGates.indexOf(nx) >= 0) { visited2[nx] = true; queue2.push(nx); }
							}
						}
						if (isDownstream) break;
					}
					if (!isDownstream) combRoots.push(combRootArr[rr]);
				}
				// 输出 Comb（每个 combRoot 对应一个顶层 Comb，内部递归嵌套）
				// 先分配 combId
				var combIdCtr = { count: 0 };
				for (var tt = 0; tt < combRoots.length; tt++) {
					assignCombIds(combRoots[tt], combIdCtr, cGates);
				}
				for (var tt = 0; tt < combRoots.length; tt++) {
					xml += combXml(combRoots[tt], '        ');
				}
				// 输出链级门 — 若存在直接 Input 连接（未进入 Comb 的），先行输出
				for (var cg = 0; cg < chainGates.length; cg++) {
					var cgNode = editNodes[chainGates[cg]];
					var directInputs = [];
					for (var cui = 0; cui < adjIn[chainGates[cg]].length; cui++) {
						var upIdx = adjIn[chainGates[cg]][cui];
						if (editNodes[upIdx].type === NT_INPUT) {
							directInputs.push(upIdx);
						}
					}
					if (directInputs.length > 0) {
						xml += '        <Inputs>\n';
						for (var di2 = 0; di2 < directInputs.length; di2++) {
							xml += '            ' + sigXml(editNodes[directInputs[di2]]) + '\n';
						}
						xml += '        </Inputs>\n';
					}
					xml += '        ' + gateTag(cgNode, chainGates[cg]) + '\n';
				}
				// 链级 Outputs — 仅包含直接连到 chainGate 的输出信号
				// （Comb 内部的输出信号已在 combXml 中处理）
				var chainOutSet = {};
				for (var cg2 = 0; cg2 < chainGates.length; cg2++) {
					for (var oi = 0; oi < adjOut[chainGates[cg2]].length; oi++) {
						var outIdx = adjOut[chainGates[cg2]][oi];
						if (editNodes[outIdx].type === NT_OUTPUT) chainOutSet[outIdx] = true;
					}
				}
				var chainOuts = Object.keys(chainOutSet).map(Number);
				// 同时保留直接连到输入信号（无门）的输出
				for (var oo2 = 0; oo2 < cOuts.length; oo2++) {
					var hasGateSrc = false;
					for (var ai = 0; ai < adjIn[cOuts[oo2]].length; ai++) {
						if (cGates.indexOf(adjIn[cOuts[oo2]][ai]) >= 0) hasGateSrc = true;
					}
					if (!hasGateSrc) chainOutSet[cOuts[oo2]] = true;
				}
				chainOuts = Object.keys(chainOutSet).map(Number);
				if (chainOuts.length > 0) {
					xml += '        <Outputs>\n';
					for (var oo = 0; oo < chainOuts.length; oo++) xml += '            ' + sigXml(editNodes[chainOuts[oo]]) + '\n';
					xml += '        </Outputs>\n';
				}
			}

			xml += '    </Chain>\n';
		}

		xml += '</PLCConfig>\n';
		return xml;
	}

	function escXml(s) { return String(s).replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;').replace(/"/g,'&quot;'); }

	function generateAndSaveConfig() {
		if (editNodes.length === 0) { showToast('请先添加节点', 'err'); return; }
		var hasInput = editNodes.some(function(n) { return n.type === NT_INPUT; });
		var hasOutput = editNodes.some(function(n) { return n.type === NT_OUTPUT; });
		if (!hasInput || !hasOutput) { showToast('至少需要一个输入和一个输出', 'err'); return; }
		if (editLinks.length === 0) { showToast('请先连线', 'err'); return; }

		var seenIds = {};
		for (var di = 0; di < editNodes.length; di++) {
			var dn = editNodes[di];
			if (dn.type === NT_INPUT || dn.type === NT_OUTPUT) continue;
			var key2 = dn.type + ':' + (dn.id & 0xFF);
			if (seenIds[key2]) {
				var next = editNextGateIdx[dn.type] || 0;
				editNodes[di].id = (dn.id & ~0xFF) | next;
				editNextGateIdx[dn.type] = next + 1;
			} else { seenIds[key2] = true; }
		}

		var fileContent = generateXmlConfig();
		var preview = fileContent.length > 2000 ? fileContent.substring(0,2000)+'…' : fileContent;

		var previewHtml = '<div class="modal-overlay" id="plc-modal"><div class="modal-box" style="max-width:700px">'
			+ '<h3>✅ 配置预览 (XML)</h3>'
			+ '<p>Key: <strong>' + escHtml(editKey) + '</strong> | 名称: <strong>' + escHtml(editName || 'PLC Logic') + '</strong></p>'
			+ '<p>节点: ' + editNodes.length + ' | 连线: ' + editLinks.length + '</p>'
			+ '<div style="max-height:250px;overflow:auto;background:#1a1a2e;color:#0f0;padding:12px;border-radius:6px;font-family:monospace;font-size:12px;white-space:pre-wrap">' + escHtml(preview) + '</div>'
			+ '<div style="margin-top:12px;display:flex;gap:8px">'
			+ '<button class="btn btn-primary" id="plc-modal-save">💾 保存 Reclose_logic.xml</button>'
			+ '<button class="btn btn-outline" id="plc-modal-cancel">取消</button>'
			+ '</div></div></div>';

		var div = document.createElement('div');
		div.innerHTML = previewHtml;
		document.body.appendChild(div.firstElementChild);

		document.getElementById('plc-modal-cancel').onclick = closeModal;
		document.getElementById('plc-modal-save').onclick = function() {
			closeModal();
			var oldContent = loadedFiles['config/PLC/Reclose_logic.xml'] || '';
			if (oldContent && oldContent.length > 10) {
				WsClient.send({ type: 'set_file', path: 'config/PLC/Reclose_logic.xml.bak', content: oldContent });
			}
			var sent = WsClient.send({
				type: 'set_file',
				path: 'config/PLC/Reclose_logic.xml',
				content: fileContent
			});
			if (sent) {
				showToast('XML 配置已保存！请重启 PLC 模块生效', 'ok');
				loadedFiles['config/PLC/Reclose_logic.xml'] = null;
				loadedFiles['config/PLC/Reclose_logic.xml.bak'] = null;
				logicGraphs = [];
			} else {
				showToast('保存失败：WebSocket 未连接', 'err');
			}
		};
	}

	/* ========== 页面生命周期 ========== */
	function onEnter() {
		// 重试之前因WS未连接而失败的文件请求
		if (PlcConfigPage._retryList && PlcConfigPage._retryList.length > 0) {
			var retry = PlcConfigPage._retryList.slice();
			PlcConfigPage._retryList = [];
			for (var ri = 0; ri < retry.length; ri++) {
				requestFile(retry[ri], PlcConfigPage._pending[retry[ri]] || function(){});
			}
		}
		// 自动加载 datacenter out 信号
		if (dcOutSignals.length === 0) {
			loadDcOutSignals();
		}
	}

	function onLeave() {
		editWiring = null;
		editDragNode = null;
		editSelectedNode = -1;
		editSelectedLink = -1;
	}

	function onData(data) {
		if (!data || typeof data !== 'object') return;

		// 处理 file_content 响应
		if (data.type === 'file_content') {
			var path = data.path;
			var content = data.content || '';
			var error = data.error || '';
			if (error) {
				// 文件不存在等情况：仍然触发回调（传null），确保 loadLogicFileAll 的 tryCb 能正常计数
				loadedFiles[path] = null;
				if (PlcConfigPage._pending && PlcConfigPage._pending[path]) {
					PlcConfigPage._pending[path](null);
					delete PlcConfigPage._pending[path];
				}
			} else {
				loadedFiles[path] = content;
				if (PlcConfigPage._pending && PlcConfigPage._pending[path]) {
					PlcConfigPage._pending[path](content);
					delete PlcConfigPage._pending[path];
				}
			}
			if (PlcConfigPage._retryList) {
				var rIdx = PlcConfigPage._retryList.indexOf(path);
				if (rIdx >= 0) PlcConfigPage._retryList.splice(rIdx, 1);
			}
			return;
		}

		// 处理 file_saved 响应
		if (data.type === 'file_saved') {
			if (data.success) {
				showToast('文件已保存: ' + data.path, 'ok');
			} else {
				showToast('保存失败: ' + (data.error || '未知错误'), 'err');
			}
			return;
		}
	}

	return {
		render: render,
		onEnter: onEnter,
		onLeave: onLeave,
		onData: onData,
		_pending: {}
	};
})();

Router.register('#plc_config', function(c) { PlcConfigPage.render(c); });