Repository navigation
Vista previa en vivo
English · Español
El panel central muestra las hojas tal como se imprimirán: una página por hoja de anverso, cada carta colocada exactamente donde dice el PagePlan, con miniaturas que Rust renderiza bajo demanda por la misma tubería de imagen que el PDF. React solo pinta cajas y etiquetas <img>; nunca calcula la maquetación ni procesa píxeles.
Código fuente: src/features/preview/ (preview-pane.tsx, page-view.tsx, slot-view.tsx, slot-menu.tsx, acciones-carta.ts, arrastrar-ficheros.ts, mover-a-dialog.tsx, visibilidad.tsx), src/lib/image-url.ts, src/stores/plan-store.ts, src/stores/ui-store.ts, src-tauri/src/protocol.rs, src-tauri/src/render.rs.

flowchart LR
DS["deck-store (cards, settings)"] -->|"100 ms de espera"| PS["plan-store (compute_page_plan)"]
PS -->|"plan.pages (solo anversos)"| PP["PreviewPane"]
PP --> PV["PageView por hoja"]
PV --> SV["SlotView por Slot"]
SV -->|"urlMiniatura(...)"| IMG["img src = pbimg://.../r/..."]
IMG --> PR["protocol.rs: Ruta::Render"]
PR --> M["AppState::miniatura (caché renders)"]
M --> PIPE["imaging::pipeline::render"]
-
PreviewPaneleeusePlan(el plan y el último error),useDeck(cartas, ajustes, reverso por defecto) yuseUi(zoom, selección, indicadores). Muestra soloplan.pages.filter(p => p.side === "front"). Las hojas de reversos existen en el plan (y en el PDF) pero no son páginas de la vista previa; «Ver reversos» sustituye, en cambio, la imagen de cada casilla de anverso por el reverso que llevará. - Escala:
escala = zoom * PX_POR_MM, conPX_POR_MM = 96 / 25.4(píxeles CSS por mm al 100 %). Una hoja midewidthMm * escalaporheightMm * escala; una casilla se posiciona entrim.x * escala,trim.y * escala. No existe ninguna otra geometría en la pantalla. Véase Motor de maquetación para saber por qué. - Las celdas vacías de la última hoja salen de
plan.cells(o deplan.bigCellssi la última hoja es de cartas grandes) menos los valoresindexde casilla ya ocupados (vaciasenpreview-pane.tsx). Cada una es un botón que abre «Añadir carta» (la primera lleva además el rótulo); la carta va al final del mazo. - Los avisos del plan (
dfcWithoutBack,missingDefaultBack,bigCardsDoNotFit) y el error del plan se muestran como una barra ámbar bajo la barra de herramientas. El tooltip de la barra de herramientas dice que la vista previa es aproximada y que el PDF es exacto.
Un componente memo por hoja: una cabecera «Página n de N» con un botón de eliminar las cartas de la página, y un div de hoja blanca del tamaño del papel. Dentro: un SlotView por cada Slot (con clave cardId:face), las celdas vacías y, si cutGuides.enabled y la página tiene guías, una superposición SVG con viewBox en milímetros (0 0 widthMm heightMm) para que Page.guides se dibuje sin conversión; el grosor del trazo es widthPx * 25.4 / 96 mm. El elemento de la hoja se pasa a useVisible (más abajo); conImagen={visible} decide si las casillas montan <img>.
Un componente memo. Estructura:
- Una caja posicionada de forma absoluta en el
trimde la casilla (left/top/width/height= mm x escala). Es el asa de arrastre, el destino de soltado, el objetivo del clic, y llevadata-carta=<cardId>(sirve para encontrar la carta bajo el cursor cuando se sueltan ficheros desde el sistema operativo). - Dentro, una caja de recorte en el rectángulo
bleedde la casilla (desplazada respecto atrim), de modo que el sangrado compartido con las vecinas se corta exactamente como lo hace el PDF. - Dentro de esa, el
<img>, de tamaño(trim.w + 2*full) x (trim.h + 2*full)confull = slot.bleedFullMm, colocado en(trim.x - full - bleed.x). Esto reflejadibujar_casilladepdf/compose.rs(el renderizado lleva el sangrado completo en los cuatro lados; el recorte conserva lo que permite el plan). - Superposiciones en el marco del corte: anillo de selección, anillo de hover, anillo de soltado, insignias (arriba a la izquierda), icono de candado (arriba a la derecha) y una barra de información (nombre, set, número de coleccionista, idioma cuando no es inglés) si «Info de carta» está activada.
Selección de la imagen:
- Normal:
slot.imageconslot.applyEdits ? card.edits : null. - «Ver reversos» activado y la casilla no es de reverso: la imagen es
card.back.imagepara los reversosdfc/custom, ydeck.defaultBacken otro caso; no se aplican los retoques; la miniatura se trata como un reverso (back: true, de modo que el oscurecido de bordes solo sigue aapplyBacks). Se muestra en su sitio, sin reflejar; es solo una ayuda visual. - Las cartas en blanco (
source.kind === "blank") muestran una caja discontinua con un más; un clic normal (sin Ctrl/Mayús) selecciona la casilla y abre «Añadir carta» para llenarla (anadirEnHueco). Una casilla sin imagen muestra un icono de «imagen apagada» con el nombre de la carta. -
loading="lazy",decoding="async",draggable={false}. - Para las cartas diseñadas,
slot.imagees la miniatura guardada del diseño:diseno::vista::aplicar(src-tauri/src/diseno/vista.rs) pinta una miniatura de 1000 px, la codifica como JPEG de calidad 92 y la guarda en el almacén de blobs conImageOrigin::Designed(guardar_miniatura). La vista previa muestra esa miniatura por la misma ruta/r/, mientras que el PDF vuelve a pintar desde el diseño al tamaño de impresión (Exportación, Capa de aplicación del diseñador).
insignias(card) en src/lib/cartas.ts devuelve entradas { texto, tono, ayuda } que se pintan como pequeñas insignias de colores con un tooltip:
| Insignia | Tono | Cuándo |
|---|---|---|
| etiqueta de la marca (VARIANTE / OPCIONAL / BANQUILLO) |
marca (violeta) |
DeckCard.marca está definida (MARCAS en cartas.ts) |
PROXY / BORRADOR (BORRADOR en las cadenas fuente) |
info / aviso | la carta tiene un diseño con estado ready / en otro caso (no hay insignias de imagen de Scryfall para las cartas diseñadas) |
| CUSTOM, CUSTOM <-> | info | imagen propia, con o sin carta de Scryfall vinculada |
↩ EN |
aviso |
meta.langFallback: el idioma pedido no existía y se recurrió al inglés |
| LOWRES | aviso | imageStatus === "lowres" |
| PLACEHOLDER, MISSING | error | Scryfall no tiene una imagen real |
Las marcas se ponen desde el menú de la casilla (submenú «Marca», marcarCartas). La impresión no se ve afectada; la regla cuentan excluye las cartas marcadas del tamaño del mazo, del recuento de copias, de las estadísticas y de la lista exportada (véase Estadísticas y Exportación). Las insignias se ocultan mientras «Ver reversos» está activado.
Decisión: las imágenes se montan solo para las hojas cercanas al viewport. Motivo: un mazo de 20 hojas son 180 imágenes grandes; mantenerlas todas decodificadas hace que WebKit retenga bitmaps enormes, y Rust renderizaría cada miniatura aunque nunca se viese. Al desmontar, WebKit puede liberar los bitmaps y Rust procesa solo lo visible (el comentario del protocolo dice lo mismo).
useVisible(el) en visibilidad.tsx: un IntersectionObserver con root = el elemento viewport del área de scroll y rootMargin: "100% 0px" (un viewport por encima y otro por debajo). La raíz viene de un contexto, RaizScroll, que PreviewPane rellena pasando onViewportChange={setViewport} al ScrollArea compartido (src/components/ui/scroll-area.tsx). Invariante: la raíz DEBE ser el viewport del ScrollArea, no el documento. El propio documento nunca hace scroll (solo lo hace el viewport del ScrollArea), así que con la raíz por defecto todas las hojas contarían siempre como visibles. Las cajas de las casillas (destinos de soltado) están siempre montadas; solo el <img> depende de conImagen.
El scroll debe usar el ScrollArea compartido (nunca overflow-auto), y se prueba en WebKitGTK, no en Chrome (Arquitectura del frontend).
src/lib/image-url.ts construye las URL; src-tauri/src/protocol.rs las sirve (registrado con register_asynchronous_uri_scheme_protocol en src-tauri/src/lib.rs). La base es convertFileSrc("", "pbimg") (pbimg://localhost/ en Linux y macOS, http://pbimg.localhost/ en Windows), con pbimg://localhost/ como alternativa.
| Ruta | Constructor en image-url.ts
|
Respuesta | Se usa para |
|---|---|---|---|
/r/<sha256>/<params base64url>/<width> |
urlMiniatura(blob, params, width) |
JPEG | Miniatura procesada de un blob (Ruta::Render) |
/t/<sha256 of URL> |
urlBusqueda(huella) |
JPEG/PNG | Miniatura de un resultado de búsqueda de Scryfall descargada por Rust y cacheada en renders/t/
|
/d/<huella>/<width> |
urlDiseno |
PNG | Vista previa de un diseño registrado (design_register) |
/c/<huella>/<width> |
urlCapa |
PNG | El diseño sin la ilustración (design_overlay) |
/b/<sha256>/<side> |
urlArte(blob, 1000 | 2000) |
imagen | Una ilustración reducida del almacén de blobs |
/x/<pack>/<n> |
urlExtra |
PNG | Una imagen extra de un pack de marcos descargable |
/f/<huella>.woff2 |
(raizProtocolo()) |
letra | Letras de los temas instalados |
leer_ruta valida de forma estricta: los hashes deben ser de 64 caracteres hex (es_huella), los anchos deben ser uno de ANCHOS = [146, 244, 488, 672, 745, 1000] (reflejado en image-url.ts), los parámetros como máximo de 2048 caracteres. Una entrada incorrecta da 400, las rutas desconocidas 404, los blobs que faltan 404 y los demás errores 500. Las respuestas correctas llevan Cache-Control: max-age=31536000, immutable: la URL contiene todo lo que cambia la imagen, así que se puede cachear para siempre.
PreviewParams (src-tauri/src/render.rs, exportado a TS): { edits, edge, back, bleedMm, bleedMode, fillCorners, card }, en JSON y luego base64url, construido por parametros(settings, edits, isBack, bleedMm). Rust decide si el oscurecido de bordes se aplica a esta imagen (origen y bandera de reverso) con el mismo edge_for que la exportación, de modo que la pantalla y el PDF no pueden discrepar. SlotView sustituye card por el tamaño de corte propio de la casilla para que las cartas grandes se rendericen a su tamaño. fillCorners es #[serde(default)], así que las URL que no lo llevan siguen siendo válidas.
- El manejador
atenderdeESQUEMAtoma un permiso deAppState.renders(untokio::Semaphoredimensionado aavailable_parallelism), de modo que una pantalla llena de miniaturas no pueda saturar la máquina. -
AppState::miniatura(blob, params, width)(render.rs): lee el blob, construyespec_de_miniatura(Width(width), JPEG calidad 85,edits,bleedMm) y buscaapp_cache_dir/renders/<spec.key()>.jpg; si no está, ejecutaimaging::pipeline::rendery escribe la caché de forma atómica. - El ancho pedido es el ancho del corte; la imagen real lleva además los píxeles de sangrado (
round(bleedMm * px_per_mm)por lado).
Por qué existe un directorio de caché: renders/ guarda solo datos derivados, así que se puede borrar sin pérdida; las imágenes de los mazos viven en el almacén de blobs (Almacenamiento y blobs).
anchoPara(mmCarta, escala, tope) en preview-pane.tsx: píxeles necesarios = cardWidthMm * escala * devicePixelRatio; se elige la entrada más pequeña de ANCHOS que los cubra, limitada por la preferencia del usuario «Calidad de la vista previa» (ui-store.calidadVista, 488 por defecto, se cambia en el panel de la lista). Un ancho distinto es una clave de caché distinta, así que al hacer zoom se vuelve a renderizar al nuevo tamaño de forma perezosa.
useUi.zoom (0,8 por defecto, limitado a ZOOM_MIN = 0.4 .. ZOOM_MAX = 2.5, redondeado a 2 decimales, persistido en localStorage bajo la clave de las preferencias). Controles: botones de menos/más, un campo numérico (porcentaje), volver al 100 % y Ctrl/Cmd + rueda (useZoomConRueda): pasos del 5 %, los clics de la rueda del ratón cuentan como un paso, los trackpads acumulan hasta 40 unidades; el listener de la rueda no es pasivo para poder cancelar el zoom propio del navegador, y los desplazamientos de scroll se corrigen en requestAnimationFrame para que el punto bajo el cursor se quede quieto.
- La selección vive en
ui-store(seleccion: string[],ancla).seleccionarConClic(acciones-carta.ts): el clic selecciona una; Ctrl/Cmd alterna; Shift selecciona el rango desde el ancla en el orden del mazo (con Ctrl, lo añade a la selección). El doble clic abre «Editar carta» (Diálogo Editar carta). Las listas de estadísticas también pueden seleccionar cartas (seleccionarEnDeck). - Teclado (
useTeclado, se ignora dentro de campos, diálogos y menús): Supr elimina la selección, Ctrl+D duplica, Ctrl+A selecciona todo, Escape la vacía. - Menú contextual (
slot-menu.tsx,SlotMenu): actúa sobre la selección si la carta pulsada está en ella; si no, selecciona solo esa carta. Elementos: en una única casilla en blanco, «Añadir carta de Scryfall…» el primero (anadirEnHueco); Duplicar; Editar carta; Cambiar variante y Cambiar carta (cartas de Scryfall); Diseñar proxy / Editar diseño / Quitar diseño; Vincular las seleccionadas… y vincular/revincular (cartas propias); Idioma de la imagen; Marca; Reemplazar imagen; Cambiar reverso (elegir imagen / usar el reverso del deck); Exportar imagen (Frente… / Reverso…,exportarImagen: diálogo de guardar con filtros PNG, JPEG, WebP, TIFF y BMP, manda la extensión; véase Exportación); Mover antes / después / a posición (MoverADialog); Fijar/Desfijar; Eliminar. El menú se pinta conContenidoContextual(src/components/contenido-contextual.tsx), que ignorapointerupyclickdurante los primeros 250 ms tras abrirse: el menú sale bajo el cursor (y se desplaza para caber en la ventana), así que al soltar el botón derecho que lo abrió una opción recibía elpointerupy Radix lo tomaba por un clic, ejecutando una acción sin querer.src/components/ui/es de solo lectura, de ahí el envoltorio deContextMenuContent; el teclado no se ve afectado. La hora de apertura la toma una referencia estable de callback (useCallback) que se ejecuta cuando Radix monta el nodo del contenido: el envoltorio se pinta con el menú aún cerrado (solo el contenido de Radix espera a abrirse), así que marcar la hora al pintarse dejaba la pausa sin efecto, y un callback nuevo en cada pintado la volvería a marcar. El menú de deck de la Biblioteca y los menús de la mesa de juego usan el mismo envoltorio (Biblioteca de mazos, Mesa de juego). - Toda mutación pasa por el store del mazo (
setCartas), así que se autoguarda y se puede deshacer; después el plan se recalcula. Las operaciones puras sobre listas están ensrc/lib/deck-ops.ts(moverGrupo,duplicar,quitar,huecos,actualizar). - Arrastrar y soltar entre cartas usa
@dnd-kit/corecon unPointerSensorque necesita 6 px de movimiento. CadaSlotViewes a la vez arrastrable y soltable (id = id de carta); las celdas vacías son soltables con idvacia:<x>:<y>. Al soltar,soltarSobre(ids, destino)mueve el grupo arrastrado (toda la selección si la carta arrastrada está en ella, medianteobjetivo): los arrastres hacia delante caen después del destino, los arrastres hacia atrás antes, y una celda vacía los envía al final. UnDragOverlaymuestra la miniatura de la carta ligeramente girada, con una insignia de cantidad para los grupos. Deducido: con «Pares DFC intercalados», el anverso y su casilladfcFacecomparten el mismo id de carta, así que ambos comparten un único id de arrastre/soltado; compruébalo antes de fiarte de los arrastres en ese modo. - Ficheros soltados desde el sistema operativo (
arrastrar-ficheros.ts,useArrastrarFicheros): elonDragDropEventde Tauri da posiciones en píxeles físicos;soltarFicherosconvierte pordevicePixelRatioy encuentra la carta bajo el cursor mediante[data-carta]; si se sueltan sobre una carta, la primera imagen sustituye el anverso de esa carta (reemplazarConFichero->blob_import_files, la carta pasa a ser propia, el recorte y la rotación se restablecen); si se sueltan en otro sitio, todas las imágenes se añaden como cartas propias. Solo cuentanpng/jpg/jpeg/webp(EXTENSIONES_IMAGEN). - «Añadir página» añade una página de cartas en blanco (
anadirPagina(cols*rows)); «Auto ordenar» llama aarrange_cards(Motor de maquetación); la barra de herramientas tiene además el filtro de DFC, «Info de carta», «Ver reversos», «Estadísticas» y «Añadir carta».
src/features/preview/__tests__/acciones-carta.test.ts (semántica de mover/soltar, deshacer, selección, el orden automático aplica el orden de Rust), arrastrar-ficheros.test.ts (filtrado de extensiones, conversión de DPR, comportamiento al soltar, limpieza), src/stores/__tests__/plan-store.test.ts; Rust: tests de render.rs y protocol.rs.
- La vista previa muestra solo las hojas de anverso del plan; no busques hojas de reversos en el DOM.
- Nunca calcules posiciones en los componentes: si algo está en el sitio equivocado, arregla
layout::plan. - Una nueva ruta en
protocol.rsdebe validar su entrada como las demás (forma del hash, tamaños permitidos) porque es accesible desde el webview. - Si cambias algo que altere los píxeles renderizados, sube
PIPELINE_VERSION(Imagen y PDF); si no,renders/sirve ficheros obsoletos y la cabeceraimmutablemantiene las URL antiguas cacheadas en el webview.
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