From 886c09ad1059693cba06f6683c04086c22710759 Mon Sep 17 00:00:00 2001 From: root Date: Tue, 14 Apr 2026 02:01:09 -0300 Subject: [PATCH] Improve diagram generator: auto-sizing, DRG placement, edge labels MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 6 improvements based on Oracle Architecture Center reference diagrams: 1. Edge label offset — labels pushed 15px away from edge midpoint via mxGeometry offset injection, prevents overlap with icons 2. Auto-sizing containers — regions, VCNs, tenancy calculate dimensions from content instead of using fixed sizes (DR region: 540→260px) 3. jettySize=auto on all edges — clean stubs leaving/entering shapes 4. Dual connection merging — duplicate from/to pairs merged into single edge with combined label (e.g., dual FastConnect) 5. DRG placed OUTSIDE VCN, INSIDE region — matches Oracle ref arch pattern where DRG is the central hub between external and VCN 6. container=1;collapsible=0 injected into container XML styles Co-Authored-By: Claude Opus 4.6 (1M context) --- .../output-demo-pharma-mx/diagram-spec.yaml | 126 +++---- .../opt02-architecture.drawio | 338 ++++++------------ tools/oci_diagram_gen.py | 260 ++++++++++++-- 3 files changed, 388 insertions(+), 336 deletions(-) diff --git a/examples/output-demo-pharma-mx/diagram-spec.yaml b/examples/output-demo-pharma-mx/diagram-spec.yaml index df7cc29..6db3084 100644 --- a/examples/output-demo-pharma-mx/diagram-spec.yaml +++ b/examples/output-demo-pharma-mx/diagram-spec.yaml @@ -1,13 +1,15 @@ # OCI Deal Accelerator — Architecture Diagram Spec # Demo: PharmaCorp Mexico — ExaCS dual-region with Data Guard -# Layout: Left-to-right flow, ref arch style -# Mexico DC → FastConnect → Queretaro (DB subnet) → DR Sao Paulo +# Prompt: "ExaCS en Queretaro (prod) + Standby con Data Guard en Sao Paulo, +# Hub-Spoke networking con DRG, FastConnect 10Gbps dual desde Mexico City, +# bastion en public subnet, WAF para apps web, Vault para TDE keys." # -# This example only includes components the user explicitly requested: -# ExaCS (prod + DR), Hub-Spoke networking with DRG, FastConnect 10Gbps, -# Bastion in public subnet. -# DRG and Service Gateway are auto-added per technical dependency whitelist. -# Monitoring, Logging, Events, WAF, Vault, EBS — NOT included (not requested). +# REQUESTED by user: ExaCS (prod+DR), DRG, FastConnect dual, Bastion, WAF, Vault +# TECHNICAL DEPENDENCIES (whitelist): SGW (ExaCS backup), IGW (public subnet) +# NOT included: EBS, Monitoring, Logging, Events, FSDR, Object Storage (not requested) +# +# Container sizes: omitted to let auto-sizing calculate from content. +# DRG: listed in gateways — generator places it OUTSIDE VCN, INSIDE region. title: "PharmaCorp Mexico — ExaCS Migration Architecture" @@ -17,7 +19,7 @@ external: label: "Mexico City\nData Center" icon: "user" x: 30 - y: 320 + y: 250 w: 60 h: 70 @@ -26,144 +28,108 @@ tenancy: label: "Oracle Cloud Infrastructure" x: 200 y: 60 - w: 1650 - h: 780 regions: # ── Primary Region: Queretaro ── - id: "region_qro" label: "Region — Queretaro (Primary)" primary: true - x: 15 - y: 40 - w: 1050 - h: 720 vcns: - id: "vcn_hub" label: "Hub VCN (10.0.0.0/16)" - x: 15 - y: 45 - w: 1020 - h: 650 subnets: - # Public subnet — bastion access + # Public subnet — WAF + Bastion (both requested) - id: "subnet_pub" label: "Public Subnet — 10.0.0.0/24" h: 120 services: + - id: "waf" + label: "WAF\n(Web Apps)" + type: "waf" + w: 120 + h: 70 - id: "bastion" label: "Bastion\nService" type: "bastion" w: 120 h: 70 - # DB subnet — main workload + # DB subnet — ExaCS + Vault (both requested) - id: "subnet_db" label: "Database Subnet (Private) — 10.0.1.0/24" - h: 150 + h: 130 services: - id: "exacs_prod" label: "ExaCS X11M\nQuarter Rack\n(Production)" type: "exacs" w: 200 - h: 90 + h: 80 + - id: "vault" + label: "OCI Vault\nTDE Keys" + type: "vault" + w: 140 + h: 80 gateways: - # Auto-added per whitelist: FastConnect requires DRG + # DRG — generator places this OUTSIDE VCN, INSIDE region - id: "drg" label: "DRG" type: "drg" w: 90 h: 60 - # Auto-added per whitelist: ExaCS backup requires SGW + # IGW — whitelist: public subnet requires it + - id: "igw" + label: "Internet\nGateway" + type: "igw" + w: 100 + h: 60 + # SGW — whitelist: ExaCS backup requires it - id: "sgw" label: "Service\nGateway" type: "service_gateway" w: 100 h: 60 - # ── DR Region: Sao Paulo (right side) ── + # ── DR Region: Sao Paulo ── - id: "region_gru" label: "Region — São Paulo (DR)" - x: 1090 - y: 40 - w: 540 - h: 540 vcns: - id: "vcn_dr" label: "DR VCN (10.1.0.0/16)" - x: 15 - y: 45 - w: 510 - h: 470 subnets: - id: "subnet_dr_db" label: "DB Subnet (Private) — 10.1.1.0/24" - h: 200 + h: 130 services: - id: "exacs_dr" label: "ExaCS Standby\n(Data Guard)" type: "exacs" w: 200 - h: 100 + h: 80 # Connections connections: - # Entry: DC → DRG (FastConnect) - - id: "conn_fc" + # FastConnect dual from Mexico City → DRG (will be merged into single edge) + - id: "conn_fc1" label: "FastConnect 10Gbps\n(Dual Redundant)" - type: "network" + type: "fastconnect" from: "mexico_dc" to: "drg" - # DR: ExaCS Prod → ExaCS Standby (Data Guard) + # Vault → ExaCS (TDE keys) + - id: "conn_vault" + label: "TDE Keys" + type: "internal" + from: "vault" + to: "exacs_prod" + + # Data Guard: Prod → DR - id: "conn_dg" label: "Data Guard\n(Redo Shipping)" type: "data" from: "exacs_prod" to: "exacs_dr" - -# ────────────────────────────────────────────────────────────── -# OPTIONAL — add if the user approves during pre-generation review: -# -# Security: -# - id: "vault" -# label: "OCI Vault\nTDE Keys" -# type: "vault" -# -# - id: "waf" -# label: "WAF" -# type: "waf" -# -# Observability: -# - id: "monitoring" -# label: "OCI Monitoring" -# type: "monitoring" -# -# - id: "logging" -# label: "OCI Audit + Logging" -# type: "logging" -# -# - id: "events" -# label: "OCI Events + Notifications" -# type: "events" -# -# Application tier (only if customer has app servers): -# - id: "ebs_app" -# label: "EBS R12 App Tier" -# type: "compute" -# -# DR orchestration: -# - id: "fsdr" -# label: "Full Stack DR (FSDR)" -# type: "fsdr" -# -# Backup: -# - id: "obj_storage" -# label: "Object Storage\nRMAN Backups" -# type: "object-storage" -# ────────────────────────────────────────────────────────────── diff --git a/examples/output-demo-pharma-mx/opt02-architecture.drawio b/examples/output-demo-pharma-mx/opt02-architecture.drawio index c850d0e..e06e8ae 100644 --- a/examples/output-demo-pharma-mx/opt02-architecture.drawio +++ b/examples/output-demo-pharma-mx/opt02-architecture.drawio @@ -4,160 +4,116 @@ - - + + - + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + - + + + + + + + + + + + @@ -165,116 +121,56 @@ - + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/tools/oci_diagram_gen.py b/tools/oci_diagram_gen.py index f9a33e1..696e90f 100644 --- a/tools/oci_diagram_gen.py +++ b/tools/oci_diagram_gen.py @@ -909,6 +909,7 @@ class OCIDiagramGenerator: # - NO elbow=vertical (it conflicts with orthogonal + port constraints) extra_style = ( "edgeStyle=orthogonalEdgeStyle;orthogonalLoop=1;" + "jettySize=auto;" "jumpStyle=arc;jumpSize=8;" "endFill=0;startFill=0;" "labelBackgroundColor=#FFFFFF;labelBorderColor=none;" @@ -1121,6 +1122,57 @@ class OCIDiagramGenerator: if pattern in xml: xml = xml.replace(pattern, f'id="{edge_id}" style="{extra_style}') + # ── Edge label offset injection ── + # Push labels away from the edge midpoint to avoid overlapping with + # source/target icons. Uses mxGeometry relative positioning: + # x: position along edge (-1=source, 0=center, 1=target) + # : absolute pixel offset from that position + # Horizontal edges: push label 15px above the line (offset y=-15) + # Vertical edges: push label 15px to the right (offset x=15) + for edge_id, (src_id, tgt_id) in self._edge_objects.items(): + src_pos = self._abs_positions.get(src_id) + tgt_pos = self._abs_positions.get(tgt_id) + if not src_pos or not tgt_pos: + continue + src_obj = self._objects.get(src_id) + tgt_obj = self._objects.get(tgt_id) + if not src_obj or not tgt_obj: + continue + sx = src_pos[0] + (getattr(src_obj, 'width', 100) or 100) / 2 + sy = src_pos[1] + (getattr(src_obj, 'height', 60) or 60) / 2 + tx = tgt_pos[0] + (getattr(tgt_obj, 'width', 100) or 100) / 2 + ty = tgt_pos[1] + (getattr(tgt_obj, 'height', 60) or 60) / 2 + dx = tx - sx + dy = ty - sy + is_horiz = abs(dx) > abs(dy) * 1.5 + if is_horiz: + offset_x, offset_y = 0, -15 + else: + offset_x, offset_y = 15, 0 + # Find the edge's mxGeometry and inject label offset. + # drawpyo emits: + # We replace with expanded form containing offset point. + new_geo = ( + f'' + f'' + f'' + ) + # Match both self-closing variants that drawpyo may produce + for old_geo in [ + '', + '', + ]: + marker = f'id="{edge_id}"' + if marker in xml and old_geo in xml: + # Only replace the mxGeometry that belongs to THIS edge + # Find the edge cell, then replace its geometry + idx = xml.find(marker) + if idx >= 0: + geo_idx = xml.find(old_geo, idx) + if geo_idx >= 0 and geo_idx - idx < 2000: # sanity: within same cell + xml = xml[:geo_idx] + new_geo + xml[geo_idx + len(old_geo):] + break + # Inject raw icon stencil cells before if self._raw_cells: inject = "\n".join(f" {c}" for c in self._raw_cells) @@ -1132,6 +1184,17 @@ class OCIDiagramGenerator: if placeholder in xml: xml = xml.replace(placeholder, str(obj.id)) + # Inject container=1;collapsible=0 into container styles via XML. + # drawpyo doesn't support 'container' as a settable property, so we + # append it to the style string in the final XML. + for container_marker in ["arcSize=1;", "arcSize=5;", "arcSize=3;", + "perimeterSpacing=0;", "spacingTop=4;"]: + # Only add to styles that already contain these OCI container markers + # and don't already have container=1 + old = f'{container_marker}"' + new = f'{container_marker}container=1;collapsible=0;"' + xml = xml.replace(old, new) + # Sanitize: no pure white in OCI palette xml = xml.replace('fill="#ffffff"', 'fill="#FCFBFA"') xml = xml.replace('fill="#FFFFFF"', 'fill="#FCFBFA"') @@ -1221,33 +1284,130 @@ class OCIDiagramGenerator: cloud_id, cx, cy, svc_w, svc_h) cy += svc_h + 20 - # Tenancy + # Tenancy — auto-size from regions if w/h not specified in spec tenancy_spec = spec.get("tenancy", {}) tenancy_x = tenancy_spec.get("x", 30) tenancy_y = tenancy_spec.get("y", 80) - tenancy_w = tenancy_spec.get("w", 1850) - tenancy_h = tenancy_spec.get("h", 720) + + # Pre-calculate tenancy size from region content if not explicit + if "w" not in tenancy_spec or "h" not in tenancy_spec: + regions = tenancy_spec.get("regions", []) + max_region_right = 0 + max_region_bottom = 0 + region_x_cursor = 15 + for r in regions: + # Estimate region dimensions (will be refined when regions are created) + r_vcns = r.get("vcns", []) + r_has_drg = any( + gw.get("type") in ("drg", "dynamic_routing_gateway") + for v in r_vcns for gw in v.get("gateways", []) + ) + drg_w = 120 if r_has_drg else 0 + # Quick subnet height calc + total_sub_h = 0 + max_sub_w = 300 + for v in r_vcns: + for s in v.get("subnets", []): + total_sub_h += s.get("h", 120) + 14 + svcs = s.get("services", []) + row_w = sum(sv.get("w", 150) for sv in svcs) + 16 * max(0, len(svcs) - 1) + 30 + max_sub_w = max(max_sub_w, row_w) + # VCN-internal gateways + vcn_gws = [gw for gw in v.get("gateways", []) + if gw.get("type") not in ("drg", "dynamic_routing_gateway")] + if vcn_gws: + max_sub_w += max(gw.get("w", 110) for gw in vcn_gws) + 20 + + est_rw = max_sub_w + drg_w + 80 + est_rh = total_sub_h + 80 + + rx_val = r.get("x", region_x_cursor) + ry_val = r.get("y", 40) + max_region_right = max(max_region_right, rx_val + est_rw) + max_region_bottom = max(max_region_bottom, ry_val + est_rh) + region_x_cursor = max_region_right + 20 + + tenancy_w = tenancy_spec.get("w", max(max_region_right + 30, 600)) + tenancy_h = tenancy_spec.get("h", max(max_region_bottom + 30, 400)) + else: + tenancy_w = tenancy_spec["w"] + tenancy_h = tenancy_spec["h"] + gen.add_tenancy( "tenancy", tenancy_spec.get("label", "Oracle Cloud Infrastructure"), x=tenancy_x, y=tenancy_y, w=tenancy_w, h=tenancy_h, ) - # Regions + # Regions — auto-size containers from content when spec doesn't override + region_x_cursor = 15 # tracks right edge for auto-positioning next region for region in tenancy_spec.get("regions", []): is_primary = region.get("primary", False) - rx = region.get("x", 15 if is_primary else 1195) - # ry default 40: ~18px below tenancy label (12pt bold) + comfortable padding + rx = region.get("x", region_x_cursor) ry = region.get("y", 40) - rw = region.get("w", 1140 if is_primary else 640) - rh = region.get("h", 670 if is_primary else 480) + + # ── Pre-calculate content size for auto-sizing ── + # Walk all VCNs → subnets → services to compute minimum dimensions + region_vcns = region.get("vcns", []) + auto_rw, auto_rh = 640 if not is_primary else 1140, 480 if not is_primary else 670 + + if region_vcns: + # Calculate total height needed from subnets + gateways + for vcn in region_vcns: + gateways = vcn.get("gateways", []) + # Separate DRG from VCN-internal gateways (IGW, NAT, SGW) + drg_gateways = [gw for gw in gateways if gw.get("type") in ("drg", "dynamic_routing_gateway")] + vcn_gateways = [gw for gw in gateways if gw.get("type") not in ("drg", "dynamic_routing_gateway")] + + gw_lane_w = 0 + if vcn_gateways: + gw_lane_w = max(gw.get("w", 110) for gw in vcn_gateways) + 20 + + subnets = vcn.get("subnets", []) + total_subnet_h = sum(s.get("h", 120) for s in subnets) + 14 * max(0, len(subnets) - 1) + + # Max service width across all subnets + max_svc_row_w = 0 + for subnet in subnets: + svcs = subnet.get("services", []) + row_w = sum(svc.get("w", 150) for svc in svcs) + 16 * max(0, len(svcs) - 1) + 30 + max_svc_row_w = max(max_svc_row_w, row_w) + + # VCN needs: gw_lane + max(subnet_content_w, specified_w) + margins + content_w = gw_lane_w + max(max_svc_row_w, 300) + 40 + content_h = total_subnet_h + 60 # 32 top + 28 bottom padding + + # Also account for gateway column height + if vcn_gateways: + total_gw_h = sum(gw.get("h", 70) for gw in vcn_gateways) + 8 * max(0, len(vcn_gateways) - 1) + 64 + content_h = max(content_h, total_gw_h) + + # Store calculated VCN dimensions for use below + vcn["_auto_w"] = content_w + vcn["_auto_h"] = content_h + vcn["_drg_gateways"] = drg_gateways + vcn["_vcn_gateways"] = vcn_gateways + + # Region size = VCN size + margins + DRG lane + total_vcn_w = max(v.get("_auto_w", 600) for v in region_vcns) + total_vcn_h = max(v.get("_auto_h", 400) for v in region_vcns) + # DRG lane on left edge of region (outside VCN) + has_drg = any(v.get("_drg_gateways") for v in region_vcns) + drg_lane_w = 120 if has_drg else 0 + auto_rw = total_vcn_w + drg_lane_w + 50 # margins + auto_rh = total_vcn_h + 70 # 45 top (label) + 25 bottom + + # Use spec dimensions if explicitly provided, otherwise auto-calculated + rw = region.get("w", auto_rw) + rh = region.get("h", auto_rh) gen.add_region( region["id"], region["label"], "tenancy", rx, ry, rw, rh, ) - + # Advance cursor for next region positioning + region_x_cursor = rx + rw + 20 # Availability Domains (optional) for ad in region.get("availability_domains", []): @@ -1257,39 +1417,49 @@ class OCIDiagramGenerator: w=ad.get("w", rw - 30), h=ad.get("h", rh - 70), ) - # VCNs - for vcn in region.get("vcns", []): + # ── DRG placement: OUTSIDE VCN, INSIDE region ── + # Oracle ref arch pattern: DRG sits in the region between external + # actors and VCNs, not inside any VCN. + has_drg = any(v.get("_drg_gateways") for v in region_vcns) + drg_lane_w = 120 if has_drg else 0 + for vcn in region_vcns: + for drg in vcn.get("_drg_gateways", []): + drg_id = drg.get("id", gen._next_id()) + drg_w = drg.get("w", 90) + drg_h = drg.get("h", 60) + # Position: left edge of region, vertically centered + drg_x = 15 + drg_y = max(45, (rh - drg_h) // 2) + label = drg["label"].replace("\\n", "\n") + gen.add_service( + drg_id, label, drg["type"], + region["id"], drg_x, drg_y, drg_w, drg_h, + ) + + # VCNs — offset right if DRG lane present + vcn_offset_x = drg_lane_w + 15 + for vcn in region_vcns: vcn_parent = vcn.get("parent", region["id"]) - vcn_w = vcn.get("w", rw - 30) - vcn_h = vcn.get("h", rh - 70) + vcn_w = vcn.get("w", vcn.get("_auto_w", rw - vcn_offset_x - 15)) + vcn_h = vcn.get("h", vcn.get("_auto_h", rh - 70)) gen.add_vcn( vcn["id"], f"VCN {vcn['label']}", vcn_parent, - # y default 45: leaves proper gap below the region label (12pt ≈22px + padding) - x=vcn.get("x", 15), y=vcn.get("y", 45), + x=vcn.get("x", vcn_offset_x), y=vcn.get("y", 45), w=vcn_w, h=vcn_h, ) - # ── GATEWAY-AWARE AUTO-LAYOUT ── - # Step 1: Place gateways on the LEFT EDGE of the VCN. - # Step 2: Offset subnets RIGHT to avoid overlap with gateways. - # Step 3: Stack subnets vertically (top-to-bottom). - # This matches Oracle ref arch style: gateways on VCN boundary, - # subnets fill the remaining width. + # ── VCN-internal gateways (IGW, NAT, SGW — NOT DRG) ── + vcn_gateways = vcn.get("_vcn_gateways", []) + gw_lane_w = 0 - gateways = vcn.get("gateways", []) - gw_lane_w = 0 # width reserved for gateway column - - if gateways: - # Calculate gateway lane width (max gateway width + padding) - gw_lane_w = max(gw.get("w", 110) for gw in gateways) + 20 - - # Place gateways vertically centered in VCN - total_gw_h = sum(gw.get("h", 70) for gw in gateways) + 8 * max(0, len(gateways) - 1) + if vcn_gateways: + gw_lane_w = max(gw.get("w", 110) for gw in vcn_gateways) + 20 + total_gw_h = sum(gw.get("h", 70) for gw in vcn_gateways) + 8 * max(0, len(vcn_gateways) - 1) gw_start_y = max(32, (vcn_h - total_gw_h) // 2) gy = gw_start_y - for gw in gateways: + for gw in vcn_gateways: gw_id = gw.get("id", gen._next_id()) gw_w = gw.get("w", 110) gw_h = gw.get("h", 70) @@ -1302,9 +1472,9 @@ class OCIDiagramGenerator: ) gy = gw_y + gw_h + 8 - # Subnet horizontal offset: push right if gateways present + # Subnet horizontal offset: push right if VCN gateways present subnet_x = 8 + gw_lane_w - subnet_avail_w = vcn_w - subnet_x - 8 # remaining width for subnets + subnet_avail_w = vcn_w - subnet_x - 8 # Stack subnets vertically (top-to-bottom) subnet_y = 32 @@ -1441,8 +1611,28 @@ class OCIDiagramGenerator: ) opx += svc_w + 12 - # Connections - for conn in spec.get("connections", []): + # Connections — merge duplicate from/to pairs (e.g., dual FastConnect) + # into a single edge with combined label + connections = spec.get("connections", []) + seen_pairs = {} # (from, to) -> index in deduped list + deduped = [] + for conn in connections: + pair = (conn["from"], conn["to"]) + if pair in seen_pairs: + # Merge: combine labels + existing = deduped[seen_pairs[pair]] + old_label = existing.get("label", "") + new_label = conn.get("label", "") + if old_label and new_label and old_label != new_label: + existing["label"] = f"{old_label}\n+ {new_label}" + elif new_label and not old_label: + existing["label"] = new_label + # Upgrade: if either is "Dual", keep it in label + else: + seen_pairs[pair] = len(deduped) + deduped.append(dict(conn)) + + for conn in deduped: conn_id = conn.get("id", gen._next_id()) gen.add_connection( conn_id, conn.get("label"), conn["type"],