#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""生成 docs/images/采样环缓冲_环形布局.svg —— 采样环缓冲的设计思想落在图上的三件事:
写指针指「下一格」而不是「最后一格」、窗永远紧贴指针身后、整窗随每次写入右移一格。
为什么不用 Mermaid:本图要读的是"40 个格子里哪 20 格是窗、指针停在第几格"——
Mermaid 画不出等宽格子与指针位置,盒子宽度随文本变,"窗贴着指针"这个关系就没了。
用法(仓库根目录,任意 python3):
python tools/arch_diagram/gen_sample_ring_layout_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 = 580
PAD = 46
RIGHT = W - PAD
FONT = "Microsoft YaHei, Segoe UI, PingFang SC, sans-serif"
TITLE_FONT = 21
SUB_FONT = 13
CARD_FONT = 13.5
BODY_FONT = 11.5
TINY_FONT = 11
SLOT_FONT = 9
INK = "#0f172a"
LINE = "#334155"
MUTED = "#64748b"
FAINT = "#94a3b8"
RULE = "#cbd5e1"
WHITE = "#ffffff"
C_BLUE = "#2563eb"
BG_BLUE = "#eff6ff"
C_AMBER = "#d97706"
BG_AMBER = "#fffbeb"
C_GREEN = "#16a34a"
BG_GREEN = "#f0fdf4"
C_ROSE = "#e11d48"
BG_GREY = "#f1f5f9"
# ----------------------------- 数据(改这里) -----------------------------
DEPTH = 40 # SAMPLE_RING_DEPTH
WIN_N = 20 # 示例窗长(一周波)
HEAD = 30 # 示例写指针位置
START = (HEAD + DEPTH - WIN_N) % DEPTH # = 10
TITLE = "40 格环:写指针指「下一格」,窗永远紧贴它身后"
SUB1 = "DEPTH = 2 周波 × 20 点 = 40;窗长 n 取 1~40,取的是 [head−n, head−1] 这 n 格。"
SUB2 = "图例:蓝 = 本窗 20 点,灰 = 更旧的 10 点,琥珀 = 最旧的 10 点(下一批被覆盖)。"
SUB3 = ("窗首 start = (head + 40 − n) % 40 —— 先加 40 再减,减法就不落到负数;"
"每写一格整窗右移一格。")
MARK_START = "start(最老点)"
MARK_NEW = "head−1(最新点)"
MARK_HEAD = "head(下一拍写这里)"
WRAP_NOTE = "写满后回绕:39 的下一格是 0 —— 窗可能跨过环尾,所以取数必须逐点取模"
CARDS = [
("整帧写完才推进 head", C_GREEN, BG_GREEN,
"9 路语义值一次写齐、再动指针。单帧内不存在「半新半旧」,"
"消费侧取到的窗永远落在整帧边界上。"),
("只存不算是分层契约", C_BLUE, BG_BLUE,
"同刻性归上游 adc_input 负责;本层不假设、不校正,也不做任何跨通道运算——"
"所以它能脱离硬件做主机侧单测。"),
("容量取 2 周波", C_AMBER, BG_AMBER,
"1 周波就够算全周傅氏;另 1 周波是给 1.5~2 周波长窗(抗衰减直流)留的余量,"
"改容量只动一个宏。"),
]
FOOTNOTE = ("n = 40 时 start == head:环满时「下一拍要覆盖的那一格」恰好就是最老点 —— 不是特例,是唯一自洽解。"
"同理 n = 20 时窗正好是 head 身后的 20 格,不多不少。")
# ----------------------------- 布局 -----------------------------
SLOT_W = 22
SLOT_GAP = 2
SLOT_H = 54
SLOT_X0 = PAD
ROW_Y = 200
MARK_Y = 176 # 标记文字基线
STEM_Y1 = 184 # 标记引线
STEM_Y2 = ROW_Y
WRAP_Y = 286
WRAP_TEXT_Y = 308
CARD_TOP = 340
CARD_W = 300
CARD_GAP = 19
CARD_H = 120
# ----------------------------- 工具函数 -----------------------------
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 ('%s'
% (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 (''
% (x1, y1, x2, y2, color, width, d))
def rect(x, y, w, h, fill, stroke=None, rx=8, sw=1.6, dash=None):
s = "" if stroke is None else ' stroke="%s" stroke-width="%g"' % (stroke, sw)
d = "" if dash is None else ' stroke-dasharray="%s"' % dash
return '' % (
x, y, w, h, rx, fill, s, d)
def tri(points, fill):
return '' % (
" ".join("%.1f,%.1f" % (px, py) for px, py in points), fill)
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 slot_x(i):
return SLOT_X0 + i * (SLOT_W + SLOT_GAP)
def slot_c(i):
"""第 i 格的横向中心。"""
return slot_x(i) + SLOT_W / 2.0
# ----------------------------- 生成 -----------------------------
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"), (SUB3, SUB_FONT, "sub3")):
check_fit(s, fs, avail, where)
if OVERFLOW:
for o in OVERFLOW:
emit("OVERFLOW WARN " + o)
add('' % (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(txt(PAD, 108, SUB3, SUB_FONT, MUTED))
# ---- 40 个格子 ----
win_lo = START
win_hi = HEAD - 1
for i in range(DEPTH):
x = slot_x(i)
if win_lo <= i <= win_hi:
fill, stroke, sw = BG_BLUE, C_BLUE, 2.0
elif i >= HEAD:
fill, stroke, sw = BG_AMBER, C_AMBER, 1.4
else:
fill, stroke, sw = BG_GREY, FAINT, 1.2
add(rect(x, ROW_Y, SLOT_W, SLOT_H, fill, stroke, rx=3, sw=sw))
add(txt(slot_c(i), ROW_Y + SLOT_H / 2.0 + 3.5, str(i), SLOT_FONT, MUTED, anchor="middle"))
# ---- 三个关键位置标记 ----
marks = [
(slot_c(START), MARK_START, C_BLUE, "start"),
(slot_c(HEAD - 1), MARK_NEW, C_BLUE, "end"),
(slot_c(HEAD), MARK_HEAD, C_AMBER, "start"),
]
for cx, label, color, anchor in marks:
add(line(cx, STEM_Y1, cx, STEM_Y2 - 2, color, 1.4))
add(tri([(cx, STEM_Y2), (cx - 4.5, STEM_Y2 - 8), (cx + 4.5, STEM_Y2 - 8)], color))
check_fit(label, TINY_FONT, 240, "mark %s" % label)
add(txt(cx, MARK_Y, label, TINY_FONT, color, weight=700, anchor=anchor))
# ---- 回绕提示:39 的下一格是 0 ----
x_from = slot_c(DEPTH - 1)
x_to = slot_c(0)
add(line(x_from, WRAP_Y, x_to + 8, WRAP_Y, C_ROSE, 1.6, dash="7 5"))
add(tri([(x_to, WRAP_Y), (x_to + 9, WRAP_Y - 4.5), (x_to + 9, WRAP_Y + 4.5)], C_ROSE))
for ln in wrap(WRAP_NOTE, TINY_FONT, avail, 1, "wrap note"):
add(txt(PAD, WRAP_TEXT_Y, ln, TINY_FONT, C_ROSE, weight=700))
# ---- 三条设计不变量 ----
inner = CARD_W - 24
for i, (name, c, bg, body) in enumerate(CARDS):
x = PAD + i * (CARD_W + CARD_GAP)
add(rect(x, CARD_TOP, CARD_W, CARD_H, bg, c, rx=8, sw=1.8))
check_fit(name, CARD_FONT, inner, "card %d name" % i)
add(txt(x + 12, CARD_TOP + 24, name, CARD_FONT, INK, weight=700))
y = CARD_TOP + 46
for ln in wrap(body, BODY_FONT, inner, 5, "card %d body" % i):
add(txt(x + 12, y, ln, BODY_FONT, MUTED))
y += 15
# ---- 底部说明 ----
fy = CARD_TOP + CARD_H + 30
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 = ('\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()