The Question Nobody Asked
I’ve been using a Shapeoko 5 Pro for a while now, and like most CNC hobbyists, I’ve built up a library of .c2d files in Carbide Create. At some point, I started wondering: what is a .c2d file? Could I generate one programmatically? Could I template my projects, parametrically resize joinery, or batch-produce cutting files without clicking through the UI every time?
So I did what any reasonable person would do. I cracked one open.
The First Surprise: It’s a Database
$ file reference.c2d
reference.c2d: SQLite 3.x database, last written using SQLite version 3037000
Not XML. Not JSON. Not some inscrutable binary blob. A .c2d file is a SQLite 3 database. You can open it with any SQLite client, query it with SQL, and inspect every last detail of your project with standard tooling.
This is, frankly, a great engineering decision. SQLite gives Carbide Create atomic writes (no half-saved corrupt files), built-in compression via the sqlar table format, structured storage without needing a custom parser, and the ability to evolve the schema over time. It’s the kind of choice that shows someone on the team has shipped software before.
The Architecture
The database contains five tables. Here’s the conceptual layout:
reference.c2d (SQLite 3)
│
├── params ──────── Project settings (stock size, machine, material)
├── items ───────── Everything: layers, geometry, toolpaths, model
├── sqlar ───────── Embedded files (SVG previews, G-code, images)
├── metadata ────── Reserved (empty in practice)
└── log ─────────── Reserved (empty in practice)
Two tables (metadata and log) are defined but unused in every file I’ve examined. They’re ghosts of features planned or abandoned — maybe undo history, maybe external metadata. The real action happens in params, items, and sqlar.
params: The Key-Value Store
The simplest table is params — a flat key-value store where every value is a text string, regardless of what it actually represents:
SELECT key, value FROM params WHERE key IN ('width', 'height', 'thickness', 'material', 'machine');
| Key | Value |
|---|---|
width | 457.2 |
height | 1168.4 |
thickness | 18.9992 |
material | Softwood |
machine | Shapeoko 5 Pro |
Those dimensions are in millimeters — always. Even when the UI is set to display inches (display_mm=0), the underlying storage is metric. The numbers above translate to an 18" × 46" × ¾" piece of softwood, which is a common cutting board blank.
The full list of parameters covers everything from grid spacing to tiling configuration to background image opacity. There are about 30 keys in total, and they tell you everything about the project setup without touching the geometry.
One detail worth noting: the file embeds version gating. A build_num of 836 means “this file was last saved by build 836,” and a minimum_build_num of 810 means “don’t try to open this with anything older.” There’s even a minimum_carbide_motion_version for the G-code sender. It’s a simple but effective compatibility mechanism.
items: Where the Design Lives
This is the heart of the file. Every layer, every vector shape, every toolpath, and even the 3D simulation model is a row in a single table:
CREATE TABLE items(
id INTEGER PRIMARY KEY AUTOINCREMENT,
uuid TEXT UNIQUE,
name TEXT,
type TEXT, -- "layer", "element", "toolpath", "toolpath_group", "model"
version TEXT, -- "J1" for JSON, "model_2" for binary
sz INT, -- original uncompressed size
data BLOB -- zlib-compressed content
);
The data column is zlib-compressed JSON (tagged with version J1). Decompress it, and you get clean, readable JSON for every object in the design. This was the moment reverse engineering stopped being hard and started being fun.
Layers
Layers come first (IDs 1–5 in my file) and are simple:
{
"blue": 255,
"green": 255,
"locked": false,
"name": "Rabbet",
"red": 0,
"uuid": "{b8ff5e4e-b047-4b6e-81a7-82db4b137ac0}",
"visible": true
}
RGB color, a name, visibility, lock state, and a UUID. The DEFAULT layer is special: it always gets id=1 and uses an empty string for its UUID instead of a GUID. Every project has one, and it can’t be deleted.
Vector Geometry: Everything is Cubic Bezier
This is where it gets interesting. Open a rectangle element, and you might expect to find corner coordinates. Instead, you find this:
{
"geometryType": "rectangle",
"center": [158.75, 73.025],
"width": 254.0,
"height": 19.05,
"corner_type": 0,
"points": [[127, 9.525], [127, -9.525], [-127, -9.525], [-127, 9.525], ...],
"cp1": [[127, 9.525], [127, 9.525], [127, -9.525], [-127, -9.525], ...],
"cp2": [[127, 9.525], [127, -9.525], [-127, -9.525], [-127, 9.525], ...],
"point_type": [0, 1, 1, 1, 1, 4],
"smooth": [0, 0, 0, 0, 0, 1]
}
Every shape — rectangles, circles, freeform paths, dog-bone fillets — is stored as cubic Bezier curves. Even a plain rectangle with sharp corners has full cp1/cp2 control point arrays. For straight line segments, the control points simply collapse to the endpoints (a degenerate cubic that traces a straight line).
This uniform representation means the rendering engine never has to ask “what kind of shape is this?” It just walks the Bezier arrays. The geometryType field exists purely for the UI to know what editing handles to show.
The coordinate system is also worth understanding:
center/position: Absolute coordinates on the stock, in millimeters, origin at bottom-leftpoints,cp1,cp2: All relative to center — local coordinate spacepoint_type:0= start,1= line-to,3= curve-to,4= close path
Dog-Bone Corners: CNC-Aware Geometry
The most CNC-specific feature is corner_type: 4 — dog-bone fillets. These are small circular cutouts at inside corners that extend past the rectangle boundary, allowing a round end mill to fully clear a square corner for joinery.
In the file, a dog-bone rectangle produces 18 control points instead of the usual 6. Each corner contributes four curve points that trace the fillet arc. The coordinates include the characteristic “overshoot” — points that extend beyond the nominal rectangle dimensions by the tool radius. It’s a nice example of how the file format is shaped by the physical reality of CNC routing.
The Denormalization Curiosity
Here’s something that made me do a double-take: every element embeds a complete copy of its layer definition:
{
"geometryType": "rectangle",
"layer": {
"blue": 255,
"green": 255,
"locked": false,
"name": "Rabbet",
"red": 0,
"uuid": "{b8ff5e4e-b047-4b6e-81a7-82db4b137ac0}",
"visible": true
},
...
}
The layer data is duplicated inside every single element that belongs to it. Change a layer’s color? You’d need to update the canonical layer item and every element that references it. This is a deliberate trade-off: it makes reads simple (each element is self-contained) at the cost of write complexity. In a desktop application where saves are infrequent and reads happen every frame, this makes sense.
Toolpaths: Where Design Meets Manufacturing
Toolpaths are stored with the same zlib+JSON pattern, and they’re where the manufacturing intent lives:
{
"type": "contour",
"name": "Top",
"end_depth": "19.050",
"stepdown": 3.175,
"ofset_dir": -1,
"tab_height": 3,
"tab_width": 11.999,
"speeds": {
"feedrate": 1524,
"plungerate": 304.8,
"rpm": 18000
},
"tool": {
"diameter": 6.35,
"flutes": 3,
"model": "201",
"number": 201,
"vendor": "Carbide 3D"
},
"elements": [
{ "uuid": "{9700251e-ba3d-4c97-94f9-cf00f6648173}" }
]
}
A few things stand out:
Depth values are strings:
"19.050"instead of19.05. Every other numeric value is a native JSON number — but depths are strings. This is almost certainly to preserve exact decimal representation for user-facing values. Floating-point19.05might display as19.049999999999997; the string"19.050"is always"19.050".Tool definitions are fully embedded: Just like layers in elements, every toolpath carries a complete copy of its tool definition. The
#201end mill (¼" 3-flute) appears identically in all five toolpaths. Again — denormalization for read simplicity.Two selection mechanisms: Toolpaths can reference geometry explicitly by UUID (
elementsarray) or implicitly by layer (toolpath_layersarray — “cut everything on the Dog Bones layer”). This matches the two workflows in the UI.Feed rates in mm/min: 1524 mm/min = 60 inches per minute. 304.8 mm/min plunge = 12 ipm. Standard values for a ¼" end mill in softwood.
The Encrypted G-Code
The generated G-code lives in the sqlar table as gcode.egc:
First 4 bytes: 43 43 56 31 = "CCV1"
Remaining: encrypted/obfuscated binary
This is the one part of the file that’s deliberately opaque. The CCV1 format (Carbide Create V1) is not zlib-compressed, not plaintext, and not trivially reversible. It’s designed to be consumed exclusively by Carbide Motion, the companion G-code sender.
This is a business decision, not a technical one. Carbide 3D sells machines, and keeping the toolchain integrated helps maintain the ecosystem. You can always export standard G-code through the UI — but the embedded copy is locked down.
The Model: 52 Bytes of Simulation
The 3D stock simulation model is the only non-JSON item. After decompression, it’s a 52-byte binary struct:
Offset Type Value Meaning
0x00 uint32 2 Version
0x08 float64 0.7303... Pixel size (mm)
0x10 float64 1.0 Scale
0x28 uint32 626 Grid width
0x2C uint32 1599 Grid height
The grid dimensions (626 × 1599) multiplied by the pixel size (0.73mm) give you 457mm × 1168mm — exactly the stock dimensions. This is a heightmap resolution descriptor. The actual simulation data (the carved surface) is likely computed at runtime and not stored in the file.
What This Enables
Understanding the file format opens up real possibilities:
- Parametric generation: Write a Python script that takes dimensions and generates a complete
.c2dfile with all the joinery calculated - Batch processing: Template a project once, then generate variants for different stock sizes
- Migration tools: Convert between
.c2dand other CAM formats - Inspection tools: Validate toolpaths and speeds without opening the GUI
- Version control: Since it’s SQLite, you could extract the JSON and diff it meaningfully
The fact that it’s SQLite with JSON blobs makes all of this accessible with standard libraries in any language. No custom parsers needed.
Closing Thoughts
Carbide Create’s file format is surprisingly well-structured. The choice of SQLite as a container is pragmatic and robust. The universal Bezier representation for geometry is elegant. The denormalization is a reasonable trade-off for a desktop app. The only real lock-in is the encrypted G-code, and that’s easily worked around by exporting separately.
If you’re building tools around the Carbide 3D ecosystem, or just curious about how your projects are stored, grab a copy of sqlite3 and start querying. Everything you need is right there in the database.