# Arquitectura del backend
Este documento describe cómo se organiza el backend: los componentes que lo forman,
el flujo de una conversión, la API que usa el frontend, el modelo de datos, la
estructura del código y el entorno Docker.
## 1. Visión general
Plano3D sigue una arquitectura cliente-servidor. La aplicación de escritorio
(frontend, de Joel) es el cliente y se comunica con el backend mediante una API REST
que intercambia JSON. El backend se ejecuta en contenedores Docker: uno para la
aplicación Python (servidor y motor) y otro para MySQL, más un volumen donde se
guardan los archivos.
```mermaid
flowchart LR
APP[Frontend
aplicación de escritorio] -- HTTP + JSON
token JWT --> API
subgraph Contenedor backend
API[API REST
FastAPI] --> SEG[Seguridad
JWT, bcrypt]
API --> SRV[Servicios]
SRV --> REP[Repositorios
SQLAlchemy]
SRV --> MOT[Motor
motor.api.procesar]
end
REP --> DB[(MySQL)]
SRV --> VOL[/Volumen de archivos
DXF, STEP, STL/]
```
| Componente | Responsabilidad |
| --- | --- |
| API REST (FastAPI + Uvicorn) | Recibe las peticiones del frontend, valida los datos y devuelve las respuestas. |
| Seguridad | Registro, inicio de sesión, hash de contraseñas y verificación de los tokens JWT. |
| Servicios | Lógica de negocio: proyectos, lanzamiento de conversiones en segundo plano, guardado de resultados. |
| Motor (`motor/`) | Convierte el plano en pieza: lectura, vistas, reconstrucción, verificación y exportación. Se usa solo a través de `motor/api.py`. |
| Repositorios (SQLAlchemy) | Todas las consultas a la base de datos. |
| MySQL | Usuarios, proyectos, conversiones, avisos y metadatos de los archivos. |
| Volumen de archivos | Guarda los DXF, STEP y STL. En la base de datos solo se guarda su ruta. |
### Dos contratos
- **API REST** (servidor ↔ frontend). Es lo único que Joel necesita conocer del
backend. Se documenta sola en `/docs` (Swagger) y cualquier cambio se acuerda
antes.
- **`motor/api.py`** (servidor ↔ motor). Es interno del backend: el servidor llama
a `procesar()` y `exportar()`, y guarda el `Resultado` en la base de datos.
Mientras el motor real no esté terminado, `procesar()` devuelve el motor simulado
(CUBE01), de modo que el servidor y el frontend pueden avanzar en paralelo.
## 2. Flujo de una conversión
La conversión puede tardar varios segundos, así que no se hace dentro de la propia
petición. La API responde enseguida con el identificador de la conversión y el
trabajo sigue en segundo plano. El frontend consulta el estado hasta que termina y
entonces descarga los archivos.
1. **Subida.** El frontend envía el plano y los parámetros (escala, espesor para
el plan B, alternativa).
2. **Validación.** Se comprueba la extensión, que no supere 20 MB y que se pueda
abrir. Si falla, se responde 400 con el motivo.
3. **Registro.** Se guarda el plano en el volumen, se crea la conversión en estado
PENDIENTE y se devuelve su identificador (202).
4. **Procesado** (en segundo plano, estado PROCESANDO). El servicio llama a
`motor.api.procesar()`, que ejecuta las etapas del motor:
1. lectura del plano, unidades y escala;
2. separación e identificación de alzado, planta y perfil (con una sola vista,
plan B: extrusión);
3. sistema de proyección ISO-E / ISO-A;
4. reconstrucción: celdas y búsqueda con CP-SAT;
5. verificación: proyectar las aristas y compararlas con el plano.
5. **Exportación.** Si hay sólido, se generan el STEP y el STL con
`motor.api.exportar()` y se guardan en el volumen.
6. **Registro final.** Se guardan en MySQL el estado del resultado (OK,
CON_AVISOS, IMPOSIBLE), los avisos, la escala aplicada, el número de
alternativas y los archivos. La conversión pasa a COMPLETADA, o a ERROR si el
resultado es IMPOSIBLE o ha fallado algo inesperado.
7. **Descarga.** El frontend consulta el estado, muestra los avisos y descarga los
archivos.
## 3. API REST
Todos los endpoints, salvo el registro y el inicio de sesión, piden la cabecera
`Authorization: Bearer `.
| Método | Ruta | Descripción |
| --- | --- | --- |
| POST | `/auth/registro` | Crear una cuenta. |
| POST | `/auth/login` | Iniciar sesión y obtener el token JWT. |
| GET | `/proyectos` | Listar los proyectos del usuario. |
| POST | `/proyectos` | Crear un proyecto subiendo un plano. |
| GET | `/proyectos/{id}` | Consultar un proyecto y su historial de conversiones. |
| DELETE | `/proyectos/{id}` | Eliminar un proyecto y sus archivos. |
| POST | `/proyectos/{id}/conversiones` | Lanzar una conversión con sus parámetros. |
| GET | `/conversiones/{id}` | Consultar el estado, el resultado y los avisos de una conversión. |
| GET | `/archivos/{id}` | Descargar un archivo (plano, STEP o STL). |
Ejemplo de respuesta de `GET /conversiones/{id}`:
```json
{
"id": 12,
"estado": "COMPLETADA",
"resultado": "CON_AVISOS",
"escala": 2.0,
"alternativas": 1,
"avisos": [
{"codigo": "SIN_COTAS", "gravedad": "aviso",
"mensaje": "El plano no tiene cotas: las medidas se deducen de la geometría y la escala.",
"vista": null},
{"codigo": "PROYECCION_MIXTA", "gravedad": "aviso",
"mensaje": "Sistemas de proyección mezclados: ...", "vista": "perfil"}
],
"archivos": [{"id": 31, "tipo": "STEP"}, {"id": 32, "tipo": "STL"}]
}
```
### Códigos de respuesta
| Código | Significado |
| --- | --- |
| 200 / 201 | Operación correcta / recurso creado. |
| 202 | Conversión aceptada y en proceso. |
| 400 | Archivo o datos no válidos (se indica el motivo). |
| 401 | Falta el token o no es válido. |
| 404 | El recurso no existe o es de otro usuario. |
| 413 | El archivo supera el tamaño máximo. |
## 4. Modelo de datos
```mermaid
erDiagram
USUARIO ||--o{ PROYECTO : tiene
PROYECTO ||--o{ CONVERSION : tiene
PROYECTO ||--o{ ARCHIVO : guarda
CONVERSION ||--o{ AVISO : genera
CONVERSION |o--o{ ARCHIVO : produce
```
| Tabla | Campos principales |
| --- | --- |
| **USUARIO** | id, correo (único), hash de la contraseña, fecha de alta |
| **PROYECTO** | id, usuario_id, nombre, fecha de creación |
| **CONVERSION** | id, proyecto_id, parámetros (escala, espesor, alternativa), estado (PENDIENTE, PROCESANDO, COMPLETADA, ERROR), resultado (OK, CON_AVISOS, IMPOSIBLE), escala aplicada, alternativas, duración, fechas |
| **AVISO** | id, conversion_id, código, gravedad, mensaje, vista |
| **ARCHIVO** | id, proyecto_id, conversion_id (nulo para el plano original), tipo (DXF, DWG, PDF, STEP, STL), ruta, tamaño, hash SHA-256 |
## 5. Estructura del código
Se añade la carpeta `servidor/` al repositorio. El motor sigue en `motor/` y el
frontend en `app/`.
```
plano3d/
├── servidor/
│ ├── main.py # arranque de FastAPI
│ ├── api/routers/ # auth, proyectos, conversiones, archivos
│ ├── schemas/ # modelos Pydantic (entrada/salida)
│ ├── services/ # proyectos, conversiones (llama a motor.api)
│ ├── repositories/ # consultas a la base de datos
│ ├── models/ # tablas SQLAlchemy
│ └── core/ # configuración y seguridad (JWT, bcrypt)
├── motor/
│ ├── api.py # contrato servidor ↔ motor
│ ├── simulado.py # motor simulado (CUBE01)
│ ├── procesado.py # orquesta las etapas
│ ├── lectura.py # DXF a segmentos, textos y cotas
│ ├── escala.py # unidades y escala
│ ├── vistas.py # alzado, planta y perfil
│ ├── proyeccion.py # ISO-E / ISO-A
│ ├── reconstruccion.py # celdas y CP-SAT
│ ├── pieza_plana.py # plan B: extrusión
│ ├── verificacion.py # comparar el sólido con el plano
│ └── avisos.py # catálogo de códigos de aviso
├── alembic/ # migraciones de la base de datos
├── pruebas/ # pytest: motor, catálogo y API
├── Dockerfile
├── docker-compose.yml
└── .env.example # variables necesarias, sin valores reales
```
Las rutas no tienen lógica de negocio ni acceden a la base de datos. Las
excepciones del motor se capturan en el servicio de conversiones: la conversión
queda en ERROR con el motivo y el servidor sigue funcionando.
## 6. Catálogo de avisos
Los códigos son estables: el frontend puede usarlos para mostrar ayuda o resaltar
la vista afectada. Los dos primeros ya aparecen en el motor simulado. El resto es
una propuesta que hay que acordar con Joel.
| Código | Gravedad | Cuándo |
| --- | --- | --- |
| `SIN_COTAS` | Aviso | El plano no tiene cotas |
| `PROYECCION_MIXTA` | Aviso | Planta y perfil siguen sistemas de proyección distintos |
| `ESCALA_DETECTADA` | Info | Se ha leído la escala del cajetín |
| `SIN_ESCALA` | Aviso | No hay escala ni en el plano ni en los parámetros |
| `SIN_UNIDADES` | Info | El DXF no define unidades; se suponen mm |
| `ARCHIVO_NO_VALIDO` | Bloqueante | No se puede abrir el archivo |
| `VISTAS_NO_IDENTIFICADAS` | Bloqueante | No se encuentran alzado, planta y perfil alineados |
| `SIN_PERFIL_CERRADO` | Bloqueante | Plan B: no hay ningún contorno cerrado |
| `ENTIDAD_NO_SOPORTADA` | Aviso | Hay *splines* u otras entidades que se ignoran |
| `ROSCA` | Info | Se ha modelado una rosca como agujero |
| `PLANO_AMBIGUO` | Aviso | Varias piezas reproducen el plano |
| `NO_REPRODUCE` | Aviso | La proyección del sólido no coincide del todo con el plano |
| `COTA_INCOHERENTE` | Aviso | Una cota no coincide con la geometría |
## 7. Entorno Docker
`docker-compose.yml` define tres servicios:
| Servicio | Qué hace |
| --- | --- |
| `mysql` | MySQL 8 con un volumen para sus datos. |
| `backend` | La imagen del backend ejecutando Uvicorn en el puerto 8000. La documentación de la API queda en `http://localhost:8000/docs`. |
| `pruebas` | La misma imagen, ejecutando `pytest` contra una base de datos de pruebas que se crea y se borra en cada ejecución. |
Además hay un volumen para los archivos generados. Las credenciales se leen de un
archivo `.env` que no se sube al repositorio; en él se incluye `.env.example` con
los nombres de las variables.
La imagen parte de `python:3.12-slim`, instala las bibliotecas del sistema que
necesita OpenCASCADE (`libgl1`, `libglib2.0-0`, `libxrender1`) y las dependencias
con `uv sync --frozen`, usando el mismo `uv.lock` que en local. Así las pruebas
dan el mismo resultado en el contenedor y en cualquier equipo.
```bash
docker compose up -d # levantar backend y MySQL
docker compose run --rm pruebas # ejecutar todas las pruebas
docker compose run --rm pruebas pytest pruebas/test_contrato.py # una sola
```
## 8. Pruebas
- `pruebas/test_contrato.py` (ya existe): comprueba el contrato `motor/api.py`.
- Una prueba por pieza del catálogo (`pruebas/catalogo/`): volumen esperado y
códigos de aviso esperados.
- Pruebas unitarias de cada etapa del motor con planos pequeños generados en la
prueba.
- Pruebas de la API con el `TestClient` de FastAPI: registro e inicio de sesión,
acceso sin token (401), acceso a proyectos de otro usuario (404), subida de un
archivo no válido (400), y una conversión completa de CUBE01 hasta la descarga.