Useful when you want to change the handedness or orientation of an export without changing anything in the scene. Also fixed some issues with the normal contextual export operator such as not respecting the selection status of the objects and doing double-exports on objects.
145 lines
5.9 KiB
Python
145 lines
5.9 KiB
Python
'''
|
|
Depth-first object iterator functions for Blender collections and view layers.
|
|
|
|
These functions are used to iterate over objects in a collection or view layer in a depth-first manner, including
|
|
instances. This is useful for exporters that need to traverse the object hierarchy in a predictable order.
|
|
'''
|
|
|
|
from typing import Optional, Set, Iterable, List
|
|
|
|
from bpy.types import Collection, Object, ViewLayer, LayerCollection, Context
|
|
from mathutils import Matrix
|
|
|
|
|
|
class DfsObject:
|
|
'''
|
|
Represents an object in a depth-first search.
|
|
'''
|
|
def __init__(self, obj: Object, instance_objects: List[Object], matrix_world: Matrix):
|
|
self.obj = obj
|
|
self.instance_objects = instance_objects
|
|
self.matrix_world = matrix_world
|
|
|
|
@property
|
|
def is_visible(self) -> bool:
|
|
'''
|
|
Check if the object is visible.
|
|
@return: True if the object is visible, False otherwise.
|
|
'''
|
|
if self.instance_objects:
|
|
return self.instance_objects[-1].visible_get()
|
|
return self.obj.visible_get()
|
|
|
|
@property
|
|
def is_selected(self) -> bool:
|
|
'''
|
|
Check if the object is selected.
|
|
@return: True if the object is selected, False otherwise.
|
|
'''
|
|
if self.instance_objects:
|
|
return self.instance_objects[-1].select_get()
|
|
return self.obj.select_get()
|
|
|
|
|
|
|
|
def _dfs_object_children(obj: Object, collection: Collection) -> Iterable[Object]:
|
|
'''
|
|
Construct a list of objects in hierarchy order from `collection.objects`, only keeping those that are in the
|
|
collection.
|
|
@param obj: The object to start the search from.
|
|
@param collection: The collection to search in.
|
|
@return: An iterable of objects in hierarchy order.
|
|
'''
|
|
yield obj
|
|
for child in obj.children:
|
|
if child.name in collection.objects:
|
|
yield from _dfs_object_children(child, collection)
|
|
|
|
|
|
def dfs_objects_in_collection(collection: Collection) -> Iterable[Object]:
|
|
'''
|
|
Returns a depth-first iterator over all objects in a collection, only keeping those that are directly in the
|
|
collection.
|
|
@param collection: The collection to search in.
|
|
@return: An iterable of objects in hierarchy order.
|
|
'''
|
|
objects_hierarchy = []
|
|
for obj in collection.objects:
|
|
if obj.parent is None or obj.parent not in set(collection.objects):
|
|
objects_hierarchy.append(obj)
|
|
for obj in objects_hierarchy:
|
|
yield from _dfs_object_children(obj, collection)
|
|
|
|
|
|
def dfs_collection_objects(collection: Collection, visible_only: bool = False) -> Iterable[DfsObject]:
|
|
'''
|
|
Depth-first search of objects in a collection, including recursing into instances.
|
|
@param collection: The collection to search in.
|
|
@return: An iterable of tuples containing the object, the instance objects, and the world matrix.
|
|
'''
|
|
yield from _dfs_collection_objects_recursive(collection)
|
|
|
|
|
|
def _dfs_collection_objects_recursive(
|
|
collection: Collection,
|
|
instance_objects: Optional[List[Object]] = None,
|
|
matrix_world: Matrix = Matrix.Identity(4),
|
|
visited: Optional[Set[Object]]=None
|
|
) -> Iterable[DfsObject]:
|
|
'''
|
|
Depth-first search of objects in a collection, including recursing into instances.
|
|
This is a recursive function.
|
|
@param collection: The collection to search in.
|
|
@param instance_objects: The running hierarchy of instance objects.
|
|
@param matrix_world: The world matrix of the current object.
|
|
@param visited: A set of visited object-instance pairs.
|
|
@return: An iterable of tuples containing the object, the instance objects, and the world matrix.
|
|
'''
|
|
|
|
# We want to also yield the top-level instance object so that callers can inspect the selection status etc.
|
|
if visited is None:
|
|
visited = set()
|
|
|
|
if instance_objects is None:
|
|
instance_objects = list()
|
|
|
|
# First, yield all objects in child collections.
|
|
for child in collection.children:
|
|
yield from _dfs_collection_objects_recursive(child, instance_objects, matrix_world.copy(), visited)
|
|
|
|
# Then, evaluate all objects in this collection.
|
|
for obj in dfs_objects_in_collection(collection):
|
|
visited_pair = (obj, instance_objects[-1] if instance_objects else None)
|
|
if visited_pair in visited:
|
|
continue
|
|
# If this an instance, we need to recurse into it.
|
|
if obj.instance_collection is not None:
|
|
# Calculate the instance transform.
|
|
instance_offset_matrix = Matrix.Translation(-obj.instance_collection.instance_offset)
|
|
# Recurse into the instance collection.
|
|
yield from _dfs_collection_objects_recursive(obj.instance_collection,
|
|
instance_objects + [obj],
|
|
matrix_world @ (obj.matrix_world @ instance_offset_matrix),
|
|
visited)
|
|
else:
|
|
# Object is not an instance, yield it.
|
|
yield DfsObject(obj, instance_objects, matrix_world @ obj.matrix_world)
|
|
visited.add(visited_pair)
|
|
|
|
|
|
def dfs_view_layer_objects(view_layer: ViewLayer) -> Iterable[DfsObject]:
|
|
'''
|
|
Depth-first iterator over all objects in a view layer, including recursing into instances.
|
|
@param view_layer: The view layer to inspect.
|
|
@return: An iterable of tuples containing the object, the instance objects, and the world matrix.
|
|
'''
|
|
def layer_collection_objects_recursive(layer_collection: LayerCollection, visited: Set[Object]=None):
|
|
if visited is None:
|
|
visited = set()
|
|
for child in layer_collection.children:
|
|
yield from layer_collection_objects_recursive(child, visited=visited)
|
|
# Iterate only the top-level objects in this collection first.
|
|
yield from _dfs_collection_objects_recursive(layer_collection.collection, visited=visited)
|
|
|
|
yield from layer_collection_objects_recursive(view_layer.layer_collection)
|