Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
81 commits
Select commit Hold shift + click to select a range
cecc934
feat(layout): improve alignment and connector routing
Jun 27, 2026
7a0d09f
feat(layout): auto-optimize node order and spread overlapping bends
Jun 27, 2026
e8e3ca0
feat(layout): detect edge-edge crossings and collinear overlap
Jun 27, 2026
e9dfb96
feat(layout): replace heuristic sort with brute-force min-crossing se…
Jun 27, 2026
d5dc8db
feat(layout): optimize port assignment order to minimize crossings
Jun 27, 2026
bd88363
feat(layout): search-based bend optimization to resolve all crossings
Jun 27, 2026
eb71c25
feat(layout): detour routing for reverse-flow + bend separation impro…
Jun 27, 2026
42ee283
feat(layout): consistent fan-out port sides and dst_side correction
Jun 27, 2026
53e688b
fix(layout): center leaf nodes at nearest sibling group median Y
Jun 27, 2026
593586a
fix(layout): never move port-anchored endpoints during bend adjustment
Jun 27, 2026
a1aa013
feat(layout): add graph layout engine and improve routing for non-tre…
Jun 27, 2026
59aa198
fix(layout): prevent arrows from bending backwards through source icon
Jun 27, 2026
c47e484
fix(layout): correct elbow type detection when points have been shifted
Jun 27, 2026
351026a
fix(layout): reduce padding/margin for structural containers without …
Jun 27, 2026
0c07349
feat(layout): support targetArea in input JSON for layout positioning
Jun 27, 2026
3217f5c
fix(layout): force consistent side for fan-out connections
Jun 27, 2026
2298667
fix(layout): pre-decide fan-out exit side by majority vote with right…
Jun 27, 2026
080ba1a
fix(layout): implement fan-out shared bend pattern and protect from s…
Jun 27, 2026
78aad24
fix(layout): correct fan-out dst_side and render as polyline
Jun 27, 2026
d9b7d35
fix(layout): add node icons as obstacles and fix _calc_bend to avoid …
Jun 27, 2026
7e487c8
fix(layout): render all 4+ point paths as polyline for precise routing
Jun 27, 2026
c9bad5d
fix(layout): post-process bends to avoid passing through node icons
Jun 27, 2026
f82de1b
fix(layout): eliminate all diagonal segments in arrow paths
Jun 27, 2026
333bba5
fix(layout): prevent arrows from going backwards through source/targe…
Jun 27, 2026
9ad7014
fix(layout): fix fan-out bend_x calculation for targets to the left
Jun 27, 2026
e38df0d
fix(layout): use natural dst_side for fan-out targets directly above/…
Jun 27, 2026
3949e42
fix(layout): render 3+ point paths as polyline and iterate diagonal fix
Jun 27, 2026
5f89f50
fix(layout): guarantee all arrow segments are axis-aligned
Jun 27, 2026
97514bf
fix(layout): revert to natural src_side for fan-out targets directly …
Jun 27, 2026
0f55026
fix(layout): improve backwards detection using closest-edge heuristic
Jun 27, 2026
a1e2614
fix(layout): disable _spread_overlapping_bends to guarantee perpendic…
Jun 27, 2026
9019b5e
feat(layout): add safe bend separation that preserves axis-alignment
Jun 27, 2026
f176628
feat(layout): enhance optimize_order for mixed groups and cross-group…
Jun 27, 2026
cabcd9c
feat(layout): pierce-aware bend optimization + side/detour routing
Jun 28, 2026
cf33d55
feat(layout): graze-aware pierce detection + backwards penalty
Jun 28, 2026
893d79e
tune(layout): widen icon keep-out margin to 9px
Jun 28, 2026
5eb0c26
fix(layout): stop misrouting fan-out targets as reverse-flow U-detours
Jun 28, 2026
6ec311e
feat(layout): fan-merge trunk routing, alignment fix, port recentering
Jun 28, 2026
c1366a3
feat(layout): allow groups as connection endpoints (many-to-one to a …
Jun 28, 2026
2395c50
feat(layout): bundle group-endpoint edges into a parallel bus (T14)
Jun 28, 2026
df9c2d8
fix(layout): group hubs in fan-merge + roll back fans that pierce/cross
Jun 28, 2026
9f79b45
fix(layout): decide fan hub side from geometry; keep trunk inside the…
Jun 28, 2026
9e089fd
fix(layout): don't count a fan bundle's shared trunk as crossings
Jun 28, 2026
8e1045f
feat(layout): promote degree-1 branch nodes to perpendicular lane
Jun 29, 2026
26d3af8
fix(layout): keep branch-lane anchor on the main flow line
Jun 29, 2026
87a35db
feat(layout): fan-merge pierce guard + manual branch-lane anchoring
Jun 29, 2026
765b8e3
docs(guides): add nested-JSON layout engine guide for LLMs
Jun 29, 2026
b228f5e
feat(layout): detect & avoid group-frame pierces
Jun 29, 2026
fd87a56
fix(layout): rewrap sub-group frame after leaf-alignment shifts icons
Jun 29, 2026
7dd98c2
fix(layout): re-flow siblings after a sub-group box grows
Jun 29, 2026
f315023
feat(layout): compress only the overflowing group, not the whole slide
Jun 30, 2026
4e2ca7f
feat(layout): cancel cross-axis squash on groups that already fit
Jun 30, 2026
289fcbc
fix(layout): straighten group-endpoint arrows + stop cross-group row …
Jun 30, 2026
c9bde54
fix(layout): row-align only clean columns so group centers stay aligned
Jun 30, 2026
ff55fc6
feat(layout): push fan split/merge outside the hub's framed group
Jun 30, 2026
b6015d8
fix(layout): detect T-junction crossings + normalize detour paths
Jun 30, 2026
a56a00c
feat(layout): U-turn group-endpoint monitor edges around framed boxes
Jun 30, 2026
2fc6d68
refactor(layout): routing warnings state facts, not prescriptions
Jun 30, 2026
946ed8a
docs(layout): add anti-pattern for one-source-set fanning to two targets
Jun 30, 2026
7da1a85
perf+fix(layout): faster routing + order-model straightens group-endp…
Jul 1, 2026
29c23be
refactor(layout): extract canonical render pipeline into sdpm.layout
Jul 8, 2026
231dbd1
feat(mcp): expose arch_diagram tool (auto-layout + QA metrics)
Jul 8, 2026
b5e0a72
docs(layout): document arch_diagram MCP tool alongside the CLI
Jul 8, 2026
c3567b8
Merge remote-tracking branch 'origin/main' into feat/layout-engine-al…
Jul 8, 2026
54c1858
feat(layout): auto-reflow anonymous tile pools to shorten wiring
Jul 9, 2026
16975f7
fix(layout): group-bus bundles per side, not one centroid side
Jul 9, 2026
f6f0d73
fix(layout): builder crossing warning uses the QA detector
Jul 9, 2026
96935cb
docs(layout): correct crossing-warning note, add preview step
Jul 9, 2026
8013548
docs(layout): defer preview to the review step, not a separate pass
Jul 9, 2026
82af54d
fix(mcp): preview_files empty for <10-page decks (pdftoppm naming)
Jul 9, 2026
3295e70
fix(build): warn when an include is missing or empty
Jul 9, 2026
4aedb31
fix(build): icon labels no longer wrap one glyph per line
Jul 9, 2026
dd6d6f0
docs(workflow): route architecture diagrams to the layout engine
Jul 9, 2026
81fc3fc
style(layout): remove unused imports and dead locals (ruff clean)
Jul 9, 2026
7bba8e6
docs(layout): fan:merge tidies any shared-hub bundle, not just same-p…
Jul 9, 2026
01bd119
chore(layout): add missing MIT-0 license header to graph.py
ShotaroKataoka Jul 13, 2026
bb4ccf6
fix(builder): account for CJK glyph width in icon label sizing
ShotaroKataoka Jul 13, 2026
61c946a
test(layout): add coverage for render pipeline, metrics, and include …
ShotaroKataoka Jul 13, 2026
1c70242
refactor(layout): split 4.5k-line __init__.py into cohesive submodules
ShotaroKataoka Jul 13, 2026
e6e7feb
test(layout): add seeded property-based tests for the routing engine
ShotaroKataoka Jul 13, 2026
8940317
Merge branch 'main' into feat/layout-engine-alignment-improvements
ShotaroKataoka Jul 13, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 24 additions & 6 deletions mcp-local/sandbox_tools.py
Original file line number Diff line number Diff line change
Expand Up @@ -369,18 +369,36 @@ def _fp(c: dict) -> str:
cmd_png = ["pdftoppm", "-png", "-scale-to", "1280", str(pdf_path), str(png_outdir / "page")]
subprocess.run(cmd_png, capture_output=True, text=True, stdin=subprocess.DEVNULL)

# pdftoppm zero-pads the page number to the width of
# the largest page index, so a 1-9 page export is
# "page-1.png" (no padding), 10-99 is "page-01.png",
# etc. Try every plausible width instead of assuming
# one — the old code only tried 6/2/3 and silently
# produced an empty preview list for <10-page decks.
filtered_previews = []
missing = []
for idx_p, slug in enumerate(pptx_slugs):
src_png = png_outdir / f"page-{idx_p + 1:06d}.png"
if not src_png.exists():
src_png = png_outdir / f"page-{idx_p + 1:02d}.png"
if not src_png.exists():
src_png = png_outdir / f"page-{idx_p + 1:03d}.png"
if src_png.exists():
n_p = idx_p + 1
src_png = None
for width in (1, 2, 3, 4, 6):
cand = png_outdir / f"page-{n_p:0{width}d}.png"
if cand.exists():
src_png = cand
break
if src_png:
dst = preview_dir / f"{slug}.png"
shutil.copy2(src_png, dst)
filtered_previews.append(str(dst))
else:
missing.append(slug)
result["preview_files"] = filtered_previews
if missing:
# Surface the silent gap instead of returning [].
produced = sorted(p.name for p in png_outdir.glob("*.png"))
result["preview_error"] = (
f"No PNG matched slugs {missing}; "
f"pdftoppm produced {produced}"
)
except Exception as e:
result["preview_error"] = str(e)

Expand Down
1 change: 1 addition & 0 deletions mcp-local/server.py
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,7 @@
mcp.tool()(tools.read_guides)
mcp.tool()(tools.code_to_slide)
mcp.tool()(tools.grid)
mcp.tool()(tools.arch_diagram)
mcp.tool()(tools.pptx_to_json)

# Sandbox tools (shared)
Expand Down
1 change: 1 addition & 0 deletions mcp-local/server_acp.py
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@
mcp.tool()(tools.read_guides)
mcp.tool()(tools.code_to_slide)
mcp.tool()(tools.grid)
mcp.tool()(tools.arch_diagram)
mcp.tool()(tools.pptx_to_json)

# Sandbox tools (shared)
Expand Down
1 change: 1 addition & 0 deletions mcp-local/server_with_instruction.py
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@
mcp.tool()(tools.read_guides)
mcp.tool()(tools.code_to_slide)
mcp.tool()(tools.grid)
mcp.tool()(tools.arch_diagram)
mcp.tool()(tools.pptx_to_json)

# Sandbox tools (shared)
Expand Down
55 changes: 55 additions & 0 deletions mcp-local/tools.py
Original file line number Diff line number Diff line change
Expand Up @@ -322,6 +322,61 @@ def grid(purpose: str, spec: str) -> dict[str, Any]:
return compute_grid(grid_spec)


def arch_diagram(
spec: str,
x: int = 100, y: int = 180, width: int = 1720, height: int = 800,
theme: str = "dark",
) -> dict[str, Any]:
"""Auto-layout an architecture/flow diagram from a logical-structure JSON.

Turns a nested structure of groups, icons and connections into fully-placed
slide elements with auto-routed orthogonal arrows. You describe *what
connects to what*; the engine computes coordinates, clusters related icons,
picks arrow ports/bends, and minimizes crossings and icon pierces. Prefer
this over hand-placing coordinates for any diagram with more than a few
connections.

Read the guide `arch-layout-engine` (via read_guides) for the JSON schema
and the techniques that reach 0 crossings (connect to a GROUP not every
icon; `fan: "merge"` bundles; perpendicular branch wrappers).

Args:
spec: JSON string. Top-level keys: `direction` ("horizontal"/"vertical"),
`iconSize`, `children` (nested nodes/groups), `connections`
(`{from, to, label?, fan?}`). A `targetArea` object inside the JSON
overrides x/y/width/height.
x: Target area X offset in px.
y: Target area Y offset in px.
width: Target area width in px (engine scales the diagram to fit).
height: Target area height in px.
theme: "dark" or "light" — affects box-node text colors.

Returns:
Dict with:
- `elements`: sdpm element array — drop into a slide, or write to a
file and reference with `{"type": "include", "src": "..."}`.
- `bbox`: final bounding box after scale-to-fit.
- `warnings`: human-readable facts about layout defects (facts, not
prescriptions) — may be absent when clean.
- `metrics`: objective QA numbers. `crossings` / `pierces` /
`group_pierces` are 0 for a clean diagram; `overflow` > 0 means the
layout spills off the target box; `score` is the internal judge's
lexicographic tuple (lower is better). Inspect these and iterate on
STRUCTURE (not coordinates) when defects remain.
"""
import json
from sdpm.layout.render import render_architecture

try:
tree = json.loads(spec)
except (json.JSONDecodeError, TypeError) as e:
return {"error": f"Invalid diagram spec JSON: {e}"}
return render_architecture(
tree, x=x, y=y, width=width, height=height, theme=theme,
include_metrics=True,
)


def pptx_to_json(pptx_path: str) -> dict[str, Any]:
"""Convert an existing PPTX file to JSON representation.

Expand Down
51 changes: 51 additions & 0 deletions mcp-server/server.py
Original file line number Diff line number Diff line change
Expand Up @@ -943,6 +943,57 @@ def grid(spec: str, purpose: str = "") -> str:
return json.dumps(result, ensure_ascii=False, indent=2)


@mcp.tool()
def arch_diagram(
spec: str,
x: int = 100, y: int = 180, width: int = 1720, height: int = 800,
theme: str = "dark",
) -> str:
"""Auto-layout an architecture/flow diagram from a logical-structure JSON.

Turns a nested structure of groups, icons and connections into fully-placed
slide elements with auto-routed orthogonal arrows. You describe *what
connects to what*; the engine computes coordinates, clusters related icons,
picks arrow ports/bends, and minimizes crossings and icon pierces. Prefer
this over hand-placing coordinates for any diagram with more than a few
connections.

Read the guide `arch-layout-engine` (via read_guides) for the JSON schema
and the techniques that reach 0 crossings (connect to a GROUP not every
icon; `fan: "merge"` bundles; perpendicular branch wrappers).

Args:
spec: JSON string. Top-level keys: `direction` ("horizontal"/"vertical"),
`iconSize`, `children` (nested nodes/groups), `connections`
(`{from, to, label?, fan?}`). A `targetArea` object inside the JSON
overrides x/y/width/height.
x: Target area X offset in px.
y: Target area Y offset in px.
width: Target area width in px (engine scales the diagram to fit).
height: Target area height in px.
theme: "dark" or "light" — affects box-node text colors.

Returns:
JSON with `elements` (sdpm element array), `bbox` (final bounding box),
optional `warnings` (facts about layout defects), and `metrics`
(objective QA numbers: `crossings`/`pierces`/`group_pierces` are 0 for a
clean diagram; `overflow` > 0 means the layout spills off the box;
`score` is the judge's lexicographic tuple, lower is better). Inspect
these and iterate on STRUCTURE (not coordinates) when defects remain.
"""
from sdpm.layout.render import render_architecture

try:
tree = json.loads(spec)
except (json.JSONDecodeError, TypeError) as e:
return json.dumps({"error": f"Invalid diagram spec JSON: {e}"})
result = render_architecture(
tree, x=x, y=y, width=width, height=height, theme=theme,
include_metrics=True,
)
return json.dumps(result, ensure_ascii=False, indent=2)


# --- Style Execution (Code Interpreter) ---


Expand Down
9 changes: 8 additions & 1 deletion skill/references/guides/arch-elements.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,13 @@ Common components for architecture diagrams (borders, groups, arrows, scale, col

Shared components and rules used across architecture diagram patterns (simple / complex).

> **Building a diagram from connections (what-links-to-what)?** Prefer the
> nested-JSON layout engine — it auto-places icons and auto-routes orthogonal
> arrows, minimizing crossings/pierces. See
> [arch-layout-engine.md](arch-layout-engine.md). This file (arch-elements.md)
> covers the shared vocabulary — group colors/types, boundary & logo, box
> nodes, manual arrow geometry — used by both the engine and hand-placement.

---

## Core Principles
Expand Down Expand Up @@ -289,7 +296,7 @@ Minimal (label only):

### Rendering

The layout engine expands box nodes into existing element types (colors switch with `--theme`):
The layout engine expands box nodes into existing element types (colors switch with the `theme` arg / `--theme` flag):

- `rounded_rectangle`: semi-transparent background (opacity 0.18) + border + shadow "sm" + corner radius (0.07)
- `textbox`: sublabel (muted) → label (bold) → description (muted), with autofit
Expand Down
Loading
Loading