# SPDX-FileCopyrightText: 2025 Blender Studio Tools Authors # # SPDX-License-Identifier: GPL-3.0-or-later import bpy from bpy.types import Context from pathlib import Path import os from . import constants, config, opscore, logging from .asset_catalog import get_asset_catalog_items, get_asset_id from .config import verify_task_layer_json_data from .hooks import Hooks, get_production_hook_dir, get_asset_hook_dir from .images import save_images from .merge import publish, task_layer, naming from .prefs import get_addon_prefs class ASSETPIPE_OT_create_new_asset(bpy.types.Operator): bl_idname = "assetpipe.create_new_asset" bl_label = "Create New Asset" bl_description = """Create a new Asset Files and Folders at a given directory""" _name = "" _prefix = "" _json_path = None _asset_pipe = None create_files: bpy.props.BoolProperty( name="Create Files for Unselected Task Layers", default=True ) # Only Active/Stage Publish Types are avaliable publish_type: bpy.props.EnumProperty( name="Publish Type", items=constants.PUBLISH_TYPES[:2], ) @classmethod def poll(cls, context: bpy.types.Context) -> bool: asset_pipe = context.scene.asset_pipeline if asset_pipe.new_file_mode == "KEEP": if not asset_pipe.asset_collection: cls.poll_message_set("Missing Top Level Collection") return False else: if asset_pipe.name == "" or asset_pipe.dir == "": cls.poll_message_set("Asset Name and Directory must be valid") return False return True def invoke(self, context: bpy.types.Context, event): # Dynamically Create Task Layer Bools self._asset_pipe = context.scene.asset_pipeline config.verify_task_layer_json_data(self._asset_pipe.task_layer_config_type) all_task_layers = self._asset_pipe.all_task_layers all_task_layers.clear() for task_layer_key in config.TASK_LAYER_TYPES: if task_layer_key == "NONE": continue new_task_layer = all_task_layers.add() new_task_layer.name = task_layer_key self.publish_type = constants.STAGED_PUBLISH_KEY return context.window_manager.invoke_props_dialog(self, width=400) def draw(self, context: bpy.types.Context): box = self.layout.box() all_task_layers = self._asset_pipe.all_task_layers box.label(text="Choose Which Task Layers will be local the current file") for task_layer_bool in all_task_layers: box.prop(task_layer_bool, "is_local", text=task_layer_bool.name) self.layout.prop(self, "create_files") self.layout.prop(self, "publish_type") def _asset_name_set(self, context) -> None: if self._asset_pipe.new_file_mode == "KEEP": asset_col = self._asset_pipe.asset_collection name = ( asset_col.name if constants.NAME_DELIMITER not in asset_col.name else asset_col.name.split(constants.NAME_DELIMITER, 1)[1] ) prefix = ( "" if constants.NAME_DELIMITER not in asset_col.name else asset_col.name.split(constants.NAME_DELIMITER, 1)[0] ) else: name = self._asset_pipe.name prefix = self._asset_pipe.prefix # Set to easily access these properties self._name = name self._prefix = prefix # Store these in the asset pipeline props group self._asset_pipe.name = name self._asset_pipe.prefix = prefix def _asset_dir_get(self, context) -> str: if self._asset_pipe.new_file_mode == "KEEP": return Path(bpy.data.filepath).parent.__str__() else: user_dir = bpy.path.abspath(self._asset_pipe.dir) return os.path.join(user_dir, self._name) def _load_task_layers(self, context): all_task_layers = self._asset_pipe.all_task_layers local_tls = [] for task_layer_bool in all_task_layers: if task_layer_bool.is_local: local_tls.append(task_layer_bool.name) if not any(task_layer_bool.is_local for task_layer_bool in all_task_layers): self.report( {'ERROR'}, "Please select at least one task layer to be local to the current file", ) return {'CANCELLED'} return local_tls def _create_publish_directories(self, context, asset_directory): for publish_type in constants.PUBLISH_KEYS: new_dir_path = os.path.join(asset_directory, publish_type) if os.path.exists(new_dir_path): self.report( {'ERROR'}, f"Directory for '{publish_type}' already exists", ) return {'CANCELLED'} os.mkdir(new_dir_path) def _asset_collection_get(self, context, local_tls): if self._asset_pipe.new_file_mode == "KEEP": asset_col = self._asset_pipe.asset_collection for col in asset_col.children: col.asset_id_owner = local_tls[0] else: bpy.data.collections.new(self._name) asset_col = bpy.data.collections.get(self._name) if asset_col not in list(asset_col.children): context.scene.collection.children.link(asset_col) self._asset_pipe.asset_collection = asset_col return asset_col def _remove_collections(self, context): # Remove Data From task layer Files except for asset_collection for col in bpy.data.collections: if not col == self._asset_pipe.asset_collection: bpy.data.collections.remove(col) for obj in bpy.data.objects: bpy.data.objects.remove(obj) bpy.ops.outliner.orphans_purge(do_local_ids=True, do_linked_ids=False, do_recursive=True) def _task_layer_collections_set(self, context, asset_col, local_tls): for task_layer_key in config.TASK_LAYER_TYPES: if task_layer_key not in local_tls: continue col_name = (f"{self._name}{constants.NAME_DELIMITER}{task_layer_key}").lower() bpy.data.collections.new(col_name) task_layer_col = bpy.data.collections.get(col_name) task_layer_col.asset_id_owner = task_layer_key if task_layer_col not in list(asset_col.children): asset_col.children.link(task_layer_col) def _first_file_create(self, context, local_tls, asset_directory) -> str: self._asset_pipe.is_asset_pipeline_file = True asset_col = self._asset_collection_get(context, local_tls) self._task_layer_collections_set(context, asset_col, local_tls) if bpy.data.filepath != "": first_file_name = Path(bpy.data.filepath).name else: first_file_name = ( self._name + constants.FILE_DELIMITER + local_tls[0].lower().replace(" ", "_") + ".blend" ) first_file = os.path.join(asset_directory, first_file_name) self._asset_pipe.set_local_task_layers(local_tls) bpy.ops.wm.save_as_mainfile(filepath=first_file, copy=True) return first_file def _task_layer_file_create(self, context, task_layer_key, asset_directory): name = self._name + constants.FILE_DELIMITER + task_layer_key.lower().replace(" ", "_") + ".blend" self._asset_pipe.set_local_task_layers([task_layer_key]) self._task_layer_collections_set( context, self._asset_pipe.asset_collection, [task_layer_key] ) task_layer_file = os.path.join(asset_directory, name) bpy.ops.wm.save_as_mainfile(filepath=task_layer_file, copy=True) def execute(self, context: bpy.types.Context): self._asset_name_set(context) asset_directory = self._asset_dir_get(context) local_tls = self._load_task_layers(context) if not os.path.exists(asset_directory): os.mkdir(asset_directory) self._create_publish_directories(context, asset_directory) # Save Task Layer Config File config.write_json_file( asset_path=Path(asset_directory), source_file_path=Path(self._asset_pipe.task_layer_config_type), ) if self._asset_pipe.new_file_mode == "BLANK": self._remove_collections(context) starting_file = self._first_file_create(context, local_tls, asset_directory) for task_layer_key in config.TASK_LAYER_TYPES: if task_layer_key == "NONE" or task_layer_key in local_tls: continue self._remove_collections(context) self._task_layer_file_create(context, task_layer_key, asset_directory) # Create intial publish based on task layers. self._remove_collections(context) publish.create_next_published_file(Path(starting_file), self.publish_type) if starting_file: bpy.ops.wm.open_mainfile(filepath=starting_file) return {'FINISHED'} class ASSETPIPE_OT_prepare_sync(bpy.types.Operator): bl_idname = "assetpipe.prepare_sync" bl_label = "Prepare Sync" bl_description = ( "Prepare all Objects for Sync; by updating the Ownership of Objects " "and Transferable Data. Also runs Pre-Pull hooks" ) bl_options = {'REGISTER', 'UNDO'} _temp_transfer_data = None _invalid_objs = [] _other_ids = [] def invoke(self, context: bpy.types.Context, event: bpy.types.Event): opscore.sync_invoke(self, context) return context.window_manager.invoke_props_dialog(self, width=400) def draw(self, context: bpy.types.Context): opscore.sync_draw(self, context) def execute(self, context: bpy.types.Context): asset_col = context.scene.asset_pipeline.asset_collection hooks_instance = Hooks() hooks_instance.load_hooks(context) hooks_instance.execute_hooks(merge_mode="pull", merge_status='pre', asset_col=asset_col) opscore.sync_execute_update_ownership(self, context) self.report({'INFO'}, "Ownership Updated") return {'FINISHED'} class ASSETPIPE_OT_sync_pull(bpy.types.Operator): bl_idname = "assetpipe.sync_pull" bl_label = "Pull Asset" bl_description = """Pull Task Layers from the published sync target""" bl_options = {'REGISTER', 'UNDO'} _temp_transfer_data = None _invalid_objs = [] _other_ids = [] _temp_dir: Path = None _current_file: Path = None _task_layer_key: str = "" _sync_target: Path = None save: bpy.props.BoolProperty( name="Save File & Images", default=True, description="Save Current File and Images before Push", ) @classmethod def poll(cls, context: bpy.types.Context) -> bool: if context.mode == 'OBJECT': return True cls.poll_message_set("Pull is only avaliable in Object Mode") return False def invoke(self, context: bpy.types.Context, event: bpy.types.Event): opscore.sync_invoke(self, context) return context.window_manager.invoke_props_dialog(self, width=400) def draw(self, context: bpy.types.Context): self.layout.prop(self, "save") opscore.sync_draw(self, context) def execute(self, context: bpy.types.Context): profiler = logging.get_profiler() profiler.reset() asset_col = context.scene.asset_pipeline.asset_collection if self.save: save_images() bpy.ops.wm.save_mainfile() hooks_instance = Hooks() hooks_instance.load_hooks(context) hooks_instance.execute_hooks(merge_mode="pull", merge_status='pre', asset_col=asset_col) # Find current task Layer opscore.sync_execute_update_ownership(self, context) opscore.sync_execute_prepare_sync(self, context) opscore.sync_execute_pull(self, context) hooks_instance.execute_hooks(merge_mode="pull", merge_status='post', asset_col=asset_col) self.report({'INFO'}, "Asset Pull Complete") profiler.log_all() return {'FINISHED'} class ASSETPIPE_OT_sync_push(bpy.types.Operator): bl_idname = "assetpipe.sync_push" bl_label = "Sync Asset" bl_description = """Sync the current Task Layer to the published sync target. File will be saved as part of the Push process""" _temp_transfer_data = None _invalid_objs = [] _other_ids = [] _temp_dir: Path = None _current_file: Path = None _task_layer_key: str = "" _sync_target: Path = None pull: bpy.props.BoolProperty( name="Pull before Pushing", default=True, description="Pull in any new data from the Published file before Pushing", ) @classmethod def poll(cls, context: bpy.types.Context) -> bool: if context.mode == 'OBJECT': return True cls.poll_message_set("Push is only avaliable in Object Mode") return False def invoke(self, context: bpy.types.Context, event: bpy.types.Event): opscore.sync_invoke(self, context) return context.window_manager.invoke_props_dialog(self, width=400) def draw(self, context: bpy.types.Context): if not self.pull: col = self.layout.column() col.label(text="Force Pushing without pulling can cause data loss", icon="ERROR") col.separator() opscore.sync_draw(self, context) def execute(self, context: bpy.types.Context): profiler = logging.get_profiler() profiler.reset() asset_col = context.scene.asset_pipeline.asset_collection hooks_instance = Hooks() hooks_instance.load_hooks(context) save_images() bpy.ops.wm.save_mainfile() # Seperate if statement so hook can execute before updating ownership/prep sync if self.pull: hooks_instance.execute_hooks(merge_mode="pull", merge_status='pre', asset_col=asset_col) # Find current task Layer opscore.sync_execute_update_ownership(self, context) opscore.sync_execute_prepare_sync(self, context) if self.pull: opscore.sync_execute_pull(self, context) hooks_instance.execute_hooks( merge_mode="pull", merge_status='post', asset_col=asset_col ) profiler.set_push() hooks_instance.execute_hooks(merge_mode="push", merge_status='pre', asset_col=asset_col) bpy.ops.wm.save_mainfile(filepath=self._current_file.__str__()) opscore.sync_execute_push(self, context) if self.pull: self.report({'INFO'}, "Asset Sync Complete") else: self.report({'INFO'}, "Asset Force Push Complete") profiler.log_all() return {'FINISHED'} class ASSETPIPE_OT_open_file(bpy.types.Operator): bl_idname = "assetpipe.open_file" bl_label = "Open File" bl_description = """Open an Asset Pipeline File, will not prompt to save current file""" filepath: bpy.props.StringProperty(name="Filepath") def execute(self, context: Context): bpy.ops.wm.open_mainfile(filepath=self.filepath) return {'FINISHED'} def get_publish_type_enum(self, context): sync_target = [ ( "sync_target", "Sync Target", "Find the Sync Target File, either Staged or Active", ), ] return sync_target + constants.PUBLISH_TYPES class ASSETPIPE_OT_open_publish(bpy.types.Operator): bl_idname = "assetpipe.open_publish" bl_label = "Open Latest Publish" bl_description = """Open the current Published File used for Push/Pull/Sync.""" publish_types: bpy.props.EnumProperty( name="Type", items=get_publish_type_enum, ) save_file: bpy.props.BoolProperty( name="Save Changes before Closing?", default=False, description="Save the file before opening Published File", ) # type: ignore def invoke(self, context: bpy.types.Context, event: bpy.types.Event): # self.publish_types = "sync_target" return context.window_manager.invoke_props_dialog(self, width=400) def draw(self, context: bpy.types.Context): layout = self.layout layout.prop(self, "publish_types") if bpy.data.is_dirty: layout.prop(self, "save_file") def execute(self, context: Context): if not context.scene.asset_pipeline.is_asset_pipeline_file: self.report({'ERROR'}, "Cannot open Publish, current file isn't asset pipeline file") return {'CANCELLED'} if Path(bpy.data.filepath).parent.name in constants.PUBLISH_KEYS: self.report({'ERROR'}, "Cannot open Publish, if current file is published") return {'CANCELLED'} if self.publish_types == "sync_target": published_file = publish.find_sync_target(Path(bpy.data.filepath)) else: published_file = publish.find_latest_publish( Path(bpy.data.filepath), self.publish_types ) if not published_file.exists(): self.report( {'ERROR'}, f"Cannot open {self.publish_types} no published file found at {str(published_file.parent)}", ) return {'CANCELLED'} if self.save_file: save_images() bpy.ops.wm.save_mainfile() bpy.ops.wm.open_mainfile(filepath=str(published_file)) return {'FINISHED'} class ASSETPIPE_OT_publish_new_version(bpy.types.Operator): bl_idname = "assetpipe.publish_new_version" bl_label = "Publish New Version" bl_description = """Create a new Published Version in the Publish Area""" publish_types: bpy.props.EnumProperty( name="Type", items=constants.PUBLISH_TYPES, ) @classmethod def poll(cls, context: bpy.types.Context) -> bool: if bpy.data.is_dirty: cls.poll_message_set( "Save the current file and/or Pull from last publish before creating new Publish" ) return False return True def invoke(self, context: bpy.types.Context, event: bpy.types.Event): return context.window_manager.invoke_props_dialog(self, width=400) def draw(self, context: bpy.types.Context): layout = self.layout layout.prop(self, "publish_types") def execute(self, context: bpy.types.Context): if ( publish.is_staged_publish(Path(bpy.data.filepath)) and self.publish_types != constants.SANDBOX_PUBLISH_KEY ): self.report( {'ERROR'}, f"Only '{constants.SANDBOX_PUBLISH_KEY}' Publish is supported when a version is staged", ) return {'CANCELLED'} catalog_id = get_asset_id(context.scene.asset_pipeline.asset_catalog_name) new_filepath = publish.create_next_published_file( current_file=Path(bpy.data.filepath), publish_type=self.publish_types, catalog_id=catalog_id, ) self.report({'INFO'}, f"New Publish {self.publish_types} created at: {new_filepath}") return {'FINISHED'} class ASSETPIPE_OT_publish_staged_as_active(bpy.types.Operator): bl_idname = "assetpipe.publish_staged_as_active" bl_label = "Publish Staged to Active" bl_description = """Create a new Published Version in the Publish Area""" @classmethod def poll(cls, context: bpy.types.Context) -> bool: if bpy.data.is_dirty: cls.poll_message_set( "Save the current file and/or Pull from last publish before creating new Publish" ) return False if not publish.is_staged_publish(Path(bpy.data.filepath)): cls.poll_message_set("No File is currently staged") return False return True def invoke(self, context: bpy.types.Context, event: bpy.types.Event): return context.window_manager.invoke_props_dialog(self, width=400) def draw(self, context: bpy.types.Context): layout = self.layout layout.alert = True layout.label( text="Delete the current staged file and replace with an active publish.", icon="ERROR", ) def execute(self, context: bpy.types.Context): current_file = Path(bpy.data.filepath) staged_file = publish.find_latest_publish( current_file, publish_type=constants.STAGED_PUBLISH_KEY ) # Delete Staged File staged_file.unlink() catalog_id = get_asset_id(context.scene.asset_pipeline.asset_catalog_name) publish.create_next_published_file(current_file=current_file, catalog_id=catalog_id) return {'FINISHED'} class ASSETPIPE_OT_reset_ownership(bpy.types.Operator): bl_idname = "assetpipe.reset_ownership" bl_label = "Reset Ownership" bl_description = """Reset the Object owner and Transferable Data on selected object(s)""" bl_options = {'REGISTER', 'UNDO'} @classmethod def poll(cls, context: bpy.types.Context) -> bool: if len(context.selected_objects) == 0: cls.poll_message_set("No Objects Selected") return False return True def execute(self, context: bpy.types.Context): objs = context.selected_objects for obj in objs: obj.asset_id_owner = "NONE" obj.transfer_data_ownership.clear() self.report( {'INFO'}, f"'{obj.name}' ownership data cleared ", ) return {'FINISHED'} class ASSETPIPE_OT_update_local_task_layers(bpy.types.Operator): bl_idname = "assetpipe.update_local_task_layers" bl_label = "Update Local Task Layers" bl_description = """Change the Task Layers that are Local to your file""" bl_options = {'REGISTER', 'UNDO'} @classmethod def poll(cls, context: bpy.types.Context) -> bool: asset_pipe = context.scene.asset_pipeline new_local_tl = [tl.name for tl in asset_pipe.all_task_layers if tl.is_local == True] local_tl = [tl.name for tl in asset_pipe.local_task_layers] if new_local_tl == local_tl: cls.poll_message_set("Local Task Layers already match current selection") return False return True def invoke(self, context: bpy.types.Context, event: bpy.types.Event): return context.window_manager.invoke_props_dialog(self, width=400) def draw(self, context: bpy.types.Context): layout = self.layout layout.alert = True layout.label( text="Caution, this only affects current file.", icon="ERROR", ) layout.label(text="Two files owning the same task layer can break merge process.") def execute(self, context: bpy.types.Context): asset_pipe = context.scene.asset_pipeline all_task_layers = asset_pipe.all_task_layers local_tl = [tl.name for tl in all_task_layers if tl.is_local == True] asset_pipe.set_local_task_layers(local_tl) return {'FINISHED'} class ASSETPIPE_OT_revert_file(bpy.types.Operator): bl_idname = "assetpipe.revert_file" bl_label = "Revert File" bl_description = """Revert File to Pre-Sync State. Revert will not affect Published files""" _temp_file = "" _source_file = "" def execute(self, context: bpy.types.Context): asset_pipe = context.scene.asset_pipeline self._temp_file = asset_pipe.temp_file self._source_file = asset_pipe.source_file if not Path(self._temp_file).exists(): self.report( {'ERROR'}, "Revert failed; no file found", ) return {'CANCELLED'} bpy.ops.wm.open_mainfile(filepath=self._temp_file) bpy.ops.wm.save_as_mainfile(filepath=self._source_file) return {'FINISHED'} class ASSETPIPE_OT_fix_prefixes(bpy.types.Operator): bl_idname = "assetpipe.fix_prefixes" bl_label = "Fix Modifier Prefixes" bl_description = """Fix Prefixes for Modifiers so they match Transferable Data Owner on selected object(s)""" bl_options = {'REGISTER', 'UNDO'} _updated_prefix = False @classmethod def poll(cls, context: bpy.types.Context) -> bool: if len(context.selected_objects) == 0: cls.poll_message_set("No Objects Selected") return False return True def execute(self, context: bpy.types.Context): objs = context.selected_objects asset_pipe = context.scene.asset_pipeline for obj in objs: transfer_data_items = obj.transfer_data_ownership for transfer_data_item in transfer_data_items: if transfer_data_item.type != 'MODIFIER': continue modifier = obj.modifiers.get(transfer_data_item.name) if not modifier: continue owner = task_layer.get_transfer_data_owner(asset_pipe, transfer_data_item.type) if not owner: continue prefixed = naming.task_layer_prefix_name_get(modifier.name, owner[0]) if prefixed == modifier.name: continue transfer_data_item.name = modifier.name = prefixed self.report( {'INFO'}, f"Renamed {transfer_data_item.name} on '{obj.name}'", ) self._updated_prefix = True if not self._updated_prefix: self.report( {'WARNING'}, f"No Prefixes found to update", ) return {'FINISHED'} class ASSETPIPE_OT_update_surrendered_object(bpy.types.Operator): bl_idname = "assetpipe.update_surrendered_object" bl_label = "Claim Surrendered" bl_description = """Claim Surrended Object Owner""" bl_options = {'REGISTER', 'UNDO'} def invoke(self, context: bpy.types.Context, event: bpy.types.Event): obj = context.active_object self._old_owner = obj.asset_id_owner # Set Asset ID Owner to a local ID obj.asset_id_owner = context.scene.asset_pipeline.get_local_task_layers()[0] return context.window_manager.invoke_props_dialog(self, width=400) def draw(self, context: bpy.types.Context): layout = self.layout row = layout.row() task_layer.draw_task_layer_selection( context, layout=row, data=context.active_object, ) def execute(self, context: bpy.types.Context): obj = context.active_object if obj.asset_id_owner == self._old_owner: self.report( {'ERROR'}, f"Object Owner was not updated", ) return {'CANCELLED'} obj.asset_id_surrender = False return {'FINISHED'} class ASSETPIPE_OT_update_surrendered_transfer_data(bpy.types.Operator): bl_idname = "assetpipe.update_surrendered_transfer_data" bl_label = "Claim Surrendered" bl_description = """Claim Surrended Transferable Data Owner""" bl_options = {'REGISTER', 'UNDO'} transfer_data_item_name: bpy.props.StringProperty(name="Transferable Data Item Name") _surrendered_transfer_data = None _old_owner = "" def invoke(self, context: bpy.types.Context, event: bpy.types.Event): obj = context.active_object for transfer_data_item in obj.transfer_data_ownership: if transfer_data_item.name == self.transfer_data_item_name: self._surrendered_transfer_data = transfer_data_item self._old_owner = self._surrendered_transfer_data.owner # Set Default Owner asset_pipe = context.scene.asset_pipeline owner, _ = task_layer.get_transfer_data_owner( asset_pipe, self._surrendered_transfer_data.type, self._surrendered_transfer_data.name ) self._surrendered_transfer_data.owner = owner return context.window_manager.invoke_props_dialog(self, width=400) def draw(self, context: bpy.types.Context): layout = self.layout row = layout.row() task_layer.draw_task_layer_selection( context, layout=row, data=self._surrendered_transfer_data, ) def execute(self, context: bpy.types.Context): asset_pipe = context.scene.asset_pipeline if self._surrendered_transfer_data.owner == self._old_owner: self.report( {'ERROR'}, f"Transferable Data Owner was not updated", ) return {'CANCELLED'} self._surrendered_transfer_data.surrender = False task_layer.get_transfer_data_owner(asset_pipe, self._surrendered_transfer_data.type) return {'FINISHED'} class ASSETPIPE_OT_batch_ownership_change(bpy.types.Operator): # TODO Update Operator Documentation bl_idname = "assetpipe.batch_ownership_change" bl_label = "Batch Set Ownership" bl_description = """Re-Assign Ownership in a batch operation""" bl_options = {'REGISTER', 'UNDO'} name_filter: bpy.props.StringProperty( name="Filter by Name", description="Filter Object or Transferable Data items by name", default="", ) data_source: bpy.props.EnumProperty( name="Objects", items=( ('SELECT', "Selected", "Update Selected Objects Only"), ('ALL', "All", "Update All Objects"), ), ) data_type: bpy.props.EnumProperty( name="Ownership Type", items=( ( 'OBJECT', "Object", "Update Owner of Objects", ), ( 'TRANSFER_DATA', "Transferable Data", "Update Owner of Transferable Data within Objects", ), ), ) filter_owners: bpy.props.EnumProperty( name="Owner Filter", items=( ('LOCAL', "If Locally Owned", "Only data that is owned locally"), ('OWNED', "If Owned By Any", "Only data that already have assignment"), ('ALL', "No Filter", "Set Ownership on any data, even without an owner"), ), ) avaliable_owners: bpy.props.EnumProperty( name="Avaliable Owners", items=( ('LOCAL', "Local Task Layers", "Only show local task layers as options"), ( 'ALL', "All Task Layers", "Show all task layers as options", ), ), ) transfer_data_type: bpy.props.EnumProperty( name="Type Filter", items=constants.TRANSFER_DATA_TYPES_ENUM_ITEMS ) owner_selection: bpy.props.StringProperty(name="Set Owner") def update_set_surrender(self, context): if self.set_surrender: self.claim_surrender = False set_surrender: bpy.props.BoolProperty( name="Set Surrender", default=False, update=update_set_surrender ) def update_claim_surrender(self, context): if self.claim_surrender: self.set_surrender = False claim_surrender: bpy.props.BoolProperty( name="Claim Surrender", default=False, update=update_claim_surrender ) def _filter_by_name(self, context, unfiltered_list: []): if self.name_filter == "": return unfiltered_list return [item for item in unfiltered_list if self.name_filter in item.name] def _get_transfer_data_to_update(self, context): asset_pipe = context.scene.asset_pipeline objs = self._get_objects(context) transfer_data_items_to_update = [] if self.data_type == "TRANSFER_DATA": for obj in objs: filtered_transfer_data = self._filter_by_name(context, obj.transfer_data_ownership) for transfer_data_item in filtered_transfer_data: if self.transfer_data_type != "NONE": if transfer_data_item.type == self.transfer_data_type: transfer_data_items_to_update.append(transfer_data_item) else: transfer_data_items_to_update.append(transfer_data_item) if self.claim_surrender: return [ item for item in transfer_data_items_to_update if item.surrender and item.owner not in asset_pipe.get_local_task_layers() ] if self.filter_owners == "LOCAL": transfer_data_items_to_update = [ item for item in transfer_data_items_to_update if item.owner in asset_pipe.get_local_task_layers() ] if self.set_surrender: return [item for item in transfer_data_items_to_update if not item.surrender] return transfer_data_items_to_update def _get_objects(self, context): asset_objs = context.scene.asset_pipeline.asset_collection.all_objects selected_asset_objs = [obj for obj in asset_objs if obj in context.selected_objects] return asset_objs if self.data_source == "ALL" else selected_asset_objs def _get_filtered_objects(self, context): asset_pipe = context.scene.asset_pipeline objs = self._get_objects(context) filtered_objs = self._filter_by_name(context, objs) if self.filter_owners == "LOCAL" and self.data_type == "OBJECT": filtered_objs = [ item for item in filtered_objs if item.asset_id_owner in asset_pipe.get_local_task_layers() ] if self.filter_owners == "OWNED" and self.data_type == "OBJECT": filtered_objs = [item for item in filtered_objs if item.asset_id_owner != "NONE"] if self.claim_surrender: claim_objs = self._get_objects(context) claim_filtered_objs = self._filter_by_name(context, claim_objs) return [ item for item in claim_filtered_objs if item.asset_id_surrender and item.asset_id_owner not in asset_pipe.get_local_task_layers() ] if self.set_surrender: return [ item for item in filtered_objs if not item.asset_id_surrender and item.asset_id_owner in asset_pipe.get_local_task_layers() ] return filtered_objs def _get_message(self, context) -> str: objs = self._get_filtered_objects(context) if self.data_type == "OBJECT": data_type_name = "Object(s)" length = len(objs) if objs else 0 else: transfer_data_items_to_update = self._get_transfer_data_to_update(context) data_type_name = "Transferable Data Item(s)" length = len(transfer_data_items_to_update) if transfer_data_items_to_update else 0 if self.claim_surrender: action = "Claim Surrendered on" if self.set_surrender: action = "Set Surrender on" if not (self.claim_surrender or self.set_surrender): action = "Change Ownership on" return f"{action} {length} {data_type_name}" def invoke(self, context: bpy.types.Context, event: bpy.types.Event): if not get_addon_prefs().is_advanced_mode: self.filter_owners = 'LOCAL' self.avaliable_owners = 'LOCAL' return context.window_manager.invoke_props_dialog(self, width=500) def draw(self, context: bpy.types.Context): prefs = get_addon_prefs() advanced_mode = prefs.is_advanced_mode grey_out = True if self.set_surrender: grey_out = False self.filter_owners = "LOCAL" layout = self.layout layout.use_property_split = True layout.row(align=True).prop(self, "data_source", expand=True) layout.prop(self, "data_type", expand=True) filter_owner_row = layout.row() filter_owner_row.enabled = grey_out if advanced_mode: filter_owner_row.prop(self, "filter_owners") if self.data_type == "TRANSFER_DATA": layout.prop(self, "transfer_data_type") layout.prop(self, "name_filter", text="Name Filter") layout.separator() owner_row = layout.row(align=True) owner_row.enabled = grey_out task_layer.draw_task_layer_selection( context, layout=owner_row, data=self, data_owner_name='owner_selection', current_data_owner=self.owner_selection, show_all_task_layers=self.avaliable_owners=='ALL', text="Set To", ) if advanced_mode: owner_row.prop(self, "avaliable_owners", text="") row = layout.row(align=True) row.prop(self, 'set_surrender', toggle=True) row.prop(self, 'claim_surrender', toggle=True) bottom_label = layout.row() bottom_label_split = bottom_label.split(factor=0.4) bottom_label_split.row() bottom_label_split.label(text=self._get_message(context)) def execute(self, context: bpy.types.Context): asset_pipe = context.scene.asset_pipeline objs = self._get_filtered_objects(context) message = self._get_message(context) # Only check for owner selection not surrendering data. if not self.set_surrender: if self.owner_selection == "": self.report( {'ERROR'}, "Ownership 'Set To' must be set to a task layer", ) return {'CANCELLED'} if self.data_type == "OBJECT": for obj in objs: if self.claim_surrender: obj.asset_id_surrender = False if self.set_surrender: obj.asset_id_surrender = True continue obj.asset_id_owner = self.owner_selection else: transfer_data_items_to_update = self._get_transfer_data_to_update(context) for transfer_data_item_to_update in transfer_data_items_to_update: if self.claim_surrender: transfer_data_item_to_update.surrender = False if self.set_surrender: transfer_data_item_to_update.surrender = True continue transfer_data_item_to_update.owner = self.owner_selection task_layer.get_transfer_data_owner(asset_pipe, transfer_data_item_to_update.type) self.report({'INFO'}, message) return {'FINISHED'} class ASSETPIPE_OT_refresh_asset_cat(bpy.types.Operator): bl_idname = "assetpipe.refresh_asset_cat" bl_label = "Refresh Asset Catalogs" bl_description = """Refresh Asset Catalogs""" def execute(self, context: bpy.types.Context): get_asset_catalog_items(reload=True) verify_task_layer_json_data() self.report({'INFO'}, "Asset Catalogs Refreshed!") return {'FINISHED'} class ASSETPIPE_OT_save_asset_hook(bpy.types.Operator): bl_idname = "assetpipe.save_production_hook" bl_label = "Save Production Hook" bl_description = """Save new hook file based on example file. Production hooks are used across all assets. Asset hooks are only used in the current asset. - Production hooks: 'svn/pro/config' directory. - Asset hooks are stored at the root of the asset's directory'""" mode: bpy.props.EnumProperty( name="Hooks Mode", description="Choose to either save production level or asset level hooks", items=[ ('PROD', 'Production', 'Save Prododuction Level Hooks'), ('ASSET', 'Asset', 'Save Asset Level Hooks'), ], ) def execute(self, context: bpy.types.Context): if self.mode == 'PROD': example_hooks_dir = ( Path(__file__).parent.joinpath("hook_examples").joinpath('prod_hooks.py') ) save_hook_path = get_production_hook_dir().joinpath('hooks.py').resolve() else: # if self.mode == 'ASSET' example_hooks_dir = ( Path(__file__).parent.joinpath("hook_examples").joinpath('asset_hooks.py') ) save_hook_path = get_asset_hook_dir().joinpath('hooks.py').resolve() if not example_hooks_dir.exists(): self.report( {'ERROR'}, "Cannot find example hook file", ) return {'CANCELLED'} if save_hook_path.exists(): self.report( {'ERROR'}, f"Cannot overwrite existing hook file at '{save_hook_path.__str__()}'", ) return {'CANCELLED'} with example_hooks_dir.open() as source: contents = source.read() # Write contents to target file with save_hook_path.open('w') as target: target.write(contents) self.report({'INFO'}, f"Hook File saved to {save_hook_path.__str__()}") return {'FINISHED'} classes = [ ASSETPIPE_OT_prepare_sync, ASSETPIPE_OT_sync_push, ASSETPIPE_OT_sync_pull, ASSETPIPE_OT_publish_new_version, ASSETPIPE_OT_publish_staged_as_active, ASSETPIPE_OT_create_new_asset, ASSETPIPE_OT_reset_ownership, ASSETPIPE_OT_update_local_task_layers, ASSETPIPE_OT_revert_file, ASSETPIPE_OT_fix_prefixes, ASSETPIPE_OT_update_surrendered_object, ASSETPIPE_OT_update_surrendered_transfer_data, ASSETPIPE_OT_batch_ownership_change, ASSETPIPE_OT_refresh_asset_cat, ASSETPIPE_OT_save_asset_hook, ASSETPIPE_OT_open_publish, ASSETPIPE_OT_open_file, ] def register(): for i in classes: bpy.utils.register_class(i) def unregister(): for i in classes: bpy.utils.unregister_class(i)