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)
@@ -70,6 +70,7 @@ def generate_gltf_geometry_task(self, cad_file_id: str):
linear_deflection = float(sys_settings.get("gltf_preview_linear_deflection", "0.1"))
angular_deflection = float(sys_settings.get("gltf_preview_angular_deflection", "0.1"))
tessellation_engine = sys_settings.get("tessellation_engine", "occ")
step = _Path(step_path_str)
@@ -97,10 +98,11 @@ def generate_gltf_geometry_task(self, cad_file_id: str):
"--color_map", _json.dumps(color_map),
"--linear_deflection", str(linear_deflection),
"--angular_deflection", str(angular_deflection),
"--tessellation_engine", tessellation_engine,
]
log_task_event(
self.request.id,
f"OCC tessellation: linear={linear_deflection}mm, angular={angular_deflection}rad",
f"Tessellation ({tessellation_engine}): linear={linear_deflection}mm, angular={angular_deflection}rad",
"info",
)
@@ -231,6 +233,7 @@ def generate_gltf_production_task(self, cad_file_id: str, product_id: str | None
smooth_angle = float(sys_settings.get("blender_smooth_angle", "30"))
prod_linear = float(sys_settings.get("gltf_production_linear_deflection", "0.03"))
prod_angular = float(sys_settings.get("gltf_production_angular_deflection", "0.05"))
tessellation_engine = sys_settings.get("tessellation_engine", "occ")
scripts_dir = _Path(_os.environ.get("RENDER_SCRIPTS_DIR", "/render-scripts"))
occ_script = scripts_dir / "export_step_to_gltf.py"
@@ -247,6 +250,7 @@ def generate_gltf_production_task(self, cad_file_id: str, product_id: str | None
"--linear_deflection", str(prod_linear),
"--angular_deflection", str(prod_angular),
"--sharp_threshold", str(sharp_threshold),
"--tessellation_engine", tessellation_engine,
]
log_task_event(
self.request.id,