diff --git a/mkbd65/config.ini b/mkbd65/config.ini new file mode 100644 index 0000000..4444281 --- /dev/null +++ b/mkbd65/config.ini @@ -0,0 +1,14 @@ +[oneplus_8t] +phone_width=160.7 +phone_height=74.1 +phone_thickness=8.4 +phone_corner_radius=10 +phone_side_radius=5 +key_mat_margin=1 +key_mat_padding=1 +key_mat_thickness=0.5 +key_mat_key_width=9 +key_mat_key_height=6 +key_mat_key_corner_radius=1 +key_mat_key_thickness=2 +key_mat_font=DejaVu Sans:style=Bold \ No newline at end of file diff --git a/mkbd65/key_mat.py b/mkbd65/key_mat.py new file mode 100644 index 0000000..2df8061 --- /dev/null +++ b/mkbd65/key_mat.py @@ -0,0 +1,154 @@ +""" +All sizes are in millimeters. +""" +from typing import Tuple + +from solid import OpenSCADObject, cube, linear_extrude, text, translate + +from util import round_rect, get_label_size, FILAMENT_LAYER, LAYOUTS + + +def key( + width: float, + height: float, + corner_radius: float, + thickness: float, + font: str, + label: str = "" +) -> OpenSCADObject: + """ + Generates a single key + + key: + +-----+ + | | height + +-----+ + width \\ thickness + + :param width: Width in mm + :param height: Height in mm + :param corner_radius: Radius of corners in mm + :param thickness: Thickness in mm + :param label: Text label + :param font: Font to use + :return: + """ + return linear_extrude(height=thickness)( + round_rect(width=width, height=height, radius=corner_radius) + ) - translate(v=(width / 2, height / 2, thickness - 2 * FILAMENT_LAYER))( + linear_extrude(height=2 * FILAMENT_LAYER)( + text(text=label, size=get_label_size(label), font=font, halign='center', valign='center') + ) + ) + + +def key_mat( + margin: float | Tuple[float, float, float, float], + padding: float | Tuple[float, float], + thickness: float, + key_width: float, + key_height: float, + key_radius: float, + key_thickness: float, + layout: str, + font: str +) -> OpenSCADObject: + """ + Generates elastic mat with fixed layout but variable sizes + + mat: + +---------------------------------------------+ + | margin | + | +-----+ +-----+ +-----+ | + |margin| |padding| | ... | |margin| + | +-----+ +-----+ +-----+ | + | padding | + | +-----+ +-----+ +-----+ | + | | | | | ... | | | + | +-----+ +-----+ +-----+ | + | ... ... ... | + | +-----+ +-----+ +-----+ | + | | | | | ... | | | + | +-----+ +-----+ +-----+ | + | margin | + +---------------------------------------------+ + \\ thickness + + :param margin: left, right, top and bottom margin, can be single number to set all at once. + :param padding: Horizontal and vertical padding, can be single number to set both at once. + :param thickness: Thickness of matt. + :param key_width: Width of keys. + :param key_height: Height of keys. + :param key_radius: Radius of key corners. + :param key_thickness: Thickness of keys without matt. + :param layout: ansi or iso layout. + :param font: + :return: + """ + keys_h = 13 + keys_v = 5 + spacebar_start = 4 # horizontal start key of space bar (0 index) + spacebar_width = 3 # space bar length in keys + + margin_l = margin[0] if isinstance(margin, Tuple) else margin + margin_r = margin[1] if isinstance(margin, Tuple) else margin + margin_t = margin[2] if isinstance(margin, Tuple) else margin + margin_b = margin[3] if isinstance(margin, Tuple) else margin + + padding_h = padding[0] if isinstance(padding, Tuple) else padding + padding_v = padding[1] if isinstance(padding, Tuple) else padding + + mat = cube([ + keys_h * key_width + (keys_h - 1) * padding_h + margin_l + margin_r, + keys_v * key_height + (keys_v - 1) * padding_v + margin_t + margin_b, + thickness + ]) + + for x in range(keys_h): + for y in range(keys_v): + if (x == keys_h - 1 and y in [0, 2, 3]) \ + or (spacebar_start < x < spacebar_start + spacebar_width and y == keys_v - 1): + # skip for wide keys (backspace, enter, shift and space bar) + continue + elif x == keys_h - 1 and y == 1 and layout == "iso": + # big enter + mat += translate(v=(x, y, thickness))(key( + width=key_width, + height=2 * key_height + padding_v, + corner_radius=key_radius, + thickness=key_thickness, + font=font, + label=LAYOUTS[layout][y][x] + )) + elif x == keys_h - 2 and (y in [0, 3]) or (y == 2 and layout == "ansi"): + # wide key + mat += translate(v=(x, y, thickness))(key( + width=2 * key_width + padding_h, + height=key_height, + corner_radius=key_radius, + thickness=key_thickness, + font=font, + label=LAYOUTS[layout][y][x] + )) + elif x == spacebar_start and y == keys_v - 1: + # space bar + mat += translate(v=(x, y, thickness))(key( + width=spacebar_width * key_width + (spacebar_width - 1) * padding_h, + height=key_height, + corner_radius=key_radius, + thickness=key_thickness, + font=font, + label=LAYOUTS[layout][y][x] + )) + else: + # normal key + mat += translate(v=(x, y, thickness))(key( + width=key_width, + height=key_height, + corner_radius=key_radius, + thickness=key_thickness, + font=font, + label=LAYOUTS[layout][y][x] + )) + + return mat diff --git a/mkbd65/main.py b/mkbd65/main.py new file mode 100644 index 0000000..c4ccf61 --- /dev/null +++ b/mkbd65/main.py @@ -0,0 +1,66 @@ +""" +All sizes are in millimeters. +""" +import sys +from subprocess import run +from configparser import ConfigParser + +from solid import scad_render_to_file + +from key_mat import key_mat +from phone import phone + + +def main(device: str = "oneplus_8t", layout: str = "ansi"): + config = ConfigParser() + config.read_file("config.ini") + + if not config.has_section(device): + sys.exit(f"{device} not found in config.ini") + + components = [ + ["phone", phone( + width=config.getfloat(device, "phone_width"), + height=config.getfloat(device, "phone_height"), + thickness=config.getfloat(device, "phone_thickness"), + corner_radius=config.getfloat(device, "phone_corner_radius"), + side_radius=config.getfloat(device, "phone_side_radius") + )], + ["key_mat", key_mat( + margin=config.getfloat(device, "key_mat_margin", fallback=1), + padding=config.getfloat(device, "key_mat_padding", fallback=1), + thickness=config.getfloat(device, "key_mat_thickness", fallback=0.5), + key_width=config.getfloat(device, "key_mat_key_width", fallback=9), + key_height=config.getfloat(device, "key_mat_key_height", fallback=6), + key_radius=config.getfloat(device, "key_mat_key_corner_radius", fallback=1), + key_thickness=config.getfloat(device, "key_mat_key_thickness", fallback=2), + layout=layout.lower(), + font=config.get(device, "key_mat_font", fallback="DejaVu Sans:style=Bold") + )] + ] + + for component in components: + print(f"Render component {component[0]}...") + scad_render_to_file( + component[1], + filepath=f"mkbd65_{component[0]}.scad", + file_header=f'$fn = 64;', + include_orig_code=True + ) + print(f"Generate STL for component {component[0]}...") + run(["openscad", "-o", f"mkbd65_{component[0]}.stl", f"mkbd65_{component[0]}.scad"]) + print(f"Generate 3MF for component {component[0]}...") + run(["openscad", "-o", f"mkbd65_{component[0]}.3mf", f"mkbd65_{component[0]}.scad"]) + + + # obj_all = obj_phone + obj_key_mat + # scad_render_to_file(obj_all, filepath="mkbd65.scad", file_header=f'$fn = 64;', include_orig_code=True) + + +if __name__ == "__main__": + if "-h" in sys.argv or "--help" in sys.argv: + print("Usage:") + print(" ./main.py [LAYOUT]\n") + print("Layout can be ANSI or ISO.") + + main(device=sys.argv[1]) diff --git a/mkbd65/phone.py b/mkbd65/phone.py new file mode 100644 index 0000000..97782df --- /dev/null +++ b/mkbd65/phone.py @@ -0,0 +1,29 @@ +""" +All sizes are in millimeters. +""" + +from solid import OpenSCADObject, linear_extrude + +from util import round_rect + + +def phone( + width: float, + height: float, + thickness: float, + corner_radius: float, + side_radius: float +) -> OpenSCADObject: + """ + Generates rough estimate of the phones shape + + :param width: length from top to bottom (landscape) + :param height: length from left to right (landscape) + :param thickness: How thicc is it? + :param corner_radius: Assuming all phones have round corners + :return: + """ + return linear_extrude(height=thickness)( + round_rect(width=width, height=height, radius=corner_radius) + ) + diff --git a/mkbd65/util.py b/mkbd65/util.py new file mode 100644 index 0000000..b9d0c18 --- /dev/null +++ b/mkbd65/util.py @@ -0,0 +1,71 @@ +from solid import polygon, circle, translate + + +FILAMENT_LAYER = 0.1 + +LAYOUTS = { + "ansi": [ + ["Esc", "1", "2", "3", "4", "5", "6", "7", "8", "9", "0", "Bksp", None], + ["Q", "W", "E", "R", "T", "Y", "U", "I", "O", "P", "[", "]", "\\"], + ["A", "S", "D", "F", "G", "H", "J", "K", "L", ";", "\"", "Enter", None], + ["Z", "X", "C", "V", "B", "N", "M", ",", ".", "/", "Up", "Shift", None], + ["Ctrl", "Fn", "Shift", "Alt", "Space", None, None, "-", "=", "Left", "Down", "Right", "Ctrl"] + ], + "iso": [ + ["Esc", "1", "2", "3", "4", "5", "6", "7", "8", "9", "0", "Bksp", None], + ["Q", "W", "E", "R", "T", "Y", "U", "I", "O", "P", "[", "]", "Ent"], + ["A", "S", "D", "F", "G", "H", "J", "K", "L", ";", "\"", "\\", None], + ["Z", "X", "C", "V", "B", "N", "M", ",", ".", "/", "Up", "Shift", None], + ["Ctrl", "Fn", "Shift", "Alt", "Space", None, None, "-", "=", "Left", "Down", "Right", "Ctrl"] + ] +} + + +def get_label_size(label: str) -> float: + """ + Get font size depending on label. + + :param label: + :return: + """ + if label in ['⇧', '⏎']: + return 5 + elif label == '⌫': + return 3.5 + elif label in ['Esc', 'Ctrl', 'Alt', 'Fn']: + return 3 + else: + return 4 + + +def round_rect(width: float | int, height: float | int, radius: float | int): + """ + Rectangle with rounded corners. + + Combines a polygon and four circles to form the round corners. + + O/-----\O + / \ + | | + \ / + O\_____/O + + :param width: Width of the rectangle + :param height: Height of the rectangle + :param radius: Radius of rectangle corners + :return: + """ + return polygon(points=[ + (0, radius), + (0, height - radius), + (radius, height), + (width - radius, height), + (width, height - radius), + (width, radius), + (width - radius, 0), + (radius, 0)] + ) \ + + translate(v=(radius, radius, 0))(circle(r=radius)) \ + + translate(v=(radius, height - radius, 0))(circle(r=radius)) \ + + translate(v=(width - radius, height - radius, 0))(circle(r=radius)) \ + + translate(v=(width - radius, radius, 0))(circle(r=radius))