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
| Field | Type | Meaning |
|---|---|---|
format | const "pixelweaver-part-manifest" | Self-identifying document type. |
version | semver string | Format version. v1 emits "1.0.0". |
frame | FrameSpec | Shared cell geometry and anchor for ALL parts. |
alphaPolicy | const "binary" | Pixels are fully transparent or fully opaque. |
palette | array of #rrggbb | Master palette copied from the constraint manifest (empty for pure-rgba projects). |
classes | array of PaletteClass | Palette classes (index ranges into the master palette). |
parts | array of Part | Composable parts, sorted by zOrder ascending. |
pairings | array of Pairing | Part-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.jsonis 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
zipfilewith 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-manifestreturns 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_manifestandexport_part_stripexpose the same
output to agents, alongside the list_parts / define_part / update_part / remove_part part-registry tools.