Files
HartOMat/plan.md
T
Hartmut ca62319688 feat: sharp edge pipeline V02, tessellation presets, media cache-bust, GMSH plan
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>
2026-03-11 14:40:36 +01:00

12 KiB
Raw Blame History

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 gmshgmsh 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__)" 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:
    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:

    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:

    # 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:
    "tessellation_engine": "occ",  # "occ" | "gmsh"
    
    In SettingsOut ergänzen:
    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):
    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:
    <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)

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

  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.