# Plan: GMSH Tessellation — Eliminate Fan Triangles on Cylindrical Surfaces ## Kontext OCC `BRepMesh_IncrementalMesh` erzeugt strukturell fehlerhafte Dreiecke bei periodischen Flächen (Vollzylinder, Ringe): 1. **Fan-Dreiecke** an u=0=2π Naht-Kanten — BRepMesh tesselliert jede Fläche unabhängig. Am Nahtübergang entstehen hochvalente Dreiecke mit Valenz 30-40, weil die Kantenmittelpunkte nicht zur Flächentessellierung konformieren. 2. **Faceting** auf großen Zylindern — angular_deflection (Winkelbedingung) und linear_deflection (Abstandsbedingung) erzeugen unterschiedlich viele Vertices an Kanten vs. Flächeninneres. OCC überbrückt die Lücke mit Fan-Dreiecken. Diese Fehler können **nicht** mit Deflection-Parametern behoben werden — auch Fine-Settings (`0.02 rad / 0.01 mm`) zeigen exakt dieselben Artefakte. Delabella-Algorithmus liefert dieselbe Dreiecksanzahl (332.394 gegenüber DEFAULT 332.394). **Lösung**: GMSH Frontal-Delaunay Mesher via GMSH Python API. GMSH: - Kennt die periodischen Naht-Kanten der B-rep-Topologie - Erzeugt konformierende Netze über Flächen-Grenzen hinweg - Nutzt den OCC-Kernel intern (dieselbe STEP-Repräsentation) - Ist pip-installierbar: `pip install gmsh` → `gmsh 4.15.1` **Scope**: Änderungen nur in `export_step_to_gltf.py` und Dockerfile. Die Sharp-Edge-Pipeline (`_extract_sharp_edge_pairs`), das GLB-Format, Blender-Seite und die XCAF→RWGltf_CafWriter-Kette bleiben unverändert. --- ## Betroffene Dateien | Datei | Änderung | |---|---| | `render-worker/Dockerfile` | `gmsh>=4.15.0` installieren | | `render-worker/scripts/export_step_to_gltf.py` | GMSH-Tessellierung als Alternative zu BRepMesh | | `backend/app/api/routers/admin.py` | Setting `tessellation_engine` (`occ`\|`gmsh`) | | `backend/app/domains/pipeline/tasks/export_glb.py` | Setting lesen, `--tessellation_engine` an CLI-Aufruf übergeben | --- ## Tasks (in Reihenfolge) ### Task 1: Dockerfile — `gmsh` installieren - **Datei**: `render-worker/Dockerfile` - **Was**: Nach der `trimesh`-Zeile einfügen: ```dockerfile # GMSH for Frontal-Delaunay tessellation (alternative to OCC BRepMesh) RUN pip3 install --no-cache-dir "gmsh>=4.15.0" ``` - **Akzeptanzkriterium**: `docker compose exec render-worker python3 -c "import gmsh; print(gmsh.__version__)"` gibt `4.15.x`. - **Abhängigkeiten**: keine ### Task 2: `export_step_to_gltf.py` — CLI-Argument `--tessellation_engine` - **Datei**: `render-worker/scripts/export_step_to_gltf.py` - **Was**: In `parse_args()` ein neues Argument: ```python parser.add_argument( "--tessellation_engine", choices=["occ", "gmsh"], default="occ", help="Tessellation backend: 'occ' = BRepMesh (default), 'gmsh' = Frontal-Delaunay", ) ``` - **Akzeptanzkriterium**: `--help` listet `--tessellation_engine`. - **Abhängigkeiten**: keine ### Task 3: `export_step_to_gltf.py` — Funktion `_tessellate_with_gmsh()` - **Datei**: `render-worker/scripts/export_step_to_gltf.py` - **Was**: Neue Funktion vor `main()`. Nimmt den XCAF-Compound und Deflection-Parameter. Strategie: ``` Für jede Leaf-Shape in der XCAF-Hierarchie: 1. Schreibe die TopoDS_Shape als temporäre .brep-Datei 2. Lade via gmsh.model.occ.importShapes(brep_path) in GMSH-OCC-Kernel 3. Setze Mesh-Parameter: - MeshSizeMin/Max aus linear_deflection - Algorithm = 6 (Frontal-Delaunay) - RecombineAll = 0 (behalte Dreiecke, keine Quads) 4. gmsh.model.mesh.generate(2) — 2D Oberflächen-Mesh 5. Lese Knoten + Dreiecke via gmsh.model.mesh.getNodes() / getElementsByType(2) 6. Baue Poly_Triangulation aus Knoten + Dreiecken 7. Setze per BRep_Builder auf die TopoDS_Face (oder direkt auf die Shell) 8. Lösche tmp-Datei ``` Wichtige OCP-APIs: ```python from OCP.BRep import BRep_Builder from OCP.BRepTools import BRepTools from OCP.Poly import Poly_Triangulation from OCP.TColgp import TColgp_Array1OfPnt from OCP.TShort import TShort_Array1OfShortInteger from OCP.TopExp import TopExp_Explorer from OCP.TopAbs import TopAbs_FACE, TopAbs_SHELL from OCP.TopoDS import TopoDS from OCP.gp import gp_Pnt ``` Konkreter Code-Ablauf für `Poly_Triangulation`-Erstellung: ```python # nodes: list of (x, y, z) in mm # triangles: list of (n1, n2, n3) 1-indexed n_nodes = len(nodes) n_tris = len(triangles) arr_pts = TColgp_Array1OfPnt(1, n_nodes) for idx, (x, y, z) in enumerate(nodes, 1): arr_pts.SetValue(idx, gp_Pnt(x, y, z)) arr_tris = Poly_Array1OfTriangle(1, n_tris) for idx, (a, b, c) in enumerate(triangles, 1): arr_tris.SetValue(idx, Poly_Triangle(a, b, c)) tri = Poly_Triangulation(arr_pts, arr_tris) # BRep_Builder.UpdateFace weist Triangulation einer Face zu builder = BRep_Builder() builder.UpdateFace(face, tri, loc, precision) ``` **Wichtig**: GMSH gibt face-lokale Nodeindices zurück. Die XCAF-Assembly-Location (`TopLoc_Location`) muss für die Koordinatentransformation berücksichtigt werden, damit die Triangulation im richtigen Koordinatenrahmen liegt. **Fallback**: Wenn GMSH für eine bestimmte Face fehlschlägt (z.B. degenerierte Fläche) → BRepMesh für diese Face als Fallback. - **Akzeptanzkriterium**: - `python3 export_step_to_gltf.py --step_path /tmp/test.stp --output_path /tmp/out.glb --tessellation_engine gmsh` läuft ohne Fehler - Log zeigt „GMSH tessellation: N faces processed, M triangles total" - GLB kann in Blender geöffnet werden, keine degenerierten Dreiecke - Keine Fan-Vertices mit Valenz > 10 an Zylindernaht-Kanten - **Abhängigkeiten**: Task 1, Task 2 ### Task 4: Admin-Setting `tessellation_engine` - **Datei**: `backend/app/api/routers/admin.py` - **Was**: In `SETTINGS_DEFAULTS` eintragen: ```python "tessellation_engine": "occ", # "occ" | "gmsh" ``` In `SettingsOut` ergänzen: ```python tessellation_engine: str = "occ" ``` In der Admin-UI-Beschreibung (Docstring oder Kommentar) dokumentieren. - **Akzeptanzkriterium**: `GET /api/admin/settings` gibt `tessellation_engine: "occ"` zurück. - **Abhängigkeiten**: keine ### Task 5: `export_glb.py` — Setting durchreichen - **Datei**: `backend/app/domains/pipeline/tasks/export_glb.py` - **Was**: In `generate_gltf_geometry_task()` (und `generate_gltf_production_task()` wo der OCC-Befehl aufgebaut wird): ```python tessellation_engine = sys_settings.get("tessellation_engine", "occ") # ... occ_cmd = [ ..., "--tessellation_engine", tessellation_engine, ] ``` - **Akzeptanzkriterium**: Admin stellt `tessellation_engine` auf `gmsh` → nächster GLB-Export nutzt GMSH. - **Abhängigkeiten**: Task 2, Task 4 ### Task 6: Frontend — Dropdown in Admin-Settings - **Datei**: `frontend/src/pages/Admin.tsx` - **Was**: Im Tessellation-Settings-Abschnitt ein Select-Element für `tessellation_engine`: ```tsx ``` Kurze Beschreibung: „GMSH erzeugt konformierende Dreiecke ohne Fan-Artefakte an Zylindernaht-Kanten. Verarbeitungszeit: +10-30% pro Modell." - **Akzeptanzkriterium**: Dropdown sichtbar und speichert Setting korrekt. - **Abhängigkeiten**: Task 4 --- ## GMSH-Implementierungsdetails ### GMSH API Ablauf (pseudo-code) ```python import gmsh import tempfile from pathlib import Path from OCP.BRepTools import BRepTools from OCP.TopExp import TopExp_Explorer from OCP.TopAbs import TopAbs_FACE def _tessellate_with_gmsh(shape, linear_deflection: float, angular_deflection: float) -> None: """Replace BRepMesh with GMSH Frontal-Delaunay tessellation.""" gmsh.initialize() gmsh.option.setNumber("General.Terminal", 0) # suppress output gmsh.option.setNumber("Mesh.Algorithm", 6) # Frontal-Delaunay gmsh.option.setNumber("Mesh.RecombineAll", 0) # triangles only gmsh.option.setNumber("Mesh.CharacteristicLengthMin", linear_deflection * 0.5) gmsh.option.setNumber("Mesh.CharacteristicLengthMax", linear_deflection * 5.0) gmsh.option.setNumber("Mesh.AngleToleranceFacetOverlap", angular_deflection * 57.3) with tempfile.NamedTemporaryFile(suffix=".brep", delete=False) as tmp: brep_path = tmp.name try: BRepTools.Write_s(shape, brep_path) gmsh.model.add("shape") gmsh.model.occ.importShapes(brep_path) gmsh.model.occ.synchronize() gmsh.model.mesh.generate(2) # Build Poly_Triangulation per face and write back via BRep_Builder _write_gmsh_triangulation_to_occ(shape) finally: gmsh.finalize() Path(brep_path).unlink(missing_ok=True) ``` ### Poly_Triangulation Write-Back GMSH nummeriert Surfaces durch. Per XCAF-Leaf muss die Entsprechung zwischen GMSH Surface-Tags und OCC TopoDS_Face-Objekten hergestellt werden: - GMSH gibt bei `occ.importShapes()` die Surface-Tags zurück - Die Reihenfolge entspricht der TopExp_Explorer-Iteration über `TopAbs_FACE` - `BRep_Builder.UpdateFace(face, tri_triangulation, location, tolerance)` setzt die Triangulation ### Parameter-Mapping: OCC → GMSH | OCC Parameter | GMSH Entsprechung | |---|---| | `linear_deflection` (mm) | `CharacteristicLengthMax = linear_deflection * 3` | | `angular_deflection` (rad) | `Mesh.MinimumCirclePoints = ceil(2π/angular_deflection)` | --- ## Migrations-Check **Keine Migration erforderlich.** Nur Rendering-Pipeline-Änderungen. `tessellation_engine` wird in `system_settings` gespeichert (bestehendes Key-Value-Store, keine Schema-Änderung). --- ## Reihenfolge-Empfehlung ``` Task 1 (Dockerfile) → rebuild render-worker → Task 2+3 (export_step_to_gltf.py) → manueller Test → Task 4 (admin.py) → Task 5 (export_glb.py) → Task 6 (Frontend) → End-to-End Test: Admin → GMSH → Produkt-GLB regenerieren → Viewer prüfen ``` Manueller Test nach Task 3: ```bash # In render-worker container: python3 /render-scripts/export_step_to_gltf.py \ --step_path /tmp/81113-l_cut.stp \ --output_path /tmp/test_gmsh.glb \ --tessellation_engine gmsh \ --linear_deflection 0.03 \ --angular_deflection 0.05 # Dann Production GLB: /opt/blender/blender --background \ --python /render-scripts/export_gltf.py -- \ --glb_path /tmp/test_gmsh.glb \ --output_path /tmp/test_gmsh_prod.glb \ --smooth_angle 30 # In Blender öffnen: kein Faceting, keine Fan-Vertices an Naht-Kanten ``` --- ## Risiken / Offene Fragen 1. **GMSH Surface-Tag ↔ OCC Face-Mapping**: Die Reihenfolge der Surface-Tags bei `importShapes()` muss mit `TopExp_Explorer(FACE)` übereinstimmen. Falls nicht 1:1 → Koordinaten-basiertes Matching (Schwerpunkt der Face vs. GMSH-Mesh-Centroid) als Fallback. 2. **Performance**: GMSH Frontal-Delaunay ist typisch 2-5× langsamer als BRepMesh (BRepMesh 0.18s → GMSH ~0.4-0.9s für 25 Parts). Für 175-teilige Assemblies: 3-8 Min statt 1.5 Min. Liegt im 3-Min-Budget für Produktions-GLBs. 3. **GMSH subprocess-Isolation**: `gmsh.initialize()` / `gmsh.finalize()` sind nicht thread-safe. Da render-worker concurrency=1, ist das kein Problem. 4. **Sharp-Edge-Extraktion**: `_extract_sharp_edge_pairs()` läuft NACH der Tessellierung und nutzt die analytischen B-rep-Kurven (GCPnts_UniformAbscissa) — unabhängig vom Tessellierungsalgorithmus. Bleibt unverändert. 5. **Assembly-Locations**: Wenn GMSH eine Assembly-Shape als Ganzes importiert, werden Instance-Transformationen flachgeklopft. Dies ist erwünscht (Tessellierung in Weltkoordinaten), muss aber mit der späteren BRepBuilderAPI_Transform mm→m-Skalierung abgestimmt werden. Plan fertig. Bitte mit `/implement` fortfahren.