generated from ynput/ayon-addon-template
-
Notifications
You must be signed in to change notification settings - Fork 10
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #36 from ynput/feature/AY-5417_Houdini-workfile-te…
…mplates Support Houdini workfile templates
- Loading branch information
Showing
8 changed files
with
508 additions
and
2 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,294 @@ | ||
import os | ||
|
||
import hou | ||
|
||
from ayon_core.lib import ( | ||
StringTemplate, | ||
filter_profiles, | ||
) | ||
from ayon_core.pipeline import registered_host, Anatomy | ||
from ayon_core.pipeline.workfile.workfile_template_builder import ( | ||
AbstractTemplateBuilder, | ||
PlaceholderPlugin, | ||
TemplateProfileNotFound, | ||
TemplateLoadFailed, | ||
TemplateNotFound | ||
) | ||
from ayon_core.tools.workfile_template_build import ( | ||
WorkfileBuildPlaceholderDialog, | ||
) | ||
from ayon_core.tools.utils import show_message_dialog | ||
|
||
from .lib import ( | ||
imprint, | ||
lsattr, | ||
get_main_window | ||
) | ||
from .plugin import HoudiniCreator | ||
|
||
|
||
class HoudiniTemplateBuilder(AbstractTemplateBuilder): | ||
"""Concrete implementation of AbstractTemplateBuilder for Houdini""" | ||
|
||
def get_template_preset(self): | ||
"""Unified way how template preset is received using settings. | ||
Method is dependent on '_get_build_profiles' which should return filter | ||
profiles to resolve path to a template. Default implementation looks | ||
into host settings: | ||
- 'project_settings/{host name}/templated_workfile_build/profiles' | ||
Returns: | ||
dict: Dictionary with `path`, `keep_placeholder` and | ||
`create_first_version` settings from the template preset | ||
for current context. | ||
Raises: | ||
TemplateProfileNotFound: When profiles are not filled. | ||
TemplateLoadFailed: Profile was found but path is not set. | ||
TemplateNotFound: Path was set but file does not exist. | ||
""" | ||
|
||
host_name = self.host_name | ||
project_name = self.project_name | ||
task_name = self.current_task_name | ||
task_type = self.current_task_type | ||
|
||
build_profiles = self._get_build_profiles() | ||
profile = filter_profiles( | ||
build_profiles, | ||
{ | ||
"task_types": task_type, | ||
"task_names": task_name | ||
} | ||
) | ||
|
||
if not profile: | ||
raise TemplateProfileNotFound(( | ||
"No matching profile found for task '{}' of type '{}' " | ||
"with host '{}'" | ||
).format(task_name, task_type, host_name)) | ||
|
||
path = profile["path"] | ||
|
||
# switch to remove placeholders after they are used | ||
keep_placeholder = profile.get("keep_placeholder") | ||
create_first_version = profile.get("create_first_version") | ||
|
||
# backward compatibility, since default is True | ||
if keep_placeholder is None: | ||
keep_placeholder = True | ||
|
||
if not path: | ||
raise TemplateLoadFailed(( | ||
"Template path is not set.\n" | ||
"Path need to be set in {}\\Template Workfile Build " | ||
"Settings\\Profiles" | ||
).format(host_name.title())) | ||
|
||
# Try fill path with environments and anatomy roots | ||
anatomy = Anatomy(project_name) | ||
fill_data = { | ||
key: value | ||
for key, value in os.environ.items() | ||
} | ||
|
||
fill_data["root"] = anatomy.roots | ||
fill_data["project"] = { | ||
"name": project_name, | ||
"code": anatomy.project_code, | ||
} | ||
|
||
result = StringTemplate.format_template(path, fill_data) | ||
if result.solved: | ||
path = result.normalized() | ||
|
||
# I copied the whole thing because I wanted to add some | ||
# Houdini specific code here | ||
path = hou.text.expandString(path) | ||
|
||
if path and os.path.exists(path): | ||
self.log.info("Found template at: '{}'".format(path)) | ||
return { | ||
"path": path, | ||
"keep_placeholder": keep_placeholder, | ||
"create_first_version": create_first_version | ||
} | ||
|
||
solved_path = None | ||
while True: | ||
try: | ||
solved_path = anatomy.path_remapper(path) | ||
except KeyError as missing_key: | ||
raise KeyError( | ||
"Could not solve key '{}' in template path '{}'".format( | ||
missing_key, path)) | ||
|
||
if solved_path is None: | ||
solved_path = path | ||
if solved_path == path: | ||
break | ||
path = solved_path | ||
|
||
solved_path = os.path.normpath(solved_path) | ||
if not os.path.exists(solved_path): | ||
raise TemplateNotFound( | ||
"Template found in AYON settings for task '{}' with host " | ||
"'{}' does not exists. (Not found : {})".format( | ||
task_name, host_name, solved_path)) | ||
|
||
self.log.info("Found template at: '{}'".format(solved_path)) | ||
|
||
return { | ||
"path": solved_path, | ||
"keep_placeholder": keep_placeholder, | ||
"create_first_version": create_first_version | ||
} | ||
|
||
def import_template(self, path): | ||
"""Import template into current scene. | ||
Block if a template is already loaded. | ||
Args: | ||
path (str): A path to current template (usually given by | ||
get_template_preset implementation) | ||
Returns: | ||
bool: Whether the template was successfully imported or not | ||
""" | ||
|
||
# TODO Check if template is already imported | ||
|
||
# Merge (Load) template workfile in the current scene. | ||
try: | ||
hou.hipFile.merge(path, ignore_load_warnings=True) | ||
return True | ||
except hou.OperationFailed: | ||
return False | ||
|
||
|
||
class HoudiniPlaceholderPlugin(PlaceholderPlugin): | ||
"""Base Placeholder Plugin for Houdini with one unified cache. | ||
Inherited classes must still implement `populate_placeholder` | ||
""" | ||
|
||
def get_placeholder_node_name(self, placeholder_data): | ||
return self.identifier.replace(".", "_") | ||
|
||
def create_placeholder_node(self, node_name=None): | ||
"""Create node to be used as placeholder. | ||
By default, it creates a null node in '/out'. | ||
Feel free to override it in different workfile build plugins. | ||
""" | ||
|
||
node = hou.node("/out").createNode("null", node_name) | ||
node.moveToGoodPosition() | ||
parms = node.parmTemplateGroup() | ||
for parm in {"execute", "renderdialog"}: | ||
p = parms.find(parm) | ||
p.hide(True) | ||
parms.replace(parm, p) | ||
node.setParmTemplateGroup(parms) | ||
return node | ||
|
||
def create_placeholder(self, placeholder_data): | ||
|
||
node_name = self.get_placeholder_node_name(placeholder_data) | ||
|
||
placeholder_node = self.create_placeholder_node(node_name) | ||
HoudiniCreator.customize_node_look(placeholder_node) | ||
|
||
placeholder_data["plugin_identifier"] = self.identifier | ||
|
||
imprint(placeholder_node, placeholder_data) | ||
|
||
def collect_scene_placeholders(self): | ||
# Read the cache by identifier | ||
placeholder_nodes = self.builder.get_shared_populate_data( | ||
self.identifier | ||
) | ||
if placeholder_nodes is None: | ||
placeholder_nodes = [] | ||
|
||
nodes = lsattr("plugin_identifier", self.identifier) | ||
|
||
for node in nodes: | ||
placeholder_nodes.append(node) | ||
|
||
# Set the cache by identifier | ||
self.builder.set_shared_populate_data( | ||
self.identifier, placeholder_nodes | ||
) | ||
|
||
return placeholder_nodes | ||
|
||
def update_placeholder(self, placeholder_item, placeholder_data): | ||
placeholder_node = hou.node(placeholder_item.scene_identifier) | ||
imprint(placeholder_node, placeholder_data, update=True) | ||
|
||
# Update node name | ||
node_name = self.get_placeholder_node_name(placeholder_data) | ||
placeholder_node.setName(node_name, unique_name=True) | ||
|
||
def delete_placeholder(self, placeholder): | ||
placeholder_node = hou.node(placeholder.scene_identifier) | ||
placeholder_node.destroy() | ||
|
||
|
||
def build_workfile_template(*args): | ||
# NOTE Should we inform users that they'll lose unsaved changes ? | ||
builder = HoudiniTemplateBuilder(registered_host()) | ||
builder.build_template() | ||
|
||
|
||
def update_workfile_template(*args): | ||
builder = HoudiniTemplateBuilder(registered_host()) | ||
builder.rebuild_template() | ||
|
||
|
||
def create_placeholder(*args): | ||
host = registered_host() | ||
builder = HoudiniTemplateBuilder(host) | ||
window = WorkfileBuildPlaceholderDialog(host, builder, | ||
parent=get_main_window()) | ||
window.show() | ||
|
||
|
||
def update_placeholder(*args): | ||
host = registered_host() | ||
builder = HoudiniTemplateBuilder(host) | ||
placeholder_items_by_id = { | ||
placeholder_item.scene_identifier: placeholder_item | ||
for placeholder_item in builder.get_placeholders() | ||
} | ||
placeholder_items = [] | ||
for node in hou.selectedNodes(): | ||
if node.path() in placeholder_items_by_id: | ||
placeholder_items.append(placeholder_items_by_id[node.path()]) | ||
|
||
if len(placeholder_items) == 0: | ||
show_message_dialog( | ||
"Workfile Placeholder Manager", | ||
"Please select a placeholder node.", | ||
"warning", | ||
get_main_window() | ||
) | ||
return | ||
|
||
if len(placeholder_items) > 1: | ||
show_message_dialog( | ||
"Workfile Placeholder Manager", | ||
"Too many selected placeholder nodes.\n" | ||
"Please, Select one placeholder node.", | ||
"warning", | ||
get_main_window() | ||
) | ||
return | ||
|
||
placeholder_item = placeholder_items[0] | ||
window = WorkfileBuildPlaceholderDialog(host, builder, | ||
parent=get_main_window()) | ||
window.set_update_mode(placeholder_item) | ||
window.exec_() |
56 changes: 56 additions & 0 deletions
56
client/ayon_houdini/plugins/workfile_build/create_placeholder.py
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,56 @@ | ||
from ayon_core.pipeline.workfile.workfile_template_builder import ( | ||
CreatePlaceholderItem, | ||
PlaceholderCreateMixin, | ||
) | ||
from ayon_core.pipeline import registered_host | ||
from ayon_core.pipeline.create import CreateContext | ||
|
||
from ayon_houdini.api.workfile_template_builder import ( | ||
HoudiniPlaceholderPlugin | ||
) | ||
from ayon_houdini.api.lib import read | ||
|
||
|
||
class HoudiniPlaceholderCreatePlugin( | ||
HoudiniPlaceholderPlugin, PlaceholderCreateMixin | ||
): | ||
"""Workfile template plugin to create "create placeholders". | ||
"create placeholders" will be replaced by publish instances. | ||
TODO: | ||
Support imprint & read precreate data to instances. | ||
""" | ||
|
||
identifier = "ayon.create.placeholder" | ||
label = "Houdini Create" | ||
|
||
def populate_placeholder(self, placeholder): | ||
self.populate_create_placeholder(placeholder) | ||
|
||
def repopulate_placeholder(self, placeholder): | ||
self.populate_create_placeholder(placeholder) | ||
|
||
def get_placeholder_options(self, options=None): | ||
return self.get_create_plugin_options(options) | ||
|
||
def get_placeholder_node_name(self, placeholder_data): | ||
create_context = CreateContext(registered_host()) | ||
creator = create_context.creators.get(placeholder_data["creator"]) | ||
product_type = creator.product_type | ||
node_name = "{}_{}".format(self.identifier.replace(".", "_"), product_type) | ||
|
||
return node_name | ||
|
||
def collect_placeholders(self): | ||
output = [] | ||
create_placeholders = self.collect_scene_placeholders() | ||
|
||
for node in create_placeholders: | ||
placeholder_data = read(node) | ||
output.append( | ||
CreatePlaceholderItem(node.path(), placeholder_data, self) | ||
) | ||
|
||
return output | ||
|
Oops, something went wrong.