Skip to content

Interfaz del diseñador

Claude edited this page Oct 27, 2026 · 19 revisions

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

La vista del diseñador

Disposición y puntos de entrada

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.

Mapa de ficheros

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)

Modelo de estado

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

Grupos

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

useBorrador: el bucle de edición

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"]
Loading

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+←/→) mientras cambiado es verdadero abre un diálogo Confirmar («¿Descartar los cambios?») con un tercer botón con el foco, «Guardar y continuar» (accionAlternativa de Confirmar): guarda con guardarCarta() 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): mientras cambiado es verdadero, DisenarScreen llama a useEdicion.poner({ guardar }) (un efecto que lo quita con poner(null) al limpiarse, así que desaparece al guardar o descartar los cambios o al desmontarse la pantalla). guardar pasa por un ref a la última guardarCarta, 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_register en 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 === design le 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 opaco recursos ({ 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 a design_save con el designLang del mazo, aplica el resultado con un único setCartas(aplicarAlDeck(...)) para que el deshacer funcione, recuerda la colección del diseño para los próximos borradores y restablece design/original al diseño guardado. Con todas = true, las demás copias reciben el mismo diseño conservando su propio before (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) ejecuta crearVariante en disenar-screen.tsx. Si el borrador tiene cambios sin guardar, primero llama a guardar(false) (solo esta copia) para que la variante parta de ellos; después duplicar (lib/deck-ops.ts) inserta justo detrás de la actual una copia con id nuevo y el mismo diseño, esa copia lleva marca: "variante" (se imprime, pero proxy_core::model::cuentan la deja fuera del tamaño, las copias y las estadísticas), se activa «solo esta copia» y ui-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 el quitarDiseno(cartas, ids) local (restaura design.before) y sube un contador recarga (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 Rust design_remove está 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+→ (keydown en la ventana) van al grupo diseñable anterior/siguiente (dando la vuelta); también los botones con flecha. Ambos pasan por pasarConAviso, que abre el diálogo de descartar de arriba cuando el borrador tiene cambios sin guardar (antes llamaban a pasar directamente y perdían los cambios en silencio). Con la preferencia ui-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 (vecina en grupos.ts calcula 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). pasar es 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 incrementa simbolos para 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).

Vista previa (vista-previa.tsx)

La carta mostrada es lo que Rust pinta para el hash registrado: <img src={urlDiseno(huella, ancho)}>.

  • Resolución: un ResizeObserver sobre el marco elige 1000 / 745 / 488 px a partir de clientWidth × 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: useImagenViva sigue mostrando la última imagen cargada hasta que la siguiente ha terminado de cargar.
  • Controles: el selector de idioma (SelectorIdioma de src/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, usando resumen.carasImpresas) y un interruptor «Margen de seguridad» que dibuja un rectángulo discontinuo a 3 mm de cada borde (porcentajes 3/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ún nivelAviso(a) de textos.ts (esGrave = error: el texto no cabe, faltan símbolos o letras; sinTextoOficial y reglasEditadas = 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 (Popover con PopoverAnchor, 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 de Aviso en texto traducido (los idiomas, por su nombre propio). El error faltanLetras lleva un botón «Bajar las letras», que simplemente vuelve a elegir ese idioma, así que completarIdioma baja la letra.

Arrastre en vivo de la ilustración

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
Loading

Cómo encaja todo:

  • claveCapa(design, cara, lang) (encuadre.ts) es el JSON del diseño con el framing de 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 ref pedida ("<clave>|<ancho>") evita peticiones duplicadas y descarta las respuestas obsoletas. Un cambio de recursos borra 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 en pointerenter, pointerdown, wheel y cuando el encuadre cambia mediante los deslizadores del formulario (efecto framingClave). 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; visible lo activa y desactiva. Estructura: contenedor negro redondeado (borderRadius a partir de capa.radio en porcentajes) → la <img> de la ilustración dentro de una caja colocada en capa.arte (porcentajes de la carta) con width/height/transform: translate(...) rotate(...) calculados por geometriaArte → la <img> de la capa encima. Informa onLista(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 con rotate(giro) de CSS; ahí no se usa la extensión del difuminado. El arrastre se pasa al sentido de la pieza (arrastreEnLaPieza) antes de moverEncuadre, 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, o HUECO_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. geometriaArte reimplementa la colocación de la ilustración que usa el pintar_arte del motor (la misma cuenta de cubrir el hueco con la caja envolvente rotada); los tests unitarios de __tests__/encuadre.test.ts fijan la geometría solo en TypeScript (no hay test de paridad entre lenguajes).
  • Mientras se arrastra: vivo (encuadre local) se aplica al instante; onEncuadre lo pasa al diseño como máximo cada EMPUJE_MS = 100; al soltar (o FIN_RUEDA_MS = 200 ms después del último paso de la rueda) acabar() vuelca el valor final. Durante el arrastre la URL mostrada se congela (ref congelada) 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 reciben null y van por el camino antiguo (cada cambio se repinta por /d/).

src/lib/imagen-viva.ts

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

El formulario (formulario.tsx)

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 mediante abrirParaImportar (src/lib/dialogos.ts), importa el fichero con blob_import_files y luego art_from_blob. «Pegar» llama a art_from_clipboard (Rust lee el portapapeles porque WebKitGTK deniega navigator.clipboard.read()). «Arte de Scryfall» llama a art_from_scryfall con el scryfallId de la carta y la cara actual. «Recortar de una carta…» abre RecortarDialog. Los deslizadores y el arrastre en la vista previa escriben el mismo framing.
  • Estilos (estilos.ts): el selector fusiona los ESTILOS integrados (Despejado, Enmarcado extendido, Enmarcado clásico, Paisaje) con los estilos propios del pack, con el nombre de nombreEnIdioma(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 como style (su estilo integrado base, que sirve de alternativa si el pack no lo tiene) + estiloPack (su id), y se muestra como el valor pack:<id>; elegirEstilo lo 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 borra estiloPack si 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 como undefined; en las reglas, rulesSize === undefined significa «automático» (el interruptor «Reglas: tamaño automático»). hayAjustes indica si algo difiere de AJUSTES_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 como undefined) y «Altura según el texto», un Selector de tres opciones (alturaDe/AlturaCaja: «Según el marco» = undefined, «Dinámica» = true, «Fija» = false) que escribe rulesBoxDynamic. 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 guarda artFade: 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. hayAjustes también cuenta artFade, artFadeExtension, rulesBoxHeight y rulesBoxDynamic.
  • Pasar a la colección: PestanaAjustes.pasarAColeccion guarda los ajustes finos de la carta en su colección (collection_save), borra los propios de la carta y llama a rehacerMiniaturas() para que se actualicen las miniaturas de las demás cartas.
  • Estilo y ajustes por cara (por-cara.ts, AjustesDeCara en formulario.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, conAjustesPropios le 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 un cambiar envuelto por cambiarEnCara, que manda lo que cambia de estilo, marco, emblema y opciones a los ajustes de 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).impresas falso); en los demás, solo cuando ese idioma no tiene texto oficial (sinTextoEn(card, lang) de por-idioma.ts) o su línea de tipo es igual a la inglesa. «Buscar frases oficiales» llama a design_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}» (fija rulesOverride en el texto de ese idioma). La respuesta se descarta si la carta o la cara cambiaron entretanto (ref claveActual).

Diálogos

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 y rehacer miniaturas (renumerar.ts)

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

Internacionalización

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.

Tests

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

Trampas conocidas

  • 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/original es con JSON.stringify, así que el orden de las claves importa: usa siempre spread de los objetos existentes en lugar de reconstruirlos.
  • Los efectos de useBorrador omiten 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: cara vuelve 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_scryfall con cara = 1, véase Capa de aplicación del diseñador).
  • Una huella anterior a la última edición puede llegar tarde; comprueba siempre alDia antes de tratarla como exacta.
  • WithTooltip en lugar de title= nativo (lo vigila pnpm check:title); los modales son totalmente opacos.

Por dónde empezar para ampliarlo

  • 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ón Pestana… adecuada usando cambiar, 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 al disenar.json de cada idioma (si no, falla pnpm idiomas).
  • Diálogo nuevo: añade un tipo de Dialogo en ui-store.ts, móntalo en src/App.tsx y reutiliza lanzarTrabajo para los trabajos.
  • Aviso nuevo: añade la variante de Aviso en Rust, un caso en textoAviso (Deducido: el switch no tiene default, así que el compilador debería señalar el caso que falte) y una línea de test en textos.test.ts.

Véase también

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