Files
HartOMat/render-worker/scripts/_blender_scene_setup.py
T
Hartmut b583b0d7a2 feat: per-position camera settings, material alias dialog, product delete, media browser links
- Per-render-position focal_length_mm/sensor_width_mm (DB → pipeline → Blender)
- FOV-based camera distance with min clamp fix for wide-angle lenses
- Unmapped materials blocking dialog on "Dispatch Renders" with batch alias creation
- Material check endpoint (GET /orders/{id}/check-materials)
- Batch alias endpoint (POST /materials/batch-aliases)
- Quick-map "No alias" badges on Materials page
- Full product hard-delete with storage cleanup (MinIO + disk files + orphaned CadFile)
- Delete button on ProductDetail page with confirmation
- Clickable product names in Media Browser (links to product page)
- Single-line render dispatch/retry (POST /orders/{id}/lines/{id}/dispatch-render)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-14 12:16:37 +01:00

171 lines
6.8 KiB
Python

"""MODE A / MODE B scene setup for blender_render.py.
MODE A — factory settings (no template): auto-camera + auto-lights
MODE B — template file: load .blend, import into named collection
"""
import time as _time
from typing import Callable
import bpy # type: ignore[import]
from _blender_camera import setup_auto_camera, setup_auto_lights
from _blender_import import import_glb, apply_rotation, import_usd_file
from _blender_materials import (
assign_failed_material,
build_mat_map_lower,
apply_material_library,
apply_material_library_direct,
)
from _blender_scene import (
ensure_collection,
apply_smooth_batch,
apply_sharp_edges_from_occ,
setup_shadow_catcher,
)
def setup_scene(args, lap_fn: Callable[[str], None]) -> None:
"""Set up the Blender scene according to args (MODE A or B).
Handles import, rotation, smooth shading, material assignment, shadow
catcher, and auto-camera/lights. lap_fn is called with a label string
at each timing checkpoint.
"""
if args.use_template:
_setup_mode_b(args, lap_fn)
else:
_setup_mode_a(args)
def _setup_mode_b(args, lap_fn: Callable[[str], None]) -> None:
"""MODE B: Template-based render — load .blend, import into collection."""
print(f"[blender_render] Opening template: {args.template_path}")
bpy.ops.wm.open_mainfile(filepath=args.template_path)
lap_fn("template_load")
target_col = ensure_collection(args.target_collection)
usd_material_lookup: dict = {}
if args.usd_path:
parts, usd_material_lookup = import_usd_file(args.usd_path)
else:
parts = import_glb(args.glb_path)
lap_fn("glb_import")
apply_rotation(parts, args.rotation_x, args.rotation_y, args.rotation_z)
lap_fn("rotation")
for part in parts:
for col in list(part.users_collection):
col.objects.unlink(part)
target_col.objects.link(part)
apply_smooth_batch(parts, args.smooth_angle)
if not args.usd_path:
_occ_pairs = args.mesh_attributes.get("sharp_edge_pairs") or []
if _occ_pairs:
apply_sharp_edges_from_occ(parts, _occ_pairs)
lap_fn("smooth_shading")
if args.material_library_path and usd_material_lookup:
# USD primvar path: direct material assignment (no name-matching needed)
apply_material_library_direct(
parts, args.material_library_path, usd_material_lookup,
)
# Fall back to name-matching for any parts missing primvars
if args.material_map:
_unassigned = [p for p in parts if not p.data.materials or
(len(p.data.materials) == 1 and
p.data.materials[0] and
p.data.materials[0].name == "SCHAEFFLER_059999_FailedMaterial")]
if _unassigned:
print(f"[blender_render] {len(_unassigned)} parts without USD primvar — "
f"falling back to name-matching", flush=True)
apply_material_library(
_unassigned, args.material_library_path,
build_mat_map_lower(args.material_map), args.part_names_ordered,
)
elif args.material_library_path and args.material_map:
apply_material_library(
parts, args.material_library_path,
build_mat_map_lower(args.material_map), args.part_names_ordered,
)
else:
for part in parts:
assign_failed_material(part)
lap_fn("material_assign")
if args.shadow_catcher:
setup_shadow_catcher(parts)
needs_auto_camera = (
(args.lighting_only and not args.shadow_catcher)
or not bpy.context.scene.camera
)
if args.lighting_only and not args.shadow_catcher:
print("[blender_render] lighting_only mode: using template World/HDRI, forcing auto-camera")
elif needs_auto_camera:
print("[blender_render] WARNING: template has no camera — will create auto-camera")
if not needs_auto_camera and bpy.context.scene.camera:
bpy.context.scene.camera.data.clip_start = 0.001
print(f"[blender_render] template mode: {len(parts)} parts imported into collection '{args.target_collection}'")
if needs_auto_camera:
setup_auto_camera(parts, args.width, args.height,
lens_mm=args.focal_length_mm,
sensor_width_mm=args.sensor_width_mm)
def _setup_mode_a(args) -> None:
"""MODE A: Factory settings — auto-camera + auto-lights."""
bpy.ops.wm.read_factory_settings(use_empty=True)
usd_material_lookup: dict = {}
if args.usd_path:
parts, usd_material_lookup = import_usd_file(args.usd_path)
else:
parts = import_glb(args.glb_path)
apply_rotation(parts, args.rotation_x, args.rotation_y, args.rotation_z)
_t = _time.time()
apply_smooth_batch(parts, args.smooth_angle)
if not args.usd_path:
_occ_pairs = args.mesh_attributes.get("sharp_edge_pairs") or []
if _occ_pairs:
apply_sharp_edges_from_occ(parts, _occ_pairs)
for part in parts:
assign_failed_material(part)
print(f"[blender_render] smooth+fallback-material: {len(parts)} parts ({_time.time()-_t:.2f}s)", flush=True)
if args.material_library_path and usd_material_lookup:
# USD primvar path: direct material assignment
apply_material_library_direct(
parts, args.material_library_path, usd_material_lookup,
)
# Fall back to name-matching for parts without primvars
if args.material_map:
_unassigned = [p for p in parts if not p.data.materials or
(len(p.data.materials) == 1 and
p.data.materials[0] and
p.data.materials[0].name == "SCHAEFFLER_059999_FailedMaterial")]
if _unassigned:
apply_material_library(
_unassigned, args.material_library_path,
build_mat_map_lower(args.material_map), args.part_names_ordered,
)
elif args.material_library_path and args.material_map:
apply_material_library(
parts, args.material_library_path,
build_mat_map_lower(args.material_map), args.part_names_ordered,
)
bbox_center, bsphere_radius = setup_auto_camera(parts, args.width, args.height,
lens_mm=args.focal_length_mm,
sensor_width_mm=args.sensor_width_mm)
setup_auto_lights(bbox_center, bsphere_radius)
world = bpy.data.worlds.new("World")
bpy.context.scene.world = world
world.use_nodes = True
bg = world.node_tree.nodes["Background"]
bg.inputs["Color"].default_value = (0.96, 0.96, 0.97, 1.0)
bg.inputs["Strength"].default_value = 0.15