refactor(P1): M1 dead code removal + M3 blender_render.py split

M1 — dead code removed:
- Delete blender-renderer/ and threejs-renderer/ source files
- Remove PIL/Pillow fallback block from step_processor.py
  (_generate_thumbnail_placeholder, _finalise_image JPG path)
- Remove stl_quality param from render_blender.py, render_still_task,
  render_turntable_task (was always "low"; hardcode deflection values)
- render_turntable_task now reads scene_linear/angular_deflection from
  system_settings (consistent with export_glb.py pipeline)

M3 — blender_render.py split from 263 → 68 lines:
- _blender_args.py: parse_args() — all 25 positional + named args
- _blender_scene_setup.py: setup_scene() — MODE A/B including USD import
- _blender_render_config.py: configure_and_render() — engine + output

Post-review fixes:
- _db_engine.dispose() after settings read in render_turntable_task
- _finalise_image() fmt param removed (always PNG; PIL never installed)
- _blender_import.py committed together with new submodules to satisfy
  import_usd_file dependency

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-03-12 12:54:40 +01:00
parent 393e4b92a7
commit 47b5d42bb5
10 changed files with 471 additions and 496 deletions
+41 -22
View File
@@ -15,17 +15,13 @@ from pathlib import Path
logger = logging.getLogger(__name__)
def _glb_from_step(step_path: Path, glb_path: Path, quality: str = "low") -> None:
"""Convert STEP → GLB via OCC (export_step_to_gltf.py, no Blender needed).
quality: "low" → coarser mesh (~0.3 mm deflection, fast)
"high" → finer mesh (~0.05 mm deflection, slower)
"""
def _glb_from_step(step_path: Path, glb_path: Path) -> None:
"""Convert STEP → GLB via OCC (export_step_to_gltf.py, no Blender needed)."""
import subprocess
import sys as _sys
linear_deflection = 0.3 if quality == "low" else 0.05
angular_deflection = 0.5 if quality == "low" else 0.2
linear_deflection = 0.3
angular_deflection = 0.5
scripts_dir = Path(os.environ.get("RENDER_SCRIPTS_DIR", "/render-scripts"))
script_path = scripts_dir / "export_step_to_gltf.py"
@@ -71,7 +67,6 @@ def render_still(
height: int = 512,
engine: str = "cycles",
samples: int = 256,
stl_quality: str = "low",
smooth_angle: int = 30,
cycles_device: str = "auto",
transparent_bg: bool = False,
@@ -94,9 +89,13 @@ def render_still(
denoising_use_gpu: str = "",
mesh_attributes: dict | None = None,
log_callback: "Callable[[str], None] | None" = None,
usd_path: "Path | None" = None,
) -> dict:
"""Convert STEP → GLB (OCC) → PNG (Blender subprocess).
When usd_path is provided and the file exists, the GLB conversion step is
skipped and Blender imports the USD stage directly (--usd-path flag).
Returns a dict with timing, sizes, engine_used, and log_lines.
Raises RuntimeError on failure.
"""
@@ -116,15 +115,20 @@ def render_still(
t0 = time.monotonic()
# 1. GLB conversion (OCC — replaces cadquery STL)
# 1. GLB conversion (OCC)skipped when usd_path is provided
glb_path = step_path.parent / f"{step_path.stem}_thumbnail.glb"
use_usd = bool(usd_path and usd_path.exists())
t_glb = time.monotonic()
if not glb_path.exists() or glb_path.stat().st_size == 0:
_glb_from_step(step_path, glb_path, quality=stl_quality)
if use_usd:
logger.info("[render_blender] using USD path: %s", usd_path)
glb_size_bytes = 0
else:
logger.info("GLB local hit: %s (%d KB)", glb_path.name, glb_path.stat().st_size // 1024)
glb_size_bytes = glb_path.stat().st_size if glb_path.exists() else 0
if not glb_path.exists() or glb_path.stat().st_size == 0:
_glb_from_step(step_path, glb_path)
else:
logger.info("GLB local hit: %s (%d KB)", glb_path.name, glb_path.stat().st_size // 1024)
glb_size_bytes = glb_path.stat().st_size if glb_path.exists() else 0
glb_duration_s = round(time.monotonic() - t_glb, 2)
# 2. Blender render
@@ -142,12 +146,14 @@ def render_still(
env["EGL_PLATFORM"] = "surfaceless"
def _build_cmd(eng: str) -> list:
# Pass "" as glb_path when using USD — blender_render.py reads --usd-path instead
glb_arg = "" if use_usd else str(glb_path)
cmd = [
blender_bin,
"--background",
"--python", str(script_path),
"--",
str(glb_path),
glb_arg,
str(output_path),
str(width), str(height),
eng, str(samples), str(smooth_angle),
@@ -165,7 +171,11 @@ def render_still(
denoising_input_passes or "", denoising_prefilter or "",
denoising_quality or "", denoising_use_gpu or "",
]
if mesh_attributes:
if use_usd:
cmd += ["--usd-path", str(usd_path)]
if mesh_attributes:
logger.debug("[render_blender] usd_path active — mesh_attributes ignored")
elif mesh_attributes:
cmd += ["--mesh-attributes", json.dumps(mesh_attributes)]
return cmd
@@ -283,7 +293,6 @@ def render_turntable_to_file(
height: int = 1920,
engine: str = "cycles",
samples: int = 128,
stl_quality: str = "low",
smooth_angle: int = 30,
cycles_device: str = "auto",
transparent_bg: bool = False,
@@ -300,9 +309,12 @@ def render_turntable_to_file(
rotation_x: float = 0.0,
rotation_y: float = 0.0,
rotation_z: float = 0.0,
usd_path: "Path | None" = None,
) -> dict:
"""Render a turntable animation: STEP → STL → N frames (Blender) → mp4 (ffmpeg).
When usd_path is provided and exists, the GLB conversion step is skipped.
Returns a dict with timing, frame count, engine_used, log_lines.
Raises RuntimeError on failure.
"""
@@ -328,14 +340,18 @@ def render_turntable_to_file(
t0 = time.monotonic()
# 1. GLB conversion (OCC — replaces cadquery STL)
# 1. GLB conversion (OCC)skipped when usd_path is provided
glb_path = step_path.parent / f"{step_path.stem}_thumbnail.glb"
use_usd = bool(usd_path and usd_path.exists())
t_glb = time.monotonic()
if not glb_path.exists() or glb_path.stat().st_size == 0:
_glb_from_step(step_path, glb_path, quality=stl_quality)
if use_usd:
logger.info("[render_blender] turntable using USD path: %s", usd_path)
else:
logger.info("GLB local hit: %s (%d KB)", glb_path.name, glb_path.stat().st_size // 1024)
if not glb_path.exists() or glb_path.stat().st_size == 0:
_glb_from_step(step_path, glb_path)
else:
logger.info("GLB local hit: %s (%d KB)", glb_path.name, glb_path.stat().st_size // 1024)
glb_duration_s = round(time.monotonic() - t_glb, 2)
# 2. Render frames with Blender
@@ -346,12 +362,13 @@ def render_turntable_to_file(
env = dict(os.environ)
env["EGL_PLATFORM"] = "surfaceless"
glb_arg = "" if use_usd else str(glb_path)
cmd = [
blender_bin,
"--background",
"--python", str(script_path),
"--",
str(glb_path),
glb_arg,
str(frames_dir),
str(frame_count),
"360", # degrees
@@ -371,6 +388,8 @@ def render_turntable_to_file(
bg_color or "",
"1" if transparent_bg else "0",
]
if use_usd:
cmd += ["--usd-path", str(usd_path)]
log_lines: list[str] = []
+13 -90
View File
@@ -449,9 +449,7 @@ def _get_all_settings() -> dict[str, str]:
"blender_engine": "cycles",
"blender_cycles_samples": "256",
"blender_eevee_samples": "64",
"threejs_render_size": "1024",
"thumbnail_format": "jpg",
"stl_quality": "low",
"blender_smooth_angle": "30",
"cycles_device": "auto",
}
@@ -511,7 +509,6 @@ def _generate_thumbnail(
render_log.update({
"engine": engine,
"samples": int(settings[f"blender_{engine}_samples"]),
"stl_quality": settings["stl_quality"],
"smooth_angle": int(settings["blender_smooth_angle"]),
"cycles_device": settings["cycles_device"],
"width": 512,
@@ -534,7 +531,6 @@ def _generate_thumbnail(
output_path=tmp_png,
engine=engine,
samples=samples,
stl_quality=settings["stl_quality"],
smooth_angle=int(settings["blender_smooth_angle"]),
cycles_device=settings["cycles_device"],
)
@@ -543,7 +539,7 @@ def _generate_thumbnail(
logger.warning("Blender subprocess render failed: %s", exc)
rendered_png = None
else:
logger.warning("Blender not available in this container — falling back to Pillow placeholder")
logger.warning("Blender not available in this container")
# Merge rich service response data into render_log
if service_data:
@@ -555,88 +551,20 @@ def _generate_thumbnail(
render_log["completed_at"] = time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime())
if rendered_png:
result = _finalise_image(rendered_png, final_path, fmt)
result = _finalise_image(rendered_png, final_path)
tmp_png.unlink(missing_ok=True)
render_log["fallback"] = False
return result, render_log
# Pillow placeholder
render_log["fallback"] = True
return _generate_thumbnail_placeholder(step_path, final_path, fmt), render_log
return None, render_log
def _finalise_image(src: Path, dst: Path, fmt: str) -> Path | None:
"""Convert src image to dst using the requested format (jpg or png)."""
if fmt == "jpg":
try:
from PIL import Image
img = Image.open(src).convert("RGB")
img.save(str(dst), "JPEG", quality=92, optimize=True)
return dst
except Exception as exc:
logger.warning(f"JPG conversion failed: {exc}; keeping PNG")
src.rename(dst.with_suffix(".png"))
return dst.with_suffix(".png")
else:
src.rename(dst)
return dst
def _finalise_image(src: Path, dst: Path) -> Path | None:
"""Move src image to dst, always as PNG."""
out = dst.with_suffix(".png")
src.rename(out)
return out
def _generate_thumbnail_placeholder(step_path: Path, out_path: Path, fmt: str = "png") -> Path | None:
"""Generate a simple placeholder thumbnail using Pillow."""
try:
from PIL import Image, ImageDraw, ImageFont
W, H = 512, 512
img = Image.new("RGB", (W, H), color=(245, 246, 248))
draw = ImageDraw.Draw(img)
# Subtle grid
for i in range(0, W, 32):
draw.line([(i, 0), (i, H)], fill=(228, 230, 235), width=1)
draw.line([(0, i), (W, i)], fill=(228, 230, 235), width=1)
# Isometric box (front / top / right faces)
cx, cy = 256, 260
s = 110 # half-size
# Front face
draw.polygon(
[(cx - s, cy), (cx, cy + s // 2), (cx + s, cy), (cx, cy - s // 2)],
fill=(195, 208, 220), outline=(90, 110, 130), width=2,
)
# Top face
draw.polygon(
[(cx - s, cy - s), (cx, cy - s - s // 2), (cx + s, cy - s), (cx, cy - s + s // 2)],
fill=(220, 230, 240), outline=(90, 110, 130), width=2,
)
# Right pillar
draw.polygon(
[(cx + s, cy - s), (cx + s, cy), (cx, cy + s // 2), (cx, cy - s + s // 2)],
fill=(160, 178, 196), outline=(90, 110, 130), width=2,
)
# Schaeffler green top bar
draw.rectangle([0, 0, W, 10], fill=(0, 137, 61))
# Model name strip at bottom
name = step_path.stem
draw.rectangle([0, H - 52, W, H], fill=(30, 50, 70))
try:
font = ImageFont.load_default(size=15)
draw.text((W // 2, H - 26), name, fill=(255, 255, 255), anchor="mm", font=font)
except Exception:
draw.text((10, H - 38), name, fill=(255, 255, 255))
if fmt == "jpg":
img = img.convert("RGB")
img.save(str(out_path), "JPEG", quality=92, optimize=True)
else:
img.save(str(out_path), "PNG")
return out_path
except Exception as exc:
logger.warning(f"Pillow placeholder thumbnail failed: {exc}")
return None
def regenerate_cad_thumbnail(cad_file_id: str, part_colors: dict[str, str]) -> bool:
"""
@@ -713,6 +641,7 @@ def render_to_file(
denoising_quality: str = "",
denoising_use_gpu: str = "",
order_line_id: str | None = None,
usd_path: "Path | None" = None,
) -> tuple[bool, dict]:
"""Render a STEP file to a specific output path using current system settings.
@@ -769,14 +698,12 @@ def render_to_file(
h = height or 512
render_log.update({
"engine": actual_engine, "samples": actual_samples,
"stl_quality": settings["stl_quality"],
"smooth_angle": int(settings["blender_smooth_angle"]),
"cycles_device": actual_cycles_device,
"width": w, "height": h,
})
extra = {
"engine": actual_engine, "samples": actual_samples,
"stl_quality": settings["stl_quality"],
"smooth_angle": int(settings["blender_smooth_angle"]),
"cycles_device": actual_cycles_device,
"width": w, "height": h,
@@ -830,7 +757,6 @@ def render_to_file(
output_path=tmp_png,
engine=actual_engine,
samples=actual_samples,
stl_quality=settings["stl_quality"],
smooth_angle=int(settings["blender_smooth_angle"]),
cycles_device=actual_cycles_device,
width=w, height=h,
@@ -850,13 +776,14 @@ def render_to_file(
denoising_quality=denoising_quality,
denoising_use_gpu=denoising_use_gpu,
log_callback=_log_cb,
usd_path=usd_path,
)
rendered_png = tmp_png if tmp_png.exists() else None
except Exception as exc:
logger.warning("Blender subprocess render failed: %s", exc)
rendered_png = None
else:
logger.warning("Blender not available in this container — using Pillow fallback")
logger.warning("Blender not available in this container — render skipped")
if service_data:
for key in ("total_duration_s", "stl_duration_s", "render_duration_s",
@@ -867,15 +794,11 @@ def render_to_file(
render_log["completed_at"] = time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime())
if rendered_png:
result = _finalise_image(rendered_png, out, fmt)
result = _finalise_image(rendered_png, out)
tmp_png.unlink(missing_ok=True)
render_log["fallback"] = False
return result is not None, render_log
# Pillow placeholder fallback
render_log["fallback"] = True
result = _generate_thumbnail_placeholder(step, out, fmt)
return result is not None, render_log
return False, render_log
def _convert_to_gltf(step_path: Path, cad_file_id: str, upload_dir: str) -> Path | None: