Repository navigation
Deck Model
English · Español
The deck data model is the contract between Rust, the SQLite database and the React screen. It lives in src-tauri/crates/proxy-core/src/model.rs, is pure (no Tauri, no network, no database), and every type is exported to TypeScript with ts-rs into src/bindings/ (generated by pnpm bindings, never edited by hand).
One rule shapes everything: one DeckCard per physical copy, not one per card with a quantity. Each copy can therefore have its own variant, edits, back, design, mark and position.
classDiagram
class Deck {
id: String
name: String
decklistText: String
cards: Vec~DeckCard~
settings: PrintSettings
defaultBack: Option~FaceImage~
schemaVersion: u32
}
class DeckCard {
id: String (uuid v4)
source: CardSource
front: Option~FaceImage~
back: BackSpec
edits: CardEdits
locked: bool
meta: CardMeta
design: Option~CardDesign~
marca: Option~MarcaCarta~
}
class CardSource {
<<enum kind>>
Scryfall scryfallId
Token scryfallId
Custom scryfallId? externa?
Blank
}
class RefExterna {
fuente: FuenteCarta
id: String
}
class FaceImage {
blob: String (SHA-256)
width: u32
height: u32
origin: ImageOrigin
}
class BackSpec {
<<enum kind>>
DeckDefault
Dfc image
Custom image
}
class CardEdits {
rotation: Rotation
crop: Crop
adjust: Adjustments
}
class CardMeta {
name, set, collectorNumber, lang
langFallback, layout, isDfc
imageStatus, highres, typeLine
imageLang?
}
class PrintSettings
class CardDesign
Deck "1" *-- "many" DeckCard
Deck "1" *-- "1" PrintSettings
Deck "1" o-- "0..1" FaceImage : defaultBack
DeckCard *-- CardSource
DeckCard o-- "0..1" FaceImage : front
DeckCard *-- BackSpec
BackSpec o-- FaceImage
DeckCard *-- CardEdits
DeckCard *-- CardMeta
DeckCard o-- "0..1" CardDesign : design
CardSource o-- RefExterna : Custom.externa
CardDesign belongs to the card designer (src-tauri/crates/proxy-core/src/diseno/modelo.rs) and is documented in Designer-Engine.
| Field | Meaning |
|---|---|
id |
uuid v4, created in store::decks::create. |
name |
Display name (trimmed on create/rename). |
decklist_text |
The decklist exactly as the user typed it. It is an input; the deck is cards. It is kept so the panel can show it again and so Export can recover list sections (see below). |
cards |
Ordered Vec<DeckCard>; order is the print order and is persisted in deck_cards.position. |
settings |
PrintSettings (see below). |
default_back |
Back used by every card whose BackSpec is DeckDefault. A new deck gets the generic back (BlobStore::generic_back). |
schema_version |
DECK_SCHEMA_VERSION (currently 2). |
#[serde(tag = "kind", rename_all = "camelCase")], so JSON looks like {"kind":"scryfall","scryfallId":"..."}.
| Variant | Meaning |
|---|---|
Scryfall { scryfall_id } |
A concrete Scryfall printing. |
Token { scryfall_id } |
A token added from another card's all_parts. |
Custom { scryfall_id?, externa? } |
An image supplied by the user (or downloaded from an external source). scryfall_id optionally links it to the real Scryfall card it represents (used for card info and Statistics); the image stays the user's own. externa (RefExterna { fuente: FuenteCarta, id }) links to a card from a non-Scryfall source; today the only FuenteCarta is Cockatrice (tokens Scryfall lacks, see External-Sources). |
Blank |
An empty slot (what "+ Add page" leaves behind). |
Decision: externa is an optional field on Custom, not a new enum variant. Why: an older app that does not know the field still deserializes the card, and simply sees a plain custom card (serde ignores unknown fields), so older versions keep opening those decks.
FaceImage { blob, width, height, origin }: blob is the lowercase SHA-256 hex (64 chars) of the stored file; the bytes live in the blob store, see Storage-and-Blobs. ImageOrigin decides, among other things, whether edge darkening applies (EdgeDarkening.apply_scryfall / apply_uploads / apply_backs):
| Value | Source |
|---|---|
ScryfallPng |
Scryfall PNG (745x1040, transparent corners). |
ScryfallLarge |
Scryfall large JPG (672x936), when there is no PNG. |
Upload |
User upload (also the fallback when parsing an unknown origin string). |
Generated |
Generated by the app (the generic back). |
Designed |
Thumbnail of a card-designer design; the PDF does not use it, it repaints the design at export resolution. |
front: None means a blank slot or an image not downloaded yet.
DeckDefault (use Deck.default_back), Dfc { image } (the back face of a double-faced card), Custom { image } (a back chosen for this card).
rotation (R0/R90/R180/R270), crop (zoom >= 1, offset_x/offset_y in [-1, 1] inside the margin the zoom leaves; 0 = centred), and adjust (Adjustments, the eleven fabricard retouches, 0 = no effect: brightness, exposure, contrast, saturation, warmth, vibrance, sharpness, clarity, noise_reduction, vignette, fade). Formulas live in imaging::adjust, see Imaging-and-PDF. is_identity() lets the pipeline skip work.
Display/decision data: name, set, collector_number, lang, lang_fallback (a language was requested but missing, so it stayed English), layout (Scryfall layout), is_dfc, image_status, highres, type_line, and image_lang. image_lang is only meaningful for custom cards: the language of the image, which need not match the linked card's lang (a Spanish custom image linked to the English printing). It is set by whoever links (the footer read by OCR, see Card-Identification-and-OCR) or chosen in the card editor.
A pinned card: "Auto sort" and "Reapply language" skip it.
Optional CardDesign from the card designer (2.0). skip_serializing_if = "Option::is_none" keeps v1 JSON byte-identical for cards that have none.
marca: Option<MarcaCarta> is a fixed tag for organising the deck. It does not change what is printed (a marked card still goes to the PDF). It changes what counts.
MarcaCarta |
UI name | Counts toward deck size / copies / stats? |
|---|---|---|
| (none) | unmarked | Yes. |
Variante |
design variant | Only one copy per card name counts, and only if no unmarked copy of that name exists. |
Opcional |
optional | No. |
Banquillo |
sideboard | No (but exported to the sideboard section). |
Copia |
copy (acts as a token) | No. A real card used as the token of a Clone or a shapeshifter: in the game table it goes to the tokens («Las de tu deck»), not to the library, and it is not a candidate for a play list (mesa::preparar::secciones_de_mesa, candidatas_de_lista). The automatic detection (es_ficha) only knows Scryfall tokens, so this is how a real card is told to be one. Older versions of the app cannot open a deck that has this mark. |
proxy_core::model::cuentan(cards) -> (Vec<&DeckCard>, Apartadas) is the single place where that rule is implemented. Names are compared trimmed and lowercased. Apartadas { variantes, opcionales, banquillo, copias } reports how many copies were left out (total()), surfaced to the screen as DeckStats.setAside. Callers:
-
src-tauri/src/commands/stats.rs(calcular): statistics only look at counted cards. -
src-tauri/crates/proxy-core/src/decklist/exportar.rs(juntar): the exported list skips non-counted cards, exceptBanquillo, which goes to the sideboard block. Tokens and blanks are never written (va_en_la_lista); aCustomcard linked to anexternasource counts as a token.
Test: las_marcadas_no_cuentan in model.rs.
Deck has no section field. Sections come from the decklist parser (proxy_core::decklist::Section: Main, Commander, Sideboard, Companion, Maybeboard, Tokens, see Decklist-Parsing). When the list is resolved (src-tauri/src/scryfall/resolve.rs), only two sections turn into marks: Sideboard becomes Banquillo and Maybeboard (if included) becomes Opcional; the rest get no mark. When exporting, sections are re-derived by name from decklist_text (secciones_de / seccion_de in exportar.rs), so renaming a card in the deck without editing the list can lose its section: Inferred from the name-based lookup; read exportar.rs before relying on it.
Resolving a list line with quantity N creates N DeckCards with fresh uuids (resolve.rs: for _ in 0..l.qty). "Copies of a card" is therefore a count over cards filtered by cuentan, not a stored number.
PrintSettings (everything that decides how the paper comes out) is stored inside the deck as Deck.settings, serialized into decks.settings_json. The same type is what Layout-Engine and Imaging-and-PDF consume, and what the export panel edits through useDeck.setAjustes (see State-Stores). Named, reusable copies of print settings or row offsets live separately as presets (store::presets, table presets, kinds frontOffsets, backOffsets, print; UI in src/features/export/presets.tsx).
| Field | Type / default | Notes |
|---|---|---|
paper |
PaperFormat, A4
|
A3 A4 A5 Letter Legal Tabloid Photo4x6 Photo5x7 Photo8x10 Photo8x12 (paper.rs). |
landscape |
false |
|
card_size |
CardSize::MTG 63x88 mm |
Alternatives CardSize::POKER 63.5x88.9 mm (2.5x3.5 in) and CardSize::FUNDA 65x90 mm, which fills a sleeve exactly (see Export). |
min_margin_mm |
5.0 | |
gap_mm |
2.0 | White gap between cards, as fabricard; each card keeps its exact 63x88 size and bleed does not cover the gap. |
bleed_mm |
0.0 | Bleed is available but off by default. |
bleed_on_backs |
true |
|
bleed_mode |
Clamp |
Clamp repeats the edge pixel, Mirror mirrors. |
fill_corners |
false (#[serde(default)]) |
Paint transparent/light rounded corners with the edge colour; off means transparent corners come out as paper colour. |
cut_guides |
CutGuides, disabled |
enabled, color (#00c853), width_px 0.5, offset_mm 0, length_mm 5. |
edge_darkening |
EdgeDarkening, disabled |
mode (ContrastEdges/SolidBlack/Threshold), amount 80, width_pct 8, auto_detect, apply_scryfall/uploads/backs. |
duplex |
Off |
Off, LongEdge (the back is mirrored left to right and never rotated; a saved shortEdge is read as LongEdge). |
interleave_dfc |
false |
|
dfc_filter |
All |
HideDfc removes double-faced cards from the plan and the PDF. |
quality |
Dpi600 |
Dpi300/600/900; dpi() gives the number. |
encoding |
JPEG, quality 95 |
ImageFormat::{Jpeg,Png}, jpeg_quality 1..=100 (JPEG only). |
row_offsets |
empty |
RowOffsets { front, back }, millimetres per row. |
split_every |
None |
Split the PDF every N sheets. |
design_lang |
En for new decks; Es when missing in a stored deck (#[serde(default)]) |
Language designed cards print in (DesignLang::{Es,En}). |
big_cards_back |
false (#[serde(default)]) |
Big cards (planes, Vanguard) print the deck back on duplex; otherwise their back is blank. |
Invariants worth knowing:
- Decision:
RowOffsets.backis expressed in the front face's frame and indexed by the front row (what you see holding the sheet up to the light from the front).layout::planapplies it before mirroring. Missing rows are 0 (front_row,back_row). - Decision: the defaults (2 mm gap, 0 bleed, guides off, fill corners off, 600 dpi, JPEG 95) are asserted by
los_ajustes_por_defecto_son_los_de_fabricard. Change them there and in the test together. - Decision: the card is stretched into the exact slot and only cropped when the aspect ratio differs by more than 15 percent (
MAX_ESTIRADO, inimaging::frame). That is an imaging rule, not a model field.
- All structs/enums use
#[serde(rename_all = "camelCase")]; internally tagged enums usetag = "kind"(CardSource,BackSpec). Unit enums serialize as camelCase strings ("dpi600","off"). Testslos_campos_van_en_camel_caseandel_origen_de_una_carta_viaja_etiquetado. -
ts-rsmirrors this: where serde renames a field explicitly (scryfallId), the code adds#[ts(rename = ...)]and#[ts(optional)]forOptionfields that are skipped whenNone. - The database stores each card as one
card_jsonstring and the settings assettings_json(see Storage-and-Blobs). The.pbdeckpackage embeds the wholeDeckas JSON inmanifest.json(see Deck-Package-Format).
-
DECK_SCHEMA_VERSION = 2. Version 2 addedDeckCard.design. Decision: the constant was bumped because a 1.x app that opened and re-saved a v2 deck would silently drop the designs (serde ignores the unknowndesignfield and the resave omits it). -
store::decks::savealways writes the currentDECK_SCHEMA_VERSIONintodecks.schema_version;store::decks::openreturns the stored value. I found no code that rejects a deck byschema_versionwhen opening from the database; only package import checks a version (package::VERSION, see Deck-Package-Format). - Newer-field strategy (read this before adding a field): old data must still deserialize, so a new field must be
Option<T>with#[serde(default, skip_serializing_if = "Option::is_none")](asdesign,marca,image_lang,Custom.externa) or carry#[serde(default)](asfill_corners,design_lang,big_cards_back).skip_serializing_ifalso keeps unchanged cards byte-identical on resave. The vast majority ofPrintSettingsfields have no serde default, so adding a required field there would make every stored deck fail to open (ErrorCode::Db,de_jsoninstore/decks.rs). - Older app opening a newer deck: unknown fields are ignored (no
deny_unknown_fieldsanywhere insrc-tauri), so new optional data (marks, links, designs) is dropped on the next save by the older app. A new enum variant or value an old app does not know (for example a newImageOrigin) is a hard deserialize error instead. Henceexternawas not made a variant. -
CardDesignhas its ownversion(VERSION_DISENO = 1,diseno/modelo.rs).
| Where | What |
|---|---|
src-tauri/src/store/decks.rs |
create, open, save, duplicate (new ids, shared images). |
src-tauri/src/scryfall/resolve.rs |
Builds DeckCards from a parsed list (hacer, copies in list order, tokens last). |
src/stores/deck-store.ts |
The open deck in the screen, history and autosave (see Storage-and-Blobs). |
src/bindings/Deck.ts, DeckCard.ts, PrintSettings.ts... |
Generated types. |
- Add the field in
model.rswith the serde attributes above, add#[ts(optional)]if relevant, runpnpm bindings. - If it references images, make
store::decks::refs_del_deck/package::blobs_del_deckaware of it (otherwise garbage collection can delete the image, see Storage-and-Blobs). - If it changes counting, change
cuentanand its test, and checkcommands/stats.rsanddecklist/exportar.rs. - Update every
DeckCard { ... }literal the compiler flags (tests instore/,package.rs,exportar.rs,resolve.rs).
Use and build · Uso y desarrollo
- User-Guide · Guía de usuario
- Development-Setup · Preparación del entorno de desarrollo
- Testing-and-CI · Tests y CI
- Releases-and-Auto-Updates · Versiones y actualizaciones automáticas
- Conventions · Convenciones
Architecture · Arquitectura
- Architecture · Arquitectura
- IPC-and-Error-Handling · IPC y manejo de errores
- Rust-App-Layer · Capa de aplicación Rust
Data · Datos
- Deck-Model · Modelo del mazo
- Storage-and-Blobs · Almacenamiento y blobs
- Deck-Package-Format · Formato del paquete de mazo
- Deck-Library · Biblioteca de mazos
Getting cards · Conseguir cartas
- Decklist-Parsing · Parseo de la lista del mazo
- Scryfall-Integration · Integración con Scryfall
- External-Sources · Fuentes externas
- Card-Identification-and-OCR · Identificación de cartas y OCR
Layout, images, PDF · Maquetación, imagen y PDF
- Layout-Engine · Motor de maquetación
- Imaging-and-PDF · Imagen y PDF
- Export · Exportación
- Live-Preview · Vista previa en vivo
- Statistics · Estadísticas
Card designer · Diseñador de cartas
- Designer-Overview · Visión general del diseñador
- Designer-Engine · Motor del diseñador
- Designer-Composition · Composición del diseñador
- Frame-Packs · Packs de marcos
- Designer-App-Layer · Capa de aplicación del diseñador
- Designer-UI · Interfaz del diseñador
- Card-Editor-Dialog · Diálogo Editar carta
Game table · Mesa de juego
Frontend · Frontend
- Frontend-Architecture · Arquitectura del frontend
- State-Stores · Stores de estado
- Internationalization · Internacionalización
- Themes-and-Skins · Temas y skins
- Settings-and-Shell · Ajustes y shell
- Tutorials · Tutoriales
- Decklist-Panel · Panel de la lista del mazo
- Add-Card-and-Variants · Añadir carta y variantes
- Set-Browser · Explorador de sets
Reference · Referencia