refactor: replace STL intermediary with OCC-native STEP→GLB pipeline
- export_step_to_gltf.py: STEP→GLB via RWGltf_CafWriter + BRepBuilderAPI_Transform (mm→m pre-scaling, XCAFDoc_ShapeTool.GetComponents_s static method) - Blender scripts (blender_render.py, still_render.py, turntable_render.py, export_gltf.py, export_blend.py): import GLB instead of STL, remove _scale_mm_to_m - step_tasks.py: add generate_gltf_production_task, remove generate_stl_cache, replace _bbox_from_stl with _bbox_from_glb (trimesh), auto-queue geometry GLB after thumbnail render - render_blender.py: replace _stl_from_cache_or_convert with _glb_from_step, remove convert_step_to_stl and export_per_part_stls - domains/rendering/tasks.py: update render_turntable_task, export_gltf/blend tasks to use GLB instead of STL - cad.py: remove STL download/generate endpoints, add generate-gltf-production - admin.py: generate-missing-stls → generate-missing-geometry-glbs - Frontend: replace STL cache UI with GLB generate buttons, remove stl_cached field Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -59,7 +59,6 @@ class ProductOut(BaseModel):
|
||||
thumbnail_url: str | None = None
|
||||
render_image_url: str | None = None
|
||||
processing_status: str | None = None
|
||||
stl_cached: list[str] = []
|
||||
cad_parsed_objects: list[str] | None = None
|
||||
cad_mesh_attributes: dict | None = None
|
||||
arbeitspaket: str | None = None
|
||||
|
||||
@@ -193,9 +193,8 @@ def render_turntable_task(
|
||||
import os
|
||||
import shutil
|
||||
import subprocess
|
||||
from app.services.render_blender import (
|
||||
find_blender, convert_step_to_stl, export_per_part_stls
|
||||
)
|
||||
import sys
|
||||
from app.services.render_blender import find_blender
|
||||
|
||||
blender_bin = find_blender()
|
||||
if not blender_bin:
|
||||
@@ -208,27 +207,25 @@ def render_turntable_task(
|
||||
scripts_dir = Path(os.environ.get("RENDER_SCRIPTS_DIR", "/render-scripts"))
|
||||
turntable_script = scripts_dir / "turntable_render.py"
|
||||
|
||||
# STL conversion — try MinIO cache first, then convert locally
|
||||
stl_path = step.parent / f"{step.stem}_{stl_quality}.stl"
|
||||
if not stl_path.exists() or stl_path.stat().st_size == 0:
|
||||
try:
|
||||
from app.domains.products.cache_service import compute_step_hash, check_stl_cache
|
||||
step_hash = compute_step_hash(str(step))
|
||||
cached = check_stl_cache(step_hash, stl_quality)
|
||||
if cached:
|
||||
stl_path.write_bytes(cached)
|
||||
logger.info("STL restored from MinIO cache: %s", stl_path.name)
|
||||
else:
|
||||
convert_step_to_stl(step, stl_path, stl_quality)
|
||||
except Exception as exc:
|
||||
logger.warning("MinIO cache check failed (non-fatal): %s — falling back to conversion", exc)
|
||||
convert_step_to_stl(step, stl_path, stl_quality)
|
||||
parts_dir = step.parent / f"{step.stem}_{stl_quality}_parts"
|
||||
if not (parts_dir / "manifest.json").exists():
|
||||
try:
|
||||
export_per_part_stls(step, parts_dir, stl_quality)
|
||||
except Exception as exc:
|
||||
logger.warning("per-part export non-fatal: %s", exc)
|
||||
# GLB generation via OCC (replaces STL intermediary)
|
||||
linear_deflection = 0.3 if stl_quality == "low" else 0.05
|
||||
angular_deflection = 0.3 if stl_quality == "low" else 0.1
|
||||
glb_path = step.parent / f"{step.stem}_{stl_quality}.glb"
|
||||
if not glb_path.exists() or glb_path.stat().st_size == 0:
|
||||
occ_script = scripts_dir / "export_step_to_gltf.py"
|
||||
occ_cmd = [
|
||||
sys.executable, str(occ_script),
|
||||
"--step_path", str(step),
|
||||
"--output_path", str(glb_path),
|
||||
"--linear_deflection", str(linear_deflection),
|
||||
"--angular_deflection", str(angular_deflection),
|
||||
]
|
||||
occ_result = subprocess.run(occ_cmd, capture_output=True, text=True, timeout=120)
|
||||
if occ_result.returncode != 0:
|
||||
raise RuntimeError(
|
||||
f"export_step_to_gltf.py failed:\n{occ_result.stderr[-500:]}"
|
||||
)
|
||||
logger.info("render_turntable_task: GLB generated: %s", glb_path.name)
|
||||
|
||||
# Build turntable render arguments
|
||||
frames_dir = out_dir / "frames"
|
||||
@@ -238,7 +235,7 @@ def render_turntable_task(
|
||||
blender_bin, "--background",
|
||||
"--python", str(turntable_script),
|
||||
"--",
|
||||
str(stl_path),
|
||||
str(glb_path),
|
||||
str(frames_dir),
|
||||
output_name,
|
||||
str(width), str(height),
|
||||
@@ -463,93 +460,40 @@ def render_order_line_still_task(self, order_line_id: str, **params) -> dict:
|
||||
max_retries=1,
|
||||
)
|
||||
def export_gltf_for_order_line_task(self, order_line_id: str) -> dict:
|
||||
"""Export a GLB from the STL cache via Blender subprocess (with trimesh fallback).
|
||||
"""Export a geometry GLB directly from STEP via OCC (no STL intermediary).
|
||||
|
||||
Publishes a MediaAsset with asset_type='gltf_geometry' (no asset lib) or
|
||||
'gltf_production' (when an asset library is applied).
|
||||
Requires the STL low-quality cache to exist.
|
||||
Publishes a MediaAsset with asset_type='gltf_geometry'.
|
||||
"""
|
||||
import json
|
||||
import os
|
||||
import subprocess
|
||||
import sys
|
||||
|
||||
step_path_str, cad_file_id = _resolve_step_path_for_order_line(order_line_id)
|
||||
if not step_path_str:
|
||||
raise RuntimeError(f"Cannot resolve STEP path for order_line {order_line_id}")
|
||||
|
||||
step = Path(step_path_str)
|
||||
stl_path = step.parent / f"{step.stem}_low.stl"
|
||||
if not stl_path.exists():
|
||||
raise RuntimeError(
|
||||
f"STL cache not found: {stl_path}. Run thumbnail generation first."
|
||||
)
|
||||
|
||||
output_path = step.parent / f"{step.stem}_geometry.glb"
|
||||
scripts_dir = Path(os.environ.get("RENDER_SCRIPTS_DIR", "/render-scripts"))
|
||||
export_script = scripts_dir / "export_gltf.py"
|
||||
occ_script = scripts_dir / "export_step_to_gltf.py"
|
||||
|
||||
from app.services.render_blender import find_blender, is_blender_available
|
||||
if not occ_script.exists():
|
||||
raise RuntimeError(f"export_step_to_gltf.py not found at {occ_script}")
|
||||
|
||||
asset_type = "gltf_geometry"
|
||||
|
||||
# Load sharp edge hints from mesh_attributes for UV seam marking
|
||||
sharp_edges_json = "[]"
|
||||
if cad_file_id:
|
||||
try:
|
||||
import asyncio as _asyncio
|
||||
|
||||
async def _load_mesh_attrs() -> list:
|
||||
from app.database import AsyncSessionLocal
|
||||
from app.models.cad_file import CadFile as _CF
|
||||
from sqlalchemy import select as _sel
|
||||
async with AsyncSessionLocal() as _db:
|
||||
_res = await _db.execute(_sel(_CF).where(_CF.id == cad_file_id))
|
||||
_cad = _res.scalar_one_or_none()
|
||||
if _cad and _cad.mesh_attributes:
|
||||
return _cad.mesh_attributes.get("sharp_edge_midpoints") or []
|
||||
return []
|
||||
|
||||
_midpoints = _asyncio.get_event_loop().run_until_complete(_load_mesh_attrs())
|
||||
if _midpoints:
|
||||
sharp_edges_json = json.dumps(_midpoints)
|
||||
except Exception as _exc:
|
||||
logger.warning("Could not load sharp_edge_midpoints for %s: %s", cad_file_id, _exc)
|
||||
|
||||
if is_blender_available() and export_script.exists():
|
||||
blender_bin = find_blender()
|
||||
cmd = [
|
||||
blender_bin, "--background",
|
||||
"--python", str(export_script),
|
||||
"--",
|
||||
"--stl_path", str(stl_path),
|
||||
"--output_path", str(output_path),
|
||||
"--asset_library_blend", "",
|
||||
"--material_map", json.dumps({}),
|
||||
"--sharp_edges_json", sharp_edges_json,
|
||||
]
|
||||
try:
|
||||
result = subprocess.run(cmd, capture_output=True, text=True, timeout=300)
|
||||
if result.returncode != 0:
|
||||
raise RuntimeError(
|
||||
f"export_gltf.py exited {result.returncode}:\n{result.stderr[-500:]}"
|
||||
)
|
||||
publish_asset.delay(order_line_id, asset_type, str(output_path))
|
||||
logger.info("export_gltf_for_order_line_task completed via Blender: %s", output_path.name)
|
||||
return {"glb_path": str(output_path), "method": "blender"}
|
||||
except Exception as exc:
|
||||
logger.warning(
|
||||
"Blender GLB export failed for %s, falling back to trimesh: %s",
|
||||
order_line_id, exc,
|
||||
)
|
||||
|
||||
# Trimesh fallback
|
||||
try:
|
||||
import trimesh
|
||||
mesh = trimesh.load(str(stl_path))
|
||||
mesh.export(str(output_path))
|
||||
publish_asset.delay(order_line_id, asset_type, str(output_path))
|
||||
logger.info("export_gltf_for_order_line_task completed via trimesh: %s", output_path.name)
|
||||
return {"glb_path": str(output_path), "method": "trimesh"}
|
||||
cmd = [
|
||||
sys.executable, str(occ_script),
|
||||
"--step_path", str(step),
|
||||
"--output_path", str(output_path),
|
||||
]
|
||||
result = subprocess.run(cmd, capture_output=True, text=True, timeout=120)
|
||||
if result.returncode != 0:
|
||||
raise RuntimeError(
|
||||
f"export_step_to_gltf.py exited {result.returncode}:\n{result.stderr[-500:]}"
|
||||
)
|
||||
publish_asset.delay(order_line_id, "gltf_geometry", str(output_path))
|
||||
logger.info("export_gltf_for_order_line_task completed via OCC: %s", output_path.name)
|
||||
return {"glb_path": str(output_path), "method": "occ"}
|
||||
except Exception as exc:
|
||||
logger.error("export_gltf_for_order_line_task failed for %s: %s", order_line_id, exc)
|
||||
raise self.retry(exc=exc, countdown=15)
|
||||
@@ -576,9 +520,20 @@ def export_blend_for_order_line_task(self, order_line_id: str) -> dict:
|
||||
raise RuntimeError(f"Cannot resolve STEP path for order_line {order_line_id}")
|
||||
|
||||
step = Path(step_path_str)
|
||||
stl_path = step.parent / f"{step.stem}_low.stl"
|
||||
if not stl_path.exists():
|
||||
raise RuntimeError(f"STL cache not found: {stl_path}")
|
||||
# Use geometry GLB as input (generate if missing)
|
||||
glb_path = step.parent / f"{step.stem}_geometry.glb"
|
||||
if not glb_path.exists():
|
||||
import subprocess as _sp
|
||||
import sys as _sys
|
||||
scripts_dir_tmp = Path(os.environ.get("RENDER_SCRIPTS_DIR", "/render-scripts"))
|
||||
occ_cmd = [
|
||||
_sys.executable, str(scripts_dir_tmp / "export_step_to_gltf.py"),
|
||||
"--step_path", str(step),
|
||||
"--output_path", str(glb_path),
|
||||
]
|
||||
occ_res = _sp.run(occ_cmd, capture_output=True, text=True, timeout=120)
|
||||
if occ_res.returncode != 0:
|
||||
raise RuntimeError(f"GLB generation failed:\n{occ_res.stderr[-500:]}")
|
||||
|
||||
output_path = step.parent / f"{step.stem}_production.blend"
|
||||
scripts_dir = Path(os.environ.get("RENDER_SCRIPTS_DIR", "/render-scripts"))
|
||||
@@ -617,7 +572,7 @@ def export_blend_for_order_line_task(self, order_line_id: str) -> dict:
|
||||
blender_bin, "--background",
|
||||
"--python", str(export_script),
|
||||
"--",
|
||||
"--stl_path", str(stl_path),
|
||||
"--glb_path", str(glb_path),
|
||||
"--output_path", str(output_path),
|
||||
"--asset_library_blend", asset_lib_path,
|
||||
"--material_map", json.dumps(mat_map),
|
||||
@@ -670,7 +625,7 @@ def apply_asset_library_materials_task(self, order_line_id: str, asset_library_i
|
||||
if not product or not product.cad_file_id:
|
||||
return None, None, None
|
||||
cad = s.execute(sql_select(CadFile).where(CadFile.id == product.cad_file_id)).scalar_one_or_none()
|
||||
stl_path = str(Path(cad.stored_path).parent / f"{Path(cad.stored_path).stem}_low.stl") if cad else None
|
||||
glb_path = str(Path(cad.stored_path).parent / f"{Path(cad.stored_path).stem}_geometry.glb") if cad else None
|
||||
|
||||
# Resolve asset library blend path
|
||||
try:
|
||||
@@ -681,24 +636,24 @@ def apply_asset_library_materials_task(self, order_line_id: str, asset_library_i
|
||||
blend_path = None
|
||||
|
||||
mat_map = {m.get("part_name", ""): m.get("material", "") for m in (product.cad_part_materials or [])}
|
||||
return stl_path, blend_path, mat_map
|
||||
return glb_path, blend_path, mat_map
|
||||
|
||||
result = _inner()
|
||||
if result is None or result[0] is None:
|
||||
logger.warning("apply_asset_library_materials_task: could not resolve paths for %s", order_line_id)
|
||||
return {"status": "skipped"}
|
||||
|
||||
stl_path, blend_path, mat_map = result
|
||||
if not stl_path or not Path(stl_path).exists():
|
||||
logger.warning("STL not found for %s", order_line_id)
|
||||
return {"status": "skipped", "reason": "stl_not_found"}
|
||||
glb_path, blend_path, mat_map = result
|
||||
if not glb_path or not Path(glb_path).exists():
|
||||
logger.warning("Geometry GLB not found for %s", order_line_id)
|
||||
return {"status": "skipped", "reason": "glb_not_found"}
|
||||
|
||||
scripts_dir = Path(os.environ.get("RENDER_SCRIPTS_DIR", "/render-scripts"))
|
||||
script = scripts_dir / "asset_library.py"
|
||||
|
||||
cmd = [
|
||||
blender_bin, "--background", "--python", str(script), "--",
|
||||
"--stl_path", stl_path,
|
||||
"--glb_path", glb_path,
|
||||
"--asset_library_blend", blend_path or "",
|
||||
"--material_map", json.dumps(mat_map),
|
||||
]
|
||||
|
||||
Reference in New Issue
Block a user