Files
HartOMat/plan.md
T
Hartmut af320bcdc8 feat(P3): add GMSH Frontal-Delaunay tessellation engine
Introduces GMSH as an alternative to OCC BRepMesh for STEP→GLB tessellation.
GMSH produces conforming meshes that eliminate fan triangles at cylinder seam
edges — a structural limitation of OCC BRepMesh that cannot be fixed via
deflection parameters.

Changes:
- render-worker/Dockerfile: install gmsh>=4.15.0 + libglu1-mesa + libxft2
- export_step_to_gltf.py: --tessellation_engine occ|gmsh CLI arg +
  _tessellate_with_gmsh() using BRep→GMSH→Poly_Triangulation write-back
- admin.py: tessellation_engine setting (SETTINGS_DEFAULTS, SettingsOut,
  SettingsUpdate, validation)
- export_glb.py: pass tessellation_engine to export_step_to_gltf.py CLI in
  both geometry and production GLB tasks
- Admin.tsx: radio button UI for OCC vs GMSH selection

Tested: 121 faces meshed, 0 BRepMesh fallback, 649K triangles on sample part.
Clean seam edges for UV unwrap — GMSH respects B-rep periodic face boundaries.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-11 19:17:26 +01:00

279 lines
12 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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)
### [x] 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
### [x] 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
### [x] 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
### [x] 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
### [x] 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
### [x] 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
<select
value={localSettings.tessellation_engine}
onChange={(e) => handleChange('tessellation_engine', e.target.value)}
>
<option value="occ">OCC BRepMesh (Standard)</option>
<option value="gmsh">GMSH Frontal-Delaunay (besser für Zylinder)</option>
</select>
```
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.