Traversing Asset Categories

Hi @ferdinand - Thank you so much for this!

This works great for Maxon's built-in assets databases. How would I retrieve all assets that belong to a category, even if they live in custom user-mounted databases?

GetUserPrefsRepository() seems like the best bet, but it doesn't seem to include the many assets I have in custom DBs, and I don't see any other promising methods exposed in the SDK.

edit: @ferdinand - Forked from Get All Assets in Category, although the other thread was named appropriately, it was more about general asset traversal and not the specific makeup of asset category trees.

Update: There seems to be a bug in ExpandAssetCategoryId as I can retrieve all the assets (even those in custom DBs) if I start from a manually generated list of category Ids, but I only geta couple of the categories using this method.

Hello @d_keith,

Thank you for reaching out to us. So, I had a look, and I could not find a bug with ExpandAssetCategoryId. I tried built-in categories, custom categories, and custom categories inside a custom database. My traversal method does return everything what I would expect to be returned. Find a more elaborate variant of ExpandAssetCategoryId below (I turned it into a class for easier inspection of the data).

There are a few things which could go wrong here:

  1. You misunderstood the purpose of ExpandAssetCategoryId, it expands a category tree in a top-down fashion. E.g., for A->B->C, ExpandAssetCategoryId(B) will yield B, C but not A. It could certainly be done differently but that would be up to you.
  2. There is a bug in your code in another place.
  3. There might exist special conditions in the environment you are working in, but that is nothing for the forum.

Find below my example which will expand asset category trees from a root in a more visual and therefore easier to check manner.

Cheers,
Ferdinand

The result:

 --------------------------------------------------------------------------------
AssetCategoryHandler at 0X7FD2F3350B90 (repo = [email protected]):


Managed IDs:

(maxon.Id('[email protected]'),
 maxon.Id('category_32a5e2eefcc10592'),
 maxon.Id('category_3e39b2c258ae9313'),
 maxon.Id('category_f7cb610b3f172a93'),
 maxon.Id('category_8f51f303e1bbf875'),
 maxon.Id('category_bb4c48de0af7b8b3'),
 maxon.Id('category_28801c2ab359e01e'),
 maxon.Id('category_fd68f940d0b615ab'),
 maxon.Id('category_993d6d73f3c2629e'),
 maxon.Id('category_fce5f1171ae19de7'),
 maxon.Id('category_50bbe4f91e99090b'),
 maxon.Id('category_074c78c3e2d70ab9'),
 maxon.Id('category_c92b8c257f9e7517'),
 maxon.Id('[email protected]'),
 maxon.Id('category_a800e5a243f9f385'),
 maxon.Id('category_9676f914364e4e27'),
 maxon.Id('category_e024f65cd280795e'),
 maxon.Id('category_2ab08ca489795a07'),
 maxon.Id('category_c04b37955dcb3895'),
 maxon.Id('category_34ed541e2020f38e'),
 maxon.Id('category_f7316fed1fc11137'),
 maxon.Id('category_efb8eae1116900c2'),
 maxon.Id('category_c0e06630879697c2'),
 maxon.Id('category_f4afa4f900193b95'),
 maxon.Id('category_476ff23d55425ca4'),
 maxon.Id('category_58e6a054402a25fd'),
 maxon.Id('category_ddcdf72383424d7e'),
 maxon.Id('category_ca1600eb107c6082'),
 maxon.Id('category_1b0948c77cb2f61e'),
 maxon.Id('[email protected]'),
 maxon.Id('category_f40806b20b84f759'),
 maxon.Id('category_8ea059e3d503175a'),
 maxon.Id('category_9147f39c273d8342'),
 maxon.Id('[email protected]'),
 maxon.Id('[email protected]'),
 maxon.Id('[email protected]'),
 maxon.Id('[email protected]'),
 maxon.Id('[email protected]'),
 maxon.Id('[email protected]'),
 maxon.Id('[email protected]'),
 maxon.Id('[email protected]'),
 maxon.Id('[email protected]'),
 maxon.Id('[email protected]'),
 maxon.Id('[email protected]'),
 maxon.Id('[email protected]'),
 maxon.Id('[email protected]'),
 maxon.Id('[email protected]'),
 maxon.Id('[email protected]'),
 maxon.Id('[email protected]'),
 maxon.Id('[email protected]'),
 maxon.Id('[email protected]'),
 maxon.Id('[email protected]'),
 maxon.Id('category_bb2ed911f3303be2'),
 maxon.Id('category_4c6f230c27d35ad7'),
 maxon.Id('category_e2204ff5b9252604'),
 maxon.Id('category_0359297d725f327a'),
 maxon.Id('category_4cc30dbe0f38a155'),
 maxon.Id('category_1fee6083977acfee'),
 maxon.Id('category_5904e55a542a55eb'),
 maxon.Id('[email protected]'),
 maxon.Id('category_248c7a50c5c70f4c'),
 maxon.Id('category_8509ac933bece593'),
 maxon.Id('[email protected]'),
 maxon.Id('category_64f67f3727141f1a'),
 maxon.Id('[email protected]'),
 maxon.Id('category_63ebef6aab44bd4d'),
 maxon.Id('category_045526cd4a11b6dc'),
 maxon.Id('[email protected]'),
 maxon.Id('[email protected]'),
 maxon.Id('category_04cdfd3b558329b6'),
 maxon.Id('category_24b5ae0d89e20ccc'),
 maxon.Id('category_cd5ad0a08c825d60'),
 maxon.Id('category_1c643f453b71129e'),
 maxon.Id('category_f9bd2fcc800c8279'),
 maxon.Id('category_468266206e683e39'),
 maxon.Id('category_172e168c2640e535'),
 maxon.Id('[email protected]'),
 maxon.Id('category_ab975c5373b7ba07'),
 maxon.Id('[email protected]'),
 maxon.Id('category_0133f046d03a46e7'),
 maxon.Id('category_74ec3ae6ecaab0de'),
 maxon.Id('category_fcf83cd1ff7e0fea'),
 maxon.Id('category_32750a41ff5ae804'),
 maxon.Id('category_f3dfa1fc22e1760f'),
 maxon.Id('category_8c28445d28cba3b6'),
 maxon.Id('category_65263c7c1c8c423c'),
 maxon.Id('category_67a056c194d9897c'),
 maxon.Id('category_73d82da307ebac4f'),
 maxon.Id('category_fa850c478d745f9e'),
 maxon.Id('[email protected]'),
 maxon.Id('category_8cbecf2e6c1c53e6'),
 maxon.Id('category_66c6a982b7227e77'),
 maxon.Id('category_a2fde34845c656bd'),
 maxon.Id('category_95b2c4bc77dbeb2d'),
 maxon.Id('[email protected]'),
 maxon.Id('[email protected]'),
 maxon.Id('[email protected]'),
 maxon.Id('category_ecfd5ef0b32fdc6e'),
 maxon.Id('[email protected]'),
 maxon.Id('category_de343673ae7ff879'),
 maxon.Id('category_2aa95f92d383989e'),
 maxon.Id('category_aded6c15b065a4a5'),
 maxon.Id('category_fe8c8340d4cfebef'),
 maxon.Id('category_2d90a72a03048a0a'),
 maxon.Id('category_8191022b0575a8c8'),
 maxon.Id('category_94bc6daab0db3454'),
 maxon.Id('[email protected]'),
 maxon.Id('[email protected]'),
 maxon.Id('category_bdac9c772d258396'),
 maxon.Id('[email protected]'),
 maxon.Id('category_f72dcc94be532fa4'),
 maxon.Id('category_d6e2d304d1c0eb76'),
 maxon.Id('category_819c3f327c46106c'),
 maxon.Id('category_a7dd93c148997ea2'),
 maxon.Id('category_adbf1096d2400c47'),
 maxon.Id('category_6bf21b118e626b8f'),
 maxon.Id('category_66478df5e0c296b2'),
 maxon.Id('category_5ff49c9a181cad8c'),
 maxon.Id('category_06eaa38e9750e5e3'),
 maxon.Id('category_732f5bf076c873c7'),
 maxon.Id('category_781d1c68184a4319'),
 maxon.Id('category_b6d6fd4c864f01d8'),
 maxon.Id('category_bcc53614ed7486b6'),
 maxon.Id('category_20a0233895be23f3'),
 maxon.Id('category_d42b5d6450873989'),
 maxon.Id('category_cea3e864eb47b596'),
 maxon.Id('category_3385066daeaf9819'),
 maxon.Id('[email protected]'),
 maxon.Id('[email protected]'),
 maxon.Id('[email protected]'),
 maxon.Id('[email protected]'),
 maxon.Id('[email protected]'),
 maxon.Id('[email protected]'),
 maxon.Id('[email protected]'),
 maxon.Id('[email protected]'),
 maxon.Id('category_ef962b4216a17f64'),
 maxon.Id('category_5a9141a0036aa0e2'),
 maxon.Id('category_e682fa27d7eb619d'),
 maxon.Id('category_899783790d811f25'),
 maxon.Id('[email protected]'))

Tree:

 '/Objects'
  '/Objects/Info Graphics'
  '/Objects/Humans'
   '/Objects/Humans/3D Posable Silhouettes'
   '/Objects/Humans/3D People - For Animation'
   '/Objects/Humans/3D People [Low Resolution]'
   '/Objects/Humans/3D People [Medium Resolution]'
   '/Objects/Humans/Cutout'
  '/Objects/Packaging'
  '/Objects/Tools'
  '/Objects/Vehicles'
  '/Objects/Periodicals'
  '/Objects/Appliances'
  '/Objects/Eyewear'
  '/Objects/Garments'
  '/Objects/Finance'
  '/Objects/Plants'
   '/Objects/Plants/European Trees Young'
   '/Objects/Plants/Garden & Exotic'
   '/Objects/Plants/European Trees Mature'
   '/Objects/Plants/Houseplants'
   '/Objects/Plants/Cutouts'
   '/Objects/Plants/Grass Elements'
    '/Objects/Plants/Grass Elements/Low Resolution'
    '/Objects/Plants/Grass Elements/Medium Resolution'
  '/Objects/Pots'
  '/Objects/Shelving'
   '/Objects/Shelving/Modular Cabinets & Doors'
    '/Objects/Shelving/Modular Cabinets & Doors/Misc Cabinet Examples'
    '/Objects/Shelving/Modular Cabinets & Doors/Cabinet & Door Pieces'
    '/Objects/Shelving/Modular Cabinets & Doors/Misc Door Examples'
   '/Objects/Shelving/Living Room & Bedroom'
  '/Objects/Celebration'
  '/Objects/Kitbash'
   '/Objects/Kitbash/Piping'
    '/Objects/Kitbash/Piping/Corners'
    '/Objects/Kitbash/Piping/Pipes'
    '/Objects/Kitbash/Piping/Pipe'
   '/Objects/Kitbash/Details'
    '/Objects/Kitbash/Details/Squares'
    '/Objects/Kitbash/Details/Crosses'
    '/Objects/Kitbash/Details/Geometric'
    '/Objects/Kitbash/Details/Triangles'
    '/Objects/Kitbash/Details/Hexagons'
    '/Objects/Kitbash/Details/Arrows'
    '/Objects/Kitbash/Details/Rectangles'
    '/Objects/Kitbash/Details/Circles'
    '/Objects/Kitbash/Details/Pattern'
   '/Objects/Kitbash/Tubes'
   '/Objects/Kitbash/Connectors'
   '/Objects/Kitbash/Joints'
  '/Objects/Toys'
  '/Objects/Outdoor Objects'
   '/Objects/Outdoor Objects/Scaffolds'
   '/Objects/Outdoor Objects/Trash Cans'
   '/Objects/Outdoor Objects/Buildings'
    '/Objects/Outdoor Objects/Buildings/Houses'
    '/Objects/Outdoor Objects/Buildings/Cityscape'
    '/Objects/Outdoor Objects/Buildings/Misc'
   '/Objects/Outdoor Objects/Wall Decor'
   '/Objects/Outdoor Objects/Manholes'
   '/Objects/Outdoor Objects/Miscellaneous'
   '/Objects/Outdoor Objects/Traffic Lights'
   '/Objects/Outdoor Objects/Antennas'
   '/Objects/Outdoor Objects/Pavement'
   '/Objects/Outdoor Objects/Street Lights'
   '/Objects/Outdoor Objects/Bus Stops'
   '/Objects/Outdoor Objects/Fire Escapes'
   '/Objects/Outdoor Objects/Infrastructure'
   '/Objects/Outdoor Objects/Road Signs'
   '/Objects/Outdoor Objects/Barriers & Barricades'
  '/Objects/Tables'
   '/Objects/Tables/Bedside Tables'
   '/Objects/Tables/Game Tables'
   '/Objects/Tables/Office Tables'
   '/Objects/Tables/Coffee Tables'
   '/Objects/Tables/Dining Tables'
  '/Objects/Arts & Crafts'
  '/Objects/Stairs'
  '/Objects/Cogwheel Objects'
   '/Objects/Cogwheel Objects/Saws'
   '/Objects/Cogwheel Objects/Gears'
   '/Objects/Cogwheel Objects/Miscellaneous'
   '/Objects/Cogwheel Objects/Clutches'
   '/Objects/Cogwheel Objects/Ratchet'
   '/Objects/Cogwheel Objects/Watch Gears'
  '/Objects/Kitchen'
   '/Objects/Kitchen/Cutlery'
   '/Objects/Kitchen/Dinnerware'
   '/Objects/Kitchen/Accessories'
   '/Objects/Kitchen/Cookware'
   '/Objects/Kitchen/Serveware'
   '/Objects/Kitchen/Glassware'
  '/Objects/Screws'
  '/Objects/Landscape'
   '/Objects/Landscape/Wood'
   '/Objects/Landscape/Stones'
  '/Objects/Vases'
  '/Objects/Window Treatments'
  '/Objects/Seating'
   '/Objects/Seating/Sofas'
   '/Objects/Seating/Waiting Areas'
   '/Objects/Seating/Benches'
   '/Objects/Seating/Chairs'
  '/Objects/Lighting & Ceiling Fans'
   '/Objects/Lighting & Ceiling Fans/Ceiling Lighting'
   '/Objects/Lighting & Ceiling Fans/Home Safety & Security'
   '/Objects/Lighting & Ceiling Fans/Outdoor Lighting'
   '/Objects/Lighting & Ceiling Fans/Wall Lighting'
   '/Objects/Lighting & Ceiling Fans/Ceiling Fans'
   '/Objects/Lighting & Ceiling Fans/Lamps'
   '/Objects/Lighting & Ceiling Fans/Light Bulbs'
   '/Objects/Lighting & Ceiling Fans/Light Stands'
  '/Objects/Music'
  '/Objects/Bedroom'
  '/Objects/Bath'
   '/Objects/Bath/Toilets'
   '/Objects/Bath/Bathroom Vanities'
   '/Objects/Bath/Sinks'
   '/Objects/Bath/Towels'
   '/Objects/Bath/Bathroom Accessories'
   '/Objects/Bath/Bathtubs & Showers'
   '/Objects/Bath/Faucets'
  '/Objects/Gambling'
  '/Objects/Stationary'
  '/Objects/Sculpting Base Meshes'
  '/Objects/Weather'
  '/Objects/Home Decor'
   '/Objects/Home Decor/Candles'
   '/Objects/Home Decor/Books'
   '/Objects/Home Decor/Picture Frames'
   '/Objects/Home Decor/Decorative Storage'
   '/Objects/Home Decor/Candleholders'
   '/Objects/Home Decor/Decorative Boxes'
   '/Objects/Home Decor/Clocks'
  '/Objects/Electronics & Technology'
  '/Objects/Miscellaneous'
  '/Objects/Sports Items'
  '/Objects/Food'
  '/Objects/Doors & Windows'

 --------------------------------------------------------------------------------
AssetCategoryHandler at 0X7FD2F3368910 (repo = net.maxon.repository.userprefs):


Managed IDs:

(maxon.Id('[email protected]277e57ecacc'),
 maxon.Id('[email protected]'),
 maxon.Id('[email protected]'),
 maxon.Id('[email protected]'),
 maxon.Id('[email protected]'))

Tree:

 '/MyUserCategory'
  '/MyUserCategory/Blah'
   '/MyUserCategory/Blah/Blub'
  '/MyUserCategory/Blub'
   '/MyUserCategory/Blub/Blah'

 --------------------------------------------------------------------------------
AssetCategoryHandler at 0X7FD2F1CBED10 (repo = [email protected]):


Managed IDs:

(maxon.Id('[email protected]'),
 maxon.Id('[email protected]'),
 maxon.Id('[email protected]'),
 maxon.Id('[email protected]'),
 maxon.Id('[email protected]'))

Tree:

 '/AlsoUserCategory'
  '/AlsoUserCategory/Foo'
   '/AlsoUserCategory/Foo/Bar'
  '/AlsoUserCategory/Blah'
   '/AlsoUserCategory/Blah/Blub'

The code:

"""Provides a type to expand asset categories into trees.
"""

import c4d
import maxon
import typing
import pprint

class AssetCategoryHandler:
    """Handles an asset category that is a root to zero to many descendant categories.
    """
    def __init__(self, 
                 asset: maxon.AssetDescription, 
                 categories: list[maxon.AssetDescription] = [],
                 language: maxon.LanguageRef = maxon.Resource.GetCurrentLanguage(),
                 path: str = ""):
        """Initializes the handler.

        Args:
            asset (maxon.AssetDescription): The category asset to expand.
            categories (list[maxon.AssetDescription], optional): A list of categories which should 
             be respected for expansion, when the empty list is passed, the assets will be gathered
             from the repository #asset is attached to. Defaults to [].
            language (maxon.LanguageRef, optional): The language to retrieve asset strings in. 
             Defaults to maxon.Resource.GetCurrentLanguage().
            path (str, optional): [internal] The current parent path. Defaults to "".

        Raises:
            TypeError: On type assertion failures.
        """
        # Type checks and retrieving some data from the asset.
        if not isinstance(asset, maxon.AssetDescription) or asset.IsNullValue():
            raise TypeError(f"{asset = }")
        if not isinstance(categories, list):
            raise TypeError(f"{categories = }")
        if not isinstance(language, maxon.LanguageRef) or language.IsNullValue():
            raise TypeError(f"{language = }")

        self._asset: maxon.AssetDescription = asset
        self._aid: maxon.Id = maxon.Id(asset.GetId()) if isinstance(asset.GetId(), str) else asset.GetId()
        self._name: str = asset.GetMetaString(maxon.OBJECT.BASE.NAME, language, "")
        self._path: str = f"{path}/{self._name}"
        self._repo: maxon.AssetRepositoryRef = asset.GetRepository()
        self._categories: list[maxon.AssetDescription] = categories
        self._language: maxon.LanguageRef = language
        self._children: list["AssetCategoryHandler"] = []

        # Populate _categories when empty.
        if len(self._categories) < 1:
            self._categories = self._repo.FindAssets(
                maxon.AssetTypes.Category(), maxon.Id(),maxon.Id(), maxon.ASSET_FIND_MODE.LATEST)

        # Expand the tree and cache the IDs.
        self.__expand__()
        self._ids: tuple[maxon.Id] = tuple(self.__yieldids__())
    
    def __repr__(self) -> str:
        """Returns a string representation of the handler.
        """
        return f"{self.__class__.__name__} at {str(hex(id(self))).upper()}"

    def __expand__(self):
        """Expands the handler into a tree of handlers, one for each of the descendant categories
        of the wrapped category asset.
        """
        for asset in self._categories:
            if self._aid != maxon.CategoryAssetInterface.GetParentCategory(asset):
                continue

            child = AssetCategoryHandler(asset, self._categories, self._language, self._path)
            self._children.append(child)
    
    def __yieldids__(self) -> typing.Generator[maxon.Id, None, None]:
        """Provides a generator for all asset IDs associated with this handler.

        Use the property Ids instead, unless rebuilding this data is desired.
        """
        yield self._aid
        for child in self._children:
            for aid in child.__yieldids__():
                yield aid

    @property
    def Ids (self) -> tuple[maxon.Id]:
        """Returns a tuple of all asset IDs associated with this handler.
        """
        return self._ids

    def PrintTree(self, indent: int = 0) -> None:
        """Prints an asset tree for this handler.
        """
        print(f"{' ' * indent}'{self._path}'")
        for handler in self._children:
            handler.PrintTree(indent + 1)
        
    
def main() -> None :
    """Runs the example.
    """
    # Get the user preferences asset repository, in a production environment it will contain all
    # relevant assets as of 2023.1.0.
    if not maxon.AssetDataBasesInterface.WaitForDatabaseLoading():
        raise RuntimeError("Could not load asset databases.")
    
    repo: maxon.AssetRepositoryInterface = maxon.AssetInterface.GetUserPrefsRepository()
    if not repo:
        raise RuntimeError("Unable to retrieve user repository.")

    # Used to shorten the call signatures of the FindLatestAsset() calls below.
    kwargs: dict = {
        "type": maxon.AssetTypes.Category(), "version": maxon.Id(), 
        "findMode": maxon.ASSET_FIND_MODE.LATEST }

    # Get a couple of root level asset categories.
    categoryRoots: list[maxon.AssetDescription] = [
        # The /Objects category in the Asset Browser, a built-in category.
        repo.FindLatestAsset(aid=maxon.Id("[email protected]"), **kwargs),
        
        # # /Example Scenes
        # repo.FindLatestAsset(aid= maxon.Id("[email protected]"), **kwargs),
        # # /Materials
        # repo.FindLatestAsset(aid= maxon.Id("category_a1ba084a9eeedb9b"), **kwargs),
        # # /Nodes
        # repo.FindLatestAsset(aid= maxon.Id("[email protected]"), **kwargs),
        # # /Textures
        # repo.FindLatestAsset(aid= maxon.Id("[email protected]"), **kwargs),

        # Custom category trees, they will be filtered out on other machines then mine, since other
        # machines won't find these assets.

        # /MyUserCategory (stored in the database in the user prefs)
        repo.FindLatestAsset(aid= maxon.Id("[email protected]"), **kwargs),

        # /AlsoUserCategory (stored in a custom database)
        repo.FindLatestAsset(aid= maxon.Id("[email protected]"), **kwargs),
    ]

    # Wrap each one of them into a handler.
    categoryHandlerList: list["AssetCategoryHandler"] = [
        AssetCategoryHandler(asset) for asset in categoryRoots 
        if isinstance(asset, maxon.AssetDescription) and not asset.IsNullValue()]

    # Iterate over the handlers and inspect their data.
    for handler in categoryHandlerList:
        print ("\n", "-" * 80)
        print (f"{handler} (repo = {handler._asset.GetRepositoryId()}):\n")

        print ("\nManaged IDs:\n")
        pprint.pprint(handler.Ids)

        print ("\nTree:\n")
        handler.PrintTree(indent=1)

if __name__ == "__main__":
    main()