├── README.md ├── constants.scad ├── keycap_playground.scad ├── keycaps.scad ├── legends.scad ├── pregenerated ├── Riskable Profile 4.0 (Homing Dot).stl └── Riskable Profile 4.0.stl ├── profiles.scad ├── riskable_logo.dxf ├── scripts ├── gem_full.py ├── keycap.py ├── riskeyboard_70.py └── riskeycap_full.py ├── snap_fit.scad ├── stems.scad └── utils.scad /README.md: -------------------------------------------------------------------------------- 1 | # keycap_playground 2 | The Keycap Playground is a parametric OpenSCAD keycap generator made for generating keycaps of all shapes and sizes (and profiles) 3 | -------------------------------------------------------------------------------- /constants.scad: -------------------------------------------------------------------------------- 1 | // Common/useful constants 2 | 3 | KEYCAP_UNIT = 18; // Size of a standard (square) keycap (seen 18.16 used elsewhere but I dunno :shrug:) 4 | KEY_UNIT = 19.05; // Standard spacing between keys -------------------------------------------------------------------------------- /keycaps.scad: -------------------------------------------------------------------------------- 1 | // Keycaps -- Contains the poly_keycap() module which can generate pretty much any kind of keycap. 2 | 3 | use 4 | use 5 | 6 | // CONSTANTS 7 | KEY_UNIT = 19.05; // Square that makes up the entire space of a key 8 | 9 | // Draws the keycap without legends (because we need to do an intersection() of the keycap+legends to make sure legends conform to the correct shape) 10 | module _poly_keycap(height=9.0, length=18, width=18, 11 | wall_thickness=1.25, top_difference=6, top_x=0, top_y=0, dish_type="cylinder", 12 | dish_tilt=-4, dish_depth=1, dish_x=0, dish_y=0, dish_z=-0.75, dish_thickness=2, 13 | dish_fn=32, dish_corner_fn=64, dish_tilt_curve=false, stem_clips=false, 14 | stem_walls_inset=0, stem_walls_tolerance=0.25, 15 | polygon_layers=5, polygon_layer_rotation=10, polygon_curve=0, polygon_edges=4, 16 | corner_radius=0.5, corner_radius_curve=0, polygon_rotation=false, 17 | dish_division_x=4, dish_division_y=1, // Fancy schmancy control over spherical inverted dishes 18 | dish_invert=false, debug=false) { 19 | layer_x_adjust = top_x/polygon_layers; 20 | layer_y_adjust = top_y/polygon_layers; 21 | layer_tilt_adjust = dish_tilt/polygon_layers; 22 | l_height = height/polygon_layers; 23 | l_difference = (top_difference/polygon_layers); 24 | // This (reduction_factor and height_adjust) attempts to make up for the fact that when you rotate a rectangle the corner goes *up* (not perfect but damned close!): 25 | reduction_factor = dish_tilt_curve ? 2.25 : 2.35; 26 | height_adjust = ((abs(width * sin(dish_tilt)) + abs(height * cos(dish_tilt))) - height)/polygon_layers/reduction_factor; 27 | difference() { 28 | for (l=[0:polygon_layers-1]) { 29 | layer_height_adjust_below = (height_adjust*l); 30 | layer_height_adjust_above = (height_adjust*(l+1)); 31 | tilt_below_curved = dish_tilt_curve ? layer_tilt_adjust * l: 0; 32 | tilt_below_straight = dish_tilt_curve ? 0: layer_tilt_adjust * l; 33 | tilt_above_curved = dish_tilt_curve ? layer_tilt_adjust * (l+1): 0; 34 | tilt_above_straight = dish_tilt_curve ? 0: layer_tilt_adjust * (l+1); 35 | tilt_below = layer_tilt_adjust * l; 36 | tilt_above = layer_tilt_adjust * (l+1); 37 | extra_corner_radius_below = (corner_radius*corner_radius_curve/polygon_layers)*l; 38 | extra_corner_radius_above = (corner_radius*corner_radius_curve/polygon_layers)*(l+1); 39 | corner_radius_below = corner_radius + extra_corner_radius_below; 40 | corner_radius_above = corner_radius + extra_corner_radius_above; 41 | reduction_factor_below = polygon_slice(l, polygon_curve, total_steps=polygon_layers); 42 | reduction_factor_above = polygon_slice(l+1, polygon_curve, total_steps=polygon_layers); 43 | curve_val_below = (top_difference - reduction_factor_below) * (l/polygon_layers); 44 | curve_val_above = (top_difference - reduction_factor_above) * ((l+1)/polygon_layers); 45 | if (polygon_rotation) { 46 | hull() { 47 | rotate([tilt_below_curved,0,polygon_layer_rotation*l]) { 48 | translate([ 49 | layer_x_adjust*l, 50 | layer_y_adjust*l, 51 | l_height*l-layer_height_adjust_below]) 52 | rotate([tilt_below_straight,0,0]) { 53 | if (polygon_edges==4) { // Normal key 54 | xy = [length-curve_val_below,width-curve_val_below]; 55 | squarish_rpoly(xy=xy, h=0.01, r=corner_radius_below, center=false, $fn=dish_corner_fn); 56 | } else { // We're doing something funky! 57 | rpoly( 58 | d=length-curve_val_below, 59 | h=0.01, r=corner_radius_below, 60 | edges=polygon_edges, center=false, 61 | $fn=dish_corner_fn); 62 | } 63 | } 64 | } 65 | rotate([tilt_above_curved,0,polygon_layer_rotation*(l+1)]) { 66 | translate([ 67 | layer_x_adjust*(l+1), 68 | layer_y_adjust*(l+1), 69 | l_height*(l+1)-layer_height_adjust_above]) 70 | rotate([tilt_above_straight,0,0]) { 71 | if (polygon_edges==4) { // Normal key 72 | xy = [length-curve_val_above,width-curve_val_above]; 73 | squarish_rpoly( 74 | xy=xy, h=0.01, r=corner_radius_above, 75 | center=false, $fn=dish_corner_fn); 76 | } else { // We're doing something funky! 77 | rpoly( 78 | d=length-curve_val_above, 79 | h=0.01, r=corner_radius_above, 80 | edges=polygon_edges, center=false, 81 | $fn=dish_corner_fn); 82 | } 83 | } 84 | } 85 | } 86 | } else { 87 | if (is_odd(l)) { 88 | hull() { 89 | rotate([tilt_below_curved,0,polygon_layer_rotation*l]) { 90 | translate([ 91 | layer_x_adjust*l, 92 | layer_y_adjust*l, 93 | l_height*l-layer_height_adjust_below]) 94 | rotate([tilt_below_straight,0,0]) { 95 | if (polygon_edges==4) { // Normal key 96 | xy = [length-curve_val_below,width-curve_val_below]; 97 | squarish_rpoly( 98 | xy=xy, h=0.01, 99 | r=corner_radius_below, center=false, 100 | $fn=dish_corner_fn); 101 | } else { // We're doing something funky! 102 | rpoly( 103 | d=length-curve_val_below, 104 | h=0.01, r=corner_radius_below, 105 | edges=polygon_edges, center=false, 106 | $fn=dish_corner_fn); 107 | } 108 | } 109 | } 110 | rotate([tilt_above_curved,0,-polygon_layer_rotation*(l+1)]) { 111 | translate([ 112 | layer_x_adjust*(l+1), 113 | layer_y_adjust*(l+1), 114 | l_height*(l+1)-layer_height_adjust_above]) 115 | rotate([tilt_above_straight,0,0]) { 116 | if (polygon_edges==4) { // Normal key 117 | xy = [length-curve_val_above,width-curve_val_above]; 118 | squarish_rpoly( 119 | xy=xy, h=0.01, 120 | r=corner_radius_above, center=false, 121 | $fn=dish_corner_fn); 122 | } else { // We're doing something funky! 123 | rpoly( 124 | d=length-curve_val_above, 125 | h=0.01, r=corner_radius_above, 126 | edges=polygon_edges, center=false, 127 | $fn=dish_corner_fn); 128 | } 129 | } 130 | } 131 | } 132 | } else { // Even-numbered polygon layer 133 | hull() { 134 | if (l == 0) { // First layer 135 | rotate([ 136 | tilt_below_curved, 137 | 0, 138 | -polygon_layer_rotation*l-layer_height_adjust_below]) { 139 | translate([0,0,l_height*l]) 140 | rotate([tilt_below_straight,0,0]) { 141 | if (polygon_edges==4) { // Normal key 142 | xy = [length-curve_val_below,width-curve_val_below]; 143 | squarish_rpoly( 144 | xy=xy, h=0.01, 145 | r=corner_radius_below, center=false, 146 | $fn=dish_corner_fn); 147 | } else { // We're doing something funky! 148 | rpoly( 149 | d=length-curve_val_below, 150 | h=0.01, r=corner_radius_below, 151 | edges=polygon_edges, center=false, 152 | $fn=dish_corner_fn); 153 | } 154 | } 155 | } 156 | } else { 157 | rotate([tilt_below_curved,0,-polygon_layer_rotation*l]) { 158 | translate([ 159 | layer_x_adjust*l, 160 | layer_y_adjust*l, 161 | l_height*l-layer_height_adjust_below]) 162 | rotate([tilt_below_straight,0,0]) { 163 | if (polygon_edges==4) { // Normal key 164 | xy = [length-curve_val_below,width-curve_val_below]; 165 | squarish_rpoly( 166 | xy=xy, h=0.01, 167 | r=corner_radius_below, center=false, 168 | $fn=dish_corner_fn); 169 | } else { // We're doing something funky! 170 | rpoly( 171 | d=length-curve_val_below, 172 | h=0.01, r=corner_radius_below, 173 | edges=polygon_edges, center=false, 174 | $fn=dish_corner_fn); 175 | } 176 | } 177 | } 178 | } 179 | rotate([tilt_above_curved,0,polygon_layer_rotation*(l+1)]) { 180 | translate([ 181 | layer_x_adjust*(l+1), 182 | layer_y_adjust*(l+1), 183 | l_height*(l+1)-layer_height_adjust_above]) 184 | rotate([tilt_above_straight,0,0]) { 185 | if (polygon_edges==4) { // Normal key 186 | xy = [length-curve_val_above,width-curve_val_above]; 187 | squarish_rpoly( 188 | xy=xy, h=0.01, 189 | r=corner_radius_above, center=false, 190 | $fn=dish_corner_fn); 191 | } else { // We're doing something funky! 192 | rpoly( 193 | d=length-curve_val_above, 194 | h=0.01, r=corner_radius_above, 195 | edges=polygon_edges, center=false, 196 | $fn=dish_corner_fn); 197 | } 198 | } 199 | } 200 | } 201 | } 202 | } 203 | if (dish_depth != 0 && l == polygon_layers-1 && dish_invert) { 204 | // Last layer; do the inverted dish if needed 205 | rotate([tilt_above_curved,0,polygon_layer_rotation*(l+1)]) 206 | translate([top_x,top_y,height-layer_height_adjust_above]) 207 | rotate([tilt_above_straight,0,0]) { 208 | if (dish_type == "sphere") { 209 | hull() { 210 | if (polygon_edges==4) { // Normal key 211 | xy = [ 212 | length-curve_val_above-top_difference, 213 | width-curve_val_above-top_difference]; 214 | squarish_rpoly( 215 | xy=xy, h=0.1, 216 | r=corner_radius_above, center=false, 217 | $fn=dish_corner_fn); 218 | } else { // We're doing something funky! 219 | rpoly( 220 | d=length-curve_val_above-top_difference, 221 | h=0.05, r=corner_radius_above, 222 | edges=polygon_edges, center=false, 223 | $fn=dish_corner_fn); 224 | } 225 | depth_step = dish_depth/polygon_layers; 226 | depth_curve_factor = -dish_depth*4; // For this we use a fixed curve 227 | amplitude = 1; // Sine wave amplitude 228 | // To keep things simple we'll use the same number of polygon_layers from the main keycap: 229 | for (bend_layer=[0:polygon_layers-1]) { 230 | ratio = sin(bend_layer/(polygon_layers*2)*180) * amplitude; 231 | depth_reduction_factor = polygon_slice( 232 | bend_layer, depth_curve_factor, total_steps=polygon_layers); 233 | depth_curve_val = (depth_step - depth_reduction_factor) * (bend_layer/polygon_layers); 234 | adjusted_length = length - top_difference; 235 | adjusted_width = width - top_difference; 236 | layer_length = adjusted_length-(adjusted_length*ratio/dish_division_x); 237 | layer_width = adjusted_width-(adjusted_width*ratio/dish_division_y); 238 | layer_height = dish_depth*ratio; 239 | translate([ 240 | 0, 241 | 0, 242 | depth_curve_val]) { 243 | if (polygon_edges==4) { // Normal key 244 | xy = [layer_length,layer_width]; 245 | squarish_rpoly( 246 | xy=xy, h=0.01, 247 | r=corner_radius_above*(1-ratio), center=false, 248 | $fn=dish_corner_fn); 249 | } else { // We're doing something funky! 250 | rpoly( 251 | d=layer_length, h=0.01, 252 | r=corner_radius_above, 253 | edges=polygon_edges, center=false, 254 | $fn=dish_corner_fn); 255 | } 256 | } 257 | } 258 | } 259 | } else if (dish_type == "cylinder") { 260 | hull() { 261 | if (polygon_edges==4) { // Normal key 262 | xy = [length-curve_val_above-top_difference,width-curve_val_above-top_difference]; 263 | squarish_rpoly( 264 | xy=xy, h=0.1, 265 | r=corner_radius_above, center=false, 266 | $fn=dish_corner_fn); 267 | } else { // We're doing something funky! 268 | rpoly( 269 | d=length-curve_val_above-top_difference, h=0.1, 270 | r=corner_radius_above, edges=polygon_edges, 271 | center=false, $fn=dish_corner_fn); 272 | } 273 | depth_step = dish_depth/polygon_layers; 274 | depth_curve_factor = -dish_depth*4; // For this we use a fixed curve 275 | amplitude = 1; // Sine wave amplitude 276 | // To keep things simple we'll use the same number of polygon_layers from the main keycap: 277 | for (bend_layer=[0:polygon_layers-1]) { 278 | ratio = sin(bend_layer/(polygon_layers*1.5)*180) * amplitude; 279 | depth_reduction_factor = polygon_slice( 280 | bend_layer, depth_curve_factor, total_steps=polygon_layers); 281 | depth_curve_val = (depth_step - depth_reduction_factor) * (bend_layer/polygon_layers); 282 | adjusted_length = length - top_difference; 283 | adjusted_width = width - top_difference; 284 | layer_length = adjusted_length-(adjusted_length*ratio/30); 285 | layer_width = adjusted_width-(adjusted_width*ratio); 286 | layer_height = dish_depth*ratio; 287 | translate([ 288 | 0, 289 | 0, 290 | depth_curve_val]) { 291 | if (polygon_edges==4) { // Normal key 292 | xy = [layer_length,layer_width]; 293 | squarish_rpoly( 294 | xy=xy, h=0.01, 295 | r=corner_radius_above*(1-ratio), center=false, 296 | $fn=dish_corner_fn); 297 | } else { // We're doing something funky! 298 | rpoly( 299 | d=layer_length, h=0.01, 300 | r=corner_radius_above, 301 | edges=polygon_edges, center=false, 302 | $fn=dish_corner_fn); 303 | } 304 | } 305 | } 306 | } 307 | } else { 308 | warning("inv_pyramid not supported for DISH_INVERT (dish_invert) yet"); 309 | } 310 | } 311 | } 312 | } 313 | // TODO: Instead of using cylinder() and sphere() to make these use 2D primitives like we do with the inverted dishes (so we don't have to crank up the $fn so high; to improve rendering speed) 314 | // Do the dishes! 315 | tilt_above_curved = dish_tilt_curve ? layer_tilt_adjust * polygon_layers : 0; 316 | tilt_above_straight = dish_tilt_curve ? 0 : layer_tilt_adjust * polygon_layers; 317 | z_adjust = height_adjust*(polygon_layers+2); 318 | extra_corner_radius = (corner_radius*corner_radius_curve/polygon_layers)*polygon_layers; 319 | corner_radius_up_top = corner_radius + extra_corner_radius; 320 | if (!dish_invert) { // Inverted dishes aren't subtracted like this 321 | if (dish_type == "inv_pyramid") { 322 | rotate([tilt_above_curved,0,0]) 323 | translate([dish_x+top_x,dish_y+top_y,height-dish_depth+dish_z-z_adjust-0.1]) 324 | rotate([tilt_above_straight,0,0]) 325 | squarish_rpoly( 326 | xy1=[0.01,0.01], 327 | xy2=[length-top_difference+0.5,width-top_difference+0.5], 328 | h=dish_depth+0.1, r=corner_radius_up_top, 329 | center=false, $fn=dish_fn); 330 | // Cut off everything above the pyramid 331 | translate([0,0,height/2+height+dish_z-0.02]) 332 | cube([length*2,width*2,height], center=true); 333 | } else if (dish_type == "cylinder") { 334 | // Cylindrical cutout option: 335 | adjusted_key_length = length - top_difference; 336 | adjusted_key_width = width - top_difference; 337 | adjusted_dimension = length > width ? adjusted_key_length : adjusted_key_width; 338 | chord_length = dish_depth > 0 ? (pow(adjusted_dimension,2) - 4 * pow(dish_depth,2)) / (8 * dish_depth) : 0; 339 | rad = (pow(adjusted_dimension, 2) + 4 * pow(dish_depth, 2)) / (8 * dish_depth); 340 | rotate([tilt_above_curved,0,0]) 341 | translate([dish_x+top_x,dish_y+top_y,chord_length+height+dish_z-z_adjust]) 342 | rotate([tilt_above_straight,0,0]) 343 | rotate([90, 0, 0]) 344 | cylinder(h=length*3, r=rad, center=true, $fn=dish_fn); 345 | } else if (dish_type == "sphere") { 346 | adjusted_key_length = length - top_difference; 347 | adjusted_key_width = width - top_difference; 348 | adjusted_dimension = length > width ? adjusted_key_length : adjusted_key_width; 349 | rad = dish_depth > 0 ? (pow(adjusted_dimension, 2) + 4 * pow(dish_depth, 2)) / (8 * dish_depth) : 0; 350 | rotate([tilt_above_curved,0,0]) 351 | translate([dish_x+top_x,dish_y+top_y,rad*2+height-dish_depth+dish_z-z_adjust]) 352 | rotate([tilt_above_straight,0,0]) 353 | sphere(r=rad*2, $fn=dish_fn); 354 | } 355 | } 356 | } 357 | } 358 | 359 | // TODO: Document all these arguments 360 | // NOTE: If polygon_curve or corner_radius_curve are 0 they will be ignored (respectively) 361 | module poly_keycap(height=9.0, length=18, width=18, 362 | wall_thickness=1.25, top_difference=6, top_x=0, top_y=0, 363 | dish_tilt=-4, dish_tilt_curve=false, stem_clips=false, 364 | stem_walls_inset=0, stem_walls_tolerance=0.25, 365 | dish_depth=1, dish_x=0, dish_y=0, 366 | dish_z=-0.75, dish_thickness=2, 367 | dish_fn=32, dish_corner_fn=64, 368 | dish_division_x=4, dish_division_y=1, // Fancy schmancy control over spherical inverted dishes 369 | legends=[""], legend_font_sizes=[6], legend_fonts=["Roboto"], legend_carved=false, 370 | legend_trans=[[0,0,0]], legend_trans2=[[0,0,0]], legend_scale=[[1,1,1]], 371 | legend_rotation=[[0,0,0]], legend_rotation2=[[0,0,0]], legend_underset=[[0,0,0]], 372 | polygon_layers=5, polygon_layer_rotation=10, polygon_curve=0, 373 | polygon_edges=4, dish_type="cylinder", corner_radius=0.5, corner_radius_curve=0, 374 | homing_dot_length=0, homing_dot_width=0, 375 | homing_dot_x=0, homing_dot_y=0, homing_dot_z=0, 376 | visualize_legends=false, polygon_rotation=false, key_rotation=[0,0,0], 377 | dish_invert=false, uniform_wall_thickness=true, debug=false) { 378 | layer_tilt_adjust = dish_tilt/polygon_layers; 379 | // Inverted dish means we need to make the legend a little taller 380 | legend_inverted_dish_adjustment = dish_invert ? dish_depth*1.25 : 0; 381 | inverted_dish_adjustment = dish_invert ? dish_depth : 0; 382 | if (debug) { 383 | // NOTE: Tried to divide these up into logical sections; all related elements should be on one line 384 | echo(height=height, length=length, width=width); 385 | echo(wall_thickness=wall_thickness, top_difference=top_difference, top_x=top_x, top_y=top_y); 386 | echo(dish_tilt=dish_tilt, dish_depth=dish_depth, dish_x=dish_x, dish_y=dish_y, dish_z=dish_z, dish_thickness=dish_thickness, dish_fn=dish_fn, dish_type=dish_type, dish_invert=dish_invert); 387 | // Really don't need to have this spit out 90% of the time... Just uncomment if you really need to see loads and loads and LOADS of debug: 388 | // echo(legends=legends, legend_font_sizes=legend_font_sizes, legend_fonts=legend_fonts); 389 | // echo(legend_trans=legend_trans, legend_trans2=legend_trans2); 390 | // echo(legend_rotation=legend_rotation, legend_rotation2=legend_rotation2); 391 | echo(polygon_layers=polygon_layers, polygon_layer_rotation=polygon_layer_rotation, polygon_rotation=polygon_rotation, polygon_curve=polygon_curve, polygon_edges=polygon_edges, corner_radius=corner_radius, corner_radius_curve=corner_radius_curve); 392 | // These should be obvious enough that you don't need to see their values spit out in the console: 393 | // echo(visualize_legends=visualize_legends, key_rotation=key_rotation); 394 | echo(stem_clips=stem_clips, stem_walls_inset=stem_walls_inset); 395 | } 396 | rotate(key_rotation) { 397 | difference() { 398 | _poly_keycap( 399 | height=height, length=length, width=width, wall_thickness=wall_thickness, 400 | top_difference=top_difference, dish_tilt=dish_tilt, 401 | dish_tilt_curve=dish_tilt_curve, stem_clips=stem_clips, 402 | stem_walls_inset=stem_walls_inset, 403 | top_x=top_x, top_y=top_y, dish_depth=dish_depth, 404 | dish_x=dish_x, dish_y=dish_y, dish_z=dish_z, 405 | dish_thickness=dish_thickness, dish_fn=dish_fn, 406 | dish_corner_fn=dish_corner_fn, 407 | polygon_layers=polygon_layers, polygon_layer_rotation=polygon_layer_rotation, 408 | polygon_edges=polygon_edges, polygon_curve=polygon_curve, 409 | dish_type=dish_type, corner_radius=corner_radius, 410 | dish_division_x=dish_division_x, dish_division_y=dish_division_y, 411 | corner_radius_curve=corner_radius_curve, polygon_rotation=polygon_rotation, 412 | dish_invert=dish_invert); 413 | tilt_above_curved = dish_tilt_curve ? layer_tilt_adjust * polygon_layers : 0; 414 | // Take care of the legends 415 | for (i=[0:1:len(legends)-1]) { 416 | legend = legends[i] ? legends[i]: ""; 417 | rotation = legend_rotation[i] ? legend_rotation[i] : (legend_rotation[0] ? legend_rotation[0]: [0,0,0]); 418 | rotation2 = legend_rotation2[i] ? legend_rotation2[i] : (legend_rotation2[0] ? legend_rotation2[0]: [0,0,0]); 419 | trans = legend_trans[i] ? legend_trans[i] : (legend_trans[0] ? legend_trans[0]: [0,0,0]); 420 | trans2 = legend_trans2[i] ? legend_trans2[i] : (legend_trans2[0] ? legend_trans2[0]: [0,0,0]); 421 | font_size = legend_font_sizes[i] ? legend_font_sizes[i]: legend_font_sizes[0]; 422 | font = legend_fonts[i] ? legend_fonts[i] : (legend_fonts[0] ? legend_fonts[0]: "Roboto"); 423 | l_scale = legend_scale[i] ? legend_scale[i] : legend_scale[0]; 424 | underset = legend_underset[i] ? legend_underset[i] : ( 425 | legend_underset[0] ? legend_underset[0]: [0,0,0]); 426 | if (visualize_legends) { 427 | %translate(underset) { 428 | translate(trans2) rotate(rotation2) 429 | translate(trans) rotate(rotation) 430 | scale(l_scale) 431 | rotate([tilt_above_curved,0,0]) 432 | color([0.5,0.5,0.5,0.75]) difference() { 433 | draw_legend(legend, font_size, font, height+legend_inverted_dish_adjustment); 434 | // Make sure the preview matches the curve of the dish on the bottom 435 | if (legend_carved) { 436 | translate([0,0,-height+dish_depth-dish_z]) 437 | _poly_keycap( 438 | height=height, length=length, width=width, 439 | wall_thickness=wall_thickness, 440 | top_difference=top_difference, dish_tilt=dish_tilt, 441 | dish_tilt_curve=dish_tilt_curve, stem_clips=stem_clips, 442 | stem_walls_inset=stem_walls_inset, 443 | top_x=top_x, top_y=top_y, dish_depth=dish_depth, 444 | dish_x=dish_x, dish_y=dish_y, dish_z=dish_z, 445 | dish_division_x=dish_division_x, 446 | dish_division_y=dish_division_y, 447 | dish_thickness=dish_thickness+0.1, dish_fn=dish_fn, 448 | dish_corner_fn=dish_corner_fn, 449 | polygon_layers=polygon_layers, 450 | polygon_layer_rotation=polygon_layer_rotation, 451 | polygon_edges=polygon_edges, polygon_curve=polygon_curve, 452 | dish_type=dish_type, corner_radius=corner_radius, 453 | corner_radius_curve=corner_radius_curve, 454 | polygon_rotation=polygon_rotation, 455 | dish_invert=dish_invert); 456 | } 457 | } 458 | } 459 | } else { 460 | // NOTE: This translate([0,0,0.001]) call is just to fix preview rendering 461 | translate(underset) translate([0,0,0.001]) intersection() { 462 | translate(trans2) rotate(rotation2) 463 | translate(trans) rotate(rotation) 464 | scale(l_scale) 465 | rotate([tilt_above_curved,0,0]) 466 | difference() { 467 | draw_legend(legend, font_size, font, height+legend_inverted_dish_adjustment); 468 | if (legend_carved) { 469 | translate([0,0,-height+dish_depth-dish_z]) 470 | _poly_keycap( 471 | height=height, length=length, width=width, 472 | wall_thickness=wall_thickness, 473 | top_difference=top_difference, dish_tilt=dish_tilt, 474 | dish_tilt_curve=dish_tilt_curve, stem_clips=stem_clips, 475 | stem_walls_inset=stem_walls_inset, 476 | top_x=top_x, top_y=top_y, dish_depth=dish_depth, 477 | dish_x=dish_x, dish_y=dish_y, dish_z=dish_z, 478 | dish_division_x=dish_division_x, 479 | dish_division_y=dish_division_y, 480 | dish_thickness=dish_thickness+0.1, dish_fn=dish_fn, 481 | dish_corner_fn=dish_corner_fn, 482 | polygon_layers=polygon_layers, 483 | polygon_layer_rotation=polygon_layer_rotation, 484 | polygon_edges=polygon_edges, polygon_curve=polygon_curve, 485 | dish_type=dish_type, corner_radius=corner_radius, 486 | corner_radius_curve=corner_radius_curve, 487 | polygon_rotation=polygon_rotation, 488 | dish_invert=dish_invert); 489 | } 490 | } 491 | _poly_keycap( 492 | height=height, length=length, width=width, 493 | wall_thickness=wall_thickness, 494 | top_difference=top_difference, 495 | dish_tilt=dish_tilt, 496 | dish_tilt_curve=dish_tilt_curve, stem_clips=stem_clips, 497 | stem_walls_inset=stem_walls_inset, 498 | top_x=top_x, top_y=top_y, dish_depth=dish_depth, 499 | dish_x=dish_x, dish_y=dish_y, dish_z=dish_z, 500 | dish_division_x=dish_division_x, 501 | dish_division_y=dish_division_y, 502 | dish_thickness=dish_thickness+0.1, dish_fn=dish_fn, 503 | dish_corner_fn=dish_corner_fn, 504 | polygon_layers=polygon_layers, 505 | polygon_layer_rotation=polygon_layer_rotation, 506 | polygon_edges=polygon_edges, polygon_curve=polygon_curve, 507 | dish_type=dish_type, corner_radius=corner_radius, 508 | corner_radius_curve=corner_radius_curve, 509 | polygon_rotation=polygon_rotation, 510 | dish_invert=dish_invert); 511 | } 512 | } 513 | } 514 | // Interior cutout (i.e. make room inside the keycap) 515 | // TODO: Add support for snap-fit stems with uniform_wall_thickness 516 | if (uniform_wall_thickness) { // Make the interior match the shape of the dish 517 | translate([0,0,-0.001]) { 518 | _poly_keycap( 519 | height=height-wall_thickness, length=length-wall_thickness*2, 520 | width=width-wall_thickness*2, wall_thickness=wall_thickness, 521 | top_difference=top_difference, 522 | dish_tilt=dish_tilt, 523 | dish_tilt_curve=dish_tilt_curve, stem_clips=stem_clips, 524 | stem_walls_inset=stem_walls_inset, 525 | top_x=top_x, top_y=top_y, dish_depth=dish_depth, 526 | dish_x=dish_x, dish_y=dish_y, dish_z=dish_z, 527 | dish_thickness=dish_thickness, dish_fn=dish_fn, 528 | dish_corner_fn=dish_corner_fn, 529 | polygon_layers=polygon_layers, 530 | polygon_layer_rotation=polygon_layer_rotation, 531 | polygon_edges=polygon_edges, polygon_curve=polygon_curve, 532 | dish_type=dish_type, corner_radius=corner_radius/1.25, 533 | dish_division_x=dish_division_x, dish_division_y=dish_division_y, 534 | corner_radius_curve=corner_radius_curve, 535 | polygon_rotation=polygon_rotation, 536 | dish_invert=dish_invert); 537 | } 538 | if (stem_clips) { 539 | warning("STEM_SNAP_FIT/stem_clips does not currently wortk with UNIFORM_WALL_THICKNESS"); 540 | } 541 | } else { // Trapezoidal interior cutout (keeps things simple) 542 | difference() { 543 | corner_radius_factor = ((corner_radius*corner_radius_curve/polygon_layers)*polygon_layers)/1.5; 544 | translate([0,0,-0.001]) difference() { 545 | squarish_rpoly( 546 | xy1=[length-wall_thickness*2,width-wall_thickness*2], 547 | xy2=[length-wall_thickness*2-top_difference-corner_radius_factor, 548 | width-wall_thickness*2-top_difference-corner_radius_factor], 549 | xy2_offset=[top_x,top_y], 550 | h=height, r=corner_radius, center=false, 551 | $fn=dish_corner_fn); 552 | // TEMPORARILY DISABLED NORTHEAST INDICATOR SINCE IT WAS CAUSING PROBLEMS: 553 | // This adds a northeast (back right) indicator so you can tell which side is which with symmetrical keycaps 554 | // if (wall_thickness > 0) { 555 | // translate([ 556 | // length-wall_thickness*2-0.925, 557 | // width-wall_thickness*2-1.125, 558 | // 0]) squarish_rpoly( 559 | // xy1=[length-wall_thickness*2,width-wall_thickness*2], 560 | // xy2=[length-wall_thickness*2-top_difference,width-wall_thickness*2-top_difference], 561 | // xy2_offset=[top_x,top_y], 562 | // h=height, r=corner_radius/2, center=false); 563 | // } 564 | clip_width = wall_thickness*2; 565 | clip_height = 2; 566 | clip_tolerance = 0.05; // Just the tiniest smidge is all that's necessary 567 | height_factor = top_difference*(stem_walls_inset/height); 568 | // NOTE: The top half of the clip gets cut off so the clip_height is really 1 (when set to 2) 569 | if (stem_clips) { 570 | translate([ 571 | length/6, 572 | -width/2+clip_width/2+height_factor, 573 | stem_walls_inset-clip_height/2-clip_tolerance]) 574 | difference() { 575 | cube([length/5,clip_width,clip_height], center=true); 576 | translate([0,0,-clip_height/1.333]) 577 | rotate([45,0,0]) 578 | cube([length,10,clip_height], center=true); 579 | // Cut off a bit of an angle at the side so there's no printing in mid-air when printing a keycap on its side: 580 | translate([clip_width,0,clip_width/2]) 581 | rotate([0,-key_rotation[1],0]) 582 | cube([clip_height,clip_height*2,clip_width], center=true); 583 | } 584 | translate([ 585 | -length/6, 586 | -width/2+clip_width/2+height_factor, 587 | stem_walls_inset-clip_height/2-clip_tolerance]) 588 | difference() { 589 | cube([length/5,clip_width,clip_height], center=true); 590 | translate([0,0,-clip_height/1.333]) 591 | rotate([45,0,0]) 592 | cube([length,10,clip_height], center=true); 593 | translate([clip_width,0,clip_width/2]) 594 | rotate([0,-key_rotation[1],0]) 595 | cube([clip_height,clip_height*2,clip_width], center=true); 596 | } 597 | // Mirror the clips on the other side 598 | mirror([0,1,0]) { 599 | translate([ 600 | length/6, 601 | -width/2+clip_width/2+height_factor, 602 | stem_walls_inset-clip_height/2-clip_tolerance]) 603 | difference() { 604 | cube([length/5,clip_width,clip_height], center=true); 605 | translate([0,0,-clip_height/1.333]) 606 | rotate([45,0,0]) 607 | cube([length,10,clip_height], center=true); 608 | translate([clip_width,0,clip_width/2]) 609 | rotate([0,-key_rotation[1],0]) 610 | cube([ 611 | clip_height, 612 | clip_height*2, 613 | clip_width], center=true); 614 | } 615 | translate([ 616 | -length/6, 617 | -width/2+clip_width/2+height_factor, 618 | stem_walls_inset-clip_height/2-clip_tolerance]) 619 | difference() { 620 | cube([length/5,clip_width,clip_height], center=true); 621 | translate([0,0,-clip_height/1.333]) 622 | rotate([45,0,0]) 623 | cube([length,10,clip_height], center=true); 624 | translate([clip_width,0,clip_width/2]) 625 | rotate([0,-key_rotation[1],0]) 626 | cube([ 627 | clip_height, 628 | clip_height*2, 629 | clip_width], center=true); 630 | } 631 | } 632 | } 633 | } 634 | // Cut off the top (of the interior--to make it the right height) 635 | translate([0,0,height/2+height-dish_depth-dish_thickness+inverted_dish_adjustment]) 636 | cube([length*2,width*2,height], center=true); 637 | } 638 | } 639 | } 640 | // NOTE: ADA compliance calls for ~0.5mm-tall braille dots so that's why there's a -0.5mm below 641 | if (homing_dot_length && homing_dot_width) { // Add "homing dots" 642 | dot_corner_radius = homing_dot_length > homing_dot_width ? homing_dot_width/2.05 : homing_dot_length/2.05; 643 | translate([homing_dot_x, homing_dot_y, height-dish_depth+homing_dot_z-0.5]) 644 | squarish_rpoly( 645 | xy=[homing_dot_length,homing_dot_width], 646 | h=dish_depth, r=dot_corner_radius, center=false); 647 | } 648 | } 649 | } 650 | -------------------------------------------------------------------------------- /legends.scad: -------------------------------------------------------------------------------- 1 | // Legend-related modules 2 | 3 | use 4 | use 5 | use 6 | 7 | // NOTE: Named 'draw_legend' and not just 'legend' to avoid confusion in variable naming 8 | 9 | // Extrudes the legend characters the full height of the keycap so an intersection() and difference() can be performed (inside poly_keycap()) 10 | module draw_legend(chars, size, font, height) { 11 | linear_extrude(height=height) 12 | text(chars, size=size, font=font, valign="center", halign="center"); 13 | } 14 | 15 | // For multi-material prints you can generate *just* the legends in their proper locations: 16 | module just_legends(height=9.0, 17 | dish_tilt=0, dish_tilt_curve=false, polygon_layers=10, 18 | legends=[""], legend_font_sizes=[6], legend_fonts=["Roboto"], 19 | legend_trans=[[0,0,0]], legend_trans2=[[0,0,0]], 20 | legend_rotation=[[0,0,0]], legend_rotation2=[[0,0,0]], 21 | legend_underset=[[0,0,0]], legend_scale=[[1,1,1]], key_rotation=[0,0,0]) { 22 | layer_tilt_adjust = dish_tilt/polygon_layers; 23 | tilt_above_curved = dish_tilt_curve ? layer_tilt_adjust * polygon_layers : 0; 24 | if (legends[0]) { 25 | intersection() { 26 | rotate(key_rotation) union() { 27 | for (i=[0:1:len(legends)-1]) { 28 | legend = legends[i] ? legends[i]: ""; 29 | rotation = legend_rotation[i] ? legend_rotation[i] : (legend_rotation[0] ? legend_rotation[0]: [0,0,0]); 30 | rotation2 = legend_rotation2[i] ? legend_rotation2[i] : (legend_rotation2[0] ? legend_rotation2[0]: [0,0,0]); 31 | trans = legend_trans[i] ? legend_trans[i] : (legend_trans[0] ? legend_trans[0]: [0,0,0]); 32 | trans2 = legend_trans2[i] ? legend_trans2[i] : (legend_trans2[0] ? legend_trans2[0]: [0,0,0]); 33 | font_size = legend_font_sizes[i] ? legend_font_sizes[i]: legend_font_sizes[0]; 34 | font = legend_fonts[i] ? legend_fonts[i] : (legend_fonts[0] ? legend_fonts[0]: "Roboto"); 35 | l_scale = legend_scale[i] ? legend_scale[i] : legend_scale[0]; 36 | underset = legend_underset[i] ? legend_underset[i] : (legend_underset[0] ? legend_underset[0]: [0,0,0]); 37 | translate(underset) translate(trans2) rotate(rotation2) translate(trans) rotate(rotation) 38 | scale(l_scale) rotate([tilt_above_curved,0,0]) 39 | draw_legend(legend, font_size, font, height); 40 | } 41 | } 42 | children(); 43 | } 44 | } else { 45 | note("This keycap has no legends."); 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /scripts/gem_full.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | """ 4 | Generates a whole keyboard's worth of keycaps (and a few extras). Best way 5 | to use this script is from within the `keycap_playground` directory. 6 | 7 | .. bash:: 8 | 9 | $ ./scripts/riskeycap_full.py --out /tmp/output_dir 10 | 11 | .. note:: 12 | 13 | Make sure you add the correct path to colorscad.sh if you want 14 | multi-material keycaps! 15 | 16 | Fonts used by this script: 17 | -------------------------- 18 | 19 | * Gotham Rounded:style=Bold 20 | * Arial Black:style=Regular 21 | * Aharoni 22 | * FontAwesome 23 | * Font Awesome 6 Free:style=Solid 24 | * Hack 25 | * Material Design Icons:style=Regular 26 | * Code2000 27 | * Agave 28 | * DejaVu Sans:style=Bold 29 | * Noto 30 | """ 31 | 32 | # stdlib imports 33 | import os, sys 34 | from pathlib import Path 35 | import json 36 | import argparse 37 | from copy import deepcopy 38 | from subprocess import getstatusoutput 39 | # 3rd party stuff 40 | from colorama import Fore, Back, Style 41 | from colorama import init as color_init 42 | color_init() 43 | # Our own stuff 44 | from keycap import Keycap 45 | 46 | # Change these to the correct paths in your environment: 47 | OPENSCAD_PATH = Path("/home/riskable/downloads/OpenSCAD-2022.12.06.ai12948-x86_64.AppImage") 48 | COLORSCAD_PATH = Path("/home/riskable/downloads/colorscad/colorscad.sh") 49 | 50 | KEY_UNIT = 19.05 # Square that makes up the entire space of a key 51 | BETWEENSPACE = 0.8 # Space between keycaps 52 | FILE_TYPE = "3mf" # 3mf or stl 53 | 54 | class gem_base(Keycap): 55 | """ 56 | Base keycap definitions for GEM profile + our personal prefs. 57 | """ 58 | def __init__(self, **kwargs): 59 | self.openscad_path = OPENSCAD_PATH 60 | self.colorscad_path = COLORSCAD_PATH 61 | super().__init__(**kwargs, 62 | openscad_path=self.openscad_path, 63 | colorscad_path=self.colorscad_path) 64 | self.render = ["keycap", "stem"] 65 | self.file_type = FILE_TYPE 66 | self.key_profile = "gem" 67 | self.key_rotation = [0,108.6,-90] 68 | self.wall_thickness = 0.45*2.25 69 | self.uniform_wall_thickness = True 70 | self.dish_thickness = 1.0 # Note: Not actually used 71 | self.stem_type = "alps" 72 | # self.stem_type = "box_cherry" 73 | self.stem_top_thickness = 0.65 # Note: Not actually used 74 | self.stem_inside_tolerance = 0.175 75 | # Disabled stem side support because it seems it is unnecessary @0.16mm 76 | self.stem_side_supports = [0,0,0,0] 77 | self.stem_locations = [[0,0,0]] 78 | self.stem_walls_inset = 0 79 | self.stem_sides_wall_thickness = 0.8; # Thick (good sound/feel) 80 | # Because we do strange things we need legends bigger on the Z 81 | self.scale = [ 82 | [1,1,3], 83 | [1,1.75,3], # For the pipe to make it taller/more of a divider 84 | [1,1,3], 85 | ] 86 | self.fonts = [ 87 | "Gotham Rounded:style=Bold", 88 | "Gotham Rounded:style=Bold", 89 | "Arial Black:style=Regular", 90 | ] 91 | self.font_sizes = [ 92 | 5.5, 93 | 4, # Gotham Rounded second legend (top right) 94 | 4, # Front legend 95 | ] 96 | self.trans = [ 97 | [-3,-2.6,2], # Lower left corner Gotham Rounded 98 | [3.5,3,1], # Top right Gotham Rounded 99 | [0.15,-3,2], # Front legend 100 | ] 101 | self.rotation = [ 102 | [0,-20,0], 103 | [0,-20,0], 104 | [68,0,0], 105 | ] 106 | self.postinit(**kwargs) 107 | 108 | class gem_alphas(gem_base): 109 | """ 110 | Basic alphanumeric characters (centered, big legend) 111 | """ 112 | def __init__(self, **kwargs): 113 | super().__init__(**kwargs) 114 | self.font_sizes = [ 115 | 4.5, # Regular Gotham Rounded 116 | 4, 117 | 4, # Front legend 118 | ] 119 | self.trans = [ 120 | [2.6,0,0], # Centered when angled -20° 121 | [3.5,3,1], # Top right Gotham Rounded 122 | [0.15,-3,2], # Front legend 123 | ] 124 | self.postinit(**kwargs) 125 | 126 | class gem_alphas_homing_dot(gem_alphas): 127 | """ 128 | Basic alphanumeric characters (centered, big legend) with a homing "dot" 129 | e.g. for F and J. 130 | """ 131 | def __init__(self, **kwargs): 132 | super().__init__(**kwargs) 133 | self.homing_dot_length = 3 134 | self.homing_dot_width = 1 135 | self.homing_dot_x = 0 136 | self.homing_dot_y = -3 137 | self.homing_dot_z = -0.45 138 | 139 | class gem_numrow(gem_base): 140 | """ 141 | Number row numbers are slightly different 142 | """ 143 | def __init__(self, **kwargs): 144 | super().__init__(**kwargs) 145 | self.fonts = [ 146 | "Gotham Rounded:style=Bold", # Main char 147 | "Gotham Rounded:style=Bold", # Pipe character 148 | "Gotham Rounded:style=Bold", # Symbol 149 | "Arial Black:style=Regular", # F-key 150 | ] 151 | self.font_sizes = [ 152 | 4.5, # Regular character 153 | 4.5, # Pipe 154 | 4.5, # Regular Gotham Rounded symbols 155 | 3.5, # Front legend 156 | ] 157 | self.trans = [ 158 | [-0.3,0,0], # Left Gotham Rounded 159 | [2.6,0,0], # Center Gotham Rounded | 160 | [5,0,1], # Right-side Gotham symbols 161 | [0.15,-2,2], # F-key 162 | ] 163 | self.rotation = [ 164 | [0,-20,0], 165 | [0,-20,0], 166 | [0,-20,0], 167 | [68,0,0], 168 | ] 169 | self.scale = [ 170 | [1,1,3], 171 | [1,1.75,3], # For the pipe to make it taller/more of a divider 172 | [1,1,3], 173 | [1,1,3], 174 | ] 175 | self.postinit(**kwargs) 176 | 177 | class gem_tilde(gem_numrow): 178 | """ 179 | Tilde needs some changes because by default it's too small 180 | """ 181 | def __init__(self, **kwargs): 182 | super().__init__(**kwargs) 183 | self.font_sizes[0] = 6.5 # ` symbol 184 | self.font_sizes[2] = 5.5 # ~ symbol 185 | self.trans[0] = [-0.3,2.7,0] # ` 186 | self.trans[2] = [5.5,0,1] # ~ 187 | 188 | class gem_2(gem_numrow): 189 | """ 190 | 2 needs some changes based on the @ symbol 191 | """ 192 | def __init__(self, **kwargs): 193 | super().__init__(**kwargs) 194 | self.fonts[2] = "Aharoni" 195 | self.font_sizes[2] = 6.1 # @ symbol (Aharoni) 196 | self.trans[2] = [4.85,0,1] 197 | self.scale[2] = [0.75,1,3] # Squash it a bit 198 | 199 | class gem_3(gem_numrow): 200 | """ 201 | 3 needs some changes based on the # symbol (slightly too big) 202 | """ 203 | def __init__(self, **kwargs): 204 | super().__init__(**kwargs) 205 | self.font_sizes[2] = 4.5 # # symbol (Gotham Rounded) 206 | self.trans[0] = [-0.35,0,0] # Just a smidge different so it prints better 207 | self.trans[2] = [5.3,0,1] # Move to the right a bit 208 | 209 | class gem_5(gem_numrow): 210 | """ 211 | 5 needs some changes based on the % symbol (too big, too close to bar) 212 | """ 213 | def __init__(self, **kwargs): 214 | super().__init__(**kwargs) 215 | self.font_sizes[2] = 4 # % symbol 216 | self.trans[2] = [5.2,0,1] 217 | 218 | class gem_6(gem_numrow): 219 | """ 220 | 6 needs some changes based on the ^ symbol (too small, should be up high) 221 | """ 222 | def __init__(self, **kwargs): 223 | super().__init__(**kwargs) 224 | self.font_sizes[2] = 5.8 # ^ symbol 225 | self.trans[2] = [5.3,1.5,1] 226 | 227 | class gem_7(gem_numrow): 228 | """ 229 | 7 needs some changes based on the & symbol (it's too big) 230 | """ 231 | def __init__(self, **kwargs): 232 | super().__init__(**kwargs) 233 | self.font_sizes[2] = 4.5 # & symbol 234 | self.trans[2] = [5.2,0,1] 235 | 236 | class gem_8(gem_numrow): 237 | """ 238 | 8 needs some changes based on the tiny * symbol 239 | """ 240 | def __init__(self, **kwargs): 241 | super().__init__(**kwargs) 242 | self.font_sizes[2] = 8.5 # * symbol (Gotham Rounded) 243 | self.trans[2] = [5.2,0,1] # * needs a smidge of repositioning 244 | 245 | class gem_equal(gem_numrow): 246 | """ 247 | = needs some changes because it and the + are a bit off center 248 | """ 249 | def __init__(self, **kwargs): 250 | super().__init__(**kwargs) 251 | self.trans[0] = [-0.3,-0.5,0] # = sign adjustments 252 | self.trans[2] = [5,-0.3,1] # + sign adjustments 253 | 254 | class gem_dash(gem_numrow): 255 | """ 256 | The dash (-) is fine but the underscore (_) needs minor repositioning. 257 | """ 258 | def __init__(self, **kwargs): 259 | super().__init__(**kwargs) 260 | self.trans[2] = [5.2,-1,1] # _ needs to go down and to the right a bit 261 | self.scale[2] = [0.8,1,3] # Also needs to be squished a bit 262 | 263 | class gem_double_legends(gem_base): 264 | """ 265 | For regular keys that have two legends... ,./;'[] 266 | """ 267 | def __init__(self, **kwargs): 268 | super().__init__(**kwargs) 269 | self.fonts = [ 270 | "Gotham Rounded:style=Bold", # Main legend 271 | "Gotham Rounded:style=Bold", # Pipe character 272 | "Gotham Rounded:style=Bold", # Second legend 273 | ] 274 | self.font_sizes = [ 275 | 4.5, # Regular Gotham Rounded character 276 | 4.5, # Pipe 277 | 4.5, # Regular Gotham Rounded character 278 | ] 279 | self.trans = [ 280 | [-0.3,0,0], # Left Gotham Rounded 281 | [2.6,0,0], # Center Gotham Rounded | 282 | [5,0,1], # Right-side Gotham symbols 283 | ] 284 | self.rotation = [ 285 | [0,-20,0], 286 | [0,-20,0], 287 | [0,-20,0], 288 | ] 289 | self.scale = [ 290 | [1,1,3], 291 | [1,1.75,3], # For the pipe to make it taller/more of a divider 292 | [1,1,3], 293 | ] 294 | self.postinit(**kwargs) 295 | 296 | class gem_gt_lt(gem_double_legends): 297 | """ 298 | The greater than (>) and less than (<) signs need to be adjusted down a bit 299 | """ 300 | def __init__(self, **kwargs): 301 | super().__init__(**kwargs) 302 | self.trans[0] = [-0.3,-0.1,0] # , and . are the tiniest bit too high 303 | self.trans[2] = [5.2,-0.35,1] # < and > are too high for some reason 304 | 305 | class gem_brackets(gem_double_legends): 306 | """ 307 | The curly braces `{}` needs to be moved to the right a smidge 308 | """ 309 | def __init__(self, **kwargs): 310 | super().__init__(**kwargs) 311 | self.trans[2] = [5.2,0,1] # Just a smidge to the right 312 | 313 | class gem_semicolon(gem_double_legends): 314 | """ 315 | The semicolon ends up being slightly higher than the colon but it looks 316 | better if the top dot in both is aligned. 317 | """ 318 | def __init__(self, **kwargs): 319 | super().__init__(**kwargs) 320 | self.trans[0] = [0.2,-0.4,0] 321 | self.trans[2] = [4.7,0,1] 322 | 323 | class gem_1_U_text(gem_alphas): 324 | """ 325 | Ctrl, Del, and Ins need to be downsized and moved a smidge. 326 | """ 327 | def __init__(self, **kwargs): 328 | super().__init__(**kwargs) 329 | kwargs_copy = deepcopy(kwargs) 330 | self.font_sizes[0] = 4 331 | self.trans[0] = [2.6,0,0] 332 | self.postinit(**kwargs_copy) 333 | 334 | 335 | # For some reason this isn't working (it's rotating the FontAwesome icon when it shouldn't be) so I've disabled it for now: 336 | #class gem_osha(gem_alphas): 337 | #""" 338 | #OSHA No Entry key is special in that it has overlapping stuff 339 | #""" 340 | #def __init__(self, **kwargs): 341 | #super().__init__(**kwargs) 342 | #self.font_sizes = [2.35, 8.5] 343 | #self.trans = [ 344 | #[2.6,-0.1,0], 345 | #[2.6,-0.1,0], 346 | #[0,0,0], 347 | #] 348 | #self.fonts = [ 349 | #"Gotham Rounded:style=Bold", # Main legend 350 | #"FontAwesome", 351 | #"Gotham Rounded:style=Bold", 352 | #] 353 | #self.rotation = [ 354 | #[-15,-13,45], 355 | #[-15,-13,45], 356 | #[0,-20,0], 357 | #] 358 | #self.trans2 = [ 359 | #[0,-0.7,0], 360 | #[0,0,0], 361 | #[0,0,0], 362 | #] 363 | #self.scale = [ 364 | #[1,1,3], 365 | #[1,1,3], # Back to normal 366 | #[1,1,3], 367 | #] 368 | 369 | class gem_1_U_2_row_text(gem_alphas): 370 | """ 371 | Scroll Lock, Page Up, Page Down, etc need a bunch of changes. 372 | """ 373 | def __init__(self, **kwargs): 374 | super().__init__(**kwargs) 375 | kwargs_copy = deepcopy(kwargs) 376 | self.font_sizes[0] = 2.5 377 | self.font_sizes[1] = 2.5 378 | self.trans[0] = [2.6,2,0] 379 | self.trans[1] = [2.6,-2,0] 380 | self.scale = [ 381 | [1,1,3], 382 | [1,1,3], # Back to normal 383 | [1,1,3], 384 | ] 385 | self.postinit(**kwargs_copy) 386 | 387 | class gem_arrows(gem_alphas): 388 | """ 389 | Arrow symbols (◀▶▲▼) needs a different font (Hack) 390 | """ 391 | def __init__(self, **kwargs): 392 | super().__init__(**kwargs) 393 | self.fonts[0] = "Hack" 394 | self.fonts[2] = "Font Awesome 6 Free:style=Solid" # For next/prev track icons 395 | self.font_sizes[0] = 6 # Nice and big 396 | self.font_sizes[2] = 4 # FontAwesome prev/next icons 397 | self.trans[2] = [0,-2,2] # Ditto 398 | 399 | class gem_fontawesome(gem_alphas): 400 | """ 401 | For regular centered FontAwesome icon keycaps. 402 | """ 403 | def __init__(self, **kwargs): 404 | super().__init__(**kwargs) 405 | kwargs_copy = deepcopy(kwargs) # Because self.trans[0] updates in place 406 | self.fonts[0] = "Font Awesome 6 Free:style=Solid" 407 | self.font_sizes[0] = 6 408 | self.trans[0] = [2.75,0,0] 409 | self.postinit(**kwargs_copy) 410 | 411 | class gem_material_icons(gem_alphas): 412 | """ 413 | For regular centered Material Design icon keycaps. 414 | """ 415 | def __init__(self, **kwargs): 416 | super().__init__(**kwargs) 417 | kwargs_copy = deepcopy(kwargs) # Because self.trans[0] updates in place 418 | self.fonts[0] = "Material Design Icons:style=Regular" 419 | self.font_sizes[0] = 6 420 | self.trans[0] = [2.6,0.3,0] 421 | self.postinit(**kwargs_copy) 422 | 423 | class gem_1_25U(gem_alphas): 424 | """ 425 | The base for all 1.25U keycaps. 426 | """ 427 | def __init__(self, **kwargs): 428 | super().__init__(**kwargs) 429 | kwargs_copy = deepcopy(kwargs) # Because self.trans[0] updates in place 430 | self.key_length = KEY_UNIT*1.25-BETWEENSPACE 431 | self.key_rotation = [0,108.55,-90] 432 | self.trans[0] = [2.5,0.3,0] 433 | self.postinit(**kwargs_copy) 434 | if not self.name.startswith('1.25U_'): 435 | self.name = f"1.25U_{self.name}" 436 | 437 | class gem_1_5U(gem_double_legends): 438 | """ 439 | The base for all 1.5U keycaps. 440 | 441 | .. note:: Uses gem_double_legends because of the \\| key. 442 | """ 443 | def __init__(self, **kwargs): 444 | super().__init__(**kwargs) 445 | kwargs_copy = deepcopy(kwargs) # Because self.trans[0] updates in place 446 | self.key_length = KEY_UNIT*1.5-BETWEENSPACE 447 | self.key_rotation = [0,107.825,-90] 448 | self.trans[0] = [3,0.3,0] 449 | self.postinit(**kwargs_copy) 450 | if not self.name.startswith('1.5U_'): 451 | self.name = f"1.5U_{self.name}" 452 | 453 | class gem_bslash_1U(gem_double_legends): 454 | """ 455 | Backslash key needs a very minor adjustment to the backslash. 456 | """ 457 | def __init__(self, **kwargs): 458 | super().__init__(**kwargs) 459 | self.trans[0] = [-0.9,0,0] # Move \ to the left a bit more than normal 460 | 461 | class gem_bslash(gem_1_5U): 462 | """ 463 | Backslash key needs a very minor adjustment to the backslash. 464 | """ 465 | def __init__(self, **kwargs): 466 | super().__init__(**kwargs) 467 | self.trans[0] = [-0.9,0,0] # Move \ to the left a bit more than normal 468 | 469 | class gem_tab(gem_1_5U): 470 | """ 471 | "Tab" needs to be centered. 472 | """ 473 | def __init__(self, **kwargs): 474 | super().__init__(**kwargs) 475 | self.font_sizes[0] = 4.5 # Regular Gotham Rounded 476 | self.trans[0] = [3.25,0.3,0] # Centered when angled -20° 477 | 478 | class gem_1_75U(gem_alphas): 479 | """ 480 | The base for all 1.75U keycaps. 481 | """ 482 | def __init__(self, **kwargs): 483 | super().__init__(**kwargs) 484 | kwargs_copy = deepcopy(kwargs) 485 | self.key_length = KEY_UNIT*1.75-BETWEENSPACE 486 | self.key_rotation = [0,107.85,-90] 487 | self.trans[0] = [3,0.3,0] 488 | self.postinit(**kwargs_copy) 489 | if not self.name.startswith('1.75U_'): 490 | self.name = f"1.75U_{self.name}" 491 | 492 | class gem_2U(gem_alphas): 493 | """ 494 | The base for all 2U keycaps. 495 | """ 496 | def __init__(self, **kwargs): 497 | super().__init__(**kwargs) 498 | kwargs_copy = deepcopy(kwargs) 499 | self.key_length = KEY_UNIT*2-BETWEENSPACE 500 | self.key_rotation = [0,107.85,-90] # Same as 1.75U 501 | if "dish_invert" in kwargs and kwargs["dish_invert"]: 502 | self.key_rotation = [0,111.88,-90] # Spacebars are different 503 | self.stem_locations = [[0,0,0], [12,0,0], [-12,0,0]] 504 | self.postinit(**kwargs_copy) 505 | if not self.name.startswith('2U_'): 506 | self.name = f"2U_{self.name}" 507 | 508 | class gem_2UV(gem_alphas): 509 | """ 510 | The base for all 2U keycaps. 511 | """ 512 | def __init__(self, **kwargs): 513 | super().__init__(**kwargs) 514 | self.key_length = KEY_UNIT*1-BETWEENSPACE 515 | self.key_width = KEY_UNIT*2-BETWEENSPACE 516 | self.key_rotation = [0,107.85,-90] # Same as 2U 517 | if "dish_invert" in kwargs and kwargs["dish_invert"]: 518 | self.key_rotation = [0,111.88,-90] # Spacebars are different 519 | self.stem_locations = [[0,0,0], [0,12,0], [0,-12,0]] 520 | self.trans = [ 521 | [0.1,-0.1,0], # Left Gotham Rounded 522 | [0,0,0], # Unused (so far) 523 | [0,0,0], # Ditto 524 | ] 525 | self.rotation = [ 526 | [0,0,0], 527 | [0,0,0], 528 | [0,0,0], 529 | ] 530 | self.postinit(**kwargs) 531 | if not self.name.startswith('2UV_'): 532 | self.name = f"2UV_{self.name}" 533 | 534 | class gem_2_25U(gem_alphas): 535 | """ 536 | The base for all 2.25U keycaps. 537 | """ 538 | def __init__(self, **kwargs): 539 | super().__init__(**kwargs) 540 | kwargs_copy = deepcopy(kwargs) 541 | self.key_length = KEY_UNIT*2.25-BETWEENSPACE 542 | self.key_rotation = [0,107.85,-90] # Same as 1.75U and 2U 543 | if "dish_invert" in kwargs and kwargs["dish_invert"]: 544 | self.key_rotation = [0,111.88,-90] # Spacebars are different 545 | self.stem_locations = [[0,0,0], [12,0,0], [-12,0,0]] 546 | self.trans[0] = [3.1,0.2,0] 547 | self.font_sizes[0] = 4 548 | self.postinit(**kwargs_copy) 549 | if not self.name.startswith('2.25U_'): 550 | self.name = f"2.25U_{self.name}" 551 | 552 | class gem_2_5U(gem_alphas): 553 | """ 554 | The base for all 2.5U keycaps. 555 | """ 556 | def __init__(self, **kwargs): 557 | super().__init__(**kwargs) 558 | kwargs_copy = deepcopy(kwargs) 559 | self.key_length = KEY_UNIT*2.5-BETWEENSPACE 560 | self.key_rotation = [0,107.85,-90] # Same as 1.75U and 2U 561 | if "dish_invert" in kwargs and kwargs["dish_invert"]: 562 | self.key_rotation = [0,111.88,-90] # Spacebars are different 563 | self.stem_locations = [[0,0,0], [12,0,0], [-12,0,0]] 564 | self.trans[0] = [3.1,0.2,0] 565 | self.font_sizes[0] = 4 566 | self.postinit(**kwargs_copy) 567 | if not self.name.startswith('2.5U_'): 568 | self.name = f"2.5U_{self.name}" 569 | 570 | class gem_2_75U(gem_alphas): 571 | """ 572 | The base for all 2.75U keycaps. 573 | """ 574 | def __init__(self, **kwargs): 575 | super().__init__(**kwargs) 576 | kwargs_copy = deepcopy(kwargs) 577 | self.key_length = KEY_UNIT*2.75-BETWEENSPACE 578 | self.key_rotation = [0,107.85,-90] # Same as 1.75U and 2U 579 | if "dish_invert" in kwargs and kwargs["dish_invert"]: 580 | self.key_rotation = [0,111.88,-90] # Spacebars are different 581 | self.stem_locations = [[0,0,0], [12,0,0], [-12,0,0]] 582 | self.trans[0] = [3.1,0.2,0] 583 | self.font_sizes[0] = 4 584 | self.postinit(**kwargs_copy) 585 | if not self.name.startswith('2.75U_'): 586 | self.name = f"2.75U_{self.name}" 587 | 588 | class gem_6_25U(gem_alphas): 589 | """ 590 | The base for all 6.25U keycaps. 591 | """ 592 | def __init__(self, **kwargs): 593 | super().__init__(**kwargs) 594 | kwargs_copy = deepcopy(kwargs) 595 | self.key_length = KEY_UNIT*6.25-BETWEENSPACE 596 | self.key_rotation = [0,107.85,-90] # Same as 1.75U and 2U 597 | if "dish_invert" in kwargs and kwargs["dish_invert"]: 598 | self.key_rotation = [0,111.88,-90] # Spacebars are different 599 | self.stem_locations = [[0,0,0], [50,0,0], [-50,0,0]] 600 | self.trans[0] = [3.1,0.2,0] 601 | self.font_sizes[0] = 4 602 | self.postinit(**kwargs_copy) 603 | if not self.name.startswith('6.25U_'): 604 | self.name = f"6.25U_{self.name}" 605 | 606 | class gem_7U(gem_alphas): 607 | """ 608 | The base for all 7U keycaps. 609 | """ 610 | def __init__(self, **kwargs): 611 | super().__init__(**kwargs) 612 | kwargs_copy = deepcopy(kwargs) 613 | self.key_length = KEY_UNIT*7-BETWEENSPACE 614 | self.key_rotation = [0,107.85,-90] # Same as 1.75U and 2U 615 | if "dish_invert" in kwargs and kwargs["dish_invert"]: 616 | self.key_rotation = [0,111.88,-90] # Spacebars are different 617 | self.stem_locations = [[0,0,0], [57,0,0], [-57,0,0]] 618 | self.trans[0] = [3.1,0.2,0] 619 | self.font_sizes[0] = 4 620 | self.postinit(**kwargs_copy) 621 | if not self.name.startswith('7U_'): 622 | self.name = f"7U_{self.name}" 623 | 624 | KEYCAPS = [ 625 | # 1U keys 626 | gem_base(name="1U_blank"), 627 | gem_tilde(name="tilde", legends=["`", "", "~"]), 628 | gem_numrow(legends=["1", "", "!"]), 629 | gem_2(legends=["2", "", "@"]), 630 | gem_3(legends=["3", "", "#"]), 631 | gem_numrow(legends=["4", "", "$"]), 632 | gem_5(legends=["5", "", "%"]), 633 | gem_6(legends=["6", "", "^"]), 634 | gem_7(legends=["7", "", "&"]), 635 | gem_8(legends=["8", "", "*"]), 636 | gem_numrow(legends=["9", "", "("]), 637 | gem_numrow(legends=["0", "", ")"]), 638 | gem_dash(name="dash", legends=["-", "", "_"]), 639 | gem_equal(name="equal", legends=["=", "", "+"]), 640 | gem_alphas(legends=["A"]), 641 | gem_alphas(legends=["B"]), 642 | gem_alphas(legends=["C"]), 643 | gem_alphas(legends=["D"]), 644 | gem_alphas(legends=["E"]), 645 | gem_alphas(legends=["F"]), 646 | gem_alphas_homing_dot(name="F_dot", legends=["F"]), 647 | gem_alphas(legends=["G"]), 648 | gem_alphas(legends=["H"]), 649 | gem_alphas(legends=["I"]), 650 | gem_alphas(legends=["J"]), 651 | gem_alphas_homing_dot(name="J_dot", legends=["J"]), 652 | gem_alphas(legends=["K"]), 653 | gem_alphas(legends=["L"]), 654 | gem_alphas(legends=["M"]), 655 | gem_alphas(legends=["N"]), 656 | gem_alphas(legends=["O"]), 657 | gem_alphas(legends=["P"]), 658 | gem_alphas(legends=["Q"]), 659 | gem_alphas(legends=["R"]), 660 | gem_alphas(legends=["S"]), 661 | gem_alphas(legends=["T"]), 662 | gem_alphas(legends=["U"]), 663 | gem_alphas(legends=["V"]), 664 | gem_alphas(legends=["W"]), 665 | gem_alphas(legends=["X"]), 666 | gem_alphas(legends=["Y"]), 667 | gem_alphas(legends=["Z"]), 668 | gem_alphas(legends=["Z"]), 669 | # Function keys 670 | gem_alphas(legends=["F1"]), 671 | gem_alphas(legends=["F2"]), 672 | gem_alphas(legends=["F3"]), 673 | gem_alphas(legends=["F4"]), 674 | gem_alphas(legends=["F5"]), 675 | gem_alphas(legends=["F6"]), 676 | gem_alphas(legends=["F7"]), 677 | gem_alphas(legends=["F8"]), 678 | gem_alphas(legends=["F9"]), 679 | gem_alphas(legends=["F10"], font_sizes=[4.25], trans=[[2.4,0,0]]), 680 | gem_alphas(legends=["F11"], font_sizes=[4.25]), 681 | gem_alphas(legends=["F12"], font_sizes=[4.25]), 682 | # Bottom row(s) and sides 1U 683 | gem_alphas(name="menu", legends=["☰"], fonts=["Code2000"]), 684 | gem_alphas(name="option1U", legends=["⌥"], 685 | fonts=["JetBrainsMono Nerd Font"], font_sizes=[6]), 686 | gem_arrows(name="left_prev", legends=["◀", "", ""]), 687 | gem_arrows(name="right_next", legends=["▶", "", ""]), 688 | gem_arrows(name="left", legends=["◀", "", ""]), 689 | gem_arrows(name="right", legends=["▶", "", ""]), 690 | gem_arrows(name="up", legends=["▲", "", ""]), 691 | gem_arrows(name="down", legends=["▼", "", "", ""]), 692 | gem_fontawesome(name="eject", legends=[""]), # For Macs 693 | gem_fontawesome(name="camera", legends=[""]), # aka screenshot 694 | gem_fontawesome(name="bug", legends=[""]), # Just for fun 695 | gem_fontawesome(name="paws", legends=[""]), # Alternate "Paws" key (hehe) 696 | gem_fontawesome(name="home_icon", legends=[""]), # Alternate "Home" key 697 | gem_fontawesome(name="broom", legends=[""], font_sizes=[5.5]), 698 | gem_fontawesome(name="dragon", legends=[""], font_sizes=[5.5]), 699 | gem_fontawesome(name="baby", legends=[""]), 700 | gem_fontawesome(name="dungeon", legends=[""]), 701 | gem_fontawesome(name="wizard", legends=[""]), 702 | gem_fontawesome(name="headset", legends=[""]), 703 | gem_fontawesome(name="skull_n_bones", legends=[""]), 704 | gem_fontawesome(name="bath", legends=[""]), 705 | gem_fontawesome(name="keyboard", legends=[""]), 706 | gem_fontawesome(name="terminal", legends=[""]), 707 | gem_fontawesome(name="spy", legends=[""]), 708 | gem_fontawesome(name="biohazard", legends=[""]), 709 | gem_fontawesome(name="bandage", legends=[""], font_sizes=[5.5]), 710 | gem_fontawesome(name="bone", legends=[""]), 711 | gem_fontawesome(name="cannabis", legends=[""]), 712 | gem_fontawesome(name="radiation", legends=[""]), 713 | gem_fontawesome(name="crutch", legends=[""]), 714 | gem_fontawesome(name="head_side_cough", legends=[""], font_sizes=[5.5]), 715 | gem_fontawesome(name="mortar_and_pestle", legends=[""]), 716 | gem_fontawesome(name="poop", legends=[""]), 717 | gem_fontawesome(name="bomb", legends=[""]), 718 | gem_fontawesome(name="thunderstorm", legends=[""]), 719 | gem_fontawesome(name="dumpster_fire", legends=[""], font_sizes=[5.5]), 720 | gem_fontawesome(name="flask", legends=[""]), 721 | gem_fontawesome(name="middle_finger", legends=[""]), 722 | gem_fontawesome(name="hurricane", legends=[""]), 723 | gem_fontawesome(name="light_bulb", legends=[""]), 724 | gem_fontawesome(name="male", legends=[""]), 725 | gem_fontawesome(name="female", legends=[""]), 726 | gem_fontawesome(name="microphone", legends=[""]), 727 | gem_fontawesome(name="person_falling", legends=[""]), 728 | gem_fontawesome(name="shitstorm", legends=[""]), 729 | gem_fontawesome(name="toilet", legends=[""]), 730 | gem_fontawesome(name="wifi", legends=[""], font_sizes=[5.5]), 731 | gem_fontawesome(name="yinyang", legends=[""]), 732 | gem_fontawesome(name="ban", legends=[""]), 733 | gem_fontawesome(name="lemon", legends=[""], font_sizes=[6]), 734 | gem_material_icons(name="duck", legends=[""], font_sizes=[7]), 735 | gem_alphas(name="die_1", legends=["⚀"], font_sizes=[7], fonts=["DejaVu Sans:style=Bold"]), # Dice (number alternate) 736 | gem_alphas(name="die_2", legends=["⚁"], font_sizes=[7], fonts=["DejaVu Sans:style=Bold"]), # Dice (number alternate) 737 | gem_alphas(name="die_3", legends=["⚂"], font_sizes=[7], fonts=["DejaVu Sans:style=Bold"]), # Dice (number alternate) 738 | gem_alphas(name="die_4", legends=["⚃"], font_sizes=[7], fonts=["DejaVu Sans:style=Bold"]), # Dice (number alternate) 739 | gem_alphas(name="die_5", legends=["⚄"], font_sizes=[7], fonts=["DejaVu Sans:style=Bold"]), # Dice (number alternate) 740 | gem_alphas(name="die_6", legends=["⚅"], font_sizes=[7], fonts=["DejaVu Sans:style=Bold"]), # Dice (number alternate) 741 | gem_1_U_text(name="RCtrl", legends=["Ctrl"], font_sizes=[4.25]), 742 | gem_1_U_text(legends=["Del"]), 743 | gem_1_U_text(legends=["Ins"]), 744 | gem_1_U_text(legends=["Esc"]), 745 | gem_1_U_text(legends=["End"]), 746 | gem_1_U_text(legends=["BRB"], scale=[[0.75,1,3]]), 747 | gem_1_U_text(legends=["OMG"], font_sizes=[3.75], scale=[[0.75,1,3]]), 748 | gem_1_U_text(legends=["WTF"], font_sizes=[3.75], scale=[[0.75,1,3]]), 749 | gem_1_U_text(legends=["BBL"], scale=[[0.75,1,3]]), 750 | gem_1_U_text(legends=["CYA"], scale=[[0.75,1,3]]), 751 | gem_1_U_text(legends=["IDK"], scale=[[0.75,1,3]]), 752 | gem_1_U_text(legends=["ASS"], scale=[[0.75,1,3]]), 753 | gem_1_U_text(legends=["ANY", "", "KEY"], scale=[[0.75,1,3]], fonts = [ 754 | "Gotham Rounded:style=Bold", 755 | "Gotham Rounded:style=Bold", 756 | "Gotham Rounded:style=Bold", 757 | ], font_sizes=[4, 4, 4.15]), # 4.15 here works around a minor slicing issue 758 | gem_1_U_text(legends=["OK"]), 759 | gem_1_U_text(legends=["NO"]), 760 | gem_1_U_text(legends=["Yes"]), 761 | gem_1_U_text(legends=["DO"]), 762 | gem_1_U_2_row_text(name="DO_NOT", legends=["DO", "NOT"], 763 | trans=[[2.7,2.75,0],[2.7,-2,0]], font_sizes=[3.5, 3.5], 764 | scale=[[0.9,1,3]]), 765 | #gem_osha(legends=["OSHA", ""]), 766 | gem_1_U_text(legends=["FUBAR"], font_sizes=[3.25], scale=[[0.55,1,3]]), 767 | gem_1_U_text(legends=["Home"], font_sizes=[2.75]), 768 | gem_1_U_2_row_text(name="PageUp", legends=["Page", "Up"], font_sizes=[2.75, 2.75]), 769 | gem_1_U_2_row_text(name="PageDown", legends=["Page", "Down"], font_sizes=[2.75, 2.75]), 770 | gem_1_U_text(legends=["Pause"], font_sizes=[2.5]), 771 | gem_1_U_2_row_text(name="ScrollLock", legends=["Scroll", "Lock"]), 772 | gem_1_U_text(legends=["Sup"]), 773 | gem_brackets(name="lbracket", legends=["[", "", "{"]), 774 | gem_brackets(name="rbracket", legends=["]", "", "}"]), 775 | gem_semicolon(name="semicolon", legends=[";", "", ":"]), 776 | gem_double_legends(name="quote", legends=["'", "", '\\u0022']), 777 | gem_gt_lt(name="comma", legends=[",", "", "<"]), 778 | gem_gt_lt(name="dot", legends=[".", "", ">"]), 779 | gem_double_legends(name="slash", legends=["/", "", "?"]), 780 | # 60% and smaller numrow (with function key legends on the front) 781 | gem_numrow(name="1_F1", legends=["1", "", "!", "F1"]), 782 | gem_2(name="2_F2", legends=["2", "", "@", "F2"]), 783 | gem_3(name="3_F3", legends=["3", "", "#", "F3"]), 784 | gem_numrow(name="4_F4", legends=["4", "", "$", "F4"]), 785 | gem_5(name="5_F5", legends=["5", "", "%", "F5"]), 786 | gem_numrow(name="6_F6", legends=["6", "", "^", "F6"]), 787 | gem_7(name="7_F7", legends=["7", "", "&", "F7"]), 788 | gem_8(name="8_F8", legends=["8", "", "*", "F8"]), 789 | gem_numrow(name="9_F9", legends=["9", "", "(", "F9"]), 790 | gem_numrow(name="0_F10", legends=["0", "", ")", "F10"]), 791 | gem_dash(name="dash_F11", legends=["-", "", "_", "F11"]), 792 | gem_equal(name="equal_F12", legends=["=", "", "+", "F12"]), 793 | # 1.25U keys 794 | gem_1_25U(name="blank"), 795 | gem_1_25U(name="LCtrl", legends=["Ctrl"], trans=[[3,0.3,0]], font_sizes=[4.35]), 796 | gem_1_25U(name="LAlt", legends=["Alt"], trans=[[3,0.3,0]], font_sizes=[4.5]), 797 | gem_1_25U(name="RAlt", legends=["Alt Gr"], trans=[[3,0.3,0]], font_sizes=[3.75], scale=[[0.9,1,3]]), 798 | gem_1_25U(name="Command", legends=["Cmd"], font_sizes=[4]), 799 | gem_1_25U(name="CommandSymbol", legends=["⌘"], font_sizes=[7], fonts=["Agave"]), 800 | gem_1_25U(name="OptionSymbol", legends=["⌥"], font_sizes=[6], fonts=["JetBrainsMono Nerd Font"]), 801 | gem_1_25U(name="Option", legends=["Option"], font_sizes=[2.9]), 802 | gem_1_25U(name="Fun", legends=["Fun"], font_sizes=[4.5]), 803 | gem_1_25U(name="Sup", legends=["Sup"], font_sizes=[4.5]), 804 | gem_1_25U(name="MoreFun", 805 | legends=["More", "Fun"], 806 | trans=[[3,2.5,0], [3,-2.5,0]], 807 | font_sizes=[4.15, 4.15], 808 | scale=[[1,1,3], [1,1,3]]), 809 | gem_1_25U(legends=["Super", "Duper"], 810 | trans=[[3,2.25,0], [3,-2.25,0]], font_sizes=[3.25, 3.25], 811 | scale=[[1,1,3], [1,1,3]]), 812 | # 1.5U keys 813 | gem_1_5U(name="blank"), 814 | gem_bslash_1U(name="bslash", legends=["\\u005c", "", "|"]), 815 | gem_bslash(name="bslash", legends=["\\u005c", "", "|"]), 816 | gem_tab(name="Tab", legends=["Tab"]), 817 | gem_1_5U(name="LAlt", legends=["Alt"], trans=[[3.5,0.3,0]], font_sizes=[4.5]), 818 | gem_1_5U(name="RAlt", legends=["Alt Gr"], trans=[[3,0.3,0]], font_sizes=[4.5]), 819 | # 1.75U keys 820 | gem_1_75U(name="blank"), 821 | gem_1_75U(legends=["Compose"], 822 | trans=[[3.1,0.3,0]], font_sizes=[3.25]), 823 | gem_1_75U(name="CapsLock", 824 | legends=["CAPS LOCK"], trans=[[3.1,0.3,0]], font_sizes=[3], scale=[[0.9,1,3]]), 825 | gem_1_75U(name="CAPS", 826 | legends=["CAPS"], trans=[[3.1,0.3,0]]), 827 | gem_1_75U(name="Rub1Out", legends=["Rub 1 Out"], font_sizes=[3.5], scale=[[0.9,1,3]]), 828 | gem_1_75U(name="RubOut", legends=["Rub Out"], font_sizes=[3.5]), 829 | # 2U keys 830 | gem_2U(name="blank"), 831 | gem_2U(name="TOTALBS", 832 | legends=["TOTAL BS"], font_sizes=[3.75, 3.75]), 833 | gem_2U(name="Backspace", font_sizes=[3.75, 3.75]), 834 | gem_2U(name="2U_space", 835 | # Spacebars don't need to be as thick 836 | stem_sides_wall_thickness=0.0, 837 | key_rotation=[0,111.88,-90], dish_invert=True), 838 | # 2.25U keys 839 | gem_2_25U(name="blank"), 840 | gem_2_25U(name="Shift", legends=["Shift"]), 841 | gem_2_25U(name="ShiftyShift", 842 | legends=["Shift"], trans=[[9.5,-2.8,0]]), 843 | gem_2_25U(name="ShiftyShiftL", 844 | legends=["Shift"], trans=[[-4,-2.8,0]]), 845 | gem_2_25U(name="TrueShift", legends=["True Shift"]), 846 | gem_2_25U(legends=["Return"]), 847 | gem_2_25U(legends=["Enter"]), 848 | # 2.5U keys 849 | gem_2_5U(name="blank"), 850 | gem_2_5U(name="Shift", legends=["Shift"]), 851 | gem_2_5U(name="ShiftyShift", 852 | legends=["Shift"], trans=[[12,-2.8,0]]), 853 | gem_2_5U(name="ShiftyShiftL", 854 | legends=["Shift"], trans=[[-6.5,-2.8,0]]), 855 | gem_2_5U(name="TrueShift", legends=["True Shift"]), 856 | # 2.75U keys 857 | gem_2_75U(name="blank"), 858 | gem_2_75U(name="Shift", legends=["Shift"]), 859 | gem_2_75U(name="ShiftyShift", 860 | legends=["Shift"], trans=[[12.5,-2.8,0]]), 861 | gem_2_75U(name="ShiftyShiftL", 862 | legends=["Shift"], trans=[[-7,-2.8,0]]), 863 | gem_2_75U(name="TrueShift", legends=["True Shift"]), 864 | # Various spacebars 865 | gem_6_25U(name="space", 866 | # Spacebars don't need to be as thick 867 | stem_sides_wall_thickness=0.0, dish_invert=True), 868 | gem_7U(name="space", 869 | # Spacebars don't need to be as thick 870 | stem_sides_wall_thickness=0.0, dish_invert=True), 871 | # Numpad keycaps 872 | gem_alphas(name="numpad1", legends=["1"]), 873 | gem_alphas(name="numpad2", legends=["2"]), 874 | gem_alphas(name="numpad3", legends=["3"]), 875 | gem_alphas(name="numpad4", legends=["4"]), 876 | gem_alphas(name="numpad5", legends=["5"]), 877 | gem_alphas_homing_dot(name="numpad5_dot", legends=["5"]), 878 | gem_alphas(name="numpad6", legends=["6"]), 879 | gem_alphas(name="numpad7", legends=["7"]), 880 | gem_alphas(name="numpad8", legends=["8"]), 881 | gem_alphas(name="numpad9", legends=["9"]), 882 | gem_alphas(name="numpad0", legends=["0"]), # For those with small 0 keys 883 | gem_alphas(name="numpadplus", legends=["+"], font_sizes=[7]), 884 | gem_2UV(name="2UV_numpadplus", legends=["+"], font_sizes=[7], 885 | trans=[[0.5,-0.1,0]]), 886 | gem_2U(name="2U_numpad0", legends=["0"], 887 | font_sizes=[4.5]), # Normal 2U numpad key 888 | gem_alphas(name="numpaddot", legends=["."]), 889 | gem_alphas(name="numlock", legends=["Num"]), 890 | gem_alphas(name="numpadslash", legends=["/"]), 891 | gem_alphas(name="numpadstar", legends=["*"]), 892 | gem_alphas(name="numpadminus", legends=["-"]), 893 | gem_2UV(name="2UV_numpadenter", legends=["↵",], font_sizes=[7], 894 | fonts=["OverpassMono Nerd Font:style=Bold"]), 895 | ] 896 | 897 | def print_keycaps(): 898 | """ 899 | Prints the names of all keycaps in KEYCAPS. 900 | """ 901 | print(Style.BRIGHT + 902 | f"Here's all the keycaps we can render:\n" + Style.RESET_ALL) 903 | keycap_names = ", ".join(a.name for a in KEYCAPS) 904 | print(f"{keycap_names}") 905 | 906 | if __name__ == "__main__": 907 | parser = argparse.ArgumentParser( 908 | description="Render a full set of GEM keycaps.") 909 | parser.add_argument('--out', 910 | metavar='', type=str, default=".", 911 | help='Where the generated files will go.') 912 | parser.add_argument('--force', 913 | required=False, action='store_true', 914 | help='Forcibly re-render keycaps even if they already exist.') 915 | parser.add_argument('--legends', 916 | required=False, action='store_true', 917 | help=f'If True, generate a separate set of {FILE_TYPE} files for legends.') 918 | parser.add_argument('--keycaps', 919 | required=False, action='store_true', 920 | help='If True, prints out the names of all keycaps we can render.') 921 | parser.add_argument('names', 922 | nargs='*', metavar="name", 923 | help='Optional name of specific keycap you wish to render') 924 | args = parser.parse_args() 925 | #print(args) 926 | if len(sys.argv) == 1: 927 | parser.print_help() 928 | print("") 929 | print_keycaps() 930 | sys.exit(1) 931 | if args.keycaps: 932 | print_keycaps() 933 | sys.exit(1) 934 | if not os.path.exists(args.out): 935 | print(Style.BRIGHT + 936 | f"Output path, '{args.out}' does not exist; making it..." 937 | + Style.RESET_ALL) 938 | os.mkdir(args.out) 939 | print(Style.BRIGHT + f"Outputting to: {args.out}" + Style.RESET_ALL) 940 | if args.names: # Just render the specified keycaps 941 | matched = False 942 | for name in args.names: 943 | for keycap in KEYCAPS: 944 | if keycap.name.lower() == name.lower(): 945 | keycap.output_path = f"{args.out}" 946 | matched = True 947 | exists = False 948 | if not args.force: 949 | if os.path.exists(f"{args.out}/{keycap.name}.{keycap.file_type}"): 950 | print(Style.BRIGHT + 951 | f"{args.out}/{keycap.name}.{keycap.file_type} exists; " 952 | f"skipping..." 953 | + Style.RESET_ALL) 954 | exists = True 955 | if not exists: 956 | print(Style.BRIGHT + 957 | f"Rendering {args.out}/{keycap.name}.{keycap.file_type}..." 958 | + Style.RESET_ALL) 959 | print(keycap) 960 | retcode, output = getstatusoutput(str(keycap)) 961 | if retcode == 0: # Success! 962 | print( 963 | f"{args.out}/{keycap.name}.{keycap.file_type} " 964 | f"rendered successfully") 965 | if args.legends: 966 | keycap.name = f"{keycap.name}_legends" 967 | # Change it to .stl since PrusaSlicer doesn't like .3mf 968 | # for "parts" for unknown reasons... 969 | keycap.file_type = "stl" 970 | if os.path.exists(f"{args.out}/{keycap.name}.{keycap.file_type}"): 971 | print(Style.BRIGHT + 972 | f"{args.out}/{keycap.name}.{keycap.file_type} exists; " 973 | f"skipping..." 974 | + Style.RESET_ALL) 975 | continue 976 | print(Style.BRIGHT + 977 | f"Rendering {args.out}/{keycap.name}.{keycap.file_type}..." 978 | + Style.RESET_ALL) 979 | print(keycap) 980 | retcode, output = getstatusoutput(str(keycap)) 981 | if retcode == 0: # Success! 982 | print( 983 | f"{args.out}/{keycap.name}.{keycap.file_type} " 984 | f"rendered successfully") 985 | if not matched: 986 | print(f"Cound not find a keycap named {name}") 987 | else: 988 | # First render the keycaps 989 | for keycap in KEYCAPS: 990 | keycap.output_path = f"{args.out}" 991 | if not args.force: 992 | if os.path.exists(f"{args.out}/{keycap.name}.{keycap.file_type}"): 993 | print(Style.BRIGHT + 994 | f"{args.out}/{keycap.name}.{keycap.file_type} exists; skipping..." 995 | + Style.RESET_ALL) 996 | continue 997 | print(Style.BRIGHT + 998 | f"Rendering {args.out}/{keycap.name}.{keycap.file_type}..." 999 | + Style.RESET_ALL) 1000 | print(keycap) 1001 | retcode, output = getstatusoutput(str(keycap)) 1002 | if retcode == 0: # Success! 1003 | print(f"{args.out}/{keycap.name}.{keycap.file_type} rendered successfully") 1004 | # Next render the legends (for multi-material, non-transparent legends) 1005 | if args.legends: 1006 | for legend in KEYCAPS: 1007 | if legend.legends == [""]: 1008 | continue # No actual legends 1009 | legend.name = f"{legend.name}_legends" 1010 | legend.output_path = f"{args.out}" 1011 | legend.render = ["legends"] 1012 | # Change it to .stl since PrusaSlicer doesn't like .3mf 1013 | # for "parts" for unknown reasons... 1014 | legend.file_type = "stl" 1015 | if not args.force: 1016 | if os.path.exists(f"{args.out}/{legend.name}.{legend.file_type}"): 1017 | print(Style.BRIGHT + 1018 | f"{args.out}/{legend.name}.{legend.file_type} exists; skipping..." 1019 | + Style.RESET_ALL) 1020 | continue 1021 | print(Style.BRIGHT + 1022 | f"Rendering {args.out}/{legend.name}.{legend.file_type}..." 1023 | + Style.RESET_ALL) 1024 | print(legend) 1025 | retcode, output = getstatusoutput(str(legend)) 1026 | if retcode == 0: # Success! 1027 | print(f"{args.out}/{legend.name}.{legend.file_type} rendered successfully") 1028 | -------------------------------------------------------------------------------- /scripts/keycap.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | """ 4 | Contains the Keycap class which makes it easy to script the generation of 5 | keycaps using the Keycap Playground. 6 | 7 | .. note:: 8 | 9 | This was made to be run on a Linux system but if you change the various 10 | `Path()` variables it should work on anything. 11 | """ 12 | 13 | import os 14 | import json 15 | from pathlib import Path 16 | 17 | KEY_UNIT = 19.05 # Square that makes up the entire space of a key 18 | BETWEENSPACE = 0.8 # Space between keycaps 19 | 20 | class OpenSCADException(Exception): 21 | """ 22 | Raised when OpenSCAD can't be found or it's not working correctly. 23 | """ 24 | pass 25 | 26 | class ColorscadException(Exception): 27 | """ 28 | Raised when colorscad.sh can't be found or it's not working correctly. 29 | """ 30 | pass 31 | 32 | class Keycap(object): 33 | """ 34 | A convenient abstraction for specifying a keycap's details. The most useful 35 | way to use this class is to subclass it to make your own "base" class 36 | containing our kecap's parameters. Example:: 37 | 38 | class KeycapBase(Keycap): 39 | ''' 40 | Base keycap definitions for keycaps using Gotham Rounded. 41 | ''' 42 | def __init__(self, **kwargs): 43 | super().__init__(**kwargs) 44 | self.key_profile = "gem" 45 | self.key_rotation = [0,108.6,90] 46 | self.wall_thickness = 0.45*2.25 47 | self.uniform_wall_thickness = True 48 | self.stem_inside_tolerance = 0.15 49 | self.stem_sides_wall_thickness = 0.8; # Thick (good sound/feel) 50 | self.fonts = [ 51 | "Gotham Rounded:style=Bold", # Main legend 52 | "Gotham Rounded:style=Bold", # Secondary (top right) 53 | "Arial Black:style=Regular", # Front-facing 54 | ] 55 | self.font_sizes = [ 56 | 5.5, # Regular legend (lower left) 57 | 4, # Gotham Rounded second legend (top right) 58 | 4, # Front legend 59 | ] 60 | self.trans = [ 61 | [-3,-2.6,2], # Lower left corner Gotham Rounded 62 | [3.5,3,1], # Top right Gotham Rounded 63 | [0.15,-3,2], # Front legend 64 | ] 65 | self.rotation = [ 66 | [0,-20,0], # -20 here to match the layer lines when printing 67 | [0,-20,0], 68 | [68,0,0], # Front-facing legends need a lot of rotation 69 | ] 70 | self.postinit(**kwargs) 71 | 72 | The call to `self.postinit(**kwargs)` is important if you want to be able 73 | to override things in `KeycapBase()` by passing them via arguments like so:: 74 | 75 | tilde = KeycapBase(name="tilde", legends=["`", "", "~"]) 76 | 77 | To actually generate a keycap you can use something like this:: 78 | 79 | from subprocess import getstatusoutput 80 | retcode, output = getstatusoutput(str(tilde)) 81 | """ 82 | def __init__(self, 83 | name=None, 84 | render=["keycap", "stem"], 85 | key_profile="riskeycap", 86 | key_length=KEY_UNIT-BETWEENSPACE, 87 | key_width=KEY_UNIT-BETWEENSPACE, 88 | key_rotation=[0,0,0], 89 | key_height=8, 90 | key_top_difference=5, 91 | key_top_x=0, 92 | key_top_y=0, 93 | wall_thickness=0.45*2.5, 94 | dish_thickness=1.0, 95 | dish_type="cylinder", 96 | dish_x=0, 97 | dish_y=0, 98 | dish_z=0, 99 | dish_depth=1, 100 | dish_invert=False, 101 | dish_invert_division_x=4, 102 | dish_invert_division_y=1, 103 | dish_tilt=0, 104 | dish_tilt_curve=True, 105 | dish_fn=256, 106 | dish_corner_fn=64, 107 | uniform_wall_thickness=True, 108 | polygon_layers=10, 109 | polygon_layer_rotation=0, 110 | polygon_edges=4, 111 | polygon_rotation=True, 112 | corner_radius=1, 113 | corner_radius_curve=3, 114 | stem_type="box_cherry", 115 | stem_height=4, 116 | stem_top_thickness=0.5, 117 | stem_inset=1, 118 | stem_inside_tolerance=0.2, 119 | stem_outside_tolerance_x=0.05, 120 | stem_outside_tolerance_y=0.05, 121 | stem_side_supports=[0,0,0,0], 122 | stem_locations=[[0,0,0]], 123 | stem_sides_wall_thickness=0.65, 124 | stem_snap_fit=False, 125 | stem_walls_inset=1.05, 126 | stem_walls_tolerance=0.2, 127 | homing_dot_length=0, # 0 means no "dot" 128 | homing_dot_width=1, 129 | homing_dot_x=0, 130 | homing_dot_y=-2, 131 | homing_dot_z=-0.35, # How far it sticks out 132 | legends=[""], 133 | fonts=[], font_sizes=[], 134 | trans=[[0,0,0]], trans2=[[0,0,0]], 135 | rotation=[[0,0,0]], rotation2=[[0,0,0]], 136 | scale=[[1,1,1]], underset=[[0,0,0]], 137 | legend_carved=False, 138 | keycap_playground_path=Path("./keycap_playground.scad"), 139 | file_type="3mf", 140 | openscad_path=Path("/usr/bin/openscad"), 141 | colorscad_path=Path(""), 142 | output_path=Path(".")): 143 | self.name = name 144 | self.output_path = output_path 145 | self.render = render 146 | self.key_height = key_height 147 | self.key_length = key_length 148 | self.key_width = key_width 149 | self.key_profile = key_profile 150 | self.key_top_difference = key_top_difference 151 | self.key_top_x = key_top_x 152 | self.key_top_y = key_top_y 153 | if not self.name: 154 | if legends and legends[0]: 155 | self.name = legends[0] 156 | else: 157 | self.name = "keycap" 158 | self.key_rotation = key_rotation 159 | self.wall_thickness = wall_thickness 160 | self.dish_thickness = dish_thickness 161 | self.dish_invert = dish_invert 162 | self.dish_invert_division_x = dish_invert_division_x 163 | self.dish_invert_division_y = dish_invert_division_y 164 | self.dish_type = dish_type 165 | self.dish_x = dish_x 166 | self.dish_y = dish_y 167 | self.dish_z = dish_z 168 | self.dish_depth = dish_depth 169 | self.dish_tilt = dish_tilt 170 | self.dish_tilt_curve = dish_tilt_curve 171 | self.dish_fn = dish_fn 172 | self.dish_corner_fn = dish_corner_fn 173 | self.uniform_wall_thickness = uniform_wall_thickness 174 | self.polygon_layers = polygon_layers 175 | self.polygon_layer_rotation = polygon_layer_rotation 176 | self.polygon_edges = polygon_edges 177 | self.polygon_rotation = polygon_rotation 178 | self.corner_radius = corner_radius 179 | self.corner_radius_curve = corner_radius_curve 180 | self.stem_type = stem_type 181 | self.stem_height = stem_height 182 | self.stem_top_thickness = stem_top_thickness 183 | self.stem_inset = stem_inset 184 | self.stem_inside_tolerance = stem_inside_tolerance 185 | self.stem_outside_tolerance_x = stem_outside_tolerance_x 186 | self.stem_outside_tolerance_y = stem_outside_tolerance_y 187 | self.stem_locations = stem_locations 188 | self.stem_side_supports = stem_side_supports 189 | self.stem_sides_wall_thickness = stem_sides_wall_thickness 190 | self.stem_snap_fit = stem_snap_fit 191 | self.stem_walls_inset = stem_walls_inset 192 | self.stem_walls_tolerance = stem_walls_tolerance 193 | self.homing_dot_length = homing_dot_length 194 | self.homing_dot_width = homing_dot_width 195 | self.homing_dot_x = homing_dot_x 196 | self.homing_dot_y = homing_dot_y 197 | self.homing_dot_z = homing_dot_z 198 | self.legends = legends 199 | self.fonts = fonts 200 | self.font_sizes = font_sizes 201 | self.trans = trans 202 | self.trans2 = trans2 203 | self.rotation = rotation 204 | self.rotation2 = rotation2 205 | self.scale = scale 206 | self.underset = underset 207 | self.legend_carved = legend_carved 208 | self.file_type = file_type 209 | self.keycap_playground_path = keycap_playground_path 210 | self.colorscad_path = colorscad_path 211 | self.openscad_path = openscad_path 212 | # This speeds things up considerably: 213 | self.openscad_args = "--enable=fast-csg" 214 | 215 | # NOTE: This doesn't seem to work right for unknown reasons so you'll want 216 | # to generate the quote keycap by hand on the command line. 217 | def quote(self, legends): 218 | """ 219 | Checks for the edge case of a single quote (') legend and converts it 220 | into `"'"'"'"` so that bash will pass it correclty to OpenSCAD via 221 | `getstatusoutput()`. Also covers the slash (\\) legend for 222 | completeness. 223 | 224 | .. note:: 225 | 226 | Example of what it should look like: `LEGENDS=["'"'"'", "", "\""];` 227 | """ 228 | properly_escaped_quote = r'''"'"'"'"''' 229 | out = "[" 230 | for i, legend in enumerate(legends): 231 | if legend == "'": 232 | out += properly_escaped_quote + "," 233 | elif legend == '"': 234 | out += r'"\""' 235 | else: 236 | out += json.dumps(legend) + "," 237 | out = out.rstrip(',') # Get rid of trailing comma 238 | return out + "]" 239 | 240 | def __repr__(self): 241 | return f""" 242 | name: {self.name} 243 | render: {self.render} 244 | key_profile: {self.key_profile} 245 | legends: {self.legends} 246 | trans: {self.trans} 247 | trans2: {self.trans2} 248 | rotation: {self.rotation} 249 | rotation2: {self.rotation2} 250 | scale: {self.scale} 251 | underset: {self.underset}""" 252 | 253 | def __str__(self): 254 | """ 255 | Returns the OpenSCAD command line to use to generate this keycap. 256 | """ 257 | first_part = ( 258 | f"{self.openscad_path} {self.openscad_args} -o " 259 | f"'{self.output_path}'/'{self.name}.{self.file_type}' -D $'" 260 | ) 261 | last_part = self.keycap_playground_path 262 | render = self.render 263 | if str(self.colorscad_path): # Use colorscad.sh 264 | # Check to make sure it actually exists 265 | if os.path.exists(self.colorscad_path): 266 | # Add openscad to the $PATH variable so colorscad can find it 267 | os.environ["PATH"] += f"{self.openscad_path.parent}" 268 | first_part = ( 269 | #f'PATH="${self.openscad_path.parent}:$PATH"; ' 270 | f"{self.colorscad_path} -i {self.keycap_playground_path} " 271 | f"-o '{self.output_path}'/'{self.name}.{self.file_type}' " 272 | f"-p '{self.openscad_path}' " 273 | f"-- {self.openscad_args} -D $'" 274 | ) 275 | last_part = "" 276 | #render = ["keycap", "stem", "legends"] 277 | render.append("legends") 278 | # NOTE: Since OpenSCAD requires double quotes I'm using the json module 279 | # to encode things that need it: 280 | return ( 281 | f"{first_part}" 282 | f"RENDER={json.dumps(render)}; " 283 | f"KEY_PROFILE={json.dumps(self.key_profile)}; " 284 | f"KEY_LENGTH={round(self.key_length,2)}; " 285 | f"KEY_WIDTH={round(self.key_width,2)}; " 286 | f"KEY_TOP_DIFFERENCE={self.key_top_difference}; " 287 | f"KEY_ROTATION={self.key_rotation}; " 288 | f"KEY_HEIGHT={self.key_height}; " 289 | f"KEY_TOP_X={self.key_top_x}; " 290 | f"KEY_TOP_Y={self.key_top_y}; " 291 | f"WALL_THICKNESS={self.wall_thickness}; " 292 | f"UNIFORM_WALL_THICKNESS={json.dumps(self.uniform_wall_thickness)}; " 293 | f"DISH_THICKNESS={self.dish_thickness}; " 294 | f"DISH_INVERT={json.dumps(self.dish_invert)}; " 295 | f"DISH_INVERT_DIVISION_X={self.dish_invert_division_x}; " 296 | f"DISH_INVERT_DIVISION_Y={self.dish_invert_division_y}; " 297 | f"DISH_TYPE={json.dumps(self.dish_type)}; " 298 | f"DISH_DEPTH={self.dish_depth}; " 299 | f"DISH_X={self.dish_x}; " 300 | f"DISH_Y={self.dish_y}; " 301 | f"DISH_Z={self.dish_z}; " 302 | f"DISH_TILT={self.dish_tilt}; " 303 | f"DISH_TILT_CURVE={json.dumps(self.dish_tilt_curve)}; " 304 | f"DISH_FN={self.dish_fn}; " 305 | f"DISH_CORNER_FN={self.dish_corner_fn}; " 306 | f"POLYGON_LAYERS={self.polygon_layers}; " 307 | f"POLYGON_LAYER_ROTATION={json.dumps(self.polygon_layer_rotation)}; " 308 | f"POLYGON_EDGES={self.polygon_edges}; " 309 | f"POLYGON_ROTATION={json.dumps(self.polygon_rotation)}; " 310 | f"CORNER_RADIUS={self.corner_radius}; " 311 | f"CORNER_RADIUS_CURVE={json.dumps(self.corner_radius_curve)}; " 312 | f"STEM_TYPE={json.dumps(self.stem_type)}; " 313 | f"STEM_HEIGHT={self.stem_height}; " 314 | f"STEM_TOP_THICKNESS={self.stem_top_thickness}; " 315 | f"STEM_INSET={self.stem_inset}; " 316 | f"STEM_INSIDE_TOLERANCE={self.stem_inside_tolerance}; " 317 | f"STEM_OUTSIDE_TOLERANCE_X={self.stem_outside_tolerance_x}; " 318 | f"STEM_OUTSIDE_TOLERANCE_Y={self.stem_outside_tolerance_y}; " 319 | f"STEM_SIDE_SUPPORTS={self.stem_side_supports}; " 320 | f"STEM_SIDES_WALL_THICKNESS={self.stem_sides_wall_thickness}; " 321 | f"STEM_LOCATIONS={self.stem_locations}; " 322 | f"STEM_SNAP_FIT={json.dumps(self.stem_snap_fit)}; " 323 | f"STEM_WALLS_INSET={self.stem_walls_inset}; " 324 | f"STEM_WALLS_TOLERANCE={self.stem_walls_tolerance}; " 325 | f"HOMING_DOT_LENGTH={self.homing_dot_length}; " 326 | f"HOMING_DOT_WIDTH={self.homing_dot_width}; " 327 | f"HOMING_DOT_X={self.homing_dot_x}; " 328 | f"HOMING_DOT_Y={self.homing_dot_y}; " 329 | f"HOMING_DOT_Z={self.homing_dot_z}; " 330 | f"LEGENDS={self.quote(self.legends)}; " 331 | f"LEGEND_FONTS={json.dumps(self.fonts)}; " 332 | f"LEGEND_FONT_SIZES={self.font_sizes}; " 333 | f"LEGEND_TRANS={self.trans}; " 334 | f"LEGEND_TRANS2={self.trans2}; " 335 | f"LEGEND_ROTATION={self.rotation}; " 336 | f"LEGEND_ROTATION2={self.rotation2}; " 337 | f"LEGEND_SCALE={self.scale}; " 338 | f"LEGEND_UNDERSET={self.underset}; " 339 | # NOTE: For some reason I have to duplicate RENDER here for it to work properly: 340 | f"RENDER={json.dumps(render)};' " 341 | f"{last_part}" 342 | ) 343 | 344 | def postinit(self, **kwargs): 345 | """ 346 | Override anything passed in via kwargs 347 | """ 348 | #print(f"postinit kwargs: {kwargs}") 349 | for k, v in kwargs.items(): 350 | #print(f"Updating: {k}: {v}") 351 | self.__dict__.update({k: v}) 352 | -------------------------------------------------------------------------------- /scripts/riskeyboard_70.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | """ 4 | Generates a whole Riskeyboard 70's worth of keycaps (and a few extras). Best way 5 | to use this script is from within the `keycap_playground` directory. 6 | 7 | .. bash:: 8 | 9 | $ ./scripts/riskeyboard_70.py --out /tmp/output_dir 10 | """ 11 | 12 | # stdlib imports 13 | import os, sys 14 | import json 15 | import argparse 16 | from copy import deepcopy 17 | from subprocess import getstatusoutput 18 | # 3rd party stuff 19 | from colorama import Fore, Back, Style 20 | from colorama import init as color_init 21 | color_init() 22 | # Our own stuff 23 | from keycap import Keycap 24 | 25 | KEY_UNIT = 19.05 # Square that makes up the entire space of a key 26 | BETWEENSPACE = 0.8 # Space between keycaps 27 | 28 | class riskeyboard70_base(Keycap): 29 | """ 30 | Base keycap definitions for Gotham Rounded 31 | """ 32 | def __init__(self, **kwargs): 33 | super().__init__(**kwargs) 34 | self.key_profile = "gem" 35 | self.key_rotation = [0,108.6,90] 36 | self.wall_thickness = 0.45*2.25 37 | self.uniform_wall_thickness = True 38 | self.dish_thickness = 1.0 # Note: Not actually used 39 | self.stem_type = "box_cherry" 40 | self.stem_top_thickness = 0.65 # Note: Not actually used 41 | self.stem_inside_tolerance = 0.15 42 | # Disabled stem side support because it seems it is unnecessary @0.16mm 43 | self.stem_side_supports = [0,0,0,0] 44 | self.stem_locations = [[0,0,0]] 45 | self.stem_sides_wall_thickness = 0.8; # Thick (good sound/feel) 46 | # Because we do strange things we need legends bigger on the Z 47 | self.scale = [ 48 | [1,1,3], 49 | [1,1.75,3], # For the pipe to make it taller/more of a divider 50 | [1,1,3], 51 | ] 52 | self.fonts = [ 53 | "Gotham Rounded:style=Bold", 54 | "Gotham Rounded:style=Bold", 55 | "Arial Black:style=Regular", 56 | ] 57 | self.font_sizes = [ 58 | 5.5, 59 | 4, # Gotham Rounded second legend (top right) 60 | 4, # Front legend 61 | ] 62 | self.trans = [ 63 | [-3,-2.6,2], # Lower left corner Gotham Rounded 64 | [3.5,3,1], # Top right Gotham Rounded 65 | [0.15,-3,2], # Front legend 66 | ] 67 | self.rotation = [ 68 | [0,-20,0], 69 | [0,-20,0], 70 | [68,0,0], 71 | ] 72 | self.postinit(**kwargs) 73 | 74 | # KEY_ROTATION = [0,107.8,90]; // GEM profile rotation 1.5U 75 | 76 | class riskeyboard70_alphas(riskeyboard70_base): 77 | """ 78 | Tilde needs some changes because by default it's too small 79 | """ 80 | def __init__(self, **kwargs): 81 | super().__init__(**kwargs) 82 | self.font_sizes = [ 83 | 4.5, # Regular Gotham Rounded 84 | 4, 85 | 4, # Front legend 86 | ] 87 | self.trans = [ 88 | [2.6,0,0], # Centered when angled -20° 89 | [3.5,3,1], # Top right Gotham Rounded 90 | [0.15,-3,2], # Front legend 91 | ] 92 | self.postinit(**kwargs) 93 | 94 | class riskeyboard70_numrow(riskeyboard70_base): 95 | """ 96 | Number row numbers are slightly different 97 | """ 98 | def __init__(self, **kwargs): 99 | super().__init__(**kwargs) 100 | self.fonts = [ 101 | "Gotham Rounded:style=Bold", # Main char 102 | "Gotham Rounded:style=Bold", # Pipe character 103 | "Gotham Rounded:style=Bold", # Symbol 104 | "Arial Black:style=Regular", # F-key 105 | ] 106 | self.font_sizes = [ 107 | 4.5, # Regular character 108 | 4.5, # Pipe 109 | 4.5, # Regular Gotham Rounded symbols 110 | 3.5, # Front legend 111 | ] 112 | self.trans = [ 113 | [-0.3,0,0], # Left Gotham Rounded 114 | [2.6,0,0], # Center Gotham Rounded | 115 | [5,0,1], # Right-side Gotham symbols 116 | [0.15,-2,2], # F-key 117 | ] 118 | self.rotation = [ 119 | [0,-20,0], 120 | [0,-20,0], 121 | [0,-20,0], 122 | [68,0,0], 123 | ] 124 | self.scale = [ 125 | [1,1,3], 126 | [1,1.75,3], # For the pipe to make it taller/more of a divider 127 | [1,1,3], 128 | [1,1,3], 129 | ] 130 | self.postinit(**kwargs) 131 | 132 | class riskeyboard70_tilde(riskeyboard70_numrow): 133 | """ 134 | Tilde needs some changes because by default it's too small 135 | """ 136 | def __init__(self, **kwargs): 137 | super().__init__(**kwargs) 138 | self.font_sizes[0] = 6.5 # ` symbol 139 | self.font_sizes[2] = 5.5 # ~ symbol 140 | self.trans[0] = [-0.3,-2.7,0] # ` 141 | self.trans[2] = [5.5,-1,1] # ~ 142 | 143 | class riskeyboard70_2(riskeyboard70_numrow): 144 | """ 145 | 2 needs some changes based on the @ symbol 146 | """ 147 | def __init__(self, **kwargs): 148 | super().__init__(**kwargs) 149 | self.fonts[2] = "Aharoni" 150 | self.font_sizes[2] = 4.5 # @ symbol (Aharoni) 151 | self.trans[2] = [5.4,0,1] 152 | 153 | class riskeyboard70_3(riskeyboard70_numrow): 154 | """ 155 | 3 needs some changes based on the # symbol (slightly too big) 156 | """ 157 | def __init__(self, **kwargs): 158 | super().__init__(**kwargs) 159 | self.font_sizes[2] = 4 # # symbol (Gotham Rounded) 160 | self.trans[2] = [5.5,0,1] # Move to the right a bit 161 | 162 | class riskeyboard70_5(riskeyboard70_numrow): 163 | """ 164 | 5 needs some changes based on the % symbol (too big, too close to bar) 165 | """ 166 | def __init__(self, **kwargs): 167 | super().__init__(**kwargs) 168 | self.font_sizes[2] = 3.75 # % symbol 169 | self.trans[2] = [5.2,0,1] 170 | 171 | class riskeyboard70_7(riskeyboard70_numrow): 172 | """ 173 | 7 needs some changes based on the & symbol (it's too big) 174 | """ 175 | def __init__(self, **kwargs): 176 | super().__init__(**kwargs) 177 | self.font_sizes[2] = 3.85 # & symbol 178 | self.trans[2] = [5.2,0,1] 179 | 180 | class riskeyboard70_8(riskeyboard70_numrow): 181 | """ 182 | 8 needs some changes based on the tiny * symbol 183 | """ 184 | def __init__(self, **kwargs): 185 | super().__init__(**kwargs) 186 | self.font_sizes[2] = 7.5 # * symbol (Gotham Rounded) 187 | self.trans[2] = [5.2,-1.9,1] # * needs a smidge of repositioning 188 | 189 | class riskeyboard70_equal(riskeyboard70_numrow): 190 | """ 191 | = needs some changes because it and the + are a bit off center 192 | """ 193 | def __init__(self, **kwargs): 194 | super().__init__(**kwargs) 195 | self.trans[0] = [-0.3,-0.5,0] # = sign adjustments 196 | self.trans[2] = [5,-0.3,1] # + sign adjustments 197 | 198 | class riskeyboard70_dash(riskeyboard70_numrow): 199 | """ 200 | The dash (-) is fine but the underscore (_) needs minor repositioning. 201 | """ 202 | def __init__(self, **kwargs): 203 | super().__init__(**kwargs) 204 | self.trans[2] = [5.2,-1,1] # _ needs to go down and to the right a bit 205 | self.scale[2] = [0.8,1,3] # Also needs to be squished a bit 206 | 207 | class riskeyboard70_double_legends(riskeyboard70_base): 208 | """ 209 | For regular keys that have two legends... ,./;'[] 210 | """ 211 | def __init__(self, **kwargs): 212 | super().__init__(**kwargs) 213 | self.fonts = [ 214 | "Gotham Rounded:style=Bold", # Main legend 215 | "Gotham Rounded:style=Bold", # Pipe character 216 | "Gotham Rounded:style=Bold", # Second legend 217 | ] 218 | self.font_sizes = [ 219 | 4.5, # Regular Gotham Rounded character 220 | 4.5, # Pipe 221 | 4.5, # Regular Gotham Rounded character 222 | ] 223 | self.trans = [ 224 | [-0.3,0,0], # Left Gotham Rounded 225 | [2.6,0,0], # Center Gotham Rounded | 226 | [5,0,1], # Right-side Gotham symbols 227 | ] 228 | self.rotation = [ 229 | [0,-20,0], 230 | [0,-20,0], 231 | [0,-20,0], 232 | ] 233 | self.scale = [ 234 | [1,1,3], 235 | [1,1.75,3], # For the pipe to make it taller/more of a divider 236 | [1,1,3], 237 | ] 238 | self.postinit(**kwargs) 239 | 240 | class riskeyboard70_gt_lt(riskeyboard70_double_legends): 241 | """ 242 | The greater than (>) and less than (<) signs need to be adjusted down a bit 243 | """ 244 | def __init__(self, **kwargs): 245 | super().__init__(**kwargs) 246 | self.trans[0] = [-0.3,-0.1,0] # , and . are the tiniest bit too high 247 | self.trans[2] = [5.2,-0.35,1] # < and > are too high for some reason 248 | 249 | class riskeyboard70_brackets(riskeyboard70_double_legends): 250 | """ 251 | The curly braces `{}` needs to be moved to the right a smidge 252 | """ 253 | def __init__(self, **kwargs): 254 | super().__init__(**kwargs) 255 | self.trans[2] = [5.2,0,1] # Just a smidge to the right 256 | 257 | class riskeyboard70_semicolon(riskeyboard70_double_legends): 258 | """ 259 | The semicolon ends up being slightly higher than the colon but it looks 260 | better if the top dot in both is aligned. 261 | """ 262 | def __init__(self, **kwargs): 263 | super().__init__(**kwargs) 264 | self.trans[0] = [0.2,-0.4,0] 265 | self.trans[2] = [4.7,0,1] 266 | 267 | class riskeyboard70_1_U_text(riskeyboard70_alphas): 268 | """ 269 | Ctrl, Del, and Ins need to be downsized and moved a smidge. 270 | """ 271 | def __init__(self, **kwargs): 272 | super().__init__(**kwargs) 273 | kwargs_copy = deepcopy(kwargs) 274 | self.font_sizes[0] = 4 275 | self.trans[0] = [2.5,0,0] 276 | self.postinit(**kwargs_copy) 277 | 278 | class riskeyboard70_arrows(riskeyboard70_alphas): 279 | """ 280 | Arrow symbols (◀▶▲▼) needs a different font (Hack) 281 | """ 282 | def __init__(self, **kwargs): 283 | super().__init__(**kwargs) 284 | self.fonts[0] = "Hack" 285 | self.fonts[2] = "FontAwesome" # For next/prev track icons 286 | self.font_sizes[2] = 4 # FontAwesome prev/next icons 287 | self.trans[2] = [0,-2,2] # Ditto 288 | 289 | class riskeyboard70_fontawesome(riskeyboard70_alphas): 290 | """ 291 | For regular centered FontAwesome icon keycaps. 292 | """ 293 | def __init__(self, **kwargs): 294 | super().__init__(**kwargs) 295 | self.fonts[0] = "FontAwesome" 296 | self.font_sizes[0] = 5 297 | self.trans[0] = [2.6,0.3,0] 298 | 299 | class riskeyboard70_1_25U(riskeyboard70_alphas): 300 | """ 301 | The base for all 1.25U keycaps. 302 | """ 303 | def __init__(self, **kwargs): 304 | super().__init__(**kwargs) 305 | kwargs_copy = deepcopy(kwargs) # Because self.trans[0] updates in place 306 | self.key_length = KEY_UNIT*1.25-BETWEENSPACE 307 | self.key_rotation = [0,108.55,90] 308 | self.trans[0] = [3,0.2,0] 309 | self.postinit(**kwargs_copy) 310 | if not self.name.startswith('1.25U_'): 311 | self.name = f"1.25U_{self.name}" 312 | 313 | class riskeyboard70_1_5U(riskeyboard70_double_legends): 314 | """ 315 | The base for all 1.5U keycaps. 316 | 317 | .. note:: Uses riskeyboard70_double_legends because of the \\| key. 318 | """ 319 | def __init__(self, **kwargs): 320 | super().__init__(**kwargs) 321 | self.key_length = KEY_UNIT*1.5-BETWEENSPACE 322 | self.key_rotation = [0,107.825,90] 323 | self.postinit(**kwargs) 324 | if not self.name.startswith('1.5U_'): 325 | self.name = f"1.5U_{self.name}" 326 | 327 | class riskeyboard70_bslash(riskeyboard70_1_5U): 328 | """ 329 | Backslash key needs a very minor adjustment to the backslash. 330 | """ 331 | def __init__(self, **kwargs): 332 | super().__init__(**kwargs) 333 | self.trans[0] = [-0.9,0,0] # Move \ to the left a bit more than normal 334 | 335 | class riskeyboard70_tab(riskeyboard70_1_5U): 336 | """ 337 | "Tab" needs to be centered. 338 | """ 339 | def __init__(self, **kwargs): 340 | super().__init__(**kwargs) 341 | self.font_sizes[0] = 4.5 # Regular Gotham Rounded 342 | self.trans[0] = [2.6,0,0] # Centered when angled -20° 343 | 344 | class riskeyboard70_1_75U(riskeyboard70_alphas): 345 | """ 346 | The base for all 1.75U keycaps. 347 | """ 348 | def __init__(self, **kwargs): 349 | super().__init__(**kwargs) 350 | self.key_length = KEY_UNIT*1.75-BETWEENSPACE 351 | self.key_rotation = [0,107.85,90] 352 | self.postinit(**kwargs) 353 | if not self.name.startswith('1.75U_'): 354 | self.name = f"1.75U_{self.name}" 355 | 356 | class riskeyboard70_2U(riskeyboard70_alphas): 357 | """ 358 | The base for all 2U keycaps. 359 | """ 360 | def __init__(self, **kwargs): 361 | super().__init__(**kwargs) 362 | self.key_length = KEY_UNIT*2-BETWEENSPACE 363 | self.key_rotation = [0,107.85,90] # Same as 1.75U 364 | if "dish_invert" in kwargs and kwargs["dish_invert"]: 365 | self.key_rotation = [0,111.88,90] # Spacebars are different 366 | self.stem_locations = [[0,0,0], [12,0,0], [-12,0,0]] 367 | self.postinit(**kwargs) 368 | if not self.name.startswith('2U_'): 369 | self.name = f"2U_{self.name}" 370 | 371 | class riskeyboard70_2_25U(riskeyboard70_alphas): 372 | """ 373 | The base for all 2.25U keycaps. 374 | """ 375 | def __init__(self, **kwargs): 376 | super().__init__(**kwargs) 377 | kwargs_copy = deepcopy(kwargs) 378 | self.key_length = KEY_UNIT*2.25-BETWEENSPACE 379 | self.key_rotation = [0,107.85,90] # Same as 1.75U and 2U 380 | if "dish_invert" in kwargs and kwargs["dish_invert"]: 381 | self.key_rotation = [0,111.88,90] # Spacebars are different 382 | self.stem_locations = [[0,0,0], [12,0,0], [-12,0,0]] 383 | self.trans[0] = [3.1,0.2,0] 384 | self.font_sizes[0] = 4 385 | self.postinit(**kwargs_copy) 386 | if not self.name.startswith('2.25U_'): 387 | self.name = f"2.25U_{self.name}" 388 | 389 | class riskeyboard70_2_5U(riskeyboard70_alphas): 390 | """ 391 | The base for all 2.5U keycaps. 392 | """ 393 | def __init__(self, **kwargs): 394 | super().__init__(**kwargs) 395 | kwargs_copy = deepcopy(kwargs) 396 | self.key_length = KEY_UNIT*2.5-BETWEENSPACE 397 | self.key_rotation = [0,107.85,90] # Same as 1.75U and 2U 398 | if "dish_invert" in kwargs and kwargs["dish_invert"]: 399 | self.key_rotation = [0,111.88,90] # Spacebars are different 400 | self.stem_locations = [[0,0,0], [12,0,0], [-12,0,0]] 401 | self.trans[0] = [3.1,0.2,0] 402 | self.font_sizes[0] = 4 403 | self.postinit(**kwargs_copy) 404 | if not self.name.startswith('2.5U_'): 405 | self.name = f"2.5U_{self.name}" 406 | 407 | class riskeyboard70_2_75U(riskeyboard70_alphas): 408 | """ 409 | The base for all 2.75U keycaps. 410 | """ 411 | def __init__(self, **kwargs): 412 | super().__init__(**kwargs) 413 | kwargs_copy = deepcopy(kwargs) 414 | self.key_length = KEY_UNIT*2.75-BETWEENSPACE 415 | self.key_rotation = [0,107.85,90] # Same as 1.75U and 2U 416 | if "dish_invert" in kwargs and kwargs["dish_invert"]: 417 | self.key_rotation = [0,111.88,90] # Spacebars are different 418 | self.stem_locations = [[0,0,0], [12,0,0], [-12,0,0]] 419 | self.trans[0] = [3.1,0.2,0] 420 | self.font_sizes[0] = 4 421 | self.postinit(**kwargs_copy) 422 | if not self.name.startswith('2.75U_'): 423 | self.name = f"2.75U_{self.name}" 424 | 425 | class riskeyboard70_6_25U(riskeyboard70_alphas): 426 | """ 427 | The base for all 6.25U keycaps. 428 | """ 429 | def __init__(self, **kwargs): 430 | super().__init__(**kwargs) 431 | kwargs_copy = deepcopy(kwargs) 432 | self.key_length = KEY_UNIT*6.25-BETWEENSPACE 433 | self.key_rotation = [0,107.85,90] # Same as 1.75U and 2U 434 | if "dish_invert" in kwargs and kwargs["dish_invert"]: 435 | self.key_rotation = [0,111.88,90] # Spacebars are different 436 | self.stem_locations = [[0,0,0], [50,0,0], [-50,0,0]] 437 | self.trans[0] = [3.1,0.2,0] 438 | self.font_sizes[0] = 4 439 | self.postinit(**kwargs_copy) 440 | if not self.name.startswith('6.25U_'): 441 | self.name = f"6.25U_{self.name}" 442 | 443 | class riskeyboard70_7U(riskeyboard70_alphas): 444 | """ 445 | The base for all 7U keycaps. 446 | """ 447 | def __init__(self, **kwargs): 448 | super().__init__(**kwargs) 449 | kwargs_copy = deepcopy(kwargs) 450 | self.key_length = KEY_UNIT*7-BETWEENSPACE 451 | self.key_rotation = [0,107.85,90] # Same as 1.75U and 2U 452 | if "dish_invert" in kwargs and kwargs["dish_invert"]: 453 | self.key_rotation = [0,111.88,90] # Spacebars are different 454 | self.stem_locations = [[0,0,0], [57,0,0], [-57,0,0]] 455 | self.trans[0] = [3.1,0.2,0] 456 | self.font_sizes[0] = 4 457 | self.postinit(**kwargs_copy) 458 | if not self.name.startswith('7U_'): 459 | self.name = f"7U_{self.name}" 460 | 461 | KEYCAPS = [ 462 | # 1U keys 463 | riskeyboard70_base(name="1U_blank"), 464 | riskeyboard70_tilde(name="tilde", legends=["`", "", "~"]), 465 | riskeyboard70_numrow(legends=["1", "", "!", "F1"]), 466 | riskeyboard70_2(legends=["2", "", "@", "F2"]), 467 | riskeyboard70_3(legends=["3", "", "#", "F3"]), 468 | riskeyboard70_numrow(legends=["4", "", "$", "F4"]), 469 | riskeyboard70_5(legends=["5", "", "%", "F5"]), 470 | riskeyboard70_numrow(legends=["6", "", "^", "F6"]), 471 | riskeyboard70_7(legends=["7", "", "&", "F7"]), 472 | riskeyboard70_8(legends=["8", "", "*", "F8"]), 473 | riskeyboard70_numrow(legends=["9", "", "(", "F9"]), 474 | riskeyboard70_numrow(legends=["0", "", ")", "F10"]), 475 | riskeyboard70_dash(name="dash", legends=["-", "", "_", "F11"]), 476 | riskeyboard70_equal(name="equal", legends=["=", "", "+", "F12"]), 477 | riskeyboard70_alphas(legends=["A"]), 478 | riskeyboard70_alphas(legends=["B"]), 479 | riskeyboard70_alphas(legends=["C"]), 480 | riskeyboard70_alphas(legends=["D"]), 481 | riskeyboard70_alphas(legends=["E"]), 482 | riskeyboard70_alphas(legends=["F"]), 483 | riskeyboard70_alphas(legends=["G"]), 484 | riskeyboard70_alphas(legends=["H"]), 485 | riskeyboard70_alphas(legends=["I"]), 486 | riskeyboard70_alphas(legends=["J"]), 487 | riskeyboard70_alphas(legends=["K"]), 488 | riskeyboard70_alphas(legends=["L"]), 489 | riskeyboard70_alphas(legends=["M"]), 490 | riskeyboard70_alphas(legends=["N"]), 491 | riskeyboard70_alphas(legends=["O"]), 492 | riskeyboard70_alphas(legends=["P"]), 493 | riskeyboard70_alphas(legends=["Q"]), 494 | riskeyboard70_alphas(legends=["R"]), 495 | riskeyboard70_alphas(legends=["S"]), 496 | riskeyboard70_alphas(legends=["T"]), 497 | riskeyboard70_alphas(legends=["U"]), 498 | riskeyboard70_alphas(legends=["V"]), 499 | riskeyboard70_alphas(legends=["W"]), 500 | riskeyboard70_alphas(legends=["X"]), 501 | riskeyboard70_alphas(legends=["Y"]), 502 | riskeyboard70_alphas(legends=["Z"]), 503 | riskeyboard70_alphas(legends=["Z"]), 504 | riskeyboard70_alphas(name="menu", legends=["☰"], fonts=["Code2000"]), 505 | riskeyboard70_alphas(name="Option1U", legends=["⌥"], fonts=["Code2000"]), 506 | riskeyboard70_arrows(name="left", legends=["◀", "", ""]), 507 | riskeyboard70_arrows(name="right", legends=["▶", "", ""]), 508 | riskeyboard70_arrows(name="up", legends=["▲", "", ""]), 509 | riskeyboard70_arrows(name="down", legends=["▼", "", "", ""]), 510 | riskeyboard70_arrows(name="eject", legends=[""]), # For Macs 511 | riskeyboard70_fontawesome(name="camera", legends=[""]), # aka screenshot 512 | riskeyboard70_fontawesome(name="bug", legends=[""]), # Just for fun 513 | riskeyboard70_1_U_text(name="RCtrl", legends=["Ctrl"]), 514 | riskeyboard70_1_U_text(legends=["Del"]), 515 | riskeyboard70_1_U_text(legends=["Ins"]), 516 | riskeyboard70_1_U_text(legends=["Esc"]), 517 | riskeyboard70_brackets(name="lbracket", legends=["[", "", "{"]), 518 | riskeyboard70_brackets(name="rbracket", legends=["]", "", "}"]), 519 | riskeyboard70_semicolon(name="semicolon", legends=[";", "", ":"]), 520 | riskeyboard70_double_legends(name="quote", legends=["'", "", '\"']), 521 | riskeyboard70_gt_lt(name="comma", legends=[",", "", "<"]), 522 | riskeyboard70_gt_lt(name="dot", legends=[".", "", ">"]), 523 | riskeyboard70_double_legends(name="slash", legends=["/", "", "?"]), 524 | # 1.25U keys 525 | riskeyboard70_1_25U(name="blank"), 526 | riskeyboard70_1_25U(name="LCtrl", legends=["Ctrl"], font_sizes=[4]), 527 | riskeyboard70_1_25U(name="LAlt", legends=["Alt"], font_sizes=[4]), 528 | riskeyboard70_1_25U(name="Command", legends=["Cmd"], font_sizes=[4]), 529 | riskeyboard70_1_25U(name="CommandSymbol", legends=["⌘"], font_sizes=[4], fonts=["Code2000"]), 530 | riskeyboard70_1_25U(name="OptionSymbol", legends=["⌥"], font_sizes=[4], fonts=["Code2000"]), 531 | riskeyboard70_1_25U(name="Option", legends=["Option"], font_sizes=[4]), 532 | riskeyboard70_1_25U(name="Fun", legends=["Fun"], font_sizes=[4]), 533 | riskeyboard70_1_25U(name="MoreFun", 534 | legends=["More", "Fun"], 535 | trans=[[3,2.5,0], [3,-2.5,0]], 536 | font_sizes=[4, 4], 537 | scale=[[1,1,3], [1,1,3]]), 538 | riskeyboard70_1_25U(legends=["Super", "Duper"], 539 | trans=[[3,2.25,0], [3,-2.25,0]], font_sizes=[3.25, 3.25], 540 | scale=[[1,1,3], [1,1,3]]), 541 | # 1.5U keys 542 | riskeyboard70_1_5U(name="blank"), 543 | riskeyboard70_bslash(name="bslash", legends=["\\", "", "|"]), 544 | riskeyboard70_tab(name="Tab", legends=["Tab"]), 545 | # 1.75U keys 546 | riskeyboard70_1_75U(name="blank"), 547 | riskeyboard70_1_75U(legends=["Compose"], 548 | trans=[[3.1,0.2,0]], font_sizes=[3.25]), 549 | riskeyboard70_1_75U(name="Caps", 550 | legends=["Caps Lock"], trans=[[3.1,0,0]], font_sizes=[3]), 551 | # 2U keys 552 | riskeyboard70_2U(name="blank"), 553 | riskeyboard70_2U(name="TOTALBS", 554 | legends=["TOTAL BS"], font_sizes=[3.75, 3.75]), 555 | riskeyboard70_2U(name="Backspace", font_sizes=[3.75, 3.75]), 556 | riskeyboard70_2U(name="2U_space", 557 | # Spacebars don't need to be as thick 558 | stem_sides_wall_thickness=0.0, 559 | key_rotation=[0,111.88,90], dish_invert=True), 560 | # 2.25U keys 561 | riskeyboard70_2_25U(name="blank"), 562 | riskeyboard70_2_25U(name="Shift", legends=["Shift"]), 563 | riskeyboard70_2_25U(name="ShiftyShift", 564 | legends=["Shift"], trans=[[9.5,-2.8,0]]), 565 | riskeyboard70_2_25U(name="TrueShift", legends=["True Shift"]), 566 | riskeyboard70_2_25U(legends=["Return"]), 567 | riskeyboard70_2_25U(legends=["Enter"]), 568 | # 2.5U keys 569 | riskeyboard70_2_5U(name="blank"), 570 | riskeyboard70_2_5U(name="Shift", legends=["Shift"]), 571 | riskeyboard70_2_5U(name="ShiftyShift", 572 | legends=["Shift"], trans=[[10,-2.8,0]]), 573 | riskeyboard70_2_5U(name="TrueShift", legends=["True Shift"]), 574 | # 2.75U keys 575 | riskeyboard70_2_75U(name="blank"), 576 | riskeyboard70_2_75U(name="Shift", legends=["Shift"]), 577 | riskeyboard70_2_75U(name="ShiftyShift", 578 | legends=["Shift"], trans=[[10.5,-2.8,0]]), 579 | riskeyboard70_2_75U(name="TrueShift", legends=["True Shift"]), 580 | # Various spacebars 581 | riskeyboard70_6_25U(name="space", 582 | # Spacebars don't need to be as thick 583 | stem_sides_wall_thickness=0.0, dish_invert=True), 584 | riskeyboard70_7U(name="space", 585 | # Spacebars don't need to be as thick 586 | stem_sides_wall_thickness=0.0, dish_invert=True), 587 | # Numpad keycaps 588 | riskeyboard70_alphas(name="numpad1", legends=["1"]), 589 | riskeyboard70_alphas(name="numpad2", legends=["2"]), 590 | riskeyboard70_alphas(name="numpad3", legends=["3"]), 591 | riskeyboard70_alphas(name="numpad4", legends=["4"]), 592 | riskeyboard70_alphas(name="numpad5", legends=["5"]), 593 | riskeyboard70_alphas(name="numpad6", legends=["6"]), 594 | riskeyboard70_alphas(name="numpad7", legends=["7"]), 595 | riskeyboard70_alphas(name="numpad8", legends=["8"]), 596 | riskeyboard70_alphas(name="numpad9", legends=["9"]), 597 | riskeyboard70_2U(name="numpad0", legends=["0"], 598 | font_sizes=[4.5]), 599 | riskeyboard70_alphas(name="numpaddot", legends=["."]), 600 | riskeyboard70_alphas(name="numlock", legends=["Num"]), 601 | riskeyboard70_alphas(name="numpadslash", legends=["/"]), 602 | riskeyboard70_alphas(name="numpadstar", legends=["*"]), 603 | riskeyboard70_alphas(name="numpadminus", legends=["-"]), 604 | ] 605 | 606 | def print_keycaps(): 607 | """ 608 | Prints the names of all keycaps in KEYCAPS. 609 | """ 610 | print(Style.BRIGHT + 611 | f"Here's all the keycaps we can render:\n" + Style.RESET_ALL) 612 | keycap_names = ", ".join(a.name for a in KEYCAPS) 613 | print(f"{keycap_names}") 614 | 615 | if __name__ == "__main__": 616 | parser = argparse.ArgumentParser( 617 | description="Render keycap STLs for all the Riskeyboard 70's switches.") 618 | parser.add_argument('--out', 619 | metavar='', type=str, default=".", 620 | help='Where the generated STL files will go.') 621 | parser.add_argument('--force', 622 | required=False, action='store_true', 623 | help='Forcibly re-render STL files even if they already exist.') 624 | parser.add_argument('--legends', 625 | required=False, action='store_true', 626 | help='If True, generate a separate set of STLs for legends.') 627 | parser.add_argument('--keycaps', 628 | required=False, action='store_true', 629 | help='If True, prints out the names of all keycaps we can render.') 630 | parser.add_argument('names', 631 | nargs='*', metavar="name", 632 | help='Optional name of specific keycap you wish to render') 633 | args = parser.parse_args() 634 | #print(args) 635 | if len(sys.argv) == 1: 636 | parser.print_help() 637 | print("") 638 | print_keycaps() 639 | sys.exit(1) 640 | if args.keycaps: 641 | print_keycaps() 642 | sys.exit(1) 643 | if not os.path.exists(args.out): 644 | print(Style.BRIGHT + 645 | f"Output path, '{args.out}' does not exist; making it..." 646 | + Style.RESET_ALL) 647 | os.mkdir(args.out) 648 | print(Style.BRIGHT + f"Outputting to: {args.out}" + Style.RESET_ALL) 649 | if args.names: # Just render the specified keycaps 650 | matched = False 651 | for name in args.names: 652 | for keycap in KEYCAPS: 653 | if keycap.name.lower() == name.lower(): 654 | keycap.output_path = f"{args.out}" 655 | matched = True 656 | exists = False 657 | if not args.force: 658 | if os.path.exists(f"{args.out}/{keycap.name}.stl"): 659 | print(Style.BRIGHT + 660 | f"{args.out}/{keycap.name}.stl exists; " 661 | f"skipping..." 662 | + Style.RESET_ALL) 663 | exists = True 664 | if not exists: 665 | print(Style.BRIGHT + 666 | f"Rendering {args.out}/{keycap.name}.stl..." 667 | + Style.RESET_ALL) 668 | print(keycap) 669 | retcode, output = getstatusoutput(str(keycap)) 670 | if retcode == 0: # Success! 671 | print( 672 | f"{args.out}/{keycap.name}.stl " 673 | f"rendered successfully") 674 | if args.legends: 675 | keycap.name = f"{keycap.name}_legends" 676 | if os.path.exists(f"{args.out}/{keycap.name}.stl"): 677 | print(Style.BRIGHT + 678 | f"{args.out}/{keycap.name}.stl exists; " 679 | f"skipping..." 680 | + Style.RESET_ALL) 681 | continue 682 | print(Style.BRIGHT + 683 | f"Rendering {args.out}/{keycap.name}.stl..." 684 | + Style.RESET_ALL) 685 | print(keycap) 686 | retcode, output = getstatusoutput(str(keycap)) 687 | if retcode == 0: # Success! 688 | print( 689 | f"{args.out}/{keycap.name}.stl " 690 | f"rendered successfully") 691 | if not matched: 692 | print(f"Cound not find a keycap named {name}") 693 | else: 694 | # First render the keycaps 695 | for keycap in KEYCAPS: 696 | keycap.output_path = f"{args.out}" 697 | if not args.force: 698 | if os.path.exists(f"{args.out}/{keycap.name}.stl"): 699 | print(Style.BRIGHT + 700 | f"{args.out}/{keycap.name}.stl exists; skipping..." 701 | + Style.RESET_ALL) 702 | continue 703 | print(Style.BRIGHT + 704 | f"Rendering {args.out}/{keycap.name}.stl..." 705 | + Style.RESET_ALL) 706 | print(keycap) 707 | retcode, output = getstatusoutput(str(keycap)) 708 | if retcode == 0: # Success! 709 | print(f"{args.out}/{keycap.name}.stl rendered successfully") 710 | # Next render the legends (for multi-material, non-transparent legends) 711 | if args.legends: 712 | for legend in KEYCAPS: 713 | if legend.legends == [""]: 714 | continue # No actual legends 715 | legend.name = f"{legend.name}_legends" 716 | legend.output_path = f"{args.out}" 717 | legend.render = ["legends"] 718 | if not args.force: 719 | if os.path.exists(f"{args.out}/{legend.name}.stl"): 720 | print(Style.BRIGHT + 721 | f"{args.out}/{legend.name}.stl exists; skipping..." 722 | + Style.RESET_ALL) 723 | continue 724 | print(Style.BRIGHT + 725 | f"Rendering {args.out}/{legend.name}.stl..." 726 | + Style.RESET_ALL) 727 | print(legend) 728 | retcode, output = getstatusoutput(str(legend)) 729 | if retcode == 0: # Success! 730 | print(f"{args.out}/{legend.name}.stl rendered successfully") 731 | -------------------------------------------------------------------------------- /scripts/riskeycap_full.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | """ 4 | Generates a whole keyboard's worth of keycaps (and a few extras). Best way 5 | to use this script is from within the `keycap_playground` directory. 6 | 7 | .. bash:: 8 | 9 | $ ./scripts/riskeycap_full.py --out /tmp/output_dir 10 | 11 | .. note:: 12 | 13 | Make sure you add the correct path to colorscad.sh if you want 14 | multi-material keycaps! 15 | 16 | Fonts used by this script: 17 | -------------------------- 18 | 19 | * Gotham Rounded:style=Bold 20 | * Arial Black:style=Regular 21 | * Aharoni 22 | * FontAwesome 23 | * Font Awesome 6 Free:style=Solid 24 | * Hack 25 | * Material Design Icons:style=Regular 26 | * Code2000 27 | * Agave 28 | * DejaVu Sans:style=Bold 29 | * Noto 30 | """ 31 | 32 | # stdlib imports 33 | import os, sys 34 | from pathlib import Path 35 | import json 36 | import argparse 37 | from copy import deepcopy 38 | from subprocess import getstatusoutput 39 | import asyncio 40 | import subprocess 41 | from functools import partial 42 | from typing import Sequence, Any 43 | from asyncio import ensure_future 44 | # 3rd party stuff 45 | from colorama import Fore, Back, Style 46 | from colorama import init as color_init 47 | color_init() 48 | # Our own stuff 49 | from keycap import Keycap 50 | 51 | # Change these to the correct paths in your environment: 52 | OPENSCAD_PATH = Path("/home/riskable/downloads/OpenSCAD-2022.12.06.ai12948-x86_64.AppImage") 53 | COLORSCAD_PATH = Path("/home/riskable/downloads/colorscad/colorscad.sh") 54 | 55 | KEY_UNIT = 19.05 # Square that makes up the entire space of a key 56 | BETWEENSPACE = 0.8 # Space between keycaps 57 | FILE_TYPE = "3mf" # 3mf or stl 58 | 59 | # BEGIN Async stuff 60 | # Mostly copied from https://gist.github.com/anti1869/e02c4212ce16286ea40f 61 | COMMANDS = [] 62 | MAX_RUNNERS = 8 63 | 64 | SEM = asyncio.Semaphore(MAX_RUNNERS) 65 | 66 | def run_command(cmd: str) -> str: 67 | """ 68 | Run prepared behave command in shell and return its output. 69 | :param cmd: Well-formed behave command to run. 70 | :return: Command output as string. 71 | """ 72 | try: 73 | output = subprocess.check_output( 74 | cmd, 75 | stderr=subprocess.STDOUT, 76 | universal_newlines=True, 77 | shell=True, 78 | cwd=os.getcwd(), 79 | ) 80 | 81 | except subprocess.CalledProcessError as e: 82 | output = e.output 83 | 84 | return output 85 | 86 | 87 | # @asyncio.coroutine 88 | async def run_command_on_loop(loop: asyncio.AbstractEventLoop, command: str) -> bool: 89 | """ 90 | Run test for one particular feature, check its result and return report. 91 | :param loop: Loop to use. 92 | :param command: Command to run. 93 | :return: Result of the command. 94 | """ 95 | async with SEM: 96 | runner = partial(run_command, command) 97 | output = await loop.run_in_executor(None, runner) 98 | await asyncio.sleep(1) # Slowing a bit for demonstration purposes 99 | return output 100 | 101 | 102 | @asyncio.coroutine 103 | def run_all_commands(command_list: Sequence[str] = COMMANDS) -> None: 104 | """ 105 | Run all commands in a list 106 | :param command_list: List of commands to run. 107 | """ 108 | loop = asyncio.get_event_loop() 109 | fs = [run_command_on_loop(loop, command) for command in command_list] 110 | for f in asyncio.as_completed(fs): 111 | result = yield from f 112 | ensure_future(process_result(result)) 113 | 114 | 115 | @asyncio.coroutine 116 | def process_result(result: Any): 117 | """ 118 | Do something useful with result of the commands 119 | """ 120 | print(result) 121 | 122 | # END Async stuff 123 | 124 | class riskeycap_base(Keycap): 125 | """ 126 | Base keycap definitions for the riskeycap profile + our personal prefs. 127 | """ 128 | def __init__(self, **kwargs): 129 | self.openscad_path = OPENSCAD_PATH 130 | self.colorscad_path = COLORSCAD_PATH 131 | super().__init__(**kwargs, 132 | openscad_path=self.openscad_path, 133 | colorscad_path=self.colorscad_path) 134 | self.render = ["keycap", "stem"] 135 | self.file_type = FILE_TYPE 136 | self.key_profile = "riskeycap" 137 | self.key_rotation = [0,110.1,-90] 138 | self.wall_thickness = 0.45*2.25 139 | self.uniform_wall_thickness = True 140 | self.dish_thickness = 1.0 # Note: Not actually used 141 | self.dish_corner_fn = 40 # Save some rendering time 142 | self.polygon_layers = 4 # Ditto 143 | # self.stem_type = "alps" 144 | self.stem_type = "box_cherry" 145 | self.stem_walls_inset = 0 146 | self.stem_top_thickness = 0.65 # Note: Not actually used 147 | self.stem_inside_tolerance = 0.175 148 | self.stem_side_supports = [0,0,0,0] 149 | self.stem_locations = [[0,0,0]] 150 | self.stem_sides_wall_thickness = 0.8; # Thick (good sound/feel) 151 | # Because we do strange things we need legends bigger on the Z 152 | self.scale = [ 153 | [1,1,3], 154 | [1,1.75,3], # For the pipe to make it taller/more of a divider 155 | [1,1,3], 156 | ] 157 | self.fonts = [ 158 | "Gotham Rounded:style=Bold", 159 | "Gotham Rounded:style=Bold", 160 | "Arial Black:style=Regular", 161 | ] 162 | self.font_sizes = [ 163 | 5.5, 164 | 4, # Gotham Rounded second legend (top right) 165 | 4, # Front legend 166 | ] 167 | self.trans = [ 168 | [-3,-2.6,2], # Lower left corner Gotham Rounded 169 | [3.5,3,1], # Top right Gotham Rounded 170 | [0.15,-3,2], # Front legend 171 | ] 172 | # Legend rotation 173 | self.rotation = [ 174 | [0,-20,0], 175 | [0,-20,0], 176 | [68,0,0], 177 | ] 178 | # Disabled this check because you'd have to be crazy to NOT use fast-csg 179 | # at this point haha... 180 | # # Check the OpenSCAD version to see if we can enable fast-csg 181 | # self.openscad_version = 0 182 | # self.enable_fast_csg = "" 183 | # retcode, output = getstatusoutput(f"{self.openscad_path} --version") 184 | # if retcode > 0: 185 | # raise OpenSCADException("Could not run OpenSCAD.") 186 | # else: 187 | # # Examples of retcode, output: 188 | # # (0, 'OpenSCAD version 2021.01') 189 | # # (0, 'OpenSCAD version 2022.12.06.ai12948') 190 | # openscad_full_version = output.split()[2] 191 | # self.openscad_version = int(openscad_full_version.split('.')[0]) 192 | # if self.openscad_version < 2022: 193 | # raise OpenSCADException("ERROR: This script requires OpenSCAD " 194 | # "version 2022 or later (found version " 195 | # f"{openscad_full_version})") 196 | # if self.openscad_version >= 2022: 197 | # self.enable_fast_csg = "--enable=fast-csg" 198 | self.postinit(**kwargs) 199 | 200 | class riskeycap_alphas(riskeycap_base): 201 | """ 202 | Basic alphanumeric characters (centered, big legend) 203 | """ 204 | def __init__(self, **kwargs): 205 | super().__init__(**kwargs) 206 | self.font_sizes = [ 207 | 4.5, # Regular Gotham Rounded 208 | 4, 209 | 4, # Front legend 210 | ] 211 | self.trans = [ 212 | [2.6,0,0], # Centered when angled -20° 213 | [3.5,3,1], # Top right Gotham Rounded 214 | [0.15,-3,2], # Front legend 215 | ] 216 | self.postinit(**kwargs) 217 | 218 | class riskeycap_alphas_homing_dot(riskeycap_alphas): 219 | """ 220 | Same as regular alpha but with a 3mm-wide homing "dot" 221 | """ 222 | def __init__(self, **kwargs): 223 | super().__init__(**kwargs) 224 | self.homing_dot_length = 3 225 | self.homing_dot_width = 1 226 | self.homing_dot_x = 0 227 | self.homing_dot_y = -3 228 | self.homing_dot_z = -0.45 229 | 230 | class riskeycap_numrow(riskeycap_base): 231 | """ 232 | Number row numbers are slightly different 233 | """ 234 | def __init__(self, **kwargs): 235 | super().__init__(**kwargs) 236 | self.fonts = [ 237 | "Gotham Rounded:style=Bold", # Main char 238 | "Gotham Rounded:style=Bold", # Pipe character 239 | "Gotham Rounded:style=Bold", # Symbol 240 | "Arial Black:style=Regular", # F-key 241 | ] 242 | self.font_sizes = [ 243 | 4.5, # Regular character 244 | 4.5, # Pipe 245 | 4.5, # Regular Gotham Rounded symbols 246 | 3.5, # Front legend 247 | ] 248 | self.trans = [ 249 | [-0.3,0,0], # Left Gotham Rounded 250 | [2.6,0,0], # Center Gotham Rounded | 251 | [5,0,1], # Right-side Gotham symbols 252 | [0.15,-2,2], # F-key 253 | ] 254 | self.rotation = [ 255 | [0,-20,0], 256 | [0,-20,0], 257 | [0,-20,0], 258 | [68,0,0], 259 | ] 260 | self.scale = [ 261 | [1,1,3], 262 | [1,1.75,3], # For the pipe to make it taller/more of a divider 263 | [1,1,3], 264 | [1,1,3], 265 | ] 266 | self.postinit(**kwargs) 267 | 268 | class riskeycap_tilde(riskeycap_numrow): 269 | """ 270 | Tilde needs some changes because by default it's too small 271 | """ 272 | def __init__(self, **kwargs): 273 | super().__init__(**kwargs) 274 | self.font_sizes[0] = 6.5 # ` symbol 275 | self.font_sizes[2] = 5.5 # ~ symbol 276 | self.trans[0] = [-0.3,-2.7,0] # ` 277 | self.trans[2] = [5.5,-1,1] # ~ 278 | 279 | class riskeycap_2(riskeycap_numrow): 280 | """ 281 | 2 needs some changes based on the @ symbol 282 | """ 283 | def __init__(self, **kwargs): 284 | super().__init__(**kwargs) 285 | self.fonts[2] = "Aharoni" 286 | self.font_sizes[2] = 6.1 # @ symbol (Aharoni) 287 | self.trans[2] = [4.85,0,1] 288 | self.scale[2] = [0.75,1,3] # Squash it a bit 289 | 290 | class riskeycap_3(riskeycap_numrow): 291 | """ 292 | 3 needs some changes based on the # symbol (slightly too big) 293 | """ 294 | def __init__(self, **kwargs): 295 | super().__init__(**kwargs) 296 | self.font_sizes[2] = 4.5 # # symbol (Gotham Rounded) 297 | self.trans[0] = [-0.35,0,0] # Just a smidge different so it prints better 298 | self.trans[2] = [5.3,0,1] # Move to the right a bit 299 | 300 | class riskeycap_5(riskeycap_numrow): 301 | """ 302 | 5 needs some changes based on the % symbol (too big, too close to bar) 303 | """ 304 | def __init__(self, **kwargs): 305 | super().__init__(**kwargs) 306 | self.font_sizes[2] = 4 # % symbol 307 | self.trans[2] = [5.2,0,1] 308 | 309 | class riskeycap_6(riskeycap_numrow): 310 | """ 311 | 6 needs some changes based on the ^ symbol (too small, should be up high) 312 | """ 313 | def __init__(self, **kwargs): 314 | super().__init__(**kwargs) 315 | self.font_sizes[2] = 5.8 # ^ symbol 316 | self.trans[2] = [5.3,1.5,1] 317 | 318 | class riskeycap_7(riskeycap_numrow): 319 | """ 320 | 7 needs some changes based on the & symbol (it's too big) 321 | """ 322 | def __init__(self, **kwargs): 323 | super().__init__(**kwargs) 324 | self.font_sizes[2] = 4.5 # & symbol 325 | self.trans[2] = [5.2,0,1] 326 | 327 | class riskeycap_8(riskeycap_numrow): 328 | """ 329 | 8 needs some changes based on the tiny * symbol 330 | """ 331 | def __init__(self, **kwargs): 332 | super().__init__(**kwargs) 333 | self.font_sizes[2] = 8.5 # * symbol (Gotham Rounded) 334 | self.trans[2] = [5.2,0,1] # * needs a smidge of repositioning 335 | 336 | class riskeycap_equal(riskeycap_numrow): 337 | """ 338 | = needs some changes because it and the + are a bit off center 339 | """ 340 | def __init__(self, **kwargs): 341 | super().__init__(**kwargs) 342 | self.trans[0] = [-0.3,-0.5,0] # = sign adjustments 343 | self.trans[2] = [5,-0.3,1] # + sign adjustments 344 | 345 | class riskeycap_dash(riskeycap_numrow): 346 | """ 347 | The dash (-) is fine but the underscore (_) needs minor repositioning. 348 | """ 349 | def __init__(self, **kwargs): 350 | super().__init__(**kwargs) 351 | self.trans[2] = [5.2,-1,1] # _ needs to go down and to the right a bit 352 | self.scale[2] = [0.8,1,3] # Also needs to be squished a bit 353 | 354 | class riskeycap_double_legends(riskeycap_base): 355 | """ 356 | For regular keys that have two legends... ,./;'[] 357 | """ 358 | def __init__(self, **kwargs): 359 | super().__init__(**kwargs) 360 | self.fonts = [ 361 | "Gotham Rounded:style=Bold", # Main legend 362 | "Gotham Rounded:style=Bold", # Pipe character 363 | "Gotham Rounded:style=Bold", # Second legend 364 | ] 365 | self.font_sizes = [ 366 | 4.5, # Regular Gotham Rounded character 367 | 4.5, # Pipe 368 | 4.5, # Regular Gotham Rounded character 369 | ] 370 | self.trans = [ 371 | [-0.3,0,0], # Left Gotham Rounded 372 | [2.6,0,0], # Center Gotham Rounded | 373 | [5,0,1], # Right-side Gotham symbols 374 | ] 375 | self.rotation = [ 376 | [0,-20,0], 377 | [0,-20,0], 378 | [0,-20,0], 379 | ] 380 | self.scale = [ 381 | [1,1,3], 382 | [1,1.75,3], # For the pipe to make it taller/more of a divider 383 | [1,1,3], 384 | ] 385 | self.postinit(**kwargs) 386 | 387 | class riskeycap_gt_lt(riskeycap_double_legends): 388 | """ 389 | The greater than (>) and less than (<) signs need to be adjusted down a bit 390 | """ 391 | def __init__(self, **kwargs): 392 | super().__init__(**kwargs) 393 | self.trans[0] = [-0.3,-0.1,0] # , and . are the tiniest bit too high 394 | self.trans[2] = [5.2,-0.35,1] # < and > are too high for some reason 395 | 396 | class riskeycap_brackets(riskeycap_double_legends): 397 | """ 398 | The curly braces `{}` needs to be moved to the right a smidge 399 | """ 400 | def __init__(self, **kwargs): 401 | super().__init__(**kwargs) 402 | self.trans[2] = [5.2,0,1] # Just a smidge to the right 403 | 404 | class riskeycap_semicolon(riskeycap_double_legends): 405 | """ 406 | The semicolon ends up being slightly higher than the colon but it looks 407 | better if the top dot in both is aligned. 408 | """ 409 | def __init__(self, **kwargs): 410 | super().__init__(**kwargs) 411 | self.trans[0] = [0.2,-0.4,0] 412 | self.trans[2] = [4.7,0,1] 413 | 414 | class riskeycap_1_U_text(riskeycap_alphas): 415 | """ 416 | Ctrl, Del, and Ins need to be downsized and moved a smidge. 417 | """ 418 | def __init__(self, **kwargs): 419 | super().__init__(**kwargs) 420 | kwargs_copy = deepcopy(kwargs) 421 | self.font_sizes[0] = 4 422 | self.trans[0] = [2.5,0,0] 423 | self.postinit(**kwargs_copy) 424 | 425 | class riskeycap_1_U_2_row_text(riskeycap_alphas): 426 | """ 427 | Scroll Lock, Page Up, Page Down, etc need a bunch of changes. 428 | """ 429 | def __init__(self, **kwargs): 430 | super().__init__(**kwargs) 431 | kwargs_copy = deepcopy(kwargs) 432 | self.font_sizes[0] = 2.5 433 | self.font_sizes[1] = 2.5 434 | self.trans[0] = [2.6,2,0] 435 | self.trans[1] = [2.6,-2,0] 436 | self.scale = [ 437 | [1,1,3], 438 | [1,1,3], # Back to normal 439 | [1,1,3], 440 | ] 441 | self.postinit(**kwargs_copy) 442 | 443 | class riskeycap_arrows(riskeycap_alphas): 444 | """ 445 | Arrow symbols (◀▶▲▼) needs a different font (Hack) 446 | """ 447 | def __init__(self, **kwargs): 448 | super().__init__(**kwargs) 449 | self.fonts[0] = "Hack" 450 | self.fonts[2] = "FontAwesome" # For next/prev track icons 451 | self.font_sizes[2] = 4 # FontAwesome prev/next icons 452 | self.trans[2] = [0,-2,2] # Ditto 453 | 454 | class riskeycap_fontawesome(riskeycap_alphas): 455 | """ 456 | For regular centered FontAwesome icon keycaps. 457 | """ 458 | def __init__(self, **kwargs): 459 | super().__init__(**kwargs) 460 | kwargs_copy = deepcopy(kwargs) # Because self.trans[0] updates in place 461 | self.fonts[0] = "Font Awesome 6 Free:style=Solid" 462 | self.font_sizes[0] = 5 463 | self.trans[0] = [2.6,0.3,0] 464 | self.postinit(**kwargs_copy) 465 | 466 | class riskeycap_material_icons(riskeycap_alphas): 467 | """ 468 | For regular centered Material Design icon keycaps. 469 | """ 470 | def __init__(self, **kwargs): 471 | super().__init__(**kwargs) 472 | kwargs_copy = deepcopy(kwargs) # Because self.trans[0] updates in place 473 | self.fonts[0] = "Material Design Icons:style=Regular" 474 | self.font_sizes[0] = 6 475 | self.trans[0] = [2.6,0.3,0] 476 | self.postinit(**kwargs_copy) 477 | 478 | class riskeycap_1_25U(riskeycap_alphas): 479 | """ 480 | The base for all 1.25U keycaps. 481 | """ 482 | def __init__(self, **kwargs): 483 | super().__init__(**kwargs) 484 | kwargs_copy = deepcopy(kwargs) # Because self.trans[0] updates in place 485 | self.key_length = KEY_UNIT*1.25-BETWEENSPACE 486 | self.key_rotation = [0,110.1,-90] 487 | self.trans[0] = [3,0.2,0] 488 | self.postinit(**kwargs_copy) 489 | if not self.name.startswith('1.25U_'): 490 | self.name = f"1.25U_{self.name}" 491 | 492 | class riskeycap_1_5U(riskeycap_double_legends): 493 | """ 494 | The base for all 1.5U keycaps. 495 | 496 | .. note:: Uses riskeycap_double_legends because of the \\| key. 497 | """ 498 | def __init__(self, **kwargs): 499 | super().__init__(**kwargs) 500 | kwargs_copy = deepcopy(kwargs) # Because self.trans[0] updates in place 501 | self.key_length = KEY_UNIT*1.5-BETWEENSPACE 502 | self.key_rotation = [0,109.335,-90] 503 | self.trans[0] = [3,0.2,0] 504 | self.postinit(**kwargs_copy) 505 | if not self.name.startswith('1.5U_'): 506 | self.name = f"1.5U_{self.name}" 507 | 508 | 509 | class riskeycap_bslash_1U(riskeycap_double_legends): 510 | """ 511 | Backslash key needs a very minor adjustment to the backslash. 512 | """ 513 | def __init__(self, **kwargs): 514 | super().__init__(**kwargs) 515 | self.trans[0] = [-0.9,0,0] # Move \ to the left a bit more than normal 516 | 517 | class riskeycap_bslash(riskeycap_1_5U): 518 | """ 519 | Backslash key needs a very minor adjustment to the backslash. 520 | """ 521 | def __init__(self, **kwargs): 522 | super().__init__(**kwargs) 523 | self.trans[0] = [-0.9,0,0] # Move \ to the left a bit more than normal 524 | 525 | class riskeycap_tab(riskeycap_1_5U): 526 | """ 527 | "Tab" needs to be centered. 528 | """ 529 | def __init__(self, **kwargs): 530 | super().__init__(**kwargs) 531 | self.font_sizes[0] = 4.5 # Regular Gotham Rounded 532 | self.trans[0] = [2.6,0,0] # Centered when angled -20° 533 | 534 | class riskeycap_1_75U(riskeycap_alphas): 535 | """ 536 | The base for all 1.75U keycaps. 537 | """ 538 | def __init__(self, **kwargs): 539 | super().__init__(**kwargs) 540 | kwargs_copy = deepcopy(kwargs) # Because self.trans[0] updates in place 541 | self.key_length = KEY_UNIT*1.75-BETWEENSPACE 542 | self.key_rotation = [0,109.335,-90] 543 | self.postinit(**kwargs_copy) 544 | if not self.name.startswith('1.75U_'): 545 | self.name = f"1.75U_{self.name}" 546 | 547 | class riskeycap_2U(riskeycap_alphas): 548 | """ 549 | The base for all 2U keycaps. 550 | """ 551 | def __init__(self, **kwargs): 552 | super().__init__(**kwargs) 553 | kwargs_copy = deepcopy(kwargs) # Because self.trans[0] updates in place 554 | self.key_length = KEY_UNIT*2-BETWEENSPACE 555 | self.key_rotation = [0,109.335,-90] # Same as 1.75U 556 | if "dish_invert" in kwargs and kwargs["dish_invert"]: 557 | self.key_rotation = [0,113.65,-90] # Spacebars are different 558 | self.stem_locations = [[0,0,0], [12,0,0], [-12,0,0]] 559 | self.postinit(**kwargs_copy) 560 | if not self.name.startswith('2U_'): 561 | self.name = f"2U_{self.name}" 562 | 563 | class riskeycap_2UV(riskeycap_alphas): 564 | """ 565 | The base for all 2U (vertical; for numpad) keycaps. 566 | """ 567 | def __init__(self, **kwargs): 568 | super().__init__(**kwargs) 569 | kwargs_copy = deepcopy(kwargs) # Because self.trans[0] updates in place 570 | self.key_length = KEY_UNIT*1-BETWEENSPACE 571 | self.key_width = KEY_UNIT*2-BETWEENSPACE 572 | self.key_rotation = [0,109.335,-90] # Same as 1.75U 573 | if "dish_invert" in kwargs and kwargs["dish_invert"]: 574 | self.key_rotation = [0,113.65,-90] # Spacebars are different 575 | self.stem_locations = [[0,0,0], [0,12,0], [0,-12,0]] 576 | self.trans = [ 577 | [0.1,-0.1,0], # Left Gotham Rounded 578 | [0,0,0], # Unused (so far) 579 | [0,0,0], # Ditto 580 | ] 581 | self.rotation = [ 582 | [0,0,0], 583 | [0,0,0], 584 | [0,0,0], 585 | ] 586 | self.postinit(**kwargs_copy) 587 | if not self.name.startswith('2UV_'): 588 | self.name = f"2UV_{self.name}" 589 | 590 | class riskeycap_2_25U(riskeycap_alphas): 591 | """ 592 | The base for all 2.25U keycaps. 593 | """ 594 | def __init__(self, **kwargs): 595 | super().__init__(**kwargs) 596 | kwargs_copy = deepcopy(kwargs) 597 | self.key_length = KEY_UNIT*2.25-BETWEENSPACE 598 | self.key_rotation = [0,109.335,-90] # Same as 1.75U and 2U 599 | if "dish_invert" in kwargs and kwargs["dish_invert"]: 600 | self.key_rotation = [0,113.65,-90] # Spacebars are different 601 | self.stem_locations = [[0,0,0], [12,0,0], [-12,0,0]] 602 | self.trans[0] = [3.1,0.2,0] 603 | self.font_sizes[0] = 4 604 | self.postinit(**kwargs_copy) 605 | if not self.name.startswith('2.25U_'): 606 | self.name = f"2.25U_{self.name}" 607 | 608 | class riskeycap_2_5U(riskeycap_alphas): 609 | """ 610 | The base for all 2.5U keycaps. 611 | """ 612 | def __init__(self, **kwargs): 613 | super().__init__(**kwargs) 614 | kwargs_copy = deepcopy(kwargs) 615 | self.key_length = KEY_UNIT*2.5-BETWEENSPACE 616 | self.key_rotation = [0,109.335,-90] # Same as 1.75U and 2U 617 | if "dish_invert" in kwargs and kwargs["dish_invert"]: 618 | self.key_rotation = [0,113.65,-90] # Spacebars are different 619 | self.stem_locations = [[0,0,0], [12,0,0], [-12,0,0]] 620 | self.trans[0] = [3.1,0.2,0] 621 | self.font_sizes[0] = 4 622 | self.postinit(**kwargs_copy) 623 | if not self.name.startswith('2.5U_'): 624 | self.name = f"2.5U_{self.name}" 625 | 626 | class riskeycap_2_75U(riskeycap_alphas): 627 | """ 628 | The base for all 2.75U keycaps. 629 | """ 630 | def __init__(self, **kwargs): 631 | super().__init__(**kwargs) 632 | kwargs_copy = deepcopy(kwargs) 633 | self.key_length = KEY_UNIT*2.75-BETWEENSPACE 634 | self.key_rotation = [0,109.335,-90] # Same as 1.75U and 2U 635 | if "dish_invert" in kwargs and kwargs["dish_invert"]: 636 | self.key_rotation = [0,113.65,-90] # Spacebars are different 637 | self.stem_locations = [[0,0,0], [12,0,0], [-12,0,0]] 638 | self.trans[0] = [3.1,0.2,0] 639 | self.font_sizes[0] = 4 640 | self.postinit(**kwargs_copy) 641 | if not self.name.startswith('2.75U_'): 642 | self.name = f"2.75U_{self.name}" 643 | 644 | class riskeycap_6_25U(riskeycap_alphas): 645 | """ 646 | The base for all 6.25U keycaps. 647 | """ 648 | def __init__(self, **kwargs): 649 | super().__init__(**kwargs) 650 | kwargs_copy = deepcopy(kwargs) 651 | self.key_length = KEY_UNIT*6.25-BETWEENSPACE 652 | self.key_rotation = [0,109.335,-90] # Same as 1.75U and 2U 653 | if "dish_invert" in kwargs and kwargs["dish_invert"]: 654 | self.key_rotation = [0,113.65,-90] # Spacebars are different 655 | self.stem_locations = [[0,0,0], [50,0,0], [-50,0,0]] 656 | self.trans[0] = [3.1,0.2,0] 657 | self.font_sizes[0] = 4 658 | self.postinit(**kwargs_copy) 659 | if not self.name.startswith('6.25U_'): 660 | self.name = f"6.25U_{self.name}" 661 | 662 | class riskeycap_7U(riskeycap_alphas): 663 | """ 664 | The base for all 7U keycaps. 665 | """ 666 | def __init__(self, **kwargs): 667 | super().__init__(**kwargs) 668 | kwargs_copy = deepcopy(kwargs) 669 | self.key_length = KEY_UNIT*7-BETWEENSPACE 670 | self.key_rotation = [0,109.335,-90] # Same as 1.75U and 2U 671 | if "dish_invert" in kwargs and kwargs["dish_invert"]: 672 | self.key_rotation = [0,113.65,-90] # Spacebars are different 673 | self.stem_locations = [[0,0,0], [57,0,0], [-57,0,0]] 674 | self.trans[0] = [3.1,0.2,0] 675 | self.font_sizes[0] = 4 676 | self.postinit(**kwargs_copy) 677 | if not self.name.startswith('7U_'): 678 | self.name = f"7U_{self.name}" 679 | 680 | KEYCAPS = [ 681 | # Basic 1U keys 682 | riskeycap_base(name="1U_blank"), 683 | riskeycap_tilde(name="tilde", legends=["`", "", "~"]), 684 | riskeycap_numrow(legends=["1", "", "!"]), 685 | riskeycap_2(legends=["2", "", "@"]), 686 | riskeycap_3(legends=["3", "", "#"]), 687 | riskeycap_numrow(legends=["4", "", "$"]), 688 | riskeycap_5(legends=["5", "", "%"]), 689 | riskeycap_6(legends=["6", "", "^"]), 690 | riskeycap_7(legends=["7", "", "&"]), 691 | riskeycap_8(legends=["8", "", "*"]), 692 | riskeycap_numrow(legends=["9", "", "("]), 693 | riskeycap_numrow(legends=["0", "", ")"]), 694 | riskeycap_dash(name="dash", legends=["-", "", "_"]), 695 | riskeycap_equal(name="equal", legends=["=", "", "+"]), 696 | # Alphas 697 | riskeycap_alphas(legends=["A"]), 698 | riskeycap_alphas(legends=["B"]), 699 | riskeycap_alphas(legends=["C"]), 700 | riskeycap_alphas(legends=["D"]), 701 | riskeycap_alphas(legends=["E"]), 702 | riskeycap_alphas(legends=["F"]), 703 | riskeycap_alphas_homing_dot(name="F_dot", legends=["F"]), 704 | riskeycap_alphas(legends=["G"]), 705 | riskeycap_alphas(legends=["H"]), 706 | riskeycap_alphas(legends=["I"]), 707 | riskeycap_alphas(legends=["J"]), 708 | riskeycap_alphas_homing_dot(name="J_dot", legends=["J"]), 709 | riskeycap_alphas(legends=["K"]), 710 | riskeycap_alphas(legends=["L"]), 711 | riskeycap_alphas(legends=["M"]), 712 | riskeycap_alphas(legends=["N"]), 713 | riskeycap_alphas(legends=["O"]), 714 | riskeycap_alphas(legends=["P"]), 715 | riskeycap_alphas(legends=["Q"]), 716 | riskeycap_alphas(legends=["R"]), 717 | riskeycap_alphas(legends=["S"]), 718 | riskeycap_alphas(legends=["T"]), 719 | riskeycap_alphas(legends=["U"]), 720 | riskeycap_alphas(legends=["V"]), 721 | riskeycap_alphas(legends=["W"]), 722 | riskeycap_alphas(legends=["X"]), 723 | riskeycap_alphas(legends=["Y"]), 724 | riskeycap_alphas(legends=["Z"]), 725 | riskeycap_alphas(legends=["Z"]), 726 | # Function keys 727 | riskeycap_alphas(legends=["F1"]), 728 | riskeycap_alphas(legends=["F2"]), 729 | riskeycap_alphas(legends=["F3"]), 730 | riskeycap_alphas(legends=["F4"]), 731 | riskeycap_alphas(legends=["F5"]), 732 | riskeycap_alphas(legends=["F6"]), 733 | riskeycap_alphas(legends=["F7"]), 734 | riskeycap_alphas(legends=["F8"]), 735 | riskeycap_alphas(legends=["F9"]), 736 | # F10 needs to be shifted to the left a bit so that when printing on its 737 | # side the 0 doesn't end up in the wall: 738 | riskeycap_alphas(legends=["F10"], font_sizes=[4.25], trans=[[2.4,0,0]]), 739 | riskeycap_alphas(legends=["F11"], font_sizes=[4.25]), 740 | riskeycap_alphas(legends=["F12"], font_sizes=[4.25]), 741 | # Bottom row(s) and sides 1U 742 | riskeycap_alphas(name="menu", legends=["☰"], fonts=["Code2000"]), 743 | riskeycap_alphas(name="option1U", legends=["⌥"], 744 | fonts=["JetBrainsMono Nerd Font"], font_sizes=[6]), 745 | riskeycap_arrows(name="left", legends=["◀", "", ""]), 746 | riskeycap_arrows(name="right", legends=["▶", "", ""]), 747 | riskeycap_arrows(name="left_rw", legends=["◀", "", ""]), 748 | riskeycap_arrows(name="right_ffw", legends=["▶", "", ""]), 749 | riskeycap_arrows(name="up", legends=["▲", "", ""]), 750 | riskeycap_arrows(name="down", legends=["▼", "", "", ""]), 751 | riskeycap_fontawesome(name="eject", legends=[""]), # Mostly for Macs 752 | riskeycap_fontawesome(name="bug", legends=[""]), # Just for fun 753 | riskeycap_fontawesome(name="camera", legends=[""]), # aka Screenshot aka Print Screen 754 | riskeycap_fontawesome(name="paws", legends=[""]), # Alternate "Paws" key (hehe) 755 | riskeycap_fontawesome(name="home_icon", legends=[""]), # Alternate "Home" key 756 | riskeycap_fontawesome(name="broom", legends=[""]), 757 | riskeycap_fontawesome(name="dragon", legends=[""]), 758 | riskeycap_fontawesome(name="baby", legends=[""]), 759 | riskeycap_fontawesome(name="dungeon", legends=[""]), 760 | riskeycap_fontawesome(name="wizard", legends=[""]), 761 | riskeycap_fontawesome(name="headset", legends=[""]), 762 | riskeycap_fontawesome(name="skull_n_bones", legends=[""]), 763 | riskeycap_fontawesome(name="bath", legends=[""]), 764 | riskeycap_fontawesome(name="keyboard", legends=[""]), 765 | riskeycap_fontawesome(name="terminal", legends=[""]), 766 | riskeycap_fontawesome(name="spy", legends=[""]), 767 | riskeycap_fontawesome(name="biohazard", legends=[""]), 768 | riskeycap_fontawesome(name="bandage", legends=[""]), 769 | riskeycap_fontawesome(name="bone", legends=[""]), 770 | riskeycap_fontawesome(name="cannabis", legends=[""]), 771 | riskeycap_fontawesome(name="radiation", legends=[""], font_sizes=[6]), 772 | riskeycap_fontawesome(name="crutch", legends=[""]), 773 | riskeycap_fontawesome(name="head_side_cough", legends=[""]), 774 | riskeycap_fontawesome(name="mortar_and_pestle", legends=[""]), 775 | riskeycap_fontawesome(name="poop", legends=[""]), 776 | riskeycap_fontawesome(name="bomb", legends=[""]), 777 | riskeycap_fontawesome(name="thunderstorm", legends=[""]), 778 | riskeycap_fontawesome(name="dumpster_fire", legends=[""]), 779 | riskeycap_fontawesome(name="flask", legends=[""]), 780 | riskeycap_fontawesome(name="middle_finger", legends=[""]), 781 | riskeycap_fontawesome(name="hurricane", legends=[""]), 782 | riskeycap_fontawesome(name="light_bulb", legends=[""]), 783 | riskeycap_fontawesome(name="male", legends=[""]), 784 | riskeycap_fontawesome(name="female", legends=[""]), 785 | riskeycap_fontawesome(name="microphone", legends=[""]), 786 | riskeycap_fontawesome(name="person_falling", legends=[""]), 787 | riskeycap_fontawesome(name="shitstorm", legends=[""]), 788 | riskeycap_fontawesome(name="toilet", legends=[""]), 789 | riskeycap_fontawesome(name="wifi", legends=[""]), 790 | riskeycap_fontawesome(name="yinyang", legends=[""]), 791 | riskeycap_fontawesome(name="ban", legends=[""]), 792 | riskeycap_fontawesome(name="lemon", legends=[""], font_sizes=[6]), 793 | riskeycap_material_icons(name="duck", legends=[""], font_sizes=[7]), 794 | riskeycap_alphas(name="die_1", legends=["⚀"], font_sizes=[7], fonts=["DejaVu Sans:style=Bold"]), # Dice (number alternate) 795 | riskeycap_alphas(name="die_2", legends=["⚁"], font_sizes=[7], fonts=["DejaVu Sans:style=Bold"]), # Dice (number alternate) 796 | riskeycap_alphas(name="die_3", legends=["⚂"], font_sizes=[7], fonts=["DejaVu Sans:style=Bold"]), # Dice (number alternate) 797 | riskeycap_alphas(name="die_4", legends=["⚃"], font_sizes=[7], fonts=["DejaVu Sans:style=Bold"]), # Dice (number alternate) 798 | riskeycap_alphas(name="die_5", legends=["⚄"], font_sizes=[7], fonts=["DejaVu Sans:style=Bold"]), # Dice (number alternate) 799 | riskeycap_alphas(name="die_6", legends=["⚅"], font_sizes=[7], fonts=["DejaVu Sans:style=Bold"]), # Dice (number alternate) 800 | riskeycap_1_U_text(name="RCtrl", legends=["Ctrl"]), 801 | riskeycap_1_U_text(legends=["Del"]), 802 | riskeycap_1_U_text(legends=["Ins"]), 803 | riskeycap_1_U_text(legends=["Esc"]), 804 | riskeycap_1_U_text(legends=["End"]), 805 | riskeycap_1_U_text(legends=["BRB"], scale=[[0.75,1,3]]), 806 | riskeycap_1_U_text(legends=["OMG"], font_sizes=[3.75], scale=[[0.75,1,3]]), 807 | riskeycap_1_U_text(legends=["WTF"], font_sizes=[3.75], scale=[[0.75,1,3]]), 808 | riskeycap_1_U_text(legends=["BBL"], scale=[[0.75,1,3]]), 809 | riskeycap_1_U_text(legends=["CYA"], scale=[[0.75,1,3]]), 810 | riskeycap_1_U_text(legends=["IDK"], scale=[[0.75,1,3]]), 811 | riskeycap_1_U_text(legends=["ASS"], scale=[[0.75,1,3]]), 812 | riskeycap_1_U_text(legends=["ANY", "", "KEY"], scale=[[0.75,1,3]], fonts = [ 813 | "Gotham Rounded:style=Bold", 814 | "Gotham Rounded:style=Bold", 815 | "Gotham Rounded:style=Bold", 816 | ], font_sizes=[4, 4, 4.15]), # 4.15 here works around a minor slicing issue 817 | riskeycap_1_U_text(legends=["OK"]), 818 | riskeycap_1_U_text(legends=["NO"]), 819 | riskeycap_1_U_text(legends=["Yes"]), 820 | riskeycap_1_U_text(legends=["DO"]), 821 | riskeycap_1_U_2_row_text(name="DO_NOT", legends=["DO", "NOT"], 822 | trans=[[2.7,2.75,0],[2.7,-2,0]], font_sizes=[3.5, 3.5]), 823 | riskeycap_1_U_text(legends=["FUBAR"], font_sizes=[3.25], scale=[[0.55,1,3]]), 824 | riskeycap_1_U_text(legends=["Home"], font_sizes=[2.75]), 825 | riskeycap_1_U_2_row_text(name="PageUp", 826 | legends=["Page", "Up"], font_sizes=[2.75, 2.75]), 827 | riskeycap_1_U_2_row_text(name="PageDown", 828 | legends=["Page", "Down"], font_sizes=[2.75, 2.75]), 829 | riskeycap_1_U_text(legends=["Pause"], font_sizes=[2.5]), 830 | riskeycap_1_U_2_row_text(name="ScrollLock", legends=["Scroll", "Lock"]), 831 | riskeycap_1_U_text(legends=["Sup"]), 832 | riskeycap_brackets(name="lbracket", legends=["[", "", "{"]), 833 | riskeycap_brackets(name="rbracket", legends=["]", "", "}"]), 834 | riskeycap_semicolon(name="semicolon", legends=[";", "", ":"]), 835 | riskeycap_double_legends(name="quote", legends=["'", "", '\\u0022']), 836 | riskeycap_gt_lt(name="comma", legends=[",", "", "<"]), 837 | riskeycap_gt_lt(name="dot", legends=[".", "", ">"]), 838 | riskeycap_double_legends(name="slash", legends=["/", "", "?"]), 839 | # 60% and smaller numrow (with function key legends on the front) 840 | riskeycap_numrow(name="1_F1", legends=["1", "", "!", "F1"]), 841 | riskeycap_2(name="2_F2", legends=["2", "", "@", "F2"]), 842 | riskeycap_3(name="3_F3", legends=["3", "", "#", "F3"]), 843 | riskeycap_numrow(name="4_F4", legends=["4", "", "$", "F4"]), 844 | riskeycap_5(name="5_F5", legends=["5", "", "%", "F5"]), 845 | riskeycap_6(name="6_F6", legends=["6", "", "^", "F6"]), 846 | riskeycap_7(name="7_F7", legends=["7", "", "&", "F7"]), 847 | riskeycap_8(name="8_F8", legends=["8", "", "*", "F8"]), 848 | riskeycap_numrow(name="9_F9", legends=["9", "", "(", "F9"]), 849 | riskeycap_numrow(name="0_F10", legends=["0", "", ")", "F10"]), 850 | riskeycap_dash(name="dash_F11", legends=["-", "", "_", "F11"]), 851 | riskeycap_equal(name="equal_F12", legends=["=", "", "+", "F12"]), 852 | # 1.25U keys 853 | riskeycap_1_25U(name="blank"), 854 | riskeycap_1_25U(name="LCtrl", legends=["Ctrl"], font_sizes=[4]), 855 | riskeycap_1_25U(name="LAlt", legends=["Alt"], font_sizes=[4]), 856 | riskeycap_1_25U(name="RAlt", legends=["Alt Gr"], font_sizes=[3.25]), 857 | riskeycap_1_25U(name="Command", legends=["Cmd"], font_sizes=[4]), 858 | riskeycap_1_25U(name="CommandSymbol", legends=["⌘"], font_sizes=[7], fonts=["Agave"]), 859 | riskeycap_1_25U(name="OptionSymbol", legends=["⌥"], font_sizes=[6], fonts=["JetBrainsMono Nerd Font"]), 860 | riskeycap_1_25U(name="Option", legends=["Option"], font_sizes=[2.9]), 861 | riskeycap_1_25U(name="Fun", legends=["Fun"], font_sizes=[4]), 862 | riskeycap_1_25U(name="MoreFun", 863 | legends=["More", "Fun"], 864 | trans=[[3,2.5,0], [3,-2.5,0]], 865 | font_sizes=[4, 4], 866 | scale=[[1,1,3], [1,1,3]]), 867 | riskeycap_1_25U(legends=["Super", "Duper"], 868 | trans=[[3,2.25,0], [3,-2.25,0]], font_sizes=[3.25, 3.25], 869 | scale=[[1,1,3], [1,1,3]]), 870 | # 1.5U keys 871 | riskeycap_1_5U(name="blank"), 872 | riskeycap_bslash_1U(name="bslash", legends=["\\u005c", "", "|"]), 873 | riskeycap_bslash(name="bslash", legends=["\\u005c", "", "|"]), 874 | riskeycap_tab(name="Tab", legends=["Tab"]), 875 | riskeycap_1_5U(name="Ctrl", legends=["Ctrl"], font_sizes=[4]), 876 | riskeycap_1_5U(name="LAlt", legends=["Alt"], font_sizes=[4]), 877 | riskeycap_1_5U(name="RAlt", legends=["Alt Gr"], font_sizes=[4]), 878 | # 1.75U keys 879 | riskeycap_1_75U(name="blank"), 880 | riskeycap_1_75U(legends=["Compose"], 881 | trans=[[3.1,0.2,0]], font_sizes=[3.25]), 882 | riskeycap_1_75U(name="Caps", 883 | legends=["Caps Lock"], trans=[[3.1,0,0]], font_sizes=[3]), 884 | #riskeycap_1_75U(name="Laps", 885 | #legends=["Laps Cock"], trans=[[3.1,0,0]], font_sizes=[3]), 886 | riskeycap_1_75U(name="Laps", 887 | legends=["Laps", "Cock"], trans=[[3,2.5,0], [3,-2.5,0]], 888 | font_sizes=[4, 4], scale=[[1,1,3], [1,1,3]]), 889 | riskeycap_1_75U(name="RubOut", 890 | legends=["Rub Out"], trans=[[3.1,0,0]], font_sizes=[3]), 891 | riskeycap_1_75U(name="Rub1Out", 892 | legends=["Rub 1 Out"], trans=[[3.1,0,0]], font_sizes=[3]), 893 | riskeycap_1_75U(legends=["Chyros"], 894 | trans=[[3.1,0,0]], font_sizes=[4.35]), 895 | # 2U keys 896 | riskeycap_2U(name="blank"), 897 | riskeycap_2U(name="TOTALBS", 898 | legends=["TOTAL BS"], font_sizes=[3.75, 3.75]), 899 | riskeycap_2U(name="Backspace", font_sizes=[3.75, 3.75]), 900 | riskeycap_2U(name="Rub Out", 901 | legends=["Rub Out"], font_sizes=[4, 4]), 902 | riskeycap_2U(name="Rub 1 Out", 903 | legends=["Rub 1 Out"], font_sizes=[3.75, 3.75]), 904 | riskeycap_2U(name="2U_space", 905 | # Spacebars don't need to be as thick 906 | stem_sides_wall_thickness=0.0, dish_invert=True), 907 | # 2.25U keys 908 | riskeycap_2_25U(name="blank"), 909 | riskeycap_2_25U(name="Shift", legends=["Shift"]), 910 | riskeycap_2_25U(name="ShiftyShift", 911 | legends=["Shift"], trans=[[9.5,-2.8,0]]), 912 | riskeycap_2_25U(name="ShiftyShiftL", 913 | legends=["Shift"], trans=[[-4,-2.8,0]]), 914 | riskeycap_2_25U(name="TrueShift", legends=["True Shift"]), 915 | riskeycap_2_25U(legends=["Return"]), 916 | riskeycap_2_25U(legends=["Enter"]), 917 | # 2.5U keys 918 | riskeycap_2_5U(name="blank"), 919 | riskeycap_2_5U(name="Shift", legends=["Shift"]), 920 | riskeycap_2_5U(name="ShiftyShift", 921 | legends=["Shift"], trans=[[12,-2.8,0]]), 922 | riskeycap_2_5U(name="ShiftyShiftL", 923 | legends=["Shift"], trans=[[-6.5,-2.8,0]]), 924 | riskeycap_2_5U(name="TrueShift", legends=["True Shift"]), 925 | ## 2.75U keys 926 | riskeycap_2_75U(name="blank"), 927 | riskeycap_2_75U(name="Shift", legends=["Shift"]), 928 | riskeycap_2_75U(name="ShiftyShift", 929 | legends=["Shift"], trans=[[14,-2.8,0]]), 930 | riskeycap_2_75U(name="ShiftyShiftL", 931 | legends=["Shift"], trans=[[-8.5,-2.8,0]]), 932 | riskeycap_2_75U(name="TrueShift", legends=["True Shift"]), 933 | # Various spacebars 934 | riskeycap_6_25U(name="space", 935 | # Spacebars don't need to be as thick 936 | stem_sides_wall_thickness=0.0, dish_invert=True), 937 | riskeycap_7U(name="space", 938 | # Spacebars don't need to be as thick 939 | stem_sides_wall_thickness=0.0, dish_invert=True), 940 | # Numpad keycaps 941 | riskeycap_alphas(name="numpad1", legends=["1"]), 942 | riskeycap_alphas(name="numpad2", legends=["2"]), 943 | riskeycap_alphas(name="numpad3", legends=["3"]), 944 | riskeycap_alphas(name="numpad4", legends=["4"]), 945 | riskeycap_alphas(name="numpad5", legends=["5"]), 946 | riskeycap_alphas(name="numpad6", legends=["6"]), 947 | riskeycap_alphas(name="numpad7", legends=["7"]), 948 | riskeycap_alphas(name="numpad8", legends=["8"]), 949 | riskeycap_alphas(name="numpad9", legends=["9"]), 950 | riskeycap_alphas(name="numpad0", legends=["0"]), 951 | riskeycap_alphas(name="numpadplus", legends=["+"], font_sizes=[6]), 952 | riskeycap_2UV(name="2UV_numpadplus", legends=["+"], font_sizes=[6], 953 | trans=[[0.5,-0.1,0]]), 954 | riskeycap_2UV(name="2UV_numpadenter", legends=["↵"], 955 | fonts=["OverpassMono Nerd Font:style=Bold"], font_sizes=[7]), 956 | riskeycap_2U(name="2U_numpad0", legends=["0"], 957 | font_sizes=[4.5]), 958 | riskeycap_alphas(name="numpaddot", legends=["."], font_sizes=[6]), 959 | riskeycap_alphas(name="numlock", legends=["Num"], font_sizes=[3.25],), 960 | riskeycap_alphas(name="numpadslash", legends=["/"]), 961 | riskeycap_alphas(name="numpadstar", legends=["*"], font_sizes=[8.5]), 962 | # Default width of - is a bit too skinny so we scale/adjust it a bit: 963 | riskeycap_alphas(name="numpadminus", legends=["-"], 964 | font_sizes=[6], scale=[[1.4,1,3]], trans = [[2.9,0,0]]), 965 | ] 966 | 967 | def print_keycaps(): 968 | """ 969 | Prints the names of all keycaps in KEYCAPS. 970 | """ 971 | print(Style.BRIGHT + 972 | f"Here's all the keycaps we can render:\n" + Style.RESET_ALL) 973 | keycap_names = ", ".join(a.name for a in KEYCAPS) 974 | print(f"{keycap_names}") 975 | 976 | if __name__ == "__main__": 977 | parser = argparse.ArgumentParser( 978 | description="Render a full set of riskeycap keycaps.") 979 | parser.add_argument('--out', 980 | metavar='', type=str, default=".", 981 | help='Where the generated files will go.') 982 | parser.add_argument('--force', 983 | required=False, action='store_true', 984 | help='Forcibly re-render keycaps even if they already exist.') 985 | parser.add_argument('--legends', 986 | required=False, action='store_true', 987 | help=f'If True, generate a separate set of {FILE_TYPE} files for legends.') 988 | parser.add_argument('--keycaps', 989 | required=False, action='store_true', 990 | help='If True, prints out the names of all keycaps we can render.') 991 | parser.add_argument('names', 992 | nargs='*', metavar="name", 993 | help='Optional name of specific keycap you wish to render') 994 | args = parser.parse_args() 995 | #print(args) 996 | if len(sys.argv) == 1: 997 | parser.print_help() 998 | print("") 999 | print_keycaps() 1000 | sys.exit(1) 1001 | if args.keycaps: 1002 | print_keycaps() 1003 | sys.exit(1) 1004 | if not os.path.exists(args.out): 1005 | print(Style.BRIGHT + 1006 | f"Output path, '{args.out}' does not exist; making it..." 1007 | + Style.RESET_ALL) 1008 | os.mkdir(args.out) 1009 | print(Style.BRIGHT + f"Outputting to: {args.out}" + Style.RESET_ALL) 1010 | if args.names: # Just render the specified keycaps 1011 | matched = False 1012 | for name in args.names: 1013 | for keycap in KEYCAPS: 1014 | if keycap.name.lower() == name.lower(): 1015 | keycap.output_path = f"{args.out}" 1016 | matched = True 1017 | exists = False 1018 | if not args.force: 1019 | if os.path.exists(f"{args.out}/{keycap.name}.{keycap.file_type}"): 1020 | print(Style.BRIGHT + 1021 | f"{args.out}/{keycap.name}.{keycap.file_type} exists; " 1022 | f"skipping..." 1023 | + Style.RESET_ALL) 1024 | exists = True 1025 | if not exists: 1026 | print(Style.BRIGHT + 1027 | f"Rendering {args.out}/{keycap.name}.{keycap.file_type}..." 1028 | + Style.RESET_ALL) 1029 | print(keycap) 1030 | COMMANDS.append(str(keycap)) 1031 | # retcode, output = getstatusoutput(str(keycap)) 1032 | # if retcode == 0: # Success! 1033 | # print( 1034 | # f"{args.out}/{keycap.name}.{keycap.file_type} " 1035 | # f"rendered successfully") 1036 | if args.legends: 1037 | keycap.name = f"{keycap.name}_legends" 1038 | # Change it to .stl since PrusaSlicer doesn't like .3mf 1039 | # for "parts" for unknown reasons... 1040 | keycap.file_type = "stl" 1041 | if os.path.exists(f"{args.out}/{keycap.name}.{keycap.file_type}"): 1042 | print(Style.BRIGHT + 1043 | f"{args.out}/{keycap.name}.{keycap.file_type} exists; " 1044 | f"skipping..." 1045 | + Style.RESET_ALL) 1046 | continue 1047 | print(Style.BRIGHT + 1048 | f"Rendering {args.out}/{keycap.name}.{keycap.file_type}..." 1049 | + Style.RESET_ALL) 1050 | print(keycap) 1051 | COMMANDS.append(str(keycap)) 1052 | # retcode, output = getstatusoutput(str(keycap)) 1053 | # if retcode == 0: # Success! 1054 | # print( 1055 | # f"{args.out}/{keycap.name}.{keycap.file_type} " 1056 | # f"rendered successfully") 1057 | if not matched: 1058 | print(f"Cound not find a keycap named {name}") 1059 | else: 1060 | # First render the keycaps 1061 | for keycap in KEYCAPS: 1062 | keycap.output_path = f"{args.out}" 1063 | if not args.force: 1064 | if os.path.exists(f"{args.out}/{keycap.name}.{keycap.file_type}"): 1065 | print(Style.BRIGHT + 1066 | f"{args.out}/{keycap.name}.{keycap.file_type} exists; skipping..." 1067 | + Style.RESET_ALL) 1068 | continue 1069 | print(Style.BRIGHT + 1070 | f"Rendering {args.out}/{keycap.name}.{keycap.file_type}..." 1071 | + Style.RESET_ALL) 1072 | print(keycap) 1073 | COMMANDS.append(str(keycap)) 1074 | # retcode, output = getstatusoutput(str(keycap)) 1075 | # if retcode == 0: # Success! 1076 | # print(f"{args.out}/{keycap.name}.{keycap.file_type} rendered successfully") 1077 | # Next render the legends (for multi-material, non-transparent legends) 1078 | if args.legends: 1079 | for legend in KEYCAPS: 1080 | if legend.legends == [""]: 1081 | continue # No actual legends 1082 | legend.name = f"{legend.name}_legends" 1083 | legend.output_path = f"{args.out}" 1084 | legend.render = ["legends"] 1085 | # Change it to .stl since PrusaSlicer doesn't like .3mf 1086 | # for "parts" for unknown reasons... 1087 | legend.file_type = "stl" 1088 | if not args.force: 1089 | if os.path.exists(f"{args.out}/{legend.name}.{legend.file_type}"): 1090 | print(Style.BRIGHT + 1091 | f"{args.out}/{legend.name}.{legend.file_type} exists; skipping..." 1092 | + Style.RESET_ALL) 1093 | continue 1094 | print(Style.BRIGHT + 1095 | f"Rendering {args.out}/{legend.name}.{legend.file_type}..." 1096 | + Style.RESET_ALL) 1097 | print(legend) 1098 | COMMANDS.append(str(keycap)) 1099 | # retcode, output = getstatusoutput(str(legend)) 1100 | # if retcode == 0: # Success! 1101 | # print(f"{args.out}/{legend.name}.{legend.file_type} rendered successfully") 1102 | loop = asyncio.get_event_loop() 1103 | loop.run_until_complete(run_all_commands()) 1104 | -------------------------------------------------------------------------------- /snap_fit.scad: -------------------------------------------------------------------------------- 1 | // This .scad demonstrates what you want to set/override in order to make a snap-fit keycap+stem. 2 | 3 | // You *could* just use this to make your keycap but sometimes it's easier to just modify these values in keycap_playground.scad. 4 | 5 | include 6 | 7 | // Pick what you want to render (you can put a '%' in front of the name to make it transparent) 8 | //RENDER = ["keycap", "stem"]; // Supported values: keycap, stem, legends, row, row_stems, row_legends, custom 9 | // NOTE: You'll want to render the keycap and stem separately in reality like so: 10 | //RENDER = ["stem"]; // Render the stem and print it upside down 11 | RENDER = ["keycap"]; // Render the stem and print it on its side (90% of the time) 12 | 13 | KEY_PROFILE = "riskeycap"; 14 | KEY_LENGTH = (KEY_UNIT*1-BETWEENSPACE); 15 | KEY_WIDTH = (KEY_UNIT*1-BETWEENSPACE); 16 | // Neat little trick to print the stem upside down and the keycap on its side: 17 | KEY_ROTATION = (RENDER==["stem"]) ? [180,0,0] : [0,110.1,90]; 18 | UNIFORM_WALL_THICKNESS = false; 19 | // Clip-in stems take up some space so we need slightly thinner outside walls in most cases: 20 | WALL_THICKNESS = 0.45*1.85; 21 | STEM_TOP_THICKNESS = 0.5; // This is the minimum you want for a 0.45mm nozzle 22 | STEM_SNAP_FIT = true; // Tells poly_keycap() to add clips to the inside of the keycap 23 | STEM_SIDES_WALL_THICKNESS = 0.65; // Any thinner than 0.65 is not recommended 24 | STEM_WALLS_INSET = 1.15; // Makes it so the stem walls don't go all the way to the bottom of the keycap; works just like STEM_INSET but for the walls 25 | // NOTE: The clips take up about 1mm of "inset space" so you need to set STEM_WALLS_INSET to some value greater than that. Consider values above 1 to be the tolerance.. So a value of 1.15 would be 0.15mm of tolerance/wiggle room (for the stem walls pressing against the clips). 26 | // TIP: If you give STEM_WALLS_INSET a bit more room than necessary (like say, 1.3) and use silicone caulk/sealant between the stem and keycap it have a cushioning effect whenever you press that keycap (it feels nice!). 27 | STEM_WALLS_TOLERANCE = 0.2; // How much wiggle room the stem sides will get inside the keycap 28 | STEM_LOCATIONS = [ // Where to place stems/stabilizers 29 | [0,0,0], // Dead center (don't comment this out when uncommenting below) 30 | // Standard examples (uncomment to use them): 31 | // [12,0,0], [-12,0,0], // Standard 2U, 2.25U, and 2.5U shift key 32 | // [0,12,0], [0,-12,0], // Standard 2U Numpad + or Enter 33 | // [50,0,0], [-50,0,0], // Cherry style 6.25U spacebar (most common) 34 | // [57,0,0], [-57,0,0], // Cherry style 7U spacebar 35 | ]; 36 | -------------------------------------------------------------------------------- /utils.scad: -------------------------------------------------------------------------------- 1 | // Keycap utility modules and functions 2 | 3 | // MODULES 4 | 5 | // Riskable's polygon: Kind of like a combo roundedCube()+cylinder() except you get also offset one of the diameters 6 | module rpoly(d=0, h=0, d1=0, d2=0, r=1, edges=4, d2_offset=[0,0], center=true, $fn=64) { 7 | // Because we use a cylinder diameter instead of a cube for the length/width we need to correct for the fact that it will be undersized (fudge factor): 8 | fudge = 1/cos(180/edges); 9 | module rpoly_proper(d1, d2, h, r, edges, d2_offset) { 10 | fudged_d1 = d1 * fudge - r*2.82845; // Corner radius magic fix everything number! 2.82845 11 | fudged_d2 = d2 * fudge - r*2.82845; // Ditto! 12 | if (edges > 3) { 13 | hull() { 14 | linear_extrude(height=0.0001) 15 | offset(r=r) rotate([0,0,45]) circle(d=fudged_d1, $fn=edges); 16 | translate([d2_offset[0],d2_offset[1],h]) 17 | linear_extrude(height=0.0001) 18 | offset(r=r) rotate([0,0,45]) circle(d=fudged_d2, $fn=edges); 19 | } 20 | } else { // Triangles need a little special attention 21 | hull() { 22 | linear_extrude(height=0.0001) 23 | offset(r=r) rotate([0,0,30]) circle(d=d1, $fn=edges); 24 | translate([d2_offset[0],d2_offset[1],h]) 25 | linear_extrude(height=0.0001) 26 | offset(r=r) rotate([0,0,30]) circle(d=d2, $fn=edges); 27 | } 28 | } 29 | } 30 | if (d1) { 31 | if (center) { 32 | translate([0,0,-h/2]) 33 | rpoly_proper(d1, d2, h, r, edges, d2_offset); 34 | } else { 35 | rpoly_proper(d1, d2, h, r, edges, d2_offset); 36 | } 37 | } else { 38 | fudged_diameter = d * fudge - r*2.82845; // Corner radius magic fix everything number! 2.82845 39 | if (center) { 40 | translate([0,0,-h/2]) 41 | rpoly_proper(d, d, h, r, edges, d2_offset); 42 | } else { 43 | rpoly_proper(d, d, h, r, edges, d2_offset); 44 | } 45 | } 46 | } 47 | 48 | module squarish_rpoly(xy=[0,0], h=0, xy1=[0,0], xy2=[0,0], r=1, xy2_offset=[0,0], center=false, $fn=64) { 49 | module square_rpoly_proper(xy1, xy2, h, r, xy2_offset) { 50 | // Need to correct for the corner radius since we're using offset() and square() 51 | corrected_x1 = xy1[0] > r ? xy1[0] - r*2 : r/10; 52 | corrected_y1 = xy1[1] > r ? xy1[1] - r*2 : r/10; 53 | corrected_x2 = xy2[0] > r ? xy2[0] - r*2 : r/10; 54 | corrected_y2 = xy2[1] > r ? xy2[1] - r*2 : r/10; 55 | if (corrected_x1 <= 0 || corrected_x2 <= 0 || corrected_y1 <= 0 || corrected_y2 <= 0) { 56 | warning("Corner Radius (x2) is larger than this rpoly! Won't render properly."); 57 | } 58 | corrected_xy1 = [corrected_x1, corrected_y1]; 59 | corrected_xy2 = [corrected_x2, corrected_y2]; 60 | hull() { 61 | linear_extrude(height=0.0001) 62 | offset(r=r) square(corrected_xy1, center=true); 63 | translate([xy2_offset[0],xy2_offset[1],h]) 64 | linear_extrude(height=0.0001) 65 | offset(r=r) square(corrected_xy2, center=true); 66 | } 67 | } 68 | if (xy1[0]) { 69 | if (center) { 70 | translate([0,0,-h/2]) 71 | square_rpoly_proper(xy1, xy2, h, r, xy2_offset); 72 | } else { 73 | square_rpoly_proper(xy1, xy2, h, r, xy2_offset); 74 | } 75 | } else { 76 | if (center) { 77 | translate([0,0,-h/2]) 78 | square_rpoly_proper(xy, xy, h, r, xy2_offset); 79 | } else { 80 | square_rpoly_proper(xy, xy, h, r, xy2_offset); 81 | } 82 | } 83 | } 84 | 85 | module note(text) echo(str("NOTE: ", text, "")); 86 | module warning(text) echo(str("WARNING: ", text, "")); 87 | 88 | // FUNCTIONS 89 | function is_odd(x) = (x % 2) == 1; 90 | // This function is used to generate curves given a total number of steps, step we're currently calculating, and the amplitude of the curve: 91 | function polygon_slice(step, amplitude, total_steps=10) = (1 - step/total_steps) * amplitude; 92 | function polygon_slice_reverse(step, amplitude, total_steps=10) = (1 - (total_steps-step)/total_steps) * amplitude; 93 | 94 | // Examples: 95 | //squarish_rpoly(xy1=[0.1,0.1], xy2=[40,40], h=10, r=0.2, center=true); 96 | // Flat sides example: 97 | //squarish_rpoly(xy1=[10,10], xy2=[18,18], h=10, r=1, center=true, $fn=4); --------------------------------------------------------------------------------