"""Generic template-input application for Blender template scenes. Template inputs are exposed to the scene via custom properties and optional visibility/selection markers. This keeps legacy templates untouched while allowing graph workflows to pass structured overrides into `.blend` scenes. """ from __future__ import annotations import json import re from typing import Any import bpy # type: ignore[import] _MARKER_PROP_NAMES = ( "hartomat_template_input", "hartomat.template_input", "template_input", "schaeffler_template_input", ) _MARKER_KEY_PROP_NAMES = ( "hartomat_template_input_key", "hartomat.template_input_key", "template_input_key", "schaeffler_template_input_key", ) _MARKER_VALUE_PROP_NAMES = ( "hartomat_template_input_value", "hartomat.template_input_value", "template_input_value", "schaeffler_template_input_value", ) _NAME_PATTERNS = ( re.compile(r"template_input__(?P[^_]+)__(?P[^_]+)", re.IGNORECASE), re.compile(r"template-input:(?P[^=]+)=(?P.+)", re.IGNORECASE), re.compile(r"ti::(?P[^:]+)::(?P.+)", re.IGNORECASE), ) def _normalize_template_inputs(template_inputs: dict[str, Any] | None) -> dict[str, str]: normalized: dict[str, str] = {} for raw_key, raw_value in (template_inputs or {}).items(): key = str(raw_key or "").strip() if not key or raw_value is None: continue if isinstance(raw_value, bool): value = "true" if raw_value else "false" else: value = str(raw_value).strip() if value: normalized[key] = value return normalized def _scene_targets(): yield ("collection", bpy.data.collections) yield ("object", bpy.data.objects) yield ("world", bpy.data.worlds) def _extract_marker(target) -> tuple[str, str] | None: for prop_name in _MARKER_PROP_NAMES: raw = target.get(prop_name) if not raw: continue if isinstance(raw, str): text = raw.strip() if not text: continue if text.startswith("{"): try: payload = json.loads(text) except Exception: payload = None if isinstance(payload, dict): key = str(payload.get("key", "")).strip() value = str(payload.get("value", "")).strip() if key and value: return key, value if "=" in text: key, value = text.split("=", 1) key = key.strip() value = value.strip() if key and value: return key, value key = None value = None for prop_name in _MARKER_KEY_PROP_NAMES: raw = target.get(prop_name) if raw: key = str(raw).strip() if key: break for prop_name in _MARKER_VALUE_PROP_NAMES: raw = target.get(prop_name) if raw is not None: value = str(raw).strip() if value: break if key and value: return key, value name = getattr(target, "name", "") or "" for pattern in _NAME_PATTERNS: match = pattern.search(name) if not match: continue key = match.group("key").strip() value = match.group("value").strip() if key and value: return key, value return None def _find_layer_collection(layer_collection, collection_name: str): if layer_collection.collection.name == collection_name: return layer_collection for child in layer_collection.children: found = _find_layer_collection(child, collection_name) if found is not None: return found return None def _apply_collection_visibility(collection, *, enabled: bool) -> None: collection.hide_render = not enabled collection.hide_viewport = not enabled for view_layer in bpy.context.scene.view_layers: layer_collection = _find_layer_collection(view_layer.layer_collection, collection.name) if layer_collection is not None: layer_collection.exclude = not enabled layer_collection.hide_viewport = not enabled def _apply_object_visibility(obj, *, enabled: bool) -> None: obj.hide_render = not enabled obj.hide_viewport = not enabled try: obj.hide_set(not enabled) except Exception: pass def _apply_world_selection(world, *, enabled: bool) -> None: if enabled: bpy.context.scene.world = world def apply_template_inputs(template_inputs: dict[str, Any] | None) -> None: normalized = _normalize_template_inputs(template_inputs) if not normalized: return scene = bpy.context.scene for key, value in normalized.items(): scene[f"template_input__{key}"] = value scene[f"hartomat_template_input__{key}"] = value matched_targets = 0 for kind, targets in _scene_targets(): for target in targets: marker = _extract_marker(target) if marker is None: continue key, expected_value = marker if key not in normalized: continue enabled = normalized[key] == expected_value matched_targets += 1 if kind == "collection": _apply_collection_visibility(target, enabled=enabled) elif kind == "object": _apply_object_visibility(target, enabled=enabled) elif kind == "world": _apply_world_selection(target, enabled=enabled) print( f"[blender_render] applied template_inputs keys={sorted(normalized)} matched_targets={matched_targets}", flush=True, )