Repository navigation
Designer App Layer
English · Español
The Tauri-side half of the card designer ("Editor de cartas", the "Diseñar" view): everything that needs the network, the disk or the database around the pure engine in proxy_core::diseno. It builds drafts from Scryfall, owns the installed fonts/packs/symbols, registers designs for live preview, paints thumbnails for the deck, exports PNGs, and feeds designed cards into the PDF export.
The pure rendering engine (models, effective design, layout, text, painting) is documented in Designer Engine and Designer Composition; frame pack format in Frame Packs. The React side is in Designer UI. A general tour is in Designer Overview.
| Layer | Lives in | Knows about |
|---|---|---|
| Engine (pure) | src-tauri/crates/proxy-core/src/diseno/ |
CardDesign, Collection, DisenoEfectivo, render, render_capa; no Tauri, no network, no DB |
| App layer (this page) |
src-tauri/src/diseno/*.rs, src-tauri/src/commands/diseno.rs
|
Scryfall, blob store, SQLite (presets, blob_refs), disk resources, pbimg://
|
| Protocol | src-tauri/src/protocol.rs |
/d/, /c/, /b/, /x/ routes that call into diseno::vista
|
| Screen | src/features/disenar/ |
Only talks to Rust through call() in src/lib/ipc.ts
|
src-tauri/src/diseno/mod.rs declares: arte, borrador, descargables, idioma, letras_cjk, letras_libres, lote, original, pintar, png, portapapeles, recursos, traduccion, vista. The downloadable-resource modules (descargables, letras_cjk, letras_libres, original) are summarised below; the pack format they install is in Frame Packs.
| File | Responsibility |
|---|---|
diseno/borrador.rs |
Scryfall card (+ Spanish printing, + the printing in the deck's design language) to CardData, then to a DesignDraft
|
diseno/idioma.rs |
"Complete a design language": fetches the missing language for every designed card of the deck, as a cancellable job |
diseno/letras_cjk.rs |
Downloads the Japanese, Korean and Chinese fonts (Noto Sans JP/KR/SC/TC) on demand |
diseno/recursos.rs |
Recursos: symbols, fonts, packs on disk; per-thread Motor; the decoded-image cache |
diseno/pintar.rs |
Effective design, render hash (huella_de), pintar/pintar_capa, DisenosDelDeck for the PDF |
diseno/vista.rs |
Live preview registry, design_save/rerender/renumber logic, overlay layer, reduced art |
diseno/arte.rs |
The four sources of artwork (upload/blob, clipboard, Scryfall art_crop, crop of a finished card); for two-half cards, the requested half of Scryfall's double art_crop (costura_de) |
diseno/lote.rs |
"Design the whole deck": drafts in bulk as a cancellable job |
diseno/png.rs |
Export designed cards as PNG files |
diseno/traduccion.rs |
Experimental "suggested translation" into any printed language by matching identical official phrases |
diseno/portapapeles.rs |
Reads an image from the system clipboard (GTK on Linux, arboard elsewhere) |
commands/diseno.rs |
The thin Tauri command layer over all of the above |
All are registered in src-tauri/src/lib.rs and typed in src/lib/ipc.ts. Errors are AppError { code, params } (see IPC and Error Handling).
| Command | Purpose |
|---|---|
design_draft(card, usados, collection, lang?) |
Build a DesignDraft for a deck card; lang (default Spanish) is the deck's design language, whose printing is fetched too (borrador::datos_de) and whose texts are pre-filled (borrador::poner_textos). Fails with InvalidInput { motivo: "sin_vincular" } if the card has no Scryfall id (borrador::id_scryfall). usados = footer numbers already in the deck, so the next free number can be chosen. |
collection_list / collection_save / collection_delete
|
CRUD on collections, stored in presets (kind = 'collection') by src-tauri/src/store/colecciones.rs. |
design_save(card, design, lang) |
Paint the thumbnail(s), store them in the blob store and return the new DeckCard. Does not write the deck; the screen applies it. |
design_remove(card) |
Restore the card from design.before. Registered, but the screen currently uses its own quitarDiseno in src/features/disenar/grupos.ts (same logic), so this command is unused by the UI. |
design_rerender(cards, lang) |
Re-paint thumbnails whose thumb_key no longer matches the current render hash; returns only changed cards. |
design_renumber(cards, lang) |
Renumber footer numbers by deck order; returns only changed cards (already re-thumbnailed). |
design_drafts_for_deck(...) |
Job: drafts for all undesigned cards (see Bulk drafts). Returns a job_id. |
design_complete_lang(cards, lang) |
Job: complete the design language lang on the designed cards (see Completing a design language). Returns a job_id. |
design_export_png(...) |
Job: PNG export. Returns a job_id. |
| Command | Purpose |
|---|---|
design_register(design, cara, lang) |
Register the design being edited; returns the render hash used in pbimg://…/d/<hash>/<width>. |
design_overlay(design, cara, lang, ancho) |
The card without its artwork plus the artwork hole (CapaInfo), or None. Rejects widths outside protocol::ANCHOS with InvalidInput. |
design_fit_report(design, lang) |
List of Aviso (text does not fit, missing symbols or fonts, collection lost, ...). |
design_structure(design, cara, lang) |
proxy_core::diseno::maquetacion::resumen: layout kind and how the text is split (abilities, chapters, levels). Synchronous and pure. |
design_suggest_translation(design, cara, lang) |
traduccion::sugerir for one face into lang. Rejected with InvalidInput { motivo: "idioma" } for English and for the languages without printed cards (DesignLang::impresas() false: Romanian, Polish, Dutch, Ukrainian), where there is nothing to match against. |
| Command | Purpose |
|---|---|
fonts_list, font_import(path), font_remove(id)
|
User fonts (.ttf/.otf), identified by SHA-256 of the file. |
packs_list, pack_import(path), pack_remove(id)
|
Frame packs from a folder or .zip. |
fonts_free_list, font_free_download(id)
|
The OFL catalogue in src-tauri/assets/letras-libres.json (diseno/letras_libres.rs). |
original_info, original_download
|
"Original" frames (Card Conjurer layers), downloaded on demand; progress on event original://progress (diseno/original.rs). |
packs_downloadable_list, pack_downloadable_download(id), pack_extra_use(id, ruta)
|
Packs from the packs repository (Balatro), manifest src-tauri/assets/packs-descargables.json; progress on packs://progress (diseno/descargables.rs). pack_extra_use copies an extra (back, token, finished card) into the blob store and returns a FaceImage. |
symbols_status(refresh) |
Ensure Scryfall mana symbols are present (downloads if missing or older than 30 days, or if refresh), then return SimbolosInfo { cargados, fecha }. |
| Command | Source (ArtSource) |
|---|---|
art_from_blob(blob) |
Upload: a file already imported with blob_import_files (the screen imports the picked file first). |
art_from_clipboard |
Upload: image from the system clipboard, read by Rust because WebKitGTK refuses navigator.clipboard.read(); None if the clipboard has no image. |
art_from_scryfall(scryfallId, cara) |
ScryfallArtCrop { url }: the face's art_crop, falling back to the card-level one, then face 0; NotFound { motivo: "sin_arte" } if none. For a two-half card (Scryfall layout: "split": split, Fuse, Room, aftermath) whose face has no art_crop of its own, the card-level image holds both illustrations side by side, so diseno::arte::desde_scryfall cuts out the requested half (cara 0 or 1) with imaging::mitades::mitad and stores it as a PNG blob; the source is still ScryfallArtCrop { url }. costura_de picks the seam: COSTURA_SECUELA for aftermath (keyword Aftermath, or a face whose Oracle text starts with "Aftermath"), COSTURA_PARTIDA otherwise; see Imaging-and-PDF. Flip, adventure and Preparar cards are not split, so every face gets the whole image. Ignored live test mitades_en_vivo (Fire // Ice, Dusk // Dawn, Akki Lavarunner). |
art_from_card(blob, x, y, w, h) |
CardCrop { blob }: crops a fraction rectangle out of a finished card image; crops smaller than 8 px on a side fail with InvalidInput { motivo: "recorte_vacio" }. |
Every ArtRef starts with ArtFraming::default() (zoom 1, offsets 0, rotation 0). The fresh image goes to the blob store with ImageOrigin::Upload.
commands/diseno.rs → en_hilo(app, f) runs closures on spawn_blocking with the AppState, because renders are CPU heavy. Preview-path commands (design_register, design_overlay, design_fit_report, design_save, design_rerender, design_renumber, art_from_clipboard's decode step, art_from_card) go through it. Network-bound ones (design_draft, art_from_scryfall, design_suggest_translation) are plain async.
flowchart LR
A["design_draft"] --> B["carta_completa (cache or Scryfall)"]
B --> C["impresion_es"]
B --> D["fundida (meld only)"]
C --> E["datos -> CardData"]
D --> F["con_fundida"]
E --> F
F --> L["impresion(lang) + anadir_idioma (other languages only)"]
L --> G["montar -> DesignDraft"]
G --> T["poner_textos(lang)"]
-
carta_completa(db, api, id)reads the Scryfall cache and uses it only ifScryCard::completa_para_disenar(); otherwise it fetches the card again and re-caches it (old cache entries lacked fields such asfull_art). -
impresion(db, api, card, lang)returns the printing inlangused as the source of that language's official text;impresion_esis the same call for Spanish (kept because the Spanish data are always fetched, as before there were more languages). It returnsNonewithout searching for English (the Oracle is the English text) and for the four languages without printed cards. Order in the code (buscar_impresion): the card itself if itslangis already that code; otherwise the most recent printing found withoracleid:<id> lang:<code>that has printed text (the current wording, which fixes old templates); otherwise the same set + collector number (cache, thencard_lang). With none, the draft falls back to the Oracle text. Some Spanish printings carry the printed text but noprinted_type_line(Scryfall leaves it empty, e.g. the Final Fantasy Commander printings);completar_tipothen fills each missing line from another card in the same language whose Englishtype_lineis identical (traduccion::traducir_tipo), so a land is not shown as "Land". Offline, or with no such card, the line comes fromproxy_core::diseno::borrador::tipo_traducido(en, lang): Spanish usestipo_en_espanol(a single card type with Basic/Legendary and known subtypes), the other languages the whole-phrase tables ofproxy_core::diseno::tipos_idiomas(see Designer Engine); both returnNonewhen something is unknown (creatures, tokens, mixed types), in which case the English line stays. Designs saved with the English line in a translated slot are repaired byarreglar_tipos_esandarreglar_tipos(card, lang)whenvista::rehacerruns on opening a deck. Tests:una_impresion_sin_linea_de_tipo_la_toma_de_otra_carta_con_el_mismo_tipo,en_frances_se_busca_la_impresion_francesa_y_en_rumano_nada. -
datos(en, es, ahora)buildsCardData: a face per entry ofcard_facesfor the layouts inpor_caras(split,flip,transform,modal_dfc,adventure,meld,battle,double_faced_token,reversible_card) when there are at least two faces; otherwise a single face. Spanish type line, rules and name come from the Spanish printing'sprinted_*fields; missing Spanish falls back to the English text.sin_texto_esis true when there is no Spanish printing or it has no printed text while the Oracle has text. -
anadir_idioma(card_data, en, lang, printing)adds one more language (never Spanish or English, whichdatosdoes) to an existingCardData: per face the printed type line (ortipo_traducido, or English), the printed rules (without a printing the language gets no rules of its own andByLang::getfalls back to the Oracle), and the printed name (FaceData::poner_printed_name); thenCardData::poner_sin_texto(lang, …)with the same rule assin_texto_es. The fused face of a meld stays in English. Only the deck's design language is added, not all 15: each language is one more Scryfall search per card. -
datos_de(db, api, en, lang)is the whole snapshot used bydesign_draftand bulk drafts: English, Spanish, the fused meld card and, whenlangis another language, that language. -
poner_textos(design, lang)gives a design theFaceTextsof a new language (not Spanish or English) on faces that lack them: a themed name, if one was typed in English or Spanish (a name different from the official one), is copied; otherwise the printed name inlang, or the English one. Flavour text is copied from English, or from Spanish. Reason: a themed card renamed by the user keeps its name when the deck switches language; rules are never copied, so they come from the official printing. - Gotchas handled in
cara_de/sin_pestana_mdfc: some printings put the whole card name ("Dusk // Dawn") in each face'sprinted_name(ignored); Spanish MDFC printings append the other face's type tab to each face's rules (stripped, because the engine paints the tab itself). - Meld:
fundidafetches themeld_resultpart, andcon_fundidaappends it as a second face and setsCardData.meld_arriba(whether this part carries the top half; decided by matching collector numbers like15a/15b). -
montar(card, datos, coleccion, usados)produces the initialCardDesign:-
style:FullArtfor "Basic Land" cards, else the collection'sdefault_style, elseBorderless.estilo_packcomes from the collection'sestilo_pack_defectoexcept for basic lands. - One
FaceDesignper face with the name pre-filled in Spanish and English (Spanish usesprinted_name_es);design_draftthen callsponer_textosfor any other design language. -
beforesnapshotssource/front/back/editsof the deck card so "Remove design" can restore it. -
footer.number=proxy_core::diseno::borrador::siguiente_numero(usados, es_ficha)(0001…, tokensT001…). -
options.show_flavor = true,status = Draft,thumb_key = None. -
Aviso::SinTextoOficialis added whendatos.sin_texto_es.
-
#[cfg(test)] mod en_vivo holds #[ignore] tests that need the network:
-
datos_de_layoutsdownloads a fixed list of ~28 real cards (Liliana of the Veil, Urza's Saga, Fire // Ice, Bonecrusher Giant, Valakut Awakening, Invasion of Zendikar, Bruna the Fading Light, planes, a vanguard, a dungeon, an emblem, ...) and writes theirCardDataJSON tosrc-tauri/crates/proxy-core/tests/dorados/layouts/datos/. Those fixtures feed the golden layout tests of the engine. Run withcargo test --lib borrador::en_vivo::datos_de_layouts -- --ignoredfromsrc-tauri/. -
progenitus_y_arcades_en_espanolchecks the Spanish-printing search live;llanowar_elves_en_alemanchecks the German name and type line.
Recursos (held as AppState.diseno) is opened once from app_data_dir by Recursos::abrir, reading only the disk (no network).
On disk (app_data_dir) |
Content |
|---|---|
simbolos/ |
One SVG per Scryfall symbol plus indice.json ({ fecha, simbolos: code -> file }). File name = sanitised code + 8 hex chars of its SHA-256. |
fuentes/<sha256>.<ext> |
Imported fonts (ttf/otf), id = SHA-256 of the file contents. |
marcos/<id>/ |
One folder per imported or downloaded pack (pack.json, SVG and PNG layers). |
letras-cjk/<code>.ttf |
Downloaded Japanese/Korean/Chinese fonts (ja, ko, zhs, zht), plus Noto-CJK-OFL.txt. |
Internals:
- State is an
RwLock<Estado>with the symbols (Arc<Simbolos>+ a content hash), packs (BTreeMap<String, Arc<Pack>>) and fonts. The built-in pack ("builtin") comes fromPack::de_fabrica()and cannot be removed or overwritten (InvalidInput { motivo: "pack_id" }). -
instantanea(pack)returns anInstantanea { simbolos, pack, huella, generacion, huellas_cjk }. An unknown pack id silently falls back tobuiltin(the caller is responsible for warning; the engine emitsAviso::EstiloNoEnElPack/others). The pack's own symbols, if any, are layered on top of Scryfall's (Simbolos::con_propios).huella=pack.huella | symbols-hash | comma-joined font idsand feeds the render hash, so changing a font, pack or the symbol set invalidates every cached render. The CJK fonts are deliberately not inhuella:Instantanea::huella_para(lang)appends|cjk:<hash>only for the language whose font it is, so downloading the Japanese font re-renders only the Japanese cards (which were showing boxes) and leaves every other card's hash, and cached render, untouched. -
generacion(anAtomicU64) is bumped whenever fonts change. The text/paintMotoris kept in athread_local!(MOTORES, a small stack of motors per thread);con_motorpops one, drops those whose stored generation differs, builds a new one if none is left (reloading all user fonts and every downloaded CJK font,Letras::cargar_cjkwith the language'sEscritura) and pushes it back after the render. The motor is out of the cell while it paints because the engine resizes withfast_image_resizeon rayon: while a worker of the export pool waits for that resize, rayon can hand it another card of the same job, which callscon_motoragain on the same thread. Holding aRefCellborrow across the render made that aRefCell already borrowedpanic that left the export stuck; now the nested render gets a second motor, kept for later.leer_cjkloadsletras-cjk/<code>.ttfat start-up;guardar_cjkwrites a new one atomically, adds it to the state and bumpsgeneracion, so every thread's motor picks it up. Reason: the engine holds mutable font state that cannot be shared across threads cheaply. -
asegurar_simbolos(api, forzar): skips the download if the index is under 30 days old and non-empty (CADUCA_SIMBOLOS); with no network it logs a warning and keeps whatever exists (never an error). Writes are atomic (store::blobs::escribir_atomico). -
importar_pack(ruta)accepts a folder or.zip(leer_zipstrips a single wrapping folder by locatingpack.json; reads only.json/.svgup to 8 MiB and.pngup to 32 MiB per file, skipping paths with..; oversize givesInvalidInput { motivo: "pack_grande" }). The pack is validated byPack::cargar_con_imagenesfirst; anyPackErrorbecomesInvalidInput { motivo: "pack_invalido", ... }with the detail in params (de_pack). The id is sanitised to[A-Za-z0-9_-]. -
leer_packsre-applies the current definition of the "Original" frames on every load (original::definicion_al_dia) so positions/layers match this app version even if the images were downloaded by an older one. That pack is loaded withoriginal::cargar(here and inrecargar_pack): when images the new definition asks for are not on disk yet, it keeps the styles that are complete instead of failing, andoriginal::completar(spawned fromsetup) downloads the rest. See Frame Packs. -
quitar_letra/quitar_packdelete files then bump state; cards that used them fall back to house resources (and show a warning). -
ImagenesDelAlmacen { db, blobs, cache }implements the engine'sImagenestrait:blob hash -> Arc<RgbaImage>viaBlobStore::locate, decoding withproxy_core::imaging::decode::decode.CacheImageneskeeps the last 12 decoded images (MAX_CACHE), FIFO eviction, because artwork is large.
PackInfo (exposed to the screen) lists estilos and estilos_propios: Vec<EstiloPropio> — the pack's own styles (those not in proxy_core::diseno::pack::ESTILOS_DE_SERIE) with id, nombres, a base built-in style and variantes (VariantePack { id, nombres }). nombres is the pack's map language code -> name, passed through as is; the screen picks the interface language, then English, then the id (nombreEnIdioma in src/features/disenar/estilos.ts). See Frame Packs.
-
colecciones_por_id(state)loads all collections once per job;efectivo_de(design, cols)resolves a design against its collection (proxy_core::diseno::efectivo::efectivo). A deleted collection yields factory values andcoleccion_perdida = true. -
huella_de(state, e, cara, lang)=efectivo::huella(e, lang, cara, inst.huella_para(lang)): SHA-256 over the serialised effective design, language, face, resource hash and the engine versionVERSION_MOTOR. It is the identity of "what this render looks like". -
pintar(...)takes theInstantanea, buildsRecursos { pack, simbolos, imagenes }and calls the engine'srenderinsidecon_motor; any engine error is mapped toErrorCode::Image { detalle }.pintar_capais the same forrender_capa. -
Salida::Pantallayields transparent rounded corners (preview);Salida::Impresionyields opaque square corners (thumbnails, PDF, PNG). A custom card corner radius (FineTuning.card_radius) applies to both outputs (componer.rs).
Design: no image bytes cross IPC. The screen registers the design being edited, gets a hash, and sets an <img src="pbimg://…/d/<hash>/<width>">. WebKit caches by URL, and the protocol response carries Cache-Control: max-age=31536000, immutable, which is safe because the URL contains everything that changes the image.
| Route | Handler | Content |
|---|---|---|
/d/<huella>/<ancho> |
vista::imagen |
PNG of the registered design, Salida::Pantalla
|
/c/<huella>/<ancho> |
vista::imagen_capa |
PNG of the card without artwork (overlay) |
/b/<blob>/<lado> |
vista::arte_reducido |
Artwork reduced to lado px on its long side (LADOS_ARTE = [1000, 2000]) |
/x/<pack>/<n> |
descargables::extra_por_indice |
An extra image of a downloadable pack |
/r/…, /t/…, /f/…
|
other modules | Not part of the designer |
- Allowed widths are
protocol::ANCHOS = [146, 244, 488, 672, 745, 1000](mirrored byANCHOSinsrc/lib/image-url.ts); anything else is a 400. Hashes are validated withes_huella(64 hex chars). The frontend builds URLs withurlDiseno,urlCapa,urlArteinsrc/lib/image-url.ts. - Each request takes a permit of
AppState.renders(a tokioSemaphore) so many parallel<img>requests do not saturate the CPU, then renders inspawn_blocking. -
Vistasis a FIFO maphash -> (DisenoEfectivo, cara, lang)capped atMAX = 64entries.registrarstores it;/d/looks the hash up. An unknown hash returnsNotFound(HTTP 404). Hence the registry is the reason apbimg://…/d/URL only works while the design is registered in this process. - Disk cache:
app_cache_dir/renders/d/<hash>-<width>.png,renders/c/…,renders/b/…. It is a derived cache and may be deleted at any time (architecture rule: the cache holds only derivatives). It is not invalidated explicitly: new inputs mean a new hash.
sequenceDiagram
participant UI as use-borrador
participant C as design_register
participant V as Vistas (64 entries)
participant P as pbimg:// /d/
participant R as pintar + Motor
UI->>C: design, cara, lang (as fast as the last call finishes)
C->>V: meter(huella, efectivo, cara, lang)
C-->>UI: huella
UI->>P: img src = /d/huella/ancho
P->>V: get(huella)
P->>R: render Salida::Pantalla
R-->>P: PNG (cached under renders/d)
P-->>UI: bytes
Dragging the artwork would otherwise cost a full render per mouse move. Instead:
-
vista::capa(state, design, cara, lang, ancho)resets theArtFramingof the face being moved (cara) to default on the effective design before computing the hash, so the layer stays valid while only that framing changes. The other faces keep theirs: on a composed card the layer of one part has the other part's artwork painted in it, and resetting it made part 1 jump back to its initial framing while part 2 was selected. - It registers that design in
Vistas(so/c/<hash>/<w>can paint it), and ifrenders/c/<hash>-<w>.pngandrenders/c/<hash>.jsonexist it returns the cachedCapaInfo. - Otherwise
pintar_capacalls the engine'srender_capa("the card painted once without that part's artwork, plus where the artwork goes";render_capainsrc-tauri/crates/proxy-core/src/diseno/componer.rs). Composed and big cards get a layer too (the whole card with that part's art window transparent, see Designer-Composition); it returnsNoneonly when the part has no artwork of its own (part 2 of a flip or adventure), for the meld back, or when the face has no artwork. Otherwise the app writes the PNG and the JSON. The hash includes the requested part, so the two parts of a split card never share a cached layer. -
CapaInfo { huella, arte: [x, y, w, h], extension, fondo, radio: [rx, ry], cara, giro }is in fractions of the card: the artwork hole as seen on the card, the corner radii andgiro, the rotation of the artwork inside the hole in degrees (90aftermath bottom half,-90split halves;#[serde(default)], so layers cached before it read as0).
The frontend composition (see Designer UI): a black box with rounded corners, the artwork <img src=/b/<blob>/<1000|2000>> placed in the hole with CSS using the same maths as the engine's pintar_arte (geometriaArte in src/features/disenar/encuadre.ts), and the overlay <img src=/c/<hash>/<w>> on top. When dragging stops, the exact Rust render replaces it.
vista::avisos paints every face (0..faces.len()) with Salida::Impresion at width 300 and merges each render's avisos without duplicates, adding Aviso::ColeccionPerdida if the collection was deleted. It renders without caching, which is why the screen debounces it by 400 ms.
aplicar(state, card, design, lang):
- Resolve the effective design;
clave = huella_de(.., cara 0, ..). - Paint face 0 at
ANCHO_MINIATURA = 1000px withSalida::Impresion, encode JPEG quality 92 and store it withImageOrigin::Designed. - If the layout prints two faces (
d.faces.len() >= 2 && maquetacion(&d.card).0.caras_impresas() == 2), paint face 1 too and setback = BackSpec::Dfc { image }. Otherwise keepd.before.back, except a former Scryfall DFC back becomesBackSpec::DeckDefault. - Return a clone of the card with
source = CardSource::Custom { scryfall_id: Some(..), externa: None },front = thumbnail,edits = default,meta.is_dfcupdated anddesign = Some(design with thumb_key = clave).
The deck itself is not written: the screen swaps the card in with one setCartas so undo works.
- Consequence: a designed card behaves in the deck like a custom card whose image is the 1000 px thumbnail. Layout, stats and preview need no special case; only the PDF re-paints from the design.
-
thumb_keyrecords the hash that produced the thumbnail.rehacercompares it with the current hash (so a changed collection, font, pack, symbol set or engine version triggers a re-thumbnail) and only re-applies mismatches. The screen callsrehacerMiniaturas()when opening a deck and when the collections/resources dialogs close. -
renumerarwalks the deck in order, skips designs withfooter.number_manual, assigns0001…to cards andT001…to tokens (CardData::es_ficha), and gives copies (samescryfall_id) the same number. It re-applies only cards whose number changes. -
quitar(card)restoressource/front/back/editsfromdesign.before.con_estado(setReady/Draft) exists but has no callers.
design_drafts_for_deck(cards, usados, collection, lang) allocates a job (state.jobs.alta() → (job_id, cancelado)), spawns an async task holding the state.trabajo() guard (marks a long job running, which blocks e.g. blob_gc with Busy), and returns the job_id. lote::borradores then, in deck order:
- Stops with
Cancelledif the flag is set (job_cancel). - For each card with no design and a Scryfall id: builds the draft with
borrador_de(same pipeline asdesign_draftplus Scryfallart_cropartwork asked for each face index: halves of split, Fuse, Room and aftermath cards each get their own piece, while flip and adventure faces get the same image because Scryfall has a single one; for melds the second face has no art of its own), applies it withvista::aplicarinspawn_blocking, and emitsdesign://progress(DesignProgressEvent { jobId, done, total, card?, failed }). - Copies of the same Scryfall card reuse the first draft (
hechosmap) so they share the design and number;usadosgrows with each new number. -
NetworkandRateLimitederrors abort the whole job; other per-card errors are logged, reported asfailed: true, and the job continues. - At the end
design://done(DesignDoneEvent { jobId, error? }) is emitted.
The dialog src/features/disenar/lote-dialog.tsx collects all cards and applies them in one setCartas (even when cancelled), so one undo removes them all. src/lib/trabajos.ts → lanzarTrabajo listens before launching so short jobs do not lose their final event.
A design keeps Spanish and English always, plus only the languages it has been shown in (ByLang.otros, see Designer Engine). When the deck's design language changes to a third language, the cards already designed have neither that language's official data nor its texts, so design_complete_lang(cards, lang) fills them in as a job (same jobs.alta() + state.trabajo() pattern as bulk drafts):
- If the language needs a downloaded font (
letras_cjk::necesita:ja,ko,zhs,zht),letras_cjk::descargarruns first, so that the thumbnails painted afterwards do not show boxes. For Spanish and English nothing else happens. - Only designs for which
idioma::le_falta(design, lang)holds are processed:!card.tiene_idioma(lang)(no printing data and not known to lack one) or some face withouttextsin that language. -
completar: if the data are missing,borrador::carta_completa+borrador::impresion(lang)+borrador::anadir_idioma; thenborrador::poner_textos. The result is applied withvista::aplicar(new thumbnail in that language) inspawn_blocking. - Copies sharing the same design (same JSON) are completed once (
hechos). - Each card is emitted on
designlang://progress(DesignProgressEvent, the same shape as bulk drafts); the end ondesignlang://done(DesignDoneEvent).NetworkandRateLimitederrors stop the job (it can simply be launched again later; the cards keep the English fallback and the warning meanwhile); other per-card errors are logged and reported asfailed: true.job_cancelstops it withCancelled.
On screen this is completarIdioma in src/features/disenar/renumerar.ts (job type "idioma" in trabajo-store), which applies the returned cards without history, even after a cancel. ponerIdiomaDiseno (the design-language selectors) and the PNG dialog call it. Test: falta_mientras_no_tenga_datos_ni_textos_del_idioma.
The house fonts cover Latin and Cyrillic, but not CJK. Noto Sans JP, KR, SC and TC (OFL) weigh between about 9.6 and 17.8 MB each, so they are not bundled in the installer: they are downloaded the first time a deck chooses one of those design languages (or from the "missing fonts" warning, which re-selects the language).
- Catalogue
src-tauri/assets/letras-cjk.json: a fixed commit of thegoogle/fontsrepository, baseofl/, and per language its family, path (notosansjp/NotoSansJP[wght].ttf…), SHA-256 and size, plus the licence file and its SHA-256. Only the Sans: the Serif would double the size. -
descargar(state, lang)does nothing for a language without an entry or whose font is already on disk; otherwise it downloads fromraw.githubusercontent.com(brackets escaped in the URL), reading in chunks with a 32 MiB cap (TOPE), and rejects anything whose SHA-256 is not the catalogue's (Network { motivo: "huella" }). The licence goes toletras-cjk/Noto-CJK-OFL.txtand the font toletras-cjk/<code>.ttfviaRecursos::guardar_cjk(underapp_data_dir, see Storage and Blobs). - The engine registers each font as a fallback for its script and locale, so the same ideograph gets the Japanese or the Chinese form depending on the card's language (see Designer Engine). Without the font the render carries
Aviso::FaltanLetras { lang }. - Tests:
el_catalogo_trae_los_cuatro_idiomas_de_asia; ignored live testbaja_la_japonesa.
design_export_png(cards, folder, card_size, dpi, langs) accepts only 300, 600 or 900 dpi and a non-empty langs (InvalidInput { motivo: "png" }), then runs in spawn_blocking as a job emitting designpng://progress and designpng://done (files, error?).
-
tareasbuilds the work list. Two copies with the same design count once: the key is the design JSON withthumb_key,statusandbeforeneutralised. For each language and each printed face (caras_impresas) one task. - Size:
componer::medida(&effective, card_size)gives millimetres; width px =mm / 25.4 * dpi. Painted withSalida::Impresionat exactly the cut size. - Layout on disk: one folder per language, named by its code (
<folder>/es/,<folder>/en/,<folder>/fr/…,carpeta); file name"<number> - <name>.png"(nombre_de_ficheroreplaces" // "with" - "and OS-illegal characters with-); multi-face cards add(1),(2); collisions get~2,~3. - Test
png_a_su_medida_y_siempre_igualespins 744×1039 px at 300 dpi for 63×88 mm, byte-identical re-exports and a third language in its own folder. - Rust paints whatever data the design has: a language that was never completed falls back to English. That is why the PNG dialog (
src/features/disenar/png-dialog.tsx) first runscompletarIdiomafor each chosen language other than Spanish and English, and only then launches the export.
flowchart TD
X["commands/export.rs exportar"] --> D["DisenosDelDeck::de(state, deck)"]
D --> E["encargo(): per slot RenderSpec"]
E -->|"referencia() returns DisenoRef"| S["spec.diseno = Some; edits default; edge None"]
S --> R["render_diseno_para_pdf"]
R --> P["pintar(Salida::Impresion, width = trim px)"]
P --> B["render_desde_imagen: bleed + encode"]
-
DisenosDelDeck::decomputes the effective design of every designed card (loading collections only if any card has a design) and recordsdeck.settings.design_lang. -
cara_de_casilla(card, slot)says which face goes on a slot:Front-> face 0;DfcFace/Back-> face 1 only if the card hasBackSpec::Dfcand the design has two faces; otherwiseNone(the deck's default back is not part of the design, and a DFC back with a single-face design is not repainted). -
referenciaproduces aDisenoRef { huella, cara, lang }for the slot'sRenderSpec.diseno, so identical designs/faces dedupe throughRenderSpec::key()and render once. -
render_diseno_para_pdfpaints at the trim width in pixels for the export dpi (trim_px(spec.card, spec.target)), thenrender_desde_imagenadds bleed and encodes. The deck thumbnail is not used for printing; the 1000 px JPEG only serves screens. - Designed cards bypass user retouches and edge darkening (
spec.edits = default,spec.edge = None). The deck preview, however, requests the thumbnail through/r/…withcard.edits(src/features/preview/slot-view.tsx,spec_de_miniatura), so a retouch made with "Edit card" (Card Editor Dialog) on a designed card shows on screen but never reaches the PDF. - The language used is the deck's
settings.design_lang(English for new decks, Spanish for decks saved without it; it travels in.pbdeck). It is written by the "Language of designed cards" selector of the export panel and by the language selector of the Diseñar preview, both throughponerIdiomaDiseno(src/features/disenar/renumerar.ts), which completes the language and re-thumbnails the deck.
More on the PDF pipeline in Imaging and PDF and Export.
Experimental, never applied automatically. For a face with no printing in the requested language (any language with printed cards; the code is passed as codigo):
-
frases(texto)splits the Oracle text per line into sentences at a period followed by a space, outside parentheses and quotes; a whole reminder text in parentheses is one sentence. Japanese and Chinese end sentences with 「。」 (and use full-width parentheses), so that character also ends a sentence, and when the result is reassembled the sentences are joined without a space inja,zhsandzht. - The card name (and its legendary short name from
proxy_core::diseno::borrador::nombre_corto) is replaced by~so searches match other cards. - For each sentence,
traducir_frasesearches Scryfallo:"<sentence without final period>" lang:<code> order:released(30 results), keeps prints in that language with printed text, newest first, andalineartakes the sentence at the same index in that card's printed text. Alignment only counts when the Oracle and printed texts split into the same number of sentences. - A bare keyword list ("Vigilance, reach") that finds nothing is translated word by word.
- The type line is searched as
t:"<type>" lang:<code>; the result must have an identical English type line. - Output
Sugerencia { tipo?, reglas, frases: [{ en, traducida? }], traducidas, total }(FraseSugerida.traducidais the sentence in the requested language); untranslated sentences stay in English and are flagged in the UI. 4xx responses from Scryfall's query parser are treated as "not found" for that sentence.
Cost: one search per distinct sentence (memoised by hechas), so it is slow. Tests: frases_fuera_de_parentesis_y_comillas, listas_de_palabras_clave, el_nombre_va_como_tilde, alinear_por_posicion, en_frances_se_busca_en_frances, el_japones_acaba_las_frases_en_su_punto, and a live veinte_cartas_sin_espanol.
The blob store deletes orphaned images in BlobStore::gc (command blob_gc, refused with Busy while a long job runs). A blob is orphaned if it is older than 24 h (GRACIA_MS) and not referenced by any deck_cards.front_blob/back_blob, decks.default_back_blob, the generic-back setting, or any row of blob_refs. See Storage and Blobs.
Designs reference images that are neither a card's front nor back, so the table blob_refs (owner_kind, owner_id, blob) (migration src-tauri/src/store/migrations/0002_blob_refs.sql) records them:
owner_kind |
owner_id |
Written by | Blobs |
|---|---|---|---|
deck |
deck id |
store/decks.rs on every deck save (refs_del_deck -> escribir_refs) |
CardDesign::blobs() of every designed card: each face's artwork image, the source blob of a CardCrop art, an own emblem, and before.front / before.back images |
preset |
collection id |
store/colecciones.rs save
|
the collection's emblem |
Details and invariants:
- Deck save rejects the whole save with
InvalidInput { motivo: "blob_missing", blob }if any referenced blob does not exist; collection save does the same for its emblem. - Refs are rewritten from scratch inside the save transaction (
DELETEthenINSERT OR IGNORE), and removed on deck/collection delete. -
rellenar_refsbackfills the table once from already stored decks, tracked by anapp_settingskeyblob_refs_v1(an "is the table empty" check would be wrong, since a deck without designs leaves it empty). - The
.pbdeckpackage includes the same set of blobs (src-tauri/src/package.rsusesd.blobs()), see Deck Package Format. - New artwork has a 24 h grace period (imported before the card is saved), so drafts survive being edited for a while without a deck referencing them.
- The designer's thumbnails are stored as normal
frontblobs of the card, so the regular deck references protect them.
| Situation | Result |
|---|---|
| Card not linked to Scryfall |
InvalidInput { motivo: "sin_vincular" } from design_draft
|
| No network while building a draft |
Network; bulk job aborts, single card shows an error toast |
Unknown render hash on /d/ or /c/
|
NotFound -> HTTP 404 (the design was evicted from the 64-entry registry or never registered) |
| Width/side not in the allowed lists |
InvalidInput (command) or HTTP 400 (protocol) |
| Engine render error | Image { detalle } |
| Invalid pack |
InvalidInput { motivo: "pack_invalido" } + detail params |
Font not .ttf/.otf or unparsable |
Unsupported |
| Symbols missing and offline | No error; render shows Aviso::FaltanSimbolos
|
| CJK design language without its font | Render shows Aviso::FaltanLetras { lang } (boxes instead of text); choosing the language again downloads it |
| CJK font download corrupted or too large |
Network { motivo: "huella" } / Network { motivo: "tamano" }; nothing is written |
| Offline while completing a design language |
Network; the job stops, the cards keep the English fallback and Aviso::SinTextoOficial
|
| Suggested translation for English or an unprinted language | InvalidInput { motivo: "idioma" } |
| Clipboard has no image |
art_from_clipboard returns null (the screen shows an info toast) |
-
src-tauri/src/diseno/borrador.rs:dusk_dawn_en_espanol_da_dos_caras_con_secuela,sin_impresion_espanola_avisa,una_basica_va_en_paisaje,una_impresion_sin_linea_de_tipo_la_toma_de_otra_carta_con_el_mismo_tipo,en_frances_se_busca_la_impresion_francesa_y_en_rumano_nada. -
src-tauri/src/diseno/idioma.rs:falta_mientras_no_tenga_datos_ni_textos_del_idioma;src-tauri/src/diseno/letras_cjk.rs:el_catalogo_trae_los_cuatro_idiomas_de_asia. -
src-tauri/src/diseno/vista.rs:vista_previa_guardar_y_quitar,capa_para_mover_la_ilustracion,numeros_de_un_deck_y_renumerar. -
src-tauri/src/diseno/pintar.rs: slot-to-face mapping, effective design lookup, hash sensitivity (la_huella_cambia_con_la_cara_y_el_idioma),DisenosDelDeck,pintar_una_cara_da_una_imagen_del_ancho_pedido. -
src-tauri/src/diseno/png.rs,arte.rs(pegar_y_recortar; ignoredmitades_en_vivo),recursos.rs(letras_y_packs_importados),traduccion.rs. -
src-tauri/src/protocol.rs:la_ruta_buena_se_entiende,las_rutas_raras_se_rechazan, and others. -
src-tauri/src/commands/export.rstests that designed cards get aDisenoRefper face in the export plan. -
src-tauri/src/store/colecciones.rs: rename keeps id, empty name or missing emblem are rejected.
Run with pnpm test:rust (see Testing and CI).
- New artwork source: add a function in
diseno/arte.rsreturningArtRefwith a newArtSourcevariant (inproxy-core), a command incommands/diseno.rs, register it insrc-tauri/src/lib.rs, add it tosrc/lib/ipc.ts, then a button inPestanaArte(src/features/disenar/formulario.tsx). RememberCardDesign::blobs()must list any new blob so it is protected. - Anything that changes how a card looks: it must be part of the effective design or of
Instantanea.huella, otherwise caches andthumb_keywill not invalidate. - New preview route: add to
protocol::Ruta,leer_ruta(validate hashes and sizes), andatender.
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