Skip to content

Designer Composition

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

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.

Two decisions per card

For every design the engine makes two independent decisions, both in src-tauri/crates/proxy-core/src/diseno/maquetacion.rs:

  1. maquetacion(&CardData) -> (Maquetacion, bool): how the whole card is composed. The bool says "layout unknown" (falls back to Normal and the painter adds Aviso::LayoutDesconocido).
  2. 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"]
Loading

Maquetacion and maquetacion()

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 and cuerpo()

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

componer.rs: assembling cards

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

Choosing the pack layout (layout_de_cara)

For cara_simple the layout name is, in order:

  1. saga if the body is Capitulos,
  2. mazmorra if Mazmorra,
  3. ficha if the card is a token or the composition is Emblema / FichaDosCaras,
  4. alto ("taller rules box") for bodies with sections: Lealtad, Niveles, Caso, Leveler, Estacion,
  5. 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 and live artwork

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.

estructura.rs: parsing the rules text

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

cuerpos.rs: sections with badges

Model

  • 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.
  • Hecho is 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).

contenido(cuerpo, reglas, sabor, lang)

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

ajustar and maquetar

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.

Painting (Carta::pintar_contenido)

  • Banded sections get a 22/255 alpha wash of the ink colour; dividers are 110/255 ink lines. Sections with repartir share 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's insignias (fondo, borde, texto), defaulting to near-black with the frame colour border and white text; pngLealtad replaces 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 per filas_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 raises Aviso::TextoNoCabe.

Card layout coverage

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

Big cards and the page plan

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.

Gotchas

  • A new composition needs three coordinated changes: the classification in maquetacion(), a branch in componer::render (and es_simple if it is not a simple face), and a layout in the builtin pack and in any pack that should support it (otherwise PackSinLayout).
  • Because composed cards call pintar_cara several times with a cloned design, warnings are de-duplicated in render and temporary rules_override edits must not leak a ReglasEditadas warning (see the Fuse handling in partida).
  • cuerpo() is per face but maquetacion() 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.

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