Repository navigation
Interfaz del diseñador
English · Español
El lado de React del diseñador de cartas: la vista «Diseñar» (tercera pestaña de la app, junto a «Deck» y «Biblioteca») más sus diálogos (colecciones, letras/marcos/símbolos, «Diseñar el deck», «Exportar imágenes», recortar una ilustración). Solo pinta y guarda el estado de pantalla; cada renderizado, hash y operación con ficheros los hace Rust (Capa de aplicación del diseñador) y el motor puro (Motor del diseñador).

src/features/disenar/disenar-screen.tsx → DisenarScreen se pasa al shell como el slot disenar (src/App.tsx → src/features/shell/app-shell.tsx, un ToggleGroup sobre useUi.vista = "deck" | "biblioteca" | "disenar"). Los diálogos se montan una sola vez en src/App.tsx (ColeccionesDialog, RecursosDialog, LoteDialog, PngDialog, CardEditorDialog) y se abren con useUi.getState().abrir({ tipo }) con los tipos de Dialogo colecciones, recursosDiseno, disenarDeck, exportarPng (src/stores/ui-store.ts).
Formas de entrar: la pestaña «Diseñar», o la opción «Diseñar proxy» / «Editar diseño» del menú contextual de la carta (src/features/preview/slot-menu.tsx, activa cuando la carta tiene id de Scryfall: scryfallDe(carta) !== null), que llama a useUi.disenar(id) (fija disenando y cambia vista).
┌ barra superior: ‹ › Alt+←/→ │ Renumerar │ Diseñar el deck │ Exportar imágenes │ Colecciones │ Letras, marcos y símbolos
├────────────┬─────────────────────────────┬────────────────────────┐
│ListaCartas │ VistaPrevia (idioma, cara, │ Formulario (6 pestañas)│
│(grupos) │ margen de seguridad, capa de│ + pie: quitar, │
│ │ ilustración, avisos) │ descartar, Guardar, │
│ │ │ Guardar y siguiente │
└────────────┴─────────────────────────────┴────────────────────────┘
Los tres paneles son un ResizablePanelGroup (ids lista, vista, formulario); los tamaños persisten en localStorage bajo proxybuilder.paneles.disenar (useDefaultLayout). El desplazamiento usa el ScrollArea compartido (regla del proyecto: nunca overflow-auto) y las listas con elementos con anillo llevan relleno p-1.
| Fichero | Función |
|---|---|
disenar-screen.tsx |
Esqueleto de la pantalla: selección, idioma de diseño, cara, recursos, atajo de teclado, botones de guardar |
use-borrador.ts |
El hook de edición: carga/crea el diseño, registra las vistas previas, avisos, guardar/quitar |
use-resumen.ts |
design_structure con retardo (tipo de maquetación y cómo se reparte el texto) |
por-cara.ts |
Estilo y ajustes por cara: proyectar, cambiarEnCara, conAjustesPropios, tieneAjustes
|
grupos.ts |
Agrupa las cartas del mazo por carta de Scryfall; aplicarAlDeck, quitarDiseno, antesDe, numerosUsados
|
lista-cartas.tsx |
Lista de la izquierda con insignias de estado y búsqueda |
vista-previa.tsx |
Centro: la imagen de la carta, selector de idioma y de cara, arrastre en vivo de la ilustración |
incidencias.tsx |
La franja de errores, avisos e información bajo la carta, con su globo al pasar el ratón |
arte-en-vivo.tsx |
El compositor de la capa superpuesta que se usa al arrastrar la ilustración |
encuadre.ts |
Matemáticas del encuadre compartidas con Rust (moverEncuadre, geometriaArte, claveCapa) |
formulario.tsx |
Panel derecho, 6 pestañas; exporta también EditorMarco (reutilizado por las colecciones) |
ajustes-finos.tsx |
Controles de ajustes finos (AjustesFinos, AJUSTES_CERO, hayAjustes) que usan el formulario y las colecciones |
estilos.ts |
Estilos integrados y estilos propios de los packs en un solo selector; nombreEnIdioma elige el nombre de un pack en el idioma de la interfaz |
renumerar.ts |
renumerar, rehacerMiniaturas, ponerIdiomaDiseno (cambiar el idioma de diseño del deck) y completarIdioma (el trabajo design_complete_lang) |
textos.ts |
Tablas de etiquetas (estilos, paletas, rarezas, maquetaciones) y textoAviso/esGrave
|
traduccion.tsx |
Panel de «Traducción sugerida» |
colecciones-dialog.tsx |
Gestor de colecciones |
recursos-dialog.tsx |
Letras, packs de marcos (incluidos los descargables), símbolos |
lote-dialog.tsx |
«Diseñar el deck» |
png-dialog.tsx |
«Exportar imágenes» |
recortar-dialog.tsx |
Dibujar un rectángulo sobre una carta terminada para usarlo como ilustración |
renumerar.ts |
renumerar, rehacerMiniaturas, aplicarCambiadas
|
marcos-originales.ts, packs-descargables.ts
|
Pequeños stores de zustand con el estado de descarga de los marcos «Original» y de los packs descargables |
src/lib/imagen-viva.ts |
Utilidades de carga de imágenes en tiempo real (las usa también el Diálogo Editar carta) |
No hay un store dedicado al diseñador. El estado se reparte según su duración:
| Estado | Dónde | Duración |
|---|---|---|
El mazo (cartas, cada una con un design opcional) |
useDeck (src/stores/deck-store.ts) |
La fuente de verdad; se puede deshacer con setCartas
|
Qué carta se está diseñando (disenando), pestaña actual (vista), diálogo abierto |
useUi |
Sesión de la app |
cara (índice de cara), soloEsta, colecciones, packs, simbolos, versionLetras
|
useState en DisenarScreen
|
Mientras la vista está montada |
design, original, huella, avisos, cargando, guardando
|
useBorrador |
Por cada carta seleccionada |
| Colección recordada para los borradores nuevos | clave de localStorage proxybuilder.disenar.coleccion (coleccionRecordada/recordarColeccion) |
Por equipo, envuelto en try/catch |
| Progreso de descarga de los marcos Original / packs descargables |
useMarcosOriginales, usePacksDescargables
|
Sesión de la app |
DisenarScreen lee lang directamente de deck.settings.designLang (no es estado de la pantalla), así que lo que enseña el diseñador es lo que imprime el PDF (véase Capa de aplicación del diseñador). El selector de idioma de la vista previa llama a ponerIdiomaDiseno(lang) (renumerar.ts), la misma función que el selector del panel de exportación: escribe designLang con setAjustes y, en cualquier idioma que no sea el español ni el inglés, lanza completarIdioma (el trabajo design_complete_lang, tipo de trabajo "idioma": baja la letra de japonés, coreano o chino si hace falta y la impresión de cada carta diseñada a la que le falte ese idioma; véase Capa de aplicación del diseñador) y luego rehacerMiniaturas(). Al acabar sube versionLetras, que forma parte de la clave de recursos, así que la vista previa se vuelve a pedir (una letra CJK recién bajada cambia la imagen sin cambiar nada más que la pantalla conozca).
agrupar(cartas) (grupos.ts) agrupa las cartas del mazo por id de Scryfall (s:<id>), incluidas las fichas y las cartas propias vinculadas a Scryfall (scryfallDe); las casillas en blanco se saltan; las cartas propias sin vínculo van solas (c:<cardId>) y no se pueden diseñar (fila desactivada con un tooltip que apunta a «Vincular con una carta…»). Un diseño se aplica a todas las copias de un grupo salvo que se marque «Solo esta copia (hay N)». La copia que se edita es la carta que dice ui-store.disenando (su posición en el grupo es copia, que useBorrador toma como grupo.cartas[copia]), así que un grupo con varias copias enseña en el pie una fila «Copia:» con botones numerados para elegir una; elegirla marca «Solo esta copia» y pregunta antes de descartar cambios sin guardar. «Quitar diseño» sigue la misma casilla (solo esta copia o todo el grupo). El estado de un grupo es el de su primera copia (estadoDe): sinVincular, sinDiseno, borrador, listo (design.status).
flowchart TD
Sel["cambia el grupo seleccionado"] --> Has{"principal.design?"}
Has -->|sí| Load["design = original = principal.design"]
Has -->|no| Draft["llama a design_draft(card, usados, collection)"]
Draft --> Set["design = original = draft.design; avisos = draft.avisos"]
Load --> Edit
Set --> Edit["cambiar(f): setDesign(f(d))"]
Edit --> Reg["efecto: llama a design_register (una en vuelo, gana la última)"]
Edit --> Warn["efecto: retardo de 400 ms, design_fit_report"]
Edit --> Struct["use-resumen: retardo de 200 ms, design_structure"]
Reg --> Hash["huella -> pbimg d/huella/ancho"]
Edit --> Save["guardar: design_save -> aplicarAlDeck -> setCartas"]
Comportamientos clave (todos en use-borrador.ts):
-
Contador de turno (ref
turno): las respuestas de una carta anterior nunca sobrescriben la actual. -
Borrador sin guardar frente al original:
cambiado = JSON.stringify(design) !== JSON.stringify(original). Un borrador recién creado cuenta como sin cambios («Descartar» vuelve a él). Elegir otra carta (lista, flechas,Alt+←/→) mientrascambiadoes verdadero abre un diálogoConfirmar(«¿Descartar los cambios?») con un tercer botón con el foco, «Guardar y continuar» (accionAlternativadeConfirmar): guarda conguardarCarta()y luego ejecuta el movimiento pendiente, así que ya no hay que cancelar, guardar y repetir. Si guardar falla, el diálogo sigue abierto (el error ya salió). «Descartar» queda en medio y Cancelar a la izquierda; mientras guarda, los tres botones están desactivados y el diálogo no se puede cerrar. -
Registro para otras pantallas (
stores/edicion-store.ts,useEdicion): mientrascambiadoes verdadero,DisenarScreenllama auseEdicion.poner({ guardar })(un efecto que lo quita conponer(null)al limpiarse, así que desaparece al guardar o descartar los cambios o al desmontarse la pantalla).guardarpasa por un ref a la últimaguardarCarta, de modo que el callback registrado nunca queda obsoleto. El shell lo usa para ofrecer el mismo «Guardar y continuar» al salir de la pestaña Diseñar y al cerrar la app, sin saber cómo se guarda una carta (Ajustes y shell). - Cambios externos: si el diseño del mazo para esta carta cambia desde fuera (borradores en bloque, deshacer, renumerar, rehacer miniaturas) y aquí no hay nada sin guardar, se adopta el diseño nuevo.
-
Registro en tiempo real: cada cambio llama a
design_registeren cuanto termina la llamada anterior; si llegan varios mientras tanto, solo se ejecuta el último (registro.enVuelo/pendiente). Sin temporizador: la vista previa sigue al teclado y a los deslizadores. La respuesta se guarda como{ h, de: design };alDia = huella.de === designle dice a la vista previa si el hash pertenece al diseño tal como está ahora. -
Señal de recursos:
useBorrador(..., recursos)recibe un valor opacorecursos({ colecciones, packs, simbolos }); cuando cambia (colecciones editadas, packs descargados, símbolos obtenidos) el diseño se vuelve a registrar y los avisos se recalculan aunque el diseño sea idéntico. -
Avisos (
design_fit_report) esperan 400 ms porque Rust renderiza cada cara para calcularlos. -
Guardar (
guardar(todas, listo?)): llama adesign_savecon eldesignLangdel mazo, aplica el resultado con un únicosetCartas(aplicarAlDeck(...))para que el deshacer funcione, recuerda la colección del diseño para los próximos borradores y restablecedesign/originalal diseño guardado. Contodas = true, las demás copias reciben el mismo diseño conservando su propiobefore(antesDe). -
Crear variante: el botón «Crear variante» (icono
CopyPlus, en el pie, en la misma fila que los botones «Copia:», visible siempre que haya una carta elegida) ejecutacrearVarianteendisenar-screen.tsx. Si el borrador tiene cambios sin guardar, primero llama aguardar(false)(solo esta copia) para que la variante parta de ellos; despuésduplicar(lib/deck-ops.ts) inserta justo detrás de la actual una copia con id nuevo y el mismo diseño, esa copia llevamarca: "variante"(se imprime, peroproxy_core::model::cuentanla deja fuera del tamaño, las copias y las estadísticas), se activa «solo esta copia» yui-store.disenar(id)la selecciona. El grupo sigue teniendo el mismo id de Scryfall, así que la lista no gana una fila; los botones «Copia: 1 2…» aparecen porque el grupo ya tiene varias copias. La línea con los botones de copia solo sale con más de una copia; la fila del botón está siempre. -
Quitar: el botón «Quitar diseño» (icono de papelera con texto, en el pie, solo si la carta tiene diseño) llama a
quitar, que usa elquitarDiseno(cartas, ids)local (restauradesign.before) y sube un contadorrecarga(dependencia del efecto de cambio de carta) para que el editor pida un borrador nuevo en vez de seguir enseñando el diseño quitado; el comando de Rustdesign_removeestá registrado pero la interfaz no lo usa.
Extras de DisenarScreen:
- Selecciona automáticamente la primera carta diseñable al entrar sin ninguna seleccionada.
-
Alt+←/Alt+→(keydownen la ventana) van al grupo diseñable anterior/siguiente (dando la vuelta); también los botones con flecha. Ambos pasan porpasarConAviso, que abre el diálogo de descartar de arriba cuando el borrador tiene cambios sin guardar (antes llamaban apasardirectamente y perdían los cambios en silencio). Con la preferenciaui-store.disenarCopias(casilla «Recorrer también las copias con las flechas», apagada de fábrica y recordada), las flechas y «Guardar y siguiente» van copia a copia (vecinaengrupos.tscalcula el destino) y al guardar solo se aplica a la copia de ahora («Solo esta copia» se marca al entrar en cada copia), de modo que cada copia de una carta puede llevar su propio diseño. - «Guardar y siguiente» guarda y luego hace
pasar(1).pasares el movimiento sin aviso a propósito: solo corre tras guardar con éxito, así que no queda nada por preguntar. - Al entrar se llama una vez a
symbols_status({ refresh: false }): la primera vez descarga los símbolos de maná de Scryfall (necesita conexión) y luego incrementasimbolospara que la vista previa se repinte; si hay 0 cargados muestra un aviso de advertencia. - Las listas de colecciones y packs se vuelven a leer cuando se abre/cierra un diálogo o cuando termina una descarga (
versionOriginales,versionDescargables).
La carta mostrada es lo que Rust pinta para el hash registrado: <img src={urlDiseno(huella, ancho)}>.
-
Resolución: un
ResizeObserversobre el marco elige 1000 / 745 / 488 px a partir declientWidth × devicePixelRatio(>760, >500, si no). -
Modo ráfaga:
useRafaga(huella)es verdadero mientras el hash cambia más a menudo que cada 250 ms; entonces la URL pide como máximo 488 px para seguir al cursor, y la resolución completa llega cuando acaba la ráfaga. -
Nunca en blanco:
useImagenVivasigue mostrando la última imagen cargada hasta que la siguiente ha terminado de cargar. -
Controles: el selector de idioma (
SelectorIdiomadesrc/components/selector-idioma.tsx: los 15 idiomas con su bandera y su nombre propio), selector de cara cuando el diseño tiene más de una («Cara n» para las caras impresas, «Parte n» para las partes de las maquetaciones split/adventure, usandoresumen.carasImpresas) y un interruptor «Margen de seguridad» que dibuja un rectángulo discontinuo a 3 mm de cada borde (porcentajes3/63,3/88). -
Franja de incidencias bajo la carta (
incidencias.tsx→Incidencias, siempre pintada con alto fijo para que la vista previa no salte cuando aparece o desaparece un aviso): un contador por nivel, incluso a 0 — errores, avisos e información — segúnnivelAviso(a)detextos.ts(esGrave= error: el texto no cabe, faltan símbolos o letras;sinTextoOficialyreglasEditadas= información; el resto, donde el marco o el texto se degradan, = aviso). Al pasar el ratón (o el foco) por un contador se abre un globo (PopoverconPopoverAnchor, controlado, que se cierra 150 ms después de salir el puntero para poder entrar en él) con sus mensajes, o «no hay ningún error/aviso/información que mostrar» si está vacío.textoAviso(a, t)convierte cada variante deAvisoen texto traducido (los idiomas, por su nombre propio). El errorfaltanLetraslleva un botón «Bajar las letras», que simplemente vuelve a elegir ese idioma, así quecompletarIdiomabaja la letra.
Problema: mover la ilustración con un renderizado completo de Rust por cada movimiento del ratón es demasiado lento. Solución (Decisión: renderizar la carta una vez sin ilustración y mover la ilustración con CSS):
sequenceDiagram
participant U as Puntero / rueda
participant VP as VistaPrevia
participant R as Rust
U->>VP: pointerenter / down / wheel
VP->>R: design_overlay(design, cara, lang, ancho)
R-->>VP: CapaInfo { huella, arte[x,y,w,h], radio, cara, giro }
VP->>VP: se monta ArteEnVivo: img de ilustración (/b/blob/1000|2000) + img de capa (/c/huella/ancho)
U->>VP: mover (dx, dy) / rueda (zoom)
VP->>VP: moverEncuadre -> vivo (CSS, instantáneo)
VP-->>VP: onEncuadre se pasa al diseño cada 100 ms
U->>VP: soltar / 200 ms tras el último paso de la rueda
VP->>R: el camino normal de design_register renderiza la carta exacta
Cómo encaja todo:
-
claveCapa(design, cara, lang)(encuadre.ts) es el JSON del diseño con elframingde la ilustración que se mueve (cara) anulado; el de las otras partes cuenta, porque la capa de una compuesta las pinta. Mientras la clave no cambia, la capa superpuesta sigue siendo válida; si cambia cualquier otra cosa (texto, marco, idioma, el encuadre de la otra parte) se pide una capa nueva de forma perezosa. La refpedida("<clave>|<ancho>") evita peticiones duplicadas y descarta las respuestas obsoletas. Un cambio derecursosborra la capa. - La capa se pide por adelantado, 500 ms después de que la carta deje de cambiar (
CAPA_ANTES_MS), y además enpointerenter,pointerdown,wheely cuando el encuadre cambia mediante los deslizadores del formulario (efectoframingClave). Por qué por adelantado: antes, el primer arrastre esperaba a la capa y sus primeros pasos iban por renders de la carta entera (se notaba en las sagas, cuyo render pesa más). -
ArteEnVivo(arte-en-vivo.tsx) está siempre montado mientras exista una capa válida, de modo que ambas imágenes ya están cargadas cuando hacen falta;visiblelo activa y desactiva. Estructura: contenedor negro redondeado (borderRadiusa partir decapa.radioen porcentajes) → la<img>de la ilustración dentro de una caja colocada encapa.arte(porcentajes de la carta) conwidth/height/transform: translate(...) rotate(...)calculados porgeometriaArte→ la<img>de la capa encima. InformaonLista(true)cuando ambas imágenes han cargado. - En una pieza girada (
capa.giro= ±90: la mitad de abajo de una Secuela, las mitades de una partida) la ilustración va en una caja sin girar del tamaño del hueco tal como lo ve la ilustración (huecoSinGirar: los lados del hueco cambiados), centrada en el hueco y girada conrotate(giro)de CSS; ahí no se usa la extensión del difuminado. El arrastre se pasa al sentido de la pieza (arrastreEnLaPieza) antes demoverEncuadre, así que la ilustración sigue al puntero. Comparado con el render de Rust de la mitad de abajo de una Secuela con zoom, desplazamiento y giro, coincide píxel a píxel salvo el remuestreo. -
moverEncuadre(framing, change, hole)(en mm: hueco =capa.arte× 63×88 mm, oHUECO_TIPICO=[0.08, 0.12, 0.84, 0.45]si todavía no hay capa) devuelve el nuevo encuadre: zoom limitado a 1–3, desplazamientos en [-1, 1] escalados por la holgura de cada eje, de modo que la ilustración sigue al puntero 1:1 hasta llegar a un borde, sea cual sea el zoom o la rotación.geometriaArtereimplementa la colocación de la ilustración que usa elpintar_artedel motor (la misma cuenta de cubrir el hueco con la caja envolvente rotada); los tests unitarios de__tests__/encuadre.test.tsfijan la geometría solo en TypeScript (no hay test de paridad entre lenguajes). - Mientras se arrastra:
vivo(encuadre local) se aplica al instante;onEncuadrelo pasa al diseño como máximo cadaEMPUJE_MS = 100; al soltar (oFIN_RUEDA_MS = 200ms después del último paso de la rueda)acabar()vuelca el valor final. Durante el arrastre la URL mostrada se congela (refcongelada) para no mostrar los renderizados intermedios de Rust;componer = enVivo && (vivo !== null || mostrada !== exacta)mantiene visible la capa superpuesta hasta que el renderizado exacto (urlDiseno(huella, ancho)del hash actualizado) está en pantalla. - La rueda sola hace zoom de ±0.04 por paso (Ctrl+rueda se ignora). El arrastre se desactiva cuando la cara no tiene ilustración.
- La ilustración que se mueve es la de
caraArte: la parte elegida, o la parte 1 si la elegida no tiene ilustración propia (resumen.artePropio === false: la parte 2 de una flip o una aventura enseña la de la parte 1). Las caras sin ilustración y el dorso de un meld recibennully van por el camino antiguo (cada cambio se repinta por/d/).
| Export | Comportamiento |
|---|---|
CargaUltima |
Carga una imagen cada vez; si llegan más mientras tanto, solo se conserva la última; informa de ocupado/libre |
useImagenViva(url) |
Conserva la última URL cargada con éxito (mostrada) hasta que carga la nueva; indicador cargando
|
useRafaga(valor, ms=250) |
true mientras el valor cambia más rápido que cada ms
|
useAlRitmo(valor, ms=60) |
Deja pasar un valor como máximo cada ms, entregando siempre el último |
cargarEnNavegador |
Cargador por defecto (un objeto Image calienta la caché del navegador) |
Tests: src/lib/__tests__/imagen-viva.test.ts (CargaUltima).
Seis pestañas en un componente Tabs; cambiar(d => d') es la única manera de editar el diseño. Todas las etiquetas pasan por t() con cadenas fuente en español.
| Pestaña | Contenido |
|---|---|
| Carta | Datos originales de solo lectura (nombre, coste, tipo, F/R, rareza, set/número, maquetación detectada mediante MAQUETACIONES), Partes (cómo se reparte el texto), selector de estilo, selector de variante (estilos de pack con variantes), selector de colección + «Gestionar colecciones…», interruptor «Lista para imprimir» (status) |
| Textos | Dos bloques: el idioma de diseño del deck y, debajo, el inglés (el original); con el inglés como idioma de diseño, inglés y español. Cada uno con nombre temático, la línea de tipo (todos los idiomas menos el inglés; cambia el typeLine de los datos de la carta en ese idioma, y un campo vacío vuelve a la línea inglesa al salir de él; el texto de ejemplo es la línea inglesa), texto de ambientación, reglas (el texto oficial se muestra de solo lectura; «Escribir a mano» fija rulesOverride; «Restaurar el oficial» lo borra); TraduccionSugerida. Los textos se leen y escriben con textosDe/conIdioma de src/lib/por-idioma.ts, porque el tipo generado ByLang solo declara es y en (véase Arquitectura del frontend); un idioma que no es el español ni el inglés enseña los campos vacíos en vez de los textos ingleses, igual que FaceDesign::textos en Rust. Interruptor «Poner el texto de ambientación» (options.showFlavor); cualquier texto puede llevar emoji, que el motor pinta en color (véase Motor del diseñador) |
| Arte | Subir imagen / Pegar / Arte de Scryfall / Recortar de una carta; origen y tamaño; aviso bajo 900 px de ancho; deslizadores de zoom (1–3), desplazamiento horizontal/vertical (-1..1), rotación (-180..180); «Encuadre inicial» |
| (Textos y Arte) | Las dos pestañas empiezan con el selector de parte (SelectorParte) cuando el diseño tiene más de una cara: «Cara n» / «Parte n» y el nombre de la cara, el mismo estado que el selector de la vista previa, para que la segunda ilustración de una Secuela o una partida se ponga justo donde se edita la ilustración. En Arte, una parte sin ilustración propia (resumen.artePropio === false) enseña «Esta parte no lleva ilustración propia…» y un botón «Ir a la parte 1» en vez de los controles. |
| Marco | Interruptor «Marco propio para esta carta» (design.frame), EditorMarco, emblema (el de la colección / ninguno / propio), estrechado de título propio |
| Pie | Número de colección (al escribirlo se fija numberManual), letra de rareza, código de set, artista, crédito, universo, año, colocación del «Nombre original»; vacío significa «el de la colección» (el placeholder muestra el valor heredado) |
| Ajustes finos | Ajustes finos propios de esta carta, o los de la colección; «Usar estos ajustes en toda la colección» |
Detalles que conviene saber:
-
Fuentes de ilustración (
PestanaArte): «Subir» abre el diálogo de ficheros nativo medianteabrirParaImportar(src/lib/dialogos.ts), importa el fichero conblob_import_filesy luegoart_from_blob. «Pegar» llama aart_from_clipboard(Rust lee el portapapeles porque WebKitGTK denieganavigator.clipboard.read()). «Arte de Scryfall» llama aart_from_scryfallcon elscryfallIdde la carta y la cara actual. «Recortar de una carta…» abreRecortarDialog. Los deslizadores y el arrastre en la vista previa escriben el mismoframing. -
Estilos (
estilos.ts): el selector fusiona losESTILOSintegrados (Despejado, Enmarcado extendido, Enmarcado clásico, Paisaje) con los estilos propios del pack, con el nombre denombreEnIdioma(nombres, lang, id): el del pack en el idioma de la interfaz; si no, el inglés; si no, el id (igual con las variantes). Un estilo de pack se guarda comostyle(su estilo integrado base, que sirve de alternativa si el pack no lo tiene) +estiloPack(su id), y se muestra como el valorpack:<id>;elegirEstilolo convierte de vuelta.packEfectivo(design, colecciones)= el pack de marcos de la carta; si no, el de la colección; si no,"builtin". Cambiar el pack de marcos en la pestaña Marco borraestiloPacksi el pack nuevo no lo tiene. -
Editor de marco (
EditorMarco, exportado y reutilizado por las colecciones): selector de pack, color (automático / un color de la paleta / degradado / hex personalizado), corona de legendaria (auto/sí/no), caja de reglas (auto/oscura/clara/teñida). Los packs aún no instalados aparecen como «Original (descargar)» y « (descargar)»; al elegir uno se inicia la descarga en Rust con una barra de progreso y luego se aplica el pack. -
Ajustes finos (
ajustes-finos.tsx→AjustesFinos): tamaño de título/símbolos de maná/tipo/reglas/F-R/pie (50–160 %), interlineado de reglas y pie (70–160 %), desplazamiento vertical por elemento en mm (-3..3), opacidad extra de la caja de reglas, radio propio para la ventana de la ilustración y para las esquinas de la carta. Un tamaño del 100 % se guarda comoundefined; en las reglas,rulesSize === undefinedsignifica «automático» (el interruptor «Reglas: tamaño automático»).hayAjustesindica si algo difiere deAJUSTES_CERO. «Ajustes propios para esta carta» copia los ajustes finos de la colección como punto de partida. En «Caja de reglas»: «Altura de la caja de reglas» (-10..10 mm de 0,5 en 0,5; 0 = como el marco, se guarda comoundefined) y «Altura según el texto», unSelectorde tres opciones (alturaDe/AlturaCaja: «Según el marco» =undefined, «Dinámica» =true, «Fija» =false) que escriberulesBoxDynamic. En el editor de marco (EditorMarco), el color del tinte propio de la caja trae un deslizador «Opacidad del tinte» (FilaDeslizador): el selector de color nativo no tiene alfa, así que la opacidad se guarda como el par alfa de un hex de 8 dígitos (opacidadDe/alfaHex) y se conserva al cambiar el color. Bajo la ilustración: un interruptor «Difuminado de la ilustración a medida» que guardaartFade: 12, artFadeExtension: 7(los valores del extendido) y enseña los deslizadores «Difuminado de la ilustración» (0–25 mm) y «Extensión del difuminado» (0–35 mm); apagarlo borra los dos.hayAjustestambién cuentaartFade,artFadeExtension,rulesBoxHeightyrulesBoxDynamic. -
Pasar a la colección:
PestanaAjustes.pasarAColeccionguarda los ajustes finos de la carta en su colección (collection_save), borra los propios de la carta y llama arehacerMiniaturas()para que se actualicen las miniaturas de las demás cartas. -
Estilo y ajustes por cara (
por-cara.ts,AjustesDeCaraenformulario.tsx): en las cartas de dos caras impresas (resumen.carasImpresas > 1), las pestañas Carta, Marco y Ajustes finos empiezan con el selector de cara y, de la cara 2 en adelante, un interruptor «Estilo y ajustes propios de esta cara». Apagado, la cara sigue a la carta (la cara 1) y la pestaña se ve atenuada y sin poder editarse; encendido,conAjustesPropiosle da una copia del estilo, la variante, el marco, el emblema y las opciones de la carta (faces[i].ajustes). Las pestañas no saben nada de caras: reciben el diseño proyectado (proyectar(design, cara)) y uncambiarenvuelto porcambiarEnCara, que manda lo que cambia de estilo, marco, emblema y opciones a losajustesde la cara y todo lo demás (colección, estado) a la carta. El pie, los textos y la ilustración siguen con su propia lógica por cara o por carta. Tests en__tests__/por-cara.test.ts. -
Traducción sugerida (
traduccion.tsx): trabaja en el idioma de diseño del deck. No sale en inglés ni en los idiomas sin cartas impresas (idioma(lang).impresasfalso); en los demás, solo cuando ese idioma no tiene texto oficial (sinTextoEn(card, lang)depor-idioma.ts) o su línea de tipo es igual a la inglesa. «Buscar frases oficiales» llama adesign_suggest_translation(design, cara, lang)(lento: una búsqueda en Scryfall por frase); el resultado muestra las frases encontradas (traducida, en verde) frente a las que se quedan en inglés (en ámbar), con «Usar» para la línea de tipo y «Usar como reglas en {idioma}» (fijarulesOverrideen el texto de ese idioma). La respuesta se descarta si la carta o la cara cambiaron entretanto (refclaveActual).
| Diálogo | Fichero | Comandos | Notas |
|---|---|---|---|
| Colecciones | colecciones-dialog.tsx |
collection_list/save/delete, fonts_list, packs_list, blob_import_files
|
Lista lateral con menú por fila (duplicar, borrar); secciones del editor General, Marco, Emblema, Letras (familia de título/reglas, grosores 400–900), Pie (código de set, letra de rareza, artista, crédito, universo, año, plantillas de pie en español y en inglés, y una más para el idioma de diseño del deck cuando es otro (Plantilla del pie ({idioma}), escrita con conIdioma), con los tokens {num} {rarity} {original} {set} {lang} {artist} {credit} {universe} {year} {brush} {star}, tabulador = columna, salto de línea = fila, el texto tras {>} se alinea a la derecha), Ajustes finos. Cerrar con cambios sin guardar pide confirmación (cambiosSinGuardar). Al cerrar, si se guardó o borró algo, se ejecuta rehacerMiniaturas(). |
| Letras, marcos y símbolos | recursos-dialog.tsx |
fonts_list/import/remove, fonts_free_list, font_free_download, packs_list/import/remove, original_*, packs_downloadable_*, pack_extra_use, symbols_status
|
Pestañas Letras / Packs de marcos / Símbolos. Importa .ttf/.otf y packs desde una carpeta o un .zip; catálogo de letras OFL libres con muestras PNG de src/assets/letras/; filas de packs descargables con un panel «Contenido» cuyos extras (reversos, fichas, tierras básicas, cartas hechas) se pueden fijar como reverso del mazo (useDeck.aplicar({ defaultBack })) o insertar como carta propia (cartaPropia + insertar). Al cerrar, rehacerMiniaturas() si algo cambió. |
| Diseñar el deck | lote-dialog.tsx |
design_drafts_for_deck, job_cancel, eventos design://progress/done
|
Alcance todo/selección, selector de colección, barra de progreso, cancelar. Los resultados se recogen y se aplican al mazo en un único setCartas incluso si se cancela. Los errores de red/límite de peticiones abortan; los fallos por carta se cuentan y se notifican. |
| Exportar imágenes | png-dialog.tsx |
design_export_png, design_complete_lang (con completarIdioma), job_cancel, eventos designpng://progress/done
|
300/600/900 ppp; «Idiomas»: una casilla por cada uno de los 15 idiomas (bandera y nombre propio), con el idioma de diseño del deck marcado de fábrica; carpeta con elegirCarpetaParaExportar. Antes de exportar, cada idioma elegido que no sea el español ni el inglés se completa en las cartas elegidas y se rehacen las miniaturas, porque Rust pinta los datos que tenga el diseño. Los idiomas se mandan en el orden de la tabla; cada uno va a una carpeta con su código. Las copias se exportan una sola vez. |
| Recortar ilustración | recortar-dialog.tsx |
art_from_card |
Lista las cartas del mazo que tienen imagen y aún no están diseñadas, una por blob; se arrastra un rectángulo (propuesta inicial x .08, y .11, w .84, h .45); debe tener al menos un 5 % por lado; el resultado es un blob PNG. |
Todos los trabajos largos usan lanzarTrabajo (src/lib/trabajos.ts): se suscribe a los eventos de progreso y de fin antes de lanzar, para que un trabajo rápido no pueda perder su evento final, y almacena los eventos en un búfer hasta que se conoce el jobId.
-
renumerar(t, mensaje)→design_renumber→aplicarCambiadas(un paso del historial) y un anuncio («{n} cartas renumeradas» / «La numeración ya estaba al día»). -
rehacerMiniaturas()→design_rerender→useDeck.refrescarCartas(antes, nuevas): sustituye solo las cartas que siguen siendo idénticas a lo que se envió y marca el mazo como modificado sin entrada en el historial (no es una edición del usuario; si el usuario cambió la carta entretanto, prevalece su cambio). Se llama al abrir un mazo (src/features/shell/use-deck-inicial.ts), al cerrar los diálogos de colecciones/recursos y después de «pasar a la colección». Los errores se tragan (volverá a intentarlo la próxima vez).
Reglas del proyecto: todo texto pasa por t("cadena fuente en español"); la traducción de cada idioma va en src/i18n/<código>/disenar.json (un objeto indexado por la cadena en español); las tablas de etiquetas que viven fuera de los componentes usan paraTraducir("…") (una marca sin efecto para que pnpm idiomas vea el literal) y se traducen con t() donde se pintan (textos.ts, GROSORES, TIPOS_EXTRA). pnpm idiomas (scripts/idiomas.mjs) falla cuando falta una cadena pedida, y una cadena no debe aparecer en dos ficheros de área. Rust nunca escribe prosa para el usuario: los avisos llegan como variantes de Aviso y los convierte en texto textoAviso; los errores, useLang().mensaje(e). Véase Internacionalización.
| Fichero | Qué cubre |
|---|---|
src/features/disenar/__tests__/encuadre.test.ts |
moverEncuadre (seguimiento 1:1, límites, zoom 1–3), claveCapa (ignora solo el encuadre), geometriaArte (centrado, bordes, solo proporción, rotación de 90°), huecoSinGirar y arrastreEnLaPieza (piezas giradas) |
.../estilos.test.ts |
estilos integrados + de pack en un solo selector; elegirEstilo
|
.../grupos.test.ts |
scryfallDe, agrupar, estadoDe, antesDe, numerosUsados, aplicarAlDeck, quitarDiseno
|
.../renumerar.test.ts |
aplicarCambiadas, renumerar, rehacerMiniaturas (errores, ediciones concurrentes) |
.../textos.test.ts |
cada variante de Aviso tiene texto, los idiomas por su nombre propio (faltanLetras, reglasEditadas, faltaNombre), esGrave, nombres de maquetación únicos |
src/lib/__tests__/imagen-viva.test.ts |
CargaUltima |
No hay tests de componente para el formulario ni para la vista previa; el comportamiento en vivo se comprueba ejecutando la app. Se ejecutan con pnpm check (Tests y CI).
- No calcules geometría de cartas en la pantalla. La única geometría aquí es la colocación de la ilustración durante un arrastre, y refleja a propósito el cálculo del propio motor; un renderizado final de Rust siempre la sustituye.
- La comparación
design/originales conJSON.stringify, así que el orden de las claves importa: usa siempre spread de los objetos existentes en lugar de reconstruirlos. - Los efectos de
useBorradoromiten a propósito algunas dependencias (se basan en el id de la carta o la clave del grupo); añadirlas recargaría el borrador en cada cambio del mazo. - Estado por cara:
caravuelve a 0 cuando cambia el grupo seleccionado. En las maquetaciones split/aftermath las «caras» son partes de una sola cara impresa (carasImpresas === 1, el selector dice «Parte N»); cada parte sigue teniendo su ilustración, así que la pestaña Arte con «Parte 2» elegida pone la de la mitad de abajo de una Secuela, y su botón «Arte de Scryfall» trae solo la ilustración de la segunda mitad (art_from_scryfallconcara = 1, véase Capa de aplicación del diseñador). - Una
huellaanterior a la última edición puede llegar tarde; comprueba siemprealDiaantes de tratarla como exacta. -
WithTooltipen lugar detitle=nativo (lo vigilapnpm check:title); los modales son totalmente opacos.
- Campo nuevo del formulario: añade el campo al modelo de Rust (
CardDesign/DesignOptions/FooterOptions,#[derive(TS)]), regenera los bindings (pnpm bindings), añade un control en la funciónPestana…adecuada usandocambiar, asegúrate de que el motor lo lee (si no, el hash no cambiará de forma visible) y escribe las cadenas fuente en español y añade su traducción aldisenar.jsonde cada idioma (si no, fallapnpm idiomas). - Diálogo nuevo: añade un tipo de
Dialogoenui-store.ts, móntalo ensrc/App.tsxy reutilizalanzarTrabajopara los trabajos. - Aviso nuevo: añade la variante de
Avisoen Rust, un caso entextoAviso(Deducido: elswitchno tienedefault, así que el compilador debería señalar el caso que falte) y una línea de test entextos.test.ts.
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