docs(采集可信): 数据可信文档台阶化重构并立项故障录波
两份工作互锁(互相引用),故同一笔提交。 数据可信:全文按「五级台阶」重排(§1 全局 → §2 信度现状 → §3 M4 退出判据逐条 → §4 设计约束 → §5 任务卡 → §6 顺序 → §7 决策 → §8 风险 → §9 边界 → §10 关系)。 恢复 §2.2 量纲错配(该节此前被瘦身删掉,test/host/prot_dim 的两处引用一直悬空); 新增 §2.7 与 §4.6:ADC 只验过直流静态标定,正弦动态波形一次也没采过——用信号发生 器在既有注入点注 ±2 V 以内的正弦即可验全周傅氏、测频、快照与削顶路由,不必往端子 加压、也不需要标准源。§6 手绘框图改 Mermaid。章节编号未重排:§2 / §4 / §7 / §10 被 App 与 test 的头注释当规范引用。 故障录波:用户裁定 2026-10-29 录波先行,三条口径——1 kHz(每周波 20 点)、8 路 电气量全录、一屏只画一路共 8 路可切换。新增设计说明(与产品需求 80 点/周波口径的 偏离显式登记;存原始码值而非工程量,绕开未标定闭锁与测频幅值门;四个所有者的跨 任务边界与三条并发陷阱;段文件字节布局;单通道回放显示契约;约 5.9 KB 静态增量 触发 R6)与卡片登记簿(数据面最小闭环 → 手动与保护启动触发 → 现场单通道回放页, 二期的 4 kHz、突变量启动、COMTRADE、104 文件传输不返工的结构约束写进设计文档)。 两张旧配图与一张新配图改由 tools/arch_diagram/ 的生成器产出,脚本与产物同笔提交。 代码一行未动:本波只交付文档与设计。
This commit is contained in:
@@ -0,0 +1,324 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
"""生成 docs/images/ADC直流标定与交流验证.svg —— 上下两条泳道讲清"同一个针脚、两种证法":
|
||||
上排 = 已做完的直流静态标定(一条水平线的高度),下排 = 本文新增的交流正弦验证
|
||||
(整条时间轴的形状与节拍),底部 = 只有正弦能证的六件事 + 安全红线(纯标准库)。
|
||||
|
||||
为什么不用 Mermaid:本图要的是"两条流水线一一对齐 + 波形示意",Mermaid 画不出
|
||||
20 点/周的正弦采样点,也无法把上下一一对齐。谁挡谁的顺序拓扑仍是 Mermaid,
|
||||
见 docs/任务规划/数据可信.md §6。
|
||||
|
||||
用法(仓库根目录,任意 python3):
|
||||
python tools/arch_diagram/gen_data_trust_dc_ac_svg.py
|
||||
输出:
|
||||
docs/images/ADC直流标定与交流验证.svg
|
||||
改内容:链路环节 / 六件事 / 红线变了才重画;调坐标编辑下方常量后重跑。
|
||||
"""
|
||||
import html
|
||||
import math
|
||||
import os
|
||||
import sys
|
||||
import xml.etree.ElementTree as ET
|
||||
|
||||
|
||||
def emit(s):
|
||||
"""控制台打印按目标编码替换,绝不因 GBK 终端遇上 ²/− 之类字符而崩
|
||||
(generated-fresh 钩子会把本脚本拷进临时目录重跑,崩了就是一片假红)。"""
|
||||
enc = getattr(sys.stdout, "encoding", None) or "ascii"
|
||||
sys.stdout.write(s.encode(enc, "replace").decode(enc, "replace") + "\n")
|
||||
|
||||
ROOT = os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
||||
OUT = os.path.join(ROOT, "docs", "images", "ADC直流标定与交流验证.svg")
|
||||
|
||||
# ----------------------------- 画布 -----------------------------
|
||||
W = 1060
|
||||
H = 818
|
||||
PAD = 46
|
||||
RIGHT = W - PAD
|
||||
INNER = RIGHT - PAD
|
||||
|
||||
FONT = "Microsoft YaHei, Segoe UI, PingFang SC, sans-serif"
|
||||
TITLE_FONT = 21
|
||||
SUB_FONT = 13
|
||||
BOX_TITLE = 12.5
|
||||
BOX_DESC = 10.5
|
||||
CHIP_TITLE = 12.0
|
||||
CHIP_DESC = 10.0
|
||||
SMALL = 11.0
|
||||
TINY = 10.5
|
||||
|
||||
INK = "#0f172a"
|
||||
LINE = "#334155"
|
||||
MUTED = "#475569"
|
||||
FAINT = "#94a3b8"
|
||||
RULE = "#cbd5e1"
|
||||
WHITE = "#ffffff"
|
||||
|
||||
C_GREEN = "#16a34a"
|
||||
BG_GREEN = "#f0fdf4"
|
||||
BD_GREEN = "#bbf7d0"
|
||||
C_ROSE = "#e11d48"
|
||||
BG_ROSE = "#fff1f2"
|
||||
BD_ROSE = "#fecdd3"
|
||||
C_BLUE = "#2563eb"
|
||||
BG_BLUE = "#eff6ff"
|
||||
C_AMBER = "#d97706"
|
||||
BG_AMBER = "#fffbeb"
|
||||
|
||||
# ----------------------------- 数据(改这里)-----------------------------
|
||||
TITLE = "同一个针脚,两种证法 —— 直流只证「高度」,正弦才证「形状与节拍」"
|
||||
SUB1 = ("上排已做完(2026-10-27 静态注入六项判据全 PASS);下排一次也没做过:正弦波从没进过这条链,"
|
||||
"所以后面的相量、测频、削顶、谐波对不对,现在无人能答")
|
||||
SUB2 = ("两排共用同一个注入点、同一套前端、同一份折算代码 —— 差别只在信号源是「平的」还是「会走 20 点/周的正弦」")
|
||||
|
||||
LANE_A_LABEL = "上排 · 直流静态标定(已做)"
|
||||
LANE_A_GLYPH = "flat"
|
||||
LANE_B_LABEL = "下排 · 交流正弦验证(本文新增 · 待做)"
|
||||
LANE_B_GLYPH = "sine"
|
||||
|
||||
# 每条泳道 6 个环节:(标题, 说明 1, 说明 2)
|
||||
BOXES_A = [
|
||||
("直流源", "电池或台式电源", "+ 电位器分压"),
|
||||
("万用表", "同时接在注入点", "当唯一基准读数"),
|
||||
("注入点 R31", "网名 AIN1 = U5.A3", "= ADC7 引脚(1:1)"),
|
||||
("前端 + 转换器", "同一条电阻网络", "TPAFE 16b、ADC1 12b"),
|
||||
("折算", "adc_cal_apply()", "单位仍是引脚 V"),
|
||||
("三点线性拟合", "0 / 0.5 / 1.0 V", "R² = 0.9999997,斜率差 −0.14 %"),
|
||||
]
|
||||
BOXES_B = [
|
||||
("信号发生器", "50 Hz 正弦", "幅度按峰值算,见底部红线"),
|
||||
("注入点 R31", "同上排同一个点", "外串 10 k、与板共地"),
|
||||
("前端 + 转换器", "同上排同一条链", "STM32 五路负半周必削顶"),
|
||||
("1 kHz 采样点", "TIM3 TRGO 同时触发", "50 Hz → 20 点/整周"),
|
||||
("全周傅氏", "phasor_run():√2/N 预乘", "直接得有效值与相位"),
|
||||
("读数出口", "meas 200 ms → 104 YC 0x4011", "幅值 + 品质 + 频率(或 UI)"),
|
||||
]
|
||||
|
||||
CHIPS_TITLE = "只有正弦能证的六件事(直流在构造上就证不到)"
|
||||
CHIPS = [
|
||||
("整周窗对不对", "窗长差 1 点,相位误差就跟着来;直流恒 0 相位,测不出"),
|
||||
("有效值是否依赖波形", "直流只有一个点:方波 / 三角 / 正弦算出的都是同一个数"),
|
||||
("相位", "φ = atan2(fIm, fRe) 全靠正弦才有意义"),
|
||||
("测频 freq_run", "共轭乘积取相位增量;直流相位不前进 → 恒无效"),
|
||||
("削顶路由 CLIPPED", "UB/UC/IB/IC/IN 单极性 0~3.3 V,负半周必削顶 → 该报 IV 就得报 IV"),
|
||||
("谐波实例 C04 / C05", "二次 / 五次相量已建但无消费者:发生器给含谐波信号才能验"),
|
||||
]
|
||||
|
||||
NOTES = [
|
||||
("安全红线(照抄 ADC通道映射说明 §7)", C_ROSE, BG_ROSE, [
|
||||
"TPAFE 侧只到 ±2 V:正弦看峰值不只看有效值 —— 1 Vrms 的峰值已是 1.41 V,再高就顶到 ±5 V 档边缘;",
|
||||
"STM32 那五路严禁超过 3.3 V(单极性,超了是硬件级风险,不是削顶那一路好笑的验证);",
|
||||
"发生器与板必须共地(不然量到的是地环差);引脚侧注入一律外串 10 k;不许拿端子侧上额定电压走捷径。"]),
|
||||
("这台发生器证不到的三件事", C_AMBER, BG_AMBER, [
|
||||
"④ 端子变比:注入点在子板之前,算出来的还是引脚域 —— 变比要靠原理图或万用表实测(另一张卡);",
|
||||
"精度等级验收:发生器不是标准源,它的幅值误差本身就要被校,不能拿它冒充送检依据;",
|
||||
"跨转换器相位:UB/UC 与 UA 分属两片转换器、无共同采样触发(V0.1 硬件缺陷),任何相位比较都不作数。"]),
|
||||
]
|
||||
|
||||
FOOTNOTE = ("读数出口现在就有:104 YC(test/host/iec104_master.py,浮点无量化)+ 串口每 10 s 的 phasor(hb) / freq(hb) / meas: 行;"
|
||||
"但板上没有「把窗里 20 个点导出来」的出口,正弦只能靠派生标量证,而 phasor 行是 ×0.1 定点(注 1 V 只剩 10 格)。")
|
||||
|
||||
# ----------------------------- 布局 -----------------------------
|
||||
LANE_TOP_A = 108
|
||||
LANE_H = 160
|
||||
LANE_TOP_B = LANE_TOP_A + LANE_H + 18
|
||||
BOX_W = 136
|
||||
BOX_H = 88
|
||||
GAP = (INNER - 6 * BOX_W) / 5.0
|
||||
LANE_LAB_H = 26
|
||||
BOX_Y_OFF = LANE_LAB_H + 34
|
||||
|
||||
CHIPS_TOP = LANE_TOP_B + LANE_H + 20
|
||||
CHIP_W = (INNER - 5 * 12) / 6.0
|
||||
CHIP_H = 74
|
||||
|
||||
NOTE_TOP = CHIPS_TOP + CHIP_H + 18
|
||||
NOTE_W = (INNER - 16) / 2.0
|
||||
NOTE_H = 128
|
||||
|
||||
# ----------------------------- 工具 -----------------------------
|
||||
OVERFLOW = []
|
||||
|
||||
|
||||
def esc(s):
|
||||
return html.escape(s, quote=True)
|
||||
|
||||
|
||||
def text_w(s, fs):
|
||||
"""粗估像素宽:CJK / 全角约 1.0em,其余约 0.58em(宁可估大)。"""
|
||||
return sum(fs * (1.0 if ord(ch) >= 0x2E80 else 0.58) for ch in s)
|
||||
|
||||
|
||||
def check_fit(s, fs, avail, where):
|
||||
if text_w(s, fs) > avail:
|
||||
OVERFLOW.append("%s: %.0f > %.0f [%s]" % (where, text_w(s, fs), avail, s))
|
||||
|
||||
|
||||
def txt(x, y, s, fs, fill, weight=400, anchor=None):
|
||||
a = "" if anchor is None else ' text-anchor="%s"' % anchor
|
||||
w = "" if weight == 400 else ' font-weight="%d"' % weight
|
||||
return ('<text x="%.1f" y="%.1f" font-size="%g"%s%s fill="%s">%s</text>'
|
||||
% (x, y, fs, a, w, fill, esc(s)))
|
||||
|
||||
|
||||
def line(x1, y1, x2, y2, color=LINE, width=2.0, dash=None):
|
||||
d = "" if dash is None else ' stroke-dasharray="%s"' % dash
|
||||
return ('<line x1="%.1f" y1="%.1f" x2="%.1f" y2="%.1f" stroke="%s" stroke-width="%g"%s/>'
|
||||
% (x1, y1, x2, y2, color, width, d))
|
||||
|
||||
|
||||
def polyline(pts, color, width=1.8, dash=None):
|
||||
d = "" if dash is None else ' stroke-dasharray="%s"' % dash
|
||||
p = " ".join("%.1f,%.1f" % (a, b) for a, b in pts)
|
||||
return '<polyline points="%s" fill="none" stroke="%s" stroke-width="%g"%s stroke-linejoin="round"/>' % (
|
||||
p, color, width, d)
|
||||
|
||||
|
||||
def rect(x, y, w, h, fill, stroke=None, rx=8, sw=1.4):
|
||||
s = "" if stroke is None else ' stroke="%s" stroke-width="%g"' % (stroke, sw)
|
||||
return '<rect x="%.1f" y="%.1f" width="%.1f" height="%.1f" rx="%g" fill="%s"%s/>' % (
|
||||
x, y, w, h, rx, fill, s)
|
||||
|
||||
|
||||
def arrow(x, y, color):
|
||||
"""向右的短箭头,画在两个环节之间。"""
|
||||
return ('<path d="M %.1f %.1f L %.1f %.1f L %.1f %.1f Z" fill="%s"/>'
|
||||
% (x, y - 4, x + 9, y, x, y + 4, color))
|
||||
|
||||
|
||||
def glyph(x, y, w, h, kind, color):
|
||||
"""泳道左侧的波形示意:flat = 一条水平线加三个取样点;sine = 正弦加 20 个采样点。"""
|
||||
out = [rect(x, y, w, h, WHITE, color, rx=6, sw=1.2)]
|
||||
mid = y + h / 2.0
|
||||
if kind == "flat":
|
||||
out.append(line(x + 6, mid - 8, x + w - 6, mid - 8, color, 2.0))
|
||||
for i in range(3):
|
||||
cx = x + 12 + i * (w - 24) / 2.0
|
||||
out.append('<circle cx="%.1f" cy="%.1f" r="2.8" fill="%s"/>' % (cx, mid - 8, color))
|
||||
else:
|
||||
pts = []
|
||||
n = 60
|
||||
for i in range(n + 1):
|
||||
t = i / float(n)
|
||||
px = x + 6 + t * (w - 12)
|
||||
py = mid - (h / 2.0 - 8) * math.sin(t * 2.0 * math.pi)
|
||||
pts.append((px, py))
|
||||
out.append(line(x + 4, mid, x + w - 4, mid, RULE, 1.0, dash="3 3"))
|
||||
out.append(polyline(pts, color, 1.9))
|
||||
for i in range(20):
|
||||
t = i / 20.0
|
||||
px = x + 6 + t * (w - 12)
|
||||
py = mid - (h / 2.0 - 8) * math.sin(t * 2.0 * math.pi)
|
||||
out.append('<circle cx="%.1f" cy="%.1f" r="2.0" fill="%s"/>' % (px, py, color))
|
||||
return "\n".join(out)
|
||||
|
||||
|
||||
# ----------------------------- 生成 -----------------------------
|
||||
def build():
|
||||
out = []
|
||||
add = out.append
|
||||
|
||||
for s, fs, where in ((TITLE, TITLE_FONT, "title"), (SUB1, SUB_FONT, "sub1"),
|
||||
(SUB2, SUB_FONT, "sub2"), (CHIPS_TITLE, BOX_TITLE, "chips title")):
|
||||
check_fit(s, fs, INNER, where)
|
||||
|
||||
add('<rect x="0" y="0" width="%d" height="%d" fill="%s"/>' % (W, H, WHITE))
|
||||
|
||||
# 标题
|
||||
add(txt(PAD, 42, TITLE, TITLE_FONT, INK, weight=700))
|
||||
for i, s in enumerate((SUB1, SUB2)):
|
||||
add(txt(PAD, 68 + i * 19, s, SUB_FONT, MUTED))
|
||||
|
||||
# 两条泳道
|
||||
for lane_i, (top, label, boxes, color, bg, bd, glyph_kind) in enumerate((
|
||||
(LANE_TOP_A, LANE_A_LABEL, BOXES_A, C_GREEN, BG_GREEN, BD_GREEN, LANE_A_GLYPH),
|
||||
(LANE_TOP_B, LANE_B_LABEL, BOXES_B, C_ROSE, BG_ROSE, BD_ROSE, LANE_B_GLYPH))):
|
||||
add(rect(PAD - 12, top, INNER + 24, LANE_H, bg, bd, rx=10, sw=1.2))
|
||||
add(txt(PAD, top + 20, label, BOX_TITLE, color, weight=700))
|
||||
add(glyph(RIGHT - 92, top + 8, 88, 34, glyph_kind, color))
|
||||
for i, (bt, d1, d2) in enumerate(boxes):
|
||||
bx = PAD + i * (BOX_W + GAP)
|
||||
by = top + BOX_Y_OFF
|
||||
add(rect(bx, by, BOX_W, BOX_H, WHITE, color, rx=8, sw=1.6))
|
||||
check_fit(bt, BOX_TITLE, BOX_W - 14, "lane%d box%d title" % (lane_i, i))
|
||||
add(txt(bx + 7, by + 18, bt, BOX_TITLE, INK, weight=700))
|
||||
yy = by + 36
|
||||
for j, d in enumerate((d1, d2)):
|
||||
for ln in wrap(d, BOX_DESC, BOX_W - 14, 2, "lane%d box%d desc%d" % (lane_i, i, j)):
|
||||
add(txt(bx + 7, yy, ln, BOX_DESC, MUTED))
|
||||
yy += 14
|
||||
if i < 5:
|
||||
add(arrow(bx + BOX_W + GAP / 2.0 - 5, by + BOX_H / 2.0, LINE))
|
||||
add(line(bx + BOX_W + 2, by + BOX_H / 2.0, bx + BOX_W + GAP / 2.0 - 3,
|
||||
by + BOX_H / 2.0, LINE, 1.6))
|
||||
|
||||
# 六件事
|
||||
add(txt(PAD, CHIPS_TOP - 10, CHIPS_TITLE, BOX_TITLE, C_BLUE, weight=700))
|
||||
for i, (t, d) in enumerate(CHIPS):
|
||||
cx = PAD + i * (CHIP_W + 12)
|
||||
add(rect(cx, CHIPS_TOP, CHIP_W, CHIP_H, BG_BLUE, None, rx=8))
|
||||
add(line(cx, CHIPS_TOP, cx, CHIPS_TOP + CHIP_H, C_BLUE, 3.0))
|
||||
check_fit(t, CHIP_TITLE, CHIP_W - 16, "chip%d title" % i)
|
||||
add(txt(cx + 10, CHIPS_TOP + 20, t, CHIP_TITLE, INK, weight=700))
|
||||
for j, ln in enumerate(wrap(d, CHIP_DESC, CHIP_W - 18, 4, "chip%d desc" % i)):
|
||||
add(txt(cx + 10, CHIPS_TOP + 38 + j * 14, ln, CHIP_DESC, MUTED))
|
||||
|
||||
# 两个说明块
|
||||
for i, (title, color, bg, rows) in enumerate(NOTES):
|
||||
nx = PAD + i * (NOTE_W + 16)
|
||||
add(rect(nx, NOTE_TOP, NOTE_W, NOTE_H, bg, color, rx=10, sw=1.6))
|
||||
check_fit(title, BOX_TITLE, NOTE_W - 20, "note%d title" % i)
|
||||
add(txt(nx + 14, NOTE_TOP + 24, title, BOX_TITLE, color, weight=700))
|
||||
y = NOTE_TOP + 46
|
||||
for r in rows:
|
||||
for j, ln in enumerate(wrap(r, SMALL, NOTE_W - 28, 2, "note%d row" % i)):
|
||||
add(txt(nx + 14, y, ln, SMALL, INK if j == 0 else MUTED))
|
||||
y += 15
|
||||
y += 4
|
||||
|
||||
# 脚注
|
||||
fy = NOTE_TOP + NOTE_H + 26
|
||||
add(line(PAD, fy - 12, RIGHT, fy - 12, RULE, 1.0))
|
||||
for i, ln in enumerate(wrap(FOOTNOTE, SUB_FONT, INNER, 2, "foot")):
|
||||
add(txt(PAD, fy + 10 + i * 19, ln, SUB_FONT, MUTED))
|
||||
|
||||
svg = ('<svg xmlns="http://www.w3.org/2000/svg" width="%d" height="%d" '
|
||||
'viewBox="0 0 %d %d" font-family="%s">\n%s\n</svg>\n'
|
||||
% (W, H, W, H, FONT, "\n".join(out)))
|
||||
|
||||
d = os.path.dirname(OUT)
|
||||
if not os.path.isdir(d):
|
||||
os.makedirs(d)
|
||||
with open(OUT, "w", encoding="utf-8", newline="\n") as fp:
|
||||
fp.write(svg)
|
||||
ET.fromstring(svg)
|
||||
emit("OK %d bytes, canvas %dx%d -> %s" % (len(svg.encode("utf-8")), W, H, OUT))
|
||||
if OVERFLOW:
|
||||
for o in OVERFLOW:
|
||||
emit("OVERFLOW WARN " + o)
|
||||
emit("!! %d overflow warnings" % len(OVERFLOW))
|
||||
|
||||
|
||||
def wrap(s, fs, avail, max_lines, where=""):
|
||||
"""按估算宽度折行(CJK 逐字可断);超出 max_lines 用省略号收尾。"""
|
||||
if text_w(s, fs) <= avail:
|
||||
return [s]
|
||||
lines = []
|
||||
cur = ""
|
||||
for ch in s:
|
||||
if text_w(cur + ch, fs) > avail:
|
||||
lines.append(cur)
|
||||
cur = ch
|
||||
else:
|
||||
cur += ch
|
||||
if cur:
|
||||
lines.append(cur)
|
||||
if len(lines) > max_lines:
|
||||
OVERFLOW.append("%s: 折行 %d > %d" % (where, len(lines), max_lines))
|
||||
lines = lines[:max_lines]
|
||||
lines[max_lines - 1] = lines[max_lines - 1][:-1] + "…"
|
||||
return lines
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
build()
|
||||
@@ -0,0 +1,250 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
"""生成 docs/images/数据可信台阶.svg —— 「数据可信」的五级台阶:现在踩在第 2 级,
|
||||
第 3 级(动态正弦波形)用手边设备就能跨(纯标准库)。
|
||||
|
||||
为什么不用 Mermaid:本图要读的是"台阶的高度差 + 每一级踩没踩上去"——Mermaid 会把
|
||||
五级压成同一高度,"还差一级"这件事就看不出来了。谁挡谁的顺序拓扑另有一张 Mermaid
|
||||
图,在 docs/任务规划/数据可信.md §6(本图刻意不重复那张的语义)。
|
||||
|
||||
用法(仓库根目录,任意 python3):
|
||||
python tools/arch_diagram/gen_data_trust_ladder_svg.py
|
||||
输出:
|
||||
docs/images/数据可信台阶.svg
|
||||
改内容:台阶事实(状态 / 判据 / 资源)变了才重画;调坐标 / 文案编辑下方常量后重跑。
|
||||
"""
|
||||
import html
|
||||
import os
|
||||
import sys
|
||||
import xml.etree.ElementTree as ET
|
||||
|
||||
|
||||
def emit(s):
|
||||
"""控制台打印按目标编码替换,绝不因 GBK 终端遇上 ²/− 之类字符而崩
|
||||
(generated-fresh 钩子会把本脚本拷进临时目录重跑,崩了就是一片假红)。"""
|
||||
enc = getattr(sys.stdout, "encoding", None) or "ascii"
|
||||
sys.stdout.write(s.encode(enc, "replace").decode(enc, "replace") + "\n")
|
||||
|
||||
ROOT = os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
||||
OUT = os.path.join(ROOT, "docs", "images", "数据可信台阶.svg")
|
||||
|
||||
# ----------------------------- 画布与尺寸 -----------------------------
|
||||
W = 1060
|
||||
H = 712
|
||||
PAD = 46
|
||||
RIGHT = W - PAD
|
||||
|
||||
FONT = "Microsoft YaHei, Segoe UI, PingFang SC, sans-serif"
|
||||
|
||||
TITLE_FONT = 21
|
||||
SUB_FONT = 13
|
||||
HEAD_FONT = 14.5
|
||||
Q_FONT = 12.5
|
||||
HOW_FONT = 11.5
|
||||
TINY_FONT = 11
|
||||
|
||||
INK = "#0f172a"
|
||||
LINE = "#334155"
|
||||
MUTED = "#64748b"
|
||||
FAINT = "#94a3b8"
|
||||
RULE = "#cbd5e1"
|
||||
WHITE = "#ffffff"
|
||||
|
||||
C_GREEN = "#16a34a"
|
||||
BG_GREEN = "#f0fdf4"
|
||||
C_AMBER = "#d97706"
|
||||
BG_AMBER = "#fffbeb"
|
||||
C_ROSE = "#e11d48"
|
||||
BG_ROSE = "#fff1f2"
|
||||
C_BLUE = "#2563eb"
|
||||
BG_BLUE = "#eff6ff"
|
||||
|
||||
# ----------------------------- 数据(改这里) -----------------------------
|
||||
TITLE = "「数据可信」是五级台阶 —— 现在踩在第 2 级"
|
||||
SUB1 = ("每升一级只多答一个问题:通路活 → 静态读数对 → 动态波形对 → 换算到端子 → 精度达标;"
|
||||
"台阶的高度差 = 下游能多信多少")
|
||||
SUB2 = ("第 3 级不要标准源、也不要往端子加压:一台信号发生器 + 已有的注入点就能做"
|
||||
"(判据见 docs/任务规划/数据可信.md §4.6)")
|
||||
|
||||
# 每列:(编号, 台阶名, 状态, 主色, 底色, 这一级多答的问题, 用什么证 / 什么挡路)
|
||||
STEPS = [
|
||||
("1", "通路活", "已证 · 2026-10-27", C_GREEN, BG_GREEN,
|
||||
"9 路都在转换?没串道?极性没反?",
|
||||
"开机自检 + 帧计数仍 1000 Hz;逐路注入只动本路"),
|
||||
("2", "静态引脚电压", "已证 · 2026-10-27", C_GREEN, BG_GREEN,
|
||||
"注 1 V 直流,读数是不是 1 V?",
|
||||
"万用表 + 电位器三点线性:斜率 6544.2 vs 理论 6553.6 码/V,差 −0.14 %"),
|
||||
("3", "动态引脚波形", "待做 · 本文新增", C_ROSE, BG_ROSE,
|
||||
"注 1 V 正弦,算出来还是不是 1 V?",
|
||||
"注正弦验全周傅氏幅值 / 相位 / 测频 / 削顶 / 谐波抑制。"
|
||||
"挡路:FREQ_AMP_MIN = 50 V 在引脚域恒不过 → 测频只能读到 IV"),
|
||||
("4", "端子二次值", "等外部资源", C_AMBER, BG_AMBER,
|
||||
"引脚上这 1 V 对应端子侧几 V?",
|
||||
"④ 子板 / 端子板变比:仓内无原理图,须拿图纸或万用表实测"),
|
||||
("5", "精度等级验收", "等外部资源", C_AMBER, BG_AMBER,
|
||||
"额定工况下整条链误差够几级?",
|
||||
"标准源两点标定写 [adc_cal] 置 VALID → 保护自动解卡 → 送检"),
|
||||
]
|
||||
|
||||
BAND_LABEL_1 = "M4 承诺边界(§2.6):蓝底这三列都还在「ADC 引脚」这一侧,eng = 0、单位是引脚电压 V"
|
||||
GATE_LABEL = "eng 升档门:④ 变比到手 + 电气 8 路 VALID 全置位 → eng 才升 1"
|
||||
FOOTNOTE = ("第 2、3 级共用同一个注入点与同一套读数出口,但证的是两件事:"
|
||||
"直流证「一条水平线的高度」,正弦证「整条时间轴的形状与节拍」——第 3 级不能由第 2 级代证。")
|
||||
|
||||
# ----------------------------- 布局 -----------------------------
|
||||
STEP_W = 196
|
||||
STEP_GAP = 6
|
||||
X0 = PAD
|
||||
FIRST_H = 96 # 第 1 级台阶高度
|
||||
RISE = 64 # 每高一级加这么多
|
||||
BASE_Y = 466 # 地面线
|
||||
GATE_X = X0 + 3 * (STEP_W + STEP_GAP) - STEP_GAP - 3 # 第 3/4 列之间
|
||||
BAND_TOP = 106
|
||||
BAND_BOTTOM = BASE_Y + 172
|
||||
|
||||
|
||||
# ----------------------------- 工具函数 -----------------------------
|
||||
def esc(s):
|
||||
return html.escape(s, quote=True)
|
||||
|
||||
|
||||
def text_w(s, fs):
|
||||
"""粗略估算文本像素宽:CJK/全角 ≈ 1.0em,其余 ≈ 0.58em(宁可偏大)。"""
|
||||
return sum(fs * (1.0 if ord(ch) >= 0x2E80 else 0.58) for ch in s)
|
||||
|
||||
|
||||
OVERFLOW = []
|
||||
|
||||
|
||||
def check_fit(s, fs, avail, where):
|
||||
if text_w(s, fs) > avail:
|
||||
OVERFLOW.append("%s: %.0f > %.0f [%s]" % (where, text_w(s, fs), avail, s))
|
||||
|
||||
|
||||
def txt(x, y, s, fs, fill, weight=400, anchor=None):
|
||||
a = "" if anchor is None else ' text-anchor="%s"' % anchor
|
||||
w = "" if weight == 400 else ' font-weight="%d"' % weight
|
||||
return ('<text x="%.1f" y="%.1f" font-size="%g"%s%s fill="%s">%s</text>'
|
||||
% (x, y, fs, a, w, fill, esc(s)))
|
||||
|
||||
|
||||
def line(x1, y1, x2, y2, color=LINE, width=2.0, dash=None):
|
||||
d = "" if dash is None else ' stroke-dasharray="%s"' % dash
|
||||
return ('<line x1="%.1f" y1="%.1f" x2="%.1f" y2="%.1f" stroke="%s" stroke-width="%g"%s/>'
|
||||
% (x1, y1, x2, y2, color, width, d))
|
||||
|
||||
|
||||
def rect(x, y, w, h, fill, stroke=None, rx=8, sw=1.6):
|
||||
s = "" if stroke is None else ' stroke="%s" stroke-width="%g"' % (stroke, sw)
|
||||
return '<rect x="%.1f" y="%.1f" width="%.1f" height="%.1f" rx="%g" fill="%s"%s/>' % (
|
||||
x, y, w, h, rx, fill, s)
|
||||
|
||||
|
||||
def wrap(s, fs, avail, max_lines, where=""):
|
||||
"""按估算宽度折行(CJK 逐字可断)。超出 max_lines 用省略号收尾——
|
||||
宁可提示文案太长,也不让字溢出盒子。"""
|
||||
if text_w(s, fs) <= avail:
|
||||
return [s]
|
||||
lines = []
|
||||
cur = ""
|
||||
for ch in s:
|
||||
if text_w(cur + ch, fs) > avail:
|
||||
lines.append(cur)
|
||||
cur = ch
|
||||
else:
|
||||
cur += ch
|
||||
if cur:
|
||||
lines.append(cur)
|
||||
if len(lines) > max_lines:
|
||||
OVERFLOW.append("%s: 折行 %d > %d" % (where, len(lines), max_lines))
|
||||
lines = lines[:max_lines]
|
||||
lines[max_lines - 1] = lines[max_lines - 1][:-1] + "…"
|
||||
return lines
|
||||
|
||||
|
||||
# ----------------------------- 生成 -----------------------------
|
||||
def build():
|
||||
out = []
|
||||
add = out.append
|
||||
|
||||
avail = RIGHT - PAD
|
||||
for s, fs, where in ((TITLE, TITLE_FONT, "title"), (SUB1, SUB_FONT, "sub1"),
|
||||
(SUB2, SUB_FONT, "sub2")):
|
||||
check_fit(s, fs, avail, where)
|
||||
if OVERFLOW:
|
||||
for o in OVERFLOW:
|
||||
emit("OVERFLOW WARN " + o)
|
||||
|
||||
add('<rect x="0" y="0" width="%d" height="%d" fill="%s"/>' % (W, H, WHITE))
|
||||
|
||||
# ---- 标题区 ----
|
||||
add(txt(PAD, 42, TITLE, TITLE_FONT, INK, weight=700))
|
||||
add(txt(PAD, 68, SUB1, SUB_FONT, MUTED))
|
||||
add(txt(PAD, 88, SUB2, SUB_FONT, MUTED))
|
||||
|
||||
# ---- 「引脚域」底衬(先画,压在台阶下面)----
|
||||
add(rect(PAD - 12, BAND_TOP, GATE_X - PAD + 3, BAND_BOTTOM - BAND_TOP, BG_BLUE, None, rx=12))
|
||||
|
||||
# ---- 台阶 ----
|
||||
inner_w = STEP_W - 20
|
||||
for i, (no, name, status, c, bg, q, how) in enumerate(STEPS):
|
||||
x = X0 + i * (STEP_W + STEP_GAP)
|
||||
h = FIRST_H + i * RISE
|
||||
top = BASE_Y - h
|
||||
add(rect(x, top, STEP_W, h, bg, c, rx=8, sw=2.0))
|
||||
add('<circle cx="%.1f" cy="%.1f" r="13" fill="%s"/>' % (x + 24, top + 26, c))
|
||||
add(txt(x + 24, top + 31, no, 13, WHITE, weight=700, anchor="middle"))
|
||||
check_fit(name, HEAD_FONT, inner_w - 30, "step %s name" % no)
|
||||
add(txt(x + 44, top + 32, name, HEAD_FONT, INK, weight=700))
|
||||
add(txt(x + 11, top + 58, status, TINY_FONT, c, weight=700))
|
||||
|
||||
# ---- 地面线 ----
|
||||
add(line(PAD, BASE_Y, RIGHT, BASE_Y, LINE, 2.2))
|
||||
|
||||
# ---- 地面下的明细(问题 + 用什么证),逐列一行 ----
|
||||
for i, (no, name, status, c, bg, q, how) in enumerate(STEPS):
|
||||
x = X0 + i * (STEP_W + STEP_GAP)
|
||||
y = BASE_Y + 24
|
||||
for ln in wrap(q, Q_FONT, inner_w, 2, "step %s q" % no):
|
||||
add(txt(x + 10, y, ln, Q_FONT, INK, weight=700))
|
||||
y += 17
|
||||
y += 3
|
||||
for ln in wrap(how, HOW_FONT, inner_w, 5, "step %s how" % no):
|
||||
add(txt(x + 10, y, ln, HOW_FONT, MUTED))
|
||||
y += 15
|
||||
|
||||
# ---- eng 升档门 ----
|
||||
add(line(GATE_X, BAND_TOP - 6, GATE_X, BAND_BOTTOM, C_ROSE, 2.0, dash="7 5"))
|
||||
gy = BAND_TOP + 12
|
||||
for ln in wrap(GATE_LABEL, TINY_FONT, STEP_W - 16, 4, "gate label"):
|
||||
add(txt(GATE_X + 8, gy, ln, TINY_FONT, C_ROSE, weight=700))
|
||||
gy += 15
|
||||
|
||||
# ---- 蓝底标签(在台阶上方的空白区)----
|
||||
bl = wrap(BAND_LABEL_1, HOW_FONT, GATE_X - PAD - 24, 2, "band label")
|
||||
for i, ln in enumerate(bl):
|
||||
add(txt(PAD, BAND_TOP + 26 + i * 17, ln, HOW_FONT, C_BLUE, weight=700))
|
||||
|
||||
# ---- 底部说明 ----
|
||||
fy = BAND_BOTTOM + 22
|
||||
add(line(PAD, fy, RIGHT, fy, RULE, 1.0))
|
||||
for i, ln in enumerate(wrap(FOOTNOTE, SUB_FONT, avail, 2, "foot wrap")):
|
||||
add(txt(PAD, fy + 24 + i * 19, ln, SUB_FONT, MUTED))
|
||||
|
||||
svg = ('<svg xmlns="http://www.w3.org/2000/svg" width="%d" height="%d" '
|
||||
'viewBox="0 0 %d %d" font-family="%s">\n%s\n</svg>\n'
|
||||
% (W, H, W, H, FONT, "\n".join(out)))
|
||||
|
||||
d = os.path.dirname(OUT)
|
||||
if not os.path.isdir(d):
|
||||
os.makedirs(d)
|
||||
with open(OUT, "w", encoding="utf-8", newline="\n") as fp:
|
||||
fp.write(svg)
|
||||
ET.fromstring(svg)
|
||||
emit("OK %d bytes, canvas %dx%d -> %s" % (len(svg.encode("utf-8")), W, H, OUT))
|
||||
if OVERFLOW:
|
||||
emit("!! %d overflow warnings" % len(OVERFLOW))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
build()
|
||||
@@ -0,0 +1,289 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
"""生成 docs/images/故障录波数据流.svg —— 录波段数据的四个所有者与四条「不许」。
|
||||
|
||||
为什么不用 Mermaid:本图要读的是**跨任务边界的所有权**——谁独占写、谁只能读、
|
||||
标志怎么过界,并把「不许」画成红线贴在对应位置。Mermaid 的拓扑图表达不了「这块
|
||||
内存归谁」;任务先后与阻塞关系另有一张 Mermaid 图,在 docs/任务规划/故障录波.md
|
||||
§3,本图刻意不重复那张的语义。
|
||||
|
||||
用法(仓库根目录,任意 python3):
|
||||
python tools/arch_diagram/gen_wave_record_flow_svg.py
|
||||
输出:
|
||||
docs/images/故障录波数据流.svg
|
||||
改内容:所有权、段形态、预算或红线变了才重画;调坐标与文案改下方常量后重跑。
|
||||
"""
|
||||
import os
|
||||
import sys
|
||||
import html
|
||||
import xml.etree.ElementTree as ET
|
||||
|
||||
def emit(s):
|
||||
"""控制台按目标编码替换输出,绝不因 GBK 终端遇上 − ² 之类字符而崩
|
||||
(generated-fresh 钩子会把本脚本拷进临时目录重跑,崩了就是一片假红)。"""
|
||||
enc = getattr(sys.stdout, "encoding", None) or "ascii"
|
||||
sys.stdout.write(s.encode(enc, "replace").decode(enc, "replace") + "\n")
|
||||
|
||||
ROOT = os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
||||
OUT = os.path.join(ROOT, "docs", "images", "故障录波数据流.svg")
|
||||
|
||||
# ----------------------------- 画布与字体 -----------------------------
|
||||
W = 1060
|
||||
H = 702
|
||||
FONT = "Microsoft YaHei, Segoe UI, PingFang SC, sans-serif"
|
||||
F_TITLE = 20
|
||||
F_SUB = 12
|
||||
F_HEAD = 12.5
|
||||
F_BODY = 10.5
|
||||
F_TINY = 10
|
||||
LH = 14.5
|
||||
|
||||
INK = "#0f172a"
|
||||
MUTED = "#475569"
|
||||
FAINT = "#64748b"
|
||||
LINE = "#334155"
|
||||
RULE = "#cbd5e1"
|
||||
WHITE = "#ffffff"
|
||||
C_BLUE = "#2563eb"
|
||||
BG_BLUE = "#eff6ff"
|
||||
C_GREEN = "#15803d"
|
||||
BG_GREEN = "#f0fdf4"
|
||||
C_VIOLET = "#6d28d9"
|
||||
BG_VIOLET = "#f5f3ff"
|
||||
C_AMBER = "#b45309"
|
||||
BG_AMBER = "#fffbeb"
|
||||
C_TEAL = "#0f766e"
|
||||
BG_TEAL = "#f0fdfa"
|
||||
C_ROSE = "#e11d48"
|
||||
BG_ROSE = "#fff1f2"
|
||||
BG_GRAY = "#f8fafc"
|
||||
|
||||
# ----------------------------- 几何 -----------------------------
|
||||
PAD = 40
|
||||
RIGHT = W - 40
|
||||
A_TOP = 104
|
||||
A_H = 118
|
||||
BOX_W = 203
|
||||
GAP = 56
|
||||
LBL_Y = 236
|
||||
B_TOP = 252
|
||||
B_H = 94
|
||||
BL_X = PAD
|
||||
BL_W = 430
|
||||
BR_X = 494
|
||||
BR_W = RIGHT - 494
|
||||
C_TOP = 356
|
||||
C_H = 136
|
||||
CHIP_W = 316
|
||||
CHIP_GAP = 16
|
||||
D_TOP = 514
|
||||
D_H = 132
|
||||
FOOT_Y = 672
|
||||
|
||||
# ----------------------------- 数据(改这里) -----------------------------
|
||||
TITLE = "录波段数据的四个所有者 —— 谁独占写、谁只能读、标志怎么过界"
|
||||
SUB1 = "写侧只有一条路:adcTask 压环 → defaultTask 落盘 → /wave 段文件;读者一律只读已封存的段。"
|
||||
SUB2 = "「已封存」是唯一的跨任务接口:没有任何人读别人的活缓冲——这是本图唯一要记住的一件事。"
|
||||
|
||||
CHAIN = [
|
||||
("触发来路:只置标志", C_AMBER, BG_AMBER, [
|
||||
"UI 手动录波操作页",
|
||||
"prot_elm 的 picked 上升沿",
|
||||
"bench 构建定周期自抓",
|
||||
"谁都只置一个原子标志",
|
||||
]),
|
||||
("adcTask:唯一写环的人", C_BLUE, BG_BLUE, [
|
||||
"每拍压 8 路 int16 原始码值",
|
||||
"专用录波环 320 点 × 8 路",
|
||||
"查标志 → 冻结 → 计满封段",
|
||||
"= 5,120 B,落 SRAM1",
|
||||
]),
|
||||
("defaultTask:文件唯一写者", C_GREEN, BG_GREEN, [
|
||||
"排空已封存区 → f_write",
|
||||
"写回字节数不符即算失败",
|
||||
"分次写 256 B,不整段驻留",
|
||||
"更新段账与最新段号",
|
||||
]),
|
||||
("/wave 段文件", C_VIOLET, BG_VIOLET, [
|
||||
"文件名 WAVE 加两位十进制",
|
||||
"头存 eng、系数与 clip_mask",
|
||||
"数据区按通道分列存放",
|
||||
"与 /log 平级,FTP 根下可见",
|
||||
]),
|
||||
]
|
||||
EDGE = ["只置标志", "已封存标志", "字节数不符即失败"]
|
||||
|
||||
LEFT_NOTE = ("采集主线照常推进(本波一行不改它)", C_TEAL, BG_TEAL, [
|
||||
"sample_ring → phasor → freq → meas → 104 / UI,全链同源",
|
||||
"冻结窗口丢的是录波样本,不是采集:相量、测频、判据、上送一律照常",
|
||||
])
|
||||
READERS = ("三个读者:一律只读已封存的段", LINE, BG_GRAY, [
|
||||
"uiTask 回放页 —— 只读窗口取一段,自己画(不写文件)",
|
||||
"FTP(netTask / lftpd)—— 只搬文件,不碰采集上下文",
|
||||
"PC 脚本 —— 重算全周傅氏,与板上 meas: 与 104 浮点对表",
|
||||
])
|
||||
|
||||
TRAPS = [
|
||||
("陷阱一 · 取窗竞态", [
|
||||
"sample_ring_window() 读 head 后逐点拷,没有临界区",
|
||||
"现在只有 adcTask 同任务调用才安全",
|
||||
"处置:录波不复用它,另立专用环;跨任务只传已封存标志",
|
||||
]),
|
||||
("陷阱二 · FatFs 追加写", [
|
||||
"该移植 FA_OPEN_APPEND 不含 FA_WRITE",
|
||||
"簇分配失败还返 FR_OK 加短写,无错误码",
|
||||
"处置:打开带 FA_WRITE,判成功必须比对写回字节数",
|
||||
]),
|
||||
("陷阱三 · 只有 8.3 短名", [
|
||||
"FF_USE_LFN=0:长名与点开头一律拒",
|
||||
"处置:段名 = WAVE 加两位十进制再加 .BIN,轮转覆盖",
|
||||
"目录 /wave 与 /log 平级,FTP 鉴权现成,零新代码",
|
||||
]),
|
||||
]
|
||||
|
||||
FORBID = [
|
||||
"不许跨任务读活的录波环或 sample_ring——没有临界区,读出来是两帧混合数据",
|
||||
"不许 protectTask 写环或落盘——它只置一个请求标志,同 prot_elm_request_reset 的模式",
|
||||
"不许 netTask 与 lftpd 读环——文件服务只搬已封存的段,不碰采集上下文",
|
||||
"不许把 U 与 I 两段叠在同一张图上比相位——跨转换器无共同采样触发,V0.1 的硬件事实",
|
||||
]
|
||||
FOOT = ("字段级布局见 docs/子系统设计/故障录波设计说明.md §6.2;预算与 R6 前置见 §7;触发源与段长冻结语义见 §4。"
|
||||
"本图由 tools/arch_diagram/gen_wave_record_flow_svg.py 生成,改图请改脚本重跑,勿手改 SVG。")
|
||||
|
||||
# ----------------------------- 排版与预检 -----------------------------
|
||||
OVERRUNS = []
|
||||
|
||||
def n(v):
|
||||
return ("%g" % (round(float(v), 2)))
|
||||
|
||||
def text_w(s, size):
|
||||
w = 0.0
|
||||
for ch in s:
|
||||
w += size * (1.0 if ord(ch) > 0x2E80 else 0.58)
|
||||
return w
|
||||
|
||||
def fit(tag, s, size, maxw):
|
||||
got = text_w(s, size)
|
||||
if got > maxw + 0.5:
|
||||
OVERRUNS.append("%s: %.0f gt %.0f %s" % (tag, got, maxw, s))
|
||||
|
||||
def wrap(s, size, maxw):
|
||||
out = []
|
||||
cur = ""
|
||||
for ch in s:
|
||||
if cur and text_w(cur + ch, size) > maxw:
|
||||
out.append(cur)
|
||||
cur = ch
|
||||
else:
|
||||
cur += ch
|
||||
if cur:
|
||||
out.append(cur)
|
||||
return out
|
||||
|
||||
def esc(s):
|
||||
return html.escape(s, quote=True)
|
||||
|
||||
E = []
|
||||
|
||||
def text(x, y, s, size, fill, weight="400", anchor="start"):
|
||||
E.append('<text x="%s" y="%s" font-size="%s" font-weight="%s" text-anchor="%s" fill="%s">%s</text>'
|
||||
% (n(x), n(y), n(size), weight, anchor, fill, esc(s)))
|
||||
|
||||
def rect(x, y, w, h, fg, bg, dash=None, rx=9):
|
||||
st = 'fill="%s" stroke="%s" stroke-width="1.2"' % (bg, fg)
|
||||
if dash:
|
||||
st += ' stroke-dasharray="%s"' % dash
|
||||
E.append('<rect x="%s" y="%s" width="%s" height="%s" rx="%s" %s/>' % (n(x), n(y), n(w), n(h), n(rx), st))
|
||||
|
||||
def box(x, y, w, h, head, lines, fg, bg, dash=None):
|
||||
rect(x, y, w, h, fg, bg, dash)
|
||||
fit("head " + head, head, F_HEAD, w - 18)
|
||||
text(x + 10, y + 21, head, F_HEAD, fg, "700")
|
||||
yy = y + 21 + 17
|
||||
for ln in lines:
|
||||
for sub in wrap(ln, F_BODY, w - 20):
|
||||
fit("body " + sub[:14], sub, F_BODY, w - 20)
|
||||
text(x + 10, yy, sub, F_BODY, INK)
|
||||
yy += LH
|
||||
if yy > y + h - 2:
|
||||
OVERRUNS.append("tall %s: need %.0f have %.0f" % (head, yy - y, h))
|
||||
|
||||
def line_seg(pts, color, dash=None, head=True):
|
||||
st = 'fill="none" stroke="%s" stroke-width="1.4"' % color
|
||||
if dash:
|
||||
st += ' stroke-dasharray="%s"' % dash
|
||||
if head:
|
||||
st += ' marker-end="url(#ah)"'
|
||||
E.append('<polyline points="%s" %s/>' % (" ".join(n(a) + "," + n(b) for a, b in pts), st))
|
||||
|
||||
def halo_text(cx, y, s, size, fill, weight="600"):
|
||||
wpx = text_w(s, size) + 8
|
||||
rect(cx - wpx / 2, y - size, wpx, size + 6, WHITE, WHITE, rx=3)
|
||||
text(cx, y, s, size, fill, weight, "middle")
|
||||
|
||||
# ----------------------------- 绘制 -----------------------------
|
||||
E.append('<rect x="0" y="0" width="%s" height="%s" fill="%s"/>' % (W, H, WHITE))
|
||||
text(PAD, 42, TITLE, F_TITLE, INK, "700")
|
||||
text(PAD, 66, SUB1, F_SUB, MUTED)
|
||||
text(PAD, 86, SUB2, F_SUB, MUTED)
|
||||
fit("sub1", SUB1, F_SUB, RIGHT - PAD)
|
||||
fit("sub2", SUB2, F_SUB, RIGHT - PAD)
|
||||
|
||||
centers = []
|
||||
for i, (head, fg, bg, lines) in enumerate(CHAIN):
|
||||
x = PAD + i * (BOX_W + GAP)
|
||||
box(x, A_TOP, BOX_W, A_H, head, lines, fg, bg)
|
||||
centers.append((x, x + BOX_W))
|
||||
|
||||
for i, lbl in enumerate(EDGE):
|
||||
x1 = centers[i][1]
|
||||
x2 = centers[i + 1][0]
|
||||
line_seg([(x1, A_TOP + A_H / 2), (x2, A_TOP + A_H / 2)], LINE)
|
||||
halo_text((x1 + x2) / 2, LBL_Y, lbl, F_TINY, MUTED)
|
||||
|
||||
bx4 = PAD + 3 * (BOX_W + GAP) + BOX_W / 2
|
||||
by = A_TOP + A_H
|
||||
line_seg([(bx4, by), (bx4, by + 15), (BR_X + 120, by + 15), (BR_X + 120, B_TOP)], LINE)
|
||||
|
||||
bx2 = PAD + 1 * (BOX_W + GAP) + BOX_W / 2
|
||||
line_seg([(bx2, by), (bx2, B_TOP)], FAINT, dash="5,4", head=False)
|
||||
|
||||
box(BL_X, B_TOP, BL_W, B_H, LEFT_NOTE[0], LEFT_NOTE[3], LEFT_NOTE[1], LEFT_NOTE[2])
|
||||
box(BR_X, B_TOP, BR_W, B_H, READERS[0], READERS[3], READERS[1], READERS[2])
|
||||
|
||||
for i, (head, lines) in enumerate(TRAPS):
|
||||
x = PAD + i * (CHIP_W + CHIP_GAP)
|
||||
box(x, C_TOP, CHIP_W, C_H, head, lines, C_AMBER, BG_AMBER)
|
||||
|
||||
rect(PAD, D_TOP, RIGHT - PAD, D_H, C_ROSE, BG_ROSE, dash="7,5")
|
||||
text(PAD + 12, D_TOP + 23, "四条「不许」——上面那张图上没有箭头经过的地方", F_HEAD, C_ROSE, "700")
|
||||
yy = D_TOP + 23 + 19
|
||||
for ln in FORBID:
|
||||
for sub in wrap(ln, F_BODY, RIGHT - PAD - 26):
|
||||
fit("forbid " + sub[:12], sub, F_BODY, RIGHT - PAD - 26)
|
||||
text(PAD + 12, yy, sub, F_BODY, INK)
|
||||
yy += LH
|
||||
if yy > D_TOP + D_H - 2:
|
||||
OVERRUNS.append("tall forbid")
|
||||
|
||||
fy = FOOT_Y
|
||||
for i, sub in enumerate(wrap(FOOT, F_TINY, RIGHT - PAD)):
|
||||
text(PAD, fy + i * 14, sub, F_TINY, FAINT)
|
||||
if i > 1:
|
||||
OVERRUNS.append("foot lines beyond two")
|
||||
|
||||
svg = ('<svg xmlns="http://www.w3.org/2000/svg" width="%s" height="%s" viewBox="0 0 %s %s" '
|
||||
'font-family="%s">%s%s</svg>') % (
|
||||
W, H, W, H, FONT,
|
||||
'<defs><marker id="ah" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="7" markerHeight="7" '
|
||||
'orient="auto-start-reverse"><path d="M0,0 L10,5 L0,10 z" fill="#334155"/></marker></defs>',
|
||||
"".join(E))
|
||||
ET.fromstring(svg)
|
||||
if not os.path.isdir(os.path.dirname(OUT)):
|
||||
os.makedirs(os.path.dirname(OUT))
|
||||
with open(OUT, "w", encoding="utf-8", newline="\n") as fh:
|
||||
fh.write(svg + "\n")
|
||||
emit("wrote %s (%d bytes)" % (os.path.relpath(OUT, ROOT).replace(os.sep, "/"), len(svg.encode("utf-8"))))
|
||||
for o in OVERRUNS:
|
||||
emit("OVERFLOW " + o)
|
||||
emit("overruns=%d" % len(OVERRUNS))
|
||||
Reference in New Issue
Block a user