diff --git a/docs/propuesta-lizette/01-requisitos-backend.md b/docs/propuesta-lizette/01-requisitos-backend.md new file mode 100644 index 0000000..aa68e20 --- /dev/null +++ b/docs/propuesta-lizette/01-requisitos-backend.md @@ -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 | diff --git a/docs/propuesta-lizette/02-tecnologias-backend.md b/docs/propuesta-lizette/02-tecnologias-backend.md new file mode 100644 index 0000000..a08f33c --- /dev/null +++ b/docs/propuesta-lizette/02-tecnologias-backend.md @@ -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.* diff --git a/docs/propuesta-lizette/03-arquitectura-backend.md b/docs/propuesta-lizette/03-arquitectura-backend.md new file mode 100644 index 0000000..21cc10f --- /dev/null +++ b/docs/propuesta-lizette/03-arquitectura-backend.md @@ -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
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. diff --git a/docs/propuesta-lizette/04-prueba-pieza-plana.md b/docs/propuesta-lizette/04-prueba-pieza-plana.md new file mode 100644 index 0000000..e5443bc --- /dev/null +++ b/docs/propuesta-lizette/04-prueba-pieza-plana.md @@ -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* diff --git a/docs/propuesta-lizette/README.md b/docs/propuesta-lizette/README.md new file mode 100644 index 0000000..f511f72 --- /dev/null +++ b/docs/propuesta-lizette/README.md @@ -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. diff --git a/docs/uso-ia.md b/docs/uso-ia.md index 29d17d4..9587b92 100644 --- a/docs/uso-ia.md +++ b/docs/uso-ia.md @@ -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 | diff --git a/prototipo/lizette/conversion.py b/prototipo/lizette/conversion.py new file mode 100644 index 0000000..9e165b8 --- /dev/null +++ b/prototipo/lizette/conversion.py @@ -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() diff --git a/prototipo/lizette/pieza_prueba.py b/prototipo/lizette/pieza_prueba.py new file mode 100644 index 0000000..bdc578a --- /dev/null +++ b/prototipo/lizette/pieza_prueba.py @@ -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")