├── .gitignore ├── .gitmodules ├── README.md ├── caps ├── copy-paste.scad ├── logo.scad └── up-downvote.scad └── enclosure.scad /.gitignore: -------------------------------------------------------------------------------- 1 | *.stl 2 | *.pwmo 3 | sandbox/ -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "KeyV2"] 2 | path = KeyV2 3 | url = https://github.com/rsheldiii/KeyV2.git 4 | [submodule "Stacks-Icons"] 5 | path = Stacks-Icons 6 | url = https://github.com/StackExchange/Stacks-Icons.git 7 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Stack Overflow Keyboard 2 | 3 | ## Building the stl files 4 | 5 | Initialize the project recursively to pull in the required submodules. 6 | 7 | ``` 8 | > git clone --recurse-submodules 9 | ``` 10 | 11 | Open each `.scad` file with [OpenSCAD](https://www.openscad.org/), then: 12 | 13 | 1. Prerender the design and check for errors 14 | * `Design > Render` 15 | 2. Export the design as an stl 16 | * `File > Export > Export as stl...` -------------------------------------------------------------------------------- /caps/copy-paste.scad: -------------------------------------------------------------------------------- 1 | include <../KeyV2/includes.scad> 2 | 3 | rounded_cherry() legend("C") key(); 4 | translate_u(1) rounded_cherry() legend("V") key(); -------------------------------------------------------------------------------- /caps/logo.scad: -------------------------------------------------------------------------------- 1 | include <../KeyV2/includes.scad> 2 | 3 | rounded_cherry() key(inset=true) { 4 | translate([0, 0, -0.5]) 5 | linear_extrude(3) 6 | import("../Stacks-Icons/src/Icon/LogoGlyph.svg", center=true); 7 | }; -------------------------------------------------------------------------------- /caps/up-downvote.scad: -------------------------------------------------------------------------------- 1 | include <../KeyV2/includes.scad> 2 | 3 | $font_size=3; 4 | 5 | difference() 6 | { 7 | rounded_cherry() key(inset=true) 8 | { 9 | translate([0, 0, -0.5]) 10 | linear_extrude(3) 11 | import("../Stacks-Icons/src/Icon/ArrowUp.svg", center=true); 12 | } 13 | 14 | front_placement() 15 | { 16 | translate([0, $inset_legend_depth, 0]) 17 | rotate([90, 0, 0]) 18 | linear_extrude(3) 19 | import("../Stacks-Icons/src/Icon/Bookmark.svg", center=true); 20 | } 21 | } 22 | 23 | translate_u(1) rounded_cherry() front_legend("[dup]") key(inset=true) 24 | { 25 | translate([0, 0, -0.5]) 26 | linear_extrude(3) 27 | import("../Stacks-Icons/src/Icon/ArrowDown.svg", center=true); 28 | }; -------------------------------------------------------------------------------- /enclosure.scad: -------------------------------------------------------------------------------- 1 | $fn=100; 2 | 3 | includePosts = false; 4 | includeEnclosure = true; 5 | includeSwitchInsert = true; 6 | includeFaceplate = true; 7 | cutOutFaceplate = true; 8 | 9 | // number of keys are in a single row 10 | columnCount = 3; 11 | 12 | // number of rows of keys to add 13 | rowCount = 1; 14 | 15 | // measurements of a cherry mx switch / keycap 16 | keycapWidth = 18; 17 | switchHeight = 14; 18 | switchWidth = 14.5;//15.6; 19 | switchStemHeight = 4; 20 | 21 | // how far apart to space the caps 22 | spaceBetweenCaps = 1; 23 | 24 | // distance from the keycaps to the walls 25 | distanceFromWalls = 2; 26 | 27 | // how thick the enclosure walls are 28 | wallWidth = 2; 29 | 30 | // the corner radius 31 | cornerCurveRadius = 5; 32 | 33 | // the path to the faceplate image 34 | logoPath = "./Stacks-Icons/src/Icon/Logo.svg"; 35 | 36 | // TODO there's probably a better way to detect and constrain dynamically, but I'm being lazy here 37 | // whether to fit the logo to the width or to the height 38 | fitLogoToLength = true; 39 | 40 | // how deep to imprint the image into the faceplate 41 | imgDepth = 0.25; 42 | 43 | // additional height to add to the enclosure to account for electronics, etc 44 | additionalEnclosureHeight = 5; 45 | 46 | // pcb dimensions 47 | pcbHeight = 1.6; 48 | pcbWidth = 19.0; 49 | pcbLength = 34.0; 50 | 51 | // how much to add/subtract from friction fit parts to give them some "wiggle room" 52 | tolerence = 0.1; 53 | 54 | // calculate the size of the enclosure based on the keycap widths 55 | encLength = 56 | (wallWidth * 2) // two walls 57 | + (keycapWidth * columnCount) // n keys that are w wide 58 | + (spaceBetweenCaps * (columnCount - 1)) // the space between each key, not including the end keys 59 | + (distanceFromWalls * 2); // distance between the walls and the caps on each end 60 | 61 | encWidth = 62 | (wallWidth * 2) // two walls 63 | + (keycapWidth * rowCount) // n keys that are w wide 64 | + (spaceBetweenCaps * (rowCount - 1)) // the space between each key, not including the end keys 65 | + (distanceFromWalls * 2); // distance between the walls and the cap 66 | 67 | encHeight = switchHeight + wallWidth + additionalEnclosureHeight; 68 | 69 | facePlateLength = encLength - (wallWidth * 2) - cornerCurveRadius; 70 | 71 | module rrect(r, x, y, h=1) 72 | { 73 | d = r * 2; 74 | tl = x - d; 75 | tw = y - d; 76 | translate([r, r, 0]) { 77 | hull() 78 | { 79 | cylinder(h=h, r=r); 80 | translate([tl, 0, 0]) cylinder(h=h, r=r); 81 | translate([0, tw, 0]) cylinder(h=h, r=r); 82 | translate([tl, tw, 0]) cylinder(h=h, r=r); 83 | } 84 | } 85 | } 86 | 87 | module post(h) 88 | { 89 | postWidth = 4; 90 | 91 | // these posts are decorational only, so don't design them to be a flush fit 92 | // if you do, you run the risk of the post breaking off inside the cap (ask me how I know...) 93 | 94 | x = postWidth; 95 | y = (postWidth / 4) - tolerence; 96 | 97 | xh = x / 2; 98 | yh = y / 2; 99 | translate([-xh, -yh, 0]) 100 | { 101 | // flush fit means two posts, but... yeah 102 | //cube([x, y, h]); 103 | translate([xh - yh, yh - xh, 0]) cube([y, x, h]); 104 | } 105 | // reinforce the post base 106 | reinforcementWidth = postWidth * 1.5; 107 | cylinder(h - switchStemHeight, d=reinforcementWidth); 108 | } 109 | 110 | module microUsbCutout(depth = wallWidth) 111 | { 112 | width = 7.40 + tolerence; 113 | height = 2.40 + tolerence; 114 | 115 | translate([width / -2, 0, 0]) 116 | cube([width, depth, height]); 117 | } 118 | 119 | module pcb(height = pcbHeight) { 120 | #cube([pcbWidth, pcbLength, height]); 121 | } 122 | 123 | module logo() 124 | { 125 | lengthResize = fitLogoToLength ? facePlateLength - wallWidth : 0; 126 | heightResize = fitLogoToLength ? 0 : encHeight - wallWidth; 127 | 128 | rotate([90, 0, 90]) 129 | resize([lengthResize, heightResize, 0], auto=[true,true,false]) 130 | linear_extrude(imgDepth) 131 | import(logoPath, center=true); 132 | } 133 | 134 | module facePlate(includeImage = true) 135 | { 136 | translate([0, wallWidth / 2, 0]) { 137 | difference() { 138 | cube([wallWidth, facePlateLength, encHeight]); 139 | 140 | if (includeImage) 141 | { 142 | lengthResize = fitLogoToLength ? facePlateLength - wallWidth : 0; 143 | heightResize = fitLogoToLength ? 0 : encHeight - wallWidth; 144 | 145 | translate([wallWidth - imgDepth, facePlateLength / 2, encHeight / 2]) 146 | logo(); 147 | } 148 | } 149 | } 150 | } 151 | 152 | // draw the enclosure 153 | if (includeEnclosure) 154 | difference() 155 | { 156 | mod = wallWidth * 2; 157 | 158 | rrect(cornerCurveRadius, encWidth, encLength, encHeight); 159 | translate([wallWidth, wallWidth, wallWidth]) 160 | rrect(cornerCurveRadius, encWidth - mod, encLength-mod, encHeight); 161 | 162 | // cutout an area for the pcb to sit, in case it is wider than the curved corners 163 | translate([(encWidth - pcbWidth) / 2, wallWidth, wallWidth]) 164 | pcb(); 165 | 166 | translate([(encWidth / 2), 0, wallWidth + pcbHeight]) 167 | microUsbCutout(); 168 | 169 | // cut out the front plate so we can print it separately 170 | if (includeFaceplate || cutOutFaceplate) 171 | { 172 | translate([encWidth - wallWidth, wallWidth * 2, 0]) 173 | rotate([0, 0, 0]) 174 | facePlate(false); 175 | } 176 | else if (len(logoPath) > 0) 177 | { 178 | translate([encWidth - imgDepth, encLength / 2, encHeight / 2]) logo(); 179 | } 180 | } 181 | 182 | if (includePosts) 183 | // simplify the internal math a bit, account for the wall/floor heights 184 | translate([wallWidth, wallWidth, 0]) 185 | { 186 | for (i = [0:(columnCount-1)]) 187 | { 188 | for (j = [0:(rowCount-1)]) 189 | { 190 | x = (keycapWidth * j) + (keycapWidth / 2) + distanceFromWalls + (spaceBetweenCaps * j); 191 | y = (keycapWidth * i) + (keycapWidth / 2) + distanceFromWalls + (spaceBetweenCaps * i); 192 | 193 | postHeight = 194 | encHeight // the height of the enclosure 195 | + switchStemHeight; // the extra height that sticks into the cap 196 | 197 | translate([x, y, 0]) 198 | post(postHeight); 199 | 200 | // add in a dummy keycap for visual debugging purposes 201 | %translate([x, y, postHeight - switchStemHeight / 2]) 202 | cube([keycapWidth, keycapWidth, switchStemHeight], center=true); 203 | } 204 | } 205 | } 206 | 207 | // create a structure inside the enclosure that the switches can attach to 208 | if (includeSwitchInsert) 209 | translate([encWidth + 5, 0, 0]) 210 | difference() { 211 | // subtract `tolerence` so it isn't a "flush" fit and will sit inside the enclosure 212 | mod = (wallWidth * 2) + tolerence; 213 | insertHeight = 5; 214 | supportHeight = 14; 215 | 216 | union() { 217 | translate([cornerCurveRadius, cornerCurveRadius, 0]) 218 | cylinder(supportHeight, r=cornerCurveRadius); 219 | 220 | translate([encWidth - cornerCurveRadius - mod, cornerCurveRadius, 0]) 221 | cylinder(supportHeight, r=cornerCurveRadius); 222 | 223 | translate([cornerCurveRadius, encLength - cornerCurveRadius - mod, 0]) 224 | cylinder(supportHeight, r=cornerCurveRadius); 225 | 226 | translate([encWidth - cornerCurveRadius - mod, encLength - cornerCurveRadius - mod, 0]) 227 | cylinder(supportHeight, r=cornerCurveRadius); 228 | 229 | rrect(cornerCurveRadius, encWidth - mod, encLength - mod, insertHeight); 230 | } 231 | 232 | // make sure to cut out an area for the pcb as well 233 | translate([(encWidth - mod - pcbWidth) / 2, 0, insertHeight]) 234 | pcb(supportHeight); 235 | 236 | for (i = [0:(columnCount-1)]) 237 | { 238 | for (j = [0:(rowCount-1)]) 239 | { 240 | x = (keycapWidth * j) + (keycapWidth / 2) + distanceFromWalls + (spaceBetweenCaps * j); 241 | y = (keycapWidth * i) + (keycapWidth / 2) + distanceFromWalls + (spaceBetweenCaps * i); 242 | 243 | translate([x, y, 0]) 244 | cube([switchWidth, switchWidth, 100], center=true); 245 | } 246 | } 247 | } 248 | 249 | // add the faceplate, rotated on its back for easy printing 250 | if (includeFaceplate) 251 | translate([(encWidth * 2) + 5 + encHeight, 0, 0]) 252 | rotate([0, -90, 0]) 253 | facePlate(); --------------------------------------------------------------------------------