PanelConfig Format
PanelConfig is the versioned JSON envelope the editor downloads as the panel order artifact. It is produced by serializePanelConfig() and consumed by parsePanelConfig(), both exported from @zpd/core (source: packages/).
Envelope shape
{
"version": 5,
"app": "zpd",
"panel": { "hp": 12, "widthMm": 60.6, "heightMm": 128.5 },
"palette": ["black", "gold", "white"],
"layers": [
{ "kind": "pcb-layer", "id": "pcb-layer-copper", "role": "copper", "children": [] },
{ "kind": "pcb-layer", "id": "pcb-layer-solder-mask", "role": "solder-mask", "children": [] },
{ "kind": "pcb-layer", "id": "pcb-layer-silkscreen", "role": "silkscreen", "children": [] }
],
"guides": []
}| Field | Type | Notes |
|---|---|---|
version | 5 | PANEL_CONFIG_VERSION. See Versioning below. |
app | "zpd" | Fixed literal identifying the producing app. |
panel.hp | number | Panel width in Eurorack HP, from the live document (doc.panelHp). |
panel.widthMm | number | Derived from hp via panelWidthMm() — advisory output, not re-trusted on import (see below). |
panel.heightMm | number | PANEL_HEIGHT_MM, the fixed 3U Eurorack panel height (128.5mm). |
palette | string[] | The three fixed color names, in index order: ["black", "gold", "white"]. |
layers | PcbLayerContainer[] | The fixed Copper, Solder mask, and Silkscreen containers in physical bottom-to-top order. |
guides | Guide[] | The document's ruler guides. Added in v2 (see Versioning); see Guide fields. |
Note
panel.widthMm, panel.heightMm, and palette are derived/advisory output for the human or order-system reader — they exist so a person or a downstream fab tool can read the file without recomputing dimensions fromhp. On the way back in, parsePanelConfig recomputes them from hp rather than trusting hand-edited values. panel.hp and layers are the authoritative source data.
Fixed material containers
v5 persists exactly three fixed roots, in physical bottom-to-top order: Copper, Solder mask, then Silkscreen. Each has kind: "pcb-layer", a stable id, a role, optional persisted hidden, and ordinary children. The UI shows the reverse top-to-bottom order. The container determines effective material—gold/HASL copper, black solder mask, or white silkscreen—so legacy child colors are normalized on import. Solder-mask children are openings, not coverage: they mark where mask is removed. A persisted hidden: true on the Solder mask root therefore means no mask anywhere, while an empty visible root means full coverage.
The roots are not ordinary groups: users cannot rename, delete, select, group, or reorder them. Ordinary groups can be nested under each root and artwork can move across roots to change material.
Layer fields
All five layer types share a base shape plus type-specific fields.
Common to every layer
| Field | Type | Notes |
|---|---|---|
id | string | Stable layer id. Missing/empty ids receive a deterministic type/group fallback on import; collisions receive stable numeric suffixes. |
name | string | Display name. Defaults to "" if missing. |
hidden | boolean (optional) | Omitted while a layer has never been hidden. Once its visibility is toggled, the field is written explicitly — including false after an even number of toggles — since the toggle op flips whatever value is already there. |
type | 'shape' | 'pattern' | 'path' | 'text' | 'image' | Discriminates the fields below. |
shape
| Field | Type | Notes |
|---|---|---|
shape | 'rect' | 'ellipse' | |
x, y, width, height | number | Millimeters, document space. |
rotation | number (optional) | Degrees clockwise around the bbox center. Omitted when unset. |
color | 0 | 1 | 2 | See ColorIndex. |
pattern
| Field | Type | Notes |
|---|---|---|
patternType | string | Free-form identifier (e.g. "dot-grid"). Kept verbatim even if unrecognized — core has no dependency on the patterns registry that would validate it. |
params | Record<string, number> | Pattern-specific numeric parameters (e.g. pitch, radius). |
x, y, size | number | Position and side length of the movable square, in millimeters. |
color | 0 | 1 | 2 |
path
| Field | Type | Notes |
|---|---|---|
points | PathPoint[] | Primary subpath — the one the pen tool edits. See PathPoint. |
extraSubpaths | PathPoint[][] (optional) | Additional closed subpaths, e.g. holes/islands produced by image tracing. Presence/absence (even an empty array) is preserved exactly for round-tripping. |
closed | boolean | |
fill | 0 | 1 | 2 | null | null means no fill. |
stroke | 0 | 1 | 2 | null | null means no stroke. |
strokeWidth | number | Millimeters. |
PathPoint
| Field | Type | Notes |
|---|---|---|
x, y | number | Anchor point, millimeters. |
hin | { x: number, y: number } (optional) | Absolute bezier handle-in coordinates. |
hout | { x: number, y: number } (optional) | Absolute bezier handle-out coordinates. |
text
| Field | Type | Notes |
|---|---|---|
content | string | May contain newlines. |
fontFamily | string | |
sizeMm | number | Font size in millimeters (canvas font px equals mm in document space). |
x, y | number | Bounding-box top-left, millimeters. |
rotation | number (optional) | Degrees clockwise. Omitted when unset. |
color | 0 | 1 | 2 |
image
| Field | Type | Notes |
|---|---|---|
src | string | A data URL. Design-time reference only — a raster cannot be manufactured; the final panel uses the vector layers traced from it. |
x, y, width, height | number | Millimeters. |
rotation | number (optional) | Degrees clockwise around the bbox center. |
Guide fields
Each entry in the guides array is a ruler guide — a reference line, not a layer (see Document state & layers → Guides).
| Field | Type | Notes |
|---|---|---|
id | string | Stable guide id. Generated and collision-repaired deterministically if missing or duplicated on import. |
orientation | 'horizontal' | 'vertical' | Required. A horizontal guide is the line y = position; a vertical guide is x = position. An entry with any other value is dropped on import (a guide with no valid axis can't plant a line). |
position | number | The line's coordinate in millimeters. Required and finite; an entry with a missing/non-finite position is dropped rather than defaulted to 0. |
hidden | boolean (optional) | Omitted unless set. A hidden guide renders faintly and never snaps. |
ColorIndex + palette
Every layer's compatibility color field (and a path layer's fill/stroke enablement) uses a ColorIndex: 0, 1, or 2. In v5 the owning fixed container is authoritative and normalizes every non-null paint field to its material index. The mapping is defined once in packages/:
| Index | Name | Meaning |
|---|---|---|
0 | black | Solder mask |
1 | gold | Exposed copper with the product's HASL finish |
2 | white | Silkscreen |
The palette array in the envelope (["black", "gold", "white"]) is these names in index order — it exists so a reader of the JSON doesn't need to know the index-to-name mapping by heart. The palette is fixed; it is not user-configurable per document.
Versioning
PANEL_CONFIG_VERSION is currently 5, and PanelConfig pins version: 5. v1–v4 files used free root layers (v2 added guides); import deterministically partitions their paint into the v5 fixed material stack, preserving ordinary groups where possible and normalizing material from container membership. v5 input is also repaired defensively if fixed-root metadata is missing, duplicated, or reordered. This is a versioned JSON migration, not a Gerber export format; the Gerber files come from the separate artwork-only .zip export (PCB material layers & sizing).