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
+29
View File
@@ -112,6 +112,7 @@ export default function AdminPage() {
gltf_preview_angular_deflection: number
gltf_production_linear_deflection: number
gltf_production_angular_deflection: number
tessellation_engine: string
}
const { data: settings } = useQuery({
@@ -1459,6 +1460,34 @@ export default function AdminPage() {
)
})()}
{/* Tessellation engine selector */}
<div className="space-y-2">
<p className="text-xs font-semibold text-content-secondary uppercase tracking-wide">Tessellation Engine</p>
<div className="flex items-start gap-4">
<div className="flex flex-col gap-2">
{[
{ value: 'occ', label: 'OCC BRepMesh', description: 'Default engine. Fast, but produces fan triangles at cylinder seam edges.' },
{ value: 'gmsh', label: 'GMSH Frontal-Delaunay', description: 'Conforming mesh no fan triangles on cylinders. +1030% export time. Recommended for cylindrical parts.' },
].map(opt => (
<label key={opt.value} className="flex items-start gap-3 cursor-pointer p-3 rounded-lg border border-border-default hover:border-blue-400 transition-colors">
<input
type="radio"
name="tessellation_engine"
value={opt.value}
checked={(tess.tessellation_engine ?? 'occ') === opt.value}
onChange={() => setTessellationDraft(d => ({ ...d, tessellation_engine: opt.value }))}
className="mt-0.5 shrink-0"
/>
<div>
<div className="text-sm font-medium">{opt.label}</div>
<div className="text-xs text-content-muted mt-0.5">{opt.description}</div>
</div>
</label>
))}
</div>
</div>
</div>
{/* Manual inputs */}
<div className="grid grid-cols-2 gap-6">
<div className="space-y-4">