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, 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 |
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) |
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. |
| 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. |
| 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. |
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 --> G["montar -> DesignDraft"]
-
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_esreturns the Spanish printing used as the source of official Spanish text. Order in the code: the card itself iflang == "es"; otherwise the most recent Spanish printing found withoracleid:<id> lang:esthat 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. (The module header comment lists the order differently; follow the function.) -
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. - 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 both languages (Spanish usesprinted_name_es). -
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.
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 fromPack::de_fabrica()and cannot be removed or overwritten (InvalidInput { motivo: "pack_id" }). -
instantanea(pack)returns anInstantanea { simbolos, pack, huella, generacion }. 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. -
generacion(anAtomicU64) is bumped whenever fonts change. The text/paintMotoris kept in athread_local!(MOTOR);con_motorrebuilds 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_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. -
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 a base built-in style and variantes. 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, recursos.huella): 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 every face'sArtFramingto 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. - 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 its artwork, plus where the artwork goes"). The engine function (render_capainsrc-tauri/crates/proxy-core/src/diseno/componer.rs, re-exported fromrender.rs) returnsNoneunlesses_simpleholds: 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 returnsNonewhen the face has no artwork. Otherwise the app writes the PNG and the JSON. -
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.
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 for each printed face; 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.
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:
<folder>/es/and<folder>/en/; 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 and byte-identical re-exports.
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. Inferred: no UI control insrc/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.
Experimental, never applied automatically. For a face with no Spanish printing (or a type line identical in both languages):
-
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. - 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:es order:released(30 results), keeps Spanish prints with printed text, newest first, andalineartakes 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. - A bare keyword list ("Vigilance, reach") that finds nothing is translated word by word.
- The type line is searched as
t:"<type>" lang:es; the result must have an identical English type line. - 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.
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
|
| 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. -
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.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
- Icons · Iconos
- 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