"""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))