# ***** BEGIN GPL LICENSE BLOCK ***** # # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License # as published by the Free Software Foundation; either version 2 # of the License, or (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software Foundation, # Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. # # ***** END GPL LICENCE BLOCK ***** # # (c) 2014, Blender Foundation - Campbell Barton # (c) 2018, Blender Foundation - Sybren A. Stüvel """Low-level functions called by file2block. Those can expand data blocks and yield their dependencies (e.g. other data blocks necessary to render/display/work with the given data block). """ import logging import typing from blender_asset_tracer import blendfile, cdefs from blender_asset_tracer.blendfile import iterators # Don't warn about these types at all. _warned_about_types = {b"LI", b"DATA"} _funcs_for_code = {} # type: typing.Dict[bytes, typing.Callable] log = logging.getLogger(__name__) def expand_block( block: blendfile.BlendFileBlock, ) -> typing.Iterator[blendfile.BlendFileBlock]: """Generator, yield the data blocks used by this data block.""" try: expander = _funcs_for_code[block.code] except KeyError: if block.code not in _warned_about_types: log.debug("No expander implemented for block type %r", block.code.decode()) _warned_about_types.add(block.code) return log.debug("Expanding block %r", block) for dependency in expander(block): if not dependency: # Filter out falsy blocks, i.e. None values. # Allowing expanders to yield None makes them more consise. continue if dependency.code == b"DATA": log.warn( "expander yielded block %s which will be ignored in later iteration", dependency, ) yield dependency def dna_code(block_code: str): """Decorator, marks decorated func as expander for that DNA code.""" assert isinstance(block_code, str) def decorator(wrapped): _funcs_for_code[block_code.encode()] = wrapped return wrapped return decorator def _expand_generic_material(block: blendfile.BlendFileBlock): array_len = block.get(b"totcol") yield from block.iter_array_of_pointers(b"mat", array_len) def _expand_generic_mtex(block: blendfile.BlendFileBlock): if not block.dna_type.has_field(b"mtex"): # mtex was removed in Blender 2.8 return for mtex in block.iter_fixed_array_of_pointers(b"mtex"): yield mtex.get_pointer(b"tex") yield mtex.get_pointer(b"object") def _expand_generic_nodetree(block: blendfile.BlendFileBlock): assert block.dna_type.dna_type_id == b"bNodeTree" nodes = block.get_pointer((b"nodes", b"first")) # See DNA_node_types.h socket_types_with_value_pointer = { cdefs.SOCK_OBJECT, # bNodeSocketValueObject cdefs.SOCK_IMAGE, # bNodeSocketValueImage cdefs.SOCK_COLLECTION, # bNodeSocketValueCollection cdefs.SOCK_TEXTURE, # bNodeSocketValueTexture cdefs.SOCK_MATERIAL, # bNodeSocketValueMaterial } for node in iterators.listbase(nodes): if node[b"type"] == cdefs.CMP_NODE_R_LAYERS: continue # The 'id' property points to whatever is used by the node # (like the image in an image texture node). yield node.get_pointer(b"id") # Default values of inputs can also point to ID datablocks. inputs = node.get_pointer((b"inputs", b"first")) for input in iterators.listbase(inputs): if input[b"type"] not in socket_types_with_value_pointer: continue value_container = input.get_pointer(b"default_value") if not value_container: continue value = value_container.get_pointer(b"value") yield value def _expand_generic_idprops(block: blendfile.BlendFileBlock): """Yield ID datablocks and their libraries referenced from ID properties.""" # TODO(@sybren): this code is very crude, and happens to work on ID # properties of Geometry Nodes modifiers, which is what it was written for. # It should probably be rewritten to properly iterate over & recurse into # all groups. settings_props = block.get_pointer((b"settings", b"properties")) if not settings_props: return subprops = settings_props.get_pointer((b"data", b"group", b"first")) for idprop in iterators.listbase(subprops): if idprop[b"type"] != cdefs.IDP_ID: continue id_datablock = idprop.get_pointer((b"data", b"pointer")) if not id_datablock: continue yield id_datablock def _expand_generic_nodetree_id(block: blendfile.BlendFileBlock): block_ntree = block.get_pointer(b"nodetree", None) if block_ntree is not None: yield from _expand_generic_nodetree(block_ntree) def _expand_generic_animdata(block: blendfile.BlendFileBlock): block_adt = block.get_pointer(b"adt") if block_adt: yield block_adt.get_pointer(b"action") # TODO, NLA @dna_code("AR") def _expand_armature(block: blendfile.BlendFileBlock): yield from _expand_generic_animdata(block) @dna_code("CU") def _expand_curve(block: blendfile.BlendFileBlock): yield from _expand_generic_animdata(block) yield from _expand_generic_material(block) for fieldname in ( b"vfont", b"vfontb", b"vfonti", b"vfontbi", b"bevobj", b"taperobj", b"textoncurve", ): yield block.get_pointer(fieldname) @dna_code("GR") def _expand_group(block: blendfile.BlendFileBlock): log.debug("Collection/group Block: %s (name=%s)", block, block.id_name) objects = block.get_pointer((b"gobject", b"first")) for item in iterators.listbase(objects): yield item.get_pointer(b"ob") # Recurse through child collections. try: children = block.get_pointer((b"children", b"first")) except KeyError: # 'children' was introduced in Blender 2.8 collections pass else: for child in iterators.listbase(children): subcoll = child.get_pointer(b"collection") if subcoll is None: continue if subcoll.dna_type_id == b"ID": # This issue happened while recursing a linked-in 'Hidden' # collection in the Chimes set of the Spring project. Such # collections named 'Hidden' were apparently created while # converting files from Blender 2.79 to 2.80. This error # isn't reproducible with just Blender 2.80. yield subcoll continue log.debug( "recursing into child collection %s (name=%r, type=%r)", subcoll, subcoll.id_name, subcoll.dna_type_name, ) yield from _expand_group(subcoll) @dna_code("LA") def _expand_lamp(block: blendfile.BlendFileBlock): yield from _expand_generic_animdata(block) yield from _expand_generic_nodetree_id(block) yield from _expand_generic_mtex(block) @dna_code("MA") def _expand_material(block: blendfile.BlendFileBlock): yield from _expand_generic_animdata(block) yield from _expand_generic_nodetree_id(block) yield from _expand_generic_mtex(block) try: yield block.get_pointer(b"group") except KeyError: # Groups were removed from Blender 2.8 pass @dna_code("MB") def _expand_metaball(block: blendfile.BlendFileBlock): yield from _expand_generic_animdata(block) yield from _expand_generic_material(block) @dna_code("ME") def _expand_mesh(block: blendfile.BlendFileBlock): yield from _expand_generic_animdata(block) yield from _expand_generic_material(block) yield block.get_pointer(b"texcomesh") # TODO, TexFace? - it will be slow, we could simply ignore :S @dna_code("NT") def _expand_node_tree(block: blendfile.BlendFileBlock): yield from _expand_generic_animdata(block) yield from _expand_generic_nodetree(block) @dna_code("OB") def _expand_object(block: blendfile.BlendFileBlock): yield from _expand_generic_animdata(block) yield from _expand_generic_material(block) yield block.get_pointer(b"data") if block[b"transflag"] & cdefs.OB_DUPLIGROUP: yield block.get_pointer(b"dup_group") yield block.get_pointer(b"proxy") yield block.get_pointer(b"proxy_group") # 'ob->pose->chanbase[...].custom' block_pose = block.get_pointer(b"pose") if block_pose: assert block_pose.dna_type.dna_type_id == b"bPose" # sdna_index_bPoseChannel = block_pose.file.sdna_index_from_id[b'bPoseChannel'] channels = block_pose.get_pointer((b"chanbase", b"first")) for pose_chan in iterators.listbase(channels): yield pose_chan.get_pointer(b"custom") # Expand the objects 'ParticleSettings' via 'ob->particlesystem[...].part' # sdna_index_ParticleSystem = block.file.sdna_index_from_id.get(b'ParticleSystem') # if sdna_index_ParticleSystem is not None: psystems = block.get_pointer((b"particlesystem", b"first")) for psystem in iterators.listbase(psystems): yield psystem.get_pointer(b"part") # Modifiers can also refer to other datablocks, which should also get expanded. for block_mod in iterators.modifiers(block): mod_type = block_mod[b"modifier", b"type"] # Currently only node groups are supported. If the support should expand # to more types, something more intelligent than this should be made. if mod_type == cdefs.eModifierType_Nodes: yield from _expand_generic_idprops(block_mod) yield block_mod.get_pointer(b"node_group") @dna_code("PA") def _expand_particle_settings(block: blendfile.BlendFileBlock): yield from _expand_generic_animdata(block) yield from _expand_generic_mtex(block) block_ren_as = block[b"ren_as"] if block_ren_as == cdefs.PART_DRAW_GR: yield block.get_pointer(b"dup_group") elif block_ren_as == cdefs.PART_DRAW_OB: yield block.get_pointer(b"dup_ob") @dna_code("SC") def _expand_scene(block: blendfile.BlendFileBlock): yield from _expand_generic_animdata(block) yield from _expand_generic_nodetree_id(block) yield block.get_pointer(b"camera") yield block.get_pointer(b"world") yield block.get_pointer(b"set", default=None) yield block.get_pointer(b"clip", default=None) # sdna_index_Base = block.file.sdna_index_from_id[b'Base'] # for item in bf_utils.iter_ListBase(block.get_pointer((b'base', b'first'))): # yield item.get_pointer(b'object', sdna_index_refine=sdna_index_Base) bases = block.get_pointer((b"base", b"first")) for base in iterators.listbase(bases): yield base.get_pointer(b"object") # Sequence Editor block_ed = block.get_pointer(b"ed") if not block_ed: return strip_type_to_field = { cdefs.SEQ_TYPE_SCENE: b"scene", cdefs.SEQ_TYPE_MOVIECLIP: b"clip", cdefs.SEQ_TYPE_MASK: b"mask", cdefs.SEQ_TYPE_SOUND_RAM: b"sound", } for strip, strip_type in iterators.sequencer_strips(block_ed): try: field_name = strip_type_to_field[strip_type] except KeyError: continue yield strip.get_pointer(field_name) @dna_code("TE") def _expand_texture(block: blendfile.BlendFileBlock): yield from _expand_generic_animdata(block) yield from _expand_generic_nodetree_id(block) yield block.get_pointer(b"ima") @dna_code("WO") def _expand_world(block: blendfile.BlendFileBlock): yield from _expand_generic_animdata(block) yield from _expand_generic_nodetree_id(block) yield from _expand_generic_mtex(block)