Skip to content

Designer Overview

Pablo Martínez López edited this page Oct 26, 2026 · 8 revisions

Designer Overview

English · Español

The card designer (the "Diseñar" view, the 2.x card editor) turns the mechanical data of a real Magic card plus a few thematic choices (name, flavour text, artwork, frame, footer) into a freshly painted proxy card. It lives in a pure Rust engine (proxy-core::diseno), a thin app layer that feeds it, and a React view that only edits the data and paints previews.

This page defines the concepts and shows how the pieces fit. The details are split across Designer-Engine, Designer-Composition, Frame-Packs, Designer-App-Layer and Designer-UI.

What the designer is (and is not)

  • It paints cards from data: one face of one card per call, at any pixel width, in a given design language (any of the 15 DesignLang codes, see Design languages). The same call serves the on-screen preview, thumbnails, PNG export and the PDF (see Imaging-and-PDF).
  • It does not edit the mechanical text of the card. Cost, type line, rules text, power/toughness, rarity and original name are a snapshot of Scryfall data (CardData) that the form never edits. The only thing the user writes is thematic: name, flavour, artwork, frame, footer. Rules can be overridden by hand (FaceTexts.rules_override), which marks the card as "hand-edited" with an Aviso::ReglasEditadas.
  • It is deterministic. Nothing reads system fonts; only the fonts loaded into the engine are used, so a card renders identically on every machine. render is a pure function of its inputs (src-tauri/crates/proxy-core/tests/dorados.rs → dos_renders_iguales_dan_los_mismos_bytes).
  • It follows the project rule "heavy work lives in Rust": React never lays out text or computes geometry (see Architecture).

Core concepts

All of these are defined in src-tauri/crates/proxy-core/src/diseno/modelo.rs and exported to TypeScript through ts-rs (src/bindings/).

Concept Type What it holds
Card design CardDesign The whole design of one card: style, optional estilo_pack / variante_pack, optional own frame, one FaceDesign per face, before (how to restore the original card), the card snapshot, footer, options, emblem, collection id, thumb_key. Stored in DeckCard.design (optional; absent for non-designed cards).
Mechanical snapshot CardData / FaceData Scryfall's layout, keywords, rarity, frame effects, colours, colour identity, cmc, set, collector number, artist, and per face: name, mana cost, type line and rules per language (ByLang<String>), printed Spanish name (printed_name_es) and printed names in the other languages (printed_names), colours, power, toughness, loyalty, defense. Also oversized, vanguard modifiers, meld_arriba, sin_texto_es / sin_texto (languages without a printing: their text is the English Oracle text).
Thematic face FaceDesign / FaceTexts / ArtRef Per face: optional artwork (ArtRef { image, source, framing }) and texts per language (ByLang<FaceTexts>: name, flavor, rules_override) and, on cards with two printed faces, optional own style and settings (ajustes: AjustesCara: style, estilo_pack, variante_pack, frame, emblem, options).
Collection Collection A reusable group look: default style, frame, emblem, set code, rarity letter, year, artist, credit, universe, original-name placement, footer template per language, title/rules font, title squeeze limits, font weights, fine tuning. Stored in the presets table with kind = collection (see the doc comment on Collection).
Frame options FrameOptions pack (builtin or an imported pack id), color (FrameColor: Auto, Palette, Gradient, Custom), crown (OnOffAuto), rules_box (RulesBox: Auto, Dark, Light, Tint).
Fine tuning FineTuning Millimetre offsets per text, rules-box opacity, art/card corner radii, art fade at the bottom of the illustration, rules-box height, per-text size multipliers, line-spacing multipliers. Lives on the collection or on the card. Always passed through FineTuning::limitado().
Warning Aviso A typed, language-neutral problem report (text does not fit, font missing, symbols missing, pack lacks a layout…). Rust never writes prose; the UI translates by kind (see IPC-and-Error-Handling).
Effective design DisenoEfectivo The design with the collection and factory defaults resolved. This is what the painter consumes and what the render hash is made from.

Styles

DesignStyle has four built-in styles: Borderless ("despejado": art over the whole card), Extended, Classic (art window plus full frame) and FullArt (landscape basics). "Token" is not a style but a layout every style knows how to paint. A frame pack may add own styles with other ids (CardDesign.estilo_pack), which inherit from one of the four (see Frame-Packs).

The effective design: card > collection > factory

efectivo(d, coleccion) in src-tauri/crates/proxy-core/src/diseno/efectivo.rs resolves every overridable value in this order:

  1. the value set on the card (CardDesign.frame, options.*, footer.*, emblem),
  2. else the value of the card's collection (only if d.collection matches the collection's id),
  3. else the factory default (constants in modelo.rs: ESTRECHADO_DEFECTO, ESTRECHADO_BASE_DEFECTO, PIE_DEFECTO, FrameOptions::default()).

Why: changing a collection (say, its frame colour) must restyle all its cards at once, so a card stores frame: None unless the user explicitly gave it its own frame. Resolution details worth knowing:

  • Footer fields come one by one (number always from the card; rarity, set_code, artist, credit, universe, year card > collection > derived from the Scryfall data). The footer template is per language and an empty one falls back to PIE_DEFECTO.
  • original_name: Auto on the card defers to the collection, and Auto there too means Footer. The result is never Auto.
  • Title squeeze (squeeze_min, clamped to 55-75 %) and the always-applied minimum (squeeze_base, clamped between squeeze_min and 1.0).
  • Font weights from the collection are clamped to 100-900.
  • If d.collection names a collection that no longer exists, coleccion_perdida is set and the factory values are used (the app adds Aviso::ColeccionPerdida in src-tauri/src/diseno/vista.rs → avisos).

The render hash (huella)

huella(e, lang, cara, recursos) is a SHA-256 over the JSON of the effective design, the language, the face index, a string fingerprint of the resources (fonts, pack, symbols) and VERSION_MOTOR (an integer in src-tauri/crates/proxy-core/src/diseno/mod.rs).

  • Because the effective design is hashed, editing a collection changes the hash of every card that uses it, and a stale render is never served.
  • efectivo clears thumb_key and resets status to Draft in its copy, so bookkeeping that does not change pixels does not change the hash.
  • Bumping VERSION_MOTOR invalidates every cached render and every stored thumbnail (CardDesign.thumb_key mismatch triggers design_rerender).
  • Only the painted language enters the hash (solo_su_idioma): editing the French name does not re-render the Spanish card. If the design text contains an emoji, |emoji-noto is added so a change of emoji font only re-renders those cards. See Designer-Engine.
  • Gotcha: a new optional field on CardDesign must use skip_serializing_if = "Option::is_none", otherwise the JSON of every existing design changes and so does its hash. The comments on estilo_pack and variante_pack say exactly this.

Design languages

The design language is per deck (PrintSettings.design_lang: English for new decks; decks saved before the field existed read as Spanish through #[serde(default)]) and is independent of the interface language. DesignLang has 15 values with Scryfall's codes, in the order of the app's language table (src/lib/idiomas.ts): es, en, fr, de, it, pt (Brazil), ro, pl, nl, ru, uk, ja, ko, zhs, zht.

  • Every per-language value is a ByLang<T>: es and en always, the others only when present. A design that only uses Spanish and English is saved byte for byte as in 2.5.x, which keeps reading it; unknown codes from newer versions are ignored.
  • Spanish and English behave exactly as before (same hashes, same pixels). Other languages do not borrow the English thematic texts: without their own, the card shows the official printed name and rules.
  • Romanian, Polish, Dutch and Ukrainian have no Scryfall printings (DesignLang::impresas() is false): the rules are the English Oracle text and the card carries Aviso::SinTextoOficial; type lines and the engine's fixed words come from offline tables.
  • Japanese, Korean and Chinese need a downloaded font (Noto Sans JP/KR/SC/TC); until it is there the card carries Aviso::FaltanLetras.
  • Choosing a new language completes the deck's designs (fetching the printing per card) through the app; see Designer-App-Layer.

Details of the model and engine side: Designer-Engine.

How it fits in the app

flowchart LR
  subgraph UI["React: src/features/disenar"]
    Form["formulario.tsx, ajustes-finos.tsx"]
    Prev["vista-previa.tsx, arte-en-vivo.tsx"]
  end
  subgraph App["Tauri app: src-tauri/src/diseno + commands/diseno.rs"]
    Draft["borrador.rs: Scryfall to CardData"]
    Res["recursos.rs: fonts, symbols, packs, per-thread Motor"]
    Paint["pintar.rs / vista.rs: efectivo + huella + render"]
  end
  subgraph Core["proxy-core::diseno (pure)"]
    Eff["efectivo"]
    Rend["render + componer"]
    Txt["texto + pintar + simbolos"]
    PackM["pack"]
  end
  Form -- "call()" --> Draft
  Prev -- "pbimg://.../d/huella/ancho" --> Paint
  Draft --> Eff
  Res --> Rend
  Paint --> Eff --> Rend --> Txt
  Rend --> PackM
  Paint --> PDF["PDF export (Imaging-and-PDF)"]
Loading
  • Data: a designed card is an ordinary DeckCard with design: Some(CardDesign). CardDesign::blobs() lists every image the design depends on (art, "crop from card" source, own emblem, the pre-design image, a custom back) so the app can protect them from blob garbage collection (blob_refs; see Storage-and-Blobs). before stores what is needed to undo the design (BeforeDesign: source, front, back, edits).
  • Preview: the UI asks for an image by hash and width through the pbimg:// protocol (…/d/<huella>/<ancho>); the app resolves the hash to a registered effective design and renders it (src-tauri/src/diseno/vista.rs). See Live-Preview.
  • Artwork dragging does not wait for Rust: the app paints the card once without the artwork (render_capa → CapaVista; on composed cards the whole card with that part's window transparent and its rotation) and React places the artwork underneath with the same maths as pintar_arte. See Designer-UI.
  • PDF: the exporter paints each designed card from the design at the requested size and DPI instead of embedding a bitmap. See Imaging-and-PDF and Export.
  • Big cards (planes, schemes, Vanguard) get their own sheets in the PagePlan, because their size is 88.9 x 127 mm (medida_grande). See Layout-Engine and Designer-Composition.
  • Resources (Scryfall symbol SVGs, imported fonts, frame packs) live under app_data_dir (simbolos/, fuentes/, marcos/<id>/, letras-cjk/) and are loaded by src-tauri/src/diseno/recursos.rs. Their combined fingerprint (Instantanea.huella) enters the render hash. Details in Designer-App-Layer.

Module map (src-tauri/crates/proxy-core/src/diseno/)

Module Responsibility Page
modelo.rs CardDesign, CardData, Collection, FineTuning, Aviso, constants this page
efectivo.rs efectivo, DisenoEfectivo, huella this page
borrador.rs pure text helpers: name substitution in rules, effective name/rules, footer numbering Designer-Engine
letras.rs font catalogue (Letras) with no system fonts; Latin/Cyrillic and CJK fallbacks Designer-Engine
texto.rs rich text, shaping/layout with parley, fit-to-box Designer-Engine
pintar.rs glyph outlines to pixels with tiny-skia Designer-Engine
simbolos.rs mana symbol SVGs and pack-own symbols Designer-Engine
render.rs paints one face on any canvas size (pintar_cara) Designer-Engine
pack.rs frame pack format, validation, builtin/Original definitions Frame-Packs
componer.rs split, aftermath, flip, battle, meld, big cards Designer-Composition
maquetacion.rs Scryfall layout to Maquetacion / Cuerpo Designer-Composition
estructura.rs parses rules text into abilities, chapters, levels (level words and headers in every printed language), and the dungeon floor plan Designer-Composition
textos_motor.rs fixed words the engine paints per design language (level word, Fuse reminder, footer language code) Designer-Engine
tipos_idiomas.rs offline type lines in the languages other than Spanish Designer-Engine
emoji.rs colour emoji (Noto COLRv1) painting Designer-Engine
cuerpos.rs section bodies with badges, adventure columns, dungeon map Designer-Composition

Public error type: DisenoError (LetraNoValida, SimboloNoValido, Lienzo). Constants: VERSION_DISENO (1, format of CardDesign), VERSION_MOTOR (engine version), MM_POR_PT.

proxy-core stays pure (no Tauri, no network, no database), which is why the engine receives images through the Imagenes trait and symbols/fonts/packs as already-loaded values.

Where to start when extending

Goal Start at
A new field on a design modelo.rs (add with #[serde(default, skip_serializing_if…)]), then efectivo.rs if it can be inherited from a collection, then render.rs; run pnpm bindings.
A new card layout maquetacion.rs::maquetacion, componer.rs::render, the pack layouts, and a case in tests/layouts.rs. See Designer-Composition.
A new frame look a pack, not code. See Frame-Packs.
Pixels changed on purpose bump VERSION_MOTOR and refresh the golden images.

See also

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