diff --git a/pcbnew/python/plugins/scrollwheel_wizard.py b/pcbnew/python/plugins/scrollwheel_wizard.py new file mode 100644 index 0000000000..33a56508e5 --- /dev/null +++ b/pcbnew/python/plugins/scrollwheel_wizard.py @@ -0,0 +1,419 @@ +# +# 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])) + + + + pad = pcbnew.PAD(self.module) + pad.SetShape(pcbnew.PAD_SHAPE_CUSTOM) + pad.SetAttribute(pcbnew.PAD_ATTRIB_SMD) + pad.SetSize(pcbnew.VECTOR2I(1, 1)) + 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)) + pad.AddPrimitive(poly, 0) + + for i in range(steps): + step_pad = pad.ClonePad() + step_pad.SetName(str(i + 1)) + step_pad.SetOrientation( pcbnew.EDA_ANGLE( i * (360 / steps), pcbnew.DEGREES_T )) + 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()