Repository navigation
Motor de maquetación
English · Español
El motor de maquetación decide dónde va cada carta en cada cara de cada hoja, en milímetros. Su salida, PagePlan, es la única geometría del producto: la vista previa la pinta y el escritor de PDF la dibuja.
Código fuente: src-tauri/crates/proxy-core/src/layout/ (grid.rs, plan.rs, arrange.rs), src-tauri/crates/proxy-core/src/paper.rs, src-tauri/crates/proxy-core/src/units.rs, el envoltorio IPC src-tauri/src/commands/layout.rs y la caché del lado de la pantalla src/stores/plan-store.ts.
Decisión: proxy_core::layout::plan es el único sitio que calcula posiciones. La vista previa (src/features/preview/) y el PDF (src-tauri/crates/proxy-core/src/pdf/compose.rs) nunca calculan una coordenada por su cuenta.
Motivo: si la pantalla y el PDF derivaran cada uno sus posiciones, acabarían desviándose (redondeos, espejo del duplex, recorte del sangrado, desplazamientos de fila). Por eso todo lo que debe coincidir entre «lo que veo» y «lo que se imprime» se decide una sola vez, en Rust, como datos.
flowchart LR
D["Deck.settings + Deck.cards"] --> P["layout::plan(deck)"]
P --> PP["PagePlan (mm)"]
PP -->|"compute_page_plan"| PS["plan-store.ts"]
PS --> PV["PageView / SlotView (x mm * scale)"]
PP -->|"plan(deck) otra vez al exportar"| C["pdf::compose (mm a pt)"]
C --> PDF["PDF"]
Consecuencias:
- La pantalla solo multiplica milímetros por una escala (
escala = zoom * PX_POR_MM, ensrc/features/preview/preview-pane.tsx). No hay código de maquetación en TypeScript. - El PDF convierte milímetros a puntos PDF con
units::mm_to_pt(1 pt = 1/72 de pulgada);units.rses el único sitio que convierte unidades. - Un test de
compose.rs(cada_imagen_cae_donde_dice_el_plan) vuelve a leer el flujo de contenido del PDF escrito y comprueba que cada imagen cae donde dice el plan, en simplex, en duplex y en duplex en horizontal.
| Elemento | Fichero | Notas |
|---|---|---|
MM_POR_PULGADA, mm_to_pt, mm_to_px, css_px_to_pt
|
units.rs |
Todo en el core son milímetros f64. mm_to_px redondea al píxel más cercano. css_px_to_pt (1 px = 0,75 pt) se usa para el grosor del trazo de las guías de corte, que la interfaz expresa en px CSS. |
PaperFormat |
paper.rs |
A3, A4, A5, Letter, Legal, Tabloid, Photo4x6, Photo5x7, Photo8x10, Photo8x12. size_mm() es siempre vertical (ancho < alto); oriented_mm(landscape) los intercambia. PaperFormat::TODOS es el orden que ofrece la interfaz. |
Todos los tipos derivan TS y se exportan a src/bindings/ (véase IPC y manejo de errores).
| Tipo | Campos (camelCase en el cable) | Significado |
|---|---|---|
RectMm (grid.rs) |
x, y, w, h |
Rectángulo en mm, con origen arriba a la izquierda. |
Grid (grid.rs) |
cols, rows, originX, originY, marginMm |
Cartas por página y esquina superior izquierda de la primera carta. marginMm es el margen efectivo usado para contar las cartas. capacity() = cols x rows. |
Slot |
index, row, col, cardId, face, image, applyEdits, trim, bleed, bleedFullMm |
Una celda ocupada en una página. |
SlotFace |
front, dfcFace, back
|
Qué cara de la carta muestra la celda: el anverso, la segunda cara de una carta de doble cara colocada como carta propia, o el reverso en una hoja de reversos. |
Page |
index, sheet, side, widthMm, heightMm, slots, guides, big |
Una página del PDF. index es el número de página del PDF, sheet la hoja física (el anverso y el reverso de una hoja la comparten). side es SheetSide::Front o Back. |
LineMm |
x1, y1, x2, y2 |
Un segmento de guía de corte. |
PlanWarning |
etiquetado por kind: dfcWithoutBack{count}, missingDefaultBack{count}, bigCardsDoNotFit{count}
|
Cosas que conviene avisar al usuario. |
PagePlan |
grid, cells, pages, sheets, placed, warnings, bigGrid, bigCells |
El resultado completo. |
Detalles de los campos de Slot:
-
indexes la posición entre las celdas de anverso de la página (fila a fila, de izquierda a derecha).rowes siempre la fila del ANVERSO, también en las hojas de reversos, para poder emparejar los desplazamientos de fila y las miniaturas de los reversos con el anverso. -
imageesNonepara una casilla en blanco, una imagen pendiente o un reverso que falta. -
trimes el rectángulo de corte.bleedes hasta dónde se pinta realmente la imagen (el corte más el sangrado recortado).bleedFullMmes el sangrado completo que lleva la imagen renderizada en cada lado (la imagen siempre se renderiza con el sangrado completo en los cuatro lados; el plan solo dice cuánto sobrevive, véase Imagen y PDF). -
applyEditses verdadero solo en las caras de anverso. La «otra cara» de una carta de doble cara y los reversos nunca reciben los retoques de la carta (edits).
PagePlan.cells guarda los cortes sin desplazar de todas las celdas de anverso. La vista previa lo usa para dibujar las celdas vacías de la última página. bigGrid/bigCells es lo mismo para las hojas de cartas grandes.
Punto de entrada: plan(deck: &Deck) -> Result<PagePlan, LayoutError>.
-
Papel y tamaño de carta.
(ancho, alto) = settings.paper.oriented_mm(settings.landscape); el tamaño de carta essettings.cardSize(63 x 88 mm por defecto; 63,5 x 88,9 y 65 x 90 mm como alternativas; los tres son constantes deCardSizeenmodel.rs, véase Exportación). -
Rejilla (
grid::grid). Margen efectivom = max(minMarginMm, bleedMm), de modo que el sangrado exterior siempre quepa en el papel. Cartas por eje:floor((paper - 2m + gap) / (card + gap) + EPS).EPS = 1e-9evita perder una columna por error de coma flotante (el comentario degrid.rscita 215.9 - 2*5 + 2 dividido entre 65). La rejilla se centra en la página. Cero columnas o filas daLayoutError::CardDoesNotFit; un tamaño de carta no positivo o una separación, margen o sangrado negativos danLayoutError::InvalidSettings. A4 vertical con los valores por defecto es de 3 x 3 con origen (8.5, 14.5), y sigue siendo de 3 x 3 con cartas de 65 x 90 mm; A4 horizontal es de 4 x 2 (tests engrid.rs). US Letter vertical con cartas de 65 x 90 mm y los valores por defecto es solo de 3 x 2 ((279.4 - 10 + 2) / 92 = 2.95); tres filas pidenminMarginMm + gapMm <= 4.7. -
Elementos (
items). Construye la secuencia de celdas de anverso a partir dedeck.cardssiguiendo las reglas de cartas de doble cara de más abajo. También recoge los avisos. -
Separación en cartas normales y grandes (
es_grande), ypaginarcada grupo en hojas degrid.capacity()celdas. - Cartas grandes (véase más abajo) tienen su propia rejilla calculada con el mismo papel, margen, separación y sangrado.
paginar por hoja: para cada elemento coloca la casilla de anverso en su celda de la rejilla más el desplazamiento de anverso de la fila, luego (si hay duplex) la casilla de reverso, después construye las guías, y por último añade una página Front y, en duplex, una página Back justo detrás (secuencia F1, B1, F2, B2...).
De la cabecera de plan.rs:
| duplex | interleaveDfc |
qué produce una carta de doble cara | reverso de una carta de una sola cara |
|---|---|---|---|
| apagado | apagado | solo la cara 0 (aviso dfcWithoutBack) |
ninguno |
| apagado | encendido | la cara 0 y la cara 1 en celdas consecutivas | ninguno |
| encendido | apagado | la cara 0 en el anverso, la cara 1 en su reverso | reverso por defecto del mazo |
| encendido | encendido | celdas consecutivas; ambas con el reverso del mazo | reverso por defecto del mazo |
DfcFilter::HideDfc (settings.dfcFilter == "hideDfc") quita del plan (y por tanto del PDF) toda carta cuyo back sea BackSpec::Dfc. La barra de herramientas de la vista previa lo expone como «Ocultar DFC».
Resolución del reverso: gana BackSpec::Custom; si no, el defaultBack del mazo. En duplex, una carta que no sea en blanco ni grande y no tenga reverso resoluble genera missingDefaultBack y su casilla de reverso queda con image: None (reverso en blanco). Las cartas en blanco (CardSource::Blank) ocupan una celda de anverso pero no tienen casilla de reverso.
Tras cada hoja de anverso viene su hoja de reverso. La celda del reverso es siempre la celda del anverso reflejada en horizontal (x' = W - x - w, misma fila), en vertical y en horizontal, y la imagen del reverso no se gira nunca: todos los reversos salen derechos en el PDF, como si se viera el anverso a través del papel. No hay modo «borde corto» (uno anterior giraba la página de reversos 180 grados, y todos los reversos salían boca abajo en el PDF); Duplex es Off o LongEdge, y el valor guardado shortEdge de los decks y .pbdeck antiguos se lee como LongEdge. En el diálogo de la impresora, voltear por el borde que refleja de izquierda a derecha (el largo en hojas verticales).
Lo cubren los tests el_dorso_nunca_se_gira_ni_en_vertical_ni_en_apaisado, borde_corto_de_un_deck_antiguo_se_lee_como_borde_largo y el_espejo_aplicado_dos_veces_vuelve_al_sitio.
PrintSettings.rowOffsets (RowOffsets { front: Vec<Offset>, back: Vec<Offset> }, Offset { xMm, yMm }) desplaza filas enteras. Las filas que faltan valen 0 (front_row/back_row).
Decisión: los desplazamientos del reverso se expresan en el marco del ANVERSO y se indexan por la fila del ANVERSO (lo que se ve al sostener la hoja a contraluz por el anverso). paginar los suma ANTES del espejo (pre = base + back offset, y luego refleja pre). Motivo: quien mide la hoja de calibración lee las desviaciones en el marco del anverso; convertirlas según la orientación es trabajo del motor, no del usuario. Tests: el_desplazamiento_del_dorso_se_mide_desde_el_frente, el_desplazamiento_del_frente_mueve_solo_su_fila. Véase Exportación para la interfaz de alineación y la hoja de calibración.
-
gapMm(2 mm por defecto) es el espacio en blanco entre cartas, de modo que cada carta conserva su tamaño exacto de 63 x 88 mm (bueno para fundas) y el sangrado no cubre la separación. -
bleedMm(0 por defecto). Por cada lado de cada celda (lados_de_sangrado): el sangrado completo hacia el exterior de la rejilla,min(bleed, gap/2)hacia una carta vecina, de modo que dos sangrados contiguos se tocan sin pisarse (dos_sangrados_contiguos_se_tocan_sin_pisarse,sin_hueco_no_hay_sangrado_interior). -
ampliarrecorta el rectángulo de sangrado a la página, de modo que ningún sangrado sale del papel (ningun_sangrado_sale_del_papel_en_ningun_formato). -
bleedOnBacks(encendido por defecto): si se apaga, las casillas de reverso tienen sangrado cero en los cuatro lados (bleedFullMm = 0). Al reflejar, se intercambian los lados de sangrado izquierdo/derecho (o superior/inferior) para que el lado exterior siga siendo el exterior.
guias(...) produce segmentos LineMm en los márgenes, solo cuando cutGuides.enabled y lengthMm > 0. Las marcas verticales van encima de la primera fila y debajo de la última en el borde izquierdo y derecho de cada carta; las marcas horizontales van a izquierda y derecha de cada fila en su borde superior e inferior, así que siguen los desplazamientos de fila. offsetMm es la distancia al borde de la carta, lengthMm la longitud de la marca; todo se recorta a la página. Las guías solo van en las páginas de anverso: en una hoja de reverso, a contraluz, se confundirían con las marcas del anverso (comentario en paginar; test las_guias_solo_van_en_el_frente_y_dentro_del_papel). El color y el grosor (widthPx, px CSS) los dibujan los consumidores, no el plan.
es_grande(card) es verdadero solo para cartas diseñadas (card.design) cuyo indicador oversized está activo y cuya maquetación Maquetacion::es_grande() (Maquetacion::Plano, Plan, Vanguard; Deducido: planos de Planechase y conspiraciones de Archenemy; véase Composición del diseñador). Un plano de Scryfall sin diseño se imprime a tamaño normal.
Las cartas grandes se apartan y se maquetan al final, en sus propias hojas (Page.big = true), con una segunda Grid calculada para maquetacion::medida_grande() = 88,9 x 127 mm (3,5 x 5 pulgadas) con el mismo papel, margen, separación y sangrado, guardada en PagePlan.bigGrid/bigCells. Si esa rejilla no cabe en el papel, las cartas normales se imprimen igualmente y se lanza PlanWarning::BigCardsDoNotFit. Una carta grande sin reverso propio recibe el reverso del mazo solo si settings.bigCardsBack está activo; si no, no tiene casilla de reverso (reverso en blanco), porque los reversos de las cartas grandes son distintos de los de las normales. Tests: las_cartas_grandes_van_al_final_en_sus_hojas, sin_cartas_grandes_el_plan_no_cambia, si_las_grandes_no_caben_las_normales_salen.
arrange(cards: &[DeckCard]) -> Vec<String> devuelve el nuevo orden de los ids de carta («Auto ordenar» en la barra de herramientas).
- Descarta las cartas en blanco que no están bloqueadas.
- Agrupa las copias de la misma carta por orden de primera aparición. Clave: el nombre en minúsculas para las cartas de Scryfall y las fichas, el blob de la imagen para las cartas propias, el id de la carta para las en blanco.
- Las cartas de doble cara van después de las de una sola cara, igualmente agrupadas.
- Las cartas bloqueadas nunca se mueven: cada una conserva su índice de posición si aún cabe; si el mazo se ha quedado más corto que la posición, va al final en su orden original.
Lo envuelve arrange_cards de commands/layout.rs; en la pantalla, autoOrdenar de src/features/preview/acciones-carta.ts llama a Rust y luego sustituye el orden de las cartas en el store del mazo (así se autoguarda y se puede deshacer). Los tests están en arrange.rs.
src-tauri/src/commands/layout.rs:
| Comando | Firma | Notas |
|---|---|---|
compute_page_plan |
(deck: Deck) -> AppResult<PagePlan> |
Puro; recibe el mazo entero desde la pantalla. |
arrange_cards |
(cards: Vec<DeckCard>) -> Vec<String> |
Nuevo orden de ids. |
LayoutError se convierte en AppError { code: InvalidInput, params: { motivo: "card_does_not_fit" | "invalid_settings" } } (impl From<LayoutError> for AppError en src-tauri/src/error.rs).
usePlan guarda { plan, error, deDeck }. iniciarPlan(retardoMs = 100) se suscribe al store del mazo:
- Un id de mazo distinto, o ningún mazo, se pide sin espera.
- Los cambios en
cards,settingsodefaultBackse agrupan con 100 ms de espera (una ráfaga de ediciones pregunta a Rust una sola vez). Renombrar el mazo no recalcula. - Las respuestas pueden llegar desordenadas; un contador de peticiones hace que gane la última.
- Si hay error se conserva el último plan bueno y se rellena
error(el panel de vista previa lo muestra en la barra de aviso ámbar). -
deDeckes la instantánea del mazo con la que se calculó el plan.
Tests: src/stores/__tests__/plan-store.test.ts (primera petición inmediata, una sola petición tras una ráfaga, petición inmediata al cambiar de mazo, se ignora el cambio de nombre, gana la que llega desordenada, un error conserva el último plan, detener cancela).
- El plan se calcula a partir de la instantánea del mazo en pantalla, no de la base de datos (el autoguardado puede ir con retraso). La exportación hace lo mismo: véase Exportación.
-
Slot.rowes la fila del anverso incluso para las casillas de reverso; no indexes los desplazamientos del reverso por la fila reflejada. -
PagePlan.cellsignora los desplazamientos de fila; soloSlot.trimlos incluye. -
placedcuenta las celdas de anverso ocupadas (normales más grandes). - Un ajuste nuevo que mueva cosas debe pasar por
plan(), nunca por el código de la vista previa ni depdf.
- Nuevo tamaño de papel: añádelo a
PaperFormat,TODOSysize_mm, regenera los bindings (pnpm bindings) y añade una etiqueta ensrc/features/export/export-panel.tsx. - Nueva regla de colocación: cambia
items()opaginar()y añade un test en el módulo de tests deplan.rs; comprueba que los tests depdf/compose.rssiguen pasando.
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