docs(配图): 菜单树总览改由生成器画,登记钩子扫不到的图
- 新增 tools/arch_diagram/gen_menu_tree_svg.py → docs/images/菜单树总览 .svg:43 节点 = 9 菜单 + 34 叶,30 已绑页 / 4 未绑 / 16 叶带密码门, 最深 4 级;读数由脚本现算,不在文档里另抄一份 - 生成器内嵌 NODE_SRC(= ui_menu_tree.txt 的 43 行节点):在仓库里跑会 与真值逐行互验、不一致 exit 1;generated-fresh 在临时目录重跑读不到 真值 → 用内嵌表,输出逐字节一致(实测临时目录产物同 sha256) - 产品需求 §8.2 原先挂的是级联框显示几何图(画的是示例级联,从来不含 整棵树的节点),换成这张树图,并删掉「该图仍是 42 节点时代所画」的 补丁注 - 图形绘制流程新增 §2.1:登记 generated-fresh 扫不到的四张图 —— 两张 tools/refgraph 产物(引用关系图 / _模块,不在 gen_*.py 命名面里)+ 两张手画稿(级联框总览、级联框几何计算)。钩子只扫 tools/arch_diagram/gen_*.py,这些图改了不会有人报警;再多一张先说清 为什么不能用生成器 - arch_diagram README 脚本一览加行
This commit is contained in:
@@ -0,0 +1,235 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="990" height="784" viewBox="0 0 990 784" font-family="Microsoft YaHei, Segoe UI, PingFang SC, sans-serif">
|
||||
<rect width="990" height="784" fill="#ffffff"/>
|
||||
<text x="28.0" y="40.0" font-size="17" fill="#0f172a" font-weight="bold" text-anchor="start">菜单树总览(层级 · 绑页 · 密码门)</text>
|
||||
<text x="28.0" y="61.0" font-size="11" fill="#475569" font-weight="normal" text-anchor="start">方框 = 菜单(可展开);细线 = 叶(对应一个页面模块,或一处空缺)</text>
|
||||
<rect x="138.0" y="87.0" width="486.0" height="18.0" rx="6" fill="#f8fafc" stroke="none" stroke-width="0"/>
|
||||
<rect x="138.0" y="105.0" width="486.0" height="54.0" rx="6" fill="#ffffff" stroke="none" stroke-width="0"/>
|
||||
<rect x="138.0" y="159.0" width="486.0" height="144.0" rx="6" fill="#f8fafc" stroke="none" stroke-width="0"/>
|
||||
<rect x="138.0" y="303.0" width="486.0" height="90.0" rx="6" fill="#ffffff" stroke="none" stroke-width="0"/>
|
||||
<rect x="138.0" y="393.0" width="486.0" height="72.0" rx="6" fill="#f8fafc" stroke="none" stroke-width="0"/>
|
||||
<rect x="138.0" y="465.0" width="486.0" height="54.0" rx="6" fill="#ffffff" stroke="none" stroke-width="0"/>
|
||||
<rect x="138.0" y="519.0" width="486.0" height="180.0" rx="6" fill="#f8fafc" stroke="none" stroke-width="0"/>
|
||||
<rect x="296.0" y="159.0" width="328.0" height="72.0" rx="5" fill="#f5f3ff" stroke="none" stroke-width="0"/>
|
||||
<rect x="296.0" y="231.0" width="328.0" height="72.0" rx="5" fill="#f5f3ff" stroke="none" stroke-width="0"/>
|
||||
<line x1="128.0" y1="96.0" x2="128.0" y2="609.0" stroke="#cbd5e1" stroke-width="1.2"/>
|
||||
<line x1="85.0" y1="352.5" x2="128.0" y2="352.5" stroke="#cbd5e1" stroke-width="1.2"/>
|
||||
<line x1="128.0" y1="96.0" x2="153.0" y2="96.0" stroke="#cbd5e1" stroke-width="1.2"/>
|
||||
<line x1="128.0" y1="132.0" x2="150.0" y2="132.0" stroke="#cbd5e1" stroke-width="1.2"/>
|
||||
<line x1="128.0" y1="231.0" x2="150.0" y2="231.0" stroke="#cbd5e1" stroke-width="1.2"/>
|
||||
<line x1="128.0" y1="348.0" x2="150.0" y2="348.0" stroke="#cbd5e1" stroke-width="1.2"/>
|
||||
<line x1="128.0" y1="429.0" x2="150.0" y2="429.0" stroke="#cbd5e1" stroke-width="1.2"/>
|
||||
<line x1="128.0" y1="492.0" x2="150.0" y2="492.0" stroke="#cbd5e1" stroke-width="1.2"/>
|
||||
<line x1="128.0" y1="609.0" x2="150.0" y2="609.0" stroke="#cbd5e1" stroke-width="1.2"/>
|
||||
<line x1="284.0" y1="114.0" x2="284.0" y2="150.0" stroke="#cbd5e1" stroke-width="1.2"/>
|
||||
<line x1="216.0" y1="132.0" x2="284.0" y2="132.0" stroke="#cbd5e1" stroke-width="1.2"/>
|
||||
<line x1="284.0" y1="114.0" x2="309.0" y2="114.0" stroke="#cbd5e1" stroke-width="1.2"/>
|
||||
<line x1="284.0" y1="132.0" x2="309.0" y2="132.0" stroke="#cbd5e1" stroke-width="1.2"/>
|
||||
<line x1="284.0" y1="150.0" x2="309.0" y2="150.0" stroke="#cbd5e1" stroke-width="1.2"/>
|
||||
<line x1="284.0" y1="195.0" x2="284.0" y2="267.0" stroke="#cbd5e1" stroke-width="1.2"/>
|
||||
<line x1="216.0" y1="231.0" x2="284.0" y2="231.0" stroke="#cbd5e1" stroke-width="1.2"/>
|
||||
<line x1="284.0" y1="195.0" x2="306.0" y2="195.0" stroke="#cbd5e1" stroke-width="1.2"/>
|
||||
<line x1="284.0" y1="267.0" x2="306.0" y2="267.0" stroke="#cbd5e1" stroke-width="1.2"/>
|
||||
<line x1="452.0" y1="168.0" x2="452.0" y2="222.0" stroke="#cbd5e1" stroke-width="1.2"/>
|
||||
<line x1="348.0" y1="195.0" x2="452.0" y2="195.0" stroke="#cbd5e1" stroke-width="1.2"/>
|
||||
<line x1="452.0" y1="168.0" x2="477.0" y2="168.0" stroke="#cbd5e1" stroke-width="1.2"/>
|
||||
<line x1="452.0" y1="186.0" x2="477.0" y2="186.0" stroke="#cbd5e1" stroke-width="1.2"/>
|
||||
<line x1="452.0" y1="204.0" x2="477.0" y2="204.0" stroke="#cbd5e1" stroke-width="1.2"/>
|
||||
<line x1="452.0" y1="222.0" x2="477.0" y2="222.0" stroke="#cbd5e1" stroke-width="1.2"/>
|
||||
<line x1="452.0" y1="240.0" x2="452.0" y2="294.0" stroke="#cbd5e1" stroke-width="1.2"/>
|
||||
<line x1="348.0" y1="267.0" x2="452.0" y2="267.0" stroke="#cbd5e1" stroke-width="1.2"/>
|
||||
<line x1="452.0" y1="240.0" x2="477.0" y2="240.0" stroke="#cbd5e1" stroke-width="1.2"/>
|
||||
<line x1="452.0" y1="258.0" x2="477.0" y2="258.0" stroke="#cbd5e1" stroke-width="1.2"/>
|
||||
<line x1="452.0" y1="276.0" x2="477.0" y2="276.0" stroke="#cbd5e1" stroke-width="1.2"/>
|
||||
<line x1="452.0" y1="294.0" x2="477.0" y2="294.0" stroke="#cbd5e1" stroke-width="1.2"/>
|
||||
<line x1="284.0" y1="312.0" x2="284.0" y2="384.0" stroke="#cbd5e1" stroke-width="1.2"/>
|
||||
<line x1="216.0" y1="348.0" x2="284.0" y2="348.0" stroke="#cbd5e1" stroke-width="1.2"/>
|
||||
<line x1="284.0" y1="312.0" x2="309.0" y2="312.0" stroke="#cbd5e1" stroke-width="1.2"/>
|
||||
<line x1="284.0" y1="330.0" x2="309.0" y2="330.0" stroke="#cbd5e1" stroke-width="1.2"/>
|
||||
<line x1="284.0" y1="348.0" x2="309.0" y2="348.0" stroke="#cbd5e1" stroke-width="1.2"/>
|
||||
<line x1="284.0" y1="366.0" x2="309.0" y2="366.0" stroke="#cbd5e1" stroke-width="1.2"/>
|
||||
<line x1="284.0" y1="384.0" x2="309.0" y2="384.0" stroke="#cbd5e1" stroke-width="1.2"/>
|
||||
<line x1="284.0" y1="402.0" x2="284.0" y2="456.0" stroke="#cbd5e1" stroke-width="1.2"/>
|
||||
<line x1="216.0" y1="429.0" x2="284.0" y2="429.0" stroke="#cbd5e1" stroke-width="1.2"/>
|
||||
<line x1="284.0" y1="402.0" x2="309.0" y2="402.0" stroke="#cbd5e1" stroke-width="1.2"/>
|
||||
<line x1="284.0" y1="420.0" x2="309.0" y2="420.0" stroke="#cbd5e1" stroke-width="1.2"/>
|
||||
<line x1="284.0" y1="438.0" x2="309.0" y2="438.0" stroke="#cbd5e1" stroke-width="1.2"/>
|
||||
<line x1="284.0" y1="456.0" x2="309.0" y2="456.0" stroke="#cbd5e1" stroke-width="1.2"/>
|
||||
<line x1="284.0" y1="474.0" x2="284.0" y2="510.0" stroke="#cbd5e1" stroke-width="1.2"/>
|
||||
<line x1="216.0" y1="492.0" x2="284.0" y2="492.0" stroke="#cbd5e1" stroke-width="1.2"/>
|
||||
<line x1="284.0" y1="474.0" x2="309.0" y2="474.0" stroke="#cbd5e1" stroke-width="1.2"/>
|
||||
<line x1="284.0" y1="492.0" x2="309.0" y2="492.0" stroke="#cbd5e1" stroke-width="1.2"/>
|
||||
<line x1="284.0" y1="510.0" x2="309.0" y2="510.0" stroke="#cbd5e1" stroke-width="1.2"/>
|
||||
<line x1="284.0" y1="528.0" x2="284.0" y2="690.0" stroke="#cbd5e1" stroke-width="1.2"/>
|
||||
<line x1="216.0" y1="609.0" x2="284.0" y2="609.0" stroke="#cbd5e1" stroke-width="1.2"/>
|
||||
<line x1="284.0" y1="528.0" x2="309.0" y2="528.0" stroke="#cbd5e1" stroke-width="1.2"/>
|
||||
<line x1="284.0" y1="546.0" x2="309.0" y2="546.0" stroke="#cbd5e1" stroke-width="1.2"/>
|
||||
<line x1="284.0" y1="564.0" x2="309.0" y2="564.0" stroke="#cbd5e1" stroke-width="1.2"/>
|
||||
<line x1="284.0" y1="582.0" x2="309.0" y2="582.0" stroke="#cbd5e1" stroke-width="1.2"/>
|
||||
<line x1="284.0" y1="600.0" x2="309.0" y2="600.0" stroke="#cbd5e1" stroke-width="1.2"/>
|
||||
<line x1="284.0" y1="618.0" x2="309.0" y2="618.0" stroke="#cbd5e1" stroke-width="1.2"/>
|
||||
<line x1="284.0" y1="636.0" x2="309.0" y2="636.0" stroke="#cbd5e1" stroke-width="1.2"/>
|
||||
<line x1="284.0" y1="654.0" x2="309.0" y2="654.0" stroke="#cbd5e1" stroke-width="1.2"/>
|
||||
<line x1="284.0" y1="672.0" x2="309.0" y2="672.0" stroke="#cbd5e1" stroke-width="1.2"/>
|
||||
<line x1="284.0" y1="690.0" x2="309.0" y2="690.0" stroke="#cbd5e1" stroke-width="1.2"/>
|
||||
<rect x="28.0" y="339.5" width="57.0" height="26.0" rx="5" fill="#eff6ff" stroke="#1d4ed8" stroke-width="1.2"/>
|
||||
<text x="56.5" y="357.2" font-size="13" fill="#1d4ed8" font-weight="bold" text-anchor="middle">主界面</text>
|
||||
<rect x="150.0" y="121.0" width="66.0" height="22.0" rx="5" fill="#ffffff" stroke="#334155" stroke-width="1.2"/>
|
||||
<text x="183.0" y="136.3" font-size="12" fill="#0f172a" font-weight="bold" text-anchor="middle">实时数据</text>
|
||||
<rect x="150.0" y="220.0" width="66.0" height="22.0" rx="5" fill="#ffffff" stroke="#334155" stroke-width="1.2"/>
|
||||
<text x="183.0" y="235.3" font-size="12" fill="#0f172a" font-weight="bold" text-anchor="middle">参数定值</text>
|
||||
<rect x="306.0" y="185.0" width="42.0" height="20.0" rx="5" fill="#ffffff" stroke="#6d28d9" stroke-width="1.2"/>
|
||||
<text x="327.0" y="199.3" font-size="12" fill="#6d28d9" font-weight="bold" text-anchor="middle">整定</text>
|
||||
<rect x="306.0" y="257.0" width="42.0" height="20.0" rx="5" fill="#ffffff" stroke="#6d28d9" stroke-width="1.2"/>
|
||||
<text x="327.0" y="271.3" font-size="12" fill="#6d28d9" font-weight="bold" text-anchor="middle">查看</text>
|
||||
<rect x="150.0" y="337.0" width="66.0" height="22.0" rx="5" fill="#ffffff" stroke="#334155" stroke-width="1.2"/>
|
||||
<text x="183.0" y="352.3" font-size="12" fill="#0f172a" font-weight="bold" text-anchor="middle">三遥设置</text>
|
||||
<rect x="150.0" y="418.0" width="66.0" height="22.0" rx="5" fill="#ffffff" stroke="#334155" stroke-width="1.2"/>
|
||||
<text x="183.0" y="433.3" font-size="12" fill="#0f172a" font-weight="bold" text-anchor="middle">装置维护</text>
|
||||
<rect x="150.0" y="481.0" width="66.0" height="22.0" rx="5" fill="#ffffff" stroke="#334155" stroke-width="1.2"/>
|
||||
<text x="183.0" y="496.3" font-size="12" fill="#0f172a" font-weight="bold" text-anchor="middle">通讯参数</text>
|
||||
<rect x="150.0" y="598.0" width="66.0" height="22.0" rx="5" fill="#ffffff" stroke="#334155" stroke-width="1.2"/>
|
||||
<text x="183.0" y="613.3" font-size="12" fill="#0f172a" font-weight="bold" text-anchor="middle">记录查询</text>
|
||||
<line x1="152.0" y1="96.0" x2="159.0" y2="96.0" stroke="#cbd5e1" stroke-width="1.2"/>
|
||||
<text x="164.0" y="100.0" font-size="12" fill="#0f172a" font-weight="normal" text-anchor="start">装置信息</text>
|
||||
<text x="222.0" y="99.5" font-size="9" fill="#94a3b8" font-weight="normal" text-anchor="start">device_info</text>
|
||||
<line x1="308.0" y1="114.0" x2="315.0" y2="114.0" stroke="#cbd5e1" stroke-width="1.2"/>
|
||||
<text x="320.0" y="118.0" font-size="12" fill="#0f172a" font-weight="normal" text-anchor="start">交流量</text>
|
||||
<text x="366.0" y="117.5" font-size="9" fill="#94a3b8" font-weight="normal" text-anchor="start">ac</text>
|
||||
<line x1="308.0" y1="132.0" x2="315.0" y2="132.0" stroke="#cbd5e1" stroke-width="1.2"/>
|
||||
<text x="320.0" y="136.0" font-size="12" fill="#0f172a" font-weight="normal" text-anchor="start">直流量</text>
|
||||
<text x="366.0" y="135.5" font-size="9" fill="#94a3b8" font-weight="normal" text-anchor="start">dc</text>
|
||||
<line x1="308.0" y1="150.0" x2="315.0" y2="150.0" stroke="#cbd5e1" stroke-width="1.2"/>
|
||||
<text x="320.0" y="154.0" font-size="12" fill="#0f172a" font-weight="normal" text-anchor="start">遥信量</text>
|
||||
<text x="366.0" y="153.5" font-size="9" fill="#94a3b8" font-weight="normal" text-anchor="start">yx</text>
|
||||
<line x1="476.0" y1="168.0" x2="483.0" y2="168.0" stroke="#cbd5e1" stroke-width="1.2"/>
|
||||
<text x="488.0" y="172.0" font-size="12" fill="#0f172a" font-weight="normal" text-anchor="start">参数</text>
|
||||
<text x="522.0" y="171.5" font-size="9" fill="#94a3b8" font-weight="normal" text-anchor="start">view_param</text>
|
||||
<text x="583.2" y="171.5" font-size="9" fill="#b45309" font-weight="bold" text-anchor="start">P</text>
|
||||
<line x1="476.0" y1="186.0" x2="483.0" y2="186.0" stroke="#cbd5e1" stroke-width="1.2"/>
|
||||
<text x="488.0" y="190.0" font-size="12" fill="#0f172a" font-weight="normal" text-anchor="start">定值</text>
|
||||
<text x="522.0" y="189.5" font-size="9" fill="#94a3b8" font-weight="normal" text-anchor="start">view_set</text>
|
||||
<text x="572.8" y="189.5" font-size="9" fill="#b45309" font-weight="bold" text-anchor="start">P</text>
|
||||
<line x1="476.0" y1="204.0" x2="483.0" y2="204.0" stroke="#cbd5e1" stroke-width="1.2"/>
|
||||
<text x="488.0" y="208.0" font-size="12" fill="#0f172a" font-weight="normal" text-anchor="start">控制字</text>
|
||||
<text x="534.0" y="207.5" font-size="9" fill="#94a3b8" font-weight="normal" text-anchor="start">view_word</text>
|
||||
<text x="590.0" y="207.5" font-size="9" fill="#b45309" font-weight="bold" text-anchor="start">P</text>
|
||||
<line x1="476.0" y1="222.0" x2="483.0" y2="222.0" stroke="#cbd5e1" stroke-width="1.2"/>
|
||||
<text x="488.0" y="226.0" font-size="12" fill="#0f172a" font-weight="normal" text-anchor="start">软压板</text>
|
||||
<text x="534.0" y="225.5" font-size="9" fill="#94a3b8" font-weight="normal" text-anchor="start">view_plate</text>
|
||||
<text x="595.2" y="225.5" font-size="9" fill="#b45309" font-weight="bold" text-anchor="start">P</text>
|
||||
<line x1="476.0" y1="240.0" x2="483.0" y2="240.0" stroke="#cbd5e1" stroke-width="1.2"/>
|
||||
<text x="488.0" y="244.0" font-size="12" fill="#0f172a" font-weight="normal" text-anchor="start">参数</text>
|
||||
<text x="522.0" y="243.5" font-size="9" fill="#94a3b8" font-weight="normal" text-anchor="start">view_param</text>
|
||||
<line x1="476.0" y1="258.0" x2="483.0" y2="258.0" stroke="#cbd5e1" stroke-width="1.2"/>
|
||||
<text x="488.0" y="262.0" font-size="12" fill="#0f172a" font-weight="normal" text-anchor="start">定值</text>
|
||||
<text x="522.0" y="261.5" font-size="9" fill="#94a3b8" font-weight="normal" text-anchor="start">view_set</text>
|
||||
<line x1="476.0" y1="276.0" x2="483.0" y2="276.0" stroke="#cbd5e1" stroke-width="1.2"/>
|
||||
<text x="488.0" y="280.0" font-size="12" fill="#0f172a" font-weight="normal" text-anchor="start">控制字</text>
|
||||
<text x="534.0" y="279.5" font-size="9" fill="#94a3b8" font-weight="normal" text-anchor="start">view_word</text>
|
||||
<line x1="476.0" y1="294.0" x2="483.0" y2="294.0" stroke="#cbd5e1" stroke-width="1.2"/>
|
||||
<text x="488.0" y="298.0" font-size="12" fill="#0f172a" font-weight="normal" text-anchor="start">软压板</text>
|
||||
<text x="534.0" y="297.5" font-size="9" fill="#94a3b8" font-weight="normal" text-anchor="start">view_plate</text>
|
||||
<line x1="308.0" y1="312.0" x2="315.0" y2="312.0" stroke="#cbd5e1" stroke-width="1.2"/>
|
||||
<text x="320.0" y="316.0" font-size="12" fill="#0f172a" font-weight="normal" text-anchor="start">遥测死区</text>
|
||||
<text x="378.0" y="315.5" font-size="9" fill="#94a3b8" font-weight="normal" text-anchor="start">deadband</text>
|
||||
<text x="428.8" y="315.5" font-size="9" fill="#b45309" font-weight="bold" text-anchor="start">P</text>
|
||||
<line x1="308.0" y1="330.0" x2="315.0" y2="330.0" stroke="#cbd5e1" stroke-width="1.2"/>
|
||||
<text x="320.0" y="334.0" font-size="12" fill="#0f172a" font-weight="normal" text-anchor="start">遥测系数</text>
|
||||
<text x="378.0" y="333.5" font-size="9" fill="#94a3b8" font-weight="normal" text-anchor="start">calib</text>
|
||||
<text x="413.1" y="333.5" font-size="9" fill="#b45309" font-weight="bold" text-anchor="start">P</text>
|
||||
<line x1="308.0" y1="348.0" x2="315.0" y2="348.0" stroke="#be123c" stroke-width="1.2" stroke-dasharray="3,2"/>
|
||||
<text x="320.0" y="352.0" font-size="12" fill="#be123c" font-weight="normal" text-anchor="start">遥信类型</text>
|
||||
<text x="377.0" y="351.5" font-size="9" fill="#b45309" font-weight="bold" text-anchor="start">P</text>
|
||||
<line x1="308.0" y1="366.0" x2="315.0" y2="366.0" stroke="#be123c" stroke-width="1.2" stroke-dasharray="3,2"/>
|
||||
<text x="320.0" y="370.0" font-size="12" fill="#be123c" font-weight="normal" text-anchor="start">遥信防抖</text>
|
||||
<text x="377.0" y="369.5" font-size="9" fill="#b45309" font-weight="bold" text-anchor="start">P</text>
|
||||
<line x1="308.0" y1="384.0" x2="315.0" y2="384.0" stroke="#be123c" stroke-width="1.2" stroke-dasharray="3,2"/>
|
||||
<text x="320.0" y="388.0" font-size="12" fill="#be123c" font-weight="normal" text-anchor="start">双点遥信</text>
|
||||
<text x="377.0" y="387.5" font-size="9" fill="#b45309" font-weight="bold" text-anchor="start">P</text>
|
||||
<line x1="308.0" y1="402.0" x2="315.0" y2="402.0" stroke="#cbd5e1" stroke-width="1.2"/>
|
||||
<text x="320.0" y="406.0" font-size="12" fill="#0f172a" font-weight="normal" text-anchor="start">时钟设置</text>
|
||||
<text x="378.0" y="405.5" font-size="9" fill="#94a3b8" font-weight="normal" text-anchor="start">clock</text>
|
||||
<text x="413.1" y="405.5" font-size="9" fill="#b45309" font-weight="bold" text-anchor="start">P</text>
|
||||
<line x1="308.0" y1="420.0" x2="315.0" y2="420.0" stroke="#cbd5e1" stroke-width="1.2"/>
|
||||
<text x="320.0" y="424.0" font-size="12" fill="#0f172a" font-weight="normal" text-anchor="start">强制复归</text>
|
||||
<text x="378.0" y="423.5" font-size="9" fill="#94a3b8" font-weight="normal" text-anchor="start">reset</text>
|
||||
<text x="413.1" y="423.5" font-size="9" fill="#b45309" font-weight="bold" text-anchor="start">P</text>
|
||||
<rect x="307.0" y="429.5" width="71.0" height="17.0" rx="4" fill="#f0fdf4" stroke="#15803d" stroke-width="0.9"/>
|
||||
<line x1="308.0" y1="438.0" x2="315.0" y2="438.0" stroke="#cbd5e1" stroke-width="1.2"/>
|
||||
<text x="320.0" y="442.0" font-size="12" fill="#15803d" font-weight="normal" text-anchor="start">手动录波</text>
|
||||
<text x="378.0" y="441.5" font-size="9" fill="#94a3b8" font-weight="normal" text-anchor="start">wave_manual</text>
|
||||
<text x="444.4" y="441.5" font-size="9" fill="#b45309" font-weight="bold" text-anchor="start">P</text>
|
||||
<line x1="308.0" y1="456.0" x2="315.0" y2="456.0" stroke="#cbd5e1" stroke-width="1.2"/>
|
||||
<text x="320.0" y="460.0" font-size="12" fill="#0f172a" font-weight="normal" text-anchor="start">清除记录</text>
|
||||
<text x="378.0" y="459.5" font-size="9" fill="#94a3b8" font-weight="normal" text-anchor="start">clear</text>
|
||||
<text x="413.1" y="459.5" font-size="9" fill="#b45309" font-weight="bold" text-anchor="start">P</text>
|
||||
<line x1="308.0" y1="474.0" x2="315.0" y2="474.0" stroke="#cbd5e1" stroke-width="1.2"/>
|
||||
<text x="320.0" y="478.0" font-size="12" fill="#0f172a" font-weight="normal" text-anchor="start">通讯设置</text>
|
||||
<text x="378.0" y="477.5" font-size="9" fill="#94a3b8" font-weight="normal" text-anchor="start">comm</text>
|
||||
<text x="407.9" y="477.5" font-size="9" fill="#b45309" font-weight="bold" text-anchor="start">P</text>
|
||||
<line x1="308.0" y1="492.0" x2="315.0" y2="492.0" stroke="#cbd5e1" stroke-width="1.2"/>
|
||||
<text x="320.0" y="496.0" font-size="12" fill="#0f172a" font-weight="normal" text-anchor="start">网口设置</text>
|
||||
<text x="378.0" y="495.5" font-size="9" fill="#94a3b8" font-weight="normal" text-anchor="start">net_cfg</text>
|
||||
<text x="423.5" y="495.5" font-size="9" fill="#b45309" font-weight="bold" text-anchor="start">P</text>
|
||||
<line x1="308.0" y1="510.0" x2="315.0" y2="510.0" stroke="#be123c" stroke-width="1.2" stroke-dasharray="3,2"/>
|
||||
<text x="320.0" y="514.0" font-size="12" fill="#be123c" font-weight="normal" text-anchor="start">SNTP设置</text>
|
||||
<text x="380.8" y="513.5" font-size="9" fill="#b45309" font-weight="bold" text-anchor="start">P</text>
|
||||
<line x1="308.0" y1="528.0" x2="315.0" y2="528.0" stroke="#cbd5e1" stroke-width="1.2"/>
|
||||
<text x="320.0" y="532.0" font-size="12" fill="#0f172a" font-weight="normal" text-anchor="start">SOE记录</text>
|
||||
<text x="374.9" y="531.5" font-size="9" fill="#94a3b8" font-weight="normal" text-anchor="start">soe</text>
|
||||
<line x1="308.0" y1="546.0" x2="315.0" y2="546.0" stroke="#cbd5e1" stroke-width="1.2"/>
|
||||
<text x="320.0" y="550.0" font-size="12" fill="#0f172a" font-weight="normal" text-anchor="start">事故记录</text>
|
||||
<text x="378.0" y="549.5" font-size="9" fill="#94a3b8" font-weight="normal" text-anchor="start">accident</text>
|
||||
<line x1="308.0" y1="564.0" x2="315.0" y2="564.0" stroke="#cbd5e1" stroke-width="1.2"/>
|
||||
<text x="320.0" y="568.0" font-size="12" fill="#0f172a" font-weight="normal" text-anchor="start">操作记录</text>
|
||||
<text x="378.0" y="567.5" font-size="9" fill="#94a3b8" font-weight="normal" text-anchor="start">oper</text>
|
||||
<line x1="308.0" y1="582.0" x2="315.0" y2="582.0" stroke="#cbd5e1" stroke-width="1.2"/>
|
||||
<text x="320.0" y="586.0" font-size="12" fill="#0f172a" font-weight="normal" text-anchor="start">保护告警</text>
|
||||
<text x="378.0" y="585.5" font-size="9" fill="#94a3b8" font-weight="normal" text-anchor="start">alarm</text>
|
||||
<line x1="308.0" y1="600.0" x2="315.0" y2="600.0" stroke="#cbd5e1" stroke-width="1.2"/>
|
||||
<text x="320.0" y="604.0" font-size="12" fill="#0f172a" font-weight="normal" text-anchor="start">保护启动</text>
|
||||
<text x="378.0" y="603.5" font-size="9" fill="#94a3b8" font-weight="normal" text-anchor="start">start</text>
|
||||
<line x1="308.0" y1="618.0" x2="315.0" y2="618.0" stroke="#cbd5e1" stroke-width="1.2"/>
|
||||
<text x="320.0" y="622.0" font-size="12" fill="#0f172a" font-weight="normal" text-anchor="start">遥控记录</text>
|
||||
<text x="378.0" y="621.5" font-size="9" fill="#94a3b8" font-weight="normal" text-anchor="start">yk</text>
|
||||
<line x1="308.0" y1="636.0" x2="315.0" y2="636.0" stroke="#cbd5e1" stroke-width="1.2"/>
|
||||
<text x="320.0" y="640.0" font-size="12" fill="#0f172a" font-weight="normal" text-anchor="start">自检记录</text>
|
||||
<text x="378.0" y="639.5" font-size="9" fill="#94a3b8" font-weight="normal" text-anchor="start">selftest</text>
|
||||
<line x1="308.0" y1="654.0" x2="315.0" y2="654.0" stroke="#cbd5e1" stroke-width="1.2"/>
|
||||
<text x="320.0" y="658.0" font-size="12" fill="#0f172a" font-weight="normal" text-anchor="start">运行记录</text>
|
||||
<text x="378.0" y="657.5" font-size="9" fill="#94a3b8" font-weight="normal" text-anchor="start">runlog</text>
|
||||
<line x1="308.0" y1="672.0" x2="315.0" y2="672.0" stroke="#cbd5e1" stroke-width="1.2"/>
|
||||
<text x="320.0" y="676.0" font-size="12" fill="#0f172a" font-weight="normal" text-anchor="start">运行报告</text>
|
||||
<text x="378.0" y="675.5" font-size="9" fill="#94a3b8" font-weight="normal" text-anchor="start">runreport</text>
|
||||
<rect x="307.0" y="681.5" width="71.0" height="17.0" rx="4" fill="#f0fdf4" stroke="#15803d" stroke-width="0.9"/>
|
||||
<line x1="308.0" y1="690.0" x2="315.0" y2="690.0" stroke="#cbd5e1" stroke-width="1.2"/>
|
||||
<text x="320.0" y="694.0" font-size="12" fill="#15803d" font-weight="normal" text-anchor="start">录波回放</text>
|
||||
<text x="378.0" y="693.5" font-size="9" fill="#94a3b8" font-weight="normal" text-anchor="start">wave_play</text>
|
||||
<rect x="624.0" y="72.0" width="350.0" height="413.0" rx="8" fill="#ffffff" stroke="#cbd5e1" stroke-width="1.2"/>
|
||||
<text x="640.0" y="102.0" font-size="12.5" fill="#0f172a" font-weight="bold" text-anchor="start">本图读数</text>
|
||||
<text x="640.0" y="125.0" font-size="10.5" fill="#475569" font-weight="normal" text-anchor="start">节点 43 = 菜单 9 + 叶 34</text>
|
||||
<text x="640.0" y="143.0" font-size="10.5" fill="#475569" font-weight="normal" text-anchor="start">已绑页 30 · 未绑页 4 · 密码门 16 叶</text>
|
||||
<text x="640.0" y="161.0" font-size="10.5" fill="#475569" font-weight="normal" text-anchor="start">最深 4 级(主界面 → 叶)</text>
|
||||
<text x="640.0" y="193.0" font-size="12.5" fill="#0f172a" font-weight="bold" text-anchor="start">怎么看</text>
|
||||
<rect x="640.0" y="206.0" width="15.0" height="12.0" rx="3" fill="#eff6ff" stroke="#1d4ed8" stroke-width="1.0"/>
|
||||
<text x="662.0" y="216.0" font-size="10.5" fill="#475569" font-weight="normal" text-anchor="start">根:主界面本身,不是一个页面</text>
|
||||
<rect x="640.0" y="224.0" width="15.0" height="12.0" rx="3" fill="#ffffff" stroke="#334155" stroke-width="1.0"/>
|
||||
<text x="662.0" y="234.0" font-size="10.5" fill="#475569" font-weight="normal" text-anchor="start">白框:一级菜单,可展开、不绑页</text>
|
||||
<rect x="640.0" y="242.0" width="15.0" height="12.0" rx="3" fill="#f5f3ff" stroke="#6d28d9" stroke-width="1.0"/>
|
||||
<text x="662.0" y="252.0" font-size="10.5" fill="#475569" font-weight="normal" text-anchor="start">紫底:二级菜单与它那一支的行带</text>
|
||||
<rect x="640.0" y="260.0" width="15.0" height="12.0" rx="3" fill="#f0fdf4" stroke="#15803d" stroke-width="1.0"/>
|
||||
<text x="662.0" y="270.0" font-size="10.5" fill="#475569" font-weight="normal" text-anchor="start">绿底:录波两叶,已绑页面模块</text>
|
||||
<rect x="640.0" y="278.0" width="15.0" height="12.0" rx="3" fill="#ffffff" stroke="#be123c" stroke-width="1.0"/>
|
||||
<line x1="640.0" y1="284.0" x2="655.0" y2="284.0" stroke="#be123c" stroke-width="1.0" stroke-dasharray="3,2"/>
|
||||
<text x="662.0" y="288.0" font-size="10.5" fill="#475569" font-weight="normal" text-anchor="start">红字 + 虚线:未绑页的叶,进去只有菜单</text>
|
||||
<text x="640.0" y="306.0" font-size="10.5" fill="#475569" font-weight="normal" text-anchor="start">灰色小字 = 页面模块名(页面注册表的键)</text>
|
||||
<text x="640.0" y="324.0" font-size="10.5" fill="#475569" font-weight="normal" text-anchor="start">行末的 P = 进这一叶要先过密码</text>
|
||||
<text x="640.0" y="342.0" font-size="10.5" fill="#475569" font-weight="normal" text-anchor="start">整定 与 查看 复用同一批页面模块</text>
|
||||
<text x="640.0" y="374.0" font-size="12.5" fill="#0f172a" font-weight="bold" text-anchor="start">未绑页的 4 叶</text>
|
||||
<text x="640.0" y="397.0" font-size="10.5" fill="#475569" font-weight="normal" text-anchor="start">· 遥信类型(三遥设置 下)</text>
|
||||
<text x="640.0" y="415.0" font-size="10.5" fill="#475569" font-weight="normal" text-anchor="start">· 遥信防抖(三遥设置 下)</text>
|
||||
<text x="640.0" y="433.0" font-size="10.5" fill="#475569" font-weight="normal" text-anchor="start">· 双点遥信(三遥设置 下)</text>
|
||||
<text x="640.0" y="451.0" font-size="10.5" fill="#475569" font-weight="normal" text-anchor="start">· SNTP设置(通讯参数 下)</text>
|
||||
<text x="28.0" y="750.0" font-size="9.5" fill="#94a3b8" font-weight="normal" text-anchor="start">真值 = ui_menu_tree.txt(tools/menu_gen/gen_menu.py 的输入);本图由 tools/arch_diagram/gen_menu_tree_svg.py 生成 —— 改菜单树要同步脚本内的 NODE_SRC 重跑,勿手改 SVG</text>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 24 KiB |
@@ -1804,10 +1804,9 @@ pre-commit run --all-files
|
||||
|
||||
### 8.2 菜单树(43 节点)
|
||||
|
||||

|
||||

|
||||
|
||||
> 该图仍是 42 节点时代所画(2026-10-29 新增的「录波回放」叶未进图):`tools/arch_diagram/` 里没有这张图的生成器,
|
||||
> 重画要另开一笔并走 `generated-fresh`;**本节下面的文字与代码生成物是真值**。
|
||||
> 图里的节点数、绑页与密码门读数由 `tools/arch_diagram/gen_menu_tree_svg.py` 现算自真值 `ui_menu_tree.txt`(改菜单树 → 同步脚本内 `NODE_SRC` 重跑);级联框在屏上怎么摆(几何)→ `docs/UI设计/菜单页面设计.md` §4.1。
|
||||
|
||||
**规模**:菜单节点 **9** 个(含根)+ 叶子节点 **34** 个 = **43** 节点;最大深度 **4 级(主界面 / 一级 / 二级 / 三级菜单)**。
|
||||
**绑定现状**(2026-10-29 复扫):**已绑内容页 30 叶 / 未绑 4 叶**(页面注册表 `UI_PAGE_MAP_COUNT` = 30,
|
||||
|
||||
@@ -54,6 +54,22 @@
|
||||
| `gen_data_trust_ladder_svg.py` | `docs/images/数据可信台阶.svg` | `STEPS` 五级台阶(状态 / 问题 / 依据)+ `BAND_LABEL_1` / `GATE_LABEL` / `FOOTNOTE`(台阶状态变了才重画) |
|
||||
| `gen_data_trust_dc_ac_svg.py` | `docs/images/ADC直流标定与交流验证.svg` | `BOXES_A` / `BOXES_B` 两条泳道 + `CHIPS` 六件事 + `NOTES` 红线与"证不到"清单 |
|
||||
| `gen_wave_record_flow_svg.py` | `docs/images/故障录波数据流.svg` | `CHAIN` 主链四段 + `EDGE` 跨边界标签 + `TRAPS` 三条陷阱 + `FORBID` 四条红线(所有权、段形态或红线变了才重画) |
|
||||
| `gen_menu_tree_svg.py` | `docs/images/菜单树总览.svg` | **内嵌 `NODE_SRC`**(= `ui_menu_tree.txt` 的 43 行节点)+ 版面常量;仓库里跑会与真值逐行互验,不一致 exit 1(临时目录里读不到真值 → 用内嵌表,输出逐字节一致) |
|
||||
|
||||
### 2.1 钩子扫不到的图:refgraph 产物与历史手画稿
|
||||
|
||||
`generated-fresh` 的 `docs-svg` 条目只把 `tools/arch_diagram/gen_*.py` 拷进临时目录重跑逐字节比对。
|
||||
上表没列到的 `docs/images/*.svg` 就是下面这几张,**它们全都不在这条钩子的扫描面里**——改了不会有任何
|
||||
人报警,这就是「42 个节点的旧图挂在 43 个节点的标题下」能一路活到今天的原因。
|
||||
|
||||
| 图 | 谁画的 | 用在哪 | 维护口径 |
|
||||
|---|---|---|---|
|
||||
| `docs/images/引用关系图.svg`、`docs/images/引用关系图_模块.svg` | `tools/refgraph/gen_refgraph.py`(graphviz,不在 `gen_*.py` 命名面里) | `docs/架构设计/引用关系图说明.md` 的产品 | 改了代码引用就跑 `tools\refgraph\gen_refgraph.bat` 重生成,**svg 与改动同一笔提交**,别只更新 `.dot`;口径 → `docs/架构设计/引用关系图说明.md` |
|
||||
| `docs/images/级联框总览.svg` | 2026-10-26 手画 | `docs/UI设计/菜单页面设计.md` §4.1(级联框显示几何:根框 + 整定子框) | 只作**显示几何示意**,不承载菜单树事实;树的结构 / 层级 / 绑页 → `docs/images/菜单树总览.svg`(有生成器) |
|
||||
| `docs/images/级联框几何计算.svg` | 同上 | `docs/UI设计/菜单页面设计.md` §5.3(逐框坐标推导) | 同上;坐标公式变了要重算重画,别在 SVG 上描字 |
|
||||
|
||||
新增精确布局图一律走 `tools/arch_diagram/gen_*.py`(`docs-svg` 条目自动发现,**不必登记基线**);上面这几张
|
||||
是这套纪律的**例外面**,再多一张先说清为什么不能用生成器。
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -21,6 +21,7 @@
|
||||
| `gen_data_trust_ladder_svg.py` | 数据可信台阶图 | 采集数据可信度的五级台阶:现在踩在哪一级、下一级挡什么 |
|
||||
| `gen_data_trust_dc_ac_svg.py` | ADC 直流标定与交流验证图 | 上排直流注入链、下排信号发生器正弦验证,中间列只有正弦能证的六件事 |
|
||||
| `gen_wave_record_flow_svg.py` | 故障录波数据流图 | 录波段数据的四个所有者与四条「不许」:谁独占写、谁只能读、标志怎么过界 |
|
||||
| `gen_menu_tree_svg.py` | 菜单树总览图 | 43 节点的层级 / 绑页 / 密码门鸟瞰;内嵌 `ui_menu_tree.txt` 的节点行并与真值互验,防图与树漂移 |
|
||||
| `gen_sample_ring_why_svg.py` | 采样环缓冲_为什么需要 | 上游 1 kHz 覆盖写只交最新一帧 vs 下游要 20 点滑窗:缺口 19 点与三个消费者 |
|
||||
| `gen_sample_ring_layout_svg.py` | 采样环缓冲_环形布局 | 40 格环里哪 20 格是本窗、写指针指「下一格」、回绕与三条设计不变量 |
|
||||
| `gen_sample_ring_start_svg.py` | 采样环缓冲_窗首下标 | 窗首 = head − n 的圆周走法,与「直接减 / 16 位回绕 / 先加 DEPTH」三种写法对照 |
|
||||
|
||||
@@ -0,0 +1,458 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
"""生成 docs/images/菜单树总览.svg —— 菜单树的层级、绑页与密码门鸟瞰图。
|
||||
|
||||
数据源 = ui_menu_tree.txt(真值,tools/menu_gen/gen_menu.py 的输入)。本脚本内嵌同一张
|
||||
节点表 NODE_SRC,两种运行环境都要有确定行为:
|
||||
· 在仓库里跑:先与 ui_menu_tree.txt 的节点行逐行互验,不一致直接 exit 1 打印差异
|
||||
—— 改了菜单树必须同步内嵌表,图才会跟着变(漂移摆在台面上,而不是让 SVG 悄悄失真);
|
||||
· 在临时目录跑(pre-commit 的 generated-fresh 钩子把脚本拷出去重跑):读不到输入
|
||||
文件 → 退回内嵌表 → 输出逐字节一致。
|
||||
"""
|
||||
import html
|
||||
import pathlib
|
||||
import sys
|
||||
import xml.etree.ElementTree as ET
|
||||
|
||||
ROOT = pathlib.Path(__file__).resolve().parents[2]
|
||||
TREE_FILE = ROOT / "ui_menu_tree.txt"
|
||||
OUT = ROOT / "docs" / "images" / "菜单树总览.svg"
|
||||
|
||||
# ui_menu_tree.txt 的节点行(缩进 2 空格 = 一层;#P = 需密码;#page=x = 已绑页面模块)。
|
||||
# 改菜单树 → 同步这张表 → 重跑本脚本 → 脚本与产物同一笔提交。
|
||||
NODE_SRC = [
|
||||
"主界面",
|
||||
" 装置信息 #page=device_info",
|
||||
" 实时数据",
|
||||
" 交流量 #page=ac",
|
||||
" 直流量 #page=dc",
|
||||
" 遥信量 #page=yx",
|
||||
" 参数定值",
|
||||
" 整定",
|
||||
" 参数 #P #page=view_param",
|
||||
" 定值 #P #page=view_set",
|
||||
" 控制字 #P #page=view_word",
|
||||
" 软压板 #P #page=view_plate",
|
||||
" 查看",
|
||||
" 参数 #page=view_param",
|
||||
" 定值 #page=view_set",
|
||||
" 控制字 #page=view_word",
|
||||
" 软压板 #page=view_plate",
|
||||
" 三遥设置",
|
||||
" 遥测死区 #P #page=deadband",
|
||||
" 遥测系数 #P #page=calib",
|
||||
" 遥信类型 #P",
|
||||
" 遥信防抖 #P",
|
||||
" 双点遥信 #P",
|
||||
" 装置维护",
|
||||
" 时钟设置 #P #page=clock",
|
||||
" 强制复归 #P #page=reset",
|
||||
" 手动录波 #P #page=wave_manual",
|
||||
" 清除记录 #P #page=clear",
|
||||
" 通讯参数",
|
||||
" 通讯设置 #P #page=comm",
|
||||
" 网口设置 #P #page=net_cfg",
|
||||
" SNTP设置 #P",
|
||||
" 记录查询",
|
||||
" SOE记录 #page=soe",
|
||||
" 事故记录 #page=accident",
|
||||
" 操作记录 #page=oper",
|
||||
" 保护告警 #page=alarm",
|
||||
" 保护启动 #page=start",
|
||||
" 遥控记录 #page=yk",
|
||||
" 自检记录 #page=selftest",
|
||||
" 运行记录 #page=runlog",
|
||||
" 运行报告 #page=runreport",
|
||||
" 录波回放 #page=wave_play",
|
||||
]
|
||||
|
||||
# ---- 版面 ----
|
||||
FS_LEAF = 12
|
||||
FS_MENU = 12
|
||||
FS_ROOT = 13
|
||||
FS_PAGE = 9
|
||||
PITCH = 18
|
||||
ROW_TOP = 96
|
||||
X_COL = (28, 150, 306, 474)
|
||||
TRUNK_OFF = 22
|
||||
LEAF_TEXT = 14
|
||||
PAD_X = 9
|
||||
BAND_X = X_COL[1] - 12
|
||||
BAND_R = 624
|
||||
PANEL_X = 640
|
||||
W = 990
|
||||
TREE_LIMIT = 630
|
||||
|
||||
INK = "#0f172a"
|
||||
MUTED = "#475569"
|
||||
FAINT = "#94a3b8"
|
||||
LINE = "#334155"
|
||||
RULE = "#cbd5e1"
|
||||
C_BLUE = "#1d4ed8"
|
||||
BG_BLUE = "#eff6ff"
|
||||
C_VIOLET = "#6d28d9"
|
||||
BG_VIOLET = "#f5f3ff"
|
||||
C_AMBER = "#b45309"
|
||||
C_ROSE = "#be123c"
|
||||
C_GREEN = "#15803d"
|
||||
BG_GREEN = "#f0fdf4"
|
||||
BG_GRAY = "#f8fafc"
|
||||
FONT = "Microsoft YaHei, Segoe UI, PingFang SC, sans-serif"
|
||||
|
||||
_over = []
|
||||
|
||||
|
||||
def emit(msg):
|
||||
"""控制台可能是 GBK:中文/符号打印不能把生成器打崩(假红 = 钩子重跑失败)。"""
|
||||
enc = getattr(sys.stdout, "encoding", None) or "utf-8"
|
||||
safe = str(msg).encode(enc, errors="replace").decode(enc, errors="replace")
|
||||
sys.stdout.write(safe + "\n")
|
||||
|
||||
|
||||
def tw(text, fs):
|
||||
"""近似字宽:CJK 约 1.0 em,其余 0.58 em。"""
|
||||
return sum(fs * (1.0 if ord(c) > 0x2E80 else 0.58) for c in str(text))
|
||||
|
||||
|
||||
def esc(text):
|
||||
return html.escape(str(text), quote=True)
|
||||
|
||||
|
||||
def fit(x_right, limit, what):
|
||||
if x_right > limit:
|
||||
_over.append("%s: %.0f > %.0f" % (what, x_right, limit))
|
||||
|
||||
|
||||
def node_lines(raw):
|
||||
"""取节点行:丢空行与 # 指令行(指令改了不影响图的形状)。"""
|
||||
out = []
|
||||
for ln in raw.splitlines():
|
||||
ln = ln.rstrip("\r")
|
||||
if not ln.strip() or ln.strip().startswith("#"):
|
||||
continue
|
||||
out.append(ln)
|
||||
return out
|
||||
|
||||
|
||||
def verify_src():
|
||||
"""仓库里跑 = 与真值互验;临时目录里跑 = 静默用内嵌表。"""
|
||||
if not TREE_FILE.exists():
|
||||
emit("note: 未找到 ui_menu_tree.txt(临时目录重跑场景)→ 用内嵌 NODE_SRC")
|
||||
return 0
|
||||
live = node_lines(TREE_FILE.read_text(encoding="utf-8-sig"))
|
||||
if live == list(NODE_SRC):
|
||||
emit("互验 OK:%d 行节点与 ui_menu_tree.txt 一致" % len(live))
|
||||
return 0
|
||||
emit("[FAIL] ui_menu_tree.txt 与本脚本内嵌 NODE_SRC 不一致 —— 图会失真,请同步内嵌表后重跑")
|
||||
for i in range(max(len(live), len(NODE_SRC))):
|
||||
a = live[i] if i < len(live) else "<缺>"
|
||||
b = NODE_SRC[i] if i < len(NODE_SRC) else "<缺>"
|
||||
if a != b:
|
||||
emit(" 第 %d 行:真值 %r / 内嵌 %r" % (i + 1, a, b))
|
||||
return 1
|
||||
|
||||
|
||||
def parse(lines):
|
||||
"""节点行 → (根, 文档序节点表)。"""
|
||||
root = None
|
||||
docs = []
|
||||
stack = []
|
||||
for ln in lines:
|
||||
depth = (len(ln) - len(ln.lstrip(" "))) // 2
|
||||
page = None
|
||||
pwd = False
|
||||
keep = []
|
||||
for tok in ln.strip().split():
|
||||
if tok == "#P":
|
||||
pwd = True
|
||||
elif tok.startswith("#page="):
|
||||
page = tok[len("#page="):]
|
||||
else:
|
||||
keep.append(tok)
|
||||
node = {"depth": depth, "label": " ".join(keep), "page": page,
|
||||
"pwd": pwd, "kids": [], "y": 0.0, "w": 0.0}
|
||||
docs.append(node)
|
||||
while stack and stack[-1]["depth"] >= depth:
|
||||
stack.pop()
|
||||
if stack:
|
||||
stack[-1]["kids"].append(node)
|
||||
else:
|
||||
root = node
|
||||
stack.append(node)
|
||||
return root, docs
|
||||
|
||||
|
||||
def layout(root):
|
||||
"""叶按文档序占行;菜单 y = 首末子节点 y 的中点。返回叶行数。"""
|
||||
state = [0]
|
||||
|
||||
def walk(n):
|
||||
if not n["kids"]:
|
||||
n["y"] = ROW_TOP + state[0] * PITCH
|
||||
state[0] += 1
|
||||
return
|
||||
for k in n["kids"]:
|
||||
walk(k)
|
||||
n["y"] = (n["kids"][0]["y"] + n["kids"][-1]["y"]) / 2.0
|
||||
|
||||
walk(root)
|
||||
return state[0]
|
||||
|
||||
|
||||
def yspan(n):
|
||||
"""子树占用的 y 区间。"""
|
||||
if not n["kids"]:
|
||||
return n["y"], n["y"]
|
||||
lo, _ = yspan(n["kids"][0])
|
||||
_, hi = yspan(n["kids"][-1])
|
||||
return lo, hi
|
||||
|
||||
|
||||
def tally(docs):
|
||||
menus = [n for n in docs if n["kids"]]
|
||||
leaves = [n for n in docs if not n["kids"]]
|
||||
unbound = [n for n in leaves if not n["page"]]
|
||||
return {"total": len(docs), "menus": len(menus), "leaves": len(leaves),
|
||||
"bound": len(leaves) - len(unbound), "unbound": unbound,
|
||||
"pwd": len([n for n in leaves if n["pwd"]]),
|
||||
"depth": max(n["depth"] for n in docs) + 1}
|
||||
|
||||
|
||||
QA = chr(34) # 双引号:拼 SVG 属性值用,免得在字面量里嵌引号
|
||||
|
||||
|
||||
def r1(v):
|
||||
"""一位小数(决定输出逐字节稳定)。"""
|
||||
return format(float(v), ".1f")
|
||||
|
||||
|
||||
def attrs(pairs):
|
||||
out = []
|
||||
for key, val in pairs:
|
||||
out.append(key + "=" + QA + str(val) + QA)
|
||||
return " ".join(out)
|
||||
|
||||
|
||||
def txt(x, y, body, fs, fill, weight="normal", anchor="start"):
|
||||
head = attrs([("x", r1(x)), ("y", r1(y)), ("font-size", fs), ("fill", fill),
|
||||
("font-weight", weight), ("text-anchor", anchor)])
|
||||
return "<text " + head + ">" + esc(body) + "</text>"
|
||||
|
||||
|
||||
def rrect(x, y, w, h, fill, stroke, rx=5, sw=1.2):
|
||||
return "<rect " + attrs([("x", r1(x)), ("y", r1(y)), ("width", r1(w)),
|
||||
("height", r1(h)), ("rx", rx), ("fill", fill),
|
||||
("stroke", stroke), ("stroke-width", sw)]) + "/>"
|
||||
|
||||
|
||||
def line(x1, y1, x2, y2, stroke, sw=1.2, dash=None):
|
||||
pairs = [("x1", r1(x1)), ("y1", r1(y1)), ("x2", r1(x2)), ("y2", r1(y2)),
|
||||
("stroke", stroke), ("stroke-width", sw)]
|
||||
if dash:
|
||||
pairs.append(("stroke-dasharray", dash))
|
||||
return "<line " + attrs(pairs) + "/>"
|
||||
|
||||
|
||||
def svg_header(h):
|
||||
return ["<?xml version=" + QA + "1.0" + QA + " encoding=" + QA + "UTF-8" + QA + "?>",
|
||||
"<svg " + attrs([("xmlns", "http://www.w3.org/2000/svg"),
|
||||
("width", W), ("height", h),
|
||||
("viewBox", "0 0 " + str(W) + " " + str(h)),
|
||||
("font-family", FONT)]) + ">",
|
||||
"<rect " + attrs([("width", W), ("height", h), ("fill", "#ffffff")]) + "/>"]
|
||||
|
||||
|
||||
def draw_tree(docs, s):
|
||||
"""分组带 -> 连接线 -> 方框与叶(后画的压在前面之上)。"""
|
||||
band = 0
|
||||
for n in docs:
|
||||
if n["depth"] != 1:
|
||||
continue
|
||||
lo, hi = yspan(n)
|
||||
fill = BG_GRAY if band % 2 == 0 else "#ffffff"
|
||||
s.append(rrect(BAND_X, lo - 9, BAND_R - BAND_X, (hi - lo) + 18, fill,
|
||||
"none", 6, 0))
|
||||
band += 1
|
||||
for n in docs:
|
||||
if n["depth"] == 2 and n["kids"]:
|
||||
lo, hi = yspan(n)
|
||||
s.append(rrect(X_COL[2] - 10, lo - 9, BAND_R - (X_COL[2] - 10),
|
||||
(hi - lo) + 18, BG_VIOLET, "none", 5, 0))
|
||||
for n in docs:
|
||||
if not n["kids"]:
|
||||
continue
|
||||
d = n["depth"]
|
||||
tx = X_COL[d + 1] - TRUNK_OFF
|
||||
s.append(line(tx, n["kids"][0]["y"], tx, n["kids"][-1]["y"], RULE))
|
||||
s.append(line(X_COL[d] + n["w"], n["y"], tx, n["y"], RULE))
|
||||
for k in n["kids"]:
|
||||
kx = X_COL[k["depth"]] if k["kids"] else X_COL[k["depth"]] + 3
|
||||
s.append(line(tx, k["y"], kx, k["y"], RULE))
|
||||
for n in docs:
|
||||
if n["kids"]:
|
||||
d = n["depth"]
|
||||
fs = FS_ROOT if d == 0 else FS_MENU
|
||||
if d == 0:
|
||||
ink, fill, stroke, bh = C_BLUE, BG_BLUE, C_BLUE, 26
|
||||
elif d == 1:
|
||||
ink, fill, stroke, bh = INK, "#ffffff", LINE, 22
|
||||
else:
|
||||
ink, fill, stroke, bh = C_VIOLET, "#ffffff", C_VIOLET, 20
|
||||
s.append(rrect(X_COL[d], n["y"] - bh / 2.0, n["w"], bh, fill, stroke, 5))
|
||||
s.append(txt(X_COL[d] + n["w"] / 2.0, n["y"] + fs * 0.36, n["label"],
|
||||
fs, ink, "bold", "middle"))
|
||||
if d + 1 < len(X_COL):
|
||||
fit(X_COL[d] + n["w"], X_COL[d + 1] - TRUNK_OFF - 4,
|
||||
"menu " + n["label"])
|
||||
for n in docs:
|
||||
if n["kids"]:
|
||||
continue
|
||||
x = X_COL[n["depth"]]
|
||||
y = n["y"]
|
||||
wave = n["label"] in ("手动录波", "录波回放")
|
||||
bound = bool(n["page"])
|
||||
lw = tw(n["label"], FS_LEAF)
|
||||
if wave:
|
||||
s.append(rrect(x + 1, y - 8.5, LEAF_TEXT + lw + 9, 17, BG_GREEN,
|
||||
C_GREEN, 4, 0.9))
|
||||
if bound:
|
||||
s.append(line(x + 2, y, x + 9, y, RULE))
|
||||
else:
|
||||
s.append(line(x + 2, y, x + 9, y, C_ROSE, 1.2, "3,2"))
|
||||
ink = INK
|
||||
if not bound:
|
||||
ink = C_ROSE
|
||||
elif wave:
|
||||
ink = C_GREEN
|
||||
s.append(txt(x + LEAF_TEXT, y + 4, n["label"], FS_LEAF, ink))
|
||||
cur = x + LEAF_TEXT + lw
|
||||
if bound:
|
||||
s.append(txt(cur + 10, y + 3.5, n["page"], FS_PAGE, FAINT))
|
||||
cur += 10 + tw(n["page"], FS_PAGE)
|
||||
if n["pwd"]:
|
||||
s.append(txt(cur + 9, y + 3.5, "P", 9, C_AMBER, "bold"))
|
||||
cur += 10 + tw("P", 9)
|
||||
fit(cur, TREE_LIMIT, "leaf " + n["label"])
|
||||
|
||||
|
||||
def panel_rows(c):
|
||||
rows = [{"k": "h", "t": "本图读数"}]
|
||||
rows.append({"k": "t", "t": "节点 %d = 菜单 %d + 叶 %d"
|
||||
% (c["total"], c["menus"], c["leaves"])})
|
||||
rows.append({"k": "t", "t": "已绑页 %d · 未绑页 %d · 密码门 %d 叶"
|
||||
% (c["bound"], len(c["unbound"]), c["pwd"])})
|
||||
rows.append({"k": "t", "t": "最深 %d 级(主界面 → 叶)" % c["depth"]})
|
||||
rows.append({"k": "s", "t": ""})
|
||||
rows.append({"k": "h", "t": "怎么看"})
|
||||
rows.append({"k": "sw", "fill": BG_BLUE, "stroke": C_BLUE,
|
||||
"t": "根:主界面本身,不是一个页面"})
|
||||
rows.append({"k": "sw", "fill": "#ffffff", "stroke": LINE,
|
||||
"t": "白框:一级菜单,可展开、不绑页"})
|
||||
rows.append({"k": "sw", "fill": BG_VIOLET, "stroke": C_VIOLET,
|
||||
"t": "紫底:二级菜单与它那一支的行带"})
|
||||
rows.append({"k": "sw", "fill": BG_GREEN, "stroke": C_GREEN,
|
||||
"t": "绿底:录波两叶,已绑页面模块"})
|
||||
rows.append({"k": "sw", "fill": "#ffffff", "stroke": C_ROSE, "dash": "3,2",
|
||||
"t": "红字 + 虚线:未绑页的叶,进去只有菜单"})
|
||||
rows.append({"k": "t", "t": "灰色小字 = 页面模块名(页面注册表的键)"})
|
||||
rows.append({"k": "t", "t": "行末的 P = 进这一叶要先过密码"})
|
||||
rows.append({"k": "t", "t": "整定 与 查看 复用同一批页面模块"})
|
||||
rows.append({"k": "s", "t": ""})
|
||||
rows.append({"k": "h", "t": "未绑页的 %d 叶" % len(c["unbound"])})
|
||||
for n in c["unbound"]:
|
||||
par = n["parent"]["label"] if n["parent"] else "?"
|
||||
rows.append({"k": "t", "t": "· " + n["label"] + "(" + par + " 下)"})
|
||||
return rows
|
||||
|
||||
|
||||
def draw_panel(c, s):
|
||||
"""右侧读数与图例:数字全部现算,文档里不必再抄一份。"""
|
||||
rows = panel_rows(c)
|
||||
x = PANEL_X
|
||||
pw = W - 30 - x
|
||||
lh = 18.0
|
||||
hh = 25.0
|
||||
gap = 12.0
|
||||
body_h = 0.0
|
||||
for r in rows:
|
||||
if r["k"] == "h":
|
||||
body_h += hh
|
||||
elif r["k"] == "s":
|
||||
body_h += gap
|
||||
else:
|
||||
body_h += lh
|
||||
y0 = ROW_TOP - 24
|
||||
s.append(rrect(x - 16, y0, pw + 30, body_h + 44, "#ffffff", RULE, 8))
|
||||
y = y0 + 26
|
||||
for r in rows:
|
||||
if r["k"] == "s":
|
||||
y += gap
|
||||
continue
|
||||
if r["k"] == "h":
|
||||
s.append(txt(x, y + 4, r["t"], 12.5, INK, "bold"))
|
||||
fit(x + tw(r["t"], 12.5), W - 40, "panel " + r["t"])
|
||||
y += hh
|
||||
continue
|
||||
tx = x
|
||||
if r["k"] == "sw":
|
||||
sw = r.get("dash")
|
||||
s.append(rrect(x, y - 8, 15, 12, r["fill"], r["stroke"], 3, 1.0))
|
||||
if sw:
|
||||
s.append(line(x, y - 2, x + 15, y - 2, r["stroke"], 1.0, sw))
|
||||
tx = x + 22
|
||||
s.append(txt(tx, y + 2, r["t"], 10.5, MUTED))
|
||||
fit(tx + tw(r["t"], 10.5), W - 40, "panel " + r["t"][:8])
|
||||
y += lh
|
||||
|
||||
|
||||
def main():
|
||||
if verify_src():
|
||||
return 1
|
||||
root, docs = parse(NODE_SRC)
|
||||
for n in docs:
|
||||
n["parent"] = None
|
||||
for n in docs:
|
||||
for k in n["kids"]:
|
||||
k["parent"] = n
|
||||
leaf_rows = layout(root)
|
||||
for n in docs:
|
||||
if n["kids"]:
|
||||
fs = FS_ROOT if n["depth"] == 0 else FS_MENU
|
||||
n["w"] = 2 * PAD_X + tw(n["label"], fs)
|
||||
c = tally(docs)
|
||||
h = ROW_TOP + leaf_rows * PITCH + 76
|
||||
s = svg_header(h)
|
||||
s.append(txt(28, 40, "菜单树总览(层级 · 绑页 · 密码门)", 17, INK, "bold"))
|
||||
s.append(txt(28, 61, "方框 = 菜单(可展开);细线 = 叶(对应一个页面模块,或一处空缺)",
|
||||
11, MUTED))
|
||||
draw_tree(docs, s)
|
||||
draw_panel(c, s)
|
||||
foot = ROW_TOP + leaf_rows * PITCH + 42
|
||||
note = ("真值 = ui_menu_tree.txt(tools/menu_gen/gen_menu.py 的输入);本图由 "
|
||||
"tools/arch_diagram/gen_menu_tree_svg.py 生成 —— 改菜单树要同步脚本内的 "
|
||||
"NODE_SRC 重跑,勿手改 SVG")
|
||||
s.append(txt(28, foot, note, 9.5, FAINT))
|
||||
fit(28 + tw(note, 9.5), W - 20, "foot")
|
||||
s.append("</svg>")
|
||||
body = "\n".join(s) + "\n"
|
||||
OUT.parent.mkdir(parents=True, exist_ok=True)
|
||||
OUT.write_bytes(body.encode("utf-8"))
|
||||
try:
|
||||
ET.fromstring(body)
|
||||
except ET.ParseError as exc:
|
||||
emit("XML FAIL: %s" % exc)
|
||||
return 1
|
||||
emit("SVG %dx%d 节点 %d 菜单 %d 叶 %d 绑页 %d 未绑 %d 密码门 %d 最深 %d 级"
|
||||
% (W, h, c["total"], c["menus"], c["leaves"], c["bound"],
|
||||
len(c["unbound"]), c["pwd"], c["depth"]))
|
||||
if _over:
|
||||
for item in _over:
|
||||
emit("OVERFLOW " + item)
|
||||
emit("[FAIL] 版面溢出 %d 处 —— 调版面常量后重跑" % len(_over))
|
||||
return 1
|
||||
emit("OVERFLOW 0 → " + str(OUT.relative_to(ROOT)))
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
Reference in New Issue
Block a user