getfunWindz

visio-style-diagram

Generate Visio-style software engineering diagrams via draw.io Desktop. Use when user asks for: Visio风格图 / 数据库ER图 / 实体关系图 / 类图 / 架构图 / draw.io图 / 软件工程图 / 数据库设计图 / 系统部署图 / 数据流图 / DFD / 程序流程图 / N-S盒图 / Nassi-Shneiderman / 盒图 / UML图 / UML用例图 / UML状态图 / UML时序图 / 用例图 / 状态图 / 时序图 / 活动图 / 泳道图 / 跨职能流程图 / 软件工程图规范. Requires draw.io Desktop installed at C:\Program Files\draw.io\draw.io.

getfunWindz 0 Updated 1mo ago

Resources

3
GitHub

Install

npx skillscat add getfunwindz/visio-style-diagram

Install via the SkillsCat registry.

SKILL.md

Visio-Style Diagram Generator

Generate Visio-style software engineering diagrams via draw.io Desktop.
Outputs .drawio (editable XML) + .png (max 1200px wide).

Skill files:

  • SKILL.md — this file (skill instruction)
  • README.md — full Chinese documentation with API reference
  • examples/er_diagram.py — complete runnable ER diagram generating script (5 diagrams)

All generated files go into a diagram/ subfolder for centralized management.

软件工程规范约束 (Software Engineering Standards)

1. 数据流图 (DFD) 规范

元素 符号 含义 约束
外部实体 矩形 系统外的人/组织/系统 必须位于图的边缘
加工 圆角矩形/椭圆 数据处理 必须有编号,有输入/输出流
数据流 带箭头直线 数据流动方向 必须有名称
数据存储 双横线/开口矩形 数据存储 必须通过数据流与加工连接

2. N-S盒图 规范

结构 表示
顺序 从上到下排列的矩形块
IF-THEN-ELSE 条件+左下THEN+右下ELSE
CASE 条件+N个分支列
WHILE-DO 顶部条件+循环体框
DO-UNTIL 循环体框+底部条件

3. 程序流程图 符号

符号 用途
圆角矩形 起止框
矩形 处理框
菱形 判断框
平行四边形 数据I/O
箭头 控制流

4. UML用例图 元素

元素 含义
Actor 火柴人 角色
Use Case 椭圆 功能单元
System Boundary 矩形 系统范围
关联/泛化/include/extend 关系连线

5. UML状态图 元素

初始状态(实心圆) → 状态(圆角矩形) → 转换(箭头[guard]) → 选择(菱形) → 最终状态(牛眼)

6. UML时序图 元素

角色/对象 → 生命线(虚线) → 激活条(窄矩形) → 同步/异步/返回消息

7. E-R图 规范

元素 符号
实体 矩形
属性 椭圆
关系 菱形(1:1 / 1:N / M:N)
主键 加粗/下划线

Visual Specification

Canvas: #EBEBEB | Entity: #FFFFFF + #DCDCDC border | Header: #C2CFF2
Field: #444444 9px | PK: bold | Separator: #E8E8E8
Connector: 1pt #999999 orthogonal | Cardinality: #555555 bold
Label: italic, centered, white bg

CJK Entity Height Formula

HEADER_H=28; ROW_H=20; ENTITY_W=190; COL_GAP=60; ROW_GAP=70
def calc_h(fields): return HEADER_H + len(fields) * ROW_H + 4

Collision-Free Routing Rules

  1. Pre-compute Y positions with column_y(heights, gap)
  2. Use explicit port positions (exitX/Y, entryX/Y)
  3. Obstacle-aware: 2-bend(right/left) → 3-bend(aisle) → 4-bend(detour)
  4. Row-aligned column grid
  5. validate() before export

See examples/ files for full API usage and diagram generators.

Example Implementation — EntityLayout Engine

import os, subprocess
import xml.sax.saxutils as xesc

OUT = r'output_directory\diagram'
DRAWIO = r'C:\Program Files\draw.io\draw.io'

CID = [1]
def nid(): CID[0] += 1; return str(CID[0])
def esc(s): return xesc.escape(str(s), {'"': '"'})

HEADER_H=28; ROW_H=20; ENTITY_W=190

def calc_h(fields): return HEADER_H + len(fields) * ROW_H + 4

def entity_html(title, fields, pk):
    rows = [
        '<tr><td style="background-color:#C2CFF2;text-align:center;font-weight:bold;font-size:11px;padding:4px;" colspan="2">'
        + esc(title) + '</td></tr>']
    for fn in fields:
        b = '<b>' + esc(fn) + '</b>' if fn in pk else esc(fn)
        rows.append('<tr><td style="border-top:1px solid #E8E8E8;font-size:9px;padding:3px 6px;" colspan="2">' + b + '</td></tr>')
    return '<table>' + ''.join(rows) + '</table>'

class EntityLayout:
    def __init__(self):
        self.entities=[]; self.cells=[]; self._map={}

    def add_entity(self, x, y, name, fields, pk_set=None, w=ENTITY_W):
        pk=pk_set or set(); h=calc_h(fields); eid=nid()
        html=entity_html(name, fields, pk)
        self.cells.append(
            '<mxCell id="'+eid+'" value="'+esc(html)+'" '
            'style="whiteSpace=wrap;html=1;rounded=0;fillColor=#FFFFFF;'
            'strokeColor=#DCDCDC;fontColor=#444444;fontSize=9;" vertex="1" parent="1">')
        self.cells.append(
            '    <mxGeometry x="'+str(x)+'" y="'+str(y)+'" '
            'width="'+str(w)+'" height="'+str(h)+'" as="geometry"/>')
        self.cells.append('  </mxCell>')
        rect=(x,y,w,h)
        self.entities.append((eid,name,rect))
        self._map[eid]=rect
        return eid

    def add_connector(self, src, dst, label='', c1='', c2='', sx=1, sy=0.5, tx=0, ty=0.5):
        sr=self._map[src]; dr=self._map[dst]
        ex=sr[0]+sx*sr[2]; ey=sr[1]+sy*sr[3]
        nx=dr[0]+tx*dr[2]; ny=dr[1]+ty*dr[3]
        ob=[r for e,_,r in self.entities if e not in (src,dst)]
        wpts=self._route(ex,ey,nx,ny,ob)
        eid=nid(); lbl=esc(label) if label else ''
        style=('edgeStyle=orthogonalEdgeStyle;rounded=0;html=1;'
               'strokeColor=#999999;strokeWidth=1;fontSize=10;'
               'fontColor=#444444;fontStyle=2;'
               'labelBackgroundColor=#FFFFFF;endArrow=classic;endFill=1;'
               'exitX={};exitY={};entryX={};entryY={};').format(sx,sy,tx,ty)
        self.cells.append(
            '<mxCell id="'+eid+'" value="'+lbl+'" style="'+style
            +'" edge="1" parent="1" source="'+src+'" target="'+dst+'">')
        if wpts:
            pts=''.join('        <mxPoint x="{}" y="{}"/>\n'.format(x,y) for x,y in wpts)
            self.cells.append('    <mxGeometry relative="1" as="geometry">\n'+pts+'    </mxGeometry>')
        else:
            self.cells.append('    <mxGeometry relative="1" as="geometry"/>')
        self.cells.append('  </mxCell>')
        # Cardinality labels at waypoint midpoint
        if c1 or c2:
            if wpts: mi=len(wpts)//2; mx_pt,my_pt=wpts[mi]
            else: mx_pt,my_pt=(ex+nx)/2,(ey+ny)/2
            for txt,xoff in [(c1,-30),(c2,30)]:
                if not txt: continue
                ci=nid()
                self.cells.append(
                    '<mxCell id="'+ci+'" value="'+esc(txt)+'" '
                    'style="text;html=1;fontSize=10;fontStyle=1;fontColor=#555555;'
                    'align=center;verticalAlign=middle;" vertex="1" connectable="0" parent="1">')
                self.cells.append(
                    '    <mxGeometry x="'+str(mx_pt+xoff-10)+'" y="'+str(my_pt-10)+'" '
                    'width="20" height="20" as="geometry"/>')
                self.cells.append('  </mxCell>')

    @staticmethod
    def _line_hits(x1,y1,x2,y2,r):
        rx,ry,rw,rh=r; eps=1  # 1px epsilon prevents boundary false-positives
        if x1==x2:
            if not(rx-eps<=x1<=rx+rw+eps): return False
            return not(max(y1,y2)<=ry-eps or min(y1,y2)>=ry+rh+eps)
        if y1==y2:
            if not(ry-eps<=y1<=ry+rh+eps): return False
            return not(max(x1,x2)<=rx-eps or min(x1,x2)>=rx+rw+eps)
        return False

    @staticmethod
    def _path_ok(pts,ob):
        for i in range(len(pts)-1):
            for r in ob:
                if EntityLayout._line_hits(pts[i][0],pts[i][1],pts[i+1][0],pts[i+1][1],r): return False
        return True

    def _route(self,ex,ey,nx,ny,ob):
        if not ob: return []
        ro=max(r[0]+r[2] for r in ob)+80
        lo=min(r[0] for r in ob)-80
        to=min(r[1] for r in ob)-60
        bo=max(r[1]+r[3] for r in ob)+60
        for my in [ny,ey,(ey+ny)/2,to+30,bo-30]:
            p=[(ex,ey),(ro,my),(nx,ny)]
            if self._path_ok(p,ob): return [(ro,my)]
        for my in [ny,ey,(ey+ny)/2,to+30,bo-30]:
            p=[(ex,ey),(lo,my),(nx,ny)]
            if self._path_ok(p,ob): return [(lo,my)]
        for my in [to,bo,(to+bo)/2]:
            for mx in [ex,nx,ro]:
                p=[(ex,ey),(ex,my),(mx,my),(nx,ny)]
                if self._path_ok(p,ob): return [(ex,my),(mx,my)]
        for my in [to,bo,(to+bo)/2]:
            for mx in [ex,nx,lo]:
                p=[(ex,ey),(ex,my),(mx,my),(nx,ny)]
                if self._path_ok(p,ob): return [(ex,my),(mx,my)]
        p=[(ex,ey),(ex,to),(ro,to),(ro,ny),(nx,ny)]
        if self._path_ok(p,ob): return [(ex,to),(ro,to),(ro,ny)]
        p=[(ex,ey),(ex,bo),(ro,bo),(ro,ny),(nx,ny)]
        if self._path_ok(p,ob): return [(ex,bo),(ro,bo),(ro,ny)]
        p=[(ex,ey),(ex,to),(lo,to),(lo,ny),(nx,ny)]
        if self._path_ok(p,ob): return [(ex,to),(lo,to),(lo,ny)]
        p=[(ex,ey),(ex,bo),(lo,bo),(lo,ny),(nx,ny)]
        if self._path_ok(p,ob): return [(ex,bo),(lo,bo),(lo,ny)]
        return [(ro,ey),(ro,ny)]

    def validate(self):
        issues=[]
        for i,(_,n1,r1) in enumerate(self.entities):
            for j,(_,n2,r2) in enumerate(self.entities):
                if i>=j: continue
                x1,y1,w1,h1=r1; x2,y2,w2,h2=r2
                if not(x1+w1<=x2 or x1>=x2+w2 or y1+h1<=y2 or y1>=y2+h2):
                    issues.append(f'  OVERLAP: "{n1}" <-> "{n2}"')
        return issues

    def to_xml(self):
        return '<mxfile host="Electron" modified="2026-06-15T00:00:00.000Z" agent="Mozilla/5.0" version="30.0.4"><diagram name="d1"><mxGraphModel pageWidth="2400" pageHeight="1800"><root><mxCell id="0"/><mxCell id="1" parent="0"/>'+'\n'.join(self.cells)+'</root></mxGraphModel></diagram></mxfile>'

    def export(self, subdir, name):
        d=os.path.join(OUT,subdir); os.makedirs(d,exist_ok=True)
        src=os.path.join(d,name+'.drawio')
        with open(src,'w',encoding='utf-8') as f: f.write(self.to_xml())
        dst=src.replace('.drawio','.png')
        subprocess.run([DRAWIO,'--export','--format','png','--width','1200','--border','16','--output',dst,src],capture_output=True,timeout=30)
        return dst