Compare commits
1 Commits
main
...
docs/propu
| Author | SHA1 | Date |
|---|---|---|
|
|
a43635ac37 |
|
|
@ -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 |
|
||||||
|
|
@ -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.*
|
||||||
|
|
@ -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.
|
||||||
|
|
@ -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*
|
||||||
|
|
@ -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.
|
||||||
|
|
@ -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 | 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-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-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 |
|
||||||
|
|
|
||||||
|
|
@ -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()
|
||||||
|
|
@ -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")
|
||||||
Loading…
Reference in New Issue