Loading source
Pulling the file list, source metadata, and syntax-aware rendering for this listing.
Source from repo
Generate Excalidraw .excalidraw JSON diagram files from natural language descriptions of processes and systems.
Files
Skill
Size
Entrypoint
Format
Open file
Syntax-highlighted preview of this file as included in the skill package.
scripts/add-icon-to-diagram.py
1#!/usr/bin/env python32"""3Add icons from Excalidraw libraries to diagrams.45This script reads an icon JSON file from an Excalidraw library, transforms its coordinates6to a target position, generates unique IDs, and adds it to an existing Excalidraw diagram.7Works with any Excalidraw library (AWS, GCP, Azure, Kubernetes, etc.).89Usage:10python add-icon-to-diagram.py <diagram_path> <icon_name> <x> <y> [OPTIONS]1112Options:13--library-path PATH Path to the icon library directory (default: aws-architecture-icons)14--label TEXT Add a text label below the icon15--use-edit-suffix Edit via .excalidraw.edit to avoid editor overwrite issues (enabled by default; use --no-use-edit-suffix to disable)1617Examples:18python add-icon-to-diagram.py diagram.excalidraw EC2 500 30019python add-icon-to-diagram.py diagram.excalidraw EC2 500 300 --label "Web Server"20python add-icon-to-diagram.py diagram.excalidraw VPC 200 150 --library-path libraries/gcp-icons21python add-icon-to-diagram.py diagram.excalidraw EC2 500 300 --use-edit-suffix22"""2324import json25import sys26import uuid27from pathlib import Path28from typing import Dict, List, Any, Tuple293031def generate_unique_id() -> str:32"""Generate a unique ID for Excalidraw elements."""33return str(uuid.uuid4()).replace('-', '')[:16]343536def calculate_bounding_box(elements: List[Dict[str, Any]]) -> Tuple[float, float, float, float]:37"""Calculate the bounding box (min_x, min_y, max_x, max_y) of icon elements."""38if not elements:39return (0, 0, 0, 0)4041min_x = float('inf')42min_y = float('inf')43max_x = float('-inf')44max_y = float('-inf')4546for element in elements:47if 'x' in element and 'y' in element:48x = element['x']49y = element['y']50width = element.get('width', 0)51height = element.get('height', 0)5253min_x = min(min_x, x)54min_y = min(min_y, y)55max_x = max(max_x, x + width)56max_y = max(max_y, y + height)5758return (min_x, min_y, max_x, max_y)596061def transform_icon_elements(62elements: List[Dict[str, Any]],63target_x: float,64target_y: float65) -> List[Dict[str, Any]]:66"""67Transform icon elements to target coordinates with unique IDs.6869Args:70elements: Icon elements from JSON file71target_x: Target X coordinate (top-left position)72target_y: Target Y coordinate (top-left position)7374Returns:75Transformed elements with new coordinates and IDs76"""77if not elements:78return []7980# Calculate bounding box81min_x, min_y, max_x, max_y = calculate_bounding_box(elements)8283# Calculate offset84offset_x = target_x - min_x85offset_y = target_y - min_y8687# Create ID mapping: old_id -> new_id88id_mapping = {}89for element in elements:90if 'id' in element:91old_id = element['id']92id_mapping[old_id] = generate_unique_id()9394# Create group ID mapping95group_id_mapping = {}96for element in elements:97if 'groupIds' in element:98for old_group_id in element['groupIds']:99if old_group_id not in group_id_mapping:100group_id_mapping[old_group_id] = generate_unique_id()101102# Transform elements103transformed = []104for element in elements:105new_element = element.copy()106107# Update coordinates108if 'x' in new_element:109new_element['x'] = new_element['x'] + offset_x110if 'y' in new_element:111new_element['y'] = new_element['y'] + offset_y112113# Update ID114if 'id' in new_element:115new_element['id'] = id_mapping[new_element['id']]116117# Update group IDs118if 'groupIds' in new_element:119new_element['groupIds'] = [120group_id_mapping[gid] for gid in new_element['groupIds']121]122123# Update binding references if they exist124if 'startBinding' in new_element and new_element['startBinding']:125if 'elementId' in new_element['startBinding']:126old_id = new_element['startBinding']['elementId']127if old_id in id_mapping:128new_element['startBinding']['elementId'] = id_mapping[old_id]129130if 'endBinding' in new_element and new_element['endBinding']:131if 'elementId' in new_element['endBinding']:132old_id = new_element['endBinding']['elementId']133if old_id in id_mapping:134new_element['endBinding']['elementId'] = id_mapping[old_id]135136# Update containerId if it exists137if 'containerId' in new_element and new_element['containerId']:138old_id = new_element['containerId']139if old_id in id_mapping:140new_element['containerId'] = id_mapping[old_id]141142# Update boundElements if they exist143if 'boundElements' in new_element and new_element['boundElements']:144new_bound_elements = []145for bound_elem in new_element['boundElements']:146if isinstance(bound_elem, dict) and 'id' in bound_elem:147old_id = bound_elem['id']148if old_id in id_mapping:149bound_elem['id'] = id_mapping[old_id]150new_bound_elements.append(bound_elem)151new_element['boundElements'] = new_bound_elements152153transformed.append(new_element)154155return transformed156157158def load_icon(icon_name: str, library_path: Path) -> List[Dict[str, Any]]:159"""160Load icon elements from library.161162Args:163icon_name: Name of the icon (e.g., "EC2", "VPC")164library_path: Path to the icon library directory165166Returns:167List of icon elements168"""169icon_file = library_path / "icons" / f"{icon_name}.json"170171if not icon_file.exists():172raise FileNotFoundError(f"Icon file not found: {icon_file}")173174with open(icon_file, 'r', encoding='utf-8') as f:175icon_data = json.load(f)176177return icon_data.get('elements', [])178179180def prepare_edit_path(diagram_path: Path, use_edit_suffix: bool) -> tuple[Path, Path | None]:181"""182Prepare a safe edit path to avoid editor overwrite issues.183184Returns:185(work_path, final_path)186- work_path: file path to read/write during edit187- final_path: file path to rename back to (or None if not used)188"""189if not use_edit_suffix:190return diagram_path, None191192if diagram_path.suffix != ".excalidraw":193return diagram_path, None194195edit_path = diagram_path.with_suffix(diagram_path.suffix + ".edit")196197if diagram_path.exists():198if edit_path.exists():199raise FileExistsError(f"Edit file already exists: {edit_path}")200diagram_path.rename(edit_path)201202return edit_path, diagram_path203204205def finalize_edit_path(work_path: Path, final_path: Path | None) -> None:206"""Finalize edit by renaming .edit back to .excalidraw if needed."""207if final_path is None:208return209210if final_path.exists():211final_path.unlink()212213work_path.rename(final_path)214215216def create_text_label(text: str, x: float, y: float) -> Dict[str, Any]:217"""218Create a text label element.219220Args:221text: Label text222x: X coordinate223y: Y coordinate224225Returns:226Text element dictionary227"""228return {229"id": generate_unique_id(),230"type": "text",231"x": x,232"y": y,233"width": len(text) * 10, # Approximate width234"height": 20,235"angle": 0,236"strokeColor": "#1e1e1e",237"backgroundColor": "transparent",238"fillStyle": "solid",239"strokeWidth": 2,240"strokeStyle": "solid",241"roughness": 1,242"opacity": 100,243"groupIds": [],244"frameId": None,245"index": "a0",246"roundness": None,247"seed": 1000000000 + hash(text) % 1000000000,248"version": 1,249"versionNonce": 2000000000 + hash(text) % 1000000000,250"isDeleted": False,251"boundElements": [],252"updated": 1738195200000,253"link": None,254"locked": False,255"text": text,256"fontSize": 16,257"fontFamily": 5, # Excalifont258"textAlign": "center",259"verticalAlign": "top",260"containerId": None,261"originalText": text,262"autoResize": True,263"lineHeight": 1.25264}265266267def add_icon_to_diagram(268diagram_path: Path,269icon_name: str,270x: float,271y: float,272library_path: Path,273label: str = None274) -> None:275"""276Add an icon to an Excalidraw diagram.277278Args:279diagram_path: Path to the Excalidraw diagram file280icon_name: Name of the icon to add281x: Target X coordinate282y: Target Y coordinate283library_path: Path to the icon library directory284label: Optional text label to add below the icon285"""286# Load icon elements287print(f"Loading icon: {icon_name}")288icon_elements = load_icon(icon_name, library_path)289print(f" Loaded {len(icon_elements)} elements")290291# Transform icon elements292print(f"Transforming to position ({x}, {y})")293transformed_elements = transform_icon_elements(icon_elements, x, y)294295# Calculate icon bounding box for label positioning296if label and transformed_elements:297min_x, min_y, max_x, max_y = calculate_bounding_box(transformed_elements)298icon_width = max_x - min_x299icon_height = max_y - min_y300301# Position label below icon, centered302label_x = min_x + (icon_width / 2) - (len(label) * 5)303label_y = max_y + 10304305label_element = create_text_label(label, label_x, label_y)306transformed_elements.append(label_element)307print(f" Added label: '{label}'")308309# Load diagram310print(f"Loading diagram: {diagram_path}")311with open(diagram_path, 'r', encoding='utf-8') as f:312diagram = json.load(f)313314# Add transformed elements315if 'elements' not in diagram:316diagram['elements'] = []317318original_count = len(diagram['elements'])319diagram['elements'].extend(transformed_elements)320print(f" Added {len(transformed_elements)} elements (total: {original_count} -> {len(diagram['elements'])})")321322# Save diagram323print(f"Saving diagram")324with open(diagram_path, 'w', encoding='utf-8') as f:325json.dump(diagram, f, indent=2, ensure_ascii=False)326327print(f"✓ Successfully added '{icon_name}' icon to diagram")328329330def main():331"""Main entry point."""332if hasattr(sys.stdout, "reconfigure"):333# Ensure consistent UTF-8 output on Windows consoles.334sys.stdout.reconfigure(encoding="utf-8")335if len(sys.argv) < 5:336print("Usage: python add-icon-to-diagram.py <diagram_path> <icon_name> <x> <y> [OPTIONS]")337print("\nOptions:")338print(" --library-path PATH Path to icon library directory")339print(" --label TEXT Add text label below icon")340print(" --use-edit-suffix Edit via .excalidraw.edit to avoid editor overwrite issues (enabled by default; use --no-use-edit-suffix to disable)")341print("\nExamples:")342print(" python add-icon-to-diagram.py diagram.excalidraw EC2 500 300")343print(" python add-icon-to-diagram.py diagram.excalidraw EC2 500 300 --label 'Web Server'")344sys.exit(1)345346diagram_path = Path(sys.argv[1])347icon_name = sys.argv[2]348x = float(sys.argv[3])349y = float(sys.argv[4])350351# Default library path352script_dir = Path(__file__).parent353default_library_path = script_dir.parent / "libraries" / "aws-architecture-icons"354355# Parse optional arguments356library_path = default_library_path357label = None358# Default: use edit suffix to avoid editor overwrite issues359use_edit_suffix = True360361i = 5362while i < len(sys.argv):363if sys.argv[i] == '--library-path':364if i + 1 < len(sys.argv):365library_path = Path(sys.argv[i + 1])366i += 2367else:368print("Error: --library-path requires a path argument")369sys.exit(1)370elif sys.argv[i] == '--label':371if i + 1 < len(sys.argv):372label = sys.argv[i + 1]373i += 2374else:375print("Error: --label requires a text argument")376sys.exit(1)377elif sys.argv[i] == '--use-edit-suffix':378use_edit_suffix = True379i += 1380elif sys.argv[i] == '--no-use-edit-suffix':381use_edit_suffix = False382i += 1383else:384print(f"Error: Unknown option: {sys.argv[i]}")385sys.exit(1)386387# Validate inputs388if not diagram_path.exists():389print(f"Error: Diagram file not found: {diagram_path}")390sys.exit(1)391392if not library_path.exists():393print(f"Error: Library path not found: {library_path}")394sys.exit(1)395396try:397work_path, final_path = prepare_edit_path(diagram_path, use_edit_suffix)398add_icon_to_diagram(work_path, icon_name, x, y, library_path, label)399finalize_edit_path(work_path, final_path)400except Exception as e:401print(f"Error: {e}")402sys.exit(1)403404405if __name__ == '__main__':406main()407408