I’ve been building a generator for 3D-printable mechanical keyboard cases. You describe the board as JSON — a grid of keys, some algebraic expressions for how those keys are laid out in space, and a set of options for walls, skirts, baseplates, and threaded inserts — and it produces a watertight mesh you can slice and print.

This post walks through how it works: the coordinate system, the expression language that positions and tilts each key, and then a full reference for every JSON field.

Overview

The project started life as an OpenSCAD generator and is now a standalone Blender 5.x add-on with a dependency-free Python geometry core. The split matters:

The design goal throughout is a single watertight solid straight out of the generator — no manual mesh repair before slicing. The plate is built as a constant-thickness shell that follows the tilt of each key, walls and skirts are fused into that shell, and threaded-insert holders are unioned in so the whole thing prints as one piece.

The coordinate system

Every key lives at a grid position (x, y) — think column and row — and those grid coordinates map into world space through expressions you control.

The important thing to internalise is which quadrant we work in. Coordinates come from the bottom-right quadrant of a graph: x is positive, y is negative. The top-left key of the board is the origin, x grows to the right, and y grows downward as a negative number. That’s why the default vertical expression is -y*key_1u rather than y*key_1u — increasing the row index y should move a key down the board, which is the negative Y direction.

So for a board with key_1u (the 1-unit key pitch) of 19.05 mm, the key at grid (2, 1) lands at roughly world (38.1, -19.05): two units right, one unit down.

Heights work the same way you’d expect — z is up — so tilting and doming the board is a matter of making the Z (and rotation) expressions depend on x and y.

The x / y / z algorithms

The heart of the layout is six expressions, evaluated once per key:

Field Purpose Default
x_algo world X position x*key_1u
y_algo world Y position -y*key_1u
z_algo world Z position (height) 10
x_rot_algo rotation about X, degrees 0
y_rot_algo rotation about Y, degrees 0
z_rot_algo rotation about Z, degrees 0

Each expression is evaluated for every (x, y) in the grid, producing that key’s position and orientation. Because they’re just expressions, you get a lot of expressive power for free: a flat board is z_algo: "10", a board that domes toward the middle row might be z_algo: "abs(y-2)*5 + 6", and a thumb cluster that peels away can be gated behind a condition.

Available variables

Inside any of the six expressions you can reference:

Available functions

A small whitelist is exposed:

abs, min, max, floor, ceil, round

Operators

Standard arithmetic (+ - * / // % **), comparisons (== != < <= > >=), boolean and / or / not, and — crucially — ternary conditionals (A if cond else B). The conditional is what makes per-region behaviour possible in a single line.

For example, this z_algo gives the last column of the bottom row one height, the far-right column another, one row a fixed height, and everything else a shallow dome:

17 if (x == 5 and y == 4) else 12 if (x == 6) else 9 if (y == 2) else abs(y-2)*5 + 6

A note on safety

Expressions are not run through Python’s eval blindly. They’re parsed to an AST and every node is checked against the whitelist above — attribute access, arbitrary function calls, string literals, and anything else outside the allowed set are rejected before evaluation. So a keyboard definition shared as JSON can’t smuggle in arbitrary code.

JSON field reference

A keyboard definition is a JSON object (or a list of them). Below is every field, grouped by what it controls. Fields are optional unless noted, and defaults are given where they exist.

Board basics

Field Meaning Default
name Board name, used for object/file naming "default"
width Number of columns 6
height Number of rows 4
key_1u 1-unit key pitch, mm 19.05
hole_size Switch cutout size, mm (square) 14.5
thickness Plate thickness, mm 5

Layout expressions

Covered in detail above. All optional; each falls back to its default.

Field Default
x_algo x*key_1u
y_algo -y*key_1u
z_algo 10
x_rot_algo 0
y_rot_algo 0
z_rot_algo 0

Occupancy: ignored_keys

A list of [col, row] pairs to omit from the grid — for non-rectangular boards, thumb clusters carved out of a block, and so on.

"ignored_keys": [[0, 4], [1, 4], [6, 0], [6, 1]]

Wider and taller keys: u_diff

By default every cell is 1u × 1u. u_diff widens or heightens specific keys and re-centres them so the enlarged cell stays visually anchored.

"u_diff": [
  { "keys": [[3, 4]], "u_width": 2 },
  { "keys": [[0, 1]], "u_height": 1.5 }
]

Explicit joins: linked_keys

For boards where keys don’t sit on a clean grid — split layouts, staggered thumb clusters — linked_keys explicitly joins two keys so the shell bridges the gap between them. Each entry names the keys on each side of the join:

"linked_keys": [
  { "l": [1, 3, "br"], "r": [2, 4] }
]

Wall settings

By default the plate is a bare constant-thickness shell. These fields turn its perimeter into a proper wall with a recess the plate seats into, or grow the plate’s own edges into vertical walls.

Field Meaning Default
vertical_edges Drop the plate’s perimeter straight down to wall_base_z instead of following the constant-thickness offset true
wall_base_z The floor height the walls drop to — the plane the case sits on 0.0
wall_thickness Wall thickness at the top of the skirt (it can taper below) 2.0
flange_offset Outward wall material beyond the plate edge, mm 0
flange_z Recess depth — how far the plate top sits below the wall rim, mm 0
plate_lip Width of the inward ledge the plate rests on, mm 1.5
plate_gap Per-side clearance between plate and wall inner faces (print tolerance; total gap is 2 × plate_gap), mm 0.25

The recess cross-section, top to bottom, is: an outer wall out at flange_offset, a rim at plate_top_z + flange_z, an inward ledge of width plate_lip for the plate to rest on, and the wall dropping to the z = 0 base.

Skirt settings

The skirt is the swept outer wall that drops from the plate perimeter down to wall_base_z. Enabling it forces vertical_edges and gives you a fully enclosed case; it’s also the prerequisite for a baseplate.

Field Meaning Default
skirt Enable the fused skirt false
skirt_mode "angle" (constant draft angle) or "flare" (constant outward run) "angle"
skirt_angle Outward draft angle from vertical, degrees (angle mode) 0.0
skirt_flare Outward run at the base, mm (flare mode) 0.0
skirt_flange Outward offset applied to the skirt footprint
skirt_profile A multi-segment cross-section (below); overrides the single-segment modes

Multi-segment profiles: skirt_profile

For anything more shaped than a single straight run, skirt_profile is a list of segments walked from just below the plate down to wall_base_z:

"skirt_profile": [
  { "fraction": 0.3, "angle": 5 },
  { "fraction": 0.0, "out": 2.0 },
  { "fraction": 0.7, "angle": -3 }
]

The step generator

Rather than hand-writing a profile, you can have one generated by interpolating the draft angle from top to bottom across evenly-sized steps. These fields drive that generator (the resulting skirt_profile is what actually gets built):

Field Meaning Default
skirt_steps Number of rotation steps between plate and base (1 = a single straight run) 1
skirt_angle_end Draft angle of the last step (the first uses skirt_angle) 0.0
skirt_step_out Horizontal ledge inserted between steps (0 = smooth) 0.0

Baseplate

A flat bottom cover matching the skirt’s outer footprint, extruded downward. Requires the skirt.

Field Meaning Default
baseplate_thickness Cover thickness, mm (must be > 0) 2.0
insert_clearance_d Screw clearance-hole diameter cut through the baseplate, coaxial with each insert hole, mm 3.0

Threaded inserts

The board can carry threaded-insert holders — the little towers that a heat-set brass insert presses into, so the baseplate can be screwed on. Each holder is a three-legged “wishbone”: a central disc with three legs splaying out to reach nearby walls and corners, drilled through the middle for the insert.

The outline is the convex hull of the three leg tips plus the disc — one leg to the left, one to the centre, one to the right — with each leg’s length set independently. The three legs let a holder tie into the surrounding walls for rigidity; when placing one, it’s worth orienting it so its legs catch the outer corners of the case in preference to inner ones, since a leg anchored to an outer corner braces the holder far more.

Inserts are given as a list, each attached to a key by col/row:

"inserts": [
  {
    "col": 0, "row": 0,
    "x": -8.5, "y": 9.5,
    "od": 8.0, "id": 4.0,
    "rot": 45.0,
    "leg_0": 4.0, "leg_1": 8.0, "leg_2": 4.0,
    "hole_x": 0, "hole_y": 0
  }
]
Field Meaning Default
col, row The key this insert is attached to
x, y Disc-centre offset from that key’s centre, scaled by the key’s unit size
od Disc diameter, mm; radius is od/2 8.0
id Insert hole diameter, mm (the heat-set insert’s press-fit bore) 4.0
height Holder height above the base plane, mm 4.2
rot Holder rotation about Z, degrees 0
leg_0 Left leg length (tip distance from disc centre), mm 5.0
leg_1 Centre leg length, mm 7.0
leg_2 Right leg length, mm 5.0
hole_x, hole_y Hole-centre offset from the disc centre, mm (clamped to stay inside the disc) 0, 0
clearance_d Per-insert override for the baseplate clearance-hole diameter inherits insert_clearance_d

Meshing resolution

Field Meaning Default
insert_hole_segments Number of segments used to facet insert holes and circular cuts 32

Assembly entries

The top-level JSON can be a list, mixing buildable keyboard definitions with combined/assembly entries — objects with an items list that reference other boards by name and place them with a position, rotation, and optional mirror (for laying out a split keyboard’s two halves together, for instance).

{
  "name": "MyBoard-Combined",
  "items": [
    { "name": "MyBoard", "pos": [-120, 0, 0], "rot": [0, 0, -20] },
    { "name": "MyBoard", "pos": [120, 0, 0], "rot": [0, 0, 20], "mirror": [1, 0, 0] }
  ]
}

Wrapping up

The whole thing is deliberately layered: a compact JSON definition, expanded into an explicit per-key list, turned into pure-Python geometry, and finally handed to Blender for the boolean-heavy assembly. The expression language is where most of the character of a board lives — a few conditionals in the Z and rotation algos is enough to describe a domed, tilted, split layout — while the field reference above covers everything else you’d reach for to make it printable.

loading model…

drag to rotate · scroll to zoom · staggered.stl

[
    {
    "name": "Staggered_6x5_stepped",
    "width": 7,
    "height": 5,
    "hole_size": 14.5,
    "key_1u": 19.05,
    "thickness": 4,
    "x_algo": "(x*key_1u+16-x) if (x >= 5 and y == 4) else x*key_1u",
    "y_algo": "-4*key_1u-14-(x-5)*0.5*key_1u if (x >= 5 and y == 4) else (-y)-y*key_1u-(12 if x == 0 else (10 if x == 1 else (2 if x == 2 else (4 if x >= 4 else 0))))",
    "z_algo": "17 if (x == 5 and y == 4) else 12 if (x == 6) else 9 if (y == 2) else abs(y-2)*(5)+6",
    "x_rot_algo": "x*3 if (x >= 5 and y == 4) else ((y-2)*(-10))",
    "y_rot_algo": "20 if (x == 6) else 10 if (x == 5 and y == 4) else 0",
    "z_rot_algo": "-30 if (x == 6) else -20 if (x == 5 and y == 4) else 0",
    "flange_offset": 3.0,
    "flange_z": 0.0,
    "plate_lip": 1.5,
    "plate_gap": 0.0,
    "wall_base_z": 0.0,
    "vertical_edges": false,
    "ignored_keys": [
      [ 0, 4 ],
      [ 1, 4 ],
      [ 6, 0 ],
      [ 6, 1 ],
      [ 6, 2 ],
      [ 6, 3 ]
    ],
    "linked_keys": [
      {"l": [ 1, 3, "br" ], "r": [ 2, 4 ]
      }
    ],
    "u_diff": [],
    "wall_thickness": 2.0,
    "skirt_flange": 1.4,
    "skirt_mode": "angle",
    "skirt_angle": 30.0,
    "skirt_flare": 0.0,
    "skirt": true,
    "skirt_steps": 8,
    "skirt_angle_end": 5.0,
    "skirt_step_out": 0.5,
    "constant_thickness_walls": true,
    "skirt_profile": [
      {
        "fraction": 0.125,
        "angle": 30.0
      },
      {
        "fraction": 0.0,
        "out": 0.5
      },
      {
        "fraction": 0.125,
        "angle": 26.0
      },
      {
        "fraction": 0.0,
        "out": 0.5
      },
      {
        "fraction": 0.125,
        "angle": 22.0
      },
      {
        "fraction": 0.0,
        "out": 0.5
      },
      {
        "fraction": 0.125,
        "angle": 19.0
      },
      {
        "fraction": 0.0,
        "out": 0.5
      },
      {
        "fraction": 0.125,
        "angle": 15.0
      },
      {
        "fraction": 0.0,
        "out": 0.5
      },
      {
        "fraction": 0.125,
        "angle": 12.0
      },
      {
        "fraction": 0.0,
        "out": 0.5
      },
      {
        "fraction": 0.125,
        "angle": 8.0
      },
      {
        "fraction": 0.125,
        "angle": 0.0
      }
    ],
    "inserts": [
      {
        "col": 3,
        "row": 0,
        "x": 0,
        "y": 13,
        "od": 8.0,
        "id": 4.0,
        "rot": 0,
        "hole_x": 0,
        "hole_y": 0,
        "leg_0": 4.0,
        "leg_1": 4.0,
        "leg_2": 4.0
      },
      {
        "col": 0,
        "row": 0,
        "x": -9,
        "y": 10,
        "od": 8.0,
        "id": 4.0,
        "rot": 45.0,
        "hole_x": 0,
        "hole_y": 0,
        "leg_0": 3.5,
        "leg_1": 7.0,
        "leg_2": 4.0
      },
      {
        "col": 0,
        "row": 3,
        "x": -8,
        "y": -8,
        "od": 8.0,
        "id": 4.0,
        "rot": 135.0,
        "hole_x": 0,
        "hole_y": 0,
        "leg_0": 4.0,
        "leg_1": 8.0,
        "leg_2": 4.0
      },
      {
        "col": 2,
        "row": 4,
        "x": -9,
        "y": -11,
        "od": 8.0,
        "id": 4.0,
        "rot": 135,
        "hole_x": 0,
        "hole_y": 0,
        "leg_0": 3.0,
        "leg_1": 7.0,
        "leg_2": 3.0
      },
      {
        "col": 5,
        "row": 4,
        "x": -13,
        "y": -8,
        "od": 8.0,
        "id": 4.0,
        "rot": 175,
        "hole_x": 0,
        "hole_y": 0,
        "leg_0": 4.0,
        "leg_1": 4.0,
        "leg_2": 4.0
      },
      {
        "col": 4,
        "row": 4,
        "x": 13,
        "y": -14,
        "od": 8.0,
        "id": 4.0,
        "rot": 175,
        "hole_x": 0,
        "hole_y": 0,
        "leg_0": 4.0,
        "leg_1": 4.0,
        "leg_2": 4.0
      },
      {
        "col": 5,
        "row": 1,
        "x": 10,
        "y": 0,
        "od": 8.0,
        "id": 4.0,
        "rot": 270.0,
        "hole_x": 0,
        "hole_y": 0,
        "leg_0": 4.0,
        "leg_1": 4.0,
        "leg_2": 4.0
      },
      {
        "col": 5,
        "row": 4,
        "x": 12,
        "y": 8,
        "od": 8.0,
        "id": 4.0,
        "rot": 325.0,
        "hole_x": 0,
        "hole_y": 0,
        "leg_0": 4.0,
        "leg_1": 4.0,
        "leg_2": 4.0
      }
    ],
    "baseplate_thickness": 2.0,
    "insert_clearance_d": 3.0
  },
{
    "name": "Staggered_6x5-Combined",
    "items": [
        {"name": "Staggered_6x5_stepped", "pos": [-120,0,0], "rot": [0,0,-20]},
        {"name": "Staggered_6x5_stepped", "pos": [120,0,0], "rot": [0,0,20], "mirror": [1,0,0]}
    ]
}
]