OpenSCAD for Keyboard Design


W̶o̶r̶k̶ ̶i̶n̶ ̶P̶r̶o̶g̶r̶e̶s̶s̶ Abandoned

Lately I got into keyboard and decided to design my own keyboard. But, because I don't have 3d printer, I decided to design it as a plat based frame or something along that line to simplify the process in case something goes wrong. Not only that, I wrote a simple design directly using OpenSCAD. Not going to lie, it was a dumpster fire. And let me be honest here, I don't really like writing in that kind of language.

Surely there are better choices, right? Like what Matt Adereth did with his Dactyl Keyborad and Tom Short with his Dactyl Manuform Keyboard. Unfortunately, I'm not that comfortable with Clojure, but I'm pretty comfortable with Haskell. So yeah, I wrote the design of this shitty keyboard in Haskell.

First step, I look for an OpenSCAD library and found one. Second, studied it a bit. And then, wrote some modifications.

There are a few things that I've added. For example, I've added center option for rectangle. Oh, perhaps I only added that one thing. Well, whatever.

So, let's start writing that code. (p.s. i hate pressing shift when writing)

Of course I started it by defining some contants like:

twenty = 20
fnsomething = 10

facets = fn fnsomething

keylength = 19 -- a single keycaps side is approximately 19 mm.
halfkeylength = keylength / 2
quarterkeylength = halfkeylength / 2
eigthkeylength = quarterkeylength / 2

switchlength = 13.97 -- based on cherry's mx side length.

materialthickness = 1.5 -- i don't know, but people always told to use this thickness.

And then, I wrote a function for a key area. That is a square with side length keylength with a square holed with switchlength as its side length.

singlekey = square keylength True
singleswitch = square switchlength True

singleholedkey = difference singlekey singleswitch

Like what I've said before, I modified openscad library so it can know whether we want the square to be centered or not.

And if we evaluate that piece of code and render it, it will show this following snippet (prettified)

difference(){
  square([19.0, 19.0], center = true);
  square([13.97, 13.97], center = true);
}

The next part is, of course, writing the column by using singleholedkey as the building block.

switchcolumn keys = union $ map
  (\i -> translate ((0, i * keylength) :: Vector2d) singleholedkey)
  [0 .. keys - 1]

And when we render that function with 4 as keys, we will get something like this.

union(){
  translate([0.0,0.0])
    difference(){
      square([19.0,19.0], center = true);
      square([13.97,13.97], center = true);
  }
  translate([0.0,19.0])
    difference(){
      square([19.0,19.0], center = true);
      square([13.97,13.97], center = true);
  }
  translate([0.0,38.0])
    difference(){
      square([19.0,19.0], center = true);
      square([13.97,13.97], center = true);
  }
  translate([0.0,57.0])
    difference(){
      square([19.0,19.0], center = true);
      square([13.97,13.97], center = true);
  }

Of course OpenSCAD suppports for loop, but I want to be a galaxy-brain by using a map to translate those singleholedkey into column and join them together. Although it seems manual if you compare it with for construct, it's ok, I guess. If you're still confused, look at this illustration:

              +---+
              | o | <- this is the fourth element
              +---+
              | o | <- this is the third element
              +---+
              | o | <- this is the second element
              +---+
---- x axis - | o | <- this is the first element
              +---+
                |
                |
              y axis

There should be an area to ease the bending process by creating a something like a neck for the column.

Something like this, I guess.

+---+
|   |
 \ /
 / \
|   |
+---+

Looks weird, right? Well, yeah it does. Now, how did I write that? Something like the following:

First, I created a weird pentagon

pentagon width height depth =
  let diff = width - depth
      nul  = 0
   in polygon
        0
        [ [ (nul  , nul)
          , (diff , nul)
          , (width, depth)
          , (width, height)
          , (nul  , height)
          ]
        ]

Which when I replace width, height, and depth with 5, 5, 2.5, respectively, will generate something like the following

    5 mm
    +---+
5mm |   | 2.5mm
    |   /
    +--'
    2.5mm

That will be a building block for the bending area. Now, I will complete the bending area by mirroring it twice.

hexagon width height depth = union
  [ pentagon width height depth
  , mirror (1, 0) $ pentagon width height depth
  ]

That mirror (1, 0) makes the next shape to mirror x axis and the rendered result will look like this, if using the same parameter.

   10 mm
+-------+
|       | 2.5mm
\       /
 '-----'
   5 mm

Followed by mirroring the above hexagon by y axis, I achieved the neck / bending area.

bendingarea width height depth =
  let x = width / 2
      y = heigth / 2
      hex = hexagon x y depth
   in union [ hex, mirror (0, 1) hex ]

Which when it gets rendered, after feeding it 10, 10, and 2.5 as its parameters, would show something like the folllowing snippet:

union(){
  union(){
    polygon(points=[[0.0,0.0],[2.5,0.0],[5.0,2.5],[5.0,5.0],[0.0,5.0]],paths=[[0,1,2,3,4]],convexity=0);
    mirror([1.0,0.0])
      polygon(points=[[0.0,0.0],[2.5,0.0],[5.0,2.5],[5.0,5.0],[0.0,5.0]],paths=[[0,1,2,3,4]],convexity=0);
  }
  mirror([0.0,1.0])
    union(){
      polygon(points=[[0.0,0.0],[2.5,0.0],[5.0,2.5],[5.0,5.0],[0.0,5.0]],paths=[[0,1,2,3,4]],convexity=0);
      mirror([1.0,0.0])
        polygon(points=[[0.0,0.0],[2.5,0.0],[5.0,2.5],[5.0,5.0],[0.0,5.0]],paths=[[0,1,2,3,4]],convexity=0);
    }
}

And there you go... A neck area or bending area or whatever you want to call it.

              10 mm
            +-------+
            |       | 2.5mm
            \       /
-- x axis -  >     <
            /       \
            |       |
            +-------+
                |
              y axis
                |
                |

Pardon the retarded neck.

The next part for the column part is the part where somebody put a hole on it. I can't give you an ascii art about it, so here's the image.

screw area

How did I do that, you ask? First, create a circle with a certain radiuus. Then, remove the half of it, preferably from the lower region. Finally, create a hole for the screw itself. For the reason why I did choose for circle, because I want it to be able to rotate it from z axis. Now, let me show you something unsightly.

screwarea arearadius screwradius =
  let diam = arearadius * 2
   in foldl difference (circle arearadius facets)
        [ translate (0, -arearadius) (square diam True)
        , translate (0, arearadius / 2) (circle screwradius True)
        ]

If you read the manual page of OpenSCAD, you will see that difference takes a list of shape. But, as I've said before, as a galaxy brain, I used foldl like a PhD student in Category Theory just to get the differences from those three shapes. And no, I won't show you how the generated scad function.

The next part is combining those three parts together, if you want to curse me.

switchcolumnplate keycount bendinglength =
  let
    columnoffset  = (keycount - 1) / 2 * keylength
    bendingoffset = keycount * keylength / 2 + bendinglength / 2
    screwoffset   = keycount * keylength / 2 + bendinglength
    bending =
      translate (0, bendingoffset) $ bendingarea keylength bendinglength 2.5
    screw = translate (0, screwoffset) $ screwarea halfkeylength screwradius
  in
    union
      [ translate (0, -columnoffset) $ switchcolumn keycount
      , bending
      , mirror (0, 1) bending
      , screw
      , mirror (0, 1) screw
      ]

As an observer of American Internet Culture, I will say, "there's a lot to unpack here" like a true UCLA academian.

When I provide 3 and 10 as that function parameters and render it, it will spit something that when I open it in openscad, will look like this pic:

if you squint hard enough, you will see a two headed and circumcised penis.

P.S: I rotated it 90 degrees because I don't like tall images.

Now, I have a function to generate the switch plate. Time to create a function to generate the frame for the previous plate.

As I've defined above, there's a value named materialthickness. Since the frame should account for the thickness of the switch plate, I'm going to add that value twice so it will be fit. Pretty sure you're confused, right? Just look at the following ascii thing.

         .-----------.
         |           |  <- the switch plate.

        |             | <- the frame.
        `-------------`

Notice the length difference between the two. Basically I intend to put the switch into the frame. As for the reason? There's none. I just want to make my life a bit complicated.

Now, let me write that thing.

switchcolumnframev2 keycount bendinglength =
  let
    bendingoffset =
      keycount * keylength / 2 + materialthickness + bendinglength / 2
    walloffset =
      keycount * keylength / 2
      + materialthickness
      + bendinglength
      + halfkeylength
    screwoffset =
      keycount * keylength / 2 + materialthickness + bendinglength + keylength
    framebody =
      rectangle keylength (keylength * keycount + materialthickness * 2) True
    bending =
      translate (0, bendingoffset) $ bendingarea keylength bendinglength 2.5
    wall  = translate (0, walloffset) singlekey
    screw = translate (0, screwoffset) $ screwareav2 halfkeylength screwradius
    frame = union
      [ framebody
      , bending
      , mirror (0, 1) bending
      , wall
      , mirror (0, 1) wall
      , screw
      , mirror (0, 1) screw
      ]
    screwline = rectangle
      screwdiameter
      ( keycount * keylength
      + (bendinglength + keylength + materialthickness) * 2
      + halfkeylength
      )
      True
  in
    difference frame screwline

Again, there are a lot to unpack here. sips soylent But calm down, there are a few things that is not that different with the previous explanation.

Now, time to render this thing and you will see thing thing.

three switches switch frame

As you can see, I've completed the body parts. Now I have to create something to make them together. No, marriage is not a solution. Yes, just create some planks and sandwich the body with them. What a magnificent show of ingenuity!

-- | yeah, i know, shitty name.
--   but what do you call a plank that holds some things together?
marriageplank columncount =
  let planklength = keylength * (columncount + 1)
      halflength  = planklength / 2
      theplank = hull
        [ translate (0, halflength) $ circle halfkeylength facets
        , translate (0, -halflength) $ circle halfkeylength facets
        ]
      screwline = hull
        [ translate (0, halflength) $ circle screwradius facets
        , translate (0, -halflength) $ circle screwradius facets
        ]
  in  difference theplank screwline

(Un)Fortunately, there is not much to unpack here. Why did I put that columncount there? Of course, comrade, it is to make it "parametric", though I don't even care what that does actually mean. And why is planklength longer than columncount? Because some of you have wide fingers so I gave it some margins in case you want to give a bit space between the column. About those values which are the results from hull functions, it's simply the plank itself and where the screwlines from the body and the planck intersect.

Here's the picture when I put 6 as the columncount.

six column plank

Apparently, I have the main body of the keyboard, now. The next part is writing the thumb cluster.

Talk about thumb cluster, I can't imagine myself using Kinesis, Maltron, Ergodox's style of thumbcluster. For a couple of month of Dactyl Manuform usage, I can tell that at most I can use 4 switches for the thumb cluster. It's not the layout's fault. It's my and my fucked up right thumb's fault. The thumbcluster that looks usable for my right thumb is Redox's.

Now, let me show you another an ingenious thing.

oneandhalfkey =
  let plate = scale (1, 1.5) singlekey
   in difference plate singleswitch

twoandhalfholedkey = union
  [ oneandhalfkey
  , translate (0, keylength * 1.25) singleholedkey
  ]

In short, I wrote a value of a column with 2.5 unit which consisted of a 1.5u and 1u. If I think it a bit carefully, openscad is pretty nice.

Because I already have the building block, I can write the cluster.

thumbcolumn = union [ twoandhalfholedkey , translate (keylength, 0) twoandhalfholedkey ]

Of course, I started by writing the column. You know, the plate. Though it basically just an union of two twoandhalfholedkey.

thumbscrewarea =
  let screw = hull
        [ translate (halfkeylength, 0) $ circle halfkeylength facets
        , translate (-halfkeylength, 0) $ circle halfkeylength facets ]
      halfhelper = translate (0, -halfkeylength) $ rectangle (keylength * 2) keylength True
      screwline = hull
        [ translate (halfkeylength, 5) $ circle screwradius facets
        , translate (-halfkeylength, 5) $ circle screwradius facets ]
  in foldl difference screw [halfhelper, screwline]

Though this one is a bit worrying, it's still simple and not that dissimilar compared to screwarea. The differences are:

I mean, look at this thing.

             | |
             | |  <-- two parallel screwlines
             | |
           '-----`   \
          / ===== \   ) <-- rotation thingy
          |_______|  /
             | |
             | |

When the screwline is longer than the distance of that two parallel screwlines, I am able to rotate them a bit drastic. Well, whatever. Pretty sure I've gotten my point across.

thumbcluster bendinglength =
  let plate         = translate (-halfkeylength, -0.5 * keylength) thumbcolumn
      bendingoffset = keylength * 2.5 / 2 + bendinglength / 2
      screwoffset   = keylength * 2.5 / 2 + bendinglength
      bending = translate (0, bendingoffset) $ bendingarea (keylength * 2) bendinglength 5
      screw = translate (0, screwoffset) $ thumbscrewarea
  in union [plate, bending, mirror (0, 1) bending, screw, mirror (0, 1) screw]

Now, this one is simple too. Whatever I've used to explain switchcolumnplate can also be used to explain this part. Anyway, here's a rendered result of that function if I use 10 as bendinglength parameter.

similar to redox's thumbcluster

As you can see, the thumbcluster doesn't differ much compared to Redox's. This leads to the need of something that holds it.



This material is shared under the CC-BY License.