Defensive Parsing
parsePanelConfig() (packages/) is a total, non-throwing repair boundary for arbitrary JSON. tryParsePanelConfig() is the stricter import/autosave gate: it first requires app: "zpd", an integer version from 1 through 5, and a layers array, then delegates field recovery to parsePanelConfig().
Top-level recovery
| Input | Result |
|---|---|
| Not a plain object | The complete createDefaultDoc() fixed-stack starter. |
Object with missing/non-array layers | A canonical three-container stack with empty children. |
Missing/non-array guides | []. |
Invalid/missing/non-positive hp | DEFAULT_PANEL_HP; values above MAX_PANEL_HP are clamped. |
| Hand-edited panel dimensions or palette | Ignored; they are derived advisory output. |
| Unknown keys | Dropped. |
parsePanelConfig() treats an absent/non-integer version as legacy v4 for repair purposes. The strict sibling rejects a missing, future, or otherwise unsupported version instead.
Fixed-stack canonicalization
Every object input returns the same physical bottom-to-top roots:
Copper
Solder mask
Silkscreen
For v1–v4 free-root documents, ordinary leaves and groups are partitioned by their legacy paint. A group spanning materials is split while preserving its ordinary shells, order, names, and visibility where possible. A path whose fill and stroke map to different materials is split into material-specific copies. Images, paintless paths, and empty groups default to Copper.
For v5, wrapper ids, labels, and order are not trusted. Valid roles are merged into the canonical roots, missing roles are synthesized, duplicate roles are combined, and recoverable children of malformed wrappers are legacy- partitioned. Every non-null vector paint is normalized to its final container membership. Fixed wrappers can never be recovered as selectable ordinary groups.
Ordinary nodes and ids
parseLayerNode() accepts ordinary kind: "group" nodes recursively up to MAX_GROUP_DEPTH, plus the five leaf types. Malformed/non-object nodes and unknown types are dropped independently. Unknown fields do not survive.
Ids are allocated deterministically across the whole ordinary tree. Missing or empty ids receive a deterministic type/group fallback; collisions with an earlier id or a fixed root receive stable numeric suffixes. Repeating the same parse produces byte-identical output. No time/random mintId() call occurs at this persistence boundary.
Common field behavior:
| Field | Recovery |
|---|---|
name | Non-string becomes "". |
hidden | Kept only when boolean; otherwise omitted. |
| finite geometry/rotation | Non-finite values fall back or are omitted, depending on whether the field is required. |
color, non-null fill/stroke | Invalid indexes first fall back to 0, then final container membership normalizes them. |
path fill/stroke | null remains disabled; non-null remains enabled in the owning material. |
| path points/handles/subpaths | Invalid entries are dropped individually; handles require finite x and y. |
pattern params | Only finite numeric entries survive. |
| pattern geometry | Invalid size falls back to safe panel-cover geometry and is capped by MAX_PATTERN_SIZE_MM. |
Guides
A guide survives only with a valid horizontal/vertical orientation and finite position. Missing ids use a deterministic one-based guide-${sourceIndex + 1} fallback, then all explicit/generated collisions are repaired with stable suffixes. hidden is retained only when boolean. One malformed guide never drops the others.
Why two entry points?
parsePanelConfig()is useful for trusted migration and low-level recovery: it never throws and always returns a safeDocState.tryParsePanelConfig()prevents a random JSON file, foreign app envelope, or future format from being silently accepted by the import UI and autosave. See Round-Trip → Importing.
The repaired result is still PanelConfig JSON, not Gerber data. Gerber files come from the separate artwork-only .zip export, which runs off the in-memory document rather than off this file.