generate_connector_images.py 39 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035
  1. #!/usr/bin/env python3
  2. """
  3. Generate the connector drawings displayed for power port and power outlet types.
  4. Each drawing depicts the mating face of a connector as a 64x64 SVG: the face is outlined and its contacts are
  5. filled solid black, on a transparent background (the UI inverts the drawings in dark mode). Outlets are drawn as
  6. seen from the front; ports (plugs and inlets) are drawn as the mirror image of the face they mate with. Geometry
  7. is defined in millimetres (x right, y down, origin at the face's centre) and each face is scaled to fill the frame,
  8. so relative physical size is not conveyed. Contacts too small to read at icon size may be exaggerated.
  9. Usage: python scripts/generate_connector_images.py
  10. """
  11. import math
  12. from pathlib import Path
  13. OUTPUT_DIR = Path(__file__).resolve().parent.parent / 'netbox' / 'project-static' / 'img' / 'connectors'
  14. SIZE = 64 # Frame size (px)
  15. CONTENT = 56 # Size of the face's longest dimension within the frame
  16. OUTLINE = 3 # Stroke width of face outlines
  17. DETAIL = 2 # Stroke width of secondary lines
  18. def fmt(n):
  19. """Format a number compactly and deterministically."""
  20. s = f'{n:.2f}'.rstrip('0').rstrip('.')
  21. return '0' if s == '-0' else s
  22. def polar(cx, cy, radius, angle):
  23. """Return the point at the given radius and clock angle (degrees clockwise from 12 o'clock) about a centre."""
  24. rad = math.radians(angle)
  25. return cx + radius * math.sin(rad), cy - radius * math.cos(rad)
  26. def rotate(point, angle, about=(0, 0)):
  27. """Rotate a point clockwise (on screen) by the given angle in degrees."""
  28. rad = math.radians(angle)
  29. x, y = point[0] - about[0], point[1] - about[1]
  30. return (
  31. about[0] + x * math.cos(rad) - y * math.sin(rad),
  32. about[1] + x * math.sin(rad) + y * math.cos(rad),
  33. )
  34. #
  35. # Shapes
  36. #
  37. class Path:
  38. """
  39. A path made of move (M), line (L), circular arc (A) and close (Z) segments. Arc segments are
  40. ('A', radius, large_arc, sweep, x, y).
  41. """
  42. def __init__(self, segments):
  43. self.segments = segments
  44. def svg(self, tx):
  45. out = []
  46. for seg in self.segments:
  47. if seg[0] in ('M', 'L'):
  48. x, y = tx.point(seg[1], seg[2])
  49. out.append(f'{seg[0]}{fmt(x)} {fmt(y)}')
  50. elif seg[0] == 'A':
  51. _, r, large, sweep, x, y = seg
  52. x, y = tx.point(x, y)
  53. sweep = 1 - sweep if tx.mirror else sweep
  54. out.append(f'A{fmt(r * tx.scale)} {fmt(r * tx.scale)} 0 {large} {sweep} {fmt(x)} {fmt(y)}')
  55. else:
  56. out.append('Z')
  57. return f'<path d="{"".join(out)}"/>'
  58. class Circle:
  59. def __init__(self, cx, cy, r):
  60. self.cx, self.cy, self.r = cx, cy, r
  61. def svg(self, tx):
  62. x, y = tx.point(self.cx, self.cy)
  63. return f'<circle cx="{fmt(x)}" cy="{fmt(y)}" r="{fmt(self.r * tx.scale)}"/>'
  64. def polygon(points):
  65. return Path([('M', *points[0]), *(('L', *p) for p in points[1:]), ('Z',)])
  66. def rect(cx, cy, w, h, r=0, angle=0):
  67. """A rectangle centred on (cx, cy), optionally with rounded corners and rotated clockwise by angle degrees."""
  68. r = min(r, w / 2, h / 2)
  69. x0, x1, y0, y1 = -w / 2, w / 2, -h / 2, h / 2
  70. def pt(x, y):
  71. x, y = rotate((x, y), angle)
  72. return cx + x, cy + y
  73. if not r:
  74. return polygon([pt(x0, y0), pt(x1, y0), pt(x1, y1), pt(x0, y1)])
  75. return Path([
  76. ('M', *pt(x0 + r, y0)), ('L', *pt(x1 - r, y0)), ('A', r, 0, 1, *pt(x1, y0 + r)),
  77. ('L', *pt(x1, y1 - r)), ('A', r, 0, 1, *pt(x1 - r, y1)),
  78. ('L', *pt(x0 + r, y1)), ('A', r, 0, 1, *pt(x0, y1 - r)),
  79. ('L', *pt(x0, y0 + r)), ('A', r, 0, 1, *pt(x0 + r, y0)), ('Z',),
  80. ])
  81. def chamfered_rect(cx, cy, w, h, tl=0, tr=0, br=0, bl=0):
  82. """A rectangle with the given corners (top-left, top-right, bottom-right, bottom-left) chamfered."""
  83. x0, x1, y0, y1 = cx - w / 2, cx + w / 2, cy - h / 2, cy + h / 2
  84. points = [
  85. (x0 + tl, y0), (x1 - tr, y0), (x1, y0 + tr), (x1, y1 - br),
  86. (x1 - br, y1), (x0 + bl, y1), (x0, y1 - bl), (x0, y0 + tl),
  87. ]
  88. # Drop duplicate points where a corner isn't chamfered
  89. deduped = [p for i, p in enumerate(points) if p != points[i - 1]]
  90. return polygon(deduped)
  91. def rounded_polygon(points, radii=0):
  92. """A polygon whose corners are rounded by the given radius (or per-vertex radii; 0 leaves a corner sharp)."""
  93. if not isinstance(radii, (list, tuple)):
  94. radii = [radii] * len(points)
  95. segments = []
  96. for i, (p, r) in enumerate(zip(points, radii)):
  97. if not r:
  98. segments.append(('L', *p))
  99. continue
  100. a, b = points[i - 1], points[(i + 1) % len(points)]
  101. ux, uy = a[0] - p[0], a[1] - p[1]
  102. vx, vy = b[0] - p[0], b[1] - p[1]
  103. lu, lv = math.hypot(ux, uy), math.hypot(vx, vy)
  104. ux, uy, vx, vy = ux / lu, uy / lu, vx / lv, vy / lv
  105. theta = math.acos(max(-1, min(1, ux * vx + uy * vy)))
  106. t = r / math.tan(theta / 2)
  107. # A right turn (on screen) is a clockwise arc
  108. sweep = 1 if (p[0] - a[0]) * (b[1] - p[1]) - (p[1] - a[1]) * (b[0] - p[0]) > 0 else 0
  109. segments.append(('L', p[0] + ux * t, p[1] + uy * t))
  110. segments.append(('A', r, 0, sweep, p[0] + vx * t, p[1] + vy * t))
  111. segments[0] = ('M', *segments[0][1:3])
  112. return Path([*segments, ('Z',)])
  113. def arc_to(cx, cy, r, start, end, clockwise=True):
  114. """An arc segment along the given circle from start to end."""
  115. a0 = math.atan2(start[1] - cy, start[0] - cx)
  116. a1 = math.atan2(end[1] - cy, end[0] - cx)
  117. sweep = (a1 - a0) % (2 * math.pi) if clockwise else (a0 - a1) % (2 * math.pi)
  118. return ('A', r, 1 if sweep > math.pi else 0, 1 if clockwise else 0, *end)
  119. def circle_intersections(c0, r0, c1, r1):
  120. """Return the two intersections of two circles, ordered left-hand then right-hand of the line c0 -> c1."""
  121. dx, dy = c1[0] - c0[0], c1[1] - c0[1]
  122. d = math.hypot(dx, dy)
  123. a = (r0 ** 2 - r1 ** 2 + d ** 2) / (2 * d)
  124. h = math.sqrt(r0 ** 2 - a ** 2)
  125. mx, my = c0[0] + a * dx / d, c0[1] + a * dy / d
  126. return (mx + h * dy / d, my - h * dx / d), (mx - h * dy / d, my + h * dx / d)
  127. def keyed_circle(r, key_r, key_offset):
  128. """A circle with a semicircular key bump at 6 o'clock, centred key_offset below the circle's centre."""
  129. right, left = circle_intersections((0, 0), r, (0, key_offset), key_r)
  130. left, right = sorted((left, right))
  131. return Path([
  132. ('M', *left), arc_to(0, 0, r, left, right), arc_to(0, key_offset, key_r, right, left), ('Z',),
  133. ])
  134. def arc_slot(cx, cy, radius, start, end, width):
  135. """A curved slot following a circle of the given radius from clock angle start to end (clockwise)."""
  136. ro, ri = radius + width / 2, radius - width / 2
  137. large = 1 if (end - start) % 360 > 180 else 0
  138. return Path([
  139. ('M', *polar(cx, cy, ro, start)), ('A', ro, large, 1, *polar(cx, cy, ro, end)),
  140. ('L', *polar(cx, cy, ri, end)), ('A', ri, large, 0, *polar(cx, cy, ri, start)), ('Z',),
  141. ])
  142. #
  143. # Drawings
  144. #
  145. class Transform:
  146. def __init__(self, scale, mirror):
  147. self.scale = scale
  148. self.mirror = mirror
  149. def point(self, x, y):
  150. x *= self.scale
  151. return SIZE / 2 + (-x if self.mirror else x), SIZE / 2 + y * self.scale
  152. class Drawing:
  153. """
  154. The face of a connector. width and height give the extent of the face (in mm) about its centre, which is
  155. scaled to fill the frame.
  156. """
  157. def __init__(self, width, height):
  158. self.width = width
  159. self.height = height
  160. self.elements = []
  161. def outline(self, *shapes):
  162. self.elements.extend(('outline', s) for s in shapes)
  163. return self
  164. def detail(self, *shapes):
  165. self.elements.extend(('detail', s) for s in shapes)
  166. return self
  167. def fill(self, *shapes):
  168. self.elements.extend(('fill', s) for s in shapes)
  169. return self
  170. def svg(self, mirror=False):
  171. tx = Transform(CONTENT / max(self.width, self.height), mirror)
  172. groups = {
  173. 'outline': f'<g fill="none" stroke="#000" stroke-width="{OUTLINE}" stroke-linejoin="round">',
  174. 'detail': f'<g fill="none" stroke="#000" stroke-width="{DETAIL}" stroke-linejoin="round">',
  175. 'fill': '<g fill="#000">',
  176. }
  177. lines = [f'<svg xmlns="http://www.w3.org/2000/svg" width="{SIZE}" height="{SIZE}" viewBox="0 0 {SIZE} {SIZE}">']
  178. for style, opening in groups.items():
  179. shapes = [s.svg(tx) for st, s in self.elements if st == style]
  180. if shapes:
  181. lines.append(opening + ''.join(shapes) + '</g>')
  182. lines.append('</svg>')
  183. return '\n'.join(lines) + '\n'
  184. def mirrored(func):
  185. """Mark a face drawing to be rendered as its mirror image (i.e. the face of the mating connector)."""
  186. def wrapper(*args, **kwargs):
  187. return func(*args, **kwargs), True
  188. return wrapper
  189. #
  190. # IEC 60320 appliance couplers (earth at the top; outlets are female connectors, ports are male inlets)
  191. #
  192. def iec_c13_family(male=False, earth=True, notch=False):
  193. """C13/C14 (and the C15/C16 and C17/C18 variants): a rectangle with chamfered top corners."""
  194. if male:
  195. w, h, chamfer_x, chamfer_y, pin = 24, 16, 7, -3, (2.0, 4.0)
  196. else:
  197. w, h, chamfer_x, chamfer_y, pin = 23, 15.8, 6.1, -2.5, (2.5, 5.0)
  198. x, y = w / 2, h / 2
  199. points = [(-chamfer_x, -y), (chamfer_x, -y), (x, chamfer_y), (x, y)]
  200. radii = [1.5, 1.5, 1.5, 3]
  201. if notch:
  202. nw, apex = (2.25, 3.3) if male else (2.5, 3.0)
  203. points.append((nw, y))
  204. radii.append(0.8)
  205. for angle in range(0, 181, 15):
  206. points.append((nw * math.cos(math.radians(angle)), apex + nw - nw * math.sin(math.radians(angle))))
  207. radii.append(0)
  208. points.append((-nw, y))
  209. radii.append(0.8)
  210. points += [(-x, y), (-x, chamfer_y)]
  211. radii += [3, 1.5]
  212. drawing = Drawing(w, h).outline(rounded_polygon(points, radii))
  213. drawing.fill(rect(-7, 2, *pin), rect(7, 2, *pin))
  214. if earth:
  215. drawing.fill(rect(0, -2, *pin))
  216. return drawing
  217. def iec_c19_family(male=False, chamfer=False):
  218. """C19/C20 (and the C21/C22 variants): a rectangle with horizontal contacts."""
  219. if male:
  220. w, h, chamfer_x, chamfer_y, earth, line, thickness = 29, 21, 7.5, -3.5, 6, 5, 2.0
  221. else:
  222. w, h, chamfer_x, chamfer_y, earth, line, thickness = 28, 20, 7, -3, 7, 6, 2.5
  223. x, y = w / 2, h / 2
  224. if chamfer:
  225. outline = rounded_polygon(
  226. [(-chamfer_x, -y), (chamfer_x, -y), (x, chamfer_y), (x, y), (-x, y), (-x, chamfer_y)],
  227. [1.5, 1.5, 1.5, 3, 3, 1.5],
  228. )
  229. else:
  230. outline = rect(0, 0, w, h, 3)
  231. return Drawing(w, h).outline(outline).fill(
  232. rect(0, -4, earth, thickness), rect(-6.5, 4, line, thickness), rect(6.5, 4, line, thickness),
  233. )
  234. def iec_c5_family(male=False):
  235. """C5/C6 ("cloverleaf"): the union of three circular lobes."""
  236. earth, live, neutral = ((0, -2), 4.5), ((-5, 2.5), 4.0), ((5, 2.5), 4.0)
  237. en_top, en_bottom = circle_intersections(earth[0], earth[1], neutral[0], neutral[1])
  238. el_bottom, el_top = circle_intersections(earth[0], earth[1], live[0], live[1])
  239. outline = Path([
  240. ('M', *el_top),
  241. arc_to(*earth[0], earth[1], el_top, en_top),
  242. arc_to(*neutral[0], neutral[1], en_top, en_bottom),
  243. arc_to(*earth[0], earth[1], en_bottom, el_bottom),
  244. arc_to(*live[0], live[1], el_bottom, el_top),
  245. ('Z',),
  246. ])
  247. earth_r, line_r = (1.6, 1.18) if male else (1.9, 1.45)
  248. return Drawing(18, 13).outline(outline).fill(
  249. Circle(0, -2, earth_r), Circle(-5, 2.5, line_r), Circle(5, 2.5, line_r),
  250. )
  251. def iec_c7_family(male=False):
  252. """C7/C8 ("figure 8"): two overlapping circular lobes."""
  253. r, pin_r = (4.1, 1.18) if male else (4.0, 1.45)
  254. top, bottom = circle_intersections((-3.6, 0), r, (3.6, 0), r)
  255. outline = Path([
  256. ('M', *top), arc_to(3.6, 0, r, top, bottom), arc_to(-3.6, 0, r, bottom, top), ('Z',),
  257. ])
  258. return Drawing(7.2 + 2 * r, 2 * r).outline(outline).fill(Circle(-4.3, 0, pin_r), Circle(4.3, 0, pin_r))
  259. #
  260. # IEC 60309 pin & sleeve (16 A). Viewed from the front of the socket with the keyway at 6 o'clock, the earth
  261. # contact sits at the clock hour given by the type and the remaining contacts follow it clockwise.
  262. #
  263. IEC_60309_SIZES = {
  264. # Contacts: (recess radius, key bump offset, pin circle radius, plug shroud inner/outer radii)
  265. 3: (22.15, 21.85, 8.75, (18.95, 21.75)),
  266. 4: (25.2, 25.3, 10.75, (21.4, 24.75)),
  267. 5: (28.65, 29.15, 13.25, (24.4, 28.05)),
  268. }
  269. def iec_60309(contacts, hour, male=False):
  270. radius, key_offset, pin_circle, shroud = IEC_60309_SIZES[contacts]
  271. if male:
  272. outer = shroud[1]
  273. drawing = Drawing(2 * outer, 2 * outer).outline(keyed_circle(outer, 3, outer + 0.2))
  274. drawing.detail(Circle(0, 0, shroud[0]))
  275. earth_r, line_r = 3.5, 2.5
  276. else:
  277. drawing = Drawing(2 * radius, 2 * radius).outline(keyed_circle(radius, 3.5, key_offset))
  278. earth_r, line_r = 4.0, 3.0
  279. for i in range(contacts):
  280. x, y = polar(0, 0, pin_circle, hour * 30 + i * 360 / contacts)
  281. drawing.fill(Circle(x, y, earth_r if i == 0 else line_r))
  282. return drawing
  283. #
  284. # IEC 60906-1 & NBR 14136 (three round contacts within a flattened hexagon; earth offset towards the top)
  285. #
  286. def iec_60906(pin_r):
  287. outline = rounded_polygon([(-9.9, -8.5), (9.9, -8.5), (18.4, 0), (9.9, 8.5), (-9.9, 8.5), (-18.4, 0)], 1.5)
  288. return Drawing(35.5, 17).outline(outline).fill(Circle(-9.5, 0, pin_r), Circle(9.5, 0, pin_r), Circle(0, -3, pin_r))
  289. #
  290. # NEMA straight-blade receptacles (face radius = 1; oriented as on the NEMA configuration chart, ground at the top)
  291. #
  292. NEMA_SLOT = 0.15
  293. def nema_v(x, y, length):
  294. """A vertical slot."""
  295. return rect(x, y, NEMA_SLOT, length)
  296. def nema_h(x, y, length):
  297. """A horizontal slot."""
  298. return rect(x, y, length, NEMA_SLOT)
  299. def nema_arm(x0, x1, y):
  300. """A horizontal arm extending from a vertical slot at x0 to x1 (forming a T- or L-shaped slot)."""
  301. return rect((x0 + x1) / 2, y, abs(x1 - x0), NEMA_SLOT)
  302. def nema_ground(y=-0.58):
  303. """A U-shaped ground hole, flat side towards the rim."""
  304. return Path([
  305. ('M', -0.15, y - 0.14), ('L', 0.15, y - 0.14), ('L', 0.15, y), arc_to(0, y, 0.15, (0.15, y), (-0.15, y)),
  306. ('Z',),
  307. ])
  308. def nema(*slots, ground=-0.58):
  309. drawing = Drawing(2, 2).outline(Circle(0, 0, 1)).fill(*slots)
  310. if ground is not None:
  311. drawing.fill(nema_ground(ground))
  312. return drawing
  313. def nema_10(neutral):
  314. return nema(*neutral, rect(0.39, 0.25, NEMA_SLOT, 0.5, angle=30), rect(-0.37, 0.23, NEMA_SLOT, 0.5, angle=-30),
  315. ground=None)
  316. NEMA_STRAIGHT = {
  317. '1-15': lambda: nema(nema_v(0.38, 0, 0.76), nema_v(-0.38, 0, 0.48), ground=None),
  318. '5-15': lambda: nema(nema_v(0.38, 0.12, 0.76), nema_v(-0.38, 0.12, 0.47)),
  319. '5-20': lambda: nema(nema_v(0.29, 0.12, 0.7), nema_arm(0.29, 0.75, 0.1), nema_v(-0.38, 0.12, 0.47)),
  320. '5-30': lambda: nema(nema_v(0.375, 0.11, 0.47), nema_arm(0.375, 0.1, -0.02), nema_v(-0.38, 0.12, 0.47)),
  321. '5-50': lambda: nema(nema_v(0.38, 0.16, 0.76), nema_v(-0.57, 0.13, 0.48)),
  322. '6-15': lambda: nema(nema_h(0.43, 0.23, 0.48), nema_h(-0.43, 0.23, 0.48)),
  323. '6-20': lambda: nema(nema_v(0.25, 0.115, 0.75), nema_arm(0.25, 0.79, 0.13), nema_h(-0.42, 0.15, 0.47)),
  324. '6-30': lambda: nema(nema_h(0.43, 0.14, 0.47), nema_h(-0.43, 0.14, 0.47)),
  325. '6-50': lambda: nema(nema_v(-0.46, 0.17, 0.76), nema_v(0.48, 0.12, 0.48)),
  326. '10-30': lambda: nema_10([nema_v(-0.08, -0.52, 0.44), nema_arm(-0.08, 0.28, -0.37)]),
  327. '10-50': lambda: nema_10([nema_v(0.01, -0.46, 0.66)]),
  328. '14-20': lambda: nema(
  329. nema_v(0.55, -0.06, 0.47), nema_v(-0.55, -0.06, 0.47), nema_h(0, 0.45, 0.47), ground=-0.63
  330. ),
  331. '14-30': lambda: nema(
  332. nema_v(0.53, -0.04, 0.65), nema_v(-0.51, -0.04, 0.65), nema_v(0.135, 0.51, 0.48), nema_arm(0.135, -0.2, 0.37),
  333. ground=-0.63,
  334. ),
  335. '14-50': lambda: nema(
  336. nema_v(0.53, -0.04, 0.66), nema_v(-0.53, -0.04, 0.66), nema_v(0.01, 0.51, 0.66), ground=-0.63
  337. ),
  338. '14-60': lambda: nema(
  339. nema_v(0.53, -0.04, 0.66), nema_v(-0.53, -0.04, 0.66), nema_h(0.03, 0.56, 0.65), ground=-0.63
  340. ),
  341. '15-15': lambda: nema(nema_v(-0.58, 0, 0.48), nema_h(0.55, 0, 0.48), nema_h(0, 0.58, 0.48)),
  342. '15-20': lambda: nema(nema_v(-0.57, 0, 0.66), nema_h(0.45, 0, 0.64), nema_h(0, 0.59, 0.64)),
  343. '15-30': lambda: nema(nema_h(-0.52, 0, 0.47), nema_h(0.55, 0, 0.47), nema_h(0, 0.58, 0.47)),
  344. '15-50': lambda: nema(nema_v(0.6, 0, 0.48), nema_h(-0.51, 0, 0.47), nema_h(0, 0.58, 0.47)),
  345. '15-60': lambda: nema(
  346. nema_v(-0.52, -0.04, 0.66), nema_h(0.45, 0, 0.65), nema_h(0.03, 0.56, 0.65), ground=-0.63
  347. ),
  348. }
  349. #
  350. # NEMA locking ("twist-lock") receptacles (face radius = 1; oriented as on the NEMA configuration chart). Each
  351. # slot is an arc between two clock angles, optionally with a tab at one end stepping in or out to a given radius.
  352. #
  353. LOCKING_SLOT = 0.15
  354. TAB_SPAN = 15
  355. TAB_EXAGGERATION = 0.04
  356. def locking_slot(start, end, radius=0.555, width=LOCKING_SLOT, tab=None):
  357. """
  358. A curved slot. tab is (end, radius): the angle of the slot end at which a tab extends radially to the given
  359. radius.
  360. """
  361. shapes = [arc_slot(0, 0, radius, start, end, width)]
  362. if tab:
  363. at, to = tab
  364. # Tabs are exaggerated slightly to remain legible at small sizes
  365. to += TAB_EXAGGERATION if to > radius else -TAB_EXAGGERATION
  366. edge = radius + width / 2 if to > radius else radius - width / 2
  367. span = (at - TAB_SPAN, at) if at % 360 == end % 360 else (at, at + TAB_SPAN)
  368. shapes.append(arc_slot(0, 0, (edge + to) / 2, *span, abs(to - edge) + 0.02))
  369. return shapes
  370. def locking(*slots, center_ground=False):
  371. drawing = Drawing(2, 2).outline(Circle(0, 0, 1))
  372. for slot in slots:
  373. drawing.fill(*slot)
  374. if center_ground:
  375. drawing.fill(Circle(0, 0, 0.1))
  376. return drawing
  377. def l_14(w, ground_tab):
  378. return locking(
  379. locking_slot(331, 26), locking_slot(*w), locking_slot(152, 208), locking_slot(238, 298, tab=ground_tab),
  380. )
  381. def l_15(ground_tab):
  382. return locking(
  383. locking_slot(330, 13), locking_slot(65, 124), locking_slot(152, 208), locking_slot(242, 297, tab=ground_tab),
  384. )
  385. def l_21(x, y, z, neutral_tab):
  386. return locking(
  387. locking_slot(*x), locking_slot(*y), locking_slot(*z),
  388. locking_slot(237, 297, radius=0.52, tab=neutral_tab), center_ground=True,
  389. )
  390. NEMA_LOCKING = {
  391. 'l1-15': lambda: locking(locking_slot(329, 31, 0.605), locking_slot(145, 214, 0.54, tab=(145, 0.69))),
  392. 'l5-15': lambda: locking(
  393. locking_slot(16, 85, 0.525), locking_slot(139, 200, 0.605), locking_slot(254, 317, 0.56, tab=(254, 0.41)),
  394. ),
  395. 'l5-20': lambda: locking(locking_slot(10, 44), locking_slot(116, 181), locking_slot(234, 299, tab=(299, 0.39))),
  396. 'l5-30': lambda: locking(locking_slot(7, 45), locking_slot(122, 174), locking_slot(234, 300, tab=(300, 0.39))),
  397. 'l6-15': lambda: locking(
  398. locking_slot(15, 84, 0.54), locking_slot(138, 202, 0.565), locking_slot(253, 321, 0.55, tab=(321, 0.7)),
  399. ),
  400. 'l6-20': lambda: locking(locking_slot(0, 53), locking_slot(126, 168), locking_slot(235, 298, tab=(298, 0.39))),
  401. 'l6-30': lambda: locking(locking_slot(0, 53), locking_slot(126, 168), locking_slot(235, 298, tab=(298, 0.39))),
  402. 'l10-30': lambda: locking(
  403. locking_slot(0, 43, 0.68), locking_slot(119, 162, 0.68), locking_slot(238, 287, 0.68, tab=(238, 0.51)),
  404. ),
  405. 'l14-20': lambda: l_14((68, 112), (298, 0.68)),
  406. 'l14-30': lambda: l_14((69, 107), (298, 0.4)),
  407. 'l15-20': lambda: l_15((297, 0.68)),
  408. 'l15-30': lambda: l_15((297, 0.4)),
  409. 'l21-20': lambda: l_21((327, 21), (67, 105), (147, 201), (237, 0.69)),
  410. 'l21-30': lambda: l_21((327, 21), (67, 105), (147, 201), (237, 0.38)),
  411. 'l22-20': lambda: l_21((337, 14), (58, 116), (151, 207), (237, 0.69)),
  412. 'l22-30': lambda: l_21((337, 14), (58, 116), (151, 207), (237, 0.38)),
  413. }
  414. #
  415. # California Style 50 A locking connectors (Hubbell; face radius = 1). Three curved contacts around a centre post
  416. # (a hole on the plug). Three-pole types have keying lugs around the rim and a recessed sector.
  417. #
  418. CS_SLOT = 0.17
  419. def cs_slot(start, end, tab=None, step=None):
  420. """A curved contact. step is (start, end, radius): a portion of the slot widened outward to the given radius."""
  421. shapes = locking_slot(start, end, 0.525, CS_SLOT, tab)
  422. if step:
  423. a0, a1, to = step
  424. edge = 0.525 + CS_SLOT / 2
  425. shapes.append(arc_slot(0, 0, (edge + to) / 2, a0, a1, to - edge + 0.02))
  426. return shapes
  427. def cs_outline(lugs, recess):
  428. """A circular rim with semicircular lugs at the given angles and a recessed sector (start, end)."""
  429. lug_r, recess_r = 0.125, 0.93
  430. delta = math.degrees(2 * math.asin(lug_r / 2))
  431. events = sorted([(a % 360, 'lug') for a in lugs] + [(recess[0], 'recess')])
  432. start = polar(0, 0, 1, events[0][0] - delta - 1)
  433. segments = [('M', *start)]
  434. point = start
  435. for angle, kind in events:
  436. if kind == 'lug':
  437. p0, p1 = polar(0, 0, 1, angle - delta), polar(0, 0, 1, angle + delta)
  438. segments += [arc_to(0, 0, 1, point, p0), arc_to(*polar(0, 0, 1, angle), lug_r, p0, p1)]
  439. point = p1
  440. else:
  441. p0 = polar(0, 0, 1, recess[0])
  442. q0, q1 = polar(0, 0, recess_r, recess[0]), polar(0, 0, recess_r, recess[1])
  443. point = polar(0, 0, 1, recess[1])
  444. segments += [arc_to(0, 0, 1, segments[-1][-2:], p0), ('L', *q0), arc_to(0, 0, recess_r, q0, q1),
  445. ('L', *point)]
  446. segments += [arc_to(0, 0, 1, point, start), ('Z',)]
  447. return Path(segments)
  448. def california(slots, male=False, lugs=None, recess=None):
  449. outline = cs_outline(lugs, recess) if lugs else Circle(0, 0, 1)
  450. drawing = Drawing(2.25 if lugs else 2, 2.25 if lugs else 2).outline(outline)
  451. for slot in slots:
  452. drawing.fill(*slot)
  453. if male:
  454. drawing.detail(Circle(0, 0, 0.12))
  455. else:
  456. drawing.fill(Circle(0, 0, 0.14))
  457. return drawing
  458. CALIFORNIA_STYLE = {
  459. # Connector (female), plug (male): (contacts, rim lugs, recessed sector)
  460. ('CS6360C', 'cs6361c'): ([cs_slot(5, 56), cs_slot(117, 185), cs_slot(247, 300, tab=(247, 0.72))], None, None),
  461. ('CS8264C', 'cs8265c'): ([cs_slot(9, 60), cs_slot(119, 188), cs_slot(247, 300, tab=(247, 0.72))], None, None),
  462. ('CS8464C', 'cs8465c'): ([cs_slot(6, 57), cs_slot(113, 188), cs_slot(235, 299, tab=(235, 0.36))], None, None),
  463. ('CS6364C', 'cs6365c'): (
  464. [cs_slot(359, 59, step=(359, 29, 0.66)), cs_slot(126, 175), cs_slot(239, 294, step=(239, 266, 0.7))],
  465. (5, 35, 205, 238), (89, 155),
  466. ),
  467. ('CS8364C', 'cs8365c'): (
  468. [cs_slot(3, 71, tab=(3, 0.33)), cs_slot(125, 176), cs_slot(232, 299, tab=(232, 0.34))],
  469. (13, 41, 204, 234), (90, 158),
  470. ),
  471. ('CS8164C', 'cs8165c'): (
  472. [cs_slot(1, 58, step=(1, 30, 0.66)), cs_slot(127, 181), cs_slot(230, 295, step=(230, 262, 0.7))],
  473. (330, 8, 189, 226), (87, 160),
  474. ),
  475. }
  476. #
  477. # International (ITA/IEC World Plugs types). Sockets are drawn as seen from the front; plugs whose face differs
  478. # from the mirror image of the socket are drawn separately.
  479. #
  480. def ita_c_plug():
  481. """Europlug (CEE 7/16)."""
  482. outline = rounded_polygon(
  483. [(-10.8, -6.85), (10.8, -6.85), (17.65, 0), (10.8, 6.85), (-10.8, 6.85), (-17.65, 0)], 1.5
  484. )
  485. return Drawing(35.3, 13.7).outline(outline).fill(Circle(-9.25, 0, 2), Circle(9.25, 0, 2))
  486. def schuko_plug(strips=True, earth_hole=False):
  487. """CEE 7/4 (with earth strips at 12 and 6 o'clock), CEE 7/6 (with an earth hole), and CEE 7/7 (both)."""
  488. r = 18.25
  489. # Round body with guide grooves at 3 and 9 o'clock
  490. groove = math.degrees(math.asin(1.6 / r))
  491. points = [polar(0, 0, r, a) for a in (90 - groove, 90 + groove, 270 - groove, 270 + groove)]
  492. inner = [(p[0] - math.copysign(2, p[0]), p[1]) for p in points]
  493. outline = Path([
  494. ('M', *points[3]), arc_to(0, 0, r, points[3], points[0]), ('L', *inner[0]), ('L', *inner[1]),
  495. ('L', *points[1]), arc_to(0, 0, r, points[1], points[2]), ('L', *inner[2]), ('L', *inner[3]), ('Z',),
  496. ])
  497. drawing = Drawing(2 * r, 2 * r).outline(outline).fill(Circle(-9.5, 0, 2.4), Circle(9.5, 0, 2.4))
  498. if strips:
  499. drawing.fill(rect(0, -r + 1, 6, 2), rect(0, r - 1, 6, 2))
  500. if earth_hole:
  501. drawing.detail(Circle(0, -10, 2.6))
  502. return drawing
  503. def ita_e_socket():
  504. """CEE 7/5: a round recess with a projecting earth pin."""
  505. return Drawing(40, 40).outline(Circle(0, 0, 20)).fill(
  506. Circle(-9.5, 0, 2.75), Circle(9.5, 0, 2.75), Circle(0, -10, 2.4),
  507. ).detail(Circle(0, -10, 3.8))
  508. def ita_f_socket():
  509. """CEE 7/3 (Schuko): a round recess with earth clips at 12 and 6 o'clock."""
  510. return Drawing(40, 40).outline(Circle(0, 0, 20)).fill(
  511. Circle(-9.5, 0, 2.75), Circle(9.5, 0, 2.75), rect(0, -18, 6, 2.4), rect(0, 18, 6, 2.4),
  512. )
  513. def ita_g(socket=True):
  514. """BS 1363."""
  515. earth, line = ((4.5, 8.5), (7, 4.5)) if socket else ((4, 8), (6.4, 4))
  516. outline = rect(0, 0, 48, 48, 5) if socket else rect(0, 1.5, 48, 45, 5)
  517. return Drawing(48, 48).outline(outline).fill(
  518. rect(0, -11.1, *earth), rect(-11.1, 11.1, *line), rect(11.1, 11.1, *line),
  519. )
  520. def ita_h(socket=True):
  521. """SI 32 (round pins)."""
  522. r = 2.6 if socket else 2.25
  523. return Drawing(38, 38).outline(Circle(0, 0, 19)).fill(
  524. Circle(-9.5, -4.75, r), Circle(9.5, -4.75, r), Circle(0, 4.75, r),
  525. )
  526. def ita_i(socket=True):
  527. """AS/NZS 3112."""
  528. length, thickness = (7, 2.2) if socket else (6.35, 2)
  529. outline = rect(0, 0, 38, 38, 4) if socket else rect(0, 0, 36, 38, 9)
  530. return Drawing(38, 38).outline(outline).fill(
  531. rect(-6.86, -7.2, thickness, length, angle=30), rect(6.86, -7.2, thickness, length, angle=-30),
  532. rect(0, 7.1, thickness, length),
  533. )
  534. def ita_j(socket=True):
  535. """SN 441011 (T12 plug/T13 socket): a hexagonal profile with the earth offset below the line contacts."""
  536. r = 2.25 if socket else 2
  537. outline = rounded_polygon(
  538. [(-8.38, -10), (8.38, -10), (18, -0.38), (18, 0.38), (8.38, 10), (-8.38, 10), (-18, 0.38), (-18, -0.38)],
  539. [3, 3, 0.9, 0.9, 3, 3, 0.9, 0.9] if socket else [10, 10, 0.9, 0.9, 10, 10, 0.9, 0.9],
  540. )
  541. return Drawing(36, 20).outline(outline).fill(Circle(-9.5, 0, r), Circle(9.5, 0, r), Circle(0, 5, r))
  542. def ita_k(socket=True):
  543. """DS 107-2-D1 (Danish): a D-shaped earth contact below the line contacts."""
  544. r = 2.65 if socket else 2.4
  545. cy, earth_r = 7.5, 3.25
  546. chord = math.sqrt(earth_r ** 2 - 0.75 ** 2)
  547. earth = Path([
  548. ('M', -chord, cy - 0.75), ('L', chord, cy - 0.75),
  549. arc_to(0, cy, earth_r, (chord, cy - 0.75), (-chord, cy - 0.75)), ('Z',),
  550. ])
  551. return Drawing(40, 40).outline(Circle(0, 0, 20)).fill(Circle(-9.5, -5.5, r), Circle(9.5, -5.5, r), earth)
  552. def ita_l_plug():
  553. """CEI 23-50 S11 (10 A): three contacts in line."""
  554. outline = rounded_polygon([(-10.5, -7), (10.5, -7), (17.5, 0), (10.5, 7), (-10.5, 7), (-17.5, 0)], 1.5)
  555. return Drawing(35, 14).outline(outline).fill(Circle(-9.5, 0, 2), Circle(0, 0, 2), Circle(9.5, 0, 2))
  556. def ita_l_socket():
  557. """CEI 23-50 P17/11 ("bipasso"), mounted vertically: accepts both 10 A and 16 A plugs."""
  558. drawing = Drawing(22, 44).outline(rect(0, 0, 22, 44, 4)).fill(Circle(0, 0, 2.25))
  559. for sign in (-1, 1):
  560. drawing.fill(Circle(0, sign * 9.5, 2.25), Circle(0, sign * 13, 2.75))
  561. return drawing
  562. def ita_m(socket=True):
  563. """BS 546 (15 A)."""
  564. line_r, earth_r = (3.8, 4.6) if socket else (3.53, 4.36)
  565. outline = rect(0, 0, 54, 54, 6) if socket else Circle(0, 0, 27)
  566. return Drawing(54, 54).outline(outline).fill(
  567. Circle(-12.7, 14.3, line_r), Circle(12.7, 14.3, line_r), Circle(0, -14.3, earth_r),
  568. )
  569. def ita_o(socket=True):
  570. """TIS 166-2549 (Thai)."""
  571. r = 2.65 if socket else 2.4
  572. return Drawing(39, 39).outline(Circle(0, 0, 19.5)).fill(
  573. Circle(-9.5, 5.95, r), Circle(9.5, 5.95, r), Circle(0, -5.95, r),
  574. )
  575. #
  576. # USB (receptacle openings)
  577. #
  578. def usb_a():
  579. return Drawing(12, 4.5).outline(rect(0, 0, 12, 4.5, 0.3)).fill(rect(0, -0.85, 10, 1.9))
  580. def usb_b():
  581. return Drawing(8.5, 7.8).outline(chamfered_rect(0, 0, 8.5, 7.8, tl=1.4, tr=1.4)).fill(rect(0, 0.4, 5.2, 3.2))
  582. def usb_3_b():
  583. outline = polygon([
  584. (-2.9, -5.6), (2.9, -5.6), (2.9, -2.4), (4.25, -2.4), (4.25, 5.6), (-4.25, 5.6), (-4.25, -2.4),
  585. (-2.9, -2.4),
  586. ])
  587. return Drawing(8.5, 11.2).outline(outline).fill(rect(0, 1.2, 5.2, 3.2), rect(0, -3.8, 3.6, 1.6))
  588. def usb_c():
  589. return Drawing(8.4, 2.6).outline(rect(0, 0, 8.4, 2.6, 1.3)).fill(rect(0, 0, 6.6, 0.7, 0.15))
  590. def usb_mini_a():
  591. outline = Path([
  592. ('M', -3.5, -1.5), ('L', 3.5, -1.5), ('L', 3.5, 0.3), ('A', 1.2, 0, 1, 2.3, 1.5),
  593. ('L', -2.3, 1.5), ('A', 1.2, 0, 1, -3.5, 0.3), ('Z',),
  594. ])
  595. return Drawing(7, 3).outline(outline).fill(rect(0, -0.6, 5, 0.6))
  596. def usb_mini_b():
  597. outline = polygon([
  598. (-3.5, -1.5), (3.5, -1.5), (3.5, 0.2), (2.9, 0.2), (2.3, 1.5), (-2.3, 1.5), (-2.9, 0.2), (-3.5, 0.2),
  599. ])
  600. return Drawing(7, 3).outline(outline).fill(rect(0, -0.6, 5, 0.6))
  601. def usb_micro_a():
  602. return Drawing(6.9, 1.85).outline(rect(0, 0, 6.9, 1.85, 0.2)).fill(rect(0, -0.45, 5, 0.45))
  603. def usb_micro_b():
  604. return Drawing(6.9, 1.85).outline(chamfered_rect(0, 0, 6.9, 1.85, br=0.7, bl=0.7)).fill(rect(0, -0.45, 5, 0.45))
  605. def usb_3_micro_b():
  606. outline = polygon([
  607. (-6.1, -0.92), (6.1, -0.92), (6.1, 0.22), (5.4, 0.92), (1.9, 0.92), (1.9, 0.1), (0.9, 0.1), (0.9, 0.92),
  608. (-5.4, 0.92), (-6.1, 0.22),
  609. ])
  610. return Drawing(12.2, 1.85).outline(outline).fill(rect(-2.6, -0.45, 5, 0.45), rect(3.9, -0.45, 3.3, 0.45))
  611. #
  612. # Molex Micro-Fit 3.0 (3 mm pitch; cavities alternate square and rounded for polarization)
  613. #
  614. def molex_micro_fit(rows, columns):
  615. pitch = 3.0
  616. width, height = columns * pitch + 1.4, rows * pitch + 1.4
  617. drawing = Drawing(width, height + 2.4)
  618. top = -(height + 2.4) / 2 + 2.4
  619. drawing.outline(
  620. rect(0, top + height / 2, width, height, 0.4),
  621. rect(0, top - 1.2, min(4, width - 1), 2.4, 0.3), # Latch
  622. )
  623. for row in range(rows):
  624. for column in range(columns):
  625. cx = (column - (columns - 1) / 2) * pitch
  626. cy = top + 0.7 + pitch / 2 + row * pitch
  627. rounded = (row + column) % 2
  628. drawing.fill(rect(cx, cy, 2.1, 2.1, 0.9 if rounded else 0.15))
  629. return drawing
  630. #
  631. # Proprietary
  632. #
  633. def powercon_chassis(flange=(26, 31), holes=(9.5, 12)):
  634. """The square flange and mounting holes common to powerCON chassis connectors."""
  635. w, h = flange
  636. return Drawing(w, h).outline(rect(0, 0, w, h, 3.5)).detail(
  637. Circle(-holes[0], -holes[1], 1.7), Circle(holes[0], holes[1], 1.7),
  638. )
  639. def powercon_20a():
  640. """NAC3MPA (power in); the power out variant (NAC3MPB) is its mirror image."""
  641. drawing = powercon_chassis().detail(Circle(0, 0, 11.8), Circle(0, 0, 6.4))
  642. return drawing.fill(
  643. rect(0, -9.5, 5.6, 1.9), # Latch
  644. rect(-9.4, 0, 1.6, 3.8), # Keying notch
  645. arc_slot(0, 0, 2.8, 45, 315, 0.8), # Centre insulator
  646. rect(*polar(0, 0, 4.5, 315), 0.8, 2.8, angle=315), # Long rib
  647. rect(*polar(0, 0, 3.8, 45), 0.8, 1.4, angle=45), # Short rib
  648. arc_slot(0, 0, 5.95, 65, 115, 0.9), arc_slot(0, 0, 5.95, 245, 295, 0.9),
  649. )
  650. def powercon_32a():
  651. """NAC3MP-HC (power in only)."""
  652. drawing = powercon_chassis((32, 37), (12.5, 15)).detail(Circle(0, 0, 14.8), Circle(0, 0, 10.5), Circle(0, 0, 4.65))
  653. drawing.fill(arc_slot(0, 0, 13.6, -14, 14, 1.4), rect(0, 11.8, 3.5, 1.9))
  654. for angle in (0, 90, 180, 270):
  655. x, y = rotate((-0.85, -3.9), angle)
  656. drawing.fill(rect(x, y, 3.2, 0.9, angle=angle))
  657. return drawing
  658. def powercon_true1_inlet():
  659. """NAC3MPX (also TRUE1 TOP, whose face differs only in a small centre detail)."""
  660. drawing = powercon_chassis().detail(Circle(0, 0, 11.8), Circle(0, 0, 6.45))
  661. drawing.fill(arc_slot(0, 0, 9.85, -17, 17, 1.7))
  662. for angle in (90, 180, 270):
  663. drawing.fill(rect(*polar(0, 0, 11, angle), 2.4, 1.6, angle=angle))
  664. for angle in (300, 51, 180):
  665. drawing.fill(arc_slot(0, 0, 5.1, angle - 15, angle + 15, 1.1))
  666. return drawing
  667. def powercon_true1_outlet():
  668. """NAC3FPX (also TRUE1 TOP)."""
  669. drawing = Drawing(35.4, 35.4).outline(rect(0, 0, 35.4, 35.4, 5.5)).detail(
  670. Circle(0, 0, 14.2), Circle(0, 0, 2.65),
  671. *(Circle(x, y, 1.72) for x in (-12.2, 12.2) for y in (-12.2, 12.2)),
  672. )
  673. for angle in (284, 164, 44):
  674. drawing.fill(arc_slot(0, 0, 5.125, angle - 49, angle + 49, 1.35))
  675. return drawing
  676. def saf_d_grid():
  677. """Saf-D-Grid 400 receptacle (used on both equipment and PDUs)."""
  678. opening = rounded_polygon(
  679. [(-8.9, -8.85), (8.9, -8.85), (11.9, -0.8), (11.9, 8.85), (-11.9, 8.85), (-11.9, -0.8)], [1, 1, 1, 2, 2, 1],
  680. )
  681. return Drawing(33.4, 24.1).outline(rect(0, 0, 33.4, 24.1, 2)).detail(opening).fill(
  682. rect(0, -1.8, 4.4, 1.4), rect(-6.9, 3, 4.4, 1.4), rect(6.9, 3, 4.4, 1.4),
  683. )
  684. def hdot_cx():
  685. """Accepts both C14 and C20 plugs: a C13 profile with slots combining the C13 and C19 patterns."""
  686. drawing = iec_c13_family()
  687. drawing.elements = [e for e in drawing.elements if e[0] == 'outline']
  688. return drawing.fill(
  689. rect(0, -4, 7, 2.5), rect(0, -2, 2.5, 5),
  690. rect(-6.5, 4, 6, 2.5), rect(-7, 2, 2.5, 5),
  691. rect(6.5, 4, 6, 2.5), rect(7, 2, 2.5, 5),
  692. )
  693. def eaton_c39():
  694. """Accepts both C14 and C20 plugs: a C20-sized cavity around a stepped block with T-shaped slots."""
  695. block = polygon([
  696. (-5.75, -7), (5.75, -7), (5.75, -2), (11, -2), (11, 7), (2.4, 7), (2.4, 3), (-2.4, 3), (-2.4, 7),
  697. (-11, 7), (-11, -2), (-5.75, -2),
  698. ])
  699. return Drawing(35, 27.5).outline(rect(0, 0, 35, 27.5, 3.5)).detail(block).fill(
  700. rect(0, -4, 7, 2.5), rect(0, -2, 2.5, 5),
  701. rect(-6.5, 4, 6, 2.5), rect(-7, 2, 2.5, 5),
  702. rect(6.5, 4, 6, 2.5), rect(7, 2, 2.5, 5),
  703. )
  704. #
  705. # Registry
  706. #
  707. IEC_60309_TYPES = {
  708. # Value prefix: number of contacts
  709. 'iec-60309-p-n-e': 3,
  710. 'iec-60309-2p-e': 3,
  711. 'iec-60309-3p-e': 4,
  712. 'iec-60309-3p-n-e': 5,
  713. }
  714. def iec_60309_types(male=False):
  715. """Return a drawing function for each IEC 60309 type."""
  716. def factory(contacts, hour):
  717. def func():
  718. drawing = iec_60309(contacts, hour, male=male)
  719. return (drawing, True) if male else drawing
  720. return func
  721. return {
  722. f'{prefix}-{hour}h': factory(contacts, hour)
  723. for prefix, contacts in IEC_60309_TYPES.items()
  724. for hour in (4, 6, 9)
  725. }
  726. def nema_types(male=False):
  727. """Return a drawing function for each NEMA (straight-blade and locking) and California Style type."""
  728. suffix = 'p' if male else 'r'
  729. types = {f'nema-{name}{suffix}': func for name, func in {**NEMA_STRAIGHT, **NEMA_LOCKING}.items()}
  730. if male:
  731. types = {value: mirrored(func) for value, func in types.items()}
  732. for (female_value, male_value), (slots, lugs, recess) in CALIFORNIA_STYLE.items():
  733. value = male_value if male else female_value
  734. types[value] = (lambda s=slots, lu=lugs, r=recess: (california(s, male, lu, r), male))
  735. return types
  736. # Outlet type: drawing function
  737. POWER_OUTLETS = {
  738. 'iec-60320-c5': iec_c5_family,
  739. 'iec-60320-c7': iec_c7_family,
  740. 'iec-60320-c13': iec_c13_family,
  741. 'iec-60320-c15': lambda: iec_c13_family(notch=True),
  742. 'iec-60320-c17': lambda: iec_c13_family(earth=False),
  743. 'iec-60320-c19': iec_c19_family,
  744. 'iec-60320-c21': lambda: iec_c19_family(chamfer=True),
  745. **iec_60309_types(),
  746. 'iec-60906-1': lambda: iec_60906(2.4),
  747. 'nbr-14136-10a': lambda: iec_60906(2.2),
  748. 'nbr-14136-20a': lambda: iec_60906(2.6),
  749. **nema_types(),
  750. 'ita-e': ita_e_socket,
  751. 'ita-f': ita_f_socket,
  752. 'ita-g': ita_g,
  753. 'ita-h': ita_h,
  754. 'ita-i': ita_i,
  755. 'ita-j': ita_j,
  756. 'ita-k': ita_k,
  757. 'ita-l': ita_l_socket,
  758. 'ita-m': ita_m,
  759. 'ita-n': lambda: iec_60906(2.4),
  760. 'ita-o': ita_o,
  761. 'usb-a': usb_a,
  762. 'usb-micro-b': usb_micro_b,
  763. 'usb-c': usb_c,
  764. 'molex-micro-fit-1x2': lambda: molex_micro_fit(1, 2),
  765. 'molex-micro-fit-2x2': lambda: molex_micro_fit(2, 2),
  766. 'molex-micro-fit-2x3': lambda: molex_micro_fit(2, 3),
  767. 'molex-micro-fit-2x4': lambda: molex_micro_fit(2, 4),
  768. 'eaton-c39': eaton_c39,
  769. 'hdot-cx': hdot_cx,
  770. 'saf-d-grid': saf_d_grid,
  771. # Power out chassis connectors are the mirror image of power in
  772. 'neutrik-powercon-20a': mirrored(powercon_20a),
  773. 'neutrik-powercon-32a': powercon_32a,
  774. 'neutrik-powercon-true1': powercon_true1_outlet,
  775. 'neutrik-powercon-true1-top': powercon_true1_outlet,
  776. }
  777. # Port type: drawing function. Faces wrapped with mirrored() are drawn as the mirror image of the mating face.
  778. POWER_PORTS = {
  779. # IEC 60320 inlets share the orientation of their mating connector
  780. 'iec-60320-c6': lambda: iec_c5_family(male=True),
  781. 'iec-60320-c8': lambda: iec_c7_family(male=True),
  782. 'iec-60320-c14': lambda: iec_c13_family(male=True),
  783. 'iec-60320-c16': lambda: iec_c13_family(male=True, notch=True),
  784. 'iec-60320-c18': lambda: iec_c13_family(male=True, earth=False),
  785. 'iec-60320-c20': lambda: iec_c19_family(male=True),
  786. 'iec-60320-c22': lambda: iec_c19_family(male=True, chamfer=True),
  787. **iec_60309_types(male=True),
  788. 'iec-60906-1': mirrored(lambda: iec_60906(2.25)),
  789. 'nbr-14136-10a': mirrored(lambda: iec_60906(2.0)),
  790. 'nbr-14136-20a': mirrored(lambda: iec_60906(2.4)),
  791. **nema_types(male=True),
  792. 'ita-c': ita_c_plug,
  793. 'ita-e': lambda: schuko_plug(strips=False, earth_hole=True),
  794. 'ita-f': schuko_plug,
  795. 'ita-ef': lambda: schuko_plug(earth_hole=True),
  796. 'ita-g': mirrored(lambda: ita_g(socket=False)),
  797. 'ita-h': mirrored(lambda: ita_h(socket=False)),
  798. 'ita-i': mirrored(lambda: ita_i(socket=False)),
  799. 'ita-j': mirrored(lambda: ita_j(socket=False)),
  800. 'ita-k': mirrored(lambda: ita_k(socket=False)),
  801. 'ita-l': ita_l_plug,
  802. 'ita-m': mirrored(lambda: ita_m(socket=False)),
  803. 'ita-n': mirrored(lambda: iec_60906(2.25)),
  804. 'ita-o': mirrored(lambda: ita_o(socket=False)),
  805. # USB power ports are receptacles on the device
  806. 'usb-a': usb_a,
  807. 'usb-b': usb_b,
  808. 'usb-c': usb_c,
  809. 'usb-mini-a': usb_mini_a,
  810. 'usb-mini-b': usb_mini_b,
  811. 'usb-micro-a': usb_micro_a,
  812. 'usb-micro-b': usb_micro_b,
  813. 'usb-micro-ab': usb_micro_a,
  814. 'usb-3-b': usb_3_b,
  815. 'usb-3-micro-b': usb_3_micro_b,
  816. 'molex-micro-fit-1x2': mirrored(lambda: molex_micro_fit(1, 2)),
  817. 'molex-micro-fit-2x2': mirrored(lambda: molex_micro_fit(2, 2)),
  818. 'molex-micro-fit-2x3': mirrored(lambda: molex_micro_fit(2, 3)),
  819. 'molex-micro-fit-2x4': mirrored(lambda: molex_micro_fit(2, 4)),
  820. 'saf-d-grid': saf_d_grid,
  821. 'neutrik-powercon-20': powercon_20a,
  822. 'neutrik-powercon-32': powercon_32a,
  823. 'neutrik-powercon-true1': powercon_true1_inlet,
  824. 'neutrik-powercon-true1-top': powercon_true1_inlet,
  825. }
  826. def write(directory, drawings):
  827. directory.mkdir(parents=True, exist_ok=True)
  828. expected = set()
  829. for value, func in drawings.items():
  830. result = func()
  831. drawing, mirror = result if isinstance(result, tuple) else (result, False)
  832. path = directory / f'{value}.svg'
  833. expected.add(path.name)
  834. content = drawing.svg(mirror=mirror)
  835. if not path.exists() or path.read_text() != content:
  836. path.write_text(content)
  837. print(f'Wrote {path}')
  838. # Remove drawings for types no longer defined
  839. for path in directory.glob('*.svg'):
  840. if path.name not in expected:
  841. path.unlink()
  842. print(f'Removed {path}')
  843. def main():
  844. write(OUTPUT_DIR / 'power-outlet', POWER_OUTLETS)
  845. write(OUTPUT_DIR / 'power-port', POWER_PORTS)
  846. if __name__ == '__main__':
  847. main()