diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..7473cad --- /dev/null +++ b/.gitignore @@ -0,0 +1,4 @@ +*.scad +__pycache__/ +*.pyc +.venv diff --git a/README.md b/README.md index ca60f60..7e64c26 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,74 @@ -# case +# mkbd65 case + +CAD files for the case of mkbd65 + +## Generate CAD files + +OpenSCAD Files can be created with the following command: + +``` +./generate_scad.py +``` + +You need solidpython installed for it to work. Do that by executing: + +``` +# creates a virtualenv +python -m venv venv +# activates it +source venv/bin/activate +# installs dependencies +pip install -r requirements.txt +``` + +## Layout files + +These files define the layoout that gets printed on the keymat. Currently the only file included is `layout_en_us.py` +for an english US international layout. you can use that as a template for your layout. + +The file should define a single variable `LAYOUT` which is a list of roaws. These rows are also lists of columns. Each +column has keys, which are a list of the strings that should be printed at different positions. + +Example for a one button keyboard with the labels `foo` in the top left corner and `bar` in the bottom left: + +``` +LAYOUT = [ + [ + ['foo', 'bar', '', '', ''], + ], +] +``` + +The label string will be used as `[TopLeft, BottomLeft, TopRight, BottomRight, Middle]`. + +Some strings need special escaping to be used in OpenScad later on. See the en_us layout for examples. + +## Phone config + +Properties for different phones are saved in INI files. Currently the only file included is `oneplus5t.ini` for the +Oneplus 5t phone with a english US international layout with the DejaVu Sans font. + +In the config file you can define phone specific aspects and style such as the used font and layout. Here is a list of +the possile options: + +| Group | Option | Type | Default | Description | +| ------ | --------- | ------ | ---------------------- | ----------------------------- | +| keymap | thickness | Number | 0.5 | Thickness of the keymap in mm | +| | layout | String | en_us-intl | Layout file used for labels | +| key | thickness | Number | 1 | Thickness of the key in mm | +| | font | String | DejaVu Sans:style=Bold | Font used for labels | + + +OpenScad files +-------------- + +The generator script `generate_scad.py` creates multiple files. + +| File | Material | Description | +| --------------------- | -------- | --------------------------------------------------- | +| mkbd65_keymat.scad | TPU | Keymat should be printed with a 3D printer | +| mkbd65_pcb.scad | - | Just a spacer for the 3D model fitting | +| mkbd65_top_plate.scad | Aluminum | Laser cut on a CNC | +| mkbd65_internals.scad | ABS/PLA | Inner structure should be printed with a 3D printer | +| mkbd65.scad | - | Combination of the above files | -CAD files for the case of mkbd65 \ No newline at end of file diff --git a/generate_scad.py b/generate_scad.py new file mode 100755 index 0000000..781f22f --- /dev/null +++ b/generate_scad.py @@ -0,0 +1,322 @@ +#!/usr/bin/env python3 + +""" +mkbd65 openscad generator + +Usage: + python3 generate_scad.py [] + +Options: + -h --help Show this screen. + +Example: + python3 generate_scad.py oneplus5t.ini en_us-intl +""" + +import sys +from configparser import ConfigParser +from importlib import import_module + +from solid import cube, text, union, difference, linear_extrude, scad_render_to_file, OpenSCADObject, translate, color, \ + polygon, circle +from solid.utils import Black, White, Green, Aluminum, Cyan + + +def roundrect(width, height, radius): + """ + O/-----\O + / \ + | | + \ / + O\_____/O + + :param width: + :param height: + :param radius: + :return: + """ + return union()( + polygon([ + [0, radius], + [0, height - radius], + [radius, height], + [width - radius, height], + [width, height - radius], + [width, radius], + [width - radius, 0], + [radius, 0] + ]), + translate([radius, radius, 0])( + circle(r=radius) + ), + translate([radius, height - radius, 0])( + circle(r=radius) + ), + translate([width - radius, height - radius, 0])( + circle(r=radius) + ), + translate([width - radius, radius, 0])( + circle(r=radius) + ) + ) + + +def key(dx: int, dy: int, label_ltop: str = '', label_lbottom: str = '', label_rtop: str = '', label_rbottom: str = '', + label_center: str = '') -> OpenSCADObject: + """ + Generates a single key + + :param dx: width in mm + :param dy: length in mm + :param label: List of labels (top, bottom, right, middle) + :return: + """ + def get_size(s: str) -> float: + if s in ['⇧', '⏎']: + return 5 + elif s == '⌫': + return 3.5 + elif s in ['Esc', 'Ctrl', 'Alt', 'Fn']: + return 3 + else: + return 4 + + thickness = config.getfloat('key', 'thickness', fallback=1) + font = config.get('key', 'font', fallback='DejaVu Sans:style=Bold') + + return difference()( + linear_extrude(height=thickness)( + roundrect(width=dx, height=dy, radius=0.5) + ), + + translate(v=[1, 5.5, 0.8])( + linear_extrude(height=0.2)( + text(text=label_ltop, size=3, font=font, halign='left', valign='top') + ) + ), + translate(v=[1, 0.5, 0.8])( + linear_extrude(height=0.2)( + text(text=label_lbottom, size=3, font=font, halign='left', valign='bottom') + ) + ), + translate(v=[8.5, 5.5, 0.8])( + linear_extrude(height=0.2)( + text(text=label_rtop, size=3, font=font, halign='right', valign='top') + ) + ), + translate(v=[8.5, -1.5 if label_rbottom == '`' else 0.5, 0.8])( + linear_extrude(height=0.2)( + text(text=label_rbottom, size=3, font=font, halign='right', valign='bottom') + ) + ), + translate(v=[4.5, 3, 0.8])( + linear_extrude(height=0.2)( + text(text=label_center, size=get_size(label_center), font=font, halign='center', valign='center') + ) + ) + ) + + +def keymat() -> OpenSCADObject: + """ + Generates keymat + + mat: + +---------------------------------------------+ + | margin | + | +-----+ +-----+ +-----+ | + |margin| |padding| | ... | |margin| + | +-----+ +-----+ +-----+ | + | padding | + | +-----+ +-----+ +-----+ | + | | | | | | | | + | +-----+ +-----+ +-----+ | + | ... | + | +-----+ +-----+ +-----+ | + | | | | | | | | + | +-----+ +-----+ +-----+ | + | margin | + +---------------------------------------------+ + \ thickness + key: + +-----+ + | | dy + +-----+ + dx \ thickness + + :return: + """ + thickness = config.getfloat('keymat', 'thickness', fallback=0.5) + cols = 12 + rows = 5 + key_dx = 9 + key_dy = 6 + padding = 1 + margin = 2 + spacebar_x = 5 # nth key from the left + spacebar_length = 2 # length in keys + + keys = [] + for row in range(rows): + for col in range(cols): + keys.append( + translate([ + col * padding + col * key_dx + margin, + row * padding + row * key_dy + margin, + thickness + ])( + key( + dx=key_dx, + dy=key_dy, + label_ltop=layout[row][col][0], + label_lbottom=layout[row][col][1], + label_rtop=layout[row][col][2], + label_rbottom=layout[row][col][3], + label_center=layout[row][col][4] + ) + ) + ) + + return color(Black)( + union()( + # mat + cube([ + cols * key_dx + (cols - 1) * padding + 2 * margin, + rows * key_dy + (rows - 1) * padding + 2 * margin, + thickness + ]), + # keys + *keys, + # spacebar + translate([spacebar_x * key_dx + spacebar_x * padding + margin, margin, thickness])( + key(dx=key_dx * spacebar_length + (spacebar_length - 1) * padding, dy=key_dy) + ) + ) + ) + + +def pcb() -> OpenSCADObject: + rows = 5 + columns = 12 + hspacing = 4 + vspacing = 3 + width = 6 + height = 4 + thickness = 0.78 + switches = [] + + for row in range(rows): + for col in range(columns): + switches.append( + translate([3.5 + (hspacing + width) * col, 2 + (vspacing + height) * row, 0.6])( + color(White)( + linear_extrude(height=thickness)( + # 6 +----+ 5 + # / \ + # 7 + + 4 | 2mm + # 8 + + 3 | + # \ 4mm / + # 1 +----+ 2 + polygon(points=[(1, 0), (5, 0), (6, 1), (6, 3), (5, 4), (1, 4), (0, 3), (0, 1)]), + ) + ) + ) + ) + + return union()( + color(Green)(cube([123, 49, 0.6])), + # ATMEGA32U4 + translate([13, 36, 0.6])( + color(Black)(cube([7, 7, 1])) + ), + # Crystal + translate([23, 35.75, 0.6])( + color(Black)(cube([2.5, 2, 0.55])) + ), + *switches + ) + + +def top_plate() -> OpenSCADObject: + thickness = config.getfloat('keymat', 'thickness', fallback=1) + w = config.getfloat('phone', 'width') + h = config.getfloat('phone', 'height') + r = config.getfloat('phone', 'cornerradius') + + return color(Aluminum)( + difference()( + #cube([123, 49, 0.5]), + linear_extrude(height=thickness)( + roundrect(width=w, height=h, radius=r) + ), + translate([0, 0, -1 * thickness])(keymat()) + ) + ) + + +def internals() -> OpenSCADObject: + return color(Cyan)(cube([1, 1, 1])) + + +def mkbd65() -> OpenSCADObject: + fr4 = 0.4 # pcb thickness + switch = 0.78 # switch height + keymat_thickness = config.getfloat('keymat', 'thickness', fallback=1) + + return union()( + internals(), + pcb(), + color(Black)(translate([0, -1, fr4 + switch])(keymat())), + color(Aluminum)(translate([0, -1, fr4 + switch + keymat_thickness])(top_plate())) + ) + + +if __name__ == '__main__': + config = ConfigParser() + + if '-h' in sys.argv or '--help' in sys.argv or 2 > len(sys.argv) > 3: + sys.exit(__doc__) + + if len(sys.argv) == 3: + layoutfile = sys.argv[2] + else: + layoutfile = 'en_us-intl' + + try: + with open(sys.argv[1]) as fd: + config.read_file(fd) + layout = import_module(f'layout_{ layoutfile }').LAYOUT + file_header = '$fn = 16;' + scad_render_to_file( + keymat(), + filepath='./mkbd65_keymat.scad', + file_header=file_header, + ) + # TODO: generate mkbd65_pcb.scad + scad_render_to_file( + pcb(), + filepath='./mkbd65_pcb.scad', + file_header=file_header, + ) + # TODO: generate mkbd65_top_plate.scad + scad_render_to_file( + top_plate(), + filepath='./mkbd65_top_plate.scad', + file_header=file_header, + ) + # TODO: generate mkbd65_internals.scad + scad_render_to_file( + internals(), + filepath='./mkbd65_internals.scad', + file_header=file_header, + ) + # TODO: generate mkbd65.scad + scad_render_to_file( + mkbd65(), + filepath='./mkbd65.scad', + file_header=file_header, + ) + except FileNotFoundError: + sys.exit(f'The config file { sys.argv[1] } could not be found.') + except ModuleNotFoundError: + sys.exit(f'The layout file `layout_{ layoutfile }.py` could not be found.') diff --git a/layout_en_us-intl.py b/layout_en_us-intl.py new file mode 100644 index 0000000..6a3cc44 --- /dev/null +++ b/layout_en_us-intl.py @@ -0,0 +1,80 @@ +# [TopLeft, BottomLeft, TopRight, BottomRight, Middle] + +LAYOUT = [ + # row 4 + [ + ['', '', '', '', 'Ctrl'], # Left Control + ['', '', '', '', 'Fn'], # Function + ['', '', '', '', '⇧'], # Left Shift + ['', '', '', '', 'Alt'], # Left Alt (Alt Gr) + ['', '\\\\', '|', '', ''], + ['', '', '', '', ''], # Spacebar (won't be visible) + ['', '', '', '', ''], # Spacebar (won't be visible) + ['', '-', '_', '', ''], + ['', '=', '+', '', ''], + ['', '', '', '', '◀'], # Arrow Left / Home + ['', '', '', '', '▼'], # Arrow Down / Page Down + ['', '', '', '', '▶'] # Arrow Right / End + ], + # row 3 + [ + ['', '', '', '', 'Z'], + ['', '', '', '', 'X'], + ['', '', '', '', 'C'], + ['', '', '', '', 'V'], + ['', '', '', '', 'B'], + ['', '', '', '', 'N'], + ['', '', '', '', 'M'], + ['', ',', '<', '', ''], + ['', '.', '>', '', ''], + ['', '/', '?', '', ''], + ['', '', '', '', '▲'], # Arrow Up / Page Up + ['', '', '', '', '⇧'] # Right Shift + ], + # row 2 + [ + ['', '', '', '', 'A'], + ['', '', '', '', 'S'], + ['', '', '', '', 'D'], + ['', '', '', '', 'F'], + ['', '', '', '', 'G'], + ['', '', '', '', 'H'], + ['', '', '', '', 'J'], + ['', '', '', '', 'K'], + ['', '', '', '', 'L'], + ['', ';', ':', '', ''], + ['', '\'', '\\"', '`', ''], + ['', '', '', '', '⏎'] # Return + ], + # row 1 + [ + ['', '', '', '', 'Q'], + ['', '', '', '', 'W'], + ['', '', '', '', 'E'], + ['', '', '', '', 'R'], + ['', '', '', '', 'T'], + ['', '', '', '', 'Y'], + ['', '', '', '', 'U'], + ['', '', '', '', 'I'], + ['', '', '', '', 'O'], + ['', '', '', '', 'P'], + ['', '[', '{', '', ''], + ['', ']', '}', '', ''] + ], + # row 0 + [ + ['','','','','Esc'], + ['', '1', '!', '', ''], + ['', '2', '@', '', ''], + ['', '3', '#', '', ''], + ['', '4', '$', '', ''], + #['', '5', '%', '€', ''], + ['', '5', '%', '', ''], + ['', '6', '^', '', ''], + ['', '7', '&', '', ''], + ['', '8', '*', '', ''], + ['', '9', '(', '', ''], + ['', '0', ')', '', ''], + ['', '', '', '', '⌫'] # Backspace + ] +] diff --git a/oneplus5t.ini b/oneplus5t.ini new file mode 100644 index 0000000..a6d9927 --- /dev/null +++ b/oneplus5t.ini @@ -0,0 +1,14 @@ +; mkbd65 config for OnePlus 5t + +[keymat] +thickness = 0.5 +layout = en_us-intl + +[key] +thickness = 1 +font = DejaVu Sans:style=Bold + +[phone] +width = 156 +height = 75 +cornerradius = 10 \ No newline at end of file diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..7b89a88 --- /dev/null +++ b/requirements.txt @@ -0,0 +1 @@ +solidpython