af320bcdc8
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>
279 lines
12 KiB
Markdown
279 lines
12 KiB
Markdown
|
||
# 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.
|