mirror of
https://github.com/github/awesome-copilot.git
synced 2026-02-23 20:05:12 +00:00
feat(skills): add excalidraw-diagram-generator skill and docs update
This commit is contained in:
193
skills/excalidraw-diagram-generator/scripts/README.md
Normal file
193
skills/excalidraw-diagram-generator/scripts/README.md
Normal file
@@ -0,0 +1,193 @@
|
||||
# Excalidraw Library Tools
|
||||
|
||||
This directory contains scripts for working with Excalidraw libraries.
|
||||
|
||||
## split-excalidraw-library.py
|
||||
|
||||
Splits an Excalidraw library file (`*.excalidrawlib`) into individual icon JSON files for efficient token usage by AI assistants.
|
||||
|
||||
### Prerequisites
|
||||
|
||||
- Python 3.6 or higher
|
||||
- No additional dependencies required (uses only standard library)
|
||||
|
||||
### Usage
|
||||
|
||||
```bash
|
||||
python split-excalidraw-library.py <path-to-library-directory>
|
||||
```
|
||||
|
||||
### Step-by-Step Workflow
|
||||
|
||||
1. **Create library directory**:
|
||||
```bash
|
||||
mkdir -p skills/excalidraw-diagram-generator/libraries/aws-architecture-icons
|
||||
```
|
||||
|
||||
2. **Download and place library file**:
|
||||
- Visit: https://libraries.excalidraw.com/
|
||||
- Search for "AWS Architecture Icons" and download the `.excalidrawlib` file
|
||||
- Rename it to match the directory name: `aws-architecture-icons.excalidrawlib`
|
||||
- Place it in the directory created in step 1
|
||||
|
||||
3. **Run the script**:
|
||||
```bash
|
||||
python skills/excalidraw-diagram-generator/scripts/split-excalidraw-library.py skills/excalidraw-diagram-generator/libraries/aws-architecture-icons/
|
||||
```
|
||||
|
||||
### Output Structure
|
||||
|
||||
The script creates the following structure in the library directory:
|
||||
|
||||
```
|
||||
skills/excalidraw-diagram-generator/libraries/aws-architecture-icons/
|
||||
aws-architecture-icons.excalidrawlib # Original file (kept)
|
||||
reference.md # Generated: Quick reference table
|
||||
icons/ # Generated: Individual icon files
|
||||
API-Gateway.json
|
||||
CloudFront.json
|
||||
EC2.json
|
||||
S3.json
|
||||
...
|
||||
```
|
||||
|
||||
### What the Script Does
|
||||
|
||||
1. **Reads** the `.excalidrawlib` file
|
||||
2. **Extracts** each icon from the `libraryItems` array
|
||||
3. **Sanitizes** icon names to create valid filenames (spaces → hyphens, removes special characters)
|
||||
4. **Saves** each icon as a separate JSON file in the `icons/` directory
|
||||
5. **Generates** a `reference.md` file with a table mapping icon names to filenames
|
||||
|
||||
### Benefits
|
||||
|
||||
- **Token Efficiency**: AI can first read the lightweight `reference.md` to find relevant icons, then load only the specific icon files needed
|
||||
- **Organization**: Icons are organized in a clear directory structure
|
||||
- **Extensibility**: Users can add multiple library sets side-by-side
|
||||
|
||||
### Recommended Workflow
|
||||
|
||||
1. Download desired Excalidraw libraries from https://libraries.excalidraw.com/
|
||||
2. Run this script on each library file
|
||||
3. Move the generated folders to `../libraries/`
|
||||
4. The AI assistant will use `reference.md` files to locate and use icons efficiently
|
||||
|
||||
### Library Sources (Examples — verify availability)
|
||||
|
||||
- Examples found on https://libraries.excalidraw.com/ may include cloud/service icon sets.
|
||||
- Availability changes over time; verify the exact library names on the site before use.
|
||||
- This script works with any valid `.excalidrawlib` file you provide.
|
||||
|
||||
### Troubleshooting
|
||||
|
||||
**Error: File not found**
|
||||
- Check that the file path is correct
|
||||
- Make sure the file has a `.excalidrawlib` extension
|
||||
|
||||
**Error: Invalid library file format**
|
||||
- Ensure the file is a valid Excalidraw library file
|
||||
- Check that it contains a `libraryItems` array
|
||||
|
||||
### License Considerations
|
||||
|
||||
When using third-party icon libraries:
|
||||
- **AWS Architecture Icons**: Subject to AWS Content License
|
||||
- **GCP Icons**: Subject to Google's terms
|
||||
- **Other libraries**: Check each library's license
|
||||
|
||||
This script is for personal/organizational use. Redistribution of split icon files should comply with the original library's license terms.
|
||||
|
||||
## add-icon-to-diagram.py
|
||||
|
||||
Adds a specific icon from a split Excalidraw library into an existing `.excalidraw` diagram. The script handles coordinate translation and ID collision avoidance, and can optionally add a label under the icon.
|
||||
|
||||
### Prerequisites
|
||||
|
||||
- Python 3.6 or higher
|
||||
- A diagram file (`.excalidraw`)
|
||||
- A split icon library directory (created by `split-excalidraw-library.py`)
|
||||
|
||||
### Usage
|
||||
|
||||
```bash
|
||||
python add-icon-to-diagram.py <diagram-path> <icon-name> <x> <y> [OPTIONS]
|
||||
```
|
||||
|
||||
**Options**
|
||||
- `--library-path PATH` : Path to the icon library directory (default: `aws-architecture-icons`)
|
||||
- `--label TEXT` : Add a text label below the icon
|
||||
-- `--use-edit-suffix` : Edit via `.excalidraw.edit` to avoid editor overwrite issues (enabled by default; pass `--no-use-edit-suffix` to disable)
|
||||
|
||||
### Examples
|
||||
|
||||
```bash
|
||||
# Add EC2 icon at position (400, 300)
|
||||
python add-icon-to-diagram.py diagram.excalidraw EC2 400 300
|
||||
|
||||
# Add VPC icon with label
|
||||
python add-icon-to-diagram.py diagram.excalidraw VPC 200 150 --label "VPC"
|
||||
|
||||
# Safe edit mode is enabled by default (avoids editor overwrite issues)
|
||||
# Use `--no-use-edit-suffix` to disable
|
||||
python add-icon-to-diagram.py diagram.excalidraw EC2 500 300
|
||||
|
||||
# Add icon from another library
|
||||
python add-icon-to-diagram.py diagram.excalidraw Compute-Engine 500 200 \
|
||||
--library-path libraries/gcp-icons --label "API Server"
|
||||
```
|
||||
|
||||
### What the Script Does
|
||||
|
||||
1. **Loads** the icon JSON from the library’s `icons/` directory
|
||||
2. **Calculates** the icon’s bounding box
|
||||
3. **Offsets** all coordinates to the target position
|
||||
4. **Generates** unique IDs for all elements and groups
|
||||
5. **Appends** the transformed elements to the diagram
|
||||
6. **(Optional)** Adds a label beneath the icon
|
||||
|
||||
---
|
||||
|
||||
## add-arrow.py
|
||||
|
||||
Adds a straight arrow between two points in an existing `.excalidraw` diagram. Supports optional labels and line styles.
|
||||
|
||||
### Prerequisites
|
||||
|
||||
- Python 3.6 or higher
|
||||
- A diagram file (`.excalidraw`)
|
||||
|
||||
### Usage
|
||||
|
||||
```bash
|
||||
python add-arrow.py <diagram-path> <from-x> <from-y> <to-x> <to-y> [OPTIONS]
|
||||
```
|
||||
|
||||
**Options**
|
||||
- `--style {solid|dashed|dotted}` : Line style (default: `solid`)
|
||||
- `--color HEX` : Arrow color (default: `#1e1e1e`)
|
||||
- `--label TEXT` : Add a text label on the arrow
|
||||
-- `--use-edit-suffix` : Edit via `.excalidraw.edit` to avoid editor overwrite issues (enabled by default; pass `--no-use-edit-suffix` to disable)
|
||||
|
||||
### Examples
|
||||
|
||||
```bash
|
||||
# Simple arrow
|
||||
python add-arrow.py diagram.excalidraw 300 200 500 300
|
||||
|
||||
# Arrow with label
|
||||
python add-arrow.py diagram.excalidraw 300 200 500 300 --label "HTTPS"
|
||||
|
||||
# Dashed arrow with custom color
|
||||
python add-arrow.py diagram.excalidraw 400 350 600 400 --style dashed --color "#7950f2"
|
||||
|
||||
# Safe edit mode is enabled by default (avoids editor overwrite issues)
|
||||
# Use `--no-use-edit-suffix` to disable
|
||||
python add-arrow.py diagram.excalidraw 300 200 500 300
|
||||
```
|
||||
|
||||
### What the Script Does
|
||||
|
||||
1. **Creates** an arrow element from the given coordinates
|
||||
2. **(Optional)** Adds a label near the arrow midpoint
|
||||
3. **Appends** elements to the diagram
|
||||
4. **Saves** the updated file
|
||||
312
skills/excalidraw-diagram-generator/scripts/add-arrow.py
Normal file
312
skills/excalidraw-diagram-generator/scripts/add-arrow.py
Normal file
@@ -0,0 +1,312 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Add arrows (connections) between elements in Excalidraw diagrams.
|
||||
|
||||
Usage:
|
||||
python add-arrow.py <diagram_path> <from_x> <from_y> <to_x> <to_y> [OPTIONS]
|
||||
|
||||
Options:
|
||||
--style {solid|dashed|dotted} Arrow line style (default: solid)
|
||||
--color HEX Arrow color (default: #1e1e1e)
|
||||
--label TEXT Add text label on the arrow
|
||||
--use-edit-suffix Edit via .excalidraw.edit to avoid editor overwrite issues (enabled by default; use --no-use-edit-suffix to disable)
|
||||
|
||||
Examples:
|
||||
python add-arrow.py diagram.excalidraw 300 200 500 300
|
||||
python add-arrow.py diagram.excalidraw 300 200 500 300 --label "HTTP"
|
||||
python add-arrow.py diagram.excalidraw 300 200 500 300 --style dashed --color "#7950f2"
|
||||
python add-arrow.py diagram.excalidraw 300 200 500 300 --use-edit-suffix
|
||||
"""
|
||||
|
||||
import json
|
||||
import sys
|
||||
import uuid
|
||||
from pathlib import Path
|
||||
from typing import Dict, Any
|
||||
|
||||
|
||||
def generate_unique_id() -> str:
|
||||
"""Generate a unique ID for Excalidraw elements."""
|
||||
return str(uuid.uuid4()).replace('-', '')[:16]
|
||||
|
||||
|
||||
def prepare_edit_path(diagram_path: Path, use_edit_suffix: bool) -> tuple[Path, Path | None]:
|
||||
"""
|
||||
Prepare a safe edit path to avoid editor overwrite issues.
|
||||
|
||||
Returns:
|
||||
(work_path, final_path)
|
||||
- work_path: file path to read/write during edit
|
||||
- final_path: file path to rename back to (or None if not used)
|
||||
"""
|
||||
if not use_edit_suffix:
|
||||
return diagram_path, None
|
||||
|
||||
if diagram_path.suffix != ".excalidraw":
|
||||
return diagram_path, None
|
||||
|
||||
edit_path = diagram_path.with_suffix(diagram_path.suffix + ".edit")
|
||||
|
||||
if diagram_path.exists():
|
||||
if edit_path.exists():
|
||||
raise FileExistsError(f"Edit file already exists: {edit_path}")
|
||||
diagram_path.rename(edit_path)
|
||||
|
||||
return edit_path, diagram_path
|
||||
|
||||
|
||||
def finalize_edit_path(work_path: Path, final_path: Path | None) -> None:
|
||||
"""Finalize edit by renaming .edit back to .excalidraw if needed."""
|
||||
if final_path is None:
|
||||
return
|
||||
|
||||
if final_path.exists():
|
||||
final_path.unlink()
|
||||
|
||||
work_path.rename(final_path)
|
||||
|
||||
|
||||
def create_arrow(
|
||||
from_x: float,
|
||||
from_y: float,
|
||||
to_x: float,
|
||||
to_y: float,
|
||||
style: str = "solid",
|
||||
color: str = "#1e1e1e",
|
||||
label: str = None
|
||||
) -> list:
|
||||
"""
|
||||
Create an arrow element.
|
||||
|
||||
Args:
|
||||
from_x: Starting X coordinate
|
||||
from_y: Starting Y coordinate
|
||||
to_x: Ending X coordinate
|
||||
to_y: Ending Y coordinate
|
||||
style: Line style (solid, dashed, dotted)
|
||||
color: Arrow color
|
||||
label: Optional text label on the arrow
|
||||
|
||||
Returns:
|
||||
List of elements (arrow and optional label)
|
||||
"""
|
||||
elements = []
|
||||
|
||||
# Arrow element
|
||||
arrow = {
|
||||
"id": generate_unique_id(),
|
||||
"type": "arrow",
|
||||
"x": from_x,
|
||||
"y": from_y,
|
||||
"width": to_x - from_x,
|
||||
"height": to_y - from_y,
|
||||
"angle": 0,
|
||||
"strokeColor": color,
|
||||
"backgroundColor": "transparent",
|
||||
"fillStyle": "solid",
|
||||
"strokeWidth": 2,
|
||||
"strokeStyle": style,
|
||||
"roughness": 1,
|
||||
"opacity": 100,
|
||||
"groupIds": [],
|
||||
"frameId": None,
|
||||
"index": "a0",
|
||||
"roundness": {
|
||||
"type": 2
|
||||
},
|
||||
"seed": 1000000000 + hash(f"{from_x}{from_y}{to_x}{to_y}") % 1000000000,
|
||||
"version": 1,
|
||||
"versionNonce": 2000000000 + hash(f"{from_x}{from_y}{to_x}{to_y}") % 1000000000,
|
||||
"isDeleted": False,
|
||||
"boundElements": [],
|
||||
"updated": 1738195200000,
|
||||
"link": None,
|
||||
"locked": False,
|
||||
"points": [
|
||||
[0, 0],
|
||||
[to_x - from_x, to_y - from_y]
|
||||
],
|
||||
"startBinding": None,
|
||||
"endBinding": None,
|
||||
"startArrowhead": None,
|
||||
"endArrowhead": "arrow",
|
||||
"lastCommittedPoint": None
|
||||
}
|
||||
elements.append(arrow)
|
||||
|
||||
# Optional label
|
||||
if label:
|
||||
mid_x = (from_x + to_x) / 2 - (len(label) * 5)
|
||||
mid_y = (from_y + to_y) / 2 - 10
|
||||
|
||||
label_element = {
|
||||
"id": generate_unique_id(),
|
||||
"type": "text",
|
||||
"x": mid_x,
|
||||
"y": mid_y,
|
||||
"width": len(label) * 10,
|
||||
"height": 20,
|
||||
"angle": 0,
|
||||
"strokeColor": color,
|
||||
"backgroundColor": "transparent",
|
||||
"fillStyle": "solid",
|
||||
"strokeWidth": 2,
|
||||
"strokeStyle": "solid",
|
||||
"roughness": 1,
|
||||
"opacity": 100,
|
||||
"groupIds": [],
|
||||
"frameId": None,
|
||||
"index": "a0",
|
||||
"roundness": None,
|
||||
"seed": 1000000000 + hash(label) % 1000000000,
|
||||
"version": 1,
|
||||
"versionNonce": 2000000000 + hash(label) % 1000000000,
|
||||
"isDeleted": False,
|
||||
"boundElements": [],
|
||||
"updated": 1738195200000,
|
||||
"link": None,
|
||||
"locked": False,
|
||||
"text": label,
|
||||
"fontSize": 14,
|
||||
"fontFamily": 5,
|
||||
"textAlign": "center",
|
||||
"verticalAlign": "top",
|
||||
"containerId": None,
|
||||
"originalText": label,
|
||||
"autoResize": True,
|
||||
"lineHeight": 1.25
|
||||
}
|
||||
elements.append(label_element)
|
||||
|
||||
return elements
|
||||
|
||||
|
||||
def add_arrow_to_diagram(
|
||||
diagram_path: Path,
|
||||
from_x: float,
|
||||
from_y: float,
|
||||
to_x: float,
|
||||
to_y: float,
|
||||
style: str = "solid",
|
||||
color: str = "#1e1e1e",
|
||||
label: str = None
|
||||
) -> None:
|
||||
"""
|
||||
Add an arrow to an Excalidraw diagram.
|
||||
|
||||
Args:
|
||||
diagram_path: Path to the Excalidraw diagram file
|
||||
from_x: Starting X coordinate
|
||||
from_y: Starting Y coordinate
|
||||
to_x: Ending X coordinate
|
||||
to_y: Ending Y coordinate
|
||||
style: Line style (solid, dashed, dotted)
|
||||
color: Arrow color
|
||||
label: Optional text label
|
||||
"""
|
||||
print(f"Creating arrow from ({from_x}, {from_y}) to ({to_x}, {to_y})")
|
||||
arrow_elements = create_arrow(from_x, from_y, to_x, to_y, style, color, label)
|
||||
|
||||
if label:
|
||||
print(f" With label: '{label}'")
|
||||
|
||||
# Load diagram
|
||||
print(f"Loading diagram: {diagram_path}")
|
||||
with open(diagram_path, 'r', encoding='utf-8') as f:
|
||||
diagram = json.load(f)
|
||||
|
||||
# Add arrow elements
|
||||
if 'elements' not in diagram:
|
||||
diagram['elements'] = []
|
||||
|
||||
original_count = len(diagram['elements'])
|
||||
diagram['elements'].extend(arrow_elements)
|
||||
print(f" Added {len(arrow_elements)} elements (total: {original_count} -> {len(diagram['elements'])})")
|
||||
|
||||
# Save diagram
|
||||
print(f"Saving diagram")
|
||||
with open(diagram_path, 'w', encoding='utf-8') as f:
|
||||
json.dump(diagram, f, indent=2, ensure_ascii=False)
|
||||
|
||||
print(f"✓ Successfully added arrow to diagram")
|
||||
|
||||
|
||||
def main():
|
||||
"""Main entry point."""
|
||||
if len(sys.argv) < 6:
|
||||
print("Usage: python add-arrow.py <diagram_path> <from_x> <from_y> <to_x> <to_y> [OPTIONS]")
|
||||
print("\nOptions:")
|
||||
print(" --style {solid|dashed|dotted} Line style (default: solid)")
|
||||
print(" --color HEX Color (default: #1e1e1e)")
|
||||
print(" --label TEXT Text label on arrow")
|
||||
print(" --use-edit-suffix Edit via .excalidraw.edit to avoid editor overwrite issues (enabled by default; use --no-use-edit-suffix to disable)")
|
||||
print("\nExamples:")
|
||||
print(" python add-arrow.py diagram.excalidraw 300 200 500 300")
|
||||
print(" python add-arrow.py diagram.excalidraw 300 200 500 300 --label 'HTTP'")
|
||||
sys.exit(1)
|
||||
|
||||
diagram_path = Path(sys.argv[1])
|
||||
from_x = float(sys.argv[2])
|
||||
from_y = float(sys.argv[3])
|
||||
to_x = float(sys.argv[4])
|
||||
to_y = float(sys.argv[5])
|
||||
|
||||
# Parse optional arguments
|
||||
style = "solid"
|
||||
color = "#1e1e1e"
|
||||
label = None
|
||||
# Default: use edit suffix to avoid editor overwrite issues
|
||||
use_edit_suffix = True
|
||||
|
||||
i = 6
|
||||
while i < len(sys.argv):
|
||||
if sys.argv[i] == '--style':
|
||||
if i + 1 < len(sys.argv):
|
||||
style = sys.argv[i + 1]
|
||||
if style not in ['solid', 'dashed', 'dotted']:
|
||||
print(f"Error: Invalid style '{style}'. Must be: solid, dashed, or dotted")
|
||||
sys.exit(1)
|
||||
i += 2
|
||||
else:
|
||||
print("Error: --style requires an argument")
|
||||
sys.exit(1)
|
||||
elif sys.argv[i] == '--color':
|
||||
if i + 1 < len(sys.argv):
|
||||
color = sys.argv[i + 1]
|
||||
i += 2
|
||||
else:
|
||||
print("Error: --color requires an argument")
|
||||
sys.exit(1)
|
||||
elif sys.argv[i] == '--label':
|
||||
if i + 1 < len(sys.argv):
|
||||
label = sys.argv[i + 1]
|
||||
i += 2
|
||||
else:
|
||||
print("Error: --label requires a text argument")
|
||||
sys.exit(1)
|
||||
elif sys.argv[i] == '--use-edit-suffix':
|
||||
use_edit_suffix = True
|
||||
i += 1
|
||||
elif sys.argv[i] == '--no-use-edit-suffix':
|
||||
use_edit_suffix = False
|
||||
i += 1
|
||||
else:
|
||||
print(f"Error: Unknown option: {sys.argv[i]}")
|
||||
sys.exit(1)
|
||||
|
||||
# Validate inputs
|
||||
if not diagram_path.exists():
|
||||
print(f"Error: Diagram file not found: {diagram_path}")
|
||||
sys.exit(1)
|
||||
|
||||
try:
|
||||
work_path, final_path = prepare_edit_path(diagram_path, use_edit_suffix)
|
||||
add_arrow_to_diagram(work_path, from_x, from_y, to_x, to_y, style, color, label)
|
||||
finalize_edit_path(work_path, final_path)
|
||||
except Exception as e:
|
||||
print(f"Error: {e}")
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
@@ -0,0 +1,404 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Add icons from Excalidraw libraries to diagrams.
|
||||
|
||||
This script reads an icon JSON file from an Excalidraw library, transforms its coordinates
|
||||
to a target position, generates unique IDs, and adds it to an existing Excalidraw diagram.
|
||||
Works with any Excalidraw library (AWS, GCP, Azure, Kubernetes, etc.).
|
||||
|
||||
Usage:
|
||||
python add-icon-to-diagram.py <diagram_path> <icon_name> <x> <y> [OPTIONS]
|
||||
|
||||
Options:
|
||||
--library-path PATH Path to the icon library directory (default: aws-architecture-icons)
|
||||
--label TEXT Add a text label below the icon
|
||||
--use-edit-suffix Edit via .excalidraw.edit to avoid editor overwrite issues (enabled by default; use --no-use-edit-suffix to disable)
|
||||
|
||||
Examples:
|
||||
python add-icon-to-diagram.py diagram.excalidraw EC2 500 300
|
||||
python add-icon-to-diagram.py diagram.excalidraw EC2 500 300 --label "Web Server"
|
||||
python add-icon-to-diagram.py diagram.excalidraw VPC 200 150 --library-path libraries/gcp-icons
|
||||
python add-icon-to-diagram.py diagram.excalidraw EC2 500 300 --use-edit-suffix
|
||||
"""
|
||||
|
||||
import json
|
||||
import sys
|
||||
import uuid
|
||||
from pathlib import Path
|
||||
from typing import Dict, List, Any, Tuple
|
||||
|
||||
|
||||
def generate_unique_id() -> str:
|
||||
"""Generate a unique ID for Excalidraw elements."""
|
||||
return str(uuid.uuid4()).replace('-', '')[:16]
|
||||
|
||||
|
||||
def calculate_bounding_box(elements: List[Dict[str, Any]]) -> Tuple[float, float, float, float]:
|
||||
"""Calculate the bounding box (min_x, min_y, max_x, max_y) of icon elements."""
|
||||
if not elements:
|
||||
return (0, 0, 0, 0)
|
||||
|
||||
min_x = float('inf')
|
||||
min_y = float('inf')
|
||||
max_x = float('-inf')
|
||||
max_y = float('-inf')
|
||||
|
||||
for element in elements:
|
||||
if 'x' in element and 'y' in element:
|
||||
x = element['x']
|
||||
y = element['y']
|
||||
width = element.get('width', 0)
|
||||
height = element.get('height', 0)
|
||||
|
||||
min_x = min(min_x, x)
|
||||
min_y = min(min_y, y)
|
||||
max_x = max(max_x, x + width)
|
||||
max_y = max(max_y, y + height)
|
||||
|
||||
return (min_x, min_y, max_x, max_y)
|
||||
|
||||
|
||||
def transform_icon_elements(
|
||||
elements: List[Dict[str, Any]],
|
||||
target_x: float,
|
||||
target_y: float
|
||||
) -> List[Dict[str, Any]]:
|
||||
"""
|
||||
Transform icon elements to target coordinates with unique IDs.
|
||||
|
||||
Args:
|
||||
elements: Icon elements from JSON file
|
||||
target_x: Target X coordinate (top-left position)
|
||||
target_y: Target Y coordinate (top-left position)
|
||||
|
||||
Returns:
|
||||
Transformed elements with new coordinates and IDs
|
||||
"""
|
||||
if not elements:
|
||||
return []
|
||||
|
||||
# Calculate bounding box
|
||||
min_x, min_y, max_x, max_y = calculate_bounding_box(elements)
|
||||
|
||||
# Calculate offset
|
||||
offset_x = target_x - min_x
|
||||
offset_y = target_y - min_y
|
||||
|
||||
# Create ID mapping: old_id -> new_id
|
||||
id_mapping = {}
|
||||
for element in elements:
|
||||
if 'id' in element:
|
||||
old_id = element['id']
|
||||
id_mapping[old_id] = generate_unique_id()
|
||||
|
||||
# Create group ID mapping
|
||||
group_id_mapping = {}
|
||||
for element in elements:
|
||||
if 'groupIds' in element:
|
||||
for old_group_id in element['groupIds']:
|
||||
if old_group_id not in group_id_mapping:
|
||||
group_id_mapping[old_group_id] = generate_unique_id()
|
||||
|
||||
# Transform elements
|
||||
transformed = []
|
||||
for element in elements:
|
||||
new_element = element.copy()
|
||||
|
||||
# Update coordinates
|
||||
if 'x' in new_element:
|
||||
new_element['x'] = new_element['x'] + offset_x
|
||||
if 'y' in new_element:
|
||||
new_element['y'] = new_element['y'] + offset_y
|
||||
|
||||
# Update ID
|
||||
if 'id' in new_element:
|
||||
new_element['id'] = id_mapping[new_element['id']]
|
||||
|
||||
# Update group IDs
|
||||
if 'groupIds' in new_element:
|
||||
new_element['groupIds'] = [
|
||||
group_id_mapping[gid] for gid in new_element['groupIds']
|
||||
]
|
||||
|
||||
# Update binding references if they exist
|
||||
if 'startBinding' in new_element and new_element['startBinding']:
|
||||
if 'elementId' in new_element['startBinding']:
|
||||
old_id = new_element['startBinding']['elementId']
|
||||
if old_id in id_mapping:
|
||||
new_element['startBinding']['elementId'] = id_mapping[old_id]
|
||||
|
||||
if 'endBinding' in new_element and new_element['endBinding']:
|
||||
if 'elementId' in new_element['endBinding']:
|
||||
old_id = new_element['endBinding']['elementId']
|
||||
if old_id in id_mapping:
|
||||
new_element['endBinding']['elementId'] = id_mapping[old_id]
|
||||
|
||||
# Update containerId if it exists
|
||||
if 'containerId' in new_element and new_element['containerId']:
|
||||
old_id = new_element['containerId']
|
||||
if old_id in id_mapping:
|
||||
new_element['containerId'] = id_mapping[old_id]
|
||||
|
||||
# Update boundElements if they exist
|
||||
if 'boundElements' in new_element and new_element['boundElements']:
|
||||
new_bound_elements = []
|
||||
for bound_elem in new_element['boundElements']:
|
||||
if isinstance(bound_elem, dict) and 'id' in bound_elem:
|
||||
old_id = bound_elem['id']
|
||||
if old_id in id_mapping:
|
||||
bound_elem['id'] = id_mapping[old_id]
|
||||
new_bound_elements.append(bound_elem)
|
||||
new_element['boundElements'] = new_bound_elements
|
||||
|
||||
transformed.append(new_element)
|
||||
|
||||
return transformed
|
||||
|
||||
|
||||
def load_icon(icon_name: str, library_path: Path) -> List[Dict[str, Any]]:
|
||||
"""
|
||||
Load icon elements from library.
|
||||
|
||||
Args:
|
||||
icon_name: Name of the icon (e.g., "EC2", "VPC")
|
||||
library_path: Path to the icon library directory
|
||||
|
||||
Returns:
|
||||
List of icon elements
|
||||
"""
|
||||
icon_file = library_path / "icons" / f"{icon_name}.json"
|
||||
|
||||
if not icon_file.exists():
|
||||
raise FileNotFoundError(f"Icon file not found: {icon_file}")
|
||||
|
||||
with open(icon_file, 'r', encoding='utf-8') as f:
|
||||
icon_data = json.load(f)
|
||||
|
||||
return icon_data.get('elements', [])
|
||||
|
||||
|
||||
def prepare_edit_path(diagram_path: Path, use_edit_suffix: bool) -> tuple[Path, Path | None]:
|
||||
"""
|
||||
Prepare a safe edit path to avoid editor overwrite issues.
|
||||
|
||||
Returns:
|
||||
(work_path, final_path)
|
||||
- work_path: file path to read/write during edit
|
||||
- final_path: file path to rename back to (or None if not used)
|
||||
"""
|
||||
if not use_edit_suffix:
|
||||
return diagram_path, None
|
||||
|
||||
if diagram_path.suffix != ".excalidraw":
|
||||
return diagram_path, None
|
||||
|
||||
edit_path = diagram_path.with_suffix(diagram_path.suffix + ".edit")
|
||||
|
||||
if diagram_path.exists():
|
||||
if edit_path.exists():
|
||||
raise FileExistsError(f"Edit file already exists: {edit_path}")
|
||||
diagram_path.rename(edit_path)
|
||||
|
||||
return edit_path, diagram_path
|
||||
|
||||
|
||||
def finalize_edit_path(work_path: Path, final_path: Path | None) -> None:
|
||||
"""Finalize edit by renaming .edit back to .excalidraw if needed."""
|
||||
if final_path is None:
|
||||
return
|
||||
|
||||
if final_path.exists():
|
||||
final_path.unlink()
|
||||
|
||||
work_path.rename(final_path)
|
||||
|
||||
|
||||
def create_text_label(text: str, x: float, y: float) -> Dict[str, Any]:
|
||||
"""
|
||||
Create a text label element.
|
||||
|
||||
Args:
|
||||
text: Label text
|
||||
x: X coordinate
|
||||
y: Y coordinate
|
||||
|
||||
Returns:
|
||||
Text element dictionary
|
||||
"""
|
||||
return {
|
||||
"id": generate_unique_id(),
|
||||
"type": "text",
|
||||
"x": x,
|
||||
"y": y,
|
||||
"width": len(text) * 10, # Approximate width
|
||||
"height": 20,
|
||||
"angle": 0,
|
||||
"strokeColor": "#1e1e1e",
|
||||
"backgroundColor": "transparent",
|
||||
"fillStyle": "solid",
|
||||
"strokeWidth": 2,
|
||||
"strokeStyle": "solid",
|
||||
"roughness": 1,
|
||||
"opacity": 100,
|
||||
"groupIds": [],
|
||||
"frameId": None,
|
||||
"index": "a0",
|
||||
"roundness": None,
|
||||
"seed": 1000000000 + hash(text) % 1000000000,
|
||||
"version": 1,
|
||||
"versionNonce": 2000000000 + hash(text) % 1000000000,
|
||||
"isDeleted": False,
|
||||
"boundElements": [],
|
||||
"updated": 1738195200000,
|
||||
"link": None,
|
||||
"locked": False,
|
||||
"text": text,
|
||||
"fontSize": 16,
|
||||
"fontFamily": 5, # Excalifont
|
||||
"textAlign": "center",
|
||||
"verticalAlign": "top",
|
||||
"containerId": None,
|
||||
"originalText": text,
|
||||
"autoResize": True,
|
||||
"lineHeight": 1.25
|
||||
}
|
||||
|
||||
|
||||
def add_icon_to_diagram(
|
||||
diagram_path: Path,
|
||||
icon_name: str,
|
||||
x: float,
|
||||
y: float,
|
||||
library_path: Path,
|
||||
label: str = None
|
||||
) -> None:
|
||||
"""
|
||||
Add an icon to an Excalidraw diagram.
|
||||
|
||||
Args:
|
||||
diagram_path: Path to the Excalidraw diagram file
|
||||
icon_name: Name of the icon to add
|
||||
x: Target X coordinate
|
||||
y: Target Y coordinate
|
||||
library_path: Path to the icon library directory
|
||||
label: Optional text label to add below the icon
|
||||
"""
|
||||
# Load icon elements
|
||||
print(f"Loading icon: {icon_name}")
|
||||
icon_elements = load_icon(icon_name, library_path)
|
||||
print(f" Loaded {len(icon_elements)} elements")
|
||||
|
||||
# Transform icon elements
|
||||
print(f"Transforming to position ({x}, {y})")
|
||||
transformed_elements = transform_icon_elements(icon_elements, x, y)
|
||||
|
||||
# Calculate icon bounding box for label positioning
|
||||
if label and transformed_elements:
|
||||
min_x, min_y, max_x, max_y = calculate_bounding_box(transformed_elements)
|
||||
icon_width = max_x - min_x
|
||||
icon_height = max_y - min_y
|
||||
|
||||
# Position label below icon, centered
|
||||
label_x = min_x + (icon_width / 2) - (len(label) * 5)
|
||||
label_y = max_y + 10
|
||||
|
||||
label_element = create_text_label(label, label_x, label_y)
|
||||
transformed_elements.append(label_element)
|
||||
print(f" Added label: '{label}'")
|
||||
|
||||
# Load diagram
|
||||
print(f"Loading diagram: {diagram_path}")
|
||||
with open(diagram_path, 'r', encoding='utf-8') as f:
|
||||
diagram = json.load(f)
|
||||
|
||||
# Add transformed elements
|
||||
if 'elements' not in diagram:
|
||||
diagram['elements'] = []
|
||||
|
||||
original_count = len(diagram['elements'])
|
||||
diagram['elements'].extend(transformed_elements)
|
||||
print(f" Added {len(transformed_elements)} elements (total: {original_count} -> {len(diagram['elements'])})")
|
||||
|
||||
# Save diagram
|
||||
print(f"Saving diagram")
|
||||
with open(diagram_path, 'w', encoding='utf-8') as f:
|
||||
json.dump(diagram, f, indent=2, ensure_ascii=False)
|
||||
|
||||
print(f"✓ Successfully added '{icon_name}' icon to diagram")
|
||||
|
||||
|
||||
def main():
|
||||
"""Main entry point."""
|
||||
if len(sys.argv) < 5:
|
||||
print("Usage: python add-icon-to-diagram.py <diagram_path> <icon_name> <x> <y> [OPTIONS]")
|
||||
print("\nOptions:")
|
||||
print(" --library-path PATH Path to icon library directory")
|
||||
print(" --label TEXT Add text label below icon")
|
||||
print(" --use-edit-suffix Edit via .excalidraw.edit to avoid editor overwrite issues (enabled by default; use --no-use-edit-suffix to disable)")
|
||||
print("\nExamples:")
|
||||
print(" python add-icon-to-diagram.py diagram.excalidraw EC2 500 300")
|
||||
print(" python add-icon-to-diagram.py diagram.excalidraw EC2 500 300 --label 'Web Server'")
|
||||
sys.exit(1)
|
||||
|
||||
diagram_path = Path(sys.argv[1])
|
||||
icon_name = sys.argv[2]
|
||||
x = float(sys.argv[3])
|
||||
y = float(sys.argv[4])
|
||||
|
||||
# Default library path
|
||||
script_dir = Path(__file__).parent
|
||||
default_library_path = script_dir.parent / "libraries" / "aws-architecture-icons"
|
||||
|
||||
# Parse optional arguments
|
||||
library_path = default_library_path
|
||||
label = None
|
||||
# Default: use edit suffix to avoid editor overwrite issues
|
||||
use_edit_suffix = True
|
||||
|
||||
i = 5
|
||||
while i < len(sys.argv):
|
||||
if sys.argv[i] == '--library-path':
|
||||
if i + 1 < len(sys.argv):
|
||||
library_path = Path(sys.argv[i + 1])
|
||||
i += 2
|
||||
else:
|
||||
print("Error: --library-path requires a path argument")
|
||||
sys.exit(1)
|
||||
elif sys.argv[i] == '--label':
|
||||
if i + 1 < len(sys.argv):
|
||||
label = sys.argv[i + 1]
|
||||
i += 2
|
||||
else:
|
||||
print("Error: --label requires a text argument")
|
||||
sys.exit(1)
|
||||
elif sys.argv[i] == '--use-edit-suffix':
|
||||
use_edit_suffix = True
|
||||
i += 1
|
||||
elif sys.argv[i] == '--no-use-edit-suffix':
|
||||
use_edit_suffix = False
|
||||
i += 1
|
||||
else:
|
||||
print(f"Error: Unknown option: {sys.argv[i]}")
|
||||
sys.exit(1)
|
||||
|
||||
# Validate inputs
|
||||
if not diagram_path.exists():
|
||||
print(f"Error: Diagram file not found: {diagram_path}")
|
||||
sys.exit(1)
|
||||
|
||||
if not library_path.exists():
|
||||
print(f"Error: Library path not found: {library_path}")
|
||||
sys.exit(1)
|
||||
|
||||
try:
|
||||
work_path, final_path = prepare_edit_path(diagram_path, use_edit_suffix)
|
||||
add_icon_to_diagram(work_path, icon_name, x, y, library_path, label)
|
||||
finalize_edit_path(work_path, final_path)
|
||||
except Exception as e:
|
||||
print(f"Error: {e}")
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
|
||||
@@ -0,0 +1,183 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Excalidraw Library Splitter
|
||||
|
||||
This script splits an Excalidraw library file (*.excalidrawlib) into individual
|
||||
icon JSON files and generates a reference.md file for easy lookup.
|
||||
|
||||
The script expects the following structure:
|
||||
skills/excalidraw-diagram-generator/libraries/{icon-set-name}/
|
||||
{icon-set-name}.excalidrawlib (place this file first)
|
||||
|
||||
Usage:
|
||||
python split-excalidraw-library.py <path-to-library-directory>
|
||||
|
||||
Example:
|
||||
python split-excalidraw-library.py skills/excalidraw-diagram-generator/libraries/aws-architecture-icons/
|
||||
"""
|
||||
|
||||
import json
|
||||
import os
|
||||
import re
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
def sanitize_filename(name: str) -> str:
|
||||
"""
|
||||
Sanitize icon name to create a valid filename.
|
||||
|
||||
Args:
|
||||
name: Original icon name
|
||||
|
||||
Returns:
|
||||
Sanitized filename safe for all platforms
|
||||
"""
|
||||
# Replace spaces with hyphens
|
||||
filename = name.replace(' ', '-')
|
||||
|
||||
# Remove or replace special characters
|
||||
filename = re.sub(r'[^\w\-.]', '', filename)
|
||||
|
||||
# Remove multiple consecutive hyphens
|
||||
filename = re.sub(r'-+', '-', filename)
|
||||
|
||||
# Remove leading/trailing hyphens
|
||||
filename = filename.strip('-')
|
||||
|
||||
return filename
|
||||
|
||||
|
||||
def find_library_file(directory: Path) -> Path:
|
||||
"""
|
||||
Find the .excalidrawlib file in the given directory.
|
||||
|
||||
Args:
|
||||
directory: Directory to search
|
||||
|
||||
Returns:
|
||||
Path to the library file
|
||||
|
||||
Raises:
|
||||
SystemExit: If no library file or multiple library files found
|
||||
"""
|
||||
library_files = list(directory.glob('*.excalidrawlib'))
|
||||
|
||||
if len(library_files) == 0:
|
||||
print(f"Error: No .excalidrawlib file found in {directory}")
|
||||
print(f"Please place a .excalidrawlib file in this directory first.")
|
||||
sys.exit(1)
|
||||
|
||||
if len(library_files) > 1:
|
||||
print(f"Error: Multiple .excalidrawlib files found in {directory}")
|
||||
print(f"Please keep only one library file in the directory.")
|
||||
sys.exit(1)
|
||||
|
||||
return library_files[0]
|
||||
|
||||
|
||||
def split_library(library_dir: str) -> None:
|
||||
"""
|
||||
Split an Excalidraw library file into individual icon files.
|
||||
|
||||
Args:
|
||||
library_dir: Path to the directory containing the .excalidrawlib file
|
||||
"""
|
||||
library_dir = Path(library_dir)
|
||||
|
||||
if not library_dir.exists():
|
||||
print(f"Error: Directory not found: {library_dir}")
|
||||
sys.exit(1)
|
||||
|
||||
if not library_dir.is_dir():
|
||||
print(f"Error: Path is not a directory: {library_dir}")
|
||||
sys.exit(1)
|
||||
|
||||
# Find the library file
|
||||
library_path = find_library_file(library_dir)
|
||||
print(f"Found library: {library_path.name}")
|
||||
|
||||
# Load library file
|
||||
print(f"Loading library data...")
|
||||
with open(library_path, 'r', encoding='utf-8') as f:
|
||||
library_data = json.load(f)
|
||||
|
||||
# Validate library structure
|
||||
if 'libraryItems' not in library_data:
|
||||
print("Error: Invalid library file format (missing 'libraryItems')")
|
||||
sys.exit(1)
|
||||
|
||||
# Create icons directory
|
||||
icons_dir = library_dir / 'icons'
|
||||
icons_dir.mkdir(exist_ok=True)
|
||||
print(f"Output directory: {library_dir}")
|
||||
|
||||
# Process each library item (icon)
|
||||
library_items = library_data['libraryItems']
|
||||
icon_list = []
|
||||
|
||||
print(f"Processing {len(library_items)} icons...")
|
||||
|
||||
for item in library_items:
|
||||
# Get icon name
|
||||
icon_name = item.get('name', 'Unnamed')
|
||||
|
||||
# Create sanitized filename
|
||||
filename = sanitize_filename(icon_name) + '.json'
|
||||
|
||||
# Save icon data
|
||||
icon_path = icons_dir / filename
|
||||
with open(icon_path, 'w', encoding='utf-8') as f:
|
||||
json.dump(item, f, ensure_ascii=False, indent=2)
|
||||
|
||||
# Add to reference list
|
||||
icon_list.append({
|
||||
'name': icon_name,
|
||||
'filename': filename
|
||||
})
|
||||
|
||||
print(f" ✓ {icon_name} → {filename}")
|
||||
|
||||
# Sort icon list by name
|
||||
icon_list.sort(key=lambda x: x['name'])
|
||||
|
||||
# Generate reference.md
|
||||
library_name = library_path.stem
|
||||
reference_path = library_dir / 'reference.md'
|
||||
with open(reference_path, 'w', encoding='utf-8') as f:
|
||||
f.write(f"# {library_name} Reference\n\n")
|
||||
f.write(f"This directory contains {len(icon_list)} icons extracted from `{library_path.name}`.\n\n")
|
||||
f.write("## Available Icons\n\n")
|
||||
f.write("| Icon Name | Filename |\n")
|
||||
f.write("|-----------|----------|\n")
|
||||
|
||||
for icon in icon_list:
|
||||
f.write(f"| {icon['name']} | `icons/{icon['filename']}` |\n")
|
||||
|
||||
f.write("\n## Usage\n\n")
|
||||
f.write("Each icon JSON file contains the complete `elements` array needed to render that icon in Excalidraw.\n")
|
||||
f.write("You can copy the elements from these files into your Excalidraw diagrams.\n")
|
||||
|
||||
print(f"\n✅ Successfully split library into {len(icon_list)} icons")
|
||||
print(f"📄 Reference file created: {reference_path}")
|
||||
print(f"📁 Icons directory: {icons_dir}")
|
||||
|
||||
|
||||
def main():
|
||||
"""Main entry point."""
|
||||
if hasattr(sys.stdout, "reconfigure"):
|
||||
# Ensure consistent UTF-8 output on Windows consoles.
|
||||
sys.stdout.reconfigure(encoding="utf-8")
|
||||
if len(sys.argv) != 2:
|
||||
print("Usage: python split-excalidraw-library.py <path-to-library-directory>")
|
||||
print("\nExample:")
|
||||
print(" python split-excalidraw-library.py skills/excalidraw-diagram-generator/libraries/aws-architecture-icons/")
|
||||
print("\nNote: The directory should contain a .excalidrawlib file.")
|
||||
sys.exit(1)
|
||||
|
||||
library_dir = sys.argv[1]
|
||||
split_library(library_dir)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
Reference in New Issue
Block a user