Añade la propuesta del backend con servidor, cuentas y Docker

This commit is contained in:
Lizette 2026-11-06 19:01:20 +01:00
parent 4503df96bd
commit a43635ac37
8 changed files with 606 additions and 0 deletions

View File

@ -0,0 +1,83 @@
# Requisitos del backend
Este documento recoge los requisitos del **backend** de Plano3D: el servidor (API
REST, cuentas, historial y base de datos) y el motor que convierte el plano técnico
2D en una pieza 3D. La interfaz y el visor 3D son del frontend y no se incluyen aquí.
Cada requisito tiene un identificador para poder citarlo desde el resto de la
documentación, los *issues* y las pruebas. La prioridad indica su importancia para
la primera versión: **Alta** (imprescindible), **Media** (deseable) o **Baja**
(opcional).
## 1. Requisitos funcionales
### Cuentas y proyectos
| ID | Nombre | Descripción | Prioridad |
| --- | --- | --- | --- |
| RF-01 | Registro de usuario | El usuario crea una cuenta con correo electrónico y contraseña. | Alta |
| RF-02 | Inicio de sesión | El usuario se autentica y recibe un token que envía en el resto de peticiones. | Alta |
| RF-03 | Gestión de proyectos | El usuario crea, lista, consulta y elimina sus proyectos. Solo ve los suyos. | Alta |
| RF-04 | Subida del plano | La API recibe un plano (DXF; DWG y PDF vectorial en la ampliación) asociado a un proyecto. | Alta |
| RF-05 | Validación del archivo | Antes de procesar se comprueban la extensión, el tamaño máximo y que el archivo se puede abrir. Si no, se rechaza explicando el motivo. | Alta |
### Conversión (motor)
| ID | Nombre | Descripción | Prioridad |
| --- | --- | --- | --- |
| RF-06 | Lectura del plano | Se extraen las entidades geométricas (LINE, ARC, CIRCLE, LWPOLYLINE con *bulge*, POLYLINE), se separan las líneas visibles de las ocultas y se ignoran el marco, el cajetín, los textos y las cotas. | Alta |
| RF-07 | Unidades y escala | Se pasa todo a mm según `$INSUNITS` y se aplica la escala del cajetín, o la que indique el usuario. Si falta, se avisa. | Alta |
| RF-08 | Vistas | Se agrupan las líneas en vistas y se identifican alzado, planta y perfil. | Alta |
| RF-09 | Sistema de proyección | Se detecta si el plano sigue el sistema europeo (ISO-E) o el americano (ISO-A), y se avisa si los mezcla. | Alta |
| RF-10 | Reconstrucción | Se genera el sólido cuyas tres proyecciones reproducen el plano: caras planas, chaflanes, agujeros pasantes y roscas (como agujero y nota). | Alta |
| RF-11 | Geometría ampliada | Agujeros ciegos, avellanados y cajeados, y piezas de revolución. | Media |
| RF-12 | Plan B: pieza plana | Si el plano tiene una sola vista, se extruye el contorno exterior con el espesor indicado y se restan los agujeros. | Alta |
| RF-13 | Verificación del plano | Se proyectan las aristas del sólido y se comparan con el plano, y se comprueban las cotas. Cada diferencia genera un aviso. | Alta |
| RF-14 | Avisos | Cada problema se devuelve con un código estable, un mensaje comprensible, una gravedad (info, aviso o bloqueante) y la vista afectada. | Alta |
| RF-15 | Alternativas | Si varias piezas reproducen el plano, se indica cuántas hay y el usuario puede elegir una. | Media |
| RF-16 | Exportación | La pieza se exporta a STEP (para CAD) y STL (para la vista previa del frontend). | Alta |
### Historial y archivos
| ID | Nombre | Descripción | Prioridad |
| --- | --- | --- | --- |
| RF-17 | Conversión en segundo plano | La API responde enseguida con el identificador de la conversión, y el trabajo se hace en segundo plano. | Alta |
| RF-18 | Estado de la conversión | Cada conversión tiene un estado (pendiente, procesando, completada o error) que el frontend puede consultar. | Alta |
| RF-19 | Historial | Se guardan en MySQL todas las conversiones de cada proyecto, con sus parámetros, el resultado, los avisos y los archivos generados. | Alta |
| RF-20 | Repetir una conversión | El usuario puede volver a convertir un plano con otros parámetros (escala, alternativa, espesor) sin subirlo de nuevo. | Media |
| RF-21 | Descarga de archivos | El usuario descarga el plano original y los STEP/STL de sus proyectos. | Alta |
## 2. Requisitos no funcionales
| ID | Categoría | Descripción |
| --- | --- | --- |
| RNF-01 | Seguridad | Las contraseñas se guardan con bcrypt, nunca en texto plano. |
| RNF-02 | Seguridad | Todos los endpoints, salvo el registro y el inicio de sesión, piden un token JWT válido y con caducidad. |
| RNF-03 | Seguridad | Un usuario no puede ver ni descargar proyectos ni archivos de otro usuario. |
| RNF-04 | Seguridad | Las credenciales (base de datos, clave JWT) van en variables de entorno (archivo `.env`) y no se suben al repositorio. |
| RNF-05 | Fiabilidad | Nunca se entrega una pieza que no reproduce el plano sin avisarlo. Lo que no se soporta se detecta y se avisa. |
| RNF-06 | Robustez | Un plano erróneo no tumba el servidor ni afecta a otras peticiones: la conversión queda en estado de error, con el motivo. |
| RNF-07 | Robustez | El tamaño máximo de archivo admitido es de 20 MB. |
| RNF-08 | Rendimiento | Una pieza del catálogo de pruebas se convierte en menos de 10 segundos. |
| RNF-09 | Mantenibilidad | El servidor se organiza en capas (rutas, servicios, repositorios y modelos) y el motor en etapas. Todo sigue PEP 8. |
| RNF-10 | Calidad | Hay pruebas automáticas (`pytest`) del motor, con el catálogo de piezas, y de la API. Pasan antes de cada *pull request*. |
| RNF-11 | Portabilidad | El backend y la base de datos se levantan con un solo comando (`docker compose up`), y las pruebas se ejecutan en el mismo entorno Docker en cualquier equipo. |
| RNF-12 | Documentación | La API se documenta automáticamente con OpenAPI/Swagger, y es el contrato con el frontend. |
| RNF-13 | Trazabilidad | Las operaciones y los errores se registran en un log. |
*Los valores numéricos (20 MB, 10 segundos) son objetivos iniciales y se ajustarán con las pruebas.*
## 3. Restricciones y supuestos
- Piezas mecánicas. No se admiten planos de arquitectura.
- La reconstrucción completa necesita tres vistas ortogonales alineadas. Con una sola vista se aplica el plan B (RF-12).
- No se reconstruyen superficies libres ni *splines*: se detectan y se avisa.
- La lectura de DWG depende de un conversor externo instalado en la imagen Docker.
## 4. Ampliación futura
| ID | Nombre | Descripción | Prioridad |
| --- | --- | --- | --- |
| RF-22 | PDF vectorial | Se extraen las líneas de un PDF vectorial y se procesan como un DXF. | Media |
| RF-23 | DWG | Los DWG se convierten a DXF con un conversor externo. | Media |
| RF-24 | PDF escaneado y OCR | Se detectan líneas y cotas en planos escaneados. | Baja |

View File

@ -0,0 +1,80 @@
# Tecnologías del backend
Este documento explica qué tecnologías usa el backend (servidor y motor) y por qué
se han elegido frente a otras alternativas.
## 1. Lenguaje
El backend se desarrolla en **Python 3.12**. CadQuery solo admite de 3.12 a 3.13,
como fija el `pyproject.toml`.
La razón principal es que Python tiene bibliotecas maduras y gratuitas para leer
archivos CAD y generar sólidos 3D. En Java o C#, lo equivalente es mucho más
limitado o necesita licencias comerciales. Además, permite crear APIs rápidamente.
## 2. Bibliotecas
### 2.1. Motor: geometría y modelado
| Tecnología | Para qué se usa | Por qué esta y no otra |
| --- | --- | --- |
| **ezdxf** | Leer las entidades, capas, tipos de línea, textos y cabecera de los DXF. | Es la biblioteca de referencia en Python para DXF: admite todas las versiones, está bien documentada y no necesita AutoCAD. |
| **shapely** | Geometría 2D de las vistas: unir segmentos, formar contornos, calcular áreas y saber si un contorno está dentro de otro. | Resuelve en una línea operaciones que a mano serían largas y darían problemas de tolerancia. |
| **numpy** | Cálculos con coordenadas y tolerancias. | Estándar en Python, y ya lo usa CadQuery. |
| **CadQuery** (OpenCASCADE) | Construir los sólidos (celdas, extrusiones, booleanas), proyectar aristas y exportar a STEP y STL. | Usa OpenCASCADE, el mismo núcleo que FreeCAD, con una API en Python más sencilla. pythonOCC ofrece lo mismo con un código más complejo, y FreeCAD es pesado para usarlo dentro de un servidor. |
| **OR-Tools (CP-SAT)** | Elegir qué celdas forman la pieza para que sus proyecciones coincidan con el plano. | El prototipo prueba todas las combinaciones, y eso no funciona a partir de unas 18 celdas. CP-SAT resuelve el mismo problema como restricciones. |
### 2.2. Servidor: API, seguridad y base de datos
| Tecnología | Para qué se usa | Por qué esta y no otra |
| --- | --- | --- |
| **FastAPI** | Crear la API REST con la que se comunica el frontend. | Más moderna y rápida que Flask, valida los datos sola y genera la documentación Swagger sin trabajo extra. Django sería excesivo para una API. |
| **Uvicorn** | Servidor que ejecuta FastAPI. | Es el recomendado por FastAPI. |
| **Pydantic** | Definir y validar los datos que entran y salen de la API. | Viene integrado con FastAPI. |
| **SQLAlchemy** | Acceder a MySQL mediante un ORM. | Es el ORM más usado en Python y evita escribir SQL a mano en cada consulta. |
| **PyMySQL** | Conectar SQLAlchemy con MySQL. | Está escrito en Python puro y se instala sin problemas en Docker. |
| **Alembic** | Gestionar las migraciones de la base de datos. | Es la herramienta oficial de migraciones de SQLAlchemy. |
| **passlib (bcrypt)** | Calcular el hash de las contraseñas. | bcrypt es lento por diseño, lo que dificulta los ataques de fuerza bruta. |
| **PyJWT** | Generar y verificar los tokens de sesión. | Permite una autenticación sin estado, adecuada para una API REST. |
### 2.3. Pruebas
| Tecnología | Para qué se usa | Por qué esta y no otra |
| --- | --- | --- |
| **pytest** | Pruebas del motor (catálogo de piezas) y de la API. | Es el estándar en Python, con una sintaxis más sencilla que *unittest*. |
| **httpx** (`TestClient` de FastAPI) | Llamar a los endpoints desde las pruebas sin levantar el servidor. | Es lo que usa FastAPI para probar. |
## 3. Base de datos
Se usa **MySQL 8** para guardar los usuarios (con la contraseña cifrada), los
proyectos, el historial de conversiones con sus avisos y las rutas de los archivos
generados.
Los archivos en sí (DXF, STEP, STL) no se guardan en la base de datos, sino en un
volumen de Docker, porque pueden ser grandes.
Se ha elegido MySQL porque el equipo ya lo conoce del ciclo y cubre de sobra las
necesidades del proyecto. PostgreSQL sería una alternativa igual de válida.
## 4. Herramientas de desarrollo
| Herramienta | Uso en el proyecto |
| --- | --- |
| **uv** | Gestionar las dependencias con `pyproject.toml` y `uv.lock`, tanto en local como dentro de la imagen Docker, para tener las mismas versiones en todas partes. |
| **Docker** | Ejecutar el backend con todas sus dependencias. CadQuery es especialmente delicado de instalar, y el contenedor evita problemas entre equipos. |
| **Docker Compose** | Levantar juntos el backend y MySQL, y lanzar las pruebas contra una base de datos de pruebas, con un solo comando. |
| **Git y Gitea del instituto** | Control de versiones, ramas por tarea, *pull requests* revisados, *issues* y tablero, según `CONTRIBUTING.md`. |
| **Visual Studio Code** | Editor y depurador, con integración de Python, Git y Docker. |
| **Swagger UI** | Probar los endpoints desde el navegador. Lo genera FastAPI en `/docs`. |
| **FreeCAD / Fusion 360** | Abrir los STEP generados y comprobar que la pieza es correcta. |
## 5. Ampliación futura
| Tecnología | Para qué se usará |
| --- | --- |
| **PyMuPDF** | Extraer los trazos vectoriales de los planos en PDF. |
| **OpenCV** y **Tesseract OCR** | Detectar líneas y leer cotas en planos escaneados. |
*Riesgo identificado: reconocer cotas con OCR en planos reales es poco fiable
(símbolos especiales, textos girados, solapamientos). Por eso queda como
ampliación y no como parte del producto mínimo.*

View File

@ -0,0 +1,239 @@
# 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<br>aplicación de escritorio] -- HTTP + JSON<br>token JWT --> API
subgraph Contenedor backend
API[API REST<br>FastAPI] --> SEG[Seguridad<br>JWT, bcrypt]
API --> SRV[Servicios]
SRV --> REP[Repositorios<br>SQLAlchemy]
SRV --> MOT[Motor<br>motor.api.procesar]
end
REP --> DB[(MySQL)]
SRV --> VOL[/Volumen de archivos<br>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 <token>`.
| 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.

View File

@ -0,0 +1,94 @@
# Prueba técnica: pieza plana por extrusión (plan B)
La viabilidad de la reconstrucción desde tres vistas ya la demostró
`prototipo/reconstruir.py` con CUBE01. Esta segunda prueba comprueba el **plan B**
acordado en las decisiones: si la reconstrucción completa no llega a tiempo, o el
plano tiene una sola vista, generar una pieza plana extruyendo su contorno con un
espesor.
También sirve para probar la lectura de contornos cerrados y agujeros con ezdxf y
la exportación a STEP y STL con CadQuery, que se reutilizan en el motor.
Los scripts están en `prototipo/lizette/`.
## 1. Objetivo
- Comprobar que ezdxf lee bien las polilíneas cerradas y los círculos.
- Comprobar que CadQuery genera el sólido por extrusión y resta los agujeros.
- Comprobar que el STEP y el STL generados se abren bien en un programa CAD.
## 2. Alcance
Es un script independiente: no usa la API ni la base de datos, ni `motor/api.py`. Admite polilíneas cerradas
y círculos. El contorno de mayor área se toma como exterior y el resto se restan
como agujeros.
Quedan fuera, y se resolverán en `motor/pieza_plana.py`:
- Los arcos dentro de las polilíneas (*bulge*).
- Unir líneas y arcos sueltos en contornos cerrados.
- Comprobar que los agujeros están dentro del contorno exterior.
- Las unidades.
## 3. Pieza de prueba
Una placa rectangular de 100 × 60 mm con dos taladros de Ø20 mm, generada con
`prototipo/lizette/pieza_prueba.py`.
**[ Captura de `placa.dxf` abierto en LibreCAD o AutoCAD ]**
*Figura 1. Plano 2D de entrada*
## 4. Ejecución
En local, con el entorno del proyecto (`uv sync`):
```bash
cd prototipo/lizette
uv run python pieza_prueba.py
uv run python conversion.py placa.dxf 10
```
O dentro del contenedor del backend, sin instalar nada en el equipo:
```bash
docker compose run --rm -w /app/prototipo/lizette pruebas python pieza_prueba.py
docker compose run --rm -w /app/prototipo/lizette pruebas python conversion.py placa.dxf 10
```
`conversion.py` recibe el DXF y el espesor en mm, y genera `resultado.step` y
`resultado.stl`.
**[ Captura de la terminal con la salida del script ]**
*Figura 2. Ejecución de la prueba*
## 5. Resultado
**[ Captura de `resultado.step` abierto en FreeCAD o Fusion 360 ]**
*Figura 3. Placa de 10 mm de espesor con dos taladros*
Volumen esperado: 100 × 60 × 10 − 2 × π × 10² × 10 ≈ **53 717 mm³**. Se puede
comprobar en FreeCAD o con `solido.val().Volume()`.
## 6. Conclusiones
**[ Completar tras ejecutar: ¿se ha generado bien?, ¿coincide el volumen?, ¿cuánto ha tardado?, ¿algún problema? ]**
Siguientes pasos para llevarlo al motor (`motor/pieza_plana.py`):
1. Leer los arcos de las polilíneas (*bulge*) y unir líneas y arcos sueltos en
contornos (con shapely).
2. Comprobar que cada agujero está dentro del contorno exterior y avisar si no.
3. Convertir las unidades a mm.
4. Devolver un `Resultado` del contrato, con el aviso `SIN_PERFIL_CERRADO` en lugar
de una excepción.
5. Añadir la placa al catálogo (`pruebas/catalogo/PLACA01/`) con su prueba
automática.
6. Probar la conversión completa desde la API: subir `placa.dxf` en Swagger
(`/docs`), consultar el estado y descargar el STEP.
**[ Captura de Swagger UI (http://localhost:8000/docs) con los endpoints ]**
*Figura 4. Endpoints de la API en Swagger*

View File

@ -0,0 +1,40 @@
# Propuesta de Lizette: backend (servidor y motor)
Documentación de la parte del **backend**: el servidor (API REST, cuentas, historial
y base de datos) y el motor de procesado que convierte el plano en pieza 3D.
| Documento | Contenido |
| --- | --- |
| [01-requisitos-backend.md](01-requisitos-backend.md) | Requisitos funcionales y no funcionales del backend |
| [02-tecnologias-backend.md](02-tecnologias-backend.md) | Tecnologías elegidas y por qué |
| [03-arquitectura-backend.md](03-arquitectura-backend.md) | Componentes, API, modelo de datos, estructura del código y Docker |
| [04-prueba-pieza-plana.md](04-prueba-pieza-plana.md) | Prueba técnica del plan B (pieza plana por extrusión) |
## Planteamiento
| Parte | Responsable | Qué hace |
| --- | --- | --- |
| Frontend (`app/`) | Joel | Aplicación de escritorio: interfaz, visor 3D, inicio de sesión. Habla con el servidor por la API REST. |
| Servidor (`servidor/`) | Lizette | API REST con FastAPI: cuentas, proyectos, historial de conversiones, archivos. |
| Motor (`motor/`) | Lizette | Reconstrucción de la pieza desde las tres vistas, verificación del plano y exportación STEP/STL. |
| Base de datos | Lizette | MySQL: usuarios, proyectos, conversiones, avisos y archivos. |
| Pruebas | Los dos | `pytest` dentro de Docker, contra un MySQL de pruebas. |
## Cambios que hay que reflejar en el resto del repositorio
Este planteamiento cambia decisiones que ahora mismo están escritas de otra forma en
el repositorio. Hay que acordarlas con Joel y actualizarlas en un *pull request*
propio, según `CONTRIBUTING.md`:
| Dónde | Dice ahora | Debe decir |
| --- | --- | --- |
| `docs/decisiones.md` (14-10) | «Aplicación de escritorio, no web»; sin servidor | Aplicación de escritorio **con servidor**: el frontend se conecta a una API REST que guarda cuentas e historial |
| `docs/decisiones.md` (22-10) | Joel: aplicación; compañera: motor | Joel: frontend; Lizette: backend (servidor, base de datos y motor) |
| `docs/guia-tecnica.md` | «No hay servidor ni cuentas»; historial en SQLite | Servidor FastAPI con cuentas (JWT) e historial en MySQL; Docker para pruebas y despliegue |
| `README.md` | Tabla de carpetas sin `servidor/` | Añadir `servidor/` y el arranque con `docker compose` |
| `motor/api.py` | Contrato entre motor y aplicación | Contrato entre motor y **servidor**. El contrato del frontend pasa a ser la API REST (OpenAPI en `/docs`) |
| `pyproject.toml` | PySide6 entre las dependencias comunes | PySide6 en un grupo `app`, para que la imagen Docker del servidor no lo instale |
Se mantiene lo que ya estaba decidido sobre el producto: piezas mecánicas,
reconstrucción desde tres vistas con verificación del plano, plan B de pieza plana,
y avisar siempre de lo que no se soporta.

View File

@ -8,3 +8,4 @@ Registro de los usos relevantes de IA en el proyecto, para la declaración de la
| 2026-10-14 | Claude (Claude Code) | Joel | Prototipo de reconstrucción (`prototipo/reconstruir.py`) a partir de CUBE01.dxf | Resultado abierto y comprobado en Fusion 360 |
| 2026-10-14 | Claude (Claude Code) | Joel | Revisión de decisiones técnicas y redacción de la propuesta | Revisado por el equipo antes de entregarla |
| 2026-10-22 | Claude (Claude Code) | Joel | Estructura del repositorio, contrato `motor/api.py`, motor simulado y plantillas | Revisado por el equipo |
| 2026-11-05 | Claude | Lizette | Redacción de los documentos del backend (requisitos, tecnologías, arquitectura y prueba de la pieza plana) con servidor, cuentas, historial y Docker | Pendiente de revisar por el equipo |

View File

@ -0,0 +1,59 @@
"""Prueba de concepto: DXF 2D -> modelo 3D (STEP y STL) por extrusion.
Uso: python conversion.py placa.dxf 10 (DXF y espesor en mm)
"""
import sys
import cadquery as cq
import ezdxf
def area(puntos):
"""Area de un poligono con la formula del area de Gauss (shoelace)."""
s = 0.0
for (x1, y1), (x2, y2) in zip(puntos, puntos[1:] + puntos[:1]):
s += x1 * y2 - x2 * y1
return abs(s) / 2
def leer_perfiles(ruta):
"""Devuelve las polilineas cerradas y los circulos del DXF."""
doc = ezdxf.readfile(ruta)
msp = doc.modelspace()
poligonos = []
for e in msp.query("LWPOLYLINE"):
if e.closed:
poligonos.append([(p[0], p[1]) for p in e.get_points()])
circulos = [(c.dxf.center.x, c.dxf.center.y, c.dxf.radius)
for c in msp.query("CIRCLE")]
return poligonos, circulos
def construir_solido(poligonos, circulos, altura):
"""El perfil de mayor area es el exterior; el resto se restan como agujeros."""
if not poligonos:
raise ValueError("El DXF no contiene ningun perfil cerrado")
exterior = max(poligonos, key=area)
solido = cq.Workplane("XY").polyline(exterior).close().extrude(altura)
for p in poligonos:
if p is not exterior:
hueco = cq.Workplane("XY").polyline(p).close().extrude(altura)
solido = solido.cut(hueco)
for x, y, r in circulos:
taladro = cq.Workplane("XY").center(x, y).circle(r).extrude(altura)
solido = solido.cut(taladro)
return solido
def main():
ruta, altura = sys.argv[1], float(sys.argv[2])
poligonos, circulos = leer_perfiles(ruta)
print(f"Perfiles cerrados: {len(poligonos)} · Circulos: {len(circulos)}")
solido = construir_solido(poligonos, circulos, altura)
cq.exporters.export(solido, "resultado.step")
cq.exporters.export(solido, "resultado.stl")
print("Generados resultado.step y resultado.stl")
if __name__ == "__main__":
main()

View File

@ -0,0 +1,10 @@
"""Genera un DXF de prueba: placa de 100 x 60 mm con dos taladros de Ø20."""
import ezdxf
doc = ezdxf.new("R2010", units=4) # 4 = milímetros
msp = doc.modelspace()
msp.add_lwpolyline([(0, 0), (100, 0), (100, 60), (0, 60)], close=True)
msp.add_circle((25, 30), radius=10)
msp.add_circle((75, 30), radius=10)
doc.saveas("placa.dxf")
print("Creado placa.dxf")