1136 lines
41 KiB
Python
1136 lines
41 KiB
Python
# 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)
|