Coverage for JLC2KiCadLib/footprint/footprint_handlers.py: 91%

1 import json

2 import logging

3 import re

4 from math import acos, cos, pi, pow, radians, sin, sqrt

5

6 from KicadModTree import (

7 Arc,

8 Circle,

9 Line,

10 Pad,

11 Polygon,

12 RectFill,

13 RectLine,

14 Text,

15 Vector2D,

16)

17

18 from .model3d import get_StepModel, get_WrlModel

19

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]

35

36 layer_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}

52

53

54 def mil2mm(data):

55 return float(data) / 3.937

56

57

58 def h_TRACK(data, kicad_mod, footprint_info):

59 """

60 Append a line to the footprint

61

62 data : [

63 0 : width

64 1 : layer

65 2 :

66 3 : points list

67 4 : id

68 ]

69 """

70

71 width = mil2mm(data[0])

72

73 points = [mil2mm(p) for p in data[3].split(" ") if p]

74

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"

83

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])

89

90 # append line to kicad_mod

91 kicad_mod.append(Line(start=start, end=end, width=width, layer=layer))

92

93

94 def h_PAD(data, kicad_mod, footprint_info):

95 """

96 Append a pad to the footprint

97

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 """

120

121 # PAD layer definition

122 TOPLAYER = "1"

123 BOTTOMLAYER = "2"

124 MULTILAYER = "11"

125

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]

131

132 drill_diameter = float(mil2mm(data[8])) * 2

133 drill_size = drill_diameter

134

135 rotation = float(data[10])

136 drill_offset = float(mil2mm(data[12])) if data[12] else 0

137

138 primitives = ""

139

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

156

157 if shape_type == "OVAL":

158 shape = Pad.SHAPE_OVAL

159

160 if drill_offset == 0:

161 drill_size = drill_diameter

162

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]

170

171 elif shape_type == "RECT":

172 shape = Pad.SHAPE_RECT

173

174 if drill_offset == 0:

175 drill_size = drill_diameter

176 else:

177 drill_size = [drill_diameter, drill_offset]

178

179 elif shape_type == "ELLIPSE":

180 shape = Pad.SHAPE_CIRCLE

181

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]

189

190 drill_size = 1 if drill_offset == 0 else [drill_diameter, drill_offset]

191

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

198

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])

204

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 )

218

219

220 def 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 """

232

233 width = data[0]

234 layer = layer_correspondance[data[1]]

235 svg_path = data[3]

236

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 )

242

243 match = re.search(pattern, svg_path)

244

245 if not match:

246 logging.error("footprint handler, h_ARC: failed to parse ARC")

247 return

248

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))

256

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)

264

265 start = [start_x, start_y]

266 end = [end_x, end_y]

267

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

281

282 if sweep_flag == 0:

283 start, end = end, start

284

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])

289

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

295

296 vec2 = vec1.rotate(-90) if large_arc_flag == 1 else vec1.rotate(90)

297

298 magnitude = sqrt(vec2[0] ** 2 + vec2[1] ** 2)

299 vec2 = Vector2D(vec2[0] / magnitude, vec2[1] / magnitude)

300

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

305

306 kicad_mod.append(Arc(start=start, end=end, width=width, center=cen, layer=layer))

307

308

309 def h_CIRCLE(data, kicad_mod, footprint_info):

310 # append a Circle to the footprint

311

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 ()

317

318 data[0] = mil2mm(data[0])

319 data[1] = mil2mm(data[1])

320 data[2] = mil2mm(data[2])

321 data[3] = mil2mm(data[3])

322

323 center = [data[0], data[1]]

324 radius = data[2]

325 width = data[3]

326

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"

334

335 kicad_mod.append(Circle(center=center, radius=radius, width=width, layer=layer))

336

337

338 def 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.

342

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

350

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)]

359

360 rx = abs(rx)

361 ry = abs(ry)

362

363 cos_rot = cos(radians(rotation))

364 sin_rot = sin(radians(rotation))

365

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

371

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

377

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

386

387 # Calculate center

388 denom = rx_sq * y1_prime_sq + ry_sq * x1_prime_sq

389 if denom == 0:

390 return [(x2, y2)]

391

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)

395

396 cx_prime = coef * rx * y1_prime / ry

397 cy_prime = -coef * ry * x1_prime / rx

398

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

402

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

414

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 )

422

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

428

429 # Generate points along the arc (adaptive resolution)

430 num_segments = max(8, int(abs(dtheta) / (2 * pi) * 32))

431

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))

438

439 return points

440

441

442 def h_SOLIDREGION(data, kicad_mod, footprint_info):

443 layer = "Edge.Cuts" if data[3] == "npth" else layer_correspondance[data[0]]

444

445 path = data[2]

446 points = []

447 current_pos = (0.0, 0.0)

448

449 # Parse SVG path

450 command_pattern = re.compile(

451 r"([MLAZ])\s*"

452 r"((?:[-+]?\d*\.?\d+[\s,]*)*)",

453 re.IGNORECASE,

454 )

455

456 # Pattern to extract numbers

457 number_pattern = re.compile(r"[-+]?\d*\.?\d+")

458

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)]

463

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)

469

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)

475

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]

486

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)

500

501 elif cmd == "Z":

502 # Close path - no action needed, polygon will close automatically

503 pass

504

505 # Convert from mils to mm

506 points = [(mil2mm(p[0]), mil2mm(p[1])) for p in points]

507

508 if points:

509 kicad_mod.append(Polygon(nodes=points, layer=layer))

510

511

512 def 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 ()

520

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 )

532

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 )

543

544

545 def 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 )

550

551

552 def 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])

560

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 )

580

581

582 def 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 )

595

596

597 def 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 )

606

607

608 handlers = {

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}