diff --git a/boxes/generators/atreus21.py b/boxes/generators/atreus21.py index f4edc8c..50877a8 100644 --- a/boxes/generators/atreus21.py +++ b/boxes/generators/atreus21.py @@ -14,11 +14,12 @@ class Atreus21(Boxes, Keyboard): half_btn = btn_size / 2 border = 6 - row_offsets=[3, 6, 11, 5, 0, btn_size * .5] - row_keys=[4, 4, 4, 4, 4, 1] - def __init__(self): super().__init__() + self.add_common_keyboard_parameters( + # By default, columns from Atreus 21 + default_columns_definition='4@3/4@6/4@11/4@5/4@0/1@{}'.format(self.btn_size * 0.5) + ) def render(self): """Renders the keyboard.""" @@ -85,36 +86,35 @@ class Atreus21(Boxes, Keyboard): self.moveTo(0, b) def half(self, hole_cb=None, reverse=False): - row_offsets=self.row_offsets - row_keys=self.row_keys - scheme = list(zip(row_offsets, row_keys)) if hole_cb == None: hole_cb = self.key self.moveTo(self.half_btn, self.half_btn) self.apply_callback_on_columns( hole_cb, - scheme, + self.columns_definition, self.STANDARD_KEY_SPACING, reverse, ) self.moveTo(-self.half_btn, -self.half_btn) def support(self): - self.outer_hole() + self.configured_plate_cutout(support=True) def hotplug(self): - self.pcb_holes() + self.pcb_holes( + with_hotswap=self.hotswap_enable, + with_pcb_mount=self.pcb_mount_enable, + with_diode=self.diode_enable, + with_led=self.led_enable, + ) def key(self): - self.castle_shaped_plate_cutout() + self.configured_plate_cutout() # get case sizes def _case_x_y(self): - margin = self.STANDARD_KEY_SPACING - self.btn_size - x = len(self.row_offsets) * self.STANDARD_KEY_SPACING - margin - y = sum([ - max(self.row_keys) * self.STANDARD_KEY_SPACING, # total button sizes - max(self.row_offsets), # offset of highest row - -margin, - ]) + spacing = Keyboard.STANDARD_KEY_SPACING + margin = spacing - self.btn_size + x = len(self.columns_definition) * spacing - margin + y = max(offset + keys * spacing for (offset, keys) in self.columns_definition) - margin return x, y diff --git a/boxes/generators/keyboard.py b/boxes/generators/keyboard.py index 3e1eace..b5629d1 100644 --- a/boxes/generators/keyboard.py +++ b/boxes/generators/keyboard.py @@ -15,24 +15,133 @@ # You should have received a copy of the GNU General Public License # along with this program. If not, see . +import re +import argparse +from boxes import Boxes, boolarg class Keyboard: """ Code to manage Cherry MX compatible switches and Kailh hotswap socket. + + Reference : + * https://www.cherrymx.de/en/dev.html + * https://cdn.sparkfun.com/datasheets/Components/Switches/MX%20Series.pdf + * https://www.kailhswitch.com/uploads/201815927/PG151101S11.pdf """ STANDARD_KEY_SPACING = 19 SWITCH_CASE_SIZE = 15.6 + FRAME_CUTOUT = 14 def __init__(self): pass - def pcb_holes(self): + def add_common_keyboard_parameters( + self, + add_hotswap_parameter=True, + add_pcb_mount_parameter=True, + add_led_parameter=True, + add_diode_parameter=True, + add_cutout_type_parameter=True, + default_columns_definition=None, + ): + if add_hotswap_parameter: + self.argparser.add_argument( + "--hotswap_enable", + action="store", + type=boolarg, + default=True, + help=("enlarge switches holes for hotswap pcb sockets"), + ) + if add_pcb_mount_parameter: + self.argparser.add_argument( + "--pcb_mount_enable", + action="store", + type=boolarg, + default=True, + help=("adds holes for pcb mount switches"), + ) + if add_led_parameter: + self.argparser.add_argument( + "--led_enable", + action="store", + type=boolarg, + default=False, + help=("adds pin holes under switches for leds"), + ) + if add_diode_parameter: + self.argparser.add_argument( + "--diode_enable", + action="store", + type=boolarg, + default=False, + help=("adds pin holes under switches for diodes"), + ) + if add_cutout_type_parameter: + self.argparser.add_argument( + "--cutout_type", + action="store", + type=str, + default="castle", + help=( + "Shape of the plate cutout: 'castle' allows for modding, and 'simple' is a tighter and simpler square" + ), + ) + if default_columns_definition: + self.argparser.add_argument( + "--columns_definition", + type=self.argparseColumnsDefinition, + default=default_columns_definition, + help=( + "Each column is separated by '/', and is in the form 'nb_rows @ offset x repeat_count'. " + "Nb_rows is the number of rows for this column. " + "The offset is in mm and optional. " + "Repeat_count is optional and repeats this column multiple times. " + "Spaces are not important." + "For example '3x2 / 4@11' means we want 3 columns, the two first with " + "3 rows without offset, and the last with 4 rows starting at 11mm high." + ), + ) + + def argparseColumnsDefinition(self, s): + """ + Parse columns definition parameter + + :param s: string to parse + + Each column is separated by '/', and is in the form 'nb_rows @ offset x repeat_count'. + Nb_rows is the number of rows for this column. + The offset is in mm and optional. + Repeat_count is optional and repeats this column multiple times. + Spaces are not important. + For example '3x2 / 4@11' means we want 3 columns, the two first with + 3 rows without offset, and the last with 4 rows starting at 11mm high + + """ + result = [] + try: + for column_string in s.split("/"): + m = re.match(r"^\s*(\d+)\s*@?\s*(\d*\.?\d*)(?:\s*x\s*(\d+))?\s*$", column_string) + keys_count = int(m.group(1)) + offset = float(m.group(2)) if m.group(2) else 0 + n = int(m.group(3)) if m.group(3) else 1 + result.extend([(offset, keys_count)]*n) + except: + raise argparse.ArgumentTypeError("Don't understand columns definition string") + + return result + + def pcb_holes( + self, with_hotswap=True, with_pcb_mount=True, with_led=False, with_diode=False + ): grid_unit = 1.27 main_hole_size = 4 pcb_mount_size = 1.7 led_hole_size = 1 - pin_hole_size = 2.9 + if with_hotswap: + pin_hole_size = 2.9 + else: + pin_hole_size = 1.5 def grid_hole(x, y, d): self.hole(grid_unit * x, grid_unit * y, d=d) @@ -44,8 +153,17 @@ class Keyboard: grid_hole(-3, 2, pin_hole_size) grid_hole(2, 4, pin_hole_size) - grid_hole(-4, 0, pcb_mount_size) - grid_hole(4, 0, pcb_mount_size) + if with_pcb_mount: + grid_hole(-4, 0, pcb_mount_size) + grid_hole(4, 0, pcb_mount_size) + + if with_led: + grid_hole(-1, -4, led_hole_size) + grid_hole(1, -4, led_hole_size) + + if with_diode: + grid_hole(-3, -4, led_hole_size) + grid_hole(3, -4, led_hole_size) def apply_callback_on_columns(self, cb, columns_definition, spacing, reverse=False): if reverse: @@ -102,4 +220,64 @@ class Keyboard: self.moveTo(-self.burn-0.81, -0.81) if centered: - self.moveTo(half_size, half_size) \ No newline at end of file + self.moveTo(half_size, half_size) + + def configured_plate_cutout(self, support=False): + """ + Choose which cutout to use based on configured type. + + support: if true, not the main cutout, but one to glue against the first + 1.5mm cutout to strengthen it, without the clipping part. + """ + if self.cutout_type.lower() == "castle": + if support: + self.outer_hole() + else: + self.castle_shaped_plate_cutout() + else: + self.simple_plate_cutout(with_notch=support) + + def simple_plate_cutout(self, radius=0.2, with_notch=False): + """ + A simple plate cutout, a 14mm rectangle, as specified in this reference sheet + https://cdn.sparkfun.com/datasheets/Components/Switches/MX%20Series.pdf + + With_notch shoul be used for a secondary lower plate, strengthening the first one. + A notch is added to let the hooks grasp the main upper plate. + + Current position should be switch center. + + Radius should be lower or equal to 0.3 mm + """ + size = Keyboard.FRAME_CUTOUT + half_size = size / 2 + + if with_notch: + notch_length = 5 + notch_depth = 1 + straight_part = 0.5 * (size - 2 * radius - 2 * notch_depth - notch_length) + + self.moveTo(-half_size + self.burn, 0, 90) + polyline_quarter = [ + half_size - radius, + (-90, radius), + straight_part, + (90, notch_depth / 2), + 0, + (-90, notch_depth / 2), + notch_length / 2, + ] + polyline = ( + polyline_quarter + + [0] + + list(reversed(polyline_quarter)) + + [0] + + polyline_quarter + + [0] + + list(reversed(polyline_quarter)) + ) + self.polyline(*polyline) + self.moveTo(0, 0, -90) + self.moveTo(half_size - self.burn) + else: + self.rectangularHole(0, 0, size, size, r=radius) \ No newline at end of file diff --git a/boxes/generators/keypad.py b/boxes/generators/keypad.py index d16f890..976bc3e 100644 --- a/boxes/generators/keypad.py +++ b/boxes/generators/keypad.py @@ -21,14 +21,6 @@ class Keypad(Boxes, Keyboard): '--h', action='store', type=int, default=30, help='height of the box' ) - self.argparser.add_argument( - '--btn_x', action='store', type=int, default=3, - help='number of buttons per row' - ) - self.argparser.add_argument( - '--btn_y', action='store', type=int, default=4, - help='number of buttons per column' - ) self.argparser.add_argument( '--top1_thickness', action='store', type=float, default=1.5, help=('thickness of the button hold layer, cherry like switches ' @@ -37,19 +29,31 @@ class Keypad(Boxes, Keyboard): self.argparser.add_argument( '--top2_enable', action='store', type=boolarg, default=False, help=('enables another top layer that can hold CPG151101S11 ' - 'sockets') + 'hotswap sockets') ) self.argparser.add_argument( '--top2_thickness', action='store', type=float, default=1.5, - help=('thickness of the hotplug layer, CPG151101S11 sockets ' - 'need 1.2mm to 1.5mm') + help=('thickness of the hotplug layer, CPG151101S11 hotswap ' + 'sockets need 1.2mm to 1.5mm') ) + + # Add parameter common with other keyboard projects + self.add_common_keyboard_parameters( + # Hotswap already depends on top2_enable setting, a second parameter + # for it would be useless + add_hotswap_parameter=False, + # By default, 3 columns of 4 rows + default_columns_definition="4x3" + ) + self.addSettingsArgs(FingerJointSettings, surroundingspaces=1) def _get_x_y(self): """Gets the keypad's size based on the number of buttons.""" - x = self.btn_x * (self.btn_size) + (self.btn_x - 1) * self.space_between_btn + 2*self.box_padding - y = self.btn_y * (self.btn_size) + (self.btn_y - 1) * self.space_between_btn + 2*self.box_padding + spacing = self.btn_size + self.space_between_btn + border = 2*self.box_padding - self.space_between_btn + x = len(self.columns_definition) * spacing + border + y = max(offset + keys * spacing for (offset, keys) in self.columns_definition) + border return x, y def render(self): @@ -91,26 +95,29 @@ class Keypad(Boxes, Keyboard): ) def to_grid_callback(self, inner_callback): - scheme = [(0, self.btn_y)]*self.btn_x def callback(): # move to first key center key_margin = self.box_padding + self.btn_size / 2 self.moveTo(key_margin, key_margin) self.apply_callback_on_columns( - inner_callback, scheme, self.btn_size + self.space_between_btn + inner_callback, self.columns_definition, self.btn_size + self.space_between_btn ) return [callback] def hotplug(self): """Callback for the key stabelizers.""" - self.pcb_holes() + self.pcb_holes( + with_pcb_mount=self.pcb_mount_enable, + with_diode=self.diode_enable, + with_led=self.led_enable, + ) def support_hole(self): - self.outer_hole() + self.configured_plate_cutout(support=True) def key_hole(self): - self.castle_shaped_plate_cutout() + self.configured_plate_cutout() # stolen form electronics-box def wallx_cb(self):