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.
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.
columnoffset
: I created this value because as you've seen at the first ascii
doodle above, starting from the middle of the first element, that shape is above
the x axis.
To simplify mirroring, I just centered them by translate
-ing the shape a bit
lower using this value.bendingoffset
: after I've centered the switch column, I just had to move the
bending
half of the switchcolumn
plus half of bending
's own legth.
Why? because bending
was a centered shape too, mate!screwoffset
: this one, pretty much the same. But because it wasn't a centered
shape. In fact, I cut it at the center, I just simply put it as it is.bending
: well, moved bendingarea
.screw
: moved screwarea
union
: where the things happened.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:
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.
frame
value above is basically a rectangle with the length of its
sides keylength
and keycount * keylength + materialthickness * 2
.
Just look up at the previous ascii thingy above.
The horizontal line is this value.bending
is the one that, well, being bended.
Nothing to see, I guess.wall
is the vertical line.
Because I want a wall.
And whenever you're complaining, it becomes 10 feet higher.Now, time to render this thing and you will see thing thing.
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
.
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:
hull
-ed (is that even a word?) two circles so
it will not look intimidating.
Uh, I mean, it will look a bit nice.hull
-ed two small circles so it will be a bit free
to rotate.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.
As you can see, the thumbcluster doesn't differ much compared to Redox's. This leads to the need of something that holds it.