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:
@@ -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] = []
|
||||
|
||||
|
||||
@@ -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:
|
||||
|
||||
Reference in New Issue
Block a user