Files
STM32F4-Base/.dsh/skills/draw-diagram/reference/svg-collision-audit.py
T
wandering1 045ca7f688 docs(配图): 端到端链路鸟瞰进生成器体系,补大图碰撞审计判据
把 archify 出的交互架构图落回仓内可复现形态,避免 docs/ 里出现「没有生成器的
SVG」:

- 新生成器 tools/arch_diagram/gen_arch_endtoend_svg.py → docs/images/架构总览_
  端到端链路.svg(42 框 / 37 边 / 5 条纵带,线型分数据·命令·屏障心跳三类);
  数据全部内嵌在 .py 里(generated-fresh 只把单个 gen_*.py 拷进临时目录重跑,
  外部 JSON 输入会 rc≠0 假红),SVG 内不含时间戳/commit/绝对路径
- docs/架构设计/项目架构总览.md 新增 §1.3 挂图并写清与 §1 分层图的分工
  (那张答「谁在哪一层」,这张答「一条数据走到底穿过谁」;纵带只是列分组,
  依赖方向仍以 §1.1 表与 layer-deps 门禁为准),版本 v1.2 → v1.3
- 两处生成器清单登记:tools/arch_diagram/README.md 脚本一览、
  docs/架构设计/图形绘制流程.md 的选型表与产物对照表
- draw-diagram skill 新增 §三b(大图碰撞审计判据 + 两条几何纪律)+
  reference/svg-collision-audit.py(拷进 ci-artifacts 用,刻意不进 tools/)

本窗实测(都已写进 skill):42 框 37 边的图缩略目检看着干净,解析式审计一眼
报出 23 条——边标签文字盒压到节点框会被后画的节点白底吃掉半个字(「串行执行」
渲染成「行执行」);根因是同行两框走了「同轴加一段走廊」的通用规则,产出退化
四点折线,把标签落点推到第一半中点。修法:同轴且已对齐直接返回两点线,标签
落点改按总弧长取 50%。

零余量第二遍(用生成器自己的 Box/route/label_at 真值扫,框不膨胀不内缩)又抓到
两处近失:「TRGO 触发」「帧 B 下发」与相邻框只剩 0.7px / 1.5px,缩成「TRGO」
「帧 B」后最紧余量升到 3.5px;同一遍还纠正了审计器自己的错——`hi > y0` 误写成
`hi > x0` 凭空报 3 条穿框,差点重排三条边的走线,所以判据里加了「先验审计器
本身再信 0 条」。

自查:连跑两次产物 sha1 不变;scripts/check_generated_fresh.py rc=0(25 生成器
/ 21 一致 / 4 在只减不增基线内);碰撞审计 rc=0(0 条);生成器无 OVERFLOW;
doc-links --sections-strict 逐篇 rc=0;mermaid-syntax / nested-comment /
skill-size / bom-drift 无新发;改动文件字节体检干净;/keil-build 0 错 0 警。
2026-10-30 15:59:24 +08:00

155 lines
7.8 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""一次性碰撞审计(拷进 `ci-artifacts/` 用,**不要放进 `tools/arch_diagram/`**):
解析自己生成的 SVG,报「边标签压节点 / 走线穿框 / 标签互压 / 文字出画布 / 节点文字出框 /
标签余量过窄」,并打印最紧的三条「标签文字 ↔ 最近框」水平余量。
为什么需要它:`check_fit` 预检只量**框内**文字;几十框几十边的大图,
标签压框与走线穿框在缩略 PNG 里根本看不全(实测 42 框 37 边报出 23 条),
而像素回读又慢又主观。解析产物是确定性的。
依赖:待审图对应的生成器模块必须能被 import(脚本靠它复用同一把估宽尺子 `text_w()`
与几何常量 `CW` / `CH` / `EDGE_FONT` / `ECOL`,两边同尺子才不会假绿)。
生成器侧要求:边标签白片用 `fill-opacity="0.94"`、节点框用 `fill="#ffffff"` 且
`width==CW`、`height==CH`(本脚本靠这两条分诊);文字用 `<text font-size fill>`。
**这一遍照不到的东西**(别把它当唯一判据):
- 走线检查必须把框**内缩**几像素,否则合法的「线接在框边上」会全报成穿框——从 SVG
反推不出哪两个框是本边的端点。代价:1~3px 的切边也照不到。要零余量就在**生成器侧**
再用 `Box` / `route()` / `label_at()` 的真值扫一遍(线段 vs 不膨胀的框,只跳本边两端)。
- 白片比通道宽一点、被节点白底盖住边角:无害(白片先画、节点后画),不用改;
要盯的是**文字**的余量,上面那三条数就是。
- 自写的判据脚本先拿一条**已知干净**的边验它不报,再信它的「0 条」:实测一条
`hi > x0` 写成 `hi > y0` 的复制粘贴错,凭空报出 3 条「穿框」,差点为此重排走线。
用法(仓库根):
python ci-artifacts/label_audit.py <生成器模块名> [svg 路径]
# 例:python ci-artifacts/label_audit.py gen_arch_endtoend_svg
# python ci-artifacts/label_audit.py gen_wave_record_flow_svg docs/images/故障录波数据流.svg
退出码:0 = 无问题,1 = 有问题(打印明细)。
别的生成器若没有同名常量(多数用 `CHIP_FONT` / 自己的线色变量),改脚本顶部 `EDGE_COLORS` 与 `edge_labels` 那两处筛选即可——估宽必须继续用该生成器自己的 `text_w()`,别换一把尺子。
"""
import sys
import xml.etree.ElementTree as ET
sys.path.insert(0, "tools/arch_diagram")
sys.stdout.reconfigure(encoding="utf-8", errors="replace")
mod_name = sys.argv[1]
G = __import__(mod_name)
svg_path = sys.argv[2] if len(sys.argv) > 2 else G.OUT
MASK_OP = "0.94" # 边标签白片的 fill-opacity(生成器侧约定)
EDGE_COLORS = set(G.ECOL.values()) # 边标签颜色(生成器侧的线色表)
def box_of_text(el):
"""<text> → (x0, y0, x1, y1, 文本, 字号, 颜色)。baseline y 之上约 0.85em、之下 0.22em。"""
x, y = float(el.get("x")), float(el.get("y"))
fs = float(el.get("font-size"))
s = "".join(el.itertext())
w = G.text_w(s, fs)
a = el.get("text-anchor", "start")
x0 = x if a == "start" else (x - w / 2 if a == "middle" else x - w)
return (x0, y - fs * 0.85, x0 + w, y + fs * 0.22, s, fs, el.get("fill"))
def area(a, b):
ix = min(a[2], b[2]) - max(a[0], b[0])
iy = min(a[3], b[3]) - max(a[1], b[1])
return ix * iy if (ix > 0.5 and iy > 0.5) else 0.0
def segs_of(d):
n = [float(v) for v in d.replace("M", " ").replace("L", " ").replace(",", " ").split()]
pts = list(zip(n[0::2], n[1::2]))
return list(zip(pts, pts[1:]))
def seg_hits_rect(sg, r, m=2.0):
"""轴对齐线段 vs 矩形(内缩 m 排除贴边的合法端点)。斜段一律算可疑。"""
(x0, y0), (x1, y1) = sg
rx0, ry0, rx1, ry1 = r[0] + m, r[1] + m, r[2] - m, r[3] - m
if abs(y0 - y1) < 0.5:
return ry0 < y0 < ry1 and min(x0, x1) < rx1 - m and max(x0, x1) > rx0 + m
if abs(x0 - x1) < 0.5:
return rx0 < x0 < rx1 and min(y0, y1) < ry1 - m and max(y0, y1) > ry0 + m
return True
root = ET.parse(svg_path).getroot()
W, H = float(root.get("width")), float(root.get("height"))
texts, nodes, masks, paths = [], [], [], []
for el in root.iter():
tag = el.tag.split("}")[-1] # 去命名空间,否则一个都匹配不上
if tag == "text":
texts.append(box_of_text(el))
elif tag == "rect":
w, h = float(el.get("width")), float(el.get("height"))
x, y = float(el.get("x")), float(el.get("y"))
if el.get("fill") == "#ffffff" and abs(w - G.CW) < 0.6 and abs(h - G.CH) < 0.6:
nodes.append((x, y, x + w, y + h, "node"))
elif el.get("fill") == "#ffffff" and el.get("fill-opacity") == MASK_OP:
masks.append((x, y, x + w, y + h, "mask"))
elif tag == "path" and el.get("fill") == "none":
paths.append(segs_of(el.get("d")))
edge_labels = [t for t in texts if t[6] in EDGE_COLORS and t[5] == G.EDGE_FONT]
bad = []
for lb in edge_labels:
b = lb[:4]
for n in nodes:
a = area(b, n)
if a > 6: # 压到框边 6 px² 以上就会被节点吃掉半个字
bad.append("标签压节点 %-12s 压 node@(%.0f,%.0f) %.0f px²" % (lb[4][:12], n[0], n[1], a))
for ob in edge_labels:
if ob is not lb and area(b, ob[:4]) > 6:
bad.append("标签互压 %s / %s" % (lb[4][:10], ob[4][:10]))
if b[0] < 2 or b[2] > W - 2 or b[3] > H - 2 or b[1] < 2:
bad.append("标签出画布 %s box=(%.0f,%.0f,%.0f,%.0f) 画布 %.0fx%.0f"
% (lb[4][:12], b[0], b[1], b[2], b[3], W, H))
for segs in paths:
for sg in segs:
for n in nodes:
if seg_hits_rect(sg, n):
bad.append("走线穿框 (%d,%d)-(%d,%d) 压 node@(%.0f,%.0f)"
% (sg[0][0], sg[0][1], sg[1][0], sg[1][1], n[0], n[1]))
break
# 「节点文字出框」只能按几何判:文字中心落在某个节点框里,却没被该框完整包住。
# 别拿 font-size 当筛选——本仓惯例里副题字号 = 边标签字号 = 脚注字号(9.5),按字号筛会全灭。
for t in texts:
cx, cy = (t[0] + t[2]) / 2, (t[1] + t[3]) / 2
host = [n for n in nodes if n[0] <= cx <= n[2] and n[1] <= cy <= n[3]]
for n in host:
if not (t[0] >= n[0] - 1 and t[2] <= n[2] + 1 and t[1] >= n[1] - 1 and t[3] <= n[3] + 1):
bad.append("节点文字出框 %r 超 node@(%.0f,%.0f)" % (t[4][:16], n[0], n[1]))
break
# 余量比「有没有压上」更早暴露问题:本仓约定边标签整段落在框间通道里,
# 只要文字离最近框 < 3px,字宽估算稍偏(本脚本与生成器共用同一把尺子,
# 但真实字体度量和它有出入)就会被后画的节点吃掉笔画。
MIN_CLEAR = 3.0
clears = []
for lb in edge_labels:
b = lb[:4]
worst, wid = None, None
for n in nodes:
if min(b[3], n[3]) - max(b[1], n[1]) <= 0: # 竖直方向不相干
continue
c = max(n[0] - b[2], b[0] - n[2]) # 正=间距,负=压进去多深
if worst is None or c < worst:
worst, wid = c, (n[0], n[1])
if worst is not None:
clears.append((worst, lb[4][:12], wid))
if worst < MIN_CLEAR:
bad.append("标签余量过窄 %r 距 node@%s 仅 %.1fpx(< %.1f)"
% (lb[4][:12], wid, worst, MIN_CLEAR))
seen = []
for b in bad:
if b not in seen:
seen.append(b)
print("%s:边标签 %d / 节点 %d / 文字 %d / 路径 %d"
% (svg_path, len(edge_labels), len(nodes), len(texts), len(paths)))
for c in sorted(clears)[:3]:
print(" 最紧余量 %.1fpx 标签 %r 邻 node@%s" % (c[0], c[1], c[2]))
for b in seen:
print(" ! " + b)
print("合计问题 %d 条" % len(seen))
sys.exit(1 if seen else 0)