PixelWeaver v0.5.0 /Part-manifest export format
On this page

PixelWeaver's versioned, engine-agnostic part-manifest export format (v1) for palette-LUT-recolored paper-doll sprite parts: layers, z-order, palette classes, variants, anchors, pairing, and deterministic per-part PNG strips.

#Part-manifest export format

The part manifest is PixelWeaver's own, versioned export artifact for composable paper-doll sprite parts. It captures everything an external engine needs to assemble and palette-recolor a character: the stacking order of parts, the palette classes into a shared master palette, each part's animation variants, the shared cell geometry and anchor, and part-level pairing.

The format is generic and engine-agnostic -- it contains no game-specific fields -- and it is additively extensible: future versions add fields; v1 describes exactly the legal v1 content.

The canonical schema is the strictspec schema at schemas/part-manifest.toml. strictspec is the boundary validator: TypeScript and Python validators are generated from it by strictspec gen (driven by strictspec.toml) and committed read-only (src/lib/parts/part-manifest.gen.ts, server/src/pixelweaver/_gen/part_manifest.py). The app's mutable domain types are hand-authored in src/lib/parts/part-manifest.ts; the generated validators run at ingress/at-rest (raw bytes) and in-memory (tagged value) boundaries only. Every governed document carries a format_version gate.

#Document shape

The export is a flat set of files:

  • part-manifest.json -- the manifest document.
  • <part>.rgba.png -- the resolved-RGBA strip for every part.
  • <part>.indexed.png -- the role-indexed strip for classed parts only.

#Top-level fields

Top-level fields
FieldTypeMeaning
formatconst "pixelweaver-part-manifest"Self-identifying document type.
versionsemver stringFormat version. v1 emits "1.0.0".
frameFrameSpecShared cell geometry and anchor for ALL parts.
alphaPolicyconst "binary"Pixels are fully transparent or fully opaque.
palettearray of #rrggbbMaster palette copied from the constraint manifest (empty for pure-rgba projects).
classesarray of PaletteClassPalette classes (index ranges into the master palette).
partsarray of PartComposable parts, sorted by zOrder ascending.
pairingsarray of PairingPart-level index-locks (may be empty).

FrameSpec carries width/height (cell pixel size, equal to the shared canvas dimensions) and anchorX/anchorY (the shared canvas origin, in cell pixel coordinates).

Each Part has a file-safe name (matching ^[a-zA-Z0-9][a-zA-Z0-9._-]{0,63}$), its source partSet, an integer zOrder (lower composites first, unique across parts), a paletteClassId (null for a pure-rgba part), a sorted variants list, a strips object (indexed file name -- null exactly when paletteClassId is null -- and rgba file name), and a grid describing the strip layout (frameWidth, frameHeight, columns = max frame count across variants, rows = variant count, padding = 0 in v1).

Each Variant records its index (strip row), the source canvas name, the frameCount, and a fully-resolved frameDurationsMs array (per-frame durations, with null per-frame durations resolved to round(1000 / fps) -- no nulls in the artifact).

A Pairing names two parts (partA < partB lexicographically); variant i of partA is used together with variant i of partB.

#Per-part PNG strips

For every part, one strip PNG is written per kind:

  • Role-indexed strip (<part>.indexed.png, classed parts only) -- an 8-bit

palette PNG (color type 3) whose pixel bytes are master-palette indices (class.start + role). The PLTE is the master palette padded to 256 entries; a tRNS chunk marks index 255 (the role-transparent sentinel) fully transparent and every other index opaque.

  • Resolved-RGBA strip (<part>.rgba.png, all parts) -- an RGBA8888 PNG. For

classed parts it is the exact LUT resolution of the indexed strip; for pure-rgba parts it is the source-over composite of the visible rgba layers.

Cells are laid out with one row per variant (keyed by variant index, which must be contiguous 0..N-1) and one column per animation frame; cells past a variant's frame count are fully transparent. Strips are exported at source resolution (1x); there is no scale option.

Layer selection is strict: for a classed part, a visible role layer using a different class, a visible rgba layer with any painted pixel, or a role layer that is not full-opacity/normal-blend is a hard export error. For a pure-rgba part, any visible role layer is a hard export error. Mixed documents (rgba scratch layers) are legal only when those layers are hidden or promoted.

#Determinism

Given identical project state, the export is reproducible:

  • **part-manifest.json is byte-identical** across the TypeScript (UI) and

Python (headless) implementations and across repeated runs: UTF-8, 2-space indent, \n line endings, a single trailing newline, keys in a fixed order, arrays sorted as specified, and no timestamps, host paths, or app-version stamps anywhere.

  • Strip pixels, palette, and transparency are identical across

implementations. The headless Python path is byte-deterministic (PIL with fixed save arguments; no ancillary chunks beyond PLTE/tRNS). The UI path is pixel-deterministic; its compressed bytes may differ across browser engines because it uses the platform CompressionStream.

  • The server zip route is byte-deterministic (Python zipfile with a fixed

date_time, ZIP_DEFLATED, and entries sorted with part-manifest.json first). The UI download zip is a convenience and is only pixel/JSON deterministic.

This is verified by a shared golden fixture: the committed server/tests/fixtures/parts-project/golden/part-manifest.json is compared byte-for-byte from both the Python export test and the TypeScript export test.

#Export paths

The manifest and strips can be produced from three entry points, all backed by the single headless implementation in server/src/pixelweaver/part_export.py (with a faithful UI twin in src/lib/parts/part-export.ts):

  • UI -- the Export dialog offers a "Character parts (manifest + strips)"

option that downloads a zip.

  • HTTP -- GET /api/projects/{name}/export/part-manifest returns the JSON

document; GET /api/projects/{name}/export/part-strip/{part}/{kind} returns a strip PNG (kind is indexed or rgba); GET /api/projects/{name}/export/parts-zip returns a deterministic zip. Validation problems return 422 with the full problem list; unknown project/part returns 404.

  • CLI -- pixelweaver export-parts <name> --out <dir> reads a project from

disk and writes part-manifest.json plus all strips into <dir>. It needs no running server; validation failures print the full problem list and exit non-zero.

  • MCP -- export_part_manifest and export_part_strip expose the same

output to agents, alongside the list_parts / define_part / update_part / remove_part part-registry tools.

Search