feat(plc): PLC逻辑配置完整功能实现

前端 plc_config.js:
- NODE_INFO 输入/输出限制修正 (AND/OR ≤10, NOT ≤1, 62P/62D/沿 ≤1, SR/RS ≤2)
- delayMs 独立字段,不再编码到 id 高位
- SR/RS 双引脚标注 (S 黄色/R 红色),各引脚只能接一条线
- Comb id 解析与生成 (<Comb id="N">)
- Gate R/S 属性生成 (S="Comb#N"/"Input#N" 格式)
- 延时门新增/双击编辑自定义 modal
- 新节点自动空位放置
- XML 链级 Inputs 自动输出 (RS/SR 直接 Input 连接)
- 解析补全: 链级 Gate 读 R/S/delayMs,dstPin 修复
- RS/SR R/S 属性→Input#N 链接自动创建

后端 plc.cpp:
- LogicNode 新增 delay_ms, rs_s_src[32], rs_r_src[32]
- LogicLink 新增 dst_pin 字段
- parse_xml_comb: 读 delayMs→delay_ms, R/S→链接+dst_pin
- execute_62p/62d 改用 node->delay_ms
- collect_input_signals 按 dst_pin 填入
- has_comb 路径 R/S 属性→Input 链接创建

配置文件 Reclose_logic.xml 更新为完整示例
This commit is contained in:
ypc 2026-07-08 13:22:50 +08:00
parent 780b8cc311
commit 7d68713f4c
5 changed files with 6070 additions and 4618 deletions

View File

@ -71,6 +71,9 @@ typedef struct {
struct timespec timer_start; // 定时器启动时刻
int timer_delay_ms; // 延时毫秒数
int timer_pending_output; // 定时器到期后的输出值,-1=无待定输出
int delay_ms; // 配置的延时毫秒数62p/62d从XML delayMs属性读取
char rs_s_src[32]; // S引脚来源描述SR/RS
char rs_r_src[32]; // R引脚来源描述SR/RS
// 拓扑排序字段
int indegree; // 入度(依赖的前驱节点数)
} LogicNode;
@ -80,6 +83,7 @@ typedef struct {
int src_id;
int dest_type;
int dest_id;
int dst_pin; // 目标引脚序号0=S, 1=R仅SR/RS有效-1=默认)
} LogicLink;
typedef struct {
@ -297,18 +301,90 @@ static int parse_xml_comb(tinyxml2::XMLElement *comb_el, LogicGraph *graph)
gn.id = gate_el->IntAttribute("id");
int gi = graph->node_count - 1;
// 读取延时毫秒62P/62D
const char *delay_str = gate_el->Attribute("delayMs");
if(delay_str) gn.delay_ms = atoi(delay_str);
else gn.delay_ms = 1000; // 默认1000ms
// 读取 SR/RS 的 R/S 属性
const char *r_attr = gate_el->Attribute("R");
const char *s_attr = gate_el->Attribute("S");
if(r_attr) { strncpy(gn.rs_r_src, r_attr, sizeof(gn.rs_r_src) - 1); }
if(s_attr) { strncpy(gn.rs_s_src, s_attr, sizeof(gn.rs_s_src) - 1); }
int is_srrs = (gn.type == NODE_TYPE_SR_LATCH || gn.type == NODE_TYPE_RS_LATCH);
// 收集嵌套 Comb 的 Gate 索引(按 id 映射,用于 R/S 解析)
int sub_gate_by_comb_id[16] = {-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1};
int sub_gate_count = 0;
for(tinyxml2::XMLElement *nc = comb_el->FirstChildElement("Comb"); nc;
nc = nc->NextSiblingElement("Comb"))
{
int up = parse_xml_comb(nc, graph);
if(up >= 0 && graph->link_count < MAX_LINKS_PER_GRAPH)
{ LogicLink &l = graph->links[graph->link_count++];
l.src_type = graph->nodes[up].type; l.src_id = graph->nodes[up].id;
l.dest_type = gn.type; l.dest_id = gn.id; }
if(up >= 0)
{
int cid = nc->IntAttribute("id");
if(cid >= 0 && cid < 16) sub_gate_by_comb_id[cid] = up;
sub_gate_count++;
// 非 SR/RS: 自动创建嵌套 Comb Gate → 当前 Gate 的链接
if(!is_srrs && graph->link_count < MAX_LINKS_PER_GRAPH)
{
LogicLink &l = graph->links[graph->link_count++];
memset(&l, 0, sizeof(l)); l.dst_pin = -1;
l.src_type = graph->nodes[up].type; l.src_id = graph->nodes[up].id;
l.dest_type = gn.type; l.dest_id = gn.id;
}
}
}
// SR/RS: 根据 R/S 属性创建链接
if(is_srrs)
{
// 解析 "Comb#N" 或 "Input#N"
for(int pin = 0; pin < 2; pin++)
{
const char *attr = (pin == 0) ? s_attr : r_attr;
if(!attr || graph->link_count >= MAX_LINKS_PER_GRAPH) continue;
if(strncmp(attr, "Comb#", 5) == 0)
{
int cid = atoi(attr + 5);
if(cid >= 0 && cid < 16 && sub_gate_by_comb_id[cid] >= 0)
{
int up = sub_gate_by_comb_id[cid];
LogicLink &l = graph->links[graph->link_count++];
memset(&l, 0, sizeof(l));
l.src_type = graph->nodes[up].type; l.src_id = graph->nodes[up].id;
l.dest_type = gn.type; l.dest_id = gn.id;
// SR: S=pin0, R=pin1; RS: S=pin1, R=pin0
if(gn.type == NODE_TYPE_SR_LATCH)
l.dst_pin = pin; // 0=S, 1=R
else
l.dst_pin = 1 - pin; // 0=R, 1=S
}
}
else if(strncmp(attr, "Input#", 6) == 0)
{
int ino = atoi(attr + 6);
int si = ensure_signal(graph, NODE_TYPE_HW_INPUT, ino);
if(si >= 0)
{
LogicLink &l = graph->links[graph->link_count++];
memset(&l, 0, sizeof(l));
l.src_type = NODE_TYPE_HW_INPUT; l.src_id = graph->nodes[si].id;
l.dest_type = gn.type; l.dest_id = gn.id;
if(gn.type == NODE_TYPE_SR_LATCH)
l.dst_pin = pin;
else
l.dst_pin = 1 - pin;
}
}
}
}
tinyxml2::XMLElement *ins = comb_el->FirstChildElement("Inputs");
if(ins)
if(ins && !is_srrs)
for(tinyxml2::XMLElement *sig = ins->FirstChildElement("Signal"); sig;
sig = sig->NextSiblingElement("Signal"))
{
@ -316,6 +392,7 @@ static int parse_xml_comb(tinyxml2::XMLElement *comb_el, LogicGraph *graph)
int si = ensure_signal(graph, NODE_TYPE_HW_INPUT, sig->IntAttribute("no"));
if(si >= 0)
{ LogicLink &l = graph->links[graph->link_count++];
memset(&l, 0, sizeof(l)); l.dst_pin = -1;
l.src_type=NODE_TYPE_HW_INPUT; l.src_id=graph->nodes[si].id;
l.dest_type=gn.type; l.dest_id=gn.id; }
}
@ -426,6 +503,7 @@ int parse_reclose_logic_xml(const char *file_path, LogicGraph graphs[], int *gra
if(ci >= 0 && cur->link_count < MAX_LINKS_PER_GRAPH)
{
LogicLink &l = cur->links[cur->link_count++];
memset(&l, 0, sizeof(l)); l.dst_pin = -1;
l.src_type = cur->nodes[ci].type;
l.src_id = cur->nodes[ci].id;
l.dest_type = cur->nodes[cgi].type;
@ -449,11 +527,68 @@ int parse_reclose_logic_xml(const char *file_path, LogicGraph graphs[], int *gra
int si = find_node_index(cur, NODE_TYPE_HW_OUTPUT, sig->IntAttribute("no"));
if(si >= 0)
{ LogicLink &l = cur->links[cur->link_count++];
memset(&l, 0, sizeof(l)); l.dst_pin = -1;
l.src_type=cur->nodes[cgi].type; l.src_id=cur->nodes[cgi].id;
l.dest_type=NODE_TYPE_HW_OUTPUT; l.dest_id=cur->nodes[si].id; }
}
}
}
// 链级 Inputs → 链级 Gate如 RS/SR 直接连 Input 信号)
if(gate_el && ins_el)
{
int cgt = gate_type_from_xml_str(gate_el->Attribute("type"));
int cgi = find_node_index(cur, cgt, gate_el->IntAttribute("id"));
if(cgi >= 0)
{
for(tinyxml2::XMLElement *sig = ins_el->FirstChildElement("Signal"); sig;
sig = sig->NextSiblingElement("Signal"))
{
if(cur->link_count >= MAX_LINKS_PER_GRAPH) break;
int si = find_node_index(cur, NODE_TYPE_HW_INPUT, sig->IntAttribute("no"));
if(si >= 0)
{ LogicLink &l = cur->links[cur->link_count++];
memset(&l, 0, sizeof(l)); l.dst_pin = -1;
l.src_type=NODE_TYPE_HW_INPUT; l.src_id=cur->nodes[si].id;
l.dest_type=cur->nodes[cgi].type; l.dest_id=cur->nodes[cgi].id; }
}
}
}
// RS/SR 的 R/S 属性引用的 Input 信号(无 <Inputs> 块时也需创建链接)
if(gate_el)
{
int cgt = gate_type_from_xml_str(gate_el->Attribute("type"));
if(cgt == NODE_TYPE_SR_LATCH || cgt == NODE_TYPE_RS_LATCH)
{
const char *s_attr2 = gate_el->Attribute("S");
const char *r_attr2 = gate_el->Attribute("R");
int cgi2 = find_node_index(cur, cgt, gate_el->IntAttribute("id"));
if(cgi2 >= 0)
{
if(s_attr2 && strncmp(s_attr2, "Input#", 6) == 0)
{
int ino = atoi(s_attr2 + 6);
int si = ensure_signal(cur, NODE_TYPE_HW_INPUT, ino);
if(si >= 0 && cur->link_count < MAX_LINKS_PER_GRAPH)
{ LogicLink &l = cur->links[cur->link_count++];
memset(&l, 0, sizeof(l)); l.dst_pin = -1;
l.src_type=NODE_TYPE_HW_INPUT; l.src_id=cur->nodes[si].id;
l.dest_type=cur->nodes[cgi2].type; l.dest_id=cur->nodes[cgi2].id; }
}
if(r_attr2 && strncmp(r_attr2, "Input#", 6) == 0)
{
int ino = atoi(r_attr2 + 6);
int si = ensure_signal(cur, NODE_TYPE_HW_INPUT, ino);
if(si >= 0 && cur->link_count < MAX_LINKS_PER_GRAPH)
{ LogicLink &l = cur->links[cur->link_count++];
memset(&l, 0, sizeof(l)); l.dst_pin = -1;
l.src_type=NODE_TYPE_HW_INPUT; l.src_id=cur->nodes[si].id;
l.dest_type=cur->nodes[cgi2].type; l.dest_id=cur->nodes[cgi2].id; }
}
}
}
}
}
else
{
@ -471,6 +606,7 @@ int parse_reclose_logic_xml(const char *file_path, LogicGraph graphs[], int *gra
int si = find_node_index(cur, NODE_TYPE_HW_INPUT, sig->IntAttribute("no"));
if(si >= 0)
{ LogicLink &l = cur->links[cur->link_count++];
memset(&l, 0, sizeof(l)); l.dst_pin = -1;
l.src_type=NODE_TYPE_HW_INPUT; l.src_id=cur->nodes[si].id;
l.dest_type=cur->nodes[gi].type; l.dest_id=cur->nodes[gi].id; }
}
@ -483,6 +619,7 @@ int parse_reclose_logic_xml(const char *file_path, LogicGraph graphs[], int *gra
int si = find_node_index(cur, NODE_TYPE_HW_OUTPUT, sig->IntAttribute("no"));
if(si >= 0)
{ LogicLink &l = cur->links[cur->link_count++];
memset(&l, 0, sizeof(l)); l.dst_pin = -1;
l.src_type=cur->nodes[gi].type; l.src_id=cur->nodes[gi].id;
l.dest_type=NODE_TYPE_HW_OUTPUT; l.dest_id=cur->nodes[si].id; }
}
@ -505,9 +642,19 @@ static int collect_input_signals(LogicGraph *graph, LogicNode *element_node,
((link->dest_id & 0xFF) == (element_node->id & 0xFF))) {
LogicNode *src_node = find_node(graph, link->src_type, link->src_id);
if (src_node) {
input_values[input_count++] = src_node->value;
printf(" 找到输入信号:%d:%d = %d\n",
link->src_type, link->src_id, src_node->value);
// SR/RS: 按 dst_pin 填入对应位置
int is_srrs = (element_node->type == NODE_TYPE_SR_LATCH ||
element_node->type == NODE_TYPE_RS_LATCH);
if(is_srrs && link->dst_pin >= 0) {
if(input_count < 2) {
input_values[link->dst_pin] = src_node->value;
if(link->dst_pin >= input_count) input_count = link->dst_pin + 1;
}
} else {
input_values[input_count++] = src_node->value;
}
printf(" 找到输入信号:%d:%d = %d (pin=%d)\n",
link->src_type, link->src_id, src_node->value, link->dst_pin);
}
}
}
@ -544,15 +691,15 @@ static void execute_or_gate(LogicNode *or_node, int *input_values, int input_cou
}
static void execute_62p_gate(LogicNode *node, int input_value) {
int tp = (node->id >> 8) & 0xFFFFFF;
int delay_ms = node->delay_ms > 0 ? node->delay_ms : 1000;
int component_id = node->id & 0xFF;
if (input_value == 1) {
if (!node->timer_active) {
// 启动延时定时器,延时 tp 毫秒后输出 1
// 启动延时定时器,延时 delay_ms 毫秒后输出 1
printf(" 62p延时元件 (ID=%d, 序号=%d):输入=1启动非阻塞延时%d毫秒\n",
node->id, component_id, tp);
node->id, component_id, delay_ms);
clock_gettime(CLOCK_MONOTONIC, &node->timer_start);
node->timer_delay_ms = tp;
node->timer_delay_ms = delay_ms;
node->timer_pending_output = 1;
node->timer_active = 1;
// 保持当前输出值不变,等待定时器到期
@ -574,7 +721,7 @@ static void execute_62p_gate(LogicNode *node, int input_value) {
}
static void execute_62d_gate(LogicNode *node, int input_value) {
int td = (node->id >> 8) & 0xFFFFFF;
int delay_ms = node->delay_ms > 0 ? node->delay_ms : 1000;
int component_id = node->id & 0xFF;
if (input_value == 1) {
// 输入为 1立即输出 1 并取消定时器
@ -585,11 +732,11 @@ static void execute_62d_gate(LogicNode *node, int input_value) {
node->id, component_id);
} else {
if (!node->timer_active) {
// 启动延时定时器,延时 td 毫秒后输出 0
// 启动延时定时器,延时 delay_ms 毫秒后输出 0
printf(" 62d延时元件 (ID=%d, 序号=%d):输入=0启动非阻塞延时%d毫秒\n",
node->id, component_id, td);
node->id, component_id, delay_ms);
clock_gettime(CLOCK_MONOTONIC, &node->timer_start);
node->timer_delay_ms = td;
node->timer_delay_ms = delay_ms;
node->timer_pending_output = 0;
node->timer_active = 1;
// 保持当前输出值不变,等待定时器到期

File diff suppressed because it is too large Load Diff

View File

@ -20,15 +20,15 @@
</Outputs>
</Chain>
<Chain gates="4" inputs="6" outputs="6">
<Comb>
<Comb id="2">
<Inputs>
<Signal no="67" x="-6" y="937" />
</Inputs>
<Comb>
<Comb id="1">
<Inputs>
<Signal no="66" x="-333" y="876" />
</Inputs>
<Comb>
<Comb id="0">
<Inputs>
<Signal no="62" x="-669" y="413" />
<Signal no="63" x="-670" y="534" />
@ -57,4 +57,71 @@
<Signal no="1368" x="915" y="947" />
</Outputs>
</Chain>
<Chain gates="1" inputs="1" outputs="1">
<Inputs>
<Signal no="5" x="-669" y="1065" />
</Inputs>
<Gate type="T62P" id="0" x="-345" y="1063" delayMs="1000" />
<Outputs>
<Signal no="1370" x="-10" y="1061" />
</Outputs>
</Chain>
<Chain gates="1" inputs="1" outputs="1">
<Inputs>
<Signal no="6" x="-667" y="1187" />
</Inputs>
<Gate type="T62D" id="0" x="-334" y="1185" delayMs="1000" />
<Outputs>
<Signal no="1371" x="-4" y="1186" />
</Outputs>
</Chain>
<Chain gates="1" inputs="1" outputs="1">
<Inputs>
<Signal no="7" x="-670" y="1305" />
</Inputs>
<Gate type="RISING" id="0" x="-336" y="1305" />
<Outputs>
<Signal no="1372" x="-3" y="1304" />
</Outputs>
</Chain>
<Chain gates="1" inputs="1" outputs="1">
<Inputs>
<Signal no="8" x="-662" y="1421" />
</Inputs>
<Gate type="FALLING" id="0" x="-335" y="1418" />
<Outputs>
<Signal no="1373" x="4" y="1419" />
</Outputs>
</Chain>
<Chain gates="2" inputs="4" outputs="3">
<Comb id="0">
<Inputs>
<Signal no="10" x="-665" y="1804" />
<Signal no="11" x="-671" y="1932" />
<Signal no="12" x="-671" y="2054" />
</Inputs>
<Gate type="AND" id="2" x="-322" y="1938" />
<Outputs>
<Signal no="1374" x="18" y="1881" />
<Signal no="1375" x="23" y="2004" />
</Outputs>
</Comb>
<Inputs>
<Signal no="15" x="-331" y="2193" />
</Inputs>
<Gate type="RS" id="0" x="14" y="2133" R="Comb#0" S="Input#15" />
<Outputs>
<Signal no="1376" x="375" y="2131" />
</Outputs>
</Chain>
<Chain gates="1" inputs="2" outputs="1">
<Inputs>
<Signal no="13" x="-664" y="1545" />
<Signal no="14" x="-665" y="1679" />
</Inputs>
<Gate type="SR" id="0" x="-329" y="1631" S="Input#13" R="Input#14" />
<Outputs>
<Signal no="1377" x="14" y="1628" />
</Outputs>
</Chain>
</PLCConfig>

View File

@ -1,60 +1,127 @@
<?xml version="1.0" encoding="UTF-8"?>
<PLCConfig name="PLC Logic" key="1783070862205" checksum="3486BE04">
<Chain gates="1" inputs="2" outputs="1">
<Inputs>
<Signal no="59" x="-676" y="69" />
<Signal no="60" x="-674" y="183" />
</Inputs>
<Gate type="OR" id="0" x="-346" y="141" />
<Outputs>
<Signal no="1361" x="-17" y="137" />
</Outputs>
</Chain>
<Chain gates="1" inputs="1" outputs="1">
<Inputs>
<Signal no="61" x="-669" y="298" />
</Inputs>
<Gate type="NOT" id="0" x="-345" y="296" />
<Outputs>
<Signal no="1362" x="-10" y="294" />
</Outputs>
</Chain>
<Chain gates="4" inputs="6" outputs="6">
<Comb>
<Chain gates="1" inputs="2" outputs="1">
<Inputs>
<Signal no="67" x="-6" y="937" />
<Signal no="59" x="-676" y="69" />
<Signal no="60" x="-674" y="183" />
</Inputs>
<Comb>
<Inputs>
<Signal no="66" x="-333" y="876" />
</Inputs>
<Comb>
<Inputs>
<Signal no="62" x="-669" y="413" />
<Signal no="63" x="-670" y="534" />
<Signal no="64" x="-667" y="650" />
<Signal no="65" x="-669" y="769" />
</Inputs>
<Gate type="AND" id="0" x="-346" y="595" />
<Outputs>
<Signal no="1363" x="-9" y="490" />
<Signal no="1364" x="-11" y="599" />
<Signal no="1365" x="-10" y="700" />
</Outputs>
</Comb>
<Gate type="AND" id="1" x="-42" y="829" />
<Outputs>
<Signal no="1366" x="324" y="773" />
</Outputs>
</Comb>
<Gate type="OR" id="1" x="288" y="890" />
<Gate type="OR" id="0" x="-346" y="141" />
<Outputs>
<Signal no="1367" x="627" y="836" />
<Signal no="1361" x="-17" y="137" />
</Outputs>
</Comb>
<Gate type="NOT" id="1" x="594" y="950" />
<Outputs>
<Signal no="1368" x="915" y="947" />
</Outputs>
</Chain>
</Chain>
<Chain gates="1" inputs="1" outputs="1">
<Inputs>
<Signal no="61" x="-669" y="298" />
</Inputs>
<Gate type="NOT" id="0" x="-345" y="296" />
<Outputs>
<Signal no="1362" x="-10" y="294" />
</Outputs>
</Chain>
<Chain gates="4" inputs="6" outputs="6">
<Comb id="2">
<Inputs>
<Signal no="67" x="-6" y="937" />
</Inputs>
<Comb id="1">
<Inputs>
<Signal no="66" x="-333" y="876" />
</Inputs>
<Comb id="0">
<Inputs>
<Signal no="62" x="-669" y="413" />
<Signal no="63" x="-670" y="534" />
<Signal no="64" x="-667" y="650" />
<Signal no="65" x="-669" y="769" />
</Inputs>
<Gate type="AND" id="0" x="-346" y="595" />
<Outputs>
<Signal no="1363" x="-9" y="490" />
<Signal no="1364" x="-11" y="599" />
<Signal no="1365" x="-10" y="700" />
</Outputs>
</Comb>
<Gate type="AND" id="1" x="-42" y="829" />
<Outputs>
<Signal no="1366" x="324" y="773" />
</Outputs>
</Comb>
<Gate type="OR" id="1" x="288" y="890" />
<Outputs>
<Signal no="1367" x="627" y="836" />
</Outputs>
</Comb>
<Gate type="NOT" id="1" x="594" y="950" />
<Outputs>
<Signal no="1368" x="915" y="947" />
</Outputs>
</Chain>
<Chain gates="1" inputs="1" outputs="1">
<Inputs>
<Signal no="5" x="-669" y="1065" />
</Inputs>
<Gate type="T62P" id="0" x="-345" y="1063" delayMs="1000" />
<Outputs>
<Signal no="1370" x="-10" y="1061" />
</Outputs>
</Chain>
<Chain gates="1" inputs="1" outputs="1">
<Inputs>
<Signal no="6" x="-667" y="1187" />
</Inputs>
<Gate type="T62D" id="0" x="-334" y="1185" delayMs="1000" />
<Outputs>
<Signal no="1371" x="-4" y="1186" />
</Outputs>
</Chain>
<Chain gates="1" inputs="1" outputs="1">
<Inputs>
<Signal no="7" x="-670" y="1305" />
</Inputs>
<Gate type="RISING" id="0" x="-336" y="1305" />
<Outputs>
<Signal no="1372" x="-3" y="1304" />
</Outputs>
</Chain>
<Chain gates="1" inputs="1" outputs="1">
<Inputs>
<Signal no="8" x="-662" y="1421" />
</Inputs>
<Gate type="FALLING" id="0" x="-335" y="1418" />
<Outputs>
<Signal no="1373" x="4" y="1419" />
</Outputs>
</Chain>
<Chain gates="2" inputs="4" outputs="3">
<Comb id="0">
<Inputs>
<Signal no="10" x="-665" y="1804" />
<Signal no="11" x="-671" y="1932" />
<Signal no="12" x="-671" y="2054" />
</Inputs>
<Gate type="AND" id="2" x="-322" y="1938" />
<Outputs>
<Signal no="1374" x="18" y="1881" />
<Signal no="1375" x="23" y="2004" />
</Outputs>
</Comb>
<Inputs>
<Signal no="15" x="-331" y="2193" />
</Inputs>
<Gate type="RS" id="0" x="14" y="2133" R="Comb#0" S="Input#15" />
<Outputs>
<Signal no="1376" x="375" y="2131" />
</Outputs>
</Chain>
<Chain gates="1" inputs="2" outputs="1">
<Inputs>
<Signal no="13" x="-664" y="1545" />
<Signal no="14" x="-665" y="1679" />
</Inputs>
<Gate type="SR" id="0" x="-329" y="1631" S="Input#13" R="Input#14" />
<Outputs>
<Signal no="1377" x="14" y="1628" />
</Outputs>
</Chain>
</PLCConfig>

View File

@ -62,15 +62,15 @@ var PlcConfigPage = (function() {
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: 99, outMax: 10 },
3: { name: '与门 AND', cls: 'node-and', icon: '🔷', color: '#9C27B0', inPins: 2, outPins: 1, inMax: 99, outMax: 10 },
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: 1 },
6: { name: '62d延时', cls: 'node-62d', icon: '⏱️', color: '#009688', inPins: 1, outPins: 1, inMax: 1, outMax: 1 },
7: { name: '上升沿', cls: 'node-re', icon: '⬆️', color: '#E91E63', inPins: 1, outPins: 1, inMax: 1, outMax: 1 },
8: { name: 'SR触发器', cls: 'node-sr', icon: '🔒', color: '#795548', inPins: 2, outPins: 1, inMax: 2, outMax: 1 },
9: { name: 'RS触发器', cls: 'node-rs', icon: '🔒', color: '#607D8B', inPins: 2, outPins: 1, inMax: 2, outMax: 1 },
10: { name: '下降沿', cls: 'node-fe', icon: '⬇️', color: '#FF5722', inPins: 1, outPins: 1, inMax: 1, outMax: 1 }
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 }
};
/* ========== 工具函数 ========== */
@ -401,7 +401,7 @@ var PlcConfigPage = (function() {
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.id = (delayMs << 8) | nd.id;
if (delayMs) nd.delayMs = delayMs;
g.nodes.push(nd);
return g.nodes.length - 1;
}
@ -417,6 +417,16 @@ var PlcConfigPage = (function() {
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') {
@ -482,8 +492,14 @@ var PlcConfigPage = (function() {
if (c.nodeName === 'Comb') parseComb(c);
if (c.nodeName === 'Gate') {
var gt = GTM[c.getAttribute('type')] || 0;
ensureGate(gt, parseInt(c.getAttribute('id')),
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;
}
@ -519,6 +535,29 @@ var PlcConfigPage = (function() {
}
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; }
@ -537,6 +576,38 @@ var PlcConfigPage = (function() {
}
}
// 最终遍:为 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);
}
@ -821,13 +892,14 @@ var PlcConfigPage = (function() {
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 : GATE_H;
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) / (dstInP + 1);
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);
@ -1001,32 +1073,50 @@ var PlcConfigPage = (function() {
// 中部:名称 + 序号
ctx.fillStyle = '#e8e8e8';
ctx.font = 'bold 11px -apple-system, sans-serif';
ctx.fillText(info.icon + ' #' + gateIdx, x, y - 4);
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';
ctx.fillText(realIn + '入 ' + realOut + '出', x, y + 14);
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 renderPins = isIO ? inP : 1;
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) / (inP + 1);
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 = '#4CAF50';
ctx.strokeStyle = isSRRS ? (p === 0 ? '#FFC107' : '#FF5722') : '#4CAF50';
ctx.lineWidth = 2;
ctx.stroke();
// 内芯
ctx.fillStyle = '#4CAF50';
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);
}
}
}
@ -1133,6 +1223,12 @@ var PlcConfigPage = (function() {
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);
}
// 复制链路(按节点在新数组中的索引)
@ -1140,7 +1236,11 @@ var PlcConfigPage = (function() {
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) editLinks.push({ srcIdx: si, dstIdx: di });
if (si >= 0 && di >= 0) {
var el = { srcIdx: si, dstIdx: di };
if (l.dstPin !== undefined) el.dstPin = l.dstPin;
editLinks.push(el);
}
}
editLoadedFromFile = true;
@ -1262,17 +1362,14 @@ var PlcConfigPage = (function() {
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) {
} 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 {
// 非门、62p、62d、上升沿、下降沿、SR、RS — 也可配置
if (node.type === NT_NOT || node.type === NT_62P || node.type === NT_62D ||
node.type === NT_RISING || node.type === NT_FALLING ||
node.type === NT_SR || node.type === NT_RS) {
showGateConfigModal(idx);
} else {
showToast(getNodeDisplayLabel(node) + ' (无需额外配置)', 'info');
}
showGateConfigModal(idx);
}
}
@ -1283,12 +1380,81 @@ var PlcConfigPage = (function() {
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 {
// 运算模块:双击即选中,不再弹窗对齐显示信息在状态栏即可
showToast(getNodeDisplayLabel(node), 'info');
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];
@ -1405,6 +1571,20 @@ var PlcConfigPage = (function() {
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') {
@ -1564,8 +1744,9 @@ var PlcConfigPage = (function() {
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 : (66 + Math.max(0, inP - 2) * 14);
var h = isIO ? 80 : (isSRRS ? 86 : (66 + Math.max(0, inP - 2) * 14));
var w = isIO ? 250 : 170;
return { w: w, h: h };
}
@ -1588,12 +1769,13 @@ var PlcConfigPage = (function() {
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 = isIO ? inP : 1;
var testPins = isSRRS ? 2 : (isIO ? inP : 1);
for (var p = 0; p < testPins; p++) {
var py = y - sz.h/2 + sz.h * (p + 1) / (inP + 1);
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 };
}
@ -1614,11 +1796,10 @@ var PlcConfigPage = (function() {
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 dstInPins = getNodeInPins(dn);
var gateInPins = getNodeInPins(dn);
var effInPins = (dn.type !== NT_INPUT && dn.type !== NT_OUTPUT) ? 1 : gateInPins;
var dstPinIdx2 = (l.dstPin !== undefined) ? l.dstPin : 0;
var y2 = dn._ly - szDst.h/2 + szDst.h * (dstPinIdx2 + 1) / (effInPins + 1);
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));
@ -1784,36 +1965,111 @@ var PlcConfigPage = (function() {
var rows = document.querySelectorAll('#plc-modal .plc-modal-row');
for (var j = 0; j < rows.length; j++) {
rows[j].onclick = function() {
rows[j].onclick = function(e) {
e.stopPropagation();
var gateType = parseInt(this.getAttribute('data-gate-type'));
var info = NODE_INFO[gateType];
var gateY = 100 + editNodes.filter(function(n) {
return n.type !== NT_INPUT && n.type !== NT_OUTPUT;
}).length * 120;
// 每种门类型独立序号,从已有最大序号+1 开始
var nextIdx = editNextGateIdx[gateType] || 0;
var gateId = nextIdx;
// 延时模块:弹出自定义延时输入框
if (gateType === NT_62P || gateType === NT_62D) {
var delayMs = prompt('请输入延时毫秒数 (默认 1000ms):', '1000');
if (!delayMs) { closeModal(); return; }
gateId = (parseInt(delayMs) << 8) | nextIdx;
closeModal();
showDelayInputModal(gateType, nextIdx, info);
return;
}
editNodes.push({
type: gateType, id: gateId, x: 0, y: 0,
_lx: 0, _ly: gateY,
saddr: '', desc: '', inf: ''
});
// 非延时模块:直接添加
addGateNode(gateType, nextIdx, 0, info);
editNextGateIdx[gateType] = nextIdx + 1;
closeModal();
drawEditor();
showToast('已添加: ' + info.name + ' #' + nextIdx, 'ok');
};
}
}
/** 弹出延时毫秒输入框 */
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;
@ -1919,39 +2175,89 @@ var PlcConfigPage = (function() {
}
function sigXml(n) { return '<Signal no="' + n.id + '" x="' + Math.round(n._lx) + '" y="' + Math.round(n._ly) + '" />'; }
function gateTag(n) {
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.id >> 8) + '"';
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→RRS 按出现顺序 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 += indent + '<Inputs>\n';
for (var d = 0; d < di.length; d++) s += indent + ' ' + sigXml(editNodes[di[d]]) + '\n';
s += indent + '</Inputs>\n';
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) {
s += indent + '<Comb>\n';
s += combXml(adjIn[idx][ui], indent + ' ');
s += indent + '</Comb>\n';
if (editNodes[adjIn[idx][ui]].type > 1 && cGates.indexOf(adjIn[idx][ui]) >= 0) {
s += combXml(adjIn[idx][ui], innerIndent);
}
s += indent + gateTag(editNodes[idx]) + '\n';
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 += indent + '<Outputs>\n';
for (var o = 0; o < dos.length; o++) s += indent + ' ' + sigXml(editNodes[dos[o]]) + '\n';
s += indent + '</Outputs>\n';
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;
}
@ -1977,7 +2283,7 @@ var PlcConfigPage = (function() {
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]]) + '\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';
@ -2022,14 +2328,32 @@ var PlcConfigPage = (function() {
if (!isDownstream) combRoots.push(combRootArr[rr]);
}
// 输出 Comb每个 combRoot 对应一个顶层 Comb内部递归嵌套
// 先分配 combId
var combIdCtr = { count: 0 };
for (var tt = 0; tt < combRoots.length; tt++) {
xml += ' <Comb>\n';
xml += combXml(combRoots[tt], ' ');
xml += ' </Comb>\n';
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++) {
xml += ' ' + gateTag(editNodes[chainGates[cg]]) + '\n';
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 中处理)