feat(gmsh): GMSH Frontal-Delaunay tessellation for clean cylinder seams
- Per-solid iteration prevents OOM on multi-part assemblies (25-part bearing: 2.3GB RAM when processing compound → ~100MB per solid with per-solid approach) - Fix CharacteristicLengthMax multiplier 5× → 15× and cap MinimumCirclePoints at 20 (prevents 63-pts/circle on angular_deflection=0.1rad → 231MB → 21MB) - Geometry task timeout 120s → 600s for large assemblies - Production task: reuse _geometry.glb when GMSH enabled (no re-tessellation) and cache _production_geom.glb for OCC (mtime vs STEP check) - Viewer now prefers production GLB when available (shows correct GMSH mesh) - GMSH OpenMP multithreading (min(cpu_count,16)) for 4.4× speedup Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -107,7 +107,7 @@ def generate_gltf_geometry_task(self, cad_file_id: str):
|
||||
)
|
||||
|
||||
try:
|
||||
result = _subprocess.run(cmd, capture_output=True, text=True, timeout=120)
|
||||
result = _subprocess.run(cmd, capture_output=True, text=True, timeout=600)
|
||||
for line in result.stdout.splitlines():
|
||||
logger.info("[occ-gltf] %s", line)
|
||||
for line in result.stderr.splitlines():
|
||||
@@ -243,34 +243,75 @@ def generate_gltf_production_task(self, cad_file_id: str, product_id: str | None
|
||||
prod_geom_glb = step_path.parent / f"{step_path.stem}_production_geom.glb"
|
||||
python_bin = _sys.executable
|
||||
sharp_threshold = float(sys_settings.get("sharp_edge_threshold", "20.0"))
|
||||
occ_cmd = [
|
||||
python_bin, str(occ_script),
|
||||
"--step_path", str(step_path),
|
||||
"--output_path", str(prod_geom_glb),
|
||||
"--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,
|
||||
f"Re-exporting STEP at production quality (linear={prod_linear}mm, angular={prod_angular}rad)",
|
||||
"info",
|
||||
)
|
||||
try:
|
||||
occ_result = _subprocess.run(occ_cmd, capture_output=True, text=True, timeout=180)
|
||||
for line in occ_result.stdout.splitlines():
|
||||
logger.info("[occ-prod] %s", line)
|
||||
if occ_result.returncode != 0 or not prod_geom_glb.exists() or prod_geom_glb.stat().st_size == 0:
|
||||
raise RuntimeError(
|
||||
f"OCC export failed (exit {occ_result.returncode}): {occ_result.stderr[-500:]}"
|
||||
)
|
||||
except Exception as exc:
|
||||
log_task_event(self.request.id, f"OCC re-export failed: {exc}", "error")
|
||||
pl.step_error("export_glb_production", f"OCC re-export failed: {exc}", exc)
|
||||
raise self.retry(exc=exc, countdown=30)
|
||||
|
||||
geom_glb_path = prod_geom_glb
|
||||
# --- Geometry GLB selection strategy ---
|
||||
# When GMSH is enabled, the geometry GLB (_geometry.glb) is already a conforming
|
||||
# mesh with correct seam topology — GMSH quality comes from the algorithm, not density.
|
||||
# Re-tessellating at finer production settings only wastes time and RAM on large assemblies.
|
||||
# → For GMSH: reuse the existing _geometry.glb if it is newer than the STEP file.
|
||||
# → For OCC: generate a separate _production_geom.glb at finer settings (density matters).
|
||||
|
||||
step_mtime = step_path.stat().st_mtime if step_path.exists() else 0
|
||||
preview_glb = step_path.parent / f"{step_path.stem}_geometry.glb"
|
||||
|
||||
preview_glb_valid = (
|
||||
preview_glb.exists()
|
||||
and preview_glb.stat().st_size > 0
|
||||
and preview_glb.stat().st_mtime >= step_mtime
|
||||
)
|
||||
prod_geom_cache_valid = (
|
||||
prod_geom_glb.exists()
|
||||
and prod_geom_glb.stat().st_size > 0
|
||||
and prod_geom_glb.stat().st_mtime >= step_mtime
|
||||
)
|
||||
|
||||
if tessellation_engine == "gmsh" and preview_glb_valid:
|
||||
# Fast path: reuse geometry GLB — GMSH topology is already correct at preview quality
|
||||
geom_glb_path = preview_glb
|
||||
log_task_event(
|
||||
self.request.id,
|
||||
f"GMSH: reusing geometry GLB as Blender input ({preview_glb.stat().st_size // 1024}KB, "
|
||||
f"no re-tessellation needed)",
|
||||
"info",
|
||||
)
|
||||
elif prod_geom_cache_valid:
|
||||
# Cache hit: production_geom.glb exists and is up-to-date
|
||||
geom_glb_path = prod_geom_glb
|
||||
log_task_event(
|
||||
self.request.id,
|
||||
f"Cache hit: reusing production geometry GLB ({prod_geom_glb.stat().st_size // 1024}KB)",
|
||||
"info",
|
||||
)
|
||||
else:
|
||||
# No usable cache: run tessellation from STEP
|
||||
occ_cmd = [
|
||||
python_bin, str(occ_script),
|
||||
"--step_path", str(step_path),
|
||||
"--output_path", str(prod_geom_glb),
|
||||
"--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,
|
||||
f"Tessellating STEP at production quality ({tessellation_engine}, "
|
||||
f"linear={prod_linear}mm, angular={prod_angular}rad)",
|
||||
"info",
|
||||
)
|
||||
try:
|
||||
occ_result = _subprocess.run(occ_cmd, capture_output=True, text=True, timeout=600)
|
||||
for line in occ_result.stdout.splitlines():
|
||||
logger.info("[occ-prod] %s", line)
|
||||
if occ_result.returncode != 0 or not prod_geom_glb.exists() or prod_geom_glb.stat().st_size == 0:
|
||||
raise RuntimeError(
|
||||
f"OCC export failed (exit {occ_result.returncode}): {occ_result.stderr[-500:]}"
|
||||
)
|
||||
except Exception as exc:
|
||||
log_task_event(self.request.id, f"OCC re-export failed: {exc}", "error")
|
||||
pl.step_error("export_glb_production", f"OCC re-export failed: {exc}", exc)
|
||||
raise self.retry(exc=exc, countdown=30)
|
||||
geom_glb_path = prod_geom_glb
|
||||
|
||||
# --- 2. Resolve material map from Product.cad_part_materials (SCHAEFFLER library names) ---
|
||||
# cad_part_materials lives on Product (list[dict]), NOT on CadFile.
|
||||
@@ -344,12 +385,8 @@ def generate_gltf_production_task(self, cad_file_id: str, product_id: str | None
|
||||
pl.step_error("export_glb_production", f"Blender production GLB failed: {exc}", exc)
|
||||
logger.error("generate_gltf_production_task Blender failed for cad %s: %s", cad_file_id, exc)
|
||||
raise self.retry(exc=exc, countdown=30)
|
||||
finally:
|
||||
# Clean up the high-quality temp geometry GLB (not needed after Blender export)
|
||||
try:
|
||||
prod_geom_glb.unlink(missing_ok=True)
|
||||
except Exception:
|
||||
pass
|
||||
# Note: _production_geom.glb is intentionally kept on disk as a tessellation cache.
|
||||
# It is reused on subsequent runs when the STEP file hasn't changed.
|
||||
|
||||
log_task_event(self.request.id, f"Production GLB exported: {output_path.name}", "done")
|
||||
|
||||
|
||||
Reference in New Issue
Block a user