feat(P3): add GMSH Frontal-Delaunay tessellation engine

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>
This commit is contained in:
2026-03-11 19:17:26 +01:00
parent 9c6ae18b28
commit af320bcdc8
6 changed files with 236 additions and 17 deletions
+9 -1
View File
@@ -41,7 +41,8 @@ SETTINGS_DEFAULTS: dict[str, str] = {
"smtp_user": "",
"smtp_password": "",
"smtp_from_address": "",
# glTF tessellation quality (OCC BRepMesh)
# glTF tessellation quality
"tessellation_engine": "occ", # "occ" | "gmsh" — tessellation backend
"gltf_preview_linear_deflection": "0.1", # mm — geometry GLB for viewer
"gltf_preview_angular_deflection": "0.1", # rad — Standard preset
"gltf_production_linear_deflection": "0.03", # mm — production GLB
@@ -87,6 +88,7 @@ class SettingsOut(BaseModel):
gltf_material_quality: str = "pbr_colors"
gltf_pbr_roughness: float = 0.4
gltf_pbr_metallic: float = 0.6
tessellation_engine: str = "occ"
class SettingsUpdate(BaseModel):
@@ -119,6 +121,7 @@ class SettingsUpdate(BaseModel):
gltf_material_quality: str | None = None
gltf_pbr_roughness: float | None = None
gltf_pbr_metallic: float | None = None
tessellation_engine: str | None = None
@router.get("/users", response_model=list[UserOut])
@@ -237,6 +240,7 @@ def _settings_to_out(raw: dict[str, str]) -> SettingsOut:
gltf_material_quality=raw.get("gltf_material_quality", "pbr_colors"),
gltf_pbr_roughness=float(raw.get("gltf_pbr_roughness", "0.4")),
gltf_pbr_metallic=float(raw.get("gltf_pbr_metallic", "0.6")),
tessellation_engine=raw.get("tessellation_engine", "occ"),
)
@@ -361,6 +365,10 @@ async def update_settings(
if not (0.05 <= body.gltf_production_angular_deflection <= 1.5):
raise HTTPException(400, detail="gltf_production_angular_deflection must be 0.051.5 rad")
updates["gltf_production_angular_deflection"] = str(body.gltf_production_angular_deflection)
if body.tessellation_engine is not None:
if body.tessellation_engine not in {"occ", "gmsh"}:
raise HTTPException(400, detail="tessellation_engine must be 'occ' or 'gmsh'")
updates["tessellation_engine"] = body.tessellation_engine
for k, v in updates.items():
await _save_setting(db, k, v)