Files
open-wc/reference/blender-wow-studio-3.4-1.1.0_Experimental/io_scene_wmo/utils/collections.py
T

359 lines
14 KiB
Python

from .custom_object import CustomObject
from .bl_id_types_utils import match_id_name, BL_ID_NAME_TEMPLATE
from ..ui.message_stack import MessageStack
import bpy
import re
import os
from typing import Set, Sequence, Type
from pathlib import Path
def get_collection(model_collection: bpy.types.Collection
, col_name: str) -> bpy.types.Collection | None:
"""
Get child collection matching provided name excluding Blender's copy index (.xxx).
:param model_collection: Collection to search in.
:param col_name: Name of child collection.
:return: Collection if found, else None.
"""
# check if parent collection exists
if not model_collection:
raise KeyError("get_collection() called with Null WoW Model Collection.")
# attempt regular search
if (col := model_collection.children.get(col_name)) is not None:
return col
# attempt regex search
for col in model_collection.children:
match = re.match(BL_ID_NAME_TEMPLATE, col.name)
if match is None or match.group(1) != col_name:
continue
return col
def get_or_create_collection(model_collection: bpy.types.Collection
, col_name: str) -> bpy.types.Collection:
"""
Get child collection matching provided name excluding Blender's copy index (.xxx) or create it.
:param model_collection: Collection to search in. ('wow_wmo', 'wow_m2'...)
:param col_name: Name of child collection.
:return: Requested collection.
"""
# check if model_collection exists
if not model_collection:
# print("Error : WoW Model collection doesn't exist.")
raise KeyError("get_or_create_collection() called with Null WoW Model Collection.")
# can't create it because we don't have the type (wmo/m2/adt)
# This shouldn't fail if it is called with model_collection = get_current_wow_model_collection() if there is an active collection.
col = get_collection(model_collection, col_name)
if not col:
col = bpy.data.collections.new(col_name)
model_collection.children.link(col)
col.color_tag = 'COLOR_05'
return col
def obj_swap_collections(obj: bpy.types.Object, col_from: bpy.types.Collection, col_to: bpy.types.Collection):
"""
Safely unlink an object from one collection and link to another.
:param obj: Object to swap collection.
:param col_from: Collection from which to unlink.
:param col_to: Collection to which to link.
"""
try:
col_from.objects.unlink(obj)
except RuntimeError as e:
print(f"Error: exception occured while swapping object \"{obj.name}\" from collection \"{col_from.name}\" to"
"collection \"{col_to.name}\".\n{e}")
try:
col_to.objects.link(obj)
except RuntimeError as e:
print(f"Error: exception occured while swapping object \"{obj.name}\" from collection \"{col_from.name}\" to"
"collection \"{col_to.name}\".\n{e}")
def collection_swap_parent_collection(col: bpy.types.Collection
, col_from: bpy.types.Collection
, col_to: bpy.types.Collection):
"""
Safely unlink a collection from one collection, and link to another.
:param col: Collection to swap.
:param col_from: Collection from which to unlink.
:param col_to: Collection to which to link.
"""
try:
col_from.children.unlink(col)
except RuntimeError:
print(f"Error: exception occured while swapping parent of collection \"{col.name}\" from collection"
"\"{col_from.name}\" to collection \"{col_to.name}\".\n{e}")
try:
col_to.children.link(col)
except RuntimeError:
print(f"Error: exception occured while swapping parent of collection \"{col.name}\" from collection"
"\"{col_from.name}\" to collection \"{col_to.name}\".\n{e}")
def unlink_nested_collections(parent_col: bpy.types.Collection
, col: bpy.types.Collection) -> bool:
"""
Unlinks all nested collections from col to wmo_col.
:param parent_col: WMO collection to unlink irrelevant ones into.
:param col: Collection to sanitize.
:return True if had nested collections, else False.
"""
has_children = bool(len(col.children))
for child_col in col.children:
collection_swap_parent_collection(child_col, col, parent_col)
return has_children
def create_wmo_model_collection(scene: bpy.types.Scene,
filepath: str, wowpath: str)-> bpy.types.Collection:
# col = bpy.data.collections.new(os.path.basename(wowpath)) # filename (+ extension?)
filename = ''
if wowpath:
filename = Path(wowpath).stem
else:
filename = Path(filepath).stem
col = bpy.data.collections.new(filename) # filename only without extension
col.wow_wmo.enabled = True
def filepath_wmo(filepath):
filepath = filepath.lower()
filepath = Path(filepath)
filepath_parts = filepath.parts
try:
world_index = filepath_parts.index('world')
except ValueError:
return 'World directory not found in the filepath'
return str(Path(*filepath_parts[world_index:-1]))
col.wow_wmo.dir_path = filepath_wmo(filepath)
scene.collection.children.link(col)
# set the collection as active
layer_collection = bpy.context.view_layer.layer_collection.children[col.name]
bpy.context.view_layer.active_layer_collection = layer_collection
print("Created new WMO model Collection:" + col.name)
return col
""""
def create_wow_model_collection(scene: bpy.types.Scene
, id_prop: str) -> bpy.types.Collection | None:
Create a WoW model collection.
:param scene: Current scene.
:param id_prop: Identifier of the collection property for the given model type.
:return: Model (M2/WMO/ADT) collection
col = bpy.data.collections.new(id_prop) # just name it 'wow_wmo' ?
getattr(col, id_prop).enabled = True
# scene.collection = col
bpy.context.scene.collection.children.link(col)
layer_collection = bpy.context.view_layer.layer_collection.children[col.name]
bpy.context.view_layer.active_layer_collection = layer_collection
print("Created new WoW model Collection:" + col.name)
return col
"""
def get_current_wow_model_collection(scene: bpy.types.Scene
, id_prop: str) -> bpy.types.Collection | None:
"""
Gets currently active model collection.
:param scene: Current scene.
:param id_prop: Identifier of the collection property for the given model type.
:return: Model (M2/WMO/ADT) collection if found, else None.
"""
# check if there is an active collection at all
# don't print or it will spam when using it in panels
if not bpy.context.collection:
# print("Error: Couldn't find current WoW model collection: there is no active collection at all.")
return None
# return create_wow_model_collection(scene, id_prop)
act_col: bpy.types.Collection = bpy.context.collection
# print("active collection : " + act_col.name)
# check if the collection is a model collection itself
if act_col.name in scene.collection.children:
if getattr(act_col, id_prop).enabled:
return act_col
# print("Error: Couldn't find current WoW model collection: Active collection is not enabled to be a WoW "
# "Collection.")
return None
# return create_wow_model_collection(scene, id_prop)
# check if the collection is a child of some existing collections.
for col in scene.collection.children:
# double check if this fix is right and I udnerstood it correctly
if getattr(col, id_prop).enabled and act_col in col.children_recursive:
return col
# print("Error : Failed to find a WoW Model Collection.")
return None
# return create_wow_model_collection(scene, id_prop)
class SpecialCollection:
""" Defines rules for handling updates to special collections. """
__wbs_collection_name__: str
""" Name of the special collection. """
__wbs_root_collection_id_prop_name__: str
""" Name of the property group of a root collection. E.g. wow_wmo or wow_m2"""
__wbs_bl_object_types__: Set[str] = set()
""" Set of Blender object types allowed to be part of the collection.
(Used when no custom object type is defined.
"""
__wbs_custom_object_types__: Set[Type[CustomObject]] | None = None
""" Optional set of custom object types restricted to the collection. """
_required = { '__wbs_collection_name__', '__wbs_root_collection_id_prop_name__'}
""" Required fields to override. """
def __init_subclass__(cls, **kwargs):
for requirement in cls._required:
if not hasattr(cls, requirement):
raise NotImplementedError(f'"{cls.__name__}" must override "{requirement}".')
super().__init_subclass__(**kwargs)
@classmethod
def _get_root_collection(cls
, scene: bpy.types.Scene
, col: bpy.types.Collection) -> bpy.types.Collection | None:
"""
Gets parent WoW collection (recursive) if existing, or None.
:param scene: Current scene.
:param col: Collection to search the parent for.
:return: WMO parent collection or None if not found.
"""
for scene_col in scene.collection.children:
if not getattr(scene_col, cls.__wbs_root_collection_id_prop_name__).enabled:
continue
for child_col in scene_col.children_recursive:
if child_col == col:
return scene_col
@classmethod
def verify_root_collection_integrity(cls
, root_col: bpy.types.Collection
, special_collection_ts: Sequence[Type['SpecialCollection']]):
"""
Verifies presence of all special collections within a model collection.
:param root_col: Root model collection.
:param special_collection_ts: Special collections to ensure.
"""
n_special_cols = len(special_collection_ts)
status = [False] * n_special_cols
status_counter = 0
# search for matching collections
for child_col in root_col.children:
for i, special_col_t in enumerate(special_collection_ts):
if match_id_name(child_col.name, special_col_t.__wbs_collection_name__):
status[i] = True
status_counter += 1
break
if status_counter == n_special_cols:
break
# check if we found everything
if status_counter == n_special_cols:
return
for i, s in enumerate(status):
if not s:
special_collection_ts[i].create_collection(root_col)
@classmethod
def handle_collection_if_matched(cls
, scene: bpy.types.Scene
, update: bpy.types.DepsgraphUpdate
, special_collection_ts: Sequence[Type['SpecialCollection']]) -> bool:
"""
Handle update to collection of matches the provided depsgraph update.
:param scene: Current scene.
:param update: Depsgraph update.
:param special_collection_ts: Available collection types.
:return: True if handled, else False.
"""
collection: bpy.types.Collection = update.id.original
# test if collection matches the criteria of a special collection
if not match_id_name(collection.name, cls.__wbs_collection_name__):
return False
# test if collection is within a root collection
root_col = cls._get_root_collection(scene, collection)
if root_col is None:
return False
cls.verify_root_collection_integrity(root_col, special_collection_ts)
for obj in collection.objects:
# handle custom object types
if cls.__wbs_custom_object_types__ is not None:
if not any(c_obj.match(obj) for c_obj in cls.__wbs_custom_object_types__):
obj_swap_collections(obj, collection, root_col)
MessageStack().push_message(msg=f'Object "{obj.name}" removed from special collection '
f'"{cls.__wbs_collection_name__}" due to not matching '
f'with required custom object types.', icon='ERROR')
# handle normal object types
elif obj.type not in cls.__wbs_bl_object_types__:
obj_swap_collections(obj, collection, root_col)
MessageStack().push_message(msg=f'Object "{obj.name}" removed from special collection '
f'"{cls.__wbs_collection_name__}". Objects of type \'{obj.type}\' '
f'are not allowed.', icon='ERROR')
# handle nested collections
if unlink_nested_collections(root_col, collection):
MessageStack().push_message(msg=f'This collection cannot have nested collections.', icon='ERROR')
return True
@classmethod
def create_collection(cls
, parent_col: bpy.types.Collection):
"""
Creates a special collection in a parent collection.
:param parent_col: Parent collection.
"""
col = bpy.data.collections.new(name=cls.__wbs_collection_name__)
col.color_tag = 'COLOR_02'
parent_col.children.link(col)