184 lines
5.7 KiB
Python
184 lines
5.7 KiB
Python
"""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<key>[^_]+)__(?P<value>[^_]+)", re.IGNORECASE),
|
|
re.compile(r"template-input:(?P<key>[^=]+)=(?P<value>.+)", re.IGNORECASE),
|
|
re.compile(r"ti::(?P<key>[^:]+)::(?P<value>.+)", 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,
|
|
)
|