Setting the file. One moment.
Add Arrow · Excalidraw Diagram Generator · github/awesome-copilot · Skills Docs
ContentsBack to the top of the page Next
Script Add Icon To Diagram
scripts/ add-arrow.py
Python · 315 lines · 10 KB
16
python add-arrow.py diagram.excalidraw 300 200 500 300 --label "HTTP"
17 python add-arrow.py diagram.excalidraw 300 200 500 300 --style dashed --color "#7950f2"
18 python add-arrow.py diagram.excalidraw 300 200 500 300 --use-edit-suffix
19 """
20
21 import json
22 import sys
23 import uuid
24 from pathlib import Path
25 from typing import Dict, Any
26
27
28 def generate_unique_id () -> str :
29 """Generate a unique ID for Excalidraw elements."""
30 return str (uuid.uuid4()).replace( '-' , '' )[: 16 ]
31
32
33 def prepare_edit_path (diagram_path: Path, use_edit_suffix: bool ) -> tuple[Path, Path | None ]:
34 """
35 Prepare a safe edit path to avoid editor overwrite issues.
36
37 Returns:
38 (work_path, final_path)
39 - work_path: file path to read/write during edit
40 - final_path: file path to rename back to (or None if not used)
41 """
42 if not use_edit_suffix:
43 return diagram_path, None
44
45 if diagram_path.suffix != ".excalidraw" :
46 return diagram_path, None
47
48 edit_path = diagram_path.with_suffix(diagram_path.suffix + ".edit" )
49
50 if diagram_path.exists():
51 if edit_path.exists():
52 raise FileExistsError ( f "Edit file already exists: { edit_path } " )
53 diagram_path.rename(edit_path)
54
55 return edit_path, diagram_path
56
57
58 def finalize_edit_path (work_path: Path, final_path: Path | None ) -> None :
59 """Finalize edit by renaming .edit back to .excalidraw if needed."""
60 if final_path is None :
61 return
62
63 if final_path.exists():
64 final_path.unlink()
65
66 work_path.rename(final_path)
67
68
69 def create_arrow (
70 from_x: float ,
71 from_y: float ,
72 to_x: float ,
73 to_y: float ,
74 style: str = "solid" ,
75 color: str = "#1e1e1e" ,
76 label: str = None
77 ) -> list :
78 """
79 Create an arrow element.
80
81 Args:
82 from_x: Starting X coordinate
83 from_y: Starting Y coordinate
84 to_x: Ending X coordinate
85 to_y: Ending Y coordinate
86 style: Line style (solid, dashed, dotted)
87 color: Arrow color
88 label: Optional text label on the arrow
89
90 Returns:
91 List of elements (arrow and optional label)
92 """
93 elements = []
94
95 # Arrow element
96 arrow = {
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" : 2
116 },
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" : None
134 }
135 elements.append(arrow)
136
137 # Optional label
138 if label:
139 mid_x = (from_x + to_x) / 2 - ( len (label) * 5 )
140 mid_y = (from_y + to_y) / 2 - 10
141
142 label_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.25
178 }
179 elements.append(label_element)
180
181 return elements
182
183
184 def add_arrow_to_diagram (
185 diagram_path: Path,
186 from_x: float ,
187 from_y: float ,
188 to_x: float ,
189 to_y: float ,
190 style: str = "solid" ,
191 color: str = "#1e1e1e" ,
192 label: str = None
193 ) -> None :
194 """
195 Add an arrow to an Excalidraw diagram.
196
197 Args:
198 diagram_path: Path to the Excalidraw diagram file
199 from_x: Starting X coordinate
200 from_y: Starting Y coordinate
201 to_x: Ending X coordinate
202 to_y: Ending Y coordinate
203 style: Line style (solid, dashed, dotted)
204 color: Arrow color
205 label: Optional text label
206 """
207 print ( f "Creating arrow from ( { from_x } , { from_y } ) to ( { to_x } , { to_y } )" )
208 arrow_elements = create_arrow(from_x, from_y, to_x, to_y, style, color, label)
209
210 if label:
211 print ( f " With label: ' { label } '" )
212
213 # Load diagram
214 print ( f "Loading diagram: { diagram_path } " )
215 with open (diagram_path, 'r' , encoding = 'utf-8' ) as f:
216 diagram = json.load(f)
217
218 # Add arrow elements
219 if 'elements' not in diagram:
220 diagram[ 'elements' ] = []
221
222 original_count = len (diagram[ 'elements' ])
223 diagram[ 'elements' ].extend(arrow_elements)
224 print ( f " Added { len (arrow_elements) } elements (total: { original_count } -> { len (diagram[ 'elements' ]) } )" )
225
226 # Save diagram
227 print ( f "Saving diagram" )
228 with open (diagram_path, 'w' , encoding = 'utf-8' ) as f:
229 json.dump(diagram, f, indent = 2 , ensure_ascii = False )
230
231 print ( f "✓ Successfully added arrow to diagram" )
232
233
234 def main ():
235 """Main entry point."""
236 if hasattr (sys.stdout, "reconfigure" ):
237 # Ensure consistent UTF-8 output on Windows consoles.
238 sys.stdout.reconfigure( encoding = "utf-8" )
239 if len (sys.argv) < 6 :
240 print ( "Usage: python add-arrow.py <diagram_path> <from_x> <from_y> <to_x> <to_y> [OPTIONS]" )
241 print ( " \n Options:" )
242 print ( " --style {solid|dashed|dotted} Line style (default: solid)" )
243 print ( " --color HEX Color (default: #1e1e1e)" )
244 print ( " --label TEXT Text label on arrow" )
245 print ( " --use-edit-suffix Edit via .excalidraw.edit to avoid editor overwrite issues (enabled by default; use --no-use-edit-suffix to disable)" )
246 print ( " \n Examples:" )
247 print ( " python add-arrow.py diagram.excalidraw 300 200 500 300" )
248 print ( " python add-arrow.py diagram.excalidraw 300 200 500 300 --label 'HTTP'" )
249 sys.exit( 1 )
250
251 diagram_path = Path(sys.argv[ 1 ])
252 from_x = float (sys.argv[ 2 ])
253 from_y = float (sys.argv[ 3 ])
254 to_x = float (sys.argv[ 4 ])
255 to_y = float (sys.argv[ 5 ])
256
257 # Parse optional arguments
258 style = "solid"
259 color = "#1e1e1e"
260 label = None
261 # Default: use edit suffix to avoid editor overwrite issues
262 use_edit_suffix = True
263
264 i = 6
265 while i < len (sys.argv):
266 if sys.argv[i] == '--style' :
267 if i + 1 < len (sys.argv):
268 style = sys.argv[i + 1 ]
269 if style not in [ 'solid' , 'dashed' , 'dotted' ]:
270 print ( f "Error: Invalid style ' { style } '. Must be: solid, dashed, or dotted" )
271 sys.exit( 1 )
272 i += 2
273 else :
274 print ( "Error: --style requires an argument" )
275 sys.exit( 1 )
276 elif sys.argv[i] == '--color' :
277 if i + 1 < len (sys.argv):
278 color = sys.argv[i + 1 ]
279 i += 2
280 else :
281 print ( "Error: --color requires an argument" )
282 sys.exit( 1 )
283 elif sys.argv[i] == '--label' :
284 if i + 1 < len (sys.argv):
285 label = sys.argv[i + 1 ]
286 i += 2
287 else :
288 print ( "Error: --label requires a text argument" )
289 sys.exit( 1 )
290 elif sys.argv[i] == '--use-edit-suffix' :
291 use_edit_suffix = True
292 i += 1
293 elif sys.argv[i] == '--no-use-edit-suffix' :
294 use_edit_suffix = False
295 i += 1
296 else :
297 print ( f "Error: Unknown option: { sys.argv[i] } " )
298 sys.exit( 1 )
299
300 # Validate inputs
301 if not diagram_path.exists():
302 print ( f "Error: Diagram file not found: { diagram_path } " )
303 sys.exit( 1 )
304
305 try :
306 work_path, final_path = prepare_edit_path(diagram_path, use_edit_suffix)
307 add_arrow_to_diagram(work_path, from_x, from_y, to_x, to_y, style, color, label)
308 finalize_edit_path(work_path, final_path)
309 except Exception as e:
310 print ( f "Error: { e } " )
311 sys.exit( 1 )
312
313
314 if __name__ == '__main__' :
315 main()