Sharp Edge Pipeline V02:
- export_step_to_gltf.py: replace BRep_Tool.Polygon3D_s (returns None in XCAF) with
GCPnts_UniformAbscissa curve sampling at 0.3mm step — extracts 17,129 segment pairs
- Inject sharp_edge_pairs + sharp_threshold_deg into GLB extras (scenes[0].extras)
via binary GLB JSON-chunk patching (no extra dependency)
- export_gltf.py: read schaeffler_sharp_edge_pairs from Blender scene custom props,
apply via KD-tree to mark edges sharp=True + seam=True (OCC mm Z-up → Blender transform)
- tools/restore_sharp_marks.py: dual-pass (dihedral angle + OCC pairs), updated coordinate
transform (X, -Z, Y) * 0.001
Tessellation:
- Admin UI: Draft / Standard / Fine preset buttons with active-state highlighting
- Default angular deflection: preview 0.5→0.1 rad, production 0.2→0.05 rad
- export_glb.py: read updated defaults from system_settings
Media / Cache:
- media/service.py: get_download_url appends ?v={file_size_bytes} cache-buster
- media/router.py: Cache-Control: no-cache for all download/thumbnail endpoints
Render pipeline:
- still_render.py / turntable_render.py: shared GPU activation + camera improvements
- render_order_line.py: global render position support
- render_thumbnail.py: updated defaults
Frontend:
- InlineCadViewer: file_size_bytes-aware URL update triggers re-fetch on regeneration
- ThreeDViewer: material panel, part selection, PBR mode improvements
- Admin.tsx: tessellation preset cards, GMSH setting dropdown
- MediaBrowser, ProductDetail, OrderDetail, Orders: various UI improvements
- New: MaterialPanel, GlobalRenderPositionsPanel, StepIndicator components
- New: renderPositions.ts API client
Plans / Docs:
- plan.md: GMSH Frontal-Delaunay tessellation plan (6 tasks)
- LEARNINGS.md: OCC Polygon3D_s None issue + GCPnts fix
- .gitignore: add backend/core (core dump from root process)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
12 KiB
Plan: GMSH Tessellation — Eliminate Fan Triangles on Cylindrical Surfaces
Kontext
OCC BRepMesh_IncrementalMesh erzeugt strukturell fehlerhafte Dreiecke bei periodischen Flächen (Vollzylinder, Ringe):
- 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.
- 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:# 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__)"gibt4.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:parser.add_argument( "--tessellation_engine", choices=["occ", "gmsh"], default="occ", help="Tessellation backend: 'occ' = BRepMesh (default), 'gmsh' = Frontal-Delaunay", ) - Akzeptanzkriterium:
--helplistet--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-DateiWichtige OCP-APIs:
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_PntKonkreter Code-Ablauf für
Poly_Triangulation-Erstellung:# 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 gmshlä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_DEFAULTSeintragen:In"tessellation_engine": "occ", # "occ" | "gmsh"SettingsOutergänzen:In der Admin-UI-Beschreibung (Docstring oder Kommentar) dokumentieren.tessellation_engine: str = "occ" - Akzeptanzkriterium:
GET /api/admin/settingsgibttessellation_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()(undgenerate_gltf_production_task()wo der OCC-Befehl aufgebaut wird):tessellation_engine = sys_settings.get("tessellation_engine", "occ") # ... occ_cmd = [ ..., "--tessellation_engine", tessellation_engine, ] - Akzeptanzkriterium: Admin stellt
tessellation_engineaufgmsh→ 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:Kurze Beschreibung: „GMSH erzeugt konformierende Dreiecke ohne Fan-Artefakte an Zylindernaht-Kanten. Verarbeitungszeit: +10-30% pro Modell."<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> - Akzeptanzkriterium: Dropdown sichtbar und speichert Setting korrekt.
- Abhängigkeiten: Task 4
GMSH-Implementierungsdetails
GMSH API Ablauf (pseudo-code)
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:
# 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
-
GMSH Surface-Tag ↔ OCC Face-Mapping: Die Reihenfolge der Surface-Tags bei
importShapes()muss mitTopExp_Explorer(FACE)übereinstimmen. Falls nicht 1:1 → Koordinaten-basiertes Matching (Schwerpunkt der Face vs. GMSH-Mesh-Centroid) als Fallback. -
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.
-
GMSH subprocess-Isolation:
gmsh.initialize()/gmsh.finalize()sind nicht thread-safe. Da render-worker concurrency=1, ist das kein Problem. -
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. -
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.