Skip to content

Designer App Layer

Pablo Martínez López edited this page Oct 20, 2026 · 12 revisions

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.

Boundaries

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, letras_libres, lote, original, pintar, png, portapapeles, recursos, traduccion, vista. The last three downloadable-resource modules (descargables, letras_libres, original) are summarised below; the pack format they install is in Frame Packs.

File Responsibility
diseno/borrador.rs Scryfall card (+ Spanish printing) to CardData, then to a DesignDraft
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)
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" 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

Commands

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).

Drafts, collections, saving

Command Purpose
design_draft(card, usados, collection) Build a DesignDraft for a deck card. 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_export_png(...) Job: PNG export. Returns a job_id.

Preview, structure and warnings

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) traduccion::sugerir for one face.

Resources

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 }.

Artwork

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.
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.

Heavy work off the async runtime

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.

Drafts (borrador.rs)

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 --> G["montar -> DesignDraft"]
Loading
  • carta_completa(db, api, id) reads the Scryfall cache and uses it only if ScryCard::completa_para_disenar(); otherwise it fetches the card again and re-caches it (old cache entries lacked fields such as full_art).
  • impresion_es returns the Spanish printing used as the source of official Spanish text. Order in the code: the card itself if lang == "es"; otherwise the most recent Spanish printing found with oracleid:<id> lang:es that has printed text (the current wording, which fixes old templates); otherwise the same set + collector number (cache, then card_lang). With none, the draft falls back to the Oracle text. (The module header comment lists the order differently; follow the function.)
  • datos(en, es, ahora) builds CardData: a face per entry of card_faces for the layouts in por_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's printed_* fields; missing Spanish falls back to the English text. sin_texto_es is true when there is no Spanish printing or it has no printed text while the Oracle has text.
  • Gotchas handled in cara_de/sin_pestana_mdfc: some printings put the whole card name ("Dusk // Dawn") in each face's printed_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: fundida fetches the meld_result part, and con_fundida appends it as a second face and sets CardData.meld_arriba (whether this part carries the top half; decided by matching collector numbers like 15a/15b).
  • montar(card, datos, coleccion, usados) produces the initial CardDesign:
    • style: FullArt for "Basic Land" cards, else the collection's default_style, else Borderless. estilo_pack comes from the collection's estilo_pack_defecto except for basic lands.
    • One FaceDesign per face with the name pre-filled in both languages (Spanish uses printed_name_es).
    • before snapshots source/front/back/edits of the deck card so "Remove design" can restore it.
    • footer.number = proxy_core::diseno::borrador::siguiente_numero(usados, es_ficha) (0001…, tokens T001…).
    • options.show_flavor = true, status = Draft, thumb_key = None.
    • Aviso::SinTextoOficial is added when datos.sin_texto_es.

borrador::en_vivo (ignored network tests)

#[cfg(test)] mod en_vivo holds #[ignore] tests that need the network:

  • datos_de_layouts downloads 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 their CardData JSON to src-tauri/crates/proxy-core/tests/dorados/layouts/datos/. Those fixtures feed the golden layout tests of the engine. Run with cargo test --lib borrador::en_vivo::datos_de_layouts -- --ignored from src-tauri/.
  • progenitus_y_arcades_en_espanol checks the Spanish-printing search live.

Resources (recursos.rs)

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).

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 from Pack::de_fabrica() and cannot be removed or overwritten (InvalidInput { motivo: "pack_id" }).
  • instantanea(pack) returns an Instantanea { simbolos, pack, huella, generacion }. An unknown pack id silently falls back to builtin (the caller is responsible for warning; the engine emits Aviso::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 ids and feeds the render hash, so changing a font, pack or the symbol set invalidates every cached render.
  • generacion (an AtomicU64) is bumped whenever fonts change. The text/paint Motor is kept in a thread_local! (MOTOR); con_motor rebuilds it on that thread when its stored generation differs and reloads all user fonts. 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_zip strips a single wrapping folder by locating pack.json; reads only .json/.svg up to 8 MiB and .png up to 32 MiB per file, skipping paths with ..; oversize gives InvalidInput { motivo: "pack_grande" }). The pack is validated by Pack::cargar_con_imagenes first; any PackError becomes InvalidInput { motivo: "pack_invalido", ... } with the detail in params (de_pack). The id is sanitised to [A-Za-z0-9_-].
  • leer_packs re-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.
  • quitar_letra / quitar_pack delete files then bump state; cards that used them fall back to house resources (and show a warning).
  • ImagenesDelAlmacen { db, blobs, cache } implements the engine's Imagenes trait: blob hash -> Arc<RgbaImage> via BlobStore::locate, decoding with proxy_core::imaging::decode::decode. CacheImagenes keeps 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 a base built-in style and variantes. See Frame Packs.

Painting and the render hash (pintar.rs)

  • 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 and coleccion_perdida = true.
  • huella_de(state, e, cara, lang) = efectivo::huella(e, lang, cara, recursos.huella): SHA-256 over the serialised effective design, language, face, resource hash and the engine version VERSION_MOTOR. It is the identity of "what this render looks like".
  • pintar(...) takes the Instantanea, builds Recursos { pack, simbolos, imagenes } and calls the engine's render inside con_motor; any engine error is mapped to ErrorCode::Image { detalle }. pintar_capa is the same for render_capa.
  • Salida::Pantalla yields transparent rounded corners (preview); Salida::Impresion yields opaque square corners (thumbnails, PDF, PNG). A custom card corner radius (FineTuning.card_radius) applies to both outputs (componer.rs).

Live preview and pbimg:// URLs (vista.rs + protocol.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 by ANCHOS in src/lib/image-url.ts); anything else is a 400. Hashes are validated with es_huella (64 hex chars). The frontend builds URLs with urlDiseno, urlCapa, urlArte in src/lib/image-url.ts.
  • Each request takes a permit of AppState.renders (a tokio Semaphore) so many parallel <img> requests do not saturate the CPU, then renders in spawn_blocking.
  • Vistas is a FIFO map hash -> (DisenoEfectivo, cara, lang) capped at MAX = 64 entries. registrar stores it; /d/ looks the hash up. An unknown hash returns NotFound (HTTP 404). Hence the registry is the reason a pbimg://…/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
Loading

Moving the artwork without waiting for Rust: design_overlay + render_capa

Dragging the artwork would otherwise cost a full render per mouse move. Instead:

  1. vista::capa(state, design, cara, lang, ancho) resets every face's ArtFraming to default on the effective design before computing the hash. The overlay hash therefore does not depend on framing, so it stays valid while only the framing changes.
  2. It registers that design in Vistas (so /c/<hash>/<w> can paint it), and if renders/c/<hash>-<w>.png and renders/c/<hash>.json exist it returns the cached CapaInfo.
  3. Otherwise pintar_capa calls the engine's render_capa ("the card painted once without its artwork, plus where the artwork goes"). The engine function (render_capa in src-tauri/crates/proxy-core/src/diseno/componer.rs, re-exported from render.rs) returns None unless es_simple holds: split layouts (including fuse), Aftermath, Flip, Plane/Plan/Vanguard, the Battle front, and the Meld result face are painted whole on the normal path instead. It also returns None when the face has no artwork. Otherwise the app writes the PNG and the JSON.
  4. CapaInfo { huella, arte: [x, y, w, h], radio: [rx, ry], cara } is in fractions of the card: the artwork hole and the corner radii.

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.

Warnings (design_fit_report)

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.

Saving, re-thumbnailing and renumbering (vista.rs)

aplicar(state, card, design, lang):

  1. Resolve the effective design; clave = huella_de(.., cara 0, ..).
  2. Paint face 0 at ANCHO_MINIATURA = 1000 px with Salida::Impresion, encode JPEG quality 92 and store it with ImageOrigin::Designed.
  3. If the layout prints two faces (d.faces.len() >= 2 && maquetacion(&d.card).0.caras_impresas() == 2), paint face 1 too and set back = BackSpec::Dfc { image }. Otherwise keep d.before.back, except a former Scryfall DFC back becomes BackSpec::DeckDefault.
  4. Return a clone of the card with source = CardSource::Custom { scryfall_id: Some(..), externa: None }, front = thumbnail, edits = default, meta.is_dfc updated and design = 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_key records the hash that produced the thumbnail. rehacer compares 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 calls rehacerMiniaturas() when opening a deck and when the collections/resources dialogs close.
  • renumerar walks the deck in order, skips designs with footer.number_manual, assigns 0001… to cards and T001… to tokens (CardData::es_ficha), and gives copies (same scryfall_id) the same number. It re-applies only cards whose number changes.
  • quitar(card) restores source/front/back/edits from design.before. con_estado (set Ready/Draft) exists but has no callers.

Bulk drafts: "Design the deck"

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 Cancelled if the flag is set (job_cancel).
  • For each card with no design and a Scryfall id: builds the draft with borrador_de (same pipeline as design_draft plus Scryfall art_crop artwork for each printed face; for melds the second face has no art of its own), applies it with vista::aplicar in spawn_blocking, and emits design://progress (DesignProgressEvent { jobId, done, total, card?, failed }).
  • Copies of the same Scryfall card reuse the first draft (hechos map) so they share the design and number; usados grows with each new number.
  • Network and RateLimited errors abort the whole job; other per-card errors are logged, reported as failed: 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.

PNG export (png.rs)

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?).

  • tareas builds the work list. Two copies with the same design count once: the key is the design JSON with thumb_key, status and before neutralised. 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 with Salida::Impresion at exactly the cut size.
  • Layout on disk: <folder>/es/ and <folder>/en/; file name "<number> - <name>.png" (nombre_de_fichero replaces " // " with " - " and OS-illegal characters with -); multi-face cards add (1), (2); collisions get ~2, ~3.
  • Test png_a_su_medida_y_siempre_iguales pins 744×1039 px at 300 dpi for 63×88 mm and byte-identical re-exports.

How the PDF paints designed cards

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"]
Loading
  • DisenosDelDeck::de computes the effective design of every designed card (loading collections only if any card has a design) and records deck.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 has BackSpec::Dfc and the design has two faces; otherwise None (the deck's default back is not part of the design, and a DFC back with a single-face design is not repainted).
  • referencia produces a DisenoRef { huella, cara, lang } for the slot's RenderSpec.diseno, so identical designs/faces dedupe through RenderSpec::key() and render once.
  • render_diseno_para_pdf paints at the trim width in pixels for the export dpi (trim_px(spec.card, spec.target)), then render_desde_imagen adds 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/… with card.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. Inferred: no UI control in src/ writes it (the field defaults to Spanish and travels in .pbdeck); the designer's ES/EN toggle only changes the on-screen preview.

More on the PDF pipeline in Imaging and PDF and Export.

Suggested translation (traduccion.rs)

Experimental, never applied automatically. For a face with no Spanish printing (or a type line identical in both languages):

  1. 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.
  2. The card name (and its legendary short name from proxy_core::diseno::borrador::nombre_corto) is replaced by ~ so searches match other cards.
  3. For each sentence, traducir_frase searches Scryfall o:"<sentence without final period>" lang:es order:released (30 results), keeps Spanish prints with printed text, newest first, and alinear takes the sentence at the same index in the Spanish card's printed text. Alignment only counts when the Oracle and printed texts split into the same number of sentences.
  4. A bare keyword list ("Vigilance, reach") that finds nothing is translated word by word.
  5. The type line is searched as t:"<type>" lang:es; the result must have an identical English type line.
  6. Output Sugerencia { tipo?, reglas, frases: [{ en, es? }], traducidas, total }; 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, and a live veinte_cartas_sin_espanol.

Protecting images from garbage collection (blob_refs)

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 (DELETE then INSERT OR IGNORE), and removed on deck/collection delete.
  • rellenar_refs backfills the table once from already stored decks, tracked by an app_settings key blob_refs_v1 (an "is the table empty" check would be wrong, since a deck without designs leaves it empty).
  • The .pbdeck package includes the same set of blobs (src-tauri/src/package.rs uses d.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 front blobs of the card, so the regular deck references protect them.

Failure modes

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
Clipboard has no image art_from_clipboard returns null (the screen shows an info toast)

Tests that guard this layer

  • src-tauri/src/diseno/borrador.rs: dusk_dawn_en_espanol_da_dos_caras_con_secuela, sin_impresion_espanola_avisa, una_basica_va_en_paisaje.
  • 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, DisenosDelDeck, pintar_una_cara_da_una_imagen_del_ancho_pedido.
  • src-tauri/src/diseno/png.rs, arte.rs (pegar_y_recortar), 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.rs tests that designed cards get a DisenoRef per 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).

Where to start when extending

  • New artwork source: add a function in diseno/arte.rs returning ArtRef with a new ArtSource variant (in proxy-core), a command in commands/diseno.rs, register it in src-tauri/src/lib.rs, add it to src/lib/ipc.ts, then a button in PestanaArte (src/features/disenar/formulario.tsx). Remember CardDesign::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 and thumb_key will not invalidate.
  • New preview route: add to protocol::Ruta, leer_ruta (validate hashes and sizes), and atender.

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