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:
@@ -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.
|
||||
|
||||
Reference in New Issue
Block a user