"""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") # Apply material override: replace all material lookups with a single material if getattr(args, 'material_override', None): print(f"[blender_render] material_override active: all parts → {args.material_override}", flush=True) if usd_material_lookup: usd_material_lookup = {k: args.material_override for k in usd_material_lookup} if args.material_map: args.material_map = {k: args.material_override for k in args.material_map} 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 == "HARTOMAT_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) # Apply material override: replace all material lookups with a single material if getattr(args, 'material_override', None): print(f"[blender_render] material_override active (Mode A): all parts → {args.material_override}", flush=True) if usd_material_lookup: usd_material_lookup = {k: args.material_override for k in usd_material_lookup} if args.material_map: args.material_map = {k: args.material_override for k in args.material_map} 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 == "HARTOMAT_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