iac/docs/architecture/render_preview.py

158 lines
7.6 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.

"""Рендерит представления из .archimate в один самодостаточный HTML со встроенным SVG.
Нужен, чтобы схемы можно было посмотреть без установки Archi."""
import sys, io, pathlib, html
import xml.etree.ElementTree as ET
sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding="utf-8")
XSI = "{http://www.w3.org/2001/XMLSchema-instance}type"
src, dst = pathlib.Path(sys.argv[1]), pathlib.Path(sys.argv[2])
root = ET.parse(src).getroot()
LAYER = { # цвета Archi по слоям
"ApplicationComponent": "#B5FFFF", "ApplicationService": "#B5FFFF",
"Node": "#C9E7B7", "SystemSoftware": "#C9E7B7",
"TechnologyService": "#C9E7B7", "Artifact": "#C9E7B7",
}
STROKE = {"Application": "#2a7f8f", "Technology": "#4a7f3a"}
concepts, rels = {}, {}
for folder in root.findall("folder"):
for e in folder.findall("element"):
et = (e.get(XSI) or "").split(":")[-1]
if et.endswith("Relationship"):
rels[e.get("id")] = (et[:-len("Relationship")], e.get("source"), e.get("target"))
elif et != "ArchimateDiagramModel":
concepts[e.get("id")] = (et, e.get("name") or "")
def collect(node, ox, oy, nodes, conns):
b = node.find("bounds")
x, y = ox + int(b.get("x", 0)), oy + int(b.get("y", 0))
w, h = int(b.get("width", 120)), int(b.get("height", 55))
nodes[node.get("id")] = (node.get("archimateElement"), x, y, w, h)
for c in node.findall("sourceConnection"):
conns.append((c.get("relationship"), c.get("source"), c.get("target")))
for ch in node.findall("child"):
collect(ch, x, y, nodes, conns)
def wrap(s, width):
out, line = [], ""
for word in s.split():
if len(line) + len(word) + 1 > width and line:
out.append(line); line = word
else:
line = f"{line} {word}".strip()
if line:
out.append(line)
return out[:4]
def edge_point(x, y, w, h, tx, ty):
"""Точка пересечения луча к (tx,ty) с границей прямоугольника."""
cx, cy = x + w / 2, y + h / 2
dx, dy = tx - cx, ty - cy
if dx == 0 and dy == 0:
return cx, cy
sx = (w / 2) / abs(dx) if dx else float("inf")
sy = (h / 2) / abs(dy) if dy else float("inf")
s = min(sx, sy)
return cx + dx * s, cy + dy * s
MARKERS = """
<defs>
<marker id="open" markerWidth="12" markerHeight="12" refX="10" refY="4" orient="auto">
<path d="M0,0 L10,4 L0,8" fill="none" stroke="#555" stroke-width="1.4"/></marker>
<marker id="hollow" markerWidth="14" markerHeight="14" refX="11" refY="5" orient="auto">
<path d="M0,0 L11,5 L0,10 Z" fill="#fff" stroke="#555" stroke-width="1.2"/></marker>
<marker id="filled" markerWidth="12" markerHeight="12" refX="10" refY="4" orient="auto">
<path d="M0,0 L10,4 L0,8 Z" fill="#555"/></marker>
<marker id="diamond" markerWidth="16" markerHeight="12" refX="1" refY="5" orient="auto">
<path d="M0,5 L6,1 L13,5 L6,9 Z" fill="#555"/></marker>
<marker id="ball" markerWidth="10" markerHeight="10" refX="2" refY="5" orient="auto">
<circle cx="4" cy="5" r="3.2" fill="#555"/></marker>
</defs>"""
STYLE = {
"Serving": ('stroke-dasharray="none"', 'marker-end="url(#open)"', ""),
"Realization": ('stroke-dasharray="6,4"', 'marker-end="url(#hollow)"', ""),
"Composition": ('stroke-dasharray="none"', "", 'marker-start="url(#diamond)"'),
"Assignment": ('stroke-dasharray="none"', 'marker-end="url(#filled)"', 'marker-start="url(#ball)"'),
}
parts = []
views = [e for f in root.findall("folder") for e in f.findall("element")
if (e.get(XSI) or "").endswith("ArchimateDiagramModel")]
for v in views:
nodes, conns = {}, []
for ch in v.findall("child"):
collect(ch, 0, 0, nodes, conns)
if not nodes:
continue
maxx = max(x + w for _, x, _, w, _ in nodes.values()) + 40
maxy = max(y + h for _, _, y, _, h in nodes.values()) + 40
svg = [f'<svg viewBox="0 0 {maxx} {maxy}" xmlns="http://www.w3.org/2000/svg" '
f'font-family="system-ui, sans-serif">', MARKERS]
# связи под блоками
for rid, so, to in conns:
if so not in nodes or to not in nodes or rid not in rels:
continue
rtype = rels[rid][0]
_, sx, sy, sw, sh = nodes[so]
_, tx, ty, tw, th = nodes[to]
x1, y1 = edge_point(sx, sy, sw, sh, tx + tw / 2, ty + th / 2)
x2, y2 = edge_point(tx, ty, tw, th, sx + sw / 2, sy + sh / 2)
dash, mend, mstart = STYLE.get(rtype, ('stroke-dasharray="none"', 'marker-end="url(#open)"', ""))
svg.append(f'<line x1="{x1:.0f}" y1="{y1:.0f}" x2="{x2:.0f}" y2="{y2:.0f}" '
f'stroke="#555" stroke-width="1.3" opacity="0.75" {dash} {mend} {mstart}/>')
# блоки: контейнеры раньше содержимого — сортируем по площади убыв.
for nid, (eref, x, y, w, h) in sorted(nodes.items(), key=lambda kv: -kv[1][3] * kv[1][4]):
etype, name = concepts.get(eref, ("", eref or "?"))
fill = LAYER.get(etype, "#EEEEEE")
stroke = STROKE["Application"] if etype.startswith("Application") else STROKE["Technology"]
big = w * h > 60000
svg.append(f'<rect x="{x}" y="{y}" width="{w}" height="{h}" rx="4" fill="{fill}" '
f'fill-opacity="{0.35 if big else 1}" stroke="{stroke}" stroke-width="1.2"/>')
lines = wrap(name, max(12, w // 7))
if big: # подпись контейнера — сверху слева
svg.append(f'<text x="{x+10}" y="{y+20}" font-size="13" font-weight="600" '
f'fill="#1a3a1a">{html.escape(name)}</text>')
else:
fs = 11 if len(lines) > 2 else 12
y0 = y + h / 2 - (len(lines) - 1) * fs * 0.62
for i, ln in enumerate(lines):
svg.append(f'<text x="{x + w/2}" y="{y0 + i*fs*1.25:.0f}" font-size="{fs}" '
f'text-anchor="middle" dominant-baseline="middle" fill="#111">'
f'{html.escape(ln)}</text>')
svg.append("</svg>")
doc = v.find("documentation")
parts.append(f'<section><h2>{html.escape(v.get("name") or "")}</h2>'
+ (f'<p class="doc">{html.escape(doc.text)}</p>' if doc is not None and doc.text else "")
+ "".join(svg) + "</section>")
page = f"""<!doctype html>
<html lang="ru"><meta charset="utf-8">
<title>{html.escape(root.get('name') or 'Модель')}</title>
<style>
body {{ font-family: system-ui, sans-serif; margin: 0 auto; padding: 24px; max-width: 1760px;
background:#fafafa; color:#111; }}
h1 {{ font-size: 22px; }} h2 {{ font-size: 16px; margin: 32px 0 4px; }}
.doc {{ color:#555; font-size:13px; margin:0 0 10px; max-width:900px; }}
section {{ background:#fff; border:1px solid #ddd; border-radius:6px; padding:14px; margin-bottom:18px;
overflow-x:auto; }}
svg {{ width:100%; height:auto; min-width:700px; }}
.legend {{ font-size:13px; color:#444; }}
.legend code {{ background:#eee; padding:1px 5px; border-radius:3px; }}
</style>
<h1>{html.escape(root.get('name') or '')}</h1>
<p class="legend">Автогенерация из <code>{html.escape(src.name)}</code>. Зелёный — технологический слой,
голубой — прикладной. Пунктир с полым треугольником — realization, стрелка — serving,
ромб — composition, линия с шариком — assignment.</p>
{''.join(parts)}
</html>"""
dst.write_text(page, encoding="utf-8", newline="\n")
print(f"записано: {dst} ({len(parts)} представлений)")