Coverage for JLC2KiCadLib / symbol / symbol_handlers.py: 97%
156 statements
« prev ^ index » next coverage.py v7.13.1, created at 2026-01-04 22:31 +0100
« prev ^ index » next coverage.py v7.13.1, created at 2026-01-04 22:31 +0100
1import logging
2import math
3import re
5RELATIVE_OFFSET = 0.254
6ABSOLUTE_OFFSET_X = 101.6
7ABSOLUTE_OFFSET_Y = -63.5
9__all__ = ["handlers", "h_R", "h_E", "h_P", "h_T", "h_PL", "h_PG", "h_PT", "h_A"]
12def mil2mm(data):
13 return float(data) / 3.937
16def h_R(data, translation, kicad_symbol):
17 """
18 Rectangle handler
19 data = {
20 0 : x1
21 1 : y1
22 2 :
23 3 :
24 4 : width
25 5 : length
26 6 : stroke color
27 7 : ?
28 8 : stroke style : 0 = solid, 1 = dashed, 2 = dotted
29 9 : fill color
30 10 : id
31 11 : locked
32 }
33 """
35 x1 = float(data[0])
36 y1 = float(data[1])
37 width = float(data[4])
38 length = float(data[5])
40 x2 = x1 + width
41 y2 = y1 + length
43 x1_mm = mil2mm(x1 - translation[0])
44 y1_mm = -mil2mm(y1 - translation[1])
45 x2_mm = mil2mm(x2 - translation[0])
46 y2_mm = -mil2mm(y2 - translation[1])
48 if data[8] == 1:
49 stroke_style = "dash"
50 elif data[8] == 2:
51 stroke_style = "dot"
52 else:
53 stroke_style = "default"
55 kicad_symbol.drawing += f"""
56 (rectangle
57 (start {x1_mm} {y1_mm})
58 (end {x2_mm} {y2_mm})
59 (stroke (width 0) (type {stroke_style}) (color 0 0 0 0))
60 (fill (type background))
61 )"""
64def h_E(data, translation, kicad_symbol):
65 """
66 Circle
67 """
69 x1 = mil2mm(float(data[0]) - translation[0])
70 y1 = -mil2mm(float(data[1]) - translation[1])
71 radius = mil2mm(float(data[2]))
73 kicad_symbol.drawing += f"""
74 (circle
75 (center {x1} {y1})
76 (radius {radius})
77 (stroke (width 0) (type default) (color 0 0 0 0))
78 (fill (type background))
79 )"""
82def h_P(data, translation, kicad_symbol):
83 """
84 Add Pin to the symbol
85 data = [
86 0 :
87 1 : electrical type
88 2 : pin number
89 3 : x1
90 4 : y1
91 5 : rotation
92 6 : id
93 7 :
94 8 :
95 9 :
96 10 :
97 11 :
98 12 :
99 13 :
100 14 :
101 15 :
102 16 :
103 17 : name size
104 18 :
105 19 :
106 20 :
107 21 :
108 22 :
109 23 :
110 24 : number size
111 25 :
112 ]
113 """
115 if data[1] == "0":
116 electrical_type = "unspecified"
117 elif data[1] == "1":
118 electrical_type = "input"
119 elif data[1] == "2":
120 electrical_type = "output"
121 elif data[1] == "3":
122 electrical_type = "bidirectional"
123 elif data[1] == "4":
124 electrical_type = "power_in"
125 else:
126 electrical_type = "unspecified"
128 pin_number = data[2]
129 pin_name = data[13]
131 x1 = round(mil2mm(float(data[3]) - translation[0]), 3)
132 y1 = round(-mil2mm(float(data[4]) - translation[1]), 3)
134 rotation = (int(data[5]) + 180) % 360 if data[5] else 180
136 if rotation == 0 or rotation == 180:
137 length = round(mil2mm(abs(float(data[8].split("h")[-1]))), 3)
138 elif rotation == 90 or rotation == 270:
139 length = mil2mm(abs(float(data[8].split("v")[-1])))
140 else:
141 length = 2.54
142 logging.warning(
143 f'symbol : pin number {pin_number} : "{pin_name}" failed to find length.'
144 "Using Default length"
145 )
147 if data[9].split("^^")[1] != "0":
148 kicad_symbol.pinNamesHide = ""
149 if data[17].split("^^")[1] != "0":
150 kicad_symbol.pinNumbersHide = ""
152 name_size = mil2mm(float(data[16].replace("pt", ""))) if data[16] else 1
153 number_size = mil2mm(float(data[24].replace("pt", ""))) if data[24] else 1
155 kicad_symbol.drawing += f"""
156 (pin {electrical_type} line
157 (at {x1} {y1} {rotation})
158 (length {length})
159 (name "{pin_name}" (effects (font (size {name_size} {name_size}))))
160 (number "{pin_number}" (effects (font (size {number_size} {number_size}))))
161 )"""
164def h_T(data, translation, kicad_symbol):
165 """
166 Text handler
167 data = [
168 0 :
169 1 : x1
170 2 : y1
171 3 : rotation
172 4 : color
173 5 : font
174 6 : font size
175 7 :
176 8 :
177 9 :
178 10 :
179 11 : text
180 12 :
181 13 : anchor
182 ]
183 """
185 x1 = mil2mm(float(data[1]) - translation[0])
186 y1 = -mil2mm(float(data[2]) - translation[1])
188 # From https://dev-docs.kicad.org/en/file-formats/sexpr-intro/index.html#_position_identifier
189 # Symbol text ANGLEs are stored in tenth’s of a degree. All other ANGLEs are stored
190 # in degrees.
191 rotation = ((int(data[3]) + 180) % 360) * 10
193 font_size = mil2mm(float(data[6].replace("pt", ""))) if data[6] else 15
195 text = data[11]
197 if data[13] == "middle":
198 justify = "left"
199 elif data[13] == "end":
200 justify = "right"
201 else:
202 justify = "left"
204 kicad_symbol.drawing += f"""
205 (text
206 "{text}"
207 (at {x1} {y1} {rotation})
208 (effects
209 (font (size {font_size} {font_size}))
210 (justify {justify} bottom)
211 )
212 )"""
215def h_PL(data, translation, kicad_symbol):
216 """
217 Polygone handler
218 """
220 path_string = data[0].split(" ")
221 polypts = []
222 for i, _ in enumerate(path_string[::2]):
223 polypts.append(
224 f"(xy {mil2mm(float(path_string[2 * i]) - translation[0])} "
225 f"{-mil2mm(float(path_string[2 * i + 1]) - translation[-1])})"
226 )
227 polystr = "\n ".join(polypts)
229 kicad_symbol.drawing += f"""
230 (polyline
231 (pts
232 {polystr}
233 )
234 (stroke (width 0) (type default) (color 0 0 0 0))
235 (fill (type none))
236 )"""
239def h_PG(data, translation, kicad_symbol):
240 """
241 Closed polygone handler
242 """
244 path_string = [i for i in data[0].split(" ") if i]
245 polypts = []
246 for i, _ in enumerate(path_string[::2]):
247 polypts.append(
248 f"(xy {mil2mm(float(path_string[2 * i]) - translation[0])} "
249 f"{-mil2mm(float(path_string[2 * i + 1]) - translation[1])})"
250 )
251 polypts.append(polypts[0])
252 polystr = "\n ".join(polypts)
254 kicad_symbol.drawing += f"""
255 (polyline
256 (pts
257 {polystr}
258 )
259 (stroke (width 0) (type default) (color 0 0 0 0))
260 (fill (type background))
261 )"""
264def h_PT(data, translation, kicad_symbol):
265 """
266 Triangle handler
267 """
269 data[0] = (
270 data[0].replace("M", "").replace("L", "").replace("Z", "").replace("C", "")
271 )
272 h_PG(data, translation, kicad_symbol)
275def h_A(data, translation, kicad_symbol):
276 """
277 Arc handler
278 """
280 # Parse SVG path: "M x1 y1 A rx ry rotation large-arc sweep x2 y2"
281 path = data[0].strip()
283 # Split into M and A commands
284 parts = re.split(r"[MA]", path)
285 parts = [p.strip() for p in parts if p.strip()]
287 # Parse M command (start point)
288 start_coords = re.split(r"[\s,]+", parts[0])
289 x1 = float(start_coords[0])
290 y1 = float(start_coords[1])
292 # Parse A command (arc parameters)
293 arc_params = re.split(r"[\s,]+", parts[1])
294 rx = float(arc_params[0])
295 ry = float(arc_params[1])
296 rotation = float(arc_params[2])
297 large_arc_flag = int(arc_params[3])
298 sweep_flag = int(arc_params[4])
299 x2 = float(arc_params[5])
300 y2 = float(arc_params[6])
302 cos_rot = math.cos(math.radians(rotation))
303 sin_rot = math.sin(math.radians(rotation))
305 # Step 1: Compute (x1', y1')
306 dx = (x1 - x2) / 2
307 dy = (y1 - y2) / 2
308 x1_prime = cos_rot * dx + sin_rot * dy
309 y1_prime = -sin_rot * dx + cos_rot * dy
311 # Step 2: Compute center (cx', cy')
312 rx_sq = rx * rx
313 ry_sq = ry * ry
314 x1_prime_sq = x1_prime * x1_prime
315 y1_prime_sq = y1_prime * y1_prime
317 # Correct radii if needed
318 lambda_sq = x1_prime_sq / rx_sq + y1_prime_sq / ry_sq
319 if lambda_sq > 1:
320 rx *= math.sqrt(lambda_sq)
321 ry *= math.sqrt(lambda_sq)
322 rx_sq = rx * rx
323 ry_sq = ry * ry
325 sign = -1 if large_arc_flag == sweep_flag else 1
327 if (rx_sq * y1_prime_sq + ry_sq * x1_prime_sq) == 0:
328 return
330 sq = max(
331 0,
332 (rx_sq * ry_sq - rx_sq * y1_prime_sq - ry_sq * x1_prime_sq)
333 / (rx_sq * y1_prime_sq + ry_sq * x1_prime_sq),
334 )
335 coef = sign * math.sqrt(sq)
337 cx_prime = coef * rx * y1_prime / ry
338 cy_prime = -coef * ry * x1_prime / rx
340 # Step 3: Compute center (cx, cy)
341 cx = cos_rot * cx_prime - sin_rot * cy_prime + (x1 + x2) / 2
342 cy = sin_rot * cx_prime + cos_rot * cy_prime + (y1 + y2) / 2
344 # Calculate angles for finding midpoint
345 def angle_between(ux, uy, vx, vy):
346 n = math.sqrt(ux * ux + uy * uy) * math.sqrt(vx * vx + vy * vy)
347 c = (ux * vx + uy * vy) / n
348 c = max(-1, min(1, c)) # Clamp to [-1, 1]
349 angle = math.acos(c)
350 if ux * vy - uy * vx < 0:
351 angle = -angle
352 return angle
354 theta1 = angle_between(1, 0, (x1_prime - cx_prime) / rx, (y1_prime - cy_prime) / ry)
355 dtheta = angle_between(
356 (x1_prime - cx_prime) / rx,
357 (y1_prime - cy_prime) / ry,
358 (-x1_prime - cx_prime) / rx,
359 (-y1_prime - cy_prime) / ry,
360 )
362 if sweep_flag == 0 and dtheta > 0:
363 dtheta -= 2 * math.pi
364 elif sweep_flag == 1 and dtheta < 0:
365 dtheta += 2 * math.pi
367 # Calculate midpoint angle
368 mid_angle = theta1 + dtheta / 2
370 # Calculate midpoint coordinates
371 x_mid = cx + rx * math.cos(mid_angle) * cos_rot - ry * math.sin(mid_angle) * sin_rot
372 y_mid = cy + rx * math.cos(mid_angle) * sin_rot + ry * math.sin(mid_angle) * cos_rot
374 # Convert to KiCad coordinates (mil to mm and apply translation)
375 x1_mm = mil2mm(x1 - translation[0])
376 y1_mm = -mil2mm(y1 - translation[1])
377 x2_mm = mil2mm(x2 - translation[0])
378 y2_mm = -mil2mm(y2 - translation[1])
379 x_mid_mm = mil2mm(x_mid - translation[0])
380 y_mid_mm = -mil2mm(y_mid - translation[1])
382 kicad_symbol.drawing += f"""
383 (arc
384 (start {x1_mm} {y1_mm})
385 (mid {x_mid_mm} {y_mid_mm})
386 (end {x2_mm} {y2_mm})
387 (stroke (width 0) (type default) (color 0 0 0 0))
388 (fill (type none))
389 )"""
392handlers = {
393 "R": h_R,
394 "E": h_E,
395 "P": h_P,
396 "T": h_T,
397 "PL": h_PL,
398 "PG": h_PG,
399 "PT": h_PT,
400 "A": h_A,
401 # "J" : h_NotYetImplemented,
402 # "N" : h_NotYetImplemented,
403 # "BE" : h_NotYetImplemented,
404 # "AR" : h_NotYetImplemented,
405 # "O" : h_NotYetImplemented,
406}