#!/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%s\n\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()