Skip to content

Save File Format

Claxon edited this page Sep 29, 2026 · 1 revision

Saving and importing

Export board writes the layout to control-function-layout.json. Import board reads one back, replacing the canvas as a single undoable step. Files are validated on the way in — anything that isn't a Control Function layout is refused and leaves your work untouched.

{
  "format": "cf.layout",
  "version": 3,
  "unit": "u",
  "groups": [
    { "id": "g1", "x": 4, "y": 2, "rotation": 15, "pivot": { "x": 0, "y": 0 } }
  ],
  "pcbs": [
    { "id": "p1", "shape": "rectangular", "offset": 1, "name": "Left half",
      "controller": "Elite-C", "hotswap": true, "diodeDefault": true, "backlight": false,
      "stabilizerStyle": "screw-in", "usb": { "t": 0.875, "connector": "usb-c" },
      "batteryPresent": false, "batteryCapacity": "" }
  ],
  "links": [
    { "id": "l1", "boardIds": ["p1", "p2"], "connector": "trrs" }
  ],
  "rows": [{ "id": "r1", "name": "Row 01" }],
  "cols": [{ "id": "c1", "name": "Column 01" }],
  "keys": [
    { "x": 0, "y": 0, "w": 1, "h": 1, "shape": "rectangular",
      "primary": "2", "secondary": "@", "tertiary": "€", "fn": "", "home": false,
      "groupId": "", "pcbId": "p1", "rowId": "r1", "colId": "c1" }
  ]
}

A key's groupId, if set, names an entry in groups, and its x/y are then relative to that group's own origin. Its pcbId, rowId and colId name entries in pcbs, rows and cols the same way — that is the whole of a board's, row's or column's membership. All four ids are remapped on import.

A board carries no geometry. shape is rectangular or cutout; offset is a whole number of 0.25u steps, 0–8, defaulting to 1. name is free text and unique — a duplicate keeps the first board and blanks the rest. controller and stabilizerStyle are free text, with an unrecognized value falling back to unset. hotswap and backlight default to false; diodeDefault defaults to true, so an older save keeps the original BOM assumption. usb is { t, connector } — a 0–1 fraction of the perimeter and usb-c/micro-usb — or null for no marker; the old { edge, t } form is converted rather than dropped. batteryPresent and batteryCapacity exist on every board regardless of linking.

A link's boardIds names entries in pcbs, remapped on import; a link left with fewer than two resolving boards is dropped. connector is trrs, trs or wireless, defaulting to trrs.

A rows/cols entry is nothing but { id, name }; its members are exactly the keys naming it. One with no members after import is removed.

Older files still load. Version 2 and 1 predate rows and columns, so those default to unset. Version 1 also describes a board as a placed x/y/w/h rectangle with no pcbId on any key: on import that rectangle runs its old "is this key's centre inside" test one last time, the answer is written down as membership, and the rectangle is dropped. Saves from before cutout existed read back as rectangular with no offset.

Clone this wiki locally