Repository navigation
Designer Composition
English · Español
Composition is the layer of proxy-core::diseno that decides what kind of card a design is and how to build it from the single-face painter: maquetacion classifies the card, componer assembles multi-piece cards (split, aftermath, flip, battle, meld, big cards), estructura parses rules text into abilities/chapters/levels, and cuerpos lays out and paints the resulting sections with badges.
It sits on top of the face painter described in Designer-Engine and relies on pack layouts described in Frame-Packs. Concepts are in Designer-Overview.
For every design the engine makes two independent decisions, both in src-tauri/crates/proxy-core/src/diseno/maquetacion.rs:
-
maquetacion(&CardData) -> (Maquetacion, bool): how the whole card is composed. The bool says "layout unknown" (falls back toNormaland the painter addsAviso::LayoutDesconocido). -
cuerpo(&CardData, cara) -> Cuerpo: what goes inside the rules box of one face. It comes from that face's type line (a transforming card can be a saga in front and a planeswalker behind), and otherwise from the card.
Why two: Scryfall's layout is not enough. Aftermath, Fuse and Room cards all arrive as split; battles arrive as transform; planeswalkers, dungeons and Station cards arrive as normal. So the code looks at keywords, the English type line and the rules text.
flowchart TD
CD["CardData (Scryfall snapshot)"] --> M["maquetacion(): Maquetacion"]
CD --> C["cuerpo(cara): Cuerpo"]
M --> R["componer::render"]
R -->|simple face| CS["cara_simple"]
R -->|split / fuse / room| P["partida"]
R -->|aftermath| S["secuela"]
R -->|flip| F["flip"]
R -->|battle front / plane| A["apaisada"]
R -->|meld back| MD["meld_dorso"]
R -->|scheme / vanguard| G["direct, big canvas"]
CS --> PC["render::pintar_cara"]
P --> PC
S --> PC
F --> PC
A --> PC
MD --> PC
G --> PC
C --> PC
PC --> CU["cuerpos: contenido, ajustar, pintar_contenido"]
CU --> ES["estructura: parse rules text"]
Maquetacion has 27 variants. The mapping (from maquetacion()):
Scryfall layout (plus rule) |
Maquetacion |
Printed faces |
|---|---|---|
split + keyword Fuse
|
Fusionar |
1 |
split + keyword Aftermath
|
Secuela |
1 |
split + a face type contains word Room
|
Habitacion |
1 |
split (otherwise) |
Partida |
1 |
flip |
Flip |
1 |
adventure |
Aventura (omens use this too) |
1 |
transform or battle, first face type has word Battle
|
Batalla |
2 |
transform |
Transformable |
2 |
modal_dfc |
Mdfc |
2 |
meld |
Meld |
2 |
reversible_card |
Reversible |
2 |
double_faced_token with Dungeon
|
Mazmorra |
1 |
double_faced_token |
FichaDosCaras |
2 |
leveler, class, case, saga, prototype, mutate, emblem
|
Leveler, Clase, Caso, Saga, Prototipo, Mutar, Emblema
|
1 |
planar / scheme / vanguard
|
Plano / Plan / Vanguard
|
1 |
token |
Ficha |
1 |
normal + type Dungeon
|
Mazmorra |
1 |
normal + type Planeswalker
|
Planeswalker |
1 |
normal + Station keyword, or a line starting Station ( / Station.
|
Estacion |
1 |
normal + CardData::es_ficha()
|
Ficha |
1 |
normal (otherwise) |
Normal |
1 |
| anything else |
Normal + "unknown layout" flag |
1 |
Helpers on Maquetacion: caras_impresas() (2 for transform-likes, 1 otherwise), es_grande() (plane, scheme, vanguard), es_partida() (split, fuse, room). The free function caras_impresas(&CardData) caps the number at the faces actually present (the fused card of a meld designed alone is one face).
Cuerpo |
Chosen when | Content |
|---|---|---|
Lealtad |
face type has word Planeswalker
|
loyalty abilities with a shield badge |
Capitulos |
Saga |
chapters with numerals |
Niveles |
Class |
class levels |
Caso |
Case |
"To solve / Solved" |
Mazmorra |
Dungeon |
rooms joined by arrows |
Leveler |
layout leveler
|
level ranges with P/T |
Prototipo |
layout prototype
|
prototype box |
Estacion |
face 0 of a Station card | thresholds `8+ |
Texto |
default | rules and flavour |
The type-line match is by whole word (tiene_palabra), case-sensitive, on the English type line.
maquetacion::resumen(design, cara, lang) produces a Resumen { maquetacion, caras_impresas, partes: Vec<Parte> } that the editor shows ("how this face will be laid out"); each Parte has marca, cabecera, texto, fr. It re-runs the same estructura parsers, so the UI never duplicates parsing. It returns an empty partes when the text follows no pattern (or has fewer than two sections).
Entry points: render(motor, e, cara, lang, rec, card, ancho_px, salida), render_capa(...) and medida(e, card). Everything is built with render::pintar_cara on canvases of the right shape and pasted rotated (pegar with Giro::{Nada, Izquierda, Media}; rotation by 90 degrees to the left or by 180 degrees; a 1:1 pixel paste of a rotated piece uses nearest-neighbour so text is not blurred). "One engine for everything" is the stated design.
render computes medida first: medida_grande() = 88.9 x 127 mm only when the card is a big layout and CardData.oversized is true, otherwise the deck's card size. Then it dispatches:
| Case | Function | How it is built |
|---|---|---|
| Normal, tokens, transform/MDFC/battle back, reversible, saga, class, planeswalker, adventure… | cara_simple |
One pintar_cara call. Chooses the pack layout with layout_de_cara (below). Adds a face indicator for Transformable/Batalla/Meld (sun = face 0, moon = face 1) and Mdfc (front/back triangles plus the other-face strip with its type line and cost). For Aventura passes aventura = Some(1) so face 1 (the adventure) is drawn as a book column inside face 0's rules box. |
| Split, Fuse, Room | partida |
Two mini cards rendered with the normal style and rotated 90 degrees left into the two halves of the card (first half at the bottom, second at the top, so turning the card right reads left to right). Fuse reserves a 6 mm strip (franja_fusionar, pack layout fusionar, 88 x 6) on the right and removes the Fuse line from the halves by writing a temporary rules_override that is not reported as "hand-edited". |
| Aftermath | secuela |
Top half normal with pack layout secuelaArriba (63 x 51 mm, no footer); bottom half with secuelaAbajo (33 x 63 mm, no artwork) rotated left; the footer comes from pie_suelto. |
| Flip | flip |
Face 0 on the whole card with layout flip; face 1 on a transparent canvas without artwork, rotated 180 degrees; footer from pie_suelto. The pack's flip layout only draws the top half. |
| Battle front, Plane | apaisada |
Painted landscape (canvas swapped: width = card height) with layout batalla (88 x 63) or plano (127 x 88.9), then rotated left onto the portrait card. |
| Scheme, Vanguard | direct |
pintar_cara with layouts plan / vanguard (88.9 x 127), no rotation. |
| Meld back | meld_dorso |
Paints the melded card (design face 1) on a vertical canvas twice as tall as wide (fw = card height, fh = 2 x card width), keeps the top or bottom half according to CardData.meld_arriba, rotates it left. |
pie_suelto paints only the footer by cloning the design with empty rules and flavour and the pack's pie layout (63 x 88). If the pack has no pie (or fusionar) layout the piece is simply skipped. (Both exist in the builtin pack.)
For cara_simple the layout name is, in order:
-
sagaif the body isCapitulos, -
mazmorraifMazmorra, -
fichaif the card is a token or the composition isEmblema/FichaDosCaras, -
alto("taller rules box") for bodies with sections:Lealtad,Niveles,Caso,Leveler,Estacion, - none (plain style) otherwise (prototype, mutate, normal…).
render::estilo_para then resolves the name: a layout of the style (EstiloDef.layouts) modifies the style; a complete layout of the pack (PackDef.layouts) replaces it; a missing one falls back to the builtin pack's complete layout; if none exists anywhere, Aviso::PackSinLayout (except for alto, which is optional and silently falls back to the normal box). A pack without a ficha layout for a style uses the builtin pack's token style with no warning. Details in Frame-Packs.
render_capa returns None unless the face is simple (es_simple excludes split/fuse/room, aftermath, flip, plane, scheme, vanguard, the battle front and the meld back). Otherwise it paints the face with sin_arte + transparente, applies the corner mask, and returns a CapaVista { imagen, arte, radio, cara }. The UI places the artwork under this layer and moves it without asking Rust again (see Designer-UI). Complex and big cards are always painted whole.
All patterns accept the English Oracle text and the Spanish printing. Text that does not follow its pattern stays normal text (callers check "fewer than two sections" and fall back). limpiar() first removes lines that are only a marker like //Level_2// that Scryfall leaves in some printings.
| Function | Pattern | Output |
|---|---|---|
planeswalker |
line starts with +1:, -2:, 0:, +X: (sign may be + - - – −; minus is normalised to the typographic minus) |
Vec<Habilidad { coste: Option, texto }>; lines without a cost are static abilities |
saga |
line starts with Roman numerals joined by commas, then a dash (I, II — text); lines before the first chapter are the intro; lines after a chapter without numeral continue it |
Saga { intro, capitulos: Vec<Capitulo { numeros, texto }> } (also a_romano) |
clase |
a line {cost}: Level N / Nivel N starts a level; the first level has no header |
Vec<Tramo> |
caso |
lines beginning To solve / Solved / Resolver / Para resolver / Para resolverlo / Resuelto plus a dash |
Vec<Tramo> |
leveler |
LEVEL/NIVEL 2-6 or 8+; a following a/b line is that range's P/T |
Vec<Tramo { cabecera, fr, texto }> |
estacion |
`8+ | text; text that looks like P/Tbecomes the threshold'sfr` |
prototipo |
Prototype {1}{B}{B} — 3/3 (...) or the Spanish layout (cost line, Prototipo. (...), loose P/T) |
Prototipo { coste, fr, recordatorio, resto } |
mazmorra |
Room name — text (Leads to: A, B) / (Lleva a: ...)
|
Vec<Sala { nombre, texto, lleva_a }> |
filas_de_mazmorra |
relaxation: each room goes in the row of its longest path from the first (official dungeons are acyclic) |
Vec<Vec<usize>> rows of room indices |
-
Contenido(what to show):Texto { reglas, sabor },Secciones { secciones, sabor, repartir, rayas },Aventura { cabecera, tipo, izq, der, sabor }. -
Seccion { izq: Option<Insignia>, cabecera, parrafos, fr, banda }. -
Insignia:Lealtad(String),Capitulos(Vec<u32>),Nivel { etiqueta, valor },Prototipo { coste, fr }. Each has a column width (6.4 / 4.8 / 6.4 / 9.6 mm) and a minimum section height. -
Hechois the same content laid out at a font size (Texto(CajaTexto),Secciones { items: Vec<SeccionHecha>, sabor, alto, tam },Aventura { ... }).Hecho::fin_ultima_linea()reports how far right the last line reaches so the painter can keep clear of the power/toughness box (a section with its own P/T box counts as reaching the right edge).
Builds the Contenido; anything not matching returns plain Texto.
| Body | Result |
|---|---|
Lealtad |
one section per ability, badge = loyalty cost, alternating banda, repartir: true, no dividers |
Capitulos |
optional intro section, then one section per chapter with a numerals badge; repartir, rayas
|
Niveles / Caso
|
sections headed by the level / "Solved" header (bold, can contain symbols); needs at least two sections |
Leveler |
sections with a Nivel badge (word NIVEL or LEVEL by language) and the range's P/T; no repartir
|
Estacion |
like leveler without the label word |
Prototipo |
a banded section with the prototype box and reminder, plus a section for the rest |
aventura(...) |
Contenido::Aventura: left column = adventure (name and cost, italic type, rules), right = creature rules and flavour |
maquetar lays a Contenido out at one size; ajustar is the shared "shrink until it fits" loop: for Texto it delegates to texto::ajustar_reglas; for sections and adventure it binary-searches the size in 0.1 pt steps to the minimum, then tightens spacing, and finally reports overflow (sobra). render.rs calls it with the box height from the pack's dynamic box or fixed hueco. Section geometry: per-section padding 0.24 x size, left column from the badge width plus 0.8 mm gap, a 7 mm column on the right when the section has P/T, minimum height from the badge. The adventure book takes 48 % of the width.
- Banded sections get a 22/255 alpha wash of the ink colour; dividers are 110/255 ink lines. Sections with
repartirshare the leftover height equally; the flavour text is pinned to the bottom with a divider above it. - Badges (
Carta::insignia): loyalty is a pentagon-ish shield (arrow up for+, down for the minus sign, flat for 0); chapters are small hexagons stacked per numeral; levels are a pointed tag with the label above the value; prototype is a rounded rectangle with the cost symbols and a small P/T box. Colours come from the pack'sinsignias(fondo,borde,texto), defaulting to near-black with the frame colour border and white text;pngLealtadreplaces the loyalty shape with an image. - Section P/T boxes (
caja_fr_tramo) sit at the right edge. - The adventure book is a translucent page with a border, with the adventure's name/cost/type above its rules.
-
pintar_mazmorra: rooms placed perfilas_de_mazmorra(rows at 1.9 mm gaps, max 72 % box width), arrows drawn first (line plus arrowhead from the bottom of a room to the top of its target), room rectangles at 10 % ink fill, name in bold centred, rules fitted per room (floor 60 % of size). Overflow raisesAviso::TextoNoCabe.
Every layout is covered by a golden image and by real card data in src-tauri/crates/proxy-core/tests/dorados/layouts/ (images) and tests/dorados/layouts/datos/*.json (card snapshots in the app's CardData JSON format, fetched from Scryfall by the ignored test borrador::en_vivo::datos_de_layouts in src-tauri/src/diseno/borrador.rs). tests/layouts.rs → CASOS renders each in the given style and asserts that no PackSinLayout / LayoutDesconocido warning appears; multi-face cards produce -a/-b images.
| Data file | Card used | Scryfall layout | Style rendered |
|---|---|---|---|
pw-3 / pw-4
|
Liliana of the Veil / Jace, the Mind Sculptor | normal (planeswalker) | Borderless / Extended |
saga-urza / saga
|
Urza's Saga / The Eldest Reborn | saga | Borderless |
clase |
Artificer Class | class | Borderless |
caso |
Case of the Locked Hothouse | case | Classic |
partida |
Fire // Ice | split | Borderless |
fusionar |
Turn // Burn | split + Fuse | Borderless |
secuela |
Dusk // Dawn | split + Aftermath | Borderless |
habitacion |
Bottomless Pool // Locker Room | split (Room) | Borderless |
aventura / presagio
|
Bonecrusher Giant / Bloomvine Regent | adventure | Borderless / Extended |
flip |
Akki Lavarunner | flip | Borderless |
transform |
Delver of Secrets | transform | Borderless |
mdfc |
Valakut Awakening | modal_dfc | Borderless |
batalla |
Invasion of Zendikar | transform (battle) | Borderless |
meld |
Bruna, the Fading Light | meld | Borderless |
reversible |
Ajani Goldmane | reversible_card | Borderless |
ficha2 |
Angel // Demon token | double_faced_token | Borderless |
leveler |
Student of Warfare | leveler | Borderless |
prototipo |
Phyrexian Fleshgorger | prototype | Borderless |
mutar |
Gemrazer | mutate | Borderless |
estacion |
Atmospheric Greenhouse | normal + Station | Borderless |
emblema |
Ajani, Adversary of Tyrants Emblem | emblem | Borderless |
mazmorra |
Dungeon of the Mad Mage | normal (dungeon) | Borderless |
plano |
Akoum | planar (oversized) | Borderless |
plan |
All in Good Time | scheme | Borderless |
vanguard |
Ashnod | vanguard | Borderless |
Also tested: las_cartas_grandes_salen_a_su_medida renders plane, scheme and Vanguard at 889 px wide and asserts 889 x 1270 (88.9 x 127 mm), and checks that Fire // Ice classifies as Partida. The base (non-layout) cases are in tests/dorados.rs, see Designer-Engine. Unit tests in maquetacion.rs (la_tabla_de_layouts walks the classification table row by row), componer.rs, cuerpos.rs and estructura.rs cover the rest.
An ignored matrix test, balatro_matriz in tests/layouts.rs, renders every layout above with every own style and variant of the downloadable Balatro pack, in Spanish and English, and fails on any PackSinLayout, LayoutDesconocido or EstiloNoEnElPack (text that does not fit is only counted). It needs PB_BALATRO=<pack folder>.
Planes, schemes and Vanguard have a physical size of 88.9 x 127 mm (3.5 x 5 in). The designer renders them at that size only if Scryfall marks them oversized; the app then places them on dedicated sheets (big_grid, big_cells, Page.big in the PagePlan). See Layout-Engine.
- A new composition needs three coordinated changes: the classification in
maquetacion(), a branch incomponer::render(andes_simpleif it is not a simple face), and a layout in the builtin pack and in any pack that should support it (otherwisePackSinLayout). - Because composed cards call
pintar_caraseveral times with a cloned design, warnings are de-duplicated inrenderand temporaryrules_overrideedits must not leak aReglasEditadaswarning (see the Fuse handling inpartida). -
cuerpo()is per face butmaquetacion()is per card; a transform card can have two different bodies. - Spanish text structure parsing depends on printed Spanish keywords (
Nivel,Resolver,Prototipo,Lleva a). Printings with other wording fall back to plain text.
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