Estructura inicial del proyecto

Contrato entre motor y aplicación en motor/api.py, motor simulado con
CUBE01, pruebas del contrato, prototipo de la prueba técnica,
documentación de trabajo y plantillas de Gitea.
This commit is contained in:
Joel 2026-10-22 18:53:05 +02:00
commit 4503df96bd
25 changed files with 21953 additions and 0 deletions

10
.gitattributes vendored Normal file
View File

@ -0,0 +1,10 @@
* text=auto eol=lf
# Planos y modelos: Git no debe tocar sus finales de línea ni mostrarlos como diff
*.dxf binary
*.dwg binary
*.pdf binary
*.step binary
*.stp binary
*.stl binary
*.png binary

View File

@ -0,0 +1,17 @@
---
name: "Error"
about: "Algo no funciona como debería"
---
## Qué pasa
## Qué debería pasar
## Cómo reproducirlo
- Plano usado (adjuntar o ruta en `pruebas/catalogo/`):
- Pasos:
## Gravedad
bloqueante / alta / media / baja

View File

@ -0,0 +1,14 @@
---
name: "Tarea"
about: "Trabajo planificado del backlog"
---
## Objetivo
## Criterio de terminado
- [ ]
## Área
motor / app / pruebas / docs

View File

@ -0,0 +1,15 @@
## Qué cambia
## Cómo se ha probado
- [ ] `uv run pytest` pasa
- [ ] Probado a mano (explicar):
## Issue
Closes #
## ¿Cambia el contrato `motor/api.py`?
- [ ] No
- [ ] Sí, acordado y anotado en `docs/decisiones.md`

21
.gitignore vendored Normal file
View File

@ -0,0 +1,21 @@
# Python
__pycache__/
*.py[cod]
.venv/
.pytest_cache/
# Empaquetado
build/
dist/
*.spec
# Resultados generados al probar (el catálogo sí se versiona)
salidas/
*_informe.md
*_3d.png
# Sistema y editores
.DS_Store
Thumbs.db
.vscode/
.idea/

45
CONTRIBUTING.md Normal file
View File

@ -0,0 +1,45 @@
# Cómo trabajamos
## Ramas
- `main` siempre funciona: nadie sube directamente a ella. Todo entra mediante *pull request* revisado por la otra persona.
- Una rama por tarea, creada desde `main` actualizada, con el prefijo del área:
- `motor/...`: procesado (p. ej. `motor/lectura-dwg`)
- `app/...`: interfaz y visor (p. ej. `app/visor-ejes`)
- `pruebas/...`, `docs/...`
- Ramas cortas: mejor varios PR pequeños que uno enorme. Antes de abrir el PR, actualizar la rama con `main`.
```bash
git switch main
git pull
git switch -c app/visor-ejes
# ... trabajo y commits ...
git push -u origin app/visor-ejes
```
## Commits
En español, en imperativo y diciendo qué cambia: `Añade lectura de capas ocultas`, `Corrige escala en planos sin unidades`. Un commit por cambio con sentido propio.
## Issues y tablero
- Cada tarea es un *issue* con etiqueta de área (`motor`, `app`, `pruebas`, `docs`) y asignado a quien la hace.
- El tablero del proyecto en Gitea tiene las columnas **Pendiente · En curso · En revisión · Hecho**.
- Los errores se registran como *issue* con la plantilla de error: son evidencia para el informe de calidad.
## Pull requests
- La descripción dice qué cambia, cómo se ha probado y qué *issue* cierra (`Closes #12`).
- `uv run pytest` debe pasar antes de pedir revisión.
- La otra persona revisa y aprueba. La revisión cruzada es obligatoria: los dos tenemos que entender todo el código para la defensa.
## Contrato motor/aplicación
`motor/api.py` es lo único que comparten las dos partes. Cualquier cambio en él se acuerda antes, se anota en `docs/decisiones.md` y va en un PR propio.
## Documentación continua
- **Decisiones** (`docs/decisiones.md`): cada decisión de diseño o de alcance, con motivo.
- **Bitácora** (`docs/bitacora.md`): qué ha hecho cada uno cada semana.
- **Actas** (`docs/actas/`): una por reunión semanal.
- **Uso de IA** (`docs/uso-ia.md`): cada uso relevante de herramientas de IA.

21
LICENSE Normal file
View File

@ -0,0 +1,21 @@
MIT License
Copyright (c) 2026 Equipo Plano3D
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

33
README.md Normal file
View File

@ -0,0 +1,33 @@
# Plano3D
Aplicación de escritorio que reconstruye automáticamente una pieza mecánica 3D (STEP/STL) a partir de su plano técnico 2D con alzado, planta y perfil (DXF, DWG o PDF vectorial), y verifica el plano: escala, cotas, coherencia entre vistas y ambigüedades.
Proyecto intermodular de 2.º DAM, IES La Mar.
## Estructura
| Carpeta | Contenido | Responsable |
| --- | --- | --- |
| `motor/` | Procesado: lectura del plano, vistas, reconstrucción, verificación, exportación | Motor |
| `motor/api.py` | **Contrato** entre motor y aplicación: lo único que la aplicación importa | Los dos |
| `app/` | Aplicación: interfaz Qt, visor 3D, historial, instalador | Aplicación |
| `pruebas/` | Pruebas automáticas (`pytest`) y catálogo de piezas con su resultado esperado | Los dos |
| `prototipo/` | Prueba técnica inicial (no se modifica; solo de referencia) | Los dos |
| `docs/` | Guía técnica, registro de decisiones, bitácora, actas y declaración de uso de IA | Los dos |
## Puesta en marcha
Requisitos: [uv](https://docs.astral.sh/uv/) y Git. CadQuery necesita Python 3.12 o 3.13; uv lo descarga solo.
```bash
uv sync
uv run pytest
```
## Formas de trabajo
Ver [CONTRIBUTING.md](CONTRIBUTING.md): ramas, *pull requests*, revisiones e *issues*.
## Licencia
MIT. Ver [LICENSE](LICENSE).

4
app/__init__.py Normal file
View File

@ -0,0 +1,4 @@
"""Aplicación de escritorio de Plano3D: interfaz (PySide6), visor 3D (VTK), historial e instalador.
Solo usa el motor a través de motor.api (procesar, exportar y las clases del contrato).
"""

25
docs/actas/plantilla.md Normal file
View File

@ -0,0 +1,25 @@
# Acta de reunión AAAA-MM-DD
- **Asistentes:**
- **Modera / toma notas:** (rotatorio)
- **Duración:**
## Revisión de la semana anterior
| Tarea (issue) | Responsable | Estado |
| --- | --- | --- |
| | | |
## Temas tratados
-
## Acuerdos
-
## Tareas para la próxima semana
| Tarea | Responsable | Issue |
| --- | --- | --- |
| | | |

17
docs/bitacora.md Normal file
View File

@ -0,0 +1,17 @@
# Bitácora
Una entrada por semana. Cada persona escribe lo suyo.
## Semana del AAAA-MM-DD
### Joel
- Hecho:
- Problemas:
- Próxima semana:
- Horas (clase / autónomas):
### (compañera)
- Hecho:
- Problemas:
- Próxima semana:
- Horas (clase / autónomas):

15
docs/decisiones.md Normal file
View File

@ -0,0 +1,15 @@
# Registro de decisiones
Cada decisión de alcance o de diseño, con su motivo. La más reciente, arriba.
| Fecha | Decisión | Motivo |
| --- | --- | --- |
| 2026-10-22 | Reparto: Joel, aplicación (interfaz y visor 3D); compañera, motor de procesado. Contrato común en `motor/api.py` | Partes separables que pueden avanzar en paralelo con un motor simulado |
| 2026-10-22 | Repositorio en la plataforma Gitea del instituto, con ramas por tarea y PR revisados | Requisito del módulo; trazabilidad de la contribución individual |
| 2026-10-14 | Hitos H0 (fin de SA2) y H1 (fin de SA3), con plan B de piezas planas | Gestionar el riesgo técnico de la reconstrucción |
| 2026-10-14 | Aplicación de escritorio, no web | Cálculo pesado con OpenCASCADE; privacidad de los planos; sin servidor ni costes |
| 2026-10-14 | Alcance por prioridades; roscas como agujero + nota; revolución como importante | Equilibrio entre ambición y las horas del módulo |
| 2026-10-14 | Lo no soportado se detecta y se avisa, nunca se reconstruye mal en silencio | Fiabilidad: una pieza errónea sin aviso es peor que ninguna |
| 2026-10-14 | Reconstrucción automática desde 3 vistas, con verificación del plano | Es lo que Fusion 360 no hace; la versión "DXF + espesor" no aportaba valor |
| 2026-10-14 | Prueba técnica con CUBE01.dxf: reconstrucción correcta y detección de proyección mixta | Comprobar la viabilidad antes de comprometer el alcance |
| 2026-10-08 | Piezas mecánicas (no arquitectura); entradas DXF, DWG y PDF | Propuesta inicial del equipo |

37
docs/guia-tecnica.md Normal file
View File

@ -0,0 +1,37 @@
# Guía técnica
Resumen de las decisiones técnicas vigentes y de las reglas del código. El detalle y la fecha de cada decisión están en [decisiones.md](decisiones.md).
## Producto
Aplicación de escritorio que reconstruye piezas mecánicas 3D a partir de planos de tres vistas y verifica el plano. Todo se procesa en el equipo del usuario: no hay servidor ni cuentas.
## Tecnologías
| Tecnología | Uso |
| --- | --- |
| Python 3.12 | Todo el proyecto |
| CadQuery (OpenCASCADE) | Sólidos 3D y exportación STEP/STL |
| ezdxf | Lectura de DXF |
| shapely | Geometría 2D de las vistas |
| OR-Tools CP-SAT | Búsqueda de la pieza que reproduce el plano |
| PySide6 y VTK | Interfaz y visor 3D |
| SQLite | Historial local de conversiones |
| PyInstaller | Instalador |
| pytest | Pruebas automáticas |
## Alcance
- Entradas: DXF (imprescindible), DWG mediante conversor externo, PDF vectorial (importante).
- Geometría imprescindible: tres vistas, caras planas, chaflanes, agujeros pasantes y roscas (agujero más nota).
- Geometría importante: agujeros ciegos, avellanados y cajeados; piezas de revolución.
- Sistemas de proyección europeo (ISO-E) y americano (ISO-A); se avisa si un plano los mezcla.
- Lo que no se soporta se detecta y se avisa. Nunca se entrega una pieza que no reproduce el plano sin advertirlo.
## Reglas del código
- `motor/api.py` es el contrato entre motor y aplicación. La aplicación solo importa desde ahí, y cualquier cambio se acuerda entre los dos.
- `motor/simulado.py` devuelve la pieza CUBE01 mientras el motor real no esté terminado.
- `prototipo/reconstruir.py` es la prueba técnica original. Se conserva como referencia y no se modifica.
- Cada pieza del catálogo tiene su carpeta en `pruebas/catalogo/` con `plano.dxf`, `esperado.step` e `info.md`.
- Ramas, commits y revisiones según [CONTRIBUTING.md](../CONTRIBUTING.md).

10
docs/uso-ia.md Normal file
View File

@ -0,0 +1,10 @@
# Declaración de uso de herramientas de IA
Registro de los usos relevantes de IA en el proyecto, para la declaración de la memoria final.
| Fecha | Herramienta | Quién | Para qué | Cómo se revisó |
| --- | --- | --- | --- | --- |
| 2026-10-08 | Claude (Claude Code) | Joel | Análisis de viabilidad de la idea y primer anteproyecto | Discutido y corregido en equipo |
| 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 |

4
motor/__init__.py Normal file
View File

@ -0,0 +1,4 @@
"""Motor de procesado de Plano3D: de plano 2D (DXF, DWG, PDF) a pieza 3D con informe de verificación.
La aplicación solo debe importar desde motor.api.
"""

102
motor/api.py Normal file
View File

@ -0,0 +1,102 @@
"""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))

45
motor/simulado.py Normal file
View File

@ -0,0 +1,45 @@
"""Motor simulado: devuelve siempre la pieza de CUBE01 con sus avisos reales.
Sirve para desarrollar la aplicación (interfaz y visor) antes de que exista el motor real.
Las líneas del plano sí se leen del archivo recibido, para poder dibujarlas en 2D.
"""
from __future__ import annotations
from pathlib import Path
import cadquery as cq
import ezdxf
from motor.api import Aviso, Estado, Gravedad, Opciones, Referencia, Resultado, Segmento2D, Vista
RAIZ = Path(__file__).resolve().parent.parent
PIEZA_CUBE01 = RAIZ / "pruebas" / "catalogo" / "CUBE01" / "esperado.step"
def _leer_lineas(ruta: Path) -> list[Segmento2D]:
if ruta.suffix.lower() != ".dxf":
return []
doc = ezdxf.readfile(ruta)
segmentos = []
for e in doc.modelspace().query("LINE"):
tipo = e.dxf.get("linetype", "ByLayer")
if tipo.lower() == "bylayer":
tipo = doc.layers.get(e.dxf.layer).dxf.linetype
oculta = "HIDDEN" in tipo.upper() or "HIDDEN" in e.dxf.layer.upper()
segmentos.append(Segmento2D(e.dxf.start.x, e.dxf.start.y, e.dxf.end.x, e.dxf.end.y, oculta))
return segmentos
def procesar(ruta: Path, opciones: Opciones) -> Resultado:
solido = cq.importers.importStep(str(PIEZA_CUBE01)).val()
avisos = [
Aviso("ESCALA_DETECTADA", "Escala 1:2 leída del cajetín.", Gravedad.INFO),
Aviso("SIN_COTAS", "El plano no tiene cotas: las medidas se deducen de la geometría y la escala.",
Gravedad.AVISO),
Aviso("PROYECCION_MIXTA",
"Sistemas de proyección mezclados: la planta sigue el europeo (ISO-E) y el perfil el americano (ISO-A).",
Gravedad.AVISO, Referencia(vista="perfil")),
]
# Simulado: todas las líneas van en una sola "vista"; el motor real las separará en alzado, planta y perfil.
vistas = {"plano": Vista("plano", _leer_lineas(ruta))}
return Resultado(Estado.CON_AVISOS, solido, avisos, vistas, escala=opciones.escala or 2.0)

11
prototipo/README.md Normal file
View File

@ -0,0 +1,11 @@
# Prototipo (prueba técnica)
`reconstruir.py` es la prueba técnica que demostró la viabilidad del proyecto con CUBE01.dxf: reconstrucción por descomposición en celdas, búsqueda por fuerza bruta y detección del sistema de proyección.
Se conserva como referencia y no se modifica. El motor definitivo se desarrolla en `motor/`.
```bash
uv run python prototipo/reconstruir.py pruebas/catalogo/CUBE01/plano.dxf
```
Limitaciones conocidas: solo líneas rectas (sin arcos ni círculos) y búsqueda exhaustiva que no escala a más de unas 18 celdas.

423
prototipo/reconstruir.py Normal file
View File

@ -0,0 +1,423 @@
"""Prueba técnica: reconstrucción automática de una pieza 3D a partir de un plano DXF
con tres vistas (alzado, planta y perfil).
Método (descomposición en celdas):
1. Leer las líneas visibles y ocultas; ignorar marco, cajetín, textos y cotas.
2. Buscar la escala en los textos del plano ("ESCALA 1:2").
3. Agrupar las líneas en vistas e identificar alzado, planta y perfil por su alineación.
4. Para cada convención de proyección posible (europea/americana en planta y perfil):
a. dividir el volumen en celdas con todos los planos que sugieren las vistas
(incluidos los planos inclinados de las líneas diagonales);
b. descartar celdas que se salen de la silueta de alguna vista;
c. probar combinaciones de celdas cuya silueta coincide con las tres vistas;
d. proyectar las aristas del sólido (visibles y ocultas) y compararlas con el plano.
5. Quedarse con el sólido que reproduce el plano y exportarlo a STEP y STL.
Uso: python reconstruir.py CUBE01.dxf
"""
import itertools
import math
import re
import sys
from pathlib import Path
import cadquery as cq
import ezdxf
import numpy as np
from OCP.BRepClass3d import BRepClass3d_SolidClassifier
from OCP.BRepIntCurveSurface import BRepIntCurveSurface_Inter
from OCP.gp import gp_Dir, gp_Lin, gp_Pnt
from OCP.TopAbs import TopAbs_IN
from shapely.geometry import LineString, MultiPoint
from shapely.ops import polygonize, unary_union
TOL = 1.0 # tolerancia en unidades de dibujo (las vistas no están perfectamente alineadas)
BIG = 1e4
MAX_CELDAS = 18 # límite de la búsqueda exhaustiva (2^18 combinaciones)
# ---------------------------------------------------------------- 1. lectura
def leer_dxf(ruta):
doc = ezdxf.readfile(ruta)
msp = doc.modelspace()
avisos = []
def es_oculta(e):
lt = e.dxf.get("linetype", "ByLayer")
if lt.lower() in ("bylayer", "byblock"):
lt = doc.layers.get(e.dxf.layer).dxf.linetype
nombre = (lt + " " + e.dxf.layer).upper()
return any(k in nombre for k in ("HIDDEN", "DASH", "OCULT", "ISO02"))
segs = []
for e in msp:
if e.dxftype() == "LINE":
partes = [(e.dxf.start, e.dxf.end)]
elif e.dxftype() == "LWPOLYLINE":
pts = [p[:2] for p in e.get_points()]
if e.closed:
pts.append(pts[0])
partes = list(zip(pts, pts[1:]))
else:
continue # marco y cajetín (INSERT), textos, cotas, etc.
for a, b in partes:
segs.append((float(a[0]), float(a[1]), float(b[0]), float(b[1]), es_oculta(e)))
textos = [t.dxf.text for t in msp.query("TEXT")] + [t.text for t in msp.query("MTEXT")]
for ins in msp.query("INSERT"):
textos += [a.dxf.text for a in ins.attribs]
escala = None
for t in textos:
m = re.search(r"(?:ESCALA|SCALE)\s*:?\s*(\d+(?:[.,]\d+)?)\s*[:/]\s*(\d+(?:[.,]\d+)?)", t, re.I)
if m:
a, b = (float(x.replace(",", ".")) for x in m.groups())
escala = (a, b)
break
if escala is None:
avisos.append("No se ha encontrado la escala en el plano: se supone 1:1.")
escala = (1.0, 1.0)
n_cotas = len(msp.query("DIMENSION"))
if n_cotas == 0:
avisos.append("El plano no tiene cotas: las medidas se deducen de la geometría y la escala.")
unidades = {0: "sin unidades", 1: "pulgadas", 4: "mm", 5: "cm", 6: "m"}.get(doc.header.get("$INSUNITS", 0), "otras")
return segs, escala, n_cotas, unidades, avisos
# ---------------------------------------------------------------- 2. vistas
def agrupar_vistas(segs):
n = len(segs)
padre = list(range(n))
def raiz(i):
while padre[i] != i:
padre[i] = padre[padre[i]]
i = padre[i]
return i
cajas = [(min(s[0], s[2]) - TOL, min(s[1], s[3]) - TOL, max(s[0], s[2]) + TOL, max(s[1], s[3]) + TOL) for s in segs]
for i in range(n):
for j in range(i + 1, n):
a, b = cajas[i], cajas[j]
if a[0] <= b[2] and b[0] <= a[2] and a[1] <= b[3] and b[1] <= a[3]:
padre[raiz(i)] = raiz(j)
grupos = {}
for i in range(n):
grupos.setdefault(raiz(i), []).append(segs[i])
vistas = []
for g in grupos.values():
xs = [c for s in g for c in (s[0], s[2])]
ys = [c for s in g for c in (s[1], s[3])]
vistas.append({"segs": g, "u0": min(xs), "u1": max(xs), "v0": min(ys), "v1": max(ys)})
return vistas
def identificar_vistas(vistas):
def mismo_ancho(a, b):
return abs(a["u0"] - b["u0"]) < 2 * TOL and abs(a["u1"] - b["u1"]) < 2 * TOL
def mismo_alto(a, b):
return abs(a["v0"] - b["v0"]) < 2 * TOL and abs(a["v1"] - b["v1"]) < 2 * TOL
for alz in vistas:
plantas = [v for v in vistas if v is not alz and mismo_ancho(alz, v)]
perfiles = [v for v in vistas if v is not alz and mismo_alto(alz, v)]
if plantas and perfiles:
pl, pf = plantas[0], perfiles[0]
return alz, pl, pf, ("debajo" if pl["v1"] < alz["v0"] else "encima"), ("derecha" if pf["u0"] > alz["u1"] else "izquierda")
raise SystemExit("No se han podido identificar alzado, planta y perfil alineados.")
def a_coordenadas(alz, pl, pf, pos_pl, pos_pf, conv_pl, conv_pf):
"""Pasa cada vista a coordenadas de la pieza: alzado (X,Z), planta (X,Y), perfil (Y,Z).
Y = 0 es la cara delantera. Devuelve también desde dónde mira el observador de cada vista."""
D = pl["v1"] - pl["v0"]
def map_pl(u, v):
d = (pl["v1"] - v) if pos_pl == "debajo" else (v - pl["v0"]) # distancia al borde junto al alzado
return (u - pl["u0"], D - d if conv_pl == "E" else d)
def map_pf(u, v):
d = (u - pf["u0"]) if pos_pf == "derecha" else (pf["u1"] - u)
return (D - d if conv_pf == "E" else d, v - pf["v0"])
def map_alz(u, v):
return (u - alz["u0"], v - alz["v0"])
out = {}
for nombre, vista, f in (("alzado", alz, map_alz), ("planta", pl, map_pl), ("perfil", pf, map_pf)):
out[nombre] = [(*f(s[0], s[1]), *f(s[2], s[3]), s[4]) for s in vista["segs"]]
# Observador: en el sistema europeo la vista de debajo es la superior; en el americano, al revés.
desde_arriba = (pos_pl == "debajo") == (conv_pl == "E")
desde_izquierda = (pos_pf == "derecha") == (conv_pf == "E")
observador = {
"alzado": (0, -1, 0),
"planta": (0, 0, 1) if desde_arriba else (0, 0, -1),
"perfil": (-1, 0, 0) if desde_izquierda else (1, 0, 0),
}
return out, observador, D
PROY = {"alzado": (0, 2), "planta": (0, 1), "perfil": (1, 2)} # ejes 3D que se ven en cada vista
# ---------------------------------------------------------------- 3. celdas
def agrupar_valores(vals):
vals = sorted(vals)
out = []
for v in vals:
if not out or v - out[-1] > TOL:
out.append(v)
return out
def region(segs):
lineas = [LineString([(s[0], s[1]), (s[2], s[3])]) for s in segs if not s[4]]
return unary_union(list(polygonize(unary_union(lineas))))
def semiespacio(p, n):
n = cq.Vector(*n).normalized()
aux = cq.Vector(1, 0, 0) if abs(n.x) < 0.9 else cq.Vector(0, 1, 0)
plano = cq.Plane(origin=cq.Vector(*p), xDir=aux.cross(n).normalized(), normal=n)
return cq.Workplane(plano).rect(BIG, BIG).extrude(BIG).val()
def crear_celdas(coords):
rejilla = {0: [], 1: [], 2: []}
inclinados = []
for nombre, segs in coords.items():
a, b = PROY[nombre]
for x1, y1, x2, y2, _ in segs:
rejilla[a] += [x1, x2]
rejilla[b] += [y1, y2]
if abs(x1 - x2) > TOL and abs(y1 - y2) > TOL: # línea diagonal -> plano inclinado
n = [0.0, 0.0, 0.0]
n[a], n[b] = -(y2 - y1), (x2 - x1)
p = [0.0, 0.0, 0.0]
p[a], p[b] = x1, y1
inclinados.append((tuple(p), tuple(n)))
ejes = [agrupar_valores(rejilla[i]) for i in range(3)]
celdas = []
for i, j, k in itertools.product(range(len(ejes[0]) - 1), range(len(ejes[1]) - 1), range(len(ejes[2]) - 1)):
x0, x1 = ejes[0][i], ejes[0][i + 1]
y0, y1 = ejes[1][j], ejes[1][j + 1]
z0, z1 = ejes[2][k], ejes[2][k + 1]
trozos = [cq.Solid.makeBox(x1 - x0, y1 - y0, z1 - z0, pnt=cq.Vector(x0, y0, z0))]
esquinas = np.array(list(itertools.product((x0, x1), (y0, y1), (z0, z1))))
for p, n in inclinados:
dist = (esquinas - np.array(p)) @ np.array(n)
if dist.min() < -1e-6 and dist.max() > 1e-6: # el plano atraviesa la caja
hs = semiespacio(p, n)
nuevos = []
for t in trozos:
for parte in (t.intersect(hs), t.cut(hs)):
nuevos += [s for s in parte.Solids() if s.Volume() > 1e-6]
trozos = nuevos
celdas += trozos
return celdas, ejes, inclinados
def proyeccion(solido, nombre):
a, b = PROY[nombre]
pts = [(v.toTuple()[a], v.toTuple()[b]) for v in solido.Vertices()]
return MultiPoint(pts).convex_hull
# ---------------------------------------------------------------- 4. comparación
def oculto(forma, p, d):
inter = BRepIntCurveSurface_Inter()
inter.Init(forma.wrapped, gp_Lin(gp_Pnt(*p), gp_Dir(*d)), 1e-7)
ws = []
while inter.More():
if inter.W() > 1e-5:
ws.append(inter.W())
inter.Next()
params = [0.0] + sorted(set(round(w, 6) for w in ws))
for t0, t1 in zip(params, params[1:]):
q = np.array(p) + np.array(d) * (t0 + t1) / 2
if BRepClass3d_SolidClassifier(forma.wrapped, gp_Pnt(*q), 1e-6).State() == TopAbs_IN:
return True
return False
def aristas_proyectadas(forma, nombre, obs):
a, b = PROY[nombre]
vis, ocu = [], []
for e in forma.Edges():
p0, p1 = np.array(e.startPoint().toTuple()), np.array(e.endPoint().toTuple())
if math.dist((p0[a], p0[b]), (p1[a], p1[b])) < TOL / 2:
continue # arista perpendicular a la vista
n = 5
for i in range(n):
q0, q1 = p0 + (p1 - p0) * i / n, p0 + (p1 - p0) * (i + 1) / n
seg = (q0[a], q0[b], q1[a], q1[b])
(ocu if oculto(forma, (q0 + q1) / 2, obs) else vis).append(seg)
return vis, ocu
def muestras(segs, paso=TOL / 2):
pts = []
for x1, y1, x2, y2 in segs:
n = max(1, int(math.dist((x1, y1), (x2, y2)) / paso))
for i in range(n):
t = (i + 0.5) / n
pts.append((x1 + (x2 - x1) * t, y1 + (y2 - y1) * t))
return np.array(pts).reshape(-1, 2)
def distancia(pts, segs):
if len(pts) == 0:
return np.zeros(0)
if not segs:
return np.full(len(pts), np.inf)
s = np.array(segs, dtype=float)
a, b = s[:, :2], s[:, 2:]
ab = b - a
t = np.clip(((pts[:, None, :] - a) * ab).sum(-1) / np.maximum((ab * ab).sum(-1), 1e-12), 0, 1)
proj = a + t[..., None] * ab
return np.linalg.norm(pts[:, None, :] - proj, axis=-1).min(axis=1)
def errores_vista(forma, nombre, obs, dibujo):
cvis, cocu = aristas_proyectadas(forma, nombre, obs)
dvis = [s[:4] for s in dibujo if not s[4]]
docu = [s[:4] for s in dibujo if s[4]]
err = 0
err += int((distancia(muestras(cvis), dvis) > TOL).sum()) # aristas que el plano no tiene
m = muestras(cocu)
m = m[distancia(m, cvis) > TOL] # lo visible tapa lo oculto
err += int((distancia(m, docu) > TOL).sum())
err += int((distancia(muestras(dvis), cvis) > TOL).sum()) # líneas del plano que faltan
err += int((distancia(muestras(docu), cocu) > TOL).sum())
return err
# ---------------------------------------------------------------- 5. búsqueda
def reconstruir(coords, observador):
regiones = {n: region(s) for n, s in coords.items()}
celdas, ejes, inclinados = crear_celdas(coords)
proy = [{n: proyeccion(c, n) for n in coords} for c in celdas]
# Una celda que se sale de la silueta de alguna vista no puede formar parte de la pieza.
validas = [i for i in range(len(celdas))
if all(proy[i][n].difference(regiones[n].buffer(TOL / 2)).area < 1e-3 for n in coords)]
if len(validas) > MAX_CELDAS:
raise SystemExit(f"Demasiadas celdas ({len(validas)}) para la búsqueda exhaustiva de la prueba.")
candidatos = []
for r in range(1, len(validas) + 1):
for combo in itertools.combinations(validas, r):
ok = all(unary_union([proy[i][n] for i in combo]).symmetric_difference(regiones[n]).area
< 0.005 * regiones[n].area for n in coords)
if ok:
candidatos.append(combo)
resultados = []
for combo in candidatos:
forma = celdas[combo[0]]
if len(combo) > 1:
forma = forma.fuse(*[celdas[i] for i in combo[1:]])
forma = forma.clean()
if len(forma.Solids()) != 1:
continue # piezas sueltas: no es una pieza única
forma = forma.Solids()[0]
err = sum(errores_vista(forma, n, observador[n], coords[n]) for n in coords)
resultados.append((err, combo, forma))
if err == 0 and len([r for r in resultados if r[0] == 0]) > 3:
break
resultados.sort(key=lambda r: r[0])
return resultados, len(celdas), len(validas), len(candidatos), ejes, inclinados
# ---------------------------------------------------------------- salida
def vista_previa(forma, ruta_png):
import matplotlib
matplotlib.use("Agg")
import matplotlib.pyplot as plt
from mpl_toolkits.mplot3d.art3d import Poly3DCollection
verts, tris = forma.tessellate(0.1)
v = np.array([p.toTuple() for p in verts])
caras = v[np.array(tris)]
normales = np.cross(caras[:, 1] - caras[:, 0], caras[:, 2] - caras[:, 0])
normales /= np.linalg.norm(normales, axis=1, keepdims=True) + 1e-12
luz = np.clip(normales @ np.array([0.4, -0.6, 0.7]), 0.15, 1)
fig = plt.figure(figsize=(6, 6))
ax = fig.add_subplot(projection="3d")
ax.add_collection3d(Poly3DCollection(caras, facecolors=plt.cm.Blues(0.35 + 0.55 * luz), edgecolor="none"))
for e in forma.Edges():
a, b = e.startPoint().toTuple(), e.endPoint().toTuple()
ax.plot(*zip(a, b), color="k", lw=0.8)
lo, hi = v.min(0), v.max(0)
ax.set_xlim(lo[0], hi[0]); ax.set_ylim(lo[1], hi[1]); ax.set_zlim(lo[2], hi[2])
ax.set_box_aspect(hi - lo)
ax.set_xlabel("X (mm)"); ax.set_ylabel("Y (mm, 0 = frente)"); ax.set_zlabel("Z (mm)")
ax.view_init(elev=25, azim=-60)
plt.tight_layout()
plt.savefig(ruta_png, dpi=110)
def main(ruta):
ruta = Path(ruta)
segs, (ea, eb), n_cotas, unidades, avisos = leer_dxf(ruta)
factor = eb / ea
vistas = agrupar_vistas(segs)
alz, pl, pf, pos_pl, pos_pf = identificar_vistas(vistas)
inf = [f"# Informe de reconstrucción: {ruta.name}", "",
f"- Líneas leídas: {len(segs)} ({sum(s[4] for s in segs)} ocultas); unidades del archivo: {unidades}",
f"- Escala: {ea:g}:{eb:g} -> factor x{factor:g}; cotas en el plano: {n_cotas}",
f"- Vistas encontradas: {len(vistas)}; planta {pos_pl} del alzado, perfil a la {pos_pf}",
f"- Desalineación planta/alzado: {abs(pl['u0'] - alz['u0']):.2f}; perfil/alzado: {abs(pf['v0'] - alz['v0']):.2f} (unidades de dibujo)"]
D_pf = pf["u1"] - pf["u0"]
D_pl = pl["v1"] - pl["v0"]
if abs(D_pf - D_pl) > 2 * TOL:
avisos.append(f"La profundidad no coincide entre planta ({D_pl:.1f}) y perfil ({D_pf:.1f}).")
nombres = {"E": "europeo (ISO-E)", "A": "americano (ISO-A)"}
mejor = None
inf += ["", "## Hipótesis de proyección", "", "| Planta | Perfil | Celdas (válidas) | Candidatos por silueta | Error mínimo |", "|---|---|---|---|---|"]
for conv_pl, conv_pf in itertools.product("EA", repeat=2):
coords, obs, D = a_coordenadas(alz, pl, pf, pos_pl, pos_pf, conv_pl, conv_pf)
res, n_c, n_v, n_cand, ejes, incl = reconstruir(coords, obs)
e_min = res[0][0] if res else "-"
inf.append(f"| {nombres[conv_pl]} | {nombres[conv_pf]} | {n_c} ({n_v}) | {n_cand} | {e_min} |")
print(f"planta {conv_pl} / perfil {conv_pf}: celdas {n_c}, validas {n_v}, candidatos {n_cand}, error {e_min}")
if res and (mejor is None or res[0][0] < mejor[0][0][0]):
mejor = (res, conv_pl, conv_pf)
if mejor is None:
raise SystemExit("No se ha encontrado ninguna pieza compatible con las vistas.")
res, conv_pl, conv_pf = mejor
err, combo, forma = res[0]
exactas = [r for r in res if r[0] == 0]
if conv_pl != conv_pf:
avisos.append(f"Sistemas de proyección mezclados: la planta sigue el sistema {nombres[conv_pl]} "
f"y el perfil el {nombres[conv_pf]}.")
if err > 0:
avisos.append(f"Ninguna pieza reproduce exactamente el plano (error mínimo {err}): revisar manualmente.")
if len(exactas) > 1:
avisos.append(f"Hay {len(exactas)} piezas distintas que encajan con las vistas: el plano es ambiguo.")
forma = forma.scale(factor)
bb = forma.BoundingBox()
inf += ["", "## Resultado", "",
f"- Convención elegida: planta {nombres[conv_pl]}, perfil {nombres[conv_pf]}",
f"- Celdas usadas: {len(combo)}; error de reproducción del plano: {err}",
f"- Tamaño real: {bb.xlen:.1f} x {bb.ylen:.1f} x {bb.zlen:.1f} mm; volumen {forma.Volume():.0f} mm³",
f"- Caras: {len(forma.Faces())}, aristas: {len(forma.Edges())}",
"", "## Avisos", ""] + [f"- {a}" for a in avisos]
base = ruta.with_suffix("")
cq.exporters.export(cq.Workplane().add(forma), str(base) + ".step")
cq.exporters.export(cq.Workplane().add(forma), str(base) + ".stl")
vista_previa(forma, str(base) + "_3d.png")
Path(str(base) + "_informe.md").write_text("\n".join(inf) + "\n", encoding="utf-8")
print("\n".join(inf))
if __name__ == "__main__":
main(sys.argv[1] if len(sys.argv) > 1 else "CUBE01.dxf")

View File

@ -0,0 +1,721 @@
ISO-10303-21;
HEADER;
FILE_DESCRIPTION(('Open CASCADE Model'),'2;1');
FILE_NAME('Open CASCADE Shape Model','2026-10-08T19:20:47',('Author'),(
'Open CASCADE'),'Open CASCADE STEP processor 7.9','Open CASCADE 7.9'
,'Unknown');
FILE_SCHEMA(('AUTOMOTIVE_DESIGN { 1 0 10303 214 1 1 1 1 }'));
ENDSEC;
DATA;
#1 = APPLICATION_PROTOCOL_DEFINITION('international standard',
'automotive_design',2000,#2);
#2 = APPLICATION_CONTEXT(
'core data for automotive mechanical design processes');
#3 = SHAPE_DEFINITION_REPRESENTATION(#4,#10);
#4 = PRODUCT_DEFINITION_SHAPE('','',#5);
#5 = PRODUCT_DEFINITION('design','',#6,#9);
#6 = PRODUCT_DEFINITION_FORMATION('','',#7);
#7 = PRODUCT('Open CASCADE STEP translator 7.9 1',
'Open CASCADE STEP translator 7.9 1','',(#8));
#8 = PRODUCT_CONTEXT('',#2,'mechanical');
#9 = PRODUCT_DEFINITION_CONTEXT('part definition',#2,'design');
#10 = ADVANCED_BREP_SHAPE_REPRESENTATION('',(#11,#15),#609);
#11 = AXIS2_PLACEMENT_3D('',#12,#13,#14);
#12 = CARTESIAN_POINT('',(0.,0.,0.));
#13 = DIRECTION('',(0.,0.,1.));
#14 = DIRECTION('',(1.,0.,-0.));
#15 = MANIFOLD_SOLID_BREP('',#16);
#16 = CLOSED_SHELL('',(#17,#193,#269,#322,#398,#492,#519,#547,#574,#601)
);
#17 = ADVANCED_FACE('',(#18),#32,.F.);
#18 = FACE_BOUND('',#19,.F.);
#19 = EDGE_LOOP('',(#20,#55,#83,#111,#139,#167));
#20 = ORIENTED_EDGE('',*,*,#21,.F.);
#21 = EDGE_CURVE('',#22,#24,#26,.T.);
#22 = VERTEX_POINT('',#23);
#23 = CARTESIAN_POINT('',(0.,0.,0.));
#24 = VERTEX_POINT('',#25);
#25 = CARTESIAN_POINT('',(0.,0.,100.));
#26 = SURFACE_CURVE('',#27,(#31,#43),.PCURVE_S1.);
#27 = LINE('',#28,#29);
#28 = CARTESIAN_POINT('',(0.,0.,0.));
#29 = VECTOR('',#30,1.);
#30 = DIRECTION('',(0.,0.,1.));
#31 = PCURVE('',#32,#37);
#32 = PLANE('',#33);
#33 = AXIS2_PLACEMENT_3D('',#34,#35,#36);
#34 = CARTESIAN_POINT('',(0.,0.,0.));
#35 = DIRECTION('',(1.,0.,-0.));
#36 = DIRECTION('',(0.,0.,1.));
#37 = DEFINITIONAL_REPRESENTATION('',(#38),#42);
#38 = LINE('',#39,#40);
#39 = CARTESIAN_POINT('',(0.,0.));
#40 = VECTOR('',#41,1.);
#41 = DIRECTION('',(1.,0.));
#42 = ( GEOMETRIC_REPRESENTATION_CONTEXT(2)
PARAMETRIC_REPRESENTATION_CONTEXT() REPRESENTATION_CONTEXT('2D SPACE',''
) );
#43 = PCURVE('',#44,#49);
#44 = PLANE('',#45);
#45 = AXIS2_PLACEMENT_3D('',#46,#47,#48);
#46 = CARTESIAN_POINT('',(0.,0.,0.));
#47 = DIRECTION('',(-0.,1.,0.));
#48 = DIRECTION('',(0.,0.,1.));
#49 = DEFINITIONAL_REPRESENTATION('',(#50),#54);
#50 = LINE('',#51,#52);
#51 = CARTESIAN_POINT('',(0.,0.));
#52 = VECTOR('',#53,1.);
#53 = DIRECTION('',(1.,0.));
#54 = ( GEOMETRIC_REPRESENTATION_CONTEXT(2)
PARAMETRIC_REPRESENTATION_CONTEXT() REPRESENTATION_CONTEXT('2D SPACE',''
) );
#55 = ORIENTED_EDGE('',*,*,#56,.F.);
#56 = EDGE_CURVE('',#57,#22,#59,.T.);
#57 = VERTEX_POINT('',#58);
#58 = CARTESIAN_POINT('',(0.,200.,0.));
#59 = SURFACE_CURVE('',#60,(#64,#71),.PCURVE_S1.);
#60 = LINE('',#61,#62);
#61 = CARTESIAN_POINT('',(0.,200.,0.));
#62 = VECTOR('',#63,1.);
#63 = DIRECTION('',(0.,-1.,0.));
#64 = PCURVE('',#32,#65);
#65 = DEFINITIONAL_REPRESENTATION('',(#66),#70);
#66 = LINE('',#67,#68);
#67 = CARTESIAN_POINT('',(0.,-200.));
#68 = VECTOR('',#69,1.);
#69 = DIRECTION('',(0.,1.));
#70 = ( GEOMETRIC_REPRESENTATION_CONTEXT(2)
PARAMETRIC_REPRESENTATION_CONTEXT() REPRESENTATION_CONTEXT('2D SPACE',''
) );
#71 = PCURVE('',#72,#77);
#72 = PLANE('',#73);
#73 = AXIS2_PLACEMENT_3D('',#74,#75,#76);
#74 = CARTESIAN_POINT('',(0.,0.,0.));
#75 = DIRECTION('',(0.,0.,1.));
#76 = DIRECTION('',(1.,0.,-0.));
#77 = DEFINITIONAL_REPRESENTATION('',(#78),#82);
#78 = LINE('',#79,#80);
#79 = CARTESIAN_POINT('',(0.,200.));
#80 = VECTOR('',#81,1.);
#81 = DIRECTION('',(0.,-1.));
#82 = ( GEOMETRIC_REPRESENTATION_CONTEXT(2)
PARAMETRIC_REPRESENTATION_CONTEXT() REPRESENTATION_CONTEXT('2D SPACE',''
) );
#83 = ORIENTED_EDGE('',*,*,#84,.F.);
#84 = EDGE_CURVE('',#85,#57,#87,.T.);
#85 = VERTEX_POINT('',#86);
#86 = CARTESIAN_POINT('',(0.,200.,200.));
#87 = SURFACE_CURVE('',#88,(#92,#99),.PCURVE_S1.);
#88 = LINE('',#89,#90);
#89 = CARTESIAN_POINT('',(0.,200.,200.));
#90 = VECTOR('',#91,1.);
#91 = DIRECTION('',(0.,0.,-1.));
#92 = PCURVE('',#32,#93);
#93 = DEFINITIONAL_REPRESENTATION('',(#94),#98);
#94 = LINE('',#95,#96);
#95 = CARTESIAN_POINT('',(200.,-200.));
#96 = VECTOR('',#97,1.);
#97 = DIRECTION('',(-1.,0.));
#98 = ( GEOMETRIC_REPRESENTATION_CONTEXT(2)
PARAMETRIC_REPRESENTATION_CONTEXT() REPRESENTATION_CONTEXT('2D SPACE',''
) );
#99 = PCURVE('',#100,#105);
#100 = PLANE('',#101);
#101 = AXIS2_PLACEMENT_3D('',#102,#103,#104);
#102 = CARTESIAN_POINT('',(0.,200.,0.));
#103 = DIRECTION('',(-0.,1.,0.));
#104 = DIRECTION('',(0.,0.,1.));
#105 = DEFINITIONAL_REPRESENTATION('',(#106),#110);
#106 = LINE('',#107,#108);
#107 = CARTESIAN_POINT('',(200.,0.));
#108 = VECTOR('',#109,1.);
#109 = DIRECTION('',(-1.,0.));
#110 = ( GEOMETRIC_REPRESENTATION_CONTEXT(2)
PARAMETRIC_REPRESENTATION_CONTEXT() REPRESENTATION_CONTEXT('2D SPACE',''
) );
#111 = ORIENTED_EDGE('',*,*,#112,.F.);
#112 = EDGE_CURVE('',#113,#85,#115,.T.);
#113 = VERTEX_POINT('',#114);
#114 = CARTESIAN_POINT('',(0.,100.,200.));
#115 = SURFACE_CURVE('',#116,(#120,#127),.PCURVE_S1.);
#116 = LINE('',#117,#118);
#117 = CARTESIAN_POINT('',(0.,100.,200.));
#118 = VECTOR('',#119,1.);
#119 = DIRECTION('',(-0.,1.,0.));
#120 = PCURVE('',#32,#121);
#121 = DEFINITIONAL_REPRESENTATION('',(#122),#126);
#122 = LINE('',#123,#124);
#123 = CARTESIAN_POINT('',(200.,-100.));
#124 = VECTOR('',#125,1.);
#125 = DIRECTION('',(0.,-1.));
#126 = ( GEOMETRIC_REPRESENTATION_CONTEXT(2)
PARAMETRIC_REPRESENTATION_CONTEXT() REPRESENTATION_CONTEXT('2D SPACE',''
) );
#127 = PCURVE('',#128,#133);
#128 = PLANE('',#129);
#129 = AXIS2_PLACEMENT_3D('',#130,#131,#132);
#130 = CARTESIAN_POINT('',(0.,100.,200.));
#131 = DIRECTION('',(0.,0.,1.));
#132 = DIRECTION('',(1.,0.,-0.));
#133 = DEFINITIONAL_REPRESENTATION('',(#134),#138);
#134 = LINE('',#135,#136);
#135 = CARTESIAN_POINT('',(0.,0.));
#136 = VECTOR('',#137,1.);
#137 = DIRECTION('',(0.,1.));
#138 = ( GEOMETRIC_REPRESENTATION_CONTEXT(2)
PARAMETRIC_REPRESENTATION_CONTEXT() REPRESENTATION_CONTEXT('2D SPACE',''
) );
#139 = ORIENTED_EDGE('',*,*,#140,.F.);
#140 = EDGE_CURVE('',#141,#113,#143,.T.);
#141 = VERTEX_POINT('',#142);
#142 = CARTESIAN_POINT('',(0.,100.,100.));
#143 = SURFACE_CURVE('',#144,(#148,#155),.PCURVE_S1.);
#144 = LINE('',#145,#146);
#145 = CARTESIAN_POINT('',(0.,100.,100.));
#146 = VECTOR('',#147,1.);
#147 = DIRECTION('',(0.,0.,1.));
#148 = PCURVE('',#32,#149);
#149 = DEFINITIONAL_REPRESENTATION('',(#150),#154);
#150 = LINE('',#151,#152);
#151 = CARTESIAN_POINT('',(100.,-100.));
#152 = VECTOR('',#153,1.);
#153 = DIRECTION('',(1.,0.));
#154 = ( GEOMETRIC_REPRESENTATION_CONTEXT(2)
PARAMETRIC_REPRESENTATION_CONTEXT() REPRESENTATION_CONTEXT('2D SPACE',''
) );
#155 = PCURVE('',#156,#161);
#156 = PLANE('',#157);
#157 = AXIS2_PLACEMENT_3D('',#158,#159,#160);
#158 = CARTESIAN_POINT('',(0.,100.,100.));
#159 = DIRECTION('',(-0.,1.,0.));
#160 = DIRECTION('',(0.,0.,1.));
#161 = DEFINITIONAL_REPRESENTATION('',(#162),#166);
#162 = LINE('',#163,#164);
#163 = CARTESIAN_POINT('',(0.,0.));
#164 = VECTOR('',#165,1.);
#165 = DIRECTION('',(1.,0.));
#166 = ( GEOMETRIC_REPRESENTATION_CONTEXT(2)
PARAMETRIC_REPRESENTATION_CONTEXT() REPRESENTATION_CONTEXT('2D SPACE',''
) );
#167 = ORIENTED_EDGE('',*,*,#168,.F.);
#168 = EDGE_CURVE('',#24,#141,#169,.T.);
#169 = SURFACE_CURVE('',#170,(#174,#181),.PCURVE_S1.);
#170 = LINE('',#171,#172);
#171 = CARTESIAN_POINT('',(0.,0.,100.));
#172 = VECTOR('',#173,1.);
#173 = DIRECTION('',(-0.,1.,0.));
#174 = PCURVE('',#32,#175);
#175 = DEFINITIONAL_REPRESENTATION('',(#176),#180);
#176 = LINE('',#177,#178);
#177 = CARTESIAN_POINT('',(100.,0.));
#178 = VECTOR('',#179,1.);
#179 = DIRECTION('',(0.,-1.));
#180 = ( GEOMETRIC_REPRESENTATION_CONTEXT(2)
PARAMETRIC_REPRESENTATION_CONTEXT() REPRESENTATION_CONTEXT('2D SPACE',''
) );
#181 = PCURVE('',#182,#187);
#182 = PLANE('',#183);
#183 = AXIS2_PLACEMENT_3D('',#184,#185,#186);
#184 = CARTESIAN_POINT('',(0.,0.,100.));
#185 = DIRECTION('',(0.,0.,1.));
#186 = DIRECTION('',(1.,0.,-0.));
#187 = DEFINITIONAL_REPRESENTATION('',(#188),#192);
#188 = LINE('',#189,#190);
#189 = CARTESIAN_POINT('',(0.,0.));
#190 = VECTOR('',#191,1.);
#191 = DIRECTION('',(0.,1.));
#192 = ( GEOMETRIC_REPRESENTATION_CONTEXT(2)
PARAMETRIC_REPRESENTATION_CONTEXT() REPRESENTATION_CONTEXT('2D SPACE',''
) );
#193 = ADVANCED_FACE('',(#194),#182,.T.);
#194 = FACE_BOUND('',#195,.T.);
#195 = EDGE_LOOP('',(#196,#197,#220,#248));
#196 = ORIENTED_EDGE('',*,*,#168,.F.);
#197 = ORIENTED_EDGE('',*,*,#198,.T.);
#198 = EDGE_CURVE('',#24,#199,#201,.T.);
#199 = VERTEX_POINT('',#200);
#200 = CARTESIAN_POINT('',(100.,0.,100.));
#201 = SURFACE_CURVE('',#202,(#206,#213),.PCURVE_S1.);
#202 = LINE('',#203,#204);
#203 = CARTESIAN_POINT('',(0.,0.,100.));
#204 = VECTOR('',#205,1.);
#205 = DIRECTION('',(1.,0.,-0.));
#206 = PCURVE('',#182,#207);
#207 = DEFINITIONAL_REPRESENTATION('',(#208),#212);
#208 = LINE('',#209,#210);
#209 = CARTESIAN_POINT('',(0.,0.));
#210 = VECTOR('',#211,1.);
#211 = DIRECTION('',(1.,0.));
#212 = ( GEOMETRIC_REPRESENTATION_CONTEXT(2)
PARAMETRIC_REPRESENTATION_CONTEXT() REPRESENTATION_CONTEXT('2D SPACE',''
) );
#213 = PCURVE('',#44,#214);
#214 = DEFINITIONAL_REPRESENTATION('',(#215),#219);
#215 = LINE('',#216,#217);
#216 = CARTESIAN_POINT('',(100.,0.));
#217 = VECTOR('',#218,1.);
#218 = DIRECTION('',(0.,1.));
#219 = ( GEOMETRIC_REPRESENTATION_CONTEXT(2)
PARAMETRIC_REPRESENTATION_CONTEXT() REPRESENTATION_CONTEXT('2D SPACE',''
) );
#220 = ORIENTED_EDGE('',*,*,#221,.T.);
#221 = EDGE_CURVE('',#199,#222,#224,.T.);
#222 = VERTEX_POINT('',#223);
#223 = CARTESIAN_POINT('',(100.,100.,100.));
#224 = SURFACE_CURVE('',#225,(#229,#236),.PCURVE_S1.);
#225 = LINE('',#226,#227);
#226 = CARTESIAN_POINT('',(100.,0.,100.));
#227 = VECTOR('',#228,1.);
#228 = DIRECTION('',(-0.,1.,0.));
#229 = PCURVE('',#182,#230);
#230 = DEFINITIONAL_REPRESENTATION('',(#231),#235);
#231 = LINE('',#232,#233);
#232 = CARTESIAN_POINT('',(100.,0.));
#233 = VECTOR('',#234,1.);
#234 = DIRECTION('',(0.,1.));
#235 = ( GEOMETRIC_REPRESENTATION_CONTEXT(2)
PARAMETRIC_REPRESENTATION_CONTEXT() REPRESENTATION_CONTEXT('2D SPACE',''
) );
#236 = PCURVE('',#237,#242);
#237 = PLANE('',#238);
#238 = AXIS2_PLACEMENT_3D('',#239,#240,#241);
#239 = CARTESIAN_POINT('',(100.,0.,100.));
#240 = DIRECTION('',(1.,0.,-0.));
#241 = DIRECTION('',(0.,0.,1.));
#242 = DEFINITIONAL_REPRESENTATION('',(#243),#247);
#243 = LINE('',#244,#245);
#244 = CARTESIAN_POINT('',(0.,0.));
#245 = VECTOR('',#246,1.);
#246 = DIRECTION('',(0.,-1.));
#247 = ( GEOMETRIC_REPRESENTATION_CONTEXT(2)
PARAMETRIC_REPRESENTATION_CONTEXT() REPRESENTATION_CONTEXT('2D SPACE',''
) );
#248 = ORIENTED_EDGE('',*,*,#249,.F.);
#249 = EDGE_CURVE('',#141,#222,#250,.T.);
#250 = SURFACE_CURVE('',#251,(#255,#262),.PCURVE_S1.);
#251 = LINE('',#252,#253);
#252 = CARTESIAN_POINT('',(0.,100.,100.));
#253 = VECTOR('',#254,1.);
#254 = DIRECTION('',(1.,0.,-0.));
#255 = PCURVE('',#182,#256);
#256 = DEFINITIONAL_REPRESENTATION('',(#257),#261);
#257 = LINE('',#258,#259);
#258 = CARTESIAN_POINT('',(0.,100.));
#259 = VECTOR('',#260,1.);
#260 = DIRECTION('',(1.,0.));
#261 = ( GEOMETRIC_REPRESENTATION_CONTEXT(2)
PARAMETRIC_REPRESENTATION_CONTEXT() REPRESENTATION_CONTEXT('2D SPACE',''
) );
#262 = PCURVE('',#156,#263);
#263 = DEFINITIONAL_REPRESENTATION('',(#264),#268);
#264 = LINE('',#265,#266);
#265 = CARTESIAN_POINT('',(0.,0.));
#266 = VECTOR('',#267,1.);
#267 = DIRECTION('',(0.,1.));
#268 = ( GEOMETRIC_REPRESENTATION_CONTEXT(2)
PARAMETRIC_REPRESENTATION_CONTEXT() REPRESENTATION_CONTEXT('2D SPACE',''
) );
#269 = ADVANCED_FACE('',(#270),#237,.F.);
#270 = FACE_BOUND('',#271,.F.);
#271 = EDGE_LOOP('',(#272,#300,#301));
#272 = ORIENTED_EDGE('',*,*,#273,.F.);
#273 = EDGE_CURVE('',#199,#274,#276,.T.);
#274 = VERTEX_POINT('',#275);
#275 = CARTESIAN_POINT('',(100.,100.,200.));
#276 = SURFACE_CURVE('',#277,(#281,#288),.PCURVE_S1.);
#277 = LINE('',#278,#279);
#278 = CARTESIAN_POINT('',(100.,50.,150.));
#279 = VECTOR('',#280,1.);
#280 = DIRECTION('',(0.,0.707106781187,0.707106781187));
#281 = PCURVE('',#237,#282);
#282 = DEFINITIONAL_REPRESENTATION('',(#283),#287);
#283 = LINE('',#284,#285);
#284 = CARTESIAN_POINT('',(50.,-50.));
#285 = VECTOR('',#286,1.);
#286 = DIRECTION('',(0.707106781187,-0.707106781187));
#287 = ( GEOMETRIC_REPRESENTATION_CONTEXT(2)
PARAMETRIC_REPRESENTATION_CONTEXT() REPRESENTATION_CONTEXT('2D SPACE',''
) );
#288 = PCURVE('',#289,#294);
#289 = PLANE('',#290);
#290 = AXIS2_PLACEMENT_3D('',#291,#292,#293);
#291 = CARTESIAN_POINT('',(0.,100.,200.));
#292 = DIRECTION('',(0.,0.707106781187,-0.707106781187));
#293 = DIRECTION('',(0.,0.707106781187,0.707106781187));
#294 = DEFINITIONAL_REPRESENTATION('',(#295),#299);
#295 = LINE('',#296,#297);
#296 = CARTESIAN_POINT('',(-70.71067811865,100.));
#297 = VECTOR('',#298,1.);
#298 = DIRECTION('',(1.,0.));
#299 = ( GEOMETRIC_REPRESENTATION_CONTEXT(2)
PARAMETRIC_REPRESENTATION_CONTEXT() REPRESENTATION_CONTEXT('2D SPACE',''
) );
#300 = ORIENTED_EDGE('',*,*,#221,.T.);
#301 = ORIENTED_EDGE('',*,*,#302,.T.);
#302 = EDGE_CURVE('',#222,#274,#303,.T.);
#303 = SURFACE_CURVE('',#304,(#308,#315),.PCURVE_S1.);
#304 = LINE('',#305,#306);
#305 = CARTESIAN_POINT('',(100.,100.,100.));
#306 = VECTOR('',#307,1.);
#307 = DIRECTION('',(0.,0.,1.));
#308 = PCURVE('',#237,#309);
#309 = DEFINITIONAL_REPRESENTATION('',(#310),#314);
#310 = LINE('',#311,#312);
#311 = CARTESIAN_POINT('',(0.,-100.));
#312 = VECTOR('',#313,1.);
#313 = DIRECTION('',(1.,0.));
#314 = ( GEOMETRIC_REPRESENTATION_CONTEXT(2)
PARAMETRIC_REPRESENTATION_CONTEXT() REPRESENTATION_CONTEXT('2D SPACE',''
) );
#315 = PCURVE('',#156,#316);
#316 = DEFINITIONAL_REPRESENTATION('',(#317),#321);
#317 = LINE('',#318,#319);
#318 = CARTESIAN_POINT('',(0.,100.));
#319 = VECTOR('',#320,1.);
#320 = DIRECTION('',(1.,0.));
#321 = ( GEOMETRIC_REPRESENTATION_CONTEXT(2)
PARAMETRIC_REPRESENTATION_CONTEXT() REPRESENTATION_CONTEXT('2D SPACE',''
) );
#322 = ADVANCED_FACE('',(#323),#289,.F.);
#323 = FACE_BOUND('',#324,.F.);
#324 = EDGE_LOOP('',(#325,#355,#376,#377));
#325 = ORIENTED_EDGE('',*,*,#326,.F.);
#326 = EDGE_CURVE('',#327,#329,#331,.T.);
#327 = VERTEX_POINT('',#328);
#328 = CARTESIAN_POINT('',(200.,0.,100.));
#329 = VERTEX_POINT('',#330);
#330 = CARTESIAN_POINT('',(200.,100.,200.));
#331 = SURFACE_CURVE('',#332,(#336,#343),.PCURVE_S1.);
#332 = LINE('',#333,#334);
#333 = CARTESIAN_POINT('',(200.,50.,150.));
#334 = VECTOR('',#335,1.);
#335 = DIRECTION('',(0.,0.707106781187,0.707106781187));
#336 = PCURVE('',#289,#337);
#337 = DEFINITIONAL_REPRESENTATION('',(#338),#342);
#338 = LINE('',#339,#340);
#339 = CARTESIAN_POINT('',(-70.71067811865,200.));
#340 = VECTOR('',#341,1.);
#341 = DIRECTION('',(1.,0.));
#342 = ( GEOMETRIC_REPRESENTATION_CONTEXT(2)
PARAMETRIC_REPRESENTATION_CONTEXT() REPRESENTATION_CONTEXT('2D SPACE',''
) );
#343 = PCURVE('',#344,#349);
#344 = PLANE('',#345);
#345 = AXIS2_PLACEMENT_3D('',#346,#347,#348);
#346 = CARTESIAN_POINT('',(200.,0.,0.));
#347 = DIRECTION('',(1.,0.,-0.));
#348 = DIRECTION('',(0.,0.,1.));
#349 = DEFINITIONAL_REPRESENTATION('',(#350),#354);
#350 = LINE('',#351,#352);
#351 = CARTESIAN_POINT('',(150.,-50.));
#352 = VECTOR('',#353,1.);
#353 = DIRECTION('',(0.707106781187,-0.707106781187));
#354 = ( GEOMETRIC_REPRESENTATION_CONTEXT(2)
PARAMETRIC_REPRESENTATION_CONTEXT() REPRESENTATION_CONTEXT('2D SPACE',''
) );
#355 = ORIENTED_EDGE('',*,*,#356,.F.);
#356 = EDGE_CURVE('',#199,#327,#357,.T.);
#357 = SURFACE_CURVE('',#358,(#362,#369),.PCURVE_S1.);
#358 = LINE('',#359,#360);
#359 = CARTESIAN_POINT('',(100.,0.,100.));
#360 = VECTOR('',#361,1.);
#361 = DIRECTION('',(1.,0.,-0.));
#362 = PCURVE('',#289,#363);
#363 = DEFINITIONAL_REPRESENTATION('',(#364),#368);
#364 = LINE('',#365,#366);
#365 = CARTESIAN_POINT('',(-141.4213562373,100.));
#366 = VECTOR('',#367,1.);
#367 = DIRECTION('',(0.,1.));
#368 = ( GEOMETRIC_REPRESENTATION_CONTEXT(2)
PARAMETRIC_REPRESENTATION_CONTEXT() REPRESENTATION_CONTEXT('2D SPACE',''
) );
#369 = PCURVE('',#44,#370);
#370 = DEFINITIONAL_REPRESENTATION('',(#371),#375);
#371 = LINE('',#372,#373);
#372 = CARTESIAN_POINT('',(100.,100.));
#373 = VECTOR('',#374,1.);
#374 = DIRECTION('',(0.,1.));
#375 = ( GEOMETRIC_REPRESENTATION_CONTEXT(2)
PARAMETRIC_REPRESENTATION_CONTEXT() REPRESENTATION_CONTEXT('2D SPACE',''
) );
#376 = ORIENTED_EDGE('',*,*,#273,.T.);
#377 = ORIENTED_EDGE('',*,*,#378,.T.);
#378 = EDGE_CURVE('',#274,#329,#379,.T.);
#379 = SURFACE_CURVE('',#380,(#384,#391),.PCURVE_S1.);
#380 = LINE('',#381,#382);
#381 = CARTESIAN_POINT('',(100.,100.,200.));
#382 = VECTOR('',#383,1.);
#383 = DIRECTION('',(1.,0.,-0.));
#384 = PCURVE('',#289,#385);
#385 = DEFINITIONAL_REPRESENTATION('',(#386),#390);
#386 = LINE('',#387,#388);
#387 = CARTESIAN_POINT('',(0.,100.));
#388 = VECTOR('',#389,1.);
#389 = DIRECTION('',(0.,1.));
#390 = ( GEOMETRIC_REPRESENTATION_CONTEXT(2)
PARAMETRIC_REPRESENTATION_CONTEXT() REPRESENTATION_CONTEXT('2D SPACE',''
) );
#391 = PCURVE('',#128,#392);
#392 = DEFINITIONAL_REPRESENTATION('',(#393),#397);
#393 = LINE('',#394,#395);
#394 = CARTESIAN_POINT('',(100.,0.));
#395 = VECTOR('',#396,1.);
#396 = DIRECTION('',(1.,0.));
#397 = ( GEOMETRIC_REPRESENTATION_CONTEXT(2)
PARAMETRIC_REPRESENTATION_CONTEXT() REPRESENTATION_CONTEXT('2D SPACE',''
) );
#398 = ADVANCED_FACE('',(#399),#344,.T.);
#399 = FACE_BOUND('',#400,.T.);
#400 = EDGE_LOOP('',(#401,#424,#447,#470,#491));
#401 = ORIENTED_EDGE('',*,*,#402,.F.);
#402 = EDGE_CURVE('',#403,#327,#405,.T.);
#403 = VERTEX_POINT('',#404);
#404 = CARTESIAN_POINT('',(200.,0.,0.));
#405 = SURFACE_CURVE('',#406,(#410,#417),.PCURVE_S1.);
#406 = LINE('',#407,#408);
#407 = CARTESIAN_POINT('',(200.,0.,0.));
#408 = VECTOR('',#409,1.);
#409 = DIRECTION('',(0.,0.,1.));
#410 = PCURVE('',#344,#411);
#411 = DEFINITIONAL_REPRESENTATION('',(#412),#416);
#412 = LINE('',#413,#414);
#413 = CARTESIAN_POINT('',(0.,0.));
#414 = VECTOR('',#415,1.);
#415 = DIRECTION('',(1.,0.));
#416 = ( GEOMETRIC_REPRESENTATION_CONTEXT(2)
PARAMETRIC_REPRESENTATION_CONTEXT() REPRESENTATION_CONTEXT('2D SPACE',''
) );
#417 = PCURVE('',#44,#418);
#418 = DEFINITIONAL_REPRESENTATION('',(#419),#423);
#419 = LINE('',#420,#421);
#420 = CARTESIAN_POINT('',(0.,200.));
#421 = VECTOR('',#422,1.);
#422 = DIRECTION('',(1.,0.));
#423 = ( GEOMETRIC_REPRESENTATION_CONTEXT(2)
PARAMETRIC_REPRESENTATION_CONTEXT() REPRESENTATION_CONTEXT('2D SPACE',''
) );
#424 = ORIENTED_EDGE('',*,*,#425,.F.);
#425 = EDGE_CURVE('',#426,#403,#428,.T.);
#426 = VERTEX_POINT('',#427);
#427 = CARTESIAN_POINT('',(200.,200.,0.));
#428 = SURFACE_CURVE('',#429,(#433,#440),.PCURVE_S1.);
#429 = LINE('',#430,#431);
#430 = CARTESIAN_POINT('',(200.,200.,0.));
#431 = VECTOR('',#432,1.);
#432 = DIRECTION('',(0.,-1.,0.));
#433 = PCURVE('',#344,#434);
#434 = DEFINITIONAL_REPRESENTATION('',(#435),#439);
#435 = LINE('',#436,#437);
#436 = CARTESIAN_POINT('',(0.,-200.));
#437 = VECTOR('',#438,1.);
#438 = DIRECTION('',(0.,1.));
#439 = ( GEOMETRIC_REPRESENTATION_CONTEXT(2)
PARAMETRIC_REPRESENTATION_CONTEXT() REPRESENTATION_CONTEXT('2D SPACE',''
) );
#440 = PCURVE('',#72,#441);
#441 = DEFINITIONAL_REPRESENTATION('',(#442),#446);
#442 = LINE('',#443,#444);
#443 = CARTESIAN_POINT('',(200.,200.));
#444 = VECTOR('',#445,1.);
#445 = DIRECTION('',(0.,-1.));
#446 = ( GEOMETRIC_REPRESENTATION_CONTEXT(2)
PARAMETRIC_REPRESENTATION_CONTEXT() REPRESENTATION_CONTEXT('2D SPACE',''
) );
#447 = ORIENTED_EDGE('',*,*,#448,.F.);
#448 = EDGE_CURVE('',#449,#426,#451,.T.);
#449 = VERTEX_POINT('',#450);
#450 = CARTESIAN_POINT('',(200.,200.,200.));
#451 = SURFACE_CURVE('',#452,(#456,#463),.PCURVE_S1.);
#452 = LINE('',#453,#454);
#453 = CARTESIAN_POINT('',(200.,200.,200.));
#454 = VECTOR('',#455,1.);
#455 = DIRECTION('',(0.,0.,-1.));
#456 = PCURVE('',#344,#457);
#457 = DEFINITIONAL_REPRESENTATION('',(#458),#462);
#458 = LINE('',#459,#460);
#459 = CARTESIAN_POINT('',(200.,-200.));
#460 = VECTOR('',#461,1.);
#461 = DIRECTION('',(-1.,0.));
#462 = ( GEOMETRIC_REPRESENTATION_CONTEXT(2)
PARAMETRIC_REPRESENTATION_CONTEXT() REPRESENTATION_CONTEXT('2D SPACE',''
) );
#463 = PCURVE('',#100,#464);
#464 = DEFINITIONAL_REPRESENTATION('',(#465),#469);
#465 = LINE('',#466,#467);
#466 = CARTESIAN_POINT('',(200.,200.));
#467 = VECTOR('',#468,1.);
#468 = DIRECTION('',(-1.,0.));
#469 = ( GEOMETRIC_REPRESENTATION_CONTEXT(2)
PARAMETRIC_REPRESENTATION_CONTEXT() REPRESENTATION_CONTEXT('2D SPACE',''
) );
#470 = ORIENTED_EDGE('',*,*,#471,.F.);
#471 = EDGE_CURVE('',#329,#449,#472,.T.);
#472 = SURFACE_CURVE('',#473,(#477,#484),.PCURVE_S1.);
#473 = LINE('',#474,#475);
#474 = CARTESIAN_POINT('',(200.,100.,200.));
#475 = VECTOR('',#476,1.);
#476 = DIRECTION('',(-0.,1.,0.));
#477 = PCURVE('',#344,#478);
#478 = DEFINITIONAL_REPRESENTATION('',(#479),#483);
#479 = LINE('',#480,#481);
#480 = CARTESIAN_POINT('',(200.,-100.));
#481 = VECTOR('',#482,1.);
#482 = DIRECTION('',(0.,-1.));
#483 = ( GEOMETRIC_REPRESENTATION_CONTEXT(2)
PARAMETRIC_REPRESENTATION_CONTEXT() REPRESENTATION_CONTEXT('2D SPACE',''
) );
#484 = PCURVE('',#128,#485);
#485 = DEFINITIONAL_REPRESENTATION('',(#486),#490);
#486 = LINE('',#487,#488);
#487 = CARTESIAN_POINT('',(200.,0.));
#488 = VECTOR('',#489,1.);
#489 = DIRECTION('',(0.,1.));
#490 = ( GEOMETRIC_REPRESENTATION_CONTEXT(2)
PARAMETRIC_REPRESENTATION_CONTEXT() REPRESENTATION_CONTEXT('2D SPACE',''
) );
#491 = ORIENTED_EDGE('',*,*,#326,.F.);
#492 = ADVANCED_FACE('',(#493),#156,.F.);
#493 = FACE_BOUND('',#494,.F.);
#494 = EDGE_LOOP('',(#495,#496,#497,#518));
#495 = ORIENTED_EDGE('',*,*,#249,.F.);
#496 = ORIENTED_EDGE('',*,*,#140,.T.);
#497 = ORIENTED_EDGE('',*,*,#498,.T.);
#498 = EDGE_CURVE('',#113,#274,#499,.T.);
#499 = SURFACE_CURVE('',#500,(#504,#511),.PCURVE_S1.);
#500 = LINE('',#501,#502);
#501 = CARTESIAN_POINT('',(0.,100.,200.));
#502 = VECTOR('',#503,1.);
#503 = DIRECTION('',(1.,0.,-0.));
#504 = PCURVE('',#156,#505);
#505 = DEFINITIONAL_REPRESENTATION('',(#506),#510);
#506 = LINE('',#507,#508);
#507 = CARTESIAN_POINT('',(100.,0.));
#508 = VECTOR('',#509,1.);
#509 = DIRECTION('',(0.,1.));
#510 = ( GEOMETRIC_REPRESENTATION_CONTEXT(2)
PARAMETRIC_REPRESENTATION_CONTEXT() REPRESENTATION_CONTEXT('2D SPACE',''
) );
#511 = PCURVE('',#128,#512);
#512 = DEFINITIONAL_REPRESENTATION('',(#513),#517);
#513 = LINE('',#514,#515);
#514 = CARTESIAN_POINT('',(0.,0.));
#515 = VECTOR('',#516,1.);
#516 = DIRECTION('',(1.,0.));
#517 = ( GEOMETRIC_REPRESENTATION_CONTEXT(2)
PARAMETRIC_REPRESENTATION_CONTEXT() REPRESENTATION_CONTEXT('2D SPACE',''
) );
#518 = ORIENTED_EDGE('',*,*,#302,.F.);
#519 = ADVANCED_FACE('',(#520),#128,.T.);
#520 = FACE_BOUND('',#521,.T.);
#521 = EDGE_LOOP('',(#522,#523,#524,#525,#526));
#522 = ORIENTED_EDGE('',*,*,#112,.F.);
#523 = ORIENTED_EDGE('',*,*,#498,.T.);
#524 = ORIENTED_EDGE('',*,*,#378,.T.);
#525 = ORIENTED_EDGE('',*,*,#471,.T.);
#526 = ORIENTED_EDGE('',*,*,#527,.F.);
#527 = EDGE_CURVE('',#85,#449,#528,.T.);
#528 = SURFACE_CURVE('',#529,(#533,#540),.PCURVE_S1.);
#529 = LINE('',#530,#531);
#530 = CARTESIAN_POINT('',(0.,200.,200.));
#531 = VECTOR('',#532,1.);
#532 = DIRECTION('',(1.,0.,0.));
#533 = PCURVE('',#128,#534);
#534 = DEFINITIONAL_REPRESENTATION('',(#535),#539);
#535 = LINE('',#536,#537);
#536 = CARTESIAN_POINT('',(0.,100.));
#537 = VECTOR('',#538,1.);
#538 = DIRECTION('',(1.,0.));
#539 = ( GEOMETRIC_REPRESENTATION_CONTEXT(2)
PARAMETRIC_REPRESENTATION_CONTEXT() REPRESENTATION_CONTEXT('2D SPACE',''
) );
#540 = PCURVE('',#100,#541);
#541 = DEFINITIONAL_REPRESENTATION('',(#542),#546);
#542 = LINE('',#543,#544);
#543 = CARTESIAN_POINT('',(200.,0.));
#544 = VECTOR('',#545,1.);
#545 = DIRECTION('',(0.,1.));
#546 = ( GEOMETRIC_REPRESENTATION_CONTEXT(2)
PARAMETRIC_REPRESENTATION_CONTEXT() REPRESENTATION_CONTEXT('2D SPACE',''
) );
#547 = ADVANCED_FACE('',(#548),#100,.T.);
#548 = FACE_BOUND('',#549,.T.);
#549 = EDGE_LOOP('',(#550,#571,#572,#573));
#550 = ORIENTED_EDGE('',*,*,#551,.F.);
#551 = EDGE_CURVE('',#57,#426,#552,.T.);
#552 = SURFACE_CURVE('',#553,(#557,#564),.PCURVE_S1.);
#553 = LINE('',#554,#555);
#554 = CARTESIAN_POINT('',(0.,200.,0.));
#555 = VECTOR('',#556,1.);
#556 = DIRECTION('',(1.,0.,0.));
#557 = PCURVE('',#100,#558);
#558 = DEFINITIONAL_REPRESENTATION('',(#559),#563);
#559 = LINE('',#560,#561);
#560 = CARTESIAN_POINT('',(0.,0.));
#561 = VECTOR('',#562,1.);
#562 = DIRECTION('',(0.,1.));
#563 = ( GEOMETRIC_REPRESENTATION_CONTEXT(2)
PARAMETRIC_REPRESENTATION_CONTEXT() REPRESENTATION_CONTEXT('2D SPACE',''
) );
#564 = PCURVE('',#72,#565);
#565 = DEFINITIONAL_REPRESENTATION('',(#566),#570);
#566 = LINE('',#567,#568);
#567 = CARTESIAN_POINT('',(0.,200.));
#568 = VECTOR('',#569,1.);
#569 = DIRECTION('',(1.,0.));
#570 = ( GEOMETRIC_REPRESENTATION_CONTEXT(2)
PARAMETRIC_REPRESENTATION_CONTEXT() REPRESENTATION_CONTEXT('2D SPACE',''
) );
#571 = ORIENTED_EDGE('',*,*,#84,.F.);
#572 = ORIENTED_EDGE('',*,*,#527,.T.);
#573 = ORIENTED_EDGE('',*,*,#448,.T.);
#574 = ADVANCED_FACE('',(#575),#72,.F.);
#575 = FACE_BOUND('',#576,.F.);
#576 = EDGE_LOOP('',(#577,#598,#599,#600));
#577 = ORIENTED_EDGE('',*,*,#578,.T.);
#578 = EDGE_CURVE('',#22,#403,#579,.T.);
#579 = SURFACE_CURVE('',#580,(#584,#591),.PCURVE_S1.);
#580 = LINE('',#581,#582);
#581 = CARTESIAN_POINT('',(0.,0.,0.));
#582 = VECTOR('',#583,1.);
#583 = DIRECTION('',(1.,0.,0.));
#584 = PCURVE('',#72,#585);
#585 = DEFINITIONAL_REPRESENTATION('',(#586),#590);
#586 = LINE('',#587,#588);
#587 = CARTESIAN_POINT('',(0.,0.));
#588 = VECTOR('',#589,1.);
#589 = DIRECTION('',(1.,0.));
#590 = ( GEOMETRIC_REPRESENTATION_CONTEXT(2)
PARAMETRIC_REPRESENTATION_CONTEXT() REPRESENTATION_CONTEXT('2D SPACE',''
) );
#591 = PCURVE('',#44,#592);
#592 = DEFINITIONAL_REPRESENTATION('',(#593),#597);
#593 = LINE('',#594,#595);
#594 = CARTESIAN_POINT('',(0.,0.));
#595 = VECTOR('',#596,1.);
#596 = DIRECTION('',(0.,1.));
#597 = ( GEOMETRIC_REPRESENTATION_CONTEXT(2)
PARAMETRIC_REPRESENTATION_CONTEXT() REPRESENTATION_CONTEXT('2D SPACE',''
) );
#598 = ORIENTED_EDGE('',*,*,#425,.F.);
#599 = ORIENTED_EDGE('',*,*,#551,.F.);
#600 = ORIENTED_EDGE('',*,*,#56,.T.);
#601 = ADVANCED_FACE('',(#602),#44,.F.);
#602 = FACE_BOUND('',#603,.F.);
#603 = EDGE_LOOP('',(#604,#605,#606,#607,#608));
#604 = ORIENTED_EDGE('',*,*,#578,.F.);
#605 = ORIENTED_EDGE('',*,*,#21,.T.);
#606 = ORIENTED_EDGE('',*,*,#198,.T.);
#607 = ORIENTED_EDGE('',*,*,#356,.T.);
#608 = ORIENTED_EDGE('',*,*,#402,.F.);
#609 = ( GEOMETRIC_REPRESENTATION_CONTEXT(3)
GLOBAL_UNCERTAINTY_ASSIGNED_CONTEXT((#613)) GLOBAL_UNIT_ASSIGNED_CONTEXT
((#610,#611,#612)) REPRESENTATION_CONTEXT('Context #1',
'3D Context with UNIT and UNCERTAINTY') );
#610 = ( LENGTH_UNIT() NAMED_UNIT(*) SI_UNIT(.MILLI.,.METRE.) );
#611 = ( NAMED_UNIT(*) PLANE_ANGLE_UNIT() SI_UNIT($,.RADIAN.) );
#612 = ( NAMED_UNIT(*) SI_UNIT($,.STERADIAN.) SOLID_ANGLE_UNIT() );
#613 = UNCERTAINTY_MEASURE_WITH_UNIT(LENGTH_MEASURE(2.E-07),#610,
'distance_accuracy_value','confusion accuracy');
#614 = PRODUCT_RELATED_PRODUCT_CATEGORY('part',$,(#7));
ENDSEC;
END-ISO-10303-21;

View File

@ -0,0 +1,9 @@
# CUBE01
Caso de prueba del profesorado. Cubo de 200 mm con un escalón en una mitad y un chaflán a 45° en la otra.
- Escala: 1:2 (en el cajetín, campo *Title*)
- Cotas: ninguna
- Particularidad: planta en sistema europeo y perfil en sistema americano (mezcla intencionada)
- Volumen esperado: 6 500 000 mm³
- Avisos esperados: `SIN_COTAS`, `PROYECCION_MIXTA`

File diff suppressed because it is too large Load Diff

30
pruebas/test_contrato.py Normal file
View File

@ -0,0 +1,30 @@
"""Comprueba que motor.api cumple el contrato que usa la aplicación."""
from pathlib import Path
import cadquery as cq
from motor.api import Estado, Gravedad, Resultado, exportar, procesar
CATALOGO = Path(__file__).parent / "catalogo"
def test_procesar_devuelve_un_resultado_valido():
r = procesar(CATALOGO / "CUBE01" / "plano.dxf")
assert isinstance(r, Resultado)
assert r.estado in Estado
assert all(a.gravedad in Gravedad for a in r.avisos)
def test_cube01_tiene_el_volumen_esperado():
r = procesar(CATALOGO / "CUBE01" / "plano.dxf")
# Cubo de 200 mm menos el escalón (100^3) y el chaflán (100^3 / 2)
assert abs(r.solido.Volume() - 6_500_000) < 1
def test_exportar_step_y_stl(tmp_path):
r = procesar(CATALOGO / "CUBE01" / "plano.dxf")
for ext in (".step", ".stl"):
exportar(r.solido, tmp_path / f"pieza{ext}")
assert (tmp_path / f"pieza{ext}").stat().st_size > 0
releida = cq.importers.importStep(str(tmp_path / "pieza.step")).val()
assert releida.isValid()

25
pyproject.toml Normal file
View File

@ -0,0 +1,25 @@
[project]
name = "plano3d"
version = "0.1.0"
description = "Reconstrucción automática de piezas 3D a partir de planos técnicos 2D, con verificación del plano"
readme = "README.md"
license = "MIT"
requires-python = ">=3.12,<3.14"
dependencies = [
"cadquery>=2.8",
"ezdxf>=1.4",
"shapely>=2.0",
"numpy>=2.0",
"ortools>=9.10",
"PySide6>=6.7",
]
[dependency-groups]
dev = [
"pytest>=8",
"pyinstaller>=6",
]
[tool.pytest.ini_options]
testpaths = ["pruebas"]
pythonpath = ["."]

1357
uv.lock Normal file

File diff suppressed because it is too large Load Diff