Coverage for JLC2KiCadLib / footprint / footprint_handlers.py: 91%
265 statements
« prev ^ index » next coverage.py v7.13.1, created at 2026-01-04 22:53 +0100
« prev ^ index » next coverage.py v7.13.1, created at 2026-01-04 22:53 +0100
1import json
2import logging
3import re
4from math import acos, cos, pi, pow, radians, sin, sqrt
6from KicadModTree import (
7 Arc,
8 Circle,
9 Line,
10 Pad,
11 Polygon,
12 RectFill,
13 RectLine,
14 Text,
15 Vector2D,
16)
18from .model3d import get_StepModel, get_WrlModel
20__all__ = [
21 "handlers",
22 "h_TRACK",
23 "h_PAD",
24 "h_ARC",
25 "h_CIRCLE",
26 "h_SOLIDREGION",
27 "h_SVGNODE",
28 "h_VIA",
29 "h_RECT",
30 "h_HOLE",
31 "h_TEXT",
32 "mil2mm",
33 "svg_arc_to_points",
34]
36layer_correspondance = {
37 "1": "F.Cu",
38 "2": "B.Cu",
39 "3": "F.SilkS",
40 "4": "B.Silks",
41 "5": "F.Paste",
42 "6": "B.Paste",
43 "7": "F.Mask",
44 "8": "B.Mask",
45 "10": "Edge.Cuts",
46 "11": "", # EasyEDA "Multilayer"
47 "12": "F.Fab",
48 "99": "", # EasyEDA "Component shape layer"
49 "100": "", # EasyEDA "Pin soldering layer"
50 "101": "", # EasyEDA "Component marking layer"
51}
54def mil2mm(data):
55 return float(data) / 3.937
58def h_TRACK(data, kicad_mod, footprint_info):
59 """
60 Append a line to the footprint
62 data : [
63 0 : width
64 1 : layer
65 2 :
66 3 : points list
67 4 : id
68 ]
69 """
71 width = mil2mm(data[0])
73 points = [mil2mm(p) for p in data[3].split(" ") if p]
75 for i in range(int(len(points) / 2) - 1):
76 start = [points[2 * i], points[2 * i + 1]]
77 end = [points[2 * i + 2], points[2 * i + 3]]
78 try:
79 layer = layer_correspondance[data[1]]
80 except Exception:
81 logging.exception("footprint h_TRACK: layer correspondance not found")
82 layer = "F.SilkS"
84 # update footprint borders
85 footprint_info.max_X = max(footprint_info.max_X, start[0], end[0])
86 footprint_info.min_X = min(footprint_info.min_X, start[0], end[0])
87 footprint_info.max_Y = max(footprint_info.max_Y, start[1], end[1])
88 footprint_info.min_Y = min(footprint_info.min_Y, start[1], end[1])
90 # append line to kicad_mod
91 kicad_mod.append(Line(start=start, end=end, width=width, layer=layer))
94def h_PAD(data, kicad_mod, footprint_info):
95 """
96 Append a pad to the footprint
98 data : [
99 0 : shape type
100 1 : pad position x
101 2 : pad position y
102 3 : pad size x
103 4 : pad size y
104 5 : layer
105 6 :
106 7 : pad number
107 8 : drill size
108 9 : Polygon nodes
109 10 : rotation
110 11 : id
111 12 : drill offset
112 13 :
113 14 : plated
114 15 :
115 16 :
116 17 :
117 18 :
118 ]
119 """
121 # PAD layer definition
122 TOPLAYER = "1"
123 BOTTOMLAYER = "2"
124 MULTILAYER = "11"
126 shape_type = data[0]
127 at = [mil2mm(data[1]), mil2mm(data[2])]
128 size = [mil2mm(data[3]), mil2mm(data[4])]
129 layer = data[5]
130 pad_number = data[6]
132 drill_diameter = float(mil2mm(data[8])) * 2
133 drill_size = drill_diameter
135 rotation = float(data[10])
136 drill_offset = float(mil2mm(data[12])) if data[12] else 0
138 primitives = ""
140 if layer == MULTILAYER:
141 pad_type = Pad.TYPE_THT
142 pad_layer = Pad.LAYERS_THT
143 elif layer == TOPLAYER:
144 pad_type = Pad.TYPE_SMT
145 pad_layer = Pad.LAYERS_SMT
146 elif layer == BOTTOMLAYER:
147 pad_type = Pad.TYPE_SMT
148 pad_layer = ["B.Cu", "B.Mask", "B.Paste"]
149 else:
150 logging.warning(
151 f"footprint, h_PAD: Unrecognized pad layer. Using default SMT layer for "
152 f"pad {pad_number}"
153 )
154 pad_type = Pad.TYPE_SMT
155 pad_layer = Pad.LAYERS_SMT
157 if shape_type == "OVAL":
158 shape = Pad.SHAPE_OVAL
160 if drill_offset == 0:
161 drill_size = drill_diameter
163 elif (drill_diameter < drill_offset) ^ (
164 size[0] > size[1]
165 ): # invert the orientation of the drill hole if not in the same orientation
166 # as the pad shape
167 drill_size = [drill_diameter, drill_offset]
168 else:
169 drill_size = [drill_offset, drill_diameter]
171 elif shape_type == "RECT":
172 shape = Pad.SHAPE_RECT
174 if drill_offset == 0:
175 drill_size = drill_diameter
176 else:
177 drill_size = [drill_diameter, drill_offset]
179 elif shape_type == "ELLIPSE":
180 shape = Pad.SHAPE_CIRCLE
182 elif shape_type == "POLYGON":
183 shape = Pad.SHAPE_CUSTOM
184 points = []
185 for i, coord in enumerate(data[9].split(" ")):
186 points.append(mil2mm(coord) - at[i % 2])
187 primitives = [Polygon(nodes=zip(points[::2], points[1::2], strict=True))]
188 size = [0.1, 0.1]
190 drill_size = 1 if drill_offset == 0 else [drill_diameter, drill_offset]
192 else:
193 logging.error(
194 f"footprint handler, pad : no correspondance found, using default "
195 f"SHAPE_OVAL for pad {pad_number}"
196 )
197 shape = Pad.SHAPE_OVAL
199 # update footprint borders
200 footprint_info.max_X = max(footprint_info.max_X, at[0])
201 footprint_info.min_X = min(footprint_info.min_X, at[0])
202 footprint_info.max_Y = max(footprint_info.max_Y, at[1])
203 footprint_info.min_Y = min(footprint_info.min_Y, at[1])
205 kicad_mod.append(
206 Pad(
207 number=pad_number,
208 type=pad_type,
209 shape=shape,
210 at=at,
211 size=size,
212 rotation=rotation,
213 drill=drill_size,
214 layers=pad_layer,
215 primitives=primitives,
216 )
217 )
220def h_ARC(data, kicad_mod, footprint_info):
221 """
222 append an Arc to the footprint
223 data : [
224 0 : width
225 1 : layer
226 2 :
227 3 : nodes
228 4 :
229 5 : id
230 ]
231 """
233 width = data[0]
234 layer = layer_correspondance[data[1]]
235 svg_path = data[3]
237 # Parse SVG path
238 pattern = (
239 r"M\s*([-\d.]+)[\s,]+([-\d.]+)\s*A\s*([-\d.]+)[\s,]+"
240 r"([-\d.]+)[\s,]+([-\d.]+)[\s,]+(\d)[\s,]+(\d)[\s,]+([-\d.]+)[\s,]+([-\d.]+)"
241 )
243 match = re.search(pattern, svg_path)
245 if not match:
246 logging.error("footprint handler, h_ARC: failed to parse ARC")
247 return
249 # Extract values
250 start_x, start_y = float(match.group(1)), float(match.group(2))
251 rx, ry = float(match.group(3)), float(match.group(4))
252 _ = float(match.group(5)) # rotation ?
253 large_arc_flag = int(match.group(6))
254 sweep_flag = int(match.group(7))
255 end_x, end_y = float(match.group(8)), float(match.group(9))
257 width = mil2mm(width)
258 start_x = mil2mm(start_x)
259 start_y = mil2mm(start_y)
260 radius_x = mil2mm(rx)
261 radius_y = mil2mm(ry)
262 end_x = mil2mm(end_x)
263 end_y = mil2mm(end_y)
265 start = [start_x, start_y]
266 end = [end_x, end_y]
268 # Check if this is a full circle (start == end)
269 if abs(start_x - end_x) < 1e-6 and abs(start_y - end_y) < 1e-6:
270 # Full circle: center is offset from start by radius
271 # Direction depends on sweep_flag
272 radius = radius_x # Assuming circular arc (rx == ry)
273 # For sweep_flag=1 (clockwise in SVG), center is to the right
274 # For sweep_flag=0 (counter-clockwise), center is to the left
275 if sweep_flag == 1:
276 center = [start_x + radius, start_y]
277 else:
278 center = [start_x - radius, start_y]
279 kicad_mod.append(Circle(center=center, radius=radius, width=width, layer=layer))
280 return
282 if sweep_flag == 0:
283 start, end = end, start
285 # find the midpoint of start and end
286 mid = [(start[0] + end[0]) / 2, (start[1] + end[1]) / 2]
287 # create vector from start to mid:
288 vec1 = Vector2D(mid[0] - start[0], mid[1] - start[1])
290 # create vector that's normal to vec1:
291 length_squared = radius_x * radius_y - pow(vec1.distance_to((0, 0)), 2)
292 if length_squared < 0:
293 length_squared = 0
294 large_arc_flag = 1
296 vec2 = vec1.rotate(-90) if large_arc_flag == 1 else vec1.rotate(90)
298 magnitude = sqrt(vec2[0] ** 2 + vec2[1] ** 2)
299 vec2 = Vector2D(vec2[0] / magnitude, vec2[1] / magnitude)
301 # calculate the length from mid to centre using pythagoras:
302 length = sqrt(length_squared)
303 # calculate the centre using mid and vec2 with the correct length:
304 cen = Vector2D(mid) + vec2 * length
306 kicad_mod.append(Arc(start=start, end=end, width=width, center=cen, layer=layer))
309def h_CIRCLE(data, kicad_mod, footprint_info):
310 # append a Circle to the footprint
312 if (
313 data[4] == "100"
314 ): # they want to draw a circle on pads, we don't want that. This is an empirical
315 # deduction, no idea if this is correct, but it seems to work on my tests
316 return ()
318 data[0] = mil2mm(data[0])
319 data[1] = mil2mm(data[1])
320 data[2] = mil2mm(data[2])
321 data[3] = mil2mm(data[3])
323 center = [data[0], data[1]]
324 radius = data[2]
325 width = data[3]
327 try:
328 layer = layer_correspondance[data[4]]
329 except KeyError:
330 logging.exception(
331 "footprint handler, h_CIRCLE : layer correspondance not found"
332 )
333 layer = "F.SilkS"
335 kicad_mod.append(Circle(center=center, radius=radius, width=width, layer=layer))
338def svg_arc_to_points(x1, y1, rx, ry, rotation, large_arc_flag, sweep_flag, x2, y2):
339 """
340 Convert SVG arc to list of points using center parameterization.
341 Uses SVG arc implementation algorithm from W3C spec F.6.5.
343 Args:
344 x1, y1: Start point
345 rx, ry: Ellipse radii
346 rotation: X-axis rotation in degrees
347 large_arc_flag: 0 or 1
348 sweep_flag: 0 or 1
349 x2, y2: End point
351 Returns:
352 List of (x, y) tuples representing points along the arc
353 """
354 # Handle degenerate cases
355 if x1 == x2 and y1 == y2:
356 return []
357 if rx == 0 or ry == 0:
358 return [(x2, y2)]
360 rx = abs(rx)
361 ry = abs(ry)
363 cos_rot = cos(radians(rotation))
364 sin_rot = sin(radians(rotation))
366 # Compute (x1', y1') - rotated coordinates
367 dx = (x1 - x2) / 2
368 dy = (y1 - y2) / 2
369 x1_prime = cos_rot * dx + sin_rot * dy
370 y1_prime = -sin_rot * dx + cos_rot * dy
372 # Compute center (cx', cy')
373 rx_sq = rx * rx
374 ry_sq = ry * ry
375 x1_prime_sq = x1_prime * x1_prime
376 y1_prime_sq = y1_prime * y1_prime
378 # Correct radii if needed (ensure arc is possible)
379 lambda_sq = x1_prime_sq / rx_sq + y1_prime_sq / ry_sq
380 if lambda_sq > 1:
381 scale = sqrt(lambda_sq)
382 rx *= scale
383 ry *= scale
384 rx_sq = rx * rx
385 ry_sq = ry * ry
387 # Calculate center
388 denom = rx_sq * y1_prime_sq + ry_sq * x1_prime_sq
389 if denom == 0:
390 return [(x2, y2)]
392 sign = -1 if large_arc_flag == sweep_flag else 1
393 sq = max(0, (rx_sq * ry_sq - rx_sq * y1_prime_sq - ry_sq * x1_prime_sq) / denom)
394 coef = sign * sqrt(sq)
396 cx_prime = coef * rx * y1_prime / ry
397 cy_prime = -coef * ry * x1_prime / rx
399 # Compute center (cx, cy) in original coordinates
400 cx = cos_rot * cx_prime - sin_rot * cy_prime + (x1 + x2) / 2
401 cy = sin_rot * cx_prime + cos_rot * cy_prime + (y1 + y2) / 2
403 # Calculate start angle and delta angle
404 def angle_between(ux, uy, vx, vy):
405 n = sqrt(ux * ux + uy * uy) * sqrt(vx * vx + vy * vy)
406 if n == 0:
407 return 0
408 c = (ux * vx + uy * vy) / n
409 c = max(-1, min(1, c))
410 angle = acos(c)
411 if ux * vy - uy * vx < 0:
412 angle = -angle
413 return angle
415 theta1 = angle_between(1, 0, (x1_prime - cx_prime) / rx, (y1_prime - cy_prime) / ry)
416 dtheta = angle_between(
417 (x1_prime - cx_prime) / rx,
418 (y1_prime - cy_prime) / ry,
419 (-x1_prime - cx_prime) / rx,
420 (-y1_prime - cy_prime) / ry,
421 )
423 # Adjust delta angle based on sweep flag
424 if sweep_flag == 0 and dtheta > 0:
425 dtheta -= 2 * pi
426 elif sweep_flag == 1 and dtheta < 0:
427 dtheta += 2 * pi
429 # Generate points along the arc (adaptive resolution)
430 num_segments = max(8, int(abs(dtheta) / (2 * pi) * 32))
432 points = []
433 for i in range(1, num_segments + 1): # Skip first point (it's the current position)
434 angle = theta1 + dtheta * i / num_segments
435 x = cx + rx * cos(angle) * cos_rot - ry * sin(angle) * sin_rot
436 y = cy + rx * cos(angle) * sin_rot + ry * sin(angle) * cos_rot
437 points.append((x, y))
439 return points
442def h_SOLIDREGION(data, kicad_mod, footprint_info):
443 layer = "Edge.Cuts" if data[3] == "npth" else layer_correspondance[data[0]]
445 path = data[2]
446 points = []
447 current_pos = (0.0, 0.0)
449 # Parse SVG path
450 command_pattern = re.compile(
451 r"([MLAZ])\s*"
452 r"((?:[-+]?\d*\.?\d+[\s,]*)*)",
453 re.IGNORECASE,
454 )
456 # Pattern to extract numbers
457 number_pattern = re.compile(r"[-+]?\d*\.?\d+")
459 for match in command_pattern.finditer(path):
460 cmd = match.group(1).upper()
461 params_str = match.group(2)
462 params = [float(n) for n in number_pattern.findall(params_str)]
464 if cmd == "M":
465 # Move to: M x y
466 if len(params) >= 2:
467 current_pos = (params[0], params[1])
468 points.append(current_pos)
470 elif cmd == "L":
471 # Line to: L x y
472 if len(params) >= 2:
473 current_pos = (params[0], params[1])
474 points.append(current_pos)
476 elif cmd == "A":
477 # Arc: A rx ry rotation large-arc-flag sweep-flag x y
478 if len(params) >= 7:
479 rx = params[0]
480 ry = params[1]
481 rotation = params[2]
482 large_arc_flag = int(params[3])
483 sweep_flag = int(params[4])
484 end_x = params[5]
485 end_y = params[6]
487 arc_points = svg_arc_to_points(
488 current_pos[0],
489 current_pos[1],
490 rx,
491 ry,
492 rotation,
493 large_arc_flag,
494 sweep_flag,
495 end_x,
496 end_y,
497 )
498 points.extend(arc_points)
499 current_pos = (end_x, end_y)
501 elif cmd == "Z":
502 # Close path - no action needed, polygon will close automatically
503 pass
505 # Convert from mils to mm
506 points = [(mil2mm(p[0]), mil2mm(p[1])) for p in points]
508 if points:
509 kicad_mod.append(Polygon(nodes=points, layer=layer))
512def h_SVGNODE(data, kicad_mod, footprint_info):
513 # create 3D model as a WRL file
514 # parse json data
515 try:
516 data = json.loads(data[0])
517 except Exception:
518 logging.exception("footprint handler, h_SVGNODE : failed to parse json data")
519 return ()
521 c_origin = data["attrs"]["c_origin"].split(",")
522 if "STEP" in footprint_info.models:
523 get_StepModel(
524 component_uuid=data["attrs"]["uuid"],
525 footprint_info=footprint_info,
526 kicad_mod=kicad_mod,
527 translationX=float(c_origin[0]),
528 translationY=float(c_origin[1]),
529 translationZ=data["attrs"]["z"],
530 rotation=data["attrs"]["c_rotation"],
531 )
533 if "WRL" in footprint_info.models:
534 get_WrlModel(
535 component_uuid=data["attrs"]["uuid"],
536 footprint_info=footprint_info,
537 kicad_mod=kicad_mod,
538 translationX=float(c_origin[0]),
539 translationY=float(c_origin[1]),
540 translationZ=data["attrs"]["z"],
541 rotation=data["attrs"]["c_rotation"],
542 )
545def h_VIA(data, kicad_mod, footprint_info):
546 logging.warning(
547 "VIA not supported. Via are often added for better heat dissipation. "
548 "Be careful and read datasheet if needed."
549 )
552def h_RECT(data, kicad_mod, footprint_info):
553 Xstart = float(mil2mm(data[0]))
554 Ystart = float(mil2mm(data[1]))
555 Xdelta = float(mil2mm(data[2]))
556 Ydelta = float(mil2mm(data[3]))
557 start = [Xstart, Ystart]
558 end = [Xstart + Xdelta, Ystart + Ydelta]
559 width = mil2mm(data[7])
561 if width == 0:
562 # filled:
563 kicad_mod.append(
564 RectFill(
565 start=start,
566 end=end,
567 layer=layer_correspondance[data[4]],
568 )
569 )
570 else:
571 # not filled:
572 kicad_mod.append(
573 RectLine(
574 start=start,
575 end=end,
576 width=width,
577 layer=layer_correspondance[data[4]],
578 )
579 )
582def h_HOLE(data, kicad_mod, footprint_info):
583 kicad_mod.append(
584 Pad(
585 number="",
586 type=Pad.TYPE_NPTH,
587 shape=Pad.SHAPE_CIRCLE,
588 at=[mil2mm(data[0]), mil2mm(data[1])],
589 size=mil2mm(data[2]) * 2,
590 rotation=0,
591 drill=mil2mm(data[2]) * 2,
592 layers=Pad.LAYERS_NPTH,
593 )
594 )
597def h_TEXT(data, kicad_mod, footprint_info):
598 kicad_mod.append(
599 Text(
600 type="user",
601 text=data[9],
602 at=[mil2mm(data[1]), mil2mm(data[2])],
603 layer="F.SilkS",
604 )
605 )
608handlers = {
609 "TRACK": h_TRACK,
610 "PAD": h_PAD,
611 "ARC": h_ARC,
612 "CIRCLE": h_CIRCLE,
613 "SOLIDREGION": h_SOLIDREGION,
614 "SVGNODE": h_SVGNODE,
615 "VIA": h_VIA,
616 "RECT": h_RECT,
617 "HOLE": h_HOLE,
618 "TEXT": h_TEXT,
619}