scripts/er_design.py 生成 PNG 图片(路径:相对于 SKILL.md 所在目录的 scripts/er_design.py)er_design.py 运行但 exit code ≠ 0(验证不通过,通常是有环密集图交叉/重叠消不掉),自动切换到下方的"回退方案:BFS分层+力导向布局"生成图片scripts/er_design.py 不存在(单文件分发场景),同样使用回退方案xxx_v2.png、xxx_v3.png),不覆盖之前的版本。用户确认满意后,保留满意的版本,删除其余中间版本。xxx_v2.png),方便用户参照坐标描述位移。后续每轮微调都在方格版本上迭代,直到用户满意。最终满意版本去掉方格(GRID = False),删除中间版本。import os, sys, subprocess
script_dir = os.path.dirname(os.path.abspath(__file__)) # SKILL.md 所在目录
script_path = os.path.join(script_dir, 'scripts', 'er_design.py')
if os.path.isfile(script_path):
# 首选方案存在,尝试运行
result = subprocess.run([sys.executable, script_path, ...], capture_output=True)
if result.returncode == 0:
# 成功,展示图片
pass
else:
# 验证失败,走回退方案
use_fallback()
else:
# 脚本不存在,走回退方案
use_fallback()
判断逻辑:先检测 er_design.py 是否存在 → 存在则运行 → exit code 0 直接用结果,非 0 走回退 → 不存在也走回退。
| 场景 | 说明 |
|---|---|
| ------ | ------ |
| 多实体+关系ER图 | 实体用矩形,联系统一用菱形 |
| 自动布局 | 检测图结构,选最优策略 |
| 基数标注 | 1:1、1:n、m:n 自动标注 |
| 三项验证 | 无交叉、无重叠、基数圆间距 ≥ 0.5(圆外缘到图形) |
| 场景 | 原因 |
|---|---|
| ------ | ------ |
| 单实体属性图(含椭圆属性) | 用 ER-single skill |
| 带主键/下划线 | 本工具不标注主键 |
| 彩色/装饰风格 | 黑白简约风格 |
| 修改已有图片 | 每次都重新生成 |
python scripts/er_design.py \
--entities "实体1,实体2,..." \
--relations "关系1,关系2,..." \
[--output "输出.png"] \
[--scale 1.5] \
[--font "SimHei"]
| 参数 | 说明 |
|---|---|
| ------ | ------ |
--entities | 必填,逗号分隔的实体列表 |
--relations | 必填,逗号分隔的关系列表,格式 实体1:基数--动词--基数:实体2 |
--output | 输出 PNG 路径(默认 {首个实体}_ER图.png) |
--scale | 缩放比例(默认自动搜索 0.6~4.0) |
--font | 中文字体名(默认 SimHei) |
--bfs-direction | BFS 分层方向:horizontal 横向(默认)/ vertical 纵向 |
实体1:基数--动词--基数:实体2
基数:1、n、m,动词用中文。示例:
观众:1--购买--n:票务
影厅:n--播放--n:影片
学生:m--选修--n:课程
脚本自动检测图结构,选择最优布局:
| 图类型 | 策略 | 效果 |
|---|---|---|
| -------- | ------ | ------ |
| 无环(树状结构) | BFS 分层 + 行内排列优化 | 整齐行列,零交叉,连线短 |
| 有环 | 力导向 (Fruchterman-Reingold) | 自然排布,自动避让 |
检测到有环时自动切换力导向,无环时用分层保证零交叉。
保存 PNG 前自动运行三项验证,全部通过才保存:
任意一项不通过则不保存,自动尝试其他缩放比例或随机种子。
| 返回码 | 含义 | 对用户说的话 |
|---|---|---|
| -------- | ------ | ------------- |
| 0 | 成功 | "已生成 ER 图,保存在 [路径]" |
| 1 | 输入错误 | 告诉用户具体哪里错了,让用户改后重试 |
| 2 | 缺依赖包 | "需要安装 Python 库,运行 pip install numpy matplotlib 就行" |
| 其他 | 生成失败 | 建议减少实体数量或检查关系格式后重试 |
用户提供了完整实体和关系:直接按用户说的生成,不做修改。
用户只提供系统名称,需要你自行设计:先在大脑中推演一遍实体与业务联系,确保每条关系在业务上合乎逻辑,确认无误后再生成。
当 er_design.py 验证失败(常有环密集图交叉/重叠消不掉)或脚本不存在时,用此方案。
核心思路:
import math, os, sys
import numpy as np
# ══════ 用户输入 ══════
entities = ["影院", "影厅", "影片"] # 替换为实际实体
relations = [ # 替换为实际关系 (ei1, 动词, ei2, card1, card2)
("影院", "包含", "影厅", "1", "n"),
("影厅", "播放", "影片", "n", "n"),
]
# ══════ 输入校验 ══════
errors = []
if not entities:
errors.append("实体列表不能为空")
seen = set()
for e in entities:
if e in seen:
errors.append(f"重复实体: {e}")
seen.add(e)
if not relations:
errors.append("关系列表不能为空")
for ei1, verb, ei2, c1, c2 in relations:
if ei1 not in seen: errors.append(f"实体不存在: {ei1}")
if ei2 not in seen: errors.append(f"实体不存在: {ei2}")
if c1 not in ('1','n','m'): errors.append(f"无效基数: {c1}")
if c2 not in ('1','n','m'): errors.append(f"无效基数: {c2}")
if errors:
for e in errors: print(e)
sys.exit(1)
# ══════ 依赖检查 ══════
try:
import matplotlib
matplotlib.use('Agg')
import matplotlib.pyplot as plt
import matplotlib.font_manager as fm
from matplotlib.patches import FancyBboxPatch, Polygon
except ImportError as e:
print(f"缺少依赖,运行: pip install numpy matplotlib")
sys.exit(2)
# ══════ 中文字体 ══════
font_name = None
for font in ['SimHei', 'Microsoft YaHei', 'WenQuanYi Micro Hei',
'Noto Sans CJK SC', 'PingFang SC', 'STHeiti', 'SimSun']:
if font in {f.name for f in fm.fontManager.ttflist}:
font_name = font; break
if font_name:
plt.rcParams['font.family'] = [font_name]
plt.rcParams['font.sans-serif'] = [font_name]
else:
print("警告: 未检测到中文字体")
plt.rcParams['axes.unicode_minus'] = False
# ══════ BFS 分层 ══════
N = len(entities)
ent_idx = {e: i for i, e in enumerate(entities)}
adj = [[] for _ in range(N)]
for ei1, _, ei2, _, _ in relations:
i, j = ent_idx[ei1], ent_idx[ei2]
adj[i].append(j); adj[j].append(i)
# 从连接最多的实体开始BFS
center = max(range(N), key=lambda i: len(adj[i]))
layer = {center: 0}
queue = [center]
while queue:
v = queue.pop(0)
for u in adj[v]:
if u not in layer:
layer[u] = layer[v] + 1
queue.append(u)
max_l = max(layer.values())
layers = {}
for i, l in layer.items():
layers.setdefault(l, []).append(entities[i])
# ══════ DFS 环路检测 ══════
ent_adj = {i: set() for i in range(N)}
for ei1, _, ei2, _, _ in relations:
i, j = ent_idx[ei1], ent_idx[ei2]
ent_adj[i].add(j); ent_adj[j].add(i)
def has_cycle():
visited = set()
def dfs(v, parent):
visited.add(v)
for u in ent_adj[v]:
if u not in visited:
if dfs(u, v): return True
elif u != parent: return True
return False
for v in range(N):
if v not in visited:
if dfs(v, -1): return True
return False
cyclic = has_cycle()
print(f"环检测: {'有环' if cyclic else '无环'}")
# ══════ 布局选择 ══════
entity_xy = {}
entity_xy[entities[center]] = (0.0, 0.0)
if not cyclic:
# 树状结构 → 横向BFS分层,零交叉
# 列间距 ≥ RECT_W + 菱形宽 ≈ 9+7=16,取20留余量
X_SP, Y_SP = 20.0, 10.0
for lv in range(max_l + 1):
ents = layers.get(lv, [])
x = lv * X_SP
n = len(ents)
for i, ent in enumerate(ents):
y = (i - (n - 1) / 2) * Y_SP
entity_xy[ent] = (x, y)
else:
# 有环图 → Fruchterman-Reingold 力导向布局
M = len(relations)
total = N + M
# 构建邻接矩阵(实体↔菱形)
adj_mat = np.zeros((total, total))
for ri, (ei1, verb, ei2, c1, c2) in enumerate(relations):
di = N + ri
i, j = ent_idx[ei1], ent_idx[ei2]
adj_mat[i, di] = adj_mat[di, i] = 1
adj_mat[j, di] = adj_mat[di, j] = 1
np.random.seed(42)
pos = np.random.randn(total, 2) * 0.3
k = 5.0 * math.sqrt(max(N, 4) / 4.0)
T = k
for _ in range(300):
forces = np.zeros_like(pos)
for i in range(total):
for j in range(total):
if i == j: continue
d = np.linalg.norm(pos[i] - pos[j])
if d < 0.01: d = 0.01
forces[i] += (pos[i] - pos[j]) / d * (k**2 / d)
for i in range(total):
for j in range(total):
if adj_mat[i, j] == 0: continue
d = np.linalg.norm(pos[j] - pos[i])
if d < 0.01: d = 0.01
forces[i] += (pos[j] - pos[i]) / d * (d**2 / k)
mag = np.linalg.norm(forces, axis=1)
pos += forces * np.minimum(1.0, T / np.maximum(mag, 1e-10))[:, np.newaxis]
T *= 0.92
pos -= pos.mean(axis=0)
pos /= np.std(pos) / 6.0
for i, ent in enumerate(entities):
entity_xy[ent] = (pos[i, 0], pos[i, 1])
# ══════ 缩放搜索 ══════
# 尝试多个缩放比例,通过验证后选最优
scales = [0.6, 0.8, 1.0, 1.2, 1.5, 1.8, 2.0, 2.5, 3.0, 4.0, 5.0, 6.0, 8.0]
best_scale = 1.0 # 默认
for sf in scales:
sf_ok = True
# 检查缩放下是否有线段太短
for ei1, verb, ei2, c1, c2 in relations:
x1, y1 = entity_xy[ei1]; x2, y2 = entity_xy[ei2]
mx, my = (x1+x2)/2*sf, (y1+y2)/2*sf
for ex, ey in [(x1*sf, y1*sf), (x2*sf, y2*sf)]:
seg_len = math.hypot(mx - ex, my - ey) / 2 # 半段
if seg_len < 0.6 * RECT_W * sf: # 太短
sf_ok = False; break
if not sf_ok: break
if sf_ok:
best_scale = sf; break
print(f"缩放: {best_scale}")
for ent in entity_xy:
entity_xy[ent] = (entity_xy[ent][0] * best_scale, entity_xy[ent][1] * best_scale)
# ══════ 手动微调(关键步骤)══════
# 力导向结果可能需要微调:对外层实体,将其移近关联的内层实体
# 写法: entity_xy["外层实体"] = (x, y)
# 根据实际图的布局效果调整坐标,这里留空由调用者填充
# ══════ 几何工具函数 ══════
def rect_point_distance(rx, ry, rw, rh, px, py):
"""计算点到矩形(含圆角近似)的最短距离,点在内部返回负值"""
dx = max(abs(px - rx) - rw, 0.0)
dy = max(abs(py - ry) - rh, 0.0)
return math.hypot(dx, dy)
def diamond_point_distance(dx, dy, dw, dh, px, py):
"""计算点到菱形的最短距离"""
dpx = abs(px - dx)
dpy = abs(py - dy)
if dw < 1e-10 or dh < 1e-10:
return math.hypot(px - dx, py - dy)
return (dpx / dw + dpy / dh - 1.0) * (dw * dh) / math.hypot(dw, dh)
# ══════ 统一修正:基数圆进入其他图形时推开该图形 ══════
# 注意:不检测菱形之间的碰撞(重叠不影响可读性,推开反而可能引入新问题)。
# 仅检测基数圆圆心到其他图形的距离,若圆边缘到图形边缘 < 0.5 则推开该图形。
R = 0.65
RECT_W, RECT_H = 9.0, 4.5
diamond_positions = {}
for ri, (ei1, verb, ei2, c1, c2) in enumerate(relations):
x1, y1 = entity_xy[ei1]
x2, y2 = entity_xy[ei2]
diamond_positions[ri] = [(x1 + x2) / 2, (y1 + y2) / 2]
MIN_CIRCLE_GAP = R + 0.5
for iteration in range(50):
violations = []
all_graphics = []
for ent, (ex, ey) in entity_xy.items():
rw = max(RECT_W, len(ent) * 1.1 + 1.0) / 2
rh = RECT_H / 2
all_graphics.append(('rect', ex, ey, rw, rh, ent))
for ri2 in range(len(relations)):
mx2, my2 = diamond_positions[ri2]
dw2 = max(7.0, len(relations[ri2][1]) * 1.2) / 2
dh2 = max(3.5, 2.2) / 2
all_graphics.append(('diamond', mx2, my2, dw2, dh2, ri2))
for ri, (ei1, verb, ei2, c1, c2) in enumerate(relations):
x1, y1 = entity_xy[ei1]
x2, y2 = entity_xy[ei2]
mx, my = diamond_positions[ri]
dw = max(7.0, len(verb) * 1.2)
dh = max(3.5, 2.2)
for ex, ey, card, ent_name in [(x1, y1, c1, ei1), (x2, y2, c2, ei2)]:
vx, vy = mx - ex, my - ey
length = math.hypot(vx, vy)
if length < 0.01: continue
nx, ny = vx / length, vy / length
rw = max(RECT_W, len(ent_name) * 1.1 + 1.0)
rh = RECT_H
tx = rw/2 / abs(nx) if abs(nx) > 1e-10 else float('inf')
ty = rh/2 / abs(ny) if abs(ny) > 1e-10 else float('inf')
t = min(tx, ty) * 0.92
sx, sy = ex + nx * t, ey + ny * t
a, b = dw / 2, dh / 2
denom = abs(nx) / a + abs(ny) / b
u = (1.0 / denom * 0.92) if denom > 1e-10 else 0
dx_e, dy_e = mx - nx * u, my - ny * u
cpx, cpy = (sx + dx_e) / 2, (sy + dy_e) / 2
for gtype, gx, gy, ghw, ghh, gid in all_graphics:
if gtype == 'diamond' and gid == ri: continue
if gtype == 'rect' and gid == ent_name: continue
if gtype == 'rect':
dist = rect_point_distance(gx, gy, ghw, ghh, cpx, cpy)
else:
dist = diamond_point_distance(gx, gy, ghw, ghh, cpx, cpy)
if dist < MIN_CIRCLE_GAP:
if gtype == 'diamond':
px, py = gx - cpx, gy - cpy
plen = math.hypot(px, py)
if plen < 0.01:
px, py = 0.0, 1.0
plen = 1.0
push = (MIN_CIRCLE_GAP - dist) + 0.3
violations.append((gid, px / plen * push, py / plen * push))
if not violations:
break
accum = {}
for ri, dx, dy in violations:
if ri not in accum:
accum[ri] = [0.0, 0.0, 0]
accum[ri][0] += dx
accum[ri][1] += dy
accum[ri][2] += 1
for ri, (tx, ty, cnt) in accum.items():
diamond_positions[ri][0] += tx / cnt
diamond_positions[ri][1] += ty / cnt
def validate_circle_spacing(entity_xy, relations, R, RECT_W, RECT_H):
"""检查每个基数圆边缘到所有图形边缘的距离是否 >= 0.5"""
MIN_GAP = 0.5
violations = []
# 收集所有图形: (type, cx, cy, hw, hh, name)
graphics = []
for ent, (ex, ey) in entity_xy.items():
rw = max(RECT_W, len(ent) * 1.1 + 1.0) / 2
rh = RECT_H / 2
graphics.append(('rect', ex, ey, rw, rh, ent))
# 预计算每条关系的菱形
diamond_info = {}
for ri, (ei1, verb, ei2, c1, c2) in enumerate(relations):
x1, y1 = entity_xy[ei1]
x2, y2 = entity_xy[ei2]
mx, my = (x1 + x2) / 2, (y1 + y2) / 2
dw = max(7.0, len(verb) * 1.2) / 2
dh = max(3.5, 2.2) / 2
diamond_info[ri] = (mx, my, dw, dh, verb)
graphics.append(('diamond', mx, my, dw, dh, verb))
# 检查每条线段的基数圆
for ri, (ei1, verb, ei2, c1, c2) in enumerate(relations):
x1, y1 = entity_xy[ei1]
x2, y2 = entity_xy[ei2]
dmx, dmy, ddw, ddh, _ = diamond_info[ri]
dw = ddw * 2
dh = ddh * 2
for ex, ey, card, ent_name in [(x1, y1, c1, ei1), (x2, y2, c2, ei2)]:
vx, vy = dmx - ex, dmy - ey
length = math.hypot(vx, vy)
if length < 0.01:
continue
nx, ny = vx / length, vy / length
rw_e = max(RECT_W, len(ent_name) * 1.1 + 1.0) / 2
rh_e = RECT_H / 2
tx = rw_e / abs(nx) if abs(nx) > 1e-10 else float('inf')
ty = rh_e / abs(ny) if abs(ny) > 1e-10 else float('inf')
t = min(tx, ty) * 0.92
sx, sy = ex + nx * t, ey + ny * t
a, b = dw / 2, dh / 2
denom = abs(nx) / a + abs(ny) / b
u = (1.0 / denom * 0.92) if denom > 1e-10 else 0
d_ex, d_ey = dmx - nx * u, dmy - ny * u
cpx, cpy = (sx + d_ex) / 2, (sy + d_ey) / 2
# 检查线段是否够长: 半段 >= R + 0.5
seg_len = math.hypot(d_ex - sx, d_ey - sy)
if seg_len / 2 - R < MIN_GAP:
violations.append(
f"R{ri} {ent_name}→{verb}: 线段太短({seg_len:.1f}), "
f"圆边距={seg_len/2 - R:.2f} < 0.5"
)
continue
# 检查圆心到所有图形的距离
for gtype, gx, gy, ghw, ghh, gname in graphics:
if gname in (ent_name, verb):
continue
if gtype == 'rect':
dist = rect_point_distance(gx, gy, ghw, ghh, cpx, cpy)
else:
dist = diamond_point_distance(gx, gy, ghw, ghh, cpx, cpy)
if dist < R + MIN_GAP:
violations.append(
f"R{ri} {ent_name}→{verb} 圆距{gname}: "
f"{dist - R:.2f} < 0.5"
)
return violations
def auto_fix_diamond_positions(entity_xy, relations, R, RECT_W, RECT_H, max_iter=30):
"""自动调整菱形中点位置使其远离两端实体,满足圆间距约束"""
MIN_GAP = 0.5
step = 0.5
for _ in range(max_iter):
violations = []
for ri, (ei1, verb, ei2, c1, c2) in enumerate(relations):
x1, y1 = entity_xy[ei1]
x2, y2 = entity_xy[ei2]
mx, my = (x1 + x2) / 2, (y1 + y2) / 2
dw = max(7.0, len(verb) * 1.2)
dh = max(3.5, 2.2)
for ex, ey, ent_name in [(x1, y1, ei1), (x2, y2, ei2)]:
vx, vy = mx - ex, my - ey
length = math.hypot(vx, vy)
if length < 0.01:
continue
nx, ny = vx / length, vy / length
rw_e = max(RECT_W, len(ent_name) * 1.1 + 1.0) / 2
rh_e = RECT_H / 2
tx = rw_e / abs(nx) if abs(nx) > 1e-10 else float('inf')
ty = rh_e / abs(ny) if abs(ny) > 1e-10 else float('inf')
t = min(tx, ty) * 0.92
a, b = dw / 2, dh / 2
denom = abs(nx) / a + abs(ny) / b
u = (1.0 / denom * 0.92) if denom > 1e-10 else 0
seg_len = length - t - u
if seg_len / 2 - R < MIN_GAP:
shortfall = 2 * (R + MIN_GAP) - seg_len
# 沿远离实体的方向移动菱形
entity_xy[ei1] = (x1, y1) # 不修改
entity_xy[ei2] = (x2, y2)
# 把中点沿远离该实体的方向推
new_mx = mx + nx * shortfall * 0.6
new_my = my + ny * shortfall * 0.6
# 更新中点(临时存储在entity_xy的扩展字段)
if f"__dm_{ri}" not in dir():
pass
violations.append((ri, nx * shortfall * 0.6, ny * shortfall * 0.6))
if not violations:
break
return validate_circle_spacing(entity_xy, relations, R, RECT_W, RECT_H)
# ══════ 绘图参数 ══════
R = 0.65 # 基数圆半径
RECT_W, RECT_H = 9.0, 4.5
FONT_SIZE = 8
# ══════ 基数圆间距验证 ══════
print("验证基数圆间距 (要求: 圆边缘到图形边缘 >= 0.5)...")
vios = validate_circle_spacing(entity_xy, relations, R, RECT_W, RECT_H)
if vios:
print(f" 发现 {len(vios)} 项违规:")
for v in vios:
print(f" - {v}")
print(" 尝试自动修正...")
remaining = auto_fix_diamond_positions(entity_xy, relations, R, RECT_W, RECT_H)
if remaining:
print(f" 自动修正后仍有 {len(remaining)} 项违规:")
for v in remaining:
print(f" - {v}")
print(" 请手动微调实体坐标后重试,或增大 radii 中的圆环半径")
else:
print(" 自动修正完成,全部通过")
else:
print(" 全部通过")
# ══════ 绘图 ══════
xs = [x for x, y in entity_xy.values()]
ys = [y for x, y in entity_xy.values()]
pad = 8.0
fig, ax = plt.subplots(1, 1, figsize=(28, 26))
ax.set_xlim(min(xs) - pad, max(xs) + pad)
ax.set_ylim(min(ys) - pad, max(ys) + pad)
ax.set_aspect('equal')
ax.axis('off')
# 画实体矩形
for ent, (x, y) in entity_xy.items():
rw = max(RECT_W, len(ent) * 1.1 + 1.0)
rh = RECT_H
rect = FancyBboxPatch(
(x - rw/2, y - rh/2), rw, rh,
boxstyle="round,pad=0.15", edgecolor='#000000', linewidth=2.0,
facecolor='#FFFFFF', zorder=5)
ax.add_patch(rect)
ax.text(x, y, ent, ha='center', va='center',
fontsize=FONT_SIZE + 3, fontweight='bold', color='#000000', zorder=6)
# 画关系菱形和连线
for ri, (ei1, verb, ei2, c1, c2) in enumerate(relations):
x1, y1 = entity_xy[ei1]
x2, y2 = entity_xy[ei2]
mx, my = diamond_positions[ri] # 使用碰撞检测后的位置
dw = max(7.0, len(verb) * 1.2)
dh = max(3.5, 2.2)
diamond = Polygon([
(mx, my + dh/2), (mx + dw/2, my), (mx, my - dh/2), (mx - dw/2, my)
], edgecolor='#000000', linewidth=1.5, facecolor='#FFFFFF', zorder=5)
ax.add_patch(diamond)
ax.text(mx, my, verb, ha='center', va='center',
fontsize=FONT_SIZE, color='#000000', zorder=6)
for ex, ey, card, ent_name in [(x1, y1, c1, ei1), (x2, y2, c2, ei2)]:
vx, vy = mx - ex, my - ey
length = math.hypot(vx, vy)
if length < 0.01:
continue
nx, ny = vx / length, vy / length
rw = max(RECT_W, len(ent_name) * 1.1 + 1.0)
rh = RECT_H
tx = rw/2 / abs(nx) if abs(nx) > 1e-10 else float('inf')
ty = rh/2 / abs(ny) if abs(ny) > 1e-10 else float('inf')
t = min(tx, ty) * 0.92
sx, sy = ex + nx * t, ey + ny * t
a, b = dw / 2, dh / 2
denom = abs(nx) / a + abs(ny) / b
u = (1.0 / denom * 0.92) if denom > 1e-10 else 0
dx_e, dy_e = mx - nx * u, my - ny * u
ax.plot([sx, dx_e], [sy, dy_e], color='#000000', linewidth=1.2, zorder=3)
cpx, cpy = (sx + dx_e) / 2, (sy + dy_e) / 2
circle = plt.Circle((cpx, cpy), R, facecolor='white', linewidth=0,
edgecolor='none', zorder=7)
ax.add_patch(circle)
ax.text(cpx, cpy, card, ha='center', va='center',
fontsize=FONT_SIZE + 1, color='#000000', zorder=8)
output = f"{entities[0]}等_ER图.png"
plt.tight_layout(pad=1.0)
fig.savefig(output, dpi=200, bbox_inches='tight', facecolor='#FFFFFF', edgecolor='none')
plt.close(fig)
print(f"成功: {output}")
print(f"完整路径: {os.path.abspath(output)}")
力导向布局后,节点位置由物理模拟决定,可能出现以下需要微调的情况:
微调方式:直接在 entity_xy["实体名"] = (x, y) 中设置坐标,重新运行看效果,反复调整。
多条关系的中点在几何上可能重叠(如购物车与用户的垂直中点和订单与商品的水平中点恰好都是 (0, 12)),导致两个菱形叠在一起,看起来像菱形连着菱形。回退方案在手动微调后自动运行碰撞检测:两个菱形中心距离 < 8.0 时,沿连线方向互相推开,最多迭代 20 轮。绘图时使用碰撞检测后的 diamond_positions 而非原始中点。
回退方案同样执行 基数圆到图形边缘 ≥ 0.5 的验证。验证函数 validate_circle_spacing() 检查:
验证不通过时:
auto_fix_diamond_positions() 自动推远菱形中点radii 圆环半径或手动微调实体坐标| 问题 | 处理方式 |
|---|---|
| ------ | --------- |
| ImportError | 告诉用户 pip install numpy matplotlib |
| 中文字体缺失 | 警告但继续(中文变方框),建议安装字体 |
| 绘图异常 | try/except 包裹,缩小尺寸或简化参数重试 |
| 全部失败 | 输出实体和关系列表,建议用户手动绘制 |
'#000000',禁止用颜色名称.py 文件共 5 个版本