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>
This commit is contained in:
2026-03-11 14:40:36 +01:00
parent 202b06a026
commit ca62319688
70 changed files with 6551 additions and 1130 deletions
+255 -26
View File
@@ -1,49 +1,278 @@
# Plan: Migrate blender_render.py from STL to GLB Import
# Plan: GMSH Tessellation — Eliminate Fan Triangles on Cylindrical Surfaces
## Kontext
`render_blender.py` (backend service) correctly converts STEP→GLB via OCC and passes the GLB path as `argv[0]` to `blender_render.py`. However, `blender_render.py` still calls `_import_stl()` which uses `bpy.ops.wm.stl_import()` and `_scale_mm_to_m()`. The GLB from OCC is already in metres (scaled 0.001 internally by `export_step_to_gltf.py`), so no scaling is needed.
OCC `BRepMesh_IncrementalMesh` erzeugt strukturell fehlerhafte Dreiecke bei periodischen Flächen (Vollzylinder, Ringe):
`still_render.py` already has a correct `_import_glb()` implementation using `bpy.ops.import_scene.gltf()` — this serves as the reference.
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.
This caused render failures for order SA-2026-00099: Blender tried to STL-import a `.glb` file → silent failure → cancelled renders.
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 |
|-------|----------|
| `blender-renderer/blender_render.py` | Replace `_import_stl` with `_import_glb`, remove `_scale_mm_to_m`, rename `stl_path``glb_path` |
|---|---|
| `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: Replace `_import_stl()` with `_import_glb()` in `blender_render.py`
### Task 1: Dockerfile — `gmsh` installieren
- **Datei**: `blender-renderer/blender_render.py`
- **Was**:
1. Replace `_import_stl()` function (lines ~206-289) with `_import_glb()` modeled on `still_render.py:196-229`:
- Use `bpy.ops.import_scene.gltf(filepath=glb_path)`
- Collect imported mesh objects
- No scaling needed (GLB already in metres)
2. Remove `_scale_mm_to_m()` function (lines ~166-182) — no longer needed
3. Remove all calls to `_scale_mm_to_m(parts)` (Mode A ~line 466, Mode B ~line 386)
4. Replace all calls to `_import_stl(stl_path)` with `_import_glb(glb_path)` (Mode A ~line 464, Mode B ~line 384)
5. Rename variable `stl_path``glb_path` throughout (line 65, 715, and all references)
6. Update docstring/comments referencing STL
- **Akzeptanzkriterium**: `blender_render.py` imports GLB via `bpy.ops.import_scene.gltf()`, no STL references remain, no mm→m scaling
- **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
<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 nötig — reine Script-Änderung.
**Keine Migration erforderlich.** Nur Rendering-Pipeline-Änderungen. `tessellation_engine` wird in `system_settings` gespeichert (bestehendes Key-Value-Store, keine Schema-Änderung).
---
## Reihenfolge-Empfehlung
1. Task 1 (blender_render.py)
2. Rebuild render-worker: `docker compose up -d --build render-worker`
3. Test: trigger a thumbnail render or order render and check logs
```
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. **`_apply_material_library()`** and **`_resolve_part_name()`** work on Blender objects after import — they should work identically regardless of import format (STL vs GLB).
2. **Auto-camera computation** uses bounding box of imported objects — works the same with GLB meshes.
3. **`turntable_render.py`** and **`turntable_setup.py`** — need to check if they also still use STL import. If so, they need the same fix (but out of scope for this plan unless confirmed).
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.