create_proxy_diagram.py 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401
  1. #!/usr/bin/env python3
  2. """
  3. Create a professional network architecture diagram for Bambuddy Virtual Printer Proxy Mode.
  4. Following the Signal Flow design philosophy.
  5. """
  6. from PIL import Image, ImageDraw, ImageFont
  7. from pathlib import Path
  8. # Canvas dimensions
  9. WIDTH = 1400
  10. HEIGHT = 700
  11. # Colors - Signal Flow palette
  12. BG_COLOR = (18, 18, 22) # Near black
  13. CONTAINER_BG = (28, 28, 35) # Slightly lighter
  14. CONTAINER_BORDER = (50, 50, 60) # Subtle border
  15. BAMBU_GREEN = (0, 174, 66) # #00AE42
  16. BAMBU_GREEN_DIM = (0, 120, 45) # Dimmer green for accents
  17. TEXT_PRIMARY = (240, 240, 245) # Near white
  18. TEXT_SECONDARY = (140, 140, 150) # Gray
  19. TEXT_LABEL = (100, 100, 110) # Darker gray for small labels
  20. INTERNET_COLOR = (80, 80, 95) # Cloud color
  21. TLS_BADGE_BG = (35, 55, 45) # Dark green for TLS badges
  22. LOCK_COLOR = BAMBU_GREEN
  23. # Font paths
  24. FONT_DIR = Path("/opt/claude/.claude/plugins/cache/anthropic-agent-skills/document-skills/f23222824449/skills/canvas-design/canvas-fonts")
  25. def load_fonts():
  26. """Load fonts for the diagram."""
  27. fonts = {}
  28. try:
  29. fonts['title'] = ImageFont.truetype(str(FONT_DIR / "InstrumentSans-Bold.ttf"), 28)
  30. fonts['heading'] = ImageFont.truetype(str(FONT_DIR / "InstrumentSans-Bold.ttf"), 18)
  31. fonts['label'] = ImageFont.truetype(str(FONT_DIR / "InstrumentSans-Regular.ttf"), 14)
  32. fonts['small'] = ImageFont.truetype(str(FONT_DIR / "InstrumentSans-Regular.ttf"), 12)
  33. fonts['port'] = ImageFont.truetype(str(FONT_DIR / "JetBrainsMono-Bold.ttf"), 13)
  34. fonts['port_small'] = ImageFont.truetype(str(FONT_DIR / "JetBrainsMono-Regular.ttf"), 11)
  35. fonts['tls'] = ImageFont.truetype(str(FONT_DIR / "JetBrainsMono-Bold.ttf"), 10)
  36. except Exception as e:
  37. print(f"Font loading error: {e}")
  38. # Fallback to default
  39. fonts['title'] = ImageFont.load_default()
  40. fonts['heading'] = ImageFont.load_default()
  41. fonts['label'] = ImageFont.load_default()
  42. fonts['small'] = ImageFont.load_default()
  43. fonts['port'] = ImageFont.load_default()
  44. fonts['port_small'] = ImageFont.load_default()
  45. fonts['tls'] = ImageFont.load_default()
  46. return fonts
  47. def draw_rounded_rect(draw, xy, radius, fill=None, outline=None, width=1):
  48. """Draw a rounded rectangle."""
  49. x1, y1, x2, y2 = xy
  50. if fill:
  51. # Fill
  52. draw.rectangle([x1 + radius, y1, x2 - radius, y2], fill=fill)
  53. draw.rectangle([x1, y1 + radius, x2, y2 - radius], fill=fill)
  54. draw.ellipse([x1, y1, x1 + 2*radius, y1 + 2*radius], fill=fill)
  55. draw.ellipse([x2 - 2*radius, y1, x2, y1 + 2*radius], fill=fill)
  56. draw.ellipse([x1, y2 - 2*radius, x1 + 2*radius, y2], fill=fill)
  57. draw.ellipse([x2 - 2*radius, y2 - 2*radius, x2, y2], fill=fill)
  58. if outline:
  59. # Outline
  60. draw.arc([x1, y1, x1 + 2*radius, y1 + 2*radius], 180, 270, fill=outline, width=width)
  61. draw.arc([x2 - 2*radius, y1, x2, y1 + 2*radius], 270, 360, fill=outline, width=width)
  62. draw.arc([x1, y2 - 2*radius, x1 + 2*radius, y2], 90, 180, fill=outline, width=width)
  63. draw.arc([x2 - 2*radius, y2 - 2*radius, x2, y2], 0, 90, fill=outline, width=width)
  64. draw.line([x1 + radius, y1, x2 - radius, y1], fill=outline, width=width)
  65. draw.line([x1 + radius, y2, x2 - radius, y2], fill=outline, width=width)
  66. draw.line([x1, y1 + radius, x1, y2 - radius], fill=outline, width=width)
  67. draw.line([x2, y1 + radius, x2, y2 - radius], fill=outline, width=width)
  68. def draw_lock_icon(draw, x, y, size, color):
  69. """Draw a simple lock icon."""
  70. # Lock body
  71. body_w = size * 0.7
  72. body_h = size * 0.5
  73. body_x = x - body_w / 2
  74. body_y = y + size * 0.1
  75. draw_rounded_rect(draw, [body_x, body_y, body_x + body_w, body_y + body_h], 2, fill=color)
  76. # Lock shackle (arc)
  77. shackle_w = size * 0.45
  78. shackle_h = size * 0.4
  79. shackle_x = x - shackle_w / 2
  80. shackle_y = y - size * 0.25
  81. draw.arc([shackle_x, shackle_y, shackle_x + shackle_w, shackle_y + shackle_h],
  82. 180, 360, fill=color, width=2)
  83. def draw_computer_icon(draw, x, y, size, color):
  84. """Draw a simple computer/monitor icon."""
  85. # Monitor
  86. mon_w = size * 0.8
  87. mon_h = size * 0.55
  88. mon_x = x - mon_w / 2
  89. mon_y = y - size * 0.35
  90. draw_rounded_rect(draw, [mon_x, mon_y, mon_x + mon_w, mon_y + mon_h], 3, outline=color, width=2)
  91. # Screen inner
  92. inner_margin = 4
  93. draw_rounded_rect(draw, [mon_x + inner_margin, mon_y + inner_margin,
  94. mon_x + mon_w - inner_margin, mon_y + mon_h - inner_margin],
  95. 2, fill=color)
  96. # Stand
  97. stand_w = size * 0.2
  98. stand_h = size * 0.15
  99. draw.rectangle([x - stand_w/2, mon_y + mon_h, x + stand_w/2, mon_y + mon_h + stand_h], fill=color)
  100. # Base
  101. base_w = size * 0.4
  102. draw.rectangle([x - base_w/2, mon_y + mon_h + stand_h, x + base_w/2, mon_y + mon_h + stand_h + 3], fill=color)
  103. def draw_server_icon(draw, x, y, size, color):
  104. """Draw a simple server icon."""
  105. unit_h = size * 0.25
  106. gap = 4
  107. w = size * 0.75
  108. for i in range(3):
  109. uy = y - size * 0.4 + i * (unit_h + gap)
  110. draw_rounded_rect(draw, [x - w/2, uy, x + w/2, uy + unit_h], 3, outline=color, width=2)
  111. # LED dots
  112. draw.ellipse([x + w/2 - 12, uy + unit_h/2 - 2, x + w/2 - 8, uy + unit_h/2 + 2], fill=color)
  113. def draw_printer_icon(draw, x, y, size, color):
  114. """Draw a Bambu Lab style 3D printer icon."""
  115. # Main body (cube-like)
  116. body_w = size * 0.75
  117. body_h = size * 0.7
  118. body_x = x - body_w / 2
  119. body_y = y - size * 0.35
  120. # Outer frame with thicker border
  121. draw_rounded_rect(draw, [body_x, body_y, body_x + body_w, body_y + body_h], 6, outline=color, width=2)
  122. # Inner window/chamber
  123. win_margin = 8
  124. draw_rounded_rect(draw, [body_x + win_margin, body_y + win_margin,
  125. body_x + body_w - win_margin, body_y + body_h - 16],
  126. 4, outline=color, width=1)
  127. # Print bed line
  128. bed_y = body_y + body_h - 12
  129. draw.line([body_x + 12, bed_y, body_x + body_w - 12, bed_y], fill=color, width=2)
  130. # Extruder/toolhead
  131. ext_w = 16
  132. ext_h = 8
  133. ext_y = body_y + 18
  134. draw_rounded_rect(draw, [x - ext_w/2, ext_y, x + ext_w/2, ext_y + ext_h], 2, fill=color)
  135. # Small printed object on bed
  136. obj_w = 12
  137. obj_h = 10
  138. draw_rounded_rect(draw, [x - obj_w/2, bed_y - obj_h, x + obj_w/2, bed_y], 2, fill=color)
  139. def draw_cloud_icon(draw, x, y, size, color):
  140. """Draw a simple cloud icon."""
  141. # Main cloud body using overlapping circles
  142. r1 = size * 0.25
  143. r2 = size * 0.2
  144. r3 = size * 0.18
  145. # Center circle
  146. draw.ellipse([x - r1, y - r1 * 0.8, x + r1, y + r1 * 0.8], fill=color)
  147. # Left circle
  148. draw.ellipse([x - r1 - r2 * 0.7, y - r2 * 0.3, x - r1 + r2 * 0.7, y + r2 * 1.1], fill=color)
  149. # Right circle
  150. draw.ellipse([x + r1 * 0.3 - r2 * 0.5, y - r2 * 0.4, x + r1 * 0.3 + r2 * 1.2, y + r2 * 1.0], fill=color)
  151. # Top circle
  152. draw.ellipse([x - r3 * 0.5, y - r1 - r3 * 0.3, x + r3 * 1.2, y - r1 + r3 * 0.9], fill=color)
  153. def draw_arrow(draw, x1, y1, x2, y2, color, width=2):
  154. """Draw a line with arrow head."""
  155. draw.line([x1, y1, x2, y2], fill=color, width=width)
  156. # Arrow head
  157. import math
  158. angle = math.atan2(y2 - y1, x2 - x1)
  159. arrow_len = 10
  160. arrow_angle = math.pi / 6
  161. ax1 = x2 - arrow_len * math.cos(angle - arrow_angle)
  162. ay1 = y2 - arrow_len * math.sin(angle - arrow_angle)
  163. ax2 = x2 - arrow_len * math.cos(angle + arrow_angle)
  164. ay2 = y2 - arrow_len * math.sin(angle + arrow_angle)
  165. draw.polygon([(x2, y2), (ax1, ay1), (ax2, ay2)], fill=color)
  166. def draw_bidirectional_arrow(draw, x1, y1, x2, y2, color, width=2):
  167. """Draw a bidirectional arrow."""
  168. import math
  169. # Shorten line slightly to make room for arrowheads
  170. angle = math.atan2(y2 - y1, x2 - x1)
  171. offset = 8
  172. lx1 = x1 + offset * math.cos(angle)
  173. ly1 = y1 + offset * math.sin(angle)
  174. lx2 = x2 - offset * math.cos(angle)
  175. ly2 = y2 - offset * math.sin(angle)
  176. draw.line([lx1, ly1, lx2, ly2], fill=color, width=width)
  177. # Arrow heads
  178. arrow_len = 8
  179. arrow_angle = math.pi / 6
  180. # Right arrow
  181. ax1 = x2 - arrow_len * math.cos(angle - arrow_angle)
  182. ay1 = y2 - arrow_len * math.sin(angle - arrow_angle)
  183. ax2 = x2 - arrow_len * math.cos(angle + arrow_angle)
  184. ay2 = y2 - arrow_len * math.sin(angle + arrow_angle)
  185. draw.polygon([(x2, y2), (ax1, ay1), (ax2, ay2)], fill=color)
  186. # Left arrow
  187. ax1 = x1 + arrow_len * math.cos(angle - arrow_angle)
  188. ay1 = y1 + arrow_len * math.sin(angle - arrow_angle)
  189. ax2 = x1 + arrow_len * math.cos(angle + arrow_angle)
  190. ay2 = y1 + arrow_len * math.sin(angle + arrow_angle)
  191. draw.polygon([(x1, y1), (ax1, ay1), (ax2, ay2)], fill=color)
  192. def draw_tls_badge(draw, x, y, fonts, color=TLS_BADGE_BG, text_color=BAMBU_GREEN):
  193. """Draw a TLS badge."""
  194. badge_w = 42
  195. badge_h = 18
  196. draw_rounded_rect(draw, [x - badge_w/2, y - badge_h/2, x + badge_w/2, y + badge_h/2],
  197. 4, fill=color, outline=BAMBU_GREEN_DIM, width=1)
  198. # Lock icon
  199. draw_lock_icon(draw, x - 12, y - 2, 10, text_color)
  200. # TLS text
  201. draw.text((x + 2, y), "TLS", font=fonts['tls'], fill=text_color, anchor="lm")
  202. def create_diagram():
  203. """Create the main diagram."""
  204. img = Image.new('RGB', (WIDTH, HEIGHT), BG_COLOR)
  205. draw = ImageDraw.Draw(img)
  206. fonts = load_fonts()
  207. # Title
  208. title = "VIRTUAL PRINTER PROXY MODE"
  209. draw.text((WIDTH // 2, 35), title, font=fonts['title'], fill=BAMBU_GREEN, anchor="mm")
  210. # Subtitle
  211. subtitle = "Secure remote printing through Bambuddy"
  212. draw.text((WIDTH // 2, 62), subtitle, font=fonts['small'], fill=TEXT_SECONDARY, anchor="mm")
  213. # === LAYOUT ===
  214. # Three main sections: Remote | Internet | Local
  215. section_y = 320
  216. # Remote section (left)
  217. remote_x = 180
  218. remote_box = [40, 120, 320, 520]
  219. # Internet section (center)
  220. internet_x = 510
  221. # Bambuddy section (center-right)
  222. bambuddy_x = 700
  223. bambuddy_box = [560, 140, 840, 500]
  224. # Local section (right)
  225. local_x = 1050
  226. printer_x = 1220
  227. local_box = [920, 120, 1360, 520]
  228. # === REMOTE NETWORK ZONE ===
  229. draw_rounded_rect(draw, remote_box, 12, fill=CONTAINER_BG, outline=CONTAINER_BORDER, width=1)
  230. draw.text((180, 140), "REMOTE NETWORK", font=fonts['label'], fill=TEXT_LABEL, anchor="mm")
  231. # Slicer icon and label
  232. draw_computer_icon(draw, remote_x, section_y - 40, 70, BAMBU_GREEN)
  233. draw.text((remote_x, section_y + 30), "Bambu Studio", font=fonts['heading'], fill=TEXT_PRIMARY, anchor="mm")
  234. draw.text((remote_x, section_y + 52), "or OrcaSlicer", font=fonts['small'], fill=TEXT_SECONDARY, anchor="mm")
  235. # Ports on remote side
  236. draw.text((remote_x, section_y + 100), "Connects to Bambuddy", font=fonts['small'], fill=TEXT_LABEL, anchor="mm")
  237. draw.text((remote_x, section_y + 120), "FTP :9990 MQTT :8883", font=fonts['port_small'], fill=TEXT_SECONDARY, anchor="mm")
  238. # === INTERNET CLOUD ===
  239. draw_cloud_icon(draw, internet_x, section_y, 80, INTERNET_COLOR)
  240. draw.text((internet_x, section_y + 55), "Internet", font=fonts['label'], fill=TEXT_LABEL, anchor="mm")
  241. # === BAMBUDDY SERVER ===
  242. draw_rounded_rect(draw, bambuddy_box, 12, fill=CONTAINER_BG, outline=BAMBU_GREEN_DIM, width=2)
  243. draw.text((bambuddy_x, 165), "BAMBUDDY SERVER", font=fonts['label'], fill=BAMBU_GREEN, anchor="mm")
  244. # Server icon
  245. draw_server_icon(draw, bambuddy_x, section_y - 50, 70, BAMBU_GREEN)
  246. draw.text((bambuddy_x, section_y + 20), "TLS Proxy", font=fonts['heading'], fill=TEXT_PRIMARY, anchor="mm")
  247. # Incoming ports (left side of Bambuddy)
  248. draw.text((bambuddy_x, section_y + 70), "LISTEN PORTS", font=fonts['small'], fill=TEXT_LABEL, anchor="mm")
  249. draw_rounded_rect(draw, [bambuddy_x - 55, section_y + 85, bambuddy_x + 55, section_y + 130],
  250. 6, fill=(35, 35, 45), outline=CONTAINER_BORDER, width=1)
  251. draw.text((bambuddy_x, section_y + 98), "FTP", font=fonts['small'], fill=TEXT_SECONDARY, anchor="mm")
  252. draw.text((bambuddy_x, section_y + 115), "9990", font=fonts['port'], fill=BAMBU_GREEN, anchor="mm")
  253. draw_rounded_rect(draw, [bambuddy_x - 55, section_y + 140, bambuddy_x + 55, section_y + 185],
  254. 6, fill=(35, 35, 45), outline=CONTAINER_BORDER, width=1)
  255. draw.text((bambuddy_x, section_y + 153), "MQTT", font=fonts['small'], fill=TEXT_SECONDARY, anchor="mm")
  256. draw.text((bambuddy_x, section_y + 170), "8883", font=fonts['port'], fill=BAMBU_GREEN, anchor="mm")
  257. # === LOCAL NETWORK ZONE ===
  258. draw_rounded_rect(draw, local_box, 12, fill=CONTAINER_BG, outline=CONTAINER_BORDER, width=1)
  259. draw.text((1140, 140), "LOCAL NETWORK", font=fonts['label'], fill=TEXT_LABEL, anchor="mm")
  260. # "LAN Mode" badge
  261. draw_rounded_rect(draw, [1100, 155, 1180, 175], 4, fill=TLS_BADGE_BG, outline=BAMBU_GREEN_DIM, width=1)
  262. draw.text((1140, 165), "LAN Mode", font=fonts['tls'], fill=BAMBU_GREEN, anchor="mm")
  263. # Printer icon
  264. draw_printer_icon(draw, printer_x, section_y - 40, 80, BAMBU_GREEN)
  265. draw.text((printer_x, section_y + 35), "Bambu Lab", font=fonts['heading'], fill=TEXT_PRIMARY, anchor="mm")
  266. draw.text((printer_x, section_y + 55), "Printer", font=fonts['heading'], fill=TEXT_PRIMARY, anchor="mm")
  267. # Target ports
  268. draw.text((printer_x, section_y + 100), "Printer Ports", font=fonts['small'], fill=TEXT_LABEL, anchor="mm")
  269. draw.text((printer_x, section_y + 120), "FTP :990 MQTT :8883", font=fonts['port_small'], fill=TEXT_SECONDARY, anchor="mm")
  270. # Proxy target label
  271. draw_rounded_rect(draw, [local_x - 60, section_y - 80, local_x + 60, section_y - 50],
  272. 6, fill=(35, 35, 45), outline=CONTAINER_BORDER, width=1)
  273. draw.text((local_x, section_y - 65), "Target IP", font=fonts['small'], fill=TEXT_SECONDARY, anchor="mm")
  274. # === CONNECTION ARROWS ===
  275. # Remote to Internet
  276. draw_bidirectional_arrow(draw, 325, section_y, 460, section_y, BAMBU_GREEN_DIM, 2)
  277. # TLS badge between remote and internet
  278. draw_tls_badge(draw, 392, section_y - 20, fonts)
  279. # Internet to Bambuddy
  280. draw_bidirectional_arrow(draw, 555, section_y, 620, section_y, BAMBU_GREEN_DIM, 2)
  281. # Bambuddy to Local
  282. draw_bidirectional_arrow(draw, 780, section_y, 920, section_y, BAMBU_GREEN_DIM, 2)
  283. # TLS badge between Bambuddy and printer
  284. draw_tls_badge(draw, 850, section_y - 20, fonts)
  285. # Local network arrow to printer
  286. draw_bidirectional_arrow(draw, 990, section_y, 1130, section_y, BAMBU_GREEN_DIM, 2)
  287. # === BOTTOM INFO ===
  288. info_y = 560
  289. # Flow description
  290. draw.text((WIDTH // 2, info_y), "← Slicer traffic encrypted and relayed through Bambuddy to your printer →",
  291. font=fonts['small'], fill=TEXT_SECONDARY, anchor="mm")
  292. # Key features
  293. features_y = 600
  294. features = [
  295. "End-to-end TLS encryption",
  296. "No cloud dependency",
  297. "Uses printer's access code"
  298. ]
  299. spacing = 280
  300. start_x = WIDTH // 2 - spacing
  301. for i, feature in enumerate(features):
  302. fx = start_x + i * spacing
  303. # Bullet
  304. draw.ellipse([fx - 80, features_y - 3, fx - 74, features_y + 3], fill=BAMBU_GREEN)
  305. draw.text((fx - 68, features_y), feature, font=fonts['small'], fill=TEXT_SECONDARY, anchor="lm")
  306. # Bambuddy branding
  307. draw.text((WIDTH // 2, HEIGHT - 30), "bambuddy.cool", font=fonts['small'], fill=TEXT_LABEL, anchor="mm")
  308. return img
  309. def main():
  310. """Generate and save the diagram."""
  311. img = create_diagram()
  312. output_path = Path("/opt/claude/projects/bambuddy/docs/images/proxy-mode-diagram.png")
  313. output_path.parent.mkdir(parents=True, exist_ok=True)
  314. img.save(output_path, "PNG", dpi=(150, 150))
  315. print(f"Diagram saved to: {output_path}")
  316. # Also save to frontend docs
  317. frontend_path = Path("/opt/claude/projects/bambuddy/frontend/docs/proxy-mode-diagram.png")
  318. frontend_path.parent.mkdir(parents=True, exist_ok=True)
  319. img.save(frontend_path, "PNG", dpi=(150, 150))
  320. print(f"Also saved to: {frontend_path}")
  321. if __name__ == "__main__":
  322. main()