zudo-panel-designer docs
GitHub repository

Type to search...

to open search from anywhere

Document state & layers

DocState, the Layer union, the mm coordinate space, and how a document serializes to PanelConfig.

Coordinate space: millimeters, origin top-left

Every geometric value in the document model — layer x/y, width/height, path anchors and bezier handles, stroke widths — is stored in millimeters, with the origin at the panel's top-left corner. This is deliberate: PCB fabrication data is mm-based, so mm is the single storage space throughout @zpd/core. Pixels only exist at the render boundary, where the app's camera converts mm to screen px for display. Nothing in the document model itself is pixel-based.

DocState

interface DocState {
  panelHp: number;
  layers: PcbLayerStack; // Copper -> Solder mask -> Silkscreen
  guides: Guide[]; // ruler guides — view furniture, not layers
}
  • panelHp — the panel's width, expressed in HP (see Panel sizing).

  • layers — the fixed physical stack in bottom-to-top order. Each root owns ordinary layer/group children:

    type PcbLayerRole = 'copper' | 'solder-mask' | 'silkscreen';
    
    interface PcbLayerContainer {
      kind: 'pcb-layer';
      id: `pcb-layer-${PcbLayerRole}`;
      role: PcbLayerRole;
      children: LayerNode[];
      hidden?: boolean;
    }
    
    type PcbLayerStack = [
      PcbLayerContainer<'copper'>,
      PcbLayerContainer<'solder-mask'>,
      PcbLayerContainer<'silkscreen'>,
    ];

    These roots are not selectable or mutable ordinary groups. LayerNode is either a leaf Layer or an ordinary nested GroupNode; only those ordinary nodes can be renamed, deleted, grouped, and moved.

  • guides — the document's ruler guides, a required array (see Guides).

The Layer union

A layer is one of five variants, discriminated by type. All variants share LayerBase:

interface LayerBase {
  id: string;
  name: string;
  hidden?: boolean;
}
TypeManufacturable?Summary
shapeYesRectangle or ellipse, filled with its owning container's material.
patternYesA named, parameterized pattern from @zpd/patterns (opaque to core).
pathYesA bezier path — filled, stroked, or both — from the pen tool or an image trace.
textYesRendered text in a fixed font/size and its owning material.
imageNo (design-time only)A raster reference image; the panel is fabricated from vector layers traced from it, not the raster itself.

Shape layer

interface ShapeLayer extends LayerBase {
  type: 'shape';
  shape: 'rect' | 'ellipse';
  x: number;
  y: number;
  width: number;
  height: number;
  rotation?: number; // deg clockwise around bbox center
  color: ColorIndex;
}

Pattern layer

interface PatternLayer extends LayerBase {
  type: 'pattern';
  patternType: string;
  params: Record<string, number>;
  color: ColorIndex;
  x: number;
  y: number;
  size: number;
}

patternType and params are kept as opaque data even when @zpd/core doesn't recognize the pattern — the patterns registry is an app-level concern, not a core dependency. This is also why pattern layers get special hit-testing treatment; see Geometry & editing contracts.

Path layer

interface PathPoint {
  x: number; // anchor, mm
  y: number;
  hin?: { x: number; y: number }; // absolute bezier handle coords, mm
  hout?: { x: number; y: number };
}

interface PathLayer extends LayerBase {
  type: 'path';
  points: PathPoint[]; // primary subpath (pen tool edits this one)
  extraSubpaths?: PathPoint[][];
  closed: boolean;
  fill: ColorIndex | null;
  stroke: ColorIndex | null;
  strokeWidth: number; // mm
}

extraSubpaths holds additional closed subpaths produced by image tracing — the holes/islands of one color region. They render together with the primary subpath using even-odd fill, so holes stay holes.

color, fill, and stroke are compatibility fields. Container membership forces every non-null paint to the owning material; for paths, null versus non-null still records whether fill or stroke is enabled.

Text layer

interface TextLayer extends LayerBase {
  type: 'text';
  content: string; // may contain newlines
  fontFamily: string;
  sizeMm: number; // font size in mm (canvas font px == mm in doc space)
  x: number; // bbox top-left, mm
  y: number;
  rotation?: number;
  color: ColorIndex;
}

Image layer

interface ImageLayer extends LayerBase {
  type: 'image';
  src: string; // dataURL
  x: number;
  y: number;
  width: number;
  height: number;
  rotation?: number;
}

src is a design-time source only — a raster cannot be manufactured on the panel. The final panel uses the vector layers traced from it, not this layer.

Guides

Guides are straight reference lines the editor draws to help you align layers. They are view furniture, not layers — they live in DocState.guides and never in layers, so they carry no color and are never fabricated.

interface Guide {
  id: string;
  orientation: 'horizontal' | 'vertical';
  position: number; // mm
  hidden?: boolean;
}

A horizontal guide is the line y = position (spanning the panel width); a vertical guide is x = position (spanning the height). guides is required rather than optional so read sites never need doc.guides ?? []; the serialization boundary owns backward-compat instead (an old config with no guides loads as [] — see below). The editor-side behavior — dragging guides out of the rulers, snapping, and the "Show guides" toggle — is covered in Editor → Guides.

Serializing to PanelConfig

DocState is the in-memory working document. PanelConfig is the versioned, exported shape a user downloads and hands off for fabrication:

const PANEL_CONFIG_VERSION = 5;

interface PanelConfig {
  version: 5;
  app: 'zpd';
  panel: { hp: number; widthMm: number; heightMm: number };
  palette: string[];
  layers: PcbLayerStack;
  guides: Guide[];
}

hp, layers, and guides are authoritative and round-trip. panel.widthMm, panel.heightMm, and palette are derived, advisory output for a human/order reader — computed at export time from hp and the fixed palette — and are not re-trusted when the file is loaded back in.

v5 added the fixed PCB material stack. v1–v4 free-root documents are deterministically partitioned by their legacy paint into Copper, Solder mask, and Silkscreen. Groups that span multiple materials are split while preserving ordinary structure where possible, ids are repaired deterministically, and all non-null paint is normalized to container membership. Malformed v5 roots are likewise canonicalized into the fixed order. See PanelConfig Format → Versioning.

parsePanelConfig never throws

parsePanelConfig is fed whatever JSON a user hand-edited or an old/foreign tool produced, so every field is defended individually rather than letting one bad field fail the whole document. Invalid panel/layer fields fall back or are dropped, v1–v4 are migrated, malformed v5 stacks are canonicalized, and unsupported or future config versions are rejected by the exact-version boundary used for persisted autosave data.

See the full source in packages/core/src/serialize.ts for the field-by-field parsing rules.