Text Generation
Transformers
Safetensors
English
lfm2
text-generation-inference
unsloth
conversational
Instructions to use smjain/sap-archgen-lfm2-230M with libraries, inference providers, notebooks, and local apps. Follow these links to get started.
- Libraries
- Transformers
How to use smjain/sap-archgen-lfm2-230M with Transformers:
# Use a pipeline as a high-level helper from transformers import pipeline pipe = pipeline("text-generation", model="smjain/sap-archgen-lfm2-230M") messages = [ {"role": "user", "content": "Who are you?"}, ] pipe(messages)# Load model directly from transformers import AutoTokenizer, AutoModelForCausalLM tokenizer = AutoTokenizer.from_pretrained("smjain/sap-archgen-lfm2-230M") model = AutoModelForCausalLM.from_pretrained("smjain/sap-archgen-lfm2-230M", device_map="auto") messages = [ {"role": "user", "content": "Who are you?"}, ] inputs = tokenizer.apply_chat_template( messages, add_generation_prompt=True, tokenize=True, return_dict=True, return_tensors="pt", ).to(model.device) outputs = model.generate(**inputs, max_new_tokens=40) print(tokenizer.decode(outputs[0][inputs["input_ids"].shape[-1]:])) - Notebooks
- Google Colab
- Kaggle
- Local Apps Settings
- vLLM
How to use smjain/sap-archgen-lfm2-230M with vLLM:
Install from pip and serve model
# Install vLLM from pip: pip install vllm # Start the vLLM server: vllm serve "smjain/sap-archgen-lfm2-230M" # Call the server using curl (OpenAI-compatible API): curl -X POST "http://localhost:8000/v1/chat/completions" \ -H "Content-Type: application/json" \ --data '{ "model": "smjain/sap-archgen-lfm2-230M", "messages": [ { "role": "user", "content": "What is the capital of France?" } ] }'Use Docker
docker model run hf.co/smjain/sap-archgen-lfm2-230M
- SGLang
How to use smjain/sap-archgen-lfm2-230M with SGLang:
Install from pip and serve model
# Install SGLang from pip: pip install sglang # Start the SGLang server: python3 -m sglang.launch_server \ --model-path "smjain/sap-archgen-lfm2-230M" \ --host 0.0.0.0 \ --port 30000 # Call the server using curl (OpenAI-compatible API): curl -X POST "http://localhost:30000/v1/chat/completions" \ -H "Content-Type: application/json" \ --data '{ "model": "smjain/sap-archgen-lfm2-230M", "messages": [ { "role": "user", "content": "What is the capital of France?" } ] }'Use Docker images
docker run --gpus all \ --shm-size 32g \ -p 30000:30000 \ -v ~/.cache/huggingface:/root/.cache/huggingface \ --env "HF_TOKEN=<secret>" \ --ipc=host \ lmsysorg/sglang:latest \ python3 -m sglang.launch_server \ --model-path "smjain/sap-archgen-lfm2-230M" \ --host 0.0.0.0 \ --port 30000 # Call the server using curl (OpenAI-compatible API): curl -X POST "http://localhost:30000/v1/chat/completions" \ -H "Content-Type: application/json" \ --data '{ "model": "smjain/sap-archgen-lfm2-230M", "messages": [ { "role": "user", "content": "What is the capital of France?" } ] }' - Unsloth Desktop
- Docker Model Runner
How to use smjain/sap-archgen-lfm2-230M with Docker Model Runner:
docker model run hf.co/smjain/sap-archgen-lfm2-230M
Upload layout_engine.py with huggingface_hub
Browse files- layout_engine.py +311 -0
layout_engine.py
ADDED
|
@@ -0,0 +1,311 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env python3
|
| 2 |
+
"""Semantic spec -> clean, valid SAP .drawio. The model's job shrinks to emitting
|
| 3 |
+
typed components + zones + edges; THIS engine does deterministic graphviz layout
|
| 4 |
+
and snaps labels to real SAP icon keys. No pixel coords or style keys from the model.
|
| 5 |
+
|
| 6 |
+
spec = {
|
| 7 |
+
"title": str,
|
| 8 |
+
"zones": [{"id": str, "label": str, "children": [node_id,...]}],
|
| 9 |
+
"nodes": [{"id": str, "label": str, "icon": optional key}],
|
| 10 |
+
"edges": [{"source": id, "target": id, "label": optional}]
|
| 11 |
+
}
|
| 12 |
+
"""
|
| 13 |
+
import json, re, subprocess, difflib
|
| 14 |
+
from xml.sax.saxutils import quoteattr
|
| 15 |
+
|
| 16 |
+
_LIB = json.load(open("dataset/style_library.json"))
|
| 17 |
+
_ICON_KEYS = [k for k in _LIB if not k.startswith(("box", "edge"))]
|
| 18 |
+
|
| 19 |
+
def slug(s):
|
| 20 |
+
return re.sub(r"[^a-z0-9]+", "_", (s or "").lower()).strip("_")
|
| 21 |
+
|
| 22 |
+
def snap_icon(label, given=None):
|
| 23 |
+
"""Return a library key for this label (given key wins, else fuzzy match)."""
|
| 24 |
+
if given and given in _LIB:
|
| 25 |
+
return given
|
| 26 |
+
s = slug(label)
|
| 27 |
+
if s in _LIB:
|
| 28 |
+
return s
|
| 29 |
+
m = difflib.get_close_matches(s, _ICON_KEYS, n=1, cutoff=0.72)
|
| 30 |
+
return m[0] if m else None
|
| 31 |
+
|
| 32 |
+
def _dot_layout(spec):
|
| 33 |
+
"""Run graphviz dot, return {node_id: (x,y,w,h)} in pixels, top-left origin."""
|
| 34 |
+
lines = ['digraph G {', 'rankdir=LR; nodesep=0.5; ranksep=0.9;',
|
| 35 |
+
'node [shape=box, fixedsize=true, width=1.1, height=0.7];']
|
| 36 |
+
inzone = set()
|
| 37 |
+
for z in spec.get("zones", []):
|
| 38 |
+
lines.append(f'subgraph cluster_{slug(z["id"])} {{ label={json.dumps(z.get("label",""))};')
|
| 39 |
+
for c in z.get("children", []):
|
| 40 |
+
lines.append(f'"{c}";'); inzone.add(c)
|
| 41 |
+
lines.append('}')
|
| 42 |
+
for n in spec["nodes"]:
|
| 43 |
+
lines.append(f'"{n["id"]}" [label={json.dumps(n["label"])}];')
|
| 44 |
+
for e in spec.get("edges", []):
|
| 45 |
+
lines.append(f'"{e["source"]}" -> "{e["target"]}";')
|
| 46 |
+
lines.append('}')
|
| 47 |
+
dot = "\n".join(lines)
|
| 48 |
+
plain = subprocess.run(["dot", "-Tplain"], input=dot, capture_output=True, text=True).stdout
|
| 49 |
+
scale = 1.0; H = 0.0; pos = {}
|
| 50 |
+
for ln in plain.splitlines():
|
| 51 |
+
p = ln.split()
|
| 52 |
+
if p[0] == "graph":
|
| 53 |
+
scale = float(p[1]); H = float(p[3])
|
| 54 |
+
elif p[0] == "node":
|
| 55 |
+
name = p[1].strip('"')
|
| 56 |
+
x, y, w, h = map(float, p[2:6])
|
| 57 |
+
# graphviz: inches, origin bottom-left -> pixels, top-left
|
| 58 |
+
PX = 72
|
| 59 |
+
pos[name] = (int((x - w/2)*PX), int((H - y - h/2)*PX), int(w*PX), int(h*PX))
|
| 60 |
+
return pos
|
| 61 |
+
|
| 62 |
+
def _grid_layout(spec):
|
| 63 |
+
"""Zone-column layout with SMALLER icons + an OUTER wrapping block. zones =
|
| 64 |
+
columns L->R inside the outer block; nodes stacked. Returns node pos, zone
|
| 65 |
+
rects, and the outer-block rect."""
|
| 66 |
+
NW, NH, GAP, PAD, TITLE, COLGAP, TOP, MARGIN = 96, 54, 30, 18, 30, 46, 64, 30
|
| 67 |
+
zones = spec.get("zones", [])
|
| 68 |
+
zoned = set()
|
| 69 |
+
for z in zones:
|
| 70 |
+
zoned.update(z["children"])
|
| 71 |
+
unzoned = [n["id"] for n in spec["nodes"] if n["id"] not in zoned]
|
| 72 |
+
columns = ([{"id": None, "label": None, "children": unzoned}] if unzoned else []) + \
|
| 73 |
+
[{"id": z["id"], "label": z.get("label", "Zone"), "children": z["children"]} for z in zones]
|
| 74 |
+
pos, zrects = {}, []
|
| 75 |
+
x = TOP + MARGIN
|
| 76 |
+
maxy = TOP
|
| 77 |
+
for col in columns:
|
| 78 |
+
kids = [k for k in col["children"] if any(n["id"] == k for n in spec["nodes"])]
|
| 79 |
+
if not kids:
|
| 80 |
+
continue
|
| 81 |
+
col_w = NW + 2*PAD
|
| 82 |
+
y = TOP + MARGIN + (TITLE if col["id"] else 0) + PAD
|
| 83 |
+
for k in kids:
|
| 84 |
+
pos[k] = (x + PAD, y, NW, NH)
|
| 85 |
+
y += NH + GAP
|
| 86 |
+
if col["id"]:
|
| 87 |
+
zrects.append({"label": col["label"], "rect": [x, TOP + MARGIN, col_w, y - (TOP+MARGIN) - GAP + PAD]})
|
| 88 |
+
maxy = max(maxy, y)
|
| 89 |
+
x += col_w + COLGAP
|
| 90 |
+
outer = [TOP, TOP, x - COLGAP - TOP + MARGIN, maxy - TOP - GAP + PAD + MARGIN]
|
| 91 |
+
return pos, zrects, outer
|
| 92 |
+
|
| 93 |
+
def build(spec, out_path="spec_output.drawio"):
|
| 94 |
+
pos, zrects, outer = _grid_layout(spec)
|
| 95 |
+
cells = ['<mxCell id="0" />', '<mxCell id="1" parent="0" />']
|
| 96 |
+
# outer surrounding block (behind everything)
|
| 97 |
+
ox, oy, ow, oh = outer
|
| 98 |
+
cells.append(
|
| 99 |
+
f'<mxCell id="outer" value={quoteattr(spec.get("title",""))} '
|
| 100 |
+
f'style="rounded=0;whiteSpace=wrap;html=1;fillColor=#fbfdff;strokeColor=#7a9bbe;'
|
| 101 |
+
f'verticalAlign=top;fontSize=16;fontStyle=1;fontColor=#13334f;dashed=0;" '
|
| 102 |
+
f'vertex="1" parent="1"><mxGeometry x="{ox}" y="{oy}" width="{ow}" height="{oh}" as="geometry" /></mxCell>')
|
| 103 |
+
zid = 1000
|
| 104 |
+
for z in zrects:
|
| 105 |
+
x0, y0, w0, h0 = z["rect"]; zid += 1
|
| 106 |
+
cells.append(
|
| 107 |
+
f'<mxCell id="z{zid}" value={quoteattr(z.get("label") or "")} '
|
| 108 |
+
f'style="rounded=0;whiteSpace=wrap;html=1;fillColor=#eef5fc;strokeColor=#9fc5e8;'
|
| 109 |
+
f'verticalAlign=top;fontSize=13;fontStyle=1;fontColor=#1a3b5c;" vertex="1" parent="1">'
|
| 110 |
+
f'<mxGeometry x="{x0}" y="{y0}" width="{w0}" height="{h0}" as="geometry" /></mxCell>')
|
| 111 |
+
idmap = {}; nid = 1
|
| 112 |
+
for n in spec["nodes"]:
|
| 113 |
+
if n["id"] not in pos:
|
| 114 |
+
continue
|
| 115 |
+
x, y, w, h = pos[n["id"]]
|
| 116 |
+
nid += 1; idmap[n["id"]] = f"n{nid}"
|
| 117 |
+
key = snap_icon(n["label"], n.get("icon"))
|
| 118 |
+
if key:
|
| 119 |
+
style = _LIB[key].rstrip(";") + ";verticalLabelPosition=bottom;verticalAlign=top;"
|
| 120 |
+
else:
|
| 121 |
+
style = "rounded=1;whiteSpace=wrap;html=1;fillColor=#ffffff;strokeColor=#5b7a9c;"
|
| 122 |
+
cells.append(
|
| 123 |
+
f'<mxCell id="n{nid}" value={quoteattr(n["label"])} style={quoteattr(style)} '
|
| 124 |
+
f'vertex="1" parent="1"><mxGeometry x="{x}" y="{y}" width="{w}" height="{h}" '
|
| 125 |
+
f'as="geometry" /></mxCell>')
|
| 126 |
+
# edges
|
| 127 |
+
eid = 5000
|
| 128 |
+
for e in spec.get("edges", []):
|
| 129 |
+
if e["source"] not in idmap or e["target"] not in idmap:
|
| 130 |
+
continue
|
| 131 |
+
eid += 1
|
| 132 |
+
lab = e.get("label", "")
|
| 133 |
+
va = f' value={quoteattr(lab)}' if lab else ''
|
| 134 |
+
cells.append(
|
| 135 |
+
f'<mxCell id="e{eid}"{va} style="edgeStyle=orthogonalEdgeStyle;rounded=0;html=1;'
|
| 136 |
+
f'strokeColor=#5b7a9c;" edge="1" parent="1" source={quoteattr(idmap[e["source"]])} '
|
| 137 |
+
f'target={quoteattr(idmap[e["target"]])}><mxGeometry relative="1" as="geometry" /></mxCell>')
|
| 138 |
+
xml = (f'<mxfile host="archgen"><diagram name={quoteattr(spec.get("title","Architecture"))}>'
|
| 139 |
+
f'<mxGraphModel dx="1200" dy="800" grid="0" guides="1" tooltips="1" connect="1" '
|
| 140 |
+
f'arrows="1" fold="1" page="1" pageScale="1" pageWidth="1600" pageHeight="1100" '
|
| 141 |
+
f'math="0" shadow="0"><root>' + "".join(cells) +
|
| 142 |
+
'</root></mxGraphModel></diagram></mxfile>')
|
| 143 |
+
open(out_path, "w").write(xml)
|
| 144 |
+
return out_path
|
| 145 |
+
|
| 146 |
+
# ---------------- recursive nested-block layout (hierarchical spec) ----------------
|
| 147 |
+
# wide cell (gives the label room) + SMALL icon; row pitch leaves space for the
|
| 148 |
+
# bottom-positioned 2-line label so it never overlaps the next icon.
|
| 149 |
+
ICON_W, ICON_H, BPAD, BTITLE = 124, 40, 20, 34
|
| 150 |
+
COLGAP, ROWGAP, LABEL_H = 34, 26, 34
|
| 151 |
+
BGAP = COLGAP # block-to-block spacing
|
| 152 |
+
ROW_PITCH = ICON_H + LABEL_H + ROWGAP
|
| 153 |
+
|
| 154 |
+
def _measure(block):
|
| 155 |
+
cw, ch = [], []
|
| 156 |
+
for sub in block.get("blocks", []):
|
| 157 |
+
_measure(sub); cw.append(sub["_w"]); ch.append(sub["_h"])
|
| 158 |
+
comps = block.get("components", [])
|
| 159 |
+
if comps:
|
| 160 |
+
ncol = 1 if len(comps) <= 5 else 2
|
| 161 |
+
rows = (len(comps) + ncol - 1) // ncol
|
| 162 |
+
clw = ncol*ICON_W + (ncol-1)*COLGAP
|
| 163 |
+
clh = rows*ROW_PITCH - ROWGAP # no trailing gap; last row keeps its label
|
| 164 |
+
block["_cluster"] = (ncol, clw, clh); cw.append(clw); ch.append(clh)
|
| 165 |
+
inner_w = (sum(cw) + COLGAP*(len(cw)-1)) if cw else ICON_W
|
| 166 |
+
inner_h = max(ch) if ch else ICON_H
|
| 167 |
+
block["_w"] = inner_w + 2*BPAD
|
| 168 |
+
block["_h"] = inner_h + BTITLE + BPAD
|
| 169 |
+
|
| 170 |
+
def _place(block, x, y, pos, rects, depth=0):
|
| 171 |
+
rects.append({"label": block.get("label", ""), "rect": [x, y, block["_w"], block["_h"]], "depth": depth})
|
| 172 |
+
cx = x + BPAD; cy = y + BTITLE
|
| 173 |
+
for sub in block.get("blocks", []):
|
| 174 |
+
_place(sub, cx, cy, pos, rects, depth+1); cx += sub["_w"] + COLGAP
|
| 175 |
+
comps = block.get("components", [])
|
| 176 |
+
if comps:
|
| 177 |
+
ncol = block["_cluster"][0]
|
| 178 |
+
for i, comp in enumerate(comps):
|
| 179 |
+
r, c = divmod(i, ncol)
|
| 180 |
+
pos[comp["id"]] = (cx + c*(ICON_W+COLGAP), cy + r*ROW_PITCH, ICON_W, ICON_H)
|
| 181 |
+
|
| 182 |
+
def compute_layout(spec):
|
| 183 |
+
"""Return {component_id: (x, y, w, h)} icon-cell positions (no file written)."""
|
| 184 |
+
return compute_full(spec)[0]
|
| 185 |
+
|
| 186 |
+
def compute_full(spec):
|
| 187 |
+
"""Return (pos {id:(x,y,w,h)}, block_rects [{label,rect,depth}]) — for streaming/render."""
|
| 188 |
+
import copy as _copy
|
| 189 |
+
blocks = _copy.deepcopy(spec.get("blocks", []))
|
| 190 |
+
for b in blocks: _measure(b)
|
| 191 |
+
pos, rects = {}, []; x = 50
|
| 192 |
+
for b in blocks:
|
| 193 |
+
_place(b, x, 60, pos, rects, 1); x += b["_w"] + 50
|
| 194 |
+
return pos, rects
|
| 195 |
+
|
| 196 |
+
def layout_issues(spec, tol=4):
|
| 197 |
+
"""Visual sanity check: flag any two component boxes (icon + label area) that overlap.
|
| 198 |
+
The engine is overlap-free by construction, so this should always pass."""
|
| 199 |
+
pos = compute_layout(spec)
|
| 200 |
+
# each node occupies icon cell + the label strip below it (LABEL_H tall)
|
| 201 |
+
boxes = {cid: (x, y, w, ICON_H + LABEL_H) for cid, (x, y, w, h) in pos.items()}
|
| 202 |
+
ids = list(boxes); out = []
|
| 203 |
+
for i in range(len(ids)):
|
| 204 |
+
ax, ay, aw, ah = boxes[ids[i]]
|
| 205 |
+
for j in range(i + 1, len(ids)):
|
| 206 |
+
bx, by, bw, bh = boxes[ids[j]]
|
| 207 |
+
if ax < bx+bw-tol and bx < ax+aw-tol and ay < by+bh-tol and by < ay+ah-tol:
|
| 208 |
+
out.append(f"layout overlap: {ids[i]} & {ids[j]}")
|
| 209 |
+
return out[:6]
|
| 210 |
+
|
| 211 |
+
def _fix_img(style):
|
| 212 |
+
# browsers/mxGraph need ;base64 for base64 data URIs (draw.io tolerates the comma form)
|
| 213 |
+
return re.sub(r"data:image/(svg\+xml|png|jpeg),", r"data:image/\1;base64,", style)
|
| 214 |
+
|
| 215 |
+
def stream_cells(spec):
|
| 216 |
+
"""Real draw.io cells (blocks → nodes → edges) in build order, with EXACT styles +
|
| 217 |
+
geometry — for incremental insertion into an mxGraph (pixel-identical to build_hier)."""
|
| 218 |
+
pos, rects = compute_full(spec)
|
| 219 |
+
fills = ["#eef5fc", "#e4eefb", "#dbe8f8"]
|
| 220 |
+
idlabel = {}
|
| 221 |
+
def walk(bs):
|
| 222 |
+
for b in bs:
|
| 223 |
+
for c in b.get("components", []): idlabel[c["id"]] = c["label"]
|
| 224 |
+
walk(b.get("blocks", []))
|
| 225 |
+
walk(spec.get("blocks", []))
|
| 226 |
+
cells = []
|
| 227 |
+
for i, z in enumerate(sorted(rects, key=lambda r: r["depth"])):
|
| 228 |
+
x, y, w, h = z["rect"]; d = z["depth"]
|
| 229 |
+
fc = fills[min(d-1, len(fills)-1)] if d >= 1 else "#ffffff"
|
| 230 |
+
cells.append({"id": f"b{i}", "vertex": True, "value": z.get("label") or "",
|
| 231 |
+
"x": x, "y": y, "w": w, "h": h,
|
| 232 |
+
"style": f"rounded=0;whiteSpace=wrap;html=1;fillColor={fc};strokeColor=#9fc5e8;"
|
| 233 |
+
f"verticalAlign=top;fontSize=12;fontStyle=1;fontColor=#1a3b5c;"})
|
| 234 |
+
nmap = {}
|
| 235 |
+
for cid, (x, y, w, h) in pos.items():
|
| 236 |
+
lbl = idlabel.get(cid, ""); key = snap_icon(lbl)
|
| 237 |
+
# use the EXACT style build_hier uses (draw.io renders the original comma data-URI form)
|
| 238 |
+
style = (_LIB[key].rstrip(";") + ";verticalLabelPosition=bottom;verticalAlign=top;"
|
| 239 |
+
"imageAlign=center;align=center;whiteSpace=wrap;") if key \
|
| 240 |
+
else "rounded=1;whiteSpace=wrap;html=1;fillColor=#ffffff;strokeColor=#5b7a9c;align=center;"
|
| 241 |
+
nmap[cid] = f"n{cid}"
|
| 242 |
+
cells.append({"id": nmap[cid], "vertex": True, "value": lbl,
|
| 243 |
+
"x": x, "y": y, "w": w, "h": ICON_H, "style": style})
|
| 244 |
+
for i, e in enumerate(spec.get("connections", [])):
|
| 245 |
+
if e["source"] in nmap and e["target"] in nmap:
|
| 246 |
+
cells.append({"id": f"e{i}", "edge": True, "value": e.get("label", ""),
|
| 247 |
+
"source": nmap[e["source"]], "target": nmap[e["target"]],
|
| 248 |
+
"style": "edgeStyle=orthogonalEdgeStyle;rounded=0;html=1;strokeColor=#42607d;endArrow=block;"})
|
| 249 |
+
return cells
|
| 250 |
+
|
| 251 |
+
def build_hier(spec, out_path="hier_output.drawio"):
|
| 252 |
+
import copy as _copy
|
| 253 |
+
blocks = _copy.deepcopy(spec.get("blocks", [])) # don't pollute caller's spec with _w/_h
|
| 254 |
+
for b in blocks: _measure(b)
|
| 255 |
+
pos, rects = {}, []
|
| 256 |
+
x = 50
|
| 257 |
+
maxh = 0
|
| 258 |
+
placed = []
|
| 259 |
+
for b in blocks:
|
| 260 |
+
_place(b, x, 60, pos, rects, 1); x += b["_w"] + 50; maxh = max(maxh, b["_h"])
|
| 261 |
+
# outer wrapper
|
| 262 |
+
ow = x - 50 - 50 + 2*30; oh = maxh + 2*30
|
| 263 |
+
cells = ['<mxCell id="0" />', '<mxCell id="1" parent="0" />']
|
| 264 |
+
cells.append(f'<mxCell id="outer" value={quoteattr(spec.get("title",""))} '
|
| 265 |
+
f'style="rounded=0;whiteSpace=wrap;html=1;fillColor=#ffffff;strokeColor=#5b7a9c;'
|
| 266 |
+
f'verticalAlign=top;fontSize=17;fontStyle=1;fontColor=#13334f;" vertex="1" parent="1">'
|
| 267 |
+
f'<mxGeometry x="20" y="20" width="{max(ow, x-50)}" height="{oh+60}" as="geometry" /></mxCell>')
|
| 268 |
+
fills = ["#eef5fc", "#e4eefb", "#dbe8f8"]
|
| 269 |
+
for i, z in enumerate(sorted(rects, key=lambda r: r["depth"])):
|
| 270 |
+
x0, y0, w0, h0 = z["rect"]
|
| 271 |
+
fc = fills[min(z["depth"]-1, len(fills)-1)] if z["depth"] >= 1 else "#ffffff"
|
| 272 |
+
cells.append(f'<mxCell id="blk{i}" value={quoteattr(z.get("label") or "")} '
|
| 273 |
+
f'style="rounded=0;whiteSpace=wrap;html=1;fillColor={fc};strokeColor=#9fc5e8;'
|
| 274 |
+
f'verticalAlign=top;fontSize=12;fontStyle=1;fontColor=#1a3b5c;" vertex="1" parent="1">'
|
| 275 |
+
f'<mxGeometry x="{x0}" y="{y0}" width="{w0}" height="{h0}" as="geometry" /></mxCell>')
|
| 276 |
+
idmap = {}; nid = 1
|
| 277 |
+
def walk(b):
|
| 278 |
+
for sub in b.get("blocks", []): walk(sub)
|
| 279 |
+
for comp in b.get("components", []):
|
| 280 |
+
if comp["id"] not in pos: continue
|
| 281 |
+
nonlocal nid; nid += 1; idmap[comp["id"]] = f"n{nid}"
|
| 282 |
+
xx, yy, ww, hh = pos[comp["id"]]
|
| 283 |
+
key = snap_icon(comp["label"], comp.get("icon"))
|
| 284 |
+
# small icon centered in the wide cell; label wraps below within the full width
|
| 285 |
+
style = (_LIB[key].rstrip(";") + ";verticalLabelPosition=bottom;verticalAlign=top;"
|
| 286 |
+
"imageAlign=center;labelPosition=center;align=center;whiteSpace=wrap;") if key \
|
| 287 |
+
else "rounded=1;whiteSpace=wrap;html=1;fillColor=#ffffff;strokeColor=#5b7a9c;align=center;"
|
| 288 |
+
cells.append(f'<mxCell id="n{nid}" value={quoteattr(comp["label"])} style={quoteattr(style)} '
|
| 289 |
+
f'vertex="1" parent="1"><mxGeometry x="{xx}" y="{yy}" width="{ww}" height="{hh}" as="geometry" /></mxCell>')
|
| 290 |
+
for b in blocks: walk(b)
|
| 291 |
+
eid = 5000
|
| 292 |
+
for e in spec.get("connections", []):
|
| 293 |
+
if e["source"] not in idmap or e["target"] not in idmap: continue
|
| 294 |
+
eid += 1; lab = e.get("label", ""); va = f' value={quoteattr(lab)}' if lab else ''
|
| 295 |
+
cells.append(f'<mxCell id="e{eid}"{va} style="edgeStyle=orthogonalEdgeStyle;rounded=0;html=1;'
|
| 296 |
+
f'strokeColor=#42607d;endArrow=block;" edge="1" parent="1" '
|
| 297 |
+
f'source={quoteattr(idmap[e["source"]])} target={quoteattr(idmap[e["target"]])}>'
|
| 298 |
+
f'<mxGeometry relative="1" as="geometry" /></mxCell>')
|
| 299 |
+
xml = (f'<mxfile host="archgen"><diagram name={quoteattr(spec.get("title","Architecture"))}>'
|
| 300 |
+
f'<mxGraphModel dx="1200" dy="800" grid="0" guides="1" tooltips="1" connect="1" arrows="1" '
|
| 301 |
+
f'fold="1" page="1" pageScale="1" pageWidth="1600" pageHeight="1100" math="0" shadow="0"><root>'
|
| 302 |
+
+ "".join(cells) + '</root></mxGraphModel></diagram></mxfile>')
|
| 303 |
+
open(out_path, "w").write(xml)
|
| 304 |
+
return out_path
|
| 305 |
+
|
| 306 |
+
if __name__ == "__main__":
|
| 307 |
+
import sys
|
| 308 |
+
spec = json.load(open(sys.argv[1])) if len(sys.argv) > 1 else json.load(open("demo_spec.json"))
|
| 309 |
+
fn = build_hier if "blocks" in spec else build
|
| 310 |
+
p = fn(spec, sys.argv[2] if len(sys.argv) > 2 else "spec_output.drawio")
|
| 311 |
+
print("wrote", p)
|