103 lines
3.8 KiB
Python
103 lines
3.8 KiB
Python
"""Contrato entre el motor de procesado y la aplicación.
|
|
|
|
Este módulo es lo único que la aplicación (interfaz y visor) conoce del motor.
|
|
Si hay que cambiar algo de aquí, se acuerda entre los dos y se anota en docs/decisiones.md,
|
|
porque rompe el trabajo del otro.
|
|
|
|
Uso desde la aplicación:
|
|
|
|
from motor.api import procesar, exportar, Opciones
|
|
|
|
resultado = procesar("pieza.dxf", Opciones())
|
|
if resultado.solido is not None:
|
|
exportar(resultado.solido, "pieza.step")
|
|
"""
|
|
from __future__ import annotations
|
|
|
|
from dataclasses import dataclass, field
|
|
from enum import Enum
|
|
from pathlib import Path
|
|
|
|
import cadquery as cq
|
|
|
|
|
|
class Estado(Enum):
|
|
OK = "ok" # pieza reconstruida y sin problemas
|
|
CON_AVISOS = "con_avisos" # pieza reconstruida, pero hay cosas que revisar
|
|
IMPOSIBLE = "imposible" # no se ha podido reconstruir; ver los avisos bloqueantes
|
|
|
|
|
|
class Gravedad(Enum):
|
|
INFO = "info" # dato informativo (p. ej. escala detectada)
|
|
AVISO = "aviso" # algo no cuadra, pero hay pieza
|
|
BLOQUEANTE = "bloqueante" # impide reconstruir; puede requerir un dato del usuario
|
|
|
|
|
|
@dataclass(frozen=True)
|
|
class Segmento2D:
|
|
"""Línea de una vista, en coordenadas reales de la pieza (mm)."""
|
|
x1: float
|
|
y1: float
|
|
x2: float
|
|
y2: float
|
|
oculta: bool = False
|
|
|
|
|
|
@dataclass
|
|
class Vista:
|
|
"""Geometría 2D de una vista, para mostrarla junto al modelo 3D."""
|
|
nombre: str # "alzado", "planta" o "perfil"
|
|
segmentos: list[Segmento2D] = field(default_factory=list)
|
|
|
|
|
|
@dataclass
|
|
class Referencia:
|
|
"""A qué parte del plano o de la pieza se refiere un aviso, para que el visor la resalte."""
|
|
vista: str | None = None # vista afectada, si la hay
|
|
segmento: Segmento2D | None = None # línea del plano afectada
|
|
punto: tuple[float, float, float] | None = None # punto de la pieza 3D (mm)
|
|
|
|
|
|
@dataclass
|
|
class Aviso:
|
|
codigo: str # identificador estable, p. ej. "SIN_COTAS", "PROYECCION_MIXTA"
|
|
mensaje: str # texto para el usuario, en español
|
|
gravedad: Gravedad
|
|
referencia: Referencia | None = None
|
|
|
|
|
|
@dataclass
|
|
class Opciones:
|
|
"""Datos que el usuario puede aportar cuando el motor los pide."""
|
|
escala: float | None = None # factor real/dibujo si el plano no trae escala (1:2 -> 2.0)
|
|
tolerancia: float = 1.0 # en unidades de dibujo
|
|
alternativa: int | None = None # si el plano es ambiguo, cuál de las piezas posibles se elige
|
|
|
|
|
|
@dataclass
|
|
class Resultado:
|
|
estado: Estado
|
|
solido: cq.Shape | None # pieza 3D en mm; None si el estado es IMPOSIBLE
|
|
avisos: list[Aviso] = field(default_factory=list)
|
|
vistas: dict[str, Vista] = field(default_factory=dict)
|
|
escala: float = 1.0 # factor aplicado (real/dibujo)
|
|
alternativas: int = 1 # número de piezas compatibles con el plano
|
|
|
|
|
|
def procesar(ruta_plano: str | Path, opciones: Opciones | None = None) -> Resultado:
|
|
"""Punto de entrada único del motor.
|
|
|
|
Mientras el motor real no esté listo devuelve un resultado simulado (motor/simulado.py),
|
|
para que la aplicación se pueda desarrollar en paralelo.
|
|
"""
|
|
from motor import simulado
|
|
return simulado.procesar(Path(ruta_plano), opciones or Opciones())
|
|
|
|
|
|
def exportar(solido: cq.Shape, ruta_salida: str | Path) -> None:
|
|
"""Guarda la pieza en STEP o STL según la extensión del archivo."""
|
|
ruta_salida = Path(ruta_salida)
|
|
if ruta_salida.suffix.lower() not in (".step", ".stp", ".stl"):
|
|
raise ValueError(f"Formato no admitido: {ruta_salida.suffix} (usa .step, .stp o .stl)")
|
|
cq.exporters.export(cq.Workplane().add(solido), str(ruta_salida))
|