Source code for axe_usd.core.texture_parser

import logging
import re
from pathlib import Path
from typing import Dict, List, Mapping, Optional, Sequence, Tuple, Union

from .exceptions import TextureParsingError
from .models import MaterialBundle
from .texture_keys import slot_from_path


logger = logging.getLogger(__name__)

_UDIM_PATTERN = re.compile(
    r"^(?P<stem>.+?)(?P<sep>[._-])(?P<tile>1\d{3})(?P<suffix>\.[^.]+)?$"
)


TextureKey = Union[str, Tuple[str, ...]]
TexturePathMap = Mapping[TextureKey, Sequence[str]]


def _material_name_from_key(key: TextureKey) -> str:
    """Normalize a material name from a texture map key.

    Args:
        key: Texture dictionary key from Substance export.

    Returns:
        str: Material name string.
    """
    if isinstance(key, (list, tuple)) and key:
        return str(key[0])
    return str(key)


[docs] def udim_token_path(path: str) -> Optional[str]: """Replace a UDIM tile number (e.g. 1001) with the ``<UDIM>`` token. Returns the tokenized path, or ``None`` if the path does not contain a recognizable UDIM tile pattern. """ if not path: return None if "<UDIM>" in path: return path name = Path(path).name match = _UDIM_PATTERN.match(name) if not match: return None tile = int(match.group("tile")) if tile < 1001: return None suffix = match.group("suffix") or "" token_name = f"{match.group('stem')}{match.group('sep')}<UDIM>{suffix}" return str(Path(path).with_name(token_name))
[docs] def parse_textures( textures_dict: TexturePathMap, mesh_name_map: Optional[Mapping[str, Sequence[str]]] = None, ) -> List[MaterialBundle]: """Parse texture exports into material bundles. Args: textures_dict: Mapping of texture set keys to file paths. mesh_name_map: Optional mapping of texture set name to mesh names. Returns: List[MaterialBundle]: Bundles with normalized texture slots. Raises: TextureParsingError: If textures_dict is None or not a mapping. """ if textures_dict is None: raise TextureParsingError("Texture dictionary cannot be None") if not isinstance(textures_dict, Mapping): raise TextureParsingError( "Invalid texture dictionary type", details={"type": type(textures_dict).__name__}, ) if mesh_name_map is not None and not isinstance(mesh_name_map, Mapping): raise TextureParsingError( "Invalid mesh name mapping type", details={"type": type(mesh_name_map).__name__}, ) mesh_name_map = mesh_name_map or {} material_bundles: List[MaterialBundle] = [] for key, paths in textures_dict.items(): material_name = _material_name_from_key(key) textures: Dict[str, str] = {} udim_textures: Dict[str, str] = {} udim_slots: list[str] = [] mesh_names: Tuple[str, ...] = () if mesh_name_map: raw_mesh_names = mesh_name_map.get(material_name, ()) if raw_mesh_names: if isinstance(raw_mesh_names, (str, bytes)): raw_mesh_names = [raw_mesh_names] cleaned: list[str] = [] for mesh_name in raw_mesh_names: mesh_str = str(mesh_name) if mesh_str and mesh_str not in cleaned: cleaned.append(mesh_str) mesh_names = tuple(cleaned) for path in paths: slot = slot_from_path(str(path)) if not slot: logger.debug("Skipping unrecognized texture path: %s", path) continue udim_path = udim_token_path(str(path)) if udim_path: udim_textures.setdefault(slot, udim_path) if slot not in udim_slots: udim_slots.append(slot) continue textures[slot] = str(path) if udim_textures: textures.update(udim_textures) if textures: material_bundles.append( MaterialBundle( name=material_name, textures=textures, mesh_names=mesh_names, udim_slots=tuple(udim_slots), ) ) else: logger.info( "Skipping material '%s' with no recognized textures.", material_name ) return material_bundles