Skip to content

Modelo del mazo

Pablo Martínez López edited this page Oct 27, 2026 · 5 revisions

Modelo del mazo

English · Español

El modelo de datos del mazo es el contrato entre Rust, la base de datos SQLite y la pantalla de React. Vive en src-tauri/crates/proxy-core/src/model.rs, es puro (sin Tauri, sin red, sin base de datos) y cada tipo se exporta a TypeScript con ts-rs a src/bindings/ (lo genera pnpm bindings, nunca se edita a mano).

Una regla lo condiciona todo: un DeckCard por copia física, no uno por carta con una cantidad. Así, cada copia puede tener su propia variante, ediciones, reverso, diseño, marca y posición.

Resumen de tipos

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
Loading

CardDesign pertenece al diseñador de cartas (src-tauri/crates/proxy-core/src/diseno/modelo.rs) y se documenta en Motor del diseñador.

Deck

Campo Significado
id uuid v4, creado en store::decks::create.
name Nombre visible (recortado al crear o renombrar).
decklist_text La lista del mazo tal como la escribió el usuario. Es una entrada; el mazo son las cards. Se conserva para que el panel pueda volver a mostrarla y para que Exportación pueda recuperar las secciones de la lista (véase más abajo).
cards Vec<DeckCard> ordenado; el orden es el de impresión y se guarda en deck_cards.position.
settings PrintSettings (véase más abajo).
default_back Reverso que usa toda carta cuyo BackSpec es DeckDefault. Un mazo nuevo recibe el reverso genérico (BlobStore::generic_back).
schema_version DECK_SCHEMA_VERSION (actualmente 2).

DeckCard y sus partes

CardSource (etiquetado por kind)

#[serde(tag = "kind", rename_all = "camelCase")], de modo que el JSON queda como {"kind":"scryfall","scryfallId":"..."}.

Variante Significado
Scryfall { scryfall_id } Una impresión concreta de Scryfall.
Token { scryfall_id } Una ficha añadida desde los all_parts de otra carta.
Custom { scryfall_id?, externa? } Una imagen aportada por el usuario (o descargada de una fuente externa). scryfall_id la vincula opcionalmente con la carta real de Scryfall que representa (se usa para la información de la carta y para las Estadísticas); la imagen sigue siendo la del usuario. externa (RefExterna { fuente: FuenteCarta, id }) la vincula con una carta de una fuente que no es Scryfall; hoy el único FuenteCarta es Cockatrice (fichas que Scryfall no tiene, véase Fuentes externas).
Blank Una casilla vacía (lo que deja «Añadir página»).

Decisión: externa es un campo opcional de Custom, no una variante nueva del enum. Motivo: una app antigua que no conozca el campo sigue deserializando la carta y simplemente ve una carta propia normal (serde ignora los campos desconocidos), de modo que las versiones antiguas siguen abriendo esos mazos.

Imágenes: FaceImage e ImageOrigin

FaceImage { blob, width, height, origin }: blob es el SHA-256 en hexadecimal minúscula (64 caracteres) del fichero guardado; los bytes viven en el almacén de blobs, véase Almacenamiento y blobs. ImageOrigin decide, entre otras cosas, si se aplica el oscurecimiento de bordes (EdgeDarkening.apply_scryfall / apply_uploads / apply_backs):

Valor Origen
ScryfallPng PNG de Scryfall (745x1040, esquinas transparentes).
ScryfallLarge JPG large de Scryfall (672x936), cuando no hay PNG.
Upload Imagen subida por el usuario (también el valor de reserva al interpretar una cadena de origen desconocida).
Generated Generada por la app (el reverso genérico).
Designed Miniatura de un diseño del diseñador de cartas; el PDF no la usa, vuelve a pintar el diseño a la resolución de exportación.

front: None significa una casilla en blanco o una imagen aún sin descargar.

BackSpec (etiquetado por kind)

DeckDefault (usa Deck.default_back), Dfc { image } (la cara trasera de una carta de doble cara), Custom { image } (un reverso elegido para esta carta).

CardEdits

rotation (R0/R90/R180/R270), crop (zoom >= 1, offset_x/offset_y en [-1, 1] dentro del margen que deja el zoom; 0 = centrado) y adjust (Adjustments, los once retoques de fabricard, 0 = sin efecto: brightness, exposure, contrast, saturation, warmth, vibrance, sharpness, clarity, noise_reduction, vignette, fade). Las fórmulas viven en imaging::adjust, véase Imagen y PDF. is_identity() permite a la canalización saltarse trabajo.

CardMeta

Datos de visualización y de decisión: name, set, collector_number, lang, lang_fallback (se pidió un idioma que no existía, así que se quedó en inglés), layout (el layout de Scryfall), is_dfc, image_status, highres, type_line e image_lang. image_lang solo tiene sentido en las cartas propias: es el idioma de la imagen, que no tiene por qué coincidir con el lang de la carta vinculada (una imagen propia en español vinculada a la impresión en inglés). Lo fija quien vincula (el pie leído por el OCR, véase Identificación de cartas y OCR) o se elige en el editor de cartas.

locked

Una carta fijada: «Auto ordenar» y «Reaplicar idioma» la saltan.

design

CardDesign opcional del diseñador de cartas (2.0). skip_serializing_if = "Option::is_none" mantiene el JSON de la v1 idéntico byte a byte para las cartas que no lo tienen.

Marcas (marca) y qué cuenta como «en el mazo»

marca: Option<MarcaCarta> es una etiqueta fija para organizar el mazo. No cambia lo que se imprime (una carta marcada sigue yendo al PDF). Cambia lo que cuenta.

MarcaCarta Nombre en la interfaz ¿Cuenta para el tamaño del mazo, las copias y las estadísticas?
(ninguna) sin marca Sí.
Variante variante de diseño Solo cuenta una copia por nombre de carta, y solo si no existe una copia sin marca con ese nombre.
Opcional opcional No.
Banquillo banquillo No (pero se exporta a la sección del banquillo).
Copia copia (hace de ficha) No. Una carta de verdad que hace de ficha de un Clone o un cambiaformas: en la mesa va a las fichas («Las de tu deck»), no a la biblioteca, y no es candidata de una lista para jugar (mesa::preparar::secciones_de_mesa, candidatas_de_lista). La detección automática (es_ficha) solo conoce las fichas de Scryfall, así que esta marca es como se le dice que una carta real es una. Las versiones anteriores de la app no pueden abrir un deck que lleve esta marca.

proxy_core::model::cuentan(cards) -> (Vec<&DeckCard>, Apartadas) es el único sitio donde se implementa esa regla. Los nombres se comparan recortados y en minúsculas. Apartadas { variantes, opcionales, banquillo, copias } informa de cuántas copias se dejaron fuera (total()), y se expone a la pantalla como DeckStats.setAside. Quién la usa:

  • src-tauri/src/commands/stats.rs (calcular): las estadísticas solo miran las cartas que cuentan.
  • src-tauri/crates/proxy-core/src/decklist/exportar.rs (juntar): la lista exportada omite las cartas que no cuentan, salvo Banquillo, que va al bloque del banquillo. Las fichas y los huecos nunca se escriben (va_en_la_lista); una carta Custom vinculada a una fuente externa cuenta como ficha.

Test: las_marcadas_no_cuentan en model.rs.

Secciones

Deck no tiene campo de sección. Las secciones vienen del parser de la lista (proxy_core::decklist::Section: Main, Commander, Sideboard, Companion, Maybeboard, Tokens, véase Parseo de la lista del mazo). Al resolver la lista (src-tauri/src/scryfall/resolve.rs), solo dos secciones se convierten en marcas: Sideboard pasa a Banquillo y Maybeboard (si se incluye) pasa a Opcional; el resto no recibe marca. Al exportar, las secciones se vuelven a deducir por nombre a partir de decklist_text (secciones_de / seccion_de en exportar.rs), así que renombrar una carta del mazo sin editar la lista puede hacerle perder su sección: Deducido de la búsqueda por nombre; lee exportar.rs antes de fiarte de ello.

Copias

Resolver una línea de la lista con cantidad N crea N DeckCard con uuids nuevos (resolve.rs: for _ in 0..l.qty). Por tanto, «copias de una carta» es un recuento sobre cards filtrado por cuentan, no un número guardado.

Tamaño de carta y ajustes de impresión

PrintSettings (todo lo que decide cómo sale el papel) se guarda dentro del mazo como Deck.settings, serializado en decks.settings_json. Es el mismo tipo que consumen Motor de maquetación e Imagen y PDF, y el que edita el panel de exportación mediante useDeck.setAjustes (véase Stores de estado). Las copias con nombre y reutilizables de los ajustes de impresión o de los desplazamientos de fila viven aparte como presets (store::presets, tabla presets, tipos frontOffsets, backOffsets, print; interfaz en src/features/export/presets.tsx).

Campo Tipo / valor por defecto Notas
paper PaperFormat, A4 A3 A4 A5 Letter Legal Tabloid Photo4x6 Photo5x7 Photo8x10 Photo8x12 (paper.rs).
landscape false
card_size CardSize::MTG 63x88 mm Alternativas CardSize::POKER 63,5x88,9 mm (2,5x3,5 in) y CardSize::FUNDA 65x90 mm, que llena justa una funda (véase Exportación).
min_margin_mm 5.0
gap_mm 2.0 Separación en blanco entre cartas, como en fabricard; cada carta conserva su tamaño exacto de 63x88 y el sangrado no cubre la separación.
bleed_mm 0.0 El sangrado está disponible pero apagado de fábrica.
bleed_on_backs true
bleed_mode Clamp Clamp repite el píxel del borde, Mirror lo refleja.
fill_corners false (#[serde(default)]) Pinta las esquinas redondeadas transparentes o claras con el color del borde; apagado significa que las esquinas transparentes salen del color del papel.
cut_guides CutGuides, desactivadas enabled, color (#00c853), width_px 0.5, offset_mm 0, length_mm 5.
edge_darkening EdgeDarkening, desactivado mode (ContrastEdges/SolidBlack/Threshold), amount 80, width_pct 8, auto_detect, apply_scryfall/uploads/backs.
duplex Off Off, LongEdge (el reverso se refleja de izquierda a derecha y nunca se gira; un shortEdge guardado se lee como LongEdge).
interleave_dfc false
dfc_filter All HideDfc quita las cartas de doble cara del plan y del PDF.
quality Dpi600 Dpi300/600/900; dpi() da el número.
encoding JPEG, calidad 95 ImageFormat::{Jpeg,Png}, jpeg_quality 1..=100 (solo JPEG).
row_offsets vacío RowOffsets { front, back }, milímetros por fila.
split_every None Divide el PDF cada N hojas.
design_lang En en mazos nuevos; Es si falta en un mazo guardado (#[serde(default)]) Idioma en que se imprimen las cartas diseñadas (DesignLang::{Es,En}).
big_cards_back false (#[serde(default)]) Las cartas grandes (planos, Vanguard) imprimen el reverso del mazo en dúplex; si no, su reverso queda en blanco.

Invariantes que conviene conocer:

  • Decisión: RowOffsets.back se expresa en el marco de la cara delantera y se indexa por la fila delantera (lo que ves sosteniendo la hoja a contraluz desde delante). layout::plan lo aplica antes de reflejar. Las filas que faltan valen 0 (front_row, back_row).
  • Decisión: los valores por defecto (2 mm de separación, 0 de sangrado, guías apagadas, rellenar esquinas apagado, 600 dpi, JPEG 95) los comprueba los_ajustes_por_defecto_son_los_de_fabricard. Cámbialos ahí y en el test a la vez.
  • Decisión: la carta se estira hasta la casilla exacta y solo se recorta cuando la proporción difiere en más de un 15 por ciento (MAX_ESTIRADO, en imaging::frame). Es una regla de imagen, no un campo del modelo.

Serialización

  • Todas las structs y enums usan #[serde(rename_all = "camelCase")]; los enums con etiqueta interna usan tag = "kind" (CardSource, BackSpec). Los enums de unidad se serializan como cadenas en camelCase ("dpi600", "off"). Tests los_campos_van_en_camel_case y el_origen_de_una_carta_viaja_etiquetado.
  • ts-rs lo refleja: donde serde renombra un campo de forma explícita (scryfallId), el código añade #[ts(rename = ...)] y #[ts(optional)] para los campos Option que se omiten cuando son None.
  • La base de datos guarda cada carta como una sola cadena card_json y los ajustes como settings_json (véase Almacenamiento y blobs). El paquete .pbdeck incrusta el Deck entero como JSON en manifest.json (véase Formato del paquete de mazo).

Versionado y compatibilidad hacia atrás

  • DECK_SCHEMA_VERSION = 2. La versión 2 añadió DeckCard.design. Decisión: se subió la constante porque una app 1.x que abriera y volviera a guardar un mazo v2 perdería los diseños sin avisar (serde ignora el campo desconocido design y el nuevo guardado lo omite).
  • store::decks::save siempre escribe el DECK_SCHEMA_VERSION actual en decks.schema_version; store::decks::open devuelve el valor guardado. No he encontrado código que rechace un mazo por su schema_version al abrirlo desde la base de datos; solo la importación de paquetes comprueba una versión (package::VERSION, véase Formato del paquete de mazo).
  • Estrategia para campos nuevos (léela antes de añadir un campo): los datos antiguos deben seguir deserializándose, así que un campo nuevo debe ser Option<T> con #[serde(default, skip_serializing_if = "Option::is_none")] (como design, marca, image_lang, Custom.externa) o llevar #[serde(default)] (como fill_corners, design_lang, big_cards_back). skip_serializing_if además mantiene idénticas byte a byte las cartas sin cambios al volver a guardar. La gran mayoría de los campos de PrintSettings no tienen valor serde por defecto, así que añadir un campo obligatorio ahí haría que todos los mazos guardados fallaran al abrirse (ErrorCode::Db, de_json en store/decks.rs).
  • Una app antigua que abre un mazo más nuevo: los campos desconocidos se ignoran (no hay deny_unknown_fields en ningún sitio de src-tauri), así que los datos opcionales nuevos (marcas, vínculos, diseños) los pierde la app antigua en el siguiente guardado. En cambio, una variante o valor de enum nuevo que la app antigua no conoce (por ejemplo un ImageOrigin nuevo) es un error duro de deserialización. Por eso externa no se hizo variante.
  • CardDesign tiene su propia version (VERSION_DISENO = 1, diseno/modelo.rs).

Dónde se crea y se cambia el modelo

Dónde Qué
src-tauri/src/store/decks.rs create, open, save, duplicate (ids nuevos, imágenes compartidas).
src-tauri/src/scryfall/resolve.rs Construye los DeckCard a partir de una lista interpretada (hacer, copias en el orden de la lista, fichas al final).
src/stores/deck-store.ts El mazo abierto en la pantalla, el historial y el autoguardado (véase Almacenamiento y blobs).
src/bindings/Deck.ts, DeckCard.ts, PrintSettings.ts... Tipos generados.

Por dónde empezar para ampliarlo

  1. Añade el campo en model.rs con los atributos serde de arriba, añade #[ts(optional)] si procede y ejecuta pnpm bindings.
  2. Si referencia imágenes, haz que store::decks::refs_del_deck / package::blobs_del_deck lo tengan en cuenta (si no, la recolección de basura puede borrar la imagen, véase Almacenamiento y blobs).
  3. Si cambia el recuento, cambia cuentan y su test, y revisa commands/stats.rs y decklist/exportar.rs.
  4. Actualiza cada literal DeckCard { ... } que marque el compilador (tests en store/, package.rs, exportar.rs, resolve.rs).

Véase también

Documentation · Documentación

Language: English · Español

Use and build · Uso y desarrollo

Architecture · Arquitectura

Data · Datos

Getting cards · Conseguir cartas

Layout, images, PDF · Maquetación, imagen y PDF

Card designer · Diseñador de cartas

Game table · Mesa de juego

Frontend · Frontend

Reference · Referencia

Clone this wiki locally