# # This program source code file is part of KiCad, a free EDA CAD application. # # Copyright (C) 2024 Jarrett Rainier # Copyright (C) 2012-2024 KiCad Developers # # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License # as published by the Free Software Foundation; either version 2 # of the License, or (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program; if not, you may find one here: # http://www.gnu.org/licenses/old-licenses/gpl-2.0.html # or you may search the http://www.gnu.org website for the version 2 license, # or you may write to the Free Software Foundation, Inc., # 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA # # Based upon the work in: # Microchip AN2934 Self-Capacitance Sensors # import pcbnew import FootprintWizardBase import pcbnew import math, cmath class ScrollWheelWizard(FootprintWizardBase.FootprintWizard): def GetName(self): """ Return footprint name. This is specific to each footprint class, you need to implement this """ return 'ScrollWheel' def GetDescription(self): """ Return footprint description. This is specific to each footprint class, you need to implement this """ return 'Capacitive ScrollWheel wizard' def GetValue(self): return "ScrollWheel-{od:g}x{id:g}mm".format( od = pcbnew.ToMM(self.pads['outer_diameter']), id = pcbnew.ToMM(self.pads['inner_diameter']) ) def GenerateParameterList(self): self.AddParam("Pads", "steps", self.uInteger, 3, min_value=2) self.AddParam("Pads", "bands", self.uInteger, 4, min_value=2) self.AddParam("Pads", "outer_diameter", self.uMM, 40) self.AddParam("Pads", "inner_diameter", self.uMM, 12) self.AddParam("Pads", "deadzone", self.uMM, 4) self.AddParam("Pads", "corner_radius", self.uMM, 1) self.AddParam("Pads", "clearance", self.uMM, 0.5, min_value=0.5, max_value=1.5) self.AddParam("Pads", "edge_silkscreen", self.uBool, True) self.AddParam("Pads", "full_silkscreen", self.uBool, True) @property def pads(self): return self.parameters['Pads'] def smdCircle(self,radius,pos): arc_angle_deg = 360 centerx = pos[0] centery = pos[1] startptx = centerx startpty = centery+radius self.draw.Arc(centerx, centery, startptx, startpty, pcbnew.EDA_ANGLE( arc_angle_deg, pcbnew.DEGREES_T ) ) def smdArc(self,start,end,pos): pos = complex(pos[0], pos[1]) centerx = pos.real centery = pos.imag start_angle_rad = cmath.phase(start - pos) end_angle_rad = cmath.phase(end - pos) start_angle_deg = math.degrees(start_angle_rad) end_angle_deg = math.degrees(end_angle_rad) arc_angle_deg = (end_angle_deg - start_angle_deg) % 360 self.draw.Arc(centerx, centery, start.real, start.imag, pcbnew.EDA_ANGLE( arc_angle_deg, pcbnew.DEGREES_T ) ) # Generate points along the arc and create a polygon from those points def arc_points(self, start, end, pos, cw=True): pos = complex(pos[0], pos[1]) centerx = pos.real centery = pos.imag if cw == False: start, end = end, start start_angle_rad = cmath.phase(start - pos) end_angle_rad = cmath.phase(end - pos) arc_angle_rad = (end_angle_rad - start_angle_rad) % (2 * math.pi) num_points = 20 shape = pcbnew.SHAPE_LINE_CHAIN() for i in range(num_points - 1): if cw: angle = start_angle_rad + (i + 1) * arc_angle_rad / (num_points - 1) else: angle = start_angle_rad + (num_points - (i + 1)) * arc_angle_rad / (num_points - 1) x = centerx + abs(start - pos) * math.cos(angle) y = centery + abs(start - pos) * math.sin(angle) shape.Append(pcbnew.VECTOR2I(int(x), int(y))) return shape # Find the centre, given two points and a radius def find_arc_center(self, p1, p2, radius, cw=True): x1, y1 = p1.real, p1.imag x2, y2 = p2.real, p2.imag mx, my = (x1 + x2) / 2, (y1 + y2) / 2 d = math.sqrt((x2 - x1)**2 + (y2 - y1)**2) if d > 2 * radius: raise ValueError("Radius {} is too small to pass through both points {}.".format(radius, d)) h = math.sqrt(radius**2 - (d / 2)**2) dx, dy = (y2 - y1) / d, -(x2 - x1) / d if cw: cx, cy = mx - h * dx, my - h * dy else: cx, cy = mx + h * dx, my + h * dy return (cx, cy) # Find the centre, given two points that are at 180 degrees def find_avg_centre(self, p1, p2): pt1_r, pt1_phi = cmath.polar(p1) pt2_r, pt2_phi = cmath.polar(p2) avg_r = (pt1_r + pt2_r) / 2 avg_phi = (pt1_phi + pt2_phi) / 2 centre = cmath.rect(avg_r, avg_phi) return (centre.real, centre.imag) def find_avg_radius(self, p1, p2, cw = True): pt1_r, pt1_phi = cmath.polar(p1) pt2_r, pt2_phi = cmath.polar(p2) avg_r = (pt1_r + pt2_r) / 2 return self.find_arc_center(p1, p2, avg_r, cw) def create_pads(self, outer_diameter, inner_diameter, deadzone, corner_radius, steps, bands, clearance): outer_radius = outer_diameter / 2 inner_radius = inner_diameter / 2 # Angle step for each radial section (each deadzone gap) angle_step = 2 * math.pi / steps # Right side corners, determines tangential OD-ID range inner_corner_r = inner_radius + corner_radius corner_x = deadzone / 2 + corner_radius y_min = cmath.sqrt(inner_corner_r**2 - corner_x**2) outer_corner_r = outer_radius - corner_radius y_max = cmath.sqrt(outer_corner_r**2 - corner_x**2) y_range = y_max - y_min points_right = [] points_left = [] for step in range(steps): angle = step * angle_step points_right.append([]) points_left.append([]) for i in range(bands): # Positions for right side arc corners (on the top/bot edges of the band) corner_y = y_min + (i * y_range / (bands - 1)) corner_r, corner_phi = cmath.polar(complex(corner_x, corner_y)) corner = cmath.rect(corner_r, corner_phi + angle) right = corner.real, -corner.imag p1 = cmath.rect(corner_r + corner_radius, corner_phi + angle) p1 = p1.real - p1.imag * 1j p2 = cmath.rect(corner_r - corner_radius, corner_phi + angle) p2 = p2.real - p2.imag * 1j points_right[step].append([p1, p2, right]) # Positions for left side arc corners (centered vertically within the band) if i < bands - 1: band_min = y_min + (i * y_range / (bands - 1)) band_max = y_min + ((i + 1) * y_range / (bands - 1)) corner_y = band_min + (band_max - band_min) / 2 corner_r, corner_phi = cmath.polar(complex(-1 * corner_x, corner_y)) corner = cmath.rect(corner_r, corner_phi + angle) left = corner.real, -corner.imag p1 = cmath.rect(corner_r - corner_radius, corner_phi + angle) p1 = p1.real - p1.imag * 1j p2 = cmath.rect(corner_r + corner_radius, corner_phi + angle) p2 = p2.real - p2.imag * 1j points_left[step].append([p1, p2, left]) # Format for first and second args, p1 and p2: 012 # 0 = "step" radially around the circle # 1 = "band" tangentially through the circle # 2 = inside or outside point of the arc # Third arg is switch/case statement described below # Fourth arg cw (T) / ccw (F) for arc drawing ordered_pairs = [] # ID edge ordered_pairs.append([points_right[1][0][1], points_right[0][0][1], 0, True]) # Moving tangentially, from ID to OD, on right side for i in range(bands - 1): ordered_pairs.append([points_right[0][i][1], points_right[0][i][0], 1, True]) ordered_pairs.append([points_right[0][i][0], points_left[steps - 1][i][0], 2, True]) ordered_pairs.append([points_left[steps - 1][i][0], points_left[steps - 1][i][1], 1, False]) ordered_pairs.append([points_left[steps - 1][i][1], points_right[0][i + 1][1], 2, False]) # OD edge ordered_pairs.append([points_right[0][bands - 1][1], points_right[0][bands - 1][0], 1, True]) ordered_pairs.append([points_right[0][bands - 1][0], points_right[1][bands - 1][0], 0, False]) for i in range(bands - 1): a = bands - (i + 1) ordered_pairs.append([points_right[1][a][0], points_right[1][a][1], 1, False]) ordered_pairs.append([points_right[1][a][1], points_left[0][a - 1][1], 2, True]) ordered_pairs.append([points_left[0][a - 1][1], points_left[0][a - 1][0], 1, True]) ordered_pairs.append([points_left[0][a - 1][0], points_right[1][a - 1][0], 2, False]) ordered_pairs.append([points_right[1][a - 1][0], points_right[1][a - 1][1], 1, False]) shape = pcbnew.SHAPE_LINE_CHAIN() for i in range(len(ordered_pairs)): step = ordered_pairs[i] centre = (step[0].real, step[0].imag) # Switch case: # centre = 0, # between the two points (ie. corners), or # perpendicular and average of the radius (ie. "spikes") if ordered_pairs[i][2] == 0: centre = (0,0) elif ordered_pairs[i][2] == 1: centre = self.find_avg_centre(ordered_pairs[i][0], ordered_pairs[i][1]) elif ordered_pairs[i][2] == 2: centre = self.find_avg_radius(ordered_pairs[i][0], ordered_pairs[i][1], ordered_pairs[i][3]) shape.Append(self.arc_points(ordered_pairs[i][0], ordered_pairs[i][1], centre, ordered_pairs[i][3])) offset = int((outer_radius - inner_radius) / 2 + inner_radius) pad = pcbnew.PAD(self.module) pad.SetShape(pcbnew.PAD_SHAPE_CUSTOM) pad.SetAttribute(pcbnew.PAD_ATTRIB_SMD) pad.SetSize(pcbnew.VECTOR2I(deadzone // 2, deadzone // 2)) pad.SetPosition(pcbnew.VECTOR2I(0, -offset)) fcuSet = pcbnew.LSET() fcuSet.AddLayer(pcbnew.F_Cu) pad.SetLayerSet(fcuSet) poly = pcbnew.SHAPE_POLY_SET(shape) poly.Deflate(int(clearance / 2), pcbnew.CORNER_STRATEGY_ROUND_ALL_CORNERS, int(clearance / 10)) poly.Move((pcbnew.VECTOR2I(0, offset))) pad.AddPrimitive(poly, 0) for i in range(steps): angle_step = (i * 2 * math.pi / steps) pos = cmath.rect(offset, angle_step + (math.pi / 2)) step_pad = pad.ClonePad() step_pad.SetName(str(i + 1)) step_pad.SetOrientation(pcbnew.EDA_ANGLE(angle_step, pcbnew.RADIANS_T )) step_pad.SetPosition(pcbnew.VECTOR2I(int(pos.real), int(-pos.imag))) self.module.Add(step_pad) def draw_silkscreen_arcs(self, outer_diameter, inner_diameter, deadzone, corner_radius, steps, bands): outer_radius = outer_diameter / 2 inner_radius = inner_diameter / 2 # Angle step for each radial section (each deadzone gap) angle_step = 2 * math.pi / steps # Right side inner_corner_r = inner_radius + corner_radius corner_x = deadzone / 2 + corner_radius y_min = cmath.sqrt(inner_corner_r**2 - corner_x**2) outer_corner_r = outer_radius - corner_radius y_max = cmath.sqrt(outer_corner_r**2 - corner_x**2) y_range = y_max - y_min outer_spikes = [] inner_spikes = [] right_corners = [] right_corners_ccw = [] left_corners = [] for step in range(steps): angle = step * angle_step outer_spikes.append([]) inner_spikes.append([]) right_corners.append([]) right_corners_ccw.append([]) left_corners.append([]) for i in range(bands): # Positions for right side arc corners (on the top/bot edges of the band) corner_y = y_min + (i * y_range / (bands - 1)) corner_r, corner_phi = cmath.polar(complex(corner_x, corner_y)) corner = cmath.rect(corner_r, corner_phi + angle) right = corner.real, -corner.imag outer_spike_corner_r = cmath.rect(corner_r + corner_radius, corner_phi + angle) outer_spike_corner_r = outer_spike_corner_r.real - outer_spike_corner_r.imag * 1j inner_spike_corner_r = cmath.rect(corner_r - corner_radius, corner_phi + angle) inner_spike_corner_r = inner_spike_corner_r.real - inner_spike_corner_r.imag * 1j self.smdArc(inner_spike_corner_r, outer_spike_corner_r,right) right_corners[step].append(self.arc_points(inner_spike_corner_r, outer_spike_corner_r,right)) right_corners_ccw[step].append(self.arc_points(inner_spike_corner_r, outer_spike_corner_r,right, False)) # Positions for left side arc corners (centered vertically within the band) if i < bands: band_min = y_min + (i * y_range / (bands - 1)) band_max = y_min + ((i + 1) * y_range / (bands - 1)) corner_y = band_min + (band_max - band_min) / 2 corner_r, corner_phi = cmath.polar(complex(-1 * corner_x, corner_y)) corner = cmath.rect(corner_r, corner_phi + angle) left = corner.real, -corner.imag outer_spike_corner_l = cmath.rect(corner_r - corner_radius, corner_phi + angle) outer_spike_corner_l = outer_spike_corner_l.real - outer_spike_corner_l.imag * 1j outer_spikes[step].append([outer_spike_corner_r, outer_spike_corner_l]) # if i > 0: inner_spike_corner_l = cmath.rect(corner_r + corner_radius, corner_phi + angle) inner_spike_corner_l = inner_spike_corner_l.real - inner_spike_corner_l.imag * 1j inner_spikes[step].append([inner_spike_corner_r, inner_spike_corner_l]) if (i + 1) < bands: self.smdArc(inner_spike_corner_l, outer_spike_corner_l,left) left_corners[step].append(self.arc_points(inner_spike_corner_l, outer_spike_corner_l,left, False)) for step in range(steps): id1 = inner_spikes[step][0][0] id2 = inner_spikes[(step + 1) % steps][0][0] shape = self.arc_points(id2, id1,(0,0)) for i in range(bands - 1): self.smdArc(id1, id2,(0,0)) if step == 0: shape.Append(right_corners[step][i]) spike_corner_r = outer_spikes[step][i][0] spike_corner_l = outer_spikes[(step - 1) % steps][i][1] radius_r = abs(spike_corner_r) radius_l = abs(spike_corner_l) radius = (radius_r + radius_l) / 2 spike_centre = self.find_arc_center(spike_corner_r, spike_corner_l, radius, cw=True) self.smdArc(spike_corner_r, spike_corner_l,spike_centre) if step == 0: shape.Append(self.arc_points(spike_corner_r, spike_corner_l,spike_centre, True)) shape.Append(left_corners[(step - 1) % steps][i]) spike_corner_r = inner_spikes[step][i + 1][0] spike_corner_l = inner_spikes[(step - 1) % steps][i][1] radius_r = abs(spike_corner_r) radius_l = abs(spike_corner_l) radius = (radius_r + radius_l) / 2 spike_centre = self.find_arc_center(spike_corner_r, spike_corner_l, radius, cw=True) self.smdArc(spike_corner_r, spike_corner_l,spike_centre) if step == 0: shape.Append(self.arc_points(spike_corner_r, spike_corner_l,spike_centre, False)) shape.Append(right_corners[step][bands - 1]) od2 = outer_spikes[step][bands - 1][0] od1 = outer_spikes[(step + 1) % steps][bands - 1][0] if step == 0: shape.Append(self.arc_points(od1, od2,(0,0), False)) self.smdArc(od1, od2,(0,0)) def CheckParameters(self): od = pcbnew.ToMM(self.parameters['Pads']['outer_diameter']) id = pcbnew.ToMM(self.parameters['Pads']['inner_diameter']) # Diametral height max_height = 20 * 2 min_height = 8 * 2 self.CheckParam('Pads','outer_diameter',max_value=id+max_height,info="Electrode height must be less than 20mm") self.CheckParam('Pads','outer_diameter',min_value=id+min_height,info="Electrode height must be at least 8mm") self.CheckParam('Pads','inner_diameter',min_value=od-max_height,info="Electrode height must be less than 20mm") self.CheckParam('Pads','inner_diameter',max_value=od-min_height,info="Electrode height must be at least 8mm") def BuildThisFootprint(self): param_steps = self.pads["steps"] param_bands = self.pads["bands"] param_od = self.pads["outer_diameter"] param_id = self.pads["inner_diameter"] param_deadzone = self.pads["deadzone"] param_corner_radius = self.pads["corner_radius"] param_clearance = self.pads["clearance"] param_ss_full = self.pads["full_silkscreen"] param_ss_edge = self.pads["edge_silkscreen"] step_length = float(param_od) / float(param_steps) t_size = self.GetTextSize() w_text = self.draw.GetLineThickness() ypos = param_od/2 + t_size/2 + w_text self.draw.Value(0, -ypos, t_size) ypos += t_size + w_text*2 self.draw.Reference(0, -ypos, t_size) self.module.SetAttributes(pcbnew.FP_SMD) pos = pcbnew.VECTOR2I(0, 0) if param_ss_edge == True and param_ss_full == False: self.smdCircle(param_od / 2,pos) self.smdCircle(param_id / 2,pos) if param_ss_full == True: self.draw_silkscreen_arcs(param_od, param_id, param_deadzone, param_corner_radius, param_steps, param_bands) self.create_pads(param_od, param_id, param_deadzone, param_corner_radius, param_steps, param_bands, param_clearance) ScrollWheelWizard().register()