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-arrow.py
1#!/usr/bin/env python32"""3Add arrows (connections) between elements in Excalidraw diagrams.45Usage:6python add-arrow.py <diagram_path> <from_x> <from_y> <to_x> <to_y> [OPTIONS]78Options:9--style {solid|dashed|dotted} Arrow line style (default: solid)10--color HEX Arrow color (default: #1e1e1e)11--label TEXT Add text label on the arrow12--use-edit-suffix Edit via .excalidraw.edit to avoid editor overwrite issues (enabled by default; use --no-use-edit-suffix to disable)1314Examples:15python add-arrow.py diagram.excalidraw 300 200 500 30016python add-arrow.py diagram.excalidraw 300 200 500 300 --label "HTTP"17python add-arrow.py diagram.excalidraw 300 200 500 300 --style dashed --color "#7950f2"18python add-arrow.py diagram.excalidraw 300 200 500 300 --use-edit-suffix19"""2021import json22import sys23import uuid24from pathlib import Path25from typing import Dict, Any262728def generate_unique_id() -> str:29"""Generate a unique ID for Excalidraw elements."""30return str(uuid.uuid4()).replace('-', '')[:16]313233def prepare_edit_path(diagram_path: Path, use_edit_suffix: bool) -> tuple[Path, Path | None]:34"""35Prepare a safe edit path to avoid editor overwrite issues.3637Returns:38(work_path, final_path)39- work_path: file path to read/write during edit40- final_path: file path to rename back to (or None if not used)41"""42if not use_edit_suffix:43return diagram_path, None4445if diagram_path.suffix != ".excalidraw":46return diagram_path, None4748edit_path = diagram_path.with_suffix(diagram_path.suffix + ".edit")4950if diagram_path.exists():51if edit_path.exists():52raise FileExistsError(f"Edit file already exists: {edit_path}")53diagram_path.rename(edit_path)5455return edit_path, diagram_path565758def finalize_edit_path(work_path: Path, final_path: Path | None) -> None:59"""Finalize edit by renaming .edit back to .excalidraw if needed."""60if final_path is None:61return6263if final_path.exists():64final_path.unlink()6566work_path.rename(final_path)676869def create_arrow(70from_x: float,71from_y: float,72to_x: float,73to_y: float,74style: str = "solid",75color: str = "#1e1e1e",76label: str = None77) -> list:78"""79Create an arrow element.8081Args:82from_x: Starting X coordinate83from_y: Starting Y coordinate84to_x: Ending X coordinate85to_y: Ending Y coordinate86style: Line style (solid, dashed, dotted)87color: Arrow color88label: Optional text label on the arrow8990Returns:91List of elements (arrow and optional label)92"""93elements = []9495# Arrow element96arrow = {97"id": generate_unique_id(),98"type": "arrow",99"x": from_x,100"y": from_y,101"width": to_x - from_x,102"height": to_y - from_y,103"angle": 0,104"strokeColor": color,105"backgroundColor": "transparent",106"fillStyle": "solid",107"strokeWidth": 2,108"strokeStyle": style,109"roughness": 1,110"opacity": 100,111"groupIds": [],112"frameId": None,113"index": "a0",114"roundness": {115"type": 2116},117"seed": 1000000000 + hash(f"{from_x}{from_y}{to_x}{to_y}") % 1000000000,118"version": 1,119"versionNonce": 2000000000 + hash(f"{from_x}{from_y}{to_x}{to_y}") % 1000000000,120"isDeleted": False,121"boundElements": [],122"updated": 1738195200000,123"link": None,124"locked": False,125"points": [126[0, 0],127[to_x - from_x, to_y - from_y]128],129"startBinding": None,130"endBinding": None,131"startArrowhead": None,132"endArrowhead": "arrow",133"lastCommittedPoint": None134}135elements.append(arrow)136137# Optional label138if label:139mid_x = (from_x + to_x) / 2 - (len(label) * 5)140mid_y = (from_y + to_y) / 2 - 10141142label_element = {143"id": generate_unique_id(),144"type": "text",145"x": mid_x,146"y": mid_y,147"width": len(label) * 10,148"height": 20,149"angle": 0,150"strokeColor": color,151"backgroundColor": "transparent",152"fillStyle": "solid",153"strokeWidth": 2,154"strokeStyle": "solid",155"roughness": 1,156"opacity": 100,157"groupIds": [],158"frameId": None,159"index": "a0",160"roundness": None,161"seed": 1000000000 + hash(label) % 1000000000,162"version": 1,163"versionNonce": 2000000000 + hash(label) % 1000000000,164"isDeleted": False,165"boundElements": [],166"updated": 1738195200000,167"link": None,168"locked": False,169"text": label,170"fontSize": 14,171"fontFamily": 5,172"textAlign": "center",173"verticalAlign": "top",174"containerId": None,175"originalText": label,176"autoResize": True,177"lineHeight": 1.25178}179elements.append(label_element)180181return elements182183184def add_arrow_to_diagram(185diagram_path: Path,186from_x: float,187from_y: float,188to_x: float,189to_y: float,190style: str = "solid",191color: str = "#1e1e1e",192label: str = None193) -> None:194"""195Add an arrow to an Excalidraw diagram.196197Args:198diagram_path: Path to the Excalidraw diagram file199from_x: Starting X coordinate200from_y: Starting Y coordinate201to_x: Ending X coordinate202to_y: Ending Y coordinate203style: Line style (solid, dashed, dotted)204color: Arrow color205label: Optional text label206"""207print(f"Creating arrow from ({from_x}, {from_y}) to ({to_x}, {to_y})")208arrow_elements = create_arrow(from_x, from_y, to_x, to_y, style, color, label)209210if label:211print(f" With label: '{label}'")212213# Load diagram214print(f"Loading diagram: {diagram_path}")215with open(diagram_path, 'r', encoding='utf-8') as f:216diagram = json.load(f)217218# Add arrow elements219if 'elements' not in diagram:220diagram['elements'] = []221222original_count = len(diagram['elements'])223diagram['elements'].extend(arrow_elements)224print(f" Added {len(arrow_elements)} elements (total: {original_count} -> {len(diagram['elements'])})")225226# Save diagram227print(f"Saving diagram")228with open(diagram_path, 'w', encoding='utf-8') as f:229json.dump(diagram, f, indent=2, ensure_ascii=False)230231print(f"✓ Successfully added arrow to diagram")232233234def main():235"""Main entry point."""236if hasattr(sys.stdout, "reconfigure"):237# Ensure consistent UTF-8 output on Windows consoles.238sys.stdout.reconfigure(encoding="utf-8")239if len(sys.argv) < 6:240print("Usage: python add-arrow.py <diagram_path> <from_x> <from_y> <to_x> <to_y> [OPTIONS]")241print("\nOptions:")242print(" --style {solid|dashed|dotted} Line style (default: solid)")243print(" --color HEX Color (default: #1e1e1e)")244print(" --label TEXT Text label on arrow")245print(" --use-edit-suffix Edit via .excalidraw.edit to avoid editor overwrite issues (enabled by default; use --no-use-edit-suffix to disable)")246print("\nExamples:")247print(" python add-arrow.py diagram.excalidraw 300 200 500 300")248print(" python add-arrow.py diagram.excalidraw 300 200 500 300 --label 'HTTP'")249sys.exit(1)250251diagram_path = Path(sys.argv[1])252from_x = float(sys.argv[2])253from_y = float(sys.argv[3])254to_x = float(sys.argv[4])255to_y = float(sys.argv[5])256257# Parse optional arguments258style = "solid"259color = "#1e1e1e"260label = None261# Default: use edit suffix to avoid editor overwrite issues262use_edit_suffix = True263264i = 6265while i < len(sys.argv):266if sys.argv[i] == '--style':267if i + 1 < len(sys.argv):268style = sys.argv[i + 1]269if style not in ['solid', 'dashed', 'dotted']:270print(f"Error: Invalid style '{style}'. Must be: solid, dashed, or dotted")271sys.exit(1)272i += 2273else:274print("Error: --style requires an argument")275sys.exit(1)276elif sys.argv[i] == '--color':277if i + 1 < len(sys.argv):278color = sys.argv[i + 1]279i += 2280else:281print("Error: --color requires an argument")282sys.exit(1)283elif sys.argv[i] == '--label':284if i + 1 < len(sys.argv):285label = sys.argv[i + 1]286i += 2287else:288print("Error: --label requires a text argument")289sys.exit(1)290elif sys.argv[i] == '--use-edit-suffix':291use_edit_suffix = True292i += 1293elif sys.argv[i] == '--no-use-edit-suffix':294use_edit_suffix = False295i += 1296else:297print(f"Error: Unknown option: {sys.argv[i]}")298sys.exit(1)299300# Validate inputs301if not diagram_path.exists():302print(f"Error: Diagram file not found: {diagram_path}")303sys.exit(1)304305try:306work_path, final_path = prepare_edit_path(diagram_path, use_edit_suffix)307add_arrow_to_diagram(work_path, from_x, from_y, to_x, to_y, style, color, label)308finalize_edit_path(work_path, final_path)309except Exception as e:310print(f"Error: {e}")311sys.exit(1)312313314if __name__ == '__main__':315main()316