Skip to main content


Once the raw points are available, we want to turn them into solid, continuous outlines. The points are enough to create properly positioned and rotated rectangles (with parametric side lengths), but they won't combine since there won't be any overlap. So the first part of the outline generation is "binding", where we make the individual holes bind to each other. We use a key-level declarations for this:

bind: num | [num_x, num_y] | [num_t, num_r, num_b, num_l] # default = 0

Again, key-level declaration means that both of these should be specified in the points section, benefiting from the same extension process every key-level setting does. This field declares how much we want to bind in each direction, i.e., the amount of overlap we want to make sure that we can reach the neighbor (num applies to all directions, num_x horizontally, num_y vertically, and the t/r/b/l versions to top/right/bottom/left, respectively). Note that it might make sense to have negative bind values, in case we not only don't want to bind in the given direction, but also don't want to "cover up" a potential corner rounding or bevel (see below).

If it's a one-piece design, we also need to "glue" the halves together (or we might want to leave some extra space for the controller on the inner side for splits). This is where the following section comes into play:

glue:    glue_name:        top:            left: <anchor>            right: <anchor> | num        bottom:            left: <anchor>            right: <anchor> | num        waypoints:            - percent: num              width: num | [num_left, num_right]            - ...        extra:            - <primitive shape>            - ...    ...

...where an <anchor> is the same as it was for points.

The top and bottom fields in each glue's section are both formatted the same, and describe the center line's top and bottom intersections, respectively. In a one-piece case, this means that we project a line from a left-side anchor, another from the right, and converge them to where they meet. Split designs can specify right as a single number to mean the x coordinate where the side should be "cut off".

This leads to a gluing middle patch that can be used to meld the left and right sides together, given by the counter-clockwise polygon:

  • Top intersection
  • Left top point
  • Left bottom point
  • Bottom intersection
  • Right bottom point
  • Right top point

If this is insufficient (maybe because it would leave holes), the waypoints can be used to supplement the glue. Here, percent means the y coordinate along the centerline (going from the top intersection to the bottom intersection), and width means the offset on the x axis.

If this is somehow still insufficient (or there were problems with the binding phase), we can specify additional primitive shapes under the extra key (similarly to how we would use them in the exports; see below). These are then added to what we have so far to finish out the glue. (TODO: while the extra key is reserved for this purpose, it hasn't been needed, and therefore is unimplemented for now.)

Once we're satisfied with the glue, the outline is generated by the union of the bound left/right halves and the glue polygon. Note that this outline is still parametric, so that we can specify different width/height values for the rectangles.

Now we can configure what we want to "export" as outlines from this phase, given by the combination/subtraction of the following primitives:

  • keys : the combined outline that we've just created. Its parameters include:
    • side: left | right | middle | both | glue : the part we want to use
      • left and right are just the appropriate side of the laid out keys, without the glue.
      • middle means an "ideal" version of the glue (meaning that instead of the outline.glue we defined above, we get both - left - right, so the exact middle piece we would have needed to glue everything together
      • both means both sides, held together by the glue
      • glue is just the raw glue shape we defined above under outline.glue
    • tag: <array of tags> : optional tags to filter which points to consider in this step, where tags can be specified as key-level attributes.
    • glue: <glue_name> : the name of the glue to use, if applicable
    • size: num | [num_x, num_y] : the width/height of the rectangles to lay onto the points. Note that these values are added to the evaluation context as the variables sx and sy. So during a keys layout with a size of 18, for example, a relative shift of [.5 sx, .5 sy] actually means [9, 9] in mms.
    • corner: num # default = 0) : corner radius of the rectangles
    • bevel: num # default = 0) : corner bevel of the rectangles, can be combined with rounding
    • bound: boolean # default = true : whether to use the binding declared previously
  • rectangle : an independent rectangle primitive. Parameters:
    • ref, rotate, and shift, etc. (the usual anchor settings)
    • size, corner and bevel, just like for keys
  • circle : an independent circle primitive. Parameters:
    • ref, rotate, and shift, etc. (the usual anchor settings)
    • radius: num : the radius of the circle
  • polygon : an independent polygon primitive. Parameters:
    • points: [<anchor>, ...] : the points of the polygon. Each <anchor> can have its own ref, shift, etc. (all of which are still the same as above). The only difference here is that if a ref is unspecified, the previous point will be assumed (as in a continuous chain). For the first, it's [0, 0] by default.
  • outline : a previously defined outline, see below.
    • name: outline_name : the name of the referenced outline

Using these, we define exports as follows:

exports:    my_name:        - operation: add | subtract | intersect | stack # default = add          type: <one of the types> # default = outline          <type-specific params>        - ...

Individual parts can also be specified as an object instead of an array (which could be useful when YAML or built-in inheritance is used), like so:

exports:    my_name:        first_phase:            operation: add | subtract | intersect | stack # default = add            type: <one of the types> # default = outline            <type-specific params>        second:            ...

Operations are performed in order, and the resulting shape is exported as an output. Additionally, it is going to be available for further export declarations to use (through the outline type) under the name specified (my_name, in this case). If we only want to use it as a building block for further exports, we can start the name with an underscore (e.g., _my_name) to prevent it from being actually exported. (By convention, a starting underscore is kind of like a "private" marker.)

A shorthand version of a part can be given when the elements of the above arrays/objects are simple strings instead of further objects. The syntax is a symbol from [+, -, ~, ^], followed by a name, and is equivalent to adding/subtracting/intersecting/stacking an outline of that name, respectively. More specifically, ~something is equivalent to:

type: outlinename: somethingoperation: intersect