Repository navigation
Designer Engine
English · Español
The designer engine is the part of proxy-core::diseno that turns one effective design into pixels: the data model, the effective-design resolver and render hash, text shaping with parley, glyph painting with tiny-skia, symbols, and the per-face painter in render.rs. Multi-piece cards (split, flip, meld…) are assembled on top of it and covered in Designer-Composition.
For concepts (CardDesign, Collection, card > collection > factory) read Designer-Overview first.
Crate path: src-tauri/crates/proxy-core/src/diseno/.
| Item | Where | Meaning |
|---|---|---|
efectivo(d, coleccion) -> DisenoEfectivo |
efectivo.rs |
Resolve card > collection > factory. On cards with two printed faces (caras_impresas > 1), each face with FaceDesign.ajustes also gets its own DisenoEfectivo in caras[i]: CardDesign::de_la_cara(i) puts the face's AjustesCara (style, pack style and variant, frame, emblem, options) in place of the card's and resolves it the same way, so each field still falls back to the collection and the factory. DisenoEfectivo::de_cara(i) returns it (or the card's own). render and render_capa call it right after clamping cara; huella hashes only that face's effective design (the other faces' settings are left out, so changing one does not repaint the other, and designs without own settings keep their old fingerprints: both fields are skipped when empty). The app (pintar.rs) also takes the pack from de_cara(cara).frame.pack. Composed layouts (split, aftermath, flip…) are one printed face and ignore it. |
huella(e, lang, cara, recursos) -> String |
efectivo.rs |
SHA-256 hex render key. |
Motor::de_fabrica() |
render.rs |
Per-thread typesetting state: Letras (fonts) plus a Maquetador. Loads the built-in fonts. |
Recursos { pack, simbolos, imagenes } |
render.rs |
What a render borrows: the frame Pack, the Simbolos, and an Imagenes provider. |
trait Imagenes { fn imagen(&self, blob) -> Option<Arc<RgbaImage>> } |
render.rs |
The core never reads disk; the caller supplies decoded images. SinImagenes is the empty implementation. |
render(motor, e, cara, lang, rec, card, ancho_px, salida) -> Result<Render, DisenoError> |
componer.rs (re-exported from render.rs) |
Paint one printed face. Render { imagen: RgbaImage, avisos: Vec<Aviso> }. |
render_capa(...) -> Option<CapaVista> |
componer.rs |
The card without its artwork, on transparency, plus where the artwork goes. For live artwork dragging. |
Salida::{Pantalla, Impresion} |
render.rs |
Screen: rounded transparent corners. Print: opaque, square corners (the cutter rounds them; bleed repeats the edge). |
reducida(img, lado) |
render.rs |
Downscale an artwork to at most lado px on the long side (the image the live preview moves). |
Letras::{nuevas, cargar, cargar_emoji, respaldos, cargar_cjk, tiene, tiene_escritura} |
letras.rs |
Font catalogue; cargar_emoji registers the colour emoji font as the generic emoji family; respaldos and cargar_cjk register per-script fallbacks (see below). |
textos_motor, tipos_idiomas
|
textos_motor.rs, tipos_idiomas.rs
|
Fixed words the engine paints and offline type lines, per design language (see Design languages). |
Simbolos, SimbolosPropios
|
simbolos.rs |
Scryfall symbol SVGs and pack-own symbols. |
Dependencies (src-tauri/crates/proxy-core/Cargo.toml): parley (layout/shaping), skrifa (glyph outlines), tiny-skia (rasteriser), resvg (SVG: frames and symbols), fast_image_resize (Lanczos3 and pixel resizing), image, regex, sha2.
flowchart TD
A["efectivo(): DisenoEfectivo"] --> B["componer::render: pick Maquetacion + Cuerpo"]
B --> C["pintar_cara (render.rs): one face on a Lienzo"]
C --> C1["colour choice: colores_marco + Tonos + vars"]
C1 --> C2["fit rules text: cuerpos::ajustar, dynamic box"]
C2 --> C3["fondo layers, artwork, ventana_svg"]
C3 --> C4["pack layers: capas with si conditions"]
C4 --> C5["texts: cost, title, type, label, emblem, rules, P/T, footer"]
C5 --> D["corner mask, de_pixmap: RgbaImage + avisos"]
See Designer-Overview for the type table. Engine-relevant details:
-
FineTuning::limitado()clamps everything: offsets to +-AJUSTE_MAX_MM(3 mm);rules_box_opacityto 0..1;art_radius0..20 mm;card_radius0..6 mm;art_fade0..DIFUMINADO_MAX_MM(25 mm);art_fade_extension0..EXTENSION_DIFUMINADO_MAX_MM(35 mm);rules_box_height+-ALTO_CAJA_MAX_MM(10 mm); text sizes (andcost_size, the mana symbols, which share the range) toTAMANO_TEXTO_MIN..MAX(0.5..1.6); line spacing toINTERLINEADO_MIN..MAX(0.7..1.6).efectivoalways applies it. - Title squeeze constants:
ESTRECHADO_DEFECTO0.65, rangeESTRECHADO_MIN0.55 toESTRECHADO_MAX0.75,ESTRECHADO_BASE_DEFECTO1.0. -
Avisovariants:SinTextoOficial,TextoNoCabe { hueco, sobra },TituloReducido { hueco },ReglasEditadas,FaltaNombre,LetraNoEncontrada,FaltanSimbolos,PackSinLayout,EstiloNoEnElPack,LayoutDesconocido,ColeccionPerdida,SinCinta,FaltanLetras { lang }. - Per-language values (
DesignLang,ByLang<T>,FaceData.printed_names,CardData.sin_texto) are described in Design languages. - Tests:
modelo.rsround-trips a two-face design, checks the 108-card deck stays under 2 MB of JSON (peso_de_un_deck_disenado), checks that a pre-2.0DeckCardJSON still parses without adesignkey (una_carta_1x_se_lee_igual_y_no_escribe_design), and the language compatibility tests listed below.
Not to be confused with src-tauri/src/diseno/borrador.rs (the app-side Scryfall-to-CardData builder, see Designer-App-Layer). The core one is pure text logic:
-
reglas_efectivas(d, cara, lang): the rules text that is painted. A hand-writtenrules_overridewins, but the original card name is still replaced by the thematic one. -
sustituir_nombre(texto, originales, propio, espanol): replaces the original names (full names first, longest first, then their short forms such as the part before the comma) by the thematic name. Whole words only (cambiar_palabra). It uses private-use marker characters so an already-substituted name is never replaced twice. In Spanish,quitar_articulosdrops the article before the substituted name (delbecomesde,albecomesa,el/la/los/lasremoved) because the thematic name is usually a character name, not an object. -
reglas_efectivaslooks for the original names to replace in the face name, every face name and the name printed in that language (FaceData::printed_name(lang)); the Spanish article rule only applies toDesignLang::Es. -
nombre_efectivo: thematic name of that language (FaceDesign::textos(lang)), else the printed name in that language (FaceData::printed_name(lang)), else the English name.nombre_original: all face names joined with//. -
tipo_en_espanol(en)(Spanish grammar) andtipo_traducido(en, lang)(tipos_idiomas::tipo_enfirst, which also covers Spanish, then the Spanish grammar;Nonein English or when the types are unknown) build a type line without network. A subtype nobody knows stays in English («Creature — Eldrazi» becomes «Criatura — Eldrazi»), because an almost-translated line is better than one entirely in English.arreglar_tiposonly writes a line that differs from the English one.arreglar_tipos(card, lang)fills the type of faces that still carry the English one (designs saved before the translation was known);arreglar_tipos_esis the Spanish shortcut. -
siguiente_numero/numero: footer numbering,0001...for cards andT001...for tokens, above the highest already in the deck.
Letras wraps a parley::FontContext built with CollectionOptions { shared: false, system_fonts: false }. Only fonts passed to cargar(Vec<u8>) (TTF/OTF, variable fonts included) exist. Rationale: a card must look the same on every machine, and a missing system font must never silently change layout.
- Built-in fonts (
LETRAS_DE_FABRICAinrender.rs, viainclude_bytes!fromassets/fuentes/): Spectral Regular/Italic/SemiBold/Bold and Montserrat (variable). Their licences are alongside (Spectral-LICENSE.txt,Montserrat-LICENSE.txt). - Emoji font (
LETRA_DE_EMOJIinrender.rs): Noto Color Emoji in COLRv1 (assets/fuentes/Noto-COLRv1.ttf, the2D/fonts/Noto-COLRv1.ttffile ofgooglefonts/noto-emojiat a fixed commit whose SHA-256 is noted next to the constant, about 5 MB; OFL 1.1, licence inNoto-COLRv1-LICENSE.txt).Motor::de_fabricaloads it withLetras::cargar_emoji, which sets it asGenericFamily::Emoji.parleyalready asks for that family whenever a cluster is an emoji (ZWJ sequences, skin tones, flags,U+FE0F), after the text family, so no text font needs to carry emoji and a card never shows an empty box for them. It is bundled (not downloaded) because emoji can appear in any text of any card. Caladea is inassets/fuentes/only as a test fixture (letras.rsandrecursos.rstests). -
Latin and Cyrillic fallback.
Motor::de_fabricacallsLetras::respaldos(&["Spectral", "Montserrat"]), which appends those families as fontique fallbacks for theLatnandCyrlscripts. A pack or imported font that lacks «ș», «ł» or the Cyrillic alphabet takes only those characters from the house fonts instead of painting boxes; characters the requested font has are untouched. The house fonts cover the Latin of every app language and Russian/Ukrainian Cyrillic. -
CJK fonts. Japanese, Korean and Chinese need fonts that are too big to bundle (Noto Sans JP/KR/SC/TC, downloaded by the app; see Designer-App-Layer).
Letras::cargar_cjk(datos, escritura)registers one as a fallback for its scripts both with its language (ja,ko,zh-Hans,zh-Hant) and without one, so the same ideograph gets the Japanese or the Chinese form depending on the text's locale, and a stray ideograph in a Latin-script card still has a font.tiene_escritura(e)is always true forEscritura::Latina/Cirilicaand true for a CJK script once its font is loaded; the painter raisesAviso::FaltanLetras { lang }when it is false. - Imported and downloaded fonts are loaded by the app into every thread's
Motor(Recursos::con_motorinsrc-tauri/src/diseno/recursos.rs, rebuilt when the font "generation" counter changes). Font ids are the SHA-256 of the file. See Frame-Packs for the downloadable OFL catalogue. - A collection can ask for another title/rules family (
font_title,font_rules). If it is not loaded,Carta::letra_sin_pesofalls back to the pack's font and recordsAviso::LetraNoEncontrada.
-
Parsing to
Parrafos (Vec<Trozo>,Trozo::Texto { texto, cursiva }orTrozo::Simbolo):-
reglas(texto): one paragraph per line. Text inside parentheses is italic (reminder text);{X}becomes a symbol;[[word]]marks a highlight span; the short "ability word" before—at the start of a line (at most 4 words, no{ : ( . , •) becomes italic and coloured (palabra_de_habilidad). -
sabor(texto): flavour text, all italic. -
con_simbolos(texto): symbols only (type line, footer).llano(texto): plain. - Highlight and ability-word spans use private-use markers (
RESALTE,HABILIDAD), stripped inmaquetarand painted in the pack'scoloresTextocolours if the pack has them; otherwise the markers vanish and the text is the normal colour.
-
-
Maquetador::maquetar(letras, parrafo, estilo, ancho)lays one paragraph out withparley: default family/size/weight/line height, forced lining numerals ("lnum", plus"smcp"for small caps), italics as ranges, colour spans, thenbreak_all_lines(ancho)andalign.ancho = Nonemeans a single line.- Symbols are inline boxes of side
estilo.tam * LADO_SIMBOLO(0.80), gapHUECO_SIMBOLO(0.05), droppedBAJA_SIMBOLO(0.09) below the baseline. Consecutive symbols ({2}{U}{U}:) form one box with the trailing punctuation (: , . ; )) attached as a nestedBloque(Grupo.cola), so a cost never splits across lines. -
Maquetador.locale(Option<parley::Language>) is passed toparleyas the text language: it picks Japanese versus Chinese glyph forms and locale-specific forms.componer::prepararsets it fromDesignLang::locale()only for languages other than Spanish and English; for those two it staysNone, so their layout (and pixels) are exactly what they were before more languages existed. -
Maquetador.sustituiris a list of (char, replacement) pairs set from the pack'ssustituirmap (componer::preparar), for characters a pack font lacks.
- Symbols are inline boxes of side
-
Estilo(family, weight, size px,interlinea, colour,Alinear,versalitas,resalte,habilidad) is the paint-ready style.Bloqueis a laid-out paragraph (ancho,alto,base, plusultima_linea_ancho()). -
Fitting:
-
linea_ajustada_con_base(title and type line): see Title squeeze below. -
caja_de_reglas: stacks rules paragraphs and flavour text withSeparaciones { parrafo: 0.32, sabor: 0.75 }(multiples of the font size) and records where the divider line goes between rules and flavour. -
ajustar_reglas: if the box is too tall, binary-searches the font size down in steps of 0.1 pt (paso) totam_min; at the minimum it tightens spacing (parrafo 0.12,sabor 0.45) and, if it still does not fit, returns the overflow inAjuste.sobra.
-
pintar_bloque(lienzo, bloque, x, y, pincel, simbolos) walks the parley lines. For every glyph run it fetches the outline with skrifa unhinted (works with variable fonts via normalised coordinates), accumulates one tiny_skia path per run through the Lapiz outline pen, and fills it with anti-aliasing. Details:
-
Pincel { escala_x, color }:escala_xis the horizontal squeeze factor (text is condensed by scaling x, not by switching fonts);coloroverrides the text colour (used for the drop shadow). - Synthetic italic (
synthesis().skew()) is applied as a shear in the pen; synthetic bold draws an extra stroke of0.035 * size. - Inline boxes paint their symbols through
Simbolos::pintar; in shadow mode a symbol becomes a circle of the shadow colour. Missing symbols are returned as codes (painted grey) and surface asAviso::FaltanSimbolos. -
pintar_cajapaints a rules box and the flavour divider;tiene_glifo(datos, c)checks a font for a character. - Colour glyphs (emoji): before taking an outline, each glyph is offered to
emoji::pintar_glifo, which asksskrifafor its colour layers (color_glyphs) and paints them in place with aColorPainter(Pintorinemoji.rs). It uses the same baseline, size andescala_xas the text. In shadow mode (Pincel.color) every layer takes the shadow colour.-
Brushes: solid colours from CPAL palette 0 (
0xFFFF= the text colour), and linear, radial and sweep gradients astiny_skiaLinearGradient/RadialGradient/SweepGradientshaders. Almost every Noto emoji uses gradients. A gradient tiny-skia cannot build (degenerate, negative radius) falls back to the average colour of its stops (medio). -
Composite layers:
push_layeropens an offscreenPixmapthe size of the canvas; fills go to the topmost one;pop_layerdraws it onto the layer below (or the canvas) with theBlendModematching the COLRCompositeMode(fusion). -
Clip versus brush transform: a glyph clip (
Recorte::Glifo) stores the transform active when it was pushed. Noto pushes a further transform for the gradient after the clip, so the fill paints the path with the clip's transform and gives the shader only the difference (current transform relative to the clip's). Using the current transform for the path would move the outline. -
es_emoji(c): whether the emoji font hasc(andc >= U+2190, below which any text font has it). Used by the render hash, see Render hash and emoji.
-
Brushes: solid colours from CPAL palette 0 (
Simbolos holds parsed usvg::Trees by code ("T", "2/U", "W/P"), keyed by codigo() (braces stripped, upper case). Scryfall's SVGs are downloaded and stored by the app (symbols_status command, refreshed after 30 days, CADUCA_SIMBOLOS); see Designer-App-Layer. Simbolos::nuevos() always includes two built-in footer icons from assets/iconos/: {PINCEL} (brush) and {ESTRELLA} (star). The footer text maps the characters ★ and ✎ to them.
SimbolosPropios is the pack-supplied layer (SVG or PNG per code, pixel flag for nearest-neighbour scaling). Simbolos::con_propios(Arc<SimbolosPropios>) returns a view where own symbols shadow Scryfall's; codes the pack lacks fall through to Scryfall. PNG symbols are fitted without distortion, centred in the square.
pintar_cara(motor, e, lang, rec, estilo, avisos, opciones) -> Pieza paints a face on a Lienzo (a tiny_skia::Pixmap plus the mm-to-px factors kx, ky and the canvas size in mm). Everything is specified in millimetres on a 63 x 88 card (or the style's own tamano); the canvas converts. Another deck card size (63.5 x 88.9 or 65 x 90 mm) only changes the canvas in pixels, so the 63 x 88 geometry is stretched to it (kx and ky differ by less than 1 %). The same function paints a normal card, half of a split card, the aftermath strip, a landscape plane… because Opciones (cara, cuerpo, transparente, ancho_px, alto_px, indicador, otra_cara, sin_pie, sin_arte, aventura) and the EstiloDef passed in say what to draw.
-
Canvas.
Lienzo::nuevo(w_mm, h_mm, ancho_px, alto_px). Filled with opaque black unlessopciones.transparente. -
Colour choice.
colores_marco→eleccion_marco:-
FrameColor::Palette/Gradient/Customare taken as given. -
Auto: one card colour gives its palette; two give a gradient of the two (in order); three or more givegold; none givesartifact(type contains Artifact) orcolorless. A colourless land uses its colour identity if any (so a dual land is coloured), else thelandpalette. Face colours win over card colours. - Palettes (
PaletaDef) becomeTonos { claro, medio, oscuro, tinte, caja, tinta }; a custom colour is turned into tones by lightening/darkening; a gradient mixes the two (Tonos::mezcla). A palette missing from the pack renders grey.
-
-
Rules-box mode. From
FrameOptions.rules_box(Autouses the style'scajaPorDefecto:Tinte= colour tint at 0.80 opacity,Clara= light box at 1.0;Dark= near-black 0.85;Light= cream 0.92;Tint{hex}= 0.80 for a 6-digit hex; an 8-digit#rrggbbaauses its alpha as the opacity). The fine-tuningrules_box_opacitypushes the opacity toward 1. The text ink (tintaCaja) is white or the palette ink depending on the perceived luminance of the box. -
SVG variables. A
BTreeMap<String,String>(vars) withdefs(the gradients) and the colours listed in Frame-Packs, later extended with the dynamic-box geometry. -
Rules text first. The body is laid out before anything is painted because the growing rules box moves the title bar and the artwork edge:
-
contenido_de_la_cara→ aContenido(plain text, sections with badges, or adventure columns; see Designer-Composition). -
Dynamic box (style has
caja): try the "comfortable" height (alto_comodo) letting the font shrink down to 75 % (LETRA_COMODA) unless the user fixed a rules size; if that does not fit, usealto_maxwith the full font and shrink as needed down to a 6 pt floor. If the last line would run into the power/toughness box, the bottom padding is raised and the fit redone. The resulting geometry becomes numeric variables for pack expressions (yReglas,altoReglas,yTipo) and SVG variables (yReglas,altoReglas,yTipo,yTipo04). Gotcha:yReglas/altoReglasmean the text area in hueco expressions (nums) but the whole box in SVGvars. -
Fixed box (no
caja): fit inside thereglashueco (minus the MDFC strip); the text is vertically centred (alinearVcan change that). - Overflow at the floor raises
Aviso::TextoNoCabe { hueco: "reglas", sobra }(mm).
-
-
Background layers (
EstiloDef.fondo) are painted under the artwork. Skipped whensin_arte(live-drag layer), because they would cover the artwork the UI places underneath. -
Artwork.
pintar_arte(l, img, hueco, framing):- No rotation: one step crop plus Lanczos3 resize (
reescalarwithResizeOptions::crop). Zoom >= 1; offsets in [-1, 1] move within the slack on each axis. - With rotation: scale up to cover the rotated rectangle, then draw with a bicubic rotation transform.
- No image: a neutral dark gradient (
sin_arte), unless the style hasfondolayers. The hueco may use box variables so the art reaches the type bar. - The result is returned to the caller as
Pieza.arte(fractions of the canvas) forrender_capa.
- No rotation: one step crop plus Lanczos3 resize (
-
Art window (
ventanaArte):ventana_svgpaints black outside the window with rounded corners (radius from the style, orFineTuning.art_radius). Just before it, the art fade (FineTuning.art_fadeor the style'sdifuminadoArte, plus the extension) is painted over the artwork. -
Pack layers (
Carta::capas). EachCapais skipped unless itssicondition holds for theHechos(corona/fr/cinta/emblema set here, the rest fromhechos_de). Then:-
cajaReglaslayers paint the requested box tint in their zone (tinte_caja) and nothing else; - PNG layers (
capa_png) resolve{color}/{color1}/{color2}in the path (Capa::png_para), evaluate the zone expressions with the box variables (a zone that references a variable the card lacks is skipped), and paint withpng_trozo(full stretch; withdesdeonly that piece of the PNG, on its own spot unlessensays otherwise; withmascaracut by that PNG's alpha) orpng_en_tramos(tramos: top and bottom slices keep proportions, only the middle stretches); blend (mezcla) and opacity apply throughpintura_con. The sized result is cached byimagenes_png::trozo(key: pack hash, path, piece, mask, size, scaling), so dragging the art does not redo the crop and mask; the frame colour effects (gradient, custom tint) apply to the piece the same way; - SVG layers are
rellenar-ed with the variables and rendered byresvgover the whole canvas (Lienzo::svg). A layer not found in the pack falls back to the builtin pack's files. - If the style has
ventanaArte.repintarand the user setart_radius, layers are split atrepintarTras: after the first group,repintar_ventanare-paints the artwork (or black) over the zones where the frame image drew a baked-in curve, then the remaining layers follow. The zones are clipped to start below the type bar (theyReglasvariable), because with a short dynamic box the bar sits lower than the zones' fixedy.
-
-
Texts, in this order: cost (
coste: right-aligned symbols, with a dark circle behind each unless the pack has own symbols; the title width is reduced to stay left of the cost when both are on the same row), face indicator (sun/moon/triangles for transform and MDFC, pushes the title right), title, ribbon with the original name (cinta), type line (width reduced by the label pill), label pill (etiqueta: rarity/legendary/basic land type), emblem (imagen, fitted undistorted), fullArt colour symbol, rules content (pintar_contenido) or dungeon rooms (pintar_mazmorra), Vanguard hand/life, MDFC other-face strip (franja_otra_cara), power/toughness (or loyalty, or defense;fr_texto), footer (pie). -
Warnings (missing name, hand-edited rules, missing fonts for the script, no official text in the design language) are added and the
Piezareturned.
Afterwards componer::render removes duplicate warnings, adds FaltanSimbolos (excluding the built-in icons), and applies the corner mask: FineTuning.card_radius when set (screen and print), else the style's radio on screen and square for print. de_pixmap un-premultiplies to RgbaImage.
Carta::linea(nombre, parrafo, ancho_max, squeeze) → linea_ajustada_con_base:
- Lay the text out on one line. The line is always squeezed to at least
squeeze_base(collection/card "minimum squeeze", 1.0 = none, only for thetituloandtipohuecos). - If it still does not fit, squeeze horizontally (
Pincel.escala_x) down tosqueeze_min(the limit, 55-75 %). - If even that does not fit, shrink the font size just enough, and report
Aviso::TituloReducido { hueco }.
Decision rationale: condensing the glyphs a little keeps the pack's font size and card look; shrinking is the last resort. Helper texts (texto_centrado, texto_izquierda, used by badges and the MDFC strip) use a fixed 0.6 limit.
Applied inside Carta::hueco, escala_texto, estilo_texto, pintar_cara and pie:
| Field | Effect |
|---|---|
title, cost, type_line, emblem, pt, footer (mm) |
Added to the hueco y (Carta::hueco). |
rules (mm) |
Added when the rules content is painted (the hueco also measures the box, so it is not moved in hueco). |
title_size, cost_size, type_size, rules_size, pt_size, footer_size
|
Multiply the font size of that role (escala_texto; the ribbon follows title_size). cost_size scales the side of the mana symbols in Carta::coste: they stay right-aligned and centred on the slot's height, and the title gets the room they free (fin_coste). With rules_size set the comfortable-height shrink is skipped: the box grows as needed and the font only drops if even the maximum box is too small. |
rules_line_spacing, footer_line_spacing
|
Multiply the line height / footer row pitch. |
rules_box_opacity |
See step 3. |
art_radius, card_radius
|
Corner radii (mm), see steps 8 and the corner mask. |
art_fade (mm) |
Fades the bottom edge of the artwork into black (difuminar_abajo, painted over the artwork and under the pack layers: smooth at both ends, fully black 1.5 mm before its end because the frame usually covers that edge). None uses the style's difuminadoArte (the built-in extended frame: 12 mm with 7 mm of extension); 0 turns it off. |
art_fade_extension (mm) |
How far the artwork continues below its window, fading out (the variable extensionArte, so the frame's black starts that much lower; the fade ends there). With an own art_fade the extension is the card's own (or none); without it, the card's if set, else the style's extensionDifuminado. |
rules_box_height (mm) |
Raises (positive) or lowers (negative) the top edge of the rules box; exposed to packs as the variable subeCaja. Dynamic box: alto_max and alto_comodo grow by the positive amount, the requested height is the natural one plus the offset, and with a negative value the text is refitted into the lower box (smaller font if needed, never below padding + 3 mm). Fixed frames (built-in extended and classic) move their art, type line, emblem and box edge with subeCaja. Image frames ignore it, except those that declare a caja (the Original pack, see Frame-Packs); Balatro ignores it. |
rules_box_dynamic |
None = as the frame: dynamic in the house borderless, fixed in the built-in extended and classic and in the three Original styles (CajaDinamica.fija_de_serie). Some(true): dynamic everywhere; in a fixed frame that uses subeCaja the box is shortened automatically when the text is short (recorte, at most ALTO_CAJA_MAX_MM, 10 mm) by writing a negative subeCaja, re-evaluating the reglas hueco and refitting the text (undone if the box did not really shrink). Some(false): the box always keeps alto_max (the natural-height clamp is skipped and alto_comodo is ignored) and the rules text is centred vertically in it, like in the fixed boxes (caja_creciente_fija in pintar_cara; a growing box at its natural height keeps the text at the top). Balatro does not change. |
Image-based packs (Original, Balatro) have one PNG per colour instead of a gradient SVG. For layers whose path contains {color} only:
-
ExtraMarco::Degradado(b): paint the first palette's PNG, then the second palette's PNG withfundir_a_la_derecha(alpha ramp from 35 % to 65 % of the card width). -
ExtraMarco::Tinte(color): take the neutral (colorless) PNG andtenirit, an overlay-style blend driven by luminance that keeps highlights and shadows. -
ExtraMarco::Nada: the PNG of the chosen palette. Two-colour cards inAutostay ongold(clave_marco).
render::imagenes_png keeps two process-wide LRU lists (ORIGINALES: decoded PNGs; A_MEDIDA: resized to the exact pixel size), each capped at 160 MiB of RGBA. Keys include Pack.huella, so a modified pack never reuses old pixels. Repainting the same frame while dragging artwork therefore costs only the artwork.
Carta exposes texto_centrado, texto_izquierda, simbolos_en_fila and forma (an SVG fragment in millimetres sharing the card's gradients). cuerpos.rs uses them to draw badges (Designer-Composition).
Aviso |
Raised by |
|---|---|
TextoNoCabe |
rules text overflows at the 6 pt floor |
TituloReducido |
title/type had to shrink after squeezing |
FaltaNombre |
no thematic name for the face and language |
ReglasEditadas |
rules_override present |
SinTextoOficial |
the design language has no printing (CardData::sin_texto_en(lang): Spanish via sin_texto_es, other languages via sin_texto; always for Romanian, Polish, Dutch and Ukrainian, which Scryfall does not print); rules are the English Oracle |
FaltanLetras { lang } |
the language's script has no loaded font (Letras::tiene_escritura false: Japanese, Korean or Chinese before their font is downloaded) |
LetraNoEncontrada |
collection font not loaded |
FaltanSimbolos |
symbol code with no SVG/PNG |
SinCinta |
original name in ribbon requested, style has no cinta hueco |
PackSinLayout, EstiloNoEnElPack, LayoutDesconocido
|
see Designer-Composition and Frame-Packs |
ColeccionPerdida |
added by the app, not the engine |
A design can be painted in any of 15 languages. The codes are Scryfall's and the same as the interface (src/lib/idiomas.ts), so the UI, design and deck languages share keys.
| Item | Where | Meaning |
|---|---|---|
DesignLang |
modelo.rs |
Es En Fr De It Pt Ro Pl Nl Ru Uk Ja Ko Zhs Zht. TODOS lists them in that order, which also decides the key order when saving a ByLang and must not change. codigo() / de_codigo() (Scryfall code), impresas() (false for Ro, Pl, Nl, Uk: Scryfall has no cards in them, rules come from the English Oracle), escritura() → Escritura (Latina, Cirilica, Japonesa, Coreana, ChinaSimplificada, ChinaTradicional: which fonts are needed), locale() (BCP-47, e.g. zh-Hant-TW). |
ByLang<T> |
modelo.rs |
es and en always, plus otros: BTreeMap<DesignLang, T> flattened into the same object. Saved as { "es": …, "en": …, "fr": … }: es and en first and always (2.5.x requires them and ignores the rest), the others only if present, so a design that only uses Spanish and English is saved byte for byte as before. Deserialize is hand-written and tolerant: missing es/en become defaults, unknown codes (from a newer version) are ignored. get(lang) falls back to en; get_mut(lang) creates the entry; tiene(lang) says whether it has its own value. ts-rs cannot flatten a generic map, so otros is #[ts(skip)] and the frontend reads/writes the extra keys through src/lib/por-idioma.ts. |
FaceDesign::textos(lang) |
modelo.rs |
The face texts in lang. Languages other than Spanish and English do not fall back to the English texts: without their own, the name is the printed one and the rules the official ones (the app copies a thematic name into a new language when it is chosen). reglas_a_mano(): any language has rules_override. |
FaceData.printed_names |
modelo.rs |
Printed name per code for the other languages (printed_name_es stays for Spanish, so 2.5.x keeps reading it; English is name). Read and write with printed_name(lang) / poner_printed_name(lang, nombre). Not serialised when empty. |
CardData.sin_texto |
modelo.rs |
Codes of the other languages without a printing (sin_texto_es stays for Spanish). sin_texto_en(lang), poner_sin_texto(lang, sin) (kept sorted), tiene_idioma(lang) (es/en always; otherwise known to be unprinted or every face has rules in that language). |
Engine text that is not card data:
-
textos_motor.rs:palabra_nivel(lang)(the level word of a leveler, «NIVEL», «STUFE», «等级»…),palabras_nivel()(all of them, to recognise ranges in any language),recordatorio_fusionar(lang)/palabra_fusionar(lang)/es_linea_fusionar(linea)(Fuse keyword and reminder), andcodigo_pie(lang)(the{lang}code of the footer template). Printed languages take their words from real cards on Scryfall (noted next to each value); Romanian, Polish, Dutch, Ukrainian and Korean Fuse have no official source. The footer codes were checked on real cards (FR,DE,IT,JP,KR,CS,CT,RU); Spanish staysESalthough Spanish cards printSP, because changing it would change every Spanish card already made. -
tipos_idiomas.rs: offline type lines for every design language except English (tipo_en(en, lang)). The English line always has a closed vocabulary (Scryfall's catalogs: 7 supertypes, 19 card types and about 500 subtypes), so the data is a fixed table, not a request. It has two layers. (1)assets/tipos-impresos.json, generated byscripts/tipos-impresos.mjs: for each language with printed cards, the whole type part in English → how Scryfall printed it («Legendary Artifact Creature» → «Criatura artefacto legendaria»; whole phrases, because order, gender and case change per language), and each subtype alone («Elf» → «Elfo»). It is read once (OnceLock) and compiled into the binary; the app never calls Scryfall for it. Subtypes no card carries alone («Druid») are deduced from lines with several («Elf Druid»). Rerun the script when a set adds types: it merges into the existing file and saves after each language. (2) The hand-written tables (FR,DE, …) with the separator and subtype joiner per language, each phrase noting the real card it comes from; they are the only source for Romanian, Polish, Dutch and Ukrainian (no printed cards, no official source) and for the types no foreign card shows (Token, Emblem, Dungeon, Conspiracy, Vanguard…). Order: JSON phrase, then table phrase; per subtype, table, then JSON, then the English word. Italian changes word order with several subtypes, so unknown ones go in English order (union: None). If the types themselves are unknown the result isNone(Scryfall's printing, or English, wins). Spanish has no table (ESonly gives the subtype joiner);borrador::tipo_en_espanolremains as its last resort. -
estructura.rs: level ranges (palabras_nivel), class levels and case headers («To solve», «À résoudre», «解明条件»… from Case of the Shattered Pact) are recognised in every printed language, so sections and badges work whatever the design language.
efectivo::huella hashes solo_su_idioma(e, lang): the design without the texts of languages that are not painted, so editing one language never re-renders another. For Spanish and English the reduced design is exactly what it was before more languages existed (the other languages are dropped too), so their hashes, and their cached renders, did not change. When the serialised design contains an emoji (emoji::es_emoji), |emoji-noto is added to the hash: switching the emoji font re-renders only cards with emoji, instead of every card as bumping VERSION_MOTOR would.
-
efectivo::tests::las_huellas_en_espanol_e_ingles_son_las_de_antes: fixed hashes produced by 2.5.9 for Spanish and English. -
modelo::tests::un_diseno_de_antes_se_reescribe_igual:tests/compatibilidad/diseno-2.5.9.jsonis read and written back byte-identical. -
modelo::tests::otro_idioma_se_guarda_tras_es_y_en_y_lo_lee_la_version_de_antes: a design with another language saves it afteresanden, the old two-field struct still reads it, and an unknown language key is ignored.
Directory: src-tauri/crates/proxy-core/tests/ (dorados.rs, layouts.rs, comun/mod.rs) and reference images under tests/dorados/.
-
dorados.rs→dorados_de_los_casos_base: 13 base cases (legendary creature, ribbon variant, extended legendary, two-colour wall, long title, sorcery, creature in classic, artifact in borderless and classic, extended land, full-art basic, extended and classic tokens) rendered atANCHO= 420 px with the builtin pack, built-in fonts, synthetic art and synthetic symbols (comun: hand-drawn so there are no rights issues), print output. Each image is compared totests/dorados/<name>.png; a case fails at 0.5 % of pixels differing by more than 8/255 in any channel (comun::diferencia). The test also asserts noFaltanSimboloswarning. -
dos_renders_iguales_dan_los_mismos_bytes: identical inputs give identical bytes; print output is opaque; screen output has a transparent corner pixel; poker size renders at the right aspect. -
cambiar_un_color_del_pack_cambia_el_dorado: editing a palette changesPack.huellaand the image. -
dorados_en_otros_idiomas: the same card in French, Russian and Romanian againsttests/dorados/idioma-fr.png,idioma-ru.png,idioma-ro.png, asserting noFaltanLetrasandSinTextoOficialonly for Romanian (no printing).dorado_en_japones(ignored) comparesidioma-ja.png; it needs the downloaded Japanese font:PB_CJK=<NotoSansJP[wght].ttf> cargo test -p proxy-core --test dorados -- --ignored dorado_en_japones. -
tests/layouts.rs→dorados_de_los_layouts,las_cartas_grandes_salen_a_su_medida(see Designer-Composition). - Ignored, run on demand with environment variables:
tiempos(timings),original_con_sus_imagenes(PB_ORIGINAL, folder of Card Conjurer frames; renders classic, extended and borderless),original_colores(same three styles with every frame colour and rules box),balatro_con_sus_imagenesandbalatro_contra_el_autor(PB_BALATRO,PB_AUTOR,PB_SALIDA),letras_libres_a_ojo(PB_GF),balatro_matrizinlayouts.rs. These need external assets and are for eyeballing, not CI. - Updating goldens:
PB_ACTUALIZAR_DORADOS=1 cargo test -p proxy-core --test dorados(and--test layouts) rewrites the images. Do it only for intended visual changes and review the diff; also bumpVERSION_MOTORwhen pixels change on purpose so caches refresh. - Unit tests live next to the code:
render.rs(palettes, gradients, styles, layouts, footers, tinting, sizes),componer.rs,cuerpos.rs,texto.rs,pintar.rs,pack.rs,efectivo.rs,textos_motor.rs,tipos_idiomas.rs,estructura.rs.
Also src-tauri/crates/proxy-core/examples/disenar.rs renders sample cards to disk for eyeballing, and examples/prototipo.rs is the early prototype kept as a reference (not app code).
| Failure | Behaviour |
|---|---|
| Canvas of impossible size |
DisenoError::Lienzo (pixel width is clamped to at least 16 in render; tiny-skia pixmap creation can still fail) |
| Invalid font file | DisenoError::LetraNoValida |
| Corrupt symbol | DisenoError::SimboloNoValido |
| Pack layer file missing at paint time | the layer is skipped silently (packs are validated on load, so this means a builtin-fallback case) |
| Style missing from the pack | builtin pack's style is used; then the pack's borderless; finally DisenoError::Lienzo
|
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